☰
特征级SMOTE+多头自注意力+对比损失:解决PHM故障样本稀缺
2026/10/27 6:41:46 网站建设 项目流程

1. 从一次轴承故障漏报到特征级SMOTE的引入

去年帮一个做风电齿轮箱状态监测的团队做技术复盘,他们的振动数据集里正常样本占了九成以上,早期微弱故障的样本只有个位数。模型在测试集上准确率刷到98%,上线之后连续三周把已经出现点蚀的轴承判成健康状态,直到巡检人员听到异响才发现。这个场景在重大装备健康管理(PHM)里太典型了——不是模型不够深,是数据本身长歪了。

PHM的核心任务无非是监测、诊断、预测三段:从传感器信号里判断设备当前状态,定位故障类型和程度,再推算剩余使用寿命。但重大装备有个天然属性:它不能随便坏。你不可能为了让模型学会“故障长什么样”就故意把一台价值千万的主轴跑废。所以故障样本天然稀缺,正常样本海量堆积,类别极度不平衡。传统做法是在数据层面做重采样,比如SMOTE在原始特征空间里插值生成少数类样本。但我在实际项目里发现,直接在原始振动信号或者手工特征上做SMOTE,生成出来的“假故障”往往物理上根本说不通——两个不同故障阶段的样本做线性插值,得到的中间态在频域上可能对应一种现实中不存在的振动模式。

这就是Feature-level SMOTE要解决的问题:不在原始空间插值,而是在模型学到的特征表示空间里做少数类增强。配合多头自注意力(Multi-Head Self-Attention)和对比损失(Contrastive Loss),让特征空间本身具备更好的类间可分性和类内紧凑性,再在这个空间里做SMOTE,生成的样本才真正有意义。这套组合我后来在几个PHM项目里反复验证过,下面把设计思路、实现细节和踩过的坑完整拆一遍。

2. 整体方案设计与核心思路拆解

2.1 为什么原始空间SMOTE在PHM里容易翻车

SMOTE的基本逻辑是:对每个少数类样本,找它的k个最近邻少数类样本,在两者连线上随机取一个插值点作为新样本。这个操作在二维平面上看起来很直观,但PHM的数据维度往往很高——一路振动信号经过时域、频域、时频域提取,轻松上百维。高维空间里有个要命的问题:距离度量本身会失效。我实测过,在128维的手工特征空间里,一个正常样本和一个早期故障样本的欧氏距离,可能跟两个不同故障程度样本之间的距离差不多。这种情况下找出来的“最近邻”根本不可靠,插值出来的样本自然也是噪声。

更关键的是物理约束。重大装备的故障演化是有连续物理过程的,比如轴承从正常到早期磨损到严重剥落,振动信号的包络谱会沿着特定频率成分逐步抬升。你在原始空间做线性插值,相当于假设两个故障状态之间的过渡是线性的,但实际物理过程往往是非线性的,甚至存在突变。我见过最离谱的情况是,SMOTE在原始空间生成了一批“故障样本”,结果这些样本在包络谱上同时具备正常和严重故障的特征,模型学完之后反而把正常样本误判成故障。

2.2 特征级增强的核心逻辑

Feature-level SMOTE的思路是把增强操作推迟到模型中间层。具体来说,用一个编码器网络把原始输入映射到一个低维特征空间,这个空间由多头自注意力和对比损失共同塑造,使得同类样本聚集、异类样本分离。然后在这个特征空间里做SMOTE插值,生成的特征向量再送入后续的分类或回归头。

这样做有几个好处。第一,特征空间维度通常远低于原始输入,距离度量更可靠。第二,对比损失会显式地拉大类间距离、压缩类内方差,让少数类样本在特征空间里占据更清晰的区域,插值出来的点更可能落在合理的故障流形上。第三,多头自注意力能捕捉不同传感器通道之间的长程依赖关系,这对多通道振动、温度、电流信号融合的PHM场景特别重要。

我一般把特征维度控制在32到64之间。太低会丢失判别信息,太高则SMOTE的邻居搜索又会退化。这个范围是我在多个数据集上试出来的经验值,具体项目可以根据样本量和故障类型数微调。

2.3 多头自注意力在特征编码中的角色

多头自注意力的原理说白了就是让每个位置的特征去“询问”其他所有位置的特征,决定该从谁那里吸收多少信息。在PHM场景里,如果输入是多通道时序信号,每个时间步的每个通道都可以看作一个token。自注意力能自动学到:当某个通道出现冲击成分时,其他通道的哪些时间步应该被重点关注。

