Machine-Learning-Mastery-生成对抗网络教程-六-

49 阅读1小时+

Machine Learning Mastery 生成对抗网络教程(六)

原文:Machine Learning Mastery

协议:CC BY-NC-SA 4.0

如何在 Keras 中实现渐进式增长 GAN 模型

原文:machinelearningmastery.com/how-to-impl…

渐进增长生成对抗网络是一种训练深度卷积神经网络模型以生成合成图像的方法。

它是更传统的 GAN 架构的扩展,包括在训练过程中逐渐增加生成图像的大小,从非常小的图像开始,例如 4×4 像素。这允许能够生成非常大的高质量图像的 GAN 模型的稳定训练和增长,例如大小为 1024×1024 像素的合成名人脸的图像。

在本教程中,您将发现如何使用 Keras 从零开始开发渐进增长的生成对抗网络模型。

完成本教程后,您将知道:

  • 如何在输出图像增长的每个级别开发预定义的鉴别器和生成器模型。
  • 如何通过鉴别器模型定义用于训练生成器模型的复合模型?
  • 如何在输出图像增长的每一级循环训练模型的淡入版本和正常版本。

用我的新书Python 生成对抗网络启动你的项目,包括分步教程和所有示例的 Python 源代码文件。

我们开始吧。

How to Implement Progressive Growing GAN Models in Keras

如何在 Keras 实现渐进式增长 GAN 模型 图片由迪奥戈·桑多斯·席尔瓦提供,保留部分权利。

教程概述

本教程分为五个部分;它们是:

  1. 什么是渐进式增长的 GAN 架构?
  2. 如何实现渐进增长的 GAN 鉴别器模型
  3. 如何实现渐进式增长 GAN 发生器模型
  4. 如何实现用于更新生成器的复合模型
  5. 如何训练鉴别器和生成器模型

什么是渐进式增长的 GAN 架构?

GANs 在生成清晰的合成图像方面很有效,尽管通常受限于可以生成的图像的大小。

渐进式增长 GAN 是 GAN 的扩展,它允许训练能够输出大的高质量图像的生成器模型,例如大小为 1024×1024 像素的真实感人脸。英伟达的 Tero Karras 等人在 2017 年的论文中描述了这一点,论文标题为“为提高质量、稳定性和变化性而对 GANs 进行渐进式增长

渐进式增长 GAN 的关键创新是发生器输出图像尺寸的递增,从 4×4 像素图像开始,加倍到 8×8、16×16 等,直至达到所需的输出分辨率。

我们的主要贡献是 GANs 的培训方法,从低分辨率图像开始,然后通过向网络添加层来逐步提高分辨率。

——为提高质量、稳定性和变化性而进行的肝的渐进式增长,2017 年。

这是通过一个训练过程来实现的,该过程包括用给定的输出分辨率对模型进行微调的阶段,以及用更大的分辨率缓慢地逐步引入新模型的阶段。

当发生器(G)和鉴别器(D)的分辨率加倍时,我们平滑地淡入新的层

——为提高质量、稳定性和变化性而进行的肝的渐进式增长,2017 年。

在训练过程中,所有层都保持可训练状态,包括添加新层时的现有层。

两个网络中的所有现有层在整个训练过程中都是可训练的。

——为提高质量、稳定性和变化性而进行的肝的渐进式增长,2017 年。

渐进式增长 GAN 涉及使用具有相同一般结构的生成器和鉴别器模型,并从非常小的图像开始。在训练期间,新的卷积层块被系统地添加到生成器模型和鉴别器模型中。

Example of Progressively Adding Layers to Generator and Discriminator Models.

向生成器和鉴别器模型逐步添加层的示例。 取自:为提高质量、稳定性和变异而进行的肝的渐进式增长。

层的增量添加允许模型有效地学习粗略级别的细节,并且稍后学习更精细的细节,在生成器和鉴别器侧都是如此。

这种增量性质允许训练首先发现图像分布的大尺度结构,然后将注意力转移到越来越精细的细节上,而不是必须同时学习所有尺度。

——为提高质量、稳定性和变化性而进行的肝的渐进式增长,2017 年。

模型架构复杂,不能直接实现。

在本教程中,我们将重点介绍如何使用 Keras 深度学习库实现渐进增长的 GAN。

我们将逐步了解如何定义每个鉴别器和生成器模型,如何通过鉴别器模型训练生成器,以及如何在训练过程中更新每个模型。

这些实现细节将为您开发适合自己应用的渐进增长的 GAN 提供基础。

如何实现渐进增长的 GAN 鉴别器模型

鉴别器模型被给予图像作为输入,并且必须将它们分类为真实的(来自数据集)或虚假的(生成的)。

在训练过程中,鉴别器必须增长以支持不断增大尺寸的图像,从 4×4 像素彩色图像开始,加倍到 8×8、16×16、32×32 等。

这是通过插入一个新的输入层来支持更大的输入图像,然后插入一个新的层块来实现的。然后对这个新块的输出进行下采样。此外,新图像还被直接下采样,并在与新块的输出组合之前通过旧的输入处理层。

在从较低分辨率转换到较高分辨率(例如 16×16 到 32×32)的过程中,鉴频器模型将有如下两条输入路径:

  • [32×32 图像]>[fromrgb conv]>[new block]>[down sample]->
  • [32×32 image]>[down sample]>[fromrgb conv]->

被下采样的新块的输出和旧输入处理层的输出使用加权平均进行组合,其中加权由称为α的新超参数控制。加权和计算如下:

  • 输出=((1–alpha)* from RGb)+(alpha * new block)

然后,这两条路径的加权平均值被输入到现有模型的其余部分。

最初,权重完全偏向旧的输入处理层(α= 0)并且随着训练迭代线性增加,使得新块被给予更多的权重,直到最终,输出完全是新块的乘积(α= 1)。这时,旧的路径可以被移除。

这可以用下图来总结,该图取自显示增长前的模型(a)、较大分辨率的逐步引入期间的模型(b)和逐步引入后的模型(c)。

Figure Showing the Growing of the Discriminator Model, Before (a) During (b) and After (c) the Phase-In of a High Resolution

图中显示了高分辨率相位输入之前(a)、期间(b)和之后(c)鉴频器模型的发展。 取自:为提高质量、稳定性和变异而进行的肝的渐进式增长。

来自 RGB 层的被实现为一个 1×1 卷积层。一个块由两个具有 3×3 大小的滤波器的卷积层和斜率为 0.2 的泄漏 ReLU 激活函数组成,随后是下采样层。平均池用于下采样,这与大多数其他使用转置卷积层的 GAN 模型不同。

该模型的输出包括两个卷积层,分别具有 3×3 和 4×4 大小的滤波器和漏 ReLU 激活,随后是一个完全连接的层,输出单值预测。该模型使用线性激活函数,而不是像其他鉴别器模型那样使用 sigmoid 激活函数,并且直接通过 Wasserstein 损失(特别是 WGAN-GP)或最小二乘损失进行训练;我们将在本教程中使用后者。模型权重使用何高斯(he_normal)进行初始化,这与本文中使用的方法非常相似。

该模型在输出块的开头使用了一个名为 Minibatch 标准差的自定义层,每个层都使用局部响应归一化,而不是批处理归一化,在本文中称为像素归一化。为了简洁起见,我们将省略迷你批次规范化,在本教程中使用批处理规范化。

一种实现渐进增长的 GAN 的方法是在培训期间按需手动扩展模型。另一种方法是在训练之前预先定义所有的模型,并谨慎地使用 Keras 功能 API 来确保在模型之间共享层并继续训练。

我相信后一种方法可能更容易,也是我们将在本教程中使用的方法。

首先,我们必须定义一个自定义层,以便在新的更高分辨率的输入图像和块中淡入淡出时使用。这个新层必须采用两组具有相同维度(宽度、高度、通道)的激活图,并使用加权和将它们相加。

我们可以将其实现为一个名为加权求和的新层,该层扩展了添加合并层,并使用超参数“ alpha ”来控制每个输入的贡献。下面定义了这个新类。该层仅假设两个输入:第一个用于旧层或现有层的输出,第二个用于新添加的层。新的超参数被定义为后端变量,这意味着我们可以通过改变变量的值随时改变它。

# weighted sum output
class WeightedSum(Add):
	# init with default value
	def __init__(self, alpha=0.0, **kwargs):
		super(WeightedSum, self).__init__(**kwargs)
		self.alpha = backend.variable(alpha, name='ws_alpha')

	# output a weighted sum of inputs
	def _merge_function(self, inputs):
		# only supports a weighted sum of two inputs
		assert (len(inputs) == 2)
		# ((1-a) * input1) + (a * input2)
		output = ((1.0 - self.alpha) * inputs[0]) + (self.alpha * inputs[1])
		return output

鉴别器模型比生成器要复杂得多,因为我们必须改变模型输入,所以让我们慢慢来。

首先,我们可以定义一个鉴别器模型,它以一幅 4×4 的彩色图像作为输入,并输出图像是真是假的预测。该模型由 1×1 输入处理层(fromRGB)和输出模块组成。

...
# base model input
in_image = Input(shape=(4,4,3))
# conv 1x1
g = Conv2D(64, (1,1), padding='same', kernel_initializer='he_normal')(in_image)
g = LeakyReLU(alpha=0.2)(g)
# conv 3x3 (output block)
g = Conv2D(128, (3,3), padding='same', kernel_initializer='he_normal')(g)
g = BatchNormalization()(g)
g = LeakyReLU(alpha=0.2)(g)
# conv 4x4
g = Conv2D(128, (4,4), padding='same', kernel_initializer='he_normal')(g)
g = BatchNormalization()(g)
g = LeakyReLU(alpha=0.2)(g)
# dense output layer
g = Flatten()(g)
out_class = Dense(1)(g)
# define model
model = Model(in_image, out_class)
# compile model
model.compile(loss='mse', optimizer=Adam(lr=0.001, beta_1=0, beta_2=0.99, epsilon=10e-8))

接下来,我们需要定义一个新的模型来处理这个模型和一个以 8×8 彩色图像作为输入的新鉴别器模型之间的中间阶段。

现有的输入处理层必须接收新的 8×8 图像的下采样版本。必须定义一个新的输入处理层,该层采用 8×8 输入图像,并通过一个由两个卷积层和一个下采样层组成的新块。下采样后的新块的输出和旧的输入处理层必须通过我们新的加权求和层使用加权求和加在一起,然后必须重用相同的输出块(两个卷积层和输出层)。

给定第一个定义的模型和我们关于这个模型的知识(例如,对于 Conv2D 和 LeakyReLU,输入处理层的层数是 2),我们可以使用来自旧模型的层索引来构建这个新的中间或淡入模型。

...
old_model = model
# get shape of existing model
in_shape = list(old_model.input.shape)
# define new input shape as double the size
input_shape = (in_shape[-2].value*2, in_shape[-2].value*2, in_shape[-1].value)
in_image = Input(shape=input_shape)
# define new input processing layer
g = Conv2D(64, (1,1), padding='same', kernel_initializer='he_normal')(in_image)
g = LeakyReLU(alpha=0.2)(g)
# define new block
g = Conv2D(64, (3,3), padding='same', kernel_initializer='he_normal')(g)
g = BatchNormalization()(g)
g = LeakyReLU(alpha=0.2)(g)
g = Conv2D(64, (3,3), padding='same', kernel_initializer='he_normal')(g)
g = BatchNormalization()(g)
g = LeakyReLU(alpha=0.2)(g)
g = AveragePooling2D()(g)
# downsample the new larger image
downsample = AveragePooling2D()(in_image)
# connect old input processing to downsampled new input
block_old = old_model.layers1
block_old = old_model.layers2
# fade in output of old model input layer with new input
g = WeightedSum()([block_old, g])
# skip the input, 1x1 and activation for the old model
for i in range(3, len(old_model.layers)):
	g = old_model.layersi
# define straight-through model
model = Model(in_image, g)
# compile model
model.compile(loss='mse', optimizer=Adam(lr=0.001, beta_1=0, beta_2=0.99, epsilon=10e-8))

目前为止,一切顺利。

我们还需要一个具有相同层的相同模型的版本,而没有来自旧模型的输入处理层的输入的淡入。

在我们淡入下一个加倍的输入图像尺寸之前,需要这个直通版本来进行训练。

我们可以更新上面的例子来创建模型的两个版本。首先是简单的直通版本,然后是用于淡入的版本,该版本重用了新块的层和旧模型的输出层。

下面的*add _ discriminator _ block()*函数实现了这一点,返回两个已定义模型(直通和淡入)的列表,并将旧模型作为参数,并将输入层数定义为默认参数(3)。

为了确保加权求和层正确工作,我们已经将所有卷积层固定为总是有 64 个滤波器,并依次输出 64 个特征图。如果旧模型的输入处理层和新块输出之间在特征图(通道)数量方面不匹配,则加权和将失败。

