多尺度时域融合Transformer可解释时序预测实践

发布时间:2026/11/6 7:02:57
多尺度时域融合Transformer可解释时序预测实践

时域预测这个方向,模型迭代了十几年,从传统统计方法到梯度提升树,再到循环网络,每一代都在补上一代的短板。可真到了产线上、到了业务系统里,卡住落地的往往不是精度差那零点几个百分点,而是两件事:模型给出的数字说不清来路,以及模型对多尺度节奏到底有没有学到。这套基于时域融合Transformers的可解释预测模型,我前后在三个不同场景里复现过,踩的坑比想象中多。它要解决的核心问题是:让一个具备长程建模能力的深度预测模型,既能同时吃进小时级、天级、周级三种节奏的信息,又能把“为什么这么预测”以时间步粒度交代清楚。适合正在做时序预测落地、被业务方追问过“依据是什么”的算法同学,也适合刚接触Transformer、想搞明白多尺度融合与归因怎么搭起来的新手。

1. 三个硬骨头凑到一起:这类模型到底在解决什么问题

1.1 时序预测落地路上的三道坎

做预测模型的人多半经历过这样的场景:离线指标刷到很低,上线之后业务方用了一周,回头找你,说这个预测“不敢信”。追问下去,理由通常很朴素——上周设备明明有过一次异常波动,模型完全没反应;或者某天临时停产,模型还在按老规律外推。这背后其实是三道坎。

第一道是多尺度节奏并存。真实的时序信号很少是单一周期的,工业设备的振动烈度里混着班次周期、保养周期和磨损趋势;用电负荷里叠着日内双峰、周内工作日与休息日差异、季节漂移。单一时间尺度的卷积核或者单层注意力,很难同时把这三层信息抓干净。

第二道是长程依赖。很多预测任务的有效上下文远超几十步。以15分钟采样粒度计算,一周就是672个点,如果只喂给模型最近一天的数据,模型看不到“上周同一时段”这个极有价值的参照。

第三道是解释。这一条在学术论文里往往被处理成一个附加实验,但在工程上它决定模型能不能进决策链路。一个预测值如果无法拆解成“主要受哪几个时间片段影响”,运维人员就只会把它当参考,不会把它当依据。

注意:这三道坎不是并列关系。多尺度融合做不好,长程依赖学到的往往是噪声;解释模块建在错误的融合结构上,归因结果会指向一堆无意义的时间步。

1.2 为什么换成Transformer,又不只是Transformer

循环网络处理长序列时有两个绕不开的限制:一是串行计算导致训练慢,二是信息在隐状态里逐级压缩,672步走完,早期信息已经被稀释得差不多了。Transformer的自注意力把任意两个时间步直接连起来,路径长度从O(L)降到O(1),这是它在这个任务上最实在的优势。

但直接用原版自注意力会撞上复杂度墙。长度672的序列,单层单头的注意力计算量大致是 2×L²×d,取 d=128,就是 2×672²×128 ≈ 1.16 亿次乘加,还只是一层一个样本。四层八头堆上去,显存和时间都不好看。更麻烦的是,注意力权重在长序列上容易变得很平,几乎均匀分布,这时候即使你把权重画出来,也读不出任何有用信息。

所以这套模型的骨架不是“Transformer”,而是“先做时域上的结构化压缩,再让注意力在压缩后的表示上工作”。这是我理解的整个方案的核心取舍:把多尺度融合放在注意力之前,而不是让注意力自己去学多尺度。原因很直接——让注意力从零学多尺度,需要大量数据,而且学出来的模式往往不可名状;用固定结构的卷积核先把不同尺度显式地抽出来,再让注意力去做跨尺度的关联,收敛快,解释也更好落。

1.3 可解释在这里的定位:不是汇报材料,是调试工具

很多人把可解释性理解成给领导看的图。我的用法恰恰相反,它首先是个调试工具。

举个实际的例子。早期版本在某台设备的退化预测上,验证集误差正常,但解释图显示模型高度依赖每天固定时刻的几个点。查下去发现那几个时刻正好是数据采集系统的批量写入时间,数值被集中刷新,形成了一种假周期性。模型很聪明地抓住了这个伪规律。如果没有归因这一步,这个模型上线之后,一旦采集策略调整,预测会直接崩掉,而你完全不知道为什么。

