☰
基于STFT-CNN-BiGRU的故障诊断方法:从时频图到深度学习分类
2026/10/22 11:20:30 网站建设 项目流程

搞设备故障诊断这几年,我最大的感受就是:信号处理方法和深度学习模型的组合,不是越新越好,而是匹配度越高越好。早先处理振动数据,大家习惯先做FFT看频谱,再找特征频率;可一旦设备转速波动、负载突变,非平稳信号会让频谱图变得一塌糊涂。后来我尝试把短时傅里叶变换(STFT)、卷积神经网络(CNN)和双向门控循环单元(BiGRU)串成一条链路,用STFT出时频图,用CNN抠图里的局部纹理,用BiGRU抓时间维上的前后依赖,在Matlab里把整套流程撸了出来。这篇就把这套方案的原理、代码和调参过程完整展开,适合正在做故障诊断、状态监测相关课题的同行参考。


1. 为什么故障诊断偏偏选"STFT+CNN+BiGRU"这套组合药方

1.1 只盯频谱图的传统做法,在复杂工况下为什么会失灵

先说传统FFT的毛病。FFT的本质是把整段信号看作平稳信号,求一个全局平均频谱。对实验室里稳定转速下的振动数据,这种方法确实好用:转频、啮合频率、故障特征频率都在频谱上一目了然。但到了实际现场,设备转速波动、负载变化、冲击干扰是常态,信号的频率成分会随时间改变。这个时候整段做一次FFT,得到的是无数个瞬间频谱的叠加平均,故障特征频率可能被抹平,甚至被强烈的背景噪声盖住。

我印象很深的一个案例:某工厂透平机组,轴承外圈出现早期剥落,用整段FFT分析时,特征频率峰值只比噪声底高一点点,肉眼很难判断;但把同样一段信号按时间展开看时频图,故障引起的反复冲击在时频图上一段一段地出现,非常有规律。这就是时频分析的价值——它保留了"频率随时间怎么变"这层信息,而传统FFT把它丢掉了。

1.2 STFT把振动信号变成能"看"的时频图

STFT的思路很直白:信号不是非平稳吗?那我把它切成一小段一小段,每一段足够短,短到段内的信号可以近似看成平稳的,然后对每一小段做FFT。把所有小段的频谱按时间顺序拼起来,就是一张二维的时频图:横轴是时间,纵轴是频率,颜色或灰度表示该时刻该频率上的能量强度。

用生活里的例子类比:FFT相当于给整段信号拍一张照片,记录了"这个场景整体长什么样";STFT相当于拍一段视频,记录了"画面随着时间怎么变化"。故障诊断里很多关键线索恰恰藏在"变化"里,所以视频比照片有用。

Matlab里生成STFT时频图非常方便,核心就是spectrogram函数:

fs = 20480; % 采样率,Hz window = hann(256); % 窗函数:256点汉宁窗 noverlap = 128; % 相邻窗重叠128点(50%重叠) nfft = 512; % FFT点数 [S, F, T] = spectrogram(x, window, noverlap, nfft, fs); tfSpec = abs(S); % 幅值谱,作为时频图的像素值

这里的S是一个复数矩阵,行对应频率、列对应时间,abs(S)取幅值后就是时频图的灰度值。图像大小由nfft和信号长度决定,nfft = 512时频率轴有257个点。

1.3 CNN和BiGRU在这套方案里各自扮演什么角色

时频图本质上是一张图像,而CNN最擅长的就是从图像里提取局部纹理特征。故障冲击、谐波成分、边频带这些信息,在时频图上会形成特定的纹理和形状模式。CNN通过卷积核在图上滑动,能自动把这些局部特征一层层抽象出来,不需要人工设计特征工程,这是它取代人工特征提取器的根本原因。