我通常用4到8个头。头数太少,注意力模式单一,捕捉不到多种故障特征;头数太多,参数量上去之后小样本容易过拟合。每个头的维度一般是特征总维度除以头数,比如64维特征配8个头,每个头8维。这个配置在轴承和齿轮箱数据上都比较稳。

自注意力层的堆叠层数我一般控制在2到3层。PHM的信号长度通常不会特别夸张,单次输入可能就几百到几千个时间步,两层自注意力已经能覆盖足够的时间感受野。层数再多,梯度传播和训练稳定性都会变差,尤其是故障样本少的时候。

2.4 对比损失如何塑造特征空间

对比损失的核心思想是:让同一类样本的特征表示尽可能靠近,不同类样本的特征表示尽可能远离。具体实现上,我常用的是监督对比损失(Supervised Contrastive Loss),因为它能利用标签信息,把同类样本无论来自哪个批次都拉在一起。

公式上,对于一个锚点样本,它的损失由正样本对和负样本对共同决定。正样本对是同一类的其他样本,负样本对是不同类的样本。温度系数我一般设在0.07到0.1之间,这个值控制对困难负样本的关注程度。太小会让模型只关注特别难的负样本,训练不稳定;太大则所有负样本权重趋同,判别力上不去。

对比损失和SMOTE的配合有个关键点:对比损失要在SMOTE之前生效。也就是说,编码器先通过对比学习把特征空间整理好,然后再在这个空间里做插值。如果顺序反了,先SMOTE再对比学习,生成样本的噪声会被对比损失放大,效果反而更差。这个顺序我在早期实验里搞反过,模型收敛得很差,后来调整过来才正常。

3. 核心细节解析与实操要点

3.1 数据预处理与样本划分的坑

PHM数据预处理有几个容易忽略的细节。第一,振动信号要做分帧处理,帧长和帧移的选择直接影响样本数量。我一般用1024点帧长、512点帧移,在采样率12kHz的情况下,每帧覆盖约85毫秒,这个时间尺度对轴承和齿轮的早期故障比较敏感。第二,归一化要按通道分别做,不能全局归一化,否则不同量纲的传感器信号会互相干扰。第三,训练集、验证集、测试集的划分要按时间顺序切,不能随机打乱。重大装备的故障是演化的,随机划分会导致未来信息泄露到训练集,验证指标虚高。

样本划分还有个特殊处理:少数类样本在训练集里要保证每个故障阶段都有代表。我遇到过一种情况,某类故障只有5个样本,随机划分后训练集里只剩2个,SMOTE根本没法找邻居。这时候要么调整划分策略做分层抽样,要么用数据增强先扩充到至少10个样本再划分。

3.2 编码器网络结构的具体配置

编码器我一般用一维卷积加自注意力的混合结构。前面两到三层一维卷积负责提取局部波形特征,卷积核大小分别设7、5、3,通道数逐层翻倍。卷积之后接自注意力层,让模型捕捉全局依赖。最后用一个全局平均池化把时序维度压掉,得到固定长度的特征向量。

自注意力层的实现要注意位置编码。PHM信号的时间顺序很重要,不加位置编码的话,自注意力对时间步的排列不敏感,会丢失故障演化的方向性信息。我用的是可学习的位置编码,初始化成小的随机值,让模型自己学每个时间步的位置重要性。

对比损失和分类损失是联合训练的。总损失等于分类交叉熵加上一个权重系数乘以对比损失。这个权重系数我一般设在0.1到0.5之间。太大,对比损失主导训练,分类精度会掉;太小,特征空间整理不充分,SMOTE效果打折扣。我通常从0.3开始试,根据验证集上的类间距离和分类指标微调。

3.3 特征空间SMOTE的插值策略

在特征空间做SMOTE,邻居搜索用余弦距离比欧氏距离更稳。因为对比损失塑造的特征空间里,方向信息比绝对距离更重要。余弦距离对特征向量的模长不敏感,能更好地反映类间关系。

插值的时候,我一般取k=5个最近邻,随机选一个邻居做线性插值。插值系数从Beta分布采样,而不是均匀分布。Beta(2,2)是我常用的配置,它让插值点更倾向于落在两个样本中间区域,避免生成过于靠近某一端的样本。均匀分布的话,插值点可能紧贴某个原始样本,多样性不够。

生成样本的数量要控制。我一般把少数类扩充到跟多数类数量相当,但不完全1:1。完全平衡有时候会引入太多合成样本,反而让模型对真实少数类的判别边界模糊。实践中,少数类扩充到多数类的60%到80%之间,分类指标通常最好。这个比例不是固定的,样本越少、故障类型越多,扩充比例可以适当提高。

3.4 训练过程中的稳定性技巧

