レポート[深層学習 day1]

##Section1 : 入力層～中間層
###要点
ニューラルネットワークは，入力層，中間層，出力層からなる．入力層から次の層への変換は重み$w_i$，バイアス$b$を用いて以下のように線形結合で表す．
$$u=W\boldsymbol x+b$$
重み$w_i$とバイアス$b$は一次関数における傾きと切片に相当する．重み$w_i$のサイズは任意に設定する．サイズが大きいほど表現力が高くなる．しかし，大きすぎると過学習が起こる可能性がある．
###実装演習
```
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", u1)
    print_vec("中間層出力1", z1)
    print_vec("総入力2", u2)
    print_vec("出力1", z1)
    print("出力合計: " + str(np.sum(z1)))

    return y, z1, z2
```
###「確認テスト」について自身の考察結果
ディープラーニングの目的は，多量のデータを多数の中間層を介して，入力値に対する識別・生成となる出力を得るためのモデルを構築すること．
次式の数式をPythonで表記するには内積np.dotを使うと便利である．
$$u=w_1x_1+w_2x_2+\cdots+b=Wx+b$$

```
u=np.dot(W, x)+b
```
##Section2 : 活性化関数
###要点
活性化関数はニューラルネットワークにいて，ある層から次の層へ信号伝達する大きさを決める非線形関数である．入力値に基づいて信号をON/OFFに変換したり，信号の強弱を定めたりする．
活性化関数は中間層と出力層それぞれに用いられ，以下のようなものがある．

- 中間層用の活性化関数
    - ReLU関数
    - シグモイド関数
    - ステップ関数
- 出力層用の活性化関数
    - ソフトマックス関数
    - 恒等写像
    - シグモイド関数

現在よく使用される活性化関数はReLU関数であり，次式で表される．

```math
f(x) = \left\{
\begin{array}{ll}
x & (x>0) \\
0 & (x\le 0)
\end{array}
\right.
```
この関数は，シグモイド関数で起こりうる勾配消失問題を回避し，スパース化に貢献できる．
###実装演習

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

``` 1_1_forward_propagation.ipynb
 z1 = functions.relu(u1)
```
###「確認テスト」について自身の考察結果
関数$f(x)$が次式を満たすとき，それは線形性を有する．次式を満たさなければ，関数は非線形である．
$$f(c_1x_1+c_2x_2)=c_1f(x_1)+c_2f(x_2)$$

活性関数の実装は，例えばLeLU関数は以下のように記述できる．

```
def relu(x):
    return np.maximum(0, x)
```
##Section3 : 出力層
###要点
出力層では，最終的に判断したい結果を出力する必要がある．例えば，分類問題であれば，各クラスの確率が出力層で得られる．
重みとバイアスのパラメータを学習させるため，出力データ(予測)は訓練データ(正解)との誤差を算出する．出力値が$y_j$，訓練データが$d_j$のとき，二乗和誤差は次式で表される．
$$En(\boldsymbol w)=\frac{1}{2}\sum_{j=1}^J(y_i-d_j)^2$$

出力層は扱う問題に応じて活性化関数，誤差関数が以下のように使用される．

| | 回帰 |二値分類     |     他クラス分類 |       
|:-----------------|:------------------|:---|:---|
|活性化関数|恒等写像|シグモイド関数|ソフトマックス関数|
|誤差関数|二乗誤差|交差エントロピー|交差エントロピー|




###実装演習

``` functions.py
    def cross_entropy_error(d, y):
    if y.ndim == 1:
        d = d.reshape(1, d.size)
        y = y.reshape(1, y.size)
        
    # 教師データがone-hot-vectorの場合、正解ラベルのインデックスに変換
    if d.size == y.size:
        d = d.argmax(axis=1)
             
    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size), d] + 1e-7)) / batch_size
```

```
loss = functions.cross_entropy_error(d, y)
```
###「確認テスト」について自身の考察結果
二乗和誤差が予測値と訓練データとの差に対して二乗の操作をしている理由は，各ラベルの誤差を絶対値にするため．また，係数$1/2$がある理由は，誤差関数の微分の式表現が簡単になるため．
ソフトマックス関数の実装の仕方を説明する．以下のようにコーディングできる．

```
def softmax(x):
    x = x - np.max(x) #ここはオーバーフロー対策．指数関数は指数の値が大きいと(例えば10000など)オーバーフローを起こす可能性がある．そのため，指数を予めあるで引いて小さい値にしておくことでオーバーフローを防ぐ．
    return np.exp(x) / np.sum(np.exp(x)) #ここが本来の関数の計算
```
交差エントロピーの実装の仕方を説明する．以下のようにコーディングできる．

```
def cross_entropy_error(d, y):
　　if y.ndim == 1:
        d = d.reshape(1, d.size)
        y = y.reshape(1, y.size)
        
    # 教師データがone-hot-vectorの場合、正解ラベルのインデックスに変換
    if d.size == y.size:
        d = d.argmax(axis=1)
             
    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size), d] + 1e-7)) / batch_size #1e-7という少量の正数を加えて対数の計算エラーを防ぐ．
```


##Section4 : 勾配降下法
###要点
勾配降下法は誤差$E(\boldsymbol w)$を最小化するパラメータ$\boldsymbol w$を探査するための方法である．次式で表現できる．
$$\boldsymbol w^{(t+1)}=\boldsymbol w^{(t)}-\epsilon \nabla E$$
ここで，$\epsilon$は学習率と呼び，これを適切に設定することで効率的に学習が行われる．学習率が多きすぎると発散する可能性がある．逆に大きすぎると，収束に時間がかかる，または，局所的な最適解に収束する可能性がある．
確率的郊外降下法(SGD)はランダムに抽出したサンプルから誤差勾配を計算する手法である．以下のメリットがある．

- 計算コストを削減できる．→オンライン学習が可能になる．
- 局所最適解に収束するリスクを軽減できる．

データはミニバッチにして，それぞれ勾配降下法を適用することで，計算機の並列計算など，計算資源の有効活用ができる．
誤差勾配を求める方法には，数値微分と誤差逆伝播法がある．数値微分は計算負荷が大きいため，誤差逆伝播法を利用する．

###実装演習
誤差勾配の実装は計算コストの良い誤差逆伝播法を使用する．実装はSection5: 誤差逆伝播法と合わせて記載する．

###「確認テスト」について自身の考察結果
オンライン学習とバッチ学習について説明する．

- オンライン学習：学習データが入ってくるたびに都度パラメータを更新する方法．
- バッチ学習：一度にすべての学習データを用いてパラメータ更新する方法．

勾配降下法の次式の計算プロセスを下図を用いて丁寧に説明する．
$$\boldsymbol w^{(t+1)}=\boldsymbol w^{(t)}-\epsilon \nabla E$$

| エポック| 重み | 次エポックへの重みの変化量 |
|:-:|:-:|:-:|
| $t$  |   $w^t$| $-\epsilon \nabla E_t$ |
| $t+1$  |   $w^{t+1}$| $-\epsilon \nabla E_{t+1}$  |
| $t+2$  |   $w^{t+2}$| $-\epsilon \nabla E_{t+2}$  |

##Section5 : 誤差逆伝播法
###要点
誤差逆伝播法は誤差勾配の計算を効率的に行う手法である．誤差勾配の計算は数値微分のライブラリを用いれば実装は容易であるが，しかしながら計算時間がかかるという問題点がある．誤差逆伝播法はこの問題点に対して，ニューラルネットワークの計算過程のける微分作用を層ごとに分割し，各層での微分値を解析的に計算して実装することで計算時間を短縮化する狙いがある．

###実装演習

```
# 誤差逆伝播
def backward(x, d, z1, y):
    # print("\n##### 誤差逆伝播開始 #####")    

    grad = {}
    
    W1, W2 = network['W1'], network['W2']
    b1, b2 = network['b1'], network['b2']

    # 出力層でのデルタ
    delta2 = functions.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) * functions.d_relu(z1)

    ## 試してみよう
    delta1 = np.dot(delta2, W2.T) * functions.d_sigmoid(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)
    
    # print_vec("偏微分_重み1", grad["W1"])
    # print_vec("偏微分_重み2", grad["W2"])
    # print_vec("偏微分_バイアス1", grad["b1"])
    # print_vec("偏微分_バイアス2", grad["b2"])

    return grad
```

###「確認テスト」について自身の考察結果
誤差関数$E$から逆伝播して誤差勾配$\frac{\partial E}{\partial \boldsymbol w}$を求める過程のソースコードを整理する．

| 数式 | コード |
|:-:|:-:|
|$\frac{\partial E}{\partial \boldsymbol y}$   | delta2 = functions.d_mean_squared_error(d, y)  |
|$\frac{\partial E}{\partial \boldsymbol y}\frac{\partial \boldsymbol y}{\partial \boldsymbol u}$   |  delta1 = np.dot(delta2, W2.T) * functions.d_sigmoid(z1)  |
|$\frac{\partial E}{\partial \boldsymbol y}\frac{\partial \boldsymbol y}{\partial \boldsymbol u}\frac{\partial \boldsymbol u}{\partial \boldsymbol w}$    |grad['W1'] = np.dot(x.T, delta1)   |