Kerasでfine-tuning

[Keras](https://keras.io/)の以下公式ブログにおいて、少ないデータで効率よく学習させる方法の一つとしてfine-tuningの実装が紹介されています。

[Building powerful image classification models using very little data](https://blog.keras.io/building-powerful-image-classification-models-using-very-little-data.html)

[@t_andou](https://twitter.com/t_andou)さんによる日本語訳: [【ディープラーニング】少ないデータで効率よく学習させる方法：準備編](http://blog.takuya-andou.com/entry/finetuning_keras)

上記記事内の実装が最新のバージョン(2.0.8-tf)で動作しなかったため、これを動くようにしてみました。今回検証した動かし方は[Functional API](https://keras.io/getting-started/functional-api-guide/)のほうが実装しやすかったため、全体的にSequential modelからFunctional APIによる実装に置き換えています。

# 前提

* Kerasは最新のTensorFlow(1.4)に組み込まれたものを用いています
* 元のブログが利用しているKaggleのデータセット(dogs-vs-cats)がなぜかネットワークエラーでダウンロードできなかったため、Microsoftから提供されている代わりのデータセット[Kaggle Cats and Dogs Dataset](https://www.microsoft.com/en-us/download/details.aspx?id=54765)を利用しています(データのパス等が元のブログ記事と異なっています、適宜読み替えてください)

# 小さなConvNet(ベースライン)の実装

まずTraining a small convnet from scratch: 80% accuracy in 40 lines of codeの節で登場する、ベースラインとなる小さなConvNetについて、こちらは単純にSequential modelをFunctional APIに書き換えているだけです。

## モデルの構築

```python
import numpy as np
import tensorflow as tf

from PIL import Image
from tensorflow.python.keras.preprocessing.image import ImageDataGenerator
from tensorflow.python.keras.preprocessing.image import array_to_img
from tensorflow.python.keras.preprocessing.image import img_to_array
from tensorflow.python.keras.preprocessing.image import load_img
from tensorflow.python.keras.layers import Conv2D, MaxPooling2D
from tensorflow.python.keras.layers import Activation, Dropout, Flatten, Input, Dense
from tensorflow.python.keras.models import Model

batch_size = 16

inputs = Input(shape=(150, 150, 3))
x = Conv2D(32, (3, 3))(inputs)
x = Activation("relu")(x)
x = MaxPooling2D(pool_size=(2, 2))(x)

x = Conv2D(32, (3, 3))(x)
x = Activation("relu")(x)
x = MaxPooling2D(pool_size=(2, 2))(x)

x = Conv2D(64, (3, 3))(x)
x = Activation("relu")(x)
x = MaxPooling2D(pool_size=(2, 2))(x)

x = Flatten()(x)
x = Dense(64, activation="relu")(x)
x = Dropout(0.5)(x)
prediction = Dense(1, activation="sigmoid")(x)

model = Model(inputs=inputs, outputs=prediction)
model.compile(loss="binary_crossentropy",
             optimizer="rmsprop",
             metrics=["accuracy"])
```

## 学習

```python
train_datagen = ImageDataGenerator(
    rescale=1./255,
    shear_range=0.2,
    zoom_range=0.2,
    horizontal_flip=True)

test_datagen = ImageDataGenerator(rescale=1./255)

# https://keras.io/preprocessing/image/#imagedatagenerator
# ImageDataGeneratorがディレクトリ構造に基いて自動的に
# クラスを確定してくれる
train_generator = train_datagen.flow_from_directory(
    "cats-vs-dogs/data/train",
    target_size=(150, 150), # resize
    batch_size=batch_size,
    class_mode="binary")

validation_generator = test_datagen.flow_from_directory(
    "cats-vs-dogs/data/validation/",
    target_size=(150, 150),
    batch_size=batch_size,
    class_mode="binary")
model.fit_generator(
    train_generator,
    steps_per_epoch=2000 // batch_size,
    epochs=50,
    validation_data=validation_generator,
    validation_steps=800 // batch_size)
model.save_weights("first_try.h5")
```

手元で動かすと50 epoch終了することろにはvalidation setに対するaccuracyは凡そ0.8でした。

# 事前学習したネットワーク(VGG16)のbottleneck featureを利用した実装

次にUsing the bottleneck features of a pre-trained network: 90% accuracy in a minuteの節で登場する、学習済みVGG16のbottleneck featureを利用し(畳み込みのブロックはパラメータを固定し)、全結合層移行のみ学習する場合の実装です。こちらも基本的にはSequential modelからFunctional APIへの置き換のみ行っています。

## bottoleneck featureの保存

```python
from tensorflow.python.keras import applications

def save_bottleneck_features():
    datagen = ImageDataGenerator(rescale=1./255)
    
    model = applications.VGG16(include_top=False, weights="imagenet")
    
    generator = datagen.flow_from_directory(
        "cats-vs-dogs/data/train",
        target_size=(150, 150),
        batch_size=batch_size,
        class_mode=None, # ラベルはいらない
        shuffle=False) # 頭1000枚が猫、後ろ1000枚が犬
    bottoleneck_features_train = model.predict_generator(
        generator, 2000 // batch_size)
    np.save("bottleneck_features_train.npy",
           bottoleneck_features_train)
    
    generator = datagen.flow_from_directory(
        "cats-vs-dogs/data/validation",
        target_size=(150, 150),
        batch_size=batch_size,
        class_mode=None,
        shuffle=False)
    bottoleneck_features_validation = model.predict_generator(
        generator, 800 // batch_size)
    np.save("bottleneck_features_validation.npy",
           bottoleneck_features_validation)

save_bottleneck_features()
```

## モデルの構築と学習

```python
train_data = np.load("bottleneck_features_train.npy")
train_labels = np.array([0] * 1000 + [1] * 1000)
validation_data = np.load("bottleneck_features_validation.npy")
validation_labels = np.array([0] * 400 + [1] * 400)

inputs = Input(shape=train_data.shape[1:])

x = Flatten()(inputs)
x = Dense(256, activation="relu")(x)
x = Dropout(0.5)(x)
predictions = Dense(1, activation="sigmoid")(x)

model = Model(inputs=inputs, outputs=predictions)
model.compile(optimizer="rmsprop",
             loss="binary_crossentropy", metrics=["accuracy"])

model.fit(train_data, train_labels,
         epochs=50,
         batch_size=batch_size,
         validation_data=(validation_data, validation_labels))
model.save_weights("bottleneck_fc_model.h5")
```

手元ではvalidation setに対するaccuracyは凡そ0.89となりました。

# 事前学習したネットワークの上位層のfine-tuning

最後にFine-tuning the top layers of a a pre-trained networkの節で登場するモデルです。ここでは前節のVGG16をもとにしたモデルについて、最後の畳み込みブロックと全結合層の両方を再学習することでfine-tuningします。

全結合層部分については一から学習すると学習がうまく進まないため、前節で学習時に得たパラメータを用います。この学習済みパラメータによる全結合層の初期化部分の実装が元のブログに記載されたものだと動作しなかったため以下のとおり変更しています:

* 全体的にSequential modelからFunctional APIに置き換える
* 全結合層の部分(`top_model`)を`Model`のインスタンスとして一度用意して、これの`load_weights`メソッドで学習済みパラメータを読み込む
* 全体のモデル(`model`)をやはり`Model`のインスタンスとして用意して、`top_model`と学習済みモデルであるVGG16(`base_model`)とをつなげる

また、元のブログ記事では最後の畳み込みブロックより前のブロックについてパラメータを固定するために、`model`の頭から24層の`trainable`を`False`としていますが、[KerasのVGG16の実装](https://github.com/tensorflow/tensorflow/blob/r1.5/tensorflow/python/keras/_impl/keras/applications/vgg16.py)にはそもそも24層もネットワークが存在せず、この方法をとるとすべてのパラメータが`trainable == False`となり学習が進まなくなります。KerasのVGG16の実装に合わせてこの部分は頭から14層までを固定とするように変更しています。

```python
from tensorflow.python.keras import optimizers

base_model = applications.VGG16(weights="imagenet", include_top=False,
                               input_shape=(150, 150, 3))

inputs = Input(shape=base_model.output_shape[1:])
x = Flatten()(inputs)
x = Dense(256, activation="relu")(x)
x = Dropout(0.5)(x)
predictions = Dense(1, activation="sigmoid")(x)

# topは別途パラメータを読み込みたいので
# topだけで一度Modelを作る
top_model = Model(inputs=inputs, outputs=predictions)
top_model.load_weights("bottleneck_fc_model.h5")

model = Model(inputs=base_model.input,
              outputs=top_model(base_model.output))

# 頭から25層をフリーズ ← 間違い、15が正しいらしい
# https://github.com/keras-team/keras/blob/master/keras/applications/vgg16.py
for layer in model.layers[:15]:
    layer.trainable = False

model.summary()
    
model.compile(loss="binary_crossentropy",
             optimizer=optimizers.SGD(lr=1e-4, momentum=0.9),
             metrics=["accuracy"])

train_datagen = ImageDataGenerator(
    rescale=1./255,
    shear_range=0.2,
    zoom_range=0.2,
    horizontal_flip=True)

test_datagen = ImageDataGenerator(rescale=1./255)

train_generator = train_datagen.flow_from_directory(
    "cats-vs-dogs/data/train",
    target_size=(150, 150),
    batch_size=batch_size,
    class_mode="binary")

validation_generator = test_datagen.flow_from_directory(
    "cats-vs-dogs/data/validation",
    target_size=(150, 150),
    batch_size=batch_size,
    class_mode="binary")

model.fit_generator(
    train_generator,
    steps_per_epoch=2000 // batch_size,
    epochs=50,
    validation_data=validation_generator,
    validation_steps=800 // batch_size)
```

最終的に手元では、validation setに対するaccuracyは凡そ0.93となりました。