
在深度学习这个圈子里GAN网络相关的研究和讨论早就不新鲜了但每次有新人入行我还是会推荐先读一读2014年那篇《Generative Adversarial Nets》。原因很简单如今能生成高分辨率人脸、能画画、能做视频生成和语音合成的那些模型往上追源头几乎都能追到这篇论文提出的对抗博弈思想。这篇论文不算长数学推导也不复杂但它定义了一个全新的生成范式影响范围覆盖了整个生成式AI领域。这篇博文我想从论文阅读的角度把GAN网络的核心思想、目标函数、训练过程、常见问题以及它对后续研究的影响脉络完整梳理一遍适合刚接触生成模型的读者也适合想回头补一补理论基础的从业者。1. 为什么今天还要认真读这篇论文1.1 论文到底解决了什么问题在GAN网络出现之前生成模型的思路主要是两类。一类是显式概率模型比如玻尔兹曼机、深度信念网络它们试图直接建模数据的概率分布但计算配分函数partition function极其困难处理高维图像几乎不可行。另一类是变分自编码器VAE它引入了隐变量用变分推断逼近真实后验训练相对稳定但生成的图像普遍偏模糊因为VAE优化的是对数似然的下界而不是真实分布本身。GAN网络的思路和这两类都不一样。Goodfellow等人直接跳过了显式建模概率密度这一步而是用一个生成器G和一个判别器D互相博弈。G负责把随机噪声z映射成看起来像真实数据的样本D负责判断输入是真实数据还是G生成的数据。两者对抗的最终结果是G学到的分布越来越接近真实数据分布。这个思路本质上是隐式地拟合分布不需要计算任何复杂的归一化项也不需要设计变分下界生成器只要把噪声映射得足够好D就分不出来。这一点在当时非常反直觉。生成问题居然可以变成一个二分类器的对抗问题不用最大化似然不用精确计算概率全靠两个网络互相逼迫成长。1.2 生成器和判别器的角色定位理解GAN网络最好的方式就是论文里那个经典的比喻伪造者和鉴定师。生成器G是伪造者它的目标是画出以假乱真的赝品判别器D是鉴定师它的目标是准确分辨出真品和赝品。在这个博弈里D的训练目标是最大化自己判对的概率既要把真实数据判断为真也要把G生成的假数据判断为假。G的训练目标则恰恰相反它要最小化D判对的概率换句话说就是让自己的赝品尽可能骗过D。随着博弈推进G的生成能力越来越强D的鉴别能力也不得不跟着提升两者在对抗中共同进化。论文用了一个非常简洁的极小极大博弈来描述这个过程[ \min_G \max_D V(D, G) \mathbb{E}{x \sim p{\text{data}}(x)}[\log D(x)] \mathbb{E}_{z \sim p_z(z)}[\log(1 - D(G(z)))] ]这个公式看起来简单但信息量很大。D想让V尽量大因为D希望log D(x)接近0真实样本判为真、log(1-D(G(z)))接近0假样本判为假G想让V尽量小因为G希望D把假样本误判为真即D(G(z))接近1。1.3 这个思路比当时的方法先进在哪对比传统的生成模型GAN网络的优势主要体现在三个方面。第一它不需要显式定义概率密度函数。只要G能生成足够逼真的样本分布是不是有解析形式根本不重要这大大扩展了可建模的数据类型和复杂度。第二它的生成过程是直接采样不需要MCMC这类耗时的推断过程。VAE采样也要先经过编码器GAN则是从噪声z一步映射到样本生成速度天然有优势。第三它的理论保证很漂亮。论文证明在生成器和判别器都有足够容量、且每一步都优化到最优的条件下最终生成的分布一定收敛到真实数据分布。虽然这个理论前提在实践中很难满足但它给出了一个清晰的目标p_g p_data。不过理论上的优雅和实际训练中的狼狈从那天起就绑在了一起。这正是后面几代工作不断改进的起点。2. 目标函数和收敛性证明的逐层拆解2.1 目标函数里每一项都代表什么先看目标函数里的两个期望。第一项 (\mathbb{E}{x \sim p{\text{data}}(x)}[\log D(x)]) 是在真实数据分布下D对所有真实样本输出概率取对数的期望。D想让这个值越大越好因为这意味着它能把真实样本正确识别出来。第二项 (\mathbb{E}_{z \sim p_z(z)}[\log(1 - D(G(z)))]) 是在噪声分布下G先生成假样本D再对假样本输出概率取对数然后算1减去这个概率的期望。D想让这个值越大越好因为这意味着它能正确拒绝G生成的假样本。对G来说它只影响第二项。G想让 (\log(1-D(G(z)))) 尽量小也就是D(G(z))尽量接近1这等价于G生成的样本让D误认为是真的。有一个细节经常被忽略整个目标函数中G并不直接影响第一项它只通过改变G(z)的分布来间接影响第二项。所以G的优化方向本质上是让自己生成的样本在D眼中的真实概率不断上升。2.2 最优判别器是怎么推导出来的假设G固定不变我们来求D的最优解。这时候可以把目标函数改写成对x的积分形式[ V(D, G) \int_x p_{\text{data}}(x) \log D(x) p_g(x) \log(1 - D(x)) , dx ]这里p_g是生成样本的分布。对于每一个固定的x被积函数 (p_{\text{data}}(x) \log D(x) p_g(x) \log(1 - D(x))) 可以看作是关于D(x)的函数。令a p_data(x)b p_g(x)D(x) y则函数是 (a \log y b \log(1-y))。对y求导并令其为零得到[ y^* \frac{a}{ab} ]也就是[ D^*(x) \frac{p_{\text{data}}(x)}{p_{\text{data}}(x) p_g(x)} ]这个结果非常直观。如果一个x在真实分布中出现的概率很高、在生成分布中概率很低D就会给出接近1的判断如果两类概率相等D就只能给出0.5即完全无法区分。我在复现GAN的时候经常把D的输出当做一个置信度来理解但其实在理论层面D输出的是两个分布密度的比值变形。理解这一点对后面理解为什么训练会不稳定非常有帮助。2.3 收敛性证明的核心逻辑把最优判别器D*代回目标函数可以算出一个有趣的结果。[ \begin{aligned} V(D^*, G) \mathbb{E}{x \sim p{\text{data}}} \log \frac{p_{\text{data}}(x)}{p_{\text{data}}(x) p_g(x)} \mathbb{E}{x \sim p_g} \log \frac{p_g(x)}{p{\text{data}}(x) p_g(x)} \ -\log 4 2 \cdot \text{JSD}(p_{\text{data}} | p_g) \end{aligned} ]其中JSD是两个分布的Jensen-Shannon散度。JSD永远大于等于0只有p_data p_g时才等于0。所以V(D*, G)的最小值是-log4在p_g p_data时取得。等价的表述是在全局最优时D*的输出恒为1/2也就是判别器对任何输入都只能随机猜测。这完全符合直觉——生成器的分布和真实分布完全一致时再强的判别器也无法区分。论文还给出了一个保证收敛的方向如果把G的更新当作对p_g的梯度下降并且D每一步都达到最优那么p_g最终会收敛到p_data。这里的理论前提是G在函数空间中更新有足够的表达能力。2.4 理论证明与真实训练之间的落差我在最开始读这篇论文时一直觉得收敛性证明很完美但为什么实际训练时经常不收敛后来才想明白理论证明假设G在函数空间中直接更新也就是每一步都可以把生成分布往正确的方向调整但实际训练是在深度网络的参数空间中用梯度下降做更新参数空间的拓扑结构远比函数空间复杂。更麻烦的是理论要求D每一步都训练到最优。实际中如果D真的被训练到接近最优G得到的梯度往往会非常小尤其是当D能轻松区分真假样本时log(1-D(G(z)))的梯度趋近于0G几乎学不到东西。这个矛盾贯穿了GAN研究的整个历史。后面WGAN把目标函数换成Wasserstein距离核心动机就是解决这个梯度消失问题。当然这些都是后话。我在真正动手训练GAN之后才体会到了为什么论文里反复强调训练时的平衡至关重要。3. 从论文到能跑的模型训练过程与实操要点3.1 交替训练算法怎么跑论文在训练部分给出了一种交替更新策略每个迭代中先更新k次判别器D再更新1次生成器G。论文里常用的k取1也就是说每轮先训练一次D再训练一次G。为什么D要多更新几次因为在博弈的最开始G生成的样本质量很差D很容易就能区分真伪这时D的损失非常低而G的梯度会很差。先让D多训练几步可以让D保持在一个相对较强的状态反过来迫使G不断改进。不过k的取值在实际中要谨慎如果D太强G又会陷入梯度消失如果D太弱G又缺乏足够的压力。我常用的训练循环结构是# 每个训练回合 for epoch in range(num_epochs): for batch_idx, (real_data, _) in enumerate(data_loader): batch_size real_data.size(0) noise torch.randn(batch_size, latent_dim) # 更新判别器 fake_data generator(noise).detach() d_real discriminator(real_data) d_fake discriminator(fake_data) d_loss -(torch.mean(torch.log(d_real 1e-8)) torch.mean(torch.log(1 - d_fake 1e-8))) d_optimizer.zero_grad() d_loss.backward() d_optimizer.step() # 更新生成器 noise torch.randn(batch_size, latent_dim) fake_data generator(noise) d_fake discriminator(fake_data) g_loss -torch.mean(torch.log(d_fake 1e-8)) g_optimizer.zero_grad() g_loss.backward() g_optimizer.step()这里生成器的损失用的是 (-\log D(G(z)))而不是原始目标函数里的 (\log(1-D(G(z))))。这是论文在实验部分明确提到的改进技巧最大化 (\log D(G(z))) 而非最小化 (\log(1-D(G(z))))。原因在于训练的早期D很容易拒绝G生成的样本此时D(G(z))接近0而 (\log(1-D(G(z)))) 的梯度很小G几乎收不到有效的学习信号。反过来用 (-\log D(G(z)))在D(G(z))接近0时梯度非常大能给G更强的推动力。3.2 网络结构与超参数的经验之谈论文在MNIST上的实验设置放在今天看比较简单生成器使用了ReLU和sigmoid激活判别器使用了maxout激活和dropout优化器选择的是SGD。但现在的工程实践已经发生了很大变化我复现时更推荐下面的配置组件建议配置生成器G全连接或转置卷积激活函数用ReLU隐藏层和Tanh输出层判别器D全连接或卷积激活函数用LeakyReLU优化器Adam学习率0.0002至0.0004beta1取0.5批量大小64或128噪声维度通常取100或128归一化生成器使用BatchNorm判别器视情况使用为什么激活函数这么选生成器输出层用Tanh是因为Tanh的取值区间是[-1,1]与图像像素归一化到[-1,1]后的范围一致能避免生成图像整体偏某个亮度。判别器用LeakyReLU是因为普通ReLU在负数区域的梯度恒为0判别器一旦陷入负区间就可能死掉LeakyReLU保留了负半轴的梯度让判别器更不容易停止学习。还需要注意一个容易被忽略的点生成器的输入噪声z不要使用全零或者固定常数。在训练初期如果噪声集中在很小的范围内G很容易退化成只会输出固定类型的样本。通常建议从标准正态分布中采样。3.3 训练过程中肉眼可见的规律我第一次完整跑通GAN训练时最直观的感受是损失曲线的波动非常大。D的loss会不断上下震荡G的loss也不会单调下降这是正常现象。因为两者是博弈关系一方变强必然导致另一方的loss上升。从图像生成效果上看通常会经历三个阶段。最开始G输出的完全是噪声D几乎可以100%区分真假。然后随着训练推进G开始能生成一些有结构的模糊图案这时D的loss会从几乎为0上升到0.5附近。最后如果训练顺利G生成的图像会逐渐清晰D的loss会长期稳定在0.5附近——这是判别器已经无法区分真假的信号。需要特别提醒的一点是不要因为生成样本看起来还可以就立刻停止训练。GAN的生成质量在训练过程中可能出现周期性的反复前一个epoch看起来挺好后一个epoch突然变差了。这种情况需要耐心观察几个周期再决定是否提前停止。4. 训练中的常见问题与排查思路4.1 判别器太强生成器梯度消失这是GAN初学阶段最容易踩的坑。表现是判别器的loss快速降到接近0但生成器的loss长时间不下降或者生成图像始终是一团模糊。原因在前面的数学部分已经分析过当D能轻松区分真伪时D(G(z))趋近于0此时如果还用原始的 (\log(1-D(G(z)))) 作为G的损失梯度就会趋近于0。这也是为什么论文实验部分会改用 (-\log D(G(z))) 的原因。排查思路按优先级排序检查生成器损失是否使用了 (-\log D(G(z)))而不是原始的 (\log(1-D(G(z))))调低判别器的学习率让D的更新慢于G减少每轮中D的更新次数比如k从2降到1给判别器加dropout或者减小判别器的网络容量使用标签平滑label smoothing把真实标签从1改成0.9左右给判别器留一点余地。4.2 模式崩塌生成器只会骗不会学模式崩塌mode collapse的表现是生成器找到了少数几个能骗过判别器的套路生成的样本多样性极差。比如在MNIST上G生成的数字可能只有1和7两类而且变化很小在CIFAR-10上可能生成的所有图片都带有类似的绿色背景。原论文并没有直接讨论模式崩塌但这个现象在后续的GAN研究中被反复提及。它的本质是G只需要骗过当前D就能获得足够低的损失并不需要学满整个真实分布的支撑集。早期常用的缓解手段包括小批量判别minibatch discrimination让D在判断真伪时参考同一batch内的多个样本如果某个batch内样本过于相似就判定为假。使用多个G或定期重新初始化G降低D的容量让D无法过强地对单个样本进行精确判断引入正则化项鼓励样本多样性。这些问题在现代的WGAN、StyleGAN等模型中已经得到了一定程度的缓解但模式崩塌并没有被彻底解决只是从显性变为了更隐蔽的形式。4.3 生成质量到底怎么评估GAN没有显式的对数似然所以训练到什么时候算好成了实践中的一个大问题。论文当时的处理方式是人工观察生成样本这也是早期GAN研究中最常用的手段主观性很强。后来的工作逐步引入了更客观的指标。Inception ScoreIS的思想是用预训练的Inception网络对生成样本做分类如果生成样本既清晰又多样得到的类别分布应该又集中又均匀。FIDFréchet Inception Distance则更进一步在Inception网络的某一层特征空间里计算真实样本和生成样本分布之间的Wasserstein距离数值越低越好。我在实际使用FID时的经验是FID对不同训练阶段的区分度比IS好很多而且对模式崩塌更敏感。但它也有局限它依赖Inception网络在ImageNet上学到的特征如果你处理的图像域和ImageNet差异很大FID的绝对值参考价值就会下降。下面整理一个常见问题的速查表方便对照排查现象可能原因优先处理方式D的loss快速归零D太强调低D的学习率加dropout使用标签平滑G的图像始终模糊训练没有收敛或G欠拟合增大模型容量调节学习率生成样本单一模式崩塌缩小D容量引入多样性约束训练中期loss大幅震荡博弈不平衡降低Adam的学习率使用更小的beta1生成图像出现棋盘格纹理转置卷积重叠改用上采样普通卷积或调整kernel大小5. 这篇论文到底影响了后来的什么5.1 DCGAN从全连接到卷积结构原始GAN论文在图像实验中使用的网络基本都是全连接层这在小型数据集上还能工作但对更复杂图像就力不从心。2015年Radford等人提出DCGAN把生成器和判别器全部替换成了卷积结构同时引入了BatchNorm、LeakyReLU、Adam等训练技巧让GAN的训练稳定性大幅提升。DCGAN的意义不仅在于结构改进更重要的是它提供了一套可复现的训练配方。当年我能第一次稳定地在64x64图像上训练GAN靠的就是DCGAN这套配置。可以说在那个人人复现GAN都靠玄学的年代DCGAN硬生生把训练GAN从一门艺术变成了工程。5.2 WGAN从根本上换掉损失函数原始GAN使用Jensen-Shannon散度作为理论依据但JS散度在p_data和p_g没有重叠或者重叠极小时梯度会消失。WGAN用Wasserstein距离替代了JS散度即使两个分布完全不重叠Wasserstein距离依然能提供有意义的梯度信号。WGAN的实践改动其实很小把判别器换成critic输出从概率变为实数critic的损失函数改为真实样本的输出期望减去生成样本的输出期望配合weight clipping或gradient penalty来满足Lipschitz约束。但就是这个改动解决了GAN训练中最大的痛点——梯度消失。我第一次跑WGAN的时候明显能感觉到loss曲线比原生GAN稳定好几个量级。5.3 从图像生成到多模态生成GAN的扩展版图GAN网络的影响范围远不止生成一张图片。条件生成想法Conditional GAN让生成过程可以受标签控制后来衍生出pix2pix这类图像翻译框架。CycleGAN实现了无配对图像到图像的转换让风格迁移、昼夜转换这类任务变得可落地。StyleGAN则在2018年后把高分辨率人脸生成的质量推到了几乎无法用肉眼分辨的水平。即使在扩散模型大放异彩的当下我们依然能看到GAN思想的影子。扩散模型的生成过程同样需要区分真实图像和被噪声污染的图像这种判别式的指导思想与GAN网络是一脉相承的。很多GAN时代积累的训练技巧——梯度惩罚、标签平滑、EMA——也被扩散模型的训练直接借鉴。我自己的阅读习惯是每隔一段时间就重新翻一翻这篇论文的原始版本每次都能读出一些新的东西。早年被对抗博弈这个概念震撼后来关注目标函数的设计细节再后来研究收敛性证明中的假设条件每一步都让我对生成式模型的理解更深一点。如果你刚接触GAN网络不用急着追着StyleGAN的代码跑先把这篇论文的公式亲手推一遍把训练循环的每一行代码和公式对应起来再去看后续的改进工作你会发现自己少走了很多弯路。