循环神经网络(Recurrent Neural Network,RNN)是一类专门用于处理序列数据的神经网络架构。与传统前馈网络(Feedforward Neural Network)不同,RNN 通过引入循环连接(recurrent connection)使网络具备记忆能力,能够捕获序列中的时间依赖关系。RNN 及其变体 LSTM(Long Short-Term Memory)和 GRU(Gated Recurrent Unit)在自然语言处理、语音识别、时间序列预测等任务中曾长期占据主导地位。
前馈网络假设输入之间相互独立,输入维度固定。这在处理变长序列(如句子、股票价格时间序列)时面临根本性困难:
- 变长输入:一句话有 5 个词,另一句有 50 个词,前馈网络无法处理不同长度的输入
- 时序依赖:前馈网络无法利用上下文(例如,"打"在"打篮球"和"打工人"中的含义完全不同,需要前后文判断)
- 参数爆炸:若将长度为 T 的序列展平为固定向量,参数量随 T 线性增长
一个长度为 T 的序列可表示为:
{x1,x2,…,xT},xt∈Rd
其中 d 是每个时间步的特征维度(例如词向量的维度),T 是序列长度。RNN 的核心思想是维护一个隐状态(hidden state)ht,在每个时间步更新:
ht=f(ht−1,xt)
这使得网络能够"记住"过去的信息,并将记忆传递到未来。
一个最简单的 RNN 单元在每个时间步 t 执行以下计算:
ht=tanh(Wihxt+bih+Whhht−1+bhh)
其中:
- xt∈Rd:当前时间步的输入
- ht−1∈Rh:上一时间步的隐状态
- Wih∈Rh×d:输入到隐状态的权重矩阵
- Whh∈Rh×h:隐状态到隐状态的循环权重矩阵
- bih,bhh∈Rh:偏置项
- tanh:激活函数,将输出压缩到 (−1,1) 区间
输出层通常为:
yt=softmax(Whoht+bo)
将 RNN 在时间维度上展开后,它等价于一个深度共享权重的前馈网络:
y₁ y₂ y₃ y_T
↑ ↑ ↑ ↑
h₀ → h₁ → h₂ → h₃ → ... → h_T
↑ ↑ ↑ ↑
x₁ x₂ x₃ x_T
每个时间步共享相同的权重 Wih,Whh,这使得 RNN 能够处理任意长度的序列。
假设一个简单的 RNN,输入维度 d=2,隐状态维度 h=3。给定输入序列:x1=[1,0], x2=[0,1], x3=[1,1]。
初始化 h0=0,假设权重为:
- Wih=0.50.1−0.4−0.20.30.6
- Whh=0.20.4−0.10.1−0.10.3−0.30.20.5
时间步 1:
a1=Wihx1+Whh0=[0.5,0.1,−0.4]T
h1=tanh([0.5,0.1,−0.4])≈[0.462,0.100,−0.380]
时间步 2:
a2=Wih[0,1]T+Whhh1
Wihx2=[−0.2,0.3,0.6]T
Whhh1≈[0.2(0.462)+0.1(0.100)+(−0.3)(−0.380),...]≈[0.216,0.194,0.083]T
h2=tanh([0.016,0.494,0.683])≈[0.016,0.459,0.593]
可见隐状态 h2 同时包含了当前输入 x2 和过去输入 x1 的信息——这就是 RNN 的"记忆"机制。
通过时间反向传播(Backpropagation Through Time,BPTT)是训练 RNN 的标准算法。它将展开后的 RNN 视为一个深度前馈网络,沿时间方向反向传播梯度:
- 前向传播:沿时间 t=1 到 T 计算隐状态和输出
- 计算损失:通常使用每个时间步的交叉熵损失之和 L=∑t=1TLt(yt,y^t)
- 反向传播:从时间 T 到 1 计算梯度,使用链式法则
对于权重 Whh 的梯度,链式法则展开为:
∂Whh∂L=t=1∑Tk=1∑t∂ht∂Lt∂hk∂ht∂Whh∂hk
其中 ∂hk∂ht=∏j=kt−1∂hj∂hj+1=∏j=kt−1diag(f′(aj))Whh。
这是 BPTT 的核心问题。由于 Jacobian 矩阵 ∂hj∂hj+1 中包含 Whh 的连乘:
- 若 Whh 的特征值 λ<1:连乘导致梯度指数级衰减到 0,网络无法学习长期依赖(例如,句子开头的词对末尾词的影响)
- 若 Whh 的特征值 λ>1:连乘导致梯度指数级增长,训练不稳定
具体数值示例:假设 tanh 激活函数的导数最大为 1.0,权重 w=0.9,则相距 T=50 时间步的梯度贡献为 0.950≈0.005,即梯度衰减了 200 倍。相距 100 步时衰减为 0.9100≈0.000027,几乎完全消失。
| 距离 |
w=0.5 |
w=0.9 |
w=1.1 |
| 10 步 |
9.8×10−4 |
0.35 |
2.85 |
| 50 步 |
8.9×10−16 |
0.005 |
117.4 |
| 100 步 |
7.9×10−31 |
2.7×10−5 |
13,780 |
这就是为什么简单 RNN 难以捕获长期依赖关系——梯度要么消失使长距离信息无法学习,要么爆炸使训练崩溃。
对于梯度爆炸问题,一个简单有效的解决方案是梯度裁剪:
def gradient_clipping(params, max_norm=1.0):
"""将梯度范数限制在 max_norm 以内"""
total_norm = 0
for p in params:
total_norm += p.grad.norm().item() ** 2
total_norm = total_norm ** 0.5
clip_coef = max_norm / (total_norm + 1e-6)
if clip_coef < 1:
for p in params:
p.grad *= clip_coef
这在实践中非常有效,所有 RNN 训练代码都应包含梯度裁剪。但对于梯度消失,需要更根本的架构改进——这就是 LSTM 和 GRU 出现的原因。
LSTM(Long Short-Term Memory)由 Hochreiter 和 Schmidhuber 于 1997 年提出,专门为解决 RNN 的长期依赖问题而设计。核心创新是引入门控机制(gating mechanism)和细胞状态(cell state)——一个"信息高速公路",让梯度可以更顺畅地沿时间反向传播。
LSTM 在每个时间步维护两个状态:
- 细胞状态 ct:长期记忆,通过精心设计的"传送带"传递信息
- 隐状态 ht:短期记忆,也是输出
三个门控制信息的流动:
遗忘门(Forget Gate)——决定丢弃多少过去信息:
ft=σ(Wf[ht−1,xt]+bf)
输入门(Input Gate)——决定写入多少新信息:
it=σ(Wi[ht−1,xt]+bi)
候选细胞状态:
c~t=tanh(Wc[ht−1,xt]+bc)
更新细胞状态(结合遗忘和写入):
ct=ft⊙ct−1+it⊙c~t
输出门(Output Gate)——决定输出多少细胞状态信息:
ot=σ(Wo[ht−1,xt]+bo)
最终隐状态:
ht=ot⊙tanh(ct)
其中 σ 是 sigmoid 函数(输出 [0,1]),⊙ 是逐元素乘法。
关键在于细胞状态的更新是线性加法:
ct=ft⊙ct−1+it⊙c~t
在反向传播时,梯度通过细胞状态的传递路径为:
∂ct−1∂ct=ft+(其他项)
这意味着:
- 当遗忘门 ft≈1 时,梯度几乎无损传递
- 不存在连乘权重矩阵 Whh 的反复作用
- 梯度可以选择"绕过" tanh 和 sigmoid 的饱和区,通过细胞状态"高速公路"直接回传
数值对比:在 LSTM 中,即使相距 100 个时间步,只要遗忘门保持 ft≈1,梯度衰减因子约为 0.95100≈0.006,远好于 RNN 在 w=0.9 时的 2.7×10−5(提高了约 220 倍)。如果遗忘门恰好 ft=1,梯度甚至完全不衰减。
可以把 LSTM 想象成一个有"看门人"的仓库:
- 遗忘门:决定清空哪些旧库存
- 输入门:决定接收哪些新货物
- 细胞状态:仓库本身,存储所有物品
- 输出门:决定展示哪些货物给外界
- 隐状态:展示柜上的商品
GRU(Gated Recurrent Unit)由 Cho 等人于 2014 年提出,是 LSTM 的简化变体。它合并了遗忘门和输入门,减少了参数数量,计算效率更高。
GRU 只有两个门,且没有独立的细胞状态:
重置门(Reset Gate)——决定忽略多少过去信息:
rt=σ(Wr[ht−1,xt]+br)
更新门(Update Gate)——决定保留多少过去信息、引入多少新信息:
zt=σ(Wz[ht−1,xt]+bz)
候选隐状态(使用重置门控制历史信息):
h~t=tanh(Wh[rt⊙ht−1,xt]+bh)
最终隐状态(使用更新门融合新旧信息):
ht=(1−zt)⊙ht−1+zt⊙h~t
| 特性 |
LSTM |
GRU |
| 门的数量 |
3(遗忘、输入、输出) |
2(重置、更新) |
| 是否独立细胞状态 |
是(ct) |
否(仅有 ht) |
| 参数数量(输入 d,隐层 h) |
4(dh+h2+h) |
3(dh+h2+h) |
| 参数量减少 |
基准 |
约 25% |
| 训练速度 |
较慢 |
较快 |
| 表达能力 |
理论上更强 |
近似相当 |
参数量计算(以 d=256,h=256 为例):
- LSTM:4×(256×256+256×256+256)=4×(65,536+65,536+256)=525,312
- GRU:3×(65,536+65,536+256)=393,984
GRU 比 LSTM 节省约 25% 的参数,在数据量有限时有过拟合风险更低的优势。
在很多任务中,当前输出同时依赖于过去和未来的上下文。例如:
- 命名实体识别:"我去了[苹果]店"——"苹果"是公司名;"我吃了[苹果]"——"苹果"是水果
- 语音识别:音素的识别需要前后音节信息
双向 RNN(BiRNN) 通过两个独立的 RNN 层分别从前向和后向处理序列,然后将两个方向的隐状态拼接:
ht=RNNfwd(x1,…,xt)
ht=RNNbwd(xT,…,xt)
htbi=[ht;ht]
限制:双向 RNN 不能用于在线/实时场景(如实时语音识别),因为它需要看到完整序列才能输出。
将多个 RNN 层堆叠,每层的隐状态作为下一层的输入:
ht(1)=RNN1(xt,ht−1(1))
ht(l)=RNNl(ht(l−1),ht−1(l)),l=2,3,…,L
堆叠 RNN 可以学习不同时间尺度的抽象特征:底层捕获局部模式(如音素),高层捕获长期结构(如句子意图)。
| 层数 |
适用场景 |
注意事项 |
| 1 层 |
简单序列任务、小数据集 |
训练最快,不易过拟合 |
| 2 层 |
大多数 NLP 任务(文本分类、语言模型) |
性价比最高的选择 |
| 3-4 层 |
复杂序列任务(机器翻译、语音识别) |
需要较大数据集 |
| >4 层 |
极少使用(除非有大量数据) |
训练困难,残差连接必要 |
import torch
import torch.nn as nn
class LSTMClassifier(nn.Module):
def __init__(self, vocab_size, embed_dim=128, hidden_dim=256,
num_layers=2, num_classes=2, dropout=0.3):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
self.lstm = nn.LSTM(
input_size=embed_dim,
hidden_size=hidden_dim,
num_layers=num_layers,
batch_first=True,
bidirectional=True,
dropout=dropout if num_layers > 1 else 0
)
self.classifier = nn.Sequential(
nn.Linear(hidden_dim * 2, hidden_dim), # *2 for bidirectional
nn.ReLU(),
nn.Dropout(dropout),
nn.Linear(hidden_dim, num_classes)
)
def forward(self, x, lengths):
# x: (batch, seq_len)
embedded = self.embedding(x) # (batch, seq_len, embed_dim)
# Pack padded sequence for efficient computation
packed = nn.utils.rnn.pack_padded_sequence(
embedded, lengths.cpu(), batch_first=True, enforce_sorted=False
)
_, (hidden, cell) = self.lstm(packed)
# hidden: (num_layers * num_directions, batch, hidden_dim)
# Concatenate last layer's forward and backward hidden states
last_hidden = torch.cat((hidden[-2], hidden[-1]), dim=1)
# hidden[-2] = forward, hidden[-1] = backward last
logits = self.classifier(last_hidden) # (batch, num_classes)
return logits
关键代码解释:
pack_padded_sequence:将变长序列打包,避免对填充位置的无效计算
bidirectional=True:启用双向 LSTM
- 取最后一层的前向和后向隐状态拼接:
hidden[-2] 和 hidden[-1]
RNN 语言模型预测给定上文后下一个词的概率:
P(w1,w2,…,wT)=t=1∏TP(wt∣w1,…,wt−1)
每个时间步的输出 yt 通过 softmax 得到词表中各词的概率。在维基百科文本上训练的语言模型,使用 2 层 LSTM(隐层 1024 维),可以在 perplexity 指标上达到约 40-60(越低越好,随机猜测为词表大小)。
经典的 Sequence-to-Sequence(Seq2Seq)架构:
- 编码器:读取源语言句子,生成上下文向量 c
- 解码器:根据 c 和已生成的目标词,逐步生成目标语言句子
早期的 Google Neural Machine Translation(GNMT,2016)使用了 8 层 LSTM 编码器和 8 层 LSTM 解码器,配合注意力机制,在 WMT 英法翻译任务上 BLEU 达到 41.0。
按句子级别的文本分类:RNN 读取整个句子,取最后一个时间步的隐状态(或所有时间步的池化结果)进行分类。在 IMDb 电影评论(50K 条)的二分类任务中,2 层 LSTM 可以达到约 89-91% 的准确率。
对股票价格、天气温度等时序数据的预测。以日均温度预测为例:
| 模型 |
预测步骤 MSE |
优势 |
劣势 |
| ARIMA |
平均 2.34 |
理论成熟,可解释强 |
仅线性,无法处理复杂模式 |
| 简单 RNN |
平均 1.89 |
可建模非线性 |
难以捕获长期依赖 |
| LSTM |
平均 1.21 |
长期依赖建模最优 |
训练最慢,参数量大 |
| GRU |
平均 1.28 |
训练快速,效果接近 LSTM |
略逊于 LSTM |
| 局限 |
原因 |
影响程度 |
| 顺序计算 |
每个时间步依赖前一步,无法并行 |
训练速度比 Transformer 慢 10-100 倍 |
| 梯度问题 |
即使 LSTM/GRU 也有所缓解,但长程依赖仍困难 |
序列长度 > 1000 时效果显著下降 |
| 记忆容量 |
隐状态向量维度有限 |
长序列信息压缩损失严重 |
| O(n) 路径 |
序列中相距 n 的元素需 n 步才能交互 |
信息传递效率低 |
2017 年提出的 Transformer 架构通过自注意力机制(Self-Attention)解决了 RNN 的并行化问题:
| 特性 |
RNN/LSTM |
Transformer |
| 并行计算 |
❌ 必须顺序计算 |
✅ 全并行 |
| 长程依赖 |
⚠️ 理论上可(实际困难) |
✅ 常数路径长度 |
| 复杂度 |
O(T) 时间步 |
O(T2) 注意力(可优化) |
| 参数量 |
随 T 减少 |
更大 |
| 小数据 |
✅ 更鲁棒 |
⚠️ 容易过拟合 |
例外场景:对于短序列(T<50)、小数据集、需要在线流式处理的任务,RNN/LSTM 仍然有竞争力。例如实时语音端点检测、低资源语言建模等场景。
| 超参数 |
推荐值 |
说明 |
| 隐层维度 |
128-512 |
取决于任务复杂度和数据量 |
| 层数 |
1-3 |
2 层通常是性价比最优选择 |
| Dropout |
0.2-0.5 |
层间 dropout(非时间维度) |
| 学习率 |
0.001-0.01 |
配合学习率调度 |
| 梯度裁剪阈值 |
0.25-5.0 |
梯度范数裁剪 |
| 序列长度 |
32-512 |
太长则考虑 Transformer |
- 数据预处理:对于文本,统一序列长度(填充或截断),常用
<PAD> 标记
- 权重初始化:正交初始化(orthogonal initialization)对 RNN 特别有效
- 序列打包:使用
pack_padded_sequence 避免对填充位置的计算
- 梯度裁剪:所有 RNN 训练中必须使用
- 双向化:如果不是在线推理场景,优先使用双向 RNN
- 层归一化(LayerNorm):有助于稳定 RNN 训练
建议在新项目中优先考虑 Transformer,除非遇到以下情况之一:
- 数据集极小(<10K 样本),Transformer 会过拟合
- 需要流式/在线推理(低延迟要求)
- 算力有限,无法训练 Transformer
- 序列长度非常短且简单
| 模型 |
提出年份 |
核心创新 |
主要优势 |
主要局限 |
| 简单 RNN |
1980s |
循环连接 |
序列建模的基础框架 |
梯度消失/爆炸严重 |
| LSTM |
1997 |
门控+细胞状态 |
长期依赖建模能力强 |
参数多,训练慢 |
| GRU |
2014 |
简化的门控机制 |
参数少,训练快 |
表达能力略逊 LSTM |
| BiRNN |
1997 |
双向处理 |
利用双侧上下文 |
不支持在线推理 |
| Stacked RNN |
2010s |
多层堆叠 |
层次化特征抽象 |
训练更困难 |
- Hochreiter, S., & Schmidhuber, J. (1997). "Long Short-Term Memory." Neural Computation, 9(8), 1735-1780.
- Cho, K., et al. (2014). "Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation." EMNLP 2014.
- Graves, A. (2013). "Generating Sequences With Recurrent Neural Networks." arXiv preprint arXiv:1308.0850.
- Bengio, Y., Simard, P., & Frasconi, P. (1994). "Learning long-term dependencies with gradient descent is difficult." IEEE Transactions on Neural Networks, 5(2), 157-166.
- Vaswani, A., et al. (2017). "Attention Is All You Need." NeurIPS 2017.