無から始めるKeras 第6回

無から始める Keras のもくじは[こちら](https://qiita.com/Ishotihadus/items/6ecf5684c2cbaaa6a5ef)

# 前回のおさらい

[前回](https://qiita.com/Ishotihadus/items/b171272b954147976bfc)は MNIST という手書き数字画像のデータセットを用いて、実際に簡単なネットワークを作って数字認識をした。

手書き数字の認識なので、当然問題は 10 クラス分類になる。前回のプログラムでは 97.9% 程度の正解率を上げることができた。

# 畳み込みニューラルネットワーク

CNN: Convolutional Neural Network。

画像界隈では基本の技術。今回はこれを使って MNIST を認識していきたい。

もうこれはググればなんなのかわかるくらいどこにでも書いてある説明なので省きたい感じもするが、一応紹介する。

## 畳み込み

まずは畳み込みについて説明しておこう。

「畳み込み」というのはおおよそ「フィルタリング」にあたる。

![フィルタリングのイメージ](https://qiita-image-store.s3.amazonaws.com/0/78668/3f319efc-fe0c-1cc5-b474-9166319354ef.png)

フィルタをかけることによって、ぼかし（平滑化）とかシャープネス（鮮鋭化）とかができる一方で、輪郭抽出チックなこともできる。

このフィルタがなにができるのかは、このフィルタリングで使う畳み込みの係数によって決まる（上の画像でいう表の値）。

## CNN における畳み込み

畳み込みをする（フィルタをかける）ことによって、若干の位置や回転などの違いにも頑健になる、というのが一般的な説明（な気がする）。

ただ、個人的にはフィルタをかけることによって「特徴量抽出」を明示的に行える、というほうが正しいのではないかなと思っている。
CNN では畳み込み係数すらも学習できるから、目的に適したフィルタが得られ、特徴量抽出ができるということだと思う。

もう 1 つは、2 次元的な画像情報をそのまま使えることにある。単純な結合層では 1 次元に変形してしまう（というか 2 次元で入力しても同じである）ので、画像の縦横の概念が失われてしまう。
2 次元畳み込みの場合は、ある画素を中心として周囲 9 マスとか 25 マスとかで処理する。そうすることで画像の縦横という情報を残した上で学習できる。

## 畳み込み層とプーリング層

CNN では一般的に、畳み込み層とプーリング層を交互に繰り返す。

畳み込み層ではその名の通り画像にフィルタを畳み込む。
大抵は 1 つの入力に対して複数のフィルタをかける。すると 1 つの画像がフィルタの数分増えることになる（3 次元的な並び方をすることになる）。
フィルタを複数使うことで、様々な特徴量を一度に抽出することができる。

プーリング層では、前の畳み込み層で得られたフィルタ済みの画像を（おおよそ）ダウンサンプリングする。
これは位置・回転頑健性のためという説明が多いが、そもそもダウンサンプリングしないと次元数が大きくなりすぎるという理由もありそう。まあ入れると性能が上がるなら入れよう。
プーリング層ではいろいろなダウンサンプリング法を用いうるが、一番メジャーなのは MaxPooling（近傍の最大値を取る）である。

イメージとしては下のような感じ。もちろん、畳み込み→プーリングを何度してもいいし、プーリングをしなくてもいい。最後に結合層に入れるときは、1 次元配列に変形する必要がある。

![CNN のイメージ](https://qiita-image-store.s3.amazonaws.com/0/78668/d9261603-a5ce-33ee-8b67-007141956985.png)


# CNN を実際に組んでみる

## データ整備

前回と同様。ロードするだけ。便利。
前回と異なり入力画像は 2 次元のまま扱うので、reshape する必要がない。

```py3
from keras.datasets import mnist
from keras.utils import np_utils

(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train = x_train / 255.0
x_test = x_test / 255.0
y_train = np_utils.to_categorical(y_train, num_classes=10)
y_test = np_utils.to_categorical(y_test, num_classes=10)
```

## モデルの組み立て

かなり層の種類が増えてきた。

新しいのもあるので整理しておくと、

- Reshape: データの変形（任意次元に）
- Flatten: データの変形（1 次元に）
- Dense: 通常の結合層
- Conv2D: 畳み込み層
- MaxPooling2D: プーリング層
- Dropout: ドロップアウト層（途中のネットワークをランダムに切る）

最初の Reshape は、畳み込み層がチャネル次元を含むためにこうしている。
すなわちある 1 画素に対してベクトルを持てるようになっているため、この方向に 1 次元のベクトルを作ってやる必要があるのである。

Flatten 層は、出てきた 4 次元（フィルタ枚数×画像 2 次元×チャネル 1 次元）を 1 次元にするだけ。

```py3
from keras.models import Sequential
from keras.layers import Dense, Activation, Flatten, Dropout, Conv2D, MaxPooling2D, Reshape
from keras.callbacks import EarlyStopping

model = Sequential([
    Reshape((28, 28, 1), input_shape=(28, 28)),
    Conv2D(50, (5, 5), activation='relu'),
    Conv2D(50, (3, 3), activation='relu'),
    MaxPooling2D(pool_size=(2, 2)),
    Flatten(),
    Dropout(0.2),
    Dense(100, activation='relu'),
    Dropout(0.4),
    Dense(10, activation='softmax')
])
```

## 学習

層が増えてきて複雑になってきたので学習に若干時間がかかるかも。

```py3
model.compile('adam', 'categorical_crossentropy', metrics=['accuracy'])

es = EarlyStopping(monitor='val_acc')
model.fit(x_train, y_train, batch_size=100, validation_split=0.2, callbacks=[es])
```

## テスト

モデルに evaluate とかいう便利関数があったのを知ったので、それを使ってみる。

入力は学習時（fit 時）と同じ形のものを入れる。

戻り値は 2 つの数値の配列になっていて、[0] が損失（今回はクロスエントロピー）、[1] が正解率。

```py3
model.evaluate(x_test, y_test, verbose=0)

# => [0.03113893101061576, 0.98999999999999999]
```

正解率 99%。いいでしょう。

# 今回のプログラムまとめ

いじいじすれば 99.5% くらいの正解率は出せるはずなので、ヒマな人はチューニングしてみてね。

```py3
from keras.datasets import mnist
from keras.utils import np_utils
from keras.models import Sequential
from keras.layers import Dense, Activation, Flatten, Dropout, Conv2D, MaxPooling2D, Reshape
from keras.callbacks import EarlyStopping

(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train = x_train / 255.0
x_test = x_test / 255.0
y_train = np_utils.to_categorical(y_train, num_classes=10)
y_test = np_utils.to_categorical(y_test, num_classes=10)

model = Sequential([
    Reshape((28, 28, 1), input_shape=(28, 28)),
    Conv2D(50, (5, 5), activation='relu'),
    Conv2D(50, (3, 3), activation='relu'),
    MaxPooling2D(pool_size=(2, 2)),
    Flatten(),
    Dropout(0.2),
    Dense(100, activation='relu'),
    Dropout(0.4),
    Dense(10, activation='softmax')
])

model.compile('adam', 'categorical_crossentropy', metrics=['accuracy'])

es = EarlyStopping(monitor='val_acc')
model.fit(x_train, y_train, batch_size=100, validation_split=0.2, callbacks=[es])

model.evaluate(x_test, y_test, verbose=0)
```

次回はなにするか考えてません。