非监督学习(三)GAN(生成二次元人脸)

版权声明:本文为原创文章,未经博主允许不得用于商业用途。

3. GAN

3.1 原理

3.1.1 概述

\qquad GAN最基本的原理其实就是Generator和Discriminator互相对抗共同进步的过程,有点像回合制游戏。一般的生成模型在产生新的输出时一般都是通过已有数据的合成,因此很模糊,而GAN就不会。在每一轮训练中D(Discriminator)都尽量将上一轮中Generator的输出标记为0,将原始数据集标记为1,而G(Generator)都尽量使得上一轮的D输出为1,因此在训练其中一个网络时另一个要保持参数不变。

\qquad 更进一步,设数据集为 { x 1 , x 2 , . . . , x m } \{x^1,x^2,...,x^m\} {x1,x2,...,xm},G的输入 z z z符合高斯分布,随机采样m次,G的输出为 { x ~ 1 , x ~ 2 , . . . x ~ m } \{\tilde{x}^1,\tilde{x}^2,...\tilde{x}^m\} {x~1,x~2,...x~m},则:

D的优化目标为
m a x   V ~ = 1 m ∑ i = 1 m l o g D ( x i ) + 1 m ∑ i = 1 m l o g ( 1 − D ( 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)) max V~=m1i=1mlogD(xi)+m1i=1mlog(1D(x~i))
其中前一项使数据集标记接近1,后一项使得G的生成结果标记接近0。

G的优化目标为
m a x   V ~ = 1 m ∑ i = 1 m l o g ( D ( G ( z i ) ) ) max\ \tilde{V}=\frac{1}{m}\sum_{i=1}^mlog(D(G(z^i))) max V~=m1i=1mlog(D(G(zi)))
\qquad 对于单纯的生成模型,一般很难考虑全局效果,比如之前训练的VAE,噪点出现的位置就对优化目标没有影响,而GAN得Discriminator则可以很容易得学到全局特征(比如使用卷积),因此往往效果更好。

\qquad 另外在实际训练时往往采用 − l o g D -logD logD而不是 l o g ( 1 − D ) log(1-D) log(1D),这是由于 − l o g ( 1 − D ) -log(1-D) log(1D)初期下降太慢,G训练不起来,容易训练出过强的D。

3.1.2 Conditional GAN

\qquad 由于通常情况下Generative Model的学习是完全无监督的,因此即使产生了很好的输出,但是对于输入的编码 z z z还是无法控制的,二Conditional GAN的输入增加了对样本的描述信息,因此可以根据需要产生输出。

\qquad 如果用公式表示即数据集为 { ( c 1 , x 1 ) , ( c 2 , x 2 ) , . . . , ( c m , x m ) } \{(c^1,x^1),(c^2,x^2),...,(c^m,x^m)\} {(c1,x1),(c2,x2),...,(cm,xm)},G的输入为 z ∼ N ( μ , σ ) z\sim N(\mu,\sigma) zN(μ,σ),输出 x ~ i = G ( c i , z i ) \tilde{x}^i=G(c^i,z^i) x~i=G(ci,zi),另外从数据集再选取m个数据 { x ^ 1 , x ^ 2 , . . . , x ^ m } \{\hat {x}^1,\hat {x}^2,...,\hat {x}^m\} {x^1,x^2,...,x^m}

D的优化目标为
m a x   V ~ = 1 m ∑ i = 1 m l o g D ( c i , x i ) + 1 m ∑ i = 1 m l o g ( 1 − D ( c i , x ~ i ) ) + 1 m ∑ i = 1 m l o g ( 1 − D ( c i , 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)) max V~=m1i=1mlogD(ci,xi)+m1i=1mlog(1D(ci,x~i))+m1i=1mlog(1D(ci,x^i))
第三项表示描述和输出不匹配的情况

G的优化目标
m a x   V ~ = 1 m ∑ i = 1 m l o g ( D ( G ( c i , z i ) ) ) max\ \tilde{V}=\frac{1}{m}\sum_{i=1}^mlog(D(G(c^i,z^i))) max V~=m1i=1mlog(D(G(ci,zi)))
​ 在实现时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分布的方法,其中 γ ( x p , x q ) \gamma(x_p,x_q) γ(xp,xq)表示从p的第 x p x_p xp维移动多少到q的第 x q x_q xq维。定义平均距离:
B ( γ ) = ∑ x p , x q γ ( x p , x q ) ∣ ∣ x p − x q ∣ ∣ B(\gamma)=\sum_{x_p,x_q}\gamma(x_p,x_q)||x_p-x_q|| B(γ)=xp,xqγ(xp,xq)xpxq
则Wasserstein距离即最优方案的平均距离,其优化目标为:
V ( G , D ) = m a x D ∈ 1 − L i p s c h i t z { E x ∼ P d a t a [ D ( x ) ] − E x ∼ P G [ 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)]\} V(G,D)=D1Lipschitzmax{ExPdata[D(x)]ExPG[D(x)]}
其中Lipschitz函数是一个足够平滑的函数,其定义如下:
∣ ∣ f ( x 1 ) − f ( x 2 ) ∣ ∣ ≤ K ∣ ∣ x 1 − x 2 ∣ ∣ ||f(x_1)-f(x_2)||\leq K||x_1-x_2|| f(x1)f(x2)Kx1x2
K=1时即为1-Lipschitz函数。

