CS231n课程笔记(5) Backprop Note

本文转自:https://zhuanlan.zhihu.com/p/21407711?refer=intelligentunit,并进行一定修改。原文为:http://cs231n.github.io/optimization-2/

简介

目的:本节帮助读者对反向传播形成直观而专业的理解。反向传播是利用链式法则递归计算梯度的方法。理解反向传播过程及其精妙之处,对于理解、实现、设计和调试神经网络非常关键。

问题描述:核心问题是:给定函数f(x),其中x是输入数据向量,需要计算函数f关于x的梯度,也就是$\nabla f(x)$

原因:之所以关注上述问题,是因为在神经网络中f对应的是损失函数L,输入x里面包含训练数据和神经网络权重。举个例子,损失函数可以是SVM的损失函数,输入则包含了训练数据$(x_i,y_i),i=1,…N$,权重W和偏差b。给定训练数据,权重是可以控制的变量。因此,即使使用反向传播计算输入数据$x_i$上的梯度,但在实践上为了进行参数更新,通常也只计算参数(W和b)的梯度。然而$x_i$
的梯度有时仍然是有用的:比如将神经网络所做的事情可视化便于直观理解的时候,就能用上。

梯度的简单表达、解释

首先,考虑一个简单的二元函数f(x,y)=xy。对两个输入变量分别求偏导数,能够很简单求出:$$
f(x,y)=xy \rightarrow \frac {df}{dx}=y \frac {df}{dy}=x$$
解释:要牢记导数的意义:函数变量在某个点周围的极小区域内变化,而导数就是变量变化导致的函数在该方向上的变化率。$$
\frac {df(x)}{dx} = \lim_{h\rightarrow0} \frac {f(x+h)-f(x)}{h}$$对于上述公式,当h的值非常小时,函数可以被一条直线近似,而导数就是这条直线的斜率。换句话说,每个变量的导数指明了整个表达式对于该变量的值的敏感程度。例如,若x=4,y=-3,则f(x,y)=-12,x的导数$\frac {\partial f}{\partial x}=-3$,这就说明将变量x的值变大一点,整个表达式的值就会变小,而且变小的量是x变大的量的三倍。

如上所述,梯度$\nabla f$是偏导数的向量,所以有$\nabla f(x)=[\frac {\partial f}{\partial x},\frac {\partial f}{\partial y}] = [y,x]$。我们可以对加法操作进行求导:$$f(x,y)=x+y \rightarrow \frac {df}{dx}=1 \frac {df}{dy}=1$$这就是说,无论其值如何,x,y的导数均为1。这是有道理的,因为无论增加x,y中任一个的值,函数f的值都会增加,并且增加的变化率独立于x,y的具体值(情况和乘法操作不同)。取最大值操作也是常常使用的:$$
f(x,y)=max(x,y)\rightarrow \frac {df}{dx}=1(x>=y) \frac {df}{dy}=1(y>=x)$$上式是说,如果该变量比另一个变量大,那么梯度是1,反之为0。例如,若x=4,y=2,那么max是4,所以函数对于y就不敏感。也就是说,在y上增加h,函数还是输出为4,所以梯度是0:因为对于函数输出是没有效果的。当然,如果给y增加一个很大的量,比如大于2,那么函数f的值就变化了,但是导数并没有指明输入量有巨大变化情况对于函数的效果,他们只适用于输入量变化极小时的情况,因为定义已经指明:$lim_{h\to 0}$。

使用链式法则计算复杂表达式的导数

现在考虑更复杂的包含多个函数的复合函数,比如$f(x,y,z)=(x+y)z$。虽然,这个表达式足够简单,可以直接进行微分,但是在此使用一种有助于直观理解反向传播的算法。将公式分为两部分:$q=x+y,f=qz$。在前面已经介绍过如何对这分开的两个公式进行计算导数:$$
\frac {\partial f}{\partial q} = z,\frac {\partial f}{\partial z}=q
$$因为,q=x+y,所以,$$
\frac {\partial q}{\partial x}=1,\frac {\partial q}{\partial y}=1
$$然而,并不需要关心中间量q的梯度,因为$\frac {\partial f}{\partial q}$没有用。相反,函数f关于x、y、z的梯度才是需要关注的。链式法则指出将这些梯度表达式链接起来的正确方式是相乘,比如$\frac {\partial f}{\partial x}=\frac {\partial f}{\partial q} \frac {\partial q}{\partial x}$在实际的操作中,只是简单地将两个梯度数值相乘。
最后得到变量的梯度[dfdx, dfdy, dfdz],它们告诉我们函数f对于变量[x, y, z]的敏感程度。这是一个最简单的反向传播。一般会使用一个更简洁的表达符号,这样就不用写df了。这就是说,用dq来代替dfdq,且总是假设梯度是关于最终输出的。
这次计算可以被可视化为如下计算线路的图像:


