RNN、LSTM、GRU 模型详解

一、基础概念

RNN(循环神经网络)、LSTM(长短期记忆网络)、GRU(门控循环单元)都是处理序列数据的核心神经网络架构。它们通过引入”隐藏状态”来捕捉时间步之间的依赖关系。


二、RNN(循环神经网络)

2.1 内部结构

1
2
3
输入 xt ──→ [RNN Cell] ──→ 输出 ht
↑ │
ht-1 ────┘

每个时间步共享相同的参数(W, U, b),形成”循环”展开。

2.2 计算公式

对于每个时间步 t:

1
2
ht = tanh(Wxh · xt + Whh · ht-1 + bh)       ← 隐藏状态
yt = Why · ht + by ← 输出

其中:

  • xt: 时间步 t 的输入向量 (dx,)
  • ht: 时间步 t 的隐藏状态 (dh,)
  • ht-1: 上一时间步的隐藏状态
  • Wxh: 输入到隐藏的权重矩阵 (dh, dx)
  • Whh: 隐藏到隐藏的权重矩阵 (dh, dh)
  • bh: 隐藏层偏置 (dh,)
  • Why: 隐藏到输出的权重矩阵 (dy, dh)
  • by: 输出偏置 (dy,)

2.3 PyTorch 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import torch
import torch.nn as nn
import numpy as np

class SimpleRNNCell(nn.Module):
"""单个RNN细胞"""
def __init__(self, input_size, hidden_size):
super().__init__()
self.input_size = input_size
self.hidden_size = hidden_size
# 权重矩阵
self.Wxh = nn.Parameter(torch.randn(hidden_size, input_size) * 0.1)
self.Whh = nn.Parameter(torch.randn(hidden_size, hidden_size) * 0.1)
self.bh = nn.Parameter(torch.zeros(hidden_size))

def forward(self, xt, ht_prev):
# ht = tanh(Wxh @ xt + Whh @ ht_prev + bh)
ht = torch.tanh(
xt @ self.Wxh.T + # (batch, input) @ (input, hidden) -> (batch, hidden)
ht_prev @ self.Whh.T + # (batch, hidden) @ (hidden, hidden) -> (batch, hidden)
self.bh # (hidden,)
)
return ht


class RNN(nn.Module):
"""完整RNN(支持变长序列)"""
def __init__(self, input_size, hidden_size, output_size, batch_first=True):
super().__init__()
self.hidden_size = hidden_size
self.rnn_cell = SimpleRNNCell(input_size, hidden_size)
self.Wh = nn.Parameter(torch.randn(output_size, hidden_size) * 0.1)
self.by = nn.Parameter(torch.zeros(output_size))
self.batch_first = batch_first

def forward(self, x, lengths=None):
"""
Args:
x: 输入序列 (batch, seq_len, input_size) 或 (seq_len, batch, input_size)
lengths: 每个样本的实际长度(用于pack_padded_sequence)
Returns:
outputs: 所有时间步的输出 (batch, seq_len, output_size)
h_final: 最后一个时间步的隐藏状态 (batch, hidden_size)
"""
if self.batch_first:
x = x.transpose(0, 1) # -> (seq_len, batch, input_size)

seq_len, batch_size, _ = x.size()
h = torch.zeros(batch_size, self.hidden_size, device=x.device)
outputs = []

for t in range(seq_len):
xt = x[t] # (batch, input_size)
h = self.rnn_cell(xt, h)
yt = h @ self.Wh.T + self.by # (batch, output_size)
outputs.append(yt)

outputs = torch.stack(outputs, dim=1) # (seq_len, batch, output_size)
if self.batch_first:
outputs = outputs.transpose(0, 1)

return outputs, h


# 使用示例
def demo_rnn():
batch_size = 4
seq_len = 10
input_size = 20
hidden_size = 32
output_size = 5

rnn = RNN(input_size, hidden_size, output_size)
x = torch.randn(batch_size, seq_len, input_size)

