news 2026/10/27 17:26:41

GAN生成对抗网络原理详解:从基本思想到训练实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
GAN生成对抗网络原理详解:从基本思想到训练实践

GAN(Generative Adversarial Network,生成对抗网络)这个概念,现在基本是生成模型里绕不开的名字。不少同学第一次接触它,就是通过那门经典的机器学习/深度学习公开课,其中第74讲“Basic Idea”把GAN的核心思路讲得特别直白。我在最开始看这一讲的时候,觉得“这不就是造假币的和验钞机互相较劲嘛”,听懂了原理,但真正自己动手写代码时,才发现课程里那些“轻松训练”背后全是坑。这篇文章就把那堂课里的“基本想法”展开聊一聊,并结合我自己跑实验的体会,把生成器、判别器、目标函数是怎么一步步搭起来的,以及训练时最容易翻车的地方,都给拆开说明白。适合刚接触GAN、想理解基本原理并快速跑通一个实验的同学参考。

1. GAN整体设计:为什么要把两个网络放在一起“打架”

1.1 从生成任务说起

在GAN出现之前,生成模型的工作方式通常很“辛苦”。比如传统自回归模型,要按顺序一个一个预测像素或者词,计算代价大;变分自编码器(VAE)虽然能生成,但生成图像普遍偏模糊,因为它倾向于把分布中的“平均值”搬出来。那有没有办法让模型直接学会一个高维分布,而且生成的样本清晰、多样?GAN给出的答案简单得反直觉:我不直接去拟合真实分布的公式,而是让一个造假者不断模仿真品,让一个鉴定师不断分辨真假,两个人互相逼对方进步,最后造假者生成的“作品”连鉴定师都分不出来,那生成结果就接近真实分布了。

课程里那个经典比喻,我也一直跟身边朋友讲:生成器(Generator)就是造假币的人,目标是造出足以乱真的假币;判别器(Discriminator)就是验钞机或者说警察,目标是把假币精准揪出来。一开始造假者只能造出印着“100元”的白纸,警察轻松识破;但每次被识破之后,造假者会改进工艺,而警察也会研究更细的防伪点。几轮对抗之后,假币可能真的和真币没什么区别了。

这个“两个网络互相提高”的过程,本质上是在隐式地学习训练数据的分布。GAN不需要显式定义数据的概率密度,它只要求生成器输出的样本能从判别器那里“骗过高分”。这种思路绕开了高维空间中概率密度计算困难的问题,也是它能在图像生成、风格迁移、数据增强等领域遍地开花的原因。

1.2 生成器与判别器的角色分工

用公式来说,整个对抗过程可以写成这样一个最大最小化目标:

V(D, G) = E_{x~p_data} [log D(x)] + E_{z~p_z} [log(1 - D(G(z)))]

  • 生成器G的输入是随机噪声向量z,比如64维或100维的从标准正态分布里采样的向量,输出是和数据x同样形状的假样本G(z)。
  • 判别器D的输入是真实样本x或生成样本G(z),输出是一个0到1之间的标量,表示“判定输入为真实数据”的概率。
  • D希望最大化V:尽量给真实样本高分(log D(x)接近0),给生成样本低分(log(1-D(G(z)))接近0)。
  • G希望最小化V:努力让D(G(z))接近1,从而让log(1-D(G(z)))接近负无穷,整体目标变小。

训练时,D和G是交替更新的,不是一个回合里同时把两个网络都train到收敛。通常每个迭代先固定G,更新若干步D;再固定D,更新G。这就像两个人轮流行动而不是同时挥拳,否则会乱套。

1.3 为什么这种“对抗”在理论上是成立的

课程里证明了,如果G和D都有足够的表达能力,并且我们每次都能让D在当前G下达到最优,那么优化过程最终会让生成分布p_g逼近真实分布p_data。具体思路是,对任意固定的G,最优判别器有一个解析形式:

D*(x) = p_data(x) / (p_data(x) + p_g(x))

