1. 从零开始:为什么我们需要Informer?

如果你做过时间序列预测,比如预测明天的天气、下周的股票走势,或者未来几个小时的电力负荷,你肯定知道这活儿有多头疼。传统的模型,比如ARIMA或者简单的LSTM,对付短期预测还行,一旦序列变长,比如要预测未来24小时甚至更久,它们就开始“力不从心”了。计算量爆炸、预测精度下降,这些都是家常便饭。我自己在做一个能源消耗预测项目时就踩过这个坑,用LSTM预测未来48小时的数据,训练慢得像蜗牛,结果还飘得厉害。

这时候,Informer 模型就登场了。它是在经典的Transformer架构上,专门为长序列时间序列预测(Long Sequence Time-series Forecasting, LSTF) 任务设计的。Transformer大家应该不陌生,它在自然语言处理里是大杀器,但直接拿来预测时间序列,尤其是长序列,有两个致命伤:一是自注意力机制的计算复杂度是序列长度的平方级(O(L²)),序列一长,内存和算力根本扛不住;二是原始的编解码结构在预测超长未来时,效率不高。

Informer的聪明之处在于,它用了一个叫 ProbSparse Self-Attention(概率稀疏自注意力) 的机制,巧妙地解决了计算量大的问题。简单来说,它不再傻乎乎地计算所有元素之间的注意力,而是像考试划重点一样,只挑选出那些最“关键”的查询(Query)去计算注意力,大大减少了计算量。同时,它还引入了蒸馏编码器来压缩序列长度,让模型能处理更长的历史数据。实测下来,在预测未来96个甚至更长时间点的任务上,Informer的速度和精度都比传统模型稳得多。

所以,无论你是想预测金融数据、物联网传感器数据,还是像天气、交通流量这类具有明显周期性的数据,Informer都是一个非常值得尝试的强力工具。接下来,我就带你手把手走一遍Informer的实战流程,从数据准备到模型训练,把每个环节的代码和原理都掰开揉碎了讲清楚。

2. 实战第一步:准备你的数据和环境

在撸起袖子写代码之前,我们得先把“厨房”收拾好。这里的环境准备主要就是安装必要的Python库,并确保你的数据格式是正确的。

2.1 安装依赖库

Informer的实现主要依赖于PyTorch。我建议创建一个新的虚拟环境来管理依赖,避免版本冲突。你可以用conda或者venv。这里我用pip直接安装核心库:

pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118  # 请根据你的CUDA版本调整
pip install pandas numpy scikit-learn matplotlib

如果你的数据需要更复杂的时间特征处理,可能还需要 holidays 等库,但Informer自带的工具函数基本够用。官方的Informer代码库还包含一些自定义的工具模块(utils),我们需要把这些文件也准备好。通常你可以直接从GitHub上克隆整个项目:

git clone https://github.com/zhouhaoyi/Informer2020.git
cd Informer2020

这样,utils文件夹里的 timefeatures.py, tools.py, masking.py 等关键模块就都有了。我们后续的代码会基于这个项目结构展开。

2.2 理解你的数据格式

这是至关重要的一步。Informer的DataLoader设计得很友好,但它对数据格式有一个基本假设。我们来看一下它期望的数据长什么样。通常,你的数据应该是一个CSV文件,例如 WTH.csv(天气数据),它的列可能包括:

date, WetBulbCelsius, DryBulbCelsius, RelativeHumidity, WindSpeed, ...
2020-01-01 00:00, 10.2, 12.5, 65, 3.1, ...
2020-01-01 01:00, 9.8, 12.1, 68, 2.9, ...
...

关键点:

  1. 第一列必须是时间列,列名通常为 date。Informer会解析这一列来生成时间戳特征。
  2. 中间是多列特征(比如温度、湿度、风速等)。
  3. 最后一列是你的目标变量。比如你想预测“湿球温度”(WetBulbCelsius),那么它就应该放在最后一列,并且在参数中通过 --target 指定。

