☰
WDCNN轴承故障诊断:宽卷积核与t-SNE可视化实战
2026/10/14 13:16:01 网站建设 项目流程

简介:这份资源面向深度学习与工业设备故障诊断方向的学习者和研究者,围绕WDCNN(宽卷积核深度卷积神经网络)在轴承故障诊断中的应用展开,适合具备一定Python与深度学习基础、希望快速复现经典诊断模型并理解特征分布的人群。压缩包共13个文件,以10个mat数据文件为核心,涵盖正常、内圈、外圈、滚动体等多种故障类型与不同损伤尺寸的振动信号,另含2个py脚本分别负责数据处理与模型主流程,并附1张tsne可视化结果图,整体约8.66MB,结构紧凑、开箱即用。目前已有1276人学习下载,说明该方案在同类任务中具有较好的参考价值。读者可借此获得从原始振动信号到诊断结果的完整实现路径,理解数据预处理、WDCNN建模与训练的关键环节,并通过tsne降维图直观观察不同故障类别的特征聚类效果,为后续模型改进、对比实验或论文写作提供可复用的代码与数据基础。

1. 从一段振动信号说起:WDCNN 轴承故障诊断到底在解决什么

一台旋转设备最怕的不是突然停机,而是轴承在“带病工作”时没人发现。振动传感器每秒采几千个点,数据量巨大,但真正有用的故障特征往往藏在冲击成分和调制边带里。传统做法靠人工设计特征——时域看峭度、频域看包络谱,遇到变转速、强噪声、多故障耦合就很容易翻车。基于 WDCNN 的轴承故障诊断,核心思路是用宽卷积核在第一层直接吃原始振动信号,把“特征工程”这件事交给网络自己学,后面再接深层小卷积核做抽象,最后用 t-SNE 把高维特征压到二维平面上看聚类效果。它适合已经拿到振动数据、想快速验证深度学习方案是否可行的从业者,也适合需要给设备健康管理做技术选型的人。这一章先把问题边界划清楚:WDCNN 不是万能药,它擅长的是“端到端、少人工干预、可解释性靠可视化补足”的场景。

2. WDCNN 的结构选型:为什么第一层要用宽卷积核

2.1 宽卷积核不是噱头,它解决的是高频冲击的捕获问题

轴承故障在时域上表现为周期性冲击,冲击持续时间短、频带宽。如果第一层就用 3×1 的小卷积核,感受野太窄,网络需要堆很多层才能覆盖一个冲击周期,训练难度和参数量都会上去。WDCNN 的第一层通常用 64 或 128 长度的宽卷积核,步长设 8 或 16,直接对原始信号做粗粒度扫描。这样做的物理意义是:宽核相当于一个可学习的带通滤波器组,把不同频段的冲击成分先分离出来。后面再接 3×1 的小核,负责把局部模式组合成更抽象的故障特征。我一般会先把第一层核长设成 64,步长 16,如果信号采样率是 12kHz,这个配置对应的时间窗大约 5.3ms,刚好覆盖一个典型的轴承冲击衰减段。

2.2 用 PyTorch 搭一个最小可跑的 WDCNN

下面这段代码是一个能直接跑通的最小 WDCNN 实现,输入是单通道振动信号,输出是故障类别数。注意第一层卷积的 kernel_size 和 stride 是核心参数,后面会单独讲怎么调。