所以我在整个流程里给可解释模块安排了两个位置:训练中期用它做特征体检,排查伪规律和数据泄漏;上线后用它生成解释产物,供使用方核对。这两个用途对解释方法的要求不一样,前者需要忠实度高、能定位到具体时间步,后者需要可读性好、能用自然语言讲清楚。后面第3节会展开讲怎么选。

2. 整体架构拆解:时域融合到底融的是什么

2.1 多尺度时域融合的设计逻辑

“时域融合”这个词容易让人误以为是把多个模型的预测结果加权平均,那是集成,不是融合。这里指的是在表示层把不同时间尺度的特征拼到一起。

我采用的方案是并行多核一维卷积 + 门控加权。取三个尺度的深度可分离卷积核,大小分别是3、7、15,对应在15分钟采样粒度下约45分钟、1.75小时、3.75小时的局部感受野。这三个数字不是随便定的,它们分别对应“短时扰动”“班次内波动”“半天级趋势”三个业务上真实存在的节奏。选核大小的通用经验是:让最小核覆盖噪声平滑所需的最小宽度,让最大核大致覆盖一个业务周期的一半,中间那个取几何中值。

为什么不直接上空洞卷积堆叠?空洞卷积确实能用更少层数拿到大感受野,但它的采样点是离散跳跃的,在信号存在尖锐突变(比如设备启停)时容易漏掉关键点。小核密集卷积在捕捉突变上更扎实,代价是感受野扩展慢,所以用并行多尺度来补,而不是靠堆层数。

门控融合的做法是:三个分支各自输出 d 维表示,再通过一个轻量的门控网络算出三个标量权重(softmax归一化),加权求和。门控的输入是三分支表示的拼接经过一层线性映射。这一步的意义在于让模型自适应地决定当前样本更依赖哪个尺度——平稳期偏向大核,突变期偏向小核。实测下来,门控权重的分布本身就是一个很好的诊断信号,如果某个尺度权重长期接近0,说明这个尺度在当前数据上没有信息量,可以考虑裁掉。

2.2 编码器主干:Patch化与双分支结构

融合之后的表示进入编码器。这里有个关键动作是Patch化:把长度为672的序列按 patch_len=16、stride=16 切成42个patch,每个patch通过一层线性映射变成 d 维向量。这一步把注意力复杂度从 L² 降到 (L/patch_len)²,按前面的数字算,单层单头从约1.16亿次乘加降到 2×42²×128 ≈ 45万次,降了约256倍。这个降幅是这套架构能跑起来的前提。

Patch化的代价是时间分辨率下降。42个patch,每个代表4小时,如果你想解释“模型在第317个时间步上关注了什么”,粒度就不够了。解决办法是保留一个旁路:在patch表示之外,额外维护一个逐点的时间嵌入,通过轻量的跨注意力把patch级信息回投到点级。这个旁路不参与主干计算,只在解释阶段启用,对训练速度没有影响。

编码器主干我做成了双分支。一条是时域分支,就是上面说的patch序列走标准自注意力;另一条是频域分支,对原始序列做实数傅里叶变换,取幅值谱最高的前 K 个频率分量(K取16到32之间),每个分量编码成“频率值+幅值+相位”三元组,经过线性映射后作为额外的token拼到序列前面。频域分支的价值在于捕捉全局周期性——时域注意力要跨越672步才能把“昨天同一时刻”和“今天”联系起来,而频域分支一个分量就概括了整个周期的强度,效率高得多。

两条分支的输出在通道维度拼接,再经过一层前馈网络做整合。这里不需要额外的对齐操作,因为频域token被当作全局token处理,它们和所有patch token之间天然有注意力交互。

2.3 融合门控与通道注意力:别让小尺度淹没大尺度

多分支结构有个隐蔽的问题:不同分支输出的数值量级不一样。卷积核越大,输出越平滑,方差越小;小核输出保留了更多高频成分,方差偏大。如果直接拼接送进后续层,LayerNorm会把这些差异部分抹平,但抹不干净,结果是模型倾向于依赖方差大的分支,大尺度信息被淹没。

