TensorFlowのFashion-MNISTサンプルをCNNで改良してみる

先日TensorFlowのチュートリアルにある[基本的な分類のサンプルコードを試しました](https://qiita.com/hidakanoko/items/5292ca79e3ff53867e40)が、精度はせいぜい90%足らずでそれほど高くはありません。
今回はCNNのモデル作成について調べつつ、Fashion-MNISTの精度改善に取り組んでみました。

#### Attribution

Fashion-MINISTの分類サンプルはTensorFlowのチュートリアルをコピー&改変しています。[[1]](https://www.tensorflow.org/tutorials/keras/basic_classification)

> Portions of this page are modifications based on work created and shared by Google and used according to terms described in the Creative Commons 3.0 Attribution License.

# 実行環境

今回はGoogle Colaboratoryを実行環境として使用しています。
ご存知の通りGoogle Colaboratoryは機械学習の教育・研究目的に無償で提供されているプログラムの実行環境です。

コード実行の際はハードウェアアクセラレータとしてGPUを選択しています。
Colaboratoryの実行環境はUbuntuで、GPUとしてNVIDIAのTesla K80が提供されています。

# 元のサンプル

[チュートリアル](https://www.tensorflow.org/tutorials/keras/basic_classification)にある元々のモデルは次の通りです。
入力の2次元配列を一次元配列に変換した後で全結合層を2つ通しています。

```
model = keras.Sequential([
    keras.layers.Flatten(input_shape=(28, 28)),
    keras.layers.Dense(128, activation=tf.nn.relu),
    keras.layers.Dense(10, activation=tf.nn.softmax)
])
```

元のサンプルをRaspberry Pi上で実行した際のトレーニング時の精度は89.1%, テストデータでの87.75%でした。

```
...
Epoch 5/5
60000/60000 [==============================] - 6s 94us/step - loss: 0.2944 - acc: 0.8910
10000/10000 [==============================] - 0s 47us/step
test loss 0.3410533882856369, test accuracy 0.8775
```

※精度は3回実行して得られた結果の中央値を取っています。そのため必ずしも正しいといえる値ではないかもしれません。悪しからず。

# CNNで実行

CNNのモデルはKerasのMNISTサンプルを参考にしました。[[2]](https://github.com/keras-team/keras/blob/master/examples/mnist_cnn.py)
モデル構築部分を下記のように変更して実行してみます。

```
model = Sequential()
model.add(Conv2D(32, kernel_size=(3, 3), activation='relu', input_shape=input_shape))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(len(class_names), activation='softmax'))
```

2次元データの畳み込み層であるConv2Dを2層とMaxプーリング層を1層、そのあとでドロップアウト層、平滑化層、全結合層、ドロップアウト層、全結合層(ソフトマックス)という構造になっています。

テストデータの精度は91.27%まで上昇しました。

```
Epoch 5/5
60000/60000 [==============================] - 17s 285us/step - loss: 0.2213 - acc: 0.9193
10000/10000 [==============================] - 1s 133us/step
test loss 0.23768348171114922, test accuracy 0.9127
```

## 畳み込み層を増やしてみる

Conv2Dを増やすとどうなるか試してみます。Conv2Dの最初のパラメーターはフィルタ数です。畳み込み層は画像にkernel_sizeで指定したサイズのフィルタをstridesで指定された分だけ(規定値は1)ずらしながら順番に適用して特徴マップを作成します。

Kerasのサンプルでは最初にフィルタ数32, 次に64の畳み込み層を重ねています。フィルタ数を倍にしているので、このパターンに従ってフィルタ数128の畳み込み層を増やしてみます。

```
model.add(Conv2D(32, (3, 3), activation='relu', input_shape=input_shape))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(Conv2D(128, (3, 3), activation='relu'))
```

トレーニング時、テスト時ともに92.63%まで精度が改善しました。

```
Epoch 5/5
60000/60000 [==============================] - 28s 469us/step - loss: 0.2022 - acc: 0.9264
10000/10000 [==============================] - 2s 207us/step
test loss 0.21095278133749962, test accuracy 0.9263
```

そこでフィルタ数256の層をもう一つ追加してみましたが、今度は逆に精度が下がってしまいました。

```
Epoch 5/5
60000/60000 [==============================] - 46s 769us/step - loss: 0.2113 - acc: 0.9224
10000/10000 [==============================] - 3s 319us/step
test loss 0.2241010567367077, test accuracy 0.9181
```

試しにMaxPooling層を挟んでみたりしましたが、精度はそこからさらに下がってしまいました。
このあたりは層の作り方にコツが必要...というかもう少しちゃんと仕組みを理解する必要があるのかもしれません。

## ドロップアウト層の効果を確かめる

ドロップアウト層は訓練時の更新においてランダムに入力ユニットを0とする割合であり、過学習の防止に役立ちます。[[3]](https://keras.io/ja/layers/core/#dropout)

この時点でのモデルは次のとおりです。トレーニング時・テスト時の精度はともに92%程です。

```
model = Sequential()
model.add(Conv2D(32, kernel_size=(3, 3), activation='relu', input_shape=input_shape))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(Conv2D(128, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(len(class_names), activation='softmax'))
```

試しにドロップアウト層を削除して結果がどのようになるか見てみます。

```
Epoch 5/5
60000/60000 [==============================] - 30s 498us/step - loss: 0.0840 - acc: 0.9682
10000/10000 [==============================] - 3s 260us/step
test loss 0.2690899899877608, test accuracy 0.9246
```

トレーニング時の精度は92%->から96%と大幅に上がっている一方、テスト時の精度は92.46%でほとんど変化ありません。
削除したドロップアウト層を戻して、割合を変えて実行してみましたが、減らせば過学習が進み、増やせば学習対象データが減るためなのか精度自体が低下します。サンプルのドロップアウト率はFashion-MNISTの学習に対しては適切な割合となっているようです。

## 余白追加を試してみる

ZeroPadding2D層を追加することで、画像の上下左右に余白を入れることが出来ます。
これにより画像の端の情報が十分に畳み込まれるようになります。
カーネルサイズが3x3なので、端の1ピクセルから畳み込めるようにサイズ2のパディングを入れてみました。

```
model = Sequential()
model.add(ZeroPadding2D(padding=(2,2), input_shape=input_shape))
model.add(Conv2D(32, kernel_size=(3, 3), activation='relu'))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(Conv2D(128, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(len(class_names), activation='softmax'))
```

トレーニング時の精度は93.64%, テスト時の精度は92.84%で、過学習が進んだものの微妙に精度が改善しました。
パディングの追加は精度改善に少しだけ効果があるようです。

```
60000/60000 [==============================] - 36s 598us/step - loss: 0.1727 - acc: 0.9364
10000/10000 [==============================] - 3s 302us/step
test loss 0.20603198404312134, test accuracy 0.9284
```

## 全結合層を増やしてみる

Denseを2層にしてみます。

```
(...omit)
model.add(Dense(128, activation='relu'))
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(len(class_names), activation='softmax'))
```

下記の通り精度はほとんど改善しないどころか、ちょっと下がってしまいました。
単純に全結合層を重ねても精度はあまり改善しません。全結合層は2つで十分なようです。

```
Epoch 5/5
60000/60000 [==============================] - 36s 601us/step - loss: 0.1247 - acc: 0.9546
10000/10000 [==============================] - 3s 297us/step
test loss 0.23022140022814275, test accuracy 0.9267
```

## トレーニングデータを増やしてみる

反転した画像を作成することでデータを倍に増やしてみます。
とくに靴やサンダルのデータは右向き、左向きで正しくデータが認識できていない可能性があると思います。

画像データの操作はimageDataGeneratorを使うのが標準的っぽいですが、ロードしたデータはただのNumPy配列なので単純にリバースして結合することとしました。

```
x_train_r = x_train[:,:,::-1]
x_train_dup = np.concatenate((x_train, x_train_r))
y_train_dup = np.concatenate((y_train, np.copy(y_train)))
```

テスト時の精度は若干改善して93.36%まで上昇しました。

```
Epoch 5/5
120000/120000 [==============================] - 74s 617us/step - loss: 0.1627 - acc: 0.9396
10000/10000 [==============================] - 3s 252us/step
test loss 0.1949729108273983, test accuracy 0.9336
```

## エポックを倍にしてみる

最後の手段としてエポックを5から倍の10にしてみました。

```
model.fit(x_train_dup, y_train_dup, epochs=10)
```

残念ながら過学習が進むだけで下記の通り制度は改善しませんでした...
現在のモデルでは、エポックは5で十分なようです。

```
Epoch 10/10
120000/120000 [==============================] - 71s 595us/step - loss: 0.1130 - acc: 0.9578
10000/10000 [==============================] - 3s 284us/step
test loss 0.23204924822449685, test accuracy 0.9322
```

# 最終的なコード

```
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import tensorflow as tf
from tensorflow import keras

from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv2D, MaxPooling2D, ZeroPadding2D

import numpy as np

fashion_mnist = keras.datasets.fashion_mnist
(x_train, y_train), (x_test, y_test) = fashion_mnist.load_data()

class_names = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat', 
               'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot']

img_rows, img_cols = 28, 28

x_train_r = x_train[:,:,::-1]

x_train_dup = np.concatenate((x_train, x_train_r))
y_train_dup = np.concatenate((y_train, np.copy(y_train)))

x_train_dup = x_train_dup / 255.0
x_test = x_test / 255.0

if keras.backend.image_data_format() == 'channels_first':
    x_train_dup = x_train_dup.reshape(x_train_dup.shape[0], 1, img_rows, img_cols)
    x_test = x_test.reshape(x_test.shape[0], 1, img_rows, img_cols)
    input_shape = (1, img_rows, img_cols)
else:
    x_train_dup = x_train_dup.reshape(x_train_dup.shape[0], img_rows, img_cols, 1)
    x_test = x_test.reshape(x_test.shape[0], img_rows, img_cols, 1)
    input_shape = (img_rows, img_cols, 1)

model = Sequential()
model.add(ZeroPadding2D(padding=(2,2), input_shape=input_shape))
model.add(Conv2D(32, (3, 3), activation='relu'))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(Conv2D(128, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(len(class_names), activation='softmax'))

model.compile(optimizer=tf.train.AdamOptimizer(), 
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

model.fit(x_train_dup, y_train_dup, epochs=5)

test_loss, test_acc = model.evaluate(x_test, y_test)
print("test loss %s, test accuracy %s" % (test_loss, test_acc));
```

ここに書かれている以外にもMaxPooling2DをAveragePooling2Dに変えてみたりしましたが、精度は大きく変わりませんでした。
これ以上の改善方法は思いつかないので、とりあえず今回はここまでにしますが...もう少し勉強していつかリベンジしようと思います。

---

[1]https://www.tensorflow.org/tutorials/keras/basic_classification
[2]https://github.com/keras-team/keras/blob/master/examples/mnist_cnn.py
[3]https://keras.io/ja/layers/core/#dropout