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

53 阅读1小时+

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

原文:Machine Learning Mastery

协议:CC BY-NC-SA 4.0

如何用 Keras 从零开始实现 CycleGAN 模型

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

循环生成对抗网络,简称 CycleGAN,是一个用于将图像从一个域转换到另一个域的生成器模型。

例如,该模型可用于将马的图像转换为斑马的图像,或者将夜间城市景观的照片转换为白天的城市景观。

CycleGAN 模型的好处是可以在没有成对例子的情况下进行训练。也就是说,为了训练模型,它不需要翻译前后的照片示例,例如白天和晚上同一城市景观的照片。相反,它能够使用来自每个领域的照片集合,并提取和利用集合中图像的潜在风格来执行翻译。

这个模型给人留下了深刻的印象,但是对于初学者来说,它的架构看起来相当复杂。

在本教程中,您将发现如何使用 Keras 深度学习框架从零开始实现 CycleGAN 架构。

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

  • 如何实现鉴别器和生成器模型?
  • 如何定义复合模型,通过对抗损失和周期损失来训练发电机模型?
  • 如何实现每次训练迭代更新模型权重的训练过程?

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

我们开始吧。

How to Develop CycleGAN Models From Scratch With Keras

如何用 Keras 从零开始开发 CycleGAN 模型 图片由 anokarina 提供,保留部分权利。

教程概述

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

  1. 什么是 CycleGAN 架构?
  2. 如何实现循环根鉴别器模型
  3. 如何实现循环生成器模型
  4. 如何实现最小二乘和周期损失的复合模型
  5. 如何更新鉴别器和生成器模型

什么是 CycleGAN 架构?

朱俊彦等人在 2017 年的论文《使用循环一致对抗网络的不成对图像到图像的翻译》中描述了循环根模型

模型架构由两个生成器模型组成:一个生成器(生成器-A)用于为第一个域(域-A)生成图像,第二个生成器(生成器-B)用于为第二个域(域-B)生成图像。

  • 生成器-A ->域-A
  • 生成器-B ->域-B

生成器模型执行图像转换,这意味着图像生成过程取决于输入图像,特别是来自另一个域的图像。生成器-A 从域-B 获取图像作为输入,生成器-B 从域-A 获取图像作为输入。

  • 域-B ->生成器-A ->域-A
  • 域-A ->生成器-B ->域-B

每个生成器都有相应的鉴别器模型。

第一个鉴别器模型(鉴别器-A)从域-A 获取真实图像,从生成器-A 获取生成的图像,并预测它们是真实的还是伪造的。第二个鉴别器模型(Discriminator-B)从 Domain-B 获取真实图像,从 Generator-B 获取生成的图像,并预测它们是真实的还是伪造的。

  • 域-A ->鉴别器-A->[真/假]
  • 域-B ->生成器-A ->鉴别器-A->[真/假]
  • 域-B ->鉴别器-B->[真/假]
  • 域-A ->生成器-B ->鉴别器-B->[真/假]

鉴别器和生成器模型是在一个对抗性的零和过程中训练的,就像正常的 GAN 模型一样。

生成器学会更好地欺骗鉴别器,鉴别器学会更好地检测假图像。模型一起在训练过程中找到平衡。

此外,生成器模型被正则化不仅是为了在目标域中创建新图像,而是为了从源域创建输入图像的翻译版本。这是通过将生成的图像用作相应生成器模型的输入并将输出图像与原始图像进行比较来实现的。

将图像通过两个发生器称为一个循环。每对生成器模型一起被训练以更好地再现原始源图像,这被称为周期一致性。

  • 域-B ->生成器-A ->域-A ->生成器-B ->域-B
  • 域-A ->生成器-B ->域-B ->生成器-A ->域-A

该体系结构还有一个称为身份映射的元素。

这是发生器被提供有作为来自目标域的输入的图像的地方,并且期望生成相同的图像而不改变。对体系结构的这种添加是可选的,尽管它会使输入图像的颜色轮廓更好地匹配。

  • 领域-A ->生成器-A ->领域-A
  • 域-B ->生成器-B ->域-B

既然我们已经熟悉了模型架构,我们就可以依次仔细看看每个模型,以及它们是如何实现的。

论文很好地描述了模型和训练过程,尽管官方火炬实现被用作每个模型和训练过程的最终描述,并为下面描述的模型实现提供了基础。

如何实现循环根鉴别器模型

鉴别器模型负责将真实或生成的图像作为输入,并预测它是真的还是假的。

鉴别器模型被实现为 PatchGAN 模型。

对于鉴别器网络,我们使用 70 × 70 面片,其目的是分类 70 × 70 重叠图像面片是真的还是假的。

——使用循环一致对抗网络的不成对图像到图像转换,2017。

PatchGAN 在 2016 年发表的题为“利用马尔可夫生成对抗网络的预计算实时纹理合成”的论文中进行了描述,并在 2016 年发表的题为“利用条件对抗网络的图像到图像转换的论文中描述的用于图像转换的 pix2pix 模型中使用

该架构被描述为通过对源图像的 nxn 个正方形或小块的预测进行平均来区分输入图像是真的还是假的。

……我们设计了一个鉴别器架构——我们称之为 PatchGAN——它只在补丁的规模上惩罚结构。这个鉴别器试图分类图像中的每个 NxN 补丁是真的还是假的。我们在图像上运行这个鉴别器卷积,平均所有响应,以提供 d 的最终输出。

——条件对抗网络下的图像到图像转换,2016。

这可以通过使用稍微标准的深度卷积鉴别器模型来直接实现。

PatchGAN 鉴别器模型可以输出预测的正方形或单通道特征图,而不是像传统的鉴别器模型那样输出单个值。70×70 是指模型对输入的有效感受野,而不是输出特征图的实际形状。

卷积层的感受野是指该层的一个输出映射到该层的输入中的像素数。有效感受野是指深度卷积模型(多层)输出中的一个像素到输入图像的映射。这里,PatchGAN 是一种基于有效感受野设计深度卷积网络的方法,其中模型的一个输出激活映射到输入图像的 70×70 块,而与输入图像的大小无关。

PatchGAN 具有预测输入图像中每个 70×70 的面片是真的还是假的效果。然后,这些预测可以被平均以给出模型的输出(如果需要的话),或者直接与期望值(例如 0 或 1 值)的矩阵(或者向量,如果平坦的话)进行比较。

本文描述的鉴别器模型以 256×256 彩色图像作为输入,并定义了一个用于所有测试问题的显式体系结构。该架构使用了 Conv2D-instanceorm-LeakyReLU 层的块,具有 4×4 过滤器2×2 步长

让 Ck 表示一个 4×4 卷积-实例-泄漏层,带有 k 个过滤器和步长 2。在最后一层之后,我们应用卷积来产生一维输出。我们没有对第一个 C64 层使用实例表单。我们使用斜率为 0.2 的泄漏 ReLUs。

——使用循环一致对抗网络的不成对图像到图像转换,2017。

鉴别器的架构如下:

  • C64-C128-C256-C512

这在 CycleGAN 和 Pix2Pix 命名法中被称为 3 层 PatchGAN,因为排除了第一个隐藏层,该模型有三个隐藏层,可以放大或缩小以给出不同大小的 PatchGAN 模型。

该模型没有在论文中列出,它还有一个最终隐藏层 C512,其步长为1×1,以及一个输出层 C1,其步长也为 1×1,具有线性激活功能。由于该模型主要使用 256×256 大小的图像作为输入,因此激活的输出特征图的大小为 16×16。如果使用 128×128 的图像作为输入,那么激活的输出特征图的大小将是 8×8。

模型不使用批量归一化;相反,使用实例规范化。

实例规范化在 2016 年发表的题为“实例规范化:快速风格化的缺失要素”的论文中有所描述这是一种非常简单的标准化类型,包括标准化(例如,缩放至标准高斯)每个要素图上的值。

其目的是在图像生成过程中从图像中去除特定于图像的对比度信息,从而生成更好的图像。

关键思想是用实例规范化层替换生成器体系结构中的批处理规范化层,并在测试时保留它们(而不是像批处理规范化那样冻结和简化它们)。直观地说,规范化过程允许从内容图像中移除特定于实例的对比度信息,这简化了生成。实际上,这导致了图像的极大改善。

——实例规范化:快速风格化的缺失成分,2016。

虽然是为生成器模型设计的,但它也可以在鉴别器模型中证明是有效的。

keras-contrib 项目中提供了实例规范化的实现,该项目提供了对社区提供的 keras 特性的早期访问。

keras-contrib 库可以通过 pip 安装,如下所示:

sudo pip install git+https://www.github.com/keras-team/keras-contrib.git

或者,如果您使用的是 Anaconda 虚拟环境,如 EC2 上的:

git clone https://www.github.com/keras-team/keras-contrib.git
cd keras-contrib
sudo ~/anaconda3/envs/tensorflow_p36/bin/python setup.py install

新的实例化层可以如下使用:

...
from keras_contrib.layers.normalization.instancenormalization import InstanceNormalization
# define layer
layer = InstanceNormalization(axis=-1)
...

“轴”参数设置为-1,以确保每个要素地图的要素都是标准化的。

网络权重被初始化为标准偏差为 0.02 的高斯随机数,如对 DCGANs 更一般的描述。

权重由高斯分布 N (0,0.02)初始化。

——使用循环一致对抗网络的不成对图像到图像转换,2017。

使用最小二乘损失(L2),即所谓的最小二乘生成对抗网络(LSGAN)更新鉴别器模型。

……我们用最小二乘损失代替负对数似然目标。这种损失在训练期间更稳定,并产生更高质量的结果。

——使用循环一致对抗网络的不成对图像到图像转换,2017。

这可以使用真实图像的类=1 和伪图像的类=0 的目标值之间的“均方误差”来实现。

此外,论文建议在训练期间将鉴别器的损失减半,以减缓鉴别器相对于生成器的更新。

在实践中,我们在优化 D 的同时将目标除以 2,相对于 g 的学习速度,这样会减慢 D 的学习速度。

——使用循环一致对抗网络的不成对图像到图像转换,2017。

这可以通过在编译模型时将“ loss_weights ”参数设置为 0.5 来实现。请注意,当更新在 fDx_basic()函数中定义的鉴别器模型时,该权重似乎没有在官方的 Torch 实现中实现。

在下面的例子中,我们可以用定义 PatchGAN 鉴别器的 define_discriminator() 函数将所有这些联系在一起。模型配置与本文附录中的描述相匹配,附加详细信息来自Definited _ n _ layers()函数中定义的官方 Torch 实现。

# example of defining a 70x70 patchgan discriminator model
from keras.optimizers import Adam
from keras.initializers import RandomNormal
from keras.models import Model
from keras.models import Input
from keras.layers import Conv2D
from keras.layers import LeakyReLU
from keras.layers import Activation
from keras.layers import Concatenate
from keras.layers import BatchNormalization
from keras_contrib.layers.normalization.instancenormalization import InstanceNormalization
from keras.utils.vis_utils import plot_model

