【MNIST】ImangeNetのモデルを利用してFineTuningやってみた【効率的な高精度モデルの構築】

# ①はじめに
AIを勉強するにあたり**FineTuning**も勉強しておきたいと思います。今回は**[`ImageNet学習済みのDenseNet121モデル`](https://keras.io/ja/applications/#densenet)**を利用して、**[`MNIST`](https://keras.io/ja/datasets/#mnist)**を実施してみたいと思います。ライブラリは**[`keras`](https://keras.io/ja/)**を使用します。

![ai_study_kikaigakusyu.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405376/3e081ba3-8393-27bf-f7c2-67b63d25c53a.png)

# ②ファインチューニングとは
**FineTuning**とは和訳すると**微調整**という意味です。AIでの**ファインチューニング**とは、`既存のモデルの一部を再利用して、新しいモデルを構築する手法`のことです。

## ■ファインチューニングと転移学習の違い
**ファインチューニング**に似たようなものとして、**転移学習**があります。**ファインチューニング**と**転移学習**が、いつもごっちゃになってしまうので、ここで整理します。

- **`ファインチューニング`**
**ファインチューニング**は、学習済みモデルの重みを初期値として、再度学習します。メリットは、既存モデルの重みを初期値として追加学習することで、より効率的に高精度なモデル構築が行えます。

- **`転移学習`**
**転移学習**は、学習済みモデルの重みは固定し、追加した層のみを使用して学習します。メリットは、学習済のモデルをベースに追加学習を行うため、少ないデータで、かつ短い時間でモデルの構築を行うことができます。

## ■で、今回はどっち？
すでに出来上がった画像分類モデルを再利用して、数字の分類を実施したいと思います。今回は、「ImageNetで学習した重みをもつ画像分類のモデル」を利用して、「数字画像の分類」をさせたいと思います。ですので、今回は**`ファインチューニング`**になります。

# ③ファインチューニングのやり方
**ファインチューニング元**とするモデルは[`DenseNet121`](https://keras.io/ja/applications/#densenet)を利用したいと思います。こちらのモデルは軽量だけどそこそこ認識率も良いという[`モデル`](https://keras.io/ja/applications/#documentation-for-individual-models)になっております。

**MNISTの学習データ**は、**`28 x 28 x 1ch`**、**DenseNet121の入力レイヤー**は、**`32以上 x 32以上 x 3ch`**となっているので、これらを合わせる必要があります。合わせる方法として、以下2通りがあります。

> **・MNISTの画像を合わせる方法**
> 　→モデルの入力レイヤーに合わせて、学習画像を32以上x32以上x3chに変更する方法
> 
> **・モデルの入力レイヤーを変更する方法**
> 　→学習画像に合わせて、モデルの入力レイヤーを1chに変更する方法

## ■MNISTの画像を合わせる方法
[`DenseNet121`](https://keras.io/ja/applications/#densenet)の入力形式は、(height=32以上, width=32以上, channels=3ch)となっています。
`MNIST`の画像形式は、(height=28, width=28, channels=1ch)となっています。学習画像サイズは`DenseNet121`のネットワーク定義から32以上x32以上x3chは必須となっているので、以下の処理を施します。

- 画像を64x64にリサイズ
- グレースケール1chからRGB3chに変換

![001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405376/f3229bdd-dd4f-09e7-b84a-86849988a3f7.png)

> ▶[④にてスニペットを用いて説明します](https://qiita.com/PoodleMaster/items/a98344e22327130791d8#mnist%E3%81%AE%E7%94%BB%E5%83%8F%E3%82%92%E5%90%88%E3%82%8F%E3%81%9B%E3%82%8B%E6%96%B9%E6%B3%95-1)

## ■モデルの入力レイヤーを変更する方法
[`DenseNet121`](https://keras.io/ja/applications/#densenet)の入力形式は、(height=32以上, width=32以上, channels=3ch)となっています。
`MNIST`の画像形式は、(height=28, width=28, channels=1ch)となっています。学習画像サイズは`DenseNet121`のネットワーク定義から32以上x32以上x3chは必須となっているので、以下の処理を施します。

- 画像を64x64にリサイズ
- 1chの入力レイヤーを追加

![002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405376/6481d3ed-822b-3dcd-1bd7-d5e543a6fe0f.png)

> ▶[⑤にてスニペットを用いて説明します](https://qiita.com/PoodleMaster/items/a98344e22327130791d8#%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AE%E5%85%A5%E5%8A%9B%E3%83%AC%E3%82%A4%E3%83%A4%E3%83%BC%E3%82%92%E5%A4%89%E6%9B%B4%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95-1)

# ④MNISTの画像を合わせる方法
基本的には、以下の➊～➎の手順でファインチューニングの実施が可能です。

### ➊学習済みの重みをダウンロード
「[`keras.applications.densenet.DenseNet121`](https://keras.io/ja/applications/#densenet)」のAPIにより、簡単に**Densenet121でimagenetを学習させた重みをダウンロード**することができます。毎回ダウンロードするのは大変(33MB)なので、Google Driveに保存しておきます。

``` py
from keras.applications.densenet import *

model = DenseNet121(weights='imagenet')
model.save(fullpath)
```

### ➋MNISTのデータ変換
DenseNet121の入力形式(height, width, channels)に合わせて、MNISTの画像を(28, 28, 1)から(64, 64, 3)に変換します。

``` py
import cv2
import numpy as  np
from keras.datasets import mnist

img_w, img_h, img_ch = 64, 64, 3
num_classes = 10

def pretreatment(x):
    # 画像サイズをリサイズ
    X = []
    for i in range(len(x)):
        dst = cv2.resize(x[i], (img_h, img_w), interpolation=cv2.INTER_CUBIC)
        dst = cv2.cvtColor(dst, cv2.COLOR_GRAY2RGB)
        X.append(dst)

    # リシェイプ
    X = np.array(X).reshape(len(x), img_h, img_w, img_ch)

    # 0〜255 までの範囲のデータを 0〜1 までの範囲に変更
    ret = X.astype('float32') / 255
    return ret

# ■MNISTデータ読み込み
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train, x_valid, y_train, y_valid = train_test_split(x_train, y_train, test_size=0.2)

# ■学習(Train)データ
x_train = pretreatment(x_train)
y_train = np_utils.to_categorical(y_train, num_classes)

# ■学習(Valid)データ
x_valid = pretreatment(x_valid)
y_valid = np_utils.to_categorical(y_valid, num_classes)

# ■テストデータ
x_test = pretreatment(x_test)
y_test = np_utils.to_categorical(y_test, num_classes)
```

### ➌DneseNet121のネットワーク構築を行う
#### (1)DneseNet121ネットワーク構築

kerasでは「[`keras.applications.densenet.DenseNet121`](https://keras.io/ja/applications/#densenet)」のAPIにより、簡単に**Densenet121のネットワーク構築**することができます。`include_topをFalse`にしておくことで、ネットワークの出力層側にある全結合層を含めないようにすることができます。また、この時点ではweightsは読込みしないようにします。

``` py
input_tensor = x_train.shape[1:]
model = DenseNet121(include_top=False, weights=None, input_shape=input_tensor)
```

#### (2)出力層追加
ネットワークの出力層側の全結合層を構築
DenseNet121(ImageNet)は画像分類として1000クラスを判別していましたが、今回実施するのはMNISTなので0～9の10分類にクラス分けができるようにします。

``` py
from keras.layers import *
from keras.models import Model

x = Flatten()(model.layers[-1].output)
x = Dense(10, activation="softmax")(x)
Model(model.inputs, x)
```

### ➍学習済みの重みを読み込む
ネットワーク構成後、➊でダウンロードした**重みを読み込みます**。weightsはこちらの「[`load_weights`](https://keras.io/api/models/model_saving_apis/#load_weights-method)」メソッドで読み込みます。

``` py
model.load_weights(fullpath, by_name=True)
```

### ➎学習する
あとは通常通り学習させれば**FineTuning**ができます。

### ➏実行結果

```
●TEST：検証結果
Test loss    : 0.01561666838824749
Test accuracy: 0.9944000244140625
```

![result](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405376/d88a2ba1-e94c-9a3c-54ae-96816b29cbfe.png)

# ⑤モデルの入力レイヤーを変更する方法
基本的には、以下の➊～➍の手順でファインチューニングの実施が可能です。

### ➊学習済みの重みをダウンロード
「[`keras.applications.densenet.DenseNet121`](https://keras.io/ja/applications/#densenet)」のAPIにより、簡単に**Densenet121でimagenetを学習させた重みをダウンロード**することができます。毎回ダウンロードするのは大変(33MB)なので、Google Driveに保存しておきます。

``` py
from keras.applications.densenet import *

model = DenseNet121(weights='imagenet')
model.save(fullpath)
```

### ➋MNISTのデータ変換
DenseNet121の入力形式(height, width, channels)に合わせて、MNISTの画像を(28, 28, 1)から(64, 64, 1)に変換します。

``` py
import cv2
import numpy as  np
from keras.datasets import mnist

img_w, img_h, img_ch = 64, 64, 1
num_classes = 10

def pretreatment(x):
    # 画像サイズをリサイズ
    X = []
    for i in range(len(x)):
        dst = cv2.resize(x[i], (img_h, img_w), interpolation=cv2.INTER_CUBIC)
#       dst = cv2.cvtColor(dst, cv2.COLOR_GRAY2RGB) ← 1chなので実行しない
        X.append(dst)

    # リシェイプ
    X = np.array(X).reshape(len(x), img_h, img_w, img_ch)

    # 0〜255 までの範囲のデータを 0〜1 までの範囲に変更
    ret = X.astype('float32') / 255
    return ret

# ■MNISTデータ読み込み
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train, x_valid, y_train, y_valid = train_test_split(x_train, y_train, test_size=0.2)

# ■学習(Train)データ
x_train = pretreatment(x_train)
y_train = np_utils.to_categorical(y_train, num_classes)

# ■学習(Valid)データ
x_valid = pretreatment(x_valid)
y_valid = np_utils.to_categorical(y_valid, num_classes)

# ■テストデータ
x_test = pretreatment(x_test)
y_test = np_utils.to_categorical(y_test, num_classes)
```

### ➌DneseNet121を利用して新たなネットワーク構築を行う
#### (1)DneseNet121の入力層の前に独自の入力層を追加
``` py
from keras.layers import *
from keras.models import Model, Sequential

input_tensor = x_train.shape[1:]
input_model = Sequential()
input_model.add(InputLayer(input_shape=input_tensor))
input_model.add(Conv2D(3, (3, 3), padding='same'))
input_model.add(BatchNormalization())
input_model.add(Activation('relu'))
```

#### (2)DneseNet121ネットワーク構築
kerasでは「[`keras.applications.densenet.DenseNet121`](https://keras.io/ja/applications/#densenet)」のAPIにより、簡単に**Densenet121のネットワーク構築**することができます。`include_topをFalse`にしておくことで、ネットワークの出力層側にある全結合層を含めないようにすることができます。また、この時点ではweightsは読込みしないようにします。

``` py
model = DenseNet121(include_top=False, weights=None, input_tensor=input_model.output)
```

#### (3)出力層追加
DenseNet121(ImageNet)は画像分類として1000クラスを判別していましたが、今回実施するのはMNISTなので0～9の10分類にクラス分けができるようにします。

``` py
x = Flatten()(model.layers[-1].output)
x = Dense(num_classes, activation="softmax")(x)
return Model(model.inputs, x)
``` 

### ➍学習済みの重みを読み込む
ネットワーク構成後、➊でダウンロードした**重みを読み込みます**。weightsはこちらの「[`load_weights`](https://keras.io/api/models/model_saving_apis/#load_weights-method)」メソッドで読み込みます。

``` py
model.load_weights(fullpath, by_name=True)
```

### ➎学習する
あとは通常通り学習させれば、**FineTuning**ができます。

### ➏実行結果
```
●TEST：検証結果
Test loss    : 0.019007695838809013
Test accuracy: 0.9934999942779541
```
![result](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405376/c78353e9-874d-24b1-9891-c27054d4ad8d.png)

# ⑥ソースコード
今回学習させたときは、[ImageDataGenerator](https://qiita.com/PoodleMaster/items/54c184d9f2f70cc011d0)を使用し、以下の様な回転・縮小画像を含めて学習させ、汎化性能も考慮させています。

<img src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405376/157ff730-41fa-e1ff-68be-ffbbe37569bb.png' width=120><img src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405376/3289bf5a-a213-a89d-a0bf-d06dde8f087a.png' width=120><img src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405376/cb288d5f-f2ca-d7f2-71fa-6cab4745353f.png' width=120><img src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405376/ed367b1b-be95-0fe0-3e81-22b7d0309940.png' width=120><img src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405376/f9253456-2525-d603-c708-9f2d9907bf29.png' width=120>

**全体のソースコードはこちら** ▶ [github](https://github.com/PoodleMaster/FineTuning-DenseNet121)

# ⑦以上
お疲れさまでした。
**ImageNet学習済みのDenseNet121モデル**をベースにしているので1epocの計算量は多いものの、**FineTuning**なので30epocくらいでほぼ収束します。正解率、損失値もそこそこ良いです。今回は、**FineTuning**のために重みを読み込みましたが、学習を途中まで実施し再度学習を始める場合等へ応用がきくので、本手順を理解しておくと良いと思います。