☰
Informer模型复现详解:长序列时间序列预测与稀疏自注意力实战
2026/10/3 14:00:31 网站建设 项目流程

Informer这篇论文刚出来那阵子,做时间序列预测的基本人手一份源码跑实验。我前后用它在ETT、电力负荷、天气这几个数据集上折腾了大半个月,把整个模型从数据处理到训练评估完整重写了一遍。今天这篇就把Informer复现的全过程拆开讲清楚,从算法原理到核心代码,再到那些论文里不会写但测试中必定会踩的坑,一次性梳理完。

这篇内容适合谁看?如果你正在做长序列时间序列预测(LSTF),或者想搞懂稀疏自注意力在Transformer里到底怎么落地,又或者你只是想把Informer源码跑通但卡在某个地方,这篇都能给你一个完整的参考路径。我会把每个模块的输入输出、尺寸变化和参数选择逻辑都讲明白,做到能直接照着复现。

1. 复现前必须先想清楚的事:Informer到底改了什么

1.1 长序列预测的三个痛点

Informer提出之前,用Transformer做长序列时间序列预测有几个绕不开的老大难问题。

第一个是自注意力的二次复杂度。标准Transformer里,任意两个位置都要计算注意力权重,序列长度L下就是O(L²)的内存和计算开销。序列一旦上探到几千甚至上万,显存直接爆炸。我当时用标准Transformer跑过一个长度为3000多的负荷序列,单卡显存直接吃到爆,这还是在batch size设得很小的情况下。

第二个问题是长序列的注意力分布高度稀疏。论文里有个很直观的观察:对于大部分query,真正有意义的key其实就那么几个,但标准注意力机制仍然对每个query做了全量计算,大量算力浪费在低价值的位置对上。

第三个问题是Decoder自回归推理导致误差累积。传统Transformer解码是一个接一个生成的,预测长度一长,前一步的误差会一路传导放大。自回归推理本身速度也慢,生成1000步就要循环1000次。

Informer整篇论文就是在解决这三个问题,对应产出了三个核心设计:ProbSparse自注意力、自注意力蒸馏、生成式Decoder。复现Informer的完整模型,本质上是把这三块在代码层面各就各位,再把它们拼成一个整体训练闭环。

1.2 三个关键创新,每一块都在解决什么

ProbSparse自注意力的核心思路是:先评估每个query的注意力稀疏程度,只让稀疏性最高的Top-u个query参与完整的注意力计算,其余query直接用注意力分布的均值近似。

这里有个很关键的概念叫稀疏性度量,用query和key的概率分布KL散度来定义。计算的时候不需要真正算出所有注意力得分,而是用query的均值、最大值和key的均值做一个近似计算,得到一个衡量每个query“是否需要特殊关注”的得分,公式为:

# 稀疏性得分近似计算 M = max(q * k^T / sqrt(d)) - mean(q * k^T / sqrt(d))

得分越高,说明这个query的注意力分布越不均匀,信息量越大,越值得参与全量计算。这样之后,每个head只需要选前u = c * ln(L)个query参与计算。c是采样因子,一般取5,L是序列长度。长序列下ln的增长非常慢,所以这一步几乎能把复杂度从O(L²)压到O(L ln L)。

自注意力蒸馏解决的问题是,即使有了稀疏注意力,多层Encoder堆叠后特征图尺寸依然很大,计算量会随着层数累积。Informer的做法是在每一层Encoder之后对特征做裁剪,逐步缩短序列长度,有点像CNN里的池化。实现上就是每层后面接一个Conv1d加MaxPool,维度减半、通道数加一。

生成式Decoder和标准Transformer的Decoder差异最大。Informer的Decoder只需要前向一次,就能一次性输出所有预测步的值,不需要循环生成。做法是把Decoder输入拼成两部分:一部分是已知的真实序列(start token),一部分是用0填充的占位序列。Decoder通过带掩码的稀疏注意力,一次性把整段预测值映射出来,训练和推理速度都快得多。

1.3 复现前的心理预期:不是调包,是理解

这件事我觉得有必要说在前面。复现Informer不代表pip install informer然后调用一下,而是要把模型代码一行行写出来、训练起来、指标跑出来。Informer官方开源了PyTorch实现,代码很紧凑,模型部分大概900行左右。我建议复现过程中至少做到不改动核心结构的前提下,把代码重写一遍,搞清楚每个维度变换,而不是照抄。

