果たして転移学習で改善するのか?! やってみました! ((p・ω・q))
chainerで転移学習をするにあたって以下の記事を参考にしました。
- Chainerでファインチューニングするときの個人的ベストプラクティス - Qiita
- chainerのAlexnetを用いてFine Tuningをする | TOMMY NOTES
- memo: Chainer によるシーン認識
転移学習でハマってかつ学習結果も惨敗… でしたがchainerで転移学習する方法の勉強になりました。。
ハマった点
学習済み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)
としないとコピーできませんでした。。FC層だけ学習させるときにEvaluatorでエラーがでる
- 対策: hook関数を使って重みを更新しないレイヤの勾配を削除する
最初に https://github.com/chainer/chainer/issues/724 やhttps://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を転移学習した結果
誤差は以下のようになりました。
学習データの誤差が減らずうまく学習できないことが伺えます…。[´゚Д゚`]
最後は発散してどんな入力に対しても同じ出力値(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