简介:这份资源是面向深度学习初学者与生成模型爱好者的 ACGAN PyTorch 实现代码包,帮助读者理解如何在生成对抗网络中引入类别条件信息,从而生成指定类别的样本。包内共 4 个文件,以 3 个 py 脚本和 1 个 md 说明文档为主,py 文件分别承担模型定义、训练主流程与判别逻辑等核心功能,md 文档则用于补充项目说明,压缩包整体约 3KB,结构精简、便于快速阅读与二次修改。目前已有 823 人学习下载,适合作为入门 ACGAN 的轻量级参考。通过这份代码,读者可以掌握生成器、判别器与辅助分类器的网络搭建方式,理解对抗损失与类别指导损失的组合思路,并借鉴交替优化生成器与判别器的训练策略,为图像生成、数据增强等场景打下实践基础。
1. ACGAN 的 Pytorch 实现:从条件生成到类别控制的落地路径
如果你手头有一份 ACGAN 的 Pytorch 实现代码包,却不确定它到底比普通 GAN 多解决了什么问题,那这篇笔记就是写给你的。ACGAN(Auxiliary Classifier GAN)的核心思路并不复杂:在判别器上额外挂一个分类头,让判别器不仅要判断真假,还要判断样本属于哪个类别。这样一来,生成器就被迫学会“按标签生成”——你给它一个类别标签,它就能生成对应类别的图像,而不是随机碰运气。这份资源适合已经跑通过基础 GAN、想进一步做条件生成和类别控制的从业者,也适合需要快速复现论文效果、拿来做图像增广或数据合成的工程场景。代码基于 Pytorch,结构清晰,能直接跑通,但有几个参数和训练细节如果不注意,很容易翻车。
2. 判别器双头结构:为什么多一个分类头就能控制类别
2.1 ACGAN 与普通 GAN 的结构差异
普通 GAN 的判别器只输出一个标量,表示输入图像为真的概率。ACGAN 的判别器则输出两个东西:一个是真假判别 logits,另一个是类别分类 logits。这个设计看起来只是多了一个分支,但它改变了整个博弈的性质。生成器的输入除了噪声向量 z,还拼接了一个类别标签 c(通常是 one-hot 或 embedding)。生成器要同时骗过判别器的真假判断和类别判断,所以它必须学会把类别信息编码进生成图像中。
从梯度传播的角度看,判别器的分类损失会反向传播到生成器,迫使生成器生成的图像在特征空间上靠近对应类别的真实样本。这比单纯用真假损失来训练要稳定得多,因为类别标签提供了额外的监督信号。常见做法是让判别器共享卷积主干,然后在最后分成两个全连接头,一个输出 1 维真假 logits,一个输出 N 维类别 logits。
2.2 判别器与生成器的代码实现
下面这份代码是 ACGAN 判别器的典型写法,主干用卷积层提取特征,然后分两个头输出。注意这里用了nn.LeakyReLU和nn.Dropout来防止判别器过强。
import torch import torch.nn as nn class Discriminator(nn.Module): def __init__(self, num_classes=10, img_channels=1, feature_dim=128): super(Discriminator, self).__init__() # 共享卷积主干 self.conv = nn.Sequential( nn.Conv2d(img_channels, 64, 4, 2, 1), # 输入 28x28 -> 14x14 nn.LeakyReLU(0.2, inplace=True), nn.Conv2d(64, 128, 4, 2, 1), # 14x14 -> 7x7 nn.BatchNorm2d(128), nn.LeakyReLU(0.2, inplace=True), nn.Conv2d(128, 256, 4, 2, 1), # 7x7 -> 3x3 nn.BatchNorm2d(256), nn.LeakyReLU(0.2, inplace=True), ) self.fc = nn.Linear(256 * 3 * 3, feature_dim) # 真假判别头 self.adv_head = nn.Linear(feature_dim, 1) # 类别分类头 self.cls_head = nn.Linear(feature_dim, num_classes) def forward(self, x): feat = self.conv(x) feat = feat.view(feat.size(0), -1) feat = self.fc(feat) adv_logits = self.adv_head(feat) # 真假 logits cls_logits = self.cls_head(feat) # 类别 logits return adv_logits, cls_logits这段代码里,num_classes根据数据集类别数设置,MNIST 是 10,CIFAR-10 也是 10。feature_dim是共享特征维度,太小会导致分类能力不足,太大会让判别器过强。我一般从 128 起步,如果训练不稳定再降到 64。adv_head输出 1 维,配合BCEWithLogitsLoss使用;cls_head输出num_classes维,配合CrossEntropyLoss使用。注意卷积层的通道数和 kernel size 要根据输入图像尺寸调整,上面是按 28x28 写的,如果换成 32x32 或 64x64,需要重新计算全连接层的输入维度。
生成器的结构相对简单,输入是噪声向量 z 和类别 embedding 的拼接,然后通过全连接层和转置卷积逐步上采样。
class Generator(nn.Module): def __init__(self, num_classes=10, latent_dim=100, img_channels=1): super(Generator, self).__init__() self.label_emb = nn.Embedding(num_classes, latent_dim) self.fc = nn.Sequential( nn.Linear(latent_dim * 2, 256 * 3 * 3), nn.BatchNorm1d(256 * 3 * 3), nn.ReLU(inplace=True), ) self.deconv = nn.Sequential( nn.ConvTranspose2d(256, 128, 4, 2, 1), # 3x3 -> 7x7 nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.ConvTranspose2d(128, 64, 4, 2, 1), # 7x7 -> 14x14 nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.ConvTranspose2d(64, img_channels, 4, 2, 1), # 14x14 -> 28x28 nn.Tanh(), ) def forward(self, z, labels): label_emb = self.label_emb(labels) # (batch, latent_dim) x = torch.cat([z, label_emb], dim=1) # 拼接噪声和标签 x = self.fc(x) x = x.view(x.size(0), 256, 3, 3) return self.deconv(x)这里latent_dim是噪声维度,通常取 100 或 128。label_emb把离散标签映射成连续向量,和噪声拼接后一起送入生成器。注意Tanh把输出限制在 [-1, 1],所以真实图像也要归一化到同一范围,否则判别器会学到“只要看数值范围就能分辨真假”的捷径,导致生成器学不到东西。
2.3 损失函数与训练循环
ACGAN 的损失由两部分组成:判别器的真假损失和类别损失,生成器同样要优化这两部分。下面是一个训练步骤的核心代码。
criterion_adv = nn.BCEWithLogitsLoss() criterion_cls = nn.CrossEntropyLoss() for epoch in range(num_epochs): for imgs, labels in dataloader: batch_size = imgs.size(0) real_imgs = imgs.to(device) real_labels = labels.to(device) # 训练判别器 optimizer_D.zero_grad() z = torch.randn(batch_size, latent_dim).to(device) fake_labels = torch.randint(0, num_classes, (batch_size,)).to(device) fake_imgs = G(z, fake_labels).detach() real_adv, real_cls = D(real_imgs) fake_adv, fake_cls = D(fake_imgs) # 真假损失:真实样本标签为 1,生成样本标签为 0 loss_real_adv = criterion_adv(real_adv, torch.ones_like(real_adv)) loss_fake_adv = criterion_adv(fake_adv, torch.zeros_like(fake_adv)) # 类别损失:真实样本用真实标签,生成样本用生成时指定的标签 loss_real_cls = criterion_cls(real_cls, real_labels) loss_fake_cls = criterion_cls(fake_cls, fake_labels) loss_D = loss_real_adv + loss_fake_adv + loss_real_cls + loss_fake_cls loss_D.backward() optimizer_D.step() # 训练生成器 optimizer_G.zero_grad() z = torch.randn(batch_size, latent_dim).to(device) gen_labels = torch.randint(0, num_classes, (batch_size,)).to(device) gen_imgs = G(z, gen_labels) gen_adv, gen_cls = D(gen_imgs) # 生成器希望判别器把生成样本判为真,且分类正确 loss_G_adv = criterion_adv(gen_adv, torch.ones_like(gen_adv)) loss_G_cls = criterion_cls(gen_cls, gen_labels) loss_G = loss_G_adv + loss_G_cls loss_G.backward() optimizer_G.step()这段训练循环里,判别器更新时用了detach()切断生成器梯度,这是标准做法。生成器损失里loss_G_adv让判别器把生成样本判为真,loss_G_cls让判别器把生成样本分类到指定标签。两个损失的权重通常是 1:1,但如果分类准确率上不去,可以适当调大loss_G_cls的系数。我一般会监控判别器在真实样本上的分类准确率,如果低于 80%,说明判别器分类头没学好,生成器的类别控制也会受影响。
3. 数据准备与标签对齐:别让 one-hot 和 embedding 打架
3.1 数据集加载与归一化
ACGAN 对数据归一化很敏感。因为生成器最后用了Tanh,输出范围是 [-1, 1],所以真实图像必须归一化到同一范围。常见做法是用transforms.Normalize(mean=(0.5,), std=(0.5,)),把 [0,1] 映射到 [-1,1]。如果漏了这一步,判别器会很快学会通过数值范围区分真假,生成器梯度消失,训练直接崩掉。
from torchvision import datasets, transforms transform = transforms.Compose([ transforms.Resize(28), transforms.ToTensor(), transforms.Normalize(mean=(0.5,), std=(0.5,)) # 归一化到 [-1, 1] ]) train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform) train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=64, shuffle=True, num_workers=2)这里batch_size我一般设 64 或 128。太小会导致 BatchNorm 统计量不稳定,太大则显存吃紧。num_workers根据机器 CPU 核数调整,Windows 下如果报错就设 0。
3.2 标签格式:embedding 与 one-hot 的选择
生成器里用的是nn.Embedding,输入是整数标签,输出是连续向量。判别器的分类头输出的是 logits,配合CrossEntropyLoss使用,标签也是整数。所以整个流程里标签始终保持整数格式,不需要手动转 one-hot。这一点和有些实现不一样,有些代码会在生成器里用 one-hot 拼接,那样也可以,但 embedding 更灵活,参数量也更少。
注意fake_labels和gen_labels的生成方式:用torch.randint(0, num_classes, (batch_size,))随机采样。这样每个 batch 里各类别分布大致均匀,不会出现某个类别主导训练的情况。如果数据集本身类别不平衡,可以考虑按类别权重采样,但 ACGAN 原论文里没有特别处理,我一般先跑通再调。
3.3 训练轮数与监控指标
ACGAN 在 MNIST 上通常 50 到 100 个 epoch 就能看到比较清晰的类别控制效果。监控指标除了生成器损失和判别器损失,更重要的是看判别器在真实样本上的分类准确率,以及生成样本的类别一致性。我一般每 10 个 epoch 生成一批固定噪声和固定标签的图像,肉眼观察类别是否对应。如果发现生成的数字和标签对不上,先检查标签拼接顺序有没有搞反,再检查分类损失有没有正确反向传播。
4. 训练不收敛与模式崩溃:ACGAN 常见问题排查
4.1 判别器分类准确率始终上不去
现象:训练几十个 epoch 后,判别器在真实样本上的分类准确率还在 50% 左右,生成图像模糊且类别混乱。
原因:最常见的是学习率设置不当。判别器分类头是新增的,如果学习率和卷积主干一样,可能更新太快或太慢。另一个原因是feature_dim太小,共享特征不足以同时支撑真假判别和分类。
解决:把判别器学习率降到 1e-4 或 2e-4,生成器学习率保持 2e-4。如果还不行,把feature_dim从 128 提到 256,或者给分类头单独设一个更大的学习率。我遇到过一种情况是 BatchNorm 的 momentum 默认 0.1 导致统计量波动太大,改成 0.8 后分类准确率明显提升。
4.2 生成器只生成某一类图像
现象:不管输入什么标签,生成器输出的都是同一类数字,比如全是“1”。
原因:这是典型的模式崩溃在条件生成里的表现。判别器分类头太强,生成器发现只要生成某一类就能骗过分类损失,于是放弃其他类别。也可能是loss_G_cls权重太低,生成器不关心类别是否正确。
解决:把loss_G_cls的权重从 1 调到 2 或 3,同时给判别器加 Dropout(比如 0.3)削弱它的分类能力。另外检查一下fake_labels是不是每个 batch 都重新随机采样了,如果固定不变,生成器会只学那几个标签。
4.3 损失震荡剧烈,生成图像质量忽好忽坏
现象:损失曲线上下跳动,生成的图像有时清晰有时全是噪声。
原因:判别器和生成器更新频率不平衡,或者优化器动量参数不合适。ACGAN 比普通 GAN 多了一个分类损失,梯度尺度可能差一个量级。
解决:把优化器从 Adam 换成 RMSprop,或者把 Adam 的betas从 (0.5, 0.999) 改成 (0.5, 0.9)。另一个办法是每训练判别器 2 次才训练生成器 1 次,让判别器先稳定下来。我一般会先跑 5 个 epoch 只训练判别器,等分类准确率上到 70% 再开始交替训练。
4.4 显存溢出或训练速度极慢
现象:batch_size 设到 64 就 OOM,或者每个 epoch 要跑十几分钟。
原因:生成器的全连接层参数量太大,或者数据加载num_workers设得太小。
解决:把latent_dim从 100 降到 64,feature_dim从 128 降到 64,能省不少显存。另外把num_workers设成 CPU 核数的一半,pin_memory=True也能加速数据搬运。如果还是慢,检查一下是不是在 CPU 上跑,device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')这行别忘了。
4.5 生成的图像有网格状伪影
现象:生成图像上出现规律性的方格或条纹。
原因:转置卷积的 kernel size 和 stride 不匹配,导致重叠区域计算不均匀。常见于kernel_size=4, stride=2, padding=1的组合,理论上输出尺寸是输入的两倍,但边界处理容易出问题。
解决:把转置卷积换成nn.Upsample加普通卷积,或者调整 kernel size 为 3、stride 为 2、padding 为 1、output_padding 为 1。我一般会在生成器最后加一个nn.Conv2d做平滑,能明显减少网格伪影。
5. 进阶技巧:用类别插值和条件验证确认模型真的学到了
训练完之后,怎么确认模型不是“假装”学会了类别控制?我常用的方法是做类别插值和条件验证。类别插值是在 embedding 空间里对两个不同类别的标签向量做线性插值,观察生成图像是否平滑过渡。如果模型真的学到了类别语义,插值中间应该出现两类混合的形态,而不是突变。
# 假设 label_emb 是生成器里的 Embedding 层 label_a = torch.tensor([0]).to(device) # 类别 0 label_b = torch.tensor([1]).to(device) # 类别 1 emb_a = G.label_emb(label_a) emb_b = G.label_emb(label_b) alphas = torch.linspace(0, 1, 10).to(device) for alpha in alphas: emb_interp = emb_a * (1 - alpha) + emb_b * alpha z = torch.randn(1, latent_dim).to(device) x = torch.cat([z, emb_interp], dim=1) x = G.fc(x).view(1, 256, 3, 3) img = G.deconv(x) # 保存或展示 img这段代码里,alphas从 0 到 1 变化,生成图像应该从类别 0 逐渐过渡到类别 1。如果中间出现完全无关的类别,说明 embedding 空间没有连续性,模型可能只是记住了几个离散点。另一个验证方法是固定噪声 z,只改变标签,看生成图像是否按标签变化。如果换标签后图像几乎不变,说明生成器忽略了标签输入,需要回头检查拼接顺序和分类损失。
还有一个实用技巧是给判别器分类头加标签平滑(label smoothing),把真实标签从 1 改成 0.9,能防止判别器过度自信,生成样本的多样性会更好。我一般在训练后期(最后 20 个 epoch)才加标签平滑,前期不加,让判别器先学清楚类别边界。
从那以后我每次跑 ACGAN 都会先做一轮 5 个 epoch 的判别器预热,再检查标签拼接顺序,最后才开完整训练。这个习惯帮我省了很多次重新跑一天的时间。希望帮到你。
本文还有配套的精品资源,点击获取