Tensorflow・kerasでCNNを構築して画像分類してみる（実装編２）

前回の記事[「Tensorflow・kerasでCNNを構築して画像分類してみる（実装編１）」](https://qiita.com/kazuboy/items/b768c08251c7ae5473e2)では、データのダウンロードとデータの確認をしました。
今回は、データの前処理、CNNの構築に進みたいと思います。

## 前提/環境
前提となる環境とバージョンは下記となります。
・Anaconda3
・Python3.7.7
・pip 20.0
・TensorFlow 2.0.0

この記事ではJupyter Notebookでプログラムを進めていきます。コードの部分をJupyter Notebookにコピー＆ペーストし実行することで同様の結果が得られるようにしています。

##実装　その１　データの前処理
前回の記事の最後に訓練データの画像の行列（32×32×3）について確認しました。その時値は下記のようなものでした。（一部抜粋）

[[[154 177 187]
[126 137 136]
[105 104 95]
...

これらの値を順伝播型ニューラルネットワークで行った正規化（各ピクセルのデータを0～1の間で扱えるようにする）を行います。

```python:code
x_train = x_train/255.
x_test = x_test/255.
```

この処理は評価用のデータ（x‗test）にも同様に行いましょう。処理を行ったあとのデータを確認します。

```python:code
print(x_train[1])
```

結果（一部抜粋）
[[[0.60392157 0.69411765 0.73333333]
  [0.49411765 0.5372549  0.53333333]
  [0.41176471 0.40784314 0.37254902]
  ...
となっていることがわかります。


次にラベルのデータセットについても処理をしましょう。順伝播型ニューラルネットワークの記事でも行いましたが、ラベルの値を1hotベクトルに変換します。

```python:code
from tensorflow.keras.utils import to_categorical

y_train = to_categorical(y_train,10)
y_test = to_categorical(y_test,10)
```
1hotベクトルに変換する場合には、to_categoricalメソッドを利用するのでした。今回も同様に処理を行っています。

ここまでの処理で前処理は終わりです。
ここでポイントなのは、順伝播型ニューラルネットワークとCNNで前処理で異なる点があります。それは、配列のreshapeを行わないということです。順伝播型ニューラルネットワークでは60000×784の形に行列を処理していました。
ですがCNNではそれを行いません。全記事でも書いたように処理してしまうと位置情報やその他の情報が失われるということがありました。CNNではそれを防ぐため多次元の配列のまま、畳み込み層などの処理を行っていくものです。

##実装　その２　ネットワーク構築

ネットワークを構築します。順伝播型ニューラルネットワークと同様、Sequentialを利用します。

```python:code
from tensorflow.keras.models import Sequential

model = Sequential()
```

次に畳み込み層を追加します。畳み込み層ではConv2Dレイヤーを利用します。

```python:code
from tensorflow.keras.layers import Conv2D

model.add(
   Conv2D(
       filters=32,
       input_shape=(32,32,3),
       kernel_size=(3,3),
       strides=(1,1),
       padding='same',
       activation='relu'
   )
)
```
Conv2レイヤーを追加する場合の引数は下記のようになります。

<dl>
  <dt>filters</dt>
  <dd> 整数で，出力空間の次元。これは畳み込みにおける出力フィルタの数</dd>
  <dt>input_shape</dt>
  <dd> 入力の行列。つまりは画像の行列になります。この記事でいうと32×32×3となります。 </dd>
  <dt>kernel_size</dt>
  <dd>2次元の畳み込みウィンドウの幅と高さを指定します。つまりはカーネル（フィルタ）のサイズを指定する形になります。 </dd>
  <dt>strides</dt>
  <dd> ストライドの指定です。畳み込み層の処理としてカーネルを移動させて計算を重ねますが、その移動する距離を示します。 </dd>
  <dt>padding</dt>
  <dd>パディングについて指定します。パディングは端の領域に0を埋め込みます。こうすることで出力画像を小さくしないという意味合いをもちます。出力サイズを小さくしない理由は層を深くするとサイズどんどん縮小することになります。そうした場合にはある一定の箇所で畳み込み計算ができなくなります。そういったことを避けるためパディングを行います。Conv2Dメソッドでは"valid"か"same"のどちらかを指定します。sameを指定すると入力と出力のサイズを等しくする効果があります。 </dd>
  <dt>activation</dt>
  <dd> 使用する活性化関数を指定します。何も指定しなければ，活性化は一切適用されません</dd>
</dl>

このレイヤーの指定では3×3のカーネル（フィルタ）を畳み込み時に利用するように指定しています。ストライドは1×1です。つまりこの例では一番左の位置でカーネルフィルタを重ね計算後、横方向に右に1ずらして計算を行います。最も右側に達した場合に、縦方向に1下げて計算をします。

特徴量マップの大きさはどのようになるでしょうか？（上記の例ではpaddingを指定しているので32になりますが、計算式を紹介してみます。
下記の計算式となります。
![特徴マップサイズの計算.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/659138/50d55523-7b32-864b-bca2-7bdef53a5728.png)

上記の計算式でパディングはなしで計算すると出力される特徴マップのサイズは30×30となります。今回の例ではpadding=sameを指定しているのでパディングを自動的に計算しています。計算上はパディングが1となります。

####引数filtersの意味
Conv2Dの引数でfiltersがありますが、こちらが何者であるかわかりずらいものがあります。端的に言うと出力される画像の枚数となります。
畳み込みの計算は、カーネルを重ね移動させながら計算を行って出力するというものでした。計算方法としては正しいです。このデータセットでは3チャンネルですので計算はチャンネル枚数毎に行われ加算され1枚が出力されます。

![出力フィルター.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/659138/07e1fc38-3d64-969b-5c28-0ac1ddec5cb6.png)

CNNの畳み込み層の重要な点は、出力（次の層に渡す入力）を複数にしていくことにあります。上記のようにチャンネル数1の出力から、複数の支出力を得る必要があります。
![出力フィルター2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/659138/890ce694-618f-d6bd-c33a-7d7f1abdaf9c.png)

この引数で設定する値は、上記図のFNにあたる奥行部分で、いくつ出力するかを示しています。

フィルター数に何を指定するのかというのは決まりはありません。ただし慣習として2のべき乗の数をとるということがあります。また層を重ねた際に特徴マップが1/2となった段階でフィルター数を2倍にするということもあるようです。

####カーネル（フィルタ）の指定
カーネルのサイズを3×3と指定しています。カーネルは奇数で指定されるのが慣習としてあります。

次にプーリング層を追加してみます。

```python:code
from tensorflow.keras.layers import MaxPooling2D

model.add(MaxPooling2D(2, 2))
```
プーリング層は上記の指定です。プーリングで指定するのはどの領域サイズでMAXプーリングを行うのかという指定です。上記のようにストライドを指定しない場合にはプーリングサイズの値が適用されます。
上記の引数は下記となります。
<dl>
  <dt>pool_size</dt>
  <dd> ダウンスケールする係数を決める整数（縦・横）のサイズです。 (2, 2) は画像をそれぞれの次元で半分になります。</dd>
</dl>

では、さらに層を重ねます。

```python:code
model.add(
   Conv2D(
       filters=64,
       kernel_size=(3,3),
       strides=(1,1),
       padding='same',
       activation='relu'
   )
)

model.add(MaxPooling2D(2, 2))
```
上記では畳み込み層のfiltersに64を指定しています。これはひとつ前のプーリング層で出力される特徴量マップのサイズが1/2になっているという計算なので2倍（32×2）としています。さらにプーリング層を追加しています。

ではここで現状のモデルの層がどのようになっているかを確認します。

```python:code
model.summary()
```

結果
![サマリー結果.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/659138/d68fa68a-170e-d5de-dede-ad07a891ba44.png)

モデルの構造を表示することができます。各項目の意味は下記となります。
<dl>
  <dt>Layer(type)</dt>
  <dd>層の情報です。どの種類の層であるかわかります。</dd>
  <dt>Output Shape</dt>
  <dd>各層から出力される行列の情報です。縦・横サイズと出力した特徴マップの次元が表示されます。上記の第一層のConv2Dレイヤーでは、 filtersに32を指定していたので、出力は32となっています。またプーリング層を見るとサイズが1/2となっており16×16となっていることがわかります。</dd>
<dt>Param</dt>
<dd>パラメーター数です。1層目は896となっています。この計算は
（前層のチャンネル数）×（フィルターサイズ）×（後の層への出力数)＋バイアス（後の層に対するバイアスの数）
上記の計算に当てはめると
最初の層は、前層チャンネル数は　3、フィルターサイズは（3×3）なので9、出力するのはfiltersで指定している32、そしてそのfiltersの値にそれぞれバイアスがあるとして32
つまりは、3×（3×3）×32+32=896となります。
3層目の畳み込み層は、前層チャンネル数は32、フィルターサイズは3×3なので9、出力は64です。そこにバイアスは64となるので32×（3×3）×64+64=18496となります。</dd>
</dl>

##まとめ
CNNの実装の第一弾を行いました。ポイントとしては
・CNNは順伝播型ニューラルネットワークとことなり多次元配列のまま扱う
・畳み込み層では計算結果として複数の層を出力して次の層へ渡す
という点になります。
実装はkerasの機能を使うと複雑なコードを書くことなく実現できますが、畳み込み層の計算過程の概略などをつかみ、実際の行列をみながらコードを書いていくと理解がしやすいと思います。

次回は、全結合層の追加と訓練、モデルの評価などを行います。