☰
CNN-LSTM轴承故障诊断全流程:振动信号处理、模型训练与部署
2026/10/1 1:17:33 网站建设 项目流程

简介:面向轴承故障诊断课程设计与入门实践的完整项目包,聚焦滚动轴承外圈、内圈、滚珠三类故障在3种直径组合下共9种工况的识别问题,基于振动信号实现智能分类。压缩包内代码完整、注释细致,包含搭建CNN-LSTM网络的Python源码、训练好的模型权重、数据加载与划分脚本、测试脚本以及独立项目说明,读者可借助CSV数据文件和MAT原始数据直接运行训练与测试流程,快速复现结果并理解从数据处理到模型评估的完整链路。资源共25个文件,主要涵盖9个CSV数据、4个Python源码、2个PTH模型文件,另含ipynb示例、Excel结果表、多张示意图与Matlab预处理脚本,整包约53.21MB,目录层次清晰,便于按模块学习和二次开发。目前已有122人学习,适合需要完成相关课程设计、准备故障诊断实验或入门深度学习在机械状态监测中应用的读者。

1. 基于cnn-lstm的轴承故障诊断到底解决什么问题

产线上一根电机轴承悄悄磨出裂纹,振动波形在高频段多了几个白尖脉冲,等检修发现时轴已经滚出金属屑,整个工位停机两小时。传统做法依赖人工听音和包络谱分析,但在变频器、齿轮箱混叠的现场噪声里,故障特征往往被盖得严严实实。基于cnn-lstm的轴承故障诊断,核心思路是让CNN从原始振动波形里提取冲击形态的局部特征,再让LSTM捕获这些微缺陷在时间轴上的演化记忆,最终输出正常、滚珠故障、内圈故障、外圈故障这类类别。它不需要人工设计频带和阈值,一次训练后就能端到端判别,相比单一CNN在负载波动时更稳。适合做设备预测性维护的工程师、自动化专业的学生,以及正在评估“要不要上深度学习诊断方案”的现场技术负责人。

2. 振动信号如何变成CNN-LSTM能吃的输入:序列切分与时频特征

2.1 原始振动信号为什么要分帧:1024点的滑动窗口与重叠率

传感器采回来的是连续一维时间序列,几秒钟就几十万个采样点,直接整体塞进网络既不现实也没必要。常见做法是用滑动窗口把长信号切成定长片段,每个片段独立作为一个样本。窗长一般取轴承旋转周期内能包含多次冲击的数值,比如采样率12.8kHz时,取1024个点约80ms,高速轴承一圈只有几个毫秒,一个窗口能覆盖十几次滚珠通过缺陷位置的冲击过程。

切分时重叠率很影响样本量和信息完整性。重叠率0表示窗口首尾相接,样本量最小;重叠率0.5表示窗口每次平移半个窗长,样本量翻倍;重叠率0.75样本量更大,但相邻样本高度相关,训练时容易把相似样本同时划进训练集和验证集,导致准确率虚高。我一般选0.5作为默认值,需要更多样本时提到0.75,同时配合后续的按段划分策略。

import numpy as np def make_samples(raw_signal, seq_len=1024, overlap=0.5): step = int(seq_len * (1 - overlap)) samples = [] for start in range(0, len(raw_signal) - seq_len, step): window = raw_signal[start:start + seq_len] samples.append(window) return np.asarray(samples)

这段函数把一维长信号切成二维数组,shape为[样本数, seq_len]。注意step的计算方式,overlap为0.5时step等于512,相邻窗口有一半数据是重复的。切完之后还需要做归一化,因为加速度传感器读数可能是正负几十的重力加速度值,不归一化会让CNN的卷积核在初始阶段偏向数值大的通道,收敛变慢。

归一化建议按单个窗口做z-score,而不是按整段信号做。因为现场信号是非平稳的,设备启动阶段和稳定运行阶段的幅值差异很大,按整段全局归一化会让早期窗口的均值和方差失真,按窗口归一化每个样本自己独立尺度,模型对幅值波动的适应性更强。代码里用(window - np.mean(window)) / (np.std(window) + 1e-6)即可,加一个极小值防止静默段标准差为0导致除零。

