生成对抗网络项目(三)
七、CycleGAN - 将绘画变成照片
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 的实际应用
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 的架构
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 的 Keras 实现
如本章前面“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 模型。 在下一部分中,我们将训练网络。
训练 CycleGAN
我们已经在“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 可用于增强图片质量
- 季节转换:例如,将冬天的图片变成夏天的图片,反之亦然
- 游戏风格迁移:CycleGAN 可用于将游戏 A 的风格迁移到游戏 B
总结
在本章中,我们学习了如何使用 CycleGAN 将绘画转换为照片。 我们从介绍 CyleGAN 入手,并探讨了 CycleGAN 所涉及的网络架构。 我们还探讨了训练 CycleGAN 所需的不同损失函数。 接下来是在 Keras 框架中实现 CycleGAN。 我们在monet2photo数据集上训练了 CycleGAN,并可视化了生成的图像,损失和不同网络的图。 在结束本章之前,我们探讨了 CycleGAN 的实际用例。
在下一章中,我们将在 pix2pix 网络上进行图像到图像的翻译。 在 pix2pix 中,我们将探索条件 GAN 进行图像翻译。
进一步阅读
CycleGAN 有许多已知的用例。 使用以下文章寻求帮助,探索新用途:
- 通过深度学习(CycleGAN)将 Fortnite 变成 PUBG
- GAN-CycleGAN(用图片玩魔术)
- GAN 和 CycleGAN
- CycleGANs 简介
- 在 TensorFlow 中理解和实现 CycleGAN
八、条件 GAN - 使用条件对抗网络的图像到图像翻译
Pix2pix 是一种生成对抗网络(GAN),用于图像到图像的翻译。 图像到图像转换是一种将图像的一种表示形式转换为另一种表示形式的方法。 Pix2pix 学习从输入图像到输出图像的映射。 它可用于将黑白图像转换为彩色图像,将草图转换为照片,将白天图像转换为夜间图像,将卫星图像转换为地图图像。 pix2pix 网络最早是由菲利普·伊索拉(Phillip Isola),朱俊彦,周婷慧,阿列克谢·埃弗罗斯(Alexei A. Efros)在名为《使用条件对抗网络的图像到图像转换》中引入的。 可以在以下链接中找到。
在本章中,我们将介绍以下主题:
- Pix2pix 网络介绍
- Pix2pix 网络的架构
- 数据收集与准备
- Pix2pix 的 Keras 实现
- 目标函数
- 训练 Pix2pix
- 评估训练好的模型
- Pix2pix 网络的实际应用
Pix2pix 介绍
Pix2pix 是条件 GAN 的变体。 我们已经在第 3 章,“使用条件 GAN(cGAN)进行人脸老化处理”中介绍。 在继续之前,请确保您了解什么是 cGAN。 一旦熟悉了 cGAN,就可以继续本章。 Pix2pix 是一种 GAN,能够使用机器学习(ML)的无监督方法执行图像到图像的翻译。 经过训练后,pix2pix 可以将图像从域 A 转换为域 B。朴素 CNN 也可以用于图像到图像的转换,但是它们不会生成逼真的图像。 另一方面,pix2pix 显示出巨大的潜力,能够生成逼真的图像。 我们将训练 pix2pix 将立面的标签转换为立面的图像。 让我们从了解 pix2pix 的架构开始。
pix2pix 的架构
与其他 GAN 相似,pix2pix 由两个网络组成:生成器网络和判别器网络。 生成器网络的架构受 U-Net 的架构的启发。 同样,判别器网络的架构也受到 PatchGAN 架构的启发。 这两个网络都是深度卷积神经网络。 在本节中,我们将详细探讨 pix2pix。
生成器网络
正如我们在上一节中提到的,生成器网络在很大程度上受到 U-Net 架构的启发。 U-Net 的架构与自编码器网络的架构几乎相同。 它们之间的主要区别在于,U-Net 网络在编码器中的各层之间具有跳跃连接,并且生成器网络和自编码器的解码器部分不具有跳跃连接。 U-Net 网络由两个网络组成:编码器网络和解码器网络。 下图从基本层面说明了 U-Net 的架构:
上图应使您了解 U-Net 的架构。 如您所见,第一层的输出直接与最后一层合并。 第二层的输出与第二层的第二层合并,依此类推。 如果n是层的总数,则在编码器网络的第i层与解码器网络的第n - i层之间存在跳跃连接。 第i层可以是这些层中的任何层。 让我们一个一个地仔细研究两个网络。
编码器网络
编码器网络是生成器网络的初始网络,包含八个卷积块,其配置如下:
| 层名称 | 超参数 | 输入形状 | 输出形状 |
|---|---|---|---|
| 第一个 2D 卷积层 | filter= 64, kernel_size = 4, stride= 2, padding ='same' | (256, 256, 1) | (128, 128, 64) |
| 激活层 | Activation ='leakyrelu', alpha = 0.2 | (128, 128, 64) | (128, 128, 64) |
| 第二个 2D 卷积层 | filter= 128, kernel_size = 4, stride= 2, padding ="same" | (128, 128, 64) | (64, 64, 128) |
| 批量规范化层 | 没有 | (64, 64, 128) | (64, 64, 128) |
| 激活层 | Activation ='leakyrelu', alpha = 0.2 | (64, 64, 128) | (64, 64, 128) |
| 第三个 2D 卷积层 | filter= 256, kernel_size = 4, stride= 2, padding ='same' | (64, 64, 128) | (32, 32, 256) |
| 批量规范化层 | 没有 | (32, 32, 256) | (32, 32, 256) |
| 激活层 | Activation ='leakyrelu', alpha = 0.2 | (32, 32, 256) | (32, 32, 256) |
| 第四个 2D 卷积层 | filter= 512, kernel_size = 4, stride= 2, padding ="same" | (32, 32, 256) | (16, 16, 512) |
| 批量规范化层 | 没有 | (16, 16, 512) | (16, 16, 512) |
| 激活层 | Activation ='leakyrelu', alpha = 0.2 | (16, 16, 512) | (16, 16, 512) |
| 第五个 2D 卷积层 | filter= 512, kernel_size = 4, stride= 2, padding ="same" | (16, 16, 512) | (8, 8, 512) |
| 批量规范化层 | 没有 | (8, 8, 512) | (8, 8, 512) |
| 激活层 | Activation ='leakyrelu', alpha = 0.2 | (8, 8, 512) | (8, 8, 512) |
| 第六个 2D 卷积层 | filter= 512, kernel_size = 4, stride= 2, padding ="same" | (8, 8, 512) | (4, 4, 512) |
| 批量规范化层 | 没有 | (4, 4, 512) | (4, 4, 512) |
| 激活层 | Activation ='leakyrelu', alpha = 0.2 | (4, 4, 512) | (4, 4, 512) |
| 第七个 2D 卷积层 | filter= 512, kernel_size = 4, stride= 2, padding ="same" | (4, 4, 512) | (2, 2, 512) |
| 批量规范化层 | 没有 | (2, 2, 512) | (2, 2, 512) |
| 激活层 | Activation ='leakyrelu', alpha = 0.2 | (2, 2, 512) | (2, 2, 512) |
| 第八个 2D 卷积层 | filter= 512, kernel_size = 4, stride= 2, padding ="same" | (2, 2, 512) | (1, 1, 512) |
| 批量规范化层 | 没有 | (1, 1, 512) | (1, 1, 512) |
| 激活层 | Activation ='leakyrelu', alpha = 0.2 | (1, 1, 512) | (1, 1, 512) |
编码器网络之后是解码器网络。 我们将在下一节中了解解码器网络的架构。
解码器网络
生成器网络中的解码器网络还包括八个上采样卷积块。 八个上采样卷积块的配置如下:
| 层名称 | 超参数 | 输入形状 | 输出形状 |
|---|---|---|---|
| 第一个 2D 上采样层 | size=(2, 2) | (1, 1, 512) | (2, 2, 512) |
| 2D 卷积层 | filter= 512, kernel_size = 4, stride= 1, padding="same" | (2, 2, 512) | (2, 2, 512) |
| 批量规范化层 | 没有 | (2, 2, 512) | (2, 2, 512) |
| 丢弃层 | 丢弃= 0.5 | (2, 2, 512) | (2, 2, 512) |
| 级联层(编码器网络中的第 7 个卷积层) | axis=3 | (2, 2, 512) | (2, 2, 1024) |
| 激活层 | activation="relu" | (2, 2, 1024) | (2, 2, 1024) |
| 第二个 2D 上采样层 | size=(2, 2) | (2, 2, 1024) | (4, 4, 1024) |
| 2D 卷积层 | filter= 1024, kernel_size = 4, stride= 1, padding="same" | (4, 4, 1024) | (4, 4, 1024) |
| 批量规范化层 | 没有 | (4, 4, 1024) | (4, 4, 1024) |
| 丢弃层 | 丢弃= 0.5 | (4, 4, 1024) | (4, 4, 1024) |
| 级联层(编码器网络中的第 6 个卷积层) | axis=3 | (4, 4, 1024) | (4, 4, 1536) |
| 激活层 | activation="relu" | (4, 4, 1536) | (4, 4, 1536) |
| 第三个 2D 上采样层 | size=(2, 2) | (4, 4, 1536) | (8, 8, 1536) |
| 2D 卷积层 | filter= 1024, kernel_size = 4, stride= 1, padding="same" | (8, 8, 1536) | (8, 8, 1024) |
| 批量规范化层 | 没有 | (8, 8, 1024) | (8, 8, 1024) |
| 丢弃层 | 丢弃= 0.5 | (8, 8, 1024) | (8, 8, 1024) |
| 级联层(编码器网络中的第 5 个卷积层) | axis=3 | (8, 8, 1024) | (8, 8, 1536) |
| 激活层 | activation="relu" | (8, 8, 1536) | (8, 8, 1536) |
| 第四个 2D 上采样层 | size=(2, 2) | (8, 8, 1536) | (16, 16, 1536) |
| 2D 卷积层 | filter= 1024, kernel_size = 4, stride= 1, padding="same" | (16, 16, 1536) | (16, 16, 1024) |
| 批量规范化层 | 没有 | (16, 16, 1024) | (16, 16, 1024) |
| 连接层(编码器网络的第 4 个卷积层) | axis=3 | (16, 16, 1024) | (16, 16, 1536) |
| 激活层 | activation="relu" | (16, 16, 1536) | (16, 16, 1536) |
| 第五个 2D 上采样层 | size=(2, 2) | (16, 16, 1536) | (32, 32, 1536) |
| 2D 卷积层 | filter= 1024, kernel_size = 4, stride= 1, padding="same" | (32, 32, 1536) | (32, 32, 1024) |
| 批量规范化层 | 没有 | (32, 32, 1024) | (32, 32, 1024) |
| 连接层(编码器网络的第 3 个卷积层) | axis=3 | (32, 32, 1024) | (32, 32, 1280) |
| 激活层 | activation="relu" | (32, 32, 1280) | (32, 32, 1280) |
| 第六个 2D 上采样层 | size=(2, 2) | (64, 64, 1280) | (64, 64, 1280) |
| 2D 卷积层 | filter= 512, kernel_size = 4, stride= 1, padding="same" | (64, 64, 1280) | (64, 64, 512) |
| 批量规范化层 | 没有 | (64, 64, 512) | (64, 64, 512) |
| 连接层(编码器网络中的第二个卷积层) | axis=3 | (64, 64, 512) | (64, 64, 640) |
| 激活层 | activation="relu" | (64, 64, 640) | (64, 64, 640) |
| 第七个 2D 上采样层 | size=(2, 2) | (64, 64, 640) | (128, 128, 640) |
| 2D 卷积层 | filter= 256, kernel_size = 4, stride= 1, padding="same" | (128, 128, 640) | (128, 128, 256) |
| 批量规范化层 | 没有 | (128, 128, 256) | (128, 128, 256) |
| 连接层(编码器网络中的第一个卷积层) | axis=3 | (128, 128, 256) | (128, 128, 320) |
| 激活层 | activation="relu" | (128, 128, 320) | (128, 128, 320) |
| 第八个 2D 上采样层 | size=(2, 2) | (128, 128, 320) | (256, 256, 320) |
| 2D 卷积层 | filter= 1, kernel_size = 4, stride= 1, padding="same" | (256, 256, 320) | (256, 256, 1) |
| 激活层 | activation="tanh" | (256, 256, 1) | (256, 256, 1) |
生成器网络具有七个跳跃连接,可以将其定义如下:
- 从第 1 个编码器模块到第 7 个解码器模块的输出的连接
- 从第 2 个编码器模块到第 6 个解码器模块的输出的连接
- 从第 3 个编码器模块到第 5 个解码器模块的输出的连接
- 从第 4 个编码器模块到第 4 个解码器模块的输出的连接
- 从第 5 个编码器模块到第 3 个解码器模块的输出的连接
- 从第 6 个编码器模块到第 2 个解码器模块的输出的连接
- 从第 7 个编码器模块到第 1 个解码器模块的输出的连接
连接沿着通道轴发生。 编码器网络的最后一层将张量传递到解码器网络的第一层。 在编码器网络的最后一块和解码器网络的最后一块没有连接。
生成器网络由这两个网络组成。 基本上,编码器网络是下采样器,而解码器网络是上采样器。 编码器网络将大小为(256, 256, 1)的图像采样为大小为(1, 1, 1, 512)的内部表示。 另一方面,解码器网络将大小为(1, 1, 1, 512)的内部表示采样为大小为(256, 256, 1)的输出图像。
在“pix2pix 的 Keras 实现”中,我们将详细介绍该架构。
判别器网络
pix2pix 中判别器网络的架构受到 PatchGAN 网络架构的启发。 PatchGAN 网络包含八个卷积块,如下所示:
| 层名称 | 超参数 | 输入形状 | 输出形状 |
|---|---|---|---|
| 第一个 2D 卷积层 | filter= 64, kernel_size = 4, stride= 2, padding ='same' | (256, 256, 1) | (256, 256, 64) |
| 激活层 | Activation ='leakyrelu', alpha = 0.2 | (128, 128, 64) | (128, 128, 64) |
| 第二个 2D 卷积层 | filter= 128, kernel_size = 4, stride= 2, padding ="same" | (128, 128, 64) | (64, 64, 128) |
| 批量规范化层 | 没有 | (64, 64, 128) | (64, 64, 128) |
| 激活层 | Activation ='leakyrelu', alpha = 0.2 | (64, 64, 128)[ | (64, 64, 128) |
| 第三个 2D 卷积层 | filter= 256, kernel_size = 4, stride= 2, padding ='same' | (64, 64, 128) | (32, 32, 256) |
| 批量规范化层 | 没有 | (32, 32, 256) | (32, 32, 256) |
| 激活层 | Activation ='leakyrelu', alpha = 0.2 | (32, 32, 256) | (32, 32, 256) |
| 第四个 2D 卷积层 | filter= 512, kernel_size = 4, stride= 2, padding ="same" | (32, 32, 256) | (16, 16, 512) |
| 批量规范化层 | 没有 | (16, 16, 512) | (16, 16, 512) |
| 激活层 | Activation ='leakyrelu', alpha = 0.2 | (16, 16, 512) | (16, 16, 512) |
| 第五个 2D 卷积层 | filter= 512, kernel_size = 4, stride= 2, padding ="same" | (16, 16, 512) | (8, 8, 512) |
| 批量规范化层 | 没有 | (8, 8, 512) | (8, 8, 512) |
| 激活层 | Activation ='leakyrelu', alpha = 0.2 | (8, 8, 512) | (8, 8, 512) |
| 第六个 2D 卷积层 | filter= 512, kernel_size = 4, stride= 2, padding ="same" | (8, 8, 512) | (4, 4, 512) |
| 批量规范化层 | 没有 | (4, 4, 512) | (4, 4, 512) |
| 激活层 | Activation ='leakyrelu', alpha = 0.2 | (4, 4, 512) | (4, 4, 512) |
| 第七个 2D 卷积层 | filter= 512, kernel_size = 4, stride= 2, padding ="same" | (4, 4, 512) | (2, 2, 512) |
| 批量规范化层 | 没有 | (2, 2, 512) | (2, 2, 512) |
| 激活层 | Activation ='leakyrelu', alpha = 0.2 | (2, 2, 512) | (2, 2, 512) |
| 第八个 2D 卷积层 | filter= 512, kernel_size = 4, stride= 2, padding ="same" | (4, 4, 512) | (1, 1, 512) |
| 批量规范化层 | 没有 | (1, 1, 512) | (1, 1, 512) |
| 激活层 | Activation ='leakyrelu', alpha = 0.2 | (1, 1, 512) | (1, 1, 512) |
| 展开层 | 没有 | (1, 1, 512) | (512, ) |
| 密集层 | unit= 2, activation='softmax' | (1, 1, 512) | (2, ) |
下表突出显示了判别器网络的架构和配置。 展开层将张量展开为一维数组。
判别器网络中的其余层将在本章的“Keras pix2pix 实现”部分中介绍。
现在,我们已经探索了这两个网络的架构和配置。 现在,我们将探讨训练 pix2pix 所需的训练目标函数。
训练目标函数
Pix2pix 是一个条件生成对抗网络,并且用于条件 GAN 的目标函数可以表示为:
在这里,网络G(生成器)正试图使针对对手D(判别器)的先前函数最小化,而对手D则试图使先前的函数最大化。
如果必须比较朴素 GAN 和条件 GAN 的目标函数,则朴素 GAN 的目标函数如下:
为了减少图像的模糊,我们可以向目标函数添加一个 L1 损失函数。 L1 损失函数可以表示为:
在该等式中,y是原始图像, G(x, z)是生成器网络生成的图像。 L1 损失是由原始图像的所有像素值与生成的图像的所有像素值之间的所有绝对差值的总和来计算的。
pix2pix 的最终目标函数如下:
这是条件 GAN 的损失函数和 L1 损失函数的加权和。
现在我们对 pix2pix 网络有了基本的了解。 在开始在 Keras 中实现 pix2pix 之前,让我们设置项目。
设置项目
如果尚未使用所有章节的完整代码克隆存储库,请立即克隆存储库。 克隆的存储库有一个名为Chapter09的目录,其中包含本章的全部代码。 执行以下命令来设置项目:
- 首先,导航到父目录,如下所示:
cd Generative-Adversarial-Networks-Projects
- 现在,将目录从当前目录更改为
Chapter09:
cd Chapter09
- 接下来,为该项目创建一个 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 文件,以获取有关如何设置项目的更多说明。 开发人员经常会遇到依赖关系不匹配的问题。 为每个项目创建一个单独的虚拟环境将解决此问题。
在本节中,我们已成功设置项目并安装了所需的依赖项。 在下一部分中,我们将处理数据集。 现在,我们将探讨下载和格式化数据集所需的各个步骤。
准备数据
在本章中,我们将使用 Facades 数据集,该数据集可从以下链接获得。
该数据集包含立面标签和真实立面图像。 外观通常是建筑物的正面,外观标签是外观图像的建筑标签。 下载数据集后,我们将了解有关立面的更多信息。 执行以下命令以下载和提取数据集:
- 通过执行以下命令下载数据集:
# Before downloading the dataset navigate to data directory
cd data
# Download the dataset
wget http://efrosgans.eecs.berkeley.edu/pix2pix/datasets/facades.tar.gz
- 下载数据集后,使用以下命令提取数据集:
tar -xvzf facades.tar.gz
下载的数据集的文件结构如下:
数据集分为训练,测试和验证数据集。 让我们来提取图像。
执行以下步骤以加载数据集:
- 首先创建包含外观标签的
.h5文件列表和包含外观图像的.h5另一个列表,如下所示:
data_dir_path = os.path.join(data_dir, data_type)
# Get all .h5 files containing training images facade_photos_h5 = [f for f in os.listdir(os.path.join(data_dir_path, 'images')) if '.h5' in f]
facade_labels_h5 = [f for f in os.listdir(os.path.join(data_dir_path, 'facades')) if '.h5' in f]
- 接下来,遍历列表以依次加载每个图像:
final_facade_photos = None final_facade_labels = None for index in range(len(facade_photos_h5)):
此步骤之后的所有代码都将位于前面的for循环中。
- 接下来,加载包含图像的
h5文件,并检索实际图像的 Numpy N 维数组:
facade_photos_path = data_dir_path + '/images/' +
facade_photos_h5[index]
facade_labels_path = data_dir_path + '/facades/' +
facade_labels_h5[index]
facade_photos = h5py.File(facade_photos_path, 'r')
facade_labels = h5py.File(facade_labels_path, 'r')
- 接下来,将图像调整为所需的图像大小,如下所示:
# Resize and normalize images num_photos = facade_photos['data'].shape[0]
num_labels = facade_labels['data'].shape[0]
all_facades_photos = np.array(facade_photos['data'], dtype=np.float32)
all_facades_photos = all_facades_photos.reshape((num_photos, img_width, img_height, 1)) / 255.0 all_facades_labels = np.array(facade_labels['data'], dtype=np.float32)
all_facades_labels = all_facades_labels.reshape((num_labels, img_width, img_height, 1)) / 255.0
- 接下来,将调整大小的图像添加到最终的 N 维数组中:
if final_facade_photos is not None and final_facade_labels is not None:
final_facade_photos = np.concatenate([final_facade_photos,
all_facades_photos], axis=0)
final_facade_labels = np.concatenate([final_facade_labels, all_facades_labels], axis=0)
else:
final_facade_photos = all_facades_photos
final_facade_labels = all_facades_labels
加载和调整图像大小的整个代码如下所示:
def load_dataset(data_dir, data_type, img_width, img_height):
data_dir_path = os.path.join(data_dir, data_type)
# Get all .h5 files containing training images
facade_photos_h5 = [f for f in os.listdir(os.path.join(data_dir_path, 'images')) if '.h5' in f]
facade_labels_h5 = [f for f in os.listdir(os.path.join(data_dir_path, 'facades')) if '.h5' in f]
final_facade_photos = None
final_facade_labels = None for index in range(len(facade_photos_h5)):
facade_photos_path = data_dir_path + '/images/' + facade_photos_h5[index]
facade_labels_path = data_dir_path + '/facades/' + facade_labels_h5[index]
facade_photos = h5py.File(facade_photos_path, 'r')
facade_labels = h5py.File(facade_labels_path, 'r')
# Resize and normalize images
num_photos = facade_photos['data'].shape[0]
num_labels = facade_labels['data'].shape[0]
all_facades_photos = np.array(facade_photos['data'], dtype=np.float32)
all_facades_photos = all_facades_photos.reshape((num_photos, img_width, img_height, 1)) / 255.0 all_facades_labels = np.array(facade_labels['data'], dtype=np.float32)
all_facades_labels = all_facades_labels.reshape((num_labels, img_width, img_height, 1)) / 255.0 if final_facade_photos is not None and final_facade_labels is not None:
final_facade_photos = np.concatenate([final_facade_photos, all_facades_photos], axis=0)
final_facade_labels = np.concatenate([final_facade_labels, all_facades_labels], axis=0)
else:
final_facade_photos = all_facades_photos
final_facade_labels = all_facades_labels
return final_facade_photos, final_facade_labels
先前的函数将从训练,测试和验证目录中的.h5文件加载图像。
可视化图像
可视化外观标签和外观图像的 Python 函数如下所示:
def visualize_bw_image(img):
"""
Visualize a black and white image """ fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.imshow(img, cmap='gray', interpolation='nearest')
ax.axis("off")
ax.set_title("Image")
plt.show()
使用前面的函数可以使外观标签或外观照片可视化,如下所示:
visualize_bw_image(image)
visualize_bw_image(image)
建筑物外墙的图像示例如下:
下图表示前面的门面图像的建筑标签:
我们将训练一个 pix2pix 网络,该网络能够从立面标签生成立面图像。 让我们开始使用生成器和判别器的 Keras 实现。
pix2pix 的 Keras 实现
如前所述,pix2pix 具有两个网络:一个生成器和一个判别器。 该生成器受 U-Net 架构的启发。 同样,判别器网络也受到 PatchGAN 架构的启发。 我们将在以下部分中实现这两个网络。
在开始编写实现之前,创建一个 Python 文件 main.py ,然后按以下方式导入基本模块:
import os
import time
import h5py
import keras.backend as K
import matplotlib.pyplot as plt
import numpy as np
from cv2 import imwrite
from keras import Input, Model
from keras.layers import Convolution2D, LeakyReLU, BatchNormalization, UpSampling2D, Dropout, Activation, Flatten, Dense, Lambda, Reshape, concatenate
from keras.optimizers import Adam
生成器网络
生成器网络从源域 A 拍摄大小为(256, 256, 1)的图像,并将其转换为目标域 B 中大小为(256, 256, 1)的图像。 基本上,它将图像从源域 A 转换为目标域 B。让我们在 Keras 框架中实现生成器网络。
执行以下步骤来创建生成器网络:
- 首先定义生成器网络所需的超参数:
kernel_size = 4 strides = 2 leakyrelu_alpha = 0.2 upsampling_size = 2 dropout = 0.5 output_channels = 1 input_shape = (256, 256, 1)
- 现在创建一个输入层,将输入输入到网络,如下所示:
input_layer = Input(shape=input_shape)
输入层获取形状为(256, 256, 1)的输入图像,并将其传递到网络中的下一层。
如上所述,生成器网络具有两个部分:编码器和解码器。 在接下来的几个步骤中,我们将编写编码器部分的代码。
- 使用先前在“pix2pix”部分中指示的参数,将第一个卷积块添加到生成器网络:
# 1st Convolutional block in the encoder network encoder1 = Convolution2D(filters=64, kernel_size=kernel_size, padding='same', strides=strides)(input_layer)
encoder1 = LeakyReLU(alpha=leakyrelu_alpha)(encoder1)
第一卷积块包含具有激活函数的 2D 卷积层。 与其他七个卷积块不同,它没有批量规范化层。
- 将其他七个卷积块添加到生成器网络:
# 2nd Convolutional block in the encoder network encoder2 = Convolution2D(filters=128, kernel_size=kernel_size, padding='same',
strides=strides)(encoder1)
encoder2 = BatchNormalization()(encoder2)
encoder2 = LeakyReLU(alpha=leakyrelu_alpha)(encoder2)
# 3rd Convolutional block in the encoder network encoder3 = Convolution2D(filters=256, kernel_size=kernel_size, padding='same',
strides=strides)(encoder2)
encoder3 = BatchNormalization()(encoder3)
encoder3 = LeakyReLU(alpha=leakyrelu_alpha)(encoder3)
# 4th Convolutional block in the encoder network encoder4 = Convolution2D(filters=512, kernel_size=kernel_size, padding='same',
strides=strides)(encoder3)
encoder4 = BatchNormalization()(encoder4)
encoder4 = LeakyReLU(alpha=leakyrelu_alpha)(encoder4)
# 5th Convolutional block in the encoder network encoder5 = Convolution2D(filters=512, kernel_size=kernel_size, padding='same',
strides=strides)(encoder4)
encoder5 = BatchNormalization()(encoder5)
encoder5 = LeakyReLU(alpha=leakyrelu_alpha)(encoder5)
# 6th Convolutional block in the encoder network encoder6 = Convolution2D(filters=512, kernel_size=kernel_size, padding='same',
strides=strides)(encoder5)
encoder6 = BatchNormalization()(encoder6)
encoder6 = LeakyReLU(alpha=leakyrelu_alpha)(encoder6)
# 7th Convolutional block in the encoder network encoder7 = Convolution2D(filters=512, kernel_size=kernel_size, padding='same',
strides=strides)(encoder6)
encoder7 = BatchNormalization()(encoder7)
encoder7 = LeakyReLU(alpha=leakyrelu_alpha)(encoder7)
# 8th Convolutional block in the encoder network encoder8 = Convolution2D(filters=512, kernel_size=kernel_size, padding='same',
strides=strides)(encoder7)
encoder8 = BatchNormalization()(encoder8)
encoder8 = LeakyReLU(alpha=leakyrelu_alpha)(encoder8)
这是生成器网络中编码器部分的末端。 生成器网络中的第二部分是解码器。 在接下来的几个步骤中,让我们为解码器编写代码。
- 将第一个上采样卷积块添加到先前在“pix2pix”部分的架构中指示的参数中:
# 1st Upsampling Convolutional Block in the decoder network decoder1 = UpSampling2D(size=upsampling_size)(encoder8)
decoder1 = Convolution2D(filters=512, kernel_size=kernel_size, padding='same')(decoder1)
decoder1 = BatchNormalization()(decoder1)
decoder1 = Dropout(dropout)(decoder1)
decoder1 = concatenate([decoder1, encoder7], axis=3)
decoder1 = Activation('relu')(decoder1)
第一个上采样模块从编码器部分的最后一层获取输入。 它具有一个 2D 上采样层,一个 2D 卷积层,一个批量归一化层,一个脱落层,一个连接操作和一个激活函数。 请参阅 Keras 文档以找到有关这些层的更多信息,该文档可从这里获得。
- 同样,添加以下七个卷积块,如下所示:
# 2nd Upsampling Convolutional block in the decoder network decoder2 = UpSampling2D(size=upsampling_size)(decoder1)
decoder2 = Convolution2D(filters=1024, kernel_size=kernel_size, padding='same')(decoder2)
decoder2 = BatchNormalization()(decoder2)
decoder2 = Dropout(dropout)(decoder2)
decoder2 = concatenate([decoder2, encoder6])
decoder2 = Activation('relu')(decoder2)
# 3rd Upsampling Convolutional block in the decoder network decoder3 = UpSampling2D(size=upsampling_size)(decoder2)
decoder3 = Convolution2D(filters=1024, kernel_size=kernel_size, padding='same')(decoder3)
decoder3 = BatchNormalization()(decoder3)
decoder3 = Dropout(dropout)(decoder3)
decoder3 = concatenate([decoder3, encoder5])
decoder3 = Activation('relu')(decoder3)
# 4th Upsampling Convolutional block in the decoder network decoder4 = UpSampling2D(size=upsampling_size)(decoder3)
decoder4 = Convolution2D(filters=1024, kernel_size=kernel_size, padding='same')(decoder4)
decoder4 = BatchNormalization()(decoder4)
decoder4 = concatenate([decoder4, encoder4])
decoder4 = Activation('relu')(decoder4)
# 5th Upsampling Convolutional block in the decoder network decoder5 = UpSampling2D(size=upsampling_size)(decoder4)
decoder5 = Convolution2D(filters=1024, kernel_size=kernel_size, padding='same')(decoder5)
decoder5 = BatchNormalization()(decoder5)
decoder5 = concatenate([decoder5, encoder3])
decoder5 = Activation('relu')(decoder5)
# 6th Upsampling Convolutional block in the decoder network decoder6 = UpSampling2D(size=upsampling_size)(decoder5)
decoder6 = Convolution2D(filters=512, kernel_size=kernel_size, padding='same')(decoder6)
decoder6 = BatchNormalization()(decoder6)
decoder6 = concatenate([decoder6, encoder2])
decoder6 = Activation('relu')(decoder6)
# 7th Upsampling Convolutional block in the decoder network decoder7 = UpSampling2D(size=upsampling_size)(decoder6)
decoder7 = Convolution2D(filters=256, kernel_size=kernel_size, padding='same')(decoder7)
decoder7 = BatchNormalization()(decoder7)
decoder7 = concatenate([decoder7, encoder1])
decoder7 = Activation('relu')(decoder7)
# Last Convolutional layer decoder8 = UpSampling2D(size=upsampling_size)(decoder7)
decoder8 = Convolution2D(filters=output_channels, kernel_size=kernel_size, padding='same')(decoder8)
decoder8 = Activation('tanh')(decoder8)
最后一层的激活函数为'tanh',因为我们打算让生成器生成介于 -1 到 1 之间的值。'concatenate'层用于添加跳跃连接。 最后一层将生成张量为(256, 256, 1)的张量。
The 'concatenate' layer concatenates tensors along the channel dimension. You can provide a value for the axis, along which you want your tensors to be concatenated.
- 最后,通过指定生成器网络的输入和输出来创建 Keras 模型:
# Create a Keras model
model = Model(inputs=[input_layer], outputs=[decoder8])
Python 函数内部的生成器网络的整个代码如下所示:
def build_unet_generator():
"""
Create U-Net Generator using the hyperparameter values defined
below """ kernel_size = 4
strides = 2
leakyrelu_alpha = 0.2
upsampling_size = 2
dropout = 0.5
output_channels = 1
input_shape = (256, 256, 1)
input_layer = Input(shape=input_shape)
# Encoder Network # 1st Convolutional block in the encoder network encoder1 = Convolution2D(filters=64, kernel_size=kernel_size,
padding='same',
strides=strides)(input_layer)
encoder1 = LeakyReLU(alpha=leakyrelu_alpha)(encoder1)
# 2nd Convolutional block in the encoder network
encoder2 = Convolution2D(filters=128, kernel_size=kernel_size,
padding='same',
strides=strides)(encoder1)
encoder2 = BatchNormalization()(encoder2)
encoder2 = LeakyReLU(alpha=leakyrelu_alpha)(encoder2)
# 3rd Convolutional block in the encoder network
encoder3 = Convolution2D(filters=256, kernel_size=kernel_size,
padding='same',
strides=strides)(encoder2)
encoder3 = BatchNormalization()(encoder3)
encoder3 = LeakyReLU(alpha=leakyrelu_alpha)(encoder3)
# 4th Convolutional block in the encoder network
encoder4 = Convolution2D(filters=512, kernel_size=kernel_size,
padding='same',
strides=strides)(encoder3)
encoder4 = BatchNormalization()(encoder4)
encoder4 = LeakyReLU(alpha=leakyrelu_alpha)(encoder4)
# 5th Convolutional block in the encoder network
encoder5 = Convolution2D(filters=512, kernel_size=kernel_size,
padding='same',
strides=strides)(encoder4)
encoder5 = BatchNormalization()(encoder5)
encoder5 = LeakyReLU(alpha=leakyrelu_alpha)(encoder5)
# 6th Convolutional block in the encoder network
encoder6 = Convolution2D(filters=512, kernel_size=kernel_size,
padding='same',
strides=strides)(encoder5)
encoder6 = BatchNormalization()(encoder6)
encoder6 = LeakyReLU(alpha=leakyrelu_alpha)(encoder6)
# 7th Convolutional block in the encoder network
encoder7 = Convolution2D(filters=512, kernel_size=kernel_size,
padding='same',
strides=strides)(encoder6)
encoder7 = BatchNormalization()(encoder7)
encoder7 = LeakyReLU(alpha=leakyrelu_alpha)(encoder7)
# 8th Convolutional block in the encoder network
encoder8 = Convolution2D(filters=512, kernel_size=kernel_size,
padding='same',
strides=strides)(encoder7)
encoder8 = BatchNormalization()(encoder8)
encoder8 = LeakyReLU(alpha=leakyrelu_alpha)(encoder8)
# Decoder Network # 1st Upsampling Convolutional Block in the decoder network decoder1 = UpSampling2D(size=upsampling_size)(encoder8)
decoder1 = Convolution2D(filters=512, kernel_size=kernel_size,
padding='same')(decoder1)
decoder1 = BatchNormalization()(decoder1)
decoder1 = Dropout(dropout)(decoder1)
decoder1 = concatenate([decoder1, encoder7], axis=3)
decoder1 = Activation('relu')(decoder1)
# 2nd Upsampling Convolutional block in the decoder network
decoder2 = UpSampling2D(size=upsampling_size)(decoder1)
decoder2 = Convolution2D(filters=1024, kernel_size=kernel_size,
padding='same')(decoder2)
decoder2 = BatchNormalization()(decoder2)
decoder2 = Dropout(dropout)(decoder2)
decoder2 = concatenate([decoder2, encoder6])
decoder2 = Activation('relu')(decoder2)
# 3rd Upsampling Convolutional block in the decoder network
decoder3 = UpSampling2D(size=upsampling_size)(decoder2)
decoder3 = Convolution2D(filters=1024, kernel_size=kernel_size,
padding='same')(decoder3)
decoder3 = BatchNormalization()(decoder3)
decoder3 = Dropout(dropout)(decoder3)
decoder3 = concatenate([decoder3, encoder5])
decoder3 = Activation('relu')(decoder3)
# 4th Upsampling Convolutional block in the decoder network
decoder4 = UpSampling2D(size=upsampling_size)(decoder3)
decoder4 = Convolution2D(filters=1024, kernel_size=kernel_size,
padding='same')(decoder4)
decoder4 = BatchNormalization()(decoder4)
decoder4 = concatenate([decoder4, encoder4])
decoder4 = Activation('relu')(decoder4)
# 5th Upsampling Convolutional block in the decoder network
decoder5 = UpSampling2D(size=upsampling_size)(decoder4)
decoder5 = Convolution2D(filters=1024, kernel_size=kernel_size,
padding='same')(decoder5)
decoder5 = BatchNormalization()(decoder5)
decoder5 = concatenate([decoder5, encoder3])
decoder5 = Activation('relu')(decoder5)
# 6th Upsampling Convolutional block in the decoder network
decoder6 = UpSampling2D(size=upsampling_size)(decoder5)
decoder6 = Convolution2D(filters=512, kernel_size=kernel_size,
padding='same')(decoder6)
decoder6 = BatchNormalization()(decoder6)
decoder6 = concatenate([decoder6, encoder2])
decoder6 = Activation('relu')(decoder6)
# 7th Upsampling Convolutional block in the decoder network
decoder7 = UpSampling2D(size=upsampling_size)(decoder6)
decoder7 = Convolution2D(filters=256, kernel_size=kernel_size,
padding='same')(decoder7)
decoder7 = BatchNormalization()(decoder7)
decoder7 = concatenate([decoder7, encoder1])
decoder7 = Activation('relu')(decoder7)
# Last Convolutional layer
decoder8 = UpSampling2D(size=upsampling_size)(decoder7)
decoder8 = Convolution2D(filters=output_channels,
kernel_size=kernel_size, padding='same')(decoder8)
decoder8 = Activation('tanh')(decoder8)
model = Model(inputs=[input_layer], outputs=[decoder8])
return model
我们现在已经成功地为生成器网络创建了 Keras 模型。 在下一节中,我们将为判别器网络创建 Keras 模型。
判别器网络
判别器网络受 PatchGAN 架构的启发。 它包含八个卷积块,一个密集层和一个展开层。 判别器网络获取从大小为(256, 256, 1)的图像中提取的一组补丁,并预测给定补丁的概率。 让我们在 Keras 中实现判别器。
- 首先初始化生成器网络所需的超参数:
kernel_size = 4 strides = 2 leakyrelu_alpha = 0.2 padding = 'same' num_filters_start = 64 # Number of filters to start with num_kernels = 100 kernel_dim = 5 patchgan_output_dim = (256, 256, 1)
patchgan_patch_dim = (256, 256, 1)
# Calculate number of patches
number_patches = int((patchgan_output_dim[0] / patchgan_patch_dim[0]) * (patchgan_output_dim[1] / patchgan_patch_dim[1]))
- 让我们向网络添加一个输入层。 这需要一个张量为
patchgan_patch_dim的张量的补丁:
input_layer = Input(shape=patchgan_patch_dim)
- 接下来,如下所示将卷积层添加到网络。 “pix2pix 的架构”部分中提供了该块的配置:
des = Convolution2D(filters=64, kernel_size=kernel_size, padding=padding, strides=strides)(input_layer)
des = LeakyReLU(alpha=leakyrelu_alpha)(des)
- 接下来,使用以下代码计算卷积块的数量:
# Calculate the number of convolutional layers total_conv_layers = int(np.floor(np.log(patchgan_output_dim[1]) /
np.log(2)))
list_filters = [num_filters_start * min(total_conv_layers, (2 ** i)) for i in range(total_conv_layers)]
- 接下来,使用先前在“pix2pix”部分的架构中指示的超参数值添加另外七个卷积块:
# Next 7 Convolutional blocks for filters in list_filters[1:]:
des = Convolution2D(filters=filters, kernel_size=kernel_size, padding=padding, strides=strides)(des)
des = BatchNormalization()(des)
des = LeakyReLU(alpha=leakyrelu_alpha)(des)
- 接下来,将展开层添加到网络,如下所示:
flatten_layer = Flatten()(des)
展开层将n维张量转换为一维张量。
- 类似地,添加具有两个节点/神经元的密集层,并添加
softmax作为激活函数。 这需要一个来自Flatten层的张量,并将其转换为大小为(batch_size, 2)的张量:
dense_layer = Dense(units=2, activation='softmax')(flatten_layer)
softmax函数将向量转换为概率分布。
- 接下来,为 PatchGAN 网络创建 Keras 模型,如下所示:
model_patch_gan = Model(inputs=[input_layer], outputs=[dense_layer, flatten_layer])
PatchGAN 模型将输入张量作为输入,并输出两个张量,一个来自密集层,另一个来自展开层。 我们的 PatchGAN 网络现已准备就绪。 但是,它本身不能用作判别器。 而是将单个补丁分类为真实或伪造类别。 要创建完整的判别器,请按照下列步骤操作:
- 我们将从输入图像中提取色块,并将它们逐一馈送到 PatchGAN。 创建一个等于补丁数量的输入层列表,如下所示:
# Create a list of input layers equal to number of patches list_input_layers = [Input(shape=patchgan_patch_dim) for _ in range(number_patches)]
- 接下来,将补丁传递到 PatchGAN 网络并获得概率分布:
# Pass the patches to the PatchGAN and get probability distribution output1 = [model_patch_gan(patch)[0] for patch in list_input_layers]
output2 = [model_patch_gan(patch)[1] for patch in list_input_layers]
如果我们有多个面片,则output1和output2都是张量的列表。 我们现在应该有两个张量列表。
- 如果您有多个补丁,请沿着渠道维度将其连接起来以计算永久损失:
# In case of multiple patches, concatenate them along the channel dimension to calculate perceptual loss if len(output1) > 1:
output1 = concatenate(output1)
else:
output1 = output1[0]
# In case of multiple patches, merge output2 as well if len(output2) > 1:
output2 = concatenate(output2)
else:
output2 = output2[0]
- 接下来,创建一个密集层,如下所示:
dense_layer2 = Dense(num_kernels * kernel_dim, use_bias=False, activation=None)
- 接下来,添加一个自定义损失层。 该层计算馈入该层的张量的最小批判别:
custom_loss_layer = Lambda(lambda x: K.sum(
K.exp(-K.sum(K.abs(K.expand_dims(x, 3) - K.expand_dims(K.permute_dimensions(x, pattern=(1, 2, 0)), 0)), 2)), 2))
- 接下来,使
output2张量通过dense_layer2:
output2 = dense_layer2(output2)
- 接下来,将
output2重塑为(num_kernels, kernel_dim)的张量:
output2 = Reshape((num_kernels, kernel_dim))(output2)
- 接下来,将
output2张量传递到custom_loss_layer:
output2 = custom_loss_layer(output2)
- 接下来,连接
output1和output2以创建一个张量,并将其通过密集层:
output1 = concatenate([output1, output2])
final_output = Dense(2, activation="softmax")(output1)
将"softmax"用作最后一个密集层的激活函数。 这将返回概率分布。
- 最后,通过如下指定网络的输入和输出来创建判别器模型:
discriminator = Model(inputs=list_input_layers, outputs=[final_output])
判别器网络的完整代码如下:
def build_patchgan_discriminator():
"""
Create PatchGAN discriminator using the hyperparameter values defined below """ kernel_size = 4
strides = 2
leakyrelu_alpha = 0.2
padding = 'same'
num_filters_start = 64 # Number of filters to start with
num_kernels = 100
kernel_dim = 5
patchgan_output_dim = (256, 256, 1)
patchgan_patch_dim = (256, 256, 1)
number_patches = int(
(patchgan_output_dim[0] / patchgan_patch_dim[0]) * (patchgan_output_dim[1] / patchgan_patch_dim[1]))
input_layer = Input(shape=patchgan_patch_dim)
des = Convolution2D(filters=64, kernel_size=kernel_size, padding=padding, strides=strides)(input_layer)
des = LeakyReLU(alpha=leakyrelu_alpha)(des)
# Calculate the number of convolutional layers
total_conv_layers = int(np.floor(np.log(patchgan_output_dim[1]) / np.log(2)))
list_filters = [num_filters_start * min(total_conv_layers, (2 ** i)) for i in range(total_conv_layers)]
# Next 7 Convolutional blocks
for filters in list_filters[1:]:
des = Convolution2D(filters=filters, kernel_size=kernel_size, padding=padding, strides=strides)(des)
des = BatchNormalization()(des)
des = LeakyReLU(alpha=leakyrelu_alpha)(des)
# Add a flatten layer
flatten_layer = Flatten()(des)
# Add the final dense layer
dense_layer = Dense(units=2, activation='softmax')(flatten_layer)
# Create the PatchGAN model
model_patch_gan = Model(inputs=[input_layer], outputs=[dense_layer, flatten_layer])
# Create a list of input layers equal to the number of patches
list_input_layers = [Input(shape=patchgan_patch_dim) for _ in range(number_patches)]
# Pass the patches through the PatchGAN network
output1 = [model_patch_gan(patch)[0] for patch in list_input_layers]
output2 = [model_patch_gan(patch)[1] for patch in list_input_layers]
# In case of multiple patches, concatenate outputs to calculate perceptual loss
if len(output1) > 1:
output1 = concatenate(output1)
else:
output1 = output1[0]
# In case of multiple patches, merge output2 as well
if len(output2) > 1:
output2 = concatenate(output2)
else:
output2 = output2[0]
# Add a dense layer
dense_layer2 = Dense(num_kernels * kernel_dim, use_bias=False, activation=None)
# Add a lambda layer
custom_loss_layer = Lambda(lambda x: K.sum(
K.exp(-K.sum(K.abs(K.expand_dims(x, 3) - K.expand_dims(K.permute_dimensions(x, pattern=(1, 2, 0)), 0)), 2)), 2))
# Pass the output2 tensor through dense_layer2
output2 = dense_layer2(output2)
# Reshape the output2 tensor
output2 = Reshape((num_kernels, kernel_dim))(output2)
# Pass the output2 tensor through the custom_loss_layer
output2 = custom_loss_layer(output2)
# Finally concatenate output1 and output2
output1 = concatenate([output1, output2])
final_output = Dense(2, activation="softmax")(output1)
# Create a discriminator model
discriminator = Model(inputs=list_input_layers, outputs=[final_output])
return discriminator
我们现在已经成功创建了判别器网络。 接下来,让我们创建一个对抗网络。
对抗网络
在本节中,我们将创建一个对抗网络,其中包含 U-Net 生成器网络和 PatchGAN 判别器网络。 执行以下步骤来创建对抗网络:
- 首先初始化超参数:
input_image_dim = (256, 256, 1)
patch_dim = (256, 256)
- 接下来,创建一个输入层,将输入馈送到网络,如下所示:
input_layer = Input(shape=input_image_dim)
- 接下来,使用生成器网络生成伪造的图像:
generated_images = generator(input_layer)
- 接下来,从生成的图像中提取补丁:
# Chop the generated images into patches img_height, img_width = input_img_dim[:2]
patch_height, patch_width = patch_dim
row_idx_list = [(i * patch_height, (i + 1) * patch_height) for i in range(int(img_height / patch_height))]
column_idx_list = [(i * patch_width, (i + 1) * patch_width) for i in range(int(img_width / patch_width))]
generated_patches_list = []
for row_idx in row_idx_list:
for column_idx in column_idx_list:
generated_patches_list.append(Lambda(lambda z: z[:, column_idx[0]:column_idx[1], row_idx[0]:row_idx[1], :],
output_shape=input_img_dim)(generated_images))
- 冻结了判别器网络的训练,因为我们不想训练判别器网络:
discriminator.trainable = False
- 现在,我们应该有一个补丁列表。 通过 PatchGAN 判别器网络传递它们:
dis_output = discriminator(generated_patches_list)
- 最后,通过如下指定网络的输入和输出来创建 Keras 模型:
model = Model(inputs=[input_layer], outputs=[generated_images,
dis_output])
这些步骤使用两个网络(生成器网络和判别器网络)创建对抗模型。 对抗模型的整个代码如下:
def build_adversarial_model(generator, discriminator):
"""
Create an adversarial model """ input_image_dim = (256, 256, 1)
patch_dim = (256, 256)
# Create an input layer
input_layer = Input(shape=input_image_dim)
# Use the generator network to generate images
generated_images = generator(input_layer)
# Extract patches from the generated images
img_height, img_width = input_img_dim[:2]
patch_height, patch_width = patch_dim
row_idx_list = [(i * patch_height, (i + 1) * patch_height) for i in range(int(img_height / patch_height))]
column_idx_list = [(i * patch_width, (i + 1) * patch_width) for i in range(int(img_width / patch_width))]
generated_patches_list = []
for row_idx in row_idx_list:
for column_idx in column_idx_list:
generated_patches_list.append(Lambda(lambda z: z[:, column_idx[0]:column_idx[1], row_idx[0]:row_idx[1], :],
output_shape=input_img_dim)(generated_images))
discriminator.trainable = False # Pass the generated patches through the discriminator network
dis_output = discriminator(generated_patches_list)
# Create a model
model = Model(inputs=[input_layer], outputs=[generated_images, dis_output])
return model
现在,我们已经成功地为生成器网络,判别器网络和对抗模型创建了模型。 我们准备训练 pix2pix。 在下一部分中,我们将在 Facades 数据集中训练 pix2pix 网络。
训练 pix2pix 网络
像任何其他 GAN 一样,训练 pix2pix 网络是一个两步过程。 第一步,我们训练判别器网络。 在第二步中,我们训练对抗网络,最终训练生成器网络。 让我们开始训练网络。
执行以下步骤来训练 SRGAN 网络:
- 首先定义训练所需的超参数:
epochs = 500 num_images_per_epoch = 400 batch_size = 1 img_width = 256 img_height = 256 num_channels = 1 input_img_dim = (256, 256, 1)
patch_dim = (256, 256)
# Specify dataset directory path
dataset_dir = "pix2pix-keras/pix2pix/data/facades_bw"
- 接下来,定义通用优化器,如下所示:
common_optimizer = Adam(lr=1E-4, beta_1=0.9, beta_2=0.999,
epsilon=1e-08)
对于所有网络,我们将使用Adam优化器,其中learning rate等于1e-4, beta_1 等于 0.9, beta_2等于 0.999,并且epsilon等于1e-08。
- 接下来,构建并编译 PatchGAN 判别器网络,如下所示:
patchgan_discriminator = build_patchgan_discriminator()
patchgan_discriminator.compile(loss='binary_crossentropy', optimizer=common_optimizer)
要编译判别器模型,请使用binary_crossentropy作为损失函数,并使用common_optimizer作为训练优化器。
- 现在,构建并编译生成器网络,如下所示:
unet_generator = build_unet_generator()
unet_generator.compile(loss='mae', optimizer=common_optimizer)
要编译判别器模型,请使用 mse 作为损失函数,并使用 common_optimizer 作为训练优化器。
- 接下来,构建并编译对抗模型,如下所示:
adversarial_model = build_adversarial_model(unet_generator, patchgan_discriminator)
adversarial_model.compile(loss=['mae', 'binary_crossentropy'], loss_weights=[1E2, 1], optimizer=common_optimizer)
要编译对抗模型,请使用损失列表 ['mse', 'binary_crossentropy'] 和 common_optimizer 作为训练优化器。
- 现在,按如下所示加载训练,验证和测试数据集:
training_facade_photos, training_facade_labels = load_dataset(data_dir=dataset_dir, data_type='training',img_width=img_width, img_height=img_height)
test_facade_photos, test_facade_labels = load_dataset(data_dir=dataset_dir, data_type='testing',img_width=img_width, img_height=img_height)
validation_facade_photos, validation_facade_labels = load_dataset(data_dir=dataset_dir, data_type='validation',img_width=img_width, img_height=img_height)
load_dataset函数是“数据准备”部分中定义的 。 每一组包含所有图像的一组ndarray。 每组的大小将为(#total_images, 256, 256, 1)。
- 添加
tensorboard以可视化训练损失并可视化网络图:
tensorboard = TensorBoard(log_dir="logs/".format(time.time()))
tensorboard.set_model(unet_generator)
tensorboard.set_model(patchgan_discriminator)
- 接下来,创建一个
for循环,该循环应运行的次数由周期数指定,如下所示:
for epoch in range(epochs):
print("Epoch:{}".format(epoch))
- 创建两个列表来存储所有小批量的损失:
dis_losses = []
gen_losses = []
# Initialize a variable
batch_counter = 1
- 接下来,在周期循环内创建另一个循环,并使它运行
num_batches指定的次数,如下所示:
num_batches = int(training_facade_photos.shape[0] / batch_size) for index in range(int(training_facade_photos.shape[0] / batch_size)):
print("Batch:{}".format(index))
我们用于判别器网络和对抗网络训练的整个代码将在此循环内。
- 接下来,对训练和验证数据进行小批量采样,如下所示:
train_facades_batch = training_facade_labels[index * batch_size:(index + 1) * batch_size]
train_images_batch = training_facade_photos[index * batch_size:(index + 1) * batch_size]
val_facades_batch = validation_facade_labels[index * batch_size:(index + 1) * batch_size]
val_images_batch = validation_facade_photos[index * batch_size:(index + 1) * batch_size]
- 接下来,生成一批假图像并从中提取补丁。 如下使用
generate_and_extract_patches函数:
patches, labels = generate_and_extract_patches(train_images_batch, train_facades_batch, unet_generator,batch_counter, patch_dim)
generate_and_extract_patches函数定义如下:
def generate_and_extract_patches(images, facades, generator_model, batch_counter, patch_dim):
# Alternatively, train the discriminator network on real and generated images
if batch_counter % 2 == 0:
# Generate fake images
output_images = generator_model.predict(facades)
# Create a batch of ground truth labels
labels = np.zeros((output_images.shape[0], 2), dtype=np.uint8)
labels[:, 0] = 1 else:
# Take real images
output_images = images
# Create a batch of ground truth labels
labels = np.zeros((output_images.shape[0], 2), dtype=np.uint8)
labels[:, 1] = 1 patches = []
for y in range(0, output_images.shape[0], patch_dim[0]):
for x in range(0, output_images.shape[1], patch_dim[1]):
image_patches = output_images[:, y: y + patch_dim[0], x: x + patch_dim[1], :]
patches.append(np.asarray(image_patches, dtype=np.float32))
return patches, labels
前面的函数使用生成器网络生成伪图像,然后从生成的图像中提取补丁。 现在,我们应该有一个补丁列表及其基本真理值。
- 现在,在生成的补丁上训练判别器网络:
d_loss = patchgan_discriminator.train_on_batch(patches, labels)
这将在提取的补丁和地面真相标签上训练判别器网络。
- 接下来,训练对抗模型。 对抗性模型将训练生成器网络,但冻结判别器网络的训练。 使用以下代码:
labels = np.zeros((train_images_batch.shape[0], 2), dtype=np.uint8)
labels[:, 1] = 1 # Train the adversarial model g_loss = adversarial_model.train_on_batch(train_facades_batch, [train_images_batch, labels])
- 每个小批量完成后增加批计数器:
batch_counter += 1
- 在每个微型批量上完成一次迭代(循环)后,将损失存储在名为
dis_losses和gen_losses的列表中:
dis_losses.append(d_loss)
gen_losses.append(g_loss)
- 另外,将平均损失存储到 TensorBoard 以进行可视化。 既存储损失,也要存储生成器网络的平均损失和判别器网络的平均损失:
write_log(tensorboard, 'discriminator_loss', np.mean(dis_losses),
epoch)
write_log(tensorboard, 'generator_loss', np.mean(gen_losses), epoch)
- 每 10 个周期后,使用生成器网络生成一组图像:
# After every 10th epoch, generate and save images for visualization if epoch % 10 == 0:
# Sample a batch of validation datasets
val_facades_batch = validation_facade_labels[0:5]
val_images_batch = validation_facade_photos[0:5]
# Generate images
validation_generated_images = unet_generator.predict(val_facades_batch)
# Save images
save_images(val_images_batch, val_facades_batch, validation_generated_images, epoch, 'validation', limit=5)
将前面的代码块放入周期循环中。 每隔 10 个时间段,它将生成一批伪图像并将其保存到结果目录。 这里,save_images()是如下定义的效用函数:
def save_images(real_images, real_sketches, generated_images, num_epoch, dataset_name, limit):
real_sketches = real_sketches * 255.0
real_images = real_images * 255.0
generated_images = generated_images * 255.0 # Save some images only
real_sketches = real_sketches[:limit]
generated_images = generated_images[:limit]
real_images = real_images[:limit]
# Create a stack of images
X = np.hstack((real_sketches, generated_images, real_images))
# Save stack of images
imwrite('results/X_full_{}_{}.png'.format(dataset_name, num_epoch), X[0])
现在,我们已经成功地在立面数据集上训练了 pix2pix 网络。 对网络进行 1000 个周期的训练,以获取高质量的生成器网络。
保存模型
在 Keras 中保存模型只需要一行代码。 要保存生成器模型,请添加以下行:
# Specify the path for the generator model
unet_generator.save_weights("generator.h5")
同样,通过添加以下行来保存判别器模型:
# Specify the path for the discriminator model
patchgan_discriminator.save_weights("discriminator.h5")
可视化生成的图像
在将网络训练了 20 个时间段后,网络将开始生成体面的图像:让我们看一下由生成器网络生成的图像。
在 20、50、150 和 200 个周期(从左到右) 之后,图像如下所示:
每个块均包含垂直堆叠的外观标签,生成的照片和实际图像。 我建议您将网络训练 1000 个周期。 如果一切顺利,则在 1000 个周期之后,生成器网络将开始生成逼真的图像。
可视化损失
要可视化训练损失,请启动 TensorBoard 服务器,如下所示:
tensorboard --logdir=logs
现在,在浏览器中打开localhost:6006。 TensorBoard 的标量部分包含两种损失的图表,如以下屏幕截图所示:
TensorBoard 的标量部分
这些图将帮助您决定是继续还是停止训练。 如果损失不再减少,您就可以停止训练,因为没有改善的机会。 如果损失持续增加,则必须停止训练。 尝试使用超参数,然后选择一组您认为可以提供更好结果的超参数。 如果损失逐渐减少,请继续训练模型。
可视化图
TensorBoard 的GRAPHS部分包含两个网络的图。 如果网络表现不佳,这些图可以帮助您调试网络。 它们还显示了每个图中的张量流和不同的操作:
张量流和每个图内部的不同操作
pix2pix 网络的实际应用
pix2pix 网络有很多应用。 其中包括:
- 将像素级分割转换为真实图像
- 要将白天图像转换为夜间图像,反之亦然
- 将卫星平面图像转换为地图图像
- 将草图转换为照片
- 将黑白图像转换为彩色图像,反之亦然
以下是从指定的官方文件中拍摄的图像。 它显示了 pix2pix 网络的不同用例:
使用条件对抗网络的图像到图像翻译,来源:arXiv:1611.07004 [cs.CV]
总结
在本章中,我们了解了 pix2pix 网络是什么,并探讨了其架构。 我们从数据加载开始,准备要训练的数据集,然后准备了项目,并研究了 pix2pix 网络的 Keras 实现。 之后,我们研究了训练 pix2pix 网络的目标函数。 然后,我们在立面数据集上训练了 pix2pix 网络,并探索了 pix2pix 网络的一些实际应用。
在下一章中,我们将预测 GAN 的未来。 我们将研究 GAN 领域在不久的将来会发生什么,以及它将如何改变我们的行业和我们的日常生活。