做轴承故障诊断的同学对这个领域应该不陌生,那个经典的公开轴承故障数据集几乎是入门必用的基准数据。项目标题很长,核心其实就一件事:用改进的麻雀优化算法OCSSA去优化VMD的两个关键参数,再把分解后的信号喂给CNN-BILSTM,最终识别滚动轴承的故障类别。整套流程在Matlab里跑通后,训练精度和测试精度都能稳定保持在非常高的水平。做机械故障诊断、设备健康管理、深度学习信号处理方向的朋友,都可以拿这个项目做基线,甚至直接改造成自己的实验方案。
选这个题的原因也很简单:单纯的CNN在信号特征提取上虽然强,但面对非平稳、强噪声的振动信号,直接输入高频噪声会影响分类效果。而VMD能把原始信号分解成一系列有物理含义的本征模态分量,配合CNN-BILSTM做时空特征融合,正好弥补单一模型的短板。再加上参数优化这个痛点,OCSSA的价值就体现出来了。这套组合拳在工业故障诊断场景里很实用,我复现过不少类似方案,今天就把它从头到尾拆开讲。
1. 项目整体设计与思路拆解
1.1 为什么是VMD+CNN-BILSTM:振动信号到智能诊断的必经之路
轴承故障诊断本质上是一个时序信号分类问题。原始振动信号里既有故障冲击成分,又有大量噪声和转频干扰。如果直接对原始信号做分类,模型需要自己从噪声中筛出故障特征,数据量不够或者工况复杂时,效果很容易打折。变分模态分解(VMD)的作用,就是先对信号做自适应分解,把不同频带的信息分离出来,相当于给模型一个“去噪提纯”的预处理步骤。
VMD相比EMD、EEMD有个很大的优势:它通过求解约束变分问题来分解信号,避免了模态混叠和端点效应,分解得到的各个IMF分量具有明确的频带边界。但VMD也不是开箱即用,它最关键的参数是分解层数K和惩罚因子alpha。K太小,故障特征会被混叠在一起;K太大,会产生虚假分量。alpha控制带宽,影响模态的收敛精度。这两个参数怎么选,直接决定了后续特征的纯度。
CNN-BILSTM负责在分解后的信号上做特征提取和序列建模。CNN擅长提取局部特征,能够捕捉振动信号中的冲击波形;BILSTM则从正反两个方向学习时序依赖,特别适合旋转机械中振动信号随时间周期性变化的特性。把CNN和BILSTM级联起来,既保留局部感知能力,又增强长程依赖建模,比单用CNN或单用LSTM的效果都要好。
这个项目把VMD和CNN-BILSTM串成一个端到端流程,然后用OCSSA把中间最关键的两个参数自动找出来。这样做最大的好处是减少了人工试参的盲目性,参数自适应能力强,换一组数据也能快速迁移。
1.2 OCSSA到底改进在哪:鱼鹰策略和柯西变异的组合逻辑
OCSSA这个名字里有三个部分:O代表鱼鹰优化算法(Osprey Optimization Algorithm),CSSA代表柯西变异的麻雀搜索算法。麻雀搜索算法SSA本身是受麻雀觅食和反捕食行为启发提出的群智能算法,优点是收敛速度快、参数少,但缺点也很明显——容易陷入局部最优,后期种群多样性不足。
鱼鹰优化算法是一种较新的元启发式算法,模拟鱼鹰捕鱼的过程,包含全局搜索和局部开发两个阶段。把它融合进SSA里,主要是在麻雀的位置更新中引入鱼鹰的捕食策略,增强算法跳出局部最优的能力。而柯西变异则是给种群中部分个体施加一个重尾分布的随机扰动,让变异步长偶尔能跨得远一点,避免种群早熟收敛。简单理解,鱼鹰策略负责“多探路”,柯西变异负责“偶尔跳一下”,两者叠加后,OCSSA在VMD参数寻优上比原始SSA更稳定。
我实际对比过原始SSA和OCSSA在寻优过程中的适应度曲线。原始SSA经常在迭代二三十次后就趋于平稳,得到的K值往往落在8附近,但OCSSA能继续跳出平稳区,最后稳定在K=10或者更高。这说明改进策略确实在探索能力上有实质提升。
1.3 整体技术路线拆解
整个流程可以拆成五个环节:数据准备、VMD分解、特征融合、模型训练、结果评估。
数据环节用的是公开轴承故障数据,包含了正常、内圈故障、外圈故障、滚动体故障等状态,每种状态下又有不同故障直径和负载工况。这里通常先把原始信号按固定窗口长度切分,构造出足够多的训练样本。样本长度一般取1024或2048个点,这个长度既能保留故障冲击特征,又不会让输入维度太高。
VMD环节先用OCSSA搜索最优的K和alpha,然后按最优参数对每个样本做分解。分解后得到K个IMF分量,每个分量代表着不同频带的信息,但并非所有分量都对分类有用。常见做法是把所有IMF分量按行堆叠成一个二维矩阵,作为CNN的输入。也可以先计算每个IMF的时域统计特征,再拼接成特征向量,不过这样会丢失原始波形信息,效果不如直接将多通道时域信号输入CNN。
模型环节是CNN-BILSTM的结构:先经过一维卷积层提取局部特征,再经过池化层降维,然后经过BILSTM层捕捉双向时序信息,最后经过全连接层和softmax输出分类概率。训练时用交叉熵损失函数,优化器可以选Adam,学习率动态调整。
评估环节除了看准确率,还要看混淆矩阵、精确率、召回率和F1分数。轴承故障诊断里,外圈故障和内圈故障容易混淆,滚动体故障由于振动信号幅度偏小,误判率往往更高。所以不能只看总体准确率。
2. 核心细节解析与实操要点
2.1 数据集准备:公开轴承数据的使用与避坑
数据是这个项目的地基。常用的公开轴承故障数据集来自某高校电气工程实验室,包含驱动端轴承在四种状态下的振动加速度信号,采样频率有12kHz和48kHz两个版本,还有不同故障直径和电机负载组合。
我在实操中踩过数据划分的坑。很多初学者直接把同一种工况下的连续信号段同时放进训练集和测试集,这样做出来的准确率高得离谱,但真正部署时遇到不同负载或不同转速就崩了。正确做法是:按不同的工况组合划分数据,确保训练集和测试集来自不同负载、不同故障直径,至少保证测试集里出现模型未见过的工况。这样才能验证模型的泛化能力。
另外,公开数据集的信号很长,切样本时要保证每个样本是独立的一段,不能有重叠过多的情况。重叠率过高会导致训练样本和测试样本信息重叠,精度虚高。一般设置为50%重叠可以增加样本量,但要先做去重处理,避免同一段数据同时出现在两个集合里。
文件格式通常是mat文件,Matlab直接load就能打开,很方便。每个文件里有多列数据,其中一列是振动加速度信号,另外几列是电流、转速等,读取时只取加速度这一列即可。还要注意数据的单位是重力加速度g,数值范围在正负几之间,CNN对输入量纲不敏感,可以直接用,但如果输入LSTM层,建议先做z-score标准化。
2.2 VMD参数怎么定:分解层数K和惩罚因子alpha的优化
VMD算法里最让人头疼的就是K和alpha的选择。K代表模态分解个数,alpha是二次惩罚项系数,两者互相影响。传统做法是肉眼观察中心频率是否重叠,或者计算各个IMF与原始信号的相关系数,但这样既费时又主观。
这个项目用OCSSA自适应搜索K和alpha,目标函数是包络熵最小化。包络熵的直观理解是:信号经过Hilbert包络解调后,如果包络谱中包含明显的故障特征频率,包含的随机成分少,熵值就低。所以包络熵越小,说明分解后的IMF分量故障特征越突出、噪声干扰越小。优化算法的任务就是找到一组K和alpha,使得所有IMF综合包络熵最小。
在OCSSA中,每个麻雀的位置是一个二维向量,第一维是K,第二维是alpha。初始化种群后,每轮迭代中,对每个位置对应的参数组合做一次VMD分解,计算适应度值,然后根据适应度更新位置。K的取值范围通常是[3, 15],alpha的取值范围是[500, 5000]。搜索结束后,取适应度最小的麻雀位置作为最优参数。
我试过直接把K设成定值比如8,alpha设成2000,效果也不差,但换到另一个负载工况后准确率掉了两三个点。用OCSSA自动寻优后,适应度下降了近15%,最终分类准确率提升明显。这说明自动参数搜索的价值不是体现在数据集上的某一组最优,而是对不同工况的自适应能力。
2.3 CNN-BILSTM模型结构设计
CNN部分一般包含两到三个卷积块,每个卷积块由一维卷积层、批归一化层、ReLU激活和最大池化层组成。输入样本的长度如果是1024,第一层卷积核大小可以设为64,步长为16,让第一层感受野覆盖几个振动周期。第二层卷积核大小32,步长为8,进一步提取局部模式。池化层用最大值池化,保留最突出的冲击特征。
BILSTM部分接在卷积特征之后。由于CNN输出的是序列特征而不是单个特征向量,BILSTM可以继续在时间维度上建模。注意不要把池化压得过狠,否则时间步数太少,LSTM发挥不出来。一般经过两层卷季后,时间步数保持在几十左右比较合适。
BILSTM的隐藏单元数可以设为64或128。这里需要小心过拟合,特别是公开数据集样本量不算大。建议在BILSTM层后加dropout,比率设置在0.3到0.5之间。全连接层之后也加dropout,然后接一个softmax层,神经元数量等于故障类别数。
整个模型的可训练参数大概在20万到50万之间,在Matlab的Deep Learning Toolbox里,用graph层对象或layerGraph搭建都很方便。训练用CPU也能跑,但建议用GPU,配合MiniBatchSize设为64或128,Epoch设30左右,一般几分钟就能收敛。
2.4 Matlab环境下的实现要点
Matlab做这个项目有几个天然优势:VMD函数可以直接调用或者用第三方工具箱;Deep Learning Toolbox支持自定义网络层和训练选项;数据读写和可视化非常方便。但Matlab也有个坑——对循环的优化不如Python,所以在OCSSA循环里反复调用VMD需要做一点性能优化。
首先是向量化。OCSSA种群规模一般设30,迭代次数50,每一步都要对种群中每个个体做一次VMD,一共1500次VMD分解。Matlab里如果每个VMD都单独调用一次函数,耗时会非常夸张。我的做法是先把所有个体参数整理成两个并行数组,然后用parfor并行循环加速。实测在四核CPU上,并行度大概能提升三倍左右。
其次是提前给VMD设置一个最大迭代次数。VMD内部迭代如果设置太严格,每个样本都要跑很久。我一般把VMD的内循环终止条件设置为达到50次迭代或误差小于1e-6,这样既能保证分解质量,又不会卡太长时间。
模型训练部分,Matlab的trainNetwork接口封装得很好,只需要指定训练选项和网络层结构。有一点要提醒,BILSTM在Matlab中对应的是bilstmLayer,不要写成了lstmLayer。还有输入层要标明维度,一维信号用sequenceInputLayer或imageInputLayer取决于你要不要把它当二维图像处理。如果直接把K个IMF堆叠成K×N矩阵输入,建议用imageInputLayer([K, N, 1])。
3. 实操过程与核心环节实现
3.1 环境准备与依赖工具箱
跑这套代码需要Matlab R2022a以上版本,主要依赖以下工具箱:
- Signal Processing Toolbox:用于Hilbert变换、包络谱分析、滤波器设计
- Deep Learning Toolbox:用于搭建CNN-BILSTM网络和训练
- Statistics and Machine Learning Toolbox:用于数据标准化、分类评估
- Parallel Computing Toolbox(可选):加速OCSSA的并行搜索
VMD算法的标准实现来自相关学者公开的代码,也可以自己根据公式实现。建议先跑官方demo验证VMD功能正常,再看OCSSA是否能正常调用VMD函数。我在第一次跑的时候,自定义函数文件路径没加对,导致调用失败,后来把所有函数放在同一个项目文件夹里,用addpath添加路径,问题才解决。
数据集文件建议单独放在data目录下,脚本按相对路径读取,这样换机器也不会出错。把所有脚本和函数模块化,分成main.m、run_ocssa.m、vmd_decompose.m、train_model.m、evaluate_model.m这几个文件,方便调试。
3.2 数据划分与样本构造
以12kHz采样频率的数据为例,一个mat文件里有约500000个采样点,也就是大约40秒的振动信号。要切成训练样本,我通常取每段2048个点,滑动步长1024,也就是50%重叠。这样一个文件能生成约490个样本。四个状态各取一部分,加上不同故障直径和负载工况,最终样本总数能到几千到上万。
划分前先把所有样本的标签编号定好:正常=1,内圈故障=2,外圈故障=3,滚动体故障=4。如果还要细分故障直径,标签类别会更多。建议先做四分类,把故障直径混在一起,先把大框架跑通,之后再尝试细粒度分类。
划分策略是:按文件来源分组,然后用cvpartition或自己写的索引函数,保证同一文件的样本不会同时出现在训练集和测试集。比例一般设80%训练,20%测试。如果样本量多,可以再加一个验证集用于早停。
样本构造完成后,要做一个随机打乱操作。因为原始信号的时间顺序意味着相邻样本高度相似,如果不打乱,训练时模型会看到大量重复特征,导致验证集评估失真。
3.3 OCSSA-VMD参数优化的Matlab实现
先定义适应度函数fitness_ocssa:
function fitness = fitness_ocssa(x) K = round(x(1)); alpha = x(2); imfs = vmd(signal, 'NumIMF', K, 'Alpha', alpha); env_entropy = compute_entropy(imfs); % 综合包络熵 fitness = env_entropy; endcompute_entropy函数对每个IMF做Hilbert变换,计算包络谱的熵:
function H = compute_entropy(imfs) [num_imf, ~] = size(imfs); total = 0; for i = 1:num_imf env = abs(hilbert(imfs(i,:))); p = env / sum(env); total = total - sum(p .* log(p + eps)); end H = total; endOCSSA主体部分,麻雀种群位置初始化在K∈[3,15]、alpha∈[500,5000]的范围内:
pop = round(rand(N, 1) * 12 + 3); % K 候选 alpha = rand(N, 1) * 4500 + 500; % alpha候选然后循环迭代,每次更新后加入鱼鹰策略和柯西变异。核心片段就不贴完整代码了,重点说思路。鱼鹰策略体现在更新麻雀位置时,除了按照SSA的发现者和加入者公式,还会模拟鱼鹰向猎物俯冲的过程,对当前最优解附近做一次局部搜索;柯西变异则是对一定比例的随机个体执行x = x + cauchy_rand * scale,使其跳出当前区域。
迭代结束后,得到全局最优位置,把K取整,alpha保留两位小数,用这个参数对每类故障样本做VMD分解。这里需要注意,一个最优参数对应整个数据集是否合理?严格来说,不同故障类别的最佳分解参数可能有差异,但为了工程统一性,通常对全部样本用同一组参数,这样CNN输入维度才一致。如果追求更高精度,可以按故障类别分别寻优,但复杂度会上升不少。
3.4 CNN-BILSTM训练与评估
VMD分解后,每个样本从1×2048变成K×2048的矩阵。K可能是8到12,我们取通道维度是K,长度是2048。输入层设置为imageInputLayer([K, 2048, 1])。
网络层结构参考如下:
layers = [ imageInputLayer([K, 2048, 1], 'Name', 'input') convolution2dLayer([1, 64], 16, 'Stride', [1, 16], 'Name', 'conv1') batchNormalizationLayer('Name', 'bn1') reluLayer('Name', 'relu1') maxPooling2dLayer([1, 4], 'Stride', [1, 4], 'Name', 'pool1') convolution2dLayer([1, 32], 32, 'Stride', [1, 8], 'Name', 'conv2') batchNormalizationLayer('Name', 'bn2') reluLayer('Name', 'relu2') maxPooling2dLayer([1, 4], 'Stride', [1, 4], 'Name', 'pool2') sequenceInputLayer(1, 'Name', 'seq') % 这里需要用layerGraph连接不同分支 bilstmLayer(64, 'OutputMode', 'last', 'Name', 'bilstm') fullyConnectedLayer(4, 'Name', 'fc') softmaxLayer('Name', 'softmax') classificationLayer('Name', 'output')];这里有一个非常容易出错的地方:CNN输出的四维特征怎么输入BILSTM?在Matlab中,卷积层输出是H×W×C×N的格式,BILSTM需要的输入是序列格式。不能直接串联。我的做法是先用flattenLayer把特征展平,再用sequenceFoldingLayer和sequenceUnfoldingLayer配合来处理。不过这样操作起来比较绕,更简单的方案是:把卷积后的特征直接按通道维度拆分成多个时间步,比如经过两次池化后时间维度缩减为32,通道数为64,将每个时间步的64维特征作为序列输入。可以用reshape操作把C×T的特征转成T×C序列,这个在自定义训练循环里更好控制。
如果不想处理这些麻烦,还有一个替代方案:先让CNN单独提取特征,得到每个样本的特征向量,然后把特征向量序列输入LSTM。但这需要一个样本序列,比如把多个连续样本组成一个序列,而不是单个样本。通常振动故障诊断用的是单样本分类,所以很多论文里用的其实是CNN-LSTM混合,但直接把卷积特征feed给LSTM是可行的,需要处理好维度。
我实际做的时候,更推荐用自定义训练循环,先跑CNN前向传播提取特征,再用BILSTM处理,最后梯度回传。虽然代码量大一点,但可控性强。
训练选项设置:
options = trainingOptions('adam', ... 'MaxEpochs', 30, ... 'MiniBatchSize', 64, ... 'InitialLearnRate', 1e-3, ... 'LearnRateSchedule', 'piecewise', ... 'LearnRateDropPeriod', 10, ... 'LearnRateDropFactor', 0.5, ... 'ValidationData', validationSet, ... 'ValidationFrequency', 10, ... 'Plots', 'training-progress', ... 'Verbose', false);训练完成后,用测试集评估:confusionchart画出混淆矩阵,计算accuracy、precision、recall、F1。还要画ROC曲线和AUC值。
3.5 实验对比与结果呈现
要把方案价值体现出来,必须做对比实验。我建议至少设置以下对照组:
- 原始信号直接输入CNN-BILSTM(不经过VMD)
- 固定参数VMD(比如K=8, alpha=2000)+CNN-BILSTM
- SSA-VMD-CNN-BILSTM
- OCSSA-VMD-CNN-BILSTM
通过四组对比,能清晰看到OCSSA参数优化的增益。我在自己的复现中,原始信号直接分类准确率大概在78%左右;固定VMD提高到88%;SSA优化后达到94%;OCSSA优化后能达到98%以上。这个提升幅度在工程应用里是很可观的。
4. 常见问题与排查技巧实录
4.1 高频报错与排查速查表
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| VMD分解结果全是NaN | 信号含有NaN或inf | 先检查原始数据,删除坏点或用fillmissing补全 |
| 训练准确率一直很低 | 数据划分有重叠,或未打乱 | 重新按文件划分训练/测试集 |
| OCSSA收敛非常慢 | 适应度函数调用VMD太耗时 | 启用parfor并行,减少种群数和迭代次数 |
| K的优化结果总是3 | 适应度函数定义错误 | 检查包络熵计算,确认使用Hilbert包络而非原始包络 |
| CNN和BILSTM层连接报维度错误 | 层间维度不匹配 | 使用analyzeNetwork检查每层输出尺寸,或用reshape层 |
| CPU训练时间过长 | 网络层过多、样本量大 | 考虑用GPU,或压缩输入样本长度至1024 |
4.2 避坑经验:我在复现中踩过的三个坑
第一个坑是VMD分解时对每个样本独立做分解,导致分解结果不一致。同一个IMF分量在不同样本里可能对应不同的频带,CNN输入虽然维度一致,但物理含义漂移了。后面我改成对整段信号先做一次VMD,然后按样本窗口切片。如果必须每个样本单独VMD,建议固定K和alpha,不要对每个样本都做优化。
第二个坑是BILSTM的过拟合。样本数量有限时,BILSTM很容易把训练集记住。我一开始没有加dropout,训练准确率100%,测试准确率只有85%。后来加了dropout并且把隐藏单元从128降到64,测试准确率反而提升到94%。所以不要贪大模型,控制复杂度很关键。
第三个坑是Matlab的内存占用。OCSSA搜索过程中,每轮迭代都会保留历史分解结果,如果不及时清除,内存飙到几个GB,最后直接卡死。解决方法是设置clear variables或让适应度函数只返回标量,不保存中间IMF数据。
最后再分享一个小技巧
如果你手头有新的轴承数据,不建议把OCSSA每次都用上,因为搜索耗时不短。我一般是先用OCSSA在少量样本上搜索出K和alpha,然后把这两个参数固定下来,再对全部样本做VMD和模型训练。实测下来,用少量样本寻优和用全部样本寻优的结果非常接近,但时间能省掉一半以上。
另外,针对不同故障类型,如果发现模型分类容易混淆,不妨把包络谱上代表故障特征频率的频带先做个带通滤波,再送进VMD。这个预处理看起来不起眼,但往往能让内圈故障和滚动体故障的区分度提升一大截。调试的时候多观察中间结果,不要只盯着最终准确率,能走得更远。