CS231n官方笔记:反向传播

这篇笔记介绍了反向传播的基本概念和计算过程,通过直观的解释和实例,阐述了如何通过链式法则计算复杂表达式的梯度。内容包括反向传播的动机、简单表达式的梯度解释、复合表达式的链式法则应用,以及在神经网络中的实际应用。文中还探讨了向量化操作的梯度和反向传播在实践中如何分段计算,帮助读者理解反向传播在神经网络优化中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近把CS231n关于反向传播这一节的官方笔记看了一下。这篇算是用中文把原文复述了一遍,中间一些地方加入了我自己的理解,很多地方的表述参考了 CS231n课程笔记翻译:反向传播笔记

Introduction

动机

这篇笔记介绍了一些有助于在直觉上理解反向传播(BP)的知识。反向传播是一种通过递归应用链式法则(chain rule)来计算表达式梯度的方法。理解反向传播过程及其精妙之处,对于理解、实现、设计和调试神经网络非常关键。

问题描述

这节要讨论的核心问题是:给定函数 f(x) x 是一个输入向量,我们感兴趣的问题是计算f(x)关于 x 的梯度,即f(x).

动机

回想起来,我们关注这个问题的主要原因是在神经网络中我们有损失函数(Loss function) L ,输入x由训练数据和神经网络权值组成。比如说,损失函数是SVM函数,输入 x 包含训练数据(xi,yi),i=1,...N、权重 W 和偏差b. 注意训练数据是给定并且固定不变的,而权值是变量,是我们可以控制的(在机器学习中通常都是这样)。因此,尽管我们可以很容易的用BP来计算输入输入样本 xi 上的梯度,但实际上我们通常只计算参数(如 W,b )的梯度。然而,在之后的课程中我们会见到 xi 的梯度有时仍然是有用的,比如说在可视化神经网络、解释神经网络可能在做些什么的时候。

如果你之前对用chain rule推导偏微分已经很熟悉了,我们仍然建议你至少浏览一下这篇笔记,因为它呈现了一个相对成熟的反向传播视角,在该视角中能看见基于实数值链路的反向传播过程,而对其细节的理解和收获将帮助读者更好地通过本课程。

Simple expressions and interpretation of the gradient(简单表达式,梯度的解释)

先从简单的开始,这样我们就能把符号和规则推广到比较复杂的表达式了。先考虑一个简单的二元乘法函数 f(x,y) 。对两个输入变量分别求偏导数还是很简单的:

f(x,y)=xy      fx=y,fy=x

解释:牢记这些导数的意义:函数的自变量在某个点周围的极小区域内变化,而导数就是自变量变化导致的函数在该方向上的变化率:

df(x)dx=limh0f(x+h)f(x)h

注意上式等号左边的分号和等号右边的分号不同,它不是代表除法。这个符号表示操作符 ddx 被作用于函数 f ,并返回一个不同的函数(导数)。对于上述公式,可以认为当h非常小的时候,函数可以被一条直线近似,而导数就是这条直线的斜率。换句话说,每个变量的导数指明了整个表达式的值对于该变量的值的敏感程度。

比如, x=4,y=3,f(x,y)=12 , x 的导数fx=3,这就说明如果将变量 x 的值增加一个微小的量h,整个表达式的值就会变小(原因在于负号),而且变小的量是 x 增加的量h的三倍。对上述等式进行变形可以发现这一点: f(x+h)=f(x)+hdf(x)dx . 类似的,因为 yx=4 ,可知如果将 y 的值增加一个非常小的量h,那么函数的输出也将增大(因为是正号),且增大的量是 4h .

函数关于每个变量的导数指明了整个表达式对于该变量的敏感程度。

如上所述,梯度 f 是偏导数的向量,所以有 f(x)=[fx,fy]=[y,x] . 即便是梯度实际上是一个向量,但为了简单起见,我们仍然使用类似“x上的梯度”的术语,而不是使用如“x的偏导数”的正确说法。

我们也可以对加法操作求导:

f(x,y)=x+y      fx=1,fy=1
也就是说,无论 x,y 的值是多少,它们的导数都是1。这是有道理的,因为无论增加 x,y 中任一个的值,函数 f 的值都会增加,并且增加的变化率独立于x,y的具体值(情况和乘法操作不同)。

