本课程笔记是基于今年斯坦福大学Feifei Li, Andrej Karpathy & Justin Johnson联合开设的Convolutional Neural Networks for Visual Recognition课程的学习笔记。目前课程还在更新中,此学习笔记也会尽量根据课程的进度来更新。
在这一章中,我们将介绍backpropagation,也就是反向传播算法,理解这一过程对于学习神经网络很重要。
之前的章节中我们定义了一个loss function,它是输入数据x和网络参数W的函数,我们希望通过调节W使得网络的loss最小——这在Optimazation那一节中讲过,通过梯度下降法来更新W。但是神经网络是一个多层的结构,直接用输出对输入求导计算非常复杂,因此我们会用到反向传播算法来进行逐层的梯度传播与更新。
我们先来简单地回顾一下偏导数的计算和链式法则。比如函数f(x,y,z)=(x+y)*z, 为了求得f对三个输入x,y,z的偏导(即梯度),我们可以首先把它看成一个两层的组合函数,分解成q=x+y,f=q*z。这样逐层地计算偏导数就简单了:;其中q又是x和y的函数,对x,y的偏导数又可以写成
。
那么根据链式法则,,也就是说输出对输入的偏导可以通过两层网络组合得到。如下图,假设我们代入x=-2,y=5,z=-4,计算上述函数f的值和导数分别如图中绿色数字和红色数字所示。我们可以发现计算值的过程是从输入向输出逐层计算,我们称为前向传播;而计算导数的过程则是从输出向输入逐层计算得,称为反向传播。
注意每一层的门电路都能够独立地根据局部的输入计算当前门电路的输出以及局部的梯度,而不需要去考虑其他层的操作。
说了这么多,主要想表达的意思就是 通过链式法则进行逐层的梯度反向传播,你get到了吗?
任何可微函数都可以看成门运算,并且我们还可以把多个门组合成一个,让我们来看一个常用的函数,叫做sigmoid激活函数:
这个表达式包含w和x 2个输入,我们可以把这个复杂的表达式拆分成多层的门电路的组合:
其中不同门的偏导数如下,
在这个例子中我们看到7层的一个网络,但其实我们可以把其中的部分看成一个整体:
这整个表达式叫做sigmoid function,对它求偏导很简单:
因此我们会把一连串的运算组合成一个sigmoid门,那么反向传播算法是这样的:
w = [2,-3,-3] # assume some random weights and data
x = [-1, -2]
# forward pass
dot = w[0]*x[0] + w[1]*x[1] + w[2]
f = 1.0 / (1 + math.exp(-dot)) # sigmoid function
# backward pass through the neuron (backpropagation)
ddot = (1 - f) * f # gradient on dot variable, using the sigmoid gradient derivation
dx = [w[0] * ddot, w[1] * ddot] # backprop into x
dw = [x[0] * ddot, x[1] * ddot, 1.0 * ddot] # backprop into w
# we're done! we have the gradients on the inputs to the circuit
现在我们来举一个例子具体得说明如何进行反向传播的运算。f(x,y)如下式,注意我们并不需要显式地去计算 ∂f/∂x 或 ∂f/∂y ,我们只需要知道如何对这个函数进行拆分,然后分层计算局部梯度再利用链式法则进行整合即可。
在前向传播中,我们将整个等式进行如下拆分:
x = 3 # example values
y = -4
# forward pass
sigy = 1.0 / (1 + math.exp(-y)) # sigmoid in numerator #(1)
num = x + sigy # numerator #(2)
sigx = 1.0 / (1 + math.exp(-x)) # sigmoid in denominator #(3)
xpy = x + y #(4)
xpysqr = xpy**2 #(5)
den = sigx + xpysqr # denominator #(6)
invden = 1.0 / den #(7)
f = num * invden # done! #(8)
那么通过上述代码,我们其实是把整个函数拆分成了多个简单的表达式,每个表达式相当于之前所说的一个门,这些简单的表达式使得我们可以很方便地计算局部梯度。这样在进行反向传播时只需将局部梯度相乘,在下面的代码中每个局部梯度用d…表示:
# backprop f = num * invden
dnum = invden # gradient on numerator #(8)
dinvden = num #(8)
# backprop invden = 1.0 / den
dden = (-1.0 / (den**2)) * dinvden #(7)
# backprop den = sigx + xpysqr
dsigx = (1) * dden #(6)
dxpysqr = (1) * dden #(6)
# backprop xpysqr = xpy**2
dxpy = (2 * xpy) * dxpysqr #(5)
# backprop xpy = x + y
dx = (1) * dxpy #(4)
dy = (1) * dxpy #(4)
# backprop sigx = 1.0 / (1 + math.exp(-x))
dx += ((1 - sigx) * sigx) * dsigx # Notice += !! See notes below #(3)
# backprop num = x + sigy
dx += (1) * dnum #(2)
dsigy = (1) * dnum #(2)
# backprop sigy = 1.0 / (1 + math.exp(-y))
dy += ((1 - sigy) * sigy) * dsigy #(1)
# done! phew
这样在最后我们就实现了梯度的反向传播。这里需要注意两点:
1.在进行前向传播的过程中保存好拆分变量是有必要的,这能够帮助我们快速地在反向传播过程中计算梯度。
2.前向传播的过程中涉及多次x,y的使用,因此我们在反向传播计算梯度时特别注意f对x,y的梯度是一个累加的过程(也就是说我们用的是+=,而不是=),这遵循了微分学中多变量的链式法则。
在反向传播中几个常用的门电路,如+,*,max(),其实包含了一些实际的意义。比如以下图的电路为例:
我们对梯度的反向传播进行了直观的理解,它们是如何经过分层的计算最终得到输出对输入的梯度。这里的重点就是在正向传播的过程中对整个函数进行分层次的拆分,然后在反向传播过程中计算每个门电路的局部梯度。在下一章中,我们将开始介绍神经网络。
Automatic differentiation in machine learning: a survey