outputs, h_final = rnn(x)
print(f"RNN 输出形状: {outputs.shape}") # (4, 10, 5)
print(f"最终隐藏状态形状: {h_final.shape}") # (4, 32)

# 梯度回传检查
loss = outputs.sum()
loss.backward()
print("RNN 反向传播成功 ✓")

demo_rnn()

2.4 RNN 的问题:梯度消失/爆炸

1
2
3
4
5
6
7
8
损失 L → ∂L/∂h₁ → ∂h₁/∂h₀ → ∂h₀/∂x₀

链式法则: ∂L/∂x₀ = ∂L/∂hₜ · ∏(∂hᵢ/∂hᵢ₋₁) · ∂h₀/∂x₀

其中 ∂hᵢ/∂hᵢ₋₁ = diag(tanh'(·)) · Whh

当 |λ(Whh)| < 1 时,连乘 → 0 (梯度消失)
当 |λ(Whh)| > 1 时,连乘 → ∞ (梯度爆炸)

这就是为什么需要 LSTMGRU —— 通过门控机制缓解这个问题。


三、LSTM(长短期记忆网络)

3.1 内部结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
                    ┌─────────────────────┐
xt ──→ ┌─────────→ │ 遗忘门 ft = σ(Wf·[ht-1,xt]+bf) │
│ └─────────────────────┘
ht-1 ──→│ ┌─────────────────────┐
└────────→ │ 输入门 it = σ(Wi·[ht-1,xt]+bi) │
└─────────────────────┘
┌─────────────────────┐
│ 候选细胞 ĉt = tanh(Wc·[ht-1,xt]+bc)│
└─────────────────────┘
┌─────────────────────┐
ct = ft⊙ct-1 + it⊙ĉt ← 细胞状态更新
└─────────────────────┘
┌─────────────────────┐
│ 输出门 ot = σ(Wo·[ht-1,xt]+bo) │
└─────────────────────┘
ht = ot ⊙ tanh(ct) ← 隐藏状态输出

核心:细胞状态 Ct 是一条贯穿所有时间步的”高速公路”,信息可以几乎无损地传递。

3.2 计算公式

遗忘门(决定丢弃什么旧信息):

1
ft = σ(Wf · [ht-1, xt] + bf)          ∈ [0, 1]^dh

输入门(决定记住什么新信息):

1
2
it = σ(Wi · [ht-1, xt] + bi)          ∈ [0, 1]^dh
ĉt = tanh(Wc · [ht-1, xt] + bc) ∈ [-1, 1]^dh

细胞状态更新

1
ct = ft ⊙ ct-1 + it ⊙ ĉt               ← ⊙ 为逐元素乘法

输出门(决定输出什么):

1
2
ot = σ(Wo · [ht-1, xt] + bo)          ∈ [0, 1]^dh
ht = ot ⊙ tanh(ct) ← 隐藏状态

其中 [ht-1, xt] 表示拼接操作。

3.3 权重矩阵形式(分开计算)

1
2
3
4
5
6
7
8
# 遗忘门
ft = σ(Wfh · ht-1 + Wfx · xt + bf)
# 输入门
it = σ(Wih · ht-1 + Wix · xt + bi)
# 候选细胞
ĉt = tanh(Wch · ht-1 + Wcx · xt + bc)
# 输出门
ot = σ(Oh · ht-1 + Ox · xt + bo)

3.4 PyTorch 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
class LSTMCell(nn.Module):
"""单个LSTM细胞"""
def __init__(self, input_size, hidden_size):
super().__init__()
self.hidden_size = hidden_size
# 四个门的权重 (每个门: [hidden+input] -> hidden)
gate_size = hidden_size + input_size
self.Wf = nn.Parameter(torch.randn(hidden_size, gate_size) * 0.1)
self.Wi = nn.Parameter(torch.randn(hidden_size, gate_size) * 0.1)
self.Wc = nn.Parameter(torch.randn(hidden_size, gate_size) * 0.1)
self.Wo = nn.Parameter(torch.randn(hidden_size, gate_size) * 0.1)

self.bf = nn.Parameter(torch.zeros(hidden_size))
self.bi = nn.Parameter(torch.zeros(hidden_size))
self.bc = nn.Parameter(torch.zeros(hidden_size))
self.bo = nn.Parameter(torch.zeros(hidden_size))

def forward(self, xt, state):
"""
Args:
xt: 输入 (batch, input_size)
state: (ht_prev, ct_prev)
Returns:
(ht, ct)
"""
ht_prev, ct_prev = state
# 拼接 [ht-1, xt]
combined = torch.cat([ht_prev, xt], dim=1) # (batch, hidden+input)

# 四个门
ft = torch.sigmoid(combined @ self.Wf.T + self.bf) # (batch, hidden)
it = torch.sigmoid(combined @ self.Wi.T + self.bi)
ct_tilde = torch.tanh(combined @ self.Wc.T + self.bc)
ot = torch.sigmoid(combined @ self.Wo.T + self.bo)

# 细胞状态更新
ct = ft * ct_prev + it * ct_tilde

# 隐藏状态
ht = ot * torch.tanh(ct)

return ht, ct


class LSTM(nn.Module):
"""完整LSTM"""
def __init__(self, input_size, hidden_size, output_size, batch_first=True):
super().__init__()
self.hidden_size = hidden_size
self.lstm_cell = LSTMCell(input_size, hidden_size)
self.Wh = nn.Parameter(torch.randn(output_size, hidden_size) * 0.1)
self.by = nn.Parameter(torch.zeros(output_size))
self.batch_first = batch_first

def forward(self, x, lengths=None):
if self.batch_first:
x = x.transpose(0, 1)

seq_len, batch_size, _ = x.size()
h = torch.zeros(batch_size, self.hidden_size, device=x.device)
c = torch.zeros(batch_size, self.hidden_size, device=x.device)
outputs = []

for t in range(seq_len):
xt = x[t]
h, c = self.lstm_cell(xt, (h, c))
yt = h @ self.Wh.T + self.by
outputs.append(yt)

outputs = torch.stack(outputs, dim=1)
if self.batch_first:
outputs = outputs.transpose(0, 1)

return outputs, (h, c)


# 使用示例
def demo_lstm():
batch_size = 4
seq_len = 10
input_size = 20
hidden_size = 32
output_size = 5

lstm = LSTM(input_size, hidden_size, output_size)
x = torch.randn(batch_size, seq_len, input_size)

outputs, (h_final, c_final) = lstm(x)
print(f"LSTM 输出形状: {outputs.shape}") # (4, 10, 5)
print(f"隐藏状态形状: {h_final.shape}") # (4, 32)
print(f"细胞状态形状: {c_final.shape}") # (4, 32)

loss = outputs.sum()
loss.backward()
print("LSTM 反向传播成功 ✓")

demo_lstm()

四、GRU(门控循环单元)

4.1 内部结构

GRU是LSTM的简化版本,合并了细胞状态和隐藏状态,只有两个门:

1
2
3
4
zt = σ(Wz · [ht-1, xt] + bz)      ← 更新门
rt = σ(Wr · [ht-1, xt] + br) ← 重置门
ht_tilde = tanh(Wh · [rt ⊙ ht-1, xt] + bh) ← 候选隐藏状态
ht = (1 - zt) ⊙ ht-1 + zt ⊙ ht_tilde ← 最终隐藏状态

4.2 计算公式

更新门(控制多少旧信息保留到新状态):

1
zt = σ(Wz · [ht-1, xt] + bz)          ∈ [0, 1]^dh

重置门(控制多少旧信息被遗忘,用于计算候选状态):

1
rt = σ(Wr · [ht-1, xt] + br)          ∈ [0, 1]^dh

候选隐藏状态

1
ht_tilde = tanh(Wh · [rt ⊙ ht-1, xt] + bh)

最终隐藏状态(线性插值):

1
ht = (1 - zt) ⊙ ht-1 + zt ⊙ ht_tilde

4.3 PyTorch 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
class GRUCell(nn.Module):
"""单个GRU细胞"""
def __init__(self, input_size, hidden_size):
super().__init__()
self.hidden_size = hidden_size
gate_size = hidden_size + input_size
self.Wz = nn.Parameter(torch.randn(hidden_size, gate_size) * 0.1)
self.Wr = nn.Parameter(torch.randn(hidden_size, gate_size) * 0.1)
self.Wh = nn.Parameter(torch.randn(hidden_size, gate_size) * 0.1)

self.bz = nn.Parameter(torch.zeros(hidden_size))
self.br = nn.Parameter(torch.zeros(hidden_size))
self.bh = nn.Parameter(torch.zeros(hidden_size))

def forward(self, xt, ht_prev):
"""
Args:
xt: 输入 (batch, input_size)
ht_prev: 上一隐藏状态 (batch, hidden_size)
Returns:
ht: 新隐藏状态
"""
# 拼接
combined = torch.cat([ht_prev, xt], dim=1) # (batch, hidden+input)

# 更新门
zt = torch.sigmoid(combined @ self.Wz.T + self.bz)

# 重置门
rt = torch.sigmoid(combined @ self.Wr.T + self.br)

# 候选隐藏状态 (使用重置门调节旧隐藏状态)
ht_tilde = torch.tanh(
(rt * ht_prev) @ self.Wh.T[:self.hidden_size, :self.hidden_size].T + # 注意这里需要调整
xt @ self.Wh.T[:, -input_size:] + # 简化写法
self.bh
)

# 更清晰的写法:
# 候选门权重需要区分隐藏和输入部分
Wz_h, Wz_x = self.Wz[:, :self.hidden_size], self.Wz[:, self.hidden_size:]
Wr_h, Wr_x = self.Wr[:, :self.hidden_size], self.Wr[:, self.hidden_size:]
Wh_h, Wh_x = self.Wh[:, :self.hidden_size], self.Wh[:, self.hidden_size:]

zt = torch.sigmoid(ht_prev @ Wz_h.T + xt @ Wz_x.T + self.bz)
rt = torch.sigmoid(ht_prev @ Wr_h.T + xt @ Wr_x.T + self.br)
ht_tilde = torch.tanh(
(rt * ht_prev) @ Wh_h.T + xt @ Wh_x.T + self.bh
)

# 线性插值
ht = (1 - zt) * ht_prev + zt * ht_tilde

return ht


class GRU(nn.Module):
"""完整GRU"""
def __init__(self, input_size, hidden_size, output_size, batch_first=True):
super().__init__()
self.hidden_size = hidden_size
self.gru_cell = GRUCell(input_size, hidden_size)
self.Wh = nn.Parameter(torch.randn(output_size, hidden_size) * 0.1)
self.by = nn.Parameter(torch.zeros(output_size))
self.batch_first = batch_first

def forward(self, x, lengths=None):
if self.batch_first:
x = x.transpose(0, 1)

seq_len, batch_size, _ = x.size()
h = torch.zeros(batch_size, self.hidden_size, device=x.device)
outputs = []

for t in range(seq_len):
xt = x[t]
h = self.gru_cell(xt, h)
yt = h @ self.Wh.T + self.by
outputs.append(yt)

outputs = torch.stack(outputs, dim=1)
if self.batch_first:
outputs = outputs.transpose(0, 1)

return outputs, h


# 使用示例
def demo_gru():
batch_size = 4
seq_len = 10
input_size = 20
hidden_size = 32
output_size = 5

gru = GRU(input_size, hidden_size, output_size)
x = torch.randn(batch_size, seq_len, input_size)

outputs, h_final = gru(x)
print(f"GRU 输出形状: {outputs.shape}") # (4, 10, 5)
print(f"隐藏状态形状: {h_final.shape}") # (4, 32)

loss = outputs.sum()
loss.backward()
print("GRU 反向传播成功 ✓")

demo_gru()

五、三种模型对比

5.1 结构对比

特性 RNN LSTM GRU
隐藏状态 1个 (ht) 2个 (ht, ct) 1个 (ht)
门控数量 4个(遗忘/输入/候选/输出) 2个(更新/重置)
参数量 最少 最多 (~4倍RNN) 较少 (~3倍RNN)
计算复杂度 O(d²) O(4d²) O(3d²)
梯度路径 直接连乘 通过细胞状态 Ct 通过更新门 zt

5.2 数学对比

1
2
3
4
5
6
7
8
9
10
RNN:  ht = tanh(Wxh·xt + Whh·ht-1 + bh)
梯度: ∂ht/∂ht-1 = diag(tanh') · Whh ← 容易消失/爆炸

LSTM: ct = ft⊙ct-1 + it⊙ĉt
ht = ot⊙tanh(ct)
梯度: ∂ct/∂ct-1 = ft ⊙ I ← 被遗忘门ft控制,可学习保持长期依赖

GRU: ht = (1-zt)⊙ht-1 + zt⊙ht_tilde
梯度: ∂ht/∂ht-1 = (1-zt) + zt·diag(tanh')·...
梯度: ≈ (1-zt) ← 被更新门zt控制,可学习保持长期依赖

5.3 性能对比

指标 RNN LSTM GRU
长期依赖 ❌ 差 ✅ 好 ✅ 较好
训练速度 ⚡ 最快 🐢 最慢 ⚡ 较快
参数量 中等
过拟合风险 中等
收敛速度 中等
小数据表现 尚可 可能过拟合 较好
大数据表现 较差 最好 接近LSTM

5.4 实际选择指南

1
2
3
4
5
6
7
8
9
需要处理序列数据?
├── 序列很短 (< 10步) → RNN 即可,简单高效
├── 序列中等 (10-100步) → GRU,速度和效果平衡
├── 序列很长 (> 100步) → LSTM,更好的长期记忆
├── 数据量很小 → RNN 或 GRU(减少过拟合)
├── 数据量很大 → LSTM 或 GRU(都能充分学习)
├── 计算资源有限 → GRU(参数少、速度快)
├── 需要最佳精度 → LSTM(理论上最强)
└── 现代NLP任务 → Transformer 取代了所有RNN变体

六、完整对比实验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import torch
import torch.nn as nn
import time

class SequenceDataset:
"""简单的序列分类数据集"""
def __init__(self, seq_len=50, vocab_size=100, num_classes=2, n_samples=1000):
self.seq_len = seq_len
self.vocab_size = vocab_size
self.num_classes = num_classes
self.n_samples = n_samples
self.X = torch.randint(0, vocab_size, (n_samples, seq_len))
# 简单规则: 前半段和后半段的差异决定类别
self.y = (self.X[:, :10].sum() > self.X[:, -10:].sum()).long()
self.y = torch.where(self.y, torch.ones(n_samples), torch.zeros(n_samples)).long()

def get_dataloader(self, batch_size=32, shuffle=True):
from torch.utils.data import TensorDataset, DataLoader
dataset = TensorDataset(self.X.float(), self.y)
return DataLoader(dataset, batch_size=batch_size, shuffle=shuffle)


def benchmark_models():
"""对比三种模型的训练速度和性能"""
torch.manual_seed(42)

seq_len = 50
vocab_size = 100
hidden_size = 64
embedding_dim = 32
num_classes = 2
batch_size = 64

# 创建数据
train_data = SequenceDataset(seq_len, vocab_size, num_classes, 2000)
test_data = SequenceDataset(seq_len, vocab_size, num_classes, 500)
train_loader = train_data.get_dataloader(batch_size, shuffle=True)
test_loader = test_data.get_dataloader(batch_size, shuffle=False)

# 定义三种模型
models = {
'RNN': nn.RNN(embedding_dim, hidden_size, batch_first=True),
'LSTM': nn.LSTM(embedding_dim, hidden_size, batch_first=True),
'GRU': nn.GRU(embedding_dim, hidden_size, batch_first=True),
}

results = {}

for name, encoder in models.items():
print(f"\n{'='*50}")
print(f"训练 {name}")
print('='*50)

model = nn.Sequential(
nn.Embedding(vocab_size, embedding_dim),
encoder,
nn.Linear(hidden_size, num_classes)
)

optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
criterion = nn.CrossEntropyLoss()

# 训练
start = time.time()
model.train()
for epoch in range(10):
total_loss = 0
for xb, yb in train_loader:
optimizer.zero_grad()
# 前向: embedding -> encoder -> linear
emb = model[0](xb.long()) # (batch, seq, embed)
out, _ = model[1](emb) # (batch, seq, hidden)
logits = model[2](out[:, -1, :]) # 取最后一个时间步
loss = criterion(logits, yb)
loss.backward()
optimizer.step()
total_loss += loss.item()

if (epoch + 1) % 5 == 0:
print(f" Epoch {epoch+1}/10, Loss: {total_loss/len(train_loader):.4f}")

train_time = time.time() - start

# 评估
model.eval()
correct = 0
total = 0
with torch.no_grad():
for xb, yb in test_loader:
emb = model[0](xb.long())
out, _ = model[1](emb)
logits = model[2](out[:, -1, :])
preds = logits.argmax(dim=1)
correct += (preds == yb).sum().item()
total += len(yb)

accuracy = correct / total
results[name] = {'train_time': train_time, 'accuracy': accuracy}

print(f" 训练时间: {train_time:.2f}s")
print(f" 测试准确率: {accuracy:.2%}")

# 汇总对比
print(f"\n{'='*50}")
print("对比总结")
print('='*50)
for name, metrics in results.items():
print(f"{name:6s} | 训练时间: {metrics['train_time']:6.2f}s | 准确率: {metrics['accuracy']:.2%}")

return results

# 运行对比
results = benchmark_models()

七、PyTorch 内置实现速查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 所有三种模型都可以直接用 PyTorch 内置实现

# RNN
rnn = nn.RNN(input_size=10, hidden_size=20, num_layers=2,
batch_first=True, activation='tanh') # tanh 或 relu

# LSTM
lstm = nn.LSTM(input_size=10, hidden_size=20, num_layers=2,
batch_first=True, dropout=0.3)

# GRU
gru = nn.GRU(input_size=10, hidden_size=20, num_layers=2,
batch_first=True, dropout=0.3)

# 输出
# output: (batch, seq_len, hidden_size * num_directions)
# hidden/cell: (num_layers * num_directions, batch, hidden_size)
output, hidden = rnn(input_embedded)
output, (hidden, cell) = lstm(input_embedded)
output, hidden = gru(input_embedded)

八、总结

维度 RNN LSTM GRU
发明年份 1986 1997 2014
核心创新 循环连接 细胞状态+4个门 简化门控
解决的问题 序列建模 梯度消失 训练效率
参数量 1份 4份 3份
长期记忆 较强
适用场景 短序列、基线 长序列、高精度 通用、效率优先
现代替代 Transformer

关键结论

  1. RNN 是最基础的序列模型,但存在严重的梯度消失问题
  2. LSTM 通过细胞状态和四个门解决了长期依赖问题,但参数量大、训练慢
  3. GRU 在性能和效率之间取得良好平衡,大多数情况下与LSTM表现相当
  4. 在现代NLP任务中,Transformer已逐渐取代所有RNN变体,但在时序数据、资源受限场景下仍有价值