离散韦格纳分布结合CNN与ResNet的轴承故障诊断实践 最近整理旧项目时我翻到一套基于离散韦格纳分布DWVD结合卷积神经网络CNN与残差网络ResNet的轴承故障诊断流程也就是标题里的DVRN方案。当时为了把这个流程跑通我在Matlab里折腾了两周左右从振动信号分段、时频变换到网络结构搭建、训练调参每一步都有论文里不会写清楚的小坑。这篇把我实际用到的思路、代码逻辑和踩坑记录整理出来给同样在做故障诊断项目的朋友一个可以直接照搬的参考。这套流程解决的场景很明确我们拿到的是旋转机械的振动加速度信号目标是判断设备当前处于正常、内圈故障、外圈故障还是滚动体故障。传统做法是靠人工特征加分类器比如算峭度、有效值、包络谱峰值再扔给支持向量机。麻烦的地方在于不同负载、转速、损伤尺寸下这些手工特征很不稳定换个工况就要重新调。我的做法是把振动信号先变成时频图再用深度学习网络自动提取特征省掉大量人工设计环节。下面按实际项目推进的顺序来讲。1. 从振动波形到时频图这一步为什么要做1.1 一维波形到底缺了什么先用最朴素的话回答轴承故障信号不是一个平稳的周期信号。正常轴承的振动近似高斯噪声叠加上转频成分而一旦出现局部损伤每次滚动体经过缺陷位置都会激起一个冲击这个冲击会在轴承固有频率附近激发出一段高频衰减振荡。冲击出现的时间间隔携带着故障周期信息不同故障位置对应的间隔模式完全不同。如果只看一维波形你确实能看到一些冲击但很难准确回答两个问题这些冲击发生在哪个频带它们的间隔是否稳定频域分析可以解决第二个问题的一部分却会丢失冲击发生的时间顺序。实际采集的振动信号转速往往有小幅波动载荷也不是恒定值纯频域谱线会被拉宽、模糊这时候时间和频率两个维度的信息必须同时保留才能把故障模式分开。所以当时我第一反应不是去设计更复杂的特征而是把信号投影到二维时频平面让网络自己去学时间位置和频率结构之间的关系。1.2 DWVD、STFT、CWT放在一起怎么选时频分析的选项很多项目里最常用的三样是短时傅里叶变换STFT、连续小波变换CWT和韦格纳分布家族。我简单对比过它们的差异方法频率分辨率时间分辨率交叉项最适合的场景STFT受窗长限制受窗长限制无平稳或缓变信号CWT低频好、高频差高频好无瞬态、非平稳信号DWVD高高有强非平稳瞬态、需要高时频聚集性STFT最大问题是窗长取舍窗长太长频率分辨率好但时间定位差窗长太短时间定位好但频率糊成一团。轴承故障冲击是短促宽带事件STFT经常让我顾此失彼。CWT没有交叉项问题但计算量大而且在这个分类任务里小波基函数的选择本身就会引入偏差。DWVD属于科恩Cohen类时频分布理论上对瞬时频率调制信号有很高的聚集性冲击在时频图上是一道窄而亮的竖条谐波成分是水平的亮脊线这种对比结构对CNN特别友好。不过我要提醒一句DWVD不是免费的午餐它是二次型分布多分量信号会产生交叉项干扰时频图上出现不存在的“鬼影”亮斑。这一点在后面的参数设计和网络训练里必须处理否则模型可能把交叉项当特征学进去。2. DWVD在Matlab里的落地公式、代码与调参2.1 离散韦格纳分布的数学直觉韦格纳分布的核心思想很简单对于一个连续信号 (x(t))先构造关于时间 (t) 和滞后 (\tau) 的瞬时自相关[ R_x(t,\tau) x\left(t\frac{\tau}{2}\right) x^*\left(t-\frac{\tau}{2}\right) ]再对滞后变量 (\tau) 做傅里叶变换就得到[ W_x(t,f) \int_{-\infty}^{\infty} R_x(t,\tau) e^{-j2\pi f\tau} d\tau ]直观理解就是在每个时间点看信号“自己和自己错开一点”之后的相关性这个相关性的频谱结构就反映了该时刻附近的频率成分。对离散采样信号来说把这个公式离散化再用FFT加速就是标题里说的离散韦格纳分布DWVD。实际用的时候必须做一个关键预处理先把实数振动信号通过希尔伯特变换变成解析信号。因为WVD对实信号直接计算时正频率和负频率成分会互相干扰产生大量虚假项。Matlab里一行z hilbert(x)就能拿到解析信号这一步不要省。2.2 我实际用的DWVD转图像函数下面这段是我项目里的核心工具函数输入一段振动信号输出一张归一化后的灰度时频图function img dwvdImage(x, nfft, imgSize) % DWVD - 灰度图像 % x: 一段振动信号 % nfft: FFT点数通常取和信号等长或2的幂 % imgSize: 输出图像的尺寸例如64表示64x64 x x(:); % 确保列向量 z hilbert(x); % 解析信号去掉负频率镜像干扰 N length(x); wd zeros(N, nfft); for n 1:N mMax min(n - 1, N - n); m -mMax:mMax; % 滞后范围 r z(n m) .* conj(z(n - m)); % 瞬时自相关 spec abs(fftshift(fft(r, nfft))); wd(n, :) spec; end % 只保留正频率部分便于和图像坐标对应 wd wd(:, nfft/2 1:end); % 幅值归一化压缩动态范围 wd wd / max(wd(:)); img imresize(wd, [imgSize, imgSize], bilinear); end必须说明这段代码是教学级实现没有加平滑窗交叉项会比较明显但拿来做流程验证完全够用。如果直接拿它跑最终实验我建议换用Matlab信号处理工具箱里更成熟的平滑伪WVD函数[wv, f, t] wvd(z, fs, smoothedPseudo); img abs(wv); img imresize(img, [imgSize, imgSize]); img mat2gray(img);平滑伪WVD在时间和频率两个方向都做了加窗平滑能有效压制交叉项。最终工程里我用的就是平滑伪版本。自己手写函数的主要价值是理解原理、方便打印中间结果以及在没有工具箱的机器上应急。2.3 三个关键参数段长、重叠率、图像尺寸DWVD参数里最容易影响分类结果的是信号分段长度。段长太短比如128点频率分辨率不够故障特征频率附近糊成一片段长太长比如4096点单张图计算时间明显变大而且一段信号内可能混入转速波动时频图形态变复杂。我用的采样率是12kHz最终选了2048点一段对应约0.17秒这个长度既能覆盖足够多的故障冲击周期又不会让单张图计算压力过大。重叠率决定样本数量。滑动截取时我用过0.5和0.75两种0.75得到的样本数更多对训练小网络有帮助但相邻样本高度相似这会在数据划分时埋雷。我的经验是如果按振动文件而不是按单个样本划分训练集和测试集0.75重叠率是安全的如果偷懒随机抽样划分0.75重叠率会让验证集和训练集高度重复验证精度虚高上线就露馅。图像尺寸方面64×64是性价比很高的选择。小于48×48时DWVD里谐波间隔和冲击位置这些细节丢失太厉害大于128×128后网络参数和训练时间翻倍但对分类精度的提升很小。先跑64×64网络收敛了再试128×128不要一上来就追求大图。3. CNN与ResNet双分支网络结构是怎么定的3.1 为什么不能只靠普通CNN时频图本质上是一张二维灰度图卷积神经网络天然适合处理。普通CNN堆几层卷积加池化已经能拿到不错的准确率。但问题在于当故障类别变多、故障特征变细微时浅层CNN提取的多是纹理和边缘很难捕捉“某个频带上能量分布随时间变化”这种全局结构。残差网络的出现正好补上这块。ResNet核心是残差块[ y F(x, {W_i}) x ]网络不再直接学习从 (x) 到期望输出的完整映射而是学习输入和输出之间的残差 (F)。对于故障诊断来说正常图片和早期故障图片的差异往往很小残差学习相当于只在原图上做“局部修正”比重新学习整个映射更容易收敛。这也是为什么当我把普通CNN换成带残差连接的结构后验证集上表现更稳定。3.2 我在DVRN里用的双分支结构标题里既然写的是“CNN和ResNet结合”我最终采用了双分支融合方案一个分支用浅层CNN抓局部瞬态纹理一个分支用mini ResNet抓全局能量分布最后把两种特征拼接起来分类。结构示意如下模块具体结构输出尺寸输入64×64×1 DWVD灰度图64×64×1CNN分支Conv3×3,32 → BN → ReLU → MaxPool2×2 → Conv3×3,64 → BN → ReLU → MaxPool2×2 → GlobalAvgPool64ResNet分支Conv7×7,64,stride2 → BN → ReLU → MaxPool3×3,stride2 → ResBlock(64)×2 → ResBlock(128)×2 → GlobalAvgPool128融合层Concat(CNN, ResNet) → FC(128) → BN → ReLU → Dropout(0.5) → FC(4) → Softmax4ResNet分支里的“ResBlock(64)×2”表示两个残差块每个块里是“Conv3×3 → BN → ReLU → Conv3×3 → BN”然后把输入直接加到输出上再做一次ReLU。当通道数从64变成128、空间尺寸减半时shortcut不能直接相加我用了一个1×1卷积加步长2的投影shortcut来对齐维度。这些细节在残差网络原始设计里都有落地时照做就行。选择双分支而不是简单加深单一网络是因为DWVD图像同时包含两类信息冲击引起的局部亮条纹以及谐波能量随时间展开的带状结构。浅层CNN擅长前者深层残差分支擅长后者。我把两个分支在GlobalAvgPool之后拼接后面的全连接层会自动学习如何权衡这两部分特征。实测下来双分支在“滚动体故障”和“内圈故障”这对容易混淆的类别上比单分支网络有明显改善。3.3 训练时我为什么不直接用预训练ResNet很多人会问Matlab里有现成的resnet18为什么不直接迁移过来我的回答是时频图和自然图像差别太大。ImageNet上训练出来的卷积核偏向识别猫狗、汽车这类自然物体边缘而DWVD图是物理量分布纹理规律完全不同预训练权重帮不上忙。加上我的输入图只有64×64远小于ResNet要求的224×224强行resize反而会把能量结构拉伸变形。所以在DVRN项目里所有网络都是从零开始训练没有用预训练模型。另外不要盲目把网络加到很深。输入只有64×64时我用一个浅层CNN加两组残差块的mini ResNet已经足够再深就会出现训练集准确率很高、验证集掉点的过拟合现象。先小后大是这类小样本物理信号项目里最稳妥的策略。4. 从数据准备到训练评估完整实验链路4.1 数据划分最容易出错的一步数据来自一组公开的轴承故障模拟数据包含正常、内圈故障、外圈故障、滚动体故障四类。原始信号是连续的长振动序列我先按2048点一段、重叠率0.75滑动截取再用上面的dwvdImage转成64×64图像。整个过程在Matlab里写成批量脚本保存成结构化文件dataRoot D:\dvrn_data; files dir(fullfile(dataRoot, *.mat)); imgs {}; labels {}; segLen 2048; overlap 0.75; step round(segLen * (1 - overlap)); for i 1:length(files) load(fullfile(dataRoot, files(i).name), vib, label); starts 1:step:length(vib) - segLen; for s starts seg vib(s:s segLen - 1); imgs{end1} dwvdImage(seg, 2048, 64); %#okSAGROW labels{end1} categorical(string(label)); %#okSAGROW end end这段代码的问题在于数据量大时内存会很紧张。我处理的方式是分批生成、分批保存再通过imageDatastore指向文件夹读取而不是把全部数组一次性压在变量里。实际项目里更推荐直接把每个类别按原始记录文件分成训练、验证、测试三组然后才做滑窗。要知道同一段长信号里紧挨着的两个窗口几乎是同一样本如果随机打乱后再划分验证集里会出现大量和训练集样本来自同一原始片段的“近亲样本”验证精度参考价值会大打折扣。4.2 训练主流程与超参数设置分类网络我用的Matlab深度学习工具箱。普通CNN分支可以直接用层数组写带残差连接的网络要用layerGraph手动连接。训练主流程代码如下numClasses 4; layers [ imageInputLayer([64 64 1], Normalization, none) convolution2dLayer(3, 32, Padding, same) batchNormalizationLayer reluLayer maxPooling2dLayer(2, Stride, 2) convolution2dLayer(3, 64, Padding, same) batchNormalizationLayer reluLayer maxPooling2dLayer(2, Stride, 2) globalAveragePooling2dLayer fullyConnectedLayer(128) dropoutLayer(0.5) fullyConnectedLayer(numClasses) softmaxLayer classificationLayer ]; options trainingOptions(adam, ... InitialLearnRate, 1e-3, ... MaxEpochs, 60, ... MiniBatchSize, 64, ... Shuffle, every-epoch, ... ValidationData, valImds, ... ValidationFrequency, 20, ... LearnRateSchedule, piecewise, ... LearnRateDropFactor, 0.1, ... LearnRateDropPeriod, 20, ... Plots, training-progress, ... Verbose, false); net trainNetwork(trainImds, layers, options);上面给的是普通CNN脚本。真正跑的DVRN网络里ResNet分支的残差块是用layerGraph一节节搭起来的我把残差块封装成addResBlock函数输入上一个图层名和卷积核数量函数内部完成“卷积块shortcutadditionLayer”的连接。这样可以避免同一层名重复改网络结构也方便。超参数方面初始学习率1e-3搭配20轮一次的0.1倍衰减是我的基线配置如果训练不收敛再降到1e-4不要一上来就是默认的0.01。4.3 评估指标与结果观察方法训练完成后测试阶段用classify得到预测标签然后算整体准确率、每类精确率、召回率和F1值。最直观的是混淆矩阵我习惯把行归一化打开看每一类的召回情况YPred classify(net, testImds); testLabels testImds.Labels; acc mean(YPred testLabels); figure; cm confusionchart(testLabels, YPred); cm.Normalization row-normalized;在我这个项目的实验条件下普通CNN的测试准确率大约94%纯ResNet分支约96%双分支DVRN能到98%左右。但我不建议把这个数字当成通用结论它高度依赖数据来源、损伤尺寸和样本量。更有参考价值的是混淆矩阵最容易出错的往往是滚动体故障和内圈故障因为两者在时频图上的能量分布模式比较接近双分支融合主要提升了这两个类别的区分度。5. 实际运行中最容易翻车的三个问题5.1 交叉项造成的“假特征”问题DWVD的交叉项是绕不开的。两个真实频率分量存在时WVD会在它们的时间和频率中点附近产生一个幅度可能超过真实分量的虚假项。反映在时频图上就是一些本不该存在的亮斑。如果不做平滑CNN完全可能把这些亮斑当作故障特征学进去。在训练集和测试集来自同一实验台时这种做法甚至不会暴露问题因为交叉项模式是固定的但换一台设备或换一个转速交叉项位置变了模型立刻失效。我的处理方案有三层第一所有信号先转解析信号再算DWVD第二最终工程用平滑伪WVD代替裸DWVD在时间和频率方向分别加窗把大部分交叉项抹平第三归一化时对灰度做99%分位截断避免个别极端亮斑主导整个图像的对比度。这三步做完模型在新数据上的泛化明显比裸DWVD稳定。5.2 样本不平衡和标签噪声故障诊断数据天然不平衡正常状态样本容易采集故障样本尤其是早期故障样本稀少。如果直接把原始类别比例送进网络模型会偏向多数的正常类少数故障类别召回率很低。我的做法是先按原始振动文件做分层划分保证每个类别在训练、验证、测试中的比例一致然后对少数类做在线数据增强。增益方式我用的是“随机起始点滑动截取加一点高斯白噪声小幅时间平移”而不是简单复制样本。这里有个细节我没有对时频图做水平翻转因为翻转时间轴会改变故障冲击的先后顺序对某些调制模式来说是物理上不成立的增强反而会引入错误样本。标签噪声方面故障数据里经常混着一些拆装、磨合阶段的异常振动人工标注容易错。我训练前会随机抽一些DWVD图出来人工看一遍把明显不属于任何类别的样本剔除。这个动作花不了多少时间但对最终模型鲁棒性的提升比换网络结构还明显。5.3 训练不收敛与内存不够第一次跑DVRN时我用的是自己手写的DWVD函数2048点一段信号外层循环2048次每次还要做FFT生成3000张图花了将近一个下午。后来发现大多数时间浪费在两层for循环上。解决办法一是换成内置wvd函数二是把预计算好的时频图全部缓存成.mat文件训练时只读图不重算。训练流程要设计成“变换一次、多次复用”千万不要在每次epoch里重新生成DWVD图。训练不收敛的典型表现是loss变成NaN或者准确率一直卡在随机水平。我遇到过的原因主要有三个学习率太大学到数值溢出MiniBatchSize太小、BN层统计量不稳定最后一个epoch前关闭了Shuffle导致类别顺序周期性出现。基线配置里把初始学习率设为1e-3、MiniBatchSize设为64、Shuffle设成every-epoch之后这类问题基本消失。如果数据量很小比如每类只有几十个样本BatchNorm不如不用直接把BN层去掉反而更稳。最后再分享一点个人体会。故障诊断项目里最出成绩的地方往往不是换一个更新的网络模型而是把时频图这一步做仔细。DWVD图的质量、样本划分方式、交叉项处理这些环节直接决定了网络是在学真实物理特征还是在学伪影。我的工作习惯是先跑一个20分钟的小数据冒烟测试确认数据划分、标签和训练流程都正常再扩大样本量跑正式实验。这个习惯帮我省下的冤枉时间远比调模型结构多。