循环神经网络与随时间后向传播:参考答案

目录

\[ \begin{align}\begin{aligned}\newcommand{\ba}{\boldsymbol{a}} \newcommand{\bb}{\boldsymbol{b}} \newcommand{\be}{\boldsymbol{e}} \newcommand{\bq}{\boldsymbol{q}} \newcommand{\bk}{\boldsymbol{k}} \newcommand{\bw}{\boldsymbol{w}} \newcommand{\bx}{\boldsymbol{x}} \newcommand{\by}{\boldsymbol{y}} \newcommand{\bz}{\boldsymbol{z}} \newcommand{\bd}{\boldsymbol{d}} \newcommand{\bv}{\boldsymbol{v}} \newcommand{\bs}{\boldsymbol{s}}\\\newcommand{\btheta}{\boldsymbol{\theta}} \newcommand{\bbeta}{\boldsymbol{\beta}} \newcommand{\bgamma}{\boldsymbol{\gamma}} \newcommand{\bsigma}{\boldsymbol{\sigma}} \newcommand{\md}{\mbox{d}} \newcommand{\bmu}{\boldsymbol{\mu}} \newcommand{\bone}{\boldsymbol{1}} \newcommand{\bzero}{\boldsymbol{0}} \newcommand{\bepsilon}{\boldsymbol{\epsilon}} \newcommand{\bphi}{\boldsymbol{\phi}} \newcommand{\bh}{\boldsymbol{h}} \newcommand{\bc}{\boldsymbol{c}} \newcommand{\br}{\boldsymbol{r}} \newcommand{\bQ}{\boldsymbol{Q}} \newcommand{\bK}{\boldsymbol{K}} \newcommand{\bV}{\boldsymbol{V}} \newcommand{\bSigma}{\boldsymbol{\Sigma}} \newcommand{\bg}{\boldsymbol{g}} \newcommand{\bxi}{\boldsymbol{\xi}} \newcommand{\bvarepsilon}{\boldsymbol{\varepsilon}} \newcommand{\bdelta}{\boldsymbol{\delta}} \newcommand{\bq}{\boldsymbol{q}} \newcommand{\bk}{\boldsymbol{k}} \newcommand{\bJ}{\boldsymbol{J}} \newcommand{\bp}{\boldsymbol{p}} \newcommand{\bi}{\boldsymbol{i}} \newcommand{\bo}{\boldsymbol{o}} \newcommand{\bE}{\boldsymbol{E}} \newcommand{\bH}{\boldsymbol{H}} \newcommand{\bL}{\boldsymbol{L}} \newcommand{\bu}{\boldsymbol{u}} \newcommand{\bLambda}{\boldsymbol{\Lambda}} \newcommand{\trans}{^{\rm\scriptsize T}} \newcommand{\var}{\mathrm{var}}\\\newcommand{\bA}{\boldsymbol{A}} \newcommand{\bB}{\boldsymbol{B}} \newcommand{\bC}{\boldsymbol{C}} \newcommand{\bD}{\boldsymbol{D}} \newcommand{\bG}{\boldsymbol{G}} \newcommand{\bI}{\boldsymbol{I}} \newcommand{\bM}{\boldsymbol{M}} \newcommand{\bP}{\boldsymbol{P}} \newcommand{\bS}{\boldsymbol{S}} \newcommand{\bU}{\boldsymbol{U}} \newcommand{\bW}{\boldsymbol{W}} \newcommand{\bX}{\boldsymbol{X}} \newcommand{\bY}{\boldsymbol{Y}} \newcommand{\bZ}{\boldsymbol{Z}} \newcommand{\cotp}{\textcolor[RGB]{48,209,88}{TP}} \newcommand{\cotn}{\textcolor[RGB]{100,210,255}{TN}} \newcommand{\cofp}{\textcolor[RGB]{94,92,230}{FP}} \newcommand{\cofn}{\textcolor[RGB]{191,90,242}{FN}}\\\newcommand{\numcotp}{\textcolor[RGB]{48,209,88}{50}} \newcommand{\numcotn}{\textcolor[RGB]{100,210,255}{30}} \newcommand{\numcofp}{\textcolor[RGB]{94,92,230}{10}} \newcommand{\numcofn}{\textcolor[RGB]{191,90,242}{10}} \DeclareMathOperator*{\argmin}{arg\,min}\end{aligned}\end{align} \]

循环神经网络与随时间后向传播:参考答案#

