简介基于Python的生成对抗网络GAN资源包面向机器学习初学者、计算机视觉方向学生及对生成模型感兴趣的开发者。网络由判别模型与生成模型组成判别模型以图像为输入输出真假概率大于0.5为真小于0.5为假生成模型则以一组随机数Z为输入输出一张完整图像两者协作完成对抗训练。资源共11个文件、整体约630KB包含gan.py源码、500次与1000次迭代效果图png、docx说明文档、README.md以及license、gitignore等工程配置py文件为完整网络实现png直观对比不同迭代阶段的生成效果docx与md梳理原理目录清晰便于按需查阅。已有359人参与学习下载后可根据README引导运行源码结合图文笔记掌握GAN的网络结构、训练逻辑与结果分析方法从搭建网络到观察图像逐步清晰完整走通一次生成对抗实验是入门生成对抗网络的轻量实用资料。1. 生成对抗网络GAN用 Python 做图像生成这个方向到底解决什么问题如果你手里有一批图片想让程序学会“照着样子造新图”——不复制、不拼接而是像人一样理解风格后重新画出来那基于 Python 的生成对抗网络GAN就是最直接的方案之一。GAN 的核心思路是让两个神经网络互相较劲一个负责伪造图像一个负责分辨真假在对抗中共同变强。这个 zip 里的内容指向的正是这样一套可运行的 Python 实现适合给要做图像生成、数据增强、图像修复或风格迁移的工程师做起点。很多初学者一上来就收了一堆 GAN 代码但最难的不是把训练跑起来而是跑起来之后面对一堆玄学问题不知所措。今天这篇笔记从环境搭建说到训练调参再帮你绕过几个经典翻车点最后给出一套能验证效果的小技巧。无论你之前有没有接触过深度生成模型这套方案都能在半天内给你一个看得见结果的模型。2. GAN 的核心结构与选型理由从对抗博弈到动手前的三个关键决策2.1 生成器与判别器的对抗关系损失函数怎么写梯度更新朝哪跑GAN 的对抗结构可以这样理解生成器网络 G 接收一个随机噪声向量 z输出一张假图判别器网络 D 接收一张图输出一个 0 到 1 之间的分数表示“这张图是真的”的概率。训练的过程就是让 G 尽量骗过 D同时让 D 尽量识破 G。理想情况下两者的能力同步上升最后 G 生成的图像足以以假乱真。这个过程的损失函数在原始 GAN 论文里被写成 min-max 问题形式是 V(D,G) E[log D(x)] E[log(1 - D(G(z)))]。用大白话说D 想让真图的输出接近 1、假图的输出接近 0所以它的损失是“真图被判错的损失 假图被判对的损失”G 想让假图被 D 判定为真所以它的损失是“假图被判成假图的损失”。两者交替更新生成器最终学会把随机噪声映射到目标数据分布上。在实现时有个细节值得注意生成器的损失不能直接用 min-max 原式里的 log(1 - D(G(z)))因为它的梯度在训练早期太小会导致生成器学得很慢。更常见的做法是把生成器的目标改成“最大化 D 判定假图为真的概率”也就是让 G 的损失变成 E[log(D(G(z)))] 的负值实践上则是把假图的标签设成 1 去计算 BCE 损失。这个改动几乎所有实现都在用它在早期给了生成器足够的梯度强度训练收敛速度会明显变快。如果你用的是 PyTorch还有一个建议不要在判别器最后一层接 Sigmoid 然后用普通的 BCELoss 计算损失。更推荐的做法是判别器最后输出一个未经过 Sigmoid 的 logit配合 torch.nn.BCEWithLogitsLoss 一次完成 Sigmoid 和损失计算。这么做的好处是数值稳定性更好避免了中间层输出经过 Sigmoid 后梯度消失的问题。后面给的代码统一按这个方式写新手不用再纠结激活函数放哪里。2.2 选型DCGAN 还是 WGAN-GP图像任务从哪套结构起步GAN 的变体多得让人眼花缭乱但做图像生成时常见的选择就两条路DCGAN 和 WGAN-GP。DCGAN 是卷积版 GAN 的里程碑方案核心贡献是把生成器和判别器全部改成卷积结构同时引入 BatchNorm 和 LeakyReLU 来稳定训练。它对 64x64 以下的图像尺寸表现得相当可靠训练速度快显存占用也低是新手跑通全流程的首选配置。WGAN-GP 换掉了判别器的评判方式不再输出真假二分类概率而是输出一个实数作为真实度分数配合梯度惩罚项来约束判别器的 Lipschitz 连续性。它的优势是训练曲线更平滑模式崩塌问题少一些代价是每步需要额外计算梯度惩罚训练时间大约多 30% 到 50%而且对超参数更敏感。如果你只是做验证性质的项目我建议先用 DCGAN 跑通再上 WGAN-GP 对比效果。还有一个关键决策是用什么框架。这篇笔记以 PyTorch 为例因为 PyTorch 在动态图模式下可以自由控制每一次前向传播的细节这对 GAN 这种需要交替更新两个网络的场景非常合适。TensorFlow 的 Keras 接口也能写 GAN但自定义训练循环时写法相对绕一些。至于版本只要用 1.13 以上或 2.x 版本的 PyTorch 都行不需要刻意追求最新版。动手之前三个参数值得多花两分钟确认。第一是生成器输入的噪声维度常见值是 100 或 128维度太低会导致生成图像的多样性差第二是图像分辨率起步阶段用 28x28 或 32x32 比直接上 256x256 成功率高得多因为高分辨率需要更深的网络和更多的训练步数第三是学习率DCGAN 原论文推荐 2e-4这个值比常规分类任务的 1e-3 低一个量级是稳定训练的前提之一。把这些选型定下来代码实现就能写得很顺手。3. 用 PyTorch 跑通一个最小 DCGAN从 MNIST 到自定义数据的完整代码3.1 准备数据和定义网络把 MNIST 装进 DataLoader写出最小生成器与判别器为了确保这套代码开箱即用我们先用 MNIST 手写数字作为目标数据集。MNIST 是 28x28 的单通道灰度图规模小、类别明确是验证 GAN 是否跑通的最佳环境。数据加载部分用 torchvision 自带的数据集接口即可注意要把像素值从 0 到 1 的范围转换成 -1 到 1因为生成器最后一层用 Tanh 激活它的输出范围就是 -1 到 1。import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms # 把像素值从 [0,1] 归一化到 [-1,1]和生成器的 Tanh 输出范围对齐 transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)) ]) dataset datasets.MNIST(rootdata, trainTrue, transformtransform, downloadTrue) dataloader DataLoader(dataset, batch_size64, shuffleTrue, num_workers2, pin_memoryTrue) print(f数据集大小: {len(dataset)}, 每个 batch 的 shape: {next(iter(dataloader))[0].shape})这里有几个数据加载的细节Normalize((0.5,), (0.5,)) 的含义是把像素值先归一到 0-1再套公式 (x - 0.5) / 0.5结果正好落到 -1 到 1。num_workers 在 Linux 下可以设为 2 到 4 加速数据读取在 Windows 下建议设成 0 或 1避免多进程加载报错。pin_memory 在 GPU 环境下能减少 CPU 到 GPU 的拷贝时间数据量小的时候影响不大但养成习惯没坏处。生成器我们采用 DCGAN 的结构先通过全连接层把噪声向量扩展到 7x7x128 的特征图谱再用两次转置卷积上采样到 28x28。转置卷积的 stride2 表示每次把特征图尺寸放大一倍padding 需要配合 kernel_size 计算确保尺寸正好从 7x7 变 14x14 再变 28x28。class Generator(nn.Module): def __init__(self, latent_dim100): super().__init__() self.latent_dim latent_dim self.net nn.Sequential( # 噪声 z 先经过全连接层展开成 7x7x128 的特征图 nn.Linear(latent_dim, 7 * 7 * 128), nn.BatchNorm1d(7 * 7 * 128), nn.ReLU(True), nn.Unflatten(1, (128, 7, 7)), # 第一次上采样: 7x7 - 14x14 nn.ConvTranspose2d(128, 64, kernel_size4, stride2, padding1), nn.BatchNorm2d(64), nn.ReLU(True), # 第二次上采样: 14x14 - 28x28 nn.ConvTranspose2d(64, 1, kernel_size4, stride2, padding1), nn.Tanh() ) def forward(self, z): return self.net(z)这一段代码隐藏了三个关键点。第一个是 BatchNorm1d 和 Unflatten 配合的写法Linear 输出是一个二维张量形状是 (batch, 7x7x128)BatchNorm1d 对这个维度做归一化然后 Unflatten 把最后一维拆成三个维度变成 (batch, 128, 7, 7)。第二个是每层卷积之后的 BatchNorm2d它让每层输入分布稳定下来防止训练中后期梯度震荡。第三个是最后的 Tanh 激活所有 DCGAN 生成器的输出必须用 Tanh因为它把输出限制在 -1 到 1和数据预处理对齐如果用 Sigmoid 输出范围只有 0 到 1和数据的 -1 到 1 范围不匹配生成图像会整体发灰。判别器是生成器的镜像结构仍然用卷积提取特征但激活函数换成 LeakyReLU斜率为 0.2。LeakyReLU 的作用是给负半轴保留一个小的梯度避免神经元死亡。判别器的最后输出是一个标量 logit不做 Sigmoid留给 BCEWithLogitsLoss 内部处理。class Discriminator(nn.Module): def __init__(self): super().__init__() self.net nn.Sequential( # 输入 1x28x28输出 64x14x14 nn.Conv2d(1, 64, kernel_size4, stride2, padding1), nn.LeakyReLU(0.2, True), # 输出 128x7x7 nn.Conv2d(64, 128, kernel_size4, stride2, padding1), nn.BatchNorm2d(128), nn.LeakyReLU(0.2, True), nn.Flatten(), nn.Linear(7 * 7 * 128, 1) ) def forward(self, x): return self.net(x)判别器里有一件事初学者容易漏掉第一层卷积后面不要加 BatchNorm。原因是输入图像只有一个通道且值域固定不需要归一化加了 BN 反而可能在训练早期让判别器过于依赖输入统计量导致生成器难以学到有效梯度。从第二层开始加 BN 则是为了减轻判别器内部的特征分布漂移。如果你在训练中发现判别器的 loss 降得太快而生成器一直原地不动可以检查一下是否在第一层误加了 BN。3.2 训练循环与关键参数Adam 的 betas、lr、batch size 和每一步的更新顺序网络定义好后训练循环的写法决定了训练是否稳定。GAN 的训练分为两步先训练判别器再训练生成器。判别器更新时要保证生成器不参与梯度传播做法是对假图调用 detach() 切断计算图生成器更新时则要利用判别器当前的状态来计算损失。这个顺序不能颠倒否则生成器会利用判别器上一轮已经更新过的梯度信息导致两边的训练步调错乱。import torch.optim as optim device torch.device(cuda if torch.cuda.is_available() else cpu) G Generator(latent_dim100).to(device) D Discriminator().to(device) lr 2e-4 beta1 0.5 criterion nn.BCEWithLogitsLoss() opt_G optim.Adam(G.parameters(), lrlr, betas(beta1, 0.999)) opt_D optim.Adam(D.parameters(), lrlr, betas(beta1, 0.999)) num_epochs 20 for epoch in range(num_epochs): for i, (real_imgs, _) in enumerate(dataloader): real_imgs real_imgs.to(device) cur_batch real_imgs.size(0) # 1. 训练判别器 z torch.randn(cur_batch, 100, devicedevice) fake_imgs G(z) D.zero_grad() real_pred D(real_imgs).view(-1) fake_pred D(fake_imgs.detach()).view(-1) # 真图标签设为 0.9使用 one-sided label smoothing 防止判别器过于自信 real_label torch.full((cur_batch,), 0.9, devicedevice) fake_label torch.full((cur_batch,), 0.0, devicedevice) loss_D_real criterion(real_pred, real_label) loss_D_fake criterion(fake_pred, fake_label) loss_D loss_D_real loss_D_fake loss_D.backward() opt_D.step() # 2. 训练生成器 z torch.randn(cur_batch, 100, devicedevice) fake_imgs G(z) G.zero_grad() fake_pred D(fake_imgs).view(-1) # 生成器希望假图全部被判为真目标标签设为 1 loss_G criterion(fake_pred, torch.full((cur_batch,), 1.0, devicedevice)) loss_G.backward() opt_G.step() if i % 200 0: print(fEpoch [{epoch}/{num_epochs}] Batch [{i}/{len(dataloader)}] fLoss_D: {loss_D.item():.4f} Loss_G: {loss_G.item():.4f})代码里的参数值得逐项说明。lr 设成 2e-4 是 DCGAN 原论文给出的推荐值比常规 Adam 的默认值小不少目的是让生成器和判别器的更新幅度都保持温和。Adam 的 betas 参数设置成 (0.5, 0.999)其中第一个 0.5 控制梯度一阶矩的衰减速度默认值是 0.9在 GAN 训练里建议调成 0.5原因是降低历史梯度的惯性让参数更新对当前的梯度更敏感训练震荡会小一些。batch size 设 64 是平衡稳定性与显存占用的选择小于 16 时 BatchNorm 效果会变差大于 256 时训练会明显变慢且容易让判别器过早饱和。训练 5 到 10 个 epoch 之后可以开始把生成器输出的假图保存下来看一眼。保存图片时注意要从 -1 到 1 的范围反归一化回 0 到 255 再写文件直接用 Tensor 绘图会导致图片颜色和实际效果对不上。下面是保存假图的辅助代码import torchvision.utils as vutils import matplotlib.pyplot as plt def save_fake_samples(G, epoch, device, save_pathfake_samples.png): G.eval() with torch.no_grad(): z torch.randn(16, 100, devicedevice) fake G(z).cpu() # 从 [-1,1] 反归一化到 [0,1] fake (fake 1) / 2 grid vutils.make_grid(fake, nrow4, padding2) plt.imsave(save_path, grid.permute(1, 2, 0).numpy()) G.train()反归一化那行代码是新手最容易忽略的。如果生成器输出范围是 -1 到 1直接用 plt.imsave 保存会得到一张几乎全黑的图因为 0 对应的是 128 的灰度值而不是纯黑大部分像素落在负半轴后整体看起来就偏暗。加上 (fake 1) / 2 之后数值变成 0 到 1图片显示就正常了。另外一个习惯建议尽早养成每次保存时把 fake 的样本网格一起写入同名字的文件方便后面对比不同 epoch 的生成效果。4. 训练 GAN 的 5 个常见坑模式崩塌、戏剧性梯度消失与 NaN 的排查基本功不管用哪套结构GAN 的训练过程都有一些绕不开的坑这里把频率高、现象明显的几个整理成清单。每一条都先描述现象再给原因和解决思路方便你按图索骥排查。坑一生成器输出几乎不变化所有样本长得像同一张图。现象是训练到中途保存的假图里每个数字的形态都差不多只有细微差别。这大概率是模式崩塌即生成器发现只要欺骗判别器就能获得低损失于是选择只生成少数几个“容易过关”的样本分布放弃了覆盖真实数据的多样性。解决思路有二一是降低判别器的能力比如减少判别器卷积层数或给判别器加 Dropout让它的判断不那么敏锐二是调整生成器的输入噪声分布把噪声标准差适当调大并确认 latent_dim 不低。如果用到条件信息还要检查标签是否被模型利用起来。最直接的缓解手段是用后面会说到的 minibatch discrimination 或用 WGAN-GP 替换训练目标。坑二判别器 loss 骤降到接近 0生成器 loss 反而不断上升。现象是训练前几十个迭代里判别器准确率就接近 100%生成器的假图概率几乎为 0说明判别器变强得太快生成器已经完全骗不过它。常见原因有学习率过大、BatchNorm 放在判别器第一层、以及真图标签一直用 1.0 导致判别器对真图的输出过于自信。解决办法是把判别器的学习率单独调低比如拿到 1e-4同时给真图标签改成 0.9 做 one-sided label smoothing让判别器不会为了追求极端输出而丢掉泛化能力。如果仍然不行可以把判别器的卷积层减少一层降低它的拟合能力。坑三训练到一半 loss 突然变成 NaN。现象是 loss 在几百次迭代后直接变成 nan然后整个训练报废。常见原因有三个学习率设置过高导致梯度爆炸生成器或判别器的某些层出现除以 0 的情况通常发生在 BatchNorm 的方差变为 0 时还有就是 loss 函数里用了 log(0)。解决思路是先检查学习率降到 1e-4 或更低再检查代码里所有涉及除法的地方特别是自定义损失时避免分母过小最后可以临时把 BatchNorm 换成 InstanceNorm 验证是否 BN 引起数值问题。训练脚本里加一条判断逻辑是救命习惯检测 loss 是 NaN 时立即停止训练并保存当前权重避免后续把模型参数彻底覆盖。坑四生成图像模糊像隔了一层雾。现象是假图能看出数字轮廓但边缘模糊、对比度低。通常原因有两个一是生成器网络深度不够无法学习足够的细节特征这种情况需要增加卷积层的通道数或层数二是数据归一化范围与生成器输出不匹配比如数据归一化到 -1 到 1但生成器最后一层用了 Sigmoid 输出 0 到 1生成器被迫压缩自己输出到 0-1 范围细节自然丢失。对照 2.1 的代码确认生成器最后一层是 Tanh并把数据归一化范围对齐。图像模糊还有一个容易忽略的因素是训练时间不够20 个 epoch 之后才开始明显变清晰的情况并不少见先排除这个再改网络结构。坑五判别器过拟合验证时表现远好于生成器实时对抗的表现。注意这里说的不是常规分类任务的过拟合而是判别器只记住了训练集中的具体样本对新出现的假样本一打一个准导致生成器无法从判别器那里获得有意义的梯度信号。训练集太小是最常见的原因建议至少准备几千张图片再开始训练。数据增强对 GAN 训练的作用相对有限但可以在真实图片上做随机水平翻转和随机裁剪相当于扩充有效样本分布。如果条件允许把训练集的类别数量减少到 3 到 5 个能显著加快验证迭代速度确认效果后再逐步扩大类别。5. 把生成质量再抬一个台阶cGAN 引入标签控制FID 和人工抽查做验收5.1 从 DCGAN 到 cGAN给生成器输入加上条件信息改动最小DCGAN 生成器只能生成随机数字无法指定生成的内容是 3 还是 7。如果你的任务需要生成指定类别的样本比如“只生成数字 3 的图片”那就需要改成条件生成对抗网络。cGAN 的核心改动是把标签信息作为额外输入同时注入生成器和判别器。生成器把标签编码成一个嵌入向量再和噪声向量拼接判别器把标签也编码后与图像特征拼接从而判断“这张图是否匹配这个标签”。class ConditionalGenerator(nn.Module): def __init__(self, num_classes10, latent_dim100, embed_dim10): super().__init__() self.label_embed nn.Embedding(num_classes, embed_dim) self.net nn.Sequential( nn.Linear(latent_dim embed_dim, 7 * 7 * 128), nn.BatchNorm1d(7 * 7 * 128), nn.ReLU(True), nn.Unflatten(1, (128, 7, 7)), nn.ConvTranspose2d(128, 64, kernel_size4, stride2, padding1), nn.BatchNorm2d(64), nn.ReLU(True), nn.ConvTranspose2d(64, 1, kernel_size4, stride2, padding1), nn.Tanh() ) def forward(self, z, labels): label_emb self.label_embed(labels) z torch.cat([z, label_emb], dim1) return self.net(z)这个改动的关键在 torch.cat([z, label_emb], dim1)把 100 维噪声和 10 维标签向量拼成 110 维输入生成器在此基础上做映射。Embedding 层的 num_classes 要跟你的标签总数对上类别从 0 开始编号。如果你训练的不是 MNIST 而是自定义数据集需要用 LabelEncoder 把字符串标签先转成整数。训练循环的唯一改动是每个 batch 都要把真实图片的标签拿出来分别传给生成器和判别器。条件信息让模型学会了类别和图像特征之间的对应关系生成的数字类别可控迭代效率明显提升。5.2 乘着 FID 和人工抽查的升降判断模型是否真在变好loss 曲线在 GAN 训练里只能说明训练没有崩不能说明生成质量变好。要确认生成器是否真的学到了数据分布建议同时做两项验收。第一项是计算 FID 分数。FID 用 InceptionV3 网络的中间特征来比较真实图集和生成图集的特征分布差异分数越低说明两套图像的分布越接近。FID 需要至少几百张生成图参与计算所以训练结束后一次生成 1000 张假图再统计。import numpy as np from torchmetrics.image.fid import FrechetInceptionDistance # 把两张图集都转换为 uint8 格式范围 0-255 fid FrechetInceptionDistance(feature2048) real_imgs_uint8 (real_imgs.cpu() * 255).to(torch.uint8) fake_imgs_uint8 (fake_imgs.cpu() * 255).to(torch.uint8) fid.update(real_imgs_uint8, realTrue) fid.update(fake_imgs_uint8, realFalse) print(fFID: {fid.compute().item()})torchmetrics 提供了开箱即用的 FID 实现省去自己调 Inception 参数的麻烦但注意它默认预处理要求输入是 uint8 且范围 0-255你要手动把 -1 到 1 的 tensor 先加 1 除以 2 再乘以 255。在 MNIST 上 FID 到 20 以下就算基本可用到 10 以下说明生成质量已经相当接近真实数据。第二项是人工抽查每 5 个 epoch 保存一张固定噪声向量生成的图片网格重点是每次使用同一个随机噪声种子这样才能对比生成器在不同训练阶段的效果变化。人工看的时候重点观察数字是否清晰、边缘是否有毛刺、是否所有网格里的数字都各不相同。如果 FID 在下降同时网格图里样本多样性也在增加那基本可以放心继续训练。6. 固定随机种子与训练过程留痕让每一次 GAN 实验都可以复现GAN 的随机性比普通分类模型大得多同一个脚本不固定随机种子两次训练出来的效果可能天差地别。调参时如果不固定种子你没法判断效果变化是因为改动参数还是纯粹运气。所以习惯上我会在脚本入口统一固定所有随机源import random import numpy as np import torch def seed_everything(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False seed_everything(42)这段代码里 torch.backends.cudnn.deterministic True 会让 cuDNN 选择确定性的卷积算法torch.backends.cudnn.benchmark False 则是禁止按输入形状自动选择最优算法后者如果不关即使前一个设为 True算法选择仍然可能引入非确定性。代价是推理速度略有下降但对训练影响完全可接受。另外给 dataloader 设置 generator 参数也能进一步保证数据顺序的可复现性做法是 DataLoader(..., generatortorch.Generator().manual_seed(42))。我的教训是刚开始调 GAN 时总想一次性看到最终效果于是从头跑到尾不存中间结果结果训练跑崩了直接损失一整晚的算力。后来改成每 5 个 epoch 保存一次生成图和模型权重用文件名带 epoch 和 FID 分数区分任何一次实验的失败都能精确回滚到上一个状态。现在即使是凌晨跑批量实验我也可以安心第二天起床看结果分档效果好的留下效果差的一键删除。这套做法不仅适用于 GAN也适用于其他生成模型的调试周期。希望帮到你。本文还有配套的精品资源点击获取
