简介:本资源是一份面向深度学习初学者与时间序列分析实践者的完整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_model=128, nhead=4, num_layers=3, dropout=0.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_model=d_model, nhead=nhead, dim_feedforward=d_model*4, dropout=dropout, batch_first=True ) self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_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)矩阵; - 滑动窗口切片:设输入长度
S=96,预测长度L=72,则每个样本为[i:i+96, :]→ 输入,[i+96:i+96+72, :]→ 标签; - 时间特征生成:为每个时间点计算
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_len=96, pred_len=72, stride=12): """ 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] ], axis=1) # (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:i+seq_len]) Y.append(values_scaled[i+seq_len:i+seq_len+pred_len]) T.append(time_feats[i:i+seq_len]) return np.array(X), np.array(Y), np.array(T) # 使用示例(PHM2012 示例数据) # df = pd.read_csv("phm2012_train.csv", parse_dates=['timestamp'], index_col='timestamp') # X, Y, T = create_sequences(df, seq_len=96, pred_len=72, stride=24) # print(f"X 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)参数说明:
stride=24表示每 24 步取一个新样本,平衡数据量与显存;seq_len=96对应 4 天(若数据为每小时一条),足够捕获日周期;pred_len=72即预测未来 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_dim=21, d_model=128, nhead=4, num_layers=3) optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-5) scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'min', patience=5, factor=0.5) early_stopper = EarlyStopping(patience=15, min_delta=1e-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_norm=1.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_decay=1e-5比默认1e-2更适合时序,防止 attention 权重过度稀疏;clip_grad_norm_=1.0必加,否则pos_embed参数更新时梯度爆炸;- 学习率
1e-4是起点,若 loss 下降慢,可试5e-4;若震荡,降到5e-5。
3. 长期预测的三大陷阱与可视化验证:别让模型骗了你
3.1 陷阱一:用“单步预测+滚动”代替“多步端到端”,导致误差雪崩
新手最常犯的错误:训练时只预测下一步(pred_len=1),推理时用pred_t+1作为t+1的输入,再预测t+2……如此滚动 72 次。这会导致:
- 现象:验证集上 1 步 MAE=0.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_len=72固定,训练即学出 72 步联合分布。
3.2 陷阱二:可视化只画“预测曲线 vs 真实曲线”,掩盖系统性偏差
下图是某次失败实验的典型可视化:
曲线贴合度高,但一算指标:MAE=0.12,MSE=0.025,sMAPE=8.3%—— 似乎不错?错。当你按时间分段统计误差,发现:
| 时间段 | MAE | 误差方向 |
|---|---|---|
| 00:00-06:00 | 0.31 | 系统性低估 15% |
| 12:00-18:00 | 0.08 | 基本无偏 |
| 21:00-23:00 | 0.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, quantiles=None): """ 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], label=f'True-{i}', alpha=0.7) axes[0].plot(y_pred[i, :, 0], '--', label=f'Pred-{i}', alpha=0.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, ax=axes[1], cmap='RdBu_r', center=0) 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), alpha=0.3, label='10%-90%') axes[2].plot(q50.mean(0), 'k-', label='Median') axes[2].plot(y_true[:, :, 0].mean(0), 'r:', label='True Mean') axes[2].set_title(f"Quantile 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 Recall:
- DA:预测变化方向(上升/下降)与真实方向一致的比例;
- Recall@Δt:真实故障发生在
t时刻,若预测值在t-Δt时刻已超过安全阈值,则计为召回。
def compute_directional_accuracy(y_true, y_pred): """y_true, y_pred: [N, L, D]""" # 计算相邻步变化符号:1=up, -1=down, 0=flat true_diff = np.sign(np.diff(y_true, axis=1)) # [N, L-1, D] pred_diff = np.sign(np.diff(y_pred, axis=1)) acc = (true_diff == pred_diff).mean() return acc def compute_recall_at_threshold(y_true, y_pred, threshold=0.8, lookback=24): """ y_true/y_pred: [N, L, D], 假设D=1且故障由y>threshold触发 lookback: 提前多少步预警(如24步=24小时) """ # 找到每个样本真实首次超阈值时刻 true_alarm = np.argmax(y_true > threshold, axis=1) # [N] true_alarm[true_alarm == 0] = y_true.shape[1] # 未超阈值设为最大步 # 预测首次超阈值时刻 pred_alarm = np.argmax(y_pred > threshold, axis=1) 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, threshold=0.85, lookback=48) # Rec@48h业务校准建议:
threshold不是随意设的,应基于设备手册的告警阈值;lookback必须匹配现场维护响应时间(如换轴承需 48 小时备件);- 若
Rec@48h < 0.6,宁可降低预测精度,也要提升召回——这是工业预测的铁律。
4. 避坑指南:Transformer 长期预测的 4 个高频翻车点与解法
4.1 现象:训练 loss 持续下降,但验证 loss 在第 15 个 epoch 后开始震荡上升,且幅度越来越大
原因:位置编码pos_embed未随序列长度扩展。当seq_len=96时,self.pos_embed[:, :96, :]正常;但验证时若seq_len=120(因 padding 或不同设备长度),切片越界导致pos_embed全零,模型失去时序感知能力。
解决:
- 初始化
pos_embed时预留足够长度(如nn.Parameter(torch.randn(1, 500, d_model))); - 或改用相对位置编码(Rotary Position Embedding),但会增加复杂度,首推前者。
4.2 现象:预测结果呈现“周期性条纹”,即每隔 24 步,预测值突然跳变,与真实值严重偏离
原因:时间特征time_features中hour列未做周期性编码。模型看到hour=23后接hour=0,认为是巨大跳跃,而非平滑过渡。
解决:
- 将
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), ...], axis=1)。
4.3 现象:GPU 显存占用稳定,但训练速度极慢(< 1 iter/sec),nvidia-smi显示 GPU 利用率 < 10%
原因:数据加载瓶颈。DataLoader的num_workers设为 0(Windows 默认)或过小,CPU 预处理跟不上 GPU 计算。
解决:
- Linux:
num_workers=4;Windows:num_workers=0(避免 fork 问题),但必须用pin_memory=True+non_blocking=True; - 关键代码:
train_loader = DataLoader(dataset, batch_size=32, shuffle=True, pin_memory=True, num_workers=0 if os.name == 'nt' else 4) # forward 中: x_batch = x_batch.cuda(non_blocking=True)
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_model=d_model, nhead=nhead, dim_feedforward=d_model*4, dropout=dropout, batch_first=True, return_attention_weights=True # PyTorch 2.1+ 支持 ) self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_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_k=5): """ attn_weights: [S, S] from last layer, first head input_times: list of datetime objects for input sequence (len=S) """ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5)) # 1. 全局热力图 im = ax1.imshow(attn_weights, cmap='Blues', aspect='auto') 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, ax=ax1) # 2. 关键历史步分析:对每个预测步,找 top-k 最大注意力源 S = attn_weights.shape[0] key_steps = np.argsort(attn_weights, axis=1)[:, -top_k:] # [S, K] # 统计所有预测步中,哪些历史步被引用最多 all_keys = key_steps.flatten() step_counts = np.bincount(all_keys, minlength=S) # 画柱状图,标出时间标签(只标每24步) x_ticks = np.arange(0, S, 24) ax2.bar(range(S), step_counts, alpha=0.7, color='steelblue') ax2.set_xticks(x_ticks) ax2.set_xticklabels([input_times[i].strftime('%m-%d %H') for i in x_ticks], rotation=45) ax2.set_title(f"Most 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 Attention):
class ChannelAttention(nn.Module): def __init__(self, input_dim, reduction=4): 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, dim=1) # [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_dim=21) # 在 forward 中 x = self.channel_attn(x) # 插入在 value_proj 前训练后,检查self.channel_attn.fc2.weight,若sensor_15对应权重显著高于其他,说明该传感器对长期预测贡献最大——这可反向验证你的领域知识,或暴露数据采集盲区。
我坚持一个习惯:每次调完模型,必跑一次plot_attention_heatmap,不是为了炫技,而是确认模型“思考路径”是否符合物理常识。如果它总盯着无关紧要的深夜数据点,那不是模型强,是数据或标注出了问题。这种可解释性,才是 Transformer 在工业预测中取代 LSTM 的真正护城河。
希望帮到你。
本文还有配套的精品资源,点击获取