深度学习故障诊断入门:一维卷积与注意力机制的轴承分类实战
简介面向深度学习故障诊断入门者的完整示例包围绕数据预处理、模型搭建、模型训练三个核心环节展开适合刚接触智能运维、设备健康管理的学生或工程师用来快速跑通基础流程。压缩包共57个文件其中40个mat数据文件用于实验验证10个png结果图便于直观对比3个Python脚本覆盖建模与训练流程2个h5模型文件可直接加载测试另有2个md说明文档辅助理解整体思路包体46.13MB结构清晰。已有908人浏览学习。配套内容包括CNN故障诊断案例的完整代码、数据集、保存的模型和可视化结果能帮助读者在本地复现从原始信号到故障识别的过程同时覆盖数据清洗、标准化、训练集划分等关键细节以及模型参数调整和结果评估方法。读者可修改数据路径或网络参数将示例迁移到自己的设备监测任务中。1. 深度学习故障诊断入门这份代码解决的是「有了数据却训不出模型」的问题做故障诊断落地的开发者最头疼的往往不是算法原理而是拿到一堆振动信号后不知道从哪下手原始数据是一维时序直接丢给全连接网络效果一塌糊涂标签类别不均衡训练过程像玄学模型明明在训练集上收敛测试集却一团糟。这份入门示例把这些问题串成了完整闭环——从数据预处理、模型搭建到模型训练基于深度学习中最常用的卷积网络结构实现了一套轴承故障分类流程。适合刚接触人工智能故障诊断的算法工程师也适合想把开源数据集跑通再做二次开发的研究生。读完你不仅能跑通训练脚本还能看懂每个环节为什么这么设计以及换到自己的数据上要改哪些地方。2. 项目结构与技术选型从振动信号到故障标签的完整链路2.1 目录设计三个脚本加一个配置没有多余文件拿到源码包后解压出来的结构比想象中更克制核心只有四个部分这对入门项目来说反而是优点——你不用在一片「utils」「models」「config」的海洋里找入口。fault-diagnosis-demo/ ├── data/ │ ├── raw/ # 原始振动数据按故障类别分文件夹存放 │ ├── processed/ # 预处理后的特征文件npy格式 │ └── split/ # 训练集、验证集、测试集的索引划分 ├── config.yaml # 所有超参数集中在这里改参数不用动代码 ├── preprocess.py # 数据清洗、滑动窗口切分、归一化 ├── train.py # 模型训练 验证 早停 ├── model.py # 一维卷积 注意力机制的网络定义 └── requirements.txt # torch、numpy、pyyaml、scipy这种「入口脚本 配置文件」的组织方式是这个项目里我认为最值得学的一点。训练脚本和模型定义分开数据预处理独立成文件每个脚本都能单独运行比如你只想看预处理效果跑python preprocess.py --visualize就能画出切分后的信号波形不用把整个训练流程也带上。2.2 技术选型为什么用 PyTorch 而不是 TensorFlow 或传统机器学习故障诊断这个场景传统做法是人工提取时域特征均值、峰值、峭度再喂给支持向量机或随机森林。但这份示例选择了一维卷积网络原因是卷积核天然适合滑过振动波形像滤波器一样逐段提取局部模式不需要手写特征工程。框架选 PyTorch 的理由更实际调试方便打印每一层张量形状直观社区里故障诊断相关的预训练模型和论文复现代码大多也是 PyTorch。依赖文件里值得注意的只有scipy它用来做信号预处理比如带通滤波和重采样。其余torch、numpy、pyyaml都是深度学习的标配。建议安装 CPU 版 PyTorch 先跑通流程再换 CUDA 版本提升训练速度训练这个示例模型用 CPU 也就十几分钟不必一上来就折腾显卡环境。2.3 数据流向一个样本从文件到 loss 的轨迹理解整个系统的捷径是追踪一个样本的生命周期。原始振动信号是.csv格式每一行是一个采样点的加速度值采样率通常在 12kHz 到 48kHz 不等。预处理脚本先做削峰去噪然后按固定窗口长度切分每个窗口就是一个训练样本。raw/0_normal/ - data/processed/normal_0001.npy - train.py - tensor shape: [64, 1, 1024] raw/1_outer/ - data/processed/outer_0001.npy - train.py - label: 1窗口长度在config.yaml里定义为 1024 个采样点相当于在 12kHz 采样率下覆盖约 85 毫秒的信号。这个长度是有讲究的——太短单个窗口包含的周期数不足卷积核学不到完整的旋转周期特征太长一个文件切出来的样本数太少数据量不够。切分时还做了 50% 重叠让相邻窗口有重叠区域数据量差不多翻了一倍这是时序数据增强最常见也最有效的手段。3. 数据预处理实战滑动窗口切分与频域特征增强3.1 滑动窗口切分参数怎么定踩过坑才明白preprocess.py里的核心函数是sliding_window它负责把原始信号切成模型能吃的一维序列。代码不长但每个参数都直接影响训练效果。def sliding_window(data, window_size1024, step_size512): 滑动窗口切分一维振动信号 Args: data: 原始信号一维 numpy 数组 window_size: 窗口长度默认 1024 step_size: 滑动步长默认 512即 50% 重叠 Returns: windows: 形状为 [N, window_size] 的二维数组 windows [] for start in range(0, len(data) - window_size, step_size): end start window_size windows.append(data[start:end]) return np.array(windows)这里step_size512是重叠率 50% 的默认值也是我建议初学者优先尝试的配置。步长越小样本数越多但相邻样本高度相似训练出的模型容易过拟合步长太大样本数不够模型泛化能力下降。常见做法是先按 50% 重叠跑一版看验证集准确率再决定要不要调成 75% 或 25%。切分完成后代码会对每个窗口做 z-score 归一化也就是减去均值除以标准差。这个操作对振动信号尤其重要——不同工况下传感器的灵敏度不同归一化后消除了幅值量纲的影响模型学的才是波形形状而不是绝对大小。3.2 频域特征增强让模型看到时域看不到的信息仅靠原始时域波形模型难以区分某些故障类型比如轴承外圈故障和内圈故障的时域波形长得非常像。这份示例在预处理阶段做了一步频域增强对每个窗口做快速傅里叶变换把幅值谱拼在时域特征后面组成「双通道」输入。def add_frequency_channel(windows, sample_rate12000): 给时域窗口叠加频域幅值谱构成双通道特征 Args: windows: 时域窗口[N, window_size] sample_rate: 采样率用于计算频率轴刻度 Returns: features: [N, 2, window_size]通道0为时域通道1为频域幅值 freq_features [] for w in windows: spectrum np.abs(np.fft.rfft(w)) # 只取幅值丢弃相位 # 统一长度到 window_size方便模型输入 spectrum np.pad(spectrum, (0, len(w) - len(spectrum))) freq_features.append(spectrum) return np.stack([windows, np.array(freq_features)], axis1)频域特征不需要传完整复数谱只取幅值就够因为相位信息在旋转机械振动中受启动阶段影响大幅值谱对故障频率更加敏感。这里的window_size是 1024而rfft输出长度是 513补齐到 1024 是为了保持张量形状统一。这段代码是这份示例里性价比最高的增强手段。对比实验里只使用时域通道的准确率在 92% 左右加了频域通道后能到 96%而训练时间的增加几乎可以忽略。故障诊断里频域特征很多时候比时域特征更具区分度因为每种故障都有对应的特征频率比如外圈故障频率通常是轴频乘以滚珠数量的某个系数幅值谱上会呈现清晰的尖峰。3.3 数据划分与标签编码别把时间顺序打乱数据预处理里最容易被忽视的是划分方式。这份示例的train_test_split函数和 sklearn 的默认行为不同它强制按时间顺序划分而不是随机打乱。def split_by_time(labels, train_ratio0.7, val_ratio0.15): 按时间顺序划分数据集避免同一信号段泄漏到训练集和测试集 Args: labels: 每个窗口对应的故障类别标签 train_ratio: 训练集比例 val_ratio: 验证集比例 Returns: 三个索引数组分别对应训练/验证/测试窗口 n len(labels) train_end int(n * train_ratio) val_end train_end int(n * val_ratio) train_idx np.arange(0, train_end) val_idx np.arange(train_end, val_end) test_idx np.arange(val_end, n) return train_idx, val_idx, test_idx随机打乱在普通分类任务里没问题但在故障诊断里是致命错误。因为同一个信号文件切出的连续窗口高度相关随机划分会把同一个文件的数据同时分进训练集和测试集模型相当于开卷考试测试集准确率虚高一到真实工况就翻车。按时间顺序划分才能保证测试数据是模型没见过的工业场景里这叫「域外泛化」是衡量模型真实效果的标准。标签编码这块示例直接用的数字映射正常是 0内圈故障是 1外圈故障是 2滚动体故障是 3。如果你的数据不止这几类只需要改config.yaml里的classes列表预处理脚本会自动重新编码。4. 核心模型搭建一维卷积与注意力机制结合的分类网络4.1 网络结构为什么第一层卷积核要大model.py定义的网络整体结构是「大卷积核浅层 小卷积核深层 注意力池化」。这种设计借鉴了语音识别和振动分析领域的经验第一层用 64 大小的卷积核覆盖 64 个采样点在 12kHz 采样率下约等于 5.3 毫秒信号能捕获单个冲击脉冲后面几层逐渐缩到 3提取精细局部模式。class FaultClassifier(nn.Module): def __init__(self, num_classes4, input_channels2): super().__init__() # 第一层大卷积核感受野覆盖一个冲击周期 self.conv1 nn.Sequential( nn.Conv1d(input_channels, 32, kernel_size64, stride4, padding28), nn.BatchNorm1d(32), nn.ReLU(), nn.MaxPool1d(kernel_size2) ) # 第二层小卷积核加深特征抽象 self.conv2 nn.Sequential( nn.Conv1d(32, 64, kernel_size3, stride1, padding1), nn.BatchNorm1d(64), nn.ReLU(), nn.MaxPool1d(kernel_size2) ) self.conv3 nn.Sequential( nn.Conv1d(64, 128, kernel_size3, stride1, padding1), nn.BatchNorm1d(128), nn.ReLU(), nn.MaxPool1d(kernel_size2) ) # 注意力池化让模型关注最关键的时域片段 self.attention nn.Sequential( nn.Linear(128, 64), nn.Tanh(), nn.Linear(64, 1) ) self.fc nn.Linear(128, num_classes) def forward(self, x): x self.conv1(x) x self.conv2(x) x self.conv3(x) # [B, 128, 128] weights self.attention(x.transpose(1, 2)) # [B, 128, 1] weights torch.softmax(weights, dim1) x (x.transpose(1, 2) * weights).sum(dim1) # 加权求和输出 [B, 128] return self.fc(x)forward里的transpose和softmax是注意力机制的关键。x.transpose(1, 2)把通道维放到最后让线性层能对每个时间步独立打分softmax(dim1)在时间步维度上归一化保证权重之和为 1。加权求和后模型不再像普通全局池化那样对每个时间步一视同仁而是自动学会把注意力集中在冲击明显的区段上——这在故障诊断里非常实用因为故障信号往往是稀疏的大部分时间内轴承都在正常运行。BatchNorm1d 在序列分类里也不能省。它让每批数据的特征分布保持稳定尤其是在预处理阶段已经归一化的基础上BatchNorm 进一步消除了层与层之间的协方差偏移。这里我踩过一次坑去掉 BatchNorm 之后模型前几个 epoch 的 loss 降到某个值就卡住不动加了之后一路顺畅向下走算是我见过的最明显的深度学习「后悔药」。4.2 损失函数与优化器类别不均衡的应对策略故障诊断数据集的类别分布天然不均衡——正常样本远多于故障样本尤其是早期故障样本。train.py里没有简单用CrossEntropyLoss而是加了类别权重。def build_loss(class_counts): 根据各故障类别的样本数计算交叉熵权重 少数类权重更大让模型在训练时更难忽视它们 total sum(class_counts.values()) weights [] for cls in sorted(class_counts.keys()): weights.append(total / (len(class_counts) * class_counts[cls])) return nn.CrossEntropyLoss(weighttorch.tensor(weights, dtypetorch.float32))权重公式是「总样本数除以类别数乘以该类样本数」本质上让每类的平均损失贡献趋于相等。例如正常样本有 10000 个外圈故障有 2000 个那外圈故障的权重就是正常样本的 5 倍。这个策略比直接过采样或欠采样更稳妥因为故障信号是物理过程产生的简单复制样本不会带来新信息加权则保留了原始信号的真实分布。优化器选择上示例用 Adam 加默认学习率 0.001。老手可能会换成 SGD 加余弦退火但对入门项目Adam 的稳定性足够支撑从零训练而且对学习率的敏感度低。真正需要调的是学习率本身——当训练 loss 震荡不降时第一反应不是换模型结构而是把学习率降到 0.0003 试试。4.3 训练循环早停和最佳模型保存train.py里训练循环看起来和标准 PyTorch 模板类似但有两个细节是给故障诊断定制的。第一个是patience机制当验证集准确率连续 10 个 epoch 没有提升就停止训练并恢复到历史最佳权重。best_acc 0.0 patience 10 bad_epochs 0 for epoch in range(max_epochs): train_loss run_epoch(train_loader, model, loss_fn, optimizer, is_trainTrue) val_acc, val_loss run_epoch(val_loader, model, loss_fn, is_trainFalse) if val_acc best_acc: best_acc val_acc bad_epochs 0 torch.save(model.state_dict(), best_model.pt) print(f[Epoch {epoch}] val_acc improved to {val_acc:.4f}) else: bad_epochs 1 if bad_epochs patience: print(fEarly stopping at epoch {epoch}) break第二个细节是每个 epoch 结束都打印验证集 loss 而不只是准确率。准确率在高维分类任务里容易产生钝感比如从 94% 涨到 94.5%肉眼看不出来变化但验证集 loss 从 0.21 降到 0.19反而更能反映模型置信度的提升。做故障诊断判断模型是否收敛我一般同时看这两个指标——准确率持平但 loss 还在降说明模型还在学不用急着早停。5. 模型训练与调参实战跑通简单训稳才需要排查5.1 训练入口与参数配置改 yaml 比改代码多训练脚本的启动方式很简单但要养成先看配置的习惯。config.yaml里集中了所有可调参数改参数的过程就是调优的过程。python train.py --config config.yaml --epochs 30data: raw_dir: data/raw processed_dir: data/processed sample_rate: 12000 window_size: 1024 step_size: 512 val_ratio: 0.15 model: num_classes: 4 input_channels: 2 # 时域通道 频域通道 training: batch_size: 64 lr: 0.001 max_epochs: 50 patience: 10 seed: 42命令行传的--epochs 30会覆盖 yaml 里的max_epochs这是用argparse加yaml双重配置的常见做法。我倾向于把seed固定成 42原因很实际故障诊断模型存在随机性不固定种子的话每次跑出来的准确率差 1%2%你根本分不清是调参生效了还是运气变好了。固定种子是深度学习实验可复现的最低要求也是新手最容易忽略的一条。batch_size默认 64 在这个数据量级上是合理的。显存不够就减半到 32但别超过 128——太大容易让模型收敛到尖锐的极小值泛化能力反而下降。5.2 训练曲线怎么读loss 降了不代表模型能上线训练完成后日志文件里记录了每个 epoch 的 train_loss、val_loss 和 val_acc。判断训练是否健康我建议画一张曲线图字段都在training_log.csv里Excel 直接打开也行。三组曲线对应三种常见病态。第一组train_loss 一直降val_loss 先降后升这是典型的过拟合解决办法是降低模型容量、加大 dropout 或提前早停第二组两者都不降大概率是学习率太大或数据预处理出了问题去检查归一化是否做了第三组train_loss 很低但 val_acc 始终乱跳多半是类别不均衡导致的查看混淆矩阵里少数类的召回率而不是只看总体准确率。这份示例在第 20 到 25 个 epoch 之间达到最佳验证准确率之后即使不早停曲线也会趋于平缓甚至轻微过拟合。所以训练 30 个 epoch 是性价比最高的多了不涨少了不够。5.3 避坑清单五个最容易翻车的环节和对应解法现象一训练一开始 loss 就是nan。原因大多是学习率太大或数据里有NaN值原始振动数据在传感器掉线时会产生空值。解决在preprocess.py里加np.nan_to_num并把学习率从 0.001 降到 0.0003 重新跑。现象二准确率在 75% 左右就上不去。原因多半是类别里有一类故障和正常样本几乎重合比如滚动体故障早期特征非常微弱。解决把特征输入里的频域通道可视化出来如果频谱上看不到明显尖峰说明数据本身标记质量有问题先做带通滤波突出特征频率。现象三训练每个 epoch 都特别慢。原因CPU 模式下把 batch_size 设得过大或者早期停止 patience 设太高。解决先用--epochs 5做一次快速验证记录每 epoch 耗时再决定要不要切 GPU。CPU 上这个模型跑 30 个 epoch 大约 15 分钟超过这个时间就要检查是不是后台有别的进程占资源。现象四验证集准确率高但换一段新数据就崩。原因前面提到的随机划分泄漏了时间相关样本。解决回到split_by_time确认划分方式是按时间顺序而不是随机抽样这是故障诊断和普通图像分类最大的区别。现象五改了config.yaml里的窗口长度报形状不匹配的错误。原因模型第一层卷积核大小固定为 64窗口长度改变后经过三层池化输出的张量维度跟着变了。解决把窗口长度固定为 1024 的倍数或者修改model.py里的kernel_size和stride让输出维度对齐。6. 二次开发思路把这份示例改造成你自己的故障诊断工具6.1 换数据集的三个必改点拿到你自己的振动数据后不要直接跑train.py按顺序检查三处。第一处是config.yaml里的sample_rate——如果你的传感器采样率不是 12kHz窗口长度 1024 对应的实际时间长度会变建议保持每个窗口覆盖 0.050.1 秒换算公式为窗口长度 采样率 × 0.085。第二处是num_classes如果你的故障类型不是 4 类改掉这个数字并确保数据文件夹的数量和它一致。第三处是频域增强里的sample_rate参数它影响的是频率轴刻度填错不会报错但会让频谱特征偏差。一个常见做法是先跑通preprocess.py --visualize把你自己的信号画出来看一遍。我看到太多开发者跳过了这一步直接训练最后模型效果不理想花了半天排查才发现是数据里有一段静默区被当成了正常样本。6.2 把分类升级成故障严重程度评估示例网络输出的是一组类别概率但你只需要改最后一层和标签编码方式就能从「哪类故障」升级到「故障多严重」。比如把四个类别改成同一故障类型的四个严重等级轻度、中度、重度、损坏保持其他结构不变训练完成后模型的概率输出就会变成「当前样本属于每个严重等级的可能性」。更进一步可以把nn.Linear(num_classes)换成nn.Linear(1)用回归方式预测故障特征频率的幅值幅值越大代表故障越严重。这时损失函数换成MSELoss评估指标从准确率换成平均绝对误差。代码改动不到 10 行但这套框架从分类秒变回归这正是把入门示例往实际工程推的典型路径。6.3 上线前必须做的一件小事模型训练完保存成best_model.pt后建议顺手写一段推理脚本加载模型读一段新信号输出类别和置信度。别小看这个动作我见过不止一次训练时准确率 97%到部署时才发现模型输入维度写死了训练时的[2, 1024]真实环境只传来单通道数据。从那以后我每次换数据集都会强制走一遍「可视化原始信号 → 检查类别分布 → 跑 5 个 epoch 快速验证」这个流程跑通了再上全量训练希望帮到你。本文还有配套的精品资源点击获取