还有一个常用的函数,最大值函数:

f(x,y)=max(x,y)      fx=1(x>=y),fy=1(y>=x)
上式是说,如果该变量比另一个变量大,那么梯度是1,反之为0。例如,若 x=4,y=2 ,那么max是4,所以函数对于 y 就不敏感。也就是说,在y上增加 h ,函数还是输出为4,所以梯度是0,因为对于函数输出是没有效果的。当然,如果给增加一个很大的量(比如大于2),那么函数的值就变化了,但是我们要清楚,导数的定义中,自变量是在无穷小的区间内变化的(limh0),也就是说,导数并没有告诉我们自变量产生很大变化时对函数值的影响。因此上述情况是不在导数的定义范围内的。

Compound expressions with chain rule(复杂表达式,链式法则)

现在考虑更复杂的包含多个函数的复合函数,比如 f(x,y,z)=(x+y)z . 虽然这个表达式仍然足够简单,可以直接微分,但我们这里用一种特别的方法来处理它,有助于读者直观理解BP. 将这个表达式分成两部分 q=x+y f=qz . 我们已经知道如何对这两个分开的表示进行求导。 f q z 的乘积,所以fq=z,fz=q; q x y 的和,所以qx=1,qy=1. 然而我们并不关心中间量 q 的梯度,因为fq的值没有用。相反,函数 f 关于x,y,z的梯度才是我们真正关注的。链式法则指出将这些梯度表达式链接起来的正确方式是相乘,比如 fx=fqqx 。在实际操作中,这只是简单地将两个梯度数值相乘,示例代码如下:

# 设置输入值
x = -2; y = 5; z = -4

# 进行前向传播
q = x + y # q becomes 3
f = q * z # f becomes -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]的敏感程度。这就是关于BP的一个最简单的例子。为了简洁,以后我们在表示梯度的时候都会把df这部分略去,比如说,dfdq我们会简写成dq,而且我们总是假设梯度是关于最终输出的

上述例子可以被可视化为以下计算链路图:


图片1

如上如图所示,该实值计算链路图呈现了本次计算的过程。FP是由输入值计算输出值(用绿色表示);BP则从尾端开始,根据链式法则递归地计算梯度(用红色表示),一直到网络的输入端。可以认为,梯度在从计算链路中往回流动。


Intuitive understanding of backpropagation

反向传播是一个优美的局部过程。在整个计算链路中,每个门单元都会得到一些输入并立即计算两个东西:1. 这个门的输出值,和2.其输出值关于输入值的局部梯度。门单元完成这两件事是完全独立的,它不需要知道计算链路中的其他细节。一旦FP完成,在BP过程中,每个门单元将最终获得整个网络的最终输出值在自己的输出值上的梯度(上图的红色部分)。链式法则指出,每个门单元应该将回传过来的梯度乘以它事先计算出来的其输出关于其输入的局部梯度(也就是上述要计算的第2件事的结果)。

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

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

BP可是被看做是门单元之间通过梯度信号进行通信,只要让门单元的输入沿着梯度方向变化(正梯度就增大,负梯度就减小),那么不论这个门单元自己的输出是增大了还是减小了,网络的最终输出都会增大。(所以梯度下降法要求自变量逆梯度方向增长)

Modularity: Sigmoid example(模块化)

我们上面介绍的门都是比较随意的。任何可微的函数都可以看作一个门。可以将多个门组合成一个门,也可以根据需要将一个函数分拆成多个门。现在看看一个表达式:

f(w,x)=11+e(w0x0+w1x1+w2)

这个表达式描述了一个含输入 x 和权重w的2维神经元,该神经元使用了sigmoid激活函数。但现在我们只把它看作一个简单的输入为 x,w 输出为一个数字的函数,这个函数由多个门组成。除了上面提到的几种门(加法,乘法,max),还有下面四种:

f(x)=1xdfdx=1/x2

fc(x)=c+xdfdx=1

f(x)=exdfdx=ex

fa(x)=axdfdx=a

其中,函数 fc 使用对输入值进行了常量c的平移, fa 将输入值扩大了常量a倍。它们是加法和乘法的特例,但是这里仍将它们看作一元门单元,因为确实需要计算常量 c,a 的梯度。整个计算链路图如下:


图2

