[Review] ニューラルネットワークと深層学習 CH.2 ~逆伝播の仕組み~

① 勉強テーマ
ニューラルネットワークが深層学習へと進化していく過程
② 勉強の背景
自習の一環として、これまで深層学習のことを勉強をしてきたが、それがどういった問題から生じてきた手法であったのかの原点に立ち返ることが必要であると感じたので
③ 勉強の方法
こちらのリンクに掲載をしていただいている著者の素晴らしいリソースを拝借いたします
http://nnadl-ja.github.io/nnadl_site_ja/chap2.html
④ 期待される成果
深層学習の生まれたきっかけをつかみ、根本的な問題解決の新しい方法を見つけること

[CH.1](https://qiita.com/Rowing0914/items/5df3b7c08fc70d3047ff)はこちらです。

## 構成

1. ウォーミングアップ：ニューラルネットワークの出力の行列を用いた高速な計算
2. コスト関数に必要な2つの仮定
3. アダマール積 s⊙t
4. 逆伝播の基礎となる4つの式
5. 逆伝播アルゴリズム
6. 逆伝播の実装
7. 逆伝播が速いアルゴリズムであるとはどういう意味か？
8. 逆伝播：全体像

## 前座
>
逆伝播アルゴリズムはもともと1970年代に導入されました。 しかし逆伝播が評価されたのは、 David Rumelhart・ Geoffrey Hinton・ Ronald Williams による1986年の著名な論文が登場してからでした。 その論文では、逆伝播を用いると既存の学習方法よりもずっと早く学習できる事をいくつかのニューラルネットワークに対して示し、それまでニューラルネットワークでは解けなかった問題が解ける事を示しました。 今日では、逆伝播はニューラルネットワークを学習させる便利なアルゴリズムです。

## 1. ウォーミングアップ：ニューラルネットワークの出力の行列を用いた高速な計算
まず初めに、重みの記述方式を固定する。$W^l_{jk}$
ここでいう$l$は層の番号であり、$jk$は前の層の$k$個目のニューロンから$j$個目のニューロンへの重みを意味する。
<img width="628" alt="Screen Shot 2018-03-25 at 18.55.53.png" src="https://qiita-image-store.s3.amazonaws.com/0/240801/cbd1724f-56d8-aca9-e286-cc8e475ad873.png">

これらの表記を用いると、l番目の層のj番目のニューロンの活性aljは、(l−1)番目の層の活性と以下の式で関係付けられる。

```math
a^l_j=σ(∑_k w^l_{jk} a^{l−1}_k + b^l_j)
```
また、シグモイド関数等の活性関数に関しても、同様にベクトル表現化をする。
例として、$f(x) = x^2$を想定すると、下記のように記述できる。

```math
f \biggl(
\begin{bmatrix}
2\\
3
\end{bmatrix}
\biggl )
\space = \space
\begin{bmatrix}
f(2)\\
f(3)
\end{bmatrix}
\space = \space
\begin{bmatrix}
4\\
9
\end{bmatrix}
```

## 2. コスト関数に必要な２つの仮定
前章で定義したコスト関数を振り返って見たい。

```math
C = \frac{1}{2n}\sum_x \space || y(x) - a^L(x)||^2
```
ここでは、nは訓練例の総数、和は個々の訓練例xについて足しあわせたもの、y=y(x)は対応する目標の出力、Lはニューラルネットワークの層数、aL=aL(x)はxを入力した時のニューラルネットワークの出力のベクトルである。

仮定 1. コスト関数はここの訓練データに対するコスト関数の平均$C = \frac{1}{n}\sum_xC_x$で書かれている。
なので、きちんと逆伝播によってエラーがトレーニングデータごとに伝わっていくということを認識したい。

仮定 2. コスト関数はニューラルネットワークの出力の関数で書かれている
これは、アウトプットがきちんとニューラルネットの構造に適するように行われるという当然の仮定である。

## 3. アダマール積 s⊙t
今後の議論を展開していくにあたり結構重要な式。
これの理解は数学的には簡単だが、このニューラルネットワークにおいては誤差を左から右に伝播するという直感的理解を助ける役割を担っているのである。

```math
\begin{bmatrix}
1\\
2
\end{bmatrix}
⊙
\begin{bmatrix}
3\\
4
\end{bmatrix}
\space = \space
\begin{bmatrix}
1 * 3\\
2 * 4
\end{bmatrix}
\space = \space
\begin{bmatrix}
3\\
8
\end{bmatrix}
```

## 4. 逆伝播の基礎となる4つの式
ここで、一度立ち止まり多層ニューラルネットにしてしまうと各ニューロンはどれほど大事なのかを考えてみる。
つまり、なぜニューラルネットワークの各ニューロンがネットワークに与える影響ってどれほどあるのだろうか。
今自分が$l$, $j$のニューロンの視点に立って見よう。
ニューロンにデータが来たときに少々ノイズを加えて次の層に送るという動きをしていると仮定する。
つまり、$\sigma(z^l_j)$の代わりに、$\sigma(z^l_j + z^l_j)$
そう、この変化はネットワークの後段の層に影響を与えていると言える。
そこでこの変化の仕方を少しづつ変えていき、なるべくコスト関数を最小にできる様な値を見つける。

```math
\delta^l_j = \frac{∂C}{∂z^l_j}
```
これを確認するために奥深い逆伝播の４つの大事な方程式を見ることになる。

1. 出力層での誤差$\delta^L$に関する式
2. 誤差$\delta^L$の次の層での誤差$\delta^{L+1}$に関する式
3. 任意のバイアスに関するコストの変化率の式
4. 任意の重みについてのコストの変化率の式

- 1. 出力層での誤差$\delta^L$に関する式

```math
\begin{align}
\delta^L_j &= \frac{∂C}{∂z^l_j}\\
&= \sum_k \frac{∂C}{∂a^L_k} \frac{∂a^L_k}{∂z^L_j} \\
& where \space if \space k≠j \space then \space \frac{∂a^L_k}{∂z^L_j} = 0\\
&= \frac{∂C}{∂a^L_k} \frac{∂a^L_k}{∂z^L_j}\\
&= \frac{∂C}{∂a^L_j} \space \sigma'(z^L_j)
\end{align}
```
$\frac{∂C}{∂a^L_j}$　にて出力活性関数の変化がどの程度敏感に変化するかの度合いを図っている。
つまりC が出力層の特定のニューロン（例えばj番目）にそれほど依存していなければ、我々の期待通り$\delta^L$は小さくなる。一方、右辺の第2項の$\sigma'(z^L_j)$は活性関数σが$z^L_j$の変化にどの程度敏感に反応するかの度合いを表しています。
ここで先ほど出て来たアダマール積を使用して表記をしてみる。

```math
\delta^L = \nabla_a C ⊙ \sigma'(z^L)\\
C = \frac{1}{2} \sum_j(y_j−a_j) \leftrightarrow \frac{∂C}{∂a^L_j} = (a_j−y_j)\\
\delta^L = (a^L - y) ⊙ \sigma' (z^L)
```
実装をすると下記の様になる。

```python
# where L means current layer
delta_L = cost_function_derivative(activations[L], y) * sigmoid_derivative(z[L-1])
```
こちらは実際のMicheal Nielsenの実装を少々変化させたものである。
彼の実装に関しては本文末尾を参照いただきたい。実際にbackprop関数において使用されている。



- 2. 誤差$\delta^L$の次の層での誤差$\delta^{L+1}$に関する式

```math
\delta^L = ((w^{l+1})^T \delta^{L+1}) ⊙ \sigma'(z^l)
```
これを補足すると

```math
\begin{align}
\delta^l_j &= \frac{∂C}{∂z^l_j}\\
& = \sum_k \frac{∂C}{∂z^{l+1}_k} \frac{∂z^{l+1}_k}{∂z^{l}_j}\\
& = \sum_k \frac{∂z^{l+1}_k}{∂z^{l}_j} \delta^{l+1}_k \\

& Also, \\
z^{l+1} &= \sum_j w^{l+1}_{kj}a^l_j + b^{l+1}_j\\
&= \sum_j w^{l+1}_{kj} \sigma(z^l_j) + b^{l+1}_j\\
&Hence\\
\frac{∂z^{l+1}_k}{∂z^{l}_j} &= w^{l+1}_{kj} \sigma'(z^l_j)\\
&Finally,\\
\delta^l_j &= \sum_k w^{l+1}_{kj} \delta^{l+1}_k \sigma'(z^l_j)\\
\delta^L &= ((w^{l+1})^T \delta^{L+1}) ⊙ \sigma'(z^l)
\end{align}
```
この式の意味するところは、後ろの層の誤差をきちんと伝播できているのかを証明することにある。
$((w^{l+1})^T)$の転置行列は直感的には誤差をネットワークとは逆方向に伝播させていると考える事ができる。そして、アダマール積を用いて誤差を伝播させている。

```python
# where L means current layer
delta_L = np.dot(weight[L].T, delta_L+1) * sigmoid_derivative(z)
```

- 3. 任意のバイアスに関するコストの変化率の式

```math
\frac{∂C}{∂b^l_j} = \delta^l_j
```
- 4. 任意の重みについてのコストの変化率の式

```math
\frac{∂C}{∂w^l_{jk}} = a^{l-1}_k \delta^l_j
```

この式から確認できることは活性ニューロンの値が誤差の伝播に非常に重要であるということである。
どの程度活性できているか、つまり、そのニューロンの重要性に比して誤差の伝播の仕方も変わってくるということである。
![Screen Shot 2018-04-02 at 19.19.57.png](https://qiita-image-store.s3.amazonaws.com/0/240801/c9391af4-c70f-d1c6-b4a5-101e749ec671.png)

まとめると、入力ニューロンが低活性状態であるか、出力ニューロンが飽和状態（低活性もしくは高活性状態）の時には、重みの学習が遅いと言える。

ここに実際のMichaelの実装を乗せておく。

```python
def backprop(self, x, y):
        """コスト関数の勾配を表すタプル"(nabla_b, nabla_w)"を返却する。
        "self.biases" and "self.weights"と同様に、
        "nabla_b"と"nabla_w"はnumpyのアレイのリストで
        各要素は各層に対応する。"""
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        # 順伝播
        activation = x
        activations = [x] # 層ごとに活性を格納するリスト
        zs = [] # 層ごとにzベクトルを格納するリスト
        for b, w in zip(self.biases, self.weights):
            z = np.dot(w, activation)+b
            zs.append(z)
            activation = sigmoid_vec(z)
            activations.append(activation)
        # 逆伝播
        delta = self.cost_derivative(activations[-1], y) * \
            sigmoid_prime_vec(zs[-1])
        nabla_b[-1] = delta
        nabla_w[-1] = np.dot(delta, activations[-2].transpose())
        # 下記のループ変数lは第2章での記法と使用方法が若干異なる。
        # l = 1は最終層を、l = 2は最後から2番目の層を意味する（以下同様）。
        # 本書内での方法から番号付けのルールを変更したのは、
        # Pythonのリストでの負の添字を有効活用するためである。
        for l in xrange(2, self.num_layers):
            z = zs[-l]
            spv = sigmoid_prime_vec(z)
            delta = np.dot(self.weights[-l+1].transpose(), delta) * spv
            nabla_b[-l] = delta
            nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())
        return (nabla_b, nabla_w)
```

## 5. 逆伝播アルゴリズム
Pseudo Code

1. 入力 x： 入力層に対応する活性a1をセットする
2. フィードフォワード： 各$l=2,3,…,L$に対し、$z^l=w^la^{l−1}+b^l$ and $a^l=σ(z^l)$を計算する
3. 誤差$δ^L$を出力： 誤差ベクトル$δL=∇_a C ⊙ σ'(z^L)$を計算する
4. 誤差を逆伝播： 各$l=L−1,L−2,…,2$に対し、$δ^l=((w^{l+1})^T δ^{l+1}) ⊙ σ'(z^l)$を計算する
5. 出力： コスト関数の勾配は

```math
\frac{∂ C}{∂ w^l_{jk}} = a^{l−1}_k δ^l_j\\
\frac{∂ C}{∂ b^l_j} = δ^l_j
```

ここでトレーニングデータも実際に与える部分も考えてみる。

1. 訓練例のセットを入力
2. 各訓練例xに対して： 対応する活性$a^{x,1}$をセットし、以下のステップを行う：
3. フィードフォワード：$l=2,3,…,L$に対し、$z^{x,l} = w^l a^{x,l−1} + b^l$と$a^{x,l} = σ(z^{x,l})$を計算する
4. 誤差$δ^{x,L}$を出力： ベクトル$δ^{x,L} = ∇_a C_x ⊙ σ'(z^{x,L})$を計算する
5. 誤差を逆伝播する：$l=L−1,L−2,…,2$に対し、$δ^{x,l} = ((w^{l+1})^T δ^{x,l+1} ) ⊙ σ'(z^{x,l})$を計算する
6. 勾配降下：

```math
l=L,L−1,…,2\\
w^l → w^l − η m∑_xδ^{x,l} (a^{x,l−1})^T\\
b^l → b^l − ηm ∑_x δ^{x,l}
```