多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

多元时间序列预测实战:基于DSTLinear与PyTorch的完整项目

多元时间序列预测实战:基于DSTLinear与PyTorch的完整项目 简介面向Python大作业场景的多元时间序列预测项目源码涵盖天气、电力、交通、汇率、ETT等多个经典实验适合需要完成课程设计或进阶练习的本科与研究生。源码经本地编译验证可运行难度适中并附有项目手册与论文PDF便于理解模型结构与复现结果。压缩包共56个文件以Python脚本30个和Shell脚本19个为主另有3张结构示意图、1份docx说明文档及1份PDF论文整体仅548KB轻量易部署。项目按models、data_provider、exp等模块划分每个main_*.py对应一种预测场景配套shell脚本可一键启动实验层次清晰。目前已有414人学习下载适合需要快速搭建多元时间序列预测框架并输出高分大作业的读者。1. 多元时间序列预测一份能直接跑通的高分大作业项目做多元时间序列预测卡住大多数人的不是模型公式而是数据怎么进、代码怎么组织、结果怎么出。这份源码把 Weather、ETT、ExchangeRate、Electricity、Traffic、ILI 共 8 个公开数据集的预测实验整条链路都串好了本地编译过、能直接运行难度适中拿来当大作业交或者当 baseline 跑都合适。它内部是一个以 DSTLinear 为核心的预测框架入口脚本、数据加载、实验管理、评估指标全部分层——新手照着 main 脚本改参数就能出结果熟手可以直接在 models 里换自己的线性结构。适合正在做课程大作业的学生也适合需要快速试基线的时间序列从业者连做量化交易策略代码的同事看到这套结构也会觉得顺手。2. 项目结构拆解从 main 脚本到模型的调用链2.1 文件清单别被二十多个文件吓到打开压缩包第一眼很容易被main_weather.py、main_ettm2.py、main_exchange_rate.py这一排脚本吓住。其实这正是这个项目最友好的地方每个数据集一个入口意味着你不需要改主程序只要进到对应脚本里就能独立跑一次实验。我一般会把文件分成四类来看入口脚本、模型、数据加载、实验管理。文件/目录职责main.py通用入口适合自己新增数据集时用main_weather.py/main_ettm2.py/main_etth2.py/main_etth1.py/main_electricity.py/main_traffic.py/main_exchange_rate.py/main_ill.py每个数据集单独入口内置该数据集的默认参数run_longExp.py批量跑长序列实验把多个配置串起来models/模型定义DSTLinear 在这里data_provider/读 CSV、归一化、切分数据集、构造滑窗 DataLoaderexp/实验类封装 train、validate、test 全流程utils/早停、学习率调整、指标计算等工具scripts/存放 .sh 训练脚本的目录DSTLinear.pdf和手册.docx模型说明和操作手册先读手册再跑代码这中间最值得先看的是手册.docx它把每个入口脚本的参数含义写得很清楚。很多人上来直接跑main.py发现找不到数据集报错其实是没按手册里的目录结构放数据。先把文件清单过一遍后面所有操作才不容易迷路。2.2 调用链main_weather.py 被按下后发生了什么以main_weather.py为例它做的事情可以概括为四步解析参数、构造实验类、训练、测试。展开来看训练阶段又会调data_provider拿到训练数据把每个 batch 喂给models里的 DSTLinear计算出 loss 后反向传播。# main_weather.py 的典型流程节选结构来自项目源码 args parser.parse_args() exp Exp_Main(args) # 实验类定义在 exp/ 下 exp.train() # 内部循环调用 trainer exp.test() # 测试集上输出 MSE / MAE逻辑说明Exp_Main是实验管理类train()里会先初始化模型和优化器然后开始 epoch 循环每个 epoch 结束用验证集算一次 loss早停机制会判断是否需要提前结束。test()则是在测试集上做一次前向把预测值和真实值拉平后算指标。参数说明args通过 argparse 从命令行或脚本里的 default 值读取。项目里每个main_*脚本都会覆盖一批默认参数比如main_weather.py把数据集路径指向data/weather/main_ettm2.py把数据频率设为 15 分钟一次。这让它支持一个数据集一个脚本的独立调试而不是把 8 个数据集参数全塞在同一个入口里。data_provider里的实现通常是这样读 CSV - 归一化 - 按时间顺序切 train/val/test - 用滑动窗口生成[输入窗口, 预测窗口]的样本对 - 封装成 DataLoader。这里有一个容易被忽略的点时间序列切分不能随机打乱必须按时间顺序否则测试集里混入未来信息指标会虚高。2.3 DSTLinear 的核心思路线性分解为什么能干过大模型DSTLinear 这个名字里的 DST对照压缩包里的DSTLinear.pdf和models/下的实现来看走的是先分解、后线性预测的路线。原始序列先经过一个滑动平均拆成趋势项和周期项两项各自过一个线性层再把结果相加得到最终预测。DST 的差异点在于它会在趋势分支里再叠加一次线性变换用来捕捉短期的局部斜率。# DSTLinear 前向逻辑的常见实现思路 def forward(self, x): # x: [B, L, N]B 是 batchL 是输入窗口N 是变量数 trend moving_average(x, kernel_size25) seasonal x - trend trend_out self.linear_trend(trend) seasonal_out self.linear_seasonal(seasonal) return trend_out seasonal_out逻辑说明移动平均把序列里的长周期趋势单独拎出来剩下的周期部分交给另一个线性层处理。这样拆开以后每个线性层的输入都更干净学起来也更容易收敛。参数说明kernel_size25是针对小时级数据比较常用的窗口ETT 这类数据一天 24 个点25 的窗口能覆盖掉一个整天的震荡如果数据是周级或者分钟级这个值要跟着改。linear_trend和linear_seasonal的输入输出维度一致都是L - pred_len。为什么要用线性模型而不是 Transformer2023 年前后的 LTSF-Linear 相关讨论给出过一个反直觉结论很多结构复杂的注意力模型在长期预测上甚至不如一层线性映射。原因在于注意力会把局部周期性噪声放大而线性模型天然做了一次平滑。DSTLinear 把趋势和周期分开处理等于给线性模型加了先验所以在 Weather、ETT 这类有明显日周期或季节周期的数据上表现非常稳定。这并不是说线性模型万能而是说在你一开始搭项目时线性基线是必须有的参照物。3. 复现第一步环境配置与数据准备3.1 环境要求Python 3.8 PyTorch 就能跑这个项目的依赖非常克制不需要额外装一堆深度学习的库。Python 安装好之后装 torch、numpy、pandas、matplotlib 四样就能跑通。我用的是 Python 3.8这个版本在 Windows 和 Linux 上都能稳定吃住项目里的代码如果你装的是更新版本的 Python大概率也没问题但遇到奇怪的类型报错时先回头确认是不是版本兼容问题。conda create -n ts python3.8 conda activate ts pip install torch2.0.1 numpy pandas matplotlib逻辑说明第一行创建虚拟环境第二行激活第三行安装依赖。torch 版本不用太纠结1.13 到 2.x 都能跑如果不确定自己的 CUDA 版本可以直接装 CPU 版因为 DSTLinear 本身是线性层CPU 上跑也不会慢到不能接受。参数说明torch2.0.1是我本地验证过的版本如果官网下载太慢可以用pip install torch --index-url https://download.pytorch.org/whl/cpu装 CPU 版。numpy 和 pandas 是数据处理必需matplotlib 是用来画预测结果对比图的如果你不打算可视化可以省掉但建议留着。装完以后验证一下python -c import torch, numpy, pandas; print(torch.__version__, numpy.__version__, pandas.__version__)能打印出版本号就说明环境没问题。这一步卡住的人不多真正卡人的是下一步数据放哪。3.2 数据集下载与目录组织项目默认从data/目录下读 CSV目录结构必须和data_provider里的路径对上否则会直接报FileNotFoundError。常见的做法是保留项目根目录在根目录下建好data/及各数据集的子目录然后把 CSV 文件放进去。mkdir -p data/weather data/ETTh1 data/ETTh2 data/ETTm1 data/ETTm2 data/exchange_rate data/electricity data/traffic data/illness逻辑说明mkdir -p会把路径上所有不存在的目录一次性建好。这 8 个目录对应 8 个入口脚本main_weather.py会去找data/weather/weather.csvmain_etth1.py会去找data/ETTh1/ETTh1.csv以此类推。参数说明如果你自己的数据文件名不叫weather.csv需要打开main_weather.py或data_provider里对应的文件路径参数改成实际文件名。我一般建议保持原名因为项目里所有路径参数都是按这套命名写的改文件名比改代码省事。数据集的常见来源是各论文作者公开的仓库比如 ETT、Weather、ExchangeRate 这些都有公开下载渠道。下载后注意文件格式ETT 系列是ETTh1.csv、ETTm1.csvExchangeRate 是exchange_rate.csvElectricity 是electricity.csv。这些 CSV 通常第一列是时间后面的列是各个变量。3.3 数据格式约定时间列、变量列、缺失值虽然都是 CSV但每个数据集的列结构差别很大这也是新手最容易翻车的地方。项目里data_provider读数据时通常会指定target_col或默认取最后一列作为预测目标比如 Weather 数据集最后一列是OT气温ETT 数据集的OT列是油温。数据集文件变量数数据频率预测目标列Weatherweather.csv2110 分钟OTETTh1 / ETTh2ETTh1.csv / ETTh2.csv7小时OTETTm1 / ETTm2ETTm1.csv / ETTm2.csv715 分钟OTExchangeRateexchange_rate.csv8日最后一列Electricityelectricity.csv321小时最后一列Traffictraffic.csv862小时最后一列ILIillness.csv7周最后一列时间列通常叫date或日期data_provider里会用它来切分数据集和防止未来信息泄露。如果 CSV 里没有时间列很多实现会直接把行号当成时间这会严重影响序列顺序建议先补上时间列。缺失值处理上项目里比较常见的做法是dropna()或者按前值填充。如果你换用自己的数据建议先做一步数据清洗import pandas as pd df pd.read_csv(data/weather/weather.csv) df df.fillna(methodffill) # 缺失值用前一个有效值填充 df df.dropna() # 还有空就删掉 print(df.isnull().sum())逻辑说明fillna(methodffill)是时间序列里最保守的缺失值处理方式用上一个时刻的值补当前空位不会引入未来信息。dropna()再把剩下的空行删掉确保喂给模型的数据没有空洞。参数说明methodffill是 pandas 2.0 之前的写法如果你装了 pandas 2.x推荐改成df df.ffill()。这一步对线性模型非常关键如果缺失值太多滑动窗口切出来的样本可能混入大量 NaNloss 直接变 NaN。4. 训练与评估从 main.py 到 run_longExp.py 的调用链4.1 跑通第一个实验python main_weather.py环境配好、数据放对位置后最直接的操作就是把默认实验跑起来。cd Multivariate-Time-Series-Forecasting-main python main_weather.py逻辑说明这条命令会启动 Weather 数据集的完整训练测试流程。main_weather.py里已经内置了一组默认参数比如 seq_len336、pred_len96、batch_size32、learning_rate0.001。训练过程中终端会打印每个 epoch 的 train_loss 和 val_loss训练结束后在测试集上输出Test MSE和Test MAE。参数说明第一次跑不要急着改任何参数先让默认配置完整走一遍。看到终端出现类似Test MSE: 0.1520, MAE: 0.2420的输出就说明整条链路没问题。如果你的 GPU 显存不够可以先在命令行加--use_gpu 0强制 CPU 跑虽然慢一些但至少能验证流程正确。跑通之后你要能区分三个阶段的输出训练阶段的 loss 是不断下降的验证阶段的 loss 决定是否早停测试阶段打印的指标才是论文里要用的最终结果。如果训练阶段 loss 正常下降、测试阶段却很高通常是过拟合或数据切分出了问题往下看排查章节。4.2 参数怎么改入口脚本里的配置项main_weather.py这类脚本全部基于 argparse 写参数所以改参数有两种方式一是直接改脚本里的 default 值二是在命令行覆盖。我推荐命令行覆盖这样不会污染源代码也方便在 scripts 里批量跑不同配置。参数默认值作用建议调整seq_len336输入历史窗口长度根据数据频率调整短期数据可减到 168pred_len96预测长度常用 96/192/336/720batch_size32每次迭代样本数显存不足就降到 16 或 8learning_rate0.001学习率不收敛时降到 0.0005train_epochs100最大训练轮数小数据集可设 50patience3早停容忍轮数想更稳就设 5featuresMM多变量S单变量保持 M除非你知道自己在做什么# main_weather.py 里的参数解析节选 parser.add_argument(--seq_len, typeint, default336) parser.add_argument(--pred_len, typeint, default96) parser.add_argument(--learning_rate, typefloat, default0.001) parser.add_argument(--batch_size, typeint, default32) args parser.parse_args()逻辑说明这段代码在脚本开头执行解析完的参数会传给Exp_Main再由Exp_Main初始化模型和 DataLoader。所以你在命令行传入的任何参数最终都会反映到模型输入维度和训练配置上。参数说明seq_len336表示用过去 336 个时间步预测未来 96 个时间步。在 ETT 这种小时级数据上336 正好是两周的量能覆盖多个周期。如果数据是分钟级336 只覆盖几个小时可能不够需要调大。命令行覆盖的写法是python main_weather.py --seq_len 168 --pred_len 192 --batch_size 16这样就不需要改源码跑完一个配置想换下一个也方便。4.3 批量跑实验run_longExp.py 与 scripts单次跑通只是开始论文里常见的实验对比表需要跑多个预测长度。run_longExp.py的存在就是干这个的它会按脚本里预设的配置逐条执行把每种pred_len的结果串起来。如果scripts/目录下有现成的.sh文件直接执行即可bash scripts/train_weather.sh如果 scripts 里只有脚本模板你也可以自己写一个 bash 循环这是最常见的做法for pred_len in 96 192 336 720 do python main_weather.py --pred_len $pred_len done逻辑说明这个循环把 96/192/336/720 四个预测长度各跑一遍每次跑完会打印一个测试结果。你把这 4 个结果记下来就是一张简单的长序列预测对比表。参数说明$pred_len是 bash 变量每次循环取列表里的一个值。如果你想把每个结果单独保存可以在命令后面加日志重定向python main_weather.py --pred_len $pred_len logs_$pred_len.txt。这样每个预测长度的日志都独立存在后续整理数据会省很多时间。4.4 评估指标怎么看MSE / MAE 之外还要看什么训练结束后打印的指标一般只有 MSE 和 MAE但真正要对比模型好坏建议把 R² 也一起算出来。项目里utils/如果没写 R²你可以自己补一个函数。指标含义读取建议MSE均方误差对大误差敏感值与量纲有关MAE平均绝对误差对异常值不敏感更直观R²决定系数越接近 1 越好但时序上可能虚高对比指标时有个容易踩的坑pred_len越大MSE 通常越高这是正常现象。不要拿pred_len96的 MSE 去和pred_len336的 MSE 比必须在相同预测长度下对比模型。还有一些论文会把归一化后的 MSE 和 MAE 一起贴出来因为不同数据集的量纲差别很大Weather 的 0.15 和 Electricity 的 0.15 不代表同一水平。看指标时我还会额外关注一个细节测试集最后一个 batch 预测得如何。因为滑动窗口切到最后如果序列长度不够有些实现会直接丢弃尾部样本导致测试集覆盖不全。我一般会打开测试流程确认一下test_dataset的样本数是不是总长度 - seq_len - pred_len 1。5. 避坑记录五个让项目翻车的常见问题5.1 数据加载报错FileNotFoundError 或 KeyError现象运行python main_weather.py后出现FileNotFoundError: [Errno 2] No such file or directory: data/weather/weather.csv或者读完 CSV 后报KeyError: OT。原因前者是文件路径不对CSV 没放到data/weather/下后者是列名不一致代码里默认取名为OT的列作为预测目标但你的 CSV 里没有这一列。解决先确认目录结构再打印 CSV 列名import pandas as pd df pd.read_csv(data/weather/weather.csv) print(df.columns.tolist())逻辑说明打印列名后和data_provider里的目标列对比如果列名不叫OT要么改 CSV 的表头要么改代码里读列名的参数。我一般选择改表头因为改表头只影响数据文件不影响工程代码。5.2 维度不匹配输入张量形状出问题现象训练到第一个 batch 时报RuntimeError: size mismatch for tensor或者mat1 and mat2 shapes cannot be multiplied。原因模型线性层的输入维度seq_len和data_provider里切出来的窗口长度不一致。最常见的是你在命令行改了--seq_len 168但模型初始化时用的还是默认的 336或者自定义数据集时变量数和你传入d_model不一致。解决在训练循环里打印第一个 batch 的形状for i, (batch_x, batch_y) in enumerate(train_loader): print(batch_x.shape, batch_y.shape) break逻辑说明batch_x的形状一般是[batch_size, seq_len, num_features]batch_y是[batch_size, pred_len, num_features]。打印出来后对照模型forward里的注释确认seq_len和num_features两边对得上。参数说明如果batch_x是[32, 336, 21]模型线性层的第一维必须是 336第二层映射目标是 96。你改了seq_len但没改模型的init参数就会在这里爆掉。解决方法是让模型初始化时的seq_len也读同一个 args 里的值。5.3 训练不收敛loss 振荡或干脆不降现象train_loss 一直在 0.05 到 0.1 之间震荡val_loss 不降或者训练几步后 loss 直接变成 NaN。原因学习率太大或者数据没有归一化。这是线性模型最常见的两个病根。学习率过大会让参数在最优解附近来回跳不归一化则会让某些变量量纲过大直接把梯度撑爆。解决把learning_rate从 0.001 降到 0.0005同时检查data_provider里是否用了 StandardScaler# 常见的归一化写法 sc StandardScaler() train_data sc.fit_transform(train_data) val_data sc.transform(val_data) test_data sc.transform(test_data)逻辑说明fit_transform在训练集上拟合并变换transform在验证集和测试集上使用训练集的统计量不会引入测试集信息。很多人图省事把所有数据一起fit_transform这在时间序列里属于数据泄漏会让指标虚高。参数说明learning_rate0.0005是我在 ETT 和 Weather 上验证过比较稳的值。如果你用的是 AdamW 或 Adam还要确认weight_decay不要太大超过 0.01 在小数据集上也容易拖慢收敛。5.4 GPU 显存不足out of memory现象报错CUDA out of memory尤其是跑main_electricity.py或main_traffic.py时这两个数据集变量数分别是 321 和 862单个 batch 的中间张量非常大。原因batch_size32对 862 个变量的 Traffic 数据来说太大了再叠加seq_len336模型前向时生成的中间矩阵会超出显存。解决把 batch_size 降到 16 或 8必要时把 seq_len 从 336 降到 168。python main_traffic.py --batch_size 8 --seq_len 168逻辑说明这个命令同时降低批量大小和输入窗口两个维度都对显存占用有直接影响。如果还爆就直接改用 CPU--use_gpu 0。DSTLinear 是线性模型在 CPU 上跑 Traffic 一个 epoch 也就多等几分钟不会到跑不完的程度。参数说明--seq_len 168会让精度略降但显存压力小很多。跑通之后你可以再慢慢把 seq_len 加回来对比一下指标变化找到显存和精度的平衡点。5.5 预测结果几乎是均值模型退化现象测试集上预测曲线基本是一条水平线MSE 比 ARIMA 还差画出来完全看不出趋势。原因模型没有学到有效信息。常见诱因有三个一是seq_len太短比如小于一个周期模型看不到完整周期二是pred_len远大于seq_len线性层被要求从很短的历史外推很远的未来只能学个均值回归三是数据预处理时误用了差分把趋势信息全删了。解决先跑回默认参数确认基线正常再逐项排查。我见过有人自己加了一阶差分预测的是差分序列回推成原始序列后全乱了。正确做法是直接用原始值训练让线性层自己去学趋势。调试时把预测曲线画出来plt.plot(y_true[:96], labeltrue) plt.plot(y_pred[:96], labelpred) plt.legend()逻辑说明画图能直接看出预测是滞后还是水平线。如果预测曲线整体滞后真实曲线说明模型只在复制上一个周期的形态没有真正学到季节规律如果是一条水平线大多就是差分或归一化出了问题按上面的检查顺序排查。6. 进阶用法把 DSTLinear 换成你自己的线性模型6.1 在 models 目录新增 MyLinear项目最值得利用的地方是模型层完全可替换。复制一份 DSTLinear 的文件把 forward 改成最简单的线性映射就是一个干净的基线模型。# models/MyLinear.py import torch.nn as nn class MyLinear(nn.Module): def __init__(self, seq_len, pred_len, d_model): super(MyLinear, self).__init__() self.linear nn.Linear(seq_len, pred_len) def forward(self, x): # x: [B, N, L]对每个变量独立做时间维线性变换 return self.linear(x)逻辑说明x的最后一个维度是时间步nn.Linear(seq_len, pred_len)会把这个维度从历史窗口长度映射到预测长度。d_model参数保留是为了和原模型的实例化签名保持一致这样主要改models/__init__.py里的注册名不用动实验类。参数说明seq_len和pred_len必须和入口脚本里的值一致。改完模型后把实验类里的模型名替换掉重新跑一遍对比 MyLinear 和 DSTLinear 的 MSE你就能直观看到分解模块到底带来多少提升。6.2 可视化验证不要只信 MSEMSE 是数值但时间序列预测好不好图形判断往往更敏锐。我会把测试集第一个 batch 的真实值和预测值画在同一张图里重点看趋势对齐和相位是否滞后。import matplotlib.pyplot as plt plt.figure(figsize(10, 4)) plt.plot(y_true[:96], labeltrue) plt.plot(y_pred[:96], labelpred) plt.legend() plt.savefig(pred_check.png)画完以后看三点预测是否跟随真实值的拐点有没有系统性滞后曲线有没有明显抖动。如果横坐标太密集看不清楚就把显示区间缩短到前 32 个点或者每 16 个点显示一个刻度。这个习惯比盯着 MSE 死磕效率高得多。6.3 做基线对比的好习惯从拿到这份源码开始我给自己定了个规矩换任何新数据集第一件事先跑最朴素的线性基线再决定要不要上分解、上注意力。因为如果一层线性层就能拿到很低的 MSE说明这个序列的可预测性主要靠线性趋势上去堆深层结构只会徒增方差如果线性模型崩了再考虑 DSTLinear 的分解模块或者换 Transformer。那个把 DSTLinear 改回 MyLinear 的下午我肉眼看着两条曲线的差距才真正明白分解这一步在一个预测项目里有分量。从那以后我每次换数据集都强制走一遍这个流程先线性基线再可视化第一屏预测最后才上复杂模型。希望帮到你。本文还有配套的精品资源点击获取
返回列表