基于CNN-LSTM的轴承故障诊断系统:Python实现与工程实践
2026/8/31 22:08:59 网站建设 项目流程

简介:本资源是一套面向机械故障诊断初学者与AI实践者的完整Python项目,聚焦滚动轴承三类典型损伤(外环、内环、滚动体)在九种规格下的智能识别任务,适用于工业设备状态监测课程设计、毕业设计及科研入门。压缩包共30个文件,含4个核心训练/测试脚本(.py)、2个预训练模型(.pth)、9个故障CSV样本数据、5张关键流程图(.png)及详细说明文档(.md),辅以MATLAB数据接口(.m)、Excel结果记录(.xlsx)等,整体56.3MB,结构清晰便于分模块学习与复现。已有51人下载学习,资源提供从原始振动信号预处理、重叠采样策略、CNN-LSTM联合建模到分类可视化的一站式实现,包含可直接运行的demo.ipynb、模型加载与推理脚本、工具函数封装及备份文件,显著降低复现实验门槛。 干设备故障诊断这几年,我最大的体会就是——真正能落地的模型,往往是那些结构不复杂、但每个环节都经得起推敲的模型。今天想分享的这个项目,正是基于CNN-LSTM的轴承故障诊断系统Python实现,代码、预训练模型权重和项目文档都完整打包好了。它解决的是工业场景里最常见的一个问题:拿到一段轴承振动信号,如何快速判断设备是否正常,以及故障出在内圈、外圈还是滚动体。

整个项目从数据预处理、CNN-LSTM模型训练,到模型导出和推理封装,是一条完整的链路。适合搞设备监测的研究生、想入门工业AI的工程师,或者正在做预测性维护的小团队参考。哪怕你之前没跑过深度学习项目,照着文档一步步来,也能把模型训练起来,再加载我提供的预训练模型权重直接跑推理。下面我把整个系统的设计思路、核心代码、训练细节和踩坑过程都拆开讲清楚。

1. 项目背景与整体设计思路

1.1 为什么说轴承诊断是工业AI里最容易出成果的方向

轴承是旋转机械里最脆弱的部件之一,电机、泵、风机、压缩机、机床主轴,几乎处处都有它的身影。机械故障里轴承故障占比很高,而且故障一旦恶化,轻则停机停产,重则引发连锁损坏。正因为这样,轴承状态监测一直是设备管理的刚需,也是预测性维护落地时优先级最高的切入点。

振动信号是轴承故障诊断里最常用的数据源,原因很简单:振动传感器安装方便、成本低、不破坏设备结构,而且轴承的故障特征在频域里表现非常明显。内圈故障、外圈故障、滚动体故障,各自有对应的特征频率,传统做法是人工做包络谱分析、看特征频率峰值,这要求工程师有丰富的现场经验和信号处理功底。

深度学习路线就不一样了,把一段振动波形直接喂给模型,模型自己提取特征、自己分类。这极大降低了对人工特征工程的依赖,也让“端到端”的故障诊断成为可能。我选择做这个项目,就是因为它既贴合工业实际需求,又能在模型层面做出可复现、可扩展的技术方案,非常适合作为工业AI的入门到进阶的项目。

1.2 技术选型:为什么是CNN+LSTM,而不是纯CNN或纯LSTM

这个项目最关键的技术决策,就是模型结构选择。很多人刚开始会纠结:振动信号不是时序数据吗?那直接用LSTM不就行了?或者CNN在图像上那么强,直接堆CNN也行?我实测下来,这两种思路单独用都不够理想。

先看纯CNN。CNN擅长提取局部特征,1D卷积在振动信号上可以理解为滑动窗口内的滤波器,能捕获局部冲击、局部波动模式,但它对时间顺序的长程依赖建模能力偏弱。轴承振动信号里,故障冲击往往是周期性的,前一个冲击和后一个冲击之间存在时间关联。纯CNN很难显式建模这种跨时间步的依赖关系,它更擅长的是“这一段局部像不像故障模式”。

