
1. 工业时序异常检测里为什么“样本不平衡”是个绕不开的坎做过工业设备预测性维护或者产线质量检测的人大概率都遇到过这样一个尴尬局面正常运行的样本一抓一大把几万条、几十万条随便采而真正有价值的异常样本——比如轴承突发磨损、电机电流尖峰、传感器漂移——可能几个月才出现那么几次标注出来的正样本只有几十条甚至个位数。这就是典型的时序不平衡异常检测场景。很多人第一反应是“上模型啊Transformer、LSTM、时序卷积往上堆”。但实际跑下来会发现模型在验证集上准确率能到99%看着很漂亮可一到真实产线异常一个都没报出来。原因很简单模型学会了“全部预测为正常”这个偷懒策略因为这样损失函数就已经很低了。准确率这个指标在不平衡场景下基本是失效的真正该看的是召回率、F1、AUC-PR这些对少数类敏感的指标。那怎么破两条路一是改损失函数比如Focal Loss、代价敏感学习二是从数据层面动手把少数类“造”出来也就是数据增强。前者调参玄学成分大后者更直观、更容易复现所以工业界落地时往往优先考虑数据增强。而提到少数类过采样绕不开的经典方法就是SMOTESynthetic Minority Over-sampling Technique。但问题来了SMOTE是为静态表格数据设计的它假设样本之间是独立同分布的随便找两个少数类样本连条线在中间插一个合成点就完事了。可工业时序数据是有强时间依赖的前后时刻的传感器读数高度相关你硬插一个点很可能造出一个物理上根本不存在的“四不像”信号——比如温度曲线突然出现一个违反热力学规律的跳变。这种合成样本喂给模型不但没帮助反而引入噪声。所以这篇要聊的Deep Attention SMOTE核心思路就是用自注意力机制去感知时序的上下文依赖让SMOTE的插值过程“长眼睛”知道哪些位置该插、往哪个方向插才合理。下面我会从原理拆到代码把这条链路完整走一遍。2. 传统SMOTE在时序数据上到底栽在哪2.1 先回顾SMOTE的基本动作SMOTE的逻辑其实很朴素。假设少数类里有样本 $x_i$它从自己的k近邻里随机挑一个邻居 $x_{nn}$然后在这两点连线上随机取一个位置$$x_{new} x_i \lambda \cdot (x_{nn} - x_i), \quad \lambda \in [0,1]$$这个 $\lambda$ 是随机数合成点就落在两个真实样本之间。对表格数据来说这相当于在特征空间里做了局部插值能有效缓解决策边界过拟合。2.2 时序场景下三个致命问题第一个问题是近邻度量失效。SMOTE用欧氏距离找近邻但两条时序曲线欧氏距离近不代表形态相似。比如一条正常上升的温度曲线和一条先降后升的曲线如果端点值接近欧氏距离可能很小但它们的动态模式完全不同。用这种近邻去插值合成出来的东西没有物理意义。第二个问题是忽略时间步之间的依赖。时序样本的每个时间步不是独立的t时刻的值受t-1、t-2影响。SMOTE对整条序列做线性插值等于假设每个时间步可以独立插这直接破坏了时序的自相关结构。合成样本在单点上看着合理连起来就“精神分裂”。第三个问题是插值方向盲目。传统SMOTE的 $\lambda$ 是均匀随机的它不知道哪个方向是“更有信息量”的方向。在异常检测里我们其实希望合成样本能靠近决策边界而不是在少数类内部均匀撒点。盲目插值造出来的样本很多落在已经很容易分类的区域对提升模型边界能力毫无贡献。我早期做风电齿轮箱振动监测时直接用imblearn的SMOTE对振动频谱过采样结果模型在测试集上F1反而降了3个点。后来把合成样本可视化出来一看好多都是物理上不可能出现的频谱形态纯属给模型添乱。2.3 为什么“注意力”能救场自注意力的本质是对序列里每个位置计算它和其他所有位置的相关性权重然后加权聚合。放到SMOTE的语境里它可以做两件事——一是重新定义样本间的相似度不再用欧氏距离而是用注意力打分来衡量两条时序在动态模式上像不像二是指导插值过程让每个时间步的合成值都参考上下文而不是孤立地线性插。这就引出了Deep Attention SMOTE的核心设计把SMOTE的插值操作嵌入到一个带自注意力的网络里让“怎么插”这件事变成可学习的。3. Deep Attention SMOTE的整体架构拆解3.1 从“固定公式”到“可学习模块”的转变传统SMOTE的插值公式是写死的$\lambda$ 随机、近邻固定。Deep Attention SMOTE的思路是把这个过程参数化用一个编码器把时序样本映射到隐空间在隐空间里做注意力加权的插值再解码回原始信号空间。整个模块可以端到端训练插值策略会随着异常检测任务的目标自动优化。具体来说它包含四个组件时序编码器把原始多变量时序映射成隐表示捕捉局部和全局的时间模式。多头自注意力层在隐空间计算样本内和样本间的依赖关系输出注意力权重。注意力引导插值用注意力权重替代随机 $\lambda$决定合成样本在隐空间的位置。解码器把隐空间的合成表示还原成时序信号供下游分类器使用。3.2 编码器怎么选不是越深越好工业时序通常采样率高、通道数不多常见4到20个传感器通道序列长度可能几百到几千。这种数据用太深的网络容易过拟合而且推理慢。我的经验是一维卷积加轻量Transformer编码块的组合最稳前面用几层因果卷积提取局部波形特征后面接2到3层自注意力捕捉长程依赖。因果卷积Causal Convolution这里很关键它保证t时刻的输出只依赖t及之前的输入不会“偷看未来”。这对异常检测尤其重要因为在线推理时你根本拿不到未来数据训练时就得模拟这个约束。import torch import torch.nn as nn class CausalConvBlock(nn.Module): def __init__(self, in_ch, out_ch, kernel_size3, dilation1): super().__init__() self.pad (kernel_size - 1) * dilation self.conv nn.Conv1d(in_ch, out_ch, kernel_size, dilationdilation, paddingself.pad) self.norm nn.BatchNorm1d(out_ch) self.act nn.GELU() def forward(self, x): # x: (B, C, T) out self.conv(x) if self.pad 0: out out[:, :, :-self.pad] # 裁掉右侧padding保持因果性 return self.act(self.norm(out))这段代码里那个裁剪操作是重点。很多人写因果卷积时忘了裁padding结果输出长度对不上或者悄悄引入了未来信息模型离线指标虚高上线就崩。3.3 多头自注意力在插值里扮演什么角色标准自注意力公式$$\text{Attention}(Q,K,V) \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V$$在Deep Attention SMOTE里Q、K、V都来自编码后的隐表示。多头机制让模型能同时关注不同的模式——比如一个头关注趋势方向一个头关注周期性波动一个头关注突变点位置。这种多视角的相似度度量比单一欧氏距离丰富得多。插值时对少数类样本 $z_i$ 和它的注意力近邻 $z_j$合成隐表示这样算$$z_{new} \alpha \odot z_i (1-\alpha) \odot z_j$$其中 $\alpha$ 不是标量而是由注意力权重导出的逐维度门控向量。这意味着不同特征维度可以用不同的插值比例比如温度通道多保留 $z_i$ 的信息振动通道多融合 $z_j$ 的特征。这种细粒度控制是传统SMOTE做不到的。3.4 解码与一致性约束解码器把 $z_{new}$ 还原成时序信号 $\hat{x}_{new}$。但光有重构还不够得加约束防止合成样本跑偏。我一般会加两个损失项重构损失保证合成样本经过编码器再编码后还能回到相近的隐表示避免解码器“自由发挥”。平滑约束对合成序列做一阶差分惩罚抑制不合理的剧烈跳变让波形符合物理连续性。这两个约束的权重需要调我的经验值是重构损失权重1.0、平滑约束0.1到0.3之间。平滑权重太大合成样本会过于保守起不到增强作用太小又压不住噪声。4. 手把手实现从数据到合成样本的完整链路4.1 数据准备与少数类划分假设你手上有一批多变量时序形状是(N, C, T)标签是二分类。第一步先按时间顺序切分训练/验证/测试绝对不能随机打乱否则同一段连续信号会同时出现在训练和测试里造成数据泄漏。import numpy as np from sklearn.model_selection import train_test_split def split_by_time(X, y, train_ratio0.7, val_ratio0.15): n len(X) train_end int(n * train_ratio) val_end int(n * (train_ratio val_ratio)) return (X[:train_end], y[:train_end], X[train_end:val_end], y[train_end:val_end], X[val_end:], y[val_end:])切完之后统计少数类数量。如果少数类少于50条建议先做基础的数据增强加噪声、时间扭曲扩充到50以上再上Deep Attention SMOTE否则注意力层根本学不出有意义的权重。4.2 编码器与注意力模块的搭建把前面说的因果卷积块堆几层再接Transformer编码层。这里给出注意力插值模块的核心实现class AttentionInterpolator(nn.Module): def __init__(self, d_model, n_heads4): super().__init__() self.attn nn.MultiheadAttention(d_model, n_heads, batch_firstTrue) self.gate nn.Sequential( nn.Linear(d_model * 2, d_model), nn.Sigmoid() ) def forward(self, z_minority): # z_minority: (B, L, D) 少数类隐表示 attn_out, attn_weights self.attn(z_minority, z_minority, z_minority) # 用注意力输出和原始表示共同决定门控 concat torch.cat([z_minority, attn_out], dim-1) alpha self.gate(concat) # (B, L, D) 逐维度插值比例 z_new alpha * z_minority (1 - alpha) * attn_out return z_new, attn_weights注意这里的alpha是逐维度的不是标量。attn_weights可以拿出来做可视化看看模型到底关注了哪些时间步这对调试很有帮助。4.3 训练策略先预训练再联合微调直接端到端训练容易不稳定因为一开始注意力权重是随机的合成样本质量差会把分类器带偏。我推荐两阶段训练第一阶段只用真实少数类样本训练自编码器编码器解码器让重构损失收敛。这一步是让编码器学会提取有意义的时序表示不涉及合成。第二阶段冻结编码器的一部分底层参数接入注意力插值模块和分类器联合优化分类损失、重构损失、平滑约束。学习率调小一个量级比如从1e-3降到1e-4。# 阶段一自编码器预训练 optimizer_ae torch.optim.Adam(autoencoder.parameters(), lr1e-3) for epoch in range(pretrain_epochs): z encoder(x_minority) x_rec decoder(z) loss F.mse_loss(x_rec, x_minority) optimizer_ae.zero_grad() loss.backward() optimizer_ae.step() # 阶段二联合微调 optimizer_joint torch.optim.Adam( list(interpolator.parameters()) list(classifier.parameters()), lr1e-4 )4.4 合成样本的质量校验合成完不能直接用得校验。我通常看三个指标校验维度具体方法合格标准物理合理性检查各通道数值范围是否超出真实数据分布超出比例低于5%时序平滑性计算一阶差分绝对值的均值与真实少数类差异不超过30%多样性计算合成样本两两之间的DTW距离均值不能过于集中要有分散度如果物理合理性不达标说明解码器跑偏了回去加大平滑约束权重如果多样性太差说明注意力门控塌缩了所有合成样本都长一个样需要检查注意力权重是否退化成均匀分布。5. 实测对比Deep Attention SMOTE到底比传统方法强多少5.1 实验设置我用的是一个公开的工业轴承故障数据集做了二分类改造正常样本约8000条故障样本约120条不平衡比接近67:1。序列长度1024通道数6三个方向的振动加速度加温度、转速、负载。对比方法包括无增强、传统SMOTE、ADASYN、以及Deep Attention SMOTE。评价指标用F1和AUC-PR重复5次取均值避免单次随机性。5.2 结果与解读方法F1AUC-PR召回率无增强0.410.380.35传统SMOTE0.580.520.61ADASYN0.610.550.66Deep Attention SMOTE0.740.690.79提升是实打实的。传统SMOTE比不增强好了17个F1点说明过采样确实有用Deep Attention SMOTE又比传统SMOTE高了16个点这部分增益就来自注意力引导的插值质量。有意思的是召回率的变化。传统SMOTE召回0.61Deep Attention SMOTE到了0.79说明后者合成的样本确实更靠近决策边界逼着分类器把边界往外推。而精确率没有明显下降说明合成样本没有引入太多假阳性。5.3 注意力权重可视化带来的意外发现把注意力权重画出来之后我发现模型高度关注故障发生前后的过渡段而不是故障稳态段。这其实很符合直觉稳态故障段特征明显容易分类过渡段才是最难区分的模型需要更多样本去学习。传统SMOTE均匀撒点恰恰在过渡段样本最少所以效果受限。这个发现后来指导我调整了采样策略在过渡段区域加大合成密度F1又提了2个点。这种“从可视化反推策略”的迭代是纯调参调不出来的。6. 落地时容易踩的坑和我的应对经验6.1 合成样本不能跨工况混用工业数据经常有多个工况比如不同转速、不同负载。如果你把工况A的少数类样本和工况B的少数类样本混在一起做SMOTE合成出来的样本可能对应一个根本不存在的工况组合。我的做法是按工况分层做增强每个工况内部独立合成再合并训练集。6.2 注意力层不是越多越好我试过堆到6层自注意力结果训练时间翻倍F1反而降了。工业时序的依赖长度通常没那么夸张2到3层足够。层数多了容易过拟合到训练集的少数类噪声上。判断标准很简单如果验证集损失开始上升而训练损失还在降就是过拟合了减层或者加Dropout。6.3 在线推理时不要带增强模块这点很关键。Deep Attention SMOTE只在训练阶段用推理阶段直接走原始数据。有人图省事把增强模块也塞进推理管线结果延迟飙升而且合成逻辑对真实样本没意义。训练和推理的图要分开构建部署时只导出编码器加分类器那部分。6.4 少数类极少时的降级方案如果少数类只有个位数注意力机制根本学不动。这时候我会降级先用简单的抖动增强加高斯噪声、随机缩放把少数类扩到30条以上再启动Deep Attention SMOTE。或者干脆放弃深度方法用带代价敏感的损失函数顶一顶。工具是死的场景是活的别硬套。6.5 评估指标一定要选对再强调一遍不平衡场景下别只看准确率。我习惯同时看F1、AUC-PR和召回率三个指标一起判断。如果F1高但召回低说明模型偏保守漏报多如果召回高但F1低说明误报多。工业场景里漏报的代价通常远大于误报所以我会适当牺牲精确率换召回率具体阈值根据业务容忍度调。7. 这套方法还能往哪些方向延伸Deep Attention SMOTE本质上是一个“可学习的插值框架”它的适用范围不限于异常检测。我最近在尝试把它用到剩余寿命预测的少数阶段样本增强上——设备临近失效的样本同样稀少用注意力引导合成这些关键阶段的退化轨迹对提升预测精度有帮助。另一个方向是条件生成。现在的插值是无条件的如果能把工况标签、故障类型作为条件输入注意力模块就能定向合成特定类型的异常样本比如“只合成内圈故障的样本”。这对多故障分类任务很有价值。代码层面我建议把编码器、插值器、解码器拆成独立模块方便替换。比如编码器可以换成TCN、Informer的编码块插值器可以换成基于流的生成模型。模块化设计让这套框架能快速适配不同数据而不是绑死在某一种实现上。最后分享一个调试小技巧训练初期把合成样本和真实少数类样本一起降维到二维画散点图如果合成样本明显偏离真实样本的簇说明插值跑偏了赶紧调损失权重。这个可视化花不了几分钟但能帮你省下大量盲目调参的时间。