BiGRU则负责处理时间维度上的依赖关系。时频图除了"像一张图",还隐含了"时间先后顺序"这层关系。某个故障是否发生、故障严重程度如何演变,往往和前后若干时刻的能量变化模式强相关。BiGRU是双向循环网络,它会从两个方向扫描时序特征:一个方向从开头读到结尾,另一个方向从结尾读到开头,这样每个时刻的输出都能同时参考前后文的信息。

打个比方:CNN像一个细心的检查员,逐区域观察时频图上的异常纹理;BiGRU像一个了解全局的调度员,知道"前面发生了什么、后面会怎么演变"。两者结合,既抓住了空间局部特征,又抓住了时序全局依赖,这正是故障诊断里最需要的两种视角。


2. 模型结构与关键参数:每个模块怎么定才不白搭

2.1 顶层设计:一维信号到故障类别的完整数据流

整个模型的数据流可以概括为:原始振动信号 → 滑窗分段 → 每段做STFT变换 → 得到时频图 → 归一化 → CNN提取空间特征 → 特征图按时间维拆解为特征序列 → BiGRU捕捉时序依赖 → 全连接层 → Softmax分类输出。

这里最核心、也最容易卡住新手的一步,是CNN输出特征图和BiGRU之间的衔接。CNN的卷积操作天然接受二维图像输入,输出也是二维或三维的特征图;而BiGRU要求输入是"特征序列",序列每个时刻对应一个特征向量。直接把CNN连到BiGRU在Matlab的层图里会报维度不匹配。

工程上我推荐一种两阶段思路:第一阶段用CNN训练一个图像分类器,训练完成后去掉最后的全连接分类层,把模型当作特征提取器;第二阶段把每张时频图送进CNN,得到特征图,再按时间轴把特征图展开成序列,喂给BiGRU做时序分类。这样两个阶段都能用Matlab自带的trainNetwork,不需要手写复杂的自定义训练循环,调试难度低不少。下面的章节都围绕这条路线展开。

2.2 STFT参数怎么选:窗口、重叠率和FFT点数的取舍

STFT的参数选择直接决定时频图的质量,这一步做不好,后面网络再强也没用。三个关键参数:窗函数、窗口长度、重叠率。

窗函数首选汉宁窗。振动信号分析里汉宁窗的旁瓣衰减好,能有效抑制频谱泄漏;矩形窗虽然主瓣窄,但旁瓣泄漏严重,容易把强频率成分的泄漏盖到弱频率成分上,不推荐。

窗口长度是频率分辨率和时间分辨率之间的权衡。频率分辨率Δf = fs / nfft,窗口长度越长,频率分辨率越高;但窗口越长,时间定位越模糊,对非平稳突变信号的响应越差。下面是我在采样率20480Hz下的一组对比:

参数设置频率分辨率帧数(1s信号)适用场景
窗长256,nfft 256,重叠50%80Hz159帧转速变化快,关注瞬态冲击
窗长512,nfft 512,重叠50%40Hz79帧转速较稳,兼顾频谱细节
窗长1024,nfft 1024,重叠50%20Hz39帧转速极稳,关注低频精细特征

从上表能直观看出,窗口越长、频率分辨率越好,但时间帧数变少,后续喂给BiGRU的序列长度也会缩短。序列太短,时序信息就少了。我在大多数滚动轴承诊断项目里的经验是取窗长512、50%重叠,这是一个比较稳的中间值。振动冲击类故障如果转速波动明显,建议缩到256。

重叠率方面,50%起步,75%更好但计算量增加。重叠的目的是让相邻窗口之间有连续性,避免把同一个瞬态冲击切到两个窗口里导致特征断裂。我先给一组实际跑出来的数据:重叠率从0%提高到50%,准确率大约提升2-4个百分点;再提高到75%,提升就不太明显了。

2.3 CNN层设计思路:卷积核尺寸、通道数和池化策略

CNN部分不需要堆得特别深,时频图的尺寸本来就不大,典型的输入是257×79这类灰度图,网络太深反而容易过拟合。我常用的结构是三层卷积:16通道、32通道、64通道,卷积核全部用3×3。