返回正文练习 · 返回答案索引

说明#

以下答案与正文10道题逐题对应。前四题给出主要推导与计算;第 5--7 题给出可以运行的程序和检查方法;第 8--10 题说明比较时需要保持哪些条件相同,以及实验结果能够说明什么、不能说明什么。

  1. \(a_t=x_t+0.5h_{t-1}\)⁠,逐步计算为

    \[\begin{split}\begin{aligned} a_1&=1, &h_1&=\tanh(1)\approx0.76159,\\ a_2&=0.5h_1\approx0.38080, &h_2&=\tanh(a_2)\approx0.36340,\\ a_3&=-1+0.5h_2\approx-0.81830, &h_3&=\tanh(a_3)\approx-0.67414. \end{aligned}\end{split}\]

    因而 \(z=2h_3\approx-1.34829\)⁠。每一步必须使用前一步尚未四舍五入的结果;给出的近似值允许最后几位因舍入略有差别。

  2. \(\boldsymbol{g}_t=\nabla_{\bz_t}\ell_t\)⁠,且 \(\boldsymbol{\delta}_t=\nabla_{\ba_t}\mathcal J\)⁠;二者分别是 \(d_y\) 维和 \(d_h\) 维列向量。\(\ba_t\) 既影响当前输出,也经 \(\boldsymbol{h}_t\) 影响下一时刻,因此链式法则给出

    \[\boldsymbol{\delta}_t =\left( \bW_{hy}\trans\cdot\boldsymbol{g}_t +\bW_{hh}\trans\cdot\boldsymbol{\delta}_{t+1} \right) \odot(\bone-\boldsymbol{h}_t\odot\boldsymbol{h}_t), \qquad \boldsymbol{\delta}_{T+1}=\boldsymbol{0}.\]

    每次使用共享权重都会产生一项局部梯度,所以

    \[\frac{\partial\mathcal J}{\partial\bW_{hh}} =\sum_{t=1}^{T}\boldsymbol{\delta}_t\cdot\boldsymbol{h}_{t-1}\trans, \qquad \frac{\partial\mathcal J}{\partial\bW_{xh}} =\sum_{t=1}^{T}\boldsymbol{\delta}_t\cdot\bx_t\trans.\]

    若只保存最后一项,得到的是一次参数使用的梯度,而不是共享参数对总损失的梯度。

  3. 由次乘性 \(\lVert\bA\cdot\bB\rVert\leq\lVert\bA\rVert\lVert\bB\rVert\)⁠,可得

    \[\left\lVert \boldsymbol{J}_t\cdot\boldsymbol{J}_{t-1}\cdots\boldsymbol{J}_{s+1} \right\rVert \leq\prod_{r=s+1}^{t}\lVert\boldsymbol{J}_r\rVert.\]

    若所有相关时刻均有 \(\lVert\boldsymbol{J}_r\rVert\leq\rho<1\)⁠,上界不超过 \(\rho^{t-s}\)⁠,因此指数衰减。若若干方向上的增益持续大于 1,梯度可能指数放大。不过,上界小不能排除损失梯度方向的抵消,上界大也不保证实际向量落在放大方向;非正规矩阵、tanh 饱和和不同时间的奇异向量方向都会影响真实结果,所以这些只是充分的诊断条件。

  4. 各参数维度和数量为

    \[\begin{split}\begin{array}{c|c|c} \text{参数}&\text{维度}&\text{数量}\\ \hline \bW_{xh}&4\times3&12\\ \bW_{hh}&4\times4&16\\ \bb_h&4\times1&4\\ \bW_{hy}&2\times4&8\\ \bb_y&2\times1&2 \end{array}.\end{split}\]

    总数为 \(12+16+4+8+2=42\)⁠,与 \(T\) 无关。长度为 \(T\) 的单样本前向主要成本为 \(\mathcal{O}\!\left(T(d_hd_x+d_h^2+d_yd_h)\right)\)⁠;保存所有状态供 BPTT 使用时,状态内存至少为 \(\mathcal{O}(Td_h)\)⁠。

  5. 下面的 PyTorch 骨架在填充时刻保留旧状态,并显式提取最后一个有效状态。数学公式中的输入和隐藏状态均为列向量;程序使用 \(m\times T\times d_x\)\(m\times d_h\) 的数组批量存储它们,因此固定样本和时间后,数组最后一轴存放相应列向量的转置:

    import torch
    
    def rnn_forward(x, lengths, W_xh, W_hh, b_h):
        if x.ndim != 3 or lengths.shape != (x.shape[0],):
            raise ValueError("x 或 lengths 维度错误")
        batch, steps, _ = x.shape
        hidden = W_hh.shape[0]
        if W_hh.shape != (hidden, hidden) or W_xh.shape[0] != hidden:
            raise ValueError("权重维度错误")
        if torch.any((lengths < 1) | (lengths > steps)):
            raise ValueError("序列长度越界")
        h = x.new_zeros(batch, hidden)
        states = []
        for t in range(steps):
            proposal = torch.tanh(
                x[:, t, :] @ W_xh.T + h @ W_hh.T + b_h
            )
            valid = (t < lengths).unsqueeze(-1)
            h = torch.where(valid, proposal, h)
            states.append(h)
        states = torch.stack(states, dim=1)
        last = states[torch.arange(batch, device=x.device), lengths - 1]
        return states, last
    

    用长度如 [3, 1] 的批次验证:第二个样本从第 2 个存储位置起状态不变,last[1] 等于它第一个时刻的状态。这里不讨论空序列;若任务允许空序列,需要另行规定其表示。

  6. 手写实现先缓存 \(\ba_t\)⁠、\(\boldsymbol{h}_t\) 和输出,再按第 2 题递推 \(\boldsymbol{\delta}_t\) 并累加外积。自动微分对照 可使用相同参数和 float64中心差分 对单个参数 \(w\) 使用

    \[g_{\mathrm{fd}}= \frac{\mathcal J(w+h)-\mathcal J(w-h)}{2h}, \qquad h\approx10^{-6}.\]

    参考测试应检查

    relative_error = abs(g_manual - g_ref) / max(1.0, abs(g_ref))
    assert relative_error < 1e-6
    

    另构造两个时间步都产生非零损失的例子,确认共享权重梯度等于两项之和;故意只保留最后一项时测试必须失败。中心差分 在极度饱和或损失不光滑处会受舍入影响,不能机械使用同一阈值。

  7. 独立样本的测试应分别运行后拼成批次,与一次批量运行的有效状态逐项比较;把 padding 值改为极大数后结果仍应不变。连续片段测试把长序列切成两段,第二段输入第一段末状态,结果应与一次完整前向一致。截断 BPTT 则应传递数值但分离旧图,例如 h = h.detach()可断言旧片段输入的梯度为 None新片段参数仍有梯度。只有当两段确属同一序列时才传递状态,独立样本边界必须清零。

  8. 可生成长度固定、词元计数相同但顺序决定标签的数据,例如标签为“特殊词 A 是否先于 B”。词袋逻辑回归看不到顺序,RNN 可以利用顺序。先一次性确定数据划分和词表;使用相同的随机种子,并让两种训练方法使用预先规定且相同的更新次数和训练样本。逻辑回归与 RNN 的参数量不同,因此应同时报告参数量,还可以增加一个参数量相近的线性模型作为对照。

    每个种子报告测试准确率或 F1、训练墙钟时间、固定 512 个样本的预测时间和峰值内存,再汇总均值与波动。该任务只能说明在这个明确的顺序规则上 RNN 更有表达能力,不能据此断言 RNN 对所有文本任务都优于逻辑回归。

  9. 完整 BPTT 和各截断长度使用同一数据划分、初始参数、随机种子、优化器、总处理词元数与参数更新次数;每次截断边界只 detach 状态,不改变其数值。记录测试指标、训练时间、固定批量预测时间、峰值内存、实际循环单元运行次数和各时间步的梯度范数。预测阶段模型相同,因而固定批量预测成本原则上应相近;主要差异在训练时保存的计算过程和长期梯度。较短的 \(K\) 通常节省内存,但如果目标依赖超过 \(K\) 的历史信息,性能可能下降。如果截断长度变化的同时还改变了参数更新频率,就不能认定结果差异仅由截断长度造成。

  10. 先固定划分、种子集合、最大更新次数、早停耐心和评价代码,对每个 \(d_h\) 在同一学习率集合上搜索;模型选择只看验证集。结果表给出测试指标、训练时间、固定批量预测时间、峰值内存和参数量。隐藏维度主要改变容量与 \(d_h^2\) 循环成本,学习率主要改变优化轨迹;应画出每个维度内部的学习率结果,不能只拿每组最优点就声称差异完全来自容量。若早停使实际更新次数不同,应同时报告实际更新数,并补充固定更新预算结果。