1. 先弄懂Informer为什么被造出来
1.1 长序列预测的瓶颈到底卡在哪
做时间序列预测的人都知道,模型性能的瓶颈往往不在“能不能拟合”,而在“能拟合多长”。
传统RNN/LSTM在处理长序列时存在两个天生问题:一是梯度在反向传播中越来越弱,长距离依赖基本学不到;二是预测时只能一步一步往前往后推,每步都依赖上一步的输出,误差就像滚雪球一样越滚越大。你把预测长度拉到几百步以后,模型输出的曲线看起来像“一根直线”,这种问题在很多实际业务里真的太常见了。
Transformer出现以后,自注意力机制能够直接建模任意两个时间点之间的关系,长短距离一视同仁,理论上把上面两个问题都解决了。但代价也很直接:标准自注意力的复杂度是序列长度的平方,也就是 O(L²)。假设序列长度是1000,那么注意力矩阵就是1000×1000,光这一块就要占掉几个GB的显存。看起来很美,实际工程里根本不敢把序列拉太长。
需要说明的是,基于我的经验,这个问题不能靠“把序列切短”来回避。很多真实业务里,例如电力负荷预测、设备温度监控、交通流量预测,周期和趋势往往跨度很大。你把序列切短,等于把关键信息也一起丢掉了。所以行业内真正缺的是一个“序列拉长后依然跑得动、还能保持精度”的模型。
1.2 Informer 的核心思路和创新点
Informer就是围绕这个大前提设计出来的。它仍然是Transformer结构,但在三个关键环节做了改造:
- 用ProbSparse自注意力机制代替标准自注意力,把复杂度从 O(L²) 压到 O(L log L)。
- 用自注意力蒸馏机制,让编码器在堆叠过程中不断压缩序列长度,进一步降低内存占用。
- 用生成式解码器一次前向输出整个预测序列,而不是像标准解码那样逐步自回归生成。
这三个改造单独看每一个都有道理,合在一起才是真正解决长序列预测问题的完整方案。我个人的理解是,Informer并没有推翻Transformer,而是把Transformer里那些“实际计算价值不高”的部分替掉,保留真正有用的上下文建模能力。
1.3 什么时候该选 Informer,什么时候不该选
选型这件事,我觉得比跑通模型更重要。
Informer适合的场景大概有这几个特征:序列长度长(几百到几千步)、预测步长也长(几十到几百步)、数据带有明显的周期性和趋势性、多变量之间还存在相互依赖。比如电力系统一天的负荷曲线、城市的交通流量、工厂设备的传感器读数,这些场景都很典型。
反过来,如果你的任务只是预测未来几步,序列长度也不长,那完全没必要上Informer。举个例子,做网络延迟的在线预测,一般用最近几十个点就够了,这种场景下LSTM甚至简单的统计模型可能更轻、更稳。Informer的蒸馏和解码器设计是为了长序列服务的,在小序列上反而可能显得笨重。
还有个很现实的建议:无论模型选什么,都要先和简单的基线模型对比一下。比如预测值直接取最近一个观测值,或者用简单指数平滑。如果复杂模型连这种基线都跑不过,那问题多半不在模型,而在数据或任务定义上。这个习惯能帮你省下大量无用功。
2. 核心机制逐层拆解:ProbSparse、蒸馏、一步解码
2.1 ProbSparse自注意力:为什么可以只选一部分query算
标准自注意力要计算每个 query 和所有 key 的点积,然后做softmax。问题在于,长序列里大部分注意力权重其实集中在少数几个关键位置上。换句话说,绝大多数 query 和 key 的匹配是弱相关甚至无关的,它们对最终特征的贡献近似均匀分布,基本可以忽略不计。
ProbSparse自注意力要解决的就是“如何把这些少数关键query挑出来”。论文里用了一个稀疏度度量,判断依据是query注意力分布和均匀分布的KL散度。公式大致是这样:
[ M(q_i, K) = \ln \sum_{j=1}^{L} e^{q_i k_j^\top / \sqrt{d}} - \frac{1}{L} \sum_{j=1}^{L} q_i k_j^\top / \sqrt{d} ]
这个值越大,说明这个query的注意力分布和均匀分布差异越明显,也就是它对某些key特别“敏感”,信息量更高。实际计算时,我们只需要保留 top-u 个这样的query,用它们参与完整注意力计算。u 的取值通常是 c·ln L,c 是超参数,实验里常用5到20之间。
这样改造后,注意力计算从全量变成部分,复杂度近似降到 O(L log L)。在实际复现中你会发现,训练和推理的提速非常明显,尤其是序列长度从512拉到1000以上时,差距就不是百分比的级别了,而是几倍到几十倍。
2.2 自注意力蒸馏:编码器一层层把序列压短
ProbSparse解决了单层注意力计算太大的问题,但编码器通常要堆很多层,每层的输出序列长度仍然很长。为了进一步压缩,Informer在编码器层与层之间加入了蒸馏操作。
蒸馏的过程不复杂,本质上就是先用一维卷积对序列做一次变换,再通过池化把长度减半。典型操作可以写成这样:
[ X_{j+1} = \text{MaxPool}(\text{ELU}(\text{Conv1d}(X_j))) ]
每经过一次蒸馏,序列长度就减少一半。这样设计的目的是:注意力已经提取了重要的时间点特征,池化把次要位置压缩掉,只保留主要模式。如果你有4层编码器,经过3次蒸馏后序列长度就只剩原来的八分之一,解码器拿到的是一次高度浓缩的特征表示。
我在实际项目中调整蒸馏层数时发现一个规律:不是压得越狠越好。如果原始序列本身就比较短,或者数据里的高频细节很重要,过度蒸馏会让模型丢失局部信息,预测曲线的细节会变得很粗糙。这种情况更适合减少蒸馏次数,或者在蒸馏时把卷积的stride调小。
2.3 生成式解码器:一次前向输出全部预测
标准Transformer做序列预测时,解码器是自回归的:先预测第1步,把第1步的结果拼到输入中再预测第2步,以此类推。这种方式慢且不说,还容易让前面的预测误差一直向后传导,导致长距离预测质量快速恶化。
Informer的解码器思路完全不同。它一次性接收两部分输入:一部分是真实历史数据,称为“起始标记”;另一部分是预测步长对应的占位符。解码器在内部用掩码遮住未来信息,一次前向就能输出整个预测序列。
这种方法的好处非常直观:推理速度大幅提升,因为不再需要循环解码;同时误差不会逐级累积,长预测的稳定性明显更好。我跑过对比实验,在同一份数据上,采用一步生成方式训练的模型,在预测步长达到168时,效果仍然稳定,而对应的自回归版本早就开始漂移了。
2.4 三个机制组合起来后的效果
单独看每个机制,可能觉得都是工程优化,但组合起来效果是乘法级的。
ProbSparse把每层注意力算力降下来,蒸馏把层间序列长度降下来,生成式解码器把推理步数降下来。三个机制同时作用,让Informer在长序列场景下真正变得可用。官方实验里,在多个公开数据集上做长序列预测时,Informer的推理速度比当时的自回归Transformer快了几十倍,内存占用也显著降低。
当然也要客观说一句:这三个机制给模型带来了不少超参数,比如稀疏因子、蒸馏层数、起始标记长度,这些都增加了调参成本。模型不是开箱即用的完美工具,而是需要你结合数据去调优的工程方案。
3. 模型结构细节与关键参数实录
3.1 从数据到向量:输入编码怎么做
Informer的输入不只是原始数值序列,它还会把时间信息编码成特征一起送进模型。具体来说,一个训练样本通常包含三部分:目标变量的历史窗口、对应的时间戳特征(比如小时、星期、月份)、以及可选的外部协变量。
时间戳特征非常重要。我做过一个电力负荷预测项目,如果不加小时特征,模型很难区分不同时段的负荷差异;加上之后,预测准确率提升非常明显。原因也很简单:注意力机制本质上是在找不同时间点之间的关系,如果模型不知道两个时间点分别是一天中的哪个时段,那它很难理解电力负荷的早晚周期变化。
编码层做法一般是对数值序列做linear embedding,对时间戳做embedding,然后和位置编码加在一起。需要注意,embedding维度建议和d_model保持一致,这样后续网络结构不需要额外的维度变换。
3.2 输入标准化和窗口划分的坑
这里我要踩过的坑分享一下:标准化处理一定要基于训练集来算均值和方差,千万不能拿全量数据去算。很多人图省事,直接对整份数据做标准化,这在时间序列里属于典型的数据泄露。
你想一下,测试阶段模型拿到的数据分布应该和训练阶段一致,但如果标准化时已经看到了测试数据的均值和方差,模型相当于“偷看”了未来信息,评估结果就会虚高。上线部署后,真实环境的数据分布可能完全不同,模型一下就现原形了。
窗口划分也是同理。时间序列数据不能像普通分类任务那样随机打乱,必须按时间顺序切分训练集和验证集。验证集必须是训练集之后的一段连续数据,这样才能模拟真实预测场景。有些论文报告的效果复现不出来,很多就是因为数据切分泄露了未来信息。
3.3 解码器里的起始标记到底多重要
生成式解码器虽然是一步到位,但不代表它不需要历史信息。恰恰相反,解码器开头那段真实历史值,也就是起始标记,是预测质量的关键。
我在实验中发现,起始标记长度至少要达到预测长度的一半,否则模型很容易预测出“无意义的平均值”。如果你想预测168步,起始标记长度建议设置在168或以上。这个规律背后的逻辑是:模型需要足够的真实上下文来“锚定”当前状态,如果上下文太短,就等价于让模型凭空推断未来,再强的模型也扛不住。
训练阶段通常的做法是从历史序列中随机截取一段作为起始标记,这样可以让模型适应不同的初始状态,泛化能力更好。推理阶段则直接使用最近一段历史数据。
3.4 常用超参数配置参考
这里给一份我实际使用的起步配置,适合大多数中等规模的时间序列数据,但不代表最优:
| 参数名 | 参考值 | 说明 |
|---|---|---|
| 输入序列长度 | 96~720 | 取决于数据周期,建议包含至少一个完整周期 |
| 预测序列长度 | 24~168 | 业务需求决定,增长会增大难度 |
| d_model | 512 | 嵌入维度,小数据可以降到256 |
| attention_factor | 5~20 | 控制稀疏query数量,显存紧张时调小 |
| encoder_layers | 3~4 | 层数越多表达能力越强,也越容易过拟合 |
| decoder_layers | 2 | 解码器通常不需要太多层 |
| n_heads | 8 | 注意力头数,一般和d_model匹配 |
| dropout | 0.05~0.1 | 小数据建议小dropout |
| batch_size | 32~128 | 根据显存调整 |
| 优化器 | AdamW | 配合warmup效果更好 |
| 学习率 | 1e-4 ~ 5e-4 | 过大容易不收敛,建议warmup |
这几个参数不是拍脑袋定的,而是长期实践中比较平稳的区间。实际调整时,建议一次只动一个参数,不要多个一起改,否则很难定位是哪个变化带来的效果提升。
4. 完整实操流程与实现要点
4.1 数据准备与训练样本构建
在开始写模型之前,先把数据准备好。这里给出一个Python里常用滑窗方式构建样本的示例,假设变量名为data:
import numpy as np def make_samples(data, input_len, pred_len): xs, ys = [], [] for i in range(len(data) - input_len - pred_len): x = data[i : i + input_len] y = data[i + input_len : i + input_len + pred_len] xs.append(x) ys.append(y) return np.array(xs), np.array(ys) # 假设 data 是经过标准化处理的二维数组: [时间步, 变量数] # input_len 为历史窗口长度,pred_len 为预测长度这里有三点值得注意。
第一点,样本之间重叠很严重,这会导致相邻batch的样本高度相似,训练时更容易收敛,但验证时要特别注意独立切分,不要用同一段数据既训练又验证。
第二点,如果数据有强周期,窗口长度尽量包含整数个周期。比如电力数据有日周期,那么窗口至少覆盖24个点;如果有周周期,最好覆盖168个点。这样模型看到的是一个完整的周期轮廓。
第三点,时间戳特征要和数值序列同步构建。比如小时、星期这些离散信息,先转成整数,再在模型里做embedding。
4.2 ProbSparse注意力的实现要点
很多初学者第一次看论文时会被公式劝退,但实现核心其实不复杂。这里给一个简化的PyTorch版ProbSparse注意力实现,用于理解主要逻辑:
import torch import torch.nn as nn import math class ProbSparseAttention(nn.Module): def __init__(self, d_model, n_heads, sampling_factor=5): super().__init__() self.d_head = d_model // n_heads self.sampling_factor = sampling_factor def forward(self, q, k, v): # q/k/v 形状均为 [B, H, L, D] B, H, L, D = q.shape u = min(int(self.sampling_factor * math.log(L)) + 1, L) # 随机抽样部分 key,用来估算每个 query 的稀疏度 sample_idx = torch.randint(0, L, (max(L // 2, 1),), device=q.device) k_sample = k[:, :, sample_idx, :] scores_sample = torch.einsum('bhid,bhjd->bhij', q, k_sample) / math.sqrt(D) m = scores_sample.max(-1).values - torch.logsumexp(scores_sample, -1) / scores_sample.size(-1) # 按稀疏度得分保留 top-u 的 query topk_idx = torch.topk(m, u, dim=-1).indices # [B, H, u] q_top = torch.gather(q, 2, topk_idx.unsqueeze(-1).expand(B, H, u, D)) scores = torch.einsum('bhid,bhjd->bhij', q_top, k) / math.sqrt(D) attn = torch.softmax(scores, dim=-1) out = torch.einsum('bhij,bhjd->bhid', attn, v) return out这段代码省去了多头拆分、蒸馏层和数据复原等细节,但核心选query逻辑是完整的。实际跑实验时,直接基于开源实现改造会更稳妥,自己从零写容易在维度对齐上浪费大量时间。
4.3 训练脚本里的几个关键配置项
整体训练流程和其他Transformer模型差别不大,但有四个地方建议特殊关注。
第一个是优化器。Informer对学习率比较敏感,我通常用AdamW,配合warmup,前几个epoch让学习率从很小的值逐渐升到目标值,可以有效避免早期震荡。
第二个是loss。单步MSE是最常见的,但它会天然偏向低频成分,导致预测曲线过度平滑。如果业务关注峰值,可以考虑加上MAE作为辅助,或者直接切换成Huber损失。
第三个是验证频率。长序列模型训练周期不短,我建议每个epoch都在验证集上计算一次MSE和MAE,保留最佳模型。不要只看训练loss,因为训练loss降得再漂亮,验证集上可能已经过拟合。
第四个是随机种子。Reproducibility这块在时间序列里很关键,因为模型初始化、数据抽样都有随机性。建议固定seed,并跑多次实验取平均值,报告结果时也把方差带上,这样实验结果才可信。
4.4 评估阶段该测哪些指标
评估模型不只盯MSE一个数字。时间序列预测里,常用的指标有几个,各有侧重:
| 指标 | 含义 | 适用场景 |
|---|---|---|
| MSE | 均方误差,放大较大误差 | 敏感于离群点,适合评估整体稳定性 |
| MAE | 平均绝对误差 | 更贴近业务理解的误差幅度 |
| MAPE | 平均绝对百分比误差 | 适合评估相对误差,但y接近0时会爆炸 |
| RSE/R2 | 相对误差/拟合优度 | 适合对比不同模型在同一数据上的表现 |
实操中建议至少同时关注MSE和MAE,只盯一个指标很容易被带偏。我在一个温度预测项目里就遇到过:模型MSE很低,但MAE偏高,后来发现是少数几个极端温度点被模型严重预测错误,让MSE失真,而MAE才反映了真实可用性。
5. 常见问题与排查心得
5.1 显存依然爆炸怎么办
ProbSparse虽然把复杂度降到了O(L log L),但如果序列特别长,或者d_model和batch size设得很大,显存依然可能爆掉。
我的排查顺序是:先看batch size是不是太大,这是最容易被忽视的原因;然后看attention_factor是不是设得过高,导致保留的query太多;再看编码器层数,如果序列已经被蒸馏得很短了,多层编码器就会成为显存大头;最后检查一下是否在GPU上进行了冗余的中间变量保存。
还有一种更实用的小技巧:如果显存捉襟见肘,可以先把序列长度截断到模型可承受的范围内,比如从720降到336,让模型先跑通,再逐步拉长。这样能快速排除代码或数据的问题,而不是在调参之前就被环境卡死。
5.2 预测结果是“复制粘贴”的滞后值
这是长序列预测里最经典也最让人头大的问题:模型预测出来的曲线,看起来就是把最后几个真实值往后平移了,几乎没有趋势和变化。
问题根源一般有两个方向。
一是数据不平稳,模型发现“复制最新值”这个策略能获得很低的MSE,那它当然不会费力去学趋势。这时候需要对数据做差分,或者分解掉趋势项,让模型学的是变化量而不是绝对值。
二是窗口设置有问题。如果历史窗口里包含的信息不足以推断未来阶段,模型就只能本本分分地抄作业。比如你要预测未来一周,但历史窗口只有24小时,那相当于让一个只看过昨天数据的人预测下周,肯定只能乱猜。
这类滞后问题不是模型不好的唯一信号,但说明数据表达或任务设计还有改进空间。我在每次实验前都会单独计算一下“最近值外推”的基线误差,如果模型比这个基线好不了多少,就先回去处理数据。
5.3 训练loss不降或震荡严重
Informer对学习率非常敏感,训练不收敛时,我最先怀疑的不是模型结构,而是优化器配置。
经验值是从小学习率开始,比如2e-4,配合线性warmup。如果loss还是震荡,就把batch size调大一点;如果loss一直不动,检查输入数据是否需要标准化,原始数值尺度太大很容易让模型陷入局部平坦区。
另外要注意dropout不要设置过高。我看到很多人在长序列模型上习惯性用0.3甚至更高,结果模型根本学不动。时间序列本身噪声大,dropout过高会让模型失去对微弱周期信号的捕捉能力。我常用0.05~0.1,不超过0.2。
5.4 复现论文效果总差一截
复现实验和论文报告对不上,很多情况下不是模型实现错了,而是数据切分和评估方式不一致。
先在三个地方自查:第一,归一化是否只用了训练集的统计量;第二,训练集和验证集是否按时间顺序切分,而不是随机打散;第三,是否固定了训练样本顺序和shuffle种子。时间序列模型的实验结果非常容易被这些细节影响。
另一个隐蔽问题是,不同数据集的统计特性差异很大。论文里报了在某个数据集上的最优结果,但在你的业务数据上表现平平是完全正常的。模型不是魔法,它只是把合适的数据模式提取出来。关键是把Informer的核心思路提炼出来,然后针对你的数据特征做调整,这比死磕论文数值更有价值。
5.5 蒸馏参数该怎么权衡
蒸馏层数设得越多,序列长度被压缩得越厉害,显存压力越小,但信息丢失也越严重。
实际操作中,我建议先设少量蒸馏,比如编码器4层时,前2层做蒸馏,后2层保持序列长度不变,然后逐步增加蒸馏层数,观察验证集效果的变化。如果增加蒸馏后验证loss反而变好,说明冗余信息确实在干扰训练;如果变差,说明蒸馏已经丢失了关键信息。
还有一点,蒸馏层的输出维度要和下一层编码器的输入维度对齐。很多人改蒸馏层数时只顾调整池化步长,忘了改卷积输出通道数,结果模型跑着跑着就报维度不匹配。这个问题排查起来不难,但确实容易浪费不少时间。
还有一个配置细节:起始标记和预测长度要保持合理的比例关系。我试过预测168步时起始标记只给96步,效果明显不如给到192步。解码器的上下文越充裕,模型对“当前状态”的理解就越准确,预测起点就不容易跑偏。
做完整Experiments以后,我的总体体会是:Informer最大的价值不是告诉你“某个模型有多强”,而是提供了一个工程化的思路——当序列变长、算力有限时,注意力不一定要全量计算,可以按照信息量做筛选;解码不一定要从左到右逐步生成,可以一次性输出全序列。把这种思路带进你自己的时间序列项目里,比单纯套用某个模型更值得琢磨。
如果要给新手一个建议,我会说:先把公开的基础实现跑通,用一份自己熟悉的业务数据,和简单基线模型做对比。能跑赢基线、能解释结果,再谈继续调优。模型是手段,解决业务问题才是目的。