再看纯LSTM。LSTM天然适合时序建模,能记住长距离的依赖,但它的问题是特征提取效率低、收敛慢。直接把原始波形丢给LSTM,每个时间步都是原始采样点,序列动辄上千步,训练效率很打折扣。而且工业信号里SNR往往不高,原始波形里噪声占主导,LSTM这种逐点建模的方式很容易被噪声带偏。

我的做法是两级结构:先用CNN做局部特征提取和降维,把原始振动波形压缩成更紧凑、更高层语义的特征序列,再把这组特征序列送入LSTM建模时间依赖。这样CNN负责“看局部”,LSTM负责“串全局”,各司其职。对应到代码里,就是先经过若干1D卷积池化层,把输入从1024个采样点压缩到几十个特征帧,每个特征帧是32维或64维特征向量,LSTM在这条压缩后的特征序列上建模。

这个组合还有一个额外优势:模型更稳。纯CNN在变工况、变负载下很容易过拟合到转速频率上,LSTM加入后对时序演化模式更敏感,泛化能力明显提升。论文里很多对比实验也验证了这点,CNN-LSTM在轴承故障诊断上的综合表现普遍优于单一结构。

1.3 系统整体架构与代码目录设计

项目不是只写一个训练脚本就完了,还包括源码、预训练模型和项目文档,所以代码组织从一开始就要模块化。我最终的目录结构是这样:

bearing_diagnosis/ ├── checkpoints/ # 预训练模型权重存放目录 │ └── best_model.pth ├── data/ # 原始数据与切分后的样本 │ ├── raw/ # CWRU或其他数据集的原始CSV │ └── processed/ # 预处理后的npy样本文件 ├── docs/ # 项目文档 │ ├── 数据说明.md │ ├── 训练复现指南.md │ └── 推理部署说明.md ├── models/ │ └── cnn_lstm.py # CNN-LSTM模型定义 ├── utils/ │ ├── data_loader.py # 数据加载与预处理 │ ├── metrics.py # 评价指标工具 │ └── visualize.py # 波形与混淆矩阵可视化 ├── train.py # 训练入口脚本 ├── predict.py # 单条数据推理脚本 └── app.py # 简易可视化界面

这样设计有几点考虑。models目录只放模型结构,不做数据处理,好处是换模型时不动其他代码;utils里放数据和评估工具,train.py和predict.py分离,保证训练和推理逻辑互不干扰;checkpoints单独拎出来,是为了方便后续做迁移学习,也方便直接加载预训练模型。项目文档放在docs里,所有复现步骤都有据可查,这也是这个项目区别于一般代码仓库的地方——拿到手不是一头雾水,而是有清晰的使用路径。

2. 数据准备与预处理要点

2.1 数据集选择与加载方式

轴承故障诊断领域最常用的公开数据集是凯斯西储大学(CWRU)轴承数据集。这个数据集采集了正常状态、内圈故障、外圈故障、滚动体故障四种状态下的振动信号,每种故障又有不同损伤直径(0.007英寸、0.014英寸、0.021英寸等),采样频率有12kHz和48kHz两档,还区分驱动端、风扇端,数据非常丰富,复现论文基本都从它开始。

我在项目里按12kHz采样频率、驱动端的设置来取数,一段原始信号长度约10秒,也就是12万个采样点。如果直接把整段序列丢给模型,显存和训练时间都不可控,所以必须做滑窗切分。我把窗口长度设为1024个采样点,步长512,这样每段原始信号能切出几百个样本,数据量足够训练。

这里有个需要注意的细节:滑窗切分时,窗口和步长的选择会影响样本数和信息冗余度。窗口太短,单个窗口内可能捕捉不到完整的故障冲击周期;窗口太长,样本数减少,而且LSTM的序列长度会变长,训练变慢。1024在12kHz采样率下约等于85ms,对一个旋转频率30Hz左右的轴承来说,足够覆盖几个旋转周期,能包含完整的故障冲击模式,是一个比较稳妥的折中。

数据加载代码可以封装成这样一个函数:

def make_samples(file_path, sample_length=1024, stride=512): df = pd.read_csv(file_path) signal = df['vibration'].values.astype(np.float32) samples = [] for start in range(0, len(signal) - sample_length, stride): samples.append(signal[start:start + sample_length]) return np.array(samples)

