聚类欠采样+孪生自注意力:解决不平衡故障诊断的实战方案

发布时间:2026/10/21 7:33:47
聚类欠采样+孪生自注意力:解决不平衡故障诊断的实战方案
你见过那种准确率99.8%的故障诊断模型吗我曾经在一个轴承振动数据集上跑出一个看起来非常漂亮的分类器——准确率99.8%精确率99.7%召回率99.6%我当时以为这个项目已经稳了。直到另一个同事提醒我你那个数据集里正常样本有18000条故障样本只有96条。一个把所有样本都判成正常的傻瓜模型准确率都能到99.5%。而我那个99.8%的模型故障类召回率其实只有37%——十个真实故障能漏掉六个。这就是不平衡故障诊断最坑人的地方绝大多数工业场景里设备正常时间是绝对的多数故障样本可能只有1%甚至千分之几。采集数据时你不可能为了凑样本量故意把设备跑到坏。所以面对这种历史故障样本极度稀缺正常样本多到用不完的数据我试过随机欠采样、过采样、改损失函数、设计异常检测模块效果都一般。最后让我在这个问题上获得明显提升的是一个组合方案聚类欠采样 深度孪生自注意力网络。这篇文章就把我完整跑通这套方案的思路、代码要点和踩坑记录整理出来。它解决的问题非常明确当故障样本稀少、类别分布严重倾斜时如何在不过拟合、不丢失少数类特征的情况下稳定提升故障识别率。适合正在处理设备监测数据的算法工程师、做预测性维护的团队以及被类不平衡折磨的科研人员参考。1. 不平衡故障诊断的痛点到底在哪不只是样本不够很多人一听不平衡第一反应就是那我多复制几份故障样本不就行了这个想法我一开始也有但后来发现工业故障诊断的不平衡问题远比复制粘贴复杂得多。1.1 稀缺样本同时存在数量维度和分布维度双重挑战故障样本少首先带来的是统计意义上的问题数据太少模型看不到足够的模式。但更麻烦的是第二个问题——故障样本的分布是稀疏且碎片化的。拿旋转机械来说轴承内圈磨损不是一个单一的模式它有早期轻微磨损、中期局部剥落、晚期严重碎裂等多种状态不同转速、不同负载下同一个故障的表现特征差异极大。96条故障样本里可能包含5种故障类型、每种类型下又有不同劣化阶段真正落到某一种故障的某个阶段可能只有5到10条样本。这就是为什么单纯用SMOTE合成少数类过采样技术这类方法效果有限。SMOTE在特征空间里对少数类样本做插值但如果少数类样本本身就分散在多个互相隔开的簇里插值出来的假样本可能会落在不同故障类型之间的空白区域反而污染了类别边界。我后来跑过一次SMOTE CNN的基线故障类F1从0.31提到了0.42看着是涨了但把生成样本单独拿出来做T-SNE可视化发现相当一部分合成样本落在了正常样本的领地里——这些样本进训练集之后直接把正常类和故障类的决策边界搅浑了。1.2 数据层面和算法层面先后顺序是有讲究的我的核心经验是面对极度不平衡的故障数据先做数据层面的压缩再做算法层面的适配顺序不能反。先用欠采样把多数类正常样本压缩到合理范围内把类别比例从100:1降到3:1或5:1这个量级后面不管接什么模型都好办。如果数据层面不动直接上一堆类别平衡损失函数比如Focal Loss训练过程很容易被大量正常样本主导损失函数的调节强度很难掌握。反过来如果先把数据处理到相对平衡模型就可以用标准的对比学习或度量学习框架来训练而不必过度依赖那些为了对抗不平衡而设计的复杂损失函数。所以整个流程我定为三步第一步聚类欠采样压缩正常样本同时保留其分布多样性第二步设计孪生自注意力网络把分类问题转化成相似度度量问题第三步用对比损失训练最后接一个轻量分类头输出具体故障类别。下面逐个拆开。2. 聚类欠采样压多数类样本但要压得有策略刚开始我做欠采样是用最原始的方式——随机抽。从18000条正常样本里随机抽300条和故障样本凑在一起训练。跑了几个实验发现随机欠采样有一个极隐蔽的坑它会破坏多数类的局部密度结构。2.1 为什么随机欠采样会伤到正常样本的骨架正常样本虽然量大但并不是均匀分布的。设备在不同工况下运行正常振动信号的幅值、频率成分差别很大。比如说某风机在800转/分和1500转/分下正常振动特征在特征空间里就是两个相距很远的簇。如果你的故障样本恰好主要出现在低速段而随机欠采样把高速段的正常样本抽走了大半模型就会学到低频就是故障这种错误的相关性。更麻烦的是随机欠采样没有代表性概念。它抽出来的300条正常样本可能在特征空间里扎堆在一个小区域内而另一些正常工况区域完全没被覆盖。模型根本没见过那些区域的正常样本等真正的在线数据来了那些工况下出现一点正常的波动模型就会误报成故障。聚类欠采样解决的就是这个问题。核心思路简单先把多数类样本聚成K个簇然后从每个簇里按比例抽取样本。这样无论正常样本有多少种工况模式每一种模式都能在压缩后的数据集里保留代表样本压缩的是每个簇内的冗余度而不是某些簇的生存权利。2.2 聚类欠采样的两种实现路线我在项目里尝试过两种路线效果差别不小。第一种是用现成库里的ClusterCentroids。它的逻辑是对多数类做KMeans聚类然后用每个簇的质心点作为该簇的代表样本。这样做的优点是执行极快只需要一行代码from imblearn.under_sampling import ClusterCentroids cc ClusterCentroids( sampling_strategy0.33, # 欠采样后多数类与少数类的数量比 estimatorKMeans(n_clusters120, random_state42) ) X_resampled, y_resampled cc.fit_resample(X_train, y_train)这里sampling_strategy0.33的意思是让多数类样本数压缩到少数类样本数的1/3——注意是少数类样本数的三分之一也就是说压缩完后多数类反而变少了整个训练集以故障样本为主。这个比例我反复调过最终在0.2到1.0之间选值。如果故障样本的类内差异不大取1左右即可如果故障样本稀碎、工况跨度大取0.3到0.5会更稳。ClusterCentroids的问题在于直接用簇心代表一个簇会丢掉簇内部的方差信息。正常样本在一个工况模式下也是有波动的簇心只是平均水平极端一点的正常波动可能会在压缩后消失导致模型把那些正常的极端波动误判为故障。第二种路线是我最终采用的方式先对少数类故障样本做聚类再对多数类做聚类然后根据簇间距离抽样。完整过程如下对故障样本做KMeans聚类得到几个故障子簇。比如已知有3种故障模式就聚成3簇。对正常样本做KMeans聚类得到一个初始簇划分。计算每个正常样本簇中心与各个故障子簇中心的距离优先保留距离故障簇较近的正常簇中的样本同时拉远簇中的样本也按比例保留一部分。这个做法的逻辑是靠近故障边界的正常样本是最容易混淆的区域必须保留足够多帮助模型学到细边界远离故障区域的正常样本则减少冗余即可。这个思路已经带有一定的难例挖掘味道比单纯ClusterCentroids效果好很多。2.3 聚类数的选择与最终效果对比聚类数K的选择我的经验法则是不超过最终想保留的多数类样本数除以每簇最小样本数。比如目标保留300条正常样本希望每个簇至少10条那K就控制在30以内。实际操作中K取20到50比较常见最好用轮廓系数粗筛一遍选轮廓系数较高的区间。下面这张表是我在某轴承故障数据集上做的对比训练集原始正负比例约187:1所有模型结构一致同一个轻量CNN只改变数据预处理方式数据预处理方式总体准确率故障类召回率故障类精确率故障类F1原始数据直接训练99.5%18.3%76.4%0.295随机欠采样94.7%62.5%71.2%0.665SMOTE过采样93.1%70.8%55.6%0.623ClusterCentroids欠采样96.2%75.0%82.8%0.787距离感知聚类欠采样95.4%81.3%84.5%0.829注意这个实验里故障样本本身数量少测试集中故障样本也只有50条左右所以召回率的每一步提升都对应着几条样本不需要神化某个数字。但趋势是稳定的聚类欠采样的故障类F1明显高于随机欠采样和SMOTE而且精确率和召回率相对均衡没有出现召回率提上去了但误报一堆的情况。3. 深度孪生自注意力网络把分类翻译成比较数据层面把比例处理好之后模型层面的设计是真正拉开差距的地方。我最开始用的是CNN分类器压缩后的数据上准确率能到95%但有一个绕不开的问题故障样本实在太少每种类型可能就十几条CNN分类器很容易把某个故障类型的个别特征当成类型通用特征来学——说白了就是过拟合。3.1 孪生网络的逻辑学习的是关系而不是模板孪生网络Siamese Network的思路和普通分类器完全不同。它不直接输出这个样本属于哪一类而是接受一对输入样本输出两个样本之间的相似度得分。训练时给模型看的是这一个故障样本和那一个故障样本是同一类、这个故障样本和那个正常样本不是同一类这样的成对比较。为什么这个思路对稀缺故障样本特别友好打个比方普通分类器像一个第一次见大象的人你只给他看3张大象照片让他以后在动物堆里找大象他很容易只记住照片里的背景颜色把红色墙前面的大象认成墙把背景里有水的大象认成水。而孪生网络是让他回答这两个动物是不是同一种他学会的是比较纹路、体态、轮廓这些内在属性背景干扰的影响就小得多。具体到故障诊断场景如果训练集里严重轴承剥落样本只有8条普通分类器可能把这个故障的振动冲击特征和一个特定转速下的背景噪声绑定了。但孪生网络在学习过程中会不断拿这8条样本之间的公共特征做正样本对约束把这8条样本里的真正共性提炼出来过滤掉单条样本的个性噪声。3.2 自注意力模块在振动信号里抓什么孪生网络的两个分支我最初用的是1D CNN。CNN的问题在于感受野有限想抓到振动序列里相隔很远的两个冲击之间的关联需要堆很多层网络训练难度增大。后来我引入自注意力机制Self-Attention替换掉部分卷积层效果出现了明显提升。自注意力天然适合处理非规则的长程依赖。振动信号里的故障特征往往是这样的初始冲击出现在第200个采样点附近经过共振衰减后残余信号里还有另一段微弱冲击出现在第800个采样点附近同时不同传感器通道比如驱动端、风扇端之间的相位关系也包含信息。CNN要捕捉这种跨通道、跨时间步的关联很吃力而自注意力机制在计算每个时刻的输出时可以直接看整个序列对任意两处特征做关联建模。我在每个孪生分支内嵌入了两层Transformer编码器层输入是1D振动信号先经过卷积stem切块映射成token序列然后加位置编码送入编码器。两个分支共享权重——这一点很重要如果不共享权重两个分支会各自学到不同的映射距离度量就失去意义。3.3 损失函数与距离从哪来训练孪生网络用的是对比损失Contrastive Loss公式如下L y * D^2 (1 - y) * max(margin - D, 0)^2其中D表示两个分支输出的嵌入向量之间的欧氏距离y1表示这一对样本属于同一类都是故障或者都是正常y0表示它们不是同一分类。margin是一个超参数表示不相似样本对的最小期望距离。这里有一个细节如果D是欧氏距离那么margin设多大直接影响特征空间的聚集程度。我把margin设为1.0距离通常被L2正则限制在0到2之间。推理阶段模型依然接收一对样本一个来自支撑集的已知样本一个来自待测样本计算距离D如果D小于阈值就判定为同类否则判定为不同类。这个阈值可以用训练集上性能最好的约登指数来确定不需要手动瞎调。最终如果要输出具体故障类别我的做法是在训练收敛后取出孪生网络的特征嵌入层接上一个线性分类头一个全连接层 Softmax用少量标注样本微调。这样既保留了度量学习的泛化性又补足了告诉我具体是哪种故障的输出能力。4. 落地实战数据构造、训练顺序与参数配置原理说清楚之后下面是我在一个内部项目里完整走过的流程。整体跑通大概花了三天时间大部分时间耗在数据构造和阈值调优上真正模型训练反而很快。4.1 数据时段切分与工况归一振动数据通常是连续采样的1秒钟可能就有上万个采样点。我第一步是把连续数据切成固定长度的序列片段长度选择1024个采样点对应一个脉冲周期内能覆盖至少几次旋转冲击。切段时要注意同一个连续数据文件里的相邻片段不能同时分到训练集和测试集否则会造成严重的数据泄漏。具体做法是按时间窗口ID做分组划分先给每条原始记录一个唯一的设备运行ID再按ID而不是按序列片段做train/test split。这样训练集和测试集里的片段不会来自同一条连续记录模型不会通过片段在时间上相邻这种捷径作弊。不同工况下的数据幅值差很多我在预处理里做了简单的归一化每个1024点片段减去自身均值再除自身标准差。这个操作能让模型关注波形形状而不是绝对幅值对跨转速的诊断很有帮助。4.2 样本对构造的坑与技巧孪生网络的数据构造比普通分类器麻烦。我的做法是维护一个样本索引表对于每个epoch从故障样本中随机选两条同类型样本构造成一个正样本对y1从故障样本和正常样本中各取一条构造一个负样本对y0从不同故障类型的样本中各取一条也构造一个负样本对y0正样本对的比例很关键。如果正样本对太少模型会倾向于把所有样本对都推远最后导致所有嵌入都坍塌到一个点附近。我控制每个batch里正负样本对的比例为1:1.5保证模型既能看到同类样本的距离要拉近又看到不同类样本的距离要拉开。还有一个陷阱是锚点复用太频繁。如果训练集总共只有30条故障样本那么每条故障样本可能在一次epoch里被构造了十几次样本对模型容易把这些样本本身背下来。解决办法是每个epoch进行轻微的时域增强随机裁剪、添加高斯噪声、小幅幅值缩放。增强之后的样本对每次都不完全一样模型被迫学不变特征对减少过拟合帮助很大。4.3 训练顺序先度量后分类我建议的训练顺序分两个阶段不要一上来就联合训练。第一阶段只训练孪生网络用对比损失约束嵌入空间。优化器用Adam学习率初始1e-3batch size 128训练30个epoch。这个阶段的核心目标是把正常和故障的嵌入空间分开分隔越清晰越好。第二阶段固定孪生网络的底层参数冻结前几层只训练分类头。此时每个故障类别只有很少的样本分类头参数少一个全连接层不容易过拟合。把分类头的学习率降到1e-4再微调5到10个epoch。微调结束后在验证集上搜索最佳距离阈值。搜索方式很简单遍历0到1之间步长0.01的候选阈值计算每个阈值的F1选F1最高的那个。这一步得到的是一个单纯的浮点数在部署阶段直接使用。4.4 关键超参数速查表我把实验里调过觉得比较合理的参数汇总如下具体数值需根据数据调整参数推荐值说明序列长度1024 / 2048点根据采样率和旋转周期确定需覆盖至少一个完整周期卷积stem1D CNNConv1d(3, 64, kernel7)输入多通道振动信号输出64维tokenTransformer层数2每层4个注意力头维度128嵌入维度128两个分支共享的最终特征维度对比损失margin0.5 ~ 1.0不相似样本对的最小距离宁大勿小欠采样类别比1:0.3 ~ 1:1多数类与少数类的目标比例温度/学习率第一阶段1e-3第二阶段1e-4阶段切换时必须降学习率其中margin这个参数要对嵌入维度做适配。嵌入维度是128维时高维空间中的欧氏距离天然比低维空间大margin取1.0比较稳定。如果嵌入维度只有16维margin降到0.5更合适。5. 验证与部署中的实战教训四个高频坑训练跑通只是第一步。我在把这个方案推向实际测试环境时又踩了几个坑每个都挺典型值得记录。5.1 别拿总体准确率当验收指标这是最要命的一个坑。我前面说了不平衡数据里总体准确率毫无参考价值。正确做法是盯住少数类故障类的召回率和精确率双侧指标尤其要把故障漏报看作比正常误报更严重的问题。我在验证时时刻关注故障类F1和混淆矩阵而不是盯着总体准确率这两者反映的信息完全不在一个量级。我曾见过一个团队用总体准确率98.7%的标准验收了模型等到现场发现故障根本抓不住回头检查才发现故障类召回率只有22%。验收指标一开始设错了后面全盘重来这是最亏的。5.2 阈值标定要在设备现场数据上做实验室里的验证正常样本来自训练集采集时的工况但现场设备的负荷、温度、环境噪声都不一样。嵌入空间里正常区域的中心可能发生了漂移。所以最佳路径是模型训练好后先部署到边缘网关空转一段时间收集当天的正常振动数据再基于这些新正常样本重新标定距离阈值。这一步看似繁琐却能大幅减少新设备投产初期的误报警。我遇到过实际案例某设备换了一个安装位置后振动基座共振频率变了离线测得的正常样本距离分布整体偏移了0.4原本0.6的阈值直接失效。现场重新收集一小时正常数据后把阈值校正到0.85误报率立刻降下来了。5.3 故障类型新增后分类头必须重训度量层不用代码里我把故障类别列表写死过一次后期现场新增了一种故障类型结果模型把新故障全部丢进未知类别——这本身没问题但分类头的Softmax输出会强制归一化导致已有故障类别的概率被强行压低出现新版模型把所有正常样本都误报成旧故障的诡异现象。重新做分类头微调之后才恢复正常。更合理的流程是迭代式维护一份支撑集每个故障类型保留10到20条代表性样本新故障类型确认后把新样本加入支撑集只跑分类头微调底层的度量嵌入不动。这样增量学习的成本很低效果稳定。5.4 部署推理速度两个孪生分支只需跑一次刚开始我犯过一个低级错误推理阶段把待测样本和支撑集里每一个已知样本配对依次输入孪生网络如果支撑集有30条样本就要跑30次前向计算单次推理耗时从5毫秒变成150毫秒边缘网关根本撑不住。正确做法是把支撑集所有样本的嵌入向量提前算好并缓存下来。推理时只需要把待测样本单独经过一次孪生网络的分支得到它的嵌入向量然后再和缓存的支撑集向量批量计算距离。整个流程只做一次前向推理加上一个小矩阵乘法耗时基本可控。这个优化极其简单但很多人第一次写孪生网络都会踩同样的坑。6. 写在最后这套方案的适用边界用这套方案跑过几个项目之后我对它适合什么场景、不适合什么场景有了比较清晰的判断。如果你的数据里故障样本有几千条甚至更多类别不平衡比例在10:1以内那么完全没必要上孪生网络一个普通的CNN加标准的数据处理就能做得很好。这套方案的真正适用场景是故障样本极少几十条量级、故障模式多样、且正常样本的工况分布宽广——这时候聚类欠采样保住正常工况多样性孪生网络保住故障类别不靠死记硬背自注意力保住长程特征不丢失三者各自解决问题形成互补。另外一个不可忽视的收益是这套方案天然支持开放集场景。因为它的核心是距离度量而不是固定类别分类遇到完全没见过的新故障模式时模型会给出较低的相似度得分而不是硬归到一个已知类别里给运维人员留出了人工判断的空间。最后再分享一个小经验做这类项目数据管理花费的时间往往比建模多得多。为每条样本记录设备ID、工况转速、采集时间、故障类型标注人这些元信息在构造样本对、做分组划分、现场阈值校准时全都会用到。我吃过元信息缺失导致重新返工的亏多花几天时间整理数据台账绝对值得。