我们可以把f=W*x用一张类似流程图的东西表达出来,如下图所示,我们把这张图叫做计算图。其实就是把公式分解成小的计算单元。
在这张图中,输入的训练数据x和权重W通过矩阵乘法连接到一起,运算后得到得分向量s,s经过hinge loss计算得到损失项Li,最后L是数据项和正则项R的和。
那么反向传播是如何工作的?举个例子。
1.1.1 栗子1
这个式子f(x,y,z)=(x+y)z。我们取几个中间变量,q=x+y,这样整个式子就可以写成f=qz。我们可以从计算图中直观地来看。
我们的目的是求f分别对x,y,z的梯度。
首先,求f对f的梯度,df / df = 1,so easy
下面是求f对z的梯度,我们知道df / dz = q ,而q的值是3,所以f对z的梯度就是3
接下来求f对q的梯度,我们知道df / dq = z ,而z是-4,故f对q的梯度就是-4
上面求f对y的梯度,我们利用链式求导,f对q的偏导乘以q对y的偏导,就得到了f对y的偏导。在这里我们只需要用到上一个偏导值f对q的偏导,和本次的偏导q对y。
再求f对x的偏导,同样利用链式求导。
通过这个例子我们可以看到,求f对x和y的梯度,我们从后向前依次求每个节点的梯度。到了x和y输入的节点上,我们只需要将上一次的偏导乘以这一次的偏导,就可以求得我们想要的结果。比如对x来说,我们最终要求的f对x的梯度,我们从后往前算梯度,到最后跟x有关的节点时,只需要考虑本地梯度和上游梯度。
我们来看看在反向传播算法中发生了什么,我们从图的后面开始,然后使用我们的方法从最后的地方一直到开始的地方,当我们到达每一个节点时,每一个节点都会得到一个从上游返回的梯度,这个梯度是对这个节点的输出的求导,等到我们在反向传播中到达这个节点,我们就计算出了最终损失L,关于z的梯度,现在,我们接下来想做的就是去找到关于这个节点的输入梯度,即在x和y方向上的梯度。
在每一个节点上计算我们所需的本地梯度,然后跟踪这个梯度,在反向传播过程中,我们接收从上游传回来的这个梯度值,我们用这个值乘以本地梯度,然后得到我们想要传回连接点的值,在下一个节点反向传播时,我们不考虑除了直接相连的节点之外的任何东西。
1.1.2 栗子2
在这个例子里,f是关于w和x的函数,现在我们要对它进行反向传播,我们从图中的最末端开始,输出最终变量在某个方向上的梯度结果,首先末端的梯度是1,这没什么说的,现在开始对他进行反向传播。
我们知道从上游传回来的梯度值为1,现在我们需要找到本地梯度,关于这个节点的本地梯度,这个节点是1/x,本地梯度 df / dx = -1/x^2 ,然后带入我们在前向传播中得到的x的值为1.37,最后关于这个变量的梯度就是-1/1.37^2,等于-0.53。
然后往回到下一个节点,我们用同样的流程,我们知道从上游传回来的梯度是-0.53,这里的本地梯度呢,因为这个节点是+1,参照下面的微分表达式,我们有一个常数+x的微分表达式,那么它的本地梯度就是1了,那么使用链式法则时,对这个变量怎么求梯度呢?就是用上游的梯度-0.53乘以我们这里的本地梯度1,结果就是-0.53。
我们再次进行反向传播的下一步,上游传下来的是什么呢?是-0.53对吧,那么本地梯度呢?这是一个指数节点,对他求导还是e^x,所以本地梯度就是e^-1,所以最终梯度就是-0.53 * e^-1 = -0.2
到了下一个节点,写一个节点是*-1,从上游传回来的是-0.2,那么本地梯度是什么呢?看一下参照表,因为本地梯度df / dx = -1,所以这里的最终梯度就是 -1 * -0.2 = 0.2
现在我们看看一个加法运算的节点,这里有两个分支,都跟这里的节点相连,这里的上游梯度是0.2,这里每个分支的梯度,这是一个加法运算,从前面简单的例子可以看到,当我们遇到加法运算的节点,加法运算中对每个输入的梯度正好是1,对吧,所以在这里的本地梯度就是1,所以在这里的最终梯度就是 1 * 0.2 = 0.2
现在反向移动,来到w0和x0,这里是一个乘法节点,之前我们也见过乘法节点,这种节点对于某一输入的梯度值,恰好是另一个输入的值,因此在这种情况下,w0的梯度就是上游梯度 0.2 乘以 x0 的值 -1,得到-0.2,我们可以用同样的方法计算对x0的梯度。
因此,我们也可以用同样的方法,在w1和x1方向上的计算梯度。
我想提醒的一点是,当我们创建这些计算图的时候,我们可以以我们想要的任意间隔尺寸来定义计算节点,所以在这种情况下,我们把它分解成我们能够达到的最简单的形式,分解成加法和乘法运算,基本上没有比这更简单的了,但是实际上,我们可以把一些节点组合在一起形成更复杂的节点,要是我们想这么做的话,只要我们能写出那么节点的本地梯度,举个例子,比如这个sigmoid函数
关于x的sigmoid函数,等于1/(1+e^-x),我们可以计算他的梯度,并写出这个梯度的表达式,如果我们用数学知识推导出它的解析式,我们最终可以得到一个优美的表达式,在这里他等于(1 - sigma(x))sigma(x),类似于这样的情况,我们可以得到计算图中构成这个sigmoid函数的计算节点,然后用一个大的sigmoid函数节点替换掉,因为我们知道这里的本地梯度,就是这个表达式 d sigma(x) / dx ,这里最重要的事情是,你能聚合你想要的任意节点,去组成一些在某种程度上稍微复杂的节点,只要你能够写出它的本地梯度,这就是一个权衡,一方面是你想用多少数学计算,去得到一个更加简洁和简单的计算图,另一方面是你想要每个梯度有多简单,然后你就可以按照你想要的复杂度,写出一个计算图。
我们把sigmoid函数看作一个节点,它的上游梯度是1,本地梯度就是(1 - sigma(x))sigma(x),而我们知道 sigma(x) = 0.73,故得到的最终梯度就是 (1 - 0.73) * 0.73 * 1 = 0.2
前向传播中计算节点输出,反向传播中计算梯度。
在这里有一个max gate,输入是z和w,现在如果我们得到max的上游梯度,也就是2,这个本地梯度会是什么样?一个会取0,一个会取1。确切地说,答案应该是z的梯度应该是2,w的梯度会是0,所以,其中一个变量将会得到刚传递回来的梯度完整值,然而另一个变量的梯度会取0,我们可以把这个想象成梯度路由器,在前向传播中,可以看到只有最大值能被传递到,所以,只有这个最大值真正影响到我们函数计算的唯一值,因此这样是说得通的,当我们传递梯度回去时,我们只是想要通过这个计算分支来调整它。
另一个需要说明的情况是上图所示,当有一个节点连接到多个节点时,梯度会在这个节点累加。在这些分支上,根据多元链式法则,只会获取每个节点的返回的上游梯度值,然后将它们加起来获得这个节点总的上游梯度。
可以这样思考,如果要改变这个节点一点点,当通过这个图进行前向传递时,它会影响在前向传递中影响到所有连接这个节点的节点,然后当进行反向传播时,所有传回的梯度都会影响到这个节点,这就是为什么将这些加起来得到回流到这个点的总上游梯度值。
如上图所示,输入是W和x,W是一个2x2的矩阵,x是一个2x1的矩阵,现在我们开始反向传播,还是第一步,输出的梯度是1,现在反向前进一个节点,我们想求关于L2范数前的变量q方向上的梯度,q是一个二维向量,我们想要做到事情是找到q的每个元素是怎样影响f最终的值的,如果我们观察一下公式,图片底部的f表达式,我们可以发现f在特定qi方向上梯度刚好是2倍的qi
现在我们倒退一步讲,关于W的梯度是什么,这里我们再次运用相同的概念,应用链式规则,我们希望计算关于w的q的本地梯度,我们观察一下对每个q的影响,q的每个元素对应w的每个元素,这就是雅可比矩阵。在这个乘法公式中q等于W乘以x,我们的第一个元素对应W11,所以q1对应W11,结果是什么?x1,yeah!我们可以把这个结果推广到一般,关于Wij的qk梯度等于xj,所以我们就得到了关于W的梯度。
这里图片中的关于W的梯度值计算给错了,正确的应该是图中所算出来的转置矩阵 ,敲黑板了哈
同理,可以得到关于x的梯度
所以,我们可以将上述的前向传播和后向传播的方法模块化成一个API,如下所示:
总结:
(1)神经网络都将会是非常庞大和复杂,所以将所有参数的梯度公式写下来是不现实的;
(2)为了得到这些梯度,应该使用反向传播——神经网络中的一个核心技术就是使用反向传播来计算梯度,我们利用计算图和链式法则,从后开始计算出所有中间变量的梯度;
(3)正向:希望得到计算结果,并存储所有将会在后面的梯度计算中用到的中间值;
(4)反向:使用链式法则、上游梯度将它与本地梯度相乘,计算在输出节点方向上的梯度,然后将它传递给下一个连接的节点。
在此前我们已经使用了很多这种计分函数:
现在使用一个2层的神经网络:
或者使用一个3层的神经网络:
一般来说,神经网络就是由简单函数构成的一组函数,使用一种层次化的方式将它们堆叠起来,形成一个更复杂的非线性函数;这也正是深度神经网络的由来,可以堆积很多层形成深度网络。
有很多人在谈论神经网络如何从生物学中获得灵感;说起神经元,每个神经元有很多树突用来接收脉冲信号,然后通过细胞体处理这些信号,接着通过轴突将处理后的信号输出;所以和神经元很类似,神经网络的结构和流程也是这样。
计算图里的节点相互连接,我们需要输入“信号x”,所有x的输入量比如x0、x1、x2等,采用比如赋予权重W的方法,叠加汇合到一起,将结果整合起来后得到一个激活函数,将激活函数应用在神经元的端部,得到的值作为输出。
注意:在进行这种类比时要特别小心,因为生物学上的神经元实际上比我们描述的要复杂的多,它们的树突会比表现出异常复杂的非线性,而并非像我们描述的那样只有简单的权重。
另外,提到激活函数,我们已经讨论过了多种不同的激活函数,之后我们会对所有的激活函数进行更加详细的讨论。
下面是神经网络的结构:
两层神经网络也可以叫单隐藏层网络
三层神经网络也是双隐藏层网络
总结:
(1)本节中讨论了如何将神经元组织起来进行运算;
(2)神经元抽象的好处使我们可以采用非常高效的向量化代码进行运算;
参考链接:
https://blog.csdn.net/qq_34611579/article/details/81023228
https://blog.csdn.net/poulang5786/article/details/79981764