在解优化问题时,常用的方法有Weight Clipping和WGAN-GP,其中WeightClipping和如名字描述,为权重增加上限和下限。而WGAN的优化目标如下:
V ( G , D ) = m a x D { E x ∼ P d a t a [ D ( x ) ] − E x ∼ P G [ D ( x ) ] } − λ E x ∼ P p e n a l t y [ ( ∣ ∣ ∇ D ∣ ∣ − 1 ) 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] V(G,D)=Dmax{ExPdata[D(x)]ExPG[D(x)]}λExPpenalty[(D1)2]
​ 在实作时:
D的优化目标为
m a x   V ~ = 1 m ∑ i = 1 m D ( x i ) − 1 m ∑ i = 1 m D ( x ~ i ) max\ \tilde{V}=\frac{1}{m}\sum_{i=1}^mD(x^i)-\frac{1}{m}\sum_{i=1}^mD(\tilde{x}^i) max V~=m1i=1mD(xi)m1i=1mD(x~i)
第三项表示描述和输出不匹配的情况

G的优化目标
m a x   V ~ = − 1 m ∑ i = 1 m D ( G ( z i ) ) max\ \tilde{V}=-\frac{1}{m}\sum_{i=1}^mD(G(z^i)) max V~=m1i=1mD(G(zi))
在计算梯度时加入Clipping或者WGAN-GP

3.1.5 EBGAN

​ EBGAN使用一个Autoencoder作为D,将Autoencoder的重构误差作为D的输出。因此D可以预先在数据集上训练,所以可以立刻就获得一个比较强的D。

1.2 实践

​ 本来想要根据李宏毅老师2018年提供的二次元人脸数据集训练一个CGAN的,不过实际训练效果并不是很好,所以退而求其次训练DCGAN。

下图是CGAN(输入为眼睛、头发颜色和长发/短发)迭代530轮时G的输出,之后无论如何优化都无法继续训练。

在这里插入图片描述

1.2.1 经验总结

训练的时候发现一些技巧:

  • 每一轮迭代时,当D或是G的输出正确率达到 p 0 p_0 p0就停止训练了,因为理论上 A c c D [ f a k e ] + A c c G = 1 Acc_D[fake]+Acc_G=1 AccD[fake]+AccG=1,如果一直训练下去可能会使得梯度变得很小,D和G就会失去平衡。我取的是 p 0 = A c c D [ f a k e ] = 0.999 p_0=Acc_D[fake]=0.999 p0=AccD[fake]=0.999
  • 之前训练CGAN时失败应该就是因为 p 0 p_0 p0取值过小,如果D或是G无法达到接近100%的正确率,实际上是比较失败的,D和G只会在高维空间中比较平滑的区域随机抖动。
  • DCGAN中D的生成网络初始映射的channel一定要多,不要吝惜内存,不然很容易D就偏向只对realdata或是fakedata的输入判断,另一个正确率为0。大概是因为要保持realdata和fakedata的输入准确率都很高,所以尽量避免卷积时的信息损失吧。
  • G比D难训练得多,我在训练时发现,G的参数要比D多几倍才能和D达到平衡,并且很容易准确率就变为0。
  • 为了防止出现D强G弱的现象,在计算D的损失函数时,不直接使用 { 0 , 1 } ​ \{0,1\}​ {0,1}标签,而是加入随机噪声。
  • 大部分模型学习速率0.0002最佳。
  • G的输出可以用tanh规范化到 [ − 1 , 1 ] [-1,1] [1,1]
  • D的网络中加入批规范化(BatchNormalize)效果非常好。
  • 在D中使用LeakyReLU。
  • 定时保存模型和log真的很重要。可以在必要时回退并调整训练参数
1.2.2 网路结构

其实GAN的网络结构都很简单

#for generator, input is a norm-distribution vector x[0...100]
class Generator(nn.Module):
	def __init__(self):
		super(Generator, self).__init__()
		self.fc = nn.Linear(80,4*4*512)
		self.remap = nn.Sequential(
			nn.ConvTranspose2d(512,256,4,stride=2,padding=1),
			nn.ReLU(),
			nn.BatchNorm2d(256),
			nn.ConvTranspose2d(256,128,4,stride=2,padding=1),
			nn.ReLU(),
			nn.BatchNorm2d(128),
			nn.ConvTranspose2d(128,64,4,stride=2,padding=1),
			nn.ReLU(),
			nn.BatchNorm2d(64),
			nn.ConvTranspose2d(64,3,4,stride=2,padding=1),
			)

	def forward(self, x):
		c = self.fc(x)
		c = c.view(c.shape[0],512,4,4)
		c = self.remap(c)
		c = nn.Tanh()(c)
		return c


