深度学习故障诊断模型实战:从代码跑通到二次开发
简介这份资源面向深度学习与工业故障诊断方向的研究者和工程师提供一套可直接运行的故障诊断模型代码与配套数据帮助读者跳过从零搭建系统的环节快速进入模型训练与实验验证。压缩包共63个文件、约57.74MB以mat与csv数据文件、py源码脚本为主辅以xml工程配置、h5训练权重、png模型结构图及readme说明覆盖数据预处理、一维卷积与LSTM诊断、训练验证及预测接口等模块。资源已有11425人学习下载具备较高的参考热度。读者可据此理解CNN、LSTM如何从传感器时序数据中自动提取特征并区分正常与故障状态掌握损失函数与优化器配置、验证集监控防过拟合、测试集泛化评估等完整流程并可在现有结构上调整参数或迁移到其他故障诊断任务兼顾学术研究与工程实践需求。1. 拿到一份故障诊断模型代码先别急着训练工业设备的振动信号里藏着很多信息轴承磨损、齿轮断齿、转子不平衡这些故障在时域波形上往往看不出明显差别但换到频域或者时频域就能分出眉目。这份基于深度学习的故障诊断模型代码和数据解决的就是这件事把采集到的传感器信号喂进网络输出故障类别。适合正在做设备预测性维护、想复现一个能跑通的诊断基线、或者准备在自家产线数据上做二次开发的工程师。代码和数据打包在一起省去了自己找公开数据集、对齐标签、搭训练脚本的前期折腾拿到手就能先跑一遍看效果再决定往哪个方向改。下面按「这项目能干什么 → 怎么跑起来 → 核心代码怎么读 → 怎么改」的顺序拆。2. 项目定位与技术选型为什么是深度学习做故障诊断2.1 传统方法卡在哪深度学习补了什么故障诊断不是新问题早期靠人工看频谱图、算峭度指标、做包络解调经验丰富的老师傅能判断个八九不离十。但这类方法有两个硬伤一是特征要人工设计换个设备、换个工况原来的特征可能就失效二是面对多传感器、多故障类型的组合场景规则越堆越多维护成本直线上升。深度学习把特征提取这一步交给网络自己学输入原始振动信号或者简单的频谱变换结果卷积层负责抓局部冲击特征循环层或者注意力机制负责建模时序依赖最后接分类头输出故障类别。这份代码走的就是这条路线数据侧提供的是带标签的振动信号样本模型侧是一个端到端的分类网络训练脚本、评估脚本、数据加载逻辑都齐了。选型上这类项目常见的做法是 CNN 打底因为振动信号本质是一维时序一维卷积在捕捉周期性冲击上效率很高参数量也比二维卷积小。如果数据里故障演化和时间强相关会再叠一层 LSTM 或者 GRU如果追求更轻量也有用残差块堆叠的。这份代码的结构大概率围绕「信号预处理 → 一维卷积特征提取 → 池化降维 → 全连接分类」展开具体层数和通道数要看配置文件。对二次开发来说重点不是记住某一层的参数而是搞清楚数据从文件到张量、从张量到标签的整条链路改起来才不会牵一发动全身。2.2 目录结构与文件职责拿到压缩包先别急着python train.py花五分钟把目录扫一遍后面调 bug 能省一半时间。这类项目的典型结构大致如下具体文件名以实际包内为准fault_diagnosis/ ├── data/ # 原始信号文件与标签 │ ├── train/ # 训练集样本 │ ├── test/ # 测试集样本 │ └── labels.csv # 文件名到故障类别的映射 ├── models/ # 网络定义 │ └── cnn1d.py # 一维卷积分类网络 ├── utils/ │ ├── dataset.py # 数据加载与预处理 │ └── metrics.py # 准确率、混淆矩阵等 ├── configs/ │ └── default.yaml # 超参数与路径配置 ├── train.py # 训练入口 ├── evaluate.py # 评估入口 └── requirements.txt # 依赖清单data/下放的是信号样本格式可能是.csv、.npy或者.mat取决于采集端怎么存。labels.csv是文件名和类别的对应表这一步很关键很多跑不起来的项目就卡在标签对不上。models/里是网络定义utils/dataset.py负责把文件读成张量、做归一化和切窗configs/把学习率、批大小、训练轮数这些集中管理避免散落在脚本里。train.py和evaluate.py是两个入口前者训练并保存权重后者加载权重出指标。先把这张地图在脑子里建好再往下看代码就不会迷路。2.3 环境准备与依赖安装这类项目对环境的依赖不算重常见组合是 Python 3.8 以上、PyTorch 或者 TensorFlow 二选一、numpy、pandas、scikit-learn 做指标和预处理。先建虚拟环境再装依赖别直接往系统 Python 里灌# 建独立环境避免和机器上其他项目打架 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 按依赖清单安装GPU 版本按自己的 CUDA 版本调整 pip install -r requirements.txtrequirements.txt里如果写的是torch不带版本号pip 会拉最新版可能和代码里的 API 对不上。稳妥做法是先看代码里import torch之后用了哪些接口再决定装哪个大版本。装完跑一句python -c import torch; print(torch.__version__, torch.cuda.is_available())确认框架能导入、GPU 能被识别。如果输出False但你有显卡多半是装成了 CPU 版回退重装对应 CUDA 的轮子即可。这一步看着琐碎但环境没对齐后面报的错全是玄学排查起来非常费劲。3. 跑通训练与评估从数据加载到指标输出3.1 数据加载与预处理链路故障诊断项目最容易翻车的地方不是模型是数据。信号文件读进来之后通常要做三件事切窗、归一化、划分训练测试集。切窗是因为一条长信号里故障特征可能只出现在局部按固定长度滑窗切样本能扩充数据量归一化是因为不同传感器量纲差异大不统一尺度网络很难收敛。下面这段是这类项目里dataset.py的典型写法import numpy as np import torch from torch.utils.data import Dataset class SignalDataset(Dataset): def __init__(self, file_list, label_map, window_size1024, stride512): self.samples [] self.labels [] for fname in file_list: # 读取单条信号假设存为 npy形状 (长度,) signal np.load(fname) # 滑窗切分stride 小于 window_size 可做数据增强 for start in range(0, len(signal) - window_size 1, stride): window signal[start:start window_size] # 按窗口做零均值单位方差归一化避免量纲干扰 window (window - window.mean()) / (window.std() 1e-8) self.samples.append(window) self.labels.append(label_map[fname]) def __len__(self): return len(self.samples) def __getitem__(self, idx): # 增加通道维一维卷积要求输入形状 (C, L) x torch.tensor(self.samples[idx], dtypetorch.float32).unsqueeze(0) y torch.tensor(self.labels[idx], dtypetorch.long) return x, y这段代码里几个参数值得留意。window_size决定单个样本覆盖多长时间的信号太小抓不到完整冲击周期太大又会让样本数变少、训练变慢常见取值在 1024 到 4096 之间按采样率折算成时间再定。stride控制窗口重叠程度设成window_size的一半是常见做法既能扩充样本又保留一定独立性。归一化用的是窗口内统计量而不是全局统计量这样每个样本尺度一致模型不会因为某段信号幅值大就偏科。unsqueeze(0)补的那一维是通道数一维卷积的输入要求是(batch, channel, length)少了这维会在卷积层直接报形状错误这是新手最常踩的坑之一。3.2 网络定义与训练循环模型部分一维卷积网络的骨架大致是「卷积 → 激活 → 池化」重复若干次最后全局池化或者展平接全连接。下面是一个能跑通的最小版本实际代码可能更深但结构逻辑一致import torch.nn as nn class CNN1D(nn.Module): def __init__(self, num_classes10): super().__init__() self.features nn.Sequential( # 第一层单通道输入32 个卷积核感受野覆盖局部冲击 nn.Conv1d(1, 32, kernel_size15, padding7), nn.BatchNorm1d(32), nn.ReLU(), nn.MaxPool1d(2), # 第二层通道翻倍提取更抽象的特征组合 nn.Conv1d(32, 64, kernel_size7, padding3), nn.BatchNorm1d(64), nn.ReLU(), nn.MaxPool1d(2), ) # 自适应池化把任意长度压成固定维度省去手算展平尺寸 self.gap nn.AdaptiveAvgPool1d(1) self.classifier nn.Linear(64, num_classes) def forward(self, x): x self.features(x) x self.gap(x).squeeze(-1) return self.classifier(x)kernel_size第一层给到 15 是比较大的感受野目的是让卷积核一次看到足够长的波形片段捕捉冲击的完整包络后面层逐步减小核尺寸、增加通道数是在做特征抽象。BatchNorm1d放在卷积和激活之间能稳住训练初期的梯度故障诊断数据往往类别不均衡这一层对收敛帮助明显。AdaptiveAvgPool1d(1)是个省事的设计不管前面特征图多长都压成一个数这样换window_size时不用改全连接层输入维度二次开发时改数据长度不会连锁报错。训练循环是标准套路但有几个细节决定能不能跑出结果import torch from torch.utils.data import DataLoader model CNN1D(num_classes10).cuda() criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue) for epoch in range(50): model.train() for x, y in train_loader: x, y x.cuda(), y.cuda() optimizer.zero_grad() loss criterion(model(x), y) loss.backward() optimizer.step() # 每个 epoch 后在测试集上验证防止过拟合 model.eval() # ... 评估逻辑见 evaluate.pylr1e-3配 Adam 是这类项目的默认起点如果 loss 震荡厉害就降到1e-4收敛太慢就适当加大。batch_size64在显存够的前提下比较稳太小梯度噪声大太大泛化可能变差。shuffleTrue必须开故障样本如果按类别顺序排列不打乱会让每个 batch 类别单一BatchNorm 的统计量失真。每个 epoch 后切model.eval()做验证别忘了验证完切回model.train()否则 BatchNorm 会继续用当前 batch 的统计量评估结果不可信。3.3 评估指标与结果解读训练完不能只看 loss 降没降故障诊断场景下准确率不够还得看混淆矩阵因为不同故障类别的误判代价不一样。evaluate.py里通常会算整体准确率、每类召回率有的还会出 F1。跑评估的命令大致是# 加载训练好的权重在测试集上出指标 python evaluate.py --checkpoint checkpoints/best.pth --data_dir data/test看结果时重点盯两件事一是混淆矩阵里哪些类别互相混淆比如内圈故障和外圈故障在频谱上接近模型分不清是正常的这时候要考虑加频域特征或者换更深的网络二是训练集和测试集准确率差距差太多说明过拟合回头加 dropout 或者数据增强。如果整体准确率卡在随机猜的水平先别怀疑模型回去查标签映射有没有错位、归一化有没有把信号压成常数这两个是血泪经验里最高频的翻车点。4. 核心模块拆解配置、权重保存与推理入口4.1 配置文件怎么读、怎么改把超参数抽到configs/default.yaml是这类项目的常见做法训练脚本启动时读进来改参数不用动代码# configs/default.yaml data: train_dir: data/train test_dir: data/test window_size: 1024 stride: 512 model: num_classes: 10 train: batch_size: 64 lr: 0.001 epochs: 50 checkpoint_dir: checkpoints读配置的代码一般长这样import yaml def load_config(pathconfigs/default.yaml): with open(path, r, encodingutf-8) as f: cfg yaml.safe_load(f) return cfg cfg load_config() # 用 cfg[train][lr] 这类方式取值改实验只动 yaml改配置时注意类型yaml 里0.001是浮点写成1e-3也能解析但写成字符串0.001传给优化器会报类型错误。num_classes必须和labels.csv里的类别数严格一致多一类少一类都会在损失计算时维度对不上。换数据集时优先改window_size和num_classes这两个是最容易连锁影响其他模块的参数。4.2 权重保存与断点续训训练几十个 epoch 中途断了很常见代码里一般会每个 epoch 存一次权重并保留验证集上最好的那个import os best_acc 0.0 for epoch in range(cfg[train][epochs]): # ... 训练与验证逻辑 val_acc evaluate(model, val_loader) if val_acc best_acc: best_acc val_acc # 只存最优权重避免磁盘被中间结果撑满 torch.save({ epoch: epoch, model_state: model.state_dict(), optimizer_state: optimizer.state_dict(), best_acc: best_acc, }, os.path.join(cfg[train][checkpoint_dir], best.pth))存字典而不是只存state_dict()是为了把优化器状态和 epoch 一起带上断点续训时能接着原来的动量继续跑而不是从头 warmup。best.pth只保留验证集最优的那份测试时加载它而不是最后一个 epoch 的权重能避开过拟合末期的性能回落。如果磁盘空间紧张可以只存model_state代价是续训时优化器要重新初始化收敛会慢一点。4.3 单条信号推理入口二次开发最常见的需求是拿训练好的模型对新采集的信号做推理所以项目里通常会有一个轻量的推理脚本或者函数def predict_signal(model, signal, window_size1024, devicecuda): model.eval() # 与训练时保持一致的归一化这一步漏了结果会完全错乱 signal (signal - signal.mean()) / (signal.std() 1e-8) # 截断或补齐到固定长度保证形状匹配 if len(signal) window_size: signal signal[:window_size] else: signal np.pad(signal, (0, window_size - len(signal))) x torch.tensor(signal, dtypetorch.float32).unsqueeze(0).unsqueeze(0).to(device) with torch.no_grad(): logits model(x) prob torch.softmax(logits, dim1) return prob.argmax(dim1).item(), prob.max().item()这里两次unsqueeze分别补 batch 维和通道维少一个都会报形状错误。归一化必须和训练时用同一套逻辑训练用窗口内统计量推理也得用窗口内统计量混用全局统计量会让输入分布偏移预测结果看着有模有样其实全是错的。torch.no_grad()关掉梯度计算推理时省显存也提速。返回类别索引和置信度置信度低的时候可以人工复核这在产线部署时是个实用的兜底。5. 避坑与常见问题排查5.1 训练 loss 不降或者直接变 NaN现象是跑了几十个 batchloss 一直在 2.3 附近晃十分类随机猜的水平或者几个 step 后直接 NaN。原因通常有三个学习率太大、输入没归一化、标签越界。先查归一化打印一个 batch 的x.mean()和x.std()如果均值不是接近 0、标准差不是接近 1说明预处理链路有问题。再查标签y.max()必须小于num_classes标签从 1 开始编号而类别数按 0 开始算就会越界。最后把学习率降到1e-4试一轮Adam 对初始学习率比较敏感1e-3在深一点的网络上容易炸。5.2 验证集准确率远低于训练集训练集 99%、测试集 60%典型的过拟合。故障诊断数据量往往不大网络又容易记住样本。解决顺序是先加数据增强滑窗重叠、加轻微噪声、随机幅值缩放再在网络里插Dropout最后考虑减小模型容量。别一上来就砍层数很多时候是数据不够而不是模型太大。另外检查训练集和测试集是不是来自同一批采集如果测试集工况和训练集差异大那属于分布偏移不是过拟合得靠域适应手段加 dropout 没用。5.3 换数据集后形状报错现象是RuntimeError: Expected input ... to have ... channels或者全连接层维度不匹配。原因通常是新数据的采样率或窗长和原来不同导致特征图长度变化。如果网络里用了AdaptiveAvgPool1d长度变化能自适应报错多半出在通道数——新数据如果是三轴传感器输入通道要从 1 改成 3第一层卷积的in_channels也得跟着改。改完记得同步改dataset.py里的unsqueeze逻辑三轴数据要堆成(3, L)而不是(1, L)。5.4 GPU 显存不够现象是CUDA out of memorybatch 稍微大一点就崩。先降batch_size从 64 降到 32 甚至 16这是最快见效的。如果降了还不行检查是不是在验证阶段忘了torch.no_grad()验证时建了计算图会白占一大块显存。再不行就开混合精度训练torch.cuda.amp能把显存占用压下来一截对这类中小网络收益明显。最后才考虑换小模型因为改模型结构会牵动超参数成本最高。5.5 推理结果和训练时对不上现象是训练时评估准确率很高单独拿一条信号推理却总是错。八成是推理时的预处理和训练不一致训练做了滑窗切分和窗口内归一化推理时直接拿整条信号归一化统计量完全不同。解决办法是把预处理逻辑抽成一个函数训练和推理都调它别各写一份。另一个可能是model.eval()漏了BatchNorm 在推理时用当前样本的统计量结果会飘。这两条排查完基本能对上。6. 二次开发换数据、换网络、上部署的改造思路拿到这份代码真正有价值的是把它改成能跑自家设备数据的版本。改造顺序建议从数据侧开始因为模型换起来成本高、收益慢数据对齐了基线模型往往就够用。第一步是把自家采集的信号整理成和data/一样的目录结构标签表按文件名映射类别名换成自己的故障类型。如果采样率和原来不同改window_size让单个窗口覆盖的时间长度和原来接近比如原来 1024 点对应 0.1 秒新数据采样率翻倍就改成 2048。改完先别动模型跑一遍训练看基线准确率这个数就是你后续所有改动的参照系。第二步是特征层面的增强。纯时域输入对某些故障不敏感可以在dataset.py里加一路频域特征比如对窗口做 FFT 取幅值谱和原始信号拼成双通道输入第一层卷积的in_channels改成 2。这一步改动小、见效快尤其是轴承类故障频域里的特征频率比时域冲击更稳定。代码上就是在__getitem__里多算一个np.abs(np.fft.rfft(window))截断到和时域等长后np.stack起来。注意频域幅值量纲和时域差很多两路要各自归一化再拼否则网络会偏向幅值大的那一路。第三步才是换网络。如果基线准确率已经不错别为了换而换如果卡在某个瓶颈优先试残差连接和注意力机制。残差块能加深网络而不退化注意力比如 SE 模块或者简单的通道注意力能让网络自己权衡哪些卷积核更重要。改造时保持输入输出接口不变只替换models/里的网络定义训练脚本一行不用动这样 A/B 对比才干净。下面是个带通道注意力的残差块示意class SEBlock(nn.Module): def __init__(self, channels, reduction8): super().__init__() # 全局池化把每个通道压成一个数再学一组权重 self.fc nn.Sequential( nn.AdaptiveAvgPool1d(1), nn.Flatten(), nn.Linear(channels, channels // reduction), nn.ReLU(), nn.Linear(channels // reduction, channels), nn.Sigmoid(), ) def forward(self, x): w self.fc(x).unsqueeze(-1) # (B, C, 1) return x * w # 按通道加权reduction8控制中间层压缩比例太小起不到降参作用太大信息损失严重8 或 16 是常见取值。把这个块插在卷积之后、池化之前通道数对得上就能直接用。最后是部署侧的验证。训练脚本跑通不代表能上线至少要做两件事一是用torch.jit.trace或者onnx导出模型确认导出后的输出和原模型一致误差在1e-5以内二是拿一批没参与训练的真实信号做端到端测试从读文件到出类别走完整链路别只测模型前向。我一般会在导出后写个小脚本随机抽 20 条测试样本对比原模型和导出模型的预测类别全一致才往下走。从那以后我每次改完模型结构都强制走一遍「训练 → 导出 → 端到端对比」这三步宁可多花十分钟也不想到部署时才发现形状对不上。希望这份拆解帮到你先把基线跑起来再按自己的数据一步步改。本文还有配套的精品资源点击获取