把所有类别的样本都切出来后,随便挑几段可视化,你会发现正常信号和故障信号在波形形态上有明显差异,内圈故障往往伴随周期性冲击,外圈故障的冲击更稀疏但更规则。这种可视化检查很有必要,能帮你提前发现数据装错、标签错位之类的问题。

2.2 标准化:不能让幅值差异干扰模型训练

原始振动数据的幅值范围很宽,不同设备、不同安装位置、不同工况下,甚至同一设备的正常与故障状态之间,振动幅值都可能差好几倍。直接拿原始幅值喂给模型,CNN的卷积核会被幅值主导,而不是被波形形态主导,这是新手很容易忽略的问题。

我采用的做法是z-score标准化:对每段样本减去均值再除以标准差,让每段波形落到均值为0、方差为1的分布上。

def normalize(signal): mean = np.mean(signal) std = np.std(signal) if std < 1e-8: std = 1e-8 return (signal - mean) / std

为什么逐样本标准化而不是整个数据集统一标准化?因为实际部署时,现场采集的一段信号和训练集的统计量不一定一致,如果模型依赖了训练集的绝对幅值,换到新设备上效果就会打折。逐样本标准化让模型只关注波形形态和相对波动模式,泛化性更好。这属于我在实际操作中验证过的细节,强烈建议保留。

当然也有例外:如果你明确知道故障的严重程度和绝对幅值直接相关,比如要区分小裂纹和大裂纹,那逐样本标准化反而会把幅值信息抹掉。这时候可以用全局标准化,或者把绝对RMS值作为额外特征输入。具体场景具体分析,但在基础项目里,逐样本标准化是更安全的选择。

2.3 数据划分:按“信号段”分还是按“设备工况”分

数据划分是整个项目里最容易出错、也最影响结论可信度的一步。很多人在这一步偷懒,把所有滑窗样本打乱后随机划分训练集和测试集,结果测试准确率漂亮得吓人,一换到实际场景就崩。原因在于数据泄露——同一个原始信号段切出来的相邻窗口,内容高度相似,一部分进了训练集,一部分进了测试集,模型相当于“见过”测试数据了。

正确的做法是先在原始信号层面划分。比如CWRU数据里,同一负载下同一故障状态的信号可能有多段,我把前几段用于训练,后几段用于验证/测试,保证训练集和测试集里的窗口不来自同一条原始信号。如果条件允许,更严格的做法是按不同工况划分,比如用0HP负载的数据训练,用1HP、2HP负载的数据测试,这样评测的是模型跨工况的泛化能力,更接近工业实际情况。

标签设计上,我用0到3对应四种状态:

标签状态
0正常(Normal)
1内圈故障(Inner Race Fault)
2外圈故障(Outer Race Fault)
3滚动体故障(Ball Fault)

如果有更细的故障尺寸分类,可以扩展到10类、12类,模型只需把最后一层全连接的输出维度改掉即可。我在代码里用num_classes参数控制,后面做扩展很方便。

训练集、验证集、测试集按6:2:2划分。验证集用于训练过程中的Early Stopping和模型选择,测试集只在最终评估时使用一次,绝不参与训练循环,这是保证结果可信的底线。

3. CNN-LSTM模型实现与训练调参

3.1 网络结构设计:每一层的作用和维度推演

下面是我最终使用的CNN-LSTM模型结构,完整定义在models/cnn_lstm.py里:

import torch import torch.nn as nn class CNNLSTM(nn.Module): def __init__(self, num_classes=4): super().__init__() # CNN部分:局部特征提取 + 降维 self.cnn = nn.Sequential( nn.Conv1d(1, 16, kernel_size=64, stride=8, padding=32), nn.BatchNorm1d(16), nn.ReLU(), nn.MaxPool1d(2), nn.Conv1d(16, 32, kernel_size=3, padding=1), nn.BatchNorm1d(32), nn.ReLU(), nn.MaxPool1d(2), ) # LSTM部分:时序依赖建模 self.lstm = nn.LSTM( input_size=32, hidden_size=64, num_layers=2, batch_first=True, dropout=0.3 ) # 分类头 self.fc = nn.Linear(64, num_classes) def forward(self, x): # x shape: (batch, sample_length) x = x.unsqueeze(1) # (batch, 1, sample_length) x = self.cnn(x) # (batch, 32, T') x = x.permute(0, 2, 1) # (batch, T', 32) out, _ = self.lstm(x) # (batch, T', 64) out = out[:, -1, :] # 取最后一个时间步 return self.fc(out) # (batch, num_classes)

