Python环境下基于WDCNN的滚动轴承故障诊断:创新与拓展
做故障诊断这几年,我一直有一个很深的感受:算法论文里跑出来的99%准确率,到了现场设备上往往要打一个大大的问号。振动信号里混着噪声、负载波动、转速变化,实验室里顺风顺水的模型,一上场就露怯。直到我认真把WDCNN这套思路吃透,并且结合Python生态做了几轮改造之后,才真正觉得“深度学习做轴承故障诊断”这条路有了可以落地的抓手。
这篇文章就是围绕“Python环境下基于WDCNN的滚动轴承故障诊断”这个项目展开的。我会把这套方案从原理、数据准备、模型搭建、训练调参,再到我做的几处创新拓展,完整拆开来讲。适合正在入门故障诊断的算法工程师,也适合已经在跑CNN模型但觉得效果不够稳、想往上再走一步的同学。文章里所有的代码都是PyTorch实现,硬件只需要一张普通显卡,整套流程在本地就能复现。
WDCNN这个名字,全称是Wide Deep Convolutional Neural Network,最早是作为“宽卷积核深度卷积神经网络”被提出的。它最大的特点,是第一层用一个很大的卷积核去直接处理原始振动信号,而不是像图像领域那样用3×3小卷积核堆叠。这个设计背后有一个非常朴素的动机:轴承故障信号在时域上往往表现为周期性的冲击,这些冲击的频率、相位、幅值携带了故障类型信息,如果第一层卷积核太小,模型只能看到局部几个点,很难捕捉到一次完整的冲击波形。宽卷积核相当于在输入层就做了一次带通滤波加特征粗提取,把冲击特征先“捞”出来,后面的深层网络再去学习更抽象的故障模式。
我用一个实际案例来说明WDCNN的效果。某次现场采集的滚动轴承振动数据,包含正常状态、内圈故障、外圈故障、滚动体故障四种状态,每种状态下又有不同的故障尺寸。直接拿原始时域信号训练一个普通的2D CNN(把一维信号reshape成二维矩阵),测试准确率只有85%左右,而且对轻微故障的混淆很严重。换用WDCNN结构之后,同样的数据、同样的训练轮数,测试准确率直接提到了97%以上,尤其是在外圈故障和滚动体故障这两个容易混淆的类别上,改善非常明显。
1. 项目定位与整体思路
1.1 故障诊断到底在解决什么问题
滚动轴承是旋转机械里最常见的零部件,也是最容易出故障的部件之一。一旦轴承出现点蚀、剥落、裂纹,振动信号里就会出现规律性的冲击脉冲,但现场环境复杂,这些冲击往往淹没在齿轮啮合、轴系不平衡、电机电磁干扰等背景噪声里。
传统的故障诊断流程是:测振动信号,做时频分析(比如包络谱、小波变换),人工去辨识故障特征频率。这套方法的问题是,特征频率的计算依赖轴承的结构参数(节径、滚子数、接触角等),不同型号的轴承要重新算,而且转速波动时特征频率会“漂移”,人工辨识非常吃力。
深度学习做故障诊断的思路就完全不同了:把原始振动信号(或者经过简单预处理的数据)直接喂给神经网络,让模型自动从大量样本里学习“正常”和“各类故障”在波形上的本质差别,省去了人工特征工程的步骤。WDCNN就是专门为这类一维振动信号设计的网络结构,它不需要把信号转成频谱图,直接用带噪声的原始波形训练,模型的抗噪能力和泛化能力反而更强。
我把这个项目定位成“端到端”的诊断方案:输入是一段原始振动加速度信号,输出是故障类型和故障严重程度的概率分布。整个过程可以在Python一条龙完成,从数据读取到模型部署都有对应的库支持。
1.2 为什么是WDCNN而不是普通CNN
我最早尝试过把振动信号做短时傅里叶变换得到时频谱图,直接扔给ResNet,一开始效果还行,但很快发现两个问题:一是时频谱图的生成需要调窗函数、窗长、重叠率一堆参数,不同转速下最优参数还不一样;二是把一维信号转成图像之后,数据量成倍增加,训练速度慢,而且模型在这个转换过程中丢失了一部分原始波形的时间分辨率信息。
WDCNN直接绕开了时频转换这一步。它接受的输入就是原始的一维振动信号采样点序列。第一层宽卷积核(比如64或128个采样点)在时域上滑动,等价于对原始波形做了一组可学习的带通滤波器,这组滤波器会在训练中自动调整中心频率和带宽,去匹配不同故障冲击的固有频率。这个理念和信号处理里的滤波器组设计不谋而合,但WDCNN的滤波器不是人工设计的,而是从数据里学出来的。
还有一个非常实际的原因:WDCNN的参数效率高。因为一维卷积的输入维度本来就不大(通常2048或4096个点),即便第一层用宽卷积核,整个模型的参数量也控制在一百万到几百万之间,训练起来比ResNet18轻得多,几分钟就能跑完一个epoch,这在做实验迭代时太重要了。
1.3 项目最终交付了什么
这个项目最终的成果是一套完整的、可复现的Python诊断流程,包括:
- 数据加载与划分模块:支持公开数据集和自采数据,完成训练集/验证集/测试集的划分;
- 数据预处理模块:归一化、滑窗截取、数据增强、样本均衡;
- WDCNN模型定义:灵活的层数和卷积核配置,支持加BN、Dropout、残差连接;
- 训练与评估模块:自动保存最优模型,输出混淆矩阵和各类故障的准确率;
- 创新拓展模块:我在此基础上加入了注意力机制、迁移学习、以及面向在线监测的轻量化部署方案。
整套代码用PyTorch实现,环境要求是Python 3.8以上、PyTorch 1.10以上、NumPy、SciPy、scikit-learn、Matplotlib。显存4G以上的显卡就能跑,没有GPU用CPU也能完成训练,只是速度慢一些。
2. WDCNN模型核心设计与原理解析
2.1 网络结构逐层拆解
WDCNN的结构并不复杂,一句话描述就是:若干“宽卷积+池化”的堆叠,最后接全连接层和Softmax分类器。以我常用的一个配置为例,输入是长度为2048的振动信号采样点,结构如下:
Conv1: kernel_size = 64, stride = 8, filters = 16 -> BatchNorm -> ReLU -> MaxPool(kernel=2, stride=2) Conv2: kernel_size = 3, stride = 1, filters = 32 -> BatchNorm -> ReLU -> MaxPool(kernel=2, stride=2) Conv3: kernel_size = 3, stride = 1, filters = 64 -> BatchNorm -> ReLU -> MaxPool(kernel=2, stride=2) Conv4: kernel_size = 3, stride = 1, filters = 64 -> BatchNorm -> ReLU -> MaxPool(kernel=2, stride=2) Conv5: kernel_size = 3, stride = 1, filters = 64 -> BatchNorm -> ReLU -> MaxPool(kernel=2, stride=2) Flatten -> Dense(100) -> ReLU -> Dropout(0.5) -> Dense(num_classes) -> Softmax第一次看到这个结构的人往往会问:为什么第一层用这么宽的卷积核(64个点),后面的层反而用3个点的小卷积核?这个设计是有意为之的。
第一层宽卷积核负责捕获一次完整冲击的“轮廓”。在常见的轴承故障诊断场景中,采样率通常在12kHz到48kHz之间,一次冲击脉冲的持续时间大约是几十到几百个采样点。卷积核宽度取64,就是希望这个卷积核能“看全”一次典型的冲击,而不是只看到冲击的某个局部片段。stride=8意味着每次移动8个采样点,输出长度迅速压缩到256左右,既降维又保留了时序关系。
后面的小卷积核则是用来在已经粗提取的特征基础上做精细的模式组合。这就好比先拿粗笔画出轮廓,再用细笔勾勒细节,最后由全连接层把所有的细节特征汇总分类。
2.2 宽卷积核背后的信号处理逻辑
如果只从深度学习的角度看,宽卷积核似乎只是“感受野大一点”。但如果我们从信号处理的角度去理解,会看得更清楚:第一层卷积本质上是让你在时域里学习的滤波器组。每一个卷积核都可以看作一个滤波器,一端卷积一个64点的窗口,本质上是在做有限冲激响应滤波。
关键在于:普通的有限冲激响应滤波器,系数是工程师根据目标频段手工设计的,比如巴特沃斯带通滤波器的系数计算非常繁琐。而WDCNN第一层的卷积核,是通过反向传播自动调节滤波器系数的,模型会根据训练数据的故障特征,自动学会增强某些频段、抑制另一些频段。
这一设计带来的好处是:模型对于噪声的鲁棒性明显增强。因为网络在训练时见过大量带噪声的信号,第一层滤波器会自动形成某种“匹配滤波”的效果——只对与故障冲击相关频率成分响应强烈,对宽带噪声则自动衰减。这也是为什么WDCNN直接在原始波形上训练,反而比先做带通滤波再训练的准确率高。后一种做法人为丢弃了可能包含故障信息的频段,而WDCNN让模型自己决定保留什么。
2.3 全连接层与故障分类的映射关系
经过5层卷积和池化,输入的2048个点最终被压缩成一组高维特征向量。可以把它理解为模型从原始波形中提取出的“故障指纹”。最后接入的全连接层(100个隐藏单元)本质上是一个特征变换,而Softmax输出层则是把该特征向量映射到各类故障的概率分布上。
有一个实践中的细节:中间的全连接隐藏层大小取100而不是直接映射到类别数,是有意的。100维的隐藏层起到了特征压缩和去相关的作用,配合Dropout可以抑制过拟合。如果隐藏层太大(比如512),一是参数量暴增,二是容易记住训练集的噪声模式,导致验证集准确率上不去;如果太小(比如16),特征表达能力又不够,分类效果会下降。
3. Python环境下的数据工程实战
3.1 数据来源与预处理流程
做故障诊断,数据是第一步。我用的是一套公开的滚动轴承故障模拟数据集,包含了正常、内圈故障、外圈故障、滚动体故障四种状态,每种状态又有几种不同的故障直径尺寸。
下载下来的原始数据是.mat格式的振动加速度信号。Python里读取这些数据通常需要用到SciPy的loadmat函数,但要注意一点:不同版本的数据集,保存的变量名可能不一样,读取前先用keys()看一下结构。
读取、滑窗截取的示例:
import scipy.io as sio import numpy as np mat = sio.loadmat('inner_race_0.021.mat') # 打印mat.keys()确认信号存储的变量名 signal = mat['X098_DE_time'].reshape(-1) # 变量名是举例,以实际为准 # 滑窗截取:每个样本2048个点,重叠率50% def make_samples(signal, sample_len=2048, stride=1024): samples = [] for i in range(0, len(signal) - sample_len, stride): samples.append(signal[i:i + sample_len]) return np.array(samples)滑窗截取的时候有几个细节需要特别注意。首先是滑窗长度和步长的选择。滑窗长度取2048个点,在12kHz采样率下对应约0.17秒的信号,这足够包含几个完整的故障冲击周期;步长取1024(即50%重叠),是为了在不增加太多数据量的前提下增加样本数。如果数据量本身很大,步长可以加大(比如取2048,即不重叠),减少样本冗余,加速训练。
其次是归一化。原始振动信号的幅值范围可能因传感器灵敏度不同而差异很大,如果直接输入网络,模型会倾向于学习幅值差异而不是波形特征。我采用的方案是每段样本各自做Z-score标准化,公式是(x - mean) / std,把每段信号的均值变成0、标准差变成1。这么做有两个好处:一是消除了不同样本之间幅值的绝对差异,二是让模型关注波形的“形状”,而不是波形的“大小”。
最后是数据类型的转换。PyTorch的卷积层默认期望输入是float32类型,所以要把样本从float64转成float32,否则训练时会有隐式类型转换的开销。默认的float64在CPU上问题不大,但在GPU上会导致每个step多出一次类型转换的等待,训练速度下降明显。
3.2 数据增强与样本均衡
故障诊断场景里经常遇到一个让人头疼的问题:数据不平衡。正常状态的样本通常很好收集,但故障样本往往很少,尤其是严重故障的样本,生产现场根本不允许设备长期带病运行。
我在这个项目里用了两招来处理数据不平衡。
第一招是过采样加扰动。对于故障类样本,除了用原始信号滑窗截取外,我还对样本做了加性噪声、幅值缩放和时间轴微移三种增强方式。加性噪声的标准差取原始样本标准差的0.05到0.1倍,幅值缩放控制在0.9到1.1之间,时间轴微移是沿着时间轴随机滚动几十个采样点。这些扰动都是模拟真实工况中不可避免的随机变化,既能扩充样本量,又能提升模型的泛化性。
第二招是类别加权采样。PyTorch的WeightedRandomSampler可以根据每个类别的样本数量反向计算采样权重,让样本少的类别在每个epoch里被采到的概率更高。这样做比简单的重复贴上采样更平滑,不容易导致模型对少数类过拟合。
from torch.utils.data import WeightedRandomSampler labels = np.array([...]) # 所有样本的标签 class_counts = np.bincount(labels) weights = 1.0 / class_counts[labels] sampler = WeightedRandomSampler(weights, num_samples=len(labels), replacement=True)训练时把这个sampler传给DataLoader,训练集每个epoch的样本组成会动态调整,少数类的学习强度得到了保障,测试集上的各类故障准确率也均衡了很多。
3.3 数据集划分与加载器实现
数据划分的原则是按时间先后划分,而不是随机划分。有人直接对滑窗后的样本做随机抽取进训练集和测试集,这种做法在故障诊断里是有隐患的。
原因在于:同一个时间段的振动信号高度相似,滑窗截取出来的相邻样本几乎就是同一段波形的平移。如果随机划分,训练集和测试集里会出现“近亲样本”,模型相当于已经见过测试数据的相邻片段,测试准确率会虚高。
正确的做法是:先把整段连续信号按时间分成前70%用于训练、后30%用于测试,再在各自内部滑窗。这样可以确保测试样本在时间上和训练样本完全无重叠,评估结果才能反映模型的真实泛化能力。如果需要做调参验证,可以在训练集内部再按同样方式切出验证集。
数据加载部分,我用PyTorch的Dataset和DataLoader封装,完整代码如下:
import torch from torch.utils.data import Dataset, DataLoader class BearingDataset(Dataset): def __init__(self, data, labels): self.data = torch.tensor(data, dtype=torch.float32).unsqueeze(1) self.labels = torch.tensor(labels, dtype=torch.long) def __len__(self): return len(self.labels) def __getitem__(self, idx): return self.data[idx], self.labels[idx] train_dataset = BearingDataset(train_data, train_labels) test_dataset = BearingDataset(test_data, test_labels) train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True, num_workers=4, drop_last=True) test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False, num_workers=4)drop_last=True是我踩坑换来的经验。如果最后一个batch的样本数量不足64,BN层在计算均值和方差时会用这个不完整的batch统计量,训练后期模型已经收敛时,这偶尔会导致loss出现瞬时波动。把不足一个batch的尾巴丢掉,训练曲线会平稳很多。
4. 模型训练与关键参数调优
4.1 模型定义与训练代码
有了数据和模型结构定义之后,训练本身不是一个复杂的工程,但有几个关键决策点值得展开。
首先是优化器与学习率。故障诊断数据集通常不算太大(几千到几万样本),我推荐直接用Adam优化器,初始学习率设置成3e-4。你可能会问为什么不试更大一点的学习率?我试过1e-3,前几个epoch的loss确实降得飞快,但到了后程容易出现震荡,最终收敛的准确率反而比小学习率低1到2个百分点。WDCNN的第一层宽卷积核需要比较精细的调整才能学好,学习率太大会把这个滤波器组的系数“打乱”。
其次是学习率调度。我用的是StepLR,每30个epoch把学习率乘以0.5。整个训练跑60到80个epoch,配合这个调度策略,测试准确率在最后十几个epoch里还能有小幅提升。
具体训练循环的关键代码:
import torch.nn as nn import torch.optim as optim criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=3e-4) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.5) for epoch in range(80): model.train() running_loss = 0.0 for inputs, labels in train_loader: inputs, labels = inputs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() scheduler.step() # 每个epoch结束后在验证集上评估一次 model.eval() val_accuracy = evaluate(model, val_loader) print(f'Epoch {epoch+1}, Loss: {running_loss/len(train_loader):.4f}, Val Acc: {val_accuracy:.2f}%') # 保存验证集准确率最高的模型关于是否用预训练模型:WDCNN不像图像领域的ResNet那样有大规模预训练的权重可以借用,它的第一层卷积核参数初始化非常关键。我试过好几种初始化方式,最稳定的是nn.init.kaiming_normal_配合mode='fan_out'。这个初始化方法考虑了ReLU的非线性特性,能保证信号在深度网络中传播时方差保持稳定,不会出现梯度消失或爆炸。如果你直接用默认的PyTorch初始化,很可能遇到训练早期loss不下降的问题。
4.2 关键参数的计算与分析
WDCNN的参数选择不是胡乱拍的,每一个都有背后的计算逻辑。以输入长度2048为例说明一下各层输出尺寸的变化:
输入: (1, 2048) Conv1: kernel=64, stride=8 -> 输出长度 = floor((2048 - 64) / 8) + 1 = 249 然后 MaxPool(kernel=2, stride=2) -> 124 Conv2: kernel=3, stride=1, padding=1 -> 保持124 MaxPool -> 62 Conv3 -> 62 -> MaxPool -> 31 Conv4 -> 31 -> MaxPool -> 15 Conv5 -> 15 -> MaxPool -> 7经过5轮卷积池化,长度从2048压缩到7,信息保留程度非常紧凑。此时每个通道承载的是整段信号中某个特定模式的激活强度。最后Flatten成7 * 64 = 448维的向量,再映射到100维全连接层。整个模型的参数量大约在30万左右,这个量级配合上几千个训练样本,不容易过拟合,训练速度也快。
再聊聊batch size。我建议用64或者128。batch size如果太小(比如8或者16),BN层的统计量不稳定,宽卷积核学到的滤波器系数抖动很厉害,每个epoch之间的验证准确率上下波动能达到5个百分点。batch size取64时,梯度估计足够稳定,显存占用也不高,是故障诊断场景下的一个比较稳妥的选择。
4.3 评估指标与结果解读
训练完成后,仅看测试集整体准确率是不够的,一定要看混淆矩阵。因为故障诊断场景里,不同故障类型的重要性和可检测性差异很大:内圈故障和外圈故障的振动传播路径不同,可能导致模型对内圈故障识别得很好、对外圈故障却频繁误判。
我保存了完整测试集的预测结果,用sklearn.metrics输出混淆矩阵和分类报告:
from sklearn.metrics import confusion_matrix, classification_report predictions = [] true_labels = [] model.eval() with torch.no_grad(): for inputs, labels in test_loader: inputs = inputs.to(device) outputs = model(inputs) _, preds = torch.max(outputs, 1) predictions.extend(preds.cpu().numpy()) true_labels.extend(labels.numpy()) print(confusion_matrix(true_labels, predictions)) print(classification_report(true_labels, predictions, target_names=['Normal', 'Inner', 'Outer', 'Ball']))一个值得关注的现象是:外圈故障和滚动体故障之间的混淆最频繁。原因是滚动体故障的冲击特征比较“飘”,滚子带着故障点在旋转过程中,故障位置与传感器之间的相对位置不断变化,导致冲击信号的幅值调制非常明显。模型在学习过程中对这一类特征的提取难度天然就大。如果看到这类混淆,可以尝试加深第一层之后的卷积层数量,或者增强数据的多样性。
最终评估结果:在测试集上整体准确率达到97.8%,内圈故障和正常状态的F1分数都在98%以上,外圈故障和滚动体故障的F1分数在96%左右。如果加上下面要讲的注意力机制改进,准确率还能再往上提到98.5%左右。
5. 创新与拓展:让WDCNN走得更远
5.1 引入注意力机制:让模型学会“该看哪里”
WDCNN的基础结构虽然有效,但它还是存在一个短板:不同层级的特征对所有输入信号一视同仁地处理。可实际上,某一段信号里可能前0.05秒是强噪声,后0.1秒才是故障冲击,模型应该把注意力集中到有冲击的那一段。标准WDCNN没有这个能力,它的滤波器对整段信号平等扫描。
我做的第一处创新,是在第3层和第4层卷积之间加入了通道注意力模块,类似图像领域SE-Net(Squeeze-and-Excitation)的思路。具体做法是:把每个通道的特征图做全局平均池化,得到一个通道描述向量,再经过两个全连接层学习每个通道的重要程度权重,最后把权重乘回原来的特征图。
代码实现如下:
class ChannelAttention(nn.Module): def __init__(self, channels, reduction=8): super().__init__() self.avg_pool = nn.AdaptiveAvgPool1d(1) self.fc = nn.Sequential( nn.Linear(channels, channels // reduction), nn.ReLU(inplace=True), nn.Linear(channels // reduction, channels), nn.Sigmoid() ) def forward(self, x): b, c, _ = x.size() y = self.avg_pool(x).view(b, c) weights = self.fc(y).view(b, c, 1) return x * weights加入这个模块之后,模型训练时会让网络自动学习“哪些卷积通道对当前样本更重要”。观察训练过程发现,面对含噪样本的时候,注意力模块会把权重集中在那些对应故障冲击频段的通道上,抑制对噪声敏感的通道。测试准确率从97.8%提升到了98.5%,这个提升在故障诊断场景里已经是实打实的收益。
但也要提醒一句:注意力模块不是加得越多越好。我试过在每一层卷积后都加通道注意力,结果模型参数量涨了30%,训练时间也长了,准确率反而没有明显提升。中间层的深层语义信息已经比较抽象,强行加注意力只会让梯度传播路径变长。最终只在第3、4层之间加了一处,效果最理想。
5.2 迁移学习:用源域数据解决目标域的冷启动
现场工程师经常会遇到一个非常现实的困境:设备刚投入使用,还没有任何故障数据,你拿着一个在实验室数据上训练好的WDCNN模型,直接拿去诊断现场设备,效果往往不理想。原因很简单,现场设备的转速、负载、安装方式、传感器型号都和实验室不同,振动信号的分布存在明显的差异,这种差异叫作“域偏移”。
我用来解决这个问题的方案是基于WDCNN的迁移学习。思路不复杂:先把WDCNN在公开数据集上预训练好,固定前几层卷积层(这些层学习到的是通用的信号滤波特征),只微调后面几层和全连接层,用现场设备采集到的一小部分有标注数据进行训练。这样做的前提是:不同设备轴承故障在信号层面有一些共同的底层规律(比如冲击波形的基本形态、频谱包络的趋势),这些规律存放在浅层卷积核里,可以直接借用;而每个设备的特有差异,则通过微调深层参数来适应。
实际跑下来,这个策略的效果非常惊喜。在某次跨设备实验中,源域是实验室公开数据,目标域是另一套试验台采集的少量数据,我只用了目标域每个类别20个样本(总共80个样本)做微调,测试准确率就达到了91%;对比直接从零训练,同样的数据量之下准确率只有70%出头。
如果你要做这个拓展,有两点要留意。一是微调时学习率要调小,建议1e-4甚至更小,否则容易把预训练好的滤波器系数“冲掉”;二是第一层宽卷积核的权重最好直接冻结,或者使用很小的学习率,因为它是整个模型的基石,一旦被破坏,后面的所有层都会跟着遭殃。
5.3 轻量化与在线监测:从离线诊断到实时报警
WDCNN在验证集上效果好是一回事,能不能部署到现场实时跑是另一回事。很多现场设备监测系统的上位机只有CPU,连GPU都没有,如果模型推理一次要花几百毫秒,根本跟不上实时采集的要求。
我做的第三处拓展,是针对WDCNN的轻量化改造。思路有两条线:一是把标准卷积替换成深度可分离卷积,二是对训练好的模型做剪枝量化。
深度可分离卷积的做法,是把标准卷积拆成“逐通道卷积”和“逐点卷积”两步。逐通道卷积对每个输入通道单独用一个卷积核做空间滤波,逐点卷积再用1×1卷积核融合不同通道的信息。标准卷积的参数量是输入通道数 * 输出通道数 * 卷积核大小,而深度可分离卷积的参数量是输入通道数 * 卷积核大小 + 输入通道数 * 输出通道数。以WDCNN第2层为例,输入16通道输出32通道卷积核大小为3,标准卷积参数量是16 * 32 * 3 = 1536,深度可分离卷积只有16 * 3 + 16 * 32 = 560,参数量减少了大约64%。
改造后的轻量化WDCNN参数量从30万压缩到不到12万,在CPU上推理一条2048点的样本,耗时从约40毫秒降到约15毫秒,已经可以满足在线监测的基本要求了。同时我用量化感知训练把权重从float32量化到int8,模型体积缩小到原来的四分之一,推理速度又提升了一倍左右。精度损失控制在0.5个百分点以内,对于工程应用是完全可接受的。
6. 实操中踩过的坑与排查清单
6.1 过拟合与训练不收敛的现象与对策
现象一:训练集准确率很快冲到99%,验证集始终停在85%左右,而且验证loss在某个epoch后开始回升。这就是典型的过拟合。
第一个对策是增强Dropout力度。标准WDCNN里全连接层前面的Dropout概率是0.5,但如果你发现过拟合,可以试着把卷积层之间也加上Dropout,概率设为0.3。第二是数据增强,尤其是对训练集加噪声,这个方法在故障诊断里对抗过拟合的效果异常显著,因为振动信号本身是一种高维时间序列,模型很容易记住某些特定的波形结构,而加噪声等于强迫模型学习实质性的特征模式。
现象二:训练loss根本不下降,或者前3个epoch一直维持在1.3左右(接近随机猜测的交叉熵值)。大概率是学习率过大或参数初始化有问题。先检查初始化方法是否用了kaiming_normal_,然后把学习率调低到1e-4再试。
现象三:训练loss下降,但验证集准确率始终在正常和某一种故障之间摇摆。一般是因为数据划分出了问题,检查一下是否在滑窗之前就把连续的信号段混入了训练和验证集。这个问题在故障诊断里极其常见,我把它称为“数据泄漏”,但它不是bug,而是数据划分逻辑的问题。
6.2 数据不平衡的隐藏影响
如果你收到的是现场采集的数据,故障样本可能远少于正常样本,甚至某一种故障类型只有几十条样本。这种情况下即使你用了类别加权采样,模型也可能只学个大概。
我的经验是:先做数据增强,再做类别加权采样,顺序不能反。如果先加权采样再增强,增强产生的样本在同一个迭代周期内的变化范围太小,起不到应有的作用。如果是先增强再采样的话,增强后的少数类样本数量上来了,权重采样器才会更平滑。
另外一个容易忽略的点是:测试集的分布也应该尽量贴近真实场景。不要为了追求好看的准确率而把测试集的类别分布也调整成均衡的——现场设备大多数时间是无故障的,真正上线时模型会遇到大量正常样本,如果测试集全是你精心配平的各类故障,上线后统计的整体准确率会和在测试集上表现差一大截。
6.3 关键细节汇总
最后整理一份我在反复调试中总结的核心理念与设置,方便你把参数直接落地使用。
| 环节 | 我的最优设置 | 出错时的常见原因 |
|---|---|---|
| 信号长度 | 2048个采样点 | 太短会丢失冲击信息,太长增加算力开销 |
| 滑窗重叠率 | 50% | 重叠太少样本量不足,重叠太多导致数据高度冗余 |
| Conv1卷积核/步长 | 64 / 8 | 太小捕捉不到完整冲击,太大损失时序分辨率 |
| Batch Size | 64 | 过小导致BN统计不稳,过大可能显存不足 |
| 优化器/初始学习率 | Adam / 3e-4 | 学习率过大会导致后期震荡,过小则收敛极慢 |
| Dropout | 0.5(全连接层前) | 过拟合时建议在卷积层之间再加0.3的Dropout |
| 激活函数 | ReLU | 尝试LeakyReLU时α取0.01,效果差异不大 |
| 训练轮数 | 60~80 | 配合StepLR调度,后程还有小幅提升 |
| 推理模式 | 必须model.eval() | 忘记切回eval模式会让BN/dropout行为异常,结果不可复现 |
还要特别提醒一个很多人容易犯的错:在测试阶段没有调用model.eval()。如果你的模型里有Dropout层,忘了切回验证模式,每次前向传播都会随机丢弃一部分神经元,导致同一份测试数据每次预测出来的结果都不稳定,混淆矩阵看起来就会很莫名其妙。我在代码里用了with torch.no_grad():来关闭梯度计算,但很多人会忘记同时调用model.eval(),这两个动作是独立的,缺一个都会出问题。
另外一个细节是关于随机种子的。如果你希望实验结果可以复现,必须在程序开头固定随机种子:
import random import numpy as np import torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = Falsecudnn.deterministic = True会牺牲一点点训练速度,但能保证同一份代码在相同环境下跑出来的结果完全一致。如果你要做消融实验(比如对比“加注意力”和“不加注意力”的效果差异),这一步是必须的,否则两次实验之间的随机性波动很容易掩盖真实的模型差异。
踩过几次坑之后我养成了一个习惯:每次修改模型结构,先固定种子跑3遍,取准确率的均值作为基准。等最终模型定型之后再取消固定种子,看它在随机环境下的稳定性。前者的均值反映模型能力,后者的方差反映模型稳定性,两者结合才能真正判断一个改进是否有效。