E検定で出てくるニューラルネットワーク、畳み込みニューラルネットワーク分野についてまとめ

# はじめに
本記事ではE検定で使用するニューラルネットワーク(以下、NNと略)、畳み込みニューラルネットワーク(CNN)分野についてまとめました。
具体的には以下の項目について説明し、NNやCNNの実装例を示します。

* 入力層～中間層
* 活性化関数
* 出力層
* 勾配降下法
* 誤差逆伝搬法
* 勾配消失問題
* 学習率最適化
* 過学習
* CNNの概念、AlexNet

# NNとは？
NNとは教師あり学習の一つで、回帰問題、分類問題を解くためのアルゴリズムとなります。

# 全体的なNNの構成
全体的なNNの構成として以下の図となり、左側の入力層から説明変数の値が入力され、中間層を通り、最終的に出力層に値が出力されます。
用語として、以下の図の〇はノードと呼ばれます。基本的には、各ノードは右隣の各層のノードとすべて結合されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/618469/376ba9bb-98ea-bbbe-4b73-3add1af6b0ad.png)

各層の詳細について説明します。

## 入力層～中間層
入力層～中間層の詳細を見ると以下の図の通りです。以下の図の$w_{1},w_{2},w_{3}$は重み、$b$はバイアスと呼ばれるものです。これらは実数値のパラメータとなります。
入力層の入力値を与えると中間層の各ノードに$w_{n}*入力値+b$の値が渡ってきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/618469/0a38a0fb-13db-b72e-36e8-6cbaa92b2185.png)

また、この中間層への出力はベクトル表現(中間層ベクトル = $W \times 入力層ベクトル + b$)で表すことが出来ます。

## 活性化関数
入力層から中間層へ値の渡し方を説明しましたが、このまま出力層に線形的な演算をしても、非線形分離が出来ません。
そのため、中間層以降のノードに値が渡ってきたときに重みと入力値、バイアスの計算をしたのち、活性化関数と呼ばれる非線形関数を通して、そのノードへの入力値としています。
中間層で用いられる活性化関数としては以下の関数があります。

* ReLU関数 (入力値が0以上の場合、恒等写像、0未満の場合、0となる関数)
* シグモイド関数 ($ f(x) = \frac{1}{1+exp(-u)}$)
* ステップ関数 (入力値が0以上の場合、1、0未満の場合、0となる関数)

また、出力層は出力に向けて中間層とは別の活性化関数を使う場合がありますが、出力層で用いられる活性化関数は以下の関数があります。

* ソフトマックス関数
* 恒等写像(そのまま入力値を返す関数。例：$y=x$)
* シグモイド関数

## 出力層
出力層は前述した通り、NNの最終層であり、NNの最終的な出力となります。
出力する値として、例えば、物件情報の入力値(物件の間取り、場所等)から家賃・敷金・礼金を求める回帰問題であれば出力層に家賃用、敷金用、礼金用のノードから適切な値が出力されるイメージです。
また、分類問題の場合は、0～1の間の確率出力をすることで、その分類の確からしさを出力します。

### 誤差関数
NNの学習に関して、出力層の出力値と学習データのラベル値が一致させ、未知のデータを予測する必要があります。
その最初の処理として、出力層の出力値と学習データのラベル値がどのくらい誤差が出ているか確認する必要があります。
その誤差を測る尺度として誤差関数を使用します。
誤差関数は二乗誤差等が使われますが、一般的に出力層の活性化関数によって、使用する誤差関数が異なります。

恒等写像・・・二乗誤差
シグモイド関数、ソフトマックス関数・・・交差エントロピー

# 勾配降下法
NNの学習は出力層の出力値と学習データのラベル値との誤差を最小化することですが、その際に使用するのが勾配降下法となります。
具体的には勾配降下法を使用することでNN内の全重み、バイアスの値を更新し、誤差を最小化します。
各重みは以下の式で更新されます。基本的には誤差関数の勾配を求め、その勾配値に学習率で調整した物を掛けたものを重みに引き算をします。

```math
w^{t+1} = w^{t} - \epsilon \nabla E\\
w^{t+1} ・・・ 更新後の重み\\
w^{t}・・・更新前の重み\\
\epsilon ・・・ 学習率\\
\nabla E ・・・ 誤差関数の微分値
```

