乐读窝

深度学习

乐读窝 > 文学理论 > 深度学习

6.5 反向传播和其他的微分算法

书籍名:《深度学习》    作者:伊恩.古德费洛



当我们使用前馈神经网络接收输入x并产生输出时,信息通过网络向前流动。输入x提供初始信息,然后传播到每一层的隐藏单元,最终产生输出。这称之为前向传播(forward  propagation)。在训练过程中,前向传播可以持续向前直到它产生一个标量代价函数J(θ)。反向传播(back  propagation)算法(Rumelhart  et  al.,1986c),经常简称为backprop,允许来自代价函数的信息通过网络向后流动,以便计算梯度。

计算梯度的解析表达式是很直观的,但是数值化地求解这样的表达式在计算上的代价可能很大。反向传播算法使用简单和廉价的程序来实现这个目标。

反向传播这个术语经常被误解为用于多层神经网络的整个学习算法。实际上,反向传播仅指用于计算梯度的方法,而另一种算法,例如随机梯度下降,使用该梯度来进行学习。此外,反向传播经常被误解为仅适用于多层神经网络,但是原则上它可以计算任何函数的导数(对于一些函数,正确的响应是报告函数的导数是未定义的)。特别地,我们会描述如何计算一个任意函数f的梯度,其中x是一组变量,我们需要它们的导数,而y是函数的另外一组输入变量,但我们并不需要它们的导数。在学习算法中,我们最常需要的梯度是代价函数关于参数的梯度,即。许多机器学习任务需要计算其他导数,来作为学习过程的一部分,或者用来分析学得的模型。反向传播算法也适用于这些任务,不局限于计算代价函数关于参数的梯度。通过在网络中传播信息来计算导数的想法非常普遍,它还可以用于计算诸如多输出函数f的Jacobian的值。我们这里描述的是最常用的情况,其中f只有单个输出。



6.5.1 计算图


到目前为止,我们已经用相对非正式的图形语言讨论了神经网络。为了更精确地描述反向传播算法,使用更精确的计算图(computational  graph)语言是很有帮助的。

将计算形式化为图形的方法有很多。

这里,我们使用图中的每一个节点来表示一个变量。变量可以是标量、向量、矩阵、张量或者甚至是另一类型的变量。

为了形式化图形,我们还需引入操作(operation)这一概念。操作是指一个或多个变量的简单函数。图形语言伴随着一组被允许的操作。我们可以通过将多个操作复合在一起来描述更为复杂的函数。

为了不失一般性,我们定义一个操作仅返回单个输出变量。这并没有失去一般性,是因为输出变量可以有多个条目,例如向量。反向传播的软件实现通常支持具有多个输出的操作,但是我们在描述中避免这种情况,因为它引入了对概念理解不重要的许多额外细节。

如果变量y是变量x通过一个操作计算得到的,那么我们画一条从x到y的有向边。有时我们用操作的名称来注释输出的节点,当上下文很明确时,有时也会省略这个标注。

计算图的示例可以参考图6.8。

图6.8 一些计算图的示例。(a)使用×操作计算z=xy的图。(b)用于逻辑回归预测的图。一些中间表达式在代数表达式中没有名称,但在图形中却需要。我们简单地将第i个这样的变量命名为u(i)。(c)表达式的计算图,在给定包含小批量输入数据的设计矩阵x时,它计算整流线性单元激活的设计矩阵H。(d)示例(a)到(c)对每个变量最多只实施一个操作,但是对变量实施多个操作也是可能的。这里我们展示一个计算图,它对线性回归模型的权重w实施多个操作。这个权重不仅用于预测,也用于权重衰减罚项



6.5.2 微积分中的链式法则


微积分中的链式法则(为了不与概率中的链式法则相混淆)用于计算复合函数的导数。反向传播是一种计算链式法则的算法,使用高效的特定运算顺序。

