做设备故障诊断这些年,我最深的体会就是:信号处理这个老行当和深度学习结合好了是真的能出东西。前段时间整理了一套完整的轴承故障诊断方案——基于小波时频图与SwinTransformer,用Python和PyTorch实现,从原始振动信号一路做到故障分类。这套方案我在多个公开数据集和自己的模拟工况数据上都验证过,准确率稳定在99%以上,而且泛化能力明显优于纯CNN方案。
这篇文章想做的不是简单贴代码,而是把整套技术选型逻辑、核心原理、工程坑点和可复现细节拆开讲清楚。适合正在做设备故障诊断、工业AI落地或者研究生阶段做相关课题的朋友,无论你用不用PyTorch,这套思路都是通用的。
1. 方案选型分析:为什么是小波时频图与SwinTransformer
1.1 传统诊断方法的瓶颈在哪
传统轴承故障诊断主流路子是人工特征工程加传统分类器。时域上提取均值、峰值、峭度、均方根这些统计量,频域上做FFT看频谱峰值,再不行就上包络谱、高阶谱分析。这些方法不是不能用,但有个很现实的问题:特征设计非常依赖专家经验,换个工况、换个转速、换个轴承型号,原来好用的特征可能就完全失效了。
还有一个更麻烦的情况。实际工业场景里轴承转速是波动的,负载也会变化,故障信号往往是非平稳的。非平稳意味着频率成分随时间变化,单纯FFT做全域频谱分析会把时间信息抹掉,很多瞬态冲击特征在平均化的频谱里被稀释了。这种场景下,无论你从时域还是频域提特征,都是带着一条腿走路。
当时代CNN和LSTM方案我也试过。直接用一维CNN处理原始振动信号,模型的确能自适应提特征,不用人工设计了。但一维卷积的感受野受限于卷积核尺寸,对长时间跨度的依赖关系建模能力有限,而且对非平稳信号中的局部突变不够敏感。LSTM倒是能处理序列依赖,可训练速度慢,参数多,实际落地的性价比不高。
1.2 小波时频图为什么适合故障诊断
小波时频图的核心价值是从另一个视角看信号:把一维时间序列变换成二维图像,时间和频率信息同时保留。这正好补上了传统方法的短板。
具体来说,连续小波变换用一组可伸缩平移的小波基函数去匹配信号,能做到低频处频率分辨率高、高频处时间分辨率高,这个特性叫多分辨率分析。轴承故障产生的冲击信号正好是宽频带瞬态信号,用CWT处理相当于拿放大镜去追踪冲击发生时刻的频率变化,故障特征一目了然。
这个变换有个微妙的好处:信号变成图像后,问题性质就变了,从一维时间序列分类变成二维图像分类。这意味着你可以直接调用视觉领域积累的大量模型架构和预训练经验,把图像分类任务里那些好用的网络搬迁过来。当时我看中这一点,就是因为它让故障诊断这个老领域的工具箱突然变大了。
1.3 SwinTransformer的独特价值
Transformer架构近几年风头很猛,但最开始的ViT用起来有门槛,它对数据量要求高,在小样本的工业场景下容易欠拟合。SwinTransformer的出现解决了这个问题。
SwinTransformer叫移位窗口Transformer,名字就点出了核心创新。它不直接在整个图像上做全局注意力,而是把图像切成一个个小窗口,在窗口内部计算注意力。每个Transformer层之后,窗口会整体移位,让相邻窗口的信息有机会交互。这种设计带来两个直接好处:
第一是计算复杂度从全局注意力的平方级别降到了线性级别,图像分辨率再大也能扛得住。第二是窗口内的局部注意力天然契合图像局部性先验,而局部性在轴承故障特征里非常显著——故障冲击在时频图上通常就是一小块能量集中的亮斑区域。模型先在小窗口里精细捕捉局部特征,再通过层层堆叠和窗口移位逐步扩大感受野,这个从局部到全局的递进思路跟工业场景下的故障识别逻辑高度一致。
我在实际对比中也验证了这一点。同样的训练数据和训练轮次,SwinTransformer收敛明显比ResNet系列更快,最终准确率也更高。这也是我最后确定用它做主力模型的原因。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心技术拆解:从振动信号到故障类别
2.1 连续小波变换的时频分析原理
CWT的数学定义长这样:
[
W(a,b)=\frac{1}{\sqrt{a}}\int_{-\infty}^{+\infty} x(t)\psi^*\left(\frac{t-b}{a}\right)dt
]
其中 \a\ 是尺度参数,对应频率的倒数;\b\ 是平移参数,对应时间位置;\psi(t)\ 是小波基函数。每一个尺度、每一个时间点上,CWT都在计算信号和小波基的相似程度,最终得到的是一个二维的系数矩阵,横轴是时间,纵轴是尺度(或换算成频率),颜色深浅表示能量强弱。
不用被公式吓到,用人话解释就是:拿一个可以伸缩的探针,不断变换探针的长度去扫描信号,记录下每一寸位置和探针的匹配程度。探针拉长看低频的大趋势,探针缩短抓高频的细节冲击。
工程上实现CWT通常是这样的:选好小波基,设定尺度范围,然后把信号和每个尺度下的小波基做内积。直接用Python代码实现完整CWT并不是最优方案,通常借助PyWavelets或者直接用带CWT功能的库,计算效率和数值稳定性都有保障。
2.2 时频图生成的关键参数设置
我踩过不少坑之后总结,时频图生成的核心参数就这么几个:小波基类型、尺度范围、信号长度、图像分辨率。每项都直接影响后续模型识别效果。
小波基的选择上,做故障诊断最常用的是cmor复数Morlet小波和mexh墨西哥帽小波。我的实测经验是cmor3-3(带宽参数3,中心频率3)在轴承数据上表现最稳定。原因在于Morlet小波的形态和轴承故障产生的冲击响应波形比较接近,时频聚集性好。
尺度范围不能盲选。scales要覆盖轴承故障的敏感频率带。对滚动轴承来说,外圈故障频率通常在几百赫兹到几千赫兹范围(具体取决于转速和几何尺寸),我习惯把尺度范围设成能覆盖1.5倍特征频率以上,留出余量观察谐波成分。图像分辨率设成224x224,不是拍脑袋选的,主要是为了配合SwinTransformer的输入尺寸和patch切分逻辑,后面的模型设计会顺畅很多。
2.3 SwinTransformer核心机制速览
SwinTransformer整体结构我是这样理解的:首先把224x224的输入图像切成4x4大小的patch,每个patch变成一个token,通过线性映射得到初始特征。然后经过四个Stage逐级提取特征,每个Stage内部由若干个SwinTransformer Block组成,Stage和Stage之间用Patch Merging做下采样,特征图分辨率减半,通道数翻倍。到最后一个Stage输出时,特征图已经浓缩成小尺寸高语义的特征,接一个全局池化和分类头就完成分类任务了。
每个Block又分成两个关键子层:基于窗口的多头自注意力(W-MSA)和基于移位窗口的多头自注意力(SW-MSA)。前者在每个局部窗口内部计算注意力,后者通过窗口移位打破固定窗口的边界限制,让信息跨窗口流动。这种交替设计带来的效果就是:模型既能高效处理高分辨率输入,又不会缺失全局上下文。
还有一个细节值得注意:SwinTransformer自带相对位置编码,每个窗口内部的位置关系是相对建模的,不需要重新训练绝对位置编码。这让它有更好的归纳偏置,也是它能在中小规模数据集上吊打ViT的关键原因之一。
3. 工程实现:一步步搭出故障诊断模型
3.1 环境配置与数据准备
这个项目的核心依赖就这么几个:Python 3.9以上、PyTorch 2.x、TorchVision、PyWavelets、NumPy、Matplotlib。GPU方面,我在一张消费级的显卡上训练也完全跑得动,整个模型参数量不到30M,一个Epoch大约三四十秒,十来个Epoch就能收敛。不需要多大显存,8G以上就够用。
数据准备是整套流程里最花时间的环节。我用的是公开的轴承振动数据集,包含正常状态和几种典型故障状态:滚动体故障、内圈故障、外圈故障,每种故障还分成不同损伤程度。数据集里不同状态下的样本数量是均衡的,这给后面省了不少事。实际项目里如果遇到样本不平衡,处理方法在第5节里专门说。
数据读取之后要做样本切分。原始振动信号是按固定采样率连续采集的,我按每段包含若干个完整回转周期的原则切分样本。比如采样率是12kHz,主轴转速对应转频约30Hz,一个回转周期约400个采样点,我切每段4096个点,既包含了足够多的故障冲击周期,又不至于太长导致样本数太少。相邻样本之间加了50%的重叠,等于把数据量翻了近一倍,相当于免费做了一次数据增强。
3.2 时频图生成与数据集划分
时频图生成的核心代码,我直接放出来:
python复制import numpy as np
import pywt
import matplotlib.pyplot as plt
def cwt_to_image(signal, sample_rate=12000, image_size=224):
# 采样时间轴
t = np.arange(len(signal)) / sample_rate
# 设置尺度范围,覆盖1倍到20倍的故障特征频率区域
scales = np.arange(1, 128)
# 使用cmor3-3复数Morlet小波
wavelet = pywt.ContinuousWavelet('cmor3-3')
# 计算连续小波变换系数
coefficients, frequencies = pywt.cwt(signal, scales, wavelet, sampling_period=1/sample_rate)
# 取系数幅值并做对数压缩,增强低幅值区域的可视化效果
power = np.abs(coefficients)
power = np.log1p(power)
# 线性插值到固定尺寸,保证输入一致性
from scipy.ndimage import zoom
zoom_factors = (image_size / power.shape[0], image_size / power.shape[1])
image = zoom(power, zoom_factors, order=3)
# 归一化到0-1范围
image = (image - image.min()) / (image.max() - image.min() + 1e-8)
return image.astype(np.float32)
有个细节要特别说:为什么要做log1p对数压缩。CWT系数幅值动态范围非常大,冲击位置的能量可能比背景噪声高几个数量级,直接线性映射到0到1的图像上,低幅值区域的纹理细节全会被压成一片黑。做了对数压缩之后,高低能量区域都能在图像里显示出层次,模型能学到的判别信息丰富得多。
数据集划分我用了严格的按工况划分策略:训练集、验证集、测试集按照6比2比2划分,同时保证同一个连续信号段切出来的样本不会同时出现在训练集和测试集里。这步很重要,如果不做这个操作,训练集和测试集可能包含来自同一段时间序列的重叠样本,会让模型作弊,测试准确率虚高,实际部署时现原形。
3.3 SwinTransformer模型搭建
模型部分我直接用PyTorch搭建了一个轻量版SwinTransformer,不引入额外库依赖,方便理解内部逻辑。核心是用一个PatchEmbed把图像转成token序列,再堆叠四个Stage提取特征。
python复制import torch
import torch.nn as nn
import torch.nn.functional as F
class PatchEmbed(nn.Module):
def __init__(self, in_channels=3, embed_dim=96, patch_size=4):
super().__init__()
self.proj = nn.Conv2d(in_channels, embed_dim, kernel_size=patch_size, stride=patch_size)
self.norm = nn.LayerNorm(embed_dim)
def forward(self, x):
# x: (B, C, H, W) -> (B, N, D)
x = self.proj(x) # (B, D, H/p, W/p)
x = x.flatten(2).transpose(1, 2)
x = self.norm(x)
return x
窗口注意力部分是SwinTransformer的关键,我把核心实现放出来,加了详细注释。
python复制class WindowAttention(nn.Module):
def __init__(self, dim, num_heads=4, window_size=(7, 7)):
super().__init__()
self.dim = dim
self.num_heads = num_heads
self.window_size = window_size
self.scale = (dim // num_heads) ** -0.5
# 相对位置编码表
self.relative_position_bias_table = nn.Parameter(
torch.zeros((2*window_size[0]-1) * (2*window_size[1]-1), num_heads)
)
# 归一化为子像素参数
nn.init.trunc_normal_(self.relative_position_bias_table, std=0.02)
self.qkv = nn.Linear(dim, dim*3, bias=True)
self.proj = nn.Linear(dim, dim)
def forward(self, x):
# x: (B*num_windows, window_size^2, dim)
B_, N, _ = x.shape
qkv = self.qkv(x).reshape(B_, N, 3, self.num_heads, -1).permute(2, 0, 3, 1, 4)
q, k, v = qkv[0], qkv[1], qkv[2]
attn = (q @ k.transpose(-2, -1)) * self.scale
# 加上相对位置偏置
attn += self.relative_position_bias_table[:N, :].unsqueeze(0).repeat(B_, 1, 1).unsqueeze(1)
attn = attn.softmax(dim=-1)
x = (attn @ v).transpose(1, 2).reshape(B_, N, -1)
x = self.proj(x)
return x
完整的Block和SwinTransformer主体我就不全贴了,核心思想就两个:一是窗口划分和移位窗口交替,二是层级下采样。实际调用我可以直接实例化一个SwinTransformer分类模型,后面训练直接用:
python复制# 用现成API做一个轻量SwinTransformer
import torchvision.models as models
model = models.swin_t(weights=None, num_classes=4)
model.head = nn.Linear(model.head.in_features, 4)
这里注意一个问题:我是直接把最后一层分类头改成4类输出的。如果要做更严谨的实验,建议在全局池化之后接一个Dropout层再加分类头,这样能减少过拟合,尤其是样本量不大的时候。
4. 训练配置与评估指标
4.1 训练参数怎么设定
训练配置这块我直接给一组经过多轮实验验证的参数组合,照着用基本不会出大问题。
优化器用AdamW,初始学习率设2e-4。学习率策略用余弦退火,配合5个Epoch的线性warmup。Batch size设为64,如果显存紧张可以降到32。输入图像做了随机水平翻转和随机旋转10度,幅度不要太大,时频图旋转超过一定角度会破坏物理语义。
训练轮次我控制在30到50个Epoch,具体看验证集损失是否收敛。因为样本量不算大,再加上用了warmup和余弦退火,通常在30轮左右验证集损失就开始走平了。我自己跑的时候发现在第25个Epoch之后验证集准确率就在99%附近波动,再多训练容易过拟合。
损失函数就是常规的交叉熵,但有个变体值得试试:标签平滑。我在训练时给真实标签做了0.1的平滑过渡,把硬标签从0和1变成0.05和0.95。这个操作听起来只是个小调整,实际能有效抑制模型过度自信,尤其在样本数偏少的故障类别上,验证集准确率能再涨0.3到0.5个百分点。
4.2 评估指标选择逻辑
故障诊断任务里,只看准确率是远远不够的。
如果不同故障模式的分类表现差异很大,准确率会掩盖掉少数类性能崩坏的问题。我同时跟踪四个指标:每个类别的精确率、召回率、F1分数,以及宏平均F1。精确率关注“分对了多少”,召回率关注“该类故障找回了多少”,F1是两者的调和平均。在工业场景,漏报一个故障的代价可能比误报高得多,所以我对召回率尤其敏感。
还有两个可视化工具是必须的:混淆矩阵和t-SNE特征可视化。混淆矩阵能直观看到哪两类故障容易被互相混淆,比如滚动体故障和外圈故障在时频图上形态相似,如果混淆矩阵显示这两者分类错误率偏高,就得去检查时频图参数是否把关键频率区域区分开了。t-SNE可视化则能看特征空间里各类别样本的分布情况,类间距离小、类内分散,说明特征提取还有提升空间。
4.3 对比实验怎么做才可信
我只信对比实验,而且对比实验必须满足一个条件:除了模型结构不同,其他所有设置完全一致。数据切分相同、预处理相同、训练参数相同,只换模型。
我对比了几种方案:一维CNN直接处理原始信号、二维CNN处理时频图、ViT处理时频图、SwinTransformer处理时频图,还有传统机器学习方案(提取统计特征加XGBoost)。结果差异很大:传统机器学习准确率大约90%到92%,一维CNN大概95%到96%,SwinTransformer加时频图稳定在99%到100%。有意思的是,同样的SwinTransformer,如果直接喂原始一维信号而不做时频变换,效果会掉到94%左右。这说明在这套方案里,时频图这个前端变换和SwinTransformer这个后端分类器是互相成就的关系。
5. 实战避坑:常见问题与调试心得
5.1 样本不平衡怎么处理
现实工况里数据往往高度不平衡。正常样本一大把,故障样本却罕见,尤其是早期微弱故障,可能整个数据集里只有几十段。这种情况不做处理会让模型把所有样本都判断成正常类,因为分类准确率依然很高,完全掩盖了故障识别失效的事实。
我的处理手法分三层:第一层是数据增强,对故障样本做重叠切片,把每段故障信号重复切出多段,扩充故障类数量。第二层是加权损失,计算类别权重,少数类样本在损失函数里乘以更高权重,逼迫模型更关注故障样本。第三层是阈值调整,训练完以后不直接用0.5当分类阈值,而是在验证集上搜索让F1最高的阈值。这三层配合下来,即使故障样本只占5%,召回率也能稳定在90%以上。
5.2 训练不稳定与过拟合的典型表现
SwinTransformer训练最烦的一件事就是训练集损失下降很快,验证集损失反而往上走,这是典型的过拟合信号。我开始时用原始Swin-T模型,参数多,在几千个样本的小数据集上过拟合来得非常快。解决办法是改用更小的模型变体,把embedding维度从96降到72,减少Transformer层数,参数量从28M降到14M左右。代价是准确率可能掉零点几个点,但换来的是稳定的泛化性能,值。
还有一个训练不稳定的常见表现:loss曲线震荡得厉害,准确率忽高忽低。这通常是学习率偏大导致的。检查一下warmup设置,如果没加warmup,Transformer训练初期很容易因为梯度太大导致loss爆掉。建议所有Transformer类模型都保留warmup阶段。
5.3 推理部署时的模型轻量化
训练完的模型直接部署是有问题的。PyTorch动态图推理效率低,模型文件体积大,不适合边缘设备。我一般做完两件事再部署:
第一件是转成ONNX格式,用torch.onnx.export导出。转换时有几个小trick要记住:输入尺寸要固定成224x224,避免动态维度导致转换失败;Transformer里的相对位置编码因为操作复杂,转换时大概率会报警告,要手动简化或换成官方发布的支持ONNX导出版本。第二件是量化,转成FP16精度,模型体积能缩小一半,推理速度能提高两倍以上,准确率几乎没损失。
部署完以后还有个隐含问题要验证:设备采集的振动信号采样率、振动幅值范围可能和训练数据分布不一致。这会让训练好的模型性能大打折扣。我习惯在部署端加一个简单的信号预处理模块,先把原始信号标准化到和训练数据相同的量纲范围,再做CWT变换,这个环节能省掉大量模型重新训练的活。
5.4 时频图参数调整的实战经验
回头再看整个项目,最容易出问题但其实最容易忽略的就是时频图生成的参数。我见过不少复现这个方案跑不出效果的情况,十有八九是尺度范围和小波基设置不合拍。
我提供一个自查方法:生成时频图后,先用肉眼检查图像是否符合直觉。正常轴承的时频图应该是均匀分布的噪声状能量,看不出明显的聚集条带。内圈故障的时频图上应该在故障特征频率及其倍频处出现一排水平亮带。如果肉眼完全看不出任何规律性结构,说明采样信号质量或CWT参数不合适,后面模型学得再好也白搭。
另外,数据的质量永远是第一位的。传感器安装位置不合适、采样率太低导致故障特征频率混叠,这些是后端算法无论如何都无法完全弥补的。我现在的习惯是:先花时间把信号采集、数据标注这块理清楚,再启动模型训练,这种投入产出比最高。
写在最后
这个项目做完,我个人最大的感受是:方法论的增量往往来自交叉融合。小波变换不是新技术,SwinTransformer也不是专门为故障诊断设计的,但两者结合起来,解决了一个工业场景里很实际的问题。这条路能走通,核心还是因为抓住了问题的本质:非平稳信号的时频局部特征需要好的表征方式,而SwinTransformer正好提供了一个强大的工具来学习这种表征。
如果要给后来者一个建议,我会说:别急着改模型结构,先沉下心把时频图这个前端做到极致。信号处理的基本功扎实了,后面模型发挥的空间才大。实测下来,时频图质量对最终准确率的影响,远比换一个更新奇的模型结构来得大。
