文章

反向传播

反向传播

回顾上节

目前已经讲了怎么用函数f定义一个分类器,函数f的参数是权重矩阵W,输入数据x并对你想要分类的每个类别都输出一个对应的得分向量。由此我们还可以定义一个损失函数,比如SVM损失函数。我们已经讲了这个函数的基础量化了。我们对模型预测结果满意或是不满意的程度,然后我们可以用它定义一个总的损失函数,即L。这个L是由训练数据带来的损失结合一个正则项得到的。正则项表示我们模型的复杂程度,为了更好地泛化,我们倾向取简单的模型,所以现在我们想要找到与最小损失对应的参数W,我们想要最小化损失函数,为了找到这一点,我们想要找到LW方向上的梯度。

Desktop View 回顾:我们现在在哪?(来自cs231n)

上节课我们讲了如何用优化做到这一点,我们会沿着最陡的下降方向,即梯度的负方向一步步迭代,这样就能沿着损失函数从上往下走到最低点。我们还讲了计算梯度的不同方法,我们可以使用有限差分估计来计算数值梯度,这个方法用起来很慢而且结果区分度不大,但是写起来很简单,你总是可以通过这种方式得到梯度。我们还讲了如何解析梯度计算,这个计算很快,并且得到的解析梯度的表达式是精确的,但是同时需要数学和微积分知识来推导这些结果,所以很容易弄错。所以在实践中,我们想要推导和使用解析梯度,但是同时用数值梯度来检查我们的实现结果,以确保我们的推导是正确的。

Desktop View 回顾:梯度下降(来自cs231n)

计算图

今天我们讲如何计算任意复杂函数的解析梯度,要用到一个叫做计算图的框架。

Desktop View 计算图(来自cs231n)

大体上说,计算图就是我们用这类图来表示任意函数,其中图的节点表示我们要执行的每一步计算,比如上面这个例子(我们讲过的线性分类器),输入是xW,这个乘的节点表示矩阵乘法,输出得分向量。还有一个计算节点表示hinge loss,计算数据损失项Li,还有一个正则项,在右下角,这个节点计算了正则项,在最后的总的损失L是正则项和数据项的和。这里的好处是一旦我们能用计算图来表示一个函数,那就能用所谓的反向传播技术递归地调用链式法则来计算图中每个变量的梯度。当我们涉及非常复杂的函数时,这些方法非常有用,比如这门课后面要讲到的卷积神经网络。

Desktop View 卷积神经网络 AlexNet(来自cs231n)

这里最上面是输入的图片,最下面是损失,输入必须要穿过中间的很多转换层,才能最终到达最下面的损失函数。这个有时候会很夸张,比如神经图灵机。

Desktop View Neural Turing Machine(来自cs231n)

这是另一种深度学习模型,这个例子中的计算图特别夸张。这里沿着时间展开的话,如果你想要计算任意这些中间变量的梯度几乎是不切实际的。

Desktop View Neural Turing Machine(来自cs231n)

反向传播

那么反向传播(Backpropagation)是怎么工作的呢?

简单流程示例

从一个简单的例子讲起。
假设一个函数,这个例子中函数f=(x+y)*z,我们要找到函数输出对应任一变量的梯度。

第一步,就是用计算图来表示我们整个函数。

Desktop View 反向传播,一个简单的示例(来自cs231n)

图中绿色数值是正向计算的数值,红色是反向计算得到的梯度值。

这里我们想给每个中间变量一个名字,比如q。这里写出了q对应的xy的梯度,都是1Fqz方向上的梯度分别是zq,因为乘法法则。

我们想要的是fxyz的梯度。反向传播是链式法则的递归调用。我们从最后面开始,计算图的最后面,我们从后往前计算所有的梯度。这里我们从右边的图末开始,我们要计算输出对最后一个变量f的梯度,很明显高这个梯度是1。接下来从后往前,我们想计算在z方向上的梯度,我们知道df/dz等于q,而q的值为3,所以我们知道df/dz等于3。接下来计算df/dq,等于z,而z-4。现在我们继续沿着计算图从后往前计算,我们想要找到df/dy,但在这种情况下关于y的梯度,可以看出yf没有直接关系,但是它通过中间节点q连接,所以我们可以这样做,我们可以利用链式法则。