设x是实数,f和g是从实数映射到实数的函数。假设y=g(x)并且z=f(g(x))=f(y)。那么链式法则是说

我们可以将这种标量情况进行扩展。假设,g是从的映射,f是从的映射。如果,那么

使用向量记法,可以等价地写成

这里是g的n×m的Jacobian矩阵。

从这里我们看到,变量x的梯度可以通过Jacobian矩阵和梯度相乘来得到。反向传播算法由图中每一个这样的Jacobian梯度的乘积操作所组成。

通常我们将反向传播算法应用于任意维度的张量,而不仅仅用于向量。从概念上讲,这与使用向量的反向传播完全相同。唯一的区别是如何将数字排列成网格以形成张量。我们可以想象,在运行反向传播之前,将每个张量变平为一个向量,计算一个向量值梯度,然后将该梯度重新构造成一个张量。从这种重新排列的观点上看,反向传播仍然只是将Jacobian乘以梯度。

为了表示值z关于张量Χ的梯度,我们记为,就像Χ是向量一样。Χ的索引现在有多个坐标——例如,一个3维的张量由3个坐标索引。我们可以通过使用单个变量i来表示完整的索引元组,从而完全抽象出来。对所有可能的元组i,。这与向量中索引的方式完全一致,。使用这种记法,我们可以写出适用于张量的链式法则。如果,那么



6.5.3 递归地使用链式法则来实现反向传播


使用链式规则,我们可以直接写出某个标量关于计算图中任何产生该标量的节点的梯度的代数表达式。然而,实际在计算机中计算该表达式时会引入一些额外的考虑。

具体来说,许多子表达式可能在梯度的整个表达式中重复若干次。任何计算梯度的程序都需要选择是存储这些子表达式还是重新计算它们几次。图6.9给出了一个例子来说明这些重复的子表达式是如何出现的。在某些情况下,计算两次相同的子表达式纯粹是浪费。在复杂图中,可能存在指数多的这种计算上的浪费,使得简单的链式法则不可实现。在其他情况下,计算两次相同的子表达式可能是以较高的运行时间为代价来减少内存开销的有效手段。

图6.9 计算梯度时导致重复子表达式的计算图。令为图的输入。我们对链中的每一步使用相同的操作函数,这样x=f(w),y=f(x),z=f(y)。为了计算,我们应用式(6.44)得到



式(6.50)建议我们采用的实现方式是,仅计算f(w)的值一次并将它存储在变量x中。这是反向传播算法所采用的方法。式(6.51)提出了一种替代方法,其中子表达式f(w)出现了不止一次。在替代方法中,每次只在需要时重新计算f(w)。当存储这些表达式的值所需的存储较少时,式(6.50)的反向传播方法显然是较优的,因为它减少了运行时间。然而,式(6.51)也是链式法则的有效实现,并且当存储受限时它是有用的

我们首先给出一个版本的反向传播算法,它指明了梯度的直接计算方式(算法6.2以及相关的正向计算的算法6.1  ),按照它实际完成的顺序并且递归地使用链式法则。我们可以直接执行这些计算或者将算法的描述视为用于计算反向传播的计算图的符号表示。然而,这些公式并没有明确地操作和构造用于计算梯度的符号图。这些公式将在后面的第6.5.6节和算法6.5中给出,其中我们还推广到了包含任意张量的节点。

首先考虑描述如何计算单个标量u(n)(例如训练样本上的损失函数)的计算图。我们想要计算这个标量对ni个输入节点u(1)到的梯度。换句话说,我们希望对所有的计算。在使用反向传播计算梯度来实现参数的梯度下降时,u(n)将对应单个或者小批量实例的代价函数,而u(1)到则对应于模型的参数。

假设图的节点已经以一种特殊的方式被排序,使得我们可以一个接一个地计算他们的输出,从开始,一直上升到u(n)。如算法6.1中所定义的,每个节点u(i)与操作f(i)相关联,并且通过对以下函数求值来得到

