版權聲明:本文爲原創文章,未經博主允許不得用於商業用途。
3. GAN
3.1 原理
3.1.1 概述
GAN最基本的原理其實就是Generator和Discriminator互相對抗共同進步的過程,有點像回合制遊戲。一般的生成模型在產生新的輸出時一般都是通過已有數據的合成,因此很模糊,而GAN就不會。在每一輪訓練中D(Discriminator)都儘量將上一輪中Generator的輸出標記爲0,將原始數據集標記爲1,而G(Generator)都儘量使得上一輪的D輸出爲1,因此在訓練其中一個網絡時另一個要保持參數不變。
更進一步,設數據集爲,G的輸入符合高斯分佈,隨機採樣m次,G的輸出爲,則:
D的優化目標爲:
其中前一項使數據集標記接近1,後一項使得G的生成結果標記接近0。
G的優化目標爲:
對於單純的生成模型,一般很難考慮全局效果,比如之前訓練的VAE,噪點出現的位置就對優化目標沒有影響,而GAN得Discriminator則可以很容易得學到全局特徵(比如使用卷積),因此往往效果更好。
另外在實際訓練時往往採用而不是,這是由於初期下降太慢,G訓練不起來,容易訓練出過強的D。
3.1.2 Conditional GAN
由於通常情況下Generative Model的學習是完全無監督的,因此即使產生了很好的輸出,但是對於輸入的編碼還是無法控制的,二Conditional GAN的輸入增加了對樣本的描述信息,因此可以根據需要產生輸出。
如果用公式表示即數據集爲,G的輸入爲,輸出,另外從數據集再選取m個數據
D的優化目標爲:
第三項表示描述和輸出不匹配的情況
G的優化目標:
在實現時D一般有兩種結構:
下一種會分別鑑別生成是否足夠好和於輸入要求是否匹配。
3.1.3 Stack GAN
在產生比較大的輸出時,可以先產生較小的輸出(如低分辨率的圖片),再根據第一個G的輸出產生更大的輸出。
3.1.4 WGAN(Wasserstein GAN)
普通GAN模型的評估函數一般只是簡單的分類器,所以實際上只要D的輸出爲0,G就沒有優化的必要。換句話說,如果D能夠區分出G和G’的輸出,則G和G’之間就無法比較優劣。從梯度的角度,D對G和G’的梯度太小,G很難訓練。
WGAN模型中定義了新的評估函數,即Earth Mover’s Distance,表示從一個分佈轉化到另一個分佈所需要的最短距離。
上圖中的矩陣就是一種從P分佈轉化到Q分佈的方法,其中表示從p的第維移動多少到q的第維。定義平均距離:
則Wasserstein距離即最優方案的平均距離,其優化目標爲:
其中Lipschitz函數是一個足夠平滑的函數,其定義如下:
K=1時即爲1-Lipschitz函數。
在解優化問題時,常用的方法有Weight Clipping和WGAN-GP,其中WeightClipping和如名字描述,爲權重增加上限和下限。而WGAN的優化目標如下:
在實作時:
D的優化目標爲:
第三項表示描述和輸出不匹配的情況
G的優化目標:
在計算梯度時加入Clipping或者WGAN-GP
3.1.5 EBGAN
EBGAN使用一個Autoencoder作爲D,將Autoencoder的重構誤差作爲D的輸出。因此D可以預先在數據集上訓練,所以可以立刻就獲得一個比較強的D。
3.2 實踐
本來想要根據李宏毅老師2018年提供的二次元人臉數據集訓練一個CGAN的,不過實際訓練效果並不是很好,所以退而求其次訓練DCGAN。
3.2.1 經驗總結
訓練的時候發現一些技巧:
- 每一輪迭代時,當D或是G的輸出正確率達到就停止訓練了,因爲這時候另一個模型的正確率已經很低了,如果一直訓練下去可能會使得梯度變得很小,D和G就會失去平衡。我取的是
- DCGAN中D的生成網絡初始映射的channel一定要多,不要吝惜內存,不然很容易D就偏向只對realdata或是fakedata的輸入判斷,另一個正確率爲0。大概是因爲要保持realdata和fakedata的輸入準確率都很高,所以儘量避免卷積時的信息損失吧
- G比D重要得多,我在服務器上跑有好多次都是數據忘記保存了,或是D或是G突然失效了,這種時候就要修改模型,一般如果G沒有失效,只要保存G的參數,很快就能訓練出新的D。並且DCGAN裏面G的參數少得多,所以文件也方便保存
- 對於D,real和fake的訓練最好分開,這樣纔好在D失衡的時候調整訓練方向,比如如果在real上的準確率達到0.8,就可以先停止在realdata上訓練
- 爲了防止出現D強G弱的現象,在計算D的損失函數時,不直接使用標籤,而是加入隨機噪聲
- 大部分模型學習速率0.0002最佳
- G的輸出可以用tanh規範化到
- D的網絡中加入批規範化(BatchNormalize)效果非常好
- 在D中使用LeakyReLU
3.2.2 網路結構
其實GAN的網絡結構都很簡單
#for generator, input is a norm-distribution vector x[0...80]
class Generator(nn.Module):
def __init__(self):
super(Generator, self).__init__()
self.fc = nn.Linear(80,4*4*128)
self.remap = nn.Sequential(
nn.ConvTranspose2d(128,64,5,stride=2,padding=1),
nn.ReLU(),
nn.BatchNorm2d(64),
nn.ConvTranspose2d(64,32,5,stride=2,padding=1),
nn.ReLU(),
nn.BatchNorm2d(32),
nn.ConvTranspose2d(32,3,6,stride=2,padding=1),
)
def forward(self, c):
c = self.fc(c).cuda()
c = c.view(c.shape[0],128,4,4)
c = self.remap(c)
c = torch.tanh(c)
#constrain to [-1,1]
c = c.view(c.shape[0],3,40,40)
c = (c+1)/2
#map to [0,1]
return c
#for discriminator, input is a 40*40 RGB-face
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
self.cmap = nn.Sequential(
nn.Conv2d(3,64,5),
nn.LeakyReLU(),
nn.BatchNorm2d(64),
nn.Conv2d(64,128,5),
)
self.fcm = nn.Sequential(
nn.Linear(128*32*32,5*18*18),
nn.Linear(5*18*18,512),
nn.Linear(512,128),
nn.Linear(128,32),
nn.Linear(32,1),
)
def forward(self, x):
x = self.cmap(x)
x = x.view(x.shape[0],128*32*32)
x = self.fcm(x)
x = F.sigmoid(x)
return x
實際上唯一的技術難點在訓練上,我使用如下方法訓練:
def train(epoch_num):
for epoch in range(epoch_num):
#firstly, train D
for d_epoch in range(D_step):
d_totalloss = 0
d_totalloss = 0
i = 0
for face,tag in dataloader:
face = face.cuda()
tag = tag.cuda()
i += 1
Doptim.zero_grad()
#train on real data
drealout = discrimitor(face)
#train on fake data
#get random input for Generator
gvec,gtag = GSampler(face.shape[0])
gout = generator(gvec).detach()
dfakeout = discrimitor(gout)
d_rloss,d_floss = DLossfun(drealout,dfakeout)
#use BCELog function
dacc_r = sum(drealout)/drealout.shape[0]
dacc_f = 1-sum(dfakeout)/dfakeout.shape[0]
#start with
#if(dacc_r.item()>0.8) and (dacc_f.item()>0.8):
#after 500 epoch
if(dacc_r.item()>0.7) and (dacc_f.item()>0.7):
break
#train on realdata and fakedata respectively
if(dacc_f.item()<0.7):
d_floss.backward()
if(dacc_r.item()<0.7):
d_rloss.backward()
d_totalloss += (float(d_rloss)+float(d_floss))/2
Doptim.step()
print('\tD_epoch:{}/{}: D_batch_loss:{:.8f}\t\tAcc:{:.4f}, {:.4f}'.format(d_epoch, D_step, d_floss.item(),dacc_r.item(),dacc_f.item()))
#secondly, train G
for g_epoch in range(G_step):
g_totalloss = 0
i = 0
for batch in range(int(round(datasize/batch_size))):
i += 1
Goptim.zero_grad()
#use random input
gvec,gtag = GSampler(batch_size)
gout = generator(gvec)
dout = discrimitor(gout)
#use Discriminator's output to calculate loss
g_loss = GLossfun(dout)
gacc = sum(dout)/dout.shape[0]
#init
#if gacc.item()>0.9:
#after 470 epoch
if gacc.item()>0.7:
break
g_loss.backward()
g_totalloss += float(g_loss)
Goptim.step()
print('\tG_epoch:{}/{}: G_batch_loss:{:.8f}\t\tGAcc:{:.4f}'.format(g_epoch, G_step, g_loss.item(),gacc.item()))
if epoch%10==0:
#vitest()
#save model
print('savepoint')
torch.save(generator.state_dict(),'./DCGs.pth')
torch.save(discrimitor.state_dict(),'./DCDs.pth')
torch.save(Goptim.state_dict(),'./DCGsopt.pth')
torch.save(Doptim.state_dict(),'./DCDsopt.pth')
print('\nepoch:{}/{}'.format(epoch, epoch_num))
3.2.3 訓練結果
訓練過程
實際上訓練尚未結束,不過近期可能沒時間繼續訓練了,記一下當前的進度吧,目前一共跑了大概四個多小時。
-
剛開始的300輪非常快,大概十幾分鍾就跑完了
-
800輪時D失效,無論怎麼訓練都是在realdata的正確率爲100%,在fakedata正確率爲0。回退到700輪將減小爲,重新訓練D。
-
在900輪時D再次失效,出現D在fakedata正確率100%,在realdata正確率爲0%,導致G梯度消失沒辦法訓練。這次沒有保存D,並且D已經開始不穩定,所以更改了D的結構,增加channel數,並且單獨訓練10個epoch(),G和D重新達到平衡。
1000epoch以後的訓練過程大概是這樣的:
可以看到G和D的準確率都不是特別高了,不過對抗的意味還是很明顯的。
G的輸出
下面的組圖是從300epoch(D和G),500epoch,700epoch,900epoch,1000epoch,1240epoch的記錄
500-1000中G幾乎嘗試過了RGB三種顏色來騙過D,最後到1100開始使用混合顏色了。從1240epoch能看出臉的輪廓?不過還存在ConvTranspose特有的方格。
1000以後的訓練每一輪epoch大概要5s。
未完待續
代碼見github
未完待續…