简介:面向计算机相关专业学生和毕业设计开发者,资源以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_loader:ETTh1 是怎么被切成样本的
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, flag='train', size=None, features='S', data_path='ETTh1.csv', target='OT', scale=True, timeenc=0, freq='h'): # 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)参数解释一下:features='S'表示单变量预测,只用 OT 那一列;如果想做多变量预测,改成M就会把所有特征都喂进去。timeenc=0表示不用额外时间编码,timeenc=1时会调用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_model=64, num_layers=2, dropout=0.1): super(LSTM, self).__init__() self.seq_len = seq_len self.pred_len = pred_len # enc_in 是输入特征数,比如单变量就是 1 self.lstm = nn.LSTM( input_size=enc_in, hidden_size=d_model, num_layers=num_layers, batch_first=True, dropout=dropout ) 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_model=512, n_heads=8, e_layers=2, d_layers=1, dropout=0.0, attn='prob'): 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 或 720,prob的优势就出来了。
这个模型有几个玄学参数:d_model太大容易过拟合,n_heads必须能被d_model整除,dropout在 Transformer 里建议设 0.05~0.2。原项目里d_model=512是论文常用值,但你用 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_in=1, individual=False): 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(flag='train') vali_loader = self._get_data(flag='val') test_loader = self._get_data(flag='test') model = self._build_model().to(self.device) optimizer = torch.optim.Adam(model.parameters(), lr=self.args.learning_rate) scheduler = torch.optim.lr_scheduler.LambdaLR( optimizer, lr_lambda=lambda 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是时间戳特征,timeenc=1时才会有,否则就是空张量。很多新手会问“这个 mark 有什么用”,实际上它帮模型感知周期位置。ETTh1 有很强的日周期和季节周期,喂入 hour of day 和 day of week 后,Transformer 的注意力可以学到“每天同一时刻的相关性”。如果timeenc=0,模型只能从数值本身猜周期,效果会差不少。
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 现象:测试集精度虚高,但真实场景完全没法用
原因:我在改代码时发现,如果把scale=True放在划分 train/test 之前,scaler 是在全量数据上 fit 的,测试集的均值方差就被透露给模型了。虽然测试输入经过了标准化,但模型的统计特性是在包含测试数据的情况下学到的,这就叫数据泄漏。
解决:必须把数据先按比例切出 train/val/test,再用训练集部分 fit scaler,然后 transform 全部。源码里data_loader.py已经做对了,但如果你自己写加载逻辑,一定把fit和transform分开。
5.2 现象:Transformer 模型 batch 大小改小后直接 OOM
原因:d_model=512加上seq_len=96时,注意力矩阵是96*96还好,但如果seq_len调到 336,n_heads=8,显存就被中间激活值吃满了。ETTh1 数据量不大,没必要上这么宽的模型。
解决:我一般把d_model降到 128,n_heads降到 4,e_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(seed=42): 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_in=1, d_model=32): super(ours_CNN, self).__init__() self.conv1 = nn.Conv1d(enc_in, d_model, kernel_size=3, padding=1) self.conv2 = nn.Conv1d(d_model, d_model, kernel_size=3, padding=1) 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_dim=1) # [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_len(24/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。从那以后,我每个新项目都会先跑线性基线,再决定要不要上大模型。这个习惯帮我省下了很多无意义的调参时间,希望帮到你。
本文还有配套的精品资源,点击获取