其中是u(i)所有父节点的集合。

该算法详细说明了前向传播的计算,我们可以将其放入图中。为了执行反向传播,我们可以构造一个依赖于并添加额外一组节点的计算图。这形成了一个子图,它的每个节点都是的节点。中的计算和中的计算顺序完全相反,而且中的每个节点计算导数与前向图中的节点u(i)相关联。这通过对标量输出u(n)使用链式法则来完成:

*  *  *

算法6.1 计算将ni个输入u(1)到映射到一个输出u(n)的程序。这定义了一个计算图,其中每个节点通过将函数f(i)应用到变量集合上来计算u(i)的值,包含先前节点u(j)的值满足j<i且。计算图的输入是向量x,并且被分配给前ni个节点u(1)到。计算图的输出可以从最后一个(输出)节点u(n)读出。

算法6.2 反向传播算法的简化版本,用于计算u(n)关于图中变量的导数。这个示例旨在通过演示所有变量都是标量的简化情况来进一步理解反向传播算法,这里我们希望计算关于的导数。这个简化版本计算了关于图中所有节点的导数。假定与每条边相关联的偏导数计算需要恒定的时间的话,该算法的计算成本与图中边的数量成比例。这与前向传播的计算次数具有相同的阶。每个是u(i)的父节点u(j)的函数,从而将前向图的节点链接到反向传播图中添加的节点。

*  *  *

 运行前向传播(对于此例是算法6.1  )获得网络的激活。

 初始化grad  table,用于存储计算好的导数的数据结构。grad  table[u(i)]将存储计算好的值。

 grad  table[u(n)]←1

 for  j=n−1  down  to  1  do

  下一行使用存储的值计算

end  for



*  *  *



这在算法6.2中详细说明。子图恰好包含每一条对应着中从节点u(j)到节点u(i)的边。从u(j)到u(i)的边对应着计算。另外,对于每个节点都要执行一个内积,内积的一个因子是对于uj子节点u(i)的已经计算的梯度,另一个因子是对于相同子节点u(i)的偏导数组成的向量。总而言之,执行反向传播所需的计算量与中的边的数量成比例,其中每条边的计算包括计算偏导数(节点关于它的一个父节点的偏导数)以及执行一次乘法和一次加法。下面,我们将此分析推广到张量值节点,这只是在同一节点中对多个标量值进行分组并能够更高效地实现。

反向传播算法被设计为减少公共子表达式的数量而不考虑存储的开销。具体来说,它大约对图中的每个节点执行一个Jacobian乘积。这可以从算法6.2中看出,反向传播算法访问了图中的节点u(j)到节点u(i)的每条边一次,以获得相关的偏导数。反向传播因此避免了重复子表达式的指数爆炸。然而,其他算法可能通过对计算图进行简化来避免更多的子表达式,或者也可能通过重新计算而不是存储这些子表达式来节省内存。我们将在描述完反向传播算法本身后再重新审视这些想法。



6.5.4 全连接MLP中的反向传播计算


为了阐明反向传播的上述定义,让我们考虑一个与全连接的多层MLP相关联的特定图。

算法6.3首先给出了前向传播,它将参数映射到与单个训练样本(输入,目标)(x,y)相关联的监督损失函数,其中是当x提供输入时神经网络的输出。

*  *  *

算法6.3 典型深度神经网络中的前向传播和代价函数的计算。损失函数取决于输出和目标y(参考第6.2.1.1节中损失函数的示例)。为了获得总代价J,损失函数可以加上正则项Ω(θ),其中θ包含所有参数(权重和偏置)。算法6.4说明了如何计算J关于参数和b的梯度。为简单起见,该演示仅使用单个输入样本x。实际应用应该使用小批量。请参考第6.5.7节以获得更加真实的演示。

*  *  *

Require:网络深度,l

Require:W,模型的权重矩阵

Require:,模型的偏置参数

Require:x,程序的输入

