5.3. 正向传播、反向传播和计算图
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 SageMaker Studio Lab 中打开 Notebook

到目前为止,我们只基于小批量随机梯度下降来训练模型。然而,在实现该算法时,我们只考虑了通过模型进行*正向传播*(forward propagation)所涉及的计算。当需要计算梯度时,我们只是调用了深度学习框架提供的反向传播函数。

梯度的自动计算(自动微分)大大简化了深度学习算法的实现。在自动微分之前,即使是对复杂模型的微小调整也需要手工重新计算复杂的导数。令人惊讶的是,学术论文经常不得不分配大量页面来推导更新规则。虽然我们必须继续依赖自动微分,以便我们可以专注于有趣的部分,但如果你想超越对深度学习的浅显理解,你应该知道这些梯度是如何在底层计算的。

在本节中,我们将深入探讨*反向传播*(backward propagation,通常称为*backpropagation*)的细节。为了传达对这些技术及其实施的一些见解,我们依赖一些基本的数学和计算图。首先,我们将重点放在一个带权重衰减(weight decay)(\(\ell_2\) 正则化,将在后续章节中介绍)的单隐藏层多层感知机上。

5.3.1. 正向传播

正向传播(或前向传播)指的是,按顺序(从输入层到输出层)计算和存储神经网络中每层的结果(包括输出)。我们现在将逐步研究一个单隐藏层神经网络的机理。这可能看起来很乏味,但用放克音乐大师詹姆斯·布朗(James Brown)永恒的话来说,你必须“付出代价才能当老大”。

为简单起见,我们假设输入样本为 \(\mathbf{x}\in \mathbb{R}^d\),并且我们的隐藏层不包括偏置项。这里的中间变量是

(5.3.1)\[\mathbf{z}= \mathbf{W}^{(1)} \mathbf{x},\]

其中 \(\mathbf{W}^{(1)} \in \mathbb{R}^{h \times d}\) 是隐藏层的权重参数。在将中间变量 \(\mathbf{z}\in \mathbb{R}^h\) 通过激活函数 \(\phi\) 后,我们得到长度为 \(h\) 的隐藏激活向量

(5.3.2)\[\mathbf{h}= \phi (\mathbf{z}).\]

隐藏层输出 \(\mathbf{h}\) 也是一个中间变量。假设输出层的参数只有权重 \(\mathbf{W}^{(2)} \in \mathbb{R}^{q \times h}\),我们可以得到一个长度为 \(q\) 的输出层变量

(5.3.3)\[\mathbf{o}= \mathbf{W}^{(2)} \mathbf{h}.\]

假设损失函数为 \(l\),样本标签为 \(y\),我们可以计算单个数据样本的损失项,

(5.3.4)\[L = l(\mathbf{o}, y).\]

根据我们稍后将介绍的 \(\ell_2\) 正则化的定义,给定超参数 \(\lambda\),正则化项为

(5.3.5)\[s = \frac{\lambda}{2} \left(\|\mathbf{W}^{(1)}\|_\textrm{F}^2 + \|\mathbf{W}^{(2)}\|_\textrm{F}^2\right),\]

其中矩阵的弗罗贝尼乌斯范数(Frobenius norm)只是将矩阵展平为向量后应用的 \(\ell_2\) 范数。最后,模型在给定数据样本上的正则化损失为

(5.3.6)\[J = L + s.\]

在下面的讨论中,我们将 \(J\) 称为*目标函数*。

5.3.2. 正向传播的计算图

绘制*计算图*有助于我们可视化计算中运算符和变量的依赖关系。 图 5.3.1 包含了与上述简单网络相关的图,其中正方形表示变量,圆圈表示运算符。左下角表示输入,右上角表示输出。请注意,箭头(表示数据流)的方向主要是向右和向上。

../_images/forward.svg

图 5.3.1 正向传播的计算图。

5.3.3. 反向传播

