资讯动态

CNN结合LSTM的电机故障诊断:振动信号深度学习实战

发布时间:2026/10/17 8:14:06 来源:尧图企业网站定制
电机故障诊断这件事我做了快三年才慢慢摸到门路。最早的时候全靠人工看振动频谱图眼都看花了不说现场的老师傅和算法工程师之间还经常吵起来——一个说这明显是轴承外圈故障另一个说谱线太乱特征频率都看不清最后只能拆机检查说了算。后来我开始尝试用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_size1024, stride512): 读取原始振动信号文件切分成样本并标准化。 raw_file: 每行一个采样点的一维数组文件 window_size: 窗口长度 stride: 滑动步长小于window_size则窗口重叠 data np.loadtxt(raw_file, dtypenp.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, dtypenp.float32)步长选了512也就是窗口之间重叠50%在数据量和样本独立性之间取了个平衡。如果步长太小比如64样本量大增但相邻样本几乎相同训练时间变长而且容易过拟合。3. CNN结合LSTM的模型设计每一层选择背后都有交代3.1 整体结构一览我最终采用的网络结构不算复杂但每一层都是踩过几个轮子的方案后定下来的。整体架构分四段输入层1D序列长度1024CNN特征提取段第一层Conv1D 64个卷积核卷积核尺寸32激活函数ReLU后接MaxPooling1D池化窗大小2和BatchNormalization第二层Conv1D 128个卷积核卷积核尺寸16同样接池化和批归一化LSTM时序段一层LSTM128个单元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_sequencesFalseCNN输出经过池化后还是一个序列这个序列长度取决于输入长度和各层池化的累乘效果。1024的长度经过两层池化变成256左右每个位置对应128维特征向量。这256个时间步的特征向量送给LSTM让它去学习每个时段提取出来的冲击特征在时间维度上如何先后出现、重复变化。我用了单层128单元的LSTM没有叠第二层。两层LSTM的确能建模更高层级的抽象但对这个任务收益不大反而训练时间翻倍、参数量暴涨小数据集上更容易过拟合。128个单元的LSTM已经足够记住整个256步的特征概要因为轴承故障的冲击周期通常就落在几步到几十步的范围内。return_sequences设成False这样LSTM只输出最后一个时间步的隐藏状态相当于把整段序列的信息浓缩成一个128维向量然后接全连接层。如果设成True输出是(256, 128)的序列全连接层不好接而且大多数情况下你不需要保留下游输出序列。3.5 全连接层与DropoutLSTM后面我加了一层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_classes5): input_layer layers.Input(shapeinput_shape) # CNN段提取局部冲击波形特征 x layers.Conv1D(filters64, kernel_size32, strides1, paddingsame, activationrelu)(input_layer) x layers.BatchNormalization()(x) x layers.MaxPooling1D(pool_size2, strides2)(x) x layers.Conv1D(filters128, kernel_size16, strides1, paddingsame, activationrelu)(x) x layers.BatchNormalization()(x) x layers.MaxPooling1D(pool_size2, strides2)(x) # LSTM段学习冲击模式的时间依赖 x layers.LSTM(units128, return_sequencesFalse)(x) x layers.Dropout(0.5)(x) # 分类段 x layers.Dense(units128, activationrelu)(x) x layers.Dropout(0.5)(x) output layers.Dense(unitsnum_classes, activationsoftmax)(x) model models.Model(inputsinput_layer, outputsoutput) 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。设成patience10也就是验证集loss连续10轮没有改善就停止训练。这省去了人工观察判断差不多收敛了的过程而且能有效防止过拟合。我训练时通常在30到50个epoch就触发早停。下面是一个标准的训练配置代码from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau callbacks [ ReduceLROnPlateau(monitorval_loss, factor0.5, patience5), EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue) ] model.compile( optimizertf.keras.optimizers.Adam(learning_rate0.001), losscategorical_crossentropy, metrics[accuracy] ) history model.fit( x_train, y_train, batch_size64, epochs80, validation_data(x_val, y_val), callbackscallbacks )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_weightclass_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, axis1) y_true_class np.argmax(y_val, axis1) 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 混淆矩阵里藏着判断模型好坏的关键训练完成后我在时序划分的验证集上得到的结果如下实际类别正常内圈故障外圈故障滚动体故障转子不平衡召回率正常198010199.0%内圈故障019031197.4%外圈故障141891096.9%滚动体故障226180592.3%转子不平衡102219397.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实现起来不算难关键是把数据准备和训练评估这两关做扎实。一个能用的模型加上可靠的数据流基本上就能应对大部分常见电机故障诊断的场景了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