把维度变化捋一遍:输入是(batch, 1024),unsqueeze后变成(batch, 1, 1024),这是1D卷积的标准输入格式:通道数1,长度1024。第一层卷积核大小为64、步长8,输出长度约128;MaxPool后长度64,通道数16。第二层卷积不改变长度,MaxPool后长度32,通道数32。最终CNN输出是(batch, 32, 32),也就是32个时间步、每个时间步32维特征。

这步很关键:CNN把1024个原始采样点压缩成了32个特征帧,LSTM拿到的是这32个特征帧组成的序列。input_size=32对应CNN输出的通道数,hidden_size=64决定了LSTM的隐层维度,num_layers=2增加建模深度,dropout=0.3用于正则化。

为什么最后一层取最后一个时间步的隐状态而不是所有时间步的平均?这是经验问题。故障冲击序列的判别信息往往集中在尾部,取最后一个时间步能保留LSTM经过全序列更新后的最终记忆状态。如果你用平均池化,效果也不是不行,但实测在CWRU数据上取最后一个时间步略微更稳。

3.2 训练配置与超参数:不要无脑默认参数

训练配置我按下面的参数来:

超参数取值说明
优化器Adam收敛快,适合该任务
初始学习率0.001Adam的常用起始点
学习率调度StepLR, step=20, gamma=0.5每20轮衰减一半
Batch Size64平衡显存与稳定性
Epoch60配合Early Stopping
损失函数CrossEntropyLoss多分类标准选择

用CrossEntropyLoss是因为它是多分类任务的标准损失,PyTorch里它把Softmax和交叉熵计算合并在一起,不需要在模型末尾额外加Softmax层。如果你想输出各类别的概率做可视化,在推理阶段手动加一次Softmax即可。

训练循环的完整代码如下:

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = CNNLSTM(num_classes=4).to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=20, gamma=0.5) best_acc = 0.0 for epoch in range(60): model.train() total_loss = 0.0 for xb, yb in train_loader: xb, yb = xb.float().to(device), yb.to(device) pred = model(xb) loss = criterion(pred, yb) optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() * xb.size(0) scheduler.step() # 验证 model.eval() val_correct = 0 val_total = 0 with torch.no_grad(): for xb, yb in val_loader: xb, yb = xb.float().to(device), yb.to(device) pred = model(xb) val_correct += (pred.argmax(1) == yb).sum().item() val_total += yb.size(0) val_acc = val_correct / val_total print(f"Epoch {epoch+1:02d}, Loss: {total_loss/len(train_loader.dataset):.4f}, Val Acc: {val_acc:.4f}") if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), "checkpoints/best_model.pth") print(f" -> saved best model, val_acc={best_acc:.4f}")

两个值得强调的细节。第一,weight_decay即L2正则化设的是1e-4,这个值有讲究,太小起不到约束作用,太大容易欠拟合。我试过1e-2、1e-3,效果都不如1e-4稳定。第二,模型保存不是只看最后一步,而是保存验证集准确率最高的那个epoch,也就是best_model.pth。实际训练中,到第30~40轮时验证集准确率可能反而比第50轮高,如果不做这种回退保存,就浪费了前面最好的参数。

3.3 评估指标:不要只看Accuracy

测试集上的准确率当然要汇报,但工业故障诊断场景里,我强烈建议同时看混淆矩阵、每类别的精确率、召回率和F1分数。原因很现实:如果模型把内圈故障误判成滚动体故障,虽然也是“诊断错误”,但在维护策略上,你可能需要打开轴承检查,而正常状态误判成故障,则会导致不必要的停机。