import torch import torch.nn as nn import torch.nn.functional as F class WDCNN(nn.Module): def __init__(self, num_classes=10, input_len=2048): super(WDCNN, self).__init__() # 第一层:宽卷积核,直接吃原始信号 self.conv1 = nn.Conv1d(1, 16, kernel_size=64, stride=16, padding=24) self.bn1 = nn.BatchNorm1d(16) self.pool1 = nn.MaxPool1d(kernel_size=2, stride=2) # 第二层:小卷积核,做特征抽象 self.conv2 = nn.Conv1d(16, 32, kernel_size=3, stride=1, padding=1) self.bn2 = nn.BatchNorm1d(32) self.pool2 = nn.MaxPool1d(kernel_size=2, stride=2) # 第三层:继续加深 self.conv3 = nn.Conv1d(32, 64, kernel_size=3, stride=1, padding=1) self.bn3 = nn.BatchNorm1d(64) self.pool3 = nn.MaxPool1d(kernel_size=2, stride=2) # 全连接层 self.flatten_dim = self._get_flatten_dim(input_len) self.fc1 = nn.Linear(self.flatten_dim, 100) self.fc2 = nn.Linear(100, num_classes) def _get_flatten_dim(self, input_len): x = torch.randn(1, 1, input_len) x = self.pool1(F.relu(self.bn1(self.conv1(x)))) x = self.pool2(F.relu(self.bn2(self.conv2(x)))) x = self.pool3(F.relu(self.bn3(self.conv3(x)))) return x.view(1, -1).size(1) def forward(self, x): x = self.pool1(F.relu(self.bn1(self.conv1(x)))) x = self.pool2(F.relu(self.bn2(self.conv2(x)))) x = self.pool3(F.relu(self.bn3(self.conv3(x)))) x = x.view(x.size(0), -1) x = F.relu(self.fc1(x)) x = self.fc2(x) return x

逻辑说明:第一层 conv1 的 kernel_size=64、stride=16 是 WDCNN 的标志性配置,padding 取 24 是为了让输出长度不至于缩得太快。每个卷积后面接 BatchNorm 和 ReLU,池化统一用 2×2 最大池化。全连接层前先 flatten,fc1 输出 100 维,fc2 输出类别数。参数说明:num_classes 根据你的数据集故障类别数改,input_len 是单样本信号长度,常见取 1024 或 2048。如果输入长度变了,_get_flatten_dim 会自动算全连接输入维度,不用手改。

2.3 训练循环里必须盯住的三个量

训练 WDCNN 和训练普通 CNN 最大的区别在于:振动信号的信噪比低,第一层宽卷积核参数量大,容易过拟合。我一般会在训练循环里同时盯训练损失、验证损失和验证准确率。如果训练损失降得很快但验证损失在 5 个 epoch 后开始抬头,说明第一层核长可能太大了,或者 BatchNorm 的 momentum 设得太激进。下面是一个最小训练循环的骨架:

def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss = 0 for x, y in 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() return total_loss / len(loader) # 关键参数:lr 建议 1e-3 起步,weight_decay 用 1e-4 抑制过拟合 optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4) criterion = nn.CrossEntropyLoss()

逻辑说明:weight_decay 是 WDCNN 训练里最容易被忽略的参数。因为第一层宽卷积核参数量占全网络很大比例,不加正则很容易记住训练集的噪声。参数说明:lr 从 1e-3 开始,如果 loss 震荡就降到 5e-4;weight_decay 从 1e-4 开始,验证集准确率上不去可以试 5e-4。batch_size 建议 64 或 128,太小会让 BatchNorm 统计量不稳。

3. 数据准备与 t-SNE 可视化:从原始振动到二维聚类图

3.1 振动信号切窗、归一化和标签对齐

拿到原始振动数据后,第一步是切窗。常见做法是用滑动窗口,窗长 2048,步长 1024,这样相邻样本有 50% 重叠,能增加样本量。切完窗后要做归一化,我一般用 z-score 按通道做,而不是按样本做,因为按样本归一化会抹掉不同故障程度之间的幅值差异。标签对齐要注意:如果原始数据是按文件存的,每个文件对应一种故障,切窗后每个窗继承文件标签。下面是一个切窗和归一化的函数:

import numpy as np def slice_signal(signal, window_size=2048, step=1024): """滑动窗口切分,返回 (num_windows, window_size)""" windows = [] for start in range(0, len(signal) - window_size + 1, step): windows.append(signal[start:start + window_size]) return np.array(windows) def z_score_normalize(windows): """按通道做 z-score,这里单通道直接对整体做""" mean = windows.mean() std = windows.std() return (windows - mean) / (std + 1e-8)

