目录
顺序模型概述
什么是顺序模型?
定义: 处理序列数据的神经网络,能记住历史信息
特点:
- 顺序敏感:输入的顺序影响输出
- 状态记忆:通过隐藏状态传递历史信息
- 时序依赖:当前输出依赖于之前的输入
应用场景
| 领域 | 输入 | 输出 | 应用 |
|---|---|---|---|
| NLP | 文本序列 | 词向量/标签 | 翻译、情感分析 |
| 语音 | 音频序列 | 文本/标签 | 语音识别 |
| 视频 | 帧序列 | 动作标签 | 视频分类 |
| 时间序列 | 历史数据 | 预测值 | 股价、天气预测 |
| 推荐 | 用户行为 | 下一个物品 | 个性化推荐 |
顺序数据的特点
序列: x₁, x₂, x₃, ..., xₜ
特点:
1. 长度可变
2. 时序关系:xₜ 依赖于 x₁, ..., xₜ₋₁
3. 长期依赖:xₜ 可能依赖于很早的 x₁
RNN 基础
RNN 结构
基本思想: 通过隐藏状态传递历史信息
x₁ ──┐
├─ h₁ ──┬─ y₁
x₂ ──┤ │
├─ h₂ ──┼─ y₂
x₃ ──┤ │
├─ h₃ ──┼─ y₃
... ──┤ │
├─ hₜ ──┴─ yₜ
xₜ ──┘
hₜ: 当前隐藏状态(包含历史信息)
RNN 公式
hₜ = tanh(Wₕₕ hₜ₋₁ + Wₓₕ xₜ + bₕ)
yₜ = Wₕy hₜ + bᵧ
其中:
hₜ: 当前隐藏状态
hₜ₋₁: 上一时刻的隐藏状态
xₜ: 当前输入
yₜ: 当前输出
Wₕₕ, Wₓₕ, Wₕy: 权重矩阵
bₕ, bᵧ: 偏置
RNN 计算过程
示例:处理单词序列 "I love deep learning"
时刻 1: x₁ = "I"
h₁ = tanh(Wₕₕ × 0 + Wₓₕ × I) # 初始 h₀ = 0
y₁ = Wₕy × h₁
时刻 2: x₂ = "love"
h₂ = tanh(Wₕₕ × h₁ + Wₓₕ × love) # h₂ 依赖 h₁
y₂ = Wₕy × h₂
时刻 3: x₃ = "deep"
h₃ = tanh(Wₕₕ × h₂ + Wₓₕ × deep) # h₃ 依赖 h₂
y₃ = Wₕy × h₃
...
隐藏状态 hₜ 携带了之前所有输入的信息
RNN 的问题
梯度消失/梯度爆炸
梯度传播:
∂Loss/∂h₁ = ∂Loss/∂hₜ × ∂hₜ/∂hₜ₋₁ × ... × ∂h₂/∂h₁
↑
连乘多个梯度
如果 |∂hₜ/∂hₜ₋₁| < 1:
梯度逐渐消失,无法学到长期依赖
如果 |∂hₜ/∂hₜ₋₁| > 1:
梯度爆炸,训练不稳定
无法捕捉长期依赖
例子: "我出生在法国... (长文本) ... 所以我说流利的法语"
↑
需要记住"法国"
RNN: 难以记住这么久之前的信息
LSTM
LSTM 结构
核心思想: 引入门、遗忘、输出三个门控机制
细胞状态 (Cell State) ──┬─ 遗忘门 ── 保留/遗忘信息
├─ 输入门 ── 写入新信息
└─ 输出门 ── 输出信息
LSTM 组件
1. 遗忘门 (Forget Gate)
作用: 决定从细胞状态中丢弃什么
fₜ = σ(W_f · [hₜ₋₁, xₜ] + b_f)
其中:
fₜ: 遗忘门 (0-1)
σ: sigmoid 函数
[hₜ₋₁, xₜ]: 拼接
例子:
输入: "我出生在法国,搬到了中国,现在说流利的___"
↑
遗忘门决定忘记"法国"
2. 输入门 (Input Gate)
作用: 决定向细胞状态写入什么新信息
iₜ = σ(W_i · [hₜ₋₁, xₜ] + b_i) # 输入门
C̃ₜ = tanh(W_C · [hₜ₋₁, xₜ] + b_C) # 候选细胞状态
更新细胞状态:
Cₜ = fₜ ⊙ Cₜ₋₁ + iₜ ⊙ C̃ₜ
其中:
⊙: 逐元素相乘
Cₜ: 当前细胞状态
Cₜ₋₁: 上一时刻细胞状态
例子:
输入: "我出生在法国,搬到了中国,现在说流利的___"
↑
输入门决定写入"中文"
3. 输出门 (Output Gate)
作用: 决定从细胞状态输出什么
oₜ = σ(W_o · [hₜ₋₁, xₜ] + b_o) # 输出门
hₜ = oₜ ⊙ tanh(Cₜ) # 隐藏状态
完整 LSTM 公式:
fₜ = σ(W_f · [hₜ₋₁, xₜ] + b_f) # 遗忘门
iₜ = σ(W_i · [hₜ₋₁, xₜ] + b_i) # 输入门
oₜ = σ(W_o · [hₜ₋₁, xₜ] + b_o) # 输出门
C̃ₜ = tanh(W_C · [hₜ₋₁, xₜ] + b_C) # 候选状态
Cₜ = fₜ ⊙ Cₜ₋₁ + iₜ ⊙ C̃ₜ # 更新细胞状态
hₜ = oₜ ⊙ tanh(Cₜ) # 隐藏状态
LSTM 可视化
┌─────────────────────────────────────┐
│ │
Cₜ₋₁ ────→[+]←───────┐ ┌─────────┤
│ │ │ │
遗忘门 (fₜ) │ 输出门 (oₜ) │
↓ │ ↓ │
[σ] │ [σ] ───[⊙]─────┤
│ │ │ │
│ └────────→ [tanh] ──┘ │
│ ↑ ↓
│ [⊙] hₜ ───→
│ ↑ │
│ 输入门 (iₜ) │ │
│ ↓ │ │
└──[⊙] ←─ [σ] ──→ [⊙] ───────┘ │
↑ ↑ │
C̃ₜ ←─ [tanh] │ │
↑ │ │
└── [⊙] ───┘ │
↑ │
[hₜ₋₁, xₜ] ───┘ │
│
┌───────────────────────────────────┘
│
↓
Cₜ (细胞状态)
GRU
GRU 结构
核心思想: 简化 LSTM,合并门控机制
主要变化:
- 合并细胞状态和隐藏状态
- 合并遗忘门和输入门为更新门
GRU 公式
zₜ = σ(W_z · [hₜ₋₁, xₜ] + b_z) # 更新门
rₜ = σ(W_r · [hₜ₋₁, xₜ] + b_r) # 重置门
h̃ₜ = tanh(W_h · [rₜ ⊙ hₜ₋₁, xₜ] + b_h) # 候选隐藏状态
hₜ = (1 - zₜ) ⊙ hₜ₋₁ + zₜ ⊙ h̃ₜ # 最终隐藏状态
其中:
zₜ: 更新门 (0-1)
rₜ: 重置门 (0-1)
h̃ₜ: 候选隐藏状态
hₜ: 最终隐藏状态
GRU 工作原理
更新门 (zₜ):
zₜ 决定保留多少旧信息,写入多少新信息
如果 zₜ = 0:
hₜ = hₜ₋₁ (完全保留旧信息)
如果 zₜ = 1:
hₜ = h̃ₜ (完全写入新信息)
重置门 (rₜ):
rₜ 决定如何组合旧信息和新信息
如果 rₜ = 0:
忽略 hₜ₋₁,只用 xₜ
如果 rₜ = 1:
完全考虑 hₜ₋₁
模型对比
RNN vs LSTM vs GRU
| 维度 | RNN | LSTM | GRU |
|---|---|---|---|
| 门控机制 | 无 | 3个门 (遗忘/输入/输出) | 2个门 (更新/重置) |
| 细胞状态 | 无 | 有 | 无(合并到 hₜ) |
| 参数量 | 少 | 多 | 中 |
| 计算复杂度 | 低 | 高 | 中 |
| 长期依赖 | ❌ | ✅ | ✅ |
| 训练速度 | 快 | 慢 | 中 |
| 内存占用 | 低 | 高 | 中 |
架构对比图
RNN:
hₜ₋₁ ──┐
├─ tanh ── hₜ ── yₜ
xₜ ───┘
LSTM:
Cₜ₋₁ ──┐
├─ [遗忘门] ──┬─ Cₜ ── [输出门] ── hₜ ── yₜ
xₜ ───┤ ↑
└─ [输入门] ───┘
hₜ₋₁
GRU:
hₜ₋₁ ──┐
├─ [更新门] ── hₜ ── yₜ
xₜ ───┤
└─ [重置门]
什么时候用哪个模型?
| 场景 | 推荐模型 | 原因 |
|---|---|---|
| 短序列 (<10) | RNN | 简单高效 |
| 长序列 (>20) | LSTM | 长期依赖强 |
| 计算受限 | GRU | 参数少 |
| 需要解释性 | LSTM | 门控机制清晰 |
| 实时应用 | GRU | 速度快 |
参数量计算
假设:
- 输入维度: d
- 隐藏维度: h
RNN:
Wₕₕ: h × h
Wₓₕ: d × h
Wₕy: h × output_dim
参数: h² + dh + h × output_dim
LSTM:
W_f: (d + h) × h
W_i: (d + h) × h
W_o: (d + h) × h
W_C: (d + h) × h
参数: 4 × (d + h) × h
GRU:
W_z: (d + h) × h
W_r: (d + h) × h
W_h: (d + h) × h
参数: 3 × (d + h) × h
结论: LSTM 参数量是 GRU 的 4/3 倍
双向 RNN (Bi-RNN)
基本思想
单向 RNN: 只能看过去
x₁ → x₂ → x₃ → ... → xₜ
双向 RNN: 同时看过去和未来
前向: x₁ → x₂ → x₃ → ... → xₜ
后向: x₁ ← x₂ ← x₃ ← ... ← xₜ
↓ ↓ ↓ ↓
h₁ → h₂ → h₃ → hₜ
↑ ↑ ↑ ↑
ĥ₁ ← ĥ₂ ← ĥ₃ ← ĥₜ
最终: hₜ = [hₜ, ĥₜ] (拼接)
适用场景
| 场景 | 单向 | 双向 |
|---|---|---|
| 文本生成 | ✅ | ❌ |
| 翻译 | ❌ | ✅ |
| 命名实体识别 | ❌ | ✅ |
| 语音识别 | ❌ | ✅ |
| 实时推荐 | ✅ | ❌ |
公式
前向: hₜ = f(xₜ, hₜ₋₁)
后向: ĥₜ = f(xₜ, ĥₜ₊₁)
最终: hₜ = [hₜ, ĥₜ] # 拼接
其中 f 可以是 RNN/LSTM/GRU
面试常见问题
Q1: RNN 的主要问题是什么?
A:
- 梯度消失/梯度爆炸: 长序列中梯度传播困难
- 无法捕捉长期依赖: 信息随时间丢失
- 无法并行: 必须按顺序计算
解决方法:
- LSTM/GRU: 门控机制
- 梯度裁剪: 解决梯度爆炸
- 残差连接: 缓解梯度消失
Q2: LSTM 如何解决梯度消失?
A:
LSTM 引入细胞状态 Cₜ:
Cₜ = fₜ ⊙ Cₜ₋₁ + iₜ ⊙ C̃ₜ
↑
通过加法而非乘法传播
梯度传播:
∂Loss/∂Cₜ₋₁ = ∂Loss/∂Cₜ × fₜ
如果 fₜ = 1:
∂Loss/∂Cₜ₋₁ = ∂Loss/∂Cₜ (无衰减)
LSTM 可以通过遗忘门控制信息流
Q3: LSTM 和 GRU 的区别是什么?
A:
| 维度 | LSTM | GRU |
|---|---|---|
| 细胞状态 | 有 (Cₜ) | 无 |
| 门数量 | 3 | 2 |
| 参数量 | 多 (4×) | 中 (3×) |
| 复杂度 | 高 | 低 |
| 性能 | 略好 | 接近 |
结论:
- 数据充足 → LSTM
- 计算受限 → GRU
Q4: 为什么使用 tanh 和 sigmoid?
A:
tanh:
范围: (-1, 1)
优点: 0 中心化,梯度不消失
用于:
- RNN/LSTM/GRU 的隐藏状态计算
- LSTM 的候选状态 C̃ₜ
sigmoid:
范围: (0, 1)
优点: 概率解释,门控机制
用于:
- LSTM/GRU 的门 (0=关, 1=开)
Q5: 为什么 RNN 无法并行计算?
A:
RNN 计算依赖前一时刻的隐藏状态:
hₜ = f(xₜ, hₜ₋₁) # hₜ 依赖 hₜ₋₁
hₜ₋₁ = f(xₜ₋₁, hₜ₋₂) # hₜ₋₁ 依赖 hₜ₋₂
必须串行计算,无法并行
对比 Transformer (Self-Attention):
所有位置同时计算
Q6: 如何初始化 RNN 的隐藏状态?
A:
1. 零初始化: h₀ = 0
- 简单,常用
2. 随机初始化: h₀ ~ N(0, 0.01)
- 有时收敛更快
3. 学习初始化: 将 h₀ 作为可学习参数
- 效果通常更好
代码:
h₀ = torch.zeros(num_layers, batch_size, hidden_dim)
# 或
h₀ = nn.Parameter(torch.randn(num_layers, batch_size, hidden_dim))
Q7: 什么是序列到序列模型 (Seq2Seq)?
A:
架构: Encoder-Decoder
Encoder:
输入序列 → RNN → 上下文向量
Decoder:
上下文向量 → RNN → 输出序列
应用:
- 机器翻译
- 文本摘要
- 问答系统
例子:
输入: "I love deep learning"
Encoder: → 上下文向量 (语义表示)
Decoder: → "我爱深度学习"
Q8: Teacher Forcing 是什么?
A:
训练 Decoder 时,使用真实标签作为输入
标准方式:
yₜ = Decoder(yₜ₋₁) # 用预测值
Teacher Forcing:
yₜ = Decoder(yₜ₋₁_真实) # 用真实值
优点:
- 训练更稳定
- 收敛更快
缺点:
- 训练推理不一致 (Exposure Bias)
- 可能过度依赖真实标签
解决:
- Scheduled Sampling: 混合使用预测值和真实值
代码实现
PyTorch RNN 实现
python
import torch
import torch.nn as nn
class SimpleRNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super().__init__()
self.hidden_size = hidden_size
# RNN 层
self.rnn = nn.RNN(input_size, hidden_size, batch_first=True)
# 输出层
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x):
# x: (batch_size, seq_len, input_size)
# RNN 前向传播
# output: (batch_size, seq_len, hidden_size)
# hidden: (1, batch_size, hidden_size)
output, hidden = self.rnn(x)
# 取最后一个时间步的输出
last_output = output[:, -1, :]
# 输出层
logits = self.fc(last_output)
return logits
if __name__ == '__main__':
# 参数
batch_size = 32
seq_len = 10
input_size = 16
hidden_size = 64
output_size = 10
# 模型
model = SimpleRNN(input_size, hidden_size, output_size)
# 输入
x = torch.randn(batch_size, seq_len, input_size)
# 前向传播
output = model(x)
print(f'Output shape: {output.shape}') # (32, 10)
PyTorch LSTM 实现
python
class LSTMModel(nn.Module):
def __init__(self, input_size, hidden_size, num_layers=2, dropout=0.1):
super().__init__()
self.hidden_size = hidden_size
self.num_layers = num_layers
# LSTM 层
self.lstm = nn.LSTM(
input_size=input_size,
hidden_size=hidden_size,
num_layers=num_layers,
batch_first=True,
dropout=dropout if num_layers > 1 else 0
)
# 输出层
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x, h=None, c=None):
# x: (batch_size, seq_len, input_size)
# LSTM 前向传播
if h is None:
output, (h_n, c_n) = self.lstm(x)
else:
output, (h_n, c_n) = self.lstm(x, (h, c))
# 取最后一个时间步
last_output = output[:, -1, :]
# 输出层
logits = self.fc(last_output)
return logits, h_n, c_n
if __name__ == '__main__':
# 参数
batch_size = 32
seq_len = 20
input_size = 16
hidden_size = 64
num_layers = 2
output_size = 10
# 模型
model = LSTMModel(input_size, hidden_size, num_layers)
# 输入
x = torch.randn(batch_size, seq_len, input_size)
# 前向传播
output, h, c = model(x)
print(f'Output shape: {output.shape}') # (32, 10)
print(f'Hidden state shape: {h.shape}') # (2, 32, 64)
print(f'Cell state shape: {c.shape}') # (2, 32, 64)
PyTorch GRU 实现
python
class GRUModel(nn.Module):
def __init__(self, input_size, hidden_size, num_layers=2, dropout=0.1):
super().__init__()
self.hidden_size = hidden_size
self.num_layers = num_layers
# GRU 层
self.gru = nn.GRU(
input_size=input_size,
hidden_size=hidden_size,
num_layers=num_layers,
batch_first=True,
dropout=dropout if num_layers > 1 else 0
)
# 输出层
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x, h=None):
# x: (batch_size, seq_len, input_size)
# GRU 前向传播
if h is None:
output, h_n = self.gru(x)
else:
output, h_n = self.gru(x, h)
# 取最后一个时间步
last_output = output[:, -1, :]
# 输出层
logits = self.fc(last_output)
return logits, h_n
if __name__ == '__main__':
# 参数
batch_size = 32
seq_len = 20
input_size = 16
hidden_size = 64
num_layers = 2
output_size = 10
# 模型
model = GRUModel(input_size, hidden_size, num_layers)
# 输入
x = torch.randn(batch_size, seq_len, input_size)
# 前向传播
output, h = model(x)
print(f'Output shape: {output.shape}') # (32, 10)
print(f'Hidden state shape: {h.shape}') # (2, 32, 64)
双向 LSTM 实现
python
class BiLSTM(nn.Module):
def __init__(self, input_size, hidden_size, num_layers=2):
super().__init__()
self.hidden_size = hidden_size
# 双向 LSTM
self.lstm = nn.LSTM(
input_size=input_size,
hidden_size=hidden_size,
num_layers=num_layers,
batch_first=True,
bidirectional=True
)
# 输出层 (双向隐藏状态维度是 2 * hidden_size)
self.fc = nn.Linear(hidden_size * 2, output_size)
def forward(self, x):
# x: (batch_size, seq_len, input_size)
# 双向 LSTM 前向传播
# output: (batch_size, seq_len, hidden_size * 2)
output, (h_n, c_n) = self.lstm(x)
# 取最后一个时间步
last_output = output[:, -1, :]
# 输出层
logits = self.fc(last_output)
return logits
if __name__ == '__main__':
# 参数
batch_size = 32
seq_len = 20
input_size = 16
hidden_size = 64
num_layers = 2
output_size = 10
# 模型
model = BiLSTM(input_size, hidden_size, num_layers)
# 输入
x = torch.randn(batch_size, seq_len, input_size)
# 前向传播
output = model(x)
print(f'Output shape: {output.shape}') # (32, 10)
快速检查清单
理解顺序模型,你应该能回答:
- 解释 RNN、LSTM、GRU 的区别
- 说明 LSTM 的三个门的作用
- 知道如何解决梯度消失
- 计算 LSTM 的参数量
- 理解双向 RNN 的原理
- 了解 Teacher Forcing
- 能从零实现简单的 RNN/LSTM/GRU
参考资料
- Understanding LSTM Networks: https://colah.github.io/posts/2015-08-Understanding-LSTMs/
- RNN 原始论文: https://www.researchgate.net/publication/2594470_Finding_Structure_in_Time
- LSTM 原始论文: http://www.bioinf.jku.at/publications/older/2604.pdf
- GRU 原始论文: https://arxiv.org/abs/1412.3555