Require:y,目标输出

 h(0)=x

 for  k=1,…,l  do



 end  for



*  *  *

算法6.4随后说明了将反向传播应用于该图所需的相关计算。

算法6.3和算法6.4是简单而直观的演示。然而,它们专门针对特定的问题。

现在的软件实现基于之后第6.5.6节中描述的一般形式的反向传播,它可以通过显式地操作表示符号计算的数据结构,来适应任何计算图。



6.5.5 符号到符号的导数


代数表达式和计算图都对符号(symbol)或不具有特定值的变量进行操作。这些代数或者基于图的表达式被称为符号表示(symbolic  representation)。当实际使用或者训练神经网络时,我们必须给这些符号赋特定的值。我们用一个特定的数值(numeric  value)来替代网络的符号输入x,例如。

*  *  *

算法6.4 深度神经网络中算法6.3的反向计算,它不止使用了输入x和目标y。该计算对于每一层k都产生了对激活的梯度,从输出层开始向后计算一直到第一个隐藏层。这些梯度可以看作对每层的输出应如何调整以减小误差的指导,根据这些梯度可以获得对每层参数的梯度。权重和偏置上的梯度可以立即用作随机梯度更新的一部分(梯度算出后即可执行更新),或者与其他基于梯度的优化方法一起使用。

*  *  *

 在前向计算完成后,计算顶层的梯度:

 for  k=l,l−1,…,1  do

  将关于层输出的梯度转换为非线性激活输入前的梯度(如果f是逐元素的,则逐元素地相乘):

  计算关于权重和偏置的梯度(如果需要的话,还要包括正则项):

  关于下一更低层的隐藏层传播梯度:

end  for

*  *  *

一些反向传播的方法采用计算图和一组用于图的输入的数值,然后返回在这些输入值处梯度的一组数值。我们将这种方法称为符号到数值的微分。这种方法用在诸如Torch(Collobert  et  al.,2011b)和Caffe(Jia,2013)之类的库中。

另一种方法是采用计算图以及添加一些额外的节点到计算图中,这些额外的节点提供了我们所需导数的符号描述。这是Theano(Bergstra  et  al.,2010b;Bastien  et  al.,2012b)和TensorFlow(Abadi  et  al.,2015)所采用的方法。图6.10给出了该方法如何工作的一个例子。

图6.10 使用符号到符号的方法计算导数的示例。在这种方法中,反向传播算法不需要访问任何实际的特定数值。相反,它将节点添加到计算图中来描述如何计算这些导数。通用图形求值引擎可以在随后计算任何特定数值的导数。(左)在这个例子中,我们从表示z=f(f(f(w)))的图开始。(右)我们运行反向传播算法,指导它构造表达式对应的图。在这个例子中,我们不解释反向传播算法如何工作。我们的目的只是说明想要的结果是什么:符号描述的导数的计算图

这种方法的主要优点是导数可以使用与原始表达式相同的语言来描述。因为导数只是另外一张计算图,我们可以再次运行反向传播,对导数再进行求导就能得到更高阶的导数。高阶导数的计算在第6.5.10节中描述。

我们将使用后一种方法,并且使用构造导数的计算图的方法来描述反向传播算法。图的任意子集之后都可以使用特定的数值来求值。这允许我们避免精确地指明每个操作应该在何时计算。相反,通用的图计算引擎只要当一个节点的父节点的值都可用时就可以进行求值。

基于符号到符号的方法的描述包含了符号到数值的方法。符号到数值的方法可以理解为执行了与符号到符号的方法中构建图的过程中完全相同的计算。关键的区别是符号到数值的方法不会显示出计算图。



6.5.6 一般化的反向传播


反向传播算法非常简单。为了计算某个标量z关于图中它的一个祖先x的梯度,首先观察到它关于z的梯度由给出。然后,我们可以计算对图中z的每个父节点的梯度,通过现有的梯度乘以产生z的操作的Jacobian。我们继续乘以Jacobian,以这种方式向后穿过图,直到到达x。对于从z出发可以经过两个或更多路径向后行进而到达的任意节点,我们简单地对该节点来自不同路径上的梯度进行求和。

