简介:滚动轴承故障诊断是设备健康监测中的典型任务,基于深度学习的智能诊断方案常用于毕业设计、课程设计与工程实训。该Python项目以CWRU公开数据集为对象,完整覆盖数据预处理、DNN与CNN模型构建、训练评估以及结果可视化等环节,四个Python脚本分别实现数据生成、预处理、模型训练与散点图绘制,同时配有七份Markdown说明文档,对实现细节与实验流程进行讲解。压缩包共41个文件,以30个MAT格式数据文件为主,整体大小34.86MB,目录结构清晰,便于按模块学习与二次开发。项目源自作者大四毕业设计,经导师指导并认可高分通过,评审分98.5分,非常适合计算机相关专业学生作为毕业设计、课程设计或期末大作业参考。目前已有145人学习使用,对于希望掌握深度学习故障诊断完整流程的入门与进阶学习者,是一份含金量较高的实战资源。
1. 滚动轴承故障诊断遇上深度学习:为什么这行代码值得你花时间
如果你维护过旋转设备,一定经历过这种场景:振动传感器采到了异常信号,但时域波形看不出毛病,频谱图上一堆杂峰,老师傅靠耳朵听音判断,新人只能对着报警值猜。滚动轴承故障诊断这个领域,传统方法卡在特征提取这一步——时域指标、频域特征、包络谱分析,每一套都需要大量专家经验去调。深度学习的价值在于,它把“特征工程”这件最玄学的事变成了“喂数据”和“调参”。你只需要准备好带标签的振动信号,模型自己去学故障模式的特征表达。
这类“Python基于深度学习的滚动轴承故障诊断项目源代码+使用说明”,本质是一个完整可跑的故障诊断方案,通常包含数据预处理脚本、模型定义、训练验证代码和一个教你一步步复现的说明文档。它适合两类人:一是做设备状态监测的工程师,想快速验证深度学习方法在自己数据上的效果;二是学生和转行者,需要一个能写进简历、能答辩的实战项目。这篇笔记,我按自己做过这类方案的流程,把数据怎么准备、模型怎么选、训练怎么调、坑在哪里一条条讲透。
2. 先搞懂诊断流程:从原始振动信号到故障分类结果
2.1 振动信号为什么能反映轴承故障
滚动轴承的故障类型,比如内圈裂纹、外圈点蚀、滚动体磨损,在运转时会激发出不同频率成分的冲击振动。这些冲击信号有周期性,周期由轴承几何尺寸和转速决定。传统诊断是拿包络谱去找这些特征频率,但你得先知道轴承型号,会算特征频率,还要处理噪声干扰。
深度学习方法绕开这条路径。它直接把原始振动信号或简单的时频表示作为输入,让卷积网络自动学习“内圈故障 vs 外圈故障 vs 正常”之间的细微差别。实际效果上,在公开数据集里,好的模型对角缺陷分类准确率能到95%甚至99%以上,在实验室数据上表现稳定。但记住一个前提:训练数据和实际现场数据分布越接近,效果越可靠,这一点后面避坑章节会重点说。
2.2 数据预处理:多数“高分项目”的核心工作量在这里
我见过不少人拿到这类项目源码,第一件事是去看模型结构,觉得神经网络是黑匣子,调几个卷积核就完事。实际上,这类诊断项目七成的工作量在数据预处理和数据集构建上。原始振动数据是连续采样的时序信号,不能整段塞进网络,需要切成短样本。常见做法是用滑动窗口切分,每个窗口长度覆盖轴承旋转的2到3个周期,这样每个样本既包含足够多的冲击特征,又不会因为序列太长导致训练困难。
假设采样率是12kHz,轴承转速是1800转每分钟,也就是30转每秒,每转对应的采样点是400个,覆盖两个周期就需要800个点,留点余量可以取1024。下面是切分代码的常见写法:
import numpy as np def split_windows(signal, window_len=1024, stride=512): """ 将一维振动信号切成固定长度的样本窗口。 window_len: 窗口长度,建议覆盖 2~3 个旋转周期 stride: 窗口滑动步长,一般取 window_len 的一半,保证相邻窗口有重叠 """ windows = [] total = len(signal) for start in range(0, total - window_len, stride): windows.append(signal[start:start + window_len]) return np.array(windows) # 示例:加载某段振动数据,假设 data 是 shape 为 (N,) 的 float 数组 # windows = split_windows(data, window_len=1024, stride=512) # 切出来的每个窗口就是一个独立样本 print(f"信号长度 {len(data)},切出 {len(windows)} 个样本")这段代码的核心参数是窗口长度和步长。窗口太短,样本里装不下完整的冲击周期,模型学到的特征是残缺的;窗口太长,样本量大但信息冗余,训练变慢且容易过拟合。步长决定了样本之间的重叠度,重叠越多,样本量越大,相当于做了数据增强,但相邻样本高度相关,如果你把相似的样本同时放进了训练集和测试集,评估结果会虚高。
一个容易被忽略的细节是振幅归一化。不同设备、不同测点的振动幅值范围差异很大,有的峰值0.5g,有的峰值5g,如果不做归一化,模型会偏向振幅大的维度。我一般按样本做标准化,而不是按全局最大值缩放,原因是现场的振动信号可能有偶发冲击峰值,全局缩放会把正常信号压扁。
def normalize_windows(windows): """ 逐个窗口做 z-score 标准化,均值为 0,标准差为 1。 注意:一定要在划分完训练/测试集之后再对训练集计算均值方差, 再拿训练集的统计量去标准化测试集,防止数据泄漏。 """ mean = np.mean(windows, axis=(1), keepdims=True) std = np.std(windows, axis=1, keepdims=True) return (windows - mean) / (std + 1e-8)参数里的1e-8是防止某个窗口的信号幅值恒定导致标准差为0,这种情况在传感器断线时会出现。归一化的另一个好处是让模型对不同的设备安装位置、不同的信号幅值尺度更鲁棒——这是这类项目能“迁移”到其他设备数据上的关键操作。
2.3 标签构建与数据集划分的讲究
切好窗口后,每个窗口都要对应一个故障类别标签。标签从哪里来?公开数据集里通常有配置文件说明每个文件对应的工况,按文件名映射即可。这里有一个新手最容易踩的雷:按“文件”划分数据集,还是按“窗口”划分数据集?
按窗口随机划分,代码写起来最简单,但同一段原始信号切出的相邻窗口可能被分到训练集和测试集,模型等于见过测试样本的一部分,测试准确率虚高,到了现场新数据上就翻车。正确做法是按文件或按连续信号段划分,保证同一个运行工况下的数据段不会同时出现在两边。
from sklearn.model_selection import GroupShuffleSplit # 假设 file_ids 是每个窗口对应的原始文件编号 # windows 是切好的样本数组,labels 是对应的故障类别 split = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, test_idx = next(split.split(windows, labels, groups=file_ids)) train_windows, test_windows = windows[train_idx], windows[test_idx] train_labels, test_labels = labels[train_idx], labels[test_idx]这里的关键是GroupShuffleSplit而不是普通的train_test_split。以数据文件为单位分组,防止同一文件的窗口跨集。随机种子固定一下,保证复现。组的数量如果太少,比如只有四个工况文件,那划分出来的训练集和测试集分布可能差异很大,这时候要考虑按工况混合。
3. 模型选型和训练:用PyTorch搭建一个能跑的1D-CNN诊断网络
3.1 为什么首选1D-CNN而不是LSTM或Transformer
滚动轴承振动信号是一维时序数据,候选模型有几类:1D-CNN、LSTM、Transformer。我自己的经验是,1D-CNN是这类项目最稳的起点。原因有三点:
第一,卷积核天然具有局部感受野,能捕捉振动冲击的局部波形特征,这对故障诊断是核心能力。第二,1D-CNN的训练速度和收敛稳定性远好于LSTM和Transformer,尤其在样本量不大(几千到几万窗口)时,LSTM容易过拟合且训练慢,Transformer需要更大数据量才发挥得出来。第三,1D-CNN的参数量适中,CPU也能跑,部署到边缘设备不需要特别强的算力。
当然不是说LSTM完全没用,有些场景下振动信号带有明显的缓变趋势,比如转速变化过程,LSTM能建模时序依赖,但作为打底方案,1D-CNN的性价比最高。你在项目说明里也会看到,多数高分项目用的就是1D-CNN或2D-CNN配合时频图,很少有直接用LSTM的。
3.2 网络结构设计:三个卷积块加一个分类头
下面是这类项目里最常见的一种结构,输入1024点的一维信号,输出故障类别数。以四分类为例:正常、内圈故障、外圈故障、滚动体故障。
import torch.nn as nn class BearingCNN(nn.Module): def __init__(self, num_classes=4): super(BearingCNN, self).__init__() self.features = nn.Sequential( nn.Conv1d(1, 32, kernel_size=3, padding=1), nn.BatchNorm1d(32), nn.ReLU(inplace=True), nn.MaxPool1d(kernel_size=2), nn.Conv1d(32, 64, kernel_size=3, padding=1), nn.BatchNorm1d(64), nn.ReLU(inplace=True), nn.MaxPool1d(kernel_size=2), nn.Conv1d(64, 128, kernel_size=3, padding=1), nn.BatchNorm1d(128), nn.ReLU(inplace=True), nn.MaxPool1d(kernel_size=2), ) self.classifier = nn.Sequential( nn.AdaptiveAvgPool1d(1), nn.Flatten(), nn.Dropout(0.3), nn.Linear(128, 64), nn.ReLU(inplace=True), nn.Linear(64, num_classes), ) def forward(self, x): return self.classifier(self.features(x))解释一下几个关键层选择的理由。Conv1d的卷积核设为3,这是时序信号里最安全的感受野,既能提取局部模式又不会太激进。BatchNorm1d放在卷积和激活之间,作用是稳定训练收敛,这对振动信号这种分布范围较大的输入尤其重要。AdaptiveAvgPool1d(1)把每个通道压成一个标量,这样不管输入窗口长度是1024还是2048,后面的全连接层都不需要改。
激活函数选了ReLU,这是默认选项,没有特殊原因不需要换GELU或Swish,数据集规模有限时差距不明显。Dropout取0.3,太小起不到正则作用,太大可能让网络欠拟合,0.3是一个在多个数据集上都稳的值。
3.3 训练脚本:损失函数、优化器与学习率策略
训练过程这里给出一个完整的模板,你拿到项目源码后可以直接对照这个结构去改自己的任务。
import torch from torch.utils.data import DataLoader, TensorDataset # 将 numpy 数组转成 PyTorch 数据加载器 def make_loader(windows, labels, batch_size=64, shuffle=True): tensor_x = torch.tensor(windows, dtype=torch.float32).unsqueeze(1) tensor_y = torch.tensor(labels, dtype=torch.long) dataset = TensorDataset(tensor_x, tensor_y) return DataLoader(dataset, batch_size=batch_size, shuffle=shuffle) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = BearingCNN(num_classes=4).to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode="min", factor=0.5, patience=5 ) best_acc = 0.0 for epoch in range(50): model.train() running_loss = 0.0 for xb, yb in train_loader: xb, yb = xb.to(device), yb.to(device) optimizer.zero_grad() out = model(xb) loss = criterion(out, yb) loss.backward() optimizer.step() running_loss += loss.item() * xb.size(0) # 验证 model.eval() correct, total = 0, 0 val_loss = 0.0 with torch.no_grad(): for xb, yb in val_loader: xb, yb = xb.to(device), yb.to(device) out = model(xb) loss = criterion(out, yb) val_loss += loss.item() * xb.size(0) _, preds = torch.max(out, 1) correct += (preds == yb).sum().item() total += yb.size(0) val_acc = correct / total avg_val_loss = val_loss / len(val_loader.dataset) scheduler.step(avg_val_loss) print(f"Epoch {epoch+1:02d} | Loss {running_loss/len(train_loader.dataset):.4f} " f"| Val Acc {val_acc:.4f}", flush=True)几个参数的取舍要说明。优化器直接用Adam,lr设1e-3,这是最不容易出错的组合。学习率策略用ReduceLROnPlateau,验证损失连续5个epoch不降就把学习率减半,这个策略在诊断任务上有奇效——深度学习模型的训练有个特点,前期收敛快,后期在局部极小值附近震荡,减小学习率才能继续下降。
batch_size设64,如果显存不够就降到32,但一般这个网络规模在1024输入长度下显存占用非常小,低端显卡也能跑。Epoch设50,配合早停的概念,实际训练中当验证准确率连续10个epoch不再提升就可以停了,防止过拟合。
3.4 诊断效果评估:除了准确率还要看混淆矩阵
很多项目说明只给一个最高的test accuracy,比如99.2%,看起来很美。但做故障诊断不能只看总准确率,还关心哪个类和哪个类容易被混淆。比如滚动体故障和正常状态在早期阶段特征非常相似,就算总准确率95%,滚动体故障那一类的召回率可能只有70%,这意味着大量故障被漏报。
验证时把混淆矩阵打印出来,这是检验模型真实能力的标准操作:
from sklearn.metrics import confusion_matrix, classification_report # 在测试集上收集预测结果 model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for xb, yb in test_loader: xb = xb.to(device) out = model(xb) _, preds = torch.max(out, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(yb.numpy()) print(confusion_matrix(all_labels, all_preds)) print(classification_report(all_labels, all_preds, target_names=["normal", "inner", "outer", "roller"]))classification_report会给你每个类别的精度、召回率、F1值。我判断一个诊断模型能不能交付,不是看总准确率,而是看最低那一类的召回率——只要有一类故障经常被漏掉,到了现场就是事故隐患。
4. 避坑指南:训练集99%准确率、现场却失灵的5个原因
4.1 同一个文件的数据泄漏:测试集见过训练集
现象是训练时验证准确率稳定在98%以上,测试集表现也很漂亮,但把模型拿到另一台设备上采集的数据上预测,准确率掉到60%以下。
原因是数据集划分时没有按文件分组,直接随机打乱窗口,导致同一段振动信号切出的相邻窗口被分到了训练集和测试集。模型见过的信息已经泄漏到测试集里,测试分数虚高。
解决方法是回到第2.3节的GroupShuffleSplit,按原始信号文件分组划分。划分之后检查一下:同一个文件名对应的窗口不能同时出现在两个集合里。
4.2 振动信号幅值范围不一致:模型学偏了
现象是训练损失正常下降,但测试时某几个类别概率始终偏向某一类,查看输入数据发现不同类别样本的振幅差异极大。
原因是训练集里的不同工况数据振幅范围差异过大,比如正常状态是0.2g,故障状态是3g。如果不做标准化,模型很可能直接拿振幅大小当判别依据,而不是学习故障波形特征,换了测点或者换了负载就不灵了。
解决方法是按样本做z-score标准化,而不是按全局最大最小缩放。玄学一点说,深度学习模型有时候就是会偷懒,你给它提供一个“捷径特征”,它就会走捷径,标准化可以堵住这条捷径。
4.3 训练损失不降或震荡:学习率和BatchNorm的问题
现象是loss在0.8附近上下跳动,训练50个epoch也没降下去,或者验证准确率一直在20%到40%之间随机横跳。
原因是学习率过大导致参数在最优解附近反复横跳,或者BatchNorm的batch_size太小。扁平化来说,batch_size设为16甚至8时,BatchNorm统计量不稳定,训练过程容易震荡。还有一种情况是模型初始化出了问题,但由于PyTorch默认初始化一般没事,优先级低一点。
解决方法是先把batch_size提到64,学习率降为1e-3,加上ReduceLROnPlateau策略。如果还不收敛,把Dropout临时改为0确认模型容量足够,再看是不是数据标签有错位。标签错位这种事,我还真见过不止一次——某个公开数据集的文件命名和实际故障类型对不上,需要你自己去做频谱验证。
4.4 单窗口预测结果抖动:类别输出忽高忽低
现象是拿一段连续的现场数据做诊断,模型预测结果像心电图一样上下跳,同一个轴承半分钟内预测出了三种不同的故障类型。
原因是单个窗口只包含1024个点,如果轴承转速低,这个窗口覆盖不到一个完整旋转周期,故障冲击特征不完整。另一个原因是现场噪声干扰强,单窗口特征不明显。
解决方法是不要太依赖单窗口输出,用滑动窗口连续预测然后取多数投票。工程上我一般会连续切5到10个窗口,统计预测类别的众数作为最终诊断结果。这样还能顺便输出一个置信度——如果10个窗口里有8个预测为同一类,置信度就是80%,逻辑清晰且现场可解释。
4.5 公开数据集表现好,换设备就失效:跨域泛化问题
现象是一个在公开数据集上训练好的模型,拿到工厂实际设备上测试,准确率断崖式下跌。
原因是训练数据和现场数据的分布差异,包括采样率不同、传感器安装位置不同、转速负载不同、环境噪声不同。这是深度学习诊断方向最核心的工程瓶颈,论文里叫“跨域诊断问题”。
解决思路有三条。第一,微调,拿现场少量带标签数据对模型做几次epoch的增量训练。第二,数据对齐,把现场数据的采样率重采样到与训练数据一致,并且做带通滤波把无关频段滤掉。第三,不要把模型当作万能方案,在项目交付时明确说明模型的使用边界,哪些工况可以用、哪些不能。
提示:公开数据集只是一个起点,真正要部署到现场,至少要采集目标设备正常工作时段的数据做验证。没有现场数据,任何离线测试指标都只能说明模型在实验室环境下的能力,这点在跟客户汇报时也要提前说清楚。
5. 进阶用法:把模型封装成可调用的诊断工具
5.1 多数投票和一票否决策略
训练完毕后,把模型部署成诊断工具时我一般会写一个简单封装,不搞复杂的实时流处理框架,先用一个函数跑通流程。下面这段代码做了两件事:预测每个窗口的类别,然后做多数投票,并统计各类别的投票占比。
def predict_bearing_state(model, signal, window_len=1024, stride=512, device="cpu"): model.eval() windows = split_windows(signal, window_len=window_len, stride=stride) windows = normalize_windows(windows) tensor_x = torch.tensor(windows, dtype=torch.float32).unsqueeze(1).to(device) with torch.no_grad(): outputs = model(tensor_x) probs = torch.softmax(outputs, dim=1) preds = torch.argmax(outputs, dim=1).cpu().numpy() # 统计各类别投票数 import collections vote_count = collections.Counter(preds.tolist()) total = len(preds) result = { "final_state": vote_count.most_common(1)[0][0], "vote_ratio": {k: v / total for k, v in vote_count.items()}, "mean_prob": probs.mean(dim=0).cpu().numpy().tolist(), } return resultvote_ratio这个字段不要忽略。我习惯给它设一个阈值,比如最终类别占比低于0.6,就输出“疑似异常但置信度不足”,而不是硬给一个分类结果。机器诊断可以不完美,但不能在不确定的时候乱断病。
5.2 可视化与报告输出
诊断项目如果要做成可交付的成果,图和报告比模型代码更值钱。至少需要三种可视化:原始振动信号波形图、预测类别随时间的变化图(模型输出的类别序列),以及模型在时频域的注意力可视化。
import matplotlib.pyplot as plt def plot_diagnosis(signal, window_len=1024, stride=512, model=None, device="cpu", title="Bearing Diagnosis"): fig, axes = plt.subplots(2, 1, figsize=(12, 6)) axes[0].plot(signal, linewidth=0.8) axes[0].set_ylabel("Amplitude (g)") if model is not None: # 按窗口预测并画出随时间的类别变化 states = [] positions = [] for start in range(0, len(signal) - window_len, stride): seg = signal[start:start + window_len].reshape(1, -1) seg = normalize_windows(seg) tensor_x = torch.tensor(seg, dtype=torch.float32).unsqueeze(1).to(device) with torch.no_grad(): pred = torch.argmax(model(tensor_x), dim=1).item() states.append(pred) positions.append(start + window_len // 2) axes[1].scatter(positions, states, s=4, c="darkred") axes[1].set_ylabel("Fault code") axes[1].set_yticks([0, 1, 2, 3]) plt.suptitle(title) plt.tight_layout() plt.show()这段代码胜在简单够用。故障代码的y轴刻度你自己定义,比如0正常、1内圈、2外圈、3滚动体。实际交付时,我会把这类图整理成PDF报告,配合表格输出统计结果。
5.3 模型压缩与部署思路
如果后续要把模型部署到边缘设备,比如 Raspberry Pi 或工控机,建议把PyTorch模型导出成ONNX格式。流程是先拿验证集跑一遍测试,确认导出前后结果一致。
# 导出 ONNX import torch.onnx dummy_input = torch.randn(1, 1, 1024) torch.onnx.export( model, dummy_input, "bearing_cnn.onnx", input_names=["signal"], output_names=["logits"], dynamic_axes={"signal": {0: "batch"}, "logits": {0: "batch"}}, opset_version=11, )这里dynamic_axes设置了batch维度可变,这样部署时不必重新导出就可以一次送多个样本。ONNX模型用onnxruntime推理,在CPU上的速度比PyTorch快不少,RaspberryPi上跑一个1D-CNN的推理延迟也就几十毫秒,够用。
我自己的习惯是,导完ONNX后一定在测试集上做一个完整的前后对比,偏差超过0.5%就要查是不是导出过程中算子被替换了。深度学习框架的导出看起来是傻瓜操作,但精度变化是真实存在的风险,尤其是在BatchNorm被折叠到卷积里的时候。
说到底,做故障诊断项目,最重要的不是模型结构多新颖,而是整个流程闭环能不能跑通、指标是不是真实可信。我吃过一次亏:曾经拿一个公开数据集训练出98%准确率的模型,直接拿到客户现场试用,结果因为采样率不一致,实际准确率不到70%。从那以后,我每做一个诊断模型,先看数据采集配置,再调模型,最后谈性能指标。这个顺序,希望帮到你。
本文还有配套的精品资源,点击获取