Transformer长期预测实战:从原理到PyTorch实现与踩坑总结
2026/9/24 18:13:23 网站建设 项目流程

简介:基于Transformer架构实现长期预测的Python代码包,面向NLP与时序预测方向的学习者,适合希望从零搭建序列模型、进行多步预测并输出可视化结果的开发者。压缩包内共39个文件,以13个Python脚本为核心,涵盖数据加载、时间特征处理、模型构建与训练等环节,另包含预训练权重pth、结果图png、预测结果csv及项目配置文件,整包大小26.49MB。目前已有274人学习下载。代码基于ETTh1数据集演示电力负荷长期预测,通过自注意力、多头注意力、位置编码及残差连接等模块的完整实现,帮助读者理解Transformer内部机制,并可直接运行main.py查看预测序列与真实值的对比图,还可利用现成评估指标衡量效果,为迁移到其他序列预测任务提供可复用的工程参考。

1. Transformer做长期预测:为什么这个方向越来越值得投入

如果你手里有一条以小时或天为粒度的业务指标,例如设备温度、仓库销量、集群负载,想一次性往后预测几十甚至上百个时间步,传统的LSTM和ARIMA往往会在预测窗口拉长之后快速失效——误差累积、趋势滞后、峰值被抹平。这个场景正是Transformer模型切入长期预测的价值点:用注意力机制直接建模序列中任意两个时间点之间的关系,而不是像循环网络那样按时间步顺序传递信息,从而让“未来的某个值受二十步之前某个尖峰影响”这种依赖成为可能。

本文要拆解的是一个非常具体的落地路径:拿到一套python代码.zip,里面是Transformer模型实现长期预测并可视化结果的完整流程。我按自己实际做过的方案,把任务定义、数据切分、模型搭建、训练调参、结果可视化到最后的坑踩一遍,保证你照着能跑通,也明白每一步为什么这么做。适合刚接触Transformer时间序列应用、或者已经在用LSTM做预测但效果受限的从业者。

2. 长期预测的任务定义与Transformer选型:从自回归到滑窗训练

2.1 长期预测到底在预测什么:单步与多步的数学定义

长期预测(Long-term Forecasting)和普通预测的区别不在模型,而在输出长度。假设历史序列为X = [x_1, x_2, ..., x_T],单步预测只输出x_{T+1},多步预测要一次输出[x_{T+1}, x_{T+2}, ..., x_{T+H}],这里H就是预测长度,也叫horizon。工程上,H大于等于24就可以算长期预测,按“天”粒度的数据,H=30就是预测未来一个月。

有两种做法来实现多步输出。第一种叫递归预测:先用模型预测x_{T+1},把它拼回输入序列,再预测x_{T+2},依次滚下去。缺点很明显,每一步都会把上一步的误差带进新输入,随着H加长误差迅速膨胀,最后预测结果经常退化成一条平线。第二种叫直接多步预测:一次性输出H个值,让模型自己学习未来窗口内部的相关性。我的建议是,既然用了Transformer,直接做第二种——Transformer的结构优势本来就是并行处理整个序列,你让它一次把未来H个点全吐出来,比让它自回归滚动要稳得多。

2.2 Transformer比LSTM更适合长序列的机制:注意力与位置编码

LSTM处理长序列时,信息要经过每个时间步的门控单元逐级传递,路径长度等于时间步数,这会导致远端信息在传递过程中被稀释。Transformer把这种传递路径缩短到了常数级:任意两个时间步之间,只隔了一次注意力计算。无论第i步和第j步距离多远,注意力权重都能直接计算出来。这就是Transformer能捕获长程依赖的核心原因。

但代价是Transformer本身是“无序”的,它不像RNN天然有先后顺序。所以必须把位置信息编码进输入。经典做法是用不同频率的正弦余弦函数做位置编码,也叫绝对位置编码。后来Informer、PatchTST这些模型证明,对时间序列预测来说,位置编码不一定用正弦,直接用可学习的embedding甚至截断的时间戳特征(小时、星期、月份)效果更好。原因在于时间序列的位置有语义——第23点和第0点之间的距离不只是“差了23个位置”,而是“跨了一天”,这种周期性语义,正弦编码表达得不够直接。

