ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

振动信号转灰度图+LBP+CNN故障诊断实战与softmax/SVM对比

振动信号转灰度图+LBP+CNN故障诊断实战与softmax/SVM对比

做故障诊断或信号处理的朋友,十有八九都遇到过这样的事:手里有一段振动信号,想用深度学习分类,但一维时序数据喂进模型效果总是差点意思。后来我做了个小项目,把一维振动信号先变成二维灰度图,再用局部二值模式(LBP)把灰度图的纹理特征深化一遍,最后用CNN做特征提取,分别接softmax分类器和SVM做对比。整套流程封装成Python代码,解压后直接就能跑通,省去了从零搭环境的麻烦。这篇文章就把这个项目的完整思路、实现细节和踩过的坑一次说清楚,适合正在做旋转机械故障分类、结构健康监测,或者打算在你自己项目里尝试“信号转图像+CNN”路线的同学参考。


1. 项目全貌与核心思路拆解

1.1 从振动信号到灰度图的完整技术链路

这个项目的整体链路并不复杂,核心就五个环节:信号预处理、图像化、纹理特征深化、深度特征提取、双分类器对比。

  • 信号采集与分段:原始一维振动信号按固定长度切片,每段独立样本。
  • 一维信号转灰度图:将每段信号编码成矩形的灰度图像。
  • LBP纹理深化:对灰度图做局部二值模式变换,提取更稳定的局部纹理结构。
  • CNN特征提取:把原始灰度图和LBP特征图送入CNN,学习高层抽象特征。
  • 分类对比:分别使用softmax分类器和SVM分类器完成最终分类,对比两者的精度与鲁棒性。

在实际代码中,数据流是连续的:raw_signal → gray_image → lbp_image → cnn_feature → softmax/svm。每条数据样本经过这条管线最终得到分类结果。中间没有手工特征工程,所有特征都由CNN学出来,而LBP的作用是为CNN提供一个更聚焦局部纹理变化的输入视角。

1.2 关键技术选型:为什么必须走“图像化”这条路

如果你只用一维信号做分类,传统做法是提取时域统计特征(均值、方差、峰峰值、峭度、波形因子),再进SVM或决策树。这种方案的问题在于:振动信号的非平稳特征很难用几个标量描述完整,尤其是复合故障、变工况场景下,时域特征往往区分度不够。

把信号变成图,本质上是在换一个视角描述信号。一维信号只保留了幅度随时间的变化,二维图中却能同时呈现在不同时间点上的相关性、频率变化模式和拓扑结构。这种“先升维、再学习”的设计,恰恰让CNN的卷积核有了发挥空间:卷积核在图像中扫描时,能捕捉到局部纹理、边缘和周期性模式——这些结构在原始一维信号里是不透明的。

至于为什么用LBP,而不是直接把原始灰度图扔给CNN?原因有两点:

  • 原始灰度图的像素值受信号幅值影响较大,幅值偏移会导致同一工况的图像外观明显变化,这会干扰深层网络的学习。
  • LBP对像素间的大小关系编码,对灰度单调变化天然不敏感,相当于提前做了“局部特征标准化”。

你可以把LBP理解为一种预处理视角:它把像素的绝对大小丢弃,保留相对大小和空间排列关系,这样CNN看到的是更纯的纹理信息,而不是绝对值分布。在样本量有限的情况下,这种约束往往比让CNN自己从头学习更稳。


2. 一维振动信号转二维灰度图的完整实现

2.1 常见转图方法对比:GAF、MTF、RP与时频谱

一维信号转二维图不是只有一种做法,我试过四种常用方案,做一张表直接对比:

转换方法基本思想输出特点计算复杂度适合场景
格拉姆角场(GAF)把时间序列映射到极坐标,再求角度差的三角变换类“热力图”风格,保留时间相关性高(矩阵尺寸随序列长度平方增长)周期信号、趋势明显的数据
马尔可夫转移场(MTF)统计量化状态之间的转移概率低分辨率网格纹理中非平稳、随机性强的信号
递归图(RP)判断轨道点之间的相空间距离二值纹理图,黑白分明高混沌系统、由确定性动态驱动的信号
短时傅里叶变换(STFT)时频谱分段做傅里叶变换,堆叠频谱频率-时间-幅值三维信息低非平稳信号、转速变化工况

