资讯动态

Missformer原理与PyTorch实现:缺失数据建模的Transformer新思路

发布时间:2026/10/18 13:04:09 来源:尧图企业网站定制
最近在折腾序列建模说实话最头疼的不是模型不够复杂而是数据坑坑洼洼。传感器掉线、接口超时、用户行为只记录了一半这些场景拿到手里就是一张缺了角的表。一开始我也没多想直接往Transformer里塞填0加mask跑出来的结果怎么说呢loss降得动但预测值总感觉“虚”。后来看到Missformer这个思路——把缺失本身当成模型要理解的一种模式而不是要掩盖的脏数据才把这块补上了。这篇文章主要讲清楚Missformer的机制以及我用PyTorch从零实现的轻量版本代码会贴出来一行行说适合正在做时间序列、异常检测、插值补全的朋友参考。先说结论Missformer不是某个实验室放出来的超级模型它是“面向缺失数据的Transformer变体”这类思路的统称。核心就一句话——别把缺失位置简单当成padding而是让模型知道“这里缺了”“缺了多久”“前后关系是什么”再通过掩码注意力在可靠位置之间重建信息。下面从设计思路、核心机制、代码实现、调参经验一路讲过来争取让看完的人能直接上手改自己的项目。1. Missformer是什么专为残缺序列设计的Transformer变体1.1 标准Transformer处理缺失序列的三个坑大多数人处理带缺失的序列时第一反应就是“填0”。填完之后给Transformer一个padding mask让模型别去注意那些填充位。这么做在NLP里是惯用操作因为在文本里padding位置确实没有语义只要不让它们参与注意力就行。但到了时间序列或者传感器数据里问题就来了。第一个坑是位置编码。标准Transformer的位置编码是按“第几个token”来算的它假设每个token之间的间隔一样。现实里的序列根本做不到这点——传感器可能隔5秒上报一次也可能断了2分钟后突然补上报一批填0操作会骗过位置编码让模型以为缺失区间很短。第二个坑是输出处理。padding mask只会把缺失位置挡住不让它们作为key去影响别人但缺失位置自己的输出向量仍然会计算后面接MLP时照样会读到这个“基于填0生成”的向量。模型相当于被迫为缺失位置生成一个无意义的表示这个表示还会进入后续的网络层。第三个坑更隐蔽缺失模式本身是信息。连续缺失2分钟和随机散落10个单点缺失完全不是一回事。前者大概率是设备故障后者可能只是网络抖动。标准Transformer的padding mask把这些信息全部抹掉了等于把“发生了什么”直接丢进垃圾桶。所以你会发现不是Transformer不行是我们喂数据的方式有问题。我们一直把缺失当成“需要忽略的噪声”但真正有价值的信号恰恰藏在缺失的分布里。1.2 Missformer的设计定位Missformer这个名字拆开就是Missing Transformer直译是“面向缺失的Transformer”。它要解决的事情很明确在序列存在缺失、非等间隔采样、部分特征为空的情况下依然能完成预测、分类、重建等下游任务。整体设计围绕三条主线展开显式建模缺失模式不再把缺失位置当作单纯的mask而是用向量表示“缺失”这个状态让模型学习缺失本身的语义。掩码自注意力注意力只在可靠位置之间流动尽量不让缺失位置的无效值污染上下文。双通道位置信息同时编码离散位置序号和真实时间间隔避免被填0操作误导。这三条主线可以随时拆开、单独使用。哪怕你不想完全换成Missformer只把“缺失模式嵌入”加到现有模型里通常都能看到效果提升。1.3 适用的场景和不适用场景我实际试下来Missformer最擅长的是这一类场景序列本身带有明确的时间结构缺失区间有长有短而且缺失发生的位置和长度和业务目标相关。比如设备温度曲线、电力负荷数据、电商平台的用户点击流、金融产品的日频交易记录。反过来如果你的数据本身完整、等间隔、没有任何缺失那直接用标准Transformer就行上Missformer属于画蛇添足。另外如果你做的是文本分类这类NLP任务文本里的“缺失”概念和时序里的完全不同别硬套。场景类型推荐程度原因传感器时间序列补全强烈推荐缺失普遍时间间隔不规则金融日频序列预测推荐节假日导致间隔不均数据偶发缺失用户行为序列建模推荐行为日志天然稀疏事件间隔跨度大完整等间隔图像序列不推荐标准Transformer已经够用文本分类/情感分析不推荐“缺失”语义不同没必要改造2. 核心机制拆解缺失模式如何参与建模2.1 缺失标记把“有没有值”变成模型输入Missformer最基础的一个操作是把mask从“注意力里的开关”升级成“模型输入的特征”。具体做法很简单为每个时间点设置一个二值标记1代表缺失0代表有值然后把这个标记映射成一个可学习向量加到token的embedding上。self.missing_emb nn.Embedding(2, d_model) # missing_idxs 1 表示缺失0 表示有值 x value_proj(x) self.missing_emb(missing_idxs)这段代码看着简单但它背后的逻辑很关键。标准Transformer里缺失位置填0后模型面对的全是0向量只能靠mask知道“这里不能看”。而在Missformer里模型每个位置拿到的输入是“原始值 缺失标记向量”它可以在训练中学习到“出现缺失标记时这个位置的表示不可全信”这种规则。我做的实验里有个很直观的现象加了缺失标记之后注意力权重会把更多质量分配给真实观测位置缺失位置即使参与计算贡献也明显更低。这说明模型真的学到了“哪些位置可靠”而不是全靠mask硬砍。2.2 缺失模式嵌入连续缺失也是一个特征光有缺失标记还不够因为“缺失”这件事有结构。连续缺5个点和缺1个点恢复难度完全不同。所以Missformer还会把“当前缺失连续长度”编码进去。我这里实现了一个很土但很有效的方案维护一个缺失游程编码统计每个位置所属缺失段的长度然后把这个长度经过Log变换后映射成向量。实践下来比直接输入原始长度稳定很多因为缺失段长度往往长尾分布最大能到几十上百直接编码会把数值差异拉爆。def compute_missing_run_length(mask): # mask: [B, L], 1表示缺失 run_len torch.zeros_like(mask, dtypetorch.float32) for b in range(mask.size(0)): cnt 0 for i in range(mask.size(1)): if mask[b, i] 1: cnt 1 else: cnt 0 run_len[b, i] cnt return run_len.log1p()为什么连续缺失长度重要因为模型在重建某个缺失点时如果知道它前面已经缺了很长一段会更倾向于拉长依赖距离从更早的可靠观测里找信息。如果只是单点缺失靠前后相邻值就基本能恢复。这种“根据缺失结构动态调整关注范围”的能力是标准Transformer不具备的。2.3 时间间隔编码把不规则采样变成token能懂的语言序列数据除了“缺不缺”之外还有一个隐性问题时间间隔不一致。昨天有值隔了3天又有值中间那几天算不算缺失如果算mask区域很长模型很难处理。如果不算位置编码又会把间隔当成均匀的错得离谱。Missformer的做法是把相邻时间戳的差值编码成位置信息。我常用的代码是这样class TimeIntervalEncoding(nn.Module): def __init__(self, d_model, max_len1000): super().__init__() self.d_model d_model inv_freq 1.0 / (10000 ** (torch.arange(0, d_model, 2).float() / d_model)) self.register_buffer(inv_freq, inv_freq) def forward(self, timestamps): # timestamps: [B, L] delta torch.zeros_like(timestamps, dtypetorch.float32) delta[:, 1:] timestamps[:, 1:] - timestamps[:, :-1] delta delta.unsqueeze(-1) # [B, L, 1] angles delta * self.inv_freq # [B, L, d_model//2] pos_emb torch.cat([torch.sin(angles), torch.cos(angles)], dim-1) return pos_emb这个思路本质是把“距离上一个观测点过了多久”作为位置信号。等间隔序列里delta基本是个常数这个编码退化成普通位置编码不等间隔时它能让模型感知到“这个点前面隔了很久”从而在注意力计算中决定是否降低该位置的影响。我一般在真实项目里会把timestamps先归一化比如转成小时或者分钟级别的小数避免秒级和毫秒级混用导致数值振荡。2.4 缺失感知注意力注意力只在可信位置之间流动Missformer的注意力机制和标准Transformer的区别在于它有两层mask控制。第一层是传统的padding mask但它的含义变了——不再只是“忽略填充位”而是“忽略所有已知缺失位置”。这些缺失位置的value向量不可信所以作为key时直接mask掉不让它们参与softmax计算。第二层是针对query的处理。如果query位置本身也是缺失的仍然允许它参与注意力计算因为我们要为它生成重建表示。但此时它只能attend到那些可靠的key上通过有效位置的加权组合得到自己的新表示。用PyTorch内置的TransformerEncoderLayer实现这个逻辑很方便只需要构造一个src_key_padding_mask把缺失位置标记为True即可。def forward(self, x, mask): # mask: [B, L], True表示缺失 src_key_padding_mask mask # 缺失位置不作为key x self.encoder(x, src_key_padding_masksrc_key_padding_mask) return x这里有个容易踩坑的点PyTorch内置实现里src_key_padding_mask值为True的位置会被设置为-inf从而softmax后权重为0。这意味着缺失位置的value对任何query都不会有贡献。但缺失位置自身的query仍然会参与attention计算它的输出由所有有效key加权而成正好被用来做重建预测。如果业务要求更细比如允许某些缺失位置attend到临近的缺失位置也可以自己写attention函数在logits上加一个可学习的偏置矩阵。我后面代码完整版里会给出一个自定义实现方便你按需修改。2.5 两阶段训练策略Missformer的训练一般分成两个阶段第一阶段是缺失重建预训练第二阶段是下游任务微调。重建预训练的思路借鉴了BERT的完形填空。我们从完整序列里随机挖掉一部分观测值让模型去预测这些被挖掉的值。但和BERT不同的地方在于挖的策略要贴近真实缺失模式不能单纯随机。我会按“随机长度连续块 单点”混合的方式挖因为实际场景里很少出现均匀分布的散点缺失。训练损失我通常这样设计loss_recon F.mse_loss(pred[mask], target[mask]) loss_obs F.mse_loss(pred[~mask], target[~mask]) loss loss_recon 0.1 * loss_obsloss_obs这一项是让模型在预测观测位置时也保持准确避免模型学成“只关心缺失位置其他位置随便输出”。系数0.1是我试过比较稳的取值太大会让模型忽略缺失重建太小又会让观测位置的表示失真。第二阶段微调就灵活多了。如果你要预测未来N步在预训练好的模型后面接一个回归头用真实缺失mask继续训练如果要分类就取所有有效位置的表示做pooling再接分类头。关键是微调阶段不要冻结缺失嵌入层让它跟着下游任务继续更新效果比冻结好不少。3. 代码讲解从零实现一个轻量版Missformer3.1 环境与数据准备我用的是PyTorch 2.x环境不需要什么特殊依赖numpy、pandas、PyTorch就够了。为了讲清楚代码逻辑我构造一个模拟数据假设有64个传感器每个采集128个时间点约20%~50%的位置存在缺失缺失以连续块为主。import torch import torch.nn as nn import torch.nn.functional as F import numpy as np def generate_synthetic_data(batch_size8, seq_len128, input_dim1): # 生成完整信号简单正弦 趋势 噪声 t np.linspace(0, 8 * np.pi, seq_len) base np.sin(t) 0.1 * t x np.tile(base, (batch_size, input_dim, 1)).transpose(0, 2, 1) x np.random.randn(batch_size, seq_len, input_dim) * 0.05 # 生成缺失mask随机连续块 少量单点 mask np.zeros((batch_size, seq_len), dtypebool) for b in range(batch_size): n_blocks np.random.randint(2, 6) for _ in range(n_blocks): length np.random.randint(3, 12) start np.random.randint(0, seq_len - length) mask[b, start:start length] True extra np.random.choice(seq_len, size2, replaceFalse) mask[b, extra] True x torch.tensor(x, dtypetorch.float32) mask torch.tensor(mask, dtypetorch.bool) timestamps torch.arange(seq_len, dtypetorch.float32).unsqueeze(0).repeat(batch_size, 1) # 加入不规则时间间隔 for b in range(batch_size): timestamps[b] torch.rand(seq_len) * 0.8 return x, mask, timestamps为了简化我把所有序列设成等长模型也支持变长但数据加载时要按长度排序做padding这篇文章里就不展开了。3.2 模型结构缺失嵌入 时间间隔编码 TransformerEncoder下面这段是模型核心结构。我特意没有用太花哨的模块全部基于PyTorch内置组件方便你迁移到自己的代码里。class MissformerModel(nn.Module): def __init__(self, input_dim1, d_model128, nhead8, num_layers4, dropout0.1): super().__init__() self.d_model d_model self.input_proj nn.Linear(input_dim, d_model) self.missing_emb nn.Embedding(2, d_model) self.run_len_proj nn.Linear(1, d_model) self.time_interval_enc TimeIntervalEncoding(d_model) encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, dim_feedforwardd_model * 4, dropoutdropout, batch_firstTrue, activationgelu ) self.encoder nn.TransformerEncoder(encoder_layer, num_layersnum_layers) self.head nn.Sequential( nn.Linear(d_model, d_model), nn.GELU(), nn.Linear(d_model, input_dim) ) def forward(self, x, mask, timestamps): # x: [B, L, input_dim], mask: [B, L] True表示缺失 src_key_padding_mask mask # 输入填0处理避免把nan喂进模型 x torch.where(mask.unsqueeze(-1), torch.zeros_like(x), x) token self.input_proj(x) # [B, L, d_model] missing_feat self.missing_emb(mask.long()) # [B, L, d_model] run_len compute_missing_run_length(mask) # [B, L] run_feat self.run_len_proj(run_len.unsqueeze(-1)) # [B, L, d_model] time_emb self.time_interval_enc(timestamps) # [B, L, d_model] hidden token missing_feat run_feat time_emb hidden self.encoder(hidden, src_key_padding_masksrc_key_padding_mask) pred self.head(hidden) return pred这里要解释几个容易被忽略的细节。第一输入x在进入模型前要做torch.where填0否则缺失位置的原始值如果是NaN会直接污染整个计算图。第二missing_emb用的是mask.long()作为索引所以缺失位置的嵌入向量是独立可学习的不是从观测位置的值里推断出来的。第三src_key_padding_mask直接等于mask意味着所有缺失位置的key和value都会被忽略。但注意缺失位置作为query时并不受影响——它依然会计算attention并得到输出这正是重建所需的行为。如果你对标准Transformer源码不熟记住这句话src_key_padding_mask影响的是“作为key时是否可见”不影响“作为query时是否计算”。这个特性刚好被我们拿来用。3.3 训练循环自监督重建缺失值模型定义好之后训练逻辑就很直接了。我用AdamW优化器配合一个简单的线性warmup让模型前几步先适应缺失嵌入和时间编码的尺度。def train_step(model, optimizer, x, mask, timestamps): target x.clone() x_input torch.where(mask.unsqueeze(-1), torch.zeros_like(x), x) pred model(x_input, mask, timestamps) loss_recon F.mse_loss(pred[mask], target[mask]) loss_obs F.mse_loss(pred[~mask], target[~mask]) loss loss_recon 0.1 * loss_obs optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() return loss.item() model MissformerModel(input_dim1, d_model128, nhead8, num_layers4) optimizer torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-5) for epoch in range(30): x, mask, timestamps generate_synthetic_data(batch_size8) loss train_step(model, optimizer, x, mask, timestamps) if epoch % 5 0: print(fepoch {epoch:02d}, loss{loss:.4f})我在训练里加了梯度裁剪上限1.0。因为时间间隔编码对异常时间戳非常敏感万一某个delta特别大计算出来的sin/cos编码数值不会爆但梯度过一次反向传播还是可能震荡裁剪一下会稳很多。这个自监督任务只用了30个epoch因为模拟数据比较简单。真实场景里建议按早停策略来留一部分完整数据做验证loss连续5个epoch不降就停止。3.4 推理与重建评估推理阶段我们只关心缺失位置的重建效果。拿到模型输出后把预测值填回原位评估MSE或者MAE就好。def impute(model, x, mask, timestamps): model.eval() with torch.no_grad(): pred model(x, mask, timestamps) imputed x.clone() imputed[mask] pred[mask] return imputed x, mask, timestamps generate_synthetic_data(batch_size1) imputed impute(model, x, mask, timestamps) # 只看缺失位置的重建误差 err (imputed[mask] - x[mask]).abs().mean().item() print(frecon MAE: {err:.4f})这里有一个细节推理时传入的mask必须是“真实缺失位置”不是训练时随机挖的位置。如果你把真实观测位置也标成缺失模型会傻乎乎地重新预测一遍导致有用信息被覆盖掉。4. 实验效果与调参经验这套设计到底赢在哪4.1 一组模拟对比数据我在模拟数据上跑了几个对照版本分别是标准Transformer用0填充加padding mask、标准Transformer用均值填充加padding mask、简单线性插值、不带缺失感知注意力的Missformer、完整版Missformer。评价指标是缺失位置重建MAE越小越好。方法重建MAE说明标准Transformer 0填充1.34缺失位置输出基本是无效向量的产物标准Transformer 均值填充1.08稍好但模型感知不到缺失结构简单线性插值1.21连续缺失段一长就露馅Missformer仅缺失嵌入0.86缺失标记让模型学着区分可靠位置Missformer完整版0.62缺失嵌入 时间间隔编码 掩码注意力这个数据基于我本地构造的模拟集分布不同结果会有差异但趋势是一致的显式编码缺失信息比单纯把缺失“藏起来”更有效。有意思的是标准Transformer 0填充版虽然MAE高但训练loss下降得很快。原因是它把缺失位置当成了普通的0值模型很快学会输出一个接近全局均值的数loss看着很漂亮实际重建一塌糊涂。这提醒我们训练loss不够低是问题但loss够低不代表模型真的理解了数据。4.2 关键参数怎么定Missformer里最值得花时间调的参数一个是缺失嵌入的维度一个是训练时的缺失比例还有一个是注意力层数。缺失嵌入维度我通常直接设为d_model加在token上的时候维度匹配不用额外做投影。如果你想把参数省一点可以设成d_model // 4然后broadcast加到token上效果差不太多。训练时随机掩码的比例建议控制在15%~30%之间。太低模型没机会见各种缺失模式太高有效key太少重建任务变成纯瞎猜loss下降很慢。我一般从20%起步如果真实场景缺失特别严重再逐步提高到35%。层数方面4层TransformerEncoder在大多数情况下够用。缺失重建任务本身不需要特别深的网络层数太深反而容易在长序列上注意力发散。如果你是做长序列预测可以考虑把nhead从8调到16但要同步调大dropout防止过拟合。4.3 真实场景适配要点模拟数据和真实数据最大的差别在于真实数据往往同时存在“时间维缺失”和“特征维缺失”。如果序列的某些特征列整体缺失比如传感器A一直没数据那么缺失标记就不能只按时间点算要扩展成[B, L, D]的三维mask每个特征通道单独标记。这时src_key_padding_mask就不够用了得换成更细粒度的attention mask或者干脆把缺失特征先做零填充把缺失标记拼到token向量里让模型自己学。另一个真实场景的坑是时间戳的单位。有的系统用毫秒有的用秒还有的是业务日期。我建议把所有时间戳统一转成小时或者天级别的小数再进入TimeIntervalEncoding否则delta跨度过大sinusoidal编码会产生大量高频噪声注意力会被时间间隔带偏。我还踩过一个大坑重构训练的时候如果不加任何因果关系约束模型会利用未来信息填补过去缺失位置。这在“离线补全”任务里没问题但如果你的目标是流式预测也就是只允许用历史数据预测当前值那必须在注意力里加因果mask否则离线评估指标很好看上线就崩。5. 常见问题与排查技巧实录5.1 训练loss不降或者降得太慢先检查缺失比例是不是太高。我刚开始调参时把训练mask比例设成50%结果loss长时间在1.0附近横跳。原因是有效key太少每个缺失位置只能看到两三个可靠位置模型根本学不出有效表示。把比例降到20%后loss很快就下降了。再检查时间间隔编码。如果timestamps里混进异常值比如某个delta是本来该是1.0结果因为系统bug写成了10000sinusoidal编码会产生完全无规律的向量模型前期会花大量epoch去适应这个噪声。解决办法是归一化裁剪把delta限制在合理范围比如超过均值3倍标准差时直接截断。5.2 重建结果全都接近均值这个问题通常有两个原因。第一缺失嵌入没有起到作用模型把所有缺失位置当成同一个状态学出来的表示和原始值无关。排查方法是把missing_emb的向量打出来如果不同缺失段之间的向量差距很小说明缺失标记没有和信息融合起来可以试着把缺失嵌入从“加”改成“拼接”。第二注意力分配过于均匀。标准Transformer的softmax在序列很长时注意力权重容易摊平导致每个缺失位置都从全局平均里取信息。我常用的解法是引入局部窗口偏置让注意力更关注相邻位置可以是固定窗口也可以是可学习的距离偏置。5.3 连续缺失区间太长模型无能为力这是最现实的问题。如果某个传感器连续断了100个时间点而序列总共只有128个时间点那即便是人也很难只靠前后的4个有效点恢复出中间的复杂波形。Missformer能做的是把有效区域的信息尽量利用起来但不可能无中生有。碰到这种情况我的建议是不要把重构精度当成唯一目标。你可以加一个辅助类别让模型同时判断“这个缺失段是否可恢复”或者把长缺失段单独标记出来走外插值或者查找匹配历史模式的路子。把不可恢复的部分硬塞给模型只会让它在其他部分的预测也被带偏。5.4 训练和推理mask不一致训练时我们随机挖缺失位置模型见惯了各种缺失分布。推理时用真实mask按理说没什么问题但有一种情况会出岔子真实数据里存在大量“整块缺失”的样本训练时却很少挖出这种极端模式。解决办法是在数据增强阶段加入“全序列随机丢弃一个区段”的采样逻辑模拟整块设备掉线的场景。我一般会保证训练batch里有10%~20%的序列带着一个特别长的连续缺失段这样模型到了推理时就不会对长缺失段过度恐慌。6. 最后分享几个容易踩坑的细节6.1 可视化注意力权重别只盯着loss调Missformer的时候我一直建议把注意力权重可视化出来不要只盯着loss曲线。做法很简单把TransformerEncoderLayer里某个head的attention map输出用热力图画出来颜色越深代表注意力权重越大。我见过很多次这样的现象loss已经很低了但注意力图上一片均匀分布只有偶尔几个点特别亮。这说明模型根本没学到有效的依赖关系纯粹在靠“平均”硬撑。正常情况应该是缺失位置附近的有效观测点会特别亮离得远但高度相关的历史相似片段也会发亮。如果你看到缺失位置的颜色特别深也就是所有其他位置都在重点看这个缺失点那大概率是缺失嵌入没起作用模型还把缺失位置当成了有效信息源赶紧回去查src_key_padding_mask。6.2 模型保存复用时记得把mask逻辑一起打包训练好模型之后很多人只保存state_dict结果推理脚本里忘了调用compute_missing_run_length或者用了不同方式生成mask模型输入分布变了效果断崖式下跌。我的做法是写一个完整的predict函数把填0、缺失标记、游程长度、时间间隔编码全部封装进去而不是只暴露模型类。这样无论是本地调试还是部署到服务端都不会出现“训练正常、推理维度不匹配”的问题。另外如果你用PyTorch的torch.compile加速训练保存模型后用普通模式加载偶尔会遇到op版本兼容问题。稳妥起见保存和加载两端保持相同的PyTorch版本尽量别跨小版本加载。最后再补充一个我犯过的低级错误评估的时候忘了把模型切回eval()模式dropout还在生效每次推理结果都不一样排查了半天才发现问题。所以推理函数里我一定会在forward之前加model.eval()确保结果可复现。Missformer这套思路说到底不是什么高深莫测的黑魔法它就是把“数据哪里缺了、缺成什么样”这件事做了显式建模。按照上面的代码和调参路径在大多数带缺失的序列场景里都能落地而且收益明显。我自己从标准Transformer切到Missformer之后最直观的感受是模型终于“知道”哪里有坑了而不是傻乎乎地把坑填上就当没事发生。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