mark

上图的真实值计算线路展示了计算的视觉化过程。前向传播从输入计算到输出(绿色),反向传播从尾部开始,根据链式法则递归地向前计算梯度(显示为红色),一直到网络的输入端。可以认为,梯度是从计算链路中回流。

反向传播的直观理解

反向传播是一个优美的局部过程。在整个计算线路图中,每个门单元都会得到一些输出并立即计算两个东西:

  1. 这个门的输出值;
  2. 其输出值关于输入值的局部梯度。
    门单元完成这两件事是完全独立的,它不需要知道计算路线中的其他细节。然而,一旦前向传播完毕,在反向传播的过程中,门单元将最终获得整个网络的最终输出值在自己的输出值上的梯度。链式法则指出,门单元应该将回传的梯度乘以它对其的输入的局部梯度,从而得到整个网络的输出对该门单元的每个输入值的梯度。

这里对于每个输入的乘法操作是基于链式法则的。该操作让一个相对独立的门单元变成复杂计算线路中不可或缺的一部分,这个复杂计算线路可以是神经网络等等。

下面通过例子来对这一过程进行理解。加法门收到了输入[-2, 5],计算输出是3。既然这个门是加法操作,那么对于两个输入的局部梯度都是+1。网络的其余部分计算出的最终值为-12。在反向传播时将递归地使用链式法则,算到加法门的时候,知道加法门的输出梯度是-4。如果网络想要输出值更高,那么可以认为它会想要加法门的输出更小一点,而且还有一个4的倍数。继续递归并对梯度使用链式法则,加法门拿到梯度,然后把这个梯度分别乘到每个输入值的局部梯度(就是让-4乘以x和y的局部梯度,x和y的局部梯度都是1,所以最终都是-4)。可以看到得到了想要的效果:如果x,y减小(它们的梯度为负),那么加法门的输出值减小,这会让乘法门的输出值增大。

因此,反向传播可以看做是门单元之间在通过梯度信号相互通信,只要让它们的输入沿着梯度方向变化,无论它们自己的输出值在何种程度上升或降低,都是为了让整个网络的输出值更高。

模块化:Sigmoid例子

