
简介一套面向NLP学习与研究人员的Transformer模型长期预测Python代码包帮助理解自注意力、多头注意力、位置编码等核心机制并掌握从数据预处理到模型训练、预测可视化的完整流程。压缩包共39个文件包含13个Python脚本模型构建、训练与工具模块、3个CSV数据文件ETTh1电力数据集及预测结果、1个PyTorch权重文件以及配置与说明文档整体约26.49MB结构清晰便于对照学习。已有275人学习下载。代码覆盖数据加载、时间特征处理、Transformer编码器-解码器搭建并基于ETTh1数据实现长期预测通过matplotlib绘制预测与实际序列对比图目录划分了数据、模型层、工具函数等模块便于理解各组件作用。读者可据此动手复现Transformer在时序预测上的表现为后续改进或应用提供可扩展的代码基础。1. 把 Transformer 用在长期预测上为什么这组 Python 代码值得你跑一遍预测长度一拉长到 96、192 甚至 720 个时间步LSTM 这类循环模型误差会指数膨胀而 Transformer 模型凭借注意力机制可以直接建立远端依赖成了长期预测方向的主力结构。但很多人从网上下载了这份 Transformer 长期预测并可视化结果的 python 代码 zip 包解压后直接训练十有八九会翻车预测曲线退化成一条向右平移的复读机或者在验证集上漂亮、一换数据集就崩。这篇文章从数据切分、模型改法、训练与可视化到参数排错把整条链路拆开讲清楚让你拿到代码能跑、跑了能看懂、看懂了能自己改。适合做电力负荷、流量、气象或金融时序预测的工程师把 Transformer 当 baseline 或准备上生产前先摸清边界。2. 长期预测的数据准备滑窗、切分与归一化的正确顺序时序预测的数据准备和图像分类完全不一样最大的区别在于样本不能随机抽窗口与窗口之间还有信息重叠。先讲清楚顺序再给代码因为顺序错了后面模型再强都是白搭。2.1 滑窗采样把一维时序变成输入目标样本Transformer 的输入不是一条完整的时间序列而是固定长度的窗口。给定一个input_len作为编码器看到的历史长度一个pred_len作为要预测的未来长度滑窗沿时间轴切出一对历史未来样本import numpy as np def create_samples(series, input_len, pred_len, stride1): X, y [], [] # 样本起点从 0 到 总长度减去两个窗口长度 for i in range(0, len(series) - input_len - pred_len 1, stride): X.append(series[i:i input_len]) # 历史窗口 y.append(series[i input_len:i input_len pred_len]) # 未来窗口 return np.stack(X), np.stack(y)stride1表示窗口每次只挪一个时间步样本量最大但重叠也最严重strideinput_len时样本完全不重叠适合序列特别长、但不想让训练集太冗余的场景。我一般在小数据集上用stride1先保证样本量够数据超过十万个时间步再调大stride。这里必须强调滑窗要在训练/验证/测试切分之后分别进行绝不能在整个序列上一次性滑完窗再随机打乱切分。时序样本的验证集必须来自验证集时间段测试集必须来自测试集时间段否则验证集里混进训练区间附近的样本评估指标会虚高到失去意义。滑窗的另一个关键参数是input_len。经验法则是历史窗口至少要覆盖一个完整周期。预测电力日负荷input_len至少 96一天 96 个 15 分钟点预测天气数据窗口里最好跨过 2 到 3 天。窗口太短模型看不到周期性预测出来就是一条水平线。窗口太长注意力分布会被拉平后面第 3 章会讲这个问题。2.2 先切段、再归一化训练集信息泄漏的头号来源长期预测里最常见的隐性错误是在整条序列上计算均值和标准差然后把所有数据一起标准化。这样测试集的统计信息已经参与了训练时的数值缩放属于典型的数据泄漏。虽然模型不会直接用测试集标签但测试集的均值、方差被模型看见了评估结果就是假的。正确的顺序是先按时间比例切成三段再用只来自训练段统计出的 scaler 去转换三段数据from sklearn.preprocessing import StandardScaler cut1 int(len(raw) * 0.7) # 前 70% 训练 cut2 int(len(raw) * 0.85) # 中间 15% 验证最后 15% 测试 train_raw, val_raw, test_raw raw[:cut1], raw[cut1:cut2], raw[cut2:] scaler StandardScaler() # 只拟合训练段 scaler.fit(train_raw.reshape(-1, 1)) train scaler.transform(train_raw.reshape(-1, 1)).flatten() val scaler.transform(val_raw.reshape(-1, 1)).flatten() test scaler.transform(test_raw.reshape(-1, 1)).flatten() X_train, y_train create_samples(train, input_len, pred_len) X_val, y_val create_samples(val, input_len, pred_len) X_test, y_test create_samples(test, input_len, pred_len)注意scaler.fit必须只接收train_raw后面预测阶段反归一化时用的也是同一个 scaler不然画出来的图数值会对不上。上面的代码对每个通道单独处理如果你的数据是多变量把reshape(-1, 1)改成reshape(-1, n_channels)scaler 会按列逐通道统计。2.3 多变量输入的张量形状别把 Transformer 当 RNN 喂Transformer 期望的输入形状是(batch, seq_len, features)和 LSTM 的batch_firstTrue一致这一点上两个架构不打架。真正的区别在于RNN 天然按顺序消费每个时间步而 Transformer 一次性看到全部input_len个位置因此位置编码是必须品后面模型章节会展开。组织成 PyTorch Dataset 的标准做法import torch from torch.utils.data import Dataset, DataLoader class WindowDataset(Dataset): def __init__(self, X, y): self.X torch.tensor(X, dtypetorch.float32) self.y torch.tensor(y, dtypetorch.float32) def __len__(self): return len(self.X) def __getitem__(self, idx): return self.X[idx], self.y[idx] train_loader DataLoader(WindowDataset(X_train, y_train), batch_size64, shuffleTrue) val_loader DataLoader(WindowDataset(X_val, y_val), batch_size64, shuffleFalse)shuffleTrue只打乱样本顺序不改变窗口内部的时间语义是允许的。验证集和测试集必须shuffleFalse否则每次迭代样本顺序漂移早停对比的 loss 不占同一分布。这一层看着基础实际是很多预测项目返工的根源老手也常栽在顺序上。3. 把 Transformer 改造成预测器三种结构与训练模式的选择直接拿 NLP 里的原生 Transformer 做时序预测效果往往不如一个线性回归这在长期预测领域有个专门的词叫信息衰减。怎么理解、怎么改是决定模型能不能用的分水岭。3.1 信息衰减原生 Transformer 在长序列上的三个软肋第一个软肋是注意力分布退化。序列越长每个位置关注的区域越宽注意力权重越趋向均匀分布等于每个位置都平均地看了所有位置学到的是无差别汇总关键远端信息反而被稀释了。多堆几层编码器这种均匀化还会逐层放大。第二个软肋是复杂度。自注意力的计算量是序列长度的平方input_len720时单层注意力要算 50 万对位置的关系显存和时间都扛不住。第三个软肋是误差累积。如果解码端采用逐时间步自回归生成每一步都会把上一步的预测误差带进下一步输入预测长度一长误差像滚雪球一样放大最终曲线退化成复读机。正因如此专门的时序 Transformer 都在解决这三个问题Informer 用 ProbSparse 注意力降低复杂度并用蒸馏压缩序列Autoformer 把注意力换成自相关计算并引入序列分解PatchTST 先把序列切成 patch 再做注意力。我们不需要从零复现一篇论文但至少要知道长期预测的 Transformer 必须非自回归直出或生成式一次性出序列结构设计也要避开 O(L²) 全量注意力。3.2 编码器直出结构最简、坑最少的预测头对大多数业务数据最省事的方案是只用 Transformer 编码器把最后一个时间步的隐状态接一个线性层直接输出pred_len个时间步的预测。结构清晰训练稳定也没有解码器里 teacher forcing 与推理不一致的问题import math import torch.nn as nn class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len512): super().__init__() pe torch.zeros(max_len, d_model) position torch.arange(0, max_len).unsqueeze(1).float() div_term torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] torch.sin(position * div_term) pe[:, 1::2] torch.cos(position * div_term) self.register_buffer(pe, pe) def forward(self, x): return x self.pe[:x.size(1)] class TransformerForecaster(nn.Module): def __init__(self, channels, d_model64, nhead4, num_layers2, dropout0.1, pred_len96): super().__init__() self.input_proj nn.Linear(channels, d_model) # 输入嵌入 self.pos PositionalEncoding(d_model) encoder_layer nn.TransformerEncoderLayer( d_model, nhead, dim_feedforwardd_model * 4, dropoutdropout, batch_firstTrue, norm_firstTrue) self.encoder nn.TransformerEncoder(encoder_layer, num_layers) self.head nn.Linear(d_model, pred_len * channels) self.pred_len pred_len self.channels channels def forward(self, x): # x: (batch, input_len, channels) x self.input_proj(x) # (batch, input_len, d_model) x self.pos(x) # 加入位置编码 x self.encoder(x) # (batch, input_len, d_model) x x[:, -1, :] # 取最后一个时间步的隐状态 x self.head(x) # (batch, pred_len * channels) return x.view(-1, self.pred_len, self.channels)代码里两个要点。一是norm_firstTrue采用 Pre-LN先归一化再做注意力训练比 Post-LN 稳定得多长期预测的模型又普遍偏深Pre-LN 能省掉大量调参时间如果你的 PyTorch 报这个参数不认识说明版本太老升级环境或者去掉该参数二选一。二是预测头只取最后位置x[:, -1, :]因为最后一个位置已经通过注意力汇总了整个历史窗口的信息不需要像 RNN 那样拿全部隐状态去拼。d_model64适合中小数据集数据量大再提到 128nhead必须整除d_model常见配比是 64/4、128/8、256/8。num_layers从 2 开始调3 层以上在时序数据上收益很有限反而容易过拟合。3.3 解码器与 teacher forcing什么时候该上生成式预测编码器直出适合单步直推但有些场景希望解码端额外接收已知的未来起点特别是做多变量预报、且未来初始段有确定性信息时这时用 Transformer 解码器做生成式预测更合适。这里的生成式指的不是逐时间步自回归而是一次性生成整个预测序列避免误差累积。训练阶段解码器输入由历史序列最后几个点 未来真实值拼接而成即 teacher forcing推理阶段解码器输入改为历史序列最后几个点 零填充占位。这种不对称会导致训练与推理分布不一致也就是 exposure bias。缓解办法是在推理时用历史尾部做 start token让解码器从已知位置出发decoder_layer nn.TransformerDecoderLayer( d_model, nhead, dim_feedforwardd_model * 4, batch_firstTrue, norm_firstTrue) self.decoder nn.TransformerDecoder(decoder_layer, num_layers2) # 训练tgt [历史尾部最后 pred_len 个点 未来真实值] shape (batch, pred_len, d_model) # 推理tgt [历史尾部最后 pred_len 个点 全零占位] tgt torch.cat([context_last_k, future_seed], dim1) out self.decoder(tgt, memory_from_encoder) pred self.head(out) # 映射回 (batch, pred_len, channels)注意这里tgt内部的注意力不需要上三角掩码因为每个解码位置都允许看到全部历史上下文属于并行生成只有真正的逐点自回归才需要torch.triu构造因果掩码。长期预测里我基本不用逐点自回归误差累积的问题很难根治。3.4 损失函数与优化器稳定性比涨点更重要长期预测的默认损失是 MSE因为它对异常点敏感会把模型逼着去拟合大误差位置这其实是好事时序预测最怕的就是平稳但没用。唯一我建议做的小改动验证指标同时打印 MAE因为 MSE 会把几条异常样本的权重抬得太高MAE 更容易看出曲线整体偏移。优化器选 AdamW学习率1e-4起步比默认的1e-3稳得多。Transformer 对学习率极敏感1e-3在深层编码器上很容易造成 loss 震荡。配合学习率预热加余弦退火能进一步压低终点 lossoptimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-5) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxepochs)weight_decay1e-5是经验值给太小没效果给太大会把模型压成欠拟合。还要加梯度裁剪clip_grad_norm_(model.parameters(), 1.0)时序输入偶尔有尖峰梯度一爆就 NaN裁剪等于一道保险。4. 训练、预测与可视化把结果画成能说服人的图代码跑到这一步模型结构已经搭好接下来是把训练循环写稳、把预测结果还原回原始尺度、再画成对比图。可视化这一环看着只是输出实际很多项目就是因为图画得不对误判模型不行。4.1 训练循环早停、模型保存与验证训练循环里必须做三件事验证集早停、保存验证集最优权重、切换model.eval()。初学者最容易漏掉最后一点带着 Dropout 做预测结果每次跑出来的曲线都不一样还以为模型随机性太大。import torch.nn.functional as F model TransformerForecaster(channelsn_channels, d_model64, nhead4, num_layers2, pred_lenpred_len) optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-5) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxepochs) best_val, patience float(inf), 0 for epoch in range(epochs): model.train() train_loss 0.0 for Xb, yb in train_loader: optimizer.zero_grad() pred model(Xb) loss F.mse_loss(pred, yb) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() train_loss loss.item() scheduler.step() model.eval() val_loss 0.0 with torch.no_grad(): for Xb, yb in val_loader: pred model(Xb) val_loss F.mse_loss(pred, yb).item() val_loss / len(val_loader) if val_loss best_val: best_val val_loss torch.save(model.state_dict(), fbest_{pred_len}.pt) patience 0 else: patience 1 if patience 5: print(fearly stop at epoch {epoch 1}) break if (epoch 1) % 5 0: print(fepoch {epoch 1:3d} | train {train_loss / len(train_loader):.5f} | val {val_loss:.5f})scheduler.step()每轮 epoch 调用一次而不是每个 batch 一次这是和很多图像分类代码不一样的地方。早停patience5对 30 到 50 个 epoch 的训练量是合理区间。保存的权重必须以验证集 loss 为准而不是最后一个 epoch否则无法保证泛化。4.2 从预测到反归一化还原原始尺度的标准流程模型在归一化后的数据上输出直接拿这个数画图纵轴不是真实物理量根本无法交付。反归一化要用当初训练集的 scaler厘清这层关系是这里唯一的难点X_last torch.tensor(X_test[-1:], dtypetorch.float32) model.load_state_dict(torch.load(fbest_{pred_len}.pt, map_locationcpu)) model.eval() with torch.no_grad(): pred_norm model(X_last).squeeze(0).numpy() # (pred_len, channels) # 记住输入是归一化过的预测结果也是归一化过的都要用 train scaler 还原 pred scaler.inverse_transform(pred_norm) true scaler.inverse_transform(y_test[-1])这里的X_test来自 2.2 节切分后的test段它经过 scaler 转换过所以模型输出pred_norm是标准化的尺度inverse_transform才能回到原始量纲。如果test段没有做 transform那就是用原始尺度数据喂模型错误更隐蔽loss 看起来也正常但预测结果和真实值永远差一个数量级。4.3 可视化历史、真实与预测的三段对齐画图最忌讳的是把预测序列横轴画成 0 到pred_len而真实值从input_len开始两线永远错位。正确的做法是在同一根时间轴上历史窗口占左侧真实和预测都从input_len这个位置开始import matplotlib.pyplot as plt n len(X_last[0]) # input_len plt.figure(figsize(12, 4)) plt.plot(range(n), scaler.inverse_transform(X_last.squeeze(0))[:, 0], colorgray, labelhistory) plt.plot(range(n, n pred_len), true[:, 0], colorgreen, labeltrue) plt.plot(range(n, n pred_len), pred[:, 0], colorred, linestyle--, labelpred) plt.axvline(n, colorblack, linestyle:, linewidth0.8) plt.xlabel(time step) plt.ylabel(value) plt.legend() plt.tight_layout() plt.savefig(long_term_forecast.png, dpi200):axvline画出的竖线是预测起点也是读者理解图的第一眼锚点。多变量数据一次画所有通道会很乱我会只画目标通道其他通道留到分析时才逐个展开。线条颜色固定成灰度历史、绿色真实、红色虚线预测这组配色在论文和汇报里都不容易引起歧义。4.4 关键参数速查表参数建议值说明input_len96至少覆盖一个周期历史窗口长度动态周期不明时从 96 起步pred_len96 / 192 / 336 / 720长期预测的标准设定按业务需求取d_model64 / 128嵌入维度中小数据集 64 够用nhead4 / 8必须整除 d_modelnum_layers2 / 3超过 3 层在时序上收益很小dropout0.1 / 0.2小数据集用 0.2 防过拟合batch_size64 / 128显存不够先减半lr1e-4AdamW不要用默认 1e-3clip_grad_norm1.0防梯度爆炸导致 NaNpatience5早停轮数5. 长期预测避坑数据泄漏、滞后错位与误差累积的 6 种排查方法这个方向踩坑密度极高而且多数坑不会让程序报错只会让结果悄悄变差。我把这几年反复遇到的六个问题按现象、原因、解决的顺序整理出来排查时逐个对照比对着 loss 曲线发呆高效。1. 验证集指标虚高换数据集立刻崩。现象训练和验证 loss 都很低测试集结果却一塌糊涂。原因最常见的是归一化泄漏在整条序列上 fit scaler测试段的均值和方差已经混入训练信息。另一种是做滑窗时先全局滑窗再随机切分验证样本和训练样本的时间区间重叠。解决严格按照 2.2 节的顺序先按时间切成三段只 fit 训练段再分别滑窗。检查代码里有没有scaler.fit(raw)这种写法有就一定是泄漏。2. 预测曲线整体比真实值向右平移一个时间步。现象预测曲线形状和真实值几乎一样但整体滞后一拍像把真实曲线右移了一位。MSE 可能还不高因为形状相似。原因时间轴没有对齐。常见于画图时预测序列从 0 开始画真实序列从input_len开始画或者数据处理时滑窗的标签取了iinput_len-1而不是iinput_len整体偏了一个步长。解决先核对数据打印y_test[-1]和X_test[-1]的最后一个时间步是否差pred_len再核对画图代码的横轴是否从n起画。滞后一步在图上非常隐蔽却是模型背锅的重灾区。3. 训练 loss 下降验证 loss 中途起飞。现象前几个 epoch 一切正常突然验证 loss 拐头上扬训练 loss 还在降。原因过拟合或调度器没配好。Transformer 在小数据集上过拟合是常态dropout0.1以下基本拦不住另一个可能是scheduler.step()放到了每个 batch 里学习率降得太快。解决dropout 调到 0.2num_layers降到 2 层确认scheduler.step()每个 epoch 只调一次训练超过 50 个 epoch 时关注早停是否触发。4. 自回归逐点预测让长序列退化成复读机。现象预测的前几个点还算正常越往后越像把最后一个观测值复制了pred_len次曲线是一条水平线。原因预测时逐时间步把输出拼回输入误差不断累积模型最终学到的策略是猜不动就别动输出趋向于最后观测值。解决改用编码器直出3.2 节或生成式一次性出序列3.3 节。如果你坚持用自回归必须做 scheduled sampling训练时按概率用模型自己的输出替换真实值但这条路调参成本高新手不建议碰。5. 显存爆炸或者训练慢到怀疑人生。现象input_len调到 336 或 720 后显存直接 OOM。原因原来自注意力的计算量是 O(L²)长度翻一倍显存翻四倍这是数学上绕不过去的。解决先降d_model从 128 降 64其次把max_len位置编码截短最后考虑换补丁式的 Transformer 结构比如 PatchTST 先把序列切成 patch 再做注意力显存和速度都能降一个量级。6. 多变量预测只画一个通道误判模型不行。现象模型训练正常画其中某个通道时预测曲线完全不像样但另一个通道却拟合得很好。原因多变量模型的每个通道共享同一套注意力强相关通道会把注意力资源吸引过去弱相关通道拟合差是正常的。解决画图前先算各通道的相关性业务只关心目标通道时可以用单变量输入做对比实验有时候单变量模型反而比多变量更稳。6. 用正弦数据验证模型十分钟跑通全链路的最短实验没有现成数据集时正弦信号是最可靠的模型自检工具周期、相位、噪声都是已知的模型如果学不会正弦就不该急着上真实数据。这也是热词里 Transformer 预测正弦数据被反复讨论的原因它能把问题从数据脏里剥离出来让你纯粹地看模型对不对。6.1 生成正弦序列并复用前面的标准流程t np.linspace(0, 8 * np.pi, 1024) series np.sin(t) 0.05 * np.random.randn(1024) # 带噪正弦 train_raw, val_raw, test_raw series[:700], series[700:850], series[850:] scaler StandardScaler() scaler.fit(train_raw.reshape(-1, 1)) train scaler.transform(train_raw.reshape(-1, 1)).flatten() val scaler.transform(val_raw.reshape(-1, 1)).flatten() X_train, y_train create_samples(train, 96, 96) X_val, y_val create_samples(val, 96, 96)接着直接套用 4.1 节的训练循环epochs30、pred_len96。训练结束后画 4.3 节的三段对比图需要验证两件事训练 loss 稳步下降说明优化器与结构没有硬伤预测曲线与真实曲线在幅度和相位上一致且测试段末尾不衰减成直线说明位置编码和注意力确实学到了周期性。6.2 从正弦到真实数据的调参路径正弦实验通过后逐步增加难度把噪声从 0.05 提到 0.2看模型抗噪能力把正弦换成方波或锯齿波看模型对突变的响应。再换真实数据集时按短预测先通长预测再调的顺序走先pred_len24跑通全链路再逐步放大到 96、192、336观察误差随预测长度的增长曲线。如果 336 的误差比 96 翻了一倍以上说明模型已经进入信息衰减区优先加input_len其次是加层数而不是盲目堆d_model。我拿到新数据集的习惯是先用正弦验证模型本身没问题再换真实数据找数据的问题两轮下来大部分翻车都能定位到是模型问题还是数据问题。这样拆开排查比在真实数据上一锅乱炖要省时间得多。希望这套流程对你也有用。本文还有配套的精品资源点击获取