TensorFlow2 + Keras による画像分類に挑戦7 ～層タイプ・活性化関数について理解する～

# はじめに

TensorFlow2 + Keras を利用した画像分類（Google Colaboratory 環境）についての勉強メモ（第7弾）です。題材は、ド定番である手書き数字画像（MNIST）の分類です。

- TensorFlow2 + Keras による画像分類に挑戦 シリーズ
  - [1. とりあえず動かす](https://qiita.com/code0327/items/7d3c7bd3327ff049243a)
  - [2. 入力データを詳しくみてみる](https://qiita.com/code0327/items/2c969ca4675d5a3691ef)
  - [3. MNISTデータを可視化してみる](https://qiita.com/code0327/items/a6edd5b27256429c3819)
  - [4. 学習済みモデルで予測させてみる](https://qiita.com/code0327/items/1047adc050ab6d75ad5c)
  - [5. 分類に失敗する画像を観察してみる](https://qiita.com/code0327/items/5dfc1b2ed143c1f9bd2b)
  - [6. 自分で用意した画像の前処理・分類をしてみる](https://qiita.com/code0327/items/d1a6b63915871a17020a)
  - [7. 層タイプ・活性化関数について理解する](https://qiita.com/code0327/items/68dd7beb1196320c8c15)
  - [8. 最適化アルゴリズムと損失関数を選択する](https://qiita.com/code0327/items/4f8f656bed23140b8962)
  - [9. モデルの学習、セーブ＆ロードをしてみる](https://qiita.com/code0327/items/951f418c81dbbd65c1fc)

[前回](https://qiita.com/code0327/items/d1a6b63915871a17020a)は、自分で用意した画像について、TF公式HPの「[初心者のための TensorFlow 2.0 入門](https://www.tensorflow.org/tutorials/quickstart/beginner?hl=ja)」で紹介されていたモデルで予測（分類）を行ないました。

今回は、そのチュートリアルで取り上げられているニューラルネットワークモデルについて、それを構成してる層のタイプ（`Dense`、`Dropout`、`Flatten`）と、活性化関数について勉強してみました。


> ![SoftMax.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/360ac00b-42f5-a250-6441-bc9b61839508.png)

# モデルの記述方法

以下のコードは「[初心者のための TensorFlow 2.0 入門](https://www.tensorflow.org/tutorials/quickstart/beginner?hl=ja)」からのコピペです。

```py:NNモデルの構築（記述1）
model = tf.keras.models.Sequential([
  tf.keras.layers.Flatten(input_shape=(28, 28)),
  tf.keras.layers.Dense(128, activation='relu'),
  tf.keras.layers.Dropout(0.2),
  tf.keras.layers.Dense(10, activation='softmax')
])
```

上記のコードでは**活性化関数**を指定するキーワード引数の `activation` を文字列で指定していますが、次のように直接的に関数を与えて指定することもできます。

```py:NNモデルの構築（記述2）
model = tf.keras.models.Sequential([
  tf.keras.layers.Flatten(input_shape=(28, 28)),
  tf.keras.layers.Dense(128, activation=tf.nn.relu),   # 変更
  tf.keras.layers.Dropout(0.2),
  tf.keras.layers.Dense(10, activation=tf.nn.softmax)  # 変更
])
```

また、ここでは、ニューラルネットの層構成情報を `Sequential(...)` の引数としてリスト型で与えていますが、次のように `add(...)` を使って**層をひとつずつ追加**していくこともできます。

```py:NNモデルの構築（記述3）
model = tf.keras.models.Sequential()                             # (0)
model.add( tf.keras.layers.Flatten(input_shape=(28, 28)) )       # (1)
model.add( tf.keras.layers.Dense(128, activation=tf.nn.relu) )   # (2)
model.add( tf.keras.layers.Dropout(0.2) )                        # (3)
model.add( tf.keras.layers.Dense(10, activation=tf.nn.softmax) ) # (4)
```


# モデルの概要を表示

上記のように層を設定したNNモデルは `summary()` で、その概要を確認することができます。

```py:モデルの概要確認
model.summary()
```

```:実行結果
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
flatten (Flatten)            (None, 784)               0         
_________________________________________________________________
dense (Dense)                (None, 128)               100480    
_________________________________________________________________
dropout (Dropout)            (None, 128)               0         
_________________________________________________________________
dense_1 (Dense)              (None, 10)                1290      
=================================================================
Total params: 101,770
Trainable params: 101,770
Non-trainable params: 0
_________________________________________________________________
```

表は上から下に向かって、**入力層**、**中間層（隠れ層）**、・・・、**出力層**となります。

表内の左端の値は「層の名前」になります。`add()` の際に `name=` を省略していると自動的に付与されるもので、モデルを構築するたびに `flatten_1`、`flatten_2` のように番号がついていきます。

左から2番目の `(　)` の値は「層のタイプ」になります。ここでは `Flatten`、`Dense`、`Dropout` の3タイプが登場します。この解説は次のセクションで。

項目「Output Shape」のタプルの2個目の数値は、**当該層のニューロン数（＝当該層からの出力数）**になります。`(None, 128)` であれば、その層には 128個のニューロン（ノード）が存在するということです。

つづいて、項目「Param」は、**パラメータ**（当該層の入力に係る**重み**と**バイアス**）の総数になります。

例えば、第2層「dense (Dense) 」の $100480$ は、第1層の出力数 $784$ と 第2層のノード数 $128$ を掛けた数だけの**重み**のパラメータ、第2層ノード数 $128$ 個の**バイアス** をあわせた全パラメータ数にになります。つまり、 $784\times 128 + 128=100480$ です。これらパラメータの最適値を求めるための操作がトレーニング（訓練・学習）になります。

表の最後には、Total params（総パラメータ数）、Trainable params（トレーニングにより求められるパラメータ）、Non-trainable params（トレーニングでは求めないパラメータ）の数が記載されています。

# 各層の役割・動作・意味

## Flatten層

手書き数字文字の1枚のイメージは 28 $\times$28 pixel であり、大きさが (28,28) の numpy.ndarray 型、つまり2次元配列です。Flatten層では、これを**平坦化**して 1次元配列的に直しています。よって、その出力数は、`model.summary()` で確認したように $28\times 28=784‬$ となります。

プログラムでは、次のようにしてモデルにFlatten層を追加しています。

```py
model.add( tf.keras.layers.Flatten(input_shape=(28, 28)) )   # (1)
```

`input_shape`引数には、`x_train[*].shape` と一致させて `(28, 28)` を指定しています。もし、32 $\times$32 pixel の画像を入力とするならば `input_shape=(32, 32)` のようにします。リファレンスは[こちら](https://www.tensorflow.org/api_docs/python/tf/keras/layers/Flatten)。


## Dense層

前層と当該層のあいだを全結合（密結合）した**全結合層**を意味します。ニューラルネットワークを構成する標準的な層となります。

プログラムでは、次のようにしてモデルにDense層を追加しています。


```py:中間層としての全結合層
model.add( tf.keras.layers.Dense(128, activation=tf.nn.relu) )   # (2)
```


```py:出力層としての全結合層
model.add( tf.keras.layers.Dense(10, activation=tf.nn.softmax) ) # (4)
```

1個目の引数には、その層を構成するノード数（ニューロン数）を与えます。上記の (2) のように**中間層として設定する全結合層のノード数をいくつにするか？**はモデルの性能を左右する要素になります（ユーザが勘なり試行錯誤で設定するハイパーパラメータになります）。なお、ノード数が多ければ高性能なモデルになるというわけではないです（少なくともノードが増えると、それだけパラメータ数が増えるので計算量が大きくなり学習に時間がかかります）。

一方で、多クラス分類問題を扱っている場合、**出力層として設定する全結合層のノード数は、分類したいクラス数と一致させる必要があります**。MNISTの場合は、0～9までの数字の分類、つまり **10クラス分類問題**なので、ここでは `10` を設定する必要があります。

また、`activation`引数には、**活性化関数**を与えます。ここでは、**ReLU関数**（`tf.nn.relu`）と**SoftMax関数**（tf.nn.softmax）が使われていますが、その詳細は次のセクションで解説します。なお、`activation=` を省略した場合は、活性化関数は適用されず、計算された値がそのまま出力される仕様です）。リファレンスは[こちら](https://www.tensorflow.org/api_docs/python/tf/keras/layers/Dense)。

## Dropout層

モデルを**トレーニングする際**に（ノード単位で）指定された確率に応じて前層から次層への出力を遮断する働きをします（前層の対応ノードを確率に応じて不活性／ドロップさせるとも表現されます）。この層を設けることで**過学習**という状況になりづらくなるようです。

これについては、「[【ニューラルネットワーク】Dropout(ドロップアウト)についてまとめる](https://qiita.com/shu_marubo/items/70b20c3a6c172aaeb8de)」の解説が、とても分かりやすかったです。

プログラムでは、次のようにしてモデルにDropout層を追加しています。

```py
model.add( tf.keras.layers.Dropout(0.2) )  # (3) 
```

引数には、不活性させたいノード割合を 0.0 から 1.0 の範囲で指定します。これを 0.0 に設定すると、実質的にDropout層が存在しないのと同じになります。また、1.0 に設定するとネットワークが Dropout層で完全遮断されるので、一切の学習が機能しません（実際には、`ValueError: rate must be a scalar tensor or a float in the range [0, 1), got 1` というエラーが発生します）。

なお、不活性されるノードは、指定した確率に応じて**ランダム**に選択されます。よって、このDropout層をもうけていると、**トレーニング毎に学習済みモデルが（わずかに）変化**します。そのため、Dense層のノード数と正解率の関係など、他のハイパーパラメータの影響を調べるときなどは、`seed=1` のように引数を与え、ランダムシードを固定します（ただし、トレーニングのほうでランダム要素があると、ここを固定しても実行毎に生成される学習済みモデルが変化します）。

リファレンスは[こちら](https://www.tensorflow.org/api_docs/python/tf/keras/layers/Dense)。

# 過学習に対するDropout層の有効性を評価

Dropout層のパラメータ（不活性させるノード割合 rate ）を、0.0 から 0.8 まで、0.2 刻みで変化させたモデルを用意します。これについて、Epochs数=100 でトレーニングと評価を行なって、Dropout層を入れることが**過学習に対して有効であるのか？**を観察しました。

トレーニングのEpoch毎に、トレーニングデータ `x_train` に対する正答率（accuracy）と損失関数値（loss）、テストデータ `x_test` に対する正答率（val_accuracy）と損失関数値（val_loss）を取得してプロットしました。

```py:
mport numpy as np
import tensorflow as tf

# (1) 手書き数字画像のデータセットをダウンロード
mnist = tf.keras.datasets.mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()

# (2) データの正規化
x_train, x_test = x_train / 255.0, x_test / 255.0

# (3) NNモデルを構築  
# ■■ Dropout rate を 0.0 から 0.8 まで変化させる ■■
epochs = 100
results = list()
for p in np.arange(0.0, 1.0, 0.2) :
  print(f'■ Dropout p={p:.1f}')
  tf.keras.backend.clear_session()  
  model = tf.keras.models.Sequential()
  model.add( tf.keras.layers.Flatten(input_shape=(28, 28)) )
  model.add( tf.keras.layers.Dense(128, activation=tf.nn.relu) )
  model.add( tf.keras.layers.Dropout(p) ) # ここのパラメータpの影響をみる
  model.add( tf.keras.layers.Dense(10, activation=tf.nn.softmax) )

  # (4) モデルのコンパイル
  model.compile(optimizer='adam',loss='sparse_categorical_crossentropy',metrics=['accuracy'])

  # (5) モデルのトレーニング（学習・訓練）
  r = model.fit(x_train, y_train, validation_data=(x_test,y_test), epochs=epochs)
  print(r.history)
  results.append( dict( rate=p, hist=r.history ) )

# ■■ グラフ出力 ■■
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as tk

ylim = dict( )
ylim['accuracy']     = (0.90, 1.00)
ylim['val_accuracy'] = (0.95, 1.00)
ylim['loss']         = (0.00, 0.20)
ylim['val_loss']     = (0.05, 0.30)

xt_style = lambda x, pos=None : f'{x:.0f}'

for v in ['accuracy','loss','val_accuracy','val_loss'] :
  plt.figure(dpi=96)
  for r in results :
    plt.plot( range(1,epochs+1),r['hist'][v],label=f"rate={r['rate']:.1f}")
  plt.xlim(1,epochs)
  plt.ylim( *(ylim[v]) )
  plt.gca().xaxis.set_major_formatter(tk.FuncFormatter(xt_style))
  plt.tick_params(direction='in')
  plt.legend(bbox_to_anchor=(1.02, 1), loc='upper left', borderaxespad=0)
  plt.xlabel('epoch')
  plt.ylabel(v)
  plt.show()
```

## 実験結果

### トレーニングデータに対する正答率 accuracy
![g1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/4acb7ff6-890d-3125-93d9-9c5523b4770c.png)

いずれのモデルでも学習を進めれば進めるほど良い値となっていきます。特に、実質的なDropout層ナシと等価である rate=0.0 では、最高スコア 1.0 に到達しています。基本的に、不活性化割合（rate）が小さいほど、学習が速く進み、最終的な正答率も良くなっています。

### トレーニングデータに対する損失関数値 loss
![g2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/6d94882c-81f8-6f09-f4e1-3a86224fa684.png)

基本的に、正答率（accuracy）と同様の傾向です。

### テストデータに対する正答率 val_accuracy
![g3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/d6843f7c-9e93-9c48-1f02-2f47512c21e8.png)

ここからが、汎化性能を含めた真の評価指標になります。

rate=0.2 と 0.4 は収束も早く、最終的な値も良好です。一方で、0.6 は、0.2 や 0.4 と比較すると明らかに性能が劣っています。0.0 は、他と比較して値がやや安定しません。

正答率だけを見ている限りでは、過学習を起しているような傾向は読み取れません。

### テストデータに対する損失関数値 val_loss
![g4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/daee58db-ada9-6c05-f892-b4a82a846a44.png)


rate=0.0（Dropout層なしと等価）では、Epoch=8 ぐらいを境にして徐々に値が悪くなっています。明確に過学習の傾向が読み取れます。

Epoch=20 以降の各データの傾きを観察すると、不活性割合（rate）が大きいほど過学習しずらいモデルになっていることが分かります。Dropout層の有効性を確認することができました。

総合して評価すると、チュートリアルで設定されていた rate=0.2、Epochs=5 という値は、十分にチューニングされた良いパラメータであったことが確認できました。
 
# 活性化関数

活性化関数を**適用しない**場合、第2層の1個目のノード出力 $y_1$ は次のように計算されます（$x_i$は前層の $i$ 番目ノードの出力、$w_{i1}$ は重み、$b_{1}$ はバイアス）。

$$ y_1 = b_1 + \sum_{i=1}^{784} x_{i}w_{i1} $$

一方、活性化関数 $f$ を適用すると、その $y_1$ は次のようになります。

$$ y_1 = f ( b_1 + \sum_{i=1}^{784} x_{i}w_{i1} ) $$


ニューラルネットで使用される活性化関数は、様々なものがありますが、大きくは**中間層でよく使われるもの**、問題タイプに応じて**出力層で使われるもの**に分けられます。


中間層では、**ReLU関数**や**シグモイド関数**が利用されます。また、多クラス分類問題の出力層では、その性質から**SoftnMax関数**が、2クラス分類問題では**シグモイド関数**が使用されます。

## ReLU関数

**中間層において**最も一般に用いられる活性化関数らしいです。入力が0未満の場合は出力は0、入力が0以上のときは、入力をそのまま出力します。`tf.nn.relu()`。リファレンスは[こちら](https://www.tensorflow.org/api_docs/python/tf/nn/relu)。

![ReLU.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/f89a1c93-5ff4-c20f-9751-21f1b9375762.png)


```py:ReLU関数
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf

xmin, xmax = -10,10
x = np.linspace(xmin, xmax,1000)
y = tf.nn.relu(x)  #  ReLU関数

# グラフで形状を確認
plt.figure(dpi=96)
plt.plot(x,y,lw=3)
plt.xlim(xmin, xmax)
plt.ylim(-1, 12)
plt.hlines([0],*(plt.xlim()),ls='--',lw=0.5)
plt.vlines([0],*(plt.ylim()),ls='--',lw=0.5)
```


ReLU関数の亜種に「Leaky Relu関数」「Parametric ReLU関数」などがあるようです。

- 参考：[活性化関数ReLUについてとReLU一族](https://qiita.com/hokekiyoo/items/bf7be0ae3bf4aa3905ef)


## シグモイド関数

中間層において、よく用いられる活性化関数のひとつ。ただし、**層数が多いNNモデル**でシグモイド関数を活性化関数として使用すると、勾配消失問題が起きるために、ReLU関数に人気を奪われ気味だそうです。`tf.math.sigmoid()`。リファレンスは[こちら](https://www.tensorflow.org/api_docs/python/tf/math/sigmoid)。

![sig.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/759c6b9b-f689-705e-ca17-aa00f8dba687.png)


```py:
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf

xmin, xmax = -10,10
x = np.linspace(xmin, xmax,1000)
y = tf.math.sigmoid(x)  # シグモイド関数

# グラフで形状を確認
plt.figure(dpi=96)
plt.plot(x,y,lw=3)
plt.xlim(xmin, xmax)
plt.ylim(-0.2, 1.2)
plt.hlines([0,0.5,1],*(plt.xlim()),ls='--',lw=0.5)
plt.vlines([0],*(plt.ylim()),ls='--',lw=0.5)
```

## SoftMax関数

一般に、多クラス分類問題の**出力層**で使用されます。入力に関わらず出力は 0.0 ～ 1.0 の範囲をとり、出力の総和が 1.0 となるのが特徴です。`tf.nn.softmax()`。リファレンスは[こちら](https://www.tensorflow.org/api_docs/python/tf/nn/softmax)。

例えば、次のように `[2, -1, 1, 1]` という入力に対してSoftMax関数を適用すると、`[0.56, 0.03, 0.21, 0.21]` といった出力（要素の総和は 1.0）を得ることができます。

![SoftMax.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/360ac00b-42f5-a250-6441-bc9b61839508.png)


```py:ReLU関数
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patheffects as pe
import tensorflow as tf

x = np.array( [2, -1, 1, 1], dtype=np.float64 )
fx = tf.nn.softmax(x)
fx = fx.numpy() # 'numpy.ndarray'で内容取得

np.set_printoptions(precision=2)
print(f'fx = {fx}')
print(f'fx.sum() = {fx.sum():.2f}')

fig, ax = plt.subplots(nrows=2, ncols=1, dpi=96)
plt.subplots_adjust(hspace=0.12)
ep = (pe.Stroke(linewidth=3, foreground='white'),pe.Normal())
tp = dict(horizontalalignment='center',verticalalignment='center')

ax[0].bar( np.arange(0,len(x)), x, fc='tab:red' )
ax[1].bar( np.arange(0,len(fx)), fx )
ax[1].set_ylim(0,1)

for i, p in enumerate([x,fx]) :
  ax[i].tick_params(axis='x', which='both', bottom=False, labelbottom=False)
  ax[i].set_xlim(-0.5,len(p)-0.5)
  for j, v in enumerate(p):
    t = ax[i].text(j, v/2, f'{v:.2f}',**tp)
    t.set_path_effects(ep) 

ax[0].hlines([0],*(plt.xlim()),lw=1)

ax[0].set_ylabel('x')
ax[1].set_ylabel('SoftMax(x)')
```


# 次回

未定