我在utils/metrics.py里封装了完整评估逻辑,输出每类指标和混淆矩阵:

from sklearn.metrics import classification_report, confusion_matrix import numpy as np def evaluate_model(model, test_loader, class_names, device): model.eval() all_preds = [] all_labels = [] with torch.no_grad(): for xb, yb in test_loader: xb = xb.float().to(device) pred = model(xb).argmax(1).cpu().numpy() all_preds.extend(pred) all_labels.extend(yb.numpy()) print(classification_report(all_labels, all_preds, target_names=class_names)) cm = confusion_matrix(all_labels, all_preds) print("Confusion Matrix:") print(cm) return np.array(all_labels), np.array(all_preds)

在CWRU 4分类任务上,我的实验结果是:测试集准确率达到99.2%,内圈和外圈故障的F1分数都在0.99以上,滚动体故障F1稍低,大约0.97。滚动体故障相对难分类是常见现象,因为滚动体在转动过程中会滑移,故障特征不那么稳定。如果你的项目里某一类故障准确率明显偏低,不要急着调模型,先看混淆矩阵里它最容易和哪类混淆,再针对性处理。

4. 工程化落地:预训练模型、推理封装与项目文档

4.1 预训练模型的保存与加载:别把Word2Vec才用的预训练概念搞混

这个项目标题里提到的“预训练模型”,和NLP里在超大语料上预训练出来的模型是两回事。在轴承故障诊断这个场景里,预训练模型指的是在CWRU数据集上训练好的模型权重,下载下来可以直接加载推理,或者在新数据上做Fine-tuning。

保存方式我用了state_dict,只保存模型参数,不保存整个模型结构。这样做的好处是文件小、跨PyTorch版本兼容性好。加载时你需要先实例化同一个网络结构,再把参数灌进去,代码如下:

def load_model(model_path="checkpoints/best_model.pth", num_classes=4): model = CNNLSTM(num_classes=num_classes) model.load_state_dict(torch.load(model_path, map_location="cpu")) model.eval() return model

这里有个很容易踩的坑:如果你保存模型时定义了num_classes=10(比如区分不同故障尺寸),加载时实例化模型也必须用num_classes=10,否则参数维度对不上会直接报错。我在项目文档里专门标注了模型对应的分类设定,避免用户踩坑。

在工业现场,我们经常需要把模型部署到没有GPU的设备上,所以我在predict.py里统一加了map_location="cpu",让模型在CPU上也能加载。CWRU这类短序列分类模型参数量不大,CPU推理单条样本基本在毫秒级,完全满足实时监测需求。

4.2 把推理封装成类:从脚本到可复用模块

训练完成后,最核心的是怎么把模型用到真实数据上。我封装了一个BearingDiagnoser类,让调用方不用关心模型内部细节,只要输入一段波形,就能拿到诊断结果:

class BearingDiagnoser: def __init__(self, model_path, num_classes=4): self.device = torch.device("cpu") self.model = CNNLSTM(num_classes=num_classes).to(self.device) self.model.load_state_dict(torch.load(model_path, map_location="cpu")) self.model.eval() self.idx_to_label = { 0: "正常", 1: "内圈故障", 2: "外圈故障", 3: "滚动体故障" } def predict(self, signal): # signal: np.ndarray, shape (sample_length,) signal = normalize(signal.astype(np.float32)) tensor = torch.tensor(signal).unsqueeze(0).unsqueeze(0) # (1, 1, sample_length) with torch.no_grad(): logits = self.model(tensor) probs = torch.softmax(logits, dim=1).squeeze(0).numpy() pred_idx = int(np.argmax(probs)) return self.idx_to_label[pred_idx], probs

这个类在预测前会自动做标准化,外部不需要关心预处理流程,这是工程化的关键——把复杂度封装在模块内部,对外暴露极简接口。同时返回各类别概率而不是只有最终类别,方便下游系统做风险决策,比如概率低于阈值时标志为“不确定,需要人工复核”。

我还做了一个基于Streamlit的可视化界面,就是app.py。页面左侧上传CSV格式的振动信号文件,右侧显示波形图和诊断结果,写起来不长:

import streamlit as st import pandas as pd import numpy as np from predictor import BearingDiagnoser st.title("轴承故障诊断系统") uploaded = st.file_uploader("上传振动信号CSV文件", type=["csv"]) if uploaded is not None: df = pd.read_csv(uploaded) signal = df["vibration"].values diagnoser = BearingDiagnoser("checkpoints/best_model.pth") label, probs = diagnoser.predict(signal) st.line_chart(signal) st.write(f"诊断结果:{label}") st.write("各类别概率:", probs)

Streamlit的好处是代码少,运行streamlit run app.py就能在浏览器里打开交互页面,非常适合做演示和验证流程。工业现场如果要做正式的系统集成,通常会通过REST API或者MQTT把推理服务包装起来,这部分逻辑在docs/推理部署说明.md里有详细说明。

4.3 项目文档:训练复现比模型结构更重要

这个项目最花时间的地方,其实不是模型代码,而是项目文档。我把文档拆成三个文件:数据说明、训练复现指南、推理部署说明。

数据说明里写清楚数据集来源、采样频率、每类故障的样本数量、滑窗参数、标签映射表。训练复现指南里写清楚环境依赖(Python版本、PyTorch版本、numpy/pandas/sklearn版本)、从零开始训练的完整步骤、每个脚本的入口参数、以及单卡GPU和纯CPU两种环境下的训练预期耗时。推理部署说明里写清楚预训练模型存放位置、模型对应的分类设定、推理脚本用法、可视化界面启动方式。

为什么这么重视文档?因为这类项目不是写完就结束了。一个月后你回头想改一个参数,如果文档没写清楚,你可能要重新读一遍全部代码才能想起来当时为什么这么设计。更关键的是,其他人拿到你的项目,如果文档不完善,连环境都配不起来,再好的模型也白搭。我在实际工作里就经常被迫阅读各种“代码完整但文档约等于零”的项目,浪费时间不说,还容易在关键细节上出错。所以这个项目的文档我宁可多写,也绝对不遗漏。

5. 常见问题与避坑实录

5.1 数据泄露:准确率99%以上时先别高兴

我见过最多的翻车现场,就是测试准确率刷到99%甚至100%,结果换一批数据立刻拉胯。排查下来,大概率是数据划分出了问题:滑窗切分后没有按原始信号段隔离,直接全量随机划分,导致训练集和测试集来自同一条原始信号。相邻窗口之间的重叠率很高,本质上测试集和训练集高度重复,准确率自然虚高。

正确的检查方法是:打印训练集和测试集样本对应的原始文件路径,确认没有重叠。更稳妥的做法是像我前面说的,按“原始信号段ID”做分组划分,而不是按样本ID划分。在代码里,我保存每个样本时同时记录source_file字段,划分数据集时按source_file的set进行切分,这样能从根本上杜绝泄露。

5.2 故障类别不均衡:小样本类别学不好怎么办

实际采集中,正常状态的样本总是很多,故障状态的样本相对有限,特别是某些罕见故障类型,可能只有很少的数据。类别不均衡会导致模型倾向预测多数类,少数类召回率很低。

我常用的处理手段有三个。第一,加权损失,按类别样本数的倒数给CrossEntropyLoss设置权重,让少数类样本的梯度贡献更大;第二,数据增强,对少数类样本做轻微噪声叠加、时移、幅值微调,扩充样本量;第三,合成少数类过采样,在特征空间里做插值,但这种方法对时序数据要谨慎,容易生成不真实的振动波形。

在项目里我默认在utils/data_loader.py中实现了第一和第三种方法的接口,config里加一个启用开关,按需开启。

5.3 训练不收敛或过拟合:典型表现和排查路径

如果你发现训练loss不下降,先用小批量数据测试模型能否过拟合。具体做法是只取几十个样本,把模型跑几十轮,如果loss不降,说明代码或模型结构有问题;如果loss能降到接近0,说明模型本身没问题,再回到完整数据上调参。

