深度学习 | 斯坦福cs231n第四讲学习笔记 --- backpropagation

课程主页

课程视频

课程PPT

斯坦福cs231n学习笔记系列博客基于cs231n 2017年春季版,课程主要内容关于视觉识别和深度学习,主要介绍卷积神经网络。

第四讲学习笔记初步神经网络(全联接网络),主要讲解了反向传播算法(神经网络中最小化损失函数,计算梯度更新参数的方法)。

目录

1.简介

2.简单梯度表达式

3.复合表达式的链式法则

4.反向传播的直观理解

5.Sigmoid示例

6.分阶段计算

7.反向传播中的模式

8.矢量化操作的梯度

9.总结

10.拓展阅读


1.简介

在本节中,我们将通过对反向传播的直观理解来开发专业知识,这是一种通过递归应用链式法则来计算表达式梯度的方法。 了解此过程及其细微之处对于您理解并有效开发,设计和调试神经网络至关重要。

本节研究的核心问题如下:给出一些函数f(x),其中x是输入的向量,我们感兴趣的是计算f在x处的梯度(即∇f(x))。

回想一下,我们对这个问题感兴趣的主要原因是在神经网络的特定情况下,f将对应于损失函数(L),输入x将由训练数据和神经网络权重组成。 例如,损失可以是SVM损失函数,输入是训练数据(xi,yi),i = 1 ... N和权重和偏差W,b。 请注意(通常是机器学习中的情况)我们将训练数据视为给定和固定,并将权重视为我们可控制的变量。 因此,即使我们可以很容易地使用反向传播来计算输入示例xi上的梯度,实际上我们通常只计算参数的梯度(例如W,b),以便我们可以使用它来执行参数更新。 然而,正如我们稍后看到的那样,xi上的梯度有时仍然有用,例如用于可视化和解释神经网络可能正在做什么。

 

2.简单梯度表达式

让我们从最简单的开始,以便我们可以为更复杂的表达式开发符号和约定。 考虑两个实数的简单乘法函数f(x,y)=xy。 导出任一输入的偏导数是一个简单的微积分问题:

解释:请记住导数的含义:它们表示函数相对于围绕特定点附近无限小区域的变量的变化率:

技术说明是,左侧的除法符号与右侧的除法符号不同,它并不是除法。相反,这种表示法表示运算符d/dx正在应用于函数f,并返回不同的函数(导数/导函数)。考虑上述表达式的一个好方法是,当h非常小时,函数很好地用直线近似,导数就是它的斜率。换句话说,每个变量的导数告诉你整个表达式对其值的敏感性。例如,如果x = 4,则y = -3,则f(x,y)=  -  12,f在x处的偏导数∂f/∂x= -3。这告诉我们,如果我们要将这个变量的值增加很小的数量,那么对整个表达式的影响将是减少它(由于负号),并且是该量的三倍。这可以通过重新排列上述等式(f(x+h)=f(x)+h df(x)/dx)来看出。类似地,由于∂f/∂y= 4,我们期望将y的值增加一些非常小的量h函数的输出会增加4h(由于正符号)。

每个变量的导数告诉你整个函数表达式对其值的敏感性。

如上所述,梯度∇f是偏导数的向量,因此我们得到∇f= [∂f/∂x,∂f/∂y] = [y,x]。 尽管梯度在技术上是矢量,但为了简单起见,我们通常会使用诸如“x上的梯度”之类的术语而不是技术上正确的短语“x上的偏导数”。

我们还可以推导出加法运算的导数:

也就是说,无论x,y的值是什么,x,y上的导数都是1。 这是有道理的,因为增加x,y会都增加f的输出,并且该增加的速率将与x,y的实际值无关(与上面的乘法的情况不同)。 我们使用的最后一个函数是max操作:

也就是说,更大的输入上的(子)梯度为1,而另一个输入上的梯度为0。 直观地讲,如果输入是x = 4,y = 2,那么最大值是4,并且该函数对y的设置不敏感。 也就是说,如果我们将它增加一个微量h,函数将保持输出4,此时y上的梯度为零:没有效果。 当然,如果我们要大幅度地改变y(例如大于2),那么f的值会改变,但是这些导数并没有告诉我们函数输入发生更大的改变的影响; 它们仅对输入上微小的,无限小的变化提供信息,如其定义中的lim h→0所示。

 

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。 也就是说,我们用dq来代替dfdq,并始终假设梯度是相对于最终输出f的。

