
搞深度学习和计算机视觉的这几年被问到频率最高的问题之一就是对抗生成网络到底是怎么跑起来的为什么两个网络互相掐架最后就能凭空造出以假乱真的图片。这个问题看着简单但真要讲清楚得从它的架构设计、博弈逻辑、损失函数、训练技巧一层层剥开。对抗生成网络Generative Adversarial Network后面我统一简称GAN不是某一个固定模型而是一类架构范式核心就一句话用一个生成器去骗用一个判别器去抓两者在对抗中共同进化。它能做的事情非常多从图像生成、图像修复、超分辨率、风格迁移到数据增强、隐私保护下的合成数据甚至药物分子设计都能看到它的影子。这篇内容主要面向刚入门深度学习、想搞懂GAN架构原理的开发者以及已经会调库但一直没弄明白为什么这么设计的进阶读者。我会尽量避开纯论文式的推导用从业者的视角把架构拆开讲清楚每一层为什么这么搭、参数为什么这么设、训练崩了该怎么救。看完之后你应该能自己从零手写一版能收敛的GAN并且知道遇到模式崩塌时该往哪个方向排查。1. 从零理解GAN两个网络的博弈到底在争什么GAN这个概念是2014年被提出来的它的架构设计思路在当时相当反直觉——不定义一个明确的损失去衡量生成的图好不好而是让两个网络互相打分。这个思路的妙处在于它把判断生成质量这件很难用公式描述的事情交给了另一个可学习的网络去完成。要真正理解架构先得把这两个角色的职责分清楚。1.1 生成器与判别器的分工与信息流生成器Generator记作G的输入是一段随机噪声通常是从标准正态分布或均匀分布里采样出来的低维向量比如100维或128维行业内习惯叫它隐向量或者latent code记作z。G的任务是把这段毫无意义的随机数映射成一张看起来像真实数据的样本比如一张64×64×3的图片。判别器Discriminator记作D的输入是一张图片输出是一个标量表示这张图来自真实数据而不是生成数据的概率取值在0到1之间。信息流是这样的真实数据x和生成数据G(z)分别送进DD给出判别结果然后误差反向传播G根据D的判断结果调整自己让自己生成的东西越来越能骗过DD则根据有没有判对调整自己让自己越来越能识破G。这两个网络的参数是完全独立的训练时交替更新。很多人第一次看会误以为它们是共享权重的其实不是它们是两个独立的网络只是在同一个训练循环里轮流优化。理解这一点非常关键因为后面所有的训练技巧本质上都是在调整这两个网络的实力平衡。1.2 极小极大博弈的数学直觉GAN的目标函数写出来是一个极小极大问题生成器要最小化目标判别器要最大化目标。目标函数由两部分组成一部分是真实数据被判别为真的对数概率期望另一部分是生成数据被判别为假的对数概率期望。判别器希望这两项都尽可能大也就是把真图判真、假图判假生成器则希望第二项尽可能小也就是让假图被判别为真。这里有个很漂亮的数学结论当判别器达到最优时生成器最小化这个目标等价于最小化真实分布和生成分布之间的JS散度。这句话是整个GAN理论的基石它解释了为什么对抗训练能生效。但同样的结论也埋下了隐患——JS散度在两个分布完全不重叠的时候会变成一个常数梯度消失训练就卡死了。后面我讲WGAN的时候会重点说这个问题因为它直接决定了你在实际项目里该选哪套损失函数。1.3 为什么对抗训练比直接回归更难驯服如果用普通的均方误差去训练一个生成模型网络只需要把输出往目标像素上靠梯度方向明确训练相对稳定。但GAN不是这样G的梯度来自D而D本身在训练中不断变化。这就好比你在追一个也在移动的目标而且这个目标还会根据你的行为调整策略。这种动态博弈带来两个直接后果一是训练不稳定损失曲线经常上下震荡没有明确的收敛标志二是容易出现模式崩塌G发现只要生成某一类样本就能骗过D于是干脆放弃多样性反复生成同一种图。我个人的体会是不要拿训练普通CNN的心态去训练GAN。普通网络的loss降下去就基本稳了GAN的loss震荡反而是常态。判断训练好没好的标准不能只看损失数值得看生成样本的视觉质量和多样性。这个认知转变是入门GAN的第一道坎。2. 架构选型从原始GAN到现代主流变体的演进逻辑原始GAN提出时用的是全连接网络在MNIST这种28×28的小图上勉强能跑但一到彩色图像就彻底不行生成的东西糊成一团。这十几年里架构层面的改进一直没有停过。理解这些变体不是让你背论文而是让你在选型时知道每种设计的取舍在哪里。2.1 原始GAN的架构缺陷在哪里最朴素的GANG和D都是几层全连接堆起来的。G把100维噪声映射到784维对应28×28展开的向量D把784维映射到一个概率值。这个架构在低分辨率灰度图上还能看但问题很明显全连接层参数量巨大而且完全丢掉了图像的局部空间结构。图像相邻像素之间的相关性在展平成一维向量之后就没了网络得靠大量参数硬记这种关系效率极低。更麻烦的是全连接G很容易生成大量全局都差不多、只有个别像素不同的样本这在视觉上表现为糊。所以原始GAN的架构注定只是个概念验证真正让它能用起来的是卷积结构的引入。2.2 DCGAN卷积化改造的关键设计准则DCGAN可以说是让GAN真正实用化的转折点。它提出了一套被广泛沿用的架构准则即使放到今天做图像生成大部分依然适用。这套准则里最关键的一条是用带步长的转置卷积替代池化层来做上采样用带步长的卷积替代池化来做下采样。原因在于池化操作是不可学习的而转置卷积可以让网络自己学习如何放大特征图这对于生成任务非常重要。第二条准则是生成器和判别器都加批归一化BatchNorm但G的输出层和D的输入层除外。G输出层不加是因为输出要映射到具体的像素值范围批归一化会干扰这个映射D输入层不加是因为真实数据和生成数据的分布差异正是D要捕捉的信号归一化掉反而削弱了判别能力。第三条是去掉全连接层让网络全卷积化。第四条是激活函数的选择G内部用ReLU输出层用tanh把值压到-1到1之间D内部用LeakyReLU斜率一般设0.2避免负区间梯度全死。这几条准则我是建议直接照抄的尤其在做64×64到256×256的图像生成时基本不会错。下面这张表把常见架构层面的变体做个对照方便你选型时快速定位。架构变体核心改动解决的主要问题适用场景原始GAN全连接概念验证低分辨率灰度图DCGAN转置卷积BN去全连接图像结构丢失、生成模糊64到256分辨率图像生成WGAN/WGAN-GP换Wasserstein距离梯度惩罚JS散度梯度消失、训练不稳对稳定性要求高的任务LSGAN用最小二乘损失梯度消失、生成质量差通用图像生成StyleGAN风格调制逐层噪声注入生成可控性、细节质量高分辨率人脸、艺术生成2.3 WGAN与WGAN-GP把不稳定的损失换掉前面提到JS散度在两个分布不重叠时梯度会消失WGAN的思路是干脆换一个衡量分布距离的指标——Wasserstein距离也叫推土机距离。它的直观含义是把生成分布搬成真实分布所需要的最小搬运成本。这个指标的好处是即使两个分布完全不重叠它依然能提供有效的梯度不会像JS散度那样直接变成常数。但Wasserstein距离有个约束条件要求判别器满足1-Lipschitz连续性。原始WGAN用的是权重裁剪把判别器所有参数强制截断到一个很小的区间里比如-0.01到0.01。这个方法实现简单但容易导致参数被压到边界网络表达能力退化。WGAN-GP改用梯度惩罚来满足Lipschitz约束在损失里加一项对判别器梯度范数的惩罚让它接近1。实践证明梯度惩罚比权重裁剪稳定得多也是我现在做项目时的默认选择。这里有个实操细节梯度惩罚项只在真实样本和生成样本之间的插值点上计算不需要对整个空间都算否则计算量会爆炸。2.4 StyleGAN的风格调制思路为什么值得了解如果你的目标是高分辨率、高可控性的生成StyleGAN系列的架构设计绕不开。它的核心创新是把隐向量不再直接输入生成器第一层而是经过一个映射网络变成中间隐向量w再通过自适应实例归一化AdaIN注入到生成器的每一层。这样做的效果是不同层控制不同尺度的特征——浅层控制姿态、脸型这种粗粒度属性深层控制发色、肤质这种细粒度纹理。这个设计的意义在于解耦。传统GAN的隐向量各维度纠缠在一起你想改一个属性往往其他属性也跟着变。StyleGAN通过逐层注入和噪声注入把控制粒度分开了这也是它能在人脸生成上做出那么多可控编辑的原因。虽然完整复现StyleGAN成本很高但它的思路——分层控制、噪声注入增加细节随机性——完全可以借鉴到自己的小项目里。3. 核心组件拆解与实操参数配置架构选好之后真正决定你能不能跑出结果的是每个组件的具体配置。这一块我踩过的坑最多也最值得展开讲。很多教程只给一个能跑的demo但不说清楚参数为什么这么设结果你一换数据集就崩。3.1 生成器网络结构设计要点以生成64×64彩色图为例我常用的G结构是这样的输入一个100维的噪声向量先经过一个全连接层映射到4×4×512然后reshape成4×4的特征图接着连续做四次转置卷积上采样通道数依次从512降到256、128、64、3每次上采样倍数为2最终得到64×64×3。这里的通道数递减规律不是随便定的它跟下采样时的通道递增是对称的能保证信息容量平滑过渡。转置卷积的核大小我一般用4步长用2填充用1这样输出的尺寸刚好是输入的2倍公式是输出尺寸 (输入尺寸 - 1) × 步长 - 2 × 填充 核大小。代入4×4得到(4-1)×2 - 2×1 4 8正好翻倍。这个计算你自己推一遍以后改分辨率就不用试错了。注意生成器输出层用tanh意味着训练数据的像素值必须归一化到-1到1。如果你用0到1的归一化配tanh生成质量会明显下降这是新手最常犯的错之一。3.2 判别器网络结构设计要点D的结构基本是G的镜像输入64×64×3连续四次步长为2的卷积下采样通道数从64升到128、256、512最后接一个全连接到单输出再过Sigmoid得到概率。激活函数内部用LeakyReLU斜率0.2。这里要强调一点D的输入层不加BatchNorm原因前面说过是为了保留输入分布的信息。另外D的参数量通常和G相当甚至略小不要为了让D更强而堆太多层判别器过强会直接导致生成器梯度消失。3.3 损失函数与优化器配置损失函数的选择直接决定训练稳定性。原始GAN用二元交叉熵实现简单但有梯度消失风险。我现在的默认方案是WGAN-GP判别器损失是生成样本判别均值减去真实样本判别均值再加上梯度惩罚项生成器损失就是负的生成样本判别均值。注意WGAN里判别器不再输出概率而是输出一个实数分数所以最后一层不要加Sigmoid。优化器方面Adam是最常用的学习率一般设1e-4到2e-4beta1设0.5而不是默认的0.9。这个0.5很关键因为GAN的梯度噪声大用0.9的动量会让历史梯度累积太多导致训练震荡。beta2保持0.999就行。如果你用WGAN-GP判别器和生成器的学习率可以设成不一样的判别器略高一点这叫TTUR双时间尺度更新规则能加快收敛。3.4 训练超参数与批次设置批次大小一般设64或128太小梯度噪声大太大显存吃紧且泛化差。判别器每更新一次生成器更新一次这是最常见的1:1比例。但如果你的判别器太强可以改成判别器更新5次、生成器更新1次反之亦然。隐向量维度100或128都行太小表达能力不足太大容易过拟合且没必要。下面给一份我常用的配置清单可以直接作为起点# 生成器与判别器的核心超参数配置PyTorch风格 config { latent_dim: 100, # 隐向量维度 img_size: 64, # 输出图像尺寸 channels: 3, # 图像通道数 g_lr: 1e-4, # 生成器学习率 d_lr: 2e-4, # 判别器学习率TTUR略高于G beta1: 0.5, # Adam一阶动量GAN场景下必须调低 beta2: 0.999, # Adam二阶动量 batch_size: 64, # 批次大小 n_critic: 5, # 每轮判别器更新次数WGAN-GP常用5 lambda_gp: 10, # 梯度惩罚系数 epochs: 200, # 训练轮数 }这份配置在64×64的CIFAR-10或者人脸数据集上都能跑出可看的结果。n_critic设5是WGAN-GP论文推荐的配合梯度惩罚能让判别器估计更准。4. 完整训练流程实操从数据准备到模型收敛光有架构和参数还不够训练循环的写法决定了你能不能稳定复现。这一节我把整个流程走一遍包括那些容易被忽略的细节。4.1 数据准备与预处理数据归一化到-1到1是硬性要求因为生成器输出层用tanh。除此之外我建议做随机水平翻转增强成本低且对生成多样性有好处。如果你数据量不大比如只有几千张图可以考虑做一点随机裁剪但别做太激进的几何变换否则会引入不真实的形变让生成器学到错误的分布。数据集加载建议用现成的DataLoader设置num_workers大于0来并行读取不然GPU会经常等数据。我实测过在机械硬盘上num_workers设4左右性价比最高设太高反而因为进程切换拖慢速度。4.2 训练循环实现与梯度惩罚写法训练循环的骨架是每轮先从噪声采样生成假图然后用真实图和假图分别更新判别器若干次WGAN-GP是n_critic次再更新一次生成器。梯度惩罚项的实现是容易写错的地方我这里给个规范的写法import torch def gradient_penalty(D, real, fake, device): 计算WGAN-GP的梯度惩罚项 batch_size real.size(0) # 在真实样本和生成样本之间随机插值 alpha torch.rand(batch_size, 1, 1, 1, devicedevice) interpolated (alpha * real (1 - alpha) * fake).requires_grad_(True) # 判别器对插值样本的打分 score D(interpolated) # 计算梯度 grad torch.autograd.grad( outputsscore, inputsinterpolated, grad_outputstorch.ones_like(score), create_graphTrue, retain_graphTrue, only_inputsTrue )[0] # 梯度范数惩罚目标范数为1 grad_norm grad.view(batch_size, -1).norm(2, dim1) penalty ((grad_norm - 1) ** 2).mean() return penalty这段代码有两个坑要提醒一是create_graph和retain_graph都必须为True否则惩罚项无法参与反向传播二是interpolated必须调用requires_grad_不然算不出梯度。我第一次写的时候漏了第二点程序不报错但惩罚项恒为0训练照样稀里糊涂跑完结果模型质量差得莫名其妙排查了很久才定位到。4.3 训练过程监控指标怎么选GAN的损失值不能作为训练好坏的唯一标准我一般同时监控三类指标。第一类是损失本身主要看趋势不要看绝对值只要G和D的损失没有出现一方持续爆炸或归零就行。第二类是生成样本的视觉快照每隔几个epoch存一张网格图肉眼判断质量和多样性这是最可靠的判断方式。第三类是定量指标FIDFréchet Inception Distance是业内最常用的它衡量生成分布和真实分布在特征空间的距离数值越低越好。ISInception Score也有用但缺陷较多对类别敏感我一般作为参考不作为唯一依据。监控FID要注意它依赖Inception网络的特征提取不同实现算出来的数值可能不一样所以你只能拿同一个脚本下的数值做纵向对比不要跨项目横比。4.4 收敛判断与停止条件GAN没有严格的收敛点判断可以停的标准是生成样本视觉质量稳定、FID曲线进入平台期不再明显下降、样本多样性没有塌缩。我通常设一个固定的epoch上限比如200到500然后取FID最低的那个checkpoint作为最终模型而不是取最后一个epoch。因为GAN后期容易过拟合或者模式崩塌最后一个epoch未必是最好的。提示训练过程中建议每隔10个epoch保存一次模型权重和对应的FID别只存最后一个。我吃过这个亏跑了一晚上发现最后崩了但中间最好的权重没存只能重跑。5. 常见问题与排查技巧实录GAN的训练问题八成以上集中在几个经典症状上。这一节我把踩过的坑整理成排查思路你遇到问题时可以对照着查。5.1 模式崩塌生成样本千篇一律模式崩塌最典型的表现是不管你输入什么噪声生成出来的图都差不多。根本原因是G发现某一种输出能稳定骗过D就放弃了探索其他模式。解决思路有几个方向一是改损失函数换WGAN-GP通常有明显改善二是给D加噪声削弱它对单一模式的过拟合三是用minibatch discrimination让D能看到一个批次内样本之间的差异从而惩罚缺乏多样性的生成四是做特征匹配让G去匹配真实样本在D中间层的特征统计量而不是只盯着最终打分。我实际项目里换WGAN-GP加TTUR能解决大部分轻中度模式崩塌严重的话需要额外加minibatch discrimination。但要注意minibatch discrimination计算成本高批次内部两两比较批次大了显存吃不消。5.2 训练不稳定损失震荡或梯度消失损失震荡是正常的但如果D的损失迅速降到接近0说明D太强G已经骗不过它梯度就消失了。这时候可以降低D的学习率减少D的更新次数或者给D的输入加高斯噪声。反过来如果G的损失迅速趋近0而生成质量很差通常是D太弱可以适当提高D的学习率或增加D的层数。梯度消失还有一个常见原因是激活函数用错比如D内部用了ReLU导致负区间梯度全为0一定要用LeakyReLU。另一个原因是输出层配置错误WGAN的D输出层加了Sigmoid会导致分数被压缩梯度信号变弱。5.3 标签平滑与噪声注入的实操细节标签平滑是个简单但很有效的技巧训练D时真实样本的标签不用1.0而用0.9生成样本的标签不用0.0而用0.1。这样做的目的是防止D对判断过于自信给它留一点容错空间从而让G的梯度不至于太弱。实测下来加标签平滑后训练震荡明显减轻。噪声注入也类似往D的输入上加一点高斯噪声或者往判别器的标签上加噪声。这些手段本质上都是在削弱D的判别精度让博弈更平衡。但要注意噪声尺度加太多会让D学不到有用信息一般输入噪声标准差控制在0.1以内。下面这张表把常见问题、可能原因和排查手段汇总方便你快速定位。问题现象可能原因排查与解决生成样本千篇一律模式崩塌G探索不足换WGAN-GP、加minibatch discrimination、特征匹配D损失快速趋近0判别器过强降D学习率、减少D更新次数、加输入噪声G损失趋近0但图很差判别器过弱提高D学习率、增加D容量梯度完全消失激活函数或输出层配置错用LeakyReLUWGAN的D去掉Sigmoid损失剧烈震荡动量参数过大或批次太小beta1降到0.5增大批次到64以上训练后期质量下降过拟合或后期崩塌保存中间checkpoint取FID最低的权重生成图有规律噪点转置卷积的棋盘伪影核大小用4且能被步长整除或改用最近邻上采样加卷积5.4 转置卷积棋盘伪影的成因与规避棋盘伪影是转置卷积特有的问题表现为生成图上出现规律性的网格状纹理。成因是转置卷积在步长大于核的约数时不同位置的卷积核重叠不均匀导致输出像素被不均匀地加权。规避方法有两个一是选核大小为4、步长为2这样核大小能被步长整除重叠均匀二是干脆不用转置卷积改用最近邻插值上采样接一个普通卷积虽然慢一点但几乎没有伪影。我在做高分辨率生成时更倾向第二种方案画质更干净。6. 从玩具到实战把架构思路迁移到自己的任务很多人学会跑demo之后卡在怎么迁移到自己的任务上。这一节聊聊几个实际迁移时的考虑点都是我在项目里验证过的思路。6.1 条件生成与类别控制的接入方式如果你希望生成指定类别的图像比如指定生成某种字体或某个物体可以用条件GANcGAN。做法是把类别标签经过嵌入层变成向量然后拼接到G的输入噪声后面同时也在D的输入里把标签和图像拼接起来。这样G和D都能感知类别信息。实现上要注意拼接方式比相加更稳定因为相加会让噪声和标签互相干扰网络难以区分。如果你的类别数很多比如上百类建议用嵌入层而不是独热编码能显著降低输入维度。另外条件信息的注入位置也有讲究浅层注入影响粗粒度属性深层注入影响细节这和StyleGAN的分层思路是一脉相承的。6.2 小数据集下的过拟合与数据增强策略GAN在小数据集上很容易让D记住每一张真实图从而对生成图极其苛刻导致G学不动。对策是给D加更强的正则化比如提高Dropout比例、加权重衰减、做数据增强。数据增强这块有个技巧叫可微分增强就是把翻转、裁剪等操作也做成可微的让梯度能传回去同时保证真实图和生成图经过同样的增强避免分布不匹配。另一个思路是降低D的容量别看数据集小就把D做得很深那只会加剧过拟合。我一般在小数据集上把D的层数减一到两层效果反而更好。6.3 评估一个GAN项目是否值得上线最后说点工程视角的。一个GAN能不能上线光看FID不够还要看推理速度、显存占用、生成稳定性。我通常会做一个抽检流程固定一批随机种子每次生成都记录结果跑一段时间看同样的种子能不能稳定生成相似质量的图。如果波动很大说明模型不够稳上线后用户体验会很差。另外要统计生成失败率比如出现严重畸变的比例这个指标往往比平均FID更能反映真实体验。我在实际使用中发现把训练好的生成器单独导出推理、冻结所有BatchNorm改成推理模式这一步经常被忽略。如果忘了切换推理模式BatchNorm会用当前批次的统计量导致单张生成和批量生成结果不一致线上很容易出问题。导出前一定记得调用eval()并且用torch.no_grad()包住推理过程既省显存又快。踩过几次坑之后我现在会把推理脚本单独写一份和训练脚本彻底分开避免相互干扰。这套架构思路后面还可以往视频生成、3D生成方向扩展核心的博弈逻辑不变变的是生成器和判别器的结构设计理解了博弈本质迁移起来就不难。