転移学習の使い方と効果の確認

#1. はじめに

転移学習の使い方と効果について、自分で検証をしてみたいと思い、
InceptionV3をベースとした転移学習を行ってみました。

転移学習とは、既に学習されたモデルを他の新しいタスクに適用することで、
効率的に新しいタスクに適応することを基本的な考えとしています。

題材としては、以前行ったCelebAの性別判定とし、
より少ないデータ数、試行回数で学習を行って、
どれくらいの正答率を得られるのか検証してみました。

↓以前CNNを用いて行ったCelebAの性別判定
訓練データ数：10,000
試行回数：20
テストデータ正答率：94.00%

https://qiita.com/ToppaD/items/4756f2eaa7dff82d6434

#2. 前準備
##2-1. InceptionV3とは

Inception-v3とは、Googleによって開発されたニューラルネットワークで、ILSVRCという大規模画像データセットを使った画像識別タスク用に1,000クラスの画像分類を行うよう学習され、非常に高い精度の画像識別を達成しています。
以下のコードによりkerasよりダウンロードできます。

```python
from keras.applications.inception_v3 import InceptionV3
model = InceptionV3()
```


##2-2. データの読み込み
画像の読み込みについての解説は「1.はじめに」に付けましたリンク先を参照ください。
今回は、データセットを訓練データ(1000) / 検証データ(500) / テストデータ(1000) に切り分けます。
訓練データ数を前回の1/10(10,000→1,000)まで減らしていますが、テストデータの正答率はどうなるのか、転移学習の効果が期待されます。

```python
import pathlib
imgdir_path = pathlib.Path('img_align_celeba')
file_list = sorted([str(path) for path in imgdir_path.glob('*.jpg')])

i=0
label_list = []
with open("img_align_celeba/list_attr_celeba.txt","r") as f:
    lines = f.readlines()
    for line in lines:
        if i > 1:
            line = line.split()
            #1列目のインデックスを除いてint化、-1を0に変換
            line = [int(i) if i == '1' else int(0) for i in line[1:]]
            label_list.append(line)
        i += 1

import tensorflow as tf
ds_files_labels = tf.data.Dataset.from_tensor_slices((file_list,label_list))

def load_and_preprocess(path, label):
    """画像パスから画像を読み込み、ラベルと共に返す関数"""
    image = tf.io.read_file(path)
    image = tf.image.decode_jpeg(image, channels=3)
    image = tf.cast(image,tf.float32) / 255.0
    return image,label

ds_images_labels = ds_files_labels.map(load_and_preprocess)

tf.random.set_seed(1)
ds_images_labels = ds_images_labels.shuffle(1000,reshuffle_each_iteration=False)
celeba_train = ds_images_labels.take(1000)
celeba_valid = ds_images_labels.skip(1000).take(500)
celeba_test = ds_images_labels.skip(1500).take(1000)

def preprocess(example_i,example_l,size=(64,64),mode='train'):
    """訓練時：画像をランダムに変形、テスト時：画像を整形して返す関数"""
    image = example_i
    label = example_l[20]  #ラベル21列目が性別

    if mode == 'train':
        image_resized = tf.image.resize(image,size = size)
        image_flip = tf.image.random_flip_left_right(image_resized)
        return (image_flip, tf.cast(label,tf.int32))
    else:
        image_resized = tf.image.resize(image,size=size)
        return (image_resized, tf.cast(label,tf.int32))
```

#3. 学習
##3-1. 転移学習概観

KerasのHPに転移学習のやり方について記載があります。

https://keras.io/guides/transfer_learning/

個人的に興味深いと思ったところを、雰囲気で翻訳してみます。
(*適切に伝わらないところが多々あると思いますので、興味のある方は原文をお読みください。)

>
The most common incarnation of transfer learning in the context of deep learning is the following workflow:
1. Take layers from a previously trained model.
2. Freeze them, so as to avoid destroying any of the information they contain during future training rounds.
3. Add some new, trainable layers on top of the frozen layers. They will learn to turn the old features into predictions on a new dataset.
4. Train the new layers on your dataset.


>深層学習の中の転移学習は、以下のフローが最も一般的です。
1. 学習済みのモデルから層を引用します。
2. 新しい学習でそのモデルが持つ情報を失わないように、それらの層を固定します。
3. 学習可能な層をそれらの固定された層に加えます。
　新しい層は、固定された層から出力される特徴量を、
　新しいデータセットの予測に利用できるように学習するでしょう。
4. 新しい層を新しいデータセットで学習します。


またファインチューニングについては以下のような記載があります。

>
A last, optional step, is fine-tuning, which consists of unfreezing the entire model you obtained above (or part of it), and re-training it on the new data with a very low learning rate. This can potentially achieve meaningful improvements, by incrementally adapting the pretrained features to the new data.