反向传播指的是计算神经网络参数梯度的方法。简而言之,该方法根据微积分中的*链式法则*,以相反的顺序(从输出到输入层)遍历网络。该算法在计算某些参数的梯度时,会存储所需的任何中间变量(偏导数)。假设我们有函数 \(\mathsf{Y}=f(\mathsf{X})\)\(\mathsf{Z}=g(\mathsf{Y})\),其中输入和输出 \(\mathsf{X}, \mathsf{Y}, \mathsf{Z}\) 是任意形状的张量。通过使用链式法则,我们可以计算 \(\mathsf{Z}\) 关于 \(\mathsf{X}\) 的导数

(5.3.7)\[\frac{\partial \mathsf{Z}}{\partial \mathsf{X}} = \textrm{prod}\left(\frac{\partial \mathsf{Z}}{\partial \mathsf{Y}}, \frac{\partial \mathsf{Y}}{\partial \mathsf{X}}\right).\]

这里我们使用 \(\textrm{prod}\) 运算符在执行必要的操作(如转置和交换输入位置)后将其参数相乘。对于向量,这很简单:它只是矩阵-矩阵乘法。对于更高维的张量,我们使用适当的对应项。运算符 \(\textrm{prod}\) 隐藏了所有符号上的开销。

回想一下,在 图 5.3.1 中,其计算图的单隐藏层简单网络的参数是 \(\mathbf{W}^{(1)}\)\(\mathbf{W}^{(2)}\)。反向传播的目标是计算梯度 \(\partial J/\partial \mathbf{W}^{(1)}\)\(\partial J/\partial \mathbf{W}^{(2)}\)。为此,我们应用链式法则,依次计算每个中间变量和参数的梯度。计算顺序与正向传播中执行的顺序相反,因为我们需要从计算图的结果开始,然后朝着参数的方向进行。第一步是计算目标函数 \(J=L+s\) 关于损失项 \(L\) 和正则化项 \(s\) 的梯度

(5.3.8)\[\frac{\partial J}{\partial L} = 1 \; \textrm{和} \; \frac{\partial J}{\partial s} = 1.\]

接下来,我们根据链式法则计算目标函数关于输出层变量 \(\mathbf{o}\) 的梯度

(5.3.9)\[\frac{\partial J}{\partial \mathbf{o}} = \textrm{prod}\left(\frac{\partial J}{\partial L}, \frac{\partial L}{\partial \mathbf{o}}\right) = \frac{\partial L}{\partial \mathbf{o}} \in \mathbb{R}^q.\]

接下来,我们计算正则化项关于两个参数的梯度

(5.3.10)\[\frac{\partial s}{\partial \mathbf{W}^{(1)}} = \lambda \mathbf{W}^{(1)} \; \textrm{和} \; \frac{\partial s}{\partial \mathbf{W}^{(2)}} = \lambda \mathbf{W}^{(2)}.\]

现在我们能够计算模型最接近输出层的参数的梯度 \(\partial J/\partial \mathbf{W}^{(2)} \in \mathbb{R}^{q \times h}\)。使用链式法则可以得到

(5.3.11)\[\frac{\partial J}{\partial \mathbf{W}^{(2)}}= \textrm{prod}\left(\frac{\partial J}{\partial \mathbf{o}}, \frac{\partial \mathbf{o}}{\partial \mathbf{W}^{(2)}}\right) + \textrm{prod}\left(\frac{\partial J}{\partial s}, \frac{\partial s}{\partial \mathbf{W}^{(2)}}\right)= \frac{\partial J}{\partial \mathbf{o}} \mathbf{h}^\top + \lambda \mathbf{W}^{(2)}.\]

为了获得关于 \(\mathbf{W}^{(1)}\) 的梯度,我们需要继续沿着输出层向隐藏层反向传播。关于隐藏层输出的梯度 \(\partial J/\partial \mathbf{h} \in \mathbb{R}^h\) 由下式给出

(5.3.12)\[\frac{\partial J}{\partial \mathbf{h}} = \textrm{prod}\left(\frac{\partial J}{\partial \mathbf{o}}, \frac{\partial \mathbf{o}}{\partial \mathbf{h}}\right) = {\mathbf{W}^{(2)}}^\top \frac{\partial J}{\partial \mathbf{o}}.\]

