Chainerで転移学習とファインチューニング（VGG16、ResNet、GoogLeNet）

画像系の深層学習では、学習済みモデルの重みを利用する「転移学習」や「ファインチューニング」と呼ばれる手法がよく利用されます。

* 転移学習: 学習済みのモデルから特徴量を抽出すること
* ファインチューニング: 学習済みモデルの重みを使って再学習させること

どちらも基本的には、ILSVRCなどの画像認識コンペで優秀な成績を収めたモデルのネットワークアーキテクチャを深層学習のライブラリで構築し、公開されている学習済みの重みファイルを読み込ませて利用するという流れで実装します。

Chainerでは、以下の画像認識モデルが、すでに内部で実装されています。

* VGG16
* ResNet50, ResNet101, ResNet152
* GoogLeNet

また、これらのモデルに学習済みの重みファイルを読み込ませるための便利な関数が一通り揃っていますので、それらの使い方についてまとめます。

Chainerのバージョン: `3.0.0`

# 転移学習で画像特徴量を抽出する

Chainerから呼び出せる学習済みモデルはいずれも、ImageNetの1000クラス分類のタスクを学習したモデルになります。

今回のテスト用に入力する画像として、「パグ」の画像データを１枚用意しました。

```python
img = Image.open('./pug.jpg')
plt.imshow(img)
plt.show()
```

<img src="http://www.ie110704.net/wp-content/uploads/sites/4/2018/04/201804_1_1.png" alt="" width="350" height="225" class="alignnone size-full wp-image-1469" />

1000クラスのラベルに「パグ」が含まれており、クラス番号は `254` になります。

Chainerから呼び出した学習済みモデルが、ちゃんとこの画像を入力して `254` を予測するか確認してみます。

## VGG16

VGG16の学習済みモデルを呼び出すには、 `chainer.links.VGGLayers` 関数を使います。

```python
vgg16 = chainer.links.VGG16Layers()
```

Chainerの実装では `chainer.links.VGGLayers` 関数の引数に `pretrained_model` が指定でき、何も指定しない場合にはデフォルトで学習済みモデルの重みを自動でダウンロードする仕組みになっています。

上記で、 重みを反映させたモデルインスタンス `vgg16` を宣言したことになります。

実際に画像を入力して推論させる時は、モデルインスタンスの `__call__` 関数に入力データをセットして実行することになります。

VGG16に限らず、ネットワークによって、入力する画像をそのネットワーク独自の前処理を行う場合がほとんどです。

例えば、VGG16の場合は入力画像に対し、

* (batch_size, chennels, height, width) = (batch_size, 3, 224, 224) に合わせる
* カラーのチャンネルの順番は (BGR) にする
* 各画素値から平均値 (103.939, 116.779, 123.68) を引く

といった処理を施した後、ネットワークで推論させます。

Chainerでは、`Pillow` で読み込んだ画像データに対して、上記の処理を自動でやってくれる `chainer.links.model.vision.vgg.prepare` 関数があります。

```python
img = Image.open('./pug.jpg')
x = chainer.links.model.vision.vgg.prepare(img)
x = x[np.newaxis] # batch size
result = vgg16(x)
F.argmax(F.softmax(result['prob'], axis=1), axis=1)
"""
variable([254])
"""
```

推論した結果には、クラス分類の最終レイヤーである全結合層のベクトル値が出力されますので、これを元に予測クラスを確認できます。

問題なく、 `254` の「パグ」を推論してくれました。

さて、転移学習では、この学習済みモデルによる画像の特徴量を抽出することを指します。

一般的には、最終レイヤーの全結合層の一つ手前のレイヤーの出力ベクトル値を特徴量として得ることが多いですが、用途やレイヤーごとの結果を見て選択したりと様々です。

先程のモデルインスタンスの `__call__` の実行には引数 `layers` が指定でき、どのレイヤーのベクトル値を得るかを指定することができます。

指定にはレイヤーの名前を文字列の配列で指定できます。

モデルが出力できるレイヤーの名前は以下で確認ができます。