2.2 用短时傅里叶做时频图,还是直接用一维波形

这是上手工特征还是端到端学习的分叉口。用短时傅里叶把一维波形转成二维时频图,再用CNN做图像分类,在实验室数据集上效果直观,故障频带在图上很亮,但到了现场有两个麻烦:一是每次要做FFT,推理延迟和预处理复杂度上升;二是时频图的分辨率依赖窗函数和频带范围,转速变化时同一故障的频谱位置会漂移,模型的鲁棒性反而差。

直接用一维波形则完全绕开频带设计,卷积核在数据驱动下自己学会提取冲击特征。实现上只需要把波形维度从(1024,)改成(1024,1)送进Conv1D,代码更简洁。从源码包的设计习惯看,用一维波形是主流,因为轴承故障诊断的落地瓶颈从来不是“特征不够明显”,而是“特征随工况漂移”,让模型直接学原始形态比人工定义频带更抗漂移。

有一些折中方案可以选择:如果现场转速非常稳定,可以把原始波形叠加一个阶比跟踪——先测出转频再按转频重采样。但这个步骤依赖转速计信号,多数产线没有。对纯振动数据方案,一维波形输入是鲁棒性最高、工程成本最低的选择。

2.3 数据集划分的边界坑:按“段”划分,而不是按“点”划分

准备数据集时最常见的返工点是train_test_split把同一个连续信号的窗口同时分进了训练集和测试集。假设一段10分钟的外圈故障信号切成1100个窗口,随机按8:2划分,训练集和测试集会各自包含同一时刻附近的窗口,彼此高度相似,验证准确率能到99%,但到了现场预测新数据就滑到80%以下。这种数据泄露是隐蔽的,因为在指标层面看一切正常。

正确做法是把原始信号先分成若干独立段,比如每段4秒,然后按段编号划分训练集和测试集,确保同一物理过程的窗口绝不出现在两侧。源码里一般会按文件或传感器子目录划分,拿到数据先看目录结构。可以用如下方式实现段级划分:

from sklearn.model_selection import train_test_split seg_ids = np.arange(num_segments) train_segs, test_segs = train_test_split(seg_ids, test_size=0.2, random_state=42) train_windows = [] for seg in train_segs: train_windows.extend(make_samples(signals[seg], seq_len=1024, overlap=0.5)) test_windows = [] for seg in test_segs: test_windows.extend(make_samples(signals[seg], seq_len=1024, overlap=0.5))

这里先对“段”做划分,再在每个段内部做滑窗。注意random_state固定下来,保证复现实验时结果可比。如果你手上只有一个连续故障信号,至少要按时间把前80%划给训练、后20%划给测试,让训练集和测试集在时间上完全隔离,这是没有独立段时的兜底策略。

3. CNN-LSTM模型结构设计:从一维卷积到时间记忆的两段式架构

3.1 结构选型:为什么Conv1D不用池化层

CNN部分的目标是从1024个时间点里定位瞬态冲击。池化层在图像分类里是标配,但在振动信号场景会带来一个麻烦:最大池化取窗口中最大值,会保留脉冲尖峰,但平均池化会平滑掉尖峰;更关键的是池化降采样丢失了冲击的精确时间位置。而LSTM需要完整的时间节奏信息来理解“故障冲击是不是周期性出现”,丢掉位置就相当于丢掉了周期性的证据。

常见做法是用stride代替池化做降采样。第一层Conv1D用较大的卷积核覆盖一个完整的冲击形态,比如64个采样点,然后通过stride=8把时间轴压缩到128个节点;第二层再用小卷积核提取更细的模式,stride=4压缩到32个节点。这样既降低LSTM的输入长度,又保留了每个冲击出现的相对位置。BN层放在卷积之后,对振动幅度波动大的数据效果立竿见影,能显著减少归一化尺度漂移带来的训练不稳定。

3.2 最小可运行的CNN-LSTM模型代码与参数说明

