ゼロからの画像認識②（CNNの理解と実践）



:::note
前回の直後に書き、下書きのまま放置されていた記事です。
:::

前回は、TensorFlow版「Hello World」の解説と自筆文字の判定を行いました。

https://qiita.com/dolce_itf/items/be85244a31654d1d56ef

今回は、畳み込みニューラルネットワーク（CNN: convolutional neural network）の理解と実装をし、自筆文字の判定精度を上げます。

参考：[定番のConvolutional Neural Networkをゼロから理解する - DeepAge](https://deepage.net/deep_learning/2016/11/07/convolutional_neural_network.html)

## 理論

### 1. 視覚の生理学
この項は、着想の話をします。難しかったら次の項へ進んでも構いません。そもそもニューラルネットワークは神経のモデル化を試みたものでした。CNNも視覚情報処理をヒントとしています。脳の一次視覚野には、単純型細胞・複雑型細胞・超複雑型細胞という細胞が見つかっています。

* 網膜：境界を抽出する。オン領域とオフ領域の組み合わせで判断している。
* 単純型細胞：特定位置・特定傾きの線分を認識する。オン領域とオフ領域の組み合わせで判断している。
* 複雑型細胞：位置がズレても特定傾きの線分を認識する。複数の単純型細胞から入力をもらうと実現可能。（実物は未解明）
* 超複雑型細胞：さらに複雑な形に対応。複数の複雑型細胞から入力をもらうと実現可能。（実物は未解明）

参考１：[第一次視覚野：線の傾きを検出する細胞 - 脳の世界](http://web2.chubu-gu.ac.jp/web_labo/mikami/brain/26/index-26.html)
参考２：[花沢 明俊, 視覚心理 (第2回), 映像情報メディア学会誌, 2004, 58巻, 2号, p.199-204. - J-STAGE](https://www.jstage.jst.go.jp/article/itej1997/58/2/58_2_199/_article/-char/ja/)
参考３：[Deep learningで画像認識②〜視覚野と畳み込みニューラルネットワーク〜 - IMACEL Academy](https://lp-tech.net/articles/frQNo)

### 2. 畳み込み と プーリング

網膜と単純型細胞の機能は畳み込みによって、複雑型細胞の機能はプーリングによって実現できます。
#### 畳み込み Convolution
畳み込みとは、ある範囲に対し特徴抽出を行う操作です。カーネル（フィルターともいう）との内積を一通り、スライドしながら計算します。このカーネルが視覚の受容野に相当します。

![畳み込み.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/650522/7456f32a-088b-6063-ddc8-74aa1725e898.png)

このままだと端の特徴を捉えられないので、端にデータを増やしてあげます。これをパディングと言います。MNISTでは端が0なので、ゼロパディングだとちょうどよさそうですね。

![ゼロパディング.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/650522/66740d02-4637-94a2-f771-77ec649b839a.png)

パディングを行ったので、出力の形は小さくなりません。

#### プーリング Pooling
プーリングとは、範囲内での代表値を計算する作業。

![最大値プーリング.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/650522/74555b30-2f9d-6f9d-2314-8e0be9b81199.png)

ちょっとぐらい位置がズレても対応できるようになります。本番は、このあとに活性化関数で次の層への出力を決めます。


## 実践

```python:ライブラリの読み込み
import tensorflow as tf
```

```python:データの読み込みと整形
mnist = tf.keras.datasets.mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()

# (60000枚,縦28,横28)->(60000枚,縦28,横28,1色)
x_train = x_train.reshape((60000,28,28,1))

# (10000枚,縦28,横28)->(10000枚,縦28,横28,1色)
x_test = x_test.reshape((10000,28,28,1))

x_train, x_test = x_train/255.0, x_test/255.0
```

MNISTのデータは(枚,縦,横)という形のテンソル[^テンソル]ですが、次に出てくるConv2D層に入れる前に(枚,縦,横,色)という形に変えなければなりません。`reshape(枚,縦,横,色)`をする必要があります[^reshape]。

```python:モデル作成
model = tf.keras.models.Sequential([
    tf.keras.layers.Conv2D(32, (3, 3),padding='same', activation='relu',
                            input_shape=(28, 28, 1)),
    tf.keras.layers.MaxPooling2D((2, 2)),
    tf.keras.layers.Conv2D(64, (3, 3), activation='relu'),
    tf.keras.layers.MaxPooling2D((2, 2)),
    tf.keras.layers.Conv2D(64, (3, 3), activation='relu'),

    tf.keras.layers.Flatten(),
    
    tf.keras.layers.Dense(64, activation='relu'),
    tf.keras.layers.Dropout(0.25),
    tf.keras.layers.Dense(10, activation='softmax')
])
```
モデルの層をそれぞれ作ってます。

`Conv2D(32, (3, 3), padding='same', activation='relu', input_shape=(28, 28, 1))`
(縦28,横28,1色)を、ゼロパディングした上で畳み込みします。3×3(×1色)のカーネルを32種類使って、活性化関数はReLU。(縦28,横28,32種類)のテンソルになりました。MNISTは端が0の画像なので、ゼロパディングが妥当です。

`MaxPooling2D((2, 2))`
(縦28,横28,32種)を2×2で最大値プーリングします。(縦14,横14,32種)になりました。

`Conv2D(64, (3, 3), activation='relu')`
(縦14,横14,32種)を、パディングせず畳み込みします。3×3(×32)のカーネルを64種類使って、活性化関数はReLU。(縦12,横12,64種)になりました。

`MaxPooling2D((2, 2))`
(縦12,横12,64種)を2×2で最大値プーリングします。(縦6,横6,64種)になりました。

`Conv2D(64, (3, 3), activation='relu')`
(縦6,横6,64種) → (縦4,横4,64種)

`Flatten()`
(縦4,横4,64枚) → (1024,)

`Dense(64, activation='relu')`
(1024,) → (64,)

`Dropout(0.25)`
25%、つまり16個を0にする。

`Dense(10, activation='softmax')`
「0」〜「9」の10個の神経細胞に接続。


[^テンソル]:スカラーを横に並べたものをベクトル、ベクトルを縦に並べたものを行列、行列を奥に並べたものを3次元テンソルと呼びます。例えば、行列`[[a,b],[c,d]]`を2つ並べた3次元テンソルは`[ [[a,b],[c,d]], [[a,b],[c,d]] ]`です。MNISTのデータは「画像の行列を並べたもの」であり、このような形式になっています。

[^reshape]:(枚,縦,横,色)のデータセットは、(縦,横,色)の画像を並べた配列です。本文中のreshape操作では、`[ [a,b], [c,d] ]` → `[ [[a],[b]], [[c],[d]] ]`のように各ピクセルの値`a`を`[a]`へ変えました。これは、RGB画像を使いたいときに、各ピクセルが`[aR,aG,aB]`のようなデータを扱うための仕様です。