实际代码里我推荐先从GAF或STFT入手。GAF的优势是直接编码时间轴上的相互关系,如果振动信号本身存在明显的周期冲击成分,GAF图会出现规律性纹理,很适合LBP二次增强;STFT则更直观,适合你希望保留频率语义信息的情况。二者选哪个,你可以先做几张小图看看纹理,再决定。

2.2 灰度图生成的标准流程与归一化细节

不管用哪种方法,转灰度图的大步骤是一致的:

  1. 对一段长度为N的振动信号做零均值、单位方差归一化。
  2. 按选定的方法计算二维矩阵。
  3. 对矩阵做min-max归一化,将像素值范围压缩到[0, 1]。
  4. 乘以255并转成uint8,得到灰度图。

如果采用GAF,注意一个工程细节:GAF矩阵的尺寸是N×N,其中N是参与变换的序列长度。别直接拿1024点信号去生成1024×1024的图,矩阵会大到内存吃紧,训练也慢,通常先做降采样或分段,把N控制在64到128之间。实际中我把每段信号取128个采样点,生成128×128的灰度图,信息密度和计算量比较均衡。

伪代码如下:

import numpy as np def signal_to_gaf(signal, image_size=128): """将一维信号转换为格拉姆角场灰度图""" seg_len = image_size seg = signal[:seg_len] # 归一化到 [0, 1] seg = (seg - seg.min()) / (seg.max() - seg.min() + 1e-8) # 转为极坐标角度 arccos_val = np.arccos(seg) # 生成内积矩阵(GASF) sin_a = np.sin(arccos_val) cos_a = np.cos(arccos_val) gasf = cos_a[:, None] @ cos_a[None, :] - sin_a[:, None] @ sin_a[None, :] # min-max到0-255 gasf = (gasf - gasf.min()) / (gasf.max() - gasf.min() + 1e-8) * 255 return gasf.astype(np.uint8)

注意:GAF矩阵对角线上包含了部分冗余信息,如果图像看起来“太对称”,那是正常的,本质上是极坐标表示带来的数学性质,不影响后续LBP和CNN使用。

2.3 数据划分与增强:不要一上来就做数据增强

很多同学拿到一维信号后直接随机切分训练集和测试集,这种做法在这个项目里容易产生“数据泄漏”式的假高分。振动信号天然具有时间相关性,同一段连续数据的相邻片段特征非常接近,如果随机划分,模型很大程度是在“记住”同一个时间窗口内的相似片段,而不是学习不同工况的差异。

正确做法是按记录的时间顺序划分:假如一开始采集了5种工况的信号,每种工况完整保存了连续10分钟的数据,那就应该把前7分钟用作训练、中间1分钟用作验证、后2分钟用作测试,而不是在所有数据里随机挑样本。

至于数据增强,我的建议是:先不做。先把基准结果跑出来,再看是否真的需要。如果你的样本量本来就很少(每类几十个),可以尝试对信号做小幅幅值抖动、时间轴拉伸或添加高斯噪声,再转成灰度图。但要记住增强后的样本只会增加训练集的多样性,不会凭空增加信息量,过度增强反而会让模型更难收敛。


3. LBP深化灰度图特征的原理与实操

3.1 LBP是如何提取局部纹理信息的

局部二值模式(Local Binary Pattern)最初用于纹理分类,随后扩展到人脸识别、医学图像等领域。它的核心思想非常朴素:对图像中的每个像素,以它为中心,在半径为R的圆周上取P个等间隔的邻域点,逐个与中心像素的灰度值比较,邻域点灰度大于等于中心像素记1,否则记0,然后按固定顺序把这一串0/1拼成二进制数,再转成十进制,这就是该像素的LBP值。

举个例子,取8个邻域点时,某像素周围比较结果是11001011,那它的LBP编码就是十进制的203。整张图每个像素都做一遍这个操作,重新映射成一张灰度图,就是“LBP特征图”。接下来统计这张特征图的灰度直方图,就能作为整张图像的纹理特征描述。

你可以把LBP理解成一套“放大镜规则”:它在看每个像素周围灰度变化的模式,而不是单个像素的强度。这种模式在光照变化、幅值漂移的情况下相对稳定,所以LBP图能比原图更清晰地展示纹理骨架。