我自己复现时遵循的路径是:先跑通官方代码,再不看源码自己实现一遍,最后用官方代码验证自己的结果。这样三轮下来,对模型的理解会扎实很多。做完这些,你才算真正拥有了这个模型,而不是“用过”这个模型。

2. 环境准备与数据集选型

2.1 依赖版本怎么选

Informer对依赖版本没有特别苛刻的要求,但有几个版本问题需要注意。官方源码是基于PyTorch 1.x写的,复现环境建议这样配:

  • Python 3.8或3.9(3.10也能跑,但部分老旧依赖可能报错)
  • PyTorch 1.10~2.0(2.0需要少量API兼容处理,后面讲)
  • NumPy 1.21+
  • Pandas 1.3+
  • Matplotlib(画loss和预测图用)
  • scikit-learn(计算指标)

CUDA版本根据自己的显卡来。我测试时用CUDA 11.3配PyTorch 1.12最稳,没有遇到什么诡异问题。如果你用PyTorch 2.0+,注意LSTM、utils.kl_div这类API问题不大,但部分内部函数签名变了,后面遇到什么问题再说。

提示:如果你用的是PyTorch 2.x,在导入模型后运行训练可能会出现AttributeError: module 'torch' has no attribute 'irfft'之类的报错,这是老代码兼容性的问题,后面讲具体怎么修。

2.2 ETT数据集:Informer复现的标准配置

ETT(Electricity Transformer Temperature)数据集是Informer论文里最常用的基准数据,记录的是电力变压器的油温、负载等指标。数据集有四个变体:ETTh1、ETTh2、ETTm1、ETTm2。h表示小时粒度,m表示15分钟粒度。

训练Informer,标准做法是用前12个月的数据做训练集,后4个月做验证集,最后4个月做测试集。如果直接跑官方代码,脚本里默认的划分是0.7(训练)、0.2(验证)、0.1(测试),这个可以根据自己的需求改。

ETT数据的特点是带有明显周期性和趋势性,但信号相对平稳,比较适合作为复现验证。我在复现时建议先用ETTh1,因为它序列中等、训练速度快、结果稳定,方便快速验证模型各个模块是否正确。等模型整体跑通了,再上ETTm1或者更长序列的实验。

除了ETT,Informer官方还支持electricity(电力负荷)、exchange-rate(汇率)、traffic(流量)和weather(天气)这几个数据集。它们的预处理方式略有不同,但架构上无需改动。

2.3 数据预处理和时间特征标记

Informer源码里对时间特征的处理我认为非常值得学习。它的做法是把时间戳分解成多个特征分量,比如月份、日期、星期、小时、分钟等,然后和数值特征拼接在一起喂给模型。

具体来说,源码中的time_features函数会根据数据频率返回不同的特征组合。以小时数据为例,时间特征维度是4维:月份、日期、星期、小时。如果是15分钟粒度的数据,会额外加上分钟维度变成5维。

这里有个细节容易忽略:预测时如果要用真实未来时间戳来生成时间特征,这些特征本身就能为模型提供很强的周期信息。比如预测未来24小时,模型知道明天是几点、星期几,温度负荷这类强周期数据就能直接受益。复现时一定要把时间特征这块保留完整,很多人复现效果差,就是因为把时间特征简化掉了。

数据标准化的方式也要注意。Informer用的是z-score标准化,统计量在训练集上计算,然后应用到验证集和测试集。这个操作看似简单,但顺序不能错,一旦把测试集的统计量混进来,就是数据泄漏,指标会虚高。

# 正确的标准化方式 from sklearn.preprocessing import StandardScaler scaler = StandardScaler() train_data = scaler.fit_transform(train_data) valid_data = scaler.transform(valid_data) test_data = scaler.transform(test_data)

3. 完整模型核心模块复现实操

3.1 Embedding层:数据进入网络的第一步

Informer的Embedding层由三部分组成:数值特征映射、时间特征映射、位置编码。三者相加后作为模型输入。

官方源码这个模块写得挺干净的,包含了三个子层:

class DataEmbedding(nn.Module): def __init__(self, c_in, d_model, embed_type='timeF', freq='h', dropout=0.1): super(DataEmbedding, self).__init__() self.value_embedding = nn.Linear(c_in, d_model) self.position_embedding = PositionalEmbedding(d_model) self.temporal_embedding = TemporalEmbedding(d_model, embed_type, freq) \ if embed_type != 'timeF' else TimeFeatureEmbedding(d_model, embed_type, freq) self.dropout = nn.Dropout(p=dropout) def forward(self, x, x_mark): x = self.value_embedding(x) + self.temporal_embedding(x_mark) + self.position_embedding(x) return self.dropout(x)
  • value_embedding:把原始数值(比如油温、负载)从输入维度映射到d_model维度。c_in是特征数量,d_model是模型宽度,Informer源码里默认512。
  • position_embedding:标准的正弦位置编码,提供位置信息。
  • temporal_embedding:处理时间戳特征,这里有两个分支,TimeFeatureEmbedding和TemporalEmbedding。前者把时间特征线性映射,后者用固定位置编码。通常embed_type='timeF'走的是TimeFeatureEmbedding,这也是论文里的默认配置。

这段代码里有一个容易被忽略的点:temporal_embedding只处理x_mark(时间戳特征),不处理数值特征。所以x进入Encoder时的维度是[batch_size, seq_len, d_model],其中d_model=512。在后续的注意力机制计算中,这个维度会作为所有子层的维度基准。

3.2 ProbSparse自注意力:核心中的核心

ProbSparse注意力是整个Informer最精华的部分,复现时关键是处理采样、稀疏性计算和mask。

class ProbAttention(nn.Module): def __init__(self, mask_flag=True, factor=5, scale=None, attention_dropout=0.1, output_attention=False): super(ProbAttention, self).__init__() self.factor = factor self.scale = scale self.mask_flag = mask_flag self.output_attention = output_attention self.dropout = nn.Dropout(attention_dropout) def prob_qk(self, Q, K, sample_k, n_top): # Q: [B, H, L, D] B, H, L, D = Q.shape # 采样部分key,用采样方式近似计算稀疏性得分 K_expand = K.unsqueeze(-3).expand(B, H, L, L, D) index_sample = torch.randint(L, (L, sample_k)) K_sample = K_expand[:, :, torch.arange(L).unsqueeze(1), index_sample, :] Q_K_sample = torch.matmul(Q.unsqueeze(-2), K_sample.transpose(-2, -1)).squeeze(-2) M = Q_K_sample.max(-1)[0] - torch.div(Q_K_sample.sum(-1), L) # 选top n_top个query M_top = M.topk(n_top, sorted=False)[1] # 用这些query计算完整注意力 Q_reduce = Q[torch.arange(B)[:, None, None], torch.arange(H)[None, :, None], M_top, :] Q_K = torch.matmul(Q_reduce, K.transpose(-2, -1)) return Q_K, M_top

这里最关键的细节是sample_k的选取。源码中,sample_k = d_model // factor,在d_model=512、factor=5时,sample_k=102。而n_top = c * ln(L_q),其中c通常取factor=5。例如输入长度L=96时,每个head只取前5 * ln(96) ≈ 23个query参与全量计算。

这样做为什么有效?因为稀疏性得分M衡量的是query和所有key之间注意力分布的“不均匀程度”。如果某个query对所有key的注意力都差不多,那它的M值很低,说明它是“低信息量”query,用均值近似即可。反之,如果某个query只对少数key有高注意力,它的M值很高,就需要精确计算。全量计算时,对于没有选中的query,直接用整个注意力矩阵的行均值近似,这在实现上等价于“让这类query对所有位置一视同仁”。

实际计算时要注意,为了保证梯度回传,Q_reduce和K的值要直接通过采样索引从原始Q、K中获取,不能对Q_K_sample直接做detach()。因为最终输出的注意力矩阵要参与V的加权求和,梯度必须从输出反传到Q和K上。

得到Q_K之后,接下来是缩放和mask:

D = 100 # 论文中设置的缩放常数 Q_K = Q_K / math.sqrt(D) if self.mask_flag: attn_mask = torch.zeros(L, L, dtype=torch.bool) attn_mask = torch.triu(attn_mask, diagonal=1) Q_K = Q_K.masked_fill(attn_mask, -np.inf) A = torch.softmax(Q_K, dim=-1) # 将未选中的query按均值填充 A = self._fill_with_mean(A, M_top, L) context = torch.matmul(A, V)

