kerasを用いてMNISTの画像認識をする。

# kaggleで画像処理の練習としてMNISTの画像認識に参加した。

　前回、[kaggleのtitanicの生存者予測](https://qiita.com/ac-yasushi/items/aaa5aa7c8307986ebe78)をしたが、今回はもう少し進んで畳み込みを用いた画像認識をやってみる。
　お題となるのはこれ。

![MNIST.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/371781/b08f72b5-da0e-e1aa-69c9-a34ef493f9f6.png)

 画像の時点で見えているが、ここでスコアを99以上にするのが目標である。もう一つはkerasでの畳み込みの実装とモデルの結合の練習である。

**[githubにコードを置いておきます。](https://github.com/ac-yasushi/MNIST_digit)**


# 各モデルでのコードとスコア

## まずは単純にNNでやってみる。

### モデル部分

　MNISTは28×28px、0~255のグレースケール画像なので普通に0~1に規格化してから行列をそのままNNとして扱ってみる。

```python:NN_MNIST.ipynb
import keras
import tensorflow as tf

model=keras.Sequential([
    keras.layers.Flatten(input_shape=(28, 28)),
    keras.layers.Dense(280, activation=tf.nn.relu), #input distribution
    keras.layers.Dense(28, activation=tf.nn.relu),
    keras.layers.Dense(10, activation="softmax"),
])

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

fit=model.fit(pixels,labels,epochs=200,validation_split=0.2)
```

　NNなのでkerasのSequentialモデルでポンと書ける。のちのちCNNを書く中でこの簡単さはある意味シンプルなNNの強みだと感じた。softmaxで出力し、認識したサンプルがどの数字に合致するのかを確率で表す。


### 提出の形式
　実際のSubmitの段では0~9までの確率変数を取る確率の行列に対して、np.argmax()関数を用いて最大値をとる配列番号を格納して提出した。


```python:NN_MNIST.ipynb
number_prediction=np.array([])

for i in range(len(data)):
    number_prediction=np.append(number_prediction, np.argmax(data[i]))
    
number_prediction=number_prediction.astype(np.int64)
```

### モデル評価

　model.evaluate()でモデルを評価した結果を載せる。kerasの提出スコアもそこまで違わないので大体これが0.991程度になってくれればいいわけだ。

```
42000/42000 [==============================] - 2s 39us/step
Test_accuracy: 0.8864285714285715
```
　見ての通り、200epochで0.886．88.5%と言ったところである。モデルの実装時間を考えると悪くないように思えるが、人間でも95%以上の認識率は普通に出るのでまだ物足りない。

## 単純な形式のCNNを導入する。

### モデル部分

```python:MNIST-simple-CNN.ipynb
import keras
import tensorflow as tf

model=keras.Sequential([
    keras.layers.Conv2D(64, (3, 3), padding="same", activation=tf.nn.relu, input_shape=(28, 28, 1)),
    keras.layers.Conv2D(64, (3, 3), padding="same", activation=tf.nn.relu,),
    keras.layers.MaxPooling2D(pool_size=(2,2)),
    keras.layers.BatchNormalization(),
    
    keras.layers.Conv2D(128, (3, 3), padding="same", activation=tf.nn.relu),
    keras.layers.Conv2D(128, (3, 3), padding="same", activation=tf.nn.relu,),
    keras.layers.MaxPooling2D(pool_size=(2,2)),
    keras.layers.BatchNormalization(),
    
    keras.layers.Flatten(),    
    keras.layers.Dense(512, activation=tf.nn.relu),
    keras.layers.Dense(128, activation=tf.nn.relu),
    keras.layers.Dense(10, activation="softmax"),
])

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

fit=model.fit(pixels,labels,epochs=200,batch_size=16,validation_split=0.2)
```

　Con2Dレイヤーで畳み込み、プーリングはMaxPoolingを選択。BatchNormalization()を加えて、典型的なCNNといった感じになった。まあ、ある程度写経したので、この辺で特にコードに差が出るとは思えない。

　**Conv2Dの畳み込み層が何をしているのかは理解しているが、なんで畳み込み層を2~3層積み重ねるのか、そこのところはよく分からない。**

### 学習過程
![__results___CNN.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/371781/177dafdc-9a5b-bf46-d17f-6f6f8b57a520.png)

　labelを入れてなかったが、左がloss、右がaccとなる。オレンジ色がval_accなので実際のところは0.990ちょい下の能力である。

### スコア
　これで提出したところ。スコアは**0.98971**であった。

　NNに比べて10%程度向上していることが素晴らしい。~~これもう500epochくらい回せば0.99超えるのでは？？？~~

## モデルを結合してみる。

　モデルを組むためにMNISTの画像を捏ね繰り返してみたら、なんとなく面白そうな傾向があったのでモデルに追加してみる。

|画像|分布|
|---|---|
|![__results___17_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/371781/d0710e1e-03d2-d81f-bbe6-a39d79bbfc37.png)|![__results___22_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/371781/ab9b70ba-97bf-779f-8ca7-f39be5365817.png)|

　数字の8に対して、画像内でx軸y軸にピクセルを射影した分布を取ってみると数字ごとに特徴が出ている。これをNNで処理してモデルに入れてみる。

### モデル部分

```python:CNN_MNIST.ipynb
import keras
import tensorflow as tf

input_cnn=keras.layers.Input(shape=(28, 28, 1))
model_cnn=keras.layers.Conv2D(64, (3, 3), padding="same", activation=tf.nn.relu,)(input_cnn)
model_cnn=keras.layers.Conv2D(64, (3, 3), padding="same", activation=tf.nn.relu,)(model_cnn)
model_cnn=keras.layers.MaxPooling2D(pool_size=(2,2))(model_cnn)
model_cnn=keras.layers.BatchNormalization()(model_cnn)

model_cnn=keras.layers.Conv2D(128, (3, 3), padding="same", activation=tf.nn.relu)(model_cnn)
model_cnn=keras.layers.Conv2D(128, (3, 3), padding="same", activation=tf.nn.relu,)(model_cnn)
model_cnn=keras.layers.MaxPooling2D(pool_size=(2,2))(model_cnn)
model_cnn=keras.layers.BatchNormalization()(model_cnn)
model_cnn=keras.layers.Dropout(0.1)(model_cnn)

model_cnn=keras.layers.Conv2D(512, (3, 3), padding="same", activation=tf.nn.relu)(model_cnn)
model_cnn=keras.layers.Conv2D(512, (3, 3), padding="same", activation=tf.nn.relu,)(model_cnn)
model_cnn=keras.layers.MaxPooling2D(pool_size=(2,2))(model_cnn)
model_cnn=keras.layers.BatchNormalization()(model_cnn)
model_cnn=keras.layers.Dropout(0.1)(model_cnn)

model_cnn=keras.layers.Flatten()(model_cnn)
model_cnn=keras.layers.Dense(512, activation=tf.nn.relu)(model_cnn) #input distribution
model_cnn=keras.layers.Dense(256, activation=tf.nn.relu)(model_cnn)


input_distribution=keras.layers.Input(shape=(2, 28))
model_distribution=keras.layers.Dense(128, activation=tf.nn.relu)(input_distribution)
model_distribution=keras.layers.Dense(128)(model_distribution)
model_distribution=keras.layers.Flatten()(model_distribution)
model_distribution=keras.layers.Dropout(0.1)(model_distribution)


model_out=keras.layers.concatenate([model_cnn, model_distribution])
model_out=keras.layers.Dense(256, activation=tf.nn.relu)(model_out)
model_out=keras.layers.Dense(128, activation=tf.nn.relu)(model_out)
model_out=keras.layers.Dense(10, activation="softmax")(model_out)

model=keras.models.Model(inputs=[input_cnn, input_distribution], output=model_out)
```

 長いのでモデルのcompileとfit部分は割愛。ここで問題なのが、結合のモデルではSequential一つでは書けないということだ。結合するモデルを作成して(タイプを確認したらtensorflow.python.framework.ops.Tensor)、そこにどんどん層を追加していって、convatenate([])で結合、最終的なモデルはkeras.models.Model()に入力と出力を指定することで完了する。

### 学習過程

![__results___CNNplus.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/371781/dc419313-f1dd-2a27-844b-3554715ee0d0.png)

　**何にもかわんねえ！！**

### スコア

　スコアは**0.9889**。あってもなくても変わらないということだ。まあ、CNNのパラメーターの数に比べれば48個の分布なんてゴミですし……

## CNNとCNNを組み合わせる。

　CNNがある傾きや特徴に対して反応する特徴を持っているなら、一枚の画像からより多くの特徴を取り出して処理できるようにすればいいのでは？
　実際、画像の左右反転やいくらか傾けたり変形させることで汎化性能を上げるのはよくやる手法である。

　ここでは、画像を縦横で4等分にしてそれをCNNにかけて、先ほどのCNNモデルに結合してみる。

### モデル部分

```python:CNN-modifyed
import keras
import tensorflow as tf

input_cnn=keras.layers.Input(shape=(28, 28, 1))
model_cnn=keras.layers.Conv2D(64, (3, 3), padding="same", activation=tf.nn.relu,)(input_cnn)
model_cnn=keras.layers.Conv2D(64, (3, 3), padding="same", activation=tf.nn.relu,)(model_cnn)
model_cnn=keras.layers.MaxPooling2D(pool_size=(2,2))(model_cnn)
model_cnn=keras.layers.BatchNormalization()(model_cnn)

model_cnn=keras.layers.Conv2D(128, (3, 3), padding="same", activation=tf.nn.relu)(model_cnn)
model_cnn=keras.layers.Conv2D(128, (3, 3), padding="same", activation=tf.nn.relu)(model_cnn)
model_cnn=keras.layers.MaxPooling2D(pool_size=(2,2))(model_cnn)
model_cnn=keras.layers.BatchNormalization()(model_cnn)
model_cnn=keras.layers.Dropout(0.2)(model_cnn)

model_cnn=keras.layers.Conv2D(512, (3, 3), padding="same", activation=tf.nn.relu)(model_cnn)
model_cnn=keras.layers.Conv2D(512, (3, 3), padding="same", activation=tf.nn.relu,)(model_cnn)
model_cnn=keras.layers.MaxPooling2D(pool_size=(2,2))(model_cnn)
model_cnn=keras.layers.BatchNormalization()(model_cnn)
model_cnn=keras.layers.Dropout(0.2)(model_cnn)

model_cnn=keras.layers.Flatten()(model_cnn)
model_cnn=keras.layers.Dense(512, activation=tf.nn.relu)(model_cnn)


input_four=keras.layers.Input(shape=(4, 14, 14, 1))
model_four=keras.layers.Conv3D(64, (3, 3, 3), padding="same", activation=tf.nn.relu)(input_four)
model_four=keras.layers.Conv3D(64, (3, 3, 3), padding="same", activation=tf.nn.relu)(model_four)
model_four=keras.layers.MaxPooling3D(pool_size=(2,2,2))(model_four)
model_four=keras.layers.BatchNormalization()(model_four)
model_four=keras.layers.Dropout(0.2)(model_four)

model_four=keras.layers.Conv3D(64, (3, 3, 3), padding="same", activation=tf.nn.relu)(model_four)
model_four=keras.layers.Conv3D(64, (3, 3, 3), padding="same", activation=tf.nn.relu)(model_four)
model_four=keras.layers.MaxPooling3D(pool_size=(2,2,2))(model_four)
model_four=keras.layers.BatchNormalization()(model_four)
model_four=keras.layers.Dropout(0.2)(model_four)

model_four=keras.layers.Flatten()(model_four)
model_four=keras.layers.Dense(512, activation=tf.nn.relu)(model_four)


model_two_cnn=keras.layers.concatenate([model_cnn, model_four])
model_two_cnn=keras.layers.Dense(256, activation=tf.nn.relu)(model_two_cnn)


input_distribution=keras.layers.Input(shape=(2, 28))
model_distribution=keras.layers.Dense(128, activation=tf.nn.relu)(input_distribution)
model_distribution=keras.layers.Dense(128)(model_distribution)
model_distribution=keras.layers.Flatten()(model_distribution)
model_distribution=keras.layers.Dense(64)(model_distribution)
model_distribution=keras.layers.Dropout(0.2)(model_distribution)


model_out=keras.layers.concatenate([model_two_cnn, model_distribution])
model_out=keras.layers.Dense(256, activation=tf.nn.relu)(model_out)
model_out=keras.layers.Dropout(0.2)(model_out)
model_out=keras.layers.Dense(256, activation=tf.nn.relu)(model_out)
model_out=keras.layers.Dense(10, activation="softmax")(model_out)

model=keras.models.Model(inputs=[input_cnn,input_four, input_distribution], output=model_out)
```

　正直、長い。書き方自体は単調なのでただ層を積み重ねているイメージ。

### 学習過程

![__results___CNNmodi.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/371781/e9fe1a56-3b5b-63ca-7b06-b61d14d76607.png)

　やっぱりなんだか変わった感じがしない。結局、乖離している学習曲線(過学習)をどうにか狭めないといけないのか。

　しかしながらこのモデルだと150epoch後半から0.99を超えてきている。

### スコア

　スコアは**0.99057**。

　前述の画像反転や他の手法も試したいところだが、スコアが99%を超えたのでここでひと段落。

# まとめ

　まだまだCNNは奥が深いなあという印象。今回モデルの結合の書き方を覚えたので少し凝ったモデルでも作れるようになったのが上達したところだろうか？
　ただし、CNNになると学習に時間がかかりすぎるのでCNNに入ってからは実行環境はkaggleのカーネルやgoogle colabを用いた。