小卷积核的优势是参数量少、非线性表达能力更强。两个3×3卷积堆叠的感受野等于一个5×5卷积,但参数只有后者的18/25,还多了一层非线性激活。卷积层之间插入批归一化层和ReLU,每层卷积后接一个2×2最大池化把特征图尺寸减半,这样既能压缩计算量,也能让特征具有一定的平移不变性。

一个关键细节:因为后面要接BiGRU,所以CNN的池化不能把时间维彻底压没。时频图高度方向(频率)压到只剩几行甚至1行的程度都可以接受,但宽度方向(时间)最好保留足够的帧数。如果三层池化后时间维只剩十几个点,BiGRU能利用的时序上下文就太少了。我在设计里会把时间维的池化步长控制得比频率维宽松一些,有时干脆只在频率方向做全局池化,时间方向保持原样。

2.4 BiGRU衔接技巧:特征图如何按时间轴拆成序列

这一节是整个方案里最值得仔细琢磨的环节。假设CNN最后一个卷积层输出的特征图尺寸是H×W×C,其中H是频率维高度,W是时间维宽度,C是通道数。要把它转成BiGRU能吃的序列,我的做法是把每个时间帧对应的频率维和通道维拼成一个向量。

具体来说:特征图原本有W个时间帧,每个时间帧对应一个H×C的二维块。把这个二维块拉平,变成长度H*C的一维向量,那么这个向量就同时包含了"该时刻所有频率成分的响应强度"和"各通道提取到的抽象特征"。这样,一张特征图就变成了一个长度为W、特征维度为H*C的序列。W是序列长度,H*C是每个时刻的特征维度。

实际操作时还可以加一个简化操作:在频率方向做全局平均池化,把H压缩成1,这样每个时间帧的特征长度就变成C,和通道数一致。这个操作等于把每个时刻的频谱信息压缩成"这一时刻各种特征模式的综合强度",更省内存,训练也更快。我在项目中实测,全局频率池化和直接拉平相比,准确率几乎没差别,但训练速度快了差不多三分之一。

BiGRU这边,我一般用一层或两层,隐藏单元数取64。输入维度是C,输出模式选last,也就是只取序列最后一刻的隐藏状态作为整个序列的汇总表示,接一个全连接层和Softmax层做分类。


3. Matlab代码落地:从数据预处理到训练评估

3.1 数据读取与样本分段

实际工程里的原始振动信号往往是长时间连续采集的,不能直接整段丢进网络,要先按固定长度切成样本。切分时要注意两点:一是每个样本的物理长度要足够包含几个完整的故障冲击周期;二是样本之间尽量不要有太多重叠,否则训练集和测试集容易混入同一段数据,后面会细说这个问题。

% 读取原始振动信号,假设为单一测点数据 data = load('vibration_signal.mat'); x = data.signal; fs = 20480; % 按样本长度切分:每个样本0.25秒,即5120个点 sampleLen = 5120; numSamples = floor(length(x) / sampleLen); X = zeros(sampleLen, numSamples); for i = 1:numSamples X(:, i) = x((i-1)*sampleLen + 1 : i*sampleLen); end

每个样本0.25秒,在20480Hz采样率下已经能覆盖绝大多数滚动轴承故障特征频率的好几个周期,足够CNN-BiGRU模型提取有效特征。样本数不够多的时候,可以按50%重叠再切一次,相当于做简单数据增强。

3.2 STFT时频图生成与归一化

对每个样本生成STFT时频图,保存为一个三维数组,方便后面批量训练。归一化这一步我建议放在STFT之后做,对幅值谱进行最小-最大归一化,把像素值压到0到1之间,这样CNN的输入范围稳定,训练更平稳。

