
简介本资源是一份面向深度学习初学者与时间序列分析实践者的完整PyTorch实现方案聚焦Transformer模型在长期时间序列预测任务中的落地应用。资源涵盖原理精讲、可复现代码、真实数据集ETTh1及可视化结果有效解决传统RNN/LSTM难以建模长程依赖的痛点适用于电力负荷、气象、金融等时序预测场景。压缩包共38个文件含13个核心Python源码如TransformerBlocks、Embedding、data_loader等模块、3个CSV数据文件、1个训练好的.pth模型、1张预测效果对比图results.png及requirements.txt等配套文件整体26.48MB结构清晰、模块解耦便于理解模型构建逻辑与定制化训练个人数据。目前已有984人学习下载读者可直接运行main.py完成端到端预测流程获得从数据预处理、模型定义、训练调优到结果可视化的全流程实践能力。1. 为什么用 Transformer 做长期预测不是玄学而是工程上可落地的确定性选择你手头有一组连续的时间序列数据——比如某工厂关键设备的每小时振动幅值、某区域气象站逐日的温度与湿度、或是某电商平台每分钟的订单量。你想往前看 7 天、30 天甚至 90 天不是简单外推而是要捕捉周期嵌套日周期周周期季节趋势、长程依赖今天设备的微小异常可能在 5 天后引发故障、以及多变量间的动态耦合温度突升 湿度骤降 → 加速轴承老化。这时候LSTM 往往在 20 步以上就开始“失忆”Prophet 对突发模式束手无策而传统统计模型ARIMA、ETS在非平稳、多源异构数据前直接失效。Transformer 模型实现长期预测正是为解决这类「跨度大、变量杂、模式隐」的工业级时序建模问题而生——它不靠记忆单元“硬扛”长距离而是用自注意力机制“全局扫描”让第 1 小时的传感器读数能直接、可解释地影响第 168 小时的预测结果。本文面向有 Python 基础、跑过 LSTM 但卡在长期效果上的工程师不讲《The Illustrated Transformer》式图解只聚焦如何用最小改动把你的时序数据喂进 Transformer、为什么某些参数一调就崩、可视化结果时哪些指标必须盯死、以及——最关键的怎么判断你当前的“长期预测”到底是真有效还是只是拟合了训练集里的周期幻觉。附带的代码和数据集全部基于真实工业传感器时序裁剪无合成噪声开箱即跑通 72 步预测。2. 从零构建时序 Transformer结构选型、输入构造与训练闭环2.1 为什么不用原版 NLP Transformer必须做这 3 处时序专用改造原始 Transformer 的编码器-解码器结构是为“词序列到词序列”设计的直接搬来预测时间序列会翻车。我踩过最深的坑就是照着 NLP 教程把nn.Transformer拿来用结果训练 loss 看似下降验证集 MAE 却在 30 步后爆炸式增长。根本原因有三位置编码错配NLP 用正弦位置编码sin/cos假设位置是离散 token ID但时间序列的位置是连续物理时间戳如2024-03-01 08:00:00。若只用pos torch.arange(seq_len)模型根本学不会“凌晨 3 点 vs 下午 3 点”的周期差异。嵌入层失焦NLP 把词映射成稠密向量而时序数据本身已是浮点数值。强行加nn.Embedding层等于把 0.234 这样的实数当类别处理丢失量纲信息。解码器冗余长期预测通常只需单向输出给定过去 96 小时预测未来 72 小时无需像机器翻译那样边生成边掩码。保留完整解码器徒增计算开销与过拟合风险。我的做法是放弃nn.Transformer手写轻量级 Encoder-only 架构。核心组件如下import torch import torch.nn as nn import numpy as np class TimeSeriesEncoder(nn.Module): def __init__(self, input_dim, d_model128, nhead4, num_layers3, dropout0.1): super().__init__() # 1. 时间特征嵌入将原始数值 时间戳特征联合编码 self.value_proj nn.Linear(input_dim, d_model) # 数值投影 self.time_proj nn.Linear(4, d_model) # 时间戳特征hour, day_of_week, day_of_month, month # 2. 位置编码改用可学习的时序位置编码非正弦 self.pos_embed nn.Parameter(torch.randn(1, 200, d_model)) # 预分配200步够用 # 3. 编码器层仅用 nn.TransformerEncoderLayer去掉解码器 encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, dim_feedforwardd_model*4, dropoutdropout, batch_firstTrue ) self.encoder nn.TransformerEncoder(encoder_layer, num_layersnum_layers) # 4. 输出头回归预测非分类 self.pred_head nn.Sequential( nn.Linear(d_model, d_model//2), nn.ReLU(), nn.Dropout(dropout), nn.Linear(d_model//2, input_dim) # 输出维度 输入变量数 ) def forward(self, x, time_features): x: [batch, seq_len, input_dim] - 原始时序值如 [32, 96, 5] 表示32个样本每样本96步5维传感器 time_features: [batch, seq_len, 4] - 时间戳衍生特征hour, dow, dom, month # 数值与时间特征分别投影后相加 x_val self.value_proj(x) # [B, S, D] x_time self.time_proj(time_features) # [B, S, D] x_embed x_val x_time self.pos_embed[:, :x.size(1), :] # 位置编码切片对齐 # Transformer 编码 encoded self.encoder(x_embed) # [B, S, D] # 只取最后一步的编码预测未来所有步简化版实际可接Decoder # 这里为演示用最后时刻表征预测未来72步更严谨做法见 3.2 节 pred self.pred_head(encoded[:, -1, :]) # [B, input_dim] return pred提示time_features的构造是成败关键。不要只用pd.to_datetime(df[timestamp]).dt.hour必须归一化到 [-1,1] 区间如(hour-12)/12否则模型梯度爆炸。我一般用MinMaxScaler单独拟合时间特征列避免与数值特征混用同一 scaler。2.2 数据预处理把原始 CSV 变成模型能吃的三维张量很多同学卡在第一步数据加载后x.shape (N, T)但模型要(B, S, D)。这里 D 是变量数S 是输入长度B 是 batch size。以 PHM2012 数据集涡轮发动机退化数据为例其原始格式是每台发动机一个文件含 21 个传感器列sensor_1到sensor_21和cycle运行周期。我们需要拼接多台设备数据按时间顺序合并形成(total_samples, 21)矩阵滑动窗口切片设输入长度S96预测长度L72则每个样本为[i:i96, :]→ 输入[i96:i9672, :]→ 标签时间特征生成为每个时间点计算hour,day_of_week,day_of_month,month并归一化标准化对数值特征21维传感器单独做StandardScaler均值为0方差为1绝不与时间特征共用 scaler。以下是可直接复用的切片函数from sklearn.preprocessing import StandardScaler import pandas as pd import numpy as np def create_sequences(data_df, seq_len96, pred_len72, stride12): data_df: pd.DataFrame, index 为 datetime, columns 为 sensor_1, sensor_2, ... 返回: X (N, seq_len, D), Y (N, pred_len, D), time_feats (N, seq_len, 4) # 1. 提取数值特征并标准化 value_cols [c for c in data_df.columns if c.startswith(sensor_)] scaler StandardScaler() values_scaled scaler.fit_transform(data_df[value_cols]) # (T, D) # 2. 构造时间特征hour, day_of_week, day_of_month, month time_index data_df.index time_feats np.stack([ (time_index.hour - 12) / 12, # hour: [-1,1] (time_index.dayofweek - 3) / 3, # dow: [-1,1] (time_index.day - 16) / 15, # dom: [-1,1] (time_index.month - 6.5) / 5.5 # month: [-1,1] ], axis1) # (T, 4) # 3. 滑动窗口切片注意stride 控制样本重叠度太小显存爆炸太大漏模式 X, Y, T [], [], [] for i in range(0, len(values_scaled) - seq_len - pred_len 1, stride): X.append(values_scaled[i:iseq_len]) Y.append(values_scaled[iseq_len:iseq_lenpred_len]) T.append(time_feats[i:iseq_len]) return np.array(X), np.array(Y), np.array(T) # 使用示例PHM2012 示例数据 # df pd.read_csv(phm2012_train.csv, parse_dates[timestamp], index_coltimestamp) # X, Y, T create_sequences(df, seq_len96, pred_len72, stride24) # print(fX shape: {X.shape}, Y shape: {Y.shape}, T shape: {T.shape}) # X shape: (1248, 96, 21), Y shape: (1248, 72, 21), T shape: (1248, 96, 4)参数说明stride24表示每 24 步取一个新样本平衡数据量与显存seq_len96对应 4 天若数据为每小时一条足够捕获日周期pred_len72即预测未来 3 天符合工业场景中“提前 72 小时预警”的需求时间特征归一化分母12, 3, 15, 5.5是各维度取值范围的一半确保 [-1,1] 区间。2.3 训练循环损失函数、优化器与早停策略的实战配置Transformer 易过拟合必须用强正则。我放弃MSE改用MAE Quantile Loss 加权因为工业预测更关注误差绝对值且需控制长尾风险如预测值比真实值低 20%可能错过故障class QuantileLoss(nn.Module): def __init__(self, quantiles[0.1, 0.5, 0.9]): super().__init__() self.quantiles quantiles def forward(self, preds, target): # preds: [B, L, D, Q], target: [B, L, D] assert len(preds.shape) 4 and preds.shape[-1] len(self.quantiles) losses [] for i, q in enumerate(self.quantiles): errors target - preds[..., i] losses.append(torch.max((q - 1) * errors, q * errors)) return torch.mean(torch.stack(losses)) # 主训练循环节选 model TimeSeriesEncoder(input_dim21, d_model128, nhead4, num_layers3) optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-5) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, min, patience5, factor0.5) early_stopper EarlyStopping(patience15, min_delta1e-4) # 自定义类见 4.2 节 for epoch in range(100): model.train() total_loss 0 for x_batch, y_batch, t_batch in train_loader: optimizer.zero_grad() pred model(x_batch, t_batch) # [B, D] # 注意此处为简化实际应预测整个72步见 3.2 节 loss torch.mean(torch.abs(pred - y_batch[:, 0, :])) # 只算第一步loss作示例 loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() val_loss validate(model, val_loader) scheduler.step(val_loss) early_stopper(val_loss) if early_stopper.early_stop: print(Early stopping triggered) break血泪经验weight_decay1e-5比默认1e-2更适合时序防止 attention 权重过度稀疏clip_grad_norm_1.0必加否则pos_embed参数更新时梯度爆炸学习率1e-4是起点若 loss 下降慢可试5e-4若震荡降到5e-5。3. 长期预测的三大陷阱与可视化验证别让模型骗了你3.1 陷阱一用“单步预测滚动”代替“多步端到端”导致误差雪崩新手最常犯的错误训练时只预测下一步pred_len1推理时用pred_t1作为t1的输入再预测t2……如此滚动 72 次。这会导致现象验证集上 1 步 MAE0.05但 72 步滚动后 MAE 2.5完全不可用原因每一步的微小误差被放大且模型从未见过自己预测值作为输入的分布训练/推理分布偏移解决必须端到端训练多步输出。修改模型输出头使其直接输出[B, pred_len, D]# 修改 TimeSeriesEncoder.forward() def forward(self, x, time_features): x_val self.value_proj(x) x_time self.time_proj(time_features) x_embed x_val x_time self.pos_embed[:, :x.size(1), :] encoded self.encoder(x_embed) # 关键改动不再只取最后一步而是对每个时间步做预测 # 方案A用 encoded 每个时刻表征预测未来对应步需调整位置编码 # 方案B推荐加一个线性层将 encoded 映射为 [B, S, pred_len*D]再 reshape B, S, D encoded.shape pred_flat self.pred_head(encoded.reshape(B*S, D)) # [B*S, pred_len*D] pred_reshaped pred_flat.reshape(B, S, self.pred_len, self.input_dim) # 取最后一个输入时刻的预测即用过去96步预测未来72步 return pred_reshaped[:, -1, :, :] # [B, pred_len, D]提示pred_len需作为模型初始化参数传入不能写死。实际部署时pred_len72固定训练即学出 72 步联合分布。3.2 陷阱二可视化只画“预测曲线 vs 真实曲线”掩盖系统性偏差下图是某次失败实验的典型可视化曲线贴合度高但一算指标MAE0.12,MSE0.025,sMAPE8.3%—— 似乎不错错。当你按时间分段统计误差发现时间段MAE误差方向00:00-06:000.31系统性低估 15%12:00-18:000.08基本无偏21:00-23:000.42系统性高估 22%这说明模型没学会夜间设备散热规律只记住了白天模式。真正的可视化必须包含三张图主图预测 vs 真实全时段残差热力图横轴时间步1~72纵轴样本 ID颜色为(pred - true)分位数覆盖图若用了 Quantile Loss画 10%/50%/90% 分位预测带检查真实值是否在 80% 时间内落在带内Coverage Rate。import matplotlib.pyplot as plt import seaborn as sns def plot_detailed_forecast(y_true, y_pred, quantilesNone): y_true: [N, L, D], y_pred: [N, L, D] or [N, L, D, Q] fig, axes plt.subplots(1, 3, figsize(18, 5)) # 1. 主图抽样5个样本 for i in range(min(5, len(y_true))): axes[0].plot(y_true[i, :, 0], labelfTrue-{i}, alpha0.7) axes[0].plot(y_pred[i, :, 0], --, labelfPred-{i}, alpha0.7) axes[0].set_title(Prediction vs True (first sensor)) axes[0].legend() # 2. 残差热力图取第一个变量 residuals y_pred[:, :, 0] - y_true[:, :, 0] # [N, L] sns.heatmap(residuals.T, axaxes[1], cmapRdBu_r, center0) axes[1].set_title(Residual Heatmap (sensor_1)) axes[1].set_xlabel(Sample ID) axes[1].set_ylabel(Step (1-72)) # 3. 分位数覆盖若有quantiles if quantiles is not None: q10, q50, q90 y_pred[:, :, 0, 0], y_pred[:, :, 0, 1], y_pred[:, :, 0, 2] coverage ((y_true[:, :, 0] q10) (y_true[:, :, 0] q90)).mean() axes[2].fill_between(range(len(q10[0])), q10.mean(0), q90.mean(0), alpha0.3, label10%-90%) axes[2].plot(q50.mean(0), k-, labelMedian) axes[2].plot(y_true[:, :, 0].mean(0), r:, labelTrue Mean) axes[2].set_title(fQuantile Coverage: {coverage:.2%}) axes[2].legend() plt.tight_layout() plt.show() # 调用 # plot_detailed_forecast(Y_val, Y_pred, quantiles[0.1,0.5,0.9])3.3 陷阱三用 RMSE/MAE 单一指标评估长期预测忽略业务敏感性在设备健康预测中低估故障时间比高估更致命。若模型把“剩余寿命 3 天”预测成“5 天”维护团队可能错过窗口但预测成“1 天”最多是提前备件。此时MAE无法区分方向性错误。必须引入Directional Accuracy (DA)和Threshold-based RecallDA预测变化方向上升/下降与真实方向一致的比例RecallΔt真实故障发生在t时刻若预测值在t-Δt时刻已超过安全阈值则计为召回。def compute_directional_accuracy(y_true, y_pred): y_true, y_pred: [N, L, D] # 计算相邻步变化符号1up, -1down, 0flat true_diff np.sign(np.diff(y_true, axis1)) # [N, L-1, D] pred_diff np.sign(np.diff(y_pred, axis1)) acc (true_diff pred_diff).mean() return acc def compute_recall_at_threshold(y_true, y_pred, threshold0.8, lookback24): y_true/y_pred: [N, L, D], 假设D1且故障由ythreshold触发 lookback: 提前多少步预警如24步24小时 # 找到每个样本真实首次超阈值时刻 true_alarm np.argmax(y_true threshold, axis1) # [N] true_alarm[true_alarm 0] y_true.shape[1] # 未超阈值设为最大步 # 预测首次超阈值时刻 pred_alarm np.argmax(y_pred threshold, axis1) pred_alarm[pred_alarm 0] y_true.shape[1] # 统计预测时刻 真实时刻 - lookback 即为成功召回 recall ((pred_alarm true_alarm - lookback) (true_alarm lookback)).mean() return recall # 示例 # da compute_directional_accuracy(Y_val, Y_pred) # DA 0.75 合格 # rec compute_recall_at_threshold(Y_val, Y_pred, threshold0.85, lookback48) # Rec48h业务校准建议threshold不是随意设的应基于设备手册的告警阈值lookback必须匹配现场维护响应时间如换轴承需 48 小时备件若Rec48h 0.6宁可降低预测精度也要提升召回——这是工业预测的铁律。4. 避坑指南Transformer 长期预测的 4 个高频翻车点与解法4.1 现象训练 loss 持续下降但验证 loss 在第 15 个 epoch 后开始震荡上升且幅度越来越大原因位置编码pos_embed未随序列长度扩展。当seq_len96时self.pos_embed[:, :96, :]正常但验证时若seq_len120因 padding 或不同设备长度切片越界导致pos_embed全零模型失去时序感知能力。解决初始化pos_embed时预留足够长度如nn.Parameter(torch.randn(1, 500, d_model))或改用相对位置编码Rotary Position Embedding但会增加复杂度首推前者。4.2 现象预测结果呈现“周期性条纹”即每隔 24 步预测值突然跳变与真实值严重偏离原因时间特征time_features中hour列未做周期性编码。模型看到hour23后接hour0认为是巨大跳跃而非平滑过渡。解决将hour拆为sin(hour*2π/24)和cos(hour*2π/24)两维同理处理day_of_week周期 7、month周期 12代码替换time_feats np.stack([np.sin(h*2*np.pi/24), np.cos(h*2*np.pi/24), ...], axis1)。4.3 现象GPU 显存占用稳定但训练速度极慢 1 iter/secnvidia-smi显示 GPU 利用率 10%原因数据加载瓶颈。DataLoader的num_workers设为 0Windows 默认或过小CPU 预处理跟不上 GPU 计算。解决Linuxnum_workers4Windowsnum_workers0避免 fork 问题但必须用pin_memoryTruenon_blockingTrue关键代码train_loader DataLoader(dataset, batch_size32, shuffleTrue, pin_memoryTrue, num_workers0 if os.name nt else 4) # forward 中 x_batch x_batch.cuda(non_blockingTrue)4.4 现象模型对测试集第一条样本预测完美但第二条开始全面漂移误差逐样本递增原因StandardScaler在训练集上拟合后未保存 scaler 对象导致测试时用scaler.transform()时内部均值/方差为 0数据未标准化。解决训练后joblib.dump(scaler, scaler.pkl)测试时scaler joblib.load(scaler.pkl)严禁在测试时重新fit_transform这是新手最高频的“玄学 bug”。5. 进阶技巧用注意力权重反向定位关键时间点让预测可解释Transformer 的最大优势不仅是精度更是可解释性。通过提取自注意力权重你能回答“模型预测未来高温主要依据哪几个历史时刻的传感器读数” 这对故障归因至关重要。5.1 提取最后一层 Encoder 的注意力权重修改TimeSeriesEncoder在forward中返回注意力权重class TimeSeriesEncoder(nn.Module): def __init__(self, ...): # ... 同前 # 修改 encoder_layer启用返回 attention weights encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, dim_feedforwardd_model*4, dropoutdropout, batch_firstTrue, return_attention_weightsTrue # PyTorch 2.1 支持 ) self.encoder nn.TransformerEncoder(encoder_layer, num_layersnum_layers) def forward(self, x, time_features): # ... 前处理同前 encoded, attn_weights self.encoder(x_embed) # attn_weights: [B, nhead, S, S] # 取最后一层、第一个 head 的权重用于分析 return pred, attn_weights[-1, 0] # [S, S]5.2 可视化注意力热力图识别关键历史时刻以下函数将注意力权重映射回原始时间轴标出 Top-K 关键步def plot_attention_heatmap(attn_weights, input_times, top_k5): attn_weights: [S, S] from last layer, first head input_times: list of datetime objects for input sequence (lenS) fig, (ax1, ax2) plt.subplots(1, 2, figsize(14, 5)) # 1. 全局热力图 im ax1.imshow(attn_weights, cmapBlues, aspectauto) ax1.set_title(Attention Weights (Last Layer, Head 0)) ax1.set_xlabel(Key Position (Historical Step)) ax1.set_ylabel(Query Position (Output Step)) plt.colorbar(im, axax1) # 2. 关键历史步分析对每个预测步找 top-k 最大注意力源 S attn_weights.shape[0] key_steps np.argsort(attn_weights, axis1)[:, -top_k:] # [S, K] # 统计所有预测步中哪些历史步被引用最多 all_keys key_steps.flatten() step_counts np.bincount(all_keys, minlengthS) # 画柱状图标出时间标签只标每24步 x_ticks np.arange(0, S, 24) ax2.bar(range(S), step_counts, alpha0.7, colorsteelblue) ax2.set_xticks(x_ticks) ax2.set_xticklabels([input_times[i].strftime(%m-%d %H) for i in x_ticks], rotation45) ax2.set_title(fMost Attended Historical Steps (Top-{top_k} per output)) ax2.set_xlabel(Historical Time Step) ax2.set_ylabel(Attention Count) plt.tight_layout() plt.show() # 使用示例 # _, attn model(x_batch[:1], t_batch[:1]) # 取第一个样本 # plot_attention_heatmap(attn[0].cpu().numpy(), input_timestamps)实战解读若step_counts[72]即输入序列第 72 步约 3 天前峰值最高说明模型认为“3 天前的振动突增”是预测当前高温的关键证据若step_counts[0]和step_counts[95]双峰说明模型同时关注“起始状态”和“最新趋势”符合物理直觉若权重均匀分布说明模型未学到有效模式需检查数据质量或增加层数。5.3 用注意力指导特征工程发现被忽略的强相关变量注意力不仅看时间步也看变量维度。在value_proj前可对输入做通道注意力Channel-wise Attentionclass ChannelAttention(nn.Module): def __init__(self, input_dim, reduction4): super().__init__() self.fc1 nn.Linear(input_dim, input_dim // reduction) self.fc2 nn.Linear(input_dim // reduction, input_dim) def forward(self, x): # x: [B, S, D] avg_pool torch.mean(x, dim1) # [B, D] att torch.relu(self.fc1(avg_pool)) # [B, D//reduction] att torch.sigmoid(self.fc2(att)) # [B, D] return x * att.unsqueeze(1) # [B, S, D] # 在 TimeSeriesEncoder.__init__ 中加入 self.channel_attn ChannelAttention(input_dim21) # 在 forward 中 x self.channel_attn(x) # 插入在 value_proj 前训练后检查self.channel_attn.fc2.weight若sensor_15对应权重显著高于其他说明该传感器对长期预测贡献最大——这可反向验证你的领域知识或暴露数据采集盲区。我坚持一个习惯每次调完模型必跑一次plot_attention_heatmap不是为了炫技而是确认模型“思考路径”是否符合物理常识。如果它总盯着无关紧要的深夜数据点那不是模型强是数据或标注出了问题。这种可解释性才是 Transformer 在工业预测中取代 LSTM 的真正护城河。希望帮到你。本文还有配套的精品资源点击获取