
简介面向深度学习和生成对抗网络爱好者一份基于 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_dim100, ngf64): super(Generator, self).__init__() self.main nn.Sequential( # 输入: (batch_size, 100, 1, 1) nn.ConvTranspose2d(latent_dim, ngf * 8, 4, 1, 0, biasFalse), nn.BatchNorm2d(ngf * 8), nn.ReLU(True), # 状态: (batch_size, 512, 4, 4) nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, biasFalse), nn.BatchNorm2d(ngf * 4), nn.ReLU(True), # 状态: (batch_size, 256, 8, 8) nn.ConvTranspose2d(ngf * 4, ngf * 2, 4, 2, 1, biasFalse), nn.BatchNorm2d(ngf * 2), nn.ReLU(True), # 状态: (batch_size, 128, 16, 16) nn.ConvTranspose2d(ngf * 2, ngf, 4, 2, 1, biasFalse), nn.BatchNorm2d(ngf), nn.ReLU(True), # 状态: (batch_size, 64, 32, 32) nn.ConvTranspose2d(ngf, 3, 4, 2, 1, biasFalse), nn.Tanh() # 输出: (batch_size, 3, 64, 64) )参数说明latent_dim100是随机噪声向量的长度ngf64控制特征图通道数的基数。第一层转置卷积的stride1, padding0把100维向量变成512通道的4x4特征图后续每层stride2, padding1都会让特征图尺寸翻倍。最后一层输出3通道对应RGB激活函数用Tanh把像素值压到[-1,1]因为数据预处理时已经把原图归一化到这个区间。如果发现生成图像明显偏暗或偏亮多半是输出层没有正确使用Tanh或者数据没有归一化。2.3 判别器结构解析带BatchNorm的卷积分类器判别器Discriminator是生成器的镜像输入3x64x64图像输出一个标量概率。它的每层卷积都加大通道数但空间尺寸减半最后通过Sigmoid输出0到1之间的真假判定。这是项目里判别器的核心代码class Discriminator(nn.Module): def __init__(self, ndf64): super(Discriminator, self).__init__() self.main nn.Sequential( # 输入: (batch_size, 3, 64, 64) nn.Conv2d(3, ndf, 4, 2, 1, biasFalse), nn.LeakyReLU(0.2, inplaceTrue), # 状态: (batch_size, 64, 32, 32) nn.Conv2d(ndf, ndf * 2, 4, 2, 1, biasFalse), nn.BatchNorm2d(ndf * 2), nn.LeakyReLU(0.2, inplaceTrue), # 状态: (batch_size, 128, 16, 16) nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, biasFalse), nn.BatchNorm2d(ndf * 4), nn.LeakyReLU(0.2, inplaceTrue), # 状态: (batch_size, 256, 8, 8) nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, biasFalse), nn.BatchNorm2d(ndf * 8), nn.LeakyReLU(0.2, inplaceTrue), # 状态: (batch_size, 512, 4, 4) nn.Conv2d(ndf * 8, 1, 4, 1, 0, biasFalse), nn.Sigmoid() # 输出: (batch_size, 1, 1, 1) )注意判别器第一层卷积后没有BatchNorm这是原DCGAN论文里的明确设置。因为判别器的输入是原始像素批归一化会引入batch内样本之间的依赖破坏对单个图像的判决稳定性。ndf64和生成器的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_sizebatch_size, # 建议128显存不够就调小 shuffleTrue, # 每个epoch都要打乱否则生成器会学到样本顺序 num_workers4, # 进程数Windows下建议设为0否则会报错 drop_lastTrue # 如果样本数不是batch的整数倍丢弃最后一批 )参数说明shuffleTrue必须明确因为GAN训练中随机性至关重要如果样本顺序固定判别器很容易学到“下一张是真图”的模式。num_workers在Linux上可以调大但Windows上由于多进程启动机制问题超过0会经常报错所以我看项目源码里默认了0。drop_lastTrue建议打开否则最后一个batch可能只有几张图批量归一化在这种小batch上统计不准确会导致生成图像出现噪点。这里补充一下环境配置。如果你用的是PyTorch 1.x上述代码可以直接跑如果是PyTorch 2.xDataLoader接口几乎没变。但要注意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, devicedevice) 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_label0.9意思是我们希望判别器把假图当成真图。如果生成器loss降不下去常见原因是判别器太强生成器无论怎么改都骗不过它。此时需要调整训练比例比如每训练一次生成器训练三次判别器或者反过来。这个项目的代码是每iteration各更新一次属于经典的1:1配置。4.3 监控训练曲线loss震荡与图像生成的对应关系训练过程中的loss值记录在Training_Loss.png里。你会看到D loss和G 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的损失面非常陡峭需要动量优化器才能穿过去。这里的epoch10只是快速演示用的如果生成图还是糊的可以接着把checkpoint加载续训或者从24epoch开始看曲线下降趋势。我测试时用默认参数训到50epoch生成的人脸清晰度明显上了一个档次但超过100epoch后会出现模式坍缩美女人脸开始重复。5. 用训练好的模型生成人脸加载ckpt并复现可视化效果5.1 加载model_final.pth并生成固定噪声图片项目提供了最终训练好的权重model_final.pth可以直接用generate.py生成图片而不需要再训练。关键代码是把生成器实例化后加载权重generator Generator(latent_dim100, ngf64) state_dict torch.load(model_final.pth, map_locationcpu) 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_grid和save_imagefrom torchvision.utils import make_grid, save_image save_image(fake, generated.png, nrow8, normalizeTrue, range(-1, 1))逻辑说明nrow8指定每行放8张图64张就排成8x8。normalizeTrue表示会根据图像的最小最大值把像素从[-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(fimages/Generated_Epoch_{epoch}.png) images.append(img) imageio.mimsave(gen_celeba.gif, images, duration0.5)参数说明duration0.5表示每帧显示0.5秒你可以调成0.2或1.0来改变播放速度。在训练脚本里每个epoch结束后自动调用一次保存逻辑同时把loss汇总到Training_Loss.png。我个人会额外把固定噪声对应的图片单独存一个目录比如fixed_noise/这样GIF里能看到真正的语义演化——比如眼睛从无到有、肤色从噪点过渡到平滑。如果发现某个epoch的图突然变差多半是学习率没调好或者batch_size太小可以回到第4章里的配置表检查一遍。本文还有配套的精品资源点击获取