过拟合的表现是训练准确率持续上升、验证准确率停滞或下降。我的排查顺序是这样:先加weight_decay看有没有改善,再调dropout比例(0.3到0.5区间),然后检查是不是数据量太小需要做增强,最后才考虑降低模型复杂度。不要一上来就换模型结构,先把已有的正则化手段用足。

还有一个经验之谈:BatchNorm在样本量较小的时候要慎用。如果batch size太小(比如小于16),BatchNorm的统计量会很不稳定,训练和推理时的分布不一致会导致验证集表现很差。这种情况下要么增大batch size,要么换成LayerNorm。本项目里batch size是64,BatchNorm完全没问题。

5.4 常见问题速查表

问题现象可能原因解决方案
测试准确率极高,换数据暴跌数据泄露,训练/测试窗口来自同一信号段按原始信号段划分数据集
某一类故障F1很低类别不均衡加权损失、数据增强
训练loss不降代码问题或模型无法学习小批量过拟合测试
验证集表现时好时坏BatchNorm受batch size影响增加batch size或换LayerNorm
加载模型报维度错误num_classes不匹配确认模型保存与加载时分类数一致
CPU推理慢模型/输入过长缩短输入长度、用ONNX优化
GPU训练显存不足输入序列过长或batch过大降低batch size或减小窗口长度

6. 项目扩展方向

6.1 变工况迁移学习:让模型换台设备也能用

CWRU训练出来的模型,直接用到另一台设备或者另一批数据上,效果通常会下降,因为数据分布变了。工业场景里这是常态,解决思路是迁移学习。你可以把在CWRU上训练好的CNN-LSTM权重作为初始化,用少量新设备的数据去Fine-tune整个网络,或者只Fine-tune LSTM和分类头、冻结CNN层。

冻结CNN层的逻辑是:CNN前几层学到的是比较通用的局部冲击特征,跨设备共享度比较高;而LSTM和分类层学到的时序模式可能和设备工况有较大关联,需要重新适配。实际操作时可以先冻结尝试,如果验证集效果不够理想,再放开全部参数做微调。

6.2 引入注意力机制:替代“取最后一个时间步”

我在前面说过,LSTM输出只取最后一个时间步。这个做法简单有效,但代价是丢失了中间时刻的部分信息。一个低成本改进方案是引入注意力机制:对LSTM每个时间步的输出做加权平均,权重由网络自己学习。

具体实现上,可以在LSTM输出后面接一个Attention Pooling层,计算每个时间步的重要性权重,再加权求和得到序列表示。这个模块改动很小,但能明显提升模型在中长序列下对关键故障信息的捕捉能力。我在项目代码注释里留了attention接口,有兴趣的话可以自己实现。

6.3 部署到边缘设备:从.pth到ONNX再到推理引擎

把模型部署到工业现场的边缘设备上,还需要做一步模型转换。PyTorch的.pth文件在目标设备上不一定有PyTorch环境,更通用的做法是导出成ONNX格式,再转成ONNX Runtime可加载的模型,或者进一步转成TensorRT的engine格式。ONNX导出在PyTorch里封装得很好,一行代码就能完成,但导出时要把模型切换到eval模式,并且确认输入输出维度固定,否则动态shape会带来额外复杂度。

量化是另一个部署优化方向。把CNN-LSTM的权重从FP32量化到FP16甚至INT8,可以显著降低模型体积和推理延迟。LSTM的量化比Conv和Linear麻烦一些,实测下来FP16量化方案性价比最高,INT8在部分设备上会有精度损失,需要评估后决定是否启用。

最后说一句个人体会。这类故障诊断项目,真正难的不是把模型精度刷高,而是让模型在新的设备、新的工况下还能保持稳定。我在这个项目里最大的收获,不是99%的测试准确率,而是把数据划分、代码结构、文档梳理这些基本功重新打磨了一遍。你复现这个项目的时候,如果时间有限,建议先把训练脚本跑通,然后花时间做一次“换工况测试”——把不同负载下的数据分开训练和测试,你会看到CNN-LSTM的相对优势,也会发现很多值得琢磨的细节。后续有精力的话,再按扩展方向逐步升级,这条路走下来,你收获的就不只是一个模型,而是一整套解决问题的方法论。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询