我一般会在代码里把两套都实现出来,默认用可学习位置编码,后面接一个开关切换,方便在特定数据集上对比效果。你拿到代码包后,先看它的PositionalEncoding类用的是正弦还是可学习参数,这点直接决定你的模型有没有利用到时间周期性。

2.3 两类Transformer预测架构:编码器全输出与自回归解码

抛开各种改进变体,Transformer做时间序列预测的主流框架就两种。一种是编码器-解码器结构,模仿NLP翻译任务:编码器读历史序列,解码器以teacher forcing方式生成未来序列。Informer的ProbSparse注意力就是在这一框架下做的改进。这种结构训练稳定,但实现复杂,解码器的mask机制、动态position embedding处理不好就容易训崩。

另一种是纯编码器结构,也是我拿到这类代码包后最推荐优先跑通的方案:把历史窗口整段丢进TransformerEncoder,取编码器输出的最后一个或全部时间步的表示,过一个全连接层,直接输出H个未来值。这样省略了解码器,训练更快,代码能压缩到一百行以内。代价是表达能力相对弱一些,但对大多数业务指标预测足够用。

选哪条路,取决于你手里数据的复杂度。单变量、周期明显、噪声不大的序列,纯编码器足矣;多变量强耦合、有长周期有突发事件的序列,先跑纯编码器做基线,再切换到编码器-解码器对比提升幅度。接下来我按纯编码器结构作为主线讲,因为这套代码最简单、最好排查问题,你理解了它,再看任何Transformer预测代码都不会懵。

3. 数据预处理:长期预测最容易翻车的环节

3.1 滑窗切分与数据集划分:训练/验证/测试集的比例与顺序问题

时间序列数据和图像数据最大的区别是不能随机打乱。你如果像分类任务那样把样本shuffle了,模型学到的是“记忆答案”而不是“预测未来”——训练集里包含测试集时间段的数据,验证集就失去了意义。正确做法是按时间顺序依次切分。

滑窗切分的逻辑是:给定原始序列、输入窗口长度L和预测窗口长度H,从第0个位置开始,取[0:L]作为输入、[L:L+H]作为标签,然后向后滑动步长s,重复这个过程。代码包里的预处理脚本核心就是这个函数:

import numpy as np def create_sliding_windows(data, input_len, pred_len, stride=1): """ 将一维或多维时间序列切分为滑窗样本。 data: shape (T, N),T为时间步数,N为特征数 input_len: 输入窗口长度,即用过去多少个点做预测 pred_len: 预测窗口长度,即要预测未来多少个点 stride: 滑窗步长,步长越大样本越少 返回: X: shape (num_samples, input_len, N) Y: shape (num_samples, pred_len, N) """ X, Y = [], [] for start in range(0, len(data) - input_len - pred_len + 1, stride): end_input = start + input_len X.append(data[start:end_input]) Y.append(data[end_input:end_input + pred_len]) return np.array(X), np.array(Y)

这段代码做了三件事:第一,窗口起点从0开始,以stride为间隔滑动;第二,每个样本的输入段是[start:end_input],标签段紧跟其后;第三,样本总数大约等于(T - input_len - pred_len) / stride,数据量不够时优先调小stride而不是调小input_len,因为输入窗口长度影响模型能看到的历史范围。

划分数据集时,我有一个不成文的习惯:按时间顺序切出前70%做训练,中间15%做验证,最后15%做测试。为什么留最后一段做测试?因为预测任务要模拟真实场景——用过去的数据训练,在尚未发生的时间段上验证,只有最后15%才是模型“没看过”的时段。

3.2 归一化的坑:用MinMax还是Standard,为什么要防止数据泄露

归一化在LSTM时代就是个头疼问题,Transformer更是如此。Transformer里的注意力机制计算的是Q和K的点积,数值范围直接影响softmax的锐度。输入数据如果量纲差距大(比如温度20度和压力5000),注意力分数会被大数值特征主导,模型学不到真正的依赖关系。

