Keras(+Tensorflow)でMNISTしてみる


以前に、[私的TensorFlow入門](https://qiita.com/fukuit/items/2492d183548e82073766)でも書いたんだけれど、MNISTをまたTensorFlowで書いてみる。今度は、Kerasを使ってみる。

多階層のニューラルネットでmodelを作成しようとすると、TensorFlowでは層を追加していくのってどうやってやるの？とか、直観的に分かりづらいんだけれど、Kerasはそのあたりを上手くやってくれてる感じ。

# Keras(とTensorFlow)のインストール

kerasやtensorflowのインストールごとき、特に解説する必要もない気がするけれど、一応。

```
% pip install -U keras tensorflow
```
## Kerasのソースをダウンロードしておく

kerasのソースをダウンロードしておく。

```
% git clone https://github.com/keras-team/keras.git
```

なぜなら、```examples```があるから。

## MNISTのデータをダウンロードしておく

kearsでは[データセットとしてMNISTが公開されている](https://keras.io/ja/datasets/#mnist)。

とりあえず、一旦、データセットだけをダウンロードしておきたいので、ipythonで以下のように実行すると、```~/.keras/datasets/```にmnist.npzというファイルが保存される。

```Python
from keras.datasets import mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()
```

これにより、x_trainには60000個の学習用データ、y_trainにはそれに対応するラベル、x_testには10000個のテスト用データと、ソレに対応するラベルであるy_testという変数が、それぞれ作成される。

機械学習の勉強をしていると、必ず「trainingセットとtestセットとvalidationセットは別にしておけよ」と言われると思うんだけれど、これだとtrainingセットとtestセットしかないじゃん。というワケで、trainingセットから、10000個くらいをランダムに選んで、validationセットも作っておくことにする。

```Python
from sklearn.model_selection import train_test_split
x_train1, x_valid, y_train1, y_valid = train_test_split(x_train, y_train, test_size=0.175)
```

これで、validationセットが10500個できた。

# MNISTをConvolutional Neural Networkで解く

## modelの作成

とりあえず、modelを作る。

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

model = Sequential()
model.add(Conv2D(32, kernel_size=(3, 3), activation='relu', input_shape=(28, 28, 1)))
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(10, activation='softmax'))
```

ちゃんと過学習しないように、```Dropout()```しておく。


|Layer (type)                 |Output Shape           |Param # |
|-----------------------------|-----------------------|--------|
|conv2d_1 (Conv2D)            |(None, 26, 26, 32)     |320     |
|conv2d_2 (Conv2D)            |(None, 24, 24, 64)     |18496   |
|max_pooling2d_1 (MaxPooling2 |(None, 12, 12, 64)     |0       |
|dropout_1 (Dropout)          |(None, 12, 12, 64)     |0       |
|flatten_1 (Flatten)          |(None, 9216)           |0       |
|dense_1 (Dense)              |(None, 128)            |1179776 |
|dropout_2 (Dropout)          |(None, 128)            |0       |
|dense_2 (Dense)              |(None, 10)             |1290    |


## 学習
学習する。最適化には、```RMSprop()```を使う。examplesには、```Adadelta()```を使うって書いてあるんだけれど、なんとなく```RMSprop()```で。
こういうのの選び方って、鉄則みたいなのあるんですかね。

学習前に、```x_train```、```x_valid```、```x_test```、```y_train```、```y_valid```、```y_test```の前処理をする。

```Python
# 28 x 28の画像がgrayscaleで1chなので、28, 28, 1にreshapeする
x_train = x_train.reshape(x_train.shape[0], 28, 28, 1)
x_valid = x_valid.reshape(x_valid.shape[0], 28, 28, 1)
x_test = x_test.reshape(x_test.shape[0], 28, 28, 1)

# 0-255の整数値を0〜1の小数に変換する
# MNISTって必ずこの処理入るけれど、意味あるのかな
x_train = x_train.astype('float32')
x_valid = x_valid.astype('float32')
x_test = x_test.astype('float32')
x_train /= 255
x_valid /= 255
x_test /= 255

# one-hot vector形式に変換する
y_train = keras.utils.to_categorical(y_train, 10)
y_valid = keras.utils.to_categorical(y_valid, 10)
y_test = keras.utils.to_categorical(y_test, 10)
```

前処理が済んだら、学習する。

```Python
from keras.optimizers import RMSprop

model.compile(loss='categorical_crossentropy',
              optimizer=RMSprop(),
              metrics=['accuracy'])

history = model.fit(x_train, y_train,
                    batch_size=128,
                    epochs=10,
                    verbose=1,
                    validation_data=(x_valid, y_valid))
```

学習した結果は、次のようにすると表示できた。
評価は、学習に使っていない```x_test```、```y_test```で行う。

```Python
score = model.evaluate(x_test, y_test, verbose=0)
print('Test loss:', score[0])
print('Test accuracy:', score[1])
```

我が家の2012年型のMacBook Proで結構な時間をかけて学習した結果が、コレ。

```
Test loss: 0.046481294665449875
Test accuracy: 0.9889
```

なかなか良い。[昨年の今頃にCNNを使わずにやった結果(0.9184)](https://qiita.com/fukuit/items/2492d183548e82073766#%E5%AE%9F%E8%A1%8C%E7%B5%90%E6%9E%9C) より、ずっと良い。

## 学習結果をグラフにしてみる

学習結果は、```history```に格納されている。ドキュメントによると、以下のように書いてある。

> Historyインスタンス．本インスタンスのhistory属性は訓練時に得られた全ての情報を含みます．

で、Historyってどういう構造になっているのか？っていうのが、Kerasのドキュメントを読んでもよく分からないんだけれど、```examples```をいろいろ漁ると分かる。

グラフを描くときは、もちろん```matplotlib```を使う。

```Python
from matplotlib import pyplot as plt

# 精度のplot
plt.plot(history.history['acc'], marker='.', label='acc')
plt.plot(history.history['val_acc'], marker='.', label='val_acc')
plt.title('model accuracy')
plt.grid()
plt.xlabel('epoch')
plt.ylabel('accuracy')
plt.legend(loc='best')
plt.show()

# 損失のplot
plt.plot(history.history['loss'], marker='.', label='loss')
plt.plot(history.history['val_loss'], marker='.', label='val_loss')
plt.title('model loss')
plt.grid()
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend(loc='best')
plt.show()
```

損失の方のグラフはこんな感じ。

![loss_plot.png](https://qiita-image-store.s3.amazonaws.com/0/15553/821086fe-9ec1-a81c-efe0-53e71949155b.png "loss_plot.png")

### 学習の進捗をグラフに表示する

結果を表示するのではなくて、学習している最中にaccuracyとかlossとかをplotできたら、「ちゃんと動いている」という実感が得られそうだ。そういうときは、matplotlib.pyplotをinteractiveモードで動かしながら、epochごとに結果をplotすれば良い。

epochごとに処理するために、```Callback関数```を使う。[Pythonで最適化問題を解く](https://qiita.com/fukuit/items/c7ae3f71898399530dc1)でも使った手法だ。

具体的には、```model.fit()```の時に、```callbacks```として指定する。詳しくは、[ドキュメント](https://keras.io/ja/callbacks/)を。

```Python
class PlotLosses(Callback):
    '''
    学習中のlossについてlive plotする
    '''

    def on_train_begin(self, logs={}):
        '''
        訓練開始時に実施
        '''
        self.epoch_cnt = 0      # epochの回数を初期化
        plt.axis([0, self.epochs, 0, 0.25])
        plt.ion()               # pyplotをinteractive modeにする

    def on_train_end(self, logs={}):
        '''
        訓練修了時に実施
        '''
        plt.ioff()              # pyplotのinteractive modeをoffにする
        plt.legend(['loss', 'val_loss'], loc='best')
        plt.show()

    def on_epoch_end(self, epoch, logs={}):
        '''
        epochごとに実行する処理
        '''
        loss = logs.get('loss')
        val_loss = logs.get('val_loss')
        x = self.epoch_cnt
        # epochごとのlossとval_lossをplotする
        plt.scatter(x, loss, c='b', label='loss')
        plt.scatter(x, val_loss, c='r', label='val_loss')
        plt.pause(0.05)
        # epoch回数をcount up
        self.epoch_cnt += 1

```

基本的には、```on_train_begin()```にて、```plt.ion()```して、```on_train_end()```で、```plt.ioff()```する。

そして、```on_epoch_end()```で、epochの終わりごとに```plt.scatter()```するだけの、簡単なclassを定義して、そのinstanceを```model.fit()```の```callbacks=[]```に指定するだけ。

### ついでにデータも保存しておく

進捗状況をグラフに表示するついでに、epochごとの```accuracy```や```loss```についても保存しておきたいと思ったら、```keras.callbacks```に```CSVLogger```があるので、これで引数としてファイル名を指定しておくと、CSVファイルにepochごとの結果を保存してくれるので、後からExcel等でグラフを描き直してもイイ。

というワケで、次のようなコードを書くと、進捗状況をグラフ表示しながら、CSVに保存することができるようになる。

```Python
plot_losses = PlotLosses()
csv_logger = CSVLogger('trainlog.csv')

history = model.fit(x_train, y_train,
                    batch_size=batch_size, epochs=epochs,
                    verbose=1,
                    validation_data=(x_test, y_test),
                    callbacks=[plot_losses, csv_logger])
```

ちなみに、実行中のグラフはこんな感じ。

![Figure_1.png](https://qiita-image-store.s3.amazonaws.com/0/15553/d4a1e4ff-1dea-8ede-b4fb-12fff18c53ed.png "Figure_1.png")


----

# 本日のコード

というワケで、本日のコードの全貌。
ついでというワケではないけれど、コマンドライン引数で、epochの数とbatch_sizeを指定するようにした。

- https://github.com/fukuit/Python_SelfLearning/blob/master/tensorflow_lesson/mnist_cnn_keras.py



```Python
'''
Keras(+Tensorflow)でMNISTを実施する
学習中の進捗をグラフで表示する
'''

import keras
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten, Conv2D, MaxPooling2D
from keras.optimizers import RMSprop
from keras.callbacks import Callback, CSVLogger
from matplotlib import pyplot as plt
from sklearn.model_selection import train_test_split
import argparse


class PlotLosses(Callback):
    '''
    学習中のlossについてlive plotする
    '''

    def on_train_begin(self, logs={}):
        '''
        訓練開始時に実施
        '''
        self.epoch_cnt = 0      # epochの回数を初期化
        plt.axis([0, self.epochs, 0, 0.25])
        plt.ion()               # pyplotをinteractive modeにする

    def on_train_end(self, logs={}):
        '''
        訓練修了時に実施
        '''
        plt.ioff()              # pyplotのinteractive modeをoffにする
        plt.legend(['loss', 'val_loss'], loc='best')
        plt.show()

    def on_epoch_end(self, epoch, logs={}):
        '''
        epochごとに実行する処理
        '''
        loss = logs.get('loss')
        val_loss = logs.get('val_loss')
        x = self.epoch_cnt
        # epochごとのlossとval_lossをplotする
        plt.scatter(x, loss, c='b', label='loss')
        plt.scatter(x, val_loss, c='r', label='val_loss')
        plt.pause(0.05)
        # epoch回数をcount up
        self.epoch_cnt += 1


def plot_result(history):
    '''
    plot result
    全ての学習が終了した後に、historyを参照して、accuracyとlossをそれぞれplotする
    '''

    # accuracy
    plt.figure()
    plt.plot(history.history['acc'], label='acc', marker='.')
    plt.plot(history.history['val_acc'], label='val_acc', marker='.')
    plt.grid()
    plt.legend(loc='best')
    plt.title('accuracy')
    plt.savefig('graph_accuracy.png')
    plt.show()

    # loss
    plt.figure()
    plt.plot(history.history['loss'], label='loss', marker='.')
    plt.plot(history.history['val_loss'], label='val_loss', marker='.')
    plt.grid()
    plt.legend(loc='best')
    plt.title('loss')
    plt.savefig('graph_loss.png')
    plt.show()


def main(epochs=5, batch_size=128):
    '''
    MNISTの学習とその結果の表示
    @args:
        epochs: epochの回数
        batch_size: ミニバッチのサイズ
    '''

    # load MNIST data
    (x_train, y_train), (x_test, y_test) = mnist.load_data()
    x_train1, x_valid, y_train1, y_valid = train_test_split(x_train, y_train, test_size=0.175)
    x_train = x_train1
    y_train = y_train1

    x_train = x_train.reshape(x_train.shape[0], 28, 28, 1).astype('float32')/255
    x_valid = x_valid.reshape(x_valid.shape[0], 28, 28, 1).astype('float32')/255
    x_test = x_test.reshape(x_test.shape[0], 28, 28, 1).astype('float32')/255

    # convert one-hot vector
    y_train = keras.utils.to_categorical(y_train, 10)
    y_valid = keras.utils.to_categorical(y_valid, 10)
    y_test = keras.utils.to_categorical(y_test, 10)

    # create model
    model = Sequential()
    model.add(Conv2D(32, kernel_size=(3, 3), activation='relu', input_shape=(28, 28, 1)))
    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(10, activation='softmax'))

    model.compile(loss='categorical_crossentropy',
                  optimizer=RMSprop(),
                  metrics=['accuracy'])

    print(model.summary())

    # callback function
    plot_losses = PlotLosses()      # グラフ表示(live plot)
    plot_losses.epochs = epochs
    csv_logger = CSVLogger('trainlog.csv')

    # train
    history = model.fit(x_train, y_train,
                        batch_size=batch_size, epochs=epochs,
                        verbose=1,
                        validation_data=(x_valid, y_valid),
                        callbacks=[plot_losses, csv_logger])

    # result
    score = model.evaluate(x_test, y_test, verbose=0)
    print('Test loss: {0}'.format(score[0]))
    print('Test accuracy: {0}'.format(score[1]))

    plot_result(history)


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='MNIST')
    parser.add_argument('--epochs', dest='epochs', type=int, help='size of epochs')
    parser.add_argument('--batch_size', dest='batch_size', type=int, help='size of batch')
    args = parser.parse_args()
    if args.epochs:
        epochs = args.epochs
    else:
        epochs = 5
    if args.batch_size:
        batch_size = args.batch_size
    else:
        batch_size = 128

    main(epochs, batch_size)
```

![graph_accuracy.png](https://qiita-image-store.s3.amazonaws.com/0/15553/5f6addce-33be-16b8-2905-016e9f4a2bb0.png "graph_accuracy.png")

![graph_loss.png](https://qiita-image-store.s3.amazonaws.com/0/15553/975e50f0-9adc-35b2-6aba-4b31b09f3dfe.png "graph_loss.png")