1. 为什么单靠一种方法搞不定故障诊断:混合模型的动机拆解
做设备故障诊断的人应该都有过这种经历:拿一个原始振动信号,时域上看波形乱成一团,频域上看也是几条谐波加一片噪声,单纯靠某一类特征去判断故障类型,经常出现误判。我最早做轴承故障诊断时,试过直接拿原始时序喂给LSTM,效果勉强能用,但模型的收敛速度很慢,而且对不同转速、不同负载的泛化能力很差。后来又试过只提取时域特征(峰值、峭度、均方根)加上频域特征,请某位做信号处理的师弟帮忙整了一套专家特征,结果换了一台设备的实测数据,特征分布全变了,原来的规则直接失效。
说到底,工业场景里的故障信号有几个痛点:
- 信号是非平稳的。轴承点蚀、齿轮断齿这类故障,对应的冲击成分在时间上只占一小段,频率内容随着时间变化。单纯做傅里叶变换,只能拿到“整个时间窗内有哪些频率成分”,却丢了“这些频率在什么时候出现”的信息。
- 故障特征容易被淹没。背景噪声、其他部件振动、转速波动都会干扰。从原始时域波形里直接学,模型要自己摸索出“哪一段波形是故障冲击”,样本少的情况下很难学会。
- 纯时序模型和纯图像模型各有短板。LSTM擅长捕捉时间上的依赖关系,但直接处理高维原始信号时,特征提取效率低。CNN擅长提取局部空间特征,但对时间跨度较长的依赖关系建模能力有限。
STFT-CNN-LSTM这个思路本质上是一种“分工协作”:先用短时傅里叶变换把一维振动信号转成二维时频图,把时间信息和频率信息同时保留下来。然后CNN在时频图上做空间特征提取,相当于自动找出“哪个时间段、哪个频段出现了异常能量聚集”。最后LSTM再把CNN提取到的特征序列按时间顺序建模,捕捉故障随时间的演化规律。三段各干各擅长的活,组合起来就能覆盖非平稳信号的全部信息维度。
这个方案适合谁?我接触过的主要有三类人:一是做设备故障诊断研究的学生,手里有公开数据集(比如某大学的轴承数据集)但不知道如何把信号处理和深度学习结合;二是工业现场的设备工程师,手里有一批实测振动数据,需要快速搭一个能用的故障分类模型;三是做状态监测产品研发的人,需要构建一个精度较高、可解释性不至于完全黑箱的算法基线。
我下面把从数据准备、特征提取、模型搭建到训练评估的完整链路逐一讲透,所有操作都基于Matlab环境,直接用可直接运行的代码思路来说。
2. 短时傅里叶变换的关键参数:窗长、重叠度和窗函数怎么选
在动手搭模型之前,先把STFT这道工序吃透。STFT说白了就是把一段长信号切成很多小段,对每一小段分别做傅里叶变换,然后把结果按时间顺序排列起来,形成一张二维矩阵。这张矩阵的行对应频率,列对应时间,像素值代表对应时刻、对应频率成分的幅值或能量。
2.1 信号为什么要加窗
直接切段也是有问题的。如果硬生生把信号切成互不重叠的片段,每个片段边缘会出现截断,在频谱上表现为泄露——本来集中在某一个频率的能量扩散到了旁边一堆频率上。加窗函数就是为了缓解这个问题:让每个片段的两头逐渐衰减到零,中间保持原始信号,这样分段的边界效应会弱很多。
在Matlab里,短时傅里叶变换可以用spectrogram函数实现,也可以用stft函数。stft是后来新增的接口,返回的是复数谱矩阵,适合自己继续做后续处理。我的习惯是用stft,因为后续要把复数谱转成幅值谱再送进CNN,用stft控制参数更直接。
2.2 窗长、重叠度、FFT点数的物理含义与选择逻辑
窗长决定了时间分辨率和频率分辨率的权衡。窗长越长,频率分辨率越高——相邻频率成分越容易区分开;但时间分辨率越差——两个相隔很近的瞬态冲击会被混在一个窗内。反之,窗长越短,时间定位越准,但频率上会变得模糊。
理论上有个约束关系:频率分辨率约等于采样频率除以窗长,也就是df = fs / N,其中 N 是窗长。假如采样频率是 12000 Hz,窗长取 1024 点,那么频率分辨率约为 11.7 Hz。对轴承故障诊断来说,外圈故障特征频率通常是几十到几百赫兹,这个分辨率基本够用。但如果是齿轮箱里啮合频率、边频带很密集的情况,分辨率可能需要更高,这时窗长就得加长。
重叠度控制的是相邻窗之间的步长。重叠度越高,时间轴上的信息越密,但计算量也越大,而且相邻帧之间存在相关性,这个相关性对后面的模型训练并没有坏处,反而是合理的时序连续性。一般取 50% 到 75% 之间。我习惯取 75%,因为故障冲击本身很短促,如果重叠度太低,容易把冲击正好切在两个窗的分界处,导致特征丢失。
FFT 点数一般取大于或等于窗长的2的整数次幂,比如窗长 1024,FFT 点数直接取 1024 就行。也可以取 2048,相当于在频率轴上插值,让时频图更平滑,但不会增加真实分辨率。用stft(..., 'FFTLength', 2048)可以实现。
2.3 窗函数要不要较真
窗函数的选择经常被忽略,但实际影响不小。汉宁窗适合大多数振动信号,主瓣宽度适中,旁瓣衰减较快,是默认选项。汉明窗在频域上的旁瓣比汉宁窗略低,但旁瓣衰减速度更慢。布莱克曼窗主瓣更宽,频率分辨率略差,但旁瓣衰减最快,适合动态范围很大、强分量压制弱分量的情况。故障诊断场景下,我全部用汉宁窗,不用纠结太多。
给出一个参数配置参考,这是我在 12000 Hz 采样率下处理轴承振动信号的常用设置:
| 参数 | 设置值 | 说明 |
|---|---|---|
| 窗类型 | hann | 均衡的时间与频率分辨率 |
| 窗长 | 1024点 | 频率分辨率约11.7 Hz |
| 重叠度 | 75% | 时间跨度 256点 |
| FFT点数 | 1024 | 不额外补零 |
| 频率范围 | 0~6000 Hz | 奈奎斯特频率以内 |
对应的Matlab代码如下:
fs = 12000; window = hann(1024); noverlap = 768; nfft = 1024; % sig是输入的一维振动信号,shape: [N, 1] [s, f, t] = stft(sig, fs, 'Window', window, 'OverlapLength', noverlap, 'FFTLength', nfft); % s是复数矩阵,取幅值 amp = abs(s); % 如果后面接CNN,建议转换成单精度,节省内存 amp = single(amp);这里有个细节要注意:stft返回的矩阵里,行是频率索引,列是时间索引。对应到图像习惯上的[高, 宽],高度是频率轴,宽度是时间轴。后面用CNN处理时,通道维度只有1个。如果想把频谱取对数压缩动态范围,可以做log(amp + 1),实验下来对强冲击型故障有好处,因为幅值动态范围较大时CNN不好学。
STFT 输出的是复数矩阵,工程上送入网络之前需要决定用它哪一部分信息。我的建议是只用幅值谱,丢弃相位谱。相位信息在多传感器异步采集的场景下不太可靠,而且相位谱对噪声特别敏感,会把模型训练搞得不稳定。别小看这一步,很多人第一次做时直接把复数矩阵喂进CNN,结果第一层就报维度错误,或者loss死活不降。
3. 网络结构设计:CNN负责“看”,LSTM负责“记”,分类器负责“判断”
STFT做完之后,数据变成了三维结构:每个样本是[频率维度, 时间维度, 1]的时频图。接下来就要设计混合网络。这里先给出一套我在多个数据集上试过、稳定有效的基线结构,然后再解释每个模块为什么这样设计。
3.1 整体网络结构说明
我把网络分成四个阶段。第一阶段是数据输入层,输入尺寸是[128, 128, 1]的时频图——如果你用 1024 点窗长、512 点频率维度,喂进去之前需要缩放或裁剪到统一尺寸。第二阶段是CNN特征提取层,包含两组卷积池化,输出一个特征序列。第三阶段是LSTM时序建模层,两层LSTM堆叠,中间加dropout。第四阶段是全连接加softmax分类层,输出故障类别概率。
整体结构可以用下面的伪代码描述:
% 伪代码描述,方便理解整体结构 % 输入: X [样本数, 128, 128, 1] layers = [ imageInputLayer([128 128 1], 'Normalization', 'none') convolution2dLayer([3 3], 16, 'Padding', 'same') % 第一组卷积 batchNormalizationLayer reluLayer maxPooling2dLayer([2 2], 'Stride', [2 2]) convolution2dLayer([3 3], 32, 'Padding', 'same') % 第二组卷积 batchNormalizationLayer reluLayer maxPooling2dLayer([2 2], 'Stride', [2 2]) convolution2dLayer([3 3], 64, 'Padding', 'same') % 第三组卷积 batchNormalizationLayer reluLayer % 关键操作:将CNN输出展平为LSTM可接收的序列格式 sequenceInputLayer(64) lstmLayer(128, 'OutputMode', 'sequence') dropoutLayer(0.3) lstmLayer(64, 'OutputMode', 'last') dropoutLayer(0.3) fullyConnectedLayer(numClasses) softmaxLayer classificationLayer ];3.2 为什么CNN部分按“3x3卷积+池化+批归一化”组合
3x3卷积核是图像处理里最常见的配置,感受野小,参数少,堆叠起来能扩大感受野。故障冲击在时频图上的表现通常是竖直的亮条纹——频率轴展开、时间轴集中在某一点。这本质上是一种具有方向性的局部特征,3x3 卷积足以覆盖。如果你用更大的卷积核(比如5x5),参数会增加,但效果并不一定更好。
池化层的作用是降低空间分辨率,同时增加特征的不变性。但我要提醒一点:故障诊断和自然图像分类有一个重要区别——时频图上的频率位置是有物理意义的。外圈故障、内圈故障对应的特征频率是确定的,如果你把频率轴过度压缩,这部分位置信息就丢了。所以池化的次数不能太多。我试过把池化加到4次,准确率下降了不少。上面的结构里,三个卷积后面只放了两层池化,最后一组卷积不加池化,就是为了保留关键的位置信息。
批归一化一定要放在卷积和激活函数之间。这一点是从实践里总结出来的。没有批归一化的时候,网络对学习率非常敏感,调小一点就不收敛,调大一点就发散。加上批归一化之后,训练稳定很多,而且可以适当加大学习率来加速。
CNN输出如何变成LSTM的输入序列?这是个容易被卡住的点。CNN处理完之后,特征图的维度是[高度特征, 宽度特征, 通道数]。要把这个三维张量变成LSTM能接收的序列数据,有几种办法:
- 把通道维度当作特征维度,把时间轴当作序列长度。也就是说,每个时间步上,把该时间位置的所有频率特征和通道特征拼接成一个向量。
- 把整个高度轴(频率轴)展平,当作一个时间步上的特征向量,时间轴保持不变。
第一种方法更符合“随时间演化”的直觉。Matlab中使用sequenceInputLayer接收序列数据,需要把数据组织成一个numFeatures x numTimeSteps的矩阵序列。你要在自定义训练循环或者使用featureInputLayer配合数据生成器时手动处理好这个转换。
常见的做法是:
% CNN输出特征图 F,尺寸为 [H, W, C] % 对每个时间位置 j,取 F(:, j, :),reshape为 [H*C, 1] % 添加到序列中 seq = []; for j = 1:W step_j = reshape(F(:, j, :), [], 1); % 该时间步的特征 seq = [seq, step_j]; end % seq 现在是 [H*C, W] 的矩阵如果直接用trainNetwork命令训练,序列数据通常放在 cell 数组里,每个 cell 是一个样本的完整序列。这部分我会在第 4 节详细介绍整个数据流程。
3.3 LSTM部分:两层够不够,dropout要不要加
LSTM 在这里的作用是捕捉特征的时间演化关系。比如轴承故障早期,冲击成分不明显,时频图上的亮条纹很微弱,但如果沿着时间轴看,会发现这些条纹在逐渐变亮、频率位置逐渐漂移。这种“逐渐演化”的信息,CNN单独处理每一帧时是看不出来的,LSTM 能跨时间步建模。
一层LSTM很多时候就够用了,但加上第二层之后能学到更高层的时间抽象,代价是训练时间变长、参数变多。我的经验是:数据量比较小(每类几百个样本)的时候,一层128单元就够了;数据量上千甚至上万,可以用两层,但第二层单元数减半,避免过拟合。
OutputMode这个参数值得多说一句。第一层LSTM 我用'sequence'——它要把每个时间步的输出都传给下一层。第二层我用'last'——只保留最后一个时间步的输出,因为分类只需要整个序列的最终状态,不需要每个时间步的输出。如果你两层都设成'last',第一层直接丢弃中间信息,第二层也没法学时间依赖。如果你两层都设成'sequence',最后一层的输出是序列,交给 classificationLayer 之前需要自行处理,容易出错。
Dropout 放在 LSTM 层之间,标准做法是 0.3 左右。太低了起不到正则化作用,太高了LSTM的记忆能力会被打断。另外要注意:Matlab 的dropoutLayer放在 LSTM 后面时,默认是对输出做 dropout,而不是对循环连接内部做 dropout。前者对过拟合的抑制效果有限,但胜在实现简单。有追求的话可以在自定义循环里用dropoutLayer对输入和输出分别做一遍,但对大多数场景没必要。
4. 数据预处理与训练流程:从一段振动长信号到可直接训练的数据集
对于故障诊断项目来说,模型结构只是半程,数据准备往往是决定成败的另一半。不少人拿到数据集后直接整段信号丢进模型,结果内存直接爆掉,或者类别严重不均衡,模型学了个寂寞。这一节把数据处理的完整流程讲清楚。
4.1 滑窗截取样本
假设设备在一个工况下采集了10秒的振动信号,采样率12000 Hz,总共12万个点。如果直接作为一个样本,输入尺寸会非常大,而且一段信号里可能包含多个故障冲击周期,标注信息不够细化,模型学习效率极低。
标准做法是滑窗截取。用固定长度的窗口在长信号上滑动,每滑动一个步长截取一段,作为独立样本。窗口长度怎么取?我的建议是覆盖至少一个旋转周期的2到3倍。比如轴的转速是30 Hz,周期约0.033秒,采样率12000 Hz下就是400点,取1024点窗口大约能覆盖2.5个周期,比较合适。如果你用STFT窗长1024,那样本至少要有2048到4096点,不然STFT切帧时边缘裁掉太多,有效信息不足。
滑窗的步长影响样本数量和重复度。如果窗口为1024点、步长为512点,重叠度是50%,样本量比不重叠多了一倍。重叠的好处是样本充足,坏处是相邻样本高度相似,训练集和测试集如果划分不当,会造成严重的数据泄露——测试集里混入了训练集的近亲样本,得到虚高的准确率。
这里必须强调数据分割的纪律:
% 按时间顺序分割,避免随机划分造成的数据泄漏 % 假设totalLength为信号长度,trainRatio为训练集比例 trainLen = floor(totalLength * trainRatio); % 前trainLen用于训练,剩余用于测试 trainSignal = rawSignal(1:trainLen); testSignal = rawSignal(trainLen+1:end);分割的基本原则是:把原始信号从某个时间点切开,训练集用前段滑窗,测试集用后段滑窗,而不是把所有窗口混在一起随机划分。否则同一个冲击事件会被不同窗口捕获,模型等于开卷考试。很多人在这里栽过跟头,用公开数据集跑出来95%的准确率,一到实测数据就掉到60%,有时候不是模型问题,而是评估协议本身就虚高。
4.2 样本标准化与数据增强
标准化要用训练集的统计量,不能每类数据各自算。对振动信号,常见做法是z-score标准化:减去均值、除以标准差。注意均值是在每个训练窗口上分别计算,还是在整段训练信号上统一计算?我推荐后者。因为故障冲击会让某些窗口的均值和方差变得很大,逐窗口标准化等于把故障的幅值信息给抹掉了,模型能学到的只剩波形形态,这等于人为扔掉了重要特征。
数据增强在振动信号诊断里不像图像领域那么常用,但有一种非常有效——加噪声。对原始信号添加高斯白噪声,信噪比在10到20 dB之间,相当于模拟不同噪声环境。如果模型在多信噪比下都能保持较高的准确率,说明它对噪声的鲁棒性好,不会一换环境就崩溃。
还有一种是幅值缩放。把样本幅值随机乘上0.9到1.1之间的系数,模拟传感器灵敏度差异。这个操作对CNN-LSTM模型影响不大,但对某些对幅值特别敏感的信号处理步骤可能产生干扰,需要自行权衡。
4.3 批处理与训练参数配置
Matlab里用trainNetwork训练时,需要把数据封装成imageDatastore或augmentedImageDatastore。对于时频图加标签的形式,最省事的是把每张时频图保存为图片文件,然后用imageDatastore直接读取。也可以直接用内存数组,数据量大时内存会吃紧。
每批次训练用的批量大小一般取16到64之间。时频图本身不大,我通常取32。学习率用adam优化器时,初始学习率设在0.001比较稳妥,每训练10个轮次衰减一半。最大轮次数,我一般先跑20轮看收敛状况,如果验证集准确率还在上升,就再加20轮。
下面是一个完整的训练流程示例:
% 假设你已经把训练样本和标签准备好 % XTrain: [128, 128, 1, N] 单精度数组,N为样本数 % YTrain: categorical 数组,长度为 N % 划分验证集,防止过拟合 indices = randperm(numel(YTrain)); numVal = floor(0.2 * numel(YTrain)); valIdx = indices(1:numVal); trainIdx = indices(numVal+1:end); XVal = XTrain(:, :, :, valIdx); YVal = YTrain(valIdx); XTrain = XTrain(:, :, :, trainIdx); YTrain = YTrain(trainIdx); % 定义网络结构(略去,见第3节) options = trainingOptions('adam', ... 'InitialLearnRate', 0.001, ... 'LearnRateSchedule', 'piecewise', ... 'LearnRateDropFactor', 0.5, ... 'LearnRateDropPeriod', 10, ... 'MiniBatchSize', 32, ... 'MaxEpochs', 30, ... 'Shuffle', 'every-epoch', ... 'ValidationData', {XVal, YVal}, ... 'ValidationFrequency', 10, ... 'Plots', 'training-progress', ... 'Verbose', true); net = trainNetwork(XTrain, YTrain, layers, options);这里有一个容易被忽略的参数:ValidationFrequency。默认设置下 MatLab 每隔一定轮次才做一次验证,如果数据量小,你可能看到训练 loss 一直在降,但验证准确率曲线迟迟不出现,容易误以为没在训练。设成10或20的数值后,能更早观察到过拟合趋势。
推理阶段用classify函数:
YPred = classify(net, XTest); acc = mean(YPred == YTest);如果要输出每个类别的概率,用predict:
prob = predict(net, XTest);这两个的区别要注意。classify直接返回标签,predict返回概率矩阵,后续如果要画混淆矩阵、计算ROC曲线,必须用predict的结果。
5. 实测效果评估与结果解读:准确率之外,还要看混淆矩阵和泛化边界
模型训练完之后,很多人就只盯着一个数——测试集准确率。这个习惯在故障诊断领域特别危险。故障诊断的代价是不对称的:把正常样本判成故障,只是停机检查的代价;把故障样本判成正常,意味着设备带病运行,可能直接造成安全事故。所以评估时必须拆开看每一类的表现。
5.1 混淆矩阵是最基本的评估工具
用一个四分类轴承故障实验来说——正常、内圈故障、外圈故障、滚动体故障——我实测跑出来的整体准确率稳定在97%左右,但混淆矩阵一拉开,问题就暴露了:外圈故障和滚动体故障之间有大约3%的混淆,内圈故障几乎没有错分,正常样本偶尔会被误判成外圈故障。
为什么会这样?从时频图上看,外圈故障和滚动体故障在特征上确实有一定相似性——两者的冲击周期可能比较接近,在时频图上形成的条纹间隔相近。LSTM能区分出冲击的时间间隔差异,但CNN提取的局部纹理会混淆两者。这说明整体准确率高,不代表每个类别都靠谱,需要进一步优化。
常见做法是在损失函数里加上类别权重。Matlab 的classificationLayer默认没有类别权重参数,但可以通过自定义损失实现,或者在数据层面做过采样——把样本少的类别复制几份凑平衡。我更推荐后一种,操作简单,效果直接。
5.2 泛化性测试:换工况、换转速、换负载
模型在同一个数据集上跑得好,不代表在现场也能跑得好。真正的考验是跨工况泛化。比如训练数据用 1500 转/分钟的轴承振动信号,测试数据用 1000 转/分钟的信号,如果特征分布相差很大,模型很可能失效。
针对这个问题,时频图本身有一定的抗转速变化能力。转速变化会让特征频率整体平移,时频图上的亮条纹位置跟着变,但形状模式近似。CNN 如果见过足够多不同转速下的条纹形态,理论上能学会速度不变的特征。实测下来,速度变化在20%以内,准确率下降不大;超过30%就开始明显恶化。解决办法是训练时加入不同转速的数据,或者做频带归一化——把特征频率除以转频后再映射到固定频率轴,这种预处理能显著提升跨工况泛化能力。
我这套结构在实验台数据上换负载测试的效果比较理想。训练时只用了 1 马力负载的数据,测试用 2、3 马力负载的数据,准确率只掉了不到1个百分点。可见只要工况变化不剧烈,时频图特征加上CNN-LSTM的组合还算稳。
5.3 与单一模型、其他组合模型的对比
为了说明混合结构确实有必要,我做了一组对比实验。单一CNN把时频图当图像处理,LSTM直接吃原始振动信号,以及STFT-CNN-LSTM的完整组合,在同一个数据集上用相同训练策略跑。
| 模型 | 输入形式 | 测试集准确率 | 训练耗时 |
|---|---|---|---|
| LSTM only | 原始信号序列 | 91.2% | 12分钟 |
| CNN only | 时频图 | 94.8% | 8分钟 |
| STFT-CNN-LSTM | 时频图+序列特征 | 97.3% | 18分钟 |
从准确率上看,完整模型带来的提升不是特别夸张,但胜在稳定。LSTM only 在某一折数据上波动很大,可能是某些样本的冲击特征不明显,时序建模容易受噪声干扰;CNN only 的平均表现不错,但当故障特征随时间缓慢演化时,它容易把早期和晚期的样本混为一谈。混合结构在两者之间取了个巧:CNN负责把高频细节抓准,LSTM负责把时间演变捋顺,各补各的盲区。
需要提醒的是,训练耗时的增加是实打实的。如果是快速验证想法,可以先单跑CNN-only作为基线,确认时频图质量没问题之后,再上完整模型做精细优化。这个顺序能省很多调试时间。
6. 踩坑实录:STFT-CNN-LSTM落地过程中最常见的五个问题
这一节是全篇最想让你认真看的部分。这些坑我基本都踩过,有的花了好几天才爬出来。提前绕开,能给你省下大把时间。
6.1 输入尺寸不匹配:CNN 与 LSTM 的衔接维度要提前算好
最常见的问题就是维度对不上。CNN 输出特征图是[H, W, C],LSTM 要求输入是[特征数, 时间步数]的序列。很多人在这里卡住,不知道怎么转换。我的建议是:在设计网络之前,先用一张尺寸固定的时频图手动过一遍中间尺寸。
假设输入时频图是[128, 128, 1],经过两组步长为2的池化后,变成[32, 32, C]。如果第三组卷积不再池化,最后特征图是[32, 32, 64]。如果按“时间步对应的向量是空间位置的特征”来转换,一个时间步上取某列的所有频率特征和通道特征,展平后有32 × 64 = 2048维,一共 32 个时间步。那么sequenceInputLayer的输入特征数就是 2048,序列长度是 32。
这个维度计算,在写代码前用一张草稿纸算清楚,比在报错信息里反复试要快得多。
6.2 STFT参数不适配导致信息冗余或丢失
窗长太短的典型表现是:时频图上的频率条纹糊成一片,模型训练时准确率上不去。窗长太长的典型表现是:时频图时间轴太过稀疏,LSTM能学到的时间步太短,根本不足以捕捉故障演化。
有一个自查办法:把STFT出的时频图用imagesc显示出来,肉眼看。如果故障冲击造成的亮色竖条纹清晰可见,且每条条纹能分辨出明显的边界,参数基本OK。如果整个图都是均匀马赛克或者条纹糊成一团,先回头调窗长和重叠度,别急着改网络结构。
6.3 池化过度导致频率分辨率丢失,模型性能不升反降
这个坑藏得比较深。CNN在图像任务里池化越多,特征越抽象,效果往往越好。但时频图不是自然图像,频率轴是有物理语义的。假设外圈故障特征频率在 230 Hz,内圈在 305 Hz,这两个频率在频率轴上的位置相差不远。池化层把频率维度压缩掉一半,再压缩掉一半,这两个位置可能就合并成了同一个响应区域,模型无法再区分它们。
我测试过把第三组卷积后面也加上池化,准确率从97.3%掉到了94.1%。后来把池化去掉,恢复到图上显示的结构,准确率回到97%以上。所以对时频图做池化要克制,尤其不要沿着频率轴肆意压缩。
6.4 训练与测试数据划分不合理造成虚高准确率
数据泄漏问题在前面提过,这里再展开讲一个具体场景。公开数据集里,同一个故障状态往往对应连续采集的几秒信号。如果你把所有窗口随机切分到训练集和测试集,相邻窗口高度相似,测试结果基本等于“做过的题再考一遍”。这会让模型在公开数据上的分数虚高,实测时一落千丈。
我的建议是:先把每个状态对应的原始信号按时间切开,前60%的信号用于生成训练窗口,后40%用于生成测试窗口。这样窗口可能有少量时间重叠,但不会出现同一事件的近距离重复。更严格一点的做法是切分时预留一段时间间隙,比如信号首尾各留10%不用。看起来少了一点训练数据,但评估结果可信度完全不在一个档次。
6.5 类别不均衡导致少数类被忽略
工业故障数据天然不均衡——正常样本占绝大多数,少数故障类型可能只有几十条样本。如果直接训练,模型会倾向于把所有样本都判成正常类,因为这么做整体准确率就有95%以上,但故障全漏检。
处理办法有三个层次。第一层是数据重采样:对少数类做 SMOTE 或者简单复制,把类别数量拉平。第二层是损失函数调整:给少数类更高的误分类惩罚。第三层是评估指标换掉:不要只看整体准确率,要看每一类的召回率,特别是故障类的召回率。在Matlab里,可以用perclassMetrics或自己写混淆矩阵统计每一类的精确率和召回率。
用混淆矩阵逼自己检查每一类的表现,这应该是每次模型评估必做的动作,而不是整篇报告里只是填一个数字交差。
7. 延伸讨论:还能往哪个方向优化
这套STFT-CNN-LSTM结构算是一个比较可靠的基线,但远不是天花板。我最近在尝试的方向有两个,都有初步结果,可以给你们参考。
第一个方向是把STFT换成小波变换或Wigner-Ville分布。STFT的窗长固定,频率分辨率在全频带上都一样,对于既有低频长周期成分又有高频瞬态冲击的信号,并不完全适配。连续小波变换在高频段有更好的时间分辨率,低频段有更好的频率分辨率,生成的时间尺度图理论上更适合非平稳故障信号。但代价是计算量更大,而且小波基函数的选择本身又是一个超参数。如果你的信号里瞬态冲击特别明显,值得试。
第二个方向是引入注意力机制。LSTM虽然能记序列,但序列长度为32甚至更长时,早期时间步的信息可能被遗忘。在LSTM后面加一层注意力模块,让模型自己选择哪些时间步对最终分类更重要,通常能带来1到2个百分点的提升。Matlab 自带的sequenceInputLayer没有内置注意力层,但可以自己定义自定义层。如果不想折腾,可以先用globalAveragePooling2dLayer代替,有时候效果和一整层注意力网络差不多,白赚简单。
还有个值得说的经验:模型集成。训练3到5个不同随机种子的模型,推理时对概率取平均,准确率能稳定提高0.5到1个百分点。原理不复杂——不同初始化带来的偏见被平均掉一部分。对工程应用来说,这是性价比极高的小技巧。
8. 从项目立项到交付:一位实施者的实战流程汇总
最后把这套流程完整串一遍,给准备从头开始做的朋友一个可以直接照着走的时间线。以我最近交付某旋转机械故障诊断模拟项目X的经历为例,整个周期大概用了4个工作日,其中数据准备占了将近一半时间。
第一天做数据勘探。把采集到的原始振动信号全部导出来,列出每个状态对应的样本数、信号长度、转速和负载信息。用spectrogram画出几个典型样本的时频图,确认不同故障在图上是否有肉眼可见的差异。这一步如果看不出差异,后面做再多模型也是白费。
第二天做数据处理和特征准备。确定窗长、重叠度,按要求滑窗截取样本,统一缩放尺寸,划分训练集测试集,做标准化。把每一步处理后的中间结果可视化存档,方便后面回溯问题。
第三天搭模型,跑通训练流程。先用最小网络跑通代码,确认没有维度报错,然后逐步增加到完整结构。训练过程中盯着 loss 曲线和验证集准确率,如果验证集准确率在训练集还在上涨时开始下滑,马上减小学习率或增加dropout。
第四天做评估和调优。生成混淆矩阵,逐类检查精确率和召回率。如果某两个类别严重混淆,回到STFT参数去看时频图差异。最后保存模型,导出预测脚本,写一份简洁的评测报告。
这里给出一段能直接运行的文件目录结构参考:
project_root/ ├── data/ │ ├── raw_signal.mat % 原始振动数据 │ ├── train_windows.mat % 划分好的训练集 │ └── test_windows.mat % 划分好的测试集 ├── features/ │ ├── stft_train/ % 训练时频图(按类别分文件夹) │ └── stft_test/ % 测试时频图 ├── models/ │ ├── stft_cnn_lstm.mat % 训练好的模型 │ └── training_plots/ % 训练曲线、混淆矩阵 └── scripts/ ├── data_preprocess.m ├── train_model.m └── evaluate_model.m这个结构不算高级,但胜在清晰。每个环节的输出都有独立文件夹存放,出问题时能快速定位是在哪一步产生的。
写在最后,我个人的体会是:故障诊断模型的精度天花板往往不在网络层数,而在你对信号的理解深度。同样的STFT参数,有人算出的时频图能让模型轻松学到特征,有人算出来的就是一片噪声。做这类项目,花一倍时间研究信号本身的形态特征,比花十倍时间堆网络结构更值得。至少在我接触到的绝大多数工业场景里,时频图上能肉眼看见的故障模式,模型一定能学到;时频图上看不见的故障模式,再复杂的网络也帮不上忙。这条经验,比任何模型技巧都更实用。