非監督學習(三)GAN(生成二次元人臉)

版權聲明:本文爲原創文章,未經博主允許不得用於商業用途。

3. GAN

3.1 原理

3.1.1 概述

\qquadGAN最基本的原理其實就是Generator和Discriminator互相對抗共同進步的過程,有點像回合制遊戲。一般的生成模型在產生新的輸出時一般都是通過已有數據的合成,因此很模糊,而GAN就不會。在每一輪訓練中D(Discriminator)都儘量將上一輪中Generator的輸出標記爲0,將原始數據集標記爲1,而G(Generator)都儘量使得上一輪的D輸出爲1,因此在訓練其中一個網絡時另一個要保持參數不變。

\qquad更進一步,設數據集爲{x1,x2,...,xm}\{x^1,x^2,...,x^m\},G的輸入zz符合高斯分佈,隨機採樣m次,G的輸出爲{x~1,x~2,...x~m}\{\tilde{x}^1,\tilde{x}^2,...\tilde{x}^m\},則:

D的優化目標爲
max V~=1mi=1mlogD(xi)+1mi=1mlog(1D(x~i)) max\ \tilde{V}=\frac{1}{m}\sum_{i=1}^mlogD(x^i)+\frac{1}{m}\sum_{i=1}^mlog(1-D(\tilde{x}^i))
其中前一項使數據集標記接近1,後一項使得G的生成結果標記接近0。

G的優化目標爲
max V~=1mi=1mlog(D(G(zi))) max\ \tilde{V}=\frac{1}{m}\sum_{i=1}^mlog(D(G(z^i)))
\qquad對於單純的生成模型,一般很難考慮全局效果,比如之前訓練的VAE,噪點出現的位置就對優化目標沒有影響,而GAN得Discriminator則可以很容易得學到全局特徵(比如使用卷積),因此往往效果更好。

\qquad另外在實際訓練時往往採用logD-logD而不是log(1D)log(1-D),這是由於log(1D)-log(1-D)初期下降太慢,G訓練不起來,容易訓練出過強的D。

3.1.2 Conditional GAN

\qquad由於通常情況下Generative Model的學習是完全無監督的,因此即使產生了很好的輸出,但是對於輸入的編碼zz還是無法控制的,二Conditional GAN的輸入增加了對樣本的描述信息,因此可以根據需要產生輸出。

\qquad如果用公式表示即數據集爲{(c1,x1),(c2,x2),...,(cm,xm)}\{(c^1,x^1),(c^2,x^2),...,(c^m,x^m)\},G的輸入爲zN(μ,σ)z\sim N(\mu,\sigma),輸出x~i=G(ci,zi)\tilde{x}^i=G(c^i,z^i),另外從數據集再選取m個數據{x^1,x^2,...,x^m}\{\hat {x}^1,\hat {x}^2,...,\hat {x}^m\}

D的優化目標爲
max V~=1mi=1mlogD(ci,xi)+1mi=1mlog(1D(ci,x~i))+1mi=1mlog(1D(ci,x^i)) max\ \tilde{V}=\frac{1}{m}\sum_{i=1}^mlogD(c^i,x^i)+\frac{1}{m}\sum_{i=1}^mlog(1-D(c^i,\tilde{x}^i))+\frac{1}{m}\sum_{i=1}^mlog(1-D(c^i,\hat{x}^i))
第三項表示描述和輸出不匹配的情況

G的優化目標
max V~=1mi=1mlog(D(G(ci,zi))) max\ \tilde{V}=\frac{1}{m}\sum_{i=1}^mlog(D(G(c^i,z^i)))
​ 在實現時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,表示從一個分佈轉化到另一個分佈所需要的最短距離。

在這裏插入圖片描述

上圖中的矩陣γ\gamma就是一種從P分佈轉化到Q分佈的方法,其中γ(xp,xq)\gamma(x_p,x_q)表示從p的第xpx_p維移動多少到q的第xqx_q維。定義平均距離:
B(γ)=xp,xqγ(xp,xq)xpxq B(\gamma)=\sum_{x_p,x_q}\gamma(x_p,x_q)||x_p-x_q||
則Wasserstein距離即最優方案的平均距離,其優化目標爲:
V(G,D)=maxD1Lipschitz{ExPdata[D(x)]ExPG[D(x)]} V(G,D)=\underset{D\in 1-Lipschitz}{max}\{E_{x\sim P_{data}}[D(x)]-E_{x\sim P_G}[D(x)]\}
其中Lipschitz函數是一個足夠平滑的函數,其定義如下:
f(x1)f(x2)Kx1x2 ||f(x_1)-f(x_2)||\leq K||x_1-x_2||
K=1時即爲1-Lipschitz函數。

在解優化問題時,常用的方法有Weight Clipping和WGAN-GP,其中WeightClipping和如名字描述,爲權重增加上限和下限。而WGAN的優化目標如下:
V(G,D)=maxD{ExPdata[D(x)]ExPG[D(x)]}λExPpenalty[(D1)2] V(G,D)=\underset{D}{max}\{E_{x\sim P_{data}}[D(x)]-E_{x\sim P_G}[D(x)]\}-\lambda E_{x\sim P_{penalty}}[(||\nabla D||-1)^2]
​ 在實作時:
D的優化目標爲
max V~=1mi=1mD(xi)1mi=1mD(x~i) max\ \tilde{V}=\frac{1}{m}\sum_{i=1}^mD(x^i)-\frac{1}{m}\sum_{i=1}^mD(\tilde{x}^i)
第三項表示描述和輸出不匹配的情況

G的優化目標
max V~=1mi=1mD(G(zi)) max\ \tilde{V}=-\frac{1}{m}\sum_{i=1}^mD(G(z^i))
在計算梯度時加入Clipping或者WGAN-GP

3.1.5 EBGAN

​ EBGAN使用一個Autoencoder作爲D,將Autoencoder的重構誤差作爲D的輸出。因此D可以預先在數據集上訓練,所以可以立刻就獲得一個比較強的D。

3.2 實踐

​ 本來想要根據李宏毅老師2018年提供的二次元人臉數據集訓練一個CGAN的,不過實際訓練效果並不是很好,所以退而求其次訓練DCGAN。

3.2.1 經驗總結

訓練的時候發現一些技巧:

  • 每一輪迭代時,當D或是G的輸出正確率達到p0p_0就停止訓練了,因爲這時候另一個模型的正確率已經很低了,如果一直訓練下去可能會使得梯度變得很小,D和G就會失去平衡。我取的是p0=0.8p_0=0.8
  • 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,1}\{0,1\}標籤,而是加入隨機噪聲
  • 大部分模型學習速率0.0002最佳
  • G的輸出可以用tanh規範化到[1,1][-1,1]
  • 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輪將p0p_0減小爲0.70.7,重新訓練D。

  • 在900輪時D再次失效,出現D在fakedata正確率100%,在realdata正確率爲0%,導致G梯度消失沒辦法訓練。這次沒有保存D,並且D已經開始不穩定,所以更改了D的結構,增加channel數,並且單獨訓練10個epoch(10epoch×5d_epoch10個epoch\times 5個d\_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

未完待續…

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章