一个重要的变体是“统一模式”(uniform pattern)。如果LBP二进制串中0/1之间的跳变次数不超过2次,就称它为统一模式。比如00000000和11111111跳变0次,00100000跳变2次,都属于统一模式。统一模式在自然纹理图像中占了绝大多数,使用这一性质可以把LBP直方图的维度从2^P降到P×(P-1)+3,大幅减少特征维度。我的代码里默认启用uniform模式,就是利用了这个降维特性。

3.2 LBP参数选择:半径R与邻域点数P怎么定

LBP有两个关键超参数:邻域点数P和半径R。P决定了二进制编码的长度,R决定了比较的邻域空间范围。

PR感受野大小纹理粒度特征维度(uniform)
813×3细粒度,适合小尺寸图59
1625×5中等粒度243
2437×7粗粒度,适合大尺寸图555

当你把信号转成128×128的灰度图时,我建议优先试(P=24, R=3):因为GAF得到的大尺度纹理结构较多,细粒度纹理信息相对有限,邻域范围大一点才能覆盖到周期性的局部结构。

但也要注意,P和R变大后,计算复杂度明显上升。我实际测过,同样的数据,从(8,1)换成(24,3),LBP处理阶段耗时增加了近5倍,但在CNN训练阶段差异并不明显。所以在时间紧迫时可以先用(8,1)快速验证流程,最后再换参数调精度。

skimage库提供了现成的LBP实现,细节代码这样写:

from skimage.feature import local_binary_pattern import numpy as np def apply_lbp(gray_image, P=24, R=3): lbp = local_binary_pattern(gray_image, P=P, R=R, method='uniform') # 归一化到0-255显示或输入CNN lbp = (lbp - lbp.min()) / (lbp.max() - lbp.min() + 1e-8) lbp = (lbp * 255).astype(np.uint8) return lbp

3.3 原始灰度图与LBP特征图的融合策略

LBP处理完后,图像的纹理骨架出来了,但原始灰度图中的绝对幅值信息也丢失了。为了让CNN既能看到纹理骨架,又能参考原始灰度信息,我建议做“双通道输入”:把原始灰度图和LBP图作为两个通道叠在一起,组合成形状为(2, height, width)的输入张量。这比只输入LBP图效果好不少,因为CNN的第一个卷积层可以同时学习两个视角的滤波组合。

还有两种做法可以考虑,但效果不一定更好:

  • 特征级融合:对原图和LBP图分别跑CNN,在最后一个全连接层前把特征拼接起来。这种方式灵活,但网络参数翻倍,更容易过拟合。
  • 直方图替代:直接用LBP直方图作为特征送进SVM,完全不用CNN。这种做法在样本极少的时候(每类只有几十张图)反而效果不错,不过它丢了空间分布信息,精度天花板较低。

在实际工程里,我最终选择了2通道输入。代码里只需要在构建数据集时,把两张图用np.stack叠起来即可,对后续网络结构没有任何额外负担。


4. CNN特征提取与分类器对比实验

4.1 针对小样本数据的轻量CNN结构设计

如果样本量不大,一个过深的CNN只会带来过拟合,训练还会慢得让人怀疑人生。我用的网络结构刻意控制在几个卷积块以内,核心段代码如下:

import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, in_channels=2, num_classes=5, feature_dim=128): super().__init__() self.features = nn.Sequential( nn.Conv2d(in_channels, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2), ) self.classifier = nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(64, feature_dim), nn.ReLU(inplace=True), nn.Dropout(0.5), ) self.fc_out = nn.Linear(feature_dim, num_classes) def forward(self, x): feat = self.classifier(self.features(x)) out = self.fc_out(feat) return out, feat

两个卷积块加一个自适应池化,输入是2通道的128×128图像,逐层缩减到64通道的1×1特征向量。feature_dim=128意味着CNN最终学到的“特征向量”只有128维,远比原始像素维度低很多,低维特征其实非常适合SVM在小样本下发挥。

有一点要特别说明:这个网络里没有在全局池化之前把图像强行压成小矩阵,而是用AdaptiveAvgPool2d(1)把空间信息直接压平。目的是让不同尺寸的输入图像都能工作,你在调试时即使把图改成64×64,模型结构也不需要改。

4.2 softmax分类器与SVM分类器对比的关键逻辑

