SilverIce Toolbox
Back to course

Stage 5 / Chapter 20

第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 个词 → 网络结构必须改变
  • 词与词之间的顺序信息完全丢失

序列数据的特点

数据类型例子关键特性
文本句子、文档变长、词序重要
语音音频波形时序依赖
时间序列股价、天气历史影响未来
视频帧序列时空联合

核心需求

模型需要:

  1. 处理变长输入
  2. 记住历史信息
  3. 参数与序列长度无关

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

特性LSTMGRU
门数量32
状态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

代码案例

python
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+17
W=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

  1. 梯度消失模拟:构造一个 W_aa=0.5 的 RNN,计算 100 步后的梯度缩放因子。
  1. LSTM 门控直觉:解释为什么遗忘门用 sigmoid、候选值用 tanh。
  1. 序列生成:用 RNN 训练一个字符级语言模型,生成一段文本。
  1. 双向 RNN:实现 Bi-RNN,对比与单向 RNN 在序列标注任务上的效果。
  1. LSTM vs GRU:在同一任务上对比两者的参数量、训练速度和最终效果。
  1. 梯度裁剪实现:实现梯度裁剪(clip by norm/value),观察对梯度爆炸的抑制效果。
  1. LSTM 反向传播:推导 LSTM 中 ∂L/∂c<t-1> 的表达式,证明当遗忘门≈1时梯度可以直接传递。