我的处理是在每个分支输出后加一层通道注意力(类似 Squeeze-and-Excitation 的结构),先做全局平均池化得到通道描述子,经过两层线性映射加Sigmoid得到通道权重,再乘回原表示。这一步的作用是让每个分支自己决定哪些通道该放大。参数量很小,d=128时每个分支约 128×32 + 32×128 ≈ 8200 个参数,三个分支加起来不到2.5万,但实测对收敛稳定性的帮助明显。

提示:通道注意力的压缩比不要设得太激进。我一开始用 r=16,压缩到8维,结果在小样本场景下权重几乎不动。改成 r=4 之后才正常。样本量少于5万条时,压缩比建议不超过4。

2.4 预测头设计:点预测、区间预测与多任务

预测头这块,很多实现只输出一个点估计,然后拿MSE训。工程上这不够用,因为使用方需要知道不确定性。我的做法是同时输出三个分位数:0.1、0.5、0.9。中位数作为点预测,0.1和0.9构成80%的预测区间。

分位数通过一个共享的两层前馈网络加三个独立的线性输出头实现,隐藏维度取 d 的2倍。训练时用分位数损失(pinball loss),对每个分位数 q,损失形式是 max(q×(y-ŷ), (q-1)×(y-ŷ)),取均值。这个损失的好处是不假设残差分布,对偏态数据也适用。

如果场景里同时有多个相关指标要预测(比如设备的温度、振动、电流),可以加辅助任务头做多任务学习。辅助任务的梯度能起到正则作用,主任务指标通常有小幅提升。但要注意任务权重要调,辅助任务太强会拖累主任务。我的经验是主任务权重1.0,每个辅助任务0.1到0.3之间,任务数量不超过3个。

3. 可解释模块的三条技术路线与选型

3.1 注意力类解释:好用但别全信

最省事的解释方式是把注意力权重画出来。编码器最后一层的注意力矩阵,对每个预测输出,沿head维度平均,再沿query维度求和,就得到每个patch的重要性分数。实现成本几乎为零,可视化也直观。

但这条路有个已经被反复讨论过的问题:注意力权重高不等于该位置对输出影响大。注意力只是信息路由的一个中间量,后面还有前馈网络、残差连接、归一化。在某些结构里,注意力和最终梯度之间的相关性可以低到0.3以下。

我的用法是把它当作粗筛,不作为最终结论。具体操作是对最后一层和倒数第二层都算一遍注意力分数,看两层排序的一致性。如果两层给出的top-10 patch重合度低于50%,说明这个样本的解释不稳定,需要换用更可靠的方法复核。这个一致性检查很便宜,我基本每个样本都会跑。

另外提一个实现细节:多头注意力要沿head平均,不能取最大值。取最大值会让解释变得很尖锐,看起来每个样本都只有一两个关键位置,实际上那是head之间分工差异造成的假象。

3.2 梯度与遮挡类归因:忠实度最高

真正拿来做结论的是这两类方法。

梯度类用积分梯度(Integrated Gradients)。对输入序列 x,构造从基线 x'(一般取全零或训练集均值序列)到 x 的线性插值路径,在 m 个插值点上算梯度并对插值步长积分。m 取32到64就够,再多收益递减。积分梯度满足敏感性和实现不变性两条公理,在有明确基线的时序任务上表现稳定。代价是要做 m 次前向+反向,单样本解释耗时大概是普通推理的30到60倍。离线批量跑可以接受,实时场景要斟酌。

遮挡类更朴素但更可信。对每个patch,把它替换成基线值(均值填充或者相邻patch的线性插值),观察预测输出的变化量。42个patch就要跑42次前向,成本比积分梯度低。缺陷是patch之间可能存在交互作用,单独遮挡某个patch时影响被低估。

这两类结果我一般会做一次一致性比对。如果某个patch在两种方法下都排进top-5,就认定它是关键片段;如果只在一处出现,标为待定。这套双路交叉验证的做法,比单用任何一种都靠得住。

3.3 替代模型与规则抽取:给使用方看的版本

