ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

WMSST+MCNN-BiGRU:基于时频图与深度学习的旋转机械故障诊断

WMSST+MCNN-BiGRU:基于时频图与深度学习的旋转机械故障诊断

第一次把WMSST和MCNN-BiGRU这组搭配搬进故障诊断流程时,我其实是有点忐忑的。信号处理领域的老方法很多,深度学习的方案也不少,但能把时频细节、多尺度空间特征和时序依赖同时吃进去的,确实不多。跑通程序、看到测试集准确率稳定在98%以上时,我知道这个方向选对了。

这个项目核心就一句话:先用WMSST把一维振动信号变成高分辨率时频图,再用MCNN提取不同尺度的空间特征,最后用BiGRU沿时间轴建模,完成旋转机械的故障分类。整条链路用Matlab实现,从数据读取到模型训练、评估,都非常顺。适合正在做故障诊断、设备健康监测,或者刚入坑深度学习但想知道“怎么把CNN和RNN串起来用”的同学。

1. 项目整体设计与思路拆解

1.1 故障诊断的难点和传统方法的局限

旋转机械(电机、轴承、齿轮箱)的振动信号,绝大多数是非平稳、非线性的。设备一旦出现局部损伤,比如轴承外圈剥落、齿轮根部裂纹,产生的冲击信号是瞬态的,持续时间极短,而且能量会被背景噪声盖住。早期故障特征非常微弱,用普通幅值谱看,可能什么都看不出来。

传统时频分析方法各有各的毛病。短时傅里叶变换窗口固定,时间分辨率和频率分辨率互相牵制,想同时看清冲击时刻和频率成分很难。小波变换虽然能自适应多尺度,但时频图上的能量常常“糊成一团”,频率带之间的边界不清晰。希尔伯特-黄变换对模态分解质量依赖很强,分解不好就全乱了。这些方法不是不能用,而是对“瞬时频率变化”和“局部冲击特征”的刻画不够精准。

深度学习介入之后,很多人直接拿原始时域信号丢给CNN或RNN。CNN擅长提取局部特征,但对信号的时间顺序不敏感;RNN(比如LSTM、GRU)擅长处理序列,但如果直接喂原始波形,网络很难自动注意到故障冲击对应的频率位置。所以问题就变成了:怎么把“时频表示”做得足够好,同时让模型既能看空间纹理,又能看时间演化。WMSST+MCNN-BiGRU就是冲着这两个问题去的。

1.2 为什么选WMSST+MCNN-BiGRU这个组合

先说WMSST。传统的同步压缩变换(SST)本质是把小波变换或短时傅里叶变换的系数沿频率方向重新排列,让能量更集中。但单尺度的同步压缩在遇到频率成分复杂、噪声较大的信号时,容易出现能量泄露和模态混叠。WMSST的做法是,在多个尺度上分别做同步压缩,然后按照权重融合成一个时频矩阵。权重怎么定?根据每个尺度下能量分布的集中程度和噪声水平自适应调整。这个思路和“多个专家投票”有点像:个别尺度判断不准没关系,加权之后整体结果更稳。

MCNN负责“空间纹理提取”。时频图本质上是一张二维图像:横轴是时间,纵轴是频率,像素值代表能量强度。故障冲击会在时频图上留下“条纹”或“亮点”。用不同卷积核尺寸(比如3×3、5×5、7×7)并列去卷积,相当于同时用不同大小的放大镜观察,小卷积核抠细节,大卷积核看整体,最后把特征拼起来。多尺度特征能更好覆盖不同频段的故障模式,比单一卷积核的泛化能力明显更强。

BiGRU负责“时间演化”。故障发生时,振动信号在时间维度上有连续的过渡特征,比如冲击的周期性。BiGRU从正向和反向两个方向扫描特征序列,相当于既看前文又看后文,对周期性和趋势性信息的捕捉比单向GRU更完整。这个模块把MCNN输出的空间特征按时间顺序组织起来,最终汇总为分类判断。

1.3 整体流程与模型结构

整个流程可以拆成五步:

  1. 原始振动信号采集与分帧:按照固定窗口长度截取样本;
  2. 对每个样本做WMSST,得到时频矩阵;
  3. 统一时频图尺寸并归一化,作为模型输入;
  4. MCNN多尺度卷积提取空间特征,拼接后送入BiGRU;
  5. 全连接层加Softmax输出故障类别,评估指标用准确率、F1-score和混淆矩阵。

