3

GAN[02]-边界均衡生成对抗网络:BEGAN

 2 years ago
source link: https://yerfor.github.io/2020/02/19/gan-02/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

GAN[02]-边界均衡生成对抗网络:BEGAN

发表于

2020-02-19 更新于 2020-02-20

BEGAN (Boundary Equilibrium Generative Adversarial Networks)是2017年谷歌发表的框架,它在WGAN和EBGAN的基础上做出了改进。关于WGANEBGAN

  • WGAN的原理和实现在上一篇博文中有介绍,它通过修改Loss函数使Discriminator的训练目标从传统GAN的JS散度变成了描述两个分布之间迁移所需步数的Wasserstein 距离(也叫Earth Moving Distance,即挖掘机距离)

  • EBGAN(Energy-Based GAN)是Yann Le Cun在2016年发表的算法,论文号称是将Yann教授于2006年发表的Energy-Based Model应用在GAN中,不过实际上最大的贡献是网络结构的创新,即首次将Auto-Encoder用作Discriminator。

BEGAN出现之前,DCGAN使用了卷积结构来提高生成图像的质量;EBGAN引入了Auto-Encoder结构提高收敛的稳定性和模型的鲁棒性;WGAN引入Wasserstein距离提高了稳定性和更好的模式覆盖,但是收敛速度慢。

BEGAN借鉴了EBGANAuto-Encoder结构,同样采用了Wasserstein距离,但Loss函数WGAN更简洁。其独创之处在于:BEGAN第二个提供了可以衡量收敛情况的参数(第一个是WGANgradient penalty),提供了一个超参数$\gamma$可以用来均衡生成器和判别器训练程度,此外$\gamma$还可以用来权衡生成图像的品质和多样性。

上述的创新使得BEGAN成为当时性能最好的简单GAN结构,它是第二个可以生成清晰(128x128)图像的网络结构(第一个是2016年的stackedGAN,该结构是由多层GAN网络堆叠成的)。BEGAN提供了一个简单且鲁棒性更好的 GAN 架构,使用标准的训练步骤实现了快速、稳定的收敛,生成的图像质量更高。

2017年后,对GAN的研究重心从理论提升转变到应用层面,所以BEGAN的理论在今天或许可以说仍是十分先进的。

1.网络结构

1.1 生成器(Generator)

论文所述的网络结构如下,可以看到和现在主流的全卷积网络有一定出入,论文中采用的是2个strides=1kernel_size=(3,3)通道数n=16的卷积层为一组,每组内的特征图储存相等;每组间添加一个nearest(最近邻)策略的Upsampling(上采样)层,进行特征图的扩增。下图中的结构用于生成(32x32)的图像,若要输出(128x128),只需在下图结构的基础上再加两组卷积+Upsampling层。

gen

  • 上面图中有个坑值得注意,即图中的Embedding(h),笔者一开始以为是NLP中常用的Word Embedding,对应的是tensorflow中的tf.keras.layers.Embedding,但是这不符合数学规则,最后查阅了源码发现图中Embedding的输出是feature_code,对Generator来说就是服从normal分布的噪声,对Decoder来说,就是简单的全连接层,这一点在下一小节判别器的源码中会用注释标注出来。

使用Tensorflow2实现Generator的代码如下:

import tensorflow as tf