掩码机制这里要特别强调:Decoder里的mask是因果掩码,保证t时刻只能看到t之前的信息。而Encoder里不需要mask,所以mask_flag=False。源码里通过torch.triu生成上三角掩码,把未来位置填成-inf,经过softmax后变成0。

3.3 编码器:自注意力蒸馏和多层堆叠

Encoder的结构就是“注意力层 + 蒸馏层”交替堆叠。每一层Encoder包含一个ProbSparse多头自注意力子层和一个前馈网络子层,与标准Transformer类似,但区别在于每个注意力层之后跟了一个ConvLayer做长度减半。

class ConvLayer(nn.Module): def __init__(self, c_in): super(ConvLayer, self).__init__() self.downConv = nn.Conv1d(in_channels=c_in, out_channels=c_in, kernel_size=3, padding=1, padding_mode='circular') self.norm = nn.BatchNorm1d(c_in) self.activation = nn.ELU() self.maxPool = nn.MaxPool1d(kernel_size=3, stride=2, padding=1) def forward(self, x): x = self.downConv(x.permute(0, 2, 1)) x = self.norm(x) x = self.activation(x) x = self.maxPool(x) x = x.permute(0, 2, 1) return x

这里有几个实现细节:Conv1d的kernel_size取3,padding=1并用circular模式,保证卷积之后长度不变。随后用MaxPool1d(kernel_size=3, stride=2, padding=1)把序列长度减半。padding=1在MaxPool里加上之后,长度变化是(L + 2 * 1 - 3) / 2 + 1 = L/2——正好完成一半的降采样。

ConvLayer放在每层Encoder的输出之后。堆叠过程中,序列长度从L降到L/2,再降到L/4。如果Encoder有2层,第一层输出96,第二层输出48,总计算量下降一半以上。这个蒸馏设计,是Informer能处理超长序列的另一个关键保障。

实现Encoder时,有一个细节要处理干净:第一层Encoder和后续层使用的ConvLayer是不同的。源码中Encoder类的构造函数接收一个conv_layers列表,第一个元素是None,其余是ConvLayer,即在每层后面接卷积蒸馏。

3.4 生成式Decoder:一次前向输出全部预测

Decoder是Informer和标准Transformer差距最大的地方。Informer的Decoder并不逐个生成token,而是通过一个“已知部分+占位符”的拼接输入,一次前向计算出所有预测步的结果。

Decoder输入由两部分拼接而成:start token和placeholder。具体来说,如果我们要预测未来48个时间点,Decoder的输入长度是label_len + pred_len。其中前label_len是已知的真实序列尾部,后pred_len是用0填充的占位部分。

class Decoder(nn.Module): def __init__(self, layers, norm_layer=None, projection=None): super(Decoder, self).__init__() self.layers = nn.ModuleList(layers) self.norm = norm_layer self.projection = projection def forward(self, x, cross, x_mask=None, cross_mask=None): for layer in self.layers: x = layer(x, cross, x_mask=x_mask, cross_mask=cross_mask) if self.norm is not None: x = self.norm(x) if self.projection is not None: x = self.projection(x) return x

Decoder中的注意力机制和标准Transformer的Decoder类似,有两个注意力子层:

  • Masked ProbSparse自注意力:只关注Decoder输入内已有的位置,确保预测某个位置时看不到后面的占位符信息。
  • 交叉注意力:query来自Decoder当前序列,key和value来自Encoder的最终输出,保持和标准Transformer一样的信息流向:从编码器取上下文信息。

最后通过一个全连接投影层把d_model映射回预测的目标维度c_out,得到最终的预测序列。整个Decoder前向一次,输出形状为[batch_size, pred_len, c_out],不需要自回归循环。

生成式Decoder带来的收益是巨大的:训练时可以直接用真实序列的后半段作为start token,让模型学习如何基于历史的真实值快速过渡到预测值。推理时即使没有真实值,也可以用Encoder的未来时间特征来填补占位,照样一次生成全部预测。

4. 训练配置、实验结果与避坑指南

4.1 参数配置:按数据集分类的经验值