MinMax归一化把数据压缩到[0,1]或[-1,1],适合分布比较平稳的序列;Standard标准化(z-score)把数据变成均值0、方差1,适合有离群值或分布厚尾的序列。长期预测场景我优先推荐Standard,原因有二:一是预测目标值有时会超出历史数据的[min, max]范围,MinMax会把超出部分截断在边界上,导致模型预测永远“顶格”;二是Standard能保留离群值的信息,让注意力机制有机会关注到异常点。

看代码包里preprocess.py时,重点检查一个细节:归一化器的fit操作有没有在划分数据集之后、并且只在训练集上执行。

from sklearn.preprocessing import StandardScaler # 正确做法:先分割再fit,scaler只用在训练集上 # 假设data已是按时间排序的原始数据 train_size = int(len(data) * 0.7) val_size = int(len(data) * 0.15) train_data = data[:train_size] val_data = data[train_size:train_size+val_size] test_data = data[train_size+val_size:] scaler = StandardScaler() # 只对训练序列fit,否则验证集/测试集的统计量会泄进训练过程 train_data_scaled = scaler.fit_transform(train_data) val_data_scaled = scaler.transform(val_data) test_data_scaled = scaler.transform(test_data)

这里的坑藏得很深:如果你的滑窗是在原始数据上切、然后统一normalize,就造成数据泄露——测试集的均值、方差参与了训练输入的变换,模型在训练时“偷看”了测试集的分布信息,测试结果虚高。正确顺序是先划分时间区间,再fit scaler于训练段,最后用同一个scaler去transform验证段和测试段。你判断一份代码规不规范,第一眼看这个位置就行。

3.3 多变量输入输出设计:target列的选择与特征拼接

多数业务场景不是单变量预测。以设备温度预测为例:目标变量是温度,但输入还包括环境湿度、负载功率、风速等外部变量。这些外部特征不能直接丢进Transformer,要分清楚哪些是“可预知未来值”的——比如天气预报值、计划停机时段,哪些是“未知未来值”的——比如负载功率本身就是预测目标。

我的处理策略是:把目标变量和外部特征拼成一个二维数组(时间步, 特征数)作为输入,滑窗切分时输出窗口只保留目标变量列。模型输入整段历史的所有特征,输出只有未来H步的目标变量。这样Transformer可以用注意力机制自行决定历史哪个时刻的外部特征对预测最有帮助。

如果外部特征在未来时段不可知,一个尽量简洁的做法是未来时段用零填充或重复最近值,让模型学会对这些特征降权。复杂做法是把输入分成“历史全特征”和“未来已知特征”分别编码再融合,这个在代码包里通常没有现成实现,不做第一步尝试。

4. 用PyTorch手写Transformer预测模型:编码输出与训练循环

4.1 位置编码与多头注意力的代码实现

先写出位置编码和TransformerEncoder的基础组件。虽然PyTorch的nn.TransformerEncoder是现成的,但位置编码需要自己做。以可学习位置编码为例:

import torch import torch.nn as nn import math class LearnablePositionalEncoding(nn.Module): """可学习位置编码:为每个时间步分配一个可训练的位置向量""" def __init__(self, d_model, max_len=2000): super().__init__() # 位置embedding矩阵,形状 (max_len, d_model) self.position_emb = nn.Parameter(torch.randn(max_len, d_model) * 0.1) def forward(self, x): # x: (batch_size, seq_len, d_model) seq_len = x.size(1) # 截取当前序列长度对应的位置向量并叠加 return x + self.position_emb[:seq_len, :].unsqueeze(0)

这里把位置向量初始化为均值为0、标准差0.1的正态分布,而不是全零。如果初始化全零,前几个训练步模型很难打破对称性,收敛变慢。参数max_len对应能处理的最长序列,输入长度超过它就会索引越界,所以代码包里的fff MAX_LEN建议设置成你在预处理脚本里input_len的1.5倍以上。

