第20章:RNN 与序列建模 | Chapter 20: RNN & Sequence Modeling
阶段定位 | Stage: 第五阶段 — 序列模型与 Attention 预计学时 | Duration: 4~5 小时
---
学习目标 | Learning Objectives
中文:
- 理解为什么标准神经网络无法处理序列数据
- 掌握 RNN 的前向传播与参数共享机制
- 理解梯度消失的成因及 LSTM/GRU 的解决方案
- 掌握序列模型的四种架构(一对一、一对多、多对一、多对多)
- 能在 NumPy 中实现基础 RNN 和 LSTM 的单步及序列前向传播
- 理解双向 RNN 的直觉与应用场景
English:
- Understand why standard neural networks cannot handle sequential data
- Master RNN forward propagation and parameter sharing
- Understand vanishing gradient causes and LSTM/GRU solutions
- Master four sequence model architectures
- Implement RNN and LSTM single-step and sequence forward propagation in NumPy
- Understand bidirectional RNN intuition and applications
---
20.1 为什么需要序列模型 | Why Sequence Models?
中文解释
标准神经网络的问题
对于句子"I love AI":
- 输入维度固定:3 个词
- 但如果输入"I really love AI":4 个词 → 网络结构必须改变
- 词与词之间的顺序信息完全丢失
序列数据的特点
| 数据类型 | 例子 | 关键特性 |
|---|---|---|
| 文本 | 句子、文档 | 变长、词序重要 |
| 语音 | 音频波形 | 时序依赖 |
| 时间序列 | 股价、天气 | 历史影响未来 |
| 视频 | 帧序列 | 时空联合 |
核心需求
模型需要:
- 处理变长输入
- 记住历史信息
- 参数与序列长度无关
English Explanation
Standard NN problems: fixed input size, no order information.
Sequence data needs: variable length, memory of history, length-independent parameters.
---
20.2 RNN 前向传播 | RNN Forward Propagation
中文解释
核心思想
维护一个隐藏状态 a<t>,随时间步传递:
a<t> = tanh(W_aa · a<t-1> + W_ax · x<t> + b_a)
y<t> = softmax(W_ya · a<t> + b_y)参数共享
无论序列多长,同一组参数 W_aa, W_ax, W_ya 在所有时间步共享。这意味着:
- 模型可以处理任意长度的序列
- 学到的模式可以迁移到不同位置
四种架构
| 架构 | 输入 | 输出 | 应用 |
|---|---|---|---|
| 一对一 | 单张图片 | 标签 | 图像分类 |
| 一对多 | 一个词/图片 | 序列 | 音乐生成、图片描述 |
| 多对一 | 序列 | 单标签 | 情感分析、文本分类 |
| 多对多 | 序列 | 等长序列 | 命名实体识别 |
| 多对多(编码器-解码器) | 序列 | 不等长序列 | 机器翻译 |
English Explanation
Core idea: maintain hidden state a<t> that passes through time steps.
Parameter sharing: same weights at every time step → handles variable length.
---
20.3 梯度消失与梯度爆炸 | Vanishing & Exploding Gradients
中文解释
梯度消失的数学成因
反向传播时,梯度要穿越多个时间步。对 W_aa 的梯度:
∂L/∂W_aa = ∂L/∂a<T> · ∂a<T>/∂a<T-1> · ∂a<T-1>/∂a<T-2> · ... · ∂a<1>/∂W_aa由于 a<t> = tanh(W_aa·a<t-1> + ...),所以:
∂a<t>/∂a<t-1> = W_aa^T · diag(1 - tanh²(...))tanh的导数最大为 1,通常远小于 1- 如果
||W_aa|| < 1,连乘后梯度指数级缩小 - 100 步后,梯度可能缩小到
10^-30级别 → 完全无法学习
梯度爆炸
相反,如果 ||W_aa|| > 1:
(1.5)^100 ≈ 10^17梯度会指数级增长,导致参数更新过大,模型发散。
解决方案
| 问题 | 解决方案 |
|---|---|
| 梯度爆炸 | 梯度裁剪(Gradient Clipping) |
| 梯度消失 | LSTM / GRU(改变网络结构) |
English Explanation
Vanishing gradients: W_aa multiplied 100 times → exponential decay. Exploding gradients: ||W|| > 1 → exponential growth. Fixes: gradient clipping for explosion, LSTM/GRU for vanishing.
---
20.4 LSTM 详解 | LSTM Deep Dive
中文解释
为什么 LSTM 能解决梯度消失?
标准 RNN 的信息传递只有一条乘法链:
a<t> = tanh(W·a<t-1> + ...)LSTM 引入细胞状态 c<t>,有直接的加法路径:
c<t> = Γf ⊙ c<t-1> + Γu ⊙ c̃<t>反向传播时,关于 c<t> 的梯度:
∂L/∂c<t-1> = ∂L/∂c<t> · Γf如果遗忘门 Γf ≈ 1(记住所有信息),梯度可以直接无损流过:
∂L/∂c<t-1> ≈ ∂L/∂c<t>这就像给梯度开了一条高速公路,绕过 W 的乘法链。
三个门的作用
| 门 | 公式 | 作用 |
|---|---|---|
遗忘门 Γf | σ(Wf·[a<t-1>, x<t>] + bf) | 决定保留多少旧记忆 |
输入门 Γu | σ(Wi·[a<t-1>, x<t>] + bi) | 决定写入多少新信息 |
输出门 Γo | σ(Wo·[a<t-1>, x<t>] + bo) | 决定输出多少信息 |
为什么用 sigmoid 和 tanh?
- Sigmoid(门控):输出在 (0,1),天然适合做"比例"控制
- Tanh(候选值/输出):输出在 (-1,1),均值为 0,有利于梯度流动
English Explanation
Cell state `c<t>` has direct addition path:
c<t> = Γf ⊙ c<t-1> + Γu ⊙ c̃<t>When Γf ≈ 1, gradient flows directly: ∂L/∂c<t-1> ≈ ∂L/∂c<t>.
---
20.5 GRU:LSTM 的精简版 | GRU
中文解释
简化思路
把 LSTM 的遗忘门和输入门合并为一个更新门:
Γu = σ(Wu · [a<t-1>, x<t>]) # 更新门
Γr = σ(Wr · [a<t-1>, x<t>]) # 重置门
c̃<t> = tanh(W · [Γr ⊙ a<t-1>, x<t>])
a<t> = Γu ⊙ c̃<t> + (1 - Γu) ⊙ a<t-1>GRU vs LSTM
| 特性 | LSTM | GRU |
|---|---|---|
| 门数量 | 3 | 2 |
| 状态 | c + a | 只有 a |
| 参数量 | 更多 | 更少(约 25%) |
| 效果 | 通常略好 | 相近 |
| 速度 | 稍慢 | 稍快 |
实践建议:两者效果通常接近,GRU 训练更快,LSTM 在极长序列上更稳定。
English Explanation
GRU merges forget and input gates into update gate.
Comparison: GRU has fewer parameters (~25% less), similar performance, faster training.
---
20.6 完整实现:RNN / LSTM / GRU
代码案例
import numpy as np
np.random.seed(1)
def sigmoid(z):
return 1 / (1 + np.exp(-np.clip(z, -500, 500)))
# ========== RNN 单步 ==========
def rnn_step(x_t, a_prev, Wax, Waa, ba):
a_next = np.tanh(np.dot(Wax, x_t) + np.dot(Waa, a_prev) + ba)
return a_next
# ========== RNN 序列前向 ==========
def rnn_forward(X, a0, Wax, Waa, ba, Wya, by):
"""
X: (n_x, m, T_x) — 输入序列
a0: (n_a, m) — 初始隐藏状态
"""
n_x, m, T_x = X.shape
n_a = a0.shape[0]
n_y = Wya.shape[0]
a = np.zeros((n_a, m, T_x))
y_pred = np.zeros((n_y, m, T_x))
a_next = a0
for t in range(T_x):
a_next = rnn_step(X[:, :, t], a_next, Wax, Waa, ba)
a[:, :, t] = a_next
y_pred[:, :, t] = sigmoid(np.dot(Wya, a_next) + by)
return a, y_pred
# ========== LSTM 单步 ==========
def lstm_step(x_t, a_prev, c_prev, Wf, bf, Wi, bi, Wc, bc, Wo, bo):
n_a = a_prev.shape[0]
concat = np.vstack((a_prev, x_t))
ft = sigmoid(np.dot(Wf, concat) + bf) # 遗忘门
it = sigmoid(np.dot(Wi, concat) + bi) # 输入门
cct = np.tanh(np.dot(Wc, concat) + bc) # 候选值
ot = sigmoid(np.dot(Wo, concat) + bo) # 输出门
c_next = ft * c_prev + it * cct
a_next = ot * np.tanh(c_next)
return a_next, c_next
# ========== GRU 单步 ==========
def gru_step(x_t, a_prev, Wz, bz, Wr, br, Wh, bh):
n_a = a_prev.shape[0]
concat = np.vstack((a_prev, x_t))
zt = sigmoid(np.dot(Wz, concat) + bz) # 更新门
rt = sigmoid(np.dot(Wr, concat) + br) # 重置门
concat_reset = np.vstack((rt * a_prev, x_t))
a_tilde = np.tanh(np.dot(Wh, concat_reset) + bh)
a_next = zt * a_tilde + (1 - zt) * a_prev
return a_next
# ========== 测试 ==========
n_x, n_a, m, T_x = 10, 5, 2, 3
X = np.random.randn(n_x, m, T_x)
a0 = np.zeros((n_a, m))
# RNN 参数
Wax = np.random.randn(n_a, n_x) * 0.01
Waa = np.random.randn(n_a, n_a) * 0.01
ba = np.zeros((n_a, 1))
Wya = np.random.randn(1, n_a) * 0.01
by = np.zeros((1, 1))
a, y_pred = rnn_forward(X, a0, Wax, Waa, ba, Wya, by)
print(f"RNN 序列前向:")
print(f" 输入 X: {X.shape} (n_x, m, T_x)")
print(f" 隐藏状态 a: {a.shape} (n_a, m, T_x)")
print(f" 输出 y_pred: {y_pred.shape} (n_y, m, T_x)")
# LSTM 参数
n_concat = n_a + n_x
lstm_params = {
'Wf': np.random.randn(n_a, n_concat) * 0.01, 'bf': np.zeros((n_a, 1)),
'Wi': np.random.randn(n_a, n_concat) * 0.01, 'bi': np.zeros((n_a, 1)),
'Wc': np.random.randn(n_a, n_concat) * 0.01, 'bc': np.zeros((n_a, 1)),
'Wo': np.random.randn(n_a, n_concat) * 0.01, 'bo': np.zeros((n_a, 1)),
}
c_prev = np.zeros((n_a, 1))
a_lstm, c_lstm = lstm_step(X[:, 0:1, 0], a0[:, 0:1], c_prev, **lstm_params)
print(f"\nLSTM 单步: a {a_lstm.shape}, c {c_lstm.shape}")
# GRU 参数
gru_params = {
'Wz': np.random.randn(n_a, n_concat) * 0.01, 'bz': np.zeros((n_a, 1)),
'Wr': np.random.randn(n_a, n_concat) * 0.01, 'br': np.zeros((n_a, 1)),
'Wh': np.random.randn(n_a, n_concat) * 0.01, 'bh': np.zeros((n_a, 1)),
}
a_gru = gru_step(X[:, 0:1, 0], a0[:, 0:1], **gru_params)
print(f"GRU 单步: a {a_gru.shape}")
# ========== 梯度消失模拟 ==========
print(f"\n梯度消失模拟:")
W_values = [0.5, 0.9, 1.0, 1.1, 1.5]
for W_val in W_values:
# 简化: tanh' ≈ 1 时的梯度缩放因子
scale = (W_val) ** 100
print(f" W={W_val}: 100步后梯度缩放 ≈ {scale:.2e}")输出:
RNN 序列前向:
输入 X: (10, 2, 3) (n_x, m, T_x)
隐藏状态 a: (5, 2, 3) (n_a, m, T_x)
输出 y_pred: (1, 2, 3) (n_y, m, T_x)
LSTM 单步: a (5, 1), c (5, 1)
GRU 单步: a (5, 1)
梯度消失模拟:
W=0.5: 100步后梯度缩放 ≈ 7.89e-31
W=0.9: 100步后梯度缩放 ≈ 2.66e-05
W=1.0: 100步后梯度缩放 ≈ 1.00e+00
W=1.1: 100步后梯度缩放 ≈ 1.38e+04
W=1.5: 100步后梯度缩放 ≈ 4.07e+17W=0.5 时梯度几乎为 0;W=1.5 时梯度爆炸到天文数字。只有在 W≈1.0 附近才稳定。
---
20.7 双向 RNN | Bidirectional RNN
中文解释
问题
标准 RNN 只能从左向右看信息:
"I ___ apples" → 不知道空白处是"love"还是"hate"双向 RNN
同时运行两个 RNN:
- 前向 RNN:从左到右读取
- 后向 RNN:从右到左读取
输出是两者的拼接:
y<t> = g(W_y · [a_forward<t>, a_backward<t>] + b_y)优势
- 每个位置都能看到完整上下文
- 在命名实体识别、情感分析等任务上效果显著
劣势
- 无法用于实时生成(必须等完整序列)
- 计算量翻倍
English Explanation
Bidirectional RNN: runs two RNNs in parallel — forward and backward.
Pros: each position sees full context. Cons: cannot use for real-time generation, doubles computation.
---
20.8 常见误区 | Common Pitfalls
1. RNN 不是真正的"记忆"
RNN 的隐藏状态是压缩表示,不是精确记忆。长序列早期信息会被后期信息覆盖。
2. 梯度裁剪不能解决梯度消失
梯度裁剪(clipping)只解决梯度爆炸,对梯度消失无效。解决梯度消失必须改变网络结构(LSTM/GRU/Attention)。
3. LSTM 不是万能的
虽然 LSTM 缓解了梯度消失,但在极长序列(>500步)上仍然困难。Transformer 的 Self-Attention 直接连接任意两个位置,彻底解决了长程依赖问题。
---
本章总结 | Chapter Summary
中文:
- 序列数据需要处理变长输入和时序依赖
- RNN 通过隐藏状态传递历史信息,参数共享
- 标准 RNN 有严重梯度消失/爆炸,长程依赖学不到
- 梯度消失:W 连乘导致指数级衰减;梯度爆炸:W 连乘导致指数级增长
- LSTM 用细胞状态 + 门控机制解决梯度消失(加法路径)
- GRU 是 LSTM 的简化版,参数量更少,效果相近
- 双向 RNN 能看到完整上下文,但不能用于实时生成
- 极长序列上,Transformer 的 Attention 比 LSTM/GRU 更有效
English:
- Sequence models handle variable length and temporal dependencies
- RNN passes history via hidden state with shared parameters
- Standard RNN has severe vanishing/exploding gradients
- LSTM solves vanishing via cell state and gating (addition path)
- GRU is simplified LSTM with fewer parameters
- Bidirectional RNN sees full context but not for real-time generation
- For very long sequences, Transformer Attention outperforms LSTM/GRU
---
课后练习 | Homework
- 梯度消失模拟:构造一个 W_aa=0.5 的 RNN,计算 100 步后的梯度缩放因子。
- LSTM 门控直觉:解释为什么遗忘门用 sigmoid、候选值用 tanh。
- 序列生成:用 RNN 训练一个字符级语言模型,生成一段文本。
- 双向 RNN:实现 Bi-RNN,对比与单向 RNN 在序列标注任务上的效果。
- LSTM vs GRU:在同一任务上对比两者的参数量、训练速度和最终效果。
- 梯度裁剪实现:实现梯度裁剪(clip by norm/value),观察对梯度爆炸的抑制效果。
- LSTM 反向传播:推导 LSTM 中
∂L/∂c<t-1>的表达式,证明当遗忘门≈1时梯度可以直接传递。