Pytorch实战:GAN、CycleGAN与GRAGAN源码实现解析
简介面向深度学习和生成对抗网络方向的工程师与研究者这份资源汇集了基于Pytorch实现的多类GAN项目源码涵盖CycleGAN、GRAGAN、pix2pix、StarGAN、WGAN、DCGAN等主流与变体模型便于对照学习图像生成、风格迁移、跨域转换和超分辨率重建的完整流程。包内共88个文件主体为60个Python源码文件其余包括展示生成效果的png/gif图片、方便获取标准数据集的两个sh下载脚本、依赖环境清单txt以及入门说明md压缩包整体约29.96MB目录按模型划分结构清晰便于快速定位与查阅。已有105人学习下载配套完整流程教程从GAN基本原理入手覆盖Pytorch建模、CycleGAN与GRAGAN的具体实现、模型评估与项目落地等环节并提供数据预处理、训练策略和效果可视化等关键步骤。适合有一定深度学习基础并希望深入GAN代码细节的工程师与研究人员借助源码和文档可以掌握从搭建到调试再到应用的全链路方法。1. 用 Pytorch 实现 GAN这份项目源码的入口在哪里把一个名为使用Pytorch实现的各种GAN-CycleGAN-GRAGAN等的项目 zip 解压你会看到一堆.py文件、配置目录和一份流程文档。这类 Pytorch 实战项目源码里最有价值的往往不是那几十个网络类而是训练主循环、损失配比、学习率策略这些只有跑过才知道怎么调的流程细节。GAN 网络与普通分类模型最大的区别是训练目标不稳定生成器和判别器互相追逐任何一边参数不对损失曲线就会失控。这篇文章按做这类项目最常见的工程顺序展开先搭 Pytorch 下 GAN 的公共骨架再实现 CycleGAN 的无配对图像翻译然后实现带循环结构的 GRAGAN最后给出工程组织与验证收敛的技巧。适合已经装好 Pytorch 基础框架、想真正复现并调通 GAN 系源码的开发者。2. Pytorch 实现 GAN 的公共骨架生成器、判别器与训练循环2.1 环境确认Anaconda 配置 Pytorch 环境不等于能跑 GAN先确认环境这一步很多人直接跳过。GAN 训练在 CPU 上也能跑通 MNIST但一张 64x64 的图判别器前向一次在 CPU 上要几十毫秒一个 epoch 几千张图跑下来调一次参数要等半天根本没法迭代。常见做法是先用 Anaconda 建一个独立环境再装 GPU 版 Pytorchconda create -n gan python3.10 conda activate gan pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 python -c import torch; print(torch.__version__, torch.cuda.is_available())--index-url指向 Pytorch 官方的 CUDA 11.8 预编译 wheel 索引torch.cuda.is_available()输出True才说明驱动和运行时匹配。如果机器没有 N 卡把索引里的cu118换成cpu即可。顺序上先装 Pytorch 再装 torchvision版本由 pip 自动对齐不要分开手动指定版本否则容易出现算子不匹配。装好之后先验证张量在 GPU 上的行为import torch device torch.device(cuda if torch.cuda.is_available() else cpu) z torch.randn(64, 100, devicedevice) print(z.device)这个device变量后面每个模型都要用到训练循环里所有张量必须统一到同一个 device否则会报 Expected all tensors to be on the same device。这是所有 GAN 项目源码里最常见的一类报错报错位置通常在损失计算处但根源往往是某个torch.randn忘了加devicedevice。2.2 Pytorch 神经网络里生成器和判别器的标准写法DCGAN 是几乎所有 GAN 项目源码都会包含的基线模型。生成器用ConvTranspose2d把 100 维噪声逐层上采样到 64x64 图像判别器用普通Conv2d做下采样最后输出一个 0 到 1 之间的真伪概率。生成器定义如下class Generator(nn.Module): def __init__(self, latent_dim100, ngf64, channels3): super().__init__() self.main nn.Sequential( nn.ConvTranspose2d(latent_dim, ngf * 8, 4, 1, 0, biasFalse), nn.BatchNorm2d(ngf * 8), nn.ReLU(True), nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, biasFalse), nn.BatchNorm2d(ngf * 4), nn.ReLU(True), nn.ConvTranspose2d(ngf * 4, ngf * 2, 4, 2, 1, biasFalse), nn.BatchNorm2d(ngf * 2), nn.ReLU(True), nn.ConvTranspose2d(ngf * 2, ngf, 4, 2, 1, biasFalse), nn.BatchNorm2d(ngf), nn.ReLU(True), nn.ConvTranspose2d(ngf, channels, 4, 2, 1, biasFalse), nn.Tanh() ) def forward(self, z): return self.main(z)生成器每一层做 2 倍上采样输入(batch, 100, 1, 1)输出(batch, 3, 64, 64)。最后用Tanh把像素压到 -1 到 1对应数据预处理时要把图像归一化到同样的区间常见做法是transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))。biasFalse是因为后面紧跟着 BatchNorm偏置会被归一化抵消留着只会浪费参数。判别器的写法与生成器对称class Discriminator(nn.Module): def __init__(self, channels3, ndf64): super().__init__() self.main nn.Sequential( nn.Conv2d(channels, ndf, 4, 2, 1, biasFalse), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(ndf, ndf * 2, 4, 2, 1, biasFalse), nn.BatchNorm2d(ndf * 2), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, biasFalse), nn.BatchNorm2d(ndf * 4), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(ndf * 4, 1, 4, 1, 0, biasFalse), nn.Sigmoid() ) def forward(self, x): return self.main(x)判别器用LeakyReLU(0.2)而不是 ReLU因为负半轴的梯度能让判别器在输入较差时仍有稳定的学习信号避免神经元直接死亡。第一层不加 BatchNorm这是 DCGAN 论文里明确写过的细节对原始输入直接做统计归一化会放大真假图像分布的差异判别器反而更容易过拟合。2.3 训练循环与三个必调参数GAN 的训练循环不是单损失最小化而是两个优化器交替更新。判别器要区分真假生成器要骗过判别器所以判别器对真图和假图各算一次损失生成器只用假图那一侧的梯度。核心代码criterion nn.BCELoss() opt_g torch.optim.Adam(G.parameters(), lr2e-4, betas(0.5, 0.999)) opt_d torch.optim.Adam(D.parameters(), lr2e-4, betas(0.5, 0.999)) for epoch in range(epochs): for real, _ in dataloader: real real.to(device) batch real.size(0) real_label torch.ones(batch, 1, devicedevice) fake_label torch.zeros(batch, 1, devicedevice) # 更新判别器真图判真假图判假 opt_d.zero_grad() loss_d_real criterion(D(real), real_label) z torch.randn(batch, latent_dim, devicedevice) fake G(z).detach() loss_d_fake criterion(D(fake), fake_label) loss_d loss_d_real loss_d_fake loss_d.backward() opt_d.step() # 更新生成器让假图被判为真 opt_g.zero_grad() loss_g criterion(D(G(z)), real_label) loss_g.backward() opt_g.step()这段代码有三处细节。第一G(z).detach()必须做否则判别器那次反向传播会把梯度同时传回生成器相当于生成器被隐式更新了两次。第二生成器更新时复用同一个z因为生成器要站在刚才那批假图的基础上改进而不是换一批新噪声蒙混过关。第三生成器分支里real_label也是全 1意思是生成器的目标就是让判别器把假图当成真图。三个必调参数及其异常表现参数常见取值作用异常表现lr2e-4控制两个网络更新步长判别器损失长期不降或剧烈抖动betas(0.5, 0.999)Adam 动量系数用默认 (0.9, 0.999) 时训练明显震荡batch_size64决定梯度估计质量过小时判别器过拟合过大时生成器梯度被平均掉判别器和生成器的更新比例也可以调。常见做法是 1:1但图像分辨率高、类别多的数据集上判别器通常会领先这时改成每轮判别器走 2 次、生成器走 1 次。判断依据很直接loss_d掉到接近 0 而loss_g还在涨就是判别器太强生成器梯度基本失效。3. 用 Pytorch 复现 CycleGAN无配对图像翻译的完整实现3.1 循环一致性损失CycleGAN 的地基CycleGAN 解决的是无配对图像翻译比如把真实照片转成梵高风格但训练时不需要同一场景的两张配对图。单靠对抗损失只能保证生成图像像是目标域不能保证内容不丢失一匹马的照片可能被翻译成一头牛。CycleGAN 的解法是再加一个反向生成器让F(G(x)) ≈ x这就是循环一致性损失L_cyc(G, F) E[||F(G(x)) - x||1] E[||G(F(y)) - y||1]实际工程里这个损失的权重 λ 取 10是原论文的默认值。权重太小翻译结果内容漂移权重太大生成器会倾向于输出和输入几乎一样的图像对抗损失完全失效。有些项目源码还会加一个身份损失权重取 5用来保留输入图像的颜色分布把一张已经是梵高风格的图再输入梵高生成器应该尽量不改变它。调参时如果发现翻译结果颜色整体偏掉优先加大身份损失权重修改的是lambda_identity这个配置项。3.2 生成器用 ResNet 块判别器用 PatchGANCycleGAN 的生成器是下采样-残差-上采样结构中间堆 9 个残差块。这里不用 BatchNorm 而用 InstanceNorm原因和后面训练策略直接相关CycleGAN 常见做法是 batch size 取 1BatchNorm 在 batch 为 1 时统计的是单张图的均值和方差效果等价于 InstanceNorm但多了一层不必要的内部状态同步。残差块定义class ResidualBlock(nn.Module): def __init__(self, in_channels): super().__init__() self.block nn.Sequential( nn.Conv2d(in_channels, in_channels, 3, 1, 1, biasFalse), nn.InstanceNorm2d(in_channels), nn.ReLU(True), nn.Conv2d(in_channels, in_channels, 3, 1, 1, biasFalse), nn.InstanceNorm2d(in_channels), ) def forward(self, x): return x self.block(x)残差连接让梯度可以跨块直传9 个块的堆叠才不会训练不动。判别器用的是 PatchGAN它不输出全局真伪概率而是输出一个 N×N 的 patch 得分图每个 patch 负责判断局部区域的真伪class PatchDiscriminator(nn.Module): def __init__(self, in_channels3, ndf64): super().__init__() self.main nn.Sequential( nn.Conv2d(in_channels, ndf, 4, 2, 1, biasFalse), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(ndf, ndf * 2, 4, 2, 1, biasFalse), nn.InstanceNorm2d(ndf * 2), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, biasFalse), nn.InstanceNorm2d(ndf * 4), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(ndf * 4, 1, 4, 1, 1, biasFalse), ) def forward(self, x): return self.main(x).mean(dim(2, 3))mean(dim(2, 3))把 patch 得分图压缩成一个标量配合nn.BCEWithLogitsLoss使用。PatchGAN 的好处是参数量小、关注局部纹理翻译结果里图像的边缘更锐利。项目源码里如果看到判别器输出形状是(batch, 1, 30, 30)而没有均值压缩说明它把每个 patch 位置当独立样本训练两种写法都常见但前者收敛更稳。3.3 CycleGAN 训练配置与图像池CycleGAN 的训练配置比 DCGAN 敏感得多四个参数决定成败参数常见取值说明batch_size1原论文即 1配合 InstanceNorm图像池大小50保存历史生成图缓解判别器过拟合身份损失权重5保护颜色分布可选学习率2e-4前 100 epoch 恒定后 100 epoch 线性衰减后期衰减帮助收敛图像池是这个项目里最容易写错的部分。它不是数据集缓存而是判别器训练时混入历史假图的缓冲区。每轮把新生成的假图丢进池子训练判别器时按概率从池子里取旧图。没有这个池子判别器只见过当前时刻的生成图会把生成器的最新输出记得太牢训练震荡加剧class ImagePool: def __init__(self, pool_size50): self.pool_size pool_size self.images [] def query(self, img): if self.pool_size 0: return img if len(self.images) self.pool_size: self.images.append(img) return img if random.random() 0.5: return img old self.images.pop(0) self.images.append(img) return oldquery返回的才是真正给判别器用的假图训练主循环里生成器每产出一批图先过池子再算判别器损失。对应的训练日志应同时记录四类损失正向 GAN 损失、反向 GAN 损失、循环一致性损失、身份损失。如果前后三者量级差超过 10 倍先检查数据归一化是否统一到 -1 到 1再检查mean(dim(2, 3))的写法是否和损失函数匹配。4. 用 Pytorch 复现 GRAGAN循环生成器对抗网络4.1 GRAGAN 解决什么问题GRAGAN 是 GAN 家族里把循环结构放进生成器的变体按公开项目源码里最常见的命名习惯指用 GRU 做生成器的对抗网络。它适合波形、传感器数据、时序异常检测这类序列任务生成器逐时间步生成数据判别器看完整段序列再判定真伪。和图像 GAN 相比它的训练不稳定问题更突出序列样本空间大、时间步之间强相关生成器很容易只学会输出训练集里的某几条典型样本这就是模式崩溃。GRAGAN 类模型的核心对策是让判别器看到整段序列的上下文而不是对每个时间点单独打分因此判别器必须在最后一个时间步之后才做判定。4.2 生成器和判别器的 Pytorch 实现生成器输入一段随机噪声序列z形状是(batch, seq_len, latent_dim)GRU 再把噪声逐时间步加工成目标序列class GRUGenerator(nn.Module): def __init__(self, latent_dim16, hidden_dim32, output_dim1, num_layers2): super().__init__() self.gru nn.GRU(latent_dim, hidden_dim, num_layers, batch_firstTrue) self.fc nn.Sequential( nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, output_dim), ) def forward(self, z): out, _ self.gru(z) return self.fc(out)判别器同样用 GRU但只取最后一个时间步的隐状态过全连接输出整段序列的真伪概率class SeqDiscriminator(nn.Module): def __init__(self, input_dim1, hidden_dim32, num_layers2): super().__init__() self.gru nn.GRU(input_dim, hidden_dim, num_layers, batch_firstTrue) self.fc nn.Sequential( nn.Linear(hidden_dim, hidden_dim), nn.LeakyReLU(0.2), nn.Linear(hidden_dim, 1), ) def forward(self, x): out, _ self.gru(x) last out[:, -1, :] return torch.sigmoid(self.fc(last))两个网络都用了num_layers2这是序列 GAN 的常见做法单层 GRU 在稍长的序列上表达能力明显不足。batch_firstTrue让输入输出形状统一为(batch, seq_len, feature)避免时间维和批维来回交换。训练循环和 DCGAN 完全同构区别只在z的采样方式常见做法是torch.randn(batch, seq_len, latent_dim, devicedevice)让每个时间步的噪声在隐空间连续变化生成器才能学到时间上的平滑性。如果z只采样一个向量再复制到所有时间步序列会缺乏动态变化生成结果近似静态信号。4.3 训练稳定性的三条经验与评估指标序列 GAN 训练比图像 GAN 更容易崩判别器往往前几个 epoch 就学会区分真假序列的统计特征生成器梯度迅速消失。三条工程经验如下手段常见取值解决的问题生成器与判别器隐层维度32/32 或 64/64判别器过强导致生成器梯度消失学习率1e-4 量级序列梯度方差比图像大判别器输入加噪std0.02~0.05拖慢判别器过拟合第一条生成器和判别器的隐含维度保持同一量级判别器比生成器宽一倍以上时生成器基本学不到有效梯度。第二条学习率降到 1e-4 量级比图像 GAN 的 2e-4 更低因为序列梯度的方差更大。第三条给真实序列加少量高斯噪声相当于把真实分布每个样本点膨胀成一个小球判别器没法靠局部统计量快速区分真假if noise_std 0: real_seq real_seq torch.randn_like(real_seq) * noise_std注意加噪只在判别器那一侧做生成器输入的z不加。如果加噪后判别器损失还是快速归零常见做法是给判别器最后一层加谱归一化nn.utils.spectral_norm(nn.Linear(hidden_dim, 1))它约束权重矩阵的最大奇异值直接限制判别器表达能力比反复调学习率更稳定。评估上图像 GAN 的 FID 不能直接拿来算因为序列没有现成的特征提取器。常用做法是计算生成序列和真实序列之间的 MMD最大均值差异数值越小越好更简单的做法是看判别器在验证集真序列上的准确率是否维持在 50% 附近长期 100% 说明训练回路已经坏掉。项目源码里如果带了检测头把生成序列喂给一个在真实数据上训练好的分类器比较分类效果下降幅度这比纯看损失曲线更贴近业务判断。5. GAN 项目源码的工程组织与验证技巧5.1 目录结构与配置分离解压这类项目源码后第一件事是看目录组织。常见做法是把三个模型完全隔离每个模型自己的网络、训练循环、配置互不干扰gan-project/ ├── configs/ # 每个模型的参数配置 │ ├── dcgan.yaml │ ├── cyclegan.yaml │ └── gragan.yaml ├── data/ # 数据集与预处理 ├── models/ # 生成器与判别器定义 │ ├── dcgan.py │ ├── cyclegan.py │ └── gragan.py ├── trainers/ # 训练主循环 ├── utils/ # 图像池、种子、日志工具 └── scripts/ # 训练与评估入口模型定义和训练循环分开的好处是换数据集时不用改网络。一个配置文件里通常只放两样东西数据路径和超参数。比如dcgan.yaml里的latent_dim: 100、lr: 2e-4、epochs: 200训练脚本读进来直接覆盖默认值避免为每个实验改代码。5.2 固定随机种子是复现的第一前提GAN 对随机性极度敏感同一个脚本两次运行结果可能完全不同。教程里说的自己跑一遍发现效果不一样绝大多数情况是没固定种子。固定操作必须在数据加载和模型初始化之前执行def setup_seed(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 Falsecudnn.deterministic True让卷积固定选择实现算法代价是训练变慢benchmark False禁止运行时换算法。日常调试开着这两项真正大规模训练时再关掉deterministic换回速度。另外 DataLoader 的shuffleTrue也依赖全局随机状态所以setup_seed必须放在创建 DataLoader 之前。5.3 用固定噪声和 FID 判断生成质量训练到一半想知道生成器是不是在学不要只看损失曲线损失一直在降但生成图全是噪点的情况很常见。两个技巧配合用。第一固定一个噪声向量每个 epoch 结束把它喂给生成器存图fixed_z torch.randn(64, latent_dim, devicedevice)在训练前生成一次之后永远用它推理逐 epoch 对比图像是否越来越清晰、多样性是否保持。第二用 FID 数值衡量生成分布和真实分布的差距pip install pytorch-fid python -m pytorch_fid data/real_images data/fake_imagespytorch-fid用 Inception 网络提取特征后计算统计距离分数越低越好64x64 合成数据集上从几百降到几十就是明显进步。对 CycleGAN 这类翻译模型还可以把翻译前后的图拼接保存人工检查内容是否保持、风格是否迁移。最后一步提醒如果固定噪声图 50 个 epoch 几乎不动说明生成器把所有样本映射到了同一个模式也就是模式崩溃这时候优先回头检查判别器结构和学习率而不是加大训练轮数。本文还有配套的精品资源点击获取