Desktop View 链式法则(来自cs231n)

为了找到yf的影响(df/dy),这实际上就是qf的影响(df/dq),接着我们把df/dqyq的影响(dq/dy)相结合,dq/dy=1,这就意味着,如果我们改变一点点的yq将会立马改变相同的大小,这就是yq的影响(dq/dy)。这就是说,如果我们改变了y的一点点,yq的影响也会变成1,然后qf的影响也会马上变成-4,所以把这些乘到一起,就得到了yf的影响。

所以如果我们想求解x方向上的梯度,可以使用相同的方法。

Desktop View local gradient(来自cs231n)

接下来做的是,在反向传播算法中,我们基本上所有的这些节点在我们的计算图中,每个节点只知道与它直接相连接的节点,所以在每个节点上有连接这个节点的本地输入,也就是流入这个节点的值,也有这个节点直接的输出,这里我们本地输入的是xy,输出是z。我们也知道这个节点的梯度,可以计算出关于zx方向上的梯度(dz/dx)和zy方向上的梯度(dz/dy)。

每个节点基本上都得到它的本地输入和直接传递到下一个节点的输出。相对于节点的输入,我们也可以计算出它们相应的梯度是节点的直接输出。我们来看看在反向传播算法中发生了什么?我们从图的后面开始,然后使用我们的方法从最后的地方一直到开始的地方。当我们到达每一个节点时,每一个节点都会得到一个从上游返回的梯度,这个梯度是对这个节点的输出的求导,等到我们在反向传播中到达这个节点,我们计算出了最终损失L关于z的梯度,现在我们接下来想做的就是去找到关于节点的输入的梯度,即在xy方向上的梯度。

Desktop View 梯度反向传播(来自cs231n)

就像我们之前看到的,用链式法则去计算。根据链式法则,损失函数关于x的梯度就是Lz方向上的梯度乘以zx方向上的本地梯度。在链式法则中,我们通常把上游的梯度值传回来,再乘以本地梯度值,从而得到关于输入的梯度值。把所有梯度相乘,就得到我们想要的梯度。一旦计算出这些结果,我们可以把结果传递给前面的节点或者直接相连的节点,主要工作就是在每一个节点上计算我们所需的本地梯度,然后跟踪这个梯度。在反向传播过程中,我们接收从上游传回来的这个梯度值,我们直接用这个值乘以本地梯度,然后得到我们想要传回连接点的值。在下一个节点进行反向传播时,我们不考虑除了直接相连的节点之外的任何东西。

另一个例子

我们来看另一个例子,这个例子要复杂一些,这个例子也证明了反向传播为什么这么有效。

Desktop View 反向传播的另一个例子(来自cs231n)

在这个例子里,f是关于wx的函数,通常我们的第一步是把它转换成一个计算图,在这个计算图中我们可以看出我们将w项和x项相乘。在图中写了一些值(绿色),有了这些给定的w0w1w2的值以及x0x1的值,我们就可以进行前向传播,基本上就可以算出每个阶段的结果。

在底部写出了一些微分表达式,这在后面非常有用。

Desktop View 附加公式(来自cs231n)

现在我们要对它进行反向传播。

我们从图中的最末端开始,输出最终变量在某方向上的梯度结果,得到的梯度是1,这没什么说的。然后反向进行下一步,那么1/x关于输入值的导数是什么呢?在这个例子中,我们可以知道传回的上游梯度值就是红框中的数值。

Desktop View 开始反向传播(来自cs231n)

现在我们需要找到本地梯度,关于这个节点的本地梯度,这个节点时1/x,本地梯度df/dx结果等于-1/x2,带入我们在前向传播中得到的x的值为1.37,最后关于这个变量的梯度就是-1/(1.37的平方) ,等于-0.53。我们往回下一个节点,用同样的流程我们知道从上游传回来的梯度是-0.53,这里的本地梯度呢,因为节点是加1,那么它的本地梯度就是1了。那么使用链式法则时对这个变量怎么求梯度呢?-0.53*1

再次进行反向传播的下一步,

Desktop View 再次进行反向传播的下一步(来自cs231n)

这里有一个指数,上游梯度传下来的是-0.53,本地梯度就是指数ex的求导,这是一个指数节点,链式法则告诉我们梯度等于-0.53乘以ex次方。

Desktop View 计算最终梯度结果,-0.2(来自cs231n)

例子中x-1x通过前向传播得到最终梯度是-0.2

到下一个节点,

Desktop View 到下一个节点(来自cs231n)

上游传过来-0.2,本地梯度是-1,因为本地梯度是x的系数a的值是-1

Desktop View 计算最终梯度结果,0.2(来自cs231n)

现在看看一个加法运算的节点,这里有两个分支都跟这个节点相连。上游梯度自然是0.2,本地梯度呢?这里每个分支的梯度,这是一个加法运算,从前面简单的例子可以看到,当我们遇到加法运算节点时,加法运算中对每个输入的梯度正好是1。所以这里的本地梯度是1乘以反向梯度0.2,得到总的梯度是0.2

Desktop View 到乘法节点(来自cs231n)

下面到乘法节点,这种节点对于某一输入的梯度值,恰好是另一个输入的值。因此在这种情况下,w0的梯度是0.2*-1x0的梯度是0.2*2

Desktop View 计算最终梯度结果(来自cs231n)

这里我们已经完成了大多数的梯度的计算。

之前有一个问题,相比于推导任意参数的梯度的解析表达式,为什么这样就算会更简单?从这里可以看出,我们处理过的本地梯度的表达式,我们先写出这些表达式,所以一旦我们有了这些本地梯度的表达式,我们要做的就是填充每个值,然后用链式法则从后往前乘以这些值,得到对所有变量的梯度。

组合门

需要注意的一点是,当我们创建这些计算图时,我们可以以我们想要的任意间隔尺寸来定义计算节点。所以在这种情况下,我们把它分解成我们能够达到的最简单的形式,分解成加法和乘法,基本上没有比这更简单的了。实际上我们可以把一些节点组合在一起形成更复杂的节点,要是我们想这么做的话,只要我们能写出那个节点的本地梯度。

sigmoid

举个例子,比如上面这个sigmoid函数,我们可以得到构成sigmoid函数的计算节点,然后用一个大的sigmoid节点替换掉,因为我们知道这个门的本地梯度。

Desktop View sigmoid门的本地梯度(来自cs231n)

这里最重要的事情是,你能聚合你想要的任意节点,去组成一些在某种程度上稍微复杂的节点,只要你能写出它的本地梯度。这就是一个权衡,一方面是你想用多少数学计算去得到一个更加简洁和简单的计算图,另一方面你想要每个梯度有多简单,然后你就可以按照你想要的复杂度写出一个计算图。

思考计算图,就像任何时候,我必须得到一个梯度,找到某些东西的梯度,即使这些我想要计算的梯度的表达式非常繁琐和吓人,你知道无论是像sigmoid函数,还是其他更复杂的函数。如果我想做的话,我可以推导这个。但实际上,如果我只是坐下来并写出求解这样一个计算图,我可以很简单地算出来,只要我应用反向传播和链式法则,我就能计算我需要的所有梯度。实际上当你在计算某个梯度有问题的时候,只要把它考虑成计算图问题,将其分解成这些部分,然后使用链式法则。