这个式子很直观:如果真图中某个区域非常常见,而假图中这个区域很少见,那D就会判它更像真的;如果真假概率接近,那D就只能给0.5了。

把D*代入目标函数,会得到关于p_data和p_g之间的JS散度表达式。JS散度衡量两个分布的重合程度,它在完全相同时最小。所以GAN从理论上等价于在最小化两个分布之间的JS散度,这就是它“学习分布”的数学支撑。不过,这个理论和实际训练之间存在一道裂缝:高维图像空间中,真实分布和生成分布往往只在一个低维流形上有少量重叠,一旦真的完全不重叠,JS散度会变成一个常数,梯度就消失了。这个现象就是我们后面要说的“训练不稳定”的根源之一。

2. 核心细节解析:从目标函数到损失函数,课程没展开的那些弯弯绕

2.1 目标函数如何变成能算的损失

公式里的期望是理论上的,实际训练时我们只有有限样本,所以要把期望换成损失函数。对于判别器而言,它做的是二分类,我们给真实样本打标签1,给生成样本打标签0,损失直接套二元交叉熵:

Loss_D = -E_{x} [log D(x)] - E_{z} [log(1 - D(G(z)))]

代码里通常写成nn.BCELoss(),把判别器的输出经过Sigmoid后和标签做交叉熵。

生成器的损失有两个常见版本。原始论文里是

Loss_G = E_{z} [log(1 - D(G(z)))]

这个版本在训练初期有个大问题:此时G还很弱,D轻而易举就能识破,D(G(z))接近0,所以log(1 - D(G(z)))接近0,梯度很小,G几乎学不动。课程和后来的实践普遍推荐改用“最大化D对假样本的打分”,也就是让G去最小化负对数:

Loss_G = -E_{z} [log D(G(z))]

换句话说,给假样本也贴上标签1,让生成器学着去骗过判别器。这被称为“非饱和损失”。我实际对比过,采用这个替代后,前期训练速度快很多,不至于像最早论文那样磨蹭半天生成器没动静。

2.2 判别器强到什么程度才算合适

不少初学者会有个误解:既然判别器越厉害,造假者越有挑战性,那不是很好吗?实操中完全不是。如果每次先把D训练到几乎完美,G得到的反馈会退化成一个极不均匀的信号。原因可以回到最优判别器D*(x)的公式:如果p_data和p_g之间重叠区域太小,D在大部分区域都能给出非常极端的输出,比如真实样本输出0.999,生成样本输出0.001,那么对G来说,只有极少数生成样本能获得稍微平缓的梯度,其余位置梯度都接近0,G几乎无法进步。

所以训练GAN很像在走钢丝。常见的做法不是让D先收敛到最优,而是每更新一次G,就配合更新一到五次D,让D“略强于G”但又不至于“碾压G”。我自己的经验是:G和D的迭代次数比设为1:1比较省心,如果D太强就调低D的学习率,或者减少D每轮的更新步数。

2.3 模坍缩为什么是生成器最容易踩的坑

模式坍缩(Mode Collapse)指的是生成器只学会了输出少数几种样本,甚至只输出一张固定图片,却依然可能骗过D。为什么?因为D只看单张图片的真假,并不看整体多样性。比如MNIST手写数字有10类,G如果只学会生成“1”并且“1”画得很像,D很难从单样本角度发现它丢掉了其他9类。

课程里用极大极小博弈来解释这个现象会让人感觉“理论上不会坍缩”,因为最优解要求p_g完全等于p_data。但实际优化是动态的,G在对抗中会发现某个“捷径”能暂时骗过D,就可能陷进去不出来。我在实验中遇到的一个典型过程是:D的loss稳定下降,G的loss也稳定下降,但抽样一看,生成的都是同一类图片,甚至同一张。这种情况单看损失函数是发现不了的,必须每轮都把生成的样本保存下来,肉眼观察。

2.4 原理解释:为什么原始GAN容易训练不稳定

