查看全课大纲(第 31 / 40 节)
  1. 1.从规则到学习:深度学习解决什么问题
  2. 2.表示学习与端到端训练
  3. 3.张量、计算图与自动微分
  4. 4.训练三要素:数据、模型、目标函数
  5. 5.从感知机到多层感知机
  6. 6.反向传播:手推一个两层网络
  7. 7.激活函数与初始化
  8. 8.优化器:SGD → Momentum → Adam
  9. 9.正则化:权重衰减、Dropout、BN/LN
  10. 10.卷积的直觉与参数共享
  11. 11.池化、感受野与下采样
  12. 12.经典网络演进:LeNet → AlexNet → VGG → ResNet
  13. 13.残差连接与归一化为什么有效
  14. 14.迁移学习与特征可视化
  15. 15.RNN 与 BPTT
  16. 16.LSTM/GRU:门控如何缓解梯度问题
  17. 17.Seq2Seq 与编码器-解码器
  18. 18.注意力前史:Bahdanau 加性注意力
  19. 19.Self-Attention:QKV 与缩放点积
  20. 20.多头注意力:并行的多种关系
  21. 21.位置编码:正弦与可学习
  22. 22.残差、LayerNorm 与前馈层
  23. 23.从零实现一个最小 Transformer block
  24. 24.训练稳定性、复杂度与常见误区
  25. 25.自监督与语言建模目标
  26. 26.BERT:掩码语言模型与双向编码
  27. 27.GPT:自回归生成与缩放定律
  28. 28.指令微调与对齐:SFT → RLHF/DPO
  29. 29.参数高效微调:LoRA 与适配器
  30. 30.自编码器与 VAE
  31. 31.GAN:对抗训练与模式崩塌
  32. 32.扩散模型:前向加噪与反向去噪
  33. 33.扩散模型的应用与采样加速
  34. 34.对比学习与 CLIP
  35. 35.ViT:把图像当序列
  36. 36.多模态架构:对齐、融合与生成
  37. 37.能力边界、幻觉与评测
  38. 38.PyTorch 2.x 训练循环与混合精度
  39. 39.实验管理、评测与复现
  40. 40.推理部署与成本:量化/蒸馏概览

GAN:对抗训练与模式崩塌

小象实战讲义 · 深度学习基础:从神经网络到 Transformer

上一节的 VAE 用“重建 + KL”把生成过程变成了概率建模。这一节换一条完全不同的路:不建模分布,而是让两个网络互相博弈——生成器造假,判别器抓假。学完你将能回答:GAN 的目标函数在优化什么?为什么它训练不稳、还会模式崩塌?WGAN 又是怎么用“移土距离”救场的?

先修:交叉熵与 KL 散度(第 6 章 6.4 节)、神经网络反向传播(第 1 章)、概率密度与期望。后续:下一节扩散模型,再往后进入第 8 章多模态。

💡 核心导读

  1. 对抗博弈:GAN由生成器(造假者)和判别器(警察)构成,通过极小极大博弈学习数据分布。
  2. 目标函数:原始GAN的目标是让生成器最小化判别器对其生成样本的判别准确率,等价于最小化真实分布与生成分布之间的JS散度。
  3. 训练难题:JS散度在高维空间易导致梯度消失,引发训练不稳定和模式崩塌(生成器只生成少数几种样本)。
  4. WGAN改进:用Wasserstein距离(移土距离)替代JS散度,即使两个分布没有重叠也能提供有效梯度,显著提升训练稳定性。

🧭 前置知识

  • 神经网络的前向传播与反向传播(链式法则)。
  • 概率分布的基本概念(概率密度函数、期望)。
  • 对数函数与交叉熵损失。
  • 矩阵乘法的形状规则。

生成式模型的挑战与GAN的提出

判别式模型学习条件概率 P(Y∣X)P(Y|X);生成式模型学习 P(X)P(X),能从中采样出新数据。传统方法(如高斯混合模型)在高维上有两大难题:难以为图像假设合适的参数化分布形式,以及对复杂分布高效采样(如 MCMC)成本高。

