简介面向计算机相关专业毕业设计及课程设计场景这份基于深度学习的滚动轴承故障诊断项目提供了可直接运行的完整 Python 源码和全部实验数据。项目以 CWRU 轴承数据集为核心涵盖数据预处理、特征提取、模型训练与结果可视化全流程内置 CNN、DNN 等网络实现并配有使用说明与 Markdown 文档便于理解故障诊断任务的每个环节。包内共 41 个文件以 mat 数据文件为主包含 30 份原始/处理后数据4 个 py 脚本覆盖数据生成、模型构建和散点图绘制等关键操作另有 6 个 md 说明文档和 1 个 txt 使用说明资源压缩包总大小约 34.87MB解压后即可按文档复现并在此基础上进行改进。项目代码经调试可直接复现运行既可支撑毕业设计答辩也能作为深度学习实战练习的参考范例。目前已有 970 人学习下载适合希望快速搭建故障诊断基线方案的本科生或初级学习者。1. 滚动轴承故障诊断的深度学习项目这个 Python 源码包为什么值得你复现“准确率 99%那你说说模型到底靠什么把内圈故障和滚动体故障分开的”这是我在做基于深度学习的滚动轴承故障诊断毕业设计时被导师问到的第一个硬问题。只看训练曲线根本接不住直到我把隐藏层特征投影成散点图不同故障在平面上清清楚楚分成几团答辩才算稳住。这个项目就是为解决这类问题准备的——基于深度学习的滚动轴承故障诊断 Python 源码包数据来自 CWRU 公开轴承数据集覆盖预处理、DNN 与 CNN 两个模型、训练评估、特征可视化完整链路让“模型真的学到了故障特征”这件事变得可见、可解释。适合正在做毕业设计、课程设计以及需要项目实战练习的学习者新手能照着 README 跑通全流程老手可以直接拿它的数据划分和可视化脚本做跨工况实验。2. CWRU 数据预处理从 .mat 原始信号到模型输入张量的完整链路2.1 CWRU 数据集的工况构成1750、1730、1772 rpm 分别代表什么CWRU凯斯西储大学轴承数据集是所有做机械故障诊断的人绕不开的公开基准。它记录的是电机轴承在正常、内圈故障、外圈故障、滚动体故障四种状态下的振动加速度信号每种状态按故障直径细分成 0.007、0.014、0.021 英寸等规格原始数据以 .mat 格式保存。打开任意一个 .mat 文件里面通常是三个通道同时存在DE驱动端加速度计、FE风扇端加速度计、BA基座加速度计其中 DE 和 FE 是两种信号质量差异很大的来源。整个数据落到磁盘上是按文件拆开的这个项目的目录和文件名里直接标了 1750、1730、1772 三个转速。它们与电机负载的关系是固定的整理出来长这样负载hp实测转速rpm在项目里的常见用途1约 1772轻载工况通常作为训练主力数据2约 1750中载工况适合做交叉验证3约 1730重载工况适合留到最后做盲测提示CWRU 公开数据里还有 0 负载对应的 1797 rpm这个包的默认路径里没把它收进来。下载后先扫一遍数据目录确认你手上实际有几档转速再对照源码里的 file_list 修改路径这是新手第一次运行失败的最常见原因。为什么源码要按转速把数据分开保存而不是全部混成一个训练集因为轴承在不同负载下的振动特征分布是有差异的。全混着训虽然能抬高单一测试集上的数字但会掩盖模型是否真的学到了物理上可解释的特征。按转速分好之后你就可以做跨工况验证——用 1772 的数据训练拿到 1750 上去测这个实验写在论文讨论部分非常加分。另外要提醒一点项目里默认取 DE 通道如果你的原始 .mat 文件只有 FE 通道数据预处理脚本读出来的信号来源和原设定不一致后面所有实验都会建立在错误的数据上。2.2 滑动窗口切片creat_data.py 是怎么把连续信号切成样本的CWRU 原始信号每段都是十几万甚至几十万点的连续采样深度模型不可能直接整段吃下去标准做法是滑动窗口切片。源码里的 creat_data.py 承担的就是这个工作核心逻辑抽出来看大致是下面这个样子# creat_data.py 核心逻辑抽取不完全等于原文件 import scipy.io as sio import numpy as np def load_cwru_mat(path): # CWRU 的 .mat 文件里信号存在 dict 中键是 DE 或 FE mat sio.loadmat(path) key DE if DE in mat else FE # 原始数据形状可能是 (1, N)先压成一维 return mat[key].reshape(-1) def sliding_window(signal, window_size2048, step1024): # window_size 是单个样本的长度step 是每次滑动的步长 # step window_size 时窗口有重叠样本总数会变大 total len(signal) samples [] for start in range(0, total - window_size 1, step): samples.append(signal[start : start window_size]) return np.asarray(samples)这里有两个参数直接决定样本量。window_size 取 2048在 12kHz 采样率下对应约 0.17 秒的时长足够覆盖多个旋转周期让周期性冲击特征稳定出现在每个样本里step 取 1024 时窗口重叠一半数据量大概是完全不重叠切片的两倍对 CWRU 这种单个 .mat 只有几十万点的数据来说样本量往往决定着模型会不会过拟合所以项目选择有重叠的切片方式是合理的。切片完成后每个窗口都要打上对应的故障类别标签。需要特别注意的是CWRU 的 .mat 文件名缩写很容易读错比如 IR007 表示内圈故障 0.007 英寸OR0076 表示外圈故障 0.007 英寸且负载在 6 点钟方向B007 表示滚动体故障。在写标签映射时我的习惯是先打印文件名和标签的对应关系人工核对一遍不要用目录名的字母排序去猜。2.3 数据归一化与训练/验证划分模型能不能收敛一半取决于这里切片和打标签之后下一个关键动作是归一化和数据划分。很多初学者的坏习惯是先对整个数据集做归一化再切训练集和测试集这在严格意义上属于信息泄漏——测试集的统计量已经参与了训练数据的变换。正确的顺序是先按类别分层切分再在训练集上拟合归一化参数然后用同样的参数变换验证集和测试集这一步做反了后面调参全是白费。源码里根目录下的 README.md 和使用说明.txt 对目录约定有比较清楚的交代pre_data 目录承担了数据整理工作把不同类别的样本路径、标签和划分方式统一管理起来。常见的划分与归一化实现方式如下from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split # 假设 X 是所有切片样本y 是整数标签 # 先分层划分保证每个类别在训练/测试里的比例一致 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) # 用 StandardScaler 做标准化先 fit 训练集再 transform 测试集 scaler StandardScaler() X_train_flat X_train.reshape(len(X_train), -1) scaler.fit(X_train_flat) # 只让训练集参与统计量计算 X_train scaler.transform(X_train_flat).reshape(X_train.shape) X_test_flat X_test.reshape(len(X_test), -1) X_test scaler.transform(X_test_flat).reshape(X_test.shape)两个要点解释一下。stratifyy 是分层抽样保证内圈故障、外圈故障这些类别在划分后比例不变否则某个小类别可能全落到测试集里训练时那一类直接学不到StandardScaler 生成的是每个维度的均值和方差如果某个位置在所有样本上都是常数值方差接近 0标准化后会出现极大值这也是后面训练 loss 变成 nan 的常见原因之一看到 nan 先回来查这一步。注意如果你后续修改了 creat_data.py 里的 step 参数让窗口产生了重叠那上面这种按样本随机划分的方式就存在信息泄漏风险。同一个原始 .mat 文件切出来的相邻窗口可能同时进入训练集和测试集具体处理办法在第 5 章 5.3 节展开。3. DNN 和 CNN 两个诊断模型选型逻辑与可运行的训练代码3.1 DNN 基线模型为什么毕业设计里全连接网络仍然不可少很多同学看到“深度学习故障诊断”就下意识觉得应该上 CNN、LSTM 甚至注意力机制但源码里第一个模型是 DNN三层全连接网络。这个选择不是为了应付而是有很实际的答辩价值全连接网络是所有深度模型里最好解释的一个。你可以在论文对比实验里把它当 baseline也可以用它证明“同样的特征更简单的网络已经能分到 90% 以上复杂模型的提升到底来自哪里”。与其对着《动手深度学习》里图像分类那一章硬套不如直接在机械信号上跑一个完整流程对你的毕业设计更有直接帮助。DNN 的输入就是一维信号展平后的 2048 维向量中间加两个带 Dropout 的隐藏层输出层用 Softmax 做四分类。核心结构抽出来看大致如下# dnn.py 核心结构抽取 import numpy as np from tensorflow.keras.layers import Input, Dense, Dropout, Flatten from tensorflow.keras.models import Model from tensorflow.keras.optimizers import Adam num_classes 4 # 正常 / 内圈故障 / 外圈故障 / 滚动体故障 inputs Input(shape(2048, 1), nameinput) x Flatten()(inputs) # (batch, 2048, 1) - (batch, 2048) x Dense(512, activationrelu)(x) x Dropout(0.5)(x) # 第一个隐藏层后丢一半神经元防过拟合 x Dense(128, activationrelu, namelast_dense)(x) x Dropout(0.3)(x) outputs Dense(num_classes, activationsoftmax)(x) model Model(inputs, outputs) model.compile( optimizerAdam(learning_rate1e-3), losscategorical_crossentropy, metrics[accuracy] )这里有两个参数值得展开。Dropout 的比例0.5 是隐层 dropout 的经典默认值对 CWRU 这种样本量不大的任务来说很有必要全连接层的参数量远大于 CNN不 drop 基本两三个 epoch 之后训练集就开始过拟合。第二个是隐藏层的宽度跨度——512 到 128 是递减结构先给足容量再逐步压缩让网络在高维空间里先完成粗分离再在低维空间做精细分类。如果你发现某个故障类别怎么都分不开优先把 512 改成 1024 试一轮比盲目加层效果好。3.2 CNN 模型用一维卷积提取振动信号里的局部冲击特征第二个模型是 CNN而且是直接作用在原始信号上的一维卷积网络。为什么用一维卷积而不是把信号转成频谱图再喂二维 CNN因为轴承故障的典型表现形式是振动信号里周期性出现的冲击特征内圈故障、外圈故障的冲击间隔与转频、故障尺寸有明确物理关系一维卷积的局部感受野天然适合捕捉这种短时冲击。而转频谱图需要额外做 STFT 变换引入窗函数、帧移这些超参数对刚上手的学生来说又多一层容易出错的环节。# cnn.py 核心结构抽取 from tensorflow.keras.layers import Conv1D, MaxPooling1D, Flatten inputs Input(shape(2048, 1), nameinput) x Conv1D(filters32, kernel_size64, strides2, paddingsame, activationrelu)(inputs) x MaxPooling1D(pool_size2)(x) x Conv1D(filters64, kernel_size32, strides2, paddingsame, activationrelu)(x) x MaxPooling1D(pool_size2)(x) x Flatten()(x) x Dense(128, activationrelu, namelast_dense)(x) outputs Dense(num_classes, activationsoftmax)(x) model Model(inputs, outputs) model.compile( optimizerAdam(learning_rate1e-3), losscategorical_crossentropy, metrics[accuracy] )一维卷积的几个参数需要按信号特点去理解。kernel_size 取 64对应 64 个采样点在 12kHz 采样率下大约是 5 毫秒的窗口这个长度能覆盖一个冲击响应的主要波形strides2 是下采样把 2048 点的输入在第一层就缩短到一半减少后续计算量paddingsame 保证卷积输出长度按整除圆整。MaxPooling 在卷积之后做进一步降采样提取局部区域最显著的特征值。注意 Flatten 之后那个 Dense 层被命名为 last_dense这是故意的。后面提取特征和画散点图都靠这个命名定位如果你改了层名plot_scatter.py 里的 get_layer(last_dense) 就直接报错。这就是那种“看起来不起眼、改了就翻车”的耦合点复现时尽量不要动名字。3.3 关键超参数epoch、batch_size 和学习率这样设比较稳两个模型在训练超参数上保持了统一优化器都是 Adam初始学习率 1e-3损失函数都是交叉熵。学习率 1e-3 是 Adam 最不容易出事的起点CWRU 数据归一化后特征尺度接近这个学习率通常几十个 epoch 内就能收敛如果发现 loss 震荡得很厉害降到 5e-4 再试。epoch 建议先定 30 到 50DNN 参数多过拟合来得早一般 20 个 epoch 内训练集就能到很高水平CNN 参数少一些可以多训几轮。batch_size 取 64 或 128 均可样本总量不大取太大反而让梯度平均化小类别学不到位。还有一个容易被忽略的点是随机种子。源码如果没有固定 np.random.seed 和 tf.random.set_seed每次跑出来的准确率都会有波动写论文时实验不可复现是致命的。建议在代码开头固定np.random.seed(42) tf.random.set_seed(42)固定种子后再调参你才能确定准确率变化来自模型结构改动而不是随机初始化。我见过很多同学在答辩前夜反复跑同一个脚本数字一次比一次高其实只是随机噪声这种“自我感动”在导师追问下非常容易被拆穿。4. 训练评估与特征可视化让模型“学到了什么”变得可见4.1 训练主流程与 last_layer_data这个目录是用来砸开黑匣子的dnn.py 和 cnn.py 的训练主流程很简洁加载预处理好的数据编译模型fit然后评估测试集准确率。但光有准确率远远不够深度学习模型在该场景下最容易被人质疑的就是“黑匣子”和“过拟合”。源码里专门设计了 last_layer_data 这个目录用来保存模型在测试集上的最后一个隐藏层输出这一步就是砸开黑匣子的那把锤子。提取最后一层输出的做法在 Keras 里很直接用 Model 重新定义从输入到指定层输出的子模型然后 predict。常见写法如下# 训练后提取 last layer 输出抽取 from tensorflow.keras.models import Model import numpy as np # 用之前编译好的 model从输入截到 last_dense 层 feature_extractor Model( inputsmodel.input, outputsmodel.get_layer(last_dense).output ) # 得到测试集在 128 维特征空间里的坐标 features feature_extractor.predict(X_test, batch_size64) np.save(last_layer_data/features_test.npy, features) # 顺手把 one-hot 标签转回整数索引后面画图直接能用 y_test_int np.argmax(y_test, axis1) np.save(last_layer_data/y_test.npy, y_test_int)这段代码的价值在于准确率告诉你“模型分对了 99%”而 last_dense 的 128 维输出告诉你“在模型自己看来这些样本在特征空间里长什么样”。如果不同故障类别在这个特征空间里聚成不同团说明分类器不是靠记住训练样本而是真的学到了可分的特征表示如果全部混成一团但准确率很高那就要怀疑数据泄漏或者过拟合了。这个判断逻辑在论文里可以直接作为实验证据使用。4.2 plot_scatter.py用 t-SNE 把 128 维特征压到二维平面128 维的特征人眼看不见所以源码里的 plot_scatter.py 负责把 last_layer_data 读出来降维成二维散点图。这里使用的降维方法是 t-SNE它擅长把高维空间里的近邻关系在低维上尽量保留做聚类可视化最直观。核心逻辑抽出来如下# plot_scatter.py 核心逻辑抽取 import numpy as np import matplotlib.pyplot as plt from sklearn.manifold import TSNE features np.load(last_layer_data/features_test.npy) y_test_int np.load(last_layer_data/y_test.npy) # perplexity 表示每个点考虑近邻的数量样本量中等时取 30 是稳妥值 tsne TSNE(n_components2, perplexity30, random_state42, n_iter1000) X_2d tsne.fit_transform(features) plt.figure(figsize(7, 6)) for c in np.unique(y_test_int): idx y_test_int c plt.scatter(X_2d[idx, 0], X_2d[idx, 1], s8, alpha0.7, labelfclass {c}) plt.legend() plt.savefig(last_layer_data/tsne_result.png, dpi200) plt.show()t-SNE 有两个参数会直接影响出图质量。perplexity 取 30 是默认值如果样本只有一两千个可以降到 20 到 25取值不当会让图中的簇出现断裂或者粘连n_iter 至少给 1000小于这个数时算法经常还没收敛画出来的图会有很多“拖着尾巴”的噪声点。如果你确认特征已经可分但 t-SNE 图还是一团糊先调这两个参数不要急着怀疑模型训练失败了。4.3 多转速数据的使用边界跨工况验证是论文里的加分实验源码把数据按 1750、1730、1772 分开是给你留了一手非常值得做的实验用 1772轻载的数据训练直接把 1750中载或 1730重载的数据作为测试集验证模型在工况变化下还站不站得住。CWRU 数据跨负载分类的准确率通常会比同负载低几个点到十几个点具体落差取决于模型容量和数据归一化方式这恰好是论文里“讨论与局限性”章节最实在的素材。跨工况验证的操作流程是在同一个模型下换数据路径把训练集指向 1772 目录下预处理好的特征和标签测试集指向 1750 的结果重新跑一遍 fit 和 predict。注意这类实验里不要去动随机种子和归一化方式保证唯一变量是数据工况。如果能把两种工况下的 t-SNE 图放在同一页对比答辩时讲“模型在轻载工况学到的特征到中载工况下保持/退化”会非常有说服力。这一手是只跑同分布数据的学生完全想不到的。5. 复现避坑与常见问题五条真实记录5.1 TensorFlow 版本冲突import 直接报错现象按照 README 装好依赖后运行 dnn.py 或 cnn.py 时出现 ImportError提示找不到 tf.keras 里的某个模块或者训练一开始就弹 CUDA 相关的错误。原因这份源码是 Keras 2.x 时代的写法较新的 TensorFlow 版本里部分 API 被迁移或废弃老代码的 import 路径对不上。另外 GPU 版 TensorFlow 与 CUDA 版本不匹配也会在训练中途崩掉。解决我一般会为这类毕业设计项目单独建一个虚拟环境固定安装 TensorFlow 2.6 到 2.10 之间的版本配合对应 Python 3.8 或 3.9。如果机器没有 GPU直接装 CPU 版 TensorFlow 跑 CWRU 这种小数据量任务也完全够用。装完先做一次快速验证再继续python -c import tensorflow as tf; print(tf.__version__)能正常打印版本号再往下走如果这一步就有问题大概率是 Python 版本和 TensorFlow 版本不匹配先解决环境问题不要带病运行后续脚本。5.2 数据路径或传感器通道没配对训练全在错误数据上进行现象代码能跑准确率也显示很高但后来把结果和实际标签一核对发现类别映射完全错位比如把外圈故障的样本标成了内圈故障。原因CWRU 的 .mat 文件名规则比较绕IR007、OR0076、B007 这类缩写极易读错而且每个文件里同时有 DE 和 FE 两个通道。源码默认读取 DE 通道如果你用的数据文件里只有 FE 通道又没有修改通道选择逻辑就相当于拿另一个传感器位置的信号在训练。解决在预处理脚本里加一段打印把每个 .mat 文件解析出的路径、通道、标签输出到屏幕上人工扫一遍确认映射关系。我一般会在连接后打印前三个样本的具体值看一眼信号量级是否正常for i in range(3): print(paths[i], labels[i], X[i][:5])打印出来的信号前几个值如果量级在 0.01 以下或接近 0说明这段信号接近常数段可能来自正常状态也可以作为初步判断信号来源的依据。5.3 滑动窗口重叠过大验证集出现同源样本泄漏现象测试集准确率高到不真实几乎接近 1.0但让模型去预测一批并未参与训练的陌生数据准确率立刻掉下来。原因creat_data.py 里 step 小于 window_size同一段原始信号被切成大量重叠窗口。如果不先按原始文件划分而是把所有窗口混在一起再随机划分训练集和测试集同一个冲击脉冲的多个窗口会同时出现在两边模型等于提前看过答案。解决划分时先按“原始 .mat 文件”分组保证同一个文件切出来的窗口全部进训练集或全部进测试集或者把 step 设成与 window_size 相等做成完全无重叠切片。对 CWRU 这种数据规模无重叠切片也足够得到成千上万个样本。最简单的一组改动# 按文件名做分组划分而不是按样本随机划分 unique_files list(set(file_ids)) train_files unique_files[: int(len(unique_files) * 0.8)] train_mask np.isin(file_ids, train_files) X_train, X_test X[train_mask], X[~train_mask]用文件 id 而不是样本索引来划分是诊断任务里避免同源泄漏的通用做法。5.4 归一化顺序错误loss 变成 nan现象训练一启动 loss 就是 nan或者前几个 epoch 正常随后突然溢出模型权重全部变成无效值。原因最常见的有两种——对包含近似常数段的信号做 StandardScaler某些维度方差趋近 0标准化后出现极大值或者把测试集的统计量混进了训练集导致训练样本被污染。CWRU 正常轴承振动幅值很小信号里确实存在接近常数的片段。解决先用 np.isnan(X_train).sum() 和 np.isinf(X_train).sum() 检查数据如果问题来自接近常数段把 StandardScaler 换成 MinMaxScaler把数据压到 0 到 1 之间往往更稳。同时确认划分代码执行在归一化代码之前顺序不能反。修改后重新训练前要重启内核避免旧会话里残留的变量污染新一次运行。5.5 特征可视化脚本读不到数据散点图空白现象训练完成后运行 plot_scatter.py提示文件不存在或者图能画出来但所有点挤成一团看不出任何分簇结构。原因last_layer_data 里的 npy 文件没有正确生成或路径和实际运行目录不一致另一个更隐蔽的原因是把 one-hot 格式的 y_test 直接拿去画图没有转回整数索引导致 np.unique 把多个类别混在一起。解决先检查 last_layer_data 目录下是否同时存在 features_test.npy 和 y_test.npy再验证一下形状是否符合预期import numpy as np f np.load(last_layer_data/features_test.npy) y np.load(last_layer_data/y_test.npy) print(f.shape, y.shape, np.unique(y))f 的形状应该是 (样本数, 128)y 的形状应该是 (样本数,)np.unique(y) 输出的类别数应该和真实类别数一致。如果 t-SNE 图上所有点混在一起优先怀疑特征本身没学好而不是画图代码写错了这时候回去看训练曲线如果训练集准确率已经很高但测试集明显偏低过拟合的可能性最大。6. 进阶验证用特征可视化反推模型是否学到了真实故障特征跨工况测试是验证模型泛化能力的试金石特征可视化是判断模型学习质量的照妖镜。把这两者结合整个项目就能从“我只能跑出 99% 准确率”升级成“我能解释准确率从哪里来”。进阶动作之一把 t-SNE 图和准确率放进同一节实验结果里。t-SNE 图如果呈现出“每个故障聚一团正常样本聚在另一侧”说明 last_dense 层已经把原始振动信号抽象出了可分辨的故障特征如果图上相邻区域存在明显类别交叉就去找对应样本的原始信号看看是不是某类故障的冲击幅值本身和另一类接近。这个检查过程写进论文就是从“用模型”到“理解模型”。进阶动作之二给特征可分离性加一个数值指标。t-SNE 图肉眼看分开了还不够严谨我一般会顺手算一下 Fisher 判别比率取每两个类别在 last_dense 特征上的类间均值差除以类内方差和# 计算两个类别之间的 Fisher 判别比率衡量特征可分性 def fisher_ratio(features, labels, class_i, class_j): fi features[labels class_i] fj features[labels class_j] num (np.mean(fi, axis0) - np.mean(fj, axis0)) ** 2 den np.var(fi, axis0) np.var(fj, axis0) 1e-8 return np.mean(num / den)判别比率越大说明在特征空间里两类越容易分开。把每对类别的 Fisher 比率列成表格再做跨转速对比比单纯贴一张准确率更有说服力也更能扛住答辩老师的连续追问。进阶动作之三留一组数据做“未参与训练”的盲测。我的习惯是把 1730 转速的数据完整留到最后所有模型调参、epoch 选择都在 1772 和 1750 上完成最后一轮才把 1730 的数据拿出来做最终评估。这样得到的数字才是论文里敢写出来的泛化性能。从那以后每次跑完故障诊断训练我都会强制走一遍特征可视化哪怕只是快速看一眼散点图模型分错的地方往往也正是这张图上两个类别交界的位置。明白这一点你才能在答辩时把“准确率 99%”真正讲成自己的东西。希望帮到你。需要这份源码和 CWRU 全部数据的直接去资源页下载按上面这些注意点操作复现起来会省很多力气。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?