训练不稳定在GAN里几乎是个“标配”,尤其是原始GAN。理论原因前面提过,高维空间里的图像分布和生成分布,重叠区域的测度很可能为0,这时候最优判别器会把几乎所有真实样本都输出为1,所有生成样本都输出为0,导致目标函数的梯度消失,G得不到有效反馈。就算两个分布有一点重叠,如果重叠区域很“尖”,梯度也可能异常大,导致loss爆炸。

这也是为什么后来的WGAN直接用Earth Mover's Distance(推土机距离)替换JS散度。推土机距离在两个分布完全没有重叠时依然能提供一个平缓且有意义的梯度,让G能够“顺着坡”把生成的分布推向真实分布。我在实践中的体会是,如果你在训练原始GAN时发现loss在某个值附近抖动但生成效果始终很差,不要死磕调参,换用WGAN的损失往往立刻就能看到改进。

3. 实操过程:用PyTorch从零搭一个能跑的GAN

这一节我会带大家从零写一个最简单的GAN,在二维人造数据上训练。选择二维数据而不是MNIST,是因为我们可以直接画图看到真实分布和生成分布的变化,训练也快,非常适合理解“对抗”的过程。后面你想换到图像,只需要把网络改成卷积结构即可。

3.1 准备数据和网络结构

先准备一个形状像“双月牙”的二维数据集。真实分布是两份高斯分布的混合,互不重叠,这很像高维图像分布重叠很少的情况,可以顺便感受一下原始GAN在这种设置下的困难。

import torch import torch.nn as nn import torch.optim as optim import numpy as np import matplotlib.pyplot as plt torch.manual_seed(42) def make_real_data(n_samples=1000): t = torch.rand(n_samples) * 2 * np.pi x1 = torch.cat([torch.cos(t) + 0.2 * torch.randn(n_samples), torch.sin(t) + 0.2 * torch.randn(n_samples)], dim=1) x2 = torch.cat([-torch.cos(t) - 1.0 + 0.2 * torch.randn(n_samples), -torch.sin(t) + 0.5 + 0.2 * torch.randn(n_samples)], dim=1) return torch.cat([x1, x2], dim=0)

生成器输入是2维噪声,输出是2维坐标。网络不要太深,否则对简单数据反而难训练。

class Generator(nn.Module): def __init__(self, noise_dim=2, hidden_dim=32): super().__init__() self.net = nn.Sequential( nn.Linear(noise_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, 2) ) def forward(self, z): return self.net(z) class Discriminator(nn.Module): def __init__(self, input_dim=2, hidden_dim=32): super().__init__() self.net = 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): return self.net(x)

这里判别器没有接Sigmoid,因为后面用的BCEWithLogitsLoss会把Sigmoid和交叉熵一起算,数值上更稳定。

3.2 训练循环的每个细节

初始化模型和优化器。GAN的原始论文建议使用SGD或Adam,我在小规模数据上习惯用Adam,学习率0.0002,动量参数beta1=0.5。beta1不用默认0.9,是因为0.9容易让训练震荡,0.5更平稳。

关键代码:

G = Generator() D = Discriminator() opt_G = optim.Adam(G.parameters(), lr=0.0002, betas=(0.5, 0.999)) opt_D = optim.Adam(D.parameters(), lr=0.0002, betas=(0.5, 0.999)) criterion = nn.BCEWithLogitsLoss() def compute_gradient_penalty(D, x_real, x_fake): # 可选,WGAN-GP的梯度惩罚,这里先不用 pass real_data = make_real_data(1000) batch_size = 64 num_epochs = 1000 for epoch in range(num_epochs): perm = torch.randperm(real_data.size(0)) for i in range(0, real_data.size(0), batch_size): idx = perm[i:i+batch_size] x_real = real_data[idx] # ---- 更新判别器 ---- z = torch.randn(x_real.size(0), 2) x_fake = G(z).detach() # 关键:生成器这步不参与反向传播 d_real_logit = D(x_real) d_fake_logit = D(x_fake) loss_d = criterion(d_real_logit, torch.ones_like(d_real_logit)) + \ criterion(d_fake_logit, torch.zeros_like(d_fake_logit)) opt_D.zero_grad() loss_d.backward() opt_D.step() # ---- 更新生成器 ---- z = torch.randn(x_real.size(0), 2) x_fake = G(z) d_fake_logit = D(x_fake) loss_g = criterion(d_fake_logit, torch.ones_like(d_fake_logit)) opt_G.zero_grad() loss_g.backward() opt_G.step() if epoch % 100 == 0: print(f"Epoch {epoch} | D loss: {loss_d.item():.4f} | G loss: {loss_g.item():.4f}")

