Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

2.2 RNN:沿时间展开的权值共享与 BPTT

上一节把要求提出来了:变长、保序、参数与长度无关。现在兑现它。最朴素的想法是给网络装一块「记忆」:每读入一个输入 xt\boldsymbol x_t,就用它和上一步的记忆一起更新出一份新记忆,再据此给出这一步的输出。所有时间步用同一组权重做这件事——这就是循环神经网络(recurrent neural network, RNN)。

这个设计只用三行数学就写完了,但它的训练问题要用一整节来算清楚:梯度要沿时间倒着传回去,而这条路径上全是连乘。连乘的伸缩率由权重矩阵的谱性质决定,于是要么指数衰减(梯度消失)、要么指数增长(梯度爆炸),两者都让「长程依赖」变得几乎不可学。这一节的推导是整讲的技术地基——2.3 的门控是在改这里的结构,2.4 的注意力是在绕开这里的路径,2.7 的状态空间模型是在保住 O(1)O(1) 状态的同时重写这里的递推。

2.2.1 递推的定义与形状约定

定义 2.2.1 · 循环神经网络(RNN)

给定输入序列 x1:T\boldsymbol x_{1:T}、初始状态 h0=0\boldsymbol h_0=\boldsymbol 0,RNN 用一个 dhd_h 维隐状态递推:

zt=Whhht−1+Wxhxt+bh,ht=ϕ(zt),\boldsymbol z_t=W_{hh}\boldsymbol h_{t-1}+W_{xh}\boldsymbol x_t+\boldsymbol b_h,\qquad \boldsymbol h_t=\phi(\boldsymbol z_t), y^t=ψ(Whyht+by),\hat{\boldsymbol y}_t=\psi\bigl(W_{hy}\boldsymbol h_t+\boldsymbol b_y\bigr),

其中 Whh∈Rdh×dhW_{hh}\in\mathbb R^{d_h\times d_h}、Wxh∈Rdh×dxW_{xh}\in\mathbb R^{d_h\times d_x}、bh∈Rdh\boldsymbol b_h\in\mathbb R^{d_h}、Why∈Rdy×dhW_{hy}\in\mathbb R^{d_y\times d_h};ϕ\phi 常取 tanh⁡\tanh(也可用 ReLU 族),ψ\psi 按任务选(分类配 softmax,回归可省略)。同一组 Whh,Wxh,bhW_{hh},W_{xh},\boldsymbol b_h 在所有时间步复用,这就是 2.1 要求的参数共享。

一次只看三个符号:状态、输入、输出

把递推式读成三个问题:ht−1\boldsymbol h_{t-1} 里存着什么(历史摘要)、xt\boldsymbol x_t 带来什么(当前证据)、ht\boldsymbol h_t 该丢什么留什么(由 WhhW_{hh} 与 ϕ\phi 共同决定)。注意 ht\boldsymbol h_t 同时承担两个角色:它既是输出层的输入(现在要预测什么),又是下一步的输入(记忆里要留什么)。同一个向量被两个任务拉扯,这是 RNN 表达能力的一个先天限制——LSTM 用一条独立的细胞状态把这两个角色拆开(2.3),正是为了缓解它。

形状与参数量

批优先约定下(BB 为批大小、TT 为时间步数):

量 形状 参数量 说明
X\boldsymbol X B×T×dxB\times T\times d_x — 一批序列,第 1 维是时间
ht\boldsymbol h_t B×dhB\times d_h — 每一步的状态,随 tt 更新
WxhW_{xh} dh×dxd_h\times d_x dhdxd_h d_x 输入投影
WhhW_{hh} dh×dhd_h\times d_h dh2d_h^2 状态转移(与 TT 无关)
bh\boldsymbol b_h dhd_h dhd_h 状态偏置
WhyW_{hy} dy×dhd_y\times d_h dydhd_y d_h 输出投影
合计 — (dx+dh)dh+dh+dydh(d_x+d_h)d_h+d_h+d_y d_h 与序列长度 TT 完全无关

对照一下:若把 x1:T\boldsymbol x_{1:T} 摊平后交给 MLP,第一层的参数量是 dh(Tdx)d_h(Td_x)——随长度线性增长,而且 TT 一变就得重来。参数量与 TT 无关是 RNN 全部价值的来源。

2.2.2 沿时间展开:一个 T 层、共享权重的网络

把 t=1,…,Tt=1,\dots,T 的递推式摊平写开,就得到「沿时间展开」(unrolling)这幅图:RNN 等价于一个 TT 层的网络,每层结构相同、权重完全相同,相邻层之间除了前向的输入通路,还有一条隐状态通路。

RNN 沿时间展开:同一组权重在每一步复用 将 RNN 的隐状态递推沿时间展开成链:每步读入 xₜ、结合上一步的 hₜ₋₁ 计算新的 hₜ 并输出 yₜ。所有时间步共享同一组 W_hh、W_xh、W_y;反向传播沿时间连乘每步的雅可比。 展开前:h_t = φ(W_hh h_{t−1} + W_xh x_t) → 展开后:一条链 h₁ RNN 单元 x₁ y₁ h₂ RNN 单元 x₂ y₂ h₃ RNN 单元 x₃ y₃ h_T RNN 单元 x_T y_T 同一组 (W_hh, W_xh, W_y) h₀ BPTT:∂L/∂h₀ 含 ∏ 雅可比 → 梯度消失 / 爆炸
RNN 的「展开」视角(本教程绘)。折叠时是一个带反馈的单元;展开后是一条共享权重的链。序列建模的能力(状态记忆)与缺陷(沿时间的连乘雅可比,即 BPTT)都来自这条链——这正是 2.2 节梯度分析与 2.3 节门控改造的出发点。
定理 2.2.1 · 权值共享的收益与代价

