简介这份资料包围绕一维WGAN在故障轴承振动信号生成中的应用展开面向具备Python基础、希望用深度学习做故障诊断数据增强与信号合成的高校学生、研究人员及工程实践者。内容以GAN-1D为主线提供可运行的训练与测试脚本、轴承振动数据及生成结果展示图并附带CNN检查点文件便于读者对比生成信号与真实样本、理解模型训练与采样流程。压缩包共19个文件约22.03MB主要由Python脚本、MAT数据、PNG可视化图以及TensorFlow检查点相关文件组成结构紧凑覆盖数据、训练、测试与结果查看几个环节。训练入口支持调节学习率、迭代轮数、采样间隔并可在9类信号中选择训练数据适合复现WGAN生成故障轴承振动信号实验。已有3387人学习可作为故障诊断数据扩充、一维GAN入门与实验对照的参考材料。1. 一条 1024 点的振动信号怎么被 WGAN“造”出来做轴承故障诊断的同行大多遇到过这种尴尬手里真实故障振动信号只有几十段模型一训练就过拟合换个工况准确率直接掉到六成以下。这个用 Python 写的 GAN-1D 项目就是冲着这个缺口来的它用 WGAN-GP 训练一个一维生成器直接生成 1024 点的故障轴承振动信号而不是图像并按“切分—生成—评估—扩样”这条链路把数据管线、网络定义和训练脚本组织成能跑的形式。它适合已经能跑通 PyTorch 训练、想在小样本故障诊断上做数据增强的人也适合想搞明白一维对抗生成和一维卷积怎么对齐的人。但它并不该拿来替代真实试验台也不能凭空生成物理上没出现过的故障模式这一点我后面会反复提到。2. 项目定位与技术选型一维 WGAN 凭什么补上故障样本的缺口2.1 三类真实场景不平衡、少样本、跨工况轴承故障数据的问题并不在于“数据少”而在于“分布偏”。健康状态下的振动信号采集成本几乎为零一台设备跑一天能切出上万段但内圈剥落、外圈剥落、滚动体点蚀这些故障样本要么得等设备自然劣化要么得上试验台人为加工缺陷往往一个类别只有几十段到几百段。类别比例差两个数量级的时候分类器会把“全部预测为正常”当成最优解召回率好看故障一个都抓不住。第二类场景是新设备的冷启动。产线换型、新机型上线历史故障库一条都用不上这时候能拿到的只有正常工况数据加少量已知故障。第三类场景是跨工况迁移源域某个转速、某个负载有充足标注目标域只有正常样本模型一迁移就崩。这三类问题的共同点是“某个类别的真实分布采样不足”而生成式扩样正好作用在分布这一层。项目把这三类场景统一成同一个接口按故障类别分桶每桶可以单独指定要生成多少段生成结果直接落成(N, 1024)的数组加标签能被任何分类脚本读进去。这里有个边界必须说清楚生成器只能在你给的真实样本附近做插值和延拓它学到的共振频带、冲击衰减形态都来自训练集。如果训练集里根本没有“早期微弱故障”这种形态生成再多样本也变不出来。我一般会把这句话写在项目 README 第一段免得后面有人拿生成数据去汇报“我们覆盖了全部故障模式”。2.2 为什么是 WGAN-GP而不是原始 GAN、DCGAN 或 VAE原始 GAN 的判别器用 Sigmoid 加二元交叉熵等价于在优化 JS 散度。当真实分布和生成分布的支撑集几乎不重叠时一维振动信号在 1024 维空间里极度稀疏这种情况太常见了判别器能完美分开真假梯度饱和生成器拿到的更新信号接近零训练就僵在那里。WGAN 换成一阶 Wasserstein 距离判别器改叫评论器输出的是实数分数而不是概率即使真假容易分它依然能给生成器提供连续的梯度方向。DCGAN 那套结构是为二维图像设计的卷积核 3×3、步长 2、BatchNorm 堆叠。搬到一维信号上不是简单地把Conv2d换成Conv1d图像的局部相关是二维块状的振动信号的相关性是沿时间轴的一维冲击加衰减振荡感受野要覆盖一个完整的冲击周期才有意义所以生成器的上采样层数、kernel size 都得跟着采样率重新算。VAE 我也试过重建损失MSE天然偏好平滑解生成出来的波形均值很准但高频共振带被磨平了做包络解调时轴承特征频率那一簇峰根本立不起来。这是我放弃 VAE 做振动信号生成的主要原因后来每次有人问“为什么不用 VAE 简单点”我都拿包络谱对比图回答。真正让训练稳定下来的是梯度惩罚GP。原始 WGAN 用权重裁剪把评论器的 Lipschitz 常数卡在 1 以内代价是参数被压到极小的范围评论器容量塌缩学不出复杂的评分曲面。WGAN-GP 改成在真假样本的连线上采样惩罚梯度范数偏离 1 的程度取消了裁剪评论器可以正常发挥。代价是每一步多一次反向传播训练慢差不多 30%这个开销我认为值。2.3 工程结构与技术栈一个能被改的目录长什么样项目结构不复杂但分层是清晰的模型、损失、数据各占一个模块三个入口脚本分别对应训练、生成、评估配置集中在configs/下面。要改网络只动models.py要换数据只动dataset.py这种切法对二次开发很友好。gan-1d-bearing/ ├── configs/ │ └── base.yaml # z_dim、n_critic、lambda_gp、类别映射都在这 ├── data/ │ ├── raw/ # 原始 mat/csv按工况分子目录 │ └── segments.npz # 切分产物x (N,1024) y (N,) ├── gan1d/ │ ├── models.py # Generator1D / Critic1D │ ├── losses.py # gradient_penalty │ ├── dataset.py # BearingSignalDataset 分桶采样 │ └── utils.py # 滤波、切分、包络谱、MMD、t-SNE ├── train_wgan.py # 训练入口 ├── generate.py # 按类别批量生成 ├── evaluate.py # 时域/频域/MMD 指标 └── runs/ # 每次实验一个目录存 ckpt 与曲线技术栈上没什么意外就是 Python 加 PyTorch 这一套信号处理部分靠 SciPy评估和可视化用 scikit-learn 的 t-SNE 加 Matplotlib。我一般要求 Python 3.9 以上、PyTorch 2.x因为这个项目里用到了torch.autograd.grad的create_graph参数和nn.ConvTranspose1d的output_padding版本太老会遇到行为差异。组件选型在这一维生成里承担什么深度学习框架PyTorch定义 1D 卷积、自动求导算梯度惩罚信号处理SciPysignal带通滤波、Hilbert 包络解调数值与 IONumPy切分、归一化、.npz存取评估scikit-learnt-SNE 降维、核函数 MMD可视化Matplotlib时域波形、包络谱、损失曲线3. 数据管线拆解从加速度计原始波形到 (N,1,1024) 张量3.1 带通滤波与重叠切分原始振动信号里混着大量低频机械噪声和工频干扰直接切段喂给生成器模型会把大部分容量花在拟合这些与故障无关的低频漂移上。常见做法是先做一次带通把频带卡在包含轴承共振区的范围内。采样率 12 kHz 时我一般取 200 Hz 到 4 kHz下限挡住转频以下的刚体振动上限留出足够宽的共振带让冲击激起的衰减振荡完整保留。import numpy as np from scipy.signal import butter, filtfilt def bandpass(x, fs12000, low200.0, high4000.0, order4): 零相位带通filtfilt 前后各滤一次避免相位偏移影响冲击位置 nyq fs / 2.0 b, a butter(order, [low / nyq, high / nyq], btypeband) return filtfilt(b, a, x).astype(np.float32) def segment(x, win1024, step512): 重叠切分win 是窗长step 是滑移步长step win 时样本间有重叠 starts range(0, len(x) - win 1, step) return np.stack([x[i:i win] for i in starts])filtfilt而不是lfilter是因为后者会引入相位延迟冲击在时间轴上整体后移多个样本拼起来看会错位虽然对生成任务影响有限但排查问题时容易误判。win1024在 12 kHz 下对应 85 ms 的窗频率分辨率约 11.7 Hz够分辨常见的轴承特征频率外圈通过频率通常在一百赫兹量级。想要更细的谱线可以上 2048 点分辨率翻倍但同样长度的原始数据切出来的样本数减半小样本场景下我不建议。step取win/2是个折中完全重叠step1会让相邻样本高度相关评论器容易记住训练集完全不重叠又浪费数据。步长越小样本越多但有效信息越少做小样本实验时我一般固定step512把变量留给“每类取多少段”这个更直观的参数。3.2 归一化为什么别急着对每段做 z-score归一化这一步很容易出问题。很多图像生成的教程习惯逐样本标准化放到振动信号上会抹掉一个关键信息故障的严重程度体现在冲击幅值上内圈轻微剥落和外圈重度剥落的波形形态可能相似幅值差好几倍。如果每段各自减均值除标准差所有样本的幅值都被拉到同一个量级生成器学到的分布里就没有“严重程度”这个维度了。我一般用整个训练集的统计量做统一缩放保留段间幅值差异def fit_scale(train_segments): 只用训练集估计缩放系数测试集和生成结果都用它防止信息泄露 return float(np.abs(train_segments).max()) def normalize(segments, scale): 映射到 [-1, 1]和生成器最后的 Tanh 输出对齐 return (segments / scale).astype(np.float32)这里有个必须对齐的点生成器最后一层是Tanh输出范围天然是[-1, 1]。如果真实数据只缩放到[0, 1]或者压根没缩放评论器一上来就能靠数值范围区分真假梯度惩罚会把评论器往“无视幅值只看形态”的方向拽训练能收敛但生成样本的幅值分布会跑偏。我在evaluate.py里加了一条断言检查真实数据和生成数据的abs().max()是否在同一量级这个检查救过我两次。提示缩放系数一定要存进 checkpoint 或者配置文件里。生成阶段要用同一个 scale 反归一化否则生成结果和真实数据差一个常数倍下游分类器直接失效。3.3 Dataset 与 DataLoaderdrop_last 不是可选项数据集类本身没什么花活就是把(N, 1024)的数组包成(1, 1024)的单通道张量标签转成long。真正容易忽略的是 DataLoader 的参数。import torch from torch.utils.data import Dataset, DataLoader class BearingSignalDataset(Dataset): def __init__(self, segments, labels, scale): self.x torch.from_numpy(normalize(segments, scale)) # (N, 1024) self.y torch.from_numpy(labels).long() # (N,) def __len__(self): return self.x.size(0) def __getitem__(self, i): # 升一维通道Conv1d 期望输入 (B, C, L)这里 C1 return self.x[i].unsqueeze(0), self.y[i] def build_loader(ds, batch_size64, shuffleTrue): return DataLoader( ds, batch_sizebatch_size, shuffleshuffle, drop_lastTrue, # 关键不完整的尾批会让统计量失真 num_workers2, pin_memoryTrue, )drop_lastTrue在普通分类任务里只是个小优化在 WGAN-GP 里是硬性要求。原因有两层其一评论器里用了InstanceNorm1d当批次只剩一个样本时归一化的统计量退化成单点输出不稳定其二Wasserstein 距离的蒙特卡洛估计对批次大小敏感尾批只有三五个样本时估计方差极大那一步的梯度基本是噪声训练曲线会出现规律性的锯齿。我调过很多次这个参数只要关掉drop_last损失曲线的毛刺就会明显变粗。3.4 按故障类别分桶少样本实验怎么配项目支持按类别单独生成所以数据集这一层就得先按标签分好桶。分桶不是为了训练训练时所有类别混在一起喂让评论器见到完整分布而是为了生成时能精确控制每个类别的输出数量。from collections import defaultdict def bucket_by_class(labels): 返回 {类别: 样本下标数组}用于少样本采样和按类生成 buckets defaultdict(list) for i, y in enumerate(labels): buckets[int(y)].append(i) return {k: np.asarray(v) for k, v in buckets.items()} def few_shot_subset(buckets, per_class50, seed0): 每个类别只随机取 per_class 段模拟少样本场景 rng np.random.default_rng(seed) idx [] for k, ids in buckets.items(): take min(per_class, len(ids)) idx.extend(rng.choice(ids, sizetake, replaceFalse)) return np.asarray(idx)per_class是这类项目最有实验价值的参数。我通常跑三组每类 50 段、200 段、全量分别看生成质量指标和下游分类增益。50 段那一组的曲线通常很乱评论器在几十步内就能把真假完全分开这时候要么把n_critic从 5 调到 3要么把lambda_gp从 10 往上抬一点让评论器不要跑得太快。另外一个实际问题是类别不平衡下的采样策略。如果训练时按原始比例采样正常样本会占据每个批次的八成以上评论器学到的“真实分布”几乎就是正常工况的分布生成故障样本时会明显偏离。我在分桶之后加了一个按类别均衡的WeightedRandomSampler让每个批次里各类别大致等量这个改动对生成故障类样本的质量提升比调学习率还明显。4. 网络与训练循环生成器、评论器和梯度惩罚的三件套4.1 生成器四层转置卷积把 100 维噪声拉成 1024 点生成器要把 100 维的隐向量变成 1024 点的一维波形最直接的做法是先线性映射成一个较短的序列再用转置卷积逐级上采样。项目里用了四层ConvTranspose1d每层 stride 2总上采样倍数 16所以线性层输出长度取1024/16 64。import torch import torch.nn as nn class Generator1D(nn.Module): def __init__(self, z_dim100, n_class4, base64, out_len1024): super().__init__() self.out_len out_len # 条件生成把类别 one-hot 拼到噪声后面一起送进线性层 self.fc nn.Linear(z_dim n_class, base * 8 * (out_len // 16)) self.net nn.Sequential( nn.BatchNorm1d(base * 8), nn.ReLU(True), nn.ConvTranspose1d(base * 8, base * 4, 4, 2, 1), # 64 - 128 nn.BatchNorm1d(base * 4), nn.ReLU(True), nn.ConvTranspose1d(base * 4, base * 2, 4, 2, 1), # 128 - 256 nn.BatchNorm1d(base * 2), nn.ReLU(True), nn.ConvTranspose1d(base * 2, base, 4, 2, 1), # 256 - 512 nn.BatchNorm1d(base), nn.ReLU(True), nn.ConvTranspose1d(base, 1, 4, 2, 1), # 512 - 1024 nn.Tanh(), # 对齐 [-1,1] ) def forward(self, z, labels): h torch.cat([z, labels], dim1) h self.fc(h).view(h.size(0), -1, self.out_len // 16) return self.net(h)几个参数值得说清楚。kernel_size4, stride2, padding1这组配置正好让每层长度翻倍不会出现尺寸对不齐的麻烦如果换成kernel_size3就得手动算output_padding调起来烦。base64时第一层通道数是 512参数量在几百万量级对一维信号已经足够再往上加容易过拟合那几十段训练样本。生成器里用BatchNorm1d是可以的这一点和评论器不同下面会讲原因。ReLU而不是LeakyReLU是因为生成器要产出确定性的形态负斜率在这里没有明显收益。类别条件用 one-hot 直接拼在噪声后面简单粗暴但有效比在每层加条件调制省事得多。4.2 评论器去掉 BatchNorm 和 Sigmoid 之后评论器是 WGAN-GP 里最容易写错的部分因为大家习惯性会照搬 DCGAN 判别器的写法加 BatchNorm、加 Sigmoid。这两样都不能要。class Critic1D(nn.Module): def __init__(self, n_class4, base64, in_len1024): super().__init__() self.body nn.Sequential( nn.Conv1d(1, base, 4, 2, 1), nn.LeakyReLU(0.2, True), # 1024 - 512 nn.Conv1d(base, base * 2, 4, 2, 1), nn.InstanceNorm1d(base * 2, affineTrue), nn.LeakyReLU(0.2, True), # - 256 nn.Conv1d(base * 2, base * 4, 4, 2, 1), nn.InstanceNorm1d(base * 4, affineTrue), nn.LeakyReLU(0.2, True), # - 128 nn.Conv1d(base * 4, base * 8, 4, 2, 1), nn.InstanceNorm1d(base * 8, affineTrue), nn.LeakyReLU(0.2, True), # - 64 nn.AdaptiveAvgPool1d(1), ) self.fc nn.Linear(base * 8 n_class, 1) # 输出标量分数不接 Sigmoid def forward(self, x, labels): h self.body(x).flatten(1) return self.fc(torch.cat([h, labels], dim1))为什么不能有 BatchNorm梯度惩罚要求评论器对单个输入样本的梯度范数接近 1而 BatchNorm 让同一个批次里的样本互相耦合某个样本的分数会受同批其他样本影响这时候“单样本梯度”这个概念本身就失去意义惩罚项算出来的值是在约束一个混合了批内统计的量训练会不稳定。换成InstanceNorm1d(affineTrue)之后归一化只在单个样本内部沿时间轴做样本之间解耦惩罚项才有意义。affineTrue是为了保留可学习的缩放和偏置不加的话评论器表达能力会弱一截。为什么不能有 Sigmoid评论器的输出是 Wasserstein 距离里的“分数”可以是任意实数加上 Sigmoid 就把它压回概率区间又变回 JS 散度那一套了。项目里最后的fc直接输出一维标量损失函数里用mean(fake) - mean(real)的形式语义是“生成样本的平均评分减真实样本的平均评分”。4.3 梯度惩罚WGAN-GP 真正贵的那一步梯度惩罚的逻辑是在真实样本和生成样本之间随机插值要求评论器在这个插值点上的梯度范数接近 1。因为满足 1-Lipschitz 的函数梯度范数处处不超过 1而最优评论器会尽量贴近这个上界所以约束它等于 1 是合理的。def gradient_penalty(critic, real, fake, labels, device, lambda_gp10.0): b real.size(0) # alpha 形状对齐到 (B,1,L)逐样本、逐时间点插值 alpha torch.rand(b, 1, 1, devicedevice).expand_as(real) interp (alpha * real (1 - alpha) * fake).requires_grad_(True) score critic(interp, labels) grads torch.autograd.grad( outputsscore.sum(), inputsinterp, create_graphTrue, # 必须为 True否则惩罚项无法回传 retain_graphTrue, only_inputsTrue, )[0] # 对每个样本把所有通道和时间点的梯度拉平求 L2 范数 norm grads.reshape(b, -1).norm(2, dim1) return lambda_gp * ((norm - 1.0) ** 2).mean()create_graphTrue是最容易漏的参数。梯度惩罚本身要对“梯度的梯度”求导如果这里设成 False计算图在算完梯度后就断了loss.backward()时惩罚项那部分梯度为零等于白算训练还能跑但完全没起作用损失曲线看起来也正常属于典型的隐性错误。我一般在第一次跑通之后专门打印一下gp.item()看它是否在 0.1 到 1 之间浮动如果恒等于 0 就说明这里出问题了。lambda_gp10是原论文的推荐值在振动信号上我试过 1、5、10、2010 附近比较稳。调小会让评论器跑得太快、生成器跟不上调大会让评论器被约束得太死板学不出有区分度的评分。4.4 训练循环n_critic 与两次反向传播的顺序WGAN 的训练节奏是“评论器多走几步生成器走一步”项目里用n_critic5。注意这个 5 是在每个批次内循环不是外部计数器写的时候容易和i % n_critic混起来。import torch.optim as optim G Generator1D(z_dim100, n_class4).to(device) C Critic1D(n_class4).to(device) opt_g optim.Adam(G.parameters(), lr1e-4, betas(0.5, 0.9)) opt_c optim.Adam(C.parameters(), lr1e-4, betas(0.5, 0.9)) for epoch in range(epochs): for real, labels in loader: real, labels real.to(device), labels.to(device) b real.size(0) # ---- 1) 评论器更新 ---- z torch.randn(b, 100, devicedevice) with torch.no_grad(): fake G(z, labels) # 只取样本不建生成器图 d_real C(real, labels).mean() d_fake C(fake, labels).mean() gp gradient_penalty(C, real, fake, labels, device, lambda_gp10.0) loss_c d_fake - d_real gp # 最小化拉低假分、抬高真分 opt_c.zero_grad(set_to_noneTrue) loss_c.backward() opt_c.step() # ---- 2) 生成器更新每 n_critic 步一次---- if step % n_critic 0: z torch.randn(b, 100, devicedevice) loss_g -C(G(z, labels), labels).mean() # 让假样本的分数尽量高 opt_g.zero_grad(set_to_noneTrue) loss_g.backward() opt_g.step()loss_c d_fake - d_real gp这个符号方向是固定的评论器想给真实样本打高分、给假样本打低分所以最小化“假分减真分”。生成器反过来最大化假样本的分数代码里写成负号加最小化。有人把它写成d_real - d_fake训练也能跑但评论器和生成器会朝同一个方向优化最后两者一起发散。betas(0.5, 0.9)而不是默认的(0.9, 0.999)是 WGAN-GP 的惯例一阶动量小一点避免评论器的评分曲面被历史梯度拖得太钝。学习率 1e-4 对这个小网络足够调到 2e-4 以上容易出现评论器突然崩掉、损失瞬间跳到几百的情况。4.5 环境、命令与产物目录依赖不多装完就能跑pip install torch numpy scipy scikit-learn matplotlib三条入口命令对应三个阶段。训练时把数据路径、实验名、关键超参传进去所有产物落到runs/exp/下包括每个 epoch 的生成器权重、评论器权重和损失日志。# 1) 训练--per-class 控制少样本规模-1 表示全量 python train_wgan.py \ --data ./data/segments.npz \ --epochs 300 --batch 64 --z-dim 100 \ --n-critic 5 --lambda-gp 10 --per-class 200 \ --out ./runs/exp1 # 2) 生成按类别各生成 1000 段输出 npz 供下游使用 python generate.py \ --ckpt ./runs/exp1/G_299.pth \ --n-per-class 1000 --out ./gen/exp1_aug.npz # 3) 评估MMD、峭度、包络谱对比一次跑完 python evaluate.py \ --real ./data/segments.npz \ --fake ./gen/exp1_aug.npz \ --metrics mmd,kurtosis,envelope \ --save-fig ./runs/exp1/eval.png三个参数我调得最多--per-class决定少样本的严酷程度--n-critic决定评论器和生成器的追赶节奏--lambda-gp决定评论器被约束的松紧。这三个是耦合的比如把per-class降到 50 时n_critic要相应降到 3否则评论器在几十步内就把真假分到极致生成器梯度消失。5. 避坑与排查训练曲线好看生成信号却不像轴承故障5.1 损失曲线一路下降生成波形是一条平滑正弦现象训练日志里评论器损失稳定在一个小范围生成器损失也在降看着一切正常。但把生成样本画出来是一条接近正弦的平滑曲线既没有冲击也没有衰减振荡包络谱上除了转频什么都没有。原因这是模式崩塌的典型表现。生成器发现只要输出一条固定形态的波形就能骗到一个中等分数于是放弃了多样性。常见诱因有三个n_critic太小导致评论器追不上、lambda_gp太大让评论器评分曲面太平、批次太小让 Wasserstein 估计方差过大。解决先把n_critic从 5 提到 7 到 10再看lambda_gp是否超过 10。批次小于 32 的时候基本没法稳定训练宁可减少迭代次数也要把 batch 撑到 64。另外在生成器里加一点噪声注入在某一层后拼一个小方差的高斯噪声也能缓解但会牺牲样本的干净程度我不太喜欢。5.2 包络谱上找不到轴承特征频率现象时域波形看着有冲击峭度也接近真实样本但做 Hilbert 包络解调后故障特征频率那一簇峰不明显跟真实样本的包络谱对不上。原因排在第一位的永远是归一化。对每段单独做 z-score 会把幅值信息抹平冲击的相对强度消失包络解调出来就是平的。第二位是带宽不够带通滤波的上限设得太低把冲击激起的共振带切掉了只留下低频成分。第三位是窗长选择不当1024 点在低采样率下频率分辨率不够特征频率的几根谱线糊在一起。解决改成用训练集统一 scale 归一化把带通上限抬到 4 kHz 附近对照原始信号的功率谱确认共振带位置如果采样率允许把窗长提到 2048 做一次对比。评估脚本里我固定加了一张真实和生成的包络谱叠图就是为了排查这一类问题。5.3 训练到一半评论器损失突然跳到几百现象前几十个 epoch 都很平稳某一步之后评论器损失剧烈震荡生成样本全部退化再也没恢复。原因评论器崩了。最常见的是梯度惩罚里create_graphFalse惩罚项没生效评论器无约束地放大自己的输出尺度分数越跑越大。其次是学习率偏高评论器参数一步跨太远评分曲面出现尖峰插值点的梯度范数爆炸。第三是数据里混进了 NaN 或者极大值某些通道传感器掉线会输出满量程几段异常样本足以毁掉整个训练。解决检查create_graph是否设为 True并在日志里打印gp.item()正常情况下它应该在 0.1 到 1.0 之间把学习率降到 5e-5 再跑在数据加载阶段加一条范围检查把绝对值超过阈值比如 10 倍标准差的段直接剔掉。另外别忘了 Grad 裁剪虽然 WGAN-GP 理论上不需要但作为保底加个clip_grad_norm_(1.0)能防止偶发爆炸。5.4 生成质量指标很好下游分类器却毫无提升现象MMD 数值很低t-SNE 图里生成样本和真实样本混在一起看着分布对齐得不错。但把这批生成数据加进训练集分类器在测试集上的准确率一点没涨某些类别甚至掉了。原因分布对齐不等于判别信息保留。MMD 衡量的是整体分布距离对“类间边界”不敏感生成器完全可以把两个类别的样本都生成到中间地带整体分布看起来接近但类别可分性被破坏了。另一个常见原因是数据泄露切分时相邻重叠段的样本同时进了训练集和测试集导致基线准确率本身虚高加不加生成数据都差不多。解决先检查切分方式按原始信号文件而不是按段来划分训练集和测试集重叠段必须整组划分。然后在评估里加入“条件一致性”指标用一个小分类器给生成样本打伪标签看生成样本被分到目标类别的比例。这个指标比 MMD 更能反映扩样有没有用。5.5 换个工况重新生成模型完全失效现象在某个转速下训练好的生成器换到另一个转速的数据上生成出来的波形频率成分全错冲击间隔对不上。原因一维生成器学到的是特定工况下的时域形态转速一变冲击重复周期、共振频带的中心都变了模型没有外推能力。这是数据驱动方法的固有限制不是调参能解决的。解决要么按工况分别训练多个生成器最省事代价是模型数量膨胀要么在条件里加入转速这个连续变量做条件生成需要标注转速要么在损失里加物理约束项比如要求生成样本的包络谱在理论特征频率处有能量。第三种效果最好但工程量最大我一般先做第一种把流程跑通再考虑往第二种迁。6. 二次开发把生成器接到故障诊断分类器上6.1 增强比例的消融实验怎么设计生成数据的价值不能靠肉眼判断得用下游任务说话。我的做法是固定分类器结构一个三层一维卷积加全局池化就够只改训练集的增强比例跑一组对照。测试集必须是纯真实数据而且和训练集按原始文件切分重叠段整组划分否则数字全是假的。实验组真实训练样本生成样本观察重点baseline每类 500少样本下的下限aug-1x每类 50每类 50小幅增强是否有效aug-4x每类 50每类 200增益是否随比例继续上升aug-only每类 0每类 250纯合成数据能不能撑起训练real-full每类 5000真实数据的上限用来判断还差多少aug-only那一组经常被忽略但它最能说明问题如果纯生成数据训练的分类器准确率接近real-full说明生成器确实学到了判别性特征如果只在六成左右徘徊说明生成样本只学到了表面形态。我见过不少项目只报aug-4x好看的数字一问aug-only就露馅。6.2 条件生成扩展到新故障类别项目里的条件生成是把类别 one-hot 拼在噪声后面这个设计扩展起来很方便新增一个故障类别只要在configs/base.yaml的label_map里加一行把n_class加一然后用该类别的真实样本微调生成器。注意label_map的顺序必须和训练时一致one-hot 位置错了模型不会报错只会安静地生成错误类别的波形这个 bug 我调了大半天才找到。微调有个技巧只解冻生成器的后半段最后两层转置卷积和类别嵌入部分前半段保持冻结。新类别的时域形态通常在低频结构上和已有类别相似差别在冲击间隔和衰减速度这些细节上全量微调容易把已经学好的基础形态破坏掉。学习率开到 5e-5跑 30 到 50 个 epoch 基本够用。6.3 物理约束与包络谱校验纯对抗损失没有物理概念它只关心“评论器分不出来”。想让它生成更像真实故障的信号可以在生成器损失里加一项频域约束对生成样本做 Hilbert 包络解调计算包络谱在理论故障特征频率及其谐波处的能量占比把这个占比作为奖励项。def envelope_peak_loss(fake, fs12000, f_target107.0, n_harm3, w0.1): 鼓励生成样本在理论特征频率及其谐波处有包络能量 env torch.abs(torch.fft.ifft(torch.fft.fft(fake, dim-1) * 0 0)) # 占位实际用 Hilbert # 实际实现env torch.sqrt(fake**2 hilbert(fake)**2) spec torch.fft.rfft(env, dim-1).abs() freqs torch.fft.rfftfreq(fake.size(-1), d1.0 / fs).to(fake.device) energy 0.0 for k in range(1, n_harm 1): band (freqs f_target * k - 3) (freqs f_target * k 3) energy energy spec[..., band].mean(dim-1) return -w * energy.mean() # 负号能量越大损失越小上面这段是骨架hilbert那一步实际要用torch.fft手动构造解析信号对频谱做单边化正频率乘 2、负频率置零再逆变换取模。f_target是从轴承几何参数和转速算出来的通过频率w是权重取值别超过 0.2太大就变成纯频域拟合时域形态会崩。这一项加上去之后包络谱的对比图会好看很多下游分类器的召回率通常也能再涨两三个点。代价是每类故障都得知道理论特征频率实际项目里这个信息一般是有的。从那以后我每次改生成器结构都会先跑一遍evaluate.py的三张图时域对比、包络谱叠图、t-SNE再动下游分类器绝不跳过这一步直接看准确率——省下来的那点时间后面排查“为什么指标涨了但现场还是漏报”要还回去十倍。希望这些踩过的坑能帮你少走两晚弯路。本文还有配套的精品资源点击获取