DCGAN实战:PyTorch从零构建深度卷积生成对抗网络全解析
2026/9/7 5:08:02 网站建设 项目流程

简介:一套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 python=3.9 conda activate dcgan # 先查看自己的CUDA版本,nvidia-smi显示的CUDA Version就是驱动支持的最高版本 nvidia-smi # 假设你的驱动支持CUDA 11.8,直接装对应版本 pip install torch==2.0.1 torchvision==0.15.2 --index-url https://download.pytorch.org/whl/cu118

注意一点:nvidia-smi显示的CUDA Driver版本是驱动支持的版本上限,和PyTorch运行时需要的CUDA Runtime不一定完全一致。你不需要为PyTorch单独安装完整的CUDA Toolkit,PyTorch的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 torchvision

CPU版本训练会慢很多,但用于学习原理和调试代码完全够用了。

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', train=True, transform=mnist_transform, download=True) dataloader = DataLoader(dataset, batch_size=128, shuffle=True, num_workers=4, pin_memory=True)

这里有两个细节值得展开。第一,为什么要把像素归一化到[-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_dim=100, channels=3): 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_size=4, stride=2, padding=1, bias=False), nn.BatchNorm2d(256), nn.ReLU(True), # 输出: 256 x 8 x 8 nn.ConvTranspose2d(256, 128, kernel_size=4, stride=2, padding=1, bias=False), nn.BatchNorm2d(128), nn.ReLU(True), # 输出: 128 x 16 x 16 nn.ConvTranspose2d(128, 64, kernel_size=4, stride=2, padding=1, bias=False), nn.BatchNorm2d(64), nn.ReLU(True), # 输出: 64 x 32 x 32 nn.ConvTranspose2d(64, channels, kernel_size=4, stride=2, padding=1, bias=False), 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。这里stride=2、kernel_size=4、padding=1的组合是DCGAN论文里验证过的标准配置,可以把特征图尺寸精确翻倍,输出尺寸的计算公式是(H_in-1)*stride + kernel_size - 2*padding,代入可知:(4-1)*2 + 4 - 2 = 8,符合翻倍预期。权重初始化方面,我用torch.nn.init.normal_(weight, mean=0.0, std=0.02)对所有权重做了正态分布初始化,这也是DCGAN论文里验证过能稳定收敛的做法。

3.2 判别器:判断图像真伪的概率输出

判别器的结构就是生成器的镜像,用带步长的卷积逐步下采样,最后通过Sigmoid输出一个0到1之间的概率值,表示输入图像是真实数据的概率。越接近1,说明判别器越确信图片是真的。

class Discriminator(nn.Module): def __init__(self, channels=3): super(Discriminator, self).__init__() self.main = nn.Sequential( # 输入: 3 x 64 x 64 nn.Conv2d(channels, 64, kernel_size=4, stride=2, padding=1, bias=False), nn.LeakyReLU(0.2, inplace=True), # 输出: 64 x 32 x 32 nn.Conv2d(64, 128, kernel_size=4, stride=2, padding=1, bias=False), nn.BatchNorm2d(128), nn.LeakyReLU(0.2, inplace=True), # 输出: 128 x 16 x 16 nn.Conv2d(128, 256, kernel_size=4, stride=2, padding=1, bias=False), nn.BatchNorm2d(256), nn.LeakyReLU(0.2, inplace=True), # 输出: 256 x 8 x 8 nn.Conv2d(256, 512, kernel_size=4, stride=2, padding=1, bias=False), nn.BatchNorm2d(512), nn.LeakyReLU(0.2, inplace=True), # 输出: 512 x 4 x 4 nn.Conv2d(512, 1, kernel_size=4, stride=1, padding=0, bias=False), 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而不是ReLU,LeakyReLU在负区间有一个很小的斜率(默认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, device=device) errD_real = criterion(output, label) errD_real.backward() noise = torch.randn(batch_size, latent_dim, 1, 1, device=device) 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(), lr=0.0002, betas=(0.5, 0.999)) optimizerG = torch.optim.Adam(generator.parameters(), lr=0.0002, betas=(0.5, 0.999))

beta1设成0.5是DCGAN论文里的关键设置。默认的Adam通常用beta1=0.9,但0.9会引入较大的动量累积,对于GAN这种需要快速适应对手变化的动态博弈来说,动量太大容易导致震荡和不收敛。0.5会保留更少的历史梯度信息,让优化器对当前梯度更敏感,这是GAN训练中一个很小但影响巨大的细节。

3.4 训练可视化与权重保存

我每5个epoch保存一次生成器权重,同时把固定噪声向量输入生成器产生一组图像保存到output目录。这个固定噪声向量很关键:每次用同一个随机种子生成一批固定噪声,然后把每次训练后的生成图像并排保存,就能直观看到同一个输入在不同epoch下的演变过程。从一团噪声慢慢变成清晰图像,这种过程反馈对调试非常有用。

fixed_noise = torch.randn(64, latent_dim, 1, 1, device=device) if epoch % 5 == 0: torch.save(generator.state_dict(), f'checkpoints/generator_epoch{epoch}.pth') with torch.no_grad(): fake = generator(fixed_noise).detach().cpu() save_image(fake, f'output/epoch_{epoch}.png', nrow=8, normalize=True)

4. 完整训练配置与实操记录

4.1 我用的训练跑参配置

以下是本项目实测稳定的一组训练参数,如果你用的是64x64输出尺寸,可以直接照抄:

参数名数值说明
latent_dim100输入噪声向量维度
batch_size128MNIST和CelebA通用
learning_rate0.0002生成器和判别器相同
beta10.5Adam的一阶矩衰减系数
beta20.999Adam的二阶矩衰减系数
epochsMNIST 30,CelebA 60因人脸更复杂需要更多轮次
输入图像尺寸64x64MNIST先Resize再训练

MNIST数据集在这个配置下,单卡RTX 3060跑大概1分钟一个epoch,30个epoch半小时跑完。CelebA数据量大一些,一个epoch大概3-4分钟,60个epoch需要3小时起。如果你用CPU训练,MNIST跑10个epoch可能就要1小时,建议还是尽量用GPU。另外,batch size不要设太大也不要太小,我试过batch_size=32时训练震荡明显加剧,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_THREADS=1,牺牲一点多线程性能换取稳定性。具体命令是:

export OMP_NUM_THREADS=1

6. 项目实操总结与后续扩展方向

这个DCGAN项目虽然代码量不大,但麻雀虽小五脏俱全,从环境配置、数据准备、网络设计、对抗训练到问题排查,覆盖了GAN类项目的完整生命周期。我个人在实际操作中最深的体会是:GAN的训练不是一个“设置好参数就等着出结果”的过程,而是一个需要不断观察Loss曲线、盯生成图像质量、随时调整策略的动态调试过程。多保存几个epoch的中间结果、多画几次loss趋势、多记录几组参考配置,对复现和迭代都很有价值。

如果你顺利跑通了这套基线代码,后面可以尝试几个明确的扩展方向:一是改成条件GAN,给生成器和判别器都输入类别标签,让生成图像可控;二是引入WGAN-GP的梯度惩罚损失替代BCELoss,在CelebA上往往能训练得更稳定;三是把输入的随机噪声换成编码器输出的特征,做一个简单的自编码式GAN,对接图像重建任务。

训练代码、数据集说明文档和最常用的调试脚本我都已经整理好放到项目仓库里了。如果有跑不通的地方,优先看看README里的常见问题部分,排查思路比直接改代码更有用。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询