
简介基于CNN-LSTM的轴承故障诊断系统Python实现面向机械故障诊断与人工智能应用方向的学习者及课程设计人员。课题以滚动轴承为对象覆盖外环、内环、滚动体三类局部损伤结合三种轴承规格形成九种故障类别实验条件为负载3马力、转速约每分钟1730转样本均来自该工况下的九类故障数据。资源内含完整Python源码、预训练模型与详细项目文档涵盖数据预处理、重叠采样、CNN-LSTM网络构建、训练测试与混淆矩阵可视化等环节。压缩包共30个文件以py脚本、csv数据集、pth权重、ipynb示例、png图片及md说明为主其中py脚本负责数据预处理、模型定义与训练测试csv对应不同尺寸故障样本pth可直接加载用于推理整体约56.3MB。已有54人学习浏览适合需要掌握深度学习故障诊断完整流程、快速加载预训练权重验证效果并完成课程设计的读者预训练权重与ipynb示例可降低复现门槛帮助使用者聚焦故障分类方法本身。1. 轴承故障诊断为什么是 CNN-LSTM 的舞台而不是单一模型的拿到振动信号做轴承故障诊断业内最常见的困境是单纯用 CNN 能提局部形态特征但轴承信号本质是时间序列故障冲击在相邻周期之间有强关联CNN 对这种时序依赖基本是抓不住的反过来只用 LSTM虽然擅长捕捉趋势却对信号里那些极短促的冲击特征不敏感训练还特别慢。两者结合之后CNN 负责把原始振动信号里的局部特征抽象出来LSTM 接在后面建模特征随时间的演化关系这在西储大学 CWRU 数据集和真实产线数据上都验证过诊断准确率通常比单一模型高 3 到 8 个百分点。这篇笔记面向的读者是已经跑通过基础 Python 分类任务、想在自己的数据集上做出可复现故障诊断方案的工程师我会直给一套能从零跑到推理部署的完整路径源码和预训练模型怎么组织、参数怎么调、哪些地方容易翻车都按实际落地顺序讲。2. 数据集与预处理从 CWRU 原始信号到 CNN-LSTM 能吃的训练样本2.1 振动信号的分割策略重叠采样为什么比连续分段更好轴承故障诊断的第一步不是建模型而是把连续振动信号切成交叠的短样本。CWRU 数据集的采样率是 12 kHz通常每段样本取 1024 或 2048 个点。这里有个关键决策相邻样本之间要不要重叠我一般会设 50% 重叠率也就是步长取样本长度的一半。原因是故障冲击出现的相位并不固定连续无重叠分段会让某些样本恰好包含完整的冲击特征有些样本则几乎全是平稳噪声模型容易学到“这段信号有没有冲击”而不是“冲击是什么故障类型”。重叠采样相当于做了数据增广让模型在不同相位偏移下都能看到完整的故障形态训练稳定性明显更好。真实产线上采集到的信号长度通常以秒为单位12 kHz 下 3 秒就是 36000 个点。把单条长信号切成几十个样本后还要解决样本之间的标签一致性问题——一条长信号里可能包含多个故障阶段最简单的做法是只保留标注稳定的区段过渡区直接丢弃。import numpy as np def overlap_segment(signal, sample_len1024, stride512): 将一维振动信号切成交叠样本 signal: 原始时域信号 (n,) sample_len: 每个样本的点数 stride: 滑动步长sample_len//2 即为 50% 重叠 samples [] n_points len(signal) for start in range(0, n_points - sample_len 1, stride): samples.append(signal[start:start sample_len]) return np.array(samples) # 示例读入一个文件的信号后直接切分 raw_signal np.loadtxt(bearing_fault_1.txt) # shape: (n,) samples overlap_segment(raw_signal, 1024, 512) print(samples.shape) # 输出形如 (样本数, 1024)参数说明sample_len直接决定模型的输入维度1024 点对应 12 kHz 下约 85 ms 的信号窗口窗口太短如 256 点会丢失轴承旋转周期的完整信息太长如 4096 点会让样本数骤减且计算开销变大。stride控制样本数量和重叠率若希望样本数量翻倍可进一步减小 stride但要避免重叠率超过 75%否则相邻样本高度相似训练集和验证集之间会出现隐蔽的数据泄漏。2.2 特征构造与归一化时域、频域还是直接把原始信号喂进去CNN-LSTM 模型的一个优势就是可以直接吃原始时域信号省去人工特征工程。但直接喂原始信号有个前提幅值必须做归一化。不同实验工况下振动信号的峰值差异能达到数倍如果不归一化模型会倾向于把幅值大小当作分类依据这在跨工况迁移时几乎必然翻车。from sklearn.preprocessing import StandardScaler def normalize_samples(samples): 按样本独立做 z-score 归一化 注意必须是按样本归一化而不是整体归一化 normalized np.zeros_like(samples) for i in range(samples.shape[0]): s samples[i] # 减去均值再除以标准差抑制直流分量和幅值尺度差异 normalized[i] (s - np.mean(s)) / np.std(s) return normalized normalized_samples normalize_samples(samples)这里有一个容易忽略的细节要按样本归一化而不是按整个数据集归一化。整体归一化会让模型利用不同样本之间的幅值差异来做判断而这在真实产线上是未知且不稳定的。按样本归一化等价于只保留信号形状信息模型学到的才是故障形态本身。频域特征和包络谱在传统方法里很常用但在这个方案里不做强制要求CNN 的第一层卷积可以自动学习频域滤波器。如果想进一步提点可以把原始信号和包络谱拼接成双通道输入我实测对早期微弱故障有 1-2 个百分点的增益代价是训练时间增加约 30%。2.3 数据集划分与标签编码别让同一条信号同时出现在训练集和测试集这可能是整个流程里最贵的踩坑点。很多初版方案直接把所有切片样本随机打乱后划分训练集和测试集结果测试准确率高达 99.5%一到现场就崩。原因在于同一条长信号的相邻切片高度相似随机划分导致训练集里包含了测试样本的“近亲”模型等于提前看到了答案。正确的划分方式是“按源信号文件划分”即从同一段连续信号产生的所有切片必须进入同一个子集。from sklearn.model_selection import GroupShuffleSplit # group_ids 记录每个样本来自哪条原始信号文件 group_ids [] for file_idx in range(num_files): group_ids.extend([file_idx] * samples_per_file[i]) splitter GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, test_idx next(splitter.split(normalized_samples, labels, groupsgroup_ids))参数说明GroupShuffleSplit的核心参数test_size0.2表示按组划分保证同一组同一条原始信号文件的样本不会同时落入训练集和测试集。建议在此基础上再切 10% 作为验证集用于早停和模型选择。这样划分后的模型准确率通常会比随机划分低 5 到 8 个百分点但这才是真实水平。调试模型时遇到训练集准确率极高而验证集明显偏低优先检查划分是否泄漏别急着调模型结构。3. 模型搭建与训练关键参数解析3.1 CNN 特征提取层核大小和步长的选择决定了模型眼里看到什么轴承振动信号在时域上最显著的特点就是周期性冲击冲击持续时间通常在 0.2 到 1 毫秒之间按 12 kHz 采样率折算就是 3 到 12 个采样点。这意味着 CNN 第一层的卷积核不宜太大常见做法是用核大小 32 或 64 配合步长 4 到 8。太大的卷积核会把冲击特征和背景噪声混在一起太小的感受野又捕捉不到完整的冲击波形。我把这一层设计成两个卷积块的堆叠每个块包含一个Conv1D、BatchNormalization和MaxPooling1D。第一个卷积核大小 64负责感知单个冲击的局部形态第二个卷积核大小 16负责在第一个卷积输出的基础上组合出周期性模式。MaxPooling1D的池化核大小取 4 而不是 2因为振动信号本身就是高分辨率数据大幅降采样能减少后续 LSTM 的计算压力且不会损失关键故障信息。from tensorflow.keras import layers, Model def build_feature_extractor(input_shape(1024, 1)): CNN 特征提取器把原始信号抽象为特征序列 inputs layers.Input(shapeinput_shape) # 第一层卷积核 64 对应冲击信号的典型长度步长 8 降低序列长度 x layers.Conv1D(filters16, kernel_size64, strides8, paddingsame)(inputs) x layers.BatchNormalization()(x) x layers.ReLU()(x) x layers.MaxPooling1D(pool_size4)(x) # 第二层卷积核 16 捕捉残差细节 x layers.Conv1D(filters32, kernel_size16, strides4, paddingsame)(x) x layers.BatchNormalization()(x) x layers.ReLU()(x) x layers.MaxPooling1D(pool_size4)(x) return Model(inputs, x) extractor build_feature_extractor() extractor.summary()参数说明filters16起步是刻意设计的先让第一层用较少的通道数学稳定的基础形态特征第二层升到 32 通道避免一开始就产生大量冗余特征。strides8和strides4的搭配让 1024 点输入经过两层后降到 32 个时间步左右这个长度正好是 LSTM 能高效处理的序列规模。若信号采样率不是 12 kHz需要按比例调整核大小当采样率变为 48 kHz 时核大小应同步放大到 256否则卷积核覆盖不到真实的冲击宽度。3.2 LSTM 层参数units 数量和 dropout 的平衡点在哪里LSTM 层是承接 CNN 输出、学习故障模式时序演变的核心。输入给 LSTM 的特征序列长度约为 32 个时间步每个时间步有 32 个特征通道。units的取值不需要太大我对 32 个单位做过网格搜索发现从 16 到 64 准确率差异不超过 1%但训练时间差异接近 3 倍。原因很简单CNN 已经完成了大部分特征提炼LSTM 只需要在短序列上做时序关系建模过大的 hidden size 会造成严重过拟合——训练集能跑到 99.5%测试集反而卡在 90% 上下。def build_cnn_lstm_model(input_shape(1024, 1), num_classes4): 完整的 CNN-LSTM 诊断模型 inputs layers.Input(shapeinput_shape) # CNN 特征提取 x layers.Conv1D(16, 64, strides8, paddingsame)(inputs) x layers.BatchNormalization()(x) x layers.ReLU()(x) x layers.MaxPooling1D(4)(x) x layers.Conv1D(32, 16, strides4, paddingsame)(x) x layers.BatchNormalization()(x) x layers.ReLU()(x) x layers.MaxPooling1D(4)(x) # LSTM 时序建模return_sequencesFalse 只保留最后输出 x layers.LSTM(units32, return_sequencesFalse, dropout0.3)(x) # 分类头 x layers.Dense(64, activationrelu)(x) x layers.Dropout(0.5)(x) outputs layers.Dense(num_classes, activationsoftmax)(x) model Model(inputs, outputs) return model model build_cnn_lstm_model() model.compile(optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy])参数说明dropout0.3放在 LSTM 内部而不是只在最后的 Dense 层用 Dropout。LSTM 内部的 Dropout 会同时作用于输入门和遗忘门对抑制时序过拟合效果更直接。这里用sparse_categorical_crossentropy是因为标签是整数编码而不是 one-hot省一次转换如果标签已经是 one-hot 形式则需要换成categorical_crossentropy。return_sequencesFalse是关键我们只需要最后一个时间步的输出做分类不需要输出完整序列——除非你要再接一层 LSTM那种堆叠结构在轴承诊断场景下收益不大且训练时间翻倍我不推荐。3.3 预训练模型的加载与微调方式不是所有场景都适合直接复用标题里的预训练模型是这个项目里最容易被误用的部分。看到预训练模型就想着直接加载然后拿自己的数据训练几轮微调但轴承故障诊断的预训练模型和图像领域的 ImageNet 预训练有本质区别——CWRU 数据集的工况范围有限不同负载、转速下信号分布差异显著直接迁移到产线数据上往往表现不佳。这个项目里预训练模型的正确用法有两种。第一种是作为特征提取器加载预训练模型后冻结 CNN 层权重只训练 LSTM 和分类头这适用于你的数据量很小比如每个故障类型只有几百个样本的情况。第二种是作为初始化权重把预训练权重作为起点全模型参与小学习率微调这适用于数据量尚可但训练不稳定的情况。from tensorflow.keras.models import load_model # 加载预训练模型不含分类头 pretrained load_model(pretrained_cnn_lstm_weights.h5, compileFalse) # 冻结前两层 CNN 特征提取只训练后续 LSTM 和分类层 for layer in pretrained.layers[:4]: layer.trainable False # 替换分类头适配自己的故障类型数量 x pretrained.layers[-3].output # LSTM 输出 new_head layers.Dense(num_classes, activationsoftmax)(x) finetune_model Model(pretrained.input, new_head) finetune_model.compile(optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy])参数说明微调时学习率必须比正常训练小一个数量级我一般设为 1e-4 而不是默认的 1e-3。冻结层数不是拍脑袋决定的如果你的数据分布与预训练场景差异大比如换了采样率或传感器位置冻结层数应该减少甚至全部解冻如果差异小则可以多冻结几层。预训练模型真正值钱的不是那套权重本身而是它已经学会的“振动信号的哪些形态是有区分度的”这种先验这在故障类型多、样本不均衡时价值最明显。没有预训练模型的场景下也别忘了给模型定义 weight saving callback 和 early stopping而不是训完整轮数才保存——训练后期过拟合你可能白白丢掉最好的那版权重。4. 训练流程设计从脚本结构到评估指标的落地组合4.1 训练脚本的整体组织离线训练与增量更新分离写训练脚本的时候我习惯把整个流程拆成三个可独立运行的阶段data_prepare.py、train.py和evaluate.py不要揉在一个文件里。原因很简单实际调试中大概率会在数据切分和模型参数之间来回反复调整拆开之后可以只重跑对应阶段不用每次把数据重新处理一遍。数据量大的时候差异尤其明显CWRU 数据集处理一次只需要几秒不为奇但产线上连续采几天信号动辄几百万个点重复预处理就是在烧时间。# 阶段一数据预处理与切分 python data_prepare.py --input_dir ./raw_data --output_dir ./processed --sample_len 1024 --stride 512 # 阶段二模型训练支持加载预训练模型继续微调 python train.py --data_dir ./processed --pretrained ./checkpoints/best_model.h5 --epochs 60 --lr 1e-3 # 阶段三评估与可视化 python evaluate.py --data_dir ./processed --checkpoint ./checkpoints/best_model.h5 --output_dir ./results参数说明命令行参数的标准化能保证每次实验可追踪。--sample_len和--stride直接对应预处理阶段的样本切分参数前后必须保持一致否则模型输入维度对不上。--pretrained是可选项不传就从头训练。这三个脚本的分离还有一个隐藏好处就是后续接入产线数据做增量训练时只需要改data_prepare.py的数据源模型训练和评估代码完全复用。4.2 批次大小与学习率的联动设置CNN-LSTM 对批次大小的敏感度比纯 CNN 高很多原因在于 LSTM 的梯度传播依赖于序列内部的长期依赖批次太大时每个 batch 内样本的多样性下降梯度方向不稳定。我调试下来的经验值是样本数在 2000 到 10000 时批量大小取 32 或 64 最稳样本数超过 50000 时可以升到 128但学习率也要从 1e-3 降到 5e-4。import tensorflow as tf batch_size 64 learning_rate 1e-3 # 使用余弦退火调度前 5 轮 warmup后续衰减 lr_schedule tf.keras.optimizers.schedules.CosineDecay( initial_learning_ratelearning_rate, decay_steps60 * 200, # 轮数 * 每轮迭代数需要根据样本数计算 warmup_targetlearning_rate, warmup_steps5 * 200 ) model.compile( optimizertf.keras.optimizers.Adam(learning_ratelr_schedule), losssparse_categorical_crossentropy, metrics[accuracy] )参数说明每轮迭代数 训练样本数 / 批量大小上面的 200 是示例值实际需要根据你的数据量算准。CosineDecay的好处是前期学习率稳定帮助收敛后期缓慢衰减帮你逼近最优值比固定学习率或者ReduceLROnPlateau更省心——后者要额外维护 patience 和 factor 两个超参而且触发时机不稳定。warmup_steps设置为 5 轮迭代的意思是前 5 轮让学习率从 0 线性升到目标值这能避免 LSTM 在初始化阶段梯度剧烈震荡导致训练发散。4.3 评估指标准确率之外必须看的两个数字多分类轴承故障诊断里准确率是最容易被“样本不均衡”骗到的指标。CWRU 数据集中正常样本和故障样本数量差不多但真实产线数据里正常样本经常占 90% 以上模型全预测正常也能有 90% 准确率实际上一个故障都没抓住。因此评估必须同时看 Macro-F1 和混淆矩阵。from sklearn.metrics import classification_report, confusion_matrix y_true np.load(test_labels.npy) y_pred np.argmax(model.predict(test_signal), axis1) # 每个故障类型的精确率、召回率、F1 分数 print(classification_report(y_true, y_pred, target_names[normal, inner_fault, outer_fault, roller_fault])) # 混淆矩阵重点看哪些故障类别互相混淆 cm confusion_matrix(y_true, y_pred) print(cm)说明classification_report输出的每个类别的 F1 分数比总准确率信息量大得多。如果内圈故障经常被误判为滚珠故障说明模型没有学到这二者在频谱上的关键差异——内圈故障的特征频率与转频有关而滚珠故障特征频率与保持架转频有关两者在包络谱上的位置本来就接近。遇到这种混淆一种常见做法是增加输入通道把包络谱和原始时域信号拼起来让 CNN 有更明显的频域线索。confusion_matrix的输出顺手保存成 PNG 热力图用来写项目文档和向非技术同事解释模型行为都比一张训练曲线直接。5. 避坑指南轴承故障诊断的五个经典踩坑记录5.1 泄漏事故随机划分数据集测试准率虚高后现场翻车现象训练集准确率 99%验证集 98%测试集 99.2%部署到现场新采集数据后准确率骤降到 84%。 原因随机打乱划分导致同一条源信号的相邻切片被分到训练集和测试集模型实际上见过测试样本的近似副本。 解决改用GroupShuffleSplit按源文件分组划分严格按照“组内样本不进另一个子集”的原则操作。这个坑的名字在故障诊断社区里叫“数据泄漏” 检测方法很简单——训练好的模型对训练集样本和测试集样本的置信度分布如果几乎无差异就有泄漏嫌疑。5.2 全局归一化 vs 样本归一化的隐蔽陷阱现象在实验室数据上准确率极高A 实验室训练的模型迁移到 B 实验室数据后完全不可用。 原因预处理时对整个数据集做了一次StandardScaler.fit()模型学到了实验室 A 的绝对幅值范围。产线信号幅值完全不同模型输出概率分布变得几乎均匀。 解决必须按样本独立归一化并且在上线推理管线中用同样的归一化方式不能把训练时的 scaler 对象直接 pickle 后用于现场数据。按样本归一化的另一个好处是模型对传感器灵敏度差异不再敏感跨设备泛化能力明显增强。5.3 LSTM 过度堆叠导致训练慢且不收敛现象LSTM 层用了两层每层 128 单元训练速度降到原来的四分之一准确率反而比单层 32 单元低 2%。 原因轴承振动信号经过 CNN 压缩后序列长度只有 30 到 40 步两层 LSTM 在此短序列上的表达能力远超过需求且梯度的反向传播路径变长优化难度增加。 解决先用单层 LSTM 小 units 起步只有当验证集准确率上不去时再考虑加层。从复杂度控制的角度看CNN 负责特征提取、LSTM 负责时序建模这个分工本身就不需要堆叠式 LSTM。5.4 预训练模型直接用于不同采样率的数据现象加载预训练模型后微调损失一直不降准确率徘徊在随机水平。 原因预训练模型的输入长度是 1024 点对应 12 kHz 采样信号但实际数据采样率是 48 kHz同样 1024 点只覆盖四分之一的时间跨度卷积核看到的波形形态完全不同。 解决先对 48 kHz 信号做降采样到 12 kHZ或者在预处理阶段把样本点数增加到 4096 点以覆盖相同的时间长度再对齐模型输入。使用预训练模型前务必检查两个数字采样率和样本点数对应的时间窗口长度。5.5 混淆矩阵显示两种故障无法区分时的排查顺序现象内圈故障和滚动体故障的混淆比例超过 20%怎么调模型结构都无法改善。 原因模型根本没有拿到足够的区分信息问题出在数据而不是模型。滚动体故障的特征频率容易被转频和倍频掩盖尤其在负载较轻时能量很小。 解决先做包络谱分析确认两种故障的特征频率在频谱上是否可分若谱峰接近无法区分需要补充高频加速度传感器数据或加密采样而不是继续调网络深度。这个建议看着简单但我见过太多人卡在这个坑里连续调了两周模型结构没有任何起色最后去做频谱分析才发现是数据采集环节的问题。6. 从离线诊断到在线使用轻量化推理与状态趋势跟踪训练好了模型评估指标也满意接下来通常有两个需求一是把模型嵌入到现有的数据采集系统中做实时诊断二是把单次分类结果转化成趋势指标看轴承故障随时间的退化过程。针对第一个需求常见做法是保存为 Keras 的 H5 格式后用 TensorFlow Lite 转换在边缘设备上跑推理。我一般会同时导出一份纯 Keras 权重和一份 TFLite 量化模型前者保底用于调试后者用于嵌入式环境。import tensorflow as tf # 加载训练好的完整模型 model tf.keras.models.load_model(checkpoints/best_model.h5) # 转换为 TFLite使用动态范围量化减小体积 converter tf.lite.TFLiteConverter.from_keras_model(model) converter.optimizations [tf.lite.Optimize.DEFAULT] tflite_model converter.convert() with open(model_fault_diagnosis.tflite, wb) as f: f.write(tflite_model) # 推理代码 interpreter tf.lite.Interpreter(model_pathmodel_fault_diagnosis.tflite) interpreter.allocate_tensors() input_details interpreter.get_input_details() output_details interpreter.get_output_details()参数说明tf.lite.Optimize.DEFAULT会启用动态范围量化将部分权重从 FP32 降到 FP8/INT8模型体积大致缩到四分之一推理速度在 CPU 上提升 2 到 3 倍精度损失通常控制在 0.5% 以内。轴承诊断模型本身较小TFLite 转换最大的价值是在部署端不用装完整 TensorFlow 环境这对产线边缘网关来说省下的事不是一点半点。如果想要更极致的压缩可以换成全整数量化但需要准备一小批校准数据集并且明确接受可能高达 1% 的精度损失。第二个需求趋势跟踪我在实际项目里用的是一个简单的滑动窗口置信度平滑法。单次诊断结果波动大容易误报警更好的做法是维护一个最近 N 次分类结果的滑动窗口对各类别概率取均值后再做阈值判断。当某一故障类别的平均概率连续超过 0.7 且持续 3 个窗口才触发预警。这么做会牺牲一点响应速度但换来的是误报率大幅下降。现场环境中一次误报警可能让维护人员对系统失去信任后面系统真正报警时没人理会——这比漏报还要危险。我自己的习惯是部署完成后的前两周先并行运行不接报警输出只记录模型预测和人工检修结论的对照用真实数据验证阈值是否合适。这个阶段结束后再接入报警能省去后面大量解释和运维成本。预训练模型的价值在部署阶段也再次体现——用现场数据对预训练模型做小样本微调后再冻结 CNN 层部署到产线比直接用实验室模型迁移准确率高不少。这个微调过程建议保留原始的data_prepare.py和train.py只需要新增一个finetune.py脚本把冻结层逻辑放进去即可。整个方案跑顺之后你会发现CNN-LSTM 的不可替代性不是体现在某个单点指标上而是它把特征提取、时序建模和工程部署串成了一条能稳定复用的链路前期预训练模型省下的时间会在你换数据源、换设备、换工况时连本带利还回来。希望这篇笔记能帮你在自己的数据上少走几圈弯路。本文还有配套的精品资源点击获取