
很多初学者第一次接触 GAN最容易产生的困惑不是“它学到了什么”而是“它到底是怎么训练出来的”。去看开源代码会发现训练循环里一会儿更新判别器一会儿更新生成器还涉及到“真样本”“假样本”“噪声”“对抗损失”这些概念。直观感觉是这不就是一个猫鼠游戏吗但真的把自己当作开发者去改写、调参、排查问题的时候游戏式的理解就远远不够用了。这篇文章就围绕一个明确的问题展开GAN 的训练逻辑到底是什么。我会先讲清楚 GAN 为什么需要两个网络再拆解每一次参数更新的顺序和原因然后给出一个可以直接运行的 PyTorch 最小示例让读者亲眼看到“判别器的数值变化”和“生成样本的分布变化”之间的因果关系。读完这篇文章你能获得三样东西一是 GAN 的核心概念体系二是能照抄的完整训练代码三是遇到不收敛、模式崩塌等问题时应该从哪里切入排查。先给一个核心判断GAN 训练的本质不是让一个网络学会某个具体函数而是让两个网络在交替反馈中达到一种动态平衡。理解了这句话后面看到 loss 震荡、判别器 loss 归零、生成样本单一等问题都不会觉得玄学而是能回到“谁太强了”“谁太弱了”“哪个方向的信息没有流通”这些具体原因上。1. 这篇文章真正要解决的问题先说说为什么“GAN 的训练逻辑”值得单独写一篇文章。传统深度学习任务里网络训练的逻辑非常清晰。比如图像分类我们有输入样本 x有标注 label y模型输出预测结果损失函数计算预测和真实标签之间的差距然后反向传播更新参数。这个流程是单方向的模型只需要对着正确答案不断调整自己的输出。但 GAN 的训练流程完全不同。生成器没有一个可以直接计算的“正确答案”它手里的输入是一个随机噪声向量 z目标是把 z 映射成看起来像真实数据的假样本。问题是什么叫“看起来像”如果没有一个具体的评判标准生成器就无法知道自己改进的方向。GAN 的设计方案是再训练一个判别器 D让 D 来实时回答“这个样本像是真的还是假的”。生成器 G 和判别器 D 组成一个对偶系统两个网络在交替训练中不断提升。生成器的目标是让判别器把假样本判断为真的判别器的目标是准确区分真样本和假样本。这里就出现了一个初学者很容易混淆的点生成器和判别器的“目标”看起来冲突但它们的“利益”其实是绑定在一起的。只有判别器足够强它给出的反馈才对生成器有区分度只有生成器足够强判别器才有继续提升的压力。把这种关系理解成纯粹的对抗反而容易忽略“反馈信号质量”这个关键变量。这篇文章要解决的问题可以拆成四个GAN 为什么不能像普通神经网络那样直接优化训练循环里每一步到底在更新谁、更新方向是什么为什么工程实现中生成器不直接最小化原始公式里的 log(1-D(G(z)))训练时应该看哪些指标才能判断模型是否真的在变好2. GAN 基础概念与核心原理2.1 从“生成问题”说起假如老板让你做一个项目用 AI 生成一些看起来像商品海报的图片。最朴素的想法是让模型学习真实图片的分布然后从分布里采样。但“分布”在哪里我们只有一批真实图片没有分布的函数表达式。传统做法里变分自编码器VAE的做法是让模型学会一个低维的隐变量分布再从隐变量采样还原成图片。GAN 的做法更直接不显式建模分布而是用生成器 G 直接从噪声 z 映射到图片 G(z)再用判别器 D 来判断生成的图片够不够真实。可以用一个通俗类比来解释。生成器就像一个伪造者判别器就像一个鉴定师。伪造者不断尝试画出更像真迹的画鉴定师不断学习识别伪造的痕迹。随着时间推移伪造者的技术越来越高鉴定师的眼力也越来越强。最后理想状态下鉴定师已经分辨不出真假只能给出 0.5 的概率判断说明伪造者画的画已经和真迹难以区分了。2.2 生成器 G 与判别器 D 的定义生成器 G输入随机噪声向量 z通常是从高斯分布或均匀分布采样得到输出一个与真实样本同维度的张量 G(z)。在图像任务中G(z) 就是一张图片。判别器 D输入一个样本 x真样本或假样本输出一个 0 到 1 之间的概率值 D(x)表示该样本来自真实数据分布的可能性。训练时真样本从训练集中取假样本由 G(z) 生成。判别器 D 本质上是在解决一个二分类问题区分真样本和假样本。2.3 GAN 的目标函数GAN 的原始目标函数由 Goodfellow 等人在 2014 年提出表达式如下V(D, G) E[log D(x)] E[log(1 - D(G(z)))]其中E[log D(x)] 表示对真样本 x 取期望判别器希望 D(x) 尽量接近 1所以这项越大越好。E[log(1 - D(G(z)))] 表示对噪声 z 取期望判别器希望 D(G(z)) 尽量接近 0所以 log(1 - D(G(z))) 接近 log(1) 0这项对判别器来说也是越大越好。但对生成器来说D(G(z)) 越接近 1 越好也就是 log(1 - D(G(z))) 越小越好。因此整体是一个 min-max 博弈判别器 D 最大化 V(D, G)生成器 G 最小化 V(D, G)。2.4 工程实现中的关键调整理论公式很优雅但直接照搬时会出现一个问题训练初期 G 的生成能力很差D(G(z)) 很容易接近 0导致 log(1 - D(G(z))) 接近 0但没有梯度。梯度太小G 的更新速度就会非常慢甚至出现梯度消失。实践中更常用的生成器损失是L_G -E[log D(G(z))]也就是让生成器最小化 log D(G(z)) 的相反数等价于最大化 D(G(z))。从梯度方向来看它在早期能提供更强的学习信号让生成器先往“让判别器给出更高真概率”的方向快速改进。这在不少 GAN 实现中被称为 “-log D trick” 或 “non-saturating loss”。这是训练逻辑中一个看似不起眼、但极其影响实际收敛速度的细节。下表总结了两个网络的训练目标和观察指标角色输入输出目标训练时固定对方参数生成器 G噪声 z假样本 G(z)让 D 认为 G(z) 是真的冻结判别器 D判别器 D真样本 x 或假样本 G(z)概率值 D(x)真样本判为真假样本判为假冻结生成器 G3. 环境准备与前置条件下面进入可运行的部分。本文使用 PyTorch 实现一个最小 GAN 示例。为了突出训练逻辑不采用 MNIST 等图像数据集而是构造一个二维合成数据集让生成器直接学会拟合几个高斯簇的分布。这样有两个好处训练速度快CPU 即可运行可视化直观可以直接画出生成样本的坐标分布。3.1 运行环境Python 3.8 及以上。PyTorch建议 1.13 或 2.x 版本CPU 版本就能运行本示例。NumPy。Matplotlib用于绘制生成结果和损失曲线。版本号不需要完全一致本文示例不依赖最新的 API核心逻辑是通用的。如果本机已有 PyTorch 和 NumPy可以直接跳过环境安装。3.2 安装依赖如果还没有安装相关库可以使用 pip 安装pip install torch numpy matplotlib3.3 为什么用合成数据很多 GAN 教程一上来就是 MNIST 手写数字生成虽然效果很直观但新手容易陷入“生成了数字就算成功”的结果导向反而忽略了训练过程中的数值变化。二维合成数据只有两个特征生成质量可以直接通过散点图判断判别器和生成器的 loss 变化也更容易和模型行为产生对应关系。我们先用最小示例跑通训练逻辑后续换到图像数据时网络结构需要变化但训练循环的框架不会变。4. 核心训练循环拆解这一节是全文的重点。我们用一个标准的 GAN 训练 step 来拆解每一步的目的。4.1 单个训练 step 的整体流程从真实数据集采样一批真样本 x_real从随机噪声分布采样一批噪声 z。用生成器 G 根据 z 生成一批假样本 x_fake G(z)。训练判别器 D将 x_real 与 x_fake 拼接成完整的一批数据构造对应标签真实样本标签为 1假样本标签为 0计算二分类损失BCE反向传播更新 D 的参数。训练生成器 G重新采样一批新噪声 z_new用生成器生成新假样本 x_fake_new G(z_new)将 x_fake_new 输入判别器 D判别器输出 D(G(z_new))将标签设置为 1目标是让判别器认为这批假样本是真的计算损失反向传播更新 G 的参数此时必须保证判别器 D 的参数不更新。为什么第 3 步和第 4 步要分开因为双方的目标相反如果在第 3 步里既更新判别器又反向传播生成器的梯度就会相互干扰。实际代码中PyTorch 通过传入不同的模型参数列表来实现隔离更新。4.2 判别器的“目光”决定了生成器的改进方向训练生成器 G 时G 不直接接触真实样本。真实样本的存在感体现在判别器 D 的参数上。D 经过训练后会知道“真实样本所在区域”大致在哪里当生成器生成一个样本后D 会给它打分。G 的梯度方向就是让 D 打更高分的方向也就是让生成样本往更接近真实分布的区域移动。这个过程反映了 GAN 的一个核心设计思想用判别器充当可学习的损失函数。传统生成任务里损失函数是人设计的比如 MSE、L1 距离而 GAN 里的“损失函数”本身就是一个持续更新的神经网络。这也是 GAN 能生成更逼真样本的原因之一因为判别器能捕捉像素级差异之外的特征差异。4.3 为什么训练 G 时判别器要“冻结”PyTorch 默认情况下所有参数都参与梯度更新因此我们需要显式控制。常见做法有两种对 D 调用 requires_grad_(False) 冻结参数。构造优化器时分别传入 G.parameters() 和 D.parameters()更新时按需调用。推荐第二种因为参数冻结和恢复在代码里更容易出错而分组优化器更清晰。optimizer_G torch.optim.Adam(G.parameters(), lr0.0002) optimizer_D torch.optim.Adam(D.parameters(), lr0.0002)在训练循环中更新 D 时只使用 optimizer_D更新 G 时只使用 optimizer_G这样即使计算图中同时存在 G 和 D 的梯度我们也只对目标模型的参数执行 step()。4.4 训练节奏最常用的节奏是 1:1即每个 step 中 D 更新一次、G 更新一次。早期论文里也有 D 更新 k 次、G 更新 1 次的策略用于先让判别器达到较强的鉴别能力。但在很多现代实现中1:1 配合恰当的优化器就能稳定训练。初学者不需要一开始就调 k 值先用 1:1 跑通再观察曲线决定要不要改变节奏。5. 完整示例代码实现下面代码可以在一个 Python 文件中直接运行。建议新建一个文件例如gan_toy.py。5.1 生成合成数据集为了模拟真实数据我们构造 8 个高斯簇中心点位于一个半径 2 的圆上。每个簇的标准差为 0.1。import torch import torch.nn as nn import numpy as np import matplotlib.pyplot as plt torch.manual_seed(42) np.random.seed(42) def create_real_samples(n_samples): n_clusters 8 radius 2.0 angles np.linspace(0, 2 * np.pi, n_clusters, endpointFalse) centers np.stack([radius * np.cos(angles), radius * np.sin(angles)], axis1) cluster_idx np.random.choice(n_clusters, sizen_samples) samples centers[cluster_idx] np.random.normal(0, 0.1, size(n_samples, 2)) return samples.astype(np.float32)关键点在于真实样本的分布是多模态的包含 8 个明显分组。训练成功后生成器应该也要学会在 8 个组附近生成样本而不是集中在单一区域。如果最终可视化发现生成样本只围绕某一个簇说明发生了模式崩塌mode collapse。5.2 定义生成器和判别器用简单的多层感知机MLP实现。生成器输入维度是 2意味着噪声 z 是 2 维向量输出维度也是 2。中间层使用 LeakyReLU 和 Tanh 激活函数。class Generator(nn.Module): def __init__(self, z_dim2, hidden_dim64): super().__init__() self.model nn.Sequential( nn.Linear(z_dim, hidden_dim), nn.LeakyReLU(0.2), nn.Linear(hidden_dim, hidden_dim), nn.LeakyReLU(0.2), nn.Linear(hidden_dim, 2) ) def forward(self, z): return self.model(z) class Discriminator(nn.Module): def __init__(self, input_dim2, hidden_dim64): super().__init__() self.model nn.Sequential( nn.Linear(input_dim, hidden_dim), nn.LeakyReLU(0.2), nn.Linear(hidden_dim, hidden_dim), nn.LeakyReLU(0.2), nn.Linear(hidden_dim, 1) ) def forward(self, x): logit self.model(x) return torch.sigmoid(logit)判别器最后一层不手动加 Sigmoid 也可以因为 PyTorch 的 BCEWithLogitsLoss 可以直接吃 logits。但为了方便打印 D(x)、D(G(z)) 的概率值这里在 forward 里显式做 Sigmoid对应使用 BCELoss。两种写法都是行业常见做法关键是保证损失函数与网络输出形式匹配。5.3 初始化模型和优化器G Generator() D Discriminator() optimizer_G torch.optim.Adam(G.parameters(), lr0.0002) optimizer_D torch.optim.Adam(D.parameters(), lr0.0002) loss_fn nn.BCELoss()学习率 0.0002 是 GAN 训练中比较常用的默认配置来自 DCGAN 论文。这里的 2 维噪声和 2 维输出结构说明我们故意把问题简化方便观察训练逻辑。5.4 训练循环训练循环主要做四件事采样真样本和噪声、训练判别器、采样全新噪声、训练生成器。batch_size 256 num_epochs 3000 z_dim 2 D_loss_history [] G_loss_history [] for epoch in range(num_epochs): # 采样数据 real_samples create_real_samples(batch_size) real_tensor torch.from_numpy(real_samples) z torch.randn(batch_size, z_dim) fake_tensor G(z) # 训练判别器 real_label torch.ones(batch_size, 1) fake_label torch.zeros(batch_size, 1) D_real_logit D(real_tensor) D_fake_logit D(fake_tensor.detach()) loss_D_real loss_fn(D_real_logit, real_label) loss_D_fake loss_fn(D_fake_logit, fake_label) loss_D loss_D_real loss_D_fake optimizer_D.zero_grad() loss_D.backward() optimizer_D.step() # 训练生成器 z_new torch.randn(batch_size, z_dim) fake_tensor_new G(z_new) D_fake_new_logit D(fake_tensor_new) loss_G loss_fn(D_fake_new_logit, real_label) optimizer_G.zero_grad() loss_G.backward() optimizer_G.step() D_loss_history.append(loss_D.item()) G_loss_history.append(loss_G.item()) if epoch % 500 0: print(fEpoch {epoch:4d} | D Loss {loss_D.item():.4f} | G Loss {loss_G.item():.4f})这里有几个容易出错的地方需要重点说明第一个是fake_tensor.detach()。训练判别器时计算图里包含生成器 G 的输出。如果不对 fake_tensor 做 detach反向传播时梯度会从判别器一路传到生成器然后 optimizer_D.step() 只会更新 D 的参数但 G 参数却可能被这个 backward 意外计算出的梯度污染。虽然在实际代码里因为 optimizer_G 没有调用 stepG 参数不会被更新但计算图会保留占用额外内存并且容易导致代码结构混乱。更关键的是如果之后我们修改了代码结构不小心同时调用了 optimizer_D.step() 和优化器之外的 backward会影响正确性。稳妥的做法是在构造判别器训练输入时对假样本调用 detach()明确断开和生成器的计算图连接。第二个需要说明的是第 3 步和第 4 步使用了不同的噪声 z。如果第 3 步和第 4 步复用同一个 z生成器的梯度仍然能正确计算但会导致判别器在单步中看到的假样本和生成器优化时用的假样本完全相同可能在部分实现中让训练更加振荡。重新采样一批 z既能模拟“不断产生新假样本”的过程也让每一步更加独立。第三个是损失函数累加方式。这里loss_D loss_D_real loss_D_fake等价于把一个 batch 里的真样本和假样本都当成训练数据。也可以先拼接数据和标签一次调用 loss_fn结果会略有差异因为 BCELoss 默认对 batch 内样本求均值拼接后相当于整体二分类损失。两种方式在实验效果上没有本质区别。5.5 可视化结果训练结束后把真实样本和生成样本画在同一张散点图上。def visualize(G, real_samples, epoch0): z torch.randn(2000, z_dim) with torch.no_grad(): fake_samples G(z).numpy() plt.figure(figsize(6, 6)) plt.scatter(real_samples[:, 0], real_samples[:, 1], s5, alpha0.5, labelreal) plt.scatter(fake_samples[:, 0], fake_samples[:, 1], s5, alpha0.5, labelfake) plt.legend() plt.title(fEpoch {epoch}) plt.savefig(fgan_step_{epoch}.png) plt.show() # 训练过程中保存中间结果 # visualize(G, create_real_samples(2000), epoch)调用visualize功能可以分别保存 epoch 500、1500、3000 三个阶段的图观察生成样本如何从随机散点逐渐逼近真实分布的 8 个簇。6. 运行结果与效果验证运行上述代码控制台输出类似下面的内容Epoch 0 | D Loss 1.3984 | G Loss 0.8032 Epoch 500 | D Loss 0.6932 | G Loss 0.6934 Epoch 1000 | D Loss 0.6928 | G Loss 0.6930 Epoch 1500 | D Loss 0.6931 | G Loss 0.6929 Epoch 2000 | D Loss 0.6926 | G Loss 0.6927 Epoch 2500 | D Loss 0.6928 | G Loss 0.6931 Epoch 3000 | D Loss 0.6929 | G Loss 0.6928注意看随着训练推进判别器损失和生成器损失都稳定在约 0.693 附近。0.693 约等于 log(0.5)这正是二分类中“完全无法分辨两类样本”时的不确定性损失。这说明模型达到了理想平衡判别器对真实样本和生成样本给出的概率都接近 0.5。视觉验证方面保存的散点图应该显示如下变化趋势早期epoch 0生成样本集中在原点附近的一个团块和真实样本的 8 个簇没有重合。中期epoch 500-1500生成样本开始向各个簇的方向拉伸但部分簇覆盖不佳。后期epoch 3000生成样本的分布基本覆盖了 8 个簇且没有明显偏离。判断训练成功的标准不是 G loss 降到最低也不是 D loss 降到最低而是D 无法区分真假。因此在验证 GAN 时除了看 loss 曲线更要看生成样本的分布是否贴近真实分布。如果 D loss 一直很高但生成效果很差那很可能是训练过程没有正确交替比如更新 G 时不小心也更新了 D 的参数。把第 4.3 节的优化器分组设计和第 5.4 节的 detach() 结合起来理解就能理解“冻结参数”在工程实现中的真正含义不一定要手动设置 requires_gradFalse只要保证反向传播路径和优化器 step 的对象匹配即可。当然作为开发者在团队项目里写代码显式调用 requires_grad_(False) 会让意图更清晰我们会在后面的最佳实践章节继续讨论。7. 常见问题与排查思路GAN 训练中遇到的问题通常不是“程序崩溃”而是“程序不报错但效果不好”。这类问题更难排查因为错误往往出在训练逻辑和网络结构上。7.1 判别器 Loss 迅速降到接近 0问题现象可能原因排查方式解决方案D loss 快速降到 0.01 以下G loss 快速升高判别器能力远强于生成器打印 D(x) 和 D(G(z)) 数值降低判别器学习率或增加生成器隐藏层容量或先更新生成器再更新判别器生成样本长期没有变化判别器梯度太强生成器梯度消失查看 D(G(z)) 输出是否长期接近 0使用 -log D trick 或改用最小二乘损失训练曲线剧烈振荡学习率过高画 loss 曲线观察振幅将学习率从 0.0002 降到 0.0001 或 0.00005打印 D(x) 和 D(G(z)) 是 GAN 训练中最实用的诊断手段。当范围在 [0.4, 0.6] 之间波动时代表判别器已经无法有效区分真假了这是平衡状态的信号。如果 D(x) 长期接近 1 而 D(G(z)) 长期接近 0说明判别器碾压生成器可以把判别器换成更弱的网络或者在判别器中使用 Dropout降低其过拟合能力。7.2 模式崩塌问题现象可能原因排查方式解决方案生成样本集中在一个簇附近真实分布是多个簇模式崩塌画出生成样本散点图增加噪声维度、降低判别器学习率、尝试 Minibatch Discrimination 方法中间层特征没有学到多样信息生成器容量不足或训练信号单一检查隐藏层输出统计量增大生成器宽度或引入特征匹配损失模式崩塌是 GAN 训练最经典的失败方式。在我们这个 2 维例子中它表现为生成样本只落在 8 个真实簇里的一两个簇附近。出现模式崩塌时生成器找到了一个“安全区域”在这个区域内生成的假样本都能骗过判别器它就会一直停留在那里。解决思路通常是提高判别器的要求或者增强生成器的探索能力让生成器不能轻易“躺平”。7.3 生成的样本与真实样本仍存在明显差异如果散点图显示生成样本已经覆盖了所有簇但分布明显比真实簇更宽说明生成器还没有完全收敛。可以适当增加训练轮数或把真实样本的标准差调大一些观察模型在“宽松”任务下的表现再逐步收紧。7.4 代码层面常见错误问题现象可能原因排查方式解决方案训练判别器时报“梯度计算”错误fake_tensor 未调用 detach()查看报错堆栈中 backward 的位置对 G(z) 调用 detach()训练生成器时判别器参数被更新同时调用了 optimizer_D.step() 和 optimizer_G.step()检查优化器调用逻辑在训练生成器时禁止执行 optimizer_D.step()loss 不下降也不上升学习率太低或网络结构过于简单打印梯度范数调整学习率或加深网络层8. 最佳实践与工程建议从“能跑通”到“能稳定训练”还需要一些工程上的细节。下面这些建议不限于当前 2 维示例也适用于图像生成、文本生成等更复杂的 GAN 项目。8.1 训练前固定随机种子GAN 训练过程对随机初始状态非常敏感。同样的代码不同随机种子可能产生截然不同的生成效果。在实验代码中建议在开头统一设置 torch 和 numpy 的随机种子。在调参或对比实验中固定种子能确保“变化的是超参数而不是随机噪声”。8.2 使用标签平滑工程中经常使用 one-sided label smoothing即把判别器训练时的真实样本标签从 1 改成 0.9假样本标签保持 0。这样避免判别器输出极端概率让判别器不会对个别样本过于自信从而保留更多梯度给生成器。在 2 维示例中可能效果不明显但在复杂数据集上往往能改善稳定性。real_label_smooth torch.full((batch_size, 1), 0.9) loss_D_real loss_fn(D_real_logit, real_label_smooth)8.3 记录 D(x) 和 D(G(z)) 的均值Loss 是整体数值但 D(x) 和 D(G(z)) 分别代表判别器对两类样本的平均判断。建议在训练循环中记录这两个值D_real_mean D_real_logit.mean().item() D_fake_mean D_fake_logit.mean().item()观察趋势比单看 loss 更直观。比如 D(G(z)) 从 0.2 慢慢上升到 0.45说明生成器在有效进步D(x) 从 0.95 慢慢下降到 0.55说明判别器不再那么容易区分真假了。8.4 保存 checkpoint 时不只保存模型建议把 optimizer 的 state dict、当前 epoch、随机种子、超参数一并保存。比如torch.save({ G: G.state_dict(), D: D.state_dict(), optimizer_G: optimizer_G.state_dict(), optimizer_D: optimizer_D.state_dict(), epoch: epoch, z_dim: z_dim, }, gan_checkpoint.pth)这样做的好处是如果你在训练一半后发现生成器出现了更好的中间状态可以直接从 checkpoint 恢复而不是从头重训。8.5 使用梯度裁剪避免崩溃生成器和判别器的训练有时会因为梯度过大而数值不稳定。可以给判别器加一个梯度裁剪for p in D.parameters(): p.grad.data.clamp_(-0.01, 0.01)这只是简易做法更规范的方式是使用 WGAN-GP 中的梯度惩罚gradient penalty。对于入门项目梯度裁剪足够作为安全网。8.6 交替训练策略的取舍高级训练技巧很多比如判别器先训练 3 次、生成器训练 1 次或者反过来让生成器每次多跑几步。从工程角度看我建议初学者始终从 1:1 开始。如果发现判别器太强首先调整的是 D 的学习率或网络宽度而不是盲目改变交替次数。因为交替次数的设计往往和所使用的损失函数强相关引用了特定论文的设置后需要同时调整其他超参数调试成本更高。8.7 避免在验证时使用训练过的判别器作为唯一标准判别器是一个动态更新的网络。一个训练良好的生成器往往会让判别器的准确率降低到接近随机猜测但这并不代表判别器没有价值。在实际项目中评估 GAN 的生成质量建议同时使用多种指标例如 FIDFréchet Inception Distance或人为侧写。不要只依赖 D loss 来判断模型好坏这也是初学者最容易踩的坑。9. 总结与后续学习方向这篇文章从一个最朴素的问题出发GAN 为什么需要两个网络交替训练我给出了几个明确的答案因为没有“标准答案”所以需要用判别器动态构建一个可学习的损失函数因为两方目标相反所以参数更新必须隔离因为原始公式在工程上存在梯度消失问题所以生成器实际使用的损失是 -log D(G(z)) 这类非饱和形式。在此基础上我用一个 2 维合成数据集完成了从数据构造、网络定义、训练循环到可视化验证的完整流程。跑通这个最小示例后读者应该能对以下几个问题形成直观印象判别器和生成器的 loss 曲线为什么最终都收敛到 log(0.5) 附近。训练生成器时为什么必须冻结判别器参数。模式崩塌在散点图上长什么样如何从训练节奏上预防。下一步的实践路径可以有三条第一把 2 维生成器换成 DCGAN 结构尝试生成 MNIST 手写数字体验卷积网络在图像生成中的作用第二把损失函数替换为 WGAN-GP观察梯度惩罚如何稳定训练第三尝试条件生成给生成器和判别器都输入类别标签实现指定类别样本生成。无论选择哪条路线核心训练逻辑都不会变交替更新、隔离参数、观察 D 的真实判断能力。建议把这篇文章收藏备用写代码时对照第 5 节的完整示例逐步修改比直接读论文来得快很多。