学習率は1度の重み更新をどのくらい割合で適用するか設定するパラメータで、これが小さすぎると1度の重み更新があまりされず、ゆっくり学習が進みます。
また、大きすぎると1度の重み更新の値が大きくなり、誤差が最小に収束せず発散してしまう問題点があります。
そこで、学習率を固定の値にするのではなく、学習の進行によって学習率を調整する以下のオプティマイザが利用されています。

* Momentum
* AdaGrad
* Adadelta
* Adam

また、NN全体の重みを上記の更新を1回行う単位を1エポックと呼びます。

1エポックに関して、1つの学習データで重みを更新する確率的勾配降下法と複数の単位でまとめて重みを更新するミニバッチ降下法があります。

# 誤差逆伝搬法
誤差逆伝搬法とは出力層と学習データのラベルとの誤差の勾配を出力層⇒入力層の各重み、バイアスに対して更新を行う方法です。
出力層から入力層に誤差分を伝搬すると微分の連鎖律を使用して、最小限の微分計算で各層の重み、バイアスを更新することが出来ます。

# NNの実装
今回、sklearnライブラリにあるデータセット「load_digits」(0-9の数字の画像セット)を使って画像データから0-9の数字の分類を行う実装例を示します。
具体的には0-1500枚めの画像データをNNに学習させ、1501枚めの画像データのラベル値を予測します。

●コード

```python
# 各種ライブラリのインポート
from sklearn.datasets import load_digits
from sklearn import metrics
import numpy as np
import matplotlib.pyplot as plt

# digitsデータセット取得
digits = load_digits()
# digitデータセット標準化
std_dgits_data = (digits.data - digits.data.mean()) / digits.data.std()

## 各種定義
# ソフトマックス関数
def softmax(x):
    if x.ndim == 2:
        x = x.T
        x = x - np.max(x, axis=0)
        y = np.exp(x) / np.sum(np.exp(x), axis=0)
        return y.T

    x = x - np.max(x) 
    return np.exp(x) / np.sum(np.exp(x))

# シグモイド関数（ロジスティック関数）
def sigmoid(x):
    return 1/(1 + np.exp(-x))

# ソフトマックスとクロスエントロピーの複合導関数
def d_softmax_with_loss(d, y):
    batch_size = d.shape[0]
    if d.size == y.size: 
        dx = (y - d) / batch_size
    else:
        dx = y.copy()
        dx[np.arange(batch_size), d] -= 1
        dx = dx / batch_size
    return dx

# シグモイド関数（ロジスティック関数）の導関数
def d_sigmoid(x):
    dx = (1.0 - sigmoid(x)) * sigmoid(x)
    return dx

# 学習率
rate = 0.2
# エポック数
iter = 1500

## モデル構築
# 入力層(64ノード)、中間層(32ノード)、出力層(10ノード)それぞれ１層のNNを初期作成
network = {}
network['W1'] = np.random.rand(64, 32)
network['W2'] = np.random.rand(32, 10)
network['b1'] = np.zeros(32)
network['b2'] = np.zeros(10)


## 学習プロセス
# 確率的降下法を使ってみる
for i in range(iter):
  # 学習データから1つ抽出して順伝搬をしてみる
  o1 = np.dot(std_dgits_data[i], network['W1']) + network['b1']
  s1 = sigmoid(o1)
  o2 = np.dot(s1, network['W2']) + network['b2']
  y = softmax(o2)

  # 誤差を求める
  y_true = digits.target[i]
  y_true_one_hot = np.identity(10)[y_true] 
  # 勾配計算
  grad = {}
  delta2 = d_softmax_with_loss(y_true_one_hot, y)
  grad['b2'] = np.sum(delta2, axis=0)
  grad['W2'] = np.dot(y.T, delta2)
  delta1 = np.dot(delta2, network['W2'].T) * d_sigmoid(s1)
  grad['b1'] = np.sum(delta1, axis=0)
  grad['W1'] = np.dot(s1.T, delta1)
  for key in ('W1', 'W2', 'b1', 'b2'):
    network[key]  -= rate * grad[key]

## 予測
# 予測期待値の出力
print("予測期待値:%s" % (digits.target[1500]))
# 予測してみる
o1 = np.dot(std_dgits_data[1500], network['W1']) + network['b1']
s1 = sigmoid(o1)
o2 = np.dot(s1, network['W2']) + network['b2']
y = softmax(o2)
print("予測値:%s" % (np.argmax(y)))
```