class Generator(tf.keras.models.Model):
def __init__(self,n=16):
super(Generator,self).__init__()
# fc+reshape [b, noise_length] -> [b, 8*8*n] -> [b, 8, 8, n]
self.fc = tf.keras.layers.Dense(8*8*n, actication='elu')
self.reshape = tf.keras.layers.Reshape([8,8,n])
self.conv1 = tf.keras.layers.Conv2D(filters=n,kernel_size=
(3,3),strides=1,activation='elu')
self.conv2 = tf.keras.layers.Conv2D(filters=n,kernel_size=
(3,3),strides=1,activation='elu')
# upsample [b, 8, 8, n] -> [b, 16, 16, n]
self.upsample1 = tf.keras.layers.Upsampling2D()
self.conv3 = tf.keras.layers.Conv2D(filters=n,kernel_size=
(3,3),strides=1,activation='elu')
self.conv4 = tf.keras.layers.Conv2D(filters=n,kernel_size=
(3,3),strides=1,activation='elu')
# upsample [b, 16, 16, n] -> [b, 32, 32, n]
self.upsample2 = tf.keras.layers.Upsampling2D()
self.conv5 = tf.keras.layers.Conv2D(filters=n,kernel_size=
(3,3),strides=1,activation='elu')
self.conv6 = tf.keras.layers.Conv2D(filters=n,kernel_size=
(3,3),strides=1,activation='elu')
# 输出归一化的图像矩阵,激活函数选择tanh
self.conv7 = tf.keras.layers.Conv2D(filters=n,kernel_size=
(3,3),strides=1,activation='tanh')
def call(self, inputs, training=None):
x = self.fc(inputs)
x = self.reshape(x)
x = self.conv1(x)
x = self.conv2(x)
x = self.upsample1(x)
x = self.conv3(x)
x = self.conv4(x)
x = self.upsample2(x)
x = self.conv5(x)
x = self.conv6(x)
x = self.conv7(x)
return x

1.2 判别器(Discriminator)

BEGAN的判别器网络采用Auto-Encoder结构,分为EncoderDecoder两部分,前者负责将图片编码成一维的特征向量,后者负责将特征向量解码复原成原本的图片。BEGAN采用的Decoder的结构和上一小节里的Generator一致,Encoder的结构则如下图所示。

encoder

值得注意的是上图中的Subsampling(下采样)也叫Pooling(池化),官方源码中最初使用的是最大池化。

使用Tensorflow2实现Discriminator的代码如下:

import tensorflow as tf

class Discriminator(tf.keras.models.Model):
def __init__(self,n,h):
# =======Encoder======
# conv0 [b, 32, 32, 3] -> [b, 32, 32, n]
self.conv0 = tf.keras.layers.Conv2D(filters=n,kernel_size=(3,3),
strides=1,activation='elu')
self.conv1 = tf.keras.layers.Conv2D(filters=n,kernel_size=(3,3),
strides=1,activation='elu')
# conv2 [b, 32, 32, n] -> [b, 32, 32, 2*n]
self.conv2 = tf.keras.layers.Conv2D(filters=2*n,kernel_size=(3,3),
strides=1,activation='elu')
# subsampling1 [b, 32, 32, 2*n] -> [b, 16, 16, 2*n]
self.subsampling1 = tf.keras.layers.MaxPooling()
self.conv3 = tf.keras.layers.Conv2D(filters=2*n,kernel_size=(3,3),
strides=1,activation='elu')
# conv4 [b, 16, 16, 2*n] -> [b, 16, 16, 3*n]
self.conv4 = tf.keras.layers.Conv2D(filters=3*n,kernel_size=(3,3),
strides=1,activation='elu')
# subsampling2 [b, 16, 16, 3*n] -> [b, 8, 8, 3*n]
self.subsampling1 = tf.keras.layers.MaxPooling()
self.conv5 = tf.keras.layers.Conv2D(filters=3*n,kernel_size=(3,3),
strides=1,activation='elu')
self.conv6 = tf.keras.layers.Conv2D(filters=3*n,kernel_size=(3,3),
strides=1,activation='elu')
# flatten [b, 8, 8, 3*n] -> [b, 8*8*3*n]
self.flatten = tf.keras.layers.Flatten()
# fc1 [b, 8*8*3*n] -> [b, h]
self.fc1 = tf.keras.layers.Dense(h)

