|
# 一、课程概述
本课程介绍循环神经网络(RNN)及其变体LSTM和GRU,通过经典人名分类案例,完整演示从数据准备到模型训练、评估的全流程。
### 课程定位
这是一个**理论与实践结合**的课程,旨在帮助学习者:
1. 理解RNN系列模型的核心原理
2. 掌握PyTorch实现序列建模
3. 了解工业界最佳实践
4. 建立从传统RNN到Transformer的知识桥梁
### 内容组织
```
理论基础 → 模型详解 → 代码实现 → 扩展知识 → 总结提升
```
|
|
# 二、案例介绍:人名国籍分类
### 任务说明
根据人名的拼写预测其所属的语言/国家
### 输入输出
| 项目 | 说明 |
|------|------|
| **输入** | 人名字符串,如 "Zhang"、"Smith"、"Satoshi" |
| **输出** | 18个类别之一(语言/国家) |
| **数据量** | 约20000个人名 |
| **类别** | Arabic, Chinese, Czech, Dutch, English, French, German, Greek, Irish, Italian, Japanese, Korean, Polish, Portuguese, Russian, Scottish, Spanish, Vietnamese |
### 应用场景
- **用户注册**:自动推断用户国籍,优化推荐
- **智能客服**:根据用户姓名分流服务
- **数据清洗**:标记数据来源国家
- **风控系统**:识别异常 nationality
### 为什么选择这个案例?
1. **数据规模适中**:适合快速实验
2. **任务清晰**:易于理解评估指标
3. **实用价值**:可直接应用于生产
4. **扩展性强**:可延伸到文本分类
|
|
# 三、学习目标
### 理论目标
- [ ] 理解RNN的基本原理和设计动机
- [ ] 掌握梯度消失/爆炸问题的成因
- [ ] 深入理解LSTM的门控机制
- [ ] 对比LSTM与GRU的设计差异
### 实践目标
- [ ] 能够使用PyTorch实现RNN/LSTM/GRU
- [ ] 掌握序列数据的预处理方法
- [ ] 学会训练和评估序列模型
- [ ] 了解模型调优的实用技巧
### 扩展目标
- [ ] 理解双向RNN和注意力机制
- [ ] 掌握RNN与Transformer的选择
- [ ] 了解工业界最佳实践
- [ ] 建立完整的序列建模知识体系
|
|
# 四、RNN的应用场景
### 序列数据类型
| 数据类型 | 典型任务 | RNN适用性 |
|---------|---------|-----------|
| **时间序列** | 股票预测、天气预报、异常检测 | ⭐⭐⭐⭐ |
| **文本数据** | 机器翻译、文本分类、情感分析 | ⭐⭐⭐⭐⭐ |
| **语音数据** | 语音识别、语音合成、声纹识别 | ⭐⭐⭐⭐ |
| **视频数据** | 视频分类、动作识别、视频描述 | ⭐⭐⭐ |
| **生物序列** | DNA分析、蛋白质结构预测 | ⭐⭐⭐⭐ |
| **用户行为** | 点击预测、推荐系统、用户画像 | ⭐⭐⭐⭐ |
### 工业界案例
- **机器翻译**:Google翻译(早期使用RNN,现已转向Transformer)
- **语音助手**:Siri、Alexa(仍大量使用LSTM)
- **金融预测**:量化交易、风险评估
- **医疗诊断**:心电图分析、病历理解
|
|
# 五、RNN知识体系
### 核心概念树
```
序列建模
├── 传统RNN
│ ├── 基本结构
│ ├── 梯度问题
│ └── 长距离依赖
├── LSTM
│ ├── 门控机制
│ ├── 细胞状态
│ └── 变体形式
├── GRU
│ ├── 简化设计
│ ├── 性能对比
│ └── 选择建议
└── 扩展知识
├── 双向RNN
├── 注意力机制
├── 多层RNN
└── RNN vs Transformer
```
### 学习路径
```
第一阶段:理解问题 → 为什么需要RNN
↓
第二阶段:掌握基础 → RNN结构与训练
↓
第三阶段:解决问题 → LSTM/GRU门控机制
↓
第四阶段:动手实践 → PyTorch实现
↓
第五阶段:扩展视野 → 注意力、Transformer
```
|
|
# 一、为什么需要RNN?
### 传统神经网络的局限
传统神经网络(CNN、MLP)假设数据是**独立同分布(I.I.D.)**的:
```python
# 传统处理方式:丢失顺序信息
documents = ["我 喜欢 苹果", "苹果 喜欢 我"]
# 两个句子包含相同的词,但语义完全不同
# 传统模型会输出相似的表示,无法区分
```
### 序列数据的特征
序列数据的特点是:**当前元素的含义依赖于上下文**
| 示例 | 说明 |
|------|------|
| "我吃**苹果**" | 苹果 = 水果 |
| "我买**苹果**手机" | 苹果 = 品牌 |
| "股价**上涨**5%" | 当前值依赖历史趋势 |
| "今天**下雨**,明天**晴天**" | 天气存在时间连续性 |
### RNN的核心思想
**当前时刻的输出不仅取决于当前输入,还取决于上一时刻的隐藏状态**
```
传统NN: x → f(x) → y
RNN: [x_t, h_{t-1}] → f() → [y_t, h_t]
↑ ↑
利用历史 传递给未来
```
### 数学表达
```
h_t = f(W_h · [h_{t-1}, x_t] + b_h)
y_t = g(W_y · h_t + b_y)
```
其中:
- `h_{t-1}`:上一时刻的隐藏状态(记忆)
- `x_t`:当前时刻的输入
- `h_t`:当前时刻的隐藏状态
- `y_t`:当前时刻的输出
|
|
# 二、传统RNN的结构
### 展开图理解
RNN可以按时间步展开,形成链式结构:
```
时间步: t=1 t=2 t=3 ... t=T
输入: x_1 ──→ x_2 ──→ x_3 ──→ ... ──→ x_T
│ │ │ │
↓ ↓ ↓ ↓
状态: h_0 → h_1 ──→ h_2 ──→ h_3 ──→ ... ──→ h_T
│ │ │ │
↓ ↓ ↓ ↓
输出: y_1 y_2 y_3 ... y_T
```
### 信息流动过程
每个时间步的计算:
```
1. 接收当前输入 x_t
2. 结合上一状态 h_{t-1}
3. 计算新状态 h_t = tanh(W_h · [h_{t-1}, x_t])
4. 产生输出 y_t = softmax(W_y · h_t)
```
### PyTorch中的RNN
```python
import torch.nn as nn
# 单层RNN
rnn = nn.RNN(input_size=10, hidden_size=20)
# 输入: [seq_len, batch, input_size]
input = torch.randn(5, 1, 10) # 序列长度5,批量1,输入维度10
# 输出: output, hidden
output, hidden = rnn(input)
# output: [5, 1, 20] - 每个时间步的输出
# hidden: [1, 1, 20] - 最后的隐藏状态
```
### RNN的类型
| 类型 | 说明 | 应用 |
|------|------|------|
| **One-to-One** | 单输入单输出 | 图像分类 |
| **One-to-Many** | 单输入多输出 | 图像描述 |
| **Many-to-One** | 多输入单输出 | 情感分析 |
| **Many-to-Many** | 多输入多输出 | 机器翻译 |
|
|
# 三、梯度消失/爆炸问题
### 问题根源
RNN使用**随时间反向传播(BPTT)**训练:
```
梯度传播路径:
Loss → y_T → h_T → h_{T-1} → ... → h_2 → h_1
每步乘以 ∂h_t/∂h_{t-1}
```
### 梯度消失
当 `|∂h_t/∂h_{t-1}| < 1` 时:
```
∂L/∂h_1 = ∂L/∂h_T × ∏(t=2 to T) ∂h_t/∂h_{t-1}
↓
指数级衰减 → 0
```
**后果**:
- 无法学习长距离依赖
- 模型只能记住近期信息
- 训练困难,收敛慢
### 梯度爆炸
当 `|∂h_t/∂h_{t-1}| > 1` 时:
```
∂L/∂h_1 = ∂L/∂h_T × ∏(t=2 to T) ∂h_t/∂h_{t-1}
↓
指数级增长 → ∞
```
**后果**:
- 梯度变为NaN
- 模型参数剧烈震荡
- 训练不稳定
### 解决方案
| 方案 | 原理 | 效果 |
|------|------|------|
| **梯度裁剪** | 限制梯度最大值 | 缓解爆炸 |
| **更好的初始化** | Xavier/He初始化 | 部分缓解 |
| **ReLU激活** | 缓解梯度消失 | 有效 |
| **LSTM/GRU** | 细胞状态路径 | 根本解决 |
|
|
# 四、长距离依赖问题
### 问题表现
RNN难以跨越多个时间步传递信息:
```
句子: "The clouds are in the [sky]"
└───── 15个词 ─────┘
句子: "I grew up in France, ... [20 words] ..., I speak fluent [French]"
└────────────── 30+个词 ──────────────┘
↑
RNN无法记住开头
```
### 定量分析
| 序列长度 | RNN表现 | 保留的信息比例 |
|----------|---------|---------------|
| 10步 | 良好 | ~80% |
| 20步 | 中等 | ~50% |
| 50步 | 较差 | ~20% |
| 100步 | 失败 | ~5% |
### 为什么会遗忘?
```
信息传递路径:
h_0 → h_1 → h_2 → ... → h_50
↓ ↓ ↓ ↓
1.0 0.9 0.81 ... 0.005
每次传递都丢失约10%的信息
```
### 实际案例
**语音识别**:
- 短句:"Hello world" ✅ RNN可以处理
- 长句:"Could you please tell me the way to the nearest train station" ❌ RNN可能失败
**机器翻译**:
- 短句:"I love you" → "我爱你" ✅
- 长句:包含从句的复杂句子 ❌
这促使了LSTM和GRU的诞生!
|
|
# 一、LSTM:长短期记忆网络
### 历史背景
Hochreiter & Schmidhuber (1997) 提出,解决RNN的梯度消失问题
### 核心创新
1. **细胞状态(Cell State)**:信息高速公路,梯度无损传播
2. **门控机制**:三个门精确控制信息流
3. **双重状态**:隐藏状态(短期)+ 细胞状态(长期)
### LSTM的三个门
#### 遗忘门(Forget Gate)
```
f_t = σ(W_f · [h_{t-1}, x_t] + b_f)
```
- **作用**:决定从细胞状态中丢弃哪些信息
- **输出范围**:[0, 1]
- **0** = 完全遗忘,**1** = 完全保留
#### 输入门(Input Gate)
```
i_t = σ(W_i · [h_{t-1}, x_t] + b_i)
C~_t = tanh(W_C · [h_{t-1}, x_t] + b_C)
```
- **i_t**:决定更新哪些位置
- **C~_t**:候选的新信息
#### 输出门(Output Gate)
```
o_t = σ(W_o · [h_{t-1}, x_t] + b_o)
h_t = o_t ⊙ tanh(C_t)
```
- **作用**:决定输出什么信息
- **⊙**:逐元素乘法
### 细胞状态更新
```
C_t = f_t ⊙ C_{t-1} + i_t ⊙ C~_t
```
这是LSTM的核心!前半部分遗忘旧信息,后半部分添加新信息
### 可视化理解
```
C_{t-1} ─────────────────→ C_t
│ (梯度无损传播) │
↓ ↑
f_t ⊙ ⊙ i_t
↓ ↑
└──────┬───────────────┬─┘
↓ ↓
[+ 加权加法 + ]
↓ ↓
tanh() C~_t
↓
o_t ⊙
↓
h_t → 输出
```
|
|
# 二、GRU:门控循环单元
### 历史背景
Cho et al. (2014) 提出,LSTM的简化版本
### 简化思路
1. **合并遗忘门和输入门** → **更新门**
2. **合并细胞状态和隐藏状态** → 单一状态
3. 减少参数量,提高计算效率
### GRU的两个门
#### 更新门(Update Gate, z_t)
```
z_t = σ(W_z · [h_{t-1}, x_t] + b_z)
```
- **作用**:控制前一状态的保留程度
- **z_t ≈ 0**:完全保留前一状态
- **z_t ≈ 1**:完全使用候选状态
#### 重置门(Reset Gate, r_t)
```
r_t = σ(W_r · [h_{t-1}, x_t] + b_r)
```
- **作用**:控制前一状态对候选状态的贡献
- **r_t ≈ 0**:忽略前一状态(重新开始)
- **r_t ≈ 1**:使用前一状态
### GRU的状态更新
```
# 候选隐藏状态
h~_t = tanh(W · [r_t ⊙ h_{t-1}, x_t] + b)
# 最终隐藏状态
h_t = (1 - z_t) ⊙ h_{t-1} + z_t ⊙ h~_t
```
### 直观理解
可以把GRU理解为两个阶段的平衡:
1. **重置阶段**:决定在计算候选状态时"忘掉"多少过去
2. **更新阶段**:在"保留旧信息"和"接受新信息"之间平衡
### 与LSTM的对应
| LSTM | GRU | 说明 |
|------|-----|------|
| 遗忘门 f_t | 1 - z_t | GRU中更新门的补数 |
| 输入门 i_t | z_t | GRU中直接用更新门 |
| 细胞状态 C_t | h_t | GRU合并到隐藏状态 |
| 输出门 o_t | (1 - z_t) | 隐式控制 |
|
|
# 三、LSTM vs GRU 详细对比
### 参数量对比
假设 input_size = n, hidden_size = h:
| 模型 | 参数量 | 公式 |
|------|--------|------|
| **RNN** | n×h + h² + h | 1组权重 |
| **LSTM** | 4×(n×h + h²) + 4h | 4组权重(3门+候选) |
| **GRU** | 3×(n×h + h²) + 3h | 3组权重(2门+候选) |
**GRU比LSTM少约25%的参数**
### 性能对比
| 维度 | LSTM | GRU |
|------|------|-----|
| **门数量** | 3个 | 2个 |
| **状态数量** | 2个(h, C) | 1个(h) |
| **参数量** | 更多 | 更少 |
| **训练速度** | 较慢 | 较快 |
| **推理速度** | 较慢 | 较快 |
| **内存占用** | 较高 | 较低 |
| **表达能力** | 更强 | 接近LSTM |
| **长序列能力** | 优秀 | 良好 |
| **调参难度** | 较高 | 较低 |
### 实验结果(人名分类)
| 模型 | 1000轮后损失 | 5000轮后损失 | 最终准确率 |
|------|-------------|-------------|-----------|
| RNN | 2.1 | 1.8 | ~78% |
| LSTM | 1.9 | 1.5 | ~85% |
| GRU | 1.8 | 1.4 | ~84% |
### 训练曲线对比
```
损失下降趋势:
RNN: ╱╲ ╱╲ ╱╲ (波动较大)
LSTM: ╲╲╲╲╲╲╲╲╲╲╲╲ (稳定但慢)
GRU: ╲──╲──╲──╲─── (快速稳定)
```
|
|
# 四、如何选择?
### 决策树
```
你的任务是什么?
│
├─ 超长序列(>100) + 追求极致精度
│ └→ LSTM
│
├─ 资源受限 + 快速迭代
│ └→ GRU
│
├─ 生产环境 + 通用场景
│ └→ GRU (更实用)
│
└─ 教学/理解原理
└→ 两者都学
```
### 场景推荐表
| 场景 | 推荐 | 原因 |
|------|------|------|
| **机器翻译** | LSTM | 长句子,需要强记忆 |
| **语音识别** | LSTM | 实时流,长序列 |
| **文本分类** | GRU | 效率高,够用 |
| **情感分析** | GRU | 序列较短 |
| **时间序列预测** | LSTM | 多步预测 |
| **边缘设备部署** | GRU | 资源受限 |
### 实践经验
1. **先用GRU**:大多数情况下效果接近,训练更快
2. **再试LSTM**:如果GRU不满足需求
3. **关注数据**:数据质量比模型选择更重要
4. **调参优先**:学习率、hidden_size比模型类型影响更大
### PyTorch实现对比
```python
# LSTM
lstm = nn.LSTM(input_size=57, hidden_size=128, num_layers=2)
output, (h_n, c_n) = lstm(input)
# GRU
gru = nn.GRU(input_size=57, hidden_size=128, num_layers=2)
output, h_n = gru(input)
# API几乎一致,GRU更简单
```
|
|
# 一、数据预处理
### 字符集定义
```python
import string
# 52个字母 + 常用标点
all_letters = string.ascii_letters + " .,;'"
n_letters = len(all_letters) # 57
```
### Unicode转ASCII
```python
import unicodedata
def unicodeToAscii(s):
"""去除重音标记:Ślusàrski → Slusarski"""
return ''.join(
c for c in unicodedata.normalize('NFD', s)
if unicodedata.category(c) != 'Mn'
and c in all_letters
)
# 示例
print(unicodeToAscii('Ślusàrski')) # Slusarski
print(unicodeToAscii('ÅÖé')) # AOE
```
### 读取数据文件
```python
from io import open
import glob
import os
def readLines(filename):
"""从文件读取并转为ASCII"""
lines = open(filename, encoding='utf-8').read().strip().split('\n')
return [unicodeToAscii(line) for line in lines]
# 构建数据字典
category_lines = {}
all_categories = []
data_path = './data/names/'
for filename in glob.glob(data_path + '*.txt'):
category = os.path.splitext(os.path.basename(filename))[0]
all_categories.append(category)
category_lines[category] = readLines(filename)
n_categories = len(all_categories)
print(f'Categories: {n_categories}') # 18
```
### One-Hot编码
```python
import torch
def lineToTensor(line):
"""人名 → one-hot张量"""
tensor = torch.zeros(len(line), 1, n_letters)
for li, letter in enumerate(line):
tensor[li][0][all_letters.find(letter)] = 1
return tensor
# 输出形状: [len(line), 1, n_letters]
# 示例
line = 'Zhang'
tensor = lineToTensor(line)
print(tensor.shape) # torch.Size([5, 1, 57])
```
|
|
# 二、模型定义
### 通用RNN基类
```python
import torch.nn as nn
class RNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size, num_layers=1):
super(RNN, self).__init__()
self.hidden_size = hidden_size
self.num_layers = num_layers
self.rnn = nn.RNN(input_size, hidden_size, num_layers)
self.fc = nn.Linear(hidden_size, output_size)
self.softmax = nn.LogSoftmax(dim=-1)
def forward(self, input, hidden):
input = input.unsqueeze(0)
output, hidden = self.rnn(input, hidden)
return self.softmax(self.fc(output)), hidden
def initHidden(self):
return torch.zeros(self.num_layers, 1, self.hidden_size)
```
### LSTM模型
```python
class LSTM(nn.Module):
def __init__(self, input_size, hidden_size, output_size, num_layers=1):
super(LSTM, self).__init__()
self.hidden_size = hidden_size
self.num_layers = num_layers
self.lstm = nn.LSTM(input_size, hidden_size, num_layers)
self.fc = nn.Linear(hidden_size, output_size)
self.softmax = nn.LogSoftmax(dim=-1)
def forward(self, input, hidden, c):
input = input.unsqueeze(0)
output, (hidden, c) = self.lstm(input, (hidden, c))
return self.softmax(self.fc(output)), hidden, c
def initHiddenAndC(self):
c = hidden = torch.zeros(self.num_layers, 1, self.hidden_size)
return hidden, c
```
### GRU模型
```python
class GRU(nn.Module):
def __init__(self, input_size, hidden_size, output_size, num_layers=1):
super(GRU, self).__init__()
self.hidden_size = hidden_size
self.num_layers = num_layers
self.gru = nn.GRU(input_size, hidden_size, num_layers)
self.fc = nn.Linear(hidden_size, output_size)
self.softmax = nn.LogSoftmax(dim=-1)
def forward(self, input, hidden):
input = input.unsqueeze(0)
output, hidden = self.gru(input, hidden)
return self.softmax(self.fc(output)), hidden
def initHidden(self):
return torch.zeros(self.num_layers, 1, self.hidden_size)
```
### 模型初始化
```python
n_hidden = 128
rnn = RNN(n_letters, n_hidden, n_categories)
lstm = LSTM(n_letters, n_hidden, n_categories)
gru = GRU(n_letters, n_hidden, n_categories)
```
|
|
# 三、训练流程
### 随机训练样本
```python
import random
def randomTrainingExample():
"""随机生成训练样本"""
category = random.choice(all_categories)
line = random.choice(category_lines[category])
category_tensor = torch.tensor([all_categories.index(category)],
dtype=torch.long)
line_tensor = lineToTensor(line)
return category, line, category_tensor, line_tensor
```
### 训练函数
```python
criterion = nn.NLLLoss() # 配合LogSoftmax使用
learning_rate = 0.005
def train(category_tensor, line_tensor):
"""单个样本训练"""
hidden = model.initHidden()
model.zero_grad()
# 逐字符输入
for i in range(line_tensor.size()[0]):
output, hidden = model(line_tensor[i], hidden)
loss = criterion(output.squeeze(0), category_tensor)
loss.backward()
# 手动更新参数
for p in model.parameters():
p.data.add_(-learning_rate, p.grad.data)
return output, loss.item()
```
### 主训练循环
```python
import time
n_iters = 10000
print_every = 500
plot_every = 100
all_losses = []
total_loss = 0
start = time.time()
for iter in range(1, n_iters + 1):
category, line, category_tensor, line_tensor = randomTrainingExample()
output, loss = train(category_tensor, line_tensor)
total_loss += loss
if iter % print_every == 0:
guess, guess_i = categoryFromOutput(output)
correct = '✓' if guess == category else '✗'
print(f'{iter}{iter/n_iters*100:.0f}% {timeSince(start)} {loss:.4f} {line} / {guess} {correct}')
if iter % plot_every == 0:
all_losses.append(total_loss / plot_every)
total_loss = 0
```
|
|
# 四、评估与预测
### 评估函数
```python
def evaluate(line_tensor):
"""评估,不计算梯度"""
with torch.no_grad():
hidden = model.initHidden()
for i in range(line_tensor.size()[0]):
output, hidden = model(line_tensor[i], hidden)
return output.squeeze(0)
```
### 预测函数
```python
def predict(input_line, n_predictions=3):
"""预测Top-N结果"""
print(f'\n> {input_line}')
with torch.no_grad():
output = evaluate(lineToTensor(input_line))
topv, topi = output.topk(n_predictions, 1, True)
for i in range(n_predictions):
value = topv[0][i].item()
category_index = topi[0][i].item()
print(f'({value:.2f}) {all_categories[category_index]}')
# 测试
predict('Zhang')
predict('Smith')
predict('Satoshi')
predict('Muller')
```
### 输出示例
```
> Zhang
(-0.52) Chinese
(-1.23) Korean
(-2.45) Vietnamese
> Smith
(-0.34) English
(-1.67) Scottish
(-2.12) Irish
> Satoshi
(-0.41) Japanese
(-1.89) Arabic
(-2.34) Italian
```
### 完整评估矩阵
```python
from sklearn.metrics import confusion_matrix, classification_report
def evaluate_full(model, test_data):
"""完整评估"""
y_true, y_pred = [], []
for category, line in test_data:
output = evaluate(lineToTensor(line))
pred = categoryFromOutput(output)[0]
y_true.append(category)
y_pred.append(pred)
print(classification_report(y_true, y_pred))
```
|
|
# 一、双向RNN(Bidirectional RNN)
### 核心思想
当前输出不仅取决于过去,还取决于未来信息
### 可视化对比
```
单向RNN:
x_1 → x_2 → x_3 → x_4
↓ ↓ ↓ ↓
h_1 → h_2 → h_3 → h_4 → output
双向RNN:
x_1 → x_2 → x_3 → x_4
↓ ↓ ↓ ↓
h_1 → h_2 → h_3 → h_4
↘ ↙
concat
↓
output
↑ ↑ ↑ ↑
h_4'← h_3'← h_2'← h_1' (反向)
```
### PyTorch实现
```python
class BiRNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(BiRNN, self).__init__()
self.hidden_size = hidden_size
# 双向RNN
self.rnn = nn.GRU(input_size, hidden_size,
bidirectional=True)
# 双向输出维度为 2 * hidden_size
self.fc = nn.Linear(hidden_size * 2, output_size)
def forward(self, input):
# input: [seq_len, batch, input_size]
output, _ = self.rnn(input)
# output: [seq_len, batch, hidden_size * 2]
output = self.fc(output[-1]) # 取最后时间步
return output
```
### 适用场景
| 场景 | 是否适合双向 | 原因 |
|------|-------------|------|
| 文本分类 | ✅ 适合 | 整篇文章可读 |
| 命名实体识别 | ✅ 适合 | 需要前后文 |
| 情感分析 | ✅ 适合 | 上下文重要 |
| 实时翻译 | ❌ 不适合 | 无法看到未来 |
| 股票预测 | ❌ 不适合 | 不能用未来数据 |
| 语音识别 | ✅ 适合 | 整句语音可用 |
### 实际案例
```python
# 案例:判断Apple是水果还是公司
# 需要看上下文
# 句子1: "I ate an [Apple]" → 水果
# 单向RNN只能看到前面的词
# 句子2: "[Apple] released a new phone" → 公司
# 双向RNN可以看到后面的"released"
```
|
|
# 二、注意力机制(Attention)
### 问题背景
RNN仍受限于固定长度的隐藏状态,难以处理超长序列
### 注意力机制原理
允许模型在输出时"关注"输入的不同部分:
```
传统RNN:
Input → h1 → h2 → ... → hN → Fixed_Vector → Output
注意力机制:
Input → h1, h2, ..., hN → Dynamic_Weighted_Sum → Output
↑ ↑ ↑
权重低 权重高 权重低
```
### 简单实现
```python
class AttentionRNN(nn.Module):
def __init__(self, hidden_size):
super().__init__()
self.rnn = nn.GRU(hidden_size, hidden_size,
batch_first=True)
self.attention = nn.Linear(hidden_size, 1)
def forward(self, x):
# x: [batch, seq_len, hidden_size]
rnn_output, _ = self.rnn(x)
# 计算注意力权重
attn_scores = self.attention(rnn_output) # [batch, seq_len, 1]
attn_weights = torch.softmax(attn_scores, dim=1)
# 加权求和
context = torch.sum(attn_weights * rnn_output, dim=1)
return context, attn_weights
```
### 注意力的优势
| 特性 | RNN | RNN + Attention |
|------|-----|----------------|
| 长距离依赖 | 差 | 好 |
| 可解释性 | 低 | 高(可看权重) |
| 计算复杂度 | O(n) | O(n²) |
| 性能提升 | 基准 | 显著 |
### 可视化注意力
```
句子: "The animal didn't cross the street because it was too tired"
单词: it
注意力: ━━━━━━━━━━━━━━━━★━━━ (重点关注animal)
```
|
|
# 三、实用技巧集锦
### 梯度裁剪
防止梯度爆炸:
```python
# 方法1:按范数裁剪
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
# 方法2:按值裁剪
torch.nn.utils.clip_grad_value_(model.parameters(), clip_value=1.0)
```
### Dropout
防止过拟合:
```python
class RNNWithDropout(nn.Module):
def __init__(self, input_size, hidden_size, dropout=0.3):
super().__init__()
self.rnn = nn.GRU(input_size, hidden_size,
num_layers=2, dropout=dropout)
self.dropout = nn.Dropout(dropout)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x):
output, _ = self.rnn(x)
output = self.dropout(output[-1])
return self.fc(output)
```
### 学习率调度
```python
import torch.optim as optim
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 方法1:ReduceLROnPlateau
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
optimizer, mode='min', factor=0.5, patience=2
)
# 方法2:CosineAnnealing
scheduler = optim.lr_scheduler.CosineAnnealingLR(
optimizer, T_max=10, eta_min=1e-6
)
# 训练循环
for epoch in range(epochs):
loss = train_one_epoch()
scheduler.step(loss) # 或 scheduler.step()
```
### Embedding层(替代One-Hot)
```python
class RNNWithEmbedding(nn.Module):
def __init__(self, vocab_size, embed_size, hidden_size, output_size):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_size)
self.rnn = nn.GRU(embed_size, hidden_size)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x):
# x: [seq_len, batch] (indices)
embedded = self.embedding(x) # [seq_len, batch, embed_size]
output, _ = self.rnn(embedded)
return self.fc(output[-1])
# 优势:更低维、更稠密、可学习
```
### Layer Normalization
```python
class RNNWithLayerNorm(nn.Module):
def __init__(self, input_size, hidden_size):
super().__init__()
self.rnn = nn.GRU(input_size, hidden_size)
self.layer_norm = nn.LayerNorm(hidden_size)
def forward(self, x):
output, _ = self.rnn(x)
output = self.layer_norm(output)
return output
```
|
|
# 四、RNN vs Transformer
### 全面对比
| 维度 | RNN | Transformer |
|------|-----|-------------|
| **并行计算** | ❌ 顺序处理 | ✅ 全部并行 |
| **长距离依赖** | 难以捕捉 | 自注意力直接连接 |
| **训练速度** | 慢 | 快(并行) |
| **推理速度** | 快(流式) | 慢(需全部输入) |
| **内存占用** | 低 O(n) | 高 O(n²) |
| **序列长度** | 适合短序列 | 适合长序列 |
| **位置信息** | 内置 | 需要位置编码 |
| **可解释性** | 中等 | 低 |
### 选择指南
#### 选择RNN/LSTM/GRU当:
- ✅ 需要流式/实时处理
- ✅ 计算资源受限
- ✅ 序列较短(<100 tokens)
- ✅ 边缘设备部署
- ✅ 低延迟推理需求
#### 选择Transformer当:
- ✅ 序列较长(>100 tokens)
- ✅ 需要捕捉复杂依赖
- ✅ 有充足计算资源
- ✅ 批处理场景
- ✅ 追求最优性能
### 实际应用场景
| 应用 | 主要模型 | 原因 |
|------|---------|------|
| **实时语音转录** | LSTM/RNN | 流式处理 |
| **机器翻译** | Transformer | 长序列、批处理 |
| **情感分析** | GRU/BERT | 短文本 |
| **文本生成** | GPT(Transformer) | 长距离依赖 |
| **时间序列预测** | LSTM | 时序特性 |
| **边缘AI设备** | GRU | 资源受限 |
### 混合方案
```python
# Transformer编码器 + RNN解码器
# 适合:编码长文本,流式生成输出
```
|
|
# 知识要点回顾
### 核心概念
1. **RNN基础**
- 处理序列数据的基本架构
- 梯度消失/爆炸问题
- 难以捕捉长距离依赖
2. **LSTM**
- 三个门:遗忘、输入、输出
- 细胞状态:梯度无损传播
- 解决梯度消失问题
3. **GRU**
- 两个门:更新、重置
- LSTM的简化版本
- 参数更少、训练更快
4. **实战应用**
- 人名分类器完整流程
- 数据预处理、模型定义
- 训练、评估、预测
5. **扩展知识**
- 双向RNN:利用未来信息
- 注意力机制:动态加权
- 与Transformer的选择
### 关键公式
```
# RNN
h_t = tanh(W_h · [h_{t-1}, x_t])
# LSTM
C_t = f_t ⊙ C_{t-1} + i_t ⊙ C~_t
h_t = o_t ⊙ tanh(C_t)
# GRU
h_t = (1 - z_t) ⊙ h_{t-1} + z_t ⊙ h~_t
```
|
|
# 学习路径建议
### 五个阶段
```
第一阶段:基础理解 (1-2周)
├── 传统RNN结构
├── 梯度消失/爆炸问题
└── PyTorch RNN API
第二阶段:门控机制 (2-3周)
├── LSTM三个门 + 细胞状态
├── GRU两个门设计
└── LSTM vs GRU选择
第三阶段:实战应用 (2-3周)
├── 序列数据处理
├── 模型训练与调优
└── 评估与部署
第四阶段:进阶扩展 (2-3周)
├── 双向RNN
├── 注意力机制
└── Transformer入门
第五阶段:生产实践 (持续)
├── 性能优化
├── 模型压缩
└── 部署方案
```
### 推荐资源
#### 经典论文
| 论文 | 年份 | 核心贡献 |
|------|------|----------|
| LSTM | 1997 | 提出长短期记忆网络 |
| GRU | 2014 | 提出门控循环单元 |
| Attention | 2014 | 提出注意力机制 |
| Transformer | 2017 | 自注意力架构 |
#### 实用资源
- **PyTorch官方文档**:RNN、LSTM、GRU API
- **Colah's Blog**:Understanding LSTM Networks
- **Andrej Karpathy博客**:The Unreasonable Effectiveness of RNN
- **Distill.pub**:可视化交互教程
|
|
# 常见问题FAQ
### Q1: 什么时候用RNN,什么时候用Transformer?
**答**:
- 短序列(<100)、实时处理 → RNN
- 长序列(>100)、批处理 → Transformer
### Q2: LSTM和GRU如何选择?
**答**:
- 数据量大、追求极致精度 → LSTM
- 资源受限、快速迭代 → GRU
- 大多数情况下GRU足够
### Q3: 如何处理超长序列?
**答**:
1. 截断/分段处理
2. 使用注意力机制
3. 考虑Transformer
### Q4: 梯度消失怎么检测?
**答**:
```python
# 监控梯度范数
for name, param in model.named_parameters():
if param.grad is not None:
print(f'{name}: {param.grad.norm()}')
```
### Q5: 多少层RNN合适?
**答**:
- 1层:简单任务、小数据集
- 2-3层:大多数任务(推荐)
- 4+层:需要大量数据和dropout
### Q6: batch_size如何选择?
**答**:
- 小(16-32):内存受限、需要正则化
- 中(64-128):一般情况
- 大(256+):数据充足、追求速度
|
|
## 参考文献
### 经典论文
1. **Hochreiter & Schmidhuber (1997)** - "Long Short-Term Memory"
2. **Cho et al. (2014)** - "Learning Phrase Representations using RNN Encoder-Decoder" (GRU)
3. **Vaswani et al. (2017)** - "Attention Is All You Need" (Transformer)
4. **Bahdanau et al. (2014)** - "Neural Machine Translation by Jointly Learning to Align and Translate"
### 推荐教程
- PyTorch官方教程:NLP From Scratch
- Colah's Blog:Understanding LSTM Networks
- Andrej Karpathy博客:The Unreasonable Effectiveness of RNN
- Distill.pub:Interactive LSTM Visualization
|
|
## 相关代码
### 完整代码
- Jupyter Notebook:`/ai/wks/ai58/0704-RNN/RNN_LSTM_GRU模型构建人名分类器.ipynb`
- Markdown笔记:`/ai/wks/ai58/0704-RNN/RNN_LSTM_GRU模型构建人名分类器.md`
### 数据集说明
- **来源**:来自多个国家的人名数据
- **类别**:18种语言/国家
- **大小**:约20000个人名
- **格式**:每个类别一个txt文件
### PyTorch API
```python
# 使用内置RNN/LSTM/GRU
import torch.nn as nn
rnn = nn.RNN(input_size=57, hidden_size=128)
lstm = nn.LSTM(input_size=57, hidden_size=128, num_layers=2)
gru = nn.GRU(input_size=57, hidden_size=128, bidirectional=True)
```
|