# add a discriminator block
def add_discriminator_block(old_model, n_input_layers=3):
	# get shape of existing model
	in_shape = list(old_model.input.shape)
	# define new input shape as double the size
	input_shape = (in_shape[-2].value*2, in_shape[-2].value*2, in_shape[-1].value)
	in_image = Input(shape=input_shape)
	# define new input processing layer
	d = Conv2D(64, (1,1), padding='same', kernel_initializer='he_normal')(in_image)
	d = LeakyReLU(alpha=0.2)(d)
	# define new block
	d = Conv2D(64, (3,3), padding='same', kernel_initializer='he_normal')(d)
	d = BatchNormalization()(d)
	d = LeakyReLU(alpha=0.2)(d)
	d = Conv2D(64, (3,3), padding='same', kernel_initializer='he_normal')(d)
	d = BatchNormalization()(d)
	d = LeakyReLU(alpha=0.2)(d)
	d = AveragePooling2D()(d)
	block_new = d
	# skip the input, 1x1 and activation for the old model
	for i in range(n_input_layers, len(old_model.layers)):
		d = old_model.layersi
	# define straight-through model
	model1 = Model(in_image, d)
	# compile model
	model1.compile(loss='mse', optimizer=Adam(lr=0.001, beta_1=0, beta_2=0.99, epsilon=10e-8))
	# downsample the new larger image
	downsample = AveragePooling2D()(in_image)
	# connect old input processing to downsampled new input
	block_old = old_model.layers1
	block_old = old_model.layers2
	# fade in output of old model input layer with new input
	d = WeightedSum()([block_old, block_new])
	# skip the input, 1x1 and activation for the old model
	for i in range(n_input_layers, len(old_model.layers)):
		d = old_model.layersi
	# define straight-through model
	model2 = Model(in_image, d)
	# compile model
	model2.compile(loss='mse', optimizer=Adam(lr=0.001, beta_1=0, beta_2=0.99, epsilon=10e-8))
	return [model1, model2]

这不是一个优雅的功能,因为我们有一些重复,但它是可读的,并将完成工作。

然后,当我们将输入图像的大小增加一倍时,我们可以一次又一次地调用这个函数。重要的是,该函数期望将先前模型的直通版本作为输入。

下面的示例定义了一个名为 define_discriminator() 的新函数,该函数定义了我们的基础模型,该模型需要一个 4×4 的彩色图像作为输入,然后在每次需要四倍面积的图像时,重复添加块来创建新版本的鉴别器模型。

# define the discriminator models for each image resolution
def define_discriminator(n_blocks, input_shape=(4,4,3)):
	model_list = list()
	# base model input
	in_image = Input(shape=input_shape)
	# conv 1x1
	d = Conv2D(64, (1,1), padding='same', kernel_initializer='he_normal')(in_image)
	d = LeakyReLU(alpha=0.2)(d)
	# conv 3x3 (output block)
	d = Conv2D(128, (3,3), padding='same', kernel_initializer='he_normal')(d)
	d = BatchNormalization()(d)
	d = LeakyReLU(alpha=0.2)(d)
	# conv 4x4
	d = Conv2D(128, (4,4), padding='same', kernel_initializer='he_normal')(d)
	d = BatchNormalization()(d)
	d = LeakyReLU(alpha=0.2)(d)
	# dense output layer
	d = Flatten()(d)
	out_class = Dense(1)(d)
	# define model
	model = Model(in_image, out_class)
	# compile model
	model.compile(loss='mse', optimizer=Adam(lr=0.001, beta_1=0, beta_2=0.99, epsilon=10e-8))
	# store model
	model_list.append([model, model])
	# create submodels
	for i in range(1, n_blocks):
		# get prior model without the fade-on
		old_model = model_list[i - 1][0]
		# create new model for next resolution
		models = add_discriminator_block(old_model)
		# store model
		model_list.append(models)
	return model_list

此函数将返回一个模型列表,其中列表中的每个项目都是一个双元素列表,首先包含该分辨率下模型的直通版本,其次包含该分辨率下模型的淡入版本。

我们可以将所有这些联系在一起,定义一个新的“鉴别器模型”,它将从 4×4 增长到 8×8,最后增长到 16×16。这是通过在调用*定义 _ 鉴别器()*函数时将 n_blocks 参数传递给 3 来实现的,用于创建三组模型。

下面列出了完整的示例。

# example of defining discriminator models for the progressive growing gan
from keras.optimizers import Adam
from keras.models import Model
from keras.layers import Input
from keras.layers import Dense
from keras.layers import Flatten
from keras.layers import Conv2D
from keras.layers import AveragePooling2D
from keras.layers import LeakyReLU
from keras.layers import BatchNormalization
from keras.layers import Add
from keras.utils.vis_utils import plot_model
from keras import backend

# weighted sum output
class WeightedSum(Add):
	# init with default value
	def __init__(self, alpha=0.0, **kwargs):
		super(WeightedSum, self).__init__(**kwargs)
		self.alpha = backend.variable(alpha, name='ws_alpha')

	# output a weighted sum of inputs
	def _merge_function(self, inputs):
		# only supports a weighted sum of two inputs
		assert (len(inputs) == 2)
		# ((1-a) * input1) + (a * input2)
		output = ((1.0 - self.alpha) * inputs[0]) + (self.alpha * inputs[1])
		return output

# add a discriminator block
def add_discriminator_block(old_model, n_input_layers=3):
	# get shape of existing model
	in_shape = list(old_model.input.shape)
	# define new input shape as double the size
	input_shape = (in_shape[-2].value*2, in_shape[-2].value*2, in_shape[-1].value)
	in_image = Input(shape=input_shape)
	# define new input processing layer
	d = Conv2D(64, (1,1), padding='same', kernel_initializer='he_normal')(in_image)
	d = LeakyReLU(alpha=0.2)(d)
	# define new block
	d = Conv2D(64, (3,3), padding='same', kernel_initializer='he_normal')(d)
	d = BatchNormalization()(d)
	d = LeakyReLU(alpha=0.2)(d)
	d = Conv2D(64, (3,3), padding='same', kernel_initializer='he_normal')(d)
	d = BatchNormalization()(d)
	d = LeakyReLU(alpha=0.2)(d)
	d = AveragePooling2D()(d)
	block_new = d
	# skip the input, 1x1 and activation for the old model
	for i in range(n_input_layers, len(old_model.layers)):
		d = old_model.layersi
	# define straight-through model
	model1 = Model(in_image, d)
	# compile model
	model1.compile(loss='mse', optimizer=Adam(lr=0.001, beta_1=0, beta_2=0.99, epsilon=10e-8))
	# downsample the new larger image
	downsample = AveragePooling2D()(in_image)
	# connect old input processing to downsampled new input
	block_old = old_model.layers1
	block_old = old_model.layers2
	# fade in output of old model input layer with new input
	d = WeightedSum()([block_old, block_new])
	# skip the input, 1x1 and activation for the old model
	for i in range(n_input_layers, len(old_model.layers)):
		d = old_model.layersi
	# define straight-through model
	model2 = Model(in_image, d)
	# compile model
	model2.compile(loss='mse', optimizer=Adam(lr=0.001, beta_1=0, beta_2=0.99, epsilon=10e-8))
	return [model1, model2]

# define the discriminator models for each image resolution
def define_discriminator(n_blocks, input_shape=(4,4,3)):
	model_list = list()
	# base model input
	in_image = Input(shape=input_shape)
	# conv 1x1
	d = Conv2D(64, (1,1), padding='same', kernel_initializer='he_normal')(in_image)
	d = LeakyReLU(alpha=0.2)(d)
	# conv 3x3 (output block)
	d = Conv2D(128, (3,3), padding='same', kernel_initializer='he_normal')(d)
	d = BatchNormalization()(d)
	d = LeakyReLU(alpha=0.2)(d)
	# conv 4x4
	d = Conv2D(128, (4,4), padding='same', kernel_initializer='he_normal')(d)
	d = BatchNormalization()(d)
	d = LeakyReLU(alpha=0.2)(d)
	# dense output layer
	d = Flatten()(d)
	out_class = Dense(1)(d)
	# define model
	model = Model(in_image, out_class)
	# compile model
	model.compile(loss='mse', optimizer=Adam(lr=0.001, beta_1=0, beta_2=0.99, epsilon=10e-8))
	# store model
	model_list.append([model, model])
	# create submodels
	for i in range(1, n_blocks):
		# get prior model without the fade-on
		old_model = model_list[i - 1][0]
		# create new model for next resolution
		models = add_discriminator_block(old_model)
		# store model
		model_list.append(models)
	return model_list

# define models
discriminators = define_discriminator(3)
# spot check
m = discriminators[2][1]
m.summary()
plot_model(m, to_file='discriminator_plot.png', show_shapes=True, show_layer_names=True)

运行示例首先总结了第三个模型的淡入版本,显示了 16×16 彩色图像输入和单值输出。

