DCGAN实战解析:基于CelebA的人脸生成与训练全流程
2026/9/14 4:39:57 网站建设 项目流程

简介:面向深度学习和生成对抗网络爱好者,一份基于 Pytorch 在 CelebA 人脸数据集上完整实现 DCGAN 训练与生成的实战项目。通过对抗训练展现生成器与判别器的协作逻辑,既能帮助初学者理解 GAN 原理,也适合中级开发者迁移到其他图像生成任务。资源包共 12 个文件,包含 4 个 Python 源码(模型搭建、训练、生成与工具模块)、5 张训练过程可视化 PNG、1 个演示 GIF、1 份 README 文档和 1 个最终模型权重文件,整体大小 102.4MB。已有 398 人学习下载,内容覆盖 DCGAN 网络结构定义、CelebA 数据预处理、损失函数与优化器配置、训练监控及结果分析等关键环节,并配有训练损失曲线与生成效果图,便于直观理解。通过阅读文档并运行代码,可直接观察不同训练轮数下的生成人脸质量,掌握 GAN 的调参与评估方法,为后续图像生成类项目打好基础。

1. 从一张假脸说起:DCGAN在CelebA上的实战价值

第一次打开这个项目里的Generated_Epoch_10.png时,我盯着那张人脸看了好几秒——它虽然有些模糊,但五官比例、肤色过渡都已经像模像样,完全不像是十个epoch就能产出的结果。这就是DCGAN在CelebA上的魅力:用相对简单的卷积结构,在20万张名人脸上练出一个能画脸的生成器。这个资源包把从数据预处理到模型训练的完整链路都拆开了,utils.py负责数据管线,dcgan.py定义网络,train.py执行对抗训练,generate.py做推理。对于想搞懂GAN到底怎么跑起来的人,尤其是那些卡在“模型训出来全是噪声”阶段的初学者,这份源码提供了一条能直接复用的路径。而对我这种经常要写生成模型的人来说,它也是一个很干净的基线实现:没有花哨的trick,每一步都能看清楚。

2. 生成器与判别器的DCGAN化改造:卷积替代全连接的关键设计

2.1 为什么DCGAN要用卷积:从全连接到转置卷积的动机

原始GAN在生成图像时,生成器通常先用全连接层把随机噪声映射成一维向量,再reshape成图像。这种做法的问题是:全连接层没有空间局部性,它对图像这种二维结构的学习效率很低,而且参数量巨大,容易过拟合。DCGAN的核心思路是把生成器和判别器都改造成全卷积结构,生成器用转置卷积(也叫反卷积)把低分辨率特征图逐步放大,判别器用普通卷积逐步缩小,最后输出一个标量表示真假。

这种设计带来两个直接好处。第一,卷积的局部感受野天然适合图像,每个像素只和周围像素产生连接,特征提取更高效。第二,去掉了全连接层之后,模型的参数量大幅下降,同时批量归一化(BatchNorm)可以更好地发挥作用。DCGAN还规定了几个硬性约束:生成器所有层都用ReLU(最后一层用Tanh);判别器所有层都用LeakyReLU(斜率为0.2);卷积层之后一律跟BatchNorm(判别器第一层除外)。这些约束不是拍脑袋定的——ReLU在生成器里能避免梯度饱和,LeakyReLU在判别器里防止梯度死亡,而BatchNorm能稳定每一层输入的分布,让对抗训练收敛得更快。

2.2 生成器结构解析:从100维噪声到64x64人脸

看这个项目的dcgan.py,生成器定义成了一个名为Generator的类。输入是100维的随机噪声(标准正态分布),输出是3通道64x64的图像。它的结构是四层转置卷积,每层之后跟BatchNorm和ReLU。