上述计算过程可以用计算图简洁地进行可视化:

上图的实值“计算图”显示了计算的可视化表示。 前向传播计算从输入到输出的值(以绿色显示)。 然后,向后传递执行反向传播,该反向传播从结束开始并递归地应用链式规则以计算到计算图输入的梯度(以红色显示)。 可以认为梯度向后流过计算图。

 

4.反向传播的直观理解

计算图中的每个门都有一些输入,可以立即计算两件事:1.输出值2.输入相对于输出的局部梯度。 请注意,门可以完全独立地完成此操作,而无需了解它们嵌入的完整计算图的任何细节。但是,一旦前向传递结束,在反向传播期间,门将最终了解其输出值相对于整个计算图最终输出的梯度。 链式规则说明门应该采用该梯度并将其乘以它通常为其所有输入计算的每个局部梯度,就可以得到他的输入相对于最终输出的梯度(最终输出对该门输入的梯度)。

由于链式规则的这种乘法(对于每个输入)可以将单个且相对无用的门变成复杂计算图(例如整个神经网络)中的齿轮。

让我们通过再次参考这个例子来了解它是如何工作的。加法门接收输入[x,y]=[-2,5]第一步计算输出为3.第2步计算该加法门的局部梯度,由于门正在计算加法运算,因此其两个输入的局部梯度为+1。计算图的其余部分计算出最终值,即-12。在反向传播中,递归地向后通过计算图应用链式规则,加法门(它是乘法门的输入)得知其输出的梯度为-4(乘法门输入的局部梯度*乘法门输出的梯度)。如果我们将计算图拟人化为想要输出更高的值(这有助于直觉),那么我们可以认为计算图“想要”加法门的输出更低(由于负号,-4),继续向后递归链接梯度(链式法则),加法门采用该梯度(加法门输出的梯度)并将其乘以其输入的所有局部梯度(使x和y上的梯度(x,y相对于最终输出的梯度)1 * -4 = -4)。请注意,这具有所需的效果:如果x,y减小(响应其负梯度),则加法门的输出将减小,这将会使得乘法门的输出(最终输出)增加。

因此,反向传播可以被认为是彼此通信(通过梯度信号)的门,无论它们是希望它们的输出增加还是减少(以及有多强),都是为了使最终输出值更高。

5.Sigmoid示例

我们上面介绍的门是相对武断的。 任何类型的可微函数都可以充当门,我们可以将多个门组合成一个门,或者也可以将一个函数方便的分解为多个门。 让我们看看另一个表达式:

正如我们将在本课后面看到的,这个表达式描述了一个使用S形/Sigmoid激活函数的二维神经元(输入x和权重w)。 但是现在让我们把这简单地想象为从输入w,x到单个数字的函数。 该函数由多个门组成。 除了上面已经描述的那些(add,mul,max)之外,还有四个:

函数fc,fa分别将输入用常数c进行转换和用常数a进行缩放。 这些是技术上特殊的加法和乘法的情况,但我们在这里将它们作为(新的)一元门引入,因为我们确实需要常量的梯度。C,A。 然后整个计算图如下所示:

具有S形激活函数的2D神经元(逻辑回归)的示例计算图。 输入是[x0,x1],神经元的(可学习的)权重是[w0,w1,w2]。 正如我们稍后将看到的,神经元用输入计算点积,然后通过sigmoid函数将其激活在0到1的范围内。

在上面的例子中,我们看到一长串函数应用程序,它们对w,x之间的点积的结果进行一系列操作。 这些操作实现的函数称为sigmoid函数σ(x)。 事实证明,sigmoid函数相对于其输入的导数可以进行简化,可以执行如下的推导(使用一个有趣的技巧,在分子上加1再减1,等价变形):