# define the discriminator model
def define_discriminator(image_shape):
	# weight initialization
	init = RandomNormal(stddev=0.02)
	# source image input
	in_image = Input(shape=image_shape)
	# C64
	d = Conv2D(64, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(in_image)
	d = LeakyReLU(alpha=0.2)(d)
	# C128
	d = Conv2D(128, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(d)
	d = InstanceNormalization(axis=-1)(d)
	d = LeakyReLU(alpha=0.2)(d)
	# C256
	d = Conv2D(256, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(d)
	d = InstanceNormalization(axis=-1)(d)
	d = LeakyReLU(alpha=0.2)(d)
	# C512
	d = Conv2D(512, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(d)
	d = InstanceNormalization(axis=-1)(d)
	d = LeakyReLU(alpha=0.2)(d)
	# second last output layer
	d = Conv2D(512, (4,4), padding='same', kernel_initializer=init)(d)
	d = InstanceNormalization(axis=-1)(d)
	d = LeakyReLU(alpha=0.2)(d)
	# patch output
	patch_out = Conv2D(1, (4,4), padding='same', kernel_initializer=init)(d)
	# define model
	model = Model(in_image, patch_out)
	# compile model
	model.compile(loss='mse', optimizer=Adam(lr=0.0002, beta_1=0.5), loss_weights=[0.5])
	return model

# define image shape
image_shape = (256,256,3)
# create the model
model = define_discriminator(image_shape)
# summarize the model
model.summary()
# plot the model
plot_model(model, to_file='discriminator_model_plot.png', show_shapes=True, show_layer_names=True)

:*plot _ model()*功能要求同时安装 pydot 和 pygraphviz 库。如果这是一个问题,您可以注释掉这个函数的导入和调用。

运行该示例总结了显示每个层的大小输入和输出的模型。

_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
input_1 (InputLayer)         (None, 256, 256, 3)       0
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 128, 128, 64)      3136
_________________________________________________________________
leaky_re_lu_1 (LeakyReLU)    (None, 128, 128, 64)      0
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 64, 64, 128)       131200
_________________________________________________________________
instance_normalization_1 (In (None, 64, 64, 128)       256
_________________________________________________________________
leaky_re_lu_2 (LeakyReLU)    (None, 64, 64, 128)       0
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 32, 32, 256)       524544
_________________________________________________________________
instance_normalization_2 (In (None, 32, 32, 256)       512
_________________________________________________________________
leaky_re_lu_3 (LeakyReLU)    (None, 32, 32, 256)       0
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 16, 16, 512)       2097664
_________________________________________________________________
instance_normalization_3 (In (None, 16, 16, 512)       1024
_________________________________________________________________
leaky_re_lu_4 (LeakyReLU)    (None, 16, 16, 512)       0
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 16, 16, 512)       4194816
_________________________________________________________________
instance_normalization_4 (In (None, 16, 16, 512)       1024
_________________________________________________________________
leaky_re_lu_5 (LeakyReLU)    (None, 16, 16, 512)       0
_________________________________________________________________
conv2d_6 (Conv2D)            (None, 16, 16, 1)         8193
=================================================================
Total params: 6,962,369
Trainable params: 6,962,369
Non-trainable params: 0
_________________________________________________________________

还创建了模型架构图,以帮助了解图像数据通过模型的输入、输出和转换。

Plot of the PatchGAN Discriminator Model for the CycleGAN

CycleGAN 的 PatchGAN 鉴别器模型图

如何实现循环生成器模型

CycleGAN 生成器模型将图像作为输入,并生成翻译后的图像作为输出。

该模型使用一系列下采样卷积块对输入图像进行编码,使用多个残差网络( ResNet )卷积块对图像进行变换,使用多个上采样卷积块生成输出图像。

让 c7s1-k 表示一个 7×7 卷积实例化的层,具有 k 个滤波器和步长 1。dk 表示一个 3×3 卷积-实例化-ReLU 层,具有 k 个过滤器和步长 2。反射填充用于减少伪像。Rk 表示包含两个 3 × 3 卷积层的剩余块,两层上的滤波器数量相同。uk 表示一个 3 × 3 的分数步长卷积实例化层,具有 k 个滤波器和步长 1/2。

——使用循环一致对抗网络的不成对图像到图像转换,2017。

128×128 图像的 6-resnet 块生成器的体系结构如下:

  • c7s1-64、d128、d256、R256、R256、R256、R256、R256、R256、u128、u64、c7s1-3

首先,我们需要一个函数来定义 ResNet 块。这些块由两个 3×3 有线电视新闻网层组成,其中块的输入按通道连接到块的输出。

这在 resnet_block() 函数中实现,该函数在第二个块之后创建了两个具有 3×3 过滤器和 1×1 步长的 conv-实例化块,并且没有 ReLU 激活,与 build_conv_block()函数中的官方 Torch 实现相匹配。为了简单起见,使用相同的填充代替文中推荐的反射填充。

# generator a resnet block
def resnet_block(n_filters, input_layer):
	# weight initialization
	init = RandomNormal(stddev=0.02)
	# first layer convolutional layer
	g = Conv2D(n_filters, (3,3), padding='same', kernel_initializer=init)(input_layer)
	g = InstanceNormalization(axis=-1)(g)
	g = Activation('relu')(g)
	# second convolutional layer
	g = Conv2D(n_filters, (3,3), padding='same', kernel_initializer=init)(g)
	g = InstanceNormalization(axis=-1)(g)
	# concatenate merge channel-wise with input layer
	g = Concatenate()([g, input_layer])
	return g

接下来,我们可以定义一个函数,为 256×256 个输入图像创建 9-resnet 块版本。通过将 image_shape 设置为(128 x128 x3)n _ resnet函数参数设置为 6,可以轻松将其更改为 6-resnet 块版本。

重要的是,该模型输出的像素值与输入的形状相同,并且像素值在[-1,1]的范围内,这是 GAN 发生器模型的典型情况。

# define the standalone generator model
def define_generator(image_shape=(256,256,3), n_resnet=9):
	# weight initialization
	init = RandomNormal(stddev=0.02)
	# image input
	in_image = Input(shape=image_shape)
	# c7s1-64
	g = Conv2D(64, (7,7), padding='same', kernel_initializer=init)(in_image)
	g = InstanceNormalization(axis=-1)(g)
	g = Activation('relu')(g)
	# d128
	g = Conv2D(128, (3,3), strides=(2,2), padding='same', kernel_initializer=init)(g)
	g = InstanceNormalization(axis=-1)(g)
	g = Activation('relu')(g)
	# d256
	g = Conv2D(256, (3,3), strides=(2,2), padding='same', kernel_initializer=init)(g)
	g = InstanceNormalization(axis=-1)(g)
	g = Activation('relu')(g)
	# R256
	for _ in range(n_resnet):
		g = resnet_block(256, g)
	# u128
	g = Conv2DTranspose(128, (3,3), strides=(2,2), padding='same', kernel_initializer=init)(g)
	g = InstanceNormalization(axis=-1)(g)
	g = Activation('relu')(g)
	# u64
	g = Conv2DTranspose(64, (3,3), strides=(2,2), padding='same', kernel_initializer=init)(g)
	g = InstanceNormalization(axis=-1)(g)
	g = Activation('relu')(g)
	# c7s1-3
	g = Conv2D(3, (7,7), padding='same', kernel_initializer=init)(g)
	g = InstanceNormalization(axis=-1)(g)
	out_image = Activation('tanh')(g)
	# define model
	model = Model(in_image, out_image)
	return model

生成器模型不是编译的,因为它是通过复合模型训练的,见下一节。

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

# example of an encoder-decoder generator for the cyclegan
from keras.optimizers import Adam
from keras.models import Model
from keras.models import Input
from keras.layers import Conv2D
from keras.layers import Conv2DTranspose
from keras.layers import Activation
from keras.initializers import RandomNormal
from keras.layers import Concatenate
from keras_contrib.layers.normalization.instancenormalization import InstanceNormalization
from keras.utils.vis_utils import plot_model

# generator a resnet block
def resnet_block(n_filters, input_layer):
	# weight initialization
	init = RandomNormal(stddev=0.02)
	# first layer convolutional layer
	g = Conv2D(n_filters, (3,3), padding='same', kernel_initializer=init)(input_layer)
	g = InstanceNormalization(axis=-1)(g)
	g = Activation('relu')(g)
	# second convolutional layer
	g = Conv2D(n_filters, (3,3), padding='same', kernel_initializer=init)(g)
	g = InstanceNormalization(axis=-1)(g)
	# concatenate merge channel-wise with input layer
	g = Concatenate()([g, input_layer])
	return g

# define the standalone generator model
def define_generator(image_shape=(256,256,3), n_resnet=9):
	# weight initialization
	init = RandomNormal(stddev=0.02)
	# image input
	in_image = Input(shape=image_shape)
	# c7s1-64
	g = Conv2D(64, (7,7), padding='same', kernel_initializer=init)(in_image)
	g = InstanceNormalization(axis=-1)(g)
	g = Activation('relu')(g)
	# d128
	g = Conv2D(128, (3,3), strides=(2,2), padding='same', kernel_initializer=init)(g)
	g = InstanceNormalization(axis=-1)(g)
	g = Activation('relu')(g)
	# d256
	g = Conv2D(256, (3,3), strides=(2,2), padding='same', kernel_initializer=init)(g)
	g = InstanceNormalization(axis=-1)(g)
	g = Activation('relu')(g)
	# R256
	for _ in range(n_resnet):
		g = resnet_block(256, g)
	# u128
	g = Conv2DTranspose(128, (3,3), strides=(2,2), padding='same', kernel_initializer=init)(g)
	g = InstanceNormalization(axis=-1)(g)
	g = Activation('relu')(g)
	# u64
	g = Conv2DTranspose(64, (3,3), strides=(2,2), padding='same', kernel_initializer=init)(g)
	g = InstanceNormalization(axis=-1)(g)
	g = Activation('relu')(g)
	# c7s1-3
	g = Conv2D(3, (7,7), padding='same', kernel_initializer=init)(g)
	g = InstanceNormalization(axis=-1)(g)
	out_image = Activation('tanh')(g)
	# define model
	model = Model(in_image, out_image)
	return model

# create the model
model = define_generator()
# summarize the model
model.summary()
# plot the model
plot_model(model, to_file='generator_model_plot.png', show_shapes=True, show_layer_names=True)

运行示例首先总结模型。

__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to
==================================================================================================
input_1 (InputLayer)            (None, 256, 256, 3)  0
__________________________________________________________________________________________________
conv2d_1 (Conv2D)               (None, 256, 256, 64) 9472        input_1[0][0]
__________________________________________________________________________________________________
instance_normalization_1 (Insta (None, 256, 256, 64) 128         conv2d_1[0][0]
__________________________________________________________________________________________________
activation_1 (Activation)       (None, 256, 256, 64) 0           instance_normalization_1[0][0]
__________________________________________________________________________________________________
conv2d_2 (Conv2D)               (None, 128, 128, 128 73856       activation_1[0][0]
__________________________________________________________________________________________________
instance_normalization_2 (Insta (None, 128, 128, 128 256         conv2d_2[0][0]
__________________________________________________________________________________________________
activation_2 (Activation)       (None, 128, 128, 128 0           instance_normalization_2[0][0]
__________________________________________________________________________________________________
conv2d_3 (Conv2D)               (None, 64, 64, 256)  295168      activation_2[0][0]
__________________________________________________________________________________________________
instance_normalization_3 (Insta (None, 64, 64, 256)  512         conv2d_3[0][0]
__________________________________________________________________________________________________
activation_3 (Activation)       (None, 64, 64, 256)  0           instance_normalization_3[0][0]
__________________________________________________________________________________________________
conv2d_4 (Conv2D)               (None, 64, 64, 256)  590080      activation_3[0][0]
__________________________________________________________________________________________________
instance_normalization_4 (Insta (None, 64, 64, 256)  512         conv2d_4[0][0]
__________________________________________________________________________________________________
activation_4 (Activation)       (None, 64, 64, 256)  0           instance_normalization_4[0][0]
__________________________________________________________________________________________________
conv2d_5 (Conv2D)               (None, 64, 64, 256)  590080      activation_4[0][0]
__________________________________________________________________________________________________
instance_normalization_5 (Insta (None, 64, 64, 256)  512         conv2d_5[0][0]
__________________________________________________________________________________________________
concatenate_1 (Concatenate)     (None, 64, 64, 512)  0           instance_normalization_5[0][0]
                                                                 activation_3[0][0]
__________________________________________________________________________________________________
conv2d_6 (Conv2D)               (None, 64, 64, 256)  1179904     concatenate_1[0][0]
__________________________________________________________________________________________________
instance_normalization_6 (Insta (None, 64, 64, 256)  512         conv2d_6[0][0]
__________________________________________________________________________________________________
activation_5 (Activation)       (None, 64, 64, 256)  0           instance_normalization_6[0][0]
__________________________________________________________________________________________________
conv2d_7 (Conv2D)               (None, 64, 64, 256)  590080      activation_5[0][0]
__________________________________________________________________________________________________
instance_normalization_7 (Insta (None, 64, 64, 256)  512         conv2d_7[0][0]
__________________________________________________________________________________________________
concatenate_2 (Concatenate)     (None, 64, 64, 768)  0           instance_normalization_7[0][0]
                                                                 concatenate_1[0][0]
__________________________________________________________________________________________________
conv2d_8 (Conv2D)               (None, 64, 64, 256)  1769728     concatenate_2[0][0]
__________________________________________________________________________________________________
instance_normalization_8 (Insta (None, 64, 64, 256)  512         conv2d_8[0][0]
__________________________________________________________________________________________________
activation_6 (Activation)       (None, 64, 64, 256)  0           instance_normalization_8[0][0]
__________________________________________________________________________________________________
conv2d_9 (Conv2D)               (None, 64, 64, 256)  590080      activation_6[0][0]
__________________________________________________________________________________________________
instance_normalization_9 (Insta (None, 64, 64, 256)  512         conv2d_9[0][0]
__________________________________________________________________________________________________
concatenate_3 (Concatenate)     (None, 64, 64, 1024) 0           instance_normalization_9[0][0]
                                                                 concatenate_2[0][0]
__________________________________________________________________________________________________
conv2d_10 (Conv2D)              (None, 64, 64, 256)  2359552     concatenate_3[0][0]
__________________________________________________________________________________________________
instance_normalization_10 (Inst (None, 64, 64, 256)  512         conv2d_10[0][0]
__________________________________________________________________________________________________
activation_7 (Activation)       (None, 64, 64, 256)  0           instance_normalization_10[0][0]
__________________________________________________________________________________________________
conv2d_11 (Conv2D)              (None, 64, 64, 256)  590080      activation_7[0][0]
__________________________________________________________________________________________________
instance_normalization_11 (Inst (None, 64, 64, 256)  512         conv2d_11[0][0]
__________________________________________________________________________________________________
concatenate_4 (Concatenate)     (None, 64, 64, 1280) 0           instance_normalization_11[0][0]
                                                                 concatenate_3[0][0]
__________________________________________________________________________________________________
conv2d_12 (Conv2D)              (None, 64, 64, 256)  2949376     concatenate_4[0][0]
__________________________________________________________________________________________________
instance_normalization_12 (Inst (None, 64, 64, 256)  512         conv2d_12[0][0]
__________________________________________________________________________________________________
activation_8 (Activation)       (None, 64, 64, 256)  0           instance_normalization_12[0][0]
__________________________________________________________________________________________________
conv2d_13 (Conv2D)              (None, 64, 64, 256)  590080      activation_8[0][0]
__________________________________________________________________________________________________
instance_normalization_13 (Inst (None, 64, 64, 256)  512         conv2d_13[0][0]
__________________________________________________________________________________________________
concatenate_5 (Concatenate)     (None, 64, 64, 1536) 0           instance_normalization_13[0][0]
                                                                 concatenate_4[0][0]
__________________________________________________________________________________________________
conv2d_14 (Conv2D)              (None, 64, 64, 256)  3539200     concatenate_5[0][0]
__________________________________________________________________________________________________
instance_normalization_14 (Inst (None, 64, 64, 256)  512         conv2d_14[0][0]
__________________________________________________________________________________________________
activation_9 (Activation)       (None, 64, 64, 256)  0           instance_normalization_14[0][0]
__________________________________________________________________________________________________
conv2d_15 (Conv2D)              (None, 64, 64, 256)  590080      activation_9[0][0]
__________________________________________________________________________________________________
instance_normalization_15 (Inst (None, 64, 64, 256)  512         conv2d_15[0][0]
__________________________________________________________________________________________________
concatenate_6 (Concatenate)     (None, 64, 64, 1792) 0           instance_normalization_15[0][0]
                                                                 concatenate_5[0][0]
__________________________________________________________________________________________________
conv2d_16 (Conv2D)              (None, 64, 64, 256)  4129024     concatenate_6[0][0]
__________________________________________________________________________________________________
instance_normalization_16 (Inst (None, 64, 64, 256)  512         conv2d_16[0][0]
__________________________________________________________________________________________________
activation_10 (Activation)      (None, 64, 64, 256)  0           instance_normalization_16[0][0]
__________________________________________________________________________________________________
conv2d_17 (Conv2D)              (None, 64, 64, 256)  590080      activation_10[0][0]
__________________________________________________________________________________________________
instance_normalization_17 (Inst (None, 64, 64, 256)  512         conv2d_17[0][0]
__________________________________________________________________________________________________
concatenate_7 (Concatenate)     (None, 64, 64, 2048) 0           instance_normalization_17[0][0]
                                                                 concatenate_6[0][0]
__________________________________________________________________________________________________
conv2d_18 (Conv2D)              (None, 64, 64, 256)  4718848     concatenate_7[0][0]
__________________________________________________________________________________________________
instance_normalization_18 (Inst (None, 64, 64, 256)  512         conv2d_18[0][0]
__________________________________________________________________________________________________
activation_11 (Activation)      (None, 64, 64, 256)  0           instance_normalization_18[0][0]
__________________________________________________________________________________________________
conv2d_19 (Conv2D)              (None, 64, 64, 256)  590080      activation_11[0][0]
__________________________________________________________________________________________________
instance_normalization_19 (Inst (None, 64, 64, 256)  512         conv2d_19[0][0]
__________________________________________________________________________________________________
concatenate_8 (Concatenate)     (None, 64, 64, 2304) 0           instance_normalization_19[0][0]
                                                                 concatenate_7[0][0]
__________________________________________________________________________________________________
conv2d_20 (Conv2D)              (None, 64, 64, 256)  5308672     concatenate_8[0][0]
__________________________________________________________________________________________________
instance_normalization_20 (Inst (None, 64, 64, 256)  512         conv2d_20[0][0]
__________________________________________________________________________________________________
activation_12 (Activation)      (None, 64, 64, 256)  0           instance_normalization_20[0][0]
__________________________________________________________________________________________________
conv2d_21 (Conv2D)              (None, 64, 64, 256)  590080      activation_12[0][0]
__________________________________________________________________________________________________
instance_normalization_21 (Inst (None, 64, 64, 256)  512         conv2d_21[0][0]
__________________________________________________________________________________________________
concatenate_9 (Concatenate)     (None, 64, 64, 2560) 0           instance_normalization_21[0][0]
                                                                 concatenate_8[0][0]
__________________________________________________________________________________________________
conv2d_transpose_1 (Conv2DTrans (None, 128, 128, 128 2949248     concatenate_9[0][0]
__________________________________________________________________________________________________
instance_normalization_22 (Inst (None, 128, 128, 128 256         conv2d_transpose_1[0][0]
__________________________________________________________________________________________________
activation_13 (Activation)      (None, 128, 128, 128 0           instance_normalization_22[0][0]
__________________________________________________________________________________________________
conv2d_transpose_2 (Conv2DTrans (None, 256, 256, 64) 73792       activation_13[0][0]
__________________________________________________________________________________________________
instance_normalization_23 (Inst (None, 256, 256, 64) 128         conv2d_transpose_2[0][0]
__________________________________________________________________________________________________
activation_14 (Activation)      (None, 256, 256, 64) 0           instance_normalization_23[0][0]
__________________________________________________________________________________________________
conv2d_22 (Conv2D)              (None, 256, 256, 3)  9411        activation_14[0][0]
__________________________________________________________________________________________________
instance_normalization_24 (Inst (None, 256, 256, 3)  6           conv2d_22[0][0]
__________________________________________________________________________________________________
activation_15 (Activation)      (None, 256, 256, 3)  0           instance_normalization_24[0][0]
==================================================================================================
Total params: 35,276,553
Trainable params: 35,276,553
Non-trainable params: 0
__________________________________________________________________________________________________

还会创建一个生成器模型图,显示 ResNet 块中的跳过连接。

Plot of the Generator Model for the CycleGAN

自行车发电机模型图

如何实现最小二乘和周期损失的复合模型

发电机型号不会直接更新。相反,发电机模型通过复合模型更新。

每个生成器模型的更新都涉及基于四个关注点的模型权重更改:

  • 对抗性损失(L2 或均方误差)。
  • 身份损失(L1 或平均绝对误差)。
  • 正向周期损失(L1 或平均绝对误差)。
  • 反向循环损失(L1 或平均绝对误差)。

对抗损失是通过鉴别器更新生成器的标准方法,尽管在这种情况下,使用最小二乘损失函数代替负对数似然(例如二进制交叉熵)。

首先,我们可以使用我们的函数来定义 CycleGAN 中使用的两个生成器和两个鉴别器。

...
# input shape
image_shape = (256,256,3)
# generator: A -> B
g_model_AtoB = define_generator(image_shape)
# generator: B -> A
g_model_BtoA = define_generator(image_shape)
# discriminator: A -> [real/fake]
d_model_A = define_discriminator(image_shape)
# discriminator: B -> [real/fake]
d_model_B = define_discriminator(image_shape)

每个只负责更新生成器模型权重的生成器模型都需要一个复合模型,尽管需要与相关的鉴别器模型和其他生成器模型共享权重。

这可以通过在复合模型的上下文中将其他模型的权重标记为不可训练来实现,以确保我们只更新预期的生成器。

...
# ensure the model we're updating is trainable
g_model_1.trainable = True
# mark discriminator as not trainable
d_model.trainable = False
# mark other generator model as not trainable
g_model_2.trainable = False

模型可以使用 Keras 函数 API 分段构建。

第一步是从源域定义真实图像的输入,通过我们的生成器模型,然后将生成器的输出连接到鉴别器,并将其分类为真实或虚假。

...
# discriminator element
input_gen = Input(shape=image_shape)
gen1_out = g_model_1(input_gen)
output_d = d_model(gen1_out)

接下来,我们可以将身份映射元素与来自目标域的真实图像的新输入连接起来,通过我们的生成器模型传递它,并直接输出(希望)未翻译的图像。

...
# identity element
input_id = Input(shape=image_shape)
output_id = g_model_1(input_id)

到目前为止,我们有一个复合模型,有两个真实图像输入和一个鉴别器分类和身份图像输出。接下来,我们需要添加正向和反向循环。

正向循环可以通过将我们的发电机的输出连接到另一个发电机来实现,另一个发电机的输出可以与我们的发电机的输入进行比较,并且应该相同。

...
# forward cycle
output_f = g_model_2(gen1_out)

后向循环更复杂,涉及来自目标域的真实图像的输入通过另一个生成器,然后通过我们的生成器,该生成器应该匹配来自目标域的真实图像。

...
# backward cycle
gen2_out = g_model_2(input_id)
output_b = g_model_1(gen2_out)

就这样。

然后,我们可以用两个输入定义这个复合模型:一个真实图像用于源域和目标域,四个输出,一个用于鉴别器,一个用于身份映射生成器,一个用于正向循环的另一个生成器,一个来自反向循环的生成器。

...
# define model graph
model = Model([input_gen, input_id], [output_d, output_id, output_f, output_b])

鉴频器输出的对抗性损失使用最小二乘损失,它被实现为 L2 或均方误差。发生器的输出与图像进行比较,并使用平均绝对误差实现的 L1 损耗进行优化。

生成器被更新为四个损失值的加权平均值。对抗损失通常被加权,而前向和后向循环损失使用称为λ的参数加权,并被设置为 10,例如比对抗损失重要 10 倍。身份丢失也作为 lambda 参数的一部分进行加权,在官方 Torch 实现中设置为 0.5 * 10 或 5。

...
# compile model with weighting of least squares loss and L1 loss
model.compile(loss=['mse', 'mae', 'mae', 'mae'], loss_weights=[1, 5, 10, 10], optimizer=opt)

我们可以将所有这些联系在一起,并定义函数 define_composite_model() 来创建一个复合模型,用于训练给定的生成器模型。

# define a composite model for updating generators by adversarial and cycle loss
def define_composite_model(g_model_1, d_model, g_model_2, image_shape):
	# ensure the model we're updating is trainable
	g_model_1.trainable = True
	# mark discriminator as not trainable
	d_model.trainable = False
	# mark other generator model as not trainable
	g_model_2.trainable = False
	# discriminator element
	input_gen = Input(shape=image_shape)
	gen1_out = g_model_1(input_gen)
	output_d = d_model(gen1_out)
	# identity element
	input_id = Input(shape=image_shape)
	output_id = g_model_1(input_id)
	# forward cycle
	output_f = g_model_2(gen1_out)
	# backward cycle
	gen2_out = g_model_2(input_id)
	output_b = g_model_1(gen2_out)
	# define model graph
	model = Model([input_gen, input_id], [output_d, output_id, output_f, output_b])
	# define optimization algorithm configuration
	opt = Adam(lr=0.0002, beta_1=0.5)
	# compile model with weighting of least squares loss and L1 loss
	model.compile(loss=['mse', 'mae', 'mae', 'mae'], loss_weights=[1, 5, 10, 10], optimizer=opt)
	return model

然后可以调用该函数来准备一个复合模型,用于训练 g_model_AtoB 发电机模型和 g_model_BtoA 模型;例如:

...
# composite: A -> B -> [real/fake, A]
c_model_AtoBtoA = define_composite_model(g_model_AtoB, d_model_B, g_model_BtoA, image_shape)
# composite: B -> A -> [real/fake, B]
c_model_BtoAtoB = define_composite_model(g_model_BtoA, d_model_A, g_model_AtoB, image_shape)

总结和绘制复合模型有点混乱,因为它无助于清楚地看到模型的输入和输出。

我们可以总结以下每个复合模型的输入和输出。回想一下,如果一个给定的模型在复合模型中被使用了不止一次,那么我们就是在共享或重用同一组权重。

发电机-复合模型

只有发电机-A 砝码是可训练的,其他型号的砝码是不可训练的。

  • 对抗损失:域-B - >生成器-A - >域-A - >鉴别器-A - >【真/假】
  • 身份丢失:域-A - >生成器-A - >域-A
  • 正向循环损耗:域-B - >发电机-A - >域-A - >发电机-B - >域-B
  • 反向循环损耗:域-A - >发电机-B - >域-B - >发电机-A - >域-A

发电机-B 复合模型

只有发电机 B 的重量是可训练的,其他型号的重量是不可训练的。

  • 对抗损失:域-A - >生成器-B - >域-B - >鉴别器-B - >【真/假】
  • 身份丢失:域-B - >生成器-B - >域-B
  • 正向循环损耗:域-A - >发电机-B - >域-B - >发电机-A - >域-A
  • 反向循环损耗:域-B - >发电机-A - >域-A - >发电机-B - >域-B

为了完整起见,下面列出了创建所有模型的完整示例。

# example of defining composite models for training cyclegan generators
from keras.optimizers import Adam
from keras.models import Model
from keras.models import Sequential
from keras.models import Input
from keras.layers import Conv2D
from keras.layers import Conv2DTranspose
from keras.layers import Activation
from keras.layers import LeakyReLU
from keras.initializers import RandomNormal
from keras.layers import Concatenate
from keras_contrib.layers.normalization.instancenormalization import InstanceNormalization
from keras.utils.vis_utils import plot_model

# define the discriminator model
def define_discriminator(image_shape):
	# weight initialization
	init = RandomNormal(stddev=0.02)
	# source image input
	in_image = Input(shape=image_shape)
	# C64
	d = Conv2D(64, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(in_image)
	d = LeakyReLU(alpha=0.2)(d)
	# C128
	d = Conv2D(128, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(d)
	d = InstanceNormalization(axis=-1)(d)
	d = LeakyReLU(alpha=0.2)(d)
	# C256
	d = Conv2D(256, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(d)
	d = InstanceNormalization(axis=-1)(d)
	d = LeakyReLU(alpha=0.2)(d)
	# C512
	d = Conv2D(512, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(d)
	d = InstanceNormalization(axis=-1)(d)
	d = LeakyReLU(alpha=0.2)(d)
	# second last output layer
	d = Conv2D(512, (4,4), padding='same', kernel_initializer=init)(d)
	d = InstanceNormalization(axis=-1)(d)
	d = LeakyReLU(alpha=0.2)(d)
	# patch output
	patch_out = Conv2D(1, (4,4), padding='same', kernel_initializer=init)(d)
	# define model
	model = Model(in_image, patch_out)
	# compile model
	model.compile(loss='mse', optimizer=Adam(lr=0.0002, beta_1=0.5), loss_weights=[0.5])
	return model

# generator a resnet block
def resnet_block(n_filters, input_layer):
	# weight initialization
	init = RandomNormal(stddev=0.02)
	# first layer convolutional layer
	g = Conv2D(n_filters, (3,3), padding='same', kernel_initializer=init)(input_layer)
	g = InstanceNormalization(axis=-1)(g)
	g = Activation('relu')(g)
	# second convolutional layer
	g = Conv2D(n_filters, (3,3), padding='same', kernel_initializer=init)(g)
	g = InstanceNormalization(axis=-1)(g)
	# concatenate merge channel-wise with input layer
	g = Concatenate()([g, input_layer])
	return g

# define the standalone generator model
def define_generator(image_shape, n_resnet=9):
	# weight initialization
	init = RandomNormal(stddev=0.02)
	# image input
	in_image = Input(shape=image_shape)
	# c7s1-64
	g = Conv2D(64, (7,7), padding='same', kernel_initializer=init)(in_image)
	g = InstanceNormalization(axis=-1)(g)
	g = Activation('relu')(g)
	# d128
	g = Conv2D(128, (3,3), strides=(2,2), padding='same', kernel_initializer=init)(g)
	g = InstanceNormalization(axis=-1)(g)
	g = Activation('relu')(g)
	# d256
	g = Conv2D(256, (3,3), strides=(2,2), padding='same', kernel_initializer=init)(g)
	g = InstanceNormalization(axis=-1)(g)
	g = Activation('relu')(g)
	# R256
	for _ in range(n_resnet):
		g = resnet_block(256, g)
	# u128
	g = Conv2DTranspose(128, (3,3), strides=(2,2), padding='same', kernel_initializer=init)(g)
	g = InstanceNormalization(axis=-1)(g)
	g = Activation('relu')(g)
	# u64
	g = Conv2DTranspose(64, (3,3), strides=(2,2), padding='same', kernel_initializer=init)(g)
	g = InstanceNormalization(axis=-1)(g)
	g = Activation('relu')(g)
	# c7s1-3
	g = Conv2D(3, (7,7), padding='same', kernel_initializer=init)(g)
	g = InstanceNormalization(axis=-1)(g)
	out_image = Activation('tanh')(g)
	# define model
	model = Model(in_image, out_image)
	return model

# define a composite model for updating generators by adversarial and cycle loss
def define_composite_model(g_model_1, d_model, g_model_2, image_shape):
	# ensure the model we're updating is trainable
	g_model_1.trainable = True
	# mark discriminator as not trainable
	d_model.trainable = False
	# mark other generator model as not trainable
	g_model_2.trainable = False
	# discriminator element
	input_gen = Input(shape=image_shape)
	gen1_out = g_model_1(input_gen)
	output_d = d_model(gen1_out)
	# identity element
	input_id = Input(shape=image_shape)
	output_id = g_model_1(input_id)
	# forward cycle
	output_f = g_model_2(gen1_out)
	# backward cycle
	gen2_out = g_model_2(input_id)
	output_b = g_model_1(gen2_out)
	# define model graph
	model = Model([input_gen, input_id], [output_d, output_id, output_f, output_b])
	# define optimization algorithm configuration
	opt = Adam(lr=0.0002, beta_1=0.5)
	# compile model with weighting of least squares loss and L1 loss
	model.compile(loss=['mse', 'mae', 'mae', 'mae'], loss_weights=[1, 5, 10, 10], optimizer=opt)
	return model

# input shape
image_shape = (256,256,3)
# generator: A -> B
g_model_AtoB = define_generator(image_shape)
# generator: B -> A
g_model_BtoA = define_generator(image_shape)
# discriminator: A -> [real/fake]
d_model_A = define_discriminator(image_shape)
# discriminator: B -> [real/fake]
d_model_B = define_discriminator(image_shape)
# composite: A -> B -> [real/fake, A]
c_model_AtoB = define_composite_model(g_model_AtoB, d_model_B, g_model_BtoA, image_shape)
# composite: B -> A -> [real/fake, B]
c_model_BtoA = define_composite_model(g_model_BtoA, d_model_A, g_model_AtoB, image_shape)

如何更新鉴别器和生成器模型

训练定义的模型相对简单。

首先,我们必须定义一个助手函数,它将选择一批真实图像和相关的目标(1.0)。

# select a batch of random samples, returns images and target
def generate_real_samples(dataset, n_samples, patch_shape):
	# choose random instances
	ix = randint(0, dataset.shape[0], n_samples)
	# retrieve selected images
	X = dataset[ix]
	# generate 'real' class labels (1)
	y = ones((n_samples, patch_shape, patch_shape, 1))
	return X, y

同样,我们需要一个函数来生成一批假图像和相关的目标(0.0)。

# generate a batch of images, returns images and targets
def generate_fake_samples(g_model, dataset, patch_shape):
	# generate fake instance
	X = g_model.predict(dataset)
	# create 'fake' class labels (0)
	y = zeros((len(X), patch_shape, patch_shape, 1))
	return X, y

现在,我们可以定义单次训练迭代的步骤。我们将基于官方 Torch 实现中的实现在 OptimizeParameters()函数 ( :官方代码使用了更加混乱的倒排命名约定)中对更新的顺序进行建模。

  1. 更新生成器-B (A->B)
  2. 更新鉴别器-B
  3. 更新生成器-A (B->A)
  4. 更新鉴别器-A

首先,我们必须通过调用为域-A 和域-B 生成 _real_samples() 来选择一批真实图像。

通常,批次大小( n_batch )设置为 1。在这种情况下,我们将假设 256×256 个输入图像,这意味着用于 PatchGAN 鉴别器的 n_patch 将是 16。

...
# select a batch of real samples
X_realA, y_realA = generate_real_samples(trainA, n_batch, n_patch)
X_realB, y_realB = generate_real_samples(trainB, n_batch, n_patch)

接下来,我们可以使用所选的真实图像批次来生成相应批次的生成图像或假图像。

...
# generate a batch of fake samples
X_fakeA, y_fakeA = generate_fake_samples(g_model_BtoA, X_realB, n_patch)
X_fakeB, y_fakeB = generate_fake_samples(g_model_AtoB, X_realA, n_patch)

该论文描述了使用先前生成的图像池,从中随机选择示例并用于更新鉴别器模型,其中池大小被设置为 50 个图像。

…[我们]使用生成图像的历史来更新鉴别器,而不是使用最新的生成器生成的图像。我们保留了一个图像缓冲区,用于存储 50 个先前创建的图像。

——使用循环一致对抗网络的不成对图像到图像转换,2017。

这可以通过使用每个域的列表和使用函数填充池来实现,然后在池达到容量时随机替换池中的元素。

下面的 update_image_pool() 功能是基于 image_pool.lua 中的官方 Torch 实现实现的。

# update image pool for fake images
def update_image_pool(pool, images, max_size=50):
	selected = list()
	for image in images:
		if len(pool) < max_size:
			# stock the pool
			pool.append(image)
			selected.append(image)
		elif random() < 0.5:
			# use image, but don't add it to the pool
			selected.append(image)
		else:
			# replace an existing image and use replaced image
			ix = randint(0, len(pool))
			selected.append(pool[ix])
			pool[ix] = image
	return asarray(selected)

然后,我们可以用生成的假图像更新我们的图像池,其结果可以用来训练鉴别器模型。

...
# update fakes from pool
X_fakeA = update_image_pool(poolA, X_fakeA)
X_fakeB = update_image_pool(poolB, X_fakeB)

接下来,我们可以更新生成器-A。

train_on_batch() 函数将为四个损失函数中的每一个返回一个值,每个输出一个值,以及用于更新我们感兴趣的模型权重的加权和(第一个值)。

...
# update generator B->A via adversarial and cycle loss
g_loss2, _, _, _, _  = c_model_BtoA.train_on_batch([X_realB, X_realA], [y_realA, X_realA, X_realB, X_realA])

然后,我们可以使用可能来自或可能不来自图像池的假图像来更新鉴别器模型。

...
# update discriminator for A -> [real/fake]
dA_loss1 = d_model_A.train_on_batch(X_realA, y_realA)
dA_loss2 = d_model_A.train_on_batch(X_fakeA, y_fakeA)

然后,我们可以对其他生成器和鉴别器模型进行同样的操作。

...
# update generator A->B via adversarial and cycle loss
g_loss1, _, _, _, _ = c_model_AtoB.train_on_batch([X_realA, X_realB], [y_realB, X_realB, X_realA, X_realB])
# update discriminator for B -> [real/fake]
dB_loss1 = d_model_B.train_on_batch(X_realB, y_realB)
dB_loss2 = d_model_B.train_on_batch(X_fakeB, y_fakeB)

在训练运行结束时,我们可以报告真实和虚假图像上的鉴别器模型以及每个生成器模型的当前损失。

...
# summarize performance
print('>%d, dA[%.3f,%.3f] dB[%.3f,%.3f] g[%.3f,%.3f]' % (i+1, dA_loss1,dA_loss2, dB_loss1,dB_loss2, g_loss1,g_loss2))

将这些联系在一起,我们可以定义一个名为 train() 的函数,该函数获取每个已定义模型的一个实例和一个加载的数据集(两个 NumPy 数组的列表,每个域一个),并训练该模型。

如本文所述,使用批量 1,模型适合 100 个训练时期。

# train cyclegan models
def train(d_model_A, d_model_B, g_model_AtoB, g_model_BtoA, c_model_AtoB, c_model_BtoA, dataset):
	# define properties of the training run
	n_epochs, n_batch, = 100, 1
	# determine the output square shape of the discriminator
	n_patch = d_model_A.output_shape[1]
	# unpack dataset
	trainA, trainB = dataset
	# prepare image pool for fakes
	poolA, poolB = list(), list()
	# calculate the number of batches per training epoch
	bat_per_epo = int(len(trainA) / n_batch)
	# calculate the number of training iterations
	n_steps = bat_per_epo * n_epochs
	# manually enumerate epochs
	for i in range(n_steps):
		# select a batch of real samples
		X_realA, y_realA = generate_real_samples(trainA, n_batch, n_patch)
		X_realB, y_realB = generate_real_samples(trainB, n_batch, n_patch)
		# generate a batch of fake samples
		X_fakeA, y_fakeA = generate_fake_samples(g_model_BtoA, X_realB, n_patch)
		X_fakeB, y_fakeB = generate_fake_samples(g_model_AtoB, X_realA, n_patch)
		# update fakes from pool
		X_fakeA = update_image_pool(poolA, X_fakeA)
		X_fakeB = update_image_pool(poolB, X_fakeB)
		# update generator B->A via adversarial and cycle loss
		g_loss2, _, _, _, _  = c_model_BtoA.train_on_batch([X_realB, X_realA], [y_realA, X_realA, X_realB, X_realA])
		# update discriminator for A -> [real/fake]
		dA_loss1 = d_model_A.train_on_batch(X_realA, y_realA)
		dA_loss2 = d_model_A.train_on_batch(X_fakeA, y_fakeA)
		# update generator A->B via adversarial and cycle loss
		g_loss1, _, _, _, _ = c_model_AtoB.train_on_batch([X_realA, X_realB], [y_realB, X_realB, X_realA, X_realB])
		# update discriminator for B -> [real/fake]
		dB_loss1 = d_model_B.train_on_batch(X_realB, y_realB)
		dB_loss2 = d_model_B.train_on_batch(X_fakeB, y_fakeB)
		# summarize performance
		print('>%d, dA[%.3f,%.3f] dB[%.3f,%.3f] g[%.3f,%.3f]' % (i+1, dA_loss1,dA_loss2, dB_loss1,dB_loss2, g_loss1,g_loss2))

然后可以用我们定义的模型和加载的数据集直接调用训练函数。

...
# load a dataset as a list of two numpy arrays
dataset = ...
# train models
train(d_model_A, d_model_B, g_model_AtoB, g_model_BtoA, c_model_AtoB, c_model_BtoA, dataset)

作为一种改进,可能希望将对每个鉴别器模型的更新组合成单个操作,如在官方实现的 fDx_basic()函数中执行的那样。

此外,该论文描述了在另外 100 个时期(总共 200 个)更新模型,其中学习率衰减到 0.0。这也可以作为培训过程的一个小的扩展。

进一步阅读

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

报纸

应用程序接口

项目

文章

摘要

在本教程中,您发现了如何使用 Keras 深度学习框架从零开始实现 CycleGAN 架构。

具体来说,您了解到:

  • 如何实现鉴别器和生成器模型?
  • 如何定义复合模型,通过对抗损失和周期损失来训练发电机模型?
  • 如何实现每次训练迭代更新模型权重的训练过程?

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

如何评估生成对抗网络

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

生成对抗网络,简称 GANs,是开发生成模型的有效深度学习方法。

与用损失函数训练直到收敛的其他深度学习神经网络模型不同,GAN 生成器模型是使用称为鉴别器的第二个模型训练的,该模型学习将图像分类为真实的或生成的。发生器和鉴别器模型被一起训练以保持平衡。

因此,不存在用于训练 GAN 发生器模型的目标损失函数,也没有办法仅从损失来客观地评估训练的进度和模型的相对或绝对质量。

相反,已经开发了一套定性和定量技术来基于生成的合成图像的质量和多样性来评估 GAN 模型的表现。

在这篇文章中,你将发现基于生成的合成图像评估生成对抗网络模型的技术。

看完这篇文章,你会知道:

  • 在训练 GAN 发生器模型时没有使用目标函数,这意味着必须使用生成的合成图像的质量来评估模型。
  • 开始时,手动检查生成的图像是一个很好的起点。
  • 定量测量,如初始得分和 Frechet 初始距离,可以与定性评估相结合,以提供对 GAN 模型的稳健评估。

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

我们开始吧。

How to Evaluate Generative Adversarial Networks

如何评估生成对抗网络 卡罗尔·范胡克摄,版权所有。

概观

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

  1. 评估 GAN 发电机模型的问题
  2. 人工 GAN 发生器评估
  3. 定性 GAN 发生器评估
  4. GAN 发生器的定量评估
  5. 使用哪种 GAN 评估方案

评估 GAN 发电机模型的问题

生成对抗网络是一种基于深度学习的生成模型。

事实证明,在一系列问题领域中,GANs 在生成高质量和大型合成图像方面都非常有效。

生成器模型不是直接训练的,而是由第二个模型训练的,称为鉴别器,它学习区分真实图像和伪造或生成的图像。因此,发电机模型没有目标函数或目标度量。

生成对抗网络缺乏目标函数,使得不同模型的表现难以比较。

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

这意味着没有一个普遍认可的方法来评估一个给定的 GAN 发电机模型。

这是 GANs 研究和使用的一个问题;例如,当:

  • 在训练过程中选择最终的 GAN 发生器模型。
  • 选择生成的图像来演示 GAN 发电机模型的能力。
  • 比较 GAN 模型架构。
  • 比较 GAN 模型配置。

GAN 发电机模型的客观评估仍然是一个开放的问题。

虽然已经引入了几个衡量标准,但到目前为止,对于哪一个衡量标准最能体现模型的优势和局限性以及应用于公平的模型比较,还没有达成共识。

——GAN 评估办法的利弊,2018 年。

因此,GAN 生成器模型是基于生成的图像质量来评估的,通常是在目标问题域的背景下。

人工 GAN 发生器评估

许多 GAN 从业者通过人工评估由发电机模型合成的图像,重新开始评估 GAN 发电机。

这包括使用生成器模型创建一批合成图像,然后评估与目标域相关的图像的质量和多样性。

这可以由研究人员或从业者自己完成。

人类对样本的视觉检查是评估肝组织最常见和最直观的方法之一。

——GAN 评估办法的利弊,2018 年。

生成器模型在许多训练时期内被迭代训练。由于没有对模型表现的客观衡量,我们无法知道何时应该停止训练过程,以及何时应该保存一个最终模型以备后用。

因此,通常在训练期间使用模型的当前状态来生成大量合成图像,并保存用于生成图像的生成器的当前状态。这允许通过生成的图像对每个保存的发电机模型进行事后评估。

一个训练时期是指用于更新模型的训练数据集中的图像的一个循环。模型可以跨训练时期系统地保存,例如每一个、五个、十个或更多训练时期。

尽管人工检查是模型评估的最简单方法,但它有许多局限性,包括:

  • 它是主观的,包括评审者对模型、模型配置和项目目标的偏见。
  • 它需要了解什么是现实的,什么不是目标领域的。
  • 它仅限于在合理的时间内可以查看的图像数量。

……用人类视觉评估生成图像的质量既昂贵又麻烦,有失偏颇【……】难以重现,也不能完全反映模型的容量。

——GAN 评估办法的利弊,2018 年。

几乎确定性的主观性质导致有偏见的模型选择和樱桃采摘,不应该用于非琐碎项目的最终模型选择。

然而,这是从业者熟悉该技术的起点。

令人欣慰的是,已经提出并采用了更复杂的 GAN 发生器评估方法。

有关详细调查,请参见 2018 年题为“GAN 评估措施的利弊”的论文本文将 GAN 发电机模型评估分为定性和定量措施,我们将在以下章节中使用这种划分来回顾其中的一些措施。

定性 GAN 发生器评估

定性度量是那些非数字的度量,通常涉及人的主观评估或通过比较进行的评估。

下面列出了评估 GAN 发电机模型的五种定性技术。

  1. 最近的邻居。
  2. 快速场景分类。
  3. 评级和偏好判断。
  4. 评估模式删除和模式折叠。
  5. 调查和可视化网络内部。

Summary of Qualitative GAN Generator Evaluation Methods

定性 GAN 发生器评估方法总结 摘自:GAN 评估措施的利弊。

也许最常用的定性 GAN 生成器模型是被称为“评级和偏好判断”的手动图像检查的扩展

这些类型的实验要求受试者根据生成图像的保真度对模型进行评分。

——GAN 评估办法的利弊,2018 年。

这是人类评委被要求对真实图像和从该领域生成的图像进行排序或比较的地方。

快速场景分类”方法大体相同,尽管图像呈现给人类评委的时间非常有限,比如几分之一秒,并且被分类为真或假。

图像通常成对呈现,并且询问人类判断者他们更喜欢哪个图像,例如哪个图像更真实。分数或评级是基于特定模型在此类锦标赛上生成图像的次数来确定的。通过对多个不同人类评委的评分进行平均,减少了评判的差异。

这是一项劳动密集型的工作,尽管使用像亚马逊的机械土耳其人这样的众包平台可以降低成本,使用网络界面可以提高效率。

一个直观的表现度量可以通过让人类注释者判断样本的视觉质量来获得。我们使用 Amazon Mechanical Turk【……】使用 web 界面【……】来自动化这个过程,我们使用该界面要求注释者区分生成的数据和真实的数据。

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

这种方法的一个主要缺点是人类法官的表现不是固定的,可以随着时间的推移而提高。如果给他们反馈,例如关于如何检测生成的图像的线索,情况尤其如此。

通过从这样的反馈中学习,注释者能够更好地指出生成的图像中的缺陷,给出更悲观的质量评估。

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

另一种主观总结发电机表现的流行方法是“最近邻居”这包括从域中选择真实图像的示例,并定位一个或多个最相似的生成图像进行比较。

距离度量,例如图像像素数据之间的欧几里德距离,通常用于选择最相似的生成图像。

最近邻方法对于评估生成的图像的逼真程度很有用。

GAN 发生器的定量评估

定量 GAN 发生器评估是指计算用于总结生成图像质量的特定数值分数。

下面列出了评估 GAN 发电机模型的 24 种定量技术。

  1. 平均对数似然
  2. 覆盖度量
  3. 初始得分
  4. 修改的初始得分
  5. 模式分数
  6. 调幅得分
  7. Frechet 初始距离
  8. 最大平均差异(MMD)
  9. 《Wasserstein 批评家》
  10. 生日悖论测试
  11. 分类器双样本测试
  12. 分类表现
  13. 边界失真
  14. 统计上不同的箱数(NDB)
  15. 图像检索表现
  16. 生成对抗性度量
  17. 锦标赛胜率和技能等级
  18. 标准化相对辨别分数(NRDS)
  19. 对抗性准确性和对抗性分歧
  20. 几何分数
  21. 重建误差
  22. 图像质量测量(SSIM、PSNR 和清晰度差异)
  23. 低级图像统计
  24. 精确度、召回率和 F1 分数

Summary of Quantitative GAN Generator Evaluation Methods

定量 GAN 发生器评估方法总结 摘自:GAN 评估措施的利弊。

Goodfellow 等人 2014 年发表的名为“生成对抗网络”的原始 GAN 论文使用了“平均对数似然”方法,也称为核估计或 Parzen 密度估计,来总结生成图像的质量。

这涉及到评估生成器捕获图像在域中的概率分布有多好的挑战性方法,并且通常被发现对评估 GANs 无效。

似然性的 Parzen 窗估计倾向于平凡模型,与样本的视觉保真度无关。此外,它无法逼近高维空间中的真实可能性,也无法对模型进行排序

——GAN 评估办法的利弊,2018 年。

评估生成图像的两个广泛采用的指标是初始得分和 Frechet 初始距离。

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

初始得分(IS)[……]可能是 GAN 评估中最广泛采用的分数。

——GAN 评估办法的利弊,2018 年。

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

使用该模型对大量生成的图像进行分类。具体地,预测图像属于每个类别的概率。然后将概率汇总在分数中,以捕捉每幅图像看起来有多像一个已知类别,以及图像集在已知类别中的多样性。

初始得分越高,表示生成的图像质量越好。

Frechet 初始距离,或 FID 分数是由马丁·豪塞尔等人在他们 2017 年的论文中提出并使用的,该论文题为“由两个时间尺度的更新规则训练的 GANs 收敛到局部纳什均衡”该分数是作为对现有初始得分的改进而提出的。

FID 在可区分性、鲁棒性和计算效率方面表现良好。[……]事实证明,FID 与人类的判断是一致的,并且比 is 更能抵抗噪声。

——GAN 评估办法的利弊,2018 年。

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

每个真实图像和生成图像的激活被总结为多元高斯分布,然后使用Frechet 距离计算这两个分布之间的距离,也称为 Wasserstein-2 距离。

较低的 FID 分数表示与真实图像的统计特性相匹配的更真实的图像。

使用哪种 GAN 评估方案

开始时,最好先手动检查生成的图像,以便评估和选择发电机型号。

  • 手动图像检查

开发 GAN 模型对于初学者来说已经足够复杂了。手动检查可以让您在细化模型实现和测试模型配置时走得更远。

一旦你对开发 GAN 模型的信心提高,初始得分和 Frechet 初始距离都可以用来定量总结生成图像的质量。虽然这两项措施很接近,但没有一个最佳且一致的措施。

到目前为止,对于最佳分数还没有一致的意见。不同的分数评估图像生成过程的各个方面,单个分数不可能涵盖所有方面。尽管如此,一些衡量标准似乎比其他更合理(例如 FID 评分)。

——GAN 评估办法的利弊,2018 年。

这些测量捕捉生成图像的质量和多样性,无论是单独的(前一种)还是与真实图像(后一种)相比,并且被广泛使用。

  • 初始得分
  • 弗雷谢特初始距离

这两种方法都很容易实现,并且可以对生成的图像进行批量计算。因此,在训练期间系统地生成图像和保存模型的实践可以并且应该继续被用于允许事后模型选择。

最近邻法可用于定性总结生成的图像。如果需要,也可以通过众包平台使用基于人的评级和偏好判断。

  • 最近的邻居
  • 评级和偏好判断

进一步阅读

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

摘要

在这篇文章中,你发现了基于生成的合成图像评估生成对抗网络模型的技术。

具体来说,您了解到:

  • 在训练 GAN 发生器模型时没有使用目标函数,这意味着必须使用生成的合成图像的质量来评估模型。
  • 开始时,手动检查生成的图像是一个很好的起点。
  • 定量测量,如初始得分和 Frechet 初始距离,可以与定性评估相结合,以提供对 GAN 模型的稳健评估。

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

如何入门生成对抗网络(7 天小型课程)

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

最后更新于 2019 年 7 月 12 日

生成对抗网络与 Python 速成班。

在 7 天内将生成对抗网络带到你的项目中。

生成对抗网络,简称 GANs,是一种训练生成模型的深度学习技术。

GANs 的研究和应用只有几年的历史,但所取得的成果却令人瞩目。因为这个领域如此年轻,知道如何开始、关注什么以及如何最好地使用现有技术可能是一项挑战。

在本速成课程中,您将发现如何在七天内开始并自信地使用 Python 开发深度学习生成对抗网络。

:这是一个很大很重要的岗位。你可能想把它做成书签。

我们开始吧。

  • 更新 2019 年 7 月:更改了 LeakyReLU 和 BatchNorm 层的顺序(谢谢 Chee)。

How to Get Started With Generative Adversarial Networks (7-Day Mini-Course)

如何开始使用生成对抗网络(7 天迷你课程) 图片由马蒂亚斯·里普提供,保留部分权利。

这个速成班是给谁的?

在我们开始之前,让我们确保你在正确的地方。

下面的列表提供了一些关于本课程是为谁设计的一般指南。

如果这些点不完全匹配,不要惊慌;你可能只需要在一个或另一个领域进行复习就能跟上。

你需要知道:

  • 围绕基本的 Python、NumPy 和 Keras 进行深入学习。

你不需要:

  • 数学天才!
  • 深度学习专家!
  • 一个计算机视觉研究员!

这门速成课程将把你从一个懂一点机器学习的开发人员带到一个能把 GANs 带到你自己的计算机视觉项目中的开发人员。

:本速成课程假设您有一个正在运行的 Python 2 或 3 SciPy 环境,其中至少安装了 NumPy、Pandas、Sklearn 和 Keras 2。如果您需要环境方面的帮助,可以遵循这里的逐步教程:

速成班概述

这门速成课分为七节课。

您可以每天完成一节课(推荐)或一天内完成所有课程(硬核)。这真的取决于你有多少时间和你的热情程度。

以下是让您开始使用 Python 中的生成对抗网络并提高工作效率的七堂课:

  • 第 01 课:什么是生成对抗网络?
  • 第 02 课 : GAN 提示、技巧和黑客
  • 第 03 课:鉴别器和发电机模型
  • 第 04 课 : GAN 损耗函数
  • 第 05 课 : GAN 训练算法
  • 第 06 课:图像转换的 GANs
  • 第 07 课:高级 GANs

每节课可能花费你 60 秒到 30 分钟。慢慢来,按照自己的节奏完成课程。提问,甚至在下面的评论中发布结果。

这些课程可能期望你去发现如何做事。我会给你提示,但每节课的部分要点是强迫你学习去哪里寻找关于深度学习和 GANs 的帮助(提示:我在这个博客上有所有的答案;只需使用搜索框)。

在评论中发布您的结果;我会为你加油的!

坚持住;不要放弃。

:这只是速成班。关于更多的细节和充实的教程,请参见我的书,题目是“使用 Python 的生成对抗网络”

第一课:什么是生成对抗网络?

在本课中,您将发现什么是 GANs 以及基本的模型架构。

生成对抗网络,简称 GANs,是一种使用深度学习方法的生成建模方法,例如卷积神经网络

GANs 是一种训练生成模型的聪明方法,它通过两个子模型将问题框架化为有监督的学习问题:生成器模型,我们训练它来生成新的示例,以及鉴别器模型,它试图将示例分类为真实的(来自域)或虚假的(生成的)。

  • 发电机。用于从问题域生成新的似是而非的示例的模型。
  • 鉴别器。用于将示例分类为真实(来自领域)或虚假(生成)的模型。

这两个模型在一个零和博弈中一起训练,对抗性的,直到鉴别器模型被愚弄了大约一半的时间,这意味着生成器模型正在生成似是而非的例子。

发电机

生成器模型以固定长度的随机向量作为输入,并在域中生成图像。

向量是从高斯分布(称为潜在空间)中随机抽取的,该向量用于为生成过程播种。

经过训练后,生成器模型被保留并用于生成新的样本。

歧视者

鉴别器模型以域中的一个例子作为输入(真实的或生成的),并预测真实的或虚假的二进制类标签(生成的)。

真实的例子来自训练数据集。生成的示例由生成器模型输出。

鉴别器是一个正常的(并且很好理解的)分类模型。

在训练过程之后,鉴别器模型被丢弃,因为我们对生成器感兴趣。

GAN 培训

生成器和鉴别器这两个模型是一起训练的。

单个训练周期包括首先从问题域中选择一批真实图像。生成一批潜在点并将其馈送到生成器模型以合成一批图像。

然后使用该批真实和生成的图像更新鉴别器,最小化任何二进制分类问题中使用的二进制交叉熵损失。

然后通过鉴别器模型更新生成器。这意味着生成的图像呈现给鉴别器,就好像它们是真实的(不是生成的),并且误差通过生成器模型传播回来。这具有更新生成器模型以生成更可能欺骗鉴别器的图像的效果。

然后对给定次数的训练迭代重复该过程。

你的任务

本课的任务是列出生成对抗网络的三种可能应用。你可能会从最近发表的研究论文中获得灵感。

在下面的评论中发表你的发现。我很想看看你的发现。

在下一课中,您将发现成功培训 GAN 模型的技巧和诀窍。

第 02 课:GAN 提示、技巧和技巧

在本课中,您将发现成功训练 GAN 模型所需了解的技巧、诀窍和技巧。

生成对抗网络很难训练。

这是因为该架构同时涉及一个生成器和一个鉴别器模型,它们在零和游戏中竞争。一个模型的改进是以另一个模型的表现下降为代价的。结果是一个非常不稳定的训练过程,经常会导致失败,例如,一个生成器总是生成相同的图像或生成无意义的图像。

因此,有许多试探法或最佳实践(称为“ GAN 黑客”)可以在配置和训练您的 GAN 模型时使用。

在稳定 GAN 模型的设计和训练中,最重要的一步可能是被称为深度卷积 GAN 或 DCGAN 的方法。

在实现您的 GAN 模型时,此体系结构涉及到七个要考虑的最佳实践:

  1. 使用条纹卷积进行下采样(例如,不要使用池化层)。
  2. 使用交错卷积进行上采样(例如,使用转置卷积层)。
  3. 使用 LeakyReLU(例如不要使用标准 ReLU )。
  4. 使用批量标准化(例如激活后标准化层输出)。
  5. 使用高斯权重初始化(例如,平均值为 0.0,标准偏差为 0.02)。
  6. 使用亚当随机梯度下降(例如学习率 0.0002,beta 1 0.5)。
  7. 将图像缩放至范围-1,1。

这些试探法是从业者在一系列问题上测试和评估数百或数千个配置操作组合来之不易的。

你的任务

在本课中,您的任务是列出三个可以在培训中使用的额外的 GAN 技巧或技巧。

在下面的评论中发表你的发现。我很想看看你的发现。

在下一课中,您将发现如何实现简单的鉴别器和生成器模型。

第 03 课:鉴别器和生成器模型

在本课中,您将发现如何使用 Keras 深度学习库实现一个简单的鉴别器和生成器模型。

我们将假设我们域中的图像大小和颜色为 28×28 像素,这意味着它们有三个颜色通道。

鉴别器模型

鉴别器模型接受大小为 28x28x3 像素的图像,并且必须通过 sigmoid 激活函数将其分类为真实(1)或虚假(0)。

我们的模型有两个卷积层,每个卷积层有 64 个滤波器,并使用相同的填充。每个卷积层将使用 2×2 步长对输入进行下采样,这是 GANs 的最佳实践,而不是使用池层。

同样遵循最佳实践,卷积层之后是斜率为 0.2 的 LeakyReLU 激活和批处理标准化层。

...
# define the discriminator model
model = Sequential()
# downsample to 14x14
model.add(Conv2D(64, (3,3), strides=(2, 2), padding='same', input_shape=(28,28,3)))
model.add(BatchNormalization())
model.add(LeakyReLU(alpha=0.2))
# downsample to 7x7
model.add(Conv2D(64, (3,3), strides=(2, 2), padding='same'))
model.add(BatchNormalization())
model.add(LeakyReLU(alpha=0.2))
# classify
model.add(Flatten())
model.add(Dense(1, activation='sigmoid'))

发电机模型

生成器模型以潜在空间中的 100 维点作为输入,并生成 28x28x3。

潜在空间中的点是高斯随机数的向量。这是使用密集层投影到 64 个微小的 7×7 图像的基础上。

然后,使用两个 2×2 步长的转置卷积层对小图像进行两次上采样,然后是 BatchNormalization 和 LeakyReLU 层,这是 GANs 的最佳实践。

通过 tanh 激活功能,输出是像素值在[-1,1]范围内的三通道图像。

...
# define the generator model
model = Sequential()
# foundation for 7x7 image
n_nodes = 64 * 7 * 7
model.add(Dense(n_nodes, input_dim=100))
model.add(BatchNormalization())
model.add(LeakyReLU(alpha=0.2))
model.add(Reshape((7, 7, 64)))
# upsample to 14x14
model.add(Conv2DTranspose(64, (3,3), strides=(2,2), padding='same'))
model.add(BatchNormalization())
model.add(LeakyReLU(alpha=0.2))
# upsample to 28x28
model.add(Conv2DTranspose(64, (3,3), strides=(2,2), padding='same'))
model.add(BatchNormalization())
model.add(LeakyReLU(alpha=0.2))
model.add(Conv2D(3, (3,3), activation='tanh', padding='same'))

你的任务

本课的任务是实现这两个鉴别器模型并总结它们的结构。

对于加分,更新模型以支持 64×64 像素的图像。

在下面的评论中发表你的发现。我很想看看你的发现。

在下一课中,您将了解如何配置损耗函数来训练 GAN 模型。

第 04 课:GAN 损耗函数

在本课中,您将了解如何配置用于训练 GAN 模型权重的损失函数

鉴别器损耗

鉴别器模型被优化,以最大化从数据集正确识别真实图像和生成器输出的假图像或合成图像的概率。

这可以实现为二进制分类问题,其中鉴别器输出给定图像的假和真的概率分别在 0 和 1 之间。

然后,该模型可以直接在成批的真实和虚假图像上进行训练,并最小化负对数似然性,最常见的实现方式是二进制交叉熵损失函数。

作为最佳实践,可以使用具有小学习率和保守动量的随机梯度下降的 Adam 版本来优化模型。

...
# compile model
model.compile(loss='binary_crossentropy', optimizer=Adam(lr=0.0002, beta_1=0.5))

发电机损耗

发电机不直接更新,该型号没有损失。

相反,鉴频器用于为发生器提供学习或间接损失函数。

这是通过创建一个复合模型来实现的,在该模型中,生成器输出一个图像,该图像直接输入鉴别器进行分类。

然后,可以通过在潜在空间中提供随机点作为输入并向鉴别器指示所生成的图像实际上是真实的来训练合成模型。这具有更新生成器权重的效果,以输出更有可能被鉴别器分类为真实的图像。

重要的是,鉴别器权重在此过程中不会更新,并且标记为不可训练。

复合模型使用与独立鉴别器模型相同的分类交叉熵损失和相同的随机梯度下降亚当版本来执行优化。

# create the composite model for training the generator
generator = ...
discriminator = ...
...
# make weights in the discriminator not trainable
d_model.trainable = False
# connect them
model = Sequential()
# add generator
model.add(generator)
# add the discriminator
model.add(discriminator)
# compile model
model.compile(loss='binary_crossentropy', optimizer=Adam(lr=0.0002, beta_1=0.5))

你的任务

本课中,您的任务是研究和总结可用于训练 GAN 模型的三种额外类型的损失函数。

在下面的评论中发表你的发现。我很想看看你的发现。

在下一课中,您将发现用于更新 GAN 模型权重的训练算法。

第 05 课:GAN 训练算法

在本课中,您将发现 GAN 训练算法。

定义 GAN 模型是困难的部分。GAN 训练算法相对简单。

该算法的一个周期包括首先选择一批真实图像,并使用当前的生成器模型生成一批假图像。您可以开发一些小函数来执行这两个操作。

然后,通过调用 train_on_batch() Keras 函数,这些真实和虚假图像被用来直接更新鉴别器模型。

接下来,可以生成潜在空间中的点作为复合生成器-鉴别器模型的输入,并且可以提供“真实”(类=1)的标签来更新生成器模型的权重。

然后,训练过程重复数千次。

生成器模型可以定期保存,然后加载以检查生成图像的质量。

下面的例子演示了 GAN 训练算法。

...
# gan training algorithm
discriminator = ...
generator = ...
gan_model = ...
n_batch = 16
latent_dim = 100
for i in range(10000)
	# get randomly selected 'real' samples
	X_real, y_real = select_real_samples(dataset, n_batch)
	# generate 'fake' examples
	X_fake, y_fake = generate_fake_samples(generator, latent_dim, n_batch)
	# create training set for the discriminator
	X, y = vstack((X_real, X_fake)), vstack((y_real, y_fake))
	# update discriminator model weights
	d_loss = discriminator.train_on_batch(X, y)
	# prepare points in latent space as input for the generator
	X_gan = generate_latent_points(latent_dim, n_batch)
	# create inverted labels for the fake samples
	y_gan = ones((n_batch, 1))
	# update the generator via the discriminator's error
	g_loss = gan_model.train_on_batch(X_gan, y_gan)

你的任务

在本课中,您的任务是将本课和上一课中的元素联系在一起,并在小型图像数据集(如 MNISTCIFAR-10 上训练一个 GAN。

在下面的评论中发表你的发现。我很想看看你的发现。

在下一课中,您将发现 GANs 在图像转换中的应用。

第六课:图像转换的甘斯

在本课中,您将发现用于图像转换的 GANs。

图像到图像的转换是给定源图像到目标图像的受控转换。一个例子可能是黑白照片到彩色照片的转换。

图像到图像的翻译是一个具有挑战性的问题,对于给定的翻译任务或数据集,通常需要专门的模型和损失函数。

GANs 可以被训练来执行图像到图像的转换,两个例子包括 Pix2Pix 和 CycleGAN。

Pix2Pix

Pix2Pix GAN 是一种通用的图像到图像转换方法。

该模型是在成对示例的数据集上训练的,其中每对示例都涉及所需翻译前后的图像示例。

Pix2Pix 模型基于条件生成对抗网络,在该网络中,根据给定的输入图像生成目标图像。

鉴别器模型被给予一个输入图像和一个真实的或生成的配对图像,并且必须确定配对图像是真实的还是伪造的。

生成器模型以给定的图像作为输入,并生成图像的翻译版本。生成器模型被训练成既欺骗鉴别器模型,又最小化生成的图像和期望的目标图像之间的损失。

Pix2Pix 中使用了更复杂的深度卷积神经网络模型。具体来说,U-Net 模型用于生成器模型,PatchGAN 用于鉴别器模型。

生成器的损失由正常 GAN 模型的对抗损失和生成的和预期的转换图像之间的 L1 损失组成。

-你好

Pix2Pix 模型的一个限制是,它需要所需翻译前后成对示例的数据集。

有许多图像到图像的翻译任务,我们可能没有翻译的例子,比如把斑马的照片翻译成马。还有其他不存在这种成对例子的图像转换任务,例如将风景艺术翻译成照片。

CycleGAN 是一种在没有成对例子的情况下自动训练图像到图像转换模型的技术。使用来自源域和目标域的不需要以任何方式相关的图像集合,以无监督的方式训练模型。

CycleGAN 是 GAN 架构的扩展,它涉及两个生成器模型和两个鉴别器模型的同时训练。

一个生成器将来自第一域的图像作为输入并输出第二域的图像,另一个生成器将来自第二域的图像作为输入并生成来自第一域的图像。然后使用鉴别器模型来确定生成的图像有多可信,并相应地更新生成器模型。

CycleGAN 对称为周期一致性的体系结构进行了额外的扩展。这是由第一发生器输出的图像可以用作第二发生器的输入并且第二发生器的输出应该与原始图像匹配的想法。反之亦然:第二个发生器的输出可以作为第一个发生器的输入,结果应该与第二个发生器的输入相匹配。

你的任务

在本课中,您的任务是列出五个图像到图像转换的示例,您可能希望使用 GAN 模型进行探索。

在下面的评论中发表你的发现。我很想看看你的发现。

在下一课中,您将发现 GAN 模型的一些最新进展。

第 07 课:高级 GANs

在这一课中,你将发现一些更先进的 GAN,正在展示显著的成果。

比根

BigGAN 是一种将一套最近的最佳实践整合在一起的方法,用于训练 GANs 并扩大批量和模型参数的数量。

顾名思义,BigGAN 专注于放大 GAN 模型。这包括具有以下特点的 GAN 型号:

  • 更多模型参数(例如,更多要素图)。
  • 更大的批量(例如数百或数千张图像)。
  • 架构变化(例如,自我关注模块)。

由此产生的 BigGAN 生成器模型能够在广泛的图像类别中生成高质量的 256×256 和 512×512 图像。

渐进式增长 GAN

渐进式增长 GAN 是 GAN 训练过程的扩展,允许稳定训练发电机模型,可以输出大的高质量图像。

它包括从一个非常小的图像开始,逐步增加层块,增加生成器模型的输出大小和鉴别器模型的输入大小,直到达到所需的图像大小。

渐进式增长 GAN 最令人印象深刻的成就可能是生成了大的 1024×1024 像素的真实感生成人脸。

stylenan

风格生成对抗网络,简称 StyleGAN,是 GAN 架构的扩展,它对生成器模型提出了很大的改变。

这包括使用映射网络将潜在空间中的点映射到中间潜在空间,使用中间潜在空间来控制生成器模型中每个点的样式,以及引入噪声作为生成器模型中每个点的变化源。

生成的模型不仅能够生成令人印象深刻的照片级高质量人脸照片,还可以通过改变样式向量和噪声来控制生成的图像在不同细节级别的样式。

例如,合成网络中较低分辨率的层块控制高级风格,如姿势和发型,较高分辨率的层块控制配色方案和非常精细的细节,如雀斑和发束的位置。

你的任务

在本课中,您的任务是列出 3 个示例,说明如何使用能够生成大型照片真实感图像的模型。

在下面的评论中发表你的发现。我很想看看你的发现。

这是最后一课。

末日!

(看你走了多远)

你成功了。干得好!

花一点时间,回头看看你已经走了多远。

你发现了:

  • GANs 是一种深度学习技术,用于训练能够合成高质量图像的生成模型。
  • 训练 GANs 本质上是不稳定的,并且容易失败,这可以通过在 GAN 模型的设计、配置和训练中采用最佳实践试探法来克服。
  • GAN 架构中使用的生成器和鉴别器模型可以在 Keras 深度学习库中简单直接地定义。
  • 鉴别器模型像任何其他二分类深度学习模型一样被训练。
  • 生成器模型通过复合模型架构中的鉴别器模型进行训练。
  • GANs 能够有条件地生成图像,例如使用成对和不成对的例子进行图像到图像的转换。
  • GANs 的进步,如放大模型和逐步增长模型,允许生成更大和更高质量的图像。

下一步,用 python 查看我的《生成对抗网络》一书。

摘要

你觉得迷你课程怎么样? 你喜欢这个速成班吗?

你有什么问题吗?有什么症结吗? 让我知道。请在下面留言。

如何用 Keras 从零开始实现 Pix2Pix GAN 模型

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

最后更新于 2021 年 4 月 30 日

Pix2Pix GAN 是一个生成器模型,用于执行在成对示例上训练的图像到图像的翻译。

例如,该模型可用于将白天的图像转换为夜间的图像,或者从产品草图(如鞋子)转换为产品照片。

Pix2Pix 模型的好处是,与其他用于条件图像生成的 GANs 相比,它相对简单,能够跨各种图像转换任务生成大型高质量图像。

这个模型给人留下了深刻的印象,但是对于初学者来说,它的架构看起来有些复杂。

在本教程中,您将发现如何使用 Keras 深度学习框架从零开始实现 Pix2Pix GAN 架构。

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

  • 如何为 Pix2Pix GAN 开发 PatchGAN 鉴别器模型。
  • 如何为 Pix2Pix GAN 开发 U-Net 编解码生成器模型。
  • 如何实现用于更新生成器的复合模型,以及如何训练两个模型。

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

我们开始吧。

  • 2021 年 1 月更新:更新所以层冻结用批量定额。

How to Implement Pix2Pix GAN Models From Scratch With Keras

如何用 Keras 从零开始实现 Pix2Pix GAN 模型 图片由 Ray 在马尼拉拍摄,保留部分权利。

教程概述

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

  1. 什么是 Pix2Pix GAN?
  2. 如何实现 PatchGAN 鉴别器模型
  3. 如何实现 U 网生成器模型
  4. 如何实现对抗性和 L1 损失
  5. 如何更新模型权重

什么是 Pix2Pix GAN?

Pix2Pix 是一个为通用图像到图像转换而设计的生成对抗网络模型。

该方法由菲利普·伊索拉(Phillip Isola)等人在 2016 年发表的论文《条件对抗网络下的 T2 图像到图像转换》(T3)中提出,并于 2017 年在 CVPR 的 T4 会议上提出。

GAN 架构由一个用于输出新的似是而非的合成图像的生成器模型和一个将图像分类为真实(来自数据集)或虚假(生成)的鉴别器模型组成。鉴别器模型直接更新,而生成器模型通过鉴别器模型更新。这样,两个模型在对抗过程中被同时训练,其中生成器试图更好地欺骗鉴别器,鉴别器试图更好地识别伪造图像。

Pix2Pix 模型是一种条件 GAN 或 cGAN,其中输出图像的生成取决于输入,在这种情况下是源图像。鉴别器同时具有源图像和目标图像,并且必须确定目标是否是源图像的似是而非的变换。

同样,直接更新鉴别器模型,并且通过鉴别器模型更新生成器模型,尽管损耗函数被更新。生成器通过对抗性损失进行训练,这鼓励生成器在目标域中生成似是而非的图像。生成器还通过在生成的图像和预期输出图像之间测量的 L1 损耗来更新。这种额外的损失促使生成器模型创建源图像的似是而非的翻译。

Pix2Pix GAN 已经在一系列图像到图像的转换任务中进行了演示,例如将地图转换为卫星照片,将黑白照片转换为彩色照片,以及将产品草图转换为产品照片。

现在我们已经熟悉了 Pix2Pix GAN,让我们探索如何使用 Keras 深度学习库来实现它。

如何实现 PatchGAN 鉴别器模型

Pix2Pix GAN 中的鉴别器模型被实现为一个补丁 GAN。

PatchGAN 是根据感受野的大小设计的,有时也称为有效感受野。感受野是模型的一次输出激活与输入图像上的一个区域之间的关系(实际上是在输入通道中的音量)。

使用大小为 70×70 的 PatchGAN,这意味着模型的输出(或每个输出)映射到输入图像的 70×70 平方。实际上,一个 70×70 的 PatchGAN 会将输入图像的 70×70 的 patches 分类为真或假。

……我们设计了一个鉴别器架构——我们称之为 PatchGAN——它只在补丁的规模上惩罚结构。这个鉴别器试图分类图像中的每个 NxN 补丁是真的还是假的。我们在图像上运行这个鉴别器卷积,平均所有响应,以提供 d 的最终输出。

——条件对抗网络下的图像到图像转换,2016。

在我们深入了解 PatchGAN 的配置细节之前,掌握感受野的计算非常重要。

感受野不是鉴别器模型输出的大小,例如,它不涉及模型输出的激活图的形状。它是根据输入图像的输出激活图中的一个像素来定义模型的。模型的输出可以是预测输入图像的每个面片是真的还是假的单个值或值的平方激活图。

传统上,感受野是指单个卷积层的激活图的大小,与层的输入、滤波器的大小和步幅的大小有关。有效感受野概括了这一思想,并针对原始图像输入计算了卷积层堆栈输出的感受野。这些术语经常互换使用。

Pix2Pix GAN 的作者提供了一个 Matlab 脚本,在一个名为感受野大小. m 的脚本中计算不同模型配置的有效感受野大小。通过一个 70×70 PatchGAN 感受野计算的例子,可以对我们的工作有所帮助。

无论输入图像的大小如何,70×70 PatchGAN 都有固定数量的三层(不包括输出层和倒数第二层)。一维感受野的计算公式如下:

  • 感受野=(输出大小–1)*步幅+内核大小

其中输出大小是先前层激活图的大小,跨距是应用于激活时过滤器移动的像素数,内核大小是要应用的过滤器的大小。

PatchGAN 使用 2×2 的固定步长(除了输出层和倒数第二层)和 4×4 的固定内核大小。因此,我们可以计算感受野的大小,从模型输出的一个像素开始,向后到输入图像。

我们可以开发一个名为*感受野()*的 Python 函数来计算感受野,然后计算并打印 Pix2Pix PatchGAN 模型中每一层的感受野。下面列出了完整的示例。

# example of calculating the receptive field for the PatchGAN

# calculate the effective receptive field size
def receptive_field(output_size, kernel_size, stride_size):
    return (output_size - 1) * stride_size + kernel_size

# output layer 1x1 pixel with 4x4 kernel and 1x1 stride
rf = receptive_field(1, 4, 1)
print(rf)
# second last layer with 4x4 kernel and 1x1 stride
rf = receptive_field(rf, 4, 1)
print(rf)
# 3 PatchGAN layers with 4x4 kernel and 2x2 stride
rf = receptive_field(rf, 4, 2)
print(rf)
rf = receptive_field(rf, 4, 2)
print(rf)
rf = receptive_field(rf, 4, 2)
print(rf)

运行该示例将打印模型中从输出层到输入层的每一层的感受野大小。

我们可以看到,输出层的每个 1×1 像素映射到输入层的 70×70 感受野。

4
7
16
34
70

Pix2Pix 论文的作者探索了不同的 PatchGAN 配置,包括一个称为 PixelGAN 的 1×1 感受野和一个与输入模型的 256×256 像素图像(重新采样为 286×286)匹配的感受野,称为 ImageGAN。他们发现,70×70 PatchGAN 在表现和图像质量之间取得了最佳平衡。

70×70 的 PatchGAN […]表现稍好。超出此范围,扩展到完整的 286×286 ImageGAN,似乎不会提高结果的视觉质量。

——条件对抗网络下的图像到图像转换,2016。

PatchGAN 的配置在论文的附录中提供,可以通过查看官方火炬实现中的 defineD_n_layers()功能来确认。

该模型将两幅图像作为输入,具体为一幅源图像和一幅目标图像。这些图像在通道级被连接在一起,例如每个图像的 3 个彩色通道变成输入的 6 个通道。

让 Ck 表示一个带有 k 个过滤器的卷积-batch ORM-ReLu 层。[……]所有卷积都是 4× 4 空间滤波器,应用于跨距 2。[……]70×70 鉴频器架构为:C64-C128-C256-C512。在最后一层之后,应用卷积来映射到一维输出,随后是 Sigmoid 函数。作为上述表示法的一个例外,BatchNorm 不适用于第一个 C64 层。所有 ReLUs 都有泄漏,斜率为 0.2。

——条件对抗网络下的图像到图像转换,2016。

PatchGAN 配置使用简写符号定义为:C64-C128-C256-C512,其中 C 表示卷积-batchorm-LeakyReLU 层的块,数字表示过滤器的数量。第一层不使用批量归一化。如前所述,内核大小固定为 4×4,除了模型的最后两层之外,所有层都使用 2×2 的步长。LeakyReLU 的斜率设置为 0.2,输出层使用 sigmoid 激活函数。

随机抖动是通过将 256 × 256 输入图像的大小调整为 286 × 286,然后随机裁剪回 256×256 的大小来应用的。权重从均值为 0、标准差为 0.02 的高斯分布中初始化。

——条件对抗网络下的图像到图像转换,2016。

模型权重通过随机高斯初始化,平均值为 0.0,标准偏差为 0.02。输入模型的图像为 256×256。

……我们在优化 D 的同时将目标除以 2,这样会减慢 D 相对于 g 的学习速度,我们使用 minibatch SGD 并应用 Adam 求解器,学习速度为 0.0002,动量参数β1 = 0.5,β2 = 0.999。

——条件对抗网络下的图像到图像转换,2016。

模型以一幅图像的批量大小进行训练,随机梯度下降的 Adam 版本以较小的学习范围和适度的动量使用。每次模型更新,鉴别器的损失加权 50%。

将这些联系在一起,我们可以定义一个名为 define_discriminator() 的函数,创建 70×70 的 PatchGAN 鉴别器模型。

下面列出了定义模型的完整示例。

# example of defining a 70x70 patchgan discriminator model
from keras.optimizers import Adam
from keras.initializers import RandomNormal
from keras.models import Model
from keras.models import Input
from keras.layers import Conv2D
from keras.layers import LeakyReLU
from keras.layers import Activation
from keras.layers import Concatenate
from keras.layers import BatchNormalization
from keras.utils.vis_utils import plot_model

# define the discriminator model
def define_discriminator(image_shape):
	# weight initialization
	init = RandomNormal(stddev=0.02)
	# source image input
	in_src_image = Input(shape=image_shape)
	# target image input
	in_target_image = Input(shape=image_shape)
	# concatenate images channel-wise
	merged = Concatenate()([in_src_image, in_target_image])
	# C64
	d = Conv2D(64, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(merged)
	d = LeakyReLU(alpha=0.2)(d)
	# C128
	d = Conv2D(128, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(d)
	d = BatchNormalization()(d)
	d = LeakyReLU(alpha=0.2)(d)
	# C256
	d = Conv2D(256, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(d)
	d = BatchNormalization()(d)
	d = LeakyReLU(alpha=0.2)(d)
	# C512
	d = Conv2D(512, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(d)
	d = BatchNormalization()(d)
	d = LeakyReLU(alpha=0.2)(d)
	# second last output layer
	d = Conv2D(512, (4,4), padding='same', kernel_initializer=init)(d)
	d = BatchNormalization()(d)
	d = LeakyReLU(alpha=0.2)(d)
	# patch output
	d = Conv2D(1, (4,4), padding='same', kernel_initializer=init)(d)
	patch_out = Activation('sigmoid')(d)
	# define model
	model = Model([in_src_image, in_target_image], patch_out)
	# compile model
	opt = Adam(lr=0.0002, beta_1=0.5)
	model.compile(loss='binary_crossentropy', optimizer=opt, loss_weights=[0.5])
	return model

# define image shape
image_shape = (256,256,3)
# create the model
model = define_discriminator(image_shape)
# summarize the model
model.summary()
# plot the model
plot_model(model, to_file='discriminator_model_plot.png', show_shapes=True, show_layer_names=True)

运行示例首先总结了模型,提供了输入形状如何跨层转换以及模型中参数数量的洞察。

我们可以看到,两个输入图像被连接在一起,以创建一个 256x256x6 的输入到第一隐藏卷积层。这种输入图像的连接可以发生在模型的输入层之前,但是允许模型执行连接可以使模型的行为更加清晰。

我们可以看到,模型输出将是一个激活图,大小为 16×16 像素或激活和单个通道,图中的每个值对应于输入 256×256 图像的 70×70 像素块。如果输入图像的大小是 128×128 的一半,则输出特征图也将减半至 8×8。

该模型是一个二进制分类模型,这意味着它以[0,1]范围内的概率预测输出,在这种情况下,是输入图像是真实的还是来自目标数据集的可能性。可以通过模型对这些值进行平均,以给出真实/虚假的预测。训练时,将目标与目标值矩阵进行比较,0 代表假,1 代表真。

__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to
==================================================================================================
input_1 (InputLayer)            (None, 256, 256, 3)  0
__________________________________________________________________________________________________
input_2 (InputLayer)            (None, 256, 256, 3)  0
__________________________________________________________________________________________________
concatenate_1 (Concatenate)     (None, 256, 256, 6)  0           input_1[0][0]
                                                                 input_2[0][0]
__________________________________________________________________________________________________
conv2d_1 (Conv2D)               (None, 128, 128, 64) 6208        concatenate_1[0][0]
__________________________________________________________________________________________________
leaky_re_lu_1 (LeakyReLU)       (None, 128, 128, 64) 0           conv2d_1[0][0]
__________________________________________________________________________________________________
conv2d_2 (Conv2D)               (None, 64, 64, 128)  131200      leaky_re_lu_1[0][0]
__________________________________________________________________________________________________
batch_normalization_1 (BatchNor (None, 64, 64, 128)  512         conv2d_2[0][0]
__________________________________________________________________________________________________
leaky_re_lu_2 (LeakyReLU)       (None, 64, 64, 128)  0           batch_normalization_1[0][0]
__________________________________________________________________________________________________
conv2d_3 (Conv2D)               (None, 32, 32, 256)  524544      leaky_re_lu_2[0][0]
__________________________________________________________________________________________________
batch_normalization_2 (BatchNor (None, 32, 32, 256)  1024        conv2d_3[0][0]
__________________________________________________________________________________________________
leaky_re_lu_3 (LeakyReLU)       (None, 32, 32, 256)  0           batch_normalization_2[0][0]
__________________________________________________________________________________________________
conv2d_4 (Conv2D)               (None, 16, 16, 512)  2097664     leaky_re_lu_3[0][0]
__________________________________________________________________________________________________
batch_normalization_3 (BatchNor (None, 16, 16, 512)  2048        conv2d_4[0][0]
__________________________________________________________________________________________________
leaky_re_lu_4 (LeakyReLU)       (None, 16, 16, 512)  0           batch_normalization_3[0][0]
__________________________________________________________________________________________________
conv2d_5 (Conv2D)               (None, 16, 16, 512)  4194816     leaky_re_lu_4[0][0]
__________________________________________________________________________________________________
batch_normalization_4 (BatchNor (None, 16, 16, 512)  2048        conv2d_5[0][0]
__________________________________________________________________________________________________
leaky_re_lu_5 (LeakyReLU)       (None, 16, 16, 512)  0           batch_normalization_4[0][0]
__________________________________________________________________________________________________
conv2d_6 (Conv2D)               (None, 16, 16, 1)    8193        leaky_re_lu_5[0][0]
__________________________________________________________________________________________________
activation_1 (Activation)       (None, 16, 16, 1)    0           conv2d_6[0][0]
==================================================================================================
Total params: 6,968,257
Trainable params: 6,965,441
Non-trainable params: 2,816
__________________________________________________________________________________________________

创建模型的图,以图形形式显示几乎相同的信息。该模型并不复杂,它有一条包含两个输入图像和一个输出预测的线性路径。

:创建图时假设安装了 pydot 和 pygraphviz 库。如果这是一个问题,您可以注释掉导入并调用 plot_model() 函数。

Plot of the PatchGAN Model Used in the Pix2Pix GAN Architecture

Pix2Pix GAN 架构中使用的贴片 GAN 模型图

现在我们知道了如何实现 PatchGAN 鉴别器模型,我们现在可以考虑实现 U-Net 生成器模型。

如何实现 U 网生成器模型

Pix2Pix GAN 的生成器模型被实现为一个 U 网。

U-Net 模型是用于图像转换的编码器-解码器模型,其中跳跃连接用于将编码器中的层与解码器中具有相同大小的特征映射的对应层连接起来。

模型的编码器部分由卷积层组成,卷积层使用 2×2 的步长将输入源图像下采样到瓶颈层。模型的解码器部分读取瓶颈输出,并使用转置卷积层向上采样到所需的输出图像大小。

……输入通过一系列逐渐下采样的层,直到瓶颈层,此时过程反转。

——条件对抗网络下的图像到图像转换,2016。

Architecture of the U-Net Generator Model

U-Net 生成器模型的体系结构 取自带有条件对抗网络的图像到图像转换。

在具有相同大小的特征映射的层之间添加跳过连接,使得第一下采样层与最后一个上采样层连接,第二下采样层与第二最后一个上采样层连接,以此类推。这些连接将下采样层中的要素图通道与上采样层中的要素图通道连接起来。

具体来说,我们在 I 层和 n-I 层之间添加跳跃连接,其中 n 是总层数。每个跳跃连接只是将 I 层的所有通道与 n-I 层的通道连接起来。

——条件对抗网络下的图像到图像转换,2016。

与 GAN 架构中的传统生成器模型不同,U-Net 生成器不从潜在空间中取一点作为输入。取而代之的是,缺失层在训练期间和当模型用于进行预测时都被用作随机性的来源,例如在推断时生成图像。

类似地,在训练和推理过程中,批处理规范化也以同样的方式使用,这意味着统计数据是为每个批处理计算的,而不是在训练过程结束时固定的。这称为实例规范化,特别是当批处理大小设置为 1 时,就像 Pix2Pix 模型一样。

在推理时,我们以与训练阶段完全相同的方式运行生成器网络。这与通常的协议不同,因为我们在测试时应用丢弃,并且我们使用测试批次的统计数据应用批次标准化,而不是训练批次的聚合统计数据。

——条件对抗网络下的图像到图像转换,2016。

在 Keras 中,像 DropoutBatchNormalization 这样的层在训练和推理模型中的操作是不同的。当调用这些层为“真”时,我们可以设置“训练”参数,以确保它们始终在训练模型中运行,即使在推理过程中使用。

例如,在推理和训练过程中会退出的 Dropout 层可以添加到模型中,如下所示:

...
g = Dropout(0.5)(g, training=True)

与鉴别器模型一样,生成器模型的配置细节在论文的附录中定义,并且可以在与官方火炬实现中的 defineG_unet()函数进行比较时进行确认。

编码器像鉴别器模型一样使用卷积-batchorm-LeakyReLU 块,而解码器模型使用卷积-batchorm-dropped-ReLU 块,丢弃率为 50%。所有卷积层使用 4×4 的滤波器大小和 2×2 的步长。

让 Ck 表示一个带有 k 个过滤器的卷积-batch ORM-ReLu 层。CDk 表示丢弃率为 50%的卷积-batchnomdrop-ReLU 层。所有卷积都是 4× 4 空间滤波器,应用于步长 2。

——条件对抗网络下的图像到图像转换,2016。

U-Net 模型的体系结构使用简写符号定义如下:

  • 编码器:C64-C128-C256-C512-C512-C512-C512-C512
  • 解码器:CD512-CD 1024-CD 1024-C1024-C1024-C512-C256-C128

编码器的最后一层是瓶颈层,根据对纸张的修改和代码中的确认,它不使用批量归一化,而是使用一个 ReLU 激活来代替 LeakyRelu。

…瓶颈层的激活被 batchnorm 操作归零,有效地跳过了最内层。这个问题可以通过从这个层中移除 batchnorm 来解决,就像在公共代码中所做的那样

——条件对抗网络下的图像到图像转换,2016。

U-Net 解码器中的滤波器数量有点误导,因为它是与编码器中的等效层串联后的层的滤波器数量。当我们创建模型的图时,这可能会变得更加清楚。

该模型的输出使用具有三个通道的单个卷积层,并且在输出层中使用 tanh 激活函数,这是 GAN 生成器模型所共有的。编码器的第一层不使用批处理标准化。

在解码器中的最后一层之后,应用卷积来映射到输出通道的数量(一般为 3 个[…]),随后是 Tanh 函数[…]。batch ORM 不应用于编码器中的第一个 C64 层。编码器中的所有 relu 都是泄漏的,斜率为 0.2,而解码器中的 relu 是不泄漏的。

——条件对抗网络下的图像到图像转换,2016。

将这些联系在一起,我们可以定义一个名为 define_generator() 的函数,该函数定义了 U-Net 编码器-解码器生成器模型。还提供了两个辅助函数来定义层的编码器块和层的解码器块。

下面列出了定义模型的完整示例。

# example of defining a u-net encoder-decoder generator model
from keras.initializers import RandomNormal
from keras.models import Model
from keras.models import Input
from keras.layers import Conv2D
from keras.layers import Conv2DTranspose
from keras.layers import LeakyReLU
from keras.layers import Activation
from keras.layers import Concatenate
from keras.layers import Dropout
from keras.layers import BatchNormalization
from keras.layers import LeakyReLU
from keras.utils.vis_utils import plot_model

# define an encoder block
def define_encoder_block(layer_in, n_filters, batchnorm=True):
	# weight initialization
	init = RandomNormal(stddev=0.02)
	# add downsampling layer
	g = Conv2D(n_filters, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(layer_in)
	# conditionally add batch normalization
	if batchnorm:
		g = BatchNormalization()(g, training=True)
	# leaky relu activation
	g = LeakyReLU(alpha=0.2)(g)
	return g

# define a decoder block
def decoder_block(layer_in, skip_in, n_filters, dropout=True):
	# weight initialization
	init = RandomNormal(stddev=0.02)
	# add upsampling layer
	g = Conv2DTranspose(n_filters, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(layer_in)
	# add batch normalization
	g = BatchNormalization()(g, training=True)
	# conditionally add dropout
	if dropout:
		g = Dropout(0.5)(g, training=True)
	# merge with skip connection
	g = Concatenate()([g, skip_in])
	# relu activation
	g = Activation('relu')(g)
	return g

# define the standalone generator model
def define_generator(image_shape=(256,256,3)):
	# weight initialization
	init = RandomNormal(stddev=0.02)
	# image input
	in_image = Input(shape=image_shape)
	# encoder model: C64-C128-C256-C512-C512-C512-C512-C512
	e1 = define_encoder_block(in_image, 64, batchnorm=False)
	e2 = define_encoder_block(e1, 128)
	e3 = define_encoder_block(e2, 256)
	e4 = define_encoder_block(e3, 512)
	e5 = define_encoder_block(e4, 512)
	e6 = define_encoder_block(e5, 512)
	e7 = define_encoder_block(e6, 512)
	# bottleneck, no batch norm and relu
	b = Conv2D(512, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(e7)
	b = Activation('relu')(b)
	# decoder model: CD512-CD1024-CD1024-C1024-C1024-C512-C256-C128
	d1 = decoder_block(b, e7, 512)
	d2 = decoder_block(d1, e6, 512)
	d3 = decoder_block(d2, e5, 512)
	d4 = decoder_block(d3, e4, 512, dropout=False)
	d5 = decoder_block(d4, e3, 256, dropout=False)
	d6 = decoder_block(d5, e2, 128, dropout=False)
	d7 = decoder_block(d6, e1, 64, dropout=False)
	# output
	g = Conv2DTranspose(3, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(d7)
	out_image = Activation('tanh')(g)
	# define model
	model = Model(in_image, out_image)
	return model

# define image shape
image_shape = (256,256,3)
# create the model
model = define_generator(image_shape)
# summarize the model
model.summary()
# plot the model
plot_model(model, to_file='generator_model_plot.png', show_shapes=True, show_layer_names=True)

运行示例首先总结模型。

该模型只有一个输入和输出,但跳跃连接使摘要难以阅读。

__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to
==================================================================================================
input_1 (InputLayer)            (None, 256, 256, 3)  0
__________________________________________________________________________________________________
conv2d_1 (Conv2D)               (None, 128, 128, 64) 3136        input_1[0][0]
__________________________________________________________________________________________________
leaky_re_lu_1 (LeakyReLU)       (None, 128, 128, 64) 0           conv2d_1[0][0]
__________________________________________________________________________________________________
conv2d_2 (Conv2D)               (None, 64, 64, 128)  131200      leaky_re_lu_1[0][0]
__________________________________________________________________________________________________
batch_normalization_1 (BatchNor (None, 64, 64, 128)  512         conv2d_2[0][0]
__________________________________________________________________________________________________
leaky_re_lu_2 (LeakyReLU)       (None, 64, 64, 128)  0           batch_normalization_1[0][0]
__________________________________________________________________________________________________
conv2d_3 (Conv2D)               (None, 32, 32, 256)  524544      leaky_re_lu_2[0][0]
__________________________________________________________________________________________________
batch_normalization_2 (BatchNor (None, 32, 32, 256)  1024        conv2d_3[0][0]
__________________________________________________________________________________________________
leaky_re_lu_3 (LeakyReLU)       (None, 32, 32, 256)  0           batch_normalization_2[0][0]
__________________________________________________________________________________________________
conv2d_4 (Conv2D)               (None, 16, 16, 512)  2097664     leaky_re_lu_3[0][0]
__________________________________________________________________________________________________
batch_normalization_3 (BatchNor (None, 16, 16, 512)  2048        conv2d_4[0][0]
__________________________________________________________________________________________________
leaky_re_lu_4 (LeakyReLU)       (None, 16, 16, 512)  0           batch_normalization_3[0][0]
__________________________________________________________________________________________________
conv2d_5 (Conv2D)               (None, 8, 8, 512)    4194816     leaky_re_lu_4[0][0]
__________________________________________________________________________________________________
batch_normalization_4 (BatchNor (None, 8, 8, 512)    2048        conv2d_5[0][0]
__________________________________________________________________________________________________
leaky_re_lu_5 (LeakyReLU)       (None, 8, 8, 512)    0           batch_normalization_4[0][0]
__________________________________________________________________________________________________
conv2d_6 (Conv2D)               (None, 4, 4, 512)    4194816     leaky_re_lu_5[0][0]
__________________________________________________________________________________________________
batch_normalization_5 (BatchNor (None, 4, 4, 512)    2048        conv2d_6[0][0]
__________________________________________________________________________________________________
leaky_re_lu_6 (LeakyReLU)       (None, 4, 4, 512)    0           batch_normalization_5[0][0]
__________________________________________________________________________________________________
conv2d_7 (Conv2D)               (None, 2, 2, 512)    4194816     leaky_re_lu_6[0][0]
__________________________________________________________________________________________________
batch_normalization_6 (BatchNor (None, 2, 2, 512)    2048        conv2d_7[0][0]
__________________________________________________________________________________________________
leaky_re_lu_7 (LeakyReLU)       (None, 2, 2, 512)    0           batch_normalization_6[0][0]
__________________________________________________________________________________________________
conv2d_8 (Conv2D)               (None, 1, 1, 512)    4194816     leaky_re_lu_7[0][0]
__________________________________________________________________________________________________
activation_1 (Activation)       (None, 1, 1, 512)    0           conv2d_8[0][0]
__________________________________________________________________________________________________
conv2d_transpose_1 (Conv2DTrans (None, 2, 2, 512)    4194816     activation_1[0][0]
__________________________________________________________________________________________________
batch_normalization_7 (BatchNor (None, 2, 2, 512)    2048        conv2d_transpose_1[0][0]
__________________________________________________________________________________________________
dropout_1 (Dropout)             (None, 2, 2, 512)    0           batch_normalization_7[0][0]
__________________________________________________________________________________________________
concatenate_1 (Concatenate)     (None, 2, 2, 1024)   0           dropout_1[0][0]
                                                                 leaky_re_lu_7[0][0]
__________________________________________________________________________________________________
activation_2 (Activation)       (None, 2, 2, 1024)   0           concatenate_1[0][0]
__________________________________________________________________________________________________
conv2d_transpose_2 (Conv2DTrans (None, 4, 4, 512)    8389120     activation_2[0][0]
__________________________________________________________________________________________________
batch_normalization_8 (BatchNor (None, 4, 4, 512)    2048        conv2d_transpose_2[0][0]
__________________________________________________________________________________________________
dropout_2 (Dropout)             (None, 4, 4, 512)    0           batch_normalization_8[0][0]
__________________________________________________________________________________________________
concatenate_2 (Concatenate)     (None, 4, 4, 1024)   0           dropout_2[0][0]
                                                                 leaky_re_lu_6[0][0]
__________________________________________________________________________________________________
activation_3 (Activation)       (None, 4, 4, 1024)   0           concatenate_2[0][0]
__________________________________________________________________________________________________
conv2d_transpose_3 (Conv2DTrans (None, 8, 8, 512)    8389120     activation_3[0][0]
__________________________________________________________________________________________________
batch_normalization_9 (BatchNor (None, 8, 8, 512)    2048        conv2d_transpose_3[0][0]
__________________________________________________________________________________________________
dropout_3 (Dropout)             (None, 8, 8, 512)    0           batch_normalization_9[0][0]
__________________________________________________________________________________________________
concatenate_3 (Concatenate)     (None, 8, 8, 1024)   0           dropout_3[0][0]
                                                                 leaky_re_lu_5[0][0]
__________________________________________________________________________________________________
activation_4 (Activation)       (None, 8, 8, 1024)   0           concatenate_3[0][0]
__________________________________________________________________________________________________
conv2d_transpose_4 (Conv2DTrans (None, 16, 16, 512)  8389120     activation_4[0][0]
__________________________________________________________________________________________________
batch_normalization_10 (BatchNo (None, 16, 16, 512)  2048        conv2d_transpose_4[0][0]
__________________________________________________________________________________________________
concatenate_4 (Concatenate)     (None, 16, 16, 1024) 0           batch_normalization_10[0][0]
                                                                 leaky_re_lu_4[0][0]
__________________________________________________________________________________________________
activation_5 (Activation)       (None, 16, 16, 1024) 0           concatenate_4[0][0]
__________________________________________________________________________________________________
conv2d_transpose_5 (Conv2DTrans (None, 32, 32, 256)  4194560     activation_5[0][0]
__________________________________________________________________________________________________
batch_normalization_11 (BatchNo (None, 32, 32, 256)  1024        conv2d_transpose_5[0][0]
__________________________________________________________________________________________________
concatenate_5 (Concatenate)     (None, 32, 32, 512)  0           batch_normalization_11[0][0]
                                                                 leaky_re_lu_3[0][0]
__________________________________________________________________________________________________
activation_6 (Activation)       (None, 32, 32, 512)  0           concatenate_5[0][0]
__________________________________________________________________________________________________
conv2d_transpose_6 (Conv2DTrans (None, 64, 64, 128)  1048704     activation_6[0][0]
__________________________________________________________________________________________________
batch_normalization_12 (BatchNo (None, 64, 64, 128)  512         conv2d_transpose_6[0][0]
__________________________________________________________________________________________________
concatenate_6 (Concatenate)     (None, 64, 64, 256)  0           batch_normalization_12[0][0]
                                                                 leaky_re_lu_2[0][0]
__________________________________________________________________________________________________
activation_7 (Activation)       (None, 64, 64, 256)  0           concatenate_6[0][0]
__________________________________________________________________________________________________
conv2d_transpose_7 (Conv2DTrans (None, 128, 128, 64) 262208      activation_7[0][0]
__________________________________________________________________________________________________
batch_normalization_13 (BatchNo (None, 128, 128, 64) 256         conv2d_transpose_7[0][0]
__________________________________________________________________________________________________
concatenate_7 (Concatenate)     (None, 128, 128, 128 0           batch_normalization_13[0][0]
                                                                 leaky_re_lu_1[0][0]
__________________________________________________________________________________________________
activation_8 (Activation)       (None, 128, 128, 128 0           concatenate_7[0][0]
__________________________________________________________________________________________________
conv2d_transpose_8 (Conv2DTrans (None, 256, 256, 3)  6147        activation_8[0][0]
__________________________________________________________________________________________________
activation_9 (Activation)       (None, 256, 256, 3)  0           conv2d_transpose_8[0][0]
==================================================================================================
Total params: 54,429,315
Trainable params: 54,419,459
Non-trainable params: 9,856
__________________________________________________________________________________________________

创建模型的图,以图形形式显示几乎相同的信息。该模型很复杂,该图有助于理解跳过连接及其对解码器中滤波器数量的影响。

:创建图时假设安装了 pydot 和 pygraphviz 库。如果这是一个问题,您可以注释掉导入并调用 plot_model() 函数。

从输出层向后看,如果我们查看解码器的 Concatenate 层和第一个 can 转置层,我们可以看到通道数为:

  • [128, 256, 512, 1024, 1024, 1024, 1024, 512].

颠倒这个列表给出了解码器中每一层的滤波器数量的所述配置,该配置来自于以下文件:

  • CD 512-CD 1024-CD 1024-C1024-C1024-C512-C256-C128

Plot of the U-Net Encoder-Decoder Model Used in the Pix2Pix GAN Architecture

Pix2Pix GAN 架构中使用的 U-Net 编解码模型图

既然我们已经定义了这两个模型,我们可以看看生成器模型是如何通过鉴别器模型进行更新的。

如何实现对抗性和 L1 损失

鉴别器模型可以直接更新,而生成器模型必须通过鉴别器模型更新。

这可以通过在 Keras 中定义一个新的复合模型来实现,该模型将生成器模型的输出作为输入连接到鉴别器模型。然后,鉴别器模型可以预测生成的图像是真的还是假的。我们可以更新合成模型的权重,使生成的图像标签为“真实”而不是“,这将导致生成器权重朝着生成更好的假图像的方向更新。我们还可以将鉴别器权重标记为在这种情况下不可训练,以避免误导性更新。

此外,需要更新生成器,以更好地匹配输入图像的目标翻译。这意味着合成模型还必须直接输出生成的图像,允许将其与目标图像进行比较。

因此,我们可以将这个复合模型的输入和输出总结如下:

  • 输入:源图像
  • 输出:真/假分类,生成的目标图像。

发生器的权重将通过鉴别器输出的对抗损失和直接图像输出的 L1 损失来更新。损失分数加在一起,其中 L1 损失被视为一个正则项,并通过一个名为λ的超参数加权,设置为 100。

  • 损失=对抗性损失+λ* L1 损失

下面的 define_gan() 函数实现了这一点,将定义的生成器和鉴别器模型作为输入,并创建可用于更新生成器模型权重的复合 gan 模型。

源图像输入作为输入提供给发生器和鉴别器,发生器的输出也作为输入连接到鉴别器。

当分别为鉴别器和发生器输出编译模型时,指定了两个损失函数。损失权重参数用于定义每个损失相加后的权重,以更新发电机模型权重。

# define the combined generator and discriminator model, for updating the generator
def define_gan(g_model, d_model, image_shape):
	# make weights in the discriminator not trainable
	for layer in d_model.layers:
		if not isinstance(layer, BatchNormalization):
			layer.trainable = False
	# define the source image
	in_src = Input(shape=image_shape)
	# connect the source image to the generator input
	gen_out = g_model(in_src)
	# connect the source input and generator output to the discriminator input
	dis_out = d_model([in_src, gen_out])
	# src image as input, generated image and classification output
	model = Model(in_src, [dis_out, gen_out])
	# compile model
	opt = Adam(lr=0.0002, beta_1=0.5)
	model.compile(loss=['binary_crossentropy', 'mae'], optimizer=opt, loss_weights=[1,100])
	return model

将这一点与前面几节中的模型定义结合起来,下面列出了完整的示例。

# example of defining a composite model for training the generator model
from keras.optimizers import Adam
from keras.initializers import RandomNormal
from keras.models import Model
from keras.models import Input
from keras.layers import Conv2D
from keras.layers import Conv2DTranspose
from keras.layers import LeakyReLU
from keras.layers import Activation
from keras.layers import Concatenate
from keras.layers import Dropout
from keras.layers import BatchNormalization
from keras.layers import LeakyReLU
from keras.utils.vis_utils import plot_model

# define the discriminator model
def define_discriminator(image_shape):
	# weight initialization
	init = RandomNormal(stddev=0.02)
	# source image input
	in_src_image = Input(shape=image_shape)
	# target image input
	in_target_image = Input(shape=image_shape)
	# concatenate images channel-wise
	merged = Concatenate()([in_src_image, in_target_image])
	# C64
	d = Conv2D(64, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(merged)
	d = LeakyReLU(alpha=0.2)(d)
	# C128
	d = Conv2D(128, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(d)
	d = BatchNormalization()(d)
	d = LeakyReLU(alpha=0.2)(d)
	# C256
	d = Conv2D(256, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(d)
	d = BatchNormalization()(d)
	d = LeakyReLU(alpha=0.2)(d)
	# C512
	d = Conv2D(512, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(d)
	d = BatchNormalization()(d)
	d = LeakyReLU(alpha=0.2)(d)
	# second last output layer
	d = Conv2D(512, (4,4), padding='same', kernel_initializer=init)(d)
	d = BatchNormalization()(d)
	d = LeakyReLU(alpha=0.2)(d)
	# patch output
	d = Conv2D(1, (4,4), padding='same', kernel_initializer=init)(d)
	patch_out = Activation('sigmoid')(d)
	# define model
	model = Model([in_src_image, in_target_image], patch_out)
	# compile model
	opt = Adam(lr=0.0002, beta_1=0.5)
	model.compile(loss='binary_crossentropy', optimizer=opt, loss_weights=[0.5])
	return model

# define an encoder block
def define_encoder_block(layer_in, n_filters, batchnorm=True):
	# weight initialization
	init = RandomNormal(stddev=0.02)
	# add downsampling layer
	g = Conv2D(n_filters, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(layer_in)
	# conditionally add batch normalization
	if batchnorm:
		g = BatchNormalization()(g, training=True)
	# leaky relu activation
	g = LeakyReLU(alpha=0.2)(g)
	return g

# define a decoder block
def decoder_block(layer_in, skip_in, n_filters, dropout=True):
	# weight initialization
	init = RandomNormal(stddev=0.02)
	# add upsampling layer
	g = Conv2DTranspose(n_filters, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(layer_in)
	# add batch normalization
	g = BatchNormalization()(g, training=True)
	# conditionally add dropout
	if dropout:
		g = Dropout(0.5)(g, training=True)
	# merge with skip connection
	g = Concatenate()([g, skip_in])
	# relu activation
	g = Activation('relu')(g)
	return g

# define the standalone generator model
def define_generator(image_shape=(256,256,3)):
	# weight initialization
	init = RandomNormal(stddev=0.02)
	# image input
	in_image = Input(shape=image_shape)
	# encoder model: C64-C128-C256-C512-C512-C512-C512-C512
	e1 = define_encoder_block(in_image, 64, batchnorm=False)
	e2 = define_encoder_block(e1, 128)
	e3 = define_encoder_block(e2, 256)
	e4 = define_encoder_block(e3, 512)
	e5 = define_encoder_block(e4, 512)
	e6 = define_encoder_block(e5, 512)
	e7 = define_encoder_block(e6, 512)
	# bottleneck, no batch norm and relu
	b = Conv2D(512, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(e7)
	b = Activation('relu')(b)
	# decoder model: CD512-CD1024-CD1024-C1024-C1024-C512-C256-C128
	d1 = decoder_block(b, e7, 512)
	d2 = decoder_block(d1, e6, 512)
	d3 = decoder_block(d2, e5, 512)
	d4 = decoder_block(d3, e4, 512, dropout=False)
	d5 = decoder_block(d4, e3, 256, dropout=False)
	d6 = decoder_block(d5, e2, 128, dropout=False)
	d7 = decoder_block(d6, e1, 64, dropout=False)
	# output
	g = Conv2DTranspose(3, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(d7)
	out_image = Activation('tanh')(g)
	# define model
	model = Model(in_image, out_image)
	return model

# define the combined generator and discriminator model, for updating the generator
def define_gan(g_model, d_model, image_shape):
	# make weights in the discriminator not trainable
	for layer in d_model.layers:
		if not isinstance(layer, BatchNormalization):
			layer.trainable = False
	# define the source image
	in_src = Input(shape=image_shape)
	# connect the source image to the generator input
	gen_out = g_model(in_src)
	# connect the source input and generator output to the discriminator input
	dis_out = d_model([in_src, gen_out])
	# src image as input, generated image and classification output
	model = Model(in_src, [dis_out, gen_out])
	# compile model
	opt = Adam(lr=0.0002, beta_1=0.5)
	model.compile(loss=['binary_crossentropy', 'mae'], optimizer=opt, loss_weights=[1,100])
	return model

# define image shape
image_shape = (256,256,3)
# define the models
d_model = define_discriminator(image_shape)
g_model = define_generator(image_shape)
# define the composite model
gan_model = define_gan(g_model, d_model, image_shape)
# summarize the model
gan_model.summary()
# plot the model
plot_model(gan_model, to_file='gan_model_plot.png', show_shapes=True, show_layer_names=True)

运行示例首先总结复合模型,显示 256×256 的图像输入、来自模型 _2 (生成器)的相同形状的输出和来自模型 _1 (鉴别器)的 PatchGAN 分类预测。

__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to
==================================================================================================
input_4 (InputLayer)            (None, 256, 256, 3)  0
__________________________________________________________________________________________________
model_2 (Model)                 (None, 256, 256, 3)  54429315    input_4[0][0]
__________________________________________________________________________________________________
model_1 (Model)                 (None, 16, 16, 1)    6968257     input_4[0][0]
                                                                 model_2[1][0]
==================================================================================================
Total params: 61,397,572
Trainable params: 54,419,459
Non-trainable params: 6,978,113
__________________________________________________________________________________________________

还会创建一个复合模型图,显示输入图像如何流入生成器和鉴别器,以及模型是否有两个输出或两个模型的端点。

:创建图时假设安装了 pydot 和 pygraphviz 库。如果这是一个问题,您可以注释掉导入并调用 plot_model()函数。

Plot of the Composite GAN Model Used to Train the Generator in the Pix2Pix GAN Architecture

用于训练 Pix2Pix GAN 架构中发生器的复合 GAN 模型图

如何更新模型权重

训练定义的模型相对简单。

首先,我们必须定义一个助手函数,它将选择一批真实的源图像和目标图像以及相关的输出(1.0)。这里,数据集是两个图像阵列的列表。

# select a batch of random samples, returns images and target
def generate_real_samples(dataset, n_samples, patch_shape):
	# unpack dataset
	trainA, trainB = dataset
	# choose random instances
	ix = randint(0, trainA.shape[0], n_samples)
	# retrieve selected images
	X1, X2 = trainA[ix], trainB[ix]
	# generate 'real' class labels (1)
	y = ones((n_samples, patch_shape, patch_shape, 1))
	return [X1, X2], y

同样,我们需要一个函数来生成一批假图像和相关的输出(0.0)。这里,样本是将为其生成目标图像的源图像的阵列。

# generate a batch of images, returns images and targets
def generate_fake_samples(g_model, samples, patch_shape):
	# generate fake instance
	X = g_model.predict(samples)
	# create 'fake' class labels (0)
	y = zeros((len(X), patch_shape, patch_shape, 1))
	return X, y

现在,我们可以定义单次训练迭代的步骤。

首先,我们必须通过调用 generate_real_samples()来选择一批源图像和目标图像。

通常,批次大小( n_batch )设置为 1。在这种情况下,我们将假设 256×256 个输入图像,这意味着用于 PatchGAN 鉴别器的 n_patch 将是 16,以指示 16×16 的输出特征图。

...
# select a batch of real samples
[X_realA, X_realB], y_real = generate_real_samples(dataset, n_batch, n_patch)

接下来,我们可以使用所选择的真实源图像的批次来生成相应批次的生成的或伪造的目标图像。

...
# generate a batch of fake samples
X_fakeB, y_fake = generate_fake_samples(g_model, X_realA, n_patch)

然后,我们可以使用真实和伪造的图像,以及它们的目标,来更新独立的鉴别器模型。

...
# update discriminator for real samples
d_loss1 = d_model.train_on_batch([X_realA, X_realB], y_real)
# update discriminator for generated samples
d_loss2 = d_model.train_on_batch([X_realA, X_fakeB], y_fake)

到目前为止,这对于在 Keras 更新一个 GAN 是正常的。

接下来,我们可以通过对抗性损失和 L1 损失来更新发电机模型。回想一下,复合 GAN 模型以一批源图像作为输入,首先预测真假分类,其次预测生成的目标。这里,我们向合成模型的鉴别器输出提供一个目标来指示生成的图像是“真实的”(class = 1)。提供真实目标图像用于计算它们和生成的目标图像之间的 L1 损失。

我们有两个损失函数,但是为一个批处理更新计算了三个损失值,其中只有第一个损失值是有意义的,因为它是该批处理的对抗性和 L1 损失值的加权和。

...
# update the generator
g_loss, _, _ = gan_model.train_on_batch(X_realA, [y_real, X_realB])

仅此而已。

我们可以在一个名为 train() 的函数中定义所有这些,该函数获取定义的模型和加载的数据集(作为两个 NumPy 数组的列表)并训练模型。

# train pix2pix models
def train(d_model, g_model, gan_model, dataset, n_epochs=100, n_batch=1, n_patch=16):
	# unpack dataset
	trainA, trainB = dataset
	# calculate the number of batches per training epoch
	bat_per_epo = int(len(trainA) / n_batch)
	# calculate the number of training iterations
	n_steps = bat_per_epo * n_epochs
	# manually enumerate epochs
	for i in range(n_steps):
		# select a batch of real samples
		[X_realA, X_realB], y_real = generate_real_samples(dataset, n_batch, n_patch)
		# generate a batch of fake samples
		X_fakeB, y_fake = generate_fake_samples(g_model, X_realA, n_patch)
		# update discriminator for real samples
		d_loss1 = d_model.train_on_batch([X_realA, X_realB], y_real)
		# update discriminator for generated samples
		d_loss2 = d_model.train_on_batch([X_realA, X_fakeB], y_fake)
		# update the generator
		g_loss, _, _ = gan_model.train_on_batch(X_realA, [y_real, X_realB])
		# summarize performance
		print('>%d, d1[%.3f] d2[%.3f] g[%.3f]' % (i+1, d_loss1, d_loss2, g_loss))

然后可以用我们定义的模型和加载的数据集直接调用训练函数。

...
# load image data
dataset = ...
# train model
train(d_model, g_model, gan_model, dataset)

进一步阅读

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

正式的

应用程序接口

文章

摘要

在本教程中,您发现了如何使用 Keras 深度学习框架从零开始实现 Pix2Pix GAN 架构。

具体来说,您了解到:

  • 如何为 Pix2Pix GAN 开发 PatchGAN 鉴别器模型。
  • 如何为 Pix2Pix GAN 开发 U-Net 编解码生成器模型。
  • 如何实现用于更新生成器的复合模型,以及如何训练两个模型。

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