●実行結果

```
予測期待値:1
予測値:1
```

# NNの学習について
NNの学習に関して、いくつかトピックがありますが、代表的な物として以下の物があります。今回はそれぞれの概要や対応策について説明します。

* 勾配消失問題
* 学習率最適化
* 過学習

## 勾配消失問題
NNは誤差逆伝搬法で出力層から入力層へ重み・バイアスを誤差の微分値を順次更新しますが、入力層にわたっていくにつれて誤差の微分値が小さくなり、入力層付近の重み・バイアスが更新がほとんどなくなってしまう(学習できない)問題です。
特にシグモイド関数は微分すると以下の導関数となり、最大値が0.25となるため、層が深いNNの場合、誤差を逆伝搬するにつれて最小でも1/4も誤差の微分値が少なくなってしまいます。

```math
\frac{d(sigmoid(x))}{dx} = (1 - sigmoid(x))*sigmoid(x)
```

この勾配消失問題の解決方法として以下の方法があります。

* 活性化関数の選択

勾配消失問題は活性化関数を微分した際にその導関数の最大値が少ない場合に起きがちなので、勾配消失問題が起きづらい活性化関数を選ぶという方法があります。
例えば、活性化関数としてシグモイド関数ではなくReLU関数(xが0より大きい場合は恒等写像、0以下の場合は常に0)を使用する、などです。

* 重みの初期化設定
重み・バイアスの初期値を0付近に設定すると誤差を逆伝搬する際、重み・バイアスに更新する値が小さすぎて学習が進まない問題もあります。
その対応策として、以下の方法があります。

1. Xavier・・・重みを前の層のノード数の平方根で割った値を設定
2. He・・・重みを前の層のノード数の平方根で割って$\sqrt{2}$を掛けた値を設定

* バッチ正規化
学習データによっては誤差を逆伝搬する際、重みやバイアスの更新値が極端に大きすぎたり、小さすぎたりしてうまく学習ができない場合があります。
その場合、学習データをミニバッチ単位で分けてそれを正規化処理を行うことで上記の問題を解決する手段をバッチ正規化といいます。

## 学習率最適化
学習率に関して大きすぎたり、小さすぎたりすると以下の問題が発生しますが、その問題を回避すため、学習率を学習進度によって調整することを学習率最適化といいます。

* 学習率が大きすぎる・・・学習が収束しない
* 学習率が小さすぎる・・・学習スピードが遅い、局所解に陥る可能性がある

代表的な学習率最適化法は以下の通りです。

* モメンタム・・・重みを更新する式に現在の重みの値と慣性項を掛けた値を足した物で、大域最適解となりやすい傾向があります。
* AdaGrad・・・重みを更新する式に学習率と誤差をパラメータで微分した値を掛け合わせることで、勾配が緩やかなところで最適解に近づきやすくなる
* RMSProp・・・AdaGradの改良版で誤差をパラメータ微分した値と初期値を比率を指定して足し合わせたもので、大域最適解となりやすい傾向があります。
* Adam・・・上記3つの最適化法のメリットを取り入れた最適法です。

最適化手法をビジュアル的に示したものとして以下のサイトがあります。
https://github.com/Jaewan-Yun/optimizer-visualization

## 過学習
学習データに最適化しすぎて、学習データに高い精度を示し、検証データ(本来予測したい未知のデータ)に対して精度が低くなってしまう問題です。
過学習は一般的に層のノード数が多いと起きやすいといわれており、その原因として、層内の一部のノードのみでその層内の出力を賄ってしまうことや特定の重みの値が大きいことでばらつきが発生することがあげられます。
そのため、以下の以下の手法で特定のノードのみが最適化されない様以下の手法を使います。

* L1正則化、L2正則化・・・誤差関数に正則化項を設けることで重みを調整して過学習を避ける方法です。
* ドロップアウト・・・ランダムのノードを削除して学習する手法で一部のノードのみの学習を避ける方法です。

# 勾配消失問題、学習率最適化、過学習問題を対策する実装例
手軽にNNを構築できるkerasを使用し、sklearnのdigitデータセットを使用し、学習・予測してみました。

