Informer长序列预测实战:ProbSparse注意力与蒸馏调参全攻略

📅 发布时间:2026/10/11 21:46:42
Informer长序列预测实战:ProbSparse注意力与蒸馏调参全攻略
简介针对长序列时间序列预测需求这份实战资源围绕ICLR 2020 Best Paper的Informer模型展开提供完整代码、数据集与参数讲解并关注传统Transformer在长序列上的效率问题适合具备基础深度学习知识、希望上手Transformer变体模型的读者。压缩包共64个文件约115.95MB以Python脚本.py、NumPy数据.npy、编译缓存.pyc、CSV数据表、PyTorch模型权重.pth及项目配置为主其中py文件覆盖数据加载、模型构建ProbSparse注意力、编码器/解码器与训练推理主流程pth文件为预训练检查点npy与csv文件则保留预测结果及ETTh1等时序数据便于读者对照验证。目前已有2060人学习使用评价良好。通过该案例读者可系统了解Informer在长序列预测中的ProbSparse自注意力机制与自注意力蒸馏设计学会使用个人数据集替换原有数据并根据参数配置文件调整预测长度、编码器层数等关键超参快速跑通训练与测试流程。1. 长序列预测的算力黑洞Informer 给出了一个反直觉的答案做过长序列预测的人都体会过这种无力感Transformer 能从 96 个历史点预测未来 24 步甚至 48 步但序列一长自注意力的计算量按序列长度的平方增长。显存先撑不住然后训练时间翻倍最后精度还不一定比得过调好的 LSTM。Informer 在 2021 年拿出的思路是反的——它不追求让每个位置都去关注全局而是只让少数信息量大的位置做全局注意力复杂度从 O(L²) 降到 O(L log L)在 ETTh1、ETT m2 这类长时间序列上把预测误差压了下去。这篇实战笔记用电力变压器温度ETT数据集把 Informer 跑通一遍从数据处理、模型初始化到训练参数和调参踩坑全覆盖代码可直接照抄适合刚接触长序列预测或者想在时序模型里做基线对比的从业者。2. Informer 结构拆解ProbSparse 注意力与蒸馏带来的 O(L log L) 复杂度2.1 为什么长序列预测需要稀疏注意力Transformer 的二次复杂度瓶颈传统自注意力对每个 query 都要和所有 key 计算点积再经过 softmax 得到权重。序列长度 L1000 时注意力矩阵就有 100 万个元素L10000 时直接到一亿。这个 O(L²) 的空间复杂度和时间复杂度让 Transformer 在长序列预测任务上成了一个算力无底洞。更关键的问题是多数注意力权重是浪费的。Informer 论文里做过一个统计实验在长序列场景下经过 softmax 后的注意力权重分布呈现出明显的长尾特性只有一小部分 query 的注意力得分具有主导性——它们能匹配到少数几个重要的 key其余 query 的注意力得分几乎均匀分布不携带有效信息。这一现象意味着与其让模型把算力平均撒在每个位置上不如先判断哪些 query 值得做全局注意力。ProbSparse 注意力机制就是把这个直觉变成了一个可计算的稀疏性度量。2.2 从注意力得分分布看 ProbSparse 选点逻辑Top-u 采样与稀疏性度量ProbSparse 注意力的核心是给每个 query 算一个稀疏性得分然后只取分数最高的 Top-u 个 query 参与标准的 QK 注意力计算其余 query 直接用全局均值池化的结果填充省掉它们那部分点积运算。稀疏性度量用 KL 散度刻画M(qi, K) ln( Σj exp(qi·kjᵀ / √d) ) - (1 / L) · Σj (qi·kjᵀ / √d)这个式子的含义是某个 query 的注意力分布与均匀分布差异越大说明它越能选中少数几个关键 key信息量越大。实际实现里不会真的计算完整的 KL 散度再排序那样复杂度又回去了。常见做法是随机采样一部分 key 子集来估计每个 query 的稀疏性得分再取 Top-u。u 由采样因子 factor 决定默认取 u factor · ln(L)factor 默认 5。输入长度 96 时u 大约是 22也就是说只有约 1/4 的 query 做完整注意力计算量自然降下来了。import torch import torch.nn as nn import math class ProbSparseAttention(nn.Module): def __init__(self, d_model, n_heads, factor5): super().__init__() self.d_model d_model self.n_heads n_heads self.d_k d_model // n_heads self.factor factor self.w_q nn.Linear(d_model, d_model) self.w_k nn.Linear(d_model, d_model) self.w_v nn.Linear(d_model, d_model) def forward(self, q, k, v): B, L, _ q.shape # 采样 key 子集估算每个 query 的稀疏性得分 u self.factor * int(math.log(L)) u max(min(u, L), 1) idx torch.randperm(L)[:u] k_sample k[:, idx, :] q self.w_q(q).view(B, L, self.n_heads, self.d_k).transpose(1, 2) k_sample self.w_k(k_sample).view(B, u, self.n_heads, self.d_k).transpose(1, 2) # 稀疏性得分近似 scale 1.0 / math.sqrt(self.d_k) score torch.einsum(bhld,bhmd-bhlm, q, k_sample) * scale m score.max(dim-1).values - score.mean(dim-1) # 取 top-u 的 query 做完整注意力 idx_top m.topk(u, dim-1).indices # 完整注意力结果与均值填充结果合并 # 伪代码示意完整实现需按 head 维度展开 return m代码说明这个类展示的是 ProbSparse 注意力的骨架逻辑。先随机采样 u 个 key计算稀疏性得分 m再取 Top-u 的 query。实际工程实现里Top-u 的 query 要做完整的点积注意力未被选中的 query 则用 V 的均值广播填充最后把两部分拼回去。采样因子 factor 直接控制计算量调大到 8 或 10 会保留更多 query 做完整注意力精度可能略升但训练时间也变长是 Informer 参数调优里最需要权衡的超参数之一。2.3 Informer 的编码器-解码器骨架与蒸馏操作降采样如何保住主干特征Informer 整体还是编码器-解码器架构但它做了三处针对长序列的改动ProbSparse 注意力解决二次复杂度问题自注意力蒸馏用来缩短编码器每层输出的序列长度把相邻位置的信息合并让高层特征更聚焦全局生成式解码器则让解码器一次输出全部预测点而不是像传统 Transformer 那样逐点自回归。蒸馏操作发生在编码器层层之间。每一层做完 ProbSparse 注意力后输出序列会通过一个一维卷积加一个最大池化卷积核宽度为 3、步长为 2池化核为 2序列长度减半。e_layers3 且输入长度 96 时经过三层蒸馏后编码器输出长度依次变为 48、24、12既减少了后续层的计算量又迫使模型在高层保留更全局的语义。如果你的任务是预测未来 48 步编码器最后输出的 12 个特征向量仍然要能撑起整个预测这也是为什么蒸馏层数太多会让预测结果退化。下面是一份常见开源实现的模型初始化代码用来对照理解各部分参数含义from informer import Informer model Informer( enc_in7, # 编码器输入特征数ETT 的 7 个电力负荷特征 dec_in7, # 解码器输入特征数通常与 enc_in 一致 c_out1, # 预测目标维度这里只预测油温 OT seq_len96, # 编码器输入历史窗口长度 label_len48, # 解码器 start token 长度一般取 seq_len 的一半 pred_len24, # 要预测的未来步数 d_model512, # 特征嵌入维度 n_heads8, # 多头注意力头数d_model 必须能被 n_heads 整除 e_layers3, # 编码器层数每层带一次蒸馏 d_layers2, # 解码器层数一般 2 到 3 层就够 d_ff2048, # 前馈网络隐层维度 dropout0.05, attnprob, # prob 是 ProbSparsefull 是标准注意力 factor5, # ProbSparse 采样因子 u factor * ln(L) distilTrue, # 是否启用编码器蒸馏 )参数说明d_model 和 n_heads 决定单层注意力的参数量和计算量d_model 从 512 降到 128 能把显存占用降一半多适合先在 CPU 或低显存环境上跑通流程。e_layers 与 distil 必须一起看distilTrue 时每层都会把长度减半层数太多会让编码器输出的语义密度过高反而丢失短周期细节distilFalse 时每一层保持相同长度更像普通 Transformer准确做法是把 e_layers 降到 2 配合 distilTrue。c_out1 时模型只输出油温预测如果后续要把负荷特征一起预测把 c_out 改成 7损失函数也要相应改成多输出。3. 数据准备ETT 数据集下载与滑窗样本构造3.1 ETT 数据集结构与归一化为什么用 MinMax 而不是 Z-ScoreETTElectricity Transformer Temperature是 Informer 论文使用的公开数据集采集自电力变压器包含油温OT和六个电力负荷特征HUFL、HULL、MUFL、MULL、LUFL、MUTL。ETTh1 和 ETTh2 是每小时采样ETTm1 和 ETTm2 是每 15 分钟采样。特征语义很一致但数值范围差异大负荷值可能上千油温只有几十必须做归一化。归一化方法我一般选 MinMax 而不是 Z-Score原因是 Infor忙系列的输出层默认不接激活函数回归目标通常是 [0,1] 或 [-1,1] 区间内的值。Z-Score 会把数据变成均值为 0、标准差为 1 的正态分布预测结果还要手工逆变换且逆变换后误差会被放大。MinMax 把所有特征压到 [0,1]训练时 MSE 的量级更稳定逆变换也只需乘一个缩放因子。要注意 MinMax 的坑min 和 max 必须只从训练集计算验证集和测试集直接用训练集的边界做变换不能用整个数据集算。否则测试集的数值范围相当于提前泄露给了模型评估结果会显得虚高。import pandas as pd # ETTh1.csv 格式date, HUFL, HULL, MUFL, MULL, LUFL, MUTL, OT df pd.read_csv(ETTh1.csv) print(df.columns) print(df.head()) # 按时间顺序切分训练 / 验证 / 测试 train_df df.iloc[:int(len(df) * 0.7)] val_df df.iloc[int(len(df) * 0.7):int(len(df) * 0.8)] test_df df.iloc[int(len(df) * 0.8):] feature_cols [HUFL, HULL, MUFL, MULL, LUFL, MUTL, OT] # 只在训练集上拟合 MinMax 变换 train_min train_df[feature_cols].min() train_max train_df[feature_cols].max() def minmax_scale(frame, minv, maxv): return (frame[feature_cols] - minv) / (maxv - minv 1e-6) train_scaled minmax_scale(train_df, train_min, train_max) val_scaled minmax_scale(val_df, train_min, train_max) test_scaled minmax_scale(test_df, train_min, train_max)代码说明加 1e-6 是为了防止某个特征在训练集里是常数时出现除零。官方常用划分是 12 个月数据按 7:2:3 切分或者按 12/4/4 个月切分具体比例不影响最终结论。关键点是 fit 和 transform 分开验证集和测试集一定要用 train_min 和 train_max。3.2 从原始 CSV 到训练样本滑动窗口与标签对齐代码单独一行数据不能直接喂给模型。Informer 的输入是 (batch, seq_len, enc_in) 的历史窗口输出是 (batch, pred_len, c_out) 的未来序列。滑窗采样时要注意对齐第 i 个样本的输入是 [i, iseq_len) 区间的所有特征标签是 [iseq_len, iseq_lenpred_len) 区间的油温序列。中间不能有重叠和间隙。import numpy as np import torch from torch.utils.data import TensorDataset, DataLoader def create_samples(scaled_df, seq_len96, pred_len24): data scaled_df.astype(np.float32).values total len(data) - seq_len - pred_len 1 x np.zeros((total, seq_len, data.shape[1]), dtypenp.float32) y np.zeros((total, pred_len), dtypenp.float32) for i in range(total): x[i] data[i : i seq_len] y[i] data[i seq_len : i seq_len pred_len, -1] # 最后一列是 OT return x, y train_x, train_y create_samples(train_scaled) val_x, val_y create_samples(val_scaled) test_x, test_y create_samples(test_scaled) train_dataset TensorDataset(torch.from_numpy(train_x), torch.from_numpy(train_y)) val_dataset TensorDataset(torch.from_numpy(val_x), torch.from_numpy(val_y)) test_dataset TensorDataset(torch.from_numpy(test_x), torch.from_numpy(test_y)) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue) val_loader DataLoader(val_dataset, batch_size64, shuffleFalse)逻辑说明标签 y 取的是窗口内所有特征里最后一列 OT也就是油温。因为这里做单变量预测c_out1所以标签维度是 (样本数, pred_len)。如果要做多变量预测y 的维度改成 (样本数, pred_len, 7)并且第 4 章的损失函数要跟着改。这里的性能隐患在循环样本数量大时Python 层 for 循环建数组会非常慢。样本数两万时还能忍到十万以上就要用卷积方法或无重叠滑窗替代。我一般先跑通功能再用 torch.as_strided 或滑动窗口视图优化不要在数据集准备阶段过早优化。DataLoader 的 shuffle 只在训练集开验证和测试集必须按时间顺序评估打乱顺序会破坏序列依赖关系。4. 训练循环与关键参数从配置文件到损失函数4.1 训练循环代码decoder 输入的 label shifting 是最容易写错的地方Informer 的 decoder 输入不是简单的历史窗口它由两部分拼接一部分是历史窗口最后 label_len 步的真实值另一部分是 pred_len 步的零占位符。前一部分在论文里叫 start token相当于告诉解码器“上一段真实序列是什么样的”后一部分占位符让解码器一次吐出整个预测序列而不是逐点自回归。import torch import torch.nn as nn device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size3, gamma0.7) criterion nn.MSELoss() seq_len, label_len, pred_len 96, 48, 24 best_val_loss float(inf) for epoch in range(15): model.train() train_loss 0.0 for x, y in train_loader: x, y x.to(device), y.to(device) optimizer.zero_grad() # 构造 decoder 输入真实值尾部 48 步 零填充 24 步 start_token x[:, -label_len:, :] # (B, 48, 7) zero_pad torch.zeros((x.size(0), pred_len, x.size(2)), devicedevice) # (B, 24, 7) dec_inp torch.cat([start_token, zero_pad], dim1) # (B, 72, 7) output model(x, dec_inp) # (B, 72, c_out) pred output[:, -pred_len:, 0] # 取预测段 OT loss criterion(pred, y) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() train_loss loss.item() * x.size(0) # 验证 model.eval() val_loss 0.0 with torch.no_grad(): for x_val, y_val in val_loader: x_val, y_val x_val.to(device), y_val.to(device) start_token x_val[:, -label_len:, :] zero_pad torch.zeros((x_val.size(0), pred_len, x_val.size(2)), devicedevice) dec_inp torch.cat([start_token, zero_pad], dim1) out_val model(x_val, dec_inp)[:, -pred_len:, 0] val_loss criterion(out_val, y_val).item() * x_val.size(0) scheduler.step() if val_loss best_val_loss: best_val_loss val_loss torch.save(model.state_dict(), best_informer.pt)参数说明label_len 取 48 时 decoder 输入总长 72由原论文推荐区间。clip_grad_norm 设 1.0 是为了防止长序列训练中梯度爆炸尤其是 d_model 较大时很有效。StepLR 每 3 个 epoch 把学习率乘以 0.7整套配置跑 15 轮左右就能看到验证集 loss 在下降。训练时模型输出长度为 label_len pred_len预测时只取最后 pred_len 段前面的 start token 段不参与损失计算。很多第一次跑 Informer 的人把 dec_inp 直接传成整个 x或者只传零占位符这两种做法都会让验证 loss 居高不下。前者等于把标签泄露给解码器后者让解码器没有真实值可参考。start token 的真实长度也值得单独调label_len 增大到 72 时预测结果会更平滑但滞后现象更明显减小到 24 时模型需要更多依赖编码器信息长期趋势捕获变难。4.2 关键超参数表与 informer 调参主线informer 调参不是玄学主线是先把数据侧的 seq_len、pred_len 和 label_len 定下来再调模型容量最后调 factor 和 dropout。一套能直接上手的基线参数如下参数默认值调节方向对结果的影响seq_len96长序列预测通常要 168 或 336越长捕获周期越完整训练耗时线性增加label_len48约为 seq_len 的一半太长引入延迟太短解码器依赖编码器pred_len2424/48/168 三档常用预测越远越难误差随 pred_len 增大d_model512数据量大可升 1024量小降 256模型容量与显存占用e_layers3配 distilTrue 时 2~4 层层数增加输出序列被反复减半d_layers22~3 层足够解码器过深反而难收敛factor54~8越大参与注意力的 query 越多精度略升但更慢dropout0.050.03~0.1过大导致欠拟合长序列任务尤其敏感learning_rate1e-4预热后 5e-5~3e-4太高容易在注意力层产生 NaN5. Informer 避坑手册五个高频翻车现场与排查5.1 训练损失 NaN注意力得分出现 Inf 的三种原因现象训练第 2 到第 5 个 epoch 时loss 突然变成 NaN验证集同步崩溃检查数据和标签没有脏值。原因最常见的是学习率过高ProbSparse 注意力里的点积得分经过 scale 后数值偏大softmax 反向传播出现梯度爆炸第二个原因是 d_model 与 n_heads 不匹配比如 512 配 6 个头d_k 不是整数维度对齐出错后矩阵乘出 NaN第三个原因是序列里有极端值归一化时 min 和 max 跨度太大虽然经过 MinMax 但某些窗口仍然出现异常高位。解决先把学习率降到 3e-5如果依然 NaN打印注意力得分统计看是否有绝对值超过 20 的点积值。再检查 d_model % n_heads 是否为 0。数据集层面把归一化改成 RobustScaler 或对 OT 序列做差分也能避开极端尖峰。5.2 预测结果是一条水平线蒸馏把信息全丢了现象训练过程 loss 正常下降但测试集可视化预测曲线几乎是一条水平直线只有局部微小波动。原因e_layers 设得太大比如 4 层distilTrue 时每层长度减半96 的输入经过 4 层蒸馏后只剩 6 个特征向量短周期波动信息被池化彻底抹平。解码器拿到的是高度抽象的特征只能还原出均值附近的平稳预测。解决把 e_layers 降到 2或把 distil 关掉一层常见实现里 distil 是逐个模块的开关。同时检查 label_len 是否太小start token 不足会让解码器缺少近期的真实变化轨迹。水平线问题优先看这两处。5.3 显存 OOMbatch_size 与蒸馏输出长度的关系现象batch_size 32 时训练正常改成 64 后 OOM但报错位置不在注意力层而在解码器末端。原因Informer 的显存占用不只由输入长度决定decoder 的输出长度为 label_len pred_len且每层的注意力分数都会保留用于反向传播。batch_size64 时decoder 输出张量 (64, 72, 512) 乘以 2 层解码器再乘多头注意力的中间状态瞬间把显存顶满。解决batch_size 回退到 48 或 32优先保 d_model 不降。如果 batch 必须要大就把 d_ff 从 2048 降到 1024这一步对显存的削减比降 d_model 更直接。还有一个技巧是开启梯度累积每 4 个 batch 累计一次 backward等效于 batch_size 翻倍但显存不变。5.4 评估指标比 ARIMA 差pred_len 与 seq_len 的匹配问题现象用 96 个历史点预测未来 168 步MSE 和 MAE 都比单纯用 ARIMA 差预测曲线整体滞后两个采样周期。原因预测步长越长模型需要从更远的历史中捕获周期信息。seq_len96 只能覆盖 4 天的每小时采样数据而电力负荷有明显的周周期性7 天周期意味着至少需要 168 个历史点。输入窗口覆盖不到一个完整周期时Informer 学到的只是短时惯性不如统计模型。解决把 seq_len 从 96 提到 336label_len 同步提到 168d_model 保持 512 不变。这一改动会让训练时间翻三倍左右但预测误差通常能明显下降。如果数据是 15 分钟采样的 ETTm336 个点只覆盖 84 小时远不够一周这种情况需要 seq_len672 甚至更多。5.5 验证集 loss 回升但测试集还不错早停与模型选择现象验证集 loss 在第 8 个 epoch 开始回升测试集结果却比第 7 个 epoch 更好实际部署时又不稳定。原因验证集划分如果紧邻训练集两者的数据分布存在短期重叠后期 epoch 模型过拟合训练集时验证集 loss 对过拟合的敏感度反而下降。也可能是验证集样本量太小ETT 的验证集只有几千个样本波动大loss 回升可能是噪声。解决用早停时不要只盯最小 val_loss要记录每个 epoch 在测试集上的表现选择测试集上最稳定的一段模型。常见做法是把 best 模型的保存条件改成“连续 3 个 epoch 验证集 loss 下降才覆盖”并在最后一次学习率衰减后额外跑 5 个 epoch。部署时如果推理结果抖动再回头检查训练集和验证集切分处是否做过差分或标准化处理。6. 多步滚动验证与多变量输出上线前的两条可靠路径验证 Informer 是否真正学会了趋势而不是在记忆训练集的尾巴我习惯做滚动预测用 96 步窗口预测未来 24 步后把窗口向后滑动 24 步再用真实值继续预测。这样连续滚 10 次每段预测误差独立计算比单次测试集评估更能暴露模型在长周期上的能力。滚动时如果发现第 4 段之后误差快速累积通常是 seq_len 不够或者 start token 真实值被模型过度依赖。多变量输出方面把 c_out 从 1 改成 7标签从 (B, pred_len) 换成 (B, pred_len, 7)损失函数得同时改。MSE 在每个特征维度上独立计算但七个特征量纲不同即使归一化后OT 的波动幅度也远大于负荷特征模型会把大部分容量花在拟合油温上量级大的特征误差绝对值更大。我一般会先训练 c_out1验证流程跑通后再尝试多变量并且多变量模型必须配合特征维度的加权 MSE比如给 OT 增加 0.5 的权重其他特征各 0.25不然负荷通道基本学不动。最后提醒一个最容易忽略的部署细节训练时用的 train_min 和 train_max 必须序列化保存预测服务里每次进来一个新窗口要先按旧边界做 MinMax 变换再喂给模型。很多人训练和推理各写一套数据预处理训练时 fit_transform推理时重新 fit遇到比训练集更大的极端值预测直接偏到界外。我自己最早跑 Informer 就在 decoder 输入上翻过车把 start token 长度搞错验证集 loss 怎么调都下不去后来把 label shifting 画出来才彻底弄懂。希望帮到你。本文还有配套的精品资源点击获取