一、序列处理的需求与挑战
序列数据的特点
- 可变长度:自然语言句子、语音信号、时间序列数据等长度不固定
- 上下文依赖:序列中元素的意义取决于其前后元素
- 记忆需求:理解当前输入需要参考历史信息
前馈神经网络的局限性
- 固定输入大小:难以处理可变长度序列
- 无记忆能力:相同输入总是产生相同输出,无法根据历史上下文调整
- 槽位填充问题(Slot Filling):
- 任务:从自然语言中提取特定参数(如目的地、出发时间等)
- 示例输入:“我想在6月1日从台北抵达上海”
- 需要识别:
- “台北” → 出发地(Place of Departure)
- “上海” → 目的地(Place of Destination)
- “6月1日” → 时间(Time)
- 挑战:仅看单词”上海”或”台北”无法判断其角色,必须结合上下文中的动词”从”和”抵达”来理解
- 应用场景:智能客服、订票系统、语音助手
二、RNN基本概念与架构
核心思想:带记忆的网络
- 记忆元:存储网络的历史状态
- 隐状态:记忆元中的值,表示对之前输入序列的”记忆”
- 循环连接:当前时刻的隐状态作为下一时刻的输入之一
简单循环网络 (Simple RNN/Elman Network)

数学模型
参数说明:
- :时间步的输入向量
- :时间步的隐状态向量
- :时间步的输出向量
- :输入到隐状态的权重矩阵
- :隐状态到隐状态的循环权重矩阵
- :激活函数(如tanh、ReLU)

PyTorch实现示例
import torchimport torch.nn as nn
# 定义简单RNN模型class SimpleRNN(nn.Module): def __init__(self, input_size, hidden_size, output_size): super(SimpleRNN, self).__init__() self.hidden_size = hidden_size # RNN层:input_size -> hidden_size self.rnn = nn.RNN(input_size, hidden_size, batch_first=True) # 输出层:hidden_size -> output_size self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x): # x shape: (batch_size, seq_len, input_size) # h0 shape: (1, batch_size, hidden_size) h0 = torch.zeros(1, x.size(0), self.hidden_size)
# RNN前向传播 out, hn = self.rnn(x, h0) # out: (batch_size, seq_len, hidden_size)
# 取最后一个时间步的输出 out = self.fc(out[:, -1, :]) # (batch_size, output_size) return out
# 使用示例input_size = 10 # 输入特征维度hidden_size = 20 # 隐状态维度output_size = 5 # 输出维度seq_len = 15 # 序列长度batch_size = 32 # 批次大小
model = SimpleRNN(input_size, hidden_size, output_size)x = torch.randn(batch_size, seq_len, input_size)output = model(x) # shape: (32, 5)print(f"输出形状: {output.shape}")序列的向量表示
独热编码 (One-hot Encoding)
缺点:维度灾难,无法表示语义相似性
词嵌入 (Word Embedding)
- 将单词映射到低维连续向量空间
- 语义相似的单词在向量空间中距离相近
三、RNN的变体架构
双向循环神经网络 (Bidirectional RNN)
动机
当前时刻的输出可能依赖于整个序列的信息(过去和未来)
结构
同时运行两个RNN:
- 前向RNN:从左到右处理序列,隐状态记为
- 反向RNN:从右到左处理序列,隐状态记为
输出
每个时间步的最终隐状态:
应用
词性标注、命名实体识别等需要全局上下文的任务
深层循环神经网络

结构
堆叠多个RNN层,低层的输出作为高层的输入
每层计算
- :层索引
- :第层在时间的隐状态
Jordan网络

与Elman网络的区别
将输出层的值(而非隐藏层的值)存储到记忆元中
记忆更新
稳定性考量
- Jordan Network的优势:输出层 有明确的训练目标(Target)约束
- Elman Network的特点:隐状态 是网络自己学习出来的,没有直接的监督信号
- 历史观点:早期研究认为Jordan Network可能更稳定,因为其反馈信号更”可控”
- 现状:尽管有这个理论优势,实践中Elman Network(Simple RNN)仍然是主流选择
四、长短期记忆网络 (LSTM)
LSTM的提出动机
- RNN的梯度问题:简单RNN存在梯度消失/爆炸问题
- LSTM核心思想:通过门控机制有选择地保留、更新和输出信息
LSTM单元结构
包含三个门和一个记忆单元:
- 输入门:控制新信息的流入
- 遗忘门:控制旧信息的遗忘
- 输出门:控制信息的输出
- 记忆单元:长期记忆的存储