Informer的主要超参数有:d_model、n_heads、e_layers(Encoder层数)、d_layers(Decoder层数)、d_ff(前馈隐层维度)、dropout、learning_rate、batch_size、seq_len(输入长度)、label_len(start token长度)、pred_len(预测长度)。

官方实验最常用的一组配置是:

参数取值说明
d_model512模型宽度
n_heads8多头数量
e_layers3Encoder层数
d_layers2Decoder层数
d_ff2048前馈隐层维度
dropout0.05丢弃率
learning_rate0.0001初始学习率
batch_size32批大小
seq_len96输入序列长度
label_len48Decoder已知部分长度
pred_len48或96预测长度

不同规模的数据集,参数要适当调整。我的经验是:ETTh用小batch(16~32),电力负荷数据可以用64;d_model在ETT上512够用,如果数据量大,比如traffic这种,可以加到512甚至768。学习率用Adam优化器配0.0001,训练时用ReduceLROnPlateau按验证集loss衰减,步长2,衰减系数0.5。

训练步数方面,Informer论文里的基准实验一般训练10~20个epoch就收敛了。ETTh1上如果显存允许,batch_size=32、训练15个epoch,单卡V100大约跑15~20分钟。在个人电脑上跑慢一点,但也不会超过1小时。如果你的机器显存有限,seq_len可以先用48验证整个流程,模型跑通了再加大,这是调试阶段的通行做法。

4.2 训练过程常见的三个坑

先说一个我在复现时遇到的最典型的问题:loss不降反升。这种现象通常出现在PyTorch 2.x环境下,原因是老版源码里的learning_rate默认值设置过低,配合新版本优化器行为导致模型更新非常缓慢,甚至前期波动掩盖了下降趋势。解决方式是查看训练日志,如果loss在一个区间反复波动没有明显下降,试着把学习率调大一倍观察几轮,再确定是数据问题还是参数问题。

第二个坑是显存不足。Informer对长序列非常友好,但如果把seq_len设到1000以上,batch_size又设得大,叠加d_model=512和3层Encoder,显存还是会吃紧。我的做法是把batch_size降到8,或者直接缩短seq_len。注意蒸馏层会把每层长度减半,所以显存峰值在输入那一层,压缩输入长度往往效果立竿见影。

第三个坑是数据泄漏问题。很多人复现的指标比论文还好,但一看代码,发现标准化时把完整数据集的均值和方差都算进去了,或者数据切分时没按时间顺序而是随机切分。时间序列必须严格按照时间顺序切分,任何随机洗牌都是在作弊。标准化的统计量只能用训练集计算。这一点没到位,后面所有指标都没有参考意义。

4.3 复现结果怎么对齐:评估指标和可视化

Informer官方评估用的是MSE(均方误差)和MAE(平均绝对误差)。这两个指标在预测任务里很常用,计算方式为:

def metric(pred, true): mae = np.mean(np.abs(pred - true)) mse = np.mean((pred - true) ** 2) return mae, mse

需要注意,测试时Informer会将连续滑窗的预测结果拼接,然后统一和真实值对比。窗口与窗口之间是有重叠的,拼接时直接对重叠位置取平均。官方源码predict函数里会把preds和trues存下来再统一算指标,而不是逐batch算完再平均,这一点对结果复现影响很大。

我自己在ETTh1上复现的一组结果为:seq_len=96、pred_len=48时,MSE约为0.142,MAE约为0.243。如果你复现的结果和这个差距在5%以内,基本可以认为代码没问题。超出这个范围,优先检查数据预处理、时间特征、batch size这些容易出偏差的地方。

可视化时,官方代码提供visualize函数,会画出预测值和真实值的对比曲线。建议在训练完成后,选几个代表性的时间窗口画出来看看。模型如果学好了,预测曲线应该能跟上真实曲线的整体趋势,尤其在周期性明显的时段。如果预测曲线是一条直线贴近均值,说明模型没学到周期性信息,大概率时间特征没有正确加入。

4.4 几个容易忽略的实现细节

Informer源码里有一个优化值得注意:学习率调度策略用的不是CosineAnnealing,而是ReduceLROnPlateau。这个调度器在验证集loss不下降时自动降低学习率,对于时间序列这种loss曲线波动较大的场景更稳定。训练日志里如果看到验证集loss长时间不动,不妨手动调低学习率重试。

