简介:一份基于WGAN-GP算法实现的动漫头像生成系统源码,面向深度学习初学者与生成对抗网络爱好者,目标是用更稳定的训练方式解决传统模型模式崩塌问题,最终输出二百五十六乘二百五十六像素的清晰动漫人脸。压缩包共二十六个文件,体积仅一点三二兆字节,其中两个Python源文件负责核心算法与训练流程,十一张PNG图片用于展示生成效果,六个XML文件承担配置管理,另有占位与说明文档辅助快速上手。当前已有三百零六人学习浏览,展现出不错的参考热度。借助完整源码和说明文档,读者既能直接运行观察生成头像样本,也能通过修改配置调整超参数,深入理解WGAN-GP的梯度惩罚机制,为自定义动漫形象生成、实验复现乃至相关论文写作提供扎实的代码基础。
1. 一个 256×256 动漫头像生成器:从 WGAN-GP 到能跑通的落地代码
先抛个结论:训练一个画「二次元老婆脸」的 GAN,难点从来不在网络结构,而在训练稳定性——传统 GAN 训练几十轮就模式崩塌,生成的图千篇一律。而这个项目用 WGAN-GP(带梯度惩罚的 Wasserstein GAN)把这个问题解决了大半,在你自己的 GPU 上就能跑通 256×256 动漫头像生成,11 张示例输出图直接摆在 result 目录里,效果一眼可见。项目源码包含两个 Python 文件,一个管训练主流程,一个管生成验证,配套图片输出和 readme 说明,适合两类人:想复现 WGAN-GP 训练流程的深度学习者,以及想拿现成 GAN 工程做风格化头像生成二次开发的工程党。这篇笔记从原理、网络结构、代码实现到调参踩坑,完整过一遍。
2. WGAN-GP 的工程化设计:为什么选它,网络怎么搭
2.1 传统 GAN 的训练痛点与 WGAN-GP 的解法
传统 GAN 用 JS 散度衡量真实分布和生成分布的差异,训练初期生成分布和真实分布重叠区域极小,JS 散度接近常数,梯度消失,生成器学不到东西。WGAN 换成 Wasserstein 距离,即使两个分布完全不重叠,距离依然连续可导,梯度不会断。但原始 WGAN 为了满足 Lipschitz 约束做权重裁剪,容易导致梯度爆炸或消失。WGAN-GP 的改进是加梯度惩罚项,强制判别器(Critic)的梯度范数接近 1,既满足约束又保留了表达能力。
这个项目踩的正是 WGAN-GP 路线:主训练循环里,判别器每更新五次,生成器才更新一次,这叫 n_critic 策略,因为 Wasserstein 损失要求判别器先训练得足够充分,生成器才能拿到有意义的梯度。损失函数那一段,从main.py的GP_LAMBDA参数能直接看到梯度惩罚系数,默认取 10,这是原论文验证过的稳定区间。
2.2 生成器结构:转置卷积从 8×8 一路放大到 256×256
生成器输入是一个 128 维的噪声向量(标准正态分布采样),经过全连接层投影到 8×8×1024 的特征图,然后连续四层转置卷积逐步上采样:8→16→32→64→128→256。每层转置卷积配合 BatchNorm 和 ReLU 激活,最后一层用 Tanh 把输出像素值压到 [-1, 1],和判别器的输入范围对齐。这个结构直接对应main.py里的build_generator函数。
关键参数说明如下:channels默认 3(RGB),z_dim默认 128,如果显存吃紧可以把第一层通道数从 1024 降到 512,代价是生成图像的细节纹理变弱。上采样没有用插值后接卷积的方式,而是纯转置卷积,因为转置卷积在 WGAN-GP 框架下能学到更好的分布映射。
2.3 判别器结构:PatchGAN 思路与梯度惩罚实现
判别器是标准的卷积下采样结构,四层卷积把 256×256 输入压到 8×8 特征图,每层卷积后接 LayerNorm(不是 BatchNorm,因为梯度惩罚要求对每个样本独立计算梯度,BatchNorm 的批统计会破坏这一点)和 LeakyReLU。最后一层卷积输出的是 Patch 级别的真伪分数,而不是单个标量——每个 8×8 位置的值代表原图对应区域的真假概率,这样生成器要同时骗过所有局部区域,出图的纹理更匀称。
梯度惩罚的计算集中在gradient_penalty函数里:对真实图和生成图做线性插值得到中间样本,输入判别器算梯度,取 L2 范数后和 1 做差再平方,乘上 10 加进判别器损失。这里有个工程点要提醒:插值必须发生在 CPU 上还是 GPU 上无所谓,但torch.autograd.grad的create_graph=True必须开,否则梯度惩罚本身无法被反向传播。
2.4 项目文件结构:两段式代码各司其职
整个工程源码目录非常精简,核心就两个 Python 文件:main.py管训练全流程,check.py管训练完成后的单张生成验证。data目录放训练集图像(原始数据集不包含在项目包里,需要自己准备动漫头像图片),save目录在训练过程中周期性产出生成结果 PNG,根目录下那一堆 save*.png 就是训练不同阶段的输出快照,能直观看到从噪声到清晰头像的演化过程。.idea目录是 IDE 工程配置,对运行不构成依赖,.gitignore屏蔽了 dataset 和模型权重等大文件。
3. 核心代码逐段拆解:训练主循环到生成验证
3.1 数据加载与预处理:中心裁剪加 Resize,归一化到 [-1,1]
# main.py 数据加载部分 dataset = ImageFolder(data_dir, transform=transforms.Compose([ transforms.CenterCrop(256), # 中心裁剪到 256x256 transforms.Resize((256, 256)), # 确保尺寸一致 transforms.ToTensor(), # HWC -> CHW,像素归一化到 [0,1] transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) # 映射到 [-1,1] ])) dataloader = DataLoader(dataset, batch_size=32, shuffle=True, num_workers=4)预处理三步是关键:CenterCrop先把非正方形的图裁成正方形,避免 Resize 时拉伸变形;ToTensor把通道维挪到第一维;Normalize的均值和方差都填 0.5,把像素值从 [0,1] 线性映射到 [-1,1],和生成器输出层的 Tanh 激活范围对齐。如果你的数据集图片大小不统一,记得在中心裁剪前先做 Resize,否则小图会被裁掉过多有效信息。
3.2 生成器实现:从噪声到图像的转置卷积堆叠
# main.py 生成器定义 class Generator(nn.Module): def __init__(self, z_dim=128, channels=3): super().__init__() self.fc = nn.Linear(z_dim, 8 * 8 * 1024) # 全连接展开 self.deconv = nn.Sequential( nn.ConvTranspose2d(1024, 512, 4, 2, 1), # 8x8 -> 16x16 nn.BatchNorm2d(512), nn.ReLU(True), nn.ConvTranspose2d(512, 256, 4, 2, 1), # 16x16 -> 32x32 nn.BatchNorm2d(256), nn.ReLU(True), nn.ConvTranspose2d(256, 128, 4, 2, 1), # 32x32 -> 64x64 nn.BatchNorm2d(128), nn.ReLU(True), nn.ConvTranspose2d(128, 64, 4, 2, 1), # 64x64 -> 128x128 nn.BatchNorm2d(64), nn.ReLU(True), nn.ConvTranspose2d(64, channels, 4, 2, 1),# 128x128 -> 256x256 nn.Tanh() ) def forward(self, z): x = self.fc(z).reshape(-1, 1024, 8, 8) return self.deconv(x)全连接层把 128 维噪声映射成 8×8×1024 的特征图,这个 8×8 是特征图的最小分辨率,后续每层转置卷积用 stride=2 把尺寸翻倍,kernel_size=4、padding=1 配合 stride=2 是转置卷积的标准配置,保证输出尺寸精确翻倍。中间层用 ReLU 而非 LeakyReLU,因为生成器不需要抑制负梯度;BatchNorm 在这里可以用,因为生成器的输入是批量噪声,不存在判别器那种逐样本梯度计算的需求。
3.3 判别器实现:下采样加 Patch 输出与梯度惩罚协议
# main.py 判别器定义 class Discriminator(nn.Module): def __init__(self, channels=3): super().__init__() self.conv = nn.Sequential( nn.Conv2d(channels, 128, 4, 2, 1), # 256 -> 128 nn.LeakyReLU(0.2, inplace=True), nn.Conv2d(128, 256, 4, 2, 1), # 128 -> 64 nn.LayerNorm([256, 64, 64]), nn.LeakyReLU(0.2, inplace=True), nn.Conv2d(256, 512, 4, 2, 1), # 64 -> 32 nn.LayerNorm([512, 32, 32]), nn.LeakyReLU(0.2, inplace=True), nn.Conv2d(512, 1, 4, 2, 1), # 32 -> 16 ) def forward(self, x): return self.conv(x)判别器没有在最后一层接 Sigmoid,这是 WGAN 家族和传统 GAN 的标志性区别:WGAN 输出的分数表示「真实程度」的连续度量而非概率,配合 Wasserstein 损失直接拿 logit 算期望差。LayerNorm传入特征图尺寸是因为LayerNorm在卷积场景下需要显式声明归一化维度,这和 Transformer 里的用法不同,容易写错。
梯度惩罚的完整逻辑在训练循环里体现,核心是用autograd.grad计算插值样本对判别器输入的梯度,代码实现如下:
# 梯度惩罚计算 def gradient_penalty(disc, real, fake): alpha = torch.rand(real.size(0), 1, 1, 1).to(real.device) interpolated = alpha * real + (1 - alpha) * fake interpolated.requires_grad_(True) out = disc(interpolated) grads = torch.autograd.grad(outputs=out, inputs=interpolated, grad_outputs=torch.ones_like(out), create_graph=True)[0] pen = ((grads.norm(2, dim=1) - 1) ** 2).mean() return penalpha从 [0,1] 均匀采样生成插值权重,对每张图的每个像素做加权融合。create_graph=True让梯度计算本身可被二次求导,这样惩罚项才能参与判别器的反向传播。这里一个常见失误是忘记requires_grad_(True),插值样本没有梯度历史,autograd.grad直接报错。
3.4 训练循环:n_critic 策略与双时间尺度更新
# main.py 训练主循环(关键部分) GP_LAMBDA = 10 N_CRITIC = 5 LR_G = 1e-4 LR_D = 4e-4 for epoch in range(epochs): for batch_idx, (real_img, _) in enumerate(dataloader): real_img = real_img.to(device) z = torch.randn(batch_size, z_dim).to(device) fake_img = generator(z) # 训练判别器 n_critic 次 d_real = discriminator(real_img) d_fake = discriminator(fake_img.detach()) gp = gradient_penalty(discriminator, real_img, fake_img) d_loss = -(d_real.mean() - d_fake.mean()) + GP_LAMBDA * gp d_optimizer.zero_grad() d_loss.backward() d_optimizer.step() # 每 N_CRITIC 次迭代训练一次生成器 if batch_idx % N_CRITIC == 0: z = torch.randn(batch_size, z_dim).to(device) fake_img = generator(z) g_loss = -discriminator(fake_img).mean() g_optimizer.zero_grad() g_loss.backward() g_optimizer.step()判别器损失由三部分构成:真实样本得分的均值取负、生成样本得分的均值、梯度惩罚项。优化方向是让真样本分数高、假样本分数低、梯度范数贴近 1。生成器损失是负的判别器分数均值,等价于让生成样本在判别器中获得尽可能高的真实性评分。注意fake_img.detach()这一步必须做,否则判别器的反向传播会顺带更新生成器的权重——这在 WGAN-GP 里属于严重 bug,训练会不稳。
学习率设置遵循双时间尺度更新规则(TTUR):判别器学习率是生成器的 4 倍,因为判别器需要更快收敛才能提供可靠的真伪度量。如果调大学习率,梯度惩罚项的系数也要相应增大,否则判别器容易过拟合真实样本导致生成器梯度不准。
3.5 check.py:加载权重做单体生成验证
# check.py 生成验证脚本 generator.load_state_dict(torch.load(G_MODEL_PATH, map_location=device)) generator.eval() with torch.no_grad(): z = torch.randn(1, z_dim).to(device) fake = generator(z)[0].cpu() fake = (fake + 1) / 2 # [-1,1] -> [0,1] save_image(fake, "result.png")load_state_dict加载训练中保存的生成器权重,eval()模式是关键,它会关闭 BatchNorm 和 Dropout 的随机行为,保证生成过程可复现。torch.no_grad()禁止自动梯度跟踪,推理阶段必须加,否则显存会被无谓的中间梯度占满。后处理把输出从 [-1,1] 映射回 [0,1],因为save_image期望输入是标准图像范围。
4. 环境搭建与训练实操:从零到出图的完整流程
4.1 依赖安装与硬件要求
项目依赖 PyTorch 1.8+、torchvision、PIL、numpy,显卡建议显存 8GB 以上。训练 256×256 分辨率、batch_size=32 的情况下,占用约 6GB 显存;如果显存紧张,把 batch_size 降到 16 并把判别器第一层通道数从 128 降到 64,显存占用能砍半。CPU 不是不能跑,但一个 epoch 在 5 万张图的数据集上要跑三四个小时,不建议。
安装命令在 readme.txt 里有写,核心就是 pip 安装 PyTorch 全家桶,注意先确认 CUDA 版本再选 PyTorch 版本,否则会出现 CUDA 不可用的玄学问题。
4.2 数据准备:目录结构和格式要求
程序用ImageFolder读数据,这就要求数据目录按类别分子文件夹,即使只有一个类也要建一层目录结构:
data/ images/ 0001.png 0002.jpg ...图片格式支持 jpg、png,训练代码会自动读取和预处理。数据集来源通常是自己爬取的动漫头像或开源数据集,去重和清洗很重要——重复图、带文字水印图、非头像图会拖累判别器收敛,我一般会先用感知哈希去重,再手工扫一遍明显异常的样本。数据量方面,5 万张以上效果比较好,少于 1 万张会出现生成头像同质化严重的问题。
4.3 训练启动与监控指标
python main.py --data_dir data --epochs 200 --batch_size 32 --output_dir savemain.py支持命令行参数覆盖默认配置,常用参数包括--z_dim(噪声维度,默认 128)、--gp_lambda(梯度惩罚系数,默认 10)、--n_critic(判别器更新次数,默认 5)、--sample_interval(每多少轮保存一次生成图,默认 500 次迭代)。训练过程会打印 d_loss 和 g_loss:健康状态是 d_loss 在 0 附近小幅波动,g_loss 缓慢下降;如果 d_loss 持续为负且绝对值快速增大,说明判别器过强,需要降低判别器学习率或增大梯度惩罚系数;如果 g_loss 不降反升,多半是生成器被判别器压制,可以调低n_critic到 3。
4.4 从噪声到成图的演化观察
每 500 次迭代生成的样例图,前 2000 次迭代基本是彩色噪声,轮廓都看不出来;5000 次迭代开始出现模糊的脸型轮廓,五官位置大致正确但细节糊成一团;2 万次迭代后眼睛、头发出现结构,但皮肤纹理还是塑胶感;6 万次迭代后整体观感像模像样,部分样例达到了可以直接当头像的质量。这个演化过程不是线性的,可能长时间停滞然后突然变清晰,WGAN-GP 比较常见的现象是在某个临界点后质量跃升——前提是训练的 d_loss 没有发散。
5. 避坑指南:训练 WGAN-GP 最容易踩的五个坑
5.1 现象:loss 剧烈震荡,生成图像全是彩色噪点
原因:学习率过高,判别器在真实样本和生成样本之间来回碾压,梯度幅度极大。解决:把生成器学习率降到 5e-5 以下,判别器学习率降到 2e-4 以下,同时把梯度惩罚系数从 10 调到 15——惩罚力度加大之后判别器的 Lipschitz 约束更强,震荡幅度直接衰减。
5.2 现象:训练 2 万轮之后,所有生成头像都是同一张脸
原因:模式崩塌。虽然 WGAN-GP 理论上降低了崩塌概率,但数据集多样性不足或生成器容量太小仍然会崩。解决:检查数据集里是不是某些特征(比如金发、眼镜)占比过高,先手动均衡数据分布;然后把生成器的 z_dim 从 128 提高到 256,给生成器更多「记忆不同模式」的编码空间,最后确认 n_critic=5 没有被改动过。
5.3 现象:日志里出现 NaN,训练中断
原因:梯度爆炸或者数据里有异常像素值。用torch.autograd.detect_anomaly(True)定位到具体层,多半是转置卷积层的权重在某个 batch 上发散。解决:在生成器和判别器网络结构里给卷积层加 SpectralNorm(谱归一化),再做梯度裁剪torch.nn.utils.clip_grad_norm_(params, max_norm=1.0),双保险之后 NaN 基本绝迹。
5.4 现象:生成的头像五官位置正确,但边缘有黑边或模糊
原因:转置卷积输出层没有额外的细化结构,256×256 的高频细节比 128×128 难学得多。解决:在生成器最后一层之前插入一层空洞卷积(dilation=2)的残差块,或者更换数据预处理让每张训练图过一遍轻微锐化,都能提高边缘清晰度。
5.5 现象:训练正常但 check.py 生成的图是暗色的
原因:保存权重时把G_MODEL_PATH写成了 D 的状态字典,或者加载时没有匹配到eval()导致 BatchNorm 用了批统计量。解决:先打印 state_dict 的键名确认前缀一致,torch.load(..., map_location='cpu')加载后用list(net.state_dict().keys())[0]对照一下;加载后务必调用generator.eval(),否则 BatchNorm 会按当前 batch 的均值方差算,颜色自然偏暗。
6. 二次开发指南:把 WGAN-GP 改成你想要的生成器
6.1 定向跳线改造:替换生成器层实现风格迁移
一个实用的改造场景:把手绘黑白线稿变成彩色头像。做法是把数据集的真图片全部转成线稿(OpenCV 的adaptiveThreshold提取边缘),保持生成器不变,把判别器的输入通道从 3 改成 4——多出来的通道塞线稿图,实图和线稿拼接喂进去。改动集中在build_discriminator的第一个卷积层,channels从 3 改成 4,数据加载逻辑同步拼接,其他不用动。这样训练出来的生成器,以线稿为输入条件输出彩色头像。
# 二次开发:条件输入的判别器改造 real_input = torch.cat([real_img, line_art], dim=1) # (B, 4, 256, 256) fake_img = generator(z) fake_input = torch.cat([fake_img.detach(), line_art], dim=1)6.2 分辨率迁移:从 256 升级到 512 的要点
把输出分辨率从 256 提到 512,并不是简单改一下转置卷积的层数。生成器要加一层转置卷积并且全连接层的投影尺寸翻倍(8×8×1024 改成 8×8×2048),显存占用按平方增长,batch_size 必须从 32 降到 8 左右。我的血泪经验是,短边先到 384 过渡,确认训练稳定再冲 512,直接跳 512 大多数情况会反复训练失败,白白浪费几十个小时。
另一个坑是学习率必须等比调低,分辨率变大意味着每个像素的梯度统计量更碎片化,保持原有学习率会让判别器更新步伐过大。
6.3 验证新模型的方法
拿东亚人脸数据集快速验证模型是否收敛,我用两句口诀:训练初期看 d_loss 的绝对值(跌到 -20 以下就有问题)、中期看生成图的纹理多样性、后期看相同噪声不同输出的差异性。不要指望看单张图判断效果,每 500 轮保存的样例里横向对比最后三组,如果三组图姿势发型太像,说明模型在崩塌边缘,趁早停机调参。实际操作里,凡是训练到一半想改数据集配置,我都会先清空 save 目录强制从零开始,避免在污染的状态上继续迭代。
从那以后,我每换一次数据集、每调一次分辨率,都会强制走一遍「数据清洗 → 短图搜索 → 小 batch 冒烟训练 → 全量训练」的完整流程,冒烟训练跑 200 轮看 loss 曲线是否规律性下降,才敢于继续往下投入算力。这习惯帮我避开了至少三次白烧显卡的大坑,希望也能帮你在跑这个 WGAN-GP 项目时省几张显卡的电费。
本文还有配套的精品资源,点击获取