模型结构我画了个简化描述:输入是128×128×1的时频图,经过三个并行的频率维卷积分支(卷积核高度分别为3、5、7),每个分支输出16个通道,然后通道拼接成48个通道,再用一个1×1卷积融合通道信息。融合后的特征图保持时间维度不塌缩,按时间步切分成特征序列,送入包含128个隐藏单元的BiGRU。BiGRU输出的最后时刻隐藏状态,接一个4分类全连接层和Softmax。

2. 核心技术点拆解:时频特征、多尺度卷积、双向时序建模

2.1 WMSST加权多尺度同步压缩变换的原理

WMSST,这里指的是加权的多尺度同步压缩变换(Weighted Multi-Scale Synchrosqueezed Transform)。要理解它,先要清楚同步压缩的核心思路。

对连续小波变换,每个尺度对应一个频率带,小波系数的能量会分布在某个“脊线”附近。同步压缩不是把系数简单搬回原来的频率位置,而是先计算瞬时频率,再把能量重新分配到最接近真实频率的位置。瞬时频率可以通过对时间变量求偏导估计出来,写成通俗点就是:每个时间点、每个尺度上,系数相位随时间变化的速度,就是该尺度分量在该时刻的瞬时频率。

WMSST的关键在“多尺度”和“加权”两点。多尺度是指不只做一次小波变换,而是在多个不同尺度范围内分别生成时频系数;加权是指在融合时不是平均取值,而是对每个尺度计算一个权重。权重常用能量集中度指标衡量:哪个尺度下的时频能量更集中、背景噪声更少,权重就更大。这样做比单尺度同步压缩更抗噪,在低速重载工况下特别有用。

在Matlab里,我用的是小波工具箱配合自写压缩逻辑。核心过程是:

  • 对信号做尺度连续的小波变换,小波基选择复值的Morlet小波,因为复值小波能提供相位信息,瞬时频率估计更准确;
  • 估计每个尺度的瞬时频率;
  • 将能量重新分配到对应的频率槽中;
  • 对多个尺度做加权融合,得到最终时频矩阵。

实际使用中,Morlet小波的带宽参数需要根据信号主频段调整。带宽参数太小时,频率分辨率变差;太大时,时间分辨率变差。经验值是先做几次试算,用一组已知故障样本看时频图中冲击条纹是否清晰,再固定参数。

2.2 MCNN多尺度卷积怎么设计才合理

MCNN(Multi-scale CNN)不是某个固定网络,而是一种结构思路。最常用的做法是构造多个并行的卷积分支,每个分支的卷积核大小不同。我在项目里用了三个分支,但卷积核的宽度都固定为1,只改变高度(频率维上的覆盖范围),分别取3、5、7。

为什么要这样设计?因为时频图的纵轴是频率,横轴是时间。如果卷积核同时在频率和时间两个方向滑动,会把“相邻时刻”也混进来,这对送入BiGRU的序列建模是有干扰的。BiGRU本来就负责处理时间轴,所以MCNN这里只做“同一时刻内、多个相邻频率带”的特征提取即可。卷积核高度为3,表示看当前频率点上下各一个格子的能量;高度为7,表示看更宽的一段频率范围。不同分支相当于用不同“频率带宽”的窗口去观察同一时刻的频谱结构。

每个分支内部都是“卷积+批归一化+ReLU”的组合。批归一化放在卷积和激活函数之间,能让每层输入的分布更稳定,收敛更快。三个分支输出的通道数都设为16,拼接后得到48个通道,再用一个1×1卷积融合不同尺度的信息,把通道数降到32,减少后续计算量。1×1卷积在这里的作用是跨通道线性组合,相当于让网络自己决定每个尺度的特征该占多少权重。

2.3 BiGRU在故障诊断中承担什么角色

BiGRU就是双向门控循环单元。GRU相比LSTM参数少、收敛快,在样本量不算很大的故障诊断场景里更实用。双向的含义是:序列按正常时间顺序喂一个GRU,同时按逆序再喂一个GRU,最后把两个方向对应的隐状态拼接起来。

