原文地址:http://cs231n.github.io/optimization-2/
标题:Backpropagation,Intuitions
随手翻译,不当之处请指正
介绍
动机 在这个部分我们通过一些“直觉”来理解反向传播,也就是一种使用链式(求导)法则递归地计算梯度表达式的方法。理解其中的精妙之处非常重要,帮助理解,高效地开发,设计,调试神经网络。
问题陈述 前面学到的核心问题是:我们有一个函数
f(x)
,其中
x
是一个输入向量,我们希望计算
动机 我们之所以对这个问题如此感兴趣的原因是神经网络,
f
对应的是损失函数(
如果你在上课之前就已经知道如何使用链式法则得到梯度,我仍旧希望你可以留在这里,因为我们展示了作为实值传递圈逆流的反向传播的一些很成熟的想法(it presents a rarely developed view of backpropagation as backward flow in real-valued circuits),你可能从其中得到很多帮助。
简单的表达式和梯度的理解
我们由浅入深地学习一下。想象一个简单的乘积函数
f(x,y)=xy
。求出他们的偏导数非常简单
解释 导数的意义是:在某个点的无限小的距离内,函数值沿一个变量变化方向的变化率:
等式左边的除号和等式右边的除号不一样,它不是除号。这里的操作 ddx 作用在 f 上,返回一个不同的函数(导数)。一个很好的思路就是,当
每个变量的导数表明了这个值对整个表达式的敏感度
像上面提到的,梯度 ∇f 是向量的偏导数,所以我们有 ∇f=[∂f∂x,∂f∂y]=[y,x] 。 虽然梯度事实上是一个向量,为了简化表达我们仍然会用“x上的梯度”而不是“x上的偏导”。
我们也可以导出加法函数的偏导:
也就是说, x,y 对应的导数都与 x,y 的值无关,这是因为增加 x,y 的值都会使得输出 f 增加,而增长率和x,y事实上是多少是无关的。最后我们用的比较多的是最大操作:
That is, the (sub)gradient is 1 on the input that was larger and 0 on the other input. 直觉上来说,如果输入是 x=4,y=2 ,那么最大值是4,而函数对 y 的值并不敏感。如果我们增加一个很小的
用链式求导法则混合表达式
现在我们考虑更加复杂包括多种操作的表达式,例如
f(x,y,z)=(x+y)z
。这个表达式仍然简单到可以直接区分出来,但是我们只是为理解反向传播特别设计一个方法。这个表达式可以分成两个表达式:
q=x+y,f=qz
。我们知道如何独立地求出两个表达式的导数。
f
只是
# set some inputs
x = -2; y = 5; z = -4
# perform the forward pass
q = x + y # q becomes 3
f = q * z # f becomes -12
# perform the backward pass (backpropagation) in reverse order:
# first backprop through f = q * z
dfdz = q # df/dz = q, so gradient on z becomes 3
dfdq = z # df/dq = z, so gradient on q becomes -4
# now backprop through q = x + y
dfdx = 1.0 * dfdq # dq/dx = 1. And the multiplication here is the chain rule!
dfdy = 1.0 * dfdq # dq/dy = 1
最后我们将梯度保存在变量中[dfdx,dfdy,dfdz]
,也就是告诉我们x,y,z
对于函数f
的敏感度。这是最简单的反向传播的例子。我们想要一些更加简明的标记方法,这样我们就不用一直写df
这种东西了。现在我们将dfdq
简化成dq
,并且约定这个梯度始终对应最终输出。
计算过程也可以很好的用电路图表达出来
上图中显示了计算过程的电路图。前向传播从输入开始计算输出(绿色)。反向传播则从最后开始递归地为每一个输入应用链式法则计算梯度(红色)。梯度可以看做沿着电路图反向流动。
直观理解反向传播
反向传播是非常精妙的操作,电路图中的每个门都接收一些输入,然后立刻计算两个东西:1、输出值,2、计算输出对于输入值的梯度。这些门的计算都是完全独立的,不必了解整个电路图中的分布情况。但是,一旦前向传播结束,在反向传播的过程中,这些门会习得自己的输出对于整个电路图的输出的梯度。链式法则中指出,这些门应将梯度乘进所有输入梯度中。
这额外的乘操作(对每个输入)是因为链式法则可以将一个简单的相对无用的门转变成一个复杂电路例如整个神经网络中的一个替代品。(This extra multiplication (for each input) due to the chain rule can turn a single and relatively useless gate into a cog in a complex circuit such as an entire neural network.)
我们再从例子里理解这一切如何运作。“和”门接受两个输入[-2,5],计算输出为3。由于这个门的计算为和运算,所以对每个输入的梯度都是+1。剩下的部分则进行积运算,结果为-12。在反向递归计算梯度的过程中,和门(积门的一个输入)学习到他对于输出的梯度为-4。如果我们将这个电路图人格化为想要输出更高的值,那么我们就希望和门输出的结果要小一些,并且是4倍关系。接下来,和门将所有输入都乘上梯度-4。如果x,y减小,在减小,和门输出的结果是减小的,但是总输出是增大的。
反向传播可以认为是不同的门之间的通信,以决定他们是想让输出更高还是更低(还有多快地增高或降低),以影响最终输出。
模块化:以Sigmoid为例
上面我们说到的门是胡编乱造的。任何可识别的函数都可以像门一样运作,我们也可以将许多门放到一个门中去,或者将一个函数分解为若干个门。我们看下一个例子:
后面我们可以看到,这个式子描述了使用sigmoid的二维神经元(有输入x以及权重w),但是现在我们把他想像成简单的w,x输入都是单个数字。这个函数由几个门组成。上面只介绍了和,积以及最大操作,下面有一些其他的操作:
函数 fa,fc 用常量a倍乘了输入,用常量c扩大了输入。理论上他们是加与乘运算的特殊形式,但是我们以新的一元门引入他们,因为我们确实需要常量c和a的梯度。下面是完整的电路图:
以sigmoid为激活函数的二维神经元电路图。输入是[x0,x1],可学习的 权重[w0,w1,w2]。后面我们会看到,神经元进行点积计算,然后激活函数sigmoid将结果温柔地压进0到1之间。
在上面的例子中,我们看到了依据w,x点乘结果的一长串操作。这些操作实现的是叫sigmoid函数
σ(x)
。事实上,我们可以使用函数本身化简它的导数:
梯度变得异常的简单。比如,sigmoid在前向传播的时候获得的输入时1.0,得到的输出是0.73。领域梯度(local gradient)就是(1-0.73)*0.73~=0.2,在前面曾经计算过一个表达式。所以,在实际操作中,将这些操作整合在一个门中是非常有效的。我们看这个神经元的反向传播实现:
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
实现过程的提示:分段反向传播 我们前面提到过,将前向传播的计算结果保存下来可以让返向传播更加容易实现。比如我们创建了中间变量dot
,其中保存了w
和x
的点积结果。在反向传播的过程中再依次(反向地)计算各个对应的保存了各个梯度的变量(比如ddot
和dw,dx
)。
这个部分的重点在于反向传播的细节如何实现,我们哪些部分认作为门是出于方便的考量。为了更好的使用链式法则,用更少的代码将梯度整合在一起,我们应该了解哪些部分表达式可以很方便地得到梯度。
反向传播实战:分段计算
我们来看另一个例子。假设我们有如下表达式:
这里交代一下,这个函数没有任何实际意义,甚至你都不知道你为什么要计算它的梯度,这里用到它仅仅是因为它是一个反向传播实际计算的好例子罢了。如果你陷入了对x或者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)
哦哟~最后我们终于把前向传播过程完成了。注意到我们保存了许多中间变量,每个变量都是我们已知梯度的简单表达式。因此,反向计算就很简单了:我们沿着路一个一个变量的返回(sigy,num,sigx,xpy.xpysqr.den.invden
)。我们使用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
注意一些小问题:
将前向传播过程中的变量保存下来 前向传播过程中的中间变量对于计算反向传播非常有用。实际操作中需要将他们保存下来,反向计算的时候拿出来用。如果不这么做(或者这么做很麻烦),重新计算他们一便就很浪费时间了。
梯度以叉状叠加 前向表达式中出现好几次x和y,所以我们要注意用+=
操作去累计梯度,而不是=
(否则就被覆盖了)。这符合微积分中的多变量链式求导,如果一个变量出现在“电路图”中的不同部分,回流的梯度要相加。
逆流中的模式
很有趣的是,向后流动的梯度在很多时候都可以直观地解释出来。例如,神经网络中最常用的三个门(和,积,最大)都有非常简单的理解方法。如下例:
示例电路图:反向传播计算过程中操作背后的直观解释。和操作的两个操作元对于梯度的贡献相同,求大操作的梯度依赖较大的输入,积门则将操作元互换,并与梯度相乘。
从上面的示例中我们可以看到:
和门的不同输入对于输出的梯度总是相同的,不管前向传播的时候变量的值是多少。这是因为和操作的局部梯度是简单的+1.0,因此所有的输入对输出的梯度都是相同的,因为他们的系数都是+1.0。上面的电路中,和门给到输入的梯度都是2.00,相等且不变。
求最大门则把梯度权值给了其中一个。与和门不同的是,他将梯度赋予了其中给一个输入(前向传播中有较大值的那个)。这是因为他的局部梯度给最大值以+1.0的权值,而其他输入则是0。例中变量z
比w
大,所以梯度2.00赋予了z
,w
仍然是0。
积门不是很好解释。它的局部梯度是输入(交换过的),并且在链式法则中与输出的梯度相乘。上面的例子中,x
的梯度是-8.00,也就是-4.00x2.00。
直观的影响以及结果 注意到如果积门的某一个输入很小,另一个很大。那么它会做一些事情:它会给很小的值以很大的梯度,而给大值很小的梯度。线性分类器中,权重与输入的计算是点乘 wTxi ,这就是说数据的大小对于权重梯度量级有影响。例如,将数据样例 xi 乘以1000,那么权重的梯度就会是原来的1000倍大,你就得减小学习率,以抵消这种情况。这就是为什么预处理如此重要,有的时候还很微妙。直观地理解梯度的传播可以帮助你调整这些问题。
梯度的向量化操作
上面的部分都是单个变量,把概念扩充到矩阵和向量操作中,需要额外的注意量纲和转置操作。
矩阵相乘梯度 最投机取巧的操作可能就是矩阵矩阵相乘(包括矩阵向量相乘,向量向量相乘)了:
# forward pass
W = np.random.randn(5, 10)
X = np.random.randn(10, 3)
D = W.dot(X)
# now suppose we had the gradient on D from above in the circuit
dD = np.random.randn(*D.shape) # same shape as D
dW = dD.dot(X.T) #.T gives the transpose of the matrix
dX = W.T.dot(dD)
提示:使用量纲分析!你不必记住dW
和dX
的表达式,因为他们很容易从维度(dimensions)中导出来。例如,我们知道在计算过后,权重的梯度dW
肯定和W
形状一样,并且它一定依赖X
与dD
的矩阵积。肯定有一个方法得到这种效果,所以量纲很管用。例如,X
形状为[10x3],dD
形状为[5x3],所以如果我们想要dW
和W
有[5x10]的形状,我们只能使用dD.dot(X.T)
。
研究小的、确定的例子 有的人可能发现向量化的表达式中很难导出梯度用于更新参数。这里推荐一些最小的向量化的例子,在纸上推导出梯度,再将模式泛化到有效率的向量化的形式里。
Erik Learned-Miller 也写了一篇关于矩阵、向量求导的文档,如果有兴趣可以看这里。