基于一维CNN的轴承故障诊断实战:从振动信号到模型部署
发布时间:2026/9/7 9:26:48 作者:尧图编辑部 阅读量:1,286

简介面向深度学习故障诊断入门者的完整示例资源围绕数据预处理、模型搭建、模型训练三大环节展开内容同时涉及CNN、LSTM等常见网络与评估优化思路可作为从零理解设备故障诊断流程的参考练习。压缩包共57个文件整体约46.13MB包含40个mat数据文件、10张训练/可视化图像、3个Python训练脚本、2个已训练H5模型以及2个说明文档数据、代码和结果相互对应便于边看边跑。已有904人学习下载说明该示例在入门场景中具备一定实用性。借助mat数据可直接体验数据清洗、标准化、划分训练集等预处理操作通过py脚本可快速复现模型搭建与训练流程而h5模型和png图表则有助于对照效果、理解输出。对正在学习深度学习故障诊断、希望获得完整小工程范式的高校学生或算法入门者这套资料提供了清晰的技术路径。1. 项目概述与整体设计思路我接触故障诊断这个方向最早其实是被逼的。现场的设备一停整个产线跟着趴窝靠老师傅耳朵听、手摸的经验判断人不在场就得干等。后来发现深度学习在这块能做的事远超预期把振动信号当“语音”去识别把故障类别当“文本分类”去做一套卷积网络就能在几分钟内给出一锤定音的判断。这篇文章我就拿一个最典型的案例——轴承故障诊断——完整走一遍从原始数据到训练出模型的流程包括数据预处理、模型搭建、模型训练三个核心环节。适合刚入门深度学习、手里有传感器数据但不知道从哪下手的工程师也适合做算法想补一点工程落地经验的学生。选轴承做示例有个很实在的理由轴承故障是旋转机械里最常见的故障源而且公开数据集成熟最容易复现。我在实际项目中发现只要把轴承这个场景跑通换成齿轮箱、电机、泵无非是改改信号长度和类别数整套流程的骨架完全不用动。1.1 为什么用深度学习而不是传统方法传统故障诊断的思路是先手工设计特征比如时域的均方根值、峰值因子频域的边带能量然后丢进SVM或者随机森林里分类。这套路我用了好几年最大的痛点是特征工程太吃经验不同设备、不同工况下同一个特征的表现天差地别换个转速可能就得重新调一轮特征。深度学习的逻辑完全不同。把原始振动信号或者简单变换后的频谱直接喂给网络模型自己从数据里学出“什么特征重要”。这不是说深度学习一定碾压传统方法而是它把“特征设计”这件事从人肉劳动变成了网络内部的自动学习尤其面对多工况、多故障混叠的真实场景泛化能力比手工特征稳得多。1.2 示例的完整技术路径这个项目的核心链路是采集信号 → 滑窗切片 → 特征变换 → 构造数据集 → 搭建一维CNN → 训练调优 → 评估导出。数据集我用的是CWRU凯斯西储大学轴承数据中心的标准数据采样频率12kHz驱动端加速度信号包含正常、内圈故障、外圈故障、滚动体故障四类每类又有不同损伤直径。这个数据集的样本量充足、类别标注清晰用来跑通流程最省心。整个流程里我特意把数据预处理放在最前面讲因为在实际踩坑中数据质量对模型上限的影响远大于网络结构。后面三个章节就按这条链路依次展开每一步都给出可直接运行的代码和参数选择依据。2. 数据预处理决定模型上限的隐形战场很多人入门时急着搭模型数据那边随便处理一下就开始训练结果loss死活降不下去最后反过来怀疑网络结构有问题。我的经验是深度学习项目里数据预处理占整个项目工作量的六成以上模型结构反而是最不需要折腾的部分。2.1 原始信号的读取与切片CWRU数据是mat格式用scipy.io的loadmat直接读就行。读出来之后会拿到一个一维数组这就是原始振动信号。import scipy.io import numpy as np mat scipy.io.loadmat(inner_race_7.mat) # CWRU数据的key通常是X200_DE_time这种格式 signal mat[X200_DE_time].flatten() print(f采样率: 12kHz, 信号长度: {len(signal)})拿到完整信号后不能直接整段丢给网络。一来长度不固定网络没法处理二来故障特征在信号里是局部出现的整段输入会把特征稀释掉。我的做法是滑窗切片每个样本取1024个点窗口重叠率设成50%。为什么是1024而不是512或者2048主要考虑两点一是1024个点在12kHz采样率下对应约85毫秒的时长足够覆盖轴承转频和故障特征频率的几个完整周期二是2的幂次在后续做卷积下采样时尺寸计算方便不容易出边界问题。如果你要复现自己的数据有个简单估算方法先算出故障特征频率比如轴承内圈故障频率通常在转频的5到10倍之间窗口时长至少要能包含特征频率的20个周期以上。# 滑窗切片 def sliding_window_slice(signal, window_size1024, overlap0.5): step int(window_size * (1 - overlap)) samples [] for start in range(0, len(signal) - window_size 1, step): samples.append(signal[start:start window_size]) return np.array(samples) samples sliding_window_slice(signal, 1024, 0.5) print(f切片数量: {samples.shape[0]})2.2 要不要做FFT和归一化处理这是预处理里争议最大的一个环节。我在初学阶段也纠结过原始时域信号直接给模型跟先做FFT转成频域再给模型到底哪种好实际对比下来关键结论是对一维CNN来说时域信号够用但FFT谱可以让模型更容易学。原因在于滚动轴承的故障特征天然在频域上表现得更加聚集——内圈故障会在特征频率及其倍频处出现清晰的谱峰而时域里这些特征混在幅值调制信号里模型需要更多的层数才能解耦。我的做法是双通道输入一维卷积的输入通道设为2一个通道放时域波形另一个通道放FFT幅值谱。这样模型既有时域的冲击特征信息又有频域的周期特征信息。实现方式是把FFT幅值归一化后和时域信号concat在特征维上。def build_dual_channel_sample(signal_segment): # 时域通道 time_channel signal_segment / np.max(np.abs(signal_segment)) # 频域通道 freq_spec np.abs(np.fft.rfft(signal_segment)) freq_channel freq_spec / (np.max(freq_spec) 1e-8) # 把频域谱裁到与时域一致 freq_channel np.pad(freq_channel, (0, max(0, len(time_channel) - len(freq_channel))))[:len(time_channel)] return np.stack([time_channel, freq_channel], axis0)归一化这里务必注意必须按“样本内最大值”做缩放而不是按整个数据集的全局最大值缩放。原因很直接不同工况下振动幅值差异可能达到几十倍全局归一化会让小幅值的故障样本被压到几乎为零模型直接学不到这类样本的特征。每个样本独立归一化相当于只看“这个样本内部哪些时刻相对更剧烈”这对故障诊断来说是更具区分度的特征。2.3 数据集划分小心数据泄漏这个坑这是新手最容易忽略的环节。常规做法是先把所有切片混在一起再随机划分训练集和测试集。看起来很合理对吧但实际上一旦这么做了你的测试集结果就是假的。原因在于滑窗切片之间高度重叠同一个信号段切出来的相邻样本在时间上有大量重复区域。如果这些相似样本同时出现在训练集和测试集里模型等于提前见到了“答案”测试准确率虚高到99%以上一到现场真实数据上就露馅。正确的做法是先把完整信号按时间顺序切分成不重叠的几大段比如前70%的数据段用于训练后30%的数据段用于测试然后再在各自的数据段内部做滑窗切片。这样才能保证训练集和测试集的样本在时间上没有交叉。def split_signal(signal, train_ratio0.7): split_idx int(len(signal) * train_ratio) return signal[:split_idx], signal[split_idx:] train_signal, test_signal split_signal(signal) train_samples sliding_window_slice(train_signal, 1024, 0.5) test_samples sliding_window_slice(test_signal, 1024, 0.5)同时对于样本不均衡的问题分类训练前要做直方图统计。我习惯在每个故障类别下都确认一下样本数量是否大致相等如果某一类比其他类少很多就对该类做随机过采样——也就是重复选几次切片继承原来的标签。这个方法简单有效至少能防止模型为了降低整体loss直接无视小类别。3. 模型搭建一维CNN的选型与核心结构数据整理好了模型这部分就清爽很多。对于轴承故障这类一维时序信号我不建议一上来就上LSTM或者Transformer更不推荐直接用ResNet这类为图像设计的二维网络。这里的现实是故障冲击特征在时间上局部聚集一维CNN用堆叠卷积核就能提取足够的局部特征训练速度快参数量小部署到边缘设备也更友好。我见过程度比较好的同事把轴承故障诊断做成二维频谱图丢给ResNet训练效果并不比一维CNN好但参数量和训练时间都翻了好几倍完全没有必要。3.1 一维CNN网络结构设计我用的结构是四层一维卷积加两层全连接整体设计逻辑是逐层扩大感受野底层卷积核很小负责提取局部冲击细节高层卷积核稍大负责融合更大时间跨度的特征。具体各层参数如下。层操作核大小输出通道输出尺寸输入原始样本--2×1024Conv1Conv1dBNReLUMaxPool31616×256Conv2Conv1dBNReLUMaxPool33232×64Conv3Conv1dBNReLUMaxPool56464×16Conv4Conv1dBNReLUMaxPool5128128×4Flatten展平--512FC1全连接ReLUDropout-128128FC2全连接Softmax-44这里两层使用核大小为3的卷积是为了先捕捉相邻几个采样点之间的短时冲击关系到了第三、第四层改用核大小为5是因为特征图经过池化后分辨率降低了网状尺寸更大一点的卷积核能在更粗的时间尺度上聚合信号形态。我实际调参时也试过全用核大小为3的版本分类精度会掉1%到2%改成这种渐进式扩大后效果最稳定。BatchNorm必须加在激活函数之前这一点很多人写反。BN的作用是把每一层的输出分布拉回到均值为0、方差为1附近不然深层网络的梯度会在反向传播时发生消失或爆炸。每层池化用的是MaxPool而不是AveragePool也是因为故障冲击信号的特点是“局部最大值有意义”平均池化反而会把尖锐的冲击特征抹平。3.2 损失函数与优化器选型解析分类问题损失函数直接用交叉熵。这里有个细节PyTorch的CrossEntropyLoss内部已经自带Softmax所以网络最后一层不需要额外再套Softmax激活直接输出4个类别的logits就行。优化器上AdamW比传统Adam更稳。我之前在另一个项目里反复踩过Adam权重衰减写法的坑直接用Adam加weight_decay参数实际上会干扰自适应学习率的计算AdamW把权重衰减从梯度更新中拆了出来收敛更平稳尤其是训练后期loss不容易抖动。初始学习率我设成0.001配合余弦退火调度器前几十个epoch快速下降后期慢慢收缩效果比固定学习率好很多。import torch import torch.nn as nn import torch.optim as optim class FaultCNN(nn.Module): def __init__(self, num_classes4): super().__init__() self.features nn.Sequential( nn.Conv1d(2, 16, kernel_size3, padding1), nn.BatchNorm1d(16), nn.ReLU(inplaceTrue), nn.MaxPool1d(kernel_size4), nn.Conv1d(16, 32, kernel_size3, padding1), nn.BatchNorm1d(32), nn.ReLU(inplaceTrue), nn.MaxPool1d(kernel_size4), nn.Conv1d(32, 64, kernel_size5, padding2), nn.BatchNorm1d(64), nn.ReLU(inplaceTrue), nn.MaxPool1d(kernel_size4), nn.Conv1d(64, 128, kernel_size5, padding2), nn.BatchNorm1d(128), nn.ReLU(inplaceTrue), nn.MaxPool1d(kernel_size4), ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(128 * 4, 128), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(128, num_classes) ) def forward(self, x): return self.classifier(self.features(x)) model FaultCNN(num_classes4) optimizer optim.AdamW(model.parameters(), lr0.001, weight_decay1e-4) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max80) criterion nn.CrossEntropyLoss()Dropout加在全连接层之间比例0.5这是防止过拟合性价比最高的手段。卷积层之间我没加Dropout因为BN本身已经提供了正则化效果再叠Dropout容易导致欠拟合。4. 模型训练全程实录与关键细节模型搭完训练阶段反而是最流程化的环节。但流程化不意味着可以闭眼跑——训练过程中的每一个数值变化都是在告诉你模型和数据的适配情况。我习惯一边训练一边盯三个关键指标训练loss、验证准确率、训练和验证准确率之间的差距。4.1 训练超参数配置与训练循环batch size设置成128。这个值不是拍脑袋定的太大的batch会让梯度方向趋于平滑训练前期收敛快但后期容易卡在尖锐极小值上泛化能力变差太小的batch,比如16或者32噪声太大训练过程像喝醉了走路半天找不到方向。在四分类、数万样本的规模下128是安全起点。epoch设80轮。用余弦退火的学习率曲线前20轮学习率还维持在高位模型快速下降40轮以后学习率开始明显收缩loss曲线出现缓慢的精细调整阶段。这里有个经验规律如果80轮之后验证准确率还在缓慢往上走说明模型还有余量可以加epoch反之如果50轮以内验证准确率就停滞了说明学习率衰减得太早需要调大T_max。from torch.utils.data import DataLoader, TensorDataset train_dataset TensorDataset(torch.FloatTensor(X_train), torch.LongTensor(y_train)) test_dataset TensorDataset(torch.FloatTensor(X_test), torch.LongTensor(y_test)) train_loader DataLoader(train_dataset, batch_size128, shuffleTrue) test_loader DataLoader(test_dataset, batch_size128, shuffleFalse) device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) best_acc 0.0 for epoch in range(80): model.train() total_loss 0.0 for xb, yb in train_loader: xb, yb xb.to(device), yb.to(device) optimizer.zero_grad() outputs model(xb) loss criterion(outputs, yb) loss.backward() optimizer.step() total_loss loss.item() * xb.size(0) scheduler.step() # 验证 model.eval() correct 0 total 0 with torch.no_grad(): for xb, yb in test_loader: xb, yb xb.to(device), yb.to(device) outputs model(xb) _, predicted torch.max(outputs, 1) total yb.size(0) correct (predicted yb).sum().item() acc correct / total avg_loss total_loss / len(train_dataset) print(fEpoch {epoch1:02d}, Loss: {avg_loss:.4f}, Val Acc: {acc:.4f}) if acc best_acc: best_acc acc torch.save(model.state_dict(), best_fault_model.pth)注意模型在训练和推理之间必须切换模式训练阶段model.train()让Dropout和BN的正常运行方式保持活性验证阶段model.eval()则确保Dropout关闭、BN使用全局统计量。少了这一步验证结果会有很大的随机性甚至同一份权重两次验证的精度都不一样。4.2 训练结果解读与模型评估在我跑通的这个案例中训练集上的最终准确率约99.5%测试集准确率约98.1%。这个差值说明模型几乎没有过拟合泛化能力在可接受范围。如果你发现训练准确率100%、测试准确率却只有85%那基本可以断定测试集和训练集存在数据泄漏回到之前说的信号切分方法重新做一遍。评估阶段单看准确率是不够的。我额外会输出混淆矩阵重点看哪些错误是系统性的。以我实际跑出来的结果为例混淆矩阵显示“滚动体故障被误判为内圈故障”的比例明显高于其他错误组合。这个现象有物理背景滚动体故障的特征频率会随保持架旋转而变化激励位置不固定信号在传递路径上可能与内圈故障的表现形式相似。这是数据本身的天然困难不是模型bug但你可以针对性做数据增强——比如给滚动体故障样本额外加一点噪声扰动帮助模型学到更稳健的特征。from sklearn.metrics import confusion_matrix, classification_report model.eval() all_preds [] all_labels [] with torch.no_grad(): for xb, yb in test_loader: xb xb.to(device) outputs model(xb) _, predicted torch.max(outputs, 1) all_preds.extend(predicted.cpu().numpy()) all_labels.extend(yb.numpy()) cm confusion_matrix(all_labels, all_preds) print(Confusion Matrix:) print(cm)模型导出也很重要。实际落地到现场往往不能用PyTorch裸模型直接跑我一般是先转成ONNX再根据边缘设备情况选TensorRT或者OpenVINO加速。转换时记得固定输入维度因为ONNX的batch维动态维支持有时会带来不必要的性能开销。转化完用onnxruntime测一遍输出对齐确保精度基本一致再部署。5. 训练踩坑实录六大高频问题的排查方法在带过好几个入门项目之后我总结了一套故障诊断训练阶段的排障流程。以下六个问题基本覆盖了这个场景下90%的异常情况。5.1 损失不下降或者猛涨优先检查学习率。故障诊断任务不同于图像分类振动信号的数值范围差异很大如果学习率设置偏高loss值经常在最开始几个batch就会冲到极大值甚至出现NaN。我遇到NaN的概率其实不高一旦遇到85%的原因是特征维里出现了NaN或Inf。比如FFT变换前信号里有未处理的空值或者归一化时除到了零。排查方法很简单训练前打印一下每个通道的最大值和最小值确认数值范围是正常的。5.2 验证准确率忽高忽低大概率是batch size过小加上学习率偏高的组合问题。另一个隐蔽原因是验证集太小样本量少时每一轮验证的随机波动都会很大。我会保证每个类别在验证集里至少有几百个样本否则宁可多切几段重叠率低的数据出来。5.3 训练集和测试集精度差距过大首选检查数据泄漏。很多入门教材里会把随机划分数据集写成标准流程但在滑窗切片任务里这是大忌。正确做法我在预处理章节详细写了先分段再切片或者直接用sklearn的GroupShuffleSplit按信号段分组划分。5.4 多分类中某一类精度明显偏低先画出混淆矩阵看具体误判对象。如果是物理特征相似的类比如不同故障类型在频谱上的调制频率接近可以尝试给该类别增加样本权重也就是在损失函数里按类别比例加权让模型更重视少样本类别。也可以用Focal Loss替代普通交叉熵它对难分类样本的注意力更高但训练中需要仔细调gamma和alpha两个超参数。# 类别权重加权损失 from sklearn.utils.class_weight import compute_class_weight class_weights compute_class_weight(balanced, classesnp.unique(y_train), yy_train) class_weights torch.FloatTensor(class_weights).to(device) weighted_criterion nn.CrossEntropyLoss(weightclass_weights)5.5 GPU显存不足调小batch size是最直接的办法。实时监测显存占用会发现一次前向和后向传播会占用约两倍的模型显存所以显存和batch size不是线性关系。也可以打开gradient checkpointing用一点计算时间换取显存空间但一维CNN参数本来就少这个一般用不上。5.6 测试集表现好新设备数据一测就崩这是故障诊断项目里最痛的场景。现场设备的运行转速、负载、安装位置都可能与训练数据不同模型在实验室数据上再准也没用。我目前用得比较实用的方法是数据域自适应简单说就是把训练时的信号做小幅变速扰动、幅值抖动、随机噪声叠加最大程度模拟不同工况。如果这还不行那就必须收集少量现场数据做微调没有别的捷径。6. 从轴承案例到通用故障诊断的扩展心得最后聊一点我的体会。跑完这个轴承案例之后我觉得最大的收获不是“我学会了用CNN分类信号”而是建立了一套处理时序故障数据的通用方法论先看数据质量再做合理的样本划分然后选一个和信号特性匹配的轻量网络最后严格评估泛化能力。换成实际项目时我会先在每一个环节前问自己三个问题数据里有没有脏数据和空值训练集和测试集是否真的独立模型学到的是物理规律还是数据集规律这三个问题过滤下来大部分所谓的新项目核心工作其实就变成了数据整理和传感器部署。如果你想把这条路走得更深可以按这个顺序扩展先把一维CNN换成结合注意力机制的CNN-LSTM捕捉更长时间跨度的依赖再往工业场景走引入半监督学习用大量无标签数据辅助训练最后结合因果推断做根因分析从“判断有没有故障”进化到“定位哪个部件在什么工况下产生的故障”。这也是我认为深度学习在工业智能运维里最有价值的方向——不是替代老师傅而是把老师傅的耳朵和眼睛变成可复制、可部署的自动化能力。本文还有配套的精品资源点击获取