前阵子做设备故障诊断项目时,我一直在琢磨怎么把振动信号里的故障特征挖得更彻底。试了很多路子,最后稳定下来的一套方案就是STFT+CNN+LSTM:短时傅里叶变换(STFT)把一维振动信号变成二维时频图,卷积神经网络(CNN)负责从时频图里抓局部纹理和空间特征,长短期记忆网络(LSTM)再在时间维度上补一刀,把故障演化的前后关联捞出来。这篇文章就把这套方案的思路、参数、代码和坑都摊开聊一聊。适合正在做旋转机械故障诊断、振动信号分析,或者想入门深度学习信号处理的工程师参考,尤其是打算用Matlab快速验证算法的朋友。
1. 整体设计与思路拆解
故障诊断这事,本质上是一个模式识别问题:拿到一段振动信号,判断设备处于正常状态还是某种故障状态,更进一步的还要区分故障类型和严重程度。传统做法是先人工设计特征——时域的均值、峰值、峭度,频域的幅值谱、功率谱,然后丢给SVM或者随机森林去分类。这套路子的核心痛点在于特征工程太依赖经验:换了设备、换了工况,之前好用的特征可能就失效了。所以这两年更多人倾向于让模型自己从原始信号里学特征,这就是STFT-CNN-LSTM这类混合模型派上用场的原因。
为什么偏偏是STFT、CNN、LSTM这三个东西组合在一起?其实它们的角色非常明确,各管一段:
- STFT负责把信号变成“图”。原始振动信号是一维的时间序列,直接丢给CNN不是不行,但CNN天生擅长处理二维的网格数据,比如图像。STFT把一维信号展开成时间-频率二维平面,相当于把“什么时候出现什么频率成分”这个信息完整画出来,CNN就有东西可看了。
- CNN负责“看图找特征”。时频图上有能量集中的亮斑、有边纹、有频谱泄漏的纹理,这些局部模式对应着不同的故障机理。CNN通过卷积核滑动扫描,能从图中自动学习这些判别性特征,而且对平移、轻微变形有不错的鲁棒性。
- LSTM负责“串联时间信息”。故障信号不是静态的,轴承的磨损有个逐渐发展的过程,齿轮的裂纹会在每个啮合周期里反复冲击,这些在时频图上表现为某种随时间变化的规律。LSTM擅长捕捉序列依赖,把CNN提取到的特征按时间顺序串联起来,就能学到“故障是怎么演变”的信息,对提升分类精度和泛化性都有帮助。
从整个模型结构看,它的设计逻辑是“局部特征+全局时序”的两级结构。CNN看的是局部,LSTM看的是全局。只用CNN,容易忽略信号片段之间的时间依赖;只用LSTM,又难以高效提取频域纹理特征。两者互补,这就是混合模型的核心价值所在。
还有一点值得说:这套方案对现场工程也很友好。很多时候我们采集到的振动信号是非平稳的,转速波动、负载变化都会让信号特性漂移。STFT天然适合非平稳信号的分析,它假设信号在短时间窗内近似平稳,把一段长信号切成很多小段逐段分析,所以即便工况有起伏,只要窗足够短,还是能稳住分析结果。这也是它比单纯FFT更适合实际场景的原因。
方案选型时我也对比过其他路线,比如直接用连续小波变换(CWT)做时频分析,或者用纯CNN做端到端诊断。CWT的时频分辨率确实灵活,但计算量比STFT大不少,而且小波基的选择本身又引入了新的超参数。纯CNN则是因为少了对时序信息的建模,在故障早期、特征还比较微弱的时候容易误判。相比之下,STFT-CNN-LSTM在计算成本、效果、工程实现难度之间是比较均衡的。Matlab里从信号处理到深度学习的两套工具箱都有,深度集成,代码写起来很顺手。
1.1 用混合模型而非单一模型的原因
单一模型的效果天花板其实很明显。我一开始试过直接拿原始一维振动信号喂给LSTM做分类,训练时间倒是不长,但准确率只有九成出头。表面看还行,一分析混淆矩阵就发现问题了——轻度磨损和正常状态之间经常互相误判。原因就在于原始信号里噪声干扰多,LSTM虽然能建模时序,却很难自动把频带特征分离干净。反过来只上CNN,输入STFT时频图,局部特征抓得不错,可是每个时间片是独立看待的,看不到故障特征在连续多个时间片里的演化趋势,精度同样卡在某个瓶颈上。两个模型搭在一起,等于把各自的盲区补上了。
1.2 适用于哪些场景
这套模型主要面向旋转机械的故障诊断,比如轴承的滚动体故障、内圈故障、外圈故障,齿轮箱的齿面磨损、断齿,电机的转子不对中、不平衡等。凡是能通过振动信号反映故障特征、并且故障信号带有一定时间演化趋势的场景,都适合用这套方案。对于变速变载荷的复杂工况,STFT的时间局部化特性也能兜住。当然,如果是诊断那种故障特征本身就是纯稳态、跟时间变化毫无关系的情况,比如某固定频率的持续共振,那LSTM带来的收益就不明显,用STFT+CNN就够了。
2. 核心细节解析与实操要点
2.1 STFT的关键参数:窗长、重叠率与FFT点数
STFT是所有环节的地基,地基打不好,后面的CNN再强也白搭。STFT的核心思想很简单:把信号切成很多小段,对每一段加窗后做傅里叶变换,得到该时刻附近的频谱,把所有时刻的频谱拼在一起就形成了时频图。
参数决定了时频图的质量,最核心的有三个:
- 窗长(Window Length)。窗越长,频率分辨率越高,但时间分辨率越差;窗越短则相反。这是STFT最经典的权衡取舍。对轴承故障诊断来说,故障冲击的瞬态成分往往很短暂,窗太长了会把冲击和背景混合得模糊不清。经验上,如果采样率是12kHz,取1024个点的窗长,频率分辨率能到12kHz/1024≈11.7Hz,时间分辨率大约85ms,对于多数轴承故障特征频率已经够用。
- 重叠率(Overlap)。相邻窗之间的重叠比例,常见的有50%、75%、90%。重叠率越高,时频图越平滑,时间上的信息保留越完整,代价是计算量增大。一般建议至少50%,追求细节就上75%。
- FFT点数(NFFT)。通常直接取和窗长一致,也可以填充零到下一个2的幂次,但填充零不会提升真实分辨率,只会让频谱插值更平滑,图的视觉效果更好。
我实际用Matlab时一般这么写:
fs = 12000; % 采样率 12kHz winLen = 1024; % 窗长 overlap = round(winLen * 0.75); % 75% 重叠 nfft = winLen; % FFT点数 [S, F, T] = spectrogram(x, hann(winLen), overlap, nfft, fs); SdB = 20*log10(abs(S) + eps); % 转成分贝刻度注意这里用了汉宁窗(hann)。为什么不用矩形窗?矩形窗的频谱旁瓣泄漏太严重,会让主频周围的能量散开,时频图上出现虚假的条纹。汉宁窗主瓣稍宽但旁瓣衰减快,对振动信号这种多频成分叠加的情况更友好。如果对幅值精度要求高,可以用海明窗;如果主要关注瞬态冲击,也可以试试凯泽窗。
2.2 对输出做归一化
时频图直接作为CNN输入之前,归一化这步不能省。不同样本之间的信号幅值差异可能很大,比如负载不同、传感器灵敏度不同,直接喂给网络会让训练不稳定。我习惯把每张时频图映射到0到1区间:
S_norm = (SdB - min(SdB)) / (max(SdB) - min(SdB));也有人用z-score标准化,效果差不多。但要特别注意:归一化的统计量应该从训练集算出来,再应用到验证集和测试集,而不是每个样本独立归一化。否则会把样本的绝对能量差异抹掉,而能量差异本身有时也是故障判别的信息之一。
Hmm,其实这个说法要区分两种情况。如果样本间幅值差异主要来自工况变化、传感器灵敏度,那逐样本归一化没问题,能消除无关干扰;如果幅值本身携带故障严重程度的信息,那就该用全局统一的缩放。我做实验时是先观察数据分布再定的,通常振动故障数据用逐样本归一化问题不大。
2.3 CNN网络设计要点
CNN的作用是从时频图提取特征。网络设计有几个要点:
- 输入尺寸。Matlab里可以用augmentedImageDatastore统一尺寸,我最常把输入调成224×224×3或者128×128×3。224×224是很多经典网络的默认尺寸,效果好但计算量大一点;128×128更轻量,项目初期调试建议先用小的。
- 卷积核大小。第一层用3×3的小卷积核就够,堆叠多层来增大感受野,而不是一开始就用大核。小核参数少、非线性更强,是一种更现代的实践。
- 激活函数。推荐ReLU或者LeakyReLU。ReLU计算简单、收敛快;LeakyReLU能避免某些神经元死掉,适合时频图里纹理稀疏的情况。
- 池化层。2×2最大池化是标配。它本质上是下采样,保留最显著的特征,同时减少计算量,还能让网络对微小的平移不那么敏感。
下面是一个典型的CNN特征提取部分:
layersCNN = [ imageInputLayer([128 128 3], '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') ]; % 在最后接一个卷积层,输出特征图数对应LSTM的时间步维度注意中间加了batchNormalizationLayer。这个层能加速收敛、提高稳定性,对训练深度 CNN很有帮助。做故障诊断时数据量通常没那么大,BN的正则化效果能让模型泛化得更好。
2.4 LSTM的时序建模与网络衔接
CNN输出的是一系列特征图,怎么把二维特征图喂给LSTM?常见的有两种衔接方式:
- 方案A:将特征图按时间维度展开。如果输入时频图的时间轴对应LSTM的时间步,可以在CNN的末端对频率维做全局池化,保留时间轴上的一维特征序列。也就是说,每一列特征向量作为LSTM的一个时间步输入。
- 方案B:分帧处理。把原始信号分成多段,每段做STFT后分别过CNN,得到一组特征向量序列,再输给LSTM。这种方案保持了信号段的边界,时序逻辑更清晰,但工程实现更复杂。
我在Matlab里执行方案A时,结构上用的是sequenceInputLayer配合卷积层——这里有个细节需要注意。Matlab的Deep Learning Toolbox在使用sequenceInputLayer时,每个时间步的输入可以是一个图像序列,也就是说可以把mini-batch设置为“序列的图像”。如果完全用layerGraph搭建,可以将CNN层加在sequenceInputLayer之后,然后再接扁平化和LSTM。但这么做其实不太直观,更常见的做法是:
- 对每个样本,STFT生成时频图;
- 将时频图切成多个时间片段,每个片段输入CNN提取特征;
- 把每个片段得到的特征向量按时间顺序排列成特征序列;
- 特征序列送入LSTM。
实际操作上,如果你不想自己写数据流,也可以直接用Matlab的sequenceFoldingLayer和sequenceUnfoldingLayer来实现“CNN+LSTM”的图层级组合。sequenceFoldingLayer把序列折叠成批量图像输入CNN,CNN处理完了再通过sequenceUnfoldingLayer恢复序列结构,然后送入LSTM。这个设计我在实际项目里用过,代码非常清爽。
LSTM层的参数设置方面,我建议:
- 隐藏单元数通常取50到200之间,根据分类难度和特征维度调整。分类数少、特征已经很好分,取小值就够了;多了反而容易过拟合。
- 时间步数等于时频图切片的段数。时间步太长,LSTM训练困难;太短则丢掉时序信息。16到32个时间步是好用的区间。
- Dropout默认加在LSTM层之后,0.2到0.5之间。这个正则化手段对防止过拟合特别有效,尤其是样本量不多的时候。
3. 数据准备与预处理实战
3.1 数据从哪里来
做故障诊断实验,最理想的是从试验台实测采集数据,但很多同学没有这个条件,那就用公开数据集。公开的轴承故障数据集挺多,一般包含正常状态、滚动体故障、内圈故障、外圈故障等类别,每种类别下还分了不同的故障直径和负载工况。采样率、转速这些参数在数据集文档里都有说明,动手之前先把数据集的说明文档读一遍,能省不少弯路。
我自己的经验是先用小数据集跑通流程,比如每个类别挑几十个样本,确认模型能收敛、准确率有上升趋势,再上全量数据。一上来就全量训练,出了bug都不好排查。
3.2 振动信号分帧与样本增强
原始信号是连续的长序列,不能整段丢进模型,需要做分帧。比如数据集的每段信号包含几十万个采样点,直接做STFT会产生很大的时频图,算不动也没必要。我通常的做法是滑窗截取:
- 窗口长度取0.5到1秒的振动信号,即fs×0.5到fs×1个采样点;
- 滑动步长取窗口长度的一半,相邻样本有50%重叠;
- 每个原始样本可以截出多个子样本,这本身就是一种简单的数据增强。
有个值得留意的点是,训练集和测试集必须从不同的信号段里截取,或者至少保证测试集的样本在时间上不与训练集重叠。否则模型学到的可能是同一个噪声片段的特征,验证结果虚高,部署到现场就现原形。
3.3 生成时频图数据集
分帧后的每段信号,做STFT生成对应的时频图,以图像形式存下来。Matlab里可以用spectrogram函数不用画图,直接拿矩阵数据,然后通过mat2gray归一化,再resize到统一尺寸。存的时候注意类别标签要跟上。
我写数据预处理脚本时的流程是这样的:
% 假设data是一个结构体,data.signal存放原始信号,data.label存放标签 for i = 1:length(data) x = data(i).signal; [S,~,~] = spectrogram(x, hann(winLen), overlap, nfft, fs); SdB = 20*log10(abs(S) + eps); img = mat2gray(SdB); % 归一化到0-1 img = imresize(img, [128 128]); % 统一尺寸 imwrite(img, sprintf('imgs/class%d_%d.png', data(i).label, i)); end这样每个样本就对应一张时频图,接下来就可以用imageDatastore读取和打标签。
4. Matlab代码实现:从网络搭建到训练
4.1 完整网络结构搭建
我的方案A的完整网络结构大致如下——每个样本是一张完整的时频图,CNN从图中提取出沿着时间轴排列的特征序列,再交给LSTM分类。核心代码如下:
numClasses = 4; % 四类故障 numHiddenUnits = 128; % LSTM隐藏单元数 numFeatures = 64; % CNN最后一层输出的特征通道数 % 主网络 layers = [ imageInputLayer([128 128 1], 'Name', 'input') % CNN部分 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, numFeatures, 'Padding', 'same', 'Name', 'conv3') batchNormalizationLayer('Name', 'bn3') reluLayer('Name', 'relu3') % 将二维特征图折叠为序列特征 sequenceFoldingLayer('Name', 'fold') % LSTM部分 lstmLayer(numHiddenUnits, 'OutputMode', 'last', 'Name', 'lstm') dropoutLayer(0.3, 'Name', 'drop') % 分类部分 fullyConnectedLayer(numClasses, 'Name', 'fc') softmaxLayer('Name', 'softmax') classificationLayer('Name', 'output') ]; lgraph = layerGraph(layers); % 用序列折叠层需要把CNN部分加载到sequenceInputLayer上 % 这里需要小心连接关系的调整,通常做法是: inputSeq = sequenceInputLayer([128 128 1], 'Name', 'seq_input'); lgraph = layerGraph(inputSeq); lgraph = addLayers(lgraph, layers(2:end)); % ... 根据实际依赖关系连接坦白说,Matlab里sequenceFoldingLayer和sequenceUnfoldingLayer的层图连接第一次接触会比较绕。sequenceFoldingLayer的输入是“序列的图像”格式,所以网络最前面是sequenceInputLayer;经过折叠后变成batch图像,进入CNN。CNN处理之后要把多张特征图对应的时间片恢复出来,用sequenceUnfoldingLayer实现。这里我建议你在Matlab文档里搜索“Sequence Folding”的官方示例先跑一遍,理解它的数据流方向,再迁移到自己的模型上。我记着才会这么倒腾一遍,真要下手时先localSession里跑个最小样例验证一下网络能通过层级检查(analyzeNetwork)再做训练。
如果觉得自己写数据流更可控,也可以完全不依赖sequenceFoldingLayer,用手工分帧的方式:把信号切成一个个短段、每个短段生成时频图、每个时频图过一遍CNN提取特征向量,再把时间顺序排列的特征向量矩阵喂给LSTM。这种方式代码更朴素,调试更容易,适合第一次搭这套模型的朋友。
4.2 训练选项设置
训练参数的经验值也可以直接抄:
options = trainingOptions('adam', ... 'InitialLearnRate', 0.001, ... 'MaxEpochs', 30, ... 'MiniBatchSize', 32, ... 'ValidationData', {valImgs, valLabels}, ... 'ValidationFrequency', 10, ... 'Shuffle', 'every-epoch', ... 'Plots', 'training-progress', ... 'Verbose', true, ... 'ExecutionEnvironment', 'auto');几个参数的理由:
- adam是自适应学习率优化器,故障诊断这类中小规模数据集上基本无脑选它就行,收敛快、超参不敏感。
- 初始学习率0.001是我觉得最稳的起点。太高容易震荡,太低训练半天不长进。后面如果loss卡住了,可以再用learning rate scheduler衰减,比如每10轮乘以0.1。
- MiniBatchSize 32是显存和梯度稳定性之间的平衡。如果显存不够就降到16,再不够得把输入图像尺寸调小。
- ValidationFrequency设成每几个iteration打一次,方便在训练过程中观察有没有过拟合。
训练完保存模型:
trainInfo = trainNetwork(trainImgs, trainLabels, lgraph, options); save('stft_cnn_lstm_model.mat', 'trainInfo');4.3 测试与评估
测试阶段最粗暴的指标是整体准确率,但故障诊断更看重的是每个类别的识别率,以及到底哪些类别容易混淆。所以混淆矩阵和F1分数必须算。
YPred = classify(net, testImgs); YTest = testLabels; accuracy = sum(YPred == YTest) / numel(YTest); figure; confusionchart(YTest, YPred);看混淆矩阵有个经验:如果发现两个故障类别频繁互相错分,除了调模型,先回头看看它们的时频图像长什么样。很多时候是故障特征频率比较接近,或者载荷工况影响了特征分布。这种情况调模型不如调数据:检查标签是否准确、样本是否均衡、时频图参数是否合适。
5. 常见问题与排查技巧实录
5.1 训练不收敛或准确率上不去
遇到这种情况,我的排查路径是固定的:
- 先检查数据。把时频图可视化出来看看,信号里的故障特征在不在图上,还是被噪声淹没了。这个问题最常见,尤其是窗外得太大把瞬态冲击抹平了。解决办法是缩短窗长、提高重叠率,宁可牺牲一点频率分辨率也要保住时间细节。
- 再检查标签对齐。如果预处理脚本里有任何偏移或者截取错误,标签错位了,模型就会学到一堆噪声。我会随机挑一批样本核对信号强度和标签类别是否匹配。
- 最后才调模型。降低学习率、增加训练轮次、调整卷积层数,这些是最后的手段。模型结构能跑通的情况下,问题大概率在前面两步。
5.2 过拟合:训练精度高、测试精度低
样本量不足是核心原因。处理思路按优先级排列:
- 加数据增强:对时频图做小幅随机旋转、水平翻转、加小噪声、随机裁剪。注意有些增强要克制,比如大幅旋转会扭曲频率轴的意义,对诊断不利。
- 加大Dropout:把LSTM后面的dropout从0.3提到0.5,CNN那边也可以加SpatialDropout。
- 加入L2正则:在fullyConnectedLayer或lstmLayer里设置'l2Regularization'参数。
- 减少模型容量:剪掉一个卷积块,或者把LSTM隐藏单元从128降到64。模型能力过剩、数据量喂不满时,减容量往往比加正则更有效。
5.3 STFT时频图模糊或看不出特征频率
这是预处理阶段最常出的问题,多半是窗长选得不对。窗长太长,频率分辨率高但时间模糊,瞬态冲击看不清;窗长太短,频率分辨率低,两个接近的故障特征频率被糊成一条。我把不同窗长下的时频图并排画出来对比过,效果差异肉眼可见。解决方法是给出一组窗长候选(512、1024、2048),在验证集上各测一版精度,选最优的。没有一成不变的万能参数,这是个实验活。
5.4 LSTM训练很慢
LSTM的循环结构天然比纯CNN慢,尤其是时间步长较长时。优化手段:
- 减小时间步数,而不是减小隐藏单元数。时间步缩短能直接降低展开长度。
- 检查是否在GPU上训练,Matlab的ExecutionEnvironment设置成'auto'会自动检测。
- 降低MiniBatchSize,显存不够时能减少OOM。
- 将时频图分辨率从224降到128,输入小了训练速度快很多,准确率往往降不了太多。
速度这块实测下来,128×128输入配合时间步32和隐藏单元128,在单张消费级显卡上训练几十轮也就几分钟量级,属于完全可接受的范畴;CPU上会明显慢,建议训练阶段优先GPU。
5.5 常见报错速查
| 报错/现象 | 可能原因 | 解决办法 |
|---|---|---|
| 维度错误(Incompatible input sizes) | 时频图尺寸与imageInputLayer不一致,或折叠层接线有问题 | 用analyzeNetwork检查网络结构,统一输入尺寸 |
| OOM内存不足 | MiniBatchSize太大或输入图分辨率过高 | 减小batch、缩小图片、清空已加载变量 |
| 训练loss为NaN | 学习率过高或数据里有极端值 | 降低学习率,检查归一化是否做好 |
| 验证集准确率波动大 | 验证集样本太少,或训练/测试分帧重叠 | 增加验证集样本量,重新划分数据集 |
| 类别严重不均衡 | 故障样本远多于正常样本,或反之 | 用加权损失函数,或对少数类做过采样 |
6. 从实验台到现场:部署落地需要注意的事
实验室跑通只是第一步,实际部署要考虑的问题更多。Matlab训练出来的模型可以导出为MAT文件,在Matlab Runtime环境下跑推理,也可以转换成C/C++代码集成到边缘设备里。但部署之前有几个工程问题值得提前想清楚:
传感器安装位置和采样率会直接影响效果。模型在试验台数据上学到的特征,换一套传感器、换个安装位置,分布就可能漂移。这时候最好的办法是采集一部分现场数据做微调(fine-tuning),冻结CNN的前几层,只微调后面几层,能省不少标注数据。
工况变化是常态。现场设备的转速、负载不会像试验台那么稳定,这就意味着信号的统计特性在变。有两种应对思路:一是训练数据里尽量覆盖多种工况,让模型学会“工况不变性”特征;二是在前端加一个工况识别模块,按工况选择不同的诊断模型。前者简单,后者效果更好但工程量大。
推理延迟。如果是要做在线监测,每个周期都要出诊断结果,就必须计算STFT和网络推理的总耗时。STFT本身很快,主要瓶颈在CNN和LSTM。如果延迟达不到要求,考虑把LSTM的时间步缩短,或者改用更轻量的CNN结构,甚至用蒸馏的方式训练一个小模型替代大模型。
这些坑我基本都踩过一遍。比如最开始只拿着试验台数据训练,一到现场就掉点,后来加入了少量现场数据微调才稳住。部署的时候也是在嵌入式环境里发现推理太慢,被迫把模型从224×224换成128×128输入,精度掉了一个百分点但速度翻了两倍。
7. 一个更接近实用的小建议
如果你现在准备动手做这个项目,我的建议是先别急着把STFT-CNN-LSTM完整跑起来。先用Matlab里的预训练网络,比如直接用resnet18替换自建的CNN部分,把时频图喂进去看看能达到什么精度,这算是一个快速baseline。如果这个baseline已经能满足你的精度要求,那投入产出比最高的方案其实是STFT+预训练CNN,LSTM带来的提升可能不值得那部分额外训练时间和工程复杂度。如果baseline不够,再往上叠LSTM,这样你能清楚地看到每个环节的真实贡献。
另外,时频图的“好看程度”和模型准确率并不总是正相关。我见过一些时频图肉眼看起来密密麻麻全是噪声,模型照样能分得很准;也见过看起来特征清晰的图,模型就是学不出来。模型有自己的判别逻辑,没有必要在可视化上花太多时间精力追求美观,数据处理好、网络结构合理,效果自然就出来了。
写到这里,这套STFT-CNN-LSTM的基本面貌应该已经清楚了。从信号变成图像、再从图像抽出序列特征、最终分类,每一步都有明确的物理含义和工程考量。如果你也在做类似项目,希望这些经验能帮你少踩几个坑。尤其是预处理阶段多花点心思检查数据,后面训练模型时你会感谢当时的自己。