简介:一套基于连续小波变换(CWT)与卷积门控循环单元(CNN-GRU)融合的MATLAB故障诊断完整项目实例,面向具备一定信号处理和深度学习基础的研究生、工程师及科研人员,可用于旋转机械、风电机组、航空发动机等关键设备的智能状态监测与故障识别。资源以docx文档形式呈现,共1个文件,压缩包约132KB,内容涵盖CWT时频图构建、CNN空间特征提取、GRU时序建模、多类故障分类的完整算法流程与代码详解,并包含GUI交互界面设计、数据预处理、网络训练调优、模型评估与部署方案。文档目录结构清晰,从项目背景、模型架构到数据构建、代码示例和应用领域逐步展开,便于读者按章节实践。已有57人学习下载,适合希望快速搭建CWT-CNN-GRU诊断框架、开展多工况迁移实验并提升工程落地能力的读者。
1. 信号处理里的故障诊断,为什么值得用 CWT-CNN-GRU 组合
振动信号处理里的故障诊断任务,越来越多团队把连续小波变换(CWT)和卷积门控循环单元(CNN-GRU)串成一条流水线:CWT 把一维冲击信号展开成时频图,CNN 提取纹理,GRU 消化连续时频帧之间的时间依赖。这个思路在 MATLAB 里从数据准备到界面部署都能闭环,但真正卡人的不是网络本身,而是 CWT 参数怎么定、信号怎么切帧、GUI 怎么封装。
这篇实现按「CWT 定参 → CNN-GRU 组网 → GUI 封装」的顺序把完整程序拆开讲,代码可直接换数据集复现,适合做设备状态监测的工程师和信号处理方向的研究生。
2. 连续小波变换的参数选择:母小波、尺度与 CWT 系数预处理
信号处理里选时频工具,很多人的第一反应是短时傅里叶变换。窗口长度一定,低频段的频率分辨率和高频段的时间分辨率就互相打架。CWT 用一组可变宽度的基函数替代固定窗:低频配宽窗,频率分辨率高;高频配窄窗,时间定位准。对冲击类故障,这种自适应特性正是需要的。雷达信号处理里常用的时频分析思路,在机械故障诊断里同样适用。MATLAB 的cwt函数把变换压缩成一行调用,但默认参数离「能喂给 CNN-GRU」还差三步:选母小波、定尺度范围、把系数转成图像。
2.1 cwt 的两种调用方式,以及尺度到频率的换算
% 方式一:自动尺度,f 直接是伪频率(Hz) [wt, f] = cwt(signal, 'amor', fs); % 方式二:自定义尺度向量,便于复现和不同方法对比 scales = 2.^(1:0.1:12); % 对数均匀铺开的尺度 [wt, f] = cwt(signal, scales, 'morl', fs);方式一只传信号、母小波和采样率,尺度由函数内部按信号长度自动铺满,适合第一版跑通链路。方式二把尺度范围固定下来,同一个模型换信号时结果可复现,做对比实验也更规范。伪频率和尺度的关系是f = fc * fs / scales,其中 fc 是所选母小波的中心频率。要画尺度与频率的对照曲线,用scal2frq:
f_axis = scal2frq(scales, 'morl', fs); semilogy(f_axis, scales); % 横轴频率(Hz),纵轴尺度尺度范围决定时频图覆盖的频率区间。轴承诊断里,转频和故障特征频率通常集中在 0.5~5 kHz,scales 从 2^1 铺到 2^12 已经覆盖得比较宽;如果只关心齿轮啮合频率附近的边频带,把尺度上限调小,图像在频率方向的利用率会更高。另一个容易忽略的点是 COI(锥形影响区):CWT 系数在时间轴两端是不可信的,越靠边的点受边界截断影响越大,滑窗切帧时尽量让冲击事件落在帧的中部。
2.2 母小波选型:先跑通 amor,再按混淆矩阵换
| 母小波 | 特点 | 故障诊断适用场合 |
|---|---|---|
| 'amor' | 解析 Morlet,时间与频率分辨率均衡 | 默认起步,先把整条链路跑通 |
| 'cmor3-3' | 带宽和中心频率可调 | 两个故障特征频率靠得近,需要细调分辨率 |
| 'bump' | 频带窄,频率聚焦好 | 谐波和边频带密集,需要精细频率定位 |
| 'morse' | 参数自由度最大 | 大数据量下做最后的精度调优 |
常见做法是用 'amor' 做首发,它对多数轴承、齿轮箱数据不会出大问题。等模型训完,看混淆矩阵里哪两类故障互相分不开,再回头换母小波:两个特征频率靠得近,换 'cmor3-3' 并把带宽参数调小;谐波成分密集,换 'bump' 用窄带换更精细的频率定位。不建议第一轮就做母小波穷举,参数搜索放在模型定型之后,第 5 章会给出一个轻量筛选脚本。
2.3 CWT 系数到 CNN 输入:对数压缩、频带截取和伪彩色
function img = cwtToImage(signal, fs, waveletName, freqBand) % 输入一维信号,返回 224×224×3 的 RGB 图像 [wt, f] = cwt(signal, waveletName, fs); idx = f >= freqBand(1) & f <= freqBand(2); % 截取频带 wt = wt(idx, :); spec = 20 * log10(abs(wt) + eps); % 对数压缩,加 eps 防 log10(0) spec = mat2gray(spec); % 归一化到 [0,1] img = ind2rgb(imresize(spec, [224 224]), jet(256)); end这段做了三个关键动作。一是对数压缩:CWT 系数动态范围常有几十 dB,直接线性归一化会把弱故障的调制边带压成背景,取幅值的对数才能让低幅值细节显形。二是频带截取:转频和工频噪声对分类贡献小,截断之后图像有效信息密度更高,也变相降低了 CNN 要学的冗余模式。三是尺寸统一:224×224 可以接预训练模型,自己搭小网络也够用,imresize用双线性即可。伪彩色映射只是工程习惯,如果后面要迁移 ImageNet 预训练权重,更稳妥的做法是把灰度图repmat成三通道,保持前景统计特性一致。
3. MATLAB 组网:把滑窗切帧、CNN 卷积栈和 GRU 串成端到端
CWT 出图之后,问题变成「怎么把一堆时频图组织成序列,并让 CNN 和 GRU 在同一张计算图里训练」。常见做法是先单帧提特征再训 GRU,两步分开;这里用 MATLAB 的sequenceFoldingLayer把两步合成一张图端到端训练,梯度能从 GRU 直接传回卷积层,特征提取和时序建模的目标函数一致。
3.1 滑窗切帧:把长信号变成图像序列
function [seqCell, labels] = buildSeqDataset(signalCell, fs, winLen, stepLen, seqLen, waveletName) % signalCell: N×1 cell,每个元素是一段同工况的长信号 seqCell = {}; labels = []; for s = 1:numel(signalCell) signal = signalCell{s}(:); nFrame = floor((numel(signal) - winLen) / stepLen) + 1; frameImg = cell(nFrame, 1); for i = 1:nFrame seg = signal((i-1)*stepLen + (1:winLen)); frameImg{i} = cwtToImage(seg, fs, waveletName, [500 4000]); end for i = 1:floor(nFrame / seqLen) idx = (i-1)*seqLen + (1:seqLen); seqCell{end+1} = cat(4, frameImg{idx}); % H×W×3×seqLen labels(end+1) = s; % 实际换成工况标签 end end labels = categorical(labels); end切帧是 CWT-CNN-GRU 里容易被低估的一步。winLen 建议取 1~2 个旋转周期,太短装不下一个完整冲击,太长把不同工况的信息混在同一帧;stepLen 取 winLen 的一半,50% 重叠能保证冲击不落在帧边界;seqLen 是 GRU 看到的时间步数,8~16 够用,再增大准确率提升有限,内存和训练时长却线性上涨。
| 参数 | 建议值 | 依据 |
|---|---|---|
| winLen | 1~2 个旋转周期 | 完整容纳冲击响应,又不混入过多无关工况 |
| stepLen | winLen / 2 | 重叠 50%,避免事件恰好在帧边缘被截断 |
| seqLen | 8~16 | GRU 时间步数,再大收益明显下降 |
提示:训练集和验证集必须按物理信号段划分,不能按帧随机切。同一段信号的相邻帧相关性很高,随机分帧会让验证指标虚高,上线后立刻露馅。
3.2 组网:sequenceInput + fold + conv + unfold + GRU
inputSize = [224 224 3]; numClasses = 4; % 按实际故障类别数修改 layers = [ sequenceInputLayer(inputSize, 'Name', 'seq_in') sequenceFoldingLayer('Name', 'fold') convolution2dLayer([5 5], 16, 'Padding', 'same', 'Name', 'conv1') batchNormalizationLayer('Name', 'bn1') reluLayer('Name', 'relu1') maxPooling2dLayer([2 2], 'Stride', 2, 'Name', 'pool1') convolution2dLayer([3 3], 32, 'Padding', 'same', 'Name', 'conv2') batchNormalizationLayer('Name', 'bn2') reluLayer('Name', 'relu2') maxPooling2dLayer([2 2], 'Stride', 2, 'Name', 'pool2') sequenceUnfoldingLayer('Name', 'unfold') flattenLayer('Name', 'flatten') gruLayer(64, 'Name', 'gru1') dropoutLayer(0.2, 'Name', 'drop') fullyConnectedLayer(numClasses, 'Name', 'fc') softmaxLayer('Name', 'softmax') classificationLayer('Name', 'output') ]; lgraph = layerGraph(layers); lgraph = connectLayers(lgraph, 'fold/miniBatchSize', 'unfold/miniBatchSize'); analyzeNetwork(lgraph);sequenceInputLayer接受的训练数据是 cell 数组,每个元素是 H×W×C×T 的四维矩阵,T 是该样本的序列长度。sequenceFoldingLayer把序列的时间维拆开,拍平成普通图像 batch 送进卷积栈;卷积和池化不会打乱帧顺序,sequenceUnfoldingLayer再按原样拼回序列;flattenLayer把每帧的特征图拉成向量,GRU 沿时间步消化;最后 softmax 出分类概率。倒数第二行connectLayers是必须的——fold 输出的miniBatchSize要通知 unfold 怎么还原序列,漏掉这行analyzeNetwork直接报错。
选 GRU 而不是 LSTM 的工程理由是参数量:GRU 只有重置门和更新门,参数约为 LSTM 的三分之二,故障诊断数据集规模通常不大,更少的参数意味着更不容易过拟合。gruLayer 在 R2019a 之后的 Deep Learning Toolbox 里都有,老版本可以先用 lstmLayer 占位,结构不变。
3.3 trainingOptions 参数怎么设,以及训练过程的判读
options = trainingOptions('adam', ... 'MaxEpochs', 30, ... 'MiniBatchSize', 8, ... 'InitialLearnRate', 1e-3, ... 'LearnRateSchedule', 'piecewise', ... 'LearnRateDropFactor', 0.5, ... 'LearnRateDropPeriod', 8, ... 'ValidationData', {valSeq, valLabels}, ... 'ValidationFrequency', 10, ... 'Plots', 'training-progress', ... 'Shuffle', 'every-epoch', ... 'ExecutionEnvironment', 'auto');| 参数 | 建议值 | 理由 |
|---|---|---|
| MiniBatchSize | 4~16 | 折叠后实际过卷积的图像数是它乘以 seqLen,显存不够先减它 |
| InitialLearnRate | 1e-3 | adam 下从 1e-3 起步,val 不降再减到 3e-4 |
| LearnRateDropPeriod | 8 | 每 8 轮减半,给 GRU 段留出收敛时间 |
| ValidationFrequency | 10 | 每 10 次迭代看一眼验证损失,太频繁拖慢训练 |
| Shuffle | every-epoch | 防止序列顺序给训练引入偏差 |
训练曲线里 loss 有锯齿是正常的,重点看验证准确率的拐点。如果验证集在 10 轮内不涨,先把学习率除以 3,而不是加层;GRU 超过 2 层对短序列场景往往有害。验证数据同样要包成 cell 数组的序列格式,和训练数据保持一致,否则trainNetwork会在数据格式检查阶段直接报错。
4. GUI 设计与回调代码详解:用 App Designer 把模型包成诊断工具
模型训练收敛后,剩下的工作是把推理过程交给不写代码的现场人员。App Designer 是 MATLAB 官方推荐的 GUI 方案,guide 创建的 .fig 旧项目还能开,新项目不建议再用。下面按界面布局、回调实现、打包注意事项三段讲清楚。
4.1 界面布局与控件职责
App Designer 里新建 Blank App,把左侧需要的控件拖到画布上。一个够用的诊断界面只需要七个控件:两个坐标轴、两个按钮、一个下拉框、一个数值框和一个标签。
| 控件 | 作用 |
|---|---|
| UIAxesMain | 显示原始波形,诊断后标注故障时刻 |
| UIAxesCWT | 显示当前帧的 CWT 时频图 |
| LoadButton | 从 .mat 文件载入振动信号 |
| RunButton | 触发诊断并回显结果 |
| WaveletDropDown | 运行时切换母小波 |
| WindowLenEdit | 设置帧长度(点数) |
| ResultLabel | 显示分类标签和置信度 |
关键是让每个控件职责单一:数据区只管信号,参数区只负责切帧,结果区只做显示。模型在startupFcn里一次性载入,不要在回调函数里反复load,否则每次点击按钮都会有几百毫秒的卡顿。
4.2 核心回调:加载信号、生成时频图、执行诊断
% startupFcn:应用启动时载入训练好的网络 function startupFcn(app) modelPath = fullfile('models', 'cwt_cnn_gru_net.mat'); if ~isfile(modelPath) app.ResultLabel.Text = '模型文件不存在,请先运行训练脚本'; return; end loaded = load(modelPath, 'net'); app.Net = loaded.net; end% LoadButtonPushed:读取 .mat 信号文件并绘图 function LoadButtonPushed(app, event) [file, path] = uigetfile({'*.mat'}, '选择振动信号文件'); if isequal(file, 0); return; end d = load(fullfile(path, file)); app.CurrentSignal = d.vibration(:); % 假定文件里有 vibration 字段 app.fs = d.fs; t = (0:numel(app.CurrentSignal)-1) / app.fs; plot(app.UIAxesMain, t, app.CurrentSignal) xlabel(app.UIAxesMain, '时间 / s') ylabel(app.UIAxesMain, '幅值') end% RunButtonPushed:切帧、过网络、回显结果 function RunButtonPushed(app, event) if isempty(app.CurrentSignal); return; end app.RunButton.Enable = 'off'; % 防止推理期间重复触发 drawnow; winLen = app.WindowLenEdit.Value; seqLen = app.SeqLenEdit.Value; seq = buildOneSequence(app.CurrentSignal, app.fs, winLen, seqLen, ... app.WaveletDropDown.Value); [label, score] = classify(app.Net, seq); app.ResultLabel.Text = sprintf('诊断结果:%s(置信度 %.1f%%)', ... char(label), max(score) * 100); app.RunButton.Enable = 'on'; end function seq = buildOneSequence(sig, fs, winLen, seqLen, wname) frames = zeros(224, 224, 3, seqLen); step = round(winLen / 2); for i = 1:seqLen seg = sig((i-1)*step + (1:winLen)); frames(:, :, :, i) = cwtToImage(seg, fs, wname, [500 4000]); end seq = {frames}; % 单样本也要包成 cell 序列,和训练格式一致 end三个回调串起来的逻辑是:启动时把网络放进内存;加载按钮把信号读进 app 属性并画原始波形;诊断按钮从界面取帧长和母小波,切出 seqLen 帧的序列,classify输出标签和置信度。drawnow放在按钮Enable切换之后,强制刷新界面,否则按钮状态变化要等回调结束才显示。诊断耗时主要在网络前向推理,切帧的 CWT 计算在 CPU 上做,序列不长时通常一两秒内能完成。
4.3 打包与接口差异的几个坑
% predict 和 classify 的差别:需要概率时用 predict [score, label] = predict(app.Net, seq);注意:
classify只返回标签,predict才返回各类别概率分数。要做置信度阈值判断或后续的代价敏感决策,用predict。
打包成独立应用时,模型 .mat 文件要作为附加文件加进项目,路径尽量用相对路径,不要依赖用户当前目录。用 MATLAB Compiler 部署时,Deep Learning Toolbox 的推理代码会被自动带上,但 CWT 相关的 Wavelet Toolbox 函数也要一并确认在依赖列表里。回调里只放推理不放训练,训练脚本单独留在 .m 文件里,GUI 保持轻量。
5. CWT-CNN-GRU 的验证收口:混淆矩阵、参数筛选与故障定位
5.1 先看混淆矩阵而不是总体准确率
figure; cm = confusionchart(YValidation, YPredicted); cm.Normalization = 'row-normalized'; % 逐类召回率:对角元除以对应行和 M = cm.ConfusionMatrix; perClassRecall = diag(M) ./ sum(M, 2);行归一化之后,对角线就是每类召回率。看哪两类互相混淆,比看总体准确率有用得多:滚动体和外圈故障在时频图上纹理接近,是最常见的混淆对。如果某两类确实分不开,回到第 2 章换母小波,或者把 winLen 加长让单帧包含更多冲击周期,而不是急着加深网络。
5.2 用轻量网格搜索定 CWT 参数
wavelets = {'amor', 'bump', 'cmor2-1', 'cmor3-3'}; bands = {[500 2000], [500 4000], [1000 4000]}; bestAcc = 0; for wi = 1:numel(wavelets) for bi = 1:numel(bands) % 按当前组合重新生成时频图数据集,固定 15 个 epoch 快速训练 valAcc = quickTrain(wavelets{wi}, bands{bi}); if valAcc > bestAcc bestAcc = valAcc; bestWave = wavelets{wi}; bestBand = bands{bi}; end end end这个脚本的核心思路是分层筛选:先用短训练把参数组合粗筛一遍,命中的组合再用完整 epoch 数重训。参数搜索阶段把 MaxEpochs 砍到 15,验证只看准确率不看收敛曲线,一轮网格控制在半小时内。全部跑完后用测试集验证一次,防止筛选过程过拟合到验证集。
5.3 用小波重构和包络定位故障时刻
[wt, f] = cwt(sig, 'amor', fs); % 重构故障频带分量,去掉信号均值 sigBand = icwt(wt, f, [800 3000], 'SignalMean', 'remove'); % 希尔伯特包络解调出冲击序列 env = abs(hilbert(sigBand)); [pks, locs] = findpeaks(env, fs, ... 'MinPeakHeight', 0.4*max(env), 'MinPeakDistance', 0.02); % 平均冲击间隔的倒数即为故障特征频率 faultFreq = 1 / mean(diff(locs));模型说「有故障」之后,下一步是告诉现场人员故障点在哪、周期是多少。icwt只重构诊断频带,hilbert包络把周期性冲击从调制信号里解调出来,findpeaks找到每个冲击时刻,平均间隔的倒数就是故障特征频率。把这个值和轴承故障特征频率的理论公式对照,比如外圈 BPFO、内圈 BPFI,能反过来验证 CNN-GRU 的分类是否合理。把 locs 时刻标注回原始波形,冲击间隔周期与理论故障特征频率倒数对齐,这条链路才算真正收口。
本文还有配套的精品资源,点击获取