sigmoid激活函数的2维神经元的计算链路图。输入是 [x0,x1] , (可学习的)权重是 [w0,w1,w2] . 下面我们会看到,该神经元对输入做点积运算,然后其激活数据被sigmoid函数挤压到0到1之间。


在上图中可以看见一个函数操作的长链条,链条上的门都对w和x的点积结果进行操作。该函数被称为sigmoid函数 σ(x) . sigmoid函数关于其输入的求导是可以简化的(使用了在分子上先加后减1的技巧):

σ(x)=11+ex

dσ(x)dx=ex(1+ex)2=(1+ex11+ex)(11+ex)=(1σ(x))σ(x)

可以看出,梯度计算简化了许多。举个例子,sigmoid函数接收到输入1.0并且在FP结束后计算出输出0.73. 根据上面的公式,局部梯度为(1 - 0.73) * 0.73 ~= 0.2,和之前的计算流程(计算链路图中的计算流程)比起来,现在的计算使用一个单独的简单表达式即可,简单高效,而且不容易出现数值问题。因此,在实际应用中,将这些操作装进一个单独的门单元中(模块化)将会非常有用。该神经元反向传播的代码实现如下:

w = [2,-3,-3] # 假设一些随机数据和权重
x = [-1, -2]

# 前向传播
dot = w[0]*x[0] + w[1]*x[1] + w[2]
f = 1.0 / (1 + math.exp(-dot)) # sigmoid函数

# 对神经元反向传播
ddot = (1 - f) * f # 点积变量的梯度, 使用sigmoid函数求导
dx = [w[0] * ddot, w[1] * ddot] # 回传到x
dw = [x[0] * ddot, x[1] * ddot, 1.0 * ddot] # 回传到w
# 完成!得到输入的梯度

实现技巧:分段反向传播。上面的代码展示了在实际操作中,为了使BP过程更加简洁,把FP分成不同的阶段是很有帮助的。比如说这里我们创建了一个中间变量dot来装wx的点积结果。在BP过程中,就可以(反向地)陆续计算出和这些中间变量相对应的梯度(比如ddot是对应着中间变量dot的梯度,最后计算出dw,dx)。

这一部分的要点就是展示BP的细节过程,以及FP过程中,哪些函数可以被组合成门,从而可以进行简化。知道表达式中哪部分的局部梯度计算比较简洁非常有用,这样他们可以“链”在一起,让代码量更少,效率更高。

Backprop in practice: Staged computation(BP实践,分段计算)

让我们看另外一个例子,假设有如下函数:

x+σ(y)σ(x)+(x+y)2

首先要说的是,这个函数完全没用,你是不会用到它来进行梯度计算的,这里只是用来作为实践BP的一个例子。需要强调的是,如果你直接对 x 或者y进行微分运算的话,将会得到一个巨大而复杂的表达式。然而做如此复杂的运算实际上并无必要,因为我们不需要一个明确的函数来计算梯度,只需知道如何使用BP计算梯度即可。下面是构建FP的代码模式:

# 输入样本值
x = 3
y = -4

# 开始FP
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 # FP完成                                 #(8)

啧啧,在表达式的最后我们完成了FP. 注意,我们在构建代码时创建了多个中间变量,每个都是比较简单的表达式,它们计算局部梯度的方法是已知的。因此,计算BP就简单了:我们对FP时产生每个变量sigy, num, sigx, xpy, xpysqr, den, invden进行回传,我们会有同样数量的变量,但是都以d开头,用来存储整个计算链路的最终输出关于对应变量的梯度。此外,注意在BP代码中每一块都包含了计算表达式的局部梯度,然后根据链式法则乘以上游回传过来的梯度。下面所示的每行代码我们都注明了它对应的是BP的哪一部分:

# 回传 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)
# 完成! 

将这个例子可视化为计算链路图:


图3
可视化后的计算链路图,绿色为每个门单元的输出,红色为梯度。梯度 = (门输出对其输入的局部梯度)*(回传过来的梯度)


