13 生成对抗网络
本章从极小极大博弈出发,经过 LSGAN 的最小二乘损失、WGAN 的 Wasserstein 距离与 Lipschitz 约束,最后落到 AnimeFace 头像的 DCGAN/WGAN-GP 实现。下面 8 道题按四类编排:
- 概念与推导(练习 1–3):最优判别器与 JS 散度;LSGAN 的损失、梯度与参数;Wasserstein 距离、对偶形式与 1-Lipschitz 约束。
- 编程实践(练习 4–6):从零实现 LSGAN 并做梯度/噪声诊断;实现 WGAN-GP 的梯度惩罚;核对 DCGAN 的形状与参数量。
- 开放思考(练习 7):模式坍塌、FID/IS、GAN 与 VAE/扩散模型的取舍,以及条件生成与 CycleGAN。
- 纠错与陷阱(练习 8):LSGAN 的 \(1/2\)、
detach()、Critic 中的 BatchNorm、梯度惩罚的batch_size、训练/评估模式切换。
建议用时约 3 小时。练习 1–3 可以在读正文时完成;练习 4、5 建议对照 code/13_LSGAN.R、code/13_WGAN_Clipping.R、code/13_WGAN_GP.R 逐行阅读;练习 6 需要手算一遍卷积形状。答案中的数值在 R 4.6.1、torch 0.17.0 下核对过;GAN 训练对随机种子、设备和超参数很敏感,答案里的数字用于判断量级和趋势,不应当作唯一标准。
一、概念与推导
练习 1:最优判别器、JS 散度与梯度消失
设真实分布为 \(p_d\),生成分布为 \(p_g\),判别器 \(D(x)\in(0,1)\)。GAN 的极小极大目标是
\[ V(D,G)=\mathbb E_{x\sim p_d}\bigl[\log D(x)\bigr] +\mathbb E_{z\sim p_z}\bigl[\log\bigl(1-D(G(z))\bigr)\bigr]. \]
- (a) 固定 \(G\)(从而固定 \(p_g\)),逐点对 \(D(x)\) 求导,证明最优判别器为 \[ D^*(x)=\frac{p_d(x)}{p_d(x)+p_g(x)}. \]
- (b) 令 \(m=(p_d+p_g)/2\),证明 \[ V(D^*,G)=-\log 4+2\,\mathrm{JS}(p_d\,\|\,p_g), \] 其中 \(\mathrm{JS}(p\|q)=\frac12\mathrm{KL}(p\|m)+\frac12\mathrm{KL}(q\|m)\)。
- (c) 当 \(p_g=p_d\) 时,\(D^*\)、\(\mathrm{JS}\) 和 \(V\) 各是多少?为什么均衡点的值不是 \(0\)?
- (d) 若 \(p_d\) 与 \(p_g\) 的支撑集不相交,证明 \(\mathrm{JS}=\log 2\)、\(V(D^*,G)=0\),并解释此时生成器参数 \(\theta_G\) 的梯度为什么几乎为 \(0\)。
- (e) 取 \(p_d=\mathcal N(0,1)\)、\(p_g=\mathcal N(\delta,1)\),用数值积分填写下表,并解释“\(V\) 趋近于 \(0\)”为什么不是好消息。
| \(\delta\) | 0 | 1 | 2 | 4 | 6 | 8 | 10 |
|---|---|---|---|---|---|---|---|
| \(\mathrm{JS}\) | |||||||
| \(V\) |
(a) 对固定的 \(x\),目标中被 \(D(x)\) 影响的只有
\[ p_d(x)\log D+p_g(x)\log(1-D). \]
对 \(D\) 求导并令其为零:
\[ \frac{p_d}{D}-\frac{p_g}{1-D}=0 \;\Longrightarrow\; p_d(1-D)=p_gD \;\Longrightarrow\; D^*=\frac{p_d}{p_d+p_g}. \]
当 \(p_d+p_g=0\) 时该点几乎不会出现,\(D\) 可任取;当 \(p_d=p_g=0\) 时 \(D^*\) 无定义,但不影响积分。
(b) 由
\[ \frac{p_d}{p_d+p_g}=\frac12\cdot\frac{p_d}{m}, \qquad \frac{p_g}{p_d+p_g}=\frac12\cdot\frac{p_g}{m}, \]
代入 \(V(D^*,G)\):
\[ \begin{aligned} V(D^*,G) &=\int p_d\log\frac{p_d}{p_d+p_g}+p_g\log\frac{p_g}{p_d+p_g}\,dx\\ &=\int p_d\log\frac{p_d}{m}+p_g\log\frac{p_g}{m}\,dx -\log 2\int(p_d+p_g)\,dx\\ &=2\,\mathrm{JS}(p_d\|p_g)-2\log 2\\ &=-\log 4+2\,\mathrm{JS}(p_d\|p_g). \end{aligned} \]
最后一步用了 \(\int p_d=\int p_g=1\),所以 \(\int(p_d+p_g)=2\),且 \(2\log2=\log4\)。由于 \(\mathrm{JS}\ge0\),有 \(V\ge-\log4\)。
(c) 当 \(p_g=p_d\) 时,\(D^*=p_d/(2p_d)=1/2\);\(m=p_d\),两个 KL 都为 \(0\),所以 \(\mathrm{JS}=0\);于是
\[ V(D^*,G)=-\log4\approx-1.386294. \]
均衡点的值不是 \(0\),因为 \(V\) 是“判别器给真实样本的对数似然 + 给假样本的对数似然”的期望:当 \(D=1/2\) 时,每一项都是 \(\log(1/2)=-\log2\),两项加起来正好是 \(-\log4\)。\(V=0\) 对应的是判别器完美分开真假(见下一问),不是均衡。
(d) 设 \(p_d\) 的支撑集为 \(A\)、\(p_g\) 的支撑集为 \(B\),且 \(A\cap B=\varnothing\)。则几乎处处
\[ D^*(x)= \begin{cases} 1, & x\in A,\\ 0, & x\in B. \end{cases} \]
于是
\[ V(D^*,G)=\int_A p_d\log1\,dx+\int_B p_g\log1\,dx=0. \]
另一方面,在 \(A\) 上 \(m=p_d/2\),故 \(\mathrm{KL}(p_d\|m)=\log2\);在 \(B\) 上 \(m=p_g/2\),故 \(\mathrm{KL}(p_g\|m)=\log2\)。因此
\[ \mathrm{JS}=\frac12\log2+\frac12\log2=\log2, \]
与 \(V=-\log4+2\log2=0\) 一致。
此时,只要 \(G\) 的参数发生一个小扰动后 \(p_g\) 的支撑集仍然远离 \(A\),\(D^*\) 就仍然是同一个“示性函数”,\(V\) 恒等于 \(0\)。因此 \(V\) 对 \(\theta_G\) 的导数在支撑集之间的开区域内恒为 \(0\)。用参数化判别器时,若最后一层是 Sigmoid,假样本上的 \(D(G(z))\approx0\),Sigmoid 进入饱和区,\(\partial D/\partial\theta_G\) 也趋于 \(0\),于是
\[ \frac{\partial}{\partial\theta_G}\log\bigl(1-D(G(z))\bigr) =-\frac{1}{1-D}\frac{\partial D}{\partial\theta_G} \]
仍然趋于 \(0\)。这就是“判别器太强 → 生成器梯度消失”的根源。
顺带一提:实践中常用的非饱和损失 \(-\log D(G(z))\) 在 \(D\approx0\) 时梯度约为 \(-1/D\cdot\partial D/\partial\theta_G\),反而可能很大;它避开了这个饱和点,但也会带来数值不稳定。本章的极小极大形式用的是 \(\log(1-D)\),所以饱和问题成立。
(e) 数值积分代码(用 log-sum-exp 避免尾部下溢):
js <- function(d, n = 4e6) {
lo <- min(-12, d - 12); hi <- max(12, d + 12)
x <- seq(lo, hi, length.out = n)
lp <- dnorm(x, 0, 1, log = TRUE)
lq <- dnorm(x, d, 1, log = TRUE)
mx <- pmax(lp, lq)
lm <- mx + log(exp(lp - mx) + exp(lq - mx)) - log(2)
p <- exp(lp); q <- exp(lq); m <- exp(lm)
sum(0.5 * p * log(p / m) + 0.5 * q * log(q / m)) * (x[2] - x[1])
}
d <- c(0, 1, 2, 4, 6, 8, 10)
data.frame(delta = d,
JS = sapply(d, js),
V = -log(4) + 2 * sapply(d, js))参考结果:
| \(\delta\) | 0 | 1 | 2 | 4 | 6 | 8 | 10 |
|---|---|---|---|---|---|---|---|
| \(\mathrm{JS}\) | 0.000000 | 0.111421 | 0.336831 | 0.632720 | 0.689298 | 0.693054 | 0.693146 |
| \(V\) | -1.386294 | -1.163452 | -0.712632 | -0.120854 | -0.007698 | -0.000186 | -0.000002 |
\(\mathrm{JS}\) 的上界是 \(\log2\approx0.693147\)。当 \(\delta\ge4\) 时,两个正态分布的支撑集已经几乎没有重叠,\(\mathrm{JS}\) 基本饱和,\(V\) 从下方趋近于 \(0\)。但 \(V\) 趋近于 \(0\) 意味着判别器几乎完美:假样本上的 \(D(G(z))\approx0\),生成器损失 \(\log(1-D(G(z)))\approx0\),曲线是平的,梯度几乎为 \(0\)。所以“\(V\) 变大(更接近 \(0\))”并不表示生成器在进步,恰恰表示它收不到有效信号。WGAN 用 Wasserstein 距离替换 JS,正是为了在这种情况下仍然给出与距离成正比的梯度(见练习 3)。
练习 2:LSGAN 的损失、梯度与参数
LSGAN 用最小二乘代替交叉熵。记 \(a\) 为假样本标签、\(b\) 为真样本标签、\(c\) 为生成器希望假样本得到的分数。判别器和生成器的目标分别是
\[ L_D=\frac12\mathbb E_{x\sim p_d}\bigl[(D(x)-b)^2\bigr] +\frac12\mathbb E_{z\sim p_z}\bigl[(D(G(z))-a)^2\bigr], \]
\[ L_G=\frac12\mathbb E_{z\sim p_z}\bigl[(D(G(z))-c)^2\bigr]. \]
- (a) 固定 \(G\),逐点对 \(D(x)\) 求导,证明 \[ D^*(x)=\frac{b\,p_d(x)+a\,p_g(x)}{p_d(x)+p_g(x)}. \] 本章采用 \(a=0\)、\(b=1\)、\(c=1\)。此时 \(D^*\) 是什么?和标准 GAN 的最优判别器相同吗?
- (b) 本章的判别器最后一层没有 Sigmoid。假设有人保留 Sigmoid,即 \(D=\sigma(z)\),分别计算下面两种生成器损失对 logit \(z\) 的梯度,并填写表格: \[ L_G^{\text{GAN}}=\log(1-\sigma(z)), \qquad L_G^{\text{LSGAN}}=(\sigma(z)-1)^2. \] 再对比“线性输出”的 LSGAN:\(L_G=(D-1)^2\),在 \(D=0\) 和 \(D=-10\) 处对 \(D\) 的梯度是多少?说明“去掉 Sigmoid”和“换成 MSE”哪一个是避免梯度消失的关键。
- (c) 本章 1D 玩具模型的生成器是
nn_linear(5,64) → LeakyReLU(0.2) → nn_linear(64,1),判别器是nn_linear(1,64) → LeakyReLU(0.2) → nn_linear(64,1)。分别计算两个网络的参数量(含偏置)。 - (d) 正文公式带 \(1/2\),但
code/13_LSGAN.R的loss_d、loss_g都没有乘 \(1/2\)。这对训练有影响吗?如果优化器从 RMSprop 换成 SGD,结论会变吗? - (e) 如果把生成器目标改成 \(c=0\)(即“让假样本的得分接近 0”),会发生什么?在 \(a=0,b=1\) 的编码下,正确的 \(c\) 应该是多少?
(a) 对固定的 \(x\),只保留含 \(D(x)\) 的项:
\[ \ell(D)=\frac12 p_d(x)(D-b)^2+\frac12 p_g(x)(D-a)^2. \]
令 \(\ell'(D)=0\):
\[ p_d(D-b)+p_g(D-a)=0 \;\Longrightarrow\; D(p_d+p_g)=b\,p_d+a\,p_g, \]
所以
\[ D^*=\frac{b\,p_d+a\,p_g}{p_d+p_g}. \]
取 \(a=0,b=1\),得
\[ D^*=\frac{p_d}{p_d+p_g}, \]
与标准 GAN 的最优判别器完全相同。区别不在 \(D^*\),而在把 \(D^*\) 代回生成器目标后得到的散度不同。
(b) 记 \(s=\sigma(z)\),则 \(s'=s(1-s)\)。
- 标准 GAN 的生成器损失 \(L=\log(1-s)\):
\[ \frac{dL}{dz}=\frac{-s'}{1-s}=\frac{-s(1-s)}{1-s}=-s. \]
- 保留 Sigmoid 的 LSGAN:\(L=(s-1)^2\),
\[ \frac{dL}{dz}=2(s-1)s'=-2s(1-s)^2. \]
数值如下(负号表示梯度方向指向 \(z\) 增大):
| \(z\) | \(s=\sigma(z)\) | GAN:\(d\log(1-s)/dz\) | LSGAN+Sigmoid:\(d(s-1)^2/dz\) |
|---|---|---|---|
| -10 | \(4.540\times10^{-5}\) | \(-4.540\times10^{-5}\) | \(-9.079\times10^{-5}\) |
| -20 | \(2.061\times10^{-9}\) | \(-2.061\times10^{-9}\) | \(-4.122\times10^{-9}\) |
| -40 | \(4.248\times10^{-18}\) | \(-4.248\times10^{-18}\) | \(-8.497\times10^{-18}\) |
两者都随 \(z\to-\infty\) 指数衰减;在很远的区域,MSE 的梯度只是 GAN 的约两倍,仍然是 \(0\) 的量级。所以仅仅把 BCE 换成 MSE、但保留 Sigmoid,并不能解决饱和。
真正起作用的是把最后一层改成线性输出。此时 \(L=(D-1)^2\),
\[ \frac{dL}{dD}=2(D-1). \]
在 \(D=0\) 处梯度为 \(-2\);在 \(D=-10\) 处梯度为 \(-22\)。假样本离真实区域越远、\(D\) 越负,梯度反而越大,不会饱和。换句话说:
- “MSE”改变的是损失的形状,让远离目标的样本受到更大的惩罚;
- “去掉 Sigmoid”改变的是输出是否饱和,让 \(D\) 可以取任意实数。
两者配合才有 LSGAN 的效果;只做前者,梯度仍然会被 Sigmoid 压扁。
(c)
生成器:
nn_linear(5, 64):\(5\times64+64=384\);nn_linear(64, 1):\(64\times1+1=65\);
总计 \(384+65=\mathbf{449}\)。
判别器:
nn_linear(1, 64):\(1\times64+64=128\);nn_linear(64, 1):\(64\times1+1=65\);
总计 \(128+65=\mathbf{193}\)。
可用下面的代码核对:
sum(sapply(netG$parameters, function(p) p$numel())) # 449
sum(sapply(netD$parameters, function(p) p$numel())) # 193(d) 设原始损失为 \(L\)。把 \(L\) 换成 \(L/2\),梯度也变成 \(g/2\)。
- SGD:参数更新为 \(-\eta g/2\),等价于把学习率减半。会改变训练轨迹,需要重新调学习率。
- RMSprop:更新方向大致是 \[ \frac{g/2}{\sqrt{v/4+\epsilon}} =\frac{g}{\sqrt{v+4\epsilon}}, \] 其中 \(v\) 是梯度平方的滑动平均。忽略 \(\epsilon\) 时,\(1/2\) 被分子和分母同时抵消,更新几乎不变;只有 \(\epsilon\) 的相对作用变大,影响很小。
- Adam 同理,主要影响 \(\epsilon\) 附近的数值行为。
本章脚本用的是 RMSprop,所以漏写 \(1/2\) 在数值上几乎无害;但它与正文公式不一致,读者换成 SGD 时会得到“有效学习率减半”的不同结果。严谨的做法是让公式与代码统一,或者在代码旁注明“这里省略了不影响优化方向的常数因子 \(1/2\)”。
(e) 若 \(a=0,b=1,c=0\),生成器最小化
\[ \mathbb E\bigl[D(G(z))^2\bigr], \]
也就是把假样本的判别器输出往 \(0\) 推。但判别器本身也在把假样本往 \(0\) 推(\(a=0\)),两者的目标变成“合作”而非对抗:生成器被奖励去产生更容易被识破的样本。训练会失去对抗信号,生成样本可能漂移到判别器打分低的区域(通常是数据支撑集之外),表现为发散或坍塌。
在 \(a=0,b=1\) 的 0/1 编码下,生成器应当把假样本推向真实标签,即 \(c=b=1\)。如果采用 LSGAN 论文的另一套编码 \(a=-1,b=1,c=0\),则 \(c=0\) 是正确的,因为此时假样本标签是 \(-1\),生成器要把假样本从 \(-1\) 推向 \(0\),仍然是“向真实侧移动”。判断标准不是“\(c\) 是否等于 \(0\)”,而是“\(c\) 是否在 \(a\) 与 \(b\) 之间、且靠近 \(b\)”。
练习 3:Wasserstein 距离、对偶形式与 Lipschitz 约束
- (a) 写出 \(W_1(P,Q)\) 的定义,并证明一维情形下 \[ W_1(P,Q)=\int_{-\infty}^{\infty}\bigl|F_P(t)-F_Q(t)\bigr|\,dt, \] 其中 \(F_P,F_Q\) 是累积分布函数。
- (b) 写出 Kantorovich–Rubinstein 对偶: \[ W_1(P,Q)=\sup_{\|f\|_L\le1}\Bigl(\mathbb E_P[f]-\mathbb E_Q[f]\Bigr). \] 解释为什么必须有“\(1\)-Lipschitz”约束;如果没有,会发生什么?
- (c) 设 \(P=\mathcal N(\mu_1,1)\)、\(Q=\mathcal N(\mu_2,1)\),\(\delta=|\mu_1-\mu_2|\)。用对偶形式和“平移耦合”证明 \(W_1=\delta\)。填写下表,并与练习 1 的 JS 对比:
| \(\delta\) | 0 | 1 | 2 | 4 | 6 | 8 | 10 |
|---|---|---|---|---|---|---|---|
| \(W_1\) | |||||||
| \(\mathrm{JS}\) |
- (d) 若两个分布均值相同,但标准差分别为 \(1\) 和 \(1.1\),\(W_1\) 是多少?\(W_2\) 是多少?这说明“均值相同”是否意味着 \(W_1=0\)?
- (e) 比较 WGAN 的两种 Lipschitz 实现:权重剪裁与梯度惩罚。写出梯度惩罚的公式,并解释本章实测的“\(17.6\%\) 权重恰好在边界、\(82.4\%\) 在内部”以及“梯度范数均值 \(0.918\)、中位数 \(1.125\)”各说明了什么。
(a) \(W_1\) 的定义是
\[ W_1(P,Q)=\inf_{\gamma\in\Pi(P,Q)} \mathbb E_{(X,Y)\sim\gamma}\bigl[|X-Y|\bigr], \]
其中 \(\Pi(P,Q)\) 是所有边缘分布为 \(P\) 和 \(Q\) 的联合分布。
一维公式的证明用恒等式
\[ |x-y|=\int_{-\infty}^{\infty} \bigl|\mathbf 1\{x>t\}-\mathbf 1\{y>t\}\bigr|\,dt. \]
对任意耦合 \(\gamma\) 取期望:
\[ \mathbb E_\gamma|x-y| =\int_{-\infty}^{\infty} \mathbb E_\gamma\bigl|\mathbf 1\{X>t\}-\mathbf 1\{Y>t\}\bigr|\,dt \ge\int_{-\infty}^{\infty} \bigl|P(X>t)-P(Y>t)\bigr|\,dt. \]
把 \(P(X>t)=1-F_P(t)\)、\(P(Y>t)=1-F_Q(t)\) 代入,两个 \(1\) 相消,得到下界 \(\int|F_P-F_Q|dt\)。等号可以由单调耦合(分位数耦合)达到:令 \(X=F_P^{-1}(U)\)、\(Y=F_Q^{-1}(U)\),同一个 \(U\) 同时驱动两个变量,则两个示性函数在 \(t\) 的每个区域同号,绝对值可以移到积分号外。因此
\[ W_1(P,Q)=\int|F_P(t)-F_Q(t)|\,dt. \]
(b) Kantorovich–Rubinstein 对偶是
\[ W_1(P,Q)=\sup_{f\in\mathrm{Lip}_1} \Bigl(\mathbb E_{x\sim P}[f(x)]-\mathbb E_{y\sim Q}[f(y)]\Bigr), \]
其中 \(\mathrm{Lip}_1=\{f:|f(x)-f(y)|\le|x-y|\}\)。
如果没有 Lipschitz 约束,取 \(f(x)=c\,x\),则 \(\mathbb E_P[f]-\mathbb E_Q[f]=c(\mu_P-\mu_Q)\)。只要两个均值不同,令 \(c\to\infty\) 就得到 \(+\infty\);上确界没有意义。约束 \(\|f\|_L\le1\) 的作用是给“评分函数能有多陡”设上限,使得“把真实样本评高、假样本评低”的收益不会无限增长。WGAN 用神经网络参数化 \(f\)(Critic),再通过权重剪裁、梯度惩罚或谱归一化来近似这个约束。
(c) 取 \(f(x)=x\)。它是 \(1\)-Lipschitz 的(斜率为 \(1\)),所以由对偶形式
\[ W_1(P,Q)\ge\mathbb E_P[X]-\mathbb E_Q[Y]=\mu_1-\mu_2=\delta \]
(不妨设 \(\mu_1>\mu_2\);若相反则取绝对值)。
另一方面,构造耦合:令 \(X\sim P\),取 \(Y=X-\delta\),则 \(Y\sim\mathcal N(\mu_2,1)=Q\),且
\[ \mathbb E|X-Y|=\delta. \]
由 \(W_1\) 的定义(下确界)有 \(W_1\le\delta\)。上下界相同,故 \(W_1=\delta\)。
数值表(\(W_1\) 是精确值;JS 用练习 1 的数值积分):
| \(\delta\) | 0 | 1 | 2 | 4 | 6 | 8 | 10 |
|---|---|---|---|---|---|---|---|
| \(W_1\) | 0 | 1 | 2 | 4 | 6 | 8 | 10 |
| \(\mathrm{JS}\) | 0.000000 | 0.111421 | 0.336831 | 0.632720 | 0.689298 | 0.693054 | 0.693146 |
\(W_1\) 随 \(\delta\) 线性增长,而 JS 很快饱和在 \(\log2\)。这就是 WGAN 在“支撑集几乎不重叠”时仍能提供有效梯度的原因:距离还在变大,梯度不会消失。
(d) 用数值积分
\[ W_1=\int_{-\infty}^{\infty} \bigl|\Phi(x)-\Phi(x/1.1)\bigr|\,dx \approx 0.079788, \]
其中 \(\Phi\) 是标准正态 CDF。\(W_2\) 对同均值、方差 \(\sigma_1^2,\sigma_2^2\) 的高斯有简单形式
\[ W_2^2=(\mu_1-\mu_2)^2+(\sigma_1-\sigma_2)^2, \]
所以这里 \(W_2=|1-1.1|=0.1\)。
\(W_1\ne0\),因为两个分布的形状不同:标准差 \(1.1\) 的分布更宽,CDF 在两侧有系统性的差异。均值相同只保证一阶矩相同,不保证分布相同。一般地 \(W_1\le W_2\),所以 \(0.0798<0.1\) 是合理的;\(W_2\) 对尾部/方差差异更敏感,\(W_1\) 更“稳健”但也更不敏感。
(e) 权重剪裁:每次更新后把 Critic 的每个参数限制在 \([-c,c]\),本章 \(c=0.01\)。它简单,但有两个问题:
- \(c\) 太小会让 Critic 容量不足,很多权重被钉在边界,网络无法表达复杂的评分函数;
- \(c\) 太大则 Lipschitz 约束太弱,Wasserstein 估计不可靠。
本章 10,000 步后的实测分布:约 10.9% 的权重恰好等于 \(+0.01\),6.7% 恰好等于 \(-0.01\),合计 17.6% 在边界;其余 82.4% 严格在内部;\(|w|\) 的中位数约 0.00895,离边界很近。说明剪裁确实在起作用,而且大部分权重都被压到边界附近,Critic 的表达能力受到明显限制;但并非“所有参数都等于 \(\pm c\)”,仍有约五分之一的参数在内部。
梯度惩罚(WGAN-GP):
\[ L_D=\mathbb E_{x\sim p_g}\bigl[C(x)\bigr] -\mathbb E_{x\sim p_d}\bigl[C(x)\bigr] +\lambda\,\mathbb E_{\hat x} \left[\bigl(\|\nabla_{\hat x}C(\hat x)\|_2-1\bigr)^2\right], \]
其中
\[ \hat x=\alpha x+(1-\alpha)G(z),\qquad \alpha\sim U(0,1). \]
它不是把每个权重钉在某个范围,而是要求 Critic 在真实样本与假样本连线上的梯度范数接近 \(1\)。这样既能约束 Lipschitz 常数,又不会硬性限制容量。
本章 1D 玩具模型(\(\lambda=0.1\),10,000 步)在最后一步的 512 个插值样本上实测:梯度范数均值 0.918,中位数 1.125,未乘 \(\lambda\) 的惩罚项约 0.016。均值小于 \(1\)、中位数大于 \(1\),说明分布是偏斜的:超过一半的插值点梯度范数大于 \(1\),但少数非常小的范数把均值拉低了。惩罚项只有 \(0.016\),说明大部分点的 \((\|\nabla C\|-1)^2\) 很小,约束是“软”的,不是精确的 \(1\)-Lipschitz。这也提醒我们:GP 不是把 Critic 变成严格的 \(1\)-Lipschitz 函数,而是用罚项鼓励它接近;\(\lambda\) 太大或太小都会改变训练行为(见练习 5)。
二、编程实践
练习 4:从零实现 LSGAN,并做三个诊断
下面的骨架与 code/13_LSGAN.R 一致:真实数据为 \(\mathcal N(2,1)\),噪声为 5 维均匀分布,生成器和判别器都是“线性 → LeakyReLU → 线性”,判别器没有 Sigmoid。
library(torch)
set.seed(123); torch_manual_seed(123)
get_real_data <- function(n) torch_randn(n, 1) * 1 + 2
Generator <- nn_module(
initialize = function() {
self$net <- nn_sequential(
nn_linear(5, 64), nn_leaky_relu(0.2), nn_linear(64, 1))
},
forward = function(x) self$net(x)
)
Discriminator <- nn_module(
initialize = function() {
self$net <- nn_sequential(
nn_linear(1, 64), nn_leaky_relu(0.2), nn_linear(64, 1))
},
forward = function(x) self$net(x)
)- (a) 补全训练循环:每个 step 先更新判别器,再更新生成器;判别器的假样本分支必须
detach();两个优化器都要在backward()前zero_grad()。用nn_mse_loss(),真/假标签分别为 \(1\) 和 \(0\),生成器目标为 \(1\)。 - (b) 训练 10,000 步,在第 1、1000、2500、5000、7500、10000 步各采样 5,000 个生成样本,记录均值和标准差。与下面的参考表比较,描述“先学均值、再学方差”的三阶段现象。
- (c) 故意去掉判别器假样本分支的
$detach(),运行到第一次backward()。记录报错并解释:为什么判别器的反向传播会破坏生成器的计算图? - (d) 故意注释掉判别器的
optD$zero_grad()(生成器保持正常),训练 3,000 步,比较生成样本的均值和标准差与正确版本的差异。解释梯度累加为什么会让结果偏离。 - (e) 用均匀噪声
torch_rand训练完成后,分别用torch_rand和torch_randn各采样 5,000 个点,记录均值和标准差。解释为什么同一个生成器换一种噪声会“坏掉”。
参考表(seed 123,10,000 步,每步 batch 512,采样 5,000 个点):
| step | mean | sd |
|---|---|---|
| 1 | -0.135726 | 0.054292 |
| 1000 | 1.446206 | 0.250509 |
| 2500 | 2.115996 | 0.421501 |
| 5000 | 2.064334 | 1.040605 |
| 7500 | 2.052377 | 1.018640 |
| 10000 | 1.994246 | 0.977059 |
(a) 完整循环:
netG <- Generator()
netD <- Discriminator()
optG <- optim_rmsprop(netG$parameters, lr = 5e-5)
optD <- optim_rmsprop(netD$parameters, lr = 5e-5)
criterion <- nn_mse_loss()
batch_size <- 512
capture <- c(1, 1000, 2500, 5000, 7500, 10000)
record <- list()
for (step in 1:10000) {
real_data <- get_real_data(batch_size)
fake_data <- netG(torch_rand(batch_size, 5))
# --- 判别器 ---
loss_d <- criterion(netD(real_data), torch_ones(batch_size, 1)) +
criterion(netD(fake_data$detach()), torch_zeros(batch_size, 1))
optD$zero_grad()
loss_d$backward()
optD$step()
# --- 生成器 ---
loss_g <- criterion(netD(fake_data), torch_ones(batch_size, 1))
optG$zero_grad()
loss_g$backward()
optG$step()
if (step %in% capture) {
with_no_grad({
v <- as.numeric(netG(torch_rand(5000, 5)))
record[[as.character(step)]] <- c(mean = mean(v), sd = sd(v))
})
}
}
do.call(rbind, record)关键点:
fake_data同时用于 D 和 G。D 的分支写fake_data$detach(),G 的分支写fake_data,两者共享同一组生成样本但梯度图不同。- D 的
backward()只能看到 D 的参数和detach()后的假样本;G 的backward()才穿过 G 的计算图。 - 两个
zero_grad()必须分别调用:PyTorch 默认把新梯度累加到.grad上,不会自动清零。
(b) 参考输出就是题干中的表。三个阶段与正文描述一致:
- step 1:均值约 \(-0.14\)、标准差约 \(0.054\),生成器刚初始化,输出挤在 \(0\) 附近;
- step 1000–2500:均值迅速移动到 \(1.45\to2.12\),但标准差只有 \(0.25\to0.42\)。生成器先学会“数据在 \(x=2\) 附近”,即一阶矩,输出仍然是一条很窄的线;
- step 2500–10000:均值在 \(2\) 附近小幅波动,标准差从 \(0.42\) 增长到约 \(0.98\),分布逐渐变宽,接近真实分布 \(\mathcal N(2,1)\)。
这说明 GAN 的优化并不是同时匹配所有矩:判别器最容易提供“位置错了”的信号,所以均值先收敛;方差/形状的匹配更慢,需要更长的训练。到 10,000 步标准差仍略小于 \(1\)(\(0.977\)),说明还没有完全收敛。
(c) 去掉 $detach() 后,第一次 D 的反向传播会沿着
\[ \text{loss}_D \to D(\text{fake}) \to \text{fake} \to G \]
一直传到生成器,并在过程中释放(free)计算图中的中间张量。等接着算 G 的损失并调用 loss_g$backward() 时,它需要再次穿过同一张图,就会报:
RuntimeError: Trying to backward through the graph a second time
(or directly access saved tensors after they have already been freed).
detach() 的作用正是把假样本从 G 的计算图上“摘下来”,让 D 的反向传播只更新 D,不碰 G,也不释放 G 需要的中间结果。注意:即使没有报错(例如某些框架会保留图),让 D 的梯度流进 G 也是概念错误——判别器更新时不应该修改生成器的参数。
(d) 注释掉 optD$zero_grad() 后,D 的梯度会在 step 之间累加:
\[ g_D^{(t)}=\sum_{s=1}^{t}\nabla_\theta L_D^{(s)} \]
(严格说还受优化器状态影响)。于是 D 的更新方向变成“历史梯度的和”,而不是当前 batch 的梯度;RMSprop 的平方滑动平均也会被历史梯度污染。实测在 3,000 步时:
- 正确版本:均值约 2.241,标准差约 0.538;
- 漏写
zero_grad():均值约 1.765,标准差约 0.246。
漏写版本看起来也在训练,但均值偏小、方差明显不足,分布更窄。更隐蔽的是:如果只漏掉 G 的 zero_grad(),G 的梯度会累加,更新步长和方向都会漂移,但损失曲线未必立刻异常。所以“训练能跑”不等于“梯度正确”。
(e) 实测(同一个训练好的生成器,采样 5,000 个点):
- 用训练时的
torch_rand(5000, 5)(均匀分布):均值约 2.016,标准差约 1.011; - 用
torch_randn(5000, 5)(标准正态):均值约 -1.307,标准差约 2.860。
(与参考表中的 1.994/0.977 略有差异,因为这里重新抽了一组 5,000 个噪声;量级和结论不变。)
生成器学到的映射是 \(G: [0,1)^5\to\mathbb R\)。训练时它只见过 \([0,1)^5\) 里的点;正态噪声会给出负数、大于 \(1\) 的数,以及各维组合上的新区域,属于分布外输入。神经网络在训练分布外没有理由给出合理输出,所以生成样本整体偏移、方差失控。
这不是“生成器坏了”,而是模型定义不一致:生成模型是
\[ p_g=(G)_\#p_z, \]
噪声分布 \(p_z\) 是模型的一部分。训练用 \(p_z\)、采样也必须用同一个 \(p_z\);如果希望部署时用正态噪声,就应该训练时也用正态噪声(或至少做相应的重参数化)。正文的 LSGAN 用均匀噪声、WGAN 的代码片段却建议 randn,正是容易混淆的地方(见进一步练习第 2 条)。
练习 5:实现 WGAN-GP 的梯度惩罚
WGAN-GP 的 Critic 损失为
\[ L_D=\mathbb E_{x\sim p_g}\bigl[C(x)\bigr] -\mathbb E_{x\sim p_d}\bigl[C(x)\bigr] +\lambda\,\mathbb E_{\hat x} \left[\bigl(\|\nabla_{\hat x}C(\hat x)\|_2-1\bigr)^2\right], \qquad \hat x=\alpha x+(1-\alpha)G(z). \]
- (a) 实现
calc_gradient_penalty(netC, real, fake, lambda),同时适用于 1D 数据[B,1]和图像[B,3,64,64]。说明alpha的形状为什么要写成c(B, 1, ..., 1)。 - (b) 解释
autograd_grad(..., create_graph = TRUE, retain_graph = TRUE)的两个参数。如果create_graph = FALSE,惩罚项还会训练 Critic 吗?如果retain_graph = FALSE,可能报什么错? - (c)
code/13_WGAN_GP.R在alpha_dims和gradients$view(c(batch_size, -1))里用了全局batch_size。这会在什么情况下出错?给出修正。 - (d) 在同一 1D 设置(seed 123,RMSprop,lr \(5\times10^{-4}\),
n_critic = 5,batch 512)下,比较 \(\lambda=0.1\) 与 \(\lambda=10\) 训练 5,000 步的生成样本均值和标准差。参考结果:\(\lambda=0.1\) 为 \((1.7792, 0.9479)\),\(\lambda=10\) 为 \((-1.2346, 0.5451)\)。解释为什么“论文默认 \(\lambda=10\)”不能直接照搬到这个小模型。 - (e) 在 \(\lambda=0.1\) 的最后一步,统计 512 个插值样本的梯度范数:均值约 \(0.918\)、中位数约 \(1.125\),未乘 \(\lambda\) 的惩罚项约 \(0.016\)。解释这三个数字为什么不矛盾,以及它们说明 GP 是“软约束”。
(a) 一个通用实现:
calc_gradient_penalty <- function(netC, real, fake, lambda = 0.1) {
B <- real$size(1)
# real 的形状是 [B, ...];alpha 需要变成 [B, 1, ..., 1]
alpha_dims <- c(B, rep(1, real$dim() - 1))
alpha <- torch_rand(alpha_dims, device = real$device)
x_hat <- (alpha * real + (1 - alpha) * fake)$requires_grad_(TRUE)
score <- netC(x_hat)
grad <- autograd_grad(
outputs = score,
inputs = x_hat,
grad_outputs = torch_ones_like(score),
create_graph = TRUE,
retain_graph = TRUE
)[[1]]
grad <- grad$view(c(B, -1)) # [B, 1] 或 [B, 3*64*64]
norm <- grad$norm(2, dim = 2) # 每个样本一个范数
gp <- ((norm - 1)^2)$mean()
lambda * gp
}alpha 的形状必须是 [B, 1, 1, ..., 1],这样才能和 [B, C, H, W] 的 real/fake 广播:每个样本一个标量 \(\alpha_i\),同一个 \(\alpha_i\) 广播到该样本的所有通道和像素。如果写成 [B],R torch 会把它当成最后一维,和 [B,C,H,W] 的最后一维 \(W\) 对齐(或直接报错),语义就错了。
grad$view(c(B, -1)) 把每个样本的梯度展平成一个向量,再沿第 2 维求 L2 范数,得到 \([B]\) 的逐样本范数。GP 是“逐样本”的,不能先对所有元素求一个全局范数。
(b)
create_graph = TRUE:在求梯度时同时建立“梯度的计算图”。这样gp仍然是netC参数的函数,d_loss$backward()才能把梯度继续传回 Critic,惩罚项才会真正参与训练。若为FALSE,grad只是一个常数张量(requires_grad = FALSE),gp对参数没有梯度;d_loss$backward()仍可能正常运行(因为d_loss_original还有梯度),但惩罚项不起作用,Critic 不会被推向 \(1\)-Lipschitz。retain_graph = TRUE:autograd_grad内部已经做过一次反向传播;保留前向图,后续对d_loss的backward()才能再次使用它。若为FALSE,可能在第二次反向时遇到 “Trying to backward through the graph a second time” 或访问已释放张量的错误。实践中两个都设为TRUE,代价是显存/内存更高。
(c) 全局 batch_size 假设每个 batch 都恰好是 512。以下情况会破坏这个假设:
drop_last = FALSE时,最后一个 batch 可能小于 512;- 训练结束后用 5,000 个样本做评估/画图,仍然调用同一个
calc_gradient_penalty; - 换数据或改 batch 大小时忘了同步修改全局变量。
此时 alpha_dims 仍是 c(512, 1),x_hat 的 batch 维却是 \(B'\ne512\),广播会失败;grad$view(c(512, -1)) 也会因为元素数不是 512 的整数倍而报 shape '[512, -1]' is invalid for input of size ...。修正很简单:所有形状都从输入张量读取,
B <- real$size(1)
alpha_dims <- c(B, rep(1, real$dim() - 1))
grad <- grad$view(c(B, -1))本章的 DCGAN 脚本已经用了 real_samples$size(1),而 1D 脚本仍然用全局 batch_size;后者只适合“固定 batch、从不在别处调用”的窄场景。
(d) 参考结果(5,000 步):
| \(\lambda\) | mean | sd |
|---|---|---|
| 0.1 | 1.7792 | 0.9479 |
| 10 | -1.2346 | 0.5451 |
\(\lambda=0.1\) 时生成分布已经在 \(2\) 附近,标准差接近 \(1\);\(\lambda=10\) 时均值跑到 \(-1.23\),标准差只有 \(0.55\),训练明显失败。
原因不是“\(\lambda=10\) 一定错”,而是惩罚的相对强度取决于模型、数据尺度、优化器和学习率。在这个 1D 玩具模型里:
- Critic 很小(1→64→1),容量有限;
- 数据是尺度为 \(1\) 的一维正态,Wasserstein 信号本身很小;
- RMSprop 的学习率 \(5\times10^{-4}\) 相对较大。
把 \(\lambda\) 提高到 \(10\) 后,梯度惩罚项在总损失中占主导,Critic 被强烈地推向“梯度范数处处为 \(1\)”,它的评分差异(真正给生成器提供方向的 Wasserstein 信号)被压扁。生成器收到的梯度变弱且噪声大,于是偏离真实分布。\(\lambda=0.1\) 让两项平衡,训练才稳定。
WGAN-GP 论文的 \(\lambda=10\) 是针对它的图像实验(Adam,lr \(10^{-4}\),Critic 容量更大)调的默认值,不是普适常数。实际使用时应把 \(\lambda\) 当作需要验证的超参数,观察 Wasserstein 估计、梯度范数和样本质量,而不是盲抄。
(e) 三个数字并不矛盾:
- 中位数 \(1.125>1\):超过一半的插值样本,梯度范数大于 \(1\);
- 均值 \(0.918<1\):分布右偏/左尾很长,少数非常小的范数把均值拉低;
- 惩罚项 \(0.016\):它是 \((\|\nabla C\|-1)^2\) 的平均,很多点接近 \(1\),平方后更小;即使少数点偏离较大,平均值仍可以很小。
这说明 GP 是软约束:它最小化的是“偏离 \(1\) 的平方的期望”,而不是硬性要求每个点的范数都等于 \(1\)。在 ReLU 网络中,Critic 是分段线性函数,梯度在每个线性区域内部是常数,在区域边界会跳变;插值样本落在不同区域,范数自然有高有低。只要平均偏离不大,GP 就认为“够好了”。如果希望更强约束,可以增大 \(\lambda\)、增加 Critic 容量或改用谱归一化;但过强的约束会压制 Wasserstein 信号(见上一问)。
练习 6:DCGAN 的形状、参数量与设计选择
本章的 AnimeFace 生成器(\(n_z=100\),ngf=64)为:
nn_sequential(
nn_conv_transpose2d(nz, ngf * 8, 4, 1, 0, bias = FALSE),
nn_batch_norm2d(ngf * 8), nn_relu(TRUE),
nn_conv_transpose2d(ngf * 8, ngf * 4, 4, 2, 1, bias = FALSE),
nn_batch_norm2d(ngf * 4), nn_relu(TRUE),
nn_conv_transpose2d(ngf * 4, ngf * 2, 4, 2, 1, bias = FALSE),
nn_batch_norm2d(ngf * 2), nn_relu(TRUE),
nn_conv_transpose2d(ngf * 2, ngf, 4, 2, 1, bias = FALSE),
nn_batch_norm2d(ngf), nn_relu(TRUE),
nn_conv_transpose2d(ngf, 3, 4, 2, 1, bias = FALSE),
nn_tanh()
)判别器为:
nn_sequential(
nn_conv2d(3, ndf, 4, 2, 1, bias = FALSE), nn_leaky_relu(0.2, inplace = TRUE),
nn_conv2d(ndf, ndf * 2, 4, 2, 1, bias = FALSE), nn_leaky_relu(0.2, inplace = TRUE),
nn_conv2d(ndf * 2, ndf * 4, 4, 2, 1, bias = FALSE), nn_leaky_relu(0.2, inplace = TRUE),
nn_conv2d(ndf * 4, ndf * 8, 4, 2, 1, bias = FALSE), nn_leaky_relu(0.2, inplace = TRUE),
nn_conv2d(ndf * 8, 1, 4, 1, 0, bias = FALSE)
)- (a) 写出卷积与转置卷积的输出尺寸公式,填写生成器和判别器每一层的输出形状(\(N\) 为 batch 维)。
- (b) 计算
ngf = ndf = 64时生成器和判别器的参数量;再计算ngf = ndf = 32时的参数量。解释为什么宽度减半后参数量大约变成 \(1/4\),而不是 \(1/2\)。 - (c) 回答四个“为什么”:
- 生成器为什么用 BatchNorm,而判别器完全不用 Norm?
- 卷积/转置卷积在 BatchNorm 前为什么设
bias = FALSE? - 生成器最后一层为什么用 Tanh,数据预处理为什么用
Normalize(0.5, 0.5)? - 判别器为什么用 LeakyReLU,且最后一层没有 Sigmoid?
- (d) 训练结束后直接
netG(torch_randn(...))采样,和先netG$eval()再采样,结果一样吗?用本章的生成器做实验,报告最大绝对差,并解释 BatchNorm 的 train/eval 差异。
(a) 卷积输出:
\[ H_{\text{out}} =\left\lfloor\frac{H_{\text{in}}+2p-k}{s}\right\rfloor+1. \]
转置卷积输出(output_padding = 0):
\[ H_{\text{out}}=(H_{\text{in}}-1)s-2p+k. \]
生成器(输入 [N, 100, 1, 1]):
| 层 | 操作 | 输出形状 |
|---|---|---|
| 1 | ConvT \(k=4,s=1,p=0\) | [N, 512, 4, 4] |
| 2 | ConvT \(k=4,s=2,p=1\) | [N, 256, 8, 8] |
| 3 | ConvT \(k=4,s=2,p=1\) | [N, 128, 16, 16] |
| 4 | ConvT \(k=4,s=2,p=1\) | [N, 64, 32, 32] |
| 5 | ConvT \(k=4,s=2,p=1\) | [N, 3, 64, 64] |
判别器(输入 [N, 3, 64, 64]):
| 层 | 操作 | 输出形状 |
|---|---|---|
| 1 | Conv \(k=4,s=2,p=1\) | [N, 64, 32, 32] |
| 2 | Conv \(k=4,s=2,p=1\) | [N, 128, 16, 16] |
| 3 | Conv \(k=4,s=2,p=1\) | [N, 256, 8, 8] |
| 4 | Conv \(k=4,s=2,p=1\) | [N, 512, 4, 4] |
| 5 | Conv \(k=4,s=1,p=0\) | [N, 1, 1, 1] |
最后一层 view(-1) 后得到 [N] 的分数向量。注意判别器没有 BatchNorm,所以每个卷积层只有权重、没有偏置(bias = FALSE)。
(b) 卷积/转置卷积的参数量为
\[ C_{\text{in}}\times C_{\text{out}}\times k^2 +\underbrace{C_{\text{out}}}_{\text{偏置(若启用)}}. \]
生成器 ngf=64:
| 层 | 计算 | 参数量 |
|---|---|---|
| ConvT 1 | \(100\times512\times16\) | 819,200 |
| BN 1 | \(2\times512\) | 1,024 |
| ConvT 2 | \(512\times256\times16\) | 2,097,152 |
| BN 2 | \(2\times256\) | 512 |
| ConvT 3 | \(256\times128\times16\) | 524,288 |
| BN 3 | \(2\times128\) | 256 |
| ConvT 4 | \(128\times64\times16\) | 131,072 |
| BN 4 | \(2\times64\) | 128 |
| ConvT 5 | \(64\times3\times16\) | 3,072 |
| 合计 | 3,576,704 |
判别器 ndf=64:
| 层 | 计算 | 参数量 |
|---|---|---|
| Conv 1 | \(3\times64\times16\) | 3,072 |
| Conv 2 | \(64\times128\times16\) | 131,072 |
| Conv 3 | \(128\times256\times16\) | 524,288 |
| Conv 4 | \(256\times512\times16\) | 2,097,152 |
| Conv 5 | \(512\times1\times16\) | 8,192 |
| 合计 | 2,763,776 |
ngf=ndf=32 时:
- 生成器:1,100,224;
- 判别器:693,760。
宽度减半(\(64\to32\))时,每个卷积层的 \(C_{\text{in}}C_{\text{out}}\) 变成原来的 \(1/4\)(两个因子都减半),所以参数量约变成 \(1/4\)。BatchNorm 只有 \(2C\) 个参数,是线性的,占比很小;因此总参数量也接近 \(1/4\),但不是严格 \(1/4\)(BN 项按 \(1/2\) 缩小)。
可以用代码核对:
count <- function(m) sum(sapply(m$parameters, function(p) p$numel()))
count(Generator(100, 64)) # 3576704
count(Discriminator(64)) # 2763776
count(Generator(100, 32)) # 1100224
count(Discriminator(32)) # 693760(c)
- 生成器用 BN,判别器不用。 生成器的 BN 把每层激活归一化,缓解内部协变量偏移,让深层的转置卷积更容易训练。判别器/Critic 不用 BN,主要因为 WGAN-GP 的梯度惩罚是逐样本的:BN 在一个 batch 内混合样本,单张图片的输出会依赖同 batch 的其他图片,逐样本梯度不再是真正的逐样本梯度;此外 BN 在 train/eval 下统计量不同,会让 Critic 的评分和 GP 不稳定。DCGAN 原文的判别器其实用了 BN,但 WGAN-GP 论文明确建议 Critic 不要用 BN,改用 LayerNorm/GroupNorm 或不归一化。本章脚本为了 MPS 兼容和 GP 稳定性,直接移除了所有 Norm。
- BN 前
bias = FALSE。 BN 会先减均值再除以标准差,卷积的常数偏置会被减掉;保留偏置只是多一个冗余参数(而且它的梯度会被 BN 的均值项抵消一部分)。所以标准做法是卷积不带偏置,把“平移”交给 BN 的 \(\beta\)。 - Tanh +
Normalize(0.5, 0.5)。transform_to_tensor()把像素变到 \([0,1]\),Normalize(mean=0.5, std=0.5)把它线性映射到 \([-1,1]\)。生成器最后一层 Tanh 的值域正好是 \((-1,1)\),与数据尺度匹配;如果生成器输出无界值,而数据在 \([-1,1]\),判别器很容易靠“数值范围”而不是内容区分真假。Tanh 在两端也会饱和,但这里它是输出层,且配合 MSE/BCE 的梯度通常可以工作;更现代的架构常用无界输出 + 相应的损失归一化。 - 判别器用 LeakyReLU,最后一层无 Sigmoid。 LeakyReLU 在负半轴保留一个小斜率(0.2),避免 ReLU 的“死亡神经元”让判别器过早失去梯度。最后一层无 Sigmoid,因为 WGAN 的 Critic 输出的是无界的“分数”,不是概率;Sigmoid 会把分数压到 \((0,1)\),既限制表达能力,又在两端饱和。LSGAN 的判别器同样去掉了 Sigmoid(见练习 2)。
(d) 不一样。BatchNorm 在 train() 模式下用当前 batch 的均值和方差归一化;在 eval() 模式下用训练过程中累积的 running mean/var。生成器里有 4 个 BN,采样时如果仍处于 train 模式,每张图的输出会依赖同 batch 的其他噪声样本,而且每次前向的 batch 组成不同,结果就会抖动。
用本章的生成器实测(固定同一组噪声):
train vs eval max abs diff: 1.009486
eval twice diff: 0
train 与 eval 的最大绝对差约为 \(1.01\)(像素值在 \([-1,1]\) 内),这是很大的差异;eval 连续两次前向完全一致(差为 \(0\)),因为 running stats 固定、BN 是确定性的。
正确做法:
netG$eval()
with_no_grad({
fake <- netG(fixed_noise)
})eval() 只切换 BN/Dropout 的行为,不关闭梯度;with_no_grad() 才是关闭梯度记录。两者都要。生成新样本时用 eval() 是标准做法;只有在训练、需要 batch 统计量时才用 train()。
三、开放思考
练习 7:模式坍塌、评价指标与模型取舍
- (a) 什么是模式坍塌(mode collapse)?为什么极小极大目标可能鼓励它?给出一个可观察的诊断信号和一个常用的缓解手段。
- (b) FID 和 Inception Score(IS)各衡量什么?它们各自的局限是什么?为什么不能在不同预处理/不同特征提取器下直接比较 FID?
- (c) 从样本质量、模式覆盖、训练稳定性、采样速度、似然估计五个维度,比较 GAN、VAE 和扩散模型。它们各自最适合什么场景?
- (d) 条件 GAN 与 CycleGAN 分别如何改变“生成”的定义?各举一个失败模式,并说明对应的诊断方法。
- (e) 什么情况下应该选 WGAN-GP,而不是标准 GAN 或谱归一化 GAN?选择时需要考虑哪些工程成本?
- (f) 用 AnimeFace 训练头像生成器时,数据许可、版权、生成人脸的肖像权和滥用风险分别要注意什么?列出至少三条上线前的检查。
(a) 模式坍塌指生成器把多个不同的 \(z\) 映射到几乎相同的输出,只覆盖真实分布的少数几个模式(例如只生成某一种发型/脸型),而丢掉其他模式。判别器一旦学会识破这个模式,生成器可能不是“学会其他模式”,而是“换一个模式继续骗”,形成循环。
极小极大目标可能鼓励坍塌,因为生成器的损失只关心当前判别器下的期望得分,不直接惩罚“覆盖了多少真实模式”。如果某个模式最容易骗过当前的 \(D\),把质量集中到那里就能快速降低损失;覆盖其他模式反而可能暂时降低平均得分。尤其当 \(D\) 的容量有限、只看局部纹理时,坍塌更严重。
诊断信号:
- 固定 \(z\) 网格,观察样本是否大量重复(视觉/聚类);
- 在玩具数据上统计“生成样本覆盖了多少个已知模式”;
- 计算生成样本两两距离的分布,坍塌时距离集中在很小的值;
- 监控 FID 的 recall 分量(或 precision/recall),坍塌通常表现为 recall 低而 precision 不低。
缓解手段:minibatch discrimination(让 \(D\) 看整个 batch 的多样性)、unrolled GAN(生成器考虑 \(D\) 未来几步的更新)、WGAN-GP、谱归一化、增大 batch、在 \(D\) 的输入中加噪声、或改用能覆盖模式的似然模型。没有一种方法能保证消除坍塌;通常要组合使用并做多样性评估。
(b) IS 用 Inception 分类器对生成图像做条件分布 \(p(y|x)\),计算
\[ \exp\Bigl(\mathbb E_x \mathrm{KL}(p(y|x)\|p(y))\Bigr). \]
它奖励“单张图分类置信度高”(质量)且“各类别边际分布均匀”(多样性),但它不比较真实数据分布,只衡量生成样本自身的标签分布,且对分类器偏差敏感。FID 把真实图和生成图都送进 Inception 特征提取器,拟合两个多元高斯,计算
\[ \|\mu_r-\mu_g\|_2^2 +\mathrm{Tr}\bigl(\Sigma_r+\Sigma_g-2(\Sigma_r\Sigma_g)^{1/2}\bigr). \]
它同时看均值(质量/偏差)和协方差(多样性),越低越好。FID 的局限:
- 依赖特征提取器(Inception 的版本、权重、输入分辨率);
- 假设特征服从高斯,对多模态/重尾分布不一定成立;
- 样本量小时估计有偏,\(N\) 越小 FID 往往越“好看”或越不稳定;
- 对预处理(归一化、裁剪、缩放)极其敏感。
所以只有在同一特征提取器、同一分辨率、同一预处理、同一样本量下比较 FID 才有意义;跨论文比较 FID 数字通常不可靠。更稳妥的做法是同时报告 precision/recall、KID(无高斯假设)和下游任务指标。
(c)
| 维度 | GAN | VAE | 扩散模型 |
|---|---|---|---|
| 样本质量 | 高,边缘锐利 | 偏模糊 | 很高,细节丰富 |
| 模式覆盖 | 容易坍塌 | 覆盖较好(受 KL 影响) | 覆盖好 |
| 训练稳定性 | 差,博弈/震荡 | 好,单目标优化 | 较好,但有噪声调度/采样调参 |
| 采样速度 | 一次前向,最快 | 一次解码,快 | 多步迭代,慢(几十到上千步) |
| 似然估计 | 无(或需额外工作) | 有 ELBO,可近似 | 有连续时间似然/ELBO,但计算复杂 |
取舍:
- GAN:需要实时/低延迟生成、样本质量优先、能接受训练不稳定和多样性风险(如头像、风格迁移、超分)。
- VAE:需要概率编码/潜空间、异常检测、压缩、可解释的似然下界,或作为扩散模型的分量。
- 扩散:离线生成、质量/多样性优先、算力充足(图像/视频/音频生成的主流)。
- 实际系统常常混合:VAE 编码器 + 扩散先验、GAN 判别器做感知损失、或蒸馏把扩散采样步数压到几步。
(d) 条件 GAN:生成器和判别器都额外接收条件 \(y\)(类别、文本、姿态等)。生成器学 \(p(x|y)\),判别器判断“\((x,y)\) 是否匹配”,而不只是“\(x\) 是否真实”。典型失败是条件坍塌:判别器忽略 \(y\),只根据 \(x\) 的真假打分,生成器也就不理会条件,所有类别生成同一种图。诊断:固定 \(z\)、改变 \(y\),看输出是否变化;或训练一个独立的分类器检查生成图的标签是否符合 \(y\)。缓解:projection discriminator、辅助分类器损失、条件注入到 \(D\) 的多个层、classifier-free guidance。
CycleGAN:用于无配对的图像到图像翻译。两个生成器 \(G:A\to B\)、\(F:B\to A\),两个判别器 \(D_A,D_B\),损失包含对抗项和循环一致性
\[ \lambda\,\mathbb E_{a}\|F(G(a))-a\|_1 +\lambda\,\mathbb E_{b}\|G(F(b))-b\|_1. \]
它不要求 \(a\) 与 \(b\) 成对,只要求“转过去再转回来”能还原。典型失败:循环一致性只约束“可逆”,不约束“内容不变”,\(G\) 可能把内容藏到看不见的高频/颜色通道里,或把多张输入映射到同一张输出(模式坍塌)。诊断:检查 \(G(a)\) 是否保留了输入的语义/几何(分割、关键点、光流),比较不同输入的输出多样性;缓解:identity loss、感知损失、增加对抗权重、使用注意力/可逆结构。
(e) 选 WGAN-GP 的典型场景:
- 标准 GAN 的损失曲线没有意义、训练震荡/崩溃,需要有意义的 Wasserstein 估计做模型选择;
- 生成分布与真实分布支撑集重叠很少(例如低维流形上的图像),JS 饱和导致梯度消失;
- 需要较稳定的梯度信号,且能接受额外的计算成本。
工程成本:
- GP 需要二次反向传播(对梯度再求导),训练显存/时间显著增加;
- Critic 中不能随意用 BatchNorm(逐样本梯度会被 batch 统计量污染),要改用 LayerNorm/GroupNorm/无归一化;
- 需要在真实/假样本之间做插值,输入必须是连续可微的(离散数据如文本不能直接插值);
- \(\lambda\)、
n_critic、学习率、优化器(常要求 Adam \(\beta_1=0\) 或 RMSprop)都要重新调; - 训练更慢,且不保证样本质量一定更好。
如果标准 GAN + 谱归一化已经稳定,或算力/延迟受限,未必要上 WGAN-GP。谱归一化只约束 Critic 的 Lipschitz 常数、成本低,是很多图像任务的默认选择;WGAN-GP 更适合“训练诊断困难、需要稳定距离信号”的场合。
(f) 至少检查:
- 数据许可/版权:AnimeFace 数据集里的图片来自网络动漫头像,原始版权可能不属于数据集发布者。训练前确认数据集许可证、是否允许派生模型/商用;生成模型不应被用来复刻特定受版权保护的角色。
- 肖像权/人格权:动漫角色通常不涉及真人肖像,但如果数据里混入真人照片、或模型能生成与真实人物高度相似的脸,就可能侵犯肖像权。上线前做相似度检索,禁止生成特定真人。
- 滥用/深度伪造:头像生成器可能被用于制作虚假身份、诈骗、骚扰。需要水印/元数据标记、输出内容审核、使用条款禁止冒充他人,并保留滥用追踪机制。
- 偏差与代表性:数据集可能偏向某种画风、性别、肤色;评估生成样本的多样性,避免模型放大偏见。
- 隐私:如果训练数据包含个人信息,检查是否可能被模型记忆/复现;对生成样本做近邻检索,排除复制训练样本的情况。
技术指标之外,上线前应有法务/伦理审查和明确的删除/申诉渠道。
四、纠错与陷阱
练习 8:容易踩的坑
下面每一条都来自本章正文或 code/13_* 脚本。请先判断“是否会报错/是否改变结果”,再给出修正。
- (a) 正文 LSGAN 的公式带 \(1/2\),脚本
loss_d、loss_g不带。这是 bug 吗?在 RMSprop 和 SGD 下分别会怎样? - (b) 训练判别器时忘了对假样本写
$detach(),会报什么错?为什么? - (c) 如果在 WGAN-GP 的 Critic 中加入 BatchNorm,会有什么问题?为什么本章脚本把 Norm 层全部移除?
- (d)
code/13_WGAN_GP.R的calc_gradient_penalty()用全局batch_size构造alpha_dims和view()。在什么情况下会崩?怎么改? - (e) 训练结束后直接
netG(fixed_noise)画图,和先netG$eval()再画,结果一样吗?为什么?
(a) 不是会改变优化方向的 bug,但是公式与代码不一致。
- 正文的 \(L_D\)、\(L_G\) 都带 \(1/2\);脚本把两个 MSE 直接相加,没有 \(1/2\)。数学上最小化 \(L\) 与最小化 \(L/2\) 的最优点相同,所以“解”不变。
- SGD:梯度整体变为 \(1/2\),等价于学习率减半。训练轨迹和收敛速度会变,需要重新调
lr。 - RMSprop/Adam:自适应分母会抵消全局尺度(忽略 \(\epsilon\)),更新几乎不变。脚本用 RMSprop,所以数值上影响很小。
- 但 D 和 G 的损失同时少乘 \(1/2\),两者的相对平衡不变;如果只改其中一个,才会改变 D/G 的相对步长。
建议:要么在公式中注明“这里省略了常数因子 \(1/2\)”,要么在代码中乘上,保持可复现性。对教学材料来说,公式与代码一致比“数学上等价”更重要,因为读者会照抄。
(b) 报错:
RuntimeError: Trying to backward through the graph a second time
(or directly access saved tensors after they have already been freed).
原因:D 的损失包含 netD(fake_data),而 fake_data = netG(noise) 仍挂在 G 的计算图上。D 的 backward() 会沿着 D(fake_data) → fake_data → G 一直反传,并在途中释放 G 图里的中间张量。随后 G 的 loss_g$backward() 需要再次穿过同一张图,就发现图已经被释放。
fake_data$detach() 返回一个与原图共享数据、但 requires_grad = FALSE 的张量,切断了 D 到 G 的梯度路径。这样:
- D 的反向传播只更新 D;
- G 的图保持完整,供 G 的
backward()使用; - 生成器的参数不会在 D 更新时被误改。
(c) BatchNorm 在 Critic 中会破坏 WGAN-GP 的逐样本性质:
- BN 用整个 batch的均值/方差归一化,单张图片的输出依赖于同 batch 的其他图片。GP 惩罚的是“每个插值样本 \(\hat x_i\) 的梯度范数”,但 BN 让 \(\hat x_i\) 的梯度混入了 batch 内其他样本的信息,不再是真正的逐样本梯度。
- BN 在
train()下用 batch 统计量,在eval()下用 running 统计量,Critic 的评分函数在训练/推理间发生变化,Wasserstein 估计和 GP 都会漂移。 - GP 需要对输入求梯度,BN 的反向传播还涉及 batch 维的耦合,容易引入数值不稳定。
所以 WGAN-GP 论文建议 Critic 不使用 BN,改用 LayerNorm、GroupNorm 或不归一化。本章脚本为了 MPS 兼容和稳定性,把 Critic 的 Norm 层全部移除;生成器仍然保留 BN(生成器不参与 GP 的逐样本梯度惩罚)。
(d) 会在这两种情况下崩:
- 最后一个 batch 不满(
drop_last = FALSE):real的 batch 维是 \(B'<512\),但alpha_dims = c(512, 1),广播失败;grad$view(c(512, -1))也会因为元素数不是 512 的倍数而报错。 - 用 5,000 个样本做最终评估/画图时仍调用同一个函数:
B'=5000,view(c(512,-1))直接报shape '[512, -1]' is invalid for input of size 5000(或类似的 shape 错误)。
修正:所有形状从输入读取,
B <- real$size(1)
alpha_dims <- c(B, rep(1, real$dim() - 1))
x_hat <- (alpha * real + (1 - alpha) * fake)$requires_grad_(TRUE)
# ...
grad <- grad$view(c(B, -1))本章的 DCGAN 脚本已经这样写(real_samples$size(1)),1D 脚本则是全局 batch_size;如果在 1D 脚本里改 batch 大小或复用函数做评估,就会踩坑。
(e) 不一样。生成器有 4 个 BatchNorm 层:
netG$train()(默认状态):BN 用当前 batch 的均值和方差。采样 8 张图时,每张图的输出依赖这 8 张图组成的 batch;换一批噪声、换 batch 大小,结果都会变。netG$eval():BN 用训练时累积的 running mean/var,逐样本确定;同一输入连续两次前向结果完全一致。
实测最大绝对差约 1.009(像素范围 \([-1,1]\)),eval 两次差为 0。所以画图/评估前必须:
netG$eval()
with_no_grad({
fake <- netG(fixed_noise)
})如果忘记 eval(),预览图会抖动、不可复现,而且和“模型最终学到的分布”有系统偏差(batch 统计量 vs running 统计量)。训练循环内部保持 train() 是对的;采样/评估切到 eval()。
进一步练习(可选)
如果希望把本章内容串起来,可以做一个“GAN 体检”小项目:
- 在 1D 玩具数据上同时训练 LSGAN、WGAN-Clipping、WGAN-GP,固定种子和步数,记录生成样本的均值/标准差、Wasserstein 估计和梯度范数;比较三者在“支撑集分离”场景下的收敛速度。
- 把 LSGAN 的噪声从均匀改为正态,重跑 10,000 步;再用两种噪声交叉采样,画一张 \(2\times2\) 的均值/标准差表,验证“噪声分布是模型的一部分”。
- 在 AnimeFace 上只训练 5 轮,分别用
per_row = 8和per_row = 4画网格,检查vision_make_grid的形状;再对比netG$train()与netG$eval()的采样结果。 - 给 Critic 加 BatchNorm,观察 WGAN-GP 的梯度范数分布和样本质量是否变差;再把 BN 换成 LayerNorm,比较训练稳定性。
- 用同一组生成样本计算 FID 的 precision/recall 分量(可用现成的 R/Python 实现),观察“只看 FID 总分”会漏掉哪种失败(质量差还是多样性差)。
报告时至少给出三次随机种子的均值和标准差,不要只报最好的一次;GAN 的指标波动通常比监督学习大得多。