パドック画像から距離適性を推測する(転移学習編)

果たして転移学習で改善するのか?! やってみました! ((p・ω・q))

chainerで転移学習をするにあたって以下の記事を参考にしました。

転移学習でハマってかつ学習結果も惨敗… でしたがchainerで転移学習する方法の勉強になりました。。

ハマった点

  1. 学習済みAlexnetのパラメータがcopyされない

    • 対策: L.Classifierのモデル(model)でなく model.predictor を copy_model の引数として渡す
    • 対策: モデルを定義するときに 入力次元として None を使わない
    • chainerのAlexnetを用いてFine Tuningをする | TOMMY NOTES 」の記事の中の copy_model 関数はインデントずれがあってそのままコピペするとうまく動作しません…

    chainerでは入力次元を None としたときは前方向への伝搬(Forward propagation)を計算する中で動的に次元を計算するので計算をしてない状態では次元が不定となり、copy_modelの中での次元比較の際ミスマッチとなりコピーされません。

    あと、参考にした記事ではcopy_model(original_model, model)でコピーできてそうなのですが私のコードではcopy_model(original_model, model.predictor)としないとコピーできませんでした。。

  2. FC層だけ学習させるときにEvaluatorでエラーがでる

    • 対策: hook関数を使って重みを更新しないレイヤの勾配を削除する

    最初に https://github.com/chainer/chainer/issues/724https://groups.google.com/forum/#!searchin/chainer/Finetuning/chainer/H4IWqcMBA2w/8cxt58YrBwAJ でやられている volatileフラグを制御する方法でやってたのですが、これだとEvaluatorの実行時に ValueError: ON and OFF flags cannot be mixed. というエラーが出てしまいました…。(Evaluatorをオフにするとエラーは出ずに学習できました)

    最終的に、Evaluatorでもエラーを出さずにうまくいった方法は「Chainerでfine-tuningを行う - Qiita 」にあった hook関数を使って勾配をリセットするというものでした。ちなみに、この方法はすべてのレイヤで勾配を一度計算することになるので計算時間は短縮されません。。(volatileを使うと勾配は計算しなくなるので計算時間が短くなります)

    あと、chainer v2から導入された chainer.no_backprop_mode()のスコープを使うとうまくいきそうです。v1.x.xでもこのスコープは(なぜか)使えてしまうのですがただ重みは全レイヤで更新されてしまいここで時間を食ってしまいました..。以下のMNISTでは chainer v2では特定のレイヤだけ重み更新できることを確認しています。


MNISTで特定のレイヤだけ重みを更新するサンプル github.com

Alexnetを転移学習した結果

誤差は以下のようになりました。

f:id:sanshonoki:20170626044354p:plain

