
长时间序列预测模型Informer这套东西我第一次真正上手是在给一套区域用电负荷做跨周预测的时候。当时的需求很朴素拿过去四天的逐小时数据预测未来三到十四天。我先按老办法上了LSTM码了六层训到第八个epoch就发现一个规律——预测步数一过48曲线就开始往均值方向塌越往后越像一条直线最后干脆摆烂。后来换成标准的Transformer encoder-decoder结构效果确实好了一点但训练时显存直接飙到一张卡装不下推理阶段还得一步步滚预测720个点要跑720次前向线上根本扛不住。Informer就是在那个节点进入视野的。它本质上是一个为“长输入、长输出”重新设计过的Transformer变体核心解决三件事注意力计算量随序列长度平方增长、深层堆叠时的内存爆炸、以及自回归解码带来的龟速推理。它把注意力复杂度压到O(L log L)用蒸馏把序列长度层层减半再用一个生成式解码器一次性吐出一整段预测。这套设计适合手里有几千到几万条历史记录、数据有明显周期性、需要一次输出几十到几百步的场景比如电力负荷、交通流量、气象观测、设备传感器读数。但它不是万能药对极短序列、强突变序列、需要概率区间而不是单点估计的场景硬上会很难受。下面我把这套模型从设计动机到跑通细节完整捋一遍包括我实际踩过的坑和调参时反复验证过的经验值。1. 长序列预测为什么必须换一套架构思路1.1 三个绕不开的现实痛点先说清楚问题后面所有的设计选择才有落脚点。第一个痛点是误差累积。传统的递归式预测一次只输出一个点然后把这个点塞回输入窗口再预测下一个。这个流程在业务上很直观但在数学上是个连乘过程每一步的误差都会作为下一步的输入随着步数增加误差方差不降反升。我实测过一个六层LSTM在小时级负荷数据上的表现预测24步时MAE还在可接受范围到96步误差大约翻了一倍多到336步基本失去参考价值。这不是模型容量不够而是递归结构本身在放大噪声。第二个痛点是计算与显存。标准自注意力的时间与空间复杂度都是O(L²)L是序列长度。当输入加输出合起来到720甚至更长时单头注意力矩阵就是720×720多头再多层堆起来激活值占用的显存会迅速吃掉整张卡。更麻烦的是反向传播要保存中间结果哪怕前向勉强跑得动训练时也很容易直接OOM。第三个痛点是注意力的长尾分布。这个点最容易被忽略。把训练好的注意力权重拿出来看会发现绝大多数query只对少数几个key有强响应剩下几百个key的权重几乎贴着零。也就是说完整的L×L矩阵里大部分计算是在算“几乎没有贡献”的分数。这个观察是ProbSparse注意力的直接来源——既然分数分布是长尾的那就只算那些“显著”的部分。注意这三点必须在同一套设计里一起解决才有意义。只压缩注意力复杂度而不改解码方式推理依然慢只改解码而不减内存长序列训练还是跑不起来。Informer的三个组件是互相咬合的。1.2 Informer的三处关键改动速览先把结论摆出来后面再逐个拆。ProbSparse自注意力不再对每个query都算完整注意力而是用KL散度的一个近似度量挑出“活跃query”只对这些query做点积注意力其余位置用Value的均值填充。理论复杂度从O(L²)降到O(L log L)。自注意力蒸馏每个encoder层之后最后一层除外接一个一维卷积加最大池化把序列长度砍半。三层encoder下来长度按144→72→36收敛特征图变小显存和计算量同步下降同时卷积池化本身对局部噪声有一定平滑作用。生成式解码器解码器输入不是一个个喂而是一次性拼成label_len pred_len的长度前面label_len段填真实的近期历史后面pred_len段填占位值通常是0在标准化后的空间里0等价于均值一次前向直接输出全部预测步。1.3 什么场景该用什么场景别硬上我见过不少人拿这套模型去跑只有几百条样本的日频数据然后抱怨效果不如ARIMA。这不怪模型是场景错配。适合上Informer的典型画像历史数据至少有几千个时间点目标序列和协变量都有明确的日、周、月周期需要一次性输出48到720步线上能接受秒级而不是毫秒级推理允许用单点预测配合人工经验做区间判断。不建议硬上的情况样本量低于两千序列存在大量阶跃式突变比如促销日、故障跳变这类突变更依赖事件特征而不是历史模式业务要求输出置信区间或分位数Informer的原生输出只有点估计要做区间得额外包一层分位数回归或者做残差的bootstrap推理延迟要求极严的嵌入式场景。还有一点必须提前说在pred_len拉到720这种极长跨度时Informer的中位数误差常常已经很接近“取上周同一时刻的值”这种朴素季节性基线。这不是贬低它而是提醒你在做评估报告时一定要把季节性朴素基线seasonal naive放进去做对照。我见过太多汇报只跟自己以前那版差模型比数字好看但没有说服力。2. 核心机制拆解三个设计背后的账怎么算2.1 ProbSparse注意力从平方到线性对数的取舍标准自注意力对每个query q_i 都与全部key计算点积再softmax。ProbSparse的思路是先给每个query算一个“重要性分数”只保留分数最高的那批query参与完整计算。分数怎么定原文给出的度量是这样的对第i个query定义M(q_i, K) max_j (q_i·k_j^T / √d) - (1/L_K) · Σ_j (q_i·k_j^T / √d)用人话说就是“这个query与所有key的相似度里最大值减去平均值”。如果某个query对某几个key响应特别强最大值会远高于平均值差值就大如果它对所有key都差不多差值趋近于零。这个差值恰好是KL散度的一个近似上界所以它是在近似衡量“这个query的注意力分布有多偏离均匀分布”。选出分数最高的 u 个queryu的取值是u c · ln(L_Q)其中c是常数因子代码里对应factor参数默认5。以L_Q96为例u 5 × ln(96) ≈ 5 × 4.564 ≈ 23也就是96个query里只算23个剩下73个直接用Value的均值顶上。实操里有两个必须知道的细节。一个是采样为了不真的把全部L_K算一遍再挑实现里会随机采样U c·ln(L_K)个key来做估计这意味着每次前向采样的key子集不同输出会带一点随机性。这个随机性在训练阶段其实起到了轻微的正则作用但如果你在做确定性推理复现就需要固定随机种子否则同一份输入两次推理结果会有微小差异。另一个是加速比是有门槛的。我在 L96 的短序列上对比过ProbSparse和全注意力ProbSparse反而更慢因为采样、取topk、gather这些索引操作的开销超过了省下来的矩阵乘法。真正拉开差距是在 L 到 512 以上尤其是720这个量级显存占用能降一半以上。所以如果你的场景只有96步输入用不用ProbSparse无所谓输入窗口拉到几百步它才开始回本。注意ProbSparse不是无损近似。它在某些周期性弱的序列上会漏掉真正重要的query表现为预测曲线在局部出现不该有的平坦段。判断方法是把factor从5调到3或8各跑一次如果结果对这个参数极度敏感说明你的数据分布不够长尾这时候老老实实用全注意力很多实现里把factor设成一个大于L的值就等价退化成全注意力反而更稳。2.2 自注意力蒸馏砍半序列长度的代价与收益蒸馏那一层写出来只有一行X_{t1} MaxPool(ELU(Conv1d(X_t)))Conv1d的kernel设3、stride设1、padding设1输入输出通道都是d_model保持特征维度不变然后ELU激活再做kernel3、stride2、padding1的最大池化序列长度变成ceil(L/2)。它的收益很直接三层encoder堆下来序列从144降到72再降到36注意力矩阵的元素数按四分之一、十六分之一递减显存占用大幅下降同时后面几层的感受野反而变大了——因为每层注意力在更短的序列上操作等效覆盖的原始时间跨度更宽。代价也很实在。池化是不可逆的信息压缩细粒度的位置信息会丢。我观察过的一个现象是当序列里存在持续时间短于池化窗口的尖峰比如持续两三个小时的异常高负荷经过第一层蒸馏后这个尖峰就被平滑掉一部分到第三层基本看不出来了。如果你的业务关心短时异常蒸馏层反而是个减分项。还有一个容易忽略的点蒸馏只在encoder里做decoder里不做。因为decoder的输出长度必须精确对应pred_len中间不能变长变短。所以decoder那边该吃多少注意力计算还是吃多少长跨度预测的显存瓶颈主要落在decoder上这点在调优时要心里有数。2.3 生成式解码器一次吐出720个点是怎么实现的这是我觉得整套设计里最实用的一处改动也是最容易被教程讲糊的地方。传统Transformer解码训练时用teacher forcing推理时自回归一次一步。Informer把它改成解码器输入序列长度固定为label_len pred_len。前label_len个位置填入测试段之前的真实历史值经过同样的标准化后pred_len个位置填入0在标准化空间里就是均值。整个序列一次性送进decoder输出再经过一个线性层映射回特征维度只取最后pred_len段计算损失。为什么填0而不是别的因为标准化之后0就是训练集的均值用均值作为未知未来的初始猜测是偏差最小的先验。模型要学的就是“从均值出发往正确的方向修正多少”。这个设计的代价是一处硬约束推理时必须提供label_len长度的最新真实历史。这不是可选项。线上部署的时候如果你的数据管道有延迟拿不到最近48个点模型就没法跑。我在一个项目里就因为这个卡了两天——上游数据同步是每小时一次但偶尔延迟导致label_len段里混进了NaN模型输出直接变成一片NaN。后来在数据入口加了一层前向填充加质量标记才解决。label_len取多大原文在多数实验里用48也就是seq_len的一半。我试过把它调到等于pred_len在电力数据上略好一点点但训练时间增加明显调到24又会在预测起点附近出现跳变。经验值label_len取seq_len的 0.3 到 0.5 之间绝大多数场景够用。2.4 输入表示别小看那层看着不起眼的时间戳嵌入Informer的输入表示是三项相加标量投影、位置编码、全局时间戳嵌入。标量投影就是一个线性层把每个时间点的特征维度映射到d_model。位置编码用标准的正弦余弦。全局时间戳嵌入是把月、日、星期、小时分钟级数据再加分钟各自做成一张embedding表查出来再相加。这几张表的词表大小是固定的月12、日32、星期7、小时24、分钟60。这一步看着简单但错起来很隐蔽。最常见的错误是freq参数和数据实际粒度对不上。比如数据是15分钟粒度freq却按小时传那么“小时”这一维会被算错“分钟”维干脆不生成模型学到的时间模式全是错的。表现是训练损失能降但验证损失居高不下而且预测曲线在每个整点附近出现规律性的台阶。排查方法很简单把生成的时间特征打印前20行人眼对一遍原始时间戳。还有一个细节日期列如果是字符串解析时区、解析格式、闰年、夏令时都可能出岔子。我的做法是数据进管道前先把时间列统一转成pandas.to_datetime再显式.dt.tz_localize(None)避免带时区的对象在后面做差值计算时抛异常。3. 从零跑通数据、配置与训练全流程3.1 数据管道与滑动窗口切片数据管道这块核心是一个滑动窗口Dataset。窗口长度是seq_len pred_len步长1训练集、验证集、测试集按时间顺序切分绝对不能shuffle后再切。class WindowDataset(Dataset): def __init__(self, raw_x, raw_x_mark, raw_y, raw_y_mark, size): self.seq_len, self.label_len, self.pred_len size self.x raw_x self.x_mark raw_x_mark self.y raw_y self.y_mark raw_y_mark def __len__(self): # 注意这里必须减1边界处最容易越界 return len(self.x) - 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 return (self.x[s_begin:s_end], self.y[s_begin:s_end], self.x_mark[s_begin:s_end], self.y_mark[r_begin:r_end], self.y[r_begin:r_end])这段代码里有两个地方特别容易翻车。第一是__len__的边界早期实现里有人写len - seq_len - pred_len少了那个1导致最后一个窗口取不到测试集尾部几十个点永远不被评估指标会虚高一点点。第二是归一化StandardScaler只能在训练段上fit然后transform验证段和测试段。我见过把全量数据一起fit的写法这就是典型的数据泄漏指标能好看10%以上但上线就崩。scaler StandardScaler() scaler.fit(train_df[cols].values) # 只在训练段fit train_arr scaler.transform(train_df[cols].values) val_arr scaler.transform(val_df[cols].values) test_arr scaler.transform(test_df[cols].values)如果做多变量预测还要决定是全局共用一套均值方差还是每个变量各自一套。我的做法是每个变量独立标准化因为电力、温度、流量的量纲差异太大共用一套会让小量纲变量在损失里几乎没有权重。3.2 关键超参怎么定给出一套能直接用的配置先给一个我反复验证过的基线配置以小时级数据的ETT这类公开数据集为例。参数取值说明seq_len96小时级数据4天是24的整数倍覆盖完整日周期label_len48seq_len的一半解码器起点的真实历史长度pred_len24/48/168/336/720分别对应1天、2天、1周、2周、1个月d_model512隐藏维度8头时每头64维n_heads8head_dim d_model / n_heads 64d_ff2048前馈层维度取4倍d_model是Transformer惯例e_layers2或33层时有两次蒸馏144→72→36d_layers1解码器1层足够加层收益很小但显存涨得快factor5ProbSparse的采样因子cdropout0.05时间序列数据通常不需要大dropoutbatch_size32单卡16G显存的安全值learning_rate1e-4Adam配合每轮衰减train_epochs6这类模型收敛很快6轮后基本过拟合关于seq_len的选择有个简单的推理过程数据是小时级最强的周期是24小时和168小时一周。取96既是24的整数倍也接近半个周周期能同时覆盖日周期的四个完整循环。如果你取100模型就没法利用“每天同一时刻”这种对齐关系效果会明显差一截。同样的逻辑分钟级数据取1440一天的整数分之一日频数据取7或14。d_ff为什么是4倍d_model这是从原始Transformer延续下来的经验比例前馈层承担了大部分非线性变换的容量太窄会导致欠拟合太宽参数暴涨但收益递减。参数量估算一下单层encoder的注意力部分约4·d_model² 4×262144 ≈ 1.05M前馈部分约2·d_model·d_ff 2×512×2048 ≈ 2.1M加起来约3.1M。3层encoder约9.4M加上embedding、decoder、输出投影整体在11到12M量级。这个规模在单张消费级显卡上完全跑得动。factor的实际意义值得单独说一下u 5 × ln(96) ≈ 23也就是说只有约四分之一的query参与完整注意力计算。如果你的seq_len拉到336u 5 × ln(336) ≈ 29参与比例降到不足9%加速更明显但近似误差也更大。这是我建议在长序列场景把factor适当调小比如4的原因。3.3 训练脚本与显存调优实录一条能直接跑的命令长这样python -u run.py \ --is_training 1 \ --model informer \ --data custom \ --root_path ./dataset/ \ --data_path load_hourly.csv \ --features M \ --target OT \ --freq h \ --seq_len 96 \ --label_len 48 \ --pred_len 336 \ --e_layers 3 \ --d_layers 1 \ --factor 5 \ --d_model 512 \ --d_ff 2048 \ --n_heads 8 \ --dropout 0.05 \ --train_epochs 6 \ --batch_size 32 \ --learning_rate 0.0001 \ --patience 3 \ --des exp_long_horizon--features M表示多变量预测多变量--target指定主目标列损失只在这列上算。如果你的需求是“用多个外生变量预测单一目标”改用--features MS这时候输出维度会降到1显存也能省不少。--freq h必须和数据粒度严格一致可选 h小时、t分钟、d日、w周、m月。这一项错了前面说过时间特征全废。显存不够的时候按这个优先级往下砍先降batch_size32→16→8。这是最不影响模型结构的操作代价只是训练噪声变大、收敛慢一点。再降d_model512→256。注意d_model降了n_heads要跟着降保持每头维度64比较稳256配4头。再降e_layers3→2。少一层就少一次蒸馏序列长度收敛得慢显存和计算都省。最后才考虑混合精度。AMP在注意力softmax那里容易出数值问题我的经验是开启后要监控loss是否出现NaN前200步特别关键。如果一定要开把softmax前的scale做一下clamp会更稳。还有一个容易被忽略的显存占用来源日志和中间结果保存。有些实现每步都把预测结果、注意力权重存到列表里训练几百步后这些列表本身就吃掉几个G。我的做法是只在验证阶段收集结果训练阶段只记标量loss。3.4 评估与反归一化这一步做错前面全白干评估阶段最容易犯的错是直接在标准化空间里算MSE和MAE。这样算出来的数字很好看因为数据被压到均值0方差1附近误差绝对值天然小。但这个数字对业务毫无意义——你没法跟业务方说“我们的MAE是0.08”人家问0.08什么单位你答不上来。正确做法是先把预测值和真值都做inverse_transform回到原始量纲再算MAE、RMSE、MAPE。pred scaler.inverse_transform(model_output) # 回到原始量纲 true scaler.inverse_transform(ground_truth) mae np.mean(np.abs(pred - true)) rmse np.sqrt(np.mean((pred - true) ** 2))MAPE要小心如果原始数据里存在接近0的值MAPE会爆炸。电力负荷在凌晨低谷时可能接近0我一般用 sMAPE 或者干脆只用MAE加RMSE。比单个MSE数字更有诊断价值的是分horizon误差曲线。把pred_len336的预测拆成14段每24步算一个MAE画出来看误差随步长的增长斜率。如果斜率在前段平缓后段陡增说明模型只是学会了复制最近值长期依赖没学到如果整体平缓但起点就高说明输入表示或者归一化有问题。这张图我在每次调参时都会看比盯总MSE管用得多。4. 常见问题与排查实录4.1 损失不降或者剧烈震荡训练损失从第一轮就卡在某个值附近不动先排查这四项按可能性排序。时间特征生成错误。freq参数和实际粒度不符时模型看到的时间编码是错的学不到周期模式。表现是loss能降一点但很快平台化验证集误差远高于训练集。排查方法是把x_mark的前20行打出来和原始时间列人工比对。归一化用错范围。全量fit导致泄漏的话训练loss会异常低因为测试信息漏进来了验证loss却很高。这种情况反而好识别。真正难查的是“用了原始未归一化数据直接训练”loss会直接飙到几千然后NaN。标签对齐错位。r_begin s_end - label_len这个偏移写错一位模型就是在学“预测错位一个时间点”的任务loss能降但预测曲线整体滞后一步。这个错误极其隐蔽检查方法是在测试集上把预测曲线和真值曲线画在同一张图里放大看起点是否对齐。学习率过大。1e-4是安全值如果用了1e-3前几个batch的loss会剧烈跳动甚至出现NaN。Informer用的是GELU加LayerNorm理论上比原版Transformer稳但注意力里那个除√d的缩放配合大学习率仍然容易炸。4.2 长horizon尾段崩坏pred_len拉到336或720时前几十步预测还挺贴合后面逐渐摊平成一条直线。我在这上面花的时间最多试过的方案和实际效果如下。尝试实际效果结论加大label_len到96起点改善尾段无变化作用有限加大seq_len到336尾段明显改善显存涨一倍有效但代价高损失对远端加权尾段略好近端变差需要权衡gamma取0.5左右加入傅里叶频域特征周期性强的数据有效值得试换成patch化输入效果提升最明显根本性方案加季节性朴素基线对照发现模型其实没赢多少必做这里的核心认知是长跨度预测的瓶颈不在模型容量而在信息本身。720步之后的不确定性任何模型都消不掉。所以我现在的做法是pred_len超过336就把预测结果当作“趋势参考”而不是“精确数值”交付同时把季节性朴素基线一起给业务方看让对方建立合理预期。还有一个具体技巧在decoder输出端加一个可学习的位置相关偏置项形状是pred_len × n_features初值设0。这相当于给模型一个“每个预测步允许有自己的系统性偏移”的自由度实测能让尾段的平坦化推迟大约30%的步数。代价是参数量增加pred_len × n_features对720×7这种规模完全可接受。4.3 显存与速度的典型症状速查症状最可能原因处置办法训练第一个batch就OOMbatch_size或seq_len过大先降batch到8验证能否跑通训练几轮后突然OOM中间结果累积未释放检查是否有列表在无界append显存正常但速度极慢ProbSparse在短序列上的索引开销seq_len128时改全注意力推理延迟高decoder层数过多d_layers保持1层多卡训练不加速数据管道成瓶颈num_workers调到4到8单步时间波动大数据加载在同步等待开pin_memory并预取其中“多卡不加速”这条我印象很深。一开始以为是通信开销profiling之后发现GPU利用率只有30%瓶颈在数据加载——滑动窗口的切片操作在Python层做每个batch要跑几十次numpy切片。把Dataset改成预先构造索引数组、__getitem__里只做一次数组索引之后吞吐直接翻了一倍多。注意不要一上来就开多卡。这个模型参数量才10M出头单卡完全够用多卡带来的同步开销在小模型上经常是负收益。先把单卡跑满再说。4.4 一份可以直接对照的排查清单遇到效果不达预期按这个顺序走一遍八成问题能定位打印一条完整的样本x的shape、y的shape、时间特征的数值、归一化前后的取值范围。用同一份数据跑一遍“复制最近值”的平凡基线看模型的MAE是不是真的低于它。把训练集上的预测结果画出来如果训练集上都拟合不好问题在模型或管道不在泛化。检查测试集是不是时间上真的在训练集之后中间有没有重叠。把factor从一个极大值等价全注意力调到5看结果差多少判断ProbSparse是否在拖后腿。关掉蒸馏层把e_layers设1再跑一次判断蒸馏是不是把关键信息滤掉了。最后再动学习率和dropout。这套流程看似笨但比漫无目的地调参快得多。我现在的习惯是每次换新数据集先花二十分钟把这七步跑一遍形成一个基线认知后面所有改动都和这个基线比。5. 这套架构的演进脉络与选型建议5.1 后续方案修正了哪些遗留问题Informer之后这个方向上有几类明显的修正思路了解它们能帮你判断自己该不该继续用Informer。一类是质疑注意力必要性的。有工作直接用“输入线性映射加季节性分解”的极简结构在多个公开数据集上把Transformer类模型打得很难看。这个结论对从业者的启示是先跑线性基线如果线性模型已经够用就不要上Transformer。我在两个项目里验证过其中一个日频销量预测场景线性分解模型的效果确实优于调过参的Informer而且训练快了二十倍。一类是patch化输入的。把长序列切成一个个子片段当作token而不是每个时间点一个token。这样序列长度直接降一个数量级注意力计算量骤减同时每个patch内部保留了局部形状信息——恰好补上了Informer蒸馏层丢失局部细节的短板。我现在做长跨度预测的默认起点已经改成patch化方案Informer更多作为对照基线。还有一类是换注意力维度的。把注意力从时间维搬到变量维让每个变量成为一个token专门建模变量间的相关性。这对多变量场景比如几十个传感器同时预测收益明显但如果你的变量数只有个位数收益有限。至于频域方向的尝试把序列变换到频域只保留低频分量再变回来对强周期数据效果不错但实现复杂度高调参空间大我一般放在第三顺位考虑。5.2 我的实际选型顺序经过这几年的反复试错我现在拿到一个长时间序列预测需求处理顺序是这样的。第一步永远先建朴素基线。季节性朴素取上周同位置、移动平均、线性回归三个基线一小时内能跑完它们给出的误差量级决定了后面值不值得投入。第二步如果基线误差不能接受先上轻量的线性分解模型。训练快、调参少、可解释性好很多时候这就够了。第三步如果线性模型明显不够通常是多变量、强非线性交互的场景再上Transformer类。这时候Informer是个合理的起点因为它的代码实现成熟、社区资料多、跑通成本低。但要清楚它的定位是“能跑通的基线”不是“最终方案”。第四步确定要用Transformer之后先试patch化输入再考虑蒸馏和稀疏注意力。我的实测结论是在中等长度96到336的场景下patch化的收益大于ProbSparse而蒸馏层在数据噪声大的时候反而是负担——它会把你想要捕捉的短时异常一起滤掉。最后分享一个我在实际部署里觉得最值钱的习惯把每次实验的完整配置、随机种子、数据切分边界、以及那条分horizon误差曲线全部存进一个结构化的记录里。这个模型对随机种子比想象中敏感同一个配置换个种子pred_len336的MAE能差5%到8%。没有这份记录两周后你根本说不清“上次那个好结果”到底是怎么来的。我吃过这个亏一个效果不错的配置没记全后面复现了三天才对上。