循环神经网络与随时间后向传播:参考答案#
说明#
以下答案与正文10道题逐题对应。前四题给出主要推导与计算;第 5--7 题给出可以运行的程序和检查方法;第 8--10 题说明比较时需要保持哪些条件相同,以及实验结果能够说明什么、不能说明什么。
令 \(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\)。每一步必须使用前一步尚未四舍五入的结果;给出的近似值允许最后几位因舍入略有差别。
记 \(\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.\]若只保存最后一项,得到的是一次参数使用的梯度,而不是共享参数对总损失的梯度。
由次乘性 \(\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 饱和和不同时间的奇异向量方向都会影响真实结果,所以这些只是充分的诊断条件。
各参数维度和数量为
\[\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)\)。
下面的 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]等于它第一个时刻的状态。这里不讨论空序列;若任务允许空序列,需要另行规定其表示。手写实现先缓存 \(\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
另构造两个时间步都产生非零损失的例子,确认共享权重梯度等于两项之和;故意只保留最后一项时测试必须失败。中心差分 在极度饱和或损失不光滑处会受舍入影响,不能机械使用同一阈值。
独立样本的测试应分别运行后拼成批次,与一次批量运行的有效状态逐项比较;把 padding 值改为极大数后结果仍应不变。连续片段测试把长序列切成两段,第二段输入第一段末状态,结果应与一次完整前向一致。截断 BPTT 则应传递数值但分离旧图,例如
h = h.detach();可断言旧片段输入的梯度为None,新片段参数仍有梯度。只有当两段确属同一序列时才传递状态,独立样本边界必须清零。可生成长度固定、词元计数相同但顺序决定标签的数据,例如标签为“特殊词 A 是否先于 B”。词袋逻辑回归看不到顺序,RNN 可以利用顺序。先一次性确定数据划分和词表;使用相同的随机种子,并让两种训练方法使用预先规定且相同的更新次数和训练样本。逻辑回归与 RNN 的参数量不同,因此应同时报告参数量,还可以增加一个参数量相近的线性模型作为对照。
每个种子报告测试准确率或 F1、训练墙钟时间、固定 512 个样本的预测时间和峰值内存,再汇总均值与波动。该任务只能说明在这个明确的顺序规则上 RNN 更有表达能力,不能据此断言 RNN 对所有文本任务都优于逻辑回归。
完整 BPTT 和各截断长度使用同一数据划分、初始参数、随机种子、优化器、总处理词元数与参数更新次数;每次截断边界只
detach状态,不改变其数值。记录测试指标、训练时间、固定批量预测时间、峰值内存、实际循环单元运行次数和各时间步的梯度范数。预测阶段模型相同,因而固定批量预测成本原则上应相近;主要差异在训练时保存的计算过程和长期梯度。较短的 \(K\) 通常节省内存,但如果目标依赖超过 \(K\) 的历史信息,性能可能下降。如果截断长度变化的同时还改变了参数更新频率,就不能认定结果差异仅由截断长度造成。先固定划分、种子集合、最大更新次数、早停耐心和评价代码,对每个 \(d_h\) 在同一学习率集合上搜索;模型选择只看验证集。结果表给出测试指标、训练时间、固定批量预测时间、峰值内存和参数量。隐藏维度主要改变容量与 \(d_h^2\) 循环成本,学习率主要改变优化轨迹;应画出每个维度内部的学习率结果,不能只拿每组最优点就声称差异完全来自容量。若早停使实际更新次数不同,应同时报告实际更新数,并补充固定更新预算结果。