手写Python反向传播

1.概要

本文来自博客 A Neural Network in 13 lines of Python
这篇博客只用了13行代码就实现了Python版本的反向传播,虽然有很多文章都对它进行了翻译,但在最关键反向传播部分,并没有给出详细公式,下面就对代码中的反向传播进行重点介绍。

2.全部反向传播代码

下面是13行完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import numpy as np
X = np.array([ [0,0,1],[0,1,1],[1,0,1],[1,1,1] ])
y = np.array([[0,1,1,0]]).T
lr,hidden_dim = (0.5,4)
w0 = 2*np.random.random((3,hidden_dim)) - 1
w1 = 2*np.random.random((hidden_dim,1)) - 1
for j in range(60000):
layer1 = 1/(1+np.exp(-(np.dot(X,w0))))
layer2 = 1/(1+np.exp(-(np.dot(layer1,w1))))
layer2_delta = (layer2 - y)*(layer2*(1-layer2))
layer1_delta = layer2_delta.dot(w1.T) * (layer1 * (1-layer1))
w1 -= (lr * layer1.T.dot(layer2_delta))
w0 -= (lr * X.T.dot(layer1_delta))
print(layer2)

3.关键行解释

变量 定义
X 输入数据矩阵,每行是一个训练样例
y 输出数据矩阵,每行是一个训练样例
layer1 神经网络的隐含层,输入来自X
layer2 神经网络的输出层,输入来自layer1
w1 输出层权重,连接layer1与layer2
w0 隐含层权重,连接X与layer1

line1-6: 前6行都是在定义初始化参数,学习率和权重矩阵等等,这些应该很好理解。
line8-9: 这两行是在进行前向传播,激活函数为sigmoid。
line10-13: 这四行是在进行反向传播,下面来说说代码为什么这么写

首先整体误差:

$E_{total}=\frac{1}{2}(y-out)^2 $

先计算输出层权重矩阵w1,如果我们想知道w1对整体误差产生了多少影响,可以用整体误差对w1求偏导求出:(链式法则)

$\frac{dE}{dw_1} = \frac{dE}{dout}\ast\frac{dout}{dnet_o}\ast\frac{dnet_o}{dw_1}$

分别计算上面的每一项

$\frac{dE}{dout} = -(y-out)$

$\frac{dout}{dnet_o} = out(1-out)$ 这一项就是sigmoid的导数

$\frac{dnet_o}{dw_1}=out_h$ 这一项就是隐含层的输出结果

为了表达方便,$\delta_o$用来表示输出层的误差:

$\delta=\frac{dE}{dout}*\frac{dout}{dnet_o}=\frac{dE}{dnet_o}$

最终的结果可以简化为

$\frac{dE}{dw_1}=\delta_o*out_h$

最后我们来更新w1的值:

$w_1 =w_1-\eta\ast\frac{dE}{dw_1} $

现在让我们回过头来看看代码
layer2_delta = (layer2 - y)* (layer2*(1-layer2))
layer2_delta正是在计算$\delta_0$
w1 -= (lr * layer1.T.dot(layer2_delta))
w1正是在计算$\delta_o\ast out_h$ 然后乘以学习率,在更新到原有的w1上

下面我们再来看看隐含层权重矩阵w0的更新情况

$\frac{dE}{dw_0} = \frac{dE}{dout_h}\ast\frac{dout_h}{dnet_h}\ast\frac{dnet_h}{dw_0}$

总共有三项,我们先计算第一项,将第一项展开

$\frac{dE}{dout_h}=\frac{dE}{dnet_o}\ast\frac{dnet_o}{dout_h}$

再展开$\frac{dE}{dnet_o}$

$\frac{dE}{dnet_o}=\frac{dE}{dout}\ast\frac{dout}{dnet_o}$

仔细看这两项的已经在上面计算输出层权重偏导的时候计算过了

然后我们又知道

$\frac{dnet_o}{dout_h}=w_1$

所以$\frac{dE}{dout_h}$ 这一项已经计算完成,还剩后面两项

$\frac{dout_h}{dnet_h}=out_h\ast(1-out_h)$ 这一项依旧是sigmoid的导数

最后还剩$\frac{dnet_h}{dw_0}$

$\frac{dnet_h}{dw_0}=X$ 这一项就是输入X

最后我们将3项相乘,可以得到结果
为了简化公式,用$\delta_h$表示隐含层单元的误差:
化简公式如下

$\frac{dE}{dw_0}=\delta_o*w_1\ast out_h(1-out_h)\ast X$

$\frac{dE}{dw_0}=\delta_h\ast X$

$w_0 =w_0-\eta\ast\frac{dE}{dw_0} $

现在我们再回到代码,看看更新w0的部分
layer1_delta = layer2_delta.dot(w1.T) (layer1 (1-layer1))
w0 -= (lr * X.T.dot(layer1_delta))
layer1_delta正是在计算$\delta_h$
最终再乘以学习率,完成对隐含层权重w0的更新

以上就是全部的反向传播计算过程。