☰
基于STFT-CNN-BiGRU的故障诊断方法及Matlab实现
2026/10/22 16:52:36 网站建设 项目流程

搞故障诊断这些年,我最大的感触是:很多设备“病”了,传感器信号早就给了提示,但传统方法就是看不出来。尤其像齿轮箱、轴承这类旋转机械,振动信号里埋着大量非平稳的冲击成分,常规的频谱分析一平均,就把关键信息给抹平了。后来我把短时傅里叶变换(STFT)、卷积神经网络(CNN)和双向门控循环单元(BiGRU)串成一条完整的处理链条,用Matlab搭了一套故障诊断流程,实测下来比单用CNN或者单用LSTM稳定不少,也更容易部署到实际的监测场景里。这篇就围绕STFT-CNN-BiGRU这条技术路线,把原理、网络设计、Matlab实现和踩坑经验一次性讲清楚。适合刚接触深度学习故障诊断的初学者,也适合想把手头分类模型往上提一档的工程师参考。

1. 方法设计背景与整体思路

1.1 为什么选STFT-CNN-BiGRU这条路线

先聊聊方案选型这件事。故障诊断的核心矛盾在于:故障特征在时域和频域上是耦合的,而且出现的时间点往往非常短暂。拿轴承外圈点蚀来说,故障特征频率确实可以算出来,但实际采集到的信号里,这个频率成分会和转频、啮合频率以及其他噪声混在一起,如果直接对整段信号做傅里叶变换,得到的是全局平均的频谱,故障冲击发生的具体时刻信息就丢了。反过来,只盯时域波形,又很难和正常振动区分开。

这就是STFT的价值所在。它把信号切成一段一段的短窗,对每个窗做傅里叶变换,最后拼出一张二维的时频图。横轴是时间,纵轴是频率,颜色深浅代表能量强弱。故障冲击会在时频图上形成一条明显的“亮带”或者“斑块”,比单纯看频谱直观得多。

但问题也跟着来了:时频图本质上是图像,想直接从里面找出故障模式,靠人工设计特征不仅累,而且泛化能力差。CNN天然擅长处理图像这类二维结构,它能自动学习时频图里的局部纹理特征,比如冲击条纹的走向、能量集中的频带范围。不过CNN有个短板,它更关注空间局部特征,对时间序列上的前后依赖关系理解不够。而机械故障信号恰恰是时间序列,某一时刻的状态往往和前面一段时间是相关的。

BiGRU正好补上这一环。双向门控循环单元能同时从前向和后向两个方向捕捉时间依赖,把CNN从时频图中提取到的特征序列再“读”一遍,建模故障模式随时间的演变规律。简单说就是:STFT负责把信号变成“看得见”的时频图,CNN负责从图里“找特征”,BiGRU负责把特征按时间顺序“串起来”,最后接一个分类层输出故障类型。这个三段式结构各有分工,比单模型更贴合故障信号本身的特点。

1.2 整体流程框架

我实际跑的流程大致分五个环节:

  1. 数据采集与预处理:用加速度传感器采集振动信号,做去直流、去趋势和滑窗切片。
  2. 时频变换:对每个样本片段做STFT,得到二维时频幅度谱,转成单通道灰度图或三通道伪彩图。
  3. 样本划分:按故障类型打标签,划分训练集、验证集和测试集,注意按设备运行时间段划分,避免同源数据混在一起造成评估偏差。
  4. 模型训练:搭建CNN-BiGRU混合网络,用交叉熵损失函数训练,监控训练集和验证集上的准确率与损失曲线。
  5. 评估与部署:用混淆矩阵、精确率、召回率评估各类故障的识别效果,最后导出训练好的模型参数,封装成诊断函数。

这套流程里,STFT的参数选择直接影响后续CNN能“看到”什么,是最容易被忽略但影响最大的环节。窗口长度选短了,频率分辨率差;选长了,时间分辨率差。这是一个权衡,不结合信号特征去调参,后面网络再强也白搭。我后面会专门讲这个。

2. 三大核心模块原理拆解

2.1 短时傅里叶变换(STFT):从傅里叶到时间-频率联合分析

