WDCNN轴承故障诊断:振动信号分类与t-SNE可视化实践 简介围绕WDCNN的轴承故障诊断实践包面向具备深度学习基础、希望将一维卷积网络应用于振动信号故障分类的学习者与工程人员。内容聚焦从原始轴承数据到故障类别判定的完整流程并借助t-SNE观察不同故障特征的可分性。压缩包共13个文件约8.66MB包含10个mat数据文件、2个Python脚本和1张png图像数据覆盖正常、内圈、外圈及滚动体等状态脚本对应数据处理与模型运行图像用于呈现可视化结果。已有1281人学习下载。读者可据此复现实验、梳理数据预处理与网络训练思路结合特征分布图分析模型表征效果也能为课程设计、论文实验和故障诊断入门提供可操作的参考。1. 从一段振动信号说起WDCNN 轴承故障诊断能解决什么问题设备巡检里最让人头疼的不是机器响而是响得不明不白。基于 WDCNN 的轴承故障诊断项目把一段振动信号切成固定长度样本用宽卷积核网络做端到端的深度学习特征提取再用 t-SNE 把倒数第二层特征摊到二维平面上看类别分布。它适合手上已有振动数据、想快速跑通故障分类基线并且需要一张能放进分析报告的 t-SNE 可视化图的开发者也适合想从人工特征工程转到深度学习诊断的团队做第一版骨架。2. 工程骨架与数据管线从原始振动到 1×2048 样本轴承故障诊断项目最容易翻车的地方不是网络写错而是数据管线没对齐。raw 信号怎么切窗、标签怎么绑定、训练集和验证集怎么分这三件事决定了模型上限。WDCNN 只负责从样本里提特征喂进去的样本如果混进了验证集片段后面准确率再高也只是数字好看。2.1 目录结构把数据、模型、训练、可视化拆开我一般把这类项目拆成数据、模型、训练、可视化四块不把所有逻辑塞进一个main.py。这样换数据集时只动dataset.py换网络时只动model.pyt-SNE 出图也不依赖训练脚本。bearing-wdcnn/ ├── data/raw/ # 原始 csv/mat按类别或工况分目录 ├── data/processed/ # 切片后的 npz可选 ├── src/dataset.py # 滑窗、标签映射、Dataset ├── src/model.py # WDCNN 网络 ├── src/train.py # 训练、验证、早停 ├── src/tsne.py # 特征导出、t-SNE 绘图 ├── src/utils.py # 随机种子、日志、指标 ├── configs/base.yaml # 窗长、步长、学习率 └── runs/ # best.pt、日志、tsne.png这份目录清单里data/raw只放原始记录不手工改runs专门存权重、日志和图片训练一次生成一个时间戳目录。configs/base.yaml把窗长、步长、batch size、学习率这些参数外置后面做对比实验不用改代码。src/tsne.py单独放是因为可视化经常要在不同 checkpoint 上重复跑不能每次重新训练。环境按常规 PyTorch 项目装即可命令不复杂关键是训练和可视化分两步跑python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install torch numpy scikit-learn matplotlib pandas python src/train.py --data data/raw --win 2048 --step 1024 --epochs 80 python src/tsne.py --ckpt runs/best.pt --out runs/tsne.png第一段建虚拟环境第二段装依赖。--win 2048表示每个样本 2048 个采样点--step 1024表示相邻样本重叠一半--epochs 80不是固定值小数据集通常 30 到 80 轮就能看出趋势。tsne.py读best.pt里的权重和归一化参数输出tsne.png。如果数据在.mat文件里先写一个小脚本把振动通道和标签读成 NumPy 数组不要直接在 Dataset 里解析.mat否则训练时反复读盘会拖慢很多。2.2 滑动窗口与标签生成样本怎么切才不埋雷原始振动信号是一条长序列模型吃的是固定长度片段。常见做法是用滑动窗口切窗长覆盖 2 到 3 个冲击周期步长取窗长的一半左右。窗太长会把不同故障混在一起窗太短又看不到周期性冲击。公开轴承数据常见采样率在几千到几万赫兹2048 点是一个比较稳妥的起点。import numpy as np import torch from torch.utils.data import Dataset class BearingWindowDataset(Dataset): def __init__(self, signals, labels, win2048, step1024): self.x, self.y [], [] for sig, lab in zip(signals, labels): sig np.asarray(sig, dtypenp.float32).reshape(-1) for start in range(0, len(sig) - win 1, step): seg sig[start:start win].copy() seg seg - seg.mean() # 去直流分量保留冲击成分 self.x.append(seg[None, :]) # [1, win] self.y.append(int(lab)) self.x torch.from_numpy(np.stack(self.x)) self.y torch.tensor(self.y, dtypetorch.long) def __len__(self): return self.y.numel() def __getitem__(self, idx): return self.x[idx], self.y[idx]这段代码的逻辑很直接signals是每条记录的一维振动数组列表labels是每条记录对应的故障类别。外层循环按记录走内层循环按step切窗每个窗口减去自身均值去掉直流偏置后再堆成[N, 1, win]的张量。win和step是核心参数win决定感受野step决定样本数量和重叠度。step小于win时样本有重叠能缓解样本不足但重叠太多会让训练集和验证集看起来很像后面划分要按记录文件走不能按窗口随机打乱。如果后面要做多通道振动或电流信号把seg[None, :]改成np.stack([ch1, ch2], axis0)模型第一层Conv1d的in_ch同步改成通道数。标签映射建议单独维护一个label_map比如正常、内圈、外圈、滚动体各对应一个整数不要依赖文件名排序去推类别编号否则换个目录顺序标签就错位了。2.3 归一化与数据集划分别让验证集偷看训练集归一化最常见的错误是先对整个数据集算均值和方差再划分训练验证。这样验证集的分布信息已经进了训练过程准确率会虚高。正确顺序是先按记录文件划分再只用训练集算mean/std最后把同一组参数应用到验证集和测试集。划分也要按记录或工况走不能把同一条记录切出来的窗口随机分到两边。import numpy as np def split_recordings(files, ratios(0.7, 0.15, 0.15), seed42): rng np.random.default_rng(seed) files list(files) rng.shuffle(files) n len(files) n_tr, n_va int(n * ratios[0]), int(n * ratios[1]) return files[:n_tr], files[n_tr:n_tr n_va], files[n_tr n_va:] train_files, val_files, test_files split_recordings(all_files) train_ds BearingWindowDataset(train_signals, train_labels, win2048, step1024) val_ds BearingWindowDataset(val_signals, val_labels, win2048, step1024) mean train_ds.x.mean() std train_ds.x.std() 1e-8 for ds in (train_ds, val_ds): ds.x (ds.x - mean) / stdsplit_recordings按文件列表打乱后切分ratios是训练、验证、测试比例seed固定随机种子方便复现。mean和std只从train_ds.x算1e-8防止除零。验证集和测试集用同一个mean/std不能各自再算一遍。更严格的做法院是留出一个工况做测试比如用负载 0 和 1 训练负载 2 验证负载 3 测试这样能看出模型是不是只记住了某一组工况的分布。注意如果各类样本数量差很多先看混淆矩阵再决定要不要加WeightedRandomSampler或CrossEntropyLoss(weight...)不要一上来就上重采样。3. WDCNN 模型拆解宽卷积核、BN 与特征压缩链路WDCNN 的核心思路是用第一层宽卷积核直接吃原始振动信号靠大感受野抓故障冲击的局部形态后面接几层小卷积核做深层特征组合。它比手工提取时域、频域特征再喂分类器的流程短也比普通小卷积核 CNN 更适合处理轴承冲击这种短时、周期性的波形。模型不复杂但每一层的尺寸和通道数要跟输入长度对齐。3.1 网络结构参数第一层为什么用 64×1 宽核第一层用kernel_size64、stride16目的是在原始信号上覆盖较长的时间窗同时用较大步长快速降采样。轴承故障冲击往往只持续几个采样点宽核能把冲击和周围的衰减振荡一起看进去BN 放在卷积后面训练初期收敛会更顺。后面几层用 3×1 小核逐层组合局部特征最后用全局池化压成一个向量再接全连接分类。层关键参数输出长度输入 2048Conv116 通道kernel 64stride 16padding 24128MaxPoolkernel 2stride 264Conv232 通道kernel 3stride 1padding 164MaxPool2/232Conv3-564 通道kernel 3padding 132AdaptiveAvgPool输出 11FC64→100→类别数-这张表里第一层输出长度按(2048 2×24 - 64) / 16 1 128算出来池化后变 64第二层池化后变 32。后面三层卷积不改变长度只改变通道组合。最后用AdaptiveAvgPool1d(1)而不是写死Flatten维度是为了换窗长时不用手算全连接输入。padding24是我一般会保留的设置既能控制输出长度也不会让边缘信息丢得太狠。3.2 PyTorch 实现前向链路逐段说明模型代码保持直白features负责卷积特征classifier负责分类forward里加一个return_feat开关t-SNE 直接复用同一套网络不用再写一个特征提取器。import torch import torch.nn as nn class WDCNN(nn.Module): def __init__(self, num_classes10, in_ch1): super().__init__() self.features nn.Sequential( nn.Conv1d(in_ch, 16, kernel_size64, stride16, padding24, biasFalse), nn.BatchNorm1d(16), nn.ReLU(inplaceTrue), nn.MaxPool1d(2, 2), nn.Conv1d(16, 32, kernel_size3, padding1, biasFalse), nn.BatchNorm1d(32), nn.ReLU(inplaceTrue), nn.MaxPool1d(2, 2), nn.Conv1d(32, 64, kernel_size3, padding1, biasFalse), nn.BatchNorm1d(64), nn.ReLU(inplaceTrue), nn.Conv1d(64, 64, kernel_size3, padding1, biasFalse), nn.BatchNorm1d(64), nn.ReLU(inplaceTrue), nn.Conv1d(64, 64, kernel_size3, padding1, biasFalse), nn.BatchNorm1d(64), nn.ReLU(inplaceTrue), nn.AdaptiveAvgPool1d(1) ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(64, 100), nn.ReLU(inplaceTrue), nn.Dropout(0.3), nn.Linear(100, num_classes) ) def forward(self, x, return_featFalse): feat self.features(x) # [B, 64, 1] logits self.classifier(feat) # [B, num_classes] if return_feat: return logits, feat.flatten(1) # [B, 64] return logits逻辑上输入[B, 1, 2048]先过宽卷积通道从 1 升到 16长度从 2048 降到 128两次池化后长度降到 32三层 3×1 卷积保持长度只做通道混合全局平均池化把[B, 64, 32]压成[B, 64, 1]再展平送全连接。return_featTrue返回的是分类前的 64 维特征t-SNE 用的就是这一层不是 softmax 输出。参数上num_classes按你的标签表来常见公开数据是 10 类in_ch默认 1多通道时改成实际通道数。Dropout(0.3)是防止全连接层过拟合的常规手段小数据集可以调到 0.2 到 0.5。biasFalse后面接 BN卷积层可以不写偏置少一组参数。窗长小于 1024 时第一层卷积可能输出长度不够切窗参数要提前检查。3.3 训练脚本损失、优化器与早停训练部分不要只写一个for epoch in range(...)至少要有训练和验证两套指标保存验证集最好的权重而不是最后一轮权重。损失函数用交叉熵优化器用 Adam学习率从1e-3起权重衰减加一点防止过拟合。import torch from torch import nn def run_epoch(model, loader, criterion, device, optimizerNone): training optimizer is not None model.train(training) total_loss, correct, total 0.0, 0, 0 for x, y in loader: x, y x.to(device), y.to(device) with torch.set_grad_enabled(training): logits model(x) loss criterion(logits, y) if training: optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() * y.size(0) correct (logits.argmax(1) y).sum().item() total y.size(0) return total_loss / total, correct / total device torch.device(cuda if torch.cuda.is_available() else cpu) model WDCNN(num_classeslen(class_names)).to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) best_acc, bad 0.0, 0 for epoch in range(80): tr_loss, tr_acc run_epoch(model, train_loader, criterion, device, optimizer) va_loss, va_acc run_epoch(model, val_loader, criterion, device) if va_acc best_acc: best_acc, bad va_acc, 0 torch.save({model: model.state_dict(), mean: mean, std: std}, runs/best.pt) else: bad 1 if bad 8: breakrun_epoch用optimizer is not None区分训练和验证训练时开梯度、反向传播、更新参数验证时只前向。total_loss按样本数加权平均correct统计分类正确的样本数。外层循环里验证准确率超过历史最好就保存best.pt连续 8 轮不提升就提前停避免在训练集上磨太久。保存时把mean/std一起存进去后面 t-SNE 和单条样本推理都用同一套归一化参数省得再翻训练脚本。类别不平衡明显时给CrossEntropyLoss传weight权重按训练集类别频率的倒数算再归一化。不要用测试集调权重也不要用验证集反复试到准确率最高验证集只做早停和模型选择最终指标在测试集上跑一次。4. t-SNE 可视化从倒数第二层特征看类别是否分开t-SNE 在这个项目里不是训练组件而是诊断工具。它把高维特征压到二维让人肉眼判断模型学到的类别边界有没有分开。重点看两件事同类样本是不是聚在一起不同类样本有没有明显混叠。图好看不代表模型可靠但图完全糊成一团通常说明特征没学到东西或者数据管线有问题。4.1 特征导出hook 还是直接 forward最省事的方式是模型forward里留return_feat开关验证和测试时直接取分类前特征。用 hook 也能做但多一层回调调试时容易忘记注销。我一般用显式返回代码短逻辑清楚。import numpy as np import torch torch.no_grad() def extract_features(model, loader, device): model.eval() feats, labels [], [] for x, y in loader: x x.to(device) _, f model(x, return_featTrue) feats.append(f.cpu().numpy()) labels.append(y.numpy()) return np.concatenate(feats, axis0), np.concatenate(labels, axis0)这段代码关掉梯度遍历loader每次取[B, 64]特征和标签最后拼成[N, 64]和[N]。model.eval()必须加BN 和 Dropout 在训练模式和推理模式下行为不同忘了切 eval 会让特征分布带随机性。t-SNE 建议用测试集或留出工况的特征不要用训练集训练集聚得太开参考价值不大。4.2 t-SNE 参数与绘图perplexity、随机种子、颜色t-SNE 对参数和随机种子敏感perplexity一般取 5 到 50样本多可以往 30 到 50 调样本少就降到 5 到 15。initpca比随机初始化更稳random_state固定后图可复现。画之前对特征做一次标准化避免某些维度数值大主导距离。import matplotlib.pyplot as plt from sklearn.manifold import TSNE def plot_tsne(feats, labels, class_names, out_path, seed42): feats (feats - feats.mean(0, keepdimsTrue)) / (feats.std(0, keepdimsTrue) 1e-8) emb TSNE( n_components2, perplexity30, initpca, learning_rateauto, random_stateseed ).fit_transform(feats) plt.figure(figsize(7, 6)) for c, name in enumerate(class_names): idx labels c plt.scatter(emb[idx, 0], emb[idx, 1], s12, alpha0.8, labelname) plt.legend(markerscale2, fontsize9) plt.tight_layout() plt.savefig(out_path, dpi300)逻辑上先按特征维度做 z-score再送TSNE降到二维最后按类别循环画散点。perplexity30是常用起点learning_rateauto让 sklearn 自己估学习率老版本不支持就改成learning_rate200。s12控制点大小alpha0.8让重叠点还能看出密度dpi300保证报告里不糊。类别名从label_map反查不要手写顺序否则颜色和图例对不上。注意t-SNE 的坐标轴没有物理单位只表示二维嵌入位置不要在图里标“距离多远”或“准确率多少”。4.3 结果误读t-SNE 图好看不等于模型可靠t-SNE 保留的是局部邻域关系不是全局距离。两个簇离得远不代表模型在测试集上一定准两个簇挨得近也不代表类别一定混。常见误读是拿一张训练集 t-SNE 图证明模型好训练集特征分开是应该的真正要看的是测试集或跨工况留出集。还有一个坑是每次换随机种子图变化很大看到某次图很漂亮就截下来用这种图不能当结论。更稳妥的做法是把 t-SNE 和混淆矩阵放在一起看。t-SNE 发现某两类混在一起回头查混淆矩阵是不是这两类互相误判如果混淆矩阵也很差再去看数据切片、标签映射和归一化。t-SNE 只做定性观察不参与调参也不拿它的图去反推准确率。图是给人看的指标是给模型看的两边对不上时先信指标再查数据。5. 避坑与排查轴承诊断项目最容易翻车的五件事这类项目代码量不大坑基本集中在数据边界和评估口径上。下面五条是我在类似项目里反复遇到的每条按现象、原因、解决写照着排查能省不少时间。5.1 数据泄漏同一条记录被切进训练和验证现象训练准确率 99%验证准确率也 98%换一个新工况测试直接掉到 60% 以下。原因切窗后把所有窗口随机打乱再划分同一条记录切出来的相邻窗口高度重叠训练集和验证集里都有几乎一样的片段验证集等于开卷考试。解决按记录文件或工况划分先分文件再切窗。切完加一道检查训练文件和验证文件不能有交集train_files set(train_file_names) val_files set(val_file_names) assert train_files.isdisjoint(val_files), 训练集和验证集用了同一批记录文件这段断言放在数据加载之后、训练之前train_file_names和val_file_names是原始记录文件名列表。isdisjoint返回True才继续交集不为空直接报错。更严格的做法是留一个工况完全不出现在训练集里用跨工况测试看泛化。5.2 归一化顺序错误先标准化再划分现象验证集指标比测试集高很多换个随机种子波动很大。原因先对整个数据集算mean/std再划分训练验证。验证集的分布信息提前进了归一化参数训练过程间接看到了验证集。解决先划分再只用训练集算mean/std验证集和测试集复用同一组参数。多通道数据按通道分别算不要把所有通道混在一起算一个标量。归一化参数跟权重一起保存推理时直接读不要每次重新算。5.3 标签与工况错位文件名排序埋的雷现象混淆矩阵里某两类互相误判特别严重但看波形又不像。原因用os.listdir的顺序直接映射标签或者用文件名里的数字当类别编号换一台机器目录顺序变了标签和信号对不上。解决维护显式label_map比如{normal: 0, inner: 1, outer: 2, ball: 3}读文件时按目录名或配置表取标签不依赖排序。训练前打印每个类别的样本数和一条样本的标签人工抽查几条确认信号和标签对得上。5.4 过拟合训练准确率 99%测试集掉到 60%现象训练 loss 一直降验证 loss 先降后升测试集准确率明显低于验证集。原因样本少、窗口重叠多、模型容量偏大或者全连接层参数太多。也有可能是某一类样本在训练集里占比过高模型偏向多数类。解决先加数据增强比如加轻微高斯噪声、随机裁剪、幅值缩放再降模型容量减小全连接层或提高 Dropout最后看类别权重和混淆矩阵。早停按验证集准确率或 loss 走不要按训练 loss 停。数据增强只对训练集做验证和测试保持原样。5.5 t-SNE 图不收敛随机种子和 perplexity 的影响现象同一份特征跑两次 t-SNE一次分得很开一次糊成一团。原因t-SNE 是随机算法perplexity和初始化方式对结果影响大样本量少时更明显。特征没有标准化也会让某些维度主导距离。解决固定random_state先用perplexity30跑一版再试 10 和 50 对比初始化用initpca画之前对特征做 z-score。如果多种参数下都糊不要继续调图回去看特征本身和混淆矩阵。t-SNE 图只作为辅助观察不作为模型选择依据。6. 二次开发把 WDCNN 换成自己的数据和任务换数据时先改采样率和窗长不要直接套 2048。窗长按物理时间折算比如采样率 12 kHz2048 点约 0.17 秒能覆盖多个冲击周期如果采样率降到 2 kHz同样 2048 点就是 1 秒可能混进不同工况。多通道信号把in_ch改成通道数第一层卷积核从64×1改成64×C其余层不变先跑通再考虑加残差或多尺度卷积分支。换模型不要一次改三处先保留 WDCNN 基线只加一个残差连接或一个 3×1 与 5×1 并行分支看验证集和跨工况测试有没有提升。验证方法我一般按工况留出训练集放部分负载验证集放另一部分测试集放完全没见过的负载。指标看准确率、每类召回率和混淆矩阵t-SNE 只看测试集特征。改完模型先跑一次 t-SNE再跑一次混淆矩阵两个结果对不上就回头查数据切片和标签映射。从那以后我每次拿到新振动数据先按工况留出验证集再跑模型和 t-SNE图好看但混淆矩阵差的时候一律以混淆矩阵为准。希望帮到你。本文还有配套的精品资源点击获取