联合训练对比损失和分类损失,初期容易不稳定。我的做法是先用分类损失单独训练几轮,让编码器有个基本的特征提取能力,然后再加入对比损失联合微调。这个预热阶段一般5到10个epoch,学习率用正常值的十分之一。

批次构造也有讲究。对比损失需要批次内有足够多的正样本对,所以每个批次要保证每个类别至少出现2到3次。如果某个故障类样本太少,可以用类别感知的采样器,让少数类在每个批次里被过采样。这个过采样是在批次级别做的,跟特征空间的SMOTE不冲突,两者可以叠加使用。

梯度裁剪是必须的。对比损失在温度系数小的时候,梯度容易爆炸。我一般把梯度范数裁剪到1.0,配合AdamW优化器,权重衰减设0.01。学习率用余弦退火,初始值1e-3,最低降到1e-5。

4. 实操过程与核心环节实现

4.1 环境准备与依赖配置

我用的技术栈是PyTorch加NumPy和SciPy。PyTorch版本建议1.12以上,因为要用到一些较新的注意力实现。如果数据量特别大,可以用混合精度训练,显存占用能降三成左右。

import torch import torch.nn as nn import torch.nn.functional as F import numpy as np from scipy.spatial.distance import cosine from sklearn.neighbors import NearestNeighbors

数据加载部分,我写了一个自定义Dataset,每个样本返回振动信号片段和对应的故障标签。信号片段已经做过分帧和归一化,标签是整数编码的故障类型。

4.2 编码器网络的代码实现

编码器由卷积块和自注意力块组成。卷积块用一维卷积加批归一化和ReLU。自注意力块用PyTorch的MultiheadAttention,配合可学习位置编码。

class PHMEncoder(nn.Module): def __init__(self, in_channels=1, feat_dim=64, num_heads=8, num_layers=2): super().__init__() self.conv1 = nn.Conv1d(in_channels, 32, kernel_size=7, padding=3) self.conv2 = nn.Conv1d(32, 64, kernel_size=5, padding=2) self.conv3 = nn.Conv1d(64, feat_dim, kernel_size=3, padding=1) self.bn1 = nn.BatchNorm1d(32) self.bn2 = nn.BatchNorm1d(64) self.bn3 = nn.BatchNorm1d(feat_dim) self.pos_embed = nn.Parameter(torch.randn(1, 1000, feat_dim) * 0.02) encoder_layer = nn.TransformerEncoderLayer( d_model=feat_dim, nhead=num_heads, dim_feedforward=feat_dim*4, dropout=0.1, batch_first=True ) self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) self.pool = nn.AdaptiveAvgPool1d(1) def forward(self, x): x = F.relu(self.bn1(self.conv1(x))) x = F.relu(self.bn2(self.conv2(x))) x = F.relu(self.bn3(self.conv3(x))) x = x.permute(0, 2, 1) x = x + self.pos_embed[:, :x.size(1), :] x = self.transformer(x) x = x.permute(0, 2, 1) x = self.pool(x).squeeze(-1) return x

这个编码器输入是单通道振动信号,输出64维特征向量。位置编码的最大长度设了1000,实际用的时候根据信号长度截取。自注意力层用了2层,每层8个头,前馈网络维度是特征维度的4倍。

4.3 对比损失与分类损失的联合训练

监督对比损失的实现要注意正负样本对的构造。同一个批次里,同类样本互为正样本,异类样本互为负样本。

class SupConLoss(nn.Module): def __init__(self, temperature=0.07): super().__init__() self.temperature = temperature def forward(self, features, labels): features = F.normalize(features, dim=1) sim_matrix = torch.matmul(features, features.T) / self.temperature mask = torch.eq(labels.unsqueeze(0), labels.unsqueeze(1)).float() mask.fill_diagonal_(0) exp_sim = torch.exp(sim_matrix) pos_sum = (exp_sim * mask).sum(dim=1) all_sum = exp_sim.sum(dim=1) - exp_sim.diag() loss = -torch.log(pos_sum / all_sum + 1e-8) return loss.mean()

训练循环里,总损失是分类交叉熵加上0.3倍的对比损失。预热阶段只算分类损失,10个epoch后加入对比损失。优化器用AdamW,学习率1e-3,余弦退火到1e-5。

4.4 特征空间SMOTE的完整流程

训练完编码器之后,把所有训练样本过一遍编码器,得到特征向量和标签。然后对每个少数类,在特征空间里做SMOTE。