__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to
==================================================================================================
input_3 (InputLayer)            (None, 16, 16, 3)    0
__________________________________________________________________________________________________
conv2d_7 (Conv2D)               (None, 16, 16, 64)   256         input_3[0][0]
__________________________________________________________________________________________________
leaky_re_lu_7 (LeakyReLU)       (None, 16, 16, 64)   0           conv2d_7[0][0]
__________________________________________________________________________________________________
conv2d_8 (Conv2D)               (None, 16, 16, 64)   36928       leaky_re_lu_7[0][0]
__________________________________________________________________________________________________
batch_normalization_5 (BatchNor (None, 16, 16, 64)   256         conv2d_8[0][0]
__________________________________________________________________________________________________
leaky_re_lu_8 (LeakyReLU)       (None, 16, 16, 64)   0           batch_normalization_5[0][0]
__________________________________________________________________________________________________
conv2d_9 (Conv2D)               (None, 16, 16, 64)   36928       leaky_re_lu_8[0][0]
__________________________________________________________________________________________________
average_pooling2d_4 (AveragePoo (None, 8, 8, 3)      0           input_3[0][0]
__________________________________________________________________________________________________
batch_normalization_6 (BatchNor (None, 16, 16, 64)   256         conv2d_9[0][0]
__________________________________________________________________________________________________
conv2d_4 (Conv2D)               (None, 8, 8, 64)     256         average_pooling2d_4[0][0]
__________________________________________________________________________________________________
leaky_re_lu_9 (LeakyReLU)       (None, 16, 16, 64)   0           batch_normalization_6[0][0]
__________________________________________________________________________________________________
leaky_re_lu_4 (LeakyReLU)       (None, 8, 8, 64)     0           conv2d_4[1][0]
__________________________________________________________________________________________________
average_pooling2d_3 (AveragePoo (None, 8, 8, 64)     0           leaky_re_lu_9[0][0]
__________________________________________________________________________________________________
weighted_sum_2 (WeightedSum)    (None, 8, 8, 64)     0           leaky_re_lu_4[1][0]
                                                                 average_pooling2d_3[0][0]
__________________________________________________________________________________________________
conv2d_5 (Conv2D)               (None, 8, 8, 64)     36928       weighted_sum_2[0][0]
__________________________________________________________________________________________________
batch_normalization_3 (BatchNor (None, 8, 8, 64)     256         conv2d_5[2][0]
__________________________________________________________________________________________________
leaky_re_lu_5 (LeakyReLU)       (None, 8, 8, 64)     0           batch_normalization_3[2][0]
__________________________________________________________________________________________________
conv2d_6 (Conv2D)               (None, 8, 8, 64)     36928       leaky_re_lu_5[2][0]
__________________________________________________________________________________________________
batch_normalization_4 (BatchNor (None, 8, 8, 64)     256         conv2d_6[2][0]
__________________________________________________________________________________________________
leaky_re_lu_6 (LeakyReLU)       (None, 8, 8, 64)     0           batch_normalization_4[2][0]
__________________________________________________________________________________________________
average_pooling2d_1 (AveragePoo (None, 4, 4, 64)     0           leaky_re_lu_6[2][0]
__________________________________________________________________________________________________
conv2d_2 (Conv2D)               (None, 4, 4, 128)    73856       average_pooling2d_1[2][0]
__________________________________________________________________________________________________
batch_normalization_1 (BatchNor (None, 4, 4, 128)    512         conv2d_2[4][0]
__________________________________________________________________________________________________
leaky_re_lu_2 (LeakyReLU)       (None, 4, 4, 128)    0           batch_normalization_1[4][0]
__________________________________________________________________________________________________
conv2d_3 (Conv2D)               (None, 4, 4, 128)    262272      leaky_re_lu_2[4][0]
__________________________________________________________________________________________________
batch_normalization_2 (BatchNor (None, 4, 4, 128)    512         conv2d_3[4][0]
__________________________________________________________________________________________________
leaky_re_lu_3 (LeakyReLU)       (None, 4, 4, 128)    0           batch_normalization_2[4][0]
__________________________________________________________________________________________________
flatten_1 (Flatten)             (None, 2048)         0           leaky_re_lu_3[4][0]
__________________________________________________________________________________________________
dense_1 (Dense)                 (None, 1)            2049        flatten_1[4][0]
==================================================================================================
Total params: 488,449
Trainable params: 487,425
Non-trainable params: 1,024
__________________________________________________________________________________________________

模型的相同淡入版本的绘图被创建并保存到文件中。

:创建此图假设安装了 pygraphviz 和 pydot 库。如果这是一个问题,请注释掉 import 语句并调用 plot_model()。

该图显示了 16×16 输入图像,该图像经过下采样,并通过了前一模型的 8×8 输入处理层(左)。它还显示了在使用现有模型层继续处理和输出预测之前,添加新块(右)和组合两个输入流的加权平均值。

Plot of the Fade-In Discriminator Model For the Progressive Growing GAN Transitioning From 8x8 to 16x16 Input Images

渐进式增长 GAN 从 8×8 到 16×16 输入图像过渡的淡入鉴别器模型图

既然我们已经看到了如何定义鉴别器模型,那么让我们看看如何定义生成器模型。

如何实现渐进式增长 GAN 发生器模型

渐进式增长 GAN 的生成器模型比鉴别器模型更容易在 Keras 中实现。

这样做的原因是因为每次淡入都需要对模型的输出进行微小的更改。

提高发生器的分辨率需要首先对最后一个数据块结尾的输出进行上采样。然后,它被连接到新的块和新的输出层,用于两倍高度和宽度尺寸或四倍面积的图像。在相位输入期间,上采样也连接到旧模型的输出层,并且使用加权平均合并两个输出层的输出。

相位输入完成后,旧的输出层将被移除。

这可以用下图来总结,该图取自显示增长前的模型(a)、较大分辨率的逐步引入期间的模型(b)和逐步引入后的模型(c)。

Figure Showing the Growing of the Generator Model, Before (a) During (b) and After (c) the Phase-In of a High Resolution

图中显示了发电机模型在(a)之前、期间(b)和之后(c)高分辨率相位输入的增长情况。 取自:为提高质量、稳定性和变异而进行的肝的渐进式增长。

toRGB 层是具有 3 个 1×1 滤波器的卷积层,足以输出彩色图像。

该模型将潜在空间中的一个点作为输入,例如,如本文所述的 100 元素或 512 元素向量。这被放大以提供 4×4 激活图的基础,接着是具有 4×4 滤波器的卷积层和另一个具有 3×3 滤波器的卷积层。与鉴别器一样,使用了 LeakyReLU 激活,像素归一化也是如此,为了简洁起见,我们将用批量归一化来代替。

一个块包括一个上采样层,后面是两个带有 3×3 滤波器的卷积层。通过使用最近邻方法(例如复制输入行和列)经由上采样 2D 层而不是更常见的转置卷积层来实现上采样。

我们可以定义基线模型,该模型将潜在空间中的一点作为输入,并输出 4×4 彩色图像,如下所示:

...
# base model latent input
in_latent = Input(shape=(100,))
# linear scale up to activation maps
g  = Dense(128 * 4 * 4, kernel_initializer='he_normal')(in_latent)
g = Reshape((4, 4, 128))(g)
# conv 4x4, input block
g = Conv2D(128, (3,3), padding='same', kernel_initializer='he_normal')(g)
g = BatchNormalization()(g)
g = LeakyReLU(alpha=0.2)(g)
# conv 3x3
g = Conv2D(128, (3,3), padding='same', kernel_initializer='he_normal')(g)
g = BatchNormalization()(g)
g = LeakyReLU(alpha=0.2)(g)
# conv 1x1, output block
out_image = Conv2D(3, (1,1), padding='same', kernel_initializer='he_normal')(g)
# define model
model = Model(in_latent, out_image)

接下来,我们需要定义一个使用所有相同输入层的模型版本,尽管增加了一个新的块(上采样和 2 个卷积层)和一个新的输出层(1×1 卷积层)。

这将是逐步采用新输出分辨率后的模型。这可以通过使用自己关于基线模型的知识来实现,并且最后一个块的末端是第二个最后一层,例如模型的层列表中索引-2 处的层。

添加了新块和输出层的新模型定义如下:

...
old_model = model
# get the end of the last block
block_end = old_model.layers[-2].output
# upsample, and define new block
upsampling = UpSampling2D()(block_end)
g = Conv2D(64, (3,3), padding='same', kernel_initializer='he_normal')(upsampling)
g = BatchNormalization()(g)
g = LeakyReLU(alpha=0.2)(g)
g = Conv2D(64, (3,3), padding='same', kernel_initializer='he_normal')(g)
g = BatchNormalization()(g)
g = LeakyReLU(alpha=0.2)(g)
# add new output layer
out_image = Conv2D(3, (1,1), padding='same', kernel_initializer='he_normal')(g)
# define model
model = Model(old_model.input, out_image)

这很简单;我们已经在最后一个块的末尾切掉了旧的输出层,并嫁接了一个新的块和输出层。

现在我们需要一个新模型的版本来在淡入时使用。

这包括在新块开始时将旧的输出层连接到新的上采样层,并使用上一节中定义的加权采样层的实例来组合新旧输出层的输出。

...
# get the output layer from old model
out_old = old_model.layers[-1]
# connect the upsampling to the old output layer
out_image2 = out_old(upsampling)
# define new output image as the weighted sum of the old and new models
merged = WeightedSum()([out_image2, out_image])
# define model
model2 = Model(old_model.input, merged)

我们可以将这两个操作的定义合并到一个名为 add_generator_block() 的函数中,如下定义,该函数将扩展给定的模型,并返回添加了 block 的新的 generator 模型( model1 )和带有旧输出层的新块淡入的模型版本( model2 )。

# add a generator block
def add_generator_block(old_model):
	# get the end of the last block
	block_end = old_model.layers[-2].output
	# upsample, and define new block
	upsampling = UpSampling2D()(block_end)
	g = Conv2D(64, (3,3), padding='same', kernel_initializer='he_normal')(upsampling)
	g = BatchNormalization()(g)
	g = LeakyReLU(alpha=0.2)(g)
	g = Conv2D(64, (3,3), padding='same', kernel_initializer='he_normal')(g)
	g = BatchNormalization()(g)
	g = LeakyReLU(alpha=0.2)(g)
	# add new output layer
	out_image = Conv2D(3, (1,1), padding='same', kernel_initializer='he_normal')(g)
	# define model
	model1 = Model(old_model.input, out_image)
	# get the output layer from old model
	out_old = old_model.layers[-1]
	# connect the upsampling to the old output layer
	out_image2 = out_old(upsampling)
	# define new output image as the weighted sum of the old and new models
	merged = WeightedSum()([out_image2, out_image])
	# define model
	model2 = Model(old_model.input, merged)
	return [model1, model2]

然后,我们可以用我们的基线模型调用这个函数来创建带有一个添加块的模型,并继续用后续模型调用它来继续添加块。

下面的 define_generator() 函数实现了这一点,取潜在空间的大小和要添加的块数(要创建的模型)。

基线模型定义为输出形状为 4×4 的彩色图像,由默认参数 in_dim 控制。

# define generator models
def define_generator(latent_dim, n_blocks, in_dim=4):
	model_list = list()
	# base model latent input
	in_latent = Input(shape=(latent_dim,))
	# linear scale up to activation maps
	g  = Dense(128 * in_dim * in_dim, kernel_initializer='he_normal')(in_latent)
	g = Reshape((in_dim, in_dim, 128))(g)
	# conv 4x4, input block
	g = Conv2D(128, (3,3), padding='same', kernel_initializer='he_normal')(g)
	g = BatchNormalization()(g)
	g = LeakyReLU(alpha=0.2)(g)
	# conv 3x3
	g = Conv2D(128, (3,3), padding='same', kernel_initializer='he_normal')(g)
	g = BatchNormalization()(g)
	g = LeakyReLU(alpha=0.2)(g)
	# conv 1x1, output block
	out_image = Conv2D(3, (1,1), padding='same', kernel_initializer='he_normal')(g)
	# define model
	model = Model(in_latent, out_image)
	# store model
	model_list.append([model, model])
	# create submodels
	for i in range(1, n_blocks):
		# get prior model without the fade-on
		old_model = model_list[i - 1][0]
		# create new model for next resolution
		models = add_generator_block(old_model)
		# store model
		model_list.append(models)
	return model_list

我们可以将所有这些联系在一起,定义一个基线生成器和两个块的添加,总共三个模型,其中定义了每个模型的直通和淡入版本。

下面列出了完整的示例。

# example of defining generator models for the progressive growing gan
from keras.models import Model
from keras.layers import Input
from keras.layers import Dense
from keras.layers import Reshape
from keras.layers import Conv2D
from keras.layers import UpSampling2D
from keras.layers import LeakyReLU
from keras.layers import BatchNormalization
from keras.layers import Add
from keras.utils.vis_utils import plot_model
from keras import backend

# weighted sum output
class WeightedSum(Add):
	# init with default value
	def __init__(self, alpha=0.0, **kwargs):
		super(WeightedSum, self).__init__(**kwargs)
		self.alpha = backend.variable(alpha, name='ws_alpha')

	# output a weighted sum of inputs
	def _merge_function(self, inputs):
		# only supports a weighted sum of two inputs
		assert (len(inputs) == 2)
		# ((1-a) * input1) + (a * input2)
		output = ((1.0 - self.alpha) * inputs[0]) + (self.alpha * inputs[1])
		return output

# add a generator block
def add_generator_block(old_model):
	# get the end of the last block
	block_end = old_model.layers[-2].output
	# upsample, and define new block
	upsampling = UpSampling2D()(block_end)
	g = Conv2D(64, (3,3), padding='same', kernel_initializer='he_normal')(upsampling)
	g = BatchNormalization()(g)
	g = LeakyReLU(alpha=0.2)(g)
	g = Conv2D(64, (3,3), padding='same', kernel_initializer='he_normal')(g)
	g = BatchNormalization()(g)
	g = LeakyReLU(alpha=0.2)(g)
	# add new output layer
	out_image = Conv2D(3, (1,1), padding='same', kernel_initializer='he_normal')(g)
	# define model
	model1 = Model(old_model.input, out_image)
	# get the output layer from old model
	out_old = old_model.layers[-1]
	# connect the upsampling to the old output layer
	out_image2 = out_old(upsampling)
	# define new output image as the weighted sum of the old and new models
	merged = WeightedSum()([out_image2, out_image])
	# define model
	model2 = Model(old_model.input, merged)
	return [model1, model2]

# define generator models
def define_generator(latent_dim, n_blocks, in_dim=4):
	model_list = list()
	# base model latent input
	in_latent = Input(shape=(latent_dim,))
	# linear scale up to activation maps
	g  = Dense(128 * in_dim * in_dim, kernel_initializer='he_normal')(in_latent)
	g = Reshape((in_dim, in_dim, 128))(g)
	# conv 4x4, input block
	g = Conv2D(128, (3,3), padding='same', kernel_initializer='he_normal')(g)
	g = BatchNormalization()(g)
	g = LeakyReLU(alpha=0.2)(g)
	# conv 3x3
	g = Conv2D(128, (3,3), padding='same', kernel_initializer='he_normal')(g)
	g = BatchNormalization()(g)
	g = LeakyReLU(alpha=0.2)(g)
	# conv 1x1, output block
	out_image = Conv2D(3, (1,1), padding='same', kernel_initializer='he_normal')(g)
	# define model
	model = Model(in_latent, out_image)
	# store model
	model_list.append([model, model])
	# create submodels
	for i in range(1, n_blocks):
		# get prior model without the fade-on
		old_model = model_list[i - 1][0]
		# create new model for next resolution
		models = add_generator_block(old_model)
		# store model
		model_list.append(models)
	return model_list

# define models
generators = define_generator(100, 3)
# spot check
m = generators[2][1]
m.summary()
plot_model(m, to_file='generator_plot.png', show_shapes=True, show_layer_names=True)

该示例选择最后一个模型的淡入模型进行总结。

运行该示例首先总结了模型中层的线性列表。我们可以看到,最后一个模型从潜在空间中取一个点,输出一个 16×16 的图像。

这与我们的预期相匹配,因为基线模型输出 4×4 的图像,添加一个块会将其增加到 8×8,再添加一个块会将其增加到 16×16。

__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to
==================================================================================================
input_1 (InputLayer)            (None, 100)          0
__________________________________________________________________________________________________
dense_1 (Dense)                 (None, 2048)         206848      input_1[0][0]
__________________________________________________________________________________________________
reshape_1 (Reshape)             (None, 4, 4, 128)    0           dense_1[0][0]
__________________________________________________________________________________________________
conv2d_1 (Conv2D)               (None, 4, 4, 128)    147584      reshape_1[0][0]
__________________________________________________________________________________________________
batch_normalization_1 (BatchNor (None, 4, 4, 128)    512         conv2d_1[0][0]
__________________________________________________________________________________________________
leaky_re_lu_1 (LeakyReLU)       (None, 4, 4, 128)    0           batch_normalization_1[0][0]
__________________________________________________________________________________________________
conv2d_2 (Conv2D)               (None, 4, 4, 128)    147584      leaky_re_lu_1[0][0]
__________________________________________________________________________________________________
batch_normalization_2 (BatchNor (None, 4, 4, 128)    512         conv2d_2[0][0]
__________________________________________________________________________________________________
leaky_re_lu_2 (LeakyReLU)       (None, 4, 4, 128)    0           batch_normalization_2[0][0]
__________________________________________________________________________________________________
up_sampling2d_1 (UpSampling2D)  (None, 8, 8, 128)    0           leaky_re_lu_2[0][0]
__________________________________________________________________________________________________
conv2d_4 (Conv2D)               (None, 8, 8, 64)     73792       up_sampling2d_1[0][0]
__________________________________________________________________________________________________
batch_normalization_3 (BatchNor (None, 8, 8, 64)     256         conv2d_4[0][0]
__________________________________________________________________________________________________
leaky_re_lu_3 (LeakyReLU)       (None, 8, 8, 64)     0           batch_normalization_3[0][0]
__________________________________________________________________________________________________
conv2d_5 (Conv2D)               (None, 8, 8, 64)     36928       leaky_re_lu_3[0][0]
__________________________________________________________________________________________________
batch_normalization_4 (BatchNor (None, 8, 8, 64)     256         conv2d_5[0][0]
__________________________________________________________________________________________________
leaky_re_lu_4 (LeakyReLU)       (None, 8, 8, 64)     0           batch_normalization_4[0][0]
__________________________________________________________________________________________________
up_sampling2d_2 (UpSampling2D)  (None, 16, 16, 64)   0           leaky_re_lu_4[0][0]
__________________________________________________________________________________________________
conv2d_7 (Conv2D)               (None, 16, 16, 64)   36928       up_sampling2d_2[0][0]
__________________________________________________________________________________________________
batch_normalization_5 (BatchNor (None, 16, 16, 64)   256         conv2d_7[0][0]
__________________________________________________________________________________________________
leaky_re_lu_5 (LeakyReLU)       (None, 16, 16, 64)   0           batch_normalization_5[0][0]
__________________________________________________________________________________________________
conv2d_8 (Conv2D)               (None, 16, 16, 64)   36928       leaky_re_lu_5[0][0]
__________________________________________________________________________________________________
batch_normalization_6 (BatchNor (None, 16, 16, 64)   256         conv2d_8[0][0]
__________________________________________________________________________________________________
leaky_re_lu_6 (LeakyReLU)       (None, 16, 16, 64)   0           batch_normalization_6[0][0]
__________________________________________________________________________________________________
conv2d_6 (Conv2D)               multiple             195         up_sampling2d_2[0][0]
__________________________________________________________________________________________________
conv2d_9 (Conv2D)               (None, 16, 16, 3)    195         leaky_re_lu_6[0][0]
__________________________________________________________________________________________________
weighted_sum_2 (WeightedSum)    (None, 16, 16, 3)    0           conv2d_6[1][0]
                                                                 conv2d_9[0][0]
==================================================================================================
Total params: 689,030
Trainable params: 688,006
Non-trainable params: 1,024
__________________________________________________________________________________________________

模型的相同淡入版本的绘图被创建并保存到文件中。

:创建此图假设安装了 pygraphviz 和 pydot 库。如果这是一个问题,请注释掉导入语句并调用 plot_model()

我们可以看到,最后一个块的输出在馈送添加的块和新的输出层以及旧的输出层之前,通过一个 UpSampling2D 层,然后通过加权和合并到最终的输出层。

Plot of the Fade-In Generator Model For the Progressive Growing GAN Transitioning From 8x8 to 16x16 Output Images

渐进式增长 GAN 从 8×8 到 16×16 输出图像过渡的淡入发生器模型图

既然我们已经看到了如何定义生成器模型,我们可以回顾一下如何通过鉴别器模型更新生成器模型。

如何实现用于更新生成器的复合模型

鉴别器模型直接以真实和虚假图像作为输入进行训练,目标值为假 0,真 1。

发电机模型不是直接训练的;相反,它们是通过鉴别器模型间接训练的,就像普通的 GAN 模型一样。

我们可以为模型的每个增长级别创建一个复合模型,例如,成对的 4×4 生成器和 4×4 鉴别器。我们也可以将直通模式和淡入模式配对在一起。

例如,我们可以检索给定增长水平的生成器和鉴别器模型。

...
g_models, d_models = generators[0], discriminators[0]

然后,我们可以使用它们来创建一个用于训练直通发生器的复合模型,其中发生器的输出被直接馈送到鉴别器以进行分类。

# straight-through model
d_models[0].trainable = False
model1 = Sequential()
model1.add(g_models[0])
model1.add(d_models[0])
model1.compile(loss='mse', optimizer=Adam(lr=0.001, beta_1=0, beta_2=0.99, epsilon=10e-8))

对淡入发生器的复合模型进行同样的操作。

# fade-in model
d_models[1].trainable = False
model2 = Sequential()
model2.add(g_models[1])
model2.add(d_models[1])
model2.compile(loss='mse', optimizer=Adam(lr=0.001, beta_1=0, beta_2=0.99, epsilon=10e-8))

下面的函数名为 define_composite() ,它自动执行此操作;给定定义的鉴别器和生成器模型的列表,它将创建一个适当的复合模型来训练每个生成器模型。

# define composite models for training generators via discriminators
def define_composite(discriminators, generators):
	model_list = list()
	# create composite models
	for i in range(len(discriminators)):
		g_models, d_models = generators[i], discriminators[i]
		# straight-through model
		d_models[0].trainable = False
		model1 = Sequential()
		model1.add(g_models[0])
		model1.add(d_models[0])
		model1.compile(loss='mse', optimizer=Adam(lr=0.001, beta_1=0, beta_2=0.99, epsilon=10e-8))
		# fade-in model
		d_models[1].trainable = False
		model2 = Sequential()
		model2.add(g_models[1])
		model2.add(d_models[1])
		model2.compile(loss='mse', optimizer=Adam(lr=0.001, beta_1=0, beta_2=0.99, epsilon=10e-8))
		# store
		model_list.append([model1, model2])
	return model_list

将此与上面的鉴别器和生成器模型的定义结合起来,下面列出了在每个预定义的增长级别定义所有模型的完整示例。

# example of defining composite models for the progressive growing gan
from keras.optimizers import Adam
from keras.models import Sequential
from keras.models import Model
from keras.layers import Input
from keras.layers import Dense
from keras.layers import Flatten
from keras.layers import Reshape
from keras.layers import Conv2D
from keras.layers import UpSampling2D
from keras.layers import AveragePooling2D
from keras.layers import LeakyReLU
from keras.layers import BatchNormalization
from keras.layers import Add
from keras.utils.vis_utils import plot_model
from keras import backend

# weighted sum output
class WeightedSum(Add):
	# init with default value
	def __init__(self, alpha=0.0, **kwargs):
		super(WeightedSum, self).__init__(**kwargs)
		self.alpha = backend.variable(alpha, name='ws_alpha')

	# output a weighted sum of inputs
	def _merge_function(self, inputs):
		# only supports a weighted sum of two inputs
		assert (len(inputs) == 2)
		# ((1-a) * input1) + (a * input2)
		output = ((1.0 - self.alpha) * inputs[0]) + (self.alpha * inputs[1])
		return output

# add a discriminator block
def add_discriminator_block(old_model, n_input_layers=3):
	# get shape of existing model
	in_shape = list(old_model.input.shape)
	# define new input shape as double the size
	input_shape = (in_shape[-2].value*2, in_shape[-2].value*2, in_shape[-1].value)
	in_image = Input(shape=input_shape)
	# define new input processing layer
	d = Conv2D(64, (1,1), padding='same', kernel_initializer='he_normal')(in_image)
	d = LeakyReLU(alpha=0.2)(d)
	# define new block
	d = Conv2D(64, (3,3), padding='same', kernel_initializer='he_normal')(d)
	d = BatchNormalization()(d)
	d = LeakyReLU(alpha=0.2)(d)
	d = Conv2D(64, (3,3), padding='same', kernel_initializer='he_normal')(d)
	d = BatchNormalization()(d)
	d = LeakyReLU(alpha=0.2)(d)
	d = AveragePooling2D()(d)
	block_new = d
	# skip the input, 1x1 and activation for the old model
	for i in range(n_input_layers, len(old_model.layers)):
		d = old_model.layersi
	# define straight-through model
	model1 = Model(in_image, d)
	# compile model
	model1.compile(loss='mse', optimizer=Adam(lr=0.001, beta_1=0, beta_2=0.99, epsilon=10e-8))
	# downsample the new larger image
	downsample = AveragePooling2D()(in_image)
	# connect old input processing to downsampled new input
	block_old = old_model.layers1
	block_old = old_model.layers2
	# fade in output of old model input layer with new input
	d = WeightedSum()([block_old, block_new])
	# skip the input, 1x1 and activation for the old model
	for i in range(n_input_layers, len(old_model.layers)):
		d = old_model.layersi
	# define straight-through model
	model2 = Model(in_image, d)
	# compile model
	model2.compile(loss='mse', optimizer=Adam(lr=0.001, beta_1=0, beta_2=0.99, epsilon=10e-8))
	return [model1, model2]

# define the discriminator models for each image resolution
def define_discriminator(n_blocks, input_shape=(4,4,3)):
	model_list = list()
	# base model input
	in_image = Input(shape=input_shape)
	# conv 1x1
	d = Conv2D(64, (1,1), padding='same', kernel_initializer='he_normal')(in_image)
	d = LeakyReLU(alpha=0.2)(d)
	# conv 3x3 (output block)
	d = Conv2D(128, (3,3), padding='same', kernel_initializer='he_normal')(d)
	d = BatchNormalization()(d)
	d = LeakyReLU(alpha=0.2)(d)
	# conv 4x4
	d = Conv2D(128, (4,4), padding='same', kernel_initializer='he_normal')(d)
	d = BatchNormalization()(d)
	d = LeakyReLU(alpha=0.2)(d)
	# dense output layer
	d = Flatten()(d)
	out_class = Dense(1)(d)
	# define model
	model = Model(in_image, out_class)
	# compile model
	model.compile(loss='mse', optimizer=Adam(lr=0.001, beta_1=0, beta_2=0.99, epsilon=10e-8))
	# store model
	model_list.append([model, model])
	# create submodels
	for i in range(1, n_blocks):
		# get prior model without the fade-on
		old_model = model_list[i - 1][0]
		# create new model for next resolution
		models = add_discriminator_block(old_model)
		# store model
		model_list.append(models)
	return model_list

# add a generator block
def add_generator_block(old_model):
	# get the end of the last block
	block_end = old_model.layers[-2].output
	# upsample, and define new block
	upsampling = UpSampling2D()(block_end)
	g = Conv2D(64, (3,3), padding='same', kernel_initializer='he_normal')(upsampling)
	g = BatchNormalization()(g)
	g = LeakyReLU(alpha=0.2)(g)
	g = Conv2D(64, (3,3), padding='same', kernel_initializer='he_normal')(g)
	g = BatchNormalization()(g)
	g = LeakyReLU(alpha=0.2)(g)
	# add new output layer
	out_image = Conv2D(3, (1,1), padding='same', kernel_initializer='he_normal')(g)
	# define model
	model1 = Model(old_model.input, out_image)
	# get the output layer from old model
	out_old = old_model.layers[-1]
	# connect the upsampling to the old output layer
	out_image2 = out_old(upsampling)
	# define new output image as the weighted sum of the old and new models
	merged = WeightedSum()([out_image2, out_image])
	# define model
	model2 = Model(old_model.input, merged)
	return [model1, model2]

# define generator models
def define_generator(latent_dim, n_blocks, in_dim=4):
	model_list = list()
	# base model latent input
	in_latent = Input(shape=(latent_dim,))
	# linear scale up to activation maps
	g  = Dense(128 * in_dim * in_dim, kernel_initializer='he_normal')(in_latent)
	g = Reshape((in_dim, in_dim, 128))(g)
	# conv 4x4, input block
	g = Conv2D(128, (3,3), padding='same', kernel_initializer='he_normal')(g)
	g = BatchNormalization()(g)
	g = LeakyReLU(alpha=0.2)(g)
	# conv 3x3
	g = Conv2D(128, (3,3), padding='same', kernel_initializer='he_normal')(g)
	g = BatchNormalization()(g)
	g = LeakyReLU(alpha=0.2)(g)
	# conv 1x1, output block
	out_image = Conv2D(3, (1,1), padding='same', kernel_initializer='he_normal')(g)
	# define model
	model = Model(in_latent, out_image)
	# store model
	model_list.append([model, model])
	# create submodels
	for i in range(1, n_blocks):
		# get prior model without the fade-on
		old_model = model_list[i - 1][0]
		# create new model for next resolution
		models = add_generator_block(old_model)
		# store model
		model_list.append(models)
	return model_list

# define composite models for training generators via discriminators
def define_composite(discriminators, generators):
	model_list = list()
	# create composite models
	for i in range(len(discriminators)):
		g_models, d_models = generators[i], discriminators[i]
		# straight-through model
		d_models[0].trainable = False
		model1 = Sequential()
		model1.add(g_models[0])
		model1.add(d_models[0])
		model1.compile(loss='mse', optimizer=Adam(lr=0.001, beta_1=0, beta_2=0.99, epsilon=10e-8))
		# fade-in model
		d_models[1].trainable = False
		model2 = Sequential()
		model2.add(g_models[1])
		model2.add(d_models[1])
		model2.compile(loss='mse', optimizer=Adam(lr=0.001, beta_1=0, beta_2=0.99, epsilon=10e-8))
		# store
		model_list.append([model1, model2])
	return model_list

# define models
discriminators = define_discriminator(3)
# define models
generators = define_generator(100, 3)
# define composite models
composite = define_composite(discriminators, generators)

既然我们知道了如何定义所有的模型,我们就可以回顾一下在训练期间模型是如何更新的。

如何训练鉴别器和生成器模型

预定义生成器、鉴别器和复合模型是困难的部分;训练模型是直截了当的,很像训练任何其他 GAN。

重要的是,在每次训练迭代中,每个加权求和层中的阿尔法变量必须设置为新值。这必须为生成器和鉴别器模型中的层设置,并允许从旧模型层到新模型层的平滑线性过渡,例如,在固定次数的训练迭代中α值从 0 设置为 1。

下面的 update_fadein() 函数实现了这一点,并将循环遍历模型列表,并根据给定数量的训练步骤中的当前步骤设置每个模型的 alpha 值。您可能能够使用回调更优雅地实现这一点。

# update the alpha value on each instance of WeightedSum
def update_fadein(models, step, n_steps):
	# calculate current alpha (linear from 0 to 1)
	alpha = step / float(n_steps - 1)
	# update the alpha for each model
	for model in models:
		for layer in model.layers:
			if isinstance(layer, WeightedSum):
				backend.set_value(layer.alpha, alpha)

我们可以为给定数量的训练时期定义一个通用函数来训练给定的生成器、鉴别器和复合模型。

下面的*train _ epoch()*函数实现了这一点,首先在真实图像和伪图像上更新鉴别器模型,然后更新生成器模型,并根据数据集大小和 epoch 的数量对所需的训练迭代次数重复该过程。

该函数调用助手函数,用于通过生成真实样本()检索一批真实图像,用生成器生成一批假样本()生成一批假样本,并生成潜在空间中点的样本生成 _ 潜在点()。你可以很简单地自己定义这些函数。

# train a generator and discriminator
def train_epochs(g_model, d_model, gan_model, dataset, n_epochs, n_batch, fadein=False):
	# calculate the number of batches per training epoch
	bat_per_epo = int(dataset.shape[0] / n_batch)
	# calculate the number of training iterations
	n_steps = bat_per_epo * n_epochs
	# calculate the size of half a batch of samples
	half_batch = int(n_batch / 2)
	# manually enumerate epochs
	for i in range(n_steps):
		# update alpha for all WeightedSum layers when fading in new blocks
		if fadein:
			update_fadein([g_model, d_model, gan_model], i, n_steps)
		# prepare real and fake samples
		X_real, y_real = generate_real_samples(dataset, half_batch)
		X_fake, y_fake = generate_fake_samples(g_model, latent_dim, half_batch)
		# update discriminator model
		d_loss1 = d_model.train_on_batch(X_real, y_real)
		d_loss2 = d_model.train_on_batch(X_fake, y_fake)
		# update the generator via the discriminator's error
		z_input = generate_latent_points(latent_dim, n_batch)
		y_real2 = ones((n_batch, 1))
		g_loss = gan_model.train_on_batch(z_input, y_real2)
		# summarize loss on this batch
		print('>%d, d1=%.3f, d2=%.3f g=%.3f' % (i+1, d_loss1, d_loss2, g_loss))

图像必须根据每个模型的大小进行缩放。如果图像在内存中,我们可以定义一个简单的 scale_dataset()函数来缩放加载的图像。

在这种情况下,我们使用 scikit-image library 中的skimpage . transform . resize函数将 NumPy 像素阵列调整到所需的大小,并使用最近邻插值。

# scale images to preferred size
def scale_dataset(images, new_shape):
	images_list = list()
	for image in images:
		# resize with nearest neighbor interpolation
		new_image = resize(image, new_shape, 0)
		# store
		images_list.append(new_image)
	return asarray(images_list)

首先,基线模型必须适合给定数量的训练时期,例如输出 4×4 尺寸图像的模型。

这将需要将加载的图像缩放到由生成器模型输出层的形状定义的所需大小。

# fit the baseline model
g_normal, d_normal, gan_normal = g_models[0][0], d_models[0][0], gan_models[0][0]
# scale dataset to appropriate size
gen_shape = g_normal.output_shape
scaled_data = scale_dataset(dataset, gen_shape[1:])
print('Scaled Data', scaled_data.shape)
# train normal or straight-through models
train_epochs(g_normal, d_normal, gan_normal, scaled_data, e_norm, n_batch)

然后我们可以处理每一个增长水平,例如第一个是 8×8。

这包括首先检索模型,将数据缩放到合适的大小,然后拟合淡入模型,接着训练模型的直通版本进行微调。

我们可以对循环中的每个增长水平重复这一过程。

# process each level of growth
for i in range(1, len(g_models)):
	# retrieve models for this level of growth
	[g_normal, g_fadein] = g_models[i]
	[d_normal, d_fadein] = d_models[i]
	[gan_normal, gan_fadein] = gan_models[i]
	# scale dataset to appropriate size
	gen_shape = g_normal.output_shape
	scaled_data = scale_dataset(dataset, gen_shape[1:])
	print('Scaled Data', scaled_data.shape)
	# train fade-in models for next level of growth
	train_epochs(g_fadein, d_fadein, gan_fadein, scaled_data, e_fadein, n_batch)
	# train normal or straight-through models
	train_epochs(g_normal, d_normal, gan_normal, scaled_data, e_norm, n_batch)

我们可以把这个联系在一起,定义一个叫做 train() 的函数来训练渐进式增长 GAN 函数。

# train the generator and discriminator
def train(g_models, d_models, gan_models, dataset, latent_dim, e_norm, e_fadein, n_batch):
	# fit the baseline model
	g_normal, d_normal, gan_normal = g_models[0][0], d_models[0][0], gan_models[0][0]
	# scale dataset to appropriate size
	gen_shape = g_normal.output_shape
	scaled_data = scale_dataset(dataset, gen_shape[1:])
	print('Scaled Data', scaled_data.shape)
	# train normal or straight-through models
	train_epochs(g_normal, d_normal, gan_normal, scaled_data, e_norm, n_batch)
	# process each level of growth
	for i in range(1, len(g_models)):
		# retrieve models for this level of growth
		[g_normal, g_fadein] = g_models[i]
		[d_normal, d_fadein] = d_models[i]
		[gan_normal, gan_fadein] = gan_models[i]
		# scale dataset to appropriate size
		gen_shape = g_normal.output_shape
		scaled_data = scale_dataset(dataset, gen_shape[1:])
		print('Scaled Data', scaled_data.shape)
		# train fade-in models for next level of growth
		train_epochs(g_fadein, d_fadein, gan_fadein, scaled_data, e_fadein, n_batch, True)
		# train normal or straight-through models
		train_epochs(g_normal, d_normal, gan_normal, scaled_data, e_norm, n_batch)

正常阶段的纪元数量由 e_norm 参数定义,淡入阶段的纪元数量由 e_fadein 参数定义。

必须根据图像数据集的大小指定纪元的数量,并且每个阶段可以使用相同数量的纪元,如本文中所使用的。

我们从 4×4 分辨率开始,训练网络,直到我们总共显示出鉴别器 800k 的真实图像。然后,我们在两个阶段之间交替:在接下来的 800k 图像期间淡入第一个 3 层块,为 800k 图像稳定网络,在 800k 图像期间淡入下一个 3 层块,等等。

——为提高质量、稳定性和变化性而进行的肝的渐进式增长,2017 年。

然后我们可以像上一节一样定义我们的模型,然后调用训练函数。

# number of growth phase, e.g. 3 = 16x16 images
n_blocks = 3
# size of the latent space
latent_dim = 100
# define models
d_models = define_discriminator(n_blocks)
# define models
g_models = define_generator(100, n_blocks)
# define composite models
gan_models = define_composite(d_models, g_models)
# load image data
dataset = load_real_samples()
# train model
train(g_models, d_models, gan_models, dataset, latent_dim, 100, 100, 16)

进一步阅读

如果您想更深入地了解这个主题,本节将提供更多资源。

正式的

应用程序接口

文章

摘要

在本教程中,您发现了如何使用 Keras 从零开始开发渐进增长的生成对抗网络模型。

具体来说,您了解到:

  • 如何在输出图像增长的每个级别开发预定义的鉴别器和生成器模型。
  • 如何通过鉴别器模型定义用于训练生成器模型的复合模型?
  • 如何在输出图像增长的每一级循环训练模型的淡入版本和正常版本。

你有什么问题吗? 在下面的评论中提问,我会尽力回答。

如何实现评估 GANs 的 Frechet 初始距离

原文:machinelearningmastery.com/how-to-impl…

最后更新于 2019 年 10 月 11 日

Frechet 初始距离分数(简称 FID)是一种度量标准,用于计算为真实图像和生成图像计算的特征向量之间的距离。

分数总结了两组在使用用于图像分类的初始 v3 模型计算的原始图像的计算机视觉特征的统计方面有多相似。较低的分数表示两组图像更相似,或具有更相似的统计数据,满分为 0.0 表示两组图像相同。

FID 分数用于评估生成对抗网络生成的图像质量,较低的分数已被证明与较高质量的图像有很好的相关性。

在本教程中,您将发现如何实现 Frechet 初始距离来评估生成的图像。

完成本教程后,您将知道:

  • Frechet 初始距离汇总了同一域中真实图像和生成图像的初始特征向量之间的距离。
  • 如何在 NumPy 中计算 FID 分数并从零开始实现计算?
  • 如何利用 Keras 深度学习库实现 FID 评分,并用真实图像计算。

用我的新书Python 生成对抗网络启动你的项目,包括分步教程和所有示例的 Python 源代码文件。

我们开始吧。

  • 【2019 年 10 月更新:修复了方法描述中的小错别字。

How to Implement the Frechet Inception Distance (FID) From Scratch for Evaluating Generated Images

如何从零开始实现 Frechet 初始距离(FID)来评估生成的图像 照片由德诺比克,保留部分权利。

教程概述

本教程分为五个部分;它们是:

  1. 什么是 FrechetInception 距离?
  2. 如何计算弗雷歇初始距离
  3. 如何用 NumPy 实现 Frechet 初始距离
  4. 如何用 Keras 实现 Frechet 初始距离
  5. 如何计算真实图像的弗雷歇初始距离

什么是 FrechetInception 距离?

Frechet 初始距离,简称 FID,是一种评估生成图像质量的指标,专门用于评估生成对抗网络的表现。

FID 分数是由Martin heussel等人在他们 2017 年的论文中提出并使用的,该论文的标题为“通过两时间尺度更新规则训练的 GANs 收敛到局部纳什均衡

该分数是作为对现有初始得分(即 IS)的改进而提出的。

为了评估 GANs 在图像生成方面的表现,我们引入了“Frechet 初始距离”(FID),它比初始得分更好地捕捉了生成图像与真实图像的相似性。

——通过两时间尺度更新规则训练的 GANs 收敛到局部纳什均衡,2017

初始得分基于表现最好的图像分类模型初始 v3 将合成图像分类为 1000 个已知对象之一的程度来估计合成图像集合的质量。分数结合了每个合成图像的条件类别预测的置信度(质量)和预测类别的边缘概率的积分(多样性)。

初始得分没有记录合成图像与真实图像的对比情况。开发 FID 评分的目标是基于合成图像集合的统计数据与来自目标域的真实图像集合的统计数据进行比较来评估合成图像。

初始得分的缺点是没有使用真实世界样本的统计数据,并且与合成样本的统计数据进行比较。

——通过两时间尺度更新规则训练的 GANs 收敛到局部纳什均衡,2017。

与初始得分一样,FID 分数使用初始 v3 模型。具体而言,模型的编码层(图像输出分类之前的最后一个池化层)用于捕获输入图像的计算机视觉特定特征。这些激活是为真实和生成的图像集合计算的。

通过计算图像的平均值和协方差,激活被总结为多元高斯。然后计算真实和生成图像集合中激活的统计数据。

然后使用Frechet 距离计算这两个分布之间的距离,也称为 Wasserstein-2 距离。

两个高斯图像(合成图像和真实图像)的差异是通过 Frechet 距离(也称为 Wasserstein-2 距离)来测量的。

——通过两时间尺度更新规则训练的 GANs 收敛到局部纳什均衡,2017。

使用《Inception v3》模型中的激活来总结每张图像,为该分数命名为“FrechetInception 距离

较低的 FID 表示图像质量较好;相反,较高的分数表示较低质量的图像,并且该关系可以是线性的。

该分数的作者表明,当应用系统失真(如添加随机噪声和模糊)时,较低的 FID 分数与较高质量的图像相关。

Example of How Increased Distortion of an Image Correlates with High FID Score

图像失真增加如何与高 FID 分数相关的示例。 取自:两时间尺度更新规则训练的 GANs 收敛到局部纳什均衡。

如何计算弗雷歇初始距离

FID 分数是通过首先加载一个预先训练好的 Inception v3 模型来计算的。

移除模型的输出层,并将输出作为来自最后一个池化层的激活,即全局空间池化层

该输出层有 2,048 个激活,因此,每个图像被预测为 2,048 个激活特征。这被称为图像的编码向量或特征向量。

然后为来自问题域的真实图像集合预测 2,048 个特征向量,以提供真实图像如何表示的参考。然后可以为合成图像计算特征向量。

结果将是真实图像和生成图像的 2,048 个特征向量的两个集合。

然后,使用取自论文的以下等式计算 FID 分数:

  • d ^ 2 = | | mu _ 1-mu _ 2 | ^ 2+tr(c _ 1+c _ 2–2 * sqrt(c _ 1 * c _ 2))

分数被称为 ,表示它是一个距离,有平方单位。

μ_ 1”和“μ_ 2”是指真实图像和生成图像的特征平均,例如 2,048 个元素向量,其中每个元素是在图像上观察到的平均特征。

C_1C_2 是真实和生成的特征向量的协方差矩阵,通常称为σ。

| |μ_ 1–mu_2||²是指两个均值向量之间的平方差之和。 Tr 指的是迹线性代数运算,例如沿着方阵主对角线的元素之和。

sqrt 是方阵的平方根,作为两个协方差矩阵的乘积给出。

矩阵的平方根也常写成 M^(1/2) ,例如矩阵的二分之一次方,也有同样的效果。根据矩阵中的值,该操作可能会失败,因为该操作是使用数值方法求解的。通常,所得矩阵中的一些元素可能是虚的,这通常可以被检测和移除。

如何用 NumPy 实现 Frechet 初始距离

用 Python 中的 NumPy 数组实现 FID 分数的计算非常简单。

首先,让我们定义一个函数,该函数将获取真实和生成图像的激活集合,并返回 FID 分数。

下面列出的 calculate_fid() 函数执行该程序。

这里,我们几乎直接实现了 FID 计算。值得注意的是,TensorFlow 中的官方实现以稍微不同的顺序实现计算元素,可能是为了提高效率,并在矩阵平方根周围引入了额外的检查,以处理可能的数值不稳定性。

如果您在自己的数据集上计算 FID 时遇到问题,我建议查看官方实现并扩展下面的实现来添加这些检查。

# calculate frechet inception distance
def calculate_fid(act1, act2):
	# calculate mean and covariance statistics
	mu1, sigma1 = act1.mean(axis=0), cov(act1, rowvar=False)
	mu2, sigma2 = act2.mean(axis=0), cov(act2, rowvar=False)
	# calculate sum squared difference between means
	ssdiff = numpy.sum((mu1 - mu2)**2.0)
	# calculate sqrt of product between cov
	covmean = sqrtm(sigma1.dot(sigma2))
	# check and correct imaginary numbers from sqrt
	if iscomplexobj(covmean):
		covmean = covmean.real
	# calculate score
	fid = ssdiff + trace(sigma1 + sigma2 - 2.0 * covmean)
	return fid

然后我们可以测试这个函数来计算一些人为特征向量的初始得分。

特征向量可能包含小正值,长度为 2,048 个元素。我们可以用小随机数构造两批 10 幅图像的特征向量,如下所示:

...
# define two collections of activations
act1 = random(10*2048)
act1 = act1.reshape((10,2048))
act2 = random(10*2048)
act2 = act2.reshape((10,2048))

一个测试是计算一组激活和它本身之间的 FID,我们希望它的分数为 0.0。

然后,我们可以计算两组随机激活之间的距离,我们预计这将是一个很大的数字。

...
# fid between act1 and act1
fid = calculate_fid(act1, act1)
print('FID (same): %.3f' % fid)
# fid between act1 and act2
fid = calculate_fid(act1, act2)
print('FID (different): %.3f' % fid)

将这些结合在一起,完整的示例如下所示。

# example of calculating the frechet inception distance
import numpy
from numpy import cov
from numpy import trace
from numpy import iscomplexobj
from numpy.random import random
from scipy.linalg import sqrtm

# calculate frechet inception distance
def calculate_fid(act1, act2):
	# calculate mean and covariance statistics
	mu1, sigma1 = act1.mean(axis=0), cov(act1, rowvar=False)
	mu2, sigma2 = act2.mean(axis=0), cov(act2, rowvar=False)
	# calculate sum squared difference between means
	ssdiff = numpy.sum((mu1 - mu2)**2.0)
	# calculate sqrt of product between cov
	covmean = sqrtm(sigma1.dot(sigma2))
	# check and correct imaginary numbers from sqrt
	if iscomplexobj(covmean):
		covmean = covmean.real
	# calculate score
	fid = ssdiff + trace(sigma1 + sigma2 - 2.0 * covmean)
	return fid

# define two collections of activations
act1 = random(10*2048)
act1 = act1.reshape((10,2048))
act2 = random(10*2048)
act2 = act2.reshape((10,2048))
# fid between act1 and act1
fid = calculate_fid(act1, act1)
print('FID (same): %.3f' % fid)
# fid between act1 and act2
fid = calculate_fid(act1, act2)
print('FID (different): %.3f' % fid)

运行该示例首先报告 act1 激活和自身之间的 FID,正如我们所期望的,它是 0.0(注意:分数的符号可以忽略)。

两个随机激活集合之间的距离也如我们所料:一个很大的数字,在本例中是 358。

FID (same): -0.000
FID (different): 358.927

您可能想尝试计算 FID 分数,并测试其他病理病例。

如何用 Keras 实现 Frechet 初始距离

现在我们知道如何计算 FID 分数并在 NumPy 中实现它,我们可以在 Keras 中开发一个实现。

这包括准备图像数据,并使用预训练的初始 v3 模型来计算每个图像的激活或特征向量。

首先,我们可以直接在 Keras 中加载 Inception v3 模型。

...
# load inception v3 model
model = InceptionV3()

这将准备一个初始模型的版本,用于将图像分类为 1000 个已知类别之一。我们可以通过 include_top=False 参数移除模型的输出(顶部)。令人痛苦的是,这也删除了我们需要的全局平均池层,但是我们可以通过指定池='avg' 参数将其添加回来。

当模型的输出层被移除时,我们必须指定输入图像的形状,即 299x299x3 像素,例如 input_shape=(299,299,3) 参数。

因此,初始模型可以如下加载:

...
# prepare the inception v3 model
model = InceptionV3(include_top=False, pooling='avg', input_shape=(299,299,3))

然后,该模型可用于预测一幅或多幅图像的特征向量。

我们的图像可能没有所需的形状。我们将使用 scikit-image 库将像素值的 NumPy 数组调整到所需的大小。下面的 scale_images() 功能实现了这一点。

# scale an array of images to a new size
def scale_images(images, new_shape):
	images_list = list()
	for image in images:
		# resize with nearest neighbor interpolation
		new_image = resize(image, new_shape, 0)
		# store
		images_list.append(new_image)
	return asarray(images_list)

注意,您可能需要安装 scikit-image 库。这可以通过以下方式实现:

sudo pip install scikit-image

一旦调整大小,图像像素值也需要进行缩放,以满足对初始模型输入的期望。这可以通过调用*prepare _ input()*函数来实现。

我们可以更新上一节中定义的 calculate_fid() 函数,将加载的初始模型和图像数据的两个 NumPy 数组作为参数,而不是激活。然后,该功能将像以前一样,在计算 FID 分数之前计算激活。

calculate_fid() 功能的更新版本如下。

# calculate frechet inception distance
def calculate_fid(model, images1, images2):
	# calculate activations
	act1 = model.predict(images1)
	act2 = model.predict(images2)
	# calculate mean and covariance statistics
	mu1, sigma1 = act1.mean(axis=0), cov(act1, rowvar=False)
	mu2, sigma2 = act2.mean(axis=0), cov(act2, rowvar=False)
	# calculate sum squared difference between means
	ssdiff = numpy.sum((mu1 - mu2)**2.0)
	# calculate sqrt of product between cov
	covmean = sqrtm(sigma1.dot(sigma2))
	# check and correct imaginary numbers from sqrt
	if iscomplexobj(covmean):
		covmean = covmean.real
	# calculate score
	fid = ssdiff + trace(sigma1 + sigma2 - 2.0 * covmean)
	return fid

然后,我们可以用一些人为的图像集合来测试这个函数,在这种情况下,10 个 32×32 的图像,随机像素值在[0,255]范围内。

...
# define two fake collections of images
images1 = randint(0, 255, 10*32*32*3)
images1 = images1.reshape((10,32,32,3))
images2 = randint(0, 255, 10*32*32*3)
images2 = images2.reshape((10,32,32,3))

然后,我们可以将整数像素值转换为浮点值,并将其缩放至所需的 299×299 像素大小。

...
# convert integer to floating point values
images1 = images1.astype('float32')
images2 = images2.astype('float32')
# resize images
images1 = scale_images(images1, (299,299,3))
images2 = scale_images(images2, (299,299,3))

然后可以缩放像素值以满足 Inception v3 模型的期望。

...
# pre-process images
images1 = preprocess_input(images1)
images2 = preprocess_input(images2)

然后计算 FID 分数,首先在一组图像和自身之间,然后在两组图像之间。

...
# fid between images1 and images1
fid = calculate_fid(model, images1, images1)
print('FID (same): %.3f' % fid)
# fid between images1 and images2
fid = calculate_fid(model, images1, images2)
print('FID (different): %.3f' % fid)

将所有这些结合在一起,下面列出了完整的示例。

# example of calculating the frechet inception distance in Keras
import numpy
from numpy import cov
from numpy import trace
from numpy import iscomplexobj
from numpy import asarray
from numpy.random import randint
from scipy.linalg import sqrtm
from keras.applications.inception_v3 import InceptionV3
from keras.applications.inception_v3 import preprocess_input
from keras.datasets.mnist import load_data
from skimage.transform import resize

# scale an array of images to a new size
def scale_images(images, new_shape):
	images_list = list()
	for image in images:
		# resize with nearest neighbor interpolation
		new_image = resize(image, new_shape, 0)
		# store
		images_list.append(new_image)
	return asarray(images_list)

# calculate frechet inception distance
def calculate_fid(model, images1, images2):
	# calculate activations
	act1 = model.predict(images1)
	act2 = model.predict(images2)
	# calculate mean and covariance statistics
	mu1, sigma1 = act1.mean(axis=0), cov(act1, rowvar=False)
	mu2, sigma2 = act2.mean(axis=0), cov(act2, rowvar=False)
	# calculate sum squared difference between means
	ssdiff = numpy.sum((mu1 - mu2)**2.0)
	# calculate sqrt of product between cov
	covmean = sqrtm(sigma1.dot(sigma2))
	# check and correct imaginary numbers from sqrt
	if iscomplexobj(covmean):
		covmean = covmean.real
	# calculate score
	fid = ssdiff + trace(sigma1 + sigma2 - 2.0 * covmean)
	return fid

# prepare the inception v3 model
model = InceptionV3(include_top=False, pooling='avg', input_shape=(299,299,3))
# define two fake collections of images
images1 = randint(0, 255, 10*32*32*3)
images1 = images1.reshape((10,32,32,3))
images2 = randint(0, 255, 10*32*32*3)
images2 = images2.reshape((10,32,32,3))
print('Prepared', images1.shape, images2.shape)
# convert integer to floating point values
images1 = images1.astype('float32')
images2 = images2.astype('float32')
# resize images
images1 = scale_images(images1, (299,299,3))
images2 = scale_images(images2, (299,299,3))
print('Scaled', images1.shape, images2.shape)
# pre-process images
images1 = preprocess_input(images1)
images2 = preprocess_input(images2)
# fid between images1 and images1
fid = calculate_fid(model, images1, images1)
print('FID (same): %.3f' % fid)
# fid between images1 and images2
fid = calculate_fid(model, images1, images2)
print('FID (different): %.3f' % fid)

运行该示例首先总结了合成图像的形状及其重新缩放的版本,与我们的预期相符。

:第一次使用 InceptionV3 模型时,Keras 会下载模型权重保存到 ~/。工作站上的 keras/models/ 目录。权重约为 100 兆字节,根据您的互联网连接速度,下载可能需要一些时间。

给定的一组图像和它本身之间的 FID 分数是 0.0,正如我们所期望的,两个随机图像集合之间的距离大约是 35。

Prepared (10, 32, 32, 3) (10, 32, 32, 3)
Scaled (10, 299, 299, 3) (10, 299, 299, 3)
FID (same): -0.000
FID (different): 35.495

如何计算真实图像的弗雷歇初始距离

计算两组真实图像之间的 FID 分数可能很有用。

Keras 库提供了许多计算机视觉数据集,包括 CIFAR-10 数据集。这些是小尺寸 32×32 像素的彩色照片,分为训练和测试元素,可按如下方式加载:

...
# load cifar10 images
(images1, _), (images2, _) = cifar10.load_data()

训练数据集有 50,000 个图像,而测试数据集只有 10,000 个图像。计算这两个数据集之间的 FID 分数以了解测试数据集在训练数据集中的代表性可能会很有趣。

缩放和评分 50K 图像需要很长时间,因此,我们可以将“训练集”简化为 10K 随机样本,如下所示:

...
shuffle(images1)
images1 = images1[:10000]

将所有这些联系在一起,我们可以计算列车样本和测试数据集之间的 FID 分数,如下所示。

# example of calculating the frechet inception distance in Keras for cifar10
import numpy
from numpy import cov
from numpy import trace
from numpy import iscomplexobj
from numpy import asarray
from numpy.random import shuffle
from scipy.linalg import sqrtm
from keras.applications.inception_v3 import InceptionV3
from keras.applications.inception_v3 import preprocess_input
from keras.datasets.mnist import load_data
from skimage.transform import resize
from keras.datasets import cifar10

# scale an array of images to a new size
def scale_images(images, new_shape):
	images_list = list()
	for image in images:
		# resize with nearest neighbor interpolation
		new_image = resize(image, new_shape, 0)
		# store
		images_list.append(new_image)
	return asarray(images_list)

# calculate frechet inception distance
def calculate_fid(model, images1, images2):
	# calculate activations
	act1 = model.predict(images1)
	act2 = model.predict(images2)
	# calculate mean and covariance statistics
	mu1, sigma1 = act1.mean(axis=0), cov(act1, rowvar=False)
	mu2, sigma2 = act2.mean(axis=0), cov(act2, rowvar=False)
	# calculate sum squared difference between means
	ssdiff = numpy.sum((mu1 - mu2)**2.0)
	# calculate sqrt of product between cov
	covmean = sqrtm(sigma1.dot(sigma2))
	# check and correct imaginary numbers from sqrt
	if iscomplexobj(covmean):
		covmean = covmean.real
	# calculate score
	fid = ssdiff + trace(sigma1 + sigma2 - 2.0 * covmean)
	return fid

# prepare the inception v3 model
model = InceptionV3(include_top=False, pooling='avg', input_shape=(299,299,3))
# load cifar10 images
(images1, _), (images2, _) = cifar10.load_data()
shuffle(images1)
images1 = images1[:10000]
print('Loaded', images1.shape, images2.shape)
# convert integer to floating point values
images1 = images1.astype('float32')
images2 = images2.astype('float32')
# resize images
images1 = scale_images(images1, (299,299,3))
images2 = scale_images(images2, (299,299,3))
print('Scaled', images1.shape, images2.shape)
# pre-process images
images1 = preprocess_input(images1)
images2 = preprocess_input(images2)
# calculate fid
fid = calculate_fid(model, images1, images2)
print('FID: %.3f' % fid)

根据工作站的速度,运行该示例可能需要一些时间。

在运行结束时,我们可以看到训练数据集和测试数据集之间的 FID 分数约为 5。

Loaded (10000, 32, 32, 3) (10000, 32, 32, 3)
Scaled (10000, 299, 299, 3) (10000, 299, 299, 3)
FID: 5.492

进一步阅读

如果您想更深入地了解这个主题,本节将提供更多资源。

报纸

代码项目

应用程序接口

文章

摘要

在本教程中,您发现了如何实现 Frechet 初始距离来评估生成的图像。

具体来说,您了解到:

  • Frechet 初始距离汇总了同一域中真实图像和生成图像的初始特征向量之间的距离。
  • 如何在 NumPy 中计算 FID 分数并从零开始实现计算?
  • 如何利用 Keras 深度学习库实现 FID 评分,并用真实图像计算。

你有什么问题吗? 在下面的评论中提问,我会尽力回答。

如何实现评估 GANs 的初始得分

原文:machinelearningmastery.com/how-to-impl…

最后更新于 2019 年 10 月 11 日

生成对抗网络,简称 GANs,是一种深度学习神经网络架构,用于训练生成合成图像的生成器模型。

生成模型的一个问题是没有客观的方法来评估生成图像的质量。

因此,通常在模型训练过程中周期性地生成和保存图像,并使用对所生成图像的主观人类评估,以便评估所生成图像的质量并选择最终的生成器模型。

已经进行了许多尝试来建立生成的图像质量的客观度量。一个早期的、被广泛采用的生成图像客观评估方法的例子是初始得分。

在本教程中,您将发现评估生成图像质量的初始得分。

完成本教程后,您将知道:

  • 如何计算初始得分以及它所衡量的东西背后的直觉。
  • 如何用 NumPy 和 Keras 深度学习库在 Python 中实现初始得分。
  • 如何计算小型图像的初始得分,例如 CIFAR-10 数据集中的图像。

用我的新书Python 生成对抗网络启动你的项目,包括分步教程和所有示例的 Python 源代码文件。

我们开始吧。

  • 更新 2019 年 10 月:更新了均等分配示例的初始得分小 bug。

How to Implement the Inception Score (IS) From Scratch for Evaluating Generated Images

如何从零开始实现初始评分以评估生成的图像 照片由阿尔弗雷多·阿法托拍摄,保留部分权利。

教程概述

本教程分为五个部分;它们是:

  1. 《Inception》的评分是多少?
  2. 如何计算初始得分
  3. 如何用 NumPy 实现初始评分
  4. 如何用 Keras 实现初始评分
  5. 《Inception》评分的问题

《Inception》的评分是多少?

初始得分,简称 IS,是一个客观的度量标准,用于评估生成图像的质量,特别是由生成对抗网络模型输出的合成图像。

初始得分是由蒂姆·萨利曼(Tim Salimans)等人在 2016 年发表的题为“T2 训练 GANs 的改进技术”的论文中提出的

在论文中,作者使用了一个众包平台( Amazon Mechanical Turk )来评估大量 GAN 生成的图像。他们开发了初始得分,试图消除人类对图像的主观评估。

作者发现他们的分数与主观评估有很好的相关性。

作为人类注释器的替代,我们提出了一种自动评估样本的方法,我们发现该方法与人类评估有很好的相关性。

——训练 GANs 的改进技术,2016 年。

初始得分包括使用用于图像分类的预先训练的深度学习神经网络模型来对生成的图像进行分类。具体来说,克里斯蒂安·塞格迪(Christian Szegedy)等人在 2015 年发表的题为“重新思考计算机视觉的初始架构的论文中描述了初始 v3 模型对初始模型的依赖赋予了初始得分这个名字。

使用该模型对大量生成的图像进行分类。具体地,预测图像属于每个类别的概率。这些预测然后被总结成初始得分。

该评分旨在捕捉生成的图像集合的两个属性:

  • 图像质量。图像看起来像特定的对象吗?
  • 图像多样性。是否生成了广泛的对象?

初始得分的最低值为 1.0,最高值为分类模型支持的类别数;在这种情况下,Inception v3 模型支持 ILSVRC 2012 数据集的 1000 个类,因此,该数据集上的最高 Inception 分数为 1000。

CIFAR-10 数据集是分为 10 类对象的 50,000 幅图像的集合。介绍初始阶段的原始论文在真实的 CIFAR-10 训练数据集上计算了分数,获得了 11.24 +/- 0.12 的结果。

使用他们论文中介绍的 GAN 模型,他们在为该数据集生成合成图像时获得了 8.09+/-0.07 的初始得分。

如何计算初始得分

初始得分是通过首先使用预先训练的初始 v3 模型来预测每个生成图像的类别概率来计算的。

这些是条件概率,例如以生成的图像为条件的类别标签。与所有其他类别相比,被强分类为一个类别的图像表示高质量。因此,集合中所有生成图像的条件概率应该具有低熵

包含有意义对象的图像应该具有低熵的条件标签分布 p(y|x)。

——训练 GANs 的改进技术,2016 年。

熵的计算方法是每个观测概率的负和乘以概率的对数。这里的直觉是,大概率比小概率具有更少的信息。

  • 熵=-和(p_i * log(p_i))

条件概率抓住了我们对图像质量的兴趣。

为了捕捉我们对各种图像的兴趣,我们使用了边缘概率。这是所有生成图像的概率分布。因此,我们更希望边缘概率分布的积分具有高熵。

此外,我们期望该模型生成各种图像,因此边缘积分 p(y|x = G(z))dz 应该具有高熵。

——训练 GANs 的改进技术,2016 年。

这些元素通过计算条件概率分布和边缘概率分布之间的库尔巴克-莱布勒散度或 KL 散度(相对熵)来组合。

计算两个分布之间的散度是使用“||”运算符编写的,因此我们可以说我们对条件分布的 C 和边际分布的 M 之间的 KL 散度感兴趣,或者:

  • KL (C || M)

具体来说,我们对所有生成图像的 KL 散度的平均值感兴趣。

结合这两个需求,我们提出的度量是:exp(Ex KL(p(y|x)||p(y))。

——训练 GANs 的改进技术,2016 年。

我们不需要翻译初始得分的计算。值得庆幸的是,论文的作者还提供了 GitHub 上的源代码,其中包含了一个实现的初始得分

分数的计算假设一系列对象的大量图像,例如 50,000。

将图像分成 10 组,例如每组 5,000 幅图像,并对每组图像计算初始得分,然后报告分数的平均值和标准偏差。

对一组图像的初始得分的计算包括首先使用初始 v3 模型来计算每个图像的条件概率(p(y|x))。然后,边缘概率被计算为组中图像的条件概率的平均值(p(y))。

然后为每个图像计算 KL 散度,作为条件概率乘以条件概率的对数减去边缘概率的对数。

  • KL 散度= p(y | x)*(log(p(y | x))–log(p(y)))

然后在所有图像上对 KL 散度求和,并在所有类别上求平均值,计算结果的指数以给出最终分数。

这定义了在大多数使用分数的论文中报告时使用的官方初始得分实现,尽管在如何计算分数上确实存在差异。

如何用 NumPy 实现初始评分

用 NumPy 数组在 Python 中实现初始得分的计算非常简单。

首先,让我们定义一个函数,它将收集条件概率并计算初始得分。

下面列出的*calculate _ initiation _ score()*函数执行该过程。

一个小的变化是在计算对数概率时引入了一个ε(一个接近于零的微小数字),以避免在试图计算零概率的对数时爆炸。这在实践中可能是不需要的(例如,对于真实生成的图像),但是在这里是有用的,并且是处理对数概率时的良好实践。

# calculate the inception score for p(y|x)
def calculate_inception_score(p_yx, eps=1E-16):
	# calculate p(y)
	p_y = expand_dims(p_yx.mean(axis=0), 0)
	# kl divergence for each image
	kl_d = p_yx * (log(p_yx + eps) - log(p_y + eps))
	# sum over classes
	sum_kl_d = kl_d.sum(axis=1)
	# average over images
	avg_kl_d = mean(sum_kl_d)
	# undo the logs
	is_score = exp(avg_kl_d)
	return is_score

然后我们可以测试这个函数来计算一些人为条件概率的初始得分。

我们可以想象三类图像的情况,以及对于三个图像的每一类的完美自信预测。

# conditional probabilities for high quality images
p_yx = asarray([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]])

我们希望这个案例的初始得分是 3.0(或者非常接近)。这是因为我们对于每个图像类都有相同数量的图像(三个类中的每一个都有一个图像),并且每个条件概率都是最有把握的。

下面列出了计算这些概率的初始得分的完整示例。

# calculate inception score in numpy
from numpy import asarray
from numpy import expand_dims
from numpy import log
from numpy import mean
from numpy import exp

# calculate the inception score for p(y|x)
def calculate_inception_score(p_yx, eps=1E-16):
	# calculate p(y)
	p_y = expand_dims(p_yx.mean(axis=0), 0)
	# kl divergence for each image
	kl_d = p_yx * (log(p_yx + eps) - log(p_y + eps))
	# sum over classes
	sum_kl_d = kl_d.sum(axis=1)
	# average over images
	avg_kl_d = mean(sum_kl_d)
	# undo the logs
	is_score = exp(avg_kl_d)
	return is_score

# conditional probabilities for high quality images
p_yx = asarray([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]])
score = calculate_inception_score(p_yx)
print(score)

运行该示例给出的预期得分为 3.0(或非常接近的数字)。

2.999999999999999

我们也可以试试最坏的情况。

这就是为什么我们对于每个类仍然有相同数量的图像(三个类中的每一个都有一个),但是对象是未知的,这给出了每个类的统一预测概率分布。

# conditional probabilities for low quality images
p_yx = asarray([[0.33, 0.33, 0.33], [0.33, 0.33, 0.33], [0.33, 0.33, 0.33]])
score = calculate_inception_score(p_yx)
print(score)

在这种情况下,我们期望初始得分是最差的,条件分布和边际分布之间没有差异,例如初始得分为 1.0。

将这些联系在一起,完整的示例如下所示。

# calculate inception score in numpy
from numpy import asarray
from numpy import expand_dims
from numpy import log
from numpy import mean
from numpy import exp

# calculate the inception score for p(y|x)
def calculate_inception_score(p_yx, eps=1E-16):
	# calculate p(y)
	p_y = expand_dims(p_yx.mean(axis=0), 0)
	# kl divergence for each image
	kl_d = p_yx * (log(p_yx + eps) - log(p_y + eps))
	# sum over classes
	sum_kl_d = kl_d.sum(axis=1)
	# average over images
	avg_kl_d = mean(sum_kl_d)
	# undo the logs
	is_score = exp(avg_kl_d)
	return is_score

# conditional probabilities for low quality images
p_yx = asarray([[0.33, 0.33, 0.33], [0.33, 0.33, 0.33], [0.33, 0.33, 0.33]])
score = calculate_inception_score(p_yx)
print(score)

运行该示例报告预期的初始得分为 1.0。

1.0

您可能想尝试计算初始得分,并测试其他病理情况。

如何用 Keras 实现初始评分

现在我们知道如何计算初始得分并在 Python 中实现它,我们可以在 Keras 中开发一个实现。

这包括使用真实的 Inception v3 模型对图像进行分类,并在图像集合的多个分割中平均计算得分。

首先,我们可以直接在 Keras 中加载 Inception v3 模型。

...
# load inception v3 model
model = InceptionV3()

该模型期望图像是彩色的,并且具有 299×299 像素的形状。

此外,像素值必须以与训练数据图像相同的方式进行缩放,然后才能进行分类。

这可以通过将像素值从整数转换为浮点值,然后为图像调用*prepare _ input()*函数来实现。

...
# convert from uint8 to float32
processed = images.astype('float32')
# pre-process raw images for inception v3 model
processed = preprocess_input(processed)

然后,可以为图像预测 1000 个图像类别中的每一个的条件概率。

...
# predict class probabilities for images
yhat = model.predict(images)

然后,可以像我们在上一节中所做的那样,直接在概率的 NumPy 数组上计算初始得分。

在此之前,我们必须将条件概率分成组,由 n_split 参数控制,并设置为原始论文中使用的默认值 10。

...
n_part = floor(images.shape[0] / n_split)

然后,我们可以在 n_part 图像或预测的块中枚举条件概率,并计算初始得分。

...
# retrieve p(y|x)
ix_start, ix_end = i * n_part, (i+1) * n_part
p_yx = yhat[ix_start:ix_end]

在计算了条件概率的每个分割的分数之后,我们可以计算并返回平均和标准偏差初始得分。

...
# average across images
is_avg, is_std = mean(scores), std(scores)

将所有这些联系在一起,下面的*calculate _ inception _ score()*函数获取一组预期大小和像素值在[0,255]的图像,并使用 Keras 中的 inception v3 模型计算平均和标准差的 inception 分数。

# assumes images have the shape 299x299x3, pixels in [0,255]
def calculate_inception_score(images, n_split=10, eps=1E-16):
	# load inception v3 model
	model = InceptionV3()
	# convert from uint8 to float32
	processed = images.astype('float32')
	# pre-process raw images for inception v3 model
	processed = preprocess_input(processed)
	# predict class probabilities for images
	yhat = model.predict(processed)
	# enumerate splits of images/predictions
	scores = list()
	n_part = floor(images.shape[0] / n_split)
	for i in range(n_split):
		# retrieve p(y|x)
		ix_start, ix_end = i * n_part, i * n_part + n_part
		p_yx = yhat[ix_start:ix_end]
		# calculate p(y)
		p_y = expand_dims(p_yx.mean(axis=0), 0)
		# calculate KL divergence using log probabilities
		kl_d = p_yx * (log(p_yx + eps) - log(p_y + eps))
		# sum over classes
		sum_kl_d = kl_d.sum(axis=1)
		# average over images
		avg_kl_d = mean(sum_kl_d)
		# undo the log
		is_score = exp(avg_kl_d)
		# store
		scores.append(is_score)
	# average across images
	is_avg, is_std = mean(scores), std(scores)
	return is_avg, is_std

我们可以用 50 幅所有像素值为 1.0 的人工图像来测试这个函数。

...
# pretend to load images
images = ones((50, 299, 299, 3))
print('loaded', images.shape)

这将计算每组五幅图像的分数,低质量将表明平均初始得分为 1.0。

下面列出了完整的示例。

# calculate inception score with Keras
from math import floor
from numpy import ones
from numpy import expand_dims
from numpy import log
from numpy import mean
from numpy import std
from numpy import exp
from keras.applications.inception_v3 import InceptionV3
from keras.applications.inception_v3 import preprocess_input

# assumes images have the shape 299x299x3, pixels in [0,255]
def calculate_inception_score(images, n_split=10, eps=1E-16):
	# load inception v3 model
	model = InceptionV3()
	# convert from uint8 to float32
	processed = images.astype('float32')
	# pre-process raw images for inception v3 model
	processed = preprocess_input(processed)
	# predict class probabilities for images
	yhat = model.predict(processed)
	# enumerate splits of images/predictions
	scores = list()
	n_part = floor(images.shape[0] / n_split)
	for i in range(n_split):
		# retrieve p(y|x)
		ix_start, ix_end = i * n_part, i * n_part + n_part
		p_yx = yhat[ix_start:ix_end]
		# calculate p(y)
		p_y = expand_dims(p_yx.mean(axis=0), 0)
		# calculate KL divergence using log probabilities
		kl_d = p_yx * (log(p_yx + eps) - log(p_y + eps))
		# sum over classes
		sum_kl_d = kl_d.sum(axis=1)
		# average over images
		avg_kl_d = mean(sum_kl_d)
		# undo the log
		is_score = exp(avg_kl_d)
		# store
		scores.append(is_score)
	# average across images
	is_avg, is_std = mean(scores), std(scores)
	return is_avg, is_std

# pretend to load images
images = ones((50, 299, 299, 3))
print('loaded', images.shape)
# calculate inception score
is_avg, is_std = calculate_inception_score(images)
print('score', is_avg, is_std)

运行该示例首先定义 50 个假图像,然后计算每个批次的初始得分,并报告预期的初始得分为 1.0,标准偏差为 0.0。

:第一次使用 InceptionV3 模型时,Keras 会下载模型权重保存到 ~/。工作站上的 keras/models/ 目录。权重约为 100 兆字节,根据您的互联网连接速度,下载可能需要一些时间。

loaded (50, 299, 299, 3)
score 1.0 0.0

我们可以在一些真实图像上测试初始得分的计算。

Keras 应用编程接口提供对 CIFAR-10 数据集的访问。

这些是小尺寸 32×32 像素的彩色照片。首先,我们可以将图像分成组,然后将图像上采样到预期的大小 299×299,对像素值进行预处理,预测类概率,然后计算初始得分。

如果您打算在自己生成的图像上计算初始得分,这将是一个有用的例子,因为您可能必须将图像缩放到初始 v3 模型的预期大小,或者更改模型来为您执行上采样。

首先,图像可以被加载和混洗,以确保每个分割覆盖不同的类集。

...
# load cifar10 images
(images, _), (_, _) = cifar10.load_data()
# shuffle images
shuffle(images)

接下来,我们需要一种缩放图像的方法。

我们将使用 scikit-image 库将像素值的 NumPy 数组调整到所需的大小。下面的 scale_images() 功能实现了这一点。

# scale an array of images to a new size
def scale_images(images, new_shape):
	images_list = list()
	for image in images:
		# resize with nearest neighbor interpolation
		new_image = resize(image, new_shape, 0)
		# store
		images_list.append(new_image)
	return asarray(images_list)

注意,如果尚未安装 scikit-image 库,您可能需要安装它。这可以通过以下方式实现:

sudo pip install scikit-image

然后,我们可以列举拆分的数量,选择图像的子集,缩放它们,预处理它们,并使用该模型来预测条件类概率。

...
# retrieve images
ix_start, ix_end = i * n_part, (i+1) * n_part
subset = images[ix_start:ix_end]
# convert from uint8 to float32
subset = subset.astype('float32')
# scale images to the required size
subset = scale_images(subset, (299,299,3))
# pre-process images, scale to [-1,1]
subset = preprocess_input(subset)
# predict p(y|x)
p_yx = model.predict(subset)

初始得分的其余计算是相同的。

将所有这些结合起来,下面列出了在真实的 CIFAR-10 训练数据集上计算初始得分的完整示例。

基于最初的初始得分论文中报告的类似计算,我们预计该数据集上报告的分数大约为 11。有趣的是,在使用渐进式增长 GAN 编写时,生成图像的 CIFAR-10 的最佳初始得分约为 8.8。

# calculate inception score for cifar-10 in Keras
from math import floor
from numpy import ones
from numpy import expand_dims
from numpy import log
from numpy import mean
from numpy import std
from numpy import exp
from numpy.random import shuffle
from keras.applications.inception_v3 import InceptionV3
from keras.applications.inception_v3 import preprocess_input
from keras.datasets import cifar10
from skimage.transform import resize
from numpy import asarray

# scale an array of images to a new size
def scale_images(images, new_shape):
	images_list = list()
	for image in images:
		# resize with nearest neighbor interpolation
		new_image = resize(image, new_shape, 0)
		# store
		images_list.append(new_image)
	return asarray(images_list)

# assumes images have any shape and pixels in [0,255]
def calculate_inception_score(images, n_split=10, eps=1E-16):
	# load inception v3 model
	model = InceptionV3()
	# enumerate splits of images/predictions
	scores = list()
	n_part = floor(images.shape[0] / n_split)
	for i in range(n_split):
		# retrieve images
		ix_start, ix_end = i * n_part, (i+1) * n_part
		subset = images[ix_start:ix_end]
		# convert from uint8 to float32
		subset = subset.astype('float32')
		# scale images to the required size
		subset = scale_images(subset, (299,299,3))
		# pre-process images, scale to [-1,1]
		subset = preprocess_input(subset)
		# predict p(y|x)
		p_yx = model.predict(subset)
		# calculate p(y)
		p_y = expand_dims(p_yx.mean(axis=0), 0)
		# calculate KL divergence using log probabilities
		kl_d = p_yx * (log(p_yx + eps) - log(p_y + eps))
		# sum over classes
		sum_kl_d = kl_d.sum(axis=1)
		# average over images
		avg_kl_d = mean(sum_kl_d)
		# undo the log
		is_score = exp(avg_kl_d)
		# store
		scores.append(is_score)
	# average across images
	is_avg, is_std = mean(scores), std(scores)
	return is_avg, is_std

# load cifar10 images
(images, _), (_, _) = cifar10.load_data()
# shuffle images
shuffle(images)
print('loaded', images.shape)
# calculate inception score
is_avg, is_std = calculate_inception_score(images)
print('score', is_avg, is_std)

运行该示例将加载数据集,准备模型,并计算 CIFAR-10 训练数据集的初始得分。

我们可以看到得分是 11.3,接近预期得分 11.24。

注意:第一次使用 CIFAR-10 数据集时,Keras 会下载压缩格式的图片,存储在 ~/。keras/datasets/ 目录。下载量约为 161 兆字节,根据您的互联网连接速度,可能需要几分钟。

loaded (50000, 32, 32, 3)
score 11.317895 0.14821531

《Inception》评分的问题

初始得分是有效的,但并不完美。

通常,初始得分适用于模型已知的用于计算条件类概率的对象的生成图像。

在这种情况下,因为使用了初始 v3 模型,这意味着它最适合 ILSVRC 2012 数据集中使用的 1000 种对象类型。这是一个很大的类,但不是我们感兴趣的所有对象。

您可以在这里看到完整的课程列表:

它还要求图像是正方形的,并且具有大约 300×300 像素的相对较小的尺寸,包括将生成的图像缩放到该尺寸所需的任何缩放。

一个好的分数还需要生成的图像在模型支持的可能对象中有一个好的分布,并且每个类有接近偶数个例子。对于许多不能控制生成对象类型的 GAN 模型来说,这是很难控制的。

Shane Barratt 和 Rishi Sharma 在他们 2018 年发表的题为“关于《Inception》评分的注释的论文中,仔细查看了《Inception》评分,并列出了一些技术问题和边缘案例如果你想潜得更深,这是一个很好的参考。

进一步阅读

如果您想更深入地了解这个主题,本节将提供更多资源。

报纸

项目

应用程序接口

文章

摘要

在本教程中,您发现了评估生成图像质量的初始得分。

具体来说,您了解到:

  • 如何计算初始得分以及它所衡量的东西背后的直觉。
  • 如何用 NumPy 和 Keras 深度学习库在 Python 中实现初始得分。
  • 如何计算小型图像的初始得分,例如 CIFAR-10 数据集中的图像。

你有什么问题吗? 在下面的评论中提问,我会尽力回答。