下面这个结构我在多个轴承数据集上验证过,属于“效果兜底、训练速度可接受”的最小配置。输入是1024点单通道波形,中间经过两层Conv1D压缩时间分辨率,再进LSTM学习前后文依赖,最后全连接输出类别概率。

from tensorflow.keras import layers, Model def build_cnn_lstm(num_classes): inputs = layers.Input(shape=(1024, 1)) # 第一层:大卷积核捕捉单个冲击形态,stride=8 降低时间分辨率 x = layers.Conv1D(16, 64, strides=8, padding='same', activation='relu')(inputs) x = layers.BatchNormalization()(x) # 第二层:小卷积核做局部精细特征,stride=4 继续压缩到32个时间步 x = layers.Conv1D(32, 32, strides=4, padding='same', activation='relu')(x) x = layers.BatchNormalization()(x) # LSTM 层逐帧建模时间依赖,return_sequences 让第一层输出完整序列 x = layers.LSTM(64, return_sequences=True)(x) x = layers.LSTM(32)(x) x = layers.Dense(32, activation='relu')(x) outputs = layers.Dense(num_classes, activation='softmax')(x) return Model(inputs, outputs)

conv1d的filters设为16和32,对四分类以内的任务足够,类别数多或者样本量大时可以调整到32和64。卷积核大小64是核心参数,它决定了“一次看多宽的波形”:采样率12.8kHz下对应5ms,刚好覆盖一个高速轴承冲击的衰减过程。如果设为16,卷积核只看到半截冲击,特征不完整;设到128则模糊了多个冲击之间的间隔,判别力下降。

LSTM层用64做双向记忆、32做最终聚合,中间层的return_sequences=True必须保留,否则第二层LSTM接收不到完整的时间序列。如果内存紧张,可以把两个LSTM层减为一层,但准确率通常会掉2到4个百分点。全连接层末尾加一个Dropout(0.3)可以缓解过拟合,不过在批量归一化加持下,小规模数据集的收益不如预期,不加也能跑。

3.3 训练好的模型怎么加载和快速验证

拿到源码包后不要急于重新训练。先直接用训练好的模型文件验证数据和代码链路是否通畅。TensorFlow 2.x的Keras模型一般保存为.h5格式,加载后对验证集样本执行一次推理,应该能复现项目说明中给出的准确率范围。

from tensorflow.keras.models import load_model model = load_model("bearing_cnn_lstm.h5") # 单样本推理:输入必须是(1, 1024, 1),输出是(1, num_classes)的概率分布 sample_window = np.random.randn(1024).astype(np.float32).reshape(1, 1024, 1) probs = model.predict(sample_window, verbose=0) pred_class = int(np.argmax(probs[0])) print("predicted class:", pred_class)

这里最容易报错的是输入shape。模型输入固定为(1024, 1),如果你传入的数组是(1024,)或(1, 1024),直接reshape一下就行。推理结果先和项目说明里的类别顺序对齐,确认标签顺序是[正常, 内圈, 外圈, 滚珠]还是别的排列,否则后续写评估脚本会把类别张冠李戴。验证通过后,再跑训练流程不迟。

4. 训练与验证:让模型收敛到能上线使用的准确率

4.1 损失函数与优化器的选择

轴承故障诊断是标准的多分类问题,类别互斥,损失函数用交叉熵即可。如果标签是0到N-1的整数,配合sparse_categorical_crossentropy使用,省去手写one-hot编码;如果标签已经是one-hot矩阵,则用categorical_crossentropy。两者数值上等价,但选错会在编译阶段不报错、训练到第一个epoch就崩,注意标签形式对应即可。

优化器首选Adam。对于这个场景,Adam的自适应学习率可以免除手工调整学习率的大部分痛苦。初始学习率从1e-3起步,如果损失在epoch 2之后就剧烈震荡,降到3e-4或1e-4再训。在CNN-LSTM组合结构里,LSTM梯度传播路径长,比纯CNN更容易出现梯度消失或爆炸,Adam的一阶动量能明显缓解这个问题。

4.2 训练策略:早停、动态学习率与模型保存

