ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

1DCNN轴承故障诊断实战:数据划分、模型训练与跨工况验证

1DCNN轴承故障诊断实战:数据划分、模型训练与跨工况验证 简介面向工业设备故障诊断与深度学习初学者这套基于1DCNN的轴承故障诊断源码提供了一条从振动信号到故障分类的完整技术链路。资源共50个文件压缩包大小3.64MB其中13个Python脚本覆盖数据预处理、模型构建、训练测试与t-SNE可视化等环节17张PNG图片直观呈现正常、滚动体、内圈、外圈故障的识别结果txt与xml文件补充参数说明和项目配置xls/xlsx表格记录混淆矩阵和评估指标。已有426人学习下载代码采用TensorFlow/Keras实现可帮助读者理解一维卷积、池化与全连接层如何协作提取振动信号特征并掌握从数据清洗到模型评估的完整工程流程。对工业设备维护人员、故障诊断研究者和希望快速上手的深度学习入门者都是实用的参考。1. 做基于深度学习1DCNN轴承故障诊断源码的落地最先卡住的不是模型而是数据做基于深度学习1DCNN轴承故障诊断源码的落地最容易被卡住的不是网络结构而是怎么把一段连续振动波形变成能训练、能验证、能部署的输入输出关系。振动数据不是图像采样率动辄12kHz起步一段几秒钟的信号就是几万个采样点里面混着转频、冲击、噪声和传感器响应人工提特征要反复试峭度、峰值因子、包络谱换一台设备就要重来一遍。1DCNN直接吃原始波形卷积核在时间轴上滑动把特征提取交给网络自己学源码工程里最值得花时间的部分其实是数据怎么切、标签怎么对、划分怎么做。这篇文章写给正在复现轴承故障诊断项目的工程师和研究生先把CWRU这类公开数据吃透再落一个能跑的1DCNN最后搞清楚那些让精度虚高的坑。这也是深度学习实战项目案例里最容易水过去、但现场最要命的一段。2. 从 CWRU 振动信号到训练样本读 mat、滑窗切片、归一化一条线2.1 读原始 mat 文件先拿到一段完整的时间序列CWRU凯斯西储大学轴承数据中心是轴承故障诊断方向最常用的公开数据发布的是 .mat 文件每个文件对应某一种转速、负载和故障位置的长时间振动记录。用 scipy.io 的 loadmat 读进来文件里存的是字典结构振动通道的键名通常以DE_time结尾驱动端加速度计信号先别急着画图第一步是把这段一维数组完整取出来。import numpy as np from scipy.io import loadmat def load_cwru_mat(file_path): mat loadmat(file_path) # CWRU 的 mat 里振动通道变量名以 DE_time 结尾 key [k for k in mat.keys() if k.endswith(DE_time)] if not key: raise ValueError(f{file_path} 里没有找到 DE_time 通道) data mat[key[0]].flatten().astype(np.float32) return data这里有两个容易看漏的点。第一mat 文件里除了振动数据还有转速、负载等字段用DE_time过滤是为了精准取到加速度计信号而不是其他通道第二flatten()之后要确认数据是一维的有些 mat 文件保存成列向量不展平后面所有切片逻辑都会错位。12kHz 采样率下一个文件通常是十几万到几十万点对应几秒到十几秒的连续记录这就是后面所有训练样本的原料。2.2 滑动窗口切分窗口长度和步长决定了样本的独立程度拿到连续波形之后要把它切成长度固定的短片段每段是一个训练样本。这一步的常见做法是滑动窗口设定窗口长度和步长在整段信号上从头滑到尾。窗口长度不是随便拍的它要能覆盖至少几个完整的故障冲击周期。轴承内圈或外圈故障的冲击间隔和转频、故障特征频率相关在 12kHz 采样率下一个窗口取 1024 点约合 85 毫秒能覆盖常见转速下的多个冲击周期如果采样率是 48kHz窗口长度可以对应放大到 2048 甚至 4096。import os import glob from tqdm import tqdm window_length 1024 stride 512 X, y, file_ids [], [], [] fault_label_map {normal: 0, inner: 1, outer: 2, ball: 3} for file_path in tqdm(glob.glob(./cwru_data/*.mat)): vibration load_cwru_mat(file_path) fname os.path.basename(file_path).lower() if inner in fname: fault_type inner elif outer in fname: fault_type outer elif ball in fname: fault_type ball else: fault_type normal for start in range(0, len(vibration) - window_length, stride): X.append(vibration[start:start window_length]) y.append(fault_label_map[fault_type]) file_ids.append(file_path) X np.stack(X) y np.array(y) file_ids np.array(file_ids)步长等于窗口长度时样本之间无重叠步长小于窗口长度时相邻样本有重叠。我一般会把步长设为窗口长度的一半让样本量翻倍但这一步要小心重叠率越高相邻样本越发相似后面划分训练集和测试集时不按文件分组的话会造成非常严重的“同源泄露”。file_ids这行代码现在看着多余到了第 4 章做按工况划分的时候它就是保命的关键。标签映射这里我用的是故障位置四分类如果你要区分故障尺寸0.007、0.014、0.021 英寸类别数要对应扩展到 10 类或 12 类映射表里再加一层损伤尺寸的判断就行。2.3 归一化只能用训练集的统计量这一点没有商量振动信号的幅值在不同设备、不同负载下差别很大直接送进网络会让卷积核的梯度被大数值样本带偏所以归一化是必须的。常见做法有三种Z-Score 标准化减均值除标准差、MinMax 归一化缩放到 0 到 1、RobustScaler用中位数和四分位距抗异常冲击。我会优先用 Z-Score因为振动信号里偶尔会冒出很大的冲击峰值MinMax 会把正常段压得很扁。from sklearn.preprocessing import StandardScaler, RobustScaler, MinMaxScaler def normalize_samples(X_train, X_test, modezscore): n, length X_train.shape if mode zscore: scaler StandardScaler() elif mode robust: scaler RobustScaler() else: scaler MinMaxScaler() # 关键: 只用训练集拟合 scaler scaler.fit(X_train.reshape(n, -1)) X_train_norm scaler.transform(X_train.reshape(n, -1)).reshape(n, length, 1) X_test_norm scaler.transform(X_test.reshape(X_test.shape[0], -1)).reshape(X_test.shape[0], length, 1) return X_train_norm, X_test_norm, scaler这段代码里最容易翻车的点在scaler.fit的输入。如果你先把所有样本拼在一起算均值和标准差再划分训练测试集测试集的统计信息已经混进训练过程了得到的精度会偏乐观。正确顺序是先划分、后拟合。另外要注意reshape的维度归一化是在每个样本内部做的最后要 reshape 成(样本数, 窗口长度, 1)这个 1 是通道维对应 Conv1d 的输入通道数。后面模型里x.unsqueeze(1)就是为了把二维样本补成这个形状。3. 搭一个可复现的 1DCNN网络结构选型与四个关键参数3.1 为什么非要用 1DCNN而不是把信号画成频谱图喂给 2DCNN很多做振动诊断的团队一开始会走 2DCNN 路线把振动信号做短时傅里叶变换得到时频谱图再当图像分类。这条路不是不能走但它引入了两个额外变量窗函数选什么、重叠率设多少这本质上是把特征工程换了个形式继续手工做。1DCNN 直接在原始时间序列上卷积网络自己决定在哪个尺度上捕捉冲击和调制信息源码工程里少一层预处理复现和部署都更省事。1D 卷积还有一个好处是感受野的可解释性第一层卷积核覆盖 16 个采样点在 12kHz 采样率下对应约 1.3 毫秒这个时间尺度刚好能捕捉单个冲击响应的前沿多层堆叠后后面的层能看到更长的周期关系。3.2 最小可用模型三组卷积加一个全局平均池化下面这个结构是我在轴承故障诊断任务上的默认起点参数量小、不容易过拟合CWRU 四分类任务上很容易跑到 90% 以上的测试准确率前提是数据划分不造假。import torch import torch.nn as nn class Bearing1DCNN(nn.Module): def __init__(self, num_classes4): super().__init__() self.features nn.Sequential( nn.Conv1d(1, 32, kernel_size16, stride1, padding8), nn.BatchNorm1d(32), nn.ReLU(inplaceTrue), nn.MaxPool1d(kernel_size2, stride2), nn.Conv1d(32, 64, kernel_size16, stride1, padding8), nn.BatchNorm1d(64), nn.ReLU(inplaceTrue), nn.MaxPool1d(2), nn.Conv1d(64, 128, kernel_size8, stride1, padding4), nn.BatchNorm1d(128), nn.ReLU(inplaceTrue), nn.MaxPool1d(2), ) self.classifier nn.Sequential( nn.Linear(128, 64), nn.ReLU(inplaceTrue), nn.Dropout(0.3), nn.Linear(64, num_classes), ) def forward(self, x): # x 形状: (batch, window_length) x self.features(x.unsqueeze(1)) # 全局平均池化: 把长度维度压成 1 x x.mean(dim2) x self.classifier(x) return x这里几个结构决策值得展开。第一每组卷积后面都接 BatchNorm振动信号幅值分布不稳定BN 能让中间特征分布稳定下来训练会快很多第二用MaxPool1d(2)逐步把长度减半而不是在卷积层里用大步长一步压到位这样能保留更多时域细节第三分类器之前用全局平均池化把每个通道的整个时间长度压成一个值而不是Flatten后接一个大全连接层参数量从几百万降到几万级别CWRU 这种几千个样本规模的数据集上不容易过拟合。3.3 四个必调参数核大小、通道数、Dropout 和池化策略参数我常用的起点调整方向说明第一层 kernel_size16小样本用 8长序列用 32太小看不到冲击太大参数涨得快卷积核通道数32 → 64 → 128数据量少就减半通道数翻倍是 1DCNN 的常见扩展方式Dropout0.3过拟合时调到 0.5只放在全连接层卷积层不加池化MaxPool1d(2)序列特别长时用 stride4 的池化每次只降一半信息丢失可控核大小是最值得花时间的参数。轴承故障的冲击响应不是一个点而是一段衰减振荡核太小比如 3只能看到局部毛刺学不到冲击的形态核太大比如 64虽然感受野够了但这个尺寸的卷积核在第一层就要学 64 个权重乘输入参数量明显上升而且容易把噪声也当成特征。在 12kHz 采样率下16 到 32 是一个合理的起点你在自己的数据集上可以从 16 开始用验证集精度和训练曲线的收敛速度来判断该往哪个方向调。给一个我自己常用的判断方法如果训练集精度一直上不去先把核调大如果训练集精度很高但验证集掉得厉害先把 Dropout 调大并考虑减小通道数。4. 训练与评估先做按工况划分的数据集再谈精度4.1 按文件分组划分不按样本随机划分这一步是整个源码工程里最容易被忽视、也最影响结论的地方。很多初版实现会这么写把所有滑窗样本合在一起train_test_split(X, y, test_size0.3)然后跑出来一个很高的准确率觉得自己模型很厉害。但相邻滑窗来自同一段连续信号重叠区域几乎一样测试样本和训练样本高度相关测出来的准确率本质上是在考“记没记住这段波形”。正确做法是按文件分组让同一个 mat 文件的样本要么全在训练集、要么全在测试集。from sklearn.model_selection import GroupShuffleSplit groups file_ids # 每个样本来源文件路径 gss GroupShuffleSplit(n_splits1, test_size0.3, random_state42) train_idx, test_idx next(gss.split(X, y, groupsgroups)) X_train, X_test X[train_idx], X[test_idx] y_train, y_test y[train_idx], y[test_idx] file_train_ids file_ids[train_idx]GroupShuffleSplit按groups分组后划分同一文件的所有滑窗只会进一侧。这样评估的是模型的泛化能力而不是记忆能力。我在实际项目中见过同一个模型在随机划分下 98% 准确率、按工况划分直接掉到 85% 的情况差出来的十几个百分点就是泄露带来的虚高。做 CWRU 时还应该注意不同负载0hp、1hp、2hp、3hp的振动幅值和背景噪声不同更严格的评估是拿一个负载的数据训练在另一个负载的数据上测试这能直接验证模型是否学到了物理故障特征而不是某个负载下的幅值模式。下面这个表格是 CWRU 类任务上常见的现象不是精确数字但趋势非常稳定划分方式测试集来源常见现象全样本随机划分同文件相邻窗口精度虚高经常 95% 以上按文件分组划分同负载不同文件精度回落但仍在合理范围按负载跨域划分不同负载文件精度明显下降泛化差的模型会崩4.2 DataLoader 与训练主循环batch 大小、学习率和调度器划分完数据、做完归一化就可以组装 PyTorch 的 Dataset 和 DataLoader 了。这里我通常会限制 batch size 在 64 以内原因是振动滑窗样本之间本身就有冗余过大的 batch 会让每个 step 的梯度方向过于平滑收敛变慢且更容易陷入尖锐极小值。from torch.utils.data import TensorDataset, DataLoader import torch train_ds TensorDataset(torch.from_numpy(X_train_norm).float(), torch.from_numpy(y_train).long()) test_ds TensorDataset(torch.from_numpy(X_test_norm).float(), torch.from_numpy(y_test).long()) train_loader DataLoader(train_ds, batch_size64, shuffleTrue) test_loader DataLoader(test_ds, batch_size128, shuffleFalse)训练循环用 Adam CosineAnnealingLR 组合。Adam 负责快速找到一个不错的下降方向余弦退火负责在后期把学习率降下来让参数落进更平缓的区域。weight_decay 我一般设 1e-4相当于给权重加了个轻微的 L2 约束对抑制过拟合有帮助。from torch.optim.lr_scheduler import CosineAnnealingLR device torch.device(cuda if torch.cuda.is_available() else cpu) model Bearing1DCNN(num_classes4).to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max50) best_acc 0.0 for epoch in range(50): model.train() total_loss 0.0 for x, y in train_loader: x, y x.to(device), y.to(device) optimizer.zero_grad() logits model(x) loss criterion(logits, y) loss.backward() optimizer.step() total_loss loss.item() * x.size(0) model.eval() correct 0 total 0 with torch.no_grad(): for x, y in test_loader: x, y x.to(device), y.to(device) logits model(x) pred logits.argmax(dim1) correct (pred y).sum().item() total y.size(0) acc correct / total if acc best_acc: best_acc acc torch.save(model.state_dict(), best_bearing_1dcnn.pt) scheduler.step() print(fepoch {epoch1:02d} | loss {total_loss/len(train_ds):.4f} | acc {acc:.4f})训练循环里有两件事要刻意去做。第一每个 epoch 之间要把模型切回train()和eval()模式BatchNorm 在两种模式下行为不同忘了切会让验证精度忽高忽低这是个很隐蔽的坑第二只在验证集准确率刷新时保存模型权重训练末期模型可能已经过拟合最后几个 epoch 的权重未必是最好的。4.3 评估只看准确率不够类别和错误分布才是关键CWRU 四分类里正常样本和故障样本在波形特征上有非常明显的差异模型很容易把正常和故障分开难的是区分内圈、外圈、滚动体这三种故障位置它们的冲击特征在时域上高度相似。这时候只看整体准确率会掩盖很多问题可能模型把滚动体故障全部错分成内圈故障整体准确率还有 85%但这个模型在实际设备里完全没有诊断价值。所以评估至少要看每个类别的精确率和召回率再配合混淆矩阵看错误集中在哪一对类别之间。关于混淆矩阵和 t-SNE 可视化的具体做法我在最后一部分展开那里是判断模型到底学到了什么的关键一步。5. 1DCNN 轴承诊断的常见坑泄露、归一化与核大小判断5.1 坑一随机划分导致精度虚高跨工况直接翻车现象同样的模型和训练轮数随机划分测试集准确率轻松到 95% 以上但换成按文件分组或者跨负载测试准确率掉到 80% 出头甚至更低。原因相邻滑窗样本重叠度过高训练集和测试集里存在大量几乎相同的内容模型等于在背答案。解决一律用GroupShuffleSplit按文件分组划分如果数据量充足直接按负载划分训练和测试用不同工况的数据检验模型。5.2 坑二归一化统计量从全量数据计算测试分布泄露进训练现象先对整个 X 做 StandardScaler 再划分数据集训练测试精度都很漂亮部署到新设备上效果差很多。原因scaler 用了测试样本的均值和方差测试集的幅值分布信息在预处理阶段就进入了训练流程。解决先划分、后归一化scaler.fit只喂X_trainX_test只做transform。这个顺序在代码里就一行之差但结论的可靠性差很远。5.3 坑三卷积核大小凭感觉设模型要么欠拟合要么过拟合现象把第一层 kernel_size 设成 3训练集损失一直降不下去准确率卡在某个低值设成 64训练很慢验证集却出现抖动。原因核太小覆盖不了单个冲击响应的宽度核太大把噪声和局部细节也学了进去。解决从 16 开始观察训练曲线——收敛慢就加大到 32训练集精度上不去就检查输入信号是否真的包含足够特征而不是一味加大卷积核。同时配合池化层把长度逐步降下来不要指望一个大核一步到位。5.4 坑四batch size 过大训练不稳定且泛化变差现象batch size 从 64 调到 256loss 曲线震荡幅度变大验证集最佳准确率反而更低。原因振动滑窗样本高度冗余大 batch 里的样本梯度方向高度一致容易让优化器进入尖锐的极小值区域。解决CWRU 这类几万样本规模的任务batch size 64 是我常用的稳定值如果样本量大到需要分布式训练再考虑增大 batch 并同步调大学习率。5.5 坑五故障类别不平衡多数类把少数类淹没了现象诊断报告里正常类召回率 99%某一种故障类召回率只有 60%整体准确率仍然很高。原因正常样本远多于故障样本模型学会“全部猜正常”就能拿到高准确率。解决在 DataLoader 里按样本数反比设置类别权重或者给损失函数传weight参数class_counts np.bincount(y_train) class_weights 1.0 / np.maximum(class_counts, 1) class_weights torch.from_numpy(class_weights.astype(np.float32)).to(device) criterion nn.CrossEntropyLoss(weightclass_weights)这个改动会让少数类的梯度占比提高模型不再一味偏向多数类。但要注意加了权重之后整体准确率可能会略降这是正常现象你要看的是每个类别的召回率是否变得相对均衡。6. 用混淆矩阵与 t-SNE 验证模型真的学到了故障特征6.1 混淆矩阵和分类报告看错误集中在哪里模型训练完不要只看测试集整体精度先跑一段评估脚本把每个类别的精确率、召回率和 F1 打印出来。分类报告能直接告诉你哪些故障类别容易被混淆。from sklearn.metrics import confusion_matrix, classification_report preds, truths [], [] model.eval() with torch.no_grad(): for x, y in test_loader: x x.to(device) out model(x) preds.extend(out.argmax(dim1).cpu().numpy()) truths.extend(y.numpy()) cm confusion_matrix(truths, preds) print(Confusion Matrix:) print(cm) print(classification_report(truths, preds, digits3))如果错误集中在内圈和滚动体之间说明模型把两种故障的冲击特征混在一起了下一步可以检查这两类的波形是否真的差异明显或者考虑增加输入长度让模型看到更长的冲击间隔模式。6.2 t-SNE 可视化特征层验证类内聚集程度模型倒数第二层的特征输出是判断模型有没有真正学会区分的直观手段。把测试集样本过一遍模型提取全局平均池化之后的 128 维特征再用 t-SNE 降维到二维平面。from sklearn.manifold import TSNE import matplotlib.pyplot as plt features [] test_labels [] model.eval() with torch.no_grad(): for x, y in test_loader: x x.to(device) feat model.features(x.unsqueeze(1)) feat feat.mean(dim2).cpu().numpy() features.append(feat) test_labels.extend(y.numpy()) features np.vstack(features) tsne TSNE(n_components2, perplexity30, random_state42) coords tsne.fit_transform(features) plt.figure(figsize(8, 8)) for cls in np.unique(test_labels): mask np.array(test_labels) cls plt.scatter(coords[mask, 0], coords[mask, 1], s6, labelfclass {cls}) plt.legend() plt.savefig(tsne_features.png, dpi150)同一故障类别的特征点应该聚成团不同类别之间明显分开。如果 t-SNE 图里各类别糊成一片那模型大概率只是在遍历训练集泛化无从谈起。我拿到一个源码工程时第一个操作永远是去看数据划分和特征可视化而不是看报告里的准确率数字这两样东西最能排除“虚高精度”的干扰。6.3 跨负载验证用没训练过的工况数据做最后一块试金石CWRU 上还有一个更严格的验证方式拿一个负载的数据训练在另一个负载上测试。这个过程模拟了现场“模型在 A 设备训练部署到 B 设备”的真实场景。我习惯在模型训练完成后把另一个负载的样本单独过一遍模型计算一次跨域准确率。如果这个数字和同负载测试精度差很多说明模型学到了负载相关的幅值特征而不是轴承故障本身。这时候回头调数据预处理比如更严格的归一化和模型结构才是真正让这套 1DCNN 源码具备工程价值的路径。这套流程走完你对项目的判断不再是“作者报了多少准确率”而是“这个特征边界在什么条件下成立”。我以前也吃过随机划分的亏实验报告里写着 97% 的准确率拿到另一台设备的振动数据上直接失守后来才明白不是模型不行是评估方式给了虚假的信心。做轴承故障诊断的第一原则不是追求模型把训练集学到多完美而是把数据划分和特征验证做扎实让每个精度数字都能经得起跨工况的追问。希望这篇笔记帮你在自己的源码工程上少走一段弯路。本文还有配套的精品资源点击获取
返回列表