概述
循环神经网络(Recurrent Neural Network, RNN)是一类专门用于处理序列数据的神经网络。与前馈神经网络不同,RNN 具有隐藏状态,能够记住之前的信息并将其用于当前时刻的计算。1
RNN 的核心思想
RNN 的”循环”体现在:同一个网络层在每个时间步被重复使用,权重共享使得网络能够处理任意长度的序列。
时间步 t-1 时间步 t 时间步 t+1
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ x_{t-1}│ │ x_t │ │ x_{t+1}│
└────┬───┘ └────┬───┘ └────┬───┘
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ h_{t-1} │ │ h_t │ │ h_{t+1} │
└─────────┘ └─────────┘ └─────────┘
RNN 的结构
基本 RNN 单元
RNN 的核心方程:
其中:
- :时刻 的输入
- :时刻 的隐藏状态
- :输入到隐藏的权重矩阵
- :隐藏到隐藏的权重矩阵(循环权重)
- :非线性激活函数(如 或 )
PyTorch 实现
import torch
import torch.nn as nn
class SimpleRNN(nn.Module):
"""基础RNN实现"""
def __init__(self, input_size, hidden_size, output_size):
super().__init__()
self.hidden_size = hidden_size
# 输入到隐藏
self.W_xh = nn.Linear(input_size, hidden_size)
# 隐藏到隐藏(循环连接)
self.W_hh = nn.Linear(hidden_size, hidden_size, bias=False)
# 隐藏到输出
self.W_hy = nn.Linear(hidden_size, output_size)
self.tanh = nn.Tanh()
def forward(self, x, h_prev=None):
"""
Args:
x: (batch_size, input_size) 或 (batch_size, seq_len, input_size)
h_prev: (batch_size, hidden_size) 初始隐藏状态
Returns:
output: (batch_size, output_size)
h: (batch_size, hidden_size)
"""
if h_prev is None:
h_prev = torch.zeros(x.size(0), self.hidden_size, device=x.device)
# RNN 核心方程
h = self.tanh(self.W_xh(x) + self.W_hh(h_prev))
y = self.W_hy(h)
return y, h多层 RNN
class MultiLayerRNN(nn.Module):
"""多层RNN"""
def __init__(self, input_size, hidden_size, num_layers, output_size, dropout=0.0):
super().__init__()
self.num_layers = num_layers
self.rnn_layers = nn.ModuleList()
self.rnn_layers.append(nn.RNN(input_size, hidden_size, batch_first=True))
for _ in range(num_layers - 1):
self.rnn_layers.append(nn.RNN(hidden_size, hidden_size, batch_first=True))
self.dropout = nn.Dropout(dropout)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x, h0=None):
"""
Args:
x: (batch_size, seq_len, input_size)
Returns:
output: (batch_size, seq_len, output_size)
h_n: (num_layers, batch_size, hidden_size)
"""
if h0 is None:
h0 = [None] * self.num_layers
h_n = []
for i, layer in enumerate(self.rnn_layers):
x, h = layer(x, h0[i])
if i < self.num_layers - 1:
x = self.dropout(x)
h_n.append(h)
# 只取最后一个时间步的输出
output = self.fc(x)
return output, torch.stack(h_n)前向传播
单步前向传播
对于单个时间步 :
def rnn_step_forward(x_t, h_prev, params):
"""
单步前向传播
Args:
x_t: (hidden_size,) 当前输入
h_prev: (hidden_size,) 上一时刻隐藏状态
params: 参数字典
Returns:
h: (hidden_size,) 当前隐藏状态
cache: 用于反向传播的缓存
"""
W_xh = params['W_xh']
W_hh = params['W_hh']
b = params['b']
# 循环连接的核心
h_raw = np.dot(W_xh, x_t) + np.dot(W_hh, h_prev) + b
h = np.tanh(h_raw) # 激活函数
cache = (x_t, h_prev, h_raw, W_xh, W_hh)
return h, cache序列前向传播
def rnn_forward(x, h0, params):
"""
完整序列的前向传播
Args:
x: (seq_len, input_size) 输入序列
h0: (hidden_size,) 初始隐藏状态
Returns:
h: (seq_len, hidden_size) 所有时间步的隐藏状态
cache: 反向传播所需的缓存
"""
seq_len, input_size = x.shape
hidden_size = h0.shape[0]
h = np.zeros((seq_len, hidden_size))
cache = []
h_prev = h0
for t in range(seq_len):
h[t], cache_t = rnn_step_forward(x[t], h_prev, params)
h_prev = h[t]
cache.append(cache_t)
return h, cache反向传播 Through Time(BPTT)
梯度推导
RNN 的反向传播称为 BPTT(Backpropagation Through Time),因为需要将误差沿着时间反向传播。
设损失函数为 ,对每个参数求偏导:
梯度消失与梯度爆炸
核心问题:BPTT 中需要计算雅可比矩阵的乘积
梯度消失:当 时,梯度指数衰减
梯度爆炸:当 时,梯度指数增长
BPTT 实现
def rnn_step_backward(dh_next, cache):
"""
单步反向传播
Args:
dh_next: (hidden_size,) 从下一时间步传来的梯度
cache: 前向传播时保存的缓存
Returns:
dx: (input_size,) 对输入的梯度
dh_prev: (hidden_size,) 对上一时刻隐藏状态的梯度
grads: 权重梯度字典
"""
x_t, h_prev, h_raw, W_xh, W_hh = cache
# tanh 的梯度: d(tanh(x))/dx = 1 - tanh²(x)
dh_raw = dh_next * (1 - h_raw**2)
# 权重梯度
dW_xh = np.outer(dh_raw, x_t)
dW_hh = np.outer(dh_raw, h_prev)
db = dh_raw
# 对输入和上一隐藏状态的梯度
dx = np.dot(W_xh.T, dh_raw)
dh_prev = np.dot(W_hh.T, dh_raw)
grads = {'W_xh': dW_xh, 'W_hh': dW_hh, 'b': db}
return dx, dh_prev, grads
def bptt_forward_backward(x, h0, params, target):
"""
完整的 BPTT
Args:
x: (seq_len, input_size) 输入序列
h0: (hidden_size,) 初始隐藏状态
params: 参数字典
target: (seq_len, output_size) 目标序列
Returns:
loss: 损失值
grads: 梯度字典
"""
seq_len = x.shape[0]
# 前向传播
h, forward_cache = rnn_forward(x, h0, params)
# 计算损失(简化:MSE)
loss = np.sum((h - target)**2) / 2
# 初始化梯度
grads = {k: np.zeros_like(v) for k, v in params.items()}
# 初始化 dh_next(从损失对最终隐藏状态的梯度)
dh_next = h[-1] - target[-1] # 假设MSE损失
# 反向传播(从最后一个时间步到第一个)
for t in reversed(range(seq_len)):
cache = forward_cache[t]
dx, dh_prev, step_grads = rnn_step_backward(dh_next, cache)
# 累加梯度
for k in grads:
grads[k] += step_grads.get(k, 0)
# 继续反向传播到更早的时间步
dh_next = dh_prev
return loss, grads梯度问题与解决方案
梯度裁剪
防止梯度爆炸的最简单有效的方法:
def clip_gradients(grads, max_norm=5.0):
"""梯度裁剪"""
total_norm = 0.0
for g in grads.values():
total_norm += np.sum(g**2)
total_norm = np.sqrt(total_norm)
clip_coef = max_norm / (total_norm + 1e-6)
if clip_coef < 1:
for k in grads:
grads[k] *= clip_coef
return grads梯度裁剪的 PyTorch 实现
# 方法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)截断 BPTT(Truncated BPTT)
处理超长序列的实用技巧:
class TruncatedBPTTRNN(nn.Module):
"""截断BPTT的RNN"""
def __init__(self, input_size, hidden_size, output_size, seq_len=50):
super().__init__()
self.rnn = nn.RNN(input_size, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)
self.seq_len = seq_len
def forward(self, x, h=None):
"""
处理超长序列,使用截断BPTT
"""
batch_size, total_len, input_size = x.shape
# 分段处理
h_cur = h
outputs = []
for start in range(0, total_len, self.seq_len):
end = min(start + self.seq_len, total_len)
segment = x[:, start:end, :]
out, h_cur = self.rnn(segment, h_cur)
# detach 梯度,防止反向传播到之前的段
h_cur = h_cur.detach()
outputs.append(out)
# 合并所有段的输出
output = torch.cat(outputs, dim=1)
return self.fc(output), h_curRNN 的应用场景
| 任务类型 | 输入 | 输出 | 典型应用 |
|---|---|---|---|
| Many-to-One | 序列 | 单个值 | 情感分析、文本分类 |
| One-to-Many | 单个值 | 序列 | 图像描述生成 |
| Many-to-Many | 序列 | 序列 | 机器翻译、语音识别 |
| 同步 Many-to-Many | 序列 | 序列 | 帧级视频分类 |
情感分类示例
class SentimentRNN(nn.Module):
"""基于RNN的情感分类器"""
def __init__(self, vocab_size, embedding_dim, hidden_size, num_classes=2):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embedding_dim)
self.rnn = nn.RNN(embedding_dim, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, num_classes)
def forward(self, x):
"""
Args:
x: (batch_size, seq_len) 词索引序列
Returns:
logits: (batch_size, num_classes)
"""
# 词嵌入
embedded = self.embedding(x) # (batch, seq_len, embed_dim)
# RNN 前向传播
output, hidden = self.rnn(embedded) # output: (batch, seq_len, hidden)
# 取最后一个时间步的隐藏状态
final_hidden = output[:, -1, :] # (batch, hidden)
# 分类
logits = self.fc(final_hidden)
return logitsRNN 的局限性
1. 长期依赖问题
RNN 难以学习相距很远的时间步之间的依赖关系,因为梯度在反向传播时会指数衰减。
2. 训练困难
- 梯度爆炸:需要梯度裁剪
- 梯度消失:需要特殊架构(LSTM、GRU)
3. 并行化困难
序列的时序依赖使得 GPU 并行化效率不高。
4. 表达能力有限
标准 RNN 的隐藏状态容量有限,无法记住大量信息。
从 RNN 到 LSTM
LSTM(Long Short-Term Memory)通过引入门控机制解决了 RNN 的长期依赖问题。详见 LSTM 详解。
LSTM 的关键创新:
- 细胞状态(Cell State):信息传输的”传送带”
- 门控机制:遗忘门、输入门、输出门
- 短期/长期记忆分离:更好地处理长期依赖
参考
相关阅读
- LSTM 详解 — 解决 RNN 长期依赖问题的门控架构
- xLSTM 与现代 LSTM 变体 — 最新的 LSTM 扩展研究
- LSTM 与状态空间对偶性 — SSM 如何统一 RNN 与 Transformer
- Transformer 与注意力机制 — 完全基于注意力的序列建模
Footnotes
-
Goodfellow, I., Bengio, Y., & Courville, A. (2016). Deep Learning. MIT Press. ↩