ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

深度学习故障诊断模型实战:从代码跑通到二次开发

深度学习故障诊断模型实战:从代码跑通到二次开发

简介:这份资源面向深度学习与工业故障诊断方向的研究者和工程师,提供一套可直接运行的故障诊断模型代码与配套数据,帮助读者跳过从零搭建系统的环节,快速进入模型训练与实验验证。压缩包共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.txt

requirements.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_size=1024, stride=512): 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], dtype=torch.float32).unsqueeze(0) y = torch.tensor(self.labels[idx], dtype=torch.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_classes=10): super().__init__() self.features = nn.Sequential( # 第一层:单通道输入,32 个卷积核,感受野覆盖局部冲击 nn.Conv1d(1, 32, kernel_size=15, padding=7), nn.BatchNorm1d(32), nn.ReLU(), nn.MaxPool1d(2), # 第二层:通道翻倍,提取更抽象的特征组合 nn.Conv1d(32, 64, kernel_size=7, padding=3), 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_classes=10).cuda() criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) 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.py

lr=1e-3配 Adam 是这类项目的默认起点,如果 loss 震荡厉害就降到1e-4,收敛太慢就适当加大。batch_size=64在显存够的前提下比较稳,太小梯度噪声大,太大泛化可能变差。shuffle=True必须开,故障样本如果按类别顺序排列,不打乱会让每个 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(path="configs/default.yaml"): with open(path, "r", encoding="utf-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_size=1024, device="cuda"): 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, dtype=torch.float32).unsqueeze(0).unsqueeze(0).to(device) with torch.no_grad(): logits = model(x) prob = torch.softmax(logits, dim=1) return prob.argmax(dim=1).item(), prob.max().item()

这里两次unsqueeze分别补 batch 维和通道维,少一个都会报形状错误。归一化必须和训练时用同一套逻辑,训练用窗口内统计量,推理也得用窗口内统计量,混用全局统计量会让输入分布偏移,预测结果看着有模有样其实全是错的。torch.no_grad()关掉梯度计算,推理时省显存也提速。返回类别索引和置信度,置信度低的时候可以人工复核,这在产线部署时是个实用的兜底。

5. 避坑与常见问题排查

5.1 训练 loss 不降或者直接变 NaN

现象是跑了几十个 batch,loss 一直在 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 memory,batch 稍微大一点就崩。先降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, reduction=8): 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 # 按通道加权

reduction=8控制中间层压缩比例,太小起不到降参作用,太大信息损失严重,8 或 16 是常见取值。把这个块插在卷积之后、池化之前,通道数对得上就能直接用。

最后是部署侧的验证。训练脚本跑通不代表能上线,至少要做两件事:一是用torch.jit.trace或者onnx导出模型,确认导出后的输出和原模型一致,误差在1e-5以内;二是拿一批没参与训练的真实信号做端到端测试,从读文件到出类别走完整链路,别只测模型前向。我一般会在导出后写个小脚本,随机抽 20 条测试样本,对比原模型和导出模型的预测类别,全一致才往下走。从那以后我每次改完模型结构,都强制走一遍「训练 → 导出 → 端到端对比」这三步,宁可多花十分钟,也不想到部署时才发现形状对不上。希望这份拆解帮到你,先把基线跑起来,再按自己的数据一步步改。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进