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

前回の記事[「Tensorflow・kerasでCNNを構築して画像分類してみる（実装編２）」](https://qiita.com/kazuboy/items/4ddd2db575f6fb9ec071)では、実際にCNNの構築を進めました。畳み込み層とプーリング層を重ねたところまで進めています。
今回は、全結合層の追加や訓練データを利用した訓練を進めたいと思います。

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

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

前回までは層を組み合わせ下記のようなCNNの構築をすすめました。
![サマリー結果.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/659138/5ac5c249-d16f-74de-632f-cc8f83589c9c.png)


##実装　その３　ネットワーク構築（全結合層の追加）

前回のsummaryの結果、最終的な層で出力される行列は(None,8,8,64)の四次元の配列の状態です。全結合層2次元の配列に変換しなければ扱えないので、モデルの追加の前処理を行います。

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

#2次元への変形
model.add(Flatten())

#変化形したあとの形状を確認
model.output_shape
```

結果
(None, 4096)

変換処理は終了しました。ここから全結合層を追加します。全結合層は順伝播型ニューラルネットワークの構築でも利用したDenseレイヤーを用います。

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

model.add(Dense(units=512, activation='relu'))
model.add(Dense(units=10, activation='softmax'))
```

Denesレイヤー追加時に引数を設定しています。unitsは出力する次元の数を表します。ここでは512の出力する数として指定しています。
activationはそれぞれのユニットの出力時に利用する活性化関数を指定するものです。今回はreluという関数を指定します。
input_shapeは入力される行列の形状をしていしますが、2層目以降でkerasが自動的に判断するので省略しています。

さらに出力層を追加します。こちらもDenseレイヤーを用います。順伝播型ニューラルネットワークの例でもありましたが、最終的に多クラス分類の問題です。ここではSoftmaxを活性化関数として分類することになります。



ここまでモデルを構築しました。ここからはモデルで実際に訓練をする前に最終的な構造を見ましょう。

```python:code
model.summary()
```
結果
![サマリー結果2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/659138/a199b502-a071-e69f-286f-46800c3a2886.png)


##実装　その４　モデルの訓練
まずは構築したモデルのコンパイルをします。

```python:code
model.compile(
    optimizer='adam',
    loss='categorical_crossentropy',
    metrics=['accuracy']
)
```
各引数について意味合いは下記となります。
<dl>
<dt>optimizer</dt>
<dd>optimizerを指定することで最適化が可能です。今回はAdam（Adaptive Moment Estimation)を設定しています。</dd>
<dt>loss</dt>
<dd>lossは損失関数を選択します。ここでは交差エントロピーを利用します。kerasではcategorical_crossentropyなどいくつか指定ができます。</dd>
<dt>metrics</dt>
<dd>評価関数の指定をするものです。ここではaccuracyを選択しています。評価関数も種類がありますので最適なものを利用しましょう。</dd>
</dl>


次にコンパイルをしたので訓練を進めましょう。

```python:code
model.fit(
    x_train,
    y_train,
    batch_size=32,
    epochs=5,
    validation_split=0.2
)
```
今回も訓練ではデータすべてを使うわけではありません、データセットの内50000枚を利用します。残りの10000枚のデータは未知のデータへの検証や予測のために利用します。

fit関数を用いるにあたり引数を指定しています。順伝播型ニューラルネットワークの構築と同様です。それぞれは下記の意味合いです。

<dl>
  <dt>x_train</dt>
  <dd> 訓練データの画像の行列に対応します。 </dd>
  <dt>y_train</dt>
  <dd> 訓練データのラベルに対応する行列に対応します。 </dd>
  <dt>batch_size</dt>
  <dd>整数またはNoneを指定します。動作としては設定したサンプル数ごとに勾配の更新を行います。指定しなければデフォルトで32で更新します。機械学習の原理として与えられた関数の傾き（勾配）を計算して、勾配の傾きが大きいほうへパラメーターをずらしていきます。この動きに対する指定となります。 </dd>
  <dt>epochs</dt>
  <dd> 整数で，モデルを訓練するエポック数を指定します。 </dd>
  <dt>validation_sprit</dt>
  <dd> 訓練データ全体を訓練にのみ利用するのではなく、一部を訓練中の検証用データとして用いる場合に設定します。kerasでは0から1までの浮動小数点数で指定します。この設定値は訓練データの中で検証データとして使う割合を示します。この記事では0.2を定していますが、訓練に使われるのは全体の8割、残りの2割は検証用データとして利用するということになります。ここで設定した検証データは訓練には利用されません。 </dd>
</dl>

結果
Train on 40000 samples, validate on 10000 samples
Epoch 1/5
40000/40000 [==============================] - 100s 3ms/sample - loss: 1.3853 - accuracy: 0.4956 - val_loss: 1.1217 - val_accuracy: 0.6030
Epoch 2/5
40000/40000 [==============================] - 104s 3ms/sample - loss: 0.9602 - accuracy: 0.6611 - val_loss: 0.9125 - val_accuracy: 0.6740
Epoch 3/5
40000/40000 [==============================] - 111s 3ms/sample - loss: 0.7617 - accuracy: 0.7332 - val_loss: 0.8462 - val_accuracy: 0.7027
Epoch 4/5
40000/40000 [==============================] - 110s 3ms/sample - loss: 0.6038 - accuracy: 0.7872 - val_loss: 0.8243 - val_accuracy: 0.7248
Epoch 5/5
40000/40000 [==============================] - 111s 3ms/sample - loss: 0.4530 - accuracy: 0.8400 - val_loss: 0.9037 - val_accuracy: 0.7179

結果の意味合いの再復習となりますがloss、accuracy、val_loss、val_accuracyそれぞれの意味合いは下記になります。