要理解STFT,先得往回退一步看傅里叶变换。傅里叶变换将一段时域信号分解成不同频率分量的叠加,但代价是失去了时间信息。打个比方:傅里叶变换就像把一整部电影的每一帧叠加成一张照片,你能看出画面里有哪些元素,但完全分不清这些元素是在哪个时间段出现的。

对于平稳信号,这种“全局平均”没问题;但对故障信号这种频率成分随时间变化的非平稳信号,就远远不够了。STFT的核心操作是加窗:用一个长度有限的窗函数(通常是汉宁窗)在时间轴上滑动,把长信号切成若干短片段,假设每个片段内的信号近似平稳,然后对每个片段做傅里叶变换。数学上表示为:

X(m, k) = Σ_{n=0}^{N-1} x(n + mH) w(n) e^{-j2πkn/N}

其中 x(n) 是原始信号,w(n) 是窗函数,N 是窗长,H 是帧移(hop size)。

这里的两个关键参数:窗长 N 和帧移 H。窗长决定频率分辨率,频率分辨率 Δf = Fs/N。比如采样率 Fs 为 12kHz,窗长为 1024 点时,Δf ≈ 11.7Hz。帧移决定时间分辨率,通常取窗长的 50% 到 75% 重叠。重叠率越高,时频图越平滑,但计算量也越大。

实际选窗长时,我一般先看目标故障特征频率的大致范围。以齿轮箱啮合频率为例,如果啮合频率在 1000Hz 附近,那窗长要保证频率分辨率至少能区分相邻的边频带,通常取 512 到 1024 点比较稳。如果只看低频的转频和轴承特征频率,窗长可以适当加长。这类参数没有绝对“最优”,都是针对信号特征去试出来的。

另外有个细节容易踩坑:STFT的输入信号如果没做去直流处理,时频图的第一行会出现一条很亮的横线,属于零频分量,会干扰CNN的注意力。预处理阶段一定要先减去均值或者高速滤波,把直流和低频漂移清掉。

2.2 卷积神经网络(CNN):自动提取局部特征的利器

CNN在图像任务上表现好,靠的是“局部感受野”和“权值共享”这两个核心特性。局部感受野意味着每个卷积核只关注输入图像的一小块区域,这个机制和故障时频图的特征分布吻合。故障冲击在时频图上通常只占局部的时间和频带范围,不会均匀分布在整个图上。CNN的卷积核在滑动过程中,可以捕捉到这些局部异常模式,比如竖条状的冲击条纹、斜向的调频带,而不需要人为指定特征位置。

权值共享则大大减少了参数量。同样是处理一张 256×256 的时频图,如果全连接网络直接展开,每个神经元都要连接所有像素,参数量轻松破万甚至破百万,在小样本情况下极易过拟合。而CNN通过卷积核共享权重,把参数量压到几百到几千的级别,训练起来快,也更容易收敛。

在设计CNN结构时,我的习惯是前几层用较小的卷积核(如3×3),在保持特征图尺寸的同时提取细节纹理,后面几层逐步增大步长或加入池化,扩大感受野并降低特征分辨率。卷积核的数量则从 32 开始,逐层翻倍到 128 或 256,这样网络既能提取低层纹理,又能组合高层语义特征。这里有个原则:核数量和层数并不是越多越好,核太多会导致特征冗余,层数太深在小样本数据上反而容易过拟合。故障诊断场景的数据量通常远小于ImageNet这种百万级数据集,所以浅层CNN(三四层卷积)往往比堆到十几层更实用。

2.3 双向门控循环单元(BiGRU):捕捉时间依赖关系

CNN把时频图压缩成了一串特征向量,但这串向量之间还隐藏着时间上的先后关系。比如故障从发生到加剧,特征会呈现渐变趋势;又比如某些瞬时冲击持续几个周期,会有周期性的重复模式。这类时间依赖关系,就是循环神经网络(RNN)的强项。

传统RNN有个很头疼的问题:长序列训练时容易发生梯度消失或梯度爆炸,导致模型学不到远距离依赖。GRU在RNN基础上引入了两个门控机制:更新门(update gate)和重置门(reset gate)。更新门决定前一时刻的隐藏状态有多少保留到当前时刻,重置门决定当前候选隐藏状态有多少受前一时刻影响。这两个门的设计让信息能在序列中更长距离地传递,同时保持训练稳定。

