简介:基于Transformer的时间序列预测项目,面向机器学习和深度学习开发者,针对天气预报、股票分析、电力消耗预测等长序列场景,提供一套从理论复现到工程部署的完整方案。压缩包共91个文件,包含40个Jupyter Notebook、24个Python脚本,并辅以RST技术文档、PNG结果图、Markdown说明和JSON配置,整体约48.85MB,目录按功能模块划分,便于按需查看与二次开发。项目覆盖数据预处理、模型构建、训练调优、交叉验证、性能评估和导出部署等关键环节,完整实现了编码器-解码器结构、多头自注意力、位置编码、前馈网络等Transformer核心模块;Notebook适合交互式分析数据与训练过程,Python脚本适合批量执行与自动化调参。此外还提供基准对比、学习曲线绘制和超参数搜索模块,可量化与其他时序预测方法的差异。已有299人学习下载,对于希望吃透注意力机制并实践时序建模的开发者而言,是兼顾理论与代码的优质案例。
1. 基于transformer的时间序列预测.zip:这份代码包到底在解决什么问题
“基于transformer的时间序列预测.zip”这类压缩包,在数据竞赛和工业项目里出镜率很高,里面通常把数据加载、模型定义、训练和推理脚本打包齐了。它解决的是“用自注意力机制替代LSTM做多步预测”这件事:给定一段历史窗口,输出未来N个时间点的预测值。适合手里有业务历史数据、正在研究transformer时序预测方向的从业者,也适合把这份zip当骨架来改的新手。
不过,直接把包解压后跑train.py,大概率会翻车。时序预测里的Transformer和NLP版本在位置编码、掩码设计、归一化时机上差异很大,不做改动就跑,效果常常还不如一个Naive基线。这篇笔记按拆结构、跑通、调参、避坑、验收的顺序,把能出可信预测结果的那条路走一遍。
2. 拆开zip看结构:Transformer做时间序列预测的原理、文件组织与选型理由
2.1 从LSTM到Transformer:时间序列预测为什么需要自注意力
在Transformer进入时间序列领域之前,LSTM是处理序列预测最常见的骨架。它按时间步顺序更新隐状态,网络必须把过去信息“搬运”到当前时刻,序列一长,前面的信息经过多次非线性变换之后就会衰减,训练也慢,因为每一步都必须等上一步算完。而Transformer架构的工作原理,核心是自注意力机制:它把整段历史窗口同时送进模型,直接计算任意两个时间点之间的依赖权重。时间序列里的日周期、周周期、节假日效应,本质上都是全局依赖,这种结构正好戳中要害。
但直接把NLP里的transformer架构搬过来是行不通的。NLP做的是离散词表上的分类输出,时间序列预测是连续值回归,所以模型最后要把输出层从softmax换成普通Linear层,把d_model维映射到pred_len维。很多zip包里的model.py保留了NLP版的输出头,这是拿到包以后第一个要核对的位置。
另一个让新手跑transformer模型容易卡住的是“位置”怎么表示。attention本身对位置不敏感,它只知道哪两个位置互相影响,但不知道谁在谁前面。这就是“transformer的位置信息怎么计算”那类问题被反复追问的原因:如果不把位置信息喂给模型,它看到的所有窗口都像一堆乱序的点。这个问题的处理方法放在下一节。
2.2 位置编码与因果掩码:时序Transformer的两个核心改动
先说位置编码。最简单的方式是沿用NLP的sin/cos固定位置编码,但时间序列比文本多一层信息:时间戳本身是有语义的。同样是第48个位置,在工作日和周末的凌晨两点,业务含义完全不同。常见的做法是在构造样本时拼上外部时间特征,比如星期几、小时、是否节假日、月份,先归一化或做嵌入,再和数值特征拼接。对小时级数据,我一般至少加入“小时”“星期几”“是否节假日”三个特征;对日度数据,加入“星期几”和“月份”通常就够了。
除此之外,再把sin/cos位置编码加到输入或attention输出上,两套信息并不冲突。如果你发现包的data_loader里根本没有时间特征这一列,那这个zip大概率是纯NLP结构改过来的,需要自己补。这里可以用一个简单的例子验证:让模型去预测正弦函数,不做位置编码也能拟合得像模像样,因为正弦波本身规律太强;换成真实业务数据,有趋势、有缺失、有异常点,位置信息的缺失就会让预测曲线整体错位。
第二个核心改动是因果掩码。预测任务讲究“不能偷看未来”,t时刻的输出只能依赖t时刻及之前的信息。标准Transformer的Encoder注意力是双向的,如果不加处理直接拿Encoder当预测器,训练时输入窗口里的未来点会被当前点“看见”,训练loss低到离谱,推理时却完全没有未来可看,输出直接崩掉。解决办法是生成一个上三角掩码,把未来位置的attention分数设为负无穷,softmax之后权重自然变成0。在PyTorch里一行就能生成:
# 生成因果掩码:上三角为True的位置不允许被关注 causal_mask = torch.triu(torch.ones(seq_len, seq_len), diagonal=1).bool() # 之后把它传给attention的attn_mask参数即可这个掩码,encoder部分可以不用,但decoder部分必须有。
2.3 解压后的文件通常怎么组织:先找这四个文件
不同zip包的文件名可能不一样,但结构上基本逃不开下面这几块。我拿到任何一份代码包,第一步就是先看这四个文件,确认它是不是按时间序列任务正确改过。
| 常见文件名 | 职责 | 打开后重点核对什么 |
|---|---|---|
| data_loader.py | 读原始数据、切窗口、归一化 | 归一化的fit用在哪部分数据;验证集是否按时间切分 |
| model.py | 定义Transformer结构 | attention里有没有因果掩码;输出层是不是Linear |
| train.py | 训练循环 | loss函数是什么;训练数据有没有被shuffle |
| predict.py / inference.py | 加载权重做预测 | 是否支持滚动多步预测 |
四个文件里,model.py和data_loader.py是重点。model.py决定了模型在结构上有没有留下“偷看未来”或“位置缺失”的隐患;data_loader.py决定了数据在输入端有没有泄漏。很多看起来训练曲线很漂亮的代码包,问题都出在这两个文件里,而不是出在超参数上。如果解压出来只有一个train.py,全部逻辑挤在一起也没关系,先从main函数往里读,把“数据构造”和“模型定义”两段单独拎出来看,再动手跑。
3. 从解压到出第一个预测结果:本地跑通的最小命令与推理脚本
3.1 环境准备:用conda把torch依赖一次装齐
拿到zip之后不要急着打开代码读,先把环境装好,让项目先跑起来再说。时序Transformer的依赖不算复杂,核心就是PyTorch加上数据处理那几件套。我的习惯是用conda建一个独立环境,避免和别的项目互相污染。
# 解压到本地目录,进入项目根目录 unzip 基于transformer的时间序列的预测.zip -d ./tsf cd ./tsf ls -la # 先看根目录:入口脚本、requirements、data在什么位置 # 创建干净环境,Python 3.10 对torch兼容性比较稳 conda create -n tsf python=3.10 -y conda activate tsf # 有requirements.txt就优先用它,版本不容易打架 pip install -r requirements.txt # 没有requirements.txt时,这套组合足够跑通大部分项目 pip install torch numpy pandas matplotlib scikit-learn第一行解压命令里的zip文件名按你实际下载的文件名改就行。ls -la这一步很关键,因为很多zip包的入口脚本不叫train.py,有的叫main.py,有的叫run.py;数据目录有的叫data,有的叫dataset,先看清再动手,能省去后面猜路径的时间。requirements.txt存在时优先安装它,这是避免版本冲突最省事的办法;不存在时,上面那五个库就是大多数时间序列Transformer包的公共依赖,暂时不用装额外花哨的东西,用到什么缺什么再补。
3.2 用自带样例数据启动训练:最小命令与关键日志
环境就绪后,先别急着改任何代码,直接用包自带的样例数据跑一次训练。作用是验证整条链路,包括数据读取、模型前向、loss回传、checkpoint保存,至少是通的。
# 常见入口长这样,具体文件名以ls的结果为准 python train.py \ --data ./data/example.csv \ --seq_len 96 \ --pred_len 24 \ --epochs 30 \ --batch_size 32如果不确定参数名,先执行python train.py --help,几乎所有这类包都会用argparse定义参数,帮助信息会列出全部可选项。上面这组参数对样例数据是比较通用的起步值:seq_len 96表示用最近96个历史点作为输入窗口,pred_len 24表示一次预测未来24个点;epochs 30对样例数据足够看趋势;batch_size 32是常规值,没有GPU时把它降到8或16,再在命令后面加--device cpu。
训练开始后看日志,我一般只关注两个地方。第一是train loss是否整体下行,如果十个epoch过去还在原地震荡,先停下来,问题多半不是训练步数不够,而是学习率或数据归一化出了问题;第二是eval loss是否和train loss同步变化,如果train loss一直降、eval loss连续抬升,那是过拟合信号,该考虑降低层数或加大dropout。训练结束后,包里通常会把最优权重存到checkpoints目录,名字可能是best.pt或model_best.pth,下一节用它做推理。
3.3 加载权重做预测:自回归滚动预测的推理脚本
训练完成后最想做的事,就是看看模型到底能不能预测出像样的曲线。这里给一个通用的推理脚本,它加载训练好的checkpoint,从原始数据里取最近seq_len个点,然后滚动预测未来pred_len个点。
import torch from model import build_model from data_loader import load_last_window # 超参数必须和训练时保持一致,否则加载权重会报shape mismatch model = build_model(seq_len=96, pred_len=24, d_model=128, n_heads=4) state = torch.load("checkpoints/best.pt", map_location="cpu") model.load_state_dict(state["model"]) model.eval() # 取原始数据里最后96个点,shape: [1, 96, feature_dim] x = load_last_window("data/example.csv", seq_len=96) preds = [] for step in range(24): with torch.no_grad(): y = model(x) # 单步输出 shape [1, 1] preds.append(y.item()) # 目标列用预测值替换 y_expand = y.unsqueeze(-1) # shape [1, 1, 1] # 时间特征(星期几、小时等)按未来时间戳重新生成 future_features = make_future_features(step) # shape [1, 1, n_features - 1] new_row = torch.cat([y_expand, future_features], dim=-1) x = torch.cat([x[:, 1:, :], new_row], dim=1)这个脚本体现的是自回归滚动预测思路:模型每次只预测下一个点,然后把预测值当作新的输入拼进窗口,继续预测下下个点。好处是每一步都基于最新状态,对趋势变化的适应性强;缺点是误差会逐步累积,预测越往后越不可信,所以pred_len不宜设得过大。如果你的包里模型本身就是多步直接输出,即一次forward直接返回24个点的序列,那就不需要这个循环,直接把模型输出reshape成pred_len维即可。
load_last_window和make_future_features这两个函数包里不一定有。如果没有,用pandas读原始csv,取最后seq_len行,再按训练时的特征顺序做同样预处理;未来时间特征可以直接由日期推算,外生变量的未来值如果拿不到,常见做法是用最近值填充。推理完成后,把预测值和真实值画在一张图里,先做这一步再谈指标。
4. 把预测误差降下来:需要重点关注的5组参数与配置
4.1 输入窗口长度与预测长度:先让Transformer看见一个完整周期
训练能跑通不代表预测能用,真正花时间的地方在参数调整。第一个要动的是seq_len和pred_len这对窗口参数,它们直接决定模型能看见多少历史、需要输出多远未来。
| 数据粒度 | 常见seq_len | 选择理由 |
|---|---|---|
| 分钟级设备指标 | 128-512 | 抓短周期变化和设备漂移 |
| 小时级客流、负荷 | 96-336 | 覆盖4~14天,能刻出周周期 |
| 日度销量、库存 | 14-90 | 覆盖2~12周,包含自然周和月度节奏 |
选seq_len的首要原则是“覆盖一个完整业务周期”。做小时级客流预测,输入窗口如果只有24小时,模型只能看到日内规律,看不到昨天同一时刻和上周同一时刻的对比,周周期信息全部丢失。金融时序预测也是同样道理,日频数据的输入窗口至少要覆盖一周以上,否则模型对周五和周一的行为模式无法区分。
pred_len的设置一般不超过seq_len的一半。一次性预测太远的未来,既会加重误差累积,也会让模型被迫学习很多不必要的长距离关联,反而把近期的预测精度拖下来。如果业务确实需要预测很远,优先改成滚动预测,窗口短一点、滚动多几步,比强行设一个超大pred_len更可靠。
4.2 d_model、n_heads与层数:模型容量和时序任务不匹配怎么办
第二个容易踩坑的地方是模型容量。NLP任务里d_model取512、encoder堆6层是很常规的配置,但时间序列任务的数据量通常远小于NLP语料,照搬这套结构几乎必过拟合。对大多数业务预测场景,下面这个范围足够用了:
| 参数 | 常见取值范围 | 调整方向 |
|---|---|---|
| d_model | 64-256 | 数据量大往256,数据小用64 |
| n_heads | 2、4、8 | 必须满足d_model能整除n_heads |
| encoder层数 | 2-4 | 验证loss抬升就降层数 |
| decoder层数 | 1-2 | 多数场景1层就够 |
判断容量够不够,看train loss是否还在降。如果训练到最后train loss依然高,适当加大d_model或增加层数;如果train loss降得很好但验证集一塌糊涂,说明容量过大,优先减层数而不是减d_model。d_model和n_heads之间有个硬约束:d_model必须能被n_heads整除,否则维度切分时会报错。新手跑transformer模型常在这里卡住,报错信息里出现shape相关异常,先检查这个整除关系。
4.3 dropout与学习率:过拟合与loss震荡的处置办法
时序预测模型的过拟合非常隐蔽,因为它的特征维度通常不高,模型容量一大,很快就能把训练集中的噪声也记住。dropout一般设在0.1到0.2之间,太高会把有效信息也丢掉,预测曲线整体变平;太低则起不到约束作用。学习率方面,我偏好用一个带warmup的调度器,前20%的步数线性升温到max_lr,后面再用余弦衰减回落。
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.OneCycleLR( optimizer, max_lr=1e-3, total_steps=epochs * len(train_loader), pct_start=0.2, ) for epoch in range(epochs): for x, y in train_loader: loss = criterion(model(x), y) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() scheduler.step()loss震荡是时序Transformer最常见的学习率问题。如果loss曲线上下跳得厉害,先把max_lr降到3e-4试试;如果loss完全不动,检查是不是学习率太小加上没有warmup,导致模型在局部平坦区域一直走不动。梯度裁剪max_norm设为1.0是一个保守而有效的默认值,它可以防止个别样本把梯度推爆,尤其适合带异常点的业务数据。
4.4 batch_size与梯度累积:单卡也能训长序列的折中方案
最后一个旋钮是batch_size。时间序列任务对batch_size不像CV那么敏感,但它会影响模型输出的“平滑度”。batch_size太大时,模型会更多关注所有样本的平均模式,预测曲线偏平,峰值经常被压掉;batch_size太小时,梯度噪声大,训练过程波动大。一般先从32开始,看loss走势再决定往16还是64调。
如果硬件限制,batch_size只能设到8或16,又想要大batch的平滑梯度效果,可以用梯度累积模拟。常见做法是把loss除以累积步数再回传,每累积足够步数才做一次参数更新:
accum_steps = 4 optimizer.zero_grad() for i, (x, y) in enumerate(train_loader): loss = criterion(model(x), y) / accum_steps loss.backward() if (i + 1) % accum_steps == 0: optimizer.step() optimizer.zero_grad() scheduler.step()关键是最后要把loss除以accum_steps,否则累积反向传播后的梯度尺度是原来的4倍,等效学习率直接变大,训练容易翻车。这里的分母和batch_size缩小的倍数保持一致,梯度累积的效果才和真正的大batch最接近。
5. 时间序列Transformer的踩坑排查:6个让预测翻车的问题与处理方式
5.1 数据泄漏:归一化偷看未来,回测全线翻车
现象:训练loss很低,验证集曲线看起来也不错,但拿最新的真实数据去做预测,结果错得离谱。
原因:代码里对全部数据做了StandardScaler的fit,均值和方差包含了验证期、测试期的信息。模型在训练时等于间接看到了未来数据的分布,所以验证集上的表现是虚高的。
解决:只在训练集上fit缩放器,验证集和测试集一律用训练集的参数做transform。这是数据管路里最典型的泄漏源。
from sklearn.preprocessing import StandardScaler # 错误写法:用全部数据拟合,泄漏未来信息 # scaler = StandardScaler().fit(all_data) # 正确写法:训练集拟合,其余集合只transform scaler = StandardScaler().fit(train_data) train_norm = scaler.transform(train_data) val_norm = scaler.transform(val_data)5.2 预测滞后:模型输出的曲线比真实曲线慢半拍
现象:画图时发现预测曲线整体向右平移,真实曲线抬头,预测曲线还在低位徘徊,看起来像“跟涨不跟涨”。
原因:MSE或MAE在数学上要求模型输出条件均值,而条件均值天然落后于趋势变化。当特征里没有明显的趋势先导信息时,模型会倾向于输出最近几个点的平均水平,滞后就出现了。
解决:一是评估指标不要只看MSE,加上方向命中率、相位延迟这类指标;二是对趋势明显的序列先做差分或去趋势,让模型拟合增量而不是拟合绝对值;三是在特征里补上滞后差分项,帮模型提前感知方向变化。
5.3 因果掩码缺失:训练正常,推理时预测值崩成一条直线
现象:训练loss低到0.01以下,显得模型非常聪明,但推理时输出不随时间变化,甚至直接发散。
原因:刚才在2.2里提到的因果掩码问题,训练阶段注意力看到了未来位置,loss当然漂亮;推理时未来不可见,模型只能依据残缺信息输出,表现崩坏。
解决:检查model.py里的attention调用有没有传attn_mask。如果用的是PyTorch自带的transformer模块,把is_causal=True或对应mask传入;如果是手写attention,用上三角掩码把未来位置遮掉。
5.4 随机切分验证集:时间被打乱,评估指标虚高
现象:验证集指标好得异常,模型拿去做样本外预测却明显更差。
原因:用sklearn的train_test_split,甚至直接在dataloader里对全量数据做随机shuffle。时间序列一旦被打乱,训练集会混入验证时间段附近的样本,模型等于见过未来。
解决:按时间顺序切分,训练集在前、验证集在后,两者之间留一段gap。gap的长度至少要和pred_len相当,否则验证集起点附近的数据和训练集末尾高度相关,评估结果仍然偏乐观。
5.5 特征和目标重叠:预测准到不真实,上线后完全不能用
现象:训练集和验证集上的误差接近0,曲线几乎完全重合,但任何人都知道预测不可能这么完美。
原因:构造特征时把目标列本身或其滞后0期版本放进了输入。比如要预测明天的销量,特征矩阵里却包含了明天的销量实际值,模型当然一学就会。
解决:把特征列和目标列一起拉出来人工核对,确认所有输入特征的时间戳都早于预测起点。特别是复制黏贴特征代码时,很容易把目标列多带了一列,这个检查花两分钟,能省后边两天的排查。
5.6 两个低频但高代价的补充坑:NaN与随机种子
还有一个经常被忽略的坑是loss训练到中途变成NaN。常见原因有三:学习率太大、输入数据没归一化、dropout在推理时没有关闭。排查顺序是先clip_grad_norm看是否缓解,再把输入数据标准化,最后确认model.eval()真的调用了。
另一个是随机种子。Transformer初始化、数据加载顺序和dropout都带随机性,不固定种子的话,同一个参数跑两次结果差异很大,尤其在做模板调参时,很容易把随机波动误判成参数改善。训练入口至少固定torch、numpy、random三个库的seed,否则前面调参的结论全是在看运气。
6. 用回测和Baseline给预测结果做“体检”:验证Transformer值不值得上线
zip包自带的样例数据上效果好看,不代表换到自己的数据上就能用。我拿到一份时间序列Transformer代码包之后,最后一个步骤永远是先做Baseline对比,再做滚动回测,两关都过了才会考虑上线。
先跟Baseline比。这里说的不是和别人论文里的SOTA比,而是跟你自己最容易实现的几种方法比。
| 模型 | 该看什么 | 结论 |
|---|---|---|
| Naive(直接用最近值填未来) | Transformer是否跑赢了它 | 跑不赢说明特征或预处理有问题,先别调参 |
| ARIMA或指数平滑 | Transformer是否显著更优 | 短序列、弱周期场景不一定需要Transformer |
| LSTM | Transformer相对LSTM提升多少 | 长序列场景里attention优势才明显 |
| Transformer自身 | 三个时间段里的表现一致性 | 换段数据就崩,说明泛化不合格 |
再做一个walk-forward回测。不要只做一次随机切分,而是把数据按时间分成多段,逐段向前推进,每段重新训练、重新预测,最后看各段指标的波动幅度。
folds = 5 step = len(ts_data) // (folds + 1) scores = [] for i in range(folds): train = ts_data[: (i + 1) * step] val = ts_data[(i + 1) * step: (i + 2) * step] model = build_model(...) model.fit(train) preds = model.predict(val) scores.append(sMAPE(preds, val)) print("fold scores:", scores)从fold scores里能看出两件事:各段sMAPE的平均值代表模型整体水平,最大值和最小值的差距代表结果的一致性。如果Transformer在Naive面前没有明显优势,又在fold之间大幅波动,那说明问题大概率出在第5章的某个坑里,先把数据管路修干净再谈上线。
我现在的习惯是,拿到任何时间序列预测的代码包,第一件事就是拿Naive当照妖镜。如果Transformer连Naive都跑不赢,我不会去调参,而是回头查数据泄漏和特征重叠;如果只是赢0.1个百分点,也不会急着替换线上模型,继续用LSTM更省心。预测的本质是拿到可验证、可重复的结果,不是把曲线画得好看。希望帮到你。
本文还有配套的精品资源,点击获取