逻辑说明:slice_signal 的 window_size 和 step 是两个关键参数。窗长太短,一个冲击周期都覆盖不全;窗长太长,样本量不够。2048 点配 1024 步长是振动诊断里比较稳的起点。z_score_normalize 里的 1e-8 是防止 std 为 0 的后悔药。参数说明:如果采样率是 12kHz,2048 点对应约 170ms,足够覆盖中低速轴承的冲击间隔。

3.2 用 t-SNE 把全连接层特征压到二维

t-SNE 可视化的对象不是原始信号,而是网络某一层的输出。我一般取 fc1 之后的 100 维特征,因为这一层既有抽象能力又没被分类层压得太死。做法是:把验证集或测试集过一遍网络,收集 fc1 的输出,然后用 sklearn 的 TSNE 降到二维,最后用散点图按类别着色。下面是对应的代码:

from sklearn.manifold import TSNE import matplotlib.pyplot as plt def extract_features(model, loader, device): model.eval() features, labels = [], [] with torch.no_grad(): for x, y in loader: x = x.to(device) # 取 fc1 之后的特征 feat = model.conv1(x) feat = model.pool1(F.relu(model.bn1(feat))) feat = model.pool2(F.relu(model.bn2(model.conv2(feat)))) feat = model.pool3(F.relu(model.bn3(model.conv3(feat)))) feat = feat.view(feat.size(0), -1) feat = F.relu(model.fc1(feat)) features.append(feat.cpu().numpy()) labels.append(y.numpy()) return np.concatenate(features), np.concatenate(labels) def plot_tsne(features, labels, save_path='tsne.png'): tsne = TSNE(n_components=2, perplexity=30, n_iter=1000, random_state=42) emb = tsne.fit_transform(features) plt.figure(figsize=(8, 6)) scatter = plt.scatter(emb[:, 0], emb[:, 1], c=labels, cmap='tab10', s=8) plt.colorbar(scatter) plt.title('WDCNN feature t-SNE') plt.savefig(save_path, dpi=200)

逻辑说明:extract_features 里手动前向到 fc1,是为了拿到分类前的特征。如果直接取 fc2 输出,类别间边界会被 softmax 压扁,t-SNE 图上反而看不出层次。参数说明:perplexity 一般取 30,样本量小于 500 时降到 10 到 20;n_iter 至少 1000,否则点会挤在一起。random_state 固定住,方便复现。

3.3 t-SNE 图怎么看:聚类紧、类间距大才算好

t-SNE 图不是越漂亮越好,要看两个指标:同类点的紧密度和不同类之间的间隔。如果某一类散成好几团,说明网络没学到该类故障的稳定特征,可能是该类样本太少或者窗长不合适。如果两类点混在一起,说明第一层宽卷积核没把它们的频段分开,可以试着把 kernel_size 从 64 加到 128,或者把 stride 从 16 降到 8。我一般会把 t-SNE 图和混淆矩阵对着看,t-SNE 上混得厉害的两类,混淆矩阵里大概率也互相误判。

4. 避坑与排查:WDCNN 训练里最容易翻车的五件事

4.1 验证集准确率卡在 60% 上不去

现象:训练损失一直在降,验证损失降到某个值后不动,准确率在 60% 附近震荡。原因:第一层宽卷积核的 stride 太大,把冲击成分跳过去了。解决:把 stride 从 16 降到 8,同时把 kernel_size 从 64 加到 128,让第一层覆盖更完整的时间窗。如果显存不够,先把 batch_size 减半。

4.2 t-SNE 图上所有点挤成一团

现象:可视化出来是一个大圆球,类别之间没有边界。原因:取特征的位置不对,取了 fc2 输出或者 softmax 之后的概率。解决:改取 fc1 之后的 100 维特征,并且确认在 eval 模式下做前向,BatchNorm 的 running_mean 和 running_var 要用训练集统计量。

4.3 训练集准确率 99%,测试集只有 70%

现象:典型过拟合。原因:第一层宽卷积核参数量大,加上样本量不够,网络记住了训练集的噪声。解决:weight_decay 从 1e-4 加到 5e-4,同时在第一层卷积后加 Dropout,p 取 0.3。如果还不行,把第一层核数从 16 降到 8。