生成对抗网络(Generative Adversarial Networks,GAN)不显式建模 P(X)P(X),而是用对抗博弈隐式地学分布(Goodfellow et al., 2014,arXiv:1406.2661)。

GAN的基本原理:极小极大博弈

GAN框架包含两个可微分的网络:

  • 生成器(Generator, G):输入一个随机噪声向量 zz(通常从简单分布如均匀分布或高斯分布采样),输出一个合成数据样本 G(z)G(z)。目标是生成足以“以假乱真”的数据。
  • 判别器(Discriminator, D):输入一个数据样本 xx(可能来自真实数据,也可能来自生成器),输出一个标量 D(x)D(x),表示 xx 来自真实数据分布 pdatap_{\text{data}} 的概率。目标是准确区分真假数据。

两者构成一个零和博弈。判别器 DD 试图最大化它正确分类真假样本的能力,而生成器 GG 试图最小化判别器识别出其生成样本的能力。这个博弈的值函数(Value Function) 定义为:

min⁡Gmax⁡DV(D,G)=Ex∼pdata(x)[log⁡D(x)]+Ez∼pz(z)[log⁡(1−D(G(z)))]\min_G \max_D V(D, G) = \mathbb{E}_{x \sim p_{\text{data}}(x)}[\log D(x)] + \mathbb{E}_{z \sim p_z(z)}[\log(1 - D(G(z)))]

如何理解这个公式? 对 DD:真实样本希望 D(x)→1D(x)\to 1、生成样本希望 D(G(z))→0D(G(z))\to 0;对 GG:希望 D(G(z))→1D(G(z))\to 1,使 log⁡(1−D(G(z)))\log(1-D(G(z))) 变小。

实际训练交替进行:先固定 GG,用真实数据与生成数据更新 DD;再固定 DD,用 DD 的打分更新 GG。

原始论文里,生成器的损失写为 log⁡(1−D(G(z)))\log(1 - D(G(z))),但在训练初期,DD 能轻易识别出差的生成样本,导致该损失梯度很小。现代实现通常采用非饱和(non-saturating)损失,即让生成器最大化 log⁡D(G(z))\log D(G(z)),这在训练初期能提供更强的梯度。

下面我们用极简的线性模型模拟一个单步的对抗更新过程,观察损失的变化。

import torch
import torch.nn as nn

torch.manual_seed(0)

# 使用简单的线性模型模拟GAN的核心更新步骤
batch_size = 2
data_dim = 3
z_dim = 5

# 模拟真实数据
real_data = torch.randn(batch_size, data_dim)
# 模拟噪声输入
noise = torch.randn(batch_size, z_dim)

# 极简的生成器和判别器(均为单层线性网络)
G = nn.Linear(z_dim, data_dim, bias=False)
D = nn.Linear(data_dim, 1, bias=False)

# 1. 生成器前向:生成假数据
fake_data = G(noise)

# 2. 判别器前向:对真实和假数据打分
d_real = D(real_data)
d_fake = D(fake_data.detach())  # 训练D时,断开G的梯度

# 3. 计算判别器损失(最大化 log(D(real)) + log(1-D(fake)))
#    等价于最小化负的上述和
d_loss = - (torch.log(torch.sigmoid(d_real)).mean() + torch.log(1 - torch.sigmoid(d_fake)).mean())
print(f"判别器损失 (D loss): {d_loss.item():.4f}")

# 4. 计算生成器损失(最小化 log(1-D(fake)) 或 最大化 log(D(fake)))
#    这里采用非饱和损失:最大化 log(D(fake))
g_loss = - torch.log(torch.sigmoid(D(fake_data))).mean()  # 注意这里fake_data梯度是连通的
print(f"生成器损失 (G loss, non-saturating): {g_loss.item():.4f}")