多头注意力不用自己写,nn.MultiheadAttention或nn.TransformerEncoderLayer内部已经做了Q/K/V投影和head拼接。但你要理解两个参数的作用:nhead是注意力头数,d_model必须能被nhead整除;dropout默认0.1,在数据量少时建议降到0.05或0,否则训练损失降不下去。

4.2 编码器堆叠与全连接预测头:把序列映射到未来窗口

有了位置编码,核心的预测模块组装如下。这里选择纯编码器结构,把TransformerEncoder最后一层的输出取出来,先做一个flatten再接全连接层,输出直接是pred_len个值。注意flatten之前要把特征维度压缩掉,否则全连接层参数量爆炸。

class TransformerForecaster(nn.Module): def __init__(self, d_model=64, nhead=4, num_layers=3, input_len=168, pred_len=24, num_features=3, dropout=0.05): super().__init__() # 输入特征维度映射到d_model self.input_proj = nn.Linear(num_features, d_model) self.pos_enc = LearnablePositionalEncoding(d_model, max_len=input_len + 50) # 编码器层:batch_first=True方便输入格式为 (batch, seq, feature) encoder_layer = nn.TransformerEncoderLayer( d_model=d_model, nhead=nhead, dim_feedforward=d_model * 4, dropout=dropout, batch_first=True, activation='gelu' ) self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) # 展平后接两层MLP输出预测窗口 self.head = nn.Sequential( nn.Linear(input_len * d_model, 128), nn.GELU(), nn.Dropout(dropout), nn.Linear(128, pred_len) ) def forward(self, x): # x: (batch_size, input_len, num_features) x = self.input_proj(x) # (batch, input_len, d_model) x = self.pos_enc(x) # 叠加位置编码 x = self.encoder(x) # (batch, input_len, d_model) # 取全部时间步输出而不是最后一个时间步 x = x.reshape(x.size(0), -1) # (batch, input_len * d_model) return self.head(x) # (batch, pred_len)

这段代码的关键决策有三个。第一,取全部时间步输出做flatten而不是只取最后一步,因为长期预测中未来某个点可能依赖历史任意位置的信息,全量保留能让注意力机制充分发挥;代价是input_len*d_model的中间向量维度可能很大,所以我把预测头设计成先降到128再输出。第二,激活函数用了GELU而不是ReLU,它在负半轴不是硬截断,梯度更平滑,对回归任务更友好。第三,dim_feedforward设置成d_model的4倍,这是Transformer论文里的默认比例,太小会让前馈网络表达能力不足,太大会拖慢训练。

如果你拿到代码包发现预测头只有一层线性层,也正常,响应式工业预测里有人为了省显存这么干。但如果你的训练集样本量不大,两层MLP反而容易过拟合,可以用dropout控制。

4.3 训练循环与早停:Loss曲线怎么判读

训练部分的核心是损失函数。长期预测是回归任务,均方误差MSE是最常用的选择,它对大误差点的惩罚更重,能迫使模型把尖峰也拟合到。如果你的业务更关注趋势方向判对没判对,可以在MSE基础上加一个方向惩罚项,但先从纯MSE开始,不要一上来就设计复杂Loss。

训练循环里有一个参数对Transformer特别敏感:学习率。Transformer对学习率非常挑剔,太大直接梯度爆炸,太小收敛极慢,很多时候表现为“怎么训练Loss都不降”。我习惯先跑一个3到5个epoch的烟雾测试,观察一下Loss曲线是下降还是震荡:

