GAN 图像生成


文档摘要

GAN 图像生成 本节摘要:GAN 是两个神经网络在一场固定博弈里对峙:一个画图,一个挑刺,它们一起变强,直到画出的图骗过挑刺的。本节讲清生成器与判别器的极小极大博弈、为何均衡对应 、为什么不饱和损失取代了原始形式,并实现一个 DCGAN(非饱和损失 + 谱归一化 + TTUR 三大稳定技巧),在合成数据上 60 行内生成连贯的 32×32 图像。我们还会读训练曲线,区分健康收敛、模式崩溃、判别器完胜、振荡四种状态。读完本节,你能用 FID 评估生成质量,并知道 2026 年 GAN 仍在哪些场景(实时、风格迁移、精确 I2I)胜过扩散。 对应原课程:Phase 4 · Lesson 09 · (原英文 )。

GAN 图像生成

本节摘要:GAN 是两个神经网络在一场固定博弈里对峙:一个画图,一个挑刺,它们一起变强,直到画出的图骗过挑刺的。本节讲清生成器与判别器的极小极大博弈、为何均衡对应 p_model = p_data、为什么不饱和损失取代了原始形式,并实现一个 DCGAN(非饱和损失 + 谱归一化 + TTUR 三大稳定技巧),在合成数据上 60 行内生成连贯的 32×32 图像。我们还会读训练曲线,区分健康收敛、模式崩溃、判别器完胜、振荡四种状态。读完本节,你能用 FID 评估生成质量,并知道 2026 年 GAN 仍在哪些场景(实时、风格迁移、精确 I2I)胜过扩散。

对应原课程:Phase 4 · Lesson 09 · image-generation-gans(原英文 phases/04-computer-vision/09-image-generation-gans/docs/en.md)。

学习目标

阅读完本节,你应当能够:

  1. 解释生成器与判别器之间的极小极大博弈,为何均衡对应 p_model = p_data
  2. 在 PyTorch 中实现一个 DCGAN,在 60 行内生成连贯的 32×32 合成图像。
  3. 用三大标准技巧稳定 GAN 训练:非饱和损失、谱归一化、TTUR(双时间尺度更新)。
  4. 读训练曲线,区分健康收敛模式崩溃、振荡、判别器完胜

一、问题与直觉

分类教网络把图像映射到标签。生成反转这个问题:采样出看起来像来自同一分布的新图像。没有你可以逐像素 diff 的「正确」输出,只有一个你要模仿的分布。

标准损失函数(MSE、交叉熵)无法度量「这个样本是否来自真实分布」。最小化逐像素误差产生模糊的平均,而非逼真样本。突破在于学习损失:训练第二个网络,它的职责是分辨真假,用它的判断来推生成器。

GAN(Goodfellow 等,2014)定义了这个框架。到 2018 年 StyleGAN 已能产出与照片难辨的 1024×1024 人脸。扩散模型后来在质量与可控性上夺走王座,但让扩散可行的每一招——归一化选择、隐空间、特征损失——都是先在 GAN 上理解的。

两个网络

生成器 G 取噪声向量 z,输出一张图像。判别器 D 取一张图像,输出一个标量:这张图是真实的概率。

博弈

G 希望 D 错。D 希望自己对。形式上:

min_G max_D E_x[log D(x)] + E_z[log(1 - D(G(z)))]

从右往左读:D 在真(log D(real))和假(log(1 - D(fake)))图上最大化准确率。G 在假图上最小化 D 的准确率——它要 D(G(z)) 高。

Goodfellow 证明这个极小极大有一个全局均衡:p_G = p_data,D 处处输出 0.5,生成分布与真实分布之间的 Jensen-Shannon 散度为零。难的是走过去。

非饱和损失

上面这个形式数值不稳定。训练早期,每个假图的 D(G(z)) 都接近零,所以 log(1 - D(G(z))) 对 G 的梯度消失。修法:翻转 G 的损失。

L_D = -E_x[log D(x)] - E_z[log(1 - D(G(z)))] L_G = -E_z[log D(G(z))] # 非饱和

现在 D(G(z)) 接近零时,G 的损失大、梯度有信息。每个现代 GAN 都用这个变体。

DCGAN 架构规则

Radford、Metz、Chintala(2015)把多年失败实验蒸馏成让 GAN 训练稳定的五条规则:

  1. 用带步幅的卷积替代池化(两个网络都是)。
  2. 在生成器和判别器里都用批归一化,除了 G 的输出和 D 的输入。
  3. 更深的架构上去掉全连接层。
  4. G 除输出层外都用 ReLU(输出用 tanh,范围 [-1, 1])。
  5. D 所有层用 LeakyReLU(negative_slope=0.2)。

每个现代基于卷积的 GAN(StyleGAN、BigGAN、GigaGAN)仍从这些规则出发,一次替换一个零件。

失败模式及其特征

  • 模式崩溃:G 找到一张能骗过 D 的图,只产那一张。修法:加 minibatch discrimination、谱归一化或标签条件。
  • 判别器完胜:D 太快变强,G 的梯度消失。修法:更小的 D、更低的 D 学习率,或对真标签做标签平滑。
  • 振荡:两个网络互相拉锯,从不接近均衡。修法:TTUR(D 比 G 快 2-4 倍),或换 Wasserstein 损失。

评估

GAN 没有真值,怎么知道它在工作?

  • 样本检查 —— 每个 epoch 末就看 64 个样本。不容商量。
  • FID(Fréchet 起始距离) —— 真实集与生成集的 Inception-v3 特征分布之间的距离。越低越好。社区标准。
  • Inception Score —— 更老、更脆;优先用 FID。
  • 生成模型的 Precision/Recall —— 分别度量质量(precision)和覆盖(recall)。比单看 FID 更有信息。

对小合成数据运行,样本检查就够。

二、从零实现

步骤 1:生成器

一个小 DCGAN 生成器,取 64 维噪声,产 32×32 图像。

import torch import torch.nn as nn class Generator(nn.Module): def __init__(self, z_dim=64, img_channels=3, feat=64): super().__init__() self.net = nn.Sequential( nn.ConvTranspose2d(z_dim, feat * 4, kernel_size=4, stride=1, padding=0, bias=False), nn.BatchNorm2d(feat * 4), nn.ReLU(inplace=True), nn.ConvTranspose2d(feat * 4, feat * 2, kernel_size=4, stride=2, padding=1, bias=False), nn.BatchNorm2d(feat * 2), nn.ReLU(inplace=True), nn.ConvTranspose2d(feat * 2, feat, kernel_size=4, stride=2, padding=1, bias=False), nn.BatchNorm2d(feat), nn.ReLU(inplace=True), nn.ConvTranspose2d(feat, img_channels, kernel_size=4, stride=2, padding=1, bias=False), nn.Tanh(), ) def forward(self, z): return self.net(z.view(z.size(0), -1, 1, 1))

四个转置卷积,每个 kernel_size=4, stride=2, padding=1,干净地加倍空间尺寸。tanh 把输出限制在 [-1, 1]。

步骤 2:判别器

生成器的镜像。LeakyReLU、带步幅卷积,以一个标量 logit 结束。

class Discriminator(nn.Module): def __init__(self, img_channels=3, feat=64): super().__init__() self.net = nn.Sequential( nn.Conv2d(img_channels, feat, kernel_size=4, stride=2, padding=1), nn.LeakyReLU(0.2, inplace=True), nn.Conv2d(feat, feat * 2, kernel_size=4, stride=2, padding=1, bias=False), nn.BatchNorm2d(feat * 2), nn.LeakyReLU(0.2, inplace=True), nn.Conv2d(feat * 2, feat * 4, kernel_size=4, stride=2, padding=1, bias=False), nn.BatchNorm2d(feat * 4), nn.LeakyReLU(0.2, inplace=True), nn.Conv2d(feat * 4, 1, kernel_size=4, stride=1, padding=0), ) def forward(self, x): return self.net(x).view(-1)

最后一个卷积把 4×4 特征图降到 1×1。输出是每图一个标量;只在算损失时套 sigmoid。

步骤 3:训练步

交替:每批先更新 D,再更新 G。

import torch.nn.functional as F def train_step(G, D, real, z, opt_g, opt_d, device): real = real.to(device) bs = real.size(0) # D 步 opt_d.zero_grad() d_real = D(real) d_fake = D(G(z).detach()) loss_d = (F.binary_cross_entropy_with_logits(d_real, torch.ones_like(d_real)) + F.binary_cross_entropy_with_logits(d_fake, torch.zeros_like(d_fake))) loss_d.backward() opt_d.step() # G 步 opt_g.zero_grad() d_fake = D(G(z)) loss_g = F.binary_cross_entropy_with_logits(d_fake, torch.ones_like(d_fake)) loss_g.backward() opt_g.step() return loss_d.item(), loss_g.item()

D 步里的 G(z).detach() 至关重要:D 更新时不要把梯度流回 G。忘了这个是经典新手 bug。

步骤 4:合成形状上的完整训练循环

from torch.utils.data import DataLoader, TensorDataset import numpy as np def synthetic_images(num=2000, size=32, seed=0): rng = np.random.default_rng(seed) imgs = np.zeros((num, 3, size, size), dtype=np.float32) - 1.0 for i in range(num): r = rng.uniform(6, 12) cx, cy = rng.uniform(r, size - r, size=2) yy, xx = np.meshgrid(np.arange(size), np.arange(size), indexing="ij") mask = (xx - cx) ** 2 + (yy - cy) ** 2 < r ** 2 color = rng.uniform(-0.5, 1.0, size=3) for c in range(3): imgs[i, c][mask] = color[c] return torch.from_numpy(imgs) device = "cuda" if torch.cuda.is_available() else "cpu" data = synthetic_images() loader = DataLoader(TensorDataset(data), batch_size=64, shuffle=True) G = Generator(z_dim=64, img_channels=3, feat=32).to(device) D = Discriminator(img_channels=3, feat=32).to(device) opt_g = torch.optim.Adam(G.parameters(), lr=2e-4, betas=(0.5, 0.999)) opt_d = torch.optim.Adam(D.parameters(), lr=2e-4, betas=(0.5, 0.999)) for epoch in range(10): for (batch,) in loader: z = torch.randn(batch.size(0), 64, device=device) ld, lg = train_step(G, D, batch, z, opt_g, opt_d, device) print(f"epoch {epoch} D {ld:.3f} G {lg:.3f}")

Adam(lr=2e-4, betas=(0.5, 0.999)) 是 DCGAN 默认——低 beta1 防止动量项过度稳定对抗博弈。

步骤 5:采样

@torch.no_grad() def sample(G, n=16, z_dim=64, device="cpu"): G.eval() z = torch.randn(n, z_dim, device=device) imgs = G(z) imgs = (imgs + 1) / 2 return imgs.clamp(0, 1)

采样前永远切到 eval 模式。对 DCGAN 这很重要,因为要用 BN 的运行统计而非批次统计。

步骤 6:谱归一化

判别器里 BN 的即插即用替代,保证网络是 1-Lipschitz。修大多数「D 赢太狠」的失败。

from torch.nn.utils import spectral_norm def build_sn_discriminator(img_channels=3, feat=64): return nn.Sequential( spectral_norm(nn.Conv2d(img_channels, feat, 4, 2, 1)), nn.LeakyReLU(0.2, inplace=True), spectral_norm(nn.Conv2d(feat, feat * 2, 4, 2, 1)), nn.LeakyReLU(0.2, inplace=True), spectral_norm(nn.Conv2d(feat * 2, feat * 4, 4, 2, 1)), nn.LeakyReLU(0.2, inplace=True), spectral_norm(nn.Conv2d(feat * 4, 1, 4, 1, 0)), )

Discriminator 换成 build_sn_discriminator(),常常就不需要 TTUR 技巧了。谱归一化是你能加的最简单的单一鲁棒性升级。

三、框架对比

认真的生成任务,用预训练权重或转向扩散。两个标准库:

  • torch_fidelity 在你的生成器上算 FID / IS,无需写自定义评估代码。
  • pytorch-gan-zoo(旧)和 StudioGAN 提供 DCGAN、WGAN-GP、SN-GAN、StyleGAN、BigGAN 的测试过的实现。

2026 年,GAN 仍是以下场景的最佳选择:实时图像生成(延迟 <10 ms)、风格迁移、需要精确控制的图像到图像翻译(Pix2Pix、CycleGAN)。扩散在照片真实感和文本条件化上胜出。

四、可复用产物

本节产出两个可复用文件(位于原课程 outputs/):

  • prompt-gan-training-triage.md:一个提示词——读训练曲线描述,挑出失败模式(模式崩溃、D 完胜、振荡)并给出单一推荐修复。
  • skill-dcgan-scaffold.md:一个技能——从 z_dim、目标 image_sizenum_channels 写出一个 DCGAN 骨架,含训练循环和样本保存器。

五、练习

  1. (简单) 在上面的合成圆形数据集上训 DCGAN,每个 epoch 末存一张 16 样本的网格。第几个 epoch 起,生成的圆明显是圆?
  2. (中等) 把判别器的批归一化换成谱归一化。并排训两版。哪个收敛快?哪个跨三个种子方差低?
  3. (困难) 实现条件 DCGAN:把类标签同时喂给 G 和 D(G 里把 one-hot 拼到噪声上,D 里拼一个类嵌入通道)。在第 7 节的合成「圆 vs 方」数据集上训,通过用特定标签采样证明类条件有效。

本节要点回顾

  1. GAN 是两个网络的博弈——生成器画图骗判别器,判别器分真假,均衡时 p_G = p_data、D 处处 0.5。
  2. 极小极大——min_G max_D 对抗损失;JS 散度为零是均衡,难在走过去。
  3. 非饱和损失是必须——原始 log(1-D(G(z))) 早期梯度消失;改 -log D(G(z)),梯度才有信息。
  4. DCGAN 五规则——带步幅卷积代池化、BN(除 G 出/D 入)、去 FC、G 用 ReLU/tanh、D 用 LeakyReLU。
  5. 三大失败模式——模式崩溃(G 只产一类)、判别器完胜(G 梯度消失)、振荡(永不收敛)。
  6. 稳定三招——非饱和损失、谱归一化(保 1-Lipschitz)、TTUR(D 比 G 快 2-4 倍)。
  7. G(z).detach() 是关键——D 更新时切断对 G 的梯度,忘掉是经典 bug。
  8. DCGAN 默认——Adam(lr=2e-4, betas=(0.5, 0.999)),低 beta1 防动量过度稳定。
  9. 评估靠样本检查 + FID——Inception-v3 特征分布距离,越低越好;IS 已过时。
  10. 2026 年 GAN 仍占优的场景——实时(<10ms)、风格迁移、精确 I2I;扩散在真实感和文本条件上胜。

下一节,我们离开对抗博弈,进入扩散模型——讲清「加噪再去噪」为何在图像生成上超越了 GAN。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U