☰
PyTorch DCGAN实战:从源码到训练避坑全解析
2026/10/5 8:55:11 网站建设 项目流程

简介:基于PyTorch在CelebA数据集上训练DCGAN的完整实战项目源码包,面向对生成对抗网络感兴趣的初学者与希望提升工程能力的开发者。项目从理论到实践,系统涵盖DCGAN网络结构搭建、CelebA数据预处理、训练流程与结果可视化等关键环节,配套详细说明文档,可直接运行复现。资源共12个文件,以4个Python脚本为核心,分别负责模型定义、训练、生成与工具封装,并包含训练完成的模型权重、损失曲线及生成图像的图片与动图可视化结果,整体约102.4兆字节,结构清晰便于按需查阅。目前已吸引三百九十九人学习下载,兼具参考与复现价值。通过本项目可掌握生成对抗网络的核心原理与实际调参方法,学会监控损失变化、评估生成图像质量,并能基于名人面部属性数据集独立完成人脸生成实验。训练过程中的可视化文件与最终模型文件,为后续迁移到其他图像生成任务提供了可复用的基线,也适合进一步改造与扩展。

1. 训练 DCGAN 最怕的不是模型复杂,而是反复翻车

我第一次用 PyTorch 在 CelebA 数据集上训练 DCGAN 时,模型结构和损失函数都照着论文抄,结果跑到第 5 个 epoch,生成图像还是一片模糊色块。后来拿到这份项目源码才发现,问题不在网络结构,而在数据预处理和优化器参数上。这份资源把 DCGAN 的完整训练流程打包好了:dcgan.py 定义生成器和判别器,utils.py 处理数据加载,train.py 跑对抗训练,还附带训练好的 model_final.pth 权重、损失曲线图和一张展示训练过程的 GIF。适合刚看完 GAN 理论、想用真实数据集跑通完整流程的初学者,也适合要拿 DCGAN 做基线实验、想在自采数据集上复现图像生成的中级开发者。下面按代码结构、环境复现、训练踩坑、进阶验证这条线拆开讲,每一部分都给出能直接照抄的改法和参数建议。

2. 读懂 PyTorch 源码结构:生成器、判别器与训练循环的三层拆解

拿到压缩包先别急着训练,花十分钟把几个 Python 文件的关系理清楚。这份项目遵循 DCGAN 代码的经典组织方式,模型、工具、训练三个脚本各司其职,理解了这个结构,后面改任何参数都不会像无头苍蝇一样乱试。

2.1 dcgan.py:生成器的转置卷积与判别器的 LeakyReLU 设计

生成器的本质是把低维随机噪声映射到高维图像空间。项目沿用了 DCGAN 论文里的推荐结构,用 5 层转置卷积把 100 维噪声向量逐步放大成 64×64 的三通道图像。转置卷积并不是卷积的逆运算,它通过在输入像素间补零再执行普通卷积来放大特征图尺寸,参数里 kernel size、stride、padding 三者的配合直接决定每一层输出尺寸。下面这段是生成器的核心定义:

# dcgan.py - 生成器:从 100 维噪声生成 64x64 人脸图像 class Generator(nn.Module): def __init__(self, latent_dim=100, ngf=64): super(Generator, self).__init__() self.main = nn.Sequential( # 输入 (latent_dim, 1, 1),输出 (ngf*8, 4, 4) nn.ConvTranspose2d(latent_dim, ngf * 8, 4, 1, 0, bias=False), nn.BatchNorm2d(ngf * 8), nn.ReLU(True), # 输出 (ngf*4, 8, 8) nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False), nn.BatchNorm2d(ngf * 4), nn.ReLU(True), # 输出 (ngf*2, 16, 16) nn.ConvTranspose2d(ngf * 4, ngf * 2, 4, 2, 1, bias=False), nn.BatchNorm2d(ngf * 2), nn.ReLU(True), # 输出 (ngf, 32, 32) nn.ConvTranspose2d(ngf * 2, ngf, 4, 2, 1, bias=False), nn.BatchNorm2d(ngf), nn.ReLU(True), # 输出 (3, 64, 64) nn.ConvTranspose2d(ngf, 3, 4, 2, 1, bias=False), nn.Tanh() )