学習データの誤差が減らずうまく学習できないことが伺えます…。[´゚Д゚`]

最後は発散してどんな入力に対しても同じ出力値(4.2 = おおよその中央値)が出るようになってしまいました…。 通常の学習の検証用データでの誤差が同じ値を出し続ける場合の誤差よりも大きいということで何とも切ないです.. ( ;∀;)

ちなみにFC層だけ学習させるのではなく、全階層で学習させたら通常と同等の誤差まで減っていきました。

なので、Kerasで学ぶ転移学習 で書かれている

データが少ない・似ていない これは転移学習が困難なパターンです。データが少ないので過学習を防ぐために上層だけを学習させたいところですが、似ていないデータを使って学習しているため、上層の特徴を使ってもうまく学習できないと考えられます。

というパターンなのでしょう。。

一般的な物体認識問題とはかなり異なる問題設定だったということでしょう、そりゃあそうですよねという気はしますが残念です。。でも、勉強になりました。

最後に、誰かの役に立つかもしれないので転移学習に関係する箇所のコードをのせておきます。

– train_ft.py –

class DelGradient(object):
    name = 'DelGradient'

    def __init__(self, delTgt):
        self.delTgt = delTgt

    def __call__(self, opt):
        for name, param in opt.target.namedparams():
            for d in self.delTgt:
                if d in name:
                    grad = param.grad
                    with chainer.cuda.get_device(grad):
                        grad *= 0

def copy_model(src, dst):
    assert isinstance(src, chainer.Chain)
    assert isinstance(dst, chainer.Chain)
    for child in src.children():
        if child.name not in dst.__dict__: continue
        dst_child = dst[child.name]
        if type(child) != type(dst_child): continue
        if isinstance(child, chainer.Chain):
            copy_model(child, dst_child)
        if isinstance(child, chainer.Link):
            match = True
            for a, b in zip(child.namedparams(), dst_child.namedparams()):
                if a[0] != b[0]:
                    match = False
                    break
                if a[1].data.shape != b[1].data.shape:
                    match = False
                    break
            if not match:
                print('Ignore %s because of parameter mismatch' % child.name)
                continue
            for a, b in zip(child.namedparams(), dst_child.namedparams()):
                b[1].data = a[1].data
            print('Copy %s' % child.name)

def main():
    ...

    model = L.Classifier(alexnet.FromCaffeAlexnet(1), lossfun=F.mean_squared_err
or)

    original_model = pickle.load(open('alexnet.pkl', 'rb'))
    copy_model(original_model, model.predictor)
    model.compute_accuracy = False

    ...

    optimizer.add_hook(DelGradient(["conv1", "conv2", "conv3", "conv4", "conv5"]))

    ...

– alexnet.py –

class FromCaffeAlexnet(chainer.Chain):
    insize = 128
    def __init__(self, n_out):
        super(FromCaffeAlexnet, self).__init__(
            # conv1=L.Convolution2D(None, 96, 11, stride=2),
            # conv2=L.Convolution2D(None, 256, 5, pad=2),
            # conv3=L.Convolution2D(None, 384, 3, pad=1),
            # conv4=L.Convolution2D(None, 384, 3, pad=1),
            # conv5=L.Convolution2D(None, 256, 3, pad=1),
            # my_fc6=L.Linear(None, 4096),
            # my_fc7=L.Linear(None, 1024),
            # my_fc8=L.Linear(None, n_out),

            # Don't use None when you copy parameters
            conv1=L.Convolution2D(3, 96, 11, stride=2),
            conv2=L.Convolution2D(96, 256, 5, pad=2),
            conv3=L.Convolution2D(256, 384, 3, pad=1),
            conv4=L.Convolution2D(384, 384, 3, pad=1),
            conv5=L.Convolution2D(384, 256, 3, pad=1),
            # my_fc6=L.Linear(None, 4096),
            # my_fc7=L.Linear(None, 1024),
            # my_fc8=L.Linear(None, n_out),
            my_fc6=L.Linear(256 * 7 * 7, 4096),
            my_fc7=L.Linear(4096, 1024),
            my_fc8=L.Linear(1024, n_out),
        )
        self.train = True
 
    def __call__(self, x):
        # for chainer v1.x.x 
        h = F.max_pooling_2d(F.local_response_normalization(
            F.relu(self.conv1(x))), 3, stride=2)
        h = F.max_pooling_2d(F.local_response_normalization(
            F.relu(self.conv2(h))), 3, stride=2)
        h = F.relu(self.conv3(h))
        h = F.relu(self.conv4(h))
        h = F.max_pooling_2d(F.relu(self.conv5(h)), 3, stride=2)
        h = F.dropout(F.relu(self.my_fc6(h)), train=self.train)
        h = F.dropout(F.relu(self.my_fc7(h)), train=self.train)
        h = self.my_fc8(h)

        # for chainer v2.x.x
        # You don't need to use DelGradient hook.

        # with chainer.no_backprop_mode():
        #     h = F.max_pooling_2d(F.local_response_normalization(
        #         F.relu(self.conv1(x))), 3, stride=2)
        #     h = F.max_pooling_2d(F.local_response_normalization(
        #         F.relu(self.conv2(h))), 3, stride=2)
        #     h = F.relu(self.conv3(h))
        #     h = F.relu(self.conv4(h))
        #     h = F.max_pooling_2d(F.relu(self.conv5(h)), 3, stride=2)
        #     with chainer.force_backprop_mode():
        #         h = F.dropout(F.relu(self.my_fc6(h)), train=self.train)
        #         h = F.dropout(F.relu(self.my_fc7(h)), train=self.train)
        #         h = self.my_fc8(h)

        return h

コード全体はこちらにあります。 github.com