
简介基于CNN-LSTM的轴承故障诊断完整Python工程面向深度学习和机械故障诊断方向的课程设计学生及入门实践者可提供从数据预处理、特征提取到模型训练与测试的一站式参考代码。项目聚焦滚动轴承外圈、内圈、滚珠三类局部故障结合三种不同直径形成9种故障类别并在3HP负载、约1730转速条件下完成振动信号采集与识别。压缩包共25个文件包含4个Python脚本、2个训练好的模型权重、9个CSV振动数据、1个交互式notebook演示以及Matlab数据/脚本、项目说明文档和结果表格总大小53.21MB目录结构清晰。代码附有详细注释覆盖数据重叠采样、CNN-LSTM网络构建、训练与测试流程可直接运行验证同时提供已训练权重便于快速评估模型在9类故障上的识别效果。已有122人学习该资源适合作为课程设计参考、故障诊断入门实践或进一步改进的基线项目。1. 从振动信号到故障标签这套 CNN-LSTM 轴承故障诊断源码拿到手就能跑拿到这套 cnn-lstm 轴承故障诊断 python 源码时我第一件事不是看模型结构而是确认它能不能直接跑。做轴承故障诊断这几年九成时间都耗在数据整理、环境搭配和训练复现上写模型反而是最省事的部分。这份资源把难啃的地方都处理好了训练好的模型权重、逐段注释的源码、一份项目说明文档。拿到手可以先跑通推理再换自己的振动数据重训两条路都走得通。它适合做设备健康管理课题的学生、要给产线写故障判读程序的一线工程师以及想快速搭 CNN-LSTM 基线做对比的算法岗。整个流程围绕振动信号多分类展开输出故障类别和置信度是标准的监督学习端到端方案下面按跑通顺序拆。2. 为什么是 CNN 加 LSTM局部冲击特征与时序记忆的分工2.1 CNN 卷积层在振动信号里找的是局部冲击模式轴承出现局部损伤时内圈点蚀、外圈剥落、滚动体裂纹滚动体每次滚过缺陷位置都会激起一个冲击脉冲这个脉冲随即激励轴承座的固有频率在时域波形上表现为一小段高频振荡。一维卷积要做的事就是在这段窗口里扫描类似的局部模式。卷积核只有几个点宽滑过信号时与波形做内积波形形状越接近卷积核学到的“冲击模板”输出激活越大。后面的最大池化把每个局部区域里最强的响应保留下来顺带让模型对冲击出现的精确位置不那么敏感——冲击偏移半个采样点结论不应该跟着变。为什么用一维卷积而不是把信号转成时频图再做二维卷积一维卷积参数少直接吃原始波形省掉 STFT 的窗函数选择、频率分辨率、重叠率这些额外超参数。那些参数一旦没调好预处理环节就会变成一个说不清的黑匣子。更关键的一点是一维卷积的输出仍然保留时间顺序这个顺序是后面 LSTM 建模的输入前提二维卷积加全连接这条路线天然丢掉时间轴上的先后关系。常见做法是把卷积核大小放在 3 到 7通道数从 64 往上加池化步长取 2后面会有一张参数表给到具体数值。2.2 LSTM 补的是 CNN 缺失的时间依赖关系CNN 对窗口内局部响应的聚合是“一视同仁”的它不关心这些冲击脉冲在时间上的先后与间隔。但轴承故障信号恰恰在时间顺序里藏着判别信息冲击按转频周期出现幅值随承载区位置被调制不同故障类型对应的特征频率又各不相同这些规律靠单个脉冲表达不出来。LSTM 靠一个携带历史信息的隐藏状态把时间依赖补上。每个时间步输入门决定新特征写入多少遗忘门决定旧记忆丢掉多少输出门决定当前步对外暴露什么。最后一个时间步的隐藏状态把整个窗口的时序上下文压缩成一个固定长度向量交给全连接层分类。纯 CNN 想覆盖同样的时间跨度得堆很多层卷积感受野才够长纯 LSTM 直接面对 1024 维原始输入训练慢且容易过拟合。把 CNN 放在前面先把原始信号压成低维特征序列LSTM 再在小维度上建模时间关系两个模型的短板正好互补。这也是混合结构在振动诊断里一直能打的原因——特征提取和时序建模各司其职。实际项目里也有人把 LSTM 换成 GRU参数更少、训练更快但在轴承这类信号上两者效果差异不大。源码用的是 LSTM我建议先按原结构跑通再决定要不要换。2.3 数据流从原始信号到输入张量的形状变化把整条链路写清楚训练脚本里每一步都能对上号出问题也好定位。原始信号先按滑动窗口切成若干样本每个样本做一次 Z-Score 归一化然后 reshape 成 (1, 窗口长度) 送进 Conv1d。经过两层“卷积加池化”时间维从 1024 缩到 256通道维从 1 涨到 128张量形状是 (batch, 128, 256)。要喂给 LSTM必须先做一次 permute把形状换成 (batch, 时间步 256, 特征维 128)。LSTM 输出 (batch, 256, 64)取最后一个时间步的 64 维向量进全连接层得到 (batch, 类别数)。这一路的形状变化用表格看更直观阶段张量形状说明模型输入(batch, 1, 1024)单通道振动波形两层卷积池化后(batch, 128, 256)128 通道特征时间维 256permute 后(batch, 256, 128)时间步在前符合 batch_first 约定LSTM 输出(batch, 256, 64)每个时间步输出 64 维取末步(batch, 64)时序上下文压缩分类输出(batch, num_classes)softmax 概率建议进训练脚本前先对着这张表把维度推一遍很多 dimensions mismatch 的报错本质上就是中间某一步的形状没对上。3. 跑通训练流程环境准备、数据预处理与脚本拆解3.1 环境准备与源码目录结构常见做法是 Python 3.8 以上torch 用 1.13 或 2.x再配上 numpy、scikit-learn、matplotlib。没有独立显卡也能训batch 调小一点、epoch 适当减少小型轴承数据集在 CPU 上几十分钟内也能收敛。环境搭建最大的坑是 torch 和 CUDA 版本对不上装完 torch 之后用 torch.cuda.is_available() 先验一下返回 True 再继续否则后面训练脚本会静默落到 CPU 模式。源码的目录结构大致是这样cnn-lstm-bearing/ ├── data/ # 原始振动数据或切好的 npy 文件 ├── models/ # 训练好的 best_model.pth ├── config.py # 超参数集中管理 ├── preprocess.py # 滑动窗口 归一化 数据集划分 ├── train.py # 模型定义、训练循环、模型保存 ├── predict.py # 加载权重做单样本推理 └── README.md # 项目说明文档把超参数集中到 config.py而不是散落在各个脚本里是我一直坚持的习惯。调参时只改一个文件不会出现“训练脚本改了 lr、验证脚本忘了同步”这种低级失误。3.2 数据预处理滑动窗口切割与归一化轴承振动数据是长序列训练样本得自己切最常用的是滑动窗口import numpy as np def sliding_window(data, window_size1024, step256): 把一段连续振动信号切成等长训练样本。 参数: data: 一维 ndarray, 原始振动信号 window_size: 单样本长度, 决定模型看到的时间跨度 step: 窗口移动步长, 小于 window_size 时相邻样本重叠 返回: (样本数, window_size) 的 ndarray samples [] for i in range(0, len(data) - window_size, step): samples.append(data[i:i window_size]) return np.asarray(samples)这段逻辑不复杂值得拎出来说的是 step 和 window_size 的关系。step 小于 window_size相邻样本就会重叠相当于做了一次数据增广模型看到的冲击相位有偏移泛化更好step 等于 window_size 则互不重叠样本数少训练集不够时容易过拟合。切完检查一下样本数量一个文件长度固定窗口越大、步长越大样本越少。归一化同样关键。不同转速、不同负载下的振动幅值差异很大不处理的话模型会把幅值大小当作判别依据而不是学波形形状。常见做法是按单个样本做 Z-Score 归一化def zscore_normalize(x): 按样本独立归一化, 消除幅值尺度差异。 加 1e-6 防止某小段信号完全为零时除零出错。 mean x.mean(axis-1, keepdimsTrue) std x.std(axis-1, keepdimsTrue) return (x - mean) / (std 1e-6)注意是“按样本”而不是“按整个数据集”。原则上全局统计量也能用但部署推理时你手里只有当前这一段信号拿不到全局均值方差所以从一开始就按样本归一化训练和推理的行为才能保持一致。数据划分有个高频坑不能把切完的窗口混在一起随机划分。正确顺序是先按原始数据文件划分训练集和测试集再各自切窗口from sklearn.model_selection import train_test_split # 按文件划分, 避免同一段信号的窗口同时出现在训练集和测试集 train_files, test_files train_test_split( file_list, test_size0.2, random_state42, stratifylabels )stratifylabels 保证每个故障类型在两个集合里的比例一致。类别不均衡是轴承数据集的常见问题正常样本往往远多于故障样本这一行能缓解训练时的大类偏向。3.3 模型定义与训练循环拆解模型结构是整套源码的核心。两层一维卷积加两层 LSTM是这类任务里参数量和效果最平衡的配置import torch import torch.nn as nn class CNNLSTM(nn.Module): CNN-LSTM 混合模型, 用于轴承振动信号故障分类。 def __init__(self, num_classes4): super().__init__() self.conv1 nn.Conv1d(1, 64, kernel_size3, padding1) self.relu nn.ReLU() self.pool nn.MaxPool1d(2) # 时间维每池化一次减半 self.conv2 nn.Conv1d(64, 128, kernel_size3, padding1) # dropout 仅在训练时生效, 推理时自动关闭 self.lstm nn.LSTM(input_size128, hidden_size64, num_layers2, batch_firstTrue, dropout0.2) self.fc nn.Linear(64, num_classes) def forward(self, x): # x: (batch, 1, 1024) x self.pool(self.relu(self.conv1(x))) # (batch, 64, 512) x self.pool(self.relu(self.conv2(x))) # (batch, 128, 256) x x.permute(0, 2, 1) # (batch, 256, 128) out, _ self.lstm(x) # (batch, 256, 64) out out[:, -1, :] # 取最后时间步 return self.fc(out)permute 那一步是把通道维挪到特征维、时间步放到中间满足 LSTM batch_firstTrue 的输入布局。取 out[:, -1, :] 等于把整个时间序列的上下文压到最后一步是分类任务最常见的收口方式比把所有时间步求平均更能体现“最后时刻带着全部记忆”。dropout 加在 LSTM 层间抑制过拟合推理时它会自动关闭不需要手工干预。训练循环本身平淡但有两个细节容易被忽略梯度裁剪和学习率衰减。轴承信号里偶发强冲击对应样本 loss 会突然飙高不裁剪的话 LSTM 的梯度极容易爆炸device torch.device(cuda if torch.cuda.is_available() else cpu) model CNNLSTM(num_classes4).to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size30, gamma0.1) for epoch in range(50): model.train() running_loss 0.0 for x_batch, y_batch in train_loader: x_batch, y_batch x_batch.to(device), y_batch.to(device) optimizer.zero_grad() logits model(x_batch.unsqueeze(1)) loss criterion(logits, y_batch) loss.backward() # 梯度裁剪: 梯度范数超过 5.0 就缩放回来, 防止梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) optimizer.step() running_loss loss.item() * x_batch.size(0) scheduler.step() # 每 30 个 epoch 学习率降为原来的 1/10 avg_loss running_loss / len(train_loader.dataset) print(fepoch {epoch1:02d}, loss {avg_loss:.4f})x_batch.unsqueeze(1) 这句相当重要。DataLoader 出来的张量形状是 (batch, 1024)Conv1d 要求有通道维所以补成 (batch, 1, 1024)。我帮人排查代码时发现这类维度问题出现的频率远高于模型本身的问题。提示训练过程中最好每个 epoch 在验证集上算一次准确率只保存表现最优的那份权重。很多人等训练全部跑完才想起来保存结果最优模型已经死在过拟合里了后面避坑章会单独说。3.4 超参数怎么调一张表说清每个参数源码的默认超参数是作者跑通实验的配置换数据集时建议先照抄跑一轮拿基准再针对性地动。常用参数的作用与取值范围超参数源码默认常见取值范围作用与调参方向window_size1024512 ~ 2048单样本时间跨度, 越大信息越全, 但样本数和 batch 受限step256128 ~ 512控制重叠度, 越小增广越多, 训练越慢batch_size6432 ~ 128显存小就减半, 太小收敛慢且震荡learning_rate1e-31e-4 ~ 3e-3loss 震荡就降, 训练过慢就升hidden_size6432 ~ 128LSTM 隐层宽度, 越大拟合能力越强也越容易过拟合num_layers21 ~ 3超过 3 层收益很小还容易梯度爆炸epochs5030 ~ 100看验证集是否还在提升, 不盲目跑满调参习惯我建议一次只动一个参数。先拿默认配置跑出基准再单独调 window_size 或 lr每动一次记录一次验证集准确率。如果验证集比训练集差很多优先怀疑过拟合加 dropout 或扩大重叠度比无脑加层更有效。4. 加载训练好的模型做推理从权重文件到故障类别4.1 权重加载的正确姿势与常见报错资源里的 best_model.pth 是训练完成后的最优权重。加载前先确认模型定义里的 num_classes 和权重保存时一致否则直接 size mismatchimport torch from model import CNNLSTM # 按源码里的模块名导入 model CNNLSTM(num_classes4) state_dict torch.load(models/best_model.pth, map_locationcpu) model.load_state_dict(state_dict) # strictTrue 默认开启 model.eval()map_locationcpu 解决训练在 GPU、推理在 CPU 的迁移问题。如果权重键名带 module. 前缀说明保存时用了 DataParallel加载前要去掉前缀from collections import OrderedDict new_state_dict OrderedDict() for k, v in state_dict.items(): new_state_dict[k.replace(module., )] v model.load_state_dict(new_state_dict)这段代码在实验室里用过太多次。多卡训练出来的模型单卡加载时几乎必然遇到这个前缀问题提前写好转换逻辑能省不少排查时间。如果加载时用 strictFalse 把不匹配的键打印出来对照 missing_keys 和 unexpected_keys也能很快定位是类别数改过还是结构动过。4.2 单样本推理把一段原始信号变成故障类型推理函数做的事和训练预处理完全对称归一化、reshape、前向传播、softmaximport numpy as np import torch def predict_segment(model, signal, devicecpu): 输入一段一维振动信号, 输出故障类别与各类别置信度。 参数: model: 已加载权重的 CNNLSTM 实例 signal: 一维 ndarray, 长度需不小于 window_size 返回: (pred_class, prob_array), prob_array 形状为 (num_classes,) model.eval() mean signal.mean() std signal.std() x (signal - mean) / (std 1e-6) # 与训练一致的按样本归一化 x torch.tensor(x, dtypetorch.float32).view(1, 1, -1) with torch.no_grad(): logits model(x.to(device)) prob torch.softmax(logits, dim-1) pred int(torch.argmax(prob, dim-1).item()) return pred, prob[0].cpu().numpy()eval() 和 no_grad() 缺一不可。前者关掉 dropout后者关掉梯度计算推理才能又稳定又省内存。归一化务必和训练保持一字不差训练按样本归一化推理也按样本归一化分布一旦不一致准确率会掉得莫名其妙。view(1, 1, -1) 把一维数组补成单样本单通道输入-1 让 torch 自动推断长度。调用前还要加一道长度判断len(signal) 小于 window_size 时要么补零到窗口长度要么直接拒绝并提示重新采样。宁可多写三行防御代码也不要让一个短信号把整个推理流程带崩。4.3 类别映射与置信度判断模型输出 4 个类别的概率要和故障类型对上号。源码沿用的是主流的轴承故障分类约定类别索引故障类型英文缩写故障特征频率方向0正常N无固定周期冲击1内圈故障IR冲击频率与内圈故障特征频率相关2外圈故障OR冲击频率与外圈故障特征频率相关3滚动体故障B冲击间隔随保持架位置变化实际部署我不建议只看最大概率就下结论。单段信号可能恰好落在两次冲击的间隙特征不典型。更稳的做法是连续取多段信号分别预测再投票from collections import Counter def predict_stream(model, signal, window_size1024, step512): 对长信号分窗口预测, 返回出现次数最多的类别 preds [] for i in range(0, len(signal) - window_size, step): seg signal[i:i window_size] pred, _ predict_segment(model, seg) preds.append(pred) return Counter(preds).most_common(1)[0][0]投票还会带来一个额外收益如果不同窗口的预测结果明显打架比如一半判正常一半判外圈故障说明这段信号处于临界状态应该判为“疑似故障、建议复检”而不是强行给一个肯定结论。这种打架信号本身就是很有价值的检修提示。5. 避坑与排查五个把训练搞翻车的常见问题以下五条是我在类似项目里反复踩过的坑也是这套源码最容易在复现时出问题的地方。每条按“现象、原因、解决”展开照着排查比从头读源码快得多。5.1 训练集和测试集混着切窗口准确率虚高到 99%现象验证集上准确率接近 99%一把模型拿到现场新采集的数据上准确率直接掉到五六成。 原因切窗口之前没有按文件划分数据集而是把所有文件拼成大数组切完窗口再做随机划分。同一段信号的相邻窗口高度重叠等于把测试答案提前泄给了训练过程这是轴承故障诊断里最典型的数据泄漏。 解决先按原始文件分 train/test再对每份文件单独切窗口。划分时带上 stratify 保证类别比例一致。代码就是我前面写的 train_test_split 那种写法顺序不能反过来这是血泪经验。5.2 load_state_dict 报 size mismatch 或者 key 对不上现象加载权重时报错提示 fc.weight 形状对不上比如拷入的 shape 是 [4, 64]当前模型期望 [6, 64]或者提示 size mismatch for lstm.weight_ih_l0。 原因模型定义里的 num_classes 和权重保存时不一致这是最常见的原因另一类是改动过模型结构层名或维度漂移。 解决先查项目说明文档确认类别数把 CNNLSTM(num_classesN) 改一致。多卡保存的权重会带 module. 前缀用 OrderedDict 转换去前缀。最后用 strictFalse 加载一次打印 missing_keys 和 unexpected_keys对照差异很快能定位是哪层变了。5.3 loss 突然变成 NaN准确率归零现象训练到中途 loss 变成 nan后面指标全部归零重新跑也时好时坏。 原因学习率太大导致参数发散或者 LSTM 梯度爆炸偶尔是数据里混进了 inf 或 nan。轴承信号幅值波动大强冲击段最容易触发这类问题。 解决先把 lr 降到 1e-4 或 3e-4再在 optimizer.step() 前加 clip_grad_norm_ 把梯度范数限制到 5.0同时用 np.isnan(data).any() 和 np.isinf(data).any() 检查预处理后的数据。三步下来99% 的 NaN 能解决。5.4 实验室指标很好现场数据却全错现象在实验室数据集上跑出 95% 以上准确率模型部署到现场预测结果和人工判级对不上。 原因数据域不同。实验室数据的采样率、转速、负载、传感器安装位置固定现场工况千变万化特征分布已经偏移验证集指标只在分布相近的数据上有效。 解决两个方向。一用现场少量已标注数据微调冻结卷积层只训 LSTM 和全连接层lr 降到 1e-4几十个 epoch 就能拉回现场分布二把实验室训练好的模型当特征提取器现场数据只训最后的分类头比从零训练靠谱得多。5.5 训练跑完才发现最优权重没保存现象训练日志显示第 30 个 epoch 验证准确率最高结果文件里保存的是第 50 个 epoch 的权重性能已经回落。 原因训练脚本只在结束时保存了一次权重没有按验证集指标做 checkpoint。后几个 epoch 在过拟合等想找最优模型时后悔药都没得吃。 解决每个 epoch 结束后在验证集上算准确率高于历史最优就覆盖保存 best_model.pth同时把 epoch 号写进文件名。这样不管中途断电还是提前停手里最差还有最优权重兜底。6. 进阶验证混淆矩阵、t-SNE 与多窗口投票的配合6.1 用混淆矩阵定位易混类别训练完别急着看总准确率先在验证集上出一张混淆矩阵看哪些类别在互相打架from sklearn.metrics import confusion_matrix, classification_report cm confusion_matrix(y_true, y_pred) print(classification_report( y_true, y_pred, target_names[正常, 内圈, 外圈, 滚动体] ))滚动体故障常常和外圈故障混在一起原因是两者的冲击周期接近窗口长度短的时候特征差异不明显。如果混淆集中在这两类上优先把 window_size 调大让模型看到更长的周期信息比加层数更管用。6.2 t-SNE 看特征是否真的可分分类准确率是一个标量它不告诉你特征空间长什么样。t-SNE 能把高维特征压到二维直观看到各类别是否聚成簇from sklearn.manifold import TSNE feats all_features # 取模型全连接层之前的特征, 形状 (N, 64) tsne TSNE(n_components2, perplexity30, random_state42) proj tsne.fit_transform(feats)如果图里各类别的点各自聚成一团、边界清晰说明 CNN-LSTM 学到的特征分得开后面接任何分类器都能有不错的表现。如果两类点完全叠在一起那不要指望把全连接层换成更复杂的分类器能救回来问题出在前面的特征提取。t-SNE 的 perplexity 一般设在 5 到 50 之间样本少就取小值样本多就取大值30 是中等规模验证集的经验起点。从拆到这套源码到现在我逐渐养成一个习惯每次接手新的诊断模型第一步不是调参而是把数据划分和归一化逻辑从头到尾顺一遍确认训练和推理完全对称第二步是跑验证集混淆矩阵加 t-SNE先看特征再说准确率。这两步走完后面调参基本水到渠成。希望帮到你。本文还有配套的精品资源点击获取