训练过程中的回调函数比网络结构本身更容易被忽略,但它们决定了模型最终能不能收敛到可用状态。EarlyStopping在验证损失连续多个epoch不下降时停止训练,避免过拟合;ReduceLROnPlateau在损失进入平台期后把学习率减半,帮助模型跨过局部极小点;ModelCheckpoint在验证指标最好时保存权重,防止最后几个epoch抖动把好权重覆盖掉。

from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau, ModelCheckpoint callbacks = [ EarlyStopping(monitor='val_loss', patience=15, restore_best_weights=True), ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=5, min_lr=1e-6), ModelCheckpoint('best_model.h5', monitor='val_accuracy', save_best_only=True) ] model.compile( optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'] ) history = model.fit( X_train, y_train, validation_data=(X_valid, y_valid), batch_size=64, epochs=60, callbacks=callbacks )

batch_size选64对1024点输入是性能和内存的平衡点。显存够的话提到128能让训练速度提升30%左右,但损失曲线会变得略粗糙。epochs设60是上限,实际配合EarlyStopping经常在25到40轮就停住。patience设15意味着模型在连续15轮内验证损失都没创新低就被判为“到头了”,这个值不要设太小,因为ReduceLROnPlateau生效需要几轮缓冲,patience太小会把一个还没降到位的训练提前终止。

训练完成后,先把加载best_model.h5的推理结果跟训练日志对比。如果验证准确率比训练准确率低5个百分点以上,大概率是数据泄露或模型容量过大,回到第2章检查数据集划分。如果两者都低,说明模型欠拟合,优先增加第一层卷积核宽度或LSTM单元数,而不是盲目堆层数。

4.3 评估指标怎么读:混淆矩阵与单类准确率

测试集总体准确率只是个起点,真正决定上线价值的是每个故障类别的召回率。滚动体故障的冲击幅度比内圈故障小一个数量级,误判率往往集中在这里。用分类报告逐类查看精准率和召回率,再用混淆矩阵确认哪些类别互相混淆。

from sklearn.metrics import classification_report, confusion_matrix y_pred = model.predict(X_test) y_pred_label = np.argmax(y_pred, axis=1) report = classification_report(y_test, y_pred_label, target_names=['normal', 'inner', 'outer', 'ball']) print(report) print(confusion_matrix(y_test, y_pred_label))

诊断这类模型时,近邻混类信息很有价值:外圈故障误判成内圈,通常是冲击间隔的计算被忽略,说明LSTM没学到周期特性;滚珠故障误判成正常,说明特征幅度被前几层卷积压缩掉了,需要减小stride或增加第一层通道数。看到混淆矩阵里哪两个类别纠缠,就知道该动模型的哪个部位,比盯着整体准确率更有效。

5. 轴承故障诊断避坑指南:4个让我返工的实测教训

5.1 训练集准确率高、测试集崩盘:数据泄露的三种隐蔽形态

现象:训练和验证准确率都上了98%,测试集一跑掉到75%。原因:数据划分时窗口重叠导致同源泄漏,或者归一化参数用了全局统计量让测试集信息提前进入训练过程。解决:检查处理流程,确认归一化是针对每个窗口独立计算,确认划分是按段而不是按点,最后看测试集和训练集之间是否存在同一时间段的窗口。这三个位置都过了,数据泄露基本排除。

5.2 输入维度报错:张量形状的“差一维”卡掉半天

现象:model.fit执行不到第一步就报ValueError: Shapes (None, 1024) and (None, 1) are incompatible。原因:输入层期望(1024,1),但训练数据shape是(n_samples, 1024),少了一个通道维度。解决:在预处理最后加X = X.reshape(-1, 1024, 1)。如果数据本身是二维的,要么reshape扩维,要么把Input的shape改成(1024,),但两种选择会影响后续通道方向的语义,统一用扩展维最省事。

5.3 模型输出均匀分布:Adam也救不回来的收敛失败

现象:训练损失从0.8慢慢走平,预测概率接近四类均分,准确率卡在25%上下。原因:学习率过大导致LSTM内部状态震荡,或者第一层卷积核宽度设为4导致看不到完整冲击。解决:把学习率降到3e-4,把第一层卷积核宽度改为32到64。如果仍不下降,打印几层输出的标准差,看到LSTM输出全是同一个值就说明梯度没能传回CNN层,考虑调整网络初始化和残差连接,不过在小数据集上换用更平滑的学习率调度通常就能恢复。

5.4 显存或内存爆掉:一次性加载整个数据集的代价

现象:数据总共几万窗口,每个窗口1024点,全量加载进内存后机器卡死或显存溢出。原因:所有预处理完成的数据一次性塞进了np数组,训练时再整体进GPU。解决:把数据写成TFRecord或segment-level的批读取方案;简单做法是先按段保留原始波形数组,训练时按batch切窗口,滑窗函数只在取数时执行。这个方法能把内存峰值降到原来的十分之一,代价是每次epoch重复计算滑窗,训练时间增加不足一成。

5.5 实测数据预测乱跳:转速和工况漂移带来的模型失效

现象:实验室数据测试准确率94%,拿到产线同一型号轴承上预测结果忽好忽坏,频繁误报。原因:训练数据在恒定转速下采集,现场转速、负载、油温都在变,冲击间隔和幅值分布都变了。解决:优先收集至少三个转速段的数据扩充训练集,如果无法覆盖,就在预处理做包络谱归一化,只保留冲击的包络形状输入模型。这一步在任何诊断方案中都是绕不开的物理边界——模型只能学习到数据分布之内的模式。

6. 把训练好的模型推到现场:TFLite导出与滚动预测的最后一公里

6.1 导出TFLite模型:给边缘设备和部署端减负

训练好的Keras模型带着大量训练算子,直接在现场工控机上推理不仅慢,还依赖完整的TensorFlow环境。常见做法是导出为TFLite格式,体积缩小约三分之一,CPU推理在普通工控机上单条窗口耗时从十几毫秒降到三五毫秒,完全跟得上产线实时采集节奏。

import tensorflow as tf converter = tf.lite.TFLiteConverter.from_keras_model(model) converter.optimizations = [tf.lite.Optimize.DEFAULT] converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS] tflite_model = converter.convert() with open("bearing_model.tflite", "wb") as f: f.write(tflite_model)

