
简介面向计算机相关专业学生与时间序列预测开发者的毕业设计项目基于公开的ETTh1数据集分别使用LSTM、Transformer及自定义线性模型构建时间序列预测系统支持通过调整模型名称、序列长度等超参数优化性能技术栈为Python。压缩包共39个文件包括34个Python脚本、2个Shell脚本、2个Markdown文档及1个内置zip包其中Python脚本为核心覆盖模型定义、数据加载、特征处理、评估指标、训练与实验等模块Shell脚本用于快速启动实验Markdown提供项目说明整体仅64KB。已有1429人学习下载尤其适合作为毕业设计、课程设计或入门进阶的实战样例。项目不仅包含可直接运行的完整源码还提供Transformer子模块辅助实现、实验配置脚本和说明文档可帮助理解不同序列模型在ETTh1上的特征提取与预测表现差异方便在此基础上进行二次开发与算法改进。1. ETTh1 时间序列预测这份源码包能跑出什么如果你正在做毕业设计或者课程设计手头需要一份能直接跑起来的时间序列预测代码那 ETTh1 数据集搭配 LSTM、Transformer、自定义线性模型三套实现应该是比较省事的组合。ETTh1 是每小时采样的电力变压器油温数据7 个特征列在时序预测论文里属于基准数据集。这份资源把入口脚本 run_longExp.py、模型文件、数据加载模块、实验管理模块都分好了你只需要改模型名称和序列长度这类超参数就能切换算法。适合想快速拿到对比结果的人也适合打算在已有代码上改模型做消融实验的从业者本文按复现顺序把每个文件的作用讲清楚。2. 三套模型怎么选LSTM、Transformer 与自定义线性模型的取舍2.1 ETTh1 数据特性与选型逻辑时间序列预测的第一步不是写模型而是看清数据。ETTh1 全称是 Electricity Transformer Temperature每小时一条记录字段包括油温 OT 和 6 个外部电力负荷特征共 7 列。预测任务通常是给定过去一段窗口比如 96 小时预测未来一段窗口96/192/336/720 小时。这类数据有明显的日周期性、周周期性但不像股票那样噪声占主导因此模型能不能学到周期模式直接决定预测误差。选型逻辑上LSTM 是循环网络按时间步顺序读取适合捕捉短到中期的时序依赖但长序列上存在梯度传播效率问题Transformer 靠自注意力直接建立任意两个时间步的联系长序列建模能力强但训练更吃显存和调参自定义线性模型大多是把输入窗口展平后做线性映射结构简单、训练快往往能作为最朴素的基线。一份源码里同时给这三种模型本质是让你在同一个数据加载和评估框架下公平对比而不是各写一套评测逻辑。2.2 LSTM.py 的核心搭法models 目录下的 LSTM.py 是标准的 PyTorch 实现表面看只是调用了 nn.LSTM但要注意它这个结构式是「LSTM 编码 全连接投影」。也就是说输入形状是 [batch, seq_len, enc_in]经过 LSTM 层后取每个时间步的隐藏状态再拍平成一个长向量最后通过一个 Linear 层映射到 [batch, pred_len * c_out]。# models/LSTM.py 结构示意 import torch.nn as nn class LSTM(nn.Module): def __init__(self, enc_in7, seq_len96, pred_len96, d_model16, num_layers2): super().__init__() self.lstm nn.LSTM( input_sizeenc_in, # 输入特征数ETTh1 是 7 hidden_sized_model, # 隐藏单元数源码里常叫 d_model num_layersnum_layers, # 层数2 层左右够用 batch_firstTrue # 输入维度按 [batch, time, feature] ) self.proj nn.Linear(d_model * seq_len, pred_len * enc_in) def forward(self, x): out, _ self.lstm(x) # [batch, seq_len, d_model] out out.reshape(out.size(0), -1) # 展平所有时间步 return self.proj(out) # [batch, pred_len * enc_in]这里有个关键点d_model 在 LSTM 里就是 hidden_size它决定模型的记忆容量。d_model 太小模型学不到负荷突变太大训练变慢且容易过拟合在 ETTh1 这种 7 特征数据上16 到 64 是比较常见的选择。num_layers2 时LSTM 堆叠两层能建模更高层级的依赖但层数再往上收益有限反而训练时间翻倍。最后的 proj 层直接把整个历史窗口压成预测结果没有做逐时间步的解码这是简化做法适合做对比实验但不适合做滚动多步预测。2.3 Transformer.py 与自注意力模块的分工Transformer.py 的实现比 LSTM 复杂它依赖 layers 目录下的三个文件Embed.py 负责把原始序列转成嵌入向量并加入位置信息SelfAttention_Family.py 封装多头自注意力Transformer_EncDec.py 提供编码器和解码器的基础结构。实际训练时输入先经过 Embed.py 得到 d_model 维度的向量再进入多头注意力层最后通过解码器输出预测值。多头注意力里最有价值的是掩码机制。训练时预测未来值不能让模型看到未来信息所以 SelfAttention_Family.py 里会有 mask 参数控制注意力可见范围。如果直接把整个序列送进注意力不遮住未来时刻模型会「抄答案」训练 loss 很低但验证集效果崩掉。源码里 masking.py 就是干这个的它生成上三角掩码矩阵把未来位置的注意力权重置为负无穷经过 softmax 后变成 0。这个文件平时不起眼但删了或者用错模型就废了。Transformer 在 ETTh1 上的优势在长序列比如 seq_len336 甚至 720 时LSTM 已经很难记住早期信息Transformer 可以靠注意力直接跳到任意时间步。代价是显存占用随序列长度平方增长我一般先跑一遍短序列确认代码没跑通问题再拉长序列避免上来就 OOM。2.4 ours_Linear.py自定义模型的设计边界ours_Linear.py 是这份源码里最值得研究的文件因为它篇幅短、改动自由度高。常见的自定义线性模型有两种做法一种是直接把序列展平过一两个线性层另一种是仿照 DLinear先对序列做趋势和季节分解再分别线性映射后相加。这份源码里的自定义模型如果只是简单线性层那它的定位就是「底线模型」用来证明 LSTM 和 Transformer 至少不能比一个线性映射差。# models/ours_Linear.py 结构示意以展平线性层为例 import torch.nn as nn class ours_Linear(nn.Module): def __init__(self, enc_in7, seq_len96, pred_len96): super().__init__() self.flatten nn.Flatten() self.linear nn.Linear(seq_len * enc_in, pred_len * enc_in) def forward(self, x): # x: [batch, seq_len, enc_in] x self.flatten(x) # [batch, seq_len * enc_in] return self.linear(x) # [batch, pred_len * enc_in]自定义模型的边界在于它没有时序建模能力输入窗口内的时间顺序对它来说只是特征位置。如果 ETTh1 序列的周期性强线性模型也能拿到不算差的 MSE因为它能学到「过去 96 小时均值 ≈ 未来一段时间均值」这种统计规律。用它做基线可以判断复杂模型到底是在学真实规律还是过拟合噪声。想做消融实验就从改这个文件开始加一层激活函数或者分解模块对比前后指标变化。3. 完整复现流程从 etth1.sh 到 run_longExp.py 的每个参数3.1 文件结构分工解压后先别急着跑把目录结构看清楚。run_longExp.py 是总入口所有参数从这里进models 目录放三个模型文件data_provider 里的 data_loader.py 读 CSV 并切分窗口data_factory.py 是个工厂函数根据 --data 参数返回对应的数据集类exp 目录里 exp_main.py 是训练和测试逻辑exp_basic.py 定义实验基类utils 目录有四个小工具metrics.py 计算 MSE 和 MAEmasking.py 生成掩码timefeatures.py 把时间戳转成特征tools.py 提供学习率调度和早停等辅助函数。这个划分是时序预测项目里比较典型的套路入口、数据处理、模型、实验调度分层清晰。如果只看一个文件先看 run_longExp.py。它用 argparse 定义了所有超参数包括模型名、数据路径、序列长度、预测长度、学习率、早停耐心值等。实验管理器 exp_main.py 接收这些参数实例化模型循环训练轮次每轮结束在验证集上算 loss满足早停条件就停下来最后跑测试集并打印指标。3.2 etth1.sh一条脚本拉起训练etth1.sh 是 shell 脚本里面通常是一组训练命令每一行对应一次完整实验。拿到后先打开文件看它会把多组参数串起来方便你一键复现论文里所有表格数据。# etth1.sh 常见组织方式 #!/bin/bash export CUDA_VISIBLE_DEVICES0 # LSTM 基线输入 96 小时预测 96 小时 python -u run_longExp.py \ --is_training 1 \ --root_path ./dataset/ \ --data_path ETTh1.csv \ --model_id ETTh1_LSTM_96_96 \ --model LSTM \ --data ETTh1 \ --features M \ --seq_len 96 \ --label_len 48 \ --pred_len 96 \ --enc_in 7 \ --dec_in 7 \ --c_out 7 \ --d_model 16 \ --lr 0.0001 \ --batch_size 32 \ --train_epochs 10 \ --patience 3 # Transformer 对比同一数据、同一预测长度 python -u run_longExp.py \ --is_training 1 \ --root_path ./dataset/ \ --data_path ETTh1.csv \ --model_id ETTh1_Transformer_96_96 \ --model Transformer \ --data ETTh1 \ --features M \ --seq_len 96 \ --label_len 48 \ --pred_len 96 \ --enc_in 7 \ --dec_in 7 \ --c_out 7 \ --d_model 16 \ --lr 0.0001 \ --batch_size 32 \ --train_epochs 10 \ --patience 3每个参数的含义要理解再改。CUDA_VISIBLE_DEVICES 指定用哪块 GPU--model 决定加载 models 目录下的哪个类值是类名改错了直接报模块找不到--seq_len 是输入窗口长度--pred_len 是输出窗口长度--label_len 是 Transformer 解码器里已知标签部分的长度LSTM 和线性模型用不到这个参数但脚本里保留着是为了统一次命令行--features M 表示多变量输入多变量输出改成 S 就是只用油温单变量跑enc_in、dec_in、c_out 在 ETTh1 数据集上都是 7。第一次跑建议只改 --model 和 --model_id其他参数保持原样确保代码能通。3.3 run_longExp.py 主流程与数据工厂跑脚本时执行顺序大概是这样的argparse 收集所有参数然后 data_factory.py 根据 --data ETTh1 实例化数据集返回训练、验证、测试三个 DataLoader。每个样本的构造方式是在原始 CSV 上滑动窗口取连续 seq_len 个点做输入后面连续 pred_len 个点做标签。窗口滑动步长为 1所以样本数比原始行数少一个窗口长度。# 主流程逻辑示意简化版 import argparse from data_provider.data_factory import data_provider from exp.exp_main import Exp_Main args argparse.ArgumentParser().parse_args() exp Exp_Main(args) # 实验管理器 if args.is_training: for epoch in range(args.train_epochs): train_loss exp.train_one_epoch() # 训练一个 epoch valid_loss exp.validate() # 验证集评估 if valid_loss best_loss: best_loss valid_loss exp.save_model() # 保存最优权重 else: patience - 1 if patience 0: break # 早停 else: exp.test() # 单独跑测试集exp_main.py 里的训练循环是核心它每个 batch 从 DataLoader 取 [batch, seq_len, enc_in] 的输入和对应标签前向得到预测与真实值算 MSE loss反向传播更新参数。验证集在每个 epoch 结束后跑一次目的是监测过拟合和触发早停。保存的模型权重默认放在 checkpoints 目录文件名包含 model_id这样不同实验之间不会互相覆盖。3.4 替换模型跑对比改哪里、不动哪里跑对比实验时大多数初学者会犯同一个错误改完 --model 参数发现报错说没有这个属性。原因是 run_longExp.py 里加载模型的方式是硬编码的字典映射比如model_dict {LSTM: LSTM, Transformer: Transformer, ours_Linear: ours_Linear}新增模型必须手动注册。改动点只有两个先在 models 目录下新建模型文件并保证类名和文件名一致然后在 run_longExp.py 的模型字典里加一行。数据加载、实验管理、评估指标这些都不用动因为它们只依赖 --data、--seq_len、--pred_len 这些通用参数不关心模型内部结构。这个设计的好处是你加第十个模型改动量仍然只有两处对比实验的公平性也有保障所有模型用同一份数据切分、同一个 loss 函数、同一个早停规则。4. 调参实战序列长度、学习率与预测长度怎么配4.1 seq_len 与 pred_len 的搭配逻辑调参不能只盯着单个参数seq_len 和 pred_len 是绑在一起的。ETTh1 是小时级数据一天 24 个点一周 168 个点。seq_len96 意味着模型看过去四天的数据这个长度够捕捉日周期但周周期只能看到一小部分seq_len336 是看足两周模型有机会学到工作日和周末的负荷差异代价是训练时间变长LSTM 的梯度传播压力也变大。比较合理的做法是固定 pred_len分别用 96、192、336 跑一轮画出测试集 MSE 随 seq_len 的变化找到拐点。pred_len 的影响更直接。pred_len96 时三个模型的差距通常不大pred_len720 时线性模型往往会快速变差Transformer 的优势会体现出来。如果你要写进毕设对比表建议至少覆盖 96/192/336 三个预测长度不要只跑最短的否则结论没有说服力。label_len 这个参数在 Transformer 里表示解码器已知的真实序列长度一般设为 pred_len 的一半或者 48它对结果影响不如 seq_len 大可以先不动。4.2 学习率、batch size 与早停策略学习率是翻车重灾区。ETTh1 这类数据量不大的场景不到两万条小时记录学习率太高容易在训练初期 loss 直接跳到 NaN太低则收敛慢10 个 epoch 根本不够。源码里默认的 0.0001 是在 Adam 优化器下的常见起点我自己的习惯是先按 0.0001 跑 5 个 epoch 看 loss 曲线如果下降太慢就提到 0.0005如果震荡就降到 0.00005。batch size 的影响相对温和32 和 64 在最终指标上差别不大但 batch size 太大时每个 epoch 的更新次数少需要同步增加 train_epochs。patience 是早停的耐心值含义是连续几个 epoch 验证集 loss 不降低就停止设为 3 比较合理设太大会浪费时间设太小容易在 loss 还没平稳时被提前掐断。# 一条适合快速验证的调试命令 python -u run_longExp.py \ --is_training 1 \ --data_path ETTh1.csv \ --model LSTM \ --data ETTh1 \ --seq_len 96 \ --pred_len 96 \ --lr 0.0001 \ --batch_size 32 \ --train_epochs 5 \ --patience 3跑之前建议先确认 d_model 和相关维度是否匹配。在 exp_main.py 里实例化模型后会打印模型结构和参数量你一眼就能看到 Linear 层的输入输出维度。如果 seq_len 改了但模型里展平后的尺寸没同步改会在这里直接抛维度不匹配的异常这是好事说明模型在暴露问题而不是默默算错。4.3 用 metrics.py 解读 MSE 与 MAE评估环节不需要自己写指标函数源码里 utils/metrics.py 已经实现好了跑完测试集会在控制台打印类似MSE:0.382, MAE:0.412的结果。MSE 是均方误差对大的预测偏差敏感它的单位是目标变量的平方在 ETTh1 这种油温数据上小数点后三位的变化就有实际意义MAE 是平均绝对误差更直观直接反映平均偏差多少度。两个指标一起看如果 MSE 高但 MAE 低说明大部分预测还行但少数几个点偏差很大这种情况常见于数据里有尖峰负荷如果两个都高说明模型整体没学到规律。一定不能只看训练集 loss。训练集 MSE 降到 0.01、测试集 MSE 却 0.5这是典型的过拟合Transformer 在数据不足时尤其容易发生。判断标准是验证集和测试集指标是否接近差距超过 30% 就要考虑加正则或者减小 d_model。另外utils/metrics.py 里可能还有 RSE、CORR 等其他指标它们的计算基础都是预测值和真实值两个数组理解 MSE 之后其他指标就是形式变换。5. 避坑与常见问题排查数据加载、掩码机制与日志异常5.1 数据加载报错时间戳解析与 freq 参数对不上现象运行脚本后 DataLoader 构建阶段报错提示日期解析失败或者返回的样本数比预期少很多。原因ETTh1.csv 的第一列是时间戳格式是2016-07-01 00:00:00data_loader.py 里通常用pd.to_datetime解析并把它设为索引。如果你的数据文件是从别处复制的时间格式可能是2016/7/1或者末尾带了时区信息解析就会出问题。另一个常见原因是 timefeatures.py 需要根据 freq 参数如 h生成小时特征如果 CSV 里有缺失行或者重复时间戳构造的窗口就会错位。解决先确认 CSV 第一列格式统一改成标准 ISO 格式检查 dataset 目录下是否有缺失值常见处理是前向填充或直接删除对应行在 data_loader.py 里打印len(data)原始行数和切窗后样本数两者应该是「原始行数 - seq_len - pred_len 1」的关系对不上就检查切窗逻辑。从那以后我每次拿到新数据都先做这一项检查避免训练跑到一半才发现数据是对不齐的。5.2 Transformer 训练出现 NaN掩码与归一化双重问题现象LSTM 和线性模型正常切换到 Transformer 后第一个 epoch loss 正常第二个 epoch 直接变成 NaN。原因最常见的是两个因素叠加。第一注意力掩码没有正确生效未来时间步的信息泄漏导致梯度爆炸第二输入数据没有做归一化或者归一化方式不对Transformer 对数值范围比 LSTM 敏感得多原始油温如果是几十度的量级经过 attention 的矩阵乘法后数值很容易溢出。解决先检查 masking.py 生成的掩码矩阵打印中间层的 attention 权重确认上三角位置确实是负无穷而不是 0再检查数据预处理ETTh1 在做窗口切分后应该用训练集的均值和标准差做标准化而不是用全局或测试集的统计量。调试时可以临时把学习率降到 0.00001如果 NaN 消失说明是数值稳定性的问题如果还出现就把 d_model 调小注意力头数调小缩小数值累积范围。5.3 训练 loss 一直震荡不下降现象训练多个 epochloss 曲线上下波动完全没有收敛趋势验证集指标和随机猜测差不多。原因这个现象在学习率设置不当和随机初始化两种情况下都会出现。学习率太高参数在最优解附近反复横跳忘记设置随机种子每次运行结果差异大但不会导致单次运行不收敛。更隐蔽的原因是 tools.py 里的学习率调度器如果配置成 CosineAnnealing而训练轮次太短学习率还没来得及降到低位就结束了。解决先用一个固定小学习率 0.00005 跑三个 epoch 做冒烟测试确认 loss 能稳定下降改回来后再对比调度器的影响。源码里如果提供了 tools.py 的 adjust_learning_rate 函数通常会在每个 epoch 结束后更新学习率可以打印当前学习率确认它确实在变化。如果训练长度很短建议直接关闭调度器用恒定学习率结果更容易复现。5.4 自定义模型维度不匹配改 seq_len 后报错现象把 --seq_len 从 96 改成 336自定义模型报 Linear 层输入维度错误但 LSTM 运行正常。原因ours_Linear.py 里如果写死了nn.Linear(seq_len * enc_in, pred_len * enc_in)用的是初始化时传入的 seq_len模型 key 的--seq_len参数变了初始化时传入的值没变或者模型内部对输入形状做了硬编码。LSTM 不报错是因为它按时间步逐步处理输入长度变化不影响维度计算而线性层对维度是敏感的。解决用--seq_len参数值去初始化线性层在 forward 开头加一行assert x.size(1) self.seq_len快速暴露问题。更稳妥的做法是让模型在 forward 里动态计算展平后的维度用x.shape推导而不是依赖初始化时固定的数字。这个坑也提醒我改参数时优先改入口脚本不要改模型内部写死的数字否则不同参数组合会互相污染。5.5 复现结果不一致GPU 随机性与缓存现象同一份代码、同一个参数在 A 机器上跑 MSE 是 0.382在 B 机器上跑变成 0.395甚至在同一台机器上重复跑也有小幅波动。原因PyTorch 的 GPU 算子多数是随机的cuDNN 的自动调优机制会根据机器硬件选择不同卷积或矩阵乘法实现结果天然有波动。另外 exp_basic.py 里如果没设置随机种子初始化权重每次都不一样哪怕训练数据相同最终收敛位置也不同。解决在入口处固定所有随机源包括torch.manual_seed、numpy.random.seed并设置torch.backends.cudnn.benchmark False和torch.backends.cudnn.deterministic True这样同一台机器上结果基本一致。跨机器完全一致通常做不到但固定种子后波动一般能控制在 0.005 以内。写报告时注明使用的 PyTorch 版本和 CUDA 版本也是对比实验的必要信息。6. 进阶玩法把自定义模型改成多变量输入并验证效果这份源码里三个模型默认都是多变量输入--features M表示用全部 7 个特征预测未来的 7 个特征。但如果你想做消融实验验证油温 OT 这一个变量到底能预测到什么程度最简单的方法是改成单变量模式把--features S同时把 enc_in、dec_in、c_out 都改成 1。这个改动不需要动代码数据工厂会自动从 CSV 里抽 OT 列作为输入输出。更有价值的进阶动作是改 ours_Linear.py把它变成非线性版本用来验证线性假设是否成立。思路是保留展平线性层在中间插入一个激活层和一个瓶颈层形成一个两层的 MLPimport torch.nn as nn import torch.nn.functional as F class ours_MLP(nn.Module): def __init__(self, enc_in7, seq_len96, pred_len96, d_model64): super().__init__() self.linear1 nn.Linear(seq_len * enc_in, d_model) self.linear2 nn.Linear(d_model, pred_len * enc_in) def forward(self, x): x x.reshape(x.size(0), -1) # [batch, seq_len * enc_in] x F.relu(self.linear1(x)) return self.linear2(x)然后把 run_longExp.py 的模型字典里加一行ours_MLP: ours_MLP命令行参数--model ours_MLP就能跑。对比线性版和非线性版在 ETTh1 上的 MSE可以直观看到激活函数是否为模型带来额外增益这个结果往往比盲目堆 LSTM 层数更有说服力。做这类改动时注意保持--seq_len和--pred_len不变只改变模型内部容量否则两个变量同时变化实验结论就不纯净了。验证时我习惯把三个模型、两种特征模式的结果统一记录到一张表里横轴是 pred_len纵轴是 MSE一眼就能看出模型在不同预测长度下的退化速度。从那以后我每次跑对比实验都强制走一遍这个流程先验证数据窗口切分正确再跑基线线性模型最后才上复杂模型并记录随机种子确保每次改动只引入一个变量希望帮到你。本文还有配套的精品资源点击获取