>
最後にオプションとして、すべてのモデル(あるいはその一部)を学習可能にし、**とても低い学習率**で再学習するファインチューニングがあります。
これは学習済みの特徴を徐々に新しいデータに適応することで、潜在的に有意義な改良を達成することができます。

この**とても低い学習率**というところが重要かと思っていて、ファインチューニングの項目ではさらにこんなことを書いています。

>
It's also critical to use a very low learning rate at this stage, because you are training a much larger model than in the first round of training, on a dataset that is typically very small. As a result, you are at risk of overfitting very quickly if you apply large weight updates. Here, you only want to readapt the pretrained weights in an incremental way.

>
とても低い学習率を用いることはとても重要です、なぜなら最初に学習した層よりも全体のモデルは非常に大きく、かつデータセットの数はとても小さいためです。
そのため大きな結合係数の更新を行うと、とても早く過学習に陥る可能性があります。
そのため、事前に学習されている結合係数は徐々に適応させたいです。


##3-2. 新たな層の追加

訓練済みモデルの層の引用は以下のように行います。

```python
base_model = InceptionV3(weights="imagenet", include_top=False)
```

include_top=Falseとすることで、もともとのクラス分類に用いられていた上位の層は取り除かれます。
その取り除かれた層は、新たなクラス分類のために置き換えられます。

```python
x = base_model.output
x = GlobalAveragePooling2D()(x)
x = Dense(1024,activation="relu")(x)
predictions = Dense(1, activation=None)(x)

model = Model(inputs=base_model.input, outputs=predictions)
```
イメージはこんな感じです。
Layer Xは転移したモデルの最上位層としています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2048607/c5d1ac43-0406-1e28-c243-c9616203ad2d.png)


##3-3. 学習1

新たな層の学習は以下の通りになります。
この時転移したモデルの変数は固定しています。
(layer.trainable = False)

```python
for layer in base_model.layers:
    layer.trainable = False

model.compile(optimizer = tf.keras.optimizers.Adam(),
                loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
                metrics=['accuracy'])

history1 = model.fit(ds_train,validation_data=ds_valid,
                    epochs=10,steps_per_epoch=steps_per_epoch,
                    callbacks=[
                    TensorBoard(log_dir="./logdir1"),
                    ModelCheckpoint("./logdir1/model1.hdf5")
                    ])
```

学習後の各数値は以下の通りになりました。

```
loss: 0.0716 - accuracy: 0.9707 - val_loss: 0.1979 - val_accuracy: 0.9260
```

##3-4.　学習2(ファインチューニング)

次にモデル全体の学習を行います。
ベースモデルの一部(249層目以降)の変数を学習可能にします。

```python
for layer in model.layers[:249]:
    layer.trainable = False

for layer in model.layers[249:]:
    layer.trainable = True

model.compile(optimizer=tf.keras.optimizers.SGD(learning_rate=0.0001, momentum=0.9),
    loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
    metrics=['accuracy'])

history2 = model.fit(ds_train,validation_data=ds_valid,
                    epochs=10,steps_per_epoch=steps_per_epoch,
                    callbacks=[
                    TensorBoard(log_dir="./logdir2"),
                    ModelCheckpoint("./logdir2/model2.hdf5")
                    ])
```
学習後の各数値は以下の通りになりました。


```
loss: 0.0628 - accuracy: 0.9854 - val_loss: 0.1994 - val_accuracy: 0.9140
```

#4. 効果の確認
##4-1. 学習1 vs 学習2

**学習1終了時のテストデータ正答率：94.30%**
**学習2終了時のテストデータ正答率：91.30%**

*
学習1終了時の訓練データ正答率：97.07%
学習2終了時の訓練データ正答率：98.54%

学習1終了時のテストデータの正答率の方が高く、
今回の場合は、学習2(ファインチューニング)によって過学習を起こしていることがわかります。

やはりファインチューニングは小さいデータ数に対して大きなモデルを使用するため、
過学習が起きやすいことが推測されます。

##4-2. 学習1 vs CNN
学習1終了時点での結果と、以前行ったCNNの結果を比較すると,

今回
訓練データ数：1,000
試行回数：10 
テストデータ正答率：94.30%

以前
訓練データ数：10,000
試行回数：20
テストデータ正答率：94.00%

1/10のデータ数、1/2の試行回数でほぼ同じな正答率となりました。
これは転移学習の有効性がわかる結果になったと思います。

#5. まとめ

今回は少ないデータ数、少ない試行回数で良好な結果を得ることができました。
ただファインチューニングは過学習を起こしやすく、適切な学習率の設定や固定する層の数等、
調整すべき変数が難しいなと感じました。