在代码里,我们用 features 参数来控制输入和输出的模式:

  • M (Multivariate → Multivariate): 用所有特征预测所有特征。输入是N列特征,输出也是同样的N列。
  • S (Univariate → Univariate): 只用目标变量这一列预测它自己。这是单变量预测。
  • MS (Multivariate → Univariate): 最常用的场景! 用所有特征(多变量)的信息,来辅助预测最终的那个目标变量(单变量)。这能充分利用其他相关特征的信息,通常效果最好。

比如,你用温度、湿度、风速来预测最终的湿度,这就是 MS 模式。在数据加载时,模型会自动处理好这些列的组合。

3. 代码核心解析:从DataLoader到模型前向传播

现在,我们进入硬核部分,一行行代码看Informer是怎么工作的。我会结合我自己的调试经验,把容易踩坑的地方都指出来。

3.1 数据加载器(DataLoader):如何喂数据给模型?

数据加载器是模型训练的“炊事班”,它负责把原始数据切成模型能消化的小块(batch)。Informer自定义的 Dataset_Custom 类是这个环节的灵魂。我们仔细看看它的 __getitem__ 方法,这是理解数据如何组织的关键。

def __getitem__(self, index):
    s_begin = index
    s_end = s_begin + self.seq_len  # 输入序列的结束位置
    r_begin = s_end - self.label_len
    r_end = r_begin + self.label_len + self.pred_len

    seq_x = self.data_x[s_begin:s_end]  # 编码器输入
    seq_y = self.data_y[r_begin:r_end]   # 解码器预期输出(包含已知部分和待预测部分)
    seq_x_mark = self.data_stamp[s_begin:s_end]  # 输入序列的时间特征
    seq_y_mark = self.data_stamp[r_begin:r_end]  # 输出序列的时间特征
    return seq_x, seq_y, seq_x_mark, seq_y_mark

这里有几个关键参数需要理解,它们直接对应主程序 main.py 里的设置:

  • seq_len (例如96): 编码器(Encoder)看的过去历史序列的长度。模型通过这96个时间点的数据来学习规律。
  • label_len (例如48): 这是一个非常巧妙的设计!它表示解码器(Decoder)输入中,已知部分的长度。在预测未来时,我们不是凭空猜测,而是把紧挨着预测点之前的48个已知真实数据(作为“提示”或“开始令牌”)送给解码器。
  • pred_len (例如24): 我们要预测的未来序列长度。

数据组织的核心逻辑: 假设 seq_len=96, label_len=48, pred_len=24。

  1. 编码器拿到的是从索引 s_begin 开始的连续96个时间步的数据 (seq_x)。
  2. 解码器的输入(在训练时由 seq_y 的前 label_len+pred_len 步构成)和预期输出(seq_y)是从 r_begin 开始的72步(48+24)。注意,r_begin = s_end - label_len。这意味着解码器输入的已知部分(前48步)与编码器输入的最后48步是重叠的。这种设计让解码器在生成预测时,有一个平滑的、基于已知真实数据的“起步”。
  3. 在训练时,seq_y 的后24步(pred_len)是真实值,用于计算损失。在预测(推理)时,这24步就是我们要模型填写的“空白”。

我第一次看的时候也被这个 label_len 绕了一下,后来想通了:这就像是让你续写一篇作文,我不仅告诉你作文题目(编码器看全文),还把最后两段真实的结尾(label_len)给你看,然后让你接着这两段往下写新的段落(pred_len)。这样你续写的时候,风格和上下文就更连贯了。

3.2 模型架构总览:Informer类

数据准备好了,接下来看模型本身。informer.py 里的 Informer 类是总装车间。它的初始化函数定义了所有组件:

