13 生成对抗网络

本章从极小极大博弈出发,经过 LSGAN 的最小二乘损失、WGAN 的 Wasserstein 距离与 Lipschitz 约束,最后落到 AnimeFace 头像的 DCGAN/WGAN-GP 实现。下面 8 道题按四类编排:

建议用时约 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\)。它简单,但有两个问题:

  1. \(c\) 太小会让 Critic 容量不足,很多权重被钉在边界,网络无法表达复杂的评分函数;
  2. \(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) 参考输出就是题干中的表。三个阶段与正文描述一致:

  1. step 1:均值约 \(-0.14\)、标准差约 \(0.054\),生成器刚初始化,输出挤在 \(0\) 附近;
  2. step 1000–2500:均值迅速移动到 \(1.45\to2.12\),但标准差只有 \(0.25\to0.42\)。生成器先学会“数据在 \(x=2\) 附近”,即一阶矩,输出仍然是一条很窄的线;
  3. 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) 回答四个“为什么”:
    1. 生成器为什么用 BatchNorm,而判别器完全不用 Norm?
    2. 卷积/转置卷积在 BatchNorm 前为什么设 bias = FALSE?
    3. 生成器最后一层为什么用 Tanh,数据预处理为什么用 Normalize(0.5, 0.5)?
    4. 判别器为什么用 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)

  1. 生成器用 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。
  2. BN 前 bias = FALSE。 BN 会先减均值再除以标准差,卷积的常数偏置会被减掉;保留偏置只是多一个冗余参数(而且它的梯度会被 BN 的均值项抵消一部分)。所以标准做法是卷积不带偏置,把“平移”交给 BN 的 \(\beta\)。
  3. 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 的梯度通常可以工作;更现代的架构常用无界输出 + 相应的损失归一化。
  4. 判别器用 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) 至少检查:

  1. 数据许可/版权:AnimeFace 数据集里的图片来自网络动漫头像,原始版权可能不属于数据集发布者。训练前确认数据集许可证、是否允许派生模型/商用;生成模型不应被用来复刻特定受版权保护的角色。
  2. 肖像权/人格权:动漫角色通常不涉及真人肖像,但如果数据里混入真人照片、或模型能生成与真实人物高度相似的脸,就可能侵犯肖像权。上线前做相似度检索,禁止生成特定真人。
  3. 滥用/深度伪造:头像生成器可能被用于制作虚假身份、诈骗、骚扰。需要水印/元数据标记、输出内容审核、使用条款禁止冒充他人,并保留滥用追踪机制。
  4. 偏差与代表性:数据集可能偏向某种画风、性别、肤色;评估生成样本的多样性,避免模型放大偏见。
  5. 隐私:如果训练数据包含个人信息,检查是否可能被模型记忆/复现;对生成样本做近邻检索,排除复制训练样本的情况。

技术指标之外,上线前应有法务/伦理审查和明确的删除/申诉渠道。

四、纠错与陷阱

练习 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) 会在这两种情况下崩:

  1. 最后一个 batch 不满(drop_last = FALSE):real 的 batch 维是 \(B'<512\),但 alpha_dims = c(512, 1),广播失败;grad$view(c(512, -1)) 也会因为元素数不是 512 的倍数而报错。
  2. 用 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 体检”小项目:

  1. 在 1D 玩具数据上同时训练 LSGAN、WGAN-Clipping、WGAN-GP,固定种子和步数,记录生成样本的均值/标准差、Wasserstein 估计和梯度范数;比较三者在“支撑集分离”场景下的收敛速度。
  2. 把 LSGAN 的噪声从均匀改为正态,重跑 10,000 步;再用两种噪声交叉采样,画一张 \(2\times2\) 的均值/标准差表,验证“噪声分布是模型的一部分”。
  3. 在 AnimeFace 上只训练 5 轮,分别用 per_row = 8 和 per_row = 4 画网格,检查 vision_make_grid 的形状;再对比 netG$train() 与 netG$eval() 的采样结果。
  4. 给 Critic 加 BatchNorm,观察 WGAN-GP 的梯度范数分布和样本质量是否变差;再把 BN 换成 LayerNorm,比较训练稳定性。
  5. 用同一组生成样本计算 FID 的 precision/recall 分量(可用现成的 R/Python 实现),观察“只看 FID 总分”会漏掉哪种失败(质量差还是多样性差)。

报告时至少给出三次随机种子的均值和标准差,不要只报最好的一次;GAN 的指标波动通常比监督学习大得多。