简介:面向电力系统故障诊断研究的一份轻量资料包,基于Simulink仿真生成包含ACB三相接地等11种故障类型的数据集,并通过Python与scikit-learn完成故障分类建模。包内共7个文件,含2个MATLAB脚本、1个Simulink模型(slx)、1个Jupyter Notebook、1份Markdown说明、1份Word文档和1个CSV数据文件,整体仅652KB,结构清晰,便于快速下载、阅读与复现。内容围绕决策树、K近邻、支持向量机三种监督学习算法展开对比实验,其中SVM在生成数据集上测试准确率达到91.6%,同时呈现了从仿真数据采集、数据预处理到模型评估的完整流程。已有4242人学习/下载,适合电气工程、能源自动化领域的学生或研究人员用于课程设计、毕业设计、故障诊断算法验证或进一步扩展研究。即使对电力系统仿真不熟悉,也可借助文档和示例代码快速上手,替换故障参数完成个性化实验。
1. 电力系统故障分类数据生成模型:为什么说故障样本稀缺才是分类准确率的天花板
做过电力系统故障诊断的人都有同感:分类模型调了几个月,准确率卡在某个点上不去,不是模型不行,是训练数据里故障样本太少。一条 10kV 馈线一年真正录到故障波形的次数屈指可数,而健全运行的数据每分钟都在产生。拿一万条正常数据配三十条故障数据去训练分类器,模型学到的全是怎么判别“正常”,故障类别一出现就翻车。这个标题里的核心思路是用数据生成模型——常见做法是 GAN、VAE 或扩散模型——把稀缺的故障样本成倍地合成出来,再把合成样本和真实样本混合喂给分类器。这个方向业内验证过很多次,生成样本能显著改善少数类别的召回率,特别是单相接地这类占比最高的故障类型。这套方案适合两类人:一是做配网故障诊断算法开发的工程师,二是研究电力信号处理方向的学生,前者拿到的是能直接用的生产方案,后者拿到的是可复现的实验框架。
2. 数据侧先过关:故障样本要怎么处理才能喂给生成模型
2.1 原始录波数据的形态与归一化:先搞清楚你的样本长什么样
电力系统故障录波数据来自故障录波装置或配电终端,典型的格式是 COMTRADE,里面有 cfg 文件描述通道和采样率,dat 文件存实际采样值。最常见的配置是 4 通道——三相电压和零序电压,或者 6 通道——三相电压加三相电流,采样率从 1kHz 到 10kHz 不等。拿 6 通道、采样率 10kHz、录波时长 0.2 秒来算,一条样本就是 6×2000=12000 个浮点数。
生成模型不能直接吃原始波形,必须先做两步:切片和归一化。切片的目的是把故障发生时刻放在样本的正中间,这样生成模型学到的就不是“什么时候故障”而是“故障长什么样”。我一般取故障前 2 个周波加故障后 6 个周波作为标准样本长度,在 50Hz 工频下也就是 40ms 加 120ms,共 160ms。归一化有两种做法,一种是按通道各自做最大最小值缩放,另一种是按全局幅值缩放。前者保留了通道间的相对幅值关系,后者会丢失这个信息。
对于 GAN 类模型我更推荐前者,因为电流通道和电压通道的幅值本来就不在一个量级,全局缩放会让电压分量主导整个训练过程,电流波形细节直接被丢掉。归一化的代码逻辑不复杂,但要写对通道维。
import numpy as np def normalize_per_channel(waveform): """按通道归一化,保留通道间相对关系 waveform: numpy array, shape = (channels, sample_points) return: 归一化后的数组和每通道的缩放参数 """ # 初始化输出数组 normalized = np.zeros_like(waveform, dtype=np.float32) scale_params = [] for ch in range(waveform.shape[0]): ch_data = waveform[ch] ch_min = np.min(ch_data) ch_max = np.max(ch_data) # 防止除零:如果该通道全为0(比如某些未接入的通道) if ch_max - ch_min < 1e-8: scaled = ch_data scale_params.append((ch_min, ch_max)) else: scaled = (ch_data - ch_min) / (ch_max - ch_min) * 2 - 1 scale_params.append((ch_min, ch_max)) normalized[ch] = scaled return normalized, scale_params def denormalize_per_channel(normalized, scale_params): """从归一化空间还原实际幅值,用于后续评估和可视化""" waveform = np.zeros_like(normalized, dtype=np.float32) for ch in range(normalized.shape[0]): ch_min, ch_max = scale_params[ch] if ch_max - ch_min < 1e-8: waveform[ch] = normalized[ch] else: waveform[ch] = (normalized[ch] + 1) / 2 * (ch_max - ch_min) + ch_min return waveform这段代码里 scale_params 必须保存下来,因为生成模型输出的是归一化空间的数据,下游接分类器训练时或者做人工波形分析时,需要还原成实际的电压电流幅值。我在实际项目里吃过这个亏,生成出来的波形形态看起来正常,画出来幅度和真实波形差了十倍,一开始还以为是模型崩了,后来发现是反归一化参数没对上。
2.2 样本切片与类别标签组织:一个样本应该覆盖多少故障周期
切片长度直接影响生成样本的质量。取太短,比如只取故障前 1 个周波和故障后 2 个周波,模型学到的暂态过程不完整,生成出来的波形缺少故障发展的过程;取太长,比如整段录波全放进去,样本维度大幅增加,模型训练难度和训练时间都上去了,而且一个样本里大部分是正常的正弦波,故障特征被稀释了。
我一般按“故障前 2 周波 + 故障后 6 周波”来切,这个长度既能覆盖故障发生的暂态过程(前 3 个周波是暂态最剧烈的阶段),又不至于让数据维度爆炸。在采样率 10kHz 下,这个窗口就是 1600 个采样点,6 通道加起来是 9600 维。这个维度对生成模型是合理的,不会大到让训练不收敛。
类别标签的粒度和现场需求有关。如果做的是线路故障诊断,至少要区分单相接地、两相短路、两相接地短路、三相短路这几大类。如果做的是设备状态评估,可能还需要区分故障发生的相别。标签粒度越细,每个类别的样本数量就越少,这就形成了矛盾——生成模型是在学分布,每个类别至少要有几十条真实样本才能学出一个像样的分布,如果类别细化到几十种,每种只有几条样本,生成出来的东西大概率是一堆噪声。我的经验是:初始阶段标签只分大类,等生成效果稳定了再考虑细化。另一个可操作的做法是给每种故障类型至少凑 50 条真实样本再做生成。
3. 选什么样的生成模型:AC-GAN 还是 WGAN-GP,为什么
3.1 生成模型选型逻辑:用条件生成还是非条件生成
数据生成模型有多个选择,VAE、GAN、扩散模型都可以做分布学习,但电力系统故障数据场景有自己的约束:样本维度高、类别多但每类样本少、波形有强周期性。VAE 生成质量偏模糊,扩散模型效果好但训练和推理成本高,对工程师来说是负担。GAN 在「样本量少+高维度」场景下虽然训练不稳定,但生成质量上限最高,算子扰动的细节更锐利。在故障分类这个场景里,我们不是要用生成样本替代真实样本,而是当作数据增强手段,对样本细节的要求高于对多样性的要求,因此 GAN 是最常见的做法。
在这个基础上要多说一句条件生成的问题。直接跑一个普通的 GAN,它会在所有类别上学一个混合分布,生成出来的样本没法指定故障类别。我们要的是“给我生成 50 条单相接地故障”,所以必须用条件生成——给定一个类别标签,模型按这个标签去生成对应类别的样本。两类常见方案:AC-GAN(Auxiliary Classifier GAN)和 Conditional GAN(cGAN)。AC-GAN 的判别器同时输出真伪判定和类别判定,生成器输入里带上类别标签,实现简单且稳定。cGAN 则是把标签同时拼进生成器和判别器的输入。
对我来说 AC-GAN 在这里更合适,因为它让判别器除了判断真伪还被迫学习波形特征和故障类别之间的关系,这个额外的类别约束对生成器的约束力更强,不容易出现 mode collapse。特征匹配损失在这个场景里很关键,后面会专门展开。
3.2 AC-GAN 网络结构设计与核心代码:生成器和判别器的具体构造
实际我常用的是 AC-GAN 的框架加上 WGAN-GP 的损失函数,把这两者的优点拼在一起——AC-GAN 负责加类别条件,WGAN-GP 负责稳定训练。具体结构上,生成器输入是随机噪声 z(128 维)加类别标签的 embedding(用 4 类故障,embedding 维度取 32,拼起来是 160 维),经过全连接层升维再 reshape 成波形形状,然后接几个转置卷积层逐步把时间轴从 20 扩到 1600。
判别器是生成器的镜像:输入是一段 6×1600 的波形加条件标签,但现在时序信号不同于图像,卷积在时间维上用的是 1D 卷积(常被称为时序卷积),而不是图像里常用的 2D 卷积。几个卷积层把信号逐步下采样,最后分成两个头——一个输出真伪得分,一个输出类别概率。真伪得分不做 sigmoid,直接输出原始 logit,因为 WGAN-GP 用的是 Wasserstein 距离;类别概率要过 softmax 或者 log_softmax。
import torch import torch.nn as nn class Generator(nn.Module): def __init__(self, noise_dim=128, num_classes=4, channels=6, seq_len=1600): super().__init__() self.label_embed = nn.Embedding(num_classes, 32) input_dim = noise_dim + 32 self.fc = nn.Linear(input_dim, 256 * 100) # 先用全连接拉伸 self.conv_trans = nn.Sequential( nn.ConvTranspose1d(256, 128, kernel_size=8, stride=4, padding=2), nn.BatchNorm1d(128), nn.ReLU(), nn.ConvTranspose1d(128, 64, kernel_size=8, stride=4, padding=2), nn.BatchNorm1d(64), nn.ReLU(), nn.ConvTranspose1d(64, channels, kernel_size=8, stride=4, padding=2), nn.Tanh() ) def forward(self, noise, labels): label_emb = self.label_embed(labels) x = torch.cat([noise, label_emb], dim=1) x = self.fc(x) x = x.view(x.size(0), 256, 100) x = self.conv_trans(x) # 保证输出长度=seq_len,用插值兜底 if x.size(-1) != seq_len: x = nn.functional.interpolate(x, size=(seq_len,), mode='linear', align_corners=False) return x class Discriminator(nn.Module): def __init__(self, num_classes=4, channels=6): super().__init__() self.conv = nn.Sequential( nn.Conv1d(channels, 64, kernel_size=8, stride=4, padding=2), nn.LeakyReLU(0.2), nn.Conv1d(64, 128, kernel_size=8, stride=4, padding=2), nn.LeakyReLU(0.2), nn.Conv1d(128, 256, kernel_size=8, stride=4, padding=2), nn.LeakyReLU(0.2), ) self.global_pool = nn.AdaptiveAvgPool1d(1) self.fc_real = nn.Linear(256, 1) # 真伪得分,不接sigmoid self.fc_cls = nn.Linear(256, num_classes) # 类别概率 def forward(self, waveform, labels): feat = self.conv(waveform) feat = self.global_pool(feat).squeeze(-1) real_score = self.fc_real(feat) label_logits = self.fc_cls(feat) return real_score, label_logits这里有几个参数值得解释:生成器的转置卷积用了 stride=4 加 kernel_size=8 的组合,逐步放大时间轴;判别器的池化层选 AdaptiveAvgPool1d 而不是直接展平后接全连接,目的是让全连接层的输入维度固定下来,不随输入序列长度的变化而改变。如果以后想改采样率,把 seq_len 换了,模型除了输入尺寸检查那行插值兜底之外不用动结构。
3.3 损失函数组合:WGAN-GP 加辅助分类损失的细节
AC-GAN 的标准损失包含三个部分:判别器的真伪损失、判别器的分类损失、生成器的分类损失。如果用的是普通 GAN 的判别器,真伪损失是二分类交叉熵,但如果用 WGAN-GP 框架,真伪部分换成 Wasserstein 损失即真实的得分均值减去生成的得分均值,外加梯度惩罚项。梯度惩罚是 WGAN-GP 稳定训练的关键。这个惩罚项在每次训练迭代里对真实样本和生成样本之间的插值点计算判别器输出的梯度范数,强制它接近 1,这样才能保证 Wasserstein 距离的 Lipschitz 约束成立。
def compute_gp(discriminator, real_wave, fake_wave, labels, device): """WGAN-GP 梯度惩罚项""" batch_size = real_wave.size(0) alpha = torch.rand(batch_size, 1, 1, device=device) # 在真实和生成样本之间插值 interpolated = alpha * real_wave + (1 - alpha) * fake_wave interpolated = interpolated.requires_grad_(True) real_score, _ = discriminator(interpolated, labels) grads = torch.autograd.grad( outputs=real_score, inputs=interpolated, grad_outputs=torch.ones_like(real_score), create_graph=True, retain_graph=True )[0] grads = grads.view(batch_size, -1) gradient_penalty = ((grads.norm(2, dim=1) - 1) ** 2).mean() return gradient_penalty这里插值点的计算是一个重点。alpha 是在 [0,1] 均匀采样得到的随机系数,逐元素作用到真实样本和生成样本的每一个点上。梯度惩罚的强度系数 lambda 一般取 10,这是 WGAN-GP 原论文给的推荐值。我在电力波形数据上调过几次,从 1 到 100 都试过,10 确实是最稳的,太小了判别器梯度容易爆炸,太大了生成器梯度消失。
分类损失用标准交叉熵。判别器的分类损失只需要对真实样本计算,生成器的分类损失是对生成样本的类别预测算的。这个分类损失会反向传播到生成器,强迫生成器生成的波形不仅像数据,而且能被识别为指定的故障类别。这个机制是 AC-GAN 能生成指定类别人样本的关键,应对不了会发生类别纠缠的现象——生成器把全部类别糅合到同一种波形形态上。
3.4 训练循环:判别器多走一步,生成器才能跟上
GAN 的训练节奏在电力波形上有一个经验值——每轮迭代判别器更新 5 次生成器更新 1 次,对 WGAN-GP 来说是标准的推荐。判别器训得足够强,它给出的梯度信号对生成器才能构成有效的压力,强制生成器不断提升。
学习率这块,判别器和生成器都设成 1e-4 甚至 2e-4,使用 Adam 优化器,beta1 取 0.5 而不是默认的 0.9。这个细节对 GAN 很重要。beta1=0.9 时 Adam 会累计过去很多步的梯度平均动量,在 GAN 这种动态博弈场景下,生成器的梯度方向变化很快,动量累积太多反而容易震荡甚至发散。beta1=0.5 是 GAN 社区调出来比较安全的值。
# 每轮训练的核心循环(伪代码风格) for epoch in range(epochs): for batch_idx, (real_wave, labels) in enumerate(train_loader): batch_size = real_wave.size(0) device = real_wave.device # 1. 训练判别器:5次 for _ in range(5): noise = torch.randn(batch_size, noise_dim, device=device) fake_wave = generator(noise, labels) real_score, real_cls = discriminator(real_wave, labels) fake_score, _ = discriminator(fake_wave.detach(), labels) gp = compute_gp(discriminator, real_wave, fake_wave, labels, device) d_loss = -(real_score.mean() - fake_score.mean()) + 10.0 * gp + cls_loss(real_cls, labels) d_optimizer.zero_grad() d_loss.backward() d_optimizer.step() # 2. 训练生成器:1次 noise = torch.randn(batch_size, noise_dim, device=device) fake_wave = generator(noise, labels) fake_score, fake_cls = discriminator(fake_wave, labels) g_loss = -fake_score.mean() + cls_loss(fake_cls, labels) + 0.1 * feature_matching_loss g_optimizer.zero_grad() g_loss.backward() g_optimizer.step()在这个循环里要额外关注的是fake_wave.detach()。判别器训练时对生成样本反传梯度会同时更新判别器的参数,可是生成器这轮根本不参与更新,如果不 detach,计算图里会多保留一条生成器的梯度路径,内存消耗大且可能产生梯度震荡的干扰。这一步属于很常见的踩坑点,少了个 detach,训练到一半 loss 就出现奇怪跳动。
4. 训练与调参:特征匹配损失为什么能救回模式崩溃
4.1 三个最容易出现的训练异常及现象特征
训练 GAN 做电力波形生成,最常见的三个异常分别是:判别器 loss 归零、生成器 loss 反复横跳、生成的样本陷入只有一到两种形态的模式崩溃。
判别器 loss 归零的特征是 d_loss 降到负几甚至负十几,然后整个训练就僵住,生成器怎么更新都没反应。原因是判别器太强了,把真伪分差拉得太大,给生成器的梯度直接就消失了。WGAN-GP 里如果梯度惩罚项没生效,Lipschitz 约束被破坏,就会出现这个状态。排查方式是把梯度惩罚项单独打出来看看数值,正常应该在 0 到 5 之间浮动的数值,如果变成 0 或者上百,惩罚项基本废了。
生成器 loss 反复横跳的特征是 g_loss 在前 100 轮持续下降,到 200 轮左右突然跳高,又掉下来,循环往复。这通常不是网络结构的问题,而是学习率太高了——把学习率从 2e-4 降到 1e-4 能解决大部分问题。还有一个隐藏原因是 batch size 太小,只有 8 或者 16 的时候,判别器的真伪得分方差太大,梯度方向不稳定。我一般至少用 32。
模式崩溃是 GAN 家族最出名的问题,生成出来的波形全部是同一个形态的变体。在电力故障数据场景下尤其危险——如果这个形态恰好是某种故障类型,其他类别的样本就全废了,本来是想补少数类,结果生成的样本反而加偏了。缓解手段很多,特征匹配损失是我试过效果最直观的一个。
4.2 特征匹配损失是怎么救场的
特征匹配损失的核心思路是:不让生成器直接去骗判别器的最终输出,而是让生成器去匹配判别器中间层的特征统计量。判别器学到了真实故障波形的特征表达,生成器只要生成的波形经过判别器中间层得到的特征和真实样本的中间特征接近,波形本身就会越来越接近真实分布。
具体实现分两步:先把判别器的中间特征拿下来,对真实样本和生成样本分别计算特征向量,然后比较两者的 L2 距离,把这个距离作为附加损失加到生成器的总损失上。权重不需要很大,0.1 到 1 之间就行,太大了会让生成器过于保守,学不到多样化的分布。
class FeatureMatchingLoss(nn.Module): def __init__(self, weight=0.1): super().__init__() self.weight = weight def forward(self, real_feat, fake_feat): # 在batch维度上取均值,然后算L2距离 real_mean = real_feat.mean(dim=0) fake_mean = fake_feat.mean(dim=0) loss = torch.mean((real_mean - fake_mean) ** 2) return self.weight * loss # 在判别器forward里把中间特征也返回出来 def forward_with_feature(self, waveform, labels): feat = self.conv(waveform) # 中间卷积特征 x = self.global_pool(feat).squeeze(-1) real_score = self.fc_real(x) label_logits = self.fc_cls(x) return real_score, label_logits, x接入方式是在训练循环里先对真实样本和生成样本都过一次判别器拿到中间特征,在生成器的总损失里把这一项加进去。加了特征匹配之后,特有的一个变化是训练初期生成器收敛变快,局部细节的电平起伏、暂态振荡的幅值变化都会先稳定下来,后续再做细微调整。
4.3 训练时长和保存策略:多久才算训练好了
训练时长和样本量直接相关。假设你有 400 条真实故障样本,每条长度 1600 个采样点,batch size 32,在单张消费级 GPU 上大概训练 500 到 800 个 epoch 就能收敛,差不多几十分钟的量级。如果你用的是 CPU 训练,时间翻 10 倍都不止,我建议至少弄一张普通 GPU。
这里给一个判断训练是否收敛的实用标准:固定一组随机噪声和标签,每隔固定迭代轮数用生成器生成一批样本保存下来,画成波形图人工检查。单看 loss 曲线不可靠——GAN 的 loss 不像普通监督学习那样单调下降,有时整体稳定但波形形态不对,有时 loss 在跳但生成效果反而在改善。保存生成的样本波形、肉眼观察暂态过程是否合理,比盯 loss 数字靠谱得多。
生成器训练完成后要保存完整的状态字典,同时把归一化参数一起存下来。后续做数据增强时,这两个东西是配合使用的,换一台机器跑推理时缺了任何一个都没法正确恢复出真实幅值。
torch.save({ 'generator_state': generator.state_dict(), 'normalize_params': scale_params, 'seq_len': seq_len, 'num_classes': num_classes, 'noise_dim': noise_dim }, 'fault_generator.pt')5. 验证与合成样本质量评估:一张混淆矩阵就把问题全暴露了
5.1 合成样本和真实样本的分布对比用什么指标看
评估生成模型的质量有两个层面:一是合成样本本身像不像真实的故障波形,二是用合成样本增强训练数据之后,下游的故障分类准确率有没有真正提升。第一个层面的评估常用 FID 之类的指标,但电力波形是一维信号,FID 算出来的分数未必和人对波形的感知一致,我用得比较多的反而是三种更直接的方法。
第一是幅值分布对比。把真实样本和合成样本每个通道的幅值分布画出来,叠加在同一张图上,有两个分布的核密度曲线明显错位就要警惕,说明生成器没有学准幅值区间。第二是频谱对比。故障波形尤其是暂态阶段有丰富的谐波和衰减直流分量,把真实样本和合成样本做 FFT,对比主要频率成分的比例,如果合成样本高频成分严重缺失,说明生成器倾向于学波形的主体而丢掉了细节。第三是时域波形直接目检,把故障时刻前后的波形画出来看暂态过程是否平滑、有无毛刺或不连续跳变。
合成样本质量不一定追求和真实样本一模一样,我们的最终目的是下游分类任务。如果分类准确率提升了,那即使合成样本在分布上和真实样本略有偏差,这个偏差也是可接受的。
5.2 用混合数据跑一轮分类实验:最直接有效的方法
最实用的验证方法是在同一个分类器上做对比实验:第一组只用真实故障样本训练,第二组用真实样本加合成样本训练,第三组只用合成样本训练。分类器用同一套结构和超参,数据集划分方式保持一致。第三组的意义在于兜底判断——如果只用合成样本训练的准确率明显低于真实样本,说明生成器学的分布还不够准,后续需要继续调;如果第二组的准确率相比第一组有提升,说明数据增强产生了正收益。
分类器选择上,不用上来就用特别复杂的模型。一个 1D ResNet 或者简单的时序卷积网络就够用来做验证了,分类任务本身只需要区分四类故障,准确率差异主要是数据驱动而不是模型复杂度驱动的。
# 混合训练的loader构造:真实样本和合成样本按比例混合 def build_mixed_loader(real_dataset, fake_dataset, batch_size=32, ratio=0.5): """按ratio比例混合真实和合成样本""" fake_samples = int(len(real_dataset) * ratio / (1 - ratio)) fake_dataset = torch.utils.data.Subset(fake_dataset, indices=range(min(fake_samples, len(fake_dataset)))) mixed = torch.utils.data.ConcatDataset([real_dataset, fake_dataset]) loader = torch.utils.data.DataLoader( mixed, batch_size=batch_size, shuffle=True, drop_last=True, pin_memory=True ) return loader混合比例也是一个有讲究的参数。合成样本占比太高,比如一半以上,分类器会过度学习合成样本的分布特征,在真实样本上的表现反而下降;占比太低又没有明显的增强效果。我的经验值是合成样本占总样本量的 30% 到 40% 左右比较合适。当然这个比例和真实样本的绝对数量有关,如果真实样本只有 50 条,合成 200 条能显著改善;如果真实样本有 2000 条,合成 500 条提升就没那么明显了。
5.3 判定合成样本价值的最终标准:少数类别召回率的变化
混合训练的分类实验还有一个重点要看的是类别级别的召回率变化,而不仅仅是总体准确率。电力系统故障数据的不均衡往往很严重,单相接地故障占比可能超过 80%,三相短路可能不到 5%。如果总体准确率从 95% 提到 96%,但三相短路的召回率不升反降,那这次增强对生产环境其实是负面的——三相短路才是最需要准确识别的故障类型。
把混淆矩阵打印出来,逐行看每个类别的召回率。合成样本的价值在少数类别上体现得最明显:真实样本不够,分类器学不好少数类别的判别边界,合成样本补上去之后,少数类别的召回率应该从 60% 左右提到 85% 以上,而多数类别的召回率不应该明显下降。这个验证结果直接决定你要不要在生产环境里部署生成增强流程。
6. 避坑清单与进阶玩法:训练数据生成落地后还能怎么用
6.1 五个工程师最容易踩的坑
坑一:生成的样本标签和实际波形不匹配。现象是波形看起来是一个单相接地,但标签却标着三相短路。原因在于 AC-GAN 在训练初期类别约束尚未充分生效,生成器输出的波形类别特征与条件嵌入不一致。解决办法是在推理阶段加一个额外的判别器做二分类验证,或者用训练好的分类器先对生成样本做一轮预筛选,把置信度低于阈值的样本丢掉。
坑二:归一化和反归一化的参数不配对。现象是训练好的生成器生成的波形,画出来电压幅值全在 -1 到 1 的区间内,完全不符合实际线路的电压等级。原因是推理时只加载了生成器模型权重,没有加载归一化参数,或者加载时通道顺序没对齐。解决方法是把归一化参数和模型权重放在同一个 checkpoint 文件里,并且给通道顺序建立一个明确的映射表。这个坑我踩过两次,每次都是因为换了一台机器跑推理,忘了同步参数文件。
坑三:合成样本大量重复,多样性不足。现象是生成的 100 条样本里有一半以上波形形状高度相似,只是做了微小的时间平移。原因是 mode collapse,生成器只学到了分布中的一个局部模式。解决方式是加入特征匹配损失,同时降低判别器的学习率,让判别器不要学得太快太强。另外可以试一下把 Dropout 加进生成器的全连接层,给生成过程加一些随机性。
坑四:训练时 loss 数值低但生成波形出现大量尖峰毛刺。现象是波形整体形态正确,但局部有周期性的尖锐脉冲。原因是转置卷积产生了棋盘效应——卷积核大小和步长不匹配时会出现这种伪影。解决方式是检查转置卷积层的 kernel_size 和 stride 的整除关系,我常用的 kernel_size=8、stride=4 组合没有这个问题,但如果改成 kernel_size=6、stride=4 就会出问题。另一种办法是在判别器端加一个频谱损失,直接对生成波形的 FFT 结果和真实波形的 FFT 结果做 L2 约束。
坑五:生成样本的故障起始时间点在样本内的位置漂移。现象是有的样本故障发生在中间位置,有的发生在开头或结尾,分类器训练时特征对齐就困难了。原因是我们前面做过切片预处理,但生成器没有学会这个对齐约束。解决办法是在切片时就做一次检测:用简单的幅值突变量检测找到故障时刻的样本点位置,自动对齐到样本中心,对齐之后再喂给生成器训练。
6.2 进阶玩法:用生成模型做数据增强之外的事
合成数据不仅可以用来加厚训练集,还可以用来做故障分类模型的鲁棒性测试。实际场景中录波装置可能受噪声影响、采样率波动、通道缺失等问题干扰,真实故障样本很难覆盖这些异常情况,但生成模型可以通过在噪声 z 上叠加扰动或者在生成波形上添加噪声模拟出这些退化场景,然后把退化后的样本拿去测试分类器的鲁棒性。这个用法比单纯做数据增强更进一步——它让你在模型上线之前就知道哪些退化场景下分类器会翻车。
另一个方向是跨区域迁移。不同变电站、不同线路长度的故障波形存在分布差异,但真实录波数据往往只覆盖某一个区域。可以用一个区域的数据训练生成模型,然后调整局部参数——比如线路阻抗相关的衰减系数、噪声基底——来生成类似其他地区特征的样本,做预训练模型的泛化验证。这比我一开始做数据增强的时候预期更有价值。
6.3 把我自己的流程固化下来
现在我在项目里固定下来的一套流程是:录波数据切片归一化 → AC-GAN+WGAN-GP 训练 → 每 100 个 epoch 抽样一波波形人工检查 → 训练完成后用分类器混合训练验证召回率 → 达标后把生成器、归一化参数、通道映射表打包归档。这套流程跑熟之后,换一个新的线路数据只需要几分钟的调整时间——改一下标签列表,重新生成一个 checkpoint。当然也有翻车的时候,有一次我把类别数配错了,导致生成的样本类别标签整体偏移,那批数据差点进了生产环境,后来我习惯在归档前跑一次快速验证脚本,用生成样本过一遍分类器确认标签和波形类别一致。这个习惯帮我挡掉了好几批不能用的数据。希望这些经验对你也有帮助。
本文还有配套的精品资源,点击获取