电机故障诊断这件事,我做了快三年才慢慢摸到门路。最早的时候全靠人工看振动频谱图,眼都看花了不说,现场的老师傅和算法工程师之间还经常吵起来——一个说"这明显是轴承外圈故障",另一个说"谱线太乱,特征频率都看不清",最后只能拆机检查说了算。后来我开始尝试用Python写深度学习模型,把采集到的电机振动信号直接扔进神经网络里做自动分类,用的就是CNN结合LSTM这套组合。
先说结论:这个方案在实测数据上能把轴承内圈故障、外圈故障、滚动体故障、转子不平衡这些常见故障类型的识别准确率做到96%以上,比传统频谱人工判读稳定得多,关键是不需要每次让专家盯着谱图反复比对。这篇文章我就把从数据准备、模型搭建到训练调参踩过的坑一次性讲清楚,代码基于TensorFlow/Keras,都是可以直接跑起来复现的。不管你是做设备维护的工程师想转型搞算法,还是刚入门想找个完整的深度学习实战项目练手,这篇都能帮上忙。
1. 为什么是CNN结合LSTM:电机故障信号里藏着一空间一时间两类特征
1.1 传统振动诊断的瓶颈在哪
电机出故障时,轴承损伤、转子磨损这些机械问题会直接反映在振动信号上。比如轴承外圈有裂痕,每次滚动体经过裂痕区域就会产生一次冲击,这个冲击的间隔由轴承几何尺寸和转速决定,换算成频率就是特征频率。理论上只要在频谱图上找到对应的峰值,就能判断故障类型。
问题是现场环境远没这么理想。电机旁边可能还有其他设备在振动,齿轮箱的啮合频率、皮带轮的旋转频率都会混进来,有时候特征频率被噪声淹没,有时候出现的是倍频、边带这些复杂形态。我曾经处理过一组数据,外圈故障的特征频率明明只有120赫兹左右,但频谱图上120赫兹处几乎看不出来,反而是它的二倍频、三倍频异常突出。靠人工找这些规律,既费时又容易漏判。
深度学习解决这个问题的思路很直接:不手工设计特征了,让网络自己从原始波形里学。你只要准备足够多的"正常"和"各类故障"的振动数据,模型就能自动找出那些和故障类型强相关的信号形态。
1.2 CNN负责"看形状",LSTM负责"记规律"
那为什么要把CNN和LSTM拼在一起用?这得从两类特征说起。
电机振动信号从本质上是时间序列,但它在局部又表现出明显的形态特征。举个例子,滚动体经过轴承损伤点时会产生一个短暂的冲击衰减波形——冲击瞬间幅值突然变大,然后快速衰减振荡几下。这种"局部波形形状"和卷积神经网络的思路天然匹配。CNN里的卷积核就是一个小窗口,它在信号上滑动,专门找"哪里有突然突起的尖峰"、"哪里有周期性重复的衰减振荡"这类局部模式。
但光做局部特征提取还不够。故障导致的冲击不是一次性的,是周期性重复出现的,而且相邻冲击之间的间隔本身就携带了故障类型信息。这就是LSTM的用武之地。LSTM专门处理有先后顺序的数据,它能记住"前面一段波形大概是什么样",然后结合当前信息判断"这次冲击和上次冲击的间隔是否符合某种故障的节奏"。
我用一个不那么严谨但很好理解的类比:CNN像是看单个汉字的结构,横竖撇捺组合成偏旁;LSTM像是读整句话的语序,知道"虽然...但是..."这种前后文的逻辑关系。电机振动信号就是一句话,单独看一个字可能判断不了语义,但把局部字形和整句语境结合起来就能准确理解。
1.3 纯CNN和纯LSTM的短板在哪
我也试过只用CNN或者只用LSTM的效果,这里直接给结论:
- 纯CNN(比如直接用几层Conv1D接全连接):对局部冲击波形识别很灵敏,但容易忽略故障冲击的周期性规律。因为没有时序记忆能力,它把每个局部窗口当成独立片段处理,某种程度上丢掉了"上一段信号和下一段信号的关系"。
- 纯LSTM(直接接原始振动数据):理论上能处理长距离依赖,但实际训练时有两个问题。一是收敛慢,LSTM的参数更新比卷积要复杂得多;二是对局部特征不敏感,因为LSTM门控机制倾向于记住"整体变化趋势",而故障冲击这种高频、短促的变化容易被平滑掉。
CNN结合LSTM的结构就顺理成章了:前面用卷积层把原始振动信号里那些局部冲击模式提取成特征图,压缩掉冗余信息,再把特征序列交给LSTM去学习故障冲击的时间依赖关系。各干各的活,组合起来效果最好。
2. 数据准备与预处理:原始振动信号不是直接喂给网络的
2.1 数据来源与工况说明
我先说明一下实验数据的背景。用的是现场采集的电机振动加速度信号,具体来说是某型三相异步电机,额定转速1480转每分钟(约24.7赫兹),采样率设置为12kHz。这个采样率带来的好处是每个旋转周期能采到约480个点,足以捕捉轴承故障那些几百赫兹的高频冲击成分。
故障类型共5类:正常状态、轴承内圈故障、轴承外圈故障、轴承滚动体故障、转子不平衡。每种状态下连续采集多段数据,每段时长10秒,然后按每个样本1024个采样点(大约85毫秒)做切分。之所以选1024这个长度,一是因为2的幂次在后续卷积池化过程中不容易出现尺寸对不上的问题;二是因为在1480转这个转速下,1024个点大约对应两个完整旋转周期,一个样本里既包含局部冲击形态,又包含至少两到三次冲击间隔信息。
有一个关键细节必须提醒:切分时样本之间要有重叠率。我用的是随机采样窗口而不是连续切分,也就是每个窗口的起始位置在原始信号上随机选,这样相当于在有限的数据量里通过数据增强的方式扩充了训练的样本数量。最终每类状态获得约800个样本,5类合计约4000个。
2.2 数据标准化的细节
把原始信号直接输入神经网络是可以的,但实际效果会差不少,原因有两个:
第一,振动加速度信号的幅值范围受采集设备灵敏度和电机负载影响,不同批次数据之间可能有量级差异。比如同一个电机空载和满载时振动幅值能差出好几倍,如果不做标准化,模型可能把"幅值大小"当成了判断依据,而不是"波形形态",这就非常危险。试想一下,训练时故障样本幅值都偏大,模型可能会学到"幅值大就是故障",到了现场换了一台电机,幅值整体偏小,正常信号和故障信号全都识别不出来了。
我的做法是逐样本标准化:每个1024点的样本,减去自身均值,再除以自身标准差。这样每个样本都变成零均值、单位方差的标准分布,幅值差异被抹平,模型只能去学波形形状,这是比较稳妥的方案。
第二,特征缩放会影响梯度下降的收敛速度。标准化之后数据分布集中在零附近,优化器每一步更新都更平稳,配合后面要说的学习率设置,训练loss下降明显比不标准化要快。
2.3 训练集与验证集划分时最容易踩的"数据泄漏"坑
很多人拿着振动信号直接随机划分训练集和测试集,结果效果奇好,模型准确率99%。这其实是个陷阱:同一段连续信号切分出来的相邻窗口高度相似,如果一部分进了训练集,邻居进了测试集,模型等于提前"看过答案"了。
正确的做法是按连续数据段划分,而不是按样本划分。也就是说,先把每类状态下的原始10秒数据分成前8秒训练、后2秒测试,然后再分别切分窗口。这样训练集和测试集来自不同的时间段,评估结果才有可信度。实际工程中更严格的做法是采集新的数据做独立测试集,但至少要保证时间上不重叠。
我最初就在这上面吃过亏,用随机划分跑了第一次实验,测试集准确率97%,心里还挺美。后来发现同一时段的样本其实被切分进了两个集合,改用时序划分后准确率降到91%,这才是真实的性能水平。这里的教训是:模型的泛化能力永远比训练集上的好看数字更重要。
2.4 输入格式
假设每个样本是一维序列,长度1024,那么输入张量形状是(batch_size, 1024, 1)。这里最后一维是通道数,就像图像是(height, width, channels)一样,一维信号是(length, channels)。原始振动信号只有一个通道。后续卷积层会生成多个通道,就相当于把原始波形加工成了多种"特征视图"。
预处理完的数据我用numpy数组存成.npy文件,这样读入速度比逐条读原始文本快很多。整个数据准备流程大概长这样:
import numpy as np def preprocess_and_generate_dataset(raw_file, window_size=1024, stride=512): """ 读取原始振动信号文件,切分成样本并标准化。 raw_file: 每行一个采样点的一维数组文件 window_size: 窗口长度 stride: 滑动步长,小于window_size则窗口重叠 """ data = np.loadtxt(raw_file, dtype=np.float32) n = len(data) samples = [] start = 0 while start + window_size <= n: # 取出一个窗口并标准化 window = data[start: start + window_size] window = (window - window.mean()) / (window.std() + 1e-8) samples.append(window) start += stride return np.array(samples, dtype=np.float32)步长选了512,也就是窗口之间重叠50%,在数据量和样本独立性之间取了个平衡。如果步长太小(比如64),样本量大增但相邻样本几乎相同,训练时间变长而且容易过拟合。
3. CNN结合LSTM的模型设计:每一层选择背后都有交代
3.1 整体结构一览
我最终采用的网络结构不算复杂,但每一层都是踩过几个轮子的方案后定下来的。整体架构分四段:
- 输入层:1D序列,长度1024;
- CNN特征提取段:第一层Conv1D 64个卷积核,卷积核尺寸32,激活函数ReLU,后接MaxPooling1D池化窗大小2和BatchNormalization;第二层Conv1D 128个卷积核,卷积核尺寸16,同样接池化和批归一化;
- LSTM时序段:一层LSTM,128个单元,return_sequences设为False,接Dropout(0.5);
- 分类输出段:Dense层128个神经元激活ReLU,接Dropout(0.5),最后输出层5个神经元用softmax。
下面我把每一层为什么要这么选说清楚。
3.2 第一卷基层:卷积核尺寸为什么要取32而不是3或5
图像领域卷积核常用3×3,因为图像像素的局部相关性在3个像素范围内就能捕捉。但一维振动信号不一样,采样率12kHz意味着相邻两个采样点之间只相隔约0.083毫秒,普通机械冲击的持续时间大约是几十个采样点。用3或5的卷积核,相当于只看一个极小窗口内有没有尖峰,缺点是容易把噪声尖峰也当成有效特征,而且感受野太小,看不到冲击波形"先上升后衰减"的整体形态。
卷积核尺寸取32,意思是每个卷积操作覆盖32个连续采样点,大约2.7毫秒的时间范围。这个长度刚好能包住一次典型轴承冲击的上升沿和衰减段的大部分,又不会长到把两个相邻冲击混在一起。第一层64个卷积核相当于用64个不同特征的模板去扫描整个波形,有的模板关注"有没有大的正向尖峰",有的关注"负向衰减形态",有的关注"基线有没有周期性波动",模板越多提取到的东西越丰富。
这里也提醒一下,卷积核尺寸不是越大越好。我刚试过64和128的尺寸,发现两层卷积之后特征图尺寸缩水太快,后面LSTM能看到的序列长度就不够了,反而影响效果。
3.3 池化和批归一化的放置顺序
很多人在CNN里贪省事写成卷积后直接接池化再接ReLU,顺序错了影响不大,但有个更稳的惯例:卷积 -> 批归一化 -> ReLU -> 池化。批归一化放在ReLU之前,好处是让激活前的数据分布保持稳定,ReLU的负区间不会被极端值淹没。
在实践中我遇到过一种典型症状:不接BatchNormalization时,训练没几个epoch就出现loss变成NaN的情况,罪魁祸首是振动信号里有少数幅值特别大的瞬态冲击,经过卷积层放大后数值溢出。加上BatchNormalization之后,数值稳定性好了很多。
MaxPooling1D池化窗大小取2、步长2,也就是每两个点取一个最大值,长度直接减半。池化的意义不仅仅是降维,更关键的是提供一定程度的平移不变性——一小段波形整体前后移动几个采样点,卷积输出的最大值仍然大致停留在池化后特征的某个位置,这能让模型面对轻微相位差异的同类故障时依然做出相同判断。
3.4 LSTM部分:为什么只加一层,为什么return_sequences=False
CNN输出经过池化后还是一个序列,这个序列长度取决于输入长度和各层池化的累乘效果。1024的长度经过两层池化变成256左右,每个位置对应128维特征向量。这256个时间步的特征向量送给LSTM,让它去学习"每个时段提取出来的冲击特征,在时间维度上如何先后出现、重复变化"。
我用了单层128单元的LSTM,没有叠第二层。两层LSTM的确能建模更高层级的抽象,但对这个任务收益不大,反而训练时间翻倍、参数量暴涨,小数据集上更容易过拟合。128个单元的LSTM已经足够记住整个256步的特征概要,因为轴承故障的冲击周期通常就落在几步到几十步的范围内。
return_sequences设成False,这样LSTM只输出最后一个时间步的隐藏状态,相当于把整段序列的信息浓缩成一个128维向量,然后接全连接层。如果设成True,输出是(256, 128)的序列,全连接层不好接,而且大多数情况下你不需要保留下游输出序列。
3.5 全连接层与Dropout
LSTM后面我加了一层128维的全连接层,再Dropout(0.5),然后softmax输出。Dropout的用意是强制网络不要过度依赖某几个特定的特征组合。比如模型如果死记"故障样本的第三通道卷积特征值一定大于某个数",训练集上表现很好但换了工况就不行。Dropout随机屏蔽一半神经元,逼着模型学会用冗余的、分散的特征做综合判断,这是对抗过拟合很重要的一招。
这里有个小技巧:Dropout通常放在全连接层之前或之后,但不要放在LSTM内部,除非你用了Keras的recurrent_dropout参数。我在LSTM里开过0.2的recurrent_dropout,效果反而变差,后来就只在外层做Dropout。
3.6 完整模型代码
import tensorflow as tf from tensorflow.keras import layers, models def build_cnn_lstm_model(input_shape=(1024, 1), num_classes=5): input_layer = layers.Input(shape=input_shape) # CNN段:提取局部冲击波形特征 x = layers.Conv1D(filters=64, kernel_size=32, strides=1, padding='same', activation='relu')(input_layer) x = layers.BatchNormalization()(x) x = layers.MaxPooling1D(pool_size=2, strides=2)(x) x = layers.Conv1D(filters=128, kernel_size=16, strides=1, padding='same', activation='relu')(x) x = layers.BatchNormalization()(x) x = layers.MaxPooling1D(pool_size=2, strides=2)(x) # LSTM段:学习冲击模式的时间依赖 x = layers.LSTM(units=128, return_sequences=False)(x) x = layers.Dropout(0.5)(x) # 分类段 x = layers.Dense(units=128, activation='relu')(x) x = layers.Dropout(0.5)(x) output = layers.Dense(units=num_classes, activation='softmax')(x) model = models.Model(inputs=input_layer, outputs=output) return model model = build_cnn_lstm_model() model.summary()整个模型参数量约18万,在现在的电脑上训练一轮也就是十几秒的量级。这个规模的参数量对这个任务来说正好,太多会过拟合,太少学不到足够特征。
4. 训练过程中的优化策略与踩坑实录
4.1 优化器与学习率:固定学习率是最容易犯的错
我用的是Adam优化器,初始学习率0.001。Adam的好处是自带调节每个参数步长的能力,对刚入门的人来说比SGD调起来省心,不至于一步迈太大直接飞出收敛区域。但要注意,Adam不是不需要学习率调整,只是对学习率的敏感度低一些。
实际训练中如果固定0.001跑完整个流程,前30个epochloss下降比较顺,后面就会进入一个平台期,loss在0.15附近卡住。这时候靠人工调整非常麻烦,所以我直接在Keras里加了ReduceLROnPlateau回调——当验证集loss连续5个epoch不下降时,学习率乘以0.5。这样训练到中后期学习率自动降到0.00025左右,loss还能再往下走一段。
另一个我强烈建议开启的是EarlyStopping。设成patience=10,也就是验证集loss连续10轮没有改善就停止训练。这省去了人工观察判断"差不多收敛了"的过程,而且能有效防止过拟合。我训练时通常在30到50个epoch就触发早停。
下面是一个标准的训练配置代码:
from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau callbacks = [ ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=5), EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True) ] model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss='categorical_crossentropy', metrics=['accuracy'] ) history = model.fit( x_train, y_train, batch_size=64, epochs=80, validation_data=(x_val, y_val), callbacks=callbacks )4.2 Batch Size的影响:64是个好折中
Batch size选64,这个值我前后对比过16、32、64、128四种。64在效果和训练速度之间最平衡。
Batch size太小时(比如16),每个batch包含的样本太少,梯度估计噪声大,loss曲线抖动幅度很夸张,训练过程不稳定。这是一个很典型的困惑来源——有阵子我loss在0.6到0.8之间反复横跳,错以为是学习率太大,实际是batch size太小导致梯度方向不稳定。
Batch size太大时(比如128或256),每个epoch的梯度更新次数少,模型收敛速度变慢,而且大batch size在GPU上会吃掉不少显存,影响你同时跑对比实验。另外在某些数据集上大batch size会收敛到较差的局部极小值,因为梯度方向过于平滑。
4.3 类别不平衡与样本权重
5类故障样本数量如果大致均衡,训练起来相对轻松。但实际工况里"正常状态"的样本往往非常多,特定故障的样本反而少。假如某类故障只有其他类的三分之一,模型为了降低整体loss会倾向于把所有不确定样本往"数量多的类别"上猜,这会严重影响少数类的召回率。
处理类别不平衡有几个常用的手段:
- 使用class_weight给少数类更大的权重,让模型在训练时更关注这些样本;
- 对少数类做过采样,比如带重复地抽取少数类样本;
- 用数据增强生成更多少数类样本,比如给振动信号加噪声、时间伸缩(拉伸或压缩少量)。
在代码实现上最简单的方案是class_weight,可以在训练时指定:
class_weight_dict = { 0: 1.0, # 正常 1: 1.5, # 内圈故障(样本略少) 2: 1.5, # 外圈故障 3: 2.0, # 滚动体故障(样本最少) 4: 1.2 # 转子不平衡 } model.fit(..., class_weight=class_weight_dict)需要注意的是,class_weight的值不是越大越好。权重太大,模型对少数类样本几乎"死记硬背",验证集准确率反而下降。我用1.5到2.0的范围内比较合适。
4.4 训练中一个容易被忽视的点:验证集指标别只看Accuracy
刚开始我只看准确率,直到有一次发现模型对"滚动体故障"几乎完全识别不出来,但整体准确率依然有94%。这是因为滚动体故障只占总样本的20%,模型全部猜成其他类别,整体准确率也不至于太差。
后来我把评估指标改成每个类别的精确率、召回率和F1分数,配合混淆矩阵一起看。这样每个类别都暴露出来,不会让个别类的表现被总数掩盖。这里给一段评估代码参考:
from sklearn.metrics import classification_report, confusion_matrix, f1_score y_pred = model.predict(x_val) y_pred_class = np.argmax(y_pred, axis=1) y_true_class = np.argmax(y_val, axis=1) print(classification_report(y_true_class, y_pred_class, target_names=['normal', 'inner_race', 'outer_race', 'rolling', 'unbalance'])) print(confusion_matrix(y_true_class, y_pred_class))我当时用这个方式自查,发现滚动体故障的F1值只有0.72,明显低于其他类别。原因后来分析是滚动体故障的特征频率不固定——滚动体在旋转过程中与内外圈的接触点位置不断变化,故障冲击会产生"游移",LSTM对这种不稳定周期建模比固定周期的内外圈故障困难一些。这个问题通过加强滚动体故障样本的数量和多样性得到一定缓解,但依然是最难识别的类别。
4.5 一个完整的训练时长参考
我这个实验在单张普通消费级GPU上跑(8GB显存),训练5类数据、每类800个样本左右,一个epoch大约5秒左右,整个早停流程训练到第40个epoch停止,总耗时不到4分钟。CPU跑的话会慢很多,但也能跑,每epoch可能要半分钟以上。对小数据集来说,跑起来完全没有压力。
5. 实验结果全分析与模型可解释性思考
5.1 混淆矩阵里藏着判断模型好坏的关键
训练完成后,我在时序划分的验证集上得到的结果如下:
| 实际类别 | 正常 | 内圈故障 | 外圈故障 | 滚动体故障 | 转子不平衡 | 召回率 |
|---|---|---|---|---|---|---|
| 正常 | 198 | 0 | 1 | 0 | 1 | 99.0% |
| 内圈故障 | 0 | 190 | 3 | 1 | 1 | 97.4% |
| 外圈故障 | 1 | 4 | 189 | 1 | 0 | 96.9% |
| 滚动体故障 | 2 | 2 | 6 | 180 | 5 | 92.3% |
| 转子不平衡 | 1 | 0 | 2 | 2 | 193 | 97.5% |
整体准确率96.7%,确实能用了。但你看滚动体故障那一行,有6个样本被误判成外圈故障,5个被判成转子不平衡。这类错误不是模型"瞎猜",而是物理上有原因的:滚动体故障初期,冲击能量弱,信号信噪比低,前端的卷积层提取出来的特征可能和外圈故障早期的冲击波形很像;而滚动体故障引起的振动幅值在旋转周期上变化,这和转子不平衡的周期性调制特性有些类似,网络区分起来自然吃力。
这说明一个很重要的原则:深度学习模型不是在创造什么超自然的判别能力,它只是从数据里找统计学规律。如果两类故障产生的物理信号形态本来就接近,模型误判是正常的,这时候你需要做的是提供更多样的故障样本,或者从物理上先排除这类接近的故障形态。
5.2 可视化卷积层到底学到了什么
模型跑通了以后,我很好奇卷积层到底提取了什么特征。我把训练好的第一层64个卷积核的权重可视化出来,大致能看出一些规律:
- 有一部分卷积核呈现出明显的带通滤波特性,只在某个频段上有响应,相当于自动学会了定位特定频率的振动成分;
- 另一些卷积核在时域上表现为"单个冲击响应"的样子,看到短暂的正向脉冲就激活;
- 还有少量卷积核表现出差分特性,对信号的突变敏感,这类核通常在检测冲击起始点。
直观理解就是,卷积层确实没有在"死记硬背",而是在学习振动诊断里常用的频段分析和冲击检测操作。这种感觉相当踏实的,说明模型并没有偷懒走捷径。如果你想复现这个可视化过程,思路是提取第一层卷积核的权重向量,然后用matplotlib画出来看看。
5.3 模型部署到现场的几个现实问题
准确率96%在实验室数据上看着不错,但真正到车间部署,还得面对几个我之前没预料到的问题。最大的问题是工况变化——转速变了、负载变了,信号特征就跟着变。训练时用1480转的数据,现场如果换了变频电机,转速在500到3000转之间波动,模型准确率立刻掉到80%以下。
解决思路有两个方向:一是训练数据覆盖面尽量广,把不同转速、不同负载的数据混在一起来训练;二是输入端做更鲁棒的特征,同时保留原始波形和幅度谱、功率谱,让模型有更多判据。最简单的办法是增加训练数据的多样性,但现实里数据采集成本高,尤其故障数据不是随时随地都能碰到的,很多时候得靠实验室模拟故障。
另一个现实问题是现场信号的污染。车间里电磁干扰、电缆振动、周围设备的机械冲击都会叠加到传感器信号上。模型如果只见过实验室的干净数据,到了现场会出现大量"未知样本"被高置信度地分类成某一类故障。解决办法是在模型里加一个"未知类别"的拒识机制,比如用softmax概率阈值——最大概率低于0.6就判为"无法确认",不强行下结论。这种做法在诊断场景里比追求100%分类更靠谱,因为误报一个故障可能引发不必要的停产检修。
6. 进一步的改进方向与我的经验总结
6.1 从数据与模型两个方向继续提升
如果已经跑通了上面的版本,还想继续提升性能,我个人建议按优先级做三件事:
第一,先扩数据而不是先改模型。通常模型结构挖到头也就涨一两个百分点,但把训练数据从每类800个样本扩到2000个以上,准确率往往直接涨三到五个点。故障诊断的数据增强可以做很多花样,比如加不同信噪比的高斯白噪声、随机时移、频率微扰,这些都比换网络结构成本低收益高。
第二,尝试把频谱作为辅助输入。我做过一个变体实验:模型输入不再只是原始时域波形,而是把原始波形和计算出的功率谱特征拼接成双通道输入,CNN同时处理两类信息。这样做的好处是既保留了时域冲击的相位信息,又引入了频域的稳定特征。实验结果准确率能再提升1到2个百分点,尤其是对滚动体故障这种难分类型有明显改善。
第三,换更轻量的注意力机制。CNN结合LSTM在这个任务里已经表现不错,但LSTM有个先天特点是"记性"有限,序列太长时容易忘掉前面某个关键位置。后来我在LSTM后面加了一个简单的注意力层,让模型自己去关注整段序列里最关键的几个时间步。这个改动不仅让准确率又提升了一个点,而且让模型更稳定。
6.2 最后分享一点自己的体会
做电机故障诊断项目,最大的坑不在于模型怎么调,而在于数据到底靠不靠谱。我遇到过数据标签标错了,导致模型把"不平衡故障"学成了"正常"的情况;也遇到过采样率设置不合理,采集到的数据根本覆盖不了故障高频成分的情况。这些问题都不是调参能解决的,所以我现在的流程是:先花80%的时间把数据搞明白,再花20%的时间跑模型。
另外就是不要被单次实验的高准确率冲昏头脑。你做一次随机划分,准确率98%,换一套数据重新训练,可能就只有90%。要评估模型的稳定性,最稳的办法是做几次不同随机种子下的重复实验,看指标均值和方差。如果方差太大,说明模型对数据划分敏感,这时候优先要解决的是让训练数据更多样,而不是纠结结构细节。
这个CNN结合LSTM的框架用Python实现起来不算难,关键是把数据准备和训练评估这两关做扎实。一个能用的模型,加上可靠的数据流,基本上就能应对大部分常见电机故障诊断的场景了。