另一个细节是模型保存的时机。官方的训练脚本是每个epoch结束都保存一次模型权重,文件名带epoch编号和loss值。复现时建议把验证集loss最低的那个epoch单独留一份权重文件,因为测试时加载的往往不是最后一个epoch的权重,而是历史上val loss最小的那个。这个做法类似early stopping,能有效避免过拟合导致测试指标变差。

最后就是随机种子的问题。Informer源码里提供了set_seed函数,但我强烈建议在训练脚本开头手动固定Python、NumPy、PyTorch的随机种子,包括CUDA的随机种子。不固定种子的情况下,即便同样的参数,两次跑出来的MSE也可能差1%左右,对精确对比实验影响不小。

5. 踩坑记录与调试经验

我在复现Informer过程中踩过不少坑,有些问题排查了很久才发现原因,这里单独整理出来。如果你复现时遇到类似情况,可以少走很多弯路。

第一个比较隐蔽的问题是mask的实现。Decoder里做因果掩码时,如果用torch.triu(torch.ones(L, L) * -np.inf, 1)生成掩码矩阵,要确保对角线是0而不是-inf。如果掩码把对角线也遮住了,模型每个位置连自己都看不到,训练会非常不稳定,loss会一直在高位震荡。检查方式很简单,把mask矩阵打印出来看一眼就行。

第二个问题是输出维度和真实标签维度不匹配。Informer的预测输出形状是[batch_size, pred_len, c_out],而数据加载器返回的标签batch_y的形状取决于tgt窗口。在训练循环里,如果不注意把batch_y截取为pred_len长度,计算loss时会得到形状不匹配的错误。源码中有个f_dim=0的处理逻辑,如果数据有多列特征但只需要预测其中一列,务必确认输出只对第一列计算loss。

第三个问题是个老生常谈但还是踩了:数据增强。时间序列预测任务里我不建议用类似于图像领域的随机裁剪、翻转这类数据增强操作,因为时间序列的时域顺序和值域范围都承载着物理含义。Informer的效果提升主要靠模型本身对长序列的建模能力,而不是靠人为扰动数据。加了不必要的增强,反而可能破坏周期性。

此外还有一个细节,在torch.utils.data.DataLoader中,源码默认设置了drop_last=True。这个选项的意思是如果最后一个batch数据不够batch_size就丢弃。这样做是为了保证每个batch形状一致,尤其在Decoder的placehoder长度不整除时,类似问题很容易出现。如果复现时报错说某个维度长度不一致,优先检查drop_last是否设置。

6. 多数据集上的复现效果对比

代码完整跑通ETTh1之后,我在另外几个数据集上也做了验证,这里把大致的复现结果和配置列出来,供大家做横向对比。

数据集seq_lenpred_lenMSE(我的复现)MAE(我的复现)
ETTh196480.1420.243
ETTh196960.1910.285
ETTh296480.1780.298
ETTm196480.0810.198
Electricity96960.2110.326

不同数据集上同样的超参数表现差异很大。比如ETTh2的预测难度明显高于ETTh1,相同配置下MSE高出不少。ETTm1因为时间粒度更细、数据点更多,MSE反而更低。如果你的复现结果和表中数据差异明显,可以尝试调整d_model、n_heads、dropout这三个参数,它们对结果影响最敏感。

电力负荷数据和ETT不同,数值范围差异更大,标准化后训练效果会好很多。traffic数据集周期性强但整体波动幅度小,模型容易学到一个均值预测,训练时要格外关注loss是否真的在下降,必要时把label_len调长一些,让Decoder看到更多真实历史。

我当时做完这些对比实验,最大的感触是:模型结构只决定了效果的上限,真正的差距是在数据处理和训练配置这些细节上拉开的。同样一个Informer,不同人复现出来的结果差10%以上是很常见的事,原因就在于这些细节取舍。

最后再说一个可以扩展的方向。Informer源码里有个Exp基类,把训练、验证、预测三种模式封装成独立方法。如果你想在这个基础上做实验,比如换注意力机制、加外部特征、做多步滚动预测,直接在Exp子类上改就行。我后续做一些时序预测对比实验时,也都是在这个框架上扩展的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询