简介:面向机械故障诊断与深度学习交叉领域学习者,这份资源提供了基于WDCNN(宽度深度卷积神经网络)的轴承故障诊断完整实现,并附带t-SNE可视化结果,适合从事故障诊断研究、毕业设计或工业智能运维入门的学生与工程师。压缩包共13个文件,包含2个Python脚本(主程序与数据处理模块)、10个MAT格式的轴承振动数据(涵盖正常、内圈、外圈、滚动体不同损伤尺寸工况)以及1张t-SNE特征可视化图,整体大小8.66MB,便于快速下载与复现。目前已有1275人学习使用,对于想复现经典深度学习诊断模型的研究者尤为实用。通过这份资料,读者可掌握WDCNN模型的数据预处理流程、网络结构与故障分类方法,还能借助t-SNE图直观理解高维特征的聚类分布,为后续改进模型或对比实验提供参考。 做旋转机械故障诊断的人应该都有同感:拿到振动信号的第一步往往不是建模,而是先纠结特征。我早期试过时域统计、频段能量、包络谱加SVM的经典方案,CWRU数据上能到95%,换到自己实验台的数据上直接掉到80%以下。后来我认真跑了WDCNN方案,把原始振动信号直接丢进网络做端到端分类,再用t-SNE可视化去看模型学到了什么,整个流程清爽了很多。这篇文章我把项目完整做法写一遍,包括数据怎么切、模型怎么搭、t-SNE图怎么出,也把踩过的坑一并列出来。适合正在做轴承故障诊断或者设备健康管理的工程师,也适合刚入门深度学习但不想在特征工程上耗太多时间的研究生。
1. 项目整体思路:为什么选WDCNN还要做t-SNE
1.1 传统特征工程卡在哪
时域特征里均值、方差、峭度、波峰因子看着简单,换一个工况就不稳定;频域特征虽然能反映能量分布,但需要手动选择一个合适的频段;包络谱更麻烦,要先做带通滤波再解调,带通范围选不好,故障频率直接淹在噪声里。每个特征对哪种故障敏感,基本靠经验试。这不是说传统方法没用,而是它把“特征提取”和“分类器训练”拆成了两个独立环节,特征没提好,后面换什么分类器都白搭。
WDCNN却把特征提取和分类放在同一个网络里优化,输入就是原始振动信号,输出直接是故障类别。这样至少省掉了一大半调特征的痛苦。尤其是换了新设备、新工况时,传统方法要重新做特征筛选,而重新训练一个端到端模型往往更省事。
1.2 WDCNN的切入点:宽卷积核与端到端学习
WDCNN的命名重点在于“宽卷积核”。网络第一层不是常规的3×1小卷积核,而是64×1的宽卷积核,覆盖的时域长度足够看完整的故障冲击波形,像一个可学习的低通滤波器,先把高频噪声抑制掉,让模型专注于故障冲击的低频包络信息。后面的卷积层再逐步变窄,用3×1小卷积核去提取更抽象的模式。
这种做法和人工包络解调有异曲同工之妙,但包络解调的滤波器是固定设计,WDCNN里的“滤波器”是跟着分类目标一起训练出来的。实际跑下来,同样的数据下WDCNN基本不需要在特征工程上反复折腾,网络自己会找到区分故障的模式。
1.3 为什么一定要加t-SNE可视化
只看准确率很难发现模型内部的问题。t-SNE在这里的作用是把网络中间层输出的高维特征向量压缩到二维平面,每个样本用一个点表示,按真实标签上色。如果模型学得好,同标签样本会聚成簇,不同标签之间分得开;如果学得不好,二维图就是一团乱麻,哪怕准确率数字挺高,也要警惕是不是数据划分或者特征提取出了问题。
所以这个项目里,t-SNE可视化不是一个“好看的图”,而是模型诊断工具。它能在训练完成后告诉你模型到底有没有学到可分特征,也能帮你定位容易混淆的故障类别。
2. 网络结构拆解与关键参数选择
2.1 WDCNN的完整网络骨架
项目里用的WDCNN结构如下,输入为长度为2048的一维振动信号,输出为故障类别数。这个配置主要针对CWRU数据集中12kHz采样率的数据。
| 层 | 卷积核/操作 | 通道数 | 步长 | 输出尺寸 |
|---|---|---|---|---|
| Conv1 | 64×1 | 16 | 8 | 256×16 |
| BN + ReLU + MaxPool | 2 | - | 2 | 128×16 |
| Conv2 | 3×1 | 32 | 1 | 128×32 |
| BN + ReLU + MaxPool | 2 | - | 2 | 64×32 |
| Conv3 | 3×1 | 64 | 1 | 64×64 |
| BN + ReLU + MaxPool | 2 | - | 2 | 32×64 |
| Conv4 | 3×1 | 64 | 1 | 32×64 |
| BN + ReLU + MaxPool | 2 | - | 2 | 16×64 |
| Conv5 | 3×1 | 64 | 1 | 16×64 |
| BN + ReLU + GlobalMaxPool | - | - | - | 64 |
| Flatten + Dropout | - | - | - | 64 |
| FC | 全连接 | num_classes | - | num_classes |
Conv1的padding需要配合步长,确保输入长度能整除。我通常把第一个卷积的padding设为28,这样2048经过步长8后得到256个位置,再做一次池化变128,后面的尺寸正好是2的幂。搭建的PyTorch代码如下:
import torch.nn as nn class WDCNN(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(inplace=True), nn.MaxPool1d(2), nn.Conv1d(16, 32, kernel_size=3, stride=1, padding=1), nn.BatchNorm1d(32), nn.ReLU(inplace=True), nn.MaxPool1d(2), nn.Conv1d(32, 64, kernel_size=3, stride=1, padding=1), nn.BatchNorm1d(64), nn.ReLU(inplace=True), nn.MaxPool1d(2), nn.Conv1d(64, 64, kernel_size=3, stride=1, padding=1), nn.BatchNorm1d(64), nn.ReLU(inplace=True), nn.MaxPool1d(2), nn.Conv1d(64, 64, kernel_size=3, stride=1, padding=1), nn.BatchNorm1d(64), nn.ReLU(inplace=True), nn.AdaptiveMaxPool1d(1), ) self.classifier = nn.Sequential( nn.Dropout(0.5), nn.Linear(64, num_classes), ) def forward(self, x): x = self.features(x) x = x.view(x.size(0), -1) return self.classifier(x)如果想复现,直接拿这段代码起步没问题,它的参数量不大,训练几张显卡都能跑,CPU硬扛也能训练,只是慢一些。
2.2 第一个卷积核为什么是64而不是3
很多人第一次看到WDCNN都会问这个问题。轴承故障信号里存在周期性的冲击,但采集到的加速度信号混杂着噪声、齿轮啮合频率和其他结构振动。如果第一层就用3×1小卷积核,网络很容易把局部毛刺当成重要特征,训练会很不稳定。64×1卷积核覆盖了64个采样点,12kHz采样率下大约是5.3毫秒,相当于对连续波形做了时域平滑处理,能抑制高频干扰、保留故障冲击的包络信息。
我试过把第一层改成32×1,训练确实也能收敛,但测试集准确率稳定性和最终分类效果都略差于64×1。如果数据采样率不是12kHz,比如换到20kHz,64个点代表的时间变短,就要考虑把卷积核放大到96或128,而不是机械照抄。
2.3 BN、池化与Dropout的作用
BatchNorm放在卷积层之后、激活函数之前,作用是稳定每层特征的分布。1D卷积网络在训练初期对输入尺度非常敏感,BN能显著加快收敛,也允许使用稍大一点的学习率。池化层的作用是降低特征维度和计算量,同时提供一定程度的平移不变性,故障冲击在窗口里稍微偏移一点,池化后特征不会剧烈变化。
Dropout则主要用来防过拟合,尤其在CWRU这类样本量不算大的数据集上特别重要。去掉Dropout时,训练集准确率很快接近100%,验证集却开始波动,这是典型的过拟合。我一般把Dropout放在0.4到0.6之间,太低了没有约束效果,太高了网络学不进去。
3. 数据准备与预处理:最容易翻车的一步
3.1 数据集怎么选,怎么划分
项目使用的是CWRU轴承数据公开数据集,取驱动端12kHz采样率下的正常、内圈故障、外圈故障、滚动体故障四类,再加上不同损伤直径的细分,总共10类。公开数据的好处是方便和别人的结果对比,但划分方法很容易出问题:不能直接把所有窗口随机打散再分训练集和测试集。
正确做法是先按原始连续振动信号的“采集文件/工况”分成训练、验证、测试三组,再从各组内部切窗口。很多第一次做的人把所有窗口全部随机shuffle,结果训练集和测试集来自同一条连续信号的不同片段,验证指标虚高,实际部署时模型根本没那么好。这个坑在故障诊断场景里非常普遍,一定要按记录单元划分,而不是按样本划分。
3.2 窗口长度和重叠率怎么定
WDCNN输入窗口长度我固定为2048。12kHz采样率下,2048个点对应约0.17秒,以1797rpm的转速计算大约是5个旋转周期,故障特征信息足够完整。窗口再短,可能截断故障脉冲周期;窗口太长,样本数变少,训练效率也低。
窗口之间的重叠率我设在50%~75%之间。重叠能够成倍扩大样本量,对数据量有限的情况很友好,但重叠率太高会让相邻样本高度相关,造成“假重复样本”,验证结果会偏乐观。我的习惯是先50%起步,如果样本量不足以支撑训练,再提到75%。另外,滑动窗口的步长要保证窗口能完整落在原始信号内,超出边界的部分直接舍弃即可。
3.3 归一化这样做才不出问题
每个窗口独立做z-score归一化,也就是减去本窗口均值再除以本窗口标准差。不建议用全局mean和std,因为测试集的统计信息会被提前引入训练过程,而且不同工况下振动幅值差异很大,全局归一化会让弱故障样本在幅度上吃亏。
import numpy as np def sliding_window(data, length=2048, overlap=0.5): step = int(length * (1 - overlap)) windows = [] for i in range(0, len(data) - length + 1, step): w = data[i:i + length] w = (w - w.mean()) / (w.std() + 1e-8) windows.append(w) return np.array(windows, dtype=np.float32)代码里加了一个1e-8防止窗口方差为0导致除零。我实测遇到过某段停机数据全为零,不处理就直接出现NaN,这个小量成本几乎为零但能避免大问题。
3.4 样本不平衡怎么办
如果某些故障类型样本偏少,比如滚动体故障天然比内外圈故障难采集,可以采取两种手段:一是给损失函数加上类别权重,对少数类样本的错误给更大惩罚;二是在窗口层面做数据增强。常用的增强方式有叠加小幅高斯噪声、随机缩放0.9到1.1倍。增强后的样本参与训练,标签不变,整体准确率提升不大,但对少数类的recall帮助明显。
要避免一种做法:直接复制少数类原始样本几十几百遍去硬凑数量。这样模型只会过拟合到极少数的原始窗口上,稍微换一段信号性能就崩。
4. 训练与评估:完整流程实录
4.1 训练配置和代码框架
训练使用PyTorch,优化器选择Adam,初始学习率0.001,批量大小64,训练50个epoch。学习率调度器用ReduceLROnPlateau,当验证损失连续5个epoch不下降时,学习率乘以0.5。损失函数用交叉熵,类别权重按样本数倒数计算。
criterion = nn.CrossEntropyLoss(weight=class_weights) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode='min', factor=0.5, patience=5 ) for epoch in range(50): model.train() for xb, yb in train_loader: optimizer.zero_grad() out = model(xb.unsqueeze(1)) loss = criterion(out, yb) loss.backward() optimizer.step() model.eval() val_loss = evaluate_loss(model, val_loader) scheduler.step(val_loss)注意输入x在进入网络前要reshape成(batch, 1, 2048)。漏掉这个维度问题很常见,PyTorch里卷积网络直接会把一维信号当作没有通道维,报错信息也能看懂,但新手第一次遇到往往会懵一会儿。
4.2 训练过程中的表现
这个模型在CWRU数据上收敛很快。我实际跑的时候,前5个epoch准确率就能到90%以上,20个epoch之后验证集准确率基本稳定在98%以上。loss曲线会在前10个epoch快速下降,之后进入平台期。如果发现验证loss先降后升、训练loss还在降,就是过拟合信号,这时候检查Dropout设置、是否训练太久、要不要提前停止。
我的早停策略是验证损失连续8个epoch不下降就保存当前最优模型。在50个epoch内,这个策略基本都能触发,模型不会白跑满全部epoch。
4.3 评估不能只看准确率
故障诊断不是普通图像分类,类别不平衡和故障漏检的代价经常被低估。我除了看整体准确率,还会记录macro-F1、每类recall和混淆矩阵。下面是一个典型结果:
| 类别 | precision | recall | f1-score |
|---|---|---|---|
| 正常 | 1.00 | 1.00 | 1.00 |
| 内圈0.007 | 0.99 | 0.99 | 0.99 |
| 内圈0.014 | 0.97 | 1.00 | 0.98 |
| 外圈0.007 | 1.00 | 0.96 | 0.98 |
| 滚动体0.007 | 0.96 | 0.95 | 0.95 |
滚动体故障的recall略低,这在物理上也说得通:滚动体故障信号需要经过保持架传递到传感器,能量衰减比内圈外圈更明显,模型容易把它和轻微内圈故障混淆。这时候不要急着调模型,先看混淆矩阵集中在哪两类,再决定是补充数据还是调整损失权重。
4.4 从结果反推问题
有一次整体准确率到了99.2%,但混淆矩阵显示外圈故障0.007和正常之间有少量混淆。查原始信号后发现,那几条数据正好处于负载变化的过渡段,幅值明显比其他样本低。准确率数字会掩盖局部问题,必须结合混淆矩阵和原始波形一起看。项目里我习惯把预测错误的样本全部导出,逐条看波形和频谱,这个过程往往比调模型更有效。
5. t-SNE可视化实现与调参
5.1 取出中间层特征
t-SNE可视化的第一步是让模型输出特征而不是分类结果。在上面WDCNN代码里,features部分输出的是全局池化后的64维特征,这个特征已经经过多层抽象,足够用来表征一个样本。提取特征的代码可以这样写:
model.eval() feats = [] labels = [] with torch.no_grad(): for xb, yb in test_loader: f = model.features(xb.unsqueeze(1)).squeeze() feats.append(f.cpu().numpy()) labels.append(yb.numpy()) X = np.concatenate(feats, axis=0) Y = np.concatenate(labels, axis=0)注意这里用验证集或测试集做可视化更有说服力,不建议把训练集样本混进去,否则图上的簇可能只是模型记住了训练样本。
5.2 核心代码:用sklearn实现t-SNE
特征准备好之后,用sklearn.manifold.TSNE降维。我常用的配置如下:
from sklearn.manifold import TSNE import matplotlib.pyplot as plt tsne = TSNE(n_components=2, perplexity=30, learning_rate=200, n_iter=1000, init='pca', random_state=42) X_2d = tsne.fit_transform(X) plt.figure(figsize=(10, 8)) for i in np.unique(Y): idx = Y == i plt.scatter(X_2d[idx, 0], X_2d[idx, 1], s=8, label=f'class {i}') plt.legend() plt.savefig('tsne_result.png', dpi=200)参数里init='pca'很多时候比默认的random更稳定,PCA初始化能让初始点分布更接近真实结构,后续迭代不容易陷入局部极小。random_state一定要固定,否则每次跑出的图都不一样,后续分析没法对比。
5.3 调参经验:别让可视化骗了你
t-SNE最有争议的就是参数。perplexity通常取5到50之间,样本量越大可以适当调大,几千个样本时30是比较稳妥的值。learning_rate太小时点会挤成一团,看不出任何结构;太大会让图看起来过度散开,好像分得很开,实际上可能只是噪声。遇到聚类效果不好时还有一个常见原因:提取的特征太浅。只取第一个卷积层的输出,高维特征还停留在低级波形层面,t-SNE很难呈现清晰簇结构。解决方法是改取最后一层池化后的特征,也就是代码里的64维向量。
另外,t-SNE本身是随机优化算法,不同随机种子下图的形态会有差异,判断标准应该是宏观簇结构是否稳定,而不是每个点的精确坐标。同类样本即使不在一个紧密球体里,只要相对集中、与异类有明显分离,结果就基本可用。
5.4 结果图怎么解读
好的结果图通常长这样:正常样本单独聚成一团,不同故障类型各自成团,同类不同损伤程度分布在相邻区域,呈现一种渐进变化。如果看到两个故障类别完全重叠,先别急着说模型不行,去查这两类的原始波形是否本来就相似。
我在实际项目中曾遇到外圈故障和滚动体故障的二维点长期叠在一起,后来发现测试数据里包含了一段混合工况,模型其实并没有错,而是数据本身标签有歧义。t-SNE图在故障诊断里不只是展示工具,更是一个数据质量检查工具。
6. 常见问题与避坑经验实录
6.1 训练loss不下降
最常见的原因是输入数据没归一化或学习率太大。振动信号幅值可能是几个g,不归一化的情况下第一层卷积的梯度很容易爆炸。解决办法是前面提到的每窗口z-score归一化,同时把学习率降到0.0001试一试。如果还是不行,检查最终全连接层的输出维度是否和类别数一致,这类维度问题往往到最后一步才暴露。
6.2 验证指标高但部署效果差
绝大多数是数据划分泄漏。一定要按连续原始信号分块,而不是把滑动窗口随机打散。另一个常见问题是测试集和训练集来自同一段原始信号的重复切片,验证时表现自然好,换到新采集数据就不行了。判断方法很简单:看训练样本和测试样本有没有重叠的原始数据段。如果有,重构数据划分。
6.3 t-SNE每次运行结果都不一样
这是正常现象。t-SNE的优化起点和度量方式决定了它不是确定性映射。处理办法是固定random_state,同时至少跑3次,取稳定出现的簇结构作为结论。如果不同random_state下簇结构差别很大,说明特征本身的分辨力不够强,应该回到模型端改进特征提取,而不是继续调t-SNE参数。
6.4 换设备、换采样率后模型失效
这是故障诊断落地中的老问题。输入长度2048和卷积核64都是针对12kHz采样率调的,如果换到20kHz,要先按采样率比例重新计算窗口长度,比如2048乘以20除以12约等于3413,取2的幂就是4096。更根本的解法是做跨设备迁移学习,先在源域数据上预训练,再用目标域少量数据微调。这个方向比较深,但在实际项目中比反复调参更划算。
这个项目做完之后,我自己留下两个习惯。一是不管模型准确率多高,每次训练完都强制出一张t-SNE图,图能帮我快速判断特征是不是真的可分;二是在任何换工况、换设备实验前,先核对数据划分方式和归一化策略,再谈模型调参。WDCNN本身不是特别花哨的模型,但配合合理的预处理和可视化检查,在轴承故障诊断这个场景里非常能打。如果你也在复现类似项目,建议先跑通这条主线,再去尝试更复杂的注意力机制或者域适应算法,你会更容易判断哪些改动真的有价值。
本文还有配套的精品资源,点击获取