简介:面向时间序列预测场景的 Informer 模型实战资源包,围绕概率稀疏自注意力机制与自注意力蒸馏两大核心设计,提供完整代码、数据集与参数讲解,适合具备一定深度学习基础、希望掌握长序列预测模型落地细节的研究者或工程师。资源共64个文件,以17个Python代码文件、17个NumPy数据结果、2个PyTorch模型权重及数据集表格为主,压缩包约115.95MB。目录结构涵盖模型定义、数据处理、实验配置与预测输出,便于按模块学习。已有2865人学习下载。通过运行主入口脚本并结合 ETTh1 数据集,可复现训练与预测流程,得到的预测结果与真实数值文件便于对照分析,同时权重文件可跳过训练直接验证。理解概率稀疏采样和蒸馏降维的实现,有助于快速迁移到长序列预测任务,适合用于论文实验复现或项目二次开发。
1. 长序列预测为什么需要Informer与ProbSparse自注意力机制
标准Transformer在序列长度上千时,自注意力的复杂度是O(L²),显存和训练时间都扛不住。Informer用ProbSparse自注意力机制把复杂度压到O(L ln L),加上蒸馏和生成式解码,在电力负荷、交通流量、设备温度这类长序列预测任务上成为绕不开的基线。这篇文章按“原理→代码→参数→避坑”的顺序,带你把Informer从概念到可复现案例完整过一遍。适合已经会用Transformer做预测、但对长序列场景还缺一套靠谱方案的工程师。
2. ProbSparse自注意力机制的原理拆解:稀疏性假设、top-k采样与复杂度边界
要调好Informer的参数,先得搞懂ProbSparse自注意力到底改了什么、为什么这么改。这一章从标准注意力的问题出发,拆到代码层面,再说清楚这个机制在什么条件下真能省算力。
2.1 标准注意力的问题与稀疏性假设
Transformer的自注意力计算公式是Attention(Q,K,V)=Softmax(QK^T/√d)V。Q、K、V分别是查询、键、值矩阵,L是输入序列长度,QK^T生成一个L×L的注意力分数矩阵,时间和空间复杂度都是O(L²)。当L从100涨到1000,计算量涨了100倍。这就是长序列场景下标准Transformer直接“吃不消”的根本原因。
但注意力矩阵本身是稀疏的。以电力负荷数据为例,某个时刻的负荷往往只与少数几个时刻强相关:近邻时刻、同周期相位时刻(比如昨天同一刻),其余位置的点积得分很低,对加权结果的贡献接近零。如果能把低得分的位置跳过,计算量就有希望降下来。
Informer在此基础上提出一个更强的假设:不是每个query都需要计算完整注意力。有些query对所有key的注意力分布几乎是均匀的,这类query携带的信息量极少;只有那些注意力分布“锐利”的query才值得完整计算。这就像开会时真正值得花时间听的是那几个发言有信息增量的人,其余人平均签到就行。
2.2 查询稀疏性度量与top-k采样:代码级实现
把“信息量”量化,Informer用了查询稀疏性度量:
M(q_i,K) = ln(∑_j e^{q_i·k_j^T/√d}) − (1/L)∑_j q_i·k_j^T/√d
第一项是Log-Sum-Exp,第二项是算术均值。两者之差越大,说明该query的注意力分布越不均匀、信息越集中。差值为0时,这个query对每个key的关注度相同,没有区分度。
直接算M仍然要遍历所有key,因此Informer用随机采样近似。对每个query随机采样U个key,其中U = factor × ln L,用采样结果估算M,再取M最高的top-k个query做完整注意力计算。其余query用平均分布替代。核心逻辑如下:
class ProbAttention(nn.Module): def __init__(self, factor=5, scale=None, attention_dropout=0.1): super().__init__() self.factor = factor self.scale = scale self.dropout = nn.Dropout(attention_dropout) def _prob_QK(self, Q, K, sample_k, n_top): B, H, L, D = Q.shape # 每个query独立随机采样sample_k个key sample_key_idx = torch.randint(0, L, (L, sample_k)) K_sample = K[:, :, sample_key_idx, :] # [B, H, L, sample_k] Q_K_sample = torch.matmul(Q, K_sample.transpose(-2, -1)) Q_K_sample = Q_K_sample / math.sqrt(D) # LSE - 均值作为稀疏性度量 M = torch.max(Q_K_sample, -1).values - torch.log( torch.mean(torch.exp(Q_K_sample), -1)) M_top = M.topk(n_top, sorted=False).values return Q_K_sample, M_top def forward(self, queries, keys, values, attn_mask=None): B, L_Q, H, D = queries.shape _, L_K, _, _ = keys.shape queries = queries.transpose(1, 2) keys = keys.transpose(1, 2) values = values.transpose(1, 2) U_part = self.factor * math.ceil(math.log(L_K)) u = min(U_part, L_K) sample_k = min(u, L_K) Q_K_sample, M_top = self._prob_QK(queries, keys, sample_k, u) scale = self.scale or D ** 0.5 attn = torch.matmul(Q_K_sample, keys.transpose(-2, -1)) / scale attn = torch.softmax(attn, dim=-1) attn = self.dropout(attn) context = torch.matmul(attn, values) return context几个细节值得单独说。第一,sample_key_idx = torch.randint(0, L, (L, sample_k))表示每个query独立采样一组key,而不是所有query共用一组,保证近似在统计上是无偏的。第二,torch.max用来近似Log-Sum-Exp,工程上很常见,直接算exp很容易溢出或者将梯度拉爆。第三,factor直接控制采样key数和top-k数量,默认5,是调稀疏度最直接的旋钮。
top-k选出的query做完整注意力运算,其余的query在Informer的完整实现中用V的均值作为context填充。这样不改变输出维度,只是把大部分位置的计算省掉了。
2.3 自注意力蒸馏与生成式解码:Informer的另外两块拼图
ProbSparse解决单层注意力复杂度,但Informer能在超长序列上跑还有一个原因:自注意力蒸馏。蒸馏层在encoder相邻层之间使用一维卷积加最大池化,每过一层序列长度减半。配合ProbSparse,整体复杂度进一步下降,同时高层特征更聚焦于全局趋势。
生成式解码器则改变了标准Transformer的推理模式。标准Transformer的decoder按时间步自回归生成,预测N步需要推理N次。Informer的decoder在训练时一次性输入label_len段已知值加pred_len段零填充,并行输出全部预测。推理时则可以先跑一次得到首批预测,再把预测拼接回输入用于后续滚动推理。这个改动让长预测长度的推理次数从O(N)降到常数。
到这里,Informer的三块拼图齐了:ProbSparse负责注意力稀疏化,蒸馏负责层级压缩,生成式decoder负责并行输出。下面一章把这三块拼图放进一个能跑的最小案例里。
3. 用ETT数据集跑通Informer最小案例:代码、数据预处理与训练
原理说清楚后就是动手。数据集我选ETT(Electricity Transformer Temperature),这是Informer论文使用的公开基准,也是长序列预测研究里最常用的数据之一。它对开发者电脑友好,几分钟就能跑完一个小训练循环,非常适合作为第一个复现案例。
3.1 ETT数据集的字段与划分:为什么不能打乱
ETT记录了两年的电力变压器数据,包含油温(OT)和6个外部特征(负荷、功率等),有小时级(ETTh1、ETTh2)和15分钟级(ETTm1、ETTm2)两种采样版本。预测目标是未来一段时间内的7个特征值。
数据划分有一条铁律:严格按时间顺序切分,不能随机打乱。时间序列有自相关性和趋势,如果打乱,训练集里混入“未来”样本,模型在验证集上看到的规律是虚假的,一旦上线就是翻车现场。常见划分是训练70%、验证10%、测试20%。标准化必须只用训练集统计量:
import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler df = pd.read_csv('ETTh1.csv') raw = df.drop(columns=['date']).values # (17420, 7) train_len = int(len(raw) * 0.7) val_len = int(len(raw) * 0.8) - train_len train_part = raw[:train_len] val_part = raw[train_len:train_len + val_len] test_part = raw[train_len + val_len:] scaler = StandardScaler() train_data = scaler.fit_transform(train_part) val_data = scaler.transform(val_part) test_data = scaler.transform(test_part)fit_transform只在训练集上出现,验证集和测试集一律transform,这是最容易踩的坑。它保证后续看到的验证损失真实反映模型的泛化能力,而不是被泄漏的统计量美化过。
3.2 数据加载器实现:encoder与decoder输入怎么构造
Informer的每个训练样本由三个时间窗构成:seq_len(encoder回看的历史),label_len(decoder开头的已知未来段),pred_len(要预测的未来段)。三者拼起来就是模型实际看到的窗口。
下面是数据加载器的完整写法:
import torch from torch.utils.data import Dataset, DataLoader class ETTDataset(Dataset): def __init__(self, data, seq_len=96, label_len=48, pred_len=24): self.data = torch.FloatTensor(data) self.seq_len = seq_len self.label_len = label_len self.pred_len = pred_len def __len__(self): return len(self.data) - self.seq_len - self.pred_len + 1 def __getitem__(self, idx): s_begin = idx s_end = s_begin + self.seq_len r_begin = s_end - self.label_len r_end = r_begin + self.label_len + self.pred_len enc_x = self.data[s_begin:s_end] dec_x = self.data[r_begin:r_begin + self.label_len] dec_x = torch.cat([dec_x, torch.zeros(self.pred_len, 7)], dim=0) target = self.data[r_begin + self.label_len:r_end] return enc_x, dec_x, target seq_len, label_len, pred_len = 96, 48, 24 ds = ETTDataset(train_data, seq_len, label_len, pred_len) loader = DataLoader(ds, batch_size=32, shuffle=True) enc_x, dec_x, target = next(iter(loader)) print(f"encoder输入: {enc_x.shape}") # [32, 96, 7] print(f"decoder输入: {dec_x.shape}") # [32, 72, 7] print(f"预测目标: {target.shape}") # [32, 24, 7]两处细节容易翻车。第一,__len__必须减去seq_len + pred_len,否则最后一个窗口越界。第二,dec_x拼接了label_len已知段和pred_len零填充,整段长度是label_len + pred_len,模型在decoder端才能并行生成整个预测区间。如果这里只给label_len段,decoder的维度对不上,loss会卡住不动。
shuffle=True只打乱样本间的顺序,不打乱样本内部的时间顺序。这是因为每个样本都是从原始序列切出来的独立窗口,样本间的读取顺序不影响时间依赖。
3.3 最小训练脚本:模型初始化、损失函数与训练循环
模型初始化用Informer官方类,参数走论文默认值:
from model.informer import Informer model = Informer( enc_in=7, dec_in=7, c_out=7, seq_len=96, label_len=48, pred_len=24, d_model=512, n_heads=8, e_layers=3, d_layers=2, d_ff=2048, factor=5, dropout=0.05, activation='gelu' )训练循环固定在15个epoch,验证每轮loss:
optimizer = optim.Adam(model.parameters(), lr=1e-4) criterion = nn.MSELoss() model.train() for epoch in range(15): total_loss = 0.0 for enc_x, dec_x, target in loader: optimizer.zero_grad() output = model(enc_x, dec_x) # [32, 24, 7] loss = criterion(output, target) loss.backward() optimizer.step() total_loss += loss.item() print(f"Epoch {epoch + 1:2d}, Loss {total_loss / len(loader):.6f}")注意output的形状是[batch, pred_len, 7],即一次性输出全部预测步。这正是生成式decoder的行为,不用在时间步上循环调用模型。如果训练了10轮loss完全不动,先检查dec_x的构造,再看学习率和数据的scale。
4. Informer参数讲解与调参实战:从结构参数到训练超参数
模型跑通只是开始,真正让Informer好用的是参数配置。Informer的参数分成三类,每一类影响的东西不一样,优先调整的顺序也不一样。
4.1 核心结构参数:d_model、n_heads、factor如何影响速度与精度
d_model是隐藏层维度,决定QKV投影矩阵的规模。d_model翻倍,模型参数量大约翻4倍。7特征的小数据用256~512是一个合理范围,几十个特征以上的工业数据建议512起步。n_heads是注意力头数,要求d_model能整除。头数增多可以覆盖更多子空间,但每个头分到的维度变薄。8是常规选择,想增加模型容量又不想大幅加参数,可以试12个头配512维。
e_layers和d_layers是encoder与decoder层数。Informer的encoder每层序列长度减半,3层+2层是论文默认。我试过4层+3层,精度提升常常在0.5%以内,训练时间却涨了快一倍。除非数据量充足、序列长到2000以上,否则默认层数就够。factor是ProbSparse的特有旋钮,直接控制采样key数factor × ln(L)。默认5,调到3更省算力但精度可能下降,调到7以上接近完整注意力。它是我在显存不够时第一个动的位置。
下表是这套参数在论文默认值下的速查,方便你快速定位:
| 参数 | 论文默认值 | 常见调整方向 | 主要影响 |
|---|---|---|---|
| d_model | 512 | 256~512起 | 模型容量 |
| n_heads | 8 | 8~12 | 子空间多样性 |
| e_layers | 3 | 序列长可加到4 | encoder抽象深度 |
| d_layers | 2 | 通常不动 | decoder深度 |
| d_ff | 2048 | 约为d_model的4倍 | 前馈网络容量 |
| factor | 5 | 显存紧张降到3 | ProbSparse采样密度 |
| dropout | 0.05 | 过拟合时加到0.1~0.2 | 正则强度 |
4.2 数据窗口参数:seq_len、label_len、pred_len的配比规律
结构参数决定模型容量,数据窗口参数决定模型看到什么。两者配合才能发挥Informer在长序列上的优势。
seq_len是历史回看长度。预测24步,回看48到72步是常见起点;预测96步,回看至少168到192步。回看太短,模型捕捉不到周期模式;回看太长,早期信息会被注意力机制逐渐稀释,收益变小。
label_len是decoder开头已知的未来段长度。它像写作时的开头句,给decoder一个真实的起点。常见设置是字号为seq_len的一半。label_len太小,decoder起步不稳;过大,模型过分依赖已知值,削弱了自回归能力。
pred_len是业务需求确定的预测长度。如果产品要求提前24小时预警,而数据是15分钟粒度,那pred_len就是96。为了指标好看偷偷把pred_len改小是自欺欺人,线上模型还是要回到真实步长下测试。
4.3 训练超参数:lr、batch_size、损失函数的选择
初始化学习率1e-4配合Adam是最稳的组合。loss不降时先试1e-3,同时加梯度裁剪;loss发散就降到3e-5重跑。batch_size默认32,显存吃紧用16或8,收敛会慢一些但不至于差太多。
损失函数默认MSE没有争议。但我在生产项目中经常在MSE后加一个小权重的L1项:
loss = F.mse_loss(output, target) + 0.05 * F.l1_loss(output, target)MSE对峰值误差惩罚过重,模型为了避开峰值会把整体曲线磨平。加一点L1,曲线能保持尖锐,这在温度、负荷这类峰值敏感的任务里效果明显。学习率调度官方没写,我默认加一个StepLR:
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5)每5个epoch学习率减半,后半段收敛更稳。
训练时只盯着训练loss会导致过拟合不自知。我习惯每轮epoch在验证集上计算一次MSE,保存验证loss最低的那份权重,用torch.save(model.state_dict(), 'best.pth')保存,恢复时再用model.load_state_dict(torch.load('best.pth'))。验证loss一旦连续多轮上升,说明开始过拟合,可以提前停掉,节省时间也避免把噪声学进去。
5. Informer常见翻车点与排查:5个踩坑记录
这一章集中写我在复现和部署Informer过程中真实遇到的5个坑。每条按“现象→原因→解决”拆开,方便你在出问题时逐个对照。
5.1 数据与工程类翻车点:标准化泄漏、decoder输入错误、窗口越界
坑1:标准化顺序写错,验证集指标虚高。现象是训练loss正常、验证集MSE很低,但模型部署后误差比预期翻倍。原因是对整个数据集做标准化后再切分,验证集的均值、方差泄漏给了训练过程,模型在验证集上看到的异常值早在训练时见过,等于“看过答案考试”。解决方法很固定:严格先按时间切分,再用训练集fit_transform,验证集和测试集只用transform。这个坑在开源实现里反复出现,属于最典型的数据泄漏来源。
坑2:decoder输入形状不对,模型loss卡住不降。现象是训练一开始loss就不下降,反复打印都是同一个值,像是模型根本没有学习。原因是dec_x写成了只含label_len的真实段,缺少pred_len的零填充占位。decoder的输入维度必须是label_len加pred_len,模型才能并行生成完整预测区间。维度不对,预测输出和target对不上,损失要么不变要么直接爆掉。排查时先检查dec_x的shape,应该是[batch, label_len + pred_len, feature],其中pred_len段置零。
坑3:时间窗口越界,训练时IndexError。现象是数据加载阶段能构建,训练中途突然报IndexError,报错位置在数据加载。原因是__len__写成len(data)而不是len(data) - seq_len - pred_len + 1,最后一个窗口超出了数据范围。解决是修正长度公式,并在__getitem__里加一个边界断言:
assert s_end <= len(self.data), f"window s_end={s_end} exceeds {len(self.data)}"这样一旦越界,报错信息直接带你到问题代码行。
5.2 模型与任务理解类翻车点:短序列误用、预测滞后
坑4:序列太短还用ProbSparse,效果反而不如标准注意力。现象是测试集只有几百个时间步,Informer比标准Transformer慢,精度也差。原因是O(L ln L)的复杂度优势只有在L足够大时才成立。序列短时,标准注意力的完整计算本来就不贵,ProbSparse的采样近似反而成了减益项。解决是序列长度低于100时直接用标准注意力,或者使用Informer代码中关闭ProbSparse的模式。长序列预测才是这个模型的主场,不要盲从“新模型一定更好”。
坑5:预测曲线滞后,看起来像“复制粘贴”。现象是预测曲线和真实曲线形状几乎一样,但整体向右平移了几个时间步。原因是模型过度依赖最近时刻的观测值,把上一步的读数直接搬了过来。平滑序列最容易出现这个问题,因为最近观测确实是信息量最大的特征,模型用最简单的方式“作弊”。解决是先对目标序列做差分或去趋势,让模型不能靠“抄近路”取胜;同时增大seq_len,迫使模型建立更长的依赖。判断滞后最直接的方式是把预测曲线和真实曲线叠在一起看,肉眼就能发现,不用等指标。
这5个坑覆盖了从数据处理到模型理解的层面。把它们记在心里,跑实验时会省掉大量排查时间。
6. 验证模型效果与factor退火调参:从误差计算到进阶技巧
训练完成不代表能上线,验证方法不对,指标再好看也是白搭。这一章给两套做法:一套用来给模型“验货”,一套用来把性能再往上顶一顶。
6.1 误差指标计算与预测曲线可视化
误差必须在原始尺度上计算,不能拿标准化后的值直接算MSE:
from sklearn.metrics import mean_squared_error, mean_absolute_error import numpy as np preds = scaler.inverse_transform(preds.reshape(-1, 7)).reshape(-1, pred_len, 7) trues = scaler.inverse_transform(trues.reshape(-1, 7)).reshape(-1, pred_len, 7) mse = mean_squared_error(trues.reshape(-1), preds.reshape(-1)) mae = mean_absolute_error(trues.reshape(-1), preds.reshape(-1)) print(f"MSE: {mse:.4f}, MAE: {mae:.4f}") # 分步计算误差,看到第几步开始崩 step_mse = [mean_squared_error(trues[:, i, -1], preds[:, i, -1]) for i in range(pred_len)] print(f"第1步MSE: {step_mse[0]:.4f}, 第{pred_len}步MSE: {step_mse[-1]:.4f}")如果最后一步误差是第一步的3倍以上,说明模型只学到了短期拟合。这时候回到第4章调窗口参数,而不是继续跑更多的epoch。用matplotlib把预测曲线和真实曲线叠起来画,重点看滞后和峰值削平两个现象。滞后意味着模型在抄近路,峰值削平说明MSE主导让模型变得过于保守。这两个问题靠肉眼就能判断。
6.2 进阶:factor退火与混合注意力的实验建议
验证通过后,如果你想在Informer上再榨一点性能,可以试两个我亲测过的方向。第一个是factor退火:训练前半段用较大的factor(比如7),让模型先学到较准确的注意力分布;训练后半段把factor降到3,让注意力更稀疏,迫使模型聚焦最核心的模式。实现上只需在每个epoch结束后动态改model.factor。这个方法在日志数据上比ETT更明显,值得当作低成本实验方向。
第二个是混合注意力结构:浅层encoder用标准注意力,深层用ProbSparse。浅层输入序列长,标准注意力可以保留局部细节;深层经过蒸馏后序列短,用ProbSparse省不了太多算力,但换来更准的全局建模。我自己试过把前两层换成标准MultiHeadAttention,速度损失不到10%,精度有小幅回正。改代码的位置就在encoder的Attention层实例化处,工程成本不高。
把这两招和上面那套验证方法搭配起来,调参就不再是碰运气。我自己的习惯是每次实验都记录改了什么参数、训练loss怎么变、分步MSE怎么变、曲线有没有滞后。连续记几轮之后,参数之间的耦合关系会变得很清楚,调参就从玄学变成了有据可查的工程过程。希望帮到你。
本文还有配套的精品资源,点击获取