def __init__(self, enc_in, dec_in, c_out, seq_len, label_len, out_len,
             factor=5, d_model=512, n_heads=8, e_layers=2, d_layers=1, ...):
    super(Informer, self).__init__()
    self.pred_len = out_len
    # 1. 嵌入层
    self.enc_embedding = DataEmbedding(enc_in, d_model, embed, freq, dropout)
    self.dec_embedding = DataEmbedding(dec_in, d_model, embed, freq, dropout)
    # 2. 选择注意力类型
    Attn = ProbAttention if attn == 'prob' else FullAttention
    # 3. 构建编码器堆叠(带蒸馏)
    self.encoder = Encoder([EncoderLayer(...) for _ in range(e_layers)],
                           [ConvLayer(d_model) for _ in range(e_layers-1)] if distil else None,
                           norm_layer=nn.LayerNorm(d_model))
    # 4. 构建解码器
    self.decoder = Decoder([DecoderLayer(...) for _ in range(d_layers)],
                           norm_layer=nn.LayerNorm(d_model))
    # 5. 最终投影层
    self.projection = nn.Linear(d_model, c_out, bias=True)

关键参数解读:

  • d_model (默认512): 这是模型内部的统一维度。无论你原始特征有多少维,嵌入层都会把它们映射到这个维度。可以理解为模型内部处理的“语言”的词汇表大小。
  • n_heads (默认8): 多头注意力的头数。每个头会从不同的“子空间”学习信息,最后再合并。8个头是一个经验上比较好的平衡点。
  • e_layers 和 d_layers: 编码器和解码器的层数。对于很多时间序列任务,2层编码器和1层解码器(论文中的默认配置)已经足够,增加层数可能会过拟合,且训练更慢。
  • factor (默认5): 这是ProbSparse注意力的核心参数!它控制了“划重点”的力度。factor 越大,参与计算的“关键查询”数量就越多(u = factor * log(L)),计算越精细但也越慢。一般保持默认值5即可。

前向传播流程:

def forward(self, x_enc, x_mark_enc, x_dec, x_mark_dec, ...):
    # 编码器部分
    enc_out = self.enc_embedding(x_enc, x_mark_enc)  # (B, seq_len, d_model)
    enc_out, attns = self.encoder(enc_out, attn_mask=enc_self_mask) # 经过多层ProbAttention和蒸馏
    # 解码器部分
    dec_out = self.dec_embedding(x_dec, x_mark_dec)  # (B, label_len+pred_len, d_model)
    dec_out = self.decoder(dec_out, enc_out, ...)    # 先自注意力,再与编码器输出做交叉注意力
    dec_out = self.projection(dec_out)               # (B, pred_len, c_out)
    return dec_out[:, -self.pred_len:, :]  # 只返回我们预测的那部分

这个过程清晰明了:编码器消化历史序列,提取出一个浓缩的上下文表示 (enc_out)。解码器则拿着这个上下文,结合自己已知的起始部分 (label_len),一步步推演出未来的序列。最终一个线性层 (projection) 把高维的 d_model 映射回你需要的输出维度 c_out(比如你要预测7个特征,c_out就是7)。

3.3 灵魂所在:ProbSparse Attention机制详解

这是Informer性能提升的关键,我们稍微深入一点。传统的自注意力需要计算一个 L x L 的矩阵(L是序列长度),每个元素代表两个位置的相关性。当L很大时(比如96甚至更长),这个矩阵的计算和存储成本极高。

ProbSparse Attention的想法很直观:不是所有查询(Query)都同等重要。有些查询和大部分键(Key)的相似度都很平均(信息量低),有些则和少数键特别相关(信息量高)。我们只需要对那些信息量高的“关键查询”进行精确计算即可。

它的实现步骤可以概括为:

  1. 随机采样:对于每个查询,不是和所有L个键计算分数,而是随机抽取一小部分(比如 U_part = factor * log(L) 个)键来计算一个“近似”分数。
  2. 评估重要性:根据这个近似分数,计算每个查询的“稀疏性度量” M。M 值越大,代表这个查询的注意力分布越“尖锐”(越不平庸),它就越重要。
  3. 筛选Top-u查询:只选出 u = factor * log(L) 个 M 值最大的查询(即最重要的查询)。
  4. 精确计算:只对这 u 个关键查询,计算它们与所有键的完整注意力分数。对于其他非关键查询,直接用所有值的平均值来近似它们的输出(这是一个非常高效的近似)。