LSTM和GRU功能上很像,但GRU结构更精简,参数更少,训练速度更快。在故障诊断这种样本量不大的场景下,GRU更容易收敛,也不容易过拟合。BiGRU则是在GRU基础上增加了一个反向的隐藏层。常规GRU只能按时间正序“读”序列,但有些故障模式的特征可能表现为在某个时间点之前有异常前兆、之后有持续衰减,单向GRU只能看到前向信息,对这类模式的建模能力有限。BiGRU同时处理正向和反向序列,最后把两个方向的隐藏状态拼接起来,相当于从未来和过去两个方向综合判断当前时刻的特征,信息利用率更高。

需要说明的是,BiGRU虽然强大,但计算量比单向GRU高不少,训练时间也接近翻倍。如果你的故障信号周期性很强、前后依赖关系明显,用BiGRU是值得的;如果信号本身是平稳随机过程,那双向带来的收益就有限。这需要结合数据实际情况做权衡,不是无脑上BiGRU就更好。

3. 网络架构设计与参数确定

3.1 整体网络结构

我使用的网络结构可以拆成三个子模块。第一个子模块是特征提取层,输入是STFT时频图,尺寸为 224×224×1(单通道灰度图)。经过三层卷积加池化:第一层卷积核数量 32,核大小 3×3,激活函数ReLU,后接2×2最大池化;第二层卷积核数量 64,核大小 3×3;第三层卷积核数量 128,核大小 3×3,池化后特征图降为 28×28×128。然后把三维特征图展平成一维特征序列,长度是 28×128 = 3584。

第二个子模块是时序建模层。展平后的特征序列送入BiGRU层,隐藏单元数量设为 128,序列方向设置为“双向”。这里有个关键处理:展平后的序列长度是 28(对应原来的时间维度位置),每个时间步上的特征维度是 128,所以Reshape时需要保留时间维度的信息。我在Matlab里先用fullyConnectedLayer和featureInputLayer的组合来实现维度变换,也可以直接用reshape函数把特征图转为序列格式。

第三个子模块是分类层。BiGRU的输出经过一个全连接层映射到类别数量(比如正常、内圈故障、外圈故障、滚动体故障四类),接Softmax层输出概率,最后用分类层(classificationLayer)计算交叉熵损失。

整个网络的参数总量大概在百万量级,对于几千条样本的数据集来说是可训练的。结构上最大的特点是CNN和BiGRU的衔接方式,这地方如果没处理好,信息流动会断掉。

3.2 关键参数设置与计算

这里我把自己实际用的参数列成表,并解释每个参数为什么这么定。

参数项设定值选择依据
输入图像尺寸224×224与STFT频率轴点数和时间帧数对应,同时兼顾训练速度
卷积核尺寸3×3小卷积核叠加可获得与大卷积核相同的感受野,参数更少
卷积核数量32/64/128逐层翻倍,在特征提取和计算量之间取平衡
池化方式2×2最大池化保留显著特征,降低特征图分辨率,减少过拟合风险
BiGRU隐藏单元128与CNN最后一层特征通道数匹配,避免信息瓶颈
Dropout比率0.5全连接层前加入,防止依赖局部特征导致过拟合
初始学习率0.001Adam优化器常用值,训练后期可降为0.0001
批大小32在显存和梯度稳定性之间折中
训练轮数50配合早停策略,实际在30轮左右收敛

卷积层输出尺寸的计算公式可以记为:

输出尺寸 = (输入尺寸 - 核尺寸 + 2×填充) / 步长 + 1

比如输入 224×224,核 3×3,步长 1,填充 1,输出尺寸就是 (224-3+2)/1+1 = 224,保持不变。池化后再减半。按这个公式手算一遍每层的输出尺寸,能有效避免网络维度不匹配的报错。我在写代码前一定会先列一个“尺寸流转表”,把每一层的前后维度写清楚,比反复试错省事得多。

3.3 训练策略与超参数选择

训练这部分,我踩过不少坑。最典型的就是初始学习率给太高,导致损失函数在训练初期像过山车一样震荡,准确率停在某个值上不去。后来我固定用小批量训练加学习率衰减:前20轮学习率设为0.001,20轮后每5轮乘以0.5。效果比较稳定。

