【AI初心者向け】mnist_transfer_cnn.pyを1行ずつ解説していく(KerasでMNISTを学習させる)

# はじめに
この記事は全3回予定の第3回の記事です。
この記事は[mnist_transfer_cnn.py](https://github.com/keras-team/keras/blob/master/examples/mnist_transfer_cnn.py)を1行ずつ解説していくだけの記事です。
前回と重なる部分もありますが、記事単体で読みやすくするため、重複して書いている内容もありますのでご了承ください。
AIに興味があるけどまだ触ったことはない人などが対象です。これを読めばディープラーニングの基本的な学習の流れが理解できるはず、と思って書いていきます。(もともとは社内で研修用に使おうと思って作成していた内容です)
1. [【AI初心者向け】mnist_mlp.pyを1行ずつ解説していく(KerasでMNISTを学習させる)](https://qiita.com/nashineko/items/4ca4ee9230266037d6ee)
2. [【AI初心者向け】mnist_cnn.pyを1行ずつ解説していく(KerasでMNISTを学習させる)](https://qiita.com/nashineko/items/69cc529117c92ecb1190)
3. 【AI初心者向け】mnist_transfer_cnn.pyを1行ずつ解説していく(KerasでMNISTを学習させる)


## 動作確認方法について
MNISTは画像なので、このコードを動かすにはGPUがあったほうがいいです（CPUだとちょっと辛いです）。
おすすめの方法は[Google Colaboratory](https://colab.research.google.com/notebooks/welcome.ipynb?hl=ja)を使う方法です。
![colab.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351213/87ea5af7-1fa3-22a8-d520-427d53695f4b.gif)
やることは2つだけ。
・Python3の新しいノートブックを開く
・ランタイムからGPUを有効にする
これでGPUが使えるようになりました。
セルにコードを貼り付けて実行(ショートカットはCTRL+ENTER)するだけで動きます。


# mnistについて
手書き文字画像のデータセットで、機械学習のチュートリアルでよく使用されます。
内容：0～9の手書き文字
画像サイズ：28px＊28px
カラー：白黒
データサイズ：7万枚（訓練データ6万、テストデータ1万の画像とラベルが用意されています）


# Fine-tuningとは
すでにある優れたモデルのパラメータを初期値として利用し、別のタスクに対応すること。こうすることで計算コストの削減と、精度の向上が望めます。

今回でいえば、


1. 0～4の画像を分類するモデルを作成(もとになる重みを作成する)
1. 作成したモデルの画像の特徴を抽出する層の重みを固定し、変更できないようにする
1. 5～9の画像を学習させる(全結合層=分類する部分の重みのみを更新する)
1. 最終的に、5～9の5種類の手書き文字を入力として受け取り、5～9のいずれであるか5種類に分類するモデルが完成する

***5～9の画像を分類できるよう学習させたので、今回最終的に完成したモデルでは0～4の画像分類はできません。***


# コードの解説

## 準備
```python
'''Trains a simple convnet on the MNIST dataset.
Gets to 99.25% test accuracy after 12 epochs
(there is still a lot of margin for parameter tuning).
16 seconds per epoch on a GRID K520 GPU.
'''
'''Transfer learning toy example.
1 - Train a simple convnet on the MNIST dataset the first 5 digits [0..4].
2 - Freeze convolutional layers and fine-tune dense layers
   for the classification of digits [5..9].
Get to 99.8% test accuracy after 5 epochs
for the first five digits classifier
and 99.2% for the last five digits after transfer + fine-tuning.
'''

# 特に必要ないコードです（Pythonのバージョンが3だが、コードがPython2で書かれている場合に必要になる）
from __future__ import print_function

# 必要なライブラリをインポート
import datetime
import keras
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Dropout, Activation, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras import backend as K

# 現在時刻を取得
now = datetime.datetime.now

# 定数
batch_size = 128   # バッチサイズ。1度に学習するデータサイズ
num_classes = 5    # 分類するラベル数。今回は手書き画像を5～9の5種類に分類する
epochs = 5         # エポック数。全データを何回学習するか
img_rows, img_cols = 28, 28  # 入力画像の次元数
filters = 32       # 畳み込みのフィルター数
pool_size = 2      # マックスプーリングするサイズ
kernel_size = 3    # 畳み込みのフィルター(カーネル)サイズ
# データの形
if K.image_data_format() == 'channels_first':
    input_shape = (1, img_rows, img_cols)
else:
    input_shape = (img_rows, img_cols, 1)
```
データの形の部分ですが、詳細は[【AI初心者向け】mnist_cnn.pyを1行ずつ解説していく(KerasでMNISTを学習させる)](https://qiita.com/nashineko/items/69cc529117c92ecb1190)で確認してください。データの前処理の部分に記載しています。

KerasのバックエンドがTheano(channels_first)か tensorflow(channels_last)かで画像の書式が異なるのを判断しています。今回はtensorflowなので、(28, 28, 1)になります。

## データの前処理
```python
# mnistのデータを読み込み、訓練データ(6万件)とテストデータ(1万件)に分割する
(x_train, y_train), (x_test, y_test) = mnist.load_data()

# ラベルが5以上か未満かで分けたデータセットを作成
# ラベル値が5未満の訓練画像
x_train_lt5 = x_train[y_train < 5]
# ラベル値が5未満の訓練ラベル
y_train_lt5 = y_train[y_train < 5]
# ラベル値が5未満のテスト画像
x_test_lt5 = x_test[y_test < 5]
# ラベル値が5未満のテストラベル
y_test_lt5 = y_test[y_test < 5]

# ラベル値が5以上の訓練画像
x_train_gte5 = x_train[y_train >= 5]
# ラベル値が5以上の訓練ラベルから5を引いたデータセット(5～9⇒0～4にする)
y_train_gte5 = y_train[y_train >= 5] - 5
# ラベル値が5以上のテスト画像
x_test_gte5 = x_test[y_test >= 5]
# ラベル値が5以上のテストラベルから5を引いたデータセット(5～9⇒0～4にする)
y_test_gte5 = y_test[y_test >= 5] - 5
```
まず、0～4の画像を分類するモデルを作成するために、0～4のデータと5～9のデータに分けておきます。

次に、5～9のデータに関してはラベルを5～9⇒0～4に変更しておきます。

## モデルの定義
```python
# モデルの定義(.add()メソッドを使わないパターン)

# 畳み込み処理で特徴を学習
feature_layers = [
    # 畳み込み層（フィルター：32枚、フィルターサイズ：(3, 3)、受け取る入力サイズ：(28, 28, 1))
    Conv2D(filters, kernel_size,
           # padding='valid'で0パディングしない。0パディングするときは'same'を指定する
           padding='valid',
           input_shape=input_shape),
    # 活性化関数：Relu
    Activation('relu'),
    # 畳み込み層（フィルター：32枚、フィルターサイズ：(3, 3))
    Conv2D(filters, kernel_size),
    # 活性化関数：Relu
    Activation('relu'),
    # プーリング層
    MaxPooling2D(pool_size=pool_size),
    # 0.25の確率でドロップアウト
    Dropout(0.25),
    # データを1次元に変換
    Flatten(),
]

# 全結合層で分類を学習
classification_layers = [
    # 全結合層（128ユニット）
    Dense(128),
    # 活性化関数：relu
    Activation('relu'),
    # 0.5の確率でドロップアウト
    Dropout(0.5),
    # 全結合層（5ユニット）
    Dense(num_classes),
    # 活性化関数：softmax(分類問題のため)
    Activation('softmax')
]

# Sequentialクラスにfeature_layersとclassification_layersを渡したものをインスタンス化
model = Sequential(feature_layers + classification_layers)
```
今回は第1回、第2回と違い.add()メソッドを使わずにモデルを定義しています。
Kerasでは、層を順番に並べたリストをSequential()に渡してインスタンス化することでもモデルの定義ができます。

なぜこのような書き方をしたのかというと、***Fine-tuningでは、学習時に特定の層のみ重みを変更しない(または変更する)***ということをします。そのためにあえてこのような書き方をしています。

***画像の特徴を抽出する層と、分類をする層を分けて定義しておくことで、どちらかの重みのみを更新するということが簡単にできる***ようになります。

## 学習させる関数
```python
# 学習させる関数を作成
def train_model(model, train, test, num_classes):
    
    # データの前処理
    # データの形式をreshapeして合わせておく
    x_train = train[0].reshape((train[0].shape[0],) + input_shape)  # (30596, 28, 28) -> reshape(30596, 28, 28, 1)
    x_test = test[0].reshape((test[0].shape[0],) + input_shape)     # (5139, 28, 28)  -> reshape(5139, 28, 28, 1)
    # 画像データは0～255の値をとるので255で割ることでデータを標準化する
    x_train = x_train.astype('float32')
    x_test = x_test.astype('float32')
    # .astype('float32')でデータ型を変換する。(しないと割ったときにエラーが出るはず)
    x_train /= 255
    x_test /= 255
    print('x_train shape:', x_train.shape)
    print(x_train.shape[0], 'train samples')
    print(x_test.shape[0], 'test samples')

    # ラベルデータをone-hot-vector化
    '''one-hot-vectorのイメージはこんな感じ
    label  0 1 2 3 4 
    0:    [1,0,0,0,0]
    3:    [0,0,0,1,0]'''
    y_train = keras.utils.to_categorical(train[1], num_classes)
    y_test = keras.utils.to_categorical(test[1], num_classes)

    # 学習プロセスを設定する
    model.compile(loss='categorical_crossentropy',  # 損失関数を設定。今回は分類なのでcategorical_crossentropy
                  optimizer='adadelta',  # 最適化アルゴリズムをadadeltaにしている
                  metrics=['accuracy'])  # 評価関数を指定

    # 学習開始時間を取得
    t = now()
    # 学習させる
    model.fit(x_train, y_train,       # 学習データ、ラベル
              batch_size=batch_size,  # バッチサイズ（128）
              epochs=epochs,          # エポック数（5）
              verbose=1,              # 学習の進捗をリアルタムに棒グラフで表示(0で非表示)
              validation_data=(x_test, y_test))  # テストデータ(エポックごとにテストを行い誤差を計算するため)
    # 学習にかかった時間を出力
    print('Training time: %s' % (now() - t))
    
    # 評価
    # テストデータを渡す(verbose=0で進行状況メッセージを出さない)
    score = model.evaluate(x_test, y_test, verbose=0)
    # 汎化誤差を出力
    print('Test score:', score[0])
    # 汎化性能を出力
    print('Test accuracy:', score[1])
```
今回、5～9の画像を分類するモデルを作るために2回学習させます。

## 学習
```python
# 上で作成した関数を使い学習させる
# 5未満の画像に0～4のラベルを与えて学習(分類させる)
train_model(model,
            (x_train_lt5, y_train_lt5),
            (x_test_lt5, y_test_lt5), num_classes)

# trainable=Falseで、レイヤーに学習をさせなくする
# 畳み込みを行う部分であるfeature_layersのパラメータ更新をしないよう設定し、classification_layersのパラメータのみ更新する
# 変更を有効にするには、プロパティの変更後のモデルでcompile()を呼ぶ必要がある
for l in feature_layers:
    l.trainable = False

# 上で作成した関数を使い学習させる
# 5以上の画像に0～4のラベルを与えて学習(分類させる)
train_model(model,
            (x_train_gte5, y_train_gte5),
            (x_test_gte5, y_test_gte5), num_classes)
```
いよいよ学習です。

1. 0～4の画像を分類するモデルを作成(もとになる重みを作成する)
1. feature_layersの重みを固定し、変更できないようにする
1. 5～9の画像を学習させる(全結合層=分類する部分の重みのみを更新する)
1. 最終的に、5～9の5種類の手書き文字を入力として受け取り、5～9のいずれであるか5種類に分類するモデルの完成！

# おわりに
以上で全3回のソース解説記事は終わりです。
解説は終わりですが、おまけとして次回、モデルの保存とロード、利用方法の記事をアップ予定です。
作っただけで保存をしないと、せっかく作ったのに消えちゃうので・・・。