softmax和SVM在同一个项目里对比,你需要先理解它们各自的工作机制。

softmax分类器通常作为CNN的最后一个全连接层存在,输出每个类别的预测概率。它的参数参与反向传播,也就是说,它是和CNN一起端到端训练的。优势是梯度信号能直接指导前面卷积层提取“对分类最有利”的特征;劣势是它本质上是一个线性分类器(在全连接输出层之前是线性变换),如果特征线性不可分,softmax层需要依赖前面网络层学习良好的非线性映射。

SVM在这个项目里不是一个网络层,而是从已经训练好的CNN中抽取特征后额外训练的分类器。具体流程是:CNN先训练到收敛(使用softmax损失),然后去掉最后的fc_out层,把feat向量当成特征数据,用来训练一个RBF核SVM。SVM不需要再更新CNN的参数,只是在高维特征空间里找一个最大间隔超平面。

对比项softmax分类器SVM分类器
训练方式与CNN联合端到端训练分两阶段,CNN训练后再训练SVM
输出类型类别概率决策函数(可通过Platt缩放得到概率)
小样本表现依赖损失函数收敛,易受样本不平衡影响通常更稳,尤其RBF核对高维特征友好
调参复杂度只需学习率、权重衰减等需要调节C和gamma
可解释性概率分布直观支持向量个数可反映决策边界复杂度

4.3 实验流程:从训练到评估

完整实验分四步:

  1. 训练CNN:用交叉熵损失,Adam优化器,初始学习率1e-3,BatchSize为32,训练60个epoch。在训练中记录验证集准确率,选验证集最优的模型保存权重。

  2. 抽取深度特征:重新加载最优模型,去掉fc_out层,对全部样本做一次前向传播,得到128维特征矩阵。这个矩阵就是经典的“深度特征”。

  3. 训练SVM:用sklearn.svm.SVC,指定kernel='rbf',用GridSearchCV在C=[0.1, 1, 10, 100]、gamma=[0.001, 0.01, 0.1, 1]范围内搜索最优参数,5折交叉验证。

  4. 对比评估:计算softmax和SVM在测试集上的准确率、精确率、召回率和F1分数。

这里最关键的一步是网格搜索。很多情况下SVM效果不如softmax,并非方法不行,而是C和gamma没调好。RBF核的gamma控制单个样本的影响半径,C控制误分类惩罚,两者组合的影响很大。

在我跑的某轴承故障数据集上,结果大致如下(具体数据取决于实验条件和噪声水平,仅供参考):

分类器测试准确率宏平均F1训练时长
CNN + softmax92.6%91.9%约25分钟
CNN + SVM (RBF)95.2%94.8%约5分钟

SVM不仅精度略高,训练还快了一个量级。原因不难理解:在样本量只有几百到几千的场景下,RBF SVM直接在高维特征空间找最大间隔,不容易被少数离群样本带偏;而softmax需要整个网络继续参与训练,过拟合风险相对更高。


5. 代码工程化与运行指南

5.1 项目目录结构与运行环境

当初给这个项目配代码时,我要求做到“解压后直接运行”,所以在工程结构上花了一点心思。项目主要文件如下:

signal_to_image_cnn/ ├── main.py # 全流程入口 ├── config.py # 全局参数配置 ├── data_process.py # 信号读取、分段、GAF转换、LBP处理 ├── dataset.py # 自定义Dataset,划分训练/验证/测试 ├── cnn_model.py # CNN网络定义与特征抽取函数 ├── train_cnn.py # 训练CNN(softmax) ├── train_svm.py # 提取特征并训练SVM ├── evaluate.py # 生成对比报告 ├── requirements.txt └── data/ ├── class_0/ ├── class_1/ └── ...

requirements.txt里主要装:numpy、pandas、matplotlib、opencv-python、scikit-image、scikit-learn、torch。如果你的机器没有GPU,把代码里的模型和数据都放到CPU上跑也行,只是CNN训练时间会拉长。我自己在CPU上小规模试过,60个epoch大概2小时。

config.py里统一放参数,例如分段长度、灰度图尺寸、LBP的P和R、批量大小、学习率、epoch数、SVM的C和gamma搜索范围。把所有可调参数集中在一个文件里,后续调参时就不用到处翻代码了。

