多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Pytorch实战DCGAN:在CelebA数据集上生成逼真人脸

Pytorch实战DCGAN:在CelebA数据集上生成逼真人脸 简介一套基于Pytorch在CelebA数据集上训练DCGAN的完整项目源码面向深度学习入门者与中级开发者旨在通过实战掌握生成对抗网络的原理与图像生成流程。压缩包共12个文件包含4个核心Python脚本分别负责模型搭建、训练流程、图像生成与辅助工具5张训练过程可视化图片展示了生成样本和损失函数变化另有1个GIF演示动画、1份README说明文档以及1个训练好的模型权重文件整体大小约102.4MB目录结构清晰便于按模块查阅。项目从搭建生成器和鉴别器开始涵盖CelebA数据预处理、损失函数与优化器选择、训练轮次监控以及生成图像结果分析完整呈现了DCGAN的训练闭环并提供了最终模型供直接测试。已有399人学习适合希望将GAN理论落地为可运行项目的研究者和爱好者也可迁移到其他图像生成任务中复用。1. 从一张噪声图到一张人脸DCGAN在CelebA上究竟学到了什么第一次把 DCGAN 在 CelebA 上跑起来的人几乎都会在某个凌晨盯着训练日志里那条死活不降的 loss 曲线怀疑人生然后在某次刷新保存的生成图片时突然看到一张虽然模糊但轮廓清晰的人脸——那种感觉跟开盲盒开到隐藏款差不多。这个项目标题看起来很常规但它其实是很多人踏入生成式模型的第一道门槛用 Pytorch 在 CelebA 人脸数据集上把 DCGAN 完整地训练出来让一个 100 维的随机噪声向量通过生成器变成一张 64x64 的逼真人脸。它能解决的问题很直接搞懂生成对抗网络里生成器和判别器是怎么博弈的、Pytorch 的动态图机制到底给这种对抗训练带来了什么便利、以及一套能跑通的最小代码骨架长什么样。适合两类人刚学完 Pytorch 基础框架想找个完整实战练手的人和已经跑过分类或回归模型、想看看生成式模型和判别式模型在训练体验上到底有多大差别的人。2. 先把地基打牢Pytorch 环境、CelebA 数据集与目录组织2.1 装对 PytorchCUDA 版本匹配与 conda 环境隔离DCGAN 的训练对显存和 CUDA 版本特别敏感很多人在第一个环节就翻车了。我见过最多的问题是GPU 驱动是新的但装 Pytorch 时选了 CPU 版本训练一晚上才发现根本没用上显卡或者 CUDA 版本对不上一跑就报CUDA error: no kernel image is available for execution on the device。我的固定流程是先用 conda 建一个独立环境然后根据显卡驱动去官网找对应的安装命令。这里建议进入 conda 环境后先确认显卡驱动支持的最高 CUDA 版本再决定装哪个版本# 创建独立环境Python 版本选 3.9 或 3.10 都比较稳 conda create -n dcgan python3.10 -y conda activate dcgan # 查看本机显卡驱动支持的 CUDA 版本 nvidia-smi # 以 CUDA 11.8 为例安装 GPU 版 Pytorch pip install torch2.1.0 torchvision0.16.0 --index-url https://download.pytorch.org/whl/cu118 # 验证 Pytorch 能否调用 GPU python -c import torch; print(torch.__version__, torch.cuda.is_available(), torch.cuda.get_device_name(0))这段命令里有三个关键点第一conda activate dcgan之后所有的包都装在这个环境里跟其他项目隔离后面换模型调库不会互相污染第二nvidia-smi显示的 CUDA 版本是驱动支持的版本必须保证 Pytorch 的 CUDA 版本不高于这个数字第三最后的验证语句里torch.cuda.is_available()返回True才算真的装好了。很多人的 Pytorch 环境搭建看起来成功就是栽在这一步——包装上了但 CUDA 的二进制库不匹配跑起来还是用 CPU 硬算。Pytorch 的版本之间 API 差异不大但如果你用的源码包是基于某个旧版本写的建议先看看源码里的import torch之后有没有用到一些被弃用的接口比如torch.nn.functional.sigmoid这类老写法。遇到报错也不要急着换版本先搜一下对应 API 在新版里的替代写法往往比折腾环境快得多。2.2 拿到 CelebA下载源选择与本地目录组织CelebA 是一个公开的人脸属性数据集包含 20 多万张名人脸部图像每张图都有标注可以说是中文社区里被用得最多的 GAN 入门数据集之一。这个数据集的获取方式有点讲究原站提供的下载速度不稳定国内直连经常中断。常见做法是直接用 Pytorch 内置的torchvision.datasets.CelebA接口下载但这个接口底层走的是 Kaggle 的认证流程新手很容易卡在证书配置上。我更推荐手动下载img_align_celeba.zip然后解压到本地目录。虽然这是个人习惯但这样能保证后续用ImageFolder读取时目录结构完全可控不用受制于内置接口的目录约定。下载和解压完成之后把目录组织成下面的结构CelebA/ ├── img_align_celeba/ │ ├── 000001.jpg │ ├── 000002.jpg │ └── ... └── list_attr_celeba.txt这里的关键是把所有图片放在一个纯图片目录里list_attr_celeba.txt是属性标注文件用于后续可能做的条件 GAN 训练现在可以暂时不管它。用ImageFolder读取时它会自动把根目录下每个子目录当作一个类别这里只有一个子目录所以classes只有 0 这一个类别训练判别器时只做真伪二分类就够了。2.3 数据读取验证一张代码确认图片通道和值域数据集拿到后不要急着训练先写一段代码确认图片能读出来、尺寸和通道数符合预期。这一步能帮你把“数据链路有问题”这个变量从后续的排错中彻底排除。from torch.utils.data import DataLoader from torchvision import datasets, transforms # 注意这里的 transform 会先把图片缩放到 64x64再转成 Tensor # 最后归一化到 [-1, 1]这个设计跟生成器输出层的 Tanh 是对应的 transform transforms.Compose([ transforms.Resize((64, 64)), transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)), ]) dataset datasets.ImageFolder(root./CelebA, transformtransform) dataloader DataLoader(dataset, batch_size128, shuffleTrue, num_workers4, pin_memoryTrue) # 验证读取结果 data_iter iter(dataloader) images, _ next(data_iter) print(f单张图片尺寸: {images[0].shape}) # 期望输出 torch.Size([3, 64, 64]) print(f像素值域: [{images.min():.2f}, {images.max():.2f}]) # 期望接近 [-1, 1]这段代码的用意很明显Resize((64, 64))把 CelebA 原始的大图缩放到 64x64这是 DCGAN 原论文里生成输出的分辨率Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))把像素值从 [0, 1] 映射到 [-1, 1]正好匹配生成器最后一层用Tanh的输出范围。如果这里不做归一化后面训练时生成器很难学习因为它的输出被Tanh限制在 [-1, 1]而真实图片却还在 [0, 1]两者根本不在同一个空间。跑完这段代码看到尺寸是 [3, 64, 64] 且值域接近 [-1, 1]数据链路才算打通。3. 搭出 DCGAN 的生成器和判别器结构决定成败3.1 生成器从 100 维噪声到 64x64 人脸的转置卷积之路DCGAN 的生成器本质上是一个“上采样”的过程输入一个 100 维的随机噪声向量经过一层全连接展开成足够大的特征图再用转置卷积逐步放大直到变成 3 通道的 64x64 图像。这里每一步上采样的通道数安排直接决定了生成质量我一般遵循原论文的设定从 1024 个通道逐步减半到 3 个通道。写成 Pytorch 代码是这个样子import torch.nn as nn class Generator(nn.Module): def __init__(self, latent_dim100): super(Generator, self).__init__() # 全连接层把噪声向量映射到 8x8x1024 的特征图 self.fc nn.Linear(latent_dim, 8 * 8 * 1024) # 主网络四层转置卷积逐步从 8x8 放大到 64x64 self.main nn.Sequential( # 输入: 1024 x 8 x 8, 输出: 512 x 16 x 16 nn.ConvTranspose2d(1024, 512, kernel_size4, stride2, padding1, biasFalse), nn.BatchNorm2d(512), nn.ReLU(True), # 输入: 512 x 16 x 16, 输出: 256 x 32 x 32 nn.ConvTranspose2d(512, 256, kernel_size4, stride2, padding1, biasFalse), nn.BatchNorm2d(256), nn.ReLU(True), # 输入: 256 x 32 x 32, 输出: 128 x 64 x 64 nn.ConvTranspose2d(256, 128, kernel_size4, stride2, padding1, biasFalse), nn.BatchNorm2d(128), nn.ReLU(True), # 输出层: 128 x 64 x 64 - 3 x 64 x 64 nn.ConvTranspose2d(128, 3, kernel_size4, stride2, padding1, biasFalse), nn.Tanh() ) def forward(self, z): x self.fc(z) x x.view(-1, 1024, 8, 8) x self.main(x) return x这段代码里几个容易被忽略的细节值得说清楚。第一biasFalse是因为后面接了BatchNorm2dBN 层自带可学习的偏置项卷积层再加 bias 就冗余了还会带来训练不稳的风险。第二nn.ReLU(True)里那个True表示inplaceTrue省显存在生成器里可以放心用。第三每层转置卷积的stride2, padding1是精心算过的输入 8x8经过第一层变成 16x16再变成 32x32最后 64x64尺寸正好翻倍两次到目标分辨率。这个尺寸演算表可以在写代码前先列好不然某一层参数算错后面输出尺寸跟预期差一倍模型根本跑不起来。之所以生成器用转置卷积而不是普通的Upsample Conv2d是因为转置卷积是目前实践里效果最稳定的上采样方式。Upsample加卷积虽然参数量更小但容易出现棋盘格伪影像素重组PixelShuffle效果更好但实现更复杂不适合入门项目。DCGAN 原论文也用的是转置卷积新手照做踩坑最少。3.2 判别器把 64x64 人脸压回 1 个真伪概率判别器的结构跟生成器正好镜像输入一张 3 通道的 64x64 图像通过四层普通卷积逐步压缩空间尺寸、增加通道数最后输出一个标量表示输入图片是真实图片的概率。这个标量经过 Sigmoid 之后落在 [0, 1] 之间1 表示“坚信这是真的”0 表示“坚信这是假的”。判别器的实现代码如下class Discriminator(nn.Module): def __init__(self): super(Discriminator, self).__init__() self.main nn.Sequential( # 输入: 3 x 64 x 64, 输出: 64 x 32 x 32 nn.Conv2d(3, 64, kernel_size4, stride2, padding1, biasFalse), nn.LeakyReLU(0.2, inplaceTrue), # 输入: 64 x 32 x 32, 输出: 128 x 16 x 16 nn.Conv2d(64, 128, kernel_size4, stride2, padding1, biasFalse), nn.BatchNorm2d(128), nn.LeakyReLU(0.2, inplaceTrue), # 输入: 128 x 16 x 16, 输出: 256 x 8 x 8 nn.Conv2d(256, 256, kernel_size4, stride2, padding1, biasFalse), nn.BatchNorm2d(256), nn.LeakyReLU(0.2, inplaceTrue), # 输入: 256 x 8 x 8, 输出: 512 x 4 x 4 nn.Conv2d(512, 512, kernel_size4, stride2, padding1, biasFalse), nn.BatchNorm2d(512), nn.LeakyReLU(0.2, inplaceTrue), # 输出层: 512 x 4 x 4 - 1 nn.Conv2d(512, 1, kernel_size4, stride1, padding0, biasFalse), nn.Sigmoid() ) def forward(self, x): return self.main(x).view(-1, 1)注意判别器里用的是LeakyReLU(0.2)斜率为 0.2而不是生成器里的ReLU。原因很简单判别器要保留梯度信息负半轴全为零的ReLU会让一些神经元彻底死掉而LeakyReLU的负半轴仍然有微弱梯度能维持训练稳定性。另外判别器的卷积层同样biasFalse加 BN但第一层卷积没有 BN——这是因为输入图像的分布已经是固定的归一化结果不需要 BN 再去拉均值方差加了反而容易干扰信息。判别器的输出层是Conv2d(512, 1, kernel_size4)在 4x4 空间上做全局卷积得到一个值再经过Sigmoid变成概率。这个做法在工程上等价于全连接层输出一个标量但用卷积实现的优势是参数量固定不会因为输入尺寸变化而报错。3.3 权重初始化DCGAN 论文里容易跳过的 0.02DCGAN 原论文里有一个特别容易跳过的细节所有权重用均值为 0、标准差为 0.02 的正态分布随机初始化。这个初始化策略在 GAN 训练里的重要性被严重低估——如果初始权重幅度太大判别器在最初几步就能轻松区分真伪生成器会被瞬间压制梯度消失后续再也学不起来。def weights_init(m): classname m.__class__.__name__ if classname.find(Conv) ! -1: nn.init.normal_(m.weight.data, 0.0, 0.02) elif classname.find(BatchNorm) ! -1: nn.init.normal_(m.weight.data, 1.0, 0.02) nn.init.constant_(m.bias.data, 0) netG Generator(latent_dim100) netD Discriminator() netG.apply(weights_init) netD.apply(weights_init)这段初始化代码的规则是卷积层和转置卷积层的权重取 N(0, 0.02)BN 层的权重取 N(1, 0.02)、偏置置 0。很多人用net.apply(weights_init)时没注意到classname的判断逻辑如果模块命名不包含Conv或BatchNorm字样初始化就是空的。apply方法是 Pytorch 里递归遍历模块的标准做法它会从顶层模块往下访问每一个子模块非常方便但务必确保weights_init里对每类层都有处理分支否则有些层会被跳过。这种初始化方式为什么有效我不能给出一个完美的数学证明但实践中盲猜这跟 GAN 训练初期需要一个“双方都略弱”的平衡状态有关。初始权重太小生成器输出趋近于零向量判别器很快学会说“你生成的东西全是假的”初始权重太大判别器收敛太快生成器梯度饱和。0.02 这个值是原论文作者在 CelebA 和 LSUN 数据集上反复试出来的经验值属于“抄作业最佳答案”没必要自己发明。4. 训练循环与参数配置让两个网络真正博弈起来4.1 数据预处理为什么 CelebA 要 Resize 到 64x64 并归一化到 [-1, 1]生成对抗网络的核心思想是用两个相互博弈的网络提升生成效果但这件事能不能成很大程度取决于真实图片和生成图片在数据分布上是否对齐。GAN 的训练目标是最小化生成器产生的数据分布和真实数据分布之间的差异如果真实图片的像素值域跟生成器输出值域不对齐这个差异就很难被优化器稳定地缩小。常见做法是Resize((64, 64))加Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))。Normalize的公式是(x - 0.5) / 0.5它会把 [0, 1] 范围变成 [-1, 1]。这个处理强烈建议保留首先生成器最后一层用的是Tanh输出范围天然是 [-1, 1]输入和输出对齐后生成器不需要额外学一个值域偏移其次BN 层在 [-1, 1] 范围的数据上表现更稳定数据分布更对称均值更接近 0BN 的归一化效果会接近理想状态。也有人尝试过用 [0, 1] 归一化加Sigmoid输出层看起来合理但实际训练时生成器的收敛速度会明显变慢。原因在于Sigmoid的输出有饱和区梯度在两端几乎为 0而Tanh虽然在两端也饱和但它的输出是零中心的BN 处理起来更友好。不要在这个细节上花太多时间纠结照 [-1, 1] 这个设计走就行。4.2 损失函数与优化器BCELoss、Adam 和 beta10.5 的由来DCGAN 的损失函数选的是二分类交叉熵在 Pytorch 里就是BCELoss。很多人第一次实现 GAN 时会想着用 MSE但原论文里用的就是 BCE而且实践证明 BCE 在 GAN 训练里更稳。标准的对抗损失把训练拆成两步先训练判别器让它对真实图片输出接近 1、对生成图片输出接近 0再训练生成器让判别器对生成图片输出接近 1。这就是“对抗”的含义也是整个 GAN 系列算法最核心的博弈关系。判别器的损失可以写成import torch import torch.nn as nn # 定义两个优化器生成器和判别器各自独立更新 def create_optimizers(netG, netD, lr0.0002, beta10.5): optD torch.optim.Adam(netD.parameters(), lrlr, betas(beta1, 0.999)) optG torch.optim.Adam(netG.parameters(), lrlr, betas(beta1, 0.999)) return optG, optD这里的参数设计是 DCGAN 的另一个特征Adam 的beta1不是默认的 0.9而是 0.5。原因是beta1是 Adam 的动量系数默认 0.9 会让优化器对历史梯度的记忆太长导致训练后期 GAN 陷入震荡。beta10.5意味着只保留最近 50% 的历史梯度更新方向更“即时”更适应 GAN 这种每一步梯度方向剧烈变化的场景。学习率设 0.0002 也是原论文的经验值太高容易震荡太低学得太慢显卡不好的时候可以降到 0.0001但不要低于这个范围。判别器在训练时真实图片和生成图片分开计算损失再相加。这里有一个工程上容易犯的错BCELoss的标签必须是FloatTensor不是LongTensor因为这是二分类交叉熵而不是多分类的CrossEntropyLoss。真实图片的标签是 1生成图片的标签是 0这个设计对应了二分类的语义没有其他隐藏含义。4.3 训练循环与日志记录什么样的 loss 曲线是不用慌的写训练循环是整个项目最关键的一步。GAN 的训练手感跟分类网络完全不同分类网络的 loss 持续下降是正常的而 GAN 的 loss 可能一直在一个区间波动这不一定代表训练失败。判别器 loss 降到接近 0 反而是坏事说明生成器已经完全跟不上梯度基本消失后面再怎么训都很难挽回。训练循环的标准写法如下# 超参数设置 batch_size 128 lr 0.0002 beta1 0.5 latent_dim 100 n_epochs 25 # 固定噪声用于每个 epoch 结束后对比生成效果 fixed_noise torch.randn(64, latent_dim, 1, 1, devicedevice) criterion nn.BCELoss() real_label 1.0 fake_label 0.0 for epoch in range(n_epochs): for i, (images, _) in enumerate(dataloader): images images.to(device) batch_size images.size(0) # ---------- 训练判别器 ---------- netD.zero_grad() # 真实图片的损失标签全 1 output netD(images).view(-1) lossD_real criterion(output, torch.full_like(output, real_label)) # 生成假图片 noise torch.randn(batch_size, latent_dim, 1, 1, devicedevice) fake_images netG(noise) # 假图片的损失标签全 0 output netD(fake_images.detach()).view(-1) lossD_fake criterion(output, torch.full_like(output, fake_label)) lossD (lossD_real lossD_fake) / 2 lossD.backward() optD.step() # ---------- 训练生成器 ---------- netG.zero_grad() # 生成器希望判别器对假图片输出 1所以标签全 1 output netD(fake_images).view(-1) lossG criterion(output, torch.full_like(output, real_label)) lossG.backward() optG.step() # 每个 epoch 结束后用固定噪声生成一组图片保存下来 with torch.no_grad(): fake netG(fixed_noise).detach().cpu() torchvision.utils.save_image(fake, f./output/epoch_{epoch:03d}.png, normalizeTrue)代码里的逻辑顺序很严格判别器更新时fake_images.detach()是必须的它的作用是切断生成器的梯度反向传播路径让判别器只更新自己的参数不影响生成器。如果不.detach()lossD_fake.backward()时梯度会顺着生成器传回去导致生成器的梯度被利用了一次更新逻辑完全乱掉。生成器更新时fake_images是之前前向传播的结果netD(fake_images)重新做一次前向梯度的路径会经过判别器指向生成器这才能更新生成器的参数。criterion(output, torch.full_like(output, real_label))里的torch.full_like是用来生成长度和output一样、值全为 1 的标签张量。不用手动构建标签列表一张卡住 batch size 不匹配的问题另一张会自动跟随设备的类型。训练日志方面我一般每 50 个 step 打印一次 4 个值lossD_real、lossD_fake、lossG和当前已读的图片张数。只看合计的lossD看不出问题分开打印才能定位到底哪一侧先崩了。5. DCGAN 训练避坑指南4 个最常见的翻车现场5.1 显存不足batch size 和图像尺寸的妥协方案现象训练刚开始或跑了几百个 step程序报CUDA out of memory进程直接被杀死。原因显存不够的根本原因是 batch size 太大或者显卡本身显存太小。特别注意的是GAN 训练一次迭代要跑 4 次前向传播真实图、生成图、生成图重新判别、固定噪声生成显存占用比普通分类模型大很多不能拿以前跑 ResNet 的经验来估。解决先把 batch size 从 128 降到 64 或 32如果还不行就降到 16。这是最简单的调整效果立竿见影。如果 batch size 已经降到 16 还是不够剩下最后的招数是把图片尺寸从 64x64 改成 48x48 甚至 32x32但这样生成效果会打折扣。我的经验是6GB 显存跑 64 分辨率 batch size 64 勉强可以8GB 以上才舒服。另外可以检查一下有没有别的进程占着显存nvidia-smi看一下有时候是上一次崩掉的 Python 进程没有释放显存。5.2 生成图像全黑或全灰现象训练了几十个 epoch保存出来的图片要么全黑要么全灰完全看不到人脸的轮廓。原因两种情况最常见。第一种是Normalize的参数写错了比如用了mean0.5, std0.5但图片本来就是 0 到 255 的整数归一化之后变成负数加负数的组合生成器根本学不出来。第二种更隐蔽lossD_real降得很快但lossG不怎么动生成器梯度消失输出的图像会趋向于一个常数——而这个常数往往就是 0黑色或者 0.5灰色因为 0.5 是 [-1, 1] 的中点是生成器在无法学习时最容易输出的值。解决先确认数据预处理链路把images.min()和images.max()打印出来值应该在 -1 和 1 附近。然后看训练日志如果lossD_real逼近 0说明判别器已经能轻松识别真实图片生成器梯度可能饱和此时需要降低判别器的学习率或者给判别器加 Dropout 保持一点不确定性。一个取巧的临时验证方式是调低学习率到 0.0001重新训练十几个 epoch如果 loss 波动变小说明原来的学习率确实太高了。5.3 模式崩塌生成来生成去都是同一张脸现象保存的生成图片里有大量重复的脸虽然每张脸的角度稍有差异但五官、表情、发型几乎一样。原因模式坍塌是 GAN 训练里最著名的失败模式。生成器找到了一个能骗过判别器的“安全答案”就不再探索其他模式了。本质上是因为 BCELoss 下生成器只要让判别器输出接近 1 就算赢它没有动力去覆盖真实数据的全部多样性只守住最容易欺骗判别器的那个区域。解决如果是早期训练出现重复往往还有救把学习率调低给判别器加Dropout(0.3)层让它稍微“迟钝”一些不要再给真假图片非常尖锐的区分。如果已经训练了几百个 epoch 还是模式崩塌建议直接重训因为生成器已经被困在局部最优里靠微调很难跳出来。重训时可以顺手调整一个参数把噪声输入从均匀分布的torch.randn改成标准正态分布randn默认就是标准正态别用torch.rand(0, 1)宽度不够会让模式崩塌概率更高。5.4 训练震荡不收敛先看判别器是否太强现象lossD和lossG剧烈震荡每个 step 的数值变化都很大生成质量时好时坏保存的图片一次清楚一次模糊。原因判别器和生成器的能力严重不对等。判别器太强的时候它能几乎完美区分真伪生成器的梯度变得非常不稳定生成器太强的时候判别器又学不到有效特征判断能力变差无法为生成器提供有效反馈。这种失衡在 GAN 里太常见了尤其是正负样本比例固定为 1:1 而双方能力差距拉大的时候。解决给判别器加 Dropout 是最省事的手段在判别器中后段加一层nn.Dropout(0.3)能显著缓解失衡。另一个技巧是“标签平滑”label smoothing把真实图片的标签从 1 改成 0.9表达式为real_label 0.9这能让判别器不把真实图片分类得太绝对保留一些梯度空间。这两个方法按顺序试先加 Dropout不行再加标签平滑一般能解决大多数震荡问题。如果还不行最后的手段是减小判别器参数量比如通道数从 64/128/256/512 改成 32/64/128/256让它“笨”一点。6. 训练完了怎么验证用潜在空间插值检验 DCGAN 是不是真学到了人脸特征训练收敛后一个很直观且非常能说明问题的验证方式是潜在空间插值latent space interpolation。具体做法是取两个随机噪声向量 z1 和 z2在它们之间线性插值生成一系列中间向量分别送入生成器观察生成的人脸是否平滑地从一张脸过渡到另一张脸。import torch # 随机取两个噪声向量 z1 torch.randn(1, 100, 1, 1, devicedevice) z2 torch.randn(1, 100, 1, 1, devicedevice) # 在 z1 和 z2 之间插值 10 步 interpolated [] for alpha in torch.linspace(0, 1, 10): z z1 * (1 - alpha) z2 * alpha with torch.no_grad(): gen_img netG(z).detach().cpu() interpolated.append(gen_img) # 拼接成一行并保存 interpolated torch.cat(interpolated, dim0) torchvision.utils.save_image(interpolated, ./interpolation.png, nrow10, normalizeTrue)这段代码的核心在torch.linspace(0, 1, 10)它生成了从 0 到 1 的 10 个等间距系数然后按z1 * (1 - alpha) z2 * alpha做线性插值。如果模型训练得好这 10 张人脸应该呈现平滑渐变的状态第一张是 A 的脸型最后一张是 B 的脸型中间逐渐过渡五官没有突变、没有模糊成一团。这个验证的厉害之处在于它测试的不只是生成器“能不能画出一张合格的人脸”而是“它是否真的在潜在空间里学会了连续的人脸特征表示”。如果过渡过程突然出现扭曲或者直接变成噪声说明生成器学到的特征表示还不够连续可能训练 epoch 不够或者模式崩塌未完全解决。如果过渡很平滑就说明生成器真正理解了人脸的结构特征而不是机械地记住了一些固定输出。更进阶的验证方式是算 FIDFréchet Inception Distance分数FID 可以评估真实图片集和生成图片集的分布距离分数越低越好但需要额外的 Inception 模型和特征提取代码对入门项目不是必需品。潜在空间插值已经足够验证模型质量而且它给人最直观的感官反馈。这里我想分享一个小教训我刚开始调 DCGAN 的时候总想找一个确定的数值指标来告诉自己“训练成功了”后来才意识到 GAN 训练里没有标准的收敛判据各个部分存在太多玄学成分。一个 loss 曲线并不能说明模型好坏能看的只有生成图片质量和插值连贯性。后来我养成了一个习惯训练时每隔 5 个 epoch 就手动检查一次生成样本并录一段插值视频这样能尽早发现模式崩塌的苗头而不是等全部训练完才面对一脸懵的结果。希望这个习惯也能帮到你训练 GAN 本来就是试错的艺术保存好中间结果才是最大的后悔药。本文还有配套的精品资源点击获取
返回列表