有一个细节特别重要:更新D时,输入x_fake = G(z).detach(),意思是让D的梯度不再传回G。如果忘记detach,你的判别器更新时会把生成器的梯度也算进去,导致生成器在不知情的情况下也被改变了,训练直接乱掉。另外,生成器更新时要确保判别器参数被冻结,也就是不要在这一步调用opt_D.step()。实际上因为我们交替执行,默认已经冻结了,不用额外requires_grad_(False)也行。

训练结束后,把真实点和生成点画在一张图里:

z = torch.randn(2000, 2) with torch.no_grad(): samples = G(z).numpy() real_np = real_data.numpy() plt.scatter(real_np[:, 0], real_np[:, 1], alpha=0.4, label="real") plt.scatter(samples[:, 0], samples[:, 1], alpha=0.4, label="fake") plt.legend() plt.show()

如果一切正常,你会看到生成点慢慢覆盖真实点的两个月牙形状。如果训练不稳定,你会看到生成点缩在某个角落,这就是模式坍缩的二维版本。

3.3 为什么用二维数据而不是直接上图像

很多同学会跳过二维数据直接跑MNIST,我一开始也是这么干的,结果一上来就遇到“loss正常但图片全是噪点”的怪现象,根本分不清是代码写错了还是调参问题。后来先在二维数据上跑通,把损失曲线和分布覆盖情况都观察清楚了,再迁移到MNIST,就顺利很多。

二维数据和图像生成的核心区别在于:

  • 二维数据的分布可以画出来,你能直接看到生成分布是否覆盖了真实分布;
  • 二维数据训练迭代速度快,几分钟就能试很多组学习率;
  • 二维数据可以让网络结构保持最简单,排除结构设计的干扰,聚焦在对抗训练本身。

把简单场景跑通,再增加复杂度,这是做这类实验最稳妥的路径。

4. 常见问题与排查技巧实录

4.1 损失函数看起来正常,生成样本却很差

这是最迷惑人的问题。我踩过最典型的坑是D loss和G loss都在下降,但生成图片始终像同一种模糊色块。原因是“D被G骗过了”并不等于“G学到了数据分布”。比如G发现了某种高频噪声模式,D无法稳定区分,但人眼一眼看出是噪声。

排查思路:只看每一轮保存的生成图,不看loss。loss只是参考,GAN的loss和生成质量之间没有稳定的单调关系。我会每隔固定迭代次数保存一组样本图,并同时打印当前“D对真实样本的平均输出”和“D对生成样本的平均输出”。如果这两个输出都接近0.5,说明判别器完全失效;如果真实样本输出高、生成样本输出低,但生成质量差,多半是G卡在了某个局部解。

4.2 模式坍缩(Mode Collapse)

表现形式是生成样本多样性和真实数据差很多,甚至生成结果都是一样的。原因前面说过,G找到了“安全区”就不再出来探索。

常用处理手段有几种:

  • 减小G的学习率,或者增大D的学习率,让D更敏感一些,逼G去覆盖更多区域;
  • 使用小批量(Mini-batch Discrimination),在判别器里加入样本之间的相互关系,让D能检测到“这批样本是不是太相似”;
  • 更换损失函数,比如用WGAN-GP,这类方法在缓解模式坍缩上表现明显更好。