前两类方法的输出是分数序列,工程人员能看懂,业务人员看不懂。所以还需要一层转换。

我用的方法是局部替代模型:以某个待解释样本为中心,在其邻域内随机扰动生成500到1000个样本(扰动方式是对各patch做时间平移、幅值缩放、局部置零),用原模型打标签,然后在这些扰动样本上训一个轻量的可解释模型——通常是用3到5个特征的决策树,或者带L1正则的线性模型。树的结构可以直接翻译成规则,比如“若最近4小时的均值高于阈值,且前一日同时段无异常,则预测上调”。

要注意这种做法解释的是局部行为,不能外推成全局规律。我在报告里会明确标注“该规则仅适用于当前样本邻域”,避免使用方误以为是模型的通用逻辑。

3.4 解释的忠实度怎么量化

解释方法本身也需要被评估,否则你不知道画出来的图是不是在骗自己。我用的核心指标是AOPC(Area Over Perturbation Curve)。

做法是:按重要性分数从高到低,依次删除(或替换为基线)前 k 个时间步,k 从1递增到序列长度的一个比例(比如30%),每删一次记录预测误差的增量。把误差增量对 k 求曲线下面积,除以 k 的总数归一化。数值越大,说明被标为重要的部分确实重要,解释越忠实。

我在三个数据集上测过的典型数值是:纯注意力解释 AOPC 约0.18,积分梯度约0.41,遮挡法约0.37。这个差距足以说明为什么不能只信注意力。

4. 实操全流程:从数据到解释产物

4.1 数据预处理与滑窗构造

假设采样周期15分钟,要做未来24小时的预测。

步长换算:24小时 = 96个点,这就是 pred_len。输入长度取一周,L = 96×7 = 672。为什么要一周?因为大多数业务节奏的最小完整周期是一周(工作日与休息日的模式差异),短于这个长度,模型学不到周内模式。

滑窗步长取1(逐点滑),这样一条长度为 N 的序列能产出 N - 672 - 96 + 1 个样本。N=10万时约9.9万个样本,数据量是够的。如果数据量小,步长可以取 pred_len 的一半,牺牲样本数换独立性。

标准化这块有个坑。全局z-score标准化在存在趋势漂移的序列上会出问题——早期数据被压到很小的区间,晚期数据超出训练分布。我改用滑动窗口内的实例标准化:对每个输入窗口单独算均值和标准差,输入和输出都按这个统计量归一化,预测完再反变换回去。这样模型学到的是“相对形状”而不是绝对水平,跨时间段泛化明显更稳。代价是模型失去了对绝对量级的感知,如果你的任务里绝对水平本身很重要(比如预测峰值是否超标),需要在输入里额外拼接一个窗口均值作为特征。

缺失值处理上,短缺口(连续少于4个点)用线性插值,长缺口用同周期同位点的中位数填充,并在输入里加一个二值的缺失指示通道,让模型自己判断这段要不要信。

4.2 核心代码骨架

下面是多尺度融合块和整体模型的骨架,PyTorch实现,去掉了一些工程细节但跑得通。

