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.3 门控:LSTM 与 GRU

2.2 节 把 RNN 的困难算成了一条式子:跨 s−ts-t 步的梯度含 diag⁡(ϕ′(zk))Whh\operatorname{diag}(\phi'(\boldsymbol z_k))W_{hh} 的连乘。而这一节要换掉的就是这个连乘本身:把状态更新写成「加法」,让梯度有一条不经过权重矩阵的通路,同时把「记多久」从权重的副产品变成网络能学的量。

2.3.1 病根是「乘」,解法是「加」

连乘的伸缩率与步数是指数关系:每个因子比 11 小一点,几十步后就差出十几个数量级;比 11 大一点,几十步后直接溢出。2.2 节给出的第一个对策是梯度裁剪——更新前把梯度范数压回阈值内、方向不变。它治得住爆炸(防止溢出与 NaN),却治不住消失:梯度已经接近零,再等比缩小一次仍然是零。

所以这一节改的是结构本身,不是训练的补丁。可行的路线有两条。

路线 A:改初始化与约束。 把 WhhW_{hh} 初始化成(近似)正交矩阵,或给它加谱约束,先把连乘因子按住。这条路值得做,但它只固定了连乘因子的一半:另一半 diag⁡(ϕ′(zk))\operatorname{diag}(\phi'(\boldsymbol z_k)) 由当前输入决定、逐步浮动,于是网络没有表达「这一步该记住、下一步该清空」的自由度;训练中 WhhW_{hh} 自己还会漂移离开正交簇,约束必须一直维护。更根本的一点是:记忆时长在路线 A 里不是网络能学的量,而是权重的副产品。

路线 B:改更新式。 让状态写成 ct=ft⊙ct−1+(⋯ )\boldsymbol c_t=\boldsymbol f_t\odot\boldsymbol c_{t-1}+(\cdots)——求导时 ∂ct/∂ct−1\partial\boldsymbol c_t/\partial\boldsymbol c_{t-1} 变成对角矩阵,不再含 WW 的连乘,而「留多少」由 ft\boldsymbol f_t 这个由当前输入算出来的量决定。这就是门控(gating),也是本节的主角。

一句话记住这条路线:病根是「乘」,解法是「加」。加法本身不稀奇,稀奇的是它把伸缩率从一个固定的权重性质换成了一个可学、且可以依赖输入的旋钮——「按需记住、按需清空」这句话,第一次成了模型能表达的东西。本节顺序:LSTM 的三门与加法通道 → 传送带的推导、条件与代价 → GRU 的压缩 → 变体与工程结论 → 组合成编码器 → 形状与参数量 → 代码核对。

2.3.2 LSTM:三门与一条加法通道

定义 2.3.1 · LSTM(long short-term memory)

用 [ht−1,xt][\boldsymbol h_{t-1},\boldsymbol x_t] 表示上一步隐状态与当前输入的拼接,LSTM 在每个时间步算六个量:

ft=σ(Wf[ht−1,xt]+bf),it=σ(Wi[ht−1,xt]+bi),ot=σ(Wo[ht−1,xt]+bo),\boldsymbol f_t=\sigma\bigl(W_f[\boldsymbol h_{t-1},\boldsymbol x_t]+\boldsymbol b_f\bigr),\qquad \boldsymbol i_t=\sigma\bigl(W_i[\boldsymbol h_{t-1},\boldsymbol x_t]+\boldsymbol b_i\bigr),\qquad \boldsymbol o_t=\sigma\bigl(W_o[\boldsymbol h_{t-1},\boldsymbol x_t]+\boldsymbol b_o\bigr),

c~t=tanh⁡(Wc[ht−1,xt]+bc),ct=ft⊙ct−1+it⊙c~t,ht=ot⊙tanh⁡(ct).\tilde{\boldsymbol c}_t=\tanh\bigl(W_c[\boldsymbol h_{t-1},\boldsymbol x_t]+\boldsymbol b_c\bigr),\qquad \boldsymbol c_t=\boldsymbol f_t\odot\boldsymbol c_{t-1}+\boldsymbol i_t\odot\tilde{\boldsymbol c}_t,\qquad \boldsymbol h_t=\boldsymbol o_t\odot\tanh(\boldsymbol c_t).

其中 σ(z)=1/(1+e−z)\sigma(z)=1/(1+e^{-z}) 是门控激活、⊙\odot 是逐元素乘,所有门与状态都是 dhd_h 维向量:三个门负责「放行多少」(逐元素,取值在开区间 (0,1)(0,1) 内),候选 c~t\tilde{\boldsymbol c}_t 负责「写入什么」(取值在 (−1,1)(-1,1) 内)。

状态的分工:ct\boldsymbol c_t 是内部的细胞状态(cell state),承担长期记忆;ht\boldsymbol h_t 是暴露给外部的隐状态,是把细胞状态「读」出来的一部分,供本步的输出层与下一步的门控使用。ct\boldsymbol c_t 不对外输出。

参数的分块形式:把四个矩阵按输入来源切开即 Wf=[Wfh,Wfx]W_f=[W_{fh},W_{fx}](Wfh∈Rdh×dhW_{fh}\in\mathbb R^{d_h\times d_h},Wfx∈Rdh×dxW_{fx}\in\mathbb R^{d_h\times d_x},其余同理)——「拼接后一次矩阵乘」与「两个矩阵分别乘再相加」是同一件事,框架实现取哪种都不改变数学模型。

工程惯例(非数学结论):遗忘门偏置 bf\boldsymbol b_f 常初始化为 11、其余偏置为 00,使初始遗忘门落在 σ(1)≈0.73\sigma(1)\approx0.73 附近,即起步时默认偏向保留,而不是从 σ(0)=0.5\sigma(0)=0.5 起步。

一条传送带,三个阀门各管一件事
  • 遗忘门 ft\boldsymbol f_t 管「要不要留」:逐元素决定旧记忆 ct−1\boldsymbol c_{t-1} 保留多少;
  • 输入门 it\boldsymbol i_t 管「要不要写」:逐元素决定候选 c~t\tilde{\boldsymbol c}_t 写进记忆多少;
  • 输出门 ot\boldsymbol o_t 管「要不要说」:记忆可以留在内部不对外暴露——这一步没说,不代表下一步不能用;
  • 真正改变梯度命运的,是那个不显眼的 ft⊙ct−1+it⊙c~t\boldsymbol f_t\odot\boldsymbol c_{t-1}+\boldsymbol i_t\odot\tilde{\boldsymbol c}_t:加法。它让 c\boldsymbol c 的传递不经过 WW 的连乘,也让「记住多久」变成三个可以学的旋钮。
推导 · 常数误差传送带(constant error carousel)

(i) 直接路径的雅可比。 在 ct=ft⊙ct−1+it⊙c~t\boldsymbol c_t=\boldsymbol f_t\odot\boldsymbol c_{t-1}+\boldsymbol i_t\odot\tilde{\boldsymbol c}_t 中对 ct−1\boldsymbol c_{t-1} 求偏导,把本步的三个门与候选视为已给定的系数、只保留ct−1\boldsymbol c_{t-1} 的直接路径(间接路径见第 (iii) 条):

∂ct∂ct−1=diag⁡(ft).\frac{\partial\boldsymbol c_t}{\partial\boldsymbol c_{t-1}}=\operatorname{diag}\bigl(\boldsymbol f_t\bigr).

(ii) 跨 kk 步的连乘。 对角矩阵之积仍是对角矩阵,且对角元逐个相乘,于是

∂ct∂ct−k=∏j=t−k+1tdiag⁡(fj)=diag⁡(∏j=t−k+1tfj),\frac{\partial\boldsymbol c_t}{\partial\boldsymbol c_{t-k}} =\prod_{j=t-k+1}^{t}\operatorname{diag}\bigl(\boldsymbol f_j\bigr) =\operatorname{diag}\Bigl(\prod_{j=t-k+1}^{t}\boldsymbol f_j\Bigr),

即第 mm 维的传递系数是标量连乘 ∏j(fj)m\prod_j(\boldsymbol f_j)_m,每个因子都由 σ\sigma 产生、严格落在 (0,1)(0,1) 内。

(iii) 三条必须同时记住的结论。

  1. 它不含 WW。 上式里既没有 WhhW_{hh} 也没有 ϕ′\phi':连乘的伸缩率不再由权重矩阵的谱结构决定。而且逐元素因子均在 (0,1)(0,1) 内,这条通道只会衰减、不会放大——「爆炸」在这条路径上被结构性排除。
  2. ft≈1\boldsymbol f_t\approx1 时近似恒等传递。 门值由 σ\sigma 产生、由网络算出来,因此「保留多久」是一个可以学的量,还可以逐维不同、逐步不同。
  3. 但「无损耗」是有条件的。 损失通常经由 ht=ot⊙tanh⁡(ct)\boldsymbol h_t=\boldsymbol o_t\odot\tanh(\boldsymbol c_t) 读出,从 ht\boldsymbol h_t 反传到 ct\boldsymbol c_t 仍要乘 diag⁡(ot)⋅diag⁡(1−tanh⁡2(ct))\operatorname{diag}(\boldsymbol o_t)\cdot\operatorname{diag}(1-\tanh^2(\boldsymbol c_t)):每一次读出都被输出门与 tanh⁡′\tanh' 打一次折。此外完整雅可比还含有经由 ht−1\boldsymbol h_{t-1} 反传到门与候选的间接项,那些项依旧含 WW 的连乘。所以这条定理说的是:存在一条不经过权重连乘的加法通道,而不是「LSTM 无损」。■\blacksquare
∎

下面这张图把这六个量与那条加法通道摆在一起——图顶那条绿色的 c\boldsymbol c 通道,就是上面推导的直接路径:

LSTM 单元:三门与细胞状态的加法通道 LSTM 单元结构:遗忘门 f、输入门 i、输出门 o 为 σ 门控;细胞状态 c 沿顶部加法通道传递(c_t = f⊙c_{t−1} + i⊙c̃);h_t = o⊙tanh(c_t)。 细胞状态 c:一条「梯度高速公路」(绿) cₜ₋₁ cₜ × + f ⊙ cₜ₋₁ [ hₜ₋₁ ; xₜ ] 拼接输入 f = σ(W_f·[h,x]+b_f) 遗忘门 i = σ(W_i·[h,x]+b_i) 输入门 c̃ = tanh(W_c·[h,x]+b_c) 候选记忆 o = σ(W_o·[h,x]+b_o) 输出门 ⊗ i ⊙ c̃ tanh(cₜ) ⊗ o hₜ = o⊙tanh(cₜ) 关键在顶部的加法通道:cₜ = f⊙cₜ₋₁ + i⊙c̃ —— 梯度对 cₜ₋₁ 的雅可比是 f≈1,不随步数连乘衰减
LSTM 单元(本教程绘)。三个 σ 门(金=遗忘 f、蓝=输入 i、紫=输出 o)决定信息去留;顶部绿色的细胞状态 c 是一条加法通道——cₜ = f⊙cₜ₋₁ + i⊙c̃ 中,梯度对 cₜ₋₁ 的雅可比就是 f≈1,连乘不衰减,长程记忆由此而来。

看图时注意两件事:每个 σ\sigma 门背后都是一次 dh×(dh+dx)d_h\times(d_h+d_x) 的矩阵乘(这是参数量的大头,见 2.3.6);而决定梯度命运的,只有顶部那条既没有矩阵、也没有非线性压缩的横线。

定理 2.3.1 · 两条路径的伸缩率对照

设梯度要从 ht\boldsymbol h_t 跨 kk 步传回 ht−k\boldsymbol h_{t-k},比较两条路径的伸缩率:

  • 朴素 RNN:含矩阵连乘 ∏jdiag⁡(ϕ′(zj))Whh\prod_{j}\operatorname{diag}(\phi&#x27;(\boldsymbol z_j))W_{hh}。取 tanh⁡\tanh 时 ∣ϕ′∣≤1|\phi&#x27;|\le1,故其 2-范数上界为 σmax⁡(Whh)k\sigma_{\max}(W_{hh})^{k}(σmax⁡\sigma_{\max} 为最大奇异值):量级由 WhhW_{hh} 的谱结构与 kk 次幂共同决定,σmax⁡>1\sigma_{\max}\gt1 指数爆炸、σmax⁡<1\sigma_{\max}\lt1 指数消失,没有中间地带。
  • LSTM 的细胞通道:含 ∏jdiag⁡(fj)\prod_{j}\operatorname{diag}(\boldsymbol f_j),是逐元素的标量连乘,每个因子都是 sigmoid 输出:∏j(fj)m≤∏jσ(zj,m)<1\prod_j(\boldsymbol f_j)_m\le\prod_j\sigma(z_{j,m})\lt1。它只能衰减,且衰减率可以随输入、随维度自适应。
同一个「kk 次幂」,两种命运

两者的形状都是「kk 次幂」,区别在底数由谁决定、能不能学:RNN 的底数由 WhhW_{hh} 的谱结构定死(ϕ′≤1\phi&#x27;\le1 只是再打个折),而且同一个矩阵必须同时服务「做计算」与「存信息」两件事;LSTM 的底数是逐维、逐步算出来的门值——同一层里第 3 维可以记上千步、第 7 维可以每步清空,网络自己分配预算。伸缩率从「权重的一个性质」变成「一个可学、可条件化的旋钮」,这是门控的全部秘密。

推导 · 标量对照与 σ\sigma 的上界

(i) 标量对照(本书计算)。 把两条路径各退化成标量,比较跨 kk 步的传递系数:

路径 跨 kk 步系数 k=30k=30 k=100k=100
RNN,w=1.05w=1.05 wkw^{k} 4.324.32 1.3×1021.3\times10^{2}
RNN,w=0.95w=0.95 wkw^{k} 2.1×10−12.1\times10^{-1} 5.9×10−35.9\times10^{-3}
LSTM,f≡σ(1)=0.731f\equiv\sigma(1)=0.731(偏置 1 的起手值) fkf^{k} 8.3×10−58.3\times10^{-5} 2.5×10−142.5\times10^{-14}
LSTM,f≡0.99f\equiv0.99(学到接近 1) fkf^{k} 7.4×10−17.4\times10^{-1} 3.7×10−13.7\times10^{-1}

前两行说明 RNN 的困境:同一条路径,ww 偏大就爆、偏小就灭,而 ww 是训练要学的量、不可能长期停在临界点。第三行说明偏置 1 只是起点:σ(1)=0.731\sigma(1)=0.731 仍然衰减得很快(30 步 8 万分之一),它买来的是「相对 0.50.5 起步的起手优势」——同样的 30 步,0.731300.731^{30} 与 0.530=9.3×10−100.5^{30}=9.3\times10^{-10} 差了约 9×1049\times10^{4} 倍。第四行说明真正的解法是让门值被学上去:f→0.99f\to0.99 时 100 步仍保留 37%37\%。

(ii) ft\boldsymbol f_t 可以学到接近 11,但 sigmoid 永远取不到 11。 因 σ(z)∈(0,1)\sigma(z)\in(0,1) 严格成立,写 (ft)m=1−γm(\boldsymbol f_t)_m=1-\gamma_m 总有 γm>0\gamma_m\gt0。设每步衰减同为 γ\gamma,则 fk=(1−γ)k≈e−γkf^{k}=(1-\gamma)^{k}\approx e^{-\gamma k};要跨 kk 步仍保留 1−ϵ1-\epsilon 以上,需要 γk≲ϵ\gamma k\lesssim\epsilon,即

γ≲ϵk⟹z≳ln⁡kϵ(大 z 时 γ≈e−z).\gamma\lesssim\frac{\epsilon}{k}\quad\Longrightarrow\quad z\gtrsim\ln\frac{k}{\epsilon}\qquad(\text{大 }z\text{ 时 }\gamma\approx e^{-z}).

取 k=100k=100、ϵ=0.1\epsilon=0.1:需要门预激活 z≳ln⁡1000≈6.9z\gtrsim\ln1000\approx6.9。也就是说,「记住 100 步」这件事在遗忘门上要求预激活大到约 77 的量级,远大于偏置 1 的起手值——它必须靠训练把某些维推上去。而塑造这些门值的梯度本身又要穿过同一条被门值缩放的通道,这是一种「先有鸡还是先有蛋」的耦合(本书判断),也是长程依赖在 LSTM 上依然困难的算术原因。

(iii) 矩阵情形多出来的两件事。 多维时 RNN 的伸缩率是矩阵的:diag⁡(ϕ′)Whh\operatorname{diag}(\phi&#x27;)W_{hh} 的奇异值给出各方向的伸缩,且方向之间会混合(WhhW_{hh} 的非对角元把不同维搅在一起);LSTM 的直接路径是对角的:维度之间不混合,每个维度各有一条彼此独立的衰减率。■\blacksquare

∎

2.3.3 GRU:把三个门压成两个

定义 2.3.2 · GRU(gated recurrent unit)

输入仍是拼接 [ht−1,xt][\boldsymbol h_{t-1},\boldsymbol x_t],但只算两个门与一个候选:

zt=σ(Wz[ht−1,xt]+bz),rt=σ(Wr[ht−1,xt]+br),\boldsymbol z_t=\sigma\bigl(W_z[\boldsymbol h_{t-1},\boldsymbol x_t]+\boldsymbol b_z\bigr),\qquad \boldsymbol r_t=\sigma\bigl(W_r[\boldsymbol h_{t-1},\boldsymbol x_t]+\boldsymbol b_r\bigr),

h~t=tanh⁡(Wh[rt⊙ht−1,xt]+bh),ht=(1−zt)⊙ht−1+zt⊙h~t.\tilde{\boldsymbol h}_t=\tanh\bigl(W_h[\boldsymbol r_t\odot\boldsymbol h_{t-1},\boldsymbol x_t]+\boldsymbol b_h\bigr),\qquad \boldsymbol h_t=(1-\boldsymbol z_t)\odot\boldsymbol h_{t-1}+\boldsymbol z_t\odot\tilde{\boldsymbol h}_t .

更新门 zt\boldsymbol z_t 在「旧状态」与「新候选」之间做逐元素插值;重置门 rt\boldsymbol r_t 决定算候选时忽略多少旧状态(rt→0\boldsymbol r_t\to0 时候选只由 xt\boldsymbol x_t 决定,相当于「从头起写」,与后面要不要留无关)。

GRU 是一个「没有 c\boldsymbol c」的 LSTM

把两条更新式并排看:LSTM 用 ct=ft⊙ct−1+it⊙c~t\boldsymbol c_t=\boldsymbol f_t\odot\boldsymbol c_{t-1}+\boldsymbol i_t\odot\tilde{\boldsymbol c}_t 分两步完成「留下旧的 + 写入新的」;GRU 用一条插值 ht=(1−zt)⊙ht−1+zt⊙h~t\boldsymbol h_t=(1-\boldsymbol z_t)\odot\boldsymbol h_{t-1}+\boldsymbol z_t\odot\tilde{\boldsymbol h}_t 一次写完:zt\boldsymbol z_t 同时是「写入多少」和「保留多少」——两个量被绑成一个(这正是 2.3.4 里「耦合门」的思路)。它的加法通道对应因子是 diag⁡(1−zt)\operatorname{diag}(1-\boldsymbol z_t),与 LSTM 的 diag⁡(ft)\operatorname{diag}(\boldsymbol f_t) 地位相同。

代价也随之而来:GRU 没有独立暴露的细胞状态——同一份 ht\boldsymbol h_t 既当记忆又当输出,因此少了「内部 c\boldsymbol c 记着、对外 h\boldsymbol h 不说」这个自由度(少一组参数、少一层保护)。此外注意候选分支:∂h~t/∂ht−1\partial\tilde{\boldsymbol h}_t/\partial\boldsymbol h_{t-1} 里仍含 WW 的连乘,门控只保护那条直通项——这一点 LSTM 也一样。

2.3.4 变体:peephole 与「谁真正有用」

定义 2.3.3 · peephole 连接与耦合门
  • peephole 连接(窥孔):把细胞状态本身接进门控的输入,例如遗忘门改为

ft=σ(Wfhht−1+Wfxxt+wfc⊙ct−1+bf),\boldsymbol f_t=\sigma\bigl(W_{fh}\boldsymbol h_{t-1}+W_{fx}\boldsymbol x_t+\boldsymbol w_{fc}\odot\boldsymbol c_{t-1}+\boldsymbol b_f\bigr),

让「记忆里现在有什么」直接参与决定「下一步留多少」;输入门与输出门也可以同样接入 ct−1\boldsymbol c_{t-1}(或 ct\boldsymbol c_t)。

  • 耦合门(coupled gates):强制 ft=1−it\boldsymbol f_t=1-\boldsymbol i_t,把两个门压成一组参数——「写多少」与「留多少」互补,与 GRU 的插值同构。
一个在加通路,一个在减自由度

peephole 增加的是信息通路(门能直接读到 c\boldsymbol c 本身,而不只读到 h\boldsymbol h);耦合门减少的是自由度与参数(f\boldsymbol f 不再独立)。一加一减,但目标相同:改变「门看到什么」与「门由谁决定」。

门越多越好吗?——两次大规模对照给出的答案

论文报告:Greff 等(2015)在若干任务上系统对照了 8 种 LSTM 变体(去掉输入门 / 去掉遗忘门 / 去掉输出门 / 去掉门的激活函数 / 耦合门 / peephole 等,具体清单以原文为准),结论是各变体之间的差别整体并不显著——删掉一两个门通常不会让效果明显变差;被反复确认真正重要的只有一条:保留遗忘门,并给它合适的偏置。Jozefowicz 等(2015)在语言建模上检索了一万余种循环网络架构,报告确实存在优于标准 LSTM 的变体,但架构改动带来的收益整体小于超参与初始化带来的差异,而「遗忘门偏置初始化为 1 的 LSTM」始终是稳妥的默认。

本书判断:这两份对照合起来给出一条工程纪律——先把标准 LSTM(或 GRU)配好超参、跑成 baseline,再考虑换变体;在换变体之前,先确认你的瓶颈是「记忆时长」而不是学习率、初始化、数据量或归一化。反过来,「门越多、结构越花哨越强」在文献对照中没有得到支持。

2.3.5 双向与堆叠:把门控单元组合成编码器

定义 2.3.4 · 双向 RNN 与残差堆叠

双向 RNN(bidirectional RNN):用两套独立参数在同一序列上分别前向与后向递推,再把两个方向拼起来:

h→t=ffwd(xt,h→t−1),h←t=fbwd(xt,h←t+1),ht=[h→t;h←t]∈R2dh.\overrightarrow{\boldsymbol h}_t=f_{\text{fwd}}\bigl(\boldsymbol x_t,\overrightarrow{\boldsymbol h}_{t-1}\bigr),\qquad \overleftarrow{\boldsymbol h}_t=f_{\text{bwd}}\bigl(\boldsymbol x_t,\overleftarrow{\boldsymbol h}_{t+1}\bigr),\qquad \boldsymbol h_t=\bigl[\overrightarrow{\boldsymbol h}_t;\overleftarrow{\boldsymbol h}_t\bigr]\in\mathbb R^{2d_h}.

多层堆叠(stacking):第 ll 层的输入是第 l−1l-1 层的输出序列 H(l−1)∈RB×T×dhH^{(l-1)}\in\mathbb R^{B\times T\times d_h},即 H(l)=RNN(l)(H(l−1))H^{(l)}=\mathrm{RNN}^{(l)}(H^{(l-1)}),逐层加高;层数多时可用残差连接把每层写成 H(l)=H(l−1)+F(l)(H(l−1))H^{(l)}=H^{(l-1)}+\mathcal F^{(l)}(H^{(l-1)}),给梯度一条不经过非线性与权重连乘的短路(思想来自 ResNet,He 等 2015)。

一个看过去、一个看未来;代价是「必须等整句到手」

双向让同一个位置同时拿到左右两份上下文,因此对「标注 / 编码整句」类任务(词性标注、命名实体识别、句子编码)特别有效。

但这也写死了它的适用范围:后向递推要求 xT\boldsymbol x_T 先到齐,所以双向必须看到整句,不能用于自回归生成、也不能用于任何因果场景——生成第 tt 个 token 时未来的 token 还不存在;若训练时用双向编码器「偷看」了右边的真实 token,训练损失会很漂亮,一旦改为逐 token 生成就立刻崩坏(2.8 的生成流程会把这个不一致暴露出来)。同理,流式 / 在线场景(每来一个新样本就要立刻输出)也不能直接用双向,除非接受固定延迟。

2.3.6 形状与参数量

例 2.3.1 · 三种循环单元的参数量对比

取输入维 dx=32d_x=32、隐状态维 dh=128d_h=128,只统计循环单元本身(不含输出层——三者共用同一个输出投影 Why∈Rdy×dh\boldsymbol W_{hy}\in\mathbb R^{d_y\times d_h},不影响倍数)。分别求朴素 RNN、GRU、LSTM 的参数量(含偏置),以及各自相对朴素 RNN 的倍数。

解

(0) 先记一个「基准块」。 三者都是由若干组「输入投影 + 循环投影 + 偏置」构成,一组的大小是

dh dx+dh dh+dh=128×32+128×128+128=4096+16,384+128=20,608.d_h\,d_x+d_h\,d_h+d_h=128\times32+128\times128+128=4096+16{,}384+128=20{,}608 .

(1) 朴素 RNN:只有一组(Wxh,Whh,bhW_{xh},W_{hh},\boldsymbol b_h),即 1×20,608=20,6081\times20{,}608=20{,}608。

(2) GRU:三组(zt\boldsymbol z_t 一组、rt\boldsymbol r_t 一组、候选 h~t\tilde{\boldsymbol h}_t 一组;候选那组同样以 [ht−1,xt][\boldsymbol h_{t-1},\boldsymbol x_t] 为输入,故块大小相同,重置门只改变输入的值、不改变形状):

3×20,608=61,824,61,82420,608=3.00.3\times20{,}608=61{,}824,\qquad \frac{61{,}824}{20{,}608}=3.00 .

(3) LSTM:四组(ft,it,ot,c~t\boldsymbol f_t,\boldsymbol i_t,\boldsymbol o_t,\tilde{\boldsymbol c}_t 各一组),4×20,608=82,4324\times20{,}608=82{,}432,倍数 4.004.00。

(4) 与框架实现对账。 用 nn.RNN / nn.GRU / nn.LSTM(32, 128) 直接 sum(p.numel() for p in m.parameters()),会看到略大的数:实现里每个门同时挂 bias_ih 与 bias_hh 两组偏置(各 dhd_h),于是 (20,608+128)×{1,3,4}={20,736, 62,208, 82,944}(20{,}608+128)\times\{1,3,4\}=\{20{,}736,\ 62{,}208,\ 82{,}944\}——倍数仍是 33 与 44,差别只在偏置项,不影响结论(具体数值以你所用版本的实现为准)。

读法:门控不是免费的——LSTM 的参数量是朴素 RNN 的 44 倍、GRU 是 33 倍;相对 GRU,LSTM 多出的那一组参数买到的正是「记忆与输出分离」这个自由度。■\blacksquare

∎
形状与参数量对照表

本书统一「批优先」(batch-first):输入 X\boldsymbol X 形状 (B,T,dx)(B,T,d_x),BB 为批大小、TT 为时间步。单步的形状推演是:X\boldsymbol X 沿 TT 切片得 xt:(B,dx)\boldsymbol x_t:(B,d_x) → 拼接 [ht−1,xt]:(B,dh+dx)[\boldsymbol h_{t-1},\boldsymbol x_t]:(B,d_h+d_x) → 四个门各 (B,dh)(B,d_h) → ct,ht:(B,dh)\boldsymbol c_t,\boldsymbol h_t:(B,d_h) → 逐步堆叠输出 (B,T,dh)(B,T,d_h)。TT 个时间步共享同一个 WfW_f,因此参数量与 TT 无关——这正是 2.1 节「参数不随序列长度变化」的要求。

量 形状 说明
输入 X\boldsymbol X (B,T,dx)(B,T,d_x) batch_first=True
单元状态(RNN / GRU) (B,dh)(B,d_h) 只有一个;h0\boldsymbol h_0 常取零向量
单元状态(LSTM) (B,dh)(B,d_h) 两组 (ht,ct)(\boldsymbol h_t,\boldsymbol c_t) 成对出现,跨步都要传
逐时间步输出 (B,T,dh)(B,T,d_h),双向为 (B,T,2dh)(B,T,2d_h) nn.LSTM 的第一个返回值
h_n / c_n (num_layers×num_directions, B, dh)(\text{num\_layers}\times\text{num\_directions},\ B,\ d_h) 只有最后一个时间步的状态;c_n 仅 LSTM 有
单元 参数量(含单组偏置) 相对倍数 dx=32, dh=128d_x=32,\ d_h=128
朴素 RNN dh(dx+dh)+dhd_h(d_x+d_h)+d_h 1×1\times 20,60820{,}608
GRU 3[dh(dx+dh)+dh]3\bigl[d_h(d_x+d_h)+d_h\bigr] 3×3\times 61,82461{,}824
LSTM 4[dh(dx+dh)+dh]4\bigl[d_h(d_x+d_h)+d_h\bigr] 4×4\times 82,43282{,}432

多层与双向的推广:层数 LL、方向数 D∈{1,2}D\in\{1,2\} 时,参数量约为单层的 L×DL\times D 倍——这是个近似,因为只有第一层的输入维是 dxd_x、其后每层是 D dhD\,d_h,所以 L>1L\gt1 时该估计会有偏差(首层偏小或偏大取决于 dxd_x 与 D dhD\,d_h 的大小关系);状态形状里的 num_layers×num_directions\text{num\_layers}\times\text{num\_directions} 就是 L×DL\times D。

2.3.7 代码:手写 LSTM 单元并与 nn.LSTM 对齐

import torch, torch.nn as nn

torch.manual_seed(0)
d_x, d_h, B = 8, 16, 4

class LSTMCellManual(nn.Module):
    """手写 LSTM 单元;门序与 PyTorch 一致:i, f, g, o"""
    def __init__(self, d_x, d_h):
        super().__init__()
        # 一个 Linear 同时接收 [x_t, h_{t-1}]:等价于把 W_i|W_f|W_g|W_o 四块拼起来(定义 2.3.1 的分块形式)
        self.W = nn.Linear(d_x + d_h, 4 * d_h)

    def forward(self, x, state):
        h, c = state
        z = self.W(torch.cat([x, h], dim=-1))            # (B, 4*d_h)
        i, f, g, o = z.chunk(4, dim=-1)                  # 关键:chunk 顺序必须是 i, f, g, o
        i, f, o = torch.sigmoid(i), torch.sigmoid(f), torch.sigmoid(o)  # 三个门取值 (0,1)
        g = torch.tanh(g)                                # 候选可以写「负」内容,故用 tanh 而非 sigmoid
        c_new = f * c + i * g                            # 加法通道:不经过 W,梯度不必连乘矩阵
        h_new = o * torch.tanh(c_new)                    # 输出门:记忆可以留在内部「不说出来」
        return h_new, (h_new, c_new), f                  # f 只为观察,不参与接口对齐

# ① 与 nn.LSTMCell 数值对齐:把参考实现的两组参数搬进同一个 Linear
ref, mine = nn.LSTMCell(d_x, d_h), LSTMCellManual(d_x, d_h)
with torch.no_grad():
    mine.W.weight.copy_(torch.cat([ref.weight_ih, ref.weight_hh], dim=1))  # 列方向拼接 = [x|h]
    mine.W.bias.copy_(ref.bias_ih + ref.bias_hh)                           # 两组偏置数学上合成一组

x  = torch.randn(B, d_x)
st = (torch.randn(B, d_h), torch.randn(B, d_h))      # 用随机初态:非零状态也必须对得上
with torch.no_grad():
    h_ref, c_ref = ref(x, st)
    h_my, (_, c_my), _ = mine(x, st)
print("max|Δh| =", (h_ref - h_my).abs().max().item(),
      " max|Δc| =", (c_ref - c_my).abs().max().item())
assert torch.allclose(h_ref, h_my, atol=1e-5) and torch.allclose(c_ref, c_my, atol=1e-5)

# ② 顺手把遗忘门偏置设为 1(定义 2.3.1 提过的工程惯例):偏置顺序是 i|f|g|o,故取第 2 段
with torch.no_grad():
    mine.W.bias[d_h:2 * d_h].fill_(1.0)              # 注意:此后 mine 与 ref 的偏置不再相同(① 已完成)

# ③ 观察遗忘门:小任务要求「每一步都报出第 1 步输入的第 0 维」——要做到就得记住很久
T, readout = 20, nn.Linear(d_h, 1)
opt = torch.optim.Adam(list(mine.parameters()) + list(readout.parameters()), lr=5e-3)

for step in range(1, 401):
    xs  = torch.randn(B, T, d_x)
    tgt = xs[:, 0, 0:1].unsqueeze(1).expand(B, T, 1)  # 整段都要回忆第 1 步:逼着门学「别忘」
    h, c, fs, preds = torch.zeros(B, d_h), torch.zeros(B, d_h), [], []
    for t in range(T):                               # 时间维只能串行——这一步依赖上一步(见 2.3.8)
        h, (h, c), f = mine(xs[:, t], (h, c))
        fs.append(f.detach().mean())                 # 只记录数值,切断反传
        preds.append(readout(h))
    loss = nn.functional.mse_loss(torch.stack(preds, dim=1), tgt)
    loss.backward(); opt.step(); opt.zero_grad()     # 每个 batch 从零状态起步,故无需 detach
    if step % 100 == 0:
        print(f"step {step:3d}  loss {loss.item():.4f}  mean(f) {torch.stack(fs).mean():.3f}")

两个观察点。一是 loss:数值对齐(max|Δh| 约 6×10−86\times10^{-8})已经证明这个手写单元与框架实现是同一个函数,「能训练」是定义的推论而非巧合。二是 mean(f):它是训练出来的量,不是常数。本书在这一设定下的一次运行(种子 0)中,它从 0.7450.745 缓慢爬到 0.8130.813,而 loss 从 11 上下落到 0.260.26——门被训练推着走,但并没有趋近 1:本任务只要求记住 2020 步,门值不需要更大;把 ② 那段去掉(不做偏置初始化)时,起点回落到约 0.50.5,跑满同样 400 步也只爬到约 0.580.58——初始化的作用,就是替门挑一个工作点。要带走的是定理 2.3.1 的算术:门值离 11 有多远,决定了它能记多久;具体数值随随机种子与超参而变,你的运行结果不必与这里相同。

2.3.8 陷阱与勘误

本节五个常见误解
  1. 「门控根治了梯度消失」:只是缓解,而且有三处代价同时存在——① 读出路径 ht=ot⊙tanh⁡(ct)\boldsymbol h_t=\boldsymbol o_t\odot\tanh(\boldsymbol c_t) 每次都要被 ot\boldsymbol o_t 与 tanh⁡′\tanh&#x27; 打折;② 完整雅可比里经由 ht−1\boldsymbol h_{t-1} 反传到门与候选的间接项仍含 WW 的连乘;③ 遗忘门严格小于 11,衰减仍按步数累积(0.99100≈0.370.99^{100}\approx0.37,0.99700≈8.8×10−40.99^{700}\approx8.8\times10^{-4})。判据:训练时按距离(时间差)统计梯度范数或门均值,看长程梯度是否仍随距离衰减。
  2. 「LSTM 训练可以并行」:不能。状态沿时间串行依赖,前向要一步步推进、反向(BPTT)要一步步回退;能并行的是批维 BB、特征维与同一层内各步矩阵乘的批量下发(cuDNN 把逐 step 的 GEMM 调用批量化,但并不消除依赖链)。要让时间维并行,必须换掉递推本身——那是 2.4 及之后的主题。
  3. 「ht\boldsymbol h_t 是 LSTM 的唯一状态」:LSTM 的状态是一对 (ht,ct)(\boldsymbol h_t,\boldsymbol c_t)。框架接口也这么写:nn.LSTM 返回 output(只含各步 h\boldsymbol h)与 (h_n, c_n);c_n 不会出现在 output 里。做状态传递或从检查点恢复时,两个都要带上——这也是 nn.GRU 只有一个状态而 nn.LSTM 有两个的原因。
  4. 「双向可以用在生成任务上」:不能。后向递推要求先看到整句,生成第 tt 个 token 时未来还不存在;训练期「偷看未来」会让离线指标虚高、部署时崩坏(见 2.3.5)。
  5. 「状态跨 batch 传不传都行」(两种典型 bug):若把上一步的状态直接带进下一个 batch,等于让 BPTT 跨过 batch 边界——计算图会跨迭代累积(显存持续增长,乃至 backward 报「再次反向同一张图」),你以为在做截断 BPTT,实际做的是「跨 batch 的完整 BPTT」。正确写法只有两种:每 batch 把状态重置为零(最常见),或显式 h = h.detach()(LSTM 还要 c = c.detach())后传递状态值——两者语义不同,必须明确选一个。
即时自测 · 2.3

Q1 写出 LSTM 的细胞状态更新式,并说明 ∂ct/∂ct−1\partial\boldsymbol c_t/\partial\boldsymbol c_{t-1} 为什么不含 WW,以及它「只能衰减、不会放大」的理由。

Q2(算参数量)dx=64d_x=64、dh=256d_h=256 时,GRU 与 LSTM 各有多少参数(单组偏置)?各自是朴素 RNN 的几倍?

Q3 为什么说「门控只是缓解、不是根治」?给出至少两条理由,并各配一个可观测的判据。

Q4 GRU 与 LSTM 的加法通道分别对应哪个因子?二者在「自由度」上的差别是什么?

参考要点

Q1 ct=ft⊙ct−1+it⊙c~t\boldsymbol c_t=\boldsymbol f_t\odot\boldsymbol c_{t-1}+\boldsymbol i_t\odot\tilde{\boldsymbol c}_t;直接路径下 ∂ct/∂ct−1=diag⁡(ft)\partial\boldsymbol c_t/\partial\boldsymbol c_{t-1}=\operatorname{diag}(\boldsymbol f_t)——该项里只有本步门值,没有 WhhW_{hh},也没有 ϕ′\phi&#x27;。跨 kk 步是对角矩阵连乘 =diag⁡(∏jfj)=\operatorname{diag}(\prod_j\boldsymbol f_j),对角元是标量连乘,每个因子 ∈(0,1)\in(0,1),故整条路径的传递系数的绝对值不会超过 11:只能衰减,不可能放大(爆炸在这条路径上被排除)。

Q2 基准块 =256×64+256×256+256=16,384+65,536+256=82,176=256\times64+256\times256+256=16{,}384+65{,}536+256=82{,}176。GRU =3×82,176=246,528=3\times82{,}176=246{,}528(33 倍);LSTM =4×82,176=328,704=4\times82{,}176=328{,}704(44 倍);朴素 RNN 为 82,17682{,}176。(框架实现里每个门多挂一组 bias_hh,数值略大,但倍数不变。)

Q3 ① 读出路径被打折:损失经 ht=ot⊙tanh⁡(ct)\boldsymbol h_t=\boldsymbol o_t\odot\tanh(\boldsymbol c_t) 读出,每步都乘 diag⁡(ot)diag⁡(1−tanh⁡2(ct))\operatorname{diag}(\boldsymbol o_t)\operatorname{diag}(1-\tanh^2(\boldsymbol c_t))——判据:固定时间差绘制梯度范数,若随距离单调衰减说明读出/间接项在起作用。② 门值严格小于 11:f≡0.99f\equiv0.99 时 100 步只剩 37%37\%——判据:打印遗忘门均值是否能被学到 0.990.99 以上,算一算 ∏f\prod f 还剩多少。③ 间接项含 WW:完整雅可比不只是 diag⁡(ft)\operatorname{diag}(\boldsymbol f_t)——判据:把门输入梯度与细胞路径梯度分开统计,看后者是否也有非零贡献。

Q4 GRU 是 diag⁡(1−zt)\operatorname{diag}(1-\boldsymbol z_t),LSTM 是 diag⁡(ft)\operatorname{diag}(\boldsymbol f_t)。自由度差别:GRU 用一个门同时决定「写入」与「保留」(两者互补,和为 1),且状态 ht\boldsymbol h_t 既当记忆又当输出;LSTM 有两个独立的门(it\boldsymbol i_t 与 ft\boldsymbol f_t 互不约束),并且 (ct,ht)(\boldsymbol c_t,\boldsymbol h_t) 分离——记忆可以存在而不对外暴露。这是 LSTM 多出的那一组参数买到的东西。

延伸阅读
  • Hochreiter & Schmidhuber, Long Short-Term Memory(1997, Neural Computation 9(8):1735–1780)——原论文,读「常数误差传送带」与门控动机的原始论证。
  • Cho 等, Learning Phrase Representations using RNN Encoder–Decoder(2014, arXiv:1406.1078)——GRU 的提出与它作为编码器—解码器的应用。
  • Greff 等, LSTM: A Search Space Odyssey(2015, arXiv:1503.04069)——8 种 LSTM 变体的大范围对照,读它的结论表与「哪一部件重要」的排序。
  • Jozefowicz 等, An Empirical Exploration of Recurrent Network Architectures(2015, ICML)——一万余种循环架构的经验检索,读它对「架构 vs 超参」的相对贡献的讨论。
  • Pascanu, Mikolov & Bengio, On the Difficulty of Training Recurrent Neural Networks(2013, arXiv:1211.5063)——连乘雅可比、爆炸与裁剪的分析,与 2.2 合读。
  • Olah, Understanding LSTM Networks(2015, colah.github.io)——用图理解三门与细胞状态的入门读物,配合本节插图看。

本节小结:门控把「梯度怎么穿过很多时间步」这个问题从乘法换成了加法——ct=ft⊙ct−1+it⊙c~t\boldsymbol c_t=\boldsymbol f_t\odot\boldsymbol c_{t-1}+\boldsymbol i_t\odot\tilde{\boldsymbol c}_t 的直接路径雅可比是 diag⁡(ft)\operatorname{diag}(\boldsymbol f_t),不含 WW 的连乘,且「保留多久」成为可学、可条件化的门值;GRU 把三个门压成两个,用插值一次完成遗忘与写入。代价有两条:参数变为朴素 RNN 的 3∼43\sim4 倍,且训练仍沿时间串行——门控没有解决并行问题,也没有根治衰减(σ\sigma 永远取不到 11)。还有一个更基础的瓶颈没被动过:无论 LSTM 还是 GRU,历史最终都要压进定长的向量 ht\boldsymbol h_t。2.4 节的注意力正是来拆这个瓶颈的:不再压缩,而是让每个位置直接回看所有位置。