cs231n系列1-4:反向传播

作者: Warren_Liu | 来源:发表于2017-01-12 11:44 被阅读422次

    说明

    1. 本系列文章翻译斯坦福大学的课程:Convolutional Neural Networks for Visual Recognition的课程讲义 原文地址:http://cs231n.github.io/。 最好有Python基础(但不是必要的),Python的介绍见该课程的module0。

    2. 本节的code见地址:
      https://github.com/anthony123/cs231n/tree/master/module1-4

    3. 如果在code中发现bug或者有什么不清楚的地方,可以及时给我留言,因为code没有经过很严格的测试。

    课程目录:

    • 简介
      
    • 简单表达式,梯度解释
      
    • 复合表达式, 链式法则, 反向传播
      
    • 反向传播的直觉理解

    • 模块化: sigmoid 例子

    • 反向传播实践:分层计算(Staged computation)

    • 反向流的模式

    • 向量化操作的梯度

    • 总结

    简介

    动机

    在这一节中,我们将介绍反向传播,它通过递归地应用链式法则来计算表达式的梯度。理解这个过程及其细节是理解,有效开发,设计及调试神经网络的关键。

    问题描述

    这节课研究的主要问题如下:我们已知一个函数 f(x),其中x是输入向量,我们的兴趣在于计算f在x处的梯度(比如:▽f(x))。

    动机

    回忆一下, 我们对这个问题感兴趣的主要原因是,在神经网络这种特殊情况下,f对应于损失函数(L),输入x由训练数据和神经网络的权重组成。比如,损失函数可能是SVM损失函数,输入由训练数据(xi, yi), i= 1 … N 和权重W与偏置量b组成。注意(在机器学习中通常是这种情况)我们通常认为训练数据已经给出并且是固定的,而权重是我们需要控制的变量。所以,尽管我们可以非常容易对输入数据xi使用反向传播计算梯度,但在实践中, 我们只计算参数的梯度(W和b),以便我们可以进行参数更新。然而,在后面的课程我们可以看到对xi求梯度有时候也是有用的,比如对神经网络的可视化及解释。

    如果你之前已经非常了解如何使用链式法则求梯度,那么我们还是建议你浏览一下这一节,因为我们将以实数电路反向流的角度解释反向传播,你在这种视角下学到的东西对你后续的课程会有帮助。

    简单表达式和梯度解释

    让我们从简单的表达式开始,这样有利于我们理解更复杂表达式的概念及我们的一些约定的符号表达。考虑一个简单的两个数字乘法函数
    f(x,y)= xy。
    对变量求偏导是一个简单的微积分问题:

    解释

    记住导数告诉你的事情:它告诉我们在一个变量无限小的周边区域内函数变化的速率。

    严格来讲,左边符号的横线不像右边横线表达的那样,它不是表示除法。这个符号表示操作d/dx被运用到f上,并返回一个不同的函数(导函数)。你可以把上面的表达式看成当h很小时,那么这个函数可以近似看成一条直线,那么导数就是斜率。也就是说,对每个变量的导数,告诉我们整个表达式在这个变量上的敏感度。比如,如果x=4,y=-3,f(x,y)= -12。对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是什么值,他们的导数都为1。这也合乎情理,因为当我们增加x或者y时,表达式也会增加x或y。所以增加的速率与x或y的值都没有关系(和乘法运算不一样)。最后一个例子是我们在后续的课程见到比较多的max函数:

    从上式可以看出,输入值较大值者的(子)梯度为1,而另外那个输入值的梯度为0。如果输入为x=4, y=2, 那么最大值为4,所以函数对y值的变化不敏感。也就是说,当我们对y提高一个微小量h,那么这个函数的输入仍然为4,所以它的梯度为0,也就是没有效果。当然,如果我们对y变化较大(比如 大于2),那么f的值就会改变,但是导数并不会告诉我们自变量发生较大变化时对函数f的影响。它们只能预测输入量微小的,无限小的变化时对f的影响,即当h趋近于0的时候。

    使用链式法则的复合表达式

    现在我们来看一下一个稍微复杂的复合函数的例子。比如:f(x,y,z) = (x+y)*z 。这个表达式依然非常简单,可以直接求导,但是,我们将使用一种特殊的方法,这种方法可以帮助我们理解反向传播。特别的,这个表达式可以分成两个更简单的表达式:q=x+y 和 f=qz 。而且,我们知道如何计算这两个简单的导数。对于f=qz,∂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
    f=q*z
    
    #逆序求出反向过程,也就是先求f=q*z
    dfdz=q
    dfdq=z
    
    #现在通过q=x+y反向传播
    dfdx=1.0*dfdq      #dq/dx=1,这里的乘法便是链式法则的运用
    dfdy=1.0*dfdq
    

    所以我们最终得到变量的梯度[dfdx,dfdy,dfdz],它告诉我们f在变量x,y,z上的敏感度。这是反向传播最简单的例子。在后续的讲解中,我们dq代替dfdq,并且总是假设梯度是关于最终表达式的。

    这个计算可以使用以下电路图直观的显示:

    上面的实数“电路”是计算的可视化表示。前向过程是从输入到输出的计算过程(绿色表示)。反向传导从输出值出发,递归调用链式法则计算梯度(红色表示),直到运算到电路的输入为止。

    反向传播的直观理解

    我们可以发现反向传播是一个非常优美的局部过程。电路图的每个门可以获得输入,并且能够立刻计算两个值:1)它的输出值;2)根据输出值计算局部梯度值。我们注意到这些门不需要知道整个电路的细节,便可以计算梯度。一旦前向传导结束,在反向传播过程中,门最终会通过整个电路的最终值知道它的输出值的梯度。链式法则告诉我们,门应该将它的梯度乘以每个输入值的梯度值。

    运用链式法则产生的乘法可以将一个单独的,相对没有用的门,变成一个复杂电路的齿轮,比如整个神经网络。

    为了获得更加直觉的理解,我们再以上面的表达式为例。加门获得输入[-2, 5], 计算的输出值为3。因为这个门是用来计算加法运算,所以这两个输入值的局部梯度值都是+1。剩下的电路计算最终的输出值:-12 。在反向传播过程中,链式法则递归地反向运用到整个网络。加门(乘门的输入)学习到它的输出值梯度为-4。如果我们把整个电路比作一个人,他希望输出一个较高的值(这个比喻可以帮助我们理解),那么电路会希望加门的输出会降低(因为是负号),并且以4倍的力气。继续递归并链接梯度,加门使用这个梯度,并且将它乘以它所有输入值的局部梯度(x和y上的梯度都为 1*-4 = -4)。这也符合我们的预期,如果x和y的值减少(对应他们的负梯度),那么加门的输出也会下降,从而会使得乘门的输出上升。

    反向传播因此可以被认为是门(通过梯度信号)之间交流他们想要他们的输出结果(增加或者减少,以多大的强度),从而使得最后的结果增加。

    模块化: sigmoid

    例子

    上面介绍的门相对比较随机,任何可微分的函数都可以作为一个门,我们可以将多个门合成一个门,或者将一个函数分解为多个门。我们来看下面这个例子:

    在后面的课程我们知道,这个表达式描述了一个使用sigmoid激活函数的二维神经元(输入为x, 权重为W)。但是现在我们把它简单的认为是一个将w,x转变成一个数字的函数。这个函数由许多门组成,除了上面描述的那些门,还有四种新类型的门:

    其中 fc 将输入值增加一个常量c, fa将输入值乘以一个常量。他们可以看作是加法和乘法的特殊情况,但是我们把它们看作新的一元门,因为我们需要这些常量。整个电路如下所示:

    一个2d神经元的电路示例,它的激活函数为sigmoid函数。输入为[x0, x1], 神经元的(可学习的)权重为[w0,w1,w2]。在后续的课程我们会讲到,神经元计算输入值得点积,然后使用sigmoid函数,把输出值映射到[0,1]的范围。

    从上面的例子可以看出,对w和x的点乘结果进行了一系列的操作,这些操作的函数称之为Sigmoid函数。Sigmoid函数的导数非常简单:

    我们可以看到,如果sigmoid的输入为1.0,输出为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))
    
    #神经元的反向传播
    
    ddot = (1-f)*f  #直接使用公式计算梯度
    dx = [w[0]*ddot, w[1]*ddot]       #反向传播到x
    dw = [x[0]*ddot, x[1]*ddot, 1.0*ddot]       #反向传播到w
    

    实现技巧:阶段化反向传播

    像上面的代码显示的那样,在实践中,我们经常将前向传播分解成容易反向传播的几个子阶段。比如,在上面的例子中,我们创建了一个新的变量dot 计算w和x之间的点积。在反向传播过程中,我们连续(逆序)计算对应变量的梯度。

    这节课的重点在于反向传播的细节,把哪部分看成是门完全是出于方便的考虑。它可以帮助了解哪一部分的表达式比较容易计算局部梯度,以便我们可以使用最少的代码和努力计算出所有变量的梯度。

    反向传播实践: 阶段化计算

    我们来看另外一个例子,假设我们有以下这个函数:

    首先说明一下,这个函数没有任何意义,它完全是用于练习的一个例子。如果你要计算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, inden)。我们将会创建相同的变量,但是将以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相吻合。所以所有输入的梯度都与输出的梯度值相同。像上图显示的那样。

    最大值门可以理解为路由梯度。不像加门,平等地将输出梯度路由到所有的输入梯度,最大值门将输出梯度路由到其中的一个分支(在前向传导过程中,
    数值较高的那个变量)。这是因为最大值门中较大的输入值的局部梯度为1,较小的输入值局部梯度为0。像上图显示的那样。

    乘法门比较难以解释,它的局部梯度就是输入的数字(位置交换),然后再与输出梯度相乘。像上图显示的那样。

    向量化操作的梯度

    上面讲的都是基于单个变量的,但是它们可以非常容易地扩展到矩阵和向量的操作。然而,我们必须要注意维度和转置操作。

    矩阵相乘的梯度 可能最容易出错的操作是矩阵与矩阵之间的乘法(最终分解成矩阵与向量,向量与向量之间的操作)。

    #前向传导
    
    W=np.random.randn(5,10)
    X=np.random.randn(10,3)
    D=W.dot(X) 
    
    #假设我们已经知道D的梯度
    
    dD = np.random.randn(*D.shape)
    dW = dD.dot(X.T)
    dX = W.T.dot(dD)
    

    技巧:使用维度分析。我们没有必要记住dW 和dX的表达式,因为我们很容易通过维度推断出来。比如:我们知道dw的大小和W的大小是一致的,并且它是由X和dD的乘积得到。(以x,W都是数字的情况一样)。总有一种方法可以确定维度。比如,x的大小为[10x3],dD的大小为[5x3], 所以如果我们知道dW的大小为[5x10](由W的大小确定),那么唯一得到这个结果的方法是dD.dot(X.T),如上所示。

    在小的,显式的例子上实验

    有些人会觉得在向量化的例子上推导梯度更新有有点困难。我们的建议是写出一个显式的,小型的向量化的例子,在纸上推导梯度,并将模式扩展到向量化的形式中。

    总结:

    1. 我们讲解了梯度的意义,它在电路中是如何反向传播以及他们如何交流,电路的哪一部分应该增加或减少,以及需要改变多少,使的最终的结果变得更高。
      
    2. 我们讨论了实现反向传播过程中**阶段化计算**的重要性。你总是需要将你的函数分解成不同的模块,而每个模块可以非常容易地计算子梯度,最后使用链式法则将梯度链接起来。而且,你可能永远不需要在纸上写出整个表达式的导数表达式。所以,把你的表达式分解成不同的阶段,并分别计算这些中间变量的梯度(这些阶段有矩阵向量乘法,或者最大值操作,或者总和操作等),最后通过反向传播计算各个变量的梯度。
      

    在下一节课中,我们开始定义神经网络,反向传播使得我们可以有效的计算神经网络连接处的梯度(关于损失函数)。也就是说,我们已经准备好训练Neural Nets, 这个课程最难的理论部分已经结束了, ConNets现在只离我一步之远。

    相关文章

      网友评论

      本文标题:cs231n系列1-4:反向传播

      本文链接:https://www.haomeiwen.com/subject/esdkbttx.html