09-循环神经网络¶
学习时间: 约6-8小时 难度级别: ⭐⭐⭐ 中级 前置知识: 神经网络基础、反向传播算法、优化器 学习目标: 理解序列建模思想,掌握RNN/LSTM/GRU的数学原理与实现,了解Seq2Seq与注意力机制的演进
🎯 学习目标¶
- 理解序列数据的特点与建模难题
- 掌握RNN的基本结构、前向传播与反向传播(BPTT)
- 深入理解梯度消失/爆炸问题及其根本原因
- 精通LSTM三门机制(遗忘门、输入门、输出门)的数学推导
- 掌握GRU结构,理解其与LSTM的区别和联系
- 了解双向RNN、深层RNN、Seq2Seq模型
- 理解注意力机制从Seq2Seq到Transformer的演进
- 能用PyTorch实现LSTM文本分类任务
目录¶
- 1. 序列数据的特点
- 2. RNN基本结构
- 3. RNN的训练:BPTT
- 4. 梯度消失与梯度爆炸
- 5. LSTM详解
- 6. GRU详解
- 7. 双向RNN与深层RNN
- 8. 序列到序列模型
- 9. 注意力机制简介
- 10. 应用场景
- 11. PyTorch实战:LSTM情感分析
- 12. RNN vs Transformer对比
- 13. 练习与自我检查
1. 序列数据的特点¶
1.1 什么是序列数据¶
序列数据是指数据元素之间存在有序依赖关系的数据,元素的位置和顺序包含重要信息。常见例子:
- 自然语言:一个句子是单词的有序序列,语法和语义依赖于词序
- 时间序列:股票价格、气温变化、心电图信号
- 语音信号:音频帧的时序序列
- DNA序列:碱基的有序排列(A, T, C, G)
- 视频:图像帧的时序序列
- 用户行为:点击流、购买历史
1.2 序列建模的挑战¶
- 可变长度:不同序列长度不同(句子有长有短),全连接网络要求固定维度输入
- 长距离依赖:序列中可能存在远距离的关联(如"The cat, which ate the fish, was happy"中was与cat的一致)
- 位置敏感:同样的元素在不同位置含义不同("dog bites man" vs "man bites dog")
- 时序因果:当前步的输出只能依赖于过去和当前的输入(在线/流式场景)
1.3 为什么需要RNN¶
传统方法处理序列的局限: - n-gram模型:只考虑有限窗口,无法捕获长距离依赖 - 全连接网络:无法处理变长输入,不共享时间步间的参数 - 1D-CNN:可以处理序列,但感受野有限
RNN通过隐状态递推实现:(1)处理任意长度序列;(2)参数在时间步间共享;(3)理论上可捕获任意长距离依赖。
2. RNN基本结构¶
图注:RNN基本结构(未展开)
2.1 核心思想¶
RNN在每个时间步 \(t\) 维护一个隐藏状态 \(\mathbf{h}_t\),它是对过去所有输入的压缩记忆。
2.2 数学公式¶
其中: - \(\mathbf{x}_t \in \mathbb{R}^d\):时间步 \(t\) 的输入 - \(\mathbf{h}_t \in \mathbb{R}^h\):时间步 \(t\) 的隐藏状态 - \(\mathbf{h}_{t-1}\):上一时间步的隐藏状态(\(\mathbf{h}_0\) 通常初始化为零) - \(\mathbf{W}_{xh} \in \mathbb{R}^{h \times d}\):输入到隐藏的权重矩阵 - \(\mathbf{W}_{hh} \in \mathbb{R}^{h \times h}\):隐藏到隐藏的权重矩阵 - \(\mathbf{W}_{hy} \in \mathbb{R}^{o \times h}\):隐藏到输出的权重矩阵
2.3 时间展开图¶
将RNN沿时间步展开,可以看到它等价于一个非常深的前馈网络,每一层对应一个时间步,且所有层共享相同的参数(\(\mathbf{W}_{xh}, \mathbf{W}_{hh}, \mathbf{W}_{hy}\)):
图注:RNN沿时间步展开,所有RNN单元共享相同的参数
2.4 参数共享的意义¶
参数共享使RNN具有以下优势: - 泛化性:同一规则应用于所有时间步,可处理训练时未见过的序列长度 - 参数效率:参数量与序列长度无关 - 平移不变性:模式在序列中任何位置出现都能被检测到
import torch
import torch.nn as nn
# PyTorch RNN基本用法
rnn = nn.RNN(input_size=10, hidden_size=64, num_layers=1, batch_first=True)
# 输入: (batch_size, seq_len, input_size)
x = torch.randn(32, 20, 10) # batch=32, 序列长度=20, 输入维度=10
h0 = torch.zeros(1, 32, 64) # 初始隐藏状态
output, hn = rnn(x, h0)
# output: (32, 20, 64) — 每个时间步的隐藏状态
# hn: (1, 32, 64) — 最后时间步的隐藏状态
3. RNN的训练:BPTT¶
图注:时间反向传播(BPTT) - 蓝色箭头表示前向传播,红色虚线箭头表示反向传播
3.1 时间反向传播(Backpropagation Through Time)¶
BPTT是反向传播算法在RNN上的扩展。将RNN沿时间展开后,按标准反向传播计算梯度。
总损失是所有时间步损失的累加:
对参数 \(\mathbf{W}_{hh}\) 的梯度需要沿时间回传:
其中每一项涉及链式法则沿时间步回传:
3.2 截断BPTT(Truncated BPTT)¶
完整BPTT计算和存储代价大。截断BPTT将长序列分成固定长度的块,每块独立反向传播,只在块之间传递隐藏状态(不传递梯度)。
这是时间和精度的折中——减少了长距离依赖的捕获能力,但显著降低了计算开销。
4. 梯度消失与梯度爆炸¶
图注:梯度消失(左)和梯度爆炸(右)的可视化
4.1 问题根源¶
从BPTT的梯度公式中,关键项是连续的雅可比矩阵乘积:
其中 \(\tanh'(\cdot) \in (0, 1]\)。
当 \(t - k\) 很大时: - 如果 \(\|\mathbf{W}_{hh}\|\) 的最大特征值 \(< 1\):梯度指数衰减 → 梯度消失 - 如果 \(\|\mathbf{W}_{hh}\|\) 的最大特征值 \(> 1\):梯度指数增长 → 梯度爆炸
4.2 梯度消失的后果¶
- 模型无法学习长距离依赖
- 靠前时间步的输入对后面的输出几乎没有影响
- 实际中,标准RNN只能有效处理约10-20步的依赖
4.3 梯度爆炸的解决¶
梯度裁剪(Gradient Clipping):当梯度范数超过阈值时,按比例缩放。
4.4 梯度消失的解决¶
梯度消失是更根本的问题,需要架构层面的创新 → LSTM和GRU。
5. LSTM详解¶
图注:LSTM单元结构,包含遗忘门、输入门、输出门和细胞状态
5.1 核心思想¶
长短期记忆网络(Long Short-Term Memory, Hochreiter & Schmidhuber, 1997)通过引入细胞状态(cell state)和门控机制来解决梯度消失问题。
核心直觉:细胞状态像一条"传送带",信息可以无损地在时间步间流动,门控决定何时写入、保留和读取信息。
5.2 三门机制 + 细胞状态¶
LSTM在每个时间步包含三个门和一个细胞状态更新:
① 遗忘门(Forget Gate) — 决定丢弃什么旧信息
\(\mathbf{f}_t \in (0, 1)^h\),每个元素控制对应细胞状态的保留程度。\(\sigma\) 是Sigmoid函数。
② 输入门(Input Gate) — 决定写入什么新信息
\(\mathbf{i}_t\) 是输入门的激活值,\(\tilde{\mathbf{C}}_t\) 是候选细胞状态。
③ 细胞状态更新
\(\odot\) 表示逐元素乘法。旧信息经遗忘门过滤,新信息经输入门选择性写入。
④ 输出门(Output Gate) — 决定输出什么
5.3 为什么LSTM解决梯度消失¶
关键在于细胞状态的更新方程:
对 \(\mathbf{C}_{t-1}\) 的梯度:
当遗忘门 \(\mathbf{f}_t\) 接近1时,梯度几乎无损传递,类似ResNet的跳跃连接。网络可以学习何时"记住"(\(\mathbf{f}_t \approx 1\))、何时"遗忘"(\(\mathbf{f}_t \approx 0\))。
5.4 LSTM信息流图示¶
C_{t-1} ──────×─────────+────────→ C_t
│ (forget) │ (input) │
x_t ──┐ │ │ │
├─→ [σ] ─── f_t ──────┘ │ │
├─→ [σ] ─── i_t ──────┐ │ │
h_{t-1}┤ ├──×─────────┘ │
├─→ [tanh] ─ C̃_t ────┘ │
│ │
└─→ [σ] ─── o_t ──────────────×──── tanh(C_t) → h_t
5.5 LSTM参数量¶
LSTM每层的参数量为 \(4 \times (d \times h + h \times h + h) = 4h(d + h + 1)\)(四个门共享相同的参数结构),其中 \(d\) 是输入维度,\(h\) 是隐藏维度。
# PyTorch LSTM
lstm = nn.LSTM(
input_size=128, # 输入维度
hidden_size=256, # 隐藏状态维度
num_layers=2, # 层数
batch_first=True, # (batch, seq, feature)
dropout=0.3, # 层间dropout
bidirectional=False # 单向
)
x = torch.randn(32, 50, 128) # (batch, seq_len, input_size)
output, (hn, cn) = lstm(x)
# output: (32, 50, 256) 每个时间步的隐藏状态
# hn: (2, 32, 256) 每层最后时间步的隐藏状态
# cn: (2, 32, 256) 每层最后时间步的细胞状态
6. GRU详解¶
图注:GRU单元结构,包含更新门和重置门
6.1 核心思想¶
门控循环单元(Gated Recurrent Unit, Cho et al., 2014)是LSTM的简化版本,合并了遗忘门和输入门,去掉了细胞状态。
6.2 GRU公式¶
重置门(Reset Gate) — 控制如何将新输入与之前的记忆组合
更新门(Update Gate) — 决定保留多少旧信息、接受多少新信息
候选隐藏状态 — 使用重置门选择性遗忘
隐藏状态更新 — 更新门在旧状态和候选状态之间插值
6.3 GRU vs LSTM 对比¶
图注:LSTM与GRU的详细对比
| 特性 | LSTM | GRU |
|---|---|---|
| 门的数量 | 3个(遗忘、输入、输出) | 2个(重置、更新) |
| 状态 | 隐藏状态 \(\mathbf{h}\) + 细胞状态 \(\mathbf{C}\) | 仅隐藏状态 \(\mathbf{h}\) |
| 参数量 | \(4h(d+h+1)\) | \(3h(d+h+1)\)(少25%) |
| 训练速度 | 较慢 | 较快 |
| 长序列建模 | 更好(细胞状态独立传递) | 稍差 |
| 短序列/小数据 | 可能过拟合 | 更适合 |
| 实际性能 | 通常相当 | 通常相当 |
经验法则:两者性能通常非常接近。数据充足时用LSTM,数据量小或序列短时用GRU。不确定时直接实验对比。
# PyTorch GRU
gru = nn.GRU(input_size=128, hidden_size=256, num_layers=2,
batch_first=True, dropout=0.3)
x = torch.randn(32, 50, 128)
output, hn = gru(x) # 没有cn(无细胞状态)
# output: (32, 50, 256)
# hn: (2, 32, 256)
7. 双向RNN与深层RNN¶
图注:双向RNN(BiRNN)结构,前向和后向RNN分别处理序列
7.1 双向RNN(Bidirectional RNN)¶
许多序列任务(如NER、情感分析)中,当前位置的判断需要参考前后文。双向RNN在两个方向分别运行一个RNN,并拼接两个方向的隐藏状态:
输出维度为 \(2h\)(两个方向拼接)。
bilstm = nn.LSTM(input_size=128, hidden_size=256, num_layers=2,
batch_first=True, bidirectional=True)
x = torch.randn(32, 50, 128)
output, (hn, cn) = bilstm(x)
# output: (32, 50, 512) — 2*256
# hn: (4, 32, 256) — 2层*2方向
注意:双向RNN不能用于需要因果推理的任务(如语言生成),因为它需要看到完整序列。
7.2 深层RNN(Stacked RNN)¶
图注:多层RNN(Stacked RNN)结构,每层提取不同抽象级别的特征
多层RNN堆叠,下一层的输入是上一层的输出。每层提取不同抽象级别的特征。
实践中RNN很少超过3-4层(相比CNN的数十层),因为每层已经在时间维度上很"深"。
8. 序列到序列模型¶
图注:Seq2Seq架构,编码器将输入序列编码为固定长度向量,解码器基于该向量生成输出序列
8.1 Seq2Seq架构(Encoder-Decoder)¶
Seq2Seq(Sutskever et al., 2014)用于输入输出均为变长序列的任务(如机器翻译)。
- 编码器:将输入序列 \((x_1, \ldots, x_T)\) 编码为固定长度的上下文向量 \(\mathbf{c}\)(通常取编码器最终隐藏状态)
- 解码器:以 \(\mathbf{c}\) 为初始输入,自回归地生成输出序列 \((y_1, \ldots, y_{T'})\)
8.2 Seq2Seq的瓶颈¶
所有输入信息被压缩到一个固定长度的向量 \(\mathbf{c}\) 中——这对长序列来说是严重的信息瓶颈。这直接催生了注意力机制。
class Encoder(nn.Module): # 继承nn.Module定义神经网络层
def __init__(self, vocab_size, embed_dim, hidden_dim, num_layers=1): # __init__构造方法,创建对象时自动调用
super().__init__() # super()调用父类方法
self.embedding = nn.Embedding(vocab_size, embed_dim)
self.lstm = nn.LSTM(embed_dim, hidden_dim, num_layers, batch_first=True)
def forward(self, x):
embedded = self.embedding(x)
outputs, (hidden, cell) = self.lstm(embedded)
return outputs, hidden, cell
class Decoder(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_dim, num_layers=1):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim)
self.lstm = nn.LSTM(embed_dim, hidden_dim, num_layers, batch_first=True)
self.fc = nn.Linear(hidden_dim, vocab_size)
def forward(self, x, hidden, cell):
embedded = self.embedding(x.unsqueeze(1)) # unsqueeze增加一个维度
output, (hidden, cell) = self.lstm(embedded, (hidden, cell))
prediction = self.fc(output.squeeze(1)) # squeeze去除大小为1的维度
return prediction, hidden, cell
9. 注意力机制简介¶
图注:注意力机制让解码器在每一步动态关注编码器的不同位置
9.1 动机¶
注意力机制(Bahdanau et al., 2014)让解码器在每一步都能动态关注编码器的不同位置,而非仅依赖单一上下文向量。
9.2 基本原理¶
对于解码器在时间步 \(t\) 的输出:
- 计算注意力分数:\(e_{t,i} = \text{score}(\mathbf{s}_{t-1}, \mathbf{h}_i)\)
- 归一化为权重:\(\alpha_{t,i} = \text{softmax}(e_{t,i})\)
- 加权求和得到上下文:\(\mathbf{c}_t = \sum_i \alpha_{t,i} \mathbf{h}_i\)
- 将 \(\mathbf{c}_t\) 与 \(\mathbf{s}_{t-1}\) 一起用于生成输出
常见的评分函数: - 加性注意力(Bahdanau):\(\text{score}(\mathbf{s}, \mathbf{h}) = \mathbf{v}^T \tanh(\mathbf{W}_1 \mathbf{s} + \mathbf{W}_2 \mathbf{h})\) - 点积注意力(Luong):\(\text{score}(\mathbf{s}, \mathbf{h}) = \mathbf{s}^T \mathbf{h}\) - 缩放点积注意力:\(\text{score}(\mathbf{s}, \mathbf{h}) = \frac{\mathbf{s}^T \mathbf{h}}{\sqrt{d}}\) → 这正是Transformer所使用的
9.3 从Seq2Seq+Attention到Transformer¶
注意力机制的演进路线:
Seq2Seq (2014) → Attention-based Seq2Seq (2014/2015)
→ Self-Attention (2017) → Transformer (2017)
关键突破:"Attention Is All You Need"(Vaswani et al., 2017)去掉了RNN,完全基于注意力构建模型 → 这就是Transformer,将在下一章详细讲解。
10. 应用场景¶
10.1 典型应用¶
| 应用 | 输入→输出 | 模型类型 |
|---|---|---|
| 文本生成 | 序列→序列 | 语言模型(单向RNN) |
| 情感分析 | 序列→标签 | 多对一(取最后隐藏状态) |
| 命名实体识别 | 序列→序列(等长) | 多对多(双向RNN) |
| 机器翻译 | 序列→序列(不等长) | Seq2Seq + Attention |
| 语音识别 | 序列→序列 | Seq2Seq / CTC |
| 时序预测 | 序列→值 | 多对一或多对多 |
| 音乐生成 | 序列→序列 | 语言模型方式 |
| 视频描述 | 帧序列→文本序列 | CNN编码器 + RNN解码器 |
10.2 RNN在时序预测中的应用¶
# 简单的时序预测LSTM
class TimeSeriesLSTM(nn.Module):
def __init__(self, input_dim=1, hidden_dim=64, output_dim=1, num_layers=2):
super().__init__()
self.lstm = nn.LSTM(input_dim, hidden_dim, num_layers, batch_first=True)
self.fc = nn.Linear(hidden_dim, output_dim)
def forward(self, x):
# x: (batch, seq_len, input_dim)
lstm_out, _ = self.lstm(x)
prediction = self.fc(lstm_out[:, -1, :]) # 取最后一步
return prediction
11. PyTorch实战:LSTM情感分析¶
11.1 完整代码¶
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
from collections import Counter
import re
# ==================== 1. 数据预处理 ====================
class TextDataset(Dataset):
"""简易文本数据集(以IMDb为例)"""
def __init__(self, texts, labels, vocab, max_len=256):
self.texts = texts
self.labels = labels
self.vocab = vocab
self.max_len = max_len
def __len__(self): # __len__定义len()的行为
return len(self.texts)
def __getitem__(self, idx): # __getitem__定义索引访问行为
tokens = tokenize(self.texts[idx])
# 将token转为索引
indices = [self.vocab.get(t, self.vocab['<UNK>']) for t in tokens]
# 截断或填充
if len(indices) > self.max_len:
indices = indices[:self.max_len]
else:
indices = indices + [self.vocab['<PAD>']] * (self.max_len - len(indices))
return torch.tensor(indices, dtype=torch.long), torch.tensor(self.labels[idx], dtype=torch.long)
def tokenize(text):
"""简单分词"""
text = text.lower()
text = re.sub(r'[^a-zA-Z\s]', '', text)
return text.split()
def build_vocab(texts, max_vocab=25000):
"""构建词表"""
counter = Counter() # Counter统计元素出现次数
for text in texts:
counter.update(tokenize(text))
# 保留高频词
most_common = counter.most_common(max_vocab - 2)
vocab = {'<PAD>': 0, '<UNK>': 1}
for word, _ in most_common:
vocab[word] = len(vocab)
return vocab
# ==================== 2. 模型定义 ====================
class LSTMClassifier(nn.Module):
"""LSTM情感分类模型"""
def __init__(self, vocab_size, embed_dim=128, hidden_dim=256,
num_layers=2, num_classes=2, dropout=0.5, bidirectional=True):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
self.lstm = nn.LSTM(
embed_dim, hidden_dim, num_layers,
batch_first=True, dropout=dropout if num_layers > 1 else 0,
bidirectional=bidirectional
)
self.dropout = nn.Dropout(dropout)
direction_factor = 2 if bidirectional else 1
self.fc = nn.Sequential(
nn.Linear(hidden_dim * direction_factor, hidden_dim),
nn.ReLU(),
nn.Dropout(dropout),
nn.Linear(hidden_dim, num_classes)
)
def forward(self, x):
# x: (batch, seq_len)
embedded = self.dropout(self.embedding(x)) # (batch, seq_len, embed_dim)
lstm_out, (hn, cn) = self.lstm(embedded) # lstm_out: (batch, seq_len, hidden_dim*2)
# 方式1:取最后时间步
# 对于双向LSTM,拼接两个方向最后时间步的隐藏状态
if self.lstm.bidirectional:
hidden = torch.cat([hn[-2], hn[-1]], dim=1) # (batch, hidden_dim*2) # [-1]负索引取最后一个元素
else:
hidden = hn[-1] # (batch, hidden_dim)
# 方式2(替代):对所有时间步做平均池化
# hidden = lstm_out.mean(dim=1)
output = self.fc(self.dropout(hidden))
return output
# ==================== 3. 训练流程 ====================
def train_sentiment_model():
"""完整的训练流程(需要配合真实数据集使用)"""
DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 假设已有train_texts, train_labels, test_texts, test_labels
# 实际使用时可加载IMDb数据集:
# from torchtext.datasets import IMDB
# 或 from datasets import load_dataset
# dataset = load_dataset("imdb")
# 示例数据(实际使用需替换)
train_texts = ["This movie is great!", "Terrible waste of time."] * 1000
train_labels = [1, 0] * 1000
vocab = build_vocab(train_texts)
VOCAB_SIZE = len(vocab)
train_dataset = TextDataset(train_texts, train_labels, vocab, max_len=256)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) # DataLoader批量加载数据,支持shuffle和多进程
model = LSTMClassifier(
vocab_size=VOCAB_SIZE,
embed_dim=128,
hidden_dim=256,
num_layers=2,
num_classes=2,
dropout=0.5,
bidirectional=True
).to(DEVICE)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-5)
scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=3, factor=0.5)
# 训练循环
for epoch in range(20):
model.train() # train()开启训练模式
total_loss = 0
correct = 0
total = 0
for inputs, labels in train_loader:
inputs, labels = inputs.to(DEVICE), labels.to(DEVICE)
optimizer.zero_grad() # 清零梯度,防止梯度累积
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward() # 反向传播计算梯度
# 梯度裁剪
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0)
optimizer.step() # 根据梯度更新模型参数
total_loss += loss.item() # .item()将单元素张量转为Python数值
_, predicted = outputs.max(1)
total += labels.size(0)
correct += predicted.eq(labels).sum().item() # 链式调用,连续执行多个方法
train_acc = 100. * correct / total
avg_loss = total_loss / len(train_loader)
scheduler.step(avg_loss)
print(f"Epoch [{epoch+1}/20] Loss: {avg_loss:.4f} | Acc: {train_acc:.2f}%")
# train_sentiment_model()
11.2 使用预训练词向量¶
import numpy as np
def load_pretrained_embeddings(vocab, embed_path='glove.6B.100d.txt', embed_dim=100):
"""加载GloVe预训练词向量"""
embeddings = np.random.normal(0, 0.1, (len(vocab), embed_dim))
embeddings[0] = 0 # PAD向量为零
with open(embed_path, 'r', encoding='utf-8') as f: # with open自动管理文件打开和关闭
for line in f:
parts = line.strip().split()
word = parts[0]
if word in vocab:
embeddings[vocab[word]] = np.array(parts[1:], dtype=np.float32) # 切片操作取子序列 # np.array创建NumPy数组
return torch.tensor(embeddings, dtype=torch.float32)
# 使用预训练嵌入初始化
# pretrained = load_pretrained_embeddings(vocab)
# model.embedding.weight.data.copy_(pretrained)
# model.embedding.weight.requires_grad = True # 可选:是否微调
12. RNN vs Transformer对比¶
| 特性 | RNN (LSTM/GRU) | Transformer |
|---|---|---|
| 并行化 | ❌ 时间步必须顺序计算 | ✅ 所有位置可并行计算 |
| 长距离依赖 | 理论上可以,实际受限 | 通过self-attention直接连接所有位置 |
| 位置信息 | 自然编码在递推过程中 | 需要额外的位置编码 |
| 计算复杂度 | \(O(T \cdot d^2)\)(\(T\)为序列长度) | \(O(T^2 \cdot d)\) |
| 内存开销 | 只需存储当前隐藏状态 | 需要存储 \(T \times T\) 注意力矩阵 |
| 训练速度 | 慢(无法并行) | 快(GPU并行友好) |
| 推理速度 | 流式推理友好 | 需要缓存KV(KV-Cache) |
| 实际表现 | 在小数据/短序列上仍有竞争力 | 大数据+长序列的首选 |
| 适用场景 | 实时系统、资源受限设备 | 大部分NLP和CV任务 |
趋势:Transformer已在大多数任务上取代RNN成为主流。但RNN在以下场景仍有价值: - 流式/实时处理(如语音识别的在线推理) - 极长序列(Transformer的 \(O(T^2)\) 复杂度成为瓶颈) - 资源受限的边缘设备 - 状态空间模型(SSM,如Mamba)融合了RNN和Transformer的优点,是新的发展方向
13. 练习与自我检查¶
✏️ 练习题¶
-
手动推导:对一个3个时间步的RNN,手动展开BPTT,写出 \(\frac{\partial \mathcal{L}_3}{\partial \mathbf{W}_{hh}}\) 的完整链式法则表达式。
-
实现vanilla RNN:不使用
nn.RNN,从零用矩阵运算实现一个RNN单元,处理一个简单序列(如加法问题:输入两个数字序列,输出它们的和)。 -
LSTM门控分析:训练一个LSTM进行文本分类,可视化遗忘门在处理不同文本时的激活值,分析它学到了什么(哪些词被"记住",哪些被"遗忘")。
-
LSTM vs GRU:在IMDb情感分析数据集上分别使用LSTM和GRU,对比准确率、训练时间和参数量。
-
序列生成:用LSTM训练一个字符级语言模型(character-level LM),输入莎士比亚文本,生成类似风格的文本。
-
梯度实验:训练不同层数的vanilla RNN(1层/3层/5层),记录各层梯度的范数变化,验证梯度消失现象。然后换成LSTM,观察梯度行为的变化。
-
时序预测:使用LSTM预测正弦波的下一个值,对比不同隐藏维度和层数的效果。
面试要点¶
Q1: 解释LSTM如何解决梯度消失问题? A: LSTM引入了细胞状态作为"记忆传送带",通过遗忘门控制信息的保留。当遗忘门接近1时,梯度可以无损地通过细胞状态传递,避免了标准RNN中梯度连乘导致的指数衰减。
Q2: LSTM和GRU的主要区别? A: GRU用2个门(重置门+更新门)替代LSTM的3个门和细胞状态,参数量少25%,训练更快。GRU的更新门兼具LSTM遗忘门和输入门的功能。实际效果两者通常相当。
Q3: 为什么Transformer逐渐取代RNN? A: Transformer解决了RNN的两大问题——无法并行(self-attention可以并行计算所有位置)和长距离依赖困难(任意两个位置直接相连)。在大数据和GPU加速下,Transformer训练效率远超RNN。
Q4: RNN还有什么实用价值? A: 流式推理(在线语音识别)、超长序列(内存友好)、边缘设备(参数量小)。此外SSM(如Mamba)在RNN框架下实现了Transformer级别的性能。
自我检查清单¶
- 能手写RNN的前向传播公式
- 理解BPTT的完整推导过程
- 能解释梯度消失的数学原因(雅可比矩阵连乘)
- 能默写LSTM四个公式(遗忘门、输入门、细胞更新、输出门)
- 理解LSTM为什么解决梯度消失(对比\(\frac{\partial C_t}{\partial C_{t-1}}\))
- 能说清GRU和LSTM的核心差异
- 理解双向RNN的使用场景和限制
- 能实现完整的LSTM文本分类流程
- 了解Seq2Seq的瓶颈以及注意力机制如何解决它
下一章: 10-Transformer架构 — 从注意力机制到改变世界的Transformer