更正式地,图中的每个节点对应着一个变量。为了实现最大的一般化,我们将这个变量描述为一个张量V。张量通常可以具有任意维度,并且包含标量、向量和矩阵。

我们假设每个变量V与下列子程序相关联:

get_operation(V):它返回用于计算V的操作,代表了在计算图中流入V的边。例如,可能有一个Python或者C++的类表示矩阵乘法操作,以及get_operation函数。假设我们的一个变量是由矩阵乘法产生的,C=AB。那么,get_operation(V)返回一个指向相应C++类的实例的指针。

:它返回一组变量,是计算图中V的子节点。

:它返回一组变量,是计算图中V的父节点。

每个操作op也与bprop操作相关联。该bprop操作可以计算如式(6.47)所描述的Jacobian向量积。这是反向传播算法能够实现很大通用性的原因。每个操作负责了解如何通过它参与的图中的边来反向传播。例如,我们可以使用矩阵乘法操作来产生变量C=AB。假设标量z关于C的梯度是G。矩阵乘法操作负责定义两个反向传播规则,每个规则对应于一个输入变量。如果我们调用bprop方法来请求关于A的梯度,那么在给定输出的梯度为G的情况下,矩阵乘法操作的bprop方法必须说明关于A的梯度是。类似地,如果我们调用bprop方法来请求关于B的梯度,那么矩阵操作负责实现bprop方法并指定希望的梯度是。反向传播算法本身并不需要知道任何微分法则。它只需要使用正确的参数调用每个操作的bprop方法即可。正式地,必须返回

这只是如式(6.47)所表达的链式法则的实现。这里,inputs是提供给操作的一组输入,op.f是操作实现的数学函数,X是输入,我们想要计算关于它的梯度,G是操作对于输出的梯度。

op.bprop方法应该总是假装它的所有输入彼此不同,即使它们不是。例如,如果mul操作传递两个x来计算x2,op.bprop方法应该仍然返回x作为对于两个输入的导数。反向传播算法后面会将这些变量加起来获得2x,这是x上总的正确的导数。

反向传播算法的软件实现通常提供操作和其bprop方法,所以深度学习软件库的用户能够对使用诸如矩阵乘法、指数运算、对数运算等常用操作构建的图进行反向传播。构建反向传播新实现的软件工程师或者需要向现有库添加自己的操作的高级用户通常必须手动为新操作推导op.bprop方法。

反向传播算法的正式描述参考算法6.5。


*  *  *

算法6.5 反向传播算法最外围的骨架。这部分做简单的设置和清理工作。大多数重要的工作发生在算法6.6的子程序build_grad中。

*  *  *

Require:,需要计算梯度的目标变量集

Require:,计算图

Require:z,要微分的变量

 令剪枝后的计算图,其中仅包括z的祖先以及中节点的后代。

 初始化grad  table,它是关联张量和对应导数的数据结构。

 grad  table[z]←1



end  for

Return  grad  table  restricted  to

*  *  *