% 批量生成STFT时频图 windowLen = 512; noverlap = round(windowLen * 0.5); nfft = 512; [numPts, numSamples] = size(X); tfImgs = []; for i = 1:numSamples [S, ~, ~] = spectrogram(X(:, i), hann(windowLen), noverlap, nfft, fs); spec = abs(S); % 幅值谱矩阵 spec = spec / (max(spec(:)) + eps); % 最小-最大归一化 tfImgs = cat(3, tfImgs, spec); % 按第三维堆叠 end % tfImgs 现在是 [freqBins, timeFrames, numSamples] 的数组

需要注意的是spectrogram默认只返回频带0到奈奎斯特频率的部分,所以freqBins是nfft/2+1。如果信号量纲差异大(不同工况采集的数据幅值差很多),归一化要按整个数据集的最大最小值做,而不是每张图单独做,否则会破坏样本间的相对幅值信息。

3.3 自定义网络与训练选项设置

CNN部分我用trainNetwork训练,先把数据转换成Matlab标准格式:

% 把时频图整理成 imageInputLayer 接受的 4 维数组 T = size(tfImgs, 2); % 时间帧数 F = size(tfImgs, 1); % 频率点数 XCNN = reshape(tfImgs, [F, T, 1, numSamples]); % H×W×C×N YCategorical = categorical(labels); % labels 是故障类别向量 % CNN网络结构 cnnLayers = [ imageInputLayer([F, T, 1], 'Name', 'input', 'Normalization', 'none') convolution2dLayer([3 3], 16, 'Padding', 'same', 'Name', 'conv1') batchNormalizationLayer('Name', 'bn1') reluLayer('Name', 'relu1') maxPooling2dLayer([2 1], 'Stride', [2 1], 'Name', 'pool1') % 只在频率方向池化 convolution2dLayer([3 3], 32, 'Padding', 'same', 'Name', 'conv2') batchNormalizationLayer('Name', 'bn2') reluLayer('Name', 'relu2') maxPooling2dLayer([2 1], 'Stride', [2 1], 'Name', 'pool2') convolution2dLayer([3 3], 64, 'Padding', 'same', 'Name', 'conv3') batchNormalizationLayer('Name', 'bn3') reluLayer('Name', 'relu3') globalMaxPooling2dLayer('Name', 'globalPool') % 全局池化输出一维特征 fullyConnectedLayer(numClasses, 'Name', 'fc') softmaxLayer('Name', 'softmax') classificationLayer('Name', 'output') ];

留意这里的池化层,我用的是[2 1]的核,只在频率方向压缩,时间方向尺寸不变,目的就是给后面的BiGRU保留足够的时序长度。

CNN训练完成后,去掉fc、softmax和classification三层,剩下的部分就是特征提取器:

cnnFeatNet = layerGraph(cnnLayers); cnnFeatNet = removeLayers(cnnFeatNet, {'fc', 'softmax', 'output'}); % 前向传播,提取每个样本的深层特征 featMap = predict(dlnetwork(cnnFeatNet), dlarray(XCNN, 'SSCB'));

这里的SSCB是Matlab dlnetwork的常用数据格式标签,表示空间(S)、通道(C)、批量(B)。特征图尺寸是[1, T, 64, N],把它reshape成序列格式:每个样本是T步、每步64维特征向量。

BiGRU网络的搭建就非常清晰了:

% 将CNN特征图转成序列形式 [~, T, C, N] = size(featMap); XSeq = squeeze(featMap); % 变成 [T, C, N],每步64维 XSeqCell = cell(N, 1); for i = 1:N XSeqCell{i} = squeeze(XSeq(:, :, i))'; % [T×C],时间作为序列步 end % BiGRU分类网络 gruLayers = [ sequenceInputLayer(C, 'Name', 'seqInput') bilstmLayer(64, 'OutputMode', 'last', 'Name', 'bilstm') fullyConnectedLayer(numClasses, 'Name', 'fc') softmaxLayer('Name', 'softmax') classificationLayer('Name', 'output') ]; options = trainingOptions('adam', ... 'MiniBatchSize', 32, ... 'InitialLearnRate', 1e-3, ... 'MaxEpochs', 60, ... 'GradientThreshold', 1, ... 'Shuffle', 'every-epoch', ... 'Plots', 'training-progress', ... 'Verbose', true); gruNet = trainNetwork(XSeqCell, YCategorical, gruLayers, options);