导出后的模型不依赖Python端Keras环境,用官方TFLite解释器就可以加载推理。量化开启后模型体积进一步缩小,但LSTM单元的浮点敏感性较高,量化后准确率可能掉0.5到1个百分点。现场存储空间充裕的话不必强行量化,先用DEFAULT优化看准确率是否可接受再做取舍。

6.2 滚动预测:滑动窗口投票策略

在实际产线上,每秒会产生上万个采样点,不能只靠单窗口做一次预测就下结论。滑窗每次平移一小段,让多个窗口覆盖同一段信号,再把它们的预测结果投票汇总。单窗口偶尔的误判会被大多数正确的窗口压制,最后以多数类作为最终判别。

from collections import Counter def rolling_predict(signal, model, seq_len=1024, step=256): votes = [] for start in range(0, len(signal) - seq_len, step): window = signal[start:start + seq_len] window_norm = (window - window.mean()) / (window.std() + 1e-6) inp = window_norm.reshape(1, seq_len, 1).astype(np.float32) pred = int(np.argmax(model.predict(inp, verbose=0)[0])) votes.append(pred) return Counter(votes).most_common(1)[0][0]

step设为256,一条4秒的12.8kHz信号能产生大约190个窗口,统计出的结果非常稳定。我自己的习惯是输出连续3秒内的故障类别投票结果,而不是用单窗口判断立即触发报警,宁可慢半秒也不能让环境噪声造成频繁误报。把原始波形存成缓冲队列、每次取尾巴上几秒滚动计算,这个部署方式在PLC场景里很实用。现场反馈报警时,再用保存下来的原始波形回放一遍,确认是哪一类故障触发,方便后续检修定位。这套处理流程跑顺之后,后续再接新的轴承型号,只需要在数据准备阶段换成对应的振动信号,模型微调几轮就行。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询