正如我们所见,对梯度进行了简化变得很简单。 例如,sigmoid表达式接收输入1.0并在前向传播期间计算输出0.73。 上面的推导表明sigmoid门的局部梯度(sigmoid门输入相对于其输出的梯度或输出对输入的梯度)是(1  -  0.73)* 0.73~ = 0.2,该局部梯度乘以sigmoid输出的梯度(输出相对于最终输出的梯度=1)就得到了sigmoid门输入的梯度0.2(输入相对于最终输出的梯度),正如上述计算图所示(见上图),它将用一个简单有效的表达式完成( 并且数值问题较少)。 因此,在任何实际应用中,将这些操作分组到单个门中(即将多个门组合成一个门)是非常有用的。

让我们在代码中看到这个神经元的backprop:

w = [2,-3,-3] # 假设一些随机权重和数据
x = [-1, -2] #2维数据 每个样本特征向量是2D的

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

# 反向传播
ddot = (1 - f) * f # simoid门梯度
dx = [w[0] * ddot, w[1] * ddot] # x的梯度
dw = [x[0] * ddot, x[1] * ddot, 1.0 * ddot] # w的梯度

实施原则分阶段反向传播。 如上面的代码所示,在实践中,将前行传播分解为易于反向传递的阶段总是有帮助的。 例如,我们在这里创建了一个中间变量dot,它保存了w和x之间的点积的输出。 在反向传播中,我们连续计算相应变量的梯度(以一个相反的顺序)(例如ddot,最终dw,dx)。

本节的重点是,反向传播的执行的具体细节以及我们认为前向传播的哪些部分作为一个门是方便的。 有助于了解表达式的哪些部分具有简单的局部梯度,以便可以使用最少量的代码和工作将它们链接在一起。

6.分阶段计算

接下来看另一个例子。假设我们有如下形式的函数:

需要说明的是,这个函数完全没用,并且不清楚为什么你想要计算它的梯度,除了它是实际中反向传播的一个很好的例子。 非常重要的是,如果你要求解x,y的偏导数,你最终会得到一个非常大而复杂的表达式。 但是,事实证明这样做是完全没必要的,因为我们不需要写下一个显式函数来直接计算梯度。 我们只需要知道如何计算它。 以下是我们如何构造这种表达式的前向传播(分阶段):

x = 3 # 样例数据
y = -4

# 前向传播
sigy = 1.0 / (1 + math.exp(-y)) # 分子中的sigmoid   #(1)
num = x + sigy # numerator       #分子                        #(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 # done!                                 #(8)

在表达式结束时我们计算了前向传播。 请注意,我们已经以这样的方式构造代码,即它包含多个中间变量/门,对于每个中间变量/门,我们已经知道其局部梯度的简单表达式。 因此,计算backprop传递很容易:我们将向后并且对于前向传播中的每个变量(sigy,num,sigx,xpy,xpysqr,den,invden),我们将使用相同的变量,但是 以字母d开头,它将保持计算图最终输出对该变量的梯度。 另外,请注意我们的backprop中的每个单件都将涉及计算该表达式的局部梯度,并使用乘法/链式法则将该表达式上的梯度链接起来。 对于每一行,我们还会突出显示它所引用的前向传播的哪一部分:

#  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多次,所以当我们执行反向传播时,我们必须小心使用+ =而不是=来累积这些变量的梯度(否则我们会覆盖它)。 这遵循微积分中的多变量链式规则,该规则指出如果变量分支到计算图的不同部分,则流回其中的梯度将进行累加。

 

7.反向传播中的模式

值得注意的是,在许多情况下,可以在直观的层面上解释反向传播流动的梯度。 例如,神经网络中最常用的三个门(add,mul,max),就它们在反向传播过程中的行为方式都有非常简单的解释。 考虑这个示例计算图:

示例计算图展示了反向传播在后向传递期间执行的操作背后的直觉,以便计算输入上的梯度。 求和运算将梯度均等地分配给其所有输入。 最大操作将梯度路由到较高的输入。 乘法门接受输入激活,交换它们并乘以其梯度。

加法门始终把其输出梯度均等地分配给其所有输入,而不管它们在前向传播期间的值是什么。 这是因为加法运算的局部梯度仅为+1.0,因此所有输入的梯度(局部梯度乘以输出梯度)将完全等于输出的梯度,因为它将乘以x1.0(并保持不变)。 在上面的示例计算图中,请注意+门将梯度2.00路由到两个输入,同样且不变。

