Run this notebook online:\ |Binder| or Colab: |Colab| .. |Binder| image:: https://mybinder.org/badge_logo.svg :target: https://mybinder.org/v2/gh/deepjavalibrary/d2l-java/master?filepath=chapter_recurrent-neural-networks/bptt.ipynb .. |Colab| image:: https://colab.research.google.com/assets/colab-badge.svg :target: https://colab.research.google.com/github/deepjavalibrary/d2l-java/blob/colab/chapter_recurrent-neural-networks/bptt.ipynb .. _sec_bptt: 通过时间反向传播 ================ 到目前为止,我们已经多次提到 *梯度爆炸*\ 、 *消失梯度*\ , 以及需要对循环神经网络 *分离梯度*\ 。 例如, 在 :numref:`sec_rnn_scratch`\ 中 我们调用了序列上的 ``detach`` 函数。 为了能够快速构建模型并了解其工作原理,所有这些都没有得到充分的解释。 在本节中, 我们将更深入地研究一下 详细介绍了序列模型的反向传播,以及数学原理。 当我们首次实现循环神经网络(:numref:\ ``sec_rnn_scratch``)时,我们遇到了梯度爆炸的问题。 如果你做了练习题,就会发现梯度截断对于确保模型收敛至关重要。 为了更好地理解此问题,本节将回顾序列模型梯度的计算方式,它的工作原理没有什么新概念,毕竟我们使用的仍然是链式法则来计算梯度。 我们在 :numref:`sec_backprop`\ 中描述了多层感知机中的 前向与反向传播及相关的计算图。 循环神经网络中的前向传播是相对简单 易懂的。 *通过时间反向传播* (backpropagation through time,BPTT) :cite:`Werbos.1990` 实际上是循环神经网络中反向传播技术的一个特定应用。 它要求我们将循环神经网络的计算图每一步展开,以获得模型变量和参数之间的依赖关系。 然后, 根据链式法则, 我们将反向传播应用于计算和分析 存储梯度。 由于序列可能相当长,因此依赖关系可能相当长。 例如,对于1000个字符的序列, 第一个标记可能会对最终位置的标记产生重大影响。 这在计算上并不可行 (这需要太长的时间和太多的内存)并且需要1000多个矩阵积,我们才能达到非常难以捉摸的梯度。 这是一个充满计算和统计不确定性的过程。 下面我们将阐明发生了什么 以及如何在实践中解决这一问题。 .. _subsec_bptt_analysis: 循环神经网络的梯度分析 ---------------------- 我们从一个描述循环神经网络工作原理的简化模型开始, 此模型忽略有关隐藏状态的细节以及如何更新隐藏状态的详细信息。 这里的数学表示没有像过去那样明确地区分 标量、向量和矩阵, 因为这些细节对分析不重要, 只会使本小节中的符号变得混乱 在这个简化模型中, 我们将时间步\ :math:`t`\ 的隐状态表示为\ :math:`h_t`\ , 输入表示为\ :math:`x_t`\ ,输出表示为\ :math:`o_t`\ 。 回想一下我们在 :numref:`subsec_rnn_w_hidden_states`\ 中的讨论, 输入和隐藏状态 可以连接到 隐藏层中的一个权重变量相乘。 因此,我们分别使用 :math:`w_h` 和 :math:`w_o` 来 分别指示隐藏层和输出层的权重。 因此,每一步骤的隐藏状态和输出可以写为: .. math:: \begin{aligned}h_t &= f(x_t, h_{t-1}, w_h),\\o_t &= g(h_t, w_o),\end{aligned} :label: eq_bptt_ht_ot 其中,\ :math:`f`\ 和\ :math:`g`\ 分别是隐藏层和输出层的转换。 因此,我们有一个链 :math:`\{\ldots, (x_{t-1}, h_{t-1}, o_{t-1}), (x_{t}, h_{t}, o_t), \ldots\}`\ , 它们通过循环计算相互依赖。 正向传播相当简单。 我们所需要的就是一次一个时间步的遍历三元组 :math:`(x_t,h_t,o_t)` 。 然后通过一个目标函数在所有 :math:`T` 个时间步内 评估输出\ :math:`o_t`\ 和对应的标签\ :math:`y_t`\ 之间的差异: .. math:: L(x_1, \ldots, x_T, y_1, \ldots, y_T, w_h, w_o) = \frac{1}{T}\sum_{t=1}^T l(y_t, o_t). 对于反向传播,问题有点棘手,特别是当我们计算关于目标函数 :math:`L` 的参数 :math:`w_h` 的梯度时。具体来说,根据链式规则, .. math:: \begin{aligned}\frac{\partial L}{\partial w_h} & = \frac{1}{T}\sum_{t=1}^T \frac{\partial l(y_t, o_t)}{\partial w_h} \\& = \frac{1}{T}\sum_{t=1}^T \frac{\partial l(y_t, o_t)}{\partial o_t} \frac{\partial g(h_t, w_h)}{\partial h_t} \frac{\partial h_t}{\partial w_h}.\end{aligned} :label: eq_bptt_partial_L_wh 第一个和第二个因素 中的乘积:eqref:\ ``eq_bptt_partial_L_wh`` 很容易计算。 第三个因素 :math:`\partial h_t/\partial w_h` 这就是事情变得棘手的地方,因为我们需要反复计算参数 :math:`w_h` 对 :math:`h_t` 的影响。 根据递归计算 :eq:`eq_bptt_ht_ot`, :math:`h_t` 同时依赖于 :math:`h_{t-1}` 和 :math:`w_h`, 其中,\ :math:`h_{t-1}` 的计算 还取决于 :math:`w_h`\ 。 因此 使用链式法则可以得出 .. math:: \frac{\partial h_t}{\partial w_h}= \frac{\partial f(x_{t},h_{t-1},w_h)}{\partial w_h} +\frac{\partial f(x_{t},h_{t-1},w_h)}{\partial h_{t-1}} \frac{\partial h_{t-1}}{\partial w_h}. :label: eq_bptt_partial_ht_wh_recur 为了推导上面的梯度,假设我们有三个序列 :math:`\{a_{t}\},\{b_{t}\},\{c_{t}\}` 满足 :math:`a_{0}=0` and :math:`a_{t}=b_{t}+c_{t}a_{t-1}` for :math:`t=1, 2,\ldots`. 那么 :math:`t\geq 1`\ ,很容易展示 .. math:: a_{t}=b_{t}+\sum_{i=1}^{t-1}\left(\prod_{j=i+1}^{t}c_{j}\right)b_{i}. :label: eq_bptt_at 将 :math:`a_t`, :math:`b_t`, 和 :math:`c_t` 替换为 根据 .. math:: \begin{aligned}a_t &= \frac{\partial h_t}{\partial w_h},\\ b_t &= \frac{\partial f(x_{t},h_{t-1},w_h)}{\partial w_h}, \\ c_t &= \frac{\partial f(x_{t},h_{t-1},w_h)}{\partial h_{t-1}},\end{aligned} 网格中的梯度计算 :eq:`eq_bptt_partial_ht_wh_recur` 满足 :math:`a_{t}=b_{t}+c_{t}a_{t-1}`. 因此, per :eq:`eq_bptt_at`, 我们可以在 :eq:`eq_bptt_partial_ht_wh_recur` 中删除递归计算 具有 .. math:: \frac{\partial h_t}{\partial w_h}=\frac{\partial f(x_{t},h_{t-1},w_h)}{\partial w_h}+\sum_{i=1}^{t-1}\left(\prod_{j=i+1}^{t} \frac{\partial f(x_{j},h_{j-1},w_h)}{\partial h_{j-1}} \right) \frac{\partial f(x_{i},h_{i-1},w_h)}{\partial w_h}. :label: eq_bptt_partial_ht_wh_gen 虽然我们可以使用链规则递归地计算 :math:`\partial h_t/\partial w_h` 但只要 :math:`t` 很长,这个链就会变得很长。让我们讨论一些处理这个问题的策略。 完全计算 ~~~~~~~~ 显然,我们可以仅仅计算 :eq:`eq_bptt_partial_ht_wh_gen`\ 中的全部总和, 然而 这是非常缓慢的,并且可能会发生梯度爆炸, 因为初始条件的细微变化可能会对结果产生很大影响。 也就是说,我们可以看到类似于蝴蝶效应的情况,即初始条件的微小变化会导致结果的不相称变化。 就我们想要估计的模型而言,这实际上是非常不可取的。 毕竟,我们正在寻找能够很好地泛化高稳定性模型的估计器。因此,这种策略几乎从未在实践中使用过。 截断时间步骤 ~~~~~~~~~~~~ 或者, 我们可以把总和截断成整数 :eq:`eq_bptt_partial_ht_wh_gen` 在 :math:`\tau` 这一步之后 这就是我们到目前为止一直在讨论的问题, 例如,当我们在 :numref:`sec_rnn_scratch`\ 中分离梯度时。 这将导致真正梯度的\ *近似*\ ,只需在 :math:`\partial h_{t-\tau}/\partial w_h`. 实际上,这很有效。 这就是通常所说的随时间缩短的后向推进 :cite:`Jaeger.2002`. 其后果之一是,该模型主要关注短期影响,而非长期后果。 他的方法实际上是“可取的”,因为它使估计偏向于更简单、更稳定的模型。 随机截断 ~~~~~~~~ 最后,我们可以替换 :math:`\partial h_t/\partial w_h` 由一个期望值正确但截断序列的随机变量生成。 这是通过使用 :math:`\xi_t` 序列实现的 使用预定义的 :math:`0 \leq \pi_t \leq 1`, 其中 :math:`P(\xi_t = 0) = 1-\pi_t` 和 :math:`P(\xi_t = \pi_t^{-1}) = \pi_t`, 因此 :math:`E[\xi_t] = 1`. 我们用它来代替梯度 :math:`\partial h_t/\partial w_h` in :eq:`eq_bptt_partial_ht_wh_recur` 具有 .. math:: z_t= \frac{\partial f(x_{t},h_{t-1},w_h)}{\partial w_h} +\xi_t \frac{\partial f(x_{t},h_{t-1},w_h)}{\partial h_{t-1}} \frac{\partial h_{t-1}}{\partial w_h}. 根据 :math:`\xi_t` 的定义 :math:`E[z_t] = \partial h_t/\partial w_h`. 每当 :math:`\xi_t = 0` 循环计算 在当前时间步骤终止 :math:`t`. 这导致了不同长度序列的加权和,其中长序列很少,但适当地过重。 这个想法是由Tallec和Ollivier提出的 :cite:`Tallec.Ollivier.2017`. .. _fig_truncated_bptt: 比较策略 ~~~~~~~~ |比较RNN中计算梯度的策略。自上而下:随机截断、规则截断和完整计算。| :numref:`fig_truncated_bptt` 说明了使用循环神经网络的时间反向传播分析《时间机器》书中前几个字符的三种策略: - 第一行是随机截断,将文本划分为不同长度的段。 - 第二行是规则截断,将文本拆分为相同长度的子序列。这也是我们在循环神经网络实验中一直在做的。 - 第三行是通过时间的完全反向传播,结果是产生了在计算上不可行的表达式。 不幸的是,尽管在理论上很有吸引力,但随机截断并不比常规截断更好,这很可能是由于一些因素。 首先,在过去的许多反向传播步骤之后,观察的效果足以在实践中捕获依赖关系。 其次,增加的方差抵消了时间步数越多梯度越精确的事实。 第三,我们真正想要的是只有短范围交互的模型。 因此,模型需要的正是截断的通过时间反向传播方法所具备的轻度正则化效果。 .. _fig_rnn_bptt: 通过时间反向传播的细节 ---------------------- 在讨论了一般原则之后, 让我们详细讨论时间的反向传播。 与 :numref:`subsec_bptt_analysis` 分析不同, 在下面 我们将展示 如何计算 目标函数的梯度。 对于所有分解的模型参数。 为了让事情简单化,我们考虑一个没有偏置参数的循环神经网络, 其在隐藏层中的激活功能使用恒等映射 (:math:`\phi(x)=x`). 对于时间步骤 :math:`t`, 让单个示例输入和标签 :math:`\mathbf{x}_t \in \mathbb{R}^d` 和 :math:`y_t`, 分别的 隐藏状态 :math:`\mathbf{h}_t \in \mathbb{R}^h` 以及 :math:`\mathbf{o}_t \in \mathbb{R}^q` 计算如下: .. math:: \begin{aligned}\mathbf{h}_t &= \mathbf{W}_{hx} \mathbf{x}_t + \mathbf{W}_{hh} \mathbf{h}_{t-1},\\ \mathbf{o}_t &= \mathbf{W}_{qh} \mathbf{h}_{t},\end{aligned} 其中权重参数为\ :math:`\mathbf{W}_{hx} \in \mathbb{R}^{h \times d}`\ 、 :math:`\mathbf{W}_{hh} \in \mathbb{R}^{h \times h}`\ 和 :math:`\mathbf{W}_{qh} \in \mathbb{R}^{q \times h}`\ 。 用\ :math:`l(\mathbf{o}_t, y_t)`\ 表示时间步\ :math:`t`\ 处 (即从序列开始起的超过\ :math:`T`\ 个时间步)的损失函数, 则我们的目标函数的总体损失是: .. math:: L = \frac{1}{T} \sum_{t=1}^T l(\mathbf{o}_t, y_t). 为了在循环神经网络的计算过程中可视化模型变量和参数之间的依赖关系, 我们可以为模型绘制一个计算图, 如 :numref:`fig_rnn_bptt` 所示。 例如,计算时间步骤三的隐藏状态, :math:`\mathbf{h}_3`, 取决于模型参数 :math:`\mathbf{W}_{hx}` 和 :math:`\mathbf{W}_{hh}`\ , 上一时间步骤的隐藏状态 :math:`\mathbf{h}_2`, 以及当前时间步骤的输入 :math:`\mathbf{x}_3`. |显示具有三个时间步的RNN模型依赖关系的计算图。框表示变量(没有颜色色)或参数(有颜色),圆表示运算符。| 如前所述, :numref:`fig_rnn_bptt`\ 中的模型参数是 :math:`\mathbf{W}_{hx}`\ 、\ :math:`\mathbf{W}_{hh}`\ 和\ :math:`\mathbf{W}_{qh}`\ 。 通常,训练该模型需要对这些参数进行梯度计算: :math:`\partial L/\partial \mathbf{W}_{hx}`, :math:`\partial L/\partial \mathbf{W}_{hh}`\ ,和 :math:`\partial L/\partial \mathbf{W}_{qh}`\ 。 根据 :numref:`fig_rnn_bptt` 的依赖关系, 我们可以沿箭头的相反方向遍历计算图,依次计算和存储梯度。 灵活表达乘法 不同形状的矩阵、向量和标量 在链式规则中, 我们继续使用如 :numref:`sec_backprop`\ 中 所述的\ :math:`\text{prod}`\ 运算符。 首先, 目标函数的微分 关于模型输出 在任何时候步骤 :math:`t` 这相当简单: .. math:: \frac{\partial L}{\partial \mathbf{o}_t} = \frac{\partial l (\mathbf{o}_t, y_t)}{T \cdot \partial \mathbf{o}_t} \in \mathbb{R}^q. :label: eq_bptt_partial_L_ot 现在,我们可以计算目标函数的梯度 关于参数 :math:`\mathbf{W}_{qh}` 在输出层中: :math:`\partial L/\partial \mathbf{W}_{qh} \in \mathbb{R}^{q \times h}`\ 。 基于: :numref:`fig_rnn_bptt`\ , 目标函数 :math:`L` 通过 :math:`\mathbf{W}_{qh}` 依赖于 :math:`\mathbf{o}_1, \ldots, \mathbf{o}_T`. 使用链式法则可以得出 .. math:: \frac{\partial L}{\partial \mathbf{W}_{qh}} = \sum_{t=1}^T \text{prod}\left(\frac{\partial L}{\partial \mathbf{o}_t}, \frac{\partial \mathbf{o}_t}{\partial \mathbf{W}_{qh}}\right) = \sum_{t=1}^T \frac{\partial L}{\partial \mathbf{o}_t} \mathbf{h}_t^\top, 其中 :math:`\partial L/\partial \mathbf{o}_t` 由以下公式得出:eqref:\ ``eq_bptt_partial_L_ot``\ 。 接下来,如所示:numref:\ ``fig_rnn_bptt``, 在最后一个时间步骤 :math:`T` 目标函数 :math:`L` 依赖于隐藏状态 :math:`\mathbf{h}_T` 仅通过 :math:`\mathbf{o}_T`. 因此,我们很容易找到 梯度 :math:`\partial L/\partial \mathbf{h}_T \in \mathbb{R}^h` 使用链式规则: .. math:: \frac{\partial L}{\partial \mathbf{h}_T} = \text{prod}\left(\frac{\partial L}{\partial \mathbf{o}_T}, \frac{\partial \mathbf{o}_T}{\partial \mathbf{h}_T} \right) = \mathbf{W}_{qh}^\top \frac{\partial L}{\partial \mathbf{o}_T}. :label: eq_bptt_partial_L_hT_final_step 任何时间步骤都会变得更加棘手 :math:`t < T`, 其中目标函数 :math:`L` 依赖于 :math:`\mathbf{h}_t` 通过 :math:`\mathbf{h}_{t+1}` 和 :math:`\mathbf{o}_t`. 按照链式法则, 隐藏的梯度 :math:`\partial L/\partial \mathbf{h}_t \in \mathbb{R}^h` 在任何时候,步骤 :math:`t < T` 可重复计算为: .. math:: \frac{\partial L}{\partial \mathbf{h}_t} = \text{prod}\left(\frac{\partial L}{\partial \mathbf{h}_{t+1}}, \frac{\partial \mathbf{h}_{t+1}}{\partial \mathbf{h}_t} \right) + \text{prod}\left(\frac{\partial L}{\partial \mathbf{o}_t}, \frac{\partial \mathbf{o}_t}{\partial \mathbf{h}_t} \right) = \mathbf{W}_{hh}^\top \frac{\partial L}{\partial \mathbf{h}_{t+1}} + \mathbf{W}_{qh}^\top \frac{\partial L}{\partial \mathbf{o}_t}. :label: eq_bptt_partial_L_ht_recur 作为分析, 扩展递归计算 对于任何时间步骤 :math:`1 \leq t \leq T` 给出 .. math:: \frac{\partial L}{\partial \mathbf{h}_t}= \sum_{i=t}^T {\left(\mathbf{W}_{hh}^\top\right)}^{T-i} \mathbf{W}_{qh}^\top \frac{\partial L}{\partial \mathbf{o}_{T+t-i}}. :label: eq_bptt_partial_L_ht 我们可以从中看到:eqref:\ ``eq_bptt_partial_L_ht`` that 这是一个简单的线性例子 展示了长序列模型的一些关键问题:它可能涉及 :math:`\mathbf{W}_{hh}^\top`\ 的非常大的幂次。 其中,小于1的特征值消失 特征值大于1的偏离 这在数值上是不稳定的, 以消失的形式表现出来 和爆炸梯度。 解决这个问题的一种方法是截断时间步骤 如:numref:\ ``subsec_bptt_analysis``. 实际上,这种截断是通过在给定的时间步骤数后分离梯度来实现的。 过后 我们将看到更复杂的序列模型,如长-短期记忆,如何进一步缓解这种情况。 最后, :numref:`fig_rnn_bptt` 表明 目标函数 :math:`L` 取决于模型参数 :math:`\mathbf{W}_{hx}` and :math:`\mathbf{W}_{hh}` 在隐藏层中 通过隐藏状态 :math:`\mathbf{h}_1, \ldots, \mathbf{h}_T`. 计算梯度 关于这些参数 :math:`\partial L / \partial \mathbf{W}_{hx} \in \mathbb{R}^{h \times d}` 和 :math:`\partial L / \partial \mathbf{W}_{hh} \in \mathbb{R}^{h \times h}`, 我们应用链式法则,给出 .. math:: \begin{aligned} \frac{\partial L}{\partial \mathbf{W}_{hx}} &= \sum_{t=1}^T \text{prod}\left(\frac{\partial L}{\partial \mathbf{h}_t}, \frac{\partial \mathbf{h}_t}{\partial \mathbf{W}_{hx}}\right) = \sum_{t=1}^T \frac{\partial L}{\partial \mathbf{h}_t} \mathbf{x}_t^\top,\\ \frac{\partial L}{\partial \mathbf{W}_{hh}} &= \sum_{t=1}^T \text{prod}\left(\frac{\partial L}{\partial \mathbf{h}_t}, \frac{\partial \mathbf{h}_t}{\partial \mathbf{W}_{hh}}\right) = \sum_{t=1}^T \frac{\partial L}{\partial \mathbf{h}_t} \mathbf{h}_{t-1}^\top, \end{aligned} 哪里 :math:`\partial L/\partial \mathbf{h}_t` 由 :eq:`eq_bptt_partial_L_hT_final_step` 和 :eq:`eq_bptt_partial_L_ht_recur` 关键数量是多少 这会影响数值稳定性。 由于\ *通过时间反向传播* 是反向传播在循环神经网络中的应用, 正如我们在:numref:\ ``sec_backprop``\ , 训练循环神经网络。 交替向前传播 随时间的反向传播。 此外, *通过时间反向传播* 计算并存储上述梯度。 具体而言,存储的中间值会被重复使用,以避免重复计算, 比如储存 :math:`\partial L/\partial \mathbf{h}_t` 用于计算两者 :math:`\partial L / \partial \mathbf{W}_{hx}` and :math:`\partial L / \partial \mathbf{W}_{hh}`\ 。 总结 ---- - *通过时间反向传播*\ 仅仅适用于反向传播在具有隐状态的序列模型。 - 为了计算方便和数值稳定性,需要进行截断,如正则截断和随机截断。 - 矩阵的高次幂会导致特征值不同或突然消失。这表现为梯度爆炸或梯度消失的形式。 - 为了提高计算效率,在\ *通过时间反向传播*\ 计算期间期会缓存中间值。 练习 ---- 1. 假设我们有一个对称矩阵 :math:`\mathbf{M} \in \mathbb{R}^{n \times n}` 特征值为 :math:`\lambda_i` 其对应的特征向量为 :math:`\mathbf{v}_i` (:math:`i = 1, \ldots, n`). 在不丢失一般性的情况下,假设它们按 :math:`|\lambda_i| \geq |\lambda_{i+1}|` 顺序排列。 2. 证明 :math:`\mathbf{M}^k` 具有特征值 :math:`\lambda_i^k`. 3. 证明对于一个随机向量 :math:`\mathbf{x} \in \mathbb{R}^n`, 极有可能 :math:`\mathbf{M}^k \mathbf{x}` 将与特征向量非常一致 :math:`\mathbf{v}_1` 的 :math:`\mathbf{M}`\ 。把这句话正式化。 4. 上述结果对RNN中的梯度意味着什么? 5. 除了梯度裁剪外,你能想出其他方法来处理递归神经网络中的梯度爆炸吗? .. |比较RNN中计算梯度的策略。自上而下:随机截断、规则截断和完整计算。| image:: https://zh.d2l.ai/_images/truncated-bptt.svg .. |显示具有三个时间步的RNN模型依赖关系的计算图。框表示变量(没有颜色色)或参数(有颜色),圆表示运算符。| image:: https://zh.d2l.ai/_images/rnn-bptt.svg