我们讲解了如果把这些节点组合在一起,变成一个sigmoid门,并且确认这实际上是等价的,我们可以把数值带入到表达式,这样,这里对sigmoid的输入是1,输出是0.73。现在我们想得到这个梯度,并且我们想要将这整个sigmoid函数作为一个节点,我们应该做的是我们需要使用我们已经推导出来的本地梯度,最终结果是1*0.2,会在sigmoid门的最前端得到相同的梯度值。

Desktop View 解析:sigmoid门的本地梯度(来自cs231n)

add、max、mul

当我们通过计算图后向传播这些梯度时,你会注意到一些模式。并且其中一些模式我们能够给出直观的解释。我们看到加法门是一个梯度分布器。当我们通过加法门连接了两个分支,它获取了上游梯度,并且只是分发和传递完全相同的梯度,给相连接的两个分支,这里我们可以考虑更多的东西,比如max门会是什么样。

Desktop View add门的本地梯度(来自cs231n)

上面一个max门,它的输入是zwz的取值是2w的取值是-1,然后我们取其中的最大值,也就是2,我们将这个值传到剩余的计算图中。现在如果我们得到对max门的梯度,也就是上游梯度,假设是2,这个本地梯度会是什么样?(0-1

其中一个变量将会得到刚传递回来的梯度完整值,并且再传递给那个变量,然后另一个变量的梯度会取为0。我们可以想象成一个梯度路由器,

Desktop View max门的本地梯度(来自cs231n)

加法节点回传相同的梯度给进来的两个分支,max门将获取梯度,并且将其路由到它其中一个分支。这样是说得通的,因为如果我们查看前向传递,可以看到只有最大值能被传递到计算图的剩余部分,所以只有这个最大值真正影响到我们函数计算的唯一值。当我们传递梯度回去的时候,我们只是想要通过这个计算分支来调整它。

另一个乘法门是什么样的呢?乘法门的本地梯度是另外一个变量的值。所以我们可以把它认为是一个梯度转换器。我猜它是一个尺度缩放器,我们获取上游梯度,然后根据另一个分支的值对其缩放。

Desktop View mul门的本地梯度(来自cs231n)

Desktop View mul门的本地梯度(来自cs231n)

扩展说明

另一个需要说明的是,当我们有一个节点连接到多个节点时,梯度会在这个节点累加,在这些分支上,根据多元链式法则,我们只会获取这些每个节点返回的上游梯度值,并且会将它们加起来,得到回流到这个节点的总上游梯度。你可以从多元链式法则中了解到这个。

你们可以这样思考这个问题,如果你要改变这个节点一点点,当你正通过这个图进行前向传递时,它会在前向传递中影响到所有连接到这个节点的节点,然后当你进行反向传播时,所有传回的梯度都会影响这个节点,这就是我们如何将这些加起来得到回流到这个节点的总上游梯度。

Desktop View gradient add at branches(来自cs231n)

如果x和多个变量都相关,比如这里不同的qi,那么根据链式法则,每一个中间变量都是输出f的自变量,而每一个局部变量x都是中间变量q的自变量,所以结果等于把这些项都相加求和。

Desktop View 偏导数公式(来自cs231n)

我们没有做任何操作来更新这个权重的值,我们只是发现了对这些变量的梯度。我们在本次课程中谈到的是如何在我们的函数中计算对于任何变量的梯度,然后一旦我们得到了这些梯度,我们就可以将我们刚才学到的知识应用到上一节关于优化的课里面。在给定梯度的情况下,我们沿着梯度方向上前进一步,就可以更新我们的权重,也就是我们的参数。

所以上节课我们学的是最优化问题的整体框架,今天学的是图和计算任意复杂函数的梯度。当我们后续讨论到神经网络中的复杂函数时,这是非常有用的。

高维向量

讲完一维的例子,我们要看一看变量是高维向量的情况。

Desktop View 雅克比矩阵(来自cs231n)

现在假设我们有xyz三个变量,三个向量而不是标量,这时整个计算流程还是一样,唯一的区别在于我们刚才的梯度变成了雅克比矩阵,所以现在是包含了每个变量里各元素导数的矩阵,比如z在每个x元素方向上的梯度。

Desktop View 对每个元素求最大值(来自cs231n)

看这样一个例子,我们有一个向量作为输入,一个有4096个元素的输入向量,在接下来要学的卷积神经网络中,这样的数据尺寸是比较常见的。中间的运算节点是对每个元素求最大值的运算,我们要求的fx中每个元素和零之间的最大值,最后的输出也是一个包含4096元素的向量。

这个例子中,雅克比矩阵的尺寸是几乘几?4096*4096

雅克比矩阵每一行的元素都是偏导数,矩阵的每个元素是输出向量的每个元素对输入向量每个元素分别求偏导的结果。

实际应用中可能会遇到更大的雅克比矩阵,因为我们要进行批处理,比如一次同时有输入100个向量,我们把这些一起送进运算节点以提高效率,那么这个矩阵长宽就要都再乘以100,这个矩阵的大小大于49000*49000,这个数据量太大而且完全无法实现,所以实际运算时,我们其实多数情况下不需要计算这么大的雅克比矩阵。为什么可以这样,这就要看这个雅克比矩阵有什么特点。如果我们思考一下这里的计算,我们对每个元素求最大值,再想想每一个偏导计算的结果,输入的某个元素和输出的某个元素之间有什么对应关系。

我们能在这个雅克比矩阵中找到什么特点呢?对角矩阵

因为是对每个元素分别运算,比如输入里的第一个元素,只和输出里的第一个元素有联系,所以算出的雅克比矩阵会是一个对角阵,所以我们并不需要把整个矩阵都写出来,我们只需要求输出向量关于x的偏导,然后把结果作为梯度填进去。

现在我们看一个跟具体的输入是向量的例子。

Desktop View 这个例子,具体的输入是向量(来自cs231n)

在这个例子中,我们有关于xW的函数f,设x是一个n维向量,W则是一个n*n的矩阵,和之前一样,第一步把计算图写出来。

Desktop View 计算图(来自cs231n)

上面给变量赋一些值,计算出中间结果和最后的输出,可以进行反向传播了。

Desktop View 开始反向传播(来自cs231n)

反向传播,第一步还是输出的梯度是1,现在反向前进一个节点,我们想求关于L2范数前的变量q方向上的梯度,q是一个二维向量,找到q的每个元素是怎样影响f最终的值的。

Desktop View 反向传播,第一步(来自cs231n)

如果我们观察下公式页面底部的f表达式,可以发现f在特定qi方向上的梯度,我们叫它q1,刚好是2倍的qi,这里做了求导。

关于每个qi元素,我们也可以写出它的向量形式,它就是2倍的q向量。如果我们写成向量形式,就是0.440.52的向量。可以看到,这里只用到了q,并扩大了两倍。所以向量的梯度总是和原向量保持相同的大小,每个梯度的元素代表着这个特定元素对最终函数影响的大小。

Desktop View 反向传播,第二步(来自cs231n)

现在我们倒退一步,关于W的梯度是什么?

这里我们运用相同的规则,应用链式法则,我们希望计算关于wq的本地梯度,让我们再看一下这个元素级别,如果我们这么做,我们观察一下对每个q的影响,q的每个元素对应w的每个元素,这就是雅克比矩阵。

Desktop View 反向传播,第三步(来自cs231n)

Desktop View 反向传播,第四步(来自cs231n)

复习WX的权重的雅克比矩阵求解原理。

模块化实现

Desktop View 前向/后向API的模块化实现(来自cs231n)

在这种情况下,非常像模块化计算。在计算图中,观察每个本地节点计算本地梯度,然后链式传播下去,利用逆向梯度,你可以把它看成一个前向和反向传播的API,在前向传播中计算节点输出,反向传播中计算梯度,在实际运行的时候,我们也是按照这个方法做的。我们可以把它想象成门,当我们运用正向和反向传播函数,用链式规则计算反向传播时,如果我们有整个计算图,我们可以迭代得到所有图的正向传播图中所有的节点,所有的门。我们运用可交换的门和节点可迭代计算所有的门。在每个门上前向传播,我们按照顺序的拓扑结构运算所有输入到一个节点,接着反向传播按照相反的方向通过所有的门,得到每个门的反向传播。

Desktop View 模块化实现的偏导数门(来自cs231n)

如果我们看一下我们偏导数门的运行,在这个例子中是一个乘法门,我们运行前向传播,输入xy,输出z。然后反向传播,输入dz逆向梯度,希望输出的是输入xy的梯度去传递下去,输出dxdy。这个例子中,所有数值都是标量。

Desktop View 模块化实现细节(来自cs231n)

看前向传播,有一件事情很重要,我们需要缓存前向传播的数值,因为我们需要用它去计算反向传播很多次。所以在正向传播中,我们希望存储xy这些值,在反向计算使用链式法则,将上游梯度的值使用其他分支的值对它进行量化,并保持。计算dx,我们需要使用已保存的self.y,然后将它与dz相乘,要计算dy也是一样。

举个例子:Caffe Layers

Desktop View Eg. Caffe Layers(来自cs231n)

当你在看很多深度学习框架和库时,你就会发现它们恰恰都使用了这样的模块化设计。比如Caffe是一个流行的深度学习框架,如果你看了Caffe的源代码,你可以看到一些目录,层与层间,这些都是基本的计算节点。通常来说层可能更多一些,其中的一些是更复杂的计算节点,像我们之前提到的sigmoid函数。你可以看到,基本上就是各种不同计算节点的整个列表。你可能会用到sigmoid,我知道还有卷积,还有Argmax作为另一个层,你会用到所有这些层,如果你深入去了解它们每一个,他们都会用到正向计算和反向计算。在我们在构建的整个网络中,在进行正向和反向计算时,所有这些都会被调用。所以我们的网络从根本上来说是由这些堆叠起来的,就是由网络中我们所选择的不同的层来构成。

比如,如果我们看看一个特定的例子。在这个例子中,这是一个sigmoid层,我们之前已经讲了sigmoid函数,你可以看到这里是正向计算,它只是根据sigmoid的表达式进行计算,然后是一个反向计算,将top_diff作为输入,在这个例子中它是上游梯度,将它与我们计算的本地梯度相乘。

Desktop View Sigmod Layer(来自cs231n)

练习:使用SVM和Softmax,计算图表的思路

作业中练习计算图表的思路,在这里将会写SVMSoftmax类,然后使用它们的梯度。所以记住第一步是使用计算图表来表示它,弄清楚要经过哪些计算可以得到你所需要的输出,然后当进行反推的时候,使用你在图表中定义的关于这些中间变量的梯度,使用链式法则将他们全部连接起来。

Desktop View 作业:SVM和Softmax,练习计算图表的思路(来自cs231n)

总结

Desktop View 总结和展望(来自cs231n)

当你使用神经网络时,他们都将会非常庞大和复杂,将所有参数的梯度公式写下来是不现实的。所以为了得到这些梯度,我们降到了应该使用反向传播,而且这算是神经网络的其中一个核心技术,就是使用反向传播来计算梯度,在我们有了计算图之后,这是一个链式法则的递归应用。我们从后开始进行反向计算,计算出相对于所有中间变量的梯度,这些中间变量是你的输入、参数和中间所有的值。我们也谈到了这些计算和图表结构都非常真实,每个节点也都真实存在,你可以将它看做正向和反向计算的API,所以在正推时,我们希望得到计算结果,并存储所有将会在后面的地图计算中用到的中间值,然后在反向计算中,我们使用链式法则使用上游梯度,将它与本地梯度相乘,计算在输出节点方向上的梯度,然后将它传递给下一个连接的节点。

本文由作者按照 CC BY 4.0 进行授权

© ManShouyuan. 保留部分权利。

本站总访问量 本站访客数人次

🚩🚩🚩🚩🚩🚩