import torch import torch.nn as nn from torch.optim import AdamW # 初始化模型、优化器 model = TransformerForecaster(d_model=64, nhead=4, num_layers=3, input_len=168, pred_len=24, num_features=train_loader.dataset.X.shape[2]) optimizer = AdamW(model.parameters(), lr=1e-3, weight_decay=1e-5) criterion = nn.MSELoss() scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=30) best_val_loss = float('inf') patience = 8 counter = 0 for epoch in range(60): model.train() train_loss_sum = 0.0 for batch_x, batch_y in train_loader: pred = model(batch_x) loss = criterion(pred, batch_y) optimizer.zero_grad() loss.backward() # 梯度裁剪:Transformer训练的关键稳定器 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() train_loss_sum += loss.item() * batch_x.size(0) scheduler.step() # 验证集评估 model.eval() val_loss = 0.0 with torch.no_grad(): for val_x, val_y in val_loader: val_pred = model(val_x) val_loss += criterion(val_pred, val_y).item() * val_x.size(0) val_loss /= len(val_loader.dataset) train_loss = train_loss_sum / len(train_loader.dataset) print(f"Epoch {epoch+1}: train_loss={train_loss:.6f}, val_loss={val_loss:.6f}") # 早停:验证集Loss连续patience轮不下降就停止 if val_loss < best_val_loss: best_val_loss = val_loss counter = 0 torch.save(model.state_dict(), "best_model.pth") else: counter += 1 if counter >= patience: print(f"Early stop at epoch {epoch+1}") break

几个参数的选值逻辑:lr=1e-3是d_model=64时的安全起点,d_model越大lr应适当降低,d_model=128时建议1e-4;weight_decay=1e-5是给全连接权重做轻微约束,防止过拟合;梯度裁剪max_norm=1.0解决注意力机制偶尔产生大梯度的问题——如果没有这行,训练到某个epoch Loss突然变成nan是常事;早停的patience设8轮,配合CosineAnnealing先快后慢的学习率衰减,基本能保证在20-30个epoch内收敛。

5. 长期预测的常见问题排查:现象、原因、解决

5.1 预测结果是一条直线:滞后效应的三个来源

现象:验证集上预测值和真实值几乎平行,像把前一段历史平移了H步,误差看起来不大,但完全没预测出峰值和拐点。

原因分三种。第一,数据里存在强自相关,模型学到的最优策略就是“照抄近期值”,这在损失为MSE时很常见,因为均值比峰值出现的概率高得多,预测均值损失最小。第二,输入窗口太长而训练数据不足,模型没有足够的样本学到真正的动态模式,于是退化成平滑策略。第三,Loss函数对峰值不敏感,如果数据分布极不均衡,尖峰带来的MSE增量在整个训练集上占比太小。

解决顺序:先看数据的自相关系数,如果lag=1的自相关超过0.9,先做一阶差分或季节差分,把趋势和周期移除后再训练;其次是缩短输入窗口,比如把168个点缩到48个点,观察验证集是否出现波动;最后可以给损失函数加权重,真实值偏离均值越大权重越高,迫使模型关注峰值。

5.2 训练Loss下降但验证集预测全盘崩溃

现象:训练Loss稳定降到0.01以下,画出预测图却完全对不上,有时还出现数值爆炸的毛刺。

原因:数据泄露的一种隐蔽形式——滑窗切分时没有先分离标准化导致均值方差泄露是一种,另一种是滑窗步长太小时训练集和验证集样本高度重叠。比如stride=1时,相邻两个训练样本的输入窗口只差1个时间点,验证集里如果混入了和训练样本重叠的区间,验证Loss表面好看,切换到一个完全没见过的连续时间段预测就崩。

解决:检查数据划分代码,按时间顺序切割后再做滑窗,两个集合的原始时间区间必须完全不重叠;同时把滑窗stride调大,训练集内部样本重叠度高本身没问题,但验证集必须每隔H个步长采一次,确保验证样本之间也没有时间重叠。

5.3 收敛很慢或Loss值一直震荡不降

现象:训练前几个epoch Loss就在一个区间反复横跳,像是随机猜测;或者Loss在某个值附近长期横盘,怎么都降不下去。

原因:学习率设置不当,要么太大导致震荡,要么太小导致长期横盘;另一个常见原因是归一化没有生效——模型输入的数值范围还是几千几万,Transformer里的注意力点积直接溢出,softmax饱和,梯度消失。

解决:先打印模型输入的均值、方差,确认StandardScaler真正生效了;然后把学习率按数量级递减搜索,1e-3不行就试1e-4,1e-4不行就试3e-5;最后检查d_model和输入数据特征维度是否匹配——如果d_model太小(比如8),注意力头之间的信息交互容量不够,也会表现为难收敛,把d_model调到32或64重试。

