生成对抗网络的思想源自博弈论中的二人零和游戏。生成器G接收一段随机噪声,输出一张合成图像;判别器D接收真实图像或生成图像,输出一个概率值,表示输入属于真实样本的可能性。生成器的目标是最小化判别器正确识别的概率,判别器的目标则是最大化对真实样本和生成样本的区分能力。二者交替优化,最终生成器输出的分布逐步逼近真实数据分布。

从数学上看,经典GAN的训练过程可以写成minimax损失函数。给定真实数据分布p_data和噪声分布p_z,生成器将噪声z映射为G(z),判别器对真实样本x输出D(x),对生成样本输出D(G(z))。理想状态下,判别器无法分辨两类输入,输出概率稳定在0.5附近,意味着生成器已经学到了真实分布。训练时不能等到判别器完全收敛再更新生成器,否则判别器会迅速过强,导致生成器梯度消失。通常的做法是每一轮迭代中先更新判别器若干次,再更新一次生成器,让对抗保持在相对均衡的状态。
一、GAN的基本结构与训练过程
基础GAN由两个可微函数组成,实现上通常是全连接网络或多层感知机。输入图像的维度可能很高,例如MNIST手写数字是28乘28的灰度图,展开后为784维向量;生成器从100维噪声开始,通过多层线性变换和激活函数逐步映射到784维。判别器方向相反,接收784维输入,最终输出一个标量概率。为了让生成图像的像素值落在合理区间,生成器最后一层常使用Tanh激活函数;判别器最后一层使用Sigmoid,将输出压缩到0到1之间。
训练循环的核心是交替计算两类损失。判别器希望真实样本被标为1,生成样本被标为0,因此使用二元交叉熵损失。生成器则希望判别器把生成样本误判为真实样本,所以在生成器损失中把生成样本的标签故意设为1。下面的代码展示了这一过程。需要注意的是,更新生成器时应当使用fake_imgs.detach()来计算判别器损失,避免生成器梯度反向传播到判别器内部,保证两个优化器各管各的参数。
import torch
import torch.nn as nn
class Generator(nn.Module):
def __init__(self, z_dim=100, img_dim=784):
super().__init__()
self.net = nn.Sequential(
nn.Linear(z_dim, 256),
nn.ReLU(),
nn.Linear(256, img_dim),
nn.Tanh()
)
def forward(self, z):
return self.net(z)
class Discriminator(nn.Module):
def __init__(self, img_dim=784):
super().__init__()
self.net = nn.Sequential(
nn.Linear(img_dim, 256),
nn.LeakyReLU(0.2),
nn.Linear(256, 1),
nn.Sigmoid()
)
def forward(self, img):
return self.net(img)
G = Generator()
D = Discriminator()
loss_fn = nn.BCELoss()
optimizer_G = torch.optim.Adam(G.parameters(), lr=0.0002)
optimizer_D = torch.optim.Adam(D.parameters(), lr=0.0002)
for epoch in range(200):
for real_imgs, _ in dataloader:
batch_size = real_imgs.size(0)
real_imgs = real_imgs.view(batch_size, -1)
z = torch.randn(batch_size, 100)
fake_imgs = G(z)
real_loss = loss_fn(D(real_imgs), torch.ones(batch_size, 1))
fake_loss = loss_fn(D(fake_imgs.detach()), torch.zeros(batch_size, 1))
d_loss = real_loss + fake_loss
optimizer_D.zero_grad()
d_loss.backward()
optimizer_D.step()
z = torch.randn(batch_size, 100)
fake_imgs = G(z)
g_loss = loss_fn(D(fake_imgs), torch.ones(batch_size, 1))
optimizer_G.zero_grad()
g_loss.backward()
optimizer_G.step()
这段代码刻意保持简单,适合理解最小训练单元。实际处理图像时,全连接网络会丢失空间结构,参数量也会迅速膨胀。以128乘128的彩色图像为例,单张图片展开后接近5万维,线性层很容易挤爆显存。因此后续研究转向卷积结构,用局部感受野和参数共享来提升效率,这也是DCGAN出现的原因。
二、DCGAN:用卷积网络提升图像生成质量
DCGAN全称是深度卷积生成对抗网络,它在生成器和判别器中引入了卷积与转置卷积。生成器不再接收一维向量后直接线性映射,而是把噪声向量重塑成四维张量,再通过一系列ConvTranspose2d逐步上采样。判别器则使用普通卷积逐步下采样,最后通过全局池化或展平得到单个概率。相比多层感知机,卷积结构能够捕捉局部纹理、边缘和颜色变化,生成的图像更连续,不容易出现杂乱噪点。
DCGAN还提出了一组稳定的架构经验。生成器除了输出层使用Tanh外,其余层使用ReLU;判别器使用LeakyReLU替代ReLU,避免神经元死亡。批归一化被用在生成器和判别器的中间层,帮助梯度稳定流动。优化器方面,Adam的动量项系数通常设为0.5而不是默认的0.9,学习率取0.0002。下面的生成器实现展示了标准DCGAN的上采样过程,输入是100维噪声,输出是64乘64的RGB图像。
import torch
import torch.nn as nn
class DCGenerator(nn.Module):
def __init__(self, z_dim=100, ngf=64):
super().__init__()
self.main = nn.Sequential(
nn.ConvTranspose2d(z_dim, ngf * 8, 4, 1, 0, bias=False),
nn.BatchNorm2d(ngf * 8),
nn.ReLU(True),
nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 4),
nn.ReLU(True),
nn.ConvTranspose2d(ngf * 4, ngf * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 2),
nn.ReLU(True),
nn.ConvTranspose2d(ngf * 2, ngf, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf),
nn.ReLU(True),
nn.ConvTranspose2d(ngf, 3, 4, 2, 1, bias=False),
nn.Tanh()
)
def forward(self, z):
return self.main(z.view(z.size(0), z.size(1), 1, 1))
训练DCGAN时,数据集通常需要先裁剪到固定尺寸,例如CelebA人脸数据集常使用64乘64或128乘128。判别器最后一层不使用Sigmoid也常见,改为直接输出logits,损失函数选择BCEWithLogitsLoss,这样数值更稳定。另一个技巧是标签平滑,把真实标签从1改为0.9附近,减少判别器过度自信,让生成器获得更持续的梯度信号。这些修改不会改变GAN的基本原理,却能明显改善收敛速度。
三、风格迁移与CycleGAN:无配对数据的图像翻译
风格迁移的目标是把一张图像的内容保留下来,同时把另一张图像的风格迁移上去。传统神经风格迁移通常需要一张内容图、一张风格图,通过预训练VGG网络提取特征,再优化一张白噪声图,使它的内容特征接近内容图、风格统计量接近风格图。这种方法每次都要重新优化,速度较慢。图像翻译领域更偏向训练一个前馈网络,直接输入内容图输出风格化结果。若存在成对数据,例如同一场景的白天照片和夜晚照片,可以直接用Pix2Pix这类条件GAN训练;但很多情况下成对数据难以获得,例如把照片转换成梵高画作,不可能找到同一场景的真实对应画作。
CycleGAN解决的是无配对图像翻译问题。它训练两个生成器和两个判别器:生成器G_A2B把A域图像转换为B域,G_B2A把B域图像转换回A域。除了对抗损失,CycleGAN引入循环一致性损失:真实A经过G_A2B生成假B,再经过G_B2A应能还原成原来的A。这个约束不依赖成对标签,只要求图像内容在转换过程中不被大幅破坏。风格可以替换,但构图、物体形状和位置要保留。
import torch
import torch.nn as nn
cycle_loss = nn.L1Loss()
fake_B = G_A2B(real_A)
recovered_A = G_B2A(fake_B)
loss_cycle_A = cycle_loss(recovered_A, real_A)
fake_A = G_B2A(real_B)
recovered_B = G_A2B(fake_A)
loss_cycle_B = cycle_loss(recovered_B, real_B)
d_fake_B = D_B(fake_B)
loss_adv_A2B = nn.functional.binary_cross_entropy_with_logits(
d_fake_B, torch.ones_like(d_fake_B)
)
total_loss = loss_adv_A2B + 10.0 * (loss_cycle_A + loss_cycle_B)
循环一致性损失通常使用L1损失,权重取10。实际训练时还会加入身份损失,即把B域真实图像输入G_A2B,输出应尽量保持原样,这能帮助生成器稳定颜色和对比度。CycleGAN的生成器内部常用残差块搭建,输入图像经过下采样、残差变换和上采样,输出与输入尺寸相同。判别器则使用PatchGAN结构,不是输出单个真假概率,而是输出一个N乘N的矩阵,每个元素对应图像局部区域的判断结果。这样更关注纹理细节,对高分辨率风格迁移更友好。
四、训练不稳定与模式崩溃的应对策略
GAN训练中一个典型问题是模式崩溃。生成器可能只学会生成少数几类样本,例如训练手写数字时只输出数字3和8,虽然判别器暂时无法识破,但生成多样性很差。模式崩溃的原因通常是生成器找到了判别器的某个盲区,并反复利用该盲区。解决思路包括使用小批量判别、历史平均、Wasserstein距离等。WGAN把判别器改为评论家,去掉Sigmoid并施加梯度惩罚,能显著缓解训练不稳定。
另一个常见现象是梯度消失。当判别器太强时,它对生成样本给出接近0的概率,生成器损失接近常数,梯度几乎为零。反过来,如果判别器太弱,生成器得不到有效指导,输出会长期停留在噪声状态。为了让双方能力匹配,可以降低判别器学习率,或每更新两次生成器才更新一次判别器。也有实践者会在判别器输入中加入轻微高斯噪声,降低判别器对单个样本的过度自信。
评估GAN的质量同样需要谨慎。单看生成图像容易受主观影响,常用的定量指标包括IS和FID。IS衡量生成图像的类别清晰度和多样性,FID则比较生成图像与真实图像在Inception特征空间的分布距离。FID值越低说明生成分布越接近真实分布。实际项目中,通常会定期保存生成样本,人工观察损失曲线和样本多样性,再结合FID做筛选。掌握这些基础技巧后,再去看StyleGAN、条件GAN、扩散模型等后续工作,会更容易理解它们各自改进的动机和边界。

