zoukankan      html  css  js  c++  java
  • BP(back propagation)反向传播

    转自:http://www.zhihu.com/question/27239198/answer/89853077

    机器学习可以看做是数理统计的一个应用,在数理统计中一个常见的任务就是拟合,也就是给定一些样本点,用合适的曲线揭示这些样本点随着自变量的变化关系。

    深度学习同样也是为了这个目的,只不过此时,样本点不再限定为(x, y)点对,而可以是由向量、矩阵等等组成的广义点对(X,Y)。而此时,(X,Y)之间的关系也变得十分复杂,不太可能用一个简单函数表示。然而,人们发现可以用多层神经网络来表示这样的关系,而多层神经网络的本质就是一个多层复合的函数。借用网上找到的一幅图[1],来直观描绘一下这种复合关系。
    其对应的表达式如下:
    上面式中的Wij就是相邻两层神经元之间的权值,它们就是深度学习需要学习的参数,也就相当于直线拟合y=k*x+b中的待求参数k和b。
     
    和直线拟合一样,深度学习的训练也有一个目标函数,这个目标函数定义了什么样的参数才算一组“好参数”,不过在机器学习中,一般是采用成本函数(cost function),然后,训练目标就是通过调整每一个权值Wij来使得cost达到最小。cost函数也可以看成是由所有待求权值Wij为自变量的复合函数,而且基本上是非凸的,即含有许多局部最小值。但实际中发现,采用我们常用的梯度下降法就可以有效的求解最小化cost函数的问题。

    梯度下降法需要给定一个初始点,并求出该点的梯度向量,然后以负梯度方向为搜索方向,以一定的步长进行搜索,从而确定下一个迭代点,再计算该新的梯度方向,如此重复直到cost收敛。那么如何计算梯度呢?
     
    假设我们把cost函数表示为
    ,
    那么它的梯度向量[2]就等于
     
    其中表示正交单位向量。为此,我们需求出cost函数H对每一个权值Wij的偏导数。而BP算法正是用来求解这种多层复合函数的所有变量的偏导数的利器
     
    我们以求e=(a+b)*(b+1)的偏导[3]为例。
    它的复合关系画出图可以表示如下:
    在图中,引入了中间变量c,d。
     
    为了求出a=2, b=1时,e的梯度,我们可以先利用偏导数的定义求出不同层之间相邻节点的偏导关系,如下图所示。
    利用链式法则我们知道:
       以及   
    链式法则在上图中的意义是什么呢?其实不难发现,的值等于从a到e的路径上的偏导值的乘积,而的值等于从b到e的路径1(b-c-e)上的偏导值的乘积加上路径2(b-d-e)上的偏导值的乘积。也就是说,对于上层节点p和下层节点q,要求得,需要找到从q节点到p节点的所有路径,并且对每条路径,求得该路径上的所有偏导数之乘积,然后将所有路径的 “乘积” 累加起来才能得到的值。


    大家也许已经注意到,这样做是十分冗余的,因为很多路径被重复访问了。比如上图中,a-c-e和b-c-e就都走了路径c-e。对于权值动则数万的深度模型中的神经网络,这样的冗余所导致的计算量是相当大的。
     
    同样是利用链式法则,BP算法则机智地避开了这种冗余,它对于每一个路径只访问一次就能求顶点对所有下层节点的偏导值。
     
    正如反向传播(BP)算法的名字说的那样,BP算法是反向(自上往下)来寻找路径的。
     
    从最上层的节点e开始,初始值为1,以层为单位进行处理。对于e的下一层的所有子节点,将1乘以e到某个节点路径上的偏导值,并将结果“堆放”在该子节点中等e所在的层按照这样传播完毕后,第二层的每一个节点都“堆放"些值,然后我们针对每个节点,把它里面所有“堆放”的值求和,就得到了顶点e对该节点的偏导。然后将这些第二层的节点各自作为起始顶点,初始值设为顶点e对它们的偏导值,以"层"为单位重复上述传播过程,即可求出顶点e对每一层节点的偏导数。


    以上图为例,节点c接受e发送的1*2并堆放起来,节点d接受e发送的1*3并堆放起来,至此第二层完毕,求出各节点总堆放量并继续向下一层发送。节点c向a发送2*1并对堆放起来,节点c向b发送2*1并堆放起来,节点d向b发送3*1并堆放起来,至此第三层完毕,节点a堆放起来的量为2,节点b堆放起来的量为2*1+3*1=5, 即顶点e对b的偏导数为5.

    举个不太恰当的例子,如果把上图中的箭头表示欠钱的关系,即c→e表示e欠c的钱。以a, b为例,直接计算e对它们俩的偏导相当于a, b各自去讨薪。a向c讨薪,c说e欠我钱,你向他要。于是a又跨过c去找e。b先向c讨薪,同样又转向e,b又向d讨薪,再次转向e。可以看到,追款之路,充满艰辛,而且还有重复,即a, b 都从c转向e。


    而BP算法就是主动还款。e把所欠之钱还给c,d。c,d收到钱,乐呵地把钱转发给了a,b,皆大欢喜。
     
    ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
    作者:杜客
    链接:https://zhuanlan.zhihu.com/p/21407711
    来源:知乎
     

    内容列表:

    • 简介
    • 简单表达式和理解梯度
    • 复合表达式,链式法则,反向传播
    • 直观理解反向传播
    • 模块:Sigmoid例子
    • 反向传播实践:分段计算
    • 回传流中的模式
    • 用户向量化操作的梯度
    • 小结

    简介

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

    问题陈述:这节的核心问题是:给定函数,其中x是输入数据的向量,需要计算函数f关于x的梯度,也就是

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

    如果读者之前对于利用链式法则计算偏微分已经很熟练,仍然建议浏览本篇笔记。因为它呈现了一个相对成熟的反向传播视角,在该视角中能看见基于实数值回路的反向传播过程,而对其细节的理解和收获将帮助读者更好地通过本课程。

    简单表达式和理解梯度

    从简单表达式入手可以为复杂表达式打好符号和规则基础。先考虑一个简单的二元乘法函数。对两个输入变量分别求偏导数还是很简单的:

    解释:牢记这些导数的意义:函数变量在某个点周围的极小区域内变化,而导数就是变量变化导致的函数在该方向上的变化率。
    注意等号左边的分号和等号右边的分号不同,不是代表分数。相反,这个符号表示操作符被应用于函数f,并返回一个不同的函数(导数)。对于上述公式,可以认为h值非常小,函数可以被一条直线近似,而导数就是这条直线的斜率。换句话说,每个变量的导数指明了整个表达式对于该变量的值的敏感程度。比如,若=4,y=-3,则的导数。这就说明如果将变量x的值变大一点,整个表达式的值就会变小(原因在于负号),而且变小的量是x变大的量的三倍。通过重新排列公式可以看到这一点()。同样,因为,可以知道如果将y的值增加h,那么函数的输出也将增加(原因在于正号),且增加量是4h。

    【*】函数关于每个变量的导数指明了整个表达式对于该变量的敏感程度。
    如上所述,梯度是偏导数的向量,所以有。即使是梯度实际上是一个向量,仍然通常使用类似“x上的梯度”的术语,而不是使用如“x的偏导数”的正确说法,原因是因为前者说起来简单。
    我们也可以对加法操作求导:

    这就是说,无论其值如何,x,y的导数均为1。这是有道理的,因为无论增加x,y中任一个的值,函数f的值都会增加,并且增加的变化率独立于x,y的具体值(情况和乘法操作不同)。取最大值操作也是常常使用的:

    上式是说,如果该变量比另一个变量大,那么梯度是1,反之为0。例如,若,那么max是4,所以函数对于y就不敏感。也就是说,在y上增加h,函数还是输出为4,所以梯度是0:因为对于函数输出是没有效果的。当然,如果给y增加一个很大的量,比如大于2,那么函数f的值就变化了,但是导数并没有指明输入量有巨大变化情况对于函数的效果,他们只适用于输入量变化极小时的情况,因为定义已经指明:

    使用链式法则计算复合表达式

    现在考虑更复杂的包含多个函数的复合函数,比如。虽然这个表达足够简单,可以直接微分,但是在此使用一种有助于读者直观理解反向传播的方法。将公式分成两部分:。在前面已经介绍过如何对这分开的两个公式进行计算,因为f是q和z相乘,所以,又因为q是x加y,所以。然而,并不需要关心中间量q的梯度,因为没有用。相反,函数f关于x,y,z的梯度才是需要关注的。链式法则指出将这些梯度表达式链接起来的正确方式是相乘,比如。在实际操作中,这只是简单地将两个梯度数值相乘,示例代码如下:

    # 设置输入值
    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]的敏感程度。这是一个最简单的反向传播。一般会使用一个更简洁的表达符号,这样就不用写df了。这就是说,用dq来代替dfdq,且总是假设梯度是关于最终输出的。

    这次计算可以被可视化为如下计算线路图像:

    ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

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

    ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

    反向传播的直观理解

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

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

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

    模块化:Sigmoid例子

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

    在后面的课程中可以看到,这个表达式描述了一个含输入x和权重w的2维的神经元,该神经元使用了sigmoid激活函数。但是现在只是看做是一个简单的输入为x和w,输出为一个数字的函数。这个函数是由多个门组成的。除了上文介绍的加法门,乘法门,取最大值门,还有下面这4种:

    其中,函数fc使用对输入值进行了常量c的平移,fa将输入值扩大了常量a倍。它们是加法和乘法的特例,但是这里将其看做一元门单元,因为确实需要计算常量c,a的梯度。整个计算线路如下:
    ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
    使用sigmoid激活函数的2维神经元的例子。输入是[x0, x1],可学习的权重是[w0, w1, w2]。一会儿会看见,这个神经元对输入数据做点积运算,然后其激活数据被sigmoid函数挤压到0到1之间。
    --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
     
    在上面的例子中可以看见一个函数操作的长链条,链条上的门都对w和x的点积结果进行操作。该函数被称为sigmoid函数。sigmoid函数关于其输入的求导是可以简化的(使用了在分子上先加后减1的技巧):
    可以看到梯度计算简单了很多。举个例子,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)) # 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
    # 完成!得到输入的梯度
    

      

    实现提示:分段反向传播。上面的代码展示了在实际操作中,为了使反向传播过程更加简洁,把向前传播分成不同的阶段将是很有帮助的。比如我们创建了一个中间变量dot,它装着wx的点乘结果。在反向传播的时,就可以(反向地)计算出装着wx等的梯度的对应的变量(比如ddotdxdw)。


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

    反向传播实践:分段计算

    看另一个例子。假设有如下函数:



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

    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)
    

      

    ┗|`O′|┛ 嗷~~,到了表达式的最后,就完成了前向传播。注意在构建代码s时创建了多个中间变量,每个都是比较简单的表达式,它们计算局部梯度的方法是已知的。这样计算反向传播就简单了:我们对前向传播时产生每个变量(sigy, num, sigx, xpy, xpysqr, den, invden)进行回传。我们会有同样数量的变量,但是都以d开头,用来存储对应变量的梯度。注意在反向传播的每一小块中都将包含了表达式的局部梯度,然后根据使用链式法则乘以上游梯度。对于每行代码,我们将指明其对应的是前向传播的哪部分。
    都是从f节点反向回去,计算f与各个节点的偏导
    # 回传 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在前向传播的表达式中出现多次,那么进行反向传播的时候就要非常小心,使用+=而不是=来累计这些变量的梯度(不然就会造成覆写)。这是遵循了在微积分中的多元链式法则,该法则指出如果变量在线路中分支走向不同的部分,那么梯度在回传的时候,就应该进行累加。

    回传流中的模式

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

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

    从上例可知:

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

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

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

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

    用向量化操作计算梯度

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

    矩阵相乘的梯度:可能最有技巧的操作是矩阵相乘(也适用于矩阵和向量,向量和向量相乘)的乘法操作:

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

      

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

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

    小结

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

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

    在下节课中,将会开始定义神经网络,而反向传播使我们能高效计算神经网络各个节点关于损失函数的梯度。换句话说,我们现在已经准备好训练神经网络了,本课程最困难的部分已经过去了!ConvNets相比只是向前走了一小步。



     
     
  • 相关阅读:
    python网站开发准备ubuntu14.04安装mysql实现windows管理
    python 数据结构之二叉树
    python 数据结构之二分查找的递归和普通实现
    python 数据结构之归并排序
    python数据结构之希尔排序
    ctf study of jarvisoj reverse
    python数据结构之quick_sort
    堆与栈
    汇编整理
    js运算符
  • 原文地址:https://www.cnblogs.com/beihaidao/p/5935683.html
Copyright © 2011-2022 走看看