查看全课大纲(第 31 / 40 节)
- 1.从规则到学习:深度学习解决什么问题
- 2.表示学习与端到端训练
- 3.张量、计算图与自动微分
- 4.训练三要素:数据、模型、目标函数
- 5.从感知机到多层感知机
- 6.反向传播:手推一个两层网络
- 7.激活函数与初始化
- 8.优化器:SGD → Momentum → Adam
- 9.正则化:权重衰减、Dropout、BN/LN
- 10.卷积的直觉与参数共享
- 11.池化、感受野与下采样
- 12.经典网络演进:LeNet → AlexNet → VGG → ResNet
- 13.残差连接与归一化为什么有效
- 14.迁移学习与特征可视化
- 15.RNN 与 BPTT
- 16.LSTM/GRU:门控如何缓解梯度问题
- 17.Seq2Seq 与编码器-解码器
- 18.注意力前史:Bahdanau 加性注意力
- 19.Self-Attention:QKV 与缩放点积
- 20.多头注意力:并行的多种关系
- 21.位置编码:正弦与可学习
- 22.残差、LayerNorm 与前馈层
- 23.从零实现一个最小 Transformer block
- 24.训练稳定性、复杂度与常见误区
- 25.自监督与语言建模目标
- 26.BERT:掩码语言模型与双向编码
- 27.GPT:自回归生成与缩放定律
- 28.指令微调与对齐:SFT → RLHF/DPO
- 29.参数高效微调:LoRA 与适配器
- 30.自编码器与 VAE
- 31.GAN:对抗训练与模式崩塌
- 32.扩散模型:前向加噪与反向去噪
- 33.扩散模型的应用与采样加速
- 34.对比学习与 CLIP
- 35.ViT:把图像当序列
- 36.多模态架构:对齐、融合与生成
- 37.能力边界、幻觉与评测
- 38.PyTorch 2.x 训练循环与混合精度
- 39.实验管理、评测与复现
- 40.推理部署与成本:量化/蒸馏概览
GAN:对抗训练与模式崩塌
小象实战讲义 · 深度学习基础:从神经网络到 Transformer
上一节的 VAE 用“重建 + KL”把生成过程变成了概率建模。这一节换一条完全不同的路:不建模分布,而是让两个网络互相博弈——生成器造假,判别器抓假。学完你将能回答:GAN 的目标函数在优化什么?为什么它训练不稳、还会模式崩塌?WGAN 又是怎么用“移土距离”救场的?
先修:交叉熵与 KL 散度(第 6 章 6.4 节)、神经网络反向传播(第 1 章)、概率密度与期望。后续:下一节扩散模型,再往后进入第 8 章多模态。
💡 核心导读
- 对抗博弈:GAN由生成器(造假者)和判别器(警察)构成,通过极小极大博弈学习数据分布。
- 目标函数:原始GAN的目标是让生成器最小化判别器对其生成样本的判别准确率,等价于最小化真实分布与生成分布之间的JS散度。
- 训练难题:JS散度在高维空间易导致梯度消失,引发训练不稳定和模式崩塌(生成器只生成少数几种样本)。
- WGAN改进:用Wasserstein距离(移土距离)替代JS散度,即使两个分布没有重叠也能提供有效梯度,显著提升训练稳定性。
🧭 前置知识
- 神经网络的前向传播与反向传播(链式法则)。
- 概率分布的基本概念(概率密度函数、期望)。
- 对数函数与交叉熵损失。
- 矩阵乘法的形状规则。
生成式模型的挑战与GAN的提出
判别式模型学习条件概率 ;生成式模型学习 ,能从中采样出新数据。传统方法(如高斯混合模型)在高维上有两大难题:难以为图像假设合适的参数化分布形式,以及对复杂分布高效采样(如 MCMC)成本高。
生成对抗网络(Generative Adversarial Networks,GAN)不显式建模 ,而是用对抗博弈隐式地学分布(Goodfellow et al., 2014,arXiv:1406.2661)。
GAN的基本原理:极小极大博弈
GAN框架包含两个可微分的网络:
- 生成器(Generator, G):输入一个随机噪声向量 (通常从简单分布如均匀分布或高斯分布采样),输出一个合成数据样本 。目标是生成足以“以假乱真”的数据。
- 判别器(Discriminator, D):输入一个数据样本 (可能来自真实数据,也可能来自生成器),输出一个标量 ,表示 来自真实数据分布 的概率。目标是准确区分真假数据。
两者构成一个零和博弈。判别器 试图最大化它正确分类真假样本的能力,而生成器 试图最小化判别器识别出其生成样本的能力。这个博弈的值函数(Value Function) 定义为:
如何理解这个公式? 对 :真实样本希望 、生成样本希望 ;对 :希望 ,使 变小。
实际训练交替进行:先固定 ,用真实数据与生成数据更新 ;再固定 ,用 的打分更新 。
原始论文里,生成器的损失写为 ,但在训练初期, 能轻易识别出差的生成样本,导致该损失梯度很小。现代实现通常采用非饱和(non-saturating)损失,即让生成器最大化 ,这在训练初期能提供更强的梯度。
下面我们用极简的线性模型模拟一个单步的对抗更新过程,观察损失的变化。
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散度问题
理论上,当生成器和判别器都有无限容量时,这个博弈存在一个全局最优解:生成分布 等于真实数据分布 ,此时判别器对任何样本的输出都是 (完全无法区分)。
可以证明,固定生成器 时,最优判别器 为: 将 代入值函数 ,经过推导(见原论文),生成器的优化目标等价于最小化真实分布 与生成分布 之间的 Jensen-Shannon(JS)散度: 其中 是JS散度,它是KL散度的一种对称化版本。
JS散度的问题:它要求两个分布有“不可忽略的重叠”才有有效梯度;而高维中 与 常是低维流形、重叠测度近 0,JS 几乎恒为 、梯度为 0。后果有两条:判别器一强,生成器梯度就消失;即便能训,生成器也可能只覆盖少数模式,即模式崩塌(Mode Collapse)。
Wasserstein GAN:用“移土距离”解决梯度问题
为了克服JS散度的问题,Wasserstein GAN(WGAN)提出使用 Wasserstein距离(又称 Earth Mover’s Distance,移土距离)来衡量两个分布之间的差异(Arjovsky et al., 2017,arXiv:1701.07875)。
直观理解:把一堆土()搬成另一堆()所需的最小工作量(土量 × 移动距离)。好处是两分布不重叠时仍连续可微、能给出有意义梯度,且数值与样本质量的相关性更强。
Wasserstein距离的计算涉及一个对所有满足边际分布的联合分布的下确界,直接计算不可行。WGAN利用了对偶性,将其转化为一个 Lipschitz 连续函数 的上确界问题: 这里 是一个满足 1-Lipschitz 连续 的函数(即函数斜率绝对值不超过 1)。在WGAN中,我们用判别器网络 (去掉最后的Sigmoid激活,做回归任务)来近似这个函数 ,并通过约束其权重(如权重裁剪)来近似满足Lipschitz条件。
因此,WGAN的目标函数变为:
- 判别器损失: (最大化差值)
- 生成器损失: (最大化生成样本的判别器输出)
WGAN相比原始GAN的主要改动:
- 判别器最后一层去掉Sigmoid,输出一个分数(logits),不再是概率。
- 损失函数不取log。
- 每次更新判别器参数后,将其权重裁剪到固定范围 ,以强制Lipschitz约束。
- 推荐使用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的生成器损失中,如果使用 ,在训练初期 时梯度会很小。请用代码计算并比较在 和 时,该损失对判别器 logit(sigmoid 之前的输出)的梯度,并对比非饱和损失 。
👉 点击查看参考答案
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读法:(训练初期,生成样本一眼假)时,饱和损失的梯度只有 ,非饱和是 ——差了 9 倍,这就是早期 G 学不动的原因。反过来 时饱和损失梯度 、非饱和只剩 。所以实现里统一用非饱和损失 ,把“生成器最需要学习”的那一段梯度保住。
常见错误
| 错误理解或写法 | 现象或报错 | 原因 | 正确做法 |
|---|---|---|---|
| 训练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 损失的走势与样本多样性。 |
本章小结
- GAN通过生成器(G)和判别器(D)的对抗博弈来隐式学习数据分布,其目标是一个极小极大值问题。
- 原始GAN的优化目标等价于最小化真实分布与生成分布之间的JS散度,该散度在高维空间易导致梯度消失和训练不稳定。
- 模式崩塌是GAN训练中的典型问题,表现为生成器只产生有限种类的样本。
- Wasserstein GAN(WGAN) 使用Wasserstein距离替代JS散度,即使分布无重叠也能提供有效梯度,并通过权重裁剪来约束判别器的Lipschitz连续性,显著提升了训练稳定性。
- GAN的训练必须采用交替优化策略,固定一个网络,更新另一个。
📋 行动清单
- 运行本节第一个代码块,理解判别器损失和生成器损失的计算过程,并尝试修改
batch_size或data_dim,观察损失值的变化。 - 运行“动手练一练”的代码,直观感受原始GAN生成器损失在训练初期梯度小的问题。
- 查阅论文 Wasserstein GAN(Arjovsky et al., 2017),重点关注其第3节“Wasserstein GAN”中对Wasserstein距离的推导和算法1。
延伸阅读
- Generative Adversarial Networks(Goodfellow et al., 2014, arXiv:1406.2661)—— GAN的开山之作。
- Wasserstein GAN(Arjovsky et al., 2017, arXiv:1701.07875)—— 提出用Wasserstein距离解决GAN训练不稳定问题。
—— 小象教研组