6、反向传播
内容列表:
- 6.1简介
- 6.2简单表达式和理解梯度
- 6.3复合表达式,链式法则,反向传播
- 6.4直观理解反向传播
- 6.5模块:Sigmoid例子
- 6.6反向传播实践:分段计算
- 6.7回传流中的模式
- 6.8用户向量化操作的梯度
- 6.9小结
6.1 简介
动机。在本节中,我们将直观地理解反向传播,这是一种通过递归应用链规则来计算函数梯度的方法。理解这一过程及其微妙之处对于理解和有效地开发、设计和调试神经网络至关重要。
问题陈述。本节研究的核心问题是:给你函数f(x),其中x是输入向量,我们要计算即f在x处的梯度(即∇f(x))。
动机。在神经网络的特定情况下,f 对应于损失函数(L),并且输入x由训练数据和神经网络权值组成。例如,损失可以是SVM损失函数,输入是训练数据(Xi,Yi),i=1…N,权重W和偏差b。注意,在机器学习领域,训练数据是给定的和固定的,而权重是我们可控制的变量。因此,我们通常只计算W、b等参数的梯度,这样我们就可以使用它来进行参数更新。当然,Xi的梯度有时也有用,例如为了可视化和解释神经网络正在做什么。
6.2 简单函数的梯度
让我们从简单开始,考虑两个数f(x,y)=XY的简单乘法函数。对于任一输入求偏导数比较简单的:
解释。导数的含义:它们指示一个函数相对于围绕某个变量的在某个特定点无限小区域的变化率:
左边的除法符号与右边的除法符号不同,不是除法。相反,该符号表示操作符d/dx被应用到函数f,并返回另外一个函数(导数)。咱们可以咱们理解导数:当h很小的时候,函数就被一条直线逼近,导数就是它的斜率。换句话说,每个变量上的导数告诉你整个函数在其值上的敏感度。例如,如果x=4,y=-3,则f(x,y)=-12,x上的导数∂f/∂x=-3。这告诉我们,如果我们将这个变量的值增加一个很小的量h,对整个表达式的影响将是减少(因为导数为负),并且是这个量的三倍。这可以通过重新排列上面的等式(f(x+h)=f(x)+h * ∂f/∂x)来看出。类似地,由于∂f /∂y=4,在y上增加非常小的h,则函数的输出会增加(因为导数为正),并且增加的值是4h。
函数关于每个变量的导数指明了整个表达式对于该变量的敏感程度。
如上所述,梯度∇f是偏导数的向量,因此我们有∇f=[∂f/∂x,∂f/∂y]=[y,x]。即使梯度在技术上是一个向量,我们将经常使用术语“x上的梯度”,而不是技术上正确的术语“x上的偏导数”。
我们还可以导出用于加法运算的导数:
f(x,y)=x+y → ∂f/∂x=1 ∂f/∂y=1
上式说明,x,y上的导数是1,而不管x,y的值是什么。这是有意义的,因为增加任一个x,y会增加f的输出,并且该增加的速率将独立于x,y的实际值(与上面的乘法的情况不同)。我们在本章使用的最后一个函数是max操作:
上式是说,如果该变量比另一个变量大,那么梯度是1,反之为0。直观地说,如果输入是x=4,y=2,则最大值是4,并且函数对y的取值不敏感。也就是说,如果我们将其增加一个很小的量h,函数将保持输出4,因此梯度为零:没有影响。当然,如果我们要改变一个大的量(例如大于2),那么f的值就会改变,但是导数没有告诉我们这些大的变化对函数的输入的影响,它们只是对输入的微小的、无穷小的变化的信息,因为定义就是lim h→0。
6.3 使用链式法则计算复合函数的梯度
现在让我们开始考虑组合函数,例如f(x,y,z)=(x+y)z。这个表达式可以直接微分,但是在此使用一种有助于读者直观理解反向传播的方法。注意到这个函数可以分解成两个函数:q= x+y和f= qz。此外,我们知道如何分别计算这两个表达式的导数。f只是q和z的乘法,所以∂f/∂q=z,∂f/∂z=q,q是x和y的加法,所以∂q/∂x=1,∂q/∂y=1。然而,我们并不一定关心中间值q的梯度 - ∂f/∂q的值是没有用的。我们最终感兴趣的是f相对于它的输入x,y,z的梯度。链式法则告诉我们,通过乘法可以地将这些梯度表达式“串”在一起。例如,∂f/∂x=∂f/∂q * ∂q/∂x。在实际操作中,只需要简单地将两个梯度数值相乘:
# 设置输入值
x = -2; y = 5; z = -4
# 进行前向传播
q = x + y # q 的值是 3 了
f = q * z # f 的值是 -12 了
# 进行反向传播:
# 首先回传到 f = q * z
dfdz = q # df/dz = q, 所以关于z的梯度是3
dfdq = z # df/dq = z, 所以关于q的梯度是-4
# 现在回传到q = x + y
dfdx = 1.0 * dfdq # dq/dx = 1. 这里的乘法是就是链式法则
dfdy = 1.0 * dfdq # dq/dy = 1
最后我们将梯度保存在变量 [dfdx,dfdy,dfdz]
中,他们告诉我们函数 f
对于变量 x,y,z
的敏感程度。这是反向传播最简单的例子。接下来,我们希望使用一个更简洁的符号,这样我们就不用继续写 df
部分了。也就是说,例如,代替dfdq
,我们将简单地写为 dq
, 并且总是假设梯度是相对于最终输出。
下图展示了这次计算的线路:
绿色值表示从输入到输出的正向计算。红色值表示从末端到开始的反向传播,递归地应用链式法规则来计算梯度。梯度可以被认为是通过网络的反向流动。
6.4 反向传播的直观理解
反向传播是一个漂亮的局部过程。网络中的每个关口都得到一些输入,并且可以立即计算两件事:1、其输出值 2、其输入相对于输出值的局部梯度。注意,每一个关口都可以完全独立地完成这一操作,而不必知道它们嵌入的完整电路的其他细节。然而,一旦正向传递结束,在反向传播期间,关口将最终获得整个网络的最终输出值在自己的输出值上的梯度。链式法则指出,关口应该将回传的梯度乘以它的局部梯度,从而得到整个网络的输出对该关口的每个输入值的梯度。
这种基于链式法则额外的乘法(对于每个输入)能够将单个和相对无用的关口转换成复杂网络(如整个神经网络)中的关键节点 。
让我们通过引用这个例子来了解这是如何工作的。加法关口接收输入[-2, 5 ]和计算输出3。由于该关口是计算加法运算,它的两个输入的局部梯度是1。网络的其余部分计算出最终值为-12。在反向传播时将递归地使用链式法则,算到加法关口(是乘法门的输入)的时候,知道加法门的输出的梯度是-4。如果网络想要输出值更高,那么可以认为它会想要加法关口的输出更小一点(因为负号),而且还有一个4的倍数。继续递归并对梯度使用链式法则,加法关口拿到梯度,然后把这个梯度分别乘到每个输入值的局部梯度(就是让-4乘以x和y的局部梯度,x和y的局部梯度都是1,所以最终都是-4)。可以看到得到了想要的效果:如果x,y减小(它们的梯度为负),那么加法关口的输出值减小,这会让乘法关口的输出值增大。
因此,反向传播可以看做是关口之间在通过梯度信号相互通信,只要让它们的输入沿着梯度方向变化,无论它们自己的输出值在何种程度上升或降低,都是为了让整个网络的输出值更高。
6.5 模块:Sigmoid例子
我们上面介绍的关口相对来说是随意的。任何一种可微函数都可以作为一个关口,我们可以将多个关口组合成一个单独的关口,或者在方便的时候将一个函数分解成多个关口。让我们看看另一个表达这一点的表达式:
后续会指出,这个表达式描述了一个使用S形激活函数的二维神经元(带有输入X和权值W)。但现在让我们简单地把它看作是从输入w,x到单个数的函数。该函数由多个关口组成。除了上面已经描述的(加法,乘法,取最大值)之外,还有四个:
其中函数fc,fa 用常量c 对输入值进行了的平移数,用a的常数对输入进行缩放。它们是加法和乘法的特例,但是这里将其看做一元关口,因为确实需要计算常量的梯度。整个计算线路如下::
math.exp(-1)*-.53 的结果为-0.19497610382086444,约等于-0.2
math.exp(-1)*-.53*2 的结果为-0.3899522076417289,约等于-0.39
math.exp(-1)*-.53*3 的结果为-0.5849283114625934,约等于-0.59
使用S形激活函数的2维神经元的图示。输入是[x0,x1],神经元(可学习)的权重是[W0,W1,W2]。神经元用输入来计算点积,然后其结果被S形函数轻轻地压缩到0到1的范围内。
在上面的例子中,我们看到了一个长链的函数操作,操作的是在W、x之间的点积的结果。这些操作实现的函数称为sigmoid 函数σ(x)。sigmoid函数求导是可以简化的(使用了在分子上先加后减1的技巧)::
你看,sigmoid的导数变得非常简单。例如,sigmoid表达式接收输入1并在正向传递期间计算输出0.73。上面的推导表明,局部梯度将简单地(1 - 0.73)* 0.73~=0.2。因此,在实际的应用中将这些操作装进一个单独的关口中将会非常有用。该神经元反向传播的代码实现如下:
dx:[0.3932238664829637, -0.5898357997244456]
dw:[-0.19661193324148185, -0.3932238664829637, 0.19661193324148185]
实现提示:分段反向传播。上述代码表明,为了使反向传播过程更加简洁,把向前传播分成不同的阶段将很有帮助。比如我们创造了中间变量dot,它装着w和x的点乘结果。在反向传播的时,就可以(反向地)计算出装着w和x等的梯度的对应的变量(比如ddot,dx和dw)。
本节主要是阐明,如何进行反向传播,以及出于便利的考虑,我们要将前向函数中的哪些部分当中关口来看待。知道表达式中哪部分的局部梯度计算比较简洁非常有用,这样他们可以“链”在一起,让代码量更少,效率更高。
6.6 反向传播实践:分段计算
再看另外一个例子,假设我们有这样一个函数:
这个函数实际上是完全无用的,它只是练习反向传播的一个很好的例子。很重要的一点是,如果你要开始对X或Y进行微分,你会得到非常复杂的表达式。然而,事实证明这样做是完全不必要的,因为我们不需要有一个显式函数来评估梯度。我们只需要知道如何计算它。下面是构建这个函数的正向传递代码:
x = 3 # 例子数值
y = -4
# 前向传播
sigy = 1.0 / (1 + math.exp(-y)) # 分子中的sigmoid #(1)
num = x + sigy # 分子 #(2)
sigx = 1.0 / (1 + math.exp(-x)) # 分母中的sigmoid #(3)
xpy = x + y #(4)
xpysqr = xpy**2 #(5)
den = sigx + xpysqr # 分母 #(6)
invden = 1.0 / den #(7)
f = num * invden # 搞定! #(8)
唷,在表达式结束时,我们计算了正向传递。注意,我们以这样的方式构造了代码,它包含多个中间变量,每个中间变量都是我们已经知道局部梯度的简单表达式。因此,计算反向传播是很容易的:我们将往回走,对于前进方向上的每个变量(sigy, num, sigx, xpy, xpysqr, den, invden
),我们定义一个对应的变量,加一个d打头,用来保存对应变量在网络中的梯度。注意在反向传播的每一小块中都将包含了表达式的局部梯度,然后根据链式法则乘以上游梯度。对于每行代码,我们将指明其对应的是前向传播的哪部分:
# 回传 f = num * invden
dnum = invden # 分子的梯度 #(8)
dinvden = num #(8)
# 回传 invden = 1.0 / den
dden = (-1.0 / (den**2)) * dinvden #(7)
# 回传 den = sigx + xpysqr
dsigx = (1) * dden #(6)
dxpysqr = (1) * dden #(6)
# 回传 xpysqr = xpy**2
dxpy = (2 * xpy) * dxpysqr #(5)
# 回传 xpy = x + y
dx = (1) * dxpy #(4)
dy = (1) * dxpy #(4)
# 回传 sigx = 1.0 / (1 + math.exp(-x))
dx += ((1 - sigx) * sigx) * dsigx # 注意使用累加符号 += #(3)
# 回传 num = x + sigy
dx += (1) * dnum #(2)
dsigy = (1) * dnum #(2)
# 回传 sigy = 1.0 / (1 + math.exp(-y))
dy += ((1 - sigy) * sigy) * dsigy #(1)
# 完成! 嗷~~
注意事项:
缓存前向传递变量。为了计算向后传播,有一些在向前传播中使用的变量是非常有帮助的。在实践中,您需要构造代码,以便缓存这些变量,以便它们在反向传播过程中可用。如果重新计算它们也可以,就是浪费计算资源。
在不同分支的梯度要相加。正向表达式涉及变量x,y多次,所以当我们执行反向传播时,我们必须小心地使用+=而不是=来积累这些变量上的梯度(否则我们会覆盖它)。这是微积分中的多元链式法则,该法则指出如果变量在线路中分支走向不同的部分,那么梯度在回传的时候,就应该进行累加。
6.7 反向流中的规律
有趣的是,在许多情况下,反向传播中的梯度可以得到很直观的解释。例如,神经网络中的三个最常用的关口(加,乘,取最大值),在它们如何在反向传播过程中起作用,都有非常简单的解释。考虑这个回路例子:
说明:加法操作将梯度相等地分发给它的输入。取最大操作将梯度传递给更大的输入。乘法节点取输入数据,对它们进行交换,然后乘以梯度。
以上面的图表为例,我们可以看到:
加法节点总是在其输出上取梯度,并将其直接分配给所有输入,而不管它们在向前传递中的值是多少。这是因为加法运算的局部梯度是简单的1,因此所有输入上的梯度将完全等于输出上的梯度,因为它将乘以X1.0(并且保持不变)。在上面的示例电路中,注意到+节点对其两个输入都相等地和不变地直接使用梯度2.00。
取最大值节点路由梯度。取最大值节点直接将梯度传递给它的一个输入(在向前传球中具有最高值的输入)。这是因为最大门的局部梯度是最高值的1,对于所有其他值的局部梯度是0。在上面的示例电路中,MAX运算将2的梯度路由到Z变量,因为Z变量的值比W的值高,W上的梯度保持为零。
乘法节点。输入值互换后就是它的局部梯度,然后根据链式法则乘以输出值的梯度。在上面的例子中,x上的局部梯度是-4.00,结果梯度是-8 00,即-4.00 x 2。
非直觉效应及其后果。注意,乘法节点中,如果其中一个输入值非常小,另一个则非常大,那么乘法节点将做一些不直观的事情:它将给小的输入分配一个相对大的梯度,并且给大的输入分配一个微小的梯度。注意,在线性分类器中,权重与输入取点积W.T*Xi,这意味着数据的规模对权重的梯度的大小有影响。例如,如果在预处理期间将所有输入数据示例Xi乘以1000,那么权重的梯度将是1000倍大,那么您必须通过降低学习率来弥补。这就是为什么预处理非常重要,有时是微妙的方式!并且对梯度流如何有直观的理解可以帮助您调试其中一些情况。
6.8 向量化操作的梯度
上述计算都说的是单个变量,但是所有概念都可以直接扩展到矩阵和向量运算。但是需要注意维度数和转置操作。
矩阵*矩阵乘法的梯度。可能最棘手的操作是矩阵 与 矩阵的乘法运算(也适用于矩阵和向量,向量和向量相乘):
# 前向传播
W = np.random.randn(5, 10)
X = np.random.randn(10, 3)
D = W.dot(X)
# 假设我们得到了D的梯度
dD = np.random.randn(*D.shape) # 和D一样的尺寸
dW = dD.dot(X.T) #.T就是对矩阵进行转置
提示:使用维度分析!请注意,您不需要记住dW和dX的表达式,因为它们很容易基于维度重新推导。例如,我们知道权重dW上的梯度必须在计算之后与W大小相同,并且它必须依赖于X和DD的矩阵乘法(如当X、W都是单数而不是矩阵)的情况下。总有一种方法可以做到这一点,这样维度就可以计算出来。例如,x是大小[ 10×3 ]和dD的大小[ 5×3 ],所以如果我们想要dW和W具有形状[5×10 ],那么实现这一点的唯一方法就是dD.dot(X.T),如上文所示。
使用小的、明确的例子。有些人可能会发现很难在一开始就推导出一些向量化表达式的梯度更新。我们的建议是显式地写出一个最小向量化的例子,在纸上导出梯度,然后对其一般化,得到一个高效的向量化操作形式。
Erik Learned Miller还写了一篇较长的有关矩阵/向量导数的相关文档,你可能会发现它是有用的。在这里找到它。
6.9小结
- 我们就梯度的含义做了直观的说明,它们是如何在网络中回流的,知道了它们是如何与网络的不同部分通信并控制其升高或者降低,并使得最终输出值更高的。
- 我们讨论了分段计算对于反向传播的实际实现的重要性。你总是想把你的函数分解成 可以很容易地得到局部梯度 的模块,然后用链规则将它们链接起来。重要的是,不需要把这些表达式写在纸上然后演算它的完整求导公式,因为实际上并不需要关于输入变量的梯度的数学公式。只需要将表达式分成不同的可以求导的模块(模块可以是矩阵向量的乘法操作,或者取最大值操作,或者加法操作等),然后在反向传播中一步一步地计算梯度。
在下一节中,我们将开始定义神经网络,并且反向传播将允许我们有效地计算神经网络连接上各节点关于损失函数的的梯度。换言之,我们现在已经准备好训练神经网络,这个概念中最困难的部分已经过去了!向前走一小步就是卷积神经网络。
斯坦福大学计算机视图课程由青星人工智能研究中心翻译整理
原文地址 CS231n Convolutional Neural Networks for Visual Recognition