LSTM的数学公式
设为当前输入,为前一时刻隐状态,为前一时刻记忆单元值。
1. 计算门控信号和候选值
2. 更新记忆单元
- :逐元素相乘
- 遗忘门决定保留多少旧记忆,输入门决定加入多少新信息
梯度的”高速公路”(Gradient Highway):
- 关键机制:当遗忘门 时, 的信息几乎原封不动地加到 上
- 与Simple RNN的对比:
- Simple RNN:,每次都要乘以权重矩阵
- LSTM:,通过加法传递信息
- 为什么能解决梯度消失:
- 在反向传播时,梯度可以通过加法操作直接传回,不会像连续乘法那样指数衰减
- 这就像在崎岖的误差表面上修建了一条”高速公路”,让梯度能够长距离传播而不衰减
- 也称为**CEC(Constant Error Carousel,恒定误差传送带)**机制
- 直观理解:加法保持梯度稳定,乘法容易导致梯度消失或爆炸
3. 计算当前隐状态
LSTM示例分析
考虑一个简化的LSTM:
- 规则:
- 当时,将的值写入记忆单元
- 当时,重置记忆单元为0
- 当时,输出记忆单元的值
时间 输入(x1,x2,x3) 记忆单元c 输出1 (3, 1, 0) 3 02 (4, 1, 0) 7 03 (2, 0, 0) 7 04 (1, 0, 1) 7 7PyTorch实现示例
import torchimport torch.nn as nn
# 定义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
# LSTM层 self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True) # 输出层 self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x): # 初始化隐状态和记忆单元 h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size) c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size)
# LSTM前向传播 # out: (batch_size, seq_len, hidden_size) # hn, cn: (num_layers, batch_size, hidden_size) out, (hn, cn) = self.lstm(x, (h0, c0))
# 取最后一个时间步的输出 out = self.fc(out[:, -1, :]) return out
# 使用示例:情感分析任务input_size = 100 # 词嵌入维度hidden_size = 128 # LSTM隐状态维度num_layers = 2 # LSTM层数output_size = 2 # 二分类(正面/负面)seq_len = 50 # 句子长度batch_size = 32
model = LSTMModel(input_size, hidden_size, num_layers, output_size)x = torch.randn(batch_size, seq_len, input_size)output = model(x) # shape: (32, 2)print(f"输出形状: {output.shape}")多层LSTM架构
在实际应用中,我们经常使用多层(堆叠)LSTM来增强模型的表达能力:
- 层次特征提取:低层LSTM学习基础特征,高层LSTM学习更抽象的特征
- 增强表达能力:多层结构可以捕获更复杂的序列模式
- 常用层数:通常使用2-4层,过深可能导致过拟合和训练困难
在PyTorch中,通过num_layers参数即可轻松实现多层LSTM(如上面代码示例中的num_layers=2)。

