画像データからのMNIST


## 概要
この記事ではkerasのdatasetsを使わずにmnistの手書き文字画像の分類を行いたいと思います。
[画像データ](https://www.kaggle.com/scolianni/mnistasjpg)はこちらのものを使用しています。
なお、機械学習初めて2週間くらいのクソザコなのでおかしな点はツッコミ入れて頂けるとありがたいです🙏

## 開発環境
MacOS Mojave 10.14
Python 3.6.3

## 環境構築
 Pythonは入っている前提で始めます。
まずkeras単体では学習を行えないのでバックエンドエンジンとなるtensorflowをインストールします。

[公式サイト](https://www.tensorflow.org/install/)の通りにやれば大丈夫です！

```shell
pip install tensorflow
```

次にkerasのインストールです、こちらも[公式サイト](https://keras.io/ja/#_2)通りにやりましょう。

```shell
sudo pip install keras
```

## 画像データの前処理
取ってきた[画像データ](https://www.kaggle.com/scolianni/mnistasjpg)を解凍すると以下のような構造になっています。

```shell
mnistasjpg
|--testSample
|--testSet.tar.gz
|--trainingSample
|--trainingSet.tar.gz
```
今回はこの中のtestSet.tar.gz、trainingSet.tar.gzを使います。

まずはこの２つを解凍しましょう

```shell
tar -xzvf testSet.tar.gz
tar -xzvf trainingSet.tar.gz
```

展開されたtestSetとtrainingSetには0,1などのディレクトリが入っておりその中に各トレーニング用の手書き文字が入っています。



また次のようなディレクトリ構造を想定して話を進めて行きます。

```shell
mnist
├── data
|     ├── testSet
|     ├── trainingSet
└── mnist.py
```
さて、次にディレクトリ内に入っている画像を学習しやすいように変換していきます。

```mnist.py
import numpy as np
from PIL import Image
import os

image_list = [] #トレーニングimage格納用list
label_list = [] #トレーニングlabel格納用list

train_path = "data/trainingSet"
test_path = "data/testSet"

for label in os.listdir(train_path):
  dir = train_path + "/" + label #各ディレクトリ名がラベル名になっているのでそれをトレーニングラベルに利用する。
  for filename in os.listdir(dir):
    label_list.append(label)
    image_path = dir + "/" + filename
    image = np.array(Image.open(image_path).convert("L").resize((28,28))) #画像をグレースケールで28x28のサイズに変換
    image_list.append(image/255.) # 255で割って正規化

```

上記のようにする事でimage_listにimageの情報が入った28x28の２次元numpy配列、image_labelにそのimageのラベルが入ります。

## 処理した画像をkerasで使えるようにする
このままkerasのmodelにリストを渡しても実行する事ができないので扱えるようにnumpy配列にする必要があります。

```mnist.py
image_list = np.array(image_list) # リストをnumpy配列に変換する。
label_list = np.array(label_list) # リストをnumpy配列に変換する。
label_list = np_utils.to_categorical(label_list)

(train_data, test_data, train_label, test_label) = train_test_split(image_list, label_list, test_size=0.3, random_state=111)
train_data = train_data.reshape(-1, 28, 28, 1)
test_data = test_data.reshape(-1, 28, 28, 1)
```

label_list = np_utils.to_categoricalで何をやっているか？
http://may46onez.hatenablog.com/entry/2016/07/14/122047
によると
>Kerasはラベルを数値ではなく、0or1を要素に持つベクトルで扱う

らしいのでlabel_listのnumpy配列を0,1の要素を持つベクトルに変換しているみたいですね。

## CNNの実装

今回は下記のようなモデルを実装して学習を行いました！
各ハイパーパラメーターなどは割と適当です🙏

```shell
Layer (type)                 Output Shape              Param #
=================================================================
conv2d_1 (Conv2D)            (None, 25, 25, 32)        544
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 22, 22, 64)        32832
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 11, 11, 64)        0
_________________________________________________________________
dropout_1 (Dropout)          (None, 11, 11, 64)        0
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 8, 8, 64)          65600
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 5, 5, 64)          65600
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 2, 2, 64)          0
_________________________________________________________________
dropout_2 (Dropout)          (None, 2, 2, 64)          0
_________________________________________________________________
dense_1 (Dense)              (None, 2, 2, 64)          4160
_________________________________________________________________
flatten_1 (Flatten)          (None, 256)               0
_________________________________________________________________
dense_2 (Dense)              (None, 10)                2570
=================================================================
```

これをコードで書くと次のようになります。

```mnist.py
batch_size = 128
epochs = 5
kernel_size = (4,4)
input_shape = train_data[0].shape

model = Sequential()

model.add(Conv2D(filters=32, kernel_size=kernel_size, input_shape=input_shape, activation="relu"))
model.add(Conv2D(filters=64, kernel_size=kernel_size, activation="relu"))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.2))
model.add(Conv2D(filters=64, kernel_size=kernel_size, activation="relu"))
model.add(Conv2D(filters=64, kernel_size=kernel_size, activation="relu"))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.25))
model.add(Dense(64, activation="relu"))
model.add(Flatten())
model.add(Dense(10, activation="softmax"))
model.summary()

model.compile(
    optimizer='adadelta',
    loss='categorical_crossentropy',
    metrics=['categorical_accuracy']
)

model.fit(train_data, train_label, batch_size=batch_size, epochs=epochs) # 学習させる
```

上でやった画像データの変換処理を間違えると

```shell
expected activation_2 to have shape (None, 10) but got array with shape (3, 1)
```
などのエラーが出るのでこれが出た場合はmodelではなく画像変換処理周りを見直してください。
ここで結構詰まりました

これでほぼ完成です！

## 結果

実際に上記のモデルをテストデータで検証したスコアは次のようになりました。

```mnist.py
scores = model.evaluate(test_data, test_label, verbose=1)

# モデル保存処理
json_string = model.to_json()
open('mnist.json', 'w').write(json_string)
model.save_weights('mnist.h5')

# スコアを標準出力に出力
print('Test loss:', scores[0])
print('Test accuracy:', scores[1])

```

結果

```shell
Test loss: 0.04206473699009548
Test accuracy: 0.988015873015873
```

実際mnistはどれくらいの正解率がないといけない....とかはわかりませんが
良いほうではないでしょうか？

ソースコードを見たい方はこちらです
https://github.com/yasuno0327/MyMNIST


次は理科大のアドベントカレンダーでGolangで機械学習する系の記事を上げる予定なのでクリスマス暇な方はぜひ見てくださいね！笑

参考
http://twosquirrel.mints.ne.jp/?p=20515