在第6.5.2节中,我们使用反向传播作为一种策略来避免多次计算链式法则中的相同子表达式。由于这些重复子表达式的存在,简单的算法可能具有指数运行时间。现在我们已经详细说明了反向传播算法,可以去理解它的计算成本了。如果我们假设每个操作的执行都有大致相同的开销,那么可以依据执行操作的数量来分析计算成本。注意这里我们将一个操作记为计算图的基本单位,它实际可能包含许多算术运算(例如,我们可能将矩阵乘法视为单个操作)。在具有n个节点的图中计算梯度,将永远不会执行超过O(n2)个操作,或者存储超过O(n2)个操作的输出。这里我们是对计算图中的操作进行计数,而不是由底层硬件执行的单独操作,所以重要的是,要记住每个操作的运行时间可能是高度可变的。例如,两个矩阵相乘可能对应着图中的一个单独的操作,但这两个矩阵可能每个都包含数百万个元素。我们可以看到,计算梯度至多需要O(n2)的操作,因为在最坏的情况下,前向传播的步骤将在原始图的全部n个节点上运行(取决于我们想要计算的值,可能不需要执行整个图)。反向传播算法在原始图的每条边添加一个Jacobian向量积,可以用O(1)个节点来表达。因为计算图是有向无环图,它至多有O(n2)条边。对于实践中常用图的类型,情况会更好。大多数神经网络的代价函数大致是链式结构的,使得反向传播只有O(n)的成本。这远远胜过简单的方法,简单方法可能需要在指数级的节点上运算。这种潜在的指数级代价可以通过非递归地扩展和重写递归链式法则(式(6.53))来看出:

*  *  *

算法6.6 反向传播算法的内循环子程序,由算法6.5中定义的反向传播算法调用。



由于节点j到节点n的路径数目可以关于这些路径的长度上指数地增长,所以上述求和符号中的项数(这些路径的数目),可能以前向传播图的深度的指数级增长。会产生如此大的成本是因为对于,相同的计算会重复进行很多次。为了避免这种重新计算,我们可以将反向传播看作一种表填充算法,利用存储的中间结果来对表进行填充。图中的每个节点对应着表中的一个位置,这个位置存储对该节点的梯度。通过顺序填充这些表的条目,反向传播算法避免了重复计算许多公共子表达式。这种表填充策略有时被称为动态规划(dynamic  programming)。



6.5.7 实例:用于MLP训练的反向传播


作为一个例子,我们利用反向传播算法来训练多层感知机。

这里,我们考虑一个具有单个隐藏层的非常简单的多层感知机。为了训练这个模型,我们将使用小批量随机梯度下降算法。反向传播算法用于计算单个小批量上的代价的梯度。具体来说,我们使用训练集上的一小批量实例,将其规范化为一个设计矩阵X以及相关联的类标签向量y。网络计算隐藏特征层。为了简化表示,我们在这个模型中不使用偏置。假设我们的图语言包含relu操作,该操作可以对表达式的每个元素分别进行计算。类的非归一化对数概率的预测将随后由HW(2)给出。假设我们的图语言包含cross_entropy操作,用以计算目标y和由这些未归一化对数概率定义的概率分布间的交叉熵。所得到的交叉熵定义了代价函数JMLE。最小化这个交叉熵将执行对分类器的最大似然估计。然而,为了使得这个例子更加真实,我们也包含一个正则项。总的代价函数为

包含了交叉熵和系数为λ的权重衰减项。它的计算图在图6.11中给出。

图6.11 用于计算代价函数的计算图,这个代价函数是使用交叉熵损失以及权重衰减训练我们的单层MLP示例所产生的

这个示例的梯度计算图实在太大,以至于绘制或者阅读都将是乏味的。这显示出了反向传播算法的优点之一,即它可以自动生成梯度,而这种计算对于软件工程师来说需要进行直观但冗长的手动推导。

我们可以通过观察图6.11中的正向传播图来粗略地描述反向传播算法的行为。为了训练,我们希望计算。有两种不同的路径从J后退到权重:一条通过交叉熵代价,另一条通过权重衰减代价。权重衰减代价相对简单,它总是对W(i)上的梯度贡献2λW(i)。

另一条通过交叉熵代价的路径稍微复杂一些。令G是由cross_entropy操作提供的对未归一化对数概率U(2)的梯度。反向传播算法现在需要探索两个不同的分支。在较短的分支上,它使用对矩阵乘法的第二个变量的反向传播规则,将加到W(2)的梯度上。另一条更长些的路径沿着网络逐步下降。首先,反向传播算法使用对矩阵乘法的第一个变量的反向传播规则,计算。接下来,relu操作使用其反向传播规则来对关于U(1)的梯度中小于0的部分清零。记上述结果为G′。反向传播算法的最后一步是使用对matmul操作的第二个变量的反向传播规则,将加到W(2)  的梯度上。