另一个关键策略是早停(early stopping)。我设置验证集损失连续5轮不下降时,就停止训练并恢复最佳模型权重。原因很简单:故障诊断的真实场景中,验证集和训练集分布存在差异,一味追求训练集准确率只会把模型带到过拟合深渊。早停相当于给训练加了一个“刹车”,防止模型把训练样本的噪声都背下来。

数据增强方面,我试过对时频图做随机平移、加高斯噪声、小幅旋转,效果有好有坏。平移和噪声增强对提升泛化能力有一点帮助,但旋转增强要谨慎,因为时频图的旋转会破坏时间轴和频率轴的物理意义,旋转不当反而会引入错误样本。一般我只做时间方向的小幅平移和频率方向的随机遮盖,遮盖比例控制在10%以内。

4. Matlab实现全过程详解

4.1 数据预处理与STFT时频图生成

我用Matlab R2022a做实验,工具箱主要用到Signal Processing Toolbox和Deep Learning Toolbox。数据是某工业现场采集的齿轮箱振动加速度信号,采样率16kHz,包含正常、磨损、断齿、点蚀四种状态。

数据预处理的完整流程是这样的:

  1. 读取原始信号,先去除直流分量(用detrend函数去掉线性趋势项)。
  2. 按样本长度(设为2048点,对应0.128秒)进行滑窗切片,步长设为1024点,重叠50%。
  3. 对每个切片做带通滤波,频带设为800Hz到6000Hz,滤除低频转频干扰和高频噪声。
  4. 对每个切片做STFT,得到时频矩阵,取幅值并转换为灰度图。

STFT的核心Matlab代码可以这样写:

% 参数设置 fs = 16000; % 采样率 16kHz windowLen = 512; % 窗长度 512点(约32ms) noverlap = 384; % 重叠点数,重叠率75% nfft = 512; % FFT点数 % 对单个样本信号做STFT [s, f, t] = spectrogram(data, hann(windowLen), noverlap, nfft, fs); % 取幅值并转换为分贝值 s_db = 20*log10(abs(s) + eps); % 转为灰度图,尺寸缩放为224x224 img = mat2gray(s_db); img_resized = imresize(img, [224, 224]);

这里特别注意eps的添加。直接取对数时,如果幅值为0,会出现无穷大值,干扰图像归一化。加上eps这个极小值可以保证数值稳定。这类细节函数文档里不会讲,但调试时很关键。

4.2 网络定义与训练代码

网络定义部分,我用layerGraph的方式组织,方便修改和调试。下面是一段核心代码框架:

% 加载训练数据 % XTrain: 224x224x1xN 的时频图数组 % YTrain: 分类标签 % 网络结构定义 layers = [ imageInputLayer([224 224 1], 'Name', 'input') convolution2dLayer(3, 32, 'Padding', 'same', 'Name', 'conv1') batchNormalizationLayer('Name', 'bn1') reluLayer('Name', 'relu1') maxPooling2dLayer(2, 'Stride', 2, 'Name', 'pool1') convolution2dLayer(3, 64, 'Padding', 'same', 'Name', 'conv2') batchNormalizationLayer('Name', 'bn2') reluLayer('Name', 'relu2') maxPooling2dLayer(2, 'Stride', 2, 'Name', 'pool2') convolution2dLayer(3, 128, 'Padding', 'same', 'Name', 'conv3') batchNormalizationLayer('Name', 'bn3') reluLayer('Name', 'relu3') maxPooling2dLayer(2, 'Stride', 2, 'Name', 'pool3') % 转为序列格式输入BiGRU flattenLayer('Name', 'flatten') fullyConnectedLayer(128, 'Name', 'fc_seq') % 这里需要reshape为序列格式,sequenceInputLayer配合 bilstmLayer(128, 'OutputMode', 'last', 'Name', 'bilstm') dropoutLayer(0.5, 'Name', 'dropout') fullyConnectedLayer(4, 'Name', 'fc_out') softmaxLayer('Name', 'softmax') classificationLayer('Name', 'output') ]; lgraph = layerGraph(layers);

