一
前面两章的网络都假设输入是一次性、固定大小的(一张图、一个向量)。但世界上大量数据是序列:一句话是词的序列,一段语音是声学帧的序列,一支股票是价格的序列。序列有两个特点让前馈网络无能为力——长度可变,且当前的理解依赖之前的内容。“他把行李放进了___”这个空,要填什么取决于前文。
循环神经网络(Recurrent Neural Network, RNN)的想法直截了当:给网络一个隐藏状态 当作“记忆”,每读入一个新元素 t,就用上一刻的记忆 {t-1} 和当前输入一起算出新的记忆:
t = \tanh!\left(W{hh},\mathbf{h}{t-1} + W{xh},\mathbf{x}_t + \mathbf{b}\right)
t = W{hy},\mathbf{h}_t
注意一个关键细节:每个时间步用的是同一组权重 ——这和 CNN 的权重共享异曲同工,只不过 CNN 在空间上共享,RNN 在时间上共享。把这个循环按时间“展开”(unroll),它就变成一个深度等于序列长度的前馈网络:每个时间步是一层,层与层之间共享权重,记忆 像一条河流从第一个时间步流到最后一个。
训练 RNN 用的还是反向传播,只不过要沿着时间轴反向传,叫随时间反向传播(Backpropagation Through Time, BPTT)。误差从最后一个时间步出发,沿着隐藏状态的链条一步步往回流。问题就出在这条往回流的链条上。
二
1991 年,Sepp Hochreiter 在他的硕士学位论文《动态神经网络研究》(Untersuchungen zu dynamischen neuronalen Netzen)里,第一次对一个困扰循环网络的现象给出了正式的数学分析。这就是后来被称为梯度消失问题(vanishing gradient problem)的东西,Schmidhuber 称之为“深度学习的根本问题”1。
机制是这样的。误差沿时间反传时,从时间步 传到时间步 ,要连续乘过 个雅可比矩阵——每跨一个时间步,梯度就要乘一次 ,再乘一次激活函数的导数 。把这些乘子的“有效大小”记为 ,那么传播 个时间步后,梯度的量级大约正比于 。
- 如果 , 随 指数衰减到零——这是梯度消失。远处时间步传来的误差信号在到达近处之前就衰减没了,网络学不到长程依赖。
- 如果 , 随 指数爆炸——这是梯度爆炸,训练直接发散。
Hochreiter 证明的正是这一点:BPTT 的梯度会随跨越的时间步数(或层数)呈指数衰减12。Bengio 等人 1994 年也从另一个角度论证了用梯度方法学习长程依赖的根本困难。这个分析的杀伤力在于:它说明 RNN 记不住远处信息,不是没训练够,而是优化机制本身的数学性质决定的——就像第 01 章感知机学不会 XOR 是表达力的硬限制一样,这是训练动力学的硬限制。
把它跑出来看最直观(配套代码 code/04_lstm_cell.py):
展开代码 · 04_lstm_cell.py
"""
第 04 章配套代码:(1) 演示 RNN 的梯度消失/爆炸 (2) 一个 LSTM cell 的前向
Runnable with: numpy only. python3 04_lstm_cell.py
"""
import numpy as np
def vanishing_gradient_demo():
"""简化的 BPTT 梯度连乘:grad ~ prod_t (W * sigma'(.))。
若有效乘子 |lambda| < 1,梯度随时间步指数衰减 -> 消失;>1 -> 爆炸。
"""
print("=== 梯度消失/爆炸(BPTT 连乘)===")
for lam, name in [(0.6, "衰减(消失)"), (1.0, "临界"), (1.3, "增长(爆炸)")]:
for T in [5, 20, 50]:
grad = lam ** T
print(f" 乘子={lam} [{name}] T={T:2d} 步后梯度 ~ {grad:.3e}")
print()
def sigmoid(x): return 1 / (1 + np.exp(-x))
def lstm_step(x, h_prev, c_prev, Wf, Wi, Wc, Wo, bf, bi, bc, bo):
"""单个时间步的 LSTM。z = [h_prev; x] 拼接。
遗忘门 f, 输入门 i, 候选 g, 输出门 o:
f = σ(Wf z + bf) i = σ(Wi z + bi)
g = tanh(Wc z + bc) o = σ(Wo z + bo)
c = f ⊙ c_prev + i ⊙ g (Constant Error Carousel: 关键的加法更新)
h = o ⊙ tanh(c)
"""
z = np.concatenate([h_prev, x])
f = sigmoid(Wf @ z + bf)
i = sigmoid(Wi @ z + bi)
g = np.tanh(Wc @ z + bc)
o = sigmoid(Wo @ z + bo)
c = f * c_prev + i * g # 细胞状态:门控的加法传递 => 误差可几乎无衰减地流过
h = o * np.tanh(c)
return h, c, dict(f=f, i=i, g=g, o=o)
if __name__ == "__main__":
vanishing_gradient_demo()
print("=== LSTM cell 前向(hidden=3, input=2)===")
rng = np.random.default_rng(0)
H, D = 3, 2
Wf, Wi, Wc, Wo = [rng.normal(0, .3, (H, H + D)) for _ in range(4)]
bf = np.ones(H) # 遗忘门偏置初始化为正:默认"记住"
bi, bc, bo = [np.zeros(H) for _ in range(3)]
h, c = np.zeros(H), np.zeros(H)
seq = [np.array([1., 0.]), np.array([0., 1.]), np.array([1., 1.])]
for t, x in enumerate(seq):
h, c, gates = lstm_step(x, h, c, Wf, Wi, Wc, Wo, bf, bi, bc, bo)
print(f" t={t} 遗忘门均值={gates['f'].mean():.2f} 输入门均值={gates['i'].mean():.2f} "
f"c={np.round(c,3)} h={np.round(h,3)}")
乘子=0.6 [衰减] T=5 步后梯度 ~ 7.8e-02 T=20 步 ~ 3.7e-05 T=50 步 ~ 8.1e-12
乘子=1.0 [临界] T=5 ~ 1.0 T=50 ~ 1.0
乘子=1.3 [爆炸] T=5 ~ 3.7 T=50 步后梯度 ~ 5.0e+05
乘子只要 0.6,传过 50 个时间步后梯度就只剩 ——第一个词对第五十个词的训练信号,实际上等于零。这就是为什么朴素 RNN 记不住长句子的开头。
三
梯度爆炸相对好治——直接给梯度设一个上限“裁剪”(gradient clipping)即可。真正难的是梯度消失。Hochreiter 和他的导师 Schmidhuber 给出的解法,是 1997 年发表的长短期记忆网络(Long Short-Term Memory, LSTM)3。
LSTM 的核心洞察可以一句话概括:与其让记忆每一步都被一个会衰减的乘法变换碾过,不如开辟一条让记忆能近乎无损地直接流过去的“高速公路”,再用几道闸门精确控制什么时候写入、什么时候擦除、什么时候读出。
这条高速公路就是 LSTM 引入的第二条状态——细胞状态(cell state),与隐藏状态 并行。Hochreiter 称这条让误差恒定流动的通路为恒定误差传送带(Constant Error Carousel, CEC)3。它的关键在于细胞状态的更新主要是加法而非反复的矩阵乘法——加法的梯度是 1,不会指数衰减,于是误差可以沿着细胞状态这条线几乎无衰减地传回很远的过去。
控制这条传送带的是三道门(gate),每道门都是一个取值在 0 到 1 之间的 sigmoid 输出,像阀门一样逐元素地调节信息流。记 t = [\mathbf{h}{t-1}; \mathbf{x}_t] 为上一刻隐藏状态与当前输入的拼接:
细胞状态的更新——LSTM 的心脏:
t \odot \mathbf{c}{t-1} + \mathbf{i}_t \odot \tilde{\mathbf{c}}_t
读这个式子:遗忘门 t 逐元素地决定旧细胞状态 {t-1} 保留多少(接近 1 = 几乎全留,接近 0 = 擦除);输入门 决定候选记忆 写入多少。当遗忘门接近 1、输入门接近 0 时,t \approx \mathbf{c}{t-1}——记忆原封不动地传到下一步,误差也就原封不动地传回上一步。这就是 CEC 让长程梯度存活的数学原因。
最后,隐藏状态(也是对外的输出)由输出门过滤后的细胞状态给出:
四
把一个 LSTM cell 跑起来,看门控如何工作(code/04_lstm_cell.py,可运行):
def lstm_step(x, h_prev, c_prev, Wf, Wi, Wc, Wo, bf, bi, bc, bo):
z = np.concatenate([h_prev, x])
f = sigmoid(Wf @ z + bf) # 遗忘门
i = sigmoid(Wi @ z + bi) # 输入门
g = np.tanh(Wc @ z + bc) # 候选记忆
o = sigmoid(Wo @ z + bo) # 输出门
c = f * c_prev + i * g # 细胞状态:门控加法更新(CEC)
h = o * np.tanh(c)
return h, c
一个常用且重要的工程技巧体现在初始化里:遗忘门的偏置 初始化为正值(代码里设为 1)。这让训练初期遗忘门默认接近 1,即默认“记住一切”——给梯度一条畅通的传送带先用着,再让网络慢慢学会该忘什么。运行可以看到,三道门的开合随输入变化,细胞状态 沿时间累积演化,而不是被反复碾平。
g * i(候选记忆乘输入门)这一项用 而非 sigmoid,是因为写入细胞的内容需要有正有负(增减记忆),而门控阀门需要的是 0 到 1 的“开合比例”,所以用 sigmoid。每个非线性的选择都对应一个明确的语义角色——这是 LSTM 设计的精巧之处。
五
LSTM 的影响怎么强调都不为过。1997 年发表后,它一度并不显眼,但随着 2000 年代中期在多项序列预测竞赛中夺冠,它逐渐成为序列学习的主力架构。从 2011 年到 2017 年 Transformer 崛起之前,LSTM 几乎是序列建模的默认选择——语音识别、手写识别、机器翻译、语言建模,背后大多是 LSTM 或它的变体4。谷歌的语音转写、苹果的 Siri、机器翻译系统,都曾建立在 LSTM 之上。
2014 年 Cho 等人提出的门控循环单元(GRU)是 LSTM 的一个简化:把遗忘门和输入门合并成一个“更新门”,去掉独立的细胞状态,参数更少、训练更快,性能在很多任务上与 LSTM 相当。LSTM 和 GRU 一起,构成了“门控循环网络”这个大家族。
六
用一张图把“朴素 RNN 为什么记不住、LSTM 为什么记得住”并排钉死:
朴素 RNN(记忆每步被矩阵乘碾过,梯度 ~ λ^k 指数衰减):
h0 ─×W─► h1 ─×W─► h2 ─×W─► ... ─×W─► hT
↑ 误差反传时每步乘 W·tanh',|λ|<1 则到 h0 时已衰减为 ~0
LSTM(细胞状态走加法高速路 CEC,遗忘门≈1 时梯度≈1 不衰减):
c0 ──+──► c1 ──+──► c2 ──+──► ... ──+──► cT ← 加法通路:梯度恒定
↑f,i ↑f,i ↑f,i (门只调节流量,不反复相乘碾压)
h0 h1 h2 hT
配套的 manim 动画 assets/manim/ch04_rnn_lstm.py(VanishingGradient 与 LSTMConveyor 两个 Scene)把两件事演成几何:前者把 RNN 沿时间展开成一条链,反向梯度每过一步乘一个小于 1 的因子,脉冲一路指数衰减,远处时间步的梯度归零——这就是“学不到长程依赖”;后者把 LSTM 的细胞态画成一条信息几乎无损直线流过的“传送带”(CEC),三个门像阀门,控制往传送带上加什么、删什么、读什么。门控让网络学会了“在正确的时刻记住正确的东西”。
LSTM 解决了“记忆能否跨越长距离”的问题,但它仍有一个结构性的代价:序列必须一步接一步顺序处理,第 步必须等第 步算完。这在 GPU 这种大规模并行硬件上是巨大的浪费,也限制了它能处理的序列长度和训练速度。如何既能建模长程依赖、又能并行计算?答案是把“循环”彻底扔掉、改用一种叫“注意力”的机制让每个位置直接看到所有其他位置。但在抵达那个答案(Transformer)之前,得先看注意力是怎么在机器翻译里作为 RNN 的一个补丁被发明出来的——以及在此之前,词本身是怎么被变成向量的。那是 word2vec 与 seq2seq 的故事。
本质
梯度消失的根源是一个朴素的数学事实:信息沿时间反传时要连乘许多个因子,而连乘的东西若不恰好等于 1,就只会爆炸或归零。LSTM 的精妙之处不是“加了门”,而是它在网络里专门修了一条让梯度可以等于 1 地直线流过的通道——细胞态这条传送带上的信息默认原样保留(加法更新而非反复相乘),于是误差能跨越几百个时间步而不衰减;门只是叠加在这条通道上、决定何时往里写、何时清空、何时读出的旁路。它真正解决的,是“如何让记忆的保持成为默认、让遗忘成为需要主动触发的选择”。这个“默认直通、旁路调节”的思想,后来以残差连接的形式回到了前馈网络,也是一切深层结构能被训练的共同密码。
参考文献
-
Hochreiter, S. (1991). Untersuchungen zu dynamischen neuronalen Netzen(硕士论文,首次形式分析梯度消失,BPTT 梯度指数衰减)。Schmidhuber, “Sepp Hochreiter’s Fundamental Deep Learning Problem (1991)”:https://people.idsia.ch/~juergen/fundamentaldeeplearningproblem.html
-
梯度消失问题机制综述(连乘雅可比、λ^k 指数衰减、与 Bengio 1994 的关系)。Vanishing gradient problem, Wikipedia:https://en.wikipedia.org/wiki/Vanishing_gradient_problem
-
Hochreiter, S., & Schmidhuber, J. (1997). Long Short-Term Memory. Neural Computation, 9(8), 1735–1780. 恒定误差传送带(CEC)与输入/遗忘/输出门。原文(ResearchGate):https://www.researchgate.net/publication/13853244_Long_Short-Term_Memory
-
LSTM 架构、门控数学与历史影响(2011–2017 序列建模主力、GRU 变体)综述。LSTM, Wikipedia:https://en.wikipedia.org/wiki/Long_short-term_memory ;教科书级推导:Dive into Deep Learning, “Long Short-Term Memory (LSTM)”:http://d2l.ai/chapter_recurrent-modern/lstm.html