首页 / 资讯中心 / 文章详情

GAN训练实战:从原理到PyTorch实现,避开三大经典坑

GAN训练实战:从原理到PyTorch实现,避开三大经典坑 ★ FEATURED ARTICLE
我第一次跑通GAN是在一个编号为G1的练习项目上。训练过程相当魔幻损失曲线一路降到近乎为零我心里想着这波稳了结果生成的图像全是灰蒙蒙的噪声后来我把判别器调整到稍微收敛一些图像又迅速塌缩成同一张模糊脸——典型的模式崩塌。反复折腾了整整三周我才真正摸清GAN这套训练框架的脾气。其实GAN的核心概念并不复杂一个生成器、一个判别器两者在对抗中共同进化。但概念简单和训练稳定之间隔着的是无数个细节坑。这篇文章我打算完整讲一遍GAN——它到底在优化什么、为什么这样优化、一个能复现的PyTorch实现长什么样、以及训练过程中最常见的三类故障该怎么排查。适合刚接触生成模型的同学也适合那些跑通demo但换个数据集就崩的实践者。1. GAN的底层直觉造假者与鉴定师如何在博弈中共振1.1 一个故事模型为什么对抗能带来真实把GAN想象成一支假画团队和一个鉴定专家。造假者生成器不断研究真迹、试图制造出以假乱真的画作鉴定专家判别器则不断钻研新的鉴定技术试图找出每一幅画中的破绽。表面看两人目标完全对立但结果却是互相逼迫造假者为了骗过鉴定专家必须把画的笔触、色彩、结构都做到极致鉴定专家为了不被骗也不得不把鉴别能力提到更高水平。GAN把这个过程形式化了。生成器G接收一个随机噪声向量z输出一个看起来像真实样本的伪造样本G(z)判别器D则是一个二分类器输入一个样本输出它来自真实数据而非生成数据的概率。真实样本记为x生成样本记为G(z)。D的目标是区分这两类G的目标则是让D分不出来。两个网络目标函数完全相反于是构成了一个max-min博弈。这种对抗设计有一个朴素但深刻的原因真实数据的分布P_data往往是高维空间里一个极端复杂的流形我们没有办法直接写出它的概率密度但可以通过采样拿到足够多的真实样本。GAN的思路就是让G去隐式逼近这个分布而不是像传统生成模型那样显式地计算似然。逼近的衡量标准不是某个固定的距离公式而是判别器是否还能分辨真假。1.2 为什么不能只用像素级损失监督生成器很多人第一次接触生成任务时会想直接让生成图像和真实图像做L2距离不就行了我最初也这么干过。但实际训练一段时间后你会发现L2或MSE损失下生成的图像永远是模糊的、发灰的。原因是欧氏距离对一个多模态分布极其不友好当真实数据的图像有多种合理的输出方式时最小化L2距离的最优解是拟合这些模式的平均值于是一张脸的五官位置、色调全被平均成了一团模糊。判别器恰好解决了这个问题。它不关心像素是否一一对应而是关心这张图整体看起来像不像真实世界的图。换句话说判别器提供了一种接近于语义层面的监督信号。它逼着生成器去学习数据分布的结构而不是逐像素地死磕。这也是为什么GAN在图像生成上能够给出比简单回归清晰得多的结果对抗压力逼迫生成器在结构上向真实分布靠拢。1.3 收敛在哪儿这不是打到一方躺平而是一个动态平衡理想情况下当生成分布P_g完全等于真实分布P_data时判别器无论如何也分不清真假正确率恒为50%生成器也不会再改进。这时整个模型到达了博弈论里的纳什均衡点。但实际训练中参数空间的博弈很少真正精确到达这个均衡点更多时候是动态平衡损失在一个区间内持续震荡但生成质量却在逐步提升。这一点非常影响训练心态。我见过不少朋友把GAN的loss当做分类任务的loss来解读看到loss上升就以为模型坏了然后疯狂调学习率结果越调越崩。你把对抗训练当成一个两人拔河的动态过程就好理解多了一方突然变强另一方就会在下一轮处于下风曲线来回摆动是常态。真正需要关注的不是某个时刻的loss数值而是生成样本的肉眼质量是否在变好。2. 目标函数与训练流程min-max这道优化题到底怎么解2.1 从二分类交叉熵出发推导原版损失GAN的原始损失可以从二分类交叉熵直接推导出来。判别器D要做的事情就是把真实样本x和生成样本G(z)区分开。如果给真实样本标记y1生成样本标记y0那么判别器在单个样本上的二分类交叉熵损失就是L_D -E_{x~P_data}[log D(x)] - E_{z~P_z}[log(1 - D(G(z)))]D要最小化这个损失等价于让D(x)尽量接近1、让D(G(z))尽量接近0。生成器的目标正好相反它希望D(G(z))尽量接近1也就是让判别器把假样本认成真样本。所以生成器要最小化L_G E_{z~P_z}[log(1 - D(G(z)))]把D的目标和G的目标合并成一个统一的表达式就得到了原论文里那个经典公式min_G max_D V(D,G) E_{x~P_data}[log D(x)] E_{z~P_z}[log(1 - D(G(z)))]注意这里的字面顺序D要最大化VG要最小化V。理解这一点后面的代码才不会写反。2.2 训练节奏为什么通常先更判别器、再更生成器在实现上GAN通常采用交替训练而不是联合训练每个批次中先更新判别器再更新生成器。这个顺序是有讲究的。如果把判别器训练得过强它会以极高的置信度拒绝所有生成样本此时生成器拿到的梯度趋近于零完全学不到东西如果判别器太弱它连真实样本都分不清生成器感受不到足够的压力生成的图像也会缺乏真实感。所以两者的水平需要大体匹配像两个水平相当的对手切磋。一个常见问题是D和G的更新次数比例。toy级别任务里1:1通常就行真实图像任务中我试过1:3、甚至1:5也就是判别器更新多个batch后再更新一次生成器目的是让判别器不要掉队太快。但反过来如果发现D_fake长期都在0.01以下就要把D的更新次数降下来回到1:1让生成器有喘息空间。这个比例是调节训练平衡的第二个旋钮——第一个旋钮是学习率。2.3 生成器损失的非饱和改进原版生成器损失minimize log(1 - D(G(z)))有一个很隐蔽的坑。当判别器远强于生成器时D(G(z))会非常接近0此时log(1 - D(G(z)))也接近0但它的导数却很小。这意味着生成器在最需要学习的时候反而学不到东西梯度消失。这种情况在训练初期极为常见。所以我在实际代码里几乎不用原版生成器损失而是用非饱和版本把生成器的目标改为最大化log(D(G(z)))也就是最小化-log(D(G(z)))。这个改动看似微小训练稳定性却有质的提升。原因是当判别器拒绝生成样本时-log(D(G(z)))仍然能给生成器一个足够大的梯度把它往更像真实样本的方向推。这是GAN训练最实用的一个trick没有之一。补充一个数学上的解释最大化log(D(G(z)))等价于最小化P_data和P_g之间的JS散度在某种条件下的近似但它的梯度更友好。具体推导不展开了记住结论生产环境代码里生成器的目标函数请优先选择非饱和版本。3. 动手实现一个稳定可复现的GAN训练管线PyTorch3.1 网络结构选型从MLP到DCGAN动手写代码之前先说网络结构怎么选。MNIST、Fashion-MNIST这类小尺寸灰度图上全连接网络就足够CIFAR-10或更大的人脸、街景图像就要用卷积结构的DCGAN了。以下是两条实践经验第一全连接结构写起来简单维度转换直白适合把训练逻辑跑通。它的问题是参数量不小、收敛慢但用来学习和调试GAN的训练节奏非常合适。第二卷积结构DCGAN能利用图像的空间结构生成质量明显更好但需要处理好BatchNorm、卷积核尺寸和反卷积配置初学者容易在维度上出错。这个输出层激活函数必须用Tanh。原因在于像素值经过归一化后范围是[-1,1]Tanh的输出范围正好匹配如果用Sigmoid输出[0,1]图像会整体偏色且对比度失衡。这个细节我第一次写的时候就踩过生成出来的图永远是发灰的。下面是一个简单的全连接生成器实现输出28x28的灰度图import torch.nn as nn class Generator(nn.Module): def __init__(self, z_dim100, img_dim28*28): super().__init__() self.net nn.Sequential( nn.Linear(z_dim, 256), nn.BatchNorm1d(256), nn.LeakyReLU(0.2, inplaceTrue), nn.Linear(256, 512), nn.BatchNorm1d(512), nn.LeakyReLU(0.2, inplaceTrue), nn.Linear(512, img_dim), nn.Tanh() ) def forward(self, z): return self.net(z)判别器结构相对简单输入是一张展平后的图像输出一个logitclass Discriminator(nn.Module): def __init__(self, img_dim28*28): super().__init__() self.net nn.Sequential( nn.Linear(img_dim, 512), nn.LeakyReLU(0.2, inplaceTrue), nn.Dropout(0.3), nn.Linear(512, 256), nn.LeakyReLU(0.2, inplaceTrue), nn.Dropout(0.3), nn.Linear(256, 1) ) def forward(self, x): return self.net(x)注意判别器里我没有加BatchNorm。这是一个值得记住的经验BatchNorm在生成器里几乎是必需品它能防止梯度模式坍缩但在判别器里要谨慎因为它会引入batch内样本之间的统计耦合导致判别器对单样本的判别能力被削弱。判别器里我用Dropout来增加随机性这反而能提高鲁棒性。3.2 训练循环的核心逻辑网络定义好之后训练循环是GAN项目的灵魂。以下是核心逻辑import torch from torch import nn, optim z_dim 100 batch_size 128 lr 0.0002 G Generator(z_dim) D Discriminator() g_opt optim.Adam(G.parameters(), lrlr, betas(0.5, 0.999)) d_opt optim.Adam(D.parameters(), lrlr, betas(0.5, 0.999)) criterion nn.BCEWithLogitsLoss() for epoch in range(epochs): for real_batch, _ in dataloader: current_batch real_batch.size(0) real_batch real_batch.view(current_batch, -1) # 1. 更新判别器 z torch.randn(current_batch, z_dim) fake_batch G(z) d_loss_real criterion(D(real_batch), torch.ones_like(real_batch[:, :1])) d_loss_fake criterion(D(fake_batch.detach()), torch.zeros_like(real_batch[:, :1])) d_loss d_loss_real d_loss_fake d_opt.zero_grad() d_loss.backward() d_opt.step() # 2. 更新生成器 z torch.randn(current_batch, z_dim) fake_batch G(z) g_loss criterion(D(fake_batch), torch.ones_like(real_batch[:, :1])) g_opt.zero_grad() g_loss.backward() g_opt.step()有几个细节很多人第一次写都会搞混。更新判别器时fake_batch必须调用detach()把梯度截断否则梯度会通过判别器反向传播到生成器一并更新了G的参数。更新生成器时生成样本的标签故意设为1也就是我希望你判别器认为这些假图是真的。这里的标签设置是让generator骗过discriminator的关键千万别写成0。我一开始会把batch里的图像用view展平因为全连接网络输入是一维向量。如果你换成DCGAN这里就要保持[batch, channel, height, width]的结构并使用Conv2d和ConvTranspose2d。训练循环的骨架是一样的只是数据形状不同。3.3 关键超参数与初始化细节GAN对超参数非常敏感下面这张表是我跑了无数组实验后沉淀下来的推荐配置参数推荐值说明潜在维度z100太小则表达空间不足太大则稀疏噪声难以训练优化器Adam自适应学习率适合非凸博弈比SGD稳定得多学习率0.0002GAN常用的经验值过高会造成震荡过低收敛慢beta10.5比默认的0.9小很多是为了减少动量累积带来的训练震荡batch size64~128过小则判别器梯度噪声大过大会把内存吃满生成器中间层BatchNorm LeakyReLUBN稳定训练LeakyReLU避免负半轴梯度死掉输出激活函数Tanh匹配[-1,1]像素范围图像颜色分布更自然标签平滑真实标签用0.9防止判别器过度自信梯度消失概率大幅下降我单独说下beta1为什么设成0.5。Adam默认的beta10.9会用很大的历史梯度动量在非凸的对抗博弈中容易让参数冲过头导致loss出现周期性暴涨。把beta1降到0.5后历史动量的影响变小生成器和判别器的相对变化会更温和。这个改动对早期训练收敛速度的影响几乎是立竿见影的。4. 训练中的三个经典坑现象、根因与完整排查链路4.1 模式崩塌当生成器学会了偷懒模式崩塌是GAN最臭名昭著的问题。现象非常直观无论你给生成器输入什么随机噪声它都只会输出同一类图像有时甚至是一模一样的图像。我遇到最典型的一次是在人脸数据集上生成器学会了只生成某一固定角度的正脸其他噪声全部被映射到了同一个点上。根因是生成器发现了一个偷懒策略与其覆盖整个数据分布不如专门模仿当前判别器最容易受骗的那一小块区域。每过几个训练步骤判别器识破了这个模式生成器又换到另一个模式循环往复。表现在训练曲线上就是loss短期起伏很大但生成图像长期没有多样性。完整的排查链路可以这样走先确认问题。取一个固定的z集合比如8个固定种子的噪声向量每训练几个epoch就把生成图像保存下来。如果不同z对应的生成图像逐渐趋同基本可以确定是模式崩塌。统计生成样本在特征空间的多样性。做法是把一批生成样本喂给一个预训练好的特征提取器计算它们之间特征向量的平均距离。平均距离骤降说明多样性在退化。尝试缓解手段。最传统的是mini-batch discrimination给判别器额外传入当前batch内样本的相似度统计让判别器有依据地惩罚同一batch里所有生成样本长得太像的行为。我自己更常用的办法是特征匹配让生成器不仅骗过判别器还要让它的中间层特征分布与真实样本的中间层特征分布尽量接近。这样一来生成器被迫去拟合更全面的特征而不是盯着一块打。如果上述手段都救不回来就直接切WGAN-GP。它的梯度惩罚机制天生对模式崩塌有更强的抑制作用。4.2 判别器过强梯度消失往往源于一边倒第二个经典坑是判别器太强。现象是训练时D(G(z))长期在0.01以下D(x)长期在0.99附近生成器的损失看起来小得可怜但生成的图像就是没有任何改善。根因是判别器对真假样本的区分已经过于自信给生成器回传的梯度几乎为零。这就像鉴定专家水平太高造假者在他眼中完全不堪一击反而失去了学习的方向。特别是在原始损失的log(1-D(G(z)))下判别器越强生成器梯度越趋于消失。我的排查顺序如下打印D(x)、D(G(z))的平均值。如果D_fake长期低于0.1优先怀疑判别器过强。标签平滑。把真实标签从1改成0.9让判别器不要过度自信。这是工作量最小、见效最快的手段。给判别器加Dropout或者减小其网络容量削弱它的判别能力。技巧是先从Dropout试起因为不动网络结构改一行就能验证。降低判别器学习率、或减少判别器更新次数。比如从D每batch更新一次改成每3个batch只更新一次给生成器一点追赶的时间。反过来增强生成器容量。判别器太强也可能是生成器太弱这种情况下给生成器多加一层、加宽隐藏维度往往能重新建立平衡。顺带一提判别器过强和过度收敛并不一定是坏事。在训练初期一个略显强大的判别器能快速把真实分布的信息传给生成器真正危险的是D_fake永远起不来。判断标准依然是生成图像是否在变好而不仅仅是看那几个概率数字。4.3 损失震荡却没进展先怀疑数据再怀疑评估方式第三种情况最隐蔽训练曲线震荡剧烈生成图像也一直模糊但你又说不清哪里出了问题。我早期在这里浪费过很多时间后来总结出一条很重要的原则GAN的loss数值不该被当作分类任务里的ACC来解读。两个博弈方在互相追赶就算生成质量在提升loss也可能不降反升。所以看到loss震荡先不要慌先做下面几件事固定随机种子确保每次实验可复现。如果连复现都做不到后面的排查毫无意义。检查数据归一化。GAN生成器输出Tanh对应像素范围[-1,1]如果你把数据归一化到[0,1]两者不匹配生成图像永远会发灰。这个问题很基础但发生率极高。检查判别器和生成器的更新比例。前面提到过1:3或1:5是常用的判别器优先配置但训练后期要及时改回1:1。切换评估方式。loss不靠谱就用FID、Inception Score或者直接看固定z集合的生成图像。我的经验是训练GAN一定要定期保存生成图像肉眼判断永远是第一位的。检查BatchNorm的使用位置。生成器在某一个epoch之后突然输出全零或者全黑通常和BN统计量还在累积有关。训练完生成样本时务必确保G处于eval模式。这里分享一个我一直在用的训练日志模板。我会在每轮epoch结束时打印D_real_mean、D_fake_mean、G_loss并把它们画在同一张图上。稳定训练的规律是D_real稳定在0.8附近D_fake在逐步爬升到0.4~0.6的区间同时生成图像肉眼逐步清晰。如果D_fake一直趴在地上后面的事就不用在优化器层面折腾了直接按4.2的方法去削弱判别器。5. 从玩具到应用GAN在数据增强、超分与异常检测中的落地思路5.1 小样本数据增强最直接的落地方向GAN最有实用价值的应用之一就是小样本场景的数据增强。当某个类别只有几百张图训练出来的分类器明显过拟合时可以用GAN生成一批额外的合成样本来做类别平衡。但这里有一个我特别想强调的坑生成样本不能无脑混入训练集。GAN生成的数据天然带有模型自身的误差如果这批合成样本恰好集中在某个错误模式上会把分类器往错误方向带。比较稳妥的做法是分两步先用FID或肉眼筛掉明显不合格的样本再把合格的合成样本按一定比例比如不超过原始数量的20%掺入训练集然后用验证集做AB测试。领域内的实践反馈是合成样本原始数据混合训练通常比纯原始数据训练提升1到3个点但收益取决于任务难度和数据量不是所有场景都有正向效果。5.2 图像超分辨率与风格迁移图像超分辨率和风格迁移是GAN在视觉领域最出名的两大应用。超分辨率赛道里的核心思想是生成器负责把低分辨率图映射到高分辨率图判别器负责判断这张高分辨率图看起来像不像一张真实的高分图。单纯用L1损失做超分结果总是不够锐利因为L1倾向于平均化细节加上对抗损失之后生成器被逼迫去恢复更真实、更锐利的纹理细节。风格迁移也是同样的逻辑。pix2pix这类条件GAN把输入图像作为条件喂给生成器同时让判别器判断输出是否与输入匹配也就是不仅要求图像像真的还要求它对应正确的输入内容。CycleGAN更进一步没有成对训练数据时用循环一致性约束——把A图变成B图再变回A图要尽量还原用这个自洽约束来替代配对的监督信号。这类结构在真实项目中落地得非常多主要价值是能把语义级风格变换做得非常自然。5.3 异常检测GAN在工业视觉异常检测里的思路很巧妙。假设生产线上只有正常样本的图片我们先用这批全是正常的数据训练一个GAN。训练完成后生成器学到了正常数据的分布。测试时把一个新的未知样本输入生成器固定生成器参数去优化潜变量z让G(z)尽量重建出这个样本然后计算重建误差。如果样本本身是正常的重建会比较容易误差小如果样本自带缺陷或异常重建的结果就会露馅误差明显偏大。超过设定阈值即可报异常。这就是AnoGAN一脉的方法它的核心优势是不需要任何异常样本来训练对罕见缺陷的任务特别合适。使用时有三个小细节值得注意。第一优化z本身需要几十上百次迭代推理速度偏慢工业实时场景要考虑是否有足够算力。第二生成器的想象力比我们想象中强它可能把带有小缺陷的样本强行重建得像正常样本一样所以单纯用像素级MSE不够实践上还要把判别器中间层的特征距离加进重建误差里这样缺陷特征才不会被抹掉。第三阈值要在验证集上调千万不要用训练集的误差直接定阈值否则线上误报率会高到怀疑人生。5.4 进阶稳定训练技巧到这里如果项目已经跑通下一步就是给训练过程上稳定buff。我按性价比从高到低排一下第一谱归一化。它把判别器每一层权重矩阵的谱范数约束在1以内从而保证判别器满足Lipschitz连续条件。好处是训练大幅变稳很多不稳定的案例加上这一招之后就不怎么震荡了实现起来也就是对每个卷积层包一层代码改动不大。第二梯度惩罚替代权值裁剪。早期的WGAN用权值裁剪来实现Lipschitz约束但裁剪边界设不好很容易让网络参数退化成二值分布。改进版WGAN-GP直接在损失里加一个梯度范数偏离1的惩罚项训练稳定性比裁剪好很多缺点是计算量增加。第三自注意力机制。在生成器和判别器的中间层加入注意力模块让网络能建模远距离像素之间的依赖。对于高清大图来说这是解决全局结构崩坏的关键手段之一。一个小技巧是注意力层不要加太多两层足矣多了反而训练困难。第四混合精度训练。GAN动辄几十上百个epoch混合精度能把训练速度提升30%-50%。但要注意生成器和判别器的loss scale要分开处理否则容易出现梯度下溢。如果基础工程都不太熟可以先不做这步稳定优先。回头看整个G1项目我最深的体会是GAN是一个少见的多智能体神经网络系统它没有一成不变的全局最小值等待你收敛只有一个动态平衡需要你去维护。这也决定了它的调试方式和传统监督学习完全不同——你要盯的不是某条单调下降的曲线而是两个网络之间的相对力量对比。最后分享一个操作习惯我在每次启动训练时都会生成一个固定的z集合并规定每5个epoch自动保存一组生成图像。训练结束后打开那些历史图像你能一眼看出模型是从混沌到清晰还是从清晰又崩回混沌。这套固定种子可视化的做法比任何loss曲线都能更快地告诉你怎么调整D和G的平衡。希望这篇G1项目手记能帮你少走一点弯路也欢迎带着你的踩坑经历来找我交流。
阅读完成 · 觉得有帮助?
咨询建站