资讯动态

基于聚类欠采样与孪生自注意力网络的不平衡故障诊断方案

发布时间:2026/10/21 17:23:27 来源:尧图企业网站定制
设备点检记录里那批最让人头疼的数据一定是故障样本少得可怜、正常样本多到发愁的那种。正常工况跑一天能攒几千条振动信号可真正出故障的那几十分钟能留下几十条有效记录就算运气好。拿这种数据去训深度学习模型结果往往很尴尬模型学会了“躺平”不管输入什么信号都预测成正常准确率还能高达95%以上可现场真正要防的故障漏报一次都没拦住。今天聊的这个方案标题叫“基于聚类欠采样与深度孪生自注意力网络的不平衡故障诊断”。简单说就是用聚类欠采样把数据配平再用孪生自注意力网络把故障特征学出来。这俩一个在数据侧“治标”一个在模型侧“治本”组合起来专门对付漏报问题。做设备健康管理、故障诊断算法或者研究不平衡学习的同学这篇内容应该能给你一些能直接落地的思路。1. 方案设计思路为什么非得是这个组合1.1 不平衡故障诊断到底难在哪故障诊断的不平衡性不是简单的“样本数量有差异”它带来的麻烦是结构性的。正常设备运行状态相对单一振动信号的分布比较集中特征也稳定。而故障状态五花八门轴承内圈磨损、外圈裂纹、滚动体点蚀、齿轮断齿每种故障的信号形态完全不同即使同一种故障在不同转速、不同负载下表现也不一样。这就导致少数类样本内部高度分散样本量又少模型根本学不到一个紧凑、有区分度的特征分布。更要命的是常规深度学习模型的训练目标是最小化整体损失在极度不平衡的数据上模型只需要无脑预测多数类就能把损失压得很低。准确率这个指标在这种场景下会骗人——全部判为正常准确率照样很好看但故障漏报的代价可能是一整条产线停机或者一次大型设备损坏。所以做不平衡故障诊断评估指标就得从准确率切换到少数类召回率、F1-score、G-mean这些更诚实的指标。方法设计也要围着“怎么让少数类不被多数类淹没”来转。1.2 数据层面方法那么多为什么选中聚类欠采样解决不平衡问题数据层面的思路无非两大类给少数类“添丁”的过采样以及给多数类“裁军”的欠采样。先说过采样。SMOTE是典型代表它在少数类样本之间插值生成新样本。听起来不错但故障信号有个特点——信噪比低、噪声干扰大。如果少数类样本本身数量极少、又混着噪声插值生成的伪样本极大概率落在噪声区域不但没帮助反而会误导模型把噪声特征当成故障特征。我在实际测试里见过有些故障类别做完SMOTE之后F1不升反降就是因为生成了大量不真实的中间态样本。再看随机欠采样。它简单粗暴地从多数类里随机丢样本把类别比例配平。问题在于随机丢会破坏多数类的分布结构。正常工况并不是一个均匀的球它可能包含多个工况、多个转速段、多种负载状态随机抽样可能会把某些本来就少见的正常子状态给丢掉导致模型对正常工况的建模出现“偏科”。聚类欠采样的思路就聪明在这里先用聚类算法把多数类划分成若干自然簇然后在每个簇内部按比例抽取样本。这个过程相当于把多数类压缩成一份保留原始分布结构的“精简版”而不是随机乱扔。工业数据里正常运行状态往往天然分成几个集群比如低速轻载一个簇、高速重载一个簇聚类欠采样能把这种结构信息保留下来。用生活类比解释随机欠采样是把一摞书随便抽掉一半抽完可能某类书一本不剩聚类欠采样是先按科目分好类每科各留几本虽然总量少了但知识结构还是完整的。1.3 为什么网络结构选“孪生自注意力”数据配平之后接下来是特征提取。常用的CNN、LSTM在平衡数据集上表现不错但在故障样本稀缺的场景下它们有个共同的问题本质上是在学一个绝对的分类边界。CNN靠堆卷积层扩大感受野但振动信号的故障冲击往往只占据很短的时间片段想捕捉这种局部突变网络得叠得很深深了就容易过拟合。LSTM能建模时序依赖但训练效率低对长序列的记忆也有天花板。在少数类样本只有几十条的情况下这两种网络都很难训练出稳健的分类器。孪生网络把问题换了个角度不直接学“这个信号属于哪类”而是学“这两段信号是不是来自同一类状态”。它天然适合小样本和类别不平衡的场景——只要构造出足够多的样本对即使某个类别只有几十条样本也能通过两两比较的方式把特征空间拉出来。故障类型区分变成了度量学习问题同类样本特征靠近异类样本特征远离分类边界是用距离衡量的对少数类非常友好。自注意力机制则是解决“特征到底该看哪里”的问题。振动信号里故障特征往往淹没在强烈的背景噪声和正常振动分量中传统卷积用固定大小的卷积核扫全图注意力不集中。自注意力让模型自己学习每个时间位置的重要性权重自动聚焦到故障冲击发生的片段等于给模型配了一个会自己打聚光灯的“信号分析师”。两个模块的组合逻辑也很清晰聚类欠采样负责让数据分布合理孪生自注意力负责让模型在有限样本下学到真正有判别力的特征表示。数据侧和模型侧各解决一段问题互相不冲突。2. 聚类欠采样模块从算法到工程实现2.1 聚类欠采样完整流程聚类欠采样的实现流程可以拆成六步第一步从训练集中把多数类和少数类分开。这里要注意多数类指的是“正常工况”这一类少数类是各类故障的合集。如果故障有好几种类型需要把每种故障单独当成一个类别来配平不能混在一起处理否则模型无法区分不同故障模式。第二步对多数类特征做标准化处理。振动信号的特征维度通常较高不同通道或不同特征的量纲差异大不标准化直接聚类距离度量会被量纲大的维度主宰。第三步对标准化后的多数类跑KMeans聚类。聚类数K是核心超参后面单独说怎么选。第四步统计每个簇的样本容量计算每个簇在多数类中的占比。第五步确定总体采样目标量然后按簇占比把采样名额分配到各个簇。分配时需要注意每个簇至少保留1个样本防止某些小簇被完全抽空。第六步在每一个簇内部做不放回随机抽样抽满该簇的分配名额。然后把采样得到的多数类样本和少数类样本合并构成新的训练集。这一步做完训练集里多数类和少数类的比例就被拉到了预设的均衡状态。2.2 代码实现与关键参数解析用Python实现聚类欠采样很直接核心代码大概长这样import numpy as np from sklearn.cluster import KMeans def cluster_undersample(majority_samples, n_clusters, sampling_ratio1.0, random_state42): 聚类欠采样 majority_samples: 多数类样本矩阵, shape (N, D) n_clusters: KMeans 聚类数 sampling_ratio: 采样后多数类总量 / 原多数类总量 kmeans KMeans(n_clustersn_clusters, random_staterandom_state, n_init10) cluster_labels kmeans.fit_predict(majority_samples) unique_clusters, cluster_counts np.unique(cluster_labels, return_countsTrue) total_majority len(majority_samples) target_total int(total_majority * sampling_ratio) sampled_indices [] for cluster_id in unique_clusters: # 按簇占比计算该簇应采样的数量 cluster_ratio cluster_counts[cluster_id] / total_majority n_from_cluster int(target_total * cluster_ratio) cluster_indices np.where(cluster_labels cluster_id)[0] # 当前簇样本量不够分配名额时取全部样本 n_from_cluster min(n_from_cluster, len(cluster_indices)) chosen np.random.choice( cluster_indices, sizen_from_cluster, replaceFalse ) sampled_indices.extend(chosen) return majority_samples[sampled_indices]两个关键参数直接决定效果。第一个是n_clusters。聚类数决定了多数类内部结构被划分的粒度。工业数据上我一般从多数类样本量的平方根附近起步比如正常样本有5000条K就设70左右。然后结合业务知识微调如果现场知道设备有几个典型工况K最好不小于工况数量如果数据还包含不同的转速段、负载段K还要再大一些。K太小簇内的样本不够“纯”保留了过多内部差异K太大每个簇的样本量太少抽样的时候小簇可能被抽空反而引入采样噪声。第二个是sampling_ratio。如果想让训练集类别完全均衡ratio就设为少数类总量除以多数类总量。但在故障类型很多、每种故障样本又极少的情况下强行完全均衡会让多数类被压缩到和最小故障类一个量级信息损失太大。我的经验是先按完全均衡跑一个基线然后逐步把ratio往上调找到故障类F1开始明显下降之前那个临界点。工程中经常取ratio让多数类最终数量是少数类的2到3倍效果比强行均衡更稳。注意KMeans对初始中心点敏感random_state必须固定否则每次跑出来的采样结果都不一样实验对比时会分不清性能变化是来自模型还是来自采样。2.3 聚类欠采样的边界情况与处理实际工业数据比理想情况脏得多几个高频出现的坑提前说下。第一噪声点会形成孤立小簇。KMeans会把远离主群的噪声点强行归到某个簇或者形成样本数极少的小簇。这些噪声点如果被采样进训练集会让模型学到虚假的“正常模式”。处理方式有两种一是对簇内样本量设定阈值比如样本数少于该簇平均样本量10%的簇直接剔除二是在聚类前先用孤立森林或3倍标准差规则把明显的离群点滤掉。第二多数类内部工况分布不均。有些工况运行时间长样本多有些短样本少。如果只按簇占比抽样稀务工况的样本可能被抽掉大半。如果稀务工况在业务上同样重要可以改成每个簇均匀分配采样名额牺牲一点整体分布保真度换来每个子状态的覆盖。第三少数类内部也有不同故障模式。上面说过不同类型故障必须分开配比。实际代码里应该对每个故障类别分别计算样本量再分别和多数类做配平。如果把所有故障样本当成一个“少数类”整体模型能区分故障和正常但分不清具体故障类型诊断价值就少了一大半。第四高维特征下的聚类退化。特征维度很高时欧氏距离会趋向于集中KMeans的聚类效果会变差。可以先做主成分分析或者自动编码器降维到50到100维再做聚类欠采样。效率更高聚类结果也更干净。3. 深度孪生自注意力网络架构拆解与实现要点3.1 孪生网络在故障诊断里怎么用孪生网络的核心是一个共享权值的特征提取器输入两段信号分别提取特征然后通过距离度量判断它们是否属于同一类别。假设有一个特征提取器f_θ输入信号x得到特征向量f_θ(x)。训练时构造样本对(x_i, x_j)如果来自同一类别优化目标就是让它们的特征距离尽可能小如果来自不同类别就让特征距离尽可能大。距离通常用欧氏距离或者余弦距离。这种“比较式”学习方式给少数类带来的优势很明显模型不需要见到足够多的某类样本来学绝对边界只需要在成对学生中学会“相似/不同”的判断。就像认人一样重要特征是通过对比来界定的不需要认识全世界的人才能认出自己的熟人。推理阶段也有讲究。模型不能直接把测试样本送进一个分类层输出类别而是要把测试信号的特征向量和已经存好的各类支撑样本的特征向量做距离比较K近邻投票决定类别。支撑样本可以从带标签的训练数据里选每个类别保留几个最有代表性的样本特征就行。3.2 自注意力模块怎么嵌进特征提取器在孪生网络里自注意力模块放在特征提取器的中后段比较合适。原始振动信号如果直接做自注意力序列太长、计算量太大而且原始信号噪声重注意力权重容易被噪声带偏。合理的结构是信号先经过几层卷积或全连接提取初步特征然后切成时间片段再把片段序列送入自注意力层最后做全局池化和特征投影。以PyTorch为例核心类大概是这样import torch import torch.nn as nn class SelfAttention1D(nn.Module): def __init__(self, embed_dim, num_heads4, dropout0.1): super().__init__() self.mha nn.MultiheadAttention(embed_dim, num_heads, dropoutdropout, batch_firstTrue) self.norm1 nn.LayerNorm(embed_dim) self.ffn nn.Sequential( nn.Linear(embed_dim, embed_dim * 4), nn.ReLU(), nn.Linear(embed_dim * 4, embed_dim) ) self.norm2 nn.LayerNorm(embed_dim) def forward(self, x): # x: (B, T, D) attn_out, _ self.mha(x, x, x) x self.norm1(x attn_out) ffn_out self.ffn(x) x self.norm2(x ffn_out) return x class FaultEncoder(nn.Module): def __init__(self, input_dim, seg_len16, embed_dim64, num_heads4): super().__init__() self.proj nn.Linear(input_dim, embed_dim) self.attn SelfAttention1D(embed_dim, num_heads) self.fc nn.Sequential( nn.Linear(seg_len * embed_dim, 128), nn.ReLU(), nn.Linear(128, 64) ) def forward(self, x): # x: (B, T) 原始振动信号 x self.proj(x.unsqueeze(-1)) # (B, T, D) x x.reshape(x.size(0), -1, self.attn.mha.embed_dim) # 如果序列过长先做分段池化再进注意力 x self.attn(x) # (B, T, D) B, T, D x.shape x x.reshape(B, T * D) # 展平 x self.fc(x) x nn.functional.normalize(x, p2, dim1) return x class SiameseNetwork(nn.Module): def __init__(self, input_dim, **kwargs): super().__init__() self.encoder FaultEncoder(input_dim, **kwargs) def forward(self, x1, x2): z1 self.encoder(x1) z2 self.encoder(x2) return z1, z2有几个实现细节需要特别提醒。一是残差连接不能省。自注意力虽然能捕捉远距离依赖但深层堆叠时容易梯度退化残差结构能稳定训练。二是特征归一化放在最后一步。输出向量做L2归一化之后欧氏距离和余弦相似度在数学上是等价的训练会更稳定。不归一化直接用原始特征计算距离特征尺度小的类别在距离比较里会吃亏。三是序列长度控制。自注意力的复杂度是O(T²)T是序列长度。振动信号一秒钟可能就有几千个采样点直接全序列做注意力计算量太大。我的做法是先用一维卷积或平均池化把序列长度降下来保留关键形态但把长度缩短到几十个时间步再做自注意力。工业数据上把这个阈值控制在64个步长以内训练速度和显存占用都比较友好。3.3 损失函数与训练策略孪生网络的训练目标是让同类特征靠近、异类特征远离。最常用的是对比损失Contrastive Loss对于一对特征(z1, z2)如果它们属于同一类损失是d²如果属于不同类损失是max(0, margin - d)²。其中d是特征间的欧氏距离margin是预定阈值表示异类样本至少要离开多远。def contrastive_loss(z1, z2, label, margin2.0): distance torch.nn.functional.pairwise_distance(z1, z2) loss torch.where( label 1, distance.pow(2), torch.clamp(margin - distance, min0).pow(2) ) return loss.mean()训练策略上样本对的构造比损失函数更重要。正对同类样本和负对异类样本的比例建议控制在1比1到1比2之间。如果负对太多模型会偷懒把所有样本都推得远远的反正没有正对压力。如果正对太多模型又可能把所有样本都拉到一起。我踩过的一个坑是“难分负对不够”。工业数据里有些故障样本的特征本来就接近正常样本如果训练时每次随机采样对大概率采到那些明显好分的样本对模型学完还是个“睁眼瞎”。后来加了在线困难样本挖掘在每个batch内部计算所有样本对的距离找出距离小于当前margin的负对把这些难分对额外加到训练里。改动不大但故障类F1能明显再上一个台阶。训练过程的优化器建议用AdamW初始学习率1e-3到1e-4之间配合warmup和余弦退火。另外用平衡后的训练集训练但验证集一定用保持了原始类别比例的数据这样早停策略才有意义——如果验证集也是配平后的模型的过拟合倾向会被隐藏。4. 实验验证与结果分析这套组合到底能涨多少点4.1 实验设置与评价指标验证方案效果需要一套完整的实验流程。我以一次模拟实验来展示完整的对比逻辑。假设数据来自某旋转机械试验台正常状态有5000条样本三种故障类型内圈故障、外圈故障、滚动体故障各只有80条样本。每条样本是1024个采样点的一维振动信号片段。按照7比3划分训练集和测试集训练集内部做不平衡处理测试集保持原始类别比例不变。评价指标重点关注四个准确率、故障类F1均值、G-mean、各类召回率。准确率只作为参考核心看少数类的召回和F1。4.2 基线方法对比方法准确率故障类F1均值G-mean现象原始CNN不做任何不平衡处理96.1%0.4820.351模型几乎放弃所有故障类随机欠采样 CNN91.5%0.6740.612有一定提升但漏报仍然偏多SMOTE CNN92.3%0.7030.656部分故障类别反而变差聚类欠采样 CNN93.8%0.7410.698数据层面的改进效果明显聚类欠采样 孪生自注意力95.4%0.8630.832本文组合方法原始CNN的不平衡问题看得非常清楚准确率96.1%很唬人但故障类F1只有0.482相当于有一半故障没被识别出来。随机欠采样把F1拉到0.674说明配平数据确实有用SMOTE表面分数更好但逐类细看会发现外圈故障的召回率反而下降插值生成的伪样本管了倒忙。聚类欠采样比随机欠采样又高了0.067的F1这个提升主要来自多数类结构信息的保留。最终组合的故障类F1从最开始的0.482涨到0.863G-mean从0.351涨到0.832。注意最终的准确率并不是表格里最高的因为模型不再无脑判正常一些边界信号会被谨慎地判为故障这在工业场景里恰恰是合理的——漏报和误报之间宁可多报几次让人工看一眼。4.3 消融实验每个模块到底在起什么作用配置故障类F1均值结论完整方案聚类欠采样 孪生自注意力0.863基准去掉聚类欠采样只用孪生自注意力0.621数据配平贡献巨大去掉自注意力用孪生 CNN0.774注意力贡献约0.09 F1去掉孪生结构用聚类欠采样 自注意力分类器0.792孪生结构贡献约0.07 F1消融实验的结果很有意思数据层面模块的贡献比模型层面更大。这也是工业经验的验证不平衡诊断问题里先把数据配平做好再谈网络结构创新。聚类欠采样不应该被当成一个可有可无的预处理它的贡献直接反映在0.24的F1差值上。自注意力的贡献主要体现在难分样本的判别上。检查自注意力权重热力图可以看到模型确实会把权重集中在故障冲击发生的时间窗附近背景噪声区域的注意力权重很低。这种特征选择能力是普通卷积层很难学到的。5. 实操中的常见问题与排查技巧5.1 高频问题速查表实践过程中我把踩过的坑整理成了速查表遇到问题可以直接对照着查。现象可能原因解决措施训练loss震荡不降学习率过高、特征未归一化降到1e-4以下在输出层加L2归一化模型把几乎所有样本判为正常训练集配平没做好、正对太少检查sampling_ratio调整正负对比例到1比1左右自注意力权重趋近均匀分布序列过长、输入噪声太大先用卷积下采样再进注意力或者增大注意力head数量测试准确率高但故障召回低验证集用了配平后的数据验证集必须保持原始类别比例聚类采样结果每次不一样KMeans初值敏感固定random_state增大n_init训练集精度高、测试集塌方过拟合支撑样本选得不好支撑样本不能选训练集全量每类只选少量代表点5.2 训练不收敛的排查思路孪生网络训练不收敛是新手最容易碰到的难题。症状通常是loss居高不下、验证集F1始终在0.5以下徘徊。排查顺序按这几步来。第一步检查特征输出是否做了归一化。对比损失对距离的绝对值敏感如果特征向量不归一化不同batch的特征尺度忽大忽小损失函数就会疯狂波动。加上L2归一化之后距离范围被限制在0到2之间训练会稳定很多。第二步检查正负样本对的比例和构造方式。我一个朋友的项目里负对数量是正对的三倍模型很快把所有样本都推开了特征空间被撕裂正对样本也靠不拢。把比例调到1比1之后问题立刻缓解。第三步检查margin的设置是否过大。对比损失里margin如果取得太大模型会拼命把异类样本推远导致特征空间过于分散正类样本反而聚不起来。工业数据上margin设在1到2之间通常够用。5.3 推理阶段的几个关键细节模型训练完之后很多人只在测试集上算了个F1就收工了。但真正上线部署推理流程和训练流程是不对称的。测试样本的特征提取和支撑样本的特征提取必须完全一致包括预处理、归一化、切段方式。有一个项目里训练时用了振动信号的加速度幅值测试时却用了速度幅值特征分布整体偏移后果就是训练F1接近0.9上线后直接掉到0.5。支撑样本的特征向量库需要持续更新。现场运行几个月后设备的状态可能因为润滑条件变化、部件磨损累积而发生漂移老支撑样本的代表性下降。我的做法是每周用新打标签的样本增量更新支撑特征库这个过程不需要重新训练整个网络成本很低但能有效延缓模型性能衰减。5.4 工程落地的几点忠告最后说几句工程角度的建议。第一切段长度要有物理意义。振动信号切段不是随便切一个1024点就完事。切段长度最好覆盖至少一个转子旋转周期这样每个样本都包含完整的故障冲击循环。如果切得太短故障冲击可能只出现在部分样本里模型学到的特征一致性差。第二对极少数样本要舍得做数据增强。如果某类故障只有二三十条样本聚类欠采样再怎么努力也很难凭空变出更多信息。这时候可以对少数类信号做小幅度的平移、加噪、幅值缩放和时域扭曲扩充样本量。增强幅度不要太大控制在原始信号幅度的5%到10%以内否则会引入失真。第三不要盲目追求端到端。有些振动信号本身含有明显的工频成分和齿轮啮合频率这些成分对故障诊断是干扰而非帮助。先用带通滤波或谱分析把关心频段提取出来再做不平衡处理和特征学习效果会比直接喂原始信号好很多。这不是多此一举而是让模型把有限的表达能力用在刀刃上。我在实际应用里最深的一个感受是这套方案真正解决的是“漏报”问题。以前巡检靠人耳听诊和经验判断微弱故障很难发现现在模型能在故障冲击还很不明显的时候给出预警值班工程师只需要去复核模型报出的那几条少数字段。不过也要说句公道话如果故障样本真的只有个位数再精巧的方法也难为无米之炊那时候不如先别上深度学习靠传统特征和规则撑住同时有意识地攒标注数据等样本量上来后再切换模型。这套方案适合的是“少数类有一定数量但不多”的场景从这个起点出发它能帮你把故障识别能力往上实实在在拉一个台阶。

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

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

免费获取报价 →
↑