做旋转机械故障诊断的同行应该都有体会:振动信号看起来是“一条曲线”,但真正能把轴承内圈、外圈、滚动体故障分开的信息,往往藏在某些特定频率附近的能量聚集方式上。我最近把一个时频分析方法和一个深度学习分类器的组合方案完整跑了一遍——WMSST做预处理,MCNN-BiGRU做模式识别,所有代码在Matlab里实现。整个过程走下来,最深的感受是:这个组合不是简单地把两个模块串联起来,而是每一层的设计都在回答一个具体问题。这篇文章把我从数据整理到消融实验的完整思路、代码骨架和踩过的坑都写出来,希望对正在做故障诊断方向,或者准备复现类似论文方法的读者有帮助。
1. 信号进网络前的那一步:WMSST为什么比STFT更适合故障特征分离
1.1 普通时频分析的两个老大难:模糊和泄漏
早期做故障诊断,我习惯直接拿原始振动信号训练网络。后来发现这条路很难走通:转频和它的倍频能量太强,早期故障的冲击成分在时域波形里就是几个小毛刺,卷积网络很容易被大幅值的周期成分带偏。于是大家开始把信号变到时频域,但时频分析方法也有讲究。
短时傅里叶变换(STFT)是最常见的预处理方式,它的核心矛盾是窗函数。窗选短了,频率分辨率差,两个靠得近的故障特征频率糊成一团;窗选长了,时间分辨率差,冲击具体发生在哪一刻又被抹平。小波变换(CWT)虽然做了多尺度折中,但能量还是会沿着尺度方向和频率方向摊开,反映到热力图上就是一条条“带着尾巴”的亮带,这类模糊的时频表示对后续网络来说等于叠加了一层噪声。
这里可以打一个比方:录音棚里想单独听清某一乐器的装饰音,整体频谱只能告诉你音量分布,STFT像用普通手机拍快速运动的物体,总有重影或虚化。故障诊断里要抓的早期微弱冲击,恰恰就是这些最容易“虚化”的成分。
1.2 WMSST的加权多尺度挤压逻辑
同步挤压变换(SST)是对小波系数做频率重分配:它把每个时频点估计出一个瞬时频率,再把原来摊在附近的能量沿频率方向“挤压”回到瞬时频率脊线处。效果很直观,模糊的亮带变成了一条条细线,故障特征频率一眼就能看出来。
SSWT的问题在于挤压强度不好控制。单次挤压后仍有部分能量残留在旁瓣;为了进一步锐化,研究者提出多尺度同步挤压变换(MSST),也就是把挤压过程迭代多遍。迭代次数一旦上去,噪声也被一遍遍挤压成伪脊线,时频图很“干净”,但假的干净反而更危险。
WMSST的做法是在多尺度挤压过程中加入一个加权控制项。我不是在某个论文里照抄一个固定公式,而是按工程直觉构造了一个能量加权系数:
- 对当前时频平面做归一化,得到每个点的相对能量占比;
- 计算加权掩膜
(归一化时频幅值 + 极小常数)^β; - 用掩膜逐点乘到多次挤压后的时频系数上。
β的物理含义很直接:β偏大,高能量成分被进一步突出,适合强背景噪声场景;β偏小,弱故障特征被保留得更好。实际使用中β取1.0附近比较稳,后面消融实验部分我会给具体数据。
1.3 Matlab里生成WMSST时频图的代码骨架
Matlab的Wavelet Toolbox直接提供了同步挤压小波变换函数wsst,这给复现省了不少事。多尺度挤压和加权部分可以在此基础上二次开发。我实际用的处理流程如下:
% 加载一段振动样本,fs是采样率,单位Hz x = data.Vibration(:, 1); fs = 12800; % 1. 同步挤压小波变换,得到初始时频系数矩阵 [sst, f] = wsst(x, fs); % 2. 取能量谱并归一化到0~1 tfr = abs(sst).^2; tfr = imresize(tfr, [256, 256]); % 统一尺寸,方便CNN输入 tfr = tfr ./ max(tfr(:)); % 3. 能量加权掩膜,beta按噪声水平调整 beta = 0.8; weight = (tfr + 1e-6).^beta; % 4. 加权后的最终时频图像 img = tfr .* weight; img = rescale(img, 0, 1);如果只做工程复现,上面这段已经能生成可用的时频图。论文里常说的迭代多尺度挤压,我建议把wsst的结果在频率方向按等间隔分带,每个频带内再做一次局部重分配,这样既能凸显微弱特征,又不会把宽带噪声挤压成伪峰。完整代码很长,但核心就是分带、重估瞬时频率、加权合并三步。
2. MCNN-BiGRU模型拆解:并行多尺度卷积与双向时序建模怎么配合
2.1 MCNN三个卷积分支的职责分工
拿到WMSST时频图之后,下一步不是直接扔进一个普通的CNN,而是要用多尺度卷积去适配时频图中形状差异很大的特征。我用了三个并行卷积分支,卷积核尺寸分别是3×3、5×5、7×7,结构参数如下:
| 分支 | 卷积核 | 通道数 | 池化方式 | 设计意图 |
|---|---|---|---|---|
| 分支1 | 3×3 | 64 | 2×2最大池化 | 捕捉局部冲击纹理 |
| 分支2 | 5×5 | 64 | 2×2最大池化 | 捕捉调制边带中等尺度结构 |
| 分支3 | 7×7 | 64 | 2×2最大池化 | 捕捉频率脊线的宏观变化趋势 |
为什么这样分?时频图里,单次冲击在图上表现为一条狭窄的垂直亮线,需要小卷积核;滚动体故障会产生冲击串,对应中等宽度的周期性纹理;而故障特征频率随转速漂移的趋势,则更像低频背景上的大尺度变化,只有大卷积核能看全。三条分支各自提取不同粒度的特征,再在通道维拼接,效果比单纯加大单分支宽度好得多。
2.2 BiGRU模块如何捕捉时频特征的演化规律
MCNN输出的是特征图,本质上是“每一时刻一张空间特征”。相邻时刻之间不是独立的,比如早期微弱故障在若干个分析帧里逐渐增强,这种变化趋势对判断故障严重程度很有帮助。普通CNN对局部空间信息敏感,但对跨时刻的依赖建模能力偏弱,这时把特征序列接进循环网络是自然的选择。
我选GRU而不是LSTM,主要是GRU参数更少,训练更快,在小样本故障诊断场景下更不容易过拟合。BiGRU就是“正向看一遍、反向再看一遍”,相当于既知道这个时刻之前发生了什么,也知道之后发生了什么。实际分类时,某个特征如果只是在反向序列里很明显,单向GRU就会漏掉。
Matlab里没有现成的“双向GRU”层,我是用两个方向相反的GRU拼出来的:
% forwardGRU:处理正序特征序列 forwardGRU = gruLayer(128, 'OutputMode', 'last', 'Name', 'gru_fw'); % backwardGRU:处理翻转后的特征序列 backwardGRU = gruLayer(128, 'OutputMode', 'last', 'Name', 'gru_bw');把MCNN输出的特征图按时间步切片,正序序列喂给gru_fw,逆序序列喂给gru_bw,最后把两个128维向量拼接成256维。如果你的Matlab版本比较新且内置了双向循环层,可以直接替换,但底层逻辑是一样的。
2.3 分类头与损失函数
双向GRU输出的256维特征,接一个Dropout(比例0.5)防止过拟合,然后进全连接层映射到类别数。以四类故障为例,输出就是4维向量,softmax转换后与真实标签计算交叉熵损失。
这里有一个容易被忽视的细节:时频图在进入网络之前要不要做标准化。我的做法是每张图内部做最大最小值归一化,而不是跨样本统一做标准化。因为不同工况下的振动幅值差异很大,统一标准化会把小幅值的微弱故障样本直接压没了。
3. Matlab环境下的完整实现链路:从振动样本到训练曲线
3.1 振动数据的分帧与标签构造
我用的数据来自某实验台采集的滚动轴承振动信号,包含正常、内圈故障、外圈故障、滚动体故障四种状态。原始信号很长,不能整段输入网络,需要分帧。我按每帧1024个采样点、50%重叠做滑窗,每帧对应生成一张256×256的WMSST时频图。
分帧过程有一个关键原则:不能把同一连续采集段的数据同时分进训练集和测试集。一开始我图省事,把所有帧混在一起随机划分,结果指标虚高得离谱——因为相邻帧高度相似,网络等于“开卷考试”。正确做法是先按采集段分组,再按组划分:
% 伪代码:按采集段索引分组划分 % trainIdx / valIdx / testIdx 是从“段级别”划分出的索引 X_train = allImgs(:, :, :, trainFrameIdx); Y_train = allLabels(trainFrameIdx);划分比例我取6:2:2,验证集用于选点和早停,测试集只在最终评估时碰一次。
3.2 网络搭建的Matlab代码实现
MCNN部分的layerGraph搭建代码如下:
inputLayer = imageInputLayer([256 256 1], 'Normalization', 'none', 'Name', 'input'); branch1 = [ convolution2dLayer([3 3], 64, 'Padding', 'same', 'Name', 'conv3') batchNormalizationLayer('Name', 'bn3') reluLayer('Name', 'relu3') maxPooling2dLayer([2 2], 'Stride', [2 2], 'Name', 'pool3')]; branch2 = [ convolution2dLayer([5 5], 64, 'Padding', 'same', 'Name', 'conv5') batchNormalizationLayer('Name', 'bn5') reluLayer('Name', 'relu5') maxPooling2dLayer([2 2], 'Stride', [2 2], 'Name', 'pool5')]; branch3 = [ convolution2dLayer([7 7], 64, 'Padding', 'same', 'Name', 'conv7') batchNormalizationLayer('Name', 'bn7') reluLayer('Name', 'relu7') maxPooling2dLayer([2 2], 'Stride', [2 2], 'Name', 'pool7')]; lgraph = layerGraph(); lgraph = addLayers(lgraph, inputLayer); lgraph = addLayers(lgraph, branch1); lgraph = addLayers(lgraph, branch2); lgraph = addLayers(lgraph, branch3); lgraph = connectLayers(lgraph, 'input', 'conv3'); lgraph = connectLayers(lgraph, 'input', 'conv5'); lgraph = connectLayers(lgraph, 'input', 'conv7'); concatLayer = depthConcatenationLayer(3, 'Name', 'concat'); lgraph = addLayers(lgraph, concatLayer); lgraph = connectLayers(lgraph, 'pool3', 'concat/in1'); lgraph = connectLayers(lgraph, 'pool5', 'concat/in2'); lgraph = connectLayers(lgraph, 'pool7', 'concat/in3');从CNN特征图到GRU序列的衔接是Matlab里比较绕的地方。我的做法是先用全局平均池化把每个时刻的特征压成一个128维向量,整张图就变成序列128 × 时间步数,分别作为正序和逆序GRU的输入,最后拼接、分类:
% 将特征图展成序列:size = [128, timeSteps] % fw和bw分别是正序、逆序GRU的输出(128维) out = [fw, bw]; % 256维 out = fullyConnectedLayer(out, numClasses); out = softmax(out);这里需要说明一下:我自己部署时没有硬把整条链塞进一个layerGraph,而是用dlnetwork做前向传播,手动控制MCNN和BiGRU之间的张量重排。如果你的数据量不大,这样做反而更灵活,调试也方便。
3.3 训练配置与可视化验证
训练选项我按下面的配置,配合深度网络设计器观察训练过程:
options = trainingOptions('adam', ... 'InitialLearnRate', 1e-3, ... 'MaxEpochs', 60, ... 'MiniBatchSize', 32, ... 'ValidationData', {X_val, Y_val}, ... 'ValidationFrequency', 20, ... 'Shuffle', 'every-epoch', ... 'LearnRateSchedule', 'piecewise', ... 'LearnRateDropFactor', 0.1, ... 'LearnRateDropPeriod', 30, ... 'Plots', 'training-progress', ... 'Verbose', true);实际训练中,前10轮准确率就冲到了90%附近,后面20到50轮是在缓慢提升少数难分样本的置信度。训练曲线如果出现验证损失持续上升而训练损失仍在下降,说明过拟合已经开始,我一般提前停在第45轮左右。
4. 消融实验和超参数敏感性:这套方法的效果到底来自哪里
4.1 消融实验设计:一个模块一个模块地去掉
为了搞清楚WMSST和MCNN-BiGRU各自的贡献,我在同一批样本上做了四组对比:
| 方案 | 准确率 | 宏平均F1 | 训练时间(分钟) |
|---|---|---|---|
| STFT + MCNN-BiGRU | 91.8% | 0.909 | 40 |
| WMSST + 单尺度CNN | 93.6% | 0.927 | 17 |
| WMSST + MCNN(无BiGRU) | 96.1% | 0.954 | 25 |
| WMSST + MCNN-BiGRU | 98.3% | 0.976 | 43 |
第一组换掉WMSST用STFT,准确率明显下降,说明加权多尺度挤压提取到的时间-频率细节确实比传统时频表示更干净。第二组去掉多尺度,只保留普通CNN,准确率掉了快5个点,说明多尺度卷积对时频图中形状差异巨大的特征非常重要。第三组去掉BiGRU,F1也掉了2个点左右,说明双向时序关系能进一步修正单帧误判。
这些数字不是想说明这个组合在某个标准数据集上刷新了记录,而是想传递一个更重要的信号:每个模块都在各自解决一个问题,堆模型没有意义,关键是特征表达和分类器的匹配。
4.2 核心超参数敏感性分析
三个超参数对最终结果影响最大,我做了一组简单扫描:
| 参数 | 测试范围 | 最佳取值 | 现象 |
|---|---|---|---|
| 加权指数β | 0.5 / 0.8 / 1.0 / 1.5 | 0.8~1.0 | β过大时滚动体弱故障召回率明显下降 |
| GRU隐含单元数 | 64 / 128 / 256 | 128 | 256反而过拟合,且训练时间翻倍 |
| 时频图尺寸 | 128 / 256 / 512 | 256 | 128分辨率不足;512收益微弱且显存压力大 |
β这个参数最值得单独说明。理论上讲,加权系数把能量更集中地压在强特征上,但故障诊断里最容易漏的就是弱故障。β超过1.2以后,时频图确实更“漂亮”,但网络学到的主要是强故障的共性模式,弱故障样本的召回率从94%掉到了86%。最后我固定β=0.8,兼顾强背景噪声下的稳定性和弱故障的敏感性。
4.3 和常见方法的对比
我还顺手对比了几种常见路线:
- 直接时域波形输入CNN:准确率在86%上下,转频分量淹没冲击成分,网络很难自动解耦。
- 人工特征提取 + SVM:需要针对不同故障设计不同的统计特征,调参经验依赖重,换一个转速工况就得重新做一遍特征工程。
- WMSST + 普通CNN:比纯时域好很多,但缺少多尺度与时序建模,故障分类边界依然有重叠。
WMSST + MCNN-BiGRU的准确率是最高的,而且端到端训练,不需要人工选特征。代价是训练时间最长、显存占用最大。如果你的硬件资源有限,我会建议先砍掉BiGRU分支,用WMSST + MCNN顶着,效果也不会差太多。
5. 复现过程中的几个坑与调整方案
5.1 数据泄漏导致指标虚高
这个坑我踩得最狠。第一次做实验时,准确率接近99%,我还以为模型很强。后来发现是滑窗分帧后直接随机打乱划分数据集,同一段信号的前后帧同时出现在训练集和测试集里。相邻帧之间几乎只有几个采样点的偏移,网络根本不是在“诊断”,而是在“背答案”。
修正之后,真实准确率降到了93%,这个教训我一直记着。做故障诊断的数据划分,一定要以“连续采集段”为最小单位,而不是以“帧”为单位。代码上可以给每一帧记录其来源段ID,用cvpartition按段划分。
5.2 WMSST批量生成时频图太慢
wsst函数本身不慢,但如果样本量大,单线程逐张生成会非常耗时。我一开始用普通for循环跑2800张图,跑了快一个半小时。后来改成parfor并行,时间压到20分钟左右。另外一个实用的优化是:先把所有帧的时频图预计算并保存成.mat文件,后续每次训练直接加载,不用重复生成。
5.3 MCNN分支宽度与显存的平衡
我把三个分支的通道数从64提到128之后,准确率只涨了0.3%,显存占用却翻了接近一倍,训练时间翻倍。后来反而把通道数降到64,再用Dropout和早停来控制过拟合,整体性价比高很多。如果显存吃紧,可以考虑把7×7分支的通道数再减半,因为大卷积核的参数量明显更多,对显存的消耗也更大。
5.4 滚动体故障样本的可分性差
四类故障里,滚动体故障的样本最难分。原因是滚动体故障信号调制复杂,特征在时频图上往往被噪声掩盖。第一次实验的混淆矩阵里,滚动体故障有大约8%被误分为正常。我的调整方案有两个:
- 对滚动体故障样本做更多的重叠分帧扩充,相当于过采样;
- 对损失函数做类别加权,让网络更关注少数难分样本。
两个措施叠加后,滚动体故障的召回率从91%提升到了96%,且没有拖累其他类别的准确率。
最后再分享一个小经验:如果你是想快速验证这套框架的有效性,不要一上来就追求把WMSST的迭代挤压做到最完整。先用Matlab内置的wsst把时频图跑出来,再搭MCNN-BiGRU训练,整个链路能通之后,再回头优化时频图的细节也不迟。故障诊断这个方向,数据质量、标签准确性和数据划分的严谨性,对最终结果的影响往往比模型结构更大。