深入浅出PyTorch:从模型到源码
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.6 链式求导法则和反向传播

优化深度学习模型的过程是优化模型的权重,从而让损失函数尽可能小的一个过程。这里可以把深度学习模型的损失函数看作是一个有两种参数的函数,其中,第一种参数是输入的数据,第二种参数是线性变换的权重,在给定输入数据(训练集)的情况下,如果想要求得最优的权重,根据微积分的知识可知,需要求得损失函数相对权重的导数(梯度),然后沿着导数的方向对权重进行变化。因此,这里需要研究一下怎么得到损失函数对于权重的导数。为了叙述的方便,我们会换用导数(Derivative)和梯度(Gradient)两个词,这两个词在上下文语境中可以认为是同义的。

1.6.1 基于链式求导的梯度计算

由于深度学习的模型是由输入层、隐含层和输出层构成的,而最后的损失函数由输出层的值决定,最终的损失函数是所有中间层的权重的复合函数。基于深度学习模型的复杂性,模型的计算是逐层叠加在一起进行的,我们可以得到式(1.34)所示的递推公式,其中,hj是第j层的神经元的值(这里称为数据),Wj是第j层的线性变换的权重,fjx)是对应的激活函数。我们可以看到,这里首先需要用到的是微积分中的链式求导法则,如式(1.35)所示(这里的xy均为向量,fy的函数,yx的函数,我们需要求fx的导数)。因此,求导应该视为对每个分量之间两两求导,因此,式(1.35)右边第一项和第二项均为导数的矩阵,链式法则最终归结为导数矩阵的乘法。

根据式(1.35)并假设最后的损失函数为L=fnhn)是输出层神经元值的函数,我们对两边求导,可以得到对应导数的递推式(1.36)和式(1.37),其中⊙符号代表按照分量一一对应相乘,其他的乘法均为矩阵乘法。假设hj+1m×1的向量,Wjm×n的矩阵,hjn×1的向量,则可以看到式(1.36)右端导数的形状和Wj的矩阵形状一致,式(1.37)右端导数的形状和hj的向量的形状一致。我们可以看到,在神经网络的前向计算过程中,需要用到式(1.34)递推进行计算,最后得到损失函数的值,而在神经网络的反向计算过程中,因为一开始获得的是损失函数相对于输出层神经元的函数和对应的导数值,则需要依靠式(1.36)和式(1.37)来反向计算损失函数相对于前一层的权重的导数式(1.36)和相对于前一层神经元值的导数式(1.37)。根据式(1.37)求出的损失函数相对于神经元的导数可以递归的应用式(1.36)和式(1.37),进一步求出前一层的对应导数,这样就能求得损失函数相对于所有权重的导数。这个过程称之为反向传播过程(Backward Propagation,BP),和计算损失函数时的前向传播(Forward)相反,同时前向计算的权重和反向计算的权重导数一一对应。为了方便起见,我们称式(1.36)求出的导数为权重梯度(Weight Gradient),式(1.37)求出的导数为数据梯度(Data Gradient)。这样公式可以总结为,前一层的数据梯度和权重梯度依赖于后一层的数据梯度,且数据梯度和权重有关,权重梯度和数据(当前神经网络层神经元的值)有关。由于需要求解的权重梯度和当前神经网络层的值有关,在计算神经网络的时候,在进行前向传播时一般需要保存每一层神经网络计算出来的结果,以便在反向传播的时候用来求权重梯度。

为了进一步方便理解,整个前向传播和反向传播过程可以画出图像,如图1.16所示。这里用矩形代表神经网络每一层神经元的值,用圆形代表每一层权重的值,用实线代表前向传播的过程,虚线代表反向传播的过程。这样我们可以看到整个递归的计算过程由神经网络层和权重层的互相连接构成。整个前向传播过程和反向传播过程可以看作是数据在图1.16中的流动,其中前向传播的计算可以看作是数据从左到右流动(右边的数据依赖左边的数据),反向传播过程与之相反,数据从右到左流动(左边的数据依赖右边的数据)。图1.16所示的节点的连接过程能够形象地描述神经网络的工作过程,我们称这个图为计算图(Computational Graph),现代几乎所有的深度学习框架都建立在计算图的构建,并沿着计算图的前向和反向传播上。当然,实际的深度学习计算图中节点之间的连接方式可能会更复杂,比如,两个不同数据层之间可能会相加、相乘等,但是基础的构建原理都是权重和数据节点进行计算,最后输出新的数据节点。

图1.16 前向传播(实线)和反向传播(虚线)示意图

1.6.2 激活函数的导数