# 摘自 attention.py 的 _prob_QK 方法(简化理解)
def _prob_QK(self, Q, K, sample_k, n_top):
    # Q, K: [B, H, L, D]
    # 1. 随机采样sample_k个键,为每个查询计算近似分数
    index_sample = torch.randint(L_K, (L_Q, sample_k))
    K_sample = K[:, :, torch.arange(L_Q).unsqueeze(1), index_sample, :]
    Q_K_sample = torch.matmul(Q.unsqueeze(-2), K_sample.transpose(-2, -1)).squeeze()
    # 2. 计算稀疏度量M:用最大分数减去平均分数
    M = Q_K_sample.max(-1)[0] - Q_K_sample.sum(-1) / L_K
    # 3. 选出最重要的n_top个查询
    M_top = M.topk(n_top, sorted=False)[1]
    # 4. 只计算这些重要查询与所有键的完整分数
    Q_reduce = Q[..., M_top, :]
    Q_K = torch.matmul(Q_reduce, K.transpose(-2, -1))
    return Q_K, M_top

通过这种方式,ProbSparse Attention将计算复杂度从 O(L²) 降低到了 O(L log L),这是一个巨大的提升。在我自己用天气数据(序列长度96)的测试中,开启ProbAttention比使用全注意力(Full Attention)训练速度快了接近40%,而预测精度几乎没有损失,在处理更长序列(如168)时优势更明显。

4. 训练与调参:让你的Informer真正工作起来

模型搭好了,最后一步就是训练和调参。这是把理论变成实际效果的关键环节,也是最容易出问题的地方。

4.1 训练循环与损失函数

训练的核心在 exp_informer.py 的 train 方法里。流程是标准的深度学习训练循环:

for epoch in range(train_epochs):
    model.train()
    for batch_x, batch_y, batch_x_mark, batch_y_mark in train_loader:
        optimizer.zero_grad()
        # 1. 构造解码器输入
        dec_input = torch.zeros_like(batch_y[:, -self.args.pred_len:, :]).float()
        dec_input = torch.cat([batch_y[:, :self.args.label_len, :], dec_input], dim=1).to(device)
        # 2. 前向传播
        outputs = model(batch_x, batch_x_mark, dec_input, batch_y_mark)
        # 3. 计算损失(只计算预测部分)
        f_dim = -1 if self.args.features == 'MS' else 0
        outputs = outputs[:, -self.args.pred_len:, f_dim:]
        batch_y = batch_y[:, -self.args.pred_len:, f_dim:].to(device)
        loss = criterion(outputs, batch_y)
        # 4. 反向传播与优化
        loss.backward()
        optimizer.step()

注意点:

  • 解码器输入构造:dec_input 的前 label_len 位是真实的已知数据 (batch_y[:, :label_len, :]),后 pred_len 位是用零填充的(表示待预测的未知部分)。这就是前面说的“续写作文”时给出的那两段真实结尾。
  • 损失计算:我们只关心模型对未来的预测 (pred_len 部分) 是否准确,所以损失函数只作用于输出的最后 pred_len 个时间步。常用的损失函数是MSE(均方误差),对于有异常值的数据可以考虑MAE(平均绝对误差)。

4.2 关键超参数调优指南

直接跑默认参数可能效果不错,但要想获得最佳性能,调参是必须的。下面是我根据多次实验总结的一些经验:

参数含义调参建议与影响
seq_len输入历史长度非常重要! 需要覆盖数据的周期。例如,小时级数据且有日周期,至少设为24。有周周期可考虑168。太短学不到规律,太长增加计算负担且可能引入噪声。建议从96或168开始尝试。
label_len解码器已知长度通常设为 pred_len 的1到2倍。它提供了预测的“上下文”。对于平稳序列可以短些(如24),对于波动大的序列可以长些(如48)。
pred_len预测未来长度根据你的实际需求设定。模型在训练时就是学习预测这个长度的序列。
d_model模型内部维度默认512对大多数任务足够。如果数据非常复杂或特征很多,可以尝试增加到768或1024,但会显著增加参数量和训练时间。
factorProbAttention采样因子控制稀疏度。默认5是一个很好的起点。如果预测任务非常复杂,可以尝试稍微增大(如8)以获取更精确的注意力;如果追求极致的训练速度,可以减小(如3),但可能会损失一些精度。
learning_rate学习率Transformer类模型通常喜欢较小的学习率。默认的0.0001 (1e-4) 是安全的起点。如果训练损失下降很慢,可以尝试增大到3e-4;如果训练不稳定(损失震荡),则降低到5e-5。配合学习率调度器(如 lradj='type1',会在训练中按计划降低学习率)效果更好。
batch_size批大小在GPU内存允许的情况下,尽量设大一些(如32, 64)。更大的batch通常能使梯度估计更稳定,有助于收敛。如果内存不足,可以减小,但可能需要更仔细地调整学习率。
dropout丢弃率防止过拟合。默认0.05较低。如果你的训练集不大,或者模型很快在训练集上过拟合(训练损失很低但验证损失很高),可以尝试增加到0.1或0.2。

一个实用的调参流程:

  1. 固定结构,调时序参数:先用默认的 d_model=512, e_layers=2, d_layers=1,重点调整 seq_len, label_len 和 learning_rate。找到能让验证集损失平稳下降的组合。
  2. 调整模型容量:如果第一步效果不佳,考虑增加 d_model 或网络层数 (e_layers, d_layers),看看模型能否学习更复杂的模式。
  3. 微调注意力与正则化:最后调整 factor 和 dropout,在速度和精度、过拟合与欠拟合之间找到最佳平衡。

4.3 常见问题与排查(踩坑记录)

在我自己的使用过程中,遇到过几个典型问题:

  1. 训练损失不下降:

    • 检查数据标准化:确保 DataLoader 中的 scale=True,并且训练集、验证集、测试集使用的是同一个 StandardScaler 拟合的均值和方差。这是最容易出错的地方之一,如果分别标准化,会导致数据分布不一致,模型完全学不到东西。
    • 检查学习率:学习率可能太大了(导致震荡)或太小了(下降极慢)。尝试使用默认的1e-4,并观察前几个epoch损失是否有明显下降趋势。
    • 检查输入/输出维度:确认 enc_in, dec_in, c_out 参数与你的数据特征数 features 设置匹配。特别是在 MS 模式下,c_out 应该是1(只预测目标列),而 enc_in 是特征总数。
  2. 验证损失早期就上升(过拟合):

    • 增加Dropout:这是最直接的手段,将 dropout 从0.05提高到0.1或0.2。
    • 减少模型容量:尝试减小 d_model(如从512降到256)或减少层数。
    • 获取更多数据或使用数据增强:对于时间序列,可以在不破坏时序的前提下,对训练数据进行小幅度的随机缩放或添加噪声。
  3. 预测结果全是均值(模型输出平淡):

    • 检查 label_len:如果 label_len 设得太短,解码器起步的“上下文”信息不足,可能导致它倾向于输出保守的均值。尝试增加 label_len。
    • 检查损失函数:确保损失函数计算的是预测部分 (pred_len) 的误差,而不是整个 seq_y。
  4. GPU内存溢出:

    • 减小 batch_size 或 减小 seq_len。
    • 如果使用了 informerstack(堆叠编码器),注意它会占用更多内存,可以回退到标准的 informer。

最后,别忘了利用TensorBoard或WandB等工具来可视化你的训练过程,监控训练/验证损失曲线、注意力权重分布等,这能帮你更直观地理解模型的行为和调参方向。Informer是一个强大的工具,但它也需要耐心和细致的调试。希望这篇详细的实战解析能帮你避开我踩过的那些坑,顺利地将Informer应用到你的时间序列预测项目中。记住,没有一劳永逸的参数,最好的模型永远是那个最理解你数据的模型。

更多推荐