CycleGAN 是一种生成对抗网络(GAN),用于跨域迁移任务,例如更改图像的样式,将绘画转变为照片, 反之亦然,例如照片增强功能,更改照片的季节等等。 CycleGAN 由朱俊彦,Taesung Park,Phillip Isola 和 Alexei A. Efros 在题为《使用循环生成对抗网络的非配对图像到图像转换》中引入。 该产品于 2018 年 2 月在加州大学伯克利分校的 Berkeley AI Research(BAIR)实验室生产,可通过以下链接获得。 由于其广泛的使用案例,CycleGAN 在 GAN 社区引起了轰动。 在本章中,我们将与 CycleGAN 一起使用,尤其是使用它们将绘画转换为照片。
在本章中,我们将介绍以下主题:
- CycleGAN 简介
- CycleGAN 的架构
- 数据收集与准备
- Keras 的 CycleGAN 实现
- 目标函数
- 训练 CycleGAN
- CycleGAN 的实际应用
为了将照片变成一幅画或一幅画,再将它们变成照片,普通 GAN 需要一对图像。 CycleGAN 是一种 GAN 网络,可以将图像从一个域 X 转换为另一个域 Y,而无需配对图像。 CycleGAN 尝试学习生成器网络,而生成器网络又学习了两个映射。 CycleGAN 无需训练大多数 GAN 中使用的单个生成器网络,而是训练两个生成器和两个判别器网络。
CycleGAN 中有两个生成器网络,如下所示:
- 生成器
A
:学习映射G: X -> Y
,其中X
是源域,Y
是目标域。 它从源域A
拍摄图像,并将其转换为与目标域B
相似的图像。 基本上,网络的目的是学习映射,以使G(X)
与Y
相似。 - 生成器
B
:学习映射F: Y -> X
,然后从目标域B
中获取图像,并将其转换为与源域A
中的图像相似的图像。类似地, 网络要学习另一个映射,以便F(G(X))
类似于X
)。
这两个网络的架构相同,但我们分别对其进行训练。
CycleGAN 中有两个判别器网络,如下所示:
- 判别器
A
:判别器A
的工作是区分由生成器网络A
生成的图像,这些图像表示为G(X)
和来自源域A
的真实图像,它们表示为X
。 - 判别器
B
:判别器B
的工作是区分由生成器网络B
生成的图像,这些图像表示为F(Y)
以及来自源域B
的真实图像,它们表示为Y
。
两个网络的架构是相同的。 与生成器网络类似,我们分别训练判别器网络。 如下图所示:
具有两个生成器和两个对抗性判别器网络的 CycleGAN 的插图。来源:arxiv:1703.10593
在下一节中,让我们详细了解 CycleGAN 的架构。
CycleGAN 总体上由两种架构组成:生成器和判别器。 生成器架构用于创建两个模型,生成器 A 和生成器 B。判别器架构用于创建另外两个模型,判别器 A 和判别器 B。我们现在将在接下来的两节中介绍两个网络的架构。
生成器网络是自编码器类型的网络。 它以图像作为输入并输出另一个图像。 它由两部分组成:编码器和解码器。 编码器包含具有下采样功能的卷积层,并将128x128x3
形状的输入转换为内部表示。 解码器包含两个上采样块和最后一个卷积层,该层将内部表示形式转换为128x128x3
形状的输出。
生成器网络包含以下块:
- 卷积块
- 残差块
- 上采样块
- 最后的卷积层
让我们逐一介绍每个组件:
- 卷积块:卷积块包含 2D 卷积层,然后是实例规范化层和 relu 作为激活函数。 请参阅第 1 章,“生成对抗网络简介”,以了解有关“实例规范化”的更多信息。
生成器网络包含三个卷积块,其配置如下:
层名称 | 超参数 | 输入形状 | 输出形状 |
---|---|---|---|
2D 卷积层 | filters=32, kernel_size=7, strides=1, padding='same' |
(128, 128, 3) |
(128, 128, 32) |
实例规范化层 | axis=1 |
(128, 128, 32) |
(128, 128, 32) |
激活层 | activation='relu' |
(128, 128, 32) |
(128, 128, 32) |
2D 卷积层 | filters=64, kernel_size=3, strides=2, padding='same' |
(128, 128, 32) |
(64, 64, 64) |
实例规范化层 | axis=1 |
(64, 64, 64) |
(64, 64, 64) |
激活层 | activation='relu' |
(64, 64, 64) |
(64, 64, 64) |
2D 卷积层 | filters=128, kernel_size=3, strides=2, padding='same' |
(64, 64, 64) |
(32, 32, 128) |
实例规范化层 | axis=1 |
(32, 32, 128) |
(32, 32, 128) |
激活层 | activation='relu' |
(32, 32, 128) |
(32, 32, 128) |
- 残差块:残差块包含两个 2D 卷积层。 两层之后是动量值等于 0.8 的批归一化层。 生成器网络包含六个残差块,其配置如下:
层名称 | 超参数 | 输入形状 | 输出形状 |
---|---|---|---|
2D 卷积层 | filters=128, kernel_size=3, strides=1, padding='same' |
(32, 32, 128) |
(32, 32, 128) |
批量规范化层 | axis=3, momentum=0.9, epsilon=1e-5 |
(32, 32, 128) |
(32, 32, 128) |
2D 卷积层 | filters=138, kernel_size=3, strides=1, padding='same' |
(32, 32, 128) |
((32, 32, 128) |
批量规范化层 | axis=3, momentum=0.9, epsilon=1e-5 |
(32, 32, 128) |
(32, 32, 128) |
加法层 | None |
(32, 32, 128) |
(32, 32, 128) |
加法层计算输入到块的张量与最后一个批量归一化层的输出之和。
- 上采样块:上采样块包含 2D 转置卷积层,并使用
relu
作为激活函数。 生成器网络中有两个上采样模块。 第一个上采样模块的配置如下:
层名称 | 超参数 | 输入形状 | 输出形状 |
---|---|---|---|
2D 转置卷积层 | filters=64, kernel_size=3, strides=2, padding='same', use_bias=False |
(32, 32, 128) |
(64, 64, 64) |
实例规范化层 | axis=1 |
(64, 64, 64) |
(64, 64, 64) |
激活层 | activation='relu' |
(64, 64, 64) |
(64, 64, 64) |
第二个上采样模块的配置如下:
层名称 | 超参数 | 输入形状 | 输出形状 |
---|---|---|---|
2D 转置卷积层 | filters=32, kernel_size=3, strides=2, padding='same', use_bias=False |
(64, 64, 64) |
(128, 128, 32) |
实例规范化层 | axis=1 |
(128, 128, 32) |
(128, 128, 32) |
激活层 | activation='relu' |
(128, 128, 32) |
(128, 128, 32) |
- 最后的卷积层:最后一层是使用
tanh
作为激活函数的 2D 卷积层。 它生成形状为(256, 256, 3)
的图像。 最后一层的配置如下:
层名称 | 超参数 | 输入形状 | 输出形状 |
---|---|---|---|
2D 卷积层 | filters=3, kernel_size=7, strides=1, padding='same', activation='tanh' |
(128, 128, 32) |
(128, 128, 3) |
这些超参数最适合 Keras 框架。 如果使用任何其他框架,请进行相应的修改。
判别器网络的架构类似于 PatchGAN 网络中的判别器架构。 它是一个深度卷积神经网络,包含多个卷积块。 基本上,它会拍摄形状为(128, 128, 3)
的图像,并预测该图像是真实的还是假的。 它包含几个 2D 零填充,可以在以下链接中找到其文档。 下表详细显示了判别器网络的架构:
层名称 | 超参数 | 输入形状 | 输出形状 |
---|---|---|---|
输入层 | none |
(128, 128, 3) |
(128, 128, 3) |
2D 零填充层 | padding(1, 1) |
(128, 128, 3) |
(130, 130, 3) |
2D 卷积层 | filters=64, kernel_size=4, strides=2, padding='valid' |
(130, 130, 3) |
(64, 64, 64) |
激活层 | activation='leakyrelu', alpha=0.2 |
(64, 64, 64) |
(64, 64, 64) |
2D 零填充层 | padding(1, 1) |
(64, 64, 64) |
(66, 66, 64) |
2D 卷积层 | filters=128, kernel_size=4, strides=2, padding='valid' |
(66, 66, 64) |
(32, 32, 128) |
实例规范化层 | axis=1 |
(32, 32, 128) |
(32, 32, 128) |
激活层 | activation='leakyrelu', alpha=0.2 |
(32, 32, 128) |
(32, 32, 128) |
2D 零填充层 | padding(1, 1) |
(32, 32, 128) |
(34, 34, 128) |
2D 卷积层 | filters=256, kernel_size=4, strides=2, padding='valid' |
(34, 34, 128) |
(16, 16, 256) |
实例规范化层 | axis=1 |
(16, 16, 256) |
(16, 16, 256) |
激活层 | activation='leakyrelu', alpha=0.2 |
(16, 16, 256) |
(16, 16, 256) |
2D 零填充层 | padding(1, 1) |
(16, 16, 256) |
(18, 18, 256) |
2D 卷积层 | filters=512, kernel_size=4, strides=2, padding='valid' |
(18, 18, 256) |
(8, 8, 512) |
实例规范化层 | axis=1 |
(8, 8, 512) |
(8, 8, 512) |
激活层 | activation='leakyrelu', alpha=0.2 |
(8, 8, 512) |
(8, 8, 512) |
2D 零填充层 | padding(1, 1) |
(8, 8, 512) |
(10, 10, 512) |
2D 卷积层 | filters=1, kernel_size=4, strides=1, padding='valid', activation='sigmoid' |
(10, 10, 512) |
(7, 7, 1) |
判别器网络返回形状为(7, 7, 1)
的张量。 现在,我们已经介绍了这两个网络的详细架构。 在下一节中,我们将介绍训练 CycleGAN 所需的目标函数。
ZeroPadding2D
层在图像张量的顶部,底部,左侧和右侧添加零行和零列。
与其他 GAN 相似,CycleGAN 具有训练目标函数,我们需要将其最小化以训练模型。 损失函数是以下损失的加权总和:
- 对抗损失
- 循环一致性损失
在以下各节中,让我们详细研究对抗性损失和周期一致性损失。
对抗性损失是实际分布A
或B
的图像与生成器网络生成的图像之间的损失。 我们有两个映射函数,我们将对两个映射应用对抗性损失。
映射G: X -> Y
的对抗损失如下所示:
在此,x
是来自分布A
的一个域的图像,y
是来自分布B
的另一个域的图像。判别器D[y]
试图区分G
映射(G(x)
)生成的图像和来自不同的分布B
的真实图像y
。判别器D[x]
试图区分F
映射生成的图像(F(y)
)和来自分布A
的真实图像x
。G
的目的是使对抗损失函数D
最小,后者不断尝试使其最大化。
仅使用对抗损失的问题在于,网络可以将同一组输入图像映射到目标域中图像的任何随机排列。 因此,任何学习的映射都可以学习类似于目标分布的输出分布。 x[i]
和y[i]
之间可能有许多可能的映射函数。 循环一致性损失通过减少可能的映射数来克服了这个问题。 周期一致映射函数是可以将图像x
从域A
转换为域B
中的另一个图像y
并生成原始图像的函数。
前向循环一致性映射函数如下所示:
向后循环一致映射函数如下所示:
循环一致性损失的公式如下:
由于循环一致性损失,由F(G(x))
和G(F(y))
重构的图像分别类似于x
和y
。
完整目标函数是对抗损失和周期一致性损失两者的加权总和,表示如下:
在此,l[GAN](G, D[Y], X, Y)
是第一个对抗性损失,l[GAN](F, D[X], Y, X)
是第二个对抗性损失。 在生成器A
和判别器B
上计算第一个对抗损失。在生成器B
和判别器A
上计算第二个对抗损失。
要训练 CycleGAN,我们需要优化以下函数:
前面的等式表明,训练一个 CycleGAN,需要最小化生成器网络的损失,并使判别器网络的损失最大化。 优化之后,我们将获得一组训练有素的网络,能够从绘画中生成照片。
如果尚未使用所有章节的完整代码克隆存储库,请立即克隆存储库。 下载的代码有一个名为Chapter07
的目录,其中包含本章的全部代码。 执行以下命令来设置项目:
- 首先,导航到父目录,如下所示:
cd Generative-Adversarial-Networks-Projects
- 现在,将目录从当前目录更改为
Chapter07
,如以下示例所示:
cd Chapter07
- 接下来,为该项目创建一个 Python 虚拟环境,如以下代码所示:
virtualenv venv
virtualenv venv -p python3 # Create a virtual environment using
python3 interpreter
virtualenv venv -p python2 # Create a virtual environment using
python2 interpreter
我们将为此项目使用此新创建的虚拟环境。 每章都有其自己单独的虚拟环境。
- 激活新创建的虚拟环境,如以下代码所示:
source venv/bin/activate
激活虚拟环境后,所有其他命令将在该虚拟环境中执行。
- 通过执行以下命令,安装
requirements.txt
文件中提供的所有库:
pip install -r requirements.txt
您可以参考 README.md
文件,以获取有关如何设置项目的更多说明。 开发人员经常会遇到依赖关系不匹配的问题。 为每个项目创建一个单独的虚拟环境将解决此问题。
在本节中,我们已经成功设置了项目并安装了必需的依赖项。 在下一节中,我们将处理数据集。
在本章中,我们将使用monet2photo
数据集。 该数据集是开放源代码,可以由 UC Berkeley 的 Berkeley AI Research(BAIR)实验室使用。 您可以从以下链接选择手动下载数据集。
下载后,将其解压缩到根目录中。
或者,要自动下载数据集,请执行以下命令:
wget https://people.eecs.berkeley.edu/~taesung_park/CycleGAN/datasets/monet2photo.zip
upzip monet2photo.zip
这些命令将下载数据集并将其解压缩到项目的根目录中。
monet2photo
数据集仅可用于教育目的。 要将其用于商业项目,您必须获得 BAIR 实验室 UC Berkeley 的许可。 我们不拥有数据集中可用图像的版权。
如本章前面“CycleGAN 简介”部分中所述,CycleGAN 具有两种网络架构,即生成器网络和判别器网络。 在本节中,我们将编写所有网络的实现。
但是,在开始编写实现之前,请创建一个 Python 文件main.py
并导入基本模块,如下所示:
from glob import glob
import matplotlib.pyplot as plt
import numpy as np
import tensorflow as tf
from keras import Input, Model
from keras.layers import Conv2D, BatchNormalization, Activation, Add, Conv2DTranspose, \
ZeroPadding2D, LeakyReLU
from keras.optimizers import Adam
from keras_contrib.layers import InstanceNormalization
from scipy.misc import imread, imresize
在本章前面的“生成器网络的架构”部分中,我们已经探讨了生成器网络的架构。 让我们首先在 Keras 框架中编写生成器网络的层,然后使用 Keras 框架的函数式 API 创建 Keras 模型。
执行以下步骤以在 Keras 中实现生成器网络:
- 首先定义生成器网络所需的超参数,如下所示:
input_shape = (128, 128, 3)
residual_blocks = 6
- 接下来,创建一个输入层,将输入馈送到网络,如下所示:
input_layer = Input(shape=input_shape)
- 将第一个卷积块与先前在“生成器网络”部分的架构中指定的超参数相加,如下所示:
x = Conv2D(filters=32, kernel_size=7, strides=1, padding="same")(input_layer)
x = InstanceNormalization(axis=1)(x)
x = Activation("relu")(x)
- 添加第二个卷积块,如下所示:
x = Conv2D(filters=64, kernel_size=3, strides=2, padding="same")(x)
x = InstanceNormalization(axis=1)(x)
x = Activation("relu")(x)
- 添加第三个卷积块,如下所示:
x = Conv2D(filters=128, kernel_size=3, strides=2, padding="same")(x)
x = InstanceNormalization(axis=1)(x)
x = Activation("relu")(x)
- 定义残差块,如下所示:
def residual_block(x):
"""
Residual block """ res = Conv2D(filters=128, kernel_size=3, strides=1, padding="same")(x)
res = BatchNormalization(axis=3, momentum=0.9, epsilon=1e-5)(res)
res = Activation('relu')(res)
res = Conv2D(filters=128, kernel_size=3, strides=1, padding="same")(res)
res = BatchNormalization(axis=3, momentum=0.9, epsilon=1e-5)(res)
return Add()([res, x])
现在,使用residual_block()
函数向模型添加六个残差块,如以下示例所示:
for _ in range(residual_blocks):
x = residual_block(x)
- 接下来,添加一个上采样块,如下所示:
x = Conv2DTranspose(filters=64, kernel_size=3, strides=2, padding='same', use_bias=False)(x)
x = InstanceNormalization(axis=1)(x)
x = Activation("relu")(x)
- 添加另一个上采样模块,如下所示:
x = Conv2DTranspose(filters=32, kernel_size=3, strides=2, padding='same', use_bias=False)(x)
x = InstanceNormalization(axis=1)(x)
x = Activation("relu")(x)
- 最后,添加输出卷积层,如下所示:
x = Conv2D(filters=3, kernel_size=7, strides=1, padding="same")(x)
output = Activation('tanh')(x)
这是生成器网络的最后一层。 它生成形状为(128, 128, 3)
的图像。
- 现在,通过为网络指定
inputs
和outputs
来创建 Keras 模型,如下所示:
model = Model(inputs=[input_layer], outputs=[output])
生成器网络的整个代码如下所示:
def build_generator():
"""
Create a generator network using the hyperparameter values defined below """ input_shape = (128, 128, 3)
residual_blocks = 6
input_layer = Input(shape=input_shape)
# First Convolution block
x = Conv2D(filters=32, kernel_size=7, strides=1, padding="same")(input_layer)
x = InstanceNormalization(axis=1)(x)
x = Activation("relu")(x)
# 2nd Convolution block
x = Conv2D(filters=64, kernel_size=3, strides=2, padding="same")(x)
x = InstanceNormalization(axis=1)(x)
x = Activation("relu")(x)
# 3rd Convolution block
x = Conv2D(filters=128, kernel_size=3, strides=2, padding="same")(x)
x = InstanceNormalization(axis=1)(x)
x = Activation("relu")(x)
# Residual blocks
for _ in range(residual_blocks):
x = residual_block(x)
# Upsampling blocks
# 1st Upsampling block
x = Conv2DTranspose(filters=64, kernel_size=3, strides=2, padding='same', use_bias=False)(x)
x = InstanceNormalization(axis=1)(x)
x = Activation("relu")(x)
# 2nd Upsampling block
x = Conv2DTranspose(filters=32, kernel_size=3, strides=2, padding='same', use_bias=False)(x)
x = InstanceNormalization(axis=1)(x)
x = Activation("relu")(x)
# Last Convolution layer
x = Conv2D(filters=3, kernel_size=7, strides=1, padding="same")(x)
output = Activation('tanh')(x)
model = Model(inputs=[input_layer], outputs=[output])
return model
我们已经成功地为生成器网络创建了 Keras 模型。 在下一节中,我们将为判别器网络创建 Keras 模型。
我们已经在“判别器网络的架构”部分中探索了判别器网络的架构。让我们首先在 Keras 框架中编写判别器网络的层,然后使用 Keras 框架的函数式 API 创建 Keras 模型。
执行以下步骤以在 Keras 中实现判别器网络:
- 首先定义判别器网络所需的超参数,如下所示:
input_shape = (128, 128, 3)
hidden_layers = 3
- 接下来,添加一个输入层,将输入馈送到网络,如下所示:
input_layer = Input(shape=input_shape)
- 接下来,添加一个二维零填充层,如下所示:
x = ZeroPadding2D(padding=(1, 1))(input_layer)
该层将在x
和y
轴上为输入张量添加填充。
- 接下来,使用先前在“判别器网络”部分的架构中指定的超参数添加卷积块,如下所示:
x = Conv2D(filters=64, kernel_size=4, strides=2, padding="valid")(x)
x = LeakyReLU(alpha=0.2)(x)
- 接下来,添加另一个 2D 零填充层,如下所示:
x = ZeroPadding2D(padding=(1, 1))(x)
- 接下来,使用先前在“判别器网络”部分中指定的超参数添加三个卷积块,如下所示:
for i in range(1, hidden_layers + 1):
x = Conv2D(filters=2 ** i * 64, kernel_size=4, strides=2, padding="valid")(x)
x = InstanceNormalization(axis=1)(x)
x = LeakyReLU(alpha=0.2)(x)
x = ZeroPadding2D(padding=(1, 1))(x)
每个卷积块都有两个卷积层,实例规范化层,激活层和 2D 零填充层。
- 现在,将最终(
output
)卷积层添加到网络,如下所示:
output = Conv2D(filters=1, kernel_size=4, strides=1, activation="sigmoid")(x)
- 最后,通过指定网络的输入和输出来创建 Keras 模型,如下所示:
model = Model(inputs=[input_layer], outputs=[output])
判别器网络的整个代码如下所示:
def build_discriminator():
"""
Create a discriminator network using the hyperparameter values defined below """ input_shape = (128, 128, 3)
hidden_layers = 3 input_layer = Input(shape=input_shape)
x = ZeroPadding2D(padding=(1, 1))(input_layer)
# 1st Convolutional block
x = Conv2D(filters=64, kernel_size=4, strides=2, padding="valid")(x)
x = LeakyReLU(alpha=0.2)(x)
x = ZeroPadding2D(padding=(1, 1))(x)
# 3 Hidden Convolution blocks
for i in range(1, hidden_layers + 1):
x = Conv2D(filters=2 ** i * 64, kernel_size=4, strides=2, padding="valid")(x)
x = InstanceNormalization(axis=1)(x)
x = LeakyReLU(alpha=0.2)(x)
x = ZeroPadding2D(padding=(1, 1))(x)
# Last Convolution layer
output = Conv2D(filters=1, kernel_size=4, strides=1, activation="sigmoid")(x)
model = Model(inputs=[input_layer], outputs=[output])
return model
我们也已经成功地为判别器网络创建了 Keras 模型。 在下一部分中,我们将训练网络。
我们已经在“CycleGANs 简介”部分中介绍了训练目标函数。 我们还为两个网络分别创建了 Keras 模型。 训练 CycleGAN 是一个多步骤的过程。 我们将执行以下步骤来训练网络:
- 加载数据集
- 创建生成器和判别器网络
- 训练网络以达到指定次数
- 绘制损失
- 生成新图像
让我们在开始训练网络之前定义基本变量,如下所示:
data_dir = "/Path/to/dataset/directory/*.*" batch_size = 1 epochs = 500
在执行其他任何操作之前,请执行以下步骤来加载数据集:
- 首先使用
glob
模块创建图像路径列表,如下所示:
imagesA = glob(data_dir + '/testA/*.*')
imagesB = glob(data_dir + '/testB/*.*')
我们具有来自两个域A
和B
的数据,这就是为什么我们创建了两个列表的原因。
- 接下来,遍历列表。 在循环中加载,调整大小和水平翻转图像,如下所示:
allImagesA = []
allImagesB = []
# Iterate over the lists
for index, filename in enumerate(imagesA):
# Load images
imgA = imread(filename, mode='RGB')
imgB = imread(imagesB[index], mode='RGB')
# Resize images
imgA = imresize(imgA, (128, 128))
imgB = imresize(imgB, (128, 128))
# Randomly horizontally flip images
if np.random.random() > 0.5:
imgA = np.fliplr(imgA)
imgB = np.fliplr(imgB)
allImagesA.append(imgA)
allImagesB.append(imgB)
- 现在,对图像进行归一化以使像素值在 -1 和 1 之间的范围内,如下所示:
# Normalize images allImagesA = np.array(allImagesA) / 127.5 - 1. allImagesB = np.array(allImagesB) / 127.5 - 1.
加载数据集的整个代码如下所示:
def load_images(data_dir):
imagesA = glob(data_dir + '/testA/*.*')
imagesB = glob(data_dir + '/testB/*.*')
allImagesA = []
allImagesB = []
for index, filename in enumerate(imagesA):
# Load images
imgA = imread(filename, mode='RGB')
imgB = imread(imagesB[index], mode='RGB')
# Resize images
imgA = imresize(imgA, (128, 128))
imgB = imresize(imgB, (128, 128))
# Randomly horizontally flip images
if np.random.random() > 0.5:
imgA = np.fliplr(imgA)
imgB = np.fliplr(imgB)
allImagesA.append(imgA)
allImagesB.append(imgB)
# Normalize images
allImagesA = np.array(allImagesA) / 127.5 - 1.
allImagesB = np.array(allImagesB) / 127.5 - 1. return allImagesA, allImagesB
前面的函数将返回两个 Numpy ndarray
。 在开始训练之前,我们将使用它来加载和预处理图像。
在本节中,让我们构建必要的网络并准备进行训练。 执行以下步骤:
- 首先定义训练所需的优化器,如以下代码所示:
# Define the common optimizer common_optimizer = Adam(0.0002, 0.5)
我们将使用Adam
优化器,其中learning_rate
等于 0.0002,并且beta_1
值等于 0.5。
- 首先创建判别器网络,如以下代码所示:
discriminatorA = build_discriminator()
discriminatorB = build_discriminator()
如“判别器网络的架构”部分所述,CycleGAN 具有两个判别器网络。
- 接下来,编译网络,如下所示:
discriminatorA.compile(loss='mse', optimizer=common_optimizer, metrics=['accuracy'])
discriminatorB.compile(loss='mse', optimizer=common_optimizer, metrics=['accuracy'])
使用mse
作为损失函数,并使用accuracy
作为度量标准来编译网络。
- 接下来,创建生成器网络
A
(generatorAToB
)和B
(generatorBToA
)。 生成器网络A
的输入是数据集A
的真实图像(realA
),输出将是重构图像(fakeB
)。 生成器网络B
的输入是来自数据集B
的真实图像(realB
),输出将是重构图像(fakeA
),如下所示:
generatorAToB = build_generator()
generatorBToA = build_generator()
如“CycleGAN 的架构”部分所述,CycleGAN 具有两个生成器网络。 generatorAToB
会将图像从域A
转换为域B
。generatorBToA
会将图像从域B
转换为域A
。
现在,我们已经创建了两个生成器网络和两个判别器网络。 在下一个小节中,我们将创建并编译一个对抗网络。
对抗网络是一个组合网络。 它在单个 Keras 模型中使用所有四个网络。 创建对抗网络的主要目的是训练生成器网络。 当我们训练对抗网络时,它只训练生成器网络,但冻结了判别器网络的训练。 让我们创建一个具有所需功能的对抗模型。
- 首先为网络创建两个输入层,如下所示:
inputA = Input(shape=(128, 128, 3))
inputB = Input(shape=(128, 128, 3))
两个输入都将拍摄大小为(128, 128, 3)
的图像。 这些是符号输入变量,不包含实际值。 它们用于创建 Keras 模型(TensorFlow 图)。
- 接下来,使用生成器网络生成伪造图像,如下所示:
generatedB = generatorAToB(inputA)
generatedA = generatorBToA(inputB)
使用符号输入层生成图像。
- 现在,再次使用生成器网络重建原始图像,如下所示:
reconstructedA = generatorBToA(generatedB)
reconstructedB = generatorAToB(generatedA)
- 使用生成器网络生成伪造图像,如下所示:
generatedAId = generatorBToA(inputA)
generatedBId = generatorAToB(inputB)
生成器网络A
(generatorAToB
)将图像从域A
转换为域B
。类似地,生成器网络B
(generatorBToA
)将图像从域B
转换为域A
。
- 接下来,使两个判别器网络均不可训练,如下所示:
discriminatorA.trainable = False discriminatorB.trainable = False
我们不想在我们的对抗网络中训练判别器网络。
- 使用判别器网络来预测每个生成的图像是真实的还是伪造的,如下所示:
probsA = discriminatorA(generatedA)
probsB = discriminatorB(generatedB)
- 创建 Keras 模型并指定网络的输入和输出,如下所示:
adversarial_model = Model(inputs=[inputA, inputB],outputs=[probsA, probsB, reconstructedA, reconstructedB, generatedAId, generatedBId])
我们的对抗网络将采用两个输入值(即张量),并返回六个输出值(即张量)。
- 接下来,按如下所示编译对抗网络:
adversarial_model.compile(loss=['mse', 'mse', 'mae', 'mae', 'mae', 'mae'],
loss_weights=[1, 1, 10.0, 10.0, 1.0, 1.0],
optimizer=common_optimizer)
对抗网络返回六个值,我们需要为每个输出值指定损失函数。 对于前两个值,我们使用均方误差损失,因为这是对抗性损失的一部分。 对于接下来的四个值,我们使用平均绝对误差损失,这是周期一致性损失的一部分。 六个不同损失的权重值为 1,1,10.0,10.0,1.0,1.0。 我们正在使用common_optimizer
训练网络。
现在,我们已经成功为对抗网络创建了 Keras 模型。 如果您在理解 Keras 模型的工作方式时遇到困难,请查看 TensorFlow 图及其函数的文档。
在开始训练之前,请执行以下两个基本步骤。 TensorBoard 将在后面的部分中使用:
添加 TensorBoard 来存储损失和图以用于可视化目的,如下所示:
tensorboard = TensorBoard(log_dir="logs/{}".format(time.time()), write_images=True, write_grads=True,
write_graph=True)
tensorboard.set_model(generatorAToB)
tensorboard.set_model(generatorBToA)
tensorboard.set_model(discriminatorA)
tensorboard.set_model(discriminatorB)
创建一个包含所有等于 1 的值的 4 维数组,该数组表示真实标签。 同样,创建另一个所有值均等于零的三维数组,代表伪标签,如下所示:
real_labels = np.ones((batch_size, 7, 7, 1))
fake_labels = np.zeros((batch_size, 7, 7, 1))
使用 numpy 的ones()
和zeros()
函数创建所需的ndarray
。 现在我们已经准备好基本组件,让我们开始训练。
要针对指定的周期数训练网络,请执行以下步骤:
- 首先加载两个域的数据集,如下所示:
imagesA, imagesB = load_images(data_dir=data_dir)
我们已经在定义了load_images
函数。
- 接下来,创建一个
for
循环,该循环应运行由周期数指定的次数,如下所示:
for epoch in range(epochs):
print("Epoch:{}".format(epoch))
- 创建两个列表以存储所有小批量的损失,如下所示:
dis_losses = []
gen_losses = []
- 计算
epochs
循环内的小批量数量,如下所示:
num_batches = int(min(imagesA.shape[0], imagesB.shape[0]) /
batch_size)
print("Number of batches:{}".format(num_batches))
- 接下来,在周期循环内创建另一个循环,并使其运行
num_batches
指定的次数,如下所示:
for index in range(num_batches):
print("Batch:{}".format(index))
我们用于判别器网络和对抗网络训练的整个代码将在此循环内。
本小节中的代码是上一节中代码的延续。 在这里,您将看到如何训练判别器网络:
- 首先对两个域的图像进行小批量采样,如以下代码所示:
batchA = imagesA[index * batch_size:(index + 1) * batch_size]
batchB = imagesB[index * batch_size:(index + 1) * batch_size]
- 接下来,使用生成器网络生成伪造图像,如下所示:
generatedB = generatorAToB.predict(batchA)
generatedA = generatorBToA.predict(batchB)
- 然后,对判别器网络
A
进行真实图像和伪图像(由生成器网络生成)的训练,如下所示:
dALoss1 = discriminatorA.train_on_batch(batchA, real_labels)
dALoss2 = discriminatorB.train_on_batch(generatedA, fake_labels)
此步骤将在真实图像和伪图像的微型批量上训练判别器A
,并会稍微改善网络。
- 接下来,对判别器
B
进行真实图像和伪图像的训练,如下所示:
dBLoss1 = discriminatorB.train_on_batch(batchB, real_labels)
dbLoss2 = discriminatorB.train_on_batch(generatedB, fake_labels)
- 现在,计算判别器网络的总损失值,如下所示:
d_loss = 0.5 * np.add(0.5 * np.add(dALoss1, dALoss2), 0.5 *
np.add(dBLoss1, dbLoss2))
到目前为止,我们一直在添加代码来训练判别器网络。 在下一部分中,我们将训练对抗性网络以训练生成器网络。
为了训练对抗网络,我们需要输入值和真实值。 网络的输入值为batchA
和batchB
。 基本真值是real_labels
,real_labels
,batchA
,batchB
,batchA
和batchB
,如下所示:
g_loss = adversarial_model.train_on_batch([batchA, batchB],
[real_labels, real_labels, batchA, batchB, batchA, batchB])
此步骤将训练生成器网络,而无需训练生成器网络。
在每个微型批量上完成一次迭代(循环)之后,将损失存储在名为dis_losses
和gen_losses
的列表中,如下所示:
dis_losses.append(d_loss)
gen_losses.append(g_loss)
每 10 个周期后,使用生成器网络生成一组图像:
# Sample and save images after every 10 epochs if epoch % 10 == 0:
# Get a batch of test data
batchA, batchB = load_test_batch(data_dir=data_dir, batch_size=2)
# Generate images
generatedB = generatorAToB.predict(batchA)
generatedA = generatorBToA.predict(batchB)
# Get reconstructed images
reconsA = generatorBToA.predict(generatedB)
reconsB = generatorAToB.predict(generatedA)
# Save original, generated and reconstructed images
for i in range(len(generatedA)):
save_images(originalA=batchA[i], generatedB=generatedB[i], recosntructedA=reconsA[i],
originalB=batchB[i], generatedA=generatedA[i], reconstructedB=reconsB[i],
path="results/gen_{}_{}".format(epoch, i))
将前面的代码块放入epochs
循环中。 每隔 10 个时间段,它将生成一批伪图像并将其保存到结果目录。
接下来,将平均损失存储到 TensorBoard 中以进行可视化。 既存储损失,也要存储生成器网络的平均损失和判别器网络的平均损失,如以下示例所示:
write_log(tensorboard, 'discriminator_loss', np.mean(dis_losses),
epoch)
write_log(tensorboard, 'generator_loss', np.mean(gen_losses), epoch)
将前面的代码块放入epochs
循环中。
在 Keras 中保存模型只需要一行代码。 要保存生成器模型,请添加以下行:
# Specify the path for the generator A model
generatorAToB.save("directory/for/the/generatorAToB/model.h5")
# Specify the path for the generator B model
generatorBToA.save("directory/for/the/generatorBToA/model.h5")
同样,通过添加以下行来保存判别器模型:
# Specify the path for the discriminator A model
discriminatorA.save("directory/for/the/discriminatorA/model.h5")
# Specify the path for the discriminator B model
discriminatorB.save("directory/for/the/discriminatorB/model.h5")
在将网络训练了 100 个周期之后,网络将开始生成体面的图像。 让我们看一下生成器网络生成的图像。
10 个周期后,图像显示如下:
在 20 个周期之后,图像显示如下:
我建议您将网络训练 1000 个周期。 如果一切顺利,则在 1000 个周期之后,生成器网络将开始生成逼真的图像。
为了可视化训练的损失,请按以下方式启动 TensorBoard 服务器:
tensorboard --logdir=logs
现在,在浏览器中打开localhost:6006
。 TensorBoard 的标量部分包含两个损失的图,如以下示例所示:
判别器网络的损失图如下所示:
生成器网络的损失图如下所示:
这些图将帮助您决定是继续还是停止训练。 如果损失不再减少,您就可以停止训练,因为没有改善的机会。 如果损失持续增加,则必须停止训练。 使用超参数,然后选择一组您认为可以提供更好结果的超参数。 如果损失逐渐减少,请继续训练模型。
TensorBoard 的GRAPHS
部分包含两个网络的图。 如果网络表现不佳,这些图可以帮助您调试网络。 它们还显示了每个图中的张量流和不同的操作,如以下示例所示:
CycleGAN 有许多应用。 在本章中,我们使用了 CycleGAN 将画作转换为照片。 它们还可以在以下情况下使用:
- 样式迁移:例如,将照片转换为绘画,反之亦然,将马的图像转换为斑马,反之亦然,将橘子的图像转换为苹果,反之亦然
- 照片增强:CycleGAN 可用于增强图片质量
- 季节转换:例如,将冬天的图片变成夏天的图片,反之亦然
- 游戏风格迁移:CycleGAN 可用于将游戏 A 的风格迁移到游戏 B
在本章中,我们学习了如何使用 CycleGAN 将绘画转换为照片。 我们从介绍 CyleGAN 入手,并探讨了 CycleGAN 所涉及的网络架构。 我们还探讨了训练 CycleGAN 所需的不同损失函数。 接下来是在 Keras 框架中实现 CycleGAN。 我们在monet2photo
数据集上训练了 CycleGAN,并可视化了生成的图像,损失和不同网络的图。 在结束本章之前,我们探讨了 CycleGAN 的实际用例。
在下一章中,我们将在 pix2pix 网络上进行图像到图像的翻译。 在 pix2pix 中,我们将探索条件 GAN 进行图像翻译。
CycleGAN 有许多已知的用例。 使用以下文章寻求帮助,探索新用途: