誤差逆伝播法の実装を完璧に理解してみる How to Construct a Backpropagation Algorithm from the Scratch

下記の実装の意味を考えることにしたが、わからなかったのでググってみたが、ネットには自分の必要な情報がなかったので自分で考えてみた。

```
import numpy as np
from common import functions
import matplotlib.pyplot as plt

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

# ウェイトとバイアスを設定
# ネートワークを作成
def init_network():
    print("##### ネットワークの初期化 #####")

    network = {}
    network['W1'] = np.array([
        [0.1, 0.3, 0.5],
        [0.2, 0.4, 0.6]
    ])

    network['W2'] = np.array([
        [0.1, 0.4],
        [0.2, 0.5],
        [0.3, 0.6]
    ])

    network['b1'] = np.array([0.1, 0.2, 0.3])
    network['b2'] = np.array([0.1, 0.2])
    
    print_vec("重み1", network['W1'])
    print_vec("重み2", network['W2'])
    print_vec("バイアス1", network['b1'])
    print_vec("バイアス2", network['b2'])

    return network

# 順伝播
def forward(network, x):
    print("##### 順伝播開始 #####")

    W1, W2 = network['W1'], network['W2']
    b1, b2 = network['b1'], network['b2']
    
    u1 = np.dot(x, W1) + b1
    z1 = functions.relu(u1)
    u2 = np.dot(z1, W2) + b2
    y = functions.softmax(u2)
    
    print_vec("総入力1", u1)
    print_vec("中間層出力1", z1)
    print_vec("総入力2", u2)
    print_vec("出力1", y)
    print("出力合計: " + str(np.sum(y)))

    return y, z1

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

    grad = {}

    W1, W2 = network['W1'], network['W2']
    b1, b2 = network['b1'], network['b2']
    #  出力層でのデルタ
    delta2 = functions.d_sigmoid_with_loss(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)
    # b1の勾配  
    grad['b1'] = np.sum(delta1, axis=0)
    #  W1の勾配
    grad['W1'] = np.dot(x.T, delta1)
        
    print_vec("偏微分_dE/du2", delta2)
    print_vec("偏微分_dE/du2", delta1)

    print_vec("偏微分_重み1", grad["W1"])
    print_vec("偏微分_重み2", grad["W2"])
    print_vec("偏微分_バイアス1", grad["b1"])
    print_vec("偏微分_バイアス2", grad["b2"])

    return grad
    
# 訓練データ
x = np.array([[1.0, 5.0]])
# 目標出力
d = np.array([[0, 1]])
#  学習率
learning_rate = 0.01
network =  init_network()
y, z1 = forward(network, x)

# 誤差
loss = functions.cross_entropy_error(d, y)

grad = backward(x, d, z1, y)
for key in ('W1', 'W2', 'b1', 'b2'):
    network[key]  -= learning_rate * grad[key]

print("##### 結果表示 #####")    


print("##### 更新後パラメータ #####") 
print_vec("重み1", network['W1'])
print_vec("重み2", network['W2'])
print_vec("バイアス1", network['b1'])
print_vec("バイアス2", network['b2'])
```
下記の式のような解釈が必要だが、ポイントは意外と簡単である。下記の式は、z1,u1,u2,yの式から導ける。例えば、z1をb1で微分するときは、合成関数の微分の公式を使う

grad['W1'] = np.dot($x.T$, delta1) = ∂E/∂w1
grad['W2'] = np.dot($z_{1}.T$, delta2) = ∂E/∂w2
delta 1 = $∂E$/$∂{z_{1}}$
delta 2 = $∂E$/$∂y$
grad['b1'] = $∂E$/$∂{b_{1}}$
grad['b2'] = $∂E$/$∂{b_{2}}$

ポイントになる式:

$∂E/∂{Z_{1}}$ = $∂E/∂yw_{2}^{T}$
$∂E/∂w_{2}$ = ${z_{1}}^{T}∂E/∂y$
$∂E/∂w_{1}$ = ${X}^{T}∂E/∂z_{1}$
$∂E/∂{b_{1}}$ = $∂E$/$∂{z_{1}} ∂{z_{1}}/∂{b_{1}}$ = delta 1
$∂E/∂{b_{2}}$ = $∂E$/$∂y ∂y/∂{b_{2}}$ = delta 2
z1 = functions.relu(u1)
u1 = np.dot(x, W1) + b1
u2 = np.dot(z1, W2) + b2
y = u2