import torch import torch.nn as nn import torch.nn.functional as F class MultiScaleFusion(nn.Module): def __init__(self, d_model, kernels=(3, 7, 15), dropout=0.1): super().__init__() self.branches = nn.ModuleList([ nn.Conv1d(d_model, d_model, k, padding=k // 2, groups=d_model) for k in kernels ]) self.points = nn.ModuleList([ nn.Conv1d(d_model, d_model, 1) for _ in kernels ]) self.gate = nn.Sequential( nn.Linear(d_model * len(kernels), d_model), nn.GELU(), nn.Linear(d_model, len(kernels)) ) self.norm = nn.LayerNorm(d_model) self.drop = nn.Dropout(dropout) def forward(self, x): # x: (B, L, D) h = x.transpose(1, 2) # (B, D, L) outs = [] for conv, pw in zip(self.branches, self.points): outs.append(pw(conv(h))) outs = torch.stack(outs, dim=1) # (B, K, D, L) pooled = outs.mean(dim=-1).mean(dim=-1) # (B, K, D) -> 全局描述子 w = torch.softmax(self.gate(pooled.flatten(1)), dim=-1) # (B, K) fused = (outs * w[:, :, None, None]).sum(dim=1) # (B, D, L) return self.norm(fused.transpose(1, 2) + x), w class FusionTransformer(nn.Module): def __init__(self, c_in=1, d_model=128, patch_len=16, n_layers=4, n_heads=8, ff_dim=512, pred_len=96, n_freq=24): super().__init__() self.patch_len, self.pred_len = patch_len, pred_len self.embed = nn.Linear(patch_len * c_in, d_model) self.fusion = MultiScaleFusion(d_model) self.freq_proj = nn.Linear(3, d_model) self.n_freq = n_freq layer = nn.TransformerEncoderLayer(d_model, n_heads, ff_dim, batch_first=True, norm_first=True) self.encoder = nn.TransformerEncoder(layer, n_layers) self.head = nn.Sequential(nn.Linear(d_model, ff_dim), nn.GELU()) self.out = nn.Linear(ff_dim, pred_len * 3) # 三个分位数 def forward(self, x, return_attn=False): B, L, C = x.shape x, gate_w = self.fusion(x) # 频域全局 token spec = torch.fft.rfft(x[..., 0], dim=1) amp = spec.abs() idx = amp.topk(self.n_freq, dim=1).indices # (B, n_freq) feat = torch.stack([ idx.float() / L, amp.gather(1, idx), spec.angle().gather(1, idx) ], dim=-1) # (B, n_freq, 3) freq_tok = self.freq_proj(feat) # patch 化 p = x.unfold(1, self.patch_len, self.patch_len) # (B, P, C, pl) p = p.permute(0, 1, 3, 2).reshape(B, -1, self.patch_len * C) tok = self.embed(p) tok = torch.cat([freq_tok, tok], dim=1) h = self.encoder(tok) z = h.mean(dim=1) y = self.out(self.head(z)).view(B, self.pred_len, 3) return (y, gate_w) if not return_attn else (y, gate_w, h)

分位数损失这么写:

def pinball_loss(pred, target, quantiles=(0.1, 0.5, 0.9)): # pred: (B, H, 3) target: (B, H) loss = 0.0 for i, q in enumerate(quantiles): e = target - pred[..., i] loss = loss + torch.maximum(q * e, (q - 1) * e).mean() return loss / len(quantiles)

4.3 训练策略与损失权重

总损失由三部分组成:主预测损失(分位数损失)、尺度一致性正则、以及可选的辅助任务损失。

主损失权重1.0。尺度一致性正则权重0.05,作用是约束不同尺度的分支输出在预测结果上的贡献不要过度倾斜,具体做法是对门控权重 w 算熵,熵太低说明模型退化成只用单一尺度,加一个负熵项做惩罚。这个系数不能大,超过0.1会让门控失去区分能力,变成均匀加权。

优化器用AdamW,学习率1e-4,权重衰减1e-4。批大小64。训练轮数看数据量,10万样本大概30到50轮收敛。学习率调度用余弦退火加3轮线性预热,预热的必要性在于Transformer对初始学习率很敏感,直接上1e-4前几轮损失会跳。

早停的判据用验证集的MASE而不是MSE。MSE会被大值样本主导,MASE用朴素预测(上一个周期同位置的值)做归一化,对量级差异更鲁棒。耐心值设8轮。

显存估算:批64、序列672、d=128、4层,实测单卡24GB显存大概能跑批96,批64留有余量。如果显存紧张,最有效的办法是减小patch重叠(stride等于patch_len,不做重叠),其次是减层数。

4.4 解释产物的生成与可视化落地

解释流程我是离线批量跑的,不做实时。流程是:对每个待解释样本,先算注意力分数做粗筛,再跑积分梯度(m=48)和遮挡法(逐patch),三路结果合并成一张重要性表,取交集作为“高置信关键片段”,取并集减交集作为“待定片段”。

输出物分三层。第一层是给算法同学的时间步重要性曲线,横轴时间,纵轴归因值,正负用颜色区分。第二层是给工程同学的片段摘要,比如“本次预测主要依据:前一日同时段(权重0.31)、最近4小时均值(权重0.24)、上周同期(权重0.19)”。第三层是给使用方的自然语言结论,由局部替代模型抽取的规则生成。

注意:自然语言结论里一定要带上不确定性说明。我早期的版本只输出结论,使用方把它当成了确定判断,后来出现过因为过度信任导致的操作失误。现在每条结论后面都会附上预测区间的宽度,宽度越大,措辞越保守。

4.5 推理加速与部署

线上推理只需要主干,解释模块全部离线。主干在推理时可以做的优化有几个。

一是把频域分支的FFT结果缓存。如果输入窗口是滑动产生的,相邻两次推理有671/672的重叠,理论上可以增量更新。实际实现里我没做这么细,因为FFT本身开销不大,单次不到1毫秒。

二是patch嵌入层和第一层注意力的计算可以合并预计算,但这属于很深的优化,收益有限。

三是量化。实测INT8量化后模型体积降到约四分之一,单样本推理延迟从8毫秒降到3毫秒左右,精度损失在MASE上约0.8%。如果业务对精度敏感,可以用FP16,延迟降到5毫秒,精度几乎无损。我的默认配置是FP16。

部署形态上,我用的是一个常驻进程加载模型,通过消息队列接收推理请求,批处理窗口设为50毫秒。这样能把零散请求攒成批,吞吐提升明显。解释任务走另一条离线队列,用同一份权重但独立进程,避免抢占在线资源。

5. 应用场景拆解与适配要点

5.1 工业设备退化趋势预测

某制造场景里,用这套模型预测关键部件的健康指标未来24小时走势。数据是振动、温度、电流三路信号,15分钟采样。这个场景的特点是突变多——设备启停、负载切换都会造成尖峰。

适配上的关键改动是:把最小卷积核从3缩到2,并在输入里增加一个“工况标记”通道(编码当前处于待机、轻载、满载)。工况标记让模型知道在不同状态下该用哪套规律,避免了把不同工况的样本混在一起学。门控权重的可视化在这个场景特别有用——停机切换时门控会明显偏向小核,说明模型识别到了突变。

解释产物在这里的主要用途是故障复核。当模型预测某指标将在数小时内突破阈值,运维会调出关键片段,看模型依据的是真实的退化趋势还是某次异常操作的残留影响。这个核对环节挡掉过不少误报。

5.2 能源与负荷侧预测

负荷预测的周期结构比工业信号规整得多,日内双峰、周内差异都很明显。这个场景里频域分支的价值最大,前几个频率分量基本就锁定了日周期和周周期。

有个细节要处理:节假日。节假日不在常规周期里,模型会按周末的模式去预测,误差偏大。我的做法是在输入里拼接一个“距最近长假的的天数”和一个“是否为特殊日”的标记,这两个特征不需要很多数据就能学出效果。另外在这个场景里,pred_len 通常取96(一天)或672(一周),取一周时要注意误差累积,分位数区间的宽度会随时间步明显张开,这是正常的,不要误以为是模型不稳定。

5.3 生理信号与院内指标预测

连续生理信号的采样率往往更高(比如1秒或5秒一次),直接套用15分钟的配置不合适。这时候要先做重采样或者加大patch_len,让每个patch覆盖的时间跨度落在分钟级。

这个场景对可解释的要求最严,因为输出要给人做参考。我的做法是把解释粒度做到点级(用前面提到的旁路回投),并且在界面上同时显示原始波形和归因热力条,让人能直观看到模型关注的是波形的哪个形态特征(比如上升沿、峰值、恢复段),而不是一个抽象的时间点。这一点在实践中比归因数值本身更重要——形态层面的解释更容易被专业人员接受和质疑。

5.4 业务量预测与运营决策

业务量预测(订单量、访问量、工单量)的好处是有大量外部特征可用,坏处是这些特征经常缺失或者延迟。这类场景里模型结构不用大改,重点在输入侧:把外部特征作为额外的通道拼进输入,缺失时用指示通道标记。

这个场景的解释产物直接对接运营决策,所以自然语言那一层要做得更细。我的经验是规则条数控制在3条以内,每条都要能对应到一个可执行的动作,比如“预测上调主要因为最近3天转化率抬升,建议提前备货”。规则太多反而没人看。

6. 常见问题与排查速查

6.1 训练侧高频问题

现象可能原因排查与处理
前几轮损失剧烈震荡学习率过高或缺少预热加3到5轮线性预热,初始学习率降到5e-5
验证损失先降后升过拟合,多尺度分支参数冗余加大dropout到0.2,或裁掉门控权重长期接近0的分支
门控权重三路接近均匀负熵正则过强或融合层梯度过小把正则系数降到0.02以下,检查融合层是否被LayerNorm压平
预测整体偏低或偏高实例标准化后未正确反变换检查反变换是否用了对应窗口的统计量
长 horizon 误差爆炸直接多步输出缺少中间约束加辅助任务预测中间步,或用分块递进预测

6.2 解释侧高频问题

注意力图和梯度归因给出的关键位置完全对不上,这是最常见的困惑。先别急着改模型,检查两件事:一是注意力是否沿head正确平均了,二是积分梯度的基线选得对不对。基线用全零在标准化后的数据上等价于用均值,但如果你的标准化是实例级的,全零对应的物理含义会随窗口变化,这时候基线应该用窗口均值序列。

另一个高频问题是解释结果在不同随机种子下差异很大。这说明模型的解不稳定,解释自然不稳。处理办法是集成多个种子的模型,对归因结果取平均,同时报告标准差。标准差大的样本直接标记为“解释不可靠”,不给结论。

还有一种情况是归因结果高度集中在序列末尾几个点。这通常意味着模型在偷懒,主要靠最近的值做外推。检查方法是看模型在长horizon上的表现,如果明显差于短horizon,说明它没学到周期结构,只是做了趋势延续。这时候要回头检查频域分支是否真的起作用了。

6.3 评估侧的高频陷阱

最大的陷阱是用随机划分做训练测试集。时序数据随机划分会造成信息泄漏——测试集的相邻窗口和训练集高度重叠,指标虚高。必须用时间切分,而且要在训练集和测试集之间留一段隔离期,隔离长度至少等于输入长度加预测长度。

第二个陷阱是只看平均误差。预测任务里,误差分布往往是长尾的,平均值好看不代表关键时刻准。我一般会额外看两个数:误差最大的5%样本的MASE,以及阈值突破事件的召回率(如果业务关心的是会不会超标)。后一个指标经常和平均误差反着走。

第三个陷阱是忽略了朴素基线。任何预测模型都应该和“上一个周期同位置的值”这个朴素预测比。我见过不少模型在MASE上没打赢朴素基线,却因为MSE比某个弱基线低而被认为有效。

7. 最后分享几条踩坑后的个人体会

关于这套架构,我最想说的是别把可解释当成最后才加的模块。它应该和融合结构一起设计——门控权重、频域分量的强度、各尺度分支的激活模式,这些都是天然的解释信号,成本几乎为零,但如果结构设计时没留出来,后期再补就要费很大劲。

第二个体会是参数不是越多越好。我最早把d_model开到256,层数堆到8层,指标只涨了不到1%,训练时间翻了三倍,而且解释变得更糊——层数一多,注意力在层间不断重组,最后层的权重已经很难追溯到输入。4层、d=128这个配置在我试过的场景里性价比最高。

第三个是解释的可信度要靠交叉验证来保。三路方法取交集这个做法看着笨,但它救过我。有一次单看注意力图,模型指向了一段完全无关的平稳区间,我差点以为是数据问题,跑了积分梯度才发现真正起作用的是前面一段被注意力稀释掉的突变。如果只看一种方法,那次排查方向就完全跑偏了。

最后一个小技巧:把门控权重的时序曲线也存下来做长期监控。这条曲线比预测误差更早暴露问题。数据分布开始漂移时,往往是门控先变——从均衡变成极端偏向某一尺度,而误差指标要过一段时间才反应过来。把这个当成一个早期预警信号,比等指标报警再查要主动得多。这套东西后续还能往两个方向延:一是把解释产物反过来喂给模型做训练信号(用归因约束模型关注合理区间),二是把门控和频域分量的统计量做成在线漂移检测器,这两个我都在试,等跑稳了再单独写。