<dl>
  <dt>loss</dt>
  <dd>訓練データを用いた場合の損失関数の値。損失関数の値が減少することが望ましい</dd>
  <dt>accuracy</dt>
  <dd>訓練データに対しての分類精度（正解率）この値が高くなることが望ましい。</dd>
  <dt>val_loss</dt>
  <dd>訓練データの一部を検証用データとして利用した場合に得られる。検証用データ（未知のデータ）に対する損失関数の値</dd>
  <dt>val_accuracy</dt>
  <dd>訓練データの一部を検証用データとして利用した場合に得られる。検証用データ（未知のデータ）に対する正解率</dd>
</dl>


今回はエポック数を5として実行しました。訓練の結果、lossは減少し、accuracyは向上しています。ただし検証用データでの評価分ではval_loss、val_accuracyとも一定の範囲内に落ち着いており精度が向上しているなどの結果は得られていません。

##精度向上を目指して
結果として検証用データ（未知のデータ）に対する性能が低いという点をさらに改善できないかと思います。まず精度を向上する観点から訓練のエポック数を5から10に増やすという方向で検証しました。結果としては以下のように改善しないものでした。

結果（エポック8からエポック10の結果を抜粋）
Epoch 8/10
40000/40000 [==============================] - 104s 3ms/sample - loss: 0.0810 - accuracy: 0.9724 - val_loss: 1.8332 - val_accuracy: 0.7144
Epoch 9/10
40000/40000 [==============================] - 104s 3ms/sample - loss: 0.0641 - accuracy: 0.9795 - val_loss: 1.9246 - val_accuracy: 0.7138
Epoch 10/10
40000/40000 [==============================] - 106s 3ms/sample - loss: 0.0744 - accuracy: 0.9755 - val_loss: 1.9128 - val_accuracy: 0.7176

訓練データの訓練用のデータに対しての精度は向上していますが、val_loss、val_accuracyに至っては改善していないどころか悪化しています。この結果からモデルに対して改善する必要があると考えられます。

###改善点　その１
現状のモデルの構造をもう一度確認します。
![サマリー結果2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/659138/27642f64-db70-cb1c-cc22-e34e58c281d1.png)

ここで改善する点を考えてみました。
１．最初のConv2d層の後にmax_pooling2d層を設けているが、この段階で特徴マップが1/2になってしまっている。畳み込み層の出力数が少ない=つまり特徴量抽出が少ない上にプーリングの処理を行っているので精度が下がるのではないか？（上流で絞っているのでそれ以降は精度がそれほど上がらないのでと考えました。

なので__最初のConv2d層の後にさらに畳み込み層を追加する__ということで精度が上がらないかと考えました。

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

結果
Train on 40000 samples, validate on 10000 samples
Epoch 1/5
40000/40000 [==============================] - 247s 6ms/sample - loss: 1.3414 - accuracy: 0.5145 - val_loss: 1.0411 - val_accuracy: 0.6298
Epoch 2/5
40000/40000 [==============================] - 259s 6ms/sample - loss: 0.8765 - accuracy: 0.6918 - val_loss: 0.8250 - val_accuracy: 0.7126
Epoch 3/5
40000/40000 [==============================] - 258s 6ms/sample - loss: 0.6652 - accuracy: 0.7649 - val_loss: 0.7685 - val_accuracy: 0.7396
Epoch 4/5
40000/40000 [==============================] - 246s 6ms/sample - loss: 0.4960 - accuracy: 0.8256 - val_loss: 0.7721 - val_accuracy: 0.7479
Epoch 5/5
40000/40000 [==============================] - 255s 6ms/sample - loss: 0.3358 - accuracy: 0.8830 - val_loss: 0.8837 - val_accuracy: 0.7381

5エポックで訓練を行ってみました。結果としては最終的な部分でlossが減少し、accurasyが向上し、val_loss、val_accurasyも微小ながら改善しています。（改善されたと言い切れる幅なのかというところはありますが）
エポック数を単純に増加させると過学習が起きているように思えます。過学習は訓練データに最適化されているが、評価データではloss、accuracyが改善されない状態です。つまり訓練データにのみ最適な状態で、未知のデータに対する汎化性能に問題があるといえます。

この実験から、モデルの層を重ねる、あるいは改善するということで進めることが効果があるといえると思います。
データセットの評価用での分類、予測精度では図っていませんので正しいとは言い切れないですが、訓練段階でこういった点をみながら改善するということが重要だと思います。


##評価用データを用いた分類・予測
###モデルの正解率の評価

```python:code
test_loss, test_acc = model.evaluate(x_test, y_test, verbose=0)
print('\nTest accuracy:', test_acc)
```
結果
Test accuracy: 0.7293

結果を評価すると、訓練データに対するaccuracyに比べ少々ですが減少しています。これは順伝播型ニューラルネットワークでも見られましたが想定範囲内です。

###モデルを使い予測してみる

```python:code
predictions = model.predict(x_test)
predictions[0]
```
結果
array([1.09461966e-04, 3.75682830e-05, 4.33398100e-06, 9.63908672e-01,
       1.67364931e-08, 3.37889567e-02, 1.49343337e-04, 1.14129936e-04,
       1.68450316e-03, 2.03081756e-04], dtype=float32)


```python:code
np.argmax(predictions[0])
```
結果
3

結果を見ると、cat（猫）と判定したようです。

##まとめ
この記事で作成したCNNでは7割の正解率になりました。簡単な実装で7割まで結果を得られるというのはすごいことだと思いました。
ただ、7割を超え、精度を9割を目指すとなると、さらなる層の追加、最適化の手法やテクニックを必要とします。
単にエポック数を増やすと過学習となるので、モデル構造の改善が必要というのがわかりました。

次回は最適化や精度向上のテクニックを書きたいと思います。