```python
# 各種ライブラリのインポート
import keras
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras import backend as K
import numpy as np
from keras.optimizers import Adam
from sklearn.datasets import load_digits

# digitsデータセット取得
digits = load_digits()
# digitデータセット標準化
std_dgits_data = np.array((digits.data - digits.data.mean()) / digits.data.std())
target_one_hot = np.array([np.identity(10)[target] for target in digits.target])

# 学習用データセットと検証用データセットを分ける
x_train = std_dgits_data[:1200]
y_train = target_one_hot[:1200]
x_test = std_dgits_data[1201:]
y_test = target_one_hot[1201:]

# NN構築(中間層は1層)
model = Sequential()
model.add(Dense(64, activation='relu', input_shape=(64,)))
# 過学習対策：10%の確率でノードをドロップアウトする
model.add(Dropout(0.1))
model.add(Dense(32, activation='relu'))
# 過学習対策：10%の確率でノードをドロップアウトする
model.add(Dropout(0.1))
model.add(Dense(10, activation='softmax'))

# 学習率最適化手法としてAdamを使用する
model.compile(loss='categorical_crossentropy',
 optimizer=Adam(),
 metrics=['accuracy'])

# 学習を行う
model_relu.fit(x_train, y_train,
 batch_size=10,
 epochs=30,
 verbose=1,
 validation_data=(x_test, y_test))

# digitデータセットの最初の10枚の画像から適切な予測が出来ている確認
# 予測値
print("予測値：%s" % (np.array([ result.argmax() for result in model_relu.predict(std_dgits_data)[0:10]])))
# 予測ラベル
print("予測ラベル:%s" % (digits.target[0:10]))
```

●実行結果

```
Epoch 1/30
120/120 [==============================] - 6s 7ms/step - loss: 1.9431 - accuracy: 0.3335 - val_loss: 0.7453 - val_accuracy: 0.8003
Epoch 2/30
120/120 [==============================] - 0s 3ms/step - loss: 0.6602 - accuracy: 0.8055 - val_loss: 0.3977 - val_accuracy: 0.8859
...
ログは省略
...
Epoch 30/30
120/120 [==============================] - 0s 3ms/step - loss: 0.0218 - accuracy: 0.9940 - val_loss: 0.2523 - val_accuracy: 0.9245
<keras.callbacks.History at 0x7f16dd05d310>
[42]

予測値：[0 1 2 3 4 5 6 7 8 9]
予測ラベル:[0 1 2 3 4 5 6 7 8 9]
```

# CNNの概要
CNNとは画像や音声(時間と声の高さ)となどの入力値とその周りの入力値に関連があるデータに対して、最適なNNの一種となります。
基本的なCNNの構成例としては、以下のような構成となっております。(実際のCNNの場合はもっと複雑ですが、CNNの概要を説明する目的なのでシンプルな構成を図示します)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/618469/0a3a94bc-37c0-66e9-bb63-d3ab25c4cf7a.png)

* 入力層・・・NNにもある入力値を受け取る層です。
* 畳み込み層・・・NNにはない層で、前の層からの入力値をフィルター・バイアスを使用し、特徴量を抽出します。
* プーリング層・・・NNにはない層で、前の層からの入力値の位置ずれを柔軟に対応するための層です。
* 全結合層・・・NNの中間層に当たる部分で、畳み込み層、プーリング層で入力値の特徴量を抽出し、この層でその特徴量の学習を行います。
* 出力層・・・CNNの最終的な出力を行う層です。

## 畳み込み層
具体的な畳み込み層の内容を見ていきます。
畳み込み層の演算は以下のとおり行われます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/618469/ea95d10a-e522-3545-f1b3-8016f8b285ce.png)

1. 入力値とフィルターがあり、入力値の端の特徴を得るために入力値の周りに0でパディングを行います。(0でパディングしない場合もあります)
2. パディングした入力値の左上から順次フィルターを適用します。フィルターと入力値の各要素を掛けて合計した値がフィルター適用後の値となります。
3. フィルターを適用する際、左上から右下にかけて適用していきますが、ストライドを使用してどのくらいスキップするか指定することが出来ます。
4. 最後にバイアスを各出力値に足しこむことで完了します。

フィルター入力値の特徴をつかむための物なので、1つの畳み込み層で入力値の複数の特徴をとらえたい場合、その特徴ごとのフィルターを用意し、フィルターを適用した各出力結果をチャンネルといいます。
例えば、1つの畳み込み層でフィルターを3つ用意し、それを適用した場合、3チャンネルの出力結果を得ます。

## プーリング層
プーリング層は各入力値の対象領域の最大値 or 平均値を割り当てていきます。
処理の流れとしては以下の通りです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/618469/05a80dc7-1593-bf9f-e92f-fa5109f716fe.png)

## AlexNet
AlexNetはCNNの一つで2012年に開かれた画像認識コンペ(ILSVRC 2012)において、2位と10%以上の誤差率を抑えて優勝したモデルです。
これまで、画像認識コンペにおいて画像の特徴量の抽出を手動で行ってきましたが、AlexNetの登場によってその抽出作業も機械学習モデルの仕事の対象となりました。
AlexNetは3層の畳み込み層と2層のプーリング層、3層の全結合層で構成されており、全結合層の出力にドロップアウトを用いて過学習を防ぐ工夫がされています。

## CNNの実装例
今回もデータセットとして、digitsデータセットを利用します。
8x8のピクセルの画像を入力とし、畳み込み⇒プーリング⇒全結合で予測しています。

●コード

```python
# 各種ライブラリのインポート
from keras import layers, models
from sklearn.datasets import load_digits
import numpy as np

# digitsデータセット取得
digits = load_digits()
# digitデータセット標準化
std_dgits_data = np.array((digits.data - digits.data.mean()) / digits.data.std())
target_one_hot = np.array([np.identity(10)[target] for target in digits.target])

x_train = std_dgits_data[:1200]
y_train = target_one_hot[:1200]
x_test = std_dgits_data[1201:]
y_test = target_one_hot[1201:]

# CNN構築
model = models.Sequential()
# 畳み込み層作成(8x8のピクセルを持つ画像から2x2の1フィルターを使った畳み込みを行う(ストライドは1x1とする))
# この畳み込みで1チャンネルの3x3サイズの出力がされる
model.add(layers.Conv2D(1,(2,2),strides=(1, 1),activation="relu",input_shape=(8,8,1)))
# プーリング層(maxプーリングを実行し、2x2サイズを出力する)
model.add(layers.MaxPooling2D(pool_size=(2, 2),strides=(1, 1), padding='valid'))
# 全結合層
model.add(Dense(2, activation='relu', input_shape=(2,2)))
# 過学習対策：10%の確率でノードをドロップアウトする
model.add(Dropout(0.1))
model.add(Dense(10, activation='softmax'))

# 学習率最適化手法としてAdamを使用
model_relu.compile(loss='categorical_crossentropy',
 optimizer=Adam(),
 metrics=['accuracy'])

# AlexNet
AlexNetとはCNNですが、2012年に画像分類コンペ(ILSVRC)において、優勝したモデルです。
これまでのコンペで使用されたモデルは人間側で画像の特徴量を設計していましたが、AlexNetはモデル自身がその特徴を見つけだすことが出来ます。
これによって、2012年のILSVRCにおいて、2位のモデルと10%以上のエラー率に差をつけることが出来ました。
参考文献：
https://axa.biopapyrus.jp/deep-learning/cnn/image-classification/alexnet.html

## 実装例
今回も例にもれずデータセットとしてdigitsデータセットを利用します。

# CNNの学習
model_relu.fit(x_train, y_train,
 batch_size=10,
 epochs=30,
 verbose=1,
 validation_data=(x_test, y_test))

# 予測値
print("予測値：%s" % (np.array([ result.argmax() for result in model_relu.predict(std_dgits_data)[0:10]])))
# 予測ラベル
print("予測ラベル:%s" % (digits.target[0:10]))

```

●実行結果

```
Epoch 1/30
120/120 [==============================] - 1s 5ms/step - loss: 0.0193 - accuracy: 0.9913 - val_loss: 0.2582 - val_accuracy: 0.9295
Epoch 2/30
120/120 [==============================] - 0s 2ms/step - loss: 0.0143 - accuracy: 0.9976 - val_loss: 0.2642 - val_accuracy: 0.9312
...
ログは省略
...
Epoch 30/30
120/120 [==============================] - 0s 2ms/step - loss: 0.0121 - accuracy: 0.9934 - val_loss: 0.2823 - val_accuracy: 0.9279
<keras.callbacks.History at 0x7f16dd331750>

予測値：[0 1 2 3 4 5 6 7 8 9]
予測ラベル:[0 1 2 3 4 5 6 7 8 9]
```