在学习深度学习过程中,无意间发现一篇介绍BP算法的文章,感觉非常直观,容易理解。这篇文章的最大亮点是:不像其他介绍BP算法的文章,用一堆数据符号和公式来推导。文中通过使用一条具体的样本数据,为我们展示了模型训练中的参数迭代计算过程,为我们理解BP算法提供了很直观的理解视角;其次,作者也给出了使用python来实现BP的算法。只要你了解过传统神经网络结构以及大学微积分的知识,都可以毫不费力的在20分钟内完全理解BP算法。这里整理出来,供大家学习参考。要看原文的同学,直接跳到文末点击原文链接。
在开始之前,提醒下大家,注意公式中的下标,结合网络结构帮忙我们理解算法推导计算过程和细节。
网络结构和样本数据
跟所有训练神经网络或深度学习模型的流程一样,首先要先确定网络结构。这里为了介绍上的方便,以2个输入节点,2个隐藏节点,2个输出节点的网络(包括bias项)为例,展开对BP算法的介绍。如下图所示:
下面作者开始引入网络中参数的初始权重,以及一个训练样本,如下图中节点和边上的数值:
BP算法的目标就是优化神经网络的权重使得学习到的模型能够将输入值正确地映射到实际的输出值(也就是,希望模型能够模型真实数据产生的机制。在统计学中就是,我们要学习一个统计模型(统计分布函数),使得真实数据分布与统计模型产生的样本分布尽可能一致)。
如上图所示,下面的参数求解迭代过程,就是为了使得输入样本是0.05和0.10时(一个2维的样本数据),神经网络的输出与0.01和0.99接近。
前向传播过程
前向传播很简单,就是在已经给定的数据和参数下,按照网络结构来逐层传递数据,最后在输出层计算网络的输出与样本真实的目标值误差,这个误差就是模型的目标函数。
具体到这个case中,在给定模型输出权重和bias的条件下,我们需要把样本数据(0.05,0.10)通过图二中的网络逐步向后传递,看网络的输出与实际的输出的差异。
下面推导计算过程中,网络中使用的激活函数是logistic函数(或sigmoid函数):
$$ \sigma(x) = \frac{1}{1+e^{-x}} $$
首先来计算隐藏节点 $h_1$ 的输入值:
$$ net_{h1} = 0.15 * 0.05 + 0.2 * 0.1 + 0.35 * 1 = 0.3775 $$
得到$h_1$的输入值后,我们使用激活函数(logistic函数)来将输入值转化为为$h_1$的输出值:
$$ out_{h1} = \frac{1}{1+e^{-net_{h1}}} = \frac{1}{1+e^{-0.3775}} = 0.593269992 $$
按同样的方式,我们可以计算$h_2$的输出值:
$$ out_{h2} = 0.596884378 $$
类似于计算$h_1$和$h_2$的过程,我们可以计算输出层节点$o_1$和$o_2$的值。下面是$o_1$的输出值计算过程:
$$ net_{o1} = w_5 * out_{h1} + w_6 * out_{h2} + b_2 * 1 $$
$$ net_{o1} = 0.4 * 0.593269992 + 0.45 * 0.596884378 + 0.6 * 1 = 1.105905967 $$
$$ out_{o1} = \frac{1}{1+e^{-net_{o1}}} = \frac{1}{1+e^{-1.105905967}} = 0.75136507 $$
同样的方式,$o_2$的输出值为:
$$ out_{o2} = 0.772928465 $$
计算模型总误差
得到了网络的输出值后,就可以计算输出值与真实值之间的误差。这里我们使用平方误差来计算模型总误差:
$$ E_{total} = \sum \frac{1}{2}(target - output)^{2} $$
上式中的target就是样本目标值,或真实值。$\frac{1}{2}$只是为了计算上的整洁,对实际参数的估计没有影响 。(The $\frac{1}{2}$ is included so that exponent is cancelled when we differentiate later on. The result is eventually multiplied by a learning rate anyway so it doesn’t matter that we introduce a constant here。)
对于输出节点$o_1$的误差为:
$$ E_{o1} = \frac{1}{2}(target_{o1} - out_{o1})^{2} = \frac{1}{2}(0.01 - 0.75136507)^{2} = 0.274811083 $$
类似的计算方法,$o_2$的误差为:
$$ E_{o2} = 0.023560026 $$
最后,通过这个前向传递后,这个神经网络的总误差为:
$$ E_{total} = E_{o1} + E_{o2} = 0.274811083 + 0.023560026 = 0.298371109 $$
后向传播过程
后向传播过程就是迭代网络参数的过程,通过误差的后向传播得到新的模型参数,基于这个新的模型参数,再经过下一次的前向传播,模型误差会减小,从而使得模型输出值与实际值越接近。
输出层(output layer)
我们先来看了离误差最近的输出层中涉及的参数。以$w_5$为例,我们想知道$w_5$的改变对整体误差的影响,那么我们自然会想到对模型总误差求关于$w_5$的偏导数$\frac{\partial E_{total}}{\partial w_{5}}$。这个值也称为误差在$w_5$方向上的梯度。
应用求导的链式法则,我们可以对偏导数$\frac{\partial E_{total}}{\partial w_{5}}$进行如下的改写:
$$ \frac{\partial E_{total}}{\partial w_{5}} = \frac{\partial E_{total}}{\partial out_{o1}} * \frac{\partial out_{o1}}{\partial net_{o1}} * \frac{\partial net_{o1}}{\partial w_{5}} $$
这个公式可以对应到具体的相应网络结构:
为了得到$\frac{\partial E_{total}}{\partial w_{5}}$的值,我们需要计算上式中的每个因子的值。首先我们来计算误差关于$o_1$输出值的偏导数,计算方式如下:
$$ E_{total} = \frac{1}{2}(target_{o1} - out_{o1})^{2} + \frac{1}{2}(target_{o2} - out_{o2})^{2} $$
$$ \frac{\partial E_{total}}{\partial out_{o1}} = 2 * \frac{1}{2}(target_{o1} - out_{o1})^{2 - 1} * -1 + 0 $$
$$ \frac{\partial E_{total}}{\partial out_{o1}} = -(target_{o1} - out_{o1}) = -(0.01 - 0.75136507) = 0.74136507 $$
下一步就是要计算$ \frac{\partial out_{o1}}{\partial net_{o1}}$,这个值的含义如上图中所示,就是激活函数对自变量的求导:
$$ out_{o1} = \frac{1}{1+e^{-net_{o1}}} $$
$$ \frac{\partial out_{o1}}{\partial net_{o1}} = out_{o1}(1 - out_{o1}) = 0.75136507(1 - 0.75136507) = 0.186815602 $$
logistic函数对自变量求导,可参考:https://en.wikipedia.org/wiki/Logistic_function#Derivative
现在还需要计算最后一个引子的值$ \frac{\partial net_{o1}}{\partial w_{5}}$,这里$net_{o1}$就是激活函数的输入值:
$$ net_{o1} = w_5 * out_{h1} + w_6 * out_{h2} + b_2 * 1 $$
那么对$w_5$求偏导就很直接了:
$$ \frac{\partial net_{o1}}{\partial w_{5}} = 1 * out_{h1} * w_5^{(1 - 1)} + 0 + 0 = out_{h1} = 0.593269992 $$
得到三个因子后,我们就得到了总误差关于$w_5$的偏导数:
$$ \frac{\partial E_{total}}{\partial w_{5}} = \frac{\partial E_{total}}{\partial out_{o1}} * \frac{\partial out_{o1}}{\partial net_{o1}} * \frac{\partial net_{o1}}{\partial w_{5}} $$
$$ \frac{\partial E_{total}}{\partial w_{5}} = 0.74136507 * 0.186815602 * 0.593269992 = 0.082167041 $$
为了减小误差,我们就可以类似于梯度下降的方式,来更新$w_5$的值:
$$ w_5^{+} = w_5 - \eta * \frac{\partial E_{total}}{\partial w_{5}} = 0.4 - 0.5 * 0.082167041 = 0.35891648 $$
上式中的$\eta$为学习率(learning rate),这里设为0.5. 在实际训练模型中,需要根据实际样本数据和网络结构来进行调整。
以类似的方式,我们同样可以得到 $w_6, w_7, w_8$的更新值:
$w_6^{+} = 0.408666186$
$w_7^{+} = 0.511301270$
$w_8^{+} = 0.561370121$
至此,我们得到了输出层节点中的参数更新值。下面我们以同样的方式来更新隐藏层节点中的参数值。
隐藏层 (hidden layer)
在隐藏层中,同样地,我们对总误差求关于$w_1, w_2, w_3, w_4$的偏导数,来获得更新值。首先还是应用求导的链式法则对总误差关于$w_1, w_2, w_3, w_4$的偏导数,以$w_1$为例,分解如下:
$$ \frac{\partial E_{total}}{\partial w_{1}} = \frac{\partial E_{total}}{\partial out_{h1}} * \frac{\partial out_{h1}}{\partial net_{h1}} * \frac{\partial net_{h1}}{\partial w_{1}} $$
用网络结构图来表示如下,从图中可以更直观地理解这种分解的物理意义:
与输出层中对权重求偏导数不同的一个地方是,由于每个隐藏层节点都会影响所有的输出层节点,在求总误差对隐藏层的输出变量求偏导数时,需要对组成总误差的每个输出层节点误差进行分别求偏导数。具体如下:
$$ \frac{\partial E_{total}}{\partial out_{h1}} = \frac{\partial E_{o1}}{\partial out_{h1}} + \frac{\partial E_{o2}}{\partial out_{h1}} $$
我们先来求第一项$\frac{\partial E_{o1}}{\partial out_{h1}}$的值,过程如下:
$$ \frac{\partial E_{o1}}{\partial out_{h1}} = \frac{\partial E_{o1}}{\partial net_{o1}} * \frac{\partial net_{o1}}{\partial out_{h1}} $$
$$ \frac{\partial E_{o1}}{\partial net_{o1}} = \frac{\partial E_{o1}}{\partial out_{o1}} * \frac{\partial out_{o1}}{\partial net_{o1}} = 0.74136507 * 0.186815602 = 0.138498562 $$
这一步可以利用输出层的计算结果。
$$ net_{o1} = w_5 * out_{h1} + w_6 * out_{h2} + b_2 * 1 $$
$$ \frac{\partial net_{o1}}{\partial out_{h1}} = w_5 = 0.40 $$
因此,
$$ \frac{\partial E_{o1}}{\partial out_{h1}} = \frac{\partial E_{o1}}{\partial net_{o1}} * \frac{\partial net_{o1}}{\partial out_{h1}} = 0.138498562 * 0.40 = 0.055399425 $$
类似地,我们可以求得$\frac{\partial E_{o2}}{\partial out_{h1}}$的值:
$$ \frac{\partial E_{o2}}{\partial out_{h1}} = -0.019049119 $$
那么我们就可以得到$\frac{\partial E_{total}}{\partial out_{h1}}$的值:
$$ \frac{\partial E_{total}}{\partial out_{h1}} = \frac{\partial E_{o1}}{\partial out_{h1}} + \frac{\partial E_{o2}}{\partial out_{h1}} = 0.055399425 + -0.019049119 = 0.036350306 $$
我们还需要计算$ \frac{\partial out_{h1}}{\partial net_{h1}}$和$\frac{\partial net_{h1}}{\partial w}$就可以得到$\frac{\partial E_{total}}{\partial w_{1}}$的值了。这两个值的计算方法跟输出层的完全类似,过程如下:
$$ out_{h1} = \frac{1}{1+e^{-net_{h1}}} $$
$$ \frac{\partial out_{h1}}{\partial net_{h1}} = out_{h1}(1 - out_{h1}) = 0.59326999(1 - 0.59326999 ) = 0.241300709 $$
$$ net_{h1} = w_1 * i_1 + w_3 * i_2 + b_1 * 1 $$
$$ \frac{\partial net_{h1}}{\partial w_1} = i_1 = 0.05 $$
最后把三个因子相乘就是我们需要计算的值:
$$ \frac{\partial E_{total}}{\partial w_{1}} = \frac{\partial E_{total}}{\partial out_{h1}} * \frac{\partial out_{h1}}{\partial net_{h1}} * \frac{\partial net_{h1}}{\partial w_{1}} $$
$$ \frac{\partial E_{total}}{\partial w_{1}} = 0.036350306 * 0.241300709 * 0.05 = 0.000438568 $$
$w_1$的更新值为:
$$ w_1^{+} = w_1 - \eta * \frac{\partial E_{total}}{\partial w_{1}} = 0.15 - 0.5 * 0.000438568 = 0.149780716 $$
同样的方式,$ w_2, w_3, w_4$的更新值为:
$$ w_2^{+} = 0.19956143 $$
$$ w_3^{+} = 0.24975114 $$
$$ w_4^{+} = 0.29950229 $$
从上面更新隐藏层节点参数的过程中,我们可以看到,这里的更新并没有用到输出层节点更新后的参数的值,还是基于老的参数来进行的。这个不能搞混。
上面的计算中,并没有对bias项的权重进行更新,更新方式其实也很简单。可以类似操作。
至此,我们已经完成了一轮BP的迭代。经过这轮迭代后,基于新的参数,再走一遍前向传播来计算新的模型误差,这时已经下降到0.291027924,相比第一次的误差 0.298371109貌似没减少太多。但是我们重复这个过程10000次后,误差已经下降到0.0000351085,下降了很多。这时模型的输出结果为0.015912196和0.984065734,跟实际的结果0.01和0.99已经很接近了。
这里只是一个样本数据,那么我们有很多样本呢?很多样本的情况下的计算跟这一个样本数据相比,有什么不同呢?自己比划比划吧~
原文链接地址:https://mattmazur.com/2015/03/17/a-step-by-step-backpropagation-example/
pyhont代码:https://github.com/mattm/simple-neural-network/blob/master/neural-network.py
附:神经网络入门材料:http://neuralnetworksanddeeplearning.com/index.html 可以整体上了解神经网络结构以及训练过程中存在的问题。虽然是英文,但使用的词汇都比较简单,看起来很顺畅