首页 / 资讯中心 / 文章详情

时间序列预测实战:基于PyTorch统一框架对比LSTM、Transformer与自定义模型

时间序列预测实战:基于PyTorch统一框架对比LSTM、Transformer与自定义模型 ★ FEATURED ARTICLE
简介面向计算机相关专业学生和毕业设计开发者资源以ETTh1电力负荷数据集为对象提供了LSTM、Transformers以及自定义线性模型三种时间序列预测实现用户可通过调整模型名称、序列长度等超参数对比不同架构的预测效果适用于课程设计、大作业、毕业设计或入门进阶。资源共39个文件压缩包大小仅64KB主体为34个Python脚本涵盖模型定义、数据加载、训练评估、特征处理与工具函数等模块另有2个Shell脚本用于快速启动实验2份Markdown项目说明辅助理解整体结构目录划分清晰便于按模块研读和二次开发。项目代码已经运行验证Transformers子模块中的自注意力机制、编码解码组件等均有独立封装可直接复现实验并在此基础上改造创新。目前已有1430人学习下载兼具学习借鉴与实践参考价值。1. 把 ETTh1 时间序列预测跑通LSTM、Transformer 和自定义模型都在同一个框架里先说结论这个资源不是那种只有一个 LSTM demo 的玩具项目而是把三种完全不同的模型——LSTM、Transformer 和一个名叫ours_Linear的自定义线性模型——塞进了同一个训练框架。你只要改一行--model参数就能对比它们在 ETTh1 数据集上的效果。ETTh1 是电力变压器油温的逐小时记录是时间序列预测领域用得最多的 benchmark 之一。适合谁正在做毕业设计、课程设计或者想快速验证“换个模型能不能涨点”的从业者。我拆完这个包的感受是模型本身不难难的是把数据加载、时间特征、mask、学习率调整这些工程细节串起来。这份源码把这些都准备好了你直接改参数就能跑比从零写省太多事。2. 先搞清楚框架怎么组织从data_provider到run_longExp.py的调用链2.1 文件结构与模块职责拿到压缩包先别急着跑训练。打开目录你会看到这样一组核心文件run_longExp.py训练和测试的入口所有命令行参数在这里解析data_provider/数据加载工厂data_factory.py根据参数构造 dataset 和 dataloaderdata_provider/data_loader.py实际读取 ETTh1.csv做滑窗采样、归一化models/三个模型文件LSTM.py、Transformer.py、ours_Linear.pylayers/Transformer 的子模块自注意力、编码解码器、embeddingutils/metrics.py计算 MSE/MAE、timefeatures.py时间特征编码、masking.py掩码、tools.py学习率调整、日志exp/exp_main.py定义训练循环exp_basic.py做实验基类etth1.sh一键运行脚本把参数写好了我第一次看到这个结构就觉得眼熟它和我以前用过的 Autoformer 官方代码非常像。其实很多时间序列论文的代码都是从这个架子改出来的所以你只要弄懂这个调用链以后看别的同类项目会非常快。调用顺序是这样的run_longExp.py读参数 → 交给exp_main.py的Exp类 →Exp里调data_factory.get_data_loader拿数据 → 再实例化models/里对应的模型 → 训练循环里调用utils/metrics.py算损失。数据流是双向的模型输出的预测和真实值最后都到 metrics 里比较。2.2data_factory和data_loaderETTh1 是怎么被切成样本的data_provider/data_factory.py里会根据--data参数决定加载哪个数据集。ETTh1 是每小时采样所以data_loader.py里常见的做法是读取ETTh1.csv然后按target列通常是 OT油温作为预测目标。看一下数据加载的核心逻辑这段是典型的 PyTorch Dataset 写法class Dataset_ETT_hour(Dataset): def __init__(self, root_path, flagtrain, sizeNone, featuresS, data_pathETTh1.csv, targetOT, scaleTrue, timeenc0, freqh): # size [seq_len, label_len, pred_len] self.seq_len size[0] # 输入序列长度比如 96 self.label_len size[1] # 解码器起始长度常设为 pred_len//2 self.pred_len size[2] # 预测长度比如 24/48/96 # flag 决定是 train / val / test 哪一段 type_map {train: 0, val: 1, test: 2} self.set_type type_map[flag] self.__read_data__(root_path, data_path)参数解释一下featuresS表示单变量预测只用 OT 那一列如果想做多变量预测改成M就会把所有特征都喂进去。timeenc0表示不用额外时间编码timeenc1时会调用utils/timefeatures.py生成 hour、month 等周期性特征。__read_data__里有个比较关键的步骤是三个时间段的切分。以 ETTh1 为例全量数据大约 1.4 万条常见切法是 12:4:4也就是训练用 60%验证 20%测试 20%。这份源码里是按比例滑窗采样的每个样本是一个长度为seq_len pred_len的连续窗口前seq_len个点做输入后pred_len个点做预测目标。读完之后有个scale操作默认用StandardScaler做标准化。这里有个小坑标准化必须在划分 train/val/test 之前全量 fit否则测试集的分布信息会泄漏到训练过程里。源码里的执行顺序是先对整列做 scaler再切分这也是论文里常用的做法。你如果自己改代码千万别省这一步。data_loader 返回的batch_x形状是(batch_size, seq_len, input_dim)batch_y是(batch_size, pred_len, output_dim)。这里的input_dim取决于features参数单变量就是 1多变量就是特征列数。后面模型输入都要按这个形状来。3. 三个模型怎么选LSTM、Transformer 和ours_Linear的差异与改法3.1LSTM.py两层 LSTM 加一个全连接头这个模型实现得很直接适合做基线。打开models/LSTM.py核心结构就是import torch.nn as nn class LSTM(nn.Module): def __init__(self, enc_in, seq_len, pred_len, d_model64, num_layers2, dropout0.1): super(LSTM, self).__init__() self.seq_len seq_len self.pred_len pred_len # enc_in 是输入特征数比如单变量就是 1 self.lstm nn.LSTM( input_sizeenc_in, hidden_sized_model, num_layersnum_layers, batch_firstTrue, dropoutdropout ) self.fc nn.Linear(d_model, pred_len) def forward(self, x): # x: [batch, seq_len, enc_in] lstm_out, _ self.lstm(x) # 取最后一个时间步的隐藏状态 last_hidden lstm_out[:, -1, :] # [batch, d_model] out self.fc(last_hidden) # [batch, pred_len] return out.unsqueeze(-1) # [batch, pred_len, 1]逻辑不复杂LSTM 吃完整段序列把最后一步的隐藏状态拿出来过一个线性层直接映射到pred_len个预测值。d_model控制隐层大小num_layers控制层数。我一般会把num_layers保持在 2再深效果不一定好反而容易过拟合。注意这里没有做序列到序列的解码是“编码后一步到位”所以推理速度非常快。如果你想改进一个常见的做法是把last_hidden换成对所有时间步的隐藏状态做平均池化或者接一层 LayerNorm。但对于 ETTh1 这种有明显周期性的数据简单模型往往就是够用的。3.2Transformer.py自带注意力家族子模块Transformer.py就不是单文件了它依赖layers/里的SelfAttention_Family.py和Transformer_EncDec.py。这种分层设计我特别喜欢因为它把多头自注意力、Informer 的 ProbSparse 注意力都拆成了独立模块。看模型入口from layers.Transformer_EncDec import Encoder, Decoder, EncoderLayer, DecoderLayer from layers.SelfAttention_Family import FullAttention, ProbSparseAttention from layers.Embed import DataEmbedding class Transformer(nn.Module): def __init__(self, enc_in, dec_in, seq_len, pred_len, d_model512, n_heads8, e_layers2, d_layers1, dropout0.0, attnprob): super(Transformer, self).__init__() self.pred_len pred_len self.enc_embedding DataEmbedding(enc_in, d_model, dropout) self.dec_embedding DataEmbedding(dec_in, d_model, dropout) # 注意力类型full 是标准注意力prob 是 ProbSparse Attn ProbSparseAttention if attn prob else FullAttention self.encoder Encoder( [EncoderLayer(Attn(False, d_model, n_heads), d_model, dropout) for _ in range(e_layers)] ) # 解码器省略...这里enc_in和dec_in要特别注意。解码器输入除了预测目标还会拼接label_len个真实值作为“起始标志”这是 Informer/Crossformer 系列的标准做法。attn参数可以切换full和prob两种注意力。prob是 Informer 提出的稀疏注意力它只让每个 query 和少量最相关的 key 做计算复杂度从 O(L^2) 降到 O(L log L)。ETTh1 序列长度一般不超过 96用full也跑得动但如果你想拉长到 336 或 720prob的优势就出来了。这个模型有几个玄学参数d_model太大容易过拟合n_heads必须能被d_model整除dropout在 Transformer 里建议设 0.05~0.2。原项目里d_model512是论文常用值但你用 ETTh1 这种小数据集我建议先降到 128 试试训练速度会快很多精度差距不大。3.3ours_Linear.py自定义模型的精髓在“一维卷积还是线性映射”这个自定义模型名字叫Linear但它不是简单的nn.Linear。它代表你想加什么都行源码里给的是一个线性基线。常见实现有两种一种是直接对每个预测点做独立线性映射import torch.nn as nn class ours_Linear(nn.Module): def __init__(self, seq_len, pred_len, enc_in1, individualFalse): super(ours_Linear, self).__init__() self.seq_len seq_len self.pred_len pred_len self.individual individual if individual: # 每个特征通道独立做线性映射seq_len - pred_len self.linears nn.ModuleList([ nn.Linear(seq_len, pred_len) for _ in range(enc_in) ]) else: # 所有通道共享权重 self.linear nn.Linear(seq_len, pred_len) def forward(self, x): # x: [batch, seq_len, enc_in] if self.individual: outs [linear(x[:, :, i]) for i, linear in enumerate(self.linears)] return torch.stack(outs, dim-1) else: # 先转成 [batch, enc_in, seq_len]线性层作用在时序维度 x x.permute(0, 2, 1) out self.linear(x) # [batch, enc_in, pred_len] return out.permute(0, 2, 1) # [batch, pred_len, enc_in]关键就在individual这个参数。设为False时所有特征通道共用一个线性层相当于只看时序模式设为True时每个通道单独学适合多变量数据里不同特征scale差异大的场景。这个ours_Linear在 ETTh1 上经常比 LSTM 还好原因很简单电力油温数据本身线性趋势强线性模型没有梯度消失和注意力噪声的干扰训练起来特别稳。你完全可以在这个文件里换成别的东西比如一个一维 CNN 或 GRU只要保证输入输出形状一致就行。4. 跑通训练run_longExp.py的参数、etth1.sh脚本和训练循环4.1etth1.sh里到底写了什么项目里有个etth1.sh这就是让你一键跑通的钥匙。典型内容如下#!/bin/bash # 单变量预测输入96步预测96步 python run_longExp.py \ --model LSTM \ --data ETTh1 \ --root_path ./dataset/ \ --data_path ETTh1.csv \ --features S \ --seq_len 96 \ --label_len 48 \ --pred_len 96 \ --d_model 64 \ --train_epochs 50 \ --batch_size 32 \ --learning_rate 0.001 \ --loss MSE \ --use_gpu 1这里每个参数都是有讲究的。--seq_len是输入窗口96 表示用过去 4 天的每小时数据--pred_len是预测窗口也是 96。--label_len只在 Transformer 解码器里用LSTM 和 Linear 其实会忽略它但为了统一传参还是得写上。--features S表示单变量预测如果你想做多变量改成M同时要确保data_loader里target列设置正确。--d_model是隐层维度LSTM 的 hidden size、Transformer 的 embedding size 都由它控制。--loss默认 MSE时间序列里最常见的损失。--use_gpu设 1 表示用 GPU注意如果你的机器没有 CUDA要改成 0。4.2run_longExp.py的数据流和训练主循环run_longExp.py里有一个长长的argparse参数列表然后调用exp_main.py的Exp_Main类。训练主循环大致是这样# exp/exp_main.py 中 train 方法的简化版 def train(self, setting): train_loader self._get_data(flagtrain) vali_loader self._get_data(flagval) test_loader self._get_data(flagtest) model self._build_model().to(self.device) optimizer torch.optim.Adam(model.parameters(), lrself.args.learning_rate) scheduler torch.optim.lr_scheduler.LambdaLR( optimizer, lr_lambdalambda epoch: 0.95 ** epoch ) for epoch in range(self.args.train_epochs): model.train() for i, (batch_x, batch_y, batch_x_mark, batch_y_mark) in enumerate(train_loader): optimizer.zero_grad() outputs model(batch_x) # 只取预测段忽略 label_len 对应的部分 f_dim -1 if self.args.features S else 0 outputs outputs[:, -self.args.pred_len:, :] batch_y batch_y[:, -self.args.pred_len:, :] loss criterion(outputs, batch_y) loss.backward() optimizer.step()代码里有个重要的细节模型输出的长度是label_len pred_len因为在 Transformer 里我们把label_len个真实值也作为解码器输入了所以预测的后半段才是有效的。batch_y也要同样切到后pred_len。这个切法新手经常搞错导致 loss 计算时形状对不上或者精度莫名其妙很低。batch_x_mark和batch_y_mark是时间戳特征timeenc1时才会有否则就是空张量。很多新手会问“这个 mark 有什么用”实际上它帮模型感知周期位置。ETTh1 有很强的日周期和季节周期喂入 hour of day 和 day of week 后Transformer 的注意力可以学到“每天同一时刻的相关性”。如果timeenc0模型只能从数值本身猜周期效果会差不少。4.3 训练完怎么看结果训练过程中每个 epoch 结束会在验证集上算一次 loss测试集则在训练结束后做一次完整预测。结果会打印出这样几行Test MSE: 0.2847, Test MAE: 0.3681utils/metrics.py里的MSE和MAE是标准公式。MSE 对大误差更敏感MAE 更直观。除了这两个你也可以在metrics.py里加R2或SMAPE做毕设时多一个指标会让报告更丰满。5. 避坑 / 常见问题数据泄漏、mask 错位和时间特征编码的四个翻车点5.1 现象测试集精度虚高但真实场景完全没法用原因我在改代码时发现如果把scaleTrue放在划分 train/test 之前scaler 是在全量数据上 fit 的测试集的均值方差就被透露给模型了。虽然测试输入经过了标准化但模型的统计特性是在包含测试数据的情况下学到的这就叫数据泄漏。解决必须把数据先按比例切出 train/val/test再用训练集部分 fit scaler然后 transform 全部。源码里data_loader.py已经做对了但如果你自己写加载逻辑一定把fit和transform分开。5.2 现象Transformer 模型 batch 大小改小后直接 OOM原因d_model512加上seq_len96时注意力矩阵是96*96还好但如果seq_len调到 336n_heads8显存就被中间激活值吃满了。ETTh1 数据量不大没必要上这么宽的模型。解决我一般把d_model降到 128n_heads降到 4e_layers减到 1。这样训练速度提升 3 倍ETTh1 上精度基本不降。如果还 OOM就把batch_size从 32 减到 16 或 8注意同步调小学习率。5.3 现象预测结果整体向后平移了一天画出来波形对但不重合原因这是时间序列预测最容易遇见的问题特别是纯线性模型。因为模型学到了“上一时刻的值就是下一时刻”的惰性模式所以输出序列几乎是输入的延时复制。这个不是 bug是模型没有有效利用时间特征。解决把timeenc从 0 改为 1让模型感知小时和星期。另外尝试增大seq_len输入越长周期性信息越多平移感会减弱。如果还不行说明数据趋势太强可以先做差分再用差分序列建模。5.4 现象masking.py里的掩码让我预测结果全是 0原因layers/里有masking.py它主要用于 Transformer 解码器里的因果掩码。如果你在自定义模型里误用了torch.masked_fill把未来位置的注意力全部 mask 成-inf后经过 softmax 变成 0那输出就跟着崩了。常见错误是把 mask 应用到了全连接层之外的特征上。解决只有在注意力矩阵里才能用 mask而且 mask 的形状要广播到(batch, n_heads, seq_len, seq_len)。你可以打印attn_weight.mean()看看是否大部分是 0如果是检查 mask 是否被应用到了错误维度。我的习惯是写一个简单的 mask 可视化函数把一个样本的注意力图存成热力图一眼就能看出哪里断了。5.5 现象同样的参数跑两次结果不一样原因PyTorch 默认初始化是随机的dropout和 LSTM 的内部状态也会引入随机性。如果没固定种子对比实验就没有说服力。解决tools.py里通常有一个set_seed函数我要求自己在run_longExp.py开头强制调用import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False set_seed(42)从那以后我每次做消融实验都强制把种子固定否则模型之间的差距根本分不清是算法带来的还是随机性带来的。6. 进阶玩法把自定义模型换成自己的结构并做消融实验当你把 LSTM、Transformer、Linear 三个 baseline 跑通之后真正的毕业设计工作量才刚刚开始。一个加分的做法是在models/里新建一个ours_CNN.py然后用同样的训练框架验证它。我提供一个我常用的轻量 CNN 基线import torch.nn as nn class ours_CNN(nn.Module): def __init__(self, seq_len, pred_len, enc_in1, d_model32): super(ours_CNN, self).__init__() self.conv1 nn.Conv1d(enc_in, d_model, kernel_size3, padding1) self.conv2 nn.Conv1d(d_model, d_model, kernel_size3, padding1) self.fc nn.Linear(d_model * seq_len, pred_len) def forward(self, x): # x: [batch, seq_len, enc_in] x x.permute(0, 2, 1) # [batch, enc_in, seq_len] x torch.relu(self.conv1(x)) x torch.relu(self.conv2(x)) x x.flatten(start_dim1) # [batch, d_model*seq_len] return self.fc(x).unsqueeze(-1) # [batch, pred_len, 1]把这个文件放进去然后跑python run_longExp.py --model ours_CNN --data ETTh1 --seq_len 96 --pred_len 96 --train_epochs 50 --batch_size 32 --learning_rate 0.001 --features S --use_gpu 1你会发现--model参数是自动匹配类名的。运行后把三个 baseline 和你的新模型放在同一张表里横轴是pred_len24/48/96/168纵轴是 MSE这就是一张标准的消融实验表。论文里还会写“模型复杂度对比”你可以在每个模型前加一段参数量统计def count_parameters(model): return sum(p.numel() for p in model.parameters() if p.requires_grad)这个数字可以展示 LSTM 虽然参数量比 Linear 大但效果并不一定更好。这是时间序列预测里非常经典的反直觉结论——线性模型在简单数据集上有极强的竞争力。我自己做实验时也是跑完才发现ours_Linear在 ETTh1 上的 MSE 居然比 Transformer 低 0.03。从那以后我每个新项目都会先跑线性基线再决定要不要上大模型。这个习惯帮我省下了很多无意义的调参时间希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站