#for discriminator, input is a 64*64 RGB-face
class Discriminator(nn.Module):
	def __init__(self):
		super(Discriminator, self).__init__()
		self.cmap = nn.Sequential(
			nn.Conv2d(3,64,4,stride=2,padding=1),
			nn.LeakyReLU(),
			nn.BatchNorm2d(64),
			nn.Conv2d(64,128,4,stride=2,padding=1),
			nn.LeakyReLU(),
			nn.BatchNorm2d(128),
			nn.Conv2d(128,256,4,stride=2,padding=1),
			nn.LeakyReLU(),
			nn.BatchNorm2d(256),
			nn.Conv2d(256,1,4,stride=1,padding=0),
			)
		self.conv = nn.Sequential(
			nn.Conv2d(65,16,3),
			nn.LeakyReLU(),
			nn.BatchNorm2d(16),
			nn.Conv2d(16,4,3),
			)
		self.fcm = nn.Sequential(
			nn.Linear(25,1),
			)
	def forward(self, x):
		x = self.cmap(x)
		x = x.view(x.shape[0],1*5*5)
		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):
			i = 0
			for face,tag in dataloader:
				face = face
				tag = tag
				face = face.cuda()
				tag = tag.cuda()
				i += 1
				Doptim.zero_grad()
				#train on real data
				drealout = discrimitor(face)
				#train on fake data
				gvec,gtag = GSampler(face.shape[0])
				gout = generator(gvec).detach()
				dfakeout = discrimitor(gout)
				#train on mislabel
				d_loss = DLossfun(drealout,dfakeout)
				dacc_r = (sum(drealout)/drealout.shape[0]).item()
				dacc_f = (1-sum(dfakeout)/dfakeout.shape[0]).item()
				if EarlyStop and dacc_f>1-0.0001:
					print('\tearlyD_epoch:{}/{}: \tAcc:{:.4f}, {:.4f}'.format(d_epoch, D_step, dacc_r,dacc_f))
					break
				d_loss.backward()
				Doptim.step()
			print('\tD_epoch:{}/{}: \tAcc:{:.4f}, {:.4f}'.format(d_epoch, D_step, dacc_r,dacc_f))
		#secondly, train G
		for g_epoch in range(G_step):
			i = 0
			for batch in range(int(round(datasize/batch_size))):
				i += 1
				Goptim.zero_grad()
				gvec,gtag = GSampler(batch_size)
				gout = generator(gvec)
				dout = discrimitor(gout)				
				g_loss = GLossfun(dout)
				gacc = sum(dout)/dout.shape[0]
				if EarlyStop and gacc.item()>1-0.0001:
					print('\tearlyG_epoch:{}/{}: GAcc:{:.4f}\t'.format(g_epoch, G_step, gacc.item()))
					break
				g_loss.backward()
				Goptim.step()
			print('\tG_epoch:{}/{}: GAcc:{:.4f}\t'.format(g_epoch, G_step, gacc.item()))
		if epoch%20==0 and epoch!=0:
			print('savepoint')
            #save params
		print('\nepoch:{}/{}'.format(start_epoch+epoch, start_epoch+epoch_num))

参数上G_step=D_step=1,因为使用EarlyStop之后,如果D已经达到精度要求会直接跳过本轮中D的训练,相当于G连续训练了多个epoch直到D不满足终止条件。

1.2.3 训练结果
训练过程

实际上训练尚未结束,记一下当前的进度吧,目前一共跑了大概两个多小时,不得不说卷积层真的非常省内存。

  • 刚开始的500轮比较稳定,不需要人为干预,D和G就可以维持平衡
  • 853轮时G失效,从853-980epoch准确率都是0,回退到840轮增加DLoss的随机性,重新开始训练。
  • 960-996epoch时D的准确率都是0,但是到了997突然变为1.0000,这次的对抗epoch较多,实际上回退具有一定风险。
  • 1220epoch时G输出为一张纯蓝色的图

平衡时的训练过程大概是这样的:

在这里插入图片描述

可以看到对抗还是很明显的,而且通常D很快就可以超过G,而G可能要D等待好多轮。

G的输出

下面的动图是从[80,180,260,280,320,420,500,640,700,760,800,840,900,960,1020,1100,1160]epoch的记录,G使用了同一个随机向量。

在这里插入图片描述

80-640中在我看来人脸的辨识度是逐渐提高的,640epoch以后不知道什么原因,G更改了优化的方向,图片直接从两只眼睛变成了一只眼睛,1020epoch时人脸有了三只眼睛,1100后又回到了两只眼睛。

如果G和D没有提前结束,每一轮epoch大概要5s左右,不过通常G或是D都会提前结束。

代码见github

未完待续…

  • 2
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值