在计算了这些梯度以后,梯度下降算法或者其他优化算法所要做的就是使用这些梯度来更新参数。

对于MLP,计算成本主要来源于矩阵乘法。在前向传播阶段,我们乘以每个权重矩阵,得到了O(w)数量的乘  -  加,其中w是权重的数量。在反向传播阶段,我们乘以每个权重矩阵的转置,这具有相同的计算成本。算法主要的存储成本是我们需要将输入存储到隐藏层的非线性中去。这些值从被计算时开始存储,直到反向过程回到了同一点。因此存储成本是O(mnh),其中m是小批量中样本的数目,nh是隐藏单元的数量。



6.5.8 复杂化


我们这里描述的反向传播算法要比实践中实际使用的实现要简单。

正如前面提到的,我们将操作的定义限制为返回单个张量的函数。大多数软件实现需要支持可以返回多个张量的操作。例如,如果我们希望计算张量中的最大值和该值的索引,则最好在单次运算中计算两者,因此将该过程实现为具有两个输出的操作效率更高。

我们还没有描述如何控制反向传播的内存消耗。反向传播经常涉及将许多张量加在一起。在朴素方法中,将分别计算这些张量中的每一个,然后在第二步中对所有这些张量求和。朴素方法具有过高的存储瓶颈,可以通过保持一个缓冲器,并且在计算时将每个值加到该缓冲器中来避免该瓶颈。

反向传播的现实实现还需要处理各种数据类型,例如32位浮点数、64位浮点数和整型。处理这些类型的策略需要特别的设计考虑。

一些操作具有未定义的梯度,并且重要的是跟踪这些情况并且确定用户请求的梯度是否是未定义的。

各种其他技术的特性使现实世界的微分更加复杂。这些技术性并不是不可逾越的,本章已经描述了计算微分所需的关键知识工具,但重要的是要知道还有许多的精妙之处存在。



6.5.9 深度学习界以外的微分


深度学习界在某种程度上已经与更广泛的计算机科学界隔离开来,并且在很大程度上发展了自己关于如何进行微分的文化态度。一般来说,自动微分(automatic  differentiation)领域关心如何以算法方式计算导数。这里描述的反向传播算法只是自动微分的一种方法。它是一种称为反向模式累加(reverse  mode  accumulation)的更广泛类型的技术的特殊情况。其他方法以不同的顺序来计算链式法则的子表达式。一般来说,确定一种计算的顺序使得计算开销最小,是困难的问题。找到计算梯度的最优操作序列是NP完全问题(Naumann,2008),在这种意义上,它可能需要将代数表达式简化为它们最廉价的形式。

例如,假设有变量p1,p2…,pn表示概率,以及变量z1,z2,…zn表示未归一化的对数概率。假设定义

其中我们通过指数化、求和与除法运算构建softmax函数,并构造交叉熵损失函数。人类数学家可以观察到J对zi的导数有一个非常简单的形式:qi−pi反向传播算法不能够以这种方式来简化梯度,而是会通过原始图中的所有对数和指数操作显式地传播梯度。一些软件库如Theano(Bergstra  et  al.,2010b;Bastien  et  al.,2012b)能够执行某些种类的代数替换来改进由纯反向传播算法提出的图。