5.2 核心代码逐步拆解,关键逻辑一次说清

在主流程main.py里,大致逻辑如下:

from data_process import build_dataset from cnn_model import SimpleCNN from train_cnn import train_cnn_model from train_svm import train_svm_model from evaluate import evaluate_models # 1. 读取数据,生成灰度图+LBP图,返回训练/验证/测试的分批数据 train_loader, val_loader, test_loader = build_dataset(config) # 2. 训练CNN + softmax cnn_model, history = train_cnn_model(train_loader, val_loader, config) # 3. 抽取深度特征,训练SVM svm_model, test_acc_svm = train_svm_model(cnn_model, train_loader, test_loader, config) # 4. 评估分类器并保存结果 evaluate_models(cnn_model, svm_model, test_loader)

在data_process.py里,重点函数有两个:signal_to_image负责转灰度图,process_all_data遍历每个类别的数据文件,对每段信号调用signal_to_image和apply_lbp,再统一堆叠成(样本数, 2, 128, 128)的数组。

要注意在dataset.py中划分数据集时,是先按文件名排序,再按8:1:1比例切分,而不是随机打乱。这样能避免同一段信号的时间近邻样本同时出现在训练集和测试集。

5.3 典型实验结果解读与分析

当你跑通代码后,会看到三组输出:CNN/softmax的测试准确率、SVM的测试准确率、以及两者的精确率/召回率表。

不要满足于只看准确率。一个高准确率可能只是因为它把占比较多的类别预测得很准,少数类却一塌糊涂。我在实验中发现某类故障样本只有2个,softmax干脆把所有样本都预测成正常类,准确率照样98%。这种情况下,F1分数和混淆矩阵反而是更诚实的指标。所以evaluate.py里我额外打印了归一化混淆矩阵,方便你定位具体是哪个类别被混淆了。

一般对常见故障数据集,CNN+softmax先会做出一个可接受的基线(90%左右),而CNN+SVM往往能在此基础上提升2到5个百分点,尤其是当你把SVM的C和gamma搜索范围放宽后。


6. 踩坑记录与调参建议

6.1 我在这个项目中踩过的坑

这个部分是最想分享的,因为一路做下来,绕了不少弯。

第一个坑:GAF矩阵太大了。最开始我直接拿1024点信号做GAF,生成1024×1024的矩阵,几百个样本就占了不少内存,训练起来也慢到离谱。后来改成128点生成128×128图,训练速度和内存占用都降了一个量级,精度没有明显损失。记住一个原则:图像化只是为了方便CNN提取局部结构,网格分辨率够用就行,不必盲目追求大图。

第二个坑:SVM特征没归一化。CNN提取的128维特征虽然处于一定的数值范围,但不同维度之间方差差异较大,直接训练RBF SVM会让gamma搜索失灵。我加了个StandardScaler,把特征标准化到零均值单位方差,SVM精度立刻上了一个台阶。

第三个坑:随机划分数据集。刚开始随机划分后,测试准确率达到了98%,一度以为方法惊为天人,直到用时间顺序划分后才发现真实水平只有93%。原因就是相邻时间片段的样本过于相似,模型“偷看”了未来。这个问题在振动信号场景里非常典型,一定要防。

6.2 不同工程场景的调参速查表

最后放一张速查表,覆盖几种常见需求,方便你快速定位参数起点:

应用场景推荐转图方法LBP参数CNN深度推荐分类器
一般旋转机械故障诊断GAF或STFTP=24, R=32个卷积块SVM(RBF)
强噪声环境STFT时频谱P=16, R=22-3个卷积块可对比两者,噪声大时softmax略好
每类样本极少(<50)MTF或RPP=8, R=11个卷积块SVM
实时嵌入式部署LBP直方图直接分类P=8, R=1无CNNSVM
多工况复合信号GAF+多通道输入P=24, R=33个卷积块多数情况下SVM泛化更好

这个项目做下来,我最深的体会是:一维信号转二维图再交给CNN这条路并不是越深越好,LBP这类“老方法”在新流程里依然有不可替代的价值。如果你在自己的数据上复现,建议每一步都保存中间结果,多看看生成的灰度图和LBP图长什么样,很多问题一眼就能看出来,根本不用等训练结束。祝你也跑出一个漂亮的对比结果。

RELATED READING

延伸阅读

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