版权声明:本文为原创文章,未经博主允许不得用于商业用途。
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=1∑mlogD(xi)+m1i=1∑mlog(1−D(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=1∑mlog(D(G(zi)))
\qquad
对于单纯的生成模型,一般很难考虑全局效果,比如之前训练的VAE,噪点出现的位置就对优化目标没有影响,而GAN得Discriminator则可以很容易得学到全局特征(比如使用卷积),因此往往效果更好。
\qquad 另外在实际训练时往往采用 − l o g D -logD −logD而不是 l o g ( 1 − D ) log(1-D) log(1−D),这是由于 − l o g ( 1 − D ) -log(1-D) −log(1−D)初期下降太慢,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) z∼N(μ,σ),输出 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=1∑mlogD(ci,xi)+m1i=1∑mlog(1−D(ci,x~i))+m1i=1∑mlog(1−D(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=1∑mlog(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)∣∣xp−xq∣∣
则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)=D∈1−Lipschitzmax{Ex∼Pdata[D(x)]−Ex∼PG[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)∣∣≤K∣∣x1−x2∣∣
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{Ex∼Pdata[D(x)]−Ex∼PG[D(x)]}−λEx∼Ppenalty[(∣∣∇D∣∣−1)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=1∑mD(xi)−m1i=1∑mD(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=1∑mD(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
未完待续…