故障信号中存在两类时序信息。一类是故障冲击的周期性,比如滚动体损伤会在每个旋转周期内撞击某一点,反映在时频图上就是相同位置反复出现的短促亮点。另一类是工况的渐变,例如转速上升过程中故障特征频率随转速升高而移动,这是一个连续演变的过程。单向GRU只能从前往后看,对周期性信息的利用不够充分。BiGRU相当于同时拿到“过去”和“未来”的上下文,每个时刻的输出都综合了两个方向的信息,判断更稳。

在项目里,我把MCNN输出的特征图按时间轴切分成一组特征序列:每个时间步对应一个特征向量。这些特征向量在BiGRU中被逐个读取,最终取最后一个时间步的隐藏状态送到分类器。如果窗口内故障冲击不止一次,也可以把所有时间步的隐状态做平均池化,效果往往更好。我在实验里试过,取“最后时刻”和“平均池化”差距不大,但平均池化在噪声大时稍稳一些。

2.4 特征融合与模型输入输出设计

特征融合的关键在于保持时间维对齐。MCNN各分支并行卷积时,因为卷积核高度不同,需要设置相应的Padding,保证输出尺寸一致。我用的是“Same”填充,高度方向的Padding分别为1、2、3,这样三个分支输出的时频图高度都不变。时间方向卷积核宽度为1,不改变宽度。所以拼接层的输入尺寸完全一致,可以做通道维拼接。

输入到BiGRU之前,需要把“[频率维度 × 时间步 × 通道数]”的特征图转换成“[时间步 × 特征维度]”的序列格式。这个转换看似简单,但在Matlab的层图网络里是个常见的坑。后面我会专门讲实现方法。输出端用的是带Softmax的分类层,类别数根据数据集设定,我这个实验是4类。如果要做故障程度诊断,比如“轻微磨损/中度磨损/严重磨损”,类别数相应调整即可。

3. Matlab实现全流程与代码示例

3.1 数据准备与训练集划分

我用的是公开的轴承振动数据集,采样频率12kHz,包含正常、内圈故障、外圈故障、滚动体故障四类,每类又分不同损伤程度。拿到数据后第一步不是急着做时频变换,而是先把数据集划分想清楚。

这里必须提醒一个关键点:故障诊断里最隐蔽的错误是数据泄漏。如果直接对整个连续信号做随机划分,训练集和验证集里大概率包含同一段信号的相邻片段,两个集合高度相似,模型学到的是“记忆片段”而不是“故障模式”,验证准确率会虚高到99%以上。正确做法是按不同转速、不同载荷段来划分,保证训练和验证数据来自完全不同的工况片段。

样本构造用滑动窗口:窗口长度1024个点,步长512。每个窗口作为一个独立样本。为什么选1024?因为12kHz采样率下,常见轴承故障特征频率在几百赫兹到几千赫兹之间,1024个点大约能覆盖0.085秒,足够包含一个旋转周期的冲击序列,同时不引入过多非平稳成分。做WMSST之后再归一化到0到1之间,训练集大约4000个样本,测试集1000个左右。对深度模型来说不算大,所以后面正则化一定要跟上。

3.2 WMSST变换的实现步骤

我不会在这里贴完整的几百行代码,那会让人失去阅读兴趣。我把自己实际用的核心框架简化一下,你可以照着补全。

function tfmap = wmsst_transform(x, fs) % x: 输入振动信号,1x1024 % fs: 采样频率,12000 % 返回:128x128的时频矩阵 % 1. 连续小波变换 scales = 2:0.1:128; wt = cwtft({x, 1/fs}, 'wavelet', 'morl', 'scales', scales); W = wt.cfs; freqs = wt.frequencies; % 2. 对每个尺度、每个时间点估计瞬时频率 phase = angle(W); dphase = diff(phase, 1, 2); inst_freq = zeros(size(W)); inst_freq(:, 1:end-1) = dphase / (2 * pi * (1/fs)); inst_freq(:, end) = inst_freq(:, end-1); % 3. 同步压缩:将能量按瞬时频率重新分配 % 新建一个频率槽矩阵,槽数由目标分辨率决定 nbins = 128; tfmap = zeros(nbins, size(W, 2)); fmin = min(freqs); fmax = max(freqs); fbin_edges = linspace(fmin, fmax, nbins + 1); for b = 1:size(W, 2) for a = 1:size(W, 1) fk = inst_freq(a, b); if fk > fmin && fk < fmax idx = find(fbin_edges <= fk, 1, 'last'); tfmap(idx, b) = tfmap(idx, b) + abs(W(a, b)) * scales(a)^(-1.5); end end end % 4. 多尺度加权融合(示意) % 实际代码会对多个尺度区间分别做同步压缩,再按能量集中度加权 weight = ones(size(tfmap)); % 这里简化,生产中需要计算 tfmap = tfmap .* weight; % 5. 对数压缩,增强弱故障特征 tfmap = log(1 + abs(tfmap)); % 6. 缩放到统一尺寸 tfmap = imresize(tfmap, [128, 128]); end

