☰
基于PyTorch与1D-CNN的轴承故障诊断实战:从滑窗切分到模型训练
2026/10/17 4:30:26 网站建设 项目流程

轴承故障诊断在工业场景里一直是个让人头疼的问题。设备转着转着突然罢工,停机损失动辄几十万,而故障信号往往淹没在环境噪声里,靠老师傅"听音辨位"和经验积累,既难复制又难规模化。用深度学习做振动信号分类,这几年在工业圈里已经不算新鲜事,但真正能把代码跑通、效果做出来,并且知道每一步在想什么的人,其实不多。这篇文章从头到尾走一遍用PyTorch搭建轴承故障诊断模型的完整流程,数据准备、网络结构、训练调参、踩坑排查都有,代码可直接复制运行。

适合两类读者:一是刚入门深度学习、想拿一个具体工业场景练手的学生或开发者,二是设备维护或故障诊断方向的技术人员,希望用AI方法替代传统特征工程,但对神经网络还不太熟。前者能学到一套完整的项目范式,后者能直接参考代码改到自己项目上。

1. 方案设计与数据准备

1.1 为什么选择深度学习方案,而不是传统特征方法

传统的轴承故障诊断通常走"信号处理+特征提取+分类器"的路线。先做傅里叶变换、小波变换或包络分析,把时域信号转到频域,然后手工设计统计特征,比如峰值、峭度、频带能量占比等,最后用支持向量机或随机森林做分类。这套方法的问题在于:特征设计依赖领域知识,换一台设备、换一种工况,原先调好的特征往往就不灵了。

深度学习方案则把特征提取和分类融合在一个端到端的模型里。你把原始振动数据喂进去,模型自己学出有用的特征表达。1D-CNN在时序信号上天然有优势——它跟处理图像的2D-CNN原理相似,只是一维卷积核沿着时间轴滑动,能捕捉局部的时序模式,而多个卷积层叠加后,模型能逐渐感知更大范围的上下文信息。对于轴承振动信号来说,不同故障类型有不同频率成分和冲击特征,CNN逐层提取局部模式再组合成高层的判别特征,这条路径完全可行。

在公开数据集上,深度学习方案通常能把分类准确率做到99%以上,比传统机器学习方法高出一截,尤其在故障类型多、工况杂的情况下,优势更明显。当然,深度学习需要更多数据和算力,这在实际应用中要考虑进去,但作为方案验证和预研,用PyTorch搭建一版模型已经是很常规的操作了。

1.2 数据集说明:故障类型与数据文件结构

这里用某开源轴承故障数据集来做演示,数据来自一个实验室台架,包含正常轴承和多类故障轴承的振动加速度信号。故障分为三个位置:滚动体故障、内圈故障、外圈故障,每个位置下又有三种不同损伤直径规格。加上正常状态,一共是10个类别。信号采样率12kHz左右,每种状态有多段时长为1秒左右的振动信号文件。这个数据结构在故障诊断领域很典型,不管后面换任何数据集,只要格式类似,代码逻辑都能直接套用。

数据文件组织成按类别分文件夹的形式:

data/ ├── normal/ # 正常 ├── ball_007/ # 滚动体故障 0.007英寸 ├── ball_014/ # 滚动体故障 0.014英寸 ├── ball_021/ # 滚动体故障 0.021英寸 ├── inner_007/ # 内圈故障 0.007英寸 ├── inner_014/ ├── inner_021/ ├── outer_007/ # 外圈故障 0.007英寸 ├── outer_014/ └── outer_021/

如果你手头的数据集命名方式不同,或者数据量更少,也没有关系。最关键的是你的数据加载代码能自动从一个文件夹对应一个类别的结构里读取数据,并且每个类别打包成独立的标签向量。

1.3 滑窗切分与训练/测试集划分

原始振动信号每段可能很长,直接整段丢进神经网络,一方面序列太长导致计算量太大,另一方面一整段信号里的特征不够纯,混入了不同时段的噪声和波动。通常的做法是滑窗切分,把长信号切成固定长度的短片段。窗口长度取多少是个关键选择。

我这里的经验值是窗口长度取1024个采样点。因为该数据集的转速约1800转/分钟,对应30Hz转频,1024个点除以12kHz采样率约等于85毫秒,正好覆盖约2.5个旋转周期。轴承每一圈转过去,滚子经过故障位置的冲击特征会出现一次,所以窗口里至少应该有2-3次完整的冲击模式,CNN才有足够信息去判别。如果窗口太短,比如256个点,只能覆盖不到一个旋转周期,冲击特征可能压根没被采集进来,模型就学不到东西。

滑动步长设为512,也就是相邻窗口之间有50%重叠。这相当于做了数据增强:一段1秒的信号能切出大约14个窗口,总样本量成倍增加。特别要注意的是,切分和划分训练集、测试集这一步必须非常谨慎,否则会严重高估模型效果。同一条原始信号切出来的相邻窗口,因为高度相关,不能一部分放进训练集,另一部分放进测试集。正确的做法是:先把原始文件按类别分成训练集文件列表、验证集文件列表和测试集文件列表,再分别切窗。确保同一个文件生成的所有窗口永远只出现在同一个集合里。这个陷阱很多第一次做时序分类的人都会踩,后面章节我详细展开讲。

2. 核心代码实现与模型设计

2.1 环境依赖与项目结构

代码用到的东西不多:PyTorch(1.10以上版本都行)、numpy、pandas、scikit-learn、matplotlib。安装命令一行解决:

pip install torch numpy pandas scikit-learn matplotlib

项目文件我习惯按下面这个结构组织,耦合度低,后面扩展方便:

bearing_fault_diagnosis/ ├── config.py # 超参数配置 ├── data_loader.py # 数据加载和滑窗切分 ├── model.py # 1D-CNN 模型定义 ├── train.py # 训练逻辑 └── evaluate.py # 测试和混淆矩阵

2.2 数据加载与滑窗切分实现

在data_loader.py中,核心是一个函数,读入所有原始振动文件,完成滑窗切分和标签映射。整个过程的核心逻辑分三步:第一步扫描文件并建立类别到标签的映射表,第二步逐文件读取振动数据并做滑窗,第三步做归一化。

振动信号的量纲和幅值在不同传感器、不同设备上差异很大,如果不归一化,模型收敛会很慢,甚至训练发散。我用的归一化方式不是简单的 min-max 缩放,而是先算整个训练集的均值和标准差,再用(x - mean) / std做标准化。保留训练集的均值和标准差参数,测试集也用它来归一化,这保证了模型在推理时面对的数据分布跟训练时一致。

import os import numpy as np import torch from torch.utils.data import Dataset, DataLoader class BearingDataset(Dataset): def __init__(self, file_list, labels, window_size=1024, step=512): self.file_list = file_list self.labels = labels self.window_size = window_size self.step = step self.samples = [] self.targets = [] self._load_and_slice() def _load_and_slice(self): for file_path, label in zip(self.file_list, self.labels): signal = np.loadtxt(file_path) for start in range(0, len(signal) - self.window_size + 1, self.step): window = signal[start:start + self.window_size] self.samples.append(window) self.targets.append(label) def __len__(self): return len(self.samples) def __getitem__(self, idx): x = self.samples[idx].astype(np.float32) x = (x - self.mean_) / (self.std_ + 1e-8) y = self.targets[idx] return torch.tensor(x).unsqueeze(0), torch.tensor(y, dtype=torch.long)

建议在BearingDataset上加两个类属性或者额外传入归一化参数,因为上面代码里的self.mean_和self.std_尚未初始化,实际中我会在建数据集之前先用所有训练文件计算好全局统计量,再传给数据集对象。这里为了可读性简化了处理。

2.3 一维CNN模型设计思路

模型我用了常见的1D卷积堆叠结构。先用几层卷积不断下采样,让感受野逐步变大,再全局平均池化压成特征向量,最后接全连接层输出10类概率。为什么要加下采样?因为1024个采样点直接接全连接层的话,参数量是1024×分类数,虽然勉强能训,但全连接层很难提取到有平移不变性的局部特征。

第一层卷积核大小取64。这个偏大是有意的。采样率12kHz下,64个采样点约5.3毫秒,覆盖一个冲击响应的大半段波形,第一层就能捕获到基础的冲击形态。后面的卷积核缩小到3或5,用于更高层的特征组合。

import torch.nn as nn class FaultCNN(nn.Module): def __init__(self, num_classes=10): super().__init__() self.features = nn.Sequential( nn.Conv1d(1, 16, kernel_size=64, stride=8, padding=28), nn.BatchNorm1d(16), nn.ReLU(), nn.MaxPool1d(kernel_size=2, stride=2), nn.Conv1d(16, 32, kernel_size=3, stride=1, padding=1), nn.BatchNorm1d(32), nn.ReLU(), nn.MaxPool1d(kernel_size=2, stride=2), nn.Conv1d(32, 64, kernel_size=3, stride=1, padding=1), nn.BatchNorm1d(64), nn.ReLU(), nn.AdaptiveAvgPool1d(1), ) self.classifier = nn.Linear(64, num_classes) def forward(self, x): feat = self.features(x) feat = feat.view(feat.size(0), -1) return self.classifier(feat)

细节上做了两个关键处理。BatchNorm放在卷积和ReLU之间,能让每层输入分布更稳定,同时一定程度缓解梯度消失。AdaptiveAvgPool1d(1)把每个通道压成一个数,这样不管输入窗口长度怎么变,全连接层前的特征维度始终是64,模型对窗口长度的适应能力更强。

参数规模很轻,整个模型大概2万多个可学习参数,一张普通显卡甚至没有显卡用CPU也能训练,这是1D-CNN处理振动信号的好处——比2D图像模型轻量太多。

2.4 训练脚本核心流程

训练逻辑相对常规,但有三点实践经验值得提。用早停法,在验证集准确率连续多个epoch没有上升时自动终止训练,防止过拟合。保存最佳模型权重,而不是最后一轮的权重。学习率epoch轮数降一下,用固定值二分衰减。

# 训练核心循环(节选) best_val_acc = 0.0 patience = 15 bad_epochs = 0 for epoch in range(max_epochs): model.train() train_loss = 0.0 for x_batch, y_batch in train_loader: x_batch, y_batch = x_batch.to(device), y_batch.to(device) optimizer.zero_grad() outputs = model(x_batch) loss = criterion(outputs, y_batch) loss.backward() optimizer.step() train_loss += loss.item() * x_batch.size(0) scheduler.step() # 每个epoch结束验证一次 val_acc = evaluate(model, val_loader, device) print(f"Epoch {epoch+1:03d} | Train Loss {train_loss/len(train_loader.dataset):.4f} | Val Acc {val_acc:.4f}") if val_acc > best_val_acc: best_val_acc = val_acc bad_epochs = 0 torch.save(model.state_dict(), "best_model.pth") else: bad_epochs += 1 if bad_epochs >= patience: print("Early stopping triggered.") break

3. 训练实验与结果解读

3.1 超参数选择的依据

我把关键超参数列了一张表,后面可以直接照抄:

超参数取值选择理由
窗口长度1024覆盖约2.5个旋转周期,捕获多次冲击特征
滑动步长512相邻窗口50%重叠,等价于数据增强
训练批次大小64平衡显存占用和梯度稳定性
初始学习率0.001Adam常用的默认值,收敛平稳
学习率衰减每10轮×0.5后期缩小步长,收敛更精细
最大轮数50配合早停,不会因为轮数过多浪费时间
早停耐心值15允许验证集震荡15轮再停

批次大小64配合这批数据没问题,如果你的数据量翻了好几倍,显存又够,适当调大到128或256可以减少训练时间。学习率初始0.001是Adam普遍好的起点,学习率再大容易震荡,再小收敛太慢。

3.2 完整实验效果与收敛过程

训练集大约6万多个窗口,验证集约1.2万个,测试集单独约1.2万个,类别完全平衡。用CPU训练大约30多分钟跑完50轮,如果换成入门级GPU,十分之一的时间就够了。最终测试集准确率能到99.6%左右。

训练过程比较有趣的是前几轮变化极大。第1轮验证集准确率只有60%多,第3轮就跳到95%以上,第8轮过后稳定在99%附近,后面几乎只有小数点后两位在动。这说明这类振动分类任务对模型来说区分度很高,CNN很快就能抓住关键特征。当然,优秀结果的前提是泄漏问题处理干净了。一旦切窗不严谨把同一个文件的窗口同时分到了训练和测试,准确率直接显示100%,那个结果完全是假的。

3.3 混淆矩阵看分类细节

光看准确率不够,我每次都要打印测试集的混淆矩阵。混淆矩阵能告诉你模型究竟错在哪两个类别上,是普遍性小错误还是某种故障类型根本分不出来。

from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay import matplotlib.pyplot as plt cm = confusion_matrix(true_labels, pred_labels) disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=class_names) disp.plot(cmap="Blues") plt.show()

在该数据集上,几乎没有明显的混淆块。偶尔出现的错误样本集中在同一故障位置的不同故障直径之间,比如内圈0.007英寸被误判为内圈0.014英寸。这个结果有物理上的合理性——损伤直径差异导致的振动冲击能量变化是连续的,相邻规格的波形特征相似度高。如果是为了区分故障位置而不关心具体损伤直径,还可以把这几个规格合并成一个大类,准确率会更加贴近100%。

4. 常见问题与排查技巧实录

4.1 数据泄漏:准确率虚高的最大陷阱

这个问题我在前面提到过一次,但必须再强调一遍。很多初学者直接把所有窗口混在一起,然后按比例随机划分为训练集和测试集,得到的准确率动辄99.99%,还以为模型很牛,其实拿到生产环境完全不行。

原因在于:同一个原始信号文件切开的重叠窗口之间存在高度相关性,它们几乎共享同一段波形,模型在训练时见过这个波形的绝大多数片段,测试时遇到这些片段的"近亲",等于开卷考试。真实场景中,设备新采集的一段信号,跟历史数据是不同工况下的独立样本,开卷考试的分数没有参考价值。

正确的做法,也是我上文强调的,先划分文件列表,再切窗。分类模型的泛化能力要用独立的、完全没见过的那部分数据来检验。

4.2 训练集准、测试集差,问题出在哪

如果训练集准确率很快接近100%,而测试集只有80%不到,说明模型过拟合了。原因可能集中在几处:窗口重叠率太高导致数据冗余度过大;模型容量太大,比如卷积通道数翻倍、全连接层加宽,而数据量又不够;还有一种容易被忽略的情况——训练集和测试集的数据来自不同的负载工况,特征分布本身就有偏移。

解决方案优先考虑降低模型容量。把这个模型第一层卷积通道数从16降到8,感受野和池化策略不动,模型仍然够用。更通用的做法是加Dropout,在全连接层前加nn.Dropout(p=0.5),让模型不能过度依赖某几个神经元。如果工况差异实在太大,那就不该勉强用一个模型通吃,而是按工况分别建模,或者引入域自适应思路。

4.3 训练速度慢到怀疑人生

代码本身很快,如果有人觉得训练慢,多半卡在数据加载环节。一个经常踩的坑是数据文件用文本格式存大量振动数据,读文件又是循环逐条load进内存,训练时GPU等CPU送数据一等就是半天。

我的排查顺序是:先看是不是在__getitem__里每次都在做重复计算或者磁盘IO,再看DataLoader是不是用了多个子进程。确保num_workers大于0,并且把切好的窗口提前全部加载到内存里,而不是训练过程中实时从原始文件滑窗切分。6万个窗口,每个1024个float32,总共约240MB内存,完全不值得为了省内存牺牲速度。

4.4 台上效果好,现场不灵

这是工业项目里最扎心的问题。实验室数据干净、转速稳定、传感器位置固定,模型在测试集上99%不代表部署到实际产线上还能达到同样水平。现场轴承的负载随机波动、变转速、环境噪声、多级传动带来的干扰,都会让数据分布偏移,模型的准确率掉到70%甚至60%也不奇怪。

应对思路有几条:采集数据时尽量覆盖多个工况;把不同转速和负载的数据都放进训练集;用加速度信号的原始值之外,再跑一下包络谱或短时傅里叶变换,把频域信息也作为输入,增强对外界干扰的鲁棒性。对部署到嵌入式设备或边缘网关的场景,这个模型也可能是偏大了,需要进一步做剪枝或量化,把模型压缩到几百KB以下。

5. 扩展方向与个人体会

5.1 模型结构还可以这样改

基础CNN在这个任务上已经接近饱和,想进一步提升或者在更复杂场景下用,可以考虑把模型扩展成CNN-LSTM混合结构。CNN负责提取局部时空特征,LSTM负责捕捉整个窗口内冲击序列的先后依赖关系。同样的数据,训练时间大概翻倍,但在工况偏移更明显的场景下,效果往往比纯CNN稳一些。

class CnnLstm(nn.Module): def __init__(self, num_classes=10): super().__init__() self.cnn = nn.Sequential( nn.Conv1d(1, 16, kernel_size=64, stride=8, padding=28), nn.BatchNorm1d(16), nn.ReLU(), nn.MaxPool1d(2), ) self.lstm = nn.LSTM(input_size=16, hidden_size=32, num_layers=1, batch_first=True) self.classifier = nn.Linear(32, num_classes) def forward(self, x): x = self.cnn(x) # [B, 16, T] x = x.transpose(1, 2) # [B, T, 16] out, _ = self.lstm(x) # out [B, T, 32] out = out[:, -1, :] # 取最后时刻 return self.classifier(out)

5.2 从时域向时频域结合延伸

纯时域波形直接进模型是最简单的方式,但不是唯一方式。很多研究者会把原始信号先做短时傅里叶变换,转成二维时频谱图,然后用2D-CNN或者ResNet来处理。这样做的优势在于模型能看到频率随时间演化的模式,劣势在于时频谱的窗口、重叠参数又多了一组需要调的超参数,训练和部署的成本也上去了。

我个人更推荐一条折中的路:时域波形用1D-CNN算一组特征,包络谱用1D-CNN再算一组特征,两组特征拼起来送到分类器。这样既保留了波形里的瞬态冲击信息,又有频域能量的整体分布信息,而且对算力要求不高。很多实际项目里这种方法在"台上实验转现场"时表现优于单通道方案。

5.3 踩过几次坑之后我的建议

静下心来做这类任务,数据处理的时间往往比模型训练长得多,这是正常的。拿到一份新的数据,先不要急着丢进模型,第一件事是可视化——把每类信号的时域波形和频谱打出来看一眼,确认故障特征是不是真的存在、文件标签和类别对应关系是不是正确。一个标签贴反的数据集能把整个实验带偏,而可视化能快速暴露这类问题。

第二个建议是每次实验都固定随机种子,保证实验可复现。PyTorch这边设置torch.manual_seed(42),numpy那边也设置np.random.seed(42),再把DataLoader的shuffle顺序固定下来。否则同一份代码两次跑出的准确率波动超过2个点,很难判断是模型改动带来的效果,还是随机性在捣乱。

第三个是别一上来就追求最先进的结构。先把这个基础版本跑通,确认数据流程没有泄漏问题,再做扩展和调优。骨架搭对了,后面加LSTM、加注意力机制都是水到渠成的事;骨架不对,结构再花哨也白搭。

这套代码直接改改数据路径就能用,祝你在自己的轴承数据上也顺顺利利跑出一个漂亮的准确率。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询