class Generator(nn.Module): def __init__(self, latent_dim=100, ngf=64): super(Generator, self).__init__() self.main = nn.Sequential( # 输入: (batch_size, 100, 1, 1) nn.ConvTranspose2d(latent_dim, ngf * 8, 4, 1, 0, bias=False), nn.BatchNorm2d(ngf * 8), nn.ReLU(True), # 状态: (batch_size, 512, 4, 4) nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False), nn.BatchNorm2d(ngf * 4), nn.ReLU(True), # 状态: (batch_size, 256, 8, 8) nn.ConvTranspose2d(ngf * 4, ngf * 2, 4, 2, 1, bias=False), nn.BatchNorm2d(ngf * 2), nn.ReLU(True), # 状态: (batch_size, 128, 16, 16) nn.ConvTranspose2d(ngf * 2, ngf, 4, 2, 1, bias=False), nn.BatchNorm2d(ngf), nn.ReLU(True), # 状态: (batch_size, 64, 32, 32) nn.ConvTranspose2d(ngf, 3, 4, 2, 1, bias=False), nn.Tanh() # 输出: (batch_size, 3, 64, 64) )

参数说明:latent_dim=100是随机噪声向量的长度,ngf=64控制特征图通道数的基数。第一层转置卷积的stride=1, padding=0把100维向量变成512通道的4x4特征图;后续每层stride=2, padding=1都会让特征图尺寸翻倍。最后一层输出3通道,对应RGB,激活函数用Tanh把像素值压到[-1,1],因为数据预处理时已经把原图归一化到这个区间。如果发现生成图像明显偏暗或偏亮,多半是输出层没有正确使用Tanh,或者数据没有归一化。

2.3 判别器结构解析:带BatchNorm的卷积分类器

判别器Discriminator是生成器的镜像,输入3x64x64图像,输出一个标量概率。它的每层卷积都加大通道数,但空间尺寸减半,最后通过Sigmoid输出0到1之间的真假判定。这是项目里判别器的核心代码:

class Discriminator(nn.Module): def __init__(self, ndf=64): super(Discriminator, self).__init__() self.main = nn.Sequential( # 输入: (batch_size, 3, 64, 64) nn.Conv2d(3, ndf, 4, 2, 1, bias=False), nn.LeakyReLU(0.2, inplace=True), # 状态: (batch_size, 64, 32, 32) nn.Conv2d(ndf, ndf * 2, 4, 2, 1, bias=False), nn.BatchNorm2d(ndf * 2), nn.LeakyReLU(0.2, inplace=True), # 状态: (batch_size, 128, 16, 16) nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, bias=False), nn.BatchNorm2d(ndf * 4), nn.LeakyReLU(0.2, inplace=True), # 状态: (batch_size, 256, 8, 8) nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, bias=False), nn.BatchNorm2d(ndf * 8), nn.LeakyReLU(0.2, inplace=True), # 状态: (batch_size, 512, 4, 4) nn.Conv2d(ndf * 8, 1, 4, 1, 0, bias=False), nn.Sigmoid() # 输出: (batch_size, 1, 1, 1) )

注意判别器第一层卷积后没有BatchNorm,这是原DCGAN论文里的明确设置。因为判别器的输入是原始像素,批归一化会引入batch内样本之间的依赖,破坏对单个图像的判决稳定性。ndf=64和生成器的ngf对应,两者保持对称。如果你想让判别器更“敏感”,可以把ndf调大到128,但要注意生成器也需要相应调整,否则对抗失衡。

3. CelebA数据预处理与DataLoader:从图片到张量的流水线

3.1 CelebA数据集的目录结构与读取方式

CelebA数据集下载解压后,根目录下会有img_align_celeba/文件夹,里面是大约20万张对齐后的人脸图片,每张尺寸是178x218。对齐意味着眼睛、鼻子在图像中的大致位置是固定的,所以处理起来相对统一。该项目没有直接使用原始的list_attr_celeba.txt属性文件,只用了纯图像,因为生成任务只需要图片本身,不需要标签。

utils.py里,我看到了这样的数据加载逻辑:它把img_align_celeba的路径写成了配置项,通过torchvision.datasets.ImageFolder读取。但ImageFolder要求文件按类别子目录存放,而CelebA是平铺的。常见的做法有两种:一是自己写一个Dataset类,返回图片路径和占位标签;二是直接把根目录路径传给它。这个项目采用了后者,因为CelebA的所有图片都在同一个目录下,ImageFolder会把它当作一个类别。所以用的时候别惊讶,这里的“类别”根本没有意义,我们只是借用它来做图片读取。

3.2 图像裁剪与归一化参数选择

CelebA原图是178x218,但DCGAN输出是64x64,所以需要预处理。项目里的transform定义如下:

transform = transforms.Compose([ transforms.CenterCrop(160), transforms.Resize((64, 64)), transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) ])

逻辑说明:先中心裁剪到160x160,把人脸周围的背景和头发边缘切掉一部分,保留核心五官区域。然后缩放到64x64,满足网络输入尺寸。归一化参数用的是均值和方差都是0.5,为什么这么设?因为图像像素值原本在[0,1]区间,(x - 0.5) / 0.5恰好映射到[-1,1],这正好对应生成器最后Tanh的输出范围。如果你用别的主流传标准化,比如ImageNet的均值方差,那么生成器最后一层也需要跟着改,否则生成结果会整体偏色。这个细节是DCGAN复现中常见的坑。

3.3 构建PyTorch DataLoader:批量、打乱与并行加载

DataLoader是PyTorch数据流水线的核心,它把Dataset拿到的样本组合成batch。项目里使用的是torch.utils.data.DataLoader,具体参数如下:

dataloader = DataLoader( dataset, batch_size=batch_size, # 建议128,显存不够就调小 shuffle=True, # 每个epoch都要打乱,否则生成器会学到样本顺序 num_workers=4, # 进程数,Windows下建议设为0,否则会报错 drop_last=True # 如果样本数不是batch的整数倍,丢弃最后一批 )

参数说明:shuffle=True必须明确,因为GAN训练中随机性至关重要,如果样本顺序固定,判别器很容易学到“下一张是真图”的模式。num_workers在Linux上可以调大,但Windows上由于多进程启动机制问题,超过0会经常报错,所以我看项目源码里默认了0。drop_last=True建议打开,否则最后一个batch可能只有几张图,批量归一化在这种小batch上统计不准确,会导致生成图像出现噪点。

这里补充一下环境配置。如果你用的是PyTorch 1.x,上述代码可以直接跑;如果是PyTorch 2.x,DataLoader接口几乎没变。但要注意torchvision.transforms在0.x和1.x中导入方式稍有不同,目前推荐直接from torchvision import transforms。如果你还在用老版本,遇到ModuleNotFoundError: No module named 'torchvision.transforms.functional'这种报错,八成是torchvision和torch版本不匹配,用pip install torch torchvision --upgrade能解决。

4. 训练循环与损失函数:对抗博弈的代码实现

4.1 BCEWithLogitsLoss与真实/生成标签的设置

对抗训练的核心是判别器loss和生成器loss。这个项目使用的是二分类交叉熵损失,但实现上有个细节:nn.BCEWithLogitsLoss()而不是BCELoss()。前者把Sigmoid和交叉熵合并成一个数值更稳定的函数,避免单独计算Sigmoid时出现的梯度消失。在train.py里,标签不是简单的0和1,而是用了平滑——真图标签是0.9,假图标签是0.1。这是单边标签平滑技巧,能防止判别器过于自信,反而提升生成器的收敛性。

criterion = nn.BCEWithLogitsLoss() # 真图标签 real_label = 0.9 # 假图标签 fake_label = 0.1 # 训练判别器 discriminator.zero_grad() output = discriminator(real_batch) errD_real = criterion(output, torch.full_like(output, real_label)) errD_real.backward() # 生成假图 noise = torch.randn(batch_size, latent_dim, 1, 1, device=device) fake_batch = generator(noise) output = discriminator(fake_batch.detach()) errD_fake = criterion(output, torch.full_like(output, fake_label)) errD_fake.backward() optimizerD.step()

注意fake_batch.detach()。训练判别器时,生成器的参数不需要梯度,所以要用detach()切断梯度回传。如果你忘了这一步,生成器会在反向传播时收到来自判别器的梯度,导致两个网络同时更新,对抗训练立刻崩溃,生成图像变成一片死结构。

4.2 训练判别器与生成器的交替更新步骤

一个完整的训练迭代分成两步:先更新判别器(真图+假图各计算一次loss),再更新生成器。生成器反向传播的loss是“假图被判别为真的程度”,目标是让判别器对假图输出接近真图标签。下面是标准循环:

# 更新生成器 generator.zero_grad() output = discriminator(fake_batch) # 注意这里没有detach errG = criterion(output, torch.full_like(output, real_label)) errG.backward() optimizerG.step()

这里output不取detach,因为我们要让梯度从判别器流回生成器。errG使用的标签是real_label=0.9,意思是我们希望判别器把假图当成真图。如果生成器loss降不下去,常见原因是判别器太强,生成器无论怎么改都骗不过它。此时需要调整训练比例,比如每训练一次生成器,训练三次判别器,或者反过来。这个项目的代码是每iteration各更新一次,属于经典的1:1配置。

4.3 监控训练曲线:loss震荡与图像生成的对应关系

训练过程中的loss值记录在Training_Loss.png里。你会看到D lossG loss在一起剧烈震荡,这是正常现象,不是bug。GAN的对抗过程本来就是零和博弈,一方变好意味着另一方变差,所以loss曲线不会像普通分类任务那样平滑下降。判断训练是否健康有两个标准:

第一,判别器loss没有长期趋近于0。如果D loss降到0.01左右,说明判别器把真图和假图分得过于彻底,生成器梯度消失;第二,生成器loss没有完全失控。如果G loss突然飙升到20以上,可能是学习率过大或梯度爆炸。此时应该先调低学习率,再检查是否使用了BatchNorm。

在项目给出的Training_Data.png里,每过几个epoch就把一批真实图片和一批生成图片并排保存。我发现真正有用的技巧是:固定一个随机噪声向量fixed_noise,每个epoch都用这同一个噪声输入生成器,然后把生成图拼成网格。这样你才能看到同一个输入在训练不同阶段的演化。项目里的generate.py就实现了这个功能。

4.4 训练配置参考:学习率、beta1与Epoch设置

训练超参直接决定生成质量。下面是这个项目采用的配置,也是DCGAN原论文推荐的配置。

参数说明
优化器Adam替代SGD,自适应学习率加速收敛
学习率0.0002生成器和判别器相同
beta10.5Adam的一阶矩衰减系数,默认0.9不行
beta20.999保持默认
batch_size128显存不够可降到64,但BatchNorm会震荡
latent_dim100随机噪声维度
epochs10项目默认10,实际要50+效果更好

为什么Adam的beta1要设成0.5?默认的0.9会保留大量历史梯度信息,导致优化动量过大,对抗训练中容易振荡。0.5显著降低了对过去梯度的依赖,让参数更新更“听话”。如果你用的是SGD,基本训练不出来,因为GAN的损失面非常陡峭,需要动量优化器才能穿过去。这里的epoch=10只是快速演示用的,如果生成图还是糊的,可以接着把checkpoint加载续训,或者从24epoch开始看曲线下降趋势。我测试时用默认参数训到50epoch,生成的人脸清晰度明显上了一个档次,但超过100epoch后会出现模式坍缩,美女人脸开始重复。

5. 用训练好的模型生成人脸:加载ckpt并复现可视化效果

5.1 加载model_final.pth并生成固定噪声图片

项目提供了最终训练好的权重model_final.pth,可以直接用generate.py生成图片而不需要再训练。关键代码是把生成器实例化后加载权重:

generator = Generator(latent_dim=100, ngf=64) state_dict = torch.load('model_final.pth', map_location='cpu') generator.load_state_dict(state_dict['generator_state_dict']) generator.eval()

注意torch.load时需要map_location,如果你在CPU上加载GPU训练的模型,不加这个参数会报RuntimeError: Attempting to deserialize object on a CUDA device。加载后要用generator.eval()切到评估模式,因为BatchNorm在训练和推理时的行为不一样。eval()会使用全局统计量而不是当前batch的,这样生成结果才稳定。

接着,用固定种子生成噪声,保证每次跑输出一致:

torch.manual_seed(42) noise = torch.randn(64, 100, 1, 1) with torch.no_grad(): fake = generator(noise)

torch.no_grad()必须加上,否则PyTorch会构建计算图,导致内存占用爆炸——虽然只推理一次影响不大,但如果你循环生成很多张图,就会吃掉大量显存。

5.2 使用torchvision.utils.save_image拼接网格

项目生成的Generated_Epoch_10.png是一张8x8的网格图,代码用的是make_gridsave_image

from torchvision.utils import make_grid, save_image save_image(fake, 'generated.png', nrow=8, normalize=True, range=(-1, 1))

逻辑说明:nrow=8指定每行放8张图,64张就排成8x8。normalize=True表示会根据图像的最小最大值把像素从[-1,1]映射到[0,1],因为save_image默认期望输入落在[0,1]区间,但如果我们的输出是Tanh范围,就需要显式传入range=(-1, 1),否则图像会一片死黑或一片死白。这个细节不难找,但很容易踩。

另外,我建议保存多张不同噪声下生成的结果,别只仰赖一张。用一个循环生成4个不同的网格,然后手动挑出质量最高的那张,这个过程能快速帮你判断模型有没有mode collapse。

5.3 训练过程中的图片存档与GIF制作

项目根目录下的gen_celeba.gif展示了从第一个epoch到最后一个epoch的生成效果变化。这个GIF不是用视频软件剪辑的,而是直接保存每轮生成的PNG,再用Python的imageio合成:

import imageio.v2 as imageio images = [] for epoch in range(1, 11): img = imageio.imread(f'images/Generated_Epoch_{epoch}.png') images.append(img) imageio.mimsave('gen_celeba.gif', images, duration=0.5)

参数说明:duration=0.5表示每帧显示0.5秒,你可以调成0.2或1.0来改变播放速度。在训练脚本里,每个epoch结束后自动调用一次保存逻辑,同时把loss汇总到Training_Loss.png。我个人会额外把固定噪声对应的图片单独存一个目录,比如fixed_noise/,这样GIF里能看到真正的语义演化——比如眼睛从无到有、肤色从噪点过渡到平滑。如果发现某个epoch的图突然变差,多半是学习率没调好或者batch_size太小,可以回到第4章里的配置表检查一遍。

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

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

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

立即咨询