4.4 换一个转速工况后模型直接失效

现象:在 A 转速下训练,到 B 转速下测试,准确率掉到随机水平。原因:WDCNN 学到的特征和转速强相关,宽卷积核捕获的频段随转速偏移。解决:训练时做转速归一化,或者用阶次分析把时域信号转成角域再切窗。常见做法是加一个转速通道,但 WDCNN 原始结构是单通道,需要改第一层输入通道数。

4.5 t-SNE 跑一次要十几分钟

现象:样本量上万时,sklearn 的 TSNE 慢得让人想砸键盘。原因:t-SNE 复杂度是 O(n²),样本越多越慢。解决:先对特征做 PCA 降到 50 维再跑 t-SNE,或者随机采样 2000 个点做可视化。我一般会固定采样 2000 点,perplexity 取 30,这样几十秒就能出图,足够看聚类趋势。

5. 进阶技巧:用 t-SNE 动画看训练过程,以及一个我常犯的错

5.1 每 10 个 epoch 存一次特征,拼成训练过程动画

单张 t-SNE 图只能看最终状态,但训练过程中特征是怎么从混乱到聚类的,才是判断网络有没有真正学到东西的关键。我一般会每 10 个 epoch 调一次 extract_features,把特征存下来,最后用 matplotlib 的动画接口拼成 GIF。这样能直观看到:前 20 个 epoch 点还在乱飘,50 个 epoch 后同类开始抱团,100 个 epoch 后类间距拉开。如果到 100 个 epoch 还在乱飘,说明学习率太大或者第一层核长不对,不用再浪费时间调后面的层。

import matplotlib.animation as animation def save_feature_snapshots(model, loader, device, epochs, interval=10): snapshots = [] for ep in range(0, epochs, interval): # 这里假设 model 已经训练到第 ep 个 epoch feats, labels = extract_features(model, loader, device) tsne = TSNE(n_components=2, perplexity=30, n_iter=1000, random_state=42) emb = tsne.fit_transform(feats) snapshots.append((emb, labels)) return snapshots def animate_tsne(snapshots, save_path='tsne_evolution.gif'): fig, ax = plt.subplots(figsize=(8, 6)) def update(frame): ax.clear() emb, labels = snapshots[frame] ax.scatter(emb[:, 0], emb[:, 1], c=labels, cmap='tab10', s=8) ax.set_title(f'Epoch {frame * 10}') ani = animation.FuncAnimation(fig, update, frames=len(snapshots), interval=500) ani.save(save_path, writer='pillow')

逻辑说明:save_feature_snapshots 里每个快照都重新跑一次 t-SNE,random_state 固定住,保证不同 epoch 之间的坐标轴可比。参数说明:interval 取 10 是精度和存储的折中,样本量大可以取 20。animate_tsne 的 interval 是动画帧间隔,500ms 比较适合观察。

5.2 我常犯的一个错:用测试集调 t-SNE 的 perplexity

早期做实验时,我为了让 t-SNE 图好看,会反复调 perplexity 直到类别分得最开,然后拿这个图去汇报。后来发现这是典型的自欺欺人:perplexity 是可视化参数,不是模型参数,调它不会让模型变好,只会让图变好看。正确的做法是固定 perplexity=30,用同一套参数看不同模型或不同 epoch 的图,比较的是特征本身的变化,而不是可视化参数的魔法。这个习惯帮我省了很多无效调参时间,也让我更关注混淆矩阵和验证集准确率这些硬指标。

5.3 一个值得试的方向:把 t-SNE 换成 UMAP

t-SNE 的缺点是慢,而且全局结构保持得不好。如果样本量超过 5000,我一般会换成 UMAP,速度能快一个数量级,类间距也更稳定。UMAP 的 n_neighbors 取 15,min_dist 取 0.1,效果和 t-SNE 的 perplexity=30 比较接近。但注意:UMAP 和 t-SNE 的坐标轴都没有物理意义,不要试图解释“这个类在左边所以它频率低”这种结论。可视化只是辅助,最终判断模型好坏还是要看验证集准确率和混淆矩阵。

希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询