latent_dim 是输入噪声维度,项目默认取 100,这是 DCGAN 原文的标准值;ngf 是特征图基数,默认 64,显存不足时可以降到 32,想提升生成质量则可以调到 128。每层之间夹着 BatchNorm2d,这也是 DCGAN 训练稳定的关键——生成器如果不做 BatchNorm,深层特征分布会漂移,图像颜色和对比度会变得很不稳定。最后一层用 Tanh 而不是 Sigmoid,因为输出需要落在 [-1, 1] 区间,与归一化后的真实图像对齐。

判别器是生成器的镜像结构,方向相反:接收 3×64×64 的图像,用普通卷积逐步下采样到 1×1,输出一个 0 到 1 之间的概率值。它有两个改动需要特别留意:激活函数用斜率为 0.2 的 LeakyReLU 而不是 ReLU,避免负值区间的梯度完全归零;输入层不加 BatchNorm,这是 DCGAN 论文里明确提到的细节,第一层直接加 BatchNorm 会引入不必要的批次相关性,影响判别器的判别能力。

2.2 utils.py:数据预处理与 DataLoader 配置

DCGAN 对输入数据有两个硬性要求:图像尺寸统一、像素值域与生成器输出对齐。CelebA 原始图片是 178×218 的彩图,直接送进网络会带来两个问题——转置卷积的尺寸递增逻辑在非方图上会失衡,像素范围 [0, 255] 与 Tanh 输出的 [-1, 1] 对不上。utils.py 用一组标准变换解决这两件事:

# utils.py - 图像变换与 DataLoader 配置 from torchvision import transforms, datasets transform = transforms.Compose([ transforms.Resize(64), # 等比缩放到短边 64 transforms.CenterCrop(64), # 中心裁剪成 64x64 方形 transforms.ToTensor(), # 转为 Tensor,像素缩放到 [0, 1] transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)), # 映射到 [-1, 1] ]) dataset = datasets.ImageFolder(root='data/celeba', transform=transform) dataloader = torch.utils.data.DataLoader( dataset, batch_size=128, # 显存 8G 以下建议改成 64 shuffle=True, num_workers=4, # Windows 上改成 0,否则容易报错 pin_memory=True, drop_last=True # 丢弃最后不足一个 batch 的样本 )

Normalize 的参数 (0.5, 0.5, 0.5) 配合 (0.5, 0.5, 0.5),含义是像素值先归一化到 [0, 1],减去 0.5 再除以 0.5,最终落到 [-1, 1]。这里特别提醒一点:如果你做过 ImageNet 预训练模型的迁移,会习惯用 mean=(0.485, 0.456, 0.406) 那组统计值,但 DCGAN 不能用那组,必须让值域对齐生成器输出 Tanh 的区间,否则判别器看到的真实图像和生成图像分布会系统性错位。

drop_last=True 值得保留。如果最后一个 batch 样本数小于 batch_size,BatchNorm 计算均值方差时会因为样本太少而不稳定,训练早期尤其容易触发异常。ImageFolder 要求数据目录按类别分子文件夹,项目里常见的组织方式是 data/celeba/celeba/ 双层目录。如果 dataset 加载出来长度是 0,先检查是不是目录层级多套了一层。

2.3 train.py:对抗训练循环中的梯度流控制与优化器选择

train.py 是整个项目的核心,实现的是 GAN 训练里最经典的交替更新策略:每个 iteration 先固定生成器更新判别器,再固定判别器更新生成器。判别器的目标是分辨真实图像和生成图像,生成器的目标则是让判别器把假图判为真。项目用 BCELoss 配合 Sigmoid 输出,这是 DCGAN 原版的标准组合:

# train.py - 对抗训练主循环核心代码 criterion = nn.BCELoss() optimizer_G = torch.optim.Adam( generator.parameters(), lr=0.0002, betas=(0.5, 0.999) ) optimizer_D = torch.optim.Adam( discriminator.parameters(), lr=0.0002, betas=(0.5, 0.999) ) for epoch in range(num_epochs): for i, (imgs,) in enumerate(dataloader): real_imgs = imgs.to(device) real_label = torch.ones(imgs.size(0), 1, device=device) fake_label = torch.zeros(imgs.size(0), 1, device=device) # 第一步:更新判别器 noise = torch.randn(imgs.size(0), 100, device=device) fake_imgs = generator(noise) # 生成器前向 real_pred = discriminator(real_imgs) # 真实图像预测 fake_pred = discriminator(fake_imgs.detach()) # 生成图像预测 loss_D = criterion(real_pred, real_label) + criterion(fake_pred, fake_label) optimizer_D.zero_grad() loss_D.backward() optimizer_D.step() # 第二步:更新生成器 fake_pred = discriminator(fake_imgs) # 重新前向,不 detach loss_G = criterion(fake_pred, real_label) # 目标是让判别器判真 optimizer_G.zero_grad() loss_G.backward() optimizer_G.step() if i % 100 == 0: print(f"Epoch [{epoch}/{num_epochs}] Batch [{i}] " f"D_loss: {loss_D.item():.4f} G_loss: {loss_G.item():.4f}")

这里的梯度流控制是理解 GAN 训练的关键。更新判别器时用 fake_imgs.detach() 切断生成器计算图,反向传播只更新判别器参数;更新生成器时重新前向计算 fake_pred,让梯度能穿过生成器的所有参数。如果忘记 detach,判别器更新时会把梯度连带传回生成器,两个网络的参数会在同一步内被重复更新,训练必然震荡。

lr=0.0002 和 betas=(0.5, 0.999) 是 DCGAN 论文明确指定的配置,尤其 beta1=0.5 不能随便改。PyTorch 里 Adam 默认的 beta1 是 0.9,代表对历史梯度的一阶矩估计惯性大,适合普通监督学习;但 GAN 的梯度方向本身不稳定,惯性太大会让优化器沿着过时方向继续走,训练曲线剧烈振荡。学习率可以微调,但一般不超过 0.0002,低于 0.0001 则收敛变慢。

3. 从零复现训练环境:PyTorch 安装、CelebA 数据准备与参数调优

要把这份源码跑起来,需要解决三个现实问题:PyTorch 版本怎么选不翻车、CelebA 数据目录怎么组织、训练参数调到什么程度合适。这一章按顺序给出可直接执行的命令和判断标准。

3.1 环境搭建:PyTorch、CUDA 与显卡驱动的版本匹配

PyTorch 安装的坑多半不在 pip 命令本身,而在版本匹配。我一般的检查顺序是:先看显卡驱动支持的最高 CUDA 版本,再选对应编译版本的 PyTorch。30 系及以上的显卡驱动通常能支持 CUDA 11.8 或 12.x,装官方源里的 cu118 或 cu121 版本都没问题;10 系老卡建议用 cu118 以下的版本。

# 查看显卡驱动信息和 CUDA 版本 nvidia-smi # 右上角 CUDA Version 是驱动支持的最高版本,不是已安装的 CUDA # 创建虚拟环境并安装 PyTorch(以 CUDA 11.8 为例) conda create -n dcgan python=3.9 -y conda activate dcgan pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118

这里有一个高频误区:nvidia-smi 显示的 CUDA 版本只是驱动支持的上限,不代表你要额外安装对应版本的 CUDA toolkit。PyTorch 在 pip 安装时自带 CUDA 运行库,只要驱动版本不低于 PyTorch 的要求就能跑。验证 PyTorch 是否真正用上 GPU,用下面这段代码,不要只看安装日志:

# 验证 PyTorch GPU 可用性 import torch print(torch.__version__) print(torch.cuda.is_available()) # 必须为 True print(torch.cuda.get_device_name(0)) # 显示实际显卡型号 device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

如果 torch.cuda.is_available() 返回 False,常见原因有三个:装成了 CPU 版 PyTorch(pip install torch 默认 CPU 版)、驱动版本太旧、或者 conda 环境里存在 CPU 版和 GPU 版混乱。Windows 上最简单可靠的路径是直接装官方预编译的 GPU wheel 包,不要先手动装 CUDA toolkit 再装 PyTorch,那样反而容易版本冲突。CPU 版也能跑通训练,但 CelebA 全量有几万张图,CPU 一个 epoch 可能要几十分钟,建议至少用一张 4G 显存以上的显卡。

3.2 CelebA 数据集下载与目录组织

CelebA 数据集包含 20 多万张名人面部图片,每张图带 40 个属性标注,数据量大、质量高、标注完整,是 GAN 图像生成最常用的真实人脸数据集之一。项目用的是全量数据训练,第一次复现建议先用 5000 到 10000 张子集跑通流程,再上全量。下载解压后,目录要按 ImageFolder 要求的格式组织:

# 数据目录结构(ImageFolder 要求) data/ └── celeba/ ├── img_align_celeba/ # 放全部图片 │ ├── 000001.jpg │ ├── 000002.jpg │ └── ... └── list_attr_celeba.txt # 属性标注(本训练不用但保留)

由于 CelebA 里的图片都是正脸人脸,不需要按类别分子目录,数据集 root 指向外层 data/celeba 即可。ImageFolder 会自动遍历所有子目录下的图片。加载不到图的常见原因就是路径写错,比如把 root 写成了 data/celeba/img_align_celeba 而不是 data/celeba,或者下载的压缩包解压后多了一层同名目录。还有一个细节:CelebA 的文件名不是严格从 000001 连续编号的,不要在代码里假设索引和文件名一一对应。

3.3 训练参数:从默认配置到自定义修改

项目默认参数基本是 DCGAN 原文的标准值,直接跑可以复现论文效果,但完全照抄在个人显卡上会碰显存瓶颈。参数调整优先动三个:batch_size、num_epochs、latent_dim。

  • batch_size:默认 128,8G 显存建议降到 64,4G 显存建议 32。改小 batch 后学习率要同步微调,因为小 batch 的梯度估计噪声更大,建议 lr 从 0.0002 降到 0.0001。
  • num_epochs:CelebA 全量数据一般 20 个 epoch 左右能看到清晰人脸。前 5 个 epoch 出人脸轮廓,10 个 epoch 后五官逐渐清晰。验证代码是否跑通,用 3 个 epoch 加几千张子集图就够了。
  • latent_dim:100 维是标准值,不用改。想增大潜在空间容量可以加到 128,但更高维度会让生成器更难收敛,收益不一定明显。

如果改了图像尺寸、网络结构或者 latent_dim,项目附带的 model_final.pth 权重就不能直接加载了,需要重新训练。模型权重和代码配置是严格绑定的,这一点在后续做迁移实验时要记住。

4. 训练避坑指南:损失异常、模式塌缩与显存不足的排查记录

这一章写的都是我复现过程中真实踩过的坑,每一条按「现象 → 原因 → 解决」展开,可以对照自己的训练日志逐项排查。这些坑单独看都很小,但任何一个都能让训练白跑几个晚上。

4.1 现象:判别器损失直接掉到 0,生成器梯度消失

训练刚开始几百个 iteration,D_loss 降到 0.001 以下,G_loss 一直飘在 10 以上,生成图像全是噪点没有任何结构。原因是判别器太强,真实图像和生成图像的分布被完全分离开,生成器无论输出什么都被一眼识破,梯度失去了指导意义。这是 GAN 训练里最经典的均衡问题——生成器和判别器的能力必须此消彼长地同步提升。