由于激活函数 \(\phi\) 是按元素应用的,因此计算中间变量 \(\mathbf{z}\) 的梯度 \(\partial J/\partial \mathbf{z} \in \mathbb{R}^h\) 需要我们使用按元素乘法运算符,我们用 \(\odot\) 表示

(5.3.13)\[\frac{\partial J}{\partial \mathbf{z}} = \textrm{prod}\left(\frac{\partial J}{\partial \mathbf{h}}, \frac{\partial \mathbf{h}}{\partial \mathbf{z}}\right) = \frac{\partial J}{\partial \mathbf{h}} \odot \phi'\left(\mathbf{z}\right).\]

最后,我们可以得到模型最接近输入层的参数的梯度 \(\partial J/\partial \mathbf{W}^{(1)} \in \mathbb{R}^{h \times d}\)。根据链式法则,我们得到

(5.3.14)\[\frac{\partial J}{\partial \mathbf{W}^{(1)}} = \textrm{prod}\left(\frac{\partial J}{\partial \mathbf{z}}, \frac{\partial \mathbf{z}}{\partial \mathbf{W}^{(1)}}\right) + \textrm{prod}\left(\frac{\partial J}{\partial s}, \frac{\partial s}{\partial \mathbf{W}^{(1)}}\right) = \frac{\partial J}{\partial \mathbf{z}} \mathbf{x}^\top + \lambda \mathbf{W}^{(1)}.\]

5.3.4. 训练神经网络

在训练神经网络时,正向传播和反向传播相互依赖。特别是,对于正向传播,我们沿着依赖关系的方向遍历计算图并计算其路径上的所有变量。然后将这些变量用于反向传播,其中图上的计算顺序是相反的。

以上述简单网络为例。一方面,在正向传播期间计算正则化项 (5.3.5) 取决于模型参数 \(\mathbf{W}^{(1)}\)\(\mathbf{W}^{(2)}\) 的当前值。它们由优化算法根据最近一次迭代中的反向传播给出。另一方面,在反向传播期间对参数 (5.3.11) 的梯度计算取决于隐藏层输出 \(\mathbf{h}\) 的当前值,该值由正向传播给出。

因此,在训练神经网络时,一旦模型参数被初始化,我们交替进行正向传播和反向传播,使用反向传播给出的梯度来更新模型参数。请注意,反向传播会重用正向传播中存储的中间值以避免重复计算。其结果之一是我们需要保留中间值直到反向传播完成。这也是为什么训练比纯预测需要更多内存的原因之一。此外,这些中间值的大小大致与网络层数和批量大小成正比。因此,使用更大的批量大小训练更深的网络更容易导致*内存不足*(out-of-memory)错误。

5.3.5. 小结

正向传播按顺序计算并存储神经网络定义的计算图中的中间变量。它从输入层到输出层进行。反向传播按相反的顺序计算并存储神经网络中中间变量和参数的梯度。在训练深度学习模型时,正向传播和反向传播是相互依赖的,并且训练比预测需要更多的内存。

5.3.6. 练习

  1. 假设某些标量函数 \(f\) 的输入 \(\mathbf{X}\)\(n \times m\) 矩阵。 \(f\) 关于 \(\mathbf{X}\) 的梯度的维度是多少?

  2. 为本节中描述的模型的隐藏层添加一个偏置项(你不需要在正则化项中包含偏置)。

    1. 画出相应的计算图。

    2. 推导正向和反向传播方程。

  3. 计算本节中描述的模型进行训练和预测时的内存占用。

  4. 假设你想计算二阶导数。计算图会发生什么?你预计计算需要多长时间?

  5. 假设计算图对于你的 GPU 来说太大了。

    1. 你能否将其分区到多个 GPU 上?

    2. 与在较小的小批量上进行训练相比,有哪些优点和缺点?

讨论