
第一次翻开那篇2014年的论文很多人会卡在同一个地方——那个 (\min_G \max_D) 的公式。生成器Generator和判别器Discriminator两个网络对着训练一个负责造假、一个负责识破听起来像绕口令但真正让人头大的往往是后面那句追问原始GAN公式里的交叉熵为什么没有负号。这篇就围绕对抗生成网络GAN这件事把它的直觉、公式、代码、训练陷阱和后来的改进串成一条线来讲。适合刚接触生成模型的同学也适合已经跑过几个Demo、但被模式崩溃和梯度消失折磨过的人。我不打算把它写成论文翻译而是按我自己复现时踩过的顺序一块一块拆开。1. GAN的本质一场造假者与鉴定师的猫鼠游戏1.1 为什么生成比判别难一个量级分类任务做久了会有一种错觉觉得让模型认东西已经够难了。但你仔细想想判别是从高维往下压——输入一张图片输出一个概率信息一直在做减法。生成恰恰相反是从一个几十维的随机噪声撑开成一张几百×几百像素的图中间每一层都在凭空补信息。判别器看到的是完整答案它只需要判断像不像生成器手里只有一堆随机数它得猜出长什么样才算像。这个信息量的不对称是GAN一切麻烦的根源。判别器给生成器的反馈只有一个标量——你造的这张我给了0.3分但生成器要拿这个0.3分去调整几十万个参数的梯度。这就好比老师只告诉你作文61分却不告诉你哪里扣了分你得自己琢磨。也正因为反馈信息如此稀疏GAN的训练才需要大量的迭代而生成质量才那么依赖网络结构和超参数的调校。另一个常被忽略的点是生成任务没有唯一正确答案。判别器判一张图假不代表这张图哪里错了只代表它整体分布不对。所以生成器优化的不是某一个正确的输出而是整个输出分布要贴合真实数据分布。这个视角一旦转过来后面公式里的期望符号 (\mathbb{E}) 就好理解了——它对着一整批数据算不是一个样本。1.2 生成器与判别器的分工与博弈把两个网络放在一起看它们的角色其实很清楚判别器D输入一张图输出一个0到1之间的数越接近1代表越相信是真的。它的目标是把真图判高、把假图判低。生成器G输入一个噪声向量 (z)通常是从标准正态分布里采的输出一张图目标是让D把它判成真的。关键在于它们共享同一个损失信号但优化方向相反。D想把这个分数体系拉开差距G想把差距抹平。整个过程不是固定的谁赢谁输而是一个动态平衡G进步了D面对的样本就更难分D进步了G就得更努力地去模仿。这种互相拉扯的设计本质上是一种对抗式的训练信号构造——用D代替了传统生成模型里那个没法直接算的数据似然。我个人的体会是理解GAN最有效的方式不是盯着公式推导而是先在脑子里放一个画面街头有个造假币的和一个验钞的验钞的每次升级防伪造假的下次就得升级工艺。真币的图案从不改变但两个玩家的水平在一轮轮往上走。GAN训练里那个真币就是真实数据分布它永远是固定的靶子。2. 把原始GAN公式拆到不能再拆那个负号问题2.1 原始公式长什么样每一项在说什么原始GAN的目标函数写成这样$$\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}}[\log D(x)]) 是在真实数据上算的从数据集里取真图 (x)喂给DD输出一个概率 (D(x))然后取对数、取期望。D判得越准把真图判成接近1这一项越接近0判错了真图判成0(\log D(x)) 就趋向负无穷。第二项 (\mathbb{E}_{z\sim p_z}[\log(1 - D(G(z)))]) 是在生成数据上算的采样噪声 (z)G造一张假图 (G(z))喂给DD输出 (D(G(z)))我们希望它接近0识别为假那么 (1 - D(G(z))) 就接近1取对数接近0。这里有个非常容易读错的地方公式是一个整体外面套了 (\min_G \max_D)。也就是判别器D要最大化整个 (V)生成器G要最小化整个 (V)。很多人看这一项一项都往0靠就以为都在最小化其实D是在最大化。2.2 交叉熵里明明有负号为什么GAN公式里没有这就是热搜里那个问题也是我当年盯着看了半小时的地方。答案其实一句话因为公式里写的是最大化而交叉熵损失函数的标准写法是最小化。先把二元交叉熵的标准形式摆出来。二分类里样本真实标签记作 (y)真为1假为0模型预测为1的概率记作 (\hat{y})那么交叉熵损失是$$L -[,y\log \hat{y} (1-y)\log(1-\hat{y}),]$$注意前面那个负号。现在把GAN的两种情况代进去。对真实样本标签 (y1)预测 (\hat{y}D(x))代入单项$$L_{real} -\log D(x)$$对生成样本标签 (y0)预测 (\hat{y}D(G(z)))代入单项$$L_{fake} -\log(1 - D(G(z)))$$两批样本合起来的平均损失就是$$L_D -,\mathbb{E}{x\sim p{data}}[\log D(x)] - \mathbb{E}_{z\sim p_z}[\log(1 - D(G(z)))]$$你把这个式子和原始GAN的 (V(D,G)) 一对比就明白了$$L_D -V(D,G)$$也就是说最小化交叉熵损失 (L_D)等价于最大化 (V(D,G))。原始论文里把 (\max_D) 写出来就是在说D要最大化 (V)既然是最优化目标本身就没有把负号提前提出来。那个负号没消失它被最大化这个词吸收掉了——你要是站在损失越小越好的习惯角度去看负号就得显式写出来你要是站在目标函数越大越好的角度去看负号就藏在符号约定里。2.3 从最大化到最小化损失函数的两种写法这个视角切换在写代码时特别重要因为绝大多数深度学习框架天然是最小化损失的。所以你在PyTorch里写判别器的损失实际上写的是 (L_D)也就是带负号的那个版本import torch import torch.nn as nn bce nn.BCEWithLogitsLoss() # 真样本标签为1 real_logits D(real_imgs) loss_real bce(real_logits, torch.ones_like(real_logits)) # 假样本标签为0 fake_imgs G(z) fake_logits D(fake_imgs.detach()) loss_fake bce(fake_logits, torch.zeros_like(fake_logits)) loss_D loss_real loss_fake loss_D.backward()而生成器这边原始论文的写法是 (\min_G \mathbb{E}_{z}[\log(1 - D(G(z)))])但实践中大家几乎都不用这个原始形式而是改成最大化(\mathbb{E}_z[\log D(G(z))])也就是让D把假图判成真的得分越高越好。对应的代码就是fake_logits D(fake_imgs) loss_G bce(fake_logits, torch.ones_like(fake_logits)) loss_G.backward()这里有个训练上的原因值得说透在训练早期生成器造出来的图很烂D一眼就能识破此时 (D(G(z))) 接近0(\log(1 - D(G(z)))) 的梯度非常小几乎是平的G学得极慢。换成 (\log D(G(z))) 之后同样的早期阶段梯度反而很大G能被推着快速改进。这不是数学上的等价变换而是为了训练动力做的一次改写代价是理论上会带来梯度方差偏大的问题但实践里利大于弊。这个取舍我在后面讲WGAN时还会再提。