```python
vgg16.available_layers
"""
['conv1_1',
 'conv1_2',
 'pool1',
 'conv2_1',
 'conv2_2',
 'pool2',
 'conv3_1',
 'conv3_2',
 'conv3_3',
 'pool3',
 'conv4_1',
 'conv4_2',
 'conv4_3',
 'pool4',
 'conv5_1',
 'conv5_2',
 'conv5_3',
 'pool5',
 'fc6',
 'fc7',
 'fc8',
 'prob']
"""
```

VGG16の場合は最終レイヤーの全結合層が `fc8` なので、手前の `fc7` や `fc6` などを指定して、以下のように出力ベクトル値を得ることができます。

```python
img = Image.open('./pug.jpg')
x = chainer.links.model.vision.vgg.prepare(img)
x = x[np.newaxis]
result = vgg16(x, layers=['fc7'])
result['fc7'].data.squeeze().shape
"""
(4096,)
"""
```

また、Chainerでは モデルインスタンスの `extract` 関数を使うと、自動でクラス分類の全結合層の一つ手前のベクトル値を得ることができます。

```python
img = Image.open('./pug.jpg')
x = chainer.links.model.vision.vgg.prepare(img)
x = x[np.newaxis]
result = vgg16.extract(x)
result['fc7'].data.squeeze().shape
"""
(4096,)
"""
```

`extract` 関数では、内部で `chainer.links.model.vision.vgg.prepare` による前処理も実装されているため、画像データをそのまま渡して結果を得ることも可能です。

```python
img = Image.open('./pug.jpg')
img = [img] # batch size
result = vgg16.extract(img)
result['fc7'].data.squeeze().shape
"""
(4096,)
"""
```

以上が、Chainerを使って、学習済みのVGG16による特徴量を得る方法となります。

## ResNet152

続いて、ResNet（ここでは例としてResNet152）になりますが、他のモデルもVGG16の時と同様のインタフェースで、学習済みモデルの読み込み、実行ができるようになっています。

ResNet152の場合は、`chainer.links.ResNet152Layers` モデルを呼び出します。

```python
resnet152 = chainer.links.ResNet152Layers()
```

ResNet50の場合は `chainer.links.ResNet50Layers`、ResNet101の場合は `chainer.links.ResNet101Layers` でそれぞれモデルを宣言できます。

ResNetに対する入力画像の前処理も同様に、`chainer.links.model.vision.resnet.prepare` 関数で実行できます。

```python
img = Image.open('./pug.jpg')
x = chainer.links.model.vision.resnet.prepare(img)
x = x[np.newaxis]
result = resnet152(x)
F.argmax(F.softmax(result['prob'], axis=1), axis=1)
"""
variable([254])
"""
```

モデルの推論で指定できるレイヤーは以下になります。

```python
resnet152.available_layers
"""
['conv1', 'pool1', 'res2', 'res3', 'res4', 'res5', 'pool5', 'fc6', 'prob']
"""
```

よって、 `res5` や `pool5` などのベクトル値を以下のように取得できます。

```python
img = Image.open('./pug.jpg')
x = chainer.links.model.vision.resnet.prepare(img)
x = x[np.newaxis]
result = resnet152(x, layers=['pool5'])
result['pool5'].data.squeeze().shape
"""
(2048,)
"""
```

ResNetも同様に `extract` 関数で、クラス分類の全結合層の一つ手前のベクトル値が得られます。

```python
mg = Image.open('./pug.jpg')
x = chainer.links.model.vision.resnet.prepare(img)
x = x[np.newaxis]
result = resnet152.extract(x)
result['pool5'].data.squeeze().shape
"""
(2048,)
"""
```

```python
img = Image.open('./pug.jpg')
img = [img]
result = resnet152.extract(img)
result['pool5'].data.squeeze().shape
"""
(2048,)
"""
```

## GoogLeNet

GoogLeNetの場合も同様です。

モデルは `chainer.links.GoogLeNet` 関数で呼び出せます。

```python
googlenet = chainer.links.GoogLeNet()
```

`chainer.links.model.vision.googlenet.prepare` 関数で前処理を施して、推論が可能です。