上面介绍的门是相对随意的。任何可微分的函数都可以看做门。可以将多个门组合成一个门,也可以根据需求将一个函数拆成多个门。现在看一个表达式:$$
f(w, x) = \frac {1}{1+e^{-(w_0x_0+w_1x_1+w_2}}
$$
在后面的课程中可以看到,这个表达式描述了一个含输入x和权重w的2维的神经元,该神经元使用了sigmoid激活函数。但是现在只是看做是一个简单的输入为x和w,输出为一个数字的函数。这个函数是由多个门组成的。除了上文介绍的加法门,乘法门,取最大值门,还有下面这4种:$$
f(x) = \frac {1}{x} \rightarrow \frac{df}{dx} = - \frac {1}{x^2}\\\\
f_c(x) = c+x \rightarrow \frac{df}{dx} = 1 \\\\
f(x) = e^x \rightarrow \frac{df}{dx} = e^x\\\\
f_a(x) = ax \rightarrow \frac{df}{dx} = a\\\\
$$其中,函数$f_c$使用对输入值进行了常量c的平移,$f_a$将输入值扩大了常量a倍。它们是加法和乘法的特例,但是这里将其看做一元门单元,因为确实需要计算常量c,a的梯度,整个计算的线路如下:


mark

在上面的例子中可以看见一个函数操作的长链条,链条上的门都对w和x的点积结果进行操作。该函数被称作为sigmoid函数,sigmoid函数关于其输入的求导是可以简化的:$$
\sigma(x) = \frac {1}{1+e^{-x}}\\\\
\frac {d\sigma(x)}{dx} = \frac {e^{-x}}{(1+e^{-x})^2}=(\frac {1+e^{-x}-1}{1+e^{-x}})(\frac {1}{1+e^{-x}}) = (1-\sigma(x))\sigma(x)
$$可以看到梯度计算简单了很多。举个例子,sigmoid表达式输入为1.0,则在前向传播中计算出输出为0.73。根据上面的公式,局部梯度为(1-0.73)*0.73~=0.2,和之前的计算流程比起来,现在的计算使用一个单独的简单表达式即可。

反向传播实践:分段计算

看另外一个例子,假设有如下函数:$$
f(x,y) = \frac {x+\sigma(y)}{\sigma(x)+(x+y)^2}
$$首先要说的是,这个函数完全没用,读者是不会用到它来进行梯度计算的,这里只是用来作为实践反向传播的一个例子,需要强调的是,如果对x或y进行微分运算,运算结束后会得到一个巨大而复杂的表达式。然而做如此复杂的运算实际上并无必要,因为我们不需要一个明确的函数来计算梯度,只需知道如何使用反向传播计算梯度即可。下面是构建前向传播的代码模式:

1
2
3
4
5
6
7
8
9
10
11
x = 3 # 例子数值
y = -4
# 前向传播
sigy = 1.0 / (1 + math.exp(-y)) # 分子中的sigmoi #(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)

到了表达式最后,就完成了前向传播。注意在构建代码s时创建了多个中间变量,每个都是比较简单的表达式,它们计算局部梯度的方法是已知的。这样计算反向传播就简单了:我们对前向传播时产生每个变量(sigy, num, sigx, xpy, xpysqr, den, invden)进行回传。我们会有同样数量的变量,但是都以d开头,用来存储对应变量的梯度。注意在反向传播的每一小块中都将包含了表达式的局部梯度,然后根据使用链式法则乘以上游梯度。对于每行代码,我们将指明其对应的是前向传播的哪部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 回传 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 # Notice += !! See notes below #(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在前向传播的表达式中出现多次,那么进行反向传播时要非常小心使用+=而不是=来累计这些变量的梯度(不然就会造成覆写)。这是遵循了在微积分中的多元链式法则,该法则指出如果变量在线路中分支走向不同的部分,那么梯度在回传的时候,就应该进行累加。

回传流中的模式

一个有趣的现象是在多数情况下,反向传播中的梯度可以被很直观的解释。例如,神经网络中最常用的加法、乘法和取最大值的这三个门单元,它们在反向传播过程中的行为都非常简单的解释,先看下面的这个例子:


mark

一个展示反向传播的例子。加法操作将梯度相等地分发给它的输入。取最大操作将梯度路由给更大的输入。乘法门拿取输入激活数据,对它们进行交换,然后乘以梯度。从此例可知:

加法门单元:把输出的梯度相等地分发给它所有的输入,这一行为与输入值在前向传播时的值无关。这是因为加法操作的局部梯度都是简单的+1,所以所有的梯度实际上就等于输出的梯度,因为乘以1.0保持不变。上例中,加法门就把梯度2.0不变且相等地路由给了两个输入。

取最大值门单元:对梯度做路由,和加法门不同,取最大值门将梯度转给其中一个输入,这个输入是在前向传播中值最大的那个输入。这是因为在取最大值门中,最高值的局部梯度是1.0,其余是0。上例中,取最大值门将梯度2.0转给类z变量,因为z的值比w高,于是w的梯度保持为0。

乘法门单元:相对不容易解释,它的局部梯度就是输入值,但是是相互交换之后的,然后根据链式法则乘以输出值的梯度。上例中,x的梯度是-4.0*2.0 = -8.0。

非直观影响及其结果。注意一种比较特殊的情况,如果乘法门单元的其中一个输入非常小,而另一个输入非常大,那么乘法门的操作将会不是那么直观:它将会把大的梯度分配给小的输入,把小的梯度分配给大的输入。在线性分类器中,权重和输入是进行点积$w^Tx_i$,这说明输入数据的大小对于权重梯度的大小有影响。例如,在计算过程中对所有输入数据样本$x_i$乘以1000,那么权重的梯度将会增大1000倍,这样就必须降低学习率来弥补。这就是为什么数据预处理关系重大,它即使只是有微小变化,也会产生巨大影响。对于梯度在计算线路中是如何流动的有一个直观的理解,可以帮助读者调试网络。

小结

  • 对梯度的含义有了直观理解,知道了梯度是如何在网络中反向传播的,知道了它们是如何与网络的不同部分通信并控制其升高或者降低,并使得最终输出值更高的。

  • 讨论了分段计算在反向传播的实现中的重要性。应该将函数分成不同的模块,这样计算局部梯度相对容易,然后基于链式法则将其“链”起来。重要的是,不需要把这些表达式写在纸上然后演算它的完整求导公式,因为实际上并不需要关于输入变量的梯度的数学公式。只需要将表达式分成不同的可以求导的模块(模块可以是矩阵向量的乘法操作,或者取最大值操作,或者加法操作等),然后在反向传播中一步一步地计算梯度。

坚持原创技术分享,您的支持将鼓励我继续创作!