解决的办法按顺序试三个。先把生成器的学习率单独调到 0.0004,缩小和判别器的能力差距;不行就给判别器加 Dropout,一般 0.3 到 0.5,降低它对训练集的记忆能力;最后可以改成每训练两次生成器再训练一次判别器,给生成器更多学习机会。项目源码里没有默认给判别器加 Dropout,自己加的时候放在每个卷积块之后、激活之前。

4.2 现象:生成图像全部塌缩成同一种脸型

训练到第 10 个 epoch,生成出来的图每张都是几乎相同的脸,姿态、肤色、表情都差不多,这就是 Mode Collapse。原因是生成器找到了判别器的盲区——只要输出一种能稳定骗过判别器的图像,就停留在原地不再探索其他模式。CelebA 这种类别单一但姿态多样的人脸数据尤其容易出现,因为判别器对全局结构的判断比局部细节更敏感。

处理 Mode Collapse 没有一击必杀的方案,常见做法是组合拳:给 latent 输入加噪声(把噪声向量一部分元素随机置零),或者调整训练比例让判别器更严格。更实用的做法是在每个 epoch 末尾保存一批生成图像,像项目里 Generated_Epoch_1.png 到 Generated_Epoch_10.png 那样连续对比,如果从第 5 个 epoch 开始面孔就不再变化,基本可以确认塌缩。重新训练时把 z 的维度加大,或者把判别器加深增加它的判别粒度,都有一定帮助。

4.3 现象:训练中途显存溢出,进程直接崩溃

报错信息是 CUDA out of memory,往往发生在某个 epoch 的中段。显存不足最直接的原因是 batch_size=128 加 64×64 图像尺寸,中间特征图占用超出显卡容量。尤其判别器要同时处理真实图像和生成图像的梯度,显存占用接近双倍。

# 第一步:确认是训练占用还是缓存占用 nvidia-smi # 看进程列表里 Python 进程的显存占用,如果高于 80% 基本就是训练本身超了

解决方式按效果排序:batch_size 降到 64 或 32,同时把 DataLoader 的 pin_memory 改成 False 减少额外锁页内存;还不够就把 num_workers 降到 2 或 0;仍然溢出,只能把图像输出尺寸从 64 降到 48,但需要同步修改生成器和判别器的卷积层参数,新手不建议折腾。PyTorch 2.x 支持混合精度训练,但 DCGAN 对精度变化敏感,我在训练里不主动开 AMP,容易诱发 NaN。

4.4 现象:图像训练到一半不再变化,loss 卡在某个固定值

生成图像停留在半清晰状态,D_loss 和 G_loss 来回拉锯但图像没有本质提升。这种停滞通常是两个原因叠加:一是学习率太小,后期优化器步长不足以逃离局部平衡;二是 BatchNorm 的 running_mean 和 running_var 在训练后期变得非常稳定,生成器对输入的响应趋于固化。

我的做法是在训练中段把 lr 手动降一个数量级到 0.00002,让优化器进入精细化调整阶段,这个操作对恢复图像细节效果明显。想要更自动的方案,可以给两个优化器挂 torch.optim.lr_scheduler.CosineAnnealingLR,周期设成剩余 epoch 数。注意:改 lr 的时刻很关键,太早会让模型停在粗糙轮廓,太晚则浪费计算资源,一般选在 60% epoch 处。

4.5 现象:loss 出现 NaN,训练直接中止

NaN 出现的直接原因是梯度值或损失值溢出。常见诱因有三个:某个 batch 的输入数据包含异常值(图像文件损坏、像素越界)、网络权重初始化范围不合适、学习率过大导致激活值爆炸。CelebA 原图质量整体高,但偶尔会有损坏的图片文件,ImageFolder 读取时不会报错,数据值却可能是错的。

