简介:这是一份基于深度学习的滚动轴承故障诊断方法完整项目,主要面向计算机相关专业毕业设计学生、需要项目实战练习的学习者,也适合作为课程设计或期末大作业。项目包含全部源码与数据,涵盖数据处理、模型构建、训练评估与可视化等环节,可帮助读者系统掌握利用卷积神经网络等深度学习方法处理西储大学(CWRU)轴承数据的流程。压缩包为zip格式,共41个文件,主要由30个mat数据文件、4个Python源码文件、6个md文档和1个使用说明txt组成,整体约34.87MB;源码中包含模型构建、数据预处理与可视化等模块,说明文档对目录结构和运行方式做了详细说明。所有代码经过严格调试,下载后可直接运行,能够完整复现滚动轴承故障分类与诊断结果。已有971人学习或下载,适合需要快速启动毕业设计或开展故障诊断方向练习的读者使用。
1. 你不是在下论文,你是在跑一套能出结果的工程
如果你打开这个压缩包,看到的不是论文截图和PPT,而是train.py、data_process.py、models.py、checkpoints/这类完整工程结构,说明这份「基于深度学习的滚动轴承故障诊断方法」是拿真实振动数据喂出来过的。它解决的是一个非常具体的工业问题:拿到一段轴承振动加速度信号,判断该轴承处于正常、外圈故障、内圈故障还是滚动体故障,更进一步还能根据故障直径细分严重程度。这类诊断是旋转机械状态监测的基础,也是机械、电气、自动化方向毕业设计里高频出现的选题。技术栈就是 Python + PyTorch,数据是实验台采集的一维波形。这篇笔记不冒充你手里那份源码的作者,我只按一线跑过类似项目的习惯,把数据怎么切、模型怎么选、参数怎么调、坑藏在哪里讲透。目标是让拿到这个标题下任何压缩包的读者,都能从「解压」一路跑到「出测试报告」。
2. 数据先行:先把振动信号切成样本,模型才有东西可学
2.1 数据目录与标签:先搞清每个文件对应哪类故障
拿到解压后的文件夹,先别急着跑train.py。我一般第一件事是列出目录树,确认数据组织方式。常见的轴承故障诊断数据有两种存放风格。一种是每个类别一个子目录,目录名直接叫normal、inner_race_fault_1,里面是一段段原始信号 CSV;另一种是只有一个大 CSV,第一列是标签,后面是采样点。
你可以用下面这个小脚本快速生成数据清单,顺便确认标签能不能正确读取:
import os from collections import Counter data_path = "./data" # 换成你的实际路径 label_map = {} # 假设数据是按子目录分类的 for cls_name in sorted(os.listdir(data_path)): cls_dir = os.path.join(data_path, cls_name) if os.path.isdir(cls_dir): files = os.listdir(cls_dir) label_map[cls_name] = len(files) print(f"{cls_name}: {len(files)} 个文件") print("总类别数:", len(label_map))这段逻辑很简单,但能暴露很多问题。如果某个类别文件数比其他类别少一个数量级,说明样本不平衡在数据源头就存在,后面训练要用加权损失或重采样。如果目录名出现inner_race_fault_0.18_0.21这种写法,说明标签里还附带了故障直径和转速信息,理论上可以做成更细的多分类任务,而不是只有四类。
看完目录就要确认采样率。振动数据的采样率直接决定了后续滑窗取多少点。工业上常用的是 12kHz 或 48kHz 采样率,一般会在原数据集的说明文件里给出。如果没有说明文件,可以读一个数据文件,看相邻采样点的时间间隔。
import pandas as pd df = pd.read_csv("./data/normal/1.csv", header=None) print("文件shape:", df.shape) print("前10个采样点:", df.iloc[:10, 0].tolist())逻辑说明:这一步不是为了看波形,是为了确认数据是单通道还是多通道。很多采集台会同时记录加速度、速度和位移,但故障诊断模型一般只取加速度通道,因为故障冲击在加速度信号上最明显。如果发现每个文件是 8 列甚至 16 列,那说明是多通道,需要选通道或做融合,不能想当然地只读第一列。
参数说明:header=None是避免把第一行采样点误当成列名;iloc[:10, 0]取的是第一通道前十个点,看数值量级能判断数据是否经过了归一化。如果数值在 ±1 之间,说明可能已经做过归一化;如果出现几百甚至上千,就要自己处理。
2.2 用滑窗把一维信号切样本:窗口长度与步长的选择
深度学习模型输入尺寸是固定的,但原始振动信号往往是一段几秒甚至几十秒的连续波形。需要把长波形切成固定长度的片段,每个片段作为一个训练样本。这里有两个关键参数:窗口长度win_len和滑动步长stride。
窗口长度通常由轴承的故障特征频率和转速共同决定。一个经验法则是:窗口内至少包含 10 个完整的轴承转频周期。比如转速为 1800 r/min,转频是 30Hz,采样率 12kHz,那么一个转频周期就是 400 个采样点,10 个周期是 4000 点。切一个 4096 点的窗口基本够用,这也是很多方案默认用 4096 或 2048 的原因。
import numpy as np def sliding_window(signal, win_len=4096, stride=2048): samples = [] n = len(signal) for start in range(0, n - win_len + 1, stride): samples.append(signal[start:start + win_len]) return np.array(samples) # 假设你读到了一个长度为 100000 的数组 raw_signal = np.random.randn(100000) # 示例数据 samples = sliding_window(raw_signal, win_len=4096, stride=2048) print("窗口数量:", samples.shape[0])逻辑说明:滑窗的目的是把每个独立样本当成一个「独立观测」。如果步长等于窗口长度,相邻窗口没有重叠,样本数量最少;如果步长小于窗口长度,相邻窗口有数据重叠,样本数量会增加。重叠的好处是样本量更大、模型更不容易过拟合,坏处是相邻样本高度相似,如果直接把训练集和测试集从同一段波形上切出来,会造成严重的数据泄露。
参数说明:win_len是每个样本的点数,一般取 1024、2048、4096 这类 2 的幂次,方便卷积网络下采样;stride决定样本密度,常见取win_len // 2让重叠率 50%。如果想更保守,可以设stride = win_len,完全没有重叠,避免样本间自相关过高。我在跑类似项目时,会先用一个小脚本统计滑窗后各类别的样本数量,确认类别分布没有被滑窗改变。
更合理的方式是:对每段原始长信号独立进行滑窗,然后按「原始信号段」划分训练集和测试集,而不是把所有窗口混在一起随机划分。这是很多初版代码最容易犯的错。
2.3 数据集划分与归一化:别把同一段信号拆到训练集和测试集
数据划分原则在故障诊断里比模型结构更重要。如果训练集和测试集都来自同一段原始信号的不同窗口,模型其实已经见过测试数据了,测试准确率虚高,真实场景一换设备就崩。正确做法是:把每段原始记录当作一个整体,同一段信号的所有窗口只能全部进训练集或全部进测试集。
from sklearn.model_selection import train_test_split # data_dict: {类别名: [原始信号段列表]},每个元素是一整段numpy数组 def build_dataset(data_dict, win_len=4096, stride=2048): all_x, all_y = [], [] for cls_idx, (cls_name, segments) in enumerate(data_dict.items()): for seg in segments: windows = sliding_window(seg, win_len, stride) all_x.extend(windows) all_y.extend([cls_idx] * len(windows)) return np.array(all_x), np.array(all_y) # 假设你已经按段组织了数据 x_all, y_all = build_dataset(data_dict) # 注意labels是0,1,2,3 # 这里按原始段索引划分才是安全的,但上面已合并窗口,所以需要保留段归属 # 更稳妥的写法见下文逻辑说明:如果build_dataset把不同段的所有窗口全部混在一起,然后直接train_test_split(x_all, y_all, test_size=0.2),就会发生段间穿插。要避免这种事,应该先给每段信号一个唯一编号,然后把段编号作为分层变量划分,再根据段编号取对应的窗口。
我常用的折中方案是:先不合并,而是维护一个segment_ids数组,每个窗口带着它来自哪一段的编号。划分测试集时先随机抽几个段编号作为测试段,剩下的段作为训练段。这样就算训练集和测试集窗口在时间上有重叠,也不至于直接把同一条波形的邻近窗口分到两边。
归一化也要在划分之后做,而且只能用训练集的统计量。错误的做法是对全部数据做StandardScaler,再用它 transform 测试集,因为测试集的均值方差已经泄露给了模型。正确做法是:
from sklearn.preprocessing import StandardScaler # 假设x_train是训练集窗口,shape: (n_train, win_len) scaler = StandardScaler() x_train_scaled = scaler.fit_transform(x_train.reshape(-1, 1)).reshape(x_train.shape) # 测试集用同一个scaler,不重新fit x_test_scaled = scaler.transform(x_test.reshape(-1, 1)).reshape(x_test.shape)参数说明:fit_transform只在训练集上调用一次,transform在测试集上只做转换。如果你观察原始信号幅值不大,也可以不做归一化直接喂给卷积网络,但归一化之后训练会稳定很多,尤其是当你使用的激活函数是 Sigmoid 或 Tanh 时。ReLU 对归一化没那么敏感,但仍然建议做,因为后续优化器的学习率设置会简单很多。
3. 模型选型与构建:一维 CNN 是轴承故障诊断的性价比之选
3.1 为什么不用图像识别那套:二维化有信息损失
很多毕业设计会把振动信号转成频谱图或时频图,然后用 ResNet 这类图像分类模型去分类。这个方法不是不行,但它绕了一个弯。原始振动信号本身就是时间序列,一维卷积天然适合提取局部冲击特征。如果先把信号做短时傅里叶变换变成二维图像,再把图片输入 ResNet,中间的时频分辨率选择、颜色映射、图片缩放都会引入人为干扰,而且计算量翻了不止一倍。
跑过项目的人大多会先试一维模型。一维 CNN 输入是(batch_size, 1, win_len),输出是类别概率,整个过程直观可控。对于滚动轴承故障,故障点产生的冲击会在振动波形上形成周期性的衰减振荡,一维卷积核恰好能捕捉到这种局部波形模式,而不需要额外做特征工程。相比之下,二维模型更适合在空间结构明确的数据上工作,比如触摸屏图片、X光片,而不是一维波形。
所以项目里如果有Conv1d而不是Conv2d,说明作者选型是合理的。如果你想超越 baseline,可以在保持一维结构不变的前提下,增加注意力机制或循环单元,而不是换成图像模型。
3.2 一维 CNN baseline 的 PyTorch 实现
下面给出一段可以直接训练的基础一维 CNN 代码,结构简单,适合作为第一个可跑通的版本:
import torch import torch.nn as nn class FaultCNN(nn.Module): def __init__(self, num_classes=4, win_len=4096): super().__init__() self.features = nn.Sequential( nn.Conv1d(1, 16, kernel_size=15, stride=2, padding=7), nn.BatchNorm1d(16), nn.ReLU(inplace=True), nn.MaxPool1d(kernel_size=2, stride=2), # 长度减半 nn.Conv1d(16, 32, kernel_size=7, stride=2, padding=3), nn.BatchNorm1d(32), nn.ReLU(inplace=True), nn.MaxPool1d(kernel_size=2, stride=2), nn.Conv1d(32, 64, kernel_size=7, stride=2, padding=3), nn.BatchNorm1d(64), nn.ReLU(inplace=True), nn.AdaptiveAvgPool1d(1), # 全局平均池化得到 [batch, 64] ) self.classifier = nn.Linear(64, num_classes) def forward(self, x): # x: [batch, win_len] or [batch, 1, win_len] if x.dim() == 2: x = x.unsqueeze(1) x = self.features(x) # [batch, 64, 1] x = x.squeeze(-1) # [batch, 64] out = self.classifier(x) return out逻辑说明:第一层卷积核大小设为 15,是考虑到振动冲击在波形上通常占据十几个采样点,大卷积核能更好地覆盖一个冲击响应。步长 2 配合 padding 7 保持输入输出长度近似不变,再用 MaxPool 减半。第二层第三层逐步增大通道数,把感受野扩展,同时降低序列长度。最后用全局平均池化把每个通道压缩成一个值,输入全连接层。
参数说明:num_classes是你的故障类别数,先别追求多任务,能四类分对就成功了一半。win_len要与你的滑窗长度一致。kernel_size如果太大,模型参数会膨胀,太小又抓不住冲击,一般第一层取 7~20,后面层取 5~9。BatchNorm1d对于加速收敛很有帮助,尤其是输入信号幅值分布不稳定的时候。
如果你把这份代码跑在 CPU 上,一个 epoch 也不慢,因为 4096 点的窗口经过三次池化后长度已经很小,计算量不大。这也是这个方向适合毕业设计的原因——不需要很高的硬件门槛。
3.3 加一个 LSTM 分支能提升多少
纯 CNN 的缺陷是只关注局部感受野内的模式,而轴承故障信号中的故障冲击是有周期性重复的。CNN 靠堆叠层数也能学到长距离依赖,但效率不高。常见做法是加入一个双向 LSTM 分支,把 CNN 提取的局部特征序列送入 LSTM,捕捉冲击的周期性。
import torch import torch.nn as nn class CNNLSTM(nn.Module): def __init__(self, num_classes=4, win_len=4096): super().__init__() self.cnn = nn.Sequential( nn.Conv1d(1, 16, 15, stride=2, padding=7), nn.ReLU(), nn.MaxPool1d(2), # 长度 /2 nn.Conv1d(16, 32, 7, stride=2, padding=3), nn.ReLU(), nn.MaxPool1d(2), # 长度 /2 ) self.lstm = nn.LSTM(input_size=32, hidden_size=64, num_layers=1, batch_first=True, bidirectional=True) self.dropout = nn.Dropout(0.3) self.classifier = nn.Linear(128, num_classes) # 双向拼接 def forward(self, x): if x.dim() == 2: x = x.unsqueeze(1) x = self.cnn(x) # [batch, 32, L] x = x.transpose(1, 2) # [batch, L, 32] 进LSTM out, _ = self.lstm(x) # [batch, L, 128] out = out.mean(dim=1) # 对时间维取平均 out = self.dropout(out) return self.classifier(out)逻辑说明:transpose把通道维变成 LSTM 的时间步维,每个时间步的特征是 CNN 输出的 32 维通道。双向 LSTM 编码过去和未来的信息,最后对时间步求平均,得到一个 128 维的向量表示。这里没有用最后一个时间步的输出,因为故障特征可能出现在任意时间段,平均池化能更稳定地汇总。
参数说明:bidirectional=True会带来两倍参数量,隐藏单元设 64 是折中。dropout=0.3是为了防止 LSTM 过拟合,因为 LSTM 的参数量通常不小。如果你发现 LSTM 训练明显慢于 CNN,可以考虑减少 hidden_size 或去掉双向,改成单向即可。
根据我在类似项目里的经验,CNN-LSTM 在准确率上比纯 CNN 一般能提升 1~3 个百分点,但训练时间会增加一倍。如果你的 baseline 已经到 95%,再为了那 1 个百分点搭上额外算力不值当;但如果你的任务有 8 类甚至 12 类,LSTM 的时序建模能力会更有优势。
4. 训练与评估:从损失曲线到混淆矩阵,逐步打通全流程
4.1 训练脚本:数据加载器、优化器、损失函数与早停
有了模型和定型的数据集,下一步就是把训练脚本写完整。我通常把训练过程封装成一个函数,支持传入模型、数据加载器、优化器和早停参数。
import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset import numpy as np def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss, correct, total = 0, 0, 0 for x_batch, y_batch in loader: x_batch, y_batch = x_batch.to(device), y_batch.to(device) optimizer.zero_grad() out = model(x_batch) loss = criterion(out, y_batch) loss.backward() optimizer.step() total_loss += loss.item() * x_batch.size(0) pred = out.argmax(dim=1) correct += (pred == y_batch).sum().item() total += y_batch.size(0) return total_loss / total, correct / total逻辑说明:TensorDataset和DataLoader配合是最省事的做法,先把 NumPy 数组转成张量,再封装成数据集。这里的criterion是交叉熵损失nn.CrossEntropyLoss(),它内部已经做了 Softmax,所以模型最后一层不需要额外加 Softmax。训练循环里optimizer.zero_grad()必须在每一步清空梯度,不然梯度会累加。
参数说明:device建议用torch.device("cuda" if torch.cuda.is_available() else "cpu"),不要手动写死。数据加载器里有两个关键参数:batch_size和shuffle。batch_size常见取 32、64、128,取决于显存。shuffle=True只在训练时用,验证和测试必须shuffle=False,否则指标波动会显得很随机。
早停是一种很有效的防过拟合策略。训练过程中监控验证集的 loss,如果连续多个 epoch 验证 loss 不下降甚至上升,就提前终止并把最优模型权重存下来。
best_loss = float("inf") patience, wait = 15, 0 for epoch in range(1, 201): train_loss, train_acc = train_one_epoch(model, train_loader, optimizer, criterion, device) val_loss, val_acc = evaluate(model, val_loader, criterion, device) if val_loss < best_loss: best_loss = val_loss torch.save(model.state_dict(), "best_model.pth") wait = 0 else: wait += 1 if wait >= patience: print(f"早停于 epoch {epoch}") break逻辑说明:patience是容忍连续多少个 epoch 没有改进。这里设 15,适合类别多、训练波动大的场景。如果设太小比如 5,可能会在损失探底之前就被打断。保存模型时只保存state_dict,不保存整个模型对象,这样加载的时候更灵活,也不容易踩 PyTorch 版本兼容的坑。
4.2 评估脚本:准确率、F1、混淆矩阵,别只盯着 loss
训练期间的 loss 是模型内部优化的信号,但最终报告里老师或评审更看重的是测试集上的准确率、F1-score 和混淆矩阵。尤其是不平衡数据下,准确率会被多数类主导,F1 才能反映少数类的表现。
from sklearn.metrics import confusion_matrix, f1_score, classification_report def evaluate(model, loader, criterion, device): model.eval() total_loss, correct, total = 0, 0, 0 all_preds, all_labels = [], [] with torch.no_grad(): for x_batch, y_batch in loader: x_batch, y_batch = x_batch.to(device), y_batch.to(device) out = model(x_batch) loss = criterion(out, y_batch) total_loss += loss.item() * x_batch.size(0) pred = out.argmax(dim=1) all_preds.extend(pred.cpu().numpy()) all_labels.extend(y_batch.cpu().numpy()) correct += (pred == y_batch).sum().item() total += y_batch.size(0) acc = correct / total avg_loss = total_loss / total f1 = f1_score(all_labels, all_preds, average="macro") cm = confusion_matrix(all_labels, all_preds) return avg_loss, acc, f1, cm逻辑说明:torch.no_grad()告诉 PyTorch 不用计算梯度,评估阶段大幅减少内存占用。all_preds和all_labels要转成 NumPy 数组再传给 sklearn,不然 Python 列表在分类数量很多时性能会很差。f1_score的average="macro"让每个类别权重相等,忽略类别样本量,适合找出哪个类别最弱。
参数说明:f1_score的average参数有三种常用取值:"macro"适合类别不平衡时公平评估;"weighted"会按样本量加权,更贴近整体表现;"binary"只用于二分类。故障诊断通常有 4 类以上,所以用macro更合理。混淆矩阵的行是真实类别,列是预测类别,如果哪一行除对角线外还有值,说明那一类容易和另一类混淆,通常物理上是外圈与滚动体故障特征频率相近导致的。
4.3 训练日志与模型保存:让每次实验可复现
训练日志写得好不好,直接影响你调参的效率。我习惯在每个 epoch 打印三行核心信息:epoch 数、训练 loss、验证准确率。同时把数据记录到 CSV,方便画曲线。
import csv log_path = "train_log.csv" with open(log_path, "w", newline="") as f: writer = csv.writer(f) writer.writerow(["epoch", "train_loss", "train_acc", "val_loss", "val_acc"]) for epoch in range(1, 201): train_loss, train_acc = train_one_epoch(...) val_loss, val_acc, _, _ = evaluate(...) writer.writerow([epoch, train_loss, train_acc, val_loss, val_acc])逻辑说明:把日志落盘比仅打印在控制台强太多。一旦你中途想横向对比不同学习率或不同窗口长度的实验,CSV 可以直接读入 pandas 画图。避免每次重新跑一遍找结果。
模型保存有两点要注意。第一,保存的时候带上验证集性能指标,比如best_model_acc_0.954.pth,这样下次打开文件夹就能知道哪个模型最好。第二,如果用了自定义网络结构,加载模型时必须先实例化一个结构相同的模型,再用load_state_dict加载权重。
model = FaultCNN(num_classes=4, win_len=4096) model.load_state_dict(torch.load("best_model.pth", map_location="cpu")) model.eval()参数说明:map_location="cpu"是在服务器上训练的模型要拿到本地 CPU 推理时用的。如果不加,PyTorch 默认要找到原来的 GPU,没有 CUDA 就会报错。
5. 避坑手册:滚动轴承故障诊断最容易翻车的五个地方
5.1 现象:测试准确率接近 100%,但新数据一测就崩——样本泄露
这是我最常看到的翻车现场。某同学交来的报告里训练准确率 99.2%,测试准确率 98.7%,看得人很兴奋。结果把训练好的模型拿去识别另一台实验台上采集的数据,准确率直接掉到 70%。合着之前 98% 都是因为训练集和测试集里包含同一条原始波形的窗口。
原因:滑窗时把同一段原始信号的所有窗口全部汇入一个大池子,然后随机打乱并划分训练集和测试集。模型很可能学到了测试集窗口的细微噪声模式,而不是轴承故障本质特征。
解决:调整划分策略,保证同一条原始信号段的所有窗口只出现在一个集合里。代码层面,把每段信号加个 ID,按 ID 划分。具体做法是先用group_id数组记录每个窗口属于哪个段,然后对 ID 做train_test_split,再根据 ID 映射回窗口。如果原数据本身每条记录只有几分钟,可以把前 60% 时间段作为训练,后 40% 时间段作为测试,这也是一种不泄露的划分。
5.2 现象:十类故障里有一类准确率极低——样本不平衡与加权损失
项目里如果有 10 个类别,你会发现有的类别训练样本就只有两三百个,而正常样本有五六千。模型为了降低整体 loss 会把多数类学得很好,少数类则几乎不学,最后准确率 90%+,但看一下混淆矩阵,滚动体故障那一类是 0%。
原因:交叉熵损失对所有样本一视同仁,多数类的梯度淹没了少数类。
解决:最直接的做法是修改损失函数权重。PyTorch 里CrossEntropyLoss自带weight参数,权重按每类样本数的倒数来算:
from sklearn.utils.class_weight import compute_class_weight import torch class_weights = compute_class_weight(class_weight="balanced", classes=np.unique(y_train), y=y_train) class_weights = torch.tensor(class_weights, dtype=torch.float32).to(device) criterion = nn.CrossEntropyLoss(weight=class_weights)逻辑说明:compute_class_weight会自动把样本少的类别权重调高,样本多的类别权重调低。这样少数类每次更新时梯度占比更大。这比手动复制少数类样本(SMOTE 在时序里效果不稳定)要稳定得多。
参数说明:如果用了加权损失,学习率可能需要稍微调低,因为权重变大等效于放大了梯度,学习率不变容易导致训练震荡。
5.3 现象:训练波动巨大,loss 曲线像锯齿——学习率与归一化问题
训练时打印 loss,发现每个 epoch 之间波动剧烈,前一个 epoch 是 0.8,后一个直接 2.3,再过两个 epoch 又掉回 0.5。这种情况下模型很难收敛到稳定点。
原因:最常见的是学习率偏大,加上输入信号没有归一化,导致梯度在各 batch 间变化范围很大。另一种可能是在 CNN 里用了BatchNorm但 batch size 太小(比如 4),BatchNorm 统计不稳定。
解决:先把学习率降到 1e-3 或 1e-4,观察十个 epoch。如果波动还大,检查输入数据是否做了标准化。另外把 batch size 提高到 32 以上。如果你已经用了 Adam,确认没有同时手动设置momentum,那是 SGD 的参数,Adam 不吃这一套。
我习惯在训练开始时先用一个小数据集跑 5 个 epoch,如果 loss 能稳定下降,再换全量训练。这样能快速把「模型实现 bug」和「超参数问题」分开。
5.4 现象:验证集上很好,换台机器就不行——设备与转速耦合
很多采集数据是在同一台实验台上、同一组转速和负载下完成的。模型学到的是「这个转速下的振动模式」,而不是「这个故障类型的特征」。换一台转速不同的设备,准确率下降是非常自然的。
原因:故障特征频率与转速强相关,不同类别的区分度可能一部分来自转速信息。模型无法区分是「轴承坏了」还是「转速变了」。
解决:如果你的毕业设计要求泛化性,建议在数据预处理阶段做包络谱或阶比分析,把时域信号转成与转速无关的阶比域。更简单的实验方案是:训练时把不同转速的数据混合,让模型同时见到多种转速。如果你手里的数据集只有单一转速,那就不要强调「跨设备通用」,在论文里把实验条件写清楚,这本身也是一种严谨。
作为工程兜底,可以在推理前先做一次快速转速估计,然后选择对应的模型,而不是指望一个模型吃遍天。
5.5 现象:复现别人结果对不上——随机种子、数据版本与平台差异
拿着同一个压缩包里的源码,按同样的参数跑,结果和 README 里的测试准确率差了五六个百分点。这不一定说明你操作错了,多数是随机性在捣乱。
原因:PyTorch 默认使用随机初始化,如果代码没有固定随机种子,每次训练结果都不同。数据预处理时如果用train_test_split且初始随机种子不同,划分结果也会不同,直接影响最终指标。
解决:在训练脚本开头固定随机种子:
import random, numpy as np, torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) set_seed(42)逻辑说明:这行代码放在import torch之后、任何模型和数据加载之前。torch.manual_seed同时影响 CPU 和当前 GPU 的随机数生成器。np.random.seed是为了让数据预处理里的随机操作可复现。如果代码里还有DataLoader的worker,最好在加载器里设generator,否则多进程数据加载依然可能不完全可复现。
参数说明:不同 PyTorch 版本甚至同一版本在不同 GPU 芯片上的卷积实现略有差异,微小精度差异是正常的。你只需要保证实验组之间使用相同环境,就能对比出谁的因素影响结果。
6. 进阶:把诊断结果变成可验证的故障特征频率
6.1 用特征图可视化模型关注的时间片段
分类准确率达到 95% 之后,评审可能会问一句:你的模型凭什么判断这是内圈故障?很多源码到此就打住,但我建议多做一步:提取模型最后一层卷积层的输出,看看模型对哪段波形响应最强烈。
def extract_attention(model, x, layer_name="features"): activations = {} def hook_fn(module, input, output): activations[layer_name] = output.detach() hook = getattr(model, layer_name).register_forward_hook(hook_fn) with torch.no_grad(): out = model(x.unsqueeze(0).to(device)) hook.remove() act = activations[layer_name].squeeze(0) # [C, L] # 对所有通道取平均,得到每个时间位置的激活强度 act_mean = act.mean(dim=0).cpu().numpy() return act_mean逻辑说明:register_forward_hook是 PyTorch 提供的钩子函数,在前向传播经过指定模块时触发,能拿到该模块的输出。这里拿到的是最后一层卷积层的特征图,时间维长度已经比原始信号短了很多,但依然能反应到哪个区间有强响应。把act_mean和原始波形对应画在同一张图里,你通常会看到激活峰值正好出现在冲击明显的时刻,这就给模型提供了直观的证据。
参数说明:如果想做更细腻的可视化,可以把梯度也钩下来,做 Grad-CAM。不过对一维信号,简单的平均激活图已经足够有说服力,也容易向非机器学习背景的老师解释。
6.2 计算故障特征频率做硬校验
深度学习模型是黑匣子,但轴承故障诊断本身有一套经典理论,就是特征频率计算公式。你可以用这套公式反过来验证模型做出的分类是否物理合理。
常见滚动轴承有内圈故障频率 BPI、外圈故障频率 BPO、滚动体故障频率 BSF,它们的近似公式(假设外圈固定):
- 外圈故障频率:BPO = n/2 * fr * (1 - d/D * cos α)
- 内圈故障频率:BPI = n/2 * fr * (1 + d/D * cos α)
- 滚动体故障频率:BSF = D/d * fr * (1 - (d/D * cos α)^2)
其中n是滚动体数量,fr是转频,d是滚动体直径,D是节圆直径,α是接触角。你可以写一个简易计算器:
def calc_characteristic_freq(n, fr, d, D, alpha_deg): alpha = np.radians(alpha_deg) cos = np.cos(alpha) bpo = n / 2 * fr * (1 - d / D * cos) bpi = n / 2 * fr * (1 + d / D * cos) bsf = D / d * fr * (1 - (d / D * cos) ** 2) return {"bpo": bpo, "bpi": bpi, "bsf": bsf} print(calc_characteristic_freq(n=9, fr=29.8, d=7.94, D=39.04, alpha_deg=0))逻辑说明:输出的这些频率值会在振动信号的包络谱上出现明显的峰值。当你用模型跑出一个「内圈故障」的预测结果后,可以再计算内圈故障频率 BPI,然后在测试样本的包络谱上找对应峰值。如果模型分类为内圈故障,但在 BPI 位置没有峰值,说明模型可能学错了特征。这种做法能把深度学习输出和传统信号处理结合起来,在毕业设计的答辩里是非常好的加分项。
参数说明:具体轴承的n, d, D, α需要查看数据集的实验台说明文件,不同轴承差别很大。如果你的压缩包里没有参数文件,可以先根据模型给出的类别反向去寻找对应频率,用峰值搜索法确认,而不是硬套公式。
做这一步的意义不只是好看,它能帮你判断当前模型是否真的可用。如果模型在大多数测试样本上都能和特征频率对上号,说明它学到了物理上正确的模式;如果只是耦合了噪声分布,那你最终交付的项目就会在换数据时崩掉。我现在的习惯是,每跑完一批测试就抽样三五个样本做频率校验,这比盲目调参更让人安心。希望这个习惯对你有用,希望这份笔记帮你在做类似项目时少踩几个坑。
本文还有配套的精品资源,点击获取