# 5. 模拟一次梯度更新(这里不实际更新参数,只展示计算)
#    实际训练中,会分别对d_loss和g_loss进行反向传播,并交替更新D和G的参数。
print("\n模拟更新:")
print("  1. 固定G,用d_loss反向传播更新D的参数。")
print("  2. 固定D,用g_loss反向传播更新G的参数。")
print("  3. 重复步骤1和2,直至收敛。")
判别器损失 (D loss): 1.1555
生成器损失 (G loss, non-saturating): 0.6795

模拟更新:
  1. 固定G,用d_loss反向传播更新D的参数。
  2. 固定D,用g_loss反向传播更新G的参数。
  3. 重复步骤1和2,直至收敛。

理论最优解与JS散度问题

理论上,当生成器和判别器都有无限容量时,这个博弈存在一个全局最优解:生成分布 pgp_g 等于真实数据分布 pdatap_{\text{data}},此时判别器对任何样本的输出都是 D∗(x)=12D^*(x) = \frac{1}{2}(完全无法区分)。

可以证明,固定生成器 GG 时,最优判别器 DG∗(x)D^*_G(x) 为: DG∗(x)=pdata(x)pdata(x)+pg(x)D^*_G(x) = \frac{p_{\text{data}}(x)}{p_{\text{data}}(x) + p_g(x)} 将 DG∗(x)D^*_G(x) 代入值函数 V(D,G)V(D, G),经过推导(见原论文),生成器的优化目标等价于最小化真实分布 pdatap_{\text{data}} 与生成分布 pgp_g 之间的 Jensen-Shannon(JS)散度: C(G)=−log⁡(4)+2⋅JSD(pdata∥pg)C(G) = -\log(4) + 2 \cdot JSD(p_{\text{data}} \parallel p_g) 其中 JSDJSD 是JS散度,它是KL散度的一种对称化版本。

JS散度的问题:它要求两个分布有“不可忽略的重叠”才有有效梯度;而高维中 pdatap_{\text{data}} 与 pgp_g 常是低维流形、重叠测度近 0,JS 几乎恒为 log⁡2\log 2、梯度为 0。后果有两条:判别器一强,生成器梯度就消失;即便能训,生成器也可能只覆盖少数模式,即模式崩塌(Mode Collapse)。

Wasserstein GAN:用“移土距离”解决梯度问题

为了克服JS散度的问题,Wasserstein GAN(WGAN)提出使用 Wasserstein距离(又称 Earth Mover’s Distance,移土距离)来衡量两个分布之间的差异(Arjovsky et al., 2017,arXiv:1701.07875)。

直观理解:把一堆土(PP)搬成另一堆(QQ)所需的最小工作量(土量 × 移动距离)。好处是两分布不重叠时仍连续可微、能给出有意义梯度,且数值与样本质量的相关性更强。

Wasserstein距离的计算涉及一个对所有满足边际分布的联合分布的下确界,直接计算不可行。WGAN利用了对偶性,将其转化为一个 Lipschitz 连续函数 的上确界问题: W(pdata,pg)=sup⁡∥f∥L≤1[Ex∼pdata[f(x)]−Ez∼pz[f(G(z))]]W(p_{\text{data}}, p_g) = \sup_{\lVert f \rVert_L \leq 1} \left[ \mathbb{E}_{x \sim p_{\text{data}}}[f(x)] - \mathbb{E}_{z \sim p_z}[f(G(z))] \right] 这里 ff 是一个满足 1-Lipschitz 连续 的函数(即函数斜率绝对值不超过 1)。在WGAN中,我们用判别器网络 DwD_w(去掉最后的Sigmoid激活,做回归任务)来近似这个函数 ff,并通过约束其权重(如权重裁剪)来近似满足Lipschitz条件。

因此,WGAN的目标函数变为:

  • 判别器损失:LD=−[Ex∼pdata[Dw(x)]−Ez∼pz[Dw(G(z))]]L_D = -\left[ \mathbb{E}_{x \sim p_{\text{data}}}[D_w(x)] - \mathbb{E}_{z \sim p_z}[D_w(G(z))] \right] (最大化差值)
  • 生成器损失:LG=−Ez∼pz[Dw(G(z))]L_G = -\mathbb{E}_{z \sim p_z}[D_w(G(z))] (最大化生成样本的判别器输出)

