ARTICLE DETAIL

资讯详情

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

ACGAN PyTorch实现:条件生成与类别控制实战

ACGAN PyTorch实现:条件生成与类别控制实战 简介这份资源是面向深度学习与生成模型学习者的 ACGAN PyTorch 实现代码包适合已掌握 GAN 基础、希望进一步理解条件生成与类别可控图像生成的开发者。ACGAN 在传统生成对抗网络基础上引入辅助分类器使生成器能依据 one-hot 类别标签生成指定类别的样本常用于图像生成、数据增强与半监督学习等场景。资源包共 4 个文件包含 3 个 py 脚本与 1 个 md 说明文档压缩包约 3KB其中 py 文件分别对应训练入口、模型定义与判别逻辑md 文档用于补充项目说明结构精简便于快速阅读与二次修改。目前已有 823 人学习下载。通过该实现读者可掌握生成器、判别器与辅助分类器的网络搭建方式理解对抗损失与类别条件损失的组合设计并参考交替训练策略完成模型调试与生成效果监控。1. ACGAN Pytorch实现从「生成得像」到「生成得准」的那一步很多人做生成模型时都遇到过这个尴尬用 DCGAN 生成手写数字图看着挺像但你想让它专门生成「数字 7」它不听使唤——你没法控制它生成什么类别。ACGANAuxiliary Classifier GAN解决的正是这个问题在生成器里同时喂进噪声和类别标签在判别器里除了判断真假再加一个辅助分类头去预测类别。这样一来生成器不仅要骗过判别器还得让判别器相信它生成的图属于指定类别。落到 Pytorch 实现上核心改动其实只有三处生成器输入维度加一个 embedding、判别器输出拆成真假和类别两路、损失函数多一项分类交叉熵。这篇笔记按「先讲清为什么这么设计再给能直接跑的代码最后说清参数和踩坑」的顺序展开适合已经跑通过基础 GAN、想进一步做条件生成的从业者。2. 先把 ACGAN 的结构拆开生成器和判别器各改哪里2.1 为什么判别器要加一个分类头普通 GAN 的判别器只输出一个标量表示输入图像为真的概率。ACGAN 的判别器输出两个东西一个是真假判别 logits一个是类别分类 logits。这里有个容易混淆的点——判别器对真实图像和生成图像都要做类别预测。真实图像的类别标签是已知的生成图像的类别标签就是生成时喂进去的那个标签。所以判别器的分类损失由两部分组成真实图的分类损失加上生成图的分类损失。生成器的损失则包含两部分欺骗判别器的对抗损失以及让判别器把生成图分到正确类别的分类损失。这种设计的直觉是如果生成器只想着骗过真假判别它可能生成一些「模棱两可」的图但加上分类约束后它必须生成类别特征足够明确的图否则分类头会给出高损失。反过来判别器的分类头也迫使它学到类间有区分度的特征而不是只学「像不像真实图」这种粗粒度判断。2.2 生成器里类别标签怎么注入类别标签的注入方式直接影响生成质量和训练稳定性。常见做法有两种一种是把标签做 embedding 后与噪声向量拼接一起送进全连接层另一种是把标签 embedding 后作为额外的通道拼到特征图上。对于 32×32 或 64×64 这种小分辨率图像拼接后进全连接层再 reshape 的方式最直接也最容易调通。下面这段代码用 embedding 维度 100、噪声维度 100拼接后总输入维度 200。import torch import torch.nn as nn class Generator(nn.Module): def __init__(self, noise_dim100, num_classes10, embed_dim100, img_channels1): super().__init__() self.noise_dim noise_dim # 类别 embedding把离散标签映射成连续向量 self.label_embed nn.Embedding(num_classes, embed_dim) # 拼接后输入维度 噪声维度 embedding 维度 self.fc nn.Sequential( nn.Linear(noise_dim embed_dim, 256), nn.BatchNorm1d(256), nn.LeakyReLU(0.2, inplaceTrue), nn.Linear(256, 512), nn.BatchNorm1d(512), nn.LeakyReLU(0.2, inplaceTrue), nn.Linear(512, 1024), nn.BatchNorm1d(1024), nn.LeakyReLU(0.2, inplaceTrue), # 假设输出 64x64 灰度图 nn.Linear(1024, img_channels * 64 * 64), nn.Tanh() # 输出归一化到 [-1, 1] ) self.img_channels img_channels def forward(self, noise, labels): # labels: (batch,) 的整数张量 label_vec self.label_embed(labels) # (batch, embed_dim) x torch.cat([noise, label_vec], dim1) # (batch, noise_dimembed_dim) x self.fc(x) return x.view(-1, self.img_channels, 64, 64)这段代码里nn.Embedding(num_classes, embed_dim)是可学习参数训练过程中会逐渐把每个类别映射到一个有区分度的向量。nn.Tanh()把输出压到 [-1,1]与后面判别器输入前把真实图像也归一化到 [-1,1] 保持一致。如果换成彩色图把img_channels改成 3 即可但要注意最后一层全连接输出维度相应变成3*64*64。2.3 判别器的双头输出怎么写判别器前半部分是共享的特征提取层后半部分分成两个头一个输出真假 logits一个输出类别 logits。注意真假判别用nn.Linear(feat_dim, 1)类别分类用nn.Linear(feat_dim, num_classes)。两个头都从同一份特征出发这样分类任务能反过来帮助特征提取器学到更细粒度的表示。class Discriminator(nn.Module): def __init__(self, num_classes10, img_channels1): super().__init__() self.feature nn.Sequential( nn.Linear(img_channels * 64 * 64, 1024), nn.LeakyReLU(0.2, inplaceTrue), nn.Dropout(0.3), nn.Linear(1024, 512), nn.LeakyReLU(0.2, inplaceTrue), nn.Dropout(0.3), nn.Linear(512, 256), nn.LeakyReLU(0.2, inplaceTrue), ) # 真假判别头输出 1 维 logit self.adv_head nn.Linear(256, 1) # 类别分类头输出 num_classes 维 logit self.cls_head nn.Linear(256, num_classes) def forward(self, img): feat self.feature(img.view(img.size(0), -1)) adv_logit self.adv_head(feat) # (batch, 1) cls_logit self.cls_head(feat) # (batch, num_classes) return adv_logit, cls_logit这里用Dropout(0.3)是为了防止判别器过强导致生成器梯度消失这是小数据集上比较稳的经验值。如果发现判别器损失迅速降到接近 0可以把 dropout 提高到 0.4 或 0.5。两个头共享self.feature所以反向传播时分类损失也会更新特征提取层这正是 ACGAN 能学到类别相关特征的关键。3. 训练循环与损失函数两个优化器、三项损失怎么配3.1 损失函数的选择与权重ACGAN 的判别器损失由两部分相加对抗损失真假二分类和分类损失多分类。生成器损失同样由两部分相加对抗损失希望判别器把假图判为真和分类损失希望判别器把假图分到正确类别。对抗损失用nn.BCEWithLogitsLoss分类损失用nn.CrossEntropyLoss。这里有个细节判别器对真实图的分类损失和生成图的分类损失都要算而生成器只关心生成图的分类损失。import torch.nn.functional as F def train_step(real_imgs, real_labels, G, D, opt_G, opt_D, criterion_adv, criterion_cls, device, num_classes10): batch_size real_imgs.size(0) real_imgs real_imgs.to(device) real_labels real_labels.to(device) # 构造生成器输入 noise torch.randn(batch_size, 100, devicedevice) fake_labels torch.randint(0, num_classes, (batch_size,), devicedevice) # ---------- 训练判别器 ---------- opt_D.zero_grad() # 真实图前向 real_adv, real_cls D(real_imgs) # 生成图前向detach 防止梯度传到生成器 fake_imgs G(noise, fake_labels).detach() 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() opt_D.step() # ---------- 训练生成器 ---------- opt_G.zero_grad() # 重新前向这次不 detach fake_imgs G(noise, fake_labels) fake_adv, fake_cls D(fake_imgs) # 生成器希望判别器把假图判为真 loss_G_adv criterion_adv(fake_adv, torch.ones_like(fake_adv)) # 生成器希望分类头把假图分到 fake_labels loss_G_cls criterion_cls(fake_cls, fake_labels) loss_G loss_G_adv loss_G_cls loss_G.backward() opt_G.step() return loss_D.item(), loss_G.item()这段代码里判别器训练时对生成图做了.detach()这是必须的否则梯度会回传到生成器导致生成器被「提前更新」。生成器训练时重新前向了一次而不是复用判别器训练时的fake_imgs因为那批已经 detach 了。两个优化器分别用opt_D和opt_G学习率通常设 0.0002beta1 设 0.5这是 DCGAN 以来比较通用的配置。3.2 训练循环的完整骨架与日志观察把上面的train_step放进 epoch 循环里配合数据加载和日志打印就是一个可运行的训练脚本。关键是要观察两个损失的变化趋势如果loss_D很快降到 0.1 以下而loss_G持续上升说明判别器太强需要降低判别器学习率或增加 dropout如果loss_D和loss_G都在 0.6 到 1.0 之间震荡通常是健康状态。from torch.utils.data import DataLoader from torchvision import datasets, transforms # 数据准备MNIST 归一化到 [-1,1] transform transforms.Compose([ transforms.Resize(64), transforms.ToTensor(), transforms.Normalize([0.5], [0.5]) ]) dataset datasets.MNIST(root./data, trainTrue, downloadTrue, transformtransform) loader DataLoader(dataset, batch_size64, shuffleTrue, drop_lastTrue) device torch.device(cuda if torch.cuda.is_available() else cpu) G Generator().to(device) D Discriminator().to(device) opt_G torch.optim.Adam(G.parameters(), lr0.0002, betas(0.5, 0.999)) opt_D torch.optim.Adam(D.parameters(), lr0.0002, betas(0.5, 0.999)) criterion_adv nn.BCEWithLogitsLoss() criterion_cls nn.CrossEntropyLoss() for epoch in range(50): for i, (imgs, labels) in enumerate(loader): loss_D, loss_G train_step(imgs, labels, G, D, opt_G, opt_D, criterion_adv, criterion_cls, device) if i % 100 0: print(fEpoch [{epoch}] Step [{i}] loss_D{loss_D:.4f} loss_G{loss_G:.4f})这里drop_lastTrue很重要因为生成器里的BatchNorm1d在 batch size 为 1 时会报错。如果显存不够把 batch size 降到 32仍然要保证drop_lastTrue。另外transforms.Resize(64)把 MNIST 从 28×28 放大到 64×64是为了配合生成器输出尺寸如果不想放大把生成器最后一层改成28*28并去掉 Resize 即可但那样全连接层的维度也要同步改。4. ACGAN Pytorch实现避坑这五个地方最容易翻车4.1 判别器分类头把生成图标签搞混现象训练一段时间后生成器生成的图类别混乱指定生成「3」却出来像「8」的图。原因判别器分类损失里生成图的标签用了真实标签而不是生成时指定的fake_labels。解决检查loss_fake_cls criterion_cls(fake_cls, fake_labels)这一行确保第二个参数是fake_labels而不是real_labels。这个错误很隐蔽因为损失值看起来也在下降但生成器学到的条件映射是错的。4.2 生成器分类损失权重过大导致模式坍塌现象生成器对每个类别只生成一种样本多样性极低。原因分类损失权重相对对抗损失过大生成器只顾着让分类头满意忽略了对抗损失对多样性的约束。解决给分类损失乘一个系数比如loss_G loss_G_adv 0.5 * loss_G_cls或者先让对抗损失主导训练后期再加大分类权重。我一般会从 1.0 开始试如果发现模式坍塌就降到 0.5 或 0.3。4.3 BatchNorm 在生成器输入层用错位置现象训练初期生成器损失剧烈震荡生成的图全是噪声。原因在生成器第一层全连接后直接加了 BatchNorm而输入是噪声加 embedding 的拼接统计量不稳定。解决生成器第一层不加 BatchNorm从第二层开始加。判别器同理第一层不加 BatchNorm用 Dropout 代替。这是小分辨率生成任务里比较稳的做法。4.4 学习率不匹配导致一方压倒另一方现象loss_D 迅速降到接近 0loss_G 持续上升生成图越来越差。原因判别器和生成器用同一个学习率但判别器任务更简单收敛更快。解决把判别器学习率降到生成器的一半比如 G 用 0.0002D 用 0.0001。或者用nn.LeakyReLU(0.2)代替 ReLU缓解梯度消失。如果已经出现压倒可以临时冻结判别器几轮只训练生成器。4.5 标签 embedding 维度设得太小现象生成器无法区分不同类别生成的图在类别间没有明显差异。原因embedding 维度太小比如设成 10类别信息容量不够。解决embedding 维度至少设成 50通常 100 比较稳。如果类别数很多比如 100 类以上可以适当加大到 200。这个参数没有理论最优值靠实验调但低于 50 基本不可用。5. 进阶技巧用条件插值验证生成器是否真的学到了类别训练完之后怎么验证生成器不是「假装」学到了类别一个很直接的办法是做条件插值固定噪声向量只改变类别标签看生成的图是否按类别语义变化。如果生成器真的学到了类别特征那么从「0」到「9」逐个标签生成时图像应该呈现出数字形状的渐变而不是随机跳变。import matplotlib.pyplot as plt G.eval() with torch.no_grad(): # 固定一个噪声向量 fixed_noise torch.randn(1, 100, devicedevice) # 对 0 到 9 每个类别生成一张图 imgs [] for label in range(10): label_tensor torch.tensor([label], devicedevice) fake G(fixed_noise, label_tensor) imgs.append(fake.squeeze().cpu()) # 拼成 2x5 网格展示 fig, axes plt.subplots(2, 5, figsize(10, 4)) for idx, ax in enumerate(axes.flat): ax.imshow(imgs[idx], cmapgray) ax.set_title(flabel{idx}) ax.axis(off) plt.tight_layout() plt.show()这段代码里G.eval()很关键因为生成器里有 BatchNorm评估时要切换到 eval 模式固定统计量否则每次生成结果会随 batch 统计量波动。如果发现某些类别生成质量明显差比如「1」很清楚但「8」很模糊说明那个类别的训练样本在判别器分类头里梯度不够可以检查一下数据集中各类别是否均衡。另一个验证方法是计算生成图的分类准确率用一个在真实数据上训练好的分类器去预测生成图的类别如果准确率高说明生成器确实按标签生成了对应类别。这个方法比人眼判断更客观但需要额外训练一个分类器。我一般会在训练 ACGAN 之前先训一个简单的 CNN 分类器准确率到 98% 以上就够用然后拿它来评估生成图。还有一个容易忽略的点ACGAN 的生成器在训练后期可能会「过拟合」到判别器的分类头即生成器生成的图能让判别器分类正确但人眼看着不像。这时候可以降低分类损失权重或者增加判别器 dropout让分类头不要那么强。我自己的习惯是每训练 10 个 epoch 就保存一次生成样本网格图对比不同阶段的生成质量而不是只看损失曲线。损失曲线好看但生成图崩了的情况在 ACGAN 里并不少见血泪经验就是别只盯着 loss要定期看图。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表