ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

特征级SMOTE+DSMHSA:破解PHM故障诊断中的极端类别不平衡难题

特征级SMOTE+DSMHSA:破解PHM故障诊断中的极端类别不平衡难题 1. 为什么特征级SMOTE在PHM里是个绕不开的坎做重大装备健康管理PHM的人都有一个共同的痛正常数据多如牛毛故障数据少得可怜。一台风电机组可能连续运转几个月都是健康状态轴承真正出现早期剥落的那几个小时数据在几十万条样本里连零头都算不上。这种极端类别不平衡直接导致分类器“学会”了一件事——把所有样本都判成健康准确率照样99%以上但故障一个都抓不住。SMOTESynthetic Minority Over-sampling Technique就是冲着这个问题来的。它的核心思路不复杂在少数类样本之间做线性插值生成新的合成样本让分类器在训练时“看到”更多故障模式。但问题在于很多人在PHM项目里直接把SMOTE套在原始振动信号或温度序列上结果合成出来的数据物理上根本说不通——两个不同转速下的振动波形插值出来的信号既不像健康也不像故障纯属给模型喂噪声。这就是“特征级SMOTE”要解决的事。不在原始时序上做而是在提取完特征之后、送入分类器之前在特征空间里做过采样。这个思路在PHM领域越来越被认可因为它尊重了信号处理的物理意义特征是你精心设计的转速、峭度、包络谱峰值这些量纲和物理含义都明确在特征空间插值至少不会造出“半真半假”的波形。我这次实践的核心目标很明确用特征级SMOTE配合DSMHSADual-Stream Multi-Head Self-Attention双流多头自注意力模型在重大装备故障诊断任务中把少数类召回率拉上来同时不让整体准确率崩掉。适合正在做PHM项目、被类别不平衡折磨、想找一个可落地方案的工程师参考。下面我把整个设计思路、实操细节、踩过的坑全部摊开讲。2. 整体方案设计与选型逻辑拆解2.1 为什么不在原始信号上做SMOTE先说清楚这个决策背后的逻辑。原始振动信号是典型的时间序列相邻采样点之间有强相关性。你在两个故障样本之间做线性插值得到的“合成信号”在频域上可能完全失真。举个例子一个样本是内圈故障特征频率在120Hz另一个样本是外圈故障特征频率在80Hz。插值出来的信号在100Hz附近出现能量但实际装备根本没有这个故障模式。这种合成数据训练出来的模型到了现场就是灾难。特征级SMOTE就不一样了。假设你提取了时域特征均方根、峭度、峰值因子和频域特征各频段能量占比、包络谱峰值这些特征在物理上是可解释的。在两个故障样本的特征向量之间插值相当于在“故障特征空间”里找到了一个中间状态。虽然这个中间状态不一定对应某个真实故障但它至少落在故障区域的凸包内不会跑到健康区域去。注意特征级SMOTE的前提是你的特征提取环节必须可靠。如果特征本身噪声大、区分度低在特征空间做SMOTE只会放大噪声。2.2 DSMHSA模型为什么适合配特征级SMOTEDSMHSA是我这次选用的分类骨干。它的双流结构设计很巧妙一路走局部特征提取比如一维卷积一路走全局注意力多头自注意力最后融合。这种结构对特征向量的输入特别友好因为特征向量本身已经是定长向量不需要像原始信号那样考虑时序对齐问题。多头自注意力机制的好处是能捕捉特征之间的高阶交互。比如“峭度升高”和“包络谱在故障特征频率处能量增大”这两个特征单独看可能都不足以判定故障但组合起来就是强证据。DSMHSA的多头机制可以同时关注不同的特征组合模式这对故障诊断这种需要综合判断的任务非常合适。另外DSMHSA的参数量可控。重大装备PHM往往要求模型能部署到边缘设备上不能搞一个几百兆的巨型网络。DSMHSA在保持较强表达能力的同时参数量比纯Transformer小很多推理速度也能接受。2.3 整体流程设计整个方案的流程分四步走数据预处理与特征提取从原始振动信号中提取时域、频域、时频域特征形成特征向量。特征级SMOTE过采样只在训练集的少数类样本上做SMOTE生成合成特征向量。DSMHSA模型训练用过采样后的训练集训练分类器。评估与调优在原始测试集不做任何过采样上评估重点关注少数类召回率和F1-score。这个流程的关键在于SMOTE只在训练集上做测试集保持原始分布。否则你评估的就是一个虚假的平衡世界到了现场照样翻车。3. 核心细节解析与实操要点3.1 特征提取SMOTE效果的上限由它决定特征提取这一步我花了整个项目一半的时间。原因很简单特征级SMOTE的效果本质上取决于特征空间里少数类样本的分布质量。如果特征本身区分度不够SMOTE生成的合成样本就是垃圾。我最终选用的特征集包括三类时域特征均方根RMS、峭度Kurtosis、峰值因子Crest Factor、裕度因子Margin Factor。这四个是旋转机械故障诊断的经典指标计算量小物理意义明确。频域特征通过FFT得到频谱后提取各频段能量占比比如0-100Hz、100-500Hz、500-1000Hz、1000Hz以上以及故障特征频率处的幅值。时频域特征用小波包分解得到各频带的能量熵。这个对早期微弱故障特别敏感。实操心得特征提取后一定要做标准化。不同特征的量纲差异巨大RMS可能是几十峭度可能是几不做标准化的话SMOTE插值时量纲大的特征会主导距离计算。标准化我用的是Z-score按特征维度分别做。注意标准化参数均值和标准差只能从训练集计算然后应用到测试集。这个细节很多人会忽略导致数据泄露。3.2 特征级SMOTE的参数选择与计算过程SMOTE的核心参数是k近邻数k_neighbors和采样倍率sampling_strategy。这两个参数直接决定合成样本的质量和数量。k_neighbors的选择默认值是5但在PHM场景下我建议根据少数类样本数量动态调整。如果少数类样本只有几十个k5可能太大会导致合成样本过度分散。我的经验公式是k min(5, floor(sqrt(n_minority)) - 1)其中n_minority是少数类样本数。比如少数类有50个样本sqrt(50)≈7floor(7)-16min(5,6)5还是用5。如果少数类只有20个样本sqrt(20)≈4floor(4)-13那就用k3。sampling_strategy的选择这个参数控制合成样本的数量。常见做法是把少数类过采样到和多数类一样多sampling_strategyauto或1.0。但在PHM里我不建议这么做因为过度过采样会引入大量合成样本导致模型过拟合到合成数据上。我的做法是分阶段调整阶段sampling_strategy目的初期0.3先少量合成观察模型表现中期0.5逐步增加找到召回率和精确率的平衡点后期0.7-1.0如果少数类召回率仍不达标继续增加实测下来sampling_strategy0.5左右往往是最佳平衡点。少数类召回率能提升20-30个百分点同时整体准确率只下降1-2个百分点。合成样本的生成过程对于少数类中的每一个样本x_iSMOTE会从它的k个最近邻中随机选一个x_j然后生成新样本x_new x_i rand(0,1) * (x_j - x_i)这个公式看起来简单但在特征空间里做的时候要注意不同特征维度的尺度已经通过标准化统一了所以rand(0,1)对所有维度是同一个随机数保证合成样本落在两个真实样本的连线上。3.3 DSMHSA模型的关键配置DSMHSA的双流结构我做了针对性调整。第一路局部流用三层一维卷积卷积核大小分别是3、5、7捕捉不同尺度的局部特征交互。第二路全局流用两层多头自注意力头数设为4因为特征维度不算特别高头数太多反而会分散注意力。融合层我用的是门控融合机制而不是简单的拼接或相加。门控融合的公式是g sigmoid(W_g * [h_local; h_global]) h_fused g * h_local (1-g) * h_global这样模型可以自适应地决定在哪些样本上更依赖局部特征哪些样本上更依赖全局注意力。实测下来门控融合比简单拼接的F1-score高了约3个百分点。训练配置方面优化器Adam学习率1e-3带余弦退火损失函数Focal Lossgamma2alpha0.25Batch size64Epochs200带早停patience20注意用了SMOTE之后训练集里少数类样本变多了但Focal Loss仍然有必要。因为SMOTE生成的合成样本和真实样本的“难度”不一样Focal Loss可以让模型更关注难分类的样本。4. 完整实操流程与关键环节实现4.1 数据准备与特征提取代码实现数据来自某型旋转机械的振动监测数据采样频率12.8kHz包含健康、内圈故障、外圈故障、滚动体故障四类状态。原始数据按时间窗口切分每个窗口1024个采样点。特征提取的代码框架如下import numpy as np from scipy.stats import kurtosis, skew from scipy.fft import fft import pywt def extract_features(signal, fs12800): features [] # 时域特征 rms np.sqrt(np.mean(signal**2)) kurt kurtosis(signal) peak np.max(np.abs(signal)) crest peak / rms margin peak / (np.mean(np.sqrt(np.abs(signal)))**2) features.extend([rms, kurt, crest, margin]) # 频域特征 n len(signal) spectrum np.abs(fft(signal))[:n//2] freqs np.fft.fftfreq(n, 1/fs)[:n//2] bands [(0,100), (100,500), (500,1000), (1000,2000), (2000,5000)] total_energy np.sum(spectrum**2) for low, high in bands: idx np.where((freqs low) (freqs high))[0] band_energy np.sum(spectrum[idx]**2) / total_energy features.append(band_energy) # 时频域特征小波包能量熵 wp pywt.WaveletPacket(signal, db4, modesymmetric, maxlevel3) energies [] for node in wp.get_level(3, natural): energies.append(np.sum(node.data**2)) energies np.array(energies) prob energies / np.sum(energies) entropy -np.sum(prob * np.log(prob 1e-10)) features.append(entropy) return np.array(features)这段代码提取了4个时域特征、5个频段能量占比、1个小波包能量熵共10维特征。实际项目中可以根据需要增加特征维度但要注意维度太高会稀释SMOTE的效果。4.2 特征级SMOTE的实现与调参SMOTE的实现我直接用了imbalanced-learn库但做了一些定制化调整from imblearn.over_sampling import SMOTE from sklearn.preprocessing import StandardScaler from collections import Counter # 假设X_train是特征矩阵y_train是标签 # 先做标准化 scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) # 注意用训练集的参数 # 查看原始类别分布 print(原始训练集分布:, Counter(y_train)) # 动态计算k_neighbors n_minority min(Counter(y_train).values()) k_neighbors min(5, int(np.floor(np.sqrt(n_minority))) - 1) k_neighbors max(k_neighbors, 1) # 至少为1 # 特征级SMOTE smote SMOTE( sampling_strategy0.5, # 少数类过采样到多数类的50% k_neighborsk_neighbors, random_state42 ) X_train_resampled, y_train_resampled smote.fit_resample( X_train_scaled, y_train ) print(过采样后训练集分布:, Counter(y_train_resampled))这里有几个关键点第一标准化必须在SMOTE之前做。因为SMOTE是基于距离的算法不标准化的话量纲大的特征会主导k近邻的选择。第二k_neighbors的动态计算很重要。我试过固定k5在少数类样本只有15个的时候合成样本明显偏离真实分布。改成动态计算后合成样本的质量好了很多。第三sampling_strategy0.5是我反复试验后的选择。我试过0.3、0.5、0.7、1.0最终0.5在召回率和精确率之间取得了最好的平衡。4.3 DSMHSA模型搭建与训练DSMHSA的PyTorch实现核心部分import torch import torch.nn as nn class DSMHSA(nn.Module): def __init__(self, input_dim, num_classes, d_model64, nhead4): super().__init__() # 局部流一维卷积 self.local_stream nn.Sequential( nn.Conv1d(1, 16, kernel_size3, padding1), nn.ReLU(), nn.Conv1d(16, 32, kernel_size5, padding2), nn.ReLU(), nn.Conv1d(32, d_model, kernel_size7, padding3), nn.ReLU(), nn.AdaptiveAvgPool1d(1) ) # 全局流多头自注意力 self.global_stream nn.TransformerEncoder( nn.TransformerEncoderLayer( d_modelinput_dim, nheadnhead, dim_feedforward128, dropout0.1, batch_firstTrue ), num_layers2 ) self.global_proj nn.Linear(input_dim, d_model) # 门控融合 self.gate nn.Sequential( nn.Linear(d_model * 2, d_model), nn.Sigmoid() ) # 分类头 self.classifier nn.Sequential( nn.Linear(d_model, 32), nn.ReLU(), nn.Dropout(0.3), nn.Linear(32, num_classes) ) def forward(self, x): # x shape: (batch, input_dim) # 局部流 x_local x.unsqueeze(1) # (batch, 1, input_dim) h_local self.local_stream(x_local).squeeze(-1) # (batch, d_model) # 全局流 x_global x.unsqueeze(1) # (batch, 1, input_dim) h_global self.global_stream(x_global).squeeze(1) # (batch, input_dim) h_global self.global_proj(h_global) # (batch, d_model) # 门控融合 gate_input torch.cat([h_local, h_global], dim-1) g self.gate(gate_input) h_fused g * h_local (1 - g) * h_global # 分类 logits self.classifier(h_fused) return logits训练循环里我加了几个技巧学习率预热前10个epoch用线性预热从1e-5升到1e-3避免初期震荡。梯度裁剪max_norm1.0防止梯度爆炸。早停监控验证集的F1-scorepatience20。from sklearn.metrics import f1_score def train_model(model, train_loader, val_loader, epochs200): optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_maxepochs ) criterion FocalLoss(gamma2, alpha0.25) best_f1 0 patience_counter 0 for epoch in range(epochs): model.train() for X_batch, y_batch in train_loader: optimizer.zero_grad() logits model(X_batch) loss criterion(logits, y_batch) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() scheduler.step() # 验证 model.eval() val_preds, val_labels [], [] with torch.no_grad(): for X_batch, y_batch in val_loader: logits model(X_batch) preds torch.argmax(logits, dim-1) val_preds.extend(preds.cpu().numpy()) val_labels.extend(y_batch.cpu().numpy()) val_f1 f1_score(val_labels, val_preds, averagemacro) if val_f1 best_f1: best_f1 val_f1 patience_counter 0 torch.save(model.state_dict(), best_model.pth) else: patience_counter 1 if patience_counter 20: print(fEarly stopping at epoch {epoch}) break return best_f14.4 评估结果与对比分析我在测试集上对比了三种方案方案准确率少数类召回率宏F1无SMOTE DSMHSA98.2%42.3%0.61原始信号SMOTE DSMHSA95.1%58.7%0.72特征级SMOTE DSMHSA97.5%76.4%0.85特征级SMOTE的优势很明显相比无SMOTE少数类召回率提升了34个百分点宏F1提升了0.24相比原始信号SMOTE准确率高了2.4个百分点召回率高了近18个百分点。这个结果验证了特征级SMOTE的核心逻辑在物理可解释的特征空间做插值比在原始信号上瞎合成要靠谱得多。5. 常见问题与排查技巧实录5.1 SMOTE之后模型反而变差了怎么办这是最常见的问题。我遇到过好几次SMOTE之后少数类召回率确实上去了但精确率暴跌导致整体F1反而下降。排查思路如下第一步检查特征质量。如果特征本身区分度低SMOTE生成的合成样本会落在健康类和故障类的边界上导致模型学到一个模糊的决策边界。解决办法是回到特征提取环节增加区分度高的特征或者用特征选择方法如互信息、递归特征消除筛掉噪声特征。第二步调整sampling_strategy。过采样倍率太高会引入太多合成样本模型过拟合到合成数据上。我建议从0.3开始试逐步增加观察验证集F1的变化。第三步检查k_neighbors。k太小会导致合成样本过于集中k太大会导致合成样本过于分散。用我前面给的动态公式重新算一下。第四步考虑SMOTE的变体。标准SMOTE对所有少数类样本一视同仁但有些样本可能在边界上有些在中心。Borderline-SMOTE只对边界样本做合成ADASYN根据样本密度自适应调整合成数量。我在另一个项目里用Borderline-SMOTE效果比标准SMOTE好了约5个百分点。5.2 DSMHSA训练不收敛的排查DSMHSA的双流结构比普通CNN复杂训练时容易遇到不收敛的问题。我踩过的坑包括学习率太大1e-3对某些数据集可能太大试试1e-4或5e-4。注意力头数太多特征维度只有10维的时候用8个头反而效果差。我建议头数不超过4。门控融合初始化门控网络的初始输出如果接近0或1会导致一路流被完全忽略。我通常把门控最后一层的偏置初始化为0让初始门控值接近0.5。Batch size太小小于32的时候BatchNorm统计量不稳定。要么增大batch size要么改用LayerNorm。5.3 常见问题速查表问题现象可能原因排查方法解决方案少数类召回率低类别不平衡严重查看类别分布增大sampling_strategy精确率暴跌合成样本质量差可视化特征空间分布调整k_neighbors或改用Borderline-SMOTE训练loss震荡学习率太大打印每epoch的loss降低学习率或加预热验证集F1不升过拟合对比训练和验证loss加Dropout或早停推理速度慢模型太大统计参数量减少注意力层数或卷积通道数独家避坑技巧在SMOTE之前先对少数类样本做一次聚类分析。如果少数类样本本身分成好几个子簇标准SMOTE会在不同簇之间插值生成不合理的样本。这时候应该对每个子簇分别做SMOTE或者用Cluster-SMOTE。6. 特征空间可视化与效果验证6.1 用t-SNE看SMOTE前后的特征分布光看数字不够直观我习惯用t-SNE把特征空间降到二维看一眼。SMOTE之前少数类样本在特征空间里是几个孤立的小点簇和多数类之间有明显的空白区域。SMOTE之后少数类样本的覆盖范围明显扩大和多数类的边界变得更清晰。这个可视化有个实用技巧用不同颜色标注真实少数类样本和合成少数类样本。如果合成样本大量落在多数类区域里说明SMOTE参数有问题需要调整。from sklearn.manifold import TSNE import matplotlib.pyplot as plt # 对过采样后的训练集做t-SNE tsne TSNE(n_components2, random_state42, perplexity30) X_tsne tsne.fit_transform(X_train_resampled) # 区分真实样本和合成样本 n_original len(X_train_scaled) plt.figure(figsize(10, 8)) plt.scatter(X_tsne[:n_original, 0], X_tsne[:n_original, 1], cy_train, cmaptab10, markero, alpha0.6, label真实样本) plt.scatter(X_tsne[n_original:, 0], X_tsne[n_original:, 1], cy_train_resampled[n_original:], cmaptab10, markerx, alpha0.8, label合成样本) plt.legend() plt.title(特征级SMOTE前后的t-SNE可视化) plt.show()6.2 消融实验每个组件的贡献为了搞清楚每个组件到底贡献了多少我做了消融实验配置准确率少数类召回率宏F1完整方案97.5%76.4%0.85去掉SMOTE98.2%42.3%0.61去掉门控融合96.8%71.2%0.80去掉全局流96.1%68.5%0.77去掉局部流95.4%65.3%0.74从消融结果看SMOTE对少数类召回率的贡献最大34.1%门控融合次之5.2%全局流和局部流各有贡献。这个结果符合预期SMOTE解决了数据层面的问题DSMHSA解决了模型层面的问题两者缺一不可。6.3 不同故障类型的诊断效果差异我还按故障类型拆分了结果故障类型召回率精确率F1内圈故障82.1%79.5%0.81外圈故障78.6%81.2%0.80滚动体故障68.4%72.3%0.70滚动体故障的召回率明显偏低。原因不难理解滚动体故障的特征频率调制现象更复杂特征提取时容易被其他频率成分掩盖。针对这个问题我后续增加了包络谱分析特征滚动体故障的召回率提升到了74%左右。7. 部署落地时的工程化考量7.1 模型轻量化与推理加速PHM系统往往要求在线实时诊断模型推理速度很关键。DSMHSA的原始版本在CPU上单样本推理约15ms对于大多数旋转机械监测场景够用了。但如果要部署到嵌入式设备上还需要进一步压缩。我试过两种方案知识蒸馏用完整DSMHSA作为教师模型训练一个单流的小模型作为学生。学生模型参数量减少60%推理速度提升2倍F1只下降约2个百分点。量化把FP32量化到INT8模型体积缩小75%推理速度提升约1.8倍。量化后的F1下降不到1个百分点完全可以接受。7.2 在线学习与模型更新装备的健康状态会随着运行时间变化模型需要定期更新。我的做法是每周用新采集的数据做一次增量训练只更新分类头冻结特征提取层。每月做一次全量训练用累积的历史数据重新训练整个模型。每次更新后在保留的验证集上评估如果F1下降超过5个百分点回滚到上一版本。注意增量训练时新数据的类别分布可能和训练集不同。如果新数据里故障样本突然增多不要直接用来更新模型先做一次分布检验。7.3 与现有PHM系统的集成DSMHSA模型的输出是四类故障的概率分布。在实际系统中我不会只依赖模型的单次判断而是结合时序逻辑做最终决策。比如连续5个窗口中有3个以上判为故障才触发报警。这样可以有效降低误报率。集成方式上我把模型封装成REST APIPHM系统通过HTTP调用。输入是特征向量JSON格式输出是各类概率。这种解耦设计方便后续替换模型不影响上层系统。8. 一些实操后的个人体会这个项目做下来最大的体会是特征级SMOTE不是银弹它只是把“数据不平衡”这个问题从原始空间转移到了特征空间。如果特征空间本身质量不高SMOTE只会放大问题。另一个体会是关于DSMHSA的门控融合。我一开始觉得门控融合是锦上添花但消融实验显示它贡献了5个百分点的F1提升。后来分析发现门控机制让模型在不同故障类型上自适应地选择局部或全局特征这对多类故障诊断特别有用。最后分享一个小技巧在SMOTE之后不要急着训练模型先做一个简单的可视化检查。用PCA或t-SNE把过采样后的特征空间画出来看看合成样本是不是落在合理的位置。这个步骤花不了几分钟但能帮你避免很多后续的调试时间。这个方案后续还可以这样扩展把特征级SMOTE和主动学习结合起来。先用SMOTE生成一批合成样本让模型标注哪些样本最有信息量然后只保留这些高信息量的合成样本。这样可以在不增加太多合成样本的情况下进一步提升模型性能。
返回列表