1. 从一张“假照片”说起GAN到底在解决什么问题第一次接触生成对抗网络Generative Adversarial Nets简称GAN的人脑子里冒出来的第一个问题通常是它跟普通的神经网络到底差在哪我刚开始学的时候也有这个困惑后来用一个特别土的例子想明白了——你把GAN想象成一个“造假币的”和一个“验钞的”在互相较劲。造假币的拼命想把假币做得跟真的一样验钞的拼命想分辨出哪张是真、哪张是假。两个人水平都在涨最后造假币的做出来的东西连验钞的都分不出来了。这就是GAN最核心的思想用两个网络互相博弈让生成器学会造出以假乱真的数据。生成器Generator负责“造假”判别器Discriminator负责“验真”。训练的过程就是这两方不断对抗、不断进步的过程。那它到底能干什么说白了GAN是一个数据生成模型。你给它一堆真实样本它就能学会生成类似的新样本。比如给它一堆人脸照片它能生成不存在的人脸给它一堆风景画它能画出新的风景。这个能力在图像生成、图像修复、超分辨率、风格迁移、数据增强等场景里都有大量应用。这篇文章适合谁看如果你刚接触深度学习听说过GAN但一直没搞明白它到底怎么运作的那这篇就是写给你的。我会从最基础的结构讲起把数学原理用大白话拆开再给出一份可以直接跑的代码最后把我自己踩过的坑和调参经验都倒出来。不要求你有多深的数学功底但最好对神经网络、梯度下降、损失函数这些概念有个基本印象。2. GAN的整体设计思路为什么是“对抗”而不是“单干”2.1 传统生成模型的困境在GAN出现之前生成模型主要有两类路子一类是基于似然函数的显式建模比如变分自编码器VAE另一类是隐式建模GAN就属于这一类。VAE的思路是把真实数据编码成一个隐空间的分布再从隐空间采样解码回数据。听起来很合理但它有个问题——VAE优化的是对数似然的下界生成出来的图像往往偏模糊。原因在于它用的重建损失通常是均方误差倾向于产生“平均化”的结果细节就丢失了。另一类方法像玻尔兹曼机理论上很漂亮但训练起来极其困难计算量巨大实际中很难 scale 到高维数据。GAN的提出者换了个思路既然直接建模数据分布很难那我干脆不建模了我让两个网络对抗间接地把分布学出来。这个想法在当时是相当大胆的。2.2 对抗训练的核心逻辑GAN的核心逻辑可以用一句话概括生成器努力骗过判别器判别器努力不被骗。具体来说生成器G输入一个随机噪声向量z输出一个生成样本G(z)。它的目标是让G(z)看起来像真实数据也就是让判别器D(G(z))的输出尽可能接近1判为真。判别器D输入一个样本x输出一个0到1之间的概率值表示这个样本是真实的概率。它的目标是对真实数据D(x)接近1对生成数据D(G(z))接近0。训练时两个网络交替更新。先固定G训练D几个step让它更好地分辨真假然后固定D训练G让它生成的东西更能骗过D。这个过程反复进行理论上最终会达到一个纳什均衡——G生成的分布和真实数据分布完全一致D对任何输入的输出都是0.5也就是完全分不出来。2.3 为什么这个思路能work你可能会问凭什么两个网络对着干最后就能学到真实分布从博弈论的角度看这个对抗过程其实是在优化一个极小极大minimax目标函数$$\min_G \max_D V(D, G) \mathbb{E}{x \sim p{data}(x)}[\log D(x)] \mathbb{E}_{z \sim p_z(z)}[\log(1 - D(G(z)))]$$这个公式看起来吓人拆开看就三块$\mathbb{E}{x \sim p{data}(x)}[\log D(x)]$判别器对真实数据的判断越大越好。$\mathbb{E}_{z \sim p_z(z)}[\log(1 - D(G(z)))]$判别器对生成数据的判断D(G(z))越小这一项越大。外层$\max_D$表示判别器要最大化这个值$\min_G$表示生成器要最小化这个值。理论上可以证明当判别器达到最优时生成器的最小化目标等价于最小化真实分布和生成分布之间的Jensen-Shannon散度。当两个分布完全一致时JS散度为0达到全局最优。当然理论归理论实际训练中远没有这么理想。这个我们后面会详细讲。2.4 方案选型为什么用全连接网络做入门原始GAN论文里用的是多层感知机MLP也就是全连接网络。虽然现在做图像生成基本都用卷积网络但作为入门我强烈建议从全连接版本开始。原因有三代码简单不需要处理卷积、池化、上采样这些操作核心逻辑一目了然。训练快在低维数据比如MNIST的28x28灰度图上全连接网络几分钟就能跑出结果。容易观察问题模式崩溃、梯度消失这些问题在全连接版本里更容易复现和理解。等你把全连接版本跑通了对GAN的训练动态有了直觉再去换卷积版本会顺畅很多。3. 核心细节拆解生成器和判别器到底长什么样3.1 生成器的结构设计生成器的任务是把一个低维噪声向量映射成高维数据。以MNIST为例输入是一个100维的噪声向量输出是一个784维的向量28x28展开。一个典型的生成器结构是这样的import torch import torch.nn as nn class Generator(nn.Module): def __init__(self, noise_dim100, hidden_dim256, output_dim784): super(Generator, self).__init__() self.net nn.Sequential( nn.Linear(noise_dim, hidden_dim), nn.LeakyReLU(0.2), nn.Linear(hidden_dim, hidden_dim * 2), nn.LeakyReLU(0.2), nn.Linear(hidden_dim * 2, output_dim), nn.Tanh() # 输出范围[-1, 1] ) def forward(self, z): return self.net(z)几个关键点激活函数用LeakyReLU原始GAN论文用的是ReLU但实践中LeakyReLU效果更好因为它能避免梯度消失。斜率设0.2是常见做法。最后一层用Tanh因为MNIST数据通常归一化到[-1, 1]Tanh的输出范围正好匹配。如果你用Sigmoid归一化到[0, 1]最后一层就用Sigmoid。隐藏层维度256和512是常用的选择。太小了生成能力不足太大了训练慢且容易过拟合。3.2 判别器的结构设计判别器就是一个二分类器输入一个784维向量输出一个标量概率。class Discriminator(nn.Module): def __init__(self, input_dim784, hidden_dim256): super(Discriminator, self).__init__() self.net nn.Sequential( nn.Linear(input_dim, hidden_dim * 2), nn.LeakyReLU(0.2), nn.Dropout(0.3), nn.Linear(hidden_dim * 2, hidden_dim), nn.LeakyReLU(0.2), nn.Dropout(0.3), nn.Linear(hidden_dim, 1), nn.Sigmoid() # 输出概率 ) def forward(self, x): return self.net(x)判别器这边有几个细节值得说Dropout加Dropout是为了防止判别器太强。如果判别器一眼就能分辨真假生成器就学不到东西了。Dropout rate设0.3左右比较合适。最后一层Sigmoid输出0到1的概率值。如果你用BCEWithLogitsLoss可以去掉Sigmoid让损失函数内部处理数值更稳定。判别器不要比生成器强太多这是GAN训练的一个核心原则。判别器太强生成器梯度消失判别器太弱生成器学不到正确的方向。3.3 损失函数的选择与实现原始GAN用的是二元交叉熵损失BCE Loss。对于判别器criterion nn.BCELoss() # 真实数据的损失 real_labels torch.ones(batch_size, 1) real_loss criterion(D(real_data), real_labels) # 生成数据的损失 fake_labels torch.zeros(batch_size, 1) fake_loss criterion(D(fake_data), fake_labels) # 判别器总损失 d_loss real_loss fake_loss对于生成器# 生成器希望判别器把生成数据判为真 g_loss criterion(D(fake_data), real_labels)这里有个容易搞混的地方生成器的损失里标签用的是real_labels全1。因为生成器的目标是骗过判别器让判别器认为生成的数据是真的。注意在实际实现中生成器的损失有两种写法。一种是上面这种“把假数据标为真”另一种是“最小化log(1 - D(G(z)))”。前者梯度更大训练更稳定实践中更常用。原始论文用的是后者但后来大家发现前者效果更好。3.4 噪声输入的采样策略生成器的输入噪声z通常从标准正态分布采样z torch.randn(batch_size, noise_dim)也有人用均匀分布但正态分布更常见。噪声维度一般设100这个维度不能太低否则生成器没有足够的“原料”来生成多样化的样本也不能太高否则训练慢且容易过拟合。有个经验噪声维度应该和数据的固有维度相匹配。MNIST的固有维度估计在10到20之间用100维噪声其实是过参数化了但这样训练更稳定。4. 完整实操从零训练一个MNIST生成器4.1 环境准备与数据加载先确保你装了PyTorch和torchvision。数据加载很简单from torchvision import datasets, transforms from torch.utils.data import DataLoader transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)) # 归一化到[-1, 1] ]) train_dataset datasets.MNIST(root./data, trainTrue, downloadTrue, transformtransform) train_loader DataLoader(train_dataset, batch_size128, shuffleTrue)归一化到[-1, 1]是为了匹配生成器最后一层的Tanh输出。如果你忘了这一步生成器输出和真实数据不在一个范围判别器一下子就能分辨出来训练会崩。4.2 训练循环的完整实现训练循环是GAN的核心也是最容易出错的地方。先看完整代码import torch.optim as optim # 初始化 device torch.device(cuda if torch.cuda.is_available() else cpu) G Generator().to(device) D Discriminator().to(device) # 优化器 lr 0.0002 beta1 0.5 optimizer_G optim.Adam(G.parameters(), lrlr, betas(beta1, 0.999)) optimizer_D optim.Adam(D.parameters(), lrlr, betas(beta1, 0.999)) criterion nn.BCELoss() # 训练参数 epochs 50 noise_dim 100 for epoch in range(epochs): for i, (real_data, _) in enumerate(train_loader): batch_size real_data.size(0) real_data real_data.view(batch_size, -1).to(device) # --------------------- # 训练判别器 # --------------------- optimizer_D.zero_grad() # 真实数据 real_labels torch.ones(batch_size, 1).to(device) output_real D(real_data) d_loss_real criterion(output_real, real_labels) # 生成数据 z torch.randn(batch_size, noise_dim).to(device) fake_data G(z) fake_labels torch.zeros(batch_size, 1).to(device) output_fake D(fake_data.detach()) # detach很重要 d_loss_fake criterion(output_fake, fake_labels) d_loss d_loss_real d_loss_fake d_loss.backward() optimizer_D.step() # --------------------- # 训练生成器 # --------------------- optimizer_G.zero_grad() z torch.randn(batch_size, noise_dim).to(device) fake_data G(z) output_fake D(fake_data) g_loss criterion(output_fake, real_labels) # 注意标签是1 g_loss.backward() optimizer_G.step() print(fEpoch [{epoch1}/{epochs}] | D Loss: {d_loss.item():.4f} | G Loss: {g_loss.item():.4f})4.3 关键参数的计算与选择学习率0.0002是GAN的经典学习率。太高了训练不稳定太低了收敛慢。Adam优化器的beta1设0.5而不是默认的0.9是因为0.9的动量会让训练初期震荡太大。Batch size128是一个平衡点。太小了梯度噪声大太大了判别器更新太稳生成器学不到东西。训练比例判别器和生成器1:1交替训练。有些实现会训练判别器k次再训练生成器1次原始论文k1。实践中k1就够了k太大会导致判别器过强。噪声维度100维。这个数字不是绝对的64、128都可以但100是社区约定俗成的默认值。4.4 训练过程的监控与可视化光看loss值是不够的GAN的loss经常看起来在震荡但实际在进步。我习惯每隔几个epoch保存一次生成样本import matplotlib.pyplot as plt def show_samples(G, noise_dim, epoch, device, num16): G.eval() with torch.no_grad(): z torch.randn(num, noise_dim).to(device) samples G(z).view(num, 28, 28).cpu() fig, axes plt.subplots(4, 4, figsize(6, 6)) for i, ax in enumerate(axes.flat): ax.imshow(samples[i], cmapgray) ax.axis(off) plt.savefig(fgan_epoch_{epoch}.png) plt.close() G.train()训练初期生成的是噪声中期开始出现数字的轮廓后期数字变得清晰。如果训练了几十个epoch还是纯噪声那肯定是哪里出问题了。5. 常见问题与排查技巧实录5.1 模式崩溃生成器只会造同一种样本这是GAN最经典的问题。你训练了半天发现生成器不管输入什么噪声输出的都是同一个数字比如全是“1”。原因生成器发现只要造出某一种特别像真的样本就能骗过判别器于是它就“躺平”了不再尝试生成其他类别。解决方法加Dropout判别器加Dropout可以降低它对特定模式的过度自信。用Mini-batch Discrimination让判别器同时看一批样本如果一批里全是同一个数字就判为假。这个实现稍复杂入门阶段可以先跳过。调整训练比例如果判别器太强减少判别器的训练次数。换损失函数Wasserstein GANWGAN用Wasserstein距离代替JS散度能有效缓解模式崩溃。这个我们后续系列会讲。5.2 梯度消失生成器loss不下降训练过程中如果判别器太强D(G(z))接近0那么生成器的损失log(1 - D(G(z)))就接近0梯度几乎消失生成器完全学不动。判断方法看判别器的loss。如果D loss接近0说明判别器太强了。解决方法降低判别器的学习率比如把D的学习率设成G的一半。给判别器加噪声在判别器的输入上加高斯噪声降低它的分辨能力。用标签平滑把真实标签从1改成0.9防止判别器过度自信。换生成器损失用“把假数据标为真”的写法梯度更大。5.3 训练不稳定loss剧烈震荡GAN的loss震荡是常态但如果震荡幅度特别大甚至出现NaN那就是有问题了。排查清单问题现象可能原因解决方法loss出现NaN学习率太高降到0.0001或更低loss剧烈震荡batch size太小增大到128或256D loss一直下降判别器过强加Dropout或降低D学习率G loss一直上升生成器太弱增加生成器层数或宽度生成样本模糊训练不充分增加epoch或调整网络结构5.4 实操避坑心得坑一忘了detach。训练判别器时对生成数据要加.detach()否则梯度会传回生成器导致生成器被错误更新。这个错误很隐蔽因为代码不会报错但训练结果会很差。坑二数据归一化不匹配。生成器最后一层用Tanh输出是[-1, 1]但真实数据没归一化到同样范围判别器一眼就能分辨。这个错误会导致D loss迅速降到0G loss迅速上升。坑三优化器zero_grad顺序。一定要先zero_grad()再计算loss再backward()。如果顺序搞反了梯度会累积训练直接崩。坑四用错标签。生成器的损失里标签要用1真实标签不是0。这个逻辑容易绕晕记住一句话生成器的目标是让判别器认为生成数据是真的。坑五过早下结论。GAN训练初期生成的东西就是噪声这是正常的。至少要训练20个epoch再判断有没有问题。我见过有人跑了3个epoch就说GAN不work这太急了。6. 从入门到进阶下一步可以怎么走全连接GAN跑通之后你算是入了门。但真正的GAN世界远不止这些。接下来可以往几个方向深入方向一换卷积网络。把全连接层换成卷积层和转置卷积层就能处理更大尺寸的图像。DCGAN是这方面的经典工作它给出了一套卷积GAN的设计规范比如用步长卷积代替池化、用BatchNorm稳定训练等。方向二换损失函数。WGAN用Wasserstein距离代替JS散度训练更稳定模式崩溃更少。WGAN-GP进一步用梯度惩罚代替权重裁剪效果更好。方向三条件生成。Conditional GANcGAN在生成器和判别器里都加入标签信息可以控制生成的内容。比如你想生成“数字3”就把标签3作为额外输入。方向四高分辨率生成。Progressive GAN、StyleGAN这些工作把生成质量推到了照片级。StyleGAN的架构设计尤其精妙值得仔细研究。方向五应用到其他领域。GAN不只能生成图像还能做图像翻译CycleGAN、超分辨率SRGAN、数据增强、异常检测等。我个人建议的路线是先把全连接GAN跑通理解对抗训练的基本动态然后换DCGAN感受卷积生成器的威力再学WGAN理解损失函数对训练稳定性的影响最后根据你的具体需求选择进阶方向。学GAN最忌讳的就是只看论文不动手。这个系列的后续文章我会带着大家一步步实现这些进阶模型把每个模型的代码都跑一遍。有些东西看论文觉得懂了一写代码就发现全是坑。动手才是硬道理。