简介:这份资源面向工业设备健康监测与机器学习入门者,聚焦轴承故障诊断这一典型场景,提供结合支持向量机与卷积神经网络的完整实践方案。包内共3201个文件,以3200张jpg灰度图像和1个py脚本为主,压缩包约4.1MB,图像用于承载轴承振动信号转换后的故障样本,脚本则对应模型训练与诊断流程。已有1739人学习下载,说明该方案在故障识别方向具有一定参考价值。读者可借此理解从振动数据到灰度图的预处理思路,掌握SVM构建决策边界与CNN逐层提取特征的两类建模路径,并对照比较二者在小样本与图像模式识别任务上的表现差异。资源同时覆盖数据标准化、模型训练验证、结果对比与部署预测等环节,适合希望将传统机器学习与深度学习方法落地到轴承故障诊断的读者参考。
1. 从振动信号到故障标签:cnn-svm轴承故障诊断到底在解决什么
产线上那台电机最近总在下午三点左右发出周期性异响,老师傅拿听音棒顶住轴承座听了几秒,说“内圈有伤,趁早换”。问题是,这种靠耳朵的判断没法写进巡检记录,更没法在夜班无人值守时自动报警。cnn-svm轴承故障诊断要解决的,就是把“老师傅的耳朵”变成一套能跑在工控机上的自动判别流程:先用 CNN 从原始振动信号里自动学出故障特征,再用 SVM 做最终分类。它适合手里有振动加速度传感器、采过一批带标签数据、但特征工程做得头疼的设备工程师和算法入门者。相比人工设计峭度、裕度、频谱峰值这些指标,CNN 省掉了“猜特征”的环节;相比纯 CNN 端到端分类,SVM 在小样本上更稳,决策边界也更好解释。这条路线在公开轴承数据集上已经被反复验证,真正难的是把它落到你自己的台架上。
2. 为什么是 CNN 提特征加 SVM 分类,而不是二选一
2.1 轴承故障诊断的信号特点决定了这个组合
轴承故障的振动信号本质上是调制信号:滚动体碾过损伤点时产生周期性冲击,冲击又调制到系统的高频共振上。外圈故障、内圈故障、滚动体故障的差别,藏在冲击的重复频率和调制边带里,而这些信息在时域波形上并不直观。传统做法是算包络谱,找到故障特征频率,再人工比对。问题是转速波动、负载变化、传感器安装位置都会让特征频率漂移,人工规则很难覆盖所有工况。
CNN 的价值在于它能把“找冲击—算包络—提谱峰”这套流程变成可学习的卷积核。一维卷积核在时域上滑动,本质上就是在检测局部冲击模式;多层堆叠后,它学到的是从低级冲击到高级调制模式的层次特征。这比人工指定“看哪个频段”要鲁棒。
但纯 CNN 做分类有个现实问题:工业现场标注数据少。一台设备正常样本多、故障样本少,故障类型还不均衡。CNN 全连接层在样本少时容易过拟合,而 SVM 的结构风险最小化原则在小样本高维特征上表现更稳。所以常见做法是:CNN 只当特征提取器,把倒数第二层的特征向量拿出来,喂给 SVM 做分类。这样 CNN 不需要在少量样本上硬学分类边界,SVM 也不需要从原始信号开始处理。
2.2 用 Python 搭一个最小可跑的 CNN-SVM 流程
下面这段代码用 PyTorch 定义一维 CNN 特征提取器,再用 sklearn 的 SVM 做分类。数据假设你已经有了形状为(样本数, 1, 信号长度)的振动信号和对应标签。
import torch import torch.nn as nn import numpy as np from sklearn.svm import SVC from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report # 一维 CNN 特征提取器:输入 (batch, 1, 2048) class CNNFeatureExtractor(nn.Module): def __init__(self): super().__init__() self.conv_blocks = nn.Sequential( # 第一层:大卷积核抓冲击,输出通道 16 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=16, stride=2, padding=8), nn.BatchNorm1d(32), nn.ReLU(), nn.MaxPool1d(2), # 第三层:小卷积核抓细节 nn.Conv1d(32, 64, kernel_size=3, stride=1, padding=1), nn.BatchNorm1d(64), nn.ReLU(), nn.AdaptiveAvgPool1d(1) # 全局平均池化,输出 (batch, 64, 1) ) def forward(self, x): feat = self.conv_blocks(x) return feat.view(feat.size(0), -1) # 展平成 (batch, 64) # 假设 X_train_raw 形状 (n, 2048),先转成 (n, 1, 2048) def to_tensor(X): return torch.tensor(X, dtype=torch.float32).unsqueeze(1) # 1. 用 CNN 提取特征(这里不训练 CNN,随机初始化也能验证流程; # 实际项目需要先用带标签数据训练 CNN 或至少自编码预训练) extractor = CNNFeatureExtractor() extractor.eval() with torch.no_grad(): feat_train = extractor(to_tensor(X_train_raw)).numpy() feat_test = extractor(to_tensor(X_test_raw)).numpy() # 2. 用 SVM 在 CNN 特征上分类 clf = make_pipeline(StandardScaler(), SVC(kernel='rbf', C=10, gamma='scale')) clf.fit(feat_train, y_train) y_pred = clf.predict(feat_test) print(classification_report(y_test, y_pred))这段代码的关键不在网络多深,而在三个参数:第一层kernel_size=64, stride=8是为了覆盖轴承冲击的典型宽度,2048 点信号里冲击大概占几十到上百点;stride=8做粗粒度下采样,减少计算量同时保留冲击位置信息。第二层kernel_size=16对应调制周期,比冲击宽度大。最后用AdaptiveAvgPool1d(1)而不是展平全连接,是为了让特征对信号平移不敏感——轴承故障的冲击出现在哪个时间点不重要,重要的是有没有、以什么节奏出现。
SVM 这边,C=10是正则强度,样本少时不要设太大,否则每个点都成支持向量;gamma='scale'是 RBF 核的宽度,特征维度是 64,用 scale 自动适配。如果分类报告里某类召回率明显低,先别调 SVM,回去看 CNN 特征是不是把那一类和其他类混在一起了。
2.3 特征提取器怎么训练才不白干
上面代码里 CNN 是随机初始化的,特征没有意义。实际项目里 CNN 必须训练,但训练目标不一定是分类。常见做法有三种:
第一种是直接用分类损失训练 CNN,然后把倒数第二层特征拿出来给 SVM。这种做法简单,但 CNN 会偏向于学“容易分类”的特征,可能丢掉一些 SVM 能利用的细节。
第二种是用自编码器重构损失预训练 CNN,让卷积核学会表示冲击波形,再拿特征给 SVM。这种做法在故障样本极少时更稳,因为重构不需要标签。
第三种是联合训练:CNN 输出特征后同时接一个 softmax 分类头和 SVM,用分类损失加 SVM 的合页损失一起优化。这种做法实现复杂,但效果通常最好。
我一般会先跑第一种,看 SVM 分类报告和纯 CNN softmax 的差距。如果 SVM 比 softmax 高 3 个点以上,说明 CNN 特征里确实有线性不可分的信息,SVM 的核映射起了作用。如果差不多,那可能数据量够大,纯 CNN 就够了,不必硬上 SVM。
3. 从原始振动到模型输入:数据切分和标签制作的四个关键决定
3.1 采样频率和信号长度怎么定
采样频率要覆盖轴承故障的特征频率。轴承故障特征频率一般在几百赫兹到几千赫兹,但冲击会激发系统共振,共振频率可能在 5kHz 到 20kHz。按照奈奎斯特采样定理,采样频率至少是关注最高频率的两倍。工程上常用 12.8kHz、20kHz、25.6kHz 这几档。如果你只关心低频故障特征,12.8kHz 够用;如果想保留共振带信息,建议 20kHz 以上。
信号长度决定 CNN 输入尺寸。太短,一个样本里可能连一个完整冲击周期都装不下;太长,计算量大且故障冲击被稀释。常见做法是取 2048 点或 4096 点。以 20kHz 采样为例,2048 点对应约 0.1 秒,足够覆盖几个冲击周期。如果转速很低,比如 300 转/分,冲击间隔约 0.2 秒,那 2048 点可能不够,需要 4096 或 8192 点。
3.2 重叠切分能救回不少样本
工业现场故障样本少,切分时用重叠可以增加样本数。比如 2048 点窗口,步长取 1024,重叠 50%。这样一段 10 秒的信号能切出约 190 个样本,而不是 95 个。重叠的代价是样本间不独立,训练时要注意验证集不能和训练集来自同一段原始信号,否则准确率虚高。
def slice_signal(signal, window=2048, step=1024): """把长信号切成重叠窗口,返回 (n, window) 数组""" n = (len(signal) - window) // step + 1 return np.stack([signal[i*step : i*step+window] for i in range(n)]) # 按设备/工况分组切分,避免同一段信号同时进训练和验证 # 假设 raw_signals 是 dict: {设备编号: (信号, 标签)} train_X, val_X, train_y, val_y = [], [], [], [] for dev_id, (sig, label) in raw_signals.items(): windows = slice_signal(sig) # 前 70% 窗口进训练,后 30% 进验证 split = int(len(windows) * 0.7) train_X.append(windows[:split]) val_X.append(windows[split:]) train_y.extend([label] * split) val_y.extend([label] * (len(windows) - split))这里最关键的是按设备或按时间段分组切分,而不是把所有窗口打乱后随机分。同一段信号切出的窗口高度相似,随机分会让验证集里出现训练集的“近亲”,准确率能到 99%,但换一台设备就崩。血泪经验:先按设备分,再按时间分,最后才考虑随机。
3.3 标签制作:故障类型和故障尺寸要分开
公开数据集里标签通常是“内圈故障”“外圈故障”“滚动体故障”,但实际诊断中故障尺寸也重要。一个 0.2mm 的早期点蚀和一个 2mm 的剥落,处理 urgency 完全不同。如果标签只写故障类型,模型学到的可能是“有没有故障”,而不是“故障多严重”。
建议标签至少两级:第一级是故障位置(正常/内圈/外圈/滚动体),第二级是故障尺寸(早期/中期/晚期)。如果样本不够,至少把正常和故障分开,故障内部再按尺寸排序做回归或有序分类。
3.4 归一化:别用全局均值方差
振动信号的幅值受负载和传感器灵敏度影响很大。归一化是必须的,但不要用整个数据集的均值和方差。正确做法是每个样本单独归一化,或者用训练集的统计量归一化验证集。每个样本单独做 z-score 归一化,能消除负载变化带来的幅值差异,让 CNN 专注于波形形状。
def normalize_per_sample(x): """对每个样本单独做 z-score,x 形状 (n, window)""" mean = x.mean(axis=1, keepdims=True) std = x.std(axis=1, keepdims=True) + 1e-8 return (x - mean) / std注意1e-8是防止除零,如果某个窗口信号完全平坦(传感器脱落),归一化后会变成噪声,这种样本应该在预处理阶段就剔除。
4. 避坑与排查:cnn-svm 轴承故障诊断最常见的五类翻车
4.1 验证集准确率 99%,换台设备就废了
现象:训练时验证集准确率很高,但把模型用到另一台同型号设备上,正常样本被大量判为故障。
原因:切分时没有按设备分组,同一段信号的重叠窗口同时进了训练和验证。模型记住的是这段信号的“指纹”,不是故障特征。
解决:按设备编号或采集时间段分组切分。如果只有一台设备的数据,至少按时间先后切,用前 70% 时间训练,后 30% 验证。更严格的做法是留一台设备完全不做训练,只做测试。
4.2 SVM 训练报错“收敛失败”或支持向量数等于样本数
现象:SVM 拟合时警告不收敛,或者n_support_接近样本总数。
原因:CNN 特征没有标准化,不同维度量纲差异大;或者C设得太大,每个样本都成了支持向量。
解决:在 SVM 前加StandardScaler,把特征拉到同一量纲。C从 1 开始试,不要一上来就 1000。如果特征维度是 64、样本只有几百,C=1到C=10通常够用。另外检查 CNN 特征是不是有大量常数维度,如果有,说明 CNN 没训练好或者某层死了。
4.3 混淆矩阵里正常和故障分得清,但内圈和外圈混在一起
现象:正常 vs 故障准确率很高,但内圈故障和外圈故障互相误判。
原因:内圈和外圈的冲击重复频率不同,但如果信号长度太短,一个窗口里冲击次数太少,频率信息体现不出来。另外,如果 CNN 第一层卷积核太大,可能把冲击位置信息抹掉了。
解决:增加信号长度,让一个窗口里至少包含 5 到 10 个冲击周期。或者改用频域输入,把 FFT 幅度谱作为 CNN 输入,频率分辨率更直接。也可以把 CNN 第一层卷积核改小,比如从 64 改成 32,保留更多时间细节。
4.4 训练损失下降但验证损失上升,加 dropout 也没用
现象:CNN 训练几个 epoch 后训练损失持续降,验证损失先降后升,典型过拟合。
原因:故障样本太少,CNN 参数量相对样本量太大。或者数据增强没做,模型看到的样本多样性不足。
解决:先减 CNN 参数量,把通道数从 64 降到 32,层数从 3 层降到 2 层。然后加数据增强:对振动信号做时间平移、加高斯噪声、幅值缩放。注意不要做翻转,振动信号翻转没有物理意义。如果还不行,考虑用自编码器预训练,或者直接用 SVM 在手工特征上跑,别硬上 CNN。
4.5 推理时单样本预测结果和批量预测不一致
现象:把单个样本喂给模型预测,结果和把它放进 batch 里预测不一样。
原因:CNN 里的BatchNorm在训练和推理时行为不同。推理时用的是训练集统计量,如果训练集和推理数据分布差异大,结果会偏。另外,如果推理时忘了model.eval(),BatchNorm 会用当前 batch 的统计量,单样本时统计量就是它自己,结果自然不同。
解决:推理前一定调extractor.eval(),并用torch.no_grad()包住。如果训练集和现场数据分布差异大,考虑用InstanceNorm替代BatchNorm,它对 batch 大小不敏感。
5. 让 CNN-SVM 真正落地的两个进阶技巧
5.1 用 CNN 特征做异常检测,解决故障样本极少的问题
现场最常见的情况是:正常样本一大堆,故障样本只有几个。这时候 SVM 分类器根本训不起来。一个实用技巧是只用正常样本训练 CNN 自编码器,让重构误差成为异常分数。推理时,重构误差超过阈值的判为异常,再对异常样本用 SVM 做故障类型细分。
class CNNAutoencoder(nn.Module): def __init__(self): super().__init__() self.encoder = nn.Sequential( nn.Conv1d(1, 16, 64, stride=8, padding=32), nn.ReLU(), nn.Conv1d(16, 32, 16, stride=2, padding=8), nn.ReLU(), nn.Conv1d(32, 64, 3, stride=1, padding=1), nn.ReLU(), ) self.decoder = nn.Sequential( nn.ConvTranspose1d(64, 32, 3, stride=1, padding=1), nn.ReLU(), nn.ConvTranspose1d(32, 16, 16, stride=2, padding=8, output_padding=0), nn.ReLU(), nn.ConvTranspose1d(16, 1, 64, stride=8, padding=32, output_padding=0), ) def forward(self, x): z = self.encoder(x) return self.decoder(z), z # 训练时只用正常样本,损失用 MSE # 推理时计算重构误差,阈值取正常样本误差的 95 分位数这个做法的好处是:正常样本容易获取,自编码器训练稳定;异常检测和故障分类解耦,故障样本少也能先报警。等积累到足够故障样本,再用 CNN 特征训练 SVM 做细分。
5.2 用交叉验证选 SVM 参数,别拍脑袋
SVM 的C和gamma对结果影响很大,但不要用测试集调参。正确做法是在训练集内部做交叉验证。如果样本按设备分组,就用GroupKFold,保证同一设备的样本不会同时出现在训练和验证折里。
from sklearn.model_selection import GridSearchCV, GroupKFold # groups 是每个样本对应的设备编号 cv = GroupKFold(n_splits=5) param_grid = { 'svc__C': [0.1, 1, 10, 100], 'svc__gamma': ['scale', 0.01, 0.1, 1] } clf = make_pipeline(StandardScaler(), SVC(kernel='rbf')) grid = GridSearchCV(clf, param_grid, cv=cv, scoring='f1_macro', n_jobs=-1) grid.fit(feat_train, y_train, groups=groups_train) print(grid.best_params_)scoring用f1_macro而不是准确率,因为故障类别通常不均衡。n_jobs=-1用满 CPU 核。如果交叉验证最优参数和默认参数差距不大,直接用默认的C=1, gamma='scale',别为了 0.5 个点过拟合验证集。
我自己的习惯是:先把 CNN 特征固定住,用GroupKFold跑一遍 SVM 参数网格,记下最优参数和对应的混淆矩阵。然后换一组 CNN 特征(比如换个训练 epoch 的 checkpoint),再跑一遍。如果最优参数在不同特征上跳来跳去,说明特征本身不稳定,回去调 CNN 比调 SVM 更值得。这套流程跑通一次大概半天,但能省掉后面反复翻车的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取