EfficientNetV2で転移学習・ファインチューニングする

# はじめに

tensorflow2.8を利用できる環境構築が完了したので、勉強がてらEfficientNetV2の学習済みモデルで転移学習・ファインチューニングを試してみました。

- 環境
OS: Ubuntu18.04LTS
CPU: Intel® Core™ i7-8700 CPU @ 3.20GHz × 12
GPU: GeForce RTX2080
Python: Python3.7.9(Anacondaで作成した仮想環境)
tensorflow:2.8.0
matplotlib:3.5.1

- 参考記事
① [TensorFlow, Kerasで転移学習・ファインチューニング（画像分類の例）](https://note.nkmk.me/python-tensorflow-keras-transfer-learning-fine-tuning)
② [tensorflow2.0 + kerasでGPUメモリの使用量を抑える方法](https://qiita.com/studio_haneya/items/4dfaf2fb2ac44818e7e0)
③ [転移学習でCIFAR-10正解率99%を達成する方法](https://qiita.com/T-STAR/items/de933c67b52a47f9efef#%E3%83%95%E3%82%A1%E3%82%A4%E3%83%B3%E3%83%81%E3%83%A5%E3%83%BC%E3%83%8B%E3%83%B3%E3%82%B0)

# 学習済みモデルの読み込み

```python
# ライブラリの読み込みとGPUの設定
import os
import tensorflow as tf
import matplotlib.pyplot as plt

physical_devices = tf.config.list_physical_devices('GPU')
if len(physical_devices) > 0:
    for device in physical_devices:
        tf.config.experimental.set_memory_growth(device, True)
        print('{} memory growth: {}'.format(device, tf.config.experimental.get_memory_growth(device)))
else:
    print("Not enough GPU hardware devices available")
```
GPUの設定部分が無いと学習実行時に使用可能なGPUのメモリをすべて確保してしまいます。
tensorflow１系→２系で書き方が変更になっています。([参考記事②](https://qiita.com/studio_haneya/items/4dfaf2fb2ac44818e7e0))

```python
# 学習済みEfficientNetV2の読み込み
model = tf.keras.applications.efficientnet_v2.EfficientNetV2B0(weights="imagenet")
```
読み込めるEfficientNetV2の種類は`B0~B3, S, M, L`の7種類が存在します。
B0からLにかけて精度は高くなりますが、パラメータ数が増えるので学習・推論により多くの時間を要するとのこと。

# 学習済みモデルで推論
まず読み込んだモデルをそのまま使ってみます。
ペンギンの画像を使用しました。

![Penguin](https://cdn.pixabay.com/photo/2014/07/04/21/59/king-penguins-384252_960_720.jpg "penguin.jpg")

```python
# 推論用画像の読み込み
base_input_shape = model.input_shape[1:]
img_pil = tf.keras.preprocessing.image.load_img("./penguin.jpg", target_size=(base_input_shape[0], base_input_shape[1]))
```
`tf.keras.preprocessing.image`に画像を読み込めるモジュールがあるので利用します。
読み込み時に引数でモデルのinput層のサイズを指定してリサイズもできちゃうので楽です。

```python
# 前処理
img = tf.keras.preprocessing.image.img_to_array(img_pil)
img = img[tf.newaxis]
x = tf.keras.applications.efficientnet_v2.preprocess_input(img)
```

モデルに入力する前の前処理を行います。
１行目：numpy配列への変換
２行目：次元の追加((224, 224, 3) → (1, 224, 224, 3))
３行目：モデルに合わせた変換処理

```python
# 推論
y = model.predict(x)
result = tf.keras.applications.efficientnet_v2.decode_predictions(y, top=5)
print(result)
```
出力結果
> [[('n02056570', 'king_penguin', 0.8875521), ('n01798484', 'prairie_chicken', 0.0006106787), ('n02071294', 'killer_whale', 0.0005540343), ('n03743016', 'megalith', 0.0005340129), ('n01629819', 'European_fire_salamander', 0.00049976655)]]

一番確率の高いラベルがking_penguinと正確に分類できていますね。複数体が写っている画像ですがきちんと分類してくれました。
確率は低いですが2番目以降のラベルも
- prairie_chicken(ソウゲンライチョウ)→首元にオレンジ色の毛が生えている鳥
- killer_whale(シャチ)→ 体が白色と黒色
- megalith(巨石)→ペンギンの背景に岩がある
- European_fire_salamander(ファイアサラマンダー)→黒地に黄色い斑模様

と、画像内の特徴を踏まえたラベルになっています。

# 転移学習

次に読み込んだモデルで転移学習を行ってみます。
今まで転移学習とファインチューニングの違いを曖昧に認識していましたが、今回調べてみて
- 転移学習：追加した出力層付近の重みだけを学習する
- ファインチューニング：追加した出力層付近＋読み込んだモデルの一部or全部の重みを学習する

と理解しました。

```python
# 転移学習用データセットの読み込み
(x_train, y_train),  (x_test, y_test) = tf.keras.datasets.cifar10.load_data()
num_classes = 10
input_shape = x_train.shape[1:]

y_train = tf.keras.utils.to_categorical(y_train)
y_test = tf.keras.utils.to_categorical(y_test) 
```
転移学習用のデータセットにはcifar-10を使用します。

```python
# モデルの実装
x = inputs = tf.keras.layers.Input(shape=input_shape)
# Resize層
x = tf.keras.layers.Lambda(lambda image: tf.image.resize(image, base_input_shape[0:2]), output_shape=base_input_shape)(x)

base_model = tf.keras.applications.efficientnet_v2.EfficientNetV2B0(include_top=False, input_shape=base_input_shape, weights="imagenet")
base_model.trainable = False
x = base_model(x, training=False)
# cifar-10用の層の追加
x = tf.keras.layers.GlobalAveragePooling2D()(x)
x = tf.keras.layers.Dropout(0.5)(x)
x = tf.keras.layers.Dense(num_classes)(x)
outputs = tf.keras.layers.Activation('softmax')(x)
model = tf.keras.Model(inputs, outputs)

model.compile(
    loss='categorical_crossentropy',
    optimizer='adam',
    metrics=["accuracy"]
)
```
モデル読み込み時に`include_top=False`として出力層付近を取り除いたモデルを読み込んでから、cifar-10用の層を追加しています。
転移学習なので、読み込んだモデルの重みは学習させないように
`base_model.trainable = False`
`x = base_model(x, training=False)`
で重みを固定します。

```python
#モデルの学習
history = model.fit(
    x_train, 
    y_train, 
    validation_data=(x_test, y_test), 
    batch_size=64, 
    epochs=10, 
    shuffle=True
)
```
出力結果
> Epoch 1/10
782/782 [==============================] - 57s 67ms/step - loss: 0.4971 - accuracy: 0.8411 - val_loss: 0.3194 - val_accuracy: 0.8919
Epoch 2/10
782/782 [==============================] - 52s 66ms/step - loss: 0.3362 - accuracy: 0.8861 - val_loss: 0.2923 - val_accuracy: 0.8990
Epoch 3/10
782/782 [==============================] - 52s 67ms/step - loss: 0.3165 - accuracy: 0.8931 - val_loss: 0.2801 - val_accuracy: 0.9017
Epoch 4/10
782/782 [==============================] - 52s 67ms/step - loss: 0.3080 - accuracy: 0.8950 - val_loss: 0.2729 - val_accuracy: 0.9050
Epoch 5/10
782/782 [==============================] - 52s 67ms/step - loss: 0.3017 - accuracy: 0.8981 - val_loss: 0.2727 - val_accuracy: 0.9061
Epoch 6/10
782/782 [==============================] - 52s 67ms/step - loss: 0.3003 - accuracy: 0.8976 - val_loss: 0.2668 - val_accuracy: 0.9078
Epoch 7/10
782/782 [==============================] - 52s 66ms/step - loss: 0.2942 - accuracy: 0.8987 - val_loss: 0.2640 - val_accuracy: 0.9075
Epoch 8/10
782/782 [==============================] - 52s 67ms/step - loss: 0.2912 - accuracy: 0.8999 - val_loss: 0.2629 - val_accuracy: 0.9085
Epoch 9/10
782/782 [==============================] - 52s 67ms/step - loss: 0.2945 - accuracy: 0.8983 - val_loss: 0.2608 - val_accuracy: 0.9078
Epoch 10/10
782/782 [==============================] - 52s 66ms/step - loss: 0.2916 - accuracy: 0.9006 - val_loss: 0.2610 - val_accuracy: 0.9095

10epoch学習させて、val_accuracyは90.95%となりました。


# ファインチューニング

最後にファインチューニングを行います。

```python
# 学習させる部分の指定
layer_names = [l.name for l in base_model.layers]
idx = layer_names.index('block6a_expand_conv')

base_model.trainable = True
for layer in base_model.layers[:idx]:
   layer.trainable = False

model.compile(
    loss='categorical_crossentropy',
    optimizer='adam',
    metrics=["accuracy"]
)
```
モデルの一部だけを学習可能にするために、まず全ての層を学習可能にしてから途中の層までの重みを固定するという処理を行っています。今回は出力層に近い`block6a_expand_conv`以降を学習可能にしました。
層の設定が終わったらもう一度compileを行います。

```python
#モデルの学習
history = model.fit(
    x_train, 
    y_train, 
    validation_data=(x_test, y_test), 
    batch_size=64, 
    epochs=10, 
    shuffle=True
)
```
出力結果
> Epoch 1/10
782/782 [==============================] - 86s 104ms/step - loss: 0.3140 - accuracy: 0.8973 - val_loss: 0.2206 - val_accuracy: 0.9221
Epoch 2/10
782/782 [==============================] - 81s 103ms/step - loss: 0.1700 - accuracy: 0.9436 - val_loss: 0.1951 - val_accuracy: 0.9350
Epoch 3/10
782/782 [==============================] - 81s 103ms/step - loss: 0.1361 - accuracy: 0.9550 - val_loss: 0.2124 - val_accuracy: 0.9326
Epoch 4/10
782/782 [==============================] - 82s 104ms/step - loss: 0.1021 - accuracy: 0.9650 - val_loss: 0.2296 - val_accuracy: 0.9296
Epoch 5/10
782/782 [==============================] - 81s 103ms/step - loss: 0.0863 - accuracy: 0.9714 - val_loss: 0.1963 - val_accuracy: 0.9394
Epoch 6/10
782/782 [==============================] - 81s 103ms/step - loss: 0.0769 - accuracy: 0.9737 - val_loss: 0.2110 - val_accuracy: 0.9308
Epoch 7/10
782/782 [==============================] - 81s 103ms/step - loss: 0.0615 - accuracy: 0.9795 - val_loss: 0.2344 - val_accuracy: 0.9313
Epoch 8/10
782/782 [==============================] - 81s 103ms/step - loss: 0.0898 - accuracy: 0.9697 - val_loss: 0.3242 - val_accuracy: 0.9168
Epoch 9/10
782/782 [==============================] - 80s 102ms/step - loss: 0.0607 - accuracy: 0.9801 - val_loss: 0.3059 - val_accuracy: 0.9160
Epoch 10/10
782/782 [==============================] - 81s 104ms/step - loss: 0.0525 - accuracy: 0.9831 - val_loss: 0.2032 - val_accuracy: 0.9405

ファインチューニングの場合だとval_accuracyが94.05%となりました。
ファインチューニングを行う際は、いきなり全ての層を学習可能とするとモデルが崩壊する恐れがあるとのことです。今回のようにまず追加した層のみを学習させて馴染ませてあげるとその可能性が減るらしいです。([参考記事③](https://qiita.com/T-STAR/items/de933c67b52a47f9efef#%E3%83%95%E3%82%A1%E3%82%A4%E3%83%B3%E3%83%81%E3%83%A5%E3%83%BC%E3%83%8B%E3%83%B3%E3%82%B0))