新PCが届いたので動作確認がてら、畳み込みニューラルネットワークで画像を分類

### 前置き
久しぶりになってしまいました。

GPU搭載PCを購入して届いたので、GPGPUのテストのために、MNISTを解く畳み込みニューラルネットワーク(Convolutional Neural Network)を組んだので、記事にしておきます。
MNISTなので、入力は(n, 28, 28, 1)で出力はOne-Hot行列で10次元(n, 10)です。
この間を畳み込みやプーリングで局所的な特徴を捉えたり、情報圧縮を図って分類精度を確保するのがCNNです。

以前、単層パーセプトロンで同じMNISTに取り組みましたが、あれは画像を縦横二次元の画像として扱えていなかったり、非線形性を持てない表現力で精度の限界がすぐにやってきてしまいますが、こちらの精度はあんな物ではありません。
とは言え、GPUによるアクセラレーションができるようになったことを確認できたらいいので、あんまり突き詰めるつもりはありません。
>[Kerasでディープラーニングに入門する（MNISTの手書き数字問題を単層パーセプトロンで解く）](https://qiita.com/saiaron/items/91e5c470ef5e82d985d9)

### 環境
PCやライブラリの環境は下記を構築しました。
バージョンの互換性合わせとかで半日ぐらいかかってしまいました。

```
Windows 10
CUDA Toolkit 9.0
cudnn 7.3.1.20
Python 3.6.1
Keras 2.2.4 (Tensorflow 1.12.0 backend)
```

### ソースコード全文
また先にソースコード全文です。
データ用意、モデル作って、学習させて、最後に評価。今回は間違えたデータの可視化までしました。
間違えたデータの共通点からモデルの改善点を見つけましょう。

```python
# -*- coding: utf-8 -*-
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import keras.backend as K
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Conv2D, MaxPool2D, Activation, Flatten, Dense, BatchNormalization
from keras.utils import to_categorical
from sklearn.metrics import classification_report, confusion_matrix

def conv_block(instance, f, fsize, pool):
    instance.add(Conv2D(f, fsize))
    instance.add(BatchNormalization())
    instance.add(Activation('relu'))
    if pool != None:
        instance.add(MaxPool2D(pool))

def cnn_model():
    ret = Sequential()
    
    ret.add(Conv2D(32, (5, 5), input_shape=(size_h, size_w, size_c)))
    ret.add(BatchNormalization())
    ret.add(Activation('relu'))
    
    for f, p in zip([32, 64, 64, 128, 128], [(2, 2), None, (2, 2), None, (2, 2)]):
        conv_block(ret, f, (2, 2), p)
        
    ret.add(Flatten())
    ret.add(Dense(64))
    ret.add(Activation('relu'))
    
    ret.add(Dense(10))
    ret.add(Activation('softmax'))
    
    ret.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['acc'])
    print(ret.summary())
    
    return ret
    
if __name__ == '__main__':
    size_h, size_w, size_c = 28, 28, 1
    (x_train, y_train), (x_test, y_test) = mnist.load_data()
    x_train = x_train.reshape(-1, size_h, size_w, size_c) / 255.
    x_test = x_test.reshape(-1, size_h, size_w, size_c) / 255.
    y_train_cate = to_categorical(y_train)
    y_test_cate = to_categorical(y_test)
    
    K.clear_session()
    dnn = cnn_model()
    e_ = 10
    b_ = 1024
    dnn.fit(x_train, y_train_cate, epochs=e_, batch_size=b_, validation_data=(x_test, y_test_cate))
    
    p_train = dnn.predict_classes(x_train)
    p_test = dnn.predict_classes(x_test)
    cm_train = pd.DataFrame(confusion_matrix(y_train, p_train), columns=np.arange(0, 10))
    cm_train.index = np.arange(0, 10)
    cm_test = pd.DataFrame(confusion_matrix(y_test, p_test), columns=np.arange(0, 10))
    cm_test.index = np.arange(0, 10)
    print('Train Data Confusion Matrix')
    print(cm_train)
    print('Classification Report')
    print(classification_report(y_train, p_train))
    print('')
    print('Test Data')
    print(cm_test)
    print('Classification Report')
    print(classification_report(y_test, p_test))
    
    e_train = x_train[y_train != p_train]
    tl_train = y_train[y_train != p_train]
    pl_train = p_train[y_train != p_train]
    e_test = x_test[y_test != p_test]
    tl_test = y_test[y_test != p_test]
    pl_test = p_test[y_test != p_test]
    
    pcnt = 1
    fig = plt.figure()
    if len(e_train) > 0:
        for i in range(len(e_train[:16])):
            fig.add_subplot(4, 8, pcnt)
            plt.imshow(e_train[i].reshape(size_h, size_w), cmap='binary')
            plt.xticks([])
            plt.yticks([])
            plt.title('Train Data\nTrue:' + str(tl_train[i]) + ';Predict:' + str(pl_train[i]))
            pcnt += 1
    if len(e_test) > 0:
        for i in range(len(e_test[:32-pcnt+1])):
            fig.add_subplot(4, 8, pcnt)
            plt.imshow(e_test[i].reshape(size_h, size_w), cmap='binary')
            plt.xticks([])
            plt.yticks([])
            plt.title('Test Data\nTrue:' + str(tl_test[i]) + ';Predict:' + str(pl_test[i]))
            pcnt += 1
```

### 解説
#### ライブラリのインポート
```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import keras.backend as K
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Conv2D, MaxPool2D, Activation, Flatten, Dense, BatchNormalization
from keras.utils import to_categorical
from sklearn.metrics import classification_report, confusion_matrix
```
*Conv2D*が畳み込み層、*MaxPool2D*が最大プーリング層です。
他や詳しいことはKerasのリファレンスを読むのがよいと思います。
*BatchNormalization*は入力値を平均0標準偏差1になるように正規化します。
これを全結合層や畳み込み層の後に入れることで、精度向上や収束の高速化が繋がります（当然、絶対にではない）。
分類問題としてのRecallやPrecisionなどの精度指標を見ようと思いますので、
scikit-learnの*classification_report*と*confusion_matrix*もインポートします。

#### モデル構築
```python
def conv_block(instance, f, fsize, pool):
    instance.add(Conv2D(f, fsize))
    instance.add(BatchNormalization())
    instance.add(Activation('relu'))
    if pool != None:
        instance.add(MaxPool2D(pool))

def cnn_model():
    ret = Sequential()
    
    ret.add(Conv2D(32, (5, 5), input_shape=(size_h, size_w, size_c)))
    ret.add(BatchNormalization())
    ret.add(Activation('relu'))
    
    for f, p in zip([32, 64, 64, 128, 128], [(2, 2), None, (2, 2), None, (2, 2)]):
        conv_block(ret, f, (2, 2), p)
        
    ret.add(Flatten())
    ret.add(Dense(64))
    ret.add(Activation('relu'))
    
    ret.add(Dense(10))
    ret.add(Activation('softmax'))
    
    ret.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['acc'])
    print(ret.summary())
    
    return ret
```
モデルはこんな感じですが、ループで構築しちゃったりしてるので、print(ret.summary())の標準出力の方がデータがどのように変形していくのかわかりやすいので、そちらを載せておきます。

```
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 24, 24, 32)        832       
_________________________________________________________________
batch_normalization_1 (Batch (None, 24, 24, 32)        128       
_________________________________________________________________
activation_1 (Activation)    (None, 24, 24, 32)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 23, 23, 32)        4128      
_________________________________________________________________
batch_normalization_2 (Batch (None, 23, 23, 32)        128       
_________________________________________________________________
activation_2 (Activation)    (None, 23, 23, 32)        0         
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 11, 11, 32)        0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 10, 10, 64)        8256      
_________________________________________________________________
batch_normalization_3 (Batch (None, 10, 10, 64)        256       
_________________________________________________________________
activation_3 (Activation)    (None, 10, 10, 64)        0         
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 9, 9, 64)          16448     
_________________________________________________________________
batch_normalization_4 (Batch (None, 9, 9, 64)          256       
_________________________________________________________________
activation_4 (Activation)    (None, 9, 9, 64)          0         
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 4, 4, 64)          0         
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 3, 3, 128)         32896     
_________________________________________________________________
batch_normalization_5 (Batch (None, 3, 3, 128)         512       
_________________________________________________________________
activation_5 (Activation)    (None, 3, 3, 128)         0         
_________________________________________________________________
conv2d_6 (Conv2D)            (None, 2, 2, 128)         65664     
_________________________________________________________________
batch_normalization_6 (Batch (None, 2, 2, 128)         512       
_________________________________________________________________
activation_6 (Activation)    (None, 2, 2, 128)         0         
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 1, 1, 128)         0         
_________________________________________________________________
flatten_1 (Flatten)          (None, 128)               0         
_________________________________________________________________
dense_1 (Dense)              (None, 64)                8256      
_________________________________________________________________
activation_7 (Activation)    (None, 64)                0         
_________________________________________________________________
dense_2 (Dense)              (None, 10)                650       
_________________________________________________________________
activation_8 (Activation)    (None, 10)                0         
=================================================================
Total params: 138,922
Trainable params: 138,026
Non-trainable params: 896
_________________________________________________________________
None
```

#### データの用意
```python
if __name__ == '__main__':
    size_h, size_w, size_c = 28, 28, 1
    (x_train, y_train), (x_test, y_test) = mnist.load_data()
    x_train = x_train.reshape(-1, size_h, size_w, size_c) / 255.
    x_test = x_test.reshape(-1, size_h, size_w, size_c) / 255.
    y_train_cate = to_categorical(y_train)
    y_test_cate = to_categorical(y_test)
```

ここからmainです。
といってもここはデータの用意だけです。
MNISTの訓練用6万件のデータと検証用1万件データを取り込みます。
Kerasの畳み込み層は色のチャンネルが必須とされているので、グレースケール画像でも最後に1というチャンネルが必要ですので、これをNumpyの*reshape*で付け加えます。
目的変数のデータはOne-Hot行列に変換しますが、後で使うscikit-learnの方はOne-Hot行列を想定していないので、One-Hot行列の方は別変数に確保しておきます。

#### 学習
```python
    K.clear_session()
    dnn = cnn_model()
    e_ = 10
    b_ = 1024
    dnn.fit(x_train, y_train_cate, epochs=e_, batch_size=b_, validation_data=(x_test, y_test_cate))
```
ここは特に解説はないです。
・・・しいて言うとMNISTは似たようなデータが多いので、バッチサイズは大きめに取った方が良いです。
学習中の標準出力の最初の方と最後の方を抜粋しておきます。

```
Train on 60000 samples, validate on 10000 samples
Epoch 1/10
60000/60000 [==============================] - 7s 124us/step - loss: 0.3911 - acc: 0.8879 - val_loss: 0.1085 - val_acc: 0.9674
Epoch 2/10
60000/60000 [==============================] - 6s 105us/step - loss: 0.0589 - acc: 0.9832 - val_loss: 0.0657 - val_acc: 0.9814
Epoch 3/10
60000/60000 [==============================] - 6s 105us/step - loss: 0.0330 - acc: 0.9912 - val_loss: 0.0556 - val_acc: 0.9829

--------------------------------

Epoch 8/10
60000/60000 [==============================] - 6s 104us/step - loss: 0.0025 - acc: 0.9999 - val_loss: 0.0310 - val_acc: 0.9901
Epoch 9/10
60000/60000 [==============================] - 6s 105us/step - loss: 0.0015 - acc: 1.0000 - val_loss: 0.0292 - val_acc: 0.9911
Epoch 10/10
60000/60000 [==============================] - 6s 105us/step - loss: 0.0011 - acc: 1.0000 - val_loss: 0.0288 - val_acc: 0.9922
```

#### 評価
```python
p_train = dnn.predict_classes(x_train)
    p_test = dnn.predict_classes(x_test)
    cm_train = pd.DataFrame(confusion_matrix(y_train, p_train), columns=np.arange(0, 10))
    cm_train.index = np.arange(0, 10)
    cm_test = pd.DataFrame(confusion_matrix(y_test, p_test), columns=np.arange(0, 10))
    cm_test.index = np.arange(0, 10)
    print('Train Data Confusion Matrix')
    print(cm_train)
    print('Classification Report')
    print(classification_report(y_train, p_train))
    print('')
    print('Test Data')
    print(cm_test)
    print('Classification Report')
    print(classification_report(y_test, p_test))
    
    e_train = x_train[y_train != p_train]
    tl_train = y_train[y_train != p_train]
    pl_train = p_train[y_train != p_train]
    e_test = x_test[y_test != p_test]
    tl_test = y_test[y_test != p_test]
    pl_test = p_test[y_test != p_test]
    
    pcnt = 1
    fig = plt.figure()
    if len(e_train) > 0:
        for i in range(len(e_train[:16])):
            fig.add_subplot(4, 8, pcnt)
            plt.imshow(e_train[i].reshape(size_h, size_w), cmap='binary')
            plt.xticks([])
            plt.yticks([])
            plt.title('Train Data' + str(i+1) + '\nTrue:' + str(tl_train[i]) + ';Predict:' + str(pl_train[i]))
            pcnt += 1
    if len(e_test) > 0:
        for i in range(len(e_test[:32-pcnt+1])):
            fig.add_subplot(4, 8, pcnt)
            plt.imshow(e_test[i].reshape(size_h, size_w), cmap='binary')
            plt.xticks([])
            plt.yticks([])
            plt.title('Test Data' + str(i+1) + '\nTrue:' + str(tl_test[i]) + ';Predict:' + str(pl_test[i]))
            pcnt += 1
```
ここは評価をしやすいようにデータを可視化したり行列化したりしています。
scikit-learnの*classification_report*と*confusion_matrix*はこんな感じです。

```
Train Data Confusion Matrix
      0     1     2     3     4     5     6     7     8     9
0  5923     0     0     0     0     0     0     0     0     0
1     0  6742     0     0     0     0     0     0     0     0
2     0     0  5958     0     0     0     0     0     0     0
3     0     0     0  6131     0     0     0     0     0     0
4     0     0     0     0  5842     0     0     0     0     0
5     0     0     0     0     0  5421     0     0     0     0
6     0     0     0     0     0     0  5918     0     0     0
7     0     0     0     0     0     0     0  6265     0     0
8     0     0     0     0     0     0     0     0  5851     0
9     0     0     0     0     0     0     0     0     0  5949
Classification Report
              precision    recall  f1-score   support

           0       1.00      1.00      1.00      5923
           1       1.00      1.00      1.00      6742
           2       1.00      1.00      1.00      5958
           3       1.00      1.00      1.00      6131
           4       1.00      1.00      1.00      5842
           5       1.00      1.00      1.00      5421
           6       1.00      1.00      1.00      5918
           7       1.00      1.00      1.00      6265
           8       1.00      1.00      1.00      5851
           9       1.00      1.00      1.00      5949

   micro avg       1.00      1.00      1.00     60000
   macro avg       1.00      1.00      1.00     60000
weighted avg       1.00      1.00      1.00     60000


Test Data
     0     1     2     3    4    5    6     7    8    9
0  974     0     1     0    0    1    2     1    0    1
1    0  1131     1     2    0    0    0     1    0    0
2    1     1  1026     0    0    0    0     4    0    0
3    0     0     1  1003    0    5    0     0    1    0
4    0     0     0     0  978    0    1     0    0    3
5    2     0     0     4    0  881    3     1    0    1
6    2     3     0     0    2    3  948     0    0    0
7    0     5     3     1    0    0    0  1018    1    0
8    1     0     2     1    1    1    0     0  966    2
9    2     0     0     1    6    1    0     2    0  997
Classification Report
              precision    recall  f1-score   support

           0       0.99      0.99      0.99       980
           1       0.99      1.00      0.99      1135
           2       0.99      0.99      0.99      1032
           3       0.99      0.99      0.99      1010
           4       0.99      1.00      0.99       982
           5       0.99      0.99      0.99       892
           6       0.99      0.99      0.99       958
           7       0.99      0.99      0.99      1028
           8       1.00      0.99      0.99       974
           9       0.99      0.99      0.99      1009

   micro avg       0.99      0.99      0.99     10000
   macro avg       0.99      0.99      0.99     10000
weighted avg       0.99      0.99      0.99     10000
```

訓練データは100%正解です。
検証データはちょっと間違っています。
間違ってしまったデータの可視化をmatplotlibで可視化しています。
結果はこちら。Trueが正解ラベル、Predictが予測ラベルです。
![Figure_1.png](https://qiita-image-store.s3.amazonaws.com/0/316366/6f39d12e-9c04-8461-40c1-4f94ac2dd329.png)
訓練データも間違っていたら出すようにしたんですが、ひとつも間違えていないので、全部検証データです。
MNISTのデータはトリッキーなやつが含まれていて、ここでもそれがよくわかります。
例えば1番と18番の7は1と、11番の8は9と、21番の0は6、28番の6は4と間違えてますが、見た目上無理も無いです。
逆にこれは正解してほしいというような6番みたいなのもあります。
ここでPrecisionやRecall等と見比べていくと、8という数字の覚え方が良さそうか良くなさそうか判断してモデルのチューニングをしていきます。訓練データが100%な点からも過学習しているかもしれませんね。

今回の目的上、これ以上は進めません。

### GPUアクセラレーションできたのか？
できました。
いつもSpyderを標準のIDEにして、IPythonから実行させていますが、これでTensorflowをバックエンドにKerasを使うと最初にGPUのメモリを取れるだけ取ってあとはずっと保持しちゃいます。
この事から、WindowsのタスクマネージャーからGPUパフォーマンスを見ると・・・
![TaskManager.png](https://qiita-image-store.s3.amazonaws.com/0/316366/aac53bec-520c-e50c-e614-d6a0ec0b42bd.png)
こんな感じに学習終了後も専用GPU使用料が張り付いています。
どのプロセスに取られているかは、同じくタスクマネージャーからプロセス一覧を見ても分かりますが、コマンドプロンプトでnvidia-smiコマンドがオススメです。

```
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 417.71       Driver Version: 417.71       CUDA Version: 10.0     |
|-------------------------------+----------------------+----------------------+
| GPU  Name            TCC/WDDM | Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  GeForce GTX 105... WDDM  | 00000000:01:00.0 Off |                  N/A |
| N/A   36C    P8    N/A /  N/A |   3351MiB /  4096MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|    0      9024      C   C:\ProgramData\Anaconda3\pythonw.exe       N/A      |
+-----------------------------------------------------------------------------+
```

下方のProcessesの所を見るとPythonだと分かります。
他にもGPUを使っているプロセスがあれば、一緒に表示されます。

なぜか、右上のCUDA Versionが10.0になってます・・・
確かに最初10.0をインストールしてしまって、Tensorflowがアウトで9.0に落としたんですが・・・
ついでなので、CUDAのバージョンとか見ておきましょう。
CUDAのバージョンを確認するにはnvcc -Vと実行すると見れます。

```
nvcc: NVIDIA (R) Cuda compiler driver
Copyright (c) 2005-2017 NVIDIA Corporation
Built on Fri_Sep__1_21:08:32_Central_Daylight_Time_2017
Cuda compilation tools, release 9.0, V9.0.176
```
やっぱり9.0じゃないか・・・
ちなみにcudnnの方のバージョンは、cudnnがただのソースコードなので、ソースを見るしかないです。

あと、nvidia-smiは一回打つと一回出るだけなので、繰り返させるためには

```
nvidia-smi -l 1
```
-lオプションの後に何秒おきに繰り返すか設定できます。
Ctrl+Cとかで割込み終了させます。

### 最後に
長くなってしまったので、今回はこの辺にしておこうと思います。
次からソースコードの解説の仕方をちょっと変えないと、毎回長くなってしまいます・・・・