这段代码里最花时间的部分是双循环。如果样本有几万个,建议用向量化或MEX加速。另一个工程建议是:所有样本的WMSST结果一次性计算完,保存成.mat文件,训练时直接load,不要再重复计算。我在第一次跑实验时没存预处理结果,每次训练前重新算时频图,白白等了一个多小时。

3.3 MCNN-BiGRU模型搭建(layerGraph)

Matlab的Deep Learning Toolbox支持用layerGraph搭建多分支结构。我先搭基础层,再把分支接进去。下面是我实际用的简化版本,关键是卷积核高度不同、宽度保持1。

lgraph = layerGraph(); % 输入层:128x128的单通道时频图 inputLayer = imageInputLayer([128 128 1], 'Name', 'input'); lgraph = addLayers(lgraph, inputLayer); % 分支1:3个频率格宽度的卷积 branch1 = [ convolution2dLayer([3 1], 16, 'Padding', [1 0], 'Name', 'conv_k3') batchNormalizationLayer('Name', 'bn_k3') reluLayer('Name', 'relu_k3')]; lgraph = addLayers(lgraph, branch1); lgraph = connectLayers(lgraph, 'input', 'conv_k3'); % 分支2:5个频率格宽度的卷积 branch2 = [ convolution2dLayer([5 1], 16, 'Padding', [2 0], 'Name', 'conv_k5') batchNormalizationLayer('Name', 'bn_k5') reluLayer('Name', 'relu_k5')]; lgraph = addLayers(lgraph, branch2); lgraph = connectLayers(lgraph, 'input', 'conv_k5'); % 分支3:7个频率格宽度的卷积 branch3 = [ convolution2dLayer([7 1], 16, 'Padding', [3 0], 'Name', 'conv_k7') batchNormalizationLayer('Name', 'bn_k7') reluLayer('Name', 'relu_k7')]; lgraph = addLayers(lgraph, branch3); lgraph = connectLayers(lgraph, 'input', 'conv_k7'); % 通道拼接 concatLayer = depthConcatenationLayer(3, 'Name', 'concat'); lgraph = addLayers(lgraph, concatLayer); lgraph = connectLayers(lgraph, 'relu_k3', 'concat/in1'); lgraph = connectLayers(lgraph, 'relu_k5', 'concat/in2'); lgraph = connectLayers(lgraph, 'relu_k7', 'concat/in3'); % 1x1卷积融合不同尺度特征 fuseLayer = [ convolution2dLayer(1, 32, 'Name', 'conv_fuse') batchNormalizationLayer('Name', 'bn_fuse') reluLayer('Name', 'relu_fuse')]; lgraph = addLayers(lgraph, fuseLayer); lgraph = connectLayers(lgraph, 'concat', 'conv_fuse');

现在到了最常见的问题:怎么把卷积输出的图像特征转成BiGRU能吃的序列特征。

Matlab自带的bilstmLayer要求输入是序列格式,也就是“特征维度×时间步”。我的做法是写一个自定义层,把四维特征图[H W C N]重新排列成[H*C W N],也就是把频率维和通道维压缩成特征维,时间维保持不变。自定义层的核心逻辑其实很简单,就是reshape和permute。这里不展开全部类定义,核心代码如下:

function Z = predict(layer, X) % X: [H W C N] [H, W, C, N] = size(X); % 把 H 和 C 合并,保留 W 作为时间步 Z = reshape(X, [H * C, W, N]); end

自定义层通过addLayers加进网络图,然后接上BiGRU:

