前面两章的网络都假设输入是一次性、固定大小的(一张图、一个向量)。但世界上大量数据是序列:一句话是词的序列,一段语音是声学帧的序列,一支股票是价格的序列。序列有两个特点让前馈网络无能为力——长度可变,且当前的理解依赖之前的内容。“他把行李放进了___”这个空,要填什么取决于前文。

循环神经网络(Recurrent Neural Network, RNN)的想法直截了当:给网络一个隐藏状态 ht\mathbf{h}_t 当作“记忆”,每读入一个新元素 xt\mathbf{x}t,就用上一刻的记忆 ht1\mathbf{h}{t-1} 和当前输入一起算出新的记忆:

ht=tanh ⁣(Whhht1+Wxhxt+b)\mathbf{h}t = \tanh!\left(W{hh},\mathbf{h}{t-1} + W{xh},\mathbf{x}_t + \mathbf{b}\right)

yt=Whyht\mathbf{y}t = W{hy},\mathbf{h}_t

注意一个关键细节:每个时间步用的是同一组权重 Whh,WxhW_{hh}, W_{xh}——这和 CNN 的权重共享异曲同工,只不过 CNN 在空间上共享,RNN 在时间上共享。把这个循环按时间“展开”(unroll),它就变成一个深度等于序列长度的前馈网络:每个时间步是一层,层与层之间共享权重,记忆 h\mathbf{h} 像一条河流从第一个时间步流到最后一个。

训练 RNN 用的还是反向传播,只不过要沿着时间轴反向传,叫随时间反向传播(Backpropagation Through Time, BPTT)。误差从最后一个时间步出发,沿着隐藏状态的链条一步步往回流。问题就出在这条往回流的链条上。


1991 年,Sepp Hochreiter 在他的硕士学位论文《动态神经网络研究》(Untersuchungen zu dynamischen neuronalen Netzen)里,第一次对一个困扰循环网络的现象给出了正式的数学分析。这就是后来被称为梯度消失问题(vanishing gradient problem)的东西,Schmidhuber 称之为“深度学习的根本问题”1

机制是这样的。误差沿时间反传时,从时间步 TT 传到时间步 tt,要连续乘过 (Tt)(T-t) 个雅可比矩阵——每跨一个时间步,梯度就要乘一次 WhhW_{hh}^\top,再乘一次激活函数的导数 tanh\tanh’。把这些乘子的“有效大小”记为 λ\lambda,那么传播 kk 个时间步后,梯度的量级大约正比于 λk\lambda^k

  • 如果 λ<1\lambda < 1λk\lambda^kkk 指数衰减到零——这是梯度消失。远处时间步传来的误差信号在到达近处之前就衰减没了,网络学不到长程依赖。
  • 如果 λ>1\lambda > 1λk\lambda^kkk 指数爆炸——这是梯度爆炸,训练直接发散。

Hochreiter 证明的正是这一点:BPTT 的梯度会随跨越的时间步数(或层数)呈指数衰减12。Bengio 等人 1994 年也从另一个角度论证了用梯度方法学习长程依赖的根本困难。这个分析的杀伤力在于:它说明 RNN 记不住远处信息,不是没训练够,而是优化机制本身的数学性质决定的——就像第 01 章感知机学不会 XOR 是表达力的硬限制一样,这是训练动力学的硬限制。

把它跑出来看最直观(配套代码 code/04_lstm_cell.py):

代码 · 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)}")

↓ 下载 04_lstm_cell.py

乘子=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 个时间步后梯度就只剩 8×10128\times10^{-12}——第一个词对第五十个词的训练信号,实际上等于零。这就是为什么朴素 RNN 记不住长句子的开头。


梯度爆炸相对好治——直接给梯度设一个上限“裁剪”(gradient clipping)即可。真正难的是梯度消失。Hochreiter 和他的导师 Schmidhuber 给出的解法,是 1997 年发表的长短期记忆网络(Long Short-Term Memory, LSTM)3

LSTM 的核心洞察可以一句话概括:与其让记忆每一步都被一个会衰减的乘法变换碾过,不如开辟一条让记忆能近乎无损地直接流过去的“高速公路”,再用几道闸门精确控制什么时候写入、什么时候擦除、什么时候读出。

这条高速公路就是 LSTM 引入的第二条状态——细胞状态(cell state)ct\mathbf{c}_t,与隐藏状态 ht\mathbf{h}_t 并行。Hochreiter 称这条让误差恒定流动的通路为恒定误差传送带(Constant Error Carousel, CEC)3。它的关键在于细胞状态的更新主要是加法而非反复的矩阵乘法——加法的梯度是 1,不会指数衰减,于是误差可以沿着细胞状态这条线几乎无衰减地传回很远的过去。

控制这条传送带的是三道门(gate),每道门都是一个取值在 0 到 1 之间的 sigmoid 输出,像阀门一样逐元素地调节信息流。记 zt=[ht1;xt]\mathbf{z}t = [\mathbf{h}{t-1}; \mathbf{x}_t] 为上一刻隐藏状态与当前输入的拼接:

ft=σ(Wfzt+bf)遗忘门:决定旧记忆擦掉多少\mathbf{f}_t = \sigma(W_f \mathbf{z}_t + \mathbf{b}_f) \quad \text{遗忘门:决定旧记忆擦掉多少} it=σ(Wizt+bi)输入门:决定新信息写入多少\mathbf{i}_t = \sigma(W_i \mathbf{z}_t + \mathbf{b}_i) \quad \text{输入门:决定新信息写入多少} ct=tanh(Wczt+bc)候选记忆:本步想写入的内容\tilde{\mathbf{c}}_t = \tanh(W_c \mathbf{z}_t + \mathbf{b}_c) \quad \text{候选记忆:本步想写入的内容} ot=σ(Wozt+bo)输出门:决定记忆读出多少\mathbf{o}_t = \sigma(W_o \mathbf{z}_t + \mathbf{b}_o) \quad \text{输出门:决定记忆读出多少}

细胞状态的更新——LSTM 的心脏:

ct=ftct1+itct\mathbf{c}_t = \mathbf{f}t \odot \mathbf{c}{t-1} + \mathbf{i}_t \odot \tilde{\mathbf{c}}_t

读这个式子:遗忘门 ft\mathbf{f}t 逐元素地决定旧细胞状态 ct1\mathbf{c}{t-1} 保留多少(接近 1 = 几乎全留,接近 0 = 擦除);输入门 it\mathbf{i}_t 决定候选记忆 ct\tilde{\mathbf{c}}_t 写入多少。当遗忘门接近 1、输入门接近 0 时,ctct1\mathbf{c}t \approx \mathbf{c}{t-1}——记忆原封不动地传到下一步,误差也就原封不动地传回上一步。这就是 CEC 让长程梯度存活的数学原因。

最后,隐藏状态(也是对外的输出)由输出门过滤后的细胞状态给出:

ht=ottanh(ct)\mathbf{h}_t = \mathbf{o}_t \odot \tanh(\mathbf{c}_t)


把一个 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

一个常用且重要的工程技巧体现在初始化里:遗忘门的偏置 bf\mathbf{b}_f 初始化为正值(代码里设为 1)。这让训练初期遗忘门默认接近 1,即默认“记住一切”——给梯度一条畅通的传送带先用着,再让网络慢慢学会该忘什么。运行可以看到,三道门的开合随输入变化,细胞状态 c\mathbf{c} 沿时间累积演化,而不是被反复碾平。

g * i(候选记忆乘输入门)这一项用 tanh\tanh 而非 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.pyVanishingGradientLSTMConveyor 两个 Scene)把两件事演成几何:前者把 RNN 沿时间展开成一条链,反向梯度每过一步乘一个小于 1 的因子,脉冲一路指数衰减,远处时间步的梯度归零——这就是“学不到长程依赖”;后者把 LSTM 的细胞态画成一条信息几乎无损直线流过的“传送带”(CEC),三个门像阀门,控制往传送带上加什么、删什么、读什么。门控让网络学会了“在正确的时刻记住正确的东西”。

LSTM 解决了“记忆能否跨越长距离”的问题,但它仍有一个结构性的代价:序列必须一步接一步顺序处理,第 tt 步必须等第 t1t-1 步算完。这在 GPU 这种大规模并行硬件上是巨大的浪费,也限制了它能处理的序列长度和训练速度。如何既能建模长程依赖、又能并行计算?答案是把“循环”彻底扔掉、改用一种叫“注意力”的机制让每个位置直接看到所有其他位置。但在抵达那个答案(Transformer)之前,得先看注意力是怎么在机器翻译里作为 RNN 的一个补丁被发明出来的——以及在此之前,词本身是怎么被变成向量的。那是 word2vec 与 seq2seq 的故事。


本质

梯度消失的根源是一个朴素的数学事实:信息沿时间反传时要连乘许多个因子,而连乘的东西若不恰好等于 1,就只会爆炸或归零。LSTM 的精妙之处不是“加了门”,而是它在网络里专门修了一条让梯度可以等于 1 地直线流过的通道——细胞态这条传送带上的信息默认原样保留(加法更新而非反复相乘),于是误差能跨越几百个时间步而不衰减;门只是叠加在这条通道上、决定何时往里写、何时清空、何时读出的旁路。它真正解决的,是“如何让记忆的保持成为默认、让遗忘成为需要主动触发的选择”。这个“默认直通、旁路调节”的思想,后来以残差连接的形式回到了前馈网络,也是一切深层结构能被训练的共同密码。


参考文献

  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

  2. 梯度消失问题机制综述(连乘雅可比、λ^k 指数衰减、与 Bengio 1994 的关系)。Vanishing gradient problem, Wikipedia:https://en.wikipedia.org/wiki/Vanishing_gradient_problem

  3. 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

  4. 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

朴素 RNN 沿时间展开成共享权重 W 的深链;BPTT 反传时每步乘一个因子 λ,梯度量级≈λᵏ 指数衰减(绿→红方向二色、亮度=量级、数值双轨),50 步后≈8×10⁻¹²——长程信号归零的数学根源。
LSTM 细胞态 c 是一条恒定误差传送带(CEC):记忆块近乎无损直线流过,三道门(遗忘 f/输入 i/输出 o)像阀门控制写入/擦除/读出;心脏式 cₜ=f⊙cₜ₋₁+i⊙c̃ₜ 是加法,f≈1、i≈0 时记忆原样直通、梯度≈1,这就是「默认直通、旁路调节」(后来的残差连接)。