如图所示,多层LSTM将前一层的输出序列作为下一层的输入序列,每一层都有自己独立的隐状态和记忆单元。第层在时间步的计算依赖于:
- 前一层(第层)在同一时间步的输出
- 本层在前一时间步的隐状态和记忆单元
这种堆叠结构使得网络能够学习到更深层次的序列表示,在复杂任务(如机器翻译、语音识别)中表现更优。
五、门控循环单元 (GRU)
GRU的设计理念
- LSTM的简化版:将输入门和遗忘门合并为更新门
- 两个门:重置门和更新门
- 没有单独的记忆单元:隐状态同时承担记忆和输出的功能
GRU的数学公式
1. 计算门控信号
2. 计算候选隐状态
3. 更新隐状态
GRU vs LSTM对比
| 特性 | LSTM | GRU |
|---|---|---|
| 门数量 | 3个(输入、遗忘、输出) | 2个(重置、更新) |
| 参数数量 | 较多(约RNN的4倍) | 较少(约RNN的3倍) |
| 记忆单元 | 有独立记忆单元 | 无,隐状态同时负责记忆 |
| 训练速度 | 较慢 | 较快 |
PyTorch实现示例
import torchimport torch.nn as nn
# 定义GRU模型class GRUModel(nn.Module): def __init__(self, input_size, hidden_size, num_layers, output_size): super(GRUModel, self).__init__() self.hidden_size = hidden_size self.num_layers = num_layers
# GRU层 self.gru = nn.GRU(input_size, hidden_size, num_layers, batch_first=True) # 输出层 self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x): # 初始化隐状态(GRU不需要记忆单元c) h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size)
# GRU前向传播 out, hn = self.gru(x, h0) # out: (batch_size, seq_len, hidden_size)
# 取最后一个时间步的输出 out = self.fc(out[:, -1, :]) return out
# 使用示例model = GRUModel(input_size=100, hidden_size=128, num_layers=2, output_size=2)x = torch.randn(32, 50, 100)output = model(x) # shape: (32, 2)print(f"输出形状: {output.shape}")六、RNN的训练
损失函数
对于序列标注任务,总损失是各个时间步损失之和:
随时间反向传播 (BPTT)
- 原理:将RNN在时间上展开,视为深度前馈网络,应用标准反向传播
- 梯度计算:需要计算损失对每个时间步参数的梯度,并沿时间反向累积
RNN的训练难题
RNN训练困难的本质在于其崎岖的误差表面(Rugged Error Surface),而不仅仅是梯度数值的大小问题。
1. 梯度消失
- 在反向传播时,梯度需要连续乘以相同的权重矩阵
- 如果权重矩阵的特征值小于1,梯度会指数衰减到0
- 后果:难以学习长期依赖关系
- 误差表面特征:极其平坦的区域,梯度几乎为0
2. 梯度爆炸
- 如果权重矩阵的特征值大于1,梯度会指数增长
- 后果:训练不稳定,参数更新过大
- 误差表面特征:陡峭的悬崖,梯度突然变得极大
3. 误差表面的崎岖性(核心问题)
- 形象比喻:RNN的误差表面就像一片平坦的沙漠中突然出现悬崖
- 在平坦区域:无论怎么推球(更新参数),球几乎不动(梯度消失)
- 遇到悬崖:轻轻一推,球就飞出去了(梯度爆炸)
- 训练困境:
- 平坦区域梯度小,学习极慢,难以找到下降方向
- 陡峭区域梯度大,一步就可能跳过最优解,甚至飞出有效范围
- 为什么难训练:不是因为梯度小(这只是”软”问题),而是因为误差表面极度不规则,导致优化算法难以稳定前进
解决梯度问题的技巧

1. 梯度裁剪(Gradient Clipping)
- 目的:专门用于解决梯度爆炸问题,而非梯度消失
- 原理:当遇到误差表面的”悬崖”时,限制梯度的步长,防止参数更新过大而飞出有效范围
- 效果:稳定训练过程,避免因单次更新过大导致的训练崩溃
- 注意:梯度裁剪无法解决梯度消失问题(平坦区域的问题需要其他方法)
2. 使用LSTM/GRU
- LSTM的加法更新创建了梯度传播的”高速公路”
- 缓解梯度消失问题
3. 更好的参数初始化
- 对循环权重矩阵使用单位矩阵初始化
- 结合ReLU激活函数
七、RNN的应用类型
多对一 (Many-to-One)

- 输入:一个序列
- 输出:一个标签或向量
- 示例:情感分析、文档分类
- 实现:通常取最后一个时间步的隐状态作为序列表示
多对多:输入输出等长

