在第 2 章中,我们回顾了微分的基础知识,并强调了两个线性映射的重要性:雅可比矩阵—向量乘积(JVP)及其伴随,即向量—雅可比矩阵乘积(VJP)。本章将借助这两个线性映射来回顾前向模式与反向模式自动微分。我们先从计算链入手,再推广到前馈网络与一般计算图。我们还会回顾检查点、可逆层与随机估计量。In Chapter 2, we reviewed the fundamentals of differentiation and stressed the importance of two linear maps: the Jacobian-vector product (JVP) and its adjoint, the vector-Jacobian product (VJP). In this chapter, we review forward-mode and reverse-mode autodiff using these two linear maps. We start with computation chains and then generalize to feedforward networks and general computation graphs. We also review checkpointing, reversible layers and randomized estimators.
首先,考虑一条计算链(4.1.1 节),它表示一个函数 ,该函数被表达为一串复合 ,其中 。对 的计算可以展开为一串操作To begin with, consider a computation chain (Section 4.1.1) representing a function expressed as a sequence of compositions , where . The computation of can be unrolled into a sequence of operations
| (8.1) |
我们的目标是计算 在给定输入 附近的变化。在前馈网络中,这相当于在参数固定时估计给定输入 的影响(我们将在后文看到如何估计关于参数 的变化)。Our goal is to compute the variations of around a given input . In a feedforward network, this amounts to estimating the influence of a given input for fixed parameters (we will see how to estimate the variations w.r.t. parameters in the sequel).
我们首先考虑完整雅可比 的计算,如记号 所示,它被视为一个矩阵。根据命题 2.2.11,我们有We first consider the computation of the full Jacobian , seen as a matrix, as the notation indicates. Following Proposition 2.2.11, we have
| (8.2) |
其中 是在 处计算得到的各中间函数的雅可比矩阵,如式 8.1 所定义。我们可能还想计算雅可比矩阵的转置where are the Jacobians of the intermediate functions computed at , as defined in Eq. 8.1. We may also want to compute the transpose of the Jacobian
在这两种情形中,该方法的主要缺点都在于计算方面:计算完整的 需要在内存中具体化各中间雅可比矩阵,并执行矩阵—矩阵乘积。然而在实践中,很少需要计算完整的雅可比矩阵。事实上,我们往往只需要右乘或左乘 。这分别引出了前向模式自动微分与反向模式自动微分。In both cases, the main drawback of this approach is computational: computing the full requires materializing the intermediate Jacobians in memory and performing matrix-matrix multiplications. However, in practice, computing the full Jacobian is rarely needed. Indeed, oftentimes, we only need to right-multiply or left-multiply with . This gives rise to forward-mode and reverse-mode autodiff, respectively.
现在我们把雅可比 解释为一个线性映射,正如非粗体的 所示。根据命题 2.3.10, 是各中间线性映射的组合,We now interpret the Jacobian as a linear map, as the non-bold indicates. Following Proposition 2.3.10, is the composition of the intermediate linear maps,
在输入方向 上求值 时,可以像式 8.1 中的函数本身那样分解为若干中间计算Evaluating on an input direction can be decomposed, like the function Eq. 8.1 itself, into intermediate computations
每个中间量 都相当于一个雅可比矩阵—向量乘积(JVP),并且可以与中间状态 的计算一同以前向方式执行。这也可以看作是用一个向量从右向左乘以式 8.2 所定义的矩阵。图 8.1 对此作了图示,算法 1 总结了该过程。Each intermediate amounts to a Jacobian-vector product (JVP) and can be performed in a forward manner, alongside the computation of the intermediate states . This can also be seen as multiplying the matrix defined in Eq. 8.2 with a vector, from right to left. This is illustrated in Fig. 8.1 and the procedure is summarized in Algorithm 1.
JVP 完全遵循 的计算过程,只是额外传播一个变量 。若认为计算 的代价大致与计算 相当,那么计算一个 JVP 的计算代价大致是 的两倍。更一般、更形式化的表述参见 8.3.3 节。The JVP follows exactly the computations of , with an additional variable being propagated. If we consider that computing is roughly as costly as computing , then computing a JVP has roughly twice the computational cost of . See Section 8.3.3 for a more general and more formal statement.
程序在给定求值步骤的内存占用,是指为确保执行完所有剩余步骤而需要存储在内存中的变量数量。程序的内存代价则是所有求值步骤中内存占用的最大值。就本书的目的而言,我们通过考察给定的程序来分析内存占用与内存代价。关于读、写、删除等内存操作及其相关内存代价的形式化定义,见 [87,第 4 章]。The memory usage of a program at a given evaluation step is the number of variables that need to be stored in memory to ensure the execution of all remaining steps. The memory cost of a program is then the maximal memory usage over all evaluation steps. For our purposes, we analyze the memory usage and memory cost by examining the given program. Formal definitions of operations on memory such as read, write, delete and associated memory costs are presented by [87, Chapter 4].
例如,为了执行链 ,在每一步 处,我们只需能够访问 ,即可执行程序的其余部分。在计算 时,我们可以从内存中删除 ,并以 取而代之。因此,与求值 相关的内存代价,仅仅是各 变量的最大维度。For example, to execute the chain , at each step , we only need to have access to to execute the rest of the program. As we compute , we can delete from memory and replace it by . Therefore, the memory cost associated with the evaluation of is just the maximal dimension of the variables.
对于前向模式自动微分,由于我们遵循 的计算过程,在每一步 处,我们只需能够访问 与 ,即可执行程序的其余部分。 与 所占用的内存,一旦 计算完成,就可以直接用于后者。与 JVP 相关的内存占用总结于图 8.2。总体而言,JVP 的内存代价正好是该函数本身内存代价的两倍。For forward mode autodiff, as we follow the computations of , at each step , we only need to have access to and to execute the rest of the program. The memory used by and can directly be used for once they are computed. The memory usage associated with the JVP is summarized in Fig. 8.2. Overall the memory cost of the JVP is then exactly twice the memory cost of the function itself.
在机器学习中,我们需要计算梯度的大多数函数都形如 ,其中 是标量值损失函数, 是一个网络。如命题 2.2.13 所示,梯度具有如下形式In machine learning, most functions whose gradient we need to compute take the form , where is a scalar-valued loss function and is a network. As seen in Proposition 2.2.13, the gradient takes the form
这正是要对 应用伴随 ,并且更一般地对任意输出方向 应用伴随的动机所在。由命题 2.3.11 可得This motivates the need for applying the adjoint to and more generally to any output direction . From Proposition 2.3.11, we have
在输出方向 上对 求值,可分解为Evaluating on an output direction is decomposed as
每个中间伴随 都对应一个向量—雅可比乘积(VJP)。它与前向模式的关键区别在于,该过程沿计算链反向进行,反向模式自动微分之名即由此而来。这也可视为对式 8.2 从左向右依次相乘。该过程如图 8.3 所示,并总结于算法 2。Each intermediate adjoint amounts to a vector-Jacobian product (VJP). The key difference with the forward mode is that the procedure runs backward through the chain, hence the name reverse mode autodiff. This can also be seen as multiplying Eq. 8.2 from left to right. The procedure is illustrated in Fig. 8.3 and summarized in Algorithm 2.
就运算次数而言,VJP 不过两次经过计算链:一次前向,一次反向。若认为中间 VJP 的代价与中间函数本身大致相当,则 VJP 的代价恰好是原函数代价的两倍,与 JVP 相同。更一般、更形式化的陈述见 8.3.3 节。In terms of number of operations, the VJP simply passes two times through the chain, once forward, then backward. If we consider the intermediate VJPs to be roughly as costly as the intermediate functions themselves, the VJP amounts to just twice the cost of the original function, just as the JVP. See Section 8.3.3 for a more generic and formal statement.
回想一下,程序在给定求值步骤的内存占用,是指为确保执行完剩余步骤而需存储在内存中的变量数量。考察算法 3,为执行所有反向步骤,即第 6 行的循环,我们需要能够访问全部中间输入 。因此,反向模式自动微分的内存代价与计算链的长度 成正比。图 8.4 展示了反向模式自动微分过程中的内存占用:它线性增长,直至前向传播结束,随后逐渐下降,直至输出函数值与 VJP。这一内存代价可借助 8.5 节介绍的检查点技术加以缓解。Recall that the memory usage of a program at a given evaluation step is the number of variables that need to be stored in memory to ensure the execution of the remaining steps. If we inspect Algorithm 3, to execute all backward steps, that is the loop in line 6, we need to have access to all the intermediate inputs . Therefore, the memory cost of reverse-mode autodiff is proportional to the length of the chain . Fig. 8.4 illustrates the memory usage during reverse mode autodiff. It grows linearly until the end of the forward pass and then progressively decreases until it outputs the value of the function and the VJP. The memory cost can be mitigated by means of checkpointing techniques presented in Section 8.5.
反向模式自动微分额外的内存开销也带来了一些优势。若我们需要对 个不同的输出方向 计算 ,只须计算并存储一次中间计算 ,再对反向传播进行 次调用即可。换言之,将中间计算 存入内存,我们便可实例化一个 VJP 算子,并通过反向传播将其作用于任意 。形式上,前向传播与反向传播可以解耦为The additional memory cost of reverse mode autodiff comes with some advantages. If we need to compute for different output directions , we only need to compute and store once the intermediate computations and then make calls to the backward pass. In other words, by storing in memory the intermediate computations , we may instantiate a VJP operator, which we may apply to any through the backward pass. Formally, the forward and backward passes can be decoupled as
其中where
在函数式编程的术语中,VJP 是一个闭包,因为它包含了中间计算结果 。若要将其用于多个方向 ,JVP 亦可同样处理。In functional programming terminology, the VJP is a closure, as it contains the intermediate computations . The same can be done for the JVP if we want to apply it to multiple directions .
本节分析前向模式与反向模式自动微分在计算计算链 的完整雅可比矩阵 时所需的时间与空间复杂度,其中 。我们假设 、 与 。于是有 与 。In this section, we analyze the time and space complexities of forward-mode and reverse-mode autodiff for computing the entire Jacobian matrix of a computation chain , where . We assume , and . Therefore, we have and .
根据定义 2.2.8,对每个 ,将雅可比矩阵与标准基向量 相乘,即可提取出该矩阵的第 列:Using Definition 2.2.8, we find that we can extract each column of the Jacobian matrix, for , by multiplying with the standard basis vector :
因此,计算完整雅可比矩阵所需的 JVP 个数为 ,且所用向量取自 。设链式组合中的每个 均具有形式 ,若将其视为矩阵,则 的大小为 。于是, 个 JVP 的计算代价为 。由于每处理完一层即可释放其对应的中间计算,内存代价为 。为简单起见设 ,并利用 ,可知计算 个 JVP 的计算代价——也就是以前向模式自动微分计算完整雅可比矩阵的代价——为 ,内存代价为 。若函数只有一个输入(),前向模式可一次算出整个雅可比矩阵,此时问题归结为单个方向导数。Computing the full Jacobian matrix therefore requires JVPs with vectors in . Assuming each in the chain composition has the form , seen as a matrix, has size . Therefore, the computational cost of JVPs is . The memory cost is , since we can release intermediate computations after each layer is processed. Setting for simplicity and using , we obtain that the computational cost of computing JVPs and therefore of computing the full Jacobian matrix by forward-mode autodiff is . The memory cost is . If a function has a single input (), then the forward mode computes the entire Jacobian at once, which reduces to a single directional derivative.
根据定义 2.2.8,对每个 ,将雅可比矩阵 与标准基向量 相乘,即可提取出该矩阵的每一行:Using Definition 2.2.8, we find that we can extract each row of the Jacobian matrix , for , by multiplying with the standard basis vector :
因此,计算完整雅可比矩阵需要执行 次 VJP,所用向量取自 。按前述假设,链式组合中的每个 均具有形式 ,于是 次 VJP 的计算代价为 。然而,内存代价为 ,因为我们需要为 层中的每一层存储中间计算结果。为简单起见,设 ,并利用 ,可得计算 次 VJP 的计算代价,亦即用反向模式自动微分计算完整雅可比矩阵的代价,为 ;内存代价则为 。若函数只有一个输出(),反向模式自动微分便可一次算出整个雅可比矩阵,此时问题归结为计算梯度。Computing the full Jacobian matrix therefore requires VJPs with vectors in . Assuming as before that each in the chain composition has the form , the computational cost of VJPs is . However, the memory cost is , as we need to store the intermediate computations for each of the layers. Setting for simplicity and using , we obtain that the computational cost of computing VJPs and therefore of computing the full Jacobian matrix by reverse-mode autodiff is . The memory cost is . If the function has a single output (), reverse-mode autodiff computes the entire Jacobian at once, which reduces to the gradient.
我们在表 8.1 中总结时间与空间复杂度。一般而言,若 ,则反向模式更具优势,代价是要占用一些内存;若 ,则前向模式更具优势。We summarize the time and space complexities in Table 8.1. Generally, if , reverse-mode is more advantageous at the price of some memory cost. If , forward mode is more advantageous.
| 前向模式Forward-mode | 反向模式Reverse-mode | |
|---|---|---|
| 时间Time | ||
| 空间Space |
上一节中,我们针对输入为 的计算链推导了前向模式自动微分与反向模式自动微分。本节中,我们转而推导前馈网络的反向模式自动微分;此时每一层 都允许显式依赖于一些额外参数 。其递推关系为In the previous section, we derived forward-mode autodiff and reverse-mode autodiff for computation chains with an input . In this section, we now derive reverse-mode autodiff for feedforward networks, in which each layer is now allowed to depend explicitly on some additional parameters . The recursion is
其中 ,且 。每个 现在都是两个自变量的函数:第一个自变量依赖于前一层,第二个自变量则不然。图 8.5 对此作了图示。下面说明如何对前馈网络求导。where and . Each is now a function of two arguments. The first argument depends on the previous layer, but the second argument does not. This is illustrated in Fig. 8.5. We now explain how to differentiate a feedforward network.
该函数的形式为 ,其中 、。由 2.3 节可知,VJP 的形式为 。因此,我们希望做到:对任意 ,都能计算 。The function has the form , where and . From Section 2.3, we know that the VJP has the form . Therefore, we want to be able to compute for any .
好在反向递推只需对计算链的情形稍加修改。事实上,由于 ,其中 、,故中间 VJP 的形式为 。由此得到递推式Fortunately, the backward recursion is only a slight modification of the computation chain case. Indeed, since , where and , the intermediate VJPs have the form . We therefore arrive at the recursion
最终输出为The final output is
我们常常把网络与一个损失函数复合起来We often compose a network with a loss function
由命题 2.3.11,梯度由下式给出From Proposition 2.3.11, the gradient is given by
其中where
且 。输出 (其中 )对应于关于 的梯度,通常并不需要,只有生成建模场合才会用到。完整流程总结于算法 3。with . The output , where , corresponds to the gradient w.r.t. and is typically not needed, except in generative modeling settings. The full procedure is summarized in Algorithm 3.
前几节回顾了计算链的自动微分及其向前馈网络的推广。本节回顾它在计算图上的推广,该推广已于 4.1.3 节引入。不失一般性,我们的形式化假设函数可以有多个输入,但只产生单个输出,因为总能把多个输出归并成一个元组。这样一来,输出变量 与函数 便一一对应。In the previous sections, we reviewed autodiff for computation chains and its extension to feedforward networks. In this section, we review its generalization to computation graphs, introduced in Section 4.1.3. Our formalism assumes, without loss of generality, that functions can take multiple inputs but only produce a single output, as we can always group multiple outputs as a tuple. This enables a one-to-one correspondence between output variables and functions .
前向模式即沿输入方向 计算程序的 JVP。对于计算链 ,每个 接受单个输入 ,每个 接受单个输入方向 。于是,从 出发,第 次迭代的前向传播为The forward mode corresponds to computing the JVP of the program in an input direction . In the case of a computation chain , each takes a single input and each takes a single input direction . The forward pass on iteration is then, starting from ,
对于由函数 按拓扑序给出的计算图,此时每个 都可以接受多个输入 ,而每个 所接受的输入方向 也与之数量相同,其中 是 的父节点。于是,第 步的前向传播会同时算出中间输入与中间方向:In the case of a computation graph, specified by functions in topological order, each may now take multiple inputs , and each takes as many input directions , where are the parents of . The forward pass on step then computes both intermediate inputs and directions as
回顾一下, 表示对 关于其第 个自变量求导。借助命题 2.3.12 中的扇入法则可得,导数按如下方式传播:Let us recall that means that we differentiate w.r.t. its th argument. Using the fan-in rule in Proposition 2.3.12, we obtain that the derivatives are propagated as
最终输出为 。由此得到的通用前向模式流程总结于图 8.7 与算法 4。直观地说,该算法就是在前向传播过程中逐个计算并累加中间 JVP。尽管文中并未明言,但当没有子节点再依赖节点 时,我们便可将 与 从内存中释放。The final output is . The resulting generic forward-mode procedure is summarized in Fig. 8.7 and in Algorithm 4. Intuitively, the algorithm consists in computing and summing intermediate JVPs along the forward pass. Although not explicitly mentioned, we can release and from memory when no child node depends on node .
反向模式指的是沿某个输出方向 计算程序的 VJP。在计算链 的情形下,每个中间变量 只被用到一次,即用于算出下一个变量 。因此,要计算该链的 VJP,只需把运算顺序颠倒过来,并使用相应的 VJP。由于每个函数只接受单个输入,每个 VJP 也只产生单个输出方向 。于是,一次反向传播便从 出发,自 一路算到 :The reverse mode corresponds to computing the VJP of the program in an output direction . In the case of a computation chain , each intermediate variable is used only once to compute the next variable . To compute the VJP of the chain, it then suffices to reverse the order of the operations and to use the corresponding VJPs. Since each function takes a single input, each VJP produces a single output direction . A backward pass therefore computes from to , starting from ,
最终输出为 。The final output is .
在计算图中,中间变量可能被多次使用,因为一个节点 可能有多个子节点。这使计算图的反转变得复杂。为规避这一问题,按照 Roy Frostig 的形式化表述 [141, 第6.2.2.2节],我们可以形式地区分输出 (即 的输出)与输入 (即 针对 的输入,后者是 的子节点)。我们通过引入一个仅复制 的操作来做到这一点,In computation graphs, intermediate variables may be used more than once, since a node may have several children nodes. This complicates reversing the computation graph. To circumvent this issue, following the formalism of Roy Frostig [141, Section 6.2.2.2], we may formally distinguish between the output of and the inputs of for , the children of . We do so by introducing an operation that simply duplicates ,
输出的元组长度为 。于是,对 ,前向传播可以形式化地改写为:The tuple output by is of length . The forward pass can then be formally rewritten as, for ,
其中 之后接where followed by
其中 。这种做法的好处在于,每个复制出的输出都只作为一次输入进入后续的子函数。得益于此,在反向传播中,我们便可以为每个变量 或 关联唯一对应的中间变量 或 。于是,反向模式只需将各函数 的 VJP 与 的 VJP 按相反顺序依次过一遍,即可写出。where . The benefit of this approach is that each duplicated output is input only once to the subsequent child functions. Thanks to this, we can now associate to each variable, or , a single corresponding intermediate variable, or , in the backward pass. The reverse mode can then simply be written by going through the VJPs of the functions and the VJP of in reverse order.
至于 运算的 VJP,我们记反向传播中与 相关联的中间变量为 。由命题 2.3.16 的扇出法则, 运算在第 次迭代的 VJP 为For the VJP of the operation, let us denote the intermediate variables in the backward pass associated with by . Following the fan-out rule in Proposition 2.3.16, the VJP of the operation on iteration is
最后一行用到了复制运算的定义 ,因此 且 。 的 VJP 恰好说明:若某个中间值 被后续若干函数 使用(其中 ),则关于 的导数须把经由这 个函数汇入变量 的所有变化求和。In the last line, we used that by definition of the duplication operation so and . The VJP of justifies why, if an intermediate value is used by later functions , for , the derivatives with respect to need to sum all the variations through the functions into the variable .
至于 各函数的 VJP,由命题 2.3.12 的扇入法则,VJP 会返回多个输出变化:For the VJP of the functions, following the fan-in rule in Proposition 2.3.12, the VJP returns multiple output variations,
其中 ;并且,对 ,有where , and where, for ,
整体形式化框架总结于图 8.8。The overall formalism is summarized in Fig. 8.8.
复制运算 不过是一种形式化手段,用于在数学上推导反向模式。实际实现中并不复制变量 ,而是在反向传播过程中多次访问它们。每当算出变量 时,便将 累加到 ,由此得到变量 。因此,对每个 ,我们都可以计算其 VJP 并执行原地更新:The duplication operation is just a formalism to mathematically derive the reverse mode. In practice, the variables are not duplicated but accessed several times during the backward pass. The variables are computed by accumulating into each time a variable is computed. Therefore, for each , we can compute the VJP and perform the in-place updates,
拓扑序保证了在到达 时, 已完成全部计算。由此得到的通用反向模式流程见算法 5。The topological ordering ensures that has been fully computed when we reach . The resulting generic reverse-mode procedure is presented in Algorithm 5.
在计算由计算图表示的函数 的梯度时,我们已经看到,反向模式比前向模式更为高效。如前所述,假设 DAG 中各基本函数 及其 VJP 的计算复杂度大致相当,那么 与 的计算复杂度也大致相当。这一事实至关重要,现代机器学习正是倚仗它才得以成立:它使我们能够通过梯度下降优化高维函数。For computing the gradient of a function represented by a computation graph, we saw that the reverse mode is more efficient than the forward mode. As we previously stated, assuming that the elementary functions in the DAG and their VJP have roughly the same computational complexity, then and have roughly the same computational complexity. This fact is crucial and is the pillar on which modern machine learning relies: it allows us to optimize high-dimensional functions by gradient descent.
对于 4.1.4 节回顾过的算术电路,这一关键事实在著名的 Baur-Strassen 定理 [14] 中得到了更精确的表述。若 是多项式,则其梯度 也是多项式。该定理以计算 的最佳电路规模为参照,给出了计算 的最佳电路规模的上界。For arithmetic circuits, reviewed in Section 4.1.4, this crucial fact is made more precise in the celebrated Baur-Strassen theorem [14]. If is a polynomial, then so is its gradient . The theorem gives an upper bound on the size of the best circuit for computing from the size of the best circuit for computing .
[138] 给出了一个借助反向归纳法的更简证明。另见 [44] 中定理 9.10 的证明。对于基本函数不止 与 的通用计算图,也可得到类似结果;例如参见 [29,定理 2]。A simpler proof by backward induction was given by [138]. See also the proof of Theorem 9.10 in [44]. For general computation graphs, that have more primitive functions than just and , a similar result can be obtained; see, e.g., [29, Theorem 2].
自动微分系统会实现一组基本函数或初等函数 ,它们经由函数复合,充当构造其他函数的构件。例如,我们已经看到,在算术电路(4.1.4 节)中有 。更一般地, 可以囊括表达程序所需的全部函数。不过我们强调, 未必局限于 log、exp 之类的底层函数,也可以包含层次更高的函数。例如,log-sum-exp 虽然可写成基本操作(log、sum、exp)的复合,却通常被单独列为基本函数:这既因为它是极为常用的构件,也因为数值稳定性的考虑。An autodiff system implements a set of primitive or elementary functions, which serve as building blocks for creating other functions, by function composition. For instance, we saw that in arithmetic circuits (Section 4.1.4), . More generally, may contain all the necessary functions for expressing programs. We emphasize, however, that is not necessarily restricted to low-level functions such as log and exp, but may also contain higher-level functions. For instance, even though the log-sum-exp can be expressed as the composition of elementary operations (log, sum, exp), it is usually included as a primitive on its own, both because it is a very commonly-used building block, but also for numerical stability reasons.
计算图中的每个函数 都属于一个集合 ,即该系统所支持的函数类。自动微分实现的一个理想性质是,集合 在函数复合下封闭,这意味着若 且 ,则 。这意味着复合函数本身可用于构造新的复合函数。这一性质对于支持高阶微分(第 9 章)和自动线性转置(8.4.4 节)也至关重要。当 是 中基本函数的复合时, 本身就是一个嵌套的 DAG。不过,我们总可以将每个复合函数内联,使 DAG 中的所有函数都属于 。Each function in a computation graph belongs to a set , the class of functions supported by the system. A desirable property of an autodiff implementation is that the set is closed under function composition, meaning that if and , then . This means that composed functions can themselves be used for composing new functions. This property is also crucial for supporting higher-order differentiation (Chapter 9) and automatic linear transposition (Section 8.4.4). When is a composition of elementary functions in , then itself is a nested DAG. However, we can always inline each composite function, such that all functions in the DAG belong to .
自动微分系统必须为每个 实现其 JVP 以支持前向模式,并实现其 VJP 以支持反向模式。我们给出几个示例,首先从线性函数的 JVP 与 VJP 开始。An autodiff system must implement for each its JVP for supporting the forward mode, and its VJP for supporting the reverse mode. We give a couple of examples. We start with the JVP and VJP of linear functions.
另一个简单的例子是逐元素可分离函数。Another simple example is element-wise separable functions.
乍看之下,若要同时支持前向模式与反向模式,似乎需要为每个基本操作 同时实现 JVP 与 VJP。幸运的是,存在一种从 JVP 得到 VJP 的方法,反之亦然。On first sight, if we want to support both forward and reverse modes, it appears like we need to implement both the JVP and the VJP for each primitive operation . Fortunately, there exists a way to recover VJPs from JVPs, and vice-versa.
如 2.3 节所示,若 是线性映射,则其 JVP 为 (与 无关)。反之,其 VJP 为 ,其中 是 的伴随算子(同样与 无关)。We saw in Section 2.3 that if is a linear map, then its JVP is (independent of ). Conversely, the VJP is , where is the adjoint operator of (again, independent of ).
我们定义 ,即 在输出方向 上的 VJP。由于 关于 是线性的,我们可以运用上述推理来计算其 VJPLet us define , i.e., the VJP of in the output direction . Since is linear in , we can apply the reasoning above to compute its VJP
它与 无关。换言之,VJP 的 VJP 正是对应的 JVP!这意味着,即使我们只能访问 VJP,也能实现前向模式自动微分。作为说明与合理性检验,我们给出下面的例子。which is independent of . In words, the VJP of a VJP is the corresponding JVP! This means that we can implement forward-mode autodiff even if we only have access to VJPs. As an illustration and sanity check, we give the following example.
我们已经看到,前向模式自动微分可以在计算过程中释放中间计算所占用的内存,而反向模式自动微分则需要缓存所有这些中间计算。这意味着,在标准形式下,反向模式自动微分的内存复杂度会随计算图中节点数量线性增长。规避这一问题的一种常用技术是检查点,它通过牺牲计算时间来换取更好的内存使用。检查点的工作方式是:只选择性地存储中间值的一个子集(称为检查点),并在需要时重新计算其余中间值。计算图中检查点位置的具体选择决定了内存与计算之间的权衡。虽然可以按用户指定的位置凭经验设置检查点,但检查点策略也可以按照算法来执行,[84, 87] 对此进行了深入研究。本节回顾两种分治算法:递归折半与动态规划。我们的阐述聚焦于计算链 ,为简单起见取 。We saw that forward-mode autodiff can release intermediate computations from memory along the way, while reverse-mode autodiff needs to cache all of them. This means that the memory complexity of reverse-mode autodiff, in its standard form, grows linearly with the number of nodes in the computation graph. A commonly-used technique to circumvent this issue is checkpointing, which trades-off computation time for better memory usage. Checkpointing works by selectively storing only a subset of the intermediate values, called checkpoints, and by recomputing others on the fly. The specific choice of the checkpoint locations in the computation graph determines the memory-computation trade-off. While it is possible to heuristically set checkpoints at user-specified locations, it is also possible to perform a checkpointing strategy algorithmically, as studied in depth by [84, 87]. In this section, we review two divide-and-conquer algorithms: recursive halving and dynamic programming. Our exposition focuses on computation chains , with for simplicity.
设 为对各个函数 的调用次数(我们忽略计算中间 VJP 的代价), 为对链 执行反向模式自动微分时缓存的函数输入个数。在一个极端,如果像算法 2 那样存储所有中间计算,以仅计算 VJP ,则我们有Let be the number of calls to the individual functions (we ignore the cost of computing the intermediate VJPs) and be the number of function inputs cached, when performing reverse-mode autodiff on a chain . On one extreme, if we store all intermediate computations, as done in Algorithm 2, to compute only the VJP , we have
这在计算复杂度上是最优的,但在内存上则不是最优的。在另一个极端,如果像算法 6 那样只存储初始输入,则我们有This is optimal w.r.t. computational complexity, but suboptimal w.r.t. memory. On the other extreme, if we only store the initial input, as done in Algorithm 6, then we have
这在内存上是最优的,但会导致计算复杂度关于 呈二次增长。This is optimal w.r.t. memory but leads to a computational complexity that is quadratic in .
作为在计算与内存之间获得更好权衡的第一步,我们可以将链 拆分为As a first step towards obtaining a better computation-memory trade-off, we may split the chain as
其中 为偶数。于是,不同于像算法 6 那样从输入 重新计算所有中间计算 ,我们可以存储 ,并从 出发对 重新计算 。形式化地说,该策略对应于以下步骤。for even. Then, rather than recomputing all intermediate computations from the input as in Algorithm 6, we can store and recompute for starting from . Formally, this strategy amounts to the following steps.
计算 Compute
计算 Compute
计算 Compute
代价是需要额外存储检查点 ;但与算法 6 相比,这已把计算复杂度大致减半。At the expense of having to store the additional checkpoint , this already roughly halves the computational complexity compared to Algorithm 6.
随后便可递归地应用这一推理,算法 7 即其形式化表述。该算法称为递归二分调度 [85],图 8.11 给出了图示。就函数求值次数 而言,当 为偶数时,我们进行 次函数调用,并将该过程递归调用两次,即,We can then apply this reasoning recursively, as formalized in Algorithm 7. The algorithm is known as recursive binary schedule [85] and illustrated in Fig. 8.11. In terms of number of function evaluations , for even, we make function calls, and we call the procedure recursively twice, that is,
若链的长度为 ,我们直接使用 VJP,因此 。于是,若 是 的幂,函数调用次数为If the chain is of length , we directly use the VJP, so . Hence, the number of function calls, if is a power of , is
就内存占用而言,算法 7 不仅在第 6 行使用 ,在第 8 行也同样使用。因此,当算法在第 7 行对链的后半部分进行递归调用时,一个内存槽被 占用。该行被递归调用,直至链缩减为单个函数。此时,所使用的内存槽总数等于我们将函数折半的次数,即当 是 的幂时为 。另一方面,输入 在算法 7 的第 8 行之后不再被使用。在该行, 所占用的内存槽可由对前半部分的递归调用使用。换言之,对前半部分递归调用算法不会带来额外的内存开销。因此,若 是 的幂,算法 7 的内存开销为In terms of memory usage, Algorithm 7 uses not only at line 6 but also at line 8. So when the algorithm is called recursively on the second half of the chain at line 7, one memory slot is taken by . This line is called recursively until the chain is reduced to a single function. At that point, the total number of memory slots used is equal to the number of times we split the function in half, that is, for a power of . On the other hand, the input is no longer used after line 8 of Algorithm 7. At that line, the memory slot taken by can be consumed by the recursive call on the first-half. In other words, calling the algorithm recursively on the first half does not incur extra memory cost. So if is a power of , the memory cost of Algorithm 7 is
对于长度为 的链,递归折半需要 个内存槽。然而,如图 8.11 所示,在给定时间步,并非所有内存槽都会被利用。Recursive halving requires memory slots for a chain of length . However, as illustrated in Fig. 8.11, at a given time step, all memory slots may not be exploited.
为改进这一方法,我们注意到递归折半不过是“将链拆分、再对每一部分递归调用自身”这类程序的一个特例。换言之,它属于分治算法。与其把链对半拆开,不如考虑在某个索引 处将其拆分:一部分通过一次递归调用来反转从 到 的计算,该调用会占用一个内存槽;另一部分同样通过递归调用来反转从 到 的计算,而这次调用无需额外内存槽,因为它可以直接复用原始输入 所占用的内存槽。要把链拆成这两部分,需要 次中间计算,以从 推进到 。于是,对于长度为 、拥有 个内存槽的链,其计算复杂度 (以函数求值次数计)满足递推式To optimize the approach, we observe that recursive halving is just one instance of a program that splits the chain and calls itself recursively on each part. In other words, it is a form of divide-and-conquer algorithm. Rather than splitting the chain in half, we may consider splitting the chain at some index . One split is used to reverse the computations from to by a recursive call that consumes one memory slot. The other split is used on a recursive call that reverses the computations from to . That second call does not require an additional memory slot, as it can use directly the memory slot used by the original input . To split the chain in such two parts, we need intermediate computations to go from to . The computational complexity , counted as the number of function evaluations, for a chain of length with memory slots then satisfies the recurrence
对一切 成立。只需取 ,得到的结果恰好就是递归折半的计算复杂度。为进一步优化后者,可以选取合适的 来拆分链,使复杂度最小。一个最优方案必须满足如下递推方程:for all . By simply taking , we recover exactly the computational complexity of recursive halving. To refine the latter, we may split the chain by selecting to minimize the complexity. An optimal scheme must satisfy the recursive equation,
| (8.3) |
注意到对 、, 可由 算出。这提示可以采用动态规划方法,通过算法寻找最优方案。对于长度为 的链,由于可以直接反转计算,代价为零,故 。而对于内存 ,只存在一种可行方案,即如算法 6 那样仅保存初始输入,故 。随后可借助式 8.3,从 到 、以及从 到 逐步递推计算各值 。在此过程中,可将最优拆分记录如下:Note that can be computed from for , . This suggests a dynamic programming approach to find an optimal scheme algorithmically. For a chain of length , the cost is null as we directly reverse the computation, so . On the other hand for a memory , there is only one possible scheme that saves only the initial input as in Algorithm 6, so . The values can then be computed incrementally from to and to using Eq. 8.3. The optimal splits can be recorded along the way as
随后,通过沿分别对应 与 的两条分支回溯最优拆分,即可求得 的最优拆分。由于最终输出表现为遍历一棵二叉树,该方案被称为 treeverse [84]。值得注意的是,这一动态规划流程具有通用性,原则上可以纳入中间函数 的不同计算代价。The optimal split for can then be found by backtracking the optimal splits along both branches corresponding to and . As the final output consists in traversing a binary tree, it was called treeverse [84]. Note that the dynamic programming procedure is generic and could a priori incorporate varying computational costs for the intermediate functions .
结果表明,我们也可以解析地找到一种最优方案。该方案由 [84] 提出,依据的是 [88] 关于分治算法如何最优地反转顺序程序的分析;简明的证明亦可参见 [85,第 6 节]。其主要思路在于考察某个求值步骤 被重复的次数。当我们在 处将计算链拆分时,从 到 的所有步骤都至少会被重复一次。换言之,处理计算链的后半部分会带来一次内存代价,而处理其前半部分则会带来一次重复代价。[84] 表明,对于固定的 、,我们可以解析地求出最小重复次数,并借助关于最优拆分的简单公式构造出相应的方案。It turns out that we can also find an optimal scheme analytically. This scheme was found by [84], following the analysis of optimal inversions of sequential programs by divide-and-conquer algorithms done by [88]; see also [85, Section 6] for a simple proof. The main idea consists in considering the number of times an evaluation step is repeated. As we split the chain at , all steps from to will be repeated at least once. In other words, treating the second half of the chain incurs one memory cost, while treating the first half of the chain incurs one repetition cost. [84] shows that for fixed , , we can find the minimal number of repetitions analytically and build the corresponding scheme with simple formulas for the optimal splits.
与动态规划方法相比,这意味着我们无需计算指针 ,并且可以用一个简单的公式来设定 。在给定 与 的情况下,我们仍需遍历相应的二叉树以获得调度方案。需要注意的是,这种最优方案并未考虑各函数 的计算代价可能不同。Compared to the dynamic programming approach, it means that we do not need to compute the pointers , and we can use a simple formula to set . We still need to traverse the corresponding binary tree given and to obtain the schedules. Note that such an optimal scheme does not take into account varying computational costs for the functions .
上文给出的最优方案要求事先知道计算图中的节点总数。然而,当通过例如 while 循环(5.10 节)进行求导时,情况并非如此。为规避这一问题,研究者提出了在线检查点方案,并证明其近乎最优 [188, 207]。这类方案先以前 次计算定义一组共 个检查点,随后随着计算的持续进行动态地重写这些检查点。一旦计算终止,就对所记录的检查点集合应用上文针对固定长度给出的最优方法。The optimal scheme presented above requires knowing the total number of nodes in the computation graph ahead of time. However, when differentiating through for example a while loop (Section 5.10), this is not the case. To circumvent this issue, online checkpointing schemes have been developed and proven to be nearly optimal [188, 207]. These schemes start by defining a set of checkpoints with the first computations, then these checkpoints are rewritten dynamically as the computations keep going. Once the computations terminate, the optimal approach presented above for a fixed length is applied on the set of checkpoints recorded.
当函数 可逆(即 存在)且 易于获取时,反向模式自动微分的内存需求可以被完全消除。在这种情况下,我们无需存储计算 VJP 所必需的中间计算结果 ,而是在反向传播过程中利用 从 即时计算得到。算法 8 针对计算链的情形总结了这一流程。与算法 2 中朴素的反向模式自动微分相比,该算法具有最优的内存复杂度,因为我们可以随计算推进及时释放 与 。The memory requirements of reverse-mode autodiff can be completely alleviated when the functions are invertible (meaning that exists) and when is easily accessible. In that case, rather than storing the intermediate computations , necessary to compute the VJP , one can compute them on the fly during the backward pass from using . We summarize the procedure for the case of computation chains in Algorithm 8. Compared to vanilla reverse-mode autodiff in Algorithm 2, the algorithm has optimal memory complexity, as we can release and as we go.
实践中,往往并不存在,或者不易获取。但是,可以把网络架构设计得易于求逆。这方面的例子包括可逆残差网络 [81]、正交归一 RNN [92]、神经常微分方程(12.6 节)以及动量残差神经网络 [174];亦可参见其中所引文献。In practice, often does not exist or may not be easily accessible. However, network architectures can be constructed to be easily invertible by design. Examples include reversible residual networks [81], orthonormal RNNs [92], neural ODEs (Section 12.6), and momentum residual neural networks [174]; see also references therein.
当每个 的 JVP 都是正交归一的线性映射时,即When the JVP of each is an orthonormal linear mapping, i.e.,
容易验证, 的 VJP 就等于 的 JVP,即it is easy to check that the VJP of is equal to the JVP of , that is
换言之,当 JVP 正交归一之时, 的反向模式自动微分与 的前向模式自动微分彼此一致!In other words, in the case of orthonormal JVPs, reverse-mode autodiff of coincides with forward-mode autodiff of !
前向模式自动微分无需存储中间激活值。然而,对于函数 ,若用前向模式自动微分计算梯度 ,就需要 个 JVP;当 很大时,这难以处理。能否用更少的 JVP 来近似 ?下面的命题给出了 的一个无偏估计量,其中只涉及 JVP。Forward-mode autodiff does not require storing intermediate activations. However, for a function , computing the gradient using forward-mode autodiff requires JVPs, which is intractable if is large. Can we approximate with fewer JVPs? The following proposition gives an unbiased estimator of that only involves JVPs.
例如,[17] 就采用了这一估计量。它可以看作扰动函数梯度的零温度极限,由得分函数估计量(SFE)估计得到;参见 14.4.6 节。This estimator is for instance used by [17]. It can be seen as the zero-temperature limit of the gradient of a perturbed function, estimated by the score-function estimator (SFE); see Section 14.4.6.
实践中,上式中的期望可通过抽取 个噪声向量 ,并对 关于 求平均来近似。In practice, the expectation above can be approximated by drawing noise vectors , and averaging over .
有一点需要提醒:这一估计量固然有用,例如当我们出于内存方面的考虑不愿存储中间激活值时;但这样做的代价是方差增大,而方差会影响 SGD 的收敛速率,如 16.2 节所述。A word of caution: while this estimator can be useful for example when we do not want to store the intermediate activations for memory reasons, this of course comes at the cost of increasing the variance, which influences the convergence rate of SGD, as seen in Section 16.2.
计算机程序可以视为有向无环图:节点对应程序中各中间运算的输出,边则表示当前运算对先前运算的依赖关系。Computer programs can be seen as directed acyclic graphs, where nodes correspond to the output of intermediate operations in the program, and edges represent the dependencies of current operations on past operations.
函数 的自动微分(autodiff)主要有两种模式:前向模式与反向模式。Automatic differentiation (autodiff) for a function has two main modes: forward mode and reverse mode.
前向模式:i)使用 JVP;ii)每次构造雅可比矩阵的一列;iii)对高瘦的雅可比矩阵()高效;iv)无需存储中间计算。The forward mode: i) uses JVPs, ii) builds the Jacobian one column at a time, iii) is efficient for tall Jacobians (), iv) need not store intermediate computations.
反向模式:i)使用 VJP;ii)每次构造雅可比矩阵的一行;iii)对宽雅可比矩阵()高效;iv)需要存储中间计算,才能达到计算上的最优性。The reverse mode: i) uses VJPs, ii) builds the Jacobian one row at a time, iii) is efficient for wide Jacobians (), iv) needs to store intermediate computations, in order to be computationally optimal.
为了以计算效率换取更好的内存效率,我们可以使用检查点技术。To trade computational efficiency for better memory efficiency, we can use checkpointing techniques.
使用反向模式计算函数 的梯度的复杂度,至多比求值该函数本身大一个常数倍。这就是算术电路中的 Baur-Strassen 定理。这一惊人的结果是现代机器学习的支柱之一。The complexity of computing the gradient of a function using the reverse mode is at most a constant time bigger than that of evaluating the function itself. This is the Baur-Strassen theorem, in arithmetic circuits. This astonishing result is one of the pillars of modern machine learning.