最大门路由梯度。 与将梯度不变地分配给其所有输入的加法门不同,最大门将梯度(未更改)分配给其输入中的一个(在前向传播期间具有最高值的输入)。 这是因为最大门的局部梯度对于最高值是1.0,对于所有其他值是0.0。 在上面的示例计算图中,最大操作将梯度2.00路由到z变量,其具有比w更高的值,并且w上的梯度保持为零。

乘法门不太容易解释。 其局部梯度是输入值(切换),并且在反向传播期间利用链式规则乘以其输出的梯度。 在上面的示例中,x上的梯度为-8.00,即-4.00 x 2.00。

不直观的影响及其后果。 请注意,如果乘法门的一个输入非常小而另一个非常大,那么乘法门会做一些稍微不直观的事情:它会为小输入分配一个相对较大的梯度,为大输入分配一个小梯度。 请注意,在线性分类器中,权重会和输入进行点乘w^Txi,这意味着数据的比例/大小对权重的梯度大小有影响。 例如,如果在预处理期间将所有输入数据样本xi乘以1000,则权重上的梯度将增大1000倍,并且必须将学习率降低以进行补偿。 这就是为什么预处理很重要,有时是微妙的方式! 对梯度流程的直观理解可以帮助你调试其中的一些情况。
 

8.矢量化操作的梯度

以上部分涉及的都是单个变量/标量,但所有概念都可以通过直接的方式扩展到矩阵和向量运算。 但是,必须密切关注尺寸和转置操作,注意维度的一致性。

矩阵 - 矩阵乘法的梯度。 可能最棘手的操作是矩阵 - 矩阵乘法(它推广所有矩阵-向量和向量 - 向量)乘法运算:

# 前向传播
W = np.random.randn(5, 10)
X = np.random.randn(10, 3)
D = W.dot(X)

# 反向传播
#假设我们在计算图中得到了D的梯度(D相对于最终输出的梯度 dD)
dD = np.random.randn(*D.shape) #dD与D维度一致
dW = dD.dot(X.T) #dW就等于dD点乘D对W的局部梯度。  与标量梯度不同 需要注意转置.T和运算次序 dW
和W维度一致
dX = W.T.dot(dD)

提示:注意使用维度分析! 请注意,我们不需要记住dW和dX的表达式,因为它们很容易根据维度重新派生。 例如,我们知道权重W上的梯度dW(W相对于最终输出的梯度)在计算之后必须与W的大小相同,并且它必须依赖于X和dD的矩阵乘法(就像X,W是单个数字/标量的情况一样, 而不是矩阵)。 总有一种方法可以实现这一目标,从而使尺寸得以满足。 例如,X的大小为[10 x 3],dD的大小为[5 x 3],因此如果我们想要dW和W的形状一致为[5 x 10],那么实现这一目标的唯一方法是使用dD.dot( XT),如上所示。

使用小的,明确的例子。 有些人可能会发现很难为某些矢量化表达式导出梯度。 我们的建议是明确地写出一个最小的矢量化示例,在纸上导出梯度,然后将模式推广到其有效的矢量化形式。

Erik Learned-Miller还撰写了一篇关于采用矩阵/向量导数的较长相关文档,您可能会发现这些文档很有帮助。 Find it here.

9.总结

  • 我们对梯度的意义,它们如何在计算图中向后流动,以及它们如何通信,计算图的哪些部分应该增加或减少以及用什么力度,来使最终输出更高,用了初步的了解并形成直觉。
  • 我们讨论了分阶段计算对反向传播的实际实现的重要性。 我们总是希望将函数分解为模块/门,然后就可以轻松地导出每个门局部梯度,然后使用链规则链接它们。 至关重要的是,我们从不需要得到输入变量梯度的明确完整的数学方程式。 只需要将表达式分解为阶段/门,以便我们可以独立对每个阶段/门进行求导/梯度(阶段/门将是矩阵向量乘法,或最大运算,或求和运算等),然后一步一步进行反向传播,用链式法则将它们链接起来,得到权重相对于最终输出的梯度,再进行更新。

之后的课程中,我们将开始定义神经网络,并且反向传播将允许我们有效地计算神经网络的权重/参数相对于损失函数的梯度。

 

10.拓展阅读

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

猜你喜欢

转载自blog.csdn.net/sdu_hao/article/details/86663491
今日推荐