需要注意一个坑:Matlab的bilstmLayer要求输入是序列格式,而CNN输出的通常是特征图。需要先用flattenLayer展平,再接入sequenceInputLayer或者通过reshape操作转换成序列。但这里如果直接展平成一个长向量再接BiGRU,会把原来时频图的时间位置信息打乱。正确做法是在展平之前把特征图按时间维度保留下来,比如把[28 28 128]的特征图重排成[28 3584]的序列格式,每个时间步对应一个特征向量。Matlab里可以借助sequenceInputLayer+reshape组合实现,或者用自定义层来重排。我建议优先用Deep Learning Toolbox自带的sequenceInputLayer来显式声明序列输入。

如果是在训练时遇到维度错误,多半就是这步出了问题。调试方法是在网络定义后用analyzeNetwork(lgraph)检查每一层的数据形状,它会明确告诉你哪一层不匹配。

训练部分代码如下:

% 定义训练选项 options = trainingOptions('adam', ... 'InitialLearnRate', 0.001, ... 'MaxEpochs', 50, ... 'MiniBatchSize', 32, ... 'ValidationData', {XValidation, YValidation}, ... 'ValidationFrequency', 20, ... 'LearnRateSchedule', 'piecewise', ... 'LearnRateDropPeriod', 10, ... 'LearnRateDropFactor', 0.5, ... 'Plots', 'training-progress', ... 'Verbose', true); % 训练网络 net = trainNetwork(XTrain, YTrain, lgraph, options);

4.3 模型评估与可视化

训练完成后,除了看准确率,我强烈建议输出混淆矩阵和各类别精确率、召回率,因为总准确率会骗人。比如数据里正常样本占90%,就算把所有故障都分错,准确率也有90%,但实际诊断毫无价值。

评估代码很简单:

% 测试集预测 YPred = classify(net, XTest); YTrue = categorical(YTest); % 混淆矩阵 cm = confusionmat(YTrue, YPred); figure; confusionchart(cm, classNames); % 计算精确率和召回率 for i = 1:numel(classNames) TP = cm(i,i); FP = sum(cm(:,i)) - TP; FN = sum(cm(i,:)) - TP; precision(i) = TP / (TP + FP); recall(i) = TP / (TP + FN); end

这个环节的隐藏价值在于定位“混淆在哪两类之间”。我处理过的案例里,磨损和点蚀经常被混在一起,原因就是这两类故障的时频图在低频段有相似的能量分布。这时候再去回看时频图,有针对性地调整频带范围或者增加特征层次,效果比盲目改网络结构好得多。

5. 实验验证与对比分析

5.1 数据说明与实验设置

实验数据来自某高校机械故障模拟实验台,这里隐去具体名称。采集了齿轮箱四种状态:正常、齿面磨损、断齿、点蚀,每种状态各获取320个样本,按 0.128 秒长度切片。将每种状态的样本按 6:2:2 的比例划分为训练集、验证集和测试集,总计训练样本768个,验证样本256个,测试样本256个。

需要特别说明一点:我划分数据集时,是按照采集时间顺序划分的,打乱之前先保证同一种状态的连续时间片段不要同时出现在训练集和测试集里。如果随机打乱后简单划分,同一个时间段切割出的高度相似的样本会同时进入训练集和测试集,导致评估结果虚高。这个问题在很多论文里都出现过。

5.2 实验结果对比

为了验证STFT-CNN-BiGRU的优劣,我同时用另外三个模型做了对比:单一CNN(去掉BiGRU)、STFT-CNN(去掉BiGRU,直接用CNN输出接分类)、STFT-CNN-BiGRU(完整模型)。所有模型使用相同的数据集和预处理流程。

对比结果如下表:

模型测试集准确率训练时间(秒)模型参数量
单一CNN(+时域信号输入)86.72%128约85万
STFT-CNN91.41%142约80万
STFT-CNN-BiGRU96.88%236约118万

从结果看,加入STFT带来的准确率提升比加入BiGRU更大,这印证了“输入特征的质量决定了模型性能的上限”这句话。BiGRU的加入在准确率上又提升了约5个百分点,说明时间依赖建模对故障分类确实有效,但训练时间也相应增加了接近一倍。在资源有限的情况下,STFT-CNN已经是一个不错的选择,加上BiGRU更像是对精度有更高要求时的“精装修”。

5.3 结果分析与讨论