- 输入:长度为的序列
- 输出:长度为的序列
- 示例:词性标注、命名实体识别
- 实现:每个时间步都产生一个输出
结构化学习(Structured Learning)
虽然RNN能够利用上下文信息,但其输出层(Softmax)在每个时间步通常是独立选择最大概率的标签。
潜在问题:
- 可能产生不合理的标签序列
- 例如在BIO标注中:
B-PER I-LOC OI-LOC(地点的内部标签)紧跟在B-PER(人名的开始标签)后面- 这在逻辑上是不合理的(I标签应该跟随对应的B标签)
解决方案:
- CRF层(Conditional Random Field):在RNN输出层之上添加CRF层,对标签之间的转移关系进行建模
- Viterbi算法:在所有可能的输出序列中寻找全局最优解,而非贪心地选择每步的局部最优
- 效果:通过约束标签转移规则,确保输出序列在结构上是合理的
序列到序列 (Sequence-to-Sequence, Seq2Seq)

- 输入:长度为的序列
- 输出:长度为的序列()
- 示例:机器翻译、语音识别
编码器-解码器架构
-
编码器:将输入序列编码为固定长度的上下文向量
通常取编码器最后一个时间步的隐状态:
-
解码器:以为初始状态,逐步生成输出序列
-
训练技巧:
- 教师强制:训练时,将真实的前一个词作为解码器输入
- 束搜索:推断时,维护多个候选序列,选择概率最高的
- 注意力机制:让解码器可以访问编码器的所有隐状态
PyTorch实现示例
import torchimport torch.nn as nn
# 编码器class Encoder(nn.Module): def __init__(self, input_size, hidden_size, num_layers=1): super(Encoder, self).__init__() self.hidden_size = hidden_size self.num_layers = num_layers self.embedding = nn.Embedding(input_size, hidden_size) self.lstm = nn.LSTM(hidden_size, hidden_size, num_layers, batch_first=True)
def forward(self, x): # x: (batch_size, seq_len) embedded = self.embedding(x) # (batch_size, seq_len, hidden_size) outputs, (hidden, cell) = self.lstm(embedded) return hidden, cell # 返回最后的隐状态和记忆单元
# 解码器class Decoder(nn.Module): def __init__(self, output_size, hidden_size, num_layers=1): super(Decoder, self).__init__() self.hidden_size = hidden_size self.num_layers = num_layers self.embedding = nn.Embedding(output_size, hidden_size) self.lstm = nn.LSTM(hidden_size, hidden_size, num_layers, batch_first=True) self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x, hidden, cell): # x: (batch_size, 1) - 单个时间步 embedded = self.embedding(x) # (batch_size, 1, hidden_size) output, (hidden, cell) = self.lstm(embedded, (hidden, cell)) prediction = self.fc(output.squeeze(1)) # (batch_size, output_size) return prediction, hidden, cell
# Seq2Seq模型class Seq2Seq(nn.Module): def __init__(self, encoder, decoder): super(Seq2Seq, self).__init__() self.encoder = encoder self.decoder = decoder
def forward(self, src, trg, teacher_forcing_ratio=0.5): # src: (batch_size, src_len) # trg: (batch_size, trg_len) batch_size = src.shape[0] trg_len = trg.shape[1] trg_vocab_size = self.decoder.fc.out_features
# 存储解码器输出 outputs = torch.zeros(batch_size, trg_len, trg_vocab_size)
# 编码器处理输入序列 hidden, cell = self.encoder(src)
# 解码器的第一个输入(通常是<SOS>标记) input = trg[:, 0].unsqueeze(1)
for t in range(1, trg_len): output, hidden, cell = self.decoder(input, hidden, cell) outputs[:, t, :] = output
# 教师强制:以一定概率使用真实标签作为下一个输入 teacher_force = torch.rand(1).item() < teacher_forcing_ratio input = trg[:, t].unsqueeze(1) if teacher_force else output.argmax(1).unsqueeze(1)
return outputs
# 使用示例input_vocab_size = 1000 # 源语言词汇表大小output_vocab_size = 1000 # 目标语言词汇表大小hidden_size = 256num_layers = 2
encoder = Encoder(input_vocab_size, hidden_size, num_layers)decoder = Decoder(output_vocab_size, hidden_size, num_layers)model = Seq2Seq(encoder, decoder)
# 示例输入src = torch.randint(0, input_vocab_size, (32, 10)) # (batch_size, src_len)trg = torch.randint(0, output_vocab_size, (32, 15)) # (batch_size, trg_len)output = model(src, trg) # shape: (32, 15, 1000)print(f"输出形状: {output.shape}")连接主义时序分类 (CTC)