```python
img = Image.open('./pug.jpg')
x = chainer.links.model.vision.googlenet.prepare(img)
x = x[np.newaxis]
result = googlenet(x)
F.argmax(F.softmax(result['prob'], axis=1), axis=1)
"""
variable([254])
"""
```

指定できるレイヤーは以下になります。

```python
googlenet.available_layers
"""
['conv1',
 'pool1',
 'conv2_reduce',
 'conv2',
 'pool2',
 'inception_3a',
 'inception_3b',
 'pool3',
 'inception_4a',
 'inception_4b',
 'inception_4c',
 'inception_4d',
 'inception_4e',
 'pool4',
 'inception_5a',
 'inception_5b',
 'pool5',
 'loss3_fc',
 'prob',
 'loss1_fc2',
 'loss2_fc2']
"""
```

GoogLeNetの場合はネットワークがややこしく、評価時のロスを計算するレイヤーが複数あるのに対し、予測時に計算するレイヤーは `loss3_fc` のみになります。

どのレイヤーを指定するかは、やはり用途や結果を見てといった話になりますが、ラベルの予測レイヤーの手前のレイヤーだと、以下などになります。

```python
img = Image.open('./pug.jpg')
x = chainer.links.model.vision.googlenet.prepare(img)
x = x[np.newaxis]
result = googlenet(x, layers=['pool5'])
result['pool5'].data.squeeze().shape
"""
(1024,)
"""
```

同様に `extract` 関数で、クラス分類の手前のベクトル値が得られます。

```python
img = Image.open('./pug.jpg')
x = chainer.links.model.vision.googlenet.prepare(img)
x = x[np.newaxis]
result = googlenet.extract(x)
result['pool5'].data.squeeze().shape
"""
(1024,)
"""
```

```python
img = Image.open('./pug.jpg')
img = [img]
result = googlenet.extract(img)
result['pool5'].data.squeeze().shape
"""
(1024,)
"""
```

# 学習済みの重みを使ってファインチューニングする

学習済みモデルは、畳み込みなどのレイヤーで画像の特徴をうまく捉えられています。

これを利用して、最後の方のクラス分類をするレイヤーを任意のレイヤーに切り替えて再学習させると、一からモデルを学習させるよりも高い精度で学習できる場合があります。

そのようにして、学習済みモデルのレイヤーに任意のレイヤーを付け加えて再学習させる方法をファインチューニングといいます。

これまでに記した通り、Chainerでは学習済みモデルのネットワークを便利に使える関数が揃っていますので、ファインチューニングも比較的楽に実装できます。

## VGG16

学習済みのVGG16をベースに、ファインチューニングさせるモデルを構築する例を紹介します。

転移学習の時に記したように、 `layers` 引数でモデルから出力させるレイヤーを選択できることを利用し、学習済みのレイヤーから得られたベクトルから、タスクに対応する任意のレイヤーを追加してモデルを実装することができます。

問題にもよりますが、VGG16の場合は、`pool5`層、`fc6`層、`fc7`層などのいずれかの出力を取って、新しくfc層を1〜3層追加することが多いです。

例えば、画像を入力して `fc7`層までの値をとり、最後に `out_size` 数のクラスに分類するモデルを実装した場合は下記のようになります。

```python
class VGG16Model(chainer.Chain):
    
    def __init__(self, out_size):
        super(VGG16Model, self).__init__(
            base = L.VGG16Layers(),
            fc = L.Linear(None, out_size)
        )
        
    def __call__(self, x):
        h = self.base(x, layers=['fc7'])
        y = self.fc(h['fc7'])
        return y
```

例えば、10クラスに置き換えた場合には、以下のようにして、10次元のベクトルを得られるように、レイヤーが追加されていることが確認できます。

```python
model = L.Classifier(VGG16Model(out_size=10))

img = Image.open('./pug.jpg')
x = chainer.links.model.vision.vgg.prepare(img)
x = x[np.newaxis]
model.predictor(x)
"""
variable([[ 0.38773811,  0.01801527, -0.16246426,  1.95145059,  0.69883728,
           -4.43975067,  1.65732336, -2.41602373, -0.59516203,  1.09255409]])
"""
```

