ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

DCGAN实战:PyTorch从零构建深度卷积生成对抗网络全解析

DCGAN实战:PyTorch从零构建深度卷积生成对抗网络全解析 简介一套DCGAN的PyTorch实现工程包面向深度学习中图像生成方向的初学者和研究者帮助快速搭建并理解生成对抗网络的完整训练流程。压缩包共31个文件、约17.35MB包含py源码与pyc编译文件、png/gif结果图、txt日志和md说明文档源码按models、utils、data、result等模块划分便于定位。项目中生成器与判别器模块分别展示转置卷积上采样和卷积下采样的具体写法数据读取脚本实现CelebA人脸数据的加载与预处理运行主程序可复现训练并输出多轮图片和动图训练日志记录损失变化说明文件提供使用指引。通过对抗训练交替优化生成器与判别器可观察生成效果随迭代次数提升的过程。已有3770人学习适合对照原理解读、调试运行和二次开发。 DCGAN全称Deep Convolutional Generative Adversarial Network也就是深度卷积生成对抗网络我最早接触这个概念是在2016年那篇经典的论文Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks。后来在PyTorch里自己从零实现了一遍跑通了MNIST和CelebA人脸数据集才算是真正把GAN的训练逻辑和工程细节吃透。这个项目除了完整可跑的代码还附带了我整理好的数据集下载脚本、训练好的权重文件以及一份包含环境配置、参数调优、常见坑位说明的文档算是一个拿来就能上手、改改就能用的DCGAN基线项目。这篇文章我会把整个项目从设计、环境、代码到debug思路全部拆开讲适合两类人看一是刚接触GAN、想在PyTorch里跑通第一个生成模型的初学者二是已经在用GAN但总觉得训练不稳定、想系统排查问题的工程向选手。不会给你堆一堆理论名词就完事重点放在“为什么这么写”和“出了事怎么查”。1. 项目整体设计与思路拆解1.1 DCGAN到底解决了什么问题传统GAN用全连接网络做生成器和判别器在MNIST这种小尺寸灰度图上勉强能跑但一到稍微复杂点的自然图像问题就全出来了。全连接层会把二维图像拉成一维向量直接丢掉空间局部性图像的特征根本没有办法被有效建模。而且全连接网络参数量巨大训练起来又慢又容易发散。DCGAN的核心贡献就是把这套东西全部换了生成器用转置卷积把随机噪声向量一步步上采样成完整图像保留空间结构。判别器用带步长的卷积替代池化操作做下采样让模型自己学习空间特征。几乎每一层后面都加BatchNorm把特征分布拉回稳定区间防止梯度爆炸或消失。生成器用ReLU做激活判别器用LeakyReLU避免判别器这边因负区间梯度为0导致死神经元。把最后的全连接层全部拿掉参数量少了一大截训练速度明显更快。用大白话说传统GAN像是一个只会背答案的选手遇到没见过的样本就容易懵。DCGAN则是让生成器和判别器都学会“看结构”和“找细节”所以生成出来的图像在纹理、边缘、物体轮廓上都自然得多。这也是这些年几乎所有图像生成模型的基础骨架很多后续的改进模型都是在DCGAN的结构上加模块。1.2 为什么用PyTorch而不是TensorFlow这个项目最开始我也考虑过TensorFlow但最终还是定了PyTorch原因很实际第一PyTorch的动态计算图对GAN这种训练模式极其友好。GAN的训练是生成器和判别器交替更新正反向传播的逻辑前后有依赖动态图模式下每一步都能随时查看中间张量和梯度值调试体验比静态图舒服太多。第二PyTorch的torchvision自带常用数据集接口和预训练权重MNIST、CIFAR-10、CelebA都能一行代码下载加载。第三社区里GAN的参考实现绝大多数是PyTorch版本遇到问题直接搜就能找到答案这个对新手来说太重要了。另外多说一句PyTorch在2.x版本之后的API设计非常稳定我们项目里使用的核心接口在1.x和2.x下都能直接跑通不用担心版本迁移问题。代码里我尽量只用了nn.Module、nn.ConvTranspose2d、nn.Conv2d这些稳定接口没有依赖实验性功能所以环境兼容性很有保障。2. 环境搭建与数据集准备2.1 PyTorch环境配置CPU和GPU两种路线在动手写模型之前先把环境搭好。我见过太多人在环境这步折腾半天最后发现是版本不匹配所以这里直接给出可复制的操作。GPU版本有NVIDIA显卡强烈推荐以Ubuntu系统为例# 建议直接用Anaconda新建独立环境避免污染系统Python conda create -n dcgan python3.9 conda activate dcgan # 先查看自己的CUDA版本nvidia-smi显示的CUDA Version就是驱动支持的最高版本 nvidia-smi # 假设你的驱动支持CUDA 11.8直接装对应版本 pip install torch2.0.1 torchvision0.15.2 --index-url https://download.pytorch.org/whl/cu118注意一点nvidia-smi显示的CUDA Driver版本是驱动支持的版本上限和PyTorch运行时需要的CUDA Runtime不一定完全一致。你不需要为PyTorch单独安装完整的CUDA ToolkitPyTorch的pip包自带了它需要的CUDA运行时库只要驱动版本足够新就行。比如我的笔记本驱动显示支持CUDA 12.1那我装cu121或者cu118的PyTorch包都能跑。安装完成后验证一下import torch print(torch.__version__) # 能打印版本说明装好了 print(torch.cuda.is_available()) # 输出True说明GPU可用 print(torch.cuda.get_device_name(0))CPU版本更简单一行命令pip install torch torchvisionCPU版本训练会慢很多但用于学习原理和调试代码完全够用了。2.2 数据集说明与读取细节这个项目我准备了两个数据集都在代码仓库里给了自动下载脚本。第一个是MNIST手写数字28x28灰度图共6万张训练图。它是GAN入门的标配因为图像结构简单训练几十个epoch就能看到像样的生成效果适合快速验证模型代码有没有bug。因为MNIST太小用它跑通全流程能极大节省排查时间。第二个是CelebA人脸数据集约20万张名人脸部图像下载后每张裁剪成64x64的RGB图。这才是真正能体现DCGAN威力的数据集生成出来的人脸清晰度、逼真度都远非MNIST能比。不过CelebA原始文件较大在网盘需要自行手动下载我在文档里写了下载方式和百度网盘链接并提供了数据清洗脚本自动完成人脸区域裁剪和resize。数据加载和预处理的关键代码如下from torchvision import datasets, transforms from torch.utils.data import DataLoader # MNIST数据集的预处理 mnist_transform transforms.Compose([ transforms.Resize(64), # 统一放缩到64x64 transforms.ToTensor(), # 转换为Tensor像素值范围[0,1] transforms.Normalize([0.5], [0.5]) # 归一化到[-1,1] ]) # CelebA数据集的预处理 celeba_transform transforms.Compose([ transforms.Resize(64), transforms.CenterCrop(64), # 中心裁剪去掉边缘乱七八糟的背景 transforms.ToTensor(), transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) ]) dataset datasets.MNIST(root./data, trainTrue, transformmnist_transform, downloadTrue) dataloader DataLoader(dataset, batch_size128, shuffleTrue, num_workers4, pin_memoryTrue)这里有两个细节值得展开。第一为什么要把像素归一化到[-1,1]因为生成器最后一层用的是Tanh激活函数输出范围就是[-1,1]把真实图像的数据分布也转到这个区间判别器才能有效区分生成数据和真实数据。如果你用了Tanh却把输入数据规范化到[0,1]判别器会学得很好生成器却永远无法收敛因为两者的数据分布根本不对齐。第二num_workers参数如果不是在Windows上跑建议设成4或更高数据加载效率和GPU利用率都能提上来。Windows系统下如果num_workers设得过大容易报DataLoader worker相关的错保守设成2比较稳妥。2.3 项目文件结构说明仓库的文件组织方式如下方便大家对应着看DCGAN-PyTorch/ ├── data/ # 数据集存放目录自动下载 ├── checkpoints/ # 训练好的模型权重保存目录 ├── output/ # 训练过程中生成的样本图像 ├── models/ │ ├── generator.py # 生成器网络定义 │ └── discriminator.py # 判别器网络定义 ├── utils/ │ └── visualize.py # 可视化工具保存生成图像 ├── train.py # 训练主脚本 ├── inference.py # 用训练好的模型生成图像的脚本 ├── requirements.txt # 依赖包列表 └── README.md # 环境配置 使用说明 常见问题我特意把模型定义、数据工具、训练流程分开放就是为了让你改起来不费劲。比如你想把64x64的生成改成128x128只需要去generator.py和discriminator.py里调整卷积层的通道数和参数就行训练代码基本不用动。3. 核心代码实现与关键参数解析3.1 生成器从随机噪声到图像生成器的本质是把一个100维的随机噪声向量通过一连串转置卷积逐步放大到64x64x3的图像。整个过程像是在用乐高积木从零搭一座房子每一层卷积叠加上去后空间尺寸越来越大、通道数越来越少最终形成一张完整的图。import torch.nn as nn class Generator(nn.Module): def __init__(self, latent_dim100, channels3): super(Generator, self).__init__() self.fc nn.Linear(latent_dim, 512 * 4 * 4) self.main nn.Sequential( # 输入: 512 x 4 x 4 nn.ConvTranspose2d(512, 256, kernel_size4, stride2, padding1, biasFalse), nn.BatchNorm2d(256), nn.ReLU(True), # 输出: 256 x 8 x 8 nn.ConvTranspose2d(256, 128, kernel_size4, stride2, padding1, biasFalse), nn.BatchNorm2d(128), nn.ReLU(True), # 输出: 128 x 16 x 16 nn.ConvTranspose2d(128, 64, kernel_size4, stride2, padding1, biasFalse), nn.BatchNorm2d(64), nn.ReLU(True), # 输出: 64 x 32 x 32 nn.ConvTranspose2d(64, channels, kernel_size4, stride2, padding1, biasFalse), nn.Tanh() # 输出: 3 x 64 x 64 ) def forward(self, z): out self.fc(z).view(z.size(0), 512, 4, 4) return self.main(out)每一步转置卷积都把空间尺寸翻倍4x4的量变成8x8再到16x16、32x32最后输出64x64。这里stride2、kernel_size4、padding1的组合是DCGAN论文里验证过的标准配置可以把特征图尺寸精确翻倍输出尺寸的计算公式是(H_in-1)*stride kernel_size - 2*padding代入可知(4-1)*2 4 - 2 8符合翻倍预期。权重初始化方面我用torch.nn.init.normal_(weight, mean0.0, std0.02)对所有权重做了正态分布初始化这也是DCGAN论文里验证过能稳定收敛的做法。3.2 判别器判断图像真伪的概率输出判别器的结构就是生成器的镜像用带步长的卷积逐步下采样最后通过Sigmoid输出一个0到1之间的概率值表示输入图像是真实数据的概率。越接近1说明判别器越确信图片是真的。class Discriminator(nn.Module): def __init__(self, channels3): super(Discriminator, self).__init__() self.main nn.Sequential( # 输入: 3 x 64 x 64 nn.Conv2d(channels, 64, kernel_size4, stride2, padding1, biasFalse), nn.LeakyReLU(0.2, inplaceTrue), # 输出: 64 x 32 x 32 nn.Conv2d(64, 128, kernel_size4, stride2, padding1, biasFalse), nn.BatchNorm2d(128), nn.LeakyReLU(0.2, inplaceTrue), # 输出: 128 x 16 x 16 nn.Conv2d(128, 256, kernel_size4, stride2, padding1, biasFalse), nn.BatchNorm2d(256), nn.LeakyReLU(0.2, inplaceTrue), # 输出: 256 x 8 x 8 nn.Conv2d(256, 512, kernel_size4, stride2, padding1, biasFalse), nn.BatchNorm2d(512), nn.LeakyReLU(0.2, inplaceTrue), # 输出: 512 x 4 x 4 nn.Conv2d(512, 1, kernel_size4, stride1, padding0, biasFalse), nn.Sigmoid() # 输出: 1 x 1 x 1 ) def forward(self, x): return self.main(x).view(x.size(0), -1)这里有个关键点判别器第一层没有加BatchNorm。这是DCGAN论文明确强调的因为原始图像的像素范围已经在数据预处理阶段固定BatchNorm反而会破坏图像的原始统计信息。如果你强行加BN训练早期判别器的loss波动会特别剧烈。另外判别器的激活函数用LeakyReLU而不是ReLULeakyReLU在负区间有一个很小的斜率默认0.2保证反向传播时负区间也有梯度流动避免判别器快速变为不可导而引发训练崩溃。3.3 损失函数与训练循环对抗的本质GAN的损失函数本质上是一个双人极大极小博弈。判别器想让自己的判断更准确生成器想让判别器的判断出错两者在对抗中共同进步。数学上写作判别器的目标函数最大化log(D(x)) log(1 - D(G(z)))生成器的目标函数最小化log(1 - D(G(z)))但在实际工程实现中如果直接按论文里的公式最小化log(1-D(G(z)))生成器在训练早期会因为判别器太强而没有任何梯度信号陷入所谓的“饱和”状态。所以一般固定使用生成器的非饱和损失也就是让生成器去最大化log(D(G(z)))从梯度角度来讲这等价于最小化-log(D(G(z)))。核心训练代码如下criterion nn.BCELoss() # 真实标签设为1生成样本的标签设为0 real_label 1. fake_label 0. for epoch in range(num_epochs): for i, (images, _) in enumerate(dataloader): images images.to(device) batch_size images.size(0) # 第一步训练判别器 discriminator.zero_grad() output discriminator(images) label torch.full((batch_size,), real_label, devicedevice) errD_real criterion(output, label) errD_real.backward() noise torch.randn(batch_size, latent_dim, 1, 1, devicedevice) fake_images generator(noise) output discriminator(fake_images.detach()) label.fill_(fake_label) errD_fake criterion(output, label) errD_fake.backward() errD errD_real errD_fake optimD.step() # 第二步训练生成器 generator.zero_grad() output discriminator(fake_images) label.fill_(real_label) # 关键点生成器要让判别器把假图判为真图 errG criterion(output, label) errG.backward() optimG.step()注意训练判别器时fake_images.detach()这一步它是为了阻断梯度向生成器回传。如果忘了detach判别器在训练时会顺带更新生成器的参数那整个对抗过程就乱了套。而训练生成器时我们用真实标签1去要求生成器骗过判别器也就是把生成图像尽量调整到“让判别器认为是真的”的方向。优化器方面两种网络都用的Adam但学习率和beta参数有讲究optimizerD torch.optim.Adam(discriminator.parameters(), lr0.0002, betas(0.5, 0.999)) optimizerG torch.optim.Adam(generator.parameters(), lr0.0002, betas(0.5, 0.999))beta1设成0.5是DCGAN论文里的关键设置。默认的Adam通常用beta10.9但0.9会引入较大的动量累积对于GAN这种需要快速适应对手变化的动态博弈来说动量太大容易导致震荡和不收敛。0.5会保留更少的历史梯度信息让优化器对当前梯度更敏感这是GAN训练中一个很小但影响巨大的细节。3.4 训练可视化与权重保存我每5个epoch保存一次生成器权重同时把固定噪声向量输入生成器产生一组图像保存到output目录。这个固定噪声向量很关键每次用同一个随机种子生成一批固定噪声然后把每次训练后的生成图像并排保存就能直观看到同一个输入在不同epoch下的演变过程。从一团噪声慢慢变成清晰图像这种过程反馈对调试非常有用。fixed_noise torch.randn(64, latent_dim, 1, 1, devicedevice) if epoch % 5 0: torch.save(generator.state_dict(), fcheckpoints/generator_epoch{epoch}.pth) with torch.no_grad(): fake generator(fixed_noise).detach().cpu() save_image(fake, foutput/epoch_{epoch}.png, nrow8, normalizeTrue)4. 完整训练配置与实操记录4.1 我用的训练跑参配置以下是本项目实测稳定的一组训练参数如果你用的是64x64输出尺寸可以直接照抄参数名数值说明latent_dim100输入噪声向量维度batch_size128MNIST和CelebA通用learning_rate0.0002生成器和判别器相同beta10.5Adam的一阶矩衰减系数beta20.999Adam的二阶矩衰减系数epochsMNIST 30CelebA 60因人脸更复杂需要更多轮次输入图像尺寸64x64MNIST先Resize再训练MNIST数据集在这个配置下单卡RTX 3060跑大概1分钟一个epoch30个epoch半小时跑完。CelebA数据量大一些一个epoch大概3-4分钟60个epoch需要3小时起。如果你用CPU训练MNIST跑10个epoch可能就要1小时建议还是尽量用GPU。另外batch size不要设太大也不要太小我试过batch_size32时训练震荡明显加剧128是个相对稳的平衡点。4.2 训练过程中的Loss曲线解读训练过程中可以打印每个batch的判别器loss和生成器loss但这里要提醒一下不要指望loss像分类任务一样单调下降。GAN的loss更像两个人的竞走你追我赶、此消彼长。判别器的loss在0.5到1.2之间波动是正常的生成器的loss在1.5到3之间波动也常见。真正需要警惕的是以下几种情况判别器loss长期接近于0这说明判别器太强把真假样本完全分开了生成器一点机会都没有。生成器loss爆炸式冲高多半是梯度爆炸检查学习率是否过大、是否有NaN出现。两个loss都彻底不动了可能是模式坍塌或者梯度消失。我的习惯是每500步保存一次loss日志训练结束后用matplotlib画出来整体看趋势。如果loss曲线呈现交替涨跌、双方逐渐进入平衡的状态那就是健康的表现。5. 常见问题与排查技巧实录5.1 训练不收敛、生成图像模糊先查这几个模块我猜用这套代码最常碰到的问题是生成出来的图像模糊、轮廓不清。排查思路按优先级排序第一检查生成器最后一层激活函数是不是Tanh。如果误用了Sigmoid或ReLU生成图像的范围会和判别器期望的数据分布不一致直接导致判别器快速收敛到0生成器训练停滞。第二检查数据归一化是否到位。MNIST要用Normalize([0.5], [0.5])CelebA要用Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])保证数据范围在[-1,1]。第三检查BatchNorm的位置。生成器的输入层不建议加BN因为生成的随机噪声本来分布就是确定的加BN反而会破坏噪声的随机性影响多样性。输出的Tanh前也没有BN这个位置对应图像像素值不应该做归一化。第四如果图像能看出轮廓但细节粗糙可以考虑加大模型容量比如把卷积核数量翻倍或者换用更深的结构。我自己在实际测试中把隐层通道数从64提到128后CelebA生成人脸的清晰度有肉眼可见的提升代价是显存占用和训练时间都增加了约60%属于资源换质量的取舍。5.2 GAN特有的模式坍塌问题怎么判断和处理模式坍塌是GAN最头疼的经典问题表现是生成器学会了用一两个固定的假样本骗过判别器生成的图像永远都是同一张或者同一种姿态。MNIST上特别容易看出来如果生成的数字只有一种或者两种比如全是0或者全是1那基本就是坍塌了。判断方法看generate出来的8x8格子里如果大量图像几乎完全相同基本可以实锤。处理手段按经验排序降低学习率到1e-4把判别器的训练频率减半比如每训练判别器一次就训练生成器两次给判别器输入加一点高斯噪声均值0方差0.1逼着生成器生成更多样化的样本让判别器没那么容易记忆假样本。这三种方法我都实测过降低学习率是最直接有效的改动小、副作用少。5.3 显存不够和Windows环境下的填坑建议如果你的显卡只有4G显存训练64x64的CelebA会非常紧张。建议把batch_size降到64同时把num_workers设成2。也可以考虑把生成器中间的通道数从512降到256对生成质量的影响有限但显存压力会明显缓解。Windows下如果遇到DataLoader worker process ... terminated unexpectedly基本都是多进程数据加载的问题。把num_workers设成0或者把代码放在ifname main:保护块里再跑就能解决。我在实际运行中还碰到过PyTorch 2.0在Windows下偶发的MKL线程报错解决方案是设置环境变量OMP_NUM_THREADS1牺牲一点多线程性能换取稳定性。具体命令是export OMP_NUM_THREADS16. 项目实操总结与后续扩展方向这个DCGAN项目虽然代码量不大但麻雀虽小五脏俱全从环境配置、数据准备、网络设计、对抗训练到问题排查覆盖了GAN类项目的完整生命周期。我个人在实际操作中最深的体会是GAN的训练不是一个“设置好参数就等着出结果”的过程而是一个需要不断观察Loss曲线、盯生成图像质量、随时调整策略的动态调试过程。多保存几个epoch的中间结果、多画几次loss趋势、多记录几组参考配置对复现和迭代都很有价值。如果你顺利跑通了这套基线代码后面可以尝试几个明确的扩展方向一是改成条件GAN给生成器和判别器都输入类别标签让生成图像可控二是引入WGAN-GP的梯度惩罚损失替代BCELoss在CelebA上往往能训练得更稳定三是把输入的随机噪声换成编码器输出的特征做一个简单的自编码式GAN对接图像重建任务。训练代码、数据集说明文档和最常用的调试脚本我都已经整理好放到项目仓库里了。如果有跑不通的地方优先看看README里的常见问题部分排查思路比直接改代码更有用。本文还有配套的精品资源点击获取
返回列表