# 在训练循环里加一道数据检查,发现非法值直接跳过 if not torch.isfinite(imgs).all(): print(f"Batch {i} contains invalid values, skipped") continue

处理 NaN 的第二个动作是检查权重初始化。DCGAN 原版推荐用均值为 0、标准差 0.02 的正态分布初始化卷积层和 BatchNorm 层。如果源码用的是 PyTorch 默认初始化,训练到较深的层数时有概率触发梯度爆炸。补上权重初始化逻辑后,NaN 问题基本能根除。

5. 模型验证与进阶技巧:权重加载、潜在空间插值与训练监控习惯

项目附带训练好的 model_final.pth 权重,路径在 model 目录下,直接加载就能生成 CelebA 风格人脸,不需要重新训练。这部分讲两个不改变网络结构就能做的验证实验,以及一个我坚持了很久的训练监控习惯。

5.1 用 training 好的权重批量生成新图像

项目的 generate.py 就是干这件事的:加载权重、生成随机噪声、前向推理、保存图像。下面是兼容 CPU 加载 GPU 权重的方式:

# generate.py - 加载训练好的模型并生成新图像 import torch from torchvision.utils import save_image from dcgan import Generator latent_dim = 100 device = "cuda" if torch.cuda.is_available() else "cpu" generator = Generator(latent_dim).to(device) # 训练时在 GPU 上保存的权重,CPU 加载必须指定 map_location generator.load_state_dict(torch.load("model/model_final.pth", map_location=device)) generator.eval() with torch.no_grad(): noise = torch.randn(64, latent_dim, device=device) fake_imgs = generator(noise) # 输出值域是 [-1, 1],需映射回 [0, 1] 才能正常显示 save_image((fake_imgs + 1) / 2, "generated_new.png", nrow=8)

两个细节值得记住。第一,map_location 参数必须加,否则在 CPU 机器或不同型号 GPU 上会报 device 不匹配错误。第二,生成器输出值域是 [-1, 1],直接保存会得到偏暗的图像,先加 1 再除以 2 恢复日常的像素范围。eval() 模式会固定 BatchNorm 的统计量,保证多次生成结果稳定。

5.2 潜在空间插值实验:检验特征空间是否连续

这是一个能直观检验生成器质量的实验:取两个随机噪声向量 z1 和 z2,在它们之间线性插值,每一步生成一张图像,连续看时脸型、表情应该平滑过渡。如果中间突然跳变或产生扭曲面孔,说明特征空间断裂,模型训练不充分。

# 潜在空间插值演示 z1 = torch.randn(1, latent_dim, device=device) z2 = torch.randn(1, latent_dim, device=device) for t in range(11): # 在 z1 和 z2 之间取 11 个点 z_interp = z1 + (z2 - z1) * (t / 10) with torch.no_grad(): img = generator(z_interp) save_image((img + 1) / 2, f"interp_{t:02d}.png")

插值平滑说明生成器学到了连续且语义化的人脸特征空间,比如表情、姿态、光照在插值中会渐进变化;如果图像在某个点突然崩坏,通常意味着训练 epoch 不够或者发生了模式塌缩。这个实验比单看几张生成图更能暴露模型的真实状态。

最后一个建议,也算是我踩过无数次坑之后形成的习惯:每次训练开始前,固定一组噪声种子和采样间隔,每隔固定 iteration 就用这些固定噪声生成一版图像,类似项目里 Generated_Epoch_1.png 到 Generated_Epoch_10.png 的做法。这样能把训练过程的中间状态完整串联起来,翻车时翻回去看中间帧,一眼就能定位模型是从哪个 epoch 开始崩的。从那以后,我每跑一次 DCGAN 都会强制走一遍这个流程,损失曲线和生成图结合着看,模型好坏基本不再靠猜。希望这篇拆解对你跑通这份项目源码有帮助,不管是新手入门还是拿去做基线实验,都值得动手复现一次。

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

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

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

立即咨询