原文:
annas-archive.org/md5/946fd2e9c806f075bc9bc101ff92dd6c
译者:飞龙
第四章 生成对抗网络(GANs)
在本章中,我们将研究生成对抗网络(GANs)[1],这是我们将要研究的三种人工智能算法中的第一个。GAN 属于生成模型的范畴。然而,与自编码器不同,生成模型能够根据任意编码生成新的、有意义的输出。
本章将讨论 GAN 的工作原理。我们还将回顾 Keras 中几个早期 GAN 的实现。稍后,我们将展示实现稳定训练所需的技术。本章的范围涵盖了两种流行的 GAN 实现示例,深度卷积生成对抗网络(DCGAN)[2]和条件生成对抗网络(CGAN)[3]。
总结一下,本章的目标是:
-
介绍生成对抗网络(GAN)的原理
-
如何在 Keras 中实现 GAN,如 DCGAN 和 CGAN
GAN 概述
在深入研究 GAN 的更高级概念之前,我们先来回顾一下 GAN,并介绍它们的基本概念。GAN 非常强大;这一简单的说法通过它们能够生成虚拟名人面孔来证明这一点,这些面孔并非真实人物,而是通过进行潜空间插值生成的。
GAN 的高级特性[4]的一个绝佳示例可以通过这个 YouTube 视频(youtu.be/G06dEcZ-QTg
)看到。视频展示了如何利用 GAN 生成逼真的面孔,这显示了它们的强大功能。这个话题比我们在本书中之前讨论的任何内容都要复杂。例如,上述视频是自编码器无法轻易实现的,正如我们在第三章中讨论的,自编码器。
GAN 能够通过训练两个竞争(又互相合作)的网络来学习如何建模输入分布,这两个网络被称为生成器和判别器(有时也称为评论器)。生成器的角色是不断探索如何生成能够欺骗判别器的虚假数据或信号(这包括音频和图像)。与此同时,判别器被训练用来区分真假信号。随着训练的进行,判别器将无法再分辨合成数据与真实数据之间的差异。从这里开始,判别器可以被丢弃,而生成器可以用来创造前所未见的全新、逼真的信号。
GAN 的基本概念很简单。然而,我们会发现最具挑战性的问题是如何实现生成器-判别器网络的稳定训练?生成器和判别器必须进行健康的竞争,以便两个网络能够同时学习。由于损失函数是从判别器的输出计算的,其参数更新速度很快。当判别器收敛得更快时,生成器不再接收到足够的梯度更新以便其参数收敛。除了难以训练外,GAN 还可能遭受部分或完全的模态崩溃,即生成器对不同的潜在编码几乎产生相似的输出。
GAN 的原理
如图 4.1.1所示,GAN 类似于一个赝品制造者(生成器)和警察(判别器)的场景。在学院里,警察被教导如何判断一张美元是真是假。从银行得到的真钞样本和赝品制造者的假钱用来训练警察。然而,赝品制造者偶尔会试图假装他印了真钞。起初,警察不会上当,会告诉赝品制造者这些钱为什么是假的。考虑到这些反馈,赝品制造者再次磨练技能,试图制造新的假美元。预计警察将能够识破这些假币,并且证明为何这些美元是假的。
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_01.jpg
图 4.1.1:GAN 的生成器和判别器类似于赝品制造者和警察。赝品制造者的目标是欺骗警察认为这些美元是真的。
这种情况将无限期地继续下去,但最终会有一个时机,赝品制造者掌握了制作与真钞无法区分的假美元的技能。赝品制造者随后可以无限制地印刷美元,而不会被警察逮捕,因为这些假币已经无法被定义为伪造品。
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_02.jpg
图 4.1.2:GAN 由两个网络组成,生成器和判别器。判别器被训练来区分真实和假的信号或数据。生成器的任务是生成能最终愚弄判别器的假信号或数据。
如图 4.1.2所示,GAN 由两个网络组成,一个生成器和一个鉴别器。生成器的输入是噪声,输出是合成信号。与此同时,鉴别器的输入将是一个真实信号或合成信号。真实信号来自真实的采样数据,而伪造信号来自生成器。所有有效信号被标记为 1.0(即 100% 真实的概率),而所有合成信号被标记为 0.0(即 0% 真实的概率)。由于标记过程是自动化的,GAN 仍然被认为是深度学习中无监督学习方法的一部分。
鉴别器的目标是从提供的数据集中学习如何区分真实信号和伪造信号。在 GAN 训练的这一部分,只有鉴别器的参数会被更新。像典型的二元分类器一样,鉴别器被训练以预测输入信号与真实信号之间的相似度,输出值在 0.0 到 1.0 的范围内表示其信心值。然而,这只是故事的一半。
在规律的间隔下,生成器将假装它的输出是一个真实信号,并要求 GAN 将其标记为 1.0。当伪造信号呈现给鉴别器时,它自然会被分类为伪造,并被标记为接近 0.0。优化器根据所呈现的标签(即 1.0)计算生成器参数的更新。它还会在训练这个新数据时考虑自身的预测。换句话说,鉴别器对其预测有一定的怀疑,因此,GAN 会考虑到这一点。这时,GAN 会让梯度从鉴别器的最后一层反向传播到生成器的第一层。然而,在大多数实践中,在这个训练阶段,鉴别器的参数通常是冻结的。生成器将使用这些梯度来更新其参数,并提高其合成伪造信号的能力。
总体而言,这个过程类似于两个网络彼此竞争,同时又在某种程度上相互合作。当 GAN 训练收敛时,最终结果是一个可以合成信号的生成器。鉴别器认为这些合成的信号是真的,或者它们的标签接近 1.0,这意味着鉴别器可以被丢弃。生成器部分将在从任意噪声输入中产生有意义的输出时发挥作用。
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_03.jpg
图 4.1.3:训练鉴别器类似于使用二元交叉熵损失训练一个二分类网络。伪造数据由生成器提供,真实数据来自真实样本。
如前图所示,鉴别器可以通过最小化以下方程中的损失函数来进行训练:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_001.jpg (方程 4.1.1)
该方程就是标准的二元交叉熵损失函数。损失是正确识别真实数据的期望值的负和,https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_002.jpg,以及正确识别合成数据的 1.0 减去期望值,https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_003.jpg。对数不会改变局部最小值的位置。训练时,判别器会提供两小批次数据:
-
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_004.jpg,来自采样数据的真实数据(即,https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_005.jpg),标签为 1.0
-
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_006.jpg,来自生成器的假数据,标签为 0.0
为了最小化损失函数,判别器的参数,https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_007.jpg,将通过反向传播来更新,通过正确识别真实数据,https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_008.jpg,以及合成数据,https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_009.jpg。正确识别真实数据等同于https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_010.jpg,而正确分类假数据则等同于https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_011.jpg 或者https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_012.jpg。在这个方程中,https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_013.jpg 是生成器用来合成新信号的任意编码或噪声向量。两者共同作用于最小化损失函数。
为了训练生成器,GAN 将判别器和生成器的损失视为零和博弈。生成器的损失函数只是判别器损失函数的负值:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_014.jpg
(公式 4.1.2)
这可以更恰当地重新写成一个价值函数:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_015.jpg
(公式 4.1.3)
从生成器的角度来看,公式 4.1.3 应该被最小化。从判别器的角度来看,价值函数应该被最大化。因此,生成器的训练准则可以写成一个最小最大问题:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_016.jpg
(公式 4.1.4)
有时,我们会通过伪装合成数据为真实数据(标签为 1.0)来试图欺骗判别器。通过相对于https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_017.jpg的最大化,优化器向判别器参数发送梯度更新,使其将该合成数据视为真实数据。与此同时,通过相对于https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_018.jpg的最小化,优化器将训练生成器的参数,教其如何欺骗判别器。然而,实际上,判别器在将合成数据分类为假数据时非常自信,因此不会更新其参数。此外,梯度更新很小,并且在传播到生成器层时已经大大减弱。因此,生成器未能收敛:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_04.jpg
图 4.1.4:训练生成器就像使用二元交叉熵损失函数训练一个网络。生成器产生的假数据被当作真实数据展示。
解决方案是将生成器的损失函数重新构建为以下形式:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_019.jpg
(方程式 4.1.5)
损失函数的作用是通过训练生成器来最大化判别器将合成数据认为是现实数据的可能性。新的公式不再是零和的,而是纯粹基于启发式的驱动。图 4.1.4 展示了训练中的生成器。在这个图中,生成器的参数仅在整个对抗网络训练时更新。这是因为梯度从判别器传递给生成器。然而,实际上,在对抗训练过程中,判别器的权重只是暂时被冻结。
在深度学习中,生成器和判别器都可以使用合适的神经网络架构来实现。如果数据或信号是图像,生成器和判别器网络将使用 CNN。对于像自然语言处理(NLP)中的一维序列,两个网络通常是递归网络(RNN、LSTM 或 GRU)。
Keras 中的 GAN 实现
在前一部分中,我们了解到 GAN 的原理是直观的。我们还学会了如何通过熟悉的网络层,如 CNN 和 RNN,来实现 GAN。使 GAN 与其他网络不同的是,它们通常很难训练。即便是简单的层变化,也可能导致网络训练的不稳定。
在这一部分中,我们将研究使用深度 CNN 实现的早期成功的 GAN 实现之一。它被称为 DCGAN [3]。
图 4.2.1 显示了用于生成假 MNIST 图像的 DCGAN。DCGAN 推荐以下设计原则:
-
使用步幅 > 1 的卷积,而不是
MaxPooling2D
或UpSampling2D
。通过步幅 > 1,卷积神经网络(CNN)学会了如何调整特征图的大小。 -
避免使用
Dense
层。在所有层中使用 CNN。Dense
层仅用作生成器的第一层,用于接受z向量。Dense
层的输出经过调整大小后,成为后续 CNN 层的输入。 -
使用批量归一化(BN)来通过标准化每一层的输入,使其均值为零,方差为单位,从而稳定学习。在生成器的输出层和判别器的输入层中不使用 BN。在这里展示的实现示例中,判别器中不使用批量归一化。
-
修正线性单元(ReLU)在生成器的所有层中使用,除了输出层,输出层使用tanh激活函数。在这里展示的实现示例中,输出层使用sigmoid而不是tanh,因为它通常能使 MNIST 数字的训练更加稳定。
-
在鉴别器的所有层中使用 Leaky ReLU。与 ReLU 不同,当输入小于零时,Leaky ReLU 不会将所有输出置为零,而是生成一个小的梯度,等于 alpha × input。在以下示例中,alpha = 0.2。
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_05.jpg
图 4.2.1:一个 DCGAN 模型
生成器学习从 100 维输入向量生成假图像([-1.0, 1.0] 范围的 100 维均匀分布随机噪声)。鉴别器将真实图像与假图像区分开,但在对抗网络训练过程中,无意中指导生成器如何生成真实图像。我们在 DCGAN 实现中使用的卷积核大小为 5,目的是增加卷积的覆盖范围和表达能力。
生成器接受由均匀分布生成的 100 维 z 向量,范围为 -1.0 到 1.0。生成器的第一层是一个 7 × 7 × 128 = 6,272 个 单元 的 Dense
层。单元的数量是根据输出图像的最终尺寸(28 × 28 × 1,28 是 7 的倍数)以及第一层 Conv2DTranspose
的滤波器数量(等于 128)来计算的。我们可以将反卷积神经网络(Conv2DTranspose
)看作是卷积神经网络(CNN)过程的反向过程。简单地说,如果 CNN 将图像转化为特征图,则反卷积 CNN 会根据特征图生成图像。因此,反卷积 CNN 在上一章的解码器和本章的生成器中都得到了应用。
在经过两次 Conv2DTranspose
(strides = 2
)处理后,特征图的大小将为 28 × 28 × 滤波器数量。每个 Conv2DTranspose
前都进行批归一化和 ReLU 激活。最终层使用 sigmoid 激活函数,生成 28 × 28 × 1 的假 MNIST 图像。每个像素被归一化到 [0.0, 1.0],对应于 [0, 255] 的灰度级别。以下列表显示了在 Keras 中实现生成器网络的代码。我们定义了一个函数来构建生成器模型。由于整个代码较长,我们将仅列出与讨论相关的部分。
注意
完整代码可在 GitHub 上获取:github.com/PacktPublishing/Advanced-Deep-
Learning-with-Keras。
列表 4.2.1,dcgan-mnist-4.2.1.py
向我们展示了 DCGAN 的生成器网络构建函数:
def build_generator(inputs, image_size):
"""Build a Generator Model
Stack of BN-ReLU-Conv2DTranpose to generate fake images.
Output activation is sigmoid instead of tanh in [1].
Sigmoid converges easily.
# Arguments
inputs (Layer): Input layer of the generator (the z-vector)
image_size: Target size of one side (assuming square image)
# Returns
Model: Generator Model
"""
image_resize = image_size // 4
# network parameters
kernel_size = 5
layer_filters = [128, 64, 32, 1]
x = Dense(image_resize * image_resize * layer_filters[0])(inputs)
x = Reshape((image_resize, image_resize, layer_filters[0]))(x)
for filters in layer_filters:
# first two convolution layers use strides = 2
# the last two use strides = 1
if filters > layer_filters[-2]:
strides = 2
else:
strides = 1
x = BatchNormalization()(x)
x = Activation('relu')(x)
x = Conv2DTranspose(filters=filters,
kernel_size=kernel_size,
strides=strides,
padding='same')(x)
x = Activation('sigmoid')(x)
generator = Model(inputs, x, name='generator')
return generator
判别器与许多基于 CNN 的分类器类似。输入是一个 28 × 28 × 1 的 MNIST 图像,分类为真实(1.0)或虚假(0.0)。共有四个 CNN 层。除了最后一个卷积层外,每个Conv2D
都使用strides = 2
来对特征图进行下采样。每个Conv2D
层后面都接一个 Leaky ReLU 层。最终的滤波器大小是 256,而初始滤波器大小是 32,并且每个卷积层的滤波器大小都会翻倍。最终的滤波器大小为 128 也能工作,但我们会发现,使用 256 时生成的图像效果更好。最终输出层会将特征图展平,并通过一个单元的Dense
层在经过 sigmoid 激活层缩放后输出介于 0.0 到 1.0 之间的预测值。输出被建模为伯努利分布。因此,使用二元交叉熵损失函数。
在构建生成器和判别器模型后,通过连接生成器和判别器网络来构建对抗模型。判别器和对抗网络都使用 RMSprop 优化器。判别器的学习率为 2e-4,而对抗网络的学习率为 1e-4。应用 RMSprop 衰减率,判别器为 6e-8,对抗网络为 3e-8。将对抗网络的学习率设置为判别器的一半将使训练更加稳定。我们从图 4.1.3和4.1.4中回忆到,GAN 训练有两个部分:判别器训练和生成器训练,这是对抗训练,期间冻结判别器权重。
清单 4.2.2展示了在 Keras 中实现判别器的代码。定义了一个函数来构建判别器模型。在清单 4.2.3中,我们将展示如何构建 GAN 模型。首先构建判别器模型,然后是生成器模型的实例化。对抗模型只是将生成器和判别器组合在一起。许多 GAN 模型中,批处理大小 64 似乎是最常见的。网络参数显示在清单 4.2.3中。
如清单 4.2.1和4.2.2所示,DCGAN 模型是直观的。构建它的难点在于网络设计中的微小变化会轻易导致训练无法收敛。例如,如果在判别器中使用了批归一化,或者将生成器中的strides = 2
转移到后面的 CNN 层,DCGAN 将无法收敛。
清单 4.2.2,dcgan-mnist-4.2.1.py
展示了我们 DCGAN 判别器网络构建函数:
def build_discriminator(inputs):
"""Build a Discriminator Model
Stack of LeakyReLU-Conv2D to discriminate real from fake.
The network does not converge with BN so it is not used here
unlike in [1] or original paper.
# Arguments
inputs (Layer): Input layer of the discriminator (the image)
# Returns
Model: Discriminator Model
"""
kernel_size = 5
layer_filters = [32, 64, 128, 256]
x = inputs
for filters in layer_filters:
# first 3 convolution layers use strides = 2
# last one uses strides = 1
if filters == layer_filters[-1]:
strides = 1
else:
strides = 2
x = LeakyReLU(alpha=0.2)(x)
x = Conv2D(filters=filters,
kernel_size=kernel_size,
strides=strides,
padding='same')(x)
x = Flatten()(x)
x = Dense(1)(x)
x = Activation('sigmoid')(x)
discriminator = Model(inputs, x, name='discriminator')
return discriminator
清单 4.2.3,dcgan-mnist-4.2.1.py
:构建 DCGAN 模型并调用训练程序的函数:
def build_and_train_models():
# load MNIST dataset
(x_train, _), (_, _) = mnist.load_data()
# reshape data for CNN as (28, 28, 1) and normalize
image_size = x_train.shape[1]
x_train = np.reshape(x_train, [-1, image_size, image_size, 1])
x_train = x_train.astype('float32') / 255
model_name = "dcgan_mnist"
# network parameters
# the latent or z vector is 100-dim
latent_size = 100
batch_size = 64
train_steps = 40000
lr = 2e-4
decay = 6e-8
input_shape = (image_size, image_size, 1)
# build discriminator model
inputs = Input(shape=input_shape, name='discriminator_input')
discriminator = build_discriminator(inputs)
# [1] or original paper uses Adam,
# but discriminator converges easily with RMSprop
optimizer = RMSprop(lr=lr, decay=decay)
discriminator.compile(loss='binary_crossentropy',
optimizer=optimizer,
metrics=['accuracy'])
discriminator.summary()
# build generator model
input_shape = (latent_size, )
inputs = Input(shape=input_shape, name='z_input')
generator = build_generator(inputs, image_size)
generator.summary()
# build adversarial model
optimizer = RMSprop(lr=lr * 0.5, decay=decay * 0.5)
# freeze the weights of discriminator
# during adversarial training
discriminator.trainable = False
# adversarial = generator + discriminator
adversarial = Model(inputs,
discriminator(generator(inputs)),
name=model_name)
adversarial.compile(loss='binary_crossentropy',
optimizer=optimizer,
metrics=['accuracy'])
adversarial.summary()
# train discriminator and adversarial networks
models = (generator, discriminator, adversarial)
params = (batch_size, latent_size, train_steps, model_name)
train(models, x_train, params)
清单 4.2.4 显示了专门用于训练判别器和对抗网络的函数。由于自定义训练,通常的 fit()
函数不会被使用。相反,调用 train_on_batch()
来对给定的数据批次进行单次梯度更新。接着,通过对抗网络来训练生成器。训练过程首先从数据集中随机选择一批真实图像,并将其标记为真实(1.0)。然后,生成器生成一批假图像,并将其标记为假(0.0)。这两批图像被连接在一起,用于训练判别器。
完成此步骤后,生成器将生成新的假图像,并标记为真实(1.0)。这一批图像将用于训练对抗网络。两个网络会交替训练约 40,000 步。在规律的间隔中,基于某个噪声向量生成的 MNIST 数字会保存在文件系统中。在最后的训练步骤中,网络已经收敛。生成器模型也会保存在文件中,以便我们可以轻松地重用已训练的模型来生成未来的 MNIST 数字。然而,只有生成器模型会被保存,因为它才是 GAN 在生成新 MNIST 数字时的有用部分。例如,我们可以通过执行以下操作生成新的随机 MNIST 数字:
python3 dcgan-mnist-4.2.1.py --generator=dcgan_mnist.h5
清单 4.2.4,dcgan-mnist-4.2.1.py
显示了用于训练判别器和对抗网络的函数:
def train(models, x_train, params):
"""Train the Discriminator and Adversarial Networks
Alternately train Discriminaor and Adversarial networks by batch.
Discriminator is trained first with properly real and fake images.
Adversarial is trained next with fake images pretending to be real
Generate sample images per save_interval.
# Arguments
models (list): Generator, Discriminator, Adversarial models
x_train (tensor): Train images
params (list) : Networks parameters
"""
# the GAN models
generator, discriminator, adversarial = models
# network parameters
batch_size, latent_size, train_steps, model_name = params
# the generator image is saved every 500 steps
save_interval = 500
# noise vector to see how the generator output evolves
# during training
noise_input = np.random.uniform(-1.0, 1.0, size=[16, latent_size])
# number of elements in train dataset
train_size = x_train.shape[0]
for i in range(train_steps):
# train the discriminator for 1 batch
# 1 batch of real (label=1.0) and fake images (label=0.0)
# randomly pick real images from dataset
rand_indexes = np.random.randint(0, train_size, size=batch_size)
real_images = x_train[rand_indexes]
# generate fake images from noise using generator
# generate noise using uniform distribution
noise = np.random.uniform(-1.0, 1.0, size=[batch_size, latent_size])
# generate fake images
fake_images = generator.predict(noise)
# real + fake images = 1 batch of train data
x = np.concatenate((real_images, fake_images))
# label real and fake images
# real images label is 1.0
y = np.ones([2 * batch_size, 1])
# fake images label is 0.0
y[batch_size:, :] = 0.0
# train discriminator network, log the loss and accuracy
loss, acc = discriminator.train_on_batch(x, y)
log = "%d: [discriminator loss: %f, acc: %f]" % (i, loss, acc)
# train the adversarial network for 1 batch
# 1 batch of fake images with label=1.0
# since the discriminator weights are frozen in adversarial network
# only the generator is trained
# generate noise using uniform distribution
noise = np.random.uniform(-1.0, 1.0, size=[batch_size, latent_size])
# label fake images as real or 1.0
y = np.ones([batch_size, 1])
# train the adversarial network
# note that unlike in discriminator training,
# we do not save the fake images in a variable
# the fake images go to the discriminator input of the adversarial
# for classification
# log the loss and accuracy
loss, acc = adversarial.train_on_batch(noise, y)
log = "%s [adversarial loss: %f, acc: %f]" % (log, loss, acc)
print(log)
if (i + 1) % save_interval == 0:
if (i + 1) == train_steps:
show = True
else:
show = False
# plot generator images on a periodic basis
plot_images(generator,
noise_input=noise_input,
show=show,
step=(i + 1),
model_name=model_name)
# save the model after training the generator
# the trained generator can be reloaded for future MNIST digit generation
generator.save(model_name + ".h5")
图 4.2.1 展示了生成器生成的假图像随着训练步骤的变化而演化的过程。在 5000 步时,生成器已经开始生成可识别的图像。这就像拥有一个能够画数字的代理一样。值得注意的是,一些数字会从一种可识别的形式(例如,最后一行第二列的 8)变换成另一种形式(例如,0)。当训练收敛时,判别器损失接近 0.5,而对抗损失接近 1.0,如下所示:
39997: [discriminator loss: 0.423329, acc: 0.796875] [adversarial loss: 0.819355, acc: 0.484375]
39998: [discriminator loss: 0.471747, acc: 0.773438] [adversarial loss: 1.570030, acc: 0.203125]
39999: [discriminator loss: 0.532917, acc: 0.742188] [adversarial loss: 0.824350, acc: 0.453125]
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_06.jpg
图 4.2.2:不同训练步骤下 DCGAN 生成器生成的假图像
条件生成对抗网络
在上一节中,DCGAN 生成的假图像是随机的。生成器无法控制生成哪些特定的数字,也没有机制可以从生成器中请求某个特定的数字。这个问题可以通过一种叫做条件生成对抗网络(CGAN)的 GAN 变种来解决[4]。
使用相同的 GAN,对生成器和判别器的输入都施加一个条件。这个条件是数字的 one-hot 向量形式。这与要生成的图像(生成器)或是否被分类为真实或假(判别器)相关联。CGAN 模型如图 4.3.1所示。
CGAN 类似于 DCGAN,唯一的区别是添加了独热向量输入。对于生成器,独热标签在进入 Dense
层之前与潜在向量连接。对于判别器,添加了一个新的 Dense
层。这个新层用于处理独热向量并将其重塑,以便它能够与后续 CNN 层的其他输入进行拼接:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_08.jpg
图 4.3.1:CGAN 模型与 DCGAN 类似,唯一不同的是使用独热向量来对生成器和判别器的输出进行条件化。
生成器学会从一个 100 维的输入向量和一个指定的数字生成假图像。判别器根据真实和假图像及其相应的标签来分类真假图像。
CGAN 的基础仍然与原始 GAN 原理相同,唯一的区别是判别器和生成器的输入是以独热标签 y 为条件的。通过将此条件结合到 方程式 4.1.1 和 4.1.5 中,判别器和生成器的损失函数分别如 方程式 4.3.1 和 4.3.2 所示。
根据 图 4.3.2,更合适的写法是将损失函数写作:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_021.jpg
和
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_022.jpg
.
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_023.jpg
(方程式 4.3.1)
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_025.jpg
(方程式 4.3.2)
判别器的新损失函数旨在最小化预测来自数据集的真实图像与来自生成器的假图像的错误,给定它们的独热标签。图 4.3.2 显示了如何训练判别器。
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_09.jpg
图 4.3.2:训练 CGAN 判别器与训练 GAN 判别器类似。唯一的区别是生成的假图像和数据集的真实图像都使用它们相应的独热标签进行条件化。
生成器的新损失函数最小化判别器对根据指定的独热标签条件化的假图像的正确预测。生成器学会生成特定的 MNIST 数字,给定其独热向量,可以欺骗判别器。以下图像展示了如何训练生成器:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_07.jpg
图 4.3.3:通过对抗网络训练 CGAN 生成器与训练 GAN 生成器类似。唯一的区别是生成的假图像是通过独热标签进行条件化的。
以下清单突出显示了判别器模型中所需的小修改。代码使用 Dense
层处理独热向量,并将其与图像输入连接。Model
实例被修改以适应图像和独热向量输入。
清单 4.3.1,cgan-mnist-4.3.1.py
向我们展示了 CGAN 判别器。高亮部分显示了 DCGAN 中做出的更改。
def build_discriminator(inputs, y_labels, image_size):
"""Build a Discriminator Model
Inputs are concatenated after Dense layer.
Stack of LeakyReLU-Conv2D to discriminate real from fake.
The network does not converge with BN so it is not used here
unlike in DCGAN paper.
# Arguments
inputs (Layer): Input layer of the discriminator (the image)
y_labels (Layer): Input layer for one-hot vector to condition
the inputs
image_size: Target size of one side (assuming square image)
# Returns
Model: Discriminator Model
"""
kernel_size = 5
layer_filters = [32, 64, 128, 256]
x = inputs
y = Dense(image_size * image_size)(y_labels)
y = Reshape((image_size, image_size, 1))(y)
x = concatenate([x, y])
for filters in layer_filters:
# first 3 convolution layers use strides = 2
# last one uses strides = 1
if filters == layer_filters[-1]:
strides = 1
else:
strides = 2
x = LeakyReLU(alpha=0.2)(x)
x = Conv2D(filters=filters,
kernel_size=kernel_size,
strides=strides,
padding='same')(x)
x = Flatten()(x)
x = Dense(1)(x)
x = Activation('sigmoid')(x)
# input is conditioned by y_labels
discriminator = Model([inputs, y_labels],
x,
name='discriminator')
return discriminator
以下列表突出显示了为了在生成器构建函数中加入条件化 one-hot 标签而做的代码更改。Model
实例已针对z-向量和 one-hot 向量输入进行了修改。
列表 4.3.2,cgan-mnist-4.3.1.py
显示了 CGAN 生成器。突出显示了 DCGAN 中所做的更改:
def build_generator(inputs, y_labels, image_size):
"""Build a Generator Model
Inputs are concatenated before Dense layer.
Stack of BN-ReLU-Conv2DTranpose to generate fake images.
Output activation is sigmoid instead of tanh in orig DCGAN.
Sigmoid converges easily.
# Arguments
inputs (Layer): Input layer of the generator (the z-vector)
y_labels (Layer): Input layer for one-hot vector to condition
the inputs
image_size: Target size of one side (assuming square image)
# Returns
Model: Generator Model
"""
image_resize = image_size // 4
# network parameters
kernel_size = 5
layer_filters = [128, 64, 32, 1]
x = concatenate([inputs, y_labels], axis=1)
x = Dense(image_resize * image_resize * layer_filters[0])(x)
x = Reshape((image_resize, image_resize, layer_filters[0]))(x)
for filters in layer_filters:
# first two convolution layers use strides = 2
# the last two use strides = 1
if filters > layer_filters[-2]:
strides = 2
else:
strides = 1
x = BatchNormalization()(x)
x = Activation('relu')(x)
x = Conv2DTranspose(filters=filters,
kernel_size=kernel_size,
strides=strides,
padding='same')(x)
x = Activation('sigmoid')(x)
# input is conditioned by y_labels
generator = Model([inputs, y_labels], x, name='generator')
return generator
列表 4.3.3 突出显示了为适应判别器和生成器的条件化 one-hot 向量而对train()
函数所做的更改。首先,CGAN 判别器使用一批真实数据和假数据进行训练,数据根据它们各自的 one-hot 标签进行条件化。然后,通过训练对抗网络,更新生成器的参数,给定条件化的假数据,假数据伪装成真实数据。与 DCGAN 类似,在对抗训练过程中,判别器的权重被冻结。
列表 4.3.3,cgan-mnist-4.3.1.py
显示了 CGAN 训练过程。突出显示了 DCGAN 中所做的更改:
def train(models, data, params):
"""Train the Discriminator and Adversarial Networks
Alternately train Discriminator and Adversarial networks by batch.
Discriminator is trained first with properly labelled real and fake images.
Adversarial is trained next with fake images pretending to be real.
Discriminator inputs are conditioned by train labels for real images,
and random labels for fake images.
Adversarial inputs are conditioned by random labels.
Generate sample images per save_interval.
# Arguments
models (list): Generator, Discriminator, Adversarial models
data (list): x_train, y_train data
params (list): Network parameters
"""
# the GAN models
generator, discriminator, adversarial = models
# images and labels
x_train, y_train = data
# network parameters
batch_size, latent_size, train_steps, num_labels, model_name = params
# the generator image is saved every 500 steps
save_interval = 500
# noise vector to see how the generator output evolves during training
noise_input = np.random.uniform(-1.0, 1.0, size=[16, latent_size])
# one-hot label the noise will be conditioned to
noise_class = np.eye(num_labels)[np.arange(0, 16) % num_labels]
# number of elements in train dataset
train_size = x_train.shape[0]
print(model_name,
"Labels for generated images: ",
np.argmax(noise_class, axis=1))
for i in range(train_steps):
# train the discriminator for 1 batch
# 1 batch of real (label=1.0) and fake images (label=0.0)
# randomly pick real images from dataset
rand_indexes = np.random.randint(0, train_size, size=batch_size)
real_images = x_train[rand_indexes]
# corresponding one-hot labels of real images
real_labels = y_train[rand_indexes]
# generate fake images from noise using generator
# generate noise using uniform distribution
noise = np.random.uniform(-1.0, 1.0, size=[batch_size, latent_size])
# assign random one-hot labels
fake_labels = np.eye(num_labels)[np.random.choice(num_labels,
batch_size)]
# generate fake images conditioned on fake labels
fake_images = generator.predict([noise, fake_labels])
# real + fake images = 1 batch of train data
x = np.concatenate((real_images, fake_images))
# real + fake one-hot labels = 1 batch of train one-hot labels
y_labels = np.concatenate((real_labels, fake_labels))
# label real and fake images
# real images label is 1.0
y = np.ones([2 * batch_size, 1])
# fake images label is 0.0
y[batch_size:, :] = 0.0
# train discriminator network, log the loss and accuracy
loss, acc = discriminator.train_on_batch([x, y_labels], y)
log = "%d: [discriminator loss: %f, acc: %f]" % (i, loss, acc)
# train the adversarial network for 1 batch
# 1 batch of fake images conditioned on fake 1-hot labels w/ label=1.0
# since the discriminator weights are frozen in adversarial network
# only the generator is trained
# generate noise using uniform distribution
noise = np.random.uniform(-1.0, 1.0, size=[batch_size, latent_size])
# assign random one-hot labels
fake_labels = np.eye(num_labels)[np.random.choice(num_labels,batch_size)]
# label fake images as real or 1.0
y = np.ones([batch_size, 1])
# train the adversarial network
# note that unlike in discriminator training,
# we do not save the fake images in a variable
# the fake images go to the discriminator input of the adversarial
# for classification
# log the loss and accuracy
loss, acc = adversarial.train_on_batch([noise, fake_labels], y)
log = "%s [adversarial loss: %f, acc: %f]" % (log, loss, acc)
print(log)
if (i + 1) % save_interval == 0:
if (i + 1) == train_steps:
show = True
else:
show = False
# plot generator images on a periodic basis
plot_images(generator,
noise_input=noise_input,
noise_class=noise_class,
show=show,
step=(i + 1),
model_name=model_name)
# save the model after training the generator
# the trained generator can be reloaded for
# future MNIST digit generation
generator.save(model_name + ".h5")
图 4.3.4 显示了当生成器被设置为生成带有以下标签的数字时,生成的 MNIST 数字的演变:
[0 1 2 3
4 5 6 7
8 9 0 1
2 3 4 5]
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_04_10.jpg
图 4.3.4:在不同训练步骤中,当使用标签[0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5]作为条件时,CGAN 生成的假图像
鼓励你运行训练好的生成器模型,以查看新的合成 MNIST 数字图像:
python3 cgan-mnist-4.3.1.py --generator=cgan_mnist.h5
或者,也可以请求生成一个特定的数字(例如,8):
cgan-mnist-4.3.1.py --generator=cgan_mnist.h5 --digit=8
使用 CGAN 就像拥有一个代理,我们可以要求它绘制类似人类书写的数字。CGAN 相较于 DCGAN 的主要优势在于,我们可以指定代理绘制哪个数字。
结论
本章讨论了生成对抗网络(GANs)背后的基本原理,为我们接下来的更高级话题奠定了基础,这些话题包括改进型 GAN、解耦表示 GAN 和跨域 GAN。我们从了解 GAN 是由两个网络组成——生成器和判别器开始。判别器的作用是区分真实信号和假信号。生成器的目标是欺骗判别器。生成器通常与判别器结合形成一个对抗网络。通过训练对抗网络,生成器学会如何生成能够欺骗判别器的假信号。
我们还了解到,虽然 GANs 很容易构建,但训练起来极为困难。文中展示了两个 Keras 中的示例实现。DCGAN 展示了如何训练 GAN 生成假图像,这些假图像是 MNIST 数字。然而,DCGAN 生成器无法控制应生成哪个具体数字。CGAN 通过将生成器条件化为绘制特定数字来解决这个问题。条件是以 one-hot 标签的形式出现的。如果我们想构建一个能够生成特定类别数据的代理,CGAN 是非常有用的。
在下一章中,将介绍 DCGAN 和 CGAN 的改进。特别关注如何稳定 DCGAN 的训练以及如何提高 CGAN 的感知质量。这将通过引入新的损失函数和稍微不同的模型架构来实现。
参考文献
-
Ian Goodfellow。NIPS 2016 教程:生成对抗网络。arXiv 预印本 arXiv:1701.00160,2016 (
arxiv.org/pdf/1701.00160.pdf
). -
Alec Radford、Luke Metz 和 Soumith Chintala。深度卷积生成对抗网络的无监督表示学习。arXiv 预印本 arXiv:1511.06434,2015 (
arxiv.org/pdf/1511.06434.pdf
). -
Mehdi Mirza 和 Simon Osindero。条件生成对抗网络。arXiv 预印本 arXiv:1411.1784,2014 (
arxiv.org/pdf/1411.1784.pdf
). -
Tero Karras 等人。渐进增长的生成对抗网络:提高质量、稳定性和变化性。ICLR,2018 (
arxiv.org/pdf/1710.10196.pdf
).
第五章:改进的 GAN
自从 2014 年生成对抗网络(GAN)[1]被提出以来,它的普及速度迅速增加。GAN 已被证明是一种有效的生成模型,能够合成看起来真实的新数据。随后的许多深度学习研究论文都提出了应对原始 GAN 困难和局限性的措施。
如我们在前几章所讨论的那样,GAN 训练 notoriously 难以进行,并且容易发生模式崩塌。模式崩塌是指生成器即使在损失函数已优化的情况下,也会生成看起来相同的输出。在 MNIST 数字的情境中,若发生模式崩塌,生成器可能只会生成数字 4 和 9,因为它们看起来相似。瓦瑟斯坦 GAN(WGAN)[2]通过认为可以通过简单地替换基于 Wasserstein 1 或地球搬运距离(EMD)的 GAN 损失函数来避免稳定性训练和模式崩塌,解决了这些问题。
然而,稳定性问题并不是 GAN 唯一的难题。还有一个日益迫切的需求是提升生成图像的感知质量。最小二乘 GAN(LSGAN)[3]提出了同时解决这两个问题的方法。基本前提是 sigmoid 交叉熵损失在训练过程中会导致梯度消失,这会导致图像质量差。最小二乘损失不会引发梯度消失。与传统的 GAN 生成图像相比,采用最小二乘损失生成的图像在感知质量上有显著提高。
在上一章中,CGAN 介绍了一种对生成器输出进行条件控制的方法。例如,如果我们想要得到数字 8,我们会在输入生成器时加入条件标签。受 CGAN 启发,辅助分类器 GAN(ACGAN)[4]提出了一种改进的条件算法,从而使得输出的感知质量和多样性得到了更好的提升。
总结来说,本章的目标是介绍这些改进的 GAN 并展示:
-
WGAN 的理论公式
-
理解 LSGAN 的原理
-
理解 ACGAN 的原理
-
知道如何使用 Keras 实现改进的 GAN——WGAN、LSGAN 和 ACGAN
瓦瑟斯坦 GAN
如我们之前提到的,GAN 的训练是非常困难的。两个网络——判别器和生成器的对立目标很容易导致训练不稳定。判别器试图正确区分真假数据,而生成器则尽力欺骗判别器。如果判别器学习得比生成器快,生成器的参数就无法得到优化。另一方面,如果判别器学习得较慢,那么梯度可能在到达生成器之前就消失了。在最糟糕的情况下,如果判别器无法收敛,生成器将无法获得任何有用的反馈。
距离函数
训练 GAN 的稳定性可以通过检查其损失函数来理解。为了更好地理解 GAN 的损失函数,我们将回顾两个概率分布之间的常见距离或散度函数。我们关心的是 p[data](真实数据分布)与 p[g](生成器数据分布)之间的距离。GAN 的目标是使 p[g] → p[data]。表 5.1.1 显示了这些散度函数。
在大多数最大似然任务中,我们将使用 Kullback-Leibler (KL) 散度或 D[KL] 作为损失函数中的度量,来衡量我们神经网络模型预测与真实分布函数之间的差距。如 方程 5.1.1 所示,D[KL] 是不对称的,因为https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_001.jpg。
Jensen-Shannon (JS) 或 D[JS] 是基于 D[KL] 的散度。然而,与 D[KL] 不同,D[JS] 是对称的,并且是有限的。在这一部分,我们将展示优化 GAN 损失函数等价于优化 D[JS]。
散度 | 表达式 |
---|---|
Kullback-Leibler (KL) 5.1.1 | https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_002.jpghttps://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_03.jpg |
Jensen-Shannon (JS) 5.1.2 | https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_004.jpg |
Earth-Mover Distance (EMD) 或 Wasserstein 15.1.3 | https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_005.jpg,其中https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_006.jpg 是所有联合分布 y(x,y) 的集合,其边际分布为 p[data] 和 p[g]。 |
表 5.1.1:两个概率分布函数 p[data] 和 p[g] 之间的散度函数
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_01.jpg
图 5.1.1:EMD 是从 x 到目标分布 y 所需运输的质量的加权量
EMD(地球搬运距离)的直觉是,它衡量了概率分布 p[data] 在与概率分布 p[g] 匹配时,需要通过 d = ||x - y|| 运输的质量量!距离函数。https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_009.jpg是所有可能的联合分布空间中的一个联合分布!距离函数。https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_011.jpg 也称为运输计划,用于反映将质量运输到以匹配两个概率分布的策略。给定这两个概率分布,有许多可能的运输计划。粗略来说,inf 表示具有最小成本的运输计划。
例如,图 5.1.1展示了两个简单的离散分布https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_012.jpg和https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_013.jpg。https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_014.jpg在位置 x i(i = 1, 2, 3 和 4)上有质量 m i,同时https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_015.jpg在位置 y i(i = 1 和 2)上有质量 m i。为了匹配分布https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_016.jpg,箭头展示了将每个质量 x i 移动 d i 的最小运输方案。EMD 计算公式为:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_017.jpg
(方程 5.1.4)
在图 5.1.1中,EMD 可以被解释为将土堆https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_018.jpg移到填满孔洞https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_019.jpg所需的最少工作量。虽然在这个例子中,inf也可以从图中推导出来,但在大多数情况下,特别是对于连续分布,穷举所有可能的运输方案是不可行的。我们将在本章稍后回到这个问题。与此同时,我们将展示 GAN 损失函数实际上是在最小化詹森-香农(JS)散度。
GAN 中的距离函数
我们现在将根据上一章中的损失函数,计算给定任何生成器的最优判别器。我们将回顾以下方程:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_020.jpg(方程 4.1.1)
除了从噪声分布进行采样,前述方程还可以表示为从生成器分布进行采样:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_021.jpg(方程 5.1.5)
为了找到最小值https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_022.jpg:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_023.jpg(方程 5.1.6)
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_024.jpg(方程 5.1.7)
积分内部的项呈 y → a log y + b log(1 - y) 形式,该式在https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_025.jpg处达到已知的最大值,对于任何https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_026.jpg,不包括 {0,0} 的https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_027.jpg 都成立。由于积分不会改变该表达式的最大值位置(或https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_028.jpg的最小值),因此最优判别器为:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_029.jpg(方程 5.1.8)
因此,损失函数给出了最优判别器:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_030.jpg(方程 5.1.9)
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_031.jpg(方程 5.1.10)
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_032.jpg(方程 5.1.11)
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_033.jpg(方程 5.1.12)
我们可以从方程 5.1.12中观察到,最优判别器的损失函数是一个常数减去真实分布p[data]与任何生成器分布p[g]之间的两倍 Jensen-Shannon 散度。最小化https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_034.jpg意味着最大化https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_035.jpg,或者判别器必须正确区分假数据和真实数据。
与此同时,我们可以合理地认为,最优生成器是当生成器分布等于真实数据分布时:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_036.jpg(方程 5.1.13)
这是有道理的,因为生成器的目标是通过学习真实数据分布来欺骗判别器。实际上,我们可以通过最小化D[JS],,或者通过使p[g] → p[data]来得到最优生成器。给定最优生成器,最优判别器是https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_037.jpg,并且https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_038.jpg。
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_02.jpg
图 5.1.2:没有重叠的两个分布示例。https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_039.jpg 适用于p[g]
问题是,当两个分布没有重叠时,没有平滑的函数可以帮助缩小它们之间的差距。通过梯度下降训练 GAN 将无法收敛。例如,假设:
p[data] = (x, y) 其中https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_040.jpg(方程 5.1.14)
p[g] = (x, y) 其中https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_041.jpg(方程 5.1.15)
如图 5.1.2所示,U(0,1)是均匀分布。每个距离函数的散度如下:
-
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_42.jpg
-
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_043.jpg
-
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_44.jpg
-
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_045.jpg
由于D[JS]是一个常数,GAN 将没有足够的梯度来驱动p[g] → p[data]。我们还会发现D[KL]或反向D[KL]也无济于事。然而,通过W(p[data],p[g])我们可以得到一个平滑的函数,以便通过梯度下降使p[g] → p[data]。EMD 或 Wasserstein 1 似乎是优化 GAN 的更合适的损失函数,因为D[JS]在两个分布几乎没有重叠的情况下无法发挥作用。
为了进一步理解,关于距离函数的精彩讨论可以在lilianweng.github.io/lil-log/2017/08/20/from-GAN-to-W
GAN.html找到。
Wasserstein 损失的使用
在使用 EMD 或 Wasserstein 1 之前,还需要克服一个问题。穷举https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_046.jpg空间以找到https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_047.jpg是不可行的。提出的解决方案是使用其 Kantorovich-Rubinstein 对偶:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_048.jpg
(方程 5.1.16)
等价地,EMD,https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_049.jpg,是所有K-Lipschitz 函数的上确界(大致为最大值):https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_050.jpg。K-Lipschitz 函数满足以下约束:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_51.jpg (方程 5.1.17)
对于所有https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_052.jpg,K-Lipschitz 函数具有有界导数,且几乎总是连续可微(例如,f(x) = |x|具有有界导数且连续,但在x = 0 处不可微)。
方程 5.1.16可以通过找到一系列K-Lipschitz 函数来求解https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_053.jpg:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_54.jpg (方程 5.1.18)
在 GAN 的上下文中,方程 5.1.18 可以通过从z-噪声分布采样并将f[w]替换为判别器函数D[w]来重写:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_55.jpg (方程 5.1.19)
我们使用粗体字母来突出显示多维样本的通用性。我们面临的最终问题是如何找到函数系列https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_056.jpg。我们将要讨论的提议解决方案是,在每次梯度更新时,判别器的权重w在下界和上界之间裁剪(例如,-0.0, 1 和 0.01):
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_57.jpg (方程 5.1.20)
w的较小值限制了判别器在紧凑的参数空间中,从而确保了 Lipschitz 连续性。
我们可以使用方程 5.1.19作为我们新 GAN 损失函数的基础。EMD 或 Wasserstein 1 是生成器试图最小化的损失函数,而判别器试图最大化(或最小化-W(p[数据], p[生成])):
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_058.jpg
(方程 5.1.21)
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_59.jpg
(方程 5.1.22)
在生成器损失函数中,第一个项会消失,因为它并没有直接与真实数据进行优化。
以下表格展示了 GAN 和 WGAN 的损失函数之间的差异。为简洁起见,我们简化了https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_060.jpg 和https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_061.jpg的符号。这些损失函数用于训练 WGAN,如算法 5.1.1所示。图 5.1.3展示了 WGAN 模型实际上与 DCGAN 模型相同,唯一的区别在于假数据/真实数据标签和损失函数:
表 5.1.1:GAN 与 WGAN 的损失函数比较
算法 5.1.1 WGAN
参数的值是https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_067.jpg,c = 0.01,m = 64,n[critic] = 5。
需要:https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_068.jpg,学习率。c,裁剪参数。m,批量大小。n[critic],每个生成器迭代中的判别器(鉴别器)迭代次数。
需要:w[0],初始判别器(鉴别器)参数。https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_069.jpg,初始生成器参数
-
while
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_070.jpg尚未收敛
do
-
for
t = 1, …, n[critic]do
-
从均匀噪声分布中采样一个批次https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_071.jpg
来自真实数据
-
从均匀噪声分布中采样一个批次https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_72.jpg
来自均匀噪声分布
-
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_073.jpg
,计算鉴别器梯度
-
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_074.jpg
,更新鉴别器参数
-
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_075.jpg
,裁剪鉴别器权重
-
end for
-
从均匀噪声分布中采样一个批次https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_076.jpg
来自均匀噪声分布
-
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_077.jpg
,计算生成器梯度
-
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_078.jpg
,更新生成器参数
-
end while
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_03.jpg
图 5.1.3:上图:训练 WGAN 鉴别器需要来自生成器的假数据和来自真实分布的真实数据。下图:训练 WGAN 生成器需要来自生成器的假数据,这些数据假装是来自真实分布。
类似于 GAN,WGAN 交替训练鉴别器和生成器(通过对抗)。然而,在 WGAN 中,鉴别器(也称为评论员)训练n[critic]次迭代(第 2 到第 8 行),然后再训练生成器一次迭代(第 9 到第 11 行)。与 GAN 相比,WGAN 在训练过程中对鉴别器和生成器的训练次数不同。训练鉴别器意味着学习鉴别器的参数(权重和偏置)。这需要从真实数据中采样一个批次(第 3 行)和从假数据中采样一个批次(第 4 行),然后在将采样的数据传入鉴别器网络后计算鉴别器参数的梯度(第 5 行)。鉴别器参数使用 RMSProp 进行优化(第 6 行)。第 5 行和第 6 行是对方程 5.1.21的优化。研究表明,在 WGAN 中,Adam 优化器表现不稳定。
最后,EM 距离优化中的 Lipschitz 约束通过裁剪判别器参数(第 7 行)来施加。第 7 行实现了方程 5.1.20。经过n[批评者]迭代的判别器训练后,判别器参数被冻结。生成器训练从采样一批假数据开始(第 9 行)。采样的数据被标记为真实(1.0),试图欺骗判别器网络。生成器的梯度在第 10 行计算,并在第 11 行使用 RMSProp 进行优化。第 10 行和第 11 行执行梯度更新,以优化方程 5.1.22。
在训练完生成器后,判别器参数会被解冻,开始另一轮n[批评者]判别器训练迭代。需要注意的是,在判别器训练期间无需冻结生成器参数,因为生成器仅参与数据的生成。与 GAN 类似,判别器可以作为一个独立的网络进行训练。然而,训练生成器始终需要判别器的参与,因为损失是从生成器网络的输出计算的。
与 GAN 不同,在 WGAN 中,真实数据标记为 1.0,而假数据标记为-1.0,这是为了在第 5 行计算梯度时作为一种解决方法。第 5-6 行和第 10-11 行执行梯度更新,分别优化方程 5.1.21和5.1.22。第 5 行和第 10 行中的每一项都被建模为:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_079.jpg
(方程 5.1.23)
其中,y[标签] = 1.0 表示真实数据,y[标签] = -1.0 表示假数据。为了简化符号,我们移除了上标(i)。对于判别器,WGAN 增加了https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_80.jpg,以在使用真实数据进行训练时最小化损失函数。当使用假数据进行训练时,WGAN 减少了https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_081.jpg,以最小化损失函数。对于生成器,当假数据在训练过程中被标记为真实时,WGAN 增加了https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_082.jpg,以最小化损失函数。请注意,y[标签]在损失函数中的直接贡献仅限于它的符号。在 Keras 中,方程 5.1.23实现为:
def wasserstein_loss(y_label, y_pred):
return -K.mean(y_label * y_pred)
使用 Keras 实现 WGAN
要在 Keras 中实现 WGAN,我们可以重用前一章中介绍的 GAN 的 DCGAN 实现。DCGAN 构建器和工具函数作为模块在lib
文件夹中的gan.py
中实现。
函数包括:
-
generator()
: 生成器模型构建器 -
discriminator()
: 判别器模型构建器 -
train()
: DCGAN 训练器 -
plot_images()
: 通用生成器输出绘图工具 -
test_generator()
: 通用生成器测试工具
如列表 5.1.1所示,我们可以通过简单调用来构建判别器:
discriminator = gan.discriminator(inputs, activation='linear')
WGAN 使用线性输出激活。对于生成器,我们执行:
generator = gan.generator(inputs, image_size)
Keras 中的整体网络模型类似于图 4.2.1中显示的 DCGAN。
列表 5.1.1 强调了使用 RMSprop 优化器和 Wasserstein 损失函数。算法 5.1.1 中的超参数在训练中使用。列表 5.1.2 是紧密跟随该 算法 的训练函数。然而,在训练判别器时有一个小的调整。我们不再在一个包含真实和虚假数据的单一批次中训练权重,而是先使用一批真实数据进行训练,然后再使用一批虚假数据进行训练。这种调整将防止由于真实和虚假数据标签的符号相反以及由于裁剪导致的权重幅度较小而导致梯度消失。
注意
完整代码可在 GitHub 上找到:
github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras
图 5.1.4 展示了 WGAN 在 MNIST 数据集上的输出演变。
列表 5.1.1,wgan-mnist-5.1.2.py
。WGAN 模型实例化和训练。判别器和生成器都使用 Wasserstein 1 损失,wasserstein_loss()
:
def build_and_train_models():
# load MNIST dataset
(x_train, _), (_, _) = mnist.load_data()
# reshape data for CNN as (28, 28, 1) and normalize
image_size = x_train.shape[1]
x_train = np.reshape(x_train, [-1, image_size, image_size, 1])
x_train = x_train.astype('float32') / 255
model_name = "wgan_mnist"
# network parameters
# the latent or z vector is 100-dim
latent_size = 100
# hyper parameters from WGAN paper [2]
n_critic = 5
clip_value = 0.01
batch_size = 64
lr = 5e-5
train_steps = 40000
input_shape = (image_size, image_size, 1)
# build discriminator model
inputs = Input(shape=input_shape, name='discriminator_input')
# WGAN uses linear activation in paper [2]
discriminator = gan.discriminator(inputs, activation='linear')
optimizer = RMSprop(lr=lr)
# WGAN discriminator uses wassertein loss
discriminator.compile(loss=wasserstein_loss,
optimizer=optimizer,
metrics=['accuracy'])
discriminator.summary()
# build generator model
input_shape = (latent_size, )
inputs = Input(shape=input_shape, name='z_input')
generator = gan.generator(inputs, image_size)
generator.summary()
# build adversarial model = generator + discriminator
# freeze the weights of discriminator
# during adversarial training
discriminator.trainable = False
adversarial = Model(inputs,
discriminator(generator(inputs)),
name=model_name)
adversarial.compile(loss=wasserstein_loss,
optimizer=optimizer,
metrics=['accuracy'])
adversarial.summary()
# train discriminator and adversarial networks
models = (generator, discriminator, adversarial)
params = (batch_size,
latent_size,
n_critic,
clip_value,
train_steps,
model_name)
train(models, x_train, params)
列表 5.1.2,wgan-mnist-5.1.2.py
。WGAN 的训练过程严格遵循 算法 5.1.1。判别器每训练一次生成器,需要进行 n [批判] 次迭代:
def train(models, x_train, params):
"""Train the Discriminator and Adversarial Networks
Alternately train Discriminator and Adversarial networks by batch.
Discriminator is trained first with properly labeled real and fake images
for n_critic times.
Discriminator weights are clipped as a requirement of Lipschitz constraint.
Generator is trained next (via Adversarial) with fake images
pretending to be real.
Generate sample images per save_interval
# Arguments
models (list): Generator, Discriminator, Adversarial models
x_train (tensor): Train images
params (list) : Networks parameters
"""
# the GAN models
generator, discriminator, adversarial = models
# network parameters
(batch_size, latent_size, n_critic,
clip_value, train_steps, model_name) = params
# the generator image is saved every 500 steps
save_interval = 500
# noise vector to see how the generator output
# evolves during training
noise_input = np.random.uniform(-1.0, 1.0, size=[16,
latent_size])
# number of elements in train dataset
train_size = x_train.shape[0]
# labels for real data
real_labels = np.ones((batch_size, 1))
for i in range(train_steps):
# train discriminator n_critic times
loss = 0
acc = 0
for _ in range(n_critic):
# train the discriminator for 1 batch
# 1 batch of real (label=1.0) and
# fake images (label=-1.0)
# randomly pick real images from dataset
rand_indexes = np.random.randint(0,
train_size,
size=batch_size)
real_images = x_train[rand_indexes]
# generate fake images from noise using generator
# generate noise using uniform distribution
noise = np.random.uniform(-1.0,
1.0,
size=[batch_size,
latent_size])
fake_images = generator.predict(noise)
# train the discriminator network
# real data label=1, fake data label=-1
# instead of 1 combined batch of real and fake images,
# train with 1 batch of real data first, then 1 batch
# of fake images.
# this tweak prevents the gradient from vanishing
# due to opposite signs of real and
# fake data labels (i.e. +1 and -1) and
# small magnitude of weights due to clipping.
real_loss, real_acc =
discriminator.train_on_batch(real_images,
real_labels)
fake_loss, fake_acc =
discriminator.train_on_batch(fake_images,
real_labels)
# accumulate average loss and accuracy
loss += 0.5 * (real_loss + fake_loss)
acc += 0.5 * (real_acc + fake_acc)
# clip discriminator weights to satisfy
# Lipschitz constraint
for layer in discriminator.layers:
weights = layer.get_weights()
weights = [np.clip(weight,
-clip_value,
clip_value) for weight in weights]
layer.set_weights(weights)
# average loss and accuracy per n_critic
# training iterations
loss /= n_critic
acc /= n_critic
log = "%d: [discriminator loss: %f, acc: %f]" % (i, loss, acc)
# train the adversarial network for 1 batch
# 1 batch of fake images with label=1.0
# since the discriminator weights are
# frozen in adversarial network
# only the generator is trained
# generate noise using uniform distribution
noise = np.random.uniform(-1.0, 1.0,
size=[batch_size, latent_size])
# train the adversarial network
# note that unlike in discriminator training,
# we do not save the fake images in a variable
# the fake images go to the discriminator input
# of the adversarial for classification
# fake images are labelled as real
# log the loss and accuracy
loss, acc = adversarial.train_on_batch(noise, real_labels)
log = "%s [adversarial loss: %f, acc: %f]" % (log, loss, acc)
print(log)
if (i + 1) % save_interval == 0:
if (i + 1) == train_steps:
show = True
else:
show = False
# plot generator images on a periodic basis
gan.plot_images(generator,
noise_input=noise_input,
show=show,
step=(i + 1),
model_name=model_name)
# save the model after training the generator
# the trained generator can be reloaded for future
# MNIST digit generation
generator.save(model_name + ".h5")
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_04.jpg
图 5.1.4:WGAN 的样本输出与训练步骤的对比。WGAN 在训练和测试过程中没有遭遇模式崩溃。
即使在网络配置发生变化时,WGAN 也依然稳定。例如,众所周知,当批量归一化插入到判别器网络中的 ReLU 激活之前时,DCGAN 会变得不稳定。而相同的配置在 WGAN 中是稳定的。
下图展示了在判别器网络上应用批量归一化时,DCGAN 和 WGAN 的输出:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_05.jpg
图 5.1.5:在判别器网络中,将批量归一化插入到 ReLU 激活之前时,DCGAN(左)和 WGAN(右)输出的对比
类似于上一章中的 GAN 训练,训练好的模型会在 40,000 次训练步骤后保存到文件中。我鼓励你运行训练好的生成器模型,查看生成的新 MNIST 数字图像:
python3 wgan-mnist-5.1.2.py --generator=wgan_mnist.h5
最小二乘 GAN(LSGAN)
正如上一节所讨论的,原始 GAN 很难训练。当 GAN 优化其损失函数时,实际上是在优化 Jensen-Shannon 散度,D[JS]。当两个分布函数之间几乎没有重叠时,优化 D[JS] 是非常困难的。
WGAN 提出了使用 EMD 或 Wasserstein 1 损失函数来解决这个问题,即使在两个分布几乎没有重叠的情况下,它也具有平滑的可微函数。然而,WGAN 并不关注生成图像的质量。除了稳定性问题外,原始 GAN 生成的图像在感知质量方面仍有提升空间。LSGAN 理论认为,这两个问题可以同时得到解决。
LSGAN 提出了最小二乘损失。图 5.2.1 说明了为何在 GAN 中使用 sigmoid 交叉熵损失会导致生成的数据质量较差。理想情况下,假样本的分布应尽可能接近真实样本的分布。然而,对于 GAN,一旦假样本已经处于正确的决策边界一侧,梯度就会消失。
这防止了生成器有足够的动力来提升生成假数据的质量。远离决策边界的假样本将不再尝试接近真实样本的分布。使用最小二乘损失函数时,只要假样本分布远离真实样本分布,梯度就不会消失。即使假样本已经处于正确的决策边界一侧,生成器也会努力提高其对真实密度分布的估计:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_06.jpg
图 5.2.1:真实样本和假样本的分布被各自的决策边界划分:Sigmoid 和最小二乘
表 5.2.1:GAN 和 LSGAN 的损失函数比较
上表展示了 GAN 和 LSGAN 之间损失函数的比较。最小化方程 5.2.1或判别器损失函数意味着真实数据分类与真实标签 1.0 之间的 MSE 应接近零。此外,假数据分类与真实标签 0.0 之间的 MSE 也应接近零。
类似于 GAN,LSGAN 的判别器被训练来区分真实数据和假数据样本。最小化方程 5.2.2意味着欺骗判别器,使其认为生成的假样本数据是标签为 1.0 的真实数据。
使用上一章中的 DCGAN 代码作为基础来实现 LSGAN 只需少量更改。如列表 5.2.1所示,移除了判别器的 sigmoid 激活函数。判别器通过以下方式构建:
discriminator = gan.discriminator(inputs, activation=None)
生成器类似于原始的 DCGAN:
generator = gan.generator(inputs, image_size)
判别器和对抗损失函数都被替换为 mse
。所有网络参数与 DCGAN 中相同。LSGAN 在 Keras 中的网络模型与图 4.2.1 类似,唯一不同的是没有线性或输出激活。训练过程与 DCGAN 中看到的相似,并由实用函数提供:
gan.train(models, x_train, params)
清单 5.2.1,lsgan-mnist-5.2.1.py
显示了判别器和生成器在 DCGAN 中是相同的,除了判别器输出激活和使用了 MSE 损失函数:
def build_and_train_models():
# MNIST dataset
(x_train, _), (_, _) = mnist.load_data()
# reshape data for CNN as (28, 28, 1) and normalize
image_size = x_train.shape[1]
x_train = np.reshape(x_train, [-1, image_size, image_size, 1])
x_train = x_train.astype('float32') / 255
model_name = "lsgan_mnist"
# network parameters
# the latent or z vector is 100-dim
latent_size = 100
input_shape = (image_size, image_size, 1)
batch_size = 64
lr = 2e-4
decay = 6e-8
train_steps = 40000
# build discriminator model
inputs = Input(shape=input_shape, name='discriminator_input')
discriminator = gan.discriminator(inputs, activation=None)
# [1] uses Adam, but discriminator converges
# easily with RMSprop
optimizer = RMSprop(lr=lr, decay=decay)
# LSGAN uses MSE loss [2]
discriminator.compile(loss='mse',
optimizer=optimizer,
metrics=['accuracy'])
discriminator.summary()
# build generator model
input_shape = (latent_size, )
inputs = Input(shape=input_shape, name='z_input')
generator = gan.generator(inputs, image_size)
generator.summary()
# build adversarial model = generator + discriminator
optimizer = RMSprop(lr=lr*0.5, decay=decay*0.5)
# freeze the weights of discriminator
# during adversarial training
discriminator.trainable = False
adversarial = Model(inputs,
discriminator(generator(inputs)),
name=model_name)
# LSGAN uses MSE loss [2]
adversarial.compile(loss='mse',
optimizer=optimizer,
metrics=['accuracy'])
adversarial.summary()
# train discriminator and adversarial networks
models = (generator, discriminator, adversarial)
params = (batch_size, latent_size, train_steps, model_name)
gan.train(models, x_train, params)
接下来的图展示了使用 MNIST 数据集进行 40,000 步训练后,LSGAN 生成的样本。与 DCGAN 中的图 4.2.1相比,输出图像具有更好的感知质量:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_07.jpg
图 5.2.2:LSGAN 与训练步数的样本输出
我鼓励你运行训练好的生成器模型,查看新合成的 MNIST 数字图像:
python3 lsgan-mnist-5.2.1.py --generator=lsgan_mnist.h5
辅助分类器 GAN (ACGAN)
ACGAN 在原理上与我们在上一章讨论的 条件 GAN (CGAN) 相似。我们将比较 CGAN 和 ACGAN。对于 CGAN 和 ACGAN,生成器的输入是噪声和标签。输出是属于输入类标签的假图像。对于 CGAN,判别器的输入是图像(假图像或真实图像)及其标签。输出是图像为真实的概率。对于 ACGAN,判别器的输入是图像,而输出是图像为真实的概率及其类别标签。接下来的图突出显示了 CGAN 和 ACGAN 在生成器训练中的区别:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_08.jpg
图 5.3.1:CGAN 与 ACGAN 生成器训练。主要区别在于判别器的输入和输出。
本质上,在 CGAN 中,我们为网络提供边信息(标签)。在 ACGAN 中,我们尝试使用辅助类解码器网络来重建边信息。ACGAN 认为,强迫网络执行额外任务已被证明能提高原始任务的性能。在这种情况下,额外的任务是图像分类。原始任务是生成假图像。
| ACGAN | https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_089.jpghttps://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_090.jpg
表 5.3.1:CGAN 与 ACGAN 损失函数的比较
5.3.15.3.2 |
---|
上表展示了与 CGAN 相比的 ACGAN 损失函数。ACGAN 的损失函数与 CGAN 相同,唯一不同的是额外的分类器损失函数。除了原本识别真假图像的任务 (https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_091.jpg),判别器的Equation 5.3.1还有一个额外任务,即正确分类真假图像 (https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_092.jpg)。生成器的Equation 5.3.2意味着除了通过假图像来欺骗判别器 (https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_093.jpg),它还要求判别器正确分类这些假图像 (https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_094.jpg)。
从 CGAN 代码开始,只需要修改判别器和训练函数以实现 ACGAN。判别器和生成器构建函数也由gan.py
提供。为了查看在判别器上做出的修改,以下listing展示了构建函数,其中突出了执行图像分类的辅助解码器网络和双输出。
Listing 5.3.1,gan.py
展示了判别器模型构建与 DCGAN 相同,用于预测图像是否为真实,作为第一个输出。添加了一个辅助解码器网络来执行图像分类并产生第二个输出:
def discriminator(inputs,
activation='sigmoid',
num_labels=None,
num_codes=None):
"""Build a Discriminator Model
Stack of LeakyReLU-Conv2D to discriminate real from fake
The network does not converge with BN so it is not used here
unlike in [1]
# Arguments
inputs (Layer): Input layer of the discriminator (the image)
activation (string): Name of output activation layer
num_labels (int): Dimension of one-hot labels for ACGAN & InfoGAN
num_codes (int): num_codes-dim Q network as output
if StackedGAN or 2 Q networks if InfoGAN
# Returns
Model: Discriminator Model
"""
kernel_size = 5
layer_filters = [32, 64, 128, 256]
x = inputs
for filters in layer_filters:
# first 3 convolution layers use strides = 2
# last one uses strides = 1
if filters == layer_filters[-1]:
strides = 1
else:
strides = 2
x = LeakyReLU(alpha=0.2)(x)
x = Conv2D(filters=filters,
kernel_size=kernel_size,
strides=strides,
padding='same')(x)
x = Flatten()(x)
# default output is probability that the image is real
outputs = Dense(1)(x)
if activation is not None:
print(activation)
outputs = Activation(activation)(outputs)
if num_labels:
# ACGAN and InfoGAN have 2nd output
# 2nd output is 10-dim one-hot vector of label
layer = Dense(layer_filters[-2])(x)
labels = Dense(num_labels)(layer)
labels = Activation('softmax', name='label')(labels)
if num_codes is None:
outputs = [outputs, labels]
else:
# InfoGAN have 3rd and 4th outputs
# 3rd output is 1-dim continous Q of 1st c given x
code1 = Dense(1)(layer)
code1 = Activation('sigmoid', name='code1')(code1)
# 4th output is 1-dim continuous Q of 2nd c given x
code2 = Dense(1)(layer)
code2 = Activation('sigmoid', name='code2')(code2)
outputs = [outputs, labels, code1, code2]
elif num_codes is not None:
# z0_recon is reconstruction of z0 normal distribution
z0_recon = Dense(num_codes)(x)
z0_recon = Activation('tanh', name='z0')(z0_recon)
outputs = [outputs, z0_recon]
return Model(inputs, outputs, name='discriminator')
然后通过调用以下代码构建判别器:
discriminator = gan.discriminator(inputs, num_labels=num_labels)
生成器与 ACGAN 中的生成器相同。回顾一下,生成器构建函数在以下listing中展示。我们需要注意的是,Listings 5.3.1和5.3.2是 WGAN 和 LSGAN 在前面章节中使用的相同构建函数。
Listing 5.3.2,gan.py
展示了生成器模型构建与 CGAN 中的相同:
def generator(inputs,
image_size,
activation='sigmoid',
labels=None,
codes=None):
"""Build a Generator Model
Stack of BN-ReLU-Conv2DTranpose to generate fake images.
Output activation is sigmoid instead of tanh in [1].
Sigmoid converges easily.
# Arguments
inputs (Layer): Input layer of the generator (the z-vector)
image_size (int): Target size of one side (assuming square image)
activation (string): Name of output activation layer
labels (tensor): Input labels
codes (list): 2-dim disentangled codes for InfoGAN
# Returns
Model: Generator Model
"""
image_resize = image_size // 4
# network parameters
kernel_size = 5
layer_filters = [128, 64, 32, 1]
if labels is not None:
if codes is None:
# ACGAN labels
# concatenate z noise vector and one-hot labels
inputs = [inputs, labels]
else:
# infoGAN codes
# concatenate z noise vector, one-hot labels
# and codes 1 & 2
inputs = [inputs, labels] + codes
x = concatenate(inputs, axis=1)
elif codes is not None:
# generator 0 of StackedGAN
inputs = [inputs, codes]
x = concatenate(inputs, axis=1)
else:
# default input is just 100-dim noise (z-code)
x = inputs
x = Dense(image_resize * image_resize * layer_filters[0])(x)
x = Reshape((image_resize, image_resize, layer_filters[0]))(x)
for filters in layer_filters:
# first two convolution layers use strides = 2
# the last two use strides = 1
if filters > layer_filters[-2]:
strides = 2
else:
strides = 1
x = BatchNormalization()(x)
x = Activation('relu')(x)
x = Conv2DTranspose(filters=filters,
kernel_size=kernel_size,
strides=strides,
padding='same')(x)
if activation is not None:
x = Activation(activation)(x)
# generator output is the synthesized image x
return Model(inputs, x, name='generator')
在 ACGAN 中,生成器被实例化为:
generator = gan.generator(inputs, image_size, labels=labels)
以下图展示了 Keras 中 ACGAN 的网络模型:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_09.jpg
图 5.3.2:ACGAN 的 Keras 模型
如Listing 5.3.3所示,判别器和对抗模型已被修改以适应判别器网络中的变化。现在我们有了两个损失函数。第一个是原始的二元交叉熵,用于训练判别器估计输入图像是否真实。第二个是图像分类器,预测类别标签。输出是一个 10 维的独热向量。
参考Listing 5.3.3,acgan-mnist-5.3.1.py
,其中突出了为适应判别器网络的图像分类器,在判别器和对抗模型中实施的变化。两个损失函数分别对应判别器的两个输出:
def build_and_train_models():
# load MNIST dataset
(x_train, y_train), (_, _) = mnist.load_data()
# reshape data for CNN as (28, 28, 1) and normalize
image_size = x_train.shape[1]
x_train = np.reshape(x_train, [-1, image_size, image_size, 1])
x_train = x_train.astype('float32') / 255
# train labels
num_labels = len(np.unique(y_train))
y_train = to_categorical(y_train)
model_name = "acgan_mnist"
# network parameters
latent_size = 100
batch_size = 64
train_steps = 40000
lr = 2e-4
decay = 6e-8
input_shape = (image_size, image_size, 1)
label_shape = (num_labels, )
# build discriminator Model
inputs = Input(shape=input_shape, name='discriminator_input')
# call discriminator builder with 2 outputs,
# pred source and labels
discriminator = gan.discriminator(inputs, num_labels=num_labels)
# [1] uses Adam, but discriminator converges easily with RMSprop
optimizer = RMSprop(lr=lr, decay=decay)
# 2 loss fuctions: 1) probability image is real
# 2) class label of the image
loss = ['binary_crossentropy', 'categorical_crossentropy']
discriminator.compile(loss=loss,
optimizer=optimizer,
metrics=['accuracy'])
discriminator.summary()
# build generator model
input_shape = (latent_size, )
inputs = Input(shape=input_shape, name='z_input')
labels = Input(shape=label_shape, name='labels')
# call generator builder with input labels
generator = gan.generator(inputs, image_size, labels=labels)
generator.summary()
# build adversarial model = generator + discriminator
optimizer = RMSprop(lr=lr*0.5, decay=decay*0.5)
# freeze the weights of discriminator
# during adversarial training
discriminator.trainable = False
adversarial = Model([inputs, labels],
discriminator(generator([inputs, labels])),
name=model_name)
# same 2 loss fuctions: 1) probability image is real
# 2) class label of the image
adversarial.compile(loss=loss,
optimizer=optimizer,
metrics=['accuracy'])
adversarial.summary()
# train discriminator and adversarial networks
models = (generator, discriminator, adversarial)
data = (x_train, y_train)
params = (batch_size, latent_size, train_steps, num_labels, model_name)
train(models, data, params)
在Listing 5.3.4中,我们突出了训练过程中实现的变化。与 CGAN 代码相比,主要的区别在于输出标签必须在判别器和对抗训练期间提供。
如Listing 5.3.4中所示,acgan-mnist-5.3.1.py
,train 函数中实现的更改已突出显示:
def train(models, data, params):
"""Train the discriminator and adversarial Networks
Alternately train discriminator and adversarial networks by batch.
Discriminator is trained first with real and fake images and
corresponding one-hot labels.
Adversarial is trained next with fake images pretending to be real and
corresponding one-hot labels.
Generate sample images per save_interval.
# Arguments
models (list): Generator, Discriminator, Adversarial models
data (list): x_train, y_train data
params (list): Network parameters
"""
# the GAN models
generator, discriminator, adversarial = models
# images and their one-hot labels
x_train, y_train = data
# network parameters
batch_size, latent_size, train_steps, num_labels, model_name = params
# the generator image is saved every 500 steps
save_interval = 500
# noise vector to see how the generator output
# evolves during training
noise_input = np.random.uniform(-1.0,
1.0,
size=[16, latent_size])
# class labels are 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5
# the generator must produce these MNIST digits
noise_label = np.eye(num_labels)[np.arange(0, 16) % num_labels]
# number of elements in train dataset
train_size = x_train.shape[0]
print(model_name,
"Labels for generated images: ",
np.argmax(noise_label, axis=1))
for i in range(train_steps):
# train the discriminator for 1 batch
# 1 batch of real (label=1.0) and fake images (label=0.0)
# randomly pick real images and corresponding labels
# from dataset
rand_indexes = np.random.randint(0,
train_size,
size=batch_size)
real_images = x_train[rand_indexes]
real_labels = y_train[rand_indexes]
# generate fake images from noise using generator
# generate noise using uniform distribution
noise = np.random.uniform(-1.0,
1.0,
size=[batch_size, latent_size])
# randomly pick one-hot labels
fake_labels = np.eye(num_labels)[np.random.choice(num_labels,
batch_size)]
# generate fake images
fake_images = generator.predict([noise, fake_labels])
# real + fake images = 1 batch of train data
x = np.concatenate((real_images, fake_images))
# real + fake labels = 1 batch of train data labels
labels = np.concatenate((real_labels, fake_labels))
# label real and fake images
# real images label is 1.0
y = np.ones([2 * batch_size, 1])
# fake images label is 0.0
y[batch_size:, :] = 0
# train discriminator network, log the loss and accuracy
# ['loss', 'activation_1_loss', 'label_loss',
# 'activation_1_acc', 'label_acc']
metrics = discriminator.train_on_batch(x, [y, labels])
fmt = "%d: [disc loss: %f, srcloss: %f, lblloss: %f, srcacc: %f, lblacc: %f]"
log = fmt % (i, metrics[0], metrics[1], metrics[2], metrics[3], metrics[4])
# train the adversarial network for 1 batch
# 1 batch of fake images with label=1.0 and
# corresponding one-hot label or class
# since the discriminator weights are frozen
# in adversarial network
# only the generator is trained
# generate noise using uniform distribution
noise = np.random.uniform(-1.0,
1.0,
size=[batch_size, latent_size])
# randomly pick one-hot labels
fake_labels = np.eye(num_labels)[np.random.choice(num_labels,
batch_size)]
# label fake images as real
y = np.ones([batch_size, 1])
# train the adversarial network
# note that unlike in discriminator training,
# we do not save the fake images in a variable
# the fake images go to the discriminator input
# of the adversarial
# for classification
# log the loss and accuracy
metrics = adversarial.train_on_batch([noise, fake_labels],
[y, fake_labels])
fmt = "%s [advr loss: %f, srcloss: %f, lblloss: %f, srcacc: %f, lblacc: %f]"
log = fmt % (log, metrics[0], metrics[1], metrics[2], metrics[3], metrics[4])
print(log)
if (i + 1) % save_interval == 0:
if (i + 1) == train_steps:
show = True
else:
show = False
# plot generator images on a periodic basis
gan.plot_images(generator,
noise_input=noise_input,
noise_label=noise_label,
show=show,
step=(i + 1),
model_name=model_name)
# save the model after training the generator
# the trained generator can be reloaded for
# future MNIST digit generation
generator.save(model_name + ".h5")
结果发现,增加了这个额外任务后,ACGAN 相比我们之前讨论过的所有 GAN,性能有了显著提升。ACGAN 的训练稳定性如图 5.3.3所示,展示了 ACGAN 生成的以下标签的样本输出:
[0 1 2 3
4 5 6 7
8 9 0 1
2 3 4 5]
与 CGAN 不同,样本输出的外观在训练过程中不会大幅变化。MNIST 数字图像的感知质量也更好。图 5.3.4 展示了由 CGAN 和 ACGAN 分别生成的每个 MNIST 数字的并排比较。数字 2 到 6 在 ACGAN 中的质量优于 CGAN。
我鼓励你运行训练好的生成器模型,以查看新的合成 MNIST 数字图像:
python3 acgan-mnist-5.3.1.py --generator=acgan_mnist.h5
另外,还可以请求生成特定的数字(例如,3):
python3 acgan-mnist-5.3.1.py --generator=acgan_mnist.h5 --digit=3
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_10.jpg
图 5.3.3:ACGAN 在训练步骤下生成的样本输出,标签为[0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5]
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_05_11.jpg
图 5.3.4:由 CGAN 和 ACGAN 生成的 0 至 9 数字输出并排比较
结论
在本章中,我们展示了对 GAN 原始算法的多种改进,这些算法在上一章中首次介绍。WGAN 提出了一种算法,通过使用 EMD 或 Wasserstein 1 损失来提高训练的稳定性。LSGAN 认为 GAN 原始的交叉熵函数容易导致梯度消失,而最小二乘损失则不同。LSGAN 提出了一种算法,实现了稳定的训练和高质量的输出。ACGAN 通过要求判别器除了判断输入图像是否为假图像或真实图像外,还需要执行分类任务,从而显著提高了 MNIST 数字条件生成的质量。
在下一章,我们将学习如何控制生成器输出的属性。尽管 CGAN 和 ACGAN 能够指示所需的数字进行生成,但我们尚未分析能够指定输出属性的 GAN。例如,我们可能希望控制 MNIST 数字的书写风格,如圆度、倾斜角度和粗细。因此,本章的目标是介绍具有解耦表示的 GAN,以控制生成器输出的特定属性。
参考文献
-
Ian Goodfellow 等人,生成对抗网络。神经信息处理系统进展,2014(
papers.nips.cc/paper/5423-generative-adversarial-nets.pdf
)。 -
Martin Arjovsky、Soumith Chintala 和 Léon Bottou,Wasserstein GAN。arXiv 预印本,2017(
arxiv.org/pdf/1701.07875.pdf
)。 -
Xudong Mao 等人。最小二乘生成对抗网络。2017 年 IEEE 计算机视觉国际会议(ICCV)。IEEE 2017 (
openaccess.thecvf.com/content_ICCV_2017/papers/Mao_Least_Squares_Generative_ICCV_2017_paper.pdf
)。 -
Augustus Odena、Christopher Olah 和 Jonathon Shlens。带有辅助分类器 GAN 的条件图像合成。ICML,2017 (
proceedings.mlr.press/v70/odena17a/odena17a.pdf
)。
第六章 解耦表示的 GANs
正如我们所探讨的,GAN 通过学习数据分布可以生成有意义的输出。然而,对于生成的输出属性并没有控制。一些 GAN 的变体,如条件 GAN(CGAN)和辅助分类器 GAN(ACGAN),如前一章所讨论的,能够训练一个受条件限制的生成器来合成特定的输出。例如,CGAN 和 ACGAN 都能引导生成器生成特定的 MNIST 数字。这是通过使用一个 100 维的噪声代码和相应的独热标签作为输入来实现的。然而,除了独热标签之外,我们没有其他方法来控制生成输出的属性。
注意
关于 CGAN 和 ACGAN 的回顾,请参见第四章,生成对抗网络(GANs),以及第五章,改进的 GANs。
在本章中,我们将介绍一些能够修改生成器输出的 GAN 变体。在 MNIST 数据集的背景下,除了生成哪个数字,我们可能还希望控制书写风格。这可能涉及到所需数字的倾斜度或宽度。换句话说,GAN 也可以学习解耦的潜在代码或表示,我们可以使用这些代码或表示来改变生成器输出的属性。解耦的代码或表示是一个张量,它可以改变输出数据的特定特征或属性,而不会影响其他属性。
本章的第一部分,我们将讨论InfoGAN:通过信息最大化生成对抗网络进行可解释表示学习 [1],这是一种 GAN 的扩展。InfoGAN 通过最大化输入代码和输出观察之间的互信息,以无监督的方式学习解耦的表示。在 MNIST 数据集上,InfoGAN 将书写风格与数字数据集解耦。
在本章的后续部分,我们还将讨论堆叠生成对抗网络(StackedGAN)[2],这是 GAN 的另一种扩展。StackedGAN 使用预训练的编码器或分类器来帮助解耦潜在代码。StackedGAN 可以视为一堆模型,每个模型由一个编码器和一个 GAN 组成。每个 GAN 通过使用相应编码器的输入和输出数据,以对抗的方式进行训练。
总结来说,本章的目标是展示:
-
解耦表示的概念
-
InfoGAN 和 StackedGAN 的原理
-
使用 Keras 实现 InfoGAN 和 StackedGAN
解耦表示
原始的 GAN 能够生成有意义的输出,但其缺点是无法进行控制。例如,如果我们训练一个 GAN 来学习名人面孔的分布,生成器会生成新的名人样貌的人物图像。然而,无法控制生成器生成我们想要的面孔的特定特征。例如,我们无法要求生成器生成一张女性名人面孔,长黑发,皮肤白皙,棕色眼睛,正在微笑。根本原因是我们使用的 100 维噪声代码将生成器输出的所有显著特征都缠结在一起。我们可以回忆起在 Keras 中,100 维代码是通过从均匀噪声分布中随机抽样生成的:
# generate 64 fake images from 64 x 100-dim uniform noise
noise = np.random.uniform(-1.0, 1.0, size=[64, 100])
fake_images = generator.predict(noise)
如果我们能够修改原始 GAN,使其能够将代码或表示分离为缠结和解耦的可解释潜在代码,我们将能够告诉生成器生成我们所需的内容。
接下来的图像展示了一个具有缠结代码的 GAN 及其结合缠结和解耦表示的变化。在假设的名人面孔生成背景下,使用解耦代码,我们可以指定我们希望生成的面孔的性别、发型、面部表情、肤色和眼睛颜色。n–dim 的缠结代码仍然用于表示我们尚未解耦的所有其他面部特征,例如面部形状、面部毛发、眼镜,仅举三个例子。缠结和解耦代码的拼接作为生成器的新输入。拼接代码的总维度不一定是 100:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_01.jpg
图 6.1.1:具有缠结代码的 GAN 以及其结合缠结和解耦代码的变化。此示例展示了名人面孔生成的背景。
从前面的图像来看,具有解耦表示的 GAN 也可以像普通的 GAN 一样进行优化。这是因为生成器的输出可以表示为:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_001.jpg
(方程式 6.1.1)
代码
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_002.jpg
由两个元素组成:
-
类似于 GAN 的不可压缩缠结噪声代码 z 或噪声向量。
-
潜在代码,c**1,c**2,…,c**L,表示数据分布的可解释解耦代码。所有潜在代码统一表示为 c。
为了简化起见,假设所有潜在代码都是独立的:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_003.jpg
(方程式 6.1.2)
生成器函数
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_004.jpg
提供了不可压缩的噪声代码和潜在代码。从生成器的角度来看,优化
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_005.jpg
之间的互信息与优化 z 相同。生成器网络在得出解决方案时会忽略由解耦代码施加的约束。生成器学习分布
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_006.jpg
。这将实际破坏解耦表示的目标。
InfoGAN
为了加强代码的解耦,InfoGAN 向原始损失函数中提出了一个正则化项,该项最大化潜在代码 c 和
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_007.jpg
:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_008.jpg
(方程 6.1.3)
该正则化项迫使生成器在构建合成假图像的函数时考虑潜在代码。在信息论领域,潜在代码 c 和
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_009.jpg
定义为:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_010.jpg
(方程 6.1.4)
其中 H(c) 是潜在代码 c 的熵,和
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_011.jpg
是观察到生成器输出后的 c 的条件熵,
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_012.jpg
。熵是衡量随机变量或事件不确定性的一个度量。例如,像 太阳从东方升起 这样的信息熵较低。而 中彩票中大奖 的熵则较高。
在 方程 6.1.4 中,最大化互信息意味着最小化
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_013.jpg
或者通过观察生成的输出减少潜在代码的不确定性。这是有道理的,例如,在 MNIST 数据集中,如果 GAN 看到它观察到数字 8,生成器会对合成数字 8 更有信心。
然而,估计它是很难的
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_014.jpg
因为它需要了解后验知识
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_015.jpg
,而这是我们无法访问的。解决方法是通过估计辅助分布 Q(c|x) 来估算互信息的下界。InfoGAN 通过以下方式估算互信息的下界:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_016.jpg
(方程 6.1.5)
在 InfoGAN 中,H(c)被假定为常数。因此,最大化互信息就是最大化期望值。生成器必须确信它已经生成了具有特定属性的输出。我们应该注意到,这个期望值的最大值是零。因此,互信息下界的最大值是 H(c)。在 InfoGAN 中,离散潜在代码的 Q(c|x) 可以通过 softmax 非线性表示。期望值是 Keras 中负的 categorical_crossentropy
损失。
对于单维连续代码,期望是对 c 和 x 的双重积分。这是因为期望从解缠代码分布和生成器分布中采样。估计期望的一种方法是假设样本是连续数据的良好度量。因此,损失被估计为 c log Q(c|x)。
为了完成 InfoGAN 网络,我们应该有一个 Q(c|x) 的实现。为了简单起见,网络 Q 是附加在判别器倒数第二层的辅助网络。因此,这对原始 GAN 的训练影响最小。下图展示了 InfoGAN 的网络图:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_02.jpg
图 6.1.2:展示 InfoGAN 中判别器和生成器训练的网络图
下表展示了 InfoGAN 相对于原始 GAN 的损失函数。InfoGAN 的损失函数相比原始 GAN 多了一个额外的项
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_017.jpg
其中
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_018.jpg
是一个小的正常数。最小化 InfoGAN 的损失函数意味着最小化原始 GAN 的损失并最大化互信息
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_019.jpg
.
| InfoGAN | https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_022.jpghttps://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_023.jpg对于连续代码,InfoGAN 推荐一个值为https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_024.jpg。在我们的示例中,我们设置为https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_025.jpg。对于离散代码,InfoGAN 推荐https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_026.jpg
表 6.1.1:GAN 和 InfoGAN 损失函数的比较
. | 6.1.16.1.2 |
如果应用于 MNIST 数据集,InfoGAN 可以学习解缠的离散和连续代码,从而修改生成器的输出属性。例如,像 CGAN 和 ACGAN 一样,离散代码以 10 维独热标签的形式用于指定要生成的数字。然而,我们可以添加两个连续代码,一个用于控制书写风格的角度,另一个用于调整笔画宽度。下图展示了 InfoGAN 中 MNIST 数字的代码。我们保留较小维度的纠缠代码来表示所有其他属性:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_03.jpg
图 6.1.3:在 MNIST 数据集背景下 GAN 和 InfoGAN 的代码
InfoGAN 在 Keras 中的实现
为了在 MNIST 数据集上实现 InfoGAN,需要对 ACGAN 的基础代码进行一些修改。如以下列表所示,生成器将纠缠的(z 噪声代码)和解缠的代码(独热标签和连续代码)拼接起来作为输入。生成器和判别器的构建函数也在 lib
文件夹中的 gan.py
中实现。
注意
完整的代码可以在 GitHub 上找到:
github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras
列表 6.1.1,infogan-mnist-6.1.1.py
展示了 InfoGAN 生成器如何将纠缠的和解耦的代码连接在一起作为输入:
def generator(inputs,
image_size,
activation='sigmoid',
labels=None,
codes=None):
"""Build a Generator Model
Stack of BN-ReLU-Conv2DTranpose to generate fake images.
Output activation is sigmoid instead of tanh in [1].
Sigmoid converges easily.
# Arguments
inputs (Layer): Input layer of the generator (the z-vector)
image_size (int): Target size of one side (assuming square image)
activation (string): Name of output activation layer
labels (tensor): Input labels
codes (list): 2-dim disentangled codes for InfoGAN
# Returns
Model: Generator Model
"""
image_resize = image_size // 4
# network parameters
kernel_size = 5
layer_filters = [128, 64, 32, 1]
if labels is not None:
if codes is None:
# ACGAN labels
# concatenate z noise vector and one-hot labels
inputs = [inputs, labels]
else:
# infoGAN codes
# concatenate z noise vector, one-hot labels,
# and codes 1 & 2
inputs = [inputs, labels] + codes
x = concatenate(inputs, axis=1)
elif codes is not None:
# generator 0 of StackedGAN
inputs = [inputs, codes]
x = concatenate(inputs, axis=1)
else:
# default input is just 100-dim noise (z-code)
x = inputs
x = Dense(image_resize * image_resize * layer_filters[0])(x)
x = Reshape((image_resize, image_resize, layer_filters[0]))(x)
for filters in layer_filters:
# first two convolution layers use strides = 2
# the last two use strides = 1
if filters > layer_filters[-2]:
strides = 2
else:
strides = 1
x = BatchNormalization()(x)
x = Activation('relu')(x)
x = Conv2DTranspose(filters=filters,
kernel_size=kernel_size,
strides=strides,
padding='same')(x)
if activation is not None:
x = Activation(activation)(x)
# generator output is the synthesized image x
return Model(inputs, x, name='generator')
上面的列表展示了带有原始默认 GAN 输出的判别器和Q网络。突出了与离散代码(用于一热标签)softmax
预测和给定输入 MNIST 数字图像的连续代码概率相对应的三个辅助输出。
列表 6.1.2,infogan-mnist-6.1.1.py
。InfoGAN 判别器和Q网络:
def discriminator(inputs,
activation='sigmoid',
num_labels=None,
num_codes=None):
"""Build a Discriminator Model
Stack of LeakyReLU-Conv2D to discriminate real from fake
The network does not converge with BN so it is not used here
unlike in [1]
# Arguments
inputs (Layer): Input layer of the discriminator (the image)
activation (string): Name of output activation layer
num_labels (int): Dimension of one-hot labels for ACGAN & InfoGAN
num_codes (int): num_codes-dim Q network as output
if StackedGAN or 2 Q networks if InfoGAN
# Returns
Model: Discriminator Model
"""
kernel_size = 5
layer_filters = [32, 64, 128, 256]
x = inputs
for filters in layer_filters:
# first 3 convolution layers use strides = 2
# last one uses strides = 1
if filters == layer_filters[-1]:
strides = 1
else:
strides = 2
x = LeakyReLU(alpha=0.2)(x)
x = Conv2D(filters=filters,
kernel_size=kernel_size,
strides=strides,
padding='same')(x)
x = Flatten()(x)
# default output is probability that the image is real
outputs = Dense(1)(x)
if activation is not None:
print(activation)
outputs = Activation(activation)(outputs)
if num_labels:
# ACGAN and InfoGAN have 2nd output
# 2nd output is 10-dim one-hot vector of label
layer = Dense(layer_filters[-2])(x)
labels = Dense(num_labels)(layer)
labels = Activation('softmax', name='label')(labels)
if num_codes is None:
outputs = [outputs, labels]
else:
# InfoGAN have 3rd and 4th outputs
# 3rd output is 1-dim continous Q of 1st c given x
code1 = Dense(1)(layer)
code1 = Activation('sigmoid', name='code1')(code1)
# 4th output is 1-dim continuous Q of 2nd c given x
code2 = Dense(1)(layer)
code2 = Activation('sigmoid', name='code2')(code2)
outputs = [outputs, labels, code1, code2]
elif num_codes is not None:
# StackedGAN Q0 output
# z0_recon is reconstruction of z0 normal distribution
z0_recon = Dense(num_codes)(x)
z0_recon = Activation('tanh', name='z0')(z0_recon)
outputs = [outputs, z0_recon]
return Model(inputs, outputs, name='discriminator')
图 6.1.4展示了 Keras 中的 InfoGAN 模型。构建判别器和对抗模型还需要一些更改。更改主要体现在所使用的损失函数上。原始的判别器损失函数是binary_crossentropy
,用于离散代码的categorical_crossentropy
,以及针对每个连续代码的mi_loss
函数,构成了整体损失函数。每个损失函数的权重为 1.0,除了mi_loss
函数,其权重为 0.5,适用于https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_027.jpg连续代码。
列表 6.1.3突出了所做的更改。然而,我们应该注意到,通过使用构建器函数,判别器的实例化方式如下:
# call discriminator builder with 4 outputs: source, label,
# and 2 codes
discriminator = gan.discriminator(inputs, num_labels=num_labels, with_codes=True)
生成器是通过以下方式创建的:
# call generator with inputs, labels and codes as total inputs
# to generator
generator = gan.generator(inputs, image_size, labels=labels, codes=[code1, code2])
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_04.jpg
图 6.1.4:InfoGAN Keras 模型
列表 6.1.3,infogan-mnist-6.1.1.py
展示了在构建 InfoGAN 判别器和对抗网络时使用的互信息损失函数:
def mi_loss(c, q_of_c_given_x):
""" Mutual information, Equation 5 in [2], assuming H(c) is constant"""
# mi_loss = -c * log(Q(c|x))
return K.mean(-K.sum(K.log(q_of_c_given_x + K.epsilon()) * c, axis=1))
def build_and_train_models(latent_size=100):
# load MNIST dataset
(x_train, y_train), (_, _) = mnist.load_data()
# reshape data for CNN as (28, 28, 1) and normalize
image_size = x_train.shape[1]
x_train = np.reshape(x_train, [-1, image_size, image_size, 1])
x_train = x_train.astype('float32') / 255
# train labels
num_labels = len(np.unique(y_train))
y_train = to_categorical(y_train)
model_name = "infogan_mnist"
# network parameters
batch_size = 64
train_steps = 40000
lr = 2e-4
decay = 6e-8
input_shape = (image_size, image_size, 1)
label_shape = (num_labels, )
code_shape = (1, )
# build discriminator model
inputs = Input(shape=input_shape, name='discriminator_input')
# call discriminator builder with 4 outputs:
# source, label, and 2 codes
discriminator = gan.discriminator(inputs,
num_labels=num_labels,
num_codes=2)
# [1] uses Adam, but discriminator converges easily with RMSprop
optimizer = RMSprop(lr=lr, decay=decay)
# loss functions: 1) probability image is real (binary crossentropy)
# 2) categorical cross entropy image label,
# 3) and 4) mutual information loss
loss = ['binary_crossentropy', 'categorical_crossentropy', mi_loss, mi_loss]
# lamda or mi_loss weight is 0.5
loss_weights = [1.0, 1.0, 0.5, 0.5]
discriminator.compile(loss=loss,
loss_weights=loss_weights,
optimizer=optimizer,
metrics=['accuracy'])
discriminator.summary()
# build generator model
input_shape = (latent_size, )
inputs = Input(shape=input_shape, name='z_input')
labels = Input(shape=label_shape, name='labels')
code1 = Input(shape=code_shape, name="code1")
code2 = Input(shape=code_shape, name="code2")
# call generator with inputs,
# labels and codes as total inputs to generator
generator = gan.generator(inputs,
image_size,
labels=labels,
codes=[code1, code2])
generator.summary()
# build adversarial model = generator + discriminator
optimizer = RMSprop(lr=lr*0.5, decay=decay*0.5)
discriminator.trainable = False
# total inputs = noise code, labels, and codes
inputs = [inputs, labels, code1, code2]
adversarial = Model(inputs,
discriminator(generator(inputs)),
name=model_name)
# same loss as discriminator
adversarial.compile(loss=loss,
loss_weights=loss_weights,
optimizer=optimizer,
metrics=['accuracy'])
adversarial.summary()
# train discriminator and adversarial networks
models = (generator, discriminator, adversarial)
data = (x_train, y_train)
params = (batch_size, latent_size, train_steps, num_labels, model_name)
train(models, data, params)
就训练而言,我们可以看到 InfoGAN 与 ACGAN 类似,唯一的区别是我们需要为连续代码提供c。c来自标准差为 0.5、均值为 0.0 的正态分布。对于假数据,我们将使用随机采样的标签,而对于真实数据,我们将使用数据集类标签来表示离散潜在代码。以下列表突出了在训练函数中所做的更改。与之前的所有 GAN 类似,判别器和生成器(通过对抗)交替训练。在对抗训练期间,判别器的权重被冻结。每 500 步间隔使用gan.py plot_images()
函数保存生成器输出的样本图像。
列表 6.1.4,infogan-mnist-6.1.1.py
展示了 InfoGAN 的训练函数如何类似于 ACGAN。唯一的区别是我们提供从正态分布中采样的连续代码:
def train(models, data, params):
"""Train the Discriminator and Adversarial networks
Alternately train discriminator and adversarial networks by batch.
Discriminator is trained first with real and fake images,
corresponding one-hot labels and continuous codes.
Adversarial is trained next with fake images pretending to be real,
corresponding one-hot labels and continous codes.
Generate sample images per save_interval.
# Arguments
models (Models): Generator, Discriminator, Adversarial models
data (tuple): x_train, y_train data
params (tuple): Network parameters
"""
# the GAN models
generator, discriminator, adversarial = models
# images and their one-hot labels
x_train, y_train = data
# network parameters
batch_size, latent_size, train_steps, num_labels, model_name = params
# the generator image is saved every 500 steps
save_interval = 500
# noise vector to see how the generator output evolves
# during training
noise_input = np.random.uniform(-1.0, 1.0, size=[16, latent_size])
# random class labels and codes
noise_label = np.eye(num_labels)[np.arange(0, 16) % num_labels]
noise_code1 = np.random.normal(scale=0.5, size=[16, 1])
noise_code2 = np.random.normal(scale=0.5, size=[16, 1])
# number of elements in train dataset
train_size = x_train.shape[0]
print(model_name,
"Labels for generated images: ",
np.argmax(noise_label, axis=1))
for i in range(train_steps):
# train the discriminator for 1 batch
# 1 batch of real (label=1.0) and fake images (label=0.0)
# randomly pick real images and corresponding labels from dataset
rand_indexes = np.random.randint(0, train_size, size=batch_size)
real_images = x_train[rand_indexes]
real_labels = y_train[rand_indexes]
# random codes for real images
real_code1 = np.random.normal(scale=0.5, size=[batch_size, 1])
real_code2 = np.random.normal(scale=0.5, size=[batch_size, 1])
# generate fake images, labels and codes
noise = np.random.uniform(-1.0, 1.0, size=[batch_size, latent_size])
fake_labels = np.eye(num_labels)[np.random.choice(num_labels,
batch_size)]
fake_code1 = np.random.normal(scale=0.5, size=[batch_size, 1])
fake_code2 = np.random.normal(scale=0.5, size=[batch_size, 1])
inputs = [noise, fake_labels, fake_code1, fake_code2]
fake_images = generator.predict(inputs)
# real + fake images = 1 batch of train data
x = np.concatenate((real_images, fake_images))
labels = np.concatenate((real_labels, fake_labels))
codes1 = np.concatenate((real_code1, fake_code1))
codes2 = np.concatenate((real_code2, fake_code2))
# label real and fake images
# real images label is 1.0
y = np.ones([2 * batch_size, 1])
# fake images label is 0.0
y[batch_size:, :] = 0
# train discriminator network, log the loss and label accuracy
outputs = [y, labels, codes1, codes2]
# metrics = ['loss', 'activation_1_loss', 'label_loss',
# 'code1_loss', 'code2_loss', 'activation_1_acc',
# 'label_acc', 'code1_acc', 'code2_acc']
# from discriminator.metrics_names
metrics = discriminator.train_on_batch(x, outputs)
fmt = "%d: [discriminator loss: %f, label_acc: %f]"
log = fmt % (i, metrics[0], metrics[6])
# train the adversarial network for 1 batch
# 1 batch of fake images with label=1.0 and
# corresponding one-hot label or class + random codes
# since the discriminator weights are frozen in
# adversarial network only the generator is trained
# generate fake images, labels and codes
noise = np.random.uniform(-1.0, 1.0, size=[batch_size, latent_size])
fake_labels = np.eye(num_labels)[np.random.choice(num_labels,
batch_size)]
fake_code1 = np.random.normal(scale=0.5, size=[batch_size, 1])
fake_code2 = np.random.normal(scale=0.5, size=[batch_size, 1])
# label fake images as real
y = np.ones([batch_size, 1])
# note that unlike in discriminator training,
# we do not save the fake images in a variable
# the fake images go to the discriminator input of the
# adversarial for classification
# log the loss and label accuracy
inputs = [noise, fake_labels, fake_code1, fake_code2]
outputs = [y, fake_labels, fake_code1, fake_code2]
metrics = adversarial.train_on_batch(inputs, outputs)
fmt = "%s [adversarial loss: %f, label_acc: %f]"
log = fmt % (log, metrics[0], metrics[6])
print(log)
if (i + 1) % save_interval == 0:
if (i + 1) == train_steps:
show = True
else:
show = False
# plot generator images on a periodic basis
gan.plot_images(generator,
noise_input=noise_input,
noise_label=noise_label,
noise_codes=[noise_code1, noise_code2],
show=show,
step=(i + 1),
model_name=model_name)
# save the model after training the generator
# the trained generator can be reloaded for
# future MNIST digit generation
generator.save(model_name + ".h5")
InfoGAN 的生成器输出
类似于我们之前介绍的所有 GAN,我们已将 InfoGAN 训练了 40,000 步。训练完成后,我们可以运行 InfoGAN 生成器,利用保存在infogan_mnist.h5
文件中的模型生成新的输出。以下是进行的验证:
-
通过将离散标签从 0 到 9 变化,生成数字 0 到 9. 两个连续编码都设置为零。结果如 图 6.1.5 所示。我们可以看到,InfoGAN 的离散编码能够控制生成器生成的数字:
python3 infogan-mnist-6.1.1.py --generator=infogan_mnist.h5 --digit=0 --code1=0 --code2=0
到
python3 infogan-mnist-6.1.1.py --generator=infogan_mnist.h5 --digit=9 --code1=0 --code2=0
-
检查第一个连续编码的效果,了解哪个属性受到了影响。我们将第一个连续编码从 -2.0 变化到 2.0,数字从 0 到 9. 第二个连续编码设置为 0.0. 图 6.1.6 显示第一个连续编码控制数字的粗细:
python3 infogan-mnist-6.1.1.py --generator=infogan_mnist.h5 --digit=0 --code1=0 --code2=0 --p1
-
与前一步骤类似,但重点更多放在第二个连续编码上。图 6.1.7 显示第二个连续编码控制书写风格的旋转角度(倾斜):
python3 infogan-mnist-6.1.1.py --generator=infogan_mnist.h5 --digit=0 --code1=0 --code2=0 --p2
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_06.jpg
图 6.1.5:InfoGAN 生成的图像,当离散编码从 0 变化到 9 时。两个连续编码都设置为零。
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_07.jpg
图 6.1.6:InfoGAN 生成的图像,当第一个连续编码从 -2.0 变化到 2.0 时,数字从 0 到 9. 第二个连续编码设置为零。第一个连续编码控制数字的粗细。
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_08.jpg
图 6.1.7:InfoGAN 生成的图像,当第二个连续编码从 -2.0 变化到 2.0 时,数字从 0 到 9. 第一个连续编码设置为零。第二个连续编码控制书写风格的旋转角度(倾斜)。
从这些验证结果中,我们可以看到,除了能够生成类似 MNIST 的数字外,InfoGAN 扩展了条件 GAN(如 CGAN 和 ACGAN)的能力。网络自动学习了两个任意编码,可以控制生成器输出的特定属性。如果我们将连续编码的数量增加到超过 2 个,看看还能控制哪些其他属性,将会很有趣。
StackedGAN
与 InfoGAN 同样的理念,StackedGAN 提出了通过分解潜在表示来调整生成器输出条件的方法。然而,StackedGAN 采用了不同的方式来解决这一问题。StackedGAN 并非学习如何调整噪声以产生所需的输出,而是将 GAN 拆解成一堆 GAN。每个 GAN 都以通常的鉴别器对抗方式独立训练,并拥有自己的潜在编码。
图 6.2.1 向我们展示了 StackedGAN 如何在假设的名人面部生成背景下工作。假设 编码器 网络经过训练,能够分类名人面孔。
编码器 网络由一堆简单的编码器组成,编码器 i 其中 i = 0 … n - 1 对应于 n 个特征。每个编码器提取某些面部特征。例如,编码器[0] 可能是用于发型特征的编码器,特征1。所有简单的编码器共同作用,使得整个 编码器 能正确预测。
StackedGAN 背后的思想是,如果我们想要构建一个生成虚假名人面孔的 GAN,我们应该简单地反转编码器。StackedGAN 由一堆简单的 GAN 组成,GAN[i],其中 i = 0 … n - 1 对应n个特征。每个 GAN[i]学习反转其对应编码器编码器[i]的过程。例如,GAN[0]从虚假的发型特征生成虚假的名人面孔,这是编码器[0]过程的反转。
每个GAN[i]使用一个潜在代码z[i],它决定生成器的输出。例如,潜在代码z[0]可以将发型从卷发改变为波浪发型。GAN 堆叠也可以作为一个整体,用来合成虚假的名人面孔,完成整个编码器的反向过程。每个GAN[i]的潜在代码z[i]可用于改变虚假名人面孔的特定属性:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_09.jpg
图 6.2.1:在生成名人面孔的背景下,StackedGAN 的基本思想。假设存在一个假设的深度编码器网络,能够对名人面孔进行分类,StackedGAN 只是反转编码器的过程。
在 Keras 中实现 StackedGAN
StackedGAN 的详细网络模型可以在下图中看到。为了简洁起见,每个堆叠中仅显示了两个编码器-GAN。图看起来可能很复杂,但它只是编码器-GAN 的重复。换句话说,如果我们理解了如何训练一个编码器-GAN,那么其他的也遵循相同的概念。在接下来的部分中,我们假设 StackedGAN 是为 MNIST 数字生成设计的:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_10.jpg
图 6.2.2:StackedGAN 由编码器和 GAN 的堆叠组成。编码器经过预训练,用于执行分类任务。生成器[1],G[1],学习基于虚假标签y[f]和潜在代码z[1][f]合成f[1][f]特征。生成器[0],G[0],使用虚假特征f[1][f]和潜在代码z[0][f]生成虚假图像。
StackedGAN 以编码器开始。它可以是一个经过训练的分类器,用于预测正确的标签。中间特征向量f[1][r]可用于 GAN 训练。对于 MNIST,我们可以使用类似于第一章中讨论的基于 CNN 的分类器,使用 Keras 介绍深度学习。下图显示了编码器及其在 Keras 中的网络模型实现:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_11.jpg
图 6.2.3:StackedGAN 中的编码器是一个简单的基于 CNN 的分类器
列表 6.2.1展示了前图的 Keras 代码。它类似于第一章中的基于 CNN 的分类器,Keras 的高级深度学习介绍,除了我们使用Dense
层提取 256 维特征。这里有两个输出模型,Encoder[0]和Encoder[1]。两者都将用于训练 StackedGAN。
Encoder[0]的输出,f[0][r],是我们希望Generator[1]学习合成的 256 维特征向量。它作为Encoder[0]的辅助输出,E[0]。整体的Encoder被训练用于分类 MNIST 数字,x [r]。正确的标签,y [r],由Encoder[1],E[1]预测。在此过程中,中间特征集,f[1]r,被学习并可用于Generator[0]的训练。在训练 GAN 时,子脚本r用于强调并区分真实数据与假数据。
列表 6.2.1,stackedgan-mnist-6.2.1.py
展示了在 Keras 中实现的编码器:
def build_encoder(inputs, num_labels=10, feature1_dim=256):
""" Build the Classifier (Encoder) Model sub networks
Two sub networks:
1) Encoder0: Image to feature1 (intermediate latent feature)
2) Encoder1: feature1 to labels
# Arguments
inputs (Layers): x - images, feature1 - feature1 layer output
num_labels (int): number of class labels
feature1_dim (int): feature1 dimensionality
# Returns
enc0, enc1 (Models): Description below
"""
kernel_size = 3
filters = 64
x, feature1 = inputs
# Encoder0 or enc0
y = Conv2D(filters=filters,
kernel_size=kernel_size,
padding='same',
activation='relu')(x)
y = MaxPooling2D()(y)
y = Conv2D(filters=filters,
kernel_size=kernel_size,
padding='same',
activation='relu')(y)
y = MaxPooling2D()(y)
y = Flatten()(y)
feature1_output = Dense(feature1_dim, activation='relu')(y)
# Encoder0 or enc0: image to feature1
enc0 = Model(inputs=x, outputs=feature1_output, name="encoder0")
# Encoder1 or enc1
y = Dense(num_labels)(feature1)
labels = Activation('softmax')(y)
# Encoder1 or enc1: feature1 to class labels
enc1 = Model(inputs=feature1, outputs=labels, name="encoder1")
# return both enc0 and enc1
return enc0, enc1
表 6.2.1:GAN 与 StackedGAN 损失函数的比较。~p [data]表示从相应的编码器数据(输入、特征或输出)中采样。
给定Encoder输入(x[r])中间特征(f1r)和标签(y r),每个 GAN 按照常规的鉴别器—对抗性方式进行训练。损失函数由方程 6.2.1至6.2.5在表 6.2.1中给出。方程6.2.1和6.2.2是通用 GAN 的常规损失函数。StackedGAN 有两个额外的损失函数,条件和熵。
条件损失函数,
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_037.jpg
在方程 6.2.3中,确保生成器在合成输出f[i]时不会忽略输入f[i+1],即使在输入噪声代码z[i]的情况下。编码器,编码器[i],必须能够通过逆转生成器生成器[i]的过程来恢复生成器输入。生成器输入与通过编码器恢复的输入之间的差异由L2或欧几里得距离均方误差(MSE)衡量。图 6.2.4展示了参与计算的网络元素!StackedGAN 在 Keras 中的实现:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_12.jpg
图 6.2.4:图 6.2.3 的简化版本,仅显示参与计算的网络元素!StackedGAN 在 Keras 中的实现
然而,条件损失函数引入了一个新问题。生成器忽略输入的噪声代码,z i,并仅依赖于f [i+1]。熵损失函数,
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_40.jpg
在方程 6.2.4中,确保生成器不忽略噪声代码,z i。Q网络从生成器的输出中恢复噪声代码。恢复的噪声与输入噪声之间的差异也通过L2或均方误差(MSE)进行测量。下图展示了参与计算的网络元素
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_041.jpg
:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_13.jpg
图 6.2.5:图 6.2.3 的简化版本,仅展示了参与计算的网络元素
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_042.jpg
最后一个损失函数与通常的 GAN 损失相似。它由一个判别器损失组成
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_043.jpg
以及一个生成器(通过对抗)损失
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_044.jpg
下图展示了我们 GAN 损失中涉及的元素:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_14.jpg
图 6.2.6:图 6.2.3 的简化版本,仅展示了参与计算的网络元素
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_045.jpg
和
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_46.jpg
在方程 6.2.5中,三个生成器损失函数的加权和是最终的生成器损失函数。在我们将要展示的 Keras 代码中,所有权重都设置为 1.0,除了熵损失设置为 10.0。方程 6.2.1至方程 6.2.5中,i表示编码器和 GAN 组 ID 或层级。在原始论文中,网络首先独立训练,然后进行联合训练。在独立训练期间,先训练编码器。在联合训练期间,使用真实数据和伪造数据。
StackedGAN 的生成器和判别器的实现仅需对 Keras 作少量修改,以便提供辅助点以访问中间特征。图 6.2.7展示了生成器 Keras 模型。列表 6.2.2阐明了构建两个生成器的函数(gen0
和 gen1
),它们分别对应 生成器0 和 生成器1。gen1
生成器由三个Dense
层组成,标签和噪声编码 z1f 作为输入。第三层生成伪造的 f[1]f 特征。gen0
生成器与我们之前介绍的其他 GAN 生成器相似,可以通过gan.py
中的生成器构建器进行实例化:
# gen0: feature1 + z0 to feature0 (image)
gen0 = gan.generator(feature1, image_size, codes=z0)
gen0
输入是 f 特征和噪声编码 z。[0] 输出是生成的伪造图像,x[f]:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_15.jpg
图 6.2.7:Keras 中的 StackedGAN 生成器模型
列表 6.2.2,stackedgan-mnist-6.2.1.py
展示了我们在 Keras 中实现生成器的代码:
def build_generator(latent_codes, image_size, feature1_dim=256):
"""Build Generator Model sub networks
Two sub networks: 1) Class and noise to feature1 (intermediate feature)
2) feature1 to image
# Arguments
latent_codes (Layers): discrete code (labels), noise and feature1 features
image_size (int): Target size of one side (assuming square image)
feature1_dim (int): feature1 dimensionality
# Returns
gen0, gen1 (Models): Description below
"""
# Latent codes and network parameters
labels, z0, z1, feature1 = latent_codes
# image_resize = image_size // 4
# kernel_size = 5
# layer_filters = [128, 64, 32, 1]
# gen1 inputs
inputs = [labels, z1] # 10 + 50 = 62-dim
x = concatenate(inputs, axis=1)
x = Dense(512, activation='relu')(x)
x = BatchNormalization()(x)
x = Dense(512, activation='relu')(x)
x = BatchNormalization()(x)
fake_feature1 = Dense(feature1_dim, activation='relu')(x)
# gen1: classes and noise (feature2 + z1) to feature1
gen1 = Model(inputs, fake_feature1, name='gen1')
# gen0: feature1 + z0 to feature0 (image)
gen0 = gan.generator(feature1, image_size, codes=z0)
return gen0, gen1
图 6.2.8展示了判别器 Keras 模型。我们提供了构建 判别器[0] 和 判别器[1] (dis0
和 dis1
) 的函数。dis0
判别器与 GAN 判别器相似,只是输入是特征向量,并且有辅助网络 Q[0] 来恢复 z[0]。gan.py
中的构建器函数用于创建 dis0
:
dis0 = gan.discriminator(inputs, num_codes=z_dim)
dis1
判别器由三层 MLP 组成,如列表 6.2.3所示。最后一层用于区分真实与伪造的 f[1]。Q[1] 网络共享 dis1
的前两层。其第三层恢复 z[1]:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_16.jpg
图 6.2.8:Keras 中的 StackedGAN 判别器模型
列表 6.2.3,stackedgan-mnist-6.2.1.py
展示了判别器[1]在 Keras 中的实现:
def build_discriminator(inputs, z_dim=50):
"""Build Discriminator 1 Model
Classifies feature1 (features) as real/fake image and recovers
the input noise or latent code (by minimizing entropy loss)
# Arguments
inputs (Layer): feature1
z_dim (int): noise dimensionality
# Returns
dis1 (Model): feature1 as real/fake and recovered latent code
"""
# input is 256-dim feature1
x = Dense(256, activation='relu')(inputs)
x = Dense(256, activation='relu')(x)
# first output is probability that feature1 is real
f1_source = Dense(1)(x)
f1_source = Activation('sigmoid', name='feature1_source')(f1_source)
# z1 reonstruction (Q1 network)
z1_recon = Dense(z_dim)(x)
z1_recon = Activation('tanh', name='z1')(z1_recon)
discriminator_outputs = [f1_source, z1_recon]
dis1 = Model(inputs, discriminator_outputs, name='dis1')
return dis1
所有构建器函数可用后,StackedGAN 在列表 6.2.4 中组装完成。在训练 StackedGAN 之前,需要先预训练编码器。注意,我们已经将三个生成器损失函数(对抗性、条件性和熵)融入到对抗模型训练中。Q-Network 与判别器模型共享一些公共层。因此,它的损失函数也会在判别器模型训练中包含。
列表 6.2.4,stackedgan-mnist-6.2.1.py
。在 Keras 中构建 StackedGAN:
def build_and_train_models():
# load MNIST dataset
(x_train, y_train), (x_test, y_test) = mnist.load_data()
# reshape and normalize images
image_size = x_train.shape[1]
x_train = np.reshape(x_train, [-1, image_size, image_size, 1])
x_train = x_train.astype('float32') / 255
x_test = np.reshape(x_test, [-1, image_size, image_size, 1])
x_test = x_test.astype('float32') / 255
# number of labels
num_labels = len(np.unique(y_train))
# to one-hot vector
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
model_name = "stackedgan_mnist"
# network parameters
batch_size = 64
train_steps = 40000
lr = 2e-4
decay = 6e-8
input_shape = (image_size, image_size, 1)
label_shape = (num_labels, )
z_dim = 50
z_shape = (z_dim, )
feature1_dim = 256
feature1_shape = (feature1_dim, )
# build discriminator 0 and Q network 0 models
inputs = Input(shape=input_shape, name='discriminator0_input')
dis0 = gan.discriminator(inputs, num_codes=z_dim)
# [1] uses Adam, but discriminator converges easily with RMSprop
optimizer = RMSprop(lr=lr, decay=decay)
# loss fuctions: 1) probability image is real (adversarial0 loss)
# 2) MSE z0 recon loss (Q0 network loss or entropy0 loss)
loss = ['binary_crossentropy', 'mse']
loss_weights = [1.0, 10.0]
dis0.compile(loss=loss,
loss_weights=loss_weights,
optimizer=optimizer,
metrics=['accuracy'])
dis0.summary() # image discriminator, z0 estimator
# build discriminator 1 and Q network 1 models
input_shape = (feature1_dim, )
inputs = Input(shape=input_shape, name='discriminator1_input')
dis1 = build_discriminator(inputs, z_dim=z_dim )
# loss fuctions: 1) probability feature1 is real (adversarial1 loss)
# 2) MSE z1 recon loss (Q1 network loss or entropy1 loss)
loss = ['binary_crossentropy', 'mse']
loss_weights = [1.0, 1.0]
dis1.compile(loss=loss,
loss_weights=loss_weights,
optimizer=optimizer,
metrics=['accuracy'])
dis1.summary() # feature1 discriminator, z1 estimator
# build generator models
feature1 = Input(shape=feature1_shape, name='feature1_input')
labels = Input(shape=label_shape, name='labels')
z1 = Input(shape=z_shape, name="z1_input")
z0 = Input(shape=z_shape, name="z0_input")
latent_codes = (labels, z0, z1, feature1)
gen0, gen1 = build_generator(latent_codes, image_size)
gen0.summary() # image generator
gen1.summary() # feature1 generator
# build encoder models
input_shape = (image_size, image_size, 1)
inputs = Input(shape=input_shape, name='encoder_input')
enc0, enc1 = build_encoder((inputs, feature1), num_labels)
enc0.summary() # image to feature1 encoder
enc1.summary() # feature1 to labels encoder (classifier)
encoder = Model(inputs, enc1(enc0(inputs)))
encoder.summary() # image to labels encoder (classifier)
data = (x_train, y_train), (x_test, y_test)
train_encoder(encoder, data, model_name=model_name)
# build adversarial0 model =
# generator0 + discriminator0 + encoder0
optimizer = RMSprop(lr=lr*0.5, decay=decay*0.5)
# encoder0 weights frozen
enc0.trainable = False
# discriminator0 weights frozen
dis0.trainable = False
gen0_inputs = [feature1, z0]
gen0_outputs = gen0(gen0_inputs)
adv0_outputs = dis0(gen0_outputs) + [enc0(gen0_outputs)]
# feature1 + z0 to prob feature1 is
# real + z0 recon + feature0/image recon
adv0 = Model(gen0_inputs, adv0_outputs, name="adv0")
# loss functions: 1) prob feature1 is real (adversarial0 loss)
# 2) Q network 0 loss (entropy0 loss)
# 3) conditional0 loss
loss = ['binary_crossentropy', 'mse', 'mse']
loss_weights = [1.0, 10.0, 1.0]
adv0.compile(loss=loss,
loss_weights=loss_weights,
optimizer=optimizer,
metrics=['accuracy'])
adv0.summary()
# build adversarial1 model =
# generator1 + discriminator1 + encoder1
# encoder1 weights frozen
enc1.trainable = False
# discriminator1 weights frozen
dis1.trainable = False
gen1_inputs = [labels, z1]
gen1_outputs = gen1(gen1_inputs)
adv1_outputs = dis1(gen1_outputs) + [enc1(gen1_outputs)]
# labels + z1 to prob labels are real + z1 recon + feature1 recon
adv1 = Model(gen1_inputs, adv1_outputs, name="adv1")
# loss functions: 1) prob labels are real (adversarial1 loss)
# 2) Q network 1 loss (entropy1 loss)
# 3) conditional1 loss (classifier error)
loss_weights = [1.0, 1.0, 1.0]
loss = ['binary_crossentropy', 'mse', 'categorical_crossentropy']
adv1.compile(loss=loss,
loss_weights=loss_weights,
optimizer=optimizer,
metrics=['accuracy'])
adv1.summary()
# train discriminator and adversarial networks
models = (enc0, enc1, gen0, gen1, dis0, dis1, adv0, adv1)
params = (batch_size, train_steps, num_labels, z_dim, model_name)
train(models, data, params)
最后,训练函数与典型的 GAN 训练相似,只是我们一次只训练一个 GAN(即 GAN[1] 然后是 GAN[0])。代码显示在列表 6.2.5中。值得注意的是,训练顺序是:
-
判别器[1] 和 Q[1] 网络通过最小化判别器和熵损失
-
判别器[0] 和 Q[0] 网络通过最小化判别器和熵损失
-
对抗性网络通过最小化对抗性、熵和条件损失
-
对抗性网络通过最小化对抗性、熵和条件损失
列表 6.2.5,stackedgan-mnist-6.2.1.py
展示了我们在 Keras 中训练 StackedGAN 的代码:
def train(models, data, params):
"""Train the discriminator and adversarial Networks
Alternately train discriminator and adversarial networks by batch.
Discriminator is trained first with real and fake images,
corresponding one-hot labels and latent codes.
Adversarial is trained next with fake images pretending to be real,
corresponding one-hot labels and latent codes.
Generate sample images per save_interval.
# Arguments
models (Models): Encoder, Generator, Discriminator, Adversarial models
data (tuple): x_train, y_train data
params (tuple): Network parameters
"""
# the StackedGAN and Encoder models
enc0, enc1, gen0, gen1, dis0, dis1, adv0, adv1 = models
# network parameters
batch_size, train_steps, num_labels, z_dim, model_name = params
# train dataset
(x_train, y_train), (_, _) = data
# the generator image is saved every 500 steps
save_interval = 500
# label and noise codes for generator testing
z0 = np.random.normal(scale=0.5, size=[16, z_dim])
z1 = np.random.normal(scale=0.5, size=[16, z_dim])
noise_class = np.eye(num_labels)[np.arange(0, 16) % num_labels]
noise_params = [noise_class, z0, z1]
# number of elements in train dataset
train_size = x_train.shape[0]
print(model_name,
"Labels for generated images: ",
np.argmax(noise_class, axis=1))
for i in range(train_steps):
# train the discriminator1 for 1 batch
# 1 batch of real (label=1.0) and fake feature1 (label=0.0)
# randomly pick real images from dataset
rand_indexes = np.random.randint(0, train_size, size=batch_size)
real_images = x_train[rand_indexes]
# real feature1 from encoder0 output
real_feature1 = enc0.predict(real_images)
# generate random 50-dim z1 latent code
real_z1 = np.random.normal(scale=0.5, size=[batch_size, z_dim])
# real labels from dataset
real_labels = y_train[rand_indexes]
# generate fake feature1 using generator1 from
# real labels and 50-dim z1 latent code
fake_z1 = np.random.normal(scale=0.5, size=[batch_size, z_dim])
fake_feature1 = gen1.predict([real_labels, fake_z1])
# real + fake data
feature1 = np.concatenate((real_feature1, fake_feature1))
z1 = np.concatenate((fake_z1, fake_z1))
# label 1st half as real and 2nd half as fake
y = np.ones([2 * batch_size, 1])
y[batch_size:, :] = 0
# train discriminator1 to classify feature1
# as real/fake and recover
# latent code (z1). real = from encoder1,
# fake = from genenerator1
# joint training using discriminator part of advserial1 loss
# and entropy1 loss
metrics = dis1.train_on_batch(feature1, [y, z1])
# log the overall loss only (fr dis1.metrics_names)
log = "%d: [dis1_loss: %f]" % (i, metrics[0])
# train the discriminator0 for 1 batch
# 1 batch of real (label=1.0) and fake images (label=0.0)
# generate random 50-dim z0 latent code
fake_z0 = np.random.normal(scale=0.5, size=[batch_size, z_dim])
# generate fake images from real feature1 and fake z0
fake_images = gen0.predict([real_feature1, fake_z0])
# real + fake data
x = np.concatenate((real_images, fake_images))
z0 = np.concatenate((fake_z0, fake_z0))
# train discriminator0 to classify image as real/fake and recover
# latent code (z0)
# joint training using discriminator part of advserial0 loss
# and entropy0 loss
metrics = dis0.train_on_batch(x, [y, z0])
# log the overall loss only (fr dis0.metrics_names)
log = "%s [dis0_loss: %f]" % (log, metrics[0])
# adversarial training
# generate fake z1, labels
fake_z1 = np.random.normal(scale=0.5, size=[batch_size, z_dim])
# input to generator1 is sampling fr real labels and
# 50-dim z1 latent code
gen1_inputs = [real_labels, fake_z1]
# label fake feature1 as real
y = np.ones([batch_size, 1])
# train generator1 (thru adversarial) by
# fooling the discriminator
# and approximating encoder1 feature1 generator
# joint training: adversarial1, entropy1, conditional1
metrics = adv1.train_on_batch(gen1_inputs, [y, fake_z1, real_labels])
fmt = "%s [adv1_loss: %f, enc1_acc: %f]"
# log the overall loss and classification accuracy
log = fmt % (log, metrics[0], metrics[6])
# input to generator0 is real feature1 and
# 50-dim z0 latent code
fake_z0 = np.random.normal(scale=0.5, size=[batch_size, z_dim])
gen0_inputs = [real_feature1, fake_z0]
# train generator0 (thru adversarial) by
# fooling the discriminator
# and approximating encoder1 image source generator
# joint training: adversarial0, entropy0, conditional0
metrics = adv0.train_on_batch(gen0_inputs, [y, fake_z0, real_feature1])
# log the overall loss only
log = "%s [adv0_loss: %f]" % (log, metrics[0])
print(log)
if (i + 1) % save_interval == 0:
if (i + 1) == train_steps:
show = True
else:
show = False
generators = (gen0, gen1)
plot_images(generators,
noise_params=noise_params,
show=show,
step=(i + 1),
model_name=model_name)
# save the modelis after training generator0 & 1
# the trained generator can be reloaded for
# future MNIST digit generation
gen1.save(model_name + "-gen1.h5")
gen0.save(model_name + "-gen0.h5")
StackedGAN 的生成器输出
经过 10,000 步的训练后,StackedGAN 的Generator[0]和Generator[1]模型被保存到文件中。将Generator[0]和Generator[1]堆叠在一起,可以基于标签和噪声代码z[0]和z[1]合成虚假图像。
可以通过以下方式定性验证 StackedGAN 生成器:
-
让离散标签从 0 到 9 变化,同时两个噪声代码,z[0]和z[1],从均值为 0.5,标准差为 1.0 的正态分布中抽取样本。结果如图 6.2.9所示。我们可以看到,StackedGAN 的离散代码能够控制生成器生成的数字:
python3 stackedgan-mnist-6.2.1.py --generator0=stackedgan_mnist-gen0.h5 --generator1=stackedgan_mnist-gen1.h5 --digit=0 python3 stackedgan-mnist-6.2.1.py --generator0=stackedgan_mnist-gen0.h5 --generator1=stackedgan_mnist-gen1.h5 --digit=9
到
-
让第一个噪声代码,z[0],作为常量向量从-4.0 变化到 4.0,用于数字 0 到 9,如下所示。第二个噪声代码,z[0],设置为零向量。图 6.2.10显示第一个噪声代码控制数字的厚度。例如,数字 8:
python3 stackedgan-mnist-6.2.1.py --generator0=stackedgan_mnist-gen0.h5 --generator1=stackedgan_mnist-gen1.h5 --z0=0 --z1=0 –p0 --digit=8
-
让第二个噪声代码,z[1],作为常量向量从-1.0 变化到 1.0,用于数字 0 到 9,如下所示。第一个噪声代码,z[0],设置为零向量。图 6.2.11显示第二个噪声代码控制数字的旋转(倾斜)以及在一定程度上数字的厚度。例如,数字 8:
python3 stackedgan-mnist-6.2.1.py --generator0=stackedgan_mnist-gen0.h5 --generator1=stackedgan_mnist-gen1.h5 --z0=0 --z1=0 –p1 --digit=8
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_17.jpg
图 6.2.9:当离散代码从 0 变动到 9 时,由 StackedGAN 生成的图像。两个图像https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_047.jpg和https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_048.jpg都来自于一个均值为 0,标准差为 0.5 的正态分布。
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_18.jpg
图 6.2.10:使用 StackedGAN 生成的图像,当第一个噪声代码,z[0],从常量向量-4.0 变化到 4.0 时,适用于数字 0 到 9。z[0]似乎控制每个数字的厚度。
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_19.jpg
图 6.2.11:当第二个噪声代码,z[1],从常量向量-1.0 到 1.0 变化时,由 StackedGAN 生成的图像。z[1]似乎控制每个数字的旋转(倾斜)和笔画厚度。
图 6.2.9到图 6.2.11展示了 StackedGAN 提供了更多的控制,可以控制生成器输出的属性。控制和属性包括(标签,数字类型),(z0,数字厚度),和(z1,数字倾斜度)。从这个例子来看,我们还可以控制其他可能的实验,例如:
-
增加堆叠元素的数量,从当前的 2 开始
-
降低代码z0 和z1 的维度,像在 InfoGAN 中一样
接下来的图展示了 InfoGAN 和 StackedGAN 的潜在代码差异。解耦代码的基本思想是对损失函数施加约束,使得只有特定的属性会被一个代码所影响。从结构上来看,InfoGAN 比 StackedGAN 更容易实现。InfoGAN 的训练速度也更快:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/adv-dl-keras/img/B08956_06_20.jpg
图 6.2.12:不同 GAN 的潜在表示
结论
在本章中,我们讨论了如何解开 GAN 的潜在表示。我们在本章的早期讨论了 InfoGAN 如何通过最大化互信息来迫使生成器学习解耦的潜在向量。在 MNIST 数据集的例子中,InfoGAN 使用了三个表示和一个噪声编码作为输入。噪声表示其余的属性,以纠缠表示的形式出现。StackedGAN 以不同的方式处理这个问题。它使用一堆编码器 GAN 来学习如何合成虚假的特征和图像。首先训练编码器以提供一个特征数据集。然后,编码器 GANs 被联合训练,学习如何利用噪声编码来控制生成器输出的属性。
在下一章中,我们将介绍一种新的 GAN 类型,它能够在另一个领域生成新数据。例如,给定一张马的图片,该 GAN 可以自动转换为斑马的图片。这种类型的 GAN 的有趣之处在于,它可以在无监督的情况下进行训练。
参考文献
-
Xi Chen 等人。InfoGAN:通过信息最大化生成对抗网络进行可解释的表示学习。《神经信息处理系统进展》,2016(
papers.nips.cc/paper/6399-infogan-interpretable-representation-learning-by-information-maximizing-generative-adversarial-nets.pdf
)。 -
Xun Huang 等人。堆叠生成对抗网络。IEEE 计算机视觉与模式识别会议(CVPR)。第 2 卷,2017(
openaccess.thecvf.com/content_cvpr_2017/papers/Huang_Stacked_Generative_Adversarial_CVPR_2017_paper.pdf
)。