生成对抗网络
这一章的主角是生成对抗网络(Generative Adversarial Network,GAN)——一种能"凭空造图"的模型,2014 年提出后火遍图像生成领域。它的思路很特别:让两个网络互相"打架"、共同进步,最后留下的那个网络,能把随机噪声变成以假乱真的数据。学完这一章,你会明白 GAN 的"造假 vs 打假"博弈是怎么回事,看得懂那个著名的 min-max 目标函数,也知道它为什么难训练,以及它怎么生成图片、给模糊照片"补像素"。
造假专家 vs 打假专家
先讲一个故事。有个造假币的人手艺越来越精,想做出足以乱真的钞票;银行里的验钞员则发誓识破每一张假币。两人互相较劲:验钞员越厉害,假币越难蒙混过关,逼得造假者改进工艺;造假者工艺越好,验钞员也必须练就更强的眼力。这样循环往复,直到有一天,验钞员对着真币假币完全分不出来——只能抛硬币瞎猜——那一刻,造假者就"出师"了。
造假币者就是生成器(Generator):把一堆随机噪声"印刷"成看起来像真实数据的样本;验钞员就是判别器(Discriminator):拿到一张图,输出一个概率,判断"这是真的还是假的"。两人对练的过程,就是 GAN 的训练过程。
正式一点:生成器记作 $g[z, \theta]$,输入是一段随机噪声 $z$(也叫潜变量,通常从标准正态分布里抽取),参数为 $\theta$,输出一张"假样本":
$$x^* = g[z, \theta]$$这个式子的意思是:让网络把"没啥意义的噪声"改写成"长得像真数据"的样本。判别器记作 $f[x, \phi]$,它把输入 $x$ 变成一个分数,再经过 sigmoid 函数压成"这玩意儿是真的"的概率。训练时给真样本打标签 $y=1$、假样本打标签 $y=0$,用我们熟悉的二元交叉熵来教判别器学会打假。
书里用了一个特别直观的一维例子:真实样本集中在某个位置,生成器 $g[z, \theta] = z + \theta$ 只是把噪声整体平移 $\theta$。训练中判别器学出一条 S 形曲线来分开两类样本,生成器则把 $\theta$ 往"更像真的"方向挪,直到两类样本混在一起、S 曲线变成一条平线——判别器彻底没辙。
零和博弈:min-max 目标函数
GAN 的训练目标是一个极小极大(min-max)博弈。把判别器简写为 $D$、生成器简写为 $G$(书中分别写作 $f$ 和 $g$),目标函数长这样:
$$\min_{G} \max_{D} \ \mathbb{E}_{x \sim p_{\text{data}}}[\log D(x)] + \mathbb{E}_{z \sim p(z)}[\log(1 - D(G(z)))]$$这个式子要拆开看。第一项里,$D(x)$ 是"判别器认为真图是真的"的概率,判别器希望它越大越好;第二项里,$D(G(z))$ 是"判别器认为假图是真的"的概率——判别器希望它越小越好,生成器却希望它越大越好(骗过判别器)。所以 $\max_D$ 是判别器的目标,$\min_G$ 是生成器的目标:一个要赢,一个要让它输,这就是一场零和博弈。
如果训练顺利,双方会达到一个均衡:生成器的输出分布与真实分布几乎重合,判别器对任何输入都只能给出 0.5——等于瞎猜(博弈论里这种谁都占不到便宜的僵局叫纳什均衡)。实践上我们并不直接优化上面这个大式子,而是把它拆成两个小损失,交替更新:先固定生成器、更新判别器让它更会打假,再固定判别器、更新生成器让它更会造假。
骨架代码:PyTorch 里的两个网络
下面用 PyTorch 搭一个最朴素的 GAN 骨架(面向 MNIST 手写数字,28×28 像素):生成器把 100 维噪声变成一张图,判别器把一张图压成一个真假概率。
import torch
import torch.nn as nn
class Generator(nn.Module):
"""造假者:把 100 维噪声 z 变成一张 28x28 的假图"""
def __init__(self):
super().__init__()
self.fc = nn.Linear(100, 128)
self.out = nn.Linear(128, 28 * 28)
def forward(self, z):
x = torch.relu(self.fc(z))
return torch.tanh(self.out(x)) # 像素值压到 [-1, 1]
class Discriminator(nn.Module):
"""验钞员:输入一张图,输出"它是真的"的概率"""
def __init__(self):
super().__init__()
self.fc = nn.Linear(28 * 28, 128)
self.out = nn.Linear(128, 1)
def forward(self, x):
h = torch.relu(self.fc(x))
return torch.sigmoid(self.out(h)) # sigmoid 压成 (0, 1)
# 训练时两步交替:更新判别器(真图→1、假图→0),再更新生成器(骗过判别器)
训练为什么这么难?
GAN 的概念很简单,训练却出了名的"玄学"。两个最经典的翻车姿势:
- 模式坍塌(Mode Collapse):生成器发现某一种"必胜"的输出——比如同一张脸——能稳定骗过判别器,于是干脆偷懒,把随机噪声 $z$ 全变成这一个模式。生成出来的样本千篇一律,多样性归零。
- 梯度消失:如果判别器练得太强、能把真假完美分开,生成器收到的梯度信号就变得极小,怎么改参数都不影响分数,训练直接停摆。因为假样本和真样本在空间里几乎不重叠,而 GAN 的损失本质上是在衡量两个分布的"重叠程度"——不重叠时,这个量没法给生成器指路。
GAN 能干什么?
把噪声变成图片只是起点,判别器这个"裁判"还能用在更多地方:
- 图像生成:DCGAN、StyleGAN 等能生成以假乱真的人脸与场景;StyleGAN 还能把脸型、发型、肤色等不同尺度的特征分开控制。
- 超分辨率(SRGAN):给模糊的小图"脑补"高清细节。判别器当"画质裁判",逼着放大网络输出让裁判分不清真假的高清图。
- 图像翻译:给灰度图着色、把照片变成名画风格(CycleGAN 连配对图片都不需要)。
- 条件生成:在输入端额外加一个属性向量 $c$,就能指定生成"金发""戴眼镜"等特定特征的人脸。