
1. 长序列预测为什么把标准Transformer逼到了墙角1.1 从“预测未来96个点”这个真实需求说起做Informer这类长时间序列预测模型起点往往不是论文而是一个很具体的业务问句接下来一段时间会怎样。比如电力调度要看未来一天的负荷曲线工业设备要做提前量足够长的温度趋势预判能源管理要按小时排产。这些场景的共同点是预测窗口很长——不是预测下一个点而是预测未来96个、192个甚至720个时间步。传统ARIMA、Prophet这类方法在单步或短窗口上还能打一旦把预测长度拉长误差会累积得很厉害因为它们大多是自回归式地一步步往外推前面一步偏一点后面全跟着歪。深度学习进来之后大家的第一个反应是用RNN或LSTM。这条路我早年走过效果不是不能用但训练慢、并行度差而且长依赖衰减的问题在几百步的窗口上非常明显。再往后Transformer成了主流选择自注意力天然能建模任意两点之间的关系,并行度也好看起来是理想的解法。但真正上手你就会发现标准Transformer在长序列上有一个绕不开的硬约束注意力的时间和内存都是序列长度的平方级。这就是Informer要解决的问题域。它是2021年一篇会议最佳论文提出的模型专门面向长序列时间序列预测Long Sequence Time-series Forecasting简称LSTF。说人话它保留了Transformer的编码器-解码器骨架但把注意力从平方复杂度压到对数线性级别同时用一次前向就吐出整段预测结果而不是一步步挤牙膏。适合谁看我认为两类人最该认真读一类是要在业务里落地多步预测的算法工程师另一类是已经用过Transformer做时序、被显存和推理时长卡住的人。1.2 平方复杂度到底贵在哪算一笔账就清楚了很多人对“平方复杂度”没概念觉得无非是慢一点。我们把数字代进去看。假设输入窗口长度L96注意力矩阵是96×96九千多个元素还好。但长序列预测的场景往往要把输入拉长到720甚至更长720的平方是518400是96的56倍。再乘上头数、批大小、层数显存会以肉眼可见的速度爆掉。我实测过在单卡24G的机器上标准Transformer用一个较长的输入窗口加上较大batch很快就OOM只能把batch压到很小训练稳定性立刻变差。更麻烦的是内存访问模式。注意力矩阵本身不只是一次矩阵乘法中间还要存softmax前后的结果用于反向传播所以显存占用不只是L²而是若干个L²。序列翻倍显存翻四倍这种增长曲线没有任何工程手段能优雅地兜住。这也是为什么Informer的核心贡献不是“把模型做深”而是“把注意力做稀”。这里有个很重要的认知稀疏化不是为了省资源而牺牲精度而是基于一个观察——真实时序里的注意力分布本身就是长尾的。绝大多数query只对少数几个key有显著响应剩下的注意力权重接近均匀分布属于无效计算。既然如此就没必要老老实实算满整个矩阵。1.3 Informer想解决的三件事以及它没解决的事把论文的动机拆开看Informer对准了三个具体问题。第一是注意力的平方复杂度用ProbSparse注意力把复杂度降到O(L log L)。第二是堆叠层数多了之后内存占用高用自注意力蒸馏在每层把序列长度砍半。第三是自回归解码太慢用生成式解码器一次性输出整段预测。这三件事是一套组合拳缺一个效果都会打折。但我也得说清楚它没解决什么。Informer不是万能的时序模型它对周期性、趋势性的建模依赖输入里显式的时间特征编码如果你的数据没有清晰的时间戳结构或者采样极不规律它的优势会打折扣。另外在输入长度只有几十步的短序列任务上ProbSparse的额外开销甚至可能让整体比标准注意力还慢——稀疏化的收益是随序列长度增长才显现的。这一点很多教程不讲实际做项目时踩了才知道。2. Informer的整体设计思路拆解2.1 骨架没变变的是“看哪里”和“怎么吐结果”第一次读Informer的结构图容易觉得它很复杂因为画了一堆分支。但拆开看它还是编码器-解码器那一套改动集中在三个位置。编码器里多头注意力换成ProbSparse版本每层后面接一个蒸馏模块把长度减半。解码器里输入不再是“上一步的输出”而是“历史已知段一段占位符”注意力用带掩码的版本防止看到未来。输出端一次给出pred_len个点的预测。这种设计选择的背后逻辑其实很朴素既然长序列预测的瓶颈在计算量和推理速度那就从这两个地方下手。ProbSparse解决“算得太多”蒸馏解决“层数堆不上去”生成式解码解决“推得太慢”。三者组合之后你可以在同样的显存下把输入窗口拉得更长把预测窗口拉得更长这在原来的标准Transformer上是做不到的。我个人比较欣赏的一点是它的改造都是“可插拔”的。你完全可以在自己的项目里只上ProbSparse注意力保留原有的解码方式也可以只用生成式解码器注意力还是标准的。这种模块化让迁移成本低了不少不像某些模型必须整套搬。2.2 三个关键改造的取舍逻辑逐个说透先说ProbSparse注意力的取舍。它是“近似”而不是“精确”意味着精度上必然有损失。论文的做法是用KL散度衡量每个query的“活跃程度”活跃的query做完整注意力不活跃的用value的均值兜底。为了进一步省算力KL散度本身也用max减mean来近似而且只在随机采样的一部分key上算。这三层近似叠起来理论上是有风险的。但实测下来在时序数据上精度损失很小因为时序的注意力分布确实集中。这是一个典型的“用一点点精度换大量算力”的交易划得来。第二个是自注意力蒸馏。做法是用一维卷积加最大池化把序列长度减半。这里要注意卷积用了循环填充circular padding目的是保持时序的边界连续性——序列首尾在周期上可能是相连的普通零填充会人为制造一个断点。这个细节很小但对周期性强的数据影响不小我早期自己实现时忽略了它结果在周期边界处的预测明显毛刺。第三个是生成式解码器。传统做法是训练时用teacher forcing推理时一步步喂回自己的输出误差累积严重。Informer的做法是训练和推理都统一成“一次性输出”解码器输入是历史已知的一段真实值加一段全零占位符。占位符位置的输出就是要预测的值。这个设计让推理只需一次前向速度提升非常明显但也带来一个约束解码器输入的已知段长度必须固定不能灵活调整。2.3 什么数据适合上Informer什么数据别硬套判断标准我总结成三条很实用。第一预测窗口要不短至少几十步以上如果你的任务只是预测下一个点或者未来五步标准Transformer甚至简单的LSTM就够上Informer属于杀鸡用牛刀还容易因为稀疏化引入不必要的偏差。第二输入要有清晰的多尺度时间结构也就是数据里存在小时、天、周这类周期性因为Informer的时间戳嵌入是吃这些特征的没有的话等于浪费了一路输入。第三样本量要够。Informer参数量在千万量级小数据集上很容易过拟合我见过在只有几千条样本的数据上直接上Informer训练损失降得很好看验证集RMSE却很糟。这种情况不如退回去用轻量模型或者先把数据增强、滑窗步长这些前置工作做扎实。表格对比一下会更直观数据特征建议方案理由预测步长小于24样本万级以下轻量模型或统计方法稀疏化收益不抵开销预测步长24至96有明显日周期Informer标准配置正好落在设计目标区间预测步长超过192输入窗口长Informer加长输入和蒸馏优势最明显采样不规律、无时间戳慎用需先重构特征时间嵌入无从下手3. 核心机制逐层拆解与手写实现3.1 输入嵌入三路信息拧成一根向量Informer的输入嵌入有三路。第一路是标量值本身通过一维卷积映射到d_model维度这里用卷积而不是全连接是为了在映射的同时保留局部时序模式相当于顺手做了一个小范围的平滑。第二路是位置编码用的是标准正弦位置编码给每个时间步一个位置标识。第三路是时间戳嵌入把每个时刻的月、日、星期、小时、分钟这些离散特征分别做Embedding再相加。三路相加之后得到编码器输入。这里有个实操细节值嵌入用卷积时卷积核大小和padding要匹配否则序列长度会变后面的位置编码就对不齐了。我一般用kernel_size3、padding1这样长度保持不变。另外时间戳特征要提前离线处理好别在训练循环里做会拖慢不少。class DataEmbedding(nn.Module): def __init__(self, c_in, d_model, dropout0.05): super().__init__() self.value_embed nn.Conv1d(c_in, d_model, kernel_size3, padding1) self.temporal_embed nn.Embedding(64, d_model) # 简化版,各类时间特征共用 self.dropout nn.Dropout(dropout) def forward(self, x, x_mark): # x: [B, L, C] x_mark: [B, L, 4] 已编码好的时间特征索引 x self.value_embed(x.permute(0, 2, 1)).permute(0, 2, 1) t self.temporal_embed(x_mark).sum(dim2) return self.dropout(x t)位置编码部分我就不重复贴了正弦公式是通用的。需要强调的是位置编码和值嵌入是相加不是拼接所以d_model必须一致这个在配置时容易写错。3.2 ProbSparse注意力怎么挑出“值得算”的query这是整个模型最核心也最容易写错的部分。思路是对每个query算一个稀疏性得分M得分高说明这个query的注意力分布偏离均匀分布值得做完整计算得分低的query就用value的均值代替因为它本来也没关注什么。M的定义基于KL散度但实际实现用max减mean来近似。import math import torch import torch.nn as nn class ProbSparseAttention(nn.Module): def __init__(self, d_model, n_heads, factor5, dropout0.05): super().__init__() assert d_model % n_heads 0 self.d_model d_model self.n_heads n_heads self.d_k d_model // n_heads self.factor factor self.q_proj nn.Linear(d_model, d_model) self.k_proj nn.Linear(d_model, d_model) self.v_proj nn.Linear(d_model, d_model) self.out_proj nn.Linear(d_model, d_model) self.dropout nn.Dropout(dropout) def forward(self, queries, keys, values, attn_maskNone): B, L, _ queries.shape S keys.shape[1] H self.n_heads q self.q_proj(queries).view(B, L, H, -1).permute(0, 2, 1, 3) k self.k_proj(keys).view(B, S, H, -1).permute(0, 2, 1, 3) v self.v_proj(values).view(B, S, H, -1).permute(0, 2, 1, 3) # 采样数量 u c * ln(L),c 就是 factor u max(1, int(self.factor * math.log(max(2, L)))) u min(u, L) score torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.d_k) if attn_mask is not None: score score.masked_fill(attn_mask, -1e9) # max - mean 近似 KL 散度,得到每个 query 的活跃度 m score.max(dim-1).values - score.mean(dim-1) # [B,H,L] top_u m.topk(u, dim-1).indices # [B,H,u] # 只对活跃 query 做完整注意力 q_sparse torch.gather( q, 2, top_u.unsqueeze(-1).expand(-1, -1, -1, self.d_k)) attn torch.softmax( q_sparse k.transpose(-2, -1) / math.sqrt(self.d_k), dim-1) attn self.dropout(attn) out_sparse attn v # [B,H,u,d] # 其余 query 用 value 均值兜底 out v.mean(dim2, keepdimTrue).expand(-1, -1, L, -1).clone() out.scatter_(2, top_u.unsqueeze(-1).expand(-1, -1, -1, self.d_k), out_sparse) out out.permute(0, 2, 1, 3).reshape(B, L, self.d_model) return self.out_proj(out)这段是教学版能跑通但没做工程优化。真正用时要注意几点factor的取值直接决定采样量原实现默认5L96时u约等于23也就是只算23个query的完整注意力剩下73个用均值省得很凶。如果发现精度掉得厉害先把factor调到8或10试试比改模型结构见效快。另外scatter回填时要确保top_u索引顺序和out_sparse对应写错的话不会报错只会静默地给出错误结果这个坑我踩过一次debug花了很久。3.3 自注意力蒸馏把序列长度一层层砍半蒸馏模块的作用是控制内存。每经过一层编码器序列长度减半两层之后长度变成四分之一。实现上就是卷积加激活加池化卷积核用循环填充保持边界连续。class Distilling(nn.Module): def __init__(self, d_model): super().__init__() self.conv nn.Conv1d(d_model, d_model, kernel_size3, padding1, padding_modecircular) self.norm nn.BatchNorm1d(d_model) self.act nn.ELU() self.pool nn.MaxPool1d(2, stride2) def forward(self, x): # x: [B, L, D] x self.conv(x.permute(0, 2, 1)) x self.pool(self.act(self.norm(x))) return x.permute(0, 2, 1) # 长度变为 L//2这里有个数字要提前算好。假设编码器堆了n层输入长度L每层减半最后一层输出长度大约是L除以2的n次方。如果L96、n2最后长度是24如果n3长度是12。长度太小会丢失信息所以层数不能无限堆。我一般把n控制在2到3L较小时用2层L超过500时才考虑3层。另外L必须是偶数才能整齐减半构造样本时要保证左填充或截断后长度为偶数否则池化会丢一个时间步累积到后面的解码就会错位。3.4 生成式解码器一次前向吐出整段预测解码器的输入是两段拼接前label_len步是历史的已知真实值后pred_len步是全零占位符。注意力里用因果掩码保证每个位置只能看到自己和更早的位置但因为占位符那部分的输入是零模型实际上是靠编码器输出的交叉注意力来“填”这些位置。# 构造解码器输入:label_len 步已知 pred_len 步占位 dec_in torch.cat([ x_enc[:, -label_len:, :], torch.zeros(B, pred_len, x_enc.shape[-1], devicex_enc.device) ], dim1) # 解码器输出只取后 pred_len 步 out model(x_enc, x_mark_enc, dec_in, x_mark_dec) pred out[:, -pred_len:, :] # [B, pred_len, C]这个设计的好处是推理速度。对比一下就很明显自回归方式预测96步要前向96次生成式只要1次。但代价是占位符位置的信息完全靠交叉注意力从编码器拿如果编码器对历史压缩得太狠比如蒸馏层太多解码器就“无米下锅”表现是预测曲线过于平滑、缺乏细节。这时候要么减一层蒸馏要么把label_len加长给解码器更多真实上下文。4. 从零跑通一个任务数据、训练与调参4.1 滑窗构造样本与标准化别在这里翻车数据准备这一步看着无聊但出问题最多。我用的是一个公开的电力负荷类数据集做验证包含多变量序列和对应的时间戳。核心是把长序列切成一个个(输入窗口, 预测窗口)的样本对。参数设定一般是seq_len96、label_len48、pred_len96滑窗步长stride1。def build_windows(data, seq_len, label_len, pred_len, stride1): samples [] total seq_len pred_len for start in range(0, len(data) - total 1, stride): x data[start : start seq_len] y data[start seq_len - label_len : start seq_len pred_len] samples.append((x, y)) return samples注意y的长度是label_len pred_len因为解码器需要前面那一段已知值。这个偏移量很多人第一次写会搞错导致标签和输入错位一格训练出来的模型会整体滞后。标准化我要单独强调必须用训练集的均值和方差不能全量数据算。我见过直接用全量标准化的验证集信息泄漏指标虚高上线立刻掉下来。稳妥做法是把均值方差存下来推理时复用。另外对多变量数据可以逐通道标准化也可以全局标准化通道量纲差异大时选前者。4.2 损失函数与学习率策略的选择原实现用MSE理由是长序列预测更看重整体形状MSE对大偏差惩罚重能压住离群点。但我实际用下来如果你的数据里存在尖峰或者异常值MSE会被少数极端样本主导训练不稳定。这时候换成MAE或者Huber会更稳。我一般先跑MSE做基线如果发现损失曲线震荡剧烈再换Huber试。学习率方面Informer原配置用了warmup加衰减的组合峰值学习率在1e-4量级。这个值不算激进配batch size 32比较合适。如果显存不够只能开到batch 8学习率相应往下调到3e-5左右否则容易发散。我的习惯是先固定小学习率跑200步看损失是否稳定下降确认没问题再开warmup。4.3 关键超参数怎么定附一组可复用的配置超参数这块我把常用的默认配置整理成表直接抄作业基本不会出大错参数取值说明d_model512太小容量不够太大显存吃紧n_heads8每头64维是常见均衡点e_layers2编码器层数配合蒸馏用d_layers1解码器一层通常够用d_ff2048前馈层一般是d_model的4倍factor5稀疏采样系数精度不够就调大dropout0.05时序任务不宜过高batch_size32显存允许下尽量大lr1e-4配warmup使用算一下参数量级方便你预估显存。每层编码器的注意力部分大约4×d_model²个参数512的平方是26万乘4约100万前馈层是2×d_model×d_ff约200万。一层加起来300万左右两层600万再加上嵌入层、输出投影、解码器整体在千万参数上下。这个量级在单卡上训练是可以接受的但如果你的数据只有几万条过拟合风险确实存在dropout和早停要配好。4.4 训练现场的观察记录实际训练时我关注三条曲线训练损失、验证损失、以及验证集上的MAE。正常的形态是训练损失快速下降后趋缓验证损失同步下降但稍高。如果验证损失在几十个epoch后开始回升就是过拟合信号这时候要么加dropout要么减层数。还有一个容易忽略的指标是“预测曲线的形状”。我不只看数值误差还会把预测结果和真实曲线画在一起看。很多模型MAE看着不错但画出来是一条平滑的直线完全没有捕捉到波动。这种情况说明模型学到的只是均值ProbSparse的稀疏化可能过度了或者factor设得太小。把factor从5调到10往往曲线立刻有了细节。训练时长上在单卡上跑一个中等规模的数据集每epoch几分钟量级通常几十个epoch收敛。如果发现跑了几百epoch还在缓慢下降多半是学习率太小或者warmup太长可以适当调整。别盲目追求训练损失降到极低时序预测的泛化比拟合重要得多。5. 常见问题与排查技巧实录5.1 损失降得漂亮预测却“贴地走”这是最高频的问题。训练损失一路降验证MAE也还行但把预测画出来一看几乎是一条水平线只是跟着均值走。原因通常有三个。第一是标准化方式不对如果对每个窗口单独标准化模型学不到绝对水平输出自然趋向均值。第二是ProbSparse的factor太小稀疏化过度模型只保留了最主导的那部分注意力细节全丢。第三是解码器的label_len太短给的上下文不足。排查顺序我一般是这样先检查标准化确认用的是全局统计量再把factor临时改成L等于退回标准注意力看曲线是否恢复细节最后调整label_len。这三步基本能定位问题。实测下来factor从5调到10是性价比最高的一次改动。5.2 ProbSparse采样引发的静默错误这个坑比较隐蔽不会报错只会让结果莫名其妙地差。典型表现是模型能训损失也降但换一个随机种子结果波动极大。原因多半是top_u索引的处理有问题比如gather和scatter的维度没对齐或者掩码在稀疏计算路径上没有正确应用。我的检查方法是写一个小单测构造一个极小的输入比如L8、单头把factor设成一个很大的值让u等于L此时ProbSparse理论上应该等价于标准注意力两者的输出应该几乎一致。如果对不上说明实现有bug。这个方法很笨但极其有效我每次改完注意力代码都会跑一遍。另外注意u取整时要用int而不是round边界情况下差一个都会导致索引越界或者采样不足。5.3 预测步长一拉长就崩问题出在哪短窗口预测还行把pred_len从96拉到336指标立刻崩掉。这通常不是模型本身的问题而是训练样本的分布变了。预测窗口变长之后样本总数减少因为要留出更长的尾部有效训练数据变少同时预测远端的信息量本来就低误差自然会涨。应对办法有三条。一是把stride调小尽量榨出更多样本二是适当增加输入窗口seq_len让编码器看到更长的历史这条最有效三是检查解码器的位置编码如果pred_len超过了训练时的最大位置位置编码会外推到没见过的区域表现会突然变差这时候要把位置编码的上限提前留足。我在一次实验里就是卡在这个点上把位置编码的max_len从500调到1000之后长窗口指标立刻回升了一大截。5.4 问题速查表把上面这些整理成表遇到问题时按顺序对一遍能省不少时间现象最可能原因处理动作预测曲线过度平滑factor过小或蒸馏层过多factor调至10减一层蒸馏损失震荡不收敛学习率偏大或存在异常样本降lr换Huber损失换种子结果差异大稀疏索引处理有bug用uL做等价性单测长窗口指标骤降位置编码外推越界提前扩大位置编码上限验证集虚高、上线掉用全量数据做了标准化改用训练集统计量边界处出现毛刺蒸馏卷积用了零填充改为循环填充6. 几个我认为值得单独说的工程细节推理阶段的批处理值得调。生成式解码器一次出整段结果意味着推理时可以把多个样本拼成一个大batch吞吐提升很明显。但要注意解码器输入的占位符长度必须一致如果你的业务场景里预测长度是变化的就得按长度分桶别硬凑。模型保存和加载也有个细节。Informer里有几处依赖配置的参数比如最大位置编码长度、时间特征的分箱数保存时要把配置一起序列化别只存state_dict。我吃过这个亏换台机器加载时因为配置默认值不同位置编码维度对不上报了一个很隐晦的shape错误查了半天才发现是配置没跟着走。最后说一个我个人觉得最实用的经验不要一上来就追最新最复杂的变体。Informer的原始结构已经把长序列预测的主要矛盾解决得差不多了很多后续改进是在特定数据分布上做微调。如果你手头的数据是典型的工业或能源类多变量序列老老实实把原始结构调好把数据窗口、标准化、factor这几个地方抠细效果往往比换模型更明显。我在几个项目里反复验证过这一点模型结构的边际收益远小于数据处理的边际收益。这个判断不一定对所有人都成立但至少在我接触的场景里它一次又一次被印证。