简介:一套基于1D-CNN与SVM的滚动轴承故障诊断实战资源,面向机械健康监测、设备运维和深度学习初学者。项目利用TensorFlow构建一维卷积神经网络自动提取振动信号特征,再交由支持向量机完成故障分类,覆盖数据预处理、模型搭建、训练评估与泛化验证等环节,适用于滚动轴承常见故障识别场景。压缩包共2个文件,包含1个Python脚本(负责模型构建、编译、训练与评估)和1个txt数据链接(指向原始振动信号数据的获取途径),整体仅3KB,结构精简,便于快速定位核心内容。资源已有3537人学习下载,具备一定参考价值。通过学习可掌握1D-CNN与SVM联合建模的整体流程,理解卷积层、池化层、全连接层以及最大间隔超平面在故障诊断中的具体作用,还能借鉴脚本中的损失函数选择、优化器配置与防止过拟合的处理方式,快速搭建属于自己的轴承故障诊断模型,用于课程设计或工程实践。
1. 滚动轴承故障诊断:从1D-CNN到SVM,为什么混合模型能打
旋转机械的故障里,滚动轴承占了相当大比例,它一旦出问题,振动信号里会出现周期性的冲击和调制边带。可是现场噪声和工况变化很容易把这个信号掩埋掉,光靠人工特征,从时域到频域要试很多组合,换一台机器就失效。我接触过不少做故障诊断的团队,上来就堆很深的CNN,结果样本只有几千段,最终表现甚至不如一个调好的SVM。反直觉的地方在于:把1D-CNN当特征提取器,末尾的Softmax换成SVM,往往在小样本、强噪声下更稳,也更方便复现。这篇笔记从原理、最小可跑通的故障诊断代码,到必踩的坑和投稿验证方法一起过一遍,适合想解决实际课题,或者准备冲击故障诊断领域二区和三区的SCI期刊的从业者。
2. 1D-CNN为什么适合振动信号:从原始波形到特征提取
2.1 振动信号的形态与1D卷积的对应关系
滚动轴承的振动信号本质上是一维时间序列。正常状态下,信号近似平稳随机振动,叠加上低速旋转分量的幅值调制;当内圈、外圈或滚动体出现局部损伤时,滚子在损伤点会产生极短时宽的冲击力,这个冲击会激起轴承座和传感器系统的固有频率振荡,在包络谱上形成以故障特征频率为间隔的边带。传统的做法是先算峰值因子、峭度、包络谱,再把这些手工特征喂给SVM或随机森林。问题在于特征工程依赖转速、轴承几何参数、采样率,工况一变就要重新设计。
1D-CNN绕开了这套手工流程。它的卷积核沿时间轴滑动,本质上是在原始波形上做局部模板匹配。第一层卷积核会学到类似“冲击+衰减振荡”的波形片段,第二层、第三层再把不同卷积核的输出组合成更复杂的模式,比如连续的冲击序列或调制包络。我做过对比,把同一段信号做短时傅里叶变换后给2D-CNN,效果不一定比直接吃原始波形的1D-CNN好,因为时频图参数非常敏感。窗长、重叠率、窗函数都会改变输入分布,而原始信号采集后只做归一化就能进网络,省掉了中间人为选择。
这里还要强调一点,一维卷积的特点在于局部感受野只覆盖时间方向上的一个区间,相比二维卷积参数量小得多。振动信号本身采样点数不多,1024点就是一个很好的输入长度,网络做三层下采样后特征图很小,整个模型非常轻量。这个特性让1D-CNN在嵌入式诊断场景里也有落地空间,不只是论文好看。
2.2 输入长度、卷积核尺寸与池化策略
输入长度是第一个需要认真调的参数。滚动轴承每转一圈,损伤点可能会冲击一次或多次。如果输入长度小于一个旋转周期,模型可能只看到半个冲击过程,特征稳定性差。比如某公开轴承数据集采样率12 kHz,转速1772 r/min,对应一圈约0.034秒,约408个采样点。很多论文直接取1024点,足够覆盖两圈以上。我一般会在512到2048之间取2的幂,1024是默认起点;如果故障特征频率比较低,或者需要捕捉更长的调制周期,就调整为2048。
卷积核尺寸同样影响巨大。第一层卷积核相当于时域窗长,设成3或5只能看见几个采样点,SPM冲击在12 kHz采样率下持续约1毫秒到2毫秒,也就是12到24个点,所以核太小抓不住冲击细节;设成256又会把信号过度平滑。我的常用配置是第一层64或128,第二层16或32,第三层8或16,让网络从宽到窄逐步提取局部到全局的特征。池化层我固定用MaxPool1d,核大小2、步长2。最大值池化对冲击峰值很敏感,能保留瞬时冲击幅度;平均池化在振动信号上容易把冲击抹平,不推荐。
另有一个布局细节:第一层卷积的padding要保证信号长度在卷积前后不变,否则输入1024经过64步长1之后会缩到961,不方便后面做下采样计算。通常会设置padding=63或padding=31,让长度保持对齐。后续层也一样。这样网络可以在前三层维持一个清晰的分辨率,最后直接用全局平均池化把特征图压成一维向量。
2.3 归一化与简单数据增强
振动信号采集时,幅值会受到传感器灵敏度、安装位置和载荷的影响。同一个故障在不同载荷下,加速度幅值可能相差一倍以上。如果不做归一化,卷积层很容易被大振幅样本主导,小故障信号被淹没。我推荐逐样本z-score归一化:先减掉样本均值,再除以标准差。这里有一个容易忽略的点:均值和标准差必须在训练样本上计算,测试样本使用训练集的统计量,而不是测试样本自己的统计量,否则会引入数据泄漏。
数据增强不是必须的,但在类别不均衡或样本量很小时值得做。对振动信号,最不容易改变故障本质的是加噪声和时间偏移。加噪声用高斯白噪声,信噪比控制在20到30 dB,太低会把真实冲击掩盖;时间偏移模拟的是采样起点不同,因为传感器不会总在同一个相位触发。下面是我常用的预处理函数。
import numpy as np def preprocess_signal(signal, length=1024, noise_snr=None, shift=False): # signal: 原始一维数组 # 1. 随机截取一个长度为 length 的片段 if len(signal) > length: start = np.random.randint(0, len(signal) - length) sample = signal[start:start + length] else: sample = np.pad(signal, (0, length - len(signal)), 'constant') # 2. 去均值、除标准差,z-score 归一化 sample = (sample - np.mean(sample)) / (np.std(sample) + 1e-10) # 3. 可选高斯噪声增强,SNR 单位 dB if noise_snr is not None: signal_power = np.mean(sample ** 2) noise_power = signal_power / (10 ** (noise_snr / 10)) noise = np.random.normal(0, np.sqrt(noise_power), size=sample.shape) sample = sample + noise # 4. 可选循环时间偏移,模拟不同采样起点 if shift: k = np.random.randint(0, length) sample = np.concatenate([sample[k:], sample[:k]]) return sample.astype(np.float32)逻辑说明:函数返回一个归一化的样本。先做随机截断,保证不同epoch看到不同片段,相当于隐式增强;然后去均值和标准差,让幅值尺度统一;加噪声和时间偏移都通过条件开关控制。参数说明:length通常取1024或2048,noise_snr设25时噪声不会盖过冲击,shift只在训练时用,测试时固定取片段,不增强。注意PyTorch的Conv1d输入形状是(batch, channels, length),送进网络前还要把一维数组变成(1, length)再扩batch维。
把预处理函数应用到训练集时,要注意每个样本独立归一化,而不是把整段长信号一起归一化再切分。因为不同样本的直流偏置不同,独立归一化才符合实际推理场景。
3. 特征融合与SVM分类器:把深度特征交给核方法
3.1 为什么用SVM替代Softmax
大多数深度学习实现都在CNN末端加Softmax,用交叉熵训练。这在数据量大、边界清晰的视觉任务上够用,但滚动轴承故障诊断的样本量往往只有几千段,故障类别之间还会因为载荷变化发生特征偏移。Softmax学到的决策边界是线性的,而CNN中间层输出的特征分布可能是弯弯曲曲的非线性流形。强行用一个线性分类面切分,容易被支持向量附近的离群点带偏。
SVM用RBF核函数把特征映射到更高维空间,在高维空间里找最大间隔超平面。最大间隔的意义在于,它不追求让所有训练样本都完全正确,而是在分类错误和边界宽度之间取平衡,所以对噪声样本和异常点位更鲁棒。我之前做过对比:CNN加Softmax在训练集上准确率能到99%,测试集只有88%;同样特征换成RBF核SVM,训练集94%,测试集93%,方差还更小。这就说明了问题。
实际操作上有两种路线:一种是把SVM作为分类器,在训练CNN时不用交叉熵,而是把特征输出喂给SVM,通过合页损失端到端更新;另一种是先用交叉熵训练CNN,让它把特征空间初步分成簇,训练结束后提取倒数第二层特征训练SVM。我推荐后者,端到端联合训练在中小样本上更容易过拟合,而且实现复杂。分步训练的好处是CNN的角色明确,就是一个特征提取器,分类任务完全交给SVM。
3.2 特征提取方式与降维选择
特征从哪一层取,决定了SVM输入维度。如果从最后一层展平的卷积输出取,1024点经过三次池化后特征图大约是128×64=8192维,直接给SVM训练很慢,也容易过拟合。常见做法是在卷积层后插入一个全局平均池化,把每个特征图压成一个标量。比如最后一层有64个通道,GAP之后特征就是64维。我习惯在GAP后再接一个带ReLU的全连接层,输出128维,这样特征表达能力强一些,又不至于维度太高。
提取特征时要关闭Dropout和BatchNorm的统计更新。PyTorch里模型切换到model.eval()就能做到。但如果你自定义了特征提取器,必须确保BatchNorm使用训练阶段累计的running_mean和running_var,而不是当前batch的统计量。很多人的测试特征和训练特征分布不一致,就是因为忘了切eval()模式。
下面这段代码展示了如何从训练好的CNN中提取特征:
import torch import torch.nn as nn # 假设 model 是训练好的 1D-CNN,包含 feature 和 classifier 两部分 # feature 输出形状为 (batch, channels, length),用全局平均池化压出特征 class FeatureExtractor(nn.Module): def __init__(self, cnn_model): super().__init__() self.feature = cnn_model.feature self.gap = nn.AdaptiveAvgPool1d(1) def forward(self, x): out = self.feature(x) # (B, C, L) out = self.gap(out).squeeze(-1) # (B, C) return out def extract_features(model, dataloader, device='cuda'): model.eval() model = model.to(device) extractor = FeatureExtractor(model) features, labels = [], [] with torch.no_grad(): for x, y in dataloader: x = x.to(device).float() f = extractor(x) features.append(f.cpu().numpy()) labels.append(y.numpy()) return np.concatenate(features, 0), np.concatenate(labels, 0)逻辑说明:FeatureExtractor把CNN的特征部分和全局平均池化串起来,输出每个样本一个特征向量。extract_features函数遍历数据加载器,把所有特征和标签拼成两个numpy数组。参数说明:如果GAP输出维度太高,可以在进入SVM前用PCA降到64维,PCA同样只允许在训练特征上拟合。对CWRU这类数据,128维特征直接给SVM通常已经够了,不需要额外降维。
还有一个细节:训练CNN时用的损失函数是交叉熵,但提取特征时分类器完全丢掉。所以CNN在训练中学习到的类间差异编码在特征向量里,而不是编码在分类权重里。最后一个全连接层的权重只能用来辅助CNN学习特征,不能用来做最终推断。
3.3 SVM核函数与C、gamma的网格搜索
SVM最常用的是RBF核,也就是径向基函数。RBF核有两个关键参数:C和gamma。C是误分类惩罚系数,C大,模型越强调把训练样本分类正确,但容易过拟合;C小,边界会更平滑,对噪声容忍度更高。gamma决定RBF核的宽度,gamma大,每个训练样本的影响范围小,边界会变得非常曲折;gamma小,影响范围大,边界更接近线性。
这两个参数必须一起调。我一般用网格搜索加五折交叉验证,搜索范围先给一个对数网格:C在[0.1, 1, 10, 100],gamma在[0.001, 0.01, 0.1, 1]。如果特征维度低,这个搜索只需要几十秒。如果特征维度上千,可以先用PCA降维再做。标准流程如下:
from sklearn.svm import SVC from sklearn.model_selection import GridSearchCV from sklearn.preprocessing import StandardScaler # 假设 X_train, y_train, X_test, y_test 已经准备好 scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) # 注意用训练集的 scaler param_grid = {'C': [0.1, 1, 10, 100], 'gamma': [0.001, 0.01, 0.1, 1]} svc = SVC(kernel='rbf', class_weight='balanced', probability=False) grid = GridSearchCV(svc, param_grid, cv=5, scoring='f1_macro', n_jobs=-1) grid.fit(X_train_scaled, y_train) best_svm = grid.best_estimator_ test_acc = best_svm.score(X_test_scaled, y_test) print(f'best params: {grid.best_params_}, test acc: {test_acc:.4f}')逻辑说明:先用训练集数据fit标准化器,再用同一个scaler去transform测试集,绝不能把训练和测试混在一起计算均值和方差。然后网格搜索以F1宏平均为优化指标,避免准确率在类别不均衡时失真。class_weight='balanced'会自动提高少数类权重,如果数据集类别均衡可以去掉。搜索完成后best_svm就是当前特征下的最优分类器。
网格搜索之后不要直接report测试准确率就完事。正确的做法是用交叉验证的F1选择参数,再用独立测试集评估。如果你发现测试准确率和交叉验证结果差距很大,大概率是测试集参与到了某个环节里,比如标准化或PCA。这个问题在下一章会再遇到。
4. 从零跑通一个最小诊断流程:数据、模型与评估
4.1 数据集准备与训练/测试划分
这里用公开的CWRU轴承数据做演示最方便,它包含正常和多种故障位置、故障尺寸的数据。拿到原始长信号后,第一件事是确定样本切分方式。我见过很多翻车现场:直接把同一段连续信号按滑动窗口切成样本,随机分给训练集和测试集,结果训练测试样本高度重叠,准确率虚高到99.9%,实际部署完全不行。
正确做法是先把原始长信号按时间分成前段和后段,例如前70%作为训练用候选信号,后30%作为测试用候选信号,然后再分别从各自的候选信号中截取样本。这样保证训练和测试信号在时间上严格分开。如果数据来自多个工况,每个工况都要按这个比例切,不能让某一工况的数据只存在于训练集。具体代码如下:
from sklearn.model_selection import train_test_split # raw_signals: list of dict,每个 dict 含 'signal' 和 'label',对应一段长信号 train_rawn, test_rawn = [], [] for record in raw_signals: sig, label = record['signal'], record['label'] split_idx = int(len(sig) * 0.7) train_rawn.append({'signal': sig[:split_idx], 'label': label}) test_rawn.append({'signal': sig[split_idx:], 'label': label}) # 再分别从 train_rawn 和 test_rawn 里随机截取样本 # 用 preprocess_signal 生成固定长度样本 X_train, y_train = [], [] for record in train_rawn: for _ in range(100): # 每段长信号生成 100 个样本 X_train.append(preprocess_signal(record['signal'], length=1024)) y_train.append(record['label']) X_test, y_test = [], [] for record in test_rawn: for _ in range(60): X_test.append(preprocess_signal(record['signal'], length=1024, noise_snr=None)) y_test.append(record['label']) X_train = np.stack(X_train, 0) X_test = np.stack(X_test, 0)逻辑说明:先把每条长信号按7:3切成训练候选段和测试候选段,再分别截样本。这样测试集里的任何样本都不会和训练集共享原始信号。参数说明:每段生成100或60个样本取决于你想要的样本总量,样本量太少CNN容易过拟合,太多会造成相邻样本高相关。我一般让训练集样本数在2000到5000之间,测试集300到1000。生成样本时训练集可以开启noise_snr增强,测试集保持原始条件。
4.2 1D-CNN主干实现(PyTorch)
下面是一个轻量级的1D-CNN模型定义,适合中小样本的滚动轴承振动信号。它用三个卷积块,每块包含一维卷积、批归一化、ReLU和最大池化,最后用全局平均池化接两个全连接层。
import torch import torch.nn as nn class BearingCNN(nn.Module): def __init__(self, in_channels=1, num_classes=4, feature_dim=128): super().__init__() self.feature = nn.Sequential( nn.Conv1d(in_channels, 16, kernel_size=64, stride=1, padding=31), nn.BatchNorm1d(16), nn.ReLU(inplace=True), nn.MaxPool1d(2, 2), nn.Conv1d(16, 32, kernel_size=16, stride=1, padding=7), nn.BatchNorm1d(32), nn.ReLU(inplace=True), nn.MaxPool1d(2, 2), nn.Conv1d(32, 64, kernel_size=8, stride=1, padding=3), nn.BatchNorm1d(64), nn.ReLU(inplace=True), nn.AdaptiveAvgPool1d(1), ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(64, feature_dim), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(feature_dim, num_classes) ) def forward(self, x): f = self.feature(x) # (B, 64, 1) return self.classifier(f)逻辑说明:第一层卷积核64,在1024点输入下能覆盖约1/16个输入长度,接近一个冲击响应的持续时间。padding设为31,保证卷积后长度仍是1024。每个池化让长度减半,三次之后变成128,最后全局平均池化直接变成64维。分类器再接128维全连接和输出层。参数说明:num_classes按你的标签类别数改,比如只区分正常和故障就是2,分故障位置就是4,细粒度到损伤尺寸就是10。Dropout只加在分类器里,特征部分不需要,否则特征容易过度平滑。
训练时用交叉熵损失,Adam优化器,学习率从1e-3开始,如果训练不稳就降到3e-4。批量大小64或128。训练20到30个epoch,每轮在验证集上保存最优模型。这里要留一个验证集,和测试集区分开。验证集负责调参和选模型,测试集只在最后评估时碰一次。
4.3 训练CNN并提取特征
训练CNN的逻辑很简单,但有几个点需要注意。第一个是学习率衰减,我习惯在验证集准确率连续5个epoch不提升时,把学习率乘以0.1。第二个是早停,防止过拟合到训练集的噪声。第三个是记录最佳模型,不是在最后一个epoch保存,而是保存验证集表现最好的那一次权重。
下面是一个简单的训练循环,保存验证集F1最高的模型:
def train_model(model, train_loader, val_loader, epochs=30, lr=1e-3): device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device) optimizer = torch.optim.Adam(model.parameters(), lr=lr) criterion = nn.CrossEntropyLoss() best_f1, best_state = 0, None for epoch in range(epochs): model.train() for x, y in train_loader: x, y = x.to(device).float(), y.to(device).long() optimizer.zero_grad() out = model(x) loss = criterion(out, y) loss.backward() optimizer.step() # 验证 model.eval() all_pred, all_label = [], [] with torch.no_grad(): for x, y in val_loader: x = x.to(device).float() pred = model(x).argmax(dim=1) all_pred.extend(pred.cpu().numpy()) all_label.extend(y.numpy()) from sklearn.metrics import f1_score f1 = f1_score(all_label, all_pred, average='macro') if f1 > best_f1: best_f1 = f1 best_state = model.state_dict() model.load_state_dict(best_state) return model逻辑说明:每个epoch先在训练集上更新权重,然后在验证集上算F1宏平均,记录最好的模型状态。参数说明:lr和epochs可以按数据规模调,样本多时epochs可以减少。验证集必须在数据切分时单独留出,不能用测试集替代。训练完成后调用extract_features(model, train_loader)和extract_features(model, test_loader),得到对应特征矩阵。
有一个经验是,CNN验证集准确率只要在90%以上,提取的特征给SVM基本都能超过95%。如果CNN验证集只有70%,说明特征没学好,这时候不要急着用SVM,回头看看输入长度、卷积核或者数据预处理,先把CNN基线提起来再说。
4.4 训练SVM并评估
特征提取完成后,SVM的训练在上一章已经讲清楚。这里要补的是评估细节。故障诊断论文里,单一准确率不够,需要混淆矩阵和各类的F1。尤其要关注那些容易被混淆的类别,比如滚动体故障信号弱、特征不明显,常常被误判为正常或内圈故障。
下面这个评估代码会输出分类报告和混淆矩阵图:
from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns import matplotlib.pyplot as plt pred = best_svm.predict(X_test_scaled) cm = confusion_matrix(y_test, pred) print(classification_report(y_test, pred, target_names=['Normal', 'IR', 'OR', 'B'])) plt.figure(figsize=(6, 5)) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues') plt.xlabel('Predicted') plt.ylabel('True') plt.show()逻辑说明:classification_report会输出每类的精确率、召回率、F1,比单个准确率更完整。混淆矩阵热力图能直观看到哪两类最容易混在一起。参数说明:target_names按数据集实际的标签顺序写,标签顺序要和训练时一致。如果类别数多,可以归一化混淆矩阵,使用fmt='.2f',同时显示百分比。
跑完这个流程,你就已经完成了一个完整的滚动轴承故障诊断实验。接下来要做的不是继续调参数,而是检查实验是否踩了数据泄漏相关的坑。很多看起来美丽的准确率,最后都倒在复现和换数据集上。
5. 避坑与常见问题:那些让准确率忽高忽低的细节
5.1 数据泄漏:归一化放在切分之前等于开卷考试
现象:训练集和测试集一起做归一化,测试准确率接近100%,但换一段完全新的数据后准确率掉到70%。原因:你在整个数据集上计算均值和标准差,测试集的统计信息在训练阶段就被模型看到了。滚动轴承振动信号幅值在不同工况下差异很大,测试样本的均值和标准差一旦被用于训练归一化,模型相当于提前知道测试样本的整体尺度,这属于典型的数据泄漏。
解决:先把信号切分成训练和测试两个不重叠的集合,再对训练集调用fit_transform得到scaler,保存这个scaler,用它对测试集调用transform。你可以打印一下训练集和测试集归一化前后的均值,会发现如果不做这一步,测试集分布信息和训练集完全融合在一起,模型训练时的损失曲线也会异常漂亮,但这种漂亮没有意义。
5.2 随机划分类别不均衡导致SVM偏向多数类
现象:总准确率95%,但某一类故障的召回率只有40%,查看预测结果,该类几乎全部被预测为其他类别。原因:故障类型样本数天然不均衡,比如内圈故障数据很容易采集,滚动体故障数据比较少。SVM优化目标是全局准确率,多数类的贡献更大,少数类被牺牲。振动信号里少数类往往是早期微弱故障,漏检的代价更高。
解决:切分数据集时使用train_test_split(..., stratify=y),保证训练集和测试集类别比例一致;给SVM设置class_weight='balanced',让少数类错误被赋予更高惩罚;如果样本量还是不够,用加噪声和时间偏移扩增少数类。不要只靠SMOTE,因为振动信号相邻点相关性很强,SMOTE在时域插值可能制造出不自然的波形,反而干扰CNN特征提取。
5.3 信号截断长度与重叠率对特征稳定性的影响
现象:输入长度从1024改成256,测试准确率从94%掉到82%,但训练准确率没怎么变。原因:256点对应不到一个旋转周期,一些样本恰好没有截到冲击,CNN只能根据噪声模式猜类别;另一些样本可能只截到冲击的一部分,特征表达不完整。模型在短样本上学到的模式碎片化,自然无法泛化。
解决:先用转速和采样率算旋转周期,再决定样本长度,通常取1到2个周期。如果不知道转速,看信号的包络谱或自相关,找到周期对应的滞后点数,再取整到2的幂。样本重叠率也很关键,重叠率太高会让相邻样本高度相关,训练集和测试集之间容易发生隐含的重叠。我一般让滑动步长不小于样本长度的1/2,测试集使用完全不重叠的样本,这样评估结果更可信。
5.4 特征标准化时用了测试集统计量
现象:用训练集标准化后,SVM测试F1只有0.8;把训练和测试特征合并后一起标准化,测试F1变成0.93,于是你觉得“必须合并标准化才好”。原因:合并标准化相当于测试集的均值和方差进入了训练过程,测试准确率被虚高。这是一个比信号层归一化更隐蔽的泄漏,因为很多人对特征维度放松了警惕。
解决:特征标准化同样遵循“只拟合训练集,然后转换测试集”。在extract_features之后,先得到X_train_feat和X_test_feat,然后写scaler.fit(X_train_feat),再分别transform。如果你想用PCA降维,PCA对象也必须在训练特征上fit,并在测试特征上transform。我通常在代码里把scaler和pca一起保存为joblib文件,最后做混淆矩阵时用同一套预处理对象加载。
5.5 随机种子与CUDA不确定性导致复现不了
现象:同一个文件连续跑三次,准确率分别是93.2%、95.0%、94.1%,每次都不完全一样,论文里的最佳结果无法复现。原因:PyTorch模型初始化是随机的,DataLoader的shuffle也依赖随机种子,CUDA卷积算子本身可能有非确定性的实现。这是深度学习训练的正常现象,但故障诊断领域二区和三区的SCI期刊往往要求实验可复现。
解决:在训练前固定所有相关种子。代码里加上:
import random import numpy as np import torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False逻辑说明:固定Python、NumPy和PyTorch的随机数生成器,并关闭CUDNN的自动优化,使得卷积计算在相同输入下尽量确定。参数说明:seed取什么值无所谓,但一旦固定就不要改。即使在固定种子之后,不同GPU型号之间仍可能有微小差异,所以论文中要报告多次独立重复实验的均值±标准差,而不是单次结果。
除了上面五个,还有一个隐性坑是训练集和验证集划分后,验证集被多次使用,模型选择时已经见过验证数据,测试集必须是全新的,而且整个调参流程只能碰一次测试集。如果你在实验过程中反复回到测试集看性能并调整参数,那测试集也已经变成了验证集,最终汇报的准确率会偏高。做科研一定要把数据划分边界守好,这是整个流程的底线。
6. 进阶:把诊断模型推向二区三区SCI的验证套路
6.1 用CWRU数据做基准对比与指标选择
如果想把1D-CNN+SVM写成论文,在CWRU这类公开数据上做基准对比是标配。对比方法至少要有:传统手工特征加SVM、1D-CNN加Softmax、2D-CNN时频图、LSTM或GRU,以及本文的1D-CNN特征加SVM。每个方法必须在同样的训练/测试划分下进行,调整各自超参到最好水平。指标不应只报准确率,最好同时给F1宏平均、召回率加权平均和多次重复实验的标准差。故障诊断领域二区和三区的SCI期刊越来越看重统计显著性,两个方法的准确率差0.5%不足以说明谁更好。
6.2 消融实验:证明1D-CNN+SVM不是玄学
消融实验要回答一个问题:SVM相比Softmax的增益来自哪里。建议至少做四组:一是完整的1D-CNN加SVM,二是1D-CNN加Softmax,三是原始振动信号经手工特征后加SVM,四是1D-CNN特征加随机森林。如果第一组最好,说明特征提取和分类器都有贡献;如果第四组和第一组差不多,说明分类器换不换不重要,CNN特征本身已经线性可分,这会让审稿人对混合模型的价值产生质疑。我一般还会把CNN特征直接从不同的池化层取出来比较,证明“在哪个抽象层级喂给SVM效果最好”,这是很扎实的讨论起点。
6.3 参数敏感性分析与t-SNE可视化
参数敏感性分析能让审稿人看到你的模型不是在一个点上偶然最优。画一条曲线,横坐标是输入长度从256到2048,纵坐标是F1值,你能直观看到长度太短时性能迅速下降,这是“为什么选1024”的最好证据。SVM的C和gamma网格可以画成热力图,而不是只给大家看选出来的最优值。
t-SNE可视化是更深层的解释工具。把原始信号的高维特征和经过CNN提取后的特征都降到二维,并标注类别颜色,如果CNN特征不同类别明显聚成簇,就说明网络学到了判别信息。下面是一个关键且可复现的t-SNE代码片段:
from sklearn.manifold import TSNE import matplotlib.pyplot as plt tsne = TSNE(n_components=2, perplexity=30, random_state=42) feat_2d = tsne.fit_transform(X_test_scaled) for i, label in enumerate(np.unique(y_test)): mask = y_test == label plt.scatter(feat_2d[mask, 0], feat_2d[mask, 1], s=5, label=f'Class {label}') plt.legend() plt.show()逻辑说明:TSNE会把测试集特征映射到二维,使相近的样本在二维图中靠近。如果同类样本聚成一团、不同类之间有明显间隔,就说明特征可分性高。参数说明:perplexity是近邻数,样本量少时设20左右,样本量超过500可以设30;random_state固定,保证每次画出的图一样。这个图放在论文里,比准确率表格更有直观冲击力。
6.4 自己的习惯
无论最后能不能发SCI,我现在做滚动轴承诊断都会把实验配置记录成一份JSON,包含数据划分的随机种子、CNN结构参数、SVM的C和gamma、标准化是否来自训练集。这个习惯救了我好几次。有时候过两周回看,某个准确率的异常波动并不是模型问题,而是当初不小心把StandardScaler用全局数据拟合了,这类错误如果不记录就特别难查。另外,我会把每次实验的混淆矩阵和t-SNE图存成文件,归档到和代码相同的目录,这样写论文时不用重新跑实验。混合模型不是把两个模型简单串联,而是要让CNN负责抓振动形态特征,SVM负责在特征空间里画边界,你实验里真正的价值点在于证明它们在你的数据上确实互补。希望帮到你。
本文还有配套的精品资源,点击获取