【ラビットチャレンジ（E資格）】深層学習（day1）

#はじめに
2021/2/19・20に実施される日本ディープラーニング協会(JDLA)Ｅ資格合格を目指して、ラビットチャレンジを受講した際の学習記録です。

###科目一覧
[応用数学](https://qiita.com/yoshikou/items/9e677e33138686f7c276)
[機械学習](https://qiita.com/yoshikou/items/bce5818c3cff72274702)
[深層学習（day1）](https://qiita.com/yoshikou/items/2d6ace77f6cdd71024c1)
[深層学習（day2）](https://qiita.com/yoshikou/items/206eb27d1a9ef3034d5f)
[深層学習（day3）](https://qiita.com/yoshikou/items/42418e9cb7a764a9ad54)
[深層学習（day4）](https://qiita.com/yoshikou/items/e7342d39d7e2c7946dc7)


#全体像
ディープラーニングは、結局何をやろうとしているのか。
→　重みとバイアスを最適化して、誤差を最小化するパラメータを発見すること。

- 入力層：2ノード1層、中間層：3ノード2層、出力層：1ノード1層のネットワーク
<img width="589" alt="スクリーンショット 2020-07-18 9.27.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508020/b85b41a5-b0f9-5b06-0260-c4e52a404f84.png">

【事前に用意する情報】
入力： $x_n=[x_{n1}…x_{ni}]$
訓練データ： $d_n=[d_{n1}…d_{nk}]$
【多層ネットワークのパラメータ】
$$ w^{(l)} = \left\\{ \begin{array} \\\
重み： & W^{(l)}= \left(
\begin{array}{ccccc}
w_{11}^{(l)} & \cdots & w_{1j}^{(l)} \\\
\vdots & \ddots & \vdots \\\
w_{j1}^{(l)} & \cdots & w_{jj}^{(l)} \\\
\end{array}
\right)\\\
バイアス： & b^{(l)}=[b_1^{(l)}…b_j^{(l)}] \\\
\end{array} \right. $$
活性化関数： $f^{(l)}(u^{(l)})=[f^{(l)}(u_1^{(l)})…f^{(l)}(u_j^{(l)})]$
中間層出力： $z^{(l)}=[z_1^{(t)}…z_K^{(t)}]=f^{(l)}(u^{(l)})$
総入力： $u^{(f)}=W^{(l)}z^{(l-1)}+b^{(l)}$
出力： $y_n^{(t)}=[y_{n1}^{(t)}…y_{nK}^{(t)}]=z^{(L)}$
誤差関数： $E_n(w)$
→ $\nabla E_n(w)=\frac{\partial E}{\partial w}$
→ $w^{(t+1)}=w^{(t)}-\epsilon\nabla E_n(w)$

入力層ノードのインデックス： $i \, (=1…L)$
中間層ノードのインデックス： $j \, (=1…J)$
出力層ノードのインデックス： $k \, (=1…K)$
層のインデックス： $l \, (=1…L)$
訓練データのインデックス： $n \, (=1…N)$
試行回数のインデックス： $t \, (=1…T)$


#Section1：入力層〜中間層
###入力層
$$ z_i^{(1)} = x_i, \; z^{(1)} = x $$
入力ユニットは活性化関数を持たず、単に値 $x_i$ を出すだけ。
入力層はニューラルネットの層数には数えない。

###中間層
第 $l$ 層中の $j$ 番目のユニットを考えると、第 $l-1$ 層の各ユニットからの入力 $z_i^{(l-1)}$ を入力として受け入れるので、活性は
$$ u_j^{(l)} = \sum_i w_{ji}^{(l)} z_{i}^{(l-1)} $$
となる。

第 $l-1$ 層のユニット $i$と第 $l$ 層のユニット $j$ の間の結合の重み $w_{ji}^{(l)}$ を $(j,i)$ 成分とする重み行列
$$ W^{(l)} = \left(
\begin{array}{ccccc}
w_{11}^{(l)} & w_{12}^{(l)} & \cdots \\\
w_{21}^{(l)} & w_{22}^{(l)} &  \\\
\vdots & & \ddots \\\
\end{array}
\right) $$
を導入し、さらにバイアスもまとめて縦ベクトル $b^{(l)}$

$$ u^{(l)} = W^{(l)}z^{(l-1)}, \; z^{(l)}=f^{(l)}\bigl(u^{(l)}+b^{(l)}\bigl) $$

```python:入力層：2ノード1層、中間層：3ノード2層、出力層：1ノード1層のネットワーク
import numpy as np

def print_vec(text, vec):
    print("*** " + text + " ***")
    print(vec)
    print("shape: " + str(vec.shape))
    print("")

# ReLU関数
def relu(x):
    return np.maximum(0, x)    

# ウェイトとバイアスを設定
# ネートワークを作成
def init_network():
    print("##### ネットワークの初期化 #####")
    network = {}
        
    network['W1'] = np.random.rand(2,3)
    network['W2'] = np.random.rand(3,3)
    network['W3'] = np.random.rand(3,1)
    
    network['b1'] = np.random.randn(3)
    network['b2'] = np.random.randn(3)
    network['b3'] = np.random.randn(1)

    print_vec("重み1", np.round(network['W1'], 2))
    print_vec("重み2", np.round(network['W2'], 2) )
    print_vec("重み3", np.round(network['W3'], 2))
    print_vec("バイアス1", np.round(network['b1'], 2))
    print_vec("バイアス2", np.round(network['b2'], 2))
    print_vec("バイアス3", np.round(network['b3'], 2))

    return network

# プロセスを作成
def forward(network, x):
    
    print("##### 順伝播開始 #####")

    W1, W2, W3 = network['W1'], network['W2'], network['W3']
    b1, b2, b3 = network['b1'], network['b2'], network['b3']
    
    # 1層の総入力
    u1 = np.dot(x, W1) + b1
    
    # 1層の総出力
    z1 = functions.relu(u1)
    
    # 2層の総入力
    u2 = np.dot(z1, W2) + b2
    
    # 2層の総出力
    z2 = functions.relu(u2)

    # 出力層の総入力
    u3 = np.dot(z2, W3) + b3    

    # 出力層の総出力
    y = u3
    
    print_vec("中間層1の総入力", np.round(u1,2))
    print_vec("中間層1の総出力", np.round(z1,2))
    print_vec("中間層2の総入力", np.round(u2,2))
    print_vec("中間層2の総出力", np.round(z2,2))
    print_vec("出力層の総入力", np.round(u3,2))
    print("出力層の総出力: " + str(np.round(y,2)))

    return y, z1, z2

# 入力値
x = np.random.randint(0,2,2)
print_vec("入力", x)

# ネットワークの初期化
network =  init_network()

y, z1, z2 = forward(network, x)
```

#Section2：活性化関数
- ステップ関数
$$ f(x) = \left\\{ \begin{array} \\\
1 & (x \geq 0) \\\
0 & (x \lt 0) \\\
\end{array} \right. $$
閾値を超えたら発火する関数であり、出力は常に1か0である。
課題：0〜1の間の値を表現できず、線形分離可能なものしか学習できない。

- シグモイド関数
$$ f(u) = \frac{1}{1+e^{-u}} $$
0〜1の間を緩やかに変化する関数で、ステップ関数ではON/OFFしかない状態に対し、信号の強弱を伝えられるようになり、ニューラルネットワーク普及のきっかけとなった。
課題：大きな値では出力の変化が微小なため、勾配消失問題を引き起こすことがある。0になることはないので、計算リソースが常に食われてしまう。

```python:step/sigmoid_function
import numpy as np
import matplotlib.pyplot as plt

x = np.arange(-10, 10, 0.1)

# step_function
def step(x):
    return np.where( x > 0, 1, 0) 

y_step = step(x)
plt.plot(x, y_step,label='step_function')

# sigmoid_function
def sigmoid(x):
    return 1/(1+np.exp(-x))

y_sigmoid = sigmoid(x)
plt.plot(x, y_sigmoid,label='sigmoid_function')
plt.legend()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508020/07ae2607-7c78-ed0b-0282-4b82eae54325.png)

- ReLU関数
$$ f(x) = \left\\{ \begin{array} \\\
x & (x > 0) \\\
0 & (x \leq 0) \\\
\end{array} \right. $$
勾配消失問題の回避とスパース化に貢献することで良い成果をもたらしている。

```python:relu_function
# relu_function
def relu(x):
    return np.maximum(0,x)

x = np.arange(-5, 5, 0.1)
y_relu = relu(x)

plt.plot(x, y_relu,c='g',label='relu_function')
plt.legend()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508020/2e8eeefd-7ca9-5fc0-2ccd-99f21327629a.png)


#Section3：出力層
###誤差関数
$$ E_n(w) = \frac{1}{2} \sum_{j=1}^J(y_j-d_j)^2 = \frac{1}{2} ||(y-d)||^2 $$
なぜ引き算ではなく、二乗するか → 値を正にするため
$\frac{1}{2}$ はどういう意味を持つか → 微分を可能にするため

###出力層の活性化関数
|             | 回帰     | 二値分類                    | 多クラス分類                                    |
|:-----------:|:--------:|:-------------------------:|:---------------------------------------------:|
| 活性化関数    | 恒等写像  | シグモイド関数                | ソフトマックス関数                                 |
| 活性化関数(式)  | $f(u)=u$ | $f(u)=\frac{1}{1+e^{-u}}$ | $f(i,u)=\frac{e^{u_i}}{\sum_{k=1}^K e^{u_k}}$ |
| 誤差関数       | 二乗誤差  | 交差エントロピー               | 交差エントロピー                                  |


#Section4：勾配降下法

深層学習の目的は、学習を通して誤差を最小にするネットワークを作成すること
→　誤差 $E(w)$ を最小化するパラメータ $w$ を発見すること


$$ \text{誤差勾配} \quad \nabla E = \frac{\partial E}{\partial w} = \Bigl[\frac{\partial E}{\partial w_1}…\frac{\partial E}{\partial w_M}\Bigl] $$

|          | 勾配降下法     | 確率的勾配降下法(SGD)         | ミニバッチ勾配降下法                 |
|:--------:|:--------:|:-------------------------:|:---------------------------------------------:|
| 式        | $w^{(t+1)}=w^{(t)}-\epsilon\nabla{E}$ | $w^{(t+1)}=w^{(t)}-\epsilon\nabla{E_n}$ | $w^{(t+1)}=w^{(t)}-\epsilon\nabla{E_t}, \; E_t=\frac{1}{N_t}\sum_{n \in D_t}{E_n}, \; N_t= D_t $ |
| 特徴      | 全サンプルの平均誤差 | ランダムに抽出したサンプルの誤差 | ランダムに分割したデータの集合（ミニバッチ）$D_t$に属するサンプルの平均誤差 |
| メリット    |  | データが冗長な場合の計算コストの軽減。望まない局所極小解に収束するリスクの軽減。オンライン学習ができる。 | 確率的勾配法のメリットを損なわず、計算機の計算資源を有効利用できる。 |


```pythom:Stochastic_Gradient_Descent
import numpy as np
import matplotlib.pyplot as plt
    
# ReLU関数
def relu(x):
    return np.maximum(0, x)    

# 平均二乗誤差
def mean_squared_error(d, y):
    return np.mean(np.square(d - y)) / 2

# ReLU関数の導関数
def d_relu(x):
    return np.where( x > 0, 1, 0)
    
# 平均二乗誤差の導関数
def d_mean_squared_error(d, y):
    if type(d) == np.ndarray:
        batch_size = d.shape[0]
        dx = (y - d)/batch_size
    else:
        dx = y - d
    return dx

# サンプルとする関数
def f(x):
    y = 3 * x[0] + 2 * x[1]
    return y

# 初期設定
def init_network():
    network = {}
    nodesNum = 5
    network['W1'] = np.random.randn(2, nodesNum)
    network['W2'] = np.random.randn(nodesNum)
    network['b1'] = np.random.randn(nodesNum)
    network['b2'] = np.random.randn()

    return network

# 順伝播
def forward(network, x):
    W1, W2 = network['W1'], network['W2']
    b1, b2 = network['b1'], network['b2']
    u1 = np.dot(x, W1) + b1
    z1 = relu(u1)
    u2 = np.dot(z1, W2) + b2
    y = u2
    
    return z1, y

# 誤差逆伝播
def backward(x, d, z1, y):
    grad = {}
    
    W1, W2 = network['W1'], network['W2']
    b1, b2 = network['b1'], network['b2']

    # 出力層でのデルタ
    delta2 = d_mean_squared_error(d, y)
    # b2の勾配
    grad['b2'] = np.sum(delta2, axis=0)
    # W2の勾配
    grad['W2'] = np.dot(z1.T, delta2)
    # 中間層でのデルタ
    delta1 = np.dot(delta2, W2.T) * d_relu(z1)
    delta1 = delta1[np.newaxis, :]
    # b1の勾配
    grad['b1'] = np.sum(delta1, axis=0)
    x = x[np.newaxis, :]
    # W1の勾配
    grad['W1'] = np.dot(x.T, delta1)
    
    return grad

# サンプルデータを作成
data_sets_size = 100000
data_sets = [0 for i in range(data_sets_size)]

for i in range(data_sets_size):
    data_sets[i] = {}
    # ランダムな値を設定
    data_sets[i]['x'] = np.random.rand(2)
      
    # 目標出力を設定
    data_sets[i]['d'] = f(data_sets[i]['x'])
    
losses = []
# 学習率
learning_rate = 0.05

# 抽出数
epoch = 1000

# パラメータの初期化
network = init_network()
# データのランダム抽出
random_datasets = np.random.choice(data_sets, epoch)

# 勾配降下の繰り返し
for dataset in random_datasets:
    x, d = dataset['x'], dataset['d']
    z1, y = forward(network, x)
    grad = backward(x, d, z1, y)
    # パラメータに勾配適用
    for key in ('W1', 'W2', 'b1', 'b2'):
        network[key]  -= learning_rate * grad[key]

    # 誤差
    loss = mean_squared_error(d, y)
    losses.append(loss)

print("##### 結果表示 #####")    
lists = range(epoch)

plt.plot(lists, losses, '.')
# グラフの表示
plt.show()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508020/3701fdad-77ad-5f22-6fcd-7ab881cdf372.png)



#Section5：誤差逆伝播法
誤差勾配を数値微分 $ \frac{\partial E}{\partial w_m} = \frac{E(w_m+h)-E(w_m-h)}{2h}$で計算すると、各パラメータ $w_m$ それぞれについて $E(w_m+h)$ や $E(w_m-h)$ を計算するために、順伝播の計算を繰り返し行う必要があり負荷が大きい。
そこで、誤差逆伝播法を利用する。
誤差逆伝播法とは、算出された誤差を出力層側から順に微分し、前の層へと伝播することにより、不要な再帰的計算を避けて各パラメータの微分値を計算する手法。