GAN-1D生成故障轴承振动信号:小样本诊断的补数据路径
简介:这份 Python 项目围绕一维故障轴承振动信号的生成任务展开,基于 WGAN 构建 GAN-1D 模型,面向具备一定深度学习基础、从事机械故障诊断或信号数据增强的开发者与研究人员。它可用于在真实故障样本不足时扩充振动数据,并通过训练脚本中的学习率、迭代轮数、采样间隔及 9 类信号选择等参数,观察生成效果与模型收敛过程。压缩包共 19 个文件、约 22.03MB,包含训练与测试用的 Python 脚本、MAT 格式振动数据、TensorFlow 模型检查点、生成样例图片以及说明文档,便于直接复现实验并对照生成图像判断质量。已有 3387 人学习下载,说明该方向具有较高关注度。读者可据此理解 WGAN 在一维轴承信号上的建模流程,掌握数据准备、模型训练、采样可视化与结果评估的基本思路,也可将脚本作为故障诊断数据扩充的起点,按自己的数据与实验条件调整参数继续研究。
1. GAN-1D 生成故障轴承振动信号:几十条实测样本撑不起诊断模型时的一条补数据路径
GAN-1D 说的是用一维卷积搭一套 WGAN,直接生成故障轴承振动信号,而不是生成图片。它出现的场景通常很朴素:某台设备上只采到几十段带标签的故障冲击,训练集小到诊断模型刚起步就过拟合,于是想用生成模型把样本量撑起来。这条路能走通的前提是把振动信号的物理形态摸清——采样率、切片长度、幅值区间、故障特征频率,一个没对齐,生成出来的东西在时域上看着像,包络谱上却是一片平。下面按实际跑通的顺序讲:信号长什么样、为什么选 WGAN、代码怎么落、踩坑在哪、参数边界怎么定。
2. 一维振动信号与二维图像差在哪:切片形状、归一化区间和 WGAN 选型的三笔账
把图像 GAN 的代码改成处理振动信号,改完之后直接训练,十有八九会在频域评估这一关翻车。时域损失降下去了,波形看着也有冲击节奏,但把生成样本丢进包络谱一看,该出峰的地方没有峰。问题多半不在生成器结构上,而在“你把这段信号当成什么”这件事上:振动信号是一维时序,它的可解释性全在频域里,采样率、切片长度、归一化区间这三笔账必须在写模型之前算清楚。
2.1 从 1024 点到 2048 点:一段轴承振动样本该切多长
轴承振动通常用加速度计采,采样率取 12 kHz 或 25.6 kHz。12 kHz 覆盖到 6 kHz 带宽,够把 2 到 5 kHz 这段结构共振带包进来——故障冲击激起的正是这段共振,包络解调要靠它当载波。采样率定低了,共振带被折叠进来,包络谱上就会多出一堆来路不明的峰,那时候你连真假都分不清,更别说判断生成质量。
切片长度不是随手定的数,它直接决定频率分辨率:Δf = fs / N。12 kHz 采样、1024 点一段,分辨率 11.7 Hz;2048 点是 5.86 Hz;4096 点是 2.93 Hz。外圈故障特征频率在 1500 r/min 下大致落在 80 到 160 Hz,内圈略高,两侧还挂着转频边带。分辨率 11.7 Hz 时,特征频率和它旁边 ±转频 的边带会糊在一起,包络谱上看到的是一坨抬起来的宽带包,不是一根能指着说“这就是外圈特征频率”的尖峰。
我一般从 2048 点起步,4096 点更保险,代价是同样长度的原始记录切出来的样本数减半。样本本来就只有几十条的时候这个代价不小,所以要权衡:类别样本数低于 100 条,用 2048;高于 300 条,可以上 4096 换更好的频率分辨率。
重叠率也影响成品质量。我固定 50% 重叠,2048 点一段、步长 1024。不重叠的话相邻样本首尾相接,边界处的冲击被劈成两半,等于人为制造了一批“半截冲击”的坏样本,生成器会把这些断头冲击当成正常形态学进去。重叠能缓解但不能彻底解决,更干净的做法是先按整段记录粗切、再在段内滑窗。
2.2 为什么用 WGAN 而不是原始 GAN:判别器丢掉 sigmoid 之后发生了什么
原始 GAN 的判别器输出过一个 sigmoid,损失等价于优化 JS 散度。问题出在真实分布和生成分布的支撑集几乎不重叠——2048 维空间里,几十上百条样本撑不起任何有体积的流形,判别器几轮就能把真假分到接近 100% 置信,JS 散度饱和成常数,生成器拿到的梯度趋近于零。表现出来就是判别器损失很快掉到 0 附近,生成器损失纹丝不动,采样出来还是初始噪声。
WGAN 换成 Wasserstein-1 距离,判别器不再叫判别器,叫 critic,最后一层去掉 sigmoid,直接输出一个实数打分。这个距离在两个分布不重叠时依然有定义、依然给得出有意义的梯度,对小样本、单类别的轴承信号特别关键。代价是要给 critic 加约束,让它满足 1-Lipschitz 连续。
约束有两种实现。原始 WGAN 用权重裁剪,把 critic 的每个参数夹到 [-0.01, 0.01],实现简单但容易把容量裁没,critic 学成一个很钝的函数,生成质量上不去。WGAN-GP 改成梯度惩罚:在真假样本的连线上随机插值,要求 critic 在插值点上的梯度范数接近 1,超出的部分按平方罚。它对批内每个样本单独算梯度范数,不依赖跨样本统计,所以 critic 里不能放 BatchNorm——放了梯度惩罚基本失效。
选 WGAN-GP 还有个实际好处:critic 的损失近似反映两个分布之间的 Wasserstein 距离,它下降通常意味着生成在变好。虽然这个相关性也没那么可靠,但比原始 GAN 里两个都看不懂的损失强。我一般同时看真实样本打分和生成样本打分的均值差,这个差值比损失曲线本身直观。
2.3 生成器与判别器的通道账本:一维卷积堆叠怎么排
GAN-1D 的骨架说到底就是把图像 GAN 里的 Conv2d 换成 Conv1d、上采样换成 ConvTranspose1d,但有两处不能照搬。第一处是长度账要算准,一维转置卷积的输出长度是 L_out = (L_in - 1) × stride - 2 × padding + kernel_size,配 kernel=8、stride=4、padding=2 时正好放 4 倍,三次就放 64 倍。32 点起步,三次之后是 2048 点,这是整个结构能对上的前提。
第二处是归一化层。生成器里可以用 BatchNorm1d,批大小 64 时统计量够稳;critic 里不能用,换 LayerNorm 或者干脆不加。有些实现用 InstanceNorm1d,效果也可以,它对每个样本每个通道单独归一化,不跨样本,不破坏梯度惩罚的前提。
latent 维度一般取 64,取 100 也行,主要影响生成多样性和训练前期收敛速度,对最终包络谱的形状影响没那么大。生成器先用一个 Linear 把 64 维映射到 base×4×32 再 reshape,base 取 64 时是 256 通道 × 32 点,然后 256→128→64→1,通道逐层减、长度逐层翻。判别器完全镜像:1→64→128→256,长度 2048→512→128→32,最后 Flatten 接一个 Linear 到标量。
通道数下限大约 32,再低生成信号的细节撑不住,包络谱上既没有毛刺也没有峰;上限受显存约束,2048 点、base=128 时单卡 batch 64 已经吃紧。核宽 8 是折中值:核太小(4)感受野不够,冲击的包络形状学不出来;核太大(16 以上)参数涨得快,几十条样本下更容易过拟合到训练集那一小撮形态上。
3. 把 GAN-1D 跑通的最小闭环:切片归一化、模型搭骨架、梯度惩罚训练
一套能跑通的实现其实不到两百行,但每一段都有坑。下面按数据→模型→训练的次序给三段代码,每段后面说清楚参数怎么改、改错了会怎样。代码假设单卡环境,输入是已经按整段记录整理好的一维数组,长度随意,切窗在函数里做。
3.1 切片、去均值与分位数归一化:三行预处理决定后面能不能收敛
import numpy as np def slice_signal(sig, win=2048, step=1024, clip_pct=99.0): """把一条长振动记录切成定长样本,返回 (N, win) 的 float32 数组。""" sig = np.asarray(sig, dtype=np.float32) starts = range(0, len(sig) - win + 1, step) seg = np.stack([sig[i:i + win] for i in starts]) # 1) 去均值:消掉传感器直流偏置,否则 critic 靠这一个维度就能分真假 seg = seg - seg.mean(axis=1, keepdims=True) # 2) 分位数缩放:用 99 分位而不是最大值,抗削波野点 scale = np.percentile(np.abs(seg), clip_pct, axis=1, keepdims=True) + 1e-8 seg = seg / scale # 3) 裁到 [-1, 1]:和生成器最后一层 tanh 的值域严格对齐 return np.clip(seg, -1.0, 1.0)win决定频率分辨率,12 kHz 下 2048 对应 5.86 Hz;step控制重叠率,取win // 2就是 50% 重叠;clip_pct是抗野点的关键,取 100 就等于用最大值缩放,一段里只要有一个削波尖峰,整段幅值被压扁,峭度信息全丢。分位数取 99 到 99.5 之间比较稳,取太低(95)会把真实的冲击幅值一起削掉。
注意这里丢掉了每段的scale,实际训练里要把它存下来。反归一化的时候x = x_norm * scale,不存回来,生成样本就只有形状没有量纲,拿去和真实信号做幅值对比会得出完全错误的结论。
3.2 生成器与判别器的 PyTorch 实现:转置卷积从 32 点长到 2048 点
import torch import torch.nn as nn class Generator1D(nn.Module): """latent z -> 2048 点一维振动信号,输出值域 [-1, 1]""" def __init__(self, z_dim=64, base=64, out_len=2048): super().__init__() assert out_len == 32 * 64, "长度账对不上,改 base 或改上采样次数" self.base = base self.fc = nn.Linear(z_dim, base * 4 * 32) self.net = nn.Sequential( nn.ConvTranspose1d(base * 4, base * 2, 8, 4, 2), # 32 -> 128 nn.BatchNorm1d(base * 2), nn.ReLU(True), nn.ConvTranspose1d(base * 2, base, 8, 4, 2), # 128 -> 512 nn.BatchNorm1d(base), nn.ReLU(True), nn.ConvTranspose1d(base, 1, 8, 4, 2), # 512 -> 2048 nn.Tanh(), ) def forward(self, z): h = self.fc(z).view(z.size(0), self.base * 4, 32) return self.net(h) class Critic1D(nn.Module): """输出标量打分,不带 sigmoid;内部不出现 BatchNorm""" def __init__(self, base=64): super().__init__() self.net = nn.Sequential( nn.Conv1d(1, base, 8, 4, 2), # 2048 -> 512 nn.LeakyReLU(0.2, True), nn.Conv1d(base, base * 2, 8, 4, 2), # 512 -> 128 nn.LeakyReLU(0.2, True), nn.Conv1d(base * 2, base * 4, 8, 4, 2), # 128 -> 32 nn.LeakyReLU(0.2, True), nn.Flatten(), nn.Linear(base * 4 * 32, 1), ) def forward(self, x): return self.net(x).squeeze(-1)生成器第一层ConvTranspose1d的 kernel/stride/padding 三件套决定了放大倍数,改成 kernel=4、stride=2、padding=1 就是每次放 2 倍,需要多堆一层才能到 2048,参数量和显存都不一样。base是最该调的旋钮:32 起步看能不能收敛,能收敛再往 64、128 加。critic 里的LeakyReLU(0.2)斜率别随手改成 0.01,太小的话负半轴几乎没梯度,梯度惩罚会变得很不稳。
3.3 梯度惩罚和训练循环:n_critic 与 λ 怎么写进代码
import torch def gradient_penalty(critic, real, fake, device="cuda"): b = real.size(0) eps = torch.rand(b, 1, 1, device=device) # 每个样本一个插值系数 x_hat = (eps * real + (1 - eps) * fake).requires_grad_(True) d_hat = critic(x_hat) grad = torch.autograd.grad( outputs=d_hat, inputs=x_hat, grad_outputs=torch.ones_like(d_hat), create_graph=True, # 惩罚项本身要参与反向传播,漏了就白算 retain_graph=True, only_inputs=True, )[0] grad = grad.view(b, -1) return ((grad.norm(2, dim=1) - 1.0) ** 2).mean() # 逐样本求范数,不取 batch 平均 def train_step(G, D, opt_g, opt_d, real, z_dim=64, n_critic=5, lam=10.0, device="cuda"): bs = real.size(0) for _ in range(n_critic): z = torch.randn(bs, z_dim, device=device) fake = G(z).detach() loss_d = D(fake).mean() - D(real).mean() + lam * gradient_penalty(D, real, fake, device) opt_d.zero_grad(set_to_none=True) loss_d.backward() opt_d.step() z = torch.randn(bs, z_dim, device=device) loss_g = -D(G(z)).mean() # 生成器只跟 critic 的打分走 opt_g.zero_grad(set_to_none=True) loss_g.backward() opt_g.step() return loss_d.item(), loss_g.item()n_critic=5意思是每更新一次生成器,critic 先连算 5 步,目的是让 critic 尽量贴近当前分布下的最优解,再给生成器一个靠谱的梯度方向。lam=10是梯度惩罚的经典起点。优化器用 Adam,学习率 1e-4,betas 取 (0.5, 0.9)——beta1 用默认的 0.9 在 WGAN 里往往会拖慢收敛,动量太大让 critic 在插值点上抖。
fake = G(z).detach()这一句容易被漏掉。不 detach 的话,critic 那几步的反向传播会把梯度一路传回生成器,等于用 critic 的损失偷偷训了生成器,两个优化目标打架,训练曲线会变得完全没法读。
3.4 训练过程盯什么:三个必须打的日志指标
只打印 loss_d 和 loss_g 是不够的,这两个数在 WGAN 里没有绝对量级,只能看趋势。我固定打三样东西:
| 指标 | 计算方式 | 健康区间 | 异常时的第一反应 |
|---|---|---|---|
| Wasserstein 估计 | D(real).mean() - D(fake).mean() | 前期快速下降后缓慢收敛 | 一路上升说明 critic 过强或 λ 太小 |
| 插值点梯度范数 | 惩罚项里 norm 的均值 | 0.8 ~ 1.2 | 远大于 1 说明约束没生效,查 create_graph |
| 生成样本峭度 | 每批生成样本峭度的中位数 | 4 ~ 8(故障冲击) | 接近 3 说明生成成了高斯噪声 |
峭度这一项特别管用。健康轴承的振动峭度在 3 附近,带冲击的故障信号峭度能到 4 以上甚至十几。生成样本峭度长期贴着 3,基本可以判定模型没学到冲击成分,继续训下去也是浪费时间,不如回头查归一化和切片。
4. 训练不稳时的排查清单:五类翻车现场与对应处置
WGAN 的调试体验有点像玄学,同一个脚本换一批数据就崩。但崩的方式是有规律的,下面五类是我反复遇到的,按“现象—原因—解决”整理,遇到问题时从上往下对一遍,多数能定位到具体那几行。
4.1 生成信号像白噪声:归一化区间和 tanh 输出没对齐
现象:训练几百步后采样,波形是一条均匀的毛刺带,幅值铺满 [-1, 1],峭度接近 3,包络谱全平。
原因:预处理用了 min-max 缩放到 [0, 1],而生成器最后一层是 tanh 输出 [-1, 1]。两边差一个偏置,critic 学到的第一个特征就是“整体直流偏了多少”——真实样本永远偏 0.5,生成样本永远偏 0,靠这一个维度就能把真假分开,剩下的形状信息完全没必要学。生成器于是退化成输出接近常数的噪声。
解决:训练前把数据统一到 [-1, 1],去均值加分位数缩放两步都不能省。改完之后重新初始化模型再训,别在已经塌掉的权重上接着跑,学不到东西。
4.2 critic 损失一路俯冲、生成器梯度爆掉
现象:loss_d 前 100 步正常,之后线性下坠到 -30、-50,采样出来的幅值炸到饱和,峭度飙到几十。
原因:梯度惩罚里的torch.autograd.grad忘了create_graph=True,惩罚项变成一个常数,没有梯度,等于完全没加约束,critic 退化成可以任意陡的函数;另一种情况是梯度范数按整个 batch 求了一个标量再平方,惩罚强度被平均稀释,批内少数几个陡点没被压住。
解决:create_graph=True和retain_graph=True都写上,grad.view(b, -1).norm(2, dim=1)逐样本求范数再平方取均值。改完先把 λ 按 10 跑,观察梯度范数是否收敛到 1 附近。
4.3 包络谱里找不到故障特征频率
现象:时域看着挺像,冲击节奏也对得上,但包络谱上目标特征频率处没有峰,或者峰位偏了几赫兹。
原因:切片太短,分辨率不够,1024 点在 12 kHz 下只有 11.7 Hz,特征频率和转频边带糊成一团;也可能是生成器容量太小,2 到 5 kHz 的共振带学不出来,包络解调失去载波,解出来自然是平的。
解决:切片拉长到 2048 或 4096;base从 32 提到 64;训练步数加到两万步以上。评估时真假样本必须走同一条包络谱流程(同样的 Hilbert 变换、同样的窗函数、同样的点数),否则差异可能来自评估代码而不是模型。
4.4 多类故障混训后串味
现象:把外圈、内圈、滚动体三类混在一个生成器里训,生成样本的包络谱上同时冒出好几个类别的特征频率,哪一类都不像。
原因:latent 是连续无标签的,模型学到的是各类分布的“平均”,也就是模式平均(mode averaging)。加上样本本来就少,不同类别的冲击周期差异被抹平。
解决:按类别分开训各自的 GAN-1D,一个类别一套权重,最省事也最稳。真要做条件生成,就把类别 one-hot 拼进 latent,critic 的输入也带上条件标签,让它对每类分别打分——这样条件下的 Wasserstein 距离才是你真正要优化的东西。
4.5 第几千步突然 NaN
现象:loss 正常走几千步,某个 step 之后突然变成 NaN,之后所有权重全废,重启也复现不了同一位置。
原因:原始记录里有传感器削波造成的野点,99 分位缩放压不住;或者梯度惩罚里norm除到了接近 0 的值;也可能是开了混合精度,autograd.grad在 fp16 下数值不稳。
解决:训练前对原始记录做一次幅值筛查,超过 5 倍标准差且持续若干点的段直接丢掉;norm后面加 1e-12;梯度惩罚这条路径全程 float32,不要套 autocast。
5. 参数边界在哪里:n_critic、λ、latent 维度和通道数的取值区间
参数表网上一搜一大把,但大多是从图像 GAN 直接搬过来的,放到 2048 点的一维振动信号上未必成立。下面这几个区间是在一维信号上试出来的经验边界,给的是“能跑”的范围而不是“最优”,具体到某批数据还是要自己扫一遍。
5.1 n_critic 从 5 到 10:多算几步 critic 到底换来什么
n_critic控制每个生成器 step 之前 critic 更新几次。取 1 就是标准 GAN 的交替训练,取 5 是 WGAN 论文的推荐值,取 10 以上在小样本上反而有害。
原因在于 critic 越接近最优,给的梯度方向越准,但代价是每一步的墙钟时间线性增长。一维信号样本量小(几百到几千条),critic 很容易在 3 到 5 步内就收敛到当前分布的最优附近,再往上加只是在重复计算。我一般在样本数少于 500 条时用 5,多于 2000 条时用 3——样本多的时候每一步的批内信息量大,critic 单步就能学到足够多。
判断n_critic设得对不对,看 Wasserstein 估计的曲线:设得太小,曲线锯齿明显、上下跳;设得合适,曲线是平滑下降的;设得太大,前期下降很慢,因为生成器半天才更新一次。
5.2 λ 与批大小的联动:10 不是万能数
梯度惩罚系数 λ 控制 Lipschitz 约束的强度。WGAN-GP 原文取 10,这个值在图像任务上很稳,在一维振动信号上通常也没问题,但它和批大小是有联动的。
批大小越小,插值点的采样越稀疏,梯度范数的估计方差越大,这时候 λ 用 10 会让训练变得很抖。批
本文还有配套的精品资源,点击获取