kumilog.net

データ分析やプログラミングの話などを書いています。

ChainerでFineTuning その2

Chainer Advent Calendar 2017 の 11日目 の記事です*1

Caffeモデルを利用したFineTuningを試したいと思います。ChainerでのFineTuningは、ChainerのLink関数であるVGG16Layers()なども用いることもできます。VGG16Layers()を用いる方法については以下にまとめています。

xkumiyu.hatenablog.com

Caffeモデルの利用

Caffeモデルのダウンロードと変換

今回もVGGのモデルを利用します。Caffeモデルは、VGG_ILSVRC_19_layers_deploy.prototxtのcaffemodel_urlのからダウンロードできます。ちなみに、このモデルは、VGG16Layers()で使われているものと同じものです。

変換には時間がかかるので、事前に実施しておき、npz形式で保存しておきます。

from chainer.links.caffe.caffe_function import CaffeFunction
from chainer.serializers import npz

caffemodel = CaffeFunction('VGG_ILSVRC_16_layers.caffemodel')
npz.save_npz('VGG_ILSVRC_16_layers.npz', caffemodel, compression=False)

ちなみに、ダウンロードに20分、変換に10分かかりました。。

モデルの定義

いつものようにChainを継承してモデルを定義します。

BaseVGGがCaffeモデルを利用するモデルで、BaseVGGを使ってオリジナルのモデルVGGを定義しています。fc6/fc7/fc8はCaffeモデルを利用しないモデルになっています。

npz.load_npz(pretrained_model, self.base)で、BaseVGGにCaffeモデルの重みを(初期化に)設定しています。

import chainer
import chainer.functions as F
import chainer.links as L
from chainer.serializers import npz


class VGG(chainer.Chain):

    def __init__(self, class_labels=100, pretrained_model='VGG_ILSVRC_16_layers.npz'):
        super(VGG, self).__init__()
        with self.init_scope():
            self.base = BaseVGG()
            self.fc6 = L.Linear(None, 512)
            self.fc7 = L.Linear(None, 512)
            self.fc8 = L.Linear(None, class_labels)
        npz.load_npz(pretrained_model, self.base)

    def __call__(self, x, t):
        h = self.predict(x)
        loss = F.softmax_cross_entropy(h, t)
        chainer.report({'loss': loss, 'accuracy': F.accuracy(h, t)}, self)
        return loss

    def predict(self, x):
        h = self.base(x)
        h = F.dropout(F.relu(self.fc6(h)))
        h = F.dropout(F.relu(self.fc7(h)))
        return self.fc8(h)


class BaseVGG(chainer.Chain):

    def __init__(self):
        super(BaseVGG, self).__init__()
        with self.init_scope():
            self.conv1_1 = L.Convolution2D(None, 64, 3, 1, 1)
            self.conv1_2 = L.Convolution2D(64, 64, 3, 1, 1)
            self.conv2_1 = L.Convolution2D(64, 128, 3, 1, 1)
            self.conv2_2 = L.Convolution2D(128, 128, 3, 1, 1)
            self.conv3_1 = L.Convolution2D(128, 256, 3, 1, 1)
            self.conv3_2 = L.Convolution2D(256, 256, 3, 1, 1)
            self.conv3_3 = L.Convolution2D(256, 256, 3, 1, 1)
            self.conv4_1 = L.Convolution2D(256, 512, 3, 1, 1)
            self.conv4_2 = L.Convolution2D(512, 512, 3, 1, 1)
            self.conv4_3 = L.Convolution2D(512, 512, 3, 1, 1)
            self.conv5_1 = L.Convolution2D(512, 512, 3, 1, 1)
            self.conv5_2 = L.Convolution2D(512, 512, 3, 1, 1)
            self.conv5_3 = L.Convolution2D(512, 512, 3, 1, 1)

    def __call__(self, x):
        h = F.relu(self.conv1_1(x))
        h = F.relu(self.conv1_2(h))
        h = F.max_pooling_2d(h, ksize=2, stride=2)

        h = F.relu(self.conv2_1(h))
        h = F.relu(self.conv2_2(h))
        h = F.max_pooling_2d(h, ksize=2, stride=2)

        h = F.relu(self.conv3_1(h))
        h = F.relu(self.conv3_2(h))
        h = F.relu(self.conv3_3(h))
        h = F.max_pooling_2d(h, ksize=2, stride=2)

        h = F.relu(self.conv4_1(h))
        h = F.relu(self.conv4_2(h))
        h = F.relu(self.conv4_3(h))
        h = F.max_pooling_2d(h, ksize=2, stride=2)

        h = F.relu(self.conv5_1(h))
        h = F.relu(self.conv5_2(h))
        h = F.relu(self.conv5_3(h))
        h = F.max_pooling_2d(h, ksize=2, stride=2)
        return h

Caffeモデルを利用するネットワーク構造は同じにする必要があります。また、インスタンス変数の名前も元のモデルの層の名前と同じにする必要があります。

ネットワーク構造や各層の名前は、VGG_ILSVRC_19_layers_deploy.prototxtを参照してください。

層の名前は、モデルファイルからも分かります。

 import numpy as np

model = np.load('VGG_ILSVRC_16_layers.npz')
print(model.keys())
['conv5_2/W', 'conv5_2/b', 'conv2_2/W', 'conv2_2/b', 'fc8/W', 'fc8/b', 'conv5_1/W', 'conv5_1/b', 'fc6/W', 'fc6/b', 'conv3_2/W', 'conv3_2/b', 'conv1_2/W', 'conv1_2/b', 'conv2_1/W', 'conv2_1/b', 'conv4_2/W', 'conv4_2/b', 'conv1_1/W', 'conv1_1/b', 'conv4_3/W', 'conv4_3/b', 'conv3_3/W', 'conv3_3/b', 'conv3_1/W', 'conv3_1/b', 'conv5_3/W', 'conv5_3/b', 'conv4_1/W', 'conv4_1/b', 'fc7/W', 'fc7/b']

重みの固定

モデル定義では、Caffeモデルの重みを初期化に設定しただけなので、固定します。

model = VGG()
optimizer = chainer.optimizers.MomentumSGD()
optimizer.setup(model)
model.base.disable_update()

重みの固定の詳細については、前回の記事も参照ください。

xkumiyu.hatenablog.com

まとめ

今回は、fc6/fc7/fc8の3層を一気に学習するFineTuningを行いましたが、出力層に近い順に重みの固定を解除していくとうまくいくと言われています。つまり、fc8以外の重みを固定して、fc8を学習する -> fc7/fc8以外の重みを固定して、fc7/fc8を学習するといったように実施します。

また、VGGを使ったFineTuningを行いましたが、実際にはVGGよりResNetやInceptionモデルを利用した方が、計算量が少なく、高い精度が期待できます。

f:id:xkumiyu:20171124165558p:plain
出展: [1605.07678] An Analysis of Deep Neural Network Models for Practical Applications

今回は、Chainerの機能を使って、Caffeモデルの利用をしましたが、異なるフレームワーク間で、学習したモデルをやり取りする仕組みが提唱されつつあります。有力なのが、FacebookMicrosoftが開発しているONNX(Open Neural Network Exchange) で、以前PyTorchとCaffe2で利用してみました。

xkumiyu.hatenablog.com

まだ、ChainerはONNXをサポートしていませんが、Chainer Advent Calendar 2017でもONNXの記事が投稿されるみたいなので、今後に期待したいところです。

また、今回使ったコードはGithubにあげています。

github.com

*1:本当はCapsNetの実装をやろうと思ったけど、すでにChainer版があったので没に...