一、循环神经网络(RNN)与序列数据处理:原理、架构与实战

1.1 本章学习目标与重点
💡 理解循环神经网络的核心原理(时序依赖建模、参数共享),掌握RNN、LSTM、GRU的结构差异与梯度传播机制;熟练使用PyTorch搭建序列数据处理模型,解决文本分类、情感分析、时序预测等实战任务;能识别并解决RNN训练中的梯度消失/爆炸、长序列依赖等关键问题。
重点:RNN的时序建模逻辑、LSTM/GRU的门控机制、序列数据预处理(分词、编码、padding)、实战中模型调优技巧;
难点:RNN的梯度传播数学推导、LSTM门控单元的工作原理、长序列数据的处理效率与建模精度平衡。
1.2 序列数据与RNN的核心价值
1.2.1 什么是序列数据?
序列数据是指具有时间或逻辑先后顺序,且元素之间存在依赖关系的数据,是现实世界中最常见的数据类型之一。
📝 典型序列数据场景:
- 文本数据:句子中单词的顺序决定语义(如“我喜欢人工智能”≠“人工智能喜欢我”);
- 时序数据:股票价格、气温、设备传感器数据(当前值与历史值强相关);
- 语音数据:音频信号的时序序列(连续帧之间存在声学依赖);
- 视频数据:帧序列(相邻帧的画面内容高度关联)。
💡 核心特点:序列数据的“顺序”是关键信息,传统模型(如MLP、CNN)无法直接建模这种时序依赖——MLP将输入扁平化,丢失顺序信息;CNN虽能捕捉局部空间依赖,但对变长序列的适应性差,且难以建模长距离依赖。
1.2.2 RNN的核心创新:时序依赖建模与参数共享
RNN(Recurrent Neural Network)的诞生正是为了解决序列数据的建模问题,其核心设计围绕“利用历史信息预测当前状态”展开,包含两大关键创新:
1. 时序依赖建模:隐藏状态传递历史信息
RNN通过“隐藏状态(Hidden State)”传递历史信息——每个时刻的输出不仅依赖当前输入,还依赖上一时刻的隐藏状态,从而实现对时序依赖的建模。
🗄️ 直观理解:RNN就像一个“带记忆的处理器”,阅读序列数据时,会把之前看到的信息(历史输入)存储在“记忆”(隐藏状态)中,结合当前输入生成输出,再更新记忆传递给下一刻。
2. 参数共享:适应变长序列+减少参数
与CNN的权值共享类似,RNN在所有时刻共享同一组权重参数(输入权重W_xh、隐藏层权重W_hh、输出权重W_hy)。
💡 价值:
- 适应变长序列:无论序列长度是5还是100,RNN都能用同一组参数处理,无需为不同长度的序列设计不同模型;
- 减少参数数量:避免因序列过长导致参数爆炸(如MLP处理长度为100的序列,输入维度=100×特征数,参数数量会急剧增加);
- 捕捉通用时序模式:参数共享意味着RNN学习到的是序列数据的通用规律(如文本中的语法结构、时序数据的趋势特征),而非特定位置的局部特征。
1.3 RNN的基础结构与数学原理
1.3.1 基本RNN的结构解析
基本RNN的结构可分为“输入层、隐藏层、输出层”,核心是隐藏层的时序传递机制。
1. 结构示意图(简化版)
时刻t=1:输入x₁ → 隐藏层h₁(依赖x₁) → 输出y₁
时刻t=2:输入x₂ → 隐藏层h₂(依赖x₂和h₁) → 输出y₂
时刻t=3:输入x₃ → 隐藏层h₃(依赖x₃和h₂) → 输出y₃
...
时刻t=T:输入x_T → 隐藏层h_T(依赖x_T和h_{T-1}) → 输出y_T
其中:
x_t:第t时刻的输入向量(如文本中第t个单词的嵌入向量);h_t:第t时刻的隐藏状态向量(存储历史信息,维度为隐藏层大小,如128、256);y_t:第t时刻的输出向量(如分类任务的类别概率、时序预测的数值)。
2. 数学公式推导
基本RNN的核心是“隐藏状态更新”和“输出计算”两步,假设激活函数为tanh(传统RNN常用):
① 隐藏状态更新(核心:融合当前输入与历史隐藏状态):
h_t = tanh(W_xh · x_t + W_hh · h_{t-1} + b_h)
W_xh:输入到隐藏层的权重矩阵(维度:隐藏层大小×输入维度);W_hh:隐藏层到隐藏层的权重矩阵(维度:隐藏层大小×隐藏层大小);b_h:隐藏层偏置项;h_{t-1}:第t-1时刻的隐藏状态(初始时刻h_0通常设为全0向量)。
② 输出计算(从隐藏状态映射到目标输出):
y_t = W_hy · h_t + b_y
W_hy:隐藏层到输出层的权重矩阵(维度:输出维度×隐藏层大小);b_y:输出层偏置项;- 若为分类任务,需在输出后添加Softmax激活;若为回归任务,直接输出数值。
💡 关键理解:隐藏状态h_t是RNN的“记忆核心”——W_hh · h_{t-1}项体现了对历史信息的继承,W_xh · x_t项体现了对当前输入的响应,两者融合后通过激活函数得到新的记忆状态。
1.3.2 基本RNN的三种经典应用模式
根据输入序列和输出序列的长度关系,RNN有三种核心应用模式,覆盖绝大多数序列数据处理场景:
| 应用模式 | 输入序列长度 | 输出序列长度 | 典型场景 |
|---|---|---|---|
| 一对一(One-to-One) | 1 | 1 | 固定长度输入的分类(如单句情感分析,输入句子→输出情感标签) |
| 一对多(One-to-Many) | 1 | N | 生成式任务(如图片描述生成,输入图片→输出句子;文本生成,输入开头→输出完整文本) |
| 多对一(Many-to-One) | N | 1 | 序列分类任务(如文本分类、视频分类,输入整个序列→输出单个类别) |
| 多对多(Many-to-Many) | N | N | 序列标注任务(如命名实体识别,输入句子→每个单词标注实体类型;机器翻译,输入源语言句子→输出目标语言句子) |
📝 示例:多对一模式(文本情感分析)
输入序列(单词嵌入):[“这部”, “电影”, “非常”, “精彩”](长度4)→ RNN处理→输出情感标签(“正面”,长度1)。
📝 示例:多对多模式(命名实体识别)
输入序列(单词):[“张三”, “出生于”, “北京”, “是”, “工程师”]→ 输出序列(实体标签):[“人名”, “无”, “地名”, “无”, “职业”]。
1.3.3 基本RNN的致命缺陷:梯度消失/爆炸
尽管基本RNN的设计思路巧妙,但在实际训练中,当序列长度较长(如超过20个时刻)时,会出现严重的梯度消失(Gradient Vanishing)或梯度爆炸(Gradient Exploding) 问题,导致模型无法学习长距离依赖。
1. 成因:梯度的时序传播特性
RNN的训练依赖反向传播(BPTT,Backpropagation Through Time),即沿着时间维度反向计算每个时刻参数的梯度。以隐藏层权重W_hh的梯度为例,其梯度是所有时刻梯度的累加,且每个时刻的梯度都包含W_hh的幂次项(如第t时刻的梯度包含(W_hh^T)^(T-t))。
- 梯度消失:当
W_hh的特征值小于1时,(W_hh^T)^(T-t)会随着序列长度T的增加指数级衰减,导致深层时刻(早期时刻)的梯度趋近于0,参数无法更新; - 梯度爆炸:当
W_hh的特征值大于1时,(W_hh^T)^(T-t)会指数级增长,导致梯度值过大,参数更新时溢出(如变成NaN)。
2. 直观影响
基本RNN只能学习到序列中“短距离依赖”(如相邻2-3个元素的关系),无法捕捉“长距离依赖”(如文本中开头和结尾的语义关联、时序数据中上周与本周的趋势关联)。例如:
- 文本:“虽然这部电影的开头有些平淡,但中间的剧情反转和结尾的升华让它成为一部佳作”——基本RNN难以将“开头平淡”与“佳作”的转折关系关联起来;
- 时序数据:股票价格的长期趋势(如月度趋势)无法通过基本RNN建模,只能捕捉短期波动。
💡 解决方案:为了解决长距离依赖和梯度消失问题,研究者提出了门控循环单元(Gated Recurrent Unit, GRU) 和长短期记忆网络(Long Short-Term Memory, LSTM),通过门控机制精准控制历史信息的传递与遗忘,成为当前序列建模的主流架构。
1.4 门控循环单元:LSTM与GRU
1.4.1 LSTM:长短期记忆网络
LSTM是1997年提出的门控RNN变体,通过三个门控单元(遗忘门、输入门、输出门) 和一个细胞状态(Cell State),实现对历史信息的“选择性遗忘”和“选择性记忆”,从根本上解决了基本RNN的梯度消失问题。
1. LSTM的核心结构:细胞状态与三门控
LSTM的核心创新是细胞状态(Cell State) ——一个类似“传送带”的结构,信息在上面直接传递,仅通过门控单元进行少量修改,梯度可以沿着细胞状态顺畅传播,避免衰减或爆炸。
三个门控单元的作用是“控制细胞状态的信息流动”,每个门控单元都是一个“ sigmoid激活函数+逐元素乘法”的组合:
- Sigmoid激活函数输出范围(0,1),表示“信息通过的概率”(1=完全通过,0=完全阻断);
- 逐元素乘法用于将门控信号与数据结合,实现对信息的筛选。
2. LSTM的工作流程(逐时刻解析)
假设当前时刻为t,输入为x_t,上一时刻隐藏状态为h_{t-1},上一时刻细胞状态为c_{t-1},LSTM的工作流程分为五步:
① 遗忘门(Forget Gate):决定“哪些历史信息需要被遗忘”
- 输入:当前输入
x_t+ 上一时刻隐藏状态h_{t-1}; - 计算:
f_t = σ(W_f · [h_{t-1}, x_t] + b_f); - 作用:
f_t与上一时刻细胞状态c_{t-1}逐元素相乘,f_t接近1时保留信息,接近0时遗忘信息。 - 示例:文本处理中,当遇到“但是”“然而”等转折词时,遗忘门会关闭,遗忘之前的负面信息,准备接收新的正面信息。
② 输入门(Input Gate):决定“哪些当前信息需要被记忆”
- 第一步:计算输入门信号,控制信息是否进入细胞状态:
i_t = σ(W_i · [h_{t-1}, x_t] + b_i); - 第二步:生成候选记忆信息(通过tanh激活,值域(-1,1)):
ã_t = tanh(W_c · [h_{t-1}, x_t] + b_c); - 第三步:候选信息与输入门信号相乘,筛选后得到“待更新记忆”:
i_t ⊙ ã_t。
③ 细胞状态更新:融合历史记忆与当前记忆
- 公式:
c_t = f_t ⊙ c_{t-1} + i_t ⊙ ã_t; - 逻辑:先通过遗忘门筛选历史记忆,再加入输入门筛选后的当前记忆,得到新的细胞状态。
④ 输出门(Output Gate):决定“哪些信息需要输出到隐藏状态”
- 第一步:计算输出门信号,控制细胞状态的输出:
o_t = σ(W_o · [h_{t-1}, x_t] + b_o); - 第二步:细胞状态通过tanh激活(将值域压缩到(-1,1)),再与输出门信号相乘,得到当前时刻隐藏状态:
h_t = o_t ⊙ tanh(c_t); - 作用:
h_t既包含了当前时刻的关键信息,也继承了筛选后的历史信息,将用于下一时刻的计算和当前时刻的输出。
⑤ 输出计算:y_t = W_hy · h_t + b_y(与基本RNN一致)
💡 核心优势:细胞状态的“长距离传播”+ 门控单元的“精准控制”,使LSTM能有效记忆长序列中的关键信息(如文本中的核心主题、时序数据的长期趋势),同时遗忘无关噪声,梯度消失问题得到根本缓解。
3. LSTM梯度传播的优势
LSTM的细胞状态更新公式c_t = f_t ⊙ c_{t-1} + i_t ⊙ ã_t中,梯度传播路径为:
∂L/∂c_{t-1} = ∂L/∂c_t · f_t
由于遗忘门f_t通常被训练为接近1(保留大部分历史信息),梯度∂L/∂c_{t-1}不会随着时间步增加而指数衰减,从而实现长距离梯度的有效传递。
1.4.2 GRU:门控循环单元(简化版LSTM)
GRU是2014年提出的LSTM变体,其核心思想是“简化LSTM的门控结构,同时保持相近的性能”,通过更新门和重置门两个门控单元,合并了LSTM的细胞状态和隐藏状态,参数更少,训练速度更快。
1. GRU的核心结构:两门控+隐藏状态
GRU取消了LSTM的细胞状态和输出门,将遗忘门和输入门合并为更新门(Update Gate),保留了类似重置门的功能,仅包含两个门控单元:
- 更新门(z_t):决定“历史信息和当前信息的融合比例”(替代LSTM的遗忘门+输入门);
- 重置门(r_t):决定“是否忽略历史信息,仅使用当前输入”;
- 隐藏状态(h_t):同时承担LSTM细胞状态的“长时记忆”和隐藏状态的“短时记忆”功能。
2. GRU的工作流程
① 重置门与更新门计算:
r_t = σ(W_r · [h_{t-1}, x_t] + b_r)(重置门:控制历史信息的使用)
z_t = σ(W_z · [h_{t-1}, x_t] + b_z)(更新门:控制历史与当前信息的融合比例)
② 候选隐藏状态计算(类似LSTM的候选记忆):
h_t~ = tanh(W_h · [r_t ⊙ h_{t-1}, x_t] + b_h)
- 逻辑:重置门
r_t与上一时刻隐藏状态h_{t-1}相乘,若r_t接近0,会忽略历史信息,仅用当前输入x_t生成候选状态;若r_t接近1,会融合历史信息和当前输入。
③ 隐藏状态更新(核心步骤):
h_t = (1 - z_t) ⊙ h_{t-1} + z_t ⊙ h_t~
- 逻辑:更新门
z_t控制融合比例——(1 - z_t) ⊙ h_{t-1}表示保留的历史信息,z_t ⊙ h_t~表示加入的当前信息; - 示例:
z_t接近1时,更多保留当前信息,适合捕捉短期依赖;z_t接近0时,更多保留历史信息,适合捕捉长期依赖。
3. LSTM与GRU的对比与选择
| 特性 | LSTM | GRU |
|---|---|---|
| 门控单元 | 3个(遗忘门、输入门、输出门) | 2个(更新门、重置门) |
| 参数数量 | 更多(计算量更大) | 更少(计算量更小,训练更快) |
| 记忆能力 | 长时记忆能力更强(细胞状态独立) | 长时记忆能力略弱于LSTM,但足够应对大部分场景 |
| 适用场景 | 长序列数据(如长文本、长时序数据)、对记忆精度要求高的任务 | 中短序列数据、对训练速度要求高的场景、资源受限的设备(如移动端) |
💡 实战选择建议:
- 优先尝试GRU:参数少、训练快,且在大部分任务(如文本分类、情感分析、短时序预测)中性能与LSTM接近;
- 复杂场景用LSTM:当序列长度超过100、长距离依赖对任务至关重要(如长文本生成、机器翻译)时,LSTM的记忆稳定性更有优势;
- 超大规模任务:可尝试更高效的变体(如LSTM的变体Peephole LSTM、GRU的变体UGRNN),或结合注意力机制(Attention)进一步提升性能。
1.5 序列数据预处理:从原始数据到模型输入
在使用RNN/LSTM/GRU建模前,必须对序列数据进行预处理,将其转换为模型可接受的数值张量。不同类型的序列数据预处理流程略有差异,本节以最常用的文本数据和时序数据为例,详细讲解预处理步骤。
1.5.1 文本数据预处理(核心流程)
文本数据是典型的离散序列数据,预处理的核心是“将文本符号(单词、字符)转换为数值向量”,流程分为五步:
1. 文本清洗与分词
- 文本清洗:去除无关字符(如标点符号、特殊符号、冗余空格)、统一大小写(如全部转为小写)、去除停用词(如“的”“是”“和”等无实义词汇,可选);
- 分词:将文本分割为最小语义单元(中文用jieba分词,英文按空格分词)。
📝 示例:
原始文本:“人工智能(AI)是21世纪最具影响力的技术之一!”
清洗后:“人工智能AI是21世纪最具影响力的技术之一”
分词后:[“人工”, “智能”, “AI”, “是”, “21”, “世纪”, “最具”, “影响力”, “的”, “技术”, “之一”]
去除停用词后:[“人工”, “智能”, “AI”, “21”, “世纪”, “最具”, “影响力”, “技术”]
2. 构建词汇表(Vocabulary)
将分词后的所有单词汇总,构建“单词→索引”的映射表(词汇表),实现从离散符号到连续索引的转换。
📝 步骤:
① 统计所有文本中的单词频率;
② 筛选高频单词(如保留频率前10000的单词,过滤低频生词);
③ 为每个单词分配唯一索引(如“人工”→0,“智能”→1,“AI”→2,…);
④ 添加特殊符号:
<PAD>:填充符号,用于将不同长度的序列补成相同长度(模型输入需固定维度);<UNK>:未知符号,用于表示词汇表中未包含的生词;<SOS>:句子开始符号(生成式任务用);<EOS>:句子结束符号(生成式任务用)。
📝 词汇表示例:
vocab = {"<PAD>":0, "<UNK>":1, "人工":2, "智能":3, "AI":4, "技术":5, ...}
3. 序列编码(单词→索引)
将每个文本序列转换为索引序列,即用词汇表中的索引替代对应的单词,未知单词用<UNK>的索引(1)表示。
📝 示例:
分词后序列:[“人工”, “智能”, “AI”, “技术”]
编码后序列:[2, 3, 4, 5]
4. 序列填充与截断(Padding & Truncation)
模型训练时需要批量输入(batch),而不同文本的长度不同(如有的句子5个单词,有的句子20个单词),因此需要将所有序列调整为相同长度:
- 填充(Padding):对长度小于目标长度的序列,在末尾添加
<PAD>的索引(0); - 截断(Truncation):对长度大于目标长度的序列,从开头或末尾截断(通常从末尾截断,保留开头信息)。
📝 示例:
目标长度=6,编码后序列:[2, 3, 4, 5]
填充后序列:[2, 3, 4, 5, 0, 0](末尾添加2个)
编码后序列:[2, 3, 4, 5, 6, 7, 8](长度=7>6)
截断后序列:[2, 3, 4, 5, 6, 7](末尾截断1个元素)
5. 词嵌入(Word Embedding):将索引→低维向量
编码后的索引序列是离散的整数(如[2,3,4,5]),直接输入模型会导致“语义鸿沟”(如“人工”和“智能”的索引值大小与语义相关性无关)。词嵌入的作用是将每个单词的索引映射为低维稠密向量(如128维、256维),使语义相近的单词向量距离更近。
💡 词嵌入的两种实现方式:
- 随机初始化:在模型中定义嵌入层(Embedding Layer),随机初始化向量,随模型一起训练;
- 预训练嵌入:使用预训练的词向量(如Word2Vec、GloVe、BERT词嵌入)初始化嵌入层,可加速训练并提升性能(尤其适用于小数据集)。
📝 PyTorch词嵌入示例:
import torch
import torch.nn as nn
# 词汇表大小=10000,嵌入维度=128
embedding = nn.Embedding(num_embeddings=10000, embedding_dim=128, padding_idx=0) # padding_idx指定<PAD>的索引,嵌入向量固定为0
# 输入:批量大小=32,序列长度=10(索引序列)
x = torch.randint(0, 10000, (32, 10)) # shape: (32, 10)
# 词嵌入:输出shape=(32, 10, 128)(批量大小×序列长度×嵌入维度)
x_embed = embedding(x)
print(x_embed.shape) # torch.Size([32, 10, 128])
1.5.2 时序数据预处理(核心流程)
时序数据是连续数值序列(如股票价格、气温),预处理的核心是“标准化数据、构建监督学习样本”,流程分为四步:
1. 数据清洗
- 缺失值处理:时序数据常存在缺失值,可通过“前向填充(用前一时刻值填充)”“后向填充(用后一时刻值填充)”或“插值填充(如线性插值)”处理;
- 异常值处理:通过箱线图、3σ原则识别异常值,采用“替换为均值/中位数”或“删除异常值”处理(需结合业务场景)。
2. 数据标准化/归一化
时序数据的量级差异可能较大(如股票价格从10元到100元),标准化/归一化能加速模型收敛,避免因量级差异导致的参数更新不均衡。
📝 常用方法:
- 标准化(Z-Score):
x' = (x - μ) / σ(μ为均值,σ为标准差),适用于数据近似正态分布的场景; - 归一化(Min-Max):
x' = (x - min) / (max - min)(映射到[0,1]区间),适用于数据分布未知的场景。
⚠️ 注意:标准化/归一化的均值、标准差、最大值、最小值必须基于训练集计算,再应用到测试集,避免数据泄露。
3. 构建监督学习样本(序列→输入-输出对)
时序预测的核心是“用历史T个时刻的数据预测未来1个或多个时刻的数据”,因此需要将连续的时序序列转换为“输入序列→输出值”的监督学习样本。
📝 示例:
原始时序数据(气温,单位℃):[20, 22, 23, 25, 24, 26, 28]
设定历史窗口T=3(用前3天气温预测第4天气温),则构建的样本为:
- 样本1:输入[20,22,23] → 输出25
- 样本2:输入[22,23,25] → 输出24
- 样本3:输入[23,25,24] → 输出26
- 样本4:输入[25,24,26] → 输出28
4. 数据格式转换
将构建的输入序列和输出值转换为PyTorch张量,输入序列的shape为(batch_size, sequence_length, feature_dim)(feature_dim为每个时刻的特征数,如仅气温则为1,若包含气温、湿度则为2)。
📝 PyTorch示例:
import numpy as np
import torch
# 原始时序数据(1维特征:气温)
data = np.array([20, 22, 23, 25, 24, 26, 28], dtype=np.float32)
# 步骤1:标准化
mean = data[:5].mean() # 训练集(前5个数据)均值
std = data[:5].std() # 训练集标准差
data_norm = (data - mean) / std
# 步骤2:构建监督样本(T=3)
def create_sequences(data, seq_len):
sequences = []
targets = []
for i in range(len(data) - seq_len):
seq = data[i:i+seq_len]
target = data[i+seq_len]
sequences.append(seq)
targets.append(target)
return np.array(sequences), np.array(targets)
seq_len = 3
X, y = create_sequences(data_norm, seq_len)
# 步骤3:转换为张量(添加特征维度,shape从(batch, seq_len)→(batch, seq_len, 1))
X_tensor = torch.tensor(X).unsqueeze(-1) # shape: (4, 3, 1)
y_tensor = torch.tensor(y).unsqueeze(-1) # shape: (4, 1)
print("输入张量shape:", X_tensor.shape)
print("输出张量shape:", y_tensor.shape)
1.5.3 预处理工具实战(PyTorch+第三方库)
手动实现预处理流程繁琐且易出错,实际项目中可使用PyTorch的torchtext库(文本数据)或pandas+numpy(时序数据)简化流程。
1. 文本数据预处理工具(torchtext)
import torch
from torchtext.data import Field, TabularDataset, BucketIterator
import jieba
# 1. 定义预处理字段
def tokenize_cn(text):
"""中文分词函数"""
return jieba.lcut(text)
# 定义文本字段(Field)
TEXT = Field(
sequential=True, # 序列数据
tokenize=tokenize_cn, # 分词函数
lower=True, # 转为小写
batch_first=True, # batch维度在前(shape: (batch, seq_len))
fix_length=20, # 固定序列长度(不足填充,过长截断)
pad_token="<PAD>",
unk_token="<UNK>"
)
LABEL = Field(
sequential=False, # 非序列数据(分类标签)
use_vocab=False, # 无需构建词汇表(标签已为数值)
dtype=torch.long
)
# 2. 加载数据集(假设数据集为CSV格式,包含text和label列)
fields = [("text", TEXT), ("label", LABEL)]
train_data, test_data = TabularDataset.splits(
path="./data", # 数据集路径
train="train.csv",
test="test.csv",
format="csv",
fields=fields,
skip_header=True # 跳过CSV表头
)
# 3. 构建词汇表
TEXT.build_vocab(train_data, max_size=10000) # 基于训练集构建词汇表,保留前10000个高频词
print("词汇表大小:", len(TEXT.vocab))
print("高频词前5个:", TEXT.vocab.freqs.most_common(5))
# 4. 创建数据加载器(批量迭代)
train_loader, test_loader = BucketIterator.splits(
(train_data, test_data),
batch_size=32,
device=torch.device("cuda" if torch.cuda.is_available() else "cpu"),
sort_key=lambda x: len(x.text), # 按序列长度排序,减少填充量
sort_within_batch=True
)
# 测试数据加载器
for batch in train_loader:
print("文本序列(索引)shape:", batch.text.shape) # (32, 20)
print("标签shape:", batch.label.shape) # (32,)
break
2. 时序数据预处理工具(pandas+PyTorch)
import pandas as pd
import numpy as np
import torch
from torch.utils.data import Dataset, DataLoader
# 1. 加载数据(假设为CSV格式,包含date和temperature列)
data = pd.read_csv("./temperature_data.csv")
data["date"] = pd.to_datetime(data["date"])
data = data.sort_values("date") # 按时间排序
# 2. 数据清洗与标准化
data = data.dropna() # 删除缺失值
temperature = data["temperature"].values.reshape(-1, 1) # 转为(样本数, 特征数)
# 训练集/测试集划分(按时间划分,避免未来信息泄露)
train_size = int(0.8 * len(temperature))
train_data = temperature[:train_size]
test_data = temperature[train_size:]
# 标准化
mean = train_data.mean()
std = train_data.std()
train_data_norm = (train_data - mean) / std
test_data_norm = (test_data - mean) / std
# 3. 自定义时序数据集
class TimeSeriesDataset(Dataset):
def __init__(self, data, seq_len):
self.data = data
self.seq_len = seq_len
def __getitem__(self, idx):
# 输入序列:idx到idx+seq_len
seq = self.data[idx:idx+self.seq_len]
# 输出值:idx+seq_len
target = self.data[idx+self.seq_len]
return torch.tensor(seq, dtype=torch.float32), torch.tensor(target, dtype=torch.float32)
def __len__(self):
# 样本数 = 总数据量 - 序列长度
return len(self.data) - self.seq_len
# 4. 创建数据集和数据加载器
seq_len = 7 # 用前7天数据预测第8天气温
train_dataset = TimeSeriesDataset(train_data_norm, seq_len)
test_dataset = TimeSeriesDataset(test_data_norm, seq_len)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)
# 测试数据加载器
for seq, target in train_loader:
print("输入序列shape:", seq.shape) # (32, 7, 1)
print("输出值shape:", target.shape) # (32, 1)
break
1.6 实战:用PyTorch搭建RNN/GRU/LSTM模型
本节通过两个经典实战任务——文本情感分析(多对一模式) 和时序预测(多对一模式),完整演示如何用PyTorch搭建RNN、GRU、LSTM模型,对比不同模型的性能差异,并讲解训练过程中的关键技巧。
1.6.1 实战1:文本情感分析(IMDB电影评论分类)
IMDB电影评论数据集包含50000条电影评论,分为正面评论(标签1)和负面评论(标签0),是文本情感分析的经典数据集。任务目标:输入一条电影评论,预测其情感倾向(正面/负面)。
步骤1:数据预处理(基于torchtext)
import torch
import torch.nn as nn
import torch.optim as optim
from torchtext.data import Field, TabularDataset, BucketIterator
from torchtext.datasets import IMDB # 直接加载IMDB数据集
# 1. 定义预处理字段
TEXT = Field(
sequential=True,
tokenize="spacy", # 英文分词(需安装spacy:pip install spacy && python -m spacy download en_core_web_sm)
lower=True,
batch_first=True,
pad_token="<PAD>",
unk_token="<UNK>"
)
LABEL = Field(
sequential=False,
use_vocab=False,
dtype=torch.long,
preprocessing=lambda x: 1 if x == "pos" else 0 # 将pos转为1,neg转为0
)
# 2. 加载IMDB数据集
train_data, test_data = IMDB.splits(TEXT, LABEL)
# 3. 构建词汇表
TEXT.build_vocab(train_data, max_size=20000, vectors="glove.6B.100d") # 使用预训练GloVe词嵌入(100维)
print("词汇表大小:", len(TEXT.vocab))
print("预训练词嵌入shape:", TEXT.vocab.vectors.shape) # (20002, 100)(包含<PAD>和<UNK>)
# 4. 创建数据加载器
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
train_loader, test_loader = BucketIterator.splits(
(train_data, test_data),
batch_size=64,
device=device,
sort_key=lambda x: len(x.text),
sort_within_batch=True
)
步骤2:搭建RNN/GRU/LSTM模型
class SentimentAnalysisModel(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_dim, output_dim, num_layers, dropout, model_type="rnn"):
super(SentimentAnalysisModel, self).__init__()
# 词嵌入层(使用预训练词向量初始化)
self.embedding = nn.Embedding(vocab_size, embed_dim)
self.embedding.weight.data.copy_(TEXT.vocab.vectors) # 加载预训练词嵌入
self.embedding.weight.requires_grad = True # 允许微调词嵌入(也可设为False固定)
# 循环层(RNN/GRU/LSTM)
self.model_type = model_type
if model_type == "rnn":
self.rnn = nn.RNN(
input_size=embed_dim,
hidden_size=hidden_dim,
num_layers=num_layers,
batch_first=True,
dropout=dropout if num_layers > 1 else 0 # 多层时才使用dropout
)
elif model_type == "gru":
self.rnn = nn.GRU(
input_size=embed_dim,
hidden_size=hidden_dim,
num_layers=num_layers,
batch_first=True,
dropout=dropout if num_layers > 1 else 0
)
elif model_type == "lstm":
self.rnn = nn.LSTM(
input_size=embed_dim,
hidden_size=hidden_dim,
num_layers=num_layers,
batch_first=True,
dropout=dropout if num_layers > 1 else 0
)
# 全连接层(多对一模式:仅使用最后一个时刻的隐藏状态)
self.fc = nn.Linear(hidden_dim, output_dim)
# Dropout层(抑制过拟合)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
# 1. 词嵌入:(batch_size, seq_len) → (batch_size, seq_len, embed_dim)
embedded = self.dropout(self.embedding(x))
# 2. 循环层:输出output和隐藏状态hidden
# output: (batch_size, seq_len, hidden_dim)(所有时刻的隐藏状态)
# hidden: (num_layers, batch_size, hidden_dim)(最后一个时刻的隐藏状态)
if self.model_type == "lstm":
output, (hidden, cell) = self.rnn(embedded)
else:
output, hidden = self.rnn(embedded)
# 3. 多对一:取最后一个时刻的隐藏状态(hidden[-1]:取最后一层的隐藏状态)
hidden = self.dropout(hidden[-1])
# 4. 全连接层:(batch_size, hidden_dim) → (batch_size, output_dim)
out = self.fc(hidden)
return out
# 模型参数设置
VOCAB_SIZE = len(TEXT.vocab)
EMBED_DIM = 100 # 与预训练词嵌入维度一致
HIDDEN_DIM = 256
OUTPUT_DIM = 2 # 二分类(正面/负面)
NUM_LAYERS = 2 # 2层循环网络
DROPOUT = 0.5
# 初始化三个模型(RNN/GRU/LSTM)
rnn_model = SentimentAnalysisModel(VOCAB_SIZE, EMBED_DIM, HIDDEN_DIM, OUTPUT_DIM, NUM_LAYERS, DROPOUT, model_type="rnn").to(device)
gru_model = SentimentAnalysisModel(VOCAB_SIZE, EMBED_DIM, HIDDEN_DIM, OUTPUT_DIM, NUM_LAYERS, DROPOUT, model_type="gru").to(device)
lstm_model = SentimentAnalysisModel(VOCAB_SIZE, EMBED_DIM, HIDDEN_DIM, OUTPUT_DIM, NUM_LAYERS, DROPOUT, model_type="lstm").to(device)
# 打印模型参数数量
def count_parameters(model):
return sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"RNN模型参数数量:{count_parameters(rnn_model):,}")
print(f"GRU模型参数数量:{count_parameters(gru_model):,}")
print(f"LSTM模型参数数量:{count_parameters(lstm_model):,}")
步骤3:模型训练与评估
def train_model(model, train_loader, criterion, optimizer, epochs=10):
model.train()
train_losses = []
train_accs = []
for epoch in range(epochs):
total_loss = 0.0
total_correct = 0
total_samples = 0
for batch in train_loader:
# 输入:batch.text(shape: (64, seq_len)),标签:batch.label(shape: (64,))
optimizer.zero_grad()
outputs = model(batch.text)
loss = criterion(outputs, batch.label)
loss.backward()
# 梯度裁剪(防止梯度爆炸)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
# 计算损失和准确率
total_loss += loss.item() * batch.text.size(0)
_, predicted = torch.max(outputs, 1)
total_correct += (predicted == batch.label).sum().item()
total_samples += batch.text.size(0)
# 平均损失和准确率
avg_loss = total_loss / total_samples
avg_acc = total_correct / total_samples
train_losses.append(avg_loss)
train_accs.append(avg_acc)
print(f"Epoch [{epoch+1}/{epochs}], Train Loss: {avg_loss:.4f}, Train Acc: {avg_acc:.4f}")
return train_losses, train_accs
def evaluate_model(model, test_loader, criterion):
model.eval()
total_loss = 0.0
total_correct = 0
total_samples = 0
with torch.no_grad():
for batch in test_loader:
outputs = model(batch.text)
loss = criterion(outputs, batch.label)
total_loss += loss.item() * batch.text.size(0)
_, predicted = torch.max(outputs, 1)
total_correct += (predicted == batch.label).sum().item()
total_samples += batch.text.size(0)
avg_loss = total_loss / total_samples
avg_acc = total_correct / total_samples
print(f"Test Loss: {avg_loss:.4f}, Test Acc: {avg_acc:.4f}")
return avg_loss, avg_acc
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer_rnn = optim.Adam(rnn_model.parameters(), lr=0.001)
optimizer_gru = optim.Adam(gru_model.parameters(), lr=0.001)
optimizer_lstm = optim.Adam(lstm_model.parameters(), lr=0.001)
# 训练RNN模型
print("=== 训练RNN模型 ===")
rnn_train_losses, rnn_train_accs = train_model(rnn_model, train_loader, criterion, optimizer_rnn, epochs=10)
rnn_test_loss, rnn_test_acc = evaluate_model(rnn_model, test_loader, criterion)
# 训练GRU模型
print("\n=== 训练GRU模型 ===")
gru_train_losses, gru_train_accs = train_model(gru_model, train_loader, criterion, optimizer_gru, epochs=10)
gru_test_loss, gru_test_acc = evaluate_model(gru_model, test_loader, criterion)
# 训练LSTM模型
print("\n=== 训练LSTM模型 ===")
lstm_train_losses, lstm_train_accs = train_model(lstm_model, train_loader, criterion, optimizer_lstm, epochs=10)
lstm_test_loss, lstm_test_acc = evaluate_model(lstm_model, test_loader, criterion)
# 对比三个模型的测试准确率
print("\n=== 模型性能对比 ===")
print(f"RNN测试准确率:{rnn_test_acc:.4f}")
print(f"GRU测试准确率:{gru_test_acc:.4f}")
print(f"LSTM测试准确率:{lstm_test_acc:.4f}")
步骤4:结果可视化与分析
import matplotlib.pyplot as plt
# 绘制训练损失曲线
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.plot(range(1, 11), rnn_train_losses, label="RNN")
plt.plot(range(1, 11), gru_train_losses, label="GRU")
plt.plot(range(1, 11), lstm_train_losses, label="LSTM")
plt.xlabel("Epoch")
plt.ylabel("Train Loss")
plt.title("Training Loss Curves")
plt.legend()
# 绘制训练准确率曲线
plt.subplot(1, 2, 2)
plt.plot(range(1, 11), rnn_train_accs, label="RNN")
plt.plot(range(1, 11), gru_train_accs, label="GRU")
plt.plot(range(1, 11), lstm_train_accs, label="LSTM")
plt.xlabel("Epoch")
plt.ylabel("Train Acc")
plt.title("Training Accuracy Curves")
plt.legend()
plt.show()
💡 结果预期:
- RNN模型:测试准确率约80%-85%,训练后期可能出现梯度消失,准确率难以提升;
- GRU模型:测试准确率约88%-90%,训练速度快,性能稳定;
- LSTM模型:测试准确率约89%-91%,长距离依赖建模能力略强于GRU,但训练时间更长。
1.6.2 实战2:时序预测(气温预测)
使用某地区的日气温数据,基于前7天的气温预测第8天的气温(回归任务),对比RNN、GRU、LSTM的预测效果。
步骤1:数据预处理(基于pandas+PyTorch)
import pandas as pd
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import matplotlib.pyplot as plt
# 1. 加载并预处理数据
data = pd.read_csv("./temperature_data.csv")
data["date"] = pd.to_datetime(data["date"])
data = data.sort_values("date").dropna() # 按时间排序并删除缺失值
# 提取气温数据(1维特征)
temperature = data["temperature"].values.reshape(-1, 1).astype(np.float32)
# 训练集/测试集划分(按时间划分,8:2)
train_size = int(0.8 * len(temperature))
train_data = temperature[:train_size]
test_data = temperature[train_size:]
# 标准化
mean = train_data.mean()
std = train_data.std()
train_data_norm = (train_data - mean) / std
test_data_norm = (test_data - mean) / std
# 2. 自定义时序数据集
class TemperatureDataset(Dataset):
def __init__(self, data, seq_len):
self.data = data
self.seq_len = seq_len
def __getitem__(self, idx):
seq = self.data[idx:idx+self.seq_len]
target = self.data[idx+self.seq_len]
return torch.tensor(seq), torch.tensor(target)
def __len__(self):
return len(self.data) - self.seq_len
# 3. 创建数据加载器
seq_len = 7 # 历史窗口长度
batch_size = 32
train_dataset = TemperatureDataset(train_data_norm, seq_len)
test_dataset = TemperatureDataset(test_data_norm, seq_len)
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
# 可视化原始数据
plt.figure(figsize=(12, 4))
plt.plot(data["date"][:train_size], train_data.squeeze(), label="Train Data")
plt.plot(data["date"][train_size:train_size+len(test_data)], test_data.squeeze(), label="Test Data")
plt.xlabel("Date")
plt.ylabel("Temperature (℃)")
plt.title("Temperature Time Series Data")
plt.legend()
plt.show()
步骤2:搭建时序预测模型
class TimeSeriesPredictionModel(nn.Module):
def __init__(self, input_dim, hidden_dim, output_dim, num_layers, dropout, model_type="rnn"):
super(TimeSeriesPredictionModel, self).__init__()
self.model_type = model_type
# 循环层
if model_type == "rnn":
self.rnn = nn.RNN(
input_size=input_dim,
hidden_size=hidden_dim,
num_layers=num_layers,
batch_first=True,
dropout=dropout if num_layers > 1 else 0
)
elif model_type == "gru":
self.rnn = nn.GRU(
input_size=input_dim,
hidden_size=hidden_dim,
num_layers=num_layers,
batch_first=True,
dropout=dropout if num_layers > 1 else 0
)
elif model_type == "lstm":
self.rnn = nn.LSTM(
input_size=input_dim,
hidden_size=hidden_dim,
num_layers=num_layers,
batch_first=True,
dropout=dropout if num_layers > 1 else 0
)
# 全连接层(回归任务,输出1个数值)
self.fc1 = nn.Linear(hidden_dim, hidden_dim // 2)
self.fc2 = nn.Linear(hidden_dim // 2, output_dim)
self.dropout = nn.Dropout(dropout)
self.relu = nn.ReLU()
def forward(self, x):
# x: (batch_size, seq_len, input_dim)
if self.model_type == "lstm":
output, (hidden, cell) = self.rnn(x)
else:
output, hidden = self.rnn(x)
# 取最后一个时刻的隐藏状态
hidden = self.dropout(hidden[-1])
# 全连接层
out = self.relu(self.fc1(hidden))
out = self.dropout(out)
out = self.fc2(out)
return out
# 模型参数
INPUT_DIM = 1 # 输入特征数(仅气温)
HIDDEN_DIM = 128
OUTPUT_DIM = 1 # 输出特征数(预测气温)
NUM_LAYERS = 2
DROPOUT = 0.3
# 初始化模型
rnn_pred_model = TimeSeriesPredictionModel(INPUT_DIM, HIDDEN_DIM, OUTPUT_DIM, NUM_LAYERS, DROPOUT, model_type="rnn")
gru_pred_model = TimeSeriesPredictionModel(INPUT_DIM, HIDDEN_DIM, OUTPUT_DIM, NUM_LAYERS, DROPOUT, model_type="gru")
lstm_pred_model = TimeSeriesPredictionModel(INPUT_DIM, HIDDEN_DIM, OUTPUT_DIM, NUM_LAYERS, DROPOUT, model_type="lstm")
# 移动到GPU(如果有)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
rnn_pred_model = rnn_pred_model.to(device)
gru_pred_model = gru_pred_model.to(device)
lstm_pred_model = lstm_pred_model.to(device)
步骤3:模型训练与评估
def train_pred_model(model, train_loader, criterion, optimizer, epochs=50):
model.train()
train_losses = []
for epoch in range(epochs):
total_loss = 0.0
for seq, target in train_loader:
seq = seq.to(device)
target = target.to(device)
optimizer.zero_grad()
outputs = model(seq)
loss = criterion(outputs, target)
loss.backward()
# 梯度裁剪
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
total_loss += loss.item() * seq.size(0)
avg_loss = total_loss / len(train_loader.dataset)
train_losses.append(avg_loss)
if (epoch + 1) % 10 == 0:
print(f"Epoch [{epoch+1}/{epochs}], Train Loss (MSE): {avg_loss:.4f}")
return train_losses
def evaluate_pred_model(model, test_loader, criterion, mean, std):
model.eval()
total_loss = 0.0
predictions = []
targets = []
with torch.no_grad():
for seq, target in test_loader:
seq = seq.to(device)
target = target.to(device)
outputs = model(seq)
loss = criterion(outputs, target)
total_loss += loss.item() * seq.size(0)
# 反标准化(将预测值和真实值恢复为原始量级)
outputs = outputs * std + mean
target = target * std + mean
predictions.extend(outputs.cpu().numpy())
targets.extend(target.cpu().numpy())
avg_loss = total_loss / len(test_loader.dataset)
rmse = np.sqrt(avg_loss)
print(f"Test RMSE: {rmse:.2f}")
return predictions, targets, rmse
# 定义损失函数和优化器(回归任务用MSE损失)
criterion = nn.MSELoss()
optimizer_rnn = optim.Adam(rnn_pred_model.parameters(), lr=0.001)
optimizer_gru = optim.Adam(gru_pred_model.parameters(), lr=0.001)
optimizer_lstm = optim.Adam(lstm_pred_model.parameters(), lr=0.001)
# 训练模型
print("=== 训练RNN时序预测模型 ===")
rnn_train_losses = train_pred_model(rnn_pred_model, train_loader, criterion, optimizer_rnn, epochs=50)
rnn_preds, rnn_targets, rnn_rmse = evaluate_pred_model(rnn_pred_model, test_loader, criterion, mean, std)
print("\n=== 训练GRU时序预测模型 ===")
gru_train_losses = train_pred_model(gru_pred_model, train_loader, criterion, optimizer_gru, epochs=50)
gru_preds, gru_targets, gru_rmse = evaluate_pred_model(gru_pred_model, test_loader, criterion, mean, std)
print("\n=== 训练LSTM时序预测模型 ===")
lstm_train_losses = train_pred_model(lstm_pred_model, train_loader, criterion, optimizer_lstm, epochs=50)
lstm_preds, lstm_targets, lstm_rmse = evaluate_pred_model(lstm_pred_model, test_loader, criterion, mean, std)
步骤4:预测结果可视化
# 绘制测试集预测结果
plt.figure(figsize=(14, 6))
plt.plot(range(len(rnn_targets)), rnn_targets, label="True Temperature", color="black", linewidth=2)
plt.plot(range(len(rnn_preds)), rnn_preds, label=f"RNN Prediction (RMSE={rnn_rmse:.2f})", color="red", linestyle="--")
plt.plot(range(len(gru_preds)), gru_preds, label=f"GRU Prediction (RMSE={gru_rmse:.2f})", color="blue", linestyle="--")
plt.plot(range(len(lstm_preds)), lstm_preds, label=f"LSTM Prediction (RMSE={lstm_rmse:.2f})", color="green", linestyle="--")
plt.xlabel("Time Step (Test Set)")
plt.ylabel("Temperature (℃)")
plt.title("Temperature Prediction Results on Test Set")
plt.legend()
plt.show()
💡 结果预期:
- RNN模型:RMSE较大(如2.5℃左右),对长时序趋势的捕捉能力弱,预测曲线波动较大;
- GRU模型:RMSE较小(如1.8℃左右),预测曲线更平滑,能较好捕捉气温的短期趋势;
- LSTM模型:RMSE最小(如1.5℃左右),对气温的长期趋势和周期性变化的建模精度最高。
1.7 RNN训练常见问题与优化策略
1.7.1 梯度消失/爆炸(循环层核心问题)
尽管LSTM/GRU缓解了梯度消失问题,但在深层循环网络或长序列场景下,仍可能出现梯度异常。
解决方案
-
⚠️ 梯度裁剪(Gradient Clipping):
- 核心逻辑:当梯度的L2范数超过预设阈值(如1.0)时,对所有梯度进行等比例缩放,限制梯度最大值;
- PyTorch实现:
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0); - 建议:所有循环模型训练时都启用梯度裁剪,阈值通常设为1.0-5.0。
-
🔧 优化循环层结构:
- 减少循环层数量(如从3层减至2层),避免梯度传播路径过长;
- 降低隐藏层维度(如从512减至256),减少参数规模,降低梯度计算复杂度。
-
📊 选择合适的激活函数:
- 循环层:LSTM/GRU默认使用tanh激活,无需修改;
- 全连接层:使用ReLU或GELU,避免使用sigmoid(易导致梯度消失)。
1.7.2 过拟合(循环模型参数多,易过拟合)
循环模型(尤其是LSTM)的参数数量较多,在小数据集场景下容易过拟合。
解决方案
-
🔧 正则化手段:
- Dropout层:在嵌入层后、循环层后、全连接层后添加Dropout(比例0.3-0.5),注意循环层的dropout仅在多层时生效;
- L2正则化:在优化器中设置
weight_decay=1e-4~1e-3,惩罚过大的参数; - 早停(Early Stopping):监控测试集损失,当连续多轮(如10轮)损失不下降时停止训练,保存最佳模型。
-
📝 数据扩充:
- 文本数据:同义词替换、句子重排、翻译回译、添加噪声(如替换部分低频词为);
- 时序数据:时间轴平移、添加微小噪声、数据插值生成新样本。
-
🗄️ 简化模型结构:
- 减少隐藏层维度(如从256减至128);
- 减少循环层数量(如从2层减至1层);
- 固定词嵌入层(不微调预训练词向量),减少可训练参数。
1.7.3 长序列处理效率低(训练慢、内存不足)
当序列长度超过1000时,循环模型的训练速度会急剧下降,且容易出现内存溢出(OOM)。
解决方案
-
🚀 截断反向传播(Truncated BPTT):
- 核心逻辑:将长序列分割为多个短序列片段(如长度为50的片段),每个片段独立训练,仅在片段内部进行反向传播,不传递跨片段的梯度;
- 适用场景:对长距离依赖要求不高的任务(如短期时序预测、短文本分类);
- PyTorch实现:手动分割序列,或使用
torch.nn.utils.rnn.pack_padded_sequence压缩padding,提升计算效率。
-
🔧 模型优化:
- 使用GRU替代LSTM(参数更少,计算更快);
- 使用轻量级循环模型(如Fastformers、Linear Attention RNN),用线性复杂度替代传统RNN的平方复杂度;
- 批量处理优化:增大批量大小(需配合梯度累积),或使用混合精度训练(AMP)减少内存占用。
-
📊 序列长度筛选:
- 统计序列长度分布,设置合理的最大序列长度(如保留95%样本的长度,截断过长序列);
- 对文本数据,过滤极端长文本(如超过500个单词的评论),或进行分句处理。
1.7.4 模型收敛慢(训练多轮后损失仍高)
循环模型的收敛速度通常慢于MLP和CNN,尤其是在长序列或复杂任务中。
解决方案
-
💡 优化优化器与学习率:
- 优先使用Adam优化器(学习率0.001),收敛速度快于SGD;
- 使用学习率调度器(如ReduceLROnPlateau),当测试损失停滞时衰减学习率(如衰减为原来的0.5);
- 初始学习率不宜过大(如超过0.005),否则会导致训练震荡不收敛。
-
📝 数据预处理优化:
- 文本数据:使用预训练词嵌入(如GloVe、Word2Vec)初始化嵌入层,加速语义特征学习;
- 时序数据:标准化/归一化必须基于训练集,避免数据泄露;对周期性时序数据,添加时间特征(如月份、星期)。
-
🛠️ 模型结构调整:
- 增加隐藏层维度(如从128增至256),提升模型表达能力;
- 增加循环层数量(如从1层增至2层),增强时序依赖建模能力;
- 在全连接层前添加BatchNorm1d层,标准化隐藏状态,加速收敛。
1.8 本章总结与实战作业
1.8.1 核心知识点总结
- 序列数据建模:RNN的核心价值是建模时序依赖,通过隐藏状态传递历史信息,参数共享适应变长序列;
- 门控机制:LSTM通过遗忘门、输入门、输出门和细胞状态解决长距离依赖,GRU简化门控结构,兼顾性能与效率;
- 数据预处理:文本数据需经过分词、词汇表构建、编码、填充,时序数据需标准化、构建监督样本;
- 实战技能:用PyTorch搭建RNN/GRU/LSTM模型解决文本分类、时序预测任务,掌握梯度裁剪、Dropout、早停等优化技巧;
- 问题解决:梯度消失/爆炸(梯度裁剪、门控模型)、过拟合(Dropout、L2正则)、长序列效率低(截断BPTT、轻量级模型)。
1.8.2 实战作业
📝 作业任务:基于“股票价格数据集”(包含日期、开盘价、收盘价、最高价、最低价、成交量),完成股票收盘价的多步预测(用前10天的股票数据预测未来3天的收盘价)。
- 数据预处理:
- 清洗数据(处理缺失值、异常值);
- 选择特征(如收盘价、成交量、最高价、最低价),进行标准化;
- 构建监督样本:用前10天的多特征数据预测未来3天的收盘价(输入shape: (batch, 10, 4),输出shape: (batch, 3))。
- 模型搭建:
- 搭建GRU和LSTM两个多步预测模型(输出层维度设为3);
- 模型结构:嵌入层(可选)→ 循环层(2层,隐藏维度256)→ Dropout层 → 全连接层(输出3个数值)。
- 模型训练与优化:
- 使用MSE损失函数,Adam优化器,训练50轮;
- 启用梯度裁剪(max_norm=1.0)和早停机制(patience=10);
- 对比GRU和LSTM模型的测试集RMSE。
- 结果可视化与分析:
- 绘制测试集上的真实收盘价与预测收盘价曲线;
- 分析模型在不同市场行情(上涨、下跌、震荡)下的预测精度差异;
- 提出至少2点模型改进方向(如增加特征、调整模型结构、使用注意力机制)。
📊 提交要求:包含完整Python代码、数据预处理流程说明、训练日志(损失曲线、RMSE变化)、预测结果可视化图、模型对比分析报告。
3557

被折叠的 条评论
为什么被折叠?



