尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

DCGAN实战:从零训练生成对抗网络打造动漫头像生成器

DCGAN实战:从零训练生成对抗网络打造动漫头像生成器 1. 项目整体设计与思路拆解1.1 为什么选择DCGAN来生成动漫头像打开这篇文章的朋友多半已经看过我前面两篇DCGAN实战文章了。第一篇我们讲了GAN的基本对抗思想第二篇把DCGAN在MNIST和CIFAR上的代码过了一遍。这次正好进入最有意思的部分——用DCGAN生成二次元动漫头像。说实话GAN这个方向能玩出视觉冲击力的任务不多动漫头像生成算是其中之一训练过程也比较直观生成结果肉眼可见地变好正反馈很强。之所以选DCGAN而不是原始的GAN或者别的变体核心原因是DCGAN把卷积网络引入生成器和判别器之后生成质量的稳定性大幅提升。原始GAN用全连接网络处理图像参数量大、特征提取能力弱到64x64这种分辨率基本就开始费劲了。DCGAN在结构上有几个关键改动——用带步长的卷积代替池化做下采样、用转置卷积做上采样、在卷积层后面加批归一化BatchNorm、激活函数统一换成ReLU族。这些改动组合起来让网络在中等分辨率图像上也能学出有意义的结构特征动漫头像这种边缘锐利、色块分明的图片恰好是DCGAN擅长的范围。另外一个现实原因是资源开销。动漫生成最出名的StyleGAN系列我也想跑但一张RTX 3060显存只有12GStyleGAN动辄几十个小时的训练时间对个人项目来说太奢侈了。DCGAN不一样模型轻、显存占用小一张普通消费级显卡就能在几小时内完成训练。我之前在CIFAR-10上面跑了一遍DCGAN50个epoch大约半小时搞定换成64x64的动漫头像200个epoch也就在两到三小时左右完全在个人可接受的范围内。1.2 整体技术路线与前置条件这次项目的整体流程可以分为四步准备数据集、构建模型、训练、评估生成效果。听起来简单但每一步都有不少容易踩的坑稍后我会挨个细说。先交代一下我使用的环境已经在前面文章里详细写过安装步骤这里只列关键版本Python 3.9PyTorch 1.13后续2.0、2.1版本也兼容torchvision自带数据集加载功能CUDA 11.7如果没有NVIDIA显卡纯CPU也能跑只是速度慢非常多matplotlib 用于可视化生成结果提示如果你没有GPU环境建议把图片分辨率降到32x32、batch_size降到64这样在CPU上也能跑通但训练时间会很长。我的建议依然是先想办法搞到一支支持CUDA的NVIDIA显卡哪怕是老一点的GTX 1060 6G训练体验也完全不一样。模型层面我会完整实现一个DCGAN的生成器和判别器这两个网络加起来大概只有几百万个参数比很多分类网络还要轻量。数据层面使用公开的动漫人脸数据集图像统一缩放到64x64分辨率这个尺寸是DCGAN原始论文验证过的标准配置不需要额外调参。训练时每轮固定生成一组随机噪声喂给生成器得到一组中间结果直观观察模型收敛过程。2. 数据集准备与预处理2.1 动漫头像数据从哪来动手训练之前最头疼的其实是数据这一点相信很多做过图像生成的朋友都有体会。我当时先尝试了自己从零开始收集结果发现二次元头像的公开资源非常零散质量参差不齐很多图片还带水印清洗成本极高。后来决定使用社区内广泛使用的公开动漫人脸数据集在Kaggle上可以搜到一份比较经典的anime faces数据集里面包含数万张已经裁剪好的头像图片分辨率基本都在96x96以上够用。如果你还没用过Kaggle注册后在数据集页面选择Download解压之后就能看到大量jpg图片。用PyTorch的ImageFolder来读取这类纯图片目录非常方便不需要自己写复杂的数据解析逻辑。可能有人会问为什么不直接用torchvision自带的下载脚本因为torchvision并没有内置动漫头像数据集所以走ImageFolder是最灵活的方案。注意务必确认自己拿到的数据集是公开合规的。看license时不能只看数据集本身还要看图片版权归属。个人学习实验一般没有太大问题但如果你要商用模型权重来源问题一定要查清楚。2.2 图像预处理的关键细节拿到原始图片后预处理环节直接决定训练能不能收敛。我踩过一次很深的坑最初把图片读进来直接扔给模型结果生成器损失完全不动。后来检查发现问题出在没有把像素值归一化到[-1, 1]区间。很多入门资料会告诉你归一化很重要但很少解释原因——GAN的生成器输出层一般用的是tanh激活函数输出范围正好是[-1, 1]如果真实图片却在[0, 1]区间判别器很容易通过学习输出值的大小直接区分真实和生成图片根本不需要学习图像结构特征这样一来生成器学到的信息几乎为零。正确的做法是用torchvision.transforms做两步处理先转换成Tensor自动把像素缩放到[0,1]再用Normalize把数值变换到[-1,1]。代码很简短transform transforms.Compose([ transforms.Resize((64, 64)), transforms.ToTensor(), transforms.Normalize(mean[0.5, 0.5, 0.5], std[0.5, 0.5, 0.5]) ])Resize到64x64的尺寸是DCGAN原始论文验证过的标准配置。也许你会问为什么不用更大的128x128DCGAN这个结构的生成器在超过64x64之后容易出现棋盘伪影因为转置卷积叠加时会有重复像素的问题。我们用64x64可以规避大量调参工作后续如果想让头像更清晰可以再加一层上采样模块但训练稳定性会明显变差得不偿失。2.3 DataLoader的batch_size与数据增强选择数据处理这块第二个容易纠结的点是batch_size。理论上batch越大训练越稳定但显存容量有限。我在12G显存的显卡上测试过DCGAN生成器和判别器加起来占用大约3-4Gbatch_size给到128甚至256都没问题。如果显存只有4G建议降到64。在动漫头像这个任务里我建议不要加太多数据增强。翻转、裁剪、色彩抖动这些常规增强手段大多是为分类任务设计的GAN训练中增强会改变真实图片的分布导致判别器评估失真。我实际测试下来只做一个随机水平翻转就已经足够了其他增强反而让生成图片出现过拟合式的不自然纹理。DataLoader构建代码dataset torchvision.datasets.ImageFolder( root./anime_faces, transformtransform ) dataloader torch.utils.data.DataLoader( dataset, batch_size128, shuffleTrue, num_workers2, pin_memoryTrue )数据集大小对最终效果影响非常明显。我试过用5000张图训练生成的图片风格非常单一五官细节也容易崩坏换到几万张图之后生成结果出现明显改观。动漫人脸的结构模式相对统一数据集如果太小模型会陷入对所有样本的“平均脸”记忆丢失多样性。所以我的结论是数据量能多就不凑合至少20000张以上。3. 模型结构与核心参数3.1 生成器结构设计理解了数据之后下一步把两个网络搭建起来。DCGAN的灵魂在于生成器和判别器如何分布卷积层。我从原始论文出发用代码一行行实现。生成器的输入是一个100维的随机噪声向量z噪声从标准正态分布中采样。这个100维的向量可以理解为模型的“创意空间”每个维度代表某种图像属性模型在训练中会学会把这些随机数字映射成有意义的图像特征。接下来的问题是100维向量怎么变成64x64x3的图片DCGAN的思路是连续做四次转置卷积ConvTranspose2d每次把特征图尺寸翻倍。为了方便理解可以把整个结构分成几个阶段第一阶段把100维向量通过全连接层映射成512x4x4的特征图相当于把噪声“铺开”成一张极低分辨率的特征图第二阶段转置卷积上采样到8x8通道数从1024降到512第三阶段上采样到16x16通道数降到256第四阶段上采样到32x32通道数降到128第五阶段上采样到64x64通道数降到3得到RGB图像。中间每一层都接BatchNorm2d和ReLU激活函数最后一层使用tanh把输出值映射到[-1,1]。生成器代码class Generator(nn.Module): def __init__(self, latent_dim100): super(Generator, self).__init__() self.latent_dim latent_dim self.fc nn.Sequential( nn.Linear(latent_dim, 512 * 4 * 4), nn.BatchNorm1d(512 * 4 * 4), nn.ReLU(True) ) self.conv_layers nn.Sequential( # 4x4 - 8x8 nn.ConvTranspose2d(512, 256, kernel_size4, stride2, padding1), nn.BatchNorm2d(256), nn.ReLU(True), # 8x8 - 16x16 nn.ConvTranspose2d(256, 128, kernel_size4, stride2, padding1), nn.BatchNorm2d(128), nn.ReLU(True), # 16x16 - 32x32 nn.ConvTranspose2d(128, 64, kernel_size4, stride2, padding1), nn.BatchNorm2d(64), nn.ReLU(True), # 32x32 - 64x64 nn.ConvTranspose2d(64, 3, kernel_size4, stride2, padding1), nn.Tanh() ) def forward(self, z): x self.fc(z) x x.view(x.size(0), 512, 4, 4) x self.conv_layers(x) return x细心的读者可能注意到这个结构比很多精简版多了一个BatchNorm层这是我个人坚持保留的。去掉了BatchNorm的DCGAN虽然也能训练但训练稳定性会明显下降尤其在batch_size不够大的情况下判别器和生成器的loss波动会变得异常剧烈。动漫头像生成这种细粒度任务稳定的训练过程比任何花哨的技巧都重要。3.2 判别器结构设计判别器的任务是判断输入图片是真实图片还是生成图片。结构上正好与生成器镜像但方向相反。输入一张64x64x3的图片经过四次普通卷积操作每次把特征图尺寸减半、通道数翻倍最后输出一个标量代表这张图是真的的概率。卷积层之间我使用LeakyReLU而不是ReLU这是DCGAN论文中一个容易被忽略但非常重要的细节。LeakyReLU在负半轴有一个很小的斜率通常设为0.2这意味着即使输入的特征值非常小梯度也能够通过反向传播传递回去。如果换成普通的ReLU负值全部被截断训练早期判别器性能较强时梯度很容易消失。判别器代码class Discriminator(nn.Module): def __init__(self): super(Discriminator, self).__init__() self.conv_layers nn.Sequential( # 64x64 - 32x32 nn.Conv2d(3, 64, kernel_size4, stride2, padding1), nn.LeakyReLU(0.2, inplaceTrue), # 32x32 - 16x16 nn.Conv2d(64, 128, kernel_size4, stride2, padding1), nn.BatchNorm2d(128), nn.LeakyReLU(0.2, inplaceTrue), # 16x16 - 8x8 nn.Conv2d(128, 256, kernel_size4, stride2, padding1), nn.BatchNorm2d(256), nn.LeakyReLU(0.2, inplaceTrue), # 8x8 - 4x4 nn.Conv2d(256, 512, kernel_size4, stride2, padding1), nn.BatchNorm2d(512), nn.LeakyReLU(0.2, inplaceTrue) ) self.classifier nn.Sequential( nn.Conv2d(512, 1, kernel_size4, stride1, padding0), nn.Sigmoid() ) def forward(self, x): x self.conv_layers(x) x self.classifier(x) return x.squeeze()这里我用了Conv2d加Sigmoid的方式替代全连接层来输出单一数值。另一条常见路线是在最后一层用Linear(51244, 1)两种方案效果接近。保留卷积结构的好处是参数更少对输入的局部结构敏感度更高。不过需要说明使用Sigmoid是为了配合BCELoss如果你喜欢用BCEWithLogitsLoss那么最后一层卷积输出之后不要加Sigmoid计算损失时由loss函数内部完成Sigmoid转换数值上更稳定这是PyTorch官方推荐的做法。3.3 权重初始化与优化器选择网络结构定了之后有一个经常被新手忽略的操作——权重初始化。DCGAN论文专门做了一个实验证明恰当的权重初始化对训练至关重要所有卷积层和全连接层的权重应该从均值为0、标准差为0.02的正态分布中随机初始化偏置项统一置为0。之所以强调0.02这个值是因为它能在不过度激活网络的前提下保证信息可以在深层网络中顺利传播。初始化代码def weights_init(m): classname m.__class__.__name__ if classname.find(Conv) ! -1: nn.init.normal_(m.weight.data, 0.0, 0.02) if m.bias is not None: nn.init.constant_(m.bias.data, 0) elif classname.find(BatchNorm) ! -1: nn.init.normal_(m.weight.data, 1.0, 0.02) nn.init.constant_(m.bias.data, 0) netG Generator().to(device) netD Discriminator().to(device) netG.apply(weights_init) netD.apply(weights_init)优化器的选择同样没有悬念。DCGAN论文的标准配置是Adam学习率设为0.0002但关键的改动是把betas参数中第一项设置为0.5。Adam默认的beta1是0.9这个值会让动量累积过大导致训练震荡。很多人在复现DCGAN时发现loss剧烈波动、图像质量差很可能就是忘了改这个参数。lr 0.0002 beta 0.5 optimizerG torch.optim.Adam(netG.parameters(), lrlr, betas(beta, 0.999)) optimizerD torch.optim.Adam(netD.parameters(), lrlr, betas(beta, 0.999))损失函数用标准的二分类交叉熵BCELoss。训练中有一个自然的认知冲击判别器要区分真假生成器要把假得变真两者目标恰好相反但又互相促进。这就是GAN的对抗本质。4. 训练实操与调参经验4.1 训练循环核心代码理论准备完毕下面进入真正硬核的训练循环。GAN训练的代码逻辑不复杂但顺序和细节非常讲究。每轮训练分为两步先更新判别器再更新生成器。判别器更新时需要准备两批输入一批是真实图片标签记为1另一批是生成器伪造的图片标签记为0。这里有一个容易混淆的地方——生成图片时要固定当前噪声不能每次重新采样否则生成器一次迭代被多次反向传播更新梯度方向会不稳定。生成器更新时我们把生成器的输出再送入判别器但这次目标标签置为1意思是希望判别器把生成的图片错认为真实图片。计算梯度后只更新生成器参数整个过程中判别器处于冻结状态。我自己封装了一个简洁的训练循环criterion nn.BCELoss() fixed_noise torch.randn(64, latent_dim, 1, 1, devicedevice) for epoch in range(num_epochs): for i, (real_images, _) in enumerate(dataloader): batch_size real_images.size(0) real_images real_images.to(device) label_real torch.full((batch_size,), 1, devicedevice) label_fake torch.full((batch_size,), 0, devicedevice) # 训练判别器 netD.zero_grad() output netD(real_images.view(-1, 3, 64, 64)).view(-1) lossD_real criterion(output, label_real) noise torch.randn(batch_size, latent_dim, 1, 1, devicedevice) fake_images netG(noise) output netD(fake_images.detach()).view(-1) lossD_fake criterion(output, label_fake) lossD lossD_real lossD_fake lossD.backward() optimizerD.step() # 训练生成器 netG.zero_grad() noise torch.randn(batch_size, latent_dim, 1, 1, devicedevice) fake_images netG(noise) output netD(fake_images).view(-1) lossG criterion(output, label_real) lossG.backward() optimizerG.step()我在代码里对真实图片先调用.view(-1, 3, 64, 64)来确保维度正确因为ImageFolder返回的数据在batch维度上会有差异。如果张量形状不对后续的卷积层会直接报错新手经常会卡在这个细节上。4.2 训练中的标签平滑技巧训练一段时间后你会发现一个棘手的问题判别器变得越来越强大真实图片的判别loss很快就接近0而生成器完全跟不上了。用GAN圈子里的说法这是“判别器把生成器碾压了”。一旦出现这种情况梯度就无法有效传递给生成器生成结果会长期停留在噪声阶段。我在这里提供一个经验性的解决方案——标签平滑Label Smoothing。把真实图片的标签从1改成0.9而不是1.0。这样判别器的输出目标不是严格等于1只需要接近0.9就算正确降低了判别器的过信程度给生成器留出喘息空间。实现方式只需把label_real定义一行代码改为label_real torch.full((batch_size,), 0.9, devicedevice)这个小改动在动漫头像任务上效果非常明显。我最初几轮训练用了严格标签到80个epoch时判别器loss已经趋近于0生成器输出一张全灰色图片换成0.9平滑后训练立即恢复到正常状态生成图片开始出现眼睛和发丝轮廓。4.3 训练周期与训练过程的观察方法关于训练轮数我在这个数据集上尝试过不同的epoch配置最终的推荐是200到300轮。少于100轮生成头像五官轮廓基本是全糊的只能勉强分辨出人脸区域超过300轮边际收益非常小还容易进入过拟合状态。训练过程中的直观判断方法是每轮训练后固定一组64个随机噪声输入生成器保存生成的图片并拼成一张8x8的网格。每个epoch结束记录一个grid拼成gif或直接翻看对比。我在训练中看到的变化大致是这样前20轮几乎都是彩色噪声没有任何人形结构loss还在剧烈波动30到60轮开始出现模糊的人脸轮廓肤色区域隐约可见但仍然混沌80到120轮五官位置逐渐稳定眼睛、嘴巴形状开始出现但细节仍然粗糙150轮以后发色、眼睛颜色、面部朝向这些高级特征逐步成形部分图片已经非常接近真实二次元头像。我的监控代码是这样写的with torch.no_grad(): fake netG(fixed_noise).detach().cpu() img_grid torchvision.utils.make_grid(fake, padding2, normalizeTrue) plt.imshow(img_grid.permute(1, 2, 0)) plt.savefig(f./outputs/epoch_{epoch:03d}.png) plt.close()make_grid函数的normalizeTrue参数很关键因为生成器输出范围是[-1,1]直接显示会整体偏黑normalize会把数值映射回[0,1]区间再显示。5. 常见问题与排查实录5.1 生成图片模糊没有识别度怎么办训练完查看生成结果最常见的抱怨就是“脸是出来了但全是糊的”。出现这个现象有几个原因按优先级从高到低排查第一检查是否真的训练充分。我见过很多人在第30个epoch就停下来说图片模糊其实只是训练不够。动漫头像比MNIST手写数字复杂得多至少训练到150轮再下结论。第二检查batch_size是否过小。batch_size为16或32时BatchNorm计算出的均值和方差波动太大生成图片质量不稳。建议不低于64。第三检查判别器是否太弱了。如果判别器过于简陋比如用两层全连接它无法从图片中有效提取特征生成器也就无法知道应该优化哪些细节。可以适当增加判别器卷积层的通道数来提升判别能力。5.2 模式崩塌与loss异常模式崩塌是GAN训练的经典问题现象是生成的图片虽多但几乎千篇一律全是同一个脸型的头像。模型把所有随机噪声都映射到了同一个输出区域。这种问题我遇到的少一些但一旦出现光调学习率几乎无效。几个有效的应对方案分别是增大潜变量维度从100提升到128、把生成器的dropout概率适当增大在多层之间随机丢弃部分神经元以破坏记忆、以及使用更强烈的标签平滑从0.9进一步降到0.8。loss异常方面最典型的是loss变成NaN。这种情况90%是学习率过大导致梯度爆炸。解决方法是把学习率从0.0002降到0.0001同时检查是否存在除以零的情况比如view操作后输入尺寸不对。如果用的是BCEWithLogitsLoss而最后一层没有去掉Sigmoid也会导致数值不稳定需要检查两者是否匹配。5.3 我的排障顺序总结训练出现问题的时候别急着乱调参数。经过这些天的折腾我总结出一个相对稳定的排障顺序第一步先看loss数量级和变化趋势。判别器loss维持在0.6到1.0之间波动生成器loss逐步下降这是健康状态。如果判别器loss瞬间降到0马上停止训练检查数据归一化。第二步看中间输出图片。固定噪声的生成结果如果始终是彩色噪声说明生成器没有有效学到特征如果全是灰色模糊块说明判别器过强。两种情况对应的调整方向相反。第三步看训练设备。GPU显存利用率是否正常会不会因为num_workers设置过大导致数据加载瓶颈。我在刚开始训练时把num_workers设成了8CPU经常跑满而GPU利用率很低后来调回2才算正常。至少对这些判断顺序有概念之后训练中遇到问题就不会手忙脚乱了。先把表象症状弄清楚再决定是调整数据、结构还是超参每一步都有依据。6. 一些实际操作中的体会最后再聊一些超出教程本身的经验。我最初跑DCGAN动漫头像生成犯过一个特别愚蠢的错误——忘记把生成器和判别器分别调用train()模式。在PyTorch中BatchNorm和Dropout在训练和推理模式下行为不同没有调用train()会导致生成器训练时完全不使用BatchNorm的更新机制结果就是训练了100轮生成结果始终是没有细节的色块。这个问题排查了很久最后还是靠逐步打印每一层输出来定位的。另一个容易忽视的点是模型的文件保存一定要冻结网络结构后保存state_dict而不是整个模型。我之前图省事直接torch.save(model)换了一台机器加载时报了一堆版本兼容性错误。官方推荐的写法是torch.save(netG.state_dict(), ./saved_model/generator.pth)加载的时候新建一个生成器结构再用load_state_dict恢复参数。这个方法对后续跨机器部署非常友好。如果你训练完之后想进一步拉高生成图片的质量下一步可以考虑在DCGAN基础上加一个VAE式的latent空间约束或者把它升级为WGAN-GP。这两个方向我都做过一些尝试改造成本都不高但对训练稳定性和生成质量的提升非常明显尤其WGAN-GP几乎可以彻底避免模式崩塌的问题。这些后续有机会再单独写文章分享。
返回列表