現(xiàn):條件生成與類別控制實(shí)戰(zhàn))
簡介這份資源是面向深度學(xué)習(xí)與生成模型學(xué)習(xí)者的 ACGAN PyTorch 實(shí)現(xiàn)代碼包適合已掌握 GAN 基礎(chǔ)、希望進(jìn)一步理解條件生成與類別可控圖像生成的開發(fā)者。ACGAN 在傳統(tǒng)生成對抗網(wǎng)絡(luò)基礎(chǔ)上引入輔助分類器使生成器能依據(jù) one-hot 類別標(biāo)簽生成指定類別的樣本常用于圖像生成、數(shù)據(jù)增強(qiáng)與半監(jiān)督學(xué)習(xí)等場景。資源包共 4 個(gè)文件包含 3 個(gè) py 腳本與 1 個(gè) md 說明文檔壓縮包約 3KB其中 py 文件分別對應(yīng)訓(xùn)練入口、模型定義與判別邏輯md 文檔用于補(bǔ)充項(xiàng)目說明結(jié)構(gòu)精簡便于快速閱讀與二次修改。目前已有 823 人學(xué)習(xí)下載。通過該實(shí)現(xiàn)讀者可掌握生成器、判別器與輔助分類器的網(wǎng)絡(luò)搭建方式理解對抗損失與類別條件損失的組合設(shè)計(jì)并參考交替訓(xùn)練策略完成模型調(diào)試與生成效果監(jiān)控。1. ACGAN Pytorch實(shí)現(xiàn)從「生成得像」到「生成得準(zhǔn)」的那一步很多人做生成模型時(shí)都遇到過這個(gè)尷尬用 DCGAN 生成手寫數(shù)字圖看著挺像但你想讓它專門生成「數(shù)字 7」它不聽使喚——你沒法控制它生成什么類別。ACGANAuxiliary Classifier GAN解決的正是這個(gè)問題在生成器里同時(shí)喂進(jìn)噪聲和類別標(biāo)簽在判別器里除了判斷真假再加一個(gè)輔助分類頭去預(yù)測類別。這樣一來生成器不僅要騙過判別器還得讓判別器相信它生成的圖屬于指定類別。落到 Pytorch 實(shí)現(xiàn)上核心改動(dòng)其實(shí)只有三處生成器輸入維度加一個(gè) embedding、判別器輸出拆成真假和類別兩路、損失函數(shù)多一項(xiàng)分類交叉熵。這篇筆記按「先講清為什么這么設(shè)計(jì)再給能直接跑的代碼最后說清參數(shù)和踩坑」的順序展開適合已經(jīng)跑通過基礎(chǔ) GAN、想進(jìn)一步做條件生成的從業(yè)者。2. 先把 ACGAN 的結(jié)構(gòu)拆開生成器和判別器各改哪里2.1 為什么判別器要加一個(gè)分類頭普通 GAN 的判別器只輸出一個(gè)標(biāo)量表示輸入圖像為真的概率。ACGAN 的判別器輸出兩個(gè)東西一個(gè)是真假判別 logits一個(gè)是類別分類 logits。這里有個(gè)容易混淆的點(diǎn)——判別器對真實(shí)圖像和生成圖像都要做類別預(yù)測。真實(shí)圖像的類別標(biāo)簽是已知的生成圖像的類別標(biāo)簽就是生成時(shí)喂進(jìn)去的那個(gè)標(biāo)簽。所以判別器的分類損失由兩部分組成真實(shí)圖的分類損失加上生成圖的分類損失。生成器的損失則包含兩部分欺騙判別器的對抗損失以及讓判別器把生成圖分到正確類別的分類損失。這種設(shè)計(jì)的直覺是如果生成器只想著騙過真假判別它可能生成一些「模棱兩可」的圖但加上分類約束后它必須生成類別特征足夠明確的圖否則分類頭會(huì)給出高損失。反過來判別器的分類頭也迫使它學(xué)到類間有區(qū)分度的特征而不是只學(xué)「像不像真實(shí)圖」這種粗粒度判斷。2.2 生成器里類別標(biāo)簽怎么注入類別標(biāo)簽的注入方式直接影響生成質(zhì)量和訓(xùn)練穩(wěn)定性。常見做法有兩種一種是把標(biāo)簽做 embedding 后與噪聲向量拼接一起送進(jìn)全連接層另一種是把標(biāo)簽 embedding 后作為額外的通道拼到特征圖上。對于 32×32 或 64×64 這種小分辨率圖像拼接后進(jìn)全連接層再 reshape 的方式最直接也最容易調(diào)通。下面這段代碼用 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把離散標(biāo)簽映射成連續(xù)向量 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), # 假設(shè)輸出 64x64 灰度圖 nn.Linear(1024, img_channels * 64 * 64), nn.Tanh() # 輸出歸一化到 [-1, 1] ) self.img_channels img_channels def forward(self, noise, labels): # labels: (batch,) 的整數(shù)張量 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)是可學(xué)習(xí)參數(shù)訓(xùn)練過程中會(huì)逐漸把每個(gè)類別映射到一個(gè)有區(qū)分度的向量。nn.Tanh()把輸出壓到 [-1,1]與后面判別器輸入前把真實(shí)圖像也歸一化到 [-1,1] 保持一致。如果換成彩色圖把img_channels改成 3 即可但要注意最后一層全連接輸出維度相應(yīng)變成3*64*64。2.3 判別器的雙頭輸出怎么寫判別器前半部分是共享的特征提取層后半部分分成兩個(gè)頭一個(gè)輸出真假 logits一個(gè)輸出類別 logits。注意真假判別用nn.Linear(feat_dim, 1)類別分類用nn.Linear(feat_dim, num_classes)。兩個(gè)頭都從同一份特征出發(fā)這樣分類任務(wù)能反過來幫助特征提取器學(xué)到更細(xì)粒度的表示。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)是為了防止判別器過強(qiáng)導(dǎo)致生成器梯度消失這是小數(shù)據(jù)集上比較穩(wěn)的經(jīng)驗(yàn)值。如果發(fā)現(xiàn)判別器損失迅速降到接近 0可以把 dropout 提高到 0.4 或 0.5。兩個(gè)頭共享self.feature所以反向傳播時(shí)分類損失也會(huì)更新特征提取層這正是 ACGAN 能學(xué)到類別相關(guān)特征的關(guān)鍵。3. 訓(xùn)練循環(huán)與損失函數(shù)兩個(gè)優(yōu)化器、三項(xiàng)損失怎么配3.1 損失函數(shù)的選擇與權(quán)重ACGAN 的判別器損失由兩部分相加對抗損失真假二分類和分類損失多分類。生成器損失同樣由兩部分相加對抗損失希望判別器把假圖判為真和分類損失希望判別器把假圖分到正確類別。對抗損失用nn.BCEWithLogitsLoss分類損失用nn.CrossEntropyLoss。這里有個(gè)細(xì)節(jié)判別器對真實(shí)圖的分類損失和生成圖的分類損失都要算而生成器只關(guān)心生成圖的分類損失。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) # 構(gòu)造生成器輸入 noise torch.randn(batch_size, 100, devicedevice) fake_labels torch.randint(0, num_classes, (batch_size,), devicedevice) # ---------- 訓(xùn)練判別器 ---------- opt_D.zero_grad() # 真實(shí)圖前向 real_adv, real_cls D(real_imgs) # 生成圖前向detach 防止梯度傳到生成器 fake_imgs G(noise, fake_labels).detach() fake_adv, fake_cls D(fake_imgs) # 對抗損失真實(shí)圖標(biāo)簽為 1生成圖標(biāo)簽為 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)) # 分類損失真實(shí)圖用真實(shí)標(biāo)簽生成圖用生成時(shí)指定的標(biāo)簽 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() # ---------- 訓(xùn)練生成器 ---------- 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()這段代碼里判別器訓(xùn)練時(shí)對生成圖做了.detach()這是必須的否則梯度會(huì)回傳到生成器導(dǎo)致生成器被「提前更新」。生成器訓(xùn)練時(shí)重新前向了一次而不是復(fù)用判別器訓(xùn)練時(shí)的fake_imgs因?yàn)槟桥呀?jīng) detach 了。兩個(gè)優(yōu)化器分別用opt_D和opt_G學(xué)習(xí)率通常設(shè) 0.0002beta1 設(shè) 0.5這是 DCGAN 以來比較通用的配置。3.2 訓(xùn)練循環(huán)的完整骨架與日志觀察把上面的train_step放進(jìn) epoch 循環(huán)里配合數(shù)據(jù)加載和日志打印就是一個(gè)可運(yùn)行的訓(xùn)練腳本。關(guān)鍵是要觀察兩個(gè)損失的變化趨勢如果loss_D很快降到 0.1 以下而loss_G持續(xù)上升說明判別器太強(qiáng)需要降低判別器學(xué)習(xí)率或增加 dropout如果loss_D和loss_G都在 0.6 到 1.0 之間震蕩通常是健康狀態(tài)。from torch.utils.data import DataLoader from torchvision import datasets, transforms # 數(shù)據(jù)準(zhǔn)備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很重要因?yàn)樯善骼锏腂atchNorm1d在 batch size 為 1 時(shí)會(huì)報(bào)錯(cuò)。如果顯存不夠把 batch size 降到 32仍然要保證drop_lastTrue。另外transforms.Resize(64)把 MNIST 從 28×28 放大到 64×64是為了配合生成器輸出尺寸如果不想放大把生成器最后一層改成28*28并去掉 Resize 即可但那樣全連接層的維度也要同步改。4. ACGAN Pytorch實(shí)現(xiàn)避坑這五個(gè)地方最容易翻車4.1 判別器分類頭把生成圖標(biāo)簽搞混現(xiàn)象訓(xùn)練一段時(shí)間后生成器生成的圖類別混亂指定生成「3」卻出來像「8」的圖。原因判別器分類損失里生成圖的標(biāo)簽用了真實(shí)標(biāo)簽而不是生成時(shí)指定的fake_labels。解決檢查loss_fake_cls criterion_cls(fake_cls, fake_labels)這一行確保第二個(gè)參數(shù)是fake_labels而不是real_labels。這個(gè)錯(cuò)誤很隱蔽因?yàn)閾p失值看起來也在下降但生成器學(xué)到的條件映射是錯(cuò)的。4.2 生成器分類損失權(quán)重過大導(dǎo)致模式坍塌現(xiàn)象生成器對每個(gè)類別只生成一種樣本多樣性極低。原因分類損失權(quán)重相對對抗損失過大生成器只顧著讓分類頭滿意忽略了對抗損失對多樣性的約束。解決給分類損失乘一個(gè)系數(shù)比如loss_G loss_G_adv 0.5 * loss_G_cls或者先讓對抗損失主導(dǎo)訓(xùn)練后期再加大分類權(quán)重。我一般會(huì)從 1.0 開始試如果發(fā)現(xiàn)模式坍塌就降到 0.5 或 0.3。4.3 BatchNorm 在生成器輸入層用錯(cuò)位置現(xiàn)象訓(xùn)練初期生成器損失劇烈震蕩生成的圖全是噪聲。原因在生成器第一層全連接后直接加了 BatchNorm而輸入是噪聲加 embedding 的拼接統(tǒng)計(jì)量不穩(wěn)定。解決生成器第一層不加 BatchNorm從第二層開始加。判別器同理第一層不加 BatchNorm用 Dropout 代替。這是小分辨率生成任務(wù)里比較穩(wěn)的做法。4.4 學(xué)習(xí)率不匹配導(dǎo)致一方壓倒另一方現(xiàn)象loss_D 迅速降到接近 0loss_G 持續(xù)上升生成圖越來越差。原因判別器和生成器用同一個(gè)學(xué)習(xí)率但判別器任務(wù)更簡單收斂更快。解決把判別器學(xué)習(xí)率降到生成器的一半比如 G 用 0.0002D 用 0.0001。或者用nn.LeakyReLU(0.2)代替 ReLU緩解梯度消失。如果已經(jīng)出現(xiàn)壓倒可以臨時(shí)凍結(jié)判別器幾輪只訓(xùn)練生成器。4.5 標(biāo)簽 embedding 維度設(shè)得太小現(xiàn)象生成器無法區(qū)分不同類別生成的圖在類別間沒有明顯差異。原因embedding 維度太小比如設(shè)成 10類別信息容量不夠。解決embedding 維度至少設(shè)成 50通常 100 比較穩(wěn)。如果類別數(shù)很多比如 100 類以上可以適當(dāng)加大到 200。這個(gè)參數(shù)沒有理論最優(yōu)值靠實(shí)驗(yàn)調(diào)但低于 50 基本不可用。5. 進(jìn)階技巧用條件插值驗(yàn)證生成器是否真的學(xué)到了類別訓(xùn)練完之后怎么驗(yàn)證生成器不是「假裝」學(xué)到了類別一個(gè)很直接的辦法是做條件插值固定噪聲向量只改變類別標(biāo)簽看生成的圖是否按類別語義變化。如果生成器真的學(xué)到了類別特征那么從「0」到「9」逐個(gè)標(biāo)簽生成時(shí)圖像應(yīng)該呈現(xiàn)出數(shù)字形狀的漸變而不是隨機(jī)跳變。import matplotlib.pyplot as plt G.eval() with torch.no_grad(): # 固定一個(gè)噪聲向量 fixed_noise torch.randn(1, 100, devicedevice) # 對 0 到 9 每個(gè)類別生成一張圖 imgs [] for label in range(10): label_tensor torch.tensor([label], devicedevice) fake G(fixed_noise, label_tensor) imgs.append(fake.squeeze().cpu()) # 拼成 2x5 網(wǎng)格展示 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()很關(guān)鍵因?yàn)樯善骼镉?BatchNorm評估時(shí)要切換到 eval 模式固定統(tǒng)計(jì)量否則每次生成結(jié)果會(huì)隨 batch 統(tǒng)計(jì)量波動(dòng)。如果發(fā)現(xiàn)某些類別生成質(zhì)量明顯差比如「1」很清楚但「8」很模糊說明那個(gè)類別的訓(xùn)練樣本在判別器分類頭里梯度不夠可以檢查一下數(shù)據(jù)集中各類別是否均衡。另一個(gè)驗(yàn)證方法是計(jì)算生成圖的分類準(zhǔn)確率用一個(gè)在真實(shí)數(shù)據(jù)上訓(xùn)練好的分類器去預(yù)測生成圖的類別如果準(zhǔn)確率高說明生成器確實(shí)按標(biāo)簽生成了對應(yīng)類別。這個(gè)方法比人眼判斷更客觀但需要額外訓(xùn)練一個(gè)分類器。我一般會(huì)在訓(xùn)練 ACGAN 之前先訓(xùn)一個(gè)簡單的 CNN 分類器準(zhǔn)確率到 98% 以上就夠用然后拿它來評估生成圖。還有一個(gè)容易忽略的點(diǎn)ACGAN 的生成器在訓(xùn)練后期可能會(huì)「過擬合」到判別器的分類頭即生成器生成的圖能讓判別器分類正確但人眼看著不像。這時(shí)候可以降低分類損失權(quán)重或者增加判別器 dropout讓分類頭不要那么強(qiáng)。我自己的習(xí)慣是每訓(xùn)練 10 個(gè) epoch 就保存一次生成樣本網(wǎng)格圖對比不同階段的生成質(zhì)量而不是只看損失曲線。損失曲線好看但生成圖崩了的情況在 ACGAN 里并不少見血淚經(jīng)驗(yàn)就是別只盯著 loss要定期看圖。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取