简介:一份基于一维卷积神经网络(1DCNN)的轴承故障诊断深度学习源码实现,面向机械状态监测与深度学习交叉领域的工程师和学习者,围绕振动时序信号处理,利用卷积层沿时间轴滑动提取局部特征,经池化与全连接映射为滚珠、内圈、外圈及保持架等故障类别,覆盖数据预处理到可视化完整流程。资源共62个文件,zip压缩包约3.7MB,含13个Python源码、17张故障可视化图、6个pyc编译文件、5个文本说明及配置备份文件;代码涵盖main入口、数据切分、CWRU数据上的1DCNN模型、测试与t-SNE降维模块,另有混淆矩阵表格和README辅助复现,data目录提供实验数据,备份文件便于对比调试。目前已有65人学习下载,通过研读完整工程,可掌握从原始振动信号端到端提取判别特征、训练1DCNN模型并验证效果的方法,适合课程设计、毕业设计或深度诊断项目参考。
1. 基于1DCNN的轴承故障诊断:为什么我扔掉手工特征改学原始波形
生产车间里电机驱动端的轴承一出现早期故障,传感器采到的振动信号里就会多出周期性的微小冲击。传统做法是先计算RMS、峰值因子、频谱边带,再靠老师傅的经验判断故障位置,这一套又慢又依赖人,而且微弱故障经常被噪声淹没。于是越来越多团队转向了1DCNN轴承故障诊断的深度学习路线:一维卷积网络直接拿原始振动波形当输入,把特征提取和分类交给网络自己学,端到端输出“正常、内圈故障、外圈故障、滚动体故障”。这篇文章不是泛谈原理,而是给出一套可复现的源码实现思路——从PyTorch模型定义、数据预处理,到训练调度、混淆矩阵评估和边缘部署。适合有一定Python基础、刚接触深度学习的故障诊断从业者,也适合想快速把1DCNN从论文落到实验台的设备健康监测工程师。跟我一起把这个方向的坑踩平,比看十篇综述有用。
2. 用1DCNN给轴承“听诊”:模型结构和选型逻辑
2.1 为什么是一维卷积,而不是把信号先变成图谱
很多文章喜欢把振动信号做短时傅里叶变换或者小波变换,变成一张时频图,再用二维CNN识别。这条路不是不行,但它至少多了两步:选什么窗函数、窗多长、重叠率多少,这些预处理参数调起来很玄学,一不小心就把故障冲击的瞬态成分抹掉了。一维卷积没有这个问题,它直接把加速度信号的一串时间序列作为输入,卷积核沿着时间轴滑动,每一层提取不同尺度的局部波形模式。
我选择1DCNN的核心理由是它的归纳偏置和故障信号天然匹配:轴承故障在时域上表现为周期性的冲击,冲击宽度很窄,一维卷积核(比如5个采样点)正好能覆盖这种短促波形;同时参数量比二维CNN少一个数量级,训练快,部署到采集器上不费力。更重要的是,原始时域波形保留了相位信息,对冲击发生的确切位置敏感,而时频图经过加窗和傅里叶变换后,这个位置信息变得模糊。做故障诊断时,相位对齐的瞬态冲击恰恰是区分内外圈故障的关键线索,丢掉它并不划算。
2.2 1DCNN的核心结构:卷积、池化、全连接,跟我写第一版
下面是我在PyTorch里最常用的一版1DCNN结构,压缩到只有三个卷积块,足够在CWRU轴承数据上拿到接近99%的准确率,同时参数量只有几十万,CPU都能跑。
import torch.nn as nn class DCNN1D(nn.Module): def __init__(self, num_classes=4): super().__init__() self.features = nn.Sequential( nn.Conv1d(1, 32, kernel_size=5, stride=2, padding=2), # 输入(batch, 1, L),输出通道32 nn.BatchNorm1d(32), nn.ReLU(), nn.MaxPool1d(kernel_size=2), nn.Conv1d(32, 64, kernel_size=3, stride=1, padding=1), nn.BatchNorm1d(64), nn.ReLU(), nn.MaxPool1d(kernel_size=2), nn.Conv1d(64, 128, kernel_size=3, stride=1, padding=1), nn.BatchNorm1d(128), nn.ReLU(), nn.AdaptiveAvgPool1d(1), # 不管前面长度变多少,这里统一压缩成1个时间点 ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(128, num_classes) ) def forward(self, x): return self.classifier(self.features(x))第一个卷积层用stride=2配合padding=2,输出长度变为原来的一半;后面两层保持长度不变,靠MaxPooling降采样。最后用AdaptiveAvgPool1d(1)替代硬编码的展平,好处是输入长度不必固定——1024点也好,2048点也好,在全局平均池化后都变成128维向量,再进全连接层。BatchNorm在这类小数据集里特别重要,它能稳住每层输入的分布,让模型更容易收敛。如果去掉批归一化,同样的样本可能需要多训练两三轮才能达到同等精度。
2.3 输入形状与标签设计:先想清楚再写代码
很多新手在这个节点翻车。1DCNN的输入形状是(batch_size, channels, length),对加速度传感器来说通道数通常是1,所以每个样本变成了(1, 1024)这种形状,代表一个通道、1024个时间采样点。这个1024不是拍脑袋定的,它要覆盖至少一个转轴的旋转周期。比如电机转速1800r/min,转频是30Hz,对应周期约33毫秒;如果采样率是12kHz,那么一个周期约400个采样点,取1024点能包含两到三个冲击周期,模型才能看到完整的故障节拍。
标签建议先做粗粒度:正常、内圈故障、外圈故障、滚动体故障,这是num_classes=4。等验证完流程再考虑把损伤直径加进去,变成不同尺寸的多个子类。我自己习惯先把类别数控制在4到10之间,否则样本太少时模型很容易把同类故障的不同尺寸当成不同类别去硬记,泛化很差。CWRU这类公开数据集里,每个故障状态还有负载、转速等工况信息,这些字段暂时不要并进标签,先放在边上,后面做跨工况验证用。
3. 数据准备与预处理:从原始振动信号到训练样本
3.1 滑动窗口切分:把连续信号变成独立样本
原始振动信号都是几分钟的长记录,不能整段丢给神经网络。常见做法是像切香肠一样用滑动窗口切成固定长度的样本。窗口长度和步长是两个最要命的参数,直接影响样本量和数据泄漏。
import numpy as np def sliding_window(signal, window_len=1024, stride=512): samples = [] for start in range(0, len(signal) - window_len + 1, stride): samples.append(signal[start:start + window_len]) return np.array(samples)这个函数很简单,signal是某一通道的全部振动数据,window_len是每个样本包含多少点,stride是窗口每次滑动的步长。取window_len=1024, stride=512意味着相邻样本有一半重叠。为什么要重叠?因为信号是连续的,故障冲击可能出现在任意位置,重叠采样能让模型在训练时看到不同相位位置附近的波形,等于一种隐式数据增强。重叠率别太高,我用过stride=128,结果训练集和验证集里大量高度相似的样本互相“传染”,验证准确率高得吓人,一换工况就露馅。一般取窗口的一半作为步长比较稳妥。
3.2 从原始数据到PyTorch Dataset:归一化与标签映射
振动信号的单位是加速度,不同采集设备、不同传感器灵敏度导致幅值千差万别。归一化是必须的,但我强调一点:归一化要在每个样本内部单独做,而不是在整个数据集上算全局均值和标准差。在实际部署时,你拿到的是一条实时数据流,根本不知道未来十分钟的全局方差是多少。
from torch.utils.data import Dataset class VibrationDataset(Dataset): def __init__(self, samples, labels, normalize=True): self.samples = samples.astype(np.float32) self.labels = labels.astype(np.int64) self.normalize = normalize def __len__(self): return len(self.samples) def __getitem__(self, idx): x = self.samples[idx] if self.normalize: mean = x.mean() std = x.std() + 1e-6 x = (x - mean) / std return x.reshape(1, -1), self.labels[idx]__getitem__里对每个样本做z-score归一化,std加上1e-6是为了防止信号段完全恒定时除零。注意reshape(1, -1)才把一维信号变成(1, length)形状,符合1DCNN的输入要求。Dataset写好之后,交给PyTorch的DataLoader就能在训练时自动打乱、按batch取数。我还习惯在构造函数里额外传一个transform位置,但振动信号别乱加增强,加正弦工频干扰反而会让模型学到伪特征。
3.3 划分训练集和验证集:千万别把同一次故障的样本切进两边
这是轴承故障诊断里最容易犯错、代价也最惨痛的一步。CWRU数据集里,同一个文件里连续滑动窗口产生的样本高度相关,如果简单地把所有样本拼起来再train_test_split,那同一个物理时刻的波形会同时出现在训练集和验证集里。模型记住的是这段波形上的具体噪声,而不是故障本身,验证集分数会虚高十多个点,跨负载测试时立刻现原形。
我现在的做法是按“文件”而不是按“样本”划分。每一个CWRU文件对应某一种故障在某一负载下的完整记录,把属于同一个文件的所有窗口归入同一侧。
import numpy as np file_dict = { 0: ["normal_0.csv", "normal_1.csv"], # 正常类下的文件列表 1: ["inner_0.csv", "inner_1.csv"], # 内圈故障 2: ["outer_0.csv", "outer_1.csv"], # 外圈故障 3: ["ball_0.csv", "ball_1.csv"] # 滚动体故障 } train_files, val_files = {}, {} for label, file_list in file_dict.items(): np.random.shuffle(file_list) split = int(len(file_list) * 0.8) train_files[label] = file_list[:split] val_files[label] = file_list[split:]划分完成后,训练和验证各自从对应的文件列表里读数据、滑动切窗,再组装成VibrationDataset。这样做可能会牺牲一点验证集样本量,但换来的评估结果才是可信的。如果用的是自己采集的数据,同样的原则:按“工况时间段”划分,而不是把每个时间段内切出来的窗口随机混洗。做故障诊断,数据泄漏比模型选型错误要命多了。
4. 模型训练与评估:从loss曲线到混淆矩阵
4.1 训练循环:损失函数、优化器、调度器怎么配
分类任务默认用交叉熵损失。优化器我推荐AdamW而不是裸Adam,因为AdamW把权重衰减放在解耦的位置上,等价于更干净的L2正则化,对提升验证集表现有帮助。初始学习率先给1e-3,同时用余弦退让调度器让学习率在训练后段逐渐降下来。
import torch import torch.nn as nn from torch.utils.data import DataLoader model = DCNN1D(num_classes=4) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50) train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True) for epoch in range(50): model.train() total_loss = 0.0 for xb, yb in train_loader: optimizer.zero_grad() out = model(xb) loss = criterion(out, yb) loss.backward() optimizer.step() total_loss += loss.item() scheduler.step() if (epoch + 1) % 10 == 0: avg_loss = total_loss / len(train_loader) print(f"Epoch {epoch + 1}, loss {avg_loss:.4f}")T_max=50表示学习率在50轮里从初始值余弦降到接近0。如果你的训练轮数改成100,T_max也要同步改成100,否则学习率在没有训练完时就已经触底了。batch size取128是因为每个样本只有1024个float32,约4KB,128个也才512KB,显存和内存都轻松。全连接层比较小,整个模型参数量只有几十万,在小数据集上已经够用。
4.2 早停与模型保存:别拿最后一轮当最优模型
训练50轮后,最后一轮的模型不一定是最优的,因为学习率降低后模型可能已经在小范围内震荡。我习惯在每轮结束后跑验证集,记录验证准确率最高的那一次,把它的权重单独存下来。
best_val_acc = 0.0 for epoch in range(50): model.train() for xb, yb in train_loader: optimizer.zero_grad() loss = criterion(model(xb), yb) loss.backward() optimizer.step() model.eval() correct = 0 total = 0 with torch.no_grad(): for xb, yb in val_loader: out = model(xb) pred = out.argmax(dim=1) correct += (pred == yb).sum().item() total += len(yb) val_acc = correct / total if val_acc > best_val_acc: best_val_acc = val_acc torch.save(model.state_dict(), "best_1dcnn.pt") print(f"Best val acc: {best_val_acc:.4f}")这段代码会在验证集准确率刷新时保存一次模型。PyTorch里model.eval()和with torch.no_grad():两个都要写,前者把BatchNorm和Dropout切换到推理模式,后者停止构建计算图,省内存。加载模型做推理时也要先model.load_state_dict()再model.eval(),否则BatchNorm在单条样本上还会算有偏的统计量,容易让结果漂移。
4.3 混淆矩阵和特征可视化:准确率也会骗人
只有整体准确率就像只知道体检总分,哪个部位出了问题是看不出来的。故障诊断里常出现内外圈故障混淆,因为它们的冲击周期很接近。打印混淆矩阵能一眼看穿模型到底卡在哪一对类别上。
from sklearn.metrics import confusion_matrix, f1_score model.eval() y_true, y_pred = [], [] with torch.no_grad(): for xb, yb in val_loader: out = model(xb) y_pred.extend(out.argmax(dim=1).numpy()) y_true.extend(yb.numpy()) cm = confusion_matrix(y_true, y_pred) print(cm) print("weighted F1:", f1_score(y_true, y_pred, average="weighted"))confusion_matrix返回的是二维数组,第i行第j列表示真实类别i被预测成类别j的样本数。对角线越亮就越健康;如果某两个类别互相错到两位数级别,就值得增加窗口长度,让模型看到更长的故障冲击序列,或者考虑在输入里拼接一个同步采集的转速信号。只看准确率不看混淆矩阵,很容易把泛化能力差的模型当宝贝留着。
5. 避坑:1DCNN轴承故障诊断最易翻车的五个场景
5.1 验证准确率99%,换一台设备就崩
现象:用CWRU数据随机切分样本,验证集准确率能到99%以上;把同一组模型拿到另一负载或现场数据上测试,准确率立刻掉到60%上下。
原因:绝大多数情况下是数据泄漏——同一文件的滑动窗口同时进入了训练集和验证集,模型记住了文件里的噪声指纹,而不是故障波形的一般特征。另外,CWRU数据的某一个负载工况下采集到的信号具有特定的转频和负载噪声,模型学到的特征与工况强绑定,一换环境就失效。
解决:按文件或按工况划分数据,把训练集和验证集彻底隔离。进一步做跨工况验证,比如用0HP负载的数据训练,用1HP、2HP负载的数据测试。如果跨工况准确率不高,那是正常现象,说明模型对工况变化敏感,需要在训练时加入更多工况的数据,或者做域适应方向的研究。以后看到“随机切分99%”的论文,先怀疑一下它有没有做好数据隔离。
5.2 Loss变成NaN,训练中途崩了
现象:前几个epoch还挺正常,loss在下降,某个batch之后突然变成NaN,接着后面所有loss都是NaN。
原因:最常见是学习率过大,导致梯度更新越过最优区域进入数值爆炸区;另一个常见原因是信号样本里存在标准差为0的常量段,虽然归一化时加了1e-6的epsilon,但除以一个极小值后产生极大数值,激活值溢出。PyTorch默认不检查数值稳定性,NaN一旦出现就会污染整个计算图。
解决:先把学习率降到1e-4再试;如果还在NaN,就在反向传播之后加梯度裁剪,把梯度范数限制到1.0:
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)同时在数据预处理阶段过滤掉标准差低于某个阈值(比如1e-3)的样本。我自己的习惯是每次看到NaN,先打印出触发NaN的那个batch样本值,看是不是有原始的满量程尖峰,如果是,就在__getitem__里对该样本做边界截断,把所有绝对值超过5倍中位数的点缩回去。
5.3 训练集loss下降,验证集loss不降:过拟合
现象:训练loss一路下探,验证loss在第20轮左右开始掉头上扬,验证准确率也停止提高甚至下降。
原因:模型容量相对训练样本太多了。一个CWRU文件滑动窗口可以切出几千个样本,但真正独立的物理片段只有一个文件里的那段时程;模型很容易记住这些高度重叠样本里的细枝末节。
解决:先加正则化。把AdamW的weight_decay从1e-4提到1e-3,同时在全连接前插入一个nn.Dropout(p=0.5)。早停是最实用的后悔药,监控验证loss连续10轮不降就停。如果正则化还不够,就要怀疑是窗口重叠率太高,把stride调大,从512改成1024,减少训练样本相关性。最后才是考虑压缩模型宽度,把第一层32个卷积核减到24个。按这个顺序排查,90%的过拟合都能缓解。
5.4 不同采样频率的数据混着用
现象:CWRU数据里有12kHz和48kHz两种采样频率,把它们放在同一个训练集里,验证准确率永远卡在80%上下,调参也上不去。
原因:同一个物理波形,用12k和48k采样后得到的数字序列差异巨大。卷积核尺寸为5时,在12kHz下覆盖约0.4毫秒,在48kHz下只覆盖约0.1毫秒,感受野对应的物理时间宽度完全不一样,模型很难学到一个统一的冲击特征。
解决:最干净的做法是只用一种采样频率。很多公开论文只使用12kHz的驱动端加速度数据,简单省事。如果你手里只有48k数据,可以先把采样率降下来,或者把窗口点数从1024增加到4096,保证窗口覆盖的物理时间长度和12k时差不多。我的血泪经验是:把不同采样频率当成两个独立的训练集,各训一个模型,部署时按设备实际采样率选择对应模型,而不是强行混合。
5.5 类别不平衡:正常样本太多,模型学会偷懒
现象:故障类别有好几种,但正常样本占80%以上,训练完发现模型把所有样本都预测为正常,整体准确率照样很好看。
原因:交叉熵损失在样本数极度不均时趋向于偏向多数类,因为把所有样本都预测为多数类就能把平均loss压得足够低。故障样本本来数量少,还被随机采样稀释,梯度贡献被淹没。
解决:用类权重重加权。权重反比于每类样本数,计算方式为N_total / (num_classes * N_class):
import numpy as np classes = np.array([1000, 250, 250, 250]) # 各类样本数 weights = classes.sum() / (len(classes) * classes) class_weight = torch.tensor(weights, dtype=torch.float32) criterion = nn.CrossEntropyLoss(weight=class_weight)这样少数类的错误分类会得到更大惩罚。如果加了类权重还是不行,就用WeightedRandomSampler对少数类过采样,让每个batch里不同类别的比例更均衡。先重加权,再考虑过采样,因为重加权不会增加训练轮数,实现也简单。
6. 不止于跑通:把1DCNN用到真实设备前的三个验证习惯
模型在CWRU上拿到高准确率只算完成了一半,真正上设备之前,我自己还坚持做三件小事。
第一是拿包络谱去对照模型判断。训练结束后,对每个预测为“内圈故障”的测试样本做希尔伯特解调,再求包络谱,看解调谱里有没有内圈故障特征频率(BPFI)及其边带。如果模型预测内圈,但包络谱上出的是外圈特征频率,那就要怀疑模型学到了别的混叠特征。这个方法不需要改网络,只需要用scipy.signal.hilbert算一遍包络谱,几行代码就能给预测结果增加一个物理验证。
第二是t-SNE看特征聚类。把模型全连接层前的128维特征取出来,映射到二维平面,正常样本和每种故障应该聚成几个紧凑的团。如果某种故障的样本散成两瓣,通常意味着该类别内部还有子模式,比如故障尺寸不同导致波形差异过大,这时候就需要回头检查标签是不是太粗了。
第三是导出ONNX测推理速度。PyTorch模型在验证机上跑得快还不够,常有中间部署环境没有GPU,只能用CPU。torch.onnx.export(model, dummy_input, "model.onnx")导出后用onnxruntime跑一下,量化观察单次推理耗时。我见过1DCNN在树莓派上单条样本只要1毫秒出头,完全够用。这三步做完,心里才有底把它接到产线上去。
现在每训练完一组模型,我的第一反应不再是打印准确率,而是按文件划分、跨工况验证、打印混淆矩阵。先把这三个习惯变成强制动作,再谈调模型结构。希望这一篇基于1DCNN的轴承故障诊断源码实现能够帮你在自己的数据上少走几趟弯路。
本文还有配套的精品资源,点击获取