- 专门用于语音识别等任务:输入序列长度 >> 输出序列长度
- 核心思想:允许模型输出扩展序列,包含重复标签和空白符
- 折叠规则:
- 合并重复的相同标签
- 移除空白符
- 示例:
- 模型输出:
"h null null i null s null" - 折叠后:
"his"
- 模型输出:
八、高级主题与扩展
注意力机制 (Attention Mechanism)
- 动机:Seq2Seq模型中,编码器的单个上下文向量可能信息不足
- 核心思想:解码器的每个时间步可以”关注”编码器的不同部分
- 计算步骤:
- 计算注意力分数
- 计算注意力权重
- 计算上下文向量
- 解码器输入
序列到序列自编码器
- 目标:学习序列的稠密向量表示
- 结构:编码器将输入序列编码为向量,解码器从该向量重构原始序列
- 应用:无监督学习序列表示、文本生成、数据压缩
层次RNN
- 结构:在不同时间尺度上运行多个RNN
- 应用:文档建模、视频理解、多尺度时间序列分析
递归神经网络(Recursive Neural Network)
与循环神经网络的区别
- Recurrent NN(循环):处理时间序列(线性结构),按时间步依次处理输入
- Recursive NN(递归):处理树状结构(Tree Structure),按树的层次结构处理输入
应用场景
- 句法分析(Syntactic Parsing):句子可以被解析为语法树
- 情感分析:利用句子的语法结构进行情感判断
- 语义组合:自底向上地组合词语和短语的语义
工作原理
- 将句子解析为语法树(如:名词短语、动词短语等)
- 从叶子节点(单词)开始,逐层向上编码
- 每个父节点的表示由其子节点通过神经网络组合得到
- 最终根节点的表示包含了整个句子的语义信息
优势
- 对于符合语法逻辑的复杂长句,树状结构比线性扫描更能捕捉句子的层次关系
- 可以更好地处理嵌套结构和长距离依赖
局限
- 需要预先进行句法分析,增加了计算复杂度
- 对句法分析的质量依赖较大
九、实践建议
架构选择指南
- 简单任务/短序列:可以尝试简单RNN
- 长序列/长期依赖:优先选择LSTM或GRU
- 需要全局上下文:使用双向RNN
- 计算资源有限:考虑GRU(参数更少)
- 需要最佳性能:尝试LSTM并调整超参数
训练技巧
- 梯度裁剪:几乎总是需要的,特别是对于RNN和LSTM
- Dropout:在RNN中应用需要小心,通常在时间步之间或层之间应用
- 批量归一化:可以应用在RNN的输入或隐状态上
- 学习率调度:使用学习率衰减或预热策略
- 早停:监控验证集损失,防止过拟合
超参数调优
- 隐状态维度:50-1000之间,取决于任务复杂度
- 层数:1-4层,更深不一定更好
- Dropout率:0.2-0.5
- 学习率:0.001-0.1,配合学习率调度
- 优化器:Adam通常是不错的选择
十、总结
循环神经网络是处理序列数据的强大工具,其核心是通过循环连接和隐状态赋予网络记忆能力。然而,简单RNN存在梯度消失/爆炸问题,难以学习长期依赖。LSTM和GRU通过门控机制有效解决了这些问题,成为实际应用中的主流选择。
RNN及其变体已广泛应用于:
- 自然语言处理:机器翻译、文本生成、情感分析
- 语音处理:语音识别、语音合成
- 时间序列分析:股票预测、气象预报
随着注意力机制和Transformer架构的发展,RNN在某些领域已被取代,但在许多序列建模任务中,RNN及其变体仍然是有效且常用的工具。
致谢
本笔记在学习李宏毅老师的深度学习课程(LeeDL)时完成。感谢李宏毅老师深入浅出的讲解,让复杂的循环神经网络概念变得易于理解。课程中的精彩示例和直观解释为本文提供了重要的参考和启发。
相关资源:
部分信息可能已经过时









