简介面向轴承故障诊断学习与复现需求这份深度学习实践资源以WDCNN卷积网络为核心适合具备一定Python与深度学习基础、希望完成振动信号分类实验的高校学生和算法初学者参考。压缩包内共13个文件以10个MAT格式的轴承振动数据为主体覆盖正常、内圈、外圈与滚动体等状态样本并配有2个Python脚本承担数据处理与模型训练流程另有1张t-SNE可视化结果图整体约8.66MB结构紧凑便于直接运行和二次修改。已有1281人学习下载说明其具有一定参考热度。读者可据此理解从原始振动数据整理、标签构造到WDCNN训练与特征可视化分析的完整思路并借鉴脚本中的参数设置、数据划分和结果呈现方式用于课程实验、方法验证或故障诊断入门复现。1. 一条振动曲线能看出什么这个故障诊断项目解决的是哪一步轴承出问题时振动信号里会多出周期性的冲击成分可这段信号交给刚接手的人大概率只能看出“抖得厉害”。这份基于 WDCNN 的轴承故障诊断项目所做的工作就是把这层判断自动化原始振动片段直接进网络输出故障类别末尾再配一张 t-SNE 可视化把模型学到的特征摊到二维平面上看不同故障到底有没有被分开。整个项目走的是纯深度学习路线不依赖人工设计的频域特征也不用先做 FFT 再挑边频带。对刚接触故障诊断、想找一个完整可跑例子的开发者比较友好对想拿一维卷积做信号分类基线的人也好用因为它的训练、评估、可视化三段是拆开的改哪一段都不牵动全局。2. 技术选型与整体链路为什么把 2048 点原始信号直接塞进一维卷积2.1 一条样本是 2048 个浮点数不是一张频谱图先把这个项目的输入说清楚否则后面所有讨论都是空的。它的训练样本形状是(1, 2048)也就是从一段连续振动记录里截出 2048 个采样点做一次标准化然后当成一维“信号图像”喂给网络。没有 FFT没有包络谱没有小波网络看到的就是原始波形。这样做的好处是实现链路短坏处是窗口长度WIN和采样率强绑定。我一般按“至少覆盖 3 个转频周期”来定这个数采样率 12 kHz 时2048 点大约 0.17 秒转频 30 Hz 上下就够覆盖五个周期比较稳换到 48 kHz 采样的台架同样 2048 点只有 0.04 秒连一个完整的冲击周期都不一定包住特征就散了。采样率建议 WIN说明12 kHz2048约 0.17 s覆盖多个转频周期项目默认值20 kHz2048 ~ 4096优先 4096保证物理时长不缩水48 kHz8192直接套 2048 会丢掉冲击间隔信息表格里这几行并非随意列出我确实经历过这样的失败把 12 kHz 下训练好的配置直接搬到一个高采样率台架上准确率掉到六成多模型连“有没有故障”都分不太清。后来把WIN按物理时长换算重设问题当场消失。2.2 第一层那个 64×1 宽卷积核在压什么WDCNN 和普通一维 CNN 最不一样的地方在第一层卷积核宽度 64步长 16输出 16 个通道。这个配置不是随手写的。宽核配合大步长本质上是一组可学习的滤波器组网络自己决定保留哪些频段、压掉哪些频段同时把 2048 点的序列一口气降到 128 点。工业现场的振动信号里高频段往往是电磁噪声和结构共振真正携带故障冲击的是低频包络宽核在这一层承担了“粗筛”的作用。后面四层换成 3×1 小核、步长 1负责在降采样后的序列上堆叠感受野、抽取更抽象的模式。这也是它和“先做特征工程再分类”路线的核心差别频带选择由数据决定不用人去挑。代价有两个一是需要足够多的样本才能学出合理的滤波器二是对采样率变化敏感——第一层核宽是按采样点算的采样率一变等效物理带宽就变了。明白这一点后面调参和迁移才不会盲目。2.3 一条样本从进入网络到画出散点的完整链路把整条链路串一遍后面拆代码时心里有图。原始记录先按文件扫描、按文件名映射到类别编号然后滑动窗口切段、逐样本标准化落盘成npz训练时用TensorDataset包一层DataLoader按批取网络前向走五层卷积加池化展平后接一个 100 维全连接当作特征层再接一个线性分类头训练用交叉熵验证集挑最优权重训练结束后重新加载权重把验证集或测试集过一遍网络只取那 100 维特征送进 t-SNE 降到二维按类别上色画散点。这里有个容易忽略的点分类头和特征层是分开的两个模块。写成一个Sequential也能跑但做可视化时就得靠 hook 去抠中间层输出反而别扭。项目里我把特征层单独命名成embedforward加一个return_feat开关取特征时一行调用就够。2.4 依赖清单五个包撑起全部功能包用途备注torch建模、训练、推理2.x 均可不必卡小版本numpy滑窗切分、数组存取切窗用广播索引比循环快一个量级scipy读取 mat 格式原始数据scipy.io.loadmatscikit-learnt-SNE、混淆矩阵、分类报告TSNE和confusion_matrixmatplotlib散点图与混淆矩阵绘图注意中文字体见第 5 章没有额外框架没有实验管理工具一个runs/目录存权重和日志就够了。这种“够用就好”的取舍在单人复现项目里我觉得是对的依赖越少别人 clone 下来跑通的概率越高。3. 数据管线拆解从 mat 文件到 npz滑动窗口这一步决定成败3.1 目录骨架拿到压缩包先看结构这个项目的组织方式很朴素按“数据 / 模型 / 脚本 / 产物”四块分开bearing-wdcnn/ ├── raw/ # 原始 mat 文件按类别前缀命名 │ ├── healthy_00.mat │ ├── inner_07.mat │ ├── outer_14.mat │ └── ball_21.mat ├── data/ # 切好的 npztrain/val/test 各一份 ├── models/ │ └── wdcnn.py # 网络定义 ├── prepare_data.py # 扫描 切窗 落盘 ├── train.py # 训练与验证 ├── evaluate.py # 混淆矩阵与分类报告 ├── visualize.py # t-SNE 出图 └── runs/ # best.pt、日志、图片raw/里的命名规则很关键前缀就是类别名后缀是文件序号。这比“按文件夹分”更省事因为同一个 mat 文件里往往存着多段不同工况的记录按文件夹分会把工况信息丢掉后面做跨负载验证就抓瞎。3.2 标签映射别让文件名排序决定类别编号血泪经验类别编号千万别用sorted(os.listdir())的下标。文件一多、名字一改类别 0 和类别 1 就可能互换模型训练完全正常混淆矩阵却整片错位排查半天以为是模型问题。显式写死映射表并且把它存成 json 一起落盘LABEL_MAP {healthy: 0, inner: 1, outer: 2, ball: 3} CLASS_NAME {v: k for k, v in LABEL_MAP.items()} def parse_label(path): key path.stem.split(_)[0] # 取文件名前缀 if key not in LABEL_MAP: raise ValueError(f未登记的类别前缀: {key} in {path.name}) return LABEL_MAP[key]parse_label遇到不认识的前缀直接抛异常而不是默认给 0。这个“宁可报错也不猜”的习惯救过我很多次——数据里混进一个命名不规范的文件训练照跑指标悄悄被污染比直接崩掉难查得多。落盘时顺手把LABEL_MAP写成data/label_map.json画混淆矩阵时读回来轴标签就不会和实际类别对不上。3.3 滑动窗口切分与逐样本标准化这是全项目最容易出错的一步代码不长但有三个参数决定了结果可不可信。import numpy as np WIN, STEP 2048, 2048 # 步长等于窗长 不重叠 def slice_signal(sig, winWIN, stepSTEP): 把一维长信号切成 (n, win) 的矩阵 n (len(sig) - win) // step 1 if n 0: raise ValueError(f信号长度 {len(sig)} 不足一个窗口 {win}) idx np.arange(win)[None, :] step * np.arange(n)[:, None] return sig[idx] def build_split(files, out_path): xs, ys [], [] for f in files: sig load_mat(f).astype(np.float32) # 一维数组 sig (sig - sig.mean()) / (sig.std() 1e-8) # 逐样本 z-score seg slice_signal(sig) xs.append(seg) ys.append(np.full(len(seg), parse_label(f), dtypenp.int64)) X np.concatenate(xs)[:, None, :] y np.concatenate(ys) np.savez_compressed(out_path, XX, yy) print(out_path, X.shape, np.bincount(y))slice_signal用广播索引一次生成所有窗口的取址矩阵比for循环拼np.stack快很多处理几百兆的原始记录时体会明显。WIN决定单样本物理时长前面已经算过STEP我默认设成和WIN相等也就是不重叠原因在 5.2 节展开那是踩得最深的一个坑。标准化放在切窗之前按整段信号算均值和标准差而不是按窗口算。按窗口算的话每个样本自己减自己幅值信息被抹平早期故障那种“冲击幅值略大”的微弱线索就没了。这一条没有绝对对错但对故障诊断任务保留段内相对幅值的做法通常更稳。3.4 Dataset 与 DataLoader保持简单就好数据量不大没必要自定义Dataset类TensorDataset足够import numpy as np, torch from torch.utils.data import DataLoader, TensorDataset def make_loader(npz_path, batch_size64, shuffleFalse, num_workers0): d np.load(npz_path) X torch.from_numpy(d[X]) # float32, (N,1,2048) y torch.from_numpy(d[y]) # int64, (N,) ds TensorDataset(X, y) return DataLoader(ds, batch_sizebatch_size, shuffleshuffle, num_workersnum_workers, drop_lastFalse, pin_memorytorch.cuda.is_available())三个参数值得说shuffle只在训练集开验证和测试必须关掉否则 t-SNE 图上点的顺序每次都不一样想对比两张图会疯num_workers在 Windows 上建议设 0设大了偶发卡死这是常见问题drop_last保持False因为网络里有 BN最后一个 batch 只剩 1 条样本时会直接抛出Expected more than 1 value per channel的错——这个报错会在第 5 章再提一次。4. 核心模块拆解WDCNN 网络、训练循环与 t-SNE 取哪一层4.1 网络定义与逐层 shape网络结构我按 2048 点输入逐层算过 shape注释里标了出来改输入长度时对照着调不会出错。import torch.nn as nn class WDCNN(nn.Module): def __init__(self, num_classes4, feat_dim100): super().__init__() def block(cin, cout, k, s, p): return nn.Sequential( nn.Conv1d(cin, cout, k, s, p, biasFalse), nn.BatchNorm1d(cout), nn.ReLU(inplaceTrue), nn.MaxPool1d(2, 2), ) self.features nn.Sequential( block(1, 16, 64, 16, 24), # 2048 - 128 - 64 block(16, 32, 3, 1, 1), # 64 - 32 block(32, 64, 3, 1, 1), # 32 - 16 block(64, 64, 3, 1, 1), # 16 - 8 block(64, 64, 3, 1, 1), # 8 - 4 ) self.embed nn.Sequential( nn.Flatten(), nn.Linear(64 * 4, feat_dim), nn.BatchNorm1d(feat_dim), nn.ReLU(inplaceTrue), ) self.classifier nn.Linear(feat_dim, num_classes) def forward(self, x, return_featFalse): f self.embed(self.features(x)) if return_feat: # t-SNE 走这一支 return f return self.classifier(f)第一层k64, s16, p24输出长度是(2048 48 - 64) // 16 1 128再经 2 倍池化变 64之后每层 3×1 卷积保持长度、池化减半走到最后是 4展平得 256 维接 100 维特征层。biasFalse是有意的卷积后紧跟 BN偏置会被 BN 的减均值抵消掉留着只是多占参数。feat_dim100是特征维度做二维可视化时 100 维已经够 t-SNE 发挥再大计算量上去、图未必更好看。真正做多分类任务时num_classes跟着你的类别数改比如健康 三种故障就是 4再加两种复合故障就改 6CLASS_NAME同步改。return_feat这个开关是整个可视化链路的关键。没有它就得用register_forward_hook抠中间层代码难读、调试也麻烦。4.2 训练循环与验证训练部分我写得比较“笨”一个 epoch 一个函数不做花哨的封装方便单步调试。def run_epoch(model, loader, device, optNone, critNone): training opt is not None model.train(training) loss_sum, correct, total 0.0, 0, 0 for x, y in loader: x, y x.to(device, non_blockingTrue), y.to(device, non_blockingTrue) with torch.set_grad_enabled(training): out model(x) loss crit(out, y) if training: opt.zero_grad() loss.backward() opt.step() loss_sum loss.item() * y.size(0) correct (out.argmax(1) y).sum().item() total y.size(0) return loss_sum / total, correct / totalmodel.train(training)一行同时管住训练和评估两种模式BN 层和将来可能加的Dropout 都跟着切比手动写两处model.train()、model.eval()靠谱。loss.item() * y.size(0)是按样本数加权避免最后一个不满的 batch 把小 batch 的损失放大。外层的调度我一般这么写Adam初始学习率1e-3ReduceLROnPlateau在验证损失三个 epoch 不降时乘 0.5最多跑 60 轮保存验证准确率最高的那份权重。项目里不做早停因为轴承数据量小、单轮很快跑满反而省心——这也是个取舍数据大的时候必须加早停不然白烧电费。4.3 t-SNE取 fc 层特征不是 softmax 输出可视化这块是很多同类项目最糊弄的地方。常见写法是拿softmax之后的概率向量做 t-SNE画出来确实一簇一簇分得很开但那是假象——softmax 输出被训练目标强行推成 one-hot 形状本来就接近簇状画出来好看不说明模型学到了东西。正确的做法是取分类头之前的那 100 维特征。import numpy as np, torch from sklearn.manifold import TSNE torch.no_grad() def extract_features(model, loader, device): model.eval() # BN 必须走 running stats feats, labels [], [] for x, y in loader: feats.append(model(x.to(device), return_featTrue).cpu().numpy()) labels.append(y.numpy()) return np.concatenate(feats), np.concatenate(labels) def tsne_plot(model, loader, device, save_pathruns/tsne.png, perplexity30, n_iter1000): F, L extract_features(model, loader, device) Z TSNE(n_components2, perplexityperplexity, initpca, learning_rateauto, n_itern_iter, random_state42).fit_transform(F) ...三个参数按顺序说。perplexity可以理解为“每个点大概关注多少邻居”取值一般 5 到 50样本几百条时用 30 左右太小会碎成一地小团太大所有点糊成一坨黑initpca比随机初始化稳定尤其样本少的时候随机初始化两次跑出来的图能差出两个样子random_state固定是为了两次实验的图能横向对比不固定的话你会怀疑模型变了其实只是随机种子变了。model.eval()那一行是必须的后面第 5 章有个坑专门讲它——忘了切评估模式BN 用当前 batch 的统计量做归一化同一批特征两次前向结果都不一样画出来的图纯属碰运气。4.4 混淆矩阵看清模型错在哪一类准确率一个数字说明不了问题尤其类别不均衡时。评估脚本里我固定输出混淆矩阵和每类召回from sklearn.metrics import confusion_matrix, classification_report def report(model, loader, device, class_name): model.eval() preds, gts [], [] with torch.no_grad(): for x, y in loader: preds.append(model(x.to(device)).argmax(1).cpu()) gts.append(y) preds, gts torch.cat(preds).numpy(), torch.cat(gts).numpy() names [class_name[i] for i in sorted(class_name)] print(classification_report(gts, preds, target_namesnames, digits4)) return confusion_matrix(gts, preds)target_names必须按类别编号顺序给不能按dict的遍历顺序Python 3.7 之后字典保序但你的映射表未必是按编号顺序写的。这一点和第 3.2 节的标签映射是同一类问题索引错位指标全废而且不报错。看混淆矩阵的经验是先看对角线外最大的那个格子。轴承数据里最常见的是内圈和外圈互错因为两者的冲击周期在时域上差别不如包络谱里明显如果某两类互相错的样本数接近基本可以判定网络的感受野不够或者WIN太短没能截到完整的冲击间隔。5. 跑通与避坑排查五个真返工过的坑5.1 环境安装与三条命令先把跑通的路走一遍。依赖不多虚拟环境里装完就能跑python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install torch numpy scipy scikit-learn matplotlib python prepare_data.py --raw ./raw --out ./data python train.py --data ./data --epochs 60 --batch-size 64 --lr 1e-3 python visualize.py --ckpt runs/best.pt --split test --perplexity 30第一条命令生成三个 npz打印出各自的形状和类别分布这一步一定要看输出——形状不对或者某一类样本数为 0后面训练再久也是白搭。第二条命令开始训练每个 epoch 打印训练/验证的 loss 和准确率最优权重存到runs/best.pt。第三条加载权重、在测试集上抽特征、出图存到runs/tsne.png。有 GPU 的话脚本里会自动选cuda没有也能跑这个数据量 CPU 上几分钟一轮不耽误事。5.2 坑一重叠切窗准确率虚高到 99.9%现象第一次跑出 99.9% 的测试准确率兴奋了十分钟换一组数据直接掉到 60%。原因滑窗步长设成了 512相邻样本有 75% 的重叠切完再随机划分训练/测试导致测试集里的样本和训练集里的某条几乎是同一条信号平移了几个点。模型相当于在背答案。解决STEP直接等于WIN不重叠更严格的做法是先按文件划分训练/测试再各自切窗切断一切共享来源。这个改动会让准确率掉几个点但那个数字才是真的。从那以后我切完数据第一件事是检查训练集和测试集有没有来自同一个原始文件的样本有就重新划分。5.3 坑二归一化统计量用了全量数据现象指标好看但推理时喂一条新信号结果飘忽。原因早期版本里我先np.concatenate所有信号算出一个全局均值和标准差再用它归一化所有样本——包括测试集的。测试集的分布信息提前泄漏进训练流程属于典型的数据泄漏。解决改成逐样本 z-score也就是每条窗口减自己的均值除以自己的标准差各管各的如果确实需要全局归一化就只用训练集统计量存成norm.json跟着权重走推理时读同一份。这个坑的隐蔽之处在于它不会让指标变差反而变好所以只能靠流程约束而不是靠看结果发现。5.4 坑三BN 的两种翻车方式现象一训练到最后一个 batch 抛出Expected more than 1 value per channel when training, got input size torch.Size([1, 64, 4])。 现象二训练正常t-SNE 图每次画出来都不一样同一份权重两次结果差很远。原因现象一是最后一个 batch 只剩下 1 条样本BN 在训练模式下要算 batch 内的方差一条样本算不出方差。现象二是抽特征时忘了model.eval()BN 仍在用当前 batch 的统计量归一化。解决前者把batch_size设成能整除样本数的值或者drop_lastTrue训练集可以用验证集别用后者在抽特征函数上加torch.no_grad()和model.eval()两行都不能少。第二个坑我当时查了快一天因为损失曲线一切正常只有图不对很容易怀疑到 t-SNE 头上。5.5 坑四t-SNE 参数和特征层选错图好看但没意义现象散点图上四类分得干干净净可模型在测试集上的准确率只有 70%。原因取了softmax输出做 t-SNE。概率向量被训练目标压成近似 one-hot几类天然分开跟特征质量没关系。解决取classifier之前的那一层也就是return_featTrue的分支。另外perplexity要跟着样本量走几百条样本用 30 是合理的上万条样本就得往上调否则局部结构全被抹平。判断图有没有信息量的土办法是看类间边界附近有没有交错的点全分开反而要警惕真实模型的错误样本总会在边界上留痕。5.6 坑五只在一个负载下训测换工况就崩现象单一转速下训练测试都很好换个负载录一段信号进去准确率腰斩。原因模型学到的是这个转速下的特定波形和特定频率分布转速一变冲击周期跟着变特征分布整体漂移。解决训练时就把多个负载的记录混进去划分时按“留一负载”做交叉验证——三个负载训练、第四个负载测试这样报出来的数字才代表泛化能力。项目里预留了--split-by load参数直接按负载分组划分不用自己改代码。这一条如果一开始不设计好后面补做实验要重跑一遍全部流程很痛苦。6. 二次开发把这套东西接到你自己的台架上换数据是迟早的事我把改动点收敛到了三个地方改动量大概半小时。第一处是采样率和窗口长度。prepare_data.py里的WIN按 2.1 节的表重新算同时确认slice_signal里那条“长度不足一个窗口”的异常有没有被触发——如果原始记录太短得先把多个文件拼起来或者降低WIN。第二处是LABEL_MAP把你的类别前缀加进去CLASS_NAME会自动反向生成混淆矩阵的轴标签跟着走。第三处是num_classestrain.py里从label_map.json读最大编号加一不要手写死。python prepare_data.py --raw ./my_rig --out ./data --win 4096 --step 4096 python train.py --data ./data --epochs 80 --batch-size 32 --lr 5e-4 --split-by load想加对比实验的话我一般这么做一张表跑完填数字比在日志里翻强得多模型参数量单负载准确率留一负载准确率加噪 6dB 准确率WDCNN本项目~0.1M待填待填待填3×1 小核首层待填待填待填待填加 SE 注意力待填待填待填待填第一行改首层核宽就能做第二行在features每个 block 后插一个通道注意力两处改动都不超过二十行。加噪测试最容易被忽略但最能说明问题x torch.randn_like(x) * stdstd按信号标准差和信噪比换算跑一遍就知道模型到底学到了结构还是记住了噪声。还有一个我挺喜欢的小改造每个 epoch 存一次特征最后拼成动图或几宫格。特征在训练早期是糊在一起的几十轮之后慢慢分开这个过程比最终那张静态图有说服力得多给同组的人讲模型在学什么时特别直观。实现上就是把extract_features挪进训练循环隔几个 epoch 调一次注意别每轮都跑t-SNE 本身不便宜。从那以后我每接一个新数据集第一件事都是先画出几段原始波形和它们的包络确认窗口长度截到的是完整冲击而不是半截噪声然后再动网络结构。那次 99.9% 的教训让我养成了先验证数据、再调模型的顺序改起来反而快。希望帮到你。本文还有配套的精品资源点击获取