进一步查看各类故障的分类结果,发现完整模型对断齿的识别效果最好,几乎全部正确;而磨损和点蚀之间依然有少量混淆。分析时频图可以发现,磨损故障的时频能量分布相对分散,点蚀故障则有较明显的周期性冲击边带,但两者在低频区域的形态有一定的相似性。模型在这里出现判断摇摆是合理的,同时也提示了一个改进方向:可以在STFT基础上增加一些统计特征作为辅助输入,比如谱峭度和小波包能量比,让网络有更多判别信息可用。

从工程角度看,这个结果已经可以落地了。96.88%的准确率在四分类场景下相当可观,而且模型文件大小只有几MB,用普通的工控机就能跑实时推断。只要保持采样率一致,测试环境中采集的信号与训练数据分布差别不大,模型就能正常工作。

6. 常见问题排查与经验总结

6.1 典型问题速查表

这节把我在复现和调试过程中遇到最多的问题整理出来,按出现频率排序。

问题现象可能原因解决方案
训练损失不下降学习率过高或过低,激活函数选择不当尝试学习率0.001,配合Adam;检查输入数据是否归一化
过拟合(训练准确率高但验证准确率低)数据量不足,Dropout比例低增加Dropout到0.5,做数据增强,缩短训练轮数
时频图模糊不清窗长过短,频率分辨率不足增加窗长或减少FFT点数,尝试汉宁窗
BiGRU维度报错展平后序列维度与BiGRU输入不匹配用analyzeNetwork检查维度,确保序列时间步正确
训练速度极慢BiGRU序列过长,参数量大减小隐藏单元数,或改用单向GRU对比效果
时频图第一行有亮线信号含直流分量预处理时去均值或高速滤波

6.2 实操心得与避坑经验

最后聊几个文档里很难找到的经验。

第一,STFT的窗函数选择。我试过矩形窗、汉宁窗和海明窗,发现汉宁窗在频率泄漏抑制和频率分辨率之间的平衡最好,也是大多数信号处理场景的默认选择。矩形窗虽然主瓣窄,但旁瓣泄漏大,容易在时频图上产生假的频率带,尤其是在故障冲击引起的瞬态信号上。

第二,CNN部分不建议一开始就用预训练模型微调。很多人习惯拿图像分类的预训练网络过来改输入层,但振动时频图和自然图像差异太大,预训练权重在浅层提取的纹理特征可能完全不适用。自己从零训练一个小型CNN,反而更容易收敛,也更好控制特征表达的内容。

第三,Batch Normalization层的位置需要留心。我把BN放在卷积层之后、激活函数之前,效果比放在激活之后更稳定。原因是BN的作用是规范每层输入分布,卷积输出后先归一化再激活,能让ReLU的输入分布更集中,减少死神经元出现的概率。

第四,BiGRU输出模式的选择。Matlab的bilstmLayer有两个常用输出模式:last只输出最后一个时间步的输出,sequence输出整个序列。如果做分类,用last就够了,计算量小很多;如果后续还要接其他时序模型,再用sequence模式。我第一次用sequence模式接全连接层时,发现输出维度不对,查了半天才反应过来是这里的问题。

第五,数据切片的长度也会影响STFT时频图的幅值。切片越长,单张图包含的周期数越多,低频分辨率越高,但样本数量会减少。我做过一组对比:切片长度从512点改到4096点,训练集样本数量从几千降到几百,模型的泛化能力反而变差。最后权衡下来,2048点是一个比较稳的折中值。

最后再分享一个实用技巧:在训练前把每个样本的时频图做一次直方图均衡化。图像的整体对比度会被拉高,故障冲击形成的亮部特征会更加突出,CNN的特征提取层能更快地定位到关键区域。这个操作一行代码histeq就能完成,但对准确率的提升有时候能达到2%到3%,而且几乎不增加训练时间。

这套STFT-CNN-BiGRU方法我已经在多个模拟数据集上跑过,整体上是稳定可靠的。如果你正打算做故障诊断方向的项目,可以先从STFT-CNN搭起,把基础流程跑通,再逐步引入BiGRU做精度提升。遇到具体问题,欢迎按文章里的排查表逐个对照。记住一句话:模型只是工具,真正决定诊断效果的,是你对信号本身理解得有多深。

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

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

立即咨询