设 y=fθ(x1:T)\boldsymbol y=f_\theta(\boldsymbol x_{1:T}) 由 定义 2.2.1 的递推给出。

收益:∣θ∣\lvert\theta\rvert 与 TT 无关(见表 method 2.2.1),且同一模式出现在任意位置都由同一组参数识别;对任意 T′T' 都有定义(可外推到训练时未见过的长度)。

代价(三条,都来自展开图):

  1. 不能沿时间并行:ht\boldsymbol h_t 依赖 ht−1\boldsymbol h_{t-1},第 tt 步的计算必须等第 t−1t-1 步完成,前向与反向的串行深度都是 TT;
  2. 梯度必须穿过 TT 次状态转移:任意两位置之间的影响力要在链上传播 TT 步(见 定理 2.2.2);
  3. 状态是信息瓶颈:所有历史必须压进固定的 dhd_h 维向量。
推导 · 参数量与「展开等价性」

参数量:WhhW_{hh}(dh2d_h^2)、WxhW_{xh}(dhdxd_h d_x)、bh\boldsymbol b_h(dhd_h)、WhyW_{hy}(dydhd_y d_h,by\boldsymbol b_y 为 dyd_y)相加即为表中合计项;每一项都不含 TT。对照「每步一套权重」的方案,参数量为 T[(dxdh+dh)+⋯ ]=O(T)T\left[(d_x d_h + d_h) + \cdots\right]=\mathcal O(T)。

展开等价性:定义 TT 个「层」ht=ϕ(Whhht−1+Wxhxt+bh)\boldsymbol h_t=\phi(W_{hh}\boldsymbol h_{t-1}+W_{xh}\boldsymbol x_t+\boldsymbol b_h)。若把它们看作一个普通的 TT 层前馈网络,则它的第 tt 层权重矩阵按定义全部相等(都等于 WhhW_{hh} 与 WxhW_{xh})。反过来,任何一组「层间权重相等」的 TT 层网络都可以折叠回递推式。因此:RNN = 一个绑定了权重共享约束的 TT 层网络。这个等价关系的价值在于,它把「沿时间反向传播」(2.2.3)变成第 1 讲里已经学过的普通反向传播,只是把「层」换成了「时间步」。■\blacksquare

∎
展开视角的实用价值:调试顺序

展开之后,很多困惑自动消失。例如「为什么 RNN 训练慢」——因为 TT 层网络的反向传播必须逐层进行,而这里的层数是序列长度;「为什么同一个词在不同位置得到同样的处理」——因为那是同一条权重;「为什么隐状态不能无限装历史」——因为所有层的中间表示都必须经过同一个 dhd_h 维瓶颈。请把 ht\boldsymbol h_t 记成「前 tt 步的有损摘要」:ht\boldsymbol h_t 是 x1:t\boldsymbol x_{1:t} 的确定性函数,但 dhd_h 固定而 tt 可以任意大,所以它不是完整记忆,而是按当前参数认为重要的那部分历史。

2.2.3 BPTT:梯度怎么沿时间传回去

序列任务的损失通常是各步损失之和 L=∑t=1TLt\mathcal L=\sum_{t=1}^{T}\mathcal L_t(逐时刻分类、逐 token 语言模型都是这个形态)。要对 WhhW_{hh} 求梯度,误差就必须传回早期时间步。做法与第 1 讲的反向传播完全一样——仍是链式法则与中间结果复用——只是「层」变成了「时间步」,因此叫沿时间反向传播(backpropagation through time, BPTT)。

定义 2.2.2 · 沿时间反向传播(BPTT)

令 zk=Whhhk−1+Wxhxk+bh\boldsymbol z_k=W_{hh}\boldsymbol h_{k-1}+W_{xh}\boldsymbol x_k+\boldsymbol b_h,定义单步雅可比