需要注意的两点:

  • 对前向传播变量进行缓存:在BP过程中,FP过程中得到的一些中间变量非常有用。在实际操作中,最在代码中实现对于这些中间变量的缓存,这样在BP的时候也能用上它们。如果这样做过于困难,也可以(但是浪费计算资源)重新计算它们。
  • 在不同分支的梯度要相加:如果变量x,y在前向传播的表达式中出现多次,那么进行反向传播的时候就要非常小心,使用+=而不是=来累计这些变量的梯度(不然就会造成覆盖)。这是遵循了在微积分中的多元链式法则,该法则指出如果变量在线路中分支走向不同的部分,那么梯度在回传的时候,就应该进行累加。(比如上图中的输入 x,y 的梯度,是由不同分支回传回来的梯度相加得到的)

Patterns in backward flow(回传流中的模式)

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


这里写图片描述

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


把上图作为例子进行观察我们可以发现:

  • 加法门单元把输出的梯度(回传梯度)相等地分发给它所有的输入,这一行为与输入值在FP时的值无关。这是因为加法操作的局部梯度都是简单的+1,所以所有输入的梯度实际上就等于输出的梯度,因为乘以1.0保持不变。上例中,加法门把梯度2.00不变且相等地路由给了两个输入。
  • 取最大值门单元对梯度做路由。和加法门不同,取最大值门将梯度转给其中一个输入,这个输入是在FP中值最大的那个输入。这是因为在取最大值门中,最大值的局部梯度是1.0,其余的是0。上例中,取最大值门将梯度2.00转给了z变量,因为z的值比w高,于是w的梯度保持为0。
  • 乘法门单元相对不那么容易解释。它的局部梯度就是它的输入值,但是是经过交换之后的,然后根据链式法则乘以它输出值的梯度(回传过来的梯度)。

不直观的影响及其后果:如果乘法门的一个输入非常小,而另一个输入非常大,那么乘法门将会进行一些不是那么直观的操作:它将会把一个很大的梯度分配给小的输入,把一个很小的梯度分配给大的输入。需要注意的是在线性分类器(权值和输入做点积 wTxi )中,这意味着输入数据的大小对权值的梯度是有影响的()。比如,在计算过程中将所有输入样本 xi 乘以1000,那么权重的梯度将会增大1000倍,这样就必须以降低同样倍数学习率来弥补。这就是数据预处理为什么重要,有时候数据的微小变化也会造成不小的影响!在了解了梯度在计算链路中是怎样流动的有了一个直观的理解之后,读者可以更好的调试网络。

Gradients for vectorized operations(向量化操作中的梯度)

上述内容考虑的都是单个变量情况,但是所有概念都适用于矩阵和向量操作。然而,在操作的时候要注意关注维度转置操作。

矩阵和矩阵相乘的梯度:最需要留心的操作可能就是矩阵和矩阵相乘的操作了(这些操作也适用于矩阵-向量、向量-向量相乘):

# FP
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) # X.T就是对矩阵X进行转置
dX = W.T.dot(dD)

提示:要去分析维度!不需要去记忆dWdX的表达,因为它们很容易通过维度推导出来。比如,我们知道权值的梯度dW一定是和权值矩阵W的尺寸是一样的,而这个形状又是由XdD的相乘决定的(上面的例子中X,W都是一个数字而不是矩阵)。总有一个方式是能够让维度之间能够对的上的。例如,X的尺寸是10x3,dD的尺寸是5x3,如果你想要dWW的尺寸是5x10,那就要dD.dot(X.T).

用小而具体的例子:有些读者可能觉得向量化的表达式的计算其梯度比较困难,建议是写出一个很小很明确的向量化例子,在纸上演算梯度,然后对其一般化,得到一个高效的向量化操作形式。

Summary

  • 我们对梯度的含义建立起了直观的理解,知道了梯度是如何在网络中反向传播的,知道了它们是如何与网络的不同部分通信并控制其升高或者降低,并使得最终输出值更高的。
  • 我们讨论了在反向传播实际实现的时候,分段计算的重要性。把一个复杂的函数不同的模块,每个模块都很容易推导其局部梯度,然后通过链式法则将它们“链”起来。关键是,你不需要把这些表达式写在在纸上,然后一次性的推导出所有变量的梯度公式,因为实际上我们并不需要这些关于输入的梯度的确切的数学公式。因此,只需要将表达式分成不同的可以求导的模块(模块可以是矩、向量的乘法操作,或者取最大值操作,或者加法操作等),然后在反向传播中一步一步地计算梯度。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值