# ======Decoder======
# fc2 + reshape [b, h] -> [b, 8*8*n] -> [b, 8, 8, n]
self.fc2 = tf.keras.layers.Dense(8*8*n)
self.reshape = tf.keras.layers.Reshape([8,8,n])
self.conv7 = tf.keras.layers.Conv2D(filters=n,kernel_size=
(3,3),strides=1,activation='elu')
self.conv8 = tf.keras.layers.Conv2D(filters=n,kernel_size=
(3,3),strides=1,activation='elu')
# upsample [b, 8, 8, n] -> [b, 16, 16, n]
self.upsample1 = tf.keras.layers.Upsampling2D()
self.conv9 = tf.keras.layers.Conv2D(filters=n,kernel_size=
(3,3),strides=1,activation='elu')
self.conv10 = tf.keras.layers.Conv2D(filters=n,kernel_size=
(3,3),strides=1,activation='elu')
# upsample [b, 16, 16, n] -> [b, 32, 32, n]
self.upsample2 = tf.keras.layers.Upsampling2D()
self.conv11 = tf.keras.layers.Conv2D(filters=n,kernel_size=
(3,3),strides=1,activation='elu')
self.conv12 = tf.keras.layers.Conv2D(filters=n,kernel_size=
(3,3),strides=1,activation='elu')
# 输出归一化的图像矩阵,激活函数选择tanh
self.conv13 = tf.keras.layers.Conv2D(filters=n,kernel_size=
(3,3),strides=1,activation='tanh')
def call(self,inputs,training=None):
x = self.conv0(inputs)
x = self.conv1(x)
x = self.conv2(x)
x = self.subsampling1(x)
x = self.conv3(x)
x = self.conv4(x)
x = self.subsampling2(x)
x = self.conv5(x)
x = self.conv6(x)
x = self.flatten(x)
x = self.fc1(x)
# fc1的输出就是长度为h的特征向量
x = self.fc2(x)
x = self.reshape(x)
x = self.conv7(x)
x = self.conv8(x)
x = self.upsample1(x)
x = self.conv9(x)
x = self.conv10(x)
x = self.upsample2(x)
x = self.conv11(x)
x = self.conv12(x)
x = self.conv13(x)
# 论文采用输出图像和输入图像之间误差的L1范数作为Discriminator的输出
recon_error = tf.reduce_mean(tf.abs(x-inputs))
return recon_error

1.3 官方源码中的网络结构更新

BEGAN官方的github仓库中可以看到,官方后来改良了卷积网络的结构,将MaxPooling改成了一个strides=2的卷积层,即基本单元(对应下图中的一次for循环)从2个卷积+1个池化层变成了3个卷积层。下面是官方源码修改处的截图。

dis_change

除此之外,官方没有在卷积层后加入BatchNormalization层,而根据其他BEGAN项目的经验,加入BN层可以加快模型收敛的时间。

最后,官方在生成器中用于扩增特征图的方法是最邻近的上采样Upsampling2D,该层内没有可训练参数。与之有类似功能的转置卷积层ConvTranspose2D则可以训练卷积核,可能效果会比Upsampling2D好,在WGAN项目中我们用的就是转置卷积。

关于转置卷积的理解,有一篇博客总结的很好。

2.基于Wasserstein距离的损失函数

2.1 Discriminator的输出

传统GAN意图用JS散度判别生成数据和原始数据的相似程度,WGAN意图用Wasserstein距离判别生成数据和原始数据的相似程度。大多数GAN的变种训练的目标都是直接比较生成数据和原始数据的距离,让这个距离越小越好。EBGANBEGAN引入了Auto-Encoder,从而提出了一个新的思路,转而衡量是Auto-Encoder的重构误差,让真实图像的重构误差和生成图像的重构误差越接近越好。

如下图是EBGANDiscriminator,输入的是一张图片,Auto-Encoder的网络结构会输出是一张相同尺寸的图片,我们要计算这两张图片之间的重构误差,EBGAN中是将两个矩阵做差值,然后求这个误差矩阵的二阶范数,BEGAN求的是一阶范数。

ebgan-dx

2.2 Wasserstein距离

由上一节我们知道,采用Auto-Encoder结构的Discriminator的输出为输入图像的重构误差,即:

其中,$\eta\in{1,2}$,$x$为图片,$Discriminator(x)$是Auto-Encoder重构的图像。

Wasserstein距离又被称作推土机距离(earth moving distance),因为它的定义就是将一个分布迁移到另一个分布上需要”搬运“的数据量。在BEGAN中,要比较的两个分布分别是真实图像的重构误差(recon_error)Generator生成图像的重构误差,这一场景下的Wasserstein距离计算式如下:

使用Jeson不等式,我们有:

其中$m_1,m_2$分别是$recon_{-}error(x_{true})$的期望和$recon_{-}{error}(x_{fake})$的期望,在实验中也就是大量的batch的平均值。

我们训练$Discrminator$的目标是让真实数据和生成数据的距离最大,要实现这点,只需$|m_1-m_2|$最大,只需$m_1\rightarrow\inf,m_2\rightarrow 0$。

因此,作者设计了如下的loss函数,只要使用该loss,训练时的度量就是Wasserstein距离

w-loss

loss函数的思路就是,Discriminator努力让真实图片的重建误差小,让生成图片的重建误差大;而Generator要努力让生成图片的重建误差小,骗过Discriminator

这里的loss函数看起来和WGANloss函数十分相似,都是去掉了交叉墒的log,直接用Discrminator的输出值。

3.均衡超参数$\gamma$

生成对抗网络最理想的状态是生成器和判别器达到纳什均衡,此时生成的样本不能被判别器与真实样本区分,二者的误差分布应该是相同的,即:

我们可以定义这样一个超参数$\gamma$控制这个平衡,限定这个参数的值域为$\gamma\in[0,1]$:

为什么$\gamma$总是小于1?这是因为生成器和判别器都是从头开始训练的,一开始判别器可能很难重构出真实图片这样复杂的图片,而生成样本一开始很简单的图片,导致生成样本从一开始的重构误差就比真实样本的重构误差要小,这个大小关系会一直持续到结束。

这个均衡超参数允许我们平衡分配给生成器和鉴别器的工作,如果$\gamma$设定得较小,会减少生成样本的多样性但会增加生成图像的质量,因为生成样本的$recon_{-}error$比较小,实验上$\gamma=0.4$左右生成图片效果较好,且样本较丰富,在0.7左右图片效果较差。

我在实验中发现,$\gamma$设置的过小(如0.25)会导致模型完全train不起来,这是因为实际BEGAN的loss在上一章的基础上做了改进,变得和实际的$\gamma$(即$\frac{E[recon_{-}error(x_{fake})]}{E[recon_{-}{error}(x_{true})]}$)有关,具体原因在下一章详细介绍。

4.满足$\gamma$比例的损失函数

BEGAN的损失函数定义如下:

began-loss

为了实现$\gamma$的功能,作者对第二章的loss函数做了如上改动,引入了$k_t$这一权重控制Discriminator训练时对减少真实图像重建误差和增加生成图像重建误差两个任务的侧重。

考虑$k_{t}$的定义式,如论文所言,实际上借鉴了部分PID控制中比例控制的思想,当实际$\gamma$等于设定值时,权重$k_t$不变化;而当$recon_{-}error(x_{fake})$相对于真实样本的重构误差太小时(这也是每次训练开始的必然状态),会导致$k_t$从初始值0开始慢慢变大,从而Discriminator在训练时会越来越侧重于增加生成样本的重构误差。这也是每次训练刚开始都要经历的过程,经验上看,由于一开始生成样本的重构误差都要比$\gamma$倍的真实样本重构误差要小,所以$k_t$一开始会从0缓慢增加,一般增加到0.04左右会开始下落,最终收敛在0.02左右。

在上一章中提到过如果$\gamma$设置得太小会导致模型train不起来,看着$k_t$的公式就很好理解了,如果$\gamma$太小,导致一开始时$\gamma L(x)-L(G(z_{G}))<0$,那么$k_t$就永远为0,也就是说Discriminator在训练时从不会考虑Generator,这对GAN的训练是致命的。

5.收敛衡量参数$M_{global}$

作者人为地设计了一个参数来衡量模型的收敛情况:

作者表示此度量可用于确定网络何时达到其最终状态或模型是否已经崩溃。

使用Tensorflow 2.0复现了BEGAN,batch_size=16,训练了20万个iterations,训练过程如下图所示:

BEGAN-demo

效果和官方有差距,原因可能有:

  • iterations不够

  • 训练集不够大,因为内存不足,只用了20000张图片,可以修改数据集对象为生成器模式,使用全部图片。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK