ニューラルネットワークの基礎

# ニューラルネットワークによる学習

## 学習とは

重みとバイアスを調整すること。

## 学習方法

1. 訓練データからランダムに一部のデータを取り出す
1. 勾配の算出
1. パラメータの更新
1. 繰り返す

ここではMNISTデータセットを用いて考えてみる。

```python
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
print(x_train.shape) # (60000, 784)
print(t_train.shape) # (60000,)
print(x_test.shape)  # (10000, 784)
print(t_test.shape)  # (10000,)
img = (x_train[0]*256).reshape(28, 28)
plt.imshow(img, cmap=plt.cm.gray_r)
```
![download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472670/2f593b99-7f5e-7be6-3553-812151f0fb8e.png)

## モデルの解説

今回のモデルを図に表すと以下のようになる。
ここで、xは入力画像の1次元配列、bはバイアス、wは重みとする。
また、重み付き信号とバイアスの総和をaとし、aを活性化関数によって変換されたものをzとする。


<img width="500" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472670/db489ef9-7e1c-24b7-1e8f-ef9d6fdfec08.png">

1層目の出力結果を数式で表すと以下のようになる。

$$
a_1^{(1)} = w_{11}^{(1)}x_1 + w_{12}^{(1)}x_2 + \cdots + w_{1784}^{(1)}x_{784} + b_1
$$

行列で表すと以下の通り。
```math
\begin{pmatrix}
a_1^{(1)} & \cdots & a_{50}^{(1)}
\end{pmatrix}=
\begin{pmatrix}
x_1 & \cdots & x_{784}
\end{pmatrix}
\begin{pmatrix}
w_{11}^{(1)} & \cdots &  w_{501}^{(1)} \\
w_{12}^{(1)} & \cdots &  w_{502}^{(1)} \\
\vdots & \ddots &  \vdots \\
w_{1784}^{(1)} & \cdots &  w_{50784}^{(1)} \\

\end{pmatrix}+
\begin{pmatrix}
b_1^{(1)} & \cdots & b_{50}^{(1)}
\end{pmatrix}
```

$$
A^{(1)} = XW^{(1)}+B^{(1)}
$$

そして、1層の出力値である$A^{(1)}$は、活性化関数による変換を受けて、$Z$となる。
ここではシグモイド関数を利用する。

$$
f(x) = \frac{1}{1+e^{-x}}
$$

$$
Z = sigmoid(A^{(1)})
$$

同じように出力層の$A^{(2)}$は

$$
A^{(2)} = ZW^{(2)}+B^{(2)}
$$

出力結果はソフトマックス関数をかけて、総和を1にする。
$$
Y = softmax(A^{(2)})
$$

となる

出力層の$A^{(2)}$の10個の要素のうち、最大値を取るものがこのモデルから得られる解となる。
（正確には、10個の選択肢のうち最も可能性が高いもの）

※モデルの層の数、隠れ層のニューロン数については別途考察する。

## 重み、バイアスの学習
モデルの設計が完了したので、重みとバイアスの最適値を求めていく。

基本的には以下の考え方で学習する。
1. 現在の重み、バイアスの値でモデルを作成し、予測値を出す。
1. 予測値と実測値の差を求め、損失関数を求める
1. 損失関数の勾配を決定する
1. 損失関数が小さくなるよう、（重み、バイアス）＝（重み、バイアス）－（勾配）×（学習率）としてパラメータを更新する
1. 1.から再度繰り返す。

### 重み、バイアスの初期値設定
初期値はランダムに設定する。バイアスは0に設定。
```python
params = {}
params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
params['b1'] = np.zeros(hidden_size)
params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
params['b2'] = np.zeros(output_size)
```

### ミニバッチの取得
すべてのデータを利用して学習すると、かなりの時間がかかってしまうため、ランダムに100個のデータを取得して学習をする。
```py
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
```

ここで取得したx_batch、t_batchを利用して今後の処理を行う。
また、ミニバッチの取得は、重み、バイアスを更新するたびに行い、別のデータで学習を行う

### 勾配の計算
ここでは、誤差逆伝播法を利用して計算する。
※数値微分により求めても良いが計算に時間がかかる。

先ほどのモデルで出てきた計算式を参考にする。
これが順伝播となる。

```math
A^{(1)} = XW^{(1)}+B^{(1)} \\
Z = sigmoid(A^{(1)}) \\
A^{(2)} = ZW^{(2)}+B^{(2)} \\
Y = softmax(A^{(2)})
```

これを逆伝播すると以下のようになる。（計算方法は省略する）
※$1$は要素がすべて1のN×1行列とする。
```math
dY = \frac{Y - T}{batchsize} \\
dW^{(2)} = Z^TdY \\
dB^{(2)} = 1^TdY \\
dZ = dY{W^{(2)}}^T \\
dA^{(1)} = A^{(1)} (1 - sigmoid(A^{(1)}))  sigmoid(A^{(1)}) \\
dW^{(1)} = X^TdA^{(1)} \\
dB^{(1)} = 1^TdA^{(1)}
```

Pythonで実装すると以下のイメージ。
```python
a1 = np.dot(x_batch, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
y = softmax(a2)


dy = (y - t_batch) / batch_num
grads['W2'] = np.dot(z1.T, dy)
grads['b2'] = np.sum(dy, axis=0)

dz1 = np.dot(dy, W2.T)
da1 = (1.0 - sigmoid(a1)) * sigmoid(a1) * dz1
grads['W1'] = np.dot(x_batch.T, da1)
grads['b1'] = np.sum(da1, axis=0)
```

### 勾配をもとにしたパラメータの更新
先ほどの計算で、勾配である、$dW^{(1)}, dW^{(2)}, dB^{(1)}, dB^{(2)}\ $が求まったので、paramsを更新する。
この時設定する学習率は大きくても小さすぎてもNG。
```python
for key in ('W1', 'b1', 'W2', 'b2'):
    params[key] -= learning_rate * grads[key]
```

パラメータを更新したら、ミニバッチの取得から再度学習を繰り返す。

### まとめ
上記までをまとめると以下のコードになる。
なお、イテレーション100回ごとに損失関数の値、正解率を記録している。

```python
train_size = x_train.shape[0]
batch_size = 100     # ミニバッチのサイズ
iters_num = 10001   # イテレーション回数
learning_rate = 0.1  # 学習率

input_size = 784     # 入力数（784要素あるため、784個）
hidden_size = 50     # 隠れ層のサイズ：50と仮定
output_size = 10     # 出力層：10　one-hotエンコーディングで0～9を割り当て
weight_init_std=0.01 # 重みに対してかける定数

params = {}
params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
params['b1'] = np.zeros(hidden_size)
params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
params['b2'] = np.zeros(output_size)

#損失関数と誤差を保持する配列
train_loss = []
train_accuracy = []
test_accuracy = []
iters = []

for i in range(iters_num):
    # ミニバッチの取得

    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]

    # 勾配の計算
    W1, W2 = params['W1'], params['W2']
    b1, b2 = params['b1'], params['b2']
    grads = {}

    batch_num = x_batch.shape[0]

    # forward
    a1 = np.dot(x_batch, W1) + b1
    z1 = sigmoid(a1)
    a2 = np.dot(z1, W2) + b2
    y = softmax(a2)

    # backward
    dy = (y - t_batch) / batch_num
    grads['W2'] = np.dot(z1.T, dy)
    grads['b2'] = np.sum(dy, axis=0)

    dz1 = np.dot(dy, W2.T)
    da1 = (1.0 - sigmoid(a1)) * sigmoid(a1) * dz1
    grads['W1'] = np.dot(x_batch.T, da1)
    grads['b1'] = np.sum(da1, axis=0)

    # パラメータの更新
    for key in ('W1', 'b1', 'W2', 'b2'):
        params[key] -= learning_rate * grads[key]
    if i % 100 == 0:
        train_acc = accuracy(x_train, t_train, params)
        test_acc = accuracy(x_test, t_test, params)
        train_loss.append(loss(x_batch, t_batch, params))
        train_accuracy.append(train_acc)
        test_accuracy.append(test_acc)
        iters.append(i)
```

また、損失関数のグラフは以下のようになる。
```py
plt.plot(iters, train_loss, color = "red", linestyle = "solid" ,label = 'train loss')
plt.legend()
plt.grid()
```
![download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472670/b3946806-268a-f938-8dce-b9df0a6d5fad.png)


同様に正解率のグラフは以下のようになる。
```py
plt.plot(iters, train_accuracy, color = "orange", linestyle = "solid" , label= 'train accuracy')
plt.plot(iters, test_accuracy, color = "blue", linestyle = "solid" , label= 'test accuracy')
plt.legend()
plt.grid()
```

![download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472670/17eec39e-fe8c-9cbd-6f54-4a5a3c2aec7d.png)

過学習が起きていないと、このグラフから見て取れる
また、最終的な精度としては約94%となった。

```python
train_acc = accuracy(x_train, t_train, params)
test_acc = accuracy(x_test, t_test, params)
print(train_acc) # 0.9486
print(test_acc)  # 0.9448
```

## さらなる精度向上のために
### パラメータの更新方法
勾配降下法以外にも以下の方式がある。
詳細は割愛するが、いずれも勾配降下法よりも早く極小値にたどり着ける可能性がある。
- Momentum
- AdaGrad
- Adam

### 重みの初期値
#### 重みの初期値を0にしてはいけない
重みの初期値を0にしてしまうと、次の層には前の層のデータがそのまま来ることになってしまい、学習ができなくなってしまう。

#### 重みの初期値の考え方
隠れ層のアクティベーションの分布を意識すべきである。

隠れ層のアクティベーション（出力結果）が偏ってしまっていると、複数のニューロンが存在する意味がなくなってしまう。
つまり、100個のニューロンが同じ値0を出力する場合、それは1個のニューロンが0を出力する場合と同じになってしまう。
そのため、表現力を増すためには、適度な広がりを持った隠れ層のアクティベーションが必要となる。

**NGなパターン**
![fig06_11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472670/a8aef7d0-dd26-2e30-3bc7-c6f3a4f26578.png)

**OKなパターン**
![fig06_13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472670/5dda0076-c236-d2a0-5cdc-496c59f9769a.png)

また、活性化関数によっても初期値の設定方法に違いがある。

**まとめ**
活性化関数によって良い初期値の設定方法は異なる
| 活性化関数 | ReLU | sigmoid,tanh |
|:-:|:-:|:-:|
|  初期値 | Heの初期値  | Xavierの初期値  |

### Batch Normalization
各層のアクティベーションの分布を強制的に調整したら、という考え方で作成された手法
イメージとしては以下の通り。各層にBatch Normレイヤを追加している。
![fig06_16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472670/d20c3910-4feb-8f1d-d5a2-216127728dc2.png)

実際にKerasにて実装した時の結果が以下。
水色がBatch Normalizationありの場合。
Batch Normalizationありのほうが早く収束することがわかる。

正答率
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472670/a1427649-50d5-b136-ecdc-2d88b35d2e09.png)

loss値
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472670/a26adf98-7300-5d0d-1697-02839d4ceec6.png)

### 正則化
#### Weight Decay
機械学習の時のように、L2正則化と同じように、重みを大きくすることに対するペナルティを持たせる方法

#### Dropout
ニューロンをランダムに消去する。
データが流れるたびに消去するニューロンもランダムに選択する。

なお、テスト時には訓練時に消去した割合を乗算して出力する。
![fig06_22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472670/826a4e83-c22d-091d-6ecb-8b0db9655303.png)

### ハイパーパラメータ
具体的には以下
- ニューロンの数
- バッチサイズ（小さすぎると安定しない）
- 学習率（～0.1くらい？）
- Weight Decay
- ドロップアウト率（20％～50％が良いらしい）