在学习深度学习过程中,无意间发现一篇介绍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函数):
σ(x)=11+e−x
首先来计算隐藏节点 h1 的输入值:
neth1=0.15∗0.05+0.2∗0.1+0.35∗1=0.3775
得到h1的输入值后,我们使用激活函数(logistic函数)来将输入值转化为为h1的输出值:
outh1=11+e−neth1=11+e−0.3775=0.593269992
按同样的方式,我们可以计算h2的输出值:
outh2=0.596884378
类似于计算h1和h2的过程,我们可以计算输出层节点o1和o2的值。下面是o1的输出值计算过程:
neto1=w5∗outh1+w6∗outh2+b2∗1
neto1=0.4∗0.593269992+0.45∗0.596884378+0.6∗1=1.105905967
outo1=11+e−neto1=11+e−1.105905967=0.75136507
同样的方式,o2的输出值为:
outo2=0.772928465
计算模型总误差
得到了网络的输出值后,就可以计算输出值与真实值之间的误差。这里我们使用平方误差来计算模型总误差:
Etotal=∑12(target−output)2
上式中的target就是样本目标值,或真实值。12只是为了计算上的整洁,对实际参数的估计没有影响 。(The 12 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。)
对于输出节点o1的误差为:
Eo1=12(targeto1−outo1)2=12(0.01−0.75136507)2=0.274811083
类似的计算方法,o2的误差为:
Eo2=0.023560026
最后,通过这个前向传递后,这个神经网络的总误差为:
Etotal=Eo1+Eo2=0.274811083+0.023560026=0.298371109
后向传播过程
后向传播过程就是迭代网络参数的过程,通过误差的后向传播得到新的模型参数,基于这个新的模型参数,再经过下一次的前向传播,模型误差会减小,从而使得模型输出值与实际值越接近。
输出层(output layer)
我们先来看了离误差最近的输出层中涉及的参数。以w5为例,我们想知道w5的改变对整体误差的影响,那么我们自然会想到对模型总误差求关于w5的偏导数∂Etotal∂w5。这个值也称为误差在w5方向上的梯度。
应用求导的链式法则,我们可以对偏导数∂Etotal∂w5进行如下的改写:
∂Etotal∂w5=∂Etotal∂outo1∗∂outo1∂neto1∗∂neto1∂w5
这个公式可以对应到具体的相应网络结构:
为了得到∂Etotal∂w5的值,我们需要计算上式中的每个因子的值。首先我们来计算误差关于o1输出值的偏导数,计算方式如下:
Etotal=12(targeto1−outo1)2+12(targeto2−outo2)2
∂Etotal∂outo1=2∗12(targeto1−outo1)2−1∗−1+0
∂Etotal∂outo1=−(targeto1−outo1)=−(0.01−0.75136507)=0.74136507
下一步就是要计算∂outo1∂neto1,这个值的含义如上图中所示,就是激活函数对自变量的求导:
outo1=11+e−neto1
∂outo1∂neto1=outo1(1−outo1)=0.75136507(1−0.75136507)=0.186815602
logistic函数对自变量求导,可参考:https://en.wikipedia.org/wiki/Logistic_function#Derivative
现在还需要计算最后一个引子的值∂neto1∂w5,这里neto1就是激活函数的输入值:
neto1=w5∗outh1+w6∗outh2+b2∗1
那么对w5求偏导就很直接了:
∂neto1∂w5=1∗outh1∗w(1−1)5+0+0=outh1=0.593269992
得到三个因子后,我们就得到了总误差关于w5的偏导数:
∂Etotal∂w5=∂Etotal∂outo1∗∂outo1∂neto1∗∂neto1∂w5
∂Etotal∂w5=0.74136507∗0.186815602∗0.593269992=0.082167041
为了减小误差,我们就可以类似于梯度下降的方式,来更新w5的值:
w+5=w5−η∗∂Etotal∂w5=0.4−0.5∗0.082167041=0.35891648
上式中的η为学习率(learning rate),这里设为0.5. 在实际训练模型中,需要根据实际样本数据和网络结构来进行调整。
以类似的方式,我们同样可以得到 w6,w7,w8的更新值:
w+6=0.408666186
w+7=0.511301270
w+8=0.561370121
至此,我们得到了输出层节点中的参数更新值。下面我们以同样的方式来更新隐藏层节点中的参数值。
隐藏层 (hidden layer)
在隐藏层中,同样地,我们对总误差求关于w1,w2,w3,w4的偏导数,来获得更新值。首先还是应用求导的链式法则对总误差关于w1,w2,w3,w4的偏导数,以w1为例,分解如下:
∂Etotal∂w1=∂Etotal∂outh1∗∂outh1∂neth1∗∂neth1∂w1
用网络结构图来表示如下,从图中可以更直观地理解这种分解的物理意义:
与输出层中对权重求偏导数不同的一个地方是,由于每个隐藏层节点都会影响所有的输出层节点,在求总误差对隐藏层的输出变量求偏导数时,需要对组成总误差的每个输出层节点误差进行分别求偏导数。具体如下:
∂Etotal∂outh1=∂Eo1∂outh1+∂Eo2∂outh1
我们先来求第一项∂Eo1∂outh1的值,过程如下:
∂Eo1∂outh1=∂Eo1∂neto1∗∂neto1∂outh1
∂Eo1∂neto1=∂Eo1∂outo1∗∂outo1∂neto1=0.74136507∗0.186815602=0.138498562
这一步可以利用输出层的计算结果。
neto1=w5∗outh1+w6∗outh2+b2∗1
∂neto1∂outh1=w5=0.40
因此,
∂Eo1∂outh1=∂Eo1∂neto1∗∂neto1∂outh1=0.138498562∗0.40=0.055399425
类似地,我们可以求得∂Eo2∂outh1的值:
∂Eo2∂outh1=−0.019049119
那么我们就可以得到∂Etotal∂outh1的值:
∂Etotal∂outh1=∂Eo1∂outh1+∂Eo2∂outh1=0.055399425+−0.019049119=0.036350306
我们还需要计算∂outh1∂neth1和∂neth1∂w就可以得到∂Etotal∂w1的值了。这两个值的计算方法跟输出层的完全类似,过程如下:
outh1=11+e−neth1
∂outh1∂neth1=outh1(1−outh1)=0.59326999(1−0.59326999)=0.241300709
neth1=w1∗i1+w3∗i2+b1∗1
∂neth1∂w1=i1=0.05
最后把三个因子相乘就是我们需要计算的值:
∂Etotal∂w1=∂Etotal∂outh1∗∂outh1∂neth1∗∂neth1∂w1
∂Etotal∂w1=0.036350306∗0.241300709∗0.05=0.000438568
w1的更新值为:
w+1=w1−η∗∂Etotal∂w1=0.15−0.5∗0.000438568=0.149780716
同样的方式,w2,w3,w4的更新值为:
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 可以整体上了解神经网络结构以及训练过程中存在的问题。虽然是英文,但使用的词汇都比较简单,看起来很顺畅