当前向图具有单个输出节点,并且每个偏导数都可以用恒定的计算量来计算时,反向传播保证梯度计算的计算数目和前向计算的计算数目是同一个量级:这可以在算法6.2中看出,因为每个局部偏导数以及递归链式公式(式(6.53))中相关的乘和加都只需计算一次。因此,总的计算量是O(#edges)。然而,可能通过对反向传播算法构建的计算图进行简化来减少这些计算量,并且这是NP完全问题。诸如Theano和TensorFlow的实现使用基于匹配已知简化模式的试探法,以便重复地尝试去简化图。我们定义反向传播仅用于计算标量输出的梯度,但是反向传播可以扩展到计算Jacobian矩阵(该Jacobian矩阵或者来源于图中的k个不同标量节点,或者来源于包含k个值的张量值节点)。朴素的实现可能需要k倍的计算:对于原始前向图中的每个内部标量节点,朴素的实现计算k个梯度而不是单个梯度。当图的输出数目大于输入的数目时,有时更偏向于使用另外一种形式的自动微分,称为前向模式累加(forward  mode  accumulation)。前向模式计算已经被提出用于循环神经网络梯度的实时计算,例如(Williams  and  Zipser,1989)。这也避免了存储整个图的值和梯度的需要,是计算效率和内存使用的折中。前向模式和后向模式的关系类似于左乘和右乘一系列矩阵之间的关系,例如

其中的矩阵可以认为是Jacobian矩阵。例如,如果D是列向量,而A有很多行,那么这对应于一幅具有单个输出和多个输入的图,并且从最后开始乘,反向进行,只需要矩阵-向量的乘积。这对应着反向模式。相反,从左边开始乘将涉及一系列的矩阵-矩阵乘积,这使得总的计算变得更加昂贵。然而,如果A的行数小于D的列数,则从左到右乘更为便宜,这对应着前向模式。

在机器学习以外的许多社区中,更常见的是使用传统的编程语言来直接实现微分软件,例如用Python或者C来编程,并且自动生成使用这些语言编写的不同函数的程序。在深度学习界中,计算图通常使用由专用库创建的明确的数据结构表示。专用方法的缺点是需要库开发人员为每个操作定义bprop方法,并且限制了库的用户仅使用定义好的那些操作。然而,专用方法也允许定制每个操作的反向传播规则,允许开发者以非显而易见的方式提高速度或稳定性,对于这种方式自动的过程可能不能复制。

因此,反向传播不是计算梯度的唯一方式或最佳方式,但它是一个非常实用的方法,继续为深度学习社区服务。在未来,深度网络的微分技术可能会提高,因为深度学习的从业者更加懂得了更广泛的自动微分领域的进步。



6.5.10 高阶微分


一些软件框架支持使用高阶导数。在深度学习软件框架中,这至少包括Theano和TensorFlow。这些库使用一种数据结构来描述要被微分的原始函数,它们使用相同类型的数据结构来描述这个函数的导数表达式。这意味着符号微分机制可以应用于导数(从而产生高阶导数)。

在深度学习的相关领域,很少会计算标量函数的单个二阶导数。相反,我们通常对Hessian矩阵的性质比较感兴趣。如果我们有函数,那么Hessian矩阵的大小是n×n。在典型的深度学习应用中,n将是模型的参数数量,可能很容易达到数十亿。因此,完整的Hessian矩阵甚至不能表示。

典型的深度学习方法是使用Krylov方法(Krylov  method),而不是显式地计算Hessian矩阵。Krylov方法是用于执行各种操作的一组迭代技术,这些操作包括像近似求解矩阵的逆或者近似矩阵的特征值/特征向量等,而不使用矩阵-向量乘法以外的任何操作。

为了在Hesssian矩阵上使用Krylov方法,我们只需要能够计算Hessian矩阵H和一个任意向量ν间的乘积即可。实现这一目标的一种直观方法(Christianson,1992)是

该表达式中两个梯度的计算都可以由适当的软件库自动完成。注意,外部梯度表达式是内部梯度表达式的函数的梯度。

如果ν本身是由计算图产生的一个向量,那么重要的是指定自动微分软件不要对产生ν的图进行微分。

虽然计算Hessian通常是不可取的,但是可以使用Hessian向量积。可以对所有的i=1,...,n简单地计算He(i),其中e(i)是并且其他元素都为