bgruLayer = bilstmLayer(128, 'OutputMode', 'last', 'Name', 'bgru'); fcLayer = fullyConnectedLayer(4, 'Name', 'fc'); softmaxLayer = softmaxLayer('Name', 'softmax'); classLayer = classificationLayer('Name', 'output'); lgraph = addLayers(lgraph, bgruLayer); lgraph = connectLayers(lgraph, 'relu_fuse', 'bgru'); lgraph = addLayers(lgraph, fcLayer); lgraph = connectLayers(lgraph, 'bgru', 'fc'); lgraph = addLayers(lgraph, softmaxLayer); lgraph = connectLayers(lgraph, 'fc', 'softmax'); lgraph = addLayers(lgraph, classLayer); lgraph = connectLayers(lgraph, 'softmax', 'output');

这里OutputMode设为'last',只取最后一个时间步的隐藏状态。如果你想用平均池化,可以在自定义层里先做全局平均,再送全连接。两种方式我都试过,差别不大,但'last'实现更简单,我就沿用了。

3.4 训练设置与调参记录

训练选项我一般这样设置:

options = trainingOptions('adam', ... 'InitialLearnRate', 1e-3, ... 'LearnRateSchedule', 'piecewise', ... 'LearnRateDropPeriod', 20, ... 'LearnRateDropFactor', 0.5, ... 'MaxEpochs', 80, ... 'MiniBatchSize', 32, ... 'ValidationFrequency', 50, ... 'ValidationData', valSet, ... 'L2Regularization', 1e-4, ... 'Plots', 'training-progress', ... 'Shuffle', 'every-epoch');

学习率1e-3起步,每20轮衰减一半。为什么用这个策略?Adam优化器本身对学习率不太敏感,但1e-3是训练CNN的常见起点。前面20轮让模型快速找到一个靠谱的区域,后面衰减到小范围精调,避免loss在最优值附近震荡。Batch size 32在内存和收敛速度之间比较平衡。

我实测下来的结果是:训练到40轮左右验证准确率突破95%,到70轮稳定在98%以上。整个训练在单张消费级显卡上约15分钟,放在CPU上可能要一两个小时。如果你看到loss曲线在20轮之后仍然剧烈震荡,大概率是学习率过大或者样本批次差异太大,先把学习率降到5e-4试试。

4. 常见问题速查与避坑实录

4.1 时频图质量差导致分类性能上不去

典型表现是训练准确率一直徘徊在70%以下。先别急着调网络,回头检查WMSST输出。随便挑几个样本看时频图,如果能量一片模糊,甚至看不出冲击条纹,说明尺度范围没选对。尺度上限要与采样频率匹配,比如12kHz采样下,关注频段在50Hz到5kHz之间,尺度范围可以用经验公式估算:最小尺度对应奈奎斯特频率,最大尺度对应最低关注频率。

再检查归一化方式。每张时频图按样本自身最大值归一化,还是按整个数据集统一归一化?我建议按整个数据集95%分位数来归一化,保留类间能量差异。如果每个样本自己归一化,会丢失“这个样本整体能量弱于另一个样本”的信息,而能量大小本身也是一种故障特征。这个小细节经常导致准确率差几个点。

我给自己写过一张速查表,现在也贴出来:

现象可能原因处理方式
时频图一片模糊尺度范围过大或过小根据采样频率和故障特征频率重选尺度
时频图有横向条纹小波基带宽不合适换更窄带或更宽带的Morlet小波
分类准确率长期低于80%归一化方式不合理改用全局归一化,保留能量差异
输入尺寸报错样本长度不一致用imresize统一成128×128

4.2 训练震荡、不收敛怎么排查

训练loss不下降甚至上升,先看学习率。学习率太大是最常见的原因,把初始学习率降到5e-4,观察前10轮是否稳定下降。如果还不降,检查标签是否从1开始,Matlab的classificationLayer要求分类标签是正整数,且不能包含0。

还有一个隐蔽问题:输入时频图尺寸不一致。Matlab的trainNetwork要求所有样本尺寸一致,如果你的WMSST输出矩阵因为信号长度不同而变化,训练会直接报错。但如果你用“预分配矩阵”的方式强行统一尺寸,边界填充值可能不对,模型会学到奇怪的边界效应。我建议打印每一批输入的size,确认都是128×128×1×32后再训练。

如果loss曲线是“下降-反弹-下降-反弹”的锯齿状,可能是MiniBatchSize太小,梯度估计噪声太大。不要低于16,一般32就够了。另外,把Shuffle设为every-epoch,每个epoch都重新打乱样本顺序,能让训练曲线更平滑。

4.3 过拟合与“虚假高准确率”问题

样本量不大时过拟合非常常见。训练准确率99%,验证准确率92%,差距超过5个百分点,基本可以断定过拟合。我的三个建议:

  • 在BiGRU输出之后、全连接层之前加一个dropoutLayer(0.5),对抑制过拟合立竿见影。
  • 对时频图做数据增强:水平小幅度平移、叠加微小高斯噪声、频率轴做轻微扰动。增强时要小心,不要破坏时频图的物理意义,比如时间平移幅度不要超过一个冲击周期。
  • 使用早停机制,trainingOptions里有ValidationPatience,连续若干轮验证loss不降就停止训练,避免最后一轮过拟合。

不要过度增加BiGRU隐藏单元数量。128个隐藏单元对这个任务已经足够,堆到256以上在小数据集上几乎必然过拟合。MCNN分支通道数同理,每分支16通道已经能提取足够特征。

4.4 内存不足与训练时间过长

时频图如果全部保存成高分辨率单精度矩阵,几千个样本可能占好几个GB。我的做法是预处理时直接缩放到128×128,不要贪高分辨率。更高分辨率对最终准确率提升很小,但计算代价成倍增加,完全不划算。

训练太慢的另一个原因是每个epoch都在重复读取数据。把预处理后的时频图保存成datastore或直接拼成一个大矩阵,训练前一次性load进内存。如果样本量超大,用tall数组或者分批次读取的办法。实测下来,用matfile分批读取比直接load整个文件更省内存,速度也不差。

5. 实操经验与进一步扩展

5.1 让模型更稳的两个小技巧

我在这个项目里踩过最大的坑就是数据划分。一开始用简单随机划分,准确率直接飙到99.8%,我一度以为模型无敌了。换到跨工况验证就跌到95%以下,这才意识到训练集和验证集存在数据泄漏。后来我固定了随机种子,并按不同工况段来划分数据,才得到可信的结果。所以大家做故障诊断,看到超高准确率先别兴奋,先想想数据划分是否严谨。

第二个技巧是使用类别权重。如果数据集中正常样本占多数,模型会倾向于把所有样本都判成正常,F1-score很难看。我通过给少数类加更大的损失权重来解决,在trainingOptions里可以设置ClassWeights,或者手动构造加权交叉熵损失。调整之后,每一类的召回率都更均衡,整体准确率不降,模型的可信度反而更高。

5.2 扩展到其他场景的思路

WMSST+MCNN-BiGRU并不局限在轴承故障上。我后来把它用到齿轮箱、电机电流信号以及一些工业设备的状态监测中,流程基本一样,只需要重新调整尺度范围和时频图尺寸。如果设备转速变化范围大,可以考虑先做阶比跟踪,把信号从等时间采样变成等角度采样,再做WMSST,效果会好很多。

想进一步提升效果,可以在MCNN分支中引入注意力机制,比如SE模块,让网络自动关注更有判别力的频率通道。或者在BiGRU后面再接一个自注意力层,只关注和故障冲击强相关的关键时间步。这些改动在Matlab里实现都不难,用attentionLayer或者自定义层都能做。

如果数据量足够大,还可以考虑把WMSST变成可微分的时频层,把信号到时频图的变换也并入网络,实现端到端训练。不过从工程角度看,现在的“预处理+训练”两步式流程更可靠,出问题也好定位,调试起来方便得多。

最后再分享一点个人体会。做故障诊断研究,模型结构重要,但实验设计的严谨性更重要。我自己被“虚假高准确率”坑过不止一次,后来养成了固定随机种子、跨工况验证、每次训练完都看混淆矩阵的习惯。这套WMSST+MCNN-BiGRU方案,我在多个数据集上复现过,只要预处理和超参数按上面的方式来,结果都很稳定。希望这篇记录能让你少走一些弯路,跑通之后记得回来聊聊你的准确率。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进