ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

ACGAN PyTorch实战:从DCGAN到条件生成,实现可控图像生成与数据增强

ACGAN PyTorch实战:从DCGAN到条件生成,实现可控图像生成与数据增强 简介这份资源是面向深度学习与生成模型学习者的 ACGAN PyTorch 实现代码包适合已掌握 GAN 基础、希望进一步理解条件生成与类别可控图像生成的开发者。ACGAN 在传统生成对抗网络基础上引入辅助分类器使生成器能依据随机噪声与 one-hot 类别标签合成指定类别的样本常用于图像生成、数据增强与半监督学习等场景。资源包共 4 个文件包含 3 个 py 脚本与 1 个 md 说明文档分别对应模型定义、训练主流程与判别逻辑整体约 3KB结构精简便于快速阅读与二次修改。目前已有 823 人学习下载。通过该实现读者可掌握生成器、判别器与辅助分类器的网络搭建方式理解对抗损失与类别条件损失的组合设计并参考交替训练策略完成模型调试与生成效果监控。1. ACGAN PyTorch 实现从「生成得像」到「生成得准」的那一步用 MNIST 跑通一个 DCGAN看着数字从噪声里慢慢浮现确实挺爽。但很快你会撞上一堵墙模型能生成「像数字的东西」却完全不听你指挥——你想让它画个 7它给你个 3你想批量生成 100 张 8它给你一堆随机数字。这不是训练不够是 DCGAN 的判别器只判断「真不真」压根不关心「是几」。ACGANAuxiliary Classifier GAN要解决的就是这件事在判别器上挂一个辅助分类头让生成器不仅骗过真假判别还得让判别器把生成样本分到正确的类别上。一句话ACGAN 把「生成得像」升级成了「生成得准」。这篇笔记面向两类人一类是已经跑通过基础 GAN、想搞懂条件生成怎么落地的人另一类是直接想拿 ACGAN 做数据增强、类别补齐、小样本扩充的工程师。我会用 PyTorch 从零搭一个能在 MNIST 和 CIFAR-10 上跑的 ACGAN把损失函数怎么写、标签怎么嵌、参数怎么调、哪里容易翻车全部拆开讲清楚。代码可以直接抄参数可以照着改坑我替你踩过了。2. ACGAN 的结构拆解判别器为什么要多一个头2.1 从 DCGAN 到 ACGAN判别器的职责变了DCGAN 的判别器只输出一个标量表示输入图像为真的概率。生成器的损失就是让这个概率越大越好。问题在于这个信号太弱了——它只告诉生成器「你画得不够真」但没告诉它「你画错了类别」。当你要生成 10 类数字时生成器完全不知道当前噪声对应的应该是哪个数字它只能随机覆盖整个数据分布。ACGAN 的做法很直接判别器输出两个东西。第一个还是真假判别输出维度 1第二个是类别分类输出维度等于类别数。生成器的输入除了噪声 z还多了一个类别标签 c。生成器要同时骗过两个头让真假头认为生成样本是真的让分类头认为生成样本属于标签 c。判别器则要同时做对两件事分清真假以及把真实样本和生成样本都分到正确的类别。这里有个关键细节判别器的分类头对真实样本和生成样本都要计算分类损失。对真实样本标签就是它的真实类别对生成样本标签是生成时用的那个 c。这意味着判别器不仅要学会分类真实图像还要学会分类生成图像——后者其实是在帮生成器做「反向监督」。2.2 损失函数两个交叉熵怎么加ACGAN 的损失函数是两部分之和。判别器的损失包括真假判别的二分类交叉熵或 BCEWithLogitsLoss加上类别分类的交叉熵。生成器的损失包括真假判别部分希望判别器把生成样本判为真加上类别分类部分希望判别器把生成样本分到正确类别。具体来说设判别器对输入 x 的输出为 (adv_out, class_out)其中 adv_out 是标量class_out 是长度为 C 的向量。对于真实样本 (x_real, y_real)判别器损失为L_D BCE(adv_out_real, 1) BCE(adv_out_fake, 0) CE(class_out_real, y_real) CE(class_out_fake, y_fake)对于生成器损失为L_G BCE(adv_out_fake, 1) CE(class_out_fake, y_fake)注意这里生成器的分类损失和判别器的分类损失方向是相反的判别器希望分类正确生成器希望判别器「误以为」生成样本属于 y_fake——但 y_fake 本来就是生成时指定的标签所以实际上生成器是在推动判别器把生成样本分到那个标签。这两个损失在训练中会形成对抗但不会像纯 GAN 那样完全零和因为分类任务本身有真实标签作为锚点。一个常见的实现细节是分类损失用 CrossEntropyLoss真假损失用 BCEWithLogitsLoss。后者把 sigmoid 和 BCE 合在一起数值更稳定。如果你用 BCELoss记得在判别器输出后手动加 sigmoid。2.3 生成器怎么把标签嵌进去生成器的输入是噪声 z 和标签 c。标签 c 是整数需要先转成 one-hot 向量或嵌入向量。两种做法都行我一般用 nn.Embedding因为它在类别数多的时候更省参数而且嵌入维度可以调。具体操作把 c 通过 Embedding 层变成维度为 embed_dim 的向量然后把 z 和这个嵌入向量拼接torch.cat一起送进生成器的反卷积网络。嵌入维度一般取 50 到 100MNIST 这种 10 类问题取 50 就够了。拼接后的向量维度是 z_dim embed_dim通常设 z_dim100embed_dim50总共 150 维。判别器这边不需要嵌入它直接接收图像输出 adv_out 和 class_out。class_out 的维度是类别数用全连接层从特征图映射过去。2.4 一个最小可跑的 ACGAN 代码骨架下面这段代码是 MNIST 上的最小实现包含生成器、判别器和训练循环的核心部分。我把它拆成几个代码块方便你逐段理解。import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from torchvision import datasets, transforms # 超参数 z_dim 100 embed_dim 50 num_classes 10 batch_size 128 lr 2e-4 epochs 50 device torch.device(cuda if torch.cuda.is_available() else cpu)这段定义了所有超参数。z_dim 是噪声维度embed_dim 是标签嵌入维度num_classes 是类别数。学习率 2e-4 是 GAN 训练的常用值比普通分类网络低因为 GAN 对学习率很敏感。batch_size 128 在 MNIST 上足够稳定。class Generator(nn.Module): def __init__(self): super().__init__() self.embed nn.Embedding(num_classes, embed_dim) self.fc nn.Sequential( nn.Linear(z_dim embed_dim, 256), nn.BatchNorm1d(256), nn.ReLU(True), nn.Linear(256, 512), nn.BatchNorm1d(512), nn.ReLU(True), nn.Linear(512, 1024), nn.BatchNorm1d(1024), nn.ReLU(True), nn.Linear(1024, 28 * 28), nn.Tanh() ) def forward(self, z, labels): c self.embed(labels) # (B, embed_dim) x torch.cat([z, c], dim1) # (B, z_dim embed_dim) x self.fc(x) return x.view(-1, 1, 28, 28)生成器用全连接层而不是反卷积因为 MNIST 只有 28x28全连接足够。注意最后一层用 Tanh把输出压到 [-1, 1]因为输入图像也归一化到了这个范围。BatchNorm1d 在 1D 特征上做归一化训练时要注意 batch 不能太小否则 BN 的统计量不准。class Discriminator(nn.Module): def __init__(self): super().__init__() self.features nn.Sequential( nn.Linear(28 * 28, 512), nn.LeakyReLU(0.2, inplaceTrue), nn.Linear(512, 256), nn.LeakyReLU(0.2, inplaceTrue), ) self.adv_head nn.Linear(256, 1) # 真假判别 self.class_head nn.Linear(256, num_classes) # 类别分类 def forward(self, x): x x.view(x.size(0), -1) feat self.features(x) adv_out self.adv_head(feat) # (B, 1) class_out self.class_head(feat) # (B, num_classes) return adv_out, class_out判别器共享特征提取层然后分两个头。adv_head 输出 1 维class_head 输出 num_classes 维。注意这里没有在 adv_head 后加 sigmoid因为后面用 BCEWithLogitsLoss它内部会做 sigmoid。LeakyReLU 的负斜率 0.2 是 GAN 里的常见选择比 ReLU 更不容易让梯度死掉。# 初始化模型、优化器、损失函数 G Generator().to(device) D Discriminator().to(device) opt_G optim.Adam(G.parameters(), lrlr, betas(0.5, 0.999)) opt_D optim.Adam(D.parameters(), lrlr, betas(0.5, 0.999)) bce nn.BCEWithLogitsLoss() ce nn.CrossEntropyLoss() # 数据加载 transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)) ]) dataset datasets.MNIST(root./data, trainTrue, downloadTrue, transformtransform) loader DataLoader(dataset, batch_sizebatch_size, shuffleTrue, drop_lastTrue)优化器用 Adambetas 设为 (0.5, 0.999) 而不是默认的 (0.9, 0.999)。这是 GAN 训练的一个经验值降低动量可以避免训练震荡。drop_lastTrue 是为了避免最后一个 batch 只有 1 个样本导致 BatchNorm 报错。for epoch in range(epochs): for imgs, labels in loader: imgs, labels imgs.to(device), labels.to(device) B imgs.size(0) # 真实和生成标签 real_target torch.ones(B, 1, devicedevice) fake_target torch.zeros(B, 1, devicedevice) # --------------------- # 训练判别器 # --------------------- z torch.randn(B, z_dim, devicedevice) fake_labels torch.randint(0, num_classes, (B,), devicedevice) fake_imgs G(z, fake_labels) adv_real, class_real D(imgs) adv_fake, class_fake D(fake_imgs.detach()) loss_D (bce(adv_real, real_target) bce(adv_fake, fake_target) ce(class_real, labels) ce(class_fake, fake_labels)) opt_D.zero_grad() loss_D.backward() opt_D.step() # --------------------- # 训练生成器 # --------------------- adv_fake, class_fake D(fake_imgs) loss_G bce(adv_fake, real_target) ce(class_fake, fake_labels) opt_G.zero_grad() loss_G.backward() opt_G.step()训练循环里有两个关键点。第一训练判别器时生成样本要 detach()否则梯度会回传到生成器打乱训练节奏。第二生成器的损失里真假部分用的是 real_target希望判别器把假样本判为真分类部分用的是 fake_labels生成时指定的标签。这两个损失一起反传生成器就会同时优化「像真」和「类别对」两个目标。3. 在 MNIST 和 CIFAR-10 上跑通参数、结构和训练策略3.1 MNIST 上的收敛曲线和生成效果MNIST 上 ACGAN 通常 20 到 30 个 epoch 就能收敛。判别器的真假损失会稳定在 0.5 到 0.8 之间分类损失会降到 0.1 以下。生成器的真假损失会波动但整体趋势是下降的。如果你看到判别器损失迅速降到 0说明判别器太强了生成器学不动这时候要降低判别器的学习率或者减少它的更新次数。生成效果上ACGAN 的优势在于你可以指定标签生成。比如固定 z只改标签就能看到同一个噪声在不同类别下的输出。如果生成器学到了类别特征同一个 z 在不同标签下应该生成不同数字但风格笔画粗细、倾斜角度保持一致。如果风格也变了说明标签嵌入没有真正起作用生成器可能只是把标签当成了额外的噪声。一个验证方法是做标签插值取两个标签的嵌入向量做线性插值看生成图像是否平滑过渡。如果从 0 到 1 的插值中间出现了 8 或 9说明嵌入空间没有组织好可能需要增大 embed_dim 或者加一个嵌入向量的正则项。3.2 CIFAR-10 上要改什么从全连接到卷积CIFAR-10 是 32x32 彩色图像全连接生成器参数量会爆炸而且生成质量很差。这时候要换成卷积结构。生成器用 ConvTranspose2d 做上采样判别器用 Conv2d 做下采样。标签嵌入还是拼在噪声后面但拼接后要 reshape 成 (B, z_dimembed_dim, 1, 1)然后通过反卷积逐层放大到 32x32。class ConvGenerator(nn.Module): def __init__(self): super().__init__() self.embed nn.Embedding(num_classes, embed_dim) self.fc nn.Linear(z_dim embed_dim, 256 * 4 * 4) self.net nn.Sequential( nn.BatchNorm2d(256), nn.ReLU(True), nn.ConvTranspose2d(256, 128, 4, 2, 1), # 4x4 - 8x8 nn.BatchNorm2d(128), nn.ReLU(True), nn.ConvTranspose2d(128, 64, 4, 2, 1), # 8x8 - 16x16 nn.BatchNorm2d(64), nn.ReLU(True), nn.ConvTranspose2d(64, 3, 4, 2, 1), # 16x16 - 32x32 nn.Tanh() ) def forward(self, z, labels): c self.embed(labels) x torch.cat([z, c], dim1) x self.fc(x).view(-1, 256, 4, 4) return self.net(x)ConvTranspose2d 的参数是 (in_channels, out_channels, kernel_size, stride, padding)。这里用 kernel_size4, stride2, padding1可以把特征图尺寸翻倍。三层反卷积从 4x4 放大到 32x32。判别器对称地用 Conv2d 下采样最后接两个头。CIFAR-10 上训练时间更长通常要 100 个 epoch 以上。学习率可以保持 2e-4但 batch_size 建议降到 64因为 CIFAR-10 的图像更复杂大 batch 容易让判别器过强。另外CIFAR-10 上可以加谱归一化Spectral Normalization来稳定训练PyTorch 里用 nn.utils.spectral_norm 包一下卷积层就行。3.3 训练策略什么时候更新判别器什么时候更新生成器标准 ACGAN 是每步同时更新 D 和 G。但实践中如果判别器太强生成器会梯度消失如果生成器太强判别器会分类不准。一个常用的策略是每训练 1 次生成器训练 1 到 2 次判别器。具体比例要看数据。MNIST 上 1:1 就够了CIFAR-10 上可以试 1:2。另一个策略是标签平滑。把真实样本的标签从 1 改成 0.9假样本从 0 改成 0.1可以防止判别器过度自信。这个技巧在 ACGAN 上同样有效尤其是分类头标签平滑能缓解过拟合。还有一个细节生成器的分类损失权重。有些实现里会给分类损失乘一个系数比如 0.5 或 1.0。如果分类损失太大生成器会优先保证类别正确牺牲图像质量如果太小类别控制会变弱。我一般从 1.0 开始调如果生成图像模糊但类别很准就降到 0.5如果类别经常错就升到 2.0。4. 避坑与排查ACGAN 训练中最容易翻车的 5 个地方4.1 生成器只生成一类图像或者类别完全随机现象训练一段时间后生成器对所有标签都输出同一个数字或者标签和输出完全不对应。原因最常见的原因是判别器的分类头太强生成器的分类损失梯度被真假损失淹没。另一个原因是标签嵌入没有学好生成器把标签当成了噪声。解决先检查判别器的分类准确率。如果真实样本的分类准确率超过 95%但生成样本的分类准确率很低说明判别器在分类任务上过拟合了。这时候要降低判别器分类损失的权重或者给判别器加 dropout。如果标签嵌入没学好可以尝试增大 embed_dim或者在生成器里加一个辅助分类器让生成器自己也预测类别。4.2 训练震荡损失忽高忽低现象判别器和生成器的损失剧烈波动生成图像质量时好时坏。原因学习率太大或者 Adam 的 beta1 太高。GAN 训练本身就不稳定ACGAN 因为多了分类任务震荡会更明显。解决把学习率降到 1e-4 或 5e-5beta1 从 0.9 降到 0.5。如果还震荡可以试 RMSprop 或者 SGD。另外减小 batch_size 也有帮助因为小 batch 的梯度噪声更大反而能平滑训练。4.3 生成图像出现棋盘格伪影现象生成图像上有规律的网格状纹理尤其在反卷积层较多时明显。原因ConvTranspose2d 的 stride 和 kernel_size 不匹配导致输出像素重叠不均匀。解决把 kernel_size 设为 stride 的整数倍比如 stride2 时 kernel_size 用 4 或 6。或者用最近邻上采样加普通卷积代替反卷积。PyTorch 里可以用 nn.Upsample(scale_factor2, modenearest) 接一个 Conv2d效果通常更干净。4.4 判别器分类头对生成样本的损失不下降现象判别器对真实样本分类很准但对生成样本的分类损失一直很高。原因生成样本的分布和真实样本差异太大判别器学到的分类边界不适用于生成样本。这其实是正常现象因为生成样本在训练初期质量很差。但如果训练很久后仍然不下降说明生成器没有学到类别特征。解决检查生成器的分类损失是否在下降。如果生成器的分类损失也不降说明标签嵌入没有起作用。可以尝试把标签嵌入和噪声拼接的方式改成相乘或者用条件批归一化Conditional BatchNorm。另一个方法是给生成器加一个独立的分类器用真实分类器来监督生成样本的类别。4.5 显存溢出或训练速度过慢现象CIFAR-10 上训练时显存不够或者每个 epoch 要跑很久。原因判别器的两个头共享特征层但分类头的参数量可能很大。另外如果 batch_size 太大BatchNorm 的统计量会占用额外显存。解决减小 batch_size或者把判别器的特征层通道数减半。如果显存还是不够可以用混合精度训练torch.cuda.amp。速度方面把 DataLoader 的 num_workers 设为 4 或 8pin_memoryTrue能明显加快数据加载。5. 进阶技巧用 ACGAN 做数据增强和类别补齐ACGAN 最实用的落地场景是数据增强。当你手头某个类别的样本特别少可以用 ACGAN 生成该类的合成样本扩充训练集。但这里有个坑生成样本的质量直接决定增强效果。如果生成样本和真实样本分布差异大增强反而会 hurt 模型。我的做法是先用真实数据训练一个分类器然后用 ACGAN 生成样本把生成样本喂给这个分类器看分类准确率。如果准确率下降超过 5%说明生成样本质量不够不能直接用。这时候可以调低生成样本的权重或者只把生成样本用于预训练再用真实数据微调。另一个技巧是类别插值。ACGAN 的标签嵌入空间是连续的你可以取两个类别的嵌入向量做插值生成中间类别的图像。这在做细粒度分类时很有用比如从「猫」到「狗」的插值可以生成一些介于两者之间的样本帮助模型学到更平滑的决策边界。验证 ACGAN 是否真正学到了类别控制有一个简单方法固定噪声 z遍历所有标签生成一组图像。如果这些图像在类别上明显不同但在风格上一致说明模型学到了解耦的表示。如果风格也跟着变说明标签和噪声没有解耦需要调整嵌入维度或者加正则。最后说一个我踩过的坑ACGAN 的生成器在训练后期容易过拟合到训练集的类别分布。如果训练集里某个类别样本少生成器对这个类别的生成质量会明显差。解决办法是在训练时对类别进行重采样让每个 batch 里各类别样本数均衡。这个细节在数据增强场景下特别重要因为你要补的恰恰是少样本类别。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表