5.4 反归一化后结果超出物理范围

现象:模型预测值的分布和真实值完全一致,但某个时间点的预测突然冲到超过正常范围几十倍,比如温度预测出500度。

原因:模型输出的是归一化空间里的值,反归一化时把scaler的inverse_transform传入了错误形状张量,或者预测值里有离群值,而标准化的逆变换会把离群值放大回原始量纲。

解决:反归一化前先对预测值做一次裁剪,把归一化空间里的值限制在训练集最小/最大值附近,比如训练集标准化后范围约[-3, 3],就把预测结果clip到[-4, 4]再反归一化。这个技巧不优雅但有效,对付偶发数值溢出很实用。真正的原因通常还是训练过程中某个batch产生了异常大的激活值,配合梯度裁剪和更小的学习率能根治。

6. 可视化结果与进阶验证:一张图判断模型是否真的学到了趋势

可视化不是最后画个曲线图就完事。我拿到任何代码包,第一件事是跑完训练后画三张图:训练集上的预测vs真实、验证集上的预测vs真实、测试集上连续时间段的多窗口预测。只有第三张图能说明模型在真实场景下可用。

测试集的可视化要这样画:从测试集起点开始,以滑窗方式向前滚动,但每次都用真实历史值做输入,预测未来H步,然后把预测值和真实值画在同一条时间轴上。这样能看出模型在多个连续窗口下是否稳定——如果窗口1预测出的趋势和窗口2预测的趋势出现明显跳变,说明模型捕捉到的是噪声而非规律。

import matplotlib.pyplot as plt # 假设test_loader中每条样本都是 (输入窗口, 真实未来窗口) # 从测试集起点开始,连续预测5个窗口 model.load_state_dict(torch.load("best_model.pth")) model.eval() future_horizon = 24 predictions = [] actuals = [] past_window = None # 用真实历史滚动预测,模拟真实在线预测的输入获取方式 inputs_list, targets_list = test_loader.dataset.X, test_loader.dataset.Y for i in range(0, min(5, len(inputs_list)), 1): x = torch.FloatTensor(inputs_list[i]).unsqueeze(0) y_true = targets_list[i] with torch.no_grad(): y_pred = model(x).squeeze(0).numpy() predictions.append(y_pred) actuals.append(y_true) predictions = np.array(predictions) actuals = np.array(actuals) # 绘制时间轴:把所有预测窗口按顺序拼接 plt.figure(figsize=(14, 4)) for i in range(len(predictions)): # 每个窗口内的x轴位置是窗口内部的相对时间 x_axis = range(i * future_horizon, (i + 1) * future_horizon) plt.plot(x_axis, actuals[i], 'b-', linewidth=1.2, label='Actual' if i == 0 else '') plt.plot(x_axis, predictions[i], 'r--', linewidth=1.0, label='Predicted' if i == 0 else '') plt.legend() plt.xlabel('Time steps') plt.ylabel('Scaled value') plt.grid(alpha=0.3) plt.savefig('forecast_visualization.png', dpi=150, bbox_inches='tight')

这段代码的核心逻辑是逐窗口展开预测值然后再拼接,而不是把每个窗口画成重叠的对比图。重叠图只能看出单窗口误差,展开放置能看出误差是否随窗口推进而累积。如果连续5个窗口的预测线都在真实线附近小幅波动,且没有出现越来越大的偏移,这个模型才算达到部署标准。

进阶方向我建议有余力时尝试两块。一是概率预测,让模型输出分布的均值和方差,而不是单点值,对业务决策更有价值;二是PatchTST这类模型,先把序列切成patch再进Transformer,把输入长度缩小一个数量级,训练速度和稳定性都更好。这套Transformer代码就是理解那些进阶模型的基础。

做这类项目的习惯我一直保留:跑通第一版后,先固定模型结构调数据预处理,再固定数据调结构,一次只动一个变量。别看Transformer很强大,它同样需要你给它干净的输入。希望这篇文章帮你少踩几个坑,把长期预测这条链路真正跑起来。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询