WGAN相比原始GAN的主要改动:

  1. 判别器最后一层去掉Sigmoid,输出一个分数(logits),不再是概率。
  2. 损失函数不取log。
  3. 每次更新判别器参数后,将其权重裁剪到固定范围 [−c,c][-c, c],以强制Lipschitz约束。
  4. 推荐使用RMSProp或SGD优化器,而非基于动量的Adam(与权重裁剪配合更稳定)。

下面我们模拟一个简化的WGAN损失计算,注意判别器输出是未归一化的分数。

import torch
import torch.nn as nn

torch.manual_seed(0)

# 模拟数据
batch_size = 2
data_dim = 4
z_dim = 8

real_data = torch.randn(batch_size, data_dim)
noise = torch.randn(batch_size, z_dim)

# 简单的线性网络作为生成器和判别器
G_simple = nn.Linear(z_dim, data_dim, bias=False)
D_w = nn.Linear(data_dim, 1, bias=False)  # WGAN的判别器,无Sigmoid

# 生成假数据
fake_data_w = G_simple(noise)

# WGAN判别器前向
d_real_w = D_w(real_data)
d_fake_w = D_w(fake_data_w.detach())  # 训练D时断开G的梯度

# WGAN判别器损失:最大化 E[D(real)] - E[D(fake)]
wasserstein_distance = d_real_w.mean() - d_fake_w.mean()
d_loss_w = - wasserstein_distance  # 梯度下降最小化负值
print(f"Wasserstein距离估计值: {wasserstein_distance.item():.4f}")
print(f"WGAN判别器损失 (需最小化): {d_loss_w.item():.4f}")

# WGAN生成器损失:最大化 E[D(fake)] 等价于最小化 -E[D(fake)]
g_loss_w = - D_w(fake_data_w).mean()
print(f"WGAN生成器损失 (需最小化): {g_loss_w.item():.4f}")

# 模拟权重裁剪(关键步骤)
clip_value = 0.01
for p in D_w.parameters():
    p.data.clamp_(-clip_value, clip_value)
print(f"\n模拟判别器权重裁剪到范围 [{-clip_value}, {clip_value}]。")
Wasserstein距离估计值: 0.1415
WGAN判别器损失 (需最小化): -0.1415
WGAN生成器损失 (需最小化): -0.0390

模拟判别器权重裁剪到范围 [-0.01, 0.01]。

📝 动手练一练

问题:在原始GAN的生成器损失中,如果使用 log⁡(1−D(G(z)))\log(1 - D(G(z))),在训练初期 D(G(z))≈0D(G(z)) \approx 0 时梯度会很小。请用代码计算并比较在 D(G(z))=0.1D(G(z)) = 0.1 和 D(G(z))=0.9D(G(z)) = 0.9 时,该损失对判别器 logit(sigmoid 之前的输出)的梯度,并对比非饱和损失 −log⁡D(G(z))-\log D(G(z))。

👉 点击查看参考答案
import torch

torch.manual_seed(0)

# D 是 sigmoid 的输出,真正传回生成器的是“对 logit 的梯度”
def grads(d_value):
    d = torch.tensor(d_value, dtype=torch.float64)
    logit = torch.log(d / (1 - d))            # 由 D 反解 logit
    logit = logit.clone().requires_grad_(True)
    dd = torch.sigmoid(logit)

    l_sat = torch.log(1 - dd)                 # 原始(饱和)损失
    l_sat.backward(retain_graph=True)
    g_sat = logit.grad.item()
    logit.grad = None

    l_non = -torch.log(dd)                    # 非饱和损失
    l_non.backward()
    g_non = logit.grad.item()
    return g_sat, g_non