また、ファインチューニングでは、学習でオプティマイザをセットする際に、学習済みのレイヤーの重みはあまり学習しないように、ハイパーパラメータを設定する場合が多いです。

これも問題によって指定の方法は様々ですが、個人的によくやっているのは、下記のように、学習済みのレイヤーのみ学習率を抑えることが多いです。

```python
# 学習済みレイヤーの学習率を抑える    

model = L.Classifier(VGG16Model(out_size=10))
optimizer = chainer.optimizers.Adam(alpha=1e-4)
optimizer.setup(model)

for func_name in model.predictor.base._children:
    for param in model.predictor.base[func_name].params():
        param.update_rule.hyperparam.alpha *= 0.1

if gpu >= 0:
    chainer.cuda.get_device(gpu).use()
    model.to_gpu(gpu)
```

学習済みのレイヤーは学習させない（重みを完全に固定する）場合には、`disable_update`関数が使えます。

```python
# 学習済みレイヤーの学習率を固定する

model = L.Classifier(VGG16Model(out_size=10))
optimizer = chainer.optimizers.Adam(alpha=1e-4)
optimizer.setup(model)

model.predictor.base.disable_update()

if gpu >= 0:
    chainer.cuda.get_device(gpu).use()
    model.to_gpu(gpu)
```

あとは普通に学習部分を実装すれば、ファインチューニングをさせることができます。

## ResNet152

ResNetも同様に実装が可能です。

`res5`層、`pool5`層などのいずれかの出力を取って、新しくfc層を1〜3層追加します。

```python
class ResNet152Model(chainer.Chain):
    
    def __init__(self, out_size):
        super(ResNet152Model, self).__init__(
            base = L.ResNet152Layers(),
            fc = L.Linear(None, out_size)
        )
        
    def __call__(self, x):
        h = self.base(x, layers=['pool5'])
        y = self.fc(h['pool5'])
        return y
```

```python
model = L.Classifier(ResNet152Model(out_size=10))

img = Image.open('./pug.jpg')
x = chainer.links.model.vision.resnet.prepare(img)
x = x[np.newaxis]
model.predictor(x)
"""
variable([[-0.21243748,  0.13713685, -0.03266272, -0.27797008, -0.13169979,
           -0.22922461,  0.11408829,  0.25870982,  0.6897561 ,  0.35164833]])
"""
```

モデルインタンスの宣言後は、オプティマイザなどの設定は同じように実装できます。

```python
# 学習済みレイヤーの学習率を抑える    

model = L.Classifier(ResNet152Model(out_size=10))
optimizer = chainer.optimizers.Adam(alpha=1e-4)
optimizer.setup(model)

for func_name in model.predictor.base._children:
    for param in model.predictor.base[func_name].params():
        param.update_rule.hyperparam.alpha *= 0.1

if gpu >= 0:
    chainer.cuda.get_device(gpu).use()
    model.to_gpu(gpu)
```

```python
# 学習済みレイヤーの学習率を固定する

model = L.Classifier(ResNet152Model(out_size=10))
optimizer = chainer.optimizers.Adam(alpha=1e-4)
optimizer.setup(model)

model.predictor.base.disable_update()

if gpu >= 0:
    chainer.cuda.get_device(gpu).use()
    model.to_gpu(gpu)
```

## GoogLeNet

GoogLeNetに関しては、上記でも述べましたが、出力までの推論がややこしく構成されています。

評価時は、`loss1`、`loss2`、`loss3`を使って計算させますが、予測は`loss3`（loss関数を通す前のベクトル）のみ利用します。

したがって、それぞれのlossに向かうfc層を変更して、同様に複数のlossを使って評価を行うモデルに変更する必要があると考えるのが自然です。

しかし、GoogLeNetの `layers` 引数に、`loss1_fc1`、`loss2_fc1`が選択できないようになっています。（指定しても何も返ってこない）

したがって、現状では、同様のネットワークを自前で作成する必要があるみたいです。