在实践中,我们需要具体的函数的导数。在这里简单的列举如下,首先是Sigmoid函数的导数,该函数的导数如式(1.38)所示。我们可以看到,这个函数的最大值取在x=0处,相应的导数的值为1/4。另外,我们看到在x很大或很小的时候,对应的激活函数的导数都会趋向于0。同理,可以求得Tanh函数的导数,如式(1.39),正如前面介绍的,因为Tanh函数是Sigmoid函数的尺度变换,对应的导数情况和Sigmoid函数类似。相比于Sigmoid函数和Tanh函数是导数连续的函数,ReLU函数的导数并不连续,可以看到,在图1.17中ReLU函数的导数在x=0处有一个阶梯跳跃,从0变化到了1。因此,为严格起见,ReLU函数在x=0处导数不存在。一般来说,导数的不连续并不会影响深度学习模型的结果(而且实际上在权重比较多的情况下,线性变换的结果很少严格等于0)。为了方便计算,一般把x=0处的导数定义为0。另外,我们可以看到,ReLU函数在x0的条件下导数处为1,在反向传播的过程中,这意味着后一层的导数能够很容易地传导到前一层而没有任何衰减,这个函数行为有助于梯度(导数)在神经网络中的反向传播,对于深度学习神经网络的训练非常有意义。

图1.17 激活函数导数示意图

最后介绍一下梯度消失(Gradient Vanishing)和梯度爆炸(Gradient Explosion)的概念。在前面的内容里可以看到,在神经网络的训练过程中梯度的传播是一个反向传播的过程,而且与每一层的权重和数据有关。当神经网络很深的时候,我们可以看到,如果使用Sigmoid函数和Tanh函数作为激活函数(因为每次的梯度都小于1),那么在反向传播的时候,因为每次数据梯度的传播都要乘以关于数据的导数,数据梯度就会越来越小,对应的权重梯度也会越来越小。这样就会造成随着梯度的反向传播,靠前的神经网络层的梯度非常小。因此,会造成难以对这部分的权重进行优化的结果,这个现象就称为梯度消失。这对于深度学习模型是非常致命的,因为深度学习依赖的是许多层神经网络叠加在一起,组合成复杂的特征来进行预测。对深度学习模型来说,基本上就是几十上百层的神经网络叠加在一起,那么,如果使用Sigmoid和Tanh函数作为激活函数,前面几层的权重梯度就会非常接近于0。这也是普通的神经网络通常都用比较浅的几层隐含层的原因。用来解决这个问题的方法有很多,其中一个方法就是使用ReLU作为激活函数。在前面已经看到,ReLU函数既可以提供深度学习需要的非线性激活函数,也因为其特殊的梯度(每次传播的时候,数据大于0的部分的数据梯度保持不变)能够解决梯度消失的问题,因此,在深度学习中有着重要的作用。因为数据梯度对于在深度学习的反向传播的过程中起着重要的作用,而数据梯度的传播和权重有关。因此,权重取值的范围对于深度学习也非常重要。假如一开始权重取值比较大,我们会看到,随着反向传播的进行,数据梯度会逐渐变大,最后在前几层会变得非常大(超过浮点数的表示范围),这个过程称为梯度爆炸。因为数据梯度和权重梯度相互耦合,在这个情况下对权重进行优化(沿着梯度的方向改变权重)就可能会导致权重进一步增大。这样就会导致权重不稳定,从而使深度学习模型变得非常难以优化,甚至优化过程中会发生数值不稳定的现象。同理,如果权重的值非常小,那么就会造成梯度消失的结果。为了避免因为权重过大或者过小造成梯度爆炸和消失的结果,权重的初始化非常重要,其核心是让下一层的数据绝对值尽可能分布在1附近。本书在后续章节会陆续介绍权重初始化的内容。除权重的初始化外,为了避免梯度爆炸的结果,还可以对神经网络反向传播过程中的梯度做梯度截断(Gradient Clipping),使得梯度的L2模长小于一定的值,这样就可以避免梯度在传播过程中过大的问题。

1.6.3 数值梯度

由于权重和数据梯度的计算是神经网络算法的核心,基本上现代的深度学习框架都自带自动微分(Automatic Differentiation)功能。也就是说,对于框架提供的所有层,如果有数据输入、数据输出和权重,能够根据反向传入的梯度来计算对应的输入数据的数据梯度和线性变换层的权重梯度。这样,通过记录建立神经网络过程中所有的节点和最后的损失函数,建立一个有向无环图(Directed Acyclic Graph,DAG),再让梯度沿着有向无环图的反方向计算数据梯度和权重梯度,递归到输入节点,就能得到路径上所有权重的梯度。自动微分功能需要有对应的正向和反向传播函数的支持。在实践中,经常会碰到一种情况就是框架没有提供用户想要的神经网络层,用户需要自己实现一个,包括神经网络的前向和反向部分。前向部分的计算是否正确一般很容易验证,反向部分的计算可以通过数值微分(Numerical Difference)的方法来验证。其基本的如式(1.41)所示,其中,ε是一个比较小的数,比如10-6。这样就得到了真正梯度(解析梯度)的一个近似,即为数值梯度,由于x是一个向量,式(1.41)给出的是导数的一个分量的计算方法,如果要计算对整个向量的导数,则需要对每个分量进行相应的操作。因为数值梯度计算起来非常简单,而且结果不容易出错,可以把它作为解析梯度的一个参考值,验证解析梯度是否计算正确(验证两个梯度值是否非常接近)。另外,需要提到的一点是尽量不要在实际的深度学习模型中使用数值梯度,因为其计算速度会非常慢(对每个分量都要做数值计算)。