我在多次实验里的体感是:如果原始GAN已经出现了“所有生成点聚成一个圆点”的坍缩,再去调学习率往往救不回来,不如直接加噪声扰动G的输入,或者换WGAN损失重跑。

4.3 判别器损失直接变成0,生成器损失卡住不动

这种情况通常说明D太强了,G的梯度完全消失。可以降低D的学习率,或减少D的更新次数,还可以把真实样本标签改成平滑值,比如0.9而不是1.0(标签平滑)。标签平滑的作用是防止D对真实样本输出过于自信,从而留一些“余地”给G传递梯度。

我在二维数据上试过,标签平滑确实能让训练更稳,但代价是收敛变慢。如果只是为了完成一个Demo,不需要太在意,直接按0.9/0.1设置即可。

4.4 训练震荡、loss忽高忽低

GAN的训练本质上是一个动态博弈,震荡是常态。只要生成质量在稳步改善,loss波动不需要过度干预。如果震荡到完全发散,可以先尝试:

  • 减小学习率,优先调G的学习率;
  • 给D的输入加少量高斯噪声,并在训练过程中逐步减小噪声幅度;
  • 把网络里的ReLU换成LeakyReLU,避免D的梯度在负半轴完全消失;
  • 增加Batch Size,让梯度估计更稳定。

4.5 常见问题速查表

现象可能原因快速处理
G loss不降D太强或梯度消失调低D学习率、标签平滑
D loss接近0D过拟合降低D能力、增加dropout
生成图像单一模式坍缩换WGAN-GP、减小G学习率
生成图像全是噪声训练发散降低学习率、检查数据归一化
训练震荡严重博弈不平衡减小lr、增大batch
真实样本和假样本D输出都接近0.5D失去判别力重置D、增大D更新频率

5. 从Basic Idea到实际项目:评估和扩展方向

5.1 怎么判断GAN训练得好不好

课程里只讲了理论和训练流程,没有详细讲评估。实际项目中,我们不可能每次都靠肉眼看图,所以出现了两个常用指标。

Inception Score(IS):把生成的图片送到预训练分类网络里,看分类结果是否明确且多样。分数高意味着“每个样本类别清晰,总体类别丰富”。但它依赖预训练模型,不擅长处理类别分布不均匀的数据。

FID(Fréchet Inception Distance):在特征空间里计算真实样本和生成样本的特征分布距离,数值越低越好。它在实践里比IS更常用,也更贴近人眼感知。不过FID需要足够的样本数(通常几千张),否则估计偏差很大。

使用指标时有一个很常见的误区:指标高不一定生成质量好。某些RGB色块可能骗过低层的特征分布统计,但人眼很容易看穿。所以最终评估还是“指标 + 人工抽样”双管齐下。

5.2 从原始GAN到实际可用的变体

原始GAN是“基本想法”,但放到真实项目里,几乎没有人直接训练原始GAN。原因前面都提到过:不稳定、易坍缩、梯度消失。下面几个变体是课程后续大概率会讲到的重点:

DCGAN:用卷积神经网络替代全连接网络,并且加入批归一化(Batch Normalization),为图像生成提供了稳定基础。很多项目到现在都以DCGAN作为基线。

WGAN / WGAN-GP:把损失从JS散度换成推土机距离,配合权重裁剪(后来改进为梯度惩罚)缓解训练不稳定问题。我从原始GAN切到WGAN-GP之后,明显觉得训练“听话”多了。

CGAN(条件GAN):在生成器和判别器里额外输入类别标签或文字向量,让生成过程变得可控。比如指定生成“猫”而不是“所有动物”。

CycleGAN:用于图到图的转换,不需要成对的训练样本,像风格迁移、马变斑马这类任务就是它的经典应用。它其实是在GAN基础上加入了循环一致性损失。

对于初学者,我的建议路线是:先把原始GAN的“Basic Idea”彻底弄明白,比如为什么用交叉熵、为什么最优判别器是一个比值、为什么原始损失在早期梯度小;然后马上去复现DCGAN的pytorch官方示例,感受一下“看起来更复杂的网络反而训练更稳”。不要一上来就追最新的模型,基础不牢的话,后面出了问题你连从哪调起都不知道。

5.3 如何把GAN的思路迁移到自己的项目里

如果你的任务是生成图像、音频或表格数据,核心流程大概是:

  • 确定数据维度与形状,归一化到合适范围;
  • 搭建Generator(上采样结构)和Discriminator(下采样结构);
  • 设计适当的潜在变量维度(图像一般用100维或128维高斯噪声);
  • 使用Adam优化器,学习率从0.0002起步;
  • 训练时每N轮保存样本,观察生成效果;
  • 生成效果稳定后,再用FID等指标做正式评估。

要注意的是,GAN不适合所有生成任务。如果你的任务对分布覆盖要求高,比如生成“多样化但每一类都不出错”的数据,GAN的表现可能不如扩散模型(Diffusion Model)稳定。扩散模型目前在图像生成质量上有优势,但采样速度慢、计算开销大。选型时应该看约束条件,而不是跟风。

最后再说一点我的体会

我从跑第一个GAN到现在,最大的感受是:GAN的代码本身并不复杂,复杂的是“什么时候该信任损失函数,什么时候不该”。它不像分类任务那样有明确的目标函数,PK方式决定了你很难用一个数字判断好坏。公开课第74讲里提出的基本想法,可能就是几句话、几个公式,真正要内化,还是得靠一遍遍改实验、观察生成结果。给新手一个很朴素的建议:先在小规模、可可视化的数据上把原始GAN跑通,跑成再上图片。这个“慢就是快”的路径,能帮你避开很多玄学问题。哪怕最后你用了更时髦的模型,这段对生成对抗机制的直觉,依旧是不可替代的地基。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 17:26:09

秒杀场景库存扣减方案深度拆解:从SQL到Redis+Lua的演进与实践

面试进行到第三轮,面试官轻描淡写丢过来一句:“你们项目做过秒杀吗?说说库存扣减策略。”这个问题几乎每个做后端的人都会遇到,但真正能答出层次的人不多。大部分候选人第一反应是背方案,从“乐观锁”背到“Redis预减库…

作者头像 李华
网站建设 2026/10/27 17:25:27

TensorFlow实战:用全连接神经网络拟合股票收盘均价

1. 从一条均线说起:这个项目到底在做什么 股票收盘均价这条线,看起来平平无奇,但真拿它当神经网络的拟合目标,你会发现里面藏着不少门道。这个项目的核心思路很直接:用 TensorFlow 搭建一个最基础的前馈神经网络&#…

作者头像 李华
网站建设 2026/10/19 14:36:35

离线知识问答系统实战:企业文档解析与RAG检索生成

你的公司里是不是也有这么一批文件——扫描存档的合同、供应商发来的PDF手册、几个部门攒了五年的制度文档,占着共享文件夹几个G的空间,真到用的时候谁也找不到里面写了什么。我做过一个项目,把这一堆“沉睡资产”捞了出来,接上本…

作者头像 李华
网站建设 2026/10/21 10:45:20

从零搭建轻量级地图可视化组件lingbot-map:设计、实现与性能优化

1. 从零搭建一个轻量级地图可视化组件:lingbot-map 项目全记录 前阵子在做数据看板的时候,产品那边提了个需求:要在一个页面里同时展示设备分布、区域热度和轨迹回放,而且要求加载快、能离线跑、还得方便嵌入到已有的前端工程里。…

作者头像 李华
网站建设 2026/10/23 5:06:44

航天器动力学建模与制导算法实践

1. 神话与科技的千年对话 人类仰望星空的历史,就是一部不断打破认知边界的历史。从嫦娥奔月的传说到万户飞天的尝试,再到现代航天器的精准入轨,我们正在用代码重写那些曾经被视为神迹的幻想。在航天控制中心,我常看到老一辈工程师…

作者头像 李华