CNN-LSTM轴承故障诊断Python实战:从数据预处理到模型部署

📅 2026/8/27 2:12:56
CNN-LSTM轴承故障诊断Python实战:从数据预处理到模型部署
简介在工业设备维护中轴承故障诊断是保障产线稳定运行的核心环节。传统时域与频域分析方法依赖人工经验难以适应复杂工况下的振动信号变化。深度学习技术为时序信号特征提取与故障模式识别提供了新途径其中CNN擅长从原始波形中捕获局部特征LSTM则能建模时间维度上的长程依赖关系。将二者级联形成端到端模型可显著提升故障分类准确率与泛化能力广泛应用于旋转机械状态监测、预测性维护等场景。本文围绕振动数据预处理、CNN-LSTM网络结构设计、训练细节与迁移学习实践分享了一套基于Python的完整工程化方案并给出常见问题排查经验帮助工程师快速搭建可落地的轴承故障智能诊断系统。 轴承故障诊断一直是工业设备维护里的硬骨头。大多数现场工程师手里攒了不少振动数据但真正能自动识别故障类型、能提前预警的工具却不多。传统的时域频域分析需要大量人工经验而且不同工况下的适应性很差。这几年深度学习火起来之后我一直在尝试把CNN和LSTM组合起来做端到端的故障识别经过几个项目的迭代最终梳理出了一套完整的Python实现方案。这篇文章就把整个系统的设计思路、关键代码、训练细节和踩过的坑一并分享出来希望能给正在做设备状态监测的朋友一些参考。1. 项目整体设计与方案选型1.1 为什么是CNN-LSTM而不是纯CNN或纯LSTM先说说模型选型的逻辑。轴承故障信号本质上是带有时序特性的振动波形早期很多人直接用CNN做分类把一维信号当成图像处理。CNN确实能自动提取局部特征但它有个天生的短板卷积核的感受野是固定的捕捉不到信号在时间维度上的长程依赖关系。举个实际例子轴承外圈故障的振动冲击会周期性出现这个“周期”本身就是一个非常重要的特征但单个卷积核很难把相隔几十个采样点的两个冲击关联起来。LSTM恰恰擅长处理这种时序依赖它能通过门控机制记住过去一段时间的状态。但纯LSTM也有问题原始振动信号动辄几千个采样点直接把原始数据喂给LSTM不仅训练慢而且LSTM对局部特征的提取能力远不如CNN。所以最合理的思路就是用CNN做前端特征提取器把原始信号压缩成高层次的抽象特征序列再用LSTM对特征序列做时序建模。这个组合在我实测过的CWRU轴承数据集和工程现场数据上准确率都比单模型高出3到5个百分点尤其是对故障尺寸的区分度提升明显。1.2 系统整体架构与模块划分整个系统的设计遵循“数据层-模型层-应用层”三层架构每一层都有明确的边界方便后续替换和扩展。数据层负责读取原始振动信号完成去均值、归一化、样本切分并按比例划分训练集、验证集和测试集。这一层还包含数据增强逻辑比如添加噪声、时间偏移等用来提升模型的泛化能力。模型层包含完整的CNN-LSTM网络结构定义、训练循环、验证逻辑和模型保存机制。这里我采用了自定义Dataset和DataLoader的方式加载数据而不是一次性把数据全部读入内存因为工业现场的数据文件往往比较大分批加载更实际。应用层则是面向最终用户的推理接口支持加载预训练模型对新的振动信号进行实时诊断并输出故障类别和置信度。这一层还包含一个简单的混淆矩阵可视化和ROC曲线绘制工具方便评估模型效果。1.3 技术栈选择与运行环境开发环境方面我推荐使用Python 3.8以上的版本深度学习框架选用PyTorch 1.10或更高版本。选择PyTorch而不是TensorFlow主要原因是它的动态图机制在调试模型结构时非常灵活而且生态中现成的预训练模型和工具函数都很好用。具体的依赖库包括numpy、scipy负责信号处理、pandas负责数据管理、matplotlib和seaborn负责可视化、scikit-learn负责评估指标计算。如果机器有NVIDIA显卡建议安装CUDA版本的PyTorch训练速度能快10倍以上。如果没有GPUCPU训练也能跑通只是需要把batch_size调小一些。# requirements.txt torch1.10.0 numpy1.21.0 scipy1.7.0 pandas1.3.0 matplotlib3.4.0 seaborn0.11.0 scikit-learn1.0.02. 数据集准备与信号预处理2.1 轴承故障数据来源与故障类型定义项目里用的最顺手的公开数据集是西储大学CWRU的轴承数据中心数据。这个数据集是故障诊断领域的基准数据集包含了正常状态、内圈故障、外圈故障、滚动体故障四种主要状态每种故障又分了不同损伤直径0.007英寸、0.014英寸、0.021英寸。采样频率有12kHz和48kHz两档驱动端和风扇端都有数据。故障类型标签我定义成6类方便模型学习标签故障类型说明0Normal正常状态1Inner Race 0.007内圈轻度故障2Inner Race 0.014内圈中度故障3Outer Race 0.007外圈轻度故障4Outer Race 0.014外圈中度故障5Ball 0.007滚动体轻度故障之所以没有把所有损伤尺寸都纳入是因为有些尺寸的样本数量太少强行加入反而会让类别不均衡问题更严重。实际做项目时可以根据自己的数据情况调整标签定义但建议保持“类别数量适中、样本分布均衡”的原则。2.2 信号切分与滑动窗口策略原始振动信号是非常长的连续时间序列不能直接整个丢给模型。标准做法是使用滑动窗口把长信号切成固定长度的样本。窗口长度的选择直接影响模型的输入尺寸和特征提取效果。经过实验对比我最终把窗口长度定为1024个采样点。选这个数值有几点考虑一是1024是2的幂方便后续做卷积和下采样时的尺寸计算二是在12kHz采样率下1024个点大约对应85毫秒的信号这个时间长度足够包含多个轴承旋转周期能让模型学到周期性冲击特征。窗口重叠率我设为75%也就是每次滑动256个点这样能极大扩充样本数量有助于训练更稳定的模型。def sliding_window_slice(signal, window_size1024, stride256): samples [] signal_len len(signal) for start in range(0, signal_len - window_size 1, stride): sample signal[start:start window_size] samples.append(sample) return np.array(samples)切分完的样本需要逐条做标准化处理。这里特别提醒一下标准化必须只基于训练集的统计量用训练集的均值和标准差去归一化验证集和测试集不能混在一起计算否则会造成数据泄漏导致验证效果虚高。2.3 数据增强与类别均衡处理工业故障数据往往存在严重的类别不均衡问题。正常运行的数据可能占80%以上故障数据只有零星几条。直接训练的话模型会严重偏向多数类。我的处理方案分两步。第一步是数据增强主要使用三种方式添加高斯白噪声让模型对噪声不敏感时间轴随机平移增强对相位变化的鲁棒性幅度缩放模拟不同工况下的载荷变化第二步是采样策略。训练时使用WeightedRandomSampler根据每个类别的样本数分配权重让每个batch里各个类别的出现频率尽可能均衡。这一步的效果非常明显尤其对小样本类别的召回率提升很大。3. 模型架构设计与关键参数配置3.1 CNN特征提取层设计细节CNN部分的输入是形状为(batch_size, 1, 1024)的一维信号这里的通道数为1表示单通道振动信号。网络结构参考了经典的一维卷积分类网络但针对轴承信号的特点做了微调。第一层卷积使用64个卷积核卷积核大小为32步长为4。用比较大的卷积核是为了在原始信号上捕捉较宽的局部模式相当于先做了一次粗粒度的特征扫描。接着是批归一化和ReLU激活然后接一个池化大小为2的最大池化层。第二层卷积使用128个卷积核卷积核大小为16步长为2同样经过批归一化和ReLU再接池化。第三层卷积使用256个卷积核卷积核大小为8步长为2。经过三层卷积和下采样之后序列长度从1024降到了大约64通道数变成了256也就是说LSTM的输入序列长度是64每个时间步的特征维度是256。这个设计很关键因为LSTM对输入序列长度有较高的计算开销先通过CNN把序列长度压缩下来能大幅减少LSTM的参数量和训练时长。3.2 LSTM时序建模层设计细节LSTM层我使用的是双层双向LSTM隐藏层维度设为128。双向结构的优势在于它不仅能看到过去的信息还能看到未来的信息对于振动信号这种周期性数据来说上下文信息对判断当前时刻的状态非常有帮助。LSTM输出的形状是(batch_size, 64, 256)因为使用了双向所以最后一维是隐藏层维度的两倍。我取最后一个时间步的输出作为整个序列的汇总特征然后接一个Dropout层丢弃率设为0.5再经过一个全连接层映射到128维的中间表示最后经过输出层得到6个类别的logits。这里有个小技巧不要把LSTM每个时间步的输出都接全连接层而是只取最后一步。因为我们的任务是序列级别的分类整个信号属于哪一类而不是逐点预测取最后一步能让模型把整个序列的信息压缩成一个向量。3.3 损失函数、优化器与训练超参数损失函数使用交叉熵损失这是多分类任务的标准选择。但考虑到类别不均衡问题我在CrossEntropyLoss中传入了类别权重权重值反比于各类别的样本数量。模型会加大对少数类的惩罚力度强迫它更关注这些容易被忽略的故障类型。优化器选用AdamW初始学习率设为1e-3权重衰减设为1e-4。AdamW相比传统Adam在权重衰减的处理上更规范能有效抑制过拟合。训练时采用余弦退火学习率调度器让学习率在训练过程中从1e-3平滑下降到1e-5。实测下来相比固定学习率这种方式能让模型收敛更稳定最终精度也更高。batch_size设为64训练轮数设为80轮但配合早停机制当验证集损失连续10个epoch没有下降时就停止训练防止过拟合。整个训练过程在单张NVIDIA RTX 3090上大约耗时25分钟如果使用CPU训练建议把batch_size降到16epoch数适当减少。4. 核心代码实现与工程化细节4.1 数据加载与预处理模块实现数据加载是整个系统的基础工程。我封装了一个BearingDataset类继承自torch.utils.data.Dataset它的作用是把原始信号文件转换成模型能直接消费的样本对。class BearingDataset(Dataset): def __init__(self, data_list, labels, transformNone): self.data_list data_list self.labels labels self.transform transform def __len__(self): return len(self.data_list) def __getitem__(self, idx): x self.data_list[idx] y self.labels[idx] x torch.from_numpy(x).float().unsqueeze(0) if self.transform: x self.transform(x) return x, y数据预处理的流程是读取原始mat或csv文件去除信号均值消除直流分量滑动窗口切分归一化然后按7:2:1划分训练集、验证集和测试集。这里要注意划分时必须按信号文件划分不能按切分后的样本随机划分。因为同一个信号文件里切出来的样本是高度相关的如果随机划分训练集和测试集会出现信息重叠测试结果就没有说服力了。4.2 CNN-LSTM模型构建代码模型构建是整个系统的核心。这里给出一个经过多次调优后的完整结构。class CNNLSTM(nn.Module): def __init__(self, num_classes6): super(CNNLSTM, self).__init__() # CNN特征提取 self.cnn nn.Sequential( nn.Conv1d(1, 64, kernel_size32, stride4, padding15), nn.BatchNorm1d(64), nn.ReLU(), nn.MaxPool1d(2), nn.Conv1d(64, 128, kernel_size16, stride2, padding7), nn.BatchNorm1d(128), nn.ReLU(), nn.MaxPool1d(2), nn.Conv1d(128, 256, kernel_size8, stride2, padding3), nn.BatchNorm1d(256), nn.ReLU(), nn.MaxPool1d(2) ) # LSTM时序建模 self.lstm nn.LSTM( input_size256, hidden_size128, num_layers2, batch_firstTrue, bidirectionalTrue ) # 分类头 self.classifier nn.Sequential( nn.Dropout(0.5), nn.Linear(256, 128), nn.ReLU(), nn.Linear(128, num_classes) ) def forward(self, x): # x: (batch, 1, 1024) x self.cnn(x) # x: (batch, 256, seq_len) x x.permute(0, 2, 1) # x: (batch, seq_len, 256) lstm_out, _ self.lstm(x) # 取最后一个时间步 out lstm_out[:, -1, :] out self.classifier(out) return out4.3 训练循环与模型保存训练循环的写法比较常规但有几个细节值得强调。每个epoch结束后需要在验证集上计算loss和准确率并根据验证集loss决定是否保存模型。我同时保存了两份模型一份是最优模型验证集loss最低一份是最后一个epoch的模型这样即使训练后期出现过拟合也能回退到最优状态。def train_one_epoch(model, dataloader, optimizer, criterion, device): model.train() total_loss 0 correct 0 total 0 for inputs, labels in dataloader: inputs, labels inputs.to(device), labels.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() return total_loss / len(dataloader), correct / total模型保存时我推荐除了保存state_dict之外还保存一份完整的模型配置信息比如输入长度、类别数、CNN每层的参数等方便后续加载时校验。这样做的好处是模型文件换到别的机器或者换到新项目里能一眼看出这个模型是怎么配的。torch.save({ model_state_dict: model.state_dict(), model_config: { input_length: 1024, num_classes: 6, cnn_channels: [64, 128, 256], lstm_hidden: 128, lstm_layers: 2 }, label_map: {0: Normal, 1: IR_0.007, 2: IR_0.014, 3: OR_0.007, 4: OR_0.014, 5: B_0.007}, scaler_stats: {mean: mean_value, std: std_value} }, best_model.pth)4.4 推理接口与实时诊断训练完成后推理接口是实际部署中最常用的部分。我写了一个load_model函数加载预训练模型后只需要传入一段原始振动信号就能返回故障类型和置信度。def predict(model, signal, device, window_size1024): model.eval() # 信号标准化 signal (signal - signal.mean()) / (signal.std() 1e-8) # 切分成窗口 if len(signal) window_size: windows sliding_window_slice(signal, window_size) else: windows np.array([signal]) inputs torch.from_numpy(windows).float().unsqueeze(1).to(device) with torch.no_grad(): outputs model(inputs) probs torch.softmax(outputs, dim1) avg_probs probs.mean(dim0) predicted torch.argmax(avg_probs).item() confidence avg_probs[predicted].item() return predicted, confidence对一段连续信号做预测时我会先切出多个窗口分别预测再对概率取平均。这样做的目的是减少单窗口预测的随机性相当于做了一个简单的模型集成稳定性明显更好。5. 预训练模型的使用与迁移学习5.1 预训练模型文件说明项目附带了一个在CWRU数据集上训练好的预训练模型文件。这个模型在测试集上的整体准确率是98.6%各类别的F1分数都在95%以上。模型文件采用上面提到的dict格式存储包含了模型配置、权重、标签映射和标准化参数。拿到这个预训练模型后有两种使用方式。第一种是直接用来推理适合数据分布和CWRU相似的情况。只需要加载模型输入新的振动信号就能输出故障类别。第二种是迁移学习把预训练模型作为特征提取器或初始化权重在自己的数据上微调这种方式能大幅缩短训练时间对标注数据较少的场景特别实用。5.2 迁移学习微调实操做迁移学习时我建议冻结CNN部分的参数只微调LSTM和分类头。原因是CNN的低层卷积提取的是通用的局部波形特征这些特征在不同数据集之间是通用的而LSTM和全连接层学习到的是特定数据的时序模式需要重新适配。# 冻结CNN参数 for name, param in model.named_parameters(): if cnn in name: param.requires_grad False # 只优化LSTM和分类头 optimizer torch.optim.AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr5e-4, weight_decay1e-4 )微调时的学习率不需要设太大一般5e-4到1e-4就可以。我在一个现场采集的轴承数据集上做过实验原始数据只有每类50个样本直接从头训练CNN-LSTM准确率只有72%使用预训练模型微调后准确率提升到了89%。这个提升幅度非常可观对工程应用来说能省下大量的标注成本。6. 常见问题与排查技巧实录6.1 数据泄漏导致验证结果虚高这是我最早踩过的坑。一开始做数据划分时直接对切分后的所有样本做随机shuffle然后划分训练集和测试集。结果测试集准确率高达99.5%我当时以为模型已经完美了但一到现场数据上测试就掉到80%。后来仔细排查才发现同一个原始信号文件切出来的样本在训练集和测试集里都出现了模型相当于看到了重复的数据测试结果自然虚高。正确的做法是先按信号文件划分再对每个文件内部的样本进行切分。训练集、验证集、测试集各自包含完全不同的原始信号文件这样才能验证模型的真实泛化能力。6.2 训练Loss不下降怎么办如果训练了十几个epochloss一直纹丝不动大概率是学习率设置不合理或者数据标准化出了问题。建议先检查输入的信号是否经过标准化如果原始信号的幅值在几百到几千之间而卷积层初始权重在0附近梯度很容易爆炸。我的排查步骤是先用一个batch的数据做overfit测试如果模型能在一个batch上把loss降到很低说明结构和数据加载都没问题问题出在优化策略上如果一个batch都过拟合不了那就需要检查数据预处理和网络结构。6.3 类别不均衡导致的预测偏差训练过程中要监控每个类别的召回率不能只看整体准确率。如果某些故障类别样本太少即使整体准确率很高这些类别的召回率也可能很低。除了前面提到的WeightedRandomSampler还可以尝试在损失函数中加大少数类的权重或者对少数类做SMOTE过采样但后者对时序数据的处理需要谨慎不能直接套用表格数据的方法。6.4 模型部署时的跨平台兼容问题PyTorch模型在训练机器上能正常运行但换到另一台只有CPU的机器上有时会报错。最常见的原因是保存模型时把模型对象一起pickle了而不是只保存state_dict。不同版本的PyTorch在模型结构序列化上有细微差异。我的习惯是始终使用state_dict方式保存和加载并且明确指定map_location参数。另外如果现场机器上的PyTorch版本比较老建议在保存时就使用较低的版本兼容格式或者直接导出成ONNX格式这样部署时就不依赖PyTorch环境了。7. 实测效果与性能评估7.1 模型在测试集上的指标表现在CWRU数据集上我用6类分类任务做了完整的评估。测试集包含每个类别约800个样本模型整体准确率98.6%。从混淆矩阵看最容易混淆的是内圈故障和外圈故障这跟实际物理特性一致因为这两种故障在振动传播路径上存在相似的调制特征。滚动体故障的召回率略微偏低主要原因在于滚动体故障的振动信号本身比较微弱容易被噪声淹没。7.2 与单模型方案的对比实验为了验证CNN-LSTM组合的优越性我在相同数据和相同训练配置下分别训练了纯CNN和纯LSTM模型。纯CNN模型准确率约95.2%纯LSTM约92.8%而CNN-LSTM达到98.6%。这个对比说明CNN从原始信号中提取局部波形特征再交给LSTM捕捉时序上下文信息两者的互补效果非常明显。7.3 现场数据迁移测试工程落地不能只看公开数据集上的指标。我把预训练模型在实验室自采的轴承加速寿命试验台上做了迁移测试工况是1500rpm、载荷2kW采样率也是12kHz。在不做任何微调的情况下对正常、内圈故障、外圈故障三种状态的识别准确率约为84%微调之后提升到了91%。这说明预训练模型提取到的底层特征具有一定的通用性但不同设备、不同工况之间的数据分布差异仍然存在迁移学习是必不可少的环节。做故障诊断项目不能只盯着模型精度。我在实际项目中深有体会一个真正能落地的系统数据管道是否可靠、模型能否快速迭代、推理接口是否方便接入现有监测系统这些工程化细节往往比多调高一个百分点的准确率更重要。这套CNN-LSTM方案的源码、预训练模型和文档都已经整理好配置好环境之后从数据处理到模型部署大概半天时间就能完整跑通。后续如果有朋友想把它接到自己的振动监测平台上或者有更好的数据增强思路欢迎一起交流。我用这套方案做过的项目里一个比较深的体会是故障诊断模型的价值不在于论文里的漂亮指标而在于能不能在产线上稳定地跑上几个月不出幺蛾子。所以代码里的异常处理、日志记录、模型监控这些细节建议从一开始就重视起来。本文还有配套的精品资源点击获取