for d in (0.1, 0.9):
    g_sat, g_non = grads(d)
    print(f"D={d}: 饱和损失 dL/dlogit={g_sat:.2f}|非饱和 dL/dlogit={g_non:.2f}")
D=0.1: 饱和损失 dL/dlogit=-0.10|非饱和 dL/dlogit=-0.90
D=0.9: 饱和损失 dL/dlogit=-0.90|非饱和 dL/dlogit=-0.10

读法:D=0.1D=0.1(训练初期,生成样本一眼假)时,饱和损失的梯度只有 −0.10-0.10,非饱和是 −0.90-0.90——差了 9 倍,这就是早期 G 学不动的原因。反过来 D=0.9D=0.9 时饱和损失梯度 −0.90-0.90、非饱和只剩 −0.10-0.10。所以实现里统一用非饱和损失 log⁡D(G(z))\log D(G(z)),把“生成器最需要学习”的那一段梯度保住。

常见错误

错误理解或写法现象或报错原因正确做法
训练GAN时同时更新G和D的参数训练不稳定,损失震荡剧烈,无法收敛。这破坏了对抗博弈的交替优化过程。生成器和判别器的目标相互冲突,同时更新会导致梯度方向抵消。采用交替训练:固定G更新D若干步,再固定D更新G一步。
在WGAN中忘记对判别器进行权重裁剪或使用梯度惩罚训练仍然不稳定,Wasserstein距离估计不准确,甚至发散。缺少对判别器函数Lipschitz连续性的约束,无法保证其近似Wasserstein距离的上确界。在WGAN中,每次更新D后执行权重裁剪 p.data.clamp_(-c, c)。或使用WGAN-GP(梯度惩罚)来施加更平滑的约束。
将模式崩塌简单理解为过拟合生成器只能生成非常相似的少数样本,多样性极差。过拟合是模型过度记忆训练数据。模式崩塌是生成器找到了一个能“欺骗”当前判别器的局部最优解,主动放弃了其他数据模式。使用改进的GAN变种(如WGAN)、在损失中加入多样性惩罚项、或采用多判别器、历史平均等技术。
只用判别器准确率(D accuracy)当训练指标准确率看着稳定(常在 50% 上下),但生成样本质量毫无改善。G 与 D 交替优化,D 的准确率没有绝对含义:D 变强准确率升、G 追赶后回落,50% 既可能是“完美平衡”也可能是“G 压根没学”。看生成样本本身,或用 FID 这类与样本分布相关的指标;训练早期更应关注 G 损失的走势与样本多样性。

本章小结

  1. GAN通过生成器(G)和判别器(D)的对抗博弈来隐式学习数据分布,其目标是一个极小极大值问题。
  2. 原始GAN的优化目标等价于最小化真实分布与生成分布之间的JS散度,该散度在高维空间易导致梯度消失和训练不稳定。
  3. 模式崩塌是GAN训练中的典型问题,表现为生成器只产生有限种类的样本。
  4. Wasserstein GAN(WGAN) 使用Wasserstein距离替代JS散度,即使分布无重叠也能提供有效梯度,并通过权重裁剪来约束判别器的Lipschitz连续性,显著提升了训练稳定性。
  5. GAN的训练必须采用交替优化策略,固定一个网络,更新另一个。

📋 行动清单

  • 运行本节第一个代码块,理解判别器损失和生成器损失的计算过程,并尝试修改 batch_size 或 data_dim,观察损失值的变化。
  • 运行“动手练一练”的代码,直观感受原始GAN生成器损失在训练初期梯度小的问题。
  • 查阅论文 Wasserstein GAN(Arjovsky et al., 2017),重点关注其第3节“Wasserstein GAN”中对Wasserstein距离的推导和算法1。

延伸阅读

  1. Generative Adversarial Networks(Goodfellow et al., 2014, arXiv:1406.2661)—— GAN的开山之作。
  2. Wasserstein GAN(Arjovsky et al., 2017, arXiv:1701.07875)—— 提出用Wasserstein距离解决GAN训练不稳定问题。

—— 小象教研组