RNN概述#
为什么需要RNN?#
问题:全连接网络和CNN无法处理序列数据:
- 文本、语音、时间序列
- 需要记忆历史信息
- 输入长度可变
RNN的特点#
- 记忆能力:能够记住历史信息
- 参数共享:不同时间步共享参数
- 变长输入:可以处理不同长度的序列
应用场景#
- 自然语言处理:文本分类、机器翻译、文本生成
- 语音识别:语音转文本
- 时间序列预测:股票价格、天气预测
- 视频分析:动作识别、视频描述
基础RNN#
结构#
时间步 t-1: h_{t-1} ──┐
│
时间步 t: x_t → [RNN] → h_t → y_t
↑
└── h_t (传递到下一时间步)数学表示#
$$ h_t = \tanh(W_{hh} \cdot h_{t-1} + W_{xh} \cdot x_t + b_h) $$ $$ y_t = W_{hy} \cdot h_t + b_y $$其中:
- $h_t$ :隐藏状态(hidden state)
- $x_t$ :输入
- $y_t$ :输出
- $W_{hh}, W_{xh}, W_{hy}$ :权重矩阵
- $b_h, b_y$ :偏置
展开形式#
$$ h_0 = 0 $$ $$ h_1 = \tanh(W_{hh} \cdot h_0 + W_{xh} \cdot x_1 + b_h) $$ $$ h_2 = \tanh(W_{hh} \cdot h_1 + W_{xh} \cdot x_2 + b_h) $$ $$ \ldots $$ $$ h_T = \tanh(W_{hh} \cdot h_{T-1} + W_{xh} \cdot x_T + b_h) $$问题:梯度消失#
问题:深层RNN中,梯度在反向传播时指数衰减。
原因:
- 每个时间步都要乘以 $W_{hh}$
- 如果 $W_{hh}$ 的特征值 $< 1$ ,梯度会消失
- 如果 $W_{hh}$ 的特征值 $> 1$ ,梯度会爆炸
影响:
- 无法学习长期依赖
- 只能记住短期信息
LSTM#
概述#
LSTM(Long Short-Term Memory) 通过门控机制 解决梯度消失问题。
核心思想#
使用三个门 控制信息流:
- 遗忘门:决定丢弃什么信息
- 输入门:决定存储什么信息
- 输出门:决定输出什么信息
结构#
输入 x_t
↓
遗忘门 f_t = σ(W_f · [h_{t-1}, x_t] + b_f)
↓
输入门 i_t = σ(W_i · [h_{t-1}, x_t] + b_i)
候选值 C̃_t = tanh(W_C · [h_{t-1}, x_t] + b_C)
↓
细胞状态 C_t = f_t ⊙ C_{t-1} + i_t ⊙ C̃_t
↓
输出门 o_t = σ(W_o · [h_{t-1}, x_t] + b_o)
↓
隐藏状态 h_t = o_t ⊙ tanh(C_t)
↓
输出 y_t数学公式#
$$ f_t = \sigma(W_f \cdot [h_{t-1}, x_t] + b_f) \quad \text{(遗忘门)} $$ $$ i_t = \sigma(W_i \cdot [h_{t-1}, x_t] + b_i) \quad \text{(输入门)} $$ $$ \tilde{C}_t = \tanh(W_C \cdot [h_{t-1}, x_t] + b_C) \quad \text{(候选值)} $$ $$ C_t = f_t \odot C_{t-1} + i_t \odot \tilde{C}_t \quad \text{(细胞状态)} $$ $$ o_t = \sigma(W_o \cdot [h_{t-1}, x_t] + b_o) \quad \text{(输出门)} $$ $$ h_t = o_t \odot \tanh(C_t) \quad \text{(隐藏状态)} $$关键机制#
1. 遗忘门#
决定从细胞状态中丢弃什么:
$$ f_t = \sigma(\ldots) $$ $$ C_t = f_t \odot C_{t-1} + \ldots $$- $f_t = 1$ :完全保留
- $f_t = 0$ :完全遗忘
2. 输入门#
决定存储什么新信息:
$$ i_t = \sigma(\ldots) $$ $$ \tilde{C}_t = \tanh(\ldots) $$ $$ C_t = \ldots + i_t \odot \tilde{C}_t $$3. 输出门#
决定输出什么:
$$ o_t = \sigma(\ldots) $$ $$ h_t = o_t \odot \tanh(C_t) $$优势#
- 长期记忆:细胞状态可以长期保存信息
- 选择性遗忘:遗忘门控制信息流
- 梯度稳定:门控机制缓解梯度消失
GRU#
概述#
GRU(Gated Recurrent Unit) 是LSTM的简化版本,只有两个门。
结构#
$$ r_t = \sigma(W_r \cdot [h_{t-1}, x_t] + b_r) \quad \text{(重置门)} $$ $$ z_t = \sigma(W_z \cdot [h_{t-1}, x_t] + b_z) \quad \text{(更新门)} $$ $$ \tilde{h}_t = \tanh(W_h \cdot [r_t \odot h_{t-1}, x_t] + b_h) \quad \text{(候选隐藏状态)} $$ $$ h_t = (1 - z_t) \odot h_{t-1} + z_t \odot \tilde{h}_t \quad \text{(隐藏状态)} $$与LSTM的对比#
| 特性 | LSTM | GRU |
|---|---|---|
| 门数量 | 3个(遗忘、输入、输出) | 2个(重置、更新) |
| 参数 | 更多 | 更少 |
| 计算 | 较慢 | 较快 |
| 性能 | 通常更好 | 相当或略差 |
选择建议#
- LSTM:需要更强的长期记忆能力
- GRU:计算资源有限,需要更快训练
双向RNN#
原理#
同时使用前向 和后向 信息:
前向: x₁ → x₂ → ... → x_T
后向: x_T → x_{T-1} → ... → x₁结构#
前向RNN: h₁^f → h₂^f → ... → h_T^f
后向RNN: h_T^b ← h_{T-1}^b ← ... ← h₁^b
↓
拼接: h_t = [h_t^f, h_t^b]应用#
- 文本分类:利用上下文信息
- 命名实体识别:识别实体边界
- 机器翻译:理解完整句子
代码实现#
基础RNN#
import torch
import torch.nn as nn
class SimpleRNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(SimpleRNN, self).__init__()
self.hidden_size = hidden_size
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, seq_len, input_size]
out, h_n = self.rnn(x)
# out: [batch, seq_len, hidden_size]
# h_n: [1, batch, hidden_size]
# 使用最后一个时间步的输出
out = self.fc(out[:, -1, :])
return out
# 使用示例
model = SimpleRNN(input_size=10, hidden_size=64, output_size=1)
x = torch.randn(32, 20, 10) # [batch=32, seq_len=20, input_size=10]
output = model(x) # [32, 1]LSTM#
class LSTMModel(nn.Module):
def __init__(self, input_size, hidden_size, num_layers, output_size):
super(LSTMModel, self).__init__()
self.hidden_size = hidden_size
self.num_layers = num_layers
self.lstm = nn.LSTM(
input_size,
hidden_size,
num_layers,
batch_first=True,
dropout=0.2 if num_layers > 1 else 0
)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x):
# x: [batch, seq_len, input_size]
out, (h_n, c_n) = self.lstm(x)
# h_n: [num_layers, batch, hidden_size]
# c_n: [num_layers, batch, hidden_size]
# 使用最后一层的最后一个时间步
out = self.fc(h_n[-1])
return out
# 使用示例
model = LSTMModel(input_size=10, hidden_size=128, num_layers=2, output_size=1)
x = torch.randn(32, 20, 10)
output = model(x) # [32, 1]GRU#
class GRUModel(nn.Module):
def __init__(self, input_size, hidden_size, num_layers, output_size):
super(GRUModel, self).__init__()
self.gru = nn.GRU(
input_size,
hidden_size,
num_layers,
batch_first=True,
dropout=0.2 if num_layers > 1 else 0
)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x):
out, h_n = self.gru(x)
out = self.fc(h_n[-1])
return out双向RNN#
class BidirectionalRNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(BidirectionalRNN, self).__init__()
self.rnn = nn.LSTM(
input_size,
hidden_size,
batch_first=True,
bidirectional=True # 双向
)
self.fc = nn.Linear(hidden_size * 2, output_size) # 双向需要*2
def forward(self, x):
out, (h_n, c_n) = self.rnn(x)
# h_n: [2, batch, hidden_size] (前向+后向)
# 拼接前向和后向的最后一个隐藏状态
h_forward = h_n[0] # 前向
h_backward = h_n[1] # 后向
h_concat = torch.cat([h_forward, h_backward], dim=1)
out = self.fc(h_concat)
return out文本分类示例#
class TextClassifier(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_size, num_classes):
super(TextClassifier, self).__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim)
self.lstm = nn.LSTM(embed_dim, hidden_size, batch_first=True, bidirectional=True)
self.fc = nn.Linear(hidden_size * 2, num_classes)
self.dropout = nn.Dropout(0.5)
def forward(self, x):
# x: [batch, seq_len] (token indices)
x = self.embedding(x) # [batch, seq_len, embed_dim]
out, (h_n, c_n) = self.lstm(x)
# 使用最后一个时间步的输出
out = torch.cat([h_n[-2], h_n[-1]], dim=1) # 双向拼接
out = self.dropout(out)
out = self.fc(out)
return out序列到序列(Seq2Seq)#
class Seq2Seq(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(Seq2Seq, self).__init__()
self.encoder = nn.LSTM(input_size, hidden_size, batch_first=True)
self.decoder = nn.LSTM(input_size, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x_enc, x_dec):
# 编码器
_, (h_enc, c_enc) = self.encoder(x_enc)
# 解码器(使用编码器的隐藏状态)
out_dec, _ = self.decoder(x_dec, (h_enc, c_enc))
# 输出
out = self.fc(out_dec)
return out总结#
- RNN:处理序列数据,具有记忆能力
- 梯度消失:基础RNN的主要问题
- LSTM:通过门控机制解决梯度消失,适合长期依赖
- GRU:LSTM的简化版本,计算更快
- 双向RNN:利用前后文信息
关键要点:
- RNN适合处理序列数据
- LSTM/GRU解决长期依赖问题
- 双向RNN利用完整上下文
- 注意梯度消失和爆炸问题