def feature_smote(encoder, dataloader, target_ratio=0.7, k=5): encoder.eval() all_feats, all_labels = [], [] with torch.no_grad(): for x, y in dataloader: feats = encoder(x) all_feats.append(feats.cpu().numpy()) all_labels.append(y.numpy()) feats = np.concatenate(all_feats) labels = np.concatenate(all_labels) classes, counts = np.unique(labels, return_counts=True) max_count = counts.max() synthetic_feats, synthetic_labels = [], [] for cls, cnt in zip(classes, counts): if cnt >= max_count * target_ratio: continue cls_feats = feats[labels == cls] n_gen = int(max_count * target_ratio) - cnt nbrs = NearestNeighbors(n_neighbors=min(k, cnt-1), metric='cosine').fit(cls_feats) for _ in range(n_gen): idx = np.random.randint(cnt) neighbors = nbrs.kneighbors(cls_feats[idx:idx+1], return_distance=False)[0] neighbor_idx = np.random.choice(neighbors[1:]) lam = np.random.beta(2, 2) new_feat = cls_feats[idx] + lam * (cls_feats[neighbor_idx] - cls_feats[idx]) synthetic_feats.append(new_feat) synthetic_labels.append(cls) if synthetic_feats: feats = np.concatenate([feats, np.array(synthetic_feats)]) labels = np.concatenate([labels, np.array(synthetic_labels)]) return feats, labels

生成完特征之后,用这些特征训练一个分类头。分类头可以很简单,两层全连接加Softmax就够了。因为特征空间已经整理得很好,分类头不需要太复杂。

4.5 完整训练与评估流程

整个流程分三步。第一步,用原始不平衡数据训练编码器和分类头,联合对比损失和分类损失。第二步,固定编码器,提取所有训练样本的特征,做特征空间SMOTE。第三步,用增强后的特征训练一个新的分类头,编码器可以冻结也可以微调。

评估的时候,测试集保持原始分布,不做任何增强。指标除了准确率,我重点关注少数类的召回率和F1分数。PHM场景里,漏报故障的代价远高于误报,所以召回率是首要指标。我一般要求少数类召回率至少到0.85以上,F1不低于0.8。

5. 常见问题与排查技巧实录

5.1 训练不收敛或损失震荡

对比损失加入后训练不稳定,最常见的原因是温度系数太小或者批次内正样本太少。温度系数低于0.05时,梯度会变得非常尖锐,损失容易震荡。我一般从0.1开始试,逐步降到0.07。批次内每个类别至少要有3个样本,否则正样本对太少,对比损失估计有偏。

另一个原因是学习率太大。联合训练时,对比损失的梯度尺度跟分类损失不一样,统一学习率可能对其中一个不合适。我的做法是给编码器和分类头分别设学习率,编码器用1e-4,分类头用1e-3。这样编码器更新更稳,分类头学得更快。

5.2 生成样本质量差导致指标下降

特征空间SMOTE之后指标反而降了,通常是生成样本的标签噪声或者插值系数分布不对。检查两个地方:一是邻居搜索的k值,k太大可能找到不同故障阶段的样本,插值出来的特征物理上不合理。我一般把k控制在3到5之间,故障阶段越多,k越小。二是插值系数分布,Beta(2,2)比均匀分布更稳,但如果故障类型之间边界模糊,可以用Beta(1,1)即均匀分布,让生成样本更分散。

还有个隐蔽的问题是编码器过拟合。如果编码器在训练集上把每个少数类样本都映射到几乎相同的特征点,SMOTE插值出来的样本会高度相似,多样性不足。这时候要检查对比损失的权重是不是太大了,或者编码器容量是不是过剩。适当降低对比损失权重、加Dropout、减小特征维度都有帮助。

5.3 少数类召回率上不去的排查路径

少数类召回率低,按这个顺序排查:先看特征空间的可视化,用t-SNE或者UMAP把特征降到二维,观察少数类样本是否跟多数类混在一起。如果混在一起,说明对比损失没学好,需要增大对比损失权重或者增加训练轮数。如果少数类自己聚成一团但跟多数类有重叠,说明特征空间本身有区分度,但分类边界没调好,可以调整分类头的阈值或者用代价敏感学习。

如果特征空间看起来没问题但召回率还是低,检查测试集的分布。有时候测试集里的少数类样本跟训练集里的故障阶段不一样,模型没见过这种模式,召回率自然低。这种情况需要在数据划分时保证故障阶段的覆盖,或者用域适应的方法。

5.4 常见问题速查表

问题现象可能原因排查方法解决措施
训练损失震荡温度系数过小打印对比损失值温度系数调到0.07-0.1
生成样本无效邻居k值过大可视化特征空间k降到3-5
少数类召回低对比损失权重不足检查类间距离权重从0.3提到0.5
编码器过拟合容量过剩对比训练验证损失加Dropout或降特征维度
批次内正样本少采样策略问题统计批次类别分布用类别感知采样器
梯度爆炸对比损失梯度大监控梯度范数梯度裁剪到1.0