Jk:=∂hk∂hk−1=diag⁡(ϕ′(zk))Whh∈Rdh×dh,J_k:=\frac{\partial \boldsymbol h_k}{\partial \boldsymbol h_{k-1}}=\operatorname{diag}\bigl(\phi'(\boldsymbol z_k)\bigr)W_{hh}\in\mathbb R^{d_h\times d_h},

并以 gt:=∂Lt/∂ht\boldsymbol g_t:=\partial\mathcal L_t/\partial \boldsymbol h_t 表示第 tt 步损失对状态的直接梯度。BPTT 就是沿 t=T,T−1,…,1t=T,T-1,\dots,1 反序传播 ∂L/∂ht\partial\mathcal L/\partial\boldsymbol h_t。

为什么实现是「一个反序递推」,而不是一条长公式

BPTT 的代码只有一个形态:从 t=Tt=T 起,把「本步损失带来的误差」与「下一步回传的误差」相加算出 δt\boldsymbol\delta_t,累加进参数梯度,再把误差传给 t−1t-1。它与第 1 讲的反向传播完全同构,唯一的区别是「层」被时间步代替、而且层与层之间的权重是同一份(共享权重带来一个直接后果:所有时间步的贡献要累加到同一个梯度上)。所以 2.2.2 的展开视角很好用——把 TT 步递推想成 TT 层网络,BPTT 就是普通的逐层反向。请记住 δt\boldsymbol\delta_t 的定义,本节后面所有关于消失与爆炸的讨论都落在它身上。

定理 2.2.2 · BPTT 的连乘结构

沿用 定义 2.2.2 的记号,则对 t<st\lt s,

∂hs∂ht=∏k=t+1sJk=JsJs−1⋯Jt+1,\frac{\partial \boldsymbol h_s}{\partial \boldsymbol h_t}=\prod_{k=t+1}^{s}J_k=J_sJ_{s-1}\cdots J_{t+1},

于是

∂L∂ht=∑s≥t(∏k=t+1sJk) ⁣T∂Ls∂hs,∂L∂Whh=∑t=1Tδt ht−1T,δt=diag⁡(ϕ′(zt))∂L∂ht.\frac{\partial \mathcal L}{\partial \boldsymbol h_t}=\sum_{s\ge t}\left(\prod_{k=t+1}^{s}J_k\right)^{\!\mathrm T}\frac{\partial \mathcal L_s}{\partial \boldsymbol h_s},\qquad \frac{\partial \mathcal L}{\partial W_{hh}}=\sum_{t=1}^{T}\boldsymbol\delta_t\,\boldsymbol h_{t-1}^{\mathrm T},\qquad \boldsymbol\delta_t=\operatorname{diag}\bigl(\phi&#x27;(\boldsymbol z_t)\bigr)\frac{\partial\mathcal L}{\partial \boldsymbol h_t}.

结论:跨 n=s−tn=s-t 步的影响,其梯度含 nn 个 JkJ_k 的连乘;连乘的伸缩率决定梯度是消失还是爆炸。

推导 · 从链式法则到连乘(完整)

(1) 单步雅可比。 由 hk=ϕ(zk)\boldsymbol h_k=\phi(\boldsymbol z_k) 与 zk=Whhhk−1+(与 hk−1 无关的项)\boldsymbol z_k=W_{hh}\boldsymbol h_{k-1}+(\text{与 }\boldsymbol h_{k-1}\text{ 无关的项}),逐元素求导得

∂hk(i)∂hk−1(j)=ϕ′(zk(i))⋅Whh(i,j)  ⟹  Jk=diag⁡(ϕ′(zk))Whh.\frac{\partial h_k^{(i)}}{\partial h_{k-1}^{(j)}}=\phi&#x27;(z_k^{(i)})\cdot W_{hh}^{(i,j)} \;\Longrightarrow\; J_k=\operatorname{diag}\bigl(\phi&#x27;(\boldsymbol z_k)\bigr)W_{hh}.

注意 diag⁡(ϕ′(zk))\operatorname{diag}(\phi&#x27;(\boldsymbol z_k)) 是逐元素调制,WhhW_{hh} 负责跨维度混合——两者交替出现是全部问题的根源。

(2) 跨多步的雅可比 = 连乘。 由链式法则(多元情形下逐段相乘):

∂hs∂ht=∂hs∂hs−1⋅∂hs−1∂hs−2⋯∂ht+1∂ht=∏k=t+1sJk.\frac{\partial \boldsymbol h_s}{\partial \boldsymbol h_t} =\frac{\partial \boldsymbol h_s}{\partial \boldsymbol h_{s-1}}\cdot\frac{\partial \boldsymbol h_{s-1}}{\partial \boldsymbol h_{s-2}}\cdots\frac{\partial \boldsymbol h_{t+1}}{\partial \boldsymbol h_t} =\prod_{k=t+1}^{s}J_k .

矩阵乘法的顺序不能交换:JsJs−1⋯Jt+1J_sJ_{s-1}\cdots J_{t+1} 与 Jt+1⋯JsJ_{t+1}\cdots J_{s} 一般不同(反向传播里出现的是转置序列)。

(3) 损失对状态的梯度。 因为 ht\boldsymbol h_t 既影响 Lt\mathcal L_t(直接),又通过 ht+1,…,hT\boldsymbol h_{t+1},\dots,\boldsymbol h_T 影响后续所有损失,把 ss 从 tt 到 TT 的所有路径相加:

∂L∂ht=∑s≥t(∏k=t+1sJk) ⁣T∂Ls∂hs,其中 s=t 一项即 gt(空乘积取单位阵),\frac{\partial \mathcal L}{\partial \boldsymbol h_t} =\sum_{s\ge t}\left(\prod_{k=t+1}^{s}J_k\right)^{\!\mathrm T}\frac{\partial \mathcal L_s}{\partial \boldsymbol h_s}, \qquad\text{其中 } s=t \text{ 一项即 } \boldsymbol g_t \text{(空乘积取单位阵)},

其中 s=ts=t 一项就是 gt\boldsymbol g_t(空乘积取单位阵)。这解释了为什么「误差回传多远」这个问题必须逐层考虑:远处损失的梯度要先穿过一长串 JkJ_k 才能到达 ht\boldsymbol h_t。

(4) 对参数的梯度(实现里用的递推形式)。 由 zt=Whhht−1+⋯\boldsymbol z_t=W_{hh}\boldsymbol h_{t-1}+\cdots,有 ∂zt/∂Whh=ht−1\partial\boldsymbol z_t/\partial W_{hh}=\boldsymbol h_{t-1} 的线性映射,故

∂L∂Whh=∑t=1Tδtht−1T,δt=diag⁡(ϕ′(zt))(WhyT∂Lt∂y^t⏟本步输出带来的误差+WhhTδt+1).\frac{\partial\mathcal L}{\partial W_{hh}}=\sum_{t=1}^{T}\boldsymbol\delta_t\boldsymbol h_{t-1}^{\mathrm T},\qquad \boldsymbol\delta_t=\operatorname{diag}\bigl(\phi&#x27;(\boldsymbol z_t)\bigr)\Bigl(\underbrace{W_{hy}^{\mathrm T}\tfrac{\partial \mathcal L_t}{\partial \hat{\boldsymbol y}_t}}_{\text{本步输出带来的误差}}+W_{hh}^{\mathrm T}\boldsymbol\delta_{t+1}\Bigr).

这个反向递推式 就是 BPTT 的实现:从 t=Tt=T 起,每一步用「本步误差 + 下一步回传的误差」算出 δt\boldsymbol\delta_t,再累加进 WhhW_{hh} 的梯度。可以看到整个反向过程的时间复杂度是 O(T dh2)\mathcal O(T\,d_h^2),且必须串行(δt\boldsymbol\delta_t 依赖 δt+1\boldsymbol\delta_{t+1})。

(5) 范数界。 对任意相容矩阵范数,

∥∏k=t+1sJk∥≤∏k=t+1s∥Jk∥≤(max⁡k∥Jk∥2) s−t,\left\lVert\prod_{k=t+1}^{s}J_k\right\rVert\le\prod_{k=t+1}^{s}\lVert J_k\rVert \le\Bigl(\max_{k}\lVert J_k\rVert_2\Bigr)^{\,s-t},

即跨 nn 步的梯度量级被「单步雅可比的最大奇异值」的 nn 次方控制(严格的最坏情形界;实际谱半径决定长期平均行为)。■\blacksquare

∎
一句话记住 BPTT 的两个数字

BPTT 只有两个关键数字:跨了几步 nn,以及每一步的伸缩率 ρ=∥Jk∥2\rho=\lVert J_k\rVert_2(长期由谱半径主导)。梯度量级大致按 ρ n\rho^{\,n} 变化。ρ=0.9\rho=0.9、n=100n=100 时约 2.7×10−52.7\times10^{-5};ρ=1.1\rho=1.1、n=100n=100 时约 1.4×1041.4\times10^{4}。也就是说:没有一个「安全的中间地带」可站——参数稍微小一点就消失,稍微大一点就爆炸,而序列一长,任何偏离 1 的乘数都会被指数放大。这就是为什么「把学习率调好就能学会长依赖」这种想法在朴素 RNN 上不成立。

2.2.4 定量分析:谱半径、最大奇异值与连乘

定理 2.2.3 · 梯度消失与爆炸的判据

在 定义 2.2.2 的记号下,若对所有 kk 有 ∥Jk∥2≤ρ\lVert J_k\rVert_2\le\rho:

  • ρ<1\rho\lt1:∥∂hs/∂ht∥≤ρ s−t→0\left\lVert\partial\boldsymbol h_s/\partial\boldsymbol h_t\right\rVert\le\rho^{\,s-t}\to0,跨 nn 步的梯度指数消失;
  • ρ>1\rho\gt1:上界随 nn 指数增长,梯度可能指数爆炸(先是数值变大,随后溢出为 inf/NaN);
  • 长期行为由 WhhW_{hh} 的谱半径 ρ(Whh)\rho(W_{hh}) 与 ϕ′\phi&#x27; 的取值共同决定;tanh⁡\tanh 满足 ∣ϕ′∣≤1\lvert\phi&#x27;\rvert\le1(在 00 处取到最大值 11,饱和区趋于 00),因此 ∥Jk∥2≤∥Whh∥2\lVert J_k\rVert_2\le\lVert W_{hh}\rVert_2。

推论(两个常被引用的事实):① 只把激活换成 ReLU 不能让梯度消失消失(∥Jk∥\lVert J_k\rVert 仍由 WhhW_{hh} 决定,见 errata);② 梯度裁剪只能压制爆炸,对消失无能为力。

推导 · 为什么会指数变化,以及「精度地板」是什么

(1) 标量情形先看透。 取 dh=1d_h=1、ht=ϕ(w ht−1+c)h_t=\phi(w\,h_{t-1}+c),则 Jk=w ϕ′(zk)J_k=w\,\phi&#x27;(z_k),跨 nn 步的乘积为

∂ht∂ht−n=∏k=t−n+1tw ϕ′(zk)=wn∏kϕ′(zk).\frac{\partial h_t}{\partial h_{t-n}}=\prod_{k=t-n+1}^{t}w\,\phi&#x27;(z_k) =w^{n}\prod_{k}\phi&#x27;(z_k).

取 ϕ=tanh⁡\phi=\tanh(∣ϕ′∣≤1\lvert\phi&#x27;\rvert\le1)得 ∣∂ht/∂ht−n∣≤∣w∣n\lvert\partial h_t/\partial h_{t-n}\rvert\le\lvert w\rvert^{n}——与 定理 2.2.2 的范数界一致。

(2) 矩阵情形。 由 ∥AB∥2≤∥A∥2∥B∥2\lVert AB\rVert_2\le\lVert A\rVert_2\lVert B\rVert_2 与 ∥diag⁡(ϕ′)∥2=max⁡i∣ϕ′(z(i))∣≤1\lVert\operatorname{diag}(\phi&#x27;)\rVert_2=\max_i\lvert\phi&#x27;(z^{(i)})\rvert\le1 立得 ∥Jk∥2≤∥Whh∥2\lVert J_k\rVert_2\le\lVert W_{hh}\rVert_2。更精细的长期刻画用谱半径:当 JkJ_k 的分布稳定时,∥Js⋯Jt+1∥1/(s−t)→ρ(J)\lVert J_s\cdots J_{t+1}\rVert^{1/(s-t)}\to\rho(J)(乘积的渐近指数增长率),因此「平均意义上」梯度量级随 nn 以 ρ(J)n\rho(J)^n 变化。

(3) 为什么有一个「地板」。 浮点数不是实数:单精度(float32)能表示的最小正规数约 1.2×10−381.2\times10^{-38},而实际训练里关心的是信噪比——反向传播还要继续与上游梯度、权重相乘,当跨 nn 步的因子衰减到 10−710^{-7} 量级时,它加到参数更新上的贡献通常已经低于学习率与优化器噪声的水平,在效果上等于零(这就是「float32 精度地板 ≈10−7\approx10^{-7}」这个工程经验值的含义)。同理,爆炸方向一旦使中间量超过 3.4×10383.4\times10^{38} 就变成 inf,随后的 inf - inf 或 0 × inf 直接产生 NaN。

(4) 与「长程依赖学不到」的联系。 假设模型需要利用第 5 步的信息来决定第 200 步的输出。该路径的梯度必须经过 n=195n=195 次连乘。由上面的界,梯度量级被 ρ195\rho^{195} 控制:ρ=0.9\rho=0.9 时为 10−910^{-9} 量级(已在地板以下),ρ=1.1\rho=1.1 时约 10810^{8}(未被裁剪就会把参数推向发散)。结论不是「RNN 学不会」,而是「朴素 RNN 对长依赖的学习信号被指数衰减吞掉了」——这正是门控结构(2.3)与注意力(2.4)各自要重写的部分。■\blacksquare

∎
这条判据是可以「看」出来的

定理最实用的推论是:它能在训练日志里被直接观测。做法是把同一个 RNN 用不同的序列长度训练,记录最远离损失的那一层(最靠近输入的权重)收到的梯度范数:若它随长度在对数坐标上呈直线下降,梯度在消失;若呈直线上升,梯度在爆炸。§2.2.7 的代码做了这件事。注意由此推出的一个反常识结论:不存在一个学习率能同时解决这两个方向——问题出在连乘的结构上,而不是步长大小;调参只能改变它发生的快慢,不能改变它的存在。

梯度随回传时间步数的衰减(对数轴) 横轴为沿时间回传的步数,纵轴为梯度量级(对数刻度)。每步乘 0.25(Sigmoid 饱和)的曲线在 10 步内衰减到百万分之一;乘 0.9(门控改善后)衰减显著变慢;乘 1(ReLU 正区或细胞态加法通道)不衰减。10⁻⁷ 为 float32 的精度地板。 0123410⁻⁸10⁻⁶10⁻⁴10⁻²10⁰回传的时间步数 t(从 t=0 算起)梯度量级(对数轴) float32 精度地板 ≈10⁻⁷ 每步 ×0.25:Sigmoid 饱和段的最坏情形,10 步即 10⁻⁶每步 ×0.5:典型 tanh 情形每步 ×0.9:门控(LSTM/GRU)把通道修到接近 1每步 ×1:ReLU 正区 / 细胞态加法通道,连乘不衰减
梯度消失有多快(本教程计算绘制,纵轴对数)。反向传播沿时间的梯度是每步雅可比的连乘:每步因子 0.25 时 10 步衰减 10610^{6} 倍、20 步直接跌破 float32 的工程精度地板——这解释了 2.2 节「RNN 记不住长程依赖」与 2.3 节「门控把通道修到接近 1」的全部动机。
例 2.2.1 · 算一算梯度能走多远

沿用标量直觉:单步伸缩率 ρ\rho 分别取 0.250.25、0.50.5、0.90.9、1.01.0、1.11.1。

(1) 计算跨 n=10,20,50,100,200n=10,20,50,100,200 步的梯度量级 ρn\rho^{n},填表。 (2) 以「10−710^{-7} 以下视为被吞掉」为判据,各种 ρ\rho 的有效记忆跨度是多少步? (3) 若训练中出现 NaN,ρ\rho 大概率落在哪一档?此时最先应该检查什么?

解

(1) 量级表(四舍五入到三位有效数字):

ρ\rho n=10n=10 n=20n=20 n=50n=50 n=100n=100 n=200n=200
0.250.25 9.5×10−79.5\times10^{-7} 9.1×10−139.1\times10^{-13} 7.9×10−317.9\times10^{-31} 6.2×10−616.2\times10^{-61} 3.9×10−1213.9\times10^{-121}
0.50.5 9.8×10−49.8\times10^{-4} 9.5×10−79.5\times10^{-7} 8.9×10−168.9\times10^{-16} 7.9×10−317.9\times10^{-31} 6.2×10−616.2\times10^{-61}
0.90.9 0.3490.349 0.1220.122 5.15×10−35.15\times10^{-3} 2.66×10−52.66\times10^{-5} 7.06×10−107.06\times10^{-10}
1.01.0 11 11 11 11 11
1.11.1 2.592.59 6.736.73 117117 1.38×1041.38\times10^{4} 1.90×1081.90\times10^{8}

(2) 令 ρn=10−7\rho^{n}=10^{-7},解得 n⋆=−7log⁡10ρn^\star=\dfrac{-7}{\log_{10}\rho}:

  • ρ=0.25\rho=0.25:n⋆≈11.6n^\star\approx11.6 步;ρ=0.5\rho=0.5:n⋆≈23.3n^\star\approx23.3 步;ρ=0.9\rho=0.9:n⋆≈153n^\star\approx153 步;ρ=1.0\rho=1.0:无衰减(理论上无限,实际受状态容量限制)。
  • 也就是说,即使只把单步衰减控制在 0.9(已经是相当乐观的假设),朴素 RNN 的有效记忆也只有百步量级;而在 0.25 这种最坏情形下,10 步就没了。

(3) 出现 NaN 说明中间量溢出,ρ>1\rho\gt1 是最可能的档位(爆炸方向)。排查顺序:① 先做梯度裁剪(method 2.2.2)确认能否止住;② 检查学习率是否过大(学习率会把参数推到 ∥Whh∥2>1\lVert W_{hh}\rVert_2\gt1 的区域);③ 检查是否有单步损失异常大(如标签错位、log(0));④ 若发生在混合精度下,检查是否需要 loss scaling(第 9 讲)。注意:这套流程针对的是爆炸;如果症状是「loss 卡住不降、长依赖学不到」,那是消失方向的问题,裁剪帮不上忙。 ■\blacksquare

∎

2.2.5 工程对策一:梯度裁剪(治爆炸)

按全局范数裁剪(global norm clipping)

设本轮所有参数的梯度拼成一个长向量 g\boldsymbol g(等价于各参数张量梯度的 L2 范数平方和再开方)。给定阈值 cc:

g←g⋅min⁡ ⁣(1, c∥g∥).\boldsymbol g\leftarrow\boldsymbol g\cdot\min\!\left(1,\ \frac{c}{\lVert\boldsymbol g\rVert}\right).

  • 语义:当 ∥g∥>c\lVert\boldsymbol g\rVert\gt c 时,把整个梯度向量等比缩小到范数恰为 cc——方向不变,只改尺度;
  • 阈值取法:先跑几十步,打印未裁剪的梯度范数,取它的中位数附近的量级(常见经验值在 11 附近,但必须看自己任务的实际分布);
  • 与逐元素裁剪的区别:clip_by_value(把每个元素截到 [−c,c][-c,c])会改变方向,可能导致更新指向错误的下降方向;序列任务里推荐按范数裁剪;
  • 边界:它只压得住爆炸。梯度已经衰减到 10−910^{-9} 时,等比缩放救不回来(乘一个系数仍是 10−910^{-9} 量级)。消失要靠门控结构(2.3)、合理的初始化与残差连接。

值得说明裁剪为什么「有效但不治本」:爆炸发生在更新之前的瞬间,裁剪把这一步的更新尺度拉回可控范围,于是训练不会崩,但它并没有改变 JkJ_k 的连乘结构——下一次反向仍然是同一串乘积。因此裁剪是安全网,不是疗法。

2.2.6 工程对策二:teacher forcing 与它的代价

自回归任务(语言模型、音乐生成、机器翻译的解码器)在训练时要面对一个选择:第 tt 步的输入用真实的 xt\boldsymbol x_t,还是用模型上一步自己猜的 x^t−1\hat{\boldsymbol x}_{t-1}?

定义 2.2.3 · teacher forcing 与 exposure bias

Teacher forcing:训练时始终以真实前缀作为输入,即对每个位置并行计算

L=−∑t=1Tlog⁡pθ(xt+1∣x≤t),\mathcal L=-\sum_{t=1}^{T}\log p_\theta\bigl(x_{t+1}\mid x_{\le t}\bigr),

其中条件里的 x≤tx_{\le t} 全部来自数据(而非模型采样)。它把训练变成「一次前向、TT 个位置同时算」,因此可以并行、且梯度无偏。

Free-running(自回归)模式:以模型自己生成的 token 作为下一步输入。它与 teacher forcing 的差别在于输入分布不同(train–inference distribution shift),这个偏移被称为 exposure bias(曝光偏差):训练时模型从未见过自己的错误输出,推理时错误会一步步累积。

为什么训练用 teacher forcing、推理只能自由跑

teacher forcing 的本质是把「多步决策问题」松弛成 TT 个独立的一步预测问题:给定真实上下文,预测下一个 token。这让损失可分解、可并行、方差小。代价是训练与推理的输入分布不一致:推理时模型看到的是自己(可能有错)的输出,而它在训练中从没见过这种输入,于是误差会沿时间累积——这也是「训练困惑度很低但生成质量差」的首要原因之一。工程上的折中手段有三类:① 课程式逐步混入模型自己的预测(scheduled sampling 一类做法);② 训练时对输入加少量噪声 / 用 beam search 减少推理误差;③ 用更长的上下文与更大的模型把单步错误率压到足够低,让累积误差仍然可接受——大语言模型的实践主要走第三条路。

2.2.7 代码:手写 RNN 单元、梯度检查与裁剪

import torch, torch.nn as nn, torch.nn.functional as F

torch.manual_seed(0)
B, T, d_x, d_h = 4, 32, 8, 16
x = torch.randn(B, T, d_x, dtype=torch.float64)      # float64:梯度检查需要高精度
y = torch.randint(0, 3, (B, T))                      # 每步 3 类(同步对齐任务)

class RNNCellManual(nn.Module):
    def __init__(self, d_x, d_h, d_y):
        super().__init__()
        # 与 PyTorch 一致:把 W_hh 与 W_xh 拼成一个矩阵,一次矩阵乘算完(实现技巧,不改变数学)
        self.W = nn.Linear(d_x + d_h, d_h, bias=True)
        self.out = nn.Linear(d_h, d_y, bias=True)

    def forward(self, x, h=None):
        B, T, _ = x.shape
        h = torch.zeros(B, self.W.out_features, dtype=x.dtype) if h is None else h
        hs = []
        for t in range(T):                             # 串行:h_t 依赖 h_{t-1}
            h = torch.tanh(self.W(torch.cat([x[:, t], h], dim=-1)))
            hs.append(h)
        return self.out(torch.stack(hs, dim=1)), h     # (B,T,d_y), (B,d_h)

model = RNNCellManual(d_x, d_h, 3)

# ① 梯度检查:用数值微分验证 BPTT 的实现(第 1 讲 1.5 的做法)
x_small = x[:, :4].clone().requires_grad_(True)      # 只取 4 步,数值梯度才跑得动
logits, _ = model(x_small)
loss = F.cross_entropy(logits.reshape(-1, 3), y[:, :4].reshape(-1))
grads = torch.autograd.grad(loss, model.parameters())

for p, g in zip(model.parameters(), grads):
    assert torch.isfinite(g).all(), "梯度出现 inf/NaN:多半是爆炸方向"

# ② 观察「连乘」:把同一权重复制到不同深度的 RNN,看梯度范数如何随步数变化
for T_try in (4, 8, 16, 32):
    logits, _ = model(x[:, :T_try])
    loss = F.cross_entropy(logits.reshape(-1, 3), y[:, :T_try].reshape(-1))
    model.zero_grad(); loss.backward()
    # 取第一层(最靠近输入)权重的梯度范数:它离损失最远,最先被连乘吞掉
    g0 = model.W.weight.grad.norm().item()
    print(f"T={T_try:3d}  最底层权重梯度范数={g0:.3e}")

# ③ 梯度裁剪:训练循环里的标准写法(放在 backward 之后、step 之前)
opt = torch.optim.AdamW(model.parameters(), lr=1e-3)
model.zero_grad()
logits, _ = model(x)
F.cross_entropy(logits.reshape(-1, 3), y.reshape(-1)).backward()
grad_norm = nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)  # 返回裁剪前的范数
print(f"裁剪前全局梯度范数={grad_norm:.4f}(超过 1.0 时被等比缩放到 1.0)")
opt.step(); opt.zero_grad()

这段代码里三个动作对应本节的三条结论:① 数值梯度检查确认 BPTT 实现正确(链式法则写错时数值梯度会立刻暴露);② 梯度范数随 TT 的变化是 定理 2.2.3 的可观测形式(把 tanh⁡\tanh 换成更陡 / 更平的结构,同一实验的斜率会明显变化);③ 裁剪的位置必须在 backward() 与 step() 之间——放到更新之后就没用了。

本节五个常见误解
  1. 「换 ReLU 就能治好梯度消失」:ReLU 只解决激活饱和造成的衰减(tanh⁡\tanh 在饱和区导数趋 0 这一项),但 定理 2.2.3 的界 ∥Jk∥2≤∥Whh∥2\lVert J_k\rVert_2\le\lVert W_{hh}\rVert_2 与激活无关:只要权重矩阵的最大奇异值小于 1,连乘照样衰减。真正有效的是正交 / 单位初始化 + 门控加法通道(2.3)与残差结构。
  2. 「梯度裁剪解决了梯度消失」:裁剪按范数等比缩小梯度,等于乘一个小于等于 1 的系数。梯度已经是 10−910^{-9} 时,它还是 10−910^{-9} 量级。裁剪只防爆炸。
  3. 「ht\boldsymbol h_t 记住了前面所有的信息」:dhd_h 固定、tt 任意大,ht\boldsymbol h_t 是有损摘要而非完整记忆;具体丢什么由参数决定。用「模型应该记住第 3 步的细节」这类假设去解释生成结果,往往会失望。
  4. 「训练损失很低就说明生成会好」:teacher forcing 下的低损失只保证「给定真实前缀,下一步预测得准」,不保证模型自己的错误不会累积(定义 2.2.3 的 exposure bias)。评估生成质量必须看自由跑(free-running)的结果。
  5. 「RNN 因为不能并行,所以一无是处」:训练不能沿时间并行是事实,但推理每步只需 O(1)O(1) 状态与 O(dh2)O(d_h^2) 计算、显存不随序列增长,天然支持流式与无限长输入。在设备端、实时信号处理、超长序列与小数据场景里,递推类结构仍有竞争力(2.7 的状态空间模型正是沿着这条路重新发力)。
即时自测 · 2.2

Q1 写出 RNN 递推式与参数量,并说明「参数量与 TT 无关」的代价是什么。

Q2 写出 ∂hs/∂ht\partial\boldsymbol h_s/\partial\boldsymbol h_t 的表达式,并指出其中每一项的物理含义(哪个因子负责「逐元素压缩」、哪个负责「跨维度混合」)。

Q3 设单步伸缩率 ρ=0.98\rho=0.98,跨 100 步的梯度量级约是多少?这个结果解释了关于长程依赖的什么现象?

Q4 为什么「训练损失 0.2、生成质量很差」是自回归模型的常见组合?给出两个缓解方向。

参考要点

Q1 ht=ϕ(Whhht−1+Wxhxt+bh)\boldsymbol h_t=\phi(W_{hh}\boldsymbol h_{t-1}+W_{xh}\boldsymbol x_t+\boldsymbol b_h)、y^t=ψ(Whyht+by)\hat{\boldsymbol y}_t=\psi(W_{hy}\boldsymbol h_t+\boldsymbol b_y);参数量 (dx+dh)dh+dh+dydh(d_x+d_h)d_h+d_h+d_y d_h。代价三条:① 沿时间不能并行(串行深度 TT);② 梯度要穿过 TT 次状态转移,连乘导致消失 / 爆炸;③ 全部历史压进固定 dhd_h 维状态,是有损瓶颈。

Q2 ∂hs/∂ht=∏k=t+1sdiag⁡(ϕ′(zk))Whh\partial\boldsymbol h_s/\partial\boldsymbol h_t=\prod_{k=t+1}^{s}\operatorname{diag}(\phi&#x27;(\boldsymbol z_k))W_{hh}。其中 diag⁡(ϕ′(zk))\operatorname{diag}(\phi&#x27;(\boldsymbol z_k)) 是逐元素的激活导数调制(决定「每个维度被压缩多少」),WhhW_{hh} 是跨维度的线性混合(决定「信息如何重组」)。两者交替连乘,因此衰减既可能来自激活饱和(ϕ′\phi&#x27; 小),也可能来自权重谱性质(∥Whh∥2\lVert W_{hh}\rVert_2 小)。

Q3 0.98100=exp⁡(100ln⁡0.98)≈exp⁡(−2.02)≈0.130.98^{100}=\exp(100\ln0.98)\approx\exp(-2.02)\approx0.13。也就是 100 步只衰减到约 13%13\%——比 0.9 的情形好得多,但仍随步数指数下降:跨 1000 步约 1.7×10−91.7\times10^{-9}(已被吞掉)。这解释了「单步衰减略小于 1 也救不了长依赖」:任何严格小于 1 的固定乘数,在足够长的链上都会归零。

Q4 因为训练用 teacher forcing(输入是真实前缀),损失衡量的是「给定正确上下文的一步预测质量」;推理时输入变成模型自己的输出,分布偏移导致误差累积(exposure bias)。缓解方向:① 训练时逐步混入模型自己的预测(scheduled sampling 类做法)或对输入加噪;② 提高单步精度(更大模型 / 更长上下文 / 更好的解码策略),让累积误差落在可接受范围;③ 用生成侧的指标(自由跑困惑度、人工评估、错误传播长度)替代纯 teacher forcing 损失作为模型选择依据。

延伸阅读
  • Werbos 与后续的教科书章节关于「沿时间反向传播」的经典叙述;本节推导与第 1 讲的反向传播是同一条链式法则,只是把「层」换成「时间步」。
  • Bengio, Simard & Frasconi, Learning Long-Term Dependencies with Gradient Descent is Difficult(1994, IEEE Transactions on Neural Networks)——「消失的梯度让长依赖学不到」这一结论的经典来源。
  • Pascanu, Mikolov & Bengio, On the Difficulty of Training Recurrent Neural Networks(2012/2013, arXiv:1211.5063)——梯度爆炸 / 消失的统一分析、梯度裁剪的规范化写法与谱半径视角。
  • Williams & Zipser, A Learning Algorithm for Continually Running Fully Recurrent Neural Networks(1989, Neural Computation)——BPTT 与 RTRL 的历史原文。
  • 关于「ht\boldsymbol h_t 是有损摘要」的直观解释,见 2.3 的延伸阅读(Olah, Understanding LSTM Networks, 2015)。

本节小结:RNN 用「同一组权重在每个时间步复用」把参数量从 O(T)\mathcal O(T) 压到与 TT 无关,代价是沿时间不能并行与梯度必须穿过 TT 次状态转移。BPTT 的推导给出关键结论:跨 nn 步的梯度含 nn 个单步雅可比 diag⁡(ϕ′)Whh\operatorname{diag}(\phi&#x27;)W_{hh} 的连乘,伸缩率由范数(长期由谱半径)决定,ρ<1\rho\lt1 指数消失、ρ>1\rho\gt1 指数爆炸,没有安全中间地带;裁剪只治爆炸,消失需要改结构。下一节把更新式从「乘」改成「加」,用门控造出一条近似恒等的梯度通道。