这里我把完整流程拆成两段,用Matlab自带的训练函数都能跑通。如果你对自定义训练循环比较熟,也可以用dlnetwork把CNN和BiGRU拼成一个整体做端到端训练,效果会更好一些,但工程复杂度会明显上升,一般项目里两阶段已经完全够用。

3.4 训练后的评估:混淆矩阵和特征可视化

模型训练完,评估不能只看准确率一个数。故障诊断里最怕的是把A类故障误判成B类,所以必须看混淆矩阵,确认哪些类别容易混淆。用confusionchart一行代码就能出图:

YPred = classify(gruNet, XTestSeqCell); confusionchart(yTest, YPred);

除了混淆矩阵,我还会用t-SNE对BiGRU最后一层全连接层前的特征做降维可视化。如果同类样本在特征空间里聚成一团、不同类之间分得很开,说明模型学到的特征有区分度;如果是混成一片的,那大概率是前面特征提取环节有问题,回去查STFT参数或CNN结构,而不是盲目调BiGRU。


4. 调参实践:对精度影响最大的几个旋钮

4.1 窗口长度:频率分辨率和时间分辨率的拔河

先说窗口长度对最终诊断准确率的影响,这组数据是我在一个四分类轴承故障数据集上实测得到的:正常、内圈故障、外圈故障、滚动体故障,每类200个样本,随机划分80%训练、20%测试。

窗长nfft时间帧数测试准确率训练时间/epoch
2562563993.6%8s
5125127997.2%12s
1024102415996.1%22s

窗口从256增加到512,频率分辨率翻倍,准确率明显提升;继续增到1024,准确率反而小幅下降。原因就在于时间帧数增多虽然给了BiGRU更长的序列,但窗口过长让时间定位变模糊,快速变化的冲击特征被平均掉了。这个趋势在不同数据集上方向一致,只不过最优窗口位置会随采样率和故障类型漂移。我的建议是:从fs/40到fs/20这个范围试,比如采样率20480Hz,就是512到1024点。

4.2 学习率与批量大小:收敛速度和稳定性的平衡

学习率是最容易踩坑的超参数。用Adam优化器时,初始学习率我测过1e-2、1e-3、1e-4三档。

1e-2的情况下,训练损失在第5个epoch左右就开始振荡,准确率始终在75%附近徘徊,这说明学习率太大,参数在最优解附近来回震荡不收敛。降到1e-3后,损失曲线平滑下降,大约20个epoch准确率突破95%,训练过程稳定。1e-4虽然也能收敛,但要到50个epoch之后才接近95%,太慢了。

批量大小的影响更多体现在稳定性上。我试过16、32、64。32是最平衡的:梯度估计足够稳定,训练速度也快。16的批量小,梯度噪声大,收敛曲线抖动明显,但对小数据集有点正则化效果;64的批量大,趋势平稳,不过在样本量只有几百的情况下容易收敛到尖锐的极小值,泛化反而略差。

实操建议:先固定批量32,学习率从1e-3起步,如果损失曲线前期抖动明显就降半档;如果损失下降地太慢,再考虑增大学习率。

4.3 BiGRU隐藏单元数与过拟合:越大不一定越好

BiGRU隐藏单元数决定了模型对时序信息的记忆容量。隐藏单元太少,模型记不住长距离的依赖关系;太多,容易把训练数据里的噪声也背下来,测试集上反而变差。我测过一组隐藏单元数16、32、64、128的结果:

隐藏单元数测试准确率训练准确率是否过拟合
1694.3%97.8%轻微
3296.1%99.4%轻微
6497.2%100%轻微
12896.7%100%较明显