5.5 几个我踩过的坑

第一个坑是位置编码的长度。我一开始把位置编码设成固定长度,结果信号长度变化时直接报错。后来改成可截断的可学习位置编码,最大长度设大一点,实际用的时候按需截取。

第二个坑是特征空间SMOTE之后忘了重新归一化。编码器输出的特征向量模长不一,SMOTE插值之后模长更乱。分类头训练之前一定要做一次L2归一化,否则分类边界会被模长大的样本主导。

第三个坑是评估时用了增强后的测试集。特征空间SMOTE只能用在训练集,测试集必须保持原始分布。我早期为了“平衡”测试集也做了增强,结果指标虚高,上线后原形毕露。这个错误很隐蔽,因为代码上只是多调用了一次SMOTE函数,但影响很大。

第四个坑是对比损失和SMOTE的顺序。前面提过,必须先对比学习再SMOTE。我试过反过来,先生成再对比,生成样本的噪声被对比损失放大,模型学到的特征空间反而更乱。这个顺序问题在论文里往往一笔带过,但实际实现时很容易搞错。

6. 实际项目中的参数调优经验

6.1 特征维度与头数的搭配

特征维度和注意力头数的搭配有个经验比例:每个头的维度不要低于8。64维特征配8个头,每头8维,这是下限。如果特征维度降到32,头数也要降到4,否则每头只有8维不到,注意力表达力不够。我试过32维配8头,每头4维,注意力权重几乎均匀分布,学不到有意义的模式。

如果故障类型特别多,比如十几种复合故障,特征维度可以提到128,头数提到8或16。但维度上去之后,SMOTE的邻居搜索会变慢,而且小样本下过拟合风险增加。这时候可以用PCA先降维再做SMOTE,或者用马氏距离代替余弦距离。

6.2 对比损失权重的动态调整

固定权重的对比损失在训练后期可能阻碍分类损失下降。我的做法是让对比损失权重随训练轮数衰减,从0.5线性降到0.1。前期对比损失主导,把特征空间整理好;后期分类损失主导,精细调整分类边界。这个策略在多个项目里都比固定权重效果好,少数类F1能提升3到5个百分点。

衰减的节奏要跟数据规模匹配。数据少的时候衰减快一点,10个epoch降到位;数据多的时候可以拉长到30个epoch。判断依据是验证集上的类间距离,如果类间距离已经稳定,就可以开始衰减了。

6.3 SMOTE扩充比例与分类阈值

扩充比例不是越高越好。我做过一组对比实验,少数类扩充到多数类的50%、70%、100%三档,70%那档的少数类F1最高。100%那档虽然平衡了类别,但合成样本太多,分类边界被推得偏向多数类,少数类召回反而降了。

分类阈值也要调。默认0.5的阈值在平衡数据上合理,但不平衡数据下,少数类的预测概率普遍偏低。我一般把少数类的分类阈值降到0.3到0.4,用验证集上的F1来确定具体值。这个操作跟SMOTE是互补的,SMOTE解决数据层面的不平衡,阈值调整解决决策层面的不平衡。

7. 写在最后的一些个人体会

这套Feature-level SMOTE加多头自注意力和对比损失的组合,我在风电齿轮箱、高铁轴承、工业泵机组三个场景里都落地过。效果最明显的是高铁轴承数据,少数类召回率从原来的0.62提到0.89,误报率只涨了不到两个百分点。风电齿轮箱因为工况变化大,提升幅度小一些,但少数类F1也涨了15个点左右。

最大的体会是:PHM的不平衡问题不能只靠数据增强解决,特征空间的质量才是根本。对比损失和多头自注意力把特征空间整理好了,SMOTE才能发挥效果。如果特征空间本身是乱的,再多的合成样本也只是在噪声上做文章。

另外,这套方法对数据预处理和训练稳定性的要求比较高。我建议新手先从简单的分类损失加原始空间SMOTE跑通基线,再逐步加入自注意力和对比损失,最后上特征空间SMOTE。每一步都验证指标变化,不要一次性全上,否则出了问题很难定位是哪个环节的锅。

最后分享一个小技巧:特征空间SMOTE之后,可以用生成样本跟原始少数类样本的余弦相似度做一次过滤,把相似度低于0.5的生成样本丢掉。这些样本大概率是插值到了不合理的区域,留着反而有害。这个过滤步骤很简单,但在实际项目里帮我省了不少调参时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询