从结果看,128个隐藏单元在训练集上已经完全记住,但测试准确率反而低于64,说明过拟合已经开始吃掉泛化能力。一个普遍的规律是:隐藏单元数量在不超过训练样本数的1/50到1/100时比较安全。我那个实验训练样本约640个,64个隐藏单元对应的比例大概是1/10,已经偏高了,但因为BiGRU结构本身有门控机制,对过拟合有一定抑制作用,所以实测还撑得住。如果你的数据集更小,建议把隐藏单元降到32,同时加一点dropout,比如bilstmLayer(32, 'OutputMode', 'last')后面接一个dropoutLayer(0.2)。


5. 最容易踩的坑

5.1 数据泄露:跨样本切分为什么会让指标虚高

这个坑我在最早做实验的时候踩过一次,后果是测试准确率高达99%,换到真实现场数据直接掉到80%出头。问题出在数据划分上:我把连续的振动信号先重叠切分,再随机把这些样本分到训练集和测试集。表面上看训练和测试样本是独立的,但实际上它们来自同一段连续信号,相邻样本之间有大量重叠窗口,模型相当于变相看过测试数据的"邻居",指标自然虚高。

正确的做法是按时间段划分:设备开机前3分钟的数据全部进训练集,后面1分钟的数据全部进测试集。或者至少保证同一个连续采集段不会被切到两个集合里。这个原则在论文写作、项目验收里都是硬指标,不提的话审稿人或客户一眼就能挑出问题。

5.2 标准化和归一化的时机

振动信号的幅值在不同工况、不同测点之间差异很大。如果只对每张时频图单独做幅值归一化,等于把所有样本的最大值都拉到1,这会抹掉"这个样本振动能量更大、故障可能更严重"这类重要信息。我一开始犯过这个错,导致模型分不清轻微故障和严重故障,只把类别分对了,严重程度判断不稳定。

正确的流程是:先用整个训练集的幅值统计(一般是均值、标准差)做标准化,再把同样的参数套到测试集上。具体到STFT场景,可以先对原始时域信号做z-score标准化,再做STFT;也可以先算STFT幅值谱,再用全局最大值做归一化。两种方式都能行,重要的是统计量只能来自训练集,不能偷看测试集。

5.3 网络不收敛时的排查顺序

训练时如果发现准确率始终很低、损失不降,千万别一上来就调网络结构。我给自己定了一个排查顺序,基本能解决90%的不收敛问题。

第一步查数据:先把训练集随机抽几个样本,人工看一眼时频图的纹理是否正常、类别标签是否对得上。最常见的问题是数据排序时标签错位了,模型在混乱标签上训练,结果自然乱。

第二步查学习率:把初始学习率设置到1e-3,如果损失前期有下降、后期停滞,说明学习率合适但容量不足;如果从第一步就开始发散,说明学习率过高。

第三步查网络连接:特别是我前面说的CNN和BiGRU衔接部分,确认特征图尺寸是否和sequenceInputLayer的输入维度一致。用size函数逐步打印每个环节输出的维度,是排查维度不匹配最直接的笨办法。

第四步才考虑改结构:加dropout、减层数、增通道,每次只能动一个变量,同时记录实验日志,不然改了什么都记不清。


最后再分享一个实用的小技巧:做这类实验,最好把STFT参数、网络结构、训练参数、测试结果全部写进一个配置脚本里,每次跑完自动保存到带时间戳的文件名里。我早期习惯手动改参数,结果有一天一连跑了十几个实验,回头对比数据时根本分不清哪个结果对应哪组参数,浪费了大量时间。现在我的Matlab工程里所有代码都从同一个config.m读参数,任何结果都能一键复现。对STFT-CNN-BiGRU这套框架,代码本身并不复杂,真正拉开效果差距的,往往是数据划分是否干净、参数是否匹配数据特点、实验过程是否可追溯这些容易被忽略的细节。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询