TensorFlow-2-和-Keras-高级深度学习-全-
TensorFlow 2 和 Keras 高级深度学习(全)
零、前言
近年来,深度学习在视觉,语音,自然语言处理和理解以及所有其他领域的大量数据难题中取得了前所未有的成功案例。 公司,大学,政府和研究组织对该领域的兴趣加速了该领域的发展。 本书通过三个新的章节介绍了深度学习中的重要主题:“对象检测”,“语义分割”和“使用互信息的无监督学习”。 通过提供原理的背景知识,挖掘概念背后的直觉,使用 Keras 实现方程式和算法以及检查结果来解释高级理论。
人工智能(AI)到今天为止还远远不是一个易于理解的领域。 深度学习(DL)作为 AI 的子字段,处于相同位置。 尽管它还不是一个成熟的领域,但许多现实世界的应用,例如基于视觉的检测和识别,自主导航,产品推荐,语音识别和合成,节能,药物发现,财务和营销,已经在使用 DL 算法。 。 将发现并构建更多应用。 本书的目的是解释高级概念,提供示例实现,并让作为其领域专家的读者识别目标应用。
尚未完全成熟的领域是一把双刃剑。 一方面,它为发现和利用提供了很多机会。 深度学习中有许多未解决的问题。 这就意味着可以抢先进入市场的机会–无论是在产品开发,发布还是认可方面。 另一个优势是,在关键任务环境中很难信任一个尚未被完全理解的领域。 我们可以肯定地说,如果被问到,很少有机器学习工程师会乘坐由深度学习系统控制的自动驾驶飞机。 要获得这种信任级别,需要做很多工作。 本书中讨论的高级概念很有可能在获得这种信任级别中扮演重要角色。
没有 DL 书能够完全涵盖整个领域。 这本书也不例外。 给定时间和空间,我们可能会涉及到有趣的领域,例如自然语言处理和理解,语音合成,自动机器学习(AutoML),图神经网络(GNN),贝叶斯深度学习等等。 但是,本书相信选择和解释选定的区域,以便读者可以从事其他未涵盖的领域。
作为即将着手阅读本书的读者,请记住,您选择的是一个令人兴奋的领域,会对社会产生巨大影响。 我们很幸运能有一份工作,希望我们在早晨醒来时继续努力。
这本书是给谁的
本书面向希望更好地了解深度学习高级主题的机器学习工程师和学生。 每个讨论都通过 Keras 中的代码实现进行了补充。 特别是,使用的是 TensorFlow 2 的 Keras API 或简称为tf.keras。这本书适合希望了解如何将理论转化为 Keras 中的工作代码实现的读者。 除了理解理论外,代码实现通常是将机器学习应用于实际问题的艰巨任务之一。
本书涵盖的内容
“第 1 章”,“Keras 高级深度学习入门”涵盖了深度学习的关键概念,例如优化,正则化,损失函数,基本层和网络及其在tf.keras中的实现 。 本章回顾了使用顺序 API 的深度学习和tf.keras。
“第 2 章”,“深度神经网络”讨论了tf.keras的函数式 API。 使用函数式 API 在tf.keras中检查并实现了两种广泛使用的深度网络架构 ResNet 和 DenseNet。
“第 3 章”,“自编码器”涵盖了一种称为自编码器的通用网络结构,该结构用于发现输入数据的潜在表示形式。 tf.keras中讨论并实现了自编码器的两个示例应用,即降噪和着色。
“第 4 章”,“生成对抗网络(GANs)”讨论了深度学习的最新重大进展之一。 GAN 用于生成看起来真实的新综合数据。 本章介绍 GAN 的原理。 在tf.keras中检查并实现了 GAN 的两个示例 DCGAN 和 CGAN。
“第 5 章”,“改进的 GAN” 涵盖了改进基本 GAN 的算法。 该算法解决了训练 GAN 的困难,并提高了合成数据的感知质量。 在tf.keras中讨论并实现了 WGAN,LSGAN 和 ACGAN。
“第 6 章”,“纠缠表示 GAN” 讨论了如何控制 GAN 生成的合成数据的属性。 如果潜在表示被解开,则可以控制属性。 tf.keras中介绍了并实现了两种解开表示的技术,即 InfoGAN 和 StackedGAN。
“第 7 章”,“跨域 GAN” 涵盖了 GAN 的实际应用,将图像从一个域转换为另一个域,通常称为跨域迁移。 CycleGAN 是一种广泛使用的跨域 GAN,在tf.keras中进行了讨论和实现。 本章演示 CycleGAN 执行着色和样式迁移。
“第 8 章”,“变分自编码器(VAE)”讨论了 DL 中的另一个重要主题。 与 GAN 类似,VAE 是用于生成综合数据的生成模型。 与 GAN 不同,VAE 专注于可解码的连续潜空间,该空间适合于变化推理。 tf.keras涵盖并实现了 VAE 及其变体 CVAE 和 β-VAE。
“第 9 章”,“深度强化学习”解释了强化学习和 Q 学习的原理。 提出了两种实现离散动作空间 Q 学习的技术,即 Q 表更新和深度 Q 网络(DQN)。 在 OpenAI Gym 环境中演示了在tf.keras中使用 Python 和 DQN 进行 Q 学习的实现。
“第 10 章”,“策略梯度方法”解释了如何使用神经网络来学习强化学习中的决策策略。 在tf.keras和 OpenAI Gym 环境中涵盖并实现了四种方法,即 REINFORCE,带有基线的 REINFORCE,演员评论家和优势演员评论家。 本章中的示例演示了连续操作空间上的策略梯度方法。
“第 11 章”,“对象检测”讨论了计算机视觉,对象检测或识别和定位图像中对象的最常见应用之一。 涵盖了称为 SSD 的多尺度目标检测算法的关键概念,并使用tf.keras逐步构建了实现。 提出了用于数据集收集和标记的示例技术。 之后,使用数据集对 SSD 的tf.keras实现进行训练和评估。
“第 12 章”,“语义分割”讨论了计算机视觉,语义分割或识别图像中每个像素的对象类别的另一种常见应用。 讨论了分割原理。 然后,将更详细地介绍语义分割。 使用tf.keras构建并评估了称为 FCN 的语义分割算法的示例实现。 使用上一章中收集的相同数据集,但重新标记了语义分割。
“第 13 章”,“使用互信息的无监督学习”研究了如果 DL 严重依赖人类标签,它将不会继续发展。 无监督学习侧重于不需要人工标签的算法。 一种实现无监督学习的有效技术是利用互信息(MI)的概念。 通过最大化 MI,可以使用tf.keras实现和评估无监督的聚类/分类。
充分利用这本书
- 深度学习和 Python:读者应该具有深度学习及其在 Python 中的实现的基础知识。 尽管以前使用 Keras 实现深度学习算法的经验很重要,但这不是必需的。“第 1 章”, “Keras 高级深度学习入门”概述了深度学习的概念及其在
tf.keras中的实现。 - 数学:本书中的讨论假定读者熟悉大学级别的微积分,线性代数,统计和概率。
- GPU:本书中的大多数
tf.keras实现都需要 GPU。 如果没有 GPU,则由于涉及的时间(数小时至数天),因此无法执行许多代码示例。 本书中的示例尽可能多地使用合理数量的数据,以最大程度地减少高性能计算机的使用。 读者应该至少可以使用 NVIDIA GTX 1060。 - 编辑器:本书的示例代码是在 Ubuntu Linux 18.04 LTS 和 MacOS Catalina 中使用 vim 编辑的。 任何支持 Python 的文本编辑器都是可以接受的。
- TensorFlow 2:本书中的代码示例是使用 TensorFlow 2 的 Keras API 或
tf2编写的。 请确保正确安装了 NVIDIA GPU 驱动和tf2。 - GitHub:我们通过示例和实验学习。 请从其 GitHub 存储库中
git pull或fork这本书的代码包。 获取代码后,对其进行检查。 运行。 更改。 再次运行。 通过调整代码进行创造性的实验。 这是欣赏本章中解释的所有理论的唯一方法。 在此书的 GitHub 存储库上点击星星也受到高度赞赏。
下载示例代码文件
我们还从这里提供了丰富的书籍和视频目录中的其他代码包。 去看一下!
下载彩色图像
我们还提供本书中使用的彩色图像图像。 您可以在此处下载。
使用约定
本书中的代码使用 Python。 更具体地说,是 Python3。例如:
代码块设置如下:
def build_generator(inputs, image_size):
"""Build a Generator Model
Stack of BN-ReLU-Conv2DTranpose to generate fake images
Output activation is sigmoid instead of tanh in [1].
Sigmoid converges easily.
Arguments:
inputs (Layer): Input layer of the generator
the z-vector)
image_size (tensor): Target size of one side
(assuming square image)
Returns:
generator (Model): Generator Model
"""
image_resize = image_size // 4
# network parameters
kernel_size = 5
layer_filters = [128, 64, 32, 1]
x = Dense(image_resize * image_resize * layer_filters[0])(inputs)
x = Reshape((image_resize, image_resize, layer_filters[0]))(x)
for filters in layer_filters:
# first two convolution layers use strides = 2
# the last two use strides = 1
if filters > layer_filters[-2]:
strides = 2
else:
strides = 1
x = BatchNormalization()(x)
x = Activation('relu')(x)
x = Conv2DTranspose(filters=filters,
kernel_size=kernel_size,
strides=strides,
padding='same')(x)
x = Activation('sigmoid')(x)
generator = Model(inputs, x, name='generator')
return generator
当我们希望提请您注意代码块的特定部分时,相关的行或项以粗体显示:
# generate fake images
fake_images = generator.predict([noise, fake_labels])
# real + fake images = 1 batch of train data
x = np.concatenate((real_images, fake_images))
# real + fake labels = 1 batch of train data labels
labels = np.concatenate((real_labels, fake_labels))
只要有可能,都包括文档字符串。 至少,文本注释用于最小化空间使用。
任何命令行代码执行都编写如下:
python3 dcgan-mnist-4.2.1.py
上面的示例具有以下布局:algorithm-dataset-chapter.section.number.py。 命令行示例是“第 4 章”,“生成对抗网络(GANs)”第二部分和第一列表中 MNIST 数据集上的 DCGAN。 在某些情况下,未编写要执行的显式命令行,但假定是:
python3 name-of-the-file-in-listing
该代码示例的文件名包含在“列表”标题中。 本书使用“列表”标识文本中的代码示例。
粗体:表示新的术语,重要的单词或您在屏幕上看到的单词,例如在菜单或对话框中,也显示在这样的文本中。 例如:StackedGAN 具有两个附加损失函数,即条件和熵。
警告或重要提示如下所示。
一、使用 Keras 入门高级深度学习
在第一章中,我们将介绍在本书中将使用的三个深度学习人工神经网络。 这些网络是 MLP,CNN 和 RNN(在第 2 节中定义和描述),它们是本书涵盖的所选高级深度学习主题的构建块,例如自回归网络(自编码器,GAN 和 VAE),深度强化学习 ,对象检测和分割以及使用互信息的无监督学习。
在本章中,我们将一起讨论如何使用 Keras 库实现基于 MLP,CNN 和 RNN 的模型。 更具体地说,我们将使用名为tf.keras的 TensorFlow Keras 库。 我们将首先探讨为什么tf.keras是我们的理想选择。 接下来,我们将深入研究三个深度学习网络中的实现细节。
本章将:
- 确定为什么
tf.keras库是进行高级深度学习的绝佳选择 - 介绍 MLP,CNN 和 RNN –高级深度学习模型的核心构建模块,我们将在本书中使用它们
- 提供有关如何使用
tf.keras实现基于 MLP,CNN 和 RNN 的模型的示例 - 在此过程中,开始引入重要的深度学习概念,包括优化,正则化和损失函数
在本章结束时,我们将使用tf.keras实现基本的深度学习网络。 在下一章中,我们将介绍基于这些基础的高级深度学习主题。 让我们通过讨论 Keras 及其作为深度学习库的功能来开始本章。
1. Keras 为什么是完美的深度学习库?
Keras [1]是一个受欢迎的深度学习库,在撰写本文时有 370,000 个开发人员在使用它-这个数字每年以大约 35% 的速度增长。 超过 800 位贡献者积极维护它。 我们将在本书中使用的一些示例已添加到 Keras GitHub 官方存储库中。
谷歌的 TensorFlow 是一个流行的开源深度学习库,它使用 Keras 作为其库的高级 API。 通常称为tf.keras。 在本书中,我们将交替使用 Keras 和tf.keras一词。
tf.keras作为深度学习库是一种流行的选择,因为它已高度集成到 TensorFlow 中,TensorFlow 因其可靠性而在生产部署中广为人知。 TensorFlow 还提供了各种工具,用于生产部署和维护,调试和可视化以及在嵌入式设备和浏览器上运行模型。 在技术行业中,Google,Netflix,Uber 和 NVIDIA 使用 Keras。
我们选择tf.keras作为本书的首选工具,因为它是致力于加速深度学习模型实现的库。 这使得 Keras 非常适合我们想要实用且动手的时候,例如,当我们探索本书中的高级深度学习概念时。 由于 Keras 旨在加速深度学习模型的开发,训练和验证,因此在有人可以最大限度地利用库之前,必须学习该领域的关键概念。
在tf.keras库中,各层之间就像乐高积木一样相互连接,从而形成了一个干净且易于理解的模型。 模型训练非常简单,只需要数据,大量训练和监控指标即可。
最终结果是,与其他深度学习库(例如 PyTorch)相比,大多数深度学习模型可以用更少的代码行来实现。 通过使用 Keras,我们将通过节省代码实现时间来提高生产率,而这些时间可以用于执行更关键的任务,例如制定更好的深度学习算法。
同样,Keras 是快速实现深度学习模型的理想选择,就像我们将在本书中使用的那样。 使用顺序模型 API,只需几行代码即可构建典型模型。 但是,不要被它的简单性所误导。
Keras 还可以使用其函数式 API 以及用于动态图的Model和Layer类来构建更高级和复杂的模型,可以对其进行定制以满足独特的需求。 函数式 API 支持构建类似图的模型,层重用以及创建行为类似于 Python 函数的模型。 同时,Model和Layer类提供了用于实现罕见或实验性深度学习模型和层的框架。
安装 Keras 和 TensorFlow
Keras 不是独立的深度学习库。 如您在“图 1.1.1”中所看到的,它建立在另一个深度学习库或后端的之上。 这可能是 Google 的 TensorFlow,MILA 的 Theano,微软的 CNTK 或 Apache MXNet。 但是,与本书的上一版不同,我们将使用 TensorFlow 2.0(tf2或简称为tf)提供的 Keras(更好地称为tf.keras),以利用 tf2 所提供的有用工具。 tf.keras也被认为是 TensorFlow 的事实上的前端,它在生产环境中表现出了公认的可靠性。 此外,在不久的将来,将不再提供 Keras 对 TensorFlow 以外的后端的支持。
从 Keras 迁移到tf.keras通常就像更改一样简单:
from keras... import ...
至
from tensorflow.keras... import ...
本书中的代码示例全部以 Python 3 编写,以支持 Python 2 于 2020 年结束。
在硬件上,Keras 在 CPU,GPU 和 Google 的 TPU 上运行。 在本书中,我们将在 CPU 和 NVIDIA GPU(特别是 GTX 1060,GTX 1080Ti,RTX 2080Ti,V100 和 Quadro RTX 8000)上进行测试:

图 1.1.1:Keras 是位于其他深度学习框架之上的高级库。 CPU,GPU 和 TPU 支持 Keras。
在继续进行本书的其余部分之前,我们需要确保正确安装了tf2。 有多种执行安装的方法。 一个示例是通过使用pip3安装tf2:
$ sudo pip3 install tensorflow
如果我们具有支持已正确安装驱动的 NVIDIA GPU,以及 NVIDIA CUDA 工具包和 cuDNN 深度神经网络库,则强烈建议您安装启用 GPU 的版本,因为它可以加快训练和预测的速度:
$ sudo pip3 install tensorflow-gpu
无需安装 Keras,因为它已经是tf2中的包。 如果您不愿意在系统范围内安装库,强烈建议使用 Anaconda 之类的环境。 除了具有隔离环境之外,Anaconda 发行版还安装了用于数据科学的常用第三方包,这些包对于深度学习是必不可少的。
本书中提供的示例将需要其他包,例如pydot,pydot_ng,vizgraph,python3-tk和matplotlib。 在继续本章之前,我们需要安装这些包。
如果安装了tf2及其依赖项,则以下内容不会产生任何错误:
$ python3
>>> import tensorflow as tf
>>> print(tf.__version__)
2.0.0
>>> from tensorflow.keras import backend as K
>>> print(K.epsilon())
1e-07
本书没有涵盖完整的 Keras API。 我们将仅介绍解释本书中选定的高级深度学习主题所需的材料。 有关更多信息,请查阅 Keras 官方文档,该文档在这里或这里。
在随后的部分中,将讨论 MLP,CNN 和 RNN 的详细信息。 这些网络将用于使用tf.keras构建简单的分类器。
2. MLP,CNN 和 RNN
我们已经提到,我们将使用三个深度学习网络,它们是:
- MLP:多层感知器
- CNN:卷积神经网络
- RNN:循环神经网络
这些是我们将在本书中使用的三个网络。 稍后,您会发现它们经常结合在一起以利用每个网络的优势。
在本章中,我们将更详细地讨论这些构建块。 在以下各节中,将介绍 MLP 以及其他重要主题,例如损失函数,优化器和正则化器。 接下来,我们将介绍 CNN 和 RNN。
MLP,CNN 和 RNN 之间的区别
MLP 是全连接(FC)网络。 在某些文献中,您经常会发现将该称为或深度前馈网络或前馈神经网络。 在本书中,我们将使用术语 MLP。 从已知目标应用的角度了解此网络将有助于我们深入了解高级深度学习模型设计的根本原因。
MLP 在简单的逻辑和线性回归问题中很常见。 但是,MLP 对于处理顺序和多维数据模式不是最佳的。 通过设计,MLP 难以记住顺序数据中的模式,并且需要大量参数来处理多维数据。
对于顺序数据输入,RNN 很受欢迎,因为内部设计允许网络发现数据历史记录中的依存关系,这对预测很有用。 对于诸如图像和视频之类的多维数据,CNN 擅长提取用于分类,分割,生成和其他下游任务的特征映射。 在某些情况下,一维卷积形式的 CNN 也用于具有顺序输入数据的网络。 但是,在大多数深度学习模型中,将 MLP 和 CNN 或 RNN 结合起来可以充分利用每个网络。
MLP,CNN 和 RNN 并不完整整个深度网络。 需要识别目标或损失函数,优化器,和调节器。 目标是减少训练期间的损失函数值,因为这样的减少是模型正在学习的一个很好的指标。
为了使值最小化,模型使用了优化器。 这是一种算法,它确定在每个训练步骤中应如何调整权重和偏差。 经过训练的模型不仅必须对训练数据起作用,而且还必须对训练环境之外的数据起作用。 正则化器的作用是确保训练后的模型能够推广到新数据。
现在,让我们进入这三个网络–我们将从谈论 MLP 网络开始。
3. 多层感知器(MLP)
我们将要看的这三个网络中的第一个是 MLP 网络。 让我们假设目标是创建一个神经网络,用于基于手写数字识别数字。 例如,当网络的输入是手写数字 8 的图像时,相应的预测也必须是数字 8。这是分类器网络的经典工作,可以使用逻辑回归进行训练。 为了训练和验证分类器网络,必须有足够大的手写数字数据集。 国家标准技术混合研究院数据集,简称 MNIST [2],通常被视为 Hello World 深度学习数据集。 它是用于手写数字分类的合适数据集。
在我们讨论 MLP 分类器模型之前,必须了解 MNIST 数据集。 本书中的大量示例都使用 MNIST 数据集。 MNIST 用于来解释并验证许多深度学习理论,因为它包含的 70,000 个样本很小,但是的信息足够丰富:

图 1.3.1:来自 MNIST 数据集的示例图像。 每个灰度图像为28×28像素。
在下面的中,我们将简要介绍 MNIST。
MNIST 数据集
MNIST 是从 0 到 9 的手写数字的集合。它具有 60,000 张图像的训练集和 10,000 张测试图像,这些图像被分为相应的类别或标签。 在某些文献中,术语目标或基本事实也用于指标签。
在上图中,可以看到 MNIST 数字的样本图像,每个样本的大小为28 x 28像素(灰度)。 为了在 Keras 中使用 MNIST 数据集,提供了一个 API,用于下载并自动提取图像和标签。“列表 1.3.1”演示了如何仅在一行中加载 MNIST 数据集,从而使我们既可以计算训练和测试标签,又可以绘制 25 个随机数字图像。
“列表 1.3.1”:mnist-sampler-1.3.1.py
import numpy as np
from tensorflow.keras.datasets import mnist
import matplotlib.pyplot as plt
# load dataset
(x_train, y_train), (x_test, y_test) = mnist.load_data()
# count the number of unique train labels
unique, counts = np.unique(y_train, return_counts=True)
print("Train labels: ", dict(zip(unique, counts)))
# count the number of unique test labels
unique, counts = np.unique(y_test, return_counts=True)
print("Test labels: ", dict(zip(unique, counts)))
# sample 25 mnist digits from train dataset
indexes = np.random.randint(0, x_train.shape[0], size=25)
images = x_train[indexes]
labels = y_train[indexes]
# plot the 25 mnist digits
plt.figure(figsize=(5,5))
for i in range(len(indexes)):
plt.subplot(5, 5, i + 1)
image = images[i]
plt.imshow(image, cmap='gray')
plt.axis('off')
plt.savefig("mnist-samples.png")
plt.show()
plt.close('all')
mnist.load_data()方法很方便,因为不需要分别加载所有 70,000 张图像和标签并将它们存储在数组中。 执行以下命令:
python3 mnist-sampler-1.3.1.py
在命令行上,该代码示例打印训练和测试数据集中的标签分布:
Train labels:{0: 5923, 1: 6742, 2: 5958, 3: 6131, 4: 5842, 5: 5421, 6: 5918, 7: 6265, 8: 5851, 9: 5949}
Test labels:{0: 980, 1: 1135, 2: 1032, 3: 1010, 4: 982, 5: 892, 6: 958, 7: 1028, 8: 974, 9: 1009}
之后,代码将绘制 25 个随机数字,如先前在“图 1.3.1”中所示。
在讨论 MLP 分类器模型之前,必须记住,虽然 MNIST 数据由二维张量组成,但应根据输入层的类型对它进行重塑。 以下“图 1.3.2”显示了如何为 MLP,CNN 和 RNN 输入层重塑3×3灰度图像:

图 1.3.2:根据输入层的类型,对与 MNIST 数据相似的输入图像进行重塑。 为简单起见,显示了3×3灰度图像的重塑。
在以下各节中,将介绍 MNIST 的 MLP 分类器模型。 我们将演示如何使用tf.keras有效地构建,训练和验证模型。
MNIST 数字分类器模型
“图 1.3.3”中显示的建议的 MLP 模型可用于 MNIST 数字分类。 当单元或感知器暴露在外时,MLP 模型是一个全连接网络,如图“图 1.3.4”所示。 我们还将展示如何根据第n个单元的权重wᵢ和偏置bₙ的输入来计算感知器的输出。 相应的tf.keras实现在“列表 1.3.2”中进行了说明:

图 1.3.3:MLP MNIST 数字分类器模型

图 1.3.4:图 1.3.3 中的 MLP MNIST 数字分类器由全连接层组成。 为简单起见,未显示激活层和退出层。 还详细显示了一个单元或感知器。
“列表 1.3.2”:mlp-mnist-1.3.2.py
import numpy as np
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Activation, Dropout
from tensorflow.keras.utils import to_categorical, plot_model
from tensorflow.keras.datasets import mnist
# load mnist dataset
(x_train, y_train), (x_test, y_test) = mnist.load_data()
# compute the number of labels
num_labels = len(np.unique(y_train))
# convert to one-hot vector
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
# image dimensions (assumed square)
image_size = x_train.shape[1]
input_size = image_size * image_size
# resize and normalize
x_train = np.reshape(x_train, [-1, input_size])
x_train = x_train.astype('float32') / 255
x_test = np.reshape(x_test, [-1, input_size])
x_test = x_test.astype('float32') / 255
# network parameters
batch_size = 128
hidden_units = 256
dropout = 0.45
# model is a 3-layer MLP with ReLU and dropout after each layer
model = Sequential()
model.add(Dense(hidden_units, input_dim=input_size))
model.add(Activation('relu'))
model.add(Dropout(dropout))
model.add(Dense(hidden_units))
model.add(Activation('relu'))
model.add(Dropout(dropout))
model.add(Dense(num_labels))
# this is the output for one-hot vector
model.add(Activation('softmax'))
model.summary()
plot_model(model, to_file='mlp-mnist.png', show_shapes=True)
# loss function for one-hot vector
# use of adam optimizer
# accuracy is good metric for classification tasks
model.compile(loss='categorical_crossentropy',
optimizer='adam',
metrics=['accuracy'])
# train the network
model.fit(x_train, y_train, epochs=20, batch_size=batch_size)
# validate the model on test dataset to determine generalization
_, acc = model.evaluate(x_test,
y_test,
batch_size=batch_size,
verbose=0)
print("\nTest accuracy: %.1f%%" % (100.0 * acc))
在讨论模型实现之前,数据必须具有正确的形状和格式。 加载 MNIST 数据集后,标签的数量计算为:
# compute the number of labels
num_labels = len(np.unique(y_train))
硬编码num_labels = 10也可以选择。 但是,让计算机完成工作始终是一个好习惯。 该代码假定y_train的标签为 0 到 9。
此时,标签为数字格式,即从 0 到 9。标签的这种稀疏标量表示形式不适用于按类别输出概率的神经网络预测层。 一种更合适的格式称为one-hot vector,这是一个十维向量,除数字类的索引外,所有元素均为 0。 例如,如果标签为 2,则等效one-hot vector为[0,0,1,0,0,0,0,0,0,0]。 第一个标签的索引为 0。
以下各行将每个标签转换为one-hot vector:
# convert to one-hot vector
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
在深度学习中,数据存储在张量中。 张量一词适用于标量(0D 张量),向量(1D 张量),矩阵(二维张量)和多维张量。
从这一点出发,除非标量,向量或矩阵使解释更清楚,否则将使用术语张量。
如下所示的其余代码将计算图像尺寸,第一密集层的input_size值,并将每个像素值从 0 缩放到 255,范围从 0.0 缩放到 1.0。 尽管可以直接使用原始像素值,但最好对输入数据进行规范化,以避免产生可能会使训练变得困难的较大梯度值。 网络的输出也被标准化。 训练后,可以通过将输出张量乘以 255 来将所有内容恢复为整数像素值。
提出的模型基于 MLP 层。 因此,输入应为一维张量。 这样,将x_train和x_test分别重塑为[60,000,28 * 28]和[10,000,28 * 28]。 在 NumPy 中,大小为 -1 表示让库计算正确的尺寸。 在x_train的情况下为 60,000。
# image dimensions (assumed square) 400
image_size = x_train.shape[1]
input_size = image_size * image_size
# resize and normalize
x_train = np.reshape(x_train, [-1, input_size])
x_train = x_train.astype('float32') / 255
x_test = np.reshape(x_test, [-1, input_size])
x_test = x_test.astype('float32') / 255
在准备好数据集之后,以下内容将重点介绍使用 Keras 的顺序 API 构建 MLP 分类器模型。
使用 MLP 和 Keras 构建模型
数据准备之后,接下来是构建模型。 所提出的模型由三个 MLP 层组成。 在 Keras 中,将 MLP 层称为密集,它表示紧密连接的层。 第一和第二个 MLP 层本质上是相同的,每个都有 256 个单元,然后是整流线性单元(ReLU)激活和退出。 由于 128、512 和 1,024 个单元的表现指标较低,因此选择 256 个单元。 在 128 个单元的情况下,网络收敛迅速,但测试精度较低。 512 或 1,024 的额外单元数量不会显着提高测试精度。
单元数是超参数。 它控制网络的容量。 容量是网络可以近似的函数复杂性的度量。 例如,对于多项式,度是超参数。 随着程度的增加,函数的能力也随之增加。
如以下代码行所示,使用 Keras 的顺序 API 实现分类器模型。 如果模型需要一个输入和一个输出(由一系列层处理),这就足够了。 为了简单起见,我们现在将使用它。 但是,在“第 2 章”,“深度神经网络”中,将引入 Keras 的函数式 API 来实现高级深度学习模型,该模型需要更复杂的结构(例如多个输入和输出)。
# model is a 3-layer MLP with ReLU and dropout after each layer model = Sequential()
model.add(Dense(hidden_units, input_dim=input_size))
model.add(Activation('relu'))
model.add(Dropout(dropout))
model.add(Dense(hidden_units))
model.add(Activation('relu'))
model.add(Dropout(dropout))
model.add(Dense(num_labels))
# this is the output for one-hot vector model.add(Activation('softmax'))
由于Dense层是线性运算,因此Dense层的序列只能近似线性函数。 问题是 MNIST 数字分类本质上是非线性过程。 在Dense层之间插入relu激活将使 MLP 网络能够对非线性映射建模。 relu或 ReLU 是一个简单的非线性函数。 这很像一个过滤器,它允许正输入不变地通过,同时将其他所有值都钳位为零。 数学上,relu用以下公式表示,见“图 1.3.5”:


图 1.3.5:ReLU 函数图。 ReLU 函数在神经网络中引入了非线性。
还可以使用其他非线性函数,例如elu,selu,softplus,sigmoid和tanh。 但是,relu是最常用的函数,由于其简单性,在计算上是有效的。 Sigmoid 和 tanh 函数在输出层中用作激活函数,稍后将描述。“表 1.3.1”显示了每个激活函数的方程式:
relu |
relu(x) = max(0, x) |
1.3.1 |
|---|---|---|
softplus |
softplus(x) = log(1 + exp(x)) |
1.3.2 |
elu |
其中a≥0并且是可调超参数 |
1.3.3 |
selu |
selu(x) = k×elu(x, a)其中k = 1.0507009873554804934193193349852946和a = 1.6732632423543772848170429916717 |
1.3.4 |
sigmoid |
![]() |
1.3.5 |
tanh |
![]() |
1.3.6 |
表 1.3.1:常见非线性激活函数的定义
尽管我们已完成 MLP 分类器模型的关键层,但我们尚未解决泛化问题或模型超出训练数据集的能力。 为了解决这个问题,我们将在下一节介绍正则化。
正则化
神经网络倾向于记住其训练数据,特别是如果它包含的容量超过。 在这种情况下,当经受测试数据时,网络将发生灾难性的故障。 这是网络无法推广的经典情况。 为了避免这种趋势,模型使用了正则化层或函数。 常见的正则化层是Dropout。
丢弃的想法很简单。 给定丢弃率(此处将其设置为dropout = 0.45),丢弃层会从参与下一层的单元中随机删除这一部分。 例如,如果第一层具有 256 个单元,则在应用dropout = 0.45之后,只有(1-0.45) * 256个单元,来自第 1 层的 140 个单元参与第 2 层。
丢弃层使神经网络对于无法预见的输入数据具有鲁棒性,因为即使缺少某些单元,训练后的神经网络也可以正确预测。 值得注意的是,输出层中没有使用丢弃,它仅在训练期间处于活动状态。 此外,在预测期间不存在丢弃现象。
除了诸如丢弃之类的正则化之外,还可以使用其他正则化器。 在 Keras 中,可以按层对偏置,权重和激活输出进行正则化。 l1和l2通过添加罚函数来支持较小的参数值。 l1和l2都使用绝对值(l1)或平方(l2)之和的分数来执行惩罚。 换句话说,惩罚函数迫使优化器找到较小的参数值。 参数值小的神经网络对来自输入数据的噪声的存在更加不敏感。
例如,带有fraction=0.001的l2权重正则器可以实现为:
from tensorflow.keras.regularizers import l2
model.add(Dense(hidden_units,
kernel_regularizer=l2(0.001),
input_dim=input_size))
如果使用l1或l2正则化,则不添加任何附加层。 正则化在内部施加在Dense层中。 对于建议的模型,丢弃仍然具有比l2更好的表现。
我们的模型几乎已经完成。 下一节将重点介绍输出层和损失函数。
输出激活和损失函数
输出的层具有 10 个单元,其后是softmax激活层。 这 10 个单元对应于 10 个可能的标签,类或类别。 可以用数学方式表示softmax激活,如以下等式所示:
(Equation 1.3.7)
该方程适用于所有N = 10输出,xᵢ对于i = 0, 1, ..., 9作最终预测。 softmax的概念非常简单。 通过对预测进行归一化,将输出压缩为概率。 在此,每个预测输出都是该索引是给定输入图像的正确标签的概率。 所有输出的所有概率之和为 1.0。 例如,当softmax层生成预测时,它将是一个 10 维一维张量,看起来像以下输出:
[3.57351579e-11 7.08998016e-08
2.30154569e-07 6.35787558e-07
5.57471187e-11 4.15353840e-09
3.55973775e-16 9.99995947e-01
1.29531730e-09 3.06023480e-06]
预测输出张量建议输入图像的索引具有最高概率,因此将为 7。 numpy.argmax()方法可用于确定具有最高值的元素的索引。
输出激活层还有其他选择,例如linear,sigmoid或tanh。 linear激活是一种恒等函数。 它将其输入复制到其输出。 sigmoid函数更具体地是,称为逻辑 Sigmoid。 如果预测张量的元素将独立地映射在 0.0 和 1.0 之间,则将使用此方法。 与softmax中不同,预测张量的所有元素的总和不限于 1.0。 例如,sigmoid用作情感预测(从 0.0 到 1.0、0.0 不好,1.0 很好)或图像生成(0.0 映射到像素级别 0 和 1.0 映射到像素 255)的最后一层 。
tanh函数将其输入映射在 -1.0 到 1.0 的范围内。 如果输出可以同时以正值和负值摆幅,则这一点很重要。 tanh函数在循环神经网络的内部层中更普遍使用,但也已用作输出层激活。 如果在输出激活中使用 tanh 代替sigmoid,则必须适当缩放使用的数据。 例如,不是使用x = x / 255缩放[0.0, 1.0]范围内的每个灰度像素,而是使用x = (x - 127.5) / 127.5将其分配在[-1.0, 1.0]范围内。
下图“图 1.3.6”显示了sigmoid和tanh函数。 数学上,Sigmoid 可以用以下公式表示:
(Equation 1.3.5)

图 1.3.6:Sigmoid 和正切图
预测张量距单热地面真值向量有多远称为损失。 损失函数的一种类型是mean_squared_error(MSE),或者是目标或标签与预测之间差异的平方的平均值。 在当前示例中,我们使用categorical_crossentropy。 它是目标或标签乘积与每个类别的预测对数之和的负数。 Keras 中还有其他损失函数,例如mean_absolute_error和binary_crossentropy。“表 1.3.2”总结了的常见损失函数。
| 损失函数 | 公式 |
|---|---|
mean_squared_error |
![]() |
mean_absolute_error |
![]() |
categorical_crossentropy |
![]() |
binary_crossentropy |
![]() |
表 1.3.2:常见损失函数汇总。 类别是指标签和预测中的类别数(例如:MNIST 为 10)。 所示的损失方程式仅适用于一个输出。 平均损失值是整个批量的平均值。
损失函数的选择不是任意的,而应作为模型正在学习的标准。 对于按类别进行分类,在softmax激活层之后,categorical_crossentropy或mean_squared_error是一个不错的选择。 binary_crossentropy损失函数通常在sigmoid激活层之后使用,而mean_squared_error是tanh输出的选项。
在下一部分中,我们将讨论优化算法以最小化我们在此处讨论的损失函数。
优化
通过优化,目标是使损失函数最小化。 这个想法是,如果将损失减少到可接受的水平,则该模型将间接学习将输入映射到输出的函数。 表现指标用于确定模型是否了解了基础数据分布。 Keras 中的默认指标是损失。 在训练,验证和测试期间,还可以包括其他指标,例如准确率。 准确率是基于地面真实性的正确预测的百分比或分数。 在深度学习中,还有许多其他表现指标。 但是,它取决于模型的目标应用。 在文献中,报告了测试数据集上训练后的模型的表现指标,用于与其他深度学习模型进行比较。
在 Keras 中,优化器有个选择。 最常用的优化器是随机梯度下降(SGD),自适应矩(Adam)和均方根传播(RMSprop)。 每个优化器均具有可调参数,例如学习率,动量和衰减。 Adam 和 RMSprop 是具有自适应学习率的 SGD 的变体。 在提出的分类器网络中,使用了 Adam,因为它具有最高的测试精度。
SGD 被认为是最基本的优化程序。 它是演算中梯度下降的简单版本。 在梯度下降(GD)中,追踪下坡函数的曲线可找到最小值,就像在山谷中下坡直至到达底部一样。
GD 算法如图 1.3.7 所示。 假设x是被调整以找到y的最小值(例如,损失函数)的参数(例如,权重)。 从x = -0.5的任意点开始。 梯度dy/dx = -2.0。 GD 算法强加x然后更新为x = -0.5 - ε(-2.0)。 x的新值等于旧值,再加上ε缩放的梯度的相反值。 小数字ε是指学习率。 如果ε = 0.01,则x的新值为 -0.48。 GD 是迭代执行的。 在每一步,y都将接近其最小值。 在x = 0.5时,dy/dx = 0。 GD 已找到y = -1.25的绝对最小值。 梯度建议不要进一步改变x。
学习率的选择至关重要。 大的ε值可能找不到最小值,因为搜索只会在最小值附近来回摆动。 一方面,在找到最小值之前,较大的ε值可能需要进行大量迭代。 在有多个最小值的情况下,搜索可能会陷入局部最小值。

图 1.3.7:GD 类似于在函数曲线上向下走直到到达最低点。 在此图中,全局最小值为x = 0.5。
多个极小值的示例可以在“图 1.3.8”中看到。 如果由于某种原因从图的左侧开始搜索并且学习率很小,则 GD 很可能会发现x = -1.51是最小值 。 GD 无法在x = 1.66时找到全局最小值。 具有足够值的学习率将使 GD 可以克服x = 0.0的问题。
在深度学习实践中,通常建议从更高的学习率开始(例如,从 0.1 到 0.001),并随着损失接近最小值而逐渐降低学习率。

图 1.3.8:具有 2 个最小值的函数图,x = -1.51和x = 1.66。 还显示了该函数的导数。
GD 通常不用于深度神经网络,因为遇到数百万个要训练的参数很常见。 执行完整的 GD 在计算上效率低下。 而是使用 SGD。 在 SGD 中,选择一小批样本以计算下降的近似值。 参数(例如权重和偏差)可通过以下公式进行调整:

在该等式中,θ和g = 1/m ᐁ[θ] ΣL分别是损失函数的参数和梯度张量。g由损失函数的偏导数计算得出。 出于 GPU 优化的目的,建议最小批量大小为 2 的幂。 在建议的网络中,batch_size = 128。
“公式 1.3.8”计算最后一层参数更新。 那么,我们如何调整前几层的参数呢? 在这种情况下,应用微分链规则将导数传播到较低层并相应地计算梯度。 该算法在深度学习中称为反向传播。 反向传播的详细信息超出了本书的范围。 但是,可以在这里找到很好的在线参考。
由于优化是基于微分的,因此得出损失函数的重要标准是它必须平滑或可微。 当引入新的损失函数时,这是要牢记的重要约束。
给定训练数据集,损失函数的选择,优化器和正则化器,现在可以通过调用fit()函数来训练模型:
# loss function for one-hot vector
# use of adam optimizer
# accuracy is a good metric for classification tasks model.compile(loss='categorical_crossentropy',
optimizer='adam', metrics=['accuracy'])
# train the network
model.fit(x_train, y_train, epochs=20, batch_size=batch_size)
这是 Keras 的另一个有用函数。 通过仅提供x和y数据,要训练的周期数和批量大小,fit()完成了其余工作。 在其他深度学习框架中,这转化为多项任务,例如以适当的格式准备输入和输出数据,加载,监视等等。 尽管所有这些都必须在for循环内完成,但在 Keras 中,一切都只需要一行即可完成。
在fit()函数中,一个周期是整个训练数据的完整采样。 batch_size参数是每个训练步骤要处理的输入数量的样本大小。 为了完成一个周期,fit()将处理等于训练数据集大小的步数除以批量大小再加上 1,以补偿任何小数部分。
训练模型后,我们现在可以评估其表现。
表现评估
至此,MNIST 数字分类器的模型现已完成。 表现评估将是的下一个关键步骤,以确定提议的训练模型是否已提出令人满意的解决方案。 将模型训练 20 个时间段就足以获得可比较的表现指标。
下表“表 1.3.3”列出了不同的网络配置和相应的表现指标。 在“层”下,显示第 1 到第 3 层的单元数。对于每个优化器,将使用tf.keras中的默认参数。 可以观察到改变正则化器,优化器和每层单元数的效果。“表 1.3.3”中的另一个重要观察结果是,更大的网络不一定会转化为更好的表现。
在训练和测试数据集的准确率方面,增加此网络的深度不会显示任何其他好处。 另一方面,较少的单元(例如 128)也可能会降低测试和训练的准确率。 删除正则器后,将在99.93%处获得最佳的训练精度,并且每层使用 256 个单元。 但是,由于网络过拟合,测试精度在98.0%时要低得多。
最高的测试精度是使用 Adam 优化器和98.5%处的Dropout(0.45)。 从技术上讲,鉴于其训练精度为99.39%,仍然存在某种程度的过拟合。 对于256-512-256,Dropout(0.45)和 SGD,在98.2%时,训练和测试精度均相同。 同时去除正则化和 ReLU 层会导致其表现最差。 通常,我们会发现Dropout层比l2具有更好的表现。
下表演示了调整期间典型的深度神经网络表现:
| 层 | 正则化函数 | 优化器 | ReLU | 训练准确率(%) | 测试准确率(%) |
|---|---|---|---|---|---|
| 256-256-256 | 没有 | SGD | 没有 | 93.65 | 92.5 |
| 256-256-256 | L2(0.001) | SGD | 是 | 99.35 | 98.0 |
| 256-256-256 | L2(0.01) | SGD | 是 | 96.90 | 96.7 |
| 256-256-256 | 没有 | SGD | 是 | 99.93 | 98.0 |
| 256-256-256 | 丢弃(0.4) | SGD | 是 | 98.23 | 98.1 |
| 256-256-256 | 丢弃(0.45) | SGD | 是 | 98.07 | 98.1 |
| 256-256-256 | 丢弃(0.5) | SGD | 是 | 97.68 | 98.1 |
| 256-256-256 | 丢弃(0.6) | SGD | 是 | 97.11 | 97.9 |
| 256-512-256 | 丢弃(0.45) | SGD | 是 | 98.21 | 98.2 |
| 512-512-512 | 丢弃(0.2) | SGD | 是 | 99.45 | 98.3 |
| 512-512-512 | 丢弃(0.4) | SGD | 是 | 98.95 | 98.3 |
| 512-1024-512 | 丢弃(0.45) | SGD | 是 | 98.90 | 98.2 |
| 1024-1024-1024 | 丢弃(0.4) | SGD | 是 | 99.37 | 98.3 |
| 256-256-256 | 丢弃(0.6) | Adam | 是 | 98.64 | 98.2 |
| 256-256-256 | 丢弃(0.55) | Adam | 是 | 99.02 | 98.3 |
| 256-256-256 | 丢弃(0.45) | Adam | 是 | 99.39 | 98.5 |
| 256-256-256 | 丢弃(0.45) | RMSprop | 是 | 98.75 | 98.1 |
| 128-128-128 | 丢弃(0.45) | Adam | 是 | 98.70 | 97.7 |
表 1.3.3 不同的 MLP 网络配置和表现指标
示例指示需要改进网络架构。 在下一节讨论了 MLP 分类器模型摘要之后,我们将介绍另一个 MNIST 分类器。 下一个模型基于 CNN,并证明了测试准确率的显着提高。
模型摘要
使用 Keras 库为我们提供了一种快速的机制,可以通过调用以下方法来仔细检查模型描述:
model.summary()
下面的“列表 1.3.3”显示了所建议网络的模型摘要。 它总共需要 269,322 个参数。 考虑到我们具有对 MNIST 数字进行分类的简单任务,这一点非常重要。 MLP 的参数效率不高。 可以通过关注如何计算感知器的输出,从“图 1.3.4”计算参数的数量。 从输入到密集层:784 × 256 + 256 = 200,960。 从第一密集层到第二密集层:256 × 256 + 256 = 65,792。 从第二个密集层到输出层:10 × 256 + 10 = 2,570。 总数是269,322。
“列表 1.3.3”:MLP MNIST 数字分类器模型的摘要:
Layer (type) Output Shape Param #
=================================================================
dense_1 (Dense) (None, 256) 200960
activation_1 (Activation) (None, 256) 0
dropout_1 (Dropout) (None, 256) 0
dense_2 (Dense) (None, 256) 65792
activation_2 (Activation) (None, 256) 0
dropout_2 (Dropout) (None, 256) 0
dense_3 (Dense) (None, 10) 2750
activation_3 (Activation) (None, 10) 0
=================================================================
Total params: 269,322
Trainable params: 269,322
Non-trainable params: 0
验证网络的另一种方法是通过调用:
plot_model(model, to_file='mlp-mnist.png', show_shapes=True)
“图 1.3.9”显示了该图。 您会发现这类似于summary()的结果,但是以图形方式显示了每个层的互连和 I/O。

图 1.3.9:MLP MNIST 数字分类器的图形描述
在总结了我们模型的之后,到此结束了我们对 MLP 的讨论。 在下一部分中,我们将基于 CNN 构建 MNIST 数字分类器模型。
4. 卷积神经网络(CNN)
现在,我们将进入第二个人工神经网络 CNN。 在本节中,我们将解决相同的 MNIST 数字分类问题,但这一次使用 CNN。
“图 1.4.1”显示了我们将用于 MNIST 数字分类的 CNN 模型,而其实现在“列表 1.4.1”中进行了说明。 实现 CNN 模型将需要对先前模型进行一些更改。 现在,输入张量不再具有输入向量,而具有新尺寸(height,width,channels)或(image_size,image_size,1)=(28,28 ,1)用于 MNIST 灰度图像。 需要调整训练和测试图像的大小以符合此输入形状要求。

图 1.4.1:用于 MNIST 数字分类的 CNN 模型
实现上图:
“列表 1.4.1”:cnn-mnist-1.4.1.py
import numpy as np
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Activation, Dense, Dropout
from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten
from tensorflow.keras.utils import to_categorical, plot_model
from tensorflow.keras.datasets import mnist
# load mnist dataset
(x_train, y_train), (x_test, y_test) = mnist.load_data()
# compute the number of labels
num_labels = len(np.unique(y_train))
# convert to one-hot vector
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
# input image dimensions
image_size = x_train.shape[1]
# resize and normalize
x_train = np.reshape(x_train,[-1, image_size, image_size, 1])
x_test = np.reshape(x_test,[-1, image_size, image_size, 1])
x_train = x_train.astype('float32') / 255
x_test = x_test.astype('float32') / 255
# network parameters
# image is processed as is (square grayscale)
input_shape = (image_size, image_size, 1)
batch_size = 128
kernel_size = 3
pool_size = 2
filters = 64
dropout = 0.2
# model is a stack of CNN-ReLU-MaxPooling
model = Sequential()
model.add(Conv2D(filters=filters,
kernel_size=kernel_size,
activation='relu',
input_shape=input_shape))
model.add(MaxPooling2D(pool_size))
model.add(Conv2D(filters=filters,
kernel_size=kernel_size,
activation='relu'))
model.add(MaxPooling2D(pool_size))
model.add(Conv2D(filters=filters,
kernel_size=kernel_size,
activation='relu'))
model.add(Flatten())
# dropout added as regularizer
model.add(Dropout(dropout))
# output layer is 10-dim one-hot vector
model.add(Dense(num_labels))
model.add(Activation('softmax'))
model.summary()
plot_model(model, to_file='cnn-mnist.png', show_shapes=True)
# loss function for one-hot vector
# use of adam optimizer
# accuracy is good metric for classification tasks
model.compile(loss='categorical_crossentropy',
optimizer='adam',
metrics=['accuracy'])
# train the network
model.fit(x_train, y_train, epochs=10, batch_size=batch_size)
_, acc = model.evaluate(x_test,
y_test,
batch_size=batch_size,
verbose=0)
print("\nTest accuracy: %.1f%%" % (100.0 * acc))
的主要更改是Conv2D层的使用。 ReLU激活函数已经是Conv2D的参数。 当模型中包含batch normalization层时,可以将ReLU函数作为Activation层使用。 Batch normalization用于深层 CNN,因此可以利用较大的学习率而不会引起训练过程中的不稳定。
卷积
如果在 MLP 模型中,单元数量表示密集层,则核表示 CNN 操作。 如图“图 1.4.2”所示,可以将核可视化为矩形补丁或窗口,该补丁或窗口从左到右,从上到下在整个图像中滑动。 此操作称为卷积。 它将输入图像转换成特征映射,该特征映射表示核从输入图像中学到的内容。 然后将特征映射转换为后续层中的另一个特征映射,依此类推。 每个Conv2D生成的特征映射的数量由filters参数控制。

图 1.4.2:3×3 核与 MNIST 数字图像卷积。
在步骤tₙ和t[n + 1]中显示了卷积,其中核向右移动了 1 个像素 。
卷积中涉及的计算显示在“图 1.4.3”中:

图 1.4.3:卷积运算显示如何计算特征映射的一个元素
为简单起见,显示了应用了3×3核的3×3输入图像(或输入特征映射)。 卷积后显示结果特征映射。 特征映射中一个元素的值被加阴影。 您会注意到,结果特征映射小于原始输入图像的,这是因为卷积仅在有效元素上执行。 核不能超出映像的边界。 如果输入的尺寸应与输出特征映射相同,则Conv2D接受选项padding='same'。 输入在其边界周围填充零,以在卷积后保持尺寸不变。
池化操作
最后的更改是添加了MaxPooling2D层以及参数pool_size=2。 MaxPooling2D压缩每个特征映射。 每个大小为pool_size × pool_size的补丁都减少为 1 个特征映射点。 该值等于补丁中的最大特征点值。 下图显示了MaxPooling2D的两个补丁:

图 1.4.4:MaxPooling2D操作。 为简单起见,输入特征映射为4×4,结果为2×2特征映射。
MaxPooling2D的意义在于特征映射尺寸的减小,这转化为感受野尺寸的增加。 例如,在MaxPooling2D(2)之后,2×2 核现在大约与4×4补丁卷积。 CNN 学会了针对不同接收场大小的一组新的特征映射。
还有其他合并和压缩方式。 例如,要使MaxPooling2D(2)的尺寸减少 50%,AveragePooling2D(2)会取一个补丁的平均值而不是找到最大值。 交叉卷积Conv2D(strides=2,…)在卷积过程中将跳过每两个像素,并且仍具有相同的 50% 缩小效果。 每种还原技术的有效性都有细微的差异。
在Conv2D和MaxPooling2D中,pool_size和kernel都可以是非正方形的。 在这些情况下,必须同时指定行和列的大小。 例如,pool_ size = (1, 2)和kernel = (3, 5)。
最后一个MaxPooling2D操作的输出是一堆特征映射。 Flatten的作用是,将特征映射的栈转换为适用于Dropout或Dense层的向量格式,类似于 MLP 模型输出层。
在下一部分中,我们将评估经过训练的 MNIST CNN 分类器模型的表现。
表现评估和模型摘要
如“列表 1.4.2”中所示,“列表 1.4.1”中的 CNN 模型在 80,226 处需要较少数量的参数,而使用 MLP 层时需要 269,322 个参数。 conv2d_1层具有 640 个参数,因为每个核具有3×3 = 9个参数,并且 64 个特征映射中的每一个都有一个核,一个偏置参数。 其他卷积层的参数数量可以类似的方式计算。
“列表 1.4.2”:CNN MNIST 数字分类器的摘要
Layer (type) Output Shape Param #
=================================================================
conv2d_1 (Conv2D) (None, 26, 26, 64) 640
max_pooling2d_1 (MaxPooiling2) (None, 13, 13, 64) 0
conv2d_2 (Conv2D) (None, 11, 11, 64) 36928
max_pooling2d_2 (MaxPooiling2) (None, 5.5, 5, 64) 0
conv2d_3 (Conv2D) (None, 3.3, 3, 64) 36928
flatten_1 (Flatten) (None, 576) 0
dropout_1 (Dropout) (None, 576) 0
dense_1 (Dense) (None, 10) 5770
activation_1 (Activation) (None, 10) 0
===================================================================
Total params: 80,266
Trainable params: 80,266
Non-trainable params: 0
“图 1.4.5”:显示了 CNN MNIST 数字分类器的图形表示形式。

图 1.4.5:CNN MNIST 数字分类器的图形描述
“表 1.4.1”显示了 99.4% 的最大测试准确率,这对于使用带有dropout=0.2的 Adam 优化器的每层具有 64 个特征映射的 3 层网络可以实现。 CNN 比 MLP 具有更高的参数效率,并且具有更高的准确率。 同样,CNN 也适合从顺序数据,图像和视频中学习表示形式。
| 层 | 优化器 | 正则化函数 | 训练准确率(%) | 测试准确率(%) |
| --- | --- | --- | --- | --- | --- |
| 64-64-64 | SGD | 丢弃(0.2) | 97.76 | 98.50 |
| 64-64-64 | RMSprop | 丢弃(0.2) | 99.11 | 99.00 |
| 64-64-64 | Adam | 丢弃(0.2) | 99.75 | 99.40 |
| 64-64-64 | Adam | 丢弃(0.4) | 99.64 | 99.30 |
表 1.4.1:CNN MNIST 数字分类器的不同 CNN 网络配置和表现指标。
看了 CNN 并评估了训练好的模型之后,让我们看一下我们将在本章中讨论的最终核心网络:RNN。
5. 循环神经网络(RNN)
现在,我们来看一下三个人工神经网络中的最后一个,即 RNN。
RNN 是网络的序列,适用于学习顺序数据的表示形式,例如自然语言处理(NLP)中的文本或仪器中的传感器数据流 。 尽管每个 MNIST 数据样本本质上都不是顺序的,但不难想象每个图像都可以解释为像素行或列的序列。 因此,基于 RNN 的模型可以将每个 MNIST 图像作为 28 个元素的输入向量序列进行处理,时间步长等于 28。下面的清单在“图 1.5.1”中显示了 RNN 模型的代码:

图 1.5.1:用于 MNIST 数字分类的 RNN 模型
“列表 1.5.1”:rnn-mnist-1.5.1.py
import numpy as np
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Activation, SimpleRNN
from tensorflow.keras.utils import to_categorical, plot_model
from tensorflow.keras.datasets import mnist
# load mnist dataset
(x_train, y_train), (x_test, y_test) = mnist.load_data()
# compute the number of labels
num_labels = len(np.unique(y_train))
# convert to one-hot vector
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
# resize and normalize
image_size = x_train.shape[1]
x_train = np.reshape(x_train,[-1, image_size, image_size])
x_test = np.reshape(x_test,[-1, image_size, image_size])
x_train = x_train.astype('float32') / 255
x_test = x_test.astype('float32') / 255
# network parameters
input_shape = (image_size, image_size)
batch_size = 128
units = 256
dropout = 0.2
# model is RNN with 256 units, input is 28-dim vector 28 timesteps
model = Sequential()
model.add(SimpleRNN(units=units,
dropout=dropout,
input_shape=input_shape))
model.add(Dense(num_labels))
model.add(Activation('softmax'))
model.summary()
plot_model(model, to_file='rnn-mnist.png', show_shapes=True)
# loss function for one-hot vector
# use of sgd optimizer
# accuracy is good metric for classification tasks
model.compile(loss='categorical_crossentropy',
optimizer='sgd',
metrics=['accuracy'])
# train the network
model.fit(x_train, y_train, epochs=20, batch_size=batch_size)
_, acc = model.evaluate(x_test,
y_test,
batch_size=batch_size,
verbose=0)
print("\nTest accuracy: %.1f%%" % (100.0 * acc))
RNN 分类器与之前的两个模型之间有两个主要区别。 首先是input_shape = (image_size, image_size),它实际上是input_ shape = (timesteps, input_dim)或时间步长的input_dim维向量序列。 其次是使用SimpleRNN层以units=256表示 RNN 单元。 units变量代表输出单元的数量。 如果 CNN 是通过输入特征映射上的核卷积来表征的,则 RNN 输出不仅是当前输入的函数,而且是先前输出或隐藏状态的函数。 由于前一个输出也是前一个输入的函数,因此当前输出也是前一个输出和输入的函数,依此类推。 Keras 中的SimpleRNN层是真实 RNN 的简化版本。 以下等式描述了SimpleRNN的输出:
(Equation 1.5.1)
在此等式中,b是偏差,而W和U被称为循环核(先前输出的权重)和核(当前输入的权重) ), 分别。 下标t用于指示序列中的位置。 对于具有units=256的SimpleRNN层,参数总数为256 + 256×256 + 256×28 = 72,960,对应于b,W和个贡献。
下图显示了用于分类任务的SimpleRNN和 RNN 的图。 使SimpleRNN比 RNN 更简单的是缺少输出值o[t] = Vh[t] + c在计算softmax函数之前:

图 1.5.2:SimpleRNN和 RNN 图
与 MLP 或 CNN 相比,RNN 最初可能较难理解。 在 MLP 中,感知器是基本单元。 一旦了解了感知器的概念,MLP 就是感知器的网络。 在 CNN 中,核是一个补丁或窗口,可在特征映射中滑动以生成另一个特征映射。 在 RNN 中,最重要的是自环的概念。 实际上只有一个单元。
出现多个单元的错觉是因为每个时间步都有一个单元,但实际上,除非网络展开,否则它只是重复使用的同一单元。 RNN 的基础神经网络在单元之间共享。
“列表 1.5.2”中的摘要指示使用SimpleRNN需要较少数量的参数。
“列表 1.5.2”:RNN MNIST 数字分类器的摘要
Layer (type) Output Shape Param #
=================================================================
simple_rnn_1 (SimpleRNN) (None, 256) 72960
dense_1 (Dense) (None, 10) 2570
activation_1 (Activation) (None, 10) 36928
=================================================================
Total params: 75,530
Trainable params: 75,530
Non-trainable params: 0
“图 1.5.3”显示了 RNN MNIST 数字分类器的图形描述。 该模型非常简洁:

图 1.5.3:RNN MNIST 数字分类器图形说明
“表 1.5.1”显示 SimpleRNN 在所呈现的网络中具有最低的准确率:
| 层 | 优化器 | 正则化函数 | 训练准确率(%) | 测试准确率(%) |
| --- | --- | --- | --- | --- | --- |
| 256 | SGD | 丢弃(0.2) | 97.26 | 98.00 |
| 256 | RMSprop | 丢弃(0.2) | 96.72 | 97.60 |
| 256 | Adam | 丢弃(0.2) | 96.79 | 97.40 |
| 512 | SGD | 丢弃(0.2) | 97.88 | 98.30 |
表 1.5.1:不同的SimpleRNN网络配置和表现指标
在许多深度神经网络中,更常使用 RNN 家族的其他成员。 例如,机器翻译和问答问题都使用了长短期记忆(LSTM)。 LSTM 解决了长期依赖或记住与当前输出相关的过去信息的问题。
与 RNN 或SimpleRNN不同,LSTM 单元的内部结构更为复杂。“图 1.5.4”显示了 LSTM 的示意图。 LSTM 不仅使用当前输入和过去的输出或隐藏状态,还引入了一个单元状态sₜ,该状态将信息从一个单元传送到另一个单元。 单元状态之间的信息流由三个门控制fₜ,iₜ和qₜ。 这三个门的作用是确定应保留或替换哪些信息,以及过去对当前单元状态或输出有贡献的信息量以及过去和当前的输入。 我们不会在本书中讨论 LSTM 单元内部结构的细节。 但是,可以在这个页面上找到 LSTM 的直观指南。
LSTM()层可以用作SimpleRNN()的嵌入式替代。 如果 LSTM 对于手头的任务过于苛刻,则可以使用更简单的版本,称为门控循环单元(GRU)。 GRU 通过将单元状态和隐藏状态组合在一起来简化 LSTM。 GRU 还将门数量减少了一个。 GRU()函数也可以用作SimpleRNN()的直接替代品。

图 1.5.4:LSTM 图。为了清楚起见,未显示参数。
还有许多其他方法可以配置 RNN。 一种方法是制作双向 RNN 模型。 默认情况下,从当前输出仅受过去状态和当前输入影响的意义上讲,RNN 是单向的。
在双向 RNN 中,未来状态还可以通过允许信息向后流动来影响当前状态和过去状态。 根据收到的新信息,根据需要更新过去的输出。 可以通过调用包装器函数使 RNN 双向。 例如,双向 LSTM 的实现是Bidirectional(LSTM())。
对于所有类型的 RNN,增加单元数量也将增加容量。 但是,增加容量的另一种方法是堆叠 RNN 层。 尽管应注意,但作为一般经验法则,只有在需要时才应增加模型的容量。 容量过大可能会导致过拟合,结果可能导致训练时间延长和预测期间的表现降低。
6. 总结
本章概述了三种深度学习模型(MLP,RNN,CNN),并介绍了 TensorFlow 2 tf.keras,这是一个用于快速开发,训练和测试适合于生产环境的深度学习模型的库。 还讨论了 Keras 的顺序 API。 在下一章中,将介绍函数式 API,这将使我们能够构建更复杂的模型,专门用于高级深度神经网络。
本章还回顾了深度学习的重要概念,例如优化,正则化和损失函数。 为了便于理解,这些概念是在 MNIST 数字分类的背景下提出的。
还讨论了使用人工神经网络(特别是 MLP,CNN 和 RNN)进行 MNIST 数字分类的不同解决方案,它们是深度神经网络的重要组成部分,并讨论了它们的表现指标。
了解了深度学习概念以及如何将 Keras 用作工具之后,我们现在可以分析高级深度学习模型。 在下一章讨论了函数式 API 之后,我们将继续执行流行的深度学习模型。 随后的章节将讨论选定的高级主题,例如自回归模型(自编码器,GAN,VAE),深度强化学习,对象检测和分段以及使用互信息的无监督学习。 随附的 Keras 代码实现将在理解这些主题方面发挥重要作用。
7. 参考
Chollet, François. Keras (2015). https://github.com/keras-team/keras.LeCun, Yann, Corinna Cortes, and C. J. Burges. MNIST handwritten digit database. AT&T Labs [Online]. Available: http://yann.lecun.com/exdb/mnist2 (2010).
二、深度神经网络
在本章中,我们将研究深度神经网络。 这些网络在更具挑战性的数据集,如 ImageNet,CIFAR10 和 CIFAR100。 为简洁起见,我们仅关注两个网络: ResNet [2] [4]和 DenseNet [5]。 尽管我们会更加详细,但重要的是花一点时间介绍这些网络。
ResNet 引入了残差学习的概念,使残障学习能够通过解决深度卷积网络中消失的梯度问题(在第 2 节中讨论)来构建非常深的网络。
DenseNet 允许每个卷积直接访问输入和较低层的特征映射,从而进一步改进了 ResNet。 通过利用瓶颈和过渡层,还可以在深层网络中将参数的数量保持为较低。
但是,为什么这些是两个模型,而不是其他? 好吧,自从引入它们以来,已经有无数的模型,例如 ResNeXt [6]和 WideResNet [7],它们受到这两个网络使用的技术的启发。 同样,在了解 ResNet 和 DenseNet 的情况下,我们将能够使用他们的设计指南来构建我们自己的模型。 通过使用迁移学习,这也将使我们能够将预训练的 ResNet 和 DenseNet 模型用于我们自己的目的,例如对象检测和分割。 仅出于这些原因,以及与 Keras 的兼容性,这两个模型非常适合探索和补充本书的高级深度学习范围。
尽管本章的重点是深度神经网络; 在本章中,我们将讨论 Keras 的重要功能,称为函数式 API。 该 API 充当在tf.keras中构建网络的替代方法,使我们能够构建更复杂的网络,而这是顺序模型 API 无法实现的。 我们之所以专注于此 API 的原因是,它将成为构建诸如本章重点介绍的两个之类的深度网络的非常有用的工具。 建议您先完成“第 1 章”,“Keras 的高级深度学习介绍”,然后再继续本章,因为我们将参考在本章中探讨的入门级代码和概念,我们将它们带入了更高的层次。
本章的目的是介绍:
- Keras 中的函数式 API,以及探索运行该 API 的网络示例
tf.keras中的深度残差网络(ResNet 版本 1 和 2)实现tf.keras中密集连接卷积网络(DenseNet)的实现- 探索两种流行的深度学习模型,即 ResNet 和 DenseNet
让我们开始讨论函数式 API。
1. 函数式 API
在我们首先在“第 1 章”,“Keras 高级深度学习入门”的顺序模型 API 中,一层堆叠在另一层之上。 通常,将通过其输入和输出层访问模型。 我们还了解到,如果我们发现自己想要在网络中间添加辅助输入,或者甚至想在最后一层之前提取辅助输出,则没有简单的机制。
这种模式也有缺点。 例如,它不支持类似图的模型或行为类似于 Python 函数的模型。 此外,在两个模型之间共享层也很困难。函数式 API 解决了这些局限性,这就是为什么它对于想要使用深度学习模型的任何人来说都是至关重要的工具的原因。
函数式 API 遵循以下两个概念:
- 层是接受张量作为参数的实例。 一层的输出是另一个张量。 为了构建模型,层实例是通过输入和输出张量彼此链接的对象。 这与在顺序模型中堆叠多个层有类似的最终结果。 但是,使用层实例会使模型更容易具有辅助或多个输入和输出,因为每个层的输入/输出将易于访问。
- 模型是一个或多个输入张量和输出张量之间的函数。 在模型输入和输出之间,张量是通过层输入和输出张量彼此链接的层实例。 因此,模型是一个或多个输入层和一个或多个输出层的函数。 该模型实例将数据从输入流到输出流的形式的计算图形式化。
在完成函数式 API 模型的构建之后,训练和评估将由顺序模型中使用的相同函数执行。 为了说明,在函数式 API 中,二维卷积层Conv2D带有 32 个过滤器,并且x作为层输入张量,y作为层输出张量可以写为:
y = Conv2D(32)(x)
我们也可以堆叠多层来构建模型。 例如,我们可以使用函数式 API 重写 MNIST cnn-mnist-1.4.1.py上的卷积神经网络(CNN),如下所示:
“列表 2.1.1”:cnn-functional-2.1.1.py
import numpy as np
from tensorflow.keras.layers import Dense, Dropout, Input
from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten
from tensorflow.keras.models import Model
from tensorflow.keras.datasets import mnist
from tensorflow.keras.utils import to_categorical
# load MNIST dataset
(x_train, y_train), (x_test, y_test) = mnist.load_data()
# from sparse label to categorical
num_labels = len(np.unique(y_train))
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
# reshape and normalize input images
image_size = x_train.shape[1]
x_train = np.reshape(x_train,[-1, image_size, image_size, 1])
x_test = np.reshape(x_test,[-1, image_size, image_size, 1])
x_train = x_train.astype('float32') / 255
x_test = x_test.astype('float32') / 255
# network parameters
input_shape = (image_size, image_size, 1)
batch_size = 128
kernel_size = 3
filters = 64
dropout = 0.3
# use functional API to build cnn layers
inputs = Input(shape=input_shape)
y = Conv2D(filters=filters,
kernel_size=kernel_size,
activation='relu')(inputs)
y = MaxPooling2D()(y)
y = Conv2D(filters=filters,
kernel_size=kernel_size,
activation='relu')(y)
y = MaxPooling2D()(y)
y = Conv2D(filters=filters,
kernel_size=kernel_size,
activation='relu')(y)
# image to vector before connecting to dense layer
y = Flatten()(y)
# dropout regularization
y = Dropout(dropout)(y)
outputs = Dense(num_labels, activation='softmax')(y)
# build the model by supplying inputs/outputs
model = Model(inputs=inputs, outputs=outputs)
# network model in text
model.summary()
# classifier loss, Adam optimizer, classifier accuracy
model.compile(loss='categorical_crossentropy',
optimizer='adam',
metrics=['accuracy'])
# train the model with input images and labels
model.fit(x_train,
y_train,
validation_data=(x_test, y_test),
epochs=20,
batch_size=batch_size)
# model accuracy on test dataset
score = model.evaluate(x_test,
y_test,
batch_size=batch_size,
verbose=0)
print("\nTest accuracy: %.1f%%" % (100.0 * score[1]))
默认情况下,使用pool_size=2作为参数,因此MaxPooling2D已被删除。
在前面的清单中,每一层都是张量的函数。 每一层生成一个张量作为输出,该张量成为下一层的输入。 要创建此模型,我们可以调用Model()并提供inputs和outputs张量,或者提供张量列表。 其他一切保持不变。
类似于顺序模型,也可以使用fit()和evaluate()函数来训练和评估相同的列表。 实际上,Sequential类是Model类的子类。 我们需要记住,我们在fit()函数中插入了validation_data参数,以查看训练期间验证准确率的进度。 在 20 个周期内,准确率范围从 99.3% 到 99.4%。
创建两输入一输出模型
现在,我们将做一些令人兴奋的事情,创建一个具有两个输入和一个输出的高级模型。 在开始之前,重要的是要知道序列模型 API 是为仅构建 1 输入和 1 输出模型而设计的。
假设发明了一种用于 MNIST 数字分类的新模型,它称为 Y 网络,如图“图 2.1.1”所示。 Y 网络在左 CNN 分支和右 CNN 分支两次使用相同的输入。 网络使用concatenate层合并结果。 合并操作concatenate类似于沿连接轴堆叠两个相同形状的张量以形成一个张量。 例如,沿着最后一个轴连接两个形状为(3, 3, 16)的张量将导致一个形状为(3, 3, 32)的张量。
concatenate层之后的所有其他内容将与上一章的 CNN MNIST 分类器模型相同:Flatten,然后是Dropout,然后是Dense:

图 2.1.1:Y 网络接受两次相同的输入,但是在卷积网络的两个分支中处理输入。 分支的输出使用连接层进行合并。最后一层的预测将类似于上一章的 CNN MNIST 分类器模型。
为了提高“列表 2.1.1”中模型的表现,我们可以提出一些更改。 首先,Y 网络的分支将过滤器数量加倍,以补偿MaxPooling2D()之后特征映射尺寸的减半。 例如,如果第一个卷积的输出为(28, 28, 32),则在最大池化之后,新形状为(14, 14, 32)。 下一个卷积的过滤器大小为 64,输出尺寸为(14, 14, 64)。
其次,尽管两个分支的核大小相同,但右分支使用 2 的扩展率。“图 2.1.2”显示了不同的扩展率对大小为 3 的核的影响。 这个想法是,通过使用扩张率增加核的有效接受域大小,CNN 将使正确的分支能够学习不同的特征映射。 使用大于 1 的扩张速率是一种计算有效的近似方法,可以增加接收场的大小。 这是近似值,因为该核实际上不是成熟的核。 这是有效的,因为我们使用与膨胀率等于 1 相同的操作数。
要了解接受域的概念,请注意,当核计算特征映射的每个点时,其输入是前一层特征映射中的补丁,该补丁也取决于其前一层特征映射。 如果我们继续将此依赖关系一直跟踪到输入图像,则核将依赖于称为接收场的图像补丁。
我们将使用选项padding='same'来确保使用扩张的 CNN 时不会出现负张量。 通过使用padding='same',我们将使输入的尺寸与输出特征映射相同。 这是通过用零填充输入以确保输出的大小相同来实现的。

图 2.1.2:通过从 1 增加膨胀率,有效的核接受域大小也增加了
“列表 2.1.2”的cnn-y-network-2.1.2.py显示了使用函数式 API 的 Y 网络的实现。 两个分支由两个for循环创建。 两个分支期望输入形状相同。 两个for循环将创建两个Conv2D-Dropout-MaxPooling2D的三层栈。 虽然我们使用concatenate层组合了左右分支的输出,但我们还可以利用tf.keras的其他合并函数,例如add,dot和multiply。 合并函数的选择并非纯粹是任意的,而必须基于合理的模型设计决策。
在 Y 网络中,concatenate不会丢弃特征映射的任何部分。 取而代之的是,我们让Dense层确定如何处理连接的特征映射。
“列表 2.1.2”:cnn-y-network-2.1.2.py
import numpy as np
from tensorflow.keras.layers import Dense, Dropout, Input
from tensorflow.keras.layers import Conv2D, MaxPooling2D
from tensorflow.keras.layers import Flatten, concatenate
from tensorflow.keras.models import Model
from tensorflow.keras.datasets import mnist
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.utils import plot_model
# load MNIST dataset
(x_train, y_train), (x_test, y_test) = mnist.load_data()
# from sparse label to categorical
num_labels = len(np.unique(y_train))
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
# reshape and normalize input images
image_size = x_train.shape[1]
x_train = np.reshape(x_train,[-1, image_size, image_size, 1])
x_test = np.reshape(x_test,[-1, image_size, image_size, 1])
x_train = x_train.astype('float32') / 255
x_test = x_test.astype('float32') / 255
# network parameters
input_shape = (image_size, image_size, 1)
batch_size = 32
kernel_size = 3
dropout = 0.4
n_filters = 32
# left branch of Y network
left_inputs = Input(shape=input_shape)
x = left_inputs
filters = n_filters
# 3 layers of Conv2D-Dropout-MaxPooling2D
# number of filters doubles after each layer (32-64-128)
for i in range(3):
x = Conv2D(filters=filters,
kernel_size=kernel_size,
padding='same',
activation='relu')(x)
x = Dropout(dropout)(x)
x = MaxPooling2D()(x)
filters *= 2
# right branch of Y network
right_inputs = Input(shape=input_shape)
y = right_inputs
filters = n_filters
# 3 layers of Conv2D-Dropout-MaxPooling2Do
# number of filters doubles after each layer (32-64-128)
for i in range(3):
y = Conv2D(filters=filters,
kernel_size=kernel_size,
padding='same',
activation='relu',
dilation_rate=2)(y)
y = Dropout(dropout)(y)
y = MaxPooling2D()(y)
filters *= 2
# merge left and right branches outputs
y = concatenate([x, y])
# feature maps to vector before connecting to Dense
y = Flatten()(y)
y = Dropout(dropout)(y)
outputs = Dense(num_labels, activation='softmax')(y)
# build the model in functional API
model = Model([left_inputs, right_inputs], outputs)
# verify the model using graph
plot_model(model, to_file='cnn-y-network.png', show_shapes=True)
# verify the model using layer text description
model.summary()
# classifier loss, Adam optimizer, classifier accuracy
model.compile(loss='categorical_crossentropy',
optimizer='adam',
metrics=['accuracy'])
# train the model with input images and labels
model.fit([x_train, x_train],
y_train,
validation_data=([x_test, x_test], y_test),
epochs=20,
batch_size=batch_size)
# model accuracy on test dataset
score = model.evaluate([x_test, x_test],
y_test,
batch_size=batch_size,
verbose=0)
print("\nTest accuracy: %.1f%%" % (100.0 * score[1]))
退后一步,我们可以注意到 Y 网络期望有两个输入用于训练和验证。 输入是相同的,因此提供了[x_train, x_train]。
在 20 个周期的过程中,Y 网络的准确率为 99.4% 至 99.5%。 与 3 叠 CNN 相比,这是一个微小的改进,CNN 的精度在 99.3% 到 99.4% 之间。 但是,这是以更高的复杂度和两倍以上的参数数量为代价的。
下图“图 2.1.3”显示了 Keras 理解并由plot_model()函数生成的 Y 网络的架构:

图 2.1.3:清单 2.1.2 中实现的 CNN Y 网络
总结我们对函数式 API 的了解。 我们应该花时间记住本章的重点是构建深度神经网络,特别是 ResNet 和 DenseNet。 因此,我们只讨论构建它们所需的函数式 API 材料,因为涵盖整个的 API 将超出本书的范围。 话虽如此,让我们继续讨论 ResNet。
有关函数式 API 的其他信息,请阅读这里。
2. 深度残差网络(ResNet)
深度网络的一个主要优点是,它们具有从输入图和特征映射学习不同级别表示的能力。 在分类,分割,检测和许多其他计算机视觉问题中,学习不同的特征映射通常可以提高性能。
但是,您会发现训练深层网络并不容易,因为在反向传播过程中,梯度可能会随着浅层中的深度消失(或爆炸)。“图 2.2.1”说明了梯度消失的问题。 通过从输出层向所有先前层的反向传播来更新网络参数。 由于反向传播是基于链法则的,因此当梯度到达浅层时,梯度会逐渐减小。 这是由于小数的乘法,尤其是对于小损失函数和参数值。
乘法运算的数量将与网络深度成正比。 还要注意的是,如果梯度降低,则不会适当更新参数。
因此,网络将无法提高其表现。

图 2.2.1:深层网络中的一个常见问题是,在反向传播过程中,梯度在到达浅层时会消失。
为了减轻深度网络中梯度的降级,ResNet 引入了深度残差学习框架的概念。 让我们分析一个块:深度网络的一小部分。
“图 2.2.2”显示了典型 CNN 块和 ResNet 残差块之间的比较。 ResNet 的想法是,为了防止梯度降级,我们将让信息通过快捷连接流到浅层。

图 2.2.2:典型 CNN 中的块与 ResNet 中的块之间的比较。 为了防止反向传播期间梯度的降低,引入了快捷连接。
接下来,我们将在中讨论两个模块之间的差异,以了解更多详细信息。“图 2.2.3”显示了另一个常用的深层网络 VGG [3]和 ResNet 的 CNN 块的更多详细信息。 我们将层特征映射表示为x。 层l的特征映射为x[l]。 在 CNN 层中的操作是 Conv2D 批量规范化(BN)- ReLU。
假设我们以H() = Conv2D-Batch Normalization(BN)-ReLU的形式表示这组操作; 然后:
x[l-1] = H(x[l-2])(公式 2.2.1)
x[l] = H(x[l-1])(方程式 2.2.2)
换句话说,通过H() =Conv2D-Batch Normalization(BN)-ReLU将l-2层上的特征映射转换为x[l-1]。 应用相同的操作集将x[l-1]转换为x[l]。 换句话说,如果我们有一个 18 层的 VGG,则在将输入图像转换为第 18 个层特征映射之前,有 18 个H()操作。
一般而言,我们可以观察到l层输出特征映射仅直接受先前的特征映射影响。 同时,对于 ResNet:
x[l-1] = H(x[l-2])(公式 2.2.3)
x[l] = ReLU(F(x[l-1]) + x[l-2])(公式 2.2.4)

图 2.2.3:普通 CNN 块和残差块的详细层操作
F(x[l-1])由Conv2D-BN制成,这也被称为残差映射。 +符号是快捷方式连接和F(x[l-1])输出之间的张量元素加法。 快捷连接不会增加额外的参数,也不会增加计算复杂度。
可以通过add()合并函数在tf.keras中实现添加操作。 但是,F(x[l-1])和x[l-2]应该具有相同的尺寸。
如果尺寸不同,例如,当更改特征映射尺寸时,我们应该在x[l-2]上进行线性投影以匹配尺寸F([l-1])的含量。 在原始论文中,当特征映射的大小减半时,情况的线性投影是通过Conv2D和 1 strides=2核完成的。
在“第 1 章”,“Keras 高级深度学习”,我们讨论了stride > 1等效于在卷积期间跳过像素。 例如,如果strides=2,则在卷积过程中滑动核时,可以跳过其他每个像素。
前面的“公式 2.2.3”和“公式 2.2.4”都对 ResNet 残余块操作进行建模。 他们暗示,如果可以训练较深的层具有较少的误差,则没有理由为什么较浅的层应具有较高的误差。
知道 ResNet 的基本构建块后,我们就可以设计一个深度残差网络来进行图像分类。 但是,这一次,我们将处理更具挑战性的数据集。
在我们的示例中,我们将考虑 CIFAR10,它是原始论文所基于的数据集之一。 在此示例中,tf.keras提供了一个 API,可以方便地访问 CIFAR10 数据集,如下所示:
from tensorflow.keras.datasets import cifar10
(x_train, y_train), (x_test, y_test) = cifar10.load_data()
与 MNIST 一样,CIFAR10 数据集也有 10 个类别。 数据集是对应于飞机,汽车,鸟,猫,鹿,狗,青蛙,马,船和卡车的小型(32×32)RGB 真实世界图像的集合。 10 个类别中的每个类别。“图 2.2.4”显示了来自 CIFAR10 的示例图像。
在数据集中,有 50,000 个标记的训练图像和 10,000 个标记的测试图像用于验证:

图 2.2.4:来自 CIFAR10 数据集的样本图像。 完整的数据集包含 50,000 张标签的训练图像和 10,000 张标签的测试图像以进行验证。
对于 CIFAR10 数据,可以使用“表 2.2.1”中所示的不同网络架构来构建 ResNet。“表 2.2.1”表示我们有三组残差块。 每组具有对应于n个残余块的2n层。32×32的额外层是输入图像的第一层。
| 层 | 输出大小 | 过滤器尺寸 | 操作 |
|---|---|---|---|
| 卷积 | 32 × 32 |
16 | 3 x 3 Conv2D |
| 残差块(1) | 32 × 32 |
![]() |
|
| 过渡层(1) | 32 × 32 |
{1 x 1 Conv2D, stride = 2} |
|
16 × 16 |
|||
| 残差块(2) | 16 × 16 |
32 | ![]() |
| 过渡层(2) | 16 × 16 |
||
8 × 8 |
|||
| 残差块(3) | 8 × 8 |
64 | ![]() |
| 平均池化 | 1 × 1 |
表 2.2.1:ResNet 网络架构配置
核大小为 3,不同大小的两个特征映射之间的过渡除外,该过渡实现了线性映射。 例如,核大小为 1 的Conv2D和strides=2。 为了与 DenseNet 保持一致,当我们连接两个大小不同的剩余块时,我们将使用项Transition层。
ResNet 使用kernel_initializer='he_normal'以便在进行反向传播时帮助收敛[1]。 最后一层由AveragePooling2D-Flatten-Dense制成。 在这一点上值得注意的是 ResNet 不使用丢弃。 似乎add 合并操作和1 x 1卷积具有自正则化效果。“图 2.2.5”显示了 CIFAR10 数据集的 ResNet 模型架构,如“表 2.2.1”中所述。

图 2.2.5:用于 CIFAR10 数据集分类的 ResNet 的模型架构
以下代码段显示了tf.keras中的部分 ResNet 实现。 该代码已添加到 Keras GitHub 存储库中。 从“表 2.2.2”(稍后显示)中,我们还可以看到,通过修改n的值,我们可以增加网络的深度。
例如,对于n = 18,我们已经拥有 ResNet110,这是一个具有 110 层的深度网络。 要构建 ResNet20,我们使用n = 3:
n = 3
# model version
# orig paper: version = 1 (ResNet v1),
# improved ResNet: version = 2 (ResNet v2)
version = 1
# computed depth from supplied model parameter n
if version == 1:
depth = n * 6 + 2
elif version == 2:
depth = n * 9 + 2
if version == 2:
model = resnet_v2(input_shape=input_shape, depth=depth)
else:
model = resnet_v1(input_shape=input_shape, depth=depth)
resnet_v1()方法是 ResNet 的模型构建器。 它使用工具函数resnet_layer(),来帮助构建Conv2D-BN-ReLU的栈。
它将称为版本 1,正如我们将在下一节中看到的那样,提出了一种改进的 ResNet,该版本称为 ResNet 版本 2 或 v2。 通过 ResNet,ResNet v2 改进了残差块设计,从而提高了表现。
以下清单显示了resnet-cifar10-2.2.1.py的部分代码,它是 ResNet v1 的tf.keras模型实现。
“列表 2.2.1”:resnet-cifar10-2.2.1.py
def resnet_v1(input_shape, depth, num_classes=10):
"""ResNet Version 1 Model builder [a]
Stacks of 2 x (3 x 3) Conv2D-BN-ReLU
Last ReLU is after the shortcut connection.
At the beginning of each stage, the feature map size is halved
(downsampled) by a convolutional layer with strides=2, while
the number of filters is doubled. Within each stage,
the layers have the same number filters and the
same number of filters.
Features maps sizes:
stage 0: 32x32, 16
stage 1: 16x16, 32
stage 2: 8x8, 64
The Number of parameters is approx the same as Table 6 of [a]:
ResNet20 0.27M
ResNet32 0.46M
ResNet44 0.66M
ResNet56 0.85M
ResNet110 1.7M
Arguments:
input_shape (tensor): shape of input image tensor
depth (int): number of core convolutional layers
num_classes (int): number of classes (CIFAR10 has 10)
Returns:
model (Model): Keras model instance
"""
if (depth - 2) % 6 != 0:
raise ValueError('depth should be 6n+2 (eg 20, 32, in [a])')
# Start model definition.
num_filters = 16
num_res_blocks = int((depth - 2) / 6)
inputs = Input(shape=input_shape)
x = resnet_layer(inputs=inputs)
# instantiate the stack of residual units
for stack in range(3):
for res_block in range(num_res_blocks):
strides = 1
# first layer but not first stack
if stack > 0 and res_block == 0:
strides = 2 # downsample
y = resnet_layer(inputs=x,
num_filters=num_filters,
strides=strides)
y = resnet_layer(inputs=y,
num_filters=num_filters,
activation=None)
# first layer but not first stack
if stack > 0 and res_block == 0:
# linear projection residual shortcut
# connection to match changed dims
x = resnet_layer(inputs=x,
num_filters=num_filters,
kernel_size=1,
strides=strides,
activation=None,
batch_normalization=False)
x = add([x, y])
x = Activation('relu')(x)
num_filters *= 2
# add classifier on top.
# v1 does not use BN after last shortcut connection-ReLU
x = AveragePooling2D(pool_size=8)(x)
y = Flatten()(x)
outputs = Dense(num_classes,
activation='softmax',
kernel_initializer='he_normal')(y)
# instantiate model.
model = Model(inputs=inputs, outputs=outputs)
return model
ResNet 在n的各种值上的表现显示在“表 2.2.2”中。
| 层 | n |
CIFAR10 的准确率百分比(原始论文) | CIFAR10 的准确率百分比(本书) |
|---|---|---|---|
| ResNet20 | 3 | 91.25 | 92.16 |
| ResNet32 | 5 | 92.49 | 92.46 |
| ResNet44 | 7 | 92.83 | 92.50 |
| ResNet56 | 9 | 93.03 | 92.71 |
| ResNet110 | 18 | 93.57 | 92.65 |
表 2.2.2:针对不同的 n 值,使用 CIFAR10 验证的 ResNet 架构
与 ResNet 的原始实现有一些细微的差异。 特别是,我们不使用 SGD,而是使用 Adam。 这是因为 ResNet 更容易与 Adam 融合。 我们还将使用学习率调度器lr_schedule(),以便将lr的减少量从默认的1e-3缩短为 80、120、160 和 180 个周期。 在训练期间的每个周期之后,都会将lr_schedule()函数作为回调变量的一部分进行调用。
每当验证准确率方面取得进展时,另一个回调将保存检查点。 训练深层网络时,保存模型或权重检查点是一个好习惯。 这是因为训练深度网络需要大量时间。
当您想使用网络时,您只需要做的就是重新加载检查点,然后恢复经过训练的模型。 这可以通过调用tf.keras load_model()来完成。 包含lr_reducer()函数。 如果指标在排定的减少之前已稳定在上,则如果在patience = 5周期之后验证损失没有改善,则此回调将以参数中提供的某个因子来降低学习率。
调用model.fit()方法时,会提供回调变量。 与原始论文相似,tf.keras实现使用数据扩充ImageDataGenerator()来提供其他训练数据作为正则化方案的一部分。 随着训练数据数量的增加,概括性将会提高。
例如,简单的数据扩充就是翻转一条狗的照片,如图“图 2.2.6”(horizontal_flip = True)所示。 如果它是狗的图像,则翻转的图像仍然是狗的图像。 您还可以执行其他变换,例如缩放,旋转,变白等等,并且标签将保持不变:

图 2.2.6:一个简单的数据扩充就是翻转原始图像
准确复制原始论文的实现通常很困难。 在本书中,我们使用了不同的优化器和数据扩充。 这可能会导致本书中所实现的tf.keras ResNet 和原始模型中的表现略有不同。
在 ResNet [4]的第二篇论文发布之后,本节中介绍的原始模型为,称为 ResNet v1。 改进的 ResNet 通常称为 ResNet v2,我们将在下一部分讨论。
3. ResNet v2
ResNet v2 的改进主要体现在残块中各层的排列中,如图“图 2.3.1”所示。
ResNet v2 的主要变化是:
- 使用
1 x 1 – 3 x 3 – 1 × 1的栈BN-ReLU-Conv2D - 批量标准化和 ReLU 激活先于二维卷积

图 2.3.1:ResNet v1 和 ResNet v2 之间的剩余块比较
ResNet v2 也以与resnet-cifar10-2.2.1.py相同的代码实现,如“列表 2.2.1”所示:
“列表 2.2.1”:resnet-cifar10-2.2.1.py
def resnet_v2(input_shape, depth, num_classes=10):
"""ResNet Version 2 Model builder [b]
Stacks of (1 x 1)-(3 x 3)-(1 x 1) BN-ReLU-Conv2D or
also known as bottleneck layer.
First shortcut connection per layer is 1 x 1 Conv2D.
Second and onwards shortcut connection is identity.
At the beginning of each stage,
the feature map size is halved (downsampled)
by a convolutional layer with strides=2,
while the number of filter maps is
doubled. Within each stage, the layers have
the same number filters and the same filter map sizes.
Features maps sizes:
conv1 : 32x32, 16
stage 0: 32x32, 64
stage 1: 16x16, 128
stage 2: 8x8, 256
Arguments:
input_shape (tensor): shape of input image tensor
depth (int): number of core convolutional layers
num_classes (int): number of classes (CIFAR10 has 10)
Returns:
model (Model): Keras model instance
"""
if (depth - 2) % 9 != 0:
raise ValueError('depth should be 9n+2 (eg 110 in [b])')
# start model definition.
num_filters_in = 16
num_res_blocks = int((depth - 2) / 9)
inputs = Input(shape=input_shape)
# v2 performs Conv2D with BN-ReLU
# on input before splitting into 2 paths
x = resnet_layer(inputs=inputs,
num_filters=num_filters_in,
conv_first=True)
# instantiate the stack of residual units
for stage in range(3):
for res_block in range(num_res_blocks):
activation = 'relu'
batch_normalization = True
strides = 1
if stage == 0:
num_filters_out = num_filters_in * 4
# first layer and first stage
if res_block == 0:
activation = None
batch_normalization = False
else:
num_filters_out = num_filters_in * 2
# first layer but not first stage
if res_block == 0:
# downsample
strides = 2
# bottleneck residual unit
y = resnet_layer(inputs=x,
num_filters=num_filters_in,
kernel_size=1,
strides=strides,
activation=activation,
batch_normalization=batch_normalization,
conv_first=False)
y = resnet_layer(inputs=y,
num_filters=num_filters_in,
conv_first=False)
y = resnet_layer(inputs=y,
num_filters=num_filters_out,
kernel_size=1,
conv_first=False)
if res_block == 0:
# linear projection residual shortcut connection
# to match changed dims
x = resnet_layer(inputs=x,
num_filters=num_filters_out,
kernel_size=1,
strides=strides,
activation=None,
batch_normalization=False)
x = add([x, y])
num_filters_in = num_filters_out
# add classifier on top.
# v2 has BN-ReLU before Pooling
x = BatchNormalization()(x)
x = Activation('relu')(x)
x = AveragePooling2D(pool_size=8)(x)
y = Flatten()(x)
outputs = Dense(num_classes,
activation='softmax',
kernel_initializer='he_normal')(y)
# instantiate model.
model = Model(inputs=inputs, outputs=outputs)
return model
下面的代码显示了 ResNet v2 的模型构建器。 例如,要构建 ResNet110 v2,我们将使用n = 12和version = 2:
n = 12
# model version
# orig paper: version = 1 (ResNet v1),
# improved ResNet: version = 2 (ResNet v2)
version = 2
# computed depth from supplied model parameter n
if version == 1:
depth = n * 6 + 2
elif version == 2:
depth = n * 9 + 2
if version == 2:
model = resnet_v2(input_shape=input_shape, depth=depth)
else:
model = resnet_v1(input_shape=input_shape, depth=depth)
ResNet v2 的准确率显示在下面的“表 2.3.1”中:
| 层 | n |
CIFAR10 的准确率百分比(原始论文) | CIFAR10 的准确率百分比(本书) |
|---|---|---|---|
| ResNet56 | 9 | 不适用 | 93.01 |
| ResNet110 | 18 | 93.63 | 93.15 |
表 2.3.1:在 CIFAR10 数据集上验证的 ResNet v2 架构
在 Keras 应用包中,已实现某些 ResNet v1 和 v2 模型(例如:50、101、152)。 这些是替代的实现方式,其中预训练的权重不清楚,可以轻松地重新用于迁移学习。 本书中使用的模型在层数方面提供了灵活性。
我们已经完成了对最常用的深度神经网络之一 ResNet v1 和 v2 的讨论。 在以下部分中,将介绍另一种流行的深度神经网络架构 DenseNet。
4. 紧密连接的卷积网络(DenseNet)

图 2.4.1:DenseNet 中的一个 4 层Dense块,每层的输入均由所有先前的特征映射组成。
DenseNet 使用另一种方法攻击梯度消失的问题。 代替使用快捷方式连接,所有先前的特征映射都将成为下一层的输入。 上图显示了一个Dense块中密集互连的示例。
为简单起见,在此图中,我们仅显示四层。 注意,层l的输入是所有先前特征映射的连接。 如果用操作H表示BN-ReLU-Conv2D(x),则层l的输出为:
x[l] = H(x[0], x[1], x[2], x[l-1])(公式 2.4.1)
Conv2D使用大小为 3 的核。每层生成的特征映射的数量称为增长率k。 通常,在 Huang 等人的论文“密集连接卷积网络”中,也使用k = 12,但是k = 24 [5]。 因此,如果特征映射x₀的数量为k₀,则“图 2.4.1”中,4 层Dense块的末尾的特征映射总数为4 x k + k[0]。
DenseNet 建议在Dense块之前加上BN-ReLU-Conv2D,以及许多是增长率两倍的特征映射k₀= 2 xk。 在Dense块的末尾,特征映射的总数将为4 x 12 + 2 x 12 = 72。
在输出层,DenseNet 建议我们在具有softmax层的Dense()之前执行平均池化。 如果未使用数据扩充,则必须在Dense块Conv2D之后跟随一个丢弃层。
随着网络的深入,将出现两个新问题。 首先,由于每一层都贡献了k特征映射,因此l层的输入数量为(l – 1) x k + k[0]。 特征映射可以在深层中快速增长,从而减慢了计算速度。 例如,对于 101 层网络,对于k = 12,这将是1200 + 24 = 1224。
其次,类似于 ResNet,随着网络的不断深入,特征映射的大小将减小,从而增加核的接收域大小。 如果 DenseNet 在合并操作中使用连接,则必须协调大小上的差异。
为了防止特征映射的数量增加到计算效率低的程度,DenseNet 引入了Bottleneck层,如图“图 2.4.2”所示。 这个想法是,在每次连接之后,现在应用1 x 1卷积,其过滤器大小等于4k。 这种降维技术阻止了Conv2D(3)处理的特征映射的数量快速增加。

图 2.4.2:DenseNet 的 Dense 块中的一层,带有和不带有瓶颈层 BN-ReLU-Conv2D(1)。 为了清楚起见,我们将核大小作为 Conv2D 的参数。
然后Bottleneck层将 DenseNet 层修改为BN-ReLU-Conv2D(1)-BN- ReLU-Conv2D(3),而不仅仅是BN-ReLU-Conv2D(3)。 为了清楚起见,我们将核大小作为Conv2D的参数。 在瓶颈层,每个Conv2D(3)仅处理 4 个k特征映射,而不是(l – 1) x k + k[0]的,对于层l。 例如,对于 101 层网络,最后一个Conv2D(3)的输入仍然是k = 12而不是先前计算的 1224 的 48 个特征映射。
为了解决特征映射大小不匹配的问题,DenseNet 将深度网络划分为多个 Dense 块,这些块通过过渡层连接在一起,如图“图 2.4.3”所示。 在每个Dense块中,特征映射的大小(即宽度和高度)将保持不变。
过渡层的作用是在两个Dense块之间从一个特征映射大小过渡到较小的特征映射大小。 尺寸通常减少一半。 这是通过平均池化层完成的。 例如,默认值为pool_size=2的AveragePooling2D会将大小从(64, 64, 256)减小为(32, 32, 256)。 过渡层的输入是前一个Dense块中最后一个连接层的输出。

图 2.4.3:两个密集块之间的过渡层
但是,在将特征映射传递到平均池之前,使用Conv2D(1)将其数量减少某个压缩因子0 < θ < 1。DenseNet 在实验中使用θ = 0.5。 例如,如果先前Dense块的最后连接的输出是(64, 64, 512),则在Conv2D(1)之后,特征映射的新尺寸将是(64, 64, 256)。 当压缩和降维放在一起时,过渡层由BN-Conv2D(1)-AveragePooling2D层组成。 实际上,批量归一化在卷积层之前。
现在,我们已经涵盖了 DenseNet 的重要概念。 接下来,我们将为tf.keras中的 CIFAR10 数据集构建并验证 DenseNet-BC。
为 CIFAR10 构建 100 层 DenseNet-BC
现在,我们将要为 CIFAR10 数据集构建一个具有 100 层的 DenseNet-BC(瓶颈压缩), 我们在上面讨论过。
“表 2.4.1”显示了模型配置,而“图 2.4.4”显示了模型架构。 清单为我们展示了具有 100 层的 DenseNet-BC 的部分 Keras 实现。 我们需要注意的是,我们使用RMSprop,因为在使用 DenseNet 时,它的收敛性优于 SGD 或 Adam。
| 层 | 输出大小 | DenseNet-100 BC |
|---|---|---|
| 卷积 | 32 x 32 |
3 x 3 Conv2D |
| 密集块(1) | 32 x 32 |
![]() |
| 过渡层(1) | 32 x 32 |
![]() |
16 x 16 |
||
| 密集块(2) | 16 x 16 |
![]() |
| 过渡层(2) | 16 x 16 |
![]() |
8 x 8 |
||
| 密集块(3) | 8 x 8 |
![]() |
| 平均池化 | 1 x 1 |
8 x 8 AveragePooling2D |
| 分类层 | Flatten-Dense(10)-softmax |
表 2.4.1:100 层的 DenseNet-BC 用于 CIFAR10 分类
将从配置移至架构:

图 2.4.4:用于 CIFAR10 分类的 100 个层的 DenseNet-BC 模型架构
下面“列表 2.4.1”是具有 100 层的 DenseNet-BC 的部分 Keras 实现,如“表 2.4.1”所示。
“列表 2.4.1”:densenet-cifar10-2.4.1.py
# start model definition
# densenet CNNs (composite function) are made of BN-ReLU-Conv2D
inputs = Input(shape=input_shape)
x = BatchNormalization()(inputs)
x = Activation('relu')(x)
x = Conv2D(num_filters_bef_dense_block,
kernel_size=3,
padding='same',
kernel_initializer='he_normal')(x)
x = concatenate([inputs, x])
# stack of dense blocks bridged by transition layers
for i in range(num_dense_blocks):
# a dense block is a stack of bottleneck layers
for j in range(num_bottleneck_layers):
y = BatchNormalization()(x)
y = Activation('relu')(y)
y = Conv2D(4 * growth_rate,
kernel_size=1,
padding='same',
kernel_initializer='he_normal')(y)
if not data_augmentation:
y = Dropout(0.2)(y)
y = BatchNormalization()(y)
y = Activation('relu')(y)
y = Conv2D(growth_rate,
kernel_size=3,
padding='same',
kernel_initializer='he_normal')(y)
if not data_augmentation:
y = Dropout(0.2)(y)
x = concatenate([x, y])
# no transition layer after the last dense block
if i == num_dense_blocks - 1:
continue
# transition layer compresses num of feature maps and # reduces the size by 2
num_filters_bef_dense_block += num_bottleneck_layers * growth_rate
num_filters_bef_dense_block = int(num_filters_bef_dense_block * compression_factor)
y = BatchNormalization()(x)
y = Conv2D(num_filters_bef_dense_block,
kernel_size=1,
padding='same',
kernel_initializer='he_normal')(y)
if not data_augmentation:
y = Dropout(0.2)(y)
x = AveragePooling2D()(y)
# add classifier on top
# after average pooling, size of feature map is 1 x 1
x = AveragePooling2D(pool_size=8)(x)
y = Flatten()(x)
outputs = Dense(num_classes,
kernel_initializer='he_normal',
activation='softmax')(y)
# instantiate and compile model
# orig paper uses SGD but RMSprop works better for DenseNet
model = Model(inputs=inputs, outputs=outputs)
model.compile(loss='categorical_crossentropy',
optimizer=RMSprop(1e-3),
metrics=['accuracy'])
model.summary()
训练 DenseNet 的tf.keras实现 200 个周期,可以达到 93.74% 的准确率,而本文中报道的是 95.49%。 使用数据扩充。 我们在 ResNet v1 / v2 中为 DenseNet 使用了相同的回调函数。
对于更深的层,必须使用 Python 代码上的表来更改growth_rate和depth变量。 但是,如本文所述,以深度 190 或 250 训练网络将需要大量时间。 为了给我们一个训练时间的想法,每个周期在 1060Ti GPU 上运行大约一个小时。 与 ResNet 相似,Keras 应用包具有针对 DenseNet 121 及更高版本的预训练模型。
DenseNet 完成了我们对深度神经网络的讨论。 与 ResNet 一起,这两个网络已成为许多下游任务中不可或缺的特征提取器网络。
5. 总结
在本章中,我们介绍了函数式 API 作为使用tf.keras构建复杂的深度神经网络模型的高级方法。 我们还演示了如何使用函数式 API 来构建多输入单输出 Y 网络。 与单分支 CNN 网络相比,该网络具有更高的准确率。 在本书的其余部分中,我们将发现在构建更复杂和更高级的模型时必不可少的函数式 API。 例如,在下一章中,函数式 API 将使我们能够构建模块化编码器,解码器和自编码器。
我们还花费了大量时间探索两个重要的深度网络 ResNet 和 DenseNet。 这两个网络不仅用于分类,而且还用于其他领域,例如分段,检测,跟踪,生成和视觉语义理解。 在“第 11 章”,“对象检测”和“第 12 章”,“语义分割”中,我们将使用 ResNet 进行对象检测和分割。 我们需要记住,与仅仅遵循原始实现相比,更仔细地了解 ResNet 和 DenseNet 中的模型设计决策至关重要。 这样,我们就可以将 ResNet 和 DenseNet 的关键概念用于我们的目的。
6. 参考
Kaiming He et al. Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification. Proceedings of the IEEE international conference on computer vision, 2015 (https://www.cv-foundation.org/openaccess/content_iccv_2015/papers/He_Delving_Deep_into_ICCV_2015_paper.pdfspm=5176.100239.blogcont55892.28.pm8zm1&file=He_Delving_Deep_into_ICCV_2015_paper.pdf).Kaiming He et al. Deep Residual Learning for Image Recognition. Proceedings of the IEEE conference on computer vision and pattern recognition, 2016a (http://openaccess.thecvf.com/content_cvpr_2016/papers/He_Deep_Residual_Learning_CVPR_2016_paper.pdf).Karen Simonyan and Andrew Zisserman. Very Deep Convolutional Networks for Large-Scale Image Recognition. ICLR, 2015 (https://arxiv.org/pdf/1409.1556/).Kaiming He et al. Identity Mappings in Deep Residual Networks. European Conference on Computer Vision. Springer International Publishing, 2016b (https://arxiv.org/pdf/1603.05027.pdf).Gao Huang et al. Densely Connected Convolutional Networks. Proceedings of the IEEE conference on computer vision and pattern recognition, 2017 (http://openaccess.thecvf.com/content_cvpr_2017/papers/Huang_Densely_Connected_Convolutional_CVPR_2017_paper.pdf).Saining Xie et al. Aggregated Residual Transformations for Deep Neural Networks. Computer Vision and Pattern Recognition (CVPR), 2017 IEEE Conference on. IEEE, 2017 (http://openaccess.thecvf.com/content_cvpr_2017/papers/Xie_Aggregated_Residual_Transformations_CVPR_2017_paper.pdf).Zagoruyko, Sergey, and Nikos Komodakis. "Wide residual networks." arXiv preprint arXiv:1605.07146 (2016).
三、自编码器
在上一章“第 2 章”,“深度神经网络”中,我们介绍了深度神经网络的概念。 现在,我们将继续研究自编码器,它是一种神经网络架构,试图找到给定输入数据的压缩表示形式。
与前面的章节相似,输入数据可以采用多种形式,包括语音,文本,图像或视频。 自编码器将尝试查找表示形式或一段代码,以便对输入数据执行有用的转换。 例如,当对自编码器进行降噪处理时,神经网络将尝试找到可用于将噪声数据转换为干净数据的代码。 嘈杂的数据可以是带有静态噪声的录音形式,然后将其转换为清晰的声音。 自编码器将自动从数据中自动学习代码,而无需人工标记。 这样,自编码器可以在无监督学习算法下分类为。
在本书的后续章节中,我们将研究生成对抗网络(GAN)和变分自编码器(VAE) 也是无监督学习算法的代表形式。 这与我们在前几章中讨论过的监督学习算法相反,后者需要人工标注。
总之,本章介绍:
- 自编码器的原理
- 如何使用
tf.keras实现自编码器 - 去噪和着色自编码器的实际应用
让我们从了解自编码器是什么以及自编码器的原理开始。
1. 自编码器的原理
自编码器以最简单的形式通过尝试将输入复制到输出中来学习表示形式或代码。 但是,使用自编码器并不像将输入复制到输出那样简单。 否则,神经网络将无法发现输入分布中的隐藏结构。
自编码器将输入分布编码为低维张量,通常采用向量形式。 这将近似通常称为潜在表示,代码或向量的隐藏结构。 该处理构成编码部分。 然后,潜在向量将由解码器部分解码,以恢复原始输入。
由于潜向量是输入分布的低维压缩表示,因此应该期望解码器恢复的输出只能近似输入。 输入和输出之间的差异可以通过损失函数来衡量。
但是为什么我们要使用自编码器? 简而言之,自编码器在原始形式或更复杂的神经网络的一部分中都有实际应用。
它们是了解深度学习的高级主题的关键工具,因为它们为我们提供了适合密度估计的低维数据表示。 此外,可以有效地对其进行处理以对输入数据执行结构化操作。 常见的操作包括去噪,着色,特征级算术,检测,跟踪和分割,仅举几例。
在本节中,我们将介绍自编码器的原理。 我们将使用前几章介绍的带有 MNIST 数据集的自编码器。
首先,我们需要意识到自编码器具有两个运算符,它们是:
- 编码器:这会将输入
x转换为低维潜向量z = f(x)。 由于潜向量是低维的,编码器被迫仅学习输入数据的最重要特征。 例如,在 MNIST 数字的情况下,要学习的重要特征可能包括书写风格,倾斜角度,笔触圆度,厚度等。 从本质上讲,这些是代表数字 0 至 9 所需的最重要的信息位。 - 解码器:这尝试从潜在向量
g(z) = x中恢复输入。
尽管潜向量的维数较小,但它的大小足以使解码器恢复输入数据。
解码器的目标是使x_tilde尽可能接近x。 通常,编码器和解码器都是非线性函数。z的尺寸是可以表示的重要特征数量的度量。 该维数通常比输入维数小得多,以提高效率,并为了限制潜在代码仅学习输入分布的最显着属性[1]。
当潜码的维数明显大于x时,自编码器倾向于记忆输入。
合适的损失函数L(x, x_tilde)衡量输入x与输出(即)恢复后的输入x_tilde的相异程度。 如下式所示,均方误差(MSE)是此类损失函数的一个示例:
(Equation 3.1.1)
在此示例中,m是输出尺寸(例如,在 MNIST 中,m = width × height × channels = 28 × 28 × 1 = 784)。xᵢ和x_tilde[i]分别是x和x_tilde的元素。 由于损失函数是输入和输出之间差异的量度,因此我们可以使用替代的重建损失函数,例如二进制交叉熵或结构相似性指数(SSIM)。
与其他神经网络类似,自编码器会在训练过程中尝试使此误差或损失函数尽可能小。“图 3.1.1”显示了一个自编码器。 编码器是将输入x压缩为低维潜向量z的函数。 该潜向量代表输入分布的重要特征。 然后,解码器尝试以x_tilde的形式从潜向量中恢复原始输入。

图 3.1.1:自编码器的框图
为了将自编码器置于上下文中,x可以是尺寸为28×28×1 = 784的 MNIST 数字。编码器将输入转换为低维的z,可以是 16 维潜在向量。 解码器将尝试从z中以x_tilde的形式恢复输入。
在视觉上,每个 MNIST 数字x看起来都类似于x_tilde。“图 3.1.2”向我们演示了此自编码过程。

图 3.1.2:带有 MNIST 数字输入和输出的自编码器。 潜在向量为 16 角
我们可以看到,虽然解码后的数字 7 并不完全相同,但仍然足够接近。
由于编码器和解码器都是非线性函数,因此我们可以使用神经网络来实现两者。 例如,在 MNIST 数据集中,自编码器可以由 MLP 或 CNN 实现。 通过最小化通过反向传播的损失函数,可以训练自编码器。 与其他神经网络类似,反向传播的要求是损失函数必须是可微的。
如果将输入视为分布,则可以将编码器解释为分布的编码器,p(z | x),将解码器解释为分布的解码器p(x | z)。 自编码器的损失函数表示为:
(Equation 3.1.2)
损失函数只是意味着我们要在给定潜在向量分布的情况下最大程度地恢复输入分布的机会。 如果假设解码器的输出分布为为高斯,则损失函数归结为 MSE,因为:
(Equation 3.1.3)
在此示例中,N(x[i]; x_tilde[i], σ²表示平均值为x_tilde[i]且方差为σ²的高斯分布。 假设恒定方差。 假定解码器输出x_tilde[i]是独立的。m是输出尺寸。
了解自编码器背后的原理将有助于我们执行代码。 在下一节中,我们将研究如何使用tf.keras函数式 API 来构建编码器,解码器和自编码器。
2. 使用 Keras 构建自编码器
现在,我们要使用进行一些令人兴奋的事情,使用tf.keras库构建一个自编码器。 为了简单起见,我们将使用 MNIST 数据集作为第一组示例。 然后,自编码器将根据输入数据生成潜向量,并使用解码器恢复输入。 在该第一示例中,潜向量是 16 维。
首先,我们将通过构建编码器来实现自编码器。
“列表 3.2.1”显示了将 MNIST 数字压缩为 16 维潜在向量的编码器。 编码器是两个Conv2D的栈。 最后阶段是具有 16 个单元的Dense层,以生成潜向量。
“列表 3.2.1”:autoencoder-mnist-3.2.1.py
from tensorflow.keras.layers import Dense, Input
from tensorflow.keras.layers import Conv2D, Flatten
from tensorflow.keras.layers import Reshape, Conv2DTranspose
from tensorflow.keras.models import Model
from tensorflow.keras.datasets import mnist
from tensorflow.keras.utils import plot_model
from tensorflow.keras import backend as K
import numpy as np
import matplotlib.pyplot as plt
# load MNIST dataset
(x_train, _), (x_test, _) = mnist.load_data()
# reshape to (28, 28, 1) and normalize input images
image_size = x_train.shape[1]
x_train = np.reshape(x_train, [-1, image_size, image_size, 1])
x_test = np.reshape(x_test, [-1, image_size, image_size, 1])
x_train = x_train.astype('float32') / 255
x_test = x_test.astype('float32') / 255
# network parameters
input_shape = (image_size, image_size, 1)
batch_size = 32
kernel_size = 3
latent_dim = 16
# encoder/decoder number of CNN layers and filters per layer
layer_filters = [32, 64]
# build the autoencoder model
# first build the encoder model
inputs = Input(shape=input_shape, name='encoder_input')
x = inputs
# stack of Conv2D(32)-Conv2D(64)
for filters in layer_filters:
x = Conv2D(filters=filters,
kernel_size=kernel_size,
activation='relu',
strides=2,
padding='same')(x)
# shape info needed to build decoder model
# so we don't do hand computation
# the input to the decoder's first
# Conv2DTranspose will have this shape
# shape is (7, 7, 64) which is processed by
# the decoder back to (28, 28, 1)
shape = K.int_shape(x)
# generate latent vector
x = Flatten()(x)
latent = Dense(latent_dim, name='latent_vector')(x)
# instantiate encoder model
encoder = Model(inputs,
latent,
name='encoder')
encoder.summary()
plot_model(encoder,
to_file='encoder.png',
show_shapes=True)
# build the decoder model
latent_inputs = Input(shape=(latent_dim,), name='decoder_input')
# use the shape (7, 7, 64) that was earlier saved
x = Dense(shape[1] * shape[2] * shape[3])(latent_inputs)
# from vector to suitable shape for transposed conv
x = Reshape((shape[1], shape[2], shape[3]))(x)
# stack of Conv2DTranspose(64)-Conv2DTranspose(32)
for filters in layer_filters[::-1]:
x = Conv2DTranspose(filters=filters,
kernel_size=kernel_size,
activation='relu',
strides=2,
padding='same')(x)
# reconstruct the input
outputs = Conv2DTranspose(filters=1,
kernel_size=kernel_size,
activation='sigmoid',
padding='same',
name='decoder_output')(x)
# instantiate decoder model
decoder = Model(latent_inputs, outputs, name='decoder')
decoder.summary()
plot_model(decoder, to_file='decoder.png', show_shapes=True)
# autoencoder = encoder + decoder
# instantiate autoencoder model
autoencoder = Model(inputs,
decoder(encoder(inputs)),
name='autoencoder')
autoencoder.summary()
plot_model(autoencoder,
to_file='autoencoder.png',
show_shapes=True)
# Mean Square Error (MSE) loss function, Adam optimizer
autoencoder.compile(loss='mse', optimizer='adam')
# train the autoencoder
autoencoder.fit(x_train,
x_train,
validation_data=(x_test, x_test),
epochs=1,
batch_size=batch_size)
# predict the autoencoder output from test data
x_decoded = autoencoder.predict(x_test)
# display the 1st 8 test input and decoded images
imgs = np.concatenate([x_test[:8], x_decoded[:8]])
imgs = imgs.reshape((4, 4, image_size, image_size))
imgs = np.vstack([np.hstack(i) for i in imgs])
plt.figure()
plt.axis('off')
plt.title('Input: 1st 2 rows, Decoded: last 2 rows')
plt.imshow(imgs, interpolation='none', cmap='gray')
plt.savefig('input_and_decoded.png')
plt.show()
“图 3.2.1”显示了plot_model()生成的架构模型图,与encoder.summary()生成的文本版本相同。 保存最后一个Conv2D的输出形状以计算解码器输入层的尺寸,以便轻松重建 MNIST 图像:shape = K.int_shape(x)。

图 3.2.1:编码器模型由Conv2D(32) - Conv2D(64) - Dense(16)组成,以生成低维潜向量
列表 3.2.1 中的解码器对潜在向量进行解压缩,以恢复 MNIST 数字。 解码器输入级是Dense层,它将接受潜在向量。 单元的数量等于从编码器保存的Conv2D输出尺寸的乘积。 这样做是为了便于我们调整Dense层Dense层的输出大小,以最终恢复原始 MNIST 图像尺寸。
解码器由三个Conv2DTranspose的栈组成。 在我们的案例中,我们将使用转置的 CNN(有时称为反卷积),它是解码器中常用的。 我们可以将转置的 CNN(Conv2DTranspose)想象成 CNN 的逆过程。
在一个简单的示例中,如果 CNN 将图像转换为特征映射,则转置的 CNN 将生成给定特征映射的图像。“图 3.2.2”显示了解码器模型:

图 3.2.2:解码器模型由Dense(16) - Conv2DTranspose(64) - Conv2DTranspose(32) - Conv2DTranspose(1)组成。 输入是经过解码以恢复原始输入的潜向量
通过将编码器和解码器连接在一起,我们可以构建自编码器。“图 3.2.3”说明了自编码器的模型图:

图 3.2.3:通过将编码器模型和解码器模型结合在一起来构建自编码器模型。 此自编码器有 178 k 个参数
编码器的张量输出也是解码器的输入,该解码器生成自编码器的输出。 在此示例中,我们将使用 MSE 损失函数和 Adam 优化器。 在训练期间,输入与输出x_train相同。 我们应该注意,在我们的示例中,只有几层足以将验证损失在一个周期内驱动到 0.01。 对于更复杂的数据集,我们可能需要更深的编码器和解码器,以及更多的训练时间。
在对自编码器进行了一个周期的验证损失为 0.01 的训练之后,我们能够验证它是否可以对以前从未见过的 MNIST 数据进行编码和解码。“图 3.2.4”向我们展示了来自测试数据和相应解码图像的八个样本:

图 3.2.4:根据测试数据预测自编码器。 前两行是原始输入测试数据。 最后两行是预测数据
除了图像中的轻微模糊之外,我们能够轻松识别出自编码器能够以良好的质量恢复输入。 随着我们训练更多的周期,结果将有所改善。
在这一点上,我们可能想知道:我们如何可视化空间中的潜在向量? 一种简单的可视化方法是强制自编码器使用 2 维潜在向量来学习 MNIST 数字特征。 从那里,我们可以将该潜在向量投影到二维空间上,以查看 MNIST 潜在向量的分布方式。“图 3.2.5”和“图 3.2.6”显示了 MNIST 数字的分布与潜在代码尺寸的关系。

图 3.2.5:MNIST 数字分布与潜在代码尺寸z₀和z₁的关系。 原始照片可以在本书的 GitHub 存储库中找到。
在“图 3.2.5”中,我们可以看到特定数字的潜向量聚集在空间的某个区域上。 例如,数字 0 在左下象限中,而数字 1 在右上象限中。 这种群集在图中得到了反映。 实际上,同一图显示了导航或从潜在空间生成新数字的结果,如图“图 3.2.5”所示。
例如,从中心开始,向右上象限改变 2 维潜向量的值,这表明数字从 9 变为 1。这是可以预期的,因为从“图 3.2.5”开始,我们可以看到数字 9 群集的潜在代码值在中心附近,数字 1 群集的潜在代码值在右上象限。
对于“图 3.2.5”和“图 3.2.6”,我们仅研究了每个潜在向量维在 -4.0 和 +4.0 之间的区域:

图 3.2.6:导航 2 维潜在向量空间时生成的数字
从“图 3.2.5”中可以看出,潜在代码分布不是连续的。 理想情况下,应该看起来像一个圆圈,其中到处都有有效值。 由于这种不连续性,因此如果解码潜伏向量,则几乎不会产生任何可识别的数字。
“图 3.2.5”和“图 3.2.6”经过 20 个训练周期后生成。 通过设置latent_dim = 2修改了autoencoder-mnist-3.2.1.py代码。 plot_ results()函数将 MNIST 数字绘制为 2 维潜在向量的函数。 为了方便起见,该程序另存为autoencoder-2dim-mnist-3.2.2.py,其部分代码显示在“列表 3.2.2”中。 其余代码实际上类似于“列表 3.2.1”,在此不再显示。
“列表 3.2.2”:autoencoder-2dim-mnist-3.2.2.py
def plot_results(models,
data,
batch_size=32,
model_name="autoencoder_2dim"):
"""Plots 2-dim latent values as scatter plot of digits
then, plot MNIST digits as function of 2-dim latent vector
Arguments:
models (list): encoder and decoder models
data (list): test data and label
batch_size (int): prediction batch size
model_name (string): which model is using this function
"""
encoder, decoder = models
x_test, y_test = data
xmin = ymin = -4
xmax = ymax = +4
os.makedirs(model_name, exist_ok=True)
filename = os.path.join(model_name, "latent_2dim.png")
# display a 2D plot of the digit classes in the latent space
z = encoder.predict(x_test,
batch_size=batch_size)
plt.figure(figsize=(12, 10))
# axes x and y ranges
axes = plt.gca()
axes.set_xlim([xmin,xmax])
axes.set_ylim([ymin,ymax])
# subsample to reduce density of points on the plot
z = z[0::2]
y_test = y_test[0::2]
plt.scatter(z[:, 0], z[:, 1], marker="")
for i, digit in enumerate(y_test):
axes.annotate(digit, (z[i, 0], z[i, 1]))
plt.xlabel("z[0]")
plt.ylabel("z[1]")
plt.savefig(filename)
plt.show()
filename = os.path.join(model_name, "digits_over_latent.png")
# display a 30x30 2D manifold of the digits
n = 30
digit_size = 28
figure = np.zeros((digit_size * n, digit_size * n))
# linearly spaced coordinates corresponding to the 2D plot
# of digit classes in the latent space
grid_x = np.linspace(xmin, xmax, n)
grid_y = np.linspace(ymin, ymax, n)[::-1]
for i, yi in enumerate(grid_y):
for j, xi in enumerate(grid_x):
z = np.array([[xi, yi]])
x_decoded = decoder.predict(z)
digit = x_decoded[0].reshape(digit_size, digit_size)
figure[i * digit_size: (i + 1) * digit_size,
j * digit_size: (j + 1) * digit_size] = digit
plt.figure(figsize=(10, 10))
start_range = digit_size // 2
end_range = n * digit_size + start_range + 1
pixel_range = np.arange(start_range, end_range, digit_size)
sample_range_x = np.round(grid_x, 1)
sample_range_y = np.round(grid_y, 1)
plt.xticks(pixel_range, sample_range_x)
plt.yticks(pixel_range, sample_range_y)
plt.xlabel("z[0]")
plt.ylabel("z[1]")
plt.imshow(figure, cmap='Greys_r')
plt.savefig(filename)
plt.show()
这样就完成了和自编码器的检查。 接下来的章节将重点介绍其实际应用。 我们将从去噪自编码器开始。
3. 去噪自编码器(DAE)
现在,我们将构建具有实际应用的自编码器。 首先,让我们画一幅画,然后想象 MNIST 的数字图像被噪声破坏了,从而使人类更难以阅读。 我们能够构建一个去噪自编码器(DAE),以消除这些图像中的噪声。“图 3.3.1”向我们展示了三组 MNIST 数字。 每组的顶部行(例如,MNIST 数字 7、2、1、9、0、6、3、4 和 9)是原始图像。 中间的行显示了 DAE 的输入,这些输入是被噪声破坏的原始图像。 作为人类,我们发现很难读取损坏的 MNIST 数字。 最后一行显示 DAE 的输出。

图 3.3.1:原始 MNIST 数字(顶部行),损坏的原始图像(中间行)和去噪图像(最后一行)
如图“图 3.3.2”所示,去噪自编码器的结构实际上与我们在上一节中介绍的 MNIST 的自编码器相同。

图 3.3.2:去噪自编码器的输入是损坏的图像。 输出是干净或去噪的图像。 假定潜向量为 16 维
“图 3.3.2”中的输入定义为:
x = x_ori + noise(公式 3.3.1)
在该公式中,x_ori表示被噪声破坏的原始 MNIST 图像。 编码器的目的是发现如何产生潜向量z,这将使解码器能够恢复诸如 MSE,如下所示:x_ori通过最小化相异损失函数:
(Equation 3.3.2)
在此示例中,m是输出尺寸(例如,在 MNIST 中,m = width × height × channels = 28 × 28 × 1 = 784)。 x_ori[i]和x_tilde[i]分别是x_ori和x_tilde的元素。
为了实现 DAE,我们将需要对上一节中介绍的自编码器进行一些更改。 首先,训练输入数据应损坏的 MNIST 数字。 训练输出数据是原始的原始 MNIST 数字相同。 这就像告诉自编码器应校正的图像是什么,或要求它找出在图像损坏的情况下如何消除噪声。 最后,我们必须在损坏的 MNIST 测试数据上验证自编码器。
“图 3.3.2"左侧所示的 MNIST 数字 7 是实际损坏的图像输入。 右边的是经过训练的降噪自编码器的干净图像输出。
“列表 3.3.1”:denoising-autoencoder-mnist-3.3.1.py
from tensorflow.keras.layers import Dense, Input
from tensorflow.keras.layers import Conv2D, Flatten
from tensorflow.keras.layers import Reshape, Conv2DTranspose
from tensorflow.keras.models import Model
from tensorflow.keras import backend as K
from tensorflow.keras.datasets import mnist
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
np.random.seed(1337)
# load MNIST dataset
(x_train, _), (x_test, _) = mnist.load_data()
# reshape to (28, 28, 1) and normalize input images
image_size = x_train.shape[1]
x_train = np.reshape(x_train, [-1, image_size, image_size, 1])
x_test = np.reshape(x_test, [-1, image_size, image_size, 1])
x_train = x_train.astype('float32') / 255
x_test = x_test.astype('float32') / 255
# generate corrupted MNIST images by adding noise with normal dist
# centered at 0.5 and std=0.5
noise = np.random.normal(loc=0.5, scale=0.5, size=x_train.shape)
x_train_noisy = x_train + noise
noise = np.random.normal(loc=0.5, scale=0.5, size=x_test.shape)
x_test_noisy = x_test + noise
# adding noise may exceed normalized pixel values>1.0 or <0.0
# clip pixel values >1.0 to 1.0 and <0.0 to 0.0
x_train_noisy = np.clip(x_train_noisy, 0., 1.)
x_test_noisy = np.clip(x_test_noisy, 0., 1.)
# network parameters
input_shape = (image_size, image_size, 1)
batch_size = 32
kernel_size = 3
latent_dim = 16
# encoder/decoder number of CNN layers and filters per layer
layer_filters = [32, 64]
# build the autoencoder model
# first build the encoder model
inputs = Input(shape=input_shape, name='encoder_input')
x = inputs
# stack of Conv2D(32)-Conv2D(64)
for filters in layer_filters:
x = Conv2D(filters=filters,
kernel_size=kernel_size,
strides=2,
activation='relu',
padding='same')(x)
# shape info needed to build decoder model so we don't do hand computation
# the input to the decoder's first Conv2DTranspose will have this shape
# shape is (7, 7, 64) which can be processed by the decoder back to (28, 28, 1)
shape = K.int_shape(x)
# generate the latent vector
x = Flatten()(x)
latent = Dense(latent_dim, name='latent_vector')(x)
# instantiate encoder model
encoder = Model(inputs, latent, name='encoder')
encoder.summary()
# build the decoder model
latent_inputs = Input(shape=(latent_dim,), name='decoder_input')
# use the shape (7, 7, 64) that was earlier saved
x = Dense(shape[1] * shape[2] * shape[3])(latent_inputs)
# from vector to suitable shape for transposed conv
x = Reshape((shape[1], shape[2], shape[3]))(x)
# stack of Conv2DTranspose(64)-Conv2DTranspose(32)
for filters in layer_filters[::-1]:
x = Conv2DTranspose(filters=filters,
kernel_size=kernel_size,
strides=2,
activation='relu',
padding='same')(x)
# reconstruct the denoised input
outputs = Conv2DTranspose(filters=1,
kernel_size=kernel_size,
padding='same',
activation='sigmoid',
name='decoder_output')(x)
# instantiate decoder model
decoder = Model(latent_inputs, outputs, name='decoder')
decoder.summary()
# autoencoder = encoder + decoder
# instantiate autoencoder model
autoencoder = Model(inputs, decoder(encoder(inputs)), name='autoencoder')
autoencoder.summary()
# Mean Square Error (MSE) loss function, Adam optimizer
autoencoder.compile(loss='mse', optimizer='adam')
# train the autoencoder
autoencoder.fit(x_train_noisy,
x_train,
validation_data=(x_test_noisy, x_test),
epochs=10,
batch_size=batch_size)
# predict the autoencoder output from corrupted test images
x_decoded = autoencoder.predict(x_test_noisy)
# 3 sets of images with 9 MNIST digits
# 1st rows - original images
# 2nd rows - images corrupted by noise
# 3rd rows - denoised images
rows, cols = 3, 9
num = rows * cols
imgs = np.concatenate([x_test[:num], x_test_noisy[:num], x_decoded[:num]])
imgs = imgs.reshape((rows * 3, cols, image_size, image_size))
imgs = np.vstack(np.split(imgs, rows, axis=1))
imgs = imgs.reshape((rows * 3, -1, image_size, image_size))
imgs = np.vstack([np.hstack(i) for i in imgs])
imgs = (imgs * 255).astype(np.uint8)
plt.figure()
plt.axis('off')
plt.title('Original images: top rows, '
'Corrupted Input: middle rows, '
'Denoised Input: third rows')
plt.imshow(imgs, interpolation='none', cmap='gray')
Image.fromarray(imgs).save('corrupted_and_denoised.png')
plt.show()
“列表 3.3.1”显示了去噪自编码器,该编码器已添加到官方 Keras GitHub 存储库中。 使用相同的 MNIST 数据集,我们可以通过添加随机噪声来模拟损坏的图像。 添加的噪声是高斯分布,平均值为μ = 0.5,标准差为σ = 0.5。 由于添加随机噪声可能会将像素数据推入小于 0 或大于 1 的无效值,因此像素值会被裁剪为[0.1, 1.0]范围。
其他所有内容实际上都与上一节中的自编码器相同。 我们将使用相同的 MSE 损失函数和 Adam 优化器。 但是,训练的周期数已增加到 10。这是为了进行足够的参数优化。
“图 3.3.3”显示了 DAE 在某种程度上的鲁棒性,因为噪声级别从σ = 0.5增至σ = 0.75和σ = 1.0。 在σ = 0.75处,DAE 仍能够恢复原始图像。 但是,在σ = 1.0处,一些数字,例如第二和第三组中的 4 和 5,将无法正确恢复。

图 3.3.3:降噪自编码器的表现随着噪声水平的提高而增加
我们已经完成去噪自编码器的讨论和实现。 尽管此概念已在 MNIST 数字上进行了演示,但该思想也适用于其他信号。 在下一节中,我们将介绍自编码器的另一种实际应用,称为着色自编码器。
4. 自动着色自编码器
现在,我们将致力于自编码器的另一个实际应用。 在这种情况下,我们将想象一下,我们有一张灰度照片,并且想要构建一个可以自动为其添加颜色的工具。 我们要复制人类的能力,以识别海洋和天空为蓝色,草地和树木为绿色,云层为白色,依此类推。
如图“图 3.4.1”所示,如果给我们前景的稻田,背景的火山和顶部的天空的灰度照片(左),我们可以添加适当的颜色(右)。

图 3.4.1:为 Mayon 火山的灰度照片添加颜色。 着色网络应通过向灰度照片添加颜色来复制人类的能力。 左照片是灰度的。 正确的照片是彩色的。 原始彩色照片可以在本书的 GitHub 存储库中找到。
对于自编码器,一种简单的自动着色算法似乎是一个合适的问题。 如果我们可以使用足够数量的灰度照片作为输入并使用相应的彩色照片作为输出来训练自编码器,则可能会在正确应用颜色时发现隐藏的结构。 大致上,这是去噪的反向过程。 问题是,自编码器能否在原始灰度图像上添加颜色(良好的噪点)?
“列表 3.4.1”显示了着色自编码器网络。 着色自编码器网络是我们用于 MNIST 数据集的降噪自编码器的修改版本。 首先,我们需要一个彩色照片的灰度数据集。 我们之前使用过的 CIFAR10 数据库进行了 50,000 次训练和 10,000 次测试,可以将32×32 RGB 照片转换为灰度图像。 如下清单所示,我们可以使用rgb2gray()函数在 R,G 和 B 分量上应用权重,以从彩色转换为灰度:
“列表 3.4.1”:colorization-autoencoder-cifar10-3.4.1.py
from tensorflow.keras.layers import Dense, Input
from tensorflow.keras.layers import Conv2D, Flatten
from tensorflow.keras.layers import Reshape, Conv2DTranspose
from tensorflow.keras.models import Model
from tensorflow.keras.callbacks import ReduceLROnPlateau
from tensorflow.keras.callbacks import ModelCheckpoint
from tensorflow.keras.datasets import cifar10
from tensorflow.keras.utils import plot_model
from tensorflow.keras import backend as K
import numpy as np
import matplotlib.pyplot as plt
import os
def rgb2gray(rgb):
"""Convert from color image (RGB) to grayscale.
Source: opencv.org
grayscale = 0.299*red + 0.587*green + 0.114*blue
Argument:
rgb (tensor): rgb image
Return:
(tensor): grayscale image
"""
return np.dot(rgb[...,:3], [0.299, 0.587, 0.114])
# load the CIFAR10 data
(x_train, _), (x_test, _) = cifar10.load_data()
# input image dimensions
# we assume data format "channels_last"
img_rows = x_train.shape[1]
img_cols = x_train.shape[2]
channels = x_train.shape[3]
# create saved_images folder
imgs_dir = 'saved_images'
save_dir = os.path.join(os.getcwd(), imgs_dir)
if not os.path.isdir(save_dir):
os.makedirs(save_dir)
# display the 1st 100 input images (color and gray)
imgs = x_test[:100]
imgs = imgs.reshape((10, 10, img_rows, img_cols, channels))
imgs = np.vstack([np.hstack(i) for i in imgs])
plt.figure()
plt.axis('off')
plt.title('Test color images (Ground Truth)')
plt.imshow(imgs, interpolation='none')
plt.savefig('%s/test_color.png' % imgs_dir)
plt.show()
# convert color train and test images to gray
x_train_gray = rgb2gray(x_train)
x_test_gray = rgb2gray(x_test)
# display grayscale version of test images
imgs = x_test_gray[:100]
imgs = imgs.reshape((10, 10, img_rows, img_cols))
imgs = np.vstack([np.hstack(i) for i in imgs])
plt.figure()
plt.axis('off')
plt.title('Test gray images (Input)')
plt.imshow(imgs, interpolation='none', cmap='gray')
plt.savefig('%s/test_gray.png' % imgs_dir)
plt.show()
# normalize output train and test color images
x_train = x_train.astype('float32') / 255
x_test = x_test.astype('float32') / 255
# normalize input train and test grayscale images
x_train_gray = x_train_gray.astype('float32') / 255
x_test_gray = x_test_gray.astype('float32') / 255
# reshape images to row x col x channel for CNN output/validation
x_train = x_train.reshape(x_train.shape[0], img_rows, img_cols, channels)
x_test = x_test.reshape(x_test.shape[0], img_rows, img_cols, channels)
# reshape images to row x col x channel for CNN input
x_train_gray = x_train_gray.reshape(x_train_gray.shape[0], img_rows, img_cols, 1)
x_test_gray = x_test_gray.reshape(x_test_gray.shape[0], img_rows, img_cols, 1)
# network parameters
input_shape = (img_rows, img_cols, 1)
batch_size = 32
kernel_size = 3
latent_dim = 256
# encoder/decoder number of CNN layers and filters per layer
layer_filters = [64, 128, 256]
# build the autoencoder model
# first build the encoder model
inputs = Input(shape=input_shape, name='encoder_input')
x = inputs
# stack of Conv2D(64)-Conv2D(128)-Conv2D(256)
for filters in layer_filters:
x = Conv2D(filters=filters,
kernel_size=kernel_size,
strides=2,
activation='relu',
padding='same')(x)
# shape info needed to build decoder model so we don't do hand computation
# the input to the decoder's first Conv2DTranspose will have this shape
# shape is (4, 4, 256) which is processed by the decoder back to (32, 32, 3)
shape = K.int_shape(x)
# generate a latent vector
x = Flatten()(x)
latent = Dense(latent_dim, name='latent_vector')(x)
# instantiate encoder model
encoder = Model(inputs, latent, name='encoder')
encoder.summary()
# build the decoder model
latent_inputs = Input(shape=(latent_dim,), name='decoder_input')
x = Dense(shape[1]*shape[2]*shape[3])(latent_inputs)
x = Reshape((shape[1], shape[2], shape[3]))(x)
# stack of Conv2DTranspose(256)-Conv2DTranspose(128)-Conv2DTranspose(64)
for filters in layer_filters[::-1]:
x = Conv2DTranspose(filters=filters,
kernel_size=kernel_size,
strides=2,
activation='relu',
padding='same')(x)
outputs = Conv2DTranspose(filters=channels,
kernel_size=kernel_size,
activation='sigmoid',
padding='same',
name='decoder_output')(x)
# instantiate decoder model
decoder = Model(latent_inputs, outputs, name='decoder')
decoder.summary()
# autoencoder = encoder + decoder
# instantiate autoencoder model
autoencoder = Model(inputs, decoder(encoder(inputs)), name='autoencoder')
autoencoder.summary()
# prepare model saving directory.
save_dir = os.path.join(os.getcwd(), 'saved_models')
model_name = 'colorized_ae_model.{epoch:03d}.h5'
if not os.path.isdir(save_dir):
os.makedirs(save_dir)
filepath = os.path.join(save_dir, model_name)
# reduce learning rate by sqrt(0.1) if the loss does not improve in 5 epochs
lr_reducer = ReduceLROnPlateau(factor=np.sqrt(0.1),
cooldown=0,
patience=5,
verbose=1,
min_lr=0.5e-6)
# save weights for future use (e.g. reload parameters w/o training)
checkpoint = ModelCheckpoint(filepath=filepath,
monitor='val_loss',
verbose=1,
save_best_only=True)
# Mean Square Error (MSE) loss function, Adam optimizer
autoencoder.compile(loss='mse', optimizer='adam')
# called every epoch
callbacks = [lr_reducer, checkpoint]
# train the autoencoder
autoencoder.fit(x_train_gray,
x_train,
validation_data=(x_test_gray, x_test),
epochs=30,
batch_size=batch_size,
callbacks=callbacks)
# predict the autoencoder output from test data
x_decoded = autoencoder.predict(x_test_gray)
# display the 1st 100 colorized images
imgs = x_decoded[:100]
imgs = imgs.reshape((10, 10, img_rows, img_cols, channels))
imgs = np.vstack([np.hstack(i) for i in imgs])
plt.figure()
plt.axis('off')
plt.title('Colorized test images (Predicted)')
plt.imshow(imgs, interpolation='none')
plt.savefig('%s/colorized.png' % imgs_dir)
plt.show()
通过添加更多卷积和转置卷积,我们提高了自编码器的容量。 我们还将每个 CNN 块的过滤器数量增加了一倍。 潜向量现在为 256 维,以增加其可以表示的显着属性的数量,如自编码器部分所述。 最后,输出过滤器的大小已增加到三倍,或等于预期的彩色输出的 RGB 中的通道数。
现在使用灰度作为输入,原始 RGB 图像作为输出来训练着色自编码器。 训练将花费更多的时间,并在验证损失没有改善的情况下使用学习率降低器来缩小学习率。 通过告诉tf.keras fit()函数中的 callbacks 参数调用lr_reducer()函数,可以轻松完成此操作。
“图 3.4.2”演示了来自 CIFAR10 测试数据集的灰度图像的着色。

图 3.4.2:使用自编码器将灰度自动转换为彩色图像。 CIFAR10 测试灰度输入图像(左)和预测的彩色图像(右)。 原始彩色照片可以在本书的 GitHub 存储库中找到,网址为 https://github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras/blob/master/chapter3-autoencoders/README.md
“图 3.4.3”将基本事实与着色自编码器预测进行了比较:

图 3.4.3:地面真彩色图像与预测彩色图像的并排比较。 原始彩色照片可以在本书的 GitHub 存储库中找到,网址为 https://github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras/blob/master/chapter3-autoencoders/README.md
自编码器执行可接受的着色作业。 预计大海或天空为蓝色,动物的阴影为棕色,云为白色,依此类推。
有一些明显的错误预测,例如红色车辆变成蓝色或蓝色车辆变成红色,偶尔的绿色领域被误认为是蓝天,而黑暗或金色的天空被转换为蓝天。
这是关于自编码器的最后一部分。 在以下各章中,我们将重新讨论以一种或另一种形式进行编码和解码的概念。 表示学习的概念在深度学习中非常基础。
5. 总结
在本章中,我们已经介绍了自编码器,它们是将输入数据压缩为低维表示形式的神经网络,以便有效地执行结构转换,例如降噪和着色。 我们为 GAN 和 VAE 的更高级主题奠定了基础,我们将在后面的章节中介绍它们。 我们已经演示了如何从两个构建模块模型(编码器和解码器)实现自编码器。 我们还学习了如何提取输入分布的隐藏结构是 AI 的常见任务之一。
一旦学习了潜在代码,就可以对原始输入分布执行许多结构操作。 为了更好地了解输入分布,可以使用低级嵌入(类似于本章内容)或通过更复杂的降维技术(例如 t-SNE 或 PCA)来可视化潜在向量形式的隐藏结构。
除了去噪和着色外,自编码器还用于将输入分布转换为低维潜向量,可以针对其他任务(例如,分割,检测,跟踪,重建和视觉理解)进一步对其进行处理。 在“第 8 章”,“变分自编码器(VAE)”中,我们将讨论 VAE,它们在结构上与自编码器相同,但具有可解释的潜在代码,这些代码可以产生连续的潜在向量投影,因此有所不同。
在下一章中,我们将着手介绍 AI 最近最重要的突破之一,即 GAN。 在下一章中,我们将学习 GAN 的核心优势,即其综合看起来真实的数据的能力。
6. 参考
Ian Goodfellow et al.: Deep Learning. Vol. 1. Cambridge: MIT press, 2016 (http://www.deeplearningbook.org/).
四、生成对抗网络(GAN)
在本章中,我们将研究生成对抗网络(GAN)[1]。 GAN 属于生成模型家族。 但是,与自编码器不同,生成模型能够在给定任意编码的情况下创建新的有意义的输出。
在本章中,将讨论 GAN 的工作原理。 我们还将使用tf.keras回顾几个早期 GAN 的实现,而在本章的后面,我们将演示实现稳定训练所需的技术。 本章的范围涵盖了 GAN 实现的两个流行示例,深度卷积 GAN(DCGAN)[2]和条件 GAN(CGAN)[3]。
总之,本章的目标是:
- GAN 的原理简介
- GAN 的早期工作实现之一的简介,称为 DCGAN
- 改进的 DCGAN,称为 CGAN,它使用条件
- 在
tf.keras中实现 DCGAN 和 CGAN
让我们从 GAN 的概述开始。
1. GAN 概述
在进入 GAN 的更高级概念之前,让我们开始研究 GAN,并介绍它们背后的基本概念。 GAN 非常强大。 通过执行潜在空间插值,他们可以生成不是真实人的新人脸这一事实证明了这一简单的陈述。
可以在以下 YouTube 视频中看到 GAN 的高级功能:
展示如何利用 GAN 产生逼真的面部的视频演示了它们的功能。 这个主题比我们之前看过的任何内容都先进得多。 例如,上面的视频演示了自编码器无法轻松完成的事情,我们在“第 3 章”,“自编码器”中介绍了这些内容。
GAN 可以通过训练两个相互竞争(且相互配合)的网络(称为生成器和判别器(有时称为评论家)。 生成器的作用是继续弄清楚如何生成伪造数据或信号(包括音频和图像),使伪造者蒙上阴影。 同时,判别器被训练以区分假信号和真实信号。 随着训练的进行,判别器将不再能够看到合成生成的数据与真实数据之间的差异。 从那里,可以丢弃判别器,然后可以使用生成器来创建从未见过的新的真实数据。
GAN 的基本概念很简单。 但是,我们将发现的一件事是,最具挑战性的问题是我们如何实现对生成器-判别器网络的稳定训练? 为了使两个网络都能同时学习,生成器和判别器之间必须存在健康的竞争。 由于损失函数是根据判别器的输出计算得出的,因此其参数会快速更新。 当判别器收敛速度更快时,生成器不再为其参数接收到足够的梯度更新,并且无法收敛。 除了难以训练之外,GAN 还可能遭受部分或全部模态崩溃的影响,这种情况下,生成器针对不同的潜在编码生成几乎相似的输出。
GAN 的原理
如图“图 4.1.1”所示,GAN 类似于伪造者(生成器)-警察(判别器)场景。 在学院里,警察被教导如何确定美钞是真钞还是假钞。 来自银行的真实美钞样本和来自伪造者的伪钞样本被用来训练警察。 但是,伪造者会不时地假装他印制了真实的美元钞票。 最初,警方不会上当,并且会告诉造假者这笔钱是假的。 考虑到此反馈,造假者再次磨练他的技能,并尝试制作新的假美元钞票。 如预期的那样,警察将能够发现这笔钱是伪造的,并说明为什么美元钞票是伪造的:

图 4.1.1:GAN 的生成器和判别器类似于伪造者和警察。 造假者的目的是欺骗警察,使他们相信美元钞票是真实的
此过程无限期地继续,但是到了造假者已经掌握了伪造货币的程度,以至于伪造品与真实货币几乎无法区分-甚至对于最受执业的警察也是如此。 然后,伪造者可以无限次打印美元钞票,而不会被警方抓获,因为它们不再可识别为伪造的。
如图“图 4.1.2”所示,GAN 由两个网络组成,一个生成器和一个判别器:

图 4.1.2:GAN 由两个网络组成,一个生成器和一个判别器。 判别器经过训练,可以区分真实信号和虚假信号或数据。 生成器的工作是生成伪造的信号或数据,这些伪造的信号或数据最终会欺骗判别器
生成器的输入是噪声,输出是合成数据。 同时,判别器的输入将是实数据或合成数据。 真实数据来自真实的采样数据,而虚假数据来自生成器。 所有有效数据均标记为 1.0(即 100% 为真实概率),而所有合成数据均标记为 0.0(即 0% 为真实概率)。 由于标记过程是自动化的,因此 GAN 仍被认为是深度学习中无监督学习方法的一部分。
判别器的目标是从此提供的数据集中学习如何区分真实数据与伪数据。 在 GAN 训练的这一部分中,仅判别器参数将被更新。 像典型的二元分类器一样,判别器经过训练,可以在 0.0 到 1.0 的范围内预测置信度值,以了解给定输入数据与真实数据的接近程度。 但是,这只是故事的一半。
生成器将以固定的时间间隔假装其输出是真实数据,并要求 GAN 将其标记为 1.0。 然后,当将伪造数据提供给判别器时,自然会将其分类为伪造,标签接近 0.0。
优化器根据显示的标签(即 1.0)计算生成器参数更新。 在对新数据进行训练时,它还会考虑自己的预测。 换句话说,判别器对其预测有一些疑问,因此,GAN 将其考虑在内。 这次,GAN 将让梯度从判别器的最后一层向下向下传播到生成器的第一层。 但是,在大多数实践中,在训练的此阶段,判别器参数会暂时冻结。 生成器将使用梯度来更新其参数并提高其合成伪数据的能力。
总体而言,整个过程类似于两个网络相互竞争,同时仍在合作。 当 GAN 训练收敛时,最终结果是生成器,可以合成看似真实的数据。 判别器认为该合成数据是真实数据或带有接近 1.0 的标签,这意味着该判别器可以被丢弃。 生成器部分将有助于从任意噪声输入中产生有意义的输出。
下面的“图 4.1.3”中概述了该过程:

图 4.1.3:训练判别器类似于使用二进制交叉熵损失训练二分类器网络。 伪数据由生成器提供,而真实数据来自真实样本
如上图所示,可以通过最小化以下等式中的损失函数来训练判别器:
(Equation 4.1.1)
该方程只是标准的二进制交叉熵代价函数。 损失是正确识别真实数据1 - D(g(z))的期望值与 1.0 正确识别合成数据1 - D(g(z))的期望值之和。 日志不会更改本地最小值的位置。
训练过程中将两个小批数据提供给判别器:
-
x,来自采样数据的实数据(换言之,x ~ p_data),标签为 1.0 -
x' = g(z),来自生成器的带有标签 0.0 的伪造数据
为了使的损失函数最小,将通过反向传播通过正确识别真实数据D(x)和合成数据1 - D(g(z))来更新判别器参数θ^(D)。 正确识别真实数据等同于D(x) -> 1.0,而正确分类伪造数据则与D(g(z)) -> 0.0或1 - D(g(z)) -> 1.0相同。 在此等式中,z是生成器用来合成新信号的任意编码或噪声向量。 两者都有助于最小化损失函数。
为了训练生成器,GAN 将判别器和生成器损失的总和视为零和博弈。 生成器损失函数只是判别器损失函数的负数:
(Equation 4.1.2)
然后可以将其更恰当地重写为值函数:
(Equation 4.1.3)
从生成器的角度来看,应将“公式 4.1.3”最小化。 从判别器的角度来看,值函数应最大化。 因此,生成器训练准则可以写成极大极小问题:
(Equation 4.1.4)
有时,我们会假装合成数据是带有标签 1.0 的真实数据,以此来欺骗判别器。 通过最大化θ^(D),优化器将梯度更新发送到判别器参数,以将该合成数据视为真实数据。 同时,通过将θ^(G)的相关性减至最小,优化器将在上训练生成器的参数,从而欺骗识别器。 但是,实际上,判别器对将合成数据分类为伪造的预测很有信心,并且不会更新 GAN 参数。 此外,梯度更新很小,并且在传播到生成器层时已大大减小。 结果,生成器无法收敛。

图 4.1.4:训练生成器就像使用二进制交叉熵损失函数训练网络一样。 来自生成器的虚假数据显示为真实数据
解决方案是按以下形式重新构造生成器的损失函数:
(Equation 4.1.5)
损失函数只是通过训练生成器,最大程度地提高了判别器认为合成数据是真实数据的机会。 新公式不再是零和,而是纯粹由启发式驱动的。“图 4.1.4”显示了训练过程中的生成器。 在此图中,仅在训练整个对抗网络时才更新生成器参数。 这是因为梯度从判别器向下传递到生成器。 但是,实际上,判别器权重仅在对抗训练期间临时冻结。
在深度学习中,可以使用合适的神经网络架构来实现生成器和判别器。 如果数据或信号是图像,则生成器和判别器网络都将使用 CNN。 对于诸如音频之类的一维序列,两个网络通常都是循环的(RNN,LSTM 或 GRU)。
在本节中,我们了解到 GAN 的原理很简单。 我们还了解了如何通过熟悉的网络层实现 GAN。 GAN 与其他网络的区别在于众所周知,它们很难训练。 只需稍作更改,就可以使网络变得不稳定。 在以下部分中,我们将研究使用深度 CNN 的 GAN 早期成功实现之一。 它称为 DCGAN [3]。
2. 在 Keras 中实现 DCGAN
“图 4.2.1”显示 DCGAN,其中用于生成伪造的 MNIST 图像:

图 4.2.1:DCGAN 模型
DCGAN 实现以下设计原则:
- 使用
stride > 1和卷积代替MaxPooling2D或UpSampling2D。 通过stride > 1,CNN 可以学习如何调整特征映射的大小。 - 避免使用
Dense层。 在所有层中使用 CNN。Dense层仅用作生成器的第一层以接受z向量。 调整Dense层的输出大小,并成为后续 CNN 层的输入。 - 使用批量归一化(BN),通过将每一层的输入归一化以使均值和单位方差为零,来稳定学习。 生成器输出层和判别器输入层中没有 BN。 在此处要介绍的实现示例中,没有在标识符中使用批量归一化。
- 整流线性单元(ReLU)在生成器的所有层中均使用,但在输出层中则使用
tanh激活。 在此处要介绍的实现示例中,在生成器的输出中使用sigmoid代替tanh,因为通常会导致对 MNIST 数字进行更稳定的训练。 - 在判别器的所有层中使用 Leaky ReLU。 与 ReLU 不同,Leaky ReLU 不会在输入小于零时将所有输出清零,而是生成一个等于
alpha x input的小梯度。 在以下示例中,alpha = 0.2。
生成器学习从 100 维输入向量([-1.0,1.0]范围内具有均匀分布的 100 维随机噪声)生成伪图像。 判别器将真实图像与伪图像分类,但是在训练对抗网络时无意中指导生成器如何生成真实图像。 在我们的 DCGAN 实现中使用的核大小为 5。这是为了允许它增加卷积的接收场大小和表达能力。
生成器接受由 -1.0 到 1.0 范围内的均匀分布生成的 100 维z向量。 生成器的第一层是7 x 7 x 128 = 6,272单元的密集层。 基于输出图像的预期最终尺寸(28 x 28 x 1,28 是 7 的倍数)和第一个Conv2DTranspose的过滤器数量(等于 128)来计算单元数量。
我们可以将转置的 CNN(Conv2DTranspose)想象成 CNN 的逆过程。 在一个简单的示例中,如果 CNN 将图像转换为特征映射,则转置的 CNN 将生成给定特征映射的图像。 因此,转置的 CNN 在上一章的解码器中和本章的生成器中使用。
在对strides = 2进行两个Conv2DTranspose之后,特征映射的大小将为28 x 28 x n_filter。 每个Conv2DTranspose之前都有批量规范化和 ReLU。 最后一层具有 Sigmoid 激活,可生成28 x 28 x 1假 MNIST 图像。 将每个像素标准化为与[0, 255]灰度级相对应的[0.0, 1.0]。 下面的“列表 4.2.1”显示了tf.keras中生成器网络的实现。 定义了一个函数来生成生成器模型。 由于整个代码的长度,我们将列表限制为正在讨论的特定行。
“列表 4.2.1”:dcgan-mnist-4.2.1.py
def build_generator(inputs, image_size):
"""Build a Generator Model
Stack of BN-ReLU-Conv2DTranpose to generate fake images
Output activation is sigmoid instead of tanh in [1].
Sigmoid converges easily.
Arguments:
inputs (Layer): Input layer of the generator
the z-vector)
image_size (tensor): Target size of one side
(assuming square image)
Returns:
generator (Model): Generator Model
"""
image_resize = image_size // 4
# network parameters
kernel_size = 5
layer_filters = [128, 64, 32, 1]
x = Dense(image_resize * image_resize * layer_filters[0])(inputs)
x = Reshape((image_resize, image_resize, layer_filters[0]))(x)
for filters in layer_filters:
# first two convolution layers use strides = 2
# the last two use strides = 1
if filters > layer_filters[-2]:
strides = 2
else:
strides = 1
x = BatchNormalization()(x)
x = Activation('relu')(x)
x = Conv2DTranspose(filters=filters,
kernel_size=kernel_size,
strides=strides,
padding='same')(x)
x = Activation('sigmoid')(x)
generator = Model(inputs, x, name='generator')
return generator
判别器与相似,是许多基于 CNN 的分类器。 输入是28 x 28 x 1MNIST 图像,分类为真实(1.0)或伪(0.0)。 有四个 CNN 层。 除了最后的卷积,每个Conv2D都使用strides = 2将特征映射下采样两个。 然后每个Conv2D之前都有一个泄漏的 ReLU 层。 最终的过滤器大小为 256,而初始的过滤器大小为 32,并使每个卷积层加倍。 最终的过滤器大小 128 也适用。 但是,我们会发现生成的图像在 256 的情况下看起来更好。最终输出层被展平,并且在通过 Sigmoid 激活层缩放后,单个单元Dense层在 0.0 到 1.0 之间生成预测。 输出被建模为伯努利分布。 因此,使用了二进制交叉熵损失函数。
建立生成器和判别器模型后,通过将生成器和判别器网络连接起来,建立对抗模型。 鉴别网络和对抗网络都使用 RMSprop 优化器。 判别器的学习率是2e-4,而对抗网络的学习率是1e-4。 判别器的 RMSprop 衰减率为6e-8,对抗网络的 RMSprop 衰减率为3e-8。
将对手的学习率设置为判别器的一半将使训练更加稳定。 您会从“图 4.1.3”和“图 4.1.4”中回忆起,GAN 训练包含两个部分:判别器训练和生成器训练,这是冻结判别器权重的对抗训练。
“列表 4.2.2”显示了tf.keras中判别器的实现。 定义一个函数来建立鉴别模型。
“列表 4.2.2”:dcgan-mnist-4.2.1.py
def build_discriminator(inputs):
"""Build a Discriminator Model
Stack of LeakyReLU-Conv2D to discriminate real from fake.
The network does not converge with BN so it is not used here
unlike in [1] or original paper.
Arguments:
inputs (Layer): Input layer of the discriminator (the image)
Returns:
discriminator (Model): Discriminator Model
"""
kernel_size = 5
layer_filters = [32, 64, 128, 256]
x = inputs
for filters in layer_filters:
# first 3 convolution layers use strides = 2
# last one uses strides = 1
if filters == layer_filters[-1]:
strides = 1
else:
strides = 2
x = LeakyReLU(alpha=0.2)(x)
x = Conv2D(filters=filters,
kernel_size=kernel_size,
strides=strides,
padding='same')(x)
x = Flatten()(x)
x = Dense(1)(x)
x = Activation('sigmoid')(x)
discriminator = Model(inputs, x, name='discriminator')
return discriminator
在“列表 4.2.3”中,我们将说明如何构建 GAN 模型。 首先,建立鉴别模型,然后实例化生成器模型。 对抗性模型只是生成器和判别器组合在一起。 在许多 GAN 中,批大小为 64 似乎是最常见的。 网络参数显示在“列表 4.2.3”中。
“列表 4.2.3”:dcgan-mnist-4.2.1.py
建立 DCGAN 模型并调用训练例程的函数:
def build_and_train_models():
# load MNIST dataset
(x_train, _), (_, _) = mnist.load_data()
# reshape data for CNN as (28, 28, 1) and normalize
image_size = x_train.shape[1]
x_train = np.reshape(x_train, [-1, image_size, image_size, 1])
x_train = x_train.astype('float32') / 255
model_name = "dcgan_mnist"
# network parameters
# the latent or z vector is 100-dim
latent_size = 100
batch_size = 64
train_steps = 40000
lr = 2e-4
decay = 6e-8
input_shape = (image_size, image_size, 1)
# build discriminator model
inputs = Input(shape=input_shape, name='discriminator_input')
discriminator = build_discriminator(inputs)
# [1] or original paper uses Adam,
# but discriminator converges easily with RMSprop
optimizer = RMSprop(lr=lr, decay=decay)
discriminator.compile(loss='binary_crossentropy',
optimizer=optimizer,
metrics=['accuracy'])
discriminator.summary()
# build generator model
input_shape = (latent_size, )
inputs = Input(shape=input_shape, name='z_input')
generator = build_generator(inputs, image_size)
generator.summary()
# build adversarial model
optimizer = RMSprop(lr=lr * 0.5, decay=decay * 0.5)
# freeze the weights of discriminator during adversarial training
discriminator.trainable = False
# adversarial = generator + discriminator
adversarial = Model(inputs,
discriminator(generator(inputs)),
name=model_name)
adversarial.compile(loss='binary_crossentropy',
optimizer=optimizer,
metrics=['accuracy'])
adversarial.summary()
# train discriminator and adversarial networks
models = (generator, discriminator, adversarial)
params = (batch_size, latent_size, train_steps, model_name)
train(models, x_train, params)
从“列表 4.2.1”和“列表 4.2.2”中可以看出,DCGAN 模型很简单。 使它们难以构建的原因是,网络中的较小更改设计很容易破坏训练收敛。 例如,如果在判别器中使用批量归一化,或者如果生成器中的strides = 2传输到后面的 CNN 层,则 DCGAN 将无法收敛。
“列表 4.2.4”显示了专用于训练判别器和对抗网络的函数。 由于自定义训练,将不使用常规的fit()函数。 取而代之的是,调用train_on_batch()对给定的数据批量运行单个梯度更新。 然后通过对抗网络训练生成器。 训练首先从数据集中随机选择一批真实图像。 这被标记为实数(1.0)。 然后,生成器将生成一批伪图像。 这被标记为假(0.0)。 这两个批量是连接在一起的,用于训练判别器。
完成此操作后,生成器将生成一批新的伪图像,并将其标记为真实(1.0)。 这批将用于训练对抗网络。 交替训练这两个网络约 40,000 步。 定期将基于特定噪声向量生成的 MNIST 数字保存在文件系统中。 在最后的训练步骤中,网络已收敛。 生成器模型也保存在文件中,因此我们可以轻松地将训练后的模型重新用于未来的 MNIST 数字生成。 但是,仅保存生成器模型,因为这是该 DCGAN 在生成新 MNIST 数字时的有用部分。 例如,我们可以通过执行以下操作来生成新的和随机的 MNIST 数字:
python3 dcgan-mnist-4.2.1.py --generator=dcgan_mnist.h5
“列表 4.2.4”:dcgan-mnist-4.2.1.py
训练判别器和对抗网络的函数:
def train(models, x_train, params):
"""Train the Discriminator and Adversarial Networks
Alternately train Discriminator and Adversarial networks by batch.
Discriminator is trained first with properly real and fake images.
Adversarial is trained next with fake images pretending to be real
Generate sample images per save_interval.
Arguments:
models (list): Generator, Discriminator, Adversarial models
x_train (tensor): Train images
params (list) : Networks parameters
"""
# the GAN component models
generator, discriminator, adversarial = models
# network parameters
batch_size, latent_size, train_steps, model_name = params
# the generator image is saved every 500 steps
save_interval = 500
# noise vector to see how the generator output evolves during training
noise_input = np.random.uniform(-1.0, 1.0, size=[16, latent_size])
# number of elements in train dataset
train_size = x_train.shape[0]
for i in range(train_steps):
# train the discriminator for 1 batch
# 1 batch of real (label=1.0) and fake images (label=0.0)
# randomly pick real images from dataset
rand_indexes = np.random.randint(0, train_size, size=batch_size)
real_images = x_train[rand_indexes]
# generate fake images from noise using generator
# generate noise using uniform distribution
noise = np.random.uniform(-1.0,
1.0,
size=[batch_size, latent_size])
# generate fake images
fake_images = generator.predict(noise)
# real + fake images = 1 batch of train data
x = np.concatenate((real_images, fake_images))
# label real and fake images
# real images label is 1.0
y = np.ones([2 * batch_size, 1])
# fake images label is 0.0
y[batch_size:, :] = 0.0
# train discriminator network, log the loss and accuracy
loss, acc = discriminator.train_on_batch(x, y)
log = "%d: [discriminator loss: %f, acc: %f]" % (i, loss, acc)
# train the adversarial network for 1 batch
# 1 batch of fake images with label=1.0
# since the discriminator weights
# are frozen in adversarial network
# only the generator is trained
# generate noise using uniform distribution
noise = np.random.uniform(-1.0,
1.0,
size=[batch_size, latent_size])
# label fake images as real or 1.0
y = np.ones([batch_size, 1])
# train the adversarial network
# note that unlike in discriminator training,
# we do not save the fake images in a variable
# the fake images go to the discriminator input of the adversarial
# for classification
# log the loss and accuracy
loss, acc = adversarial.train_on_batch(noise, y)
log = "%s [adversarial loss: %f, acc: %f]" % (log, loss, acc)
print(log)
if (i + 1) % save_interval == 0:
# plot generator images on a periodic basis
plot_images(generator,
noise_input=noise_input,
show=False,
step=(i + 1),
model_name=model_name)
# save the model after training the generator
# the trained generator can be reloaded for
# future MNIST digit generation
generator.save(model_name + ".h5")
“图 4.2.2”显示了生成器伪造图像根据训练步骤的演变。 生成器已经以 5,000 步的速度生成了可识别的图像。 非常像拥有一个知道如何绘制数字的智能体。 值得注意的是,某些数字从一种可识别的形式(例如,最后一行的第二列中的 8)变为另一种形式(例如,0)。 当训练收敛时,判别器损失接近 0.5,而对抗性损失接近 1.0,如下所示:
39997: [discriminator loss: 0.423329, acc: 0.796875] [adversarial loss:
0.819355, acc: 0.484375]
39998: [discriminator loss: 0.471747, acc: 0.773438] [adversarial loss:
1.570030, acc: 0.203125]
39999: [discriminator loss: 0.532917, acc: 0.742188] [adversarial loss:
0.824350, acc: 0.453125]
我们可以看到以下结果:

图 4.2.2:DCGAN 生成器在不同训练步骤生成的伪造图像
在本节中,由 DCGAN 生成的伪造图像是随机的。
生成器无法控制哪个特定数字。 没有机制可以请求生成器提供特定的数字。 这个问题可以通过称为 CGAN [4]的 GAN 变体来解决,我们将在下一部分中进行讨论。
3. Conditional GAN
使用与上一节相同的 GAN ,会对生成器和判别器输入都施加一个条件。 条件是数字的一键向量形式。 这与要生成的图像(生成器)或分类为真实或伪造的图像(判别器)相关。 CGAN 模型显示在“图 4.3.1”中。
CGAN 与 DCGAN 相似,除了附加的单热向量输入。 对于生成器,单热标签在Dense层之前与潜向量连接在一起。 对于判别器,添加了新的Dense层。 新层用于处理单热向量并对其进行整形,以使其适合于与后续 CNN 层的另一个输入连接。

图 4.3.1:CGAN 模型与 DCGAN 相似,只不过是单热向量,用于调节生成器和判别器的输出
生成器学习从 100 维输入向量和指定位数生成伪图像。 判别器基于真实和伪图像及其对应的标签将真实图像与伪图像分类。
CGAN 的基础仍然与原始 GAN 原理相同,区别在于判别器和生成器的输入均以“一热”标签y为条件。
通过在“公式 4.1.1”和“公式 4.1.5”中合并此条件,判别器和生成器的损失函数在“公式 4.3.1”和“公式 4.3.2”中显示,分别为:
(Equation 4.3.1)
(Equation 4.3.2)
给定“图 4.3.2”,将损失函数写为:
(Equation 4.3.3)
(Equation 4.3.4)
判别器的新损失函数旨在最大程度地减少预测来自数据集的真实图像和来自生成器的假图像(给定单热点标签)的误差。“图 4.3.2”显示了如何训练判别器。

图 4.3.2:训练 CGAN 判别器类似于训练 GAN 判别器。 唯一的区别是,所生成的伪造品和数据集的真实图像均以其相应的“一键通”标签作为条件。
生成器的新损失函数可最大程度地减少对以指定的一幅热标签为条件的伪造图像进行鉴别的正确预测。 生成器学习如何在给定单热向量的情况下生成特定的 MNIST 数字,该数字可能使判别器蒙蔽。“图 4.3.3”显示了如何训练生成器。

图 4.3.3:通过对抗网络训练 CGAN 生成器类似于训练 GAN 生成器。 唯一的区别是,生成的伪造图像以“一热”标签为条件
“列表 4.3.1”突出显示了判别器模型中所需的微小更改。 该代码使用Dense层处理单热点向量,并将其与输入图像连接在一起。 修改了Model实例以用于图像和一键输入向量。
“列表 4.3.1”:cgan-mnist-4.3.1.py
突出显示了 DCGAN 中所做的更改:
def build_discriminator(inputs, labels, image_size):
"""Build a Discriminator Model
Inputs are concatenated after Dense layer.
Stack of LeakyReLU-Conv2D to discriminate real from fake.
The network does not converge with BN so it is not used here
unlike in DCGAN paper.
Arguments:
inputs (Layer): Input layer of the discriminator (the image)
labels (Layer): Input layer for one-hot vector to condition
the inputs
image_size: Target size of one side (assuming square image)
Returns:
discriminator (Model): Discriminator Model
"""
kernel_size = 5
layer_filters = [32, 64, 128, 256]
x = inputs
y = Dense(image_size * image_size)(labels)
y = Reshape((image_size, image_size, 1))(y)
x = concatenate([x, y])
for filters in layer_filters:
# first 3 convolution layers use strides = 2
# last one uses strides = 1
if filters == layer_filters[-1]:
strides = 1
else:
strides = 2
x = LeakyReLU(alpha=0.2)(x)
x = Conv2D(filters=filters,
kernel_size=kernel_size,
strides=strides,
padding='same')(x)
x = Flatten()(x)
x = Dense(1)(x)
x = Activation('sigmoid')(x)
# input is conditioned by labels
discriminator = Model([inputs, labels], x, name='discriminator')
return discriminator
以下“列表 4.3.2”突出显示了代码更改,以在生成器生成器函数中合并条件化单热标签。 对于z向量和单热向量输入,修改了Model实例。
“列表 4.3.2”:cgan-mnist-4.3.1.py
突出显示了 DCGAN 中所做的更改:
def build_generator(inputs, labels, image_size):
"""Build a Generator Model
Inputs are concatenated before Dense layer.
Stack of BN-ReLU-Conv2DTranpose to generate fake images.
Output activation is sigmoid instead of tanh in orig DCGAN.
Sigmoid converges easily.
Arguments:
inputs (Layer): Input layer of the generator (the z-vector)
labels (Layer): Input layer for one-hot vector to condition the inputs
image_size: Target size of one side (assuming square image)
Returns:
generator (Model): Generator Model
"""
image_resize = image_size // 4
# network parameters
kernel_size = 5
layer_filters = [128, 64, 32, 1]
x = concatenate([inputs, labels], axis=1)
x = Dense(image_resize * image_resize * layer_filters[0])(x)
x = Reshape((image_resize, image_resize, layer_filters[0]))(x)
for filters in layer_filters:
# first two convolution layers use strides = 2
# the last two use strides = 1
if filters > layer_filters[-2]:
strides = 2
else:
strides = 1
x = BatchNormalization()(x)
x = Activation('relu')(x)
x = Conv2DTranspose(filters=filters,
kernel_size=kernel_size,
strides=strides,
padding='same')(x)
x = Activation('sigmoid')(x)
# input is conditioned by labels
generator = Model([inputs, labels], x, name='generator')
return generator
“列表 4.3.3”突出显示了在train()函数中所做的更改,以适应判别器和生成器的条件一热向量。 首先对 CGAN 判别器进行训练,以一批真实和伪造的数据为条件,这些数据以其各自的热门标签为条件。 然后,在给定单热标签条件假冒数据为假的情况下,通过训练对抗网络来更新生成器参数。 与 DCGAN 相似,在对抗训练中,判别器权重被冻结。
“列表 4.3.3”:cgan-mnist-4.3.1.py
着重介绍了 DCGAN 中所做的更改:
def train(models, data, params):
"""Train the Discriminator and Adversarial Networks
Alternately train Discriminator and Adversarial networks by batch.
Discriminator is trained first with properly labelled real and fake images.
Adversarial is trained next with fake images pretending to be real.
Discriminator inputs are conditioned by train labels for real images,
and random labels for fake images.
Adversarial inputs are conditioned by random labels.
Generate sample images per save_interval.
Arguments:
models (list): Generator, Discriminator, Adversarial models
data (list): x_train, y_train data
params (list): Network parameters
"""
# the GAN models
generator, discriminator, adversarial = models
# images and labels
x_train, y_train = data
# network parameters
batch_size, latent_size, train_steps, num_labels, model_name = params
# the generator image is saved every 500 steps
save_interval = 500
# noise vector to see how the generator output evolves during training
noise_input = np.random.uniform(-1.0, 1.0, size=[16, latent_size])
# one-hot label the noise will be conditioned to
noise_class = np.eye(num_labels)[np.arange(0, 16) % num_labels]
# number of elements in train dataset
train_size = x_train.shape[0]
print(model_name,
"Labels for generated images: ",
np.argmax(noise_class, axis=1))
for i in range(train_steps):
# train the discriminator for 1 batch
# 1 batch of real (label=1.0) and fake images (label=0.0)
# randomly pick real images from dataset
rand_indexes = np.random.randint(0, train_size, size=batch_size)
real_images = x_train[rand_indexes]
# corresponding one-hot labels of real images
real_labels = y_train[rand_indexes]
# generate fake images from noise using generator
noise = np.random.uniform(-1.0,
1.0,
size=[batch_size, latent_size])
# assign random one-hot labels
fake_labels = np.eye(num_labels)[np.random.choice(num_labels,batch_size)]
# generate fake images conditioned on fake labels
fake_images = generator.predict([noise, fake_labels])
# real + fake images = 1 batch of train data
x = np.concatenate((real_images, fake_images))
# real + fake one-hot labels = 1 batch of train one-hot labels
labels = np.concatenate((real_labels, fake_labels))
# label real and fake images
# real images label is 1.0
y = np.ones([2 * batch_size, 1])
# fake images label is 0.0
y[batch_size:, :] = 0.0
# train discriminator network, log the loss and accuracy
loss, acc = discriminator.train_on_batch([x, labels], y)
log = "%d: [discriminator loss: %f, acc: %f]" % (i, loss, acc)
# train the adversarial network for 1 batch
# 1 batch of fake images conditioned on fake 1-hot labels
# w/ label=1.0
# since the discriminator weights are frozen in
# adversarial network only the generator is trained
# generate noise using uniform distribution
noise = np.random.uniform(-1.0,
1.0,
size=[batch_size, latent_size])
# assign random one-hot labels
fake_labels = np.eye(num_labels)[np.random.choice(num_labels,batch_size)]
# label fake images as real or 1.0
y = np.ones([batch_size, 1])
# train the adversarial network
# note that unlike in discriminator training,
# we do not save the fake images in a variable
# the fake images go to the discriminator input of the adversarial
# for classification
# log the loss and accuracy
loss, acc = adversarial.train_on_batch([noise, fake_labels], y)
log = "%s [adversarial loss: %f, acc: %f]" % (log, loss, acc)
print(log)
if (i + 1) % save_interval == 0:
# plot generator images on a periodic basis
plot_images(generator,
noise_input=noise_input,
noise_class=noise_class,
show=False,
step=(i + 1),
model_name=model_name)
# save the model after training the generator
# the trained generator can be reloaded for
# future MNIST digit generation
generator.save(model_name + ".h5")
“图 4.3.4”显示了当生成器被调整为产生带有以下标签的数字时生成的 MNIST 数字的演变:
[0 1 2 3
4 5 6 7
8 9 0 1
2 3 4 5]
我们可以看到以下结果:

图 4.3.4:使用标签[0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5]对 CGAN 在不同训练步骤中生成的伪造图像
鼓励您运行经过训练的生成器模型,以查看新的合成 MNIST 数字图像:
python3 cgan-mnist-4.3.1.py --generator=cgan_mnist.h5
或者,也可以请求要生成的特定数字(例如 8):
python3 cgan-mnist-4.3.1.py --generator=cgan_mnist.h5 --digit=8
使用 CGAN,就像有一个智能体,我们可以要求绘制数字,类似于人类如何写数字。 与 DCGAN 相比,CGAN 的主要优势在于我们可以指定希望智能体绘制的数字。
4。结论
本章讨论了 GAN 的一般原理,以便为我们现在要讨论的更高级的主题奠定基础,包括改进的 GAN,解缠的表示 GAN 和跨域 GAN。 我们从了解 GAN 如何由两个网络(称为生成器和判别器)组成的这一章开始。 判别器的作用是区分真实信号和虚假信号。 生成器的目的是欺骗判别器。 生成器通常与判别器结合以形成对抗网络。 生成器是通过训练对抗网络来学习如何生成可欺骗判别器的虚假数据的。
我们还了解了 GAN 的构建方法,但众所周知,其操作起来非常困难。 提出了tf.keras中的两个示例实现。 DCGAN 证明了可以训练 GAN 使用深层 CNN 生成伪造图像。 伪造的图像是 MNIST 数字。 但是,DCGAN 生成器无法控制应绘制的特定数字。 CGAN 通过调节生成器以绘制特定数字来解决此问题。 该病是单热标签的形式。 如果我们要构建可以生成特定类数据的智能体,则 CGAN 很有用。
在下一章中,将介绍 DCGAN 和 CGAN 的改进。 特别是,重点将放在如何稳定 DCGAN 的训练以及如何提高 CGAN 的感知质量上。 这将通过引入新的损失函数和稍有不同的模型架构来完成。
5. 参考
Ian Goodfellow. NIPS 2016 Tutorial: Generative Adversarial Networks. arXiv preprint arXiv:1701.00160, 2016 (https://arxiv.org/pdf/1701.00160.pdf).Alec Radford, Luke Metz, and Soumith Chintala. Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks. arXiv preprint arXiv:1511.06434, 2015 (https://arxiv.org/pdf/1511.06434.pdf).Mehdi Mirza and Simon Osindero. Conditional Generative Adversarial Nets. arXiv preprint arXiv:1411.1784, 2014 (https://arxiv.org/pdf/1411.1784.pdf).Tero Karras et al. Progressive Growing of GANs for Improved Quality, Stability, and Variation. ICLR, 2018 (https://arxiv.org/pdf/1710.10196.pdf).Tero Karras, , Samuli Laine, and Timo Aila. A Style-Based Generator Architecture for Generative Adversarial Networks. Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2019.Tero Karras et al. Analyzing and Improving the Image Quality of StyleGAN. 2019 (https://arxiv.org/abs/1912.04958).
五、改进的 GAN
自 2014 年引入生成对抗网络(GAN)以来,其流行度迅速提高。 GAN 已被证明是有用的生成模型,可以合成看起来真实的新数据。 深度学习中的许多研究论文都遵循提出的措施来解决原始 GAN 的困难和局限性。
正如我们在前几章中讨论的那样,众所周知,GAN 很难训练,并且易于崩溃。 模式损失是一种情况,即使损失函数已经被优化,但生成器仍会产生看起来相同的输出。 在 MNIST 数字的情况下,模式折叠时,生成器可能只产生数字 4 和 9,因为看起来很相似。 Wasserstein GAN(WGAN)[2]解决了这些问题,认为只需替换基于 Wasserstein 的 GAN 损失函数就可以稳定的训练和避免模式崩溃,也称为陆地移动距离(EMD)。
但是,稳定性问题并不是 GAN 的唯一问题。 也越来越需要来提高所生成图像的感知质量。 最小二乘 GAN(LSGAN)[3]建议同时解决这两个问题。 基本前提是,在训练过程中,Sigmoid 交叉熵损失会导致梯度消失。 这导致较差的图像质量。 最小二乘损失不会导致梯度消失。 与原始 GAN 生成的图像相比,生成的生成图像具有更高的感知质量。
在上一章中,CGAN 介绍了一种调节生成器输出的方法。 例如,如果要获取数字 8,则可以在生成器的输入中包含条件标签。 受 CGAN 的启发,辅助分类器 GAN(ACGAN)[4]提出了一种改进的条件算法,可产生更好的感知质量和输出多样性。
总之,本章的目的是介绍:
- WGAN 的理论描述
- 对 LSGAN 原理的理解
- 对 ACGAN 原理的理解
- 改进的 GAN 的
tf.keras实现 – WGAN,LSGAN 和 ACGAN
让我们从讨论 WGAN 开始。
1. Wasserstein GAN
如前所述,众所周知,GAN 很难训练。 判别器和生成器这两个网络的相反目标很容易导致训练不稳定。 判别器尝试从真实数据中正确分类伪造数据。 同时,生成器将尽最大努力欺骗判别器。 如果判别器的学习速度比生成器快,则生成器参数将无法优化。 另一方面,如果判别器学习较慢,则梯度可能会在到达生成器之前消失。 在最坏的情况下,如果判别器无法收敛,则生成器将无法获得任何有用的反馈。
WGAN 认为 GAN 固有的不稳定性是由于它的损失函数引起的,该函数基于 Jensen-Shannon(JS)距离。 在 GAN 中,生成器的目的是学习如何将一种源分布(例如噪声)从转换为估计的目标分布(例如 MNIST 数字)。 使用 GAN 的原始公式,损失函数实际上是使目标分布与其估计值之间的距离最小。 问题是,对于某些分布对,没有平滑的路径可以最小化此 JS 距离。 因此,训练将无法收敛。
在以下部分中,我们将研究三个距离函数,并分析什么可以替代更适合 GAN 优化的 JS 距离函数。
距离函数
可以通过检查其损失函数来了解训练 GAN 的稳定性。 为了更好地理解 GAN 损失函数,我们将回顾两个概率分布之间的公共距离或散度函数。
我们关注的是用于真实数据分配的p_data与用于生成器数据分配的p_g之间的距离。 GAN 的目标是制造p_g -> p_data。“表 5.1.1”显示了散度函数。
在大多数个最大似然任务中,我们将使用 Kullback-Leibler(KL)散度,或D[KL]损失函数可以衡量我们的神经网络模型预测与真实分布函数之间的距离。 如“公式 5.1.1”所示,由于D[KL](p_data || p_g) ≠ D[KL](p_g || p_data),所以D[KL]不对称。
JS 或D[JS]是基于D[KL]的差异。 但是,与D[KL]不同,D[JS]是对称的并且是有限的。 在本节中,我们将演示优化 GAN 损失函数等同于优化D[JS]:
| 散度 | 表达式 |
|---|---|
| Kullback-Leibler(KL)“公式 5.1.1” | ![]() |
![]() |
|
| *詹森·香农(JS)“公式 5.1.2” | ![]() |
| 陆地移动距离(EMD)或 Wasserstein 1 “公式 5.1.3” | ![]() |
其中Π(p_data, p_g)是所有联合分布γ(x, y)的集合,其边际为p_data和p_g。 |
表 5.1.1:两个概率分布函数p_data和p_g之间的散度函数
EMD 背后的想法是,它是d = ||x - y||传输多少质量γ(x, y),为了让概率分布p_data匹配p_g的度量。 γ(x, y)是所有可能的联合分布Π(p_data, p_g)的空间中的联合分布。 γ(x, y)也被称为运输计划,以反映运输质量以匹配两个概率分布的策略。 给定两个概率分布,有许多可能的运输计划。 大致而言, inf表示成本最低的运输计划。
例如,“图 5.1.1”向我们展示了两个简单的离散分布x和y:

图 5.1.1:EMD 是从x传输以匹配目标分布y的质量的加权数量。
在位置i = 1, 2, 3, 4上,x在具有质量m[i], i = 1, 2, 3, 4。同时,位置y[i], i = 1, 2上,y的质量为m[i], i = 1, 2。为了匹配分布y,图中的箭头显示了将每个质量xᵢ移动dᵢ的最小运输计划。 EMD 计算如下:
(Equation 5.1.4)
在“图 5.1.1”中,EMD 可解释为移动一堆污物x填充孔y所需的最少工作量。 尽管在此示例中,也可以从图中推导出inf,但在大多数情况下,尤其是在连续分布中,用尽所有可能的运输计划是很棘手的。 我们将在本章中稍后回到这个问题。 同时,我们将向您展示 GAN 损失函数的作用,实际上是如何使 JS 的差异最小化。
GAN 中的距离函数
现在,在上一章的损失函数给定任何生成器的情况下,我们将计算最佳判别器。 我们将回顾上一章中的以下等式:
(Equation 4.1.1)
除了从噪声分布中采样外,前面的等式也可以表示为从生成器分布中采样:
(Equation 5.1.5)
找出最小的L^(D):
(Equation 5.1.6)
(Equation 5.1.7)
积分内部的项为y -> a log(y) + b log(1 - y)的形式,对于不包括{0, 0}的任何a, b ∈ R^2,在y ∈ [0. 1]的a / (a + b)处都有一个已知的最大值。 由于该积分不会更改此表达式的最大值(或L^(D)的最小值)的位置,因此最佳判别器为:
(Equation 5.1.8)
因此,给定最佳判别器的损失函数为:
(Equation 5.1.9)
(Equation 5.1.10)
(Equation 5.1.11)
(Equation 5.1.12)
我们可以从“公式 5.1.12”观察到,最佳判别器的损失函数为常数减去真实分布p_data和任何生成器分布p_g之间的 JS 散度的两倍。 最小化L^(D*)意味着最大化D[JS](p_data || p_g),否则判别器必须正确地将真实数据中的伪造物分类。
同时,我们可以放心地说,最佳生成器是当生成器分布等于真实数据分布时:
(Equation 5.1.13)
这是有道理的,因为生成器的目的是通过学习真实的数据分布来欺骗判别器。 有效地,我们可以通过最小化D[JS]或通过制作p_g -> p_data来获得最佳生成器。 给定最佳生成器,最佳判别器为D*(x) = 1 / 2和L^(D*) = 2log2 = 0.60。
问题在于,当两个分布没有重叠时,就没有平滑函数可以帮助缩小它们之间的差距。 训练 GAN 不会因梯度下降而收敛。 例如,假设:
p_data = (x, y) where x = 0, y ~ U(0, 1) (Equation 5.1.14)
p_g = (x, y) where x = θ, y ~ U(0, 1) (Equation 5.1.15)
这两个分布显示在“图 5.1.2”中:

图 5.1.2:没有重叠的两个分布的示例。 对于p_g,θ = 0.5
是均匀分布。 每个距离函数的差异如下:
由于D[JS]是一个常数,因此 GAN 将没有足够的梯度来驱动p_g -> p_data。 我们还会发现D[KL]或反向D[KL]也不起作用。 但是,通过W(p_data, p_g),我们可以拥有平滑函数,以便通过梯度下降获得p_g -> p_data。 为了优化 GAN,EMD 或 Wasserstein 1 似乎是一个更具逻辑性的损失函数,因为在两个分布具有极小或没有重叠的情况下,D[JS]会失败。
为了帮助进一步理解,可以在以下位置找到有关距离函数的精彩讨论。
在下一节中,我们将重点介绍使用 EMD 或 Wasserstein 1 距离函数来开发替代损失函数,以鼓励稳定训练 GAN。
使用 Wasserstein 损失
在使用 EMD 或 Wasserstein 1 之前,还有一个要解决的问题。 耗尽Π(p_data, p_g)的空间来找到γ ~ Π(p_data, p_g)是很棘手的。 提出的解决方案是使用其 Kantorovich-Rubinstein 对偶:
(Equation 5.1.16)
等效地,EMD sup ||f||_L <= 1是所有 K-Lipschitz 函数上的最高值(大约是最大值):f: x -> R。 K-Lipschitz 函数满足以下约束:
(Equation 5.1.17)
对于所有x[1], x[2] ∈ R。 K-Lipschitz 函数具有有界导数,并且几乎总是连续可微的(例如,f(x) = |x|具有有界导数并且是连续的,但在x = 0时不可微分)。
“公式 5.1.16”可以通过找到 K-Lipschitz 函数{f[w]}, w ∈ W的族来求解:
(Equation 5.1.18)
在 GAN 中,可以通过从z-噪声分布采样并用f[w]替换“公式 5.1.18”来重写。 鉴别函数,D[w]:
(Equation 5.1.19)
我们使用粗体字母突出显示多维样本的一般性。 最后一个问题是如何找到函数族w ∈ W。 所提出的解决方案是在每次梯度更新时进行的。 判别器w的权重被限制在上下限之间(例如,-0.01 和 0.01):
(Equation 5.1.20)
w的较小值将判别器约束到紧凑的参数空间,从而确保 Lipschitz 连续性。
我们可以使用“公式 5.1.19”作为我们新的 GAN 损失函数的基础。 EMD 或 Wasserstein 1 是生成器旨在最小化的损失函数,以及判别器试图最大化的损失函数(或最小化-W(p_data, p_g):
(Equation 5.1.21)
(Equation 5.1.22)
在生成器损失函数中,第一项消失了,因为它没有针对实际数据进行直接优化。
“表 5.1.2”显示了 GAN 和 WGAN 的损失函数之间的差异。 为简洁起见,我们简化了L^(D)和L^(G)的表示法:
| 网络 | 损失函数 | 公式 |
|---|---|---|
| GAN | ![]() |
4.1.1 |
![]() |
4.1.5 | |
| WGAN | ![]() |
5.1.21 |
![]() |
5.1.22 | |
![]() |
5.1.20 |
表 5.1.2:GAN 和 WGAN 的损失函数之间的比较
这些损失函数用于训练 WGAN,如“算法 5.1.1”中所示。
算法 5.1.1 WGAN。 参数的值为α = 0.00005,c = 0.01,m = 64和n_critic = 5。
要求:α,学习率。c是削波参数。m,批量大小。 n_critic,即每个生成器迭代的评论(鉴别)迭代次数。
要求:w[D],初始判别器(discriminator)参数。 θ[D],初始生成器参数:
-
当
θ[D]尚未收敛,执行: -
对于
t = 1, ..., n_critic,执行: -
从真实数据中抽样一批
{x^(i)} ~ p_data, i = 1, ..., m -
从均匀的噪声分布中采样一批
{z^(i)} ~ p_x, i = 1, ..., m -
![]()
计算判别器梯度
-
![]()
更新判别器参数
-
![]()
剪辑判别器权重
-
end for -
从均匀的噪声分布中采样一批
{z^(i)} ~ p_x, i = 1, ..., m -
![]()
计算生成器梯度
更新生成器参数
end while
“图 5.1.3”展示了 WGAN 模型实际上与 DCGAN 相同,除了伪造的/真实的数据标签和损失函数:

图 5.1.3:顶部:训练 WGAN 判别器需要来自生成器的虚假数据和来自真实分发的真实数据。 下:训练 WGAN 生成器要求生成器中假冒的真实数据是真实的
与 GAN 相似,WGAN 交替训练判别器和生成器(通过对抗)。 但是,在 WGAN 中,判别器(也称为评论者)在训练生成器进行一次迭代(第 9 至 11 行)之前,先训练n_critic迭代(第 2 至 8 行)。 这与对于判别器和生成器具有相同数量的训练迭代的 GAN 相反。 换句话说,在 GAN 中,n_critic = 1。
训练判别器意味着学习判别器的参数(权重和偏差)。 这需要从真实数据中采样一批(第 3 行),并从伪数据中采样一批(第 4 行),然后将采样数据馈送到判别器网络,然后计算判别器参数的梯度(第 5 行)。 判别器参数使用 RMSProp(第 6 行)进行了优化。 第 5 行和第 6 行都是“公式 5.1.21”的优化。
最后,EM 距离优化中的 Lipschitz 约束是通过裁剪判别器参数(第 7 行)来施加的。 第 7 行是“公式 5.1.20”的实现。 在n_critic迭代判别器训练之后,判别器参数被冻结。 生成器训练通过对一批伪造数据进行采样开始(第 9 行)。 采样的数据被标记为实数(1.0),以致愚弄判别器网络。 在第 10 行中计算生成器梯度,并在第 11 行中使用 RMSProp 对其进行优化。第 10 行和第 11 行执行梯度更新以优化“公式 5.1.22”。
训练生成器后,将解冻判别器参数,并开始另一个n_critic判别器训练迭代。 我们应该注意,在判别器训练期间不需要冻结生成器参数,因为生成器仅涉及数据的制造。 类似于 GAN,可以将判别器训练为一个单独的网络。 但是,训练生成器始终需要判别器通过对抗网络参与,因为损失是根据生成器网络的输出计算得出的。
与 GAN 不同,在 WGAN 中,将实际数据标记为 1.0,而将伪数据标记为 -1.0,作为计算第 5 行中的梯度的一种解决方法。第 5-6 和 10-11 行执行梯度更新以优化“公式 5.1.21”和“5.1.22”。 第 5 行和第 10 行中的每一项均建模为:
(Equation 5.1.23)
对于真实数据,其中y_label = 1.0,对于假数据,y_label= -1.0。 为了简化符号,我们删除了上标(i)。 对于判别器,当使用实际数据进行训练时,WGAN 增加y_pred = D[w](x)以最小化损失函数。
使用伪造数据进行训练时,WGAN 会降低y_pred = D[w](g(z))以最大程度地减少损失函数。 对于生成器,当在训练过程中将伪数据标记为真实数据时,WGAN 增加y_pred = D[w](g(z))以最小化损失函数。 请注意,y_label除了其符号外,对损失函数没有直接贡献。 在tf.keras中,“公式 5.1.23”实现为:
def wasserstein_loss(y_label, y_pred):
return -K.mean(y_label * y_pred)
本节最重要的部分是用于稳定训练 GAN 的新损失函数。 它基于 EMD 或 Wasserstein1。“算法 5.1.1”形式化了 WGAN 的完整训练算法,包括损失函数。 在下一节中,将介绍tf.keras中训练算法的实现。
使用 Keras 的 WGAN 实现
为了在tf.keras中实现 WGAN,我们可以重用 GAN 的 DCGAN 实现,这是我们在上一一章中介绍的。 DCGAN 构建器和工具函数在lib文件夹的gan.py中作为模块实现。
函数包括:
generator():生成器模型构建器discriminator():判别器模型构建器train():DCGAN 训练师plot_images():通用生成器输出绘图仪test_generator():通用的生成器测试工具
如“列表 5.1.1”所示,我们可以通过简单地调用以下命令来构建一个判别器:
discriminator = gan.discriminator(inputs, activation='linear')
WGAN 使用线性输出激活。 对于生成器,我们执行:
generator = gan.generator(inputs, image_size)
tf.keras中的整体网络模型类似于 DCGAN 的“图 4.2.1”中看到的模型。
“列表 5.1.1”突出显示了 RMSprop 优化器和 Wasserstein 损失函数的使用。 在训练期间使用“算法 5.1.1”中的超参数。
“列表 5.1.1”:wgan-mnist-5.1.2.py
def build_and_train_models():
"""Load the dataset, build WGAN discriminator,
generator, and adversarial models.
Call the WGAN train routine.
"""
# load MNIST dataset
(x_train, _), (_, _) = mnist.load_data()
# reshape data for CNN as (28, 28, 1) and normalize
image_size = x_train.shape[1]
x_train = np.reshape(x_train, [-1, image_size, image_size, 1])
x_train = x_train.astype('float32') / 255
model_name = "wgan_mnist"
# network parameters
# the latent or z vector is 100-dim
latent_size = 100
# hyper parameters from WGAN paper [2]
n_critic = 5
clip_value = 0.01
batch_size = 64
lr = 5e-5
train_steps = 40000
input_shape = (image_size, image_size, 1)
# build discriminator model
inputs = Input(shape=input_shape, name='discriminator_input')
# WGAN uses linear activation in paper [2]
discriminator = gan.discriminator(inputs, activation='linear')
optimizer = RMSprop(lr=lr)
# WGAN discriminator uses wassertein loss
discriminator.compile(loss=wasserstein_loss,
optimizer=optimizer,
metrics=['accuracy'])
discriminator.summary()
# build generator model
input_shape = (latent_size, )
inputs = Input(shape=input_shape, name='z_input')
generator = gan.generator(inputs, image_size)
generator.summary()
# build adversarial model = generator + discriminator
# freeze the weights of discriminator during adversarial training
discriminator.trainable = False
adversarial = Model(inputs,
discriminator(generator(inputs)),
name=model_name)
adversarial.compile(loss=wasserstein_loss,
optimizer=optimizer,
metrics=['accuracy'])
adversarial.summary()
# train discriminator and adversarial networks
models = (generator, discriminator, adversarial)
params = (batch_size,
latent_size,
n_critic,
clip_value,
train_steps,
model_name)
train(models, x_train, params)
“列表 5.1.2”是紧跟“算法 5.1.1”的训练函数。 但是,在判别器的训练中有一个小的调整。 与其在单个合并的真实数据和虚假数据中组合训练权重,不如先训练一批真实数据,然后再训练一批虚假数据。 这种调整将防止梯度消失,因为真实和伪造数据标签中的符号相反,并且由于裁剪而导致的权重较小。
“列表 5.1.2”:wgan-mnist-5.1.2.py
为 WGAN 训练算法:
def train(models, x_train, params):
"""Train the Discriminator and Adversarial Networks
Alternately train Discriminator and Adversarial
networks by batch.
Discriminator is trained first with properly labelled
real and fake images for n_critic times.
Discriminator weights are clipped as a requirement
of Lipschitz constraint.
Generator is trained next (via Adversarial) with
fake images pretending to be real.
Generate sample images per save_interval
Arguments:
models (list): Generator, Discriminator,
Adversarial models
x_train (tensor): Train images
params (list) : Networks parameters
"""
# the GAN models
generator, discriminator, adversarial = models
# network parameters
(batch_size, latent_size, n_critic,
clip_value, train_steps, model_name) = params
# the generator image is saved every 500 steps
save_interval = 500
# noise vector to see how the
# generator output evolves during training
noise_input = np.random.uniform(-1.0,
1.0,
size=[16, latent_size])
# number of elements in train dataset
train_size = x_train.shape[0]
# labels for real data
real_labels = np.ones((batch_size, 1))
for i in range(train_steps):
# train discriminator n_critic times
loss = 0
acc = 0
for _ in range(n_critic):
# train the discriminator for 1 batch
# 1 batch of real (label=1.0) and
# fake images (label=-1.0)
# randomly pick real images from dataset
rand_indexes = np.random.randint(0,
train_size,
size=batch_size)
real_images = x_train[rand_indexes]
# generate fake images from noise using generator
# generate noise using uniform distribution
noise = np.random.uniform(-1.0,
1.0,
size=[batch_size, latent_size])
fake_images = generator.predict(noise)
# train the discriminator network
# real data label=1, fake data label=-1
# instead of 1 combined batch of real and fake images,
# train with 1 batch of real data first, then 1 batch
# of fake images.
# this tweak prevents the gradient
# from vanishing due to opposite
# signs of real and fake data labels (i.e. +1 and -1) and
# small magnitude of weights due to clipping.
real_loss, real_acc = \
discriminator.train_on_batch(real_images,
real_labels)
fake_loss, fake_acc = \
discriminator.train_on_batch(fake_images,
-real_labels)
# accumulate average loss and accuracy
loss += 0.5 * (real_loss + fake_loss)
acc += 0.5 * (real_acc + fake_acc)
# clip discriminator weights to satisfy Lipschitz constraint
for layer in discriminator.layers:
weights = layer.get_weights()
weights = [np.clip(weight,
-clip_value,
clip_value) for weight in weights]
layer.set_weights(weights)
# average loss and accuracy per n_critic training iterations
loss /= n_critic
acc /= n_critic
log = "%d: [discriminator loss: %f, acc: %f]" % (i, loss, acc)
# train the adversarial network for 1 batch
# 1 batch of fake images with label=1.0
# since the discriminator weights are frozen in
# adversarial network only the generator is trained
# generate noise using uniform distribution
noise = np.random.uniform(-1.0,
1.0,
size=[batch_size, latent_size])
# train the adversarial network
# note that unlike in discriminator training,
# we do not save the fake images in a variable
# the fake images go to the discriminator
# input of the adversarial for classification
# fake images are labelled as real
# log the loss and accuracy
loss, acc = adversarial.train_on_batch(noise, real_labels)
log = "%s [adversarial loss: %f, acc: %f]" % (log, loss, acc)
print(log)
if (i + 1) % save_interval == 0:
# plot generator images on a periodic basis
gan.plot_images(generator,
noise_input=noise_input,
show=False,
step=(i + 1),
model_name=model_name)
# save the model after training the generator
# the trained generator can be reloaded
# for future MNIST digit generation
generator.save(model_name + ".h5")
“图 5.1.4”显示了 MNIST 数据集上 WGAN 输出的演变:

图 5.1.4:WGAN 与训练步骤的示例输出。 在训练和测试期间,WGAN 的任何输出均不会遭受模式崩溃
即使在网络配置更改的情况下,WGAN 也稳定。 例如,当在识别符网络的 ReLU 之前插入批量规范化时,已知 DCGAN 不稳定。 在 WGAN 中,相同的配置是稳定的。
下图“图 5.1.5”向我们展示了 DCGAN 和 WGAN 的输出,并在判别器网络上进行了批量归一化:

图 5.1.5:在判别器网络中的 ReLU 激活之前插入批量归一化时,DCGAN(左)和 WGAN(右)的输出比较
与上一章中的 GAN 训练相似,经过 40,000 个训练步骤,将训练后的模型保存在文件中。 使用训练有素的生成器模型,通过运行以下命令来生成新的合成 MNIST 数字图像:
python3 wgan-mnist-5.1.2.py --generator=wgan_mnist.h5
正如我们所讨论的,原始 GAN 很难训练。 当 GAN 优化的损失函数时,就会出现问题。 实际上是在优化 JS 差异,D[JS]。 当两个分布函数之间几乎没有重叠时,很难优化D[JS]。
WGAN 提出通过使用 EMD 或 Wasserstein 1 损失函数来解决该问题,该函数即使在两个分布之间很少或没有重叠时也具有平滑的微分函数。 但是,WGAN 与生成的图像质量无关。 除了稳定性问题之外,原始 GAN 生成的图像在感知质量方面还有很多改进的地方。 LSGAN 理论上可以同时解决两个问题。 在下一节中,我们将介绍 LSGAN。
2. 最小二乘 GAN(LSGAN)
LSGAN 提出最小二乘损失。“图 5.2.1”演示了为什么在 GAN 中使用 Sigmoid 交叉熵损失会导致生成的数据质量较差:

图 5.2.1:真实样本和虚假样本分布均除以各自的决策边界:Sigmoid 和最小二乘
理想情况下,假样本分布应尽可能接近真实样本的分布。 但是,对于 GAN,一旦伪样本已经位于决策边界的正确一侧,梯度就消失了。
这会阻止生成器具有足够的动机来提高生成的伪数据的质量。 远离决策边界的伪样本将不再试图靠近真实样本的分布。 使用最小二乘损失函数,只要假样本分布与真实样本的分布相距甚远,梯度就不会消失。 即使假样本已经位于决策边界的正确一侧,生成器也将努力改善其对实际密度分布的估计。
“表 5.2.1”显示了 GAN,WGAN 和 LSGAN 之间的损失函数的比较:
| 网络 | 损失函数 | 公式 |
|---|---|---|
| GAN | ![]() |
4.1.1 |
![]() |
4.1.5 | |
| WGAN | ![]() |
5.1.21 |
![]() |
5.1.22 | |
![]() |
5.1.20 | |
| LSGAN | ![]() |
5.2.1 |
![]() |
5.2.2 |
表 5.2.1:GAN,WGAN 和 LSGAN 损失函数之间的比较
最小化“公式 5.2.1”或判别器损失函数意味着实际数据分类与真实标签 1.0 之间的 MSE 应该接近零。 此外,假数据分类和真实标签 0.0 之间的 MSE 应该接近零。
与其他 GAN 相似,对 LSGAN 判别器进行了训练,可以从假数据样本中对真实数据进行分类。 最小化公式 5.2.2 意味着在标签 1.0 的帮助下,使判别器认为生成的假样本数据是真实的。
以上一章中的 DCGAN 代码为基础来实现 LSGAN 仅需进行一些更改。 如“列表 5.2.1”所示,删除了判别器 Sigmoid 激活。 判别器是通过调用以下命令构建的:
discriminator = gan.discriminator(inputs, activation=None)
生成器类似于原始的 DCGAN:
generator = gan.generator(inputs, image_size)
鉴别函数和对抗损失函数都被mse代替。 所有网络参数均与 DCGAN 中的相同。 tf.keras中 LSGAN 的网络模型类似于“图 4.2.1”,除了存在线性激活或无输出激活外。 训练过程类似于 DCGAN 中的训练过程,由工具函数提供:
gan.train(models, x_train, params)
“列表 5.2.1”:lsgan-mnist-5.2.1.py
def build_and_train_models():
"""Load the dataset, build LSGAN discriminator,
generator, and adversarial models.
Call the LSGAN train routine.
"""
# load MNIST dataset
(x_train, _), (_, _) = mnist.load_data()
# reshape data for CNN as (28, 28, 1) and normalize
image_size = x_train.shape[1]
x_train = np.reshape(x_train,
[-1, image_size, image_size, 1])
x_train = x_train.astype('float32') / 255
model_name = "lsgan_mnist"
# network parameters
# the latent or z vector is 100-dim
latent_size = 100
input_shape = (image_size, image_size, 1)
batch_size = 64
lr = 2e-4
decay = 6e-8
train_steps = 40000
# build discriminator model
inputs = Input(shape=input_shape, name='discriminator_input')
discriminator = gan.discriminator(inputs, activation=None)
# [1] uses Adam, but discriminator easily
# converges with RMSprop
optimizer = RMSprop(lr=lr, decay=decay)
# LSGAN uses MSE loss [2]
discriminator.compile(loss='mse',
optimizer=optimizer,
metrics=['accuracy'])
discriminator.summary()
# build generator model
input_shape = (latent_size, )
inputs = Input(shape=input_shape, name='z_input')
generator = gan.generator(inputs, image_size)
generator.summary()
# build adversarial model = generator + discriminator
optimizer = RMSprop(lr=lr*0.5, decay=decay*0.5)
# freeze the weights of discriminator
# during adversarial training
discriminator.trainable = False
adversarial = Model(inputs,
discriminator(generator(inputs)),
name=model_name)
# LSGAN uses MSE loss [2]
adversarial.compile(loss='mse',
optimizer=optimizer,
metrics=['accuracy'])
adversarial.summary()
# train discriminator and adversarial networks
models = (generator, discriminator, adversarial)
params = (batch_size, latent_size, train_steps, model_name)
gan.train(models, x_train, params)
“图 5.2.2”显示了使用 MNIST 数据集对 40,000 个训练步骤进行 LSGAN 训练后生成的样本:

图 5.2.2:LSGAN 的示例输出与训练步骤
与上一章中 DCGAN 中的“图 4.2.1”相比,输出图像的感知质量更好。
使用训练有素的生成器模型,通过运行以下命令来生成新的合成 MNIST 数字图像:
python3 lsgan-mnist-5.2.1.py --generator=lsgan_mnist.h5
在本节中,我们讨论了损失函数的另一种改进。 通过使用 MSE 或 L2,我们解决了训练 GAN 的稳定性和感知质量的双重问题。 在下一节中,提出了相对于 CGAN 的另一项改进,这已在上一章中进行了讨论。
3. 辅助分类器 GAN (ACGAN)
ACGAN 在原理上类似于我们在上一章中讨论的条件 GAN(CGAN)。 我们将比较 CGAN 和 ACGAN。 对于 CGAN 和 ACGAN,生成器输入均为噪声及其标签。 输出是属于输入类标签的伪图像。 对于 CGAN,判别器的输入是图像(假的或真实的)及其标签。 输出是图像真实的概率。 对于 ACGAN,判别器的输入是一幅图像,而输出是该图像是真实的且其类别是标签的概率。
“图 5.3.1”突出显示了生成器训练期间 CGAN 和 ACGAN 之间的区别:

图 5.3.1:CGAN 与 ACGAN 生成器训练。 主要区别是判别器的输入和输出
本质上,在 CGAN 中,我们向网络提供了边信息(标签)。 在 ACGAN 中,我们尝试使用辅助类解码器网络重建辅助信息。 ACGAN 理论认为,强制网络执行其他任务可以提高原始任务的表现。 在这种情况下,附加任务是图像分类。 原始任务是生成伪造图像。
“表 5.3.1”显示了 ACGAN 损失函数与 CGAN 损失函数的比较:
| 网络 | 损失函数 | 编号 |
|---|---|---|
| CGAN | ![]() |
4.3.1 |
![]() |
4.3.2 | |
| ACGAN | ![]() |
5.3.1 |
![]() |
5.3.2 |
表 5.3.1:CGAN 和 ACGAN 损失函数之间的比较
ACGAN 损失函数与 CGAN 相同,除了附加的分类器损失函数。 除了从假图片中识别真实图像的原始任务之外,判别器的“公式 5.3.1”还具有对真假图像正确分类的附加任务。 生成器的“公式 5.3.2”意味着,除了尝试用伪造的图像来欺骗判别器(-E[z] log D(g(z | y)))之外,它还要求判别器正确地对那些伪造的图像进行分类(-E[z] log P(c | g(z | y)))。
从 CGAN 代码开始,仅修改判别器和训练函数以实现 ACGAN。 gan.py还提供了判别器和生成器构建器函数。 要查看判别器上所做的更改,清单 5.3.1 显示了构建器函数,其中突出显示了执行图像分类的辅助解码器网络和双输出。
“列表 5.3.1”:gan.py
def discriminator(inputs,
activation='sigmoid',
num_labels=None,
num_codes=None):
"""Build a Discriminator Model
Stack of LeakyReLU-Conv2D to discriminate real from fake
The network does not converge with BN so it is not used here
unlike in [1]
Arguments:
inputs (Layer): Input layer of the discriminator (the image)
activation (string): Name of output activation layer
num_labels (int): Dimension of one-hot labels for ACGAN & InfoGAN
num_codes (int): num_codes-dim Q network as output
if StackedGAN or 2 Q networks if InfoGAN
Returns:
Model: Discriminator Model
"""
kernel_size = 5
layer_filters = [32, 64, 128, 256]
x = inputs
for filters in layer_filters:
# first 3 convolution layers use strides = 2
# last one uses strides = 1
if filters == layer_filters[-1]:
strides = 1
else:
strides = 2
x = LeakyReLU(alpha=0.2)(x)
x = Conv2D(filters=filters,
kernel_size=kernel_size,
strides=strides,
padding='same')(x)
x = Flatten()(x)
# default output is probability that the image is real
outputs = Dense(1)(x)
if activation is not None:
print(activation)
outputs = Activation(activation)(outputs)
if num_labels:
# ACGAN and InfoGAN have 2nd output
# 2nd output is 10-dim one-hot vector of label
layer = Dense(layer_filters[-2])(x)
labels = Dense(num_labels)(layer)
labels = Activation('softmax', name='label')(labels)
if num_codes is None:
outputs = [outputs, labels]
else:
# InfoGAN have 3rd and 4th outputs
# 3rd output is 1-dim continous Q of 1st c given x
code1 = Dense(1)(layer)
code1 = Activation('sigmoid', name='code1')(code1)
# 4th output is 1-dim continuous Q of 2nd c given x
code2 = Dense(1)(layer)
code2 = Activation('sigmoid', name='code2')(code2)
outputs = [outputs, labels, code1, code2]
elif num_codes is not None:
# StackedGAN Q0 output
# z0_recon is reconstruction of z0 normal distribution
z0_recon = Dense(num_codes)(x)
z0_recon = Activation('tanh', name='z0')(z0_recon)
outputs = [outputs, z0_recon]
return Model(inputs, outputs, name='discriminator')
然后通过调用以下命令来构建判别器:
discriminator = gan.discriminator(inputs, num_labels=num_labels)
生成器与 WGAN 和 LSGAN 中的生成器相同。 回想一下,在以下“列表 5.3.2”中显示了生成器生成器。 我们应该注意,“列表 5.3.1”和“5.3.2”与上一节中 WGAN 和 LSGAN 使用的生成器函数相同。 重点介绍了适用于 LSGAN 的部件。
“列表 5.3.2”:gan.py
def generator(inputs,
image_size,
activation='sigmoid',
labels=None,
codes=None):
"""Build a Generator Model
Stack of BN-ReLU-Conv2DTranpose to generate fake images.
Output activation is sigmoid instead of tanh in [1].
Sigmoid converges easily.
Arguments:
inputs (Layer): Input layer of the generator (the z-vector)
image_size (int): Target size of one side
(assuming square image)
activation (string): Name of output activation layer
labels (tensor): Input labels
codes (list): 2-dim disentangled codes for InfoGAN
Returns:
Model: Generator Model
"""
image_resize = image_size // 4
# network parameters
kernel_size = 5
layer_filters = [128, 64, 32, 1]
if labels is not None:
if codes is None:
# ACGAN labels
# concatenate z noise vector and one-hot labels
inputs = [inputs, labels]
else:
# infoGAN codes
# concatenate z noise vector,
# one-hot labels and codes 1 & 2
inputs = [inputs, labels] + codes
x = concatenate(inputs, axis=1)
elif codes is not None:
# generator 0 of StackedGAN
inputs = [inputs, codes]
x = concatenate(inputs, axis=1)
else:
# default input is just 100-dim noise (z-code)
x = inputs
x = Dense(image_resize * image_resize * layer_filters[0])(x)
x = Reshape((image_resize, image_resize, layer_filters[0]))(x)
for filters in layer_filters:
# first two convolution layers use strides = 2
# the last two use strides = 1
if filters > layer_filters[-2]:
strides = 2
else:
strides = 1
x = BatchNormalization()(x)
x = Activation('relu')(x)
x = Conv2DTranspose(filters=filters,
kernel_size=kernel_size,
strides=strides,
padding='same')(x)
if activation is not None:
x = Activation(activation)(x)
# generator output is the synthesized image x
return Model(inputs, x, name='generator')
在 ACGAN 中,生成器实例化为:
generator = gan.generator(inputs, image_size, labels=labels)
“图 5.3.2”显示了tf.keras中 ACGAN 的网络模型:

图 5.3.2:ACGAN 的tf.keras模型
如“列表 5.3.3”所示,对判别器和对抗模型进行了修改,以适应判别器网络中的更改。 现在,我们有两个损失函数。 首先是原始的二进制交叉熵,用于训练判别器来估计输入图像为实的概率。
第二个是图像分类器,用于预测类别标签。 输出是一个 10 维的单热向量。
“列表 5.3.3”:acgan-mnist-5.3.1.py
重点介绍了在判别器和对抗网络中实现的更改:
def build_and_train_models():
"""Load the dataset, build ACGAN discriminator,
generator, and adversarial models.
Call the ACGAN train routine.
"""
# load MNIST dataset
(x_train, y_train), (_, _) = mnist.load_data()
# reshape data for CNN as (28, 28, 1) and normalize
image_size = x_train.shape[1]
x_train = np.reshape(x_train,
[-1, image_size, image_size, 1])
x_train = x_train.astype('float32') / 255
# train labels
num_labels = len(np.unique(y_train))
y_train = to_categorical(y_train)
model_name = "acgan_mnist"
# network parameters
latent_size = 100
batch_size = 64
train_steps = 40000
lr = 2e-4
decay = 6e-8
input_shape = (image_size, image_size, 1)
label_shape = (num_labels, )
# build discriminator Model
inputs = Input(shape=input_shape,
name='discriminator_input')
# call discriminator builder
# with 2 outputs, pred source and labels
discriminator = gan.discriminator(inputs,
num_labels=num_labels)
# [1] uses Adam, but discriminator
# easily converges with RMSprop
optimizer = RMSprop(lr=lr, decay=decay)
# 2 loss fuctions: 1) probability image is real
# 2) class label of the image
loss = ['binary_crossentropy', 'categorical_crossentropy']
discriminator.compile(loss=loss,
optimizer=optimizer,
metrics=['accuracy'])
discriminator.summary()
# build generator model
input_shape = (latent_size, )
inputs = Input(shape=input_shape, name='z_input')
labels = Input(shape=label_shape, name='labels')
# call generator builder with input labels
generator = gan.generator(inputs,
image_size,
labels=labels)
generator.summary()
# build adversarial model = generator + discriminator
optimizer = RMSprop(lr=lr*0.5, decay=decay*0.5)
# freeze the weights of discriminator
# during adversarial training
discriminator.trainable = False
adversarial = Model([inputs, labels],
discriminator(generator([inputs, labels])),
name=model_name)
# same 2 loss fuctions: 1) probability image is real
# 2) class label of the image
adversarial.compile(loss=loss,
optimizer=optimizer,
metrics=['accuracy'])
adversarial.summary()
# train discriminator and adversarial networks
models = (generator, discriminator, adversarial)
data = (x_train, y_train)
params = (batch_size, latent_size, \
train_steps, num_labels, model_name)
train(models, data, params)
在“列表 5.3.4”中,我们重点介绍了训练例程中实现的更改。 将与 CGAN 代码进行比较的主要区别在于,必须在鉴别和对抗训练中提供输出标签。
“列表 5.3.4”:acgan-mnist-5.3.1.py
def train(models, data, params):
"""Train the discriminator and adversarial Networks
Alternately train discriminator and adversarial
networks by batch.
Discriminator is trained first with real and fake
images and corresponding one-hot labels.
Adversarial is trained next with fake images pretending
to be real and corresponding one-hot labels.
Generate sample images per save_interval.
# Arguments
models (list): Generator, Discriminator,
Adversarial models
data (list): x_train, y_train data
params (list): Network parameters
"""
# the GAN models
generator, discriminator, adversarial = models
# images and their one-hot labels
x_train, y_train = data
# network parameters
batch_size, latent_size, train_steps, num_labels, model_name \
= params
# the generator image is saved every 500 steps
save_interval = 500
# noise vector to see how the generator
# output evolves during training
noise_input = np.random.uniform(-1.0,
1.0,
size=[16, latent_size])
# class labels are 0, 1, 2, 3, 4, 5,
# 6, 7, 8, 9, 0, 1, 2, 3, 4, 5
# the generator must produce these MNIST digits
noise_label = np.eye(num_labels)[np.arange(0, 16) % num_labels]
# number of elements in train dataset
train_size = x_train.shape[0]
print(model_name,
"Labels for generated images: ",
np.argmax(noise_label, axis=1))
for i in range(train_steps):
# train the discriminator for 1 batch
# 1 batch of real (label=1.0) and fake images (label=0.0)
# randomly pick real images and
# corresponding labels from dataset
rand_indexes = np.random.randint(0,
train_size,
size=batch_size)
real_images = x_train[rand_indexes]
real_labels = y_train[rand_indexes]
# generate fake images from noise using generator
# generate noise using uniform distribution
noise = np.random.uniform(-1.0,
1.0,
size=[batch_size, latent_size])
# randomly pick one-hot labels
fake_labels = np.eye(num_labels)[np.random.choice(num_labels,
batch_size)]
# generate fake images
fake_images = generator.predict([noise, fake_labels])
# real + fake images = 1 batch of train data
x = np.concatenate((real_images, fake_images))
# real + fake labels = 1 batch of train data labels
labels = np.concatenate((real_labels, fake_labels))
# label real and fake images
# real images label is 1.0
y = np.ones([2 * batch_size, 1])
# fake images label is 0.0
y[batch_size:, :] = 0
# train discriminator network, log the loss and accuracy
# ['loss', 'activation_1_loss',
# 'label_loss', 'activation_1_acc', 'label_acc']
metrics = discriminator.train_on_batch(x, [y, labels])
fmt = "%d: [disc loss: %f, srcloss: %f,"
fmt += "lblloss: %f, srcacc: %f, lblacc: %f]"
log = fmt % (i, metrics[0], metrics[1], \
metrics[2], metrics[3], metrics[4])
# train the adversarial network for 1 batch
# 1 batch of fake images with label=1.0 and
# corresponding one-hot label or class
# since the discriminator weights are frozen
# in adversarial network only the generator is trained
# generate noise using uniform distribution
noise = np.random.uniform(-1.0,
1.0,
size=[batch_size, latent_size])
# randomly pick one-hot labels
fake_labels = np.eye(num_labels)[np.random.choice(num_labels,
batch_size)]
# label fake images as real
y = np.ones([batch_size, 1])
# train the adversarial network
# note that unlike in discriminator training,
# we do not save the fake images in a variable
# the fake images go to the discriminator input
# of the adversarial for classification
# log the loss and accuracy
metrics = adversarial.train_on_batch([noise, fake_labels],
[y, fake_labels])
fmt = "%s [advr loss: %f, srcloss: %f,"
fmt += "lblloss: %f, srcacc: %f, lblacc: %f]"
log = fmt % (log, metrics[0], metrics[1],\
metrics[2], metrics[3], metrics[4])
print(log)
if (i + 1) % save_interval == 0:
# plot generator images on a periodic basis
gan.plot_images(generator,
noise_input=noise_input,
noise_label=noise_label,
show=False,
step=(i + 1),
model_name=model_name)
# save the model after training the generator
# the trained generator can be reloaded
# for future MNIST digit generation
generator.save(model_name + ".h5")
可以看出,与其他任务相比,与我们之前讨论的所有 GAN 相比,ACGAN 的表现显着提高。 ACGAN 训练是稳定的,如“图 5.3.3”的 ACGAN 示例输出的以下标签所示:
[0 1 2 3
4 5 6 7
8 9 0 1
2 3 4 5]
与 CGAN 不同,样本输出的外观在训练过程中变化不大。 MNIST 数字图像的感知质量也更好。

图 5.3.3:ACGAN 根据标签的训练步骤生成的示例输出[0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5]
使用训练有素的生成器模型,通过运行以下命令来生成新的合成 MNIST 数字图像:
python3 acgan-mnist-5.3.1.py --generator=acgan_mnist.h5
或者,也可以请求生成要生成的特定数字(例如 3):
python3 acgan-mnist-5.3.1.py --generator=acgan_mnist.h5 --digit=3
“图 5.3.4”显示了 CGAN 和 ACGAN 产生的每个 MNIST 数字的并排比较。 ACGAN 中的数字 2-6 比 CGAN 中的数字质量更好:

图 5.3.4:以数字 0 到 9 为条件的 CGAN 和 ACGAN 输出的并排比较
与 WGAN 和 LSGAN 相似,ACGAN 通过微调的损失函数,对现有 GAN CGAN 进行了改进。 在接下来的章节中,我们将发现新的损失函数,这些函数将使 GAN 能够执行新的有用任务。
4. 总结
在本章中,我们介绍了对原始 GAN 算法的各种改进,这些改进在上一章中首次介绍。 WGAN 提出了一种通过使用 EMD 或 Wasserstein 1 损失来提高训练稳定性的算法。 LSGAN 认为,与最小二乘损失不同,GANs 的原始交叉熵函数倾向于消失梯度。 LSGAN 提出了一种实现稳定训练和高质量输出的算法。 ACGAN 通过要求判别器在确定输入图像是假的还是真实的基础上执行分类任务,来令人信服地提高了 MNIST 数字有条件生成的质量。
在下一章中,我们将研究如何控制生成器输出的属性。 尽管 CGAN 和 ACGAN 可以指示要生成的期望数字,但我们尚未分析可以指定输出属性的 GAN。 例如,我们可能想要控制 MNIST 数字的书写风格,例如圆度,倾斜角度和厚度。 因此,目标是引入具有纠缠表示的 GAN,以控制生成器输出的特定属性。
5. 参考
Ian Goodfellow et al.: Generative Adversarial Nets. Advances in neural information processing systems, 2014 (http://papers.nips.cc/paper/5423-generative-adversarial-nets.pdf).Martin Arjovsky, Soumith Chintala, and Léon Bottou: Wasserstein GAN. arXiv preprint, 2017 (https://arxiv.org/pdf/1701.07875.pdf).Xudong Mao et al.: Least Squares Generative Adversarial Networks. 2017 IEEE International Conference on Computer Vision (ICCV). IEEE 2017 (http://openaccess.thecvf.com/content_ICCV_2017/papers/Mao_Least_Squares_Generative_ICCV_2017_paper.pdf).Augustus Odena, Christopher Olah, and Jonathon Shlens. Conditional Image Synthesis with Auxiliary Classifier GANs. ICML, 2017 (http://proceedings.mlr.press/v70/odena17a/odena17a.pdf).
六、纠缠表示 GAN
正如我们已经探索的那样,GAN 可以通过学习数据分布来产生有意义的输出。 但是,无法控制所生成输出的属性。 GAN 的一些变体,例如条件 GAN(CGAN)和辅助分类器 GAN(ACGAN),如前两章所讨论的,都可以训练生成器,该生成器可以合成特定的输出。 例如,CGAN 和 ACGAN 都可以诱导生成器生成特定的 MNIST 数字。 这可以通过同时使用 100 维噪声代码和相应的一号热标签作为输入来实现。 但是,除了单热标签外,我们没有其他方法可以控制生成的输出的属性。
有关 CGAN 和 ACGAN 的评论,请参阅“第 4 章”,“生成对抗网络(GANs)”和“第 5 章”,“改进的 GANs”。
在本章中,我们将介绍使我们能够修改生成器输出的 GAN 的变体。 在 MNIST 数据集的上下文中,除了要生成的数字外,我们可能会发现我们想要控制书写样式。 这可能涉及所需数字的倾斜度或宽度。 换句话说,GAN 也可以学习纠缠的潜在代码或表示形式,我们可以使用它们来改变生成器输出的属性。 解开的代码或表示形式是张量,可以在不影响其他属性的情况下更改输出数据的特定特征或属性。
在本章的第一部分中,我们将讨论《InfoGAN:通过最大化生成对抗网络的信息进行可解释的表示学习》[1],这是 GAN 的扩展。 InfoGAN 通过最大化输入代码和输出观察值之间的互信息来以无监督的方式学习解缠结的表示形式。 在 MNIST 数据集上,InfoGAN 从数字数据集中解开了写作风格。
在本章的以下部分中,我们还将讨论《栈式生成对抗网络或 StackedGAN》[2],这是 GAN 的另一种扩展。
StackedGAN 使用预训练的编码器或分类器,以帮助解开潜在代码。 StackedGAN 可以看作是一堆模型,每个模型都由编码器和 GAN 组成。 通过使用相应编码器的输入和输出数据,以对抗性方式训练每个 GAN。
总之,本章的目的是介绍:
- 纠缠表示的概念
- InfoGAN 和 StackedGAN 的原理
- 使用
tf.keras实现 InfoGAN 和 StackedGAN
让我们从讨论纠缠的表示开始。
1. 纠缠表示
最初的 GAN 能够产生有意义的输出,但是缺点是它的属性无法控制。 例如,如果我们训练 GAN 来学习名人面孔的分布,则生成器将产生名人形象的新图像。 但是,没有任何方法可以影响生成器有关所需脸部的特定属性。 例如,我们无法向生成器询问女性名人的脸,该女性名人是黑发,白皙的肤色,棕色的眼睛,微笑着。 这样做的根本原因是因为我们使用的 100 维噪声代码纠缠了生成器输出的所有显着属性。 我们可以回想一下,在tf.keras中,100-dim代码是由均匀噪声分布的随机采样生成的:
# generate fake images from noise using generator
# generate noise using uniform distribution
noise = np.random.uniform(-1.0,
1.0,
size=[batch_size, latent_size])
# generate fake images
fake_images = generator.predict(noise)
如果我们能够修改原始 GAN,以便将表示形式分为纠缠的和解缠的可解释的潜在代码向量,则我们将能够告诉生成器合成什么。
“图 6.1.1”向我们展示了一个带纠缠代码的 GAN,以及它的纠缠和解缠表示的混合形式。 在假设的名人脸生成的情况下,使用解开的代码,我们可以指出我们希望生成的脸的性别,发型,面部表情,肤色和肤色。 仍然需要n–dim纠缠代码来表示我们尚未纠缠的所有其他面部属性,例如面部形状,面部毛发,眼镜等,仅是三个示例。 纠缠和解纠缠的代码向量的连接用作生成器的新输入。 级联代码的总维不一定是 100:

图 6.1.1:带有纠缠码的 GAN 及其随纠缠码和解缠码的变化。 此示例在名人脸生成的背景下显示
查看上图中的,似乎可以以与原始 GAN 相同的方式优化具有解缠表示的 GAN。 这是因为生成器的输出可以表示为:
(Equation 6.1.1)
代码z = (z, c)包含两个元素:
- 类似于 GANs
z或噪声向量的不可压缩纠缠噪声代码。 - 潜在代码
c₁,c₂,…,c[L], 代表数据分配的可解译的纠缠码。 所有潜在代码共同表示为c。
为简单起见,假定所有潜在代码都是独立的:
(Equation 6.1.2)
生成器函数x = g(z, c) = g(z)带有不可压缩的噪声代码和潜在代码。 从生成器的角度来看,优化z = (z, c)与优化z相同。
当提出解决方案时,生成器网络将仅忽略解纠结代码所施加的约束。
生成器学习分布p_g(x | c) = p_g(x)。 这实际上将打乱分散表示的目的。
InfoGAN 的关键思想是强制 GAN 不要忽略潜在代码c。 这是通过最大化c和g(z, c)之间的相互信息来完成的。 在下一节中,我们将公式化 InfoGAN 的损失函数。
InfoGAN
为了加强对代码的纠缠,InfoGAN 提出了一种针对原始损失函数的正则化函数,该函数可最大化潜在代码c和g(z, c)之间的互信息:
(Equation 6.1.3)
正则化器在生成用于合成伪图像的函数时,会强制生成器考虑潜在代码。 在信息论领域,潜码c和g(z, c)之间的互信息定义为:
(Equation 6.1.4)
其中H(c)是潜码c的熵,H(c | g(z | c))是观察生成器的输出后c的条件熵, g(z, c)。 熵是对随机变量或事件的不确定性的度量。 例如,在东方升起之类的信息具有较低的熵,而在彩票中赢得大奖具有较高的熵。 可以在“第 13 章”,“使用互信息的无监督学习”中找到有关互信息的更详细讨论。
在“公式 6.1.4”中,最大化互信息意味着在观察生成的输出时,将H(c | g(z | c))最小化或减小潜码中的不确定性。 这是有道理的,因为例如在 MNIST 数据集中,如果 GAN 看到生成器 8 看到了数字 8,则生成器对合成数字 8 变得更有信心。
但是,H(c | g(z | c))很难估计,因为它需要后验P(c | g(z | c)) = P(c | x)的知识,这是我们无法获得的。 为简单起见,我们将使用常规字母x表示数据分布。
解决方法是通过使用辅助分布Q(c | x)估计后验来估计互信息的下界。 InfoGAN 估计相互信息的下限为:
(Equation 6.1.5)
在 InfoGAN 中,H(c)被假定为常数。 因此,使相互信息最大化是使期望最大化的问题。 生成器必须确信已生成具有特定属性的输出。 我们应注意,此期望的最大值为零。 因此,相互信息的下限的最大值为H(c)。 在 InfoGAN 中,离散隐码的Q(c | x)可以由softmax非线性表示。 期望是tf.keras中的负categorical_crossentropy损失。
对于一维连续代码,期望是c和x的双整数。 这是由于期望从纠缠的代码分布和生成器分布中采样。 估计期望值的一种方法是通过假设样本是连续数据的良好度量。 因此,损失估计为c log Q(c | x)。 在“第 13 章”,“使用互信息的无监督学习”中,我们将提供对互信息的更精确估计。
为了完成 InfoGAN 的网络,我们应该有Q(c | x)的实现。 为了简单起见,网络 Q 是一个附加到判别器第二到最后一层的辅助网络。 因此,这对原始 GAN 的训练影响很小。
“图 6.1.2”显示了 InfoGAN 网络图:

图 6.1.2 网络图显示 InfoGAN 中的判别器和生成器训练
“表 6.1.1”显示了与 GAN 相比 InfoGAN 的损失函数:
| 网络 | 损失函数 | 编号 |
|---|---|---|
| GAN | ![]() |
4.1.1 |
![]() |
4.1.5 | |
| InfoGAN | ![]() |
6.1.1 |
![]() |
6.1.2 | |
对于连续代码,InfoGAN 建议使用λ < 1的值。 在我们的示例中,我们设置λ = 0.5。 对于离散代码,InfoGAN 建议使用λ = 1。 |
表 6.1.1:GAN 和 InfoGAN 的损失函数之间的比较
InfoGAN 的损失函数与 GAN 的区别是附加项-λI(c; g(z, c)),其中λ是一个小的正常数。 最小化 InfoGAN 的损失函数可以将原始 GAN 的损失最小化,并将互信息最大化I(c; g(z, c))。
如果将其应用于 MNIST 数据集,InfoGAN 可以学习解开的离散码和连续码,以修改生成器输出属性。 例如,像 CGAN 和 ACGAN 一样,将使用10-dim一键标签形式的离散代码来指定要生成的数字。 但是,我们可以添加两个连续的代码,一个用于控制书写样式的角度,另一个用于调整笔划宽度。“图 6.1.3”显示了 InfoGAN 中 MNIST 数字的代码。 我们保留较小尺寸的纠缠代码以表示所有其他属性:

图 6.1.3:MNIST 数据集中 GAN 和 InfoGAN 的代码
在讨论了 InfoGAN 背后的一些概念之后,让我们看一下tf.keras中的 InfoGAN 实现。
在 Keras 中实现 InfoGAN
为了在 MNIST 数据集上实现 InfoGAN,需要对 ACGAN 的基本代码进行一些更改。 如“列表 6.1.1”中突出显示的那样,生成器将纠缠的(z噪声代码)和解纠结的代码(单标签和连续代码)连接起来作为输入:
inputs = [inputs, labels] + codes
generator和discriminator的构建器函数也在lib文件夹的gan.py中实现。
“列表 6.1.1”:infogan-mnist-6.1.1.py
突出显示了特定于 InfoGAN 的行:
def generator(inputs,
image_size,
activation='sigmoid',
labels=None,
codes=None):
"""Build a Generator Model
Stack of BN-ReLU-Conv2DTranpose to generate fake images.
Output activation is sigmoid instead of tanh in [1].
Sigmoid converges easily.
Arguments:
inputs (Layer): Input layer of the generator (the z-vector)
image_size (int): Target size of one side
(assuming square image)
activation (string): Name of output activation layer
labels (tensor): Input labels
codes (list): 2-dim disentangled codes for InfoGAN
Returns:
Model: Generator Model
"""
image_resize = image_size // 4
# network parameters
kernel_size = 5
layer_filters = [128, 64, 32, 1]
if labels is not None:
if codes is None:
# ACGAN labels
# concatenate z noise vector and one-hot labels
inputs = [inputs, labels]
else:
# infoGAN codes
# concatenate z noise vector,
# one-hot labels and codes 1 & 2
inputs = [inputs, labels] + codes
x = concatenate(inputs, axis=1)
elif codes is not None:
# generator 0 of StackedGAN
inputs = [inputs, codes]
x = concatenate(inputs, axis=1)
else:
# default input is just 100-dim noise (z-code)
x = inputs
x = Dense(image_resize * image_resize * layer_filters[0])(x)
x = Reshape((image_resize, image_resize, layer_filters[0]))(x)
for filters in layer_filters:
# first two convolution layers use strides = 2
# the last two use strides = 1
if filters > layer_filters[-2]:
strides = 2
else:
strides = 1
x = BatchNormalization()(x)
x = Activation('relu')(x)
x = Conv2DTranspose(filters=filters,
kernel_size=kernel_size,
strides=strides,
padding='same')(x)
if activation is not None:
x = Activation(activation)(x)
# generator output is the synthesized image x
return Model(inputs, x, name='generator')
“列表 6.1.2”显示了具有原始默认 GAN 输出的判别器和 Q 网络。 高亮显示了三个辅助输出,它们对应于离散代码(用于单热标签)softmax预测的和给定输入 MNIST 数字图像的连续代码概率。
“列表 6.1.2”:infogan-mnist-6.1.1.py
突出显示了特定于 InfoGAN 的行:
def discriminator(inputs,
activation='sigmoid',
num_labels=None,
num_codes=None):
"""Build a Discriminator Model
Stack of LeakyReLU-Conv2D to discriminate real from fake
The network does not converge with BN so it is not used here
unlike in [1]
Arguments:
inputs (Layer): Input layer of the discriminator (the image)
activation (string): Name of output activation layer
num_labels (int): Dimension of one-hot labels for ACGAN & InfoGAN
num_codes (int): num_codes-dim Q network as output
if StackedGAN or 2 Q networks if InfoGAN
Returns:
Model: Discriminator Model
"""
kernel_size = 5
layer_filters = [32, 64, 128, 256]
x = inputs
for filters in layer_filters:
# first 3 convolution layers use strides = 2
# last one uses strides = 1
if filters == layer_filters[-1]:
strides = 1
else:
strides = 2
x = LeakyReLU(alpha=0.2)(x)
x = Conv2D(filters=filters,
kernel_size=kernel_size,
strides=strides,
padding='same')(x)
x = Flatten()(x)
# default output is probability that the image is real
outputs = Dense(1)(x)
if activation is not None:
print(activation)
outputs = Activation(activation)(outputs)
if num_labels:
# ACGAN and InfoGAN have 2nd output
# 2nd output is 10-dim one-hot vector of label
layer = Dense(layer_filters[-2])(x)
labels = Dense(num_labels)(layer)
labels = Activation('softmax', name='label')(labels)
if num_codes is None:
outputs = [outputs, labels]
else:
# InfoGAN have 3rd and 4th outputs
# 3rd output is 1-dim continous Q of 1st c given x
code1 = Dense(1)(layer)
code1 = Activation('sigmoid', name='code1')(code1)
# 4th output is 1-dim continuous Q of 2nd c given x
code2 = Dense(1)(layer)
code2 = Activation('sigmoid', name='code2')(code2)
outputs = [outputs, labels, code1, code2]
elif num_codes is not None:
# StackedGAN Q0 output
# z0_recon is reconstruction of z0 normal distribution
z0_recon = Dense(num_codes)(x)
z0_recon = Activation('tanh', name='z0')(z0_recon)
outputs = [outputs, z0_recon]
return Model(inputs, outputs, name='discriminator')
“图 6.1.4”显示了tf.keras中的 InfoGAN 模型:

图 6.1.4:InfoGAN Keras 模型
建立判别器和对抗模型还需要进行许多更改。 更改取决于所使用的损失函数。 原始的判别器损失函数binary_crossentropy,用于离散码的categorical_crossentropy和每个连续码的mi_loss函数构成了整体损失函数。 除mi_loss函数的权重为 0.5(对应于连续代码的λ = 0.5)外,每个损失函数的权重均为 1.0。
“列表 6.1.3”突出显示了所做的更改。 但是,我们应该注意,通过使用构造器函数,判别器被实例化为:
# call discriminator builder with 4 outputs:
# source, label, and 2 codes
discriminator = gan.discriminator(inputs,
num_labels=num_labels,
num_codes=2)
生成器通过以下方式创建:
# call generator with inputs,
# labels and codes as total inputs to generator
generator = gan.generator(inputs,
image_size,
labels=labels,
codes=[code1, code2])
“列表 6.1.3”:infogan-mnist-6.1.1.py
以下代码演示了互信息损失函数以及建立和训练 InfoGAN 判别器和对抗网络的过程:
def mi_loss(c, q_of_c_given_x):
""" Mutual information, Equation 5 in [2],
assuming H(c) is constant
"""
# mi_loss = -c * log(Q(c|x))
return K.mean(-K.sum(K.log(q_of_c_given_x + K.epsilon()) * c,
axis=1))
def build_and_train_models(latent_size=100):
"""Load the dataset, build InfoGAN discriminator,
generator, and adversarial models.
Call the InfoGAN train routine.
"""
# load MNIST dataset
(x_train, y_train), (_, _) = mnist.load_data()
# reshape data for CNN as (28, 28, 1) and normalize
image_size = x_train.shape[1]
x_train = np.reshape(x_train, [-1, image_size, image_size, 1])
x_train = x_train.astype('float32') / 255
# train labels
num_labels = len(np.unique(y_train))
y_train = to_categorical(y_train)
model_name = "infogan_mnist"
# network parameters
batch_size = 64
train_steps = 40000
lr = 2e-4
decay = 6e-8
input_shape = (image_size, image_size, 1)
label_shape = (num_labels, )
code_shape = (1, )
# build discriminator model
inputs = Input(shape=input_shape, name='discriminator_input')
# call discriminator builder with 4 outputs:
# source, label, and 2 codes
discriminator = gan.discriminator(inputs,
num_labels=num_labels,
num_codes=2)
# [1] uses Adam, but discriminator converges easily with RMSprop
optimizer = RMSprop(lr=lr, decay=decay)
# loss functions: 1) probability image is real
# (binary crossentropy)
# 2) categorical cross entropy image label,
# 3) and 4) mutual information loss
loss = ['binary_crossentropy',
'categorical_crossentropy',
mi_loss,
mi_loss]
# lamda or mi_loss weight is 0.5
loss_weights = [1.0, 1.0, 0.5, 0.5]
discriminator.compile(loss=loss,
loss_weights=loss_weights,
optimizer=optimizer,
metrics=['accuracy'])
discriminator.summary()
# build generator model
input_shape = (latent_size, )
inputs = Input(shape=input_shape, name='z_input')
labels = Input(shape=label_shape, name='labels')
code1 = Input(shape=code_shape, name="code1")
code2 = Input(shape=code_shape, name="code2")
# call generator with inputs,
# labels and codes as total inputs to generator
generator = gan.generator(inputs,
image_size,
labels=labels,
codes=[code1, code2])
generator.summary()
# build adversarial model = generator + discriminator
optimizer = RMSprop(lr=lr*0.5, decay=decay*0.5)
discriminator.trainable = False
# total inputs = noise code, labels, and codes
inputs = [inputs, labels, code1, code2]
adversarial = Model(inputs,
discriminator(generator(inputs)),
name=model_name)
# same loss as discriminator
adversarial.compile(loss=loss,
loss_weights=loss_weights,
optimizer=optimizer,
metrics=['accuracy'])
adversarial.summary()
# train discriminator and adversarial networks
models = (generator, discriminator, adversarial)
data = (x_train, y_train)
params = (batch_size,
latent_size,
train_steps,
num_labels,
model_name)
train(models, data, params)
就训练而言,我们可以看到 InfoGAN 与 ACGAN 类似,除了我们需要为连续代码提供c。c是从正态分布中提取的,标准差为 0.5,平均值为 0.0。 我们将对伪数据使用随机采样的标签,对实际数据使用数据集的类标签来表示离散的潜在代码。
“列表 6.1.4”突出显示了对训练函数所做的更改。 与以前的所有 GAN 相似,判别器和生成器(通过对抗性训练)被交替训练。 在对抗训练期间,判别器的权重被冻结。
通过使用gan.py plot_images()函数,样本生成器输出图像每 500 个间隔步被保存一次。
“列表 6.1.4”:infogan-mnist-6.1.1.py
def train(models, data, params):
"""Train the Discriminator and Adversarial networks
Alternately train discriminator and adversarial networks by batch.
Discriminator is trained first with real and fake images,
corresponding one-hot labels and continuous codes.
Adversarial is trained next with fake images pretending
to be real, corresponding one-hot labels and continous codes.
Generate sample images per save_interval.
# Arguments
models (Models): Generator, Discriminator, Adversarial models
data (tuple): x_train, y_train data
params (tuple): Network parameters
"""
# the GAN models
generator, discriminator, adversarial = models
# images and their one-hot labels
x_train, y_train = data
# network parameters
batch_size, latent_size, train_steps, num_labels, model_name = \
params
# the generator image is saved every 500 steps
save_interval = 500
# noise vector to see how the generator output
# evolves during training
noise_input = np.random.uniform(-1.0,
1.0,
size=[16, latent_size])
# random class labels and codes
noise_label = np.eye(num_labels)[np.arange(0, 16) % num_labels]
noise_code1 = np.random.normal(scale=0.5, size=[16, 1])
noise_code2 = np.random.normal(scale=0.5, size=[16, 1])
# number of elements in train dataset
train_size = x_train.shape[0]
print(model_name,
"Labels for generated images: ",
np.argmax(noise_label, axis=1))
for i in range(train_steps):
# train the discriminator for 1 batch
# 1 batch of real (label=1.0) and fake images (label=0.0)
# randomly pick real images and
# corresponding labels from dataset
rand_indexes = np.random.randint(0,
train_size,
size=batch_size)
real_images = x_train[rand_indexes]
real_labels = y_train[rand_indexes]
# random codes for real images
real_code1 = np.random.normal(scale=0.5,
size=[batch_size, 1])
real_code2 = np.random.normal(scale=0.5,
size=[batch_size, 1])
# generate fake images, labels and codes
noise = np.random.uniform(-1.0,
1.0,
size=[batch_size, latent_size])
fake_labels = np.eye(num_labels)[np.random.choice(num_labels,
batch_size)]
fake_code1 = np.random.normal(scale=0.5,
size=[batch_size, 1])
fake_code2 = np.random.normal(scale=0.5,
size=[batch_size, 1])
inputs = [noise, fake_labels, fake_code1, fake_code2]
fake_images = generator.predict(inputs)
# real + fake images = 1 batch of train data
x = np.concatenate((real_images, fake_images))
labels = np.concatenate((real_labels, fake_labels))
codes1 = np.concatenate((real_code1, fake_code1))
codes2 = np.concatenate((real_code2, fake_code2))
# label real and fake images
# real images label is 1.0
y = np.ones([2 * batch_size, 1])
# fake images label is 0.0
y[batch_size:, :] = 0
# train discriminator network,
# log the loss and label accuracy
outputs = [y, labels, codes1, codes2]
# metrics = ['loss', 'activation_1_loss', 'label_loss',
# 'code1_loss', 'code2_loss', 'activation_1_acc',
# 'label_acc', 'code1_acc', 'code2_acc']
# from discriminator.metrics_names
metrics = discriminator.train_on_batch(x, outputs)
fmt = "%d: [discriminator loss: %f, label_acc: %f]"
log = fmt % (i, metrics[0], metrics[6])
# train the adversarial network for 1 batch
# 1 batch of fake images with label=1.0 and
# corresponding one-hot label or class + random codes
# since the discriminator weights are frozen
# in adversarial network only the generator is trained
# generate fake images, labels and codes
noise = np.random.uniform(-1.0,
1.0,
size=[batch_size, latent_size])
fake_labels = np.eye(num_labels)[np.random.choice(num_labels,
batch_size)]
fake_code1 = np.random.normal(scale=0.5,
size=[batch_size, 1])
fake_code2 = np.random.normal(scale=0.5,
size=[batch_size, 1])
# label fake images as real
y = np.ones([batch_size, 1])
# train the adversarial network
# note that unlike in discriminator training,
# we do not save the fake images in a variable
# the fake images go to the discriminator
# input of the adversarial for classification
# log the loss and label accuracy
inputs = [noise, fake_labels, fake_code1, fake_code2]
outputs = [y, fake_labels, fake_code1, fake_code2]
metrics = adversarial.train_on_batch(inputs, outputs)
fmt = "%s [adversarial loss: %f, label_acc: %f]"
log = fmt % (log, metrics[0], metrics[6])
print(log)
if (i + 1) % save_interval == 0:
# plot generator images on a periodic basis
gan.plot_images(generator,
noise_input=noise_input,
noise_label=noise_label,
noise_codes=[noise_code1, noise_code2],
show=False,
step=(i + 1),
model_name=model_name)
# save the model after training the generator
# the trained generator can be reloaded for
# future MNIST digit generation
generator.save(model_name + ".h5")
给定 InfoGAN 的tf.keras实现,下一个部分介绍具有解缠结属性的生成器 MNIST 输出。
InfoGAN 的生成器输出
与以前提供给我们的所有 GAN 相似,我们已经对 InfoGAN 进行了 40,000 步的训练。 训练完成后,我们可以运行 InfoGAN 生成器,以使用infogan_mnist.h5文件中保存的模型生成新输出。 进行以下验证:
-
通过将离散标签从 0 更改为 9,可生成数字 0 至 9。 两个连续代码都设置为零。 结果显示在“图 6.1.5”中。 我们可以看到,InfoGAN 离散代码可以控制生成器产生的数字:
python3 infogan-mnist-6.1.1.py --generator=infogan_mnist.h5 --digit=0 --code1=0 --code2=0至
python3 infogan-mnist-6.1.1.py --generator=infogan_mnist.h5 --digit=9 --code1=0 --code2=0在“图 6.1.5”中,我们可以看到 InfoGAN 生成的图像:
![]()
图 6.1.5:当离散代码从 0 变为 9 时,InfoGAN 生成的图像都被设置为零。
-
检查第一个连续代码的效果,以了解哪个属性已受到影响。 我们将 0 到 9 的第一个连续代码从 -2.0 更改为 2.0。 第二个连续代码设置为 0.0。 “图 6.1.6”显示了第一个连续代码控制数字的粗细:
python3 infogan-mnist-6.1.1.py --generator=infogan_mnist.h5 --digit=0 --code1=0 --code2=0 --p1![]()
图 6.1.6:InfoGAN 作为第一个连续代码将 0 到 9 的数字从-2.0 更改为 2.0。第二个连续代码设置为零。 第一个连续代码控制数字的粗细
-
与上一步的类似,但更多地关注第二个连续代码。“图 6.1.7”显示第二个连续代码控制书写样式的旋转角度(倾斜):
python3 infogan-mnist-6.1.1.py --generator=infogan_mnist.h5 --digit=0 --code1=0 --code2=0 --p2

图 6.1.7:InfoGAN 生成的图像作为第二个连续代码从 0 到 9 的数字从 -2.0 变为 2.0。第一个连续代码设置为零。 第二个连续代码控制书写样式的旋转角度(倾斜)
从这些验证结果中,我们可以看到,除了生成 MNIST 外观数字的能力之外,InfoGAN 还扩展了条件 GAN(如 CGAN 和 ACGAN)的功能。 网络自动学习了两个可以控制生成器输出的特定属性的任意代码。 有趣的是,如果我们将连续代码的数量增加到 2 以上,可以控制哪些附加属性,可以通过将“列表 6.1.1”的突出显示行中的代码扩展到列表 6.1.4 来实现。
本节中的结果表明,可以通过最大化代码和数据分布之间的互信息来纠缠生成器输出的属性。 在以下部分中,介绍了一种不同的解缠结方法。 StackedGAN 的想法是在特征级别注入代码。
2. StackedGAN
与 InfoGAN 一样,StackedGAN 提出了一种用于分解潜在表示的方法,以调节生成器输出。 但是,StackedGAN 使用不同的方法来解决此问题。 与其学习如何调节噪声以产生所需的输出,不如将 StackedGAN 分解为 GAN 栈。 每个 GAN 均以通常的区分对手的方式进行独立训练,并带有自己的潜在代码。
“图 6.2.1”向我们展示了 StackedGAN 在假设名人脸生成的背景下如何工作,假设已经训练了编码器网络对名人脸进行分类:

图 6.2.1:在名人脸生成的背景下 StackedGAN 的基本思想。 假设有一个假设的深层编码器网络可以对名人脸进行分类,那么 StackedGAN 可以简单地反转编码器的过程
编码器网络是由一堆简单的编码器组成的,Encoder[i],其中i = 0 … n-1对应n个特征。 每个编码器都提取某些面部特征。 例如,Encoder[0]可能是发型特征的编码器,Feature[1]。 所有简单的编码器都有助于使整个编码器执行正确的预测。
StackedGAN 背后的想法是,如果我们想构建一个可生成假名人面孔的 GAN,则只需将编码器反转即可。 StackedGAN 由一堆更简单的 GAN 组成,GAN[i],其中i = 0 … n-1与n个特征相对应。 每个GAN[i]学会反转其相应编码器Encoder[i]的过程。 例如,GAN[0]从假发型特征生成假名人脸,这是Encoder[0]处理的逆过程。
每个GAN[i]使用潜码zᵢ,以调节其生成器输出。 例如,潜在代码z₀可以将发型从卷曲更改为波浪形。 GAN 的栈也可以用作合成假名人面孔的对象,从而完成整个编码器的逆过程。 每个GAN[i],zᵢ的潜在代码都可以用来更改假名人面孔的特定属性。
有了 StackedGAN 的工作原理的关键思想,让我们继续下一节,看看如何在tf.keras中实现它。
Keras 中 StackedGAN 的实现
StackedGAN 的详细网络模型可以在“图 6.2.2”中看到。 为简洁起见,每个栈仅显示两个编码器 GAN。 该图最初可能看起来很复杂,但这只是一个编码器 GAN 的重复,这意味着如果我们了解如何训练一个编码器 GAN,其余的将使用相同的概念。
在本节中,我们假设 StackedGAN 是为 MNIST 数字生成而设计的。

图 6.2.2:StackedGAN 包含编码器和 GAN 的栈。 对编码器进行预训练以执行分类。 Generator[1],G₁学会合成特征f[1f],假标签y[f]和潜在代码z[1f]。 Generator[0],G₀均使用这两个伪特征f[1f]生成伪图像和潜在代码z[0f]。
StackedGAN 以编码器开头。 它可能是训练有素的分类器,可以预测正确的标签。 可以将中间特征向量f[1r]用于 GAN 训练。 对于 MNIST,我们可以使用基于 CNN 的分类器,类似于在“第 1 章”,“Keras 高级深度学习”中讨论的分类器。
“图 6.2.3”显示了编码器及其在tf.keras中的网络模型实现:

图 6.2.3:StackedGAN 中的编码器是一个基于 CNN 的简单分类器
“列表 6.2.1”显示了上图的tf.keras代码。 它与“第 1 章”,“Keras 高级深度学习”中的基于 CNN 的分类器相似,不同之处在于,我们使用Dense层来提取256-dim 特征。 有两个输出模型,Encoder[0]和Encoder[1]。 两者都将用于训练 StackedGAN。
“列表 6.2.1”:stackedgan-mnist-6.2.1.py
def build_encoder(inputs, num_labels=10, feature1_dim=256):
""" Build the Classifier (Encoder) Model sub networks
Two sub networks:
1) Encoder0: Image to feature1 (intermediate latent feature)
2) Encoder1: feature1 to labels
# Arguments
inputs (Layers): x - images, feature1 -
feature1 layer output
num_labels (int): number of class labels
feature1_dim (int): feature1 dimensionality
# Returns
enc0, enc1 (Models): Description below
"""
kernel_size = 3
filters = 64
x, feature1 = inputs
# Encoder0 or enc0
y = Conv2D(filters=filters,
kernel_size=kernel_size,
padding='same',
activation='relu')(x)
y = MaxPooling2D()(y)
y = Conv2D(filters=filters,
kernel_size=kernel_size,
padding='same',
activation='relu')(y)
y = MaxPooling2D()(y)
y = Flatten()(y)
feature1_output = Dense(feature1_dim, activation='relu')(y)
# Encoder0 or enc0: image (x or feature0) to feature1
enc0 = Model(inputs=x, outputs=feature1_output, name="encoder0")
# Encoder1 or enc1
y = Dense(num_labels)(feature1)
labels = Activation('softmax')(y)
# Encoder1 or enc1: feature1 to class labels (feature2)
enc1 = Model(inputs=feature1, outputs=labels, name="encoder1")
# return both enc0 and enc1
return enc0, enc1
Encoder[0]输出f[1r]是我们想要的256维特征向量生成器 1 学习合成。 可以将用作Encoder[0],E₀的辅助输出。 训练整个编码器以对 MNIST 数字进行分类,即x[r]。 正确的标签y[r]由Encoder[1],E₁。 在此过程中,学习了的中间特征集f[1r],可用于Generator[0]训练。 当针对该编码器训练 GAN 时,下标r用于强调和区分真实数据与伪数据。
假设编码器输入(x[r])中间特征(f[1r])和标签(y[r]),每个 GAN 都采用通常的区分-对抗方式进行训练。 损失函数由“表 6.2.1”中的“公式 6.2.1”至“公式 6.2.5”给出。“公式 6.2.1”和“公式 6.2.2”是通用 GAN 的常见损失函数。 StackedGAN 具有两个附加损失函数,即有条件和熵。
| 网络 | 损失函数 | 编号 |
|---|---|---|
| GAN | ![]() |
4.1.1 |
![]() |
4.1.5 | |
| 栈式 | ![]() |
6.2.1 |
![]() |
6.2.2 | |
![]() |
6.2.3 | |
![]() |
6.2.4 | |
![]() |
6.2.5 | |
其中λ1, λ2, λ3是权重,i是编码器和 GAN ID |
表 6.2.1:GAN 和 StackedGAN 的损失函数之间的比较。 ~p_data表示从相应的编码器数据(输入,特征或输出)采样
条件“公式 6.2.3”中的损失函数L_i^(G_cond)确保生成器不会忽略输入f[i + 1], 当从输入噪声代码zᵢ合成输出fᵢ时。 编码器Encoder[i]必须能够通过反转生成器的过程Generator[i]来恢复生成器输入。 通过L2或欧几里德距离(均方误差(MSE))来测量生成器输入和使用编码器恢复的输入之间的差异。
“图 6.2.4”显示了L_0^(G_cond)计算所涉及的网络元素:

图 6.2.4:图 6.2.3 的简化版本,仅显示L_0^(G_cond)计算中涉及的网络元素
但是,条件损失函数引入了一个新问题。 生成器忽略输入噪声代码zᵢ,仅依赖f[i + 1]。 熵损失函数“公式 6.2.4”中的L_0^(G_ent)确保生成器不会忽略噪声代码zᵢ。 Q 网络从生成器的输出中恢复噪声代码。 恢复的噪声和输入噪声之间的差异也通过L2或欧几里德距离(MSE)进行测量。
“图 6.2.5”显示了L_0^(G_ent)计算中涉及的网络元素:

图 6.2.5:图 6.2.3 的简单版本仅向我们显示了L_0^(G_ent)计算中涉及的网络元素
最后的损失函数类似于通常的 GAN 损失。 它包括判别器损失L_i^(D)和生成器(通过对抗性)损失L_i^(G_adv)。“图 6.2.6”显示了 GAN 损失所涉及的元素。

图 6.2.6:图 6.2.3 的简化版本,仅显示了L_i^(D)和L_0^(G_adv)计算中涉及的网络元素
在“公式 6.2.5”中,三个生成器损失函数的加权和为最终生成器损失函数。 在我们将要介绍的 Keras 代码中,除的熵损失设置为 10.0 之外,所有权重都设置为 1.0。 在“公式 6.2.1”至“公式 6.2.5”中,i是指编码器和 GAN 组 ID 或级别。 在原始论文中,首先对网络进行独立训练,然后进行联合训练。 在独立训练期间,编码器将首先进行训练。 在联合训练期间,将使用真实数据和虚假数据。
tf.keras中 StackedGAN 生成器和判别器的实现只需进行少量更改即可提供辅助点来访问中间特征。“图 6.2.7”显示了生成器tf.keras模型。

图 6.2.7:Keras 中的 StackedGAN 生成器模型
“列表 6.2.2”说明了构建与Generator[0]和Generator[1]相对应的两个生成器(gen0和gen1)的函数。 gen1生成器由三层Dense层组成,标签为和噪声代码z[1f]作为输入。 第三层生成伪造的f[1f]特征。 gen0生成器类似于我们介绍的其他 GAN 生成器,可以使用gan.py中的生成器生成器实例化:
# gen0: feature1 + z0 to feature0 (image)
gen0 = gan.generator(feature1, image_size, codes=z0)
gen0输入为f₁特征,并且噪声代码为z₀。 输出是生成的伪图像x[f]:
“列表 6.2.2”:stackedgan-mnist-6.2.1.py
def build_generator(latent_codes, image_size, feature1_dim=256):
"""Build Generator Model sub networks
Two sub networks: 1) Class and noise to feature1
(intermediate feature)
2) feature1 to image
# Arguments
latent_codes (Layers): dicrete code (labels),
noise and feature1 features
image_size (int): Target size of one side
(assuming square image)
feature1_dim (int): feature1 dimensionality
# Returns
gen0, gen1 (Models): Description below
"""
# Latent codes and network parameters
labels, z0, z1, feature1 = latent_codes
# image_resize = image_size // 4
# kernel_size = 5
# layer_filters = [128, 64, 32, 1]
# gen1 inputs
inputs = [labels, z1] # 10 + 50 = 62-dim
x = concatenate(inputs, axis=1)
x = Dense(512, activation='relu')(x)
x = BatchNormalization()(x)
x = Dense(512, activation='relu')(x)
x = BatchNormalization()(x)
fake_feature1 = Dense(feature1_dim, activation='relu')(x)
# gen1: classes and noise (feature2 + z1) to feature1
gen1 = Model(inputs, fake_feature1, name='gen1')
# gen0: feature1 + z0 to feature0 (image)
gen0 = gan.generator(feature1, image_size, codes=z0)
return gen0, gen1
“图 6.2.8”显示了判别器tf.keras模型:

图 6.2.8:Keras 中的 StackedGAN 判别器模型
我们提供函数来构建Discriminator[0]和Discriminator[1](dis0和dis1)。 dis0判别器类似于 GAN 判别器,除了特征向量输入和辅助网络Q₀,其恢复z₀。 gan.py中的构造器函数用于创建dis0:
dis0 = gan.discriminator(inputs, num_codes=z_dim)
dis1判别器由三层 MLP 组成,如清单 6.2.3 所示。 最后一层将区分为真假f₁。Q₁网络共享dis1的前两层。 其第三层回收z₁。
“列表 6.2.3”:stackedgan-mnist-6.2.1.py
def build_discriminator(inputs, z_dim=50):
"""Build Discriminator 1 Model
Classifies feature1 (features) as real/fake image and recovers
the input noise or latent code (by minimizing entropy loss)
# Arguments
inputs (Layer): feature1
z_dim (int): noise dimensionality
# Returns
dis1 (Model): feature1 as real/fake and recovered latent code
"""
# input is 256-dim feature1
x = Dense(256, activation='relu')(inputs)
x = Dense(256, activation='relu')(x)
# first output is probability that feature1 is real
f1_source = Dense(1)(x)
f1_source = Activation('sigmoid',
name='feature1_source')(f1_source)
# z1 reonstruction (Q1 network)
z1_recon = Dense(z_dim)(x)
z1_recon = Activation('tanh', name='z1')(z1_recon)
discriminator_outputs = [f1_source, z1_recon]
dis1 = Model(inputs, discriminator_outputs, name='dis1')
return dis1
有了所有可用的构建器函数,StackedGAN 就会在“列表 6.2.4”中进行组装。 在训练 StackedGAN 之前,对编码器进行了预训练。 请注意,我们已经在对抗模型训练中纳入了三个生成器损失函数(对抗,条件和熵)。Q网络与判别器模型共享一些公共层。 因此,其损失函数也被纳入判别器模型训练中。
“列表 6.2.4”:stackedgan-mnist-6.2.1.py
def build_and_train_models():
"""Load the dataset, build StackedGAN discriminator,
generator, and adversarial models.
Call the StackedGAN train routine.
"""
# load MNIST dataset
(x_train, y_train), (x_test, y_test) = mnist.load_data()
# reshape and normalize images
image_size = x_train.shape[1]
x_train = np.reshape(x_train, [-1, image_size, image_size, 1])
x_train = x_train.astype('float32') / 255
x_test = np.reshape(x_test, [-1, image_size, image_size, 1])
x_test = x_test.astype('float32') / 255
# number of labels
num_labels = len(np.unique(y_train))
# to one-hot vector
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
model_name = "stackedgan_mnist"
# network parameters
batch_size = 64
train_steps = 10000
lr = 2e-4
decay = 6e-8
input_shape = (image_size, image_size, 1)
label_shape = (num_labels, )
z_dim = 50
z_shape = (z_dim, )
feature1_dim = 256
feature1_shape = (feature1_dim, )
# build discriminator 0 and Q network 0 models
inputs = Input(shape=input_shape, name='discriminator0_input')
dis0 = gan.discriminator(inputs, num_codes=z_dim)
# [1] uses Adam, but discriminator converges easily with RMSprop
optimizer = RMSprop(lr=lr, decay=decay)
# loss fuctions: 1) probability image is real (adversarial0 loss)
# 2) MSE z0 recon loss (Q0 network loss or entropy0 loss)
loss = ['binary_crossentropy', 'mse']
loss_weights = [1.0, 10.0]
dis0.compile(loss=loss,
loss_weights=loss_weights,
optimizer=optimizer,
metrics=['accuracy'])
dis0.summary() # image discriminator, z0 estimator
# build discriminator 1 and Q network 1 models
input_shape = (feature1_dim, )
inputs = Input(shape=input_shape, name='discriminator1_input')
dis1 = build_discriminator(inputs, z_dim=z_dim )
# loss fuctions: 1) probability feature1 is real
# (adversarial1 loss)
# 2) MSE z1 recon loss (Q1 network loss or entropy1 loss)
loss = ['binary_crossentropy', 'mse']
loss_weights = [1.0, 1.0]
dis1.compile(loss=loss,
loss_weights=loss_weights,
optimizer=optimizer,
metrics=['accuracy'])
dis1.summary() # feature1 discriminator, z1 estimator
# build generator models
feature1 = Input(shape=feature1_shape, name='feature1_input')
labels = Input(shape=label_shape, name='labels')
z1 = Input(shape=z_shape, name="z1_input")
z0 = Input(shape=z_shape, name="z0_input")
latent_codes = (labels, z0, z1, feature1)
gen0, gen1 = build_generator(latent_codes, image_size)
gen0.summary() # image generator
gen1.summary() # feature1 generator
# build encoder models
input_shape = (image_size, image_size, 1)
inputs = Input(shape=input_shape, name='encoder_input')
enc0, enc1 = build_encoder((inputs, feature1), num_labels)
enc0.summary() # image to feature1 encoder
enc1.summary() # feature1 to labels encoder (classifier)
encoder = Model(inputs, enc1(enc0(inputs)))
encoder.summary() # image to labels encoder (classifier)
data = (x_train, y_train), (x_test, y_test)
train_encoder(encoder, data, model_name=model_name)
# build adversarial0 model =
# generator0 + discriminator0 + encoder0
optimizer = RMSprop(lr=lr*0.5, decay=decay*0.5)
# encoder0 weights frozen
enc0.trainable = False
# discriminator0 weights frozen
dis0.trainable = False
gen0_inputs = [feature1, z0]
gen0_outputs = gen0(gen0_inputs)
adv0_outputs = dis0(gen0_outputs) + [enc0(gen0_outputs)]
# feature1 + z0 to prob feature1 is
# real + z0 recon + feature0/image recon
adv0 = Model(gen0_inputs, adv0_outputs, name="adv0")
# loss functions: 1) prob feature1 is real (adversarial0 loss)
# 2) Q network 0 loss (entropy0 loss)
# 3) conditional0 loss
loss = ['binary_crossentropy', 'mse', 'mse']
loss_weights = [1.0, 10.0, 1.0]
adv0.compile(loss=loss,
loss_weights=loss_weights,
optimizer=optimizer,
metrics=['accuracy'])
adv0.summary()
# build adversarial1 model =
# generator1 + discriminator1 + encoder1
# encoder1 weights frozen
enc1.trainable = False
# discriminator1 weights frozen
dis1.trainable = False
gen1_inputs = [labels, z1]
gen1_outputs = gen1(gen1_inputs)
adv1_outputs = dis1(gen1_outputs) + [enc1(gen1_outputs)]
# labels + z1 to prob labels are real + z1 recon + feature1 recon
adv1 = Model(gen1_inputs, adv1_outputs, name="adv1")
# loss functions: 1) prob labels are real (adversarial1 loss)
# 2) Q network 1 loss (entropy1 loss)
# 3) conditional1 loss (classifier error)
loss_weights = [1.0, 1.0, 1.0]
loss = ['binary_crossentropy',
'mse',
'categorical_crossentropy']
adv1.compile(loss=loss,
loss_weights=loss_weights,
optimizer=optimizer,
metrics=['accuracy'])
adv1.summary()
# train discriminator and adversarial networks
models = (enc0, enc1, gen0, gen1, dis0, dis1, adv0, adv1)
params = (batch_size, train_steps, num_labels, z_dim, model_name)
train(models, data, params)
最后,训练函数与典型的 GAN 训练相似,不同之处在于我们一次只训练一个 GAN(即GAN[0]然后是GAN[0])。 代码显示在“列表 6.2.5”中。 值得注意的是,训练顺序为:
-
Discriminator[1]和Q₁网络通过最小化判别器和熵损失 -
Discriminator[0]和Q₀网络通过最小化判别器和熵损失 -
Adversarial[1]网络通过最小化对抗性,熵和条件损失 -
Adversarial[0]网络通过最小化对抗性,熵和条件损失
“列表 6.2.5”:stackedgan-mnist-6.2.1.py
def train(models, data, params):
"""Train the discriminator and adversarial Networks
Alternately train discriminator and adversarial networks by batch.
Discriminator is trained first with real and fake images,
corresponding one-hot labels and latent codes.
Adversarial is trained next with fake images pretending
to be real, corresponding one-hot labels and latent codes.
Generate sample images per save_interval.
# Arguments
models (Models): Encoder, Generator, Discriminator,
Adversarial models
data (tuple): x_train, y_train data
params (tuple): Network parameters
"""
# the StackedGAN and Encoder models
enc0, enc1, gen0, gen1, dis0, dis1, adv0, adv1 = models
# network parameters
batch_size, train_steps, num_labels, z_dim, model_name = params
# train dataset
(x_train, y_train), (_, _) = data
# the generator image is saved every 500 steps
save_interval = 500
# label and noise codes for generator testing
z0 = np.random.normal(scale=0.5, size=[16, z_dim])
z1 = np.random.normal(scale=0.5, size=[16, z_dim])
noise_class = np.eye(num_labels)[np.arange(0, 16) % num_labels]
noise_params = [noise_class, z0, z1]
# number of elements in train dataset
train_size = x_train.shape[0]
print(model_name,
"Labels for generated images: ",
np.argmax(noise_class, axis=1))
for i in range(train_steps):
# train the discriminator1 for 1 batch
# 1 batch of real (label=1.0) and fake feature1 (label=0.0)
# randomly pick real images from dataset
rand_indexes = np.random.randint(0,
train_size,
size=batch_size)
real_images = x_train[rand_indexes]
# real feature1 from encoder0 output
real_feature1 = enc0.predict(real_images)
# generate random 50-dim z1 latent code
real_z1 = np.random.normal(scale=0.5,
size=[batch_size, z_dim])
# real labels from dataset
real_labels = y_train[rand_indexes]
# generate fake feature1 using generator1 from
# real labels and 50-dim z1 latent code
fake_z1 = np.random.normal(scale=0.5,
size=[batch_size, z_dim])
fake_feature1 = gen1.predict([real_labels, fake_z1])
# real + fake data
feature1 = np.concatenate((real_feature1, fake_feature1))
z1 = np.concatenate((fake_z1, fake_z1))
# label 1st half as real and 2nd half as fake
y = np.ones([2 * batch_size, 1])
y[batch_size:, :] = 0
# train discriminator1 to classify feature1 as
# real/fake and recover
# latent code (z1). real = from encoder1,
# fake = from genenerator1
# joint training using discriminator part of
# advserial1 loss and entropy1 loss
metrics = dis1.train_on_batch(feature1, [y, z1])
# log the overall loss only
log = "%d: [dis1_loss: %f]" % (i, metrics[0])
# train the discriminator0 for 1 batch
# 1 batch of real (label=1.0) and fake images (label=0.0)
# generate random 50-dim z0 latent code
fake_z0 = np.random.normal(scale=0.5, size=[batch_size, z_dim])
# generate fake images from real feature1 and fake z0
fake_images = gen0.predict([real_feature1, fake_z0])
# real + fake data
x = np.concatenate((real_images, fake_images))
z0 = np.concatenate((fake_z0, fake_z0))
# train discriminator0 to classify image
# as real/fake and recover latent code (z0)
# joint training using discriminator part of advserial0 loss
# and entropy0 loss
metrics = dis0.train_on_batch(x, [y, z0])
# log the overall loss only (use dis0.metrics_names)
log = "%s [dis0_loss: %f]" % (log, metrics[0])
# adversarial training
# generate fake z1, labels
fake_z1 = np.random.normal(scale=0.5,
size=[batch_size, z_dim])
# input to generator1 is sampling fr real labels and
# 50-dim z1 latent code
gen1_inputs = [real_labels, fake_z1]
# label fake feature1 as real
y = np.ones([batch_size, 1])
# train generator1 (thru adversarial) by fooling i
# the discriminator
# and approximating encoder1 feature1 generator
# joint training: adversarial1, entropy1, conditional1
metrics = adv1.train_on_batch(gen1_inputs,
[y, fake_z1, real_labels])
fmt = "%s [adv1_loss: %f, enc1_acc: %f]"
# log the overall loss and classification accuracy
log = fmt % (log, metrics[0], metrics[6])
# input to generator0 is real feature1 and
# 50-dim z0 latent code
fake_z0 = np.random.normal(scale=0.5,
size=[batch_size, z_dim])
gen0_inputs = [real_feature1, fake_z0]
# train generator0 (thru adversarial) by fooling
# the discriminator and approximating encoder1 imag
# source generator joint training:
# adversarial0, entropy0, conditional0
metrics = adv0.train_on_batch(gen0_inputs,
[y, fake_z0, real_feature1])
# log the overall loss only
log = "%s [adv0_loss: %f]" % (log, metrics[0])
print(log)
if (i + 1) % save_interval == 0:
generators = (gen0, gen1)
plot_images(generators,
noise_params=noise_params,
show=False,
step=(i + 1),
model_name=model_name)
# save the modelis after training generator0 & 1
# the trained generator can be reloaded for
# future MNIST digit generation
gen1.save(model_name + "-gen1.h5")
gen0.save(model_name + "-gen0.h5")
tf.keras中 StackedGAN 的代码实现现已完成。 训练后,可以评估生成器的输出以检查合成 MNIST 数字的某些属性是否可以以与我们在 InfoGAN 中所做的类似的方式进行控制。
StackedGAN 的生成器输出
在对 StackedGAN 进行 10,000 步训练之后,Generator[0]和Generator[1]模型被保存在文件中。 Generator[0]和Generator[1]堆叠在一起可以合成以标签和噪声代码z₀和z₁为条件的伪造图像。
StackedGAN 生成器可以通过以下方式进行定性验证:
-
从两个噪声代码
z₀和z₁的离散标签从 0 变到 9,从正态分布中采样,均值为 0.5,标准差为 1.0。 结果显示在“图 6.2.9”中。 我们可以看到 StackedGAN 离散代码可以控制生成器生成的数字:python3 stackedgan-mnist-6.2.1.py --generator0=stackedgan_mnist-gen0.h5 --generator1=stackedgan_mnist-gen1.h5 --digit=0至
python3 stackedgan-mnist-6.2.1.py --generator0=stackedgan_mnist-gen0.h5 --generator1=stackedgan_mnist-gen1.h5 --digit=9![]()
图 6.2.9:当离散代码从 0 变为 9 时,StackedGAN 生成的图像。
z0和z1均从正态分布中采样,平均值为 0,标准差为 0.5。 -
如下所示,将第一噪声码
z₀从 -4.0 到 4.0 的恒定向量变为从 0 到 9 的数字。 第二噪声代码z₁被设置为零向量。 “图 6.2.10”显示第一个噪声代码控制数字的粗细。 例如,对于数字 8:python3 stackedgan-mnist-6.2.1.py --generator0=stackedgan_mnist-gen0.h5 --generator1=stackedgan_mnist-gen1.h5 --z0=0 --z1=0 --p0 --digit=8![]()
图 6.2.10:使用 StackedGAN 作为第一个噪声代码
z0生成的图像,对于数字 0 到 9,其向量从 -4.0 到 4.0 不变。z0似乎控制着每个数字的粗细。 -
如下所示,对于数字 0 到 9,从 -1.0 到 1.0 的恒定向量变化第二噪声代码
z₁。 将第一噪声代码z₀设置为零向量。“图 6.2.11”显示第二个噪声代码控制旋转(倾斜),并在一定程度上控制手指的粗细。 例如,对于数字 8:python3 stackedgan-mnist-6.2.1.py --generator0=stackedgan_mnist-gen0.h5 --generator1=stackedgan_mnist-gen1.h5 --z0=0 --z1=0 --p1 --digit=8

图 6.2.11:由 StackedGAN 生成的图像作为第二个噪声代码z1从 0 到 9 的恒定向量 -1.0 到 1.0 变化。z1似乎控制着每个数字的旋转(倾斜)和笔划粗细
“图 6.2.9”至“图 6.2.11”证明 StackedGAN 提供了对生成器输出属性的附加控制。 控件和属性为(标签,哪个数字),(z0,数字粗细)和(z1,数字倾斜度)。 从此示例中,我们可以控制其他可能的实验,例如:
- 从当前数量 2 增加栈中的元素数量
- 像在 InfoGAN 中一样,减小代码
z₀和z₁的尺寸
“图 6.2.12”显示了 InfoGAN 和 StackedGAN 的潜在代码之间的区别:

图 6.2.12:不同 GAN 的潜在表示
解开代码的基本思想是对损失函数施加约束,以使仅特定属性受代码影响。 从结构上讲,与 StackedGAN 相比,InfoGAN 更易于实现。 InfoGAN 的训练速度也更快。
4. 总结
在本章中,我们讨论了如何解开 GAN 的潜在表示。 在本章的前面,我们讨论了 InfoGAN 如何最大化互信息以迫使生成器学习解纠缠的潜向量。 在 MNIST 数据集示例中,InfoGAN 使用三种表示形式和一个噪声代码作为输入。 噪声以纠缠的形式表示其余的属性。 StackedGAN 以不同的方式处理该问题。 它使用一堆编码器 GAN 来学习如何合成伪造的特征和图像。 首先对编码器进行训练,以提供特征数据集。 然后,对编码器 GAN 进行联合训练,以学习如何使用噪声代码控制生成器输出的属性。
在下一章中,我们将着手一种新型的 GAN,它能够在另一个域中生成新数据。 例如,给定马的图像,GAN 可以将其自动转换为斑马的图像。 这种 GAN 的有趣特征是无需监督即可对其进行训练,并且不需要成对的样本数据。
5. 参考
Xi Chen et al.: InfoGAN: Interpretable Representation Learning by Information Maximizing Generative Adversarial Nets. Advances in Neural Information Processing Systems, 2016 (http://papers.nips.cc/paper/6399-infogan-interpretable-representation-learning-by-information-maximizing-generative-adversarial-nets.pdf).Xun Huang et al. Stacked Generative Adversarial Networks. IEEE Conference on Computer Vision and Pattern Recognition (CVPR). Vol. 2, 2017 (http://openaccess.thecvf.com/content_cvpr_2017/papers/Huang_Stacked_Generative_Adversarial_CVPR_2017_paper.pdf).
七、跨域 GAN
在计算机视觉,计算机图形学和图像处理中,许多任务涉及将图像从一种形式转换为另一种形式。 灰度图像的着色,将卫星图像转换为地图,将一位艺术家的艺术品风格更改为另一位艺术家,将夜间图像转换为白天,将夏季照片转换为冬天只是几个例子。 这些任务被称为跨域迁移,将成为本章的重点。 源域中的图像将迁移到目标域,从而生成新的转换图像。
跨域迁移在现实世界中具有许多实际应用。 例如,在自动驾驶研究中,收集公路现场驾驶数据既费时又昂贵。 为了在该示例中覆盖尽可能多的场景变化,将在不同的天气条件,季节和时间中遍历道路,从而为我们提供了大量不同的数据。 使用跨域迁移,可以通过转换现有图像来生成看起来真实的新合成场景。 例如,我们可能只需要在夏天从一个区域收集道路场景,在冬天从另一地方收集道路场景。 然后,我们可以将夏季图像转换为冬季,并将冬季图像转换为夏季。 在这种情况下,它将必须完成的任务数量减少了一半。
现实的合成图像的生成是 GAN 擅长的领域。 因此,跨域翻译是 GAN 的应用之一。 在本章中,我们将重点介绍一种流行的跨域 GAN 算法,称为 CycleGAN [2]。 与其他跨域迁移算法(例如 pix2pix [3])不同,CycleGAN 不需要对齐的训练图像即可工作。 在对齐的图像中,训练数据应该是由源图像及其对应的目标图像组成的一对图像; 例如,卫星图像和从该图像得出的相应地图。
CycleGAN 仅需要卫星数据图像和地图。 这些地图可以来自其他卫星数据,而不必事先从训练数据中生成。
在本章中,我们将探讨以下内容:
- CycleGAN 的原理,包括其在
tf.keras中的实现 - CycleGAN 的示例应用,包括使用 CIFAR10 数据集对灰度图像进行着色和应用于 MNIST 数字和街景门牌号码(SVHN) [1]数据集的样式迁移
让我们开始讨论 CycleGAN 背后的原理。
1. CycleGAN 的原理
将图像从一个域转换到另一个域是计算机视觉,计算机图形学和图像处理中的常见任务。“图 7.1.1”显示了边缘检测,这是常见的图像转换任务:

图 7.1.1:对齐图像对的示例:使用 Canny 边缘检测器的左,原始图像和右,变换后的图像。 原始照片是作者拍摄的。
在此示例中,我们可以将真实照片(左)视为源域中的图像,将边缘检测的照片(右)视为目标域中的样本。 还有许多其他具有实际应用的跨域翻译过程,例如:
- 卫星图像到地图
- 脸部图像到表情符号,漫画或动画
- 身体图像到头像
- 灰度照片的着色
- 医学扫描到真实照片
- 真实照片到画家的绘画
在不同领域中还有许多其他示例。 例如,在计算机视觉和图像处理中,我们可以通过发明一种从源图像中提取特征并将其转换为目标图像的算法来执行翻译。 坎尼边缘算子就是这种算法的一个例子。 但是,在很多情况下,翻译对于手工工程师而言非常复杂,因此几乎不可能找到合适的算法。 源域分布和目标域分布都是高维且复杂的。
解决图像翻译问题的一种方法是使用深度学习技术。 如果我们具有来自源域和目标域的足够大的数据集,则可以训练神经网络对转换进行建模。 由于必须在给定源图像的情况下自动生成目标域中的图像,因此它们必须看起来像是来自目标域的真实样本。 GAN 是适合此类跨域任务的网络。 pix2pix [3]算法是跨域算法的示例。
pix2pix 算法与条件 GAN(CGAN)[4]相似,我们在“第 4 章”,“生成对抗网络(GAN)”。 我们可以回想起在 CGAN 中,除了z噪声输入之外,诸如单热向量之类的条件会限制生成器的输出。 例如,在 MNIST 数字中,如果我们希望生成器输出数字 8,则条件为单热向量[0, 0, 0, 0, 0, 0, 0, 0, 1, 0]。 在 pix2pix 中,条件是要翻译的图像。 生成器的输出是翻译后的图像。 通过优化 CGAN 损失来训练 pix2pix 算法。 为了使生成的图像中的模糊最小化,还包括 L1 损失。
类似于 pix2pix 的神经网络的主要缺点是训练输入和输出图像必须对齐。“图 7.1.1”是对齐的图像对的示例。 样本目标图像是从源生成的。 在大多数情况下,对齐的图像对不可用或无法从源图像生成,也不昂贵,或者我们不知道如何从给定的源图像生成目标图像。 我们拥有的是来自源域和目标域的样本数据。“图 7.1.2”是来自同一向日葵主题上源域(真实照片)和目标域(范高的艺术风格)的数据示例。 源图像和目标图像不一定对齐。
与 pix2pix 不同,CycleGAN 会学习图像翻译,只要源数据和目标数据之间有足够的数量和差异即可。 无需对齐。 CycleGAN 学习源和目标分布,以及如何从给定的样本数据中将源分布转换为目标分布。 无需监督。 在“图 7.1.2”的上下文中,我们只需要数千张真实向日葵的照片和数千张梵高向日葵画的照片。 在训练了 CycleGAN 之后,我们可以将向日葵的照片转换成梵高的画作:

图 7.1.2:未对齐的图像对示例:左侧为菲律宾大学沿着大学大道的真实向日葵照片,右侧为伦敦国家美术馆的梵高的向日葵, 英国。 原始照片由作者拍摄。
下一个问题是:我们如何建立可以从未配对数据中学习的模型? 在下一部分中,我们将构建一个使用正向和反向循环 GAN 的 CycleGAN,以及一个循环一致性检查,以消除对配对输入数据的需求。
CycleGAN 模型
“图 7.1.3”显示了 CycleGAN 的网络模型:

图 7.1.3:CycleGAN 模型包含四个网络:生成器G,生成器F,判别器D[y]和判别器D[x]
让我们逐个讨论“图 7.1.3”。 让我们首先关注上层网络,即转发周期 GAN。 如下图“图 7.1.4”所示,正向循环 CycleGAN 的目标是学习以下函数:
(Equation 7.1.1)

图 7.1.4:伪造y的 CycleGAN 生成器G
“公式 7.1.1”只是假目标数据y'的生成器G。 它将数据从源域x转换为目标域y。
要训练生成器,我们必须构建 GAN。 这是正向循环 GAN,如图“图 7.1.5”所示。 该图表明,它类似于“第 4 章”,“生成对抗网络(GANs)”中的典型 GAN,由生成器G和判别器D[y]组成,它可以以相同的对抗方式进行训练。通过仅利用源域中的可用实际图像x和目标域中的实际图像y,进行无监督学习。

图 7.1.5:CycleGAN 正向循环 GAN
与常规 GAN 不同,CycleGAN 施加了周期一致性约束,如图“图 7.1.6”所示。 前向循环一致性网络可确保可以从伪造的目标数据中重建真实的源数据:
(Equation 7.1.2)

图 7.1.6:CycleGAN 循环一致性检查
通过最小化正向循环一致性 L1 损失来完成:
(Equation 7.1.3)
周期一致性损失使用 L1 或平均绝对误差(MAE),因为与 L2 或均方误差(MSE)相比,它通常导致较少的模糊图像重建。
循环一致性检查表明,尽管我们已将源数据x转换为域y,但x的原始特征仍应保留在y中并且可恢复。 网络F只是我们将从反向循环 GAN 借用的另一个生成器,如下所述。
CycleGAN 是对称的。 如图“图 7.1.7”所示,后向循环 GAN 与前向循环 GAN 相同,但将源数据x和目标数据y的作用逆转。 现在,源数据为y,目标数据为x。 生成器G和F的作用也相反。F现在是生成器,而G恢复输入。 在正向循环 GAN 中,生成器F是用于恢复源数据的网络,而G是生成器。
Backward Cycle GAN 生成器的目标是合成:
(Equation 7.1.2)

图 7.1.7:CycleGAN 向后循环 GAN
这可以通过对抗性训练反向循环 GAN 来完成。 目的是让生成器F学习如何欺骗判别器D[x]。
此外,还具有类似的向后循环一致性,以恢复原始源y:
(Equation 7.1.4)
这是通过最小化后向循环一致性 L1 损失来完成的:
(Equation 7.1.5)
总而言之,CycleGAN 的最终目标是使生成器G学习如何合成伪造的目标数据y',该伪造的目标数据y'会在正向循环中欺骗识别器D[y]。 由于网络是对称的,因此 CycleGAN 还希望生成器F学习如何合成伪造的源数据x',该伪造的源数据可以使判别器D[x]在反向循环中蒙蔽。 考虑到这一点,我们现在可以将所有损失函数放在一起。
让我们从 GAN 部分开始。 受到最小二乘 GAN(LSGAN) [5]更好的感知质量的启发,如“第 5 章”,“改进的 GAN” 中所述,CycleGAN 还使用 MSE 作为判别器和生成器损失。 回想一下,LSGAN 与原始 GAN 之间的差异需要使用 MSE 损失,而不是二进制交叉熵损失。
CycleGAN 将生成器-标识符损失函数表示为:
(Equation 7.1.6)
(Equation 7.1.7)
(Equation 7.1.8)
(Equation 7.1.9)
(Equation 7.1.10)
(Equation 7.1.11)
损失函数的第二组是周期一致性损失,可以通过汇总前向和后向 GAN 的贡献来得出:

(Equation 7.1.12)
CycleGAN 的总损失为:
(Equation 7.1.13)
CycleGAN 建议使用以下权重值λ1 = 1.0和λ2 = 10.0,以更加重视循环一致性检查。
训练策略类似于原始 GAN。 “算法 7.1.1”总结了 CycleGAN 训练过程。
“算法 7.1.1”:CycleGAN 训练
对n训练步骤重复上述步骤:
-
通过使用真实的源数据和目标数据训练前向循环判别器,将
L_forward_GAN^(D)降至最低。 实际目标数据的小批量y标记为 1.0。 伪造的目标数据y' = G(x)的小批量标记为 0.0。 -
通过使用真实的源数据和目标数据训练反向循环判别器,将
L_backward_GAN^(D)最小化。 实际源数据的小批量x标记为 1.0。 一小部分伪造的源数据x' = F(y)被标记为 0.0。 -
通过训练对抗网络中的前向周期和后向周期生成器,将
L_GAN^(D)和L_cyc最小化。 伪造目标数据的一个小批量y' = G(x)被标记为 1.0。 一小部分伪造的源数据x' = F(y)被标记为 1.0。 判别器的权重被冻结。
在神经样式迁移问题中,颜色组合可能无法成功地从源图像迁移到伪造目标图像。 此问题显示在“图 7.1.8”中:

图 7.1.8:在样式迁移过程中,颜色组合可能无法成功迁移。 为了解决此问题,将恒等损失添加到总损失函数中
为了解决这个问题,CycleGAN 建议包括正向和反向循环身份损失函数:
(Equation 7.1.14)
CycleGAN 的总损失变为:
(Equation 7.1.15)
其中λ3 = 0.5。 在对抗训练中,身份损失也得到了优化。“图 7.1.9”重点介绍了实现身份正则器的 CycleGAN 辅助网络:

图 7.1.9:具有身份正则化网络的 CycleGAN 模型,图像左侧突出显示
在下一个部分,我们将在tf.keras中实现 CycleGAN。
使用 Keras 实现 CycleGAN
我们来解决,这是 CycleGAN 可以解决的简单问题。 在“第 3 章”,“自编码器”中,我们使用了自编码器为 CIFAR10 数据集中的灰度图像着色。 我们可以记得,CIFAR10 数据集包含 50,000 个训练过的数据项和 10,000 个测试数据样本,这些样本属于 10 个类别的32 x 32 RGB 图像。 我们可以使用rgb2gray(RGB)将所有彩色图像转换为灰度图像,如“第 3 章”,“自编码器”中所述。
接下来,我们可以将灰度训练图像用作源域图像,将原始彩色图像用作目标域图像。 值得注意的是,尽管数据集是对齐的,但我们 CycleGAN 的输入是彩色图像的随机样本和灰度图像的随机样本。 因此,我们的 CycleGAN 将看不到训练数据对齐。 训练后,我们将使用测试的灰度图像来观察 CycleGAN 的表现。
如前几节所述,要实现 CycleGAN,我们需要构建两个生成器和两个判别器。 CycleGAN 的生成器学习源输入分布的潜在表示,并将该表示转换为目标输出分布。 这正是自编码器的功能。 但是,类似于“第 3 章”,“自编码器”中讨论的典型自编码器,使用的编码器会对输入进行下采样,直到瓶颈层为止,此时解码器中的处理过程相反。
由于在编码器和解码器层之间共享许多低级特征,因此该结构不适用于某些图像转换问题。 例如,在着色问题中,灰度图像的形式,结构和边缘与彩色图像中的相同。 为了解决这个问题,CycleGAN 生成器使用 U-Net [7]结构,如图“图 7.1.10”所示:

图 7.1.10:在 Keras 中实现正向循环生成器G。 产生器是包括编码器和解码器的 U 网络[7]。
在 U-Net 结构中,编码器层的输出e[ni]与解码器层的输出dᵢ,其中n = 4是编码器/解码器的层数,i = 1, 2, 3是共享信息的层号。
我们应该注意,尽管该示例使用n = 4,但输入/输出尺寸较大的问题可能需要更深的编码器/解码器层。 通过 U-Net 结构,可以在编码器和解码器之间自由迁移特征级别的信息。
编码器层由Instance Normalization(IN)-LeakyReLU-Conv2D组成,而解码器层由IN-ReLU-Conv2D组成。 编码器/解码器层的实现如清单 7.1.1 所示,而生成器的实现如列表 7.1.2 所示。
实例规范化(IN)是每个数据(即 IN 是图像或每个特征的 BN)。 在样式迁移中,重要的是标准化每个样本而不是每个批量的对比度。 IN 等于,相当于对比度归一化。 同时,BN 打破了对比度标准化。
记住在使用 IN 之前先安装tensorflow-addons:
$ pip install tensorflow-addons
“列表 7.1.1”:cyclegan-7.1.1.py
def encoder_layer(inputs,
filters=16,
kernel_size=3,
strides=2,
activation='relu',
instance_norm=True):
"""Builds a generic encoder layer made of Conv2D-IN-LeakyReLU
IN is optional, LeakyReLU may be replaced by ReLU
"""
conv = Conv2D(filters=filters,
kernel_size=kernel_size,
strides=strides,
padding='same')
x = inputs
if instance_norm:
x = InstanceNormalization(axis=3)(x)
if activation == 'relu':
x = Activation('relu')(x)
else:
x = LeakyReLU(alpha=0.2)(x)
x = conv(x)
return x
def decoder_layer(inputs,
paired_inputs,
filters=16,
kernel_size=3,
strides=2,
activation='relu',
instance_norm=True):
"""Builds a generic decoder layer made of Conv2D-IN-LeakyReLU
IN is optional, LeakyReLU may be replaced by ReLU
Arguments: (partial)
inputs (tensor): the decoder layer input
paired_inputs (tensor): the encoder layer output
provided by U-Net skip connection &
concatenated to inputs.
"""
conv = Conv2DTranspose(filters=filters,
kernel_size=kernel_size,
strides=strides,
padding='same')
x = inputs
if instance_norm:
x = InstanceNormalization(axis=3)(x)
if activation == 'relu':
x = Activation('relu')(x)
else:
x = LeakyReLU(alpha=0.2)(x)
x = conv(x)
x = concatenate([x, paired_inputs])
return x
将移至生成器实现中:
“列表 7.1.2”:cyclegan-7.1.1.py
Keras 中的生成器实现:
def build_generator(input_shape,
output_shape=None,
kernel_size=3,
name=None):
"""The generator is a U-Network made of a 4-layer encoder
and a 4-layer decoder. Layer n-i is connected to layer i.
Arguments:
input_shape (tuple): input shape
output_shape (tuple): output shape
kernel_size (int): kernel size of encoder & decoder layers
name (string): name assigned to generator model
Returns:
generator (Model):
"""
inputs = Input(shape=input_shape)
channels = int(output_shape[-1])
e1 = encoder_layer(inputs,
32,
kernel_size=kernel_size,
activation='leaky_relu',
strides=1)
e2 = encoder_layer(e1,
64,
activation='leaky_relu',
kernel_size=kernel_size)
e3 = encoder_layer(e2,
128,
activation='leaky_relu',
kernel_size=kernel_size)
e4 = encoder_layer(e3,
256,
activation='leaky_relu',
kernel_size=kernel_size)
d1 = decoder_layer(e4,
e3,
128,
kernel_size=kernel_size)
d2 = decoder_layer(d1,
e2,
64,
kernel_size=kernel_size)
d3 = decoder_layer(d2,
e1,
32,
kernel_size=kernel_size)
outputs = Conv2DTranspose(channels,
kernel_size=kernel_size,
strides=1,
activation='sigmoid',
padding='same')(d3)
generator = Model(inputs, outputs, name=name)
return generator
CycleGAN 的判别器类似于原始 GAN 判别器。 输入图像被下采样数次(在此示例中为 3 次)。 最后一层是Dense(1)层,它预测输入为实数的可能性。 除了不使用 IN 之外,每个层都类似于生成器的编码器层。 然而,在大图像中,用一个数字将图像计算为真实图像或伪图像会导致参数效率低下,并导致生成器的图像质量较差。
解决方案是使用 PatchGAN [6],该方法将图像划分为补丁网格,并使用标量值网格来预测补丁是真实概率。“图 7.1.11”显示了原始 GAN 判别器和2 x 2 PatchGAN 判别器之间的比较:

图 7.1.11:GAN 与 PatchGAN 判别器的比较
在此示例中,面片不重叠且在其边界处相遇。 但是,通常,补丁可能会重叠。
我们应该注意,PatchGAN 并没有在 CycleGAN 中引入一种新型的 GAN。 为了提高生成的图像质量,如果使用2 x 2 PatchGAN,则没有四个输出可以区分,而没有一个输出可以区分。 损失函数没有变化。 从直觉上讲,这是有道理的,因为如果图像的每个面片或部分看起来都是真实的,则整个图像看起来会更加真实。
“图 7.1.12”显示了tf.keras中实现的判别器网络。 下图显示了判别器确定输入图像或色块为彩色 CIFAR10 图像的可能性:

图 7.1.12:目标标识符D[y]在tf.keras中的实现。 PatchGAN 判别器显示在右侧
由于输出图像只有32 x 32 RGB 时较小,因此表示该图像是真实的单个标量就足够了。 但是,当使用 PatchGAN 时,我们也会评估结果。“列表 7.1.3”显示了判别器的函数构建器:
“列表 7.1.3”:cyclegan-7.1.1.py
tf.keras中的判别器实现:
def build_discriminator(input_shape,
kernel_size=3,
patchgan=True,
name=None):
"""The discriminator is a 4-layer encoder that outputs either
a 1-dim or a n x n-dim patch of probability that input is real
Arguments:
input_shape (tuple): input shape
kernel_size (int): kernel size of decoder layers
patchgan (bool): whether the output is a patch
or just a 1-dim
name (string): name assigned to discriminator model
Returns:
discriminator (Model):
"""
inputs = Input(shape=input_shape)
x = encoder_layer(inputs,
32,
kernel_size=kernel_size,
activation='leaky_relu',
instance_norm=False)
x = encoder_layer(x,
64,
kernel_size=kernel_size,
activation='leaky_relu',
instance_norm=False)
x = encoder_layer(x,
128,
kernel_size=kernel_size,
activation='leaky_relu',
instance_norm=False)
x = encoder_layer(x,
256,
kernel_size=kernel_size,
strides=1,
activation='leaky_relu',
instance_norm=False)
# if patchgan=True use nxn-dim output of probability
# else use 1-dim output of probability
if patchgan:
x = LeakyReLU(alpha=0.2)(x)
outputs = Conv2D(1,
kernel_size=kernel_size,
strides=2,
padding='same')(x)
else:
x = Flatten()(x)
x = Dense(1)(x)
outputs = Activation('linear')(x)
discriminator = Model(inputs, outputs, name=name)
return discriminator
使用生成器和判别器生成器,我们现在可以构建 CycleGAN。“列表 7.1.4”显示了构建器函数。 与上一节中的讨论一致,实例化了两个生成器g_source = F和g_target = G以及两个判别器d_source = D[x]和d_target = D[y]。 正向循环为x' = F(G(x)) = reco_source = g_source(g_target(source_input))。反向循环为y' = G(F(y)) = reco_target = g_target(g_source (target_input))。
对抗模型的输入是源数据和目标数据,而输出是D[x]和D[y]的输出以及重构的输入x'和y'。 在本示例中,由于由于灰度图像和彩色图像中通道数之间的差异,因此未使用身份网络。 对于 GAN 和循环一致性损失,我们分别使用建议的λ1 = 1.0和λ2 = 10.0损失权重。 与前几章中的 GAN 相似,我们使用 RMSprop 作为判别器的优化器,其学习率为2e-4,衰减率为6e-8。 对抗的学习率和衰退率是判别器的一半。
“列表 7.1.4”:cyclegan-7.1.1.py
tf.keras中的 CycleGAN 构建器:
def build_cyclegan(shapes,
source_name='source',
target_name='target',
kernel_size=3,
patchgan=False,
identity=False
):
"""Build the CycleGAN
1) Build target and source discriminators
2) Build target and source generators
3) Build the adversarial network
Arguments:
shapes (tuple): source and target shapes
source_name (string): string to be appended on dis/gen models
target_name (string): string to be appended on dis/gen models
kernel_size (int): kernel size for the encoder/decoder
or dis/gen models
patchgan (bool): whether to use patchgan on discriminator
identity (bool): whether to use identity loss
Returns:
(list): 2 generator, 2 discriminator,
and 1 adversarial models
"""
source_shape, target_shape = shapes
lr = 2e-4
decay = 6e-8
gt_name = "gen_" + target_name
gs_name = "gen_" + source_name
dt_name = "dis_" + target_name
ds_name = "dis_" + source_name
# build target and source generators
g_target = build_generator(source_shape,
target_shape,
kernel_size=kernel_size,
name=gt_name)
g_source = build_generator(target_shape,
source_shape,
kernel_size=kernel_size,
name=gs_name)
print('---- TARGET GENERATOR ----')
g_target.summary()
print('---- SOURCE GENERATOR ----')
g_source.summary()
# build target and source discriminators
d_target = build_discriminator(target_shape,
patchgan=patchgan,
kernel_size=kernel_size,
name=dt_name)
d_source = build_discriminator(source_shape,
patchgan=patchgan,
kernel_size=kernel_size,
name=ds_name)
print('---- TARGET DISCRIMINATOR ----')
d_target.summary()
print('---- SOURCE DISCRIMINATOR ----')
d_source.summary()
optimizer = RMSprop(lr=lr, decay=decay)
d_target.compile(loss='mse',
optimizer=optimizer,
metrics=['accuracy'])
d_source.compile(loss='mse',
optimizer=optimizer,
metrics=['accuracy'])
d_target.trainable = False
d_source.trainable = False
# build the computational graph for the adversarial model
# forward cycle network and target discriminator
source_input = Input(shape=source_shape)
fake_target = g_target(source_input)
preal_target = d_target(fake_target)
reco_source = g_source(fake_target)
# backward cycle network and source discriminator
target_input = Input(shape=target_shape)
fake_source = g_source(target_input)
preal_source = d_source(fake_source)
reco_target = g_target(fake_source)
# if we use identity loss, add 2 extra loss terms
# and outputs
if identity:
iden_source = g_source(source_input)
iden_target = g_target(target_input)
loss = ['mse', 'mse', 'mae', 'mae', 'mae', 'mae']
loss_weights = [1., 1., 10., 10., 0.5, 0.5]
inputs = [source_input, target_input]
outputs = [preal_source,
preal_target,
reco_source,
reco_target,
iden_source,
iden_target]
else:
loss = ['mse', 'mse', 'mae', 'mae']
loss_weights = [1., 1., 10., 10.]
inputs = [source_input, target_input]
outputs = [preal_source,
preal_target,
reco_source,
reco_target]
# build adversarial model
adv = Model(inputs, outputs, name='adversarial')
optimizer = RMSprop(lr=lr*0.5, decay=decay*0.5)
adv.compile(loss=loss,
loss_weights=loss_weights,
optimizer=optimizer,
metrics=['accuracy'])
print('---- ADVERSARIAL NETWORK ----')
adv.summary()
return g_source, g_target, d_source, d_target, adv
我们遵循训练过程,我们可以从上一节中的“算法 7.1.1”中调用。“列表 7.1.5”显示了 CycleGAN 训练。 此训练与原始 GAN 之间的次要区别是有两个要优化的判别器。 但是,只有一种对抗模型需要优化。 对于每 2,000 步,生成器将保存预测的源图像和目标图像。 我们将的批量大小设为 32。我们也尝试了 1 的批量大小,但是输出质量几乎相同,并且需要花费更长的时间进行训练(批量为每个图像 43 ms,在 NVIDIA GTX 1060 上批量大小为 32 时,最大大小为每个图像 1 vs 3.6 ms)
“列表 7.1.5”:cyclegan-7.1.1.py
tf.keras中的 CycleGAN 训练例程:
def train_cyclegan(models,
data,
params,
test_params,
test_generator):
""" Trains the CycleGAN.
1) Train the target discriminator
2) Train the source discriminator
3) Train the forward and backward cyles of
adversarial networks
Arguments:
models (Models): Source/Target Discriminator/Generator,
Adversarial Model
data (tuple): source and target training data
params (tuple): network parameters
test_params (tuple): test parameters
test_generator (function): used for generating
predicted target and source images
"""
# the models
g_source, g_target, d_source, d_target, adv = models
# network parameters
batch_size, train_steps, patch, model_name = params
# train dataset
source_data, target_data, test_source_data, test_target_data\
= data
titles, dirs = test_params
# the generator image is saved every 2000 steps
save_interval = 2000
target_size = target_data.shape[0]
source_size = source_data.shape[0]
# whether to use patchgan or not
if patch > 1:
d_patch = (patch, patch, 1)
valid = np.ones((batch_size,) + d_patch)
fake = np.zeros((batch_size,) + d_patch)
else:
valid = np.ones([batch_size, 1])
fake = np.zeros([batch_size, 1])
valid_fake = np.concatenate((valid, fake))
start_time = datetime.datetime.now()
for step in range(train_steps):
# sample a batch of real target data
rand_indexes = np.random.randint(0,
target_size,
size=batch_size)
real_target = target_data[rand_indexes]
# sample a batch of real source data
rand_indexes = np.random.randint(0,
source_size,
size=batch_size)
real_source = source_data[rand_indexes]
# generate a batch of fake target data fr real source data
fake_target = g_target.predict(real_source)
# combine real and fake into one batch
x = np.concatenate((real_target, fake_target))
# train the target discriminator using fake/real data
metrics = d_target.train_on_batch(x, valid_fake)
log = "%d: [d_target loss: %f]" % (step, metrics[0])
# generate a batch of fake source data fr real target data
fake_source = g_source.predict(real_target)
x = np.concatenate((real_source, fake_source))
# train the source discriminator using fake/real data
metrics = d_source.train_on_batch(x, valid_fake)
log = "%s [d_source loss: %f]" % (log, metrics[0])
# train the adversarial network using forward and backward
# cycles. the generated fake source and target
# data attempts to trick the discriminators
x = [real_source, real_target]
y = [valid, valid, real_source, real_target]
metrics = adv.train_on_batch(x, y)
elapsed_time = datetime.datetime.now() - start_time
fmt = "%s [adv loss: %f] [time: %s]"
log = fmt % (log, metrics[0], elapsed_time)
print(log)
if (step + 1) % save_interval == 0:
test_generator((g_source, g_target),
(test_source_data, test_target_data),
step=step+1,
titles=titles,
dirs=dirs,
show=False)
# save the models after training the generators
g_source.save(model_name + "-g_source.h5")
g_target.save(model_name + "-g_target.h5")
最后,在使用 CycleGAN 构建和训练函数之前,我们必须执行一些数据准备。 模块cifar10_utils.py和other_ utils.py加载CIFAR10训练和测试数据。 有关这两个文件的详细信息,请参考源代码。 加载后,将训练图像和测试图像转换为灰度,以生成源数据和测试源数据。
“列表 7.1.6”显示了 CycleGAN 如何用于构建和训练用于灰度图像着色的生成器网络(g_target)。 由于 CycleGAN 是对称的,因此我们还构建并训练了第二个生成器网络(g_source),该网络可以将颜色转换为灰度。 训练了两个 CycleGAN 着色网络。 第一种使用标量输出类似于原始 GAN 的判别器,第二种使用2 x 2 PatchGAN。
“列表 7.1.6”:cyclegan-7.1.1.py
CycleGAN 用于着色:
def graycifar10_cross_colorcifar10(g_models=None):
"""Build and train a CycleGAN that can do
grayscale <--> color cifar10 images
"""
model_name = 'cyclegan_cifar10'
batch_size = 32
train_steps = 100000
patchgan = True
kernel_size = 3
postfix = ('%dp' % kernel_size) \
if patchgan else ('%d' % kernel_size)
data, shapes = cifar10_utils.load_data()
source_data, _, test_source_data, test_target_data = data
titles = ('CIFAR10 predicted source images.',
'CIFAR10 predicted target images.',
'CIFAR10 reconstructed source images.',
'CIFAR10 reconstructed target images.')
dirs = ('cifar10_source-%s' % postfix, \
'cifar10_target-%s' % postfix)
# generate predicted target(color) and source(gray) images
if g_models is not None:
g_source, g_target = g_models
other_utils.test_generator((g_source, g_target),
(test_source_data, \
test_target_data),
step=0,
titles=titles,
dirs=dirs,
show=True)
return
# build the cyclegan for cifar10 colorization
models = build_cyclegan(shapes,
"gray-%s" % postfix,
"color-%s" % postfix,
kernel_size=kernel_size,
patchgan=patchgan)
# patch size is divided by 2^n since we downscaled the input
# in the discriminator by 2^n (ie. we use strides=2 n times)
patch = int(source_data.shape[1] / 2**4) if patchgan else 1
params = (batch_size, train_steps, patch, model_name)
test_params = (titles, dirs)
# train the cyclegan
train_cyclegan(models,
data,
params,
test_params,
other_utils.test_generator)
在的下一部分中,我们将检查 CycleGAN 的生成器输出以进行着色。
CycleGAN 的生成器输出
“图 7.1.13”显示 CycleGAN 的着色结果。 源图像来自测试数据集:

图 7.1.13:使用不同技术进行着色。 显示的是基本事实,使用自编码器的着色(第 3 章,自编码器),使用带有原始 GAN 判别器的 CycleGAN 进行着色,以及使用带有 PatchGAN 判别器的 CycleGAN 进行着色。 彩色效果最佳。 原始彩色照片可以在该书的 GitHub 存储库中找到。
为了进行比较,我们使用第 3 章,“自编码器”中描述的普通自编码器显示了地面真实情况和着色结果。 通常,所有彩色图像在感觉上都是可接受的。 总体而言,似乎每种着色技术都有自己的优点和缺点。 所有着色方法与天空和车辆的正确颜色不一致。
例如,平面背景(第三行,第二列)中的天空为白色。 自编码器没错,但是 CycleGAN 认为它是浅棕色或蓝色。
对于第六行第六列,暗海上的船天空阴沉,但自编码器将其涂成蓝色和蓝色,而 CycleGAN 将其涂成蓝色和白色,而没有 PatchGAN。 两种预测在现实世界中都是有意义的。 同时,使用 PatchGAN 对 CycleGAN 的预测与基本事实相似。 在倒数第二行和第二列上,没有方法能够预测汽车的红色。 在动物身上,CycleGAN 的两种口味都具有接近真实情况的颜色。
由于 CycleGAN 是对称的,因此它还能在给定彩色图像的情况下预测灰度图像。“图 7.1.14”显示了两个 CycleGAN 变体执行的颜色到灰度转换。 目标图像来自测试数据集。 除了某些图像的灰度阴影存在细微差异外,这些预测通常是准确的。

图 7.1.14:颜色(来自图 7.1.9)到 CycleGAN 的灰度转换
要训练 CycleGAN 进行着色,命令是:
python3 cyclegan-7.1.1.py -c
读者可以使用带有 PatchGAN 的 CycleGAN 预训练模型来运行图像转换:
python3 cyclegan-7.1.1.py --cifar10_g_source=cyclegan_cifar10-g_source.h5
--cifar10_g_target=cyclegan_cifar10-g_target.h5
在本节中,我们演示了 CycleGAN 在着色上的一种实际应用。 在下一部分中,我们将在更具挑战性的数据集上训练 CycleGAN。 源域 MNIST 与目标域 SVHN 数据集有很大的不同[1]。
MNIST 和 SVHN 数据集上的 CycleGAN
我们现在要解决一个更具挑战性的问题。 假设我们使用 MNIST 灰度数字作为源数据,并且我们想从 SVHN [1]中借鉴样式,这是我们的目标数据。 每个域中的样本数据显示在“图 7.1.15”中:

图 7.1.15:两个未对齐数据的不同域。 原始彩色照片可以在该书的 GitHub 存储库中找到。
我们可以重用上一节中讨论的 CycleGAN 的所有构建和训练函数,以执行样式迁移。 唯一的区别是,我们必须添加用于加载 MNIST 和 SVHN 数据的例程。 SVHN 数据集可在这个页面中找到。
我们介绍mnist_svhn_utils.py模块来帮助我们完成此任务。“列表 7.1.7”显示了针对跨域迁移的 CycleGAN 的初始化和训练。
CycleGAN 结构与上一部分相同,不同之处在于我们使用的核大小为 5,因为两个域完全不同。
“列表 7.1.7”:cyclegan-7.1.1.py
CycleGAN 用于 MNIST 和 SVHN 之间的跨域样式迁移:
def mnist_cross_svhn(g_models=None):
"""Build and train a CycleGAN that can do mnist <--> svhn
"""
model_name = 'cyclegan_mnist_svhn'
batch_size = 32
train_steps = 100000
patchgan = True
kernel_size = 5
postfix = ('%dp' % kernel_size) \
if patchgan else ('%d' % kernel_size)
data, shapes = mnist_svhn_utils.load_data()
source_data, _, test_source_data, test_target_data = data
titles = ('MNIST predicted source images.',
'SVHN predicted target images.',
'MNIST reconstructed source images.',
'SVHN reconstructed target images.')
dirs = ('mnist_source-%s' \
% postfix, 'svhn_target-%s' % postfix)
# generate predicted target(svhn) and source(mnist) images
if g_models is not None:
g_source, g_target = g_models
other_utils.test_generator((g_source, g_target),
(test_source_data, \
test_target_data),
step=0,
titles=titles,
dirs=dirs,
show=True)
return
# build the cyclegan for mnist cross svhn
models = build_cyclegan(shapes,
"mnist-%s" % postfix,
"svhn-%s" % postfix,
kernel_size=kernel_size,
patchgan=patchgan)
# patch size is divided by 2^n since we downscaled the input
# in the discriminator by 2^n (ie. we use strides=2 n times)
patch = int(source_data.shape[1] / 2**4) if patchgan else 1
params = (batch_size, train_steps, patch, model_name)
test_params = (titles, dirs)
# train the cyclegan
train_cyclegan(models,
data,
params,
test_params,
other_utils.test_generator)
将 MNIST 从测试数据集迁移到 SVHN 的结果显示在“图 7.1.16”中。 生成的图像具有样式的 SVHN,但是数字未完全传送。 例如,在第四行上,数字 3、1 和 3 由 CycleGAN 进行样式化。
但是,在第三行中,不带有和带有 PatchGAN 的 CycleGAN 的数字 9、6 和 6 分别设置为 0、6、01、0、65 和 68:

图 7.1.16:测试数据从 MNIST 域到 SVHN 的样式迁移。 原始彩色照片可以在该书的 GitHub 存储库中找到。
向后循环的结果为“图 7.1.17”中所示的。 在这种情况下,目标图像来自 SVHN 测试数据集。 生成的图像具有 MNIST 的样式,但是数字没有正确翻译。 例如,在第一行中,对于不带和带有 PatchGAN 的 CycleGAN,数字 5、2 和 210 分别被样式化为 7、7、8、3、3 和 1:

图 7.1.17:测试数据从 SVHN 域到 MNIST 的样式迁移。 原始彩色照片可以在该书的 GitHub 存储库中找到。
在 PatchGAN 的情况下,假设预测的 MNIST 数字被限制为一位,则输出 1 是可以理解的。 有以某种方式正确的预测,例如在第二行中,不使用 PatchGAN 的 CycleGAN 将 SVHN 数字的最后三列 6、3 和 4 转换为 6、3 和 6。 但是,CycleGAN 两种版本的输出始终是个位数且可识别。
从 MNIST 到 SVHN 的转换中出现的问题称为“标签翻转”[8],其中源域中的数字转换为目标域中的另一个数字。 尽管 CycleGAN 的预测是周期一致的,但它们不一定是语义一致的。 在翻译过程中数字的含义会丢失。
为了解决这个问题, Hoffman [8]引入了一种改进的 CycleGAN,称为循环一致性对抗域自适应(CyCADA)。 不同之处在于,附加的语义损失项可确保预测不仅周期一致,而且语义一致。
“图 7.1.18”显示 CycleGAN 在正向循环中重建 MNIST 数字。 重建的 MNIST 数字几乎与源 MNIST 数字相同:

图 7.1.18:带有 MNIST 上的 PatchGAN 的 CycleGAN(源)到 SVHN(目标)的前向周期。 重建的源类似于原始源。 原始彩色照片可以在该书的 GitHub 存储库中找到。
“图 7.1.19”显示了 CycleGAN 在向后周期中重构 SVHN 数字的过程:

图 7.1.19:带有 MNIST 上的 PatchGAN 的 CycleGAN 与 SVHN(目标)的反向循环。 重建的目标与原始目标并不完全相似。 原始彩色照片可以在该书的 GitHub 存储库中找到。
在“图 7.1.3”中,CycleGAN 被描述为具有周期一致性。 换句话说,给定源x,CycleGAN 将正向循环中的源重构为x'。 另外,在给定目标y的情况下,CycleGAN 在反向循环中将目标重构为y'。
重建了许多目标图像。 有些数字显然是相同的,例如最后两列(3 和 4)中的第二行,而有些数字却是相同的但是模糊的,例如前两列列中的第一行(5 和 2)。 尽管样式仍像第二行一样,但在前两列(从 33 和 6 到 1 以及无法识别的数字)中,有些数字会转换为另一数字。
要将 MNIST 的 CycleGAN 训练为 SVHN,命令为:
python3 cyclegan-7.1.1.py -m
鼓励读者使用带有 PatchGAN 的 CycleGAN 预训练模型来运行图像翻译:
python3 cyclegan-7.1.1.py --mnist_svhn_g_source=cyclegan_mnist_svhn-g_ source.h5 --mnist_svhn_g_target=cyclegan_mnist_svhn-g_target.h5
到目前为止,我们只看到了 CycleGAN 的两个实际应用。 两者都在小型数据集上进行了演示,以强调可重复性的概念。 如本章前面所述,CycleGAN 还有许多其他实际应用。 我们在这里介绍的 CycleGAN 可以作为分辨率更高的图像转换的基础。
2. 总结
在本章中,我们讨论了 CycleGAN 作为可用于图像翻译的算法。 在 CycleGAN 中,源数据和目标数据不一定要对齐。 我们展示了两个示例,灰度 ↔ 颜色和 MNIST ↔ SVHN ,尽管 CycleGAN 可以执行许多其他可能的图像转换 。
在下一章中,我们将着手另一种生成模型,即变分自编码器(VAE)。 VAE 具有类似的学习目标–如何生成新图像(数据)。 他们专注于学习建模为高斯分布的潜在向量。 我们将以有条件的 VAE 和解开 VAE 中的潜在表示形式来证明 GAN 解决的问题中的其他相似之处。
3. 参考
Yuval Netzer et al.: Reading Digits in Natural Images with Unsupervised Feature Learning. NIPS workshop on deep learning and unsupervised feature learning. Vol. 2011. No. 2. 2011 (https://www-cs.stanford.edu/~twangcat/papers/nips2011_housenumbers.pdf).Zhu-Jun-Yan et al.: Unpaired Image-to-Image Translation using Cycle-Consistent Adversarial Networks. 2017 IEEE International Conference on Computer Vision (ICCV). IEEE, 2017 (http://openaccess.thecvf.com/content_ICCV_2017/papers/Zhu_Unpaired_Image-To-Image_Translation_ICCV_2017_paper.pdf).Phillip Isola et al.: Image-to-Image Translation with Conditional Adversarial Networks. 2017 IEEE Conference on Computer Vision and Pattern Recognition (CVPR). IEEE, 2017 (http://openaccess.thecvf.com/content_cvpr_2017/papers/Isola_Image-To-Image_Translation_With_CVPR_2017_paper.pdf).Mehdi Mirza and Simon Osindero. Conditional Generative Adversarial Nets. arXiv preprint arXiv:1411.1784, 2014 (https://arxiv.org/pdf/1411.1784.pdf).Xudong Mao et al.: Least Squares Generative Adversarial Networks. 2017 IEEE International Conference on Computer Vision (ICCV). IEEE, 2017 (http://openaccess.thecvf.com/content_ICCV_2017/papers/Mao_Least_Squares_Generative_ICCV_2017_paper.pdf).Chuan Li and Michael Wand. Precomputed Real-Time Texture Synthesis with Markovian Generative Adversarial Networks. European Conference on Computer Vision. Springer, Cham, 2016 (https://arxiv.org/pdf/1604.04382.pdf).Olaf Ronneberger, Philipp Fischer, and Thomas Brox. U-Net: Convolutional Networks for Biomedical Image Segmentation. International Conference on Medical image computing and computer-assisted intervention. Springer, Cham, 2015 (https://arxiv.org/pdf/1505.04597.pdf).Judy Hoffman et al.: CyCADA: Cycle-Consistent Adversarial Domain Adaptation. arXiv preprint arXiv:1711.03213, 2017 (https://arxiv.org/pdf/1711.03213.pdf).
八、变分自编码器(VAE)
与我们在之前的章节中讨论过的生成对抗网络(GAN)类似,变分自编码器(VAE)[1] 属于生成模型家族。 VAE 的生成器能够在导航其连续潜在空间的同时产生有意义的输出。 通过潜向量探索解码器输出的可能属性。
在 GAN 中,重点在于如何得出近似输入分布的模型。 VAE 尝试对可解码的连续潜在空间中的输入分布进行建模。 这是 GAN 与 VAE 相比能够生成更真实信号的可能的潜在原因之一。 例如,在图像生成中,GAN 可以生成看起来更逼真的图像,而相比之下,VAE 生成的图像清晰度较差。
在 VAE 中,重点在于潜在代码的变分推理。 因此,VAE 为潜在变量的学习和有效贝叶斯推理提供了合适的框架。 例如,带有解缠结表示的 VAE 可以将潜在代码重用于迁移学习。
在结构上,VAE 与自编码器相似。 它也由编码器(也称为识别或推理模型)和解码器(也称为生成模型)组成。 VAE 和自编码器都试图在学习潜向量的同时重建输入数据。
但是,与自编码器不同,VAE 的潜在空间是连续的,并且解码器本身被用作生成模型。
在前面各章中讨论的 GAN 讨论中,也可以对 VAE 的解码器进行调整。 例如,在 MNIST 数据集中,我们能够指定一个给定的单热向量产生的数字。 这种有条件的 VAE 类别称为 CVAE [2]。 也可以通过在损失函数中包含正则化超参数来解开 VAE 潜向量。 这称为 β-VAE [5]。 例如,在 MNIST 中,我们能够隔离确定每个数字的粗细或倾斜角度的潜向量。 本章的目的是介绍:
- VAE 的原理
- 了解重新参数化技巧,有助于在 VAE 优化中使用随机梯度下降
- 有条件的 VAE(CVAE)和 β-VAE 的原理
- 了解如何使用
tf.keras实现 VAE
我们将从谈论 VAE 的基本原理开始。
1. VAE 原理
在生成模型中,我们经常对使用神经网络来逼近输入的真实分布感兴趣:
(Equation 8.1.1)
在前面的等式中,θ表示训练期间确定的参数。 例如,在名人面孔数据集的上下文中,这等效于找到可以绘制面孔的分布。 同样,在 MNIST 数据集中,此分布可以生成可识别的手写数字。
在机器学习中,为了执行特定级别的推理,我们有兴趣寻找P[θ](x, z),这是输入x和潜在变量z之间的联合分布。 潜在变量不是数据集的一部分,而是对可从输入中观察到的某些属性进行编码。 在名人面孔的背景下,这些可能是面部表情,发型,头发颜色,性别等。 在 MNIST 数据集中,潜在变量可以表示数字和书写样式。
P[θ](x, z)实际上是输入数据点及其属性的分布。 P[θ](x)可以从边际分布计算得出:
(Equation 8.1.2)
换句话说,考虑所有可能的属性,我们最终得到描述输入的分布。 在名人面孔中,如果考虑所有面部表情,发型,头发颜色和性别,将恢复描述名人面孔的分布。 在 MNIST 数据集中,如果考虑所有可能的数字,书写风格等,我们以手写数字的分布来结束。
问题在于“公式 8.1.2”很难处理。 该方程式没有解析形式或有效的估计量。 它的参数无法微分。 因此,通过神经网络进行优化是不可行的。
使用贝叶斯定理,我们可以找到“公式 8.1.2”的替代表达式:
(Equation 8.1.3)
P(z)是z的先验分布。 它不以任何观察为条件。 如果z是离散的,而P[θ](x | z)是高斯分布,则P[θ](x)是高斯的混合。 如果z是连续的,则P[θ](x)是高斯的无限混合。
实际上,如果我们尝试在没有合适的损失函数的情况下建立一个近似P[θ](x | z)的神经网络,它将忽略z得出一个简单的解P[θ](x | z) = P[θ](x)。 因此,“公式 8.1.3”无法为我们提供P[θ](x)的良好估计。 或者,“公式 8.1.2”也可以表示为:
(Equation 8.1.4)
但是,P[θ](z | x)也很棘手。 VAE 的目标是在给定输入的情况下,找到一种可预测的分布,该分布易于估计P[θ](z | x),即潜在属性z的条件分布的估计。
变分推理
为了使易于处理,VAE 引入了变化推理模型(编码器):
(Equation 8.1.5)
Q[φ](z | x)提供了P[θ](z | x)的良好估计。 它既参数化又易于处理。 Q[φ](z | x)可以通过优化参数φ由深度神经网络近似。 通常,Q[φ](z | x)被选择为多元高斯:
(Equation 8.1.6)
均值μ(x)和标准差σ(x)均由编码器神经网络使用输入数据点计算得出。 对角线矩阵表示z的元素是独立的。
在下一节中,我们将求解 VAE 的核心方程。 核心方程式将引导我们找到一种优化算法,该算法将帮助我们确定推理模型的参数。
核心方程
推理模型Q[φ](z | x)从输入x生成潜向量z。 Q[φ](z | x)似于自编码器模型中的编码器。 另一方面,从潜在代码z重构输入。 P[θ](x | z)的作用类似于自编码器模型中的解码器。 要估计P[θ](x),我们必须确定其与Q[φ](z | x)和P[θ](x | z)的关系。
如果Q[φ](z | x)是P[θ](z | x)的估计值,则 Kullback-Leibler(KL)的差异决定了这两个条件密度之间的距离:
(Equation 8.1.7)
使用贝叶斯定理:
(Equation 8.1.8)
在“公式 8.1.7”中:
(Equation 8.1.9)
由于log P[θ](x)不依赖于z ~ Q,因此可能会超出预期。 重新排列“公式 8.1.9”并认识到:
,其结果是:
(Equation 8.1.10)
“公式 8.1.10”是 VAE 的核心。 左侧是项P[θ](x),由于Q[φ](z | x)与真实P[θ](z | x)的距离,我们使误差最小化。 我们可以记得,的对数不会更改最大值(或最小值)的位置。 给定提供P[θ](z | x)良好估计的推断模型,D[KL](Q[φ](z | x) || P[θ](z | x)大约为零。
右边的第一项P[θ](x | z)类似于解码器,该解码器从推理模型中抽取样本以重建输入。
第二个项是另一个距离。 这次是在Q[φ](z | x)和先前的P[θ](z)之间。 “公式 8.1.10”的左侧也称为变异下界或证据下界(ELBO)。 由于 KL 始终为正,因此 ELBO 是log P[θ](x)的下限。 通过优化神经网络的参数φ和θ来最大化 ELBO 意味着:
- 在将
z中的x属性编码时,D[KL](Q[φ](z | x) || P[θ](z | x) -> 0或推理模型变得更好。 - 右边的
log P[θ](x | z)最大化了“公式 8.1.10”或解码器模型在从潜在向量z重构x方面变得更好。 - 在下一节中,我们将利用“公式 8.1.10”的结构来确定推理模型(编码器)和解码器的损失函数。
优化
“公式 8.1.10”的右侧具有有关 VAE 的loss函数的两个重要信息。 解码器项E[z~Q] [log P[θ](x | z)]表示生成器从推理模型的输出中提取z个样本,以重建输入。 使最大化是指我们将重构损失和L_R降到最低。 如果假设图像(数据)分布为高斯分布,则可以使用 MSE。
如果每个像素(数据)都被认为是伯努利分布,那么损失函数就是二进制互熵。
第二项-D[KL](Q[φ](z | x) || P[θ](z))易于评估。 根据“公式 8.1.6”,Q[φ]是高斯分布。 通常,P[θ](z) = P(z) = N(0, 1)也是平均值为零且标准差等于 1.0 的高斯。 在“公式 8.1.11”中,我们看到 KL 项简化为:
(Equation 8.1.11)
其中J是z的维。 $1ⱼ和$1ⱼ都是通过推理模型计算的x的函数。 要最大化:-D[KL],σ[j] -> 1和μ[j] -> 9。 P(z) = N(0, 1)的选择源于各向同性单位高斯的性质,在具有适当函数的情况下,它可以变形为任意分布[6]。
根据“公式 8.1.11”,KL 损失L_KL简称为D[KL]。
总之,在“公式 8.1.12”中将 VAE loss函数定义为:
(Equation 8.1.12)
在给定编码器和解码器模型的情况下,在我们可以构建和训练 VAE(随机采样块,生成潜在属性)之前,还需要解决一个问题。 在下一节中,我们将讨论此问题以及如何使用重新参数化技巧解决它。
重新参数化技巧
“图 8.1.1”的左侧显示了 VAE 网络。 编码器获取输入x,并估计潜向量z的多元高斯分布的平均值μ和标准差σ。 解码器从潜向量z中提取样本,以将输入重构为x_tilde。 这似乎很简单,直到在反向传播期间发生梯度更新为止:

图 8.1.1:带有和不带有重新参数化技巧的 VAE 网络
反向传播梯度将不会通过随机采样块。 尽管具有用于神经网络的随机输入是可以的,但梯度不可能穿过随机层。
解决此问题的方法是将采样处理作为输入,如“图 8.1.1”右侧所示。 然后,将样本计算为:
(Equation 8.1.13)
如果ε和σ以向量格式表示,则εσ是逐元素乘法。 使用“公式 8.1.13”,看起来好像采样直接来自潜在空间一样。 这项技术被称为重新参数化技巧。
现在,在输入端发生采样时,可以使用熟悉的优化算法(例如 SGD,Adam 或 RMSProp)来训练 VAE 网络。
在讨论如何在tf.keras中实现 VAE 之前,让我们首先展示如何测试经过训练的解码器。
解码器测试
在训练了 VAE 网络之后,可以丢弃推理模型,包括加法和乘法运算符。 为了生成新的有意义的输出,请从用于生成ε的高斯分布中抽取样本。“图 8.1.2”向我们展示了解码器的测试设置:

图 8.1.2:解码器测试设置
通过重新参数化技巧解决了 VAE 上的最后一个问题,我们现在可以在tf.keras中实现和训练变分自编码器。
ALAS 与 Keras
VAE 的结构类似于典型的自编码器。 区别主要在于重新参数化技巧中的高斯随机变量的采样。“列表 8.1.1”显示了使用 MLP 实现的编码器,解码器和 VAE。
为便于显示潜在代码,将z的维设置为 2。编码器仅是两层 MLP,第二层生成均值和对数方差。 对数方差的使用是为了简化 KL 损失和重新参数化技巧的计算。 编码器的第三个输出是使用重新参数化技巧进行的z采样。 我们应该注意,在采样函数exp(0.5 log σ²) = sqrt(σ²) = σ中,因为σ > 0假定它是高斯分布的标准差。
解码器也是两层 MLP,它采用z的样本来近似输入。 编码器和解码器均使用大小为 512 的中间尺寸。
VAE 网络只是将编码器和解码器连接在一起。 loss函数是重建损失和 KL 损失的总和。 在默认的 Adam 优化器上,VAE 网络具有良好的效果。 VAE 网络中的参数总数为 807,700。
VAE MLP 的 Keras 代码具有预训练的权重。 要测试,我们需要运行:
python3 vae-mlp-mnist-8.1.1.py --weights=vae_mlp_mnist.tf
“列表 8.1.1”:vae-mlp-mnist-8.1.1.py
# reparameterization trick
# instead of sampling from Q(z|X), sample eps = N(0,I)
# z = z_mean + sqrt(var)*eps
def sampling(args):
"""Reparameterization trick by sampling
fr an isotropic unit Gaussian.
# Arguments:
args (tensor): mean and log of variance of Q(z|X)
# Returns:
z (tensor): sampled latent vector
"""
z_mean, z_log_var = args
# K is the keras backend
batch = K.shape(z_mean)[0]
dim = K.int_shape(z_mean)[1]
# by default, random_normal has mean=0 and std=1.0
epsilon = K.random_normal(shape=(batch, dim))
return z_mean + K.exp(0.5 * z_log_var) * epsilon
# MNIST dataset
(x_train, y_train), (x_test, y_test) = mnist.load_data()
image_size = x_train.shape[1]
original_dim = image_size * image_size
x_train = np.reshape(x_train, [-1, original_dim])
x_test = np.reshape(x_test, [-1, original_dim])
x_train = x_train.astype('float32') / 255
x_test = x_test.astype('float32') / 255
# network parameters
input_shape = (original_dim, )
intermediate_dim = 512
batch_size = 128
latent_dim = 2
epochs = 50
# VAE model = encoder + decoder
# build encoder model
inputs = Input(shape=input_shape, name='encoder_input')
x = Dense(intermediate_dim, activation='relu')(inputs)
z_mean = Dense(latent_dim, name='z_mean')(x)
z_log_var = Dense(latent_dim, name='z_log_var')(x)
# use reparameterization trick to push the sampling out as input
# note that "output_shape" isn't necessary
# with the TensorFlow backend
z = Lambda(sampling,
output_shape=(latent_dim,),
name='z')([z_mean, z_log_var])
# instantiate encoder model
encoder = Model(inputs, [z_mean, z_log_var, z], name='encoder')
# build decoder model
latent_inputs = Input(shape=(latent_dim,), name='z_sampling')
x = Dense(intermediate_dim, activation='relu')(latent_inputs)
outputs = Dense(original_dim, activation='sigmoid')(x)
# instantiate decoder model
decoder = Model(latent_inputs, outputs, name='decoder')
# instantiate VAE model
outputs = decoder(encoder(inputs)[2])
vae = Model(inputs, outputs, name='vae_mlp')
if __name__ == '__main__':
parser = argparse.ArgumentParser()
help_ = "Load tf model trained weights"
parser.add_argument("-w", "--weights", help=help_)
help_ = "Use binary cross entropy instead of mse (default)"
parser.add_argument("--bce", help=help_, action='store_true')
args = parser.parse_args()
models = (encoder, decoder)
data = (x_test, y_test)
# VAE loss = mse_loss or xent_loss + kl_loss
if args.bce:
reconstruction_loss = binary_crossentropy(inputs,
outputs)
else:
reconstruction_loss = mse(inputs, outputs)
reconstruction_loss *= original_dim
kl_loss = 1 + z_log_var - K.square(z_mean) - K.exp(z_log_var)
kl_loss = K.sum(kl_loss, axis=-1)
kl_loss *= -0.5
vae_loss = K.mean(reconstruction_loss + kl_loss)
vae.add_loss(vae_loss)
vae.compile(optimizer='adam')
“图 8.1.3”显示了编码器模型,它是一个 MLP,具有两个输出,即潜向量的均值和方差。 lambda 函数实现了重新参数化技巧,将随机潜在代码的采样推送到 VAE 网络之外:

图 8.1.3:VAE MLP 的编码器模型
“图 8.1.4”显示了解码器模型。 2 维输入来自 lambda 函数。 输出是重构的 MNIST 数字:

图 8.1.4:VAE MLP 的解码器模型
“图 8.1.5”显示了完整的 VAE 模型。 通过将编码器和解码器模型结合在一起制成:

图 8.1.5:使用 MLP 的 VAE 模型
“图 8.1.6”显示了使用plot_results()在 50 个周期后潜向量的连续空间。 为简单起见,此函数未在此处显示,但可以在vae-mlp-mnist-8.1.1.py的其余代码中找到。 该函数绘制两个图像,即测试数据集标签(“图 8.1.6”)和样本生成的数字(“图 8.1.7”),这两个图像都是z的函数。 这两个图都说明了潜在向量如何确定所生成数字的属性:

图 8.1.6:MNIST 数字标签作为测试数据集(VAE MLP)的潜在向量平均值的函数。 原始图像可以在该书的 GitHub 存储库中找到。
浏览时,连续空格始终会产生与 MNIST 数字相似的输出。 例如,数字 9 的区域接近数字 7 的区域。从中心附近的 9 移动到左下角会将数字变形为 7。从中心向上移动会将生成的数字从 3 更改为 5,最后变为 0.数字的变形在“图 8.1.7”中更明显,这是解释“图 8.1.6”的另一种方式。
在“图 8.1.7”中,显示生成器输出。 显示了潜在空间中数字的分布。 可以观察到所有数字都被表示。 由于中心附近分布密集,因此变化在中间迅速,在平均值较高的区域则缓慢。 我们需要记住,“图 8.1.7”是“图 8.1.6”的反映。 例如,数字 0 在两个图的左上象限中,而数字 1 在右下象限中。
“图 8.1.7”中存在一些无法识别的数字,尤其是在右上象限中。 从“图 8.1.6”可以看出,该区域大部分是空的,并且远离中心:

图 8.1.7:根据潜在向量平均值(VAE MLP)生成的数字。 为了便于解释,均值的范围类似于图 8.1.6
在本节中,我们演示了如何在 MLP 中实现 VAE。 我们还解释了导航潜在空间的结果。 在的下一部分中,我们将使用 CNN 实现相同的 VAE。
带有 CNN 的 AE
在原始论文《自编码变分贝叶斯》[1]中,使用 MLP 来实现 VAE 网络,这与我们在上一节中介绍的类似。 在本节中,我们将证明使用 CNN 将显着提高所产生数字的质量,并将参数数量显着减少至 134,165。
“列表 8.1.3”显示了编码器,解码器和 VAE 网络。 该代码也被添加到了官方的 Keras GitHub 存储库中。
为简洁起见,不再显示与 MLP VAE 类似的某些代码行。 编码器由两层 CNN 和两层 MLP 组成,以生成潜在代码。 编码器的输出结构与上一节中看到的 MLP 实现类似。 解码器由一层Dense和三层转置的 CNN 组成。
VAE CNN 的 Keras 代码具有预训练的权重。 要测试,我们需要运行:
python3 vae-cnn-mnist-8.1.2.py --weights=vae_cnn_mnist.tf
“列表 8.1.3”:vae-cnn-mnist-8.1.2.py
使用 CNN 层的tf.keras中的 VAE:
# network parameters
input_shape = (image_size, image_size, 1)
batch_size = 128
kernel_size = 3
filters = 16
latent_dim = 2
epochs = 30
# VAE model = encoder + decoder
# build encoder model
inputs = Input(shape=input_shape, name='encoder_input')
x = inputs
for i in range(2):
filters *= 2
x = Conv2D(filters=filters,
kernel_size=kernel_size,
activation='relu',
strides=2,
padding='same')(x)
# shape info needed to build decoder model
shape = K.int_shape(x)
# generate latent vector Q(z|X)
x = Flatten()(x)
x = Dense(16, activation='relu')(x)
z_mean = Dense(latent_dim, name='z_mean')(x)
z_log_var = Dense(latent_dim, name='z_log_var')(x)
# use reparameterization trick to push the sampling out as input
# note that "output_shape" isn't necessary
# with the TensorFlow backend
z = Lambda(sampling,
output_shape=(latent_dim,),
name='z')([z_mean, z_log_var])
# instantiate encoder model
encoder = Model(inputs, [z_mean, z_log_var, z], name='encoder')
# build decoder model
latent_inputs = Input(shape=(latent_dim,), name='z_sampling')
x = Dense(shape[1] * shape[2] * shape[3],
activation='relu')(latent_inputs)
x = Reshape((shape[1], shape[2], shape[3]))(x)
for i in range(2):
x = Conv2DTranspose(filters=filters,
kernel_size=kernel_size,
activation='relu',
strides=2,
padding='same')(x)
filters //= 2
outputs = Conv2DTranspose(filters=1,
kernel_size=kernel_size,
activation='sigmoid',
padding='same',
name='decoder_output')(x)
# instantiate decoder model
decoder = Model(latent_inputs, outputs, name='decoder')
# instantiate VAE model
outputs = decoder(encoder(inputs)[2])
vae = Model(inputs, outputs, name='vae')
“图 8.1.8”显示了 CNN 编码器模型的两个输出,即潜向量的均值和方差。 lambda 函数实现了重新参数化技巧,将随机潜码的采样推送到 VAE 网络之外:

图 8.1.8:VAE CNN 的编码器
“图 8.1.9”显示了 CNN 解码器模型。 2 维输入来自 lambda 函数。 输出是重构的 MNIST 数字:

图 8.1.9:VAE CNN 的解码器
“图 8.1.10”显示完整的 CNN VAE 模型。 通过将编码器和解码器模型结合在一起制成:

图 8.1.10:使用 CNN 的 VAE 模型
对 VAE 进行了 30 个周期的训练。“图 8.1.11”显示了在导航 VAE 的连续潜在空间时数字的分布。 例如,从中间到右边从 2 变为 0:

图 8.1.11:MNIST 数字标签作为测试数据集(VAE CNN)的潜在向量平均值的函数。 原始图像可以在该书的 GitHub 存储库中找到。
“图 8.1.12”向我们展示了生成模型的输出。 从质量上讲,与“图 8.1.7”(具有 MLP 实现)相比,模棱两可的位数更少:

图 8.1.12:根据潜在向量平均值(VAE CNN)生成的数字。 为了便于解释,均值的范围类似于图 8.1.11
前的两节讨论了使用 MLP 或 CNN 的 VAE 的实现。 我们分析了两种实现方式的结果,结果表明 CNN 可以减少参数数量并提高感知质量。 在下一节中,我们将演示如何在 VAE 中实现条件,以便我们可以控制要生成的数字。
2. 条件 VAE(CVAE)
有条件的 VAE [2]与 CGAN 相似。 在 MNIST 数据集的上下文中,如果随机采样潜在空间,则 VAE 无法控制将生成哪个数字。 CVAE 可以通过包含要产生的数字的条件(单标签)来解决此问题。 该条件同时施加在编码器和解码器输入上。
正式地,将“公式 8.1.10”中 VAE 的核心公式修改为包括条件c:
(Equation 8.2.1)
与 VAE 相似,“公式 8.2.1”表示如果要最大化输出条件c和P[θ](x | c),则必须最小化两个损失项:
- 给定潜在向量和条件,解码器的重建损失。
- 给定潜在向量和条件的编码器之间的 KL 损失以及给定条件的先验分布。 与 VAE 相似,我们通常选择
P[θ](x | c) = P(x | c) = N(0, 1)。
实现 CVAE 需要对 VAE 的代码进行一些修改。 对于 CVAE,使用 VAE CNN 实现是因为它可以形成一个较小的网络,并产生感知上更好的数字。
“列表 8.2.1”突出显示了针对 MNIST 数字的 VAE 原始代码所做的更改。 编码器输入现在是原始输入图像及其单标签的连接。 解码器输入现在是潜在空间采样与其应生成的图像的一键热标签的组合。 参数总数为 174,437。 与 β-VAE 相关的代码将在本章下一节中讨论。
损失函数没有改变。 但是,在训练,测试和结果绘制过程中会提供单热标签。
“列表 8.2.1”:cvae-cnn-mnist-8.2.1.py
tf.keras中使用 CNN 层的 CVAE。 重点介绍了为支持 CVAE 而进行的更改:
# compute the number of labels
num_labels = len(np.unique(y_train))
# network parameters
input_shape = (image_size, image_size, 1)
label_shape = (num_labels, )
batch_size = 128
kernel_size = 3
filters = 16
latent_dim = 2
epochs = 30
# VAE model = encoder + decoder
# build encoder model
inputs = Input(shape=input_shape, name='encoder_input')
y_labels = Input(shape=label_shape, name='class_labels')
x = Dense(image_size * image_size)(y_labels)
x = Reshape((image_size, image_size, 1))(x)
x = keras.layers.concatenate([inputs, x])
for i in range(2):
filters *= 2
x = Conv2D(filters=filters,
kernel_size=kernel_size,
activation='relu',
strides=2,
padding='same')(x)
# shape info needed to build decoder model
shape = K.int_shape(x)
# generate latent vector Q(z|X)
x = Flatten()(x)
x = Dense(16, activation='relu')(x)
z_mean = Dense(latent_dim, name='z_mean')(x)
z_log_var = Dense(latent_dim, name='z_log_var')(x)
# use reparameterization trick to push the sampling out as input
# note that "output_shape" isn't necessary
# with the TensorFlow backend
z = Lambda(sampling,
output_shape=(latent_dim,),
name='z')([z_mean, z_log_var])
# instantiate encoder model
encoder = Model([inputs, y_labels],
[z_mean, z_log_var, z],
name='encoder')
# build decoder model
latent_inputs = Input(shape=(latent_dim,), name='z_sampling')
x = concatenate([latent_inputs, y_labels])
x = Dense(shape[1]*shape[2]*shape[3], activation='relu')(x)
x = Reshape((shape[1], shape[2], shape[3]))(x)
for i in range(2):
x = Conv2DTranspose(filters=filters,
kernel_size=kernel_size,
activation='relu',
strides=2,
padding='same')(x)
filters //= 2
outputs = Conv2DTranspose(filters=1,
kernel_size=kernel_size,
activation='sigmoid',
padding='same',
name='decoder_output')(x)
# instantiate decoder model
decoder = Model([latent_inputs, y_labels],
outputs,
name='decoder')
# instantiate vae model
outputs = decoder([encoder([inputs, y_labels])[2], y_labels])
cvae = Model([inputs, y_labels], outputs, name='cvae')
“图 8.2.1”显示了 CVAE 模型的编码器。 附加输入,即单热向量class_labels形式的条件标签表示:

图 8.2.1:CVAE CNN 中的编码器。 输入现在包括 VAE 输入和条件标签的连接
“图 8.2.2”显示了 CVAE 模型的解码器。 附加输入,即单热向量class_labels形式的条件标签表示:

图 8.2.2:CVAE CNN 中的解码器。 输入现在包括 z 采样和条件标签的连接
“图 8.2.3”显示了完整的 CVAE 模型,该模型是编码器和解码器结合在一起的。 附加输入,即单热向量class_labels形式的条件标签:

图 8.2.3:使用 CNN 的 CVAE 模型。输入现在包含一个 VAE 输入和一个条件标签
在“图 8.2.4”中,每个标记的平均值分布在 30 个周期后显示。 与前面章节中的“图 8.1.6”和“图 8.1.11”不同,每个标签不是集中在一个区域上,而是分布在整个图上。 这是预期的,因为潜在空间中的每个采样都应生成一个特定的数字。 浏览潜在空间会更改该特定数字的属性。 例如,如果指定的数字为 0,则在潜伏空间中导航仍将产生 0,但是诸如倾斜角度,厚度和其他书写样式方面的属性将有所不同。

图 8.2.4:作为测试数据集(CVAE CNN)的潜在向量平均值的函数的 MNIST 数字标签。 原始图像可以在该书的 GitHub 存储库中找到。
“图 8.2.4”在“图 8.2.5”中更清楚地显示,数字 0 到 5。每个帧都有相同的数字,并且属性在我们浏览时顺畅地变化。 潜在代码:

图 8.2.5:根据潜在向量平均值和单热点标签(CVAE CNN)生成的数字 0 至 5。 为了便于解释,均值的范围类似于图 8.2.4。
“图 8.2.6”显示“图 8.2.4”,用于数字 6 至 9:

图 8.2.6:根据潜在向量平均值和单热点标签(CVAE CNN)生成的数字 6 至 9。 为了便于解释,均值的范围类似于图 8.2.4。
为了便于比较,潜向量的值范围与“图 8.2.4”中的相同。 使用预训练的权重,可以通过执行以下命令来生成数字(例如 0):
python3 cvae-cnn-mnist-8.2.1.py –bce --weights=cvae_cnn_mnist.tf --digit=0
在“图 8.2.5”和“图 8.2.6”中,可以注意到,每个数字的宽度和圆度(如果适用)随z₀的变化而变化。 从左到右追踪。 同时,当z₁从上到下导航时,每个数字的倾斜角度和圆度(如果适用)也会发生变化。 随着我们离开分布中心,数字的图像开始退化。 这是可以预期的,因为潜在空间是一个圆形。
属性中其他明显的变化可能是数字特定的。 例如,数字 1 的水平笔划(手臂)在左上象限中可见。 数字 7 的水平笔划(纵横线)只能在右象限中看到。
在下一节中,我们将发现 CVAE 实际上只是另一种称为 β-VAE 的 VAE 的特例。
3. β-VAE – 具有纠缠的潜在表示形式的 VAE
在“第 6 章”,“非纠缠表示 GAN”中,讨论了潜码非纠缠表示的概念和重要性。 我们可以回想起,一个纠缠的表示是单个潜伏单元对单个生成因子的变化敏感,而相对于其他因子的变化相对不变[3]。 更改潜在代码会导致生成的输出的一个属性发生更改,而其余属性保持不变。
在同一章中,InfoGAN [4]向我们展示了对于 MNIST 数据集,可以控制生成哪个数字以及书写样式的倾斜度和粗细。 观察上一节中的结果,可以注意到,VAE 在本质上使潜向量维解开了一定程度。 例如,查看“图 8.2.6”中的数字 8,从上到下导航z₁会减小宽度和圆度,同时顺时针旋转数字。 从左至右增加z₀也会在逆时针旋转数字时减小宽度和圆度。 换句话说,z₁控制顺时针旋转,而z₀影响逆时针旋转,并且两者都改变宽度和圆度。
在本节中,我们将演示对 VAE 损失函数的简单修改会迫使潜在代码进一步解开纠缠。 修改为正恒重β > 1,用作 KL 损失的调节器:
(Equation 8.3.1)
VAE 的这种变化称为 β-VAE [5]。 β的隐含效果是更严格的标准差。 换句话说,β强制后验分布中的潜码Q[φ](z | x)独立。
实现 β-VAE 很简单。 例如,对于上一个示例中的 CVAE,所需的修改是kl_loss中的额外beta因子:
kl_loss = 1 + z_log_var - K.square(z_mean) - K.exp(z_log_var)
kl_loss = K.sum(kl_loss, axis=-1)
kl_loss *= -0.5 * beta
CVAE 是 β-VAE 的特例,其中β = 1。 其他一切都一样。 但是,确定的值需要一些反复试验。 为了潜在的代码独立性,在重构误差和正则化之间必须有一个仔细的平衡。 解缠最大在β = 9附近。 当中β = 9的值时,β-VAE 仅被迫学习一个解纠缠的表示,而忽略另一个潜在维度。
“图 8.3.1”和“图 8.3.2”显示 β-VAE 的潜向量平均值,其中β = 9和β = 10:

图 8.3.1:MNIST 数字标签与测试数据集的潜在向量平均值的函数(β-VAE,β = 9)。 原始图像可以在该书的 GitHub 存储库中找到。
β = 9时,与 CVAE 相比,分布具有较小的标准差。 在β = 10的情况下,仅学习了潜在代码。 分布实际上缩小为一个维度,编码器和解码器忽略了第一潜码z₀。

图 8.3.2:MNIST 数字标签与测试数据集的潜向量平均值的函数(β-VAE 和β = 10)
这些观察结果反映在“图 8.3.3”中。 具有β = 9的 β-VAE 具有两个实际上独立的潜在代码。 z₀确定书写样式的倾斜度,而z₁指定数字的宽度和圆度(如果适用)。 对于中β = 10的 β-VAE,z₀被静音。 z₀的增加不会显着改变数字。z₁确定书写样式的倾斜角度和宽度:

图 8.3.3:根据潜在向量平均值和单热点标签(β-VAE,β = 1, 9, 10)生成的数字 0 至 3。 为了便于解释,均值的范围类似于图 8.3.1。
β-VAE 的tf.keras代码具有预训练的权重。 要使用β = 9生成数字 0 来测试 β-VAE,我们需要运行以下命令:
python3 cvae-cnn-mnist-8.2.1.py --beta=9 --bce --weights=beta-cvae_cnn_mnist.tf --digit=0
总而言之,我们已经证明与 GAN 相比,在 β-VAE 上更容易实现解缠表示学习。 我们所需要做的就是调整单个超参数。
4. 总结
在本章中,我们介绍了 VAE 的原理。 正如我们从 VAE 原理中学到的那样,从两次尝试从潜在空间创建合成输出的角度来看,它们都与 GAN 相似。 但是,可以注意到,与 GAN 相比,VAE 网络更简单,更容易训练。 越来越清楚的是 CVAE 和 β-VAE 在概念上分别类似于条件 GAN 和解缠表示 GAN。
VAE 具有消除潜在向量纠缠的内在机制。 因此,构建 β-VAE 很简单。 但是,我们应该注意,可解释和解开的代码对于构建智能体很重要。
在下一章中,我们将专注于强化学习。 在没有任何先验数据的情况下,智能体通过与周围的世界进行交互来学习。 我们将讨论如何为智能体的正确行为提供奖励,并为错误的行为提供惩罚。
5. 参考
Diederik P. Kingma and Max Welling. Auto-encoding Variational Bayes. arXiv preprint arXiv:1312.6114, 2013 (https://arxiv.org/pdf/1312.6114.pdf).Kihyuk Sohn, Honglak Lee, and Xinchen Yan. Learning Structured Output Representation Using Deep Conditional Generative Models. Advances in Neural Information Processing Systems, 2015 (http://papers.nips.cc/paper/5775-learning-structured-output-representation-using-deep-conditional-generative-models.pdf).Yoshua Bengio, Aaron Courville, and Pascal Vincent. Representation Learning.A Review and New Perspectives. IEEE transactions on Pattern Analysis and Machine Intelligence 35.8, 2013: 1798-1828 (https://arxiv.org/pdf/1206.5538.pdf).Xi Chen et al.: Infogan: Interpretable Representation Learning by Information Maximizing Generative Adversarial Nets. Advances in Neural Information Processing Systems, 2016 (http://papers.nips.cc/paper/6399-infogan-interpretable-representation-learning-by-information-maximizing-generative-adversarial-nets.pdf).I. Higgins, L. Matthey, A. Pal, C. Burgess, X. Glorot, M. Botvinick, S. Mohamed, and A. Lerchner. -VAE: Learning Basic Visual Concepts with a Constrained Variational Framework. ICLR, 2017 (https://openreview.net/pdf?id=Sy2fzU9gl).Carl Doersch. Tutorial on variational autoencoders. arXiv preprint arXiv:1606.05908, 2016 (https://arxiv.org/pdf/1606.05908.pdf).
九、深度强化学习
强化学习(RL)是智能体程序用于决策的框架。 智能体不一定是软件实体,例如您在视频游戏中可能看到的那样。 相反,它可以体现在诸如机器人或自动驾驶汽车之类的硬件中。 内在的智能体可能是充分理解和利用 RL 的最佳方法,因为物理实体与现实世界进行交互并接收响应。
该智能体位于环境中。 环境具有状态,可以部分或完全观察到。 该智能体具有一组操作,可用于与环境交互。 动作的结果将环境转换为新状态。 执行动作后,会收到相应的标量奖励。
智能体的目标是通过学习策略来最大化累积的未来奖励,该策略将决定在特定状态下应采取的行动。
RL 与人类心理学有很强的相似性。 人类通过体验世界来学习。 错误的行为会导致某种形式的惩罚,将来应避免使用,而正确的行为应得到奖励并应予以鼓励。 这种与人类心理学的强相似之处使许多研究人员相信 RL 可以将引向真正的人工智能(AI)。
RL 已经存在了几十年。 但是,除了简单的世界模型之外,RL 还在努力扩展规模。 这是,其中深度学习(DL)开始发挥作用。 它解决了这个可扩展性问题,从而开启了深度强化学习(DRL)的时代。 在本章中,我们的重点是 DRL。 DRL 中值得注意的例子之一是 DeepMind 在智能体上的工作,这些智能体能够在不同的视频游戏上超越最佳的人类表现。
在本章中,我们将讨论 RL 和 DRL。
总之,本章的目的是介绍:
- RL 的原理
- RL 技术,Q 学习
- 高级主题,包括深度 Q 网络(DQN)和双重 Q 学习(DDQN)
- 关于如何使用
tf.keras在 Python 和 DRL 上实现 RL 的说明
让我们从 RL 的基本原理开始。
1. 强化学习原理(RL)
“图 9.1.1”显示了用于描述 RL 的感知动作学习循环。 环境是苏打水可以坐在地板上。 智能体是一个移动机器人,其目标是拾取苏打水。 它观察周围的环境,并通过车载摄像头跟踪汽水罐的位置。 观察结果以一种状态的形式进行了汇总,机器人将使用该状态来决定要采取的动作。 所采取的动作可能与低级控制有关,例如每个车轮的旋转角度/速度,手臂的每个关节的旋转角度/速度以及抓手是打开还是关闭。
可替代地,动作可以是高级控制动作,诸如向前/向后移动机器人,以特定角度转向以及抓取/释放。 将夹持器从汽水中移开的任何动作都会得到负回报。 缩小抓取器位置和苏打之间的缝隙的任何动作都会获得积极的回报。 当机械臂成功捡起汽水罐时,它会收到丰厚的回报。 RL 的目标是学习最佳策略,该策略可帮助机器人决定在给定状态下采取哪种行动以最大化累积的折扣奖励:

图 9.1.1:RL 中的感知-动作-学习循环
形式上,RL 问题可以描述为 Markov 决策过程(MDP)。
为简单起见,我们将假定为确定性环境,在该环境中,给定状态下的某个动作将始终导致已知的下一个状态和奖励。 在本章的后面部分,我们将研究如何考虑随机性。 在时间步t时:
- 环境处于状态空间
S的状态下,状态s₀,该状态可以是离散的也可以是连续的。 起始状态为s₀,而终止状态为sₜ。 - 智能体通过遵循策略
π(a[t] | s[t])从操作空间A采取操作,即s[a]。A可以是离散的或连续的。 - 环境使用状态转换动态
T(s[t + 1] | s[t], a[t])转换为新状态,s[t + 1]。 下一个状态仅取决于当前状态和操作。 智能体不知道T。 - 智能体使用奖励函数接收标量奖励,
r[t + 1] = R(s[t], a[t]),以及r: A x S -> R。 奖励仅取决于当前状态和操作。 智能体不知道R。 - 将来的奖励折扣为
γ^k,其中γ ∈ [0, 1]和k是未来的时间步长。 - 地平线,
H是完成从s₀到sₜ的一集所需的时间步长T。
该环境可以是完全或部分可观察的。 后者也称为部分可观察的 MDP 或 POMDP。 在大多数情况下,完全观察环境是不现实的。 为了提高的可观察性,当前的观测值也考虑了过去的观测值。 状态包括对环境的足够观察,以使策略决定采取哪种措施。 回忆“图 9.1.1”,这可能是汽水罐相对于机器人抓手的三维位置,如机器人摄像头所估计的那样。
每当环境转换到新状态时,智能体都会收到标量奖励r[t + 1]。 在“图 9.1.1”中,每当机器人靠近汽水罐时,奖励可能为 +1;当机器人离汽水罐更远时,奖励为 -1;当机器人关闭夹具并成功捡起苏打时,奖励为 +100。 能够。 智能体的目标是学习一种最佳策略π*,该策略可使所有状态的收益最大化:
(Equation 9.1.1)
回报定义为折扣累积奖励R[t] = Σ γᵗ r[t+k], k = 0, ..., T。 从“公式 9.1.1”可以看出,与通常的γ^k < 1.0相比,与立即获得的奖励相比,未来的奖励权重较低。 在极端情况下,当γ = 0时,仅立即获得奖励很重要。 当γ = 1时,将来的奖励与立即奖励的权重相同。
遵循任意策略π,可以将回报解释为对给定状态值的度量:
(Equation 9.1.2)
换句话说,RL 问题是智能体的目标,是学习使所有状态s最大化的最优策略V^π:
(Equation 9.1.3)
最优策略的值函数就是V*。 在“图 9.1.1”中,最佳策略是生成最短动作序列的一种,该动作序列使机器人越来越靠近苏打罐,直到被取走为止。 状态越接近目标状态,其值越高。 可以将导致目标(或最终状态)的事件序列建模为策略的轨迹或部署:
(Equation 9.1.4)
如果 MDP 是偶发的,则当智能体到达终端状态sₜ时,状态将重置为s₀。 如果T是有限的,则我们的水平范围是有限的。 否则,视野是无限的。 在“图 9.1.1”中,如果 MDP 是情景剧集,则在收集苏打罐后,机器人可能会寻找另一个苏打罐来拾取,并且 RL 问题重发。
因此,RL 的主要目标是找到一种使每个状态的值最大化的策略。 在下一部分中,我们将介绍可用于最大化值函数的策略学习算法。
2. Q 值
如果 RL 问题是找到π*,则智能体如何通过与环境交互来学习?“公式 9.1.3”并未明确指出尝试进行的操作以及计算收益的后续状态。 在 RL 中,使用 Q 值更容易学习π*:
(Equation 9.2.1)
哪里:
(Equation 9.2.2)
换句话说,不是找到使所有状态的值最大化的策略,而是“公式 9.2.1”寻找使所有状态的质量(Q)值最大化的操作。 在找到 Q 值函数之后,分别由“公式 9.2.2”和“公式 9.1.3”确定V*,因此确定了π*。
如果对于每个动作,都可以观察到奖励和下一状态,则可以制定以下迭代或反复试验算法来学习 Q 值:
(Equation 9.2.3)
为了简化符号,s'和a'分别是下一个状态和动作。 “公式 9.2.3”被称为贝尔曼方程,它是 Q 学习算法的核心。 Q 学习尝试根据当前状态和作用来近似返回值或值的一阶展开(“公式 9.1.2”)。 从对环境动态的零知识中,智能体尝试执行操作a,观察以奖励r和下一个状态s'的形式发生的情况。 max[a'] Q(s', a')选择下一个逻辑动作,该动作将为下一个状态提供最大 Q 值。 有了“公式 9.2.3”中的所有项,该当前状态-动作对的 Q 值就会更新。 迭代地执行更新将最终使智能体能够学习 Q 值函数。
Q 学习是一种脱离策略 RL 算法。 它学习了如何通过不直接从策略中抽取经验来改进策略。 换句话说,Q 值的获取与智能体所使用的基础策略无关。 当 Q 值函数收敛时,才使用“公式 9.2.1”确定最佳策略。
在为提供有关如何使用 Q 学习的示例之前,请注意,智能体必须在不断利用其到目前为止所学知识的同时不断探索其环境。 这是 RL 中的问题之一-在探索和开发之间找到适当的平衡。 通常,在学习开始时,动作是随机的(探索)。 随着学习的进行,智能体会利用 Q 值(利用)。 例如,一开始,90% 的动作是随机的,而 10% 的动作则来自 Q 值函数。 在每个剧集的结尾,这逐渐减少。 最终,该动作是 10% 随机的,并且是 Q 值函数的 90%。
在下一节中,我们将给出有关在简单的确定性环境中如何使用 Q 学习的具体示例。
3. Q 学习实例
为了说明 Q 学习算法,我们需要考虑一个简单的确定性环境,如图“图 9.3.1”所示。 环境具有六个状态。
显示允许的过渡的奖励。 在两种情况下,奖励是非零的。 转换为目标(G)状态可获得 +100 的奖励,同时移至洞(H)状态具有 -100 奖励。 这两个状态是终端状态,从开始状态构成一个剧集的结尾:

图 9.3.1:简单确定性世界中的奖励
为了使每个状态的身份正式化,我们使用(行, 列)标识符,如图“图 9.3.2”所示。 由于智能体尚未了解有关其环境的任何信息,因此“图 9.3.2”中所示的 Q 表的初始值为零。 在此示例中,折扣因子γ = 0.9。 回想一下,在当前 Q 值的估计中,折扣因子确定了未来 Q 值的权重,该权重是步数γ^k的函数。 在“公式 9.2.3”中,我们仅考虑近期 Q 值k = 1。

图 9.3.2:简单确定性环境中的状态和智能体的初始 Q 表
最初,智能体采用的策略是 90% 的时间选择随机操作,并 10% 的时间使用 Q 表。 假设第一个动作是随机选择的,并且指示向右移动。“图 9.3.3”说明了向右移动时状态(0, 0)的新 Q 值的计算。 下一个状态是(0, 1)。 奖励为 0,所有下一个状态的 Q 值的最大值为零。 因此,向右移动的状态(0, 0)的 Q 值保持为 0。
为了轻松跟踪初始状态和下一个状态,我们在环境和 Q 表上使用不同的灰色阴影-初始状态浅灰色,下一个状态灰色。
在为下一个状态选择下一个动作时,候选动作位于较粗的边框中:

图 9.3.3:假设智能体采取的行动是向右移动,则显示状态(0, 0)的 Q 值的更新
假设下一个随机选择的动作是向下移动。“图 9.3.4”显示状态(0, 1)的 Q 值沿向下方向的移动没有变化:

图 9.3.4:假设智能体选择的动作是向下移动,则显示状态(0, 1)的 Q 值的更新
在“图 9.3.5”中,智能体的第三个随机动作是向右移动。

图 9.3.5:假设智能体选择的动作是向右移动,则显示状态(1, 1)的 Q 值的更新
它遇到了,H状态,并获得了 -100 奖励。 这次,更新不为零。 向右移动时,状态(1, 1)的新 Q 值为 -100。 注意,由于这是终端状态,因此没有下一个状态。 一集刚刚结束,智能体返回到开始状态。
假设智能体仍处于探索模式,如图“图 9.3.6”所示:

图 9.3.6:假设智能体选择的动作是向右连续两次移动,则显示状态(0, 1)的 Q 值的更新
为第二集采取的第一步是向右移动。 正如预期的那样,更新为 0。但是,它选择的第二个随机动作也是向右移动。 智能体到达G状态并获得 +100 的巨额奖励。 向右移动的状态(0, 1)的 Q 值变为 100。完成第二集,并且智能体返回到启动状态。
在第三集开始时,智能体采取的随机行动是向右移动。 现在,状态(0, 0)的 Q 值将更新为非零值,因为下一个状态的可能动作将最大 Q 值设为 100。“图 9.3.7”显示了所涉及的计算。 下一个状态(0, 1)的 Q 值波动回到较早的状态(0, 0)。 这就像对帮助找到G状态的早期状态表示赞赏。

图 9.3.7:假设智能体选择的动作是向右移动,则显示状态(0, 0)的 Q 值的更新
Q 表的进步很大。 实际上,在下一集中,如果由于某种原因该策略决定使用 Q 表而不是随机探索环境,则第一个动作是根据“图 9.3.8”中的计算向右移动。 在 Q 表的第一行中,导致最大 Q 值的动作是向右移动。 对于下一个状态(0, 1),Q 表的第二行表明下一个动作仍然是向右移动。 智能体已成功实现其目标。 该策略指导智能体采取了正确的措施来实现其目标:

图 9.3.8:在这种情况下,智能体的策略决定利用 Q 表来确定状态(0, 0)和(0, 1)的动作。 Q 表建议两个状态都向右移动
如果 Q 学习算法继续无限期运行,则 Q 表将收敛。 收敛的假设是 RL 问题必须是具有有限奖励的确定性 MDP,并且所有状态都将被无限次地访问。
在下一节中,我们将使用 Python 模拟环境。 我们还将展示 Q 学习算法的代码实现。
用 Python 进行 Q 学习
上一节中讨论的环境和 Q 学习可以在 Python 中实现。 由于该策略只是一个简单的表,因此在此时,无需使用tf.keras库。“列表 9.3.1”显示了q-learning-9.3.1.py,它是使用QWorld类实现的简单确定性世界(环境,智能体,操作和 Q 表算法)的实现。 为简洁起见,未显示处理用户界面的函数。
在此示例中,环境动态由self.transition_table表示。 在每个动作中,self.transition_table确定下一个状态。 执行动作的奖励存储在self.reward_table中。 每次通过step()函数执行动作时,都要查阅这两个表。 Q 学习算法由update_q_table()函数实现。 每当智能体需要决定要采取的操作时,它都会调用act()函数。 策略可以使用 Q 表随机抽取或决定。 所选动作是随机的机会百分比存储在self.epsilon变量中,该变量由update_epsilon()函数使用固定的epsilon_decay更新。
在执行“列表 9.3.1”中的代码之前,我们需要运行:
sudo pip3 install termcolor
安装termcolor包。 该包有助于可视化终端上的文本输出。
“列表 9.3.1”:q-learning-9.3.1.py
具有六个状态的简单确定性 MDP:
from collections import deque
import numpy as np
import argparse
import os
import time
from termcolor import colored
class QWorld:
def __init__(self):
"""Simulated deterministic world made of 6 states.
Q-Learning by Bellman Equation.
"""
# 4 actions
# 0 - Left, 1 - Down, 2 - Right, 3 - Up
self.col = 4
# 6 states
self.row = 6
# setup the environment
self.q_table = np.zeros([self.row, self.col])
self.init_transition_table()
self.init_reward_table()
# discount factor
self.gamma = 0.9
# 90% exploration, 10% exploitation
self.epsilon = 0.9
# exploration decays by this factor every episode
self.epsilon_decay = 0.9
# in the long run, 10% exploration, 90% exploitation
self.epsilon_min = 0.1
# reset the environment
self.reset()
self.is_explore = True
def reset(self):
"""start of episode"""
self.state = 0
return self.state
def is_in_win_state(self):
"""agent wins when the goal is reached"""
return self.state == 2
def init_reward_table(self):
"""
0 - Left, 1 - Down, 2 - Right, 3 - Up
----------------
| 0 | 0 | 100 |
----------------
| 0 | 0 | -100 |
----------------
"""
self.reward_table = np.zeros([self.row, self.col])
self.reward_table[1, 2] = 100.
self.reward_table[4, 2] = -100.
def init_transition_table(self):
"""
0 - Left, 1 - Down, 2 - Right, 3 - Up
-------------
| 0 | 1 | 2 |
-------------
| 3 | 4 | 5 |
-------------
"""
self.transition_table = np.zeros([self.row, self.col],
dtype=int)
self.transition_table[0, 0] = 0
self.transition_table[0, 1] = 3
self.transition_table[0, 2] = 1
self.transition_table[0, 3] = 0
self.transition_table[1, 0] = 0
self.transition_table[1, 1] = 4
self.transition_table[1, 2] = 2
self.transition_table[1, 3] = 1
# terminal Goal state
self.transition_table[2, 0] = 2
self.transition_table[2, 1] = 2
self.transition_table[2, 2] = 2
self.transition_table[2, 3] = 2
self.transition_table[3, 0] = 3
self.transition_table[3, 1] = 3
self.transition_table[3, 2] = 4
self.transition_table[3, 3] = 0
self.transition_table[4, 0] = 3
self.transition_table[4, 1] = 4
self.transition_table[4, 2] = 5
self.transition_table[4, 3] = 1
# terminal Hole state
self.transition_table[5, 0] = 5
self.transition_table[5, 1] = 5
self.transition_table[5, 2] = 5
self.transition_table[5, 3] = 5
def step(self, action):
"""execute the action on the environment
Argument:
action (tensor): An action in Action space
Returns:
next_state (tensor): next env state
reward (float): reward received by the agent
done (Bool): whether the terminal state
is reached
"""
# determine the next_state given state and action
next_state = self.transition_table[self.state, action]
# done is True if next_state is Goal or Hole
done = next_state == 2 or next_state == 5
# reward given the state and action
reward = self.reward_table[self.state, action]
# the enviroment is now in new state
self.state = next_state
return next_state, reward, done
def act(self):
"""determine the next action
either fr Q Table(exploitation) or
random(exploration)
Return:
action (tensor): action that the agent
must execute
"""
# 0 - Left, 1 - Down, 2 - Right, 3 - Up
# action is from exploration
if np.random.rand() <= self.epsilon:
# explore - do random action
self.is_explore = True
return np.random.choice(4,1)[0]
# or action is from exploitation
# exploit - choose action with max Q-value
self.is_explore = False
action = np.argmax(self.q_table[self.state])
return action
def update_q_table(self, state, action, reward, next_state):
"""Q-Learning - update the Q Table using Q(s, a)
Arguments:
state (tensor) : agent state
action (tensor): action executed by the agent
reward (float): reward after executing action
for a given state
next_state (tensor): next state after executing
action for a given state
"""
# Q(s, a) = reward + gamma * max_a' Q(s', a')
q_value = self.gamma * np.amax(self.q_table[next_state])
q_value += reward
self.q_table[state, action] = q_value
def update_epsilon(self):
"""update Exploration-Exploitation mix"""
if self.epsilon > self.epsilon_min:
self.epsilon *= self.epsilon_decay
感知动作学习循环在“列表 9.3.2”中进行了说明。 在每个剧集中,环境都会重置为开始状态。 选择要执行的动作并将其应用于环境。 观察奖励和下一个状态,并将其用于更新 Q 表。 达到目标或洞状态后,剧集完成(done = True)。
对于此示例,Q 学习运行 100 集或 10 获胜,以先到者为准。 由于在每个剧集中变量的值均降低,因此智能体开始倾向于利用 Q 表来确定在给定状态下要执行的动作。 要查看 Q 学习模拟,我们只需要运行以下命令:
python3 q-learning-9.3.1.py
“列表 9.3.2”:q-learning-9.3.1.py
主要的 Q 学习循环:
# state, action, reward, next state iteration
for episode in range(episode_count):
state = q_world.reset()
done = False
print_episode(episode, delay=delay)
while not done:
action = q_world.act()
next_state, reward, done = q_world.step(action)
q_world.update_q_table(state, action, reward, next_state)
print_status(q_world, done, step, delay=delay)
state = next_state
# if episode is done, perform housekeeping
if done:
if q_world.is_in_win_state():
wins += 1
scores.append(step)
if wins > maxwins:
print(scores)
exit(0)
# Exploration-Exploitation is updated every episode
q_world.update_epsilon()
step = 1
else:
step += 1
“图 9.3.9”显示了maxwins = 2000(达到2000 x目标状态)和delay = 0时的屏幕截图。 要仅查看最终的 Q 表,请执行:
python3 q-learning-9.3.1.py --train

图 9.3.9:屏幕快照显示智能体在 2,000 次获胜后的 Q 表
Q 表已收敛,并显示了智能体可以在给定状态下采取的逻辑操作。 例如,在第一行或状态(0, 0)中,该策略建议向右移动。 第二行的状态(0, 1)也是如此。 第二个动作达到目标状态。 scores变量转储显示,随着智能体从策略获取正确的操作,所采取的最少步骤数减少了。
从“图 9.3.9”,我们可以从“公式 9.2.2”和V*(s) = max[a] Q(s, a)计算每个状态的值。 例如,对于状态(0, 0),V*(s) = max[a](0.0, 72.9, 90.0, 81.0) = 9.0。
“图 9.3.10”显示每种状态的值。

图 9.3.10:图 9.3.9 和公式 9.2.2 中每个状态的值
这个简单的示例说明了在简单确定性世界中智能体的 Q 学习的所有元素。 在下一节中,我们将介绍考虑随机性所需的轻微修改。
4. 非确定性环境
如果环境不确定,则奖励和行动都是概率性的。 新系统是随机的 MDP。 为了反映不确定性报酬,新的值函数为:
(Equation 9.4.1)
贝尔曼方程修改为:
(Equation 9.4.2)
但是,在本章中,我们将重点介绍确定性环境。 在下一节中,我们将提出一种更通用的 Q 学习算法,称为时差(TD)学习。
5. 时差学习
Q 学习是更广义的 TD 学习TD(λ)的特例。 更具体地说,这是单步 TD 学习的特殊情况,TD(0):
(Equation 9.5.1)
其中α是学习率。 注意,当α = 1,“公式 9.5.1”与贝尔曼等式相似。 为简单起见,我们还将“公式 9.5.1”称为 Q 学习或广义 Q 学习。
以前,我们将 Q 学习称为一种非策略性 RL 算法,因为它学习 Q 值函数而没有直接使用它尝试优化的策略。 上策略一步式 TD 学习算法的示例是 SARSA,类似于“公式 9.5.1”:
(Equation 9.5.2)
主要区别是使用已优化的策略来确定a'。 必须知道项s,a,r,s'和a'(因此名称为 SARSA)才能在每次迭代时更新 Q 值函数。 Q 学习和 SARSA 都在 Q 值迭代中使用现有的估计,该过程称为自举。 在引导过程中,我们从奖励中更新当前的 Q 值估计,并随后更新 Q 值估计。
在提出另一个示例之前,似乎需要合适的 RL 模拟环境。 否则,我们只能对非常简单的问题(如上一个示例)运行 RL 模拟。 幸运的是,OpenAI 创建了 Gym,我们将在下一节中介绍。
在 OpenAI Gym 上进行 Q 学习
OpenAI Gym 是的工具包,用于开发和比较 RL 算法。 它适用于大多数 DL 库,包括tf.keras。 可以通过运行以下命令来安装健身房:
sudo pip3 install gym
该体育馆有多种可以测试 RL 算法的环境,例如玩具文字,经典控件,算法,Atari 和二维/三维机器人。 例如,FrozenLake-v0(“图 9.5.1”)是一个玩具文本环境,类似于在 Python Q 学习示例中使用的简单确定性世界:

图 9.5.1:OpenAI Gym 中的 FrozenLake-v0 环境
FrozenLake-v0具有 12 个状态,标记为S的状态为起始状态,F的状态为湖泊的冰冻部分,这是安全的,H为安全状态。 应当避免的空穴状态,G是飞盘所在的目标状态。 转换为目标状态的奖励为 +1。 对于所有其他状态,奖励为零。
在FrozenLake-v0中,还有四个可用动作(左,下,右,上),称为动作空间。 但是,与之前的简单确定性世界不同,实际运动方向仅部分取决于所选的动作。 FrozenLake-v0环境有两种变体。 滑和不滑。 不出所料,滑动模式更具挑战性。
应用于FrozenLake-v0的操作将返回观察结果(等效于下一个状态),奖励,完成(无论剧集是否完成)以及调试信息字典。 返回的观察对象捕获环境的可观察属性,称为观察空间。
通用 Q 学习可以应用于FrozenLake-v0环境。“表 9.5.1”显示了湿滑和非湿滑环境的表现改进。 衡量策略表现的一种方法是执行的事件达到目标状态的百分比。 百分比越高,效果越好。 从大约 1.5% 的纯探查(随机操作)的基准来看,该策略可以在非光滑环境中达到约 76% 的目标状态,在光滑环境中可以达到约 71% 的目标状态。 不出所料,很难控制湿滑的环境。
| 模式 | 运行 | 大约百分比的目标 |
|---|---|---|
| 训练非滑动 | python3 q-frozenlake-9.5.1.py |
26 |
| 测试非滑动 | python3 q-frozenlake-9.5.1.py -d |
76 |
| 纯随机动作非滑动 | python3 q-frozenlake-9.5.1.py -e |
1.5 |
| 训练滑动 | python3 q-frozenlake-9.5.1.py -s |
26 |
| 测试滑动 | python3 q-frozenlake-9.5.1.py -s -d |
71 |
| 纯随机动作滑动 | python3 q-frozenlake-9.5.1.py -s -e |
1.5 |
表 9.5.1:在 FrozenLake-v0 环境中学习率为 0.5 的广义 Q 学习的基线和表现
由于该代码仅需要一个 Q 表,因此仍可以在 Python 和 NumPy 中实现。“列表 9.5.1”显示了QAgent类的实现。 除了使用 OpenAI Gym 的FrozenLake-v0环境之外,最重要的更改是广义 Q 学习的实现,这由update_q_table()函数中的“公式 9.5.1”定义。
“列表 9.5.1”:q-frozenlake-9.5.1.py
关于 FrozenLake-v0 环境的 Q 学习:
from collections import deque
import numpy as np
import argparse
import os
import time
import gym
from gym import wrappers, logger
class QAgent:
def __init__(self,
observation_space,
action_space,
demo=False,
slippery=False,
episodes=40000):
"""Q-Learning agent on FrozenLake-v0 environment
Arguments:
observation_space (tensor): state space
action_space (tensor): action space
demo (Bool): whether for demo or training
slippery (Bool): 2 versions of FLv0 env
episodes (int): number of episodes to train
"""
self.action_space = action_space
# number of columns is equal to number of actions
col = action_space.n
# number of rows is equal to number of states
row = observation_space.n
# build Q Table with row x col dims
self.q_table = np.zeros([row, col])
# discount factor
self.gamma = 0.9
# initially 90% exploration, 10% exploitation
self.epsilon = 0.9
# iteratively applying decay til
# 10% exploration/90% exploitation
self.epsilon_min = 0.1
self.epsilon_decay = self.epsilon_min / self.epsilon
self.epsilon_decay = self.epsilon_decay ** \
(1\. / float(episodes))
# learning rate of Q-Learning
self.learning_rate = 0.1
# file where Q Table is saved on/restored fr
if slippery:
self.filename = 'q-frozenlake-slippery.npy'
else:
self.filename = 'q-frozenlake.npy'
# demo or train mode
self.demo = demo
# if demo mode, no exploration
if demo:
self.epsilon = 0
def act(self, state, is_explore=False):
"""determine the next action
if random, choose from random action space
else use the Q Table
Arguments:
state (tensor): agent's current state
is_explore (Bool): exploration mode or not
Return:
action (tensor): action that the agent
must execute
"""
# 0 - left, 1 - Down, 2 - Right, 3 - Up
if is_explore or np.random.rand() < self.epsilon:
# explore - do random action
return self.action_space.sample()
# exploit - choose action with max Q-value
action = np.argmax(self.q_table[state])
return action
def update_q_table(self, state, action, reward, next_state):
"""TD(0) learning (generalized Q-Learning) with learning rate
Arguments:
state (tensor): environment state
action (tensor): action executed by the agent for
the given state
reward (float): reward received by the agent for
executing the action
next_state (tensor): the environment next state
"""
# Q(s, a) +=
# alpha * (reward + gamma * max_a' Q(s', a') - Q(s, a))
q_value = self.gamma * np.amax(self.q_table[next_state])
q_value += reward
q_value -= self.q_table[state, action]
q_value *= self.learning_rate
q_value += self.q_table[state, action]
self.q_table[state, action] = q_value
def update_epsilon(self):
"""adjust epsilon"""
if self.epsilon > self.epsilon_min:
self.epsilon *= self.epsilon_decay
“列表 9.5.2”演示了智能体的感知行为学习循环。 在每个剧集中,通过调用env.reset()重置环境。 要执行的动作由agent.act()选择,并由env.step(action)应用于环境。 奖励和下一个状态将被观察并用于更新 Q 表。
在每个动作之后,通过agent.update_q_table()执行 TD 学习。 由于每次调用agent.update_epsilon()时处self.epsilon变量的值都会减少,该智能体开始支持利用 Q 表来确定在给定状态下执行的操作。 达到目标或空洞状态后,剧集完成(done = True)。 对于此示例,TD 学习运行 4,000 集。
“列表 9.5.2”:q-frozenlake-9.5.1.py。
FrozenLake-v0环境的 Q 学习循环:
# loop for the specified number of episode
for episode in range(episodes):
state = env.reset()
done = False
while not done:
# determine the agent's action given state
action = agent.act(state, is_explore=args.explore)
# get observable data
next_state, reward, done, _ = env.step(action)
# clear the screen before rendering the environment
os.system('clear')
# render the environment for human debugging
env.render()
# training of Q Table
if done:
# update exploration-exploitation ratio
# reward > 0 only when Goal is reached
# otherwise, it is a Hole
if reward > 0:
wins += 1
if not args.demo:
agent.update_q_table(state,
action,
reward,
next_state)
agent.update_epsilon()
state = next_state
percent_wins = 100.0 * wins / (episode + 1)
agent对象可以在湿滑或非湿滑模式下运行。 训练后,智能体可以利用 Q 表选择给定任何策略执行的操作,如“表 9.5.1”的测试模式所示。 如“表 9.5.1”所示,使用学习的策略可显着提高性能。 随着体育馆的使用,不再需要中构建环境的许多代码行。 例如,与上一个示例不同,使用 OpenAI Gym,我们不需要创建状态转换表和奖励表。
这将帮助我们专注于构建有效的 RL 算法。 要以慢动作方式运行代码或每个动作延迟 1 秒,请执行以下操作:
python3 q-frozenlake-9.5.1.py -d -t=1
在本节中,我们在更具挑战性的环境中演示了 Q 学习。 我们还介绍了 OpenAI 体育馆。 但是,我们的环境仍然是玩具环境。 如果我们有大量的状态或动作怎么办? 在这种情况下,使用 Q 表不再可行。 在下一节中,我们将使用深度神经网络来学习 Q 表。
6. 深度 Q 网络(DQN)
在小型离散环境中,使用 Q 表执行 Q 学习是很好的选择。 但是,在大多数情况下,当环境具有许多状态或连续时,Q 表是不可行或不实际的。 例如,如果我们观察由四个连续变量组成的状态,则表的大小是无限的。 即使我们尝试将这四个变量离散化为 1,000 个值,表中的总行数也达到了惊人的1000^4 = 1e12。 即使经过训练,该表仍是稀疏的–该表中的大多数单元都是零。
这个问题的解决方案称为 DQN [2],它使用深度神经网络来近似 Q 表,如图“图 9.6.1”所示。 有两种构建 Q 网络的方法:
- 输入是状态-动作对,预测是 Q 值
- 输入是状态,预测是每个动作的 Q 值
第一种选择不是最佳的,因为网络被调用的次数等于操作数。 第二种是首选方法。 Q 网络仅被调用一次。
最希望得到的作用就是 Q 值最大的作用。

图 9.6.1:深度 Q 网络
训练 Q 网络所需的数据来自智能体的经验:(s[0]a[0]r[1]s[1], s[1]a[1]r[2]s[2],d ..., s[T-1]a[T-1]r[T]s[T])。 每个训练样本都是经验单元s[t]a[t]r[t+1]s[t+1]。 在时间步t,s = s[t]的给定状态下,使用类似于前一部分的 Q 学习算法来确定动作a = a[t]:
(Equation 9.6.1)
为了简化符号,我们省略了下标和粗体字母的使用。 注意,Q(s, a)是 Q 网络。 严格来说,它是Q(a | s),因为动作已移至预测阶段(换句话说,是输出),如“图 9.6.1”的右侧所示。 Q 值最高的动作是应用于环境以获得奖励r = r[t+1],下一状态s' = s[t+1]和布尔值done的动作,指示下一个状态是否为终端 。 根据关于广义 Q 学习的“公式 9.5.1”,可以通过应用所选的操作来确定 MSE 损失函数:
(Equation 9.6.2)
在前面有关 Q 学习和Q(a | s) -> Q(s, a)的讨论中,所有项都很熟悉。 项max[a'] Q(a' | s') -> max[a'] Q(s', a')。 换句话说,使用 Q 网络,在给定下一个状态的情况下预测每个动作的 Q 值,并从其中获得最大值。 注意,在终端状态下,s',max[a'] Q(a' | s') -> max[a'] Q(s', a') = 0。
但是,事实证明训练 Q 网络是不稳定的。 导致不稳定的问题有两个:1)样本之间的相关性高; 2)非平稳目标。 高度相关性是由于采样经验的顺序性质。 DQN 通过创建经验缓冲解决了问题。 训练数据是从该缓冲区中随机采样的。 此过程称为经验回放。
非固定目标的问题是由于目标网络Q(s', a')在每小批训练后都会被修改。 目标网络的微小变化会导致策略,数据分布以及当前 Q 值和目标 Q 值之间的相关性发生重大变化。 这可以通过冻结C训练步骤的目标网络的权重来解决。 换句话说,创建了两个相同的 Q 网络。 在每个C训练步骤中,从训练中的 Q 网络复制目标 Q 网络参数。
“算法 9.6.1”中概述了深度 Q 网络算法。
“算法 9.6.1”: DQN 算法
要求:将重播内存D初始化为容量N
要求:使用随机权重θ初始化动作值函数Q
要求:使用权重θ- = 0初始化目标操作值函数Q_target
需要:探索率ε和折扣系数γ
-
对于
episode = 1, ..., M,执行: -
给定初始状态
s -
对于
step = 1, ..., T,执行: -
选择动作
![]()
-
执行动作
a,观察奖励r,以及下一个状态s' -
将转换
(s, a, r, s')存储在D中 -
更新状态
s = s' -
经验回放
-
从
D中抽样一小部分经验(s[j], a[j], r[j+1], s[j+1]) -
![]()
-
在
(Q_max - Q(s[j], a[j]; θ))²上相对于参数θ执行梯度下降步骤。 -
定期更新目标网络
-
每
C个步骤,即Q_target = Q,换句话说,设置θ- = θ -
end -
end
“算法 9.6.1”总结了在具有离散动作空间和连续状态空间的环境上实现 Q 学习所需的所有技术。 在下一节中,我们将演示如何在更具挑战性的 OpenAI Gym 环境中使用 DQN。
Keras 中的 DQN
为了说明 DQN,使用了 OpenAI Gym 的CartPole-v0环境。 CartPole-v0是极点平衡问题。 目的是防止电杆跌落。 环境是二维的。 动作空间由两个离散的动作(左右移动)组成。 但是,状态空间是连续的,并且包含四个变量:
- 直线位置
- 线速度
- 旋转角度
- 角速度
CartPole-v0环境如图 9.6.1 所示:

图 9.6.1:CartPole-v0 环境
最初,杆是直立的。 杆保持直立的每个时间步长都提供 +1 的奖励。 当极点与垂直方向的夹角超过 15 度或与中心的距离超过 2.4 单位时,剧集结束。 如果在 100 个连续试验中平均奖励为 195.0,则认为CartPole-v0问题已解决:
“列表 9.6.1”向我们展示了CartPole-v0的 DQN 实现。 DQNAgent类表示使用 DQN 的智能体。 创建了两个 Q 网络:
- “算法 9.6.1”中的 Q 网络或 Q
- “算法 9.6.1”中的目标 Q 网络或
Q_target
两个网络都是 MLP,每个都有 256 个单元的 3 个隐藏层。 这两个网络都是通过build_model()方法创建的。 在经验回放,replay()期间训练 Q 网络。 以update_weights()的固定间隔C = 10个训练步骤,将 Q 网络参数复制到目标 Q 网络。 在“算法 9.6.1”中,这实现了第 13 行,Q_target = Q。 每次发作后,update_epsilon()都会降低探索利用的比例,以利用已学习的策略。
“列表 9.6.1”:dqn-cartpole-9.6.1.py
tf.keras中的 DQN:
class DQNAgent:
def __init__(self,
state_space,
action_space,
episodes=500):
"""DQN Agent on CartPole-v0 environment
Arguments:
state_space (tensor): state space
action_space (tensor): action space
episodes (int): number of episodes to train
"""
self.action_space = action_space
# experience buffer
self.memory = []
# discount rate
self.gamma = 0.9
# initially 90% exploration, 10% exploitation
self.epsilon = 1.0
# iteratively applying decay til
# 10% exploration/90% exploitation
self.epsilon_min = 0.1
self.epsilon_decay = self.epsilon_min / self.epsilon
self.epsilon_decay = self.epsilon_decay ** \
(1\. / float(episodes))
# Q Network weights filename
self.weights_file = 'dqn_cartpole.h5'
# Q Network for training
n_inputs = state_space.shape[0]
n_outputs = action_space.n
self.q_model = self.build_model(n_inputs, n_outputs)
self.q_model.compile(loss='mse', optimizer=Adam())
# target Q Network
self.target_q_model = self.build_model(n_inputs, n_outputs)
# copy Q Network params to target Q Network
self.update_weights()
self.replay_counter = 0
self.ddqn = True if args.ddqn else False
def build_model(self, n_inputs, n_outputs):
"""Q Network is 256-256-256 MLP
Arguments:
n_inputs (int): input dim
n_outputs (int): output dim
Return:
q_model (Model): DQN
"""
inputs = Input(shape=(n_inputs, ), name='state')
x = Dense(256, activation='relu')(inputs)
x = Dense(256, activation='relu')(x)
x = Dense(256, activation='relu')(x)
x = Dense(n_outputs,
activation='linear',
name='action')(x)
q_model = Model(inputs, x)
q_model.summary()
return q_model
def act(self, state):
"""eps-greedy policy
Return:
action (tensor): action to execute
"""
if np.random.rand() < self.epsilon:
# explore - do random action
return self.action_space.sample()
# exploit
q_values = self.q_model.predict(state)
# select the action with max Q-value
action = np.argmax(q_values[0])
return action
def remember(self, state, action, reward, next_state, done):
"""store experiences in the replay buffer
Arguments:
state (tensor): env state
action (tensor): agent action
reward (float): reward received after executing
action on state
next_state (tensor): next state
"""
item = (state, action, reward, next_state, done)
self.memory.append(item)
def get_target_q_value(self, next_state, reward):
"""compute Q_max
Use of target Q Network solves the
non-stationarity problem
Arguments:
reward (float): reward received after executing
action on state
next_state (tensor): next state
Return:
q_value (float): max Q-value computed by
DQN or DDQN
"""
# max Q value among next state's actions
if self.ddqn:
# DDQN
# current Q Network selects the action
# a'_max = argmax_a' Q(s', a')
action = np.argmax(self.q_model.predict(next_state)[0])
# target Q Network evaluates the action
# Q_max = Q_target(s', a'_max)
q_value = self.target_q_model.predict(\
next_state)[0][action]
else:
# DQN chooses the max Q value among next actions
# selection and evaluation of action is
# on the target Q Network
# Q_max = max_a' Q_target(s', a')
q_value = np.amax(\
self.target_q_model.predict(next_state)[0])
# Q_max = reward + gamma * Q_max
q_value *= self.gamma
q_value += reward
return q_value
def replay(self, batch_size):
"""experience replay addresses the correlation issue
between samples
Arguments:
batch_size (int): replay buffer batch
sample size
"""
# sars = state, action, reward, state' (next_state)
sars_batch = random.sample(self.memory, batch_size)
state_batch, q_values_batch = [], []
# fixme: for speedup, this could be done on the tensor level
# but easier to understand using a loop
for state, action, reward, next_state, done in sars_batch:
# policy prediction for a given state
q_values = self.q_model.predict(state)
# get Q_max
q_value = self.get_target_q_value(next_state, reward)
# correction on the Q value for the action used
q_values[0][action] = reward if done else q_value
# collect batch state-q_value mapping
state_batch.append(state[0])
q_values_batch.append(q_values[0])
# train the Q-network
self.q_model.fit(np.array(state_batch),
np.array(q_values_batch),
batch_size=batch_size,
epochs=1,
verbose=0)
# update exploration-exploitation probability
self.update_epsilon()
# copy new params on old target after
# every 10 training updates
if self.replay_counter % 10 == 0:
self.update_weights()
self.replay_counter += 1
def update_epsilon(self):
"""decrease the exploration, increase exploitation"""
if self.epsilon > self.epsilon_min:
self.epsilon *= self.epsilon_decay
为了在“算法 9.6.1”经验回放replay()中实现第 10 行,对于每个体验单元(sⱼ,aⱼ,r[j + 1]和s[j + 1])将动作aⱼ的 Q 值设置为Q_max。 所有其他动作的 Q 值保持不变。
这是通过 DQNAgent replay()函数中的以下行实现的:
# policy prediction for a given state q_values = self.q_model.predict(state)
# get Q_max
q_value = self.get_target_q_value(next_state)
# correction on the Q value for the action used q_values[0][action] = reward if done else q_value
如“算法 9.6.1”的第 11 行所示,只有动作aⱼ具有等于(Q_max - Q(s[j], a[j]; θ))²的非零损失。 请注意,假设缓冲区中有足够的数据,换句话说,在每个剧集结束后,“列表 9.6.2”中的感知动作学习循环会调用经验回放。 缓冲区的大小大于或等于批量大小)。 在经验回放期间,会随机采样一批体验单元,并将其用于训练 Q 网络。
与 Q 表类似,act()实现了 ε-贪婪策略,“公式 9.6.1”。
体验由remember()存储在重播缓冲区中。 Q 通过get_target_q_value()函数计算。
“列表 9.6.2”总结了智能体的感知-行动-学习循环。 在每个剧集中,通过调用env.reset()重置环境。 要执行的动作由agent.act()选择,并由env.step(action)应用于环境。 奖励和下一状态将被观察并存储在重播缓冲区中。 在执行每个操作之后,智能体会调用replay()来训练 DQN 并调整探索利用比率。
当极点与垂直方向的夹角超过 15 度或与中心的距离超过 2.4 单位时,剧集完成(done = True)。 对于此示例,如果 DQN 智能体无法解决问题,则 Q 学习最多运行 3,000 集。 如果average mean_score奖励在 100 次连续试验win_trials中为 195.0,则认为CartPole-v0问题已解决。
“列表 9.6.2”:dqn-cartpole-9.6.1.py
tf.keras中的 DQN 训练循环:
# Q-Learning sampling and fitting
for episode in range(episode_count):
state = env.reset()
state = np.reshape(state, [1, state_size])
done = False
total_reward = 0
while not done:
# in CartPole-v0, action=0 is left and action=1 is right
action = agent.act(state)
next_state, reward, done, _ = env.step(action)
# in CartPole-v0:
# state = [pos, vel, theta, angular speed]
next_state = np.reshape(next_state, [1, state_size])
# store every experience unit in replay buffer
agent.remember(state, action, reward, next_state, done)
state = next_state
total_reward += reward
# call experience relay
if len(agent.memory) >= batch_size:
agent.replay(batch_size)
scores.append(total_reward)
mean_score = np.mean(scores)
if mean_score >= win_reward[args.env_id] \
and episode >= win_trials:
print("Solved in episode %d: \
Mean survival = %0.2lf in %d episodes"
% (episode, mean_score, win_trials))
print("Epsilon: ", agent.epsilon)
agent.save_weights()
break
if (episode + 1) % win_trials == 0:
print("Episode %d: Mean survival = \
%0.2lf in %d episodes" %
((episode + 1), mean_score, win_trials))
在平均 10 次运行的中,DQN 在 822 集内解决了。 我们需要注意的是,每次训练运行的结果可能会有所不同。
自从引入 DQN 以来,连续的论文都提出了对“算法 9.6.1”的改进。 一个很好的例子是双 DQN(DDQN),下面将对其进行讨论。
双重 Q 学习(DDQN)
在 DQN 中,目标 Q 网络选择并评估每个动作,从而导致 Q 值过高。 为了解决这个问题,DDQN [3]建议使用 Q 网络选择动作,并使用目标 Q 网络评估动作。
在 DQN 中,如“算法 9.6.1”所概述,第 10 行中 Q 值的估计为:

Q_target选择并评估动作,a[j+1]。
DDQN 建议将第 10 行更改为:

项argmax[a[j+1]] Q(s[j+1], a[j+1]; θ)使 Q 函数可以选择动作。 然后,该动作由Q_target评估。
“列表 9.6.3”显示了当我们创建一个新的DDQNAgent类时,该类继承自DQNAgent类。 只有get_target_q_value()方法被覆盖,以实现最大 Q 值计算中的更改。
“列表 9.6.3”:dqn-cartpole-9.6.1.py:
class DDQNAgent(DQNAgent):
def __init__(self,
state_space,
action_space,
episodes=500):
super().__init__(state_space,
action_space,
episodes)
"""DDQN Agent on CartPole-v0 environment
Arguments:
state_space (tensor): state space
action_space (tensor): action space
episodes (int): number of episodes to train
"""
# Q Network weights filename
self.weights_file = 'ddqn_cartpole.h5'
def get_target_q_value(self, next_state, reward):
"""compute Q_max
Use of target Q Network solves the
non-stationarity problem
Arguments:
reward (float): reward received after executing
action on state
next_state (tensor): next state
Returns:
q_value (float): max Q-value computed
"""
# max Q value among next state's actions
# DDQN
# current Q Network selects the action
# a'_max = argmax_a' Q(s', a')
action = np.argmax(self.q_model.predict(next_state)[0])
# target Q Network evaluates the action
# Q_max = Q_target(s', a'_max)
q_value = self.target_q_model.predict(\
next_state)[0][action]
# Q_max = reward + gamma * Q_max
q_value *= self.gamma
q_value += reward
return q_value
为了进行比较,在平均 10 次运行中,CartPole-v0由 DDQN 在 971 个剧集中求解。 要使用 DDQN,请运行以下命令:
python3 dqn-cartpole-9.6.1.py -d
DQN 和 DDQN 均表明,借助 DL,Q 学习能够扩展并解决具有连续状态空间和离散动作空间的问题。 在本章中,我们仅在具有连续状态空间和离散动作空间的最简单问题之一上演示了 DQN。 在原始论文中,DQN [2]证明了它可以在许多 Atari 游戏中达到超人的表现水平。
7. 总结
在本章中,我们已经介绍了 DRL,DRL 是一种强大的技术,许多研究人员认为它是 AI 的最有希望的领先者。 我们已经超越了 RL 的原则。 RL 能够解决许多玩具问题,但是 Q 表无法扩展到更复杂的现实问题。 解决方案是使用深度神经网络学习 Q 表。 但是,由于样本相关性和目标 Q 网络的非平稳性,在 RL 上训练深度神经网络非常不稳定。
DQN 提出了一种使用经验回放并将目标网络与受训 Q 网络分离的解决方案。 DDQN 建议通过将动作选择与动作评估分开来最大程度地降低 Q 值,从而进一步改进算法。 DQN 还提出了其他改进建议。 优先经验回放[6]认为,不应对体验缓冲区进行统一采样。
取而代之的是,应更频繁地采样基于 TD 误差的更重要的经验,以完成更有效的训练。 文献[7]提出了一种对决网络架构来估计状态值函数和优势函数。 这两个函数均用于估计 Q 值,以加快学习速度。
本章介绍的方法是值迭代/拟合。 通过找到最佳值函数间接学习策略。 在下一章中,方法将是使用称为策略梯度方法的一系列算法直接学习最佳策略。 学习策略有很多好处。 特别地,策略梯度方法可以处理离散和连续的动作空间。
8. 参考
Sutton and Barto: Reinforcement Learning: An Introduction, 2017 (http://incompleteideas.net/book/bookdraft2017nov5.pdf).Volodymyr Mnih et al.: Human-level Control through Deep Reinforcement Learning. Nature 518.7540, 2015: 529 (http://www.davidqiu.com:8888/research/nature14236.pdf).Hado Van Hasselt, Arthur Guez, and David Silver: Deep Reinforcement Learning with Double Q-Learning. AAAI. Vol. 16, 2016 (http://www.aaai.org/ocs/index.php/AAAI/AAAI16/paper/download/12389/11847).Kai Arulkumaran et al.: A Brief Survey of Deep Reinforcement Learning. arXiv preprint arXiv:1708.05866, 2017 (https://arxiv.org/pdf/1708.05866.pdf).David Silver: Lecture Notes on Reinforcement Learning (http://www0.cs.ucl.ac.uk/staff/d.silver/web/Teaching.html).Tom Schaul et al.: Prioritized experience replay. arXiv preprint arXiv:1511.05952, 2015 (https://arxiv.org/pdf/1511.05952.pdf).Ziyu Wang et al.: Dueling Network Architectures for Deep Reinforcement Learning. arXiv preprint arXiv:1511.06581, 2015 (https://arxiv.org/pdf/1511.06581.pdf).
十、策略梯度方法
在本章中,我们将介绍在强化学习中直接优化策略网络的算法。 这些算法统称为“策略梯度方法”。 由于策略网络是在训练期间直接优化的,因此策略梯度方法属于基于策略强化学习算法的族。 就像我们在“第 9 章”,“深度强化学习”中讨论的基于值的方法一样,策略梯度方法也可以实现为深度强化学习算法。
研究策略梯度方法的基本动机是解决 Q 学习的局限性。 我们会回想起 Q 学习是关于选择使状态值最大化的动作。 借助 Q 函数,我们能够确定策略,使智能体能够决定对给定状态采取何种操作。 选择的动作只是使智能体最大化的动作。 在这方面,Q 学习仅限于有限数量的离散动作。 它不能处理连续的动作空间环境。 此外,Q 学习不是直接优化策略。 最后,强化学习是要找到智能体能够使用的最佳策略,以便决定应采取何种行动以最大化回报。
相反,策略梯度方法适用于具有离散或连续动作空间的环境。 另外,我们将在本章中介绍的四种策略梯度方法是直接优化策略网络的表现度量。 这样就形成了一个经过训练的策略网络,智能体可以使用该网络来最佳地在其环境中采取行动。
总之,本章的目的是介绍:
- 策略梯度定理
- 四种策略梯度方法: REINFORCE , 带基线的 REINFORCE, 演员评论家和优势演员评论家(A2C)
- 在连续动作空间环境中如何在
tf.keras中实现策略梯度方法的指南
让我们从定理开始。
1. 策略梯度定理
如“第 9 章”,“深度强化学习”中所讨论的,智能体位于环境中,处于状态sₜ中,它是状态空间S的一个元素。 状态空间S可以是离散的,也可以是连续的。 智能体通过遵循策略π(a[t], s[t])从动作空间A采取动作aₜ。 A可以是离散的或连续的。 作为执行动作aₜ的结果,智能体会收到奖励r[t + 1],并且环境转换为新状态s[t + 1]。 新状态仅取决于当前状态和操作。 智能体的目标是学习一种最佳策略π*,该策略可最大化所有状态的回报:
(Equation 9.1.1)
收益Rₜ定义为从时间t直到剧集结束或达到最终状态时的折扣累积奖励:
(Equation 9.1.2)
根据“公式 9.1.2”,还可以通过遵循策略π将返回解释为给定状态的值。 从“公式 9.1.1”可以看出,与通常的γ^k < 1.0相比,与立即奖励相比,未来奖励的权重较低。
到目前为止,我们仅考虑通过优化基于值的函数Q(s, a)来学习策略。
本章的目标是通过参数化π(a[t] | s[t]) -> π(a[t] | s[t], θ)直接学习该策略。 通过参数化,我们可以使用神经网络来学习策略函数。
学习策略意味着我们将最大化某个目标函数J(θ),这是相对于参数θ的一种表现度量。在间歇式强化学习中,表现度量是起始状态的值。 在连续的情况下,目标函数是平均奖励率。
通过执行梯度上升来最大化目标函数J(θ)。 在梯度上升中,梯度更新是在要优化的函数的导数方向上。 到目前为止,我们的所有损失函数都通过最小化或通过执行梯度下降进行了优化。 稍后,在tf.keras实现中,我们将看到可以通过简单地否定目标函数并执行梯度下降来执行梯度上升。
直接学习策略的好处是它可以应用于离散和连续动作空间。 对于离散的动作空间:
(Equation 10.1.1)
其中aᵢ是第i个动作。 aᵢ可以是神经网络的预测或状态作用特征的线性函数:
(Equation 10.1.2)
φ(s[t], a[i])是将状态操作转换为特征的任何函数,例如编码器。
π(a[t] | s[t], θ)确定每个aᵢ的概率。 例如,在上一章中的柱杆平衡问题中,目标是通过沿二维轴向左或向右移动柱车来保持柱子直立。 在这种情况下,a₀和a₁分别是左右移动的概率。 通常,智能体以最高概率a[t] = max[i] π(a[t] | s[t], θ)采取行动。
对于连续动作空间,π(a[t] | s[t], θ)根据给定状态的概率分布对动作进行采样。 例如,如果连续动作空间在a[t] ∈ [-1.0, 1.0]范围内,则π(a[t] | s[t], θ)通常是高斯分布,其均值和标准差由策略网络预测。 预测动作是来自此高斯分布的样本。 为了确保不会生成任何无效的预测,该操作将被限制在 -1.0 和 1.0 之间。
正式地,对于连续的动作空间,该策略是高斯分布的样本:
(Equation 10.1.3)
平均值μ和标准差σ都是状态特征的函数:
(Equation 10.1.4)
(Equation 10.1.5)
φ(s[i])是将状态转换为其特征的任何函数。 ζ(x) = log(1 + e^x)是确保标准差为正值的softplus函数。 实现状态特征函数φ(s[t])的一种方法是使用自编码器网络的编码器。 在本章的最后,我们将训练一个自编码器,并将编码器部分用作状态特征。 因此,训练策略网络是优化参数的问题θ = [θ[μ], θ[σ]]。
给定连续可微分的策略函数π(a[t] | s[t], θ),策略梯度可以计算为:
(Equation 10.1.6)
“公式 10.1.6”也被称为策略梯度定理。 它适用于离散和连续动作空间。 根据通过 Q 值缩放的策略操作采样的自然对数,可以计算出相对于参数θ的梯度。“公式 10.1.6”利用了自然对数ᐁx/x = ᐁlnx的特性。
策略梯度定理在某种意义上是直观的,即表现梯度是根据目标策略样本估计的,并且与策略梯度成比例。 策略梯度由 Q 值缩放,以鼓励对状态值产生积极贡献的行动。 梯度还与动作概率成反比,以惩罚对提高性能没有贡献的频繁发生的动作。
有关策略梯度定理的证明,请参阅[2]和 David Silver 关于强化学习的讲义。
与策略梯度方法相关的细微优势。 例如,在某些基于纸牌的游戏中,与基于策略的方法不同,基于值的方法在处理随机性方面没有直接的过程。 在基于策略的方法中,操作概率随参数而平滑变化。
同时,相对于参数的微小变化,基于值的行为可能会发生剧烈变化。 最后,基于策略的方法对参数的依赖性使我们对如何执行表现考核的梯度提升产生了不同的表述。 这些是在后续部分中介绍的四种策略梯度方法。
基于策略的方法也有其自身的缺点。 由于趋向于收敛于局部最优而非全局最优,所以它们通常更难训练。 在本章末尾提出的实验中,智能体很容易适应并选择不一定提供最高值的动作。 策略梯度的特征还在于高差异。
梯度更新经常被高估。 此外,基于训练策略的方法非常耗时。 训练需要成千上万集(即采样效率不高)。 每个剧集仅提供少量样本。 在本章结尾处提供的实现方面的典型训练,大约需要一个小时才能在 GTX 1060 GPU 上进行 1,000 集。
在以下各节中,我们将讨论四种策略梯度方法。 虽然讨论的重点是连续的动作空间,但是该概念通常适用于离散的动作空间。
2. 蒙特卡洛策略梯度(REINFORCE)方法
最简单的策略梯度方法是 REINFORCE [4],这是蒙特卡洛策略梯度方法:
(Equation 10.2.1)
其中Rₜ是返回值,如“公式 9.1.2”所定义。Rₜ是策略梯度定理中Q^π(s[t], a[t])的无偏样本。
“算法 10.2.1”总结了 REINFORCE 算法[2]。 REINFORCE 是一种蒙特卡洛算法。 它不需要环境动态知识(换句话说,无需模型)。 仅需要经验样本(s[i], a[i], r[i+1], s[i+1])来优化策略网络π(a[t] | s[t])的参数。 折扣因子γ考虑到奖励随着步数增加而降低的事实。 梯度被γ^k打折。 在后续步骤中采用的梯度贡献较小。 学习率α是梯度更新的比例因子。
通过使用折扣梯度和学习率执行梯度上升来更新参数。 作为蒙特卡洛算法,REINFORCE 要求智能体在处理梯度更新之前先完成一集。 同样由于其蒙特卡洛性质,REINFORCE 的梯度更新具有高方差的特征。
算法 10.2.1 REINFORCE
要求:可微分的参数化目标策略网络π(a[t] | s[t], θ)。
要求:折扣因子,γ = [0, 1]和学习率α。 例如,γ = 0.99和α = 1e - 3。
要求:θ[0],初始策略网络参数(例如,θ[0] -> 0)。
-
重复。
-
通过跟随
π(a[t] | s[t], θ)来生成剧集(s[0]a[0]r[1]s[1], s[1]a[1]r[2]s[2], ..., s[T-1]a[T-1]r[T]s[T])。 -
对于步骤
t = 0, ..., T - 1,执行: -
计算返回值
R[t] = Σ γᵗ r[t+k], k = 0, ..., T。 -
计算折扣的表现梯度
ᐁJ(θ) = rᵗ R[t] ᐁ[θ] ln π(a[t] | s[t], θ)。 -
执行梯度上升
θ = θ + αᐁJ(θ)。
在 REINFORCE 中,可以通过神经网络对参数化策略进行建模,如图“图 10.2.1”所示:

图 10.2.1:策略网络
如上一节中讨论的,在连续动作空间的情况下,状态输入被转换为特征。 状态特征是策略网络的输入。 代表策略函数的高斯分布具有均值和标准差,均是状态特征的函数。 根据状态输入的性质,策略网络π(θ)可以是 MLP,CNN 或 RNN。 预测的动作只是策略函数的样本。
“列表 10.2.1”显示了REINFORCEAgent 类,该类在tf.keras中实现了“算法 10.2.1”。 train_by_episode()在剧集完成后调用,以计算每个步骤的回报。 train()通过针对目标函数logp_model优化网络来执行“算法 10.2.1”的第 5 行和第 6 行。 父类PolicyAgent在本章介绍的四种策略梯度方法的算法中实现了的通用代码。 在讨论所有策略梯度方法之后,将介绍PolicyAgent。
“列表 10.2.1”:policygradient-car-10.1.1.py
class REINFORCEAgent(PolicyAgent):
def __init__(self, env):
"""Implements the models and training of
REINFORCE policy gradient method
Arguments:
env (Object): OpenAI gym environment
"""
super().__init__(env)
def train_by_episode(self):
"""Train by episode
Prepare the dataset before the step by step training
"""
# only REINFORCE and REINFORCE with baseline
# use the ff code
# convert the rewards to returns
rewards = []
gamma = 0.99
for item in self.memory:
[_, _, _, reward, _] = item
rewards.append(reward)
# compute return per step
# return is the sum of rewards from t til end of episode
# return replaces reward in the list
for i in range(len(rewards)):
reward = rewards[i:]
horizon = len(reward)
discount = [math.pow(gamma, t) for t in range(horizon)]
return_ = np.dot(reward, discount)
self.memory[i][3] = return_
# train every step
for item in self.memory:
self.train(item, gamma=gamma)
def train(self, item, gamma=1.0):
"""Main routine for training
Arguments:
item (list) : one experience unit
gamma (float) : discount factor [0,1]
"""
[step, state, next_state, reward, done] = item
# must save state for entropy computation
self.state = state
discount_factor = gamma**step
delta = reward
# apply the discount factor as shown in Algorithms
# 10\. 2.1, 10.3.1 and 10.4.1
discounted_delta = delta * discount_factor
discounted_delta = np.reshape(discounted_delta, [-1, 1])
verbose = 1 if done else 0
# train the logp model (implies training of actor model
# as well) since they share exactly the same set of
# parameters
self.logp_model.fit(np.array(state),
discounted_delta,
batch_size=1,
epochs=1,
verbose=verbose)
以下部分提出了对 REINFORCE 方法的改进。
3. 带基线方法的 REINFORCE
REINFORCE 算法可以通过从收益δ = R[t] - B(s[t])中减去基线来概括。 基线函数B(s[t])可以是任何函数,只要它不依赖aₜ即可。 基线不会改变表现梯度的期望:
(Equation 10.3.1)
“公式 10.3.1”隐含E[π] [B(s[t]) ᐁ[θ] ln π(a[t] | s[t], θ)] = 0,因为B(s[t])不是aₜ的函数。 尽管引入基准不会改变期望值,但会减小梯度更新的方差。 方差的减少通常会加速学习。
在大多数情况下,我们使用值函数B(s[t]) = V(s[t])作为基准。 如果收益被高估,则比例系数将通过值函数成比例地减小,从而导致较低的方差。 值函数也已参数化V(s[t]) = V(s[t]; θ[v]),并与策略网络一起进行了训练。 在连续动作空间中,状态值可以是状态特征的线性函数:
(Equation 10.3.2)
“算法 10.3.1”用基线方法[1]总结了 REINFORCE。 这与 REINFORCE 相似,只不过将返回值替换为δ。 区别在于我们现在正在训练两个神经网络。
算法 10.3.1 带基线的 REINFORCE
要求:可微分的参数化目标策略网络π(a[t] | s[t], θ)。
要求:可微分的参数化值网络V(s[t], θ[v])。
要求:折扣因子γ ∈ [0, 1],表现梯度的学习率α和值梯度α[v]的学习率。
要求:θ[0],初始策略网络参数(例如,θ[0] -> 0)。 θ[v0],初始值网络参数(例如θ[v0] -> 0)。
-
重复。
-
通过跟随
π(a[t] | s[t], θ)来生成剧集(s[0]a[0]r[1]s[1], s[1]a[1]r[2]s[2], ..., a[T-1]a[T-1]r[T]s[T])。 -
对于步骤
t = 0, ..., T - 1,执行: -
计算返回值:
![]()
-
减去基线:
![]()
-
计算折扣值梯度:
![]()
-
执行梯度上升:
![]()
-
计算折扣的表现梯度:
![]()
-
执行梯度上升:
![]()
如图“图 10.3.1”所示,除了策略网络π(θ)之外,值网络V(θ)也同时受到训练。 通过表现梯度ᐁJ(θ)更新策略网络参数,而通过梯度ᐁV(θ[v])调整值网络参数。 由于 REINFORCE 是蒙特卡罗算法,因此值函数训练也是蒙特卡罗算法。
学习率不一定相同。 请注意,值网络也在执行梯度上升。

图 10.3.1:策略和值网络。 具有基线的 REINFORCE 具有一个计算基线的值网络
“列表 10.3.1”显示了REINFORCEBaselineAgent类,该类在tf.keras中实现了“算法 10.3.1”。 它继承自REINFORCEAgent,因为这两种算法仅在和train()方法上有所不同。 “算法 10.3.1”的第 5 行由delta = reward - self.value(state)[0]计算。 然后,通过调用各自模型的fit()方法来优化第 7 行和第 9 行中用于目标和值函数的网络logp_model和value_model。
“列表 10.3.1”:policygradient-car-10.1.1.py
class REINFORCEBaselineAgent(REINFORCEAgent):
def __init__(self, env):
"""Implements the models and training of
REINFORCE w/ baseline policy
gradient method
Arguments:
env (Object): OpenAI gym environment
"""
super().__init__(env)
def train(self, item, gamma=1.0):
"""Main routine for training
Arguments:
item (list) : one experience unit
gamma (float) : discount factor [0,1]
"""
[step, state, next_state, reward, done] = item
# must save state for entropy computation
self.state = state
discount_factor = gamma**step
# reinforce-baseline: delta = return - value
delta = reward - self.value(state)[0]
# apply the discount factor as shown in Algorithms
# 10\. 2.1, 10.3.1 and 10.4.1
discounted_delta = delta * discount_factor
discounted_delta = np.reshape(discounted_delta, [-1, 1])
verbose = 1 if done else 0
# train the logp model (implies training of actor model
# as well) since they share exactly the same set of
# parameters
self.logp_model.fit(np.array(state),
discounted_delta,
batch_size=1,
epochs=1,
verbose=verbose)
# train the value network (critic)
self.value_model.fit(np.array(state),
discounted_delta,
batch_size=1,
epochs=1,
verbose=verbose)
在的下一部分中,我们将介绍使用基准线方法对 REINFORCE 的改进。
4. 演员评论家方法
在带有基线的 REINFORCE 方法中,该值用作基线。 它不用于训练值函数。 在本节中,我们介绍 REINFORCE 与基线的变化,称为演员评论家方法。 策略和值网络扮演着参与者和批评者网络的角色。 策略网络是参与者决定给定状态时要采取的操作。 同时,值网络评估参与者或策略网络做出的决策。
值网络充当批评者的角色,可以量化参与者所选择的行动的好坏。 值网络通过将状态值V(s, θ[v]与收到的奖励r和观察到的下一个状态γV(s', θ[v])的折扣值之和来评估状态值。 差异δ表示为:
(Equation 10.4.1)
为了简单起见,我们在中删除了r和s的下标。“公式 10.4.1”类似于“第 9 章”,“深度强化学习”中讨论的 Q 学习中的时间差异。 下一个状态值被γ = [0.0, 1.0]折扣。估计遥远的未来奖励很困难。 因此,我们的估计仅基于近期r + γV(s', θ[v])。 这就是自举技术。
自举技术和“公式 10.4.1”中状态表示的依赖性通常会加速学习并减少差异。 从“公式 10.4.1”,我们注意到值网络评估了当前状态s = s[t],这是由于策略网络的上一个操作a[t-1]。 同时,策略梯度基于当前动作aₜ。 从某种意义上说,评估延迟了一步。
“算法 10.4.1”总结了演员评论家方法[1]。 除了评估用于训练策略和值网络的状态值评估外,还可以在线进行训练。 在每个步骤中,两个网络都经过训练。 这与 REINFORCE 和带有基线的 REINFORCE 不同,在基线之前,智能体完成了一个剧集。 首先,在当前状态的值估计期间向值网络查询两次,其次,为下一个状态的值查询。 这两个值都用于梯度计算中。
算法 10.4.1 演员评论家
要求:可微分的参数化目标策略网络π(a | s, θ)。
要求:可微分的参数化值网络V(s, θ[v])。
要求:折扣因子γ ∈ [0, 1],表现梯度的学习率α和值梯度α[v]的学习率。
要求:θ[0],初始策略网络参数(例如,θ[0] -> 0)。 θ[v0],初始值网络参数(例如θ[v0] -> 0)。
-
重复。
-
对于步骤
t = 0, ..., T - 1,执行: -
对动作
a ~ π(a | s, θ)进行采样。 -
执行动作并观察奖励
r和下一个状态s'。 -
评估状态值估计:
![]()
-
计算折扣值梯度:
![]()
-
执行梯度上升:
![]()
-
计算折扣表现梯度:
![]()
-
执行梯度上升:
![]()
-
s = s'
“图 10.4.1”显示了演员评论家网络:

图 10.4.1:演员评论家网络。 通过对值V'的第二次评估,演员评论家与 REINFORCE 的基线有所不同
“列表 10.4.1”显示了ActorCriticAgent类,该类在tf.keras中实现了“算法 10.4.1”。 与两种 REINFORCE 方法不同,演员评论家不等待剧集完成。 因此,它没有实现train_by_episode()。 在每个体验单元,通过调用各自模型的fit()方法,优化第 7 行和第 9 行中用于目标和值函数logp_model和value_model的网络。 delta变量存储第 5 行的结果。
“列表 10.4.1”:policygradient-car-10.1.1.py
class ActorCriticAgent(PolicyAgent):
def __init__(self, env):
"""Implements the models and training of
Actor Critic policy gradient method
Arguments:
env (Object): OpenAI gym environment
"""
super().__init__(env)
def train(self, item, gamma=1.0):
"""Main routine for training
Arguments:
item (list) : one experience unit
gamma (float) : discount factor [0,1]
"""
[step, state, next_state, reward, done] = item
# must save state for entropy computation
self.state = state
discount_factor = gamma**step
# actor-critic: delta = reward - value
# + discounted_next_value
delta = reward - self.value(state)[0]
# since this function is called by Actor-Critic
# directly, evaluate the value function here
if not done:
next_value = self.value(next_state)[0]
# add the discounted next value
delta += gamma*next_value
# apply the discount factor as shown in Algortihms
# 10\. 2.1, 10.3.1 and 10.4.1
discounted_delta = delta * discount_factor
discounted_delta = np.reshape(discounted_delta, [-1, 1])
verbose = 1 if done else 0
# train the logp model (implies training of actor model
# as well) since they share exactly the same set of
# parameters
self.logp_model.fit(np.array(state),
discounted_delta,
batch_size=1,
epochs=1,
verbose=verbose)
最终的策略梯度方法是 A2C。
5. 优势演员评论家(A2C)方法
在上一节的演员评论家方法中,目标是使的值函数正确评估状态值。 还有其他用于训练值网络的技术。 一种明显的方法是在值函数优化中使用均方误差(MSE),类似于 Q 学习中的算法。 新值梯度等于返回值Rₜ与状态值之间的 MSE 偏导数:
(Equation 10.5.1)
作为(R[t] - V(s, θ[v])) -> 0,值网络预测在预测给定状态的收益时变得更加准确。 我们将演员评论家算法的这种变化称为“优势演员评论家(A2C)”。 A2C 是[3]提出的“异步优势参与者关键(A3C)”的单线程或同步版本。 数量R[t] - V(s, θ[v])被称为优势。
“算法 10.5.1”总结了 A2C 方法。 A2C 和演员评论家之间存在一些差异。演员评论家在线上或根据经验样本进行训练。 A2C 类似于带基线的蒙特卡洛算法,REINFORCE 和 REINFORCE。 一集完成后,将对其进行训练。 从第一个状态到最后一个状态都对演员评论家进行了训练。 A2C 训练从最后一个状态开始,并在第一个状态结束。 此外,γᵗ不再打折 A2C 策略和值梯度。
A2C 的相应网络类似于“图 10.4.1”,因为我们仅更改了梯度计算方法。 为了鼓励训练过程中的探员探索,A3C 算法[3]建议将策略函数的加权熵值的梯度添加到到梯度函数β ᐁ[θ] H(π(a[t] | s[t], θ))中。 回想一下,熵是对信息或事件不确定性的度量。
算法 10.5.1 优势演员评论家(A2C)
要求:可微分的参数化目标策略网络π(a[t] | s[t], θ)。
要求:可微分的参数化值网络V(s[t], θ[v])。
要求:折扣因子γ ∈ [0, 1],表现梯度的学习率α,值梯度的学习率α[v]和熵权β。
要求:θ[0],初始策略网络参数(例如,θ[0] -> 0)。 θ[v0],初始值网络参数(例如θ[v0] -> 0)。
-
重复。
-
通过跟随
π(a[t] | s[t], θ)来生成剧集(s[0]a[0]r[1]s[1], s[1]a[1]r[2]s[2], ..., a[T-1]a[T-1]r[T]s[T])。 -
![]()
-
对于步骤
t = 0, ..., T - 1,执行: -
计算返回值:
![]()
-
计算值梯度:
![]()
-
累积梯度:
![]()
-
计算表现梯度:
![]()
-
执行梯度上升:
![]()
“列表 10.5.1”显示了A2CAgent类,该类在tf.keras中实现了“算法 10.5.1”。 与两个 REINFORCE 方法不同,返回值是从最后一个体验单元或状态到第一个体验单元或状态的计算得出的。 在每个体验单元,通过调用各自模型的fit()方法,优化第 7 行和第 9 行中用于目标和值函数logp_model和value_model的网络。 注意,在对象实例化期间,熵损失的beta或权重设置为0.9,以指示将使用熵损失函数。 此外,使用 MSE 损失函数训练value_model。
“列表 10.5.1”:policygradient-car-10.1.1.py
class A2CAgent(PolicyAgent):
def __init__(self, env):
"""Implements the models and training of
A2C policy gradient method
Arguments:
env (Object): OpenAI gym environment
"""
super().__init__(env)
# beta of entropy used in A2C
self.beta = 0.9
# loss function of A2C value_model is mse
self.loss = 'mse'
def train_by_episode(self, last_value=0):
"""Train by episode
Prepare the dataset before the step by step training
Arguments:
last_value (float): previous prediction of value net
"""
# implements A2C training from the last state
# to the first state
# discount factor
gamma = 0.95
r = last_value
# the memory is visited in reverse as shown
# in Algorithm 10.5.1
for item in self.memory[::-1]:
[step, state, next_state, reward, done] = item
# compute the return
r = reward + gamma*r
item = [step, state, next_state, r, done]
# train per step
# a2c reward has been discounted
self.train(item)
def train(self, item, gamma=1.0):
"""Main routine for training
Arguments:
item (list) : one experience unit
gamma (float) : discount factor [0,1]
"""
[step, state, next_state, reward, done] = item
# must save state for entropy computation
self.state = state
discount_factor = gamma**step
# a2c: delta = discounted_reward - value
delta = reward - self.value(state)[0]
verbose = 1 if done else 0
# train the logp model (implies training of actor model
# as well) since they share exactly the same set of
# parameters
self.logp_model.fit(np.array(state),
discounted_delta,
batch_size=1,
epochs=1,
verbose=verbose)
# in A2C, the target value is the return (reward
# replaced by return in the train_by_episode function)
discounted_delta = reward
discounted_delta = np.reshape(discounted_delta, [-1, 1])
# train the value network (critic)
self.value_model.fit(np.array(state),
discounted_delta,
batch_size=1,
epochs=1,
verbose=verbose)
在介绍的四种算法中,它们仅在目标函数和值(如果适用)优化方面有所不同。 在下一节中,我们将介绍四种算法的统一代码。
6. 使用 Keras 的策略梯度方法
上一节中讨论的策略梯度方法(“算法 10.2.1”至“算法 10.5.1”)使用相同的策略和值网络模型。“图 10.2.1”至“图 10.4.1”中的策略和值网络具有相同的配置。 四种策略梯度方法的不同之处仅在于:
- 表现和值梯度公式
- 训练策略
在本节中,我们将以一个代码讨论tf.keras算法 10.2.1 至“算法 10.5.1”的通用例程在tf.keras中的实现。
完整的代码可以在这个页面中找到。
但是在讨论实现之前,让我们简要探讨训练环境。
与 Q 学习不同,策略梯度方法适用于离散和连续动作空间。 在我们的示例中,我们将在连续动作空间案例示例中演示四种策略梯度方法,例如 OpenAI 健身房的MountainCarContinuous-v0。 如果您不熟悉 OpenAI Gym,请参阅“第 9 章”,“深度强化学习”。
“图 10.6.1”中显示了MountainCarContinuous-v0二维环境的快照。在此二维环境中,一辆功率不太强的汽车停在两座山之间:

图 10.6.1:MountainCarContinuous-v0 OpenAI Gym 环境
为了到达右侧山顶的黄旗,它必须来回行驶以获得足够的动力。 应用于汽车的能量越多(即动作的绝对值越大),则奖励越小(或负作用越大)。
奖励始终为负,到达标志时仅为正。 在这种情况下,汽车将获得 +100 的奖励。 但是,每个操作都会受到以下代码的惩罚:
reward-= math.pow(action[0],2)*0.1
有效动作值的连续范围是[-1.0, 1.0]。 超出范围时,动作将被剪裁为其最小值或最大值。 因此,应用大于 1.0 或小于 -1.0 的操作值是没有意义的。
MountainCarContinuous-v0环境状态包含两个元素:
- 车厢位置
- 车速
通过编码器将状态转换为状态特征。 像动作空间一样,状态空间也是连续的。 预测的动作是给定状态的策略模型的输出。 值函数的输出是状态的预测值。
如图“图 10.2.1”到“图 10.4.1”所示,在建立策略和值网络之前,我们必须首先创建一个将状态转换为特征的函数。 该函数由自编码器的编码器实现,类似于在“第 3 章”,“自编码器”中实现的编码器。
“图 10.6.2”显示了包括编码器和解码器的自编码器:

图 10.6.2:自编码器模型
在“图 10.6.3”中,编码器是由Input(2)-Dense(256, activation='relu')-Dense(128, activation='relu')-Dense(32)制成的 MLP。 每个状态都转换为 32 维特征向量:

图 10.6.3:编码器模型
在“图 10.6.4”中,解码器也是 MLP,但由Input(32)-Dense(128, activation='relu')-Dense(256, activation='relu')-Dense(2)制成:

图 10.6.4:解码器模型
自编码器使用 MSE,损失函数和tf.keras默认的 Adam 优化器训练了 10 个周期。 我们为训练和测试数据集采样了 220,000 个随机状态,并应用了 200,000:20,000 个训练测试拆分。 训练后,将保存编码器权重,以备将来在策略和值网络的训练中使用。“列表 10.6.1”显示了构建和训练自编码器的方法。
在tf.keras实现中,除非另有说明,否则我们将在本节中提及的所有例程均作为PolicyAgent类中的方法实现。 PolicyAgent的作用是代表策略梯度方法的常用功能,包括建立和训练自编码器网络模型以及预测动作,对数概率,熵和状态值。 这是“列表 10.2.1”至“列表 10.5.1”中介绍的每个策略梯度方法智能体类的超类。
“列表 10.6.1”:policygradient-car-10.1.1.py
构建和训练特征自编码器的方法:
class PolicyAgent:
def __init__(self, env):
"""Implements the models and training of
Policy Gradient Methods
Argument:
env (Object): OpenAI gym environment
"""
self.env = env
# entropy loss weight
self.beta = 0.0
# value loss for all policy gradients except A2C
self.loss = self.value_loss
# s,a,r,s' are stored in memory
self.memory = []
# for computation of input size
self.state = env.reset()
self.state_dim = env.observation_space.shape[0]
self.state = np.reshape(self.state, [1, self.state_dim])
self.build_autoencoder()
def build_autoencoder(self):
"""autoencoder to convert states into features
"""
# first build the encoder model
inputs = Input(shape=(self.state_dim, ), name='state')
feature_size = 32
x = Dense(256, activation='relu')(inputs)
x = Dense(128, activation='relu')(x)
feature = Dense(feature_size, name='feature_vector')(x)
# instantiate encoder model
self.encoder = Model(inputs, feature, name='encoder')
self.encoder.summary()
plot_model(self.encoder,
to_file='encoder.png',
show_shapes=True)
# build the decoder model
feature_inputs = Input(shape=(feature_size,),
name='decoder_input')
x = Dense(128, activation='relu')(feature_inputs)
x = Dense(256, activation='relu')(x)
outputs = Dense(self.state_dim, activation='linear')(x)
# instantiate decoder model
self.decoder = Model(feature_inputs,
outputs,
name='decoder')
self.decoder.summary()
plot_model(self.decoder,
to_file='decoder.png',
show_shapes=True)
# autoencoder = encoder + decoder
# instantiate autoencoder model
self.autoencoder = Model(inputs,
self.decoder(self.encoder(inputs)),
name='autoencoder')
self.autoencoder.summary()
plot_model(self.autoencoder,
to_file='autoencoder.png',
show_shapes=True)
# Mean Square Error (MSE) loss function, Adam optimizer
self.autoencoder.compile(loss='mse', optimizer='adam')
def train_autoencoder(self, x_train, x_test):
"""Training the autoencoder using randomly sampled
states from the environment
Arguments:
x_train (tensor): autoencoder train dataset
x_test (tensor): autoencoder test dataset
"""
# train the autoencoder
batch_size = 32
self.autoencoder.fit(x_train,
x_train,
validation_data=(x_test, x_test),
epochs=10,
batch_size=batch_size)
在给定MountainCarContinuous-v0环境的情况下,策略(或参与者)模型会预测必须应用于汽车的操作。 如本章第一部分中有关策略梯度方法的讨论所述,对于连续动作空间,策略模型从高斯分布π(a[t] | s[t], θ) = a[t] ~ N(μ(s[t]), σ²(s[t]))中采样一个动作。 在tf. keras中,实现为:
import tensorflow_probability as tfp
def action(self, args):
"""Given mean and stddev, sample an action, clip
and return
We assume Gaussian distribution of probability
of selecting an action given a state
Arguments:
args (list) : mean, stddev list
"""
mean, stddev = args
dist = tfp.distributions.Normal(loc=mean, scale=stddev)
action = dist.sample(1)
action = K.clip(action,
self.env.action_space.low[0],
self.env.action_space.high[0])
return action
动作被限制在其最小和最大可能值之间。 在这种方法中,我们使用TensorFlow probability包。 可以通过以下方式单独安装:
pip3 install --upgrade tensorflow-probability
策略网络的作用是预测高斯分布的均值和标准差。“图 10.6.5”显示了为π(a[t] | s[t], θ)建模的策略网络。

图 10.6.5:策略模型(参与者模型)
请注意,编码器模型具有冻结的预训练权重。 仅平均值和标准差权重会收到表现梯度更新。 策略网络基本上是“公式 10.1.4”和“公式 10.1.5”的实现,为方便起见在此重复:
(Equation 10.1.4)
(Equation 10.1.5)
其中φ(s[t])是编码器,θ[μ]是平均值Dense(1)层的权重,θ[σ]是标准差Dense(1)层的权重。 我们使用修改后的softplus函数ζ(·)来避免标准差为零:
def softplusk(x):
"""Some implementations use a modified softplus
to ensure that the stddev is never zero
Argument:
x (tensor): activation input
"""
return K.softplus(x) + 1e-10
策略模型构建器显示在“列表 10.6.2”中。 对数概率,熵和值模型也包含在此清单中,我们将在下面讨论。
“列表 10.6.2”:policygradient-car-10.1.1.py
根据编码后的状态特征构建策略(角色),logp,熵和值模型的方法:
def build_actor_critic(self):
"""4 models are built but 3 models share the
same parameters. hence training one, trains the rest.
The 3 models that share the same parameters
are action, logp, and entropy models.
Entropy model is used by A2C only.
Each model has the same MLP structure:
Input(2)-Encoder-Output(1).
The output activation depends on the nature
of the output.
"""
inputs = Input(shape=(self.state_dim, ), name='state')
self.encoder.trainable = False
x = self.encoder(inputs)
mean = Dense(1,
activation='linear',
kernel_initializer='zero',
name='mean')(x)
stddev = Dense(1,
kernel_initializer='zero',
name='stddev')(x)
# use of softplusk avoids stddev = 0
stddev = Activation('softplusk', name='softplus')(stddev)
action = Lambda(self.action,
output_shape=(1,),
name='action')([mean, stddev])
self.actor_model = Model(inputs, action, name='action')
self.actor_model.summary()
plot_model(self.actor_model,
to_file='actor_model.png',
show_shapes=True)
logp = Lambda(self.logp,
output_shape=(1,),
name='logp')([mean, stddev, action])
self.logp_model = Model(inputs, logp, name='logp')
self.logp_model.summary()
plot_model(self.logp_model,
to_file='logp_model.png',
show_shapes=True)
entropy = Lambda(self.entropy,
output_shape=(1,),
name='entropy')([mean, stddev])
self.entropy_model = Model(inputs, entropy, name='entropy')
self.entropy_model.summary()
plot_model(self.entropy_model,
to_file='entropy_model.png',
show_shapes=True)
value = Dense(1,
activation='linear',
kernel_initializer='zero',
name='value')(x)
self.value_model = Model(inputs, value, name='value')
self.value_model.summary()
plot_model(self.value_model,
to_file='value_model.png',
show_shapes=True)
# logp loss of policy network
loss = self.logp_loss(self.get_entropy(self.state),
beta=self.beta)
optimizer = RMSprop(lr=1e-3)
self.logp_model.compile(loss=loss, optimizer=optimizer)
optimizer = Adam(lr=1e-3)
self.value_model.compile(loss=self.loss, optimizer=optimizer)

图 10.6.6:策略的高斯对数概率模型
除了策略网络π(a[t] | s[t], θ)之外,我们还必须具有操作日志概率(logp)网络ln π(a[t] | s[t], θ),因为该实际上是计算梯度的系统。 如图“图 10.6.6”所示,logp网络只是一个策略网络,其中附加的 Lambda(1)层在给定了作用,均值和标准差的情况下计算了高斯分布的对数概率。
logp网络和参与者(策略)模型共享同一组参数。 Lambda 层没有任何参数。 它是通过以下函数实现的:
def logp(self, args):
"""Given mean, stddev, and action compute
the log probability of the Gaussian distribution
Arguments:
args (list) : mean, stddev action, list
"""
mean, stddev, action = args
dist = tfp.distributions.Normal(loc=mean, scale=stddev)
logp = dist.log_prob(action)
return logp
训练logp网络也可以训练角色模型。 在本节中讨论的训练方法中,仅训练logp网络。
如图“图 10.6.7”所示,熵模型还与策略网络共享参数:

图 10.6.7:熵模型
给定平均值和标准差,使用以下函数,输出Lambda(1)层计算高斯分布的熵:
def entropy(self, args):
"""Given the mean and stddev compute
the Gaussian dist entropy
Arguments:
args (list) : mean, stddev list
"""
mean, stddev = args
dist = tfp.distributions.Normal(loc=mean, scale=stddev)
entropy = dist.entropy()
return entropy
熵模型仅用于 A2C 方法。
“图 10.6.8”显示了值模型:

图 10.6.8:值模型
该模型还使用具有权重的预训练编码器来实现以下公式“公式 10.3.2”,为方便起见,在此重复:
(Equation 10.3.2)
θ[v]是Dense(1)层的权重,该层是唯一接收值梯度更新的层。“图 10.6.8”表示“算法 10.3.1”至“算法 10.5.1”中的V(s[t], θ[v])。 值模型可以建立在以下几行中:
inputs = Input(shape=(self.state_dim, ), name='state')
self.encoder.trainable = False
x = self.encoder(inputs)
value = Dense(1,
activation='linear',
kernel_initializer='zero',
name='value')(x)
self.value_model = Model(inputs, value, name='value')
这些行也用build_actor_critic()方法实现,如清单 10.6.2 所示。
建立网络模型后,下一步就是训练。 在“算法 10.2.1”至“算法 10.5.1”中,我们通过梯度上升执行目标函数最大化。 在tf.keras中,我们通过梯度下降执行损失函数最小化。 损失函数只是目标函数最大化的负数。 梯度下降是梯度上升的负值。“列表 10.6.3”显示了logp和值损失函数。
我们可以利用损失函数的通用结构来统一“算法 10.2.1”至“算法 10.5.1”中的损失函数。 表现和值梯度仅在其恒定因子上有所不同。 所有表现梯度都有一个通用项ᐁ[θ] ln π(a[t] | s[t], θ)。 这由策略日志概率损失函数logp_loss()中的y_pred表示。 通用项ᐁ[θ] ln π(a[t] | s[t], θ)的因素取决于哪种算法,并实现为y_true。“表 10.6.1”显示y_true的值。 其余项是熵的加权梯度β ᐁ[θ] H(π(a[t] | s[t], θ))。 这是通过logp_loss()函数中beta和entropy的乘积实现的。 仅 A2C 使用此项,因此默认为self.beta=0.0。 对于 A2C,self.beta=0.9。
“列表 10.6.3”:policygradient-car-10.1.1.py
logp和值网络的损失函数:
def logp_loss(self, entropy, beta=0.0):
"""logp loss, the 3rd and 4th variables
(entropy and beta) are needed by A2C
so we have a different loss function structure
Arguments:
entropy (tensor): Entropy loss
beta (float): Entropy loss weight
"""
def loss(y_true, y_pred):
return -K.mean((y_pred * y_true) \
+ (beta * entropy), axis=-1)
return loss
def value_loss(self, y_true, y_pred):
"""Typical loss function structure that accepts
2 arguments only
this will be used by value loss of all methods
except A2C
Arguments:
y_true (tensor): value ground truth
y_pred (tensor): value prediction
"""
return -K.mean(y_pred * y_true, axis=-1)
| 算法 | logp_loss的y_true |
value_loss的y_true |
|---|---|---|
| 10.2.1 REINFORCE | γᵗ R[t] |
不适用 |
| 10.3.1 使用基线的 REINFORCE | γᵗ δ |
γᵗ δ |
| 10.4.1 演员评论家 | γᵗ δ |
γᵗ δ |
| 10.5.1 A2C | R[t] - V(s, θ[v]) |
Rₜ |
表 10.6.1:logp_loss的y_true值和value_loss
“表 10.6.2”中显示了用于计算“表 10.6.1”中的y_true的代码实现:
| 算法 | y_true公式 |
Keras 中的y_true |
|---|---|---|
| 10.2.1 REINFORCE | γᵗ R[t] |
reward * discount_factor |
| 10.3.1 使用基线的 REINFORCE | γᵗ δ |
(reward - self.value(state)[0]) * discount_factor |
| 10.4.1 演员评论家 | γᵗ δ |
(reward - self.value(state)[0] + gamma * next_value) * discount_factor |
| 10.5.1 A2C | R[t] - V(s, θ[v])和Rₜ |
(reward - self.value(state)[0])和reward |
表 10.6.2:表 10.6.1 中的y_true值
类似地,“算法 10.3.1”和“算法 10.4.1”的值损失函数具有相同的结构。 值损失函数在tf.keras中实现为value_loss(),如“列表 10.6.3”所示。 公共梯度因子ᐁ[θ[v]] V(s[t], θ[v])由张量y_pred表示。 剩余因子由y_true表示。 y_true值也显示在“表 10.6.1”中。 REINFORCE 不使用值函数。 A2C 使用 MSE 损失函数来学习值函数。 在 A2C 中,y_true代表目标值或基本情况。
有了所有网络模型和损失函数,最后一部分是训练策略,每种算法都不同。 每个策略梯度方法的训练算法已在“列表 10.2.1”至“列表 10.5.1”中进行了讨论。 “算法 10.2.1”,“算法 10.3.1”和“算法 10.5.1”等待完整的剧集在训练之前完成,因此它同时运行train_by_episode() 和train()。 完整剧集保存在self.memory中。 演员评论家“算法 10.4.1”每步训练一次,仅运行train()。
“列表 10.6.4”显示了当智能体执行并训练策略和值模型时,一个剧集如何展开。 for循环执行 1,000 集。 当达到 1,000 步或汽车触及旗帜时,剧集终止。 智能体在每个步骤执行策略预测的操作。 在每个剧集或步骤之后,将调用训练例程。
“列表 10.6.4”:policygradient-car-10.1.1.py
# sampling and fitting
for episode in range(episode_count):
state = env.reset()
# state is car [position, speed]
state = np.reshape(state, [1, state_dim])
# reset all variables and memory before the start of
# every episode
step = 0
total_reward = 0
done = False
agent.reset_memory()
while not done:
# [min, max] action = [-1.0, 1.0]
# for baseline, random choice of action will not move
# the car pass the flag pole
if args.random:
action = env.action_space.sample()
else:
action = agent.act(state)
env.render()
# after executing the action, get s', r, done
next_state, reward, done, _ = env.step(action)
next_state = np.reshape(next_state, [1, state_dim])
# save the experience unit in memory for training
# Actor-Critic does not need this but we keep it anyway.
item = [step, state, next_state, reward, done]
agent.remember(item)
if args.actor_critic and train:
# only actor-critic performs online training
# train at every step as it happens
agent.train(item, gamma=0.99)
elif not args.random and done and train:
# for REINFORCE, REINFORCE with baseline, and A2C
# we wait for the completion of the episode before
# training the network(s)
# last value as used by A2C
if args.a2c:
v = 0 if reward > 0 else agent.value(next_state)[0]
agent.train_by_episode(last_value=v)
else:
agent.train_by_episode()
# accumulate reward
total_reward += reward
# next state is the new state
state = next_state
step += 1
在训练期间,我们收集了数据以确定每个策略梯度算法的表现。 在下一部分中,我们总结了结果。
7. 策略梯度方法的表现评估
通过训练智能体 1000 次剧集,评估了 4 种策略梯度方法。 我们将 1 次训练定义为 1,000 次训练。 第一表现度量标准是通过累计汽车在 1,000 集内达到标志的次数来衡量的。
在此指标中,A2C 达到该标志的次数最多,其次是 REINFORCE(具有基线,演员评论家和 REINFORCE)。 使用基线或批判者可以加速学习。 请注意,这些是训练会话,智能体会在其中不断提高其表现。 在实验中,有些情况下智能体的表现没有随时间改善。
第二个表现指标基于以下要求:如果每集的总奖励至少为 90.0,则认为MountainCarContinuous-v0已解决。 从每种方法的 5 个训练会话中,我们选择了最近 100 个剧集(第 900 至 999 集)中最高总奖励的 1 个训练会话。
“图 10.7.1”至“图 10.7.4”显示了在执行 1000 集时山地车到达标志的次数。

图 10.7.1:山车使用 REINFORCE 方法到达标志的次数

图 10.7.2:使用基线方法使用 REINFORCE,山地车到达标志的次数

图 10.7.3:使用演员评论家方法山地车到达旗帜的次数

图 10.7.4:山地车使用 A2C 方法到达标志的次数
“图 10.7.5”至“图 10.7.8”显示 1,000 集的总奖励。

图 10.7.5:使用 REINFORCE 方法获得的每集总奖励

图 10.7.6:使用带有基线方法的 REINFORCE,每集获得的总奖励。

图 10.7.7:使用演员评论家方法获得的每集总奖励

图 10.7.8:使用 A2C 方法获得的每集总奖励
以为基准的 REINFORCE 是唯一能够在 1,000 次训练中始终获得约 90 的总奖励的方法。 A2C 的表现仅次于第二名,但无法始终达到至少 90 分的总奖励。
在进行的实验中,我们使用相同的学习率1e-3进行对数概率和值网络优化。 折扣系数设置为 0.99(A2C 除外),以 0.95 的折扣系数更容易训练。
鼓励阅读器通过执行以下操作来运行受过训练的网络:
python3 policygradient-car-10.1.1.py
--encoder_weights=encoder_weights.h5 --actor_weights=actor_weights.h5
“表 10.7.1”显示了其他运行policygradient-car-10.1.1.py的模式。 权重文件(即*.h5)可以替换为您自己的预训练权重文件。 请查阅代码以查看其他可能的选项。
| 目的 | 运行 |
|---|---|
| 从零开始训练 REINFORCE | python3 policygradient-car-10.1.1.py |
| 从头开始使用基线训练 REINFORCE | python3 policygradient-car-10.1.1.py -b |
| 从零开始训练演员评论家 | python3 policygradient-car-10.1.1.py -a |
| 从头开始训练 A2C | python3 policygradient-car-10.1.1.py -c |
| 从先前保存的权重中训练 REINFORCE | python3 policygradient-car-10.1.1.py``--encoder-weights=encoder_weights.h5``--actor-weights=actor_weights.h5 --train |
| 使用先前保存的权重使用基线训练 REINFORCE | python3 policygradient-car-10.1.1.py``--encoder-weights=encoder_weights.h5``--actor-weights=actor_weights.h5``--value-weights=value_weights.h5 -b --train |
| 使用先前保存的权重训练演员评论家 | python3 policygradient-car-10.1.1.py``--encoder-weights=encoder_weights.h5``--actor-weights=actor_weights.h5``--value-weights=value_weights.h5 -a --train |
| 使用先前保存的权重训练 A2C | python3 policygradient-car-10.1.1.py``--encoder-weights=encoder_weights.h5``--actor-weights=actor_weights.h5``--value-weights=value_weights.h5 -c --train |
表 10.7.1:运行 policygradient-car-10.1.1.py 时的不同选项
最后一点,我们在tf.keras中对策略梯度方法的实现存在一些局限性。 例如,训练演员模型需要对动作进行重新采样。 首先对动作进行采样并将其应用于环境,以观察奖励和下一个状态。 然后,采取另一个样本来训练对数概率模型。 第二个样本不一定与第一个样本相同,但是用于训练的奖励来自第一个采样动作,这可能会在梯度计算中引入随机误差。
8. 总结
在本章中,我们介绍了策略梯度方法。 从策略梯度定理开始,我们制定了四种方法来训练策略网络。 详细讨论了四种方法:REINFORCE,带有基线的 REINFORCE,演员评论家和 A2C 算法。 我们探讨了如何在 Keras 中实现这四种方法。 然后,我们通过检查智能体成功达到目标的次数以及每集获得的总奖励来验证算法。
与上一章中讨论的深度 Q 网络[2]相似,基本策略梯度算法可以进行一些改进。 例如,最突出的一个是 A3C [3],它是 A2C 的多线程版本。 这使智能体可以同时接触不同的经验,并异步优化策略和值网络。 但是,在 OpenAI 进行的实验中,与 A2C 相比,A3C 没有强大的优势,因为前者无法利用当今提供强大的 GPU 的优势。
在接下来的两章中,我们将着手于另一个领域-对象检测和语义分割。 对象检测使智能体能够识别和定位给定图像中的对象。 语义分割基于对象类别识别给定图像中的像素区域。
9. 参考
Richard Sutton and Andrew Barto: Reinforcement Learning: An Introduction: http://incompleteideas.net/book/bookdraft2017nov5.pdf (2017)Volodymyr Mnih et al.: Human-level control through deep reinforcement learning, Nature 518.7540 (2015): 529Volodymyr Mnih et al.: Asynchronous Methods for Deep Reinforcement Learning, International conference on machine learning, 2016Ronald Williams: Simple statistical gradient-following algorithms for connectionist reinforcement learning, Machine learning 8.3-4 (1992): 229-256
十一、对象检测
目标检测是计算机视觉最重要的应用之一。 对象检测是同时定位和识别图像中存在的对象的任务。 为了使自动驾驶汽车安全地在街道上行驶,该算法必须检测到行人,道路,车辆,交通信号灯,标志和意外障碍物的存在。 在安全方面,入侵者的存在可以用来触发警报或通知适当的当局。
尽管很重要,但是对象检测一直是计算机视觉中的一个长期存在的问题。 已经提出了许多算法,但是通常很慢,并且精度和召回率很低。 与 AlexNet [1]在 ImageNet 大规模图像分类问题中所取得的成就类似,深度学习显着提高了对象检测领域。 最新的对象检测方法现在可以实时运行,并且具有更高的精度和召回率。
在本章中,我们重点介绍实时对象检测。 特别是,我们讨论了tf.keras中单发检测(SSD)[2]的概念和实现。 与其他深度学习检测算法相比,SSD 可在现代 GPU 上实现实时检测速度,而表现不会显着下降。 SSD 还易于端到端训练。
总之,本章的目的是介绍:
- 对象检测的概念
- 多尺度目标检测的概念
- SSD 作为多尺度目标检测算法
tf.keras中 SSD 的实现
我们将从介绍对象检测的概念开始。
1. 对象检测
在对象检测中,目标是在图像中定位和识别物体。“图 11.1.1”显示了目标汽水罐的目标物检测。 本地化意味着必须估计对象的边界框。 使用左上角像素坐标和右下角像素坐标是用于描述边界框的通用约定。 在“图 11.1.1”中,左上角像素具有坐标(x_min, y_min),而右下角像素的坐标为(x_max, y_max)。像素坐标系的原点(0, 0)位于整个图像的左上角像素。
在执行定位时,检测还必须识别对象。 识别是计算机视觉中的经典识别或分类任务。 至少,对象检测必须确定边界框是属于已知对象还是背景。 可以训练对象检测网络以仅检测一个特定对象,例如“图 11.1.1”中的汽水罐。 其他所有内容均视为背景,因此无需显示其边界框。 同一对象的多个实例,例如两个或多个汽水罐,也可以通过同一网络检测到,如图“图 11.1.2”所示。

图 11.1.1 说明了对象检测是在图像中定位和识别对象的过程。

图 11.1.2 被训练为检测一个对象实例的同一网络可以检测到同一对象的多个实例。
如果场景中存在多个对象,例如在“图 11.1.3”中,则对象检测方法只能识别在其上训练的一个对象。 其他两个对象将被分类为背景,并且不会分配边界框。

图 11.1.3 如果仅在检测汽水罐方面训练了对象检测,它将忽略图像中的其他两个对象。
但是,如果重新训练了网络以检测三个对象:1)汽水罐,2)果汁罐和 3)水瓶会同时定位和识别,如图“图 11.1.4”所示。

图 11.1.4 即使背景杂乱或照明发生变化,也可以重新训练对象检测网络以检测所有三个对象。
一个好的对象检测器必须在现实环境中具有鲁棒性。“图 11.1.4”显示了一个好的对象检测网络,即使背景杂乱甚至在弱光条件下,也可以定位和识别已知对象。 对象检测器必须具有鲁棒性的其他因素是物体变换(旋转和/或平移),表面反射,纹理变化和噪声。
总之,对象检测的目标是针对图像中每个可识别的对象同时预测以下内容:
y_cls或单热向量形式的类别或类y_box = ((x_min, y_min), (x_max, y_max))或像素坐标形式的边界框坐标
通过解释了对象检测的基本概念,我们可以开始讨论对象检测的某些特定机制。 我们将从介绍锚框开始。
2. 锚框
从上一节的讨论中,我们了解到,对象检测必须预测边界框区域以及其中的对象类别。 假设与此同时,我们的重点是边界框坐标估计。
网络如何预测坐标(x_min, y_min)和(x_max, y_max)? 网络可以做出与图像的左上角像素坐标和右下角像素坐标相对应的初始猜测,例如(0, 0)和(w, h)。w是图像宽度,而h是图像高度。 然后,网络通过对地面真实边界框坐标执行回归来迭代地校正估计。
由于可能的像素值存在较大差异,因此使用原始像素估计边界框坐标不是最佳方法。 SSD 代替原始像素,将地面真值边界框和预测边界框坐标之间的像素误差值最小化。 对于此示例,像素的误差值为(x_min, y_min)和(x_max - w, y_max - h)。 这些值称为offsets。
为了帮助网络找出正确的边界框坐标,将图像划分为多个区域。 每个区域称为定位框。 然后,网络估计每个锚框的偏移。 这样得出的预测更接近于基本事实。
例如,如图“图 11.2.1”所示,将普通图像尺寸640 x 480分为2 x 1个区域,从而产生两个锚框。 与2 x 2的大小不同,2 x 1的划分创建了近似方形的锚框。 在第一个锚点框中,新的偏移量是(x_min, y_min)和{x_max - w/2, y_max - h},它们比没有锚框的像素误差值更小。 第二个锚框的偏移量也较小。
在“图 11.2.2”中,图像被进一步分割。 这次,锚框为3 x 2。第二个锚框偏移为{x_min - w/3, y_min}和{x_max - 2w/3, y_max - h/2},这是迄今为止最小的。 但是,如果将图像进一步分为5 x 4,则偏移量开始再次增加。 主要思想是,在创建各种尺寸的区域的过程中,将出现最接近地面真值边界框的最佳锚框大小。 使用多尺度锚框有效地检测不同大小的对象将巩固多尺度对象检测算法的概念。
找到一个最佳的锚框并不是零成本。 尤其是,有些外部锚框的偏移量比使用整个图像还要差。 在这种情况下,SSD 建议这些锚定框不应对整个优化过程有所帮助,而应予以抑制。 在以下各节中,将更详细地讨论排除非最佳锚框的算法。
到目前为止,我们已经有三套锚框。
第一个创建一个2 x 1的锚框网格,每个锚框的尺寸为(w/2, h)。
第二个创建一个3 x 2的锚框网格,每个锚框的尺寸为(w/3, h/2)。
第三个创建一个5 x 4的锚框网格,每个锚框的尺寸为(w/5, h/4)。
我们还需要多少套锚盒? 它取决于图像的尺寸和对象最小边框的尺寸。 对于此示例中使用的640 x 480图像,其他锚点框为:
10 x 8格的锚框,每个框的尺寸为(w/10, h/8)
20 x 15格的锚框,每个锚框的尺寸为(w/20, h/15)
40 x 30格的锚框,每个框的尺寸为(w/40, h/30)
对于具有40 x 30网格的锚框的640 x 480图像,最小的锚框覆盖输入图像的16 x 16像素斑块,也称为接收域。 到目前为止,包围盒的总数为 1608。对于所有尺寸,最小的缩放因子可以总结为:
(Equation 11.2.1)
锚框如何进一步改进? 如果我们允许锚框具有不同的纵横比,则可以减少偏移量。 每个调整大小的锚点框的质心与原始锚点框相同。 除宽高比 1 外,SSD [2]包括其他宽高比:
(Equation 11.2.2)
对于每个纵横比aᵢ,对应的锚框尺寸为:
(Equation 11.2.3)
(s[xj], s[yj])是“公式 11.2.1”中的第j个比例因子。
使用每个锚框五个不同的长宽比,锚框的总数将增加到1,608 x 5 = 8,040。“图 11.2.3”显示了(s[x4], s[y4]) = (1/3, 1/2)和a[i ∈ {0, 1, 3}] = 1, 2, 1/2情况下的锚框。
请注意,为了达到一定的纵横比,我们不会使锚框变形。 而是调整锚框的宽度和高度。
对于a[0] = 1,SSD 建议使用其他尺寸的锚框:
(Equation 11.2.4)
现在每个区域有六个锚定框。 有五个是由于五个纵横比,另外还有一个纵横比为 1。新的锚框总数增加到 9,648。

图 11.2.1 将图像划分为多个区域(也称为锚框),使网络可以进行更接近地面真实情况的预测。

图 11.2.2 使用较小的锚框可以进一步减少偏移。

图 11.2.3 具有比例因子(s[x4], s[y4]) = (1/3, 1/2)和纵横比a[i ∈ {0, 1, 3}] = 1, 2, 1/2的一个区域的锚框。
下面的“列表 11.2.1”显示了锚框生成函数anchor_boxes()。 给定输入的图像形状(image_shape),纵横比(aspect_ratios)和缩放因子(sizes),将计算不同的锚框大小并将其存储在名为width_height的列表中。 从给定的特征映射形状(feature_shape或(h_fmap, w_fmap)和width_height, 生成具有尺寸(h_fmap, w_fmap, n_boxes, 4)。n_boxes或每个特征映射点的锚点框数是基于纵横比和等于 1 的纵横比的一个附加大小计算的。
“列表 11.2.1”:锚框生成函数的layer_utils.py函数:
def anchor_boxes(feature_shape,
image_shape,
index=0,
n_layers=4,
aspect_ratios=(1, 2, 0.5)):
""" Compute the anchor boxes for a given feature map.
Anchor boxes are in minmax format
Arguments:
feature_shape (list): Feature map shape
image_shape (list): Image size shape
index (int): Indicates which of ssd head layers
are we referring to
n_layers (int): Number of ssd head layers
Returns:
boxes (tensor): Anchor boxes per feature map
"""
# anchor box sizes given an index of layer in ssd head
sizes = anchor_sizes(n_layers)[index]
# number of anchor boxes per feature map pt
n_boxes = len(aspect_ratios) + 1
# ignore number of channels (last)
image_height, image_width, _ = image_shape
# ignore number of feature maps (last)
feature_height, feature_width, _ = feature_shape
# normalized width and height
# sizes[0] is scale size, sizes[1] is sqrt(scale*(scale+1))
norm_height = image_height * sizes[0]
norm_width = image_width * sizes[0]
# list of anchor boxes (width, height)
width_height = []
# anchor box by aspect ratio on resized image dims
# Equation 11.2.3
for ar in aspect_ratios:
box_width = norm_width * np.sqrt(ar)
box_height = norm_height / np.sqrt(ar)
width_height.append((box_width, box_height))
# multiply anchor box dim by size[1] for aspect_ratio = 1
# Equation 11.2.4
box_width = image_width * sizes[1]
box_height = image_height * sizes[1]
width_height.append((box_width, box_height))
# now an array of (width, height)
width_height = np.array(width_height)
# dimensions of each receptive field in pixels
grid_width = image_width / feature_width
grid_height = image_height / feature_height
# compute center of receptive field per feature pt
# (cx, cy) format
# starting at midpoint of 1st receptive field
start = grid_width * 0.5
# ending at midpoint of last receptive field
end = (feature_width - 0.5) * grid_width
cx = np.linspace(start, end, feature_width)
start = grid_height * 0.5
end = (feature_height - 0.5) * grid_height
cy = np.linspace(start, end, feature_height)
# grid of box centers
cx_grid, cy_grid = np.meshgrid(cx, cy)
# for np.tile()
cx_grid = np.expand_dims(cx_grid, -1)
cy_grid = np.expand_dims(cy_grid, -1)
# tensor = (feature_map_height, feature_map_width, n_boxes, 4)
# aligned with image tensor (height, width, channels)
# last dimension = (cx, cy, w, h)
boxes = np.zeros((feature_height, feature_width, n_boxes, 4))
# (cx, cy)
boxes[..., 0] = np.tile(cx_grid, (1, 1, n_boxes))
boxes[..., 1] = np.tile(cy_grid, (1, 1, n_boxes))
# (w, h)
boxes[..., 2] = width_height[:, 0]
boxes[..., 3] = width_height[:, 1]
# convert (cx, cy, w, h) to (xmin, xmax, ymin, ymax)
# prepend one dimension to boxes
# to account for the batch size = 1
boxes = centroid2minmax(boxes)
boxes = np.expand_dims(boxes, axis=0)
return boxes
def centroid2minmax(boxes):
"""Centroid to minmax format
(cx, cy, w, h) to (xmin, xmax, ymin, ymax)
Arguments:
boxes (tensor): Batch of boxes in centroid format
Returns:
minmax (tensor): Batch of boxes in minmax format
"""
minmax= np.copy(boxes).astype(np.float)
minmax[..., 0] = boxes[..., 0] - (0.5 * boxes[..., 2])
minmax[..., 1] = boxes[..., 0] + (0.5 * boxes[..., 2])
minmax[..., 2] = boxes[..., 1] - (0.5 * boxes[..., 3])
minmax[..., 3] = boxes[..., 1] + (0.5 * boxes[..., 3])
return minmax
我们已经介绍了锚框如何协助对象检测以及如何生成它们。 在下一节中,我们将介绍一种特殊的锚点框:真实情况锚点框。 给定图像中的对象,必须将其分配给多个锚点框之一。 这就是,称为真实情况锚定框。
3. 真实情况锚框
从“图 11.2.3”看来,给定一个对象边界框,有许多可以分配给对象的真实情况锚定框。 实际上,仅出于“图 11.2.3”中的说明,已经有 3 个锚定框。 如果考虑每个区域的所有锚框,则仅针对(s[x4], s[y4]) = (1/3, 1/2)就有6 x 6 = 36个地面真实框。 使用所有 9,648 个锚点框显然过多。 所有锚定框中只有一个应与地面真值边界框相关联。 所有其他锚点框都是背景锚点框。 选择哪个对象应被视为图像中对象的真实情况锚定框的标准是什么?
选择锚框的基础称为交并比(IoU)。 IoU 也称为 Jaccard 指数。 在“图 11.3.1”中说明了 IoU。 给定 2 个区域,对象边界框B₀和锚定框A₁,IoU 等于重叠除以合并区域的面积:
(Equation 11.3.1)

图 11.3.1 IoU 等于(左)候选锚点框A₁与(右)对象边界框B₀之间的相交面积除以并集面积。
我们删除了该等式的下标。 对于给定的对象边界框Bᵢ,对于所有锚点框Aⱼ,地面真值锚点框A[j(gt)]是具有最大 IoU 的一个:
(Equation 11.3.2)
请注意,对于每个对象,只有一个基于“公式 11.3.2”的地面真值锚定框。 此外,必须在所有比例因子和尺寸(长宽比和附加尺寸)中对所有锚框进行最大化。 在“图 11.3.1”中,在 9,648 个锚框中仅显示了一个比例因子大小。
为了说明“公式 11.3.2”,假设考虑了“图 11.3.1”中纵横比为 1 的锚框。 对于每个锚框,估计的 IoU 均显示在“表 11.3.1”中。 由于边界框B₀的最大 IoU 为 0.32,因此带有锚框A₁,A₁被分配为地面真值边界框B₀。A₁也被称为正锚框。
正锚定框的类别和偏移量是相对于其地面真值边界框确定的。 正锚定框的类别与其地面真值边界框相同。 同时,可以将正锚框偏移量计算为等于地面真实边界框坐标减去其自身的边界框坐标。
其余锚框发生了什么,A₀,A₂,A₃,A₄,和A₅? 我们可以通过找到他们的 IoU 大于某个阈值的边界框来给他们第二次机会。
例如,如果阈值为 0.5,则没有可分配给它们的地面真理边界框。 如果阈值降低到 0.25,则A₄也分配有地面真值边界框B₀,因为其 IoU 为 0.30 。 将A₄添加到肯定锚框列表中。 在这本书中,A₄被称为额外的正面锚盒。 没有地面边界框的其余锚框称为负锚框。
在以下有关损失函数的部分中,负锚框不构成偏移损失函数。
B₀ |
|
|---|---|
A₀ |
0 |
A₁ |
0.32 |
A₂ |
0 |
A₃ |
0 |
A₄ |
0.30 |
A₅ |
0 |
“表 11.3.1”每个锚框A[j ∈ 0 .. 5]的 IoU,带有对象边界框B₀,如“图 11.3.1”所示。
如果加载了另一个带有 2 个要检测的对象的图像,我们将寻找 2 个正 IoU,最大 IoU,并带有边界框B₀和B₁。 然后,我们使用边界框B₀和B₁寻找满足最小 IoU 准则的额外正锚框。
为了简化讨论,我们只考虑每个区域一个锚框。 实际上,应该考虑代表不同缩放比例,大小和纵横比的所有锚框。 在下一节中,我们讨论如何制定损失函数,这些损失函数将通过 SSD 网络进行优化。
“列表 11.3.1”显示了get_gt_data()的实现,该实现计算锚定框的真实情况标签。
“列表 11.3.1”:layer_utils.py
def get_gt_data(iou,
n_classes=4,
anchors=None,
labels=None,
normalize=False,
threshold=0.6):
"""Retrieve ground truth class, bbox offset, and mask
Arguments:
iou (tensor): IoU of each bounding box wrt each anchor box
n_classes (int): Number of object classes
anchors (tensor): Anchor boxes per feature layer
labels (list): Ground truth labels
normalize (bool): If normalization should be applied
threshold (float): If less than 1.0, anchor boxes>threshold
are also part of positive anchor boxes
Returns:
gt_class, gt_offset, gt_mask (tensor): Ground truth classes,
offsets, and masks
"""
# each maxiou_per_get is index of anchor w/ max iou
# for the given ground truth bounding box
maxiou_per_gt = np.argmax(iou, axis=0)
# get extra anchor boxes based on IoU
if threshold < 1.0:
iou_gt_thresh = np.argwhere(iou>threshold)
if iou_gt_thresh.size > 0:
extra_anchors = iou_gt_thresh[:,0]
extra_classes = iou_gt_thresh[:,1]
extra_labels = labels[extra_classes]
indexes = [maxiou_per_gt, extra_anchors]
maxiou_per_gt = np.concatenate(indexes,
axis=0)
labels = np.concatenate([labels, extra_labels],
axis=0)
# mask generation
gt_mask = np.zeros((iou.shape[0], 4))
# only indexes maxiou_per_gt are valid bounding boxes
gt_mask[maxiou_per_gt] = 1.0
# class generation
gt_class = np.zeros((iou.shape[0], n_classes))
# by default all are background (index 0)
gt_class[:, 0] = 1
# but those that belong to maxiou_per_gt are not
gt_class[maxiou_per_gt, 0] = 0
# we have to find those column indexes (classes)
maxiou_col = np.reshape(maxiou_per_gt,
(maxiou_per_gt.shape[0], 1))
label_col = np.reshape(labels[:,4],
(labels.shape[0], 1)).astype(int)
row_col = np.append(maxiou_col, label_col, axis=1)
# the label of object in maxio_per_gt
gt_class[row_col[:,0], row_col[:,1]] = 1.0
# offsets generation
gt_offset = np.zeros((iou.shape[0], 4))
#(cx, cy, w, h) format
if normalize:
anchors = minmax2centroid(anchors)
labels = minmax2centroid(labels)
# bbox = bounding box
# ((bbox xcenter - anchor box xcenter)/anchor box width)/.1
# ((bbox ycenter - anchor box ycenter)/anchor box height)/.1
# Equation 11.4.8 Chapter 11
offsets1 = labels[:, 0:2] - anchors[maxiou_per_gt, 0:2]
offsets1 /= anchors[maxiou_per_gt, 2:4]
offsets1 /= 0.1
# log(bbox width / anchor box width) / 0.2
# log(bbox height / anchor box height) / 0.2
# Equation 11.4.8 Chapter 11
offsets2 = np.log(labels[:, 2:4]/anchors[maxiou_per_gt, 2:4])
offsets2 /= 0.2
offsets = np.concatenate([offsets1, offsets2], axis=-1)
# (xmin, xmax, ymin, ymax) format
else:
offsets = labels[:, 0:4] - anchors[maxiou_per_gt]
gt_offset[maxiou_per_gt] = offsets
return gt_class, gt_offset, gt_mask
def minmax2centroid(boxes):
"""Minmax to centroid format
(xmin, xmax, ymin, ymax) to (cx, cy, w, h)
Arguments:
boxes (tensor): Batch of boxes in minmax format
Returns:
centroid (tensor): Batch of boxes in centroid format
"""
centroid = np.copy(boxes).astype(np.float)
centroid[..., 0] = 0.5 * (boxes[..., 1] - boxes[..., 0])
centroid[..., 0] += boxes[..., 0]
centroid[..., 1] = 0.5 * (boxes[..., 3] - boxes[..., 2])
centroid[..., 1] += boxes[..., 2]
centroid[..., 2] = boxes[..., 1] - boxes[..., 0]
centroid[..., 3] = boxes[..., 3] - boxes[..., 2]
return centroid
maxiou_per_gt = np.argmax(iou, axis=0)实现了“公式 11.3.2”。 额外的阳性锚框是基于由iou_gt_thresh = np.argwhere(iou>threshold)实现的用户定义的阈值确定的。
仅当阈值小于 1.0 时,才会查找额外的正锚框。 所有带有地面真值边界框的锚框(即组合的正锚框和额外的正锚框)的索引成为真实情况掩码的基础:
gt_mask[maxiou_per_gt] = 1.0。
所有其他锚定框(负锚定框)的掩码为 0.0,并且不影响偏移损失函数的优化。
每个锚定框的类别gt_class被分配为其地面实况边界框的类别。 最初,为所有锚框分配背景类:
# class generation
gt_class = np.zeros((iou.shape[0], n_classes))
# by default all are background (index 0)
gt_class[:, 0] = 1
然后,将每个正面锚点框的类分配给其非背景对象类:
# but those that belong to maxiou_per_gt are not
gt_class[maxiou_per_gt, 0] = 0
# we have to find those column indexes (classes)
maxiou_col = np.reshape(maxiou_per_gt,
(maxiou_per_gt.shape[0], 1))
label_col = np.reshape(labels[:,4],
(labels.shape[0], 1)).astype(int)
row_col = np.append(maxiou_col, label_col, axis=1)
# the label of object in maxio_per_gt
gt_class[row_col[:,0], row_col[:,1]] = 1.0
row_col[:,0]是正锚框的索引,而row_col[:,1]是它们的非背景对象类的索引。 请注意,gt_class是单热点向量的数组。 这些值都为零,除了锚点框对象的索引处。 索引 0 是背景,索引 1 是第一个非背景对象,依此类推。 最后一个非背景对象的索引等于n_classes-1。
例如,如果锚点框 0 是负锚点框,并且有 4 个对象类别(包括背景),则:
gt_class[0] = [1.0, 0.0, 0.0, 0.0]
如果锚定框 1 是正锚定框,并且其地面真值边界框包含带有标签 2 的汽水罐,则:
gt_class[1] = [0.0, 0.0, 1.0, 0.0]
最后,偏移量只是地面真实边界框坐标减去锚框坐标:
# (xmin, xmax, ymin, ymax) format
else:
offsets = labels[:, 0:4] - anchors[maxiou_per_gt]
注意,我们仅计算正锚框的偏移量。
如果选择了该选项,则可以将偏移量标准化。 下一部分将讨论偏移量归一化。 我们将看到:
#(cx, cy, w, h) format
if normalize:
anchors = minmax2centroid(anchors)
labels = minmax2centroid(labels)
# bbox = bounding box
# ((bbox xcenter - anchor box xcenter)/anchor box width)/.1
# ((bbox ycenter - anchor box ycenter)/anchor box height)/.1
# Equation 11.4.8
offsets1 = labels[:, 0:2] - anchors[maxiou_per_gt, 0:2]
offsets1 /= anchors[maxiou_per_gt, 2:4]
offsets1 /= 0.1
# log(bbox width / anchor box width) / 0.2
# log(bbox height / anchor box height) / 0.2
# Equation 11.4.8
offsets2 = np.log(labels[:, 2:4]/anchors[maxiou_per_gt, 2:4])
offsets2 /= 0.2
offsets = np.concatenate([offsets1, offsets2], axis=-1)
只是“公式 11.4.8”的实现,下一节将进行讨论,为方便起见,在此处显示:
(Equation 11.4.8)
现在我们已经了解了地面真锚框的作用,我们将继续研究对象检测中的另一个关键组件:损失函数。
4. 损失函数
在 SSD 中,有数千个锚定框。 如本章前面所述,对象检测的目的是预测每个锚框的类别和偏移量。 我们可以对每个预测使用以下损失函数:
L_cls-y_cls的分类交叉熵损失L_off- L1 或 L2,用于y_cls。 请注意,只有正锚框有助于L_offL1,也称为平均绝对误差(MAE)损失,而 L2 也称为均方误差(MSE)损失。
总的损失函数为:
(Equation 11.4.1)
对于每个定位框,网络都会预测以下内容:
y_cls或单热向量形式的类别或类y_off = ((x_omin, y_omin), (x_omax, y_omax))或相对于锚框的像素坐标形式的偏移。
为了方便计算,可以将偏移量更好地表示为以下形式:
y_off = ((x_omin, y_omin), (x_omax, y_omax)) (Equation 11.4.2)
SSD 是一种监督对象检测算法。 可以使用以下基本真值:
y_label或要检测的每个对象的类标签y_gt = (x_gmin, x_gmax, y_gmin, y_gmax)或地面真实偏差,其计算公式如下:
y_gt = (x_bmin – x_amin, x_bmax – x_amax, y_bmin – y_amin, y_bmax – y_amax) (Equation 11.4.3)
换句话说,将地面真实偏移量计算为对象包围盒相对于锚定框的地面真实偏移量。 为了清楚起见,y_box下标中的细微调整。 如上一节所述,基本真值是通过get_gt_data()函数计算的。
但是,SSD 不建议直接从预测原始像素误差值y_off。 而是使用归一化的偏移值。 地面真值边界框和锚点框坐标首先以质心尺寸格式表示:


(Equation 11.4.4)
哪里:
(Equation 11.4.5)
是边界框中心的坐标,并且:
(w[b], h[b]) = (x_max – x_min, y_max - y_min) (Equation 11.4.6)
分别对应于宽度和高度。 锚框遵循相同的约定。 归一化的真实情况偏移量表示为:
(Equation 11.4.7)
通常,y_gt的元素值很小,||y_gt|| << 1.0。 较小的梯度会使网络训练更加难以收敛。
为了缓解该问题,将每个元素除以其估计的标准差。 由此产生的基本事实抵消了:
(Equation 11.4.8)
推荐值为:σ[x] = σ[y] = 0.1和σ[w] = σ[h] = 0.2。 换句话说,沿着x和y轴的像素误差的预期范围是± 10%,而对于宽度和高度,则是`± 20%。 这些值纯粹是任意的。
“列表 11.4.1”:loss.py L1 和平滑 L1 损失函数
from tensorflow.keras.losses import Huber
def mask_offset(y_true, y_pred):
"""Pre-process ground truth and prediction data"""
# 1st 4 are offsets
offset = y_true[..., 0:4]
# last 4 are mask
mask = y_true[..., 4:8]
# pred is actually duplicated for alignment
# either we get the 1st or last 4 offset pred
# and apply the mask
pred = y_pred[..., 0:4]
offset *= mask
pred *= mask
return offset, pred
def l1_loss(y_true, y_pred):
"""MAE or L1 loss
"""
offset, pred = mask_offset(y_true, y_pred)
# we can use L1
return K.mean(K.abs(pred - offset), axis=-1)
def smooth_l1_loss(y_true, y_pred):
"""Smooth L1 loss using tensorflow Huber loss
"""
offset, pred = mask_offset(y_true, y_pred)
# Huber loss as approx of smooth L1
return Huber()(offset, pred)
此外,代替y_cls的 L1 损失,SSD 受 Fast-RCNN [3]启发,使用平滑 L1:
(Equation 11.4.9)
其中u代表地面真实情况与预测之间的误差中的每个元素:
(Equation 11.4.10)
与 L1 相比,平滑 L1 更健壮,并且对异常值的敏感性较低。 在 SSD 中,σ = 1。 作为σ -> ∞,平滑 L1 接近 L1。 L1 和平滑 L1 损失函数都在“列表 11.4.1”中显示。 mask_offset()方法可确保仅在具有地面真实边界框的预测上计算偏移量。 平滑的 L1 函数与σ = 1[8]时的 Huber 损失相同。
作为对损失函数的进一步改进,RetinaNet [3]建议将 CEy_cls的分类交叉熵函数替换为焦点损失 FL:
(Equation 11.4.11)
(Equation 11.4.12)
区别在于额外因素α(1 - p[i])^γ。 在 RetinaNet 中,当γ = 2和α = 0.25时,对象检测效果最好。 焦点损失在“列表 11.4.2”中实现。
“列表 11.4.2”:loss.py焦点损失
def focal_loss_categorical(y_true, y_pred):
"""Categorical cross-entropy focal loss"""
gamma = 2.0
alpha = 0.25
# scale to ensure sum of prob is 1.0
y_pred /= K.sum(y_pred, axis=-1, keepdims=True)
# clip the prediction value to prevent NaN and Inf
epsilon = K.epsilon()
y_pred = K.clip(y_pred, epsilon, 1\. - epsilon)
# calculate cross entropy
cross_entropy = -y_true * K.log(y_pred)
# calculate focal loss
weight = alpha * K.pow(1 - y_pred, gamma)
cross_entropy *= weight
return K.sum(cross_entropy, axis=-1)
聚焦损失的动机是,如果我们检查图像,则大多数锚框应分类为背景或负锚框。 只有很少的正锚框是代表目标对象的良好候选对象。 负熵损失是造成交叉熵损失的主要因素。 因此,负锚框的贡献使优化过程中正锚框的贡献无法实现。 这种现象也称为类不平衡,其中一个或几个类占主导地位。 有关其他详细信息,Lin 等。 文献[4]讨论了对象检测中的类不平衡问题。
有了焦点损失,我们在优化过程的早期就确信负锚框属于背景。 因此,由于p[i] -> 1.0,项(1 - p[i])^γ减少了负锚框的贡献。 对于正锚框,其贡献仍然很大,因为pᵢ远非 1.0。
既然我们已经讨论了锚定框,地面真值锚定框和损失函数的概念,我们现在准备介绍实现多尺度目标检测算法的 SSD 模型架构。
5. SSD 模型架构
“图 11.5.1”显示了 SSD 的模型架构,该模型实现了多尺度单发目标检测的概念框架。 网络接受 RGB 图像,并输出几个预测级别。 基本或骨干网络提取用于分类和偏移量预测的下游任务的特征。 ResNet50 是骨干网络的一个很好的例子,它类似于“第 2 章”,“深度神经网络”中讨论,实现和评估的内容。 在骨干网络之后,对象检测任务由执行其余的网络,我们将其称为 SSD 头。
骨干网络可以是具有冻结权重的预训练网络(例如,以前为 ImageNet 分类而训练),也可以是与对象检测一起训练的网络。 如果使用预先训练的基础网络,则可以利用重用以前从大型数据集中学习的特征提取过滤器的优势。 此外,由于冻结了骨干网参数,因此可以加快学习速度。 仅训练对象检测中的顶层。 在这本书中,骨干网是与对象检测联合训练的,因为我们假设我们不一定需要访问预先训练的骨干网。
骨干网网络通常使用跨步 2 或通过最大池化实现几轮下采样。 对于 ResNet50,这是 4 倍。 基本网络变为(w/2^4, h/2^4) = (w/16, h/16)之后,特征映射的结果尺寸。 如果图像的宽度和高度均可以被 16 整除,则尺寸是精确的。
例如,对于640 x 480的图像,生成的特征映射的尺寸为40 x 30 = 1200。 如前几节所述,这是基础网络之后长宽比等于 1 的锚点框的数量。 此数字乘以每个锚定框的大小数。 在前面的部分中,由于长宽比,有 6 种不同的尺寸,而长宽比为 1 时,还有一个其他尺寸。
在本书中,我们将纵横比限制为a[i ∈ {0, 1, 3}] = 1, 2, 1/2。 因此,将只有 4 种不同的大小。 对于640 x 480图像,第一组锚框的锚框总数为n[1] = 4,800。
在“图 11.5.1”中,指示密集网格以表明对于第一组预测,存在大量预测(例如:40 x 30 x 4),从而导致大量补丁 。 尽管每个锚点框有 4 种尺寸,但为清楚起见,仅显示了与宽高比 1 对应的16 x 16锚点框。
此锚框也是40 x 30 x n_filter特征映射中每个元素的接受字段大小。n_filter是骨干网最后卷积层中过滤器的数量。 对于每个锚框,都将预测类别和偏移量。
总共有n₁类和n₁偏移量预测。 单热类预测的维数等于要检测的对象类别的数量,背景为 1。 每个偏移量变量预测的尺寸为 4,对应于(x, y)到预测边界框的 2 个角的偏移量。
类预测器由卷积层组成,该卷积层由使用 softmax 进行分类交叉熵损失的激活层终止。 偏移量预测值是具有线性激活的独立卷积层。
在基础网络之后可以应用其他特征提取模块。 每个特征提取器块都是Conv2D(strides=2)-BN-ELU的形式。 在特征提取块之后,特征映射的大小减半,并且过滤器的数量增加一倍。 例如,基本网络之后的第一个特征提取器块具有20 x 15 x 2 n_filter特征映射。 根据该特征映射,使用卷积层进行n₂类和n₂偏移量预测。n[2] = 20 x 15 x 4 = 1,200
可以继续添加具有类和偏移量预测变量的特征提取块的过程。 在前面的部分中,对于640 x 480的图像,最大可达2 x 1 x 2^5 n_filter特征映射产生n₆类和n₆抵消了其中n[6] = 2 x 1 x 4 = 8的预测。 到 6 层特征提取和预测块。 在第 6 个块之后,一个640 x 480图像的锚点映射预测总数为 9,648。
在前面的部分中,锚定框的比例因子大小按降序排列:
Equation 11.5.1)
这样做是为了使讨论清晰。 在本节中,应该意识到比例因子的大小实际上是从骨干网之后的特征映射大小开始的。 实际上,缩放因子应按升序排列:
(Equation 11.5.2)
这意味着如果将特征提取块的数量减少到 4,则缩放因子为:
(Equation 11.5.3)
如果特征映射的宽度或高度不能被 2 整除(例如:15),则将应用天花板函数(例如:ceil(15/2) = 8)。 但是,在原始的 SSD [2]实现中,所使用的缩放因子被简化为[0.2, 0.9]范围,该范围通过缩放因子的数量或特征提取块的数量n_layers进行线性缩放:
s = np.linspace(0.2, 0.9, n_layers + 1)

图 11.5.1 SSD 模型架构。请注意,对于w/16 x h/16网格,锚框的数量可能不准确。 网格显示了锚框的紧密包装。
讨论了 SSD 模型架构之后,现在让我们看一下如何在 Keras 中实现 SSD 模型架构。
6. Keras 中的 SSD 模型架构
与前面章节中的代码示例不同,SSD 的tf.keras实现更加复杂。 与 SSD 的其他tf.keras实现相比,本章中提供的代码示例重点介绍多尺度目标检测的关键概念。 可以进一步优化代码实现的某些部分,例如缓存地面真锚框类,偏移量和掩码。 在我们的示例中,每次从文件系统加载图像时,线程都会计算出地面真实值。
“图 11.6.1”显示了包含 SSD 的tf.keras实现的代码块的概述。 ssd-11.6.1.py中的 SSD 对象可以构建,训练和评估 SSD 模型。 它借助model.py和resnet.py以及data_generator.py中的多线程数据生成器,位于 SSD 模型创建器的顶部。 SSD 模型实现了“图 11.5.1”中所示的 SSD 架构。 每个主要模块的实现将在后续部分中详细讨论。
SSD 模型使用 ResNet 作为其骨干网络。 它在resnet.py中调用 ResNet V1 或 V2 模型创建者。 与前几章中的示例不同,SSD 使用的数据集由数千个高分辨率图像组成。 多线程数据生成器将加载文件,并且将这些文件从文件系统排队。 它还计算锚点箱的地面真值标签。 如果没有多线程数据生成器,则在训练期间图像的加载和排队以及地面真值的计算将非常缓慢。
有许多小的但重要的例程在后台运行。 这些都集中存储在工具块中。 这些例程创建锚框,计算 IoU,建立真实情况标签,运行非最大抑制,绘制标签和框,在视频帧上显示检测到的对象,提供损失函数等。

图 11.6.1 实现 SSD 的代码块。
7. Keras 中的 SSD 对象
“列表 11.7.1”(很快显示)显示了 SSD 类。 说明了两个主要例程:
-
使用
build_model()创建 SSD 模型 -
通过
build_generator()实例化数据生成器
build_model首先根据训练标签创建数据字典。 字典存储图像文件名以及每个图像中每个对象的地面真实边界框坐标和类。 之后,构建骨干网和 SSD 网络模型。 模型创建的最重要产品是self.ssd – SSD 的网络模型。
标签存储在 csv 文件中。 对于本书中使用的示例训练图像,标签以以下格式保存在dataset/drinks/labels_train.csv中:
frame,xmin,xmax,ymin,ymax,class_id
0001000.jpg,310,445,104,443,1
0000999.jpg,194,354,96,478,1
0000998.jpg,105,383,134,244,1
0000997.jpg,157,493,89,194,1
0000996.jpg,51,435,207,347,1
0000995.jpg,183,536,156,283,1
0000994.jpg,156,392,178,266,2
0000993.jpg,207,449,119,213,2
0000992.jpg,47,348,213,346,2
…
“列表 11.7.1”:ssd-11.6.1.py
class SSD:
"""Made of an ssd network model and a dataset generator.
SSD defines functions to train and validate
an ssd network model.
Arguments:
args: User-defined configurations
Attributes:
ssd (model): SSD network model
train_generator: Multi-threaded data generator for training
"""
def __init__(self, args):
"""Copy user-defined configs.
Build backbone and ssd network models.
"""
self.args = args
self.ssd = None
self.train_generator = None
self.build_model()
def build_model(self):
"""Build backbone and SSD models."""
# store in a dictionary the list of image files and labels
self.build_dictionary()
# input shape is (480, 640, 3) by default
self.input_shape = (self.args.height,
self.args.width,
self.args.channels)
# build the backbone network (eg ResNet50)
# the number of feature layers is equal to n_layers
# feature layers are inputs to SSD network heads
# for class and offsets predictions
self.backbone = self.args.backbone(self.input_shape,
n_layers=self.args.layers)
# using the backbone, build ssd network
# outputs of ssd are class and offsets predictions
anchors, features, ssd = build_ssd(self.input_shape,
self.backbone,
n_layers=self.args.layers,
n_classes=self.n_classes)
# n_anchors = num of anchors per feature point (eg 4)
self.n_anchors = anchors
# feature_shapes is a list of feature map shapes
# per output layer - used for computing anchor boxes sizes
self.feature_shapes = features
# ssd network model
self.ssd = ssd
def build_dictionary(self):
"""Read input image filenames and obj detection labels
from a csv file and store in a dictionary.
"""
# train dataset path
path = os.path.join(self.args.data_path,
self.args.train_labels)
# build dictionary:
# key=image filaname, value=box coords + class label
# self.classes is a list of class labels
self.dictionary, self.classes = build_label_dictionary(path)
self.n_classes = len(self.classes)
self.keys = np.array(list(self.dictionary.keys()))
def build_generator(self):
"""Build a multi-thread train data generator."""
self.train_generator = \
DataGenerator(args=self.args,
dictionary=self.dictionary,
n_classes=self.n_classes,
feature_shapes=self.feature_shapes,
n_anchors=self.n_anchors,
shuffle=True)
“列表 11.7.2”显示了 SSD 对象中的另一种重要方法train()。 指示了使用默认损失函数或改进的损失函数的选项,如先前部分所述。 还有一个选项可以选择仅平滑 L1。
self.ssd.fit_generator()是此函数中最重要的调用。 它借助多线程数据生成器启动有监督的训练。 在每个周期,都会执行两个回调函数。 首先,将模型权重保存到文件中。 然后,对于 ResNet 模型,以与“第 2 章”,“深度神经网络”相同的方式使用的改进的学习率调度器称为:
“列表 11.7.2”:ssd-11.6.1.py
def train(self):
"""Train an ssd network."""
# build the train data generator
if self.train_generator is None:
self.build_generator()
optimizer = Adam(lr=1e-3)
# choice of loss functions via args
if self.args.improved_loss:
print_log("Focal loss and smooth L1", self.args.verbose)
loss = [focal_loss_categorical, smooth_l1_loss]
elif self.args.smooth_l1:
print_log("Smooth L1", self.args.verbose)
loss = ['categorical_crossentropy', smooth_l1_loss]
else:
print_log("Cross-entropy and L1", self.args.verbose)
loss = ['categorical_crossentropy', l1_loss]
self.ssd.compile(optimizer=optimizer, loss=loss)
# prepare callbacks for saving model weights
# and learning rate scheduler
# learning rate decreases by 50% every 20 epochs
# after 60th epoch
checkpoint = ModelCheckpoint(filepath=filepath,
verbose=1,
save_weights_only=True)
scheduler = LearningRateScheduler(lr_scheduler)
callbacks = [checkpoint, scheduler]
# train the ssd network
self.ssd.fit_generator(generator=self.train_generator,
use_multiprocessing=True,
callbacks=callbacks,
epochs=self.args.epochs,
workers=self.args.workers)
在下一部分中,我们将讨论 Keras 中 SSD 架构实现的其他详细信息。 特别是 SSD 模型和多线程数据生成器的实现。
8. Keras 中的 SSD 模型
“列表 11.8.1”显示了 SSD 模型创建函数build_ssd()。 该模型在“图 11.5.1”中进行了说明。 该函数通过调用base_outputs = backbone(inputs)从骨干网或基础网络检索输出特征的n_layers。
在本书中,backbone()是build_resnet()。 build_resnet()可以生成的 ResNet 模型类似于“第 2 章”,“深度神经网络”中讨论的残差网络。 build_resnet()函数可以由构建基础网络的任何函数名称代替。
如图“图 11.5.1”所示,返回值base_outputs是输出特征的列表,这些特征将作为类别和偏移预测层的输入。 例如,第一输出base_outputs[0]用于生成n₁类预测和n₁偏移量预测。
在build_ssd()的for循环中,类别预测是classes变量,而偏移量预测是offsets变量。 在for循环迭代之后,将类别预测连接,并最终合并为一个具有以下尺寸的classes变量:

对offsets变量执行相同的过程。 结果尺寸为:

其中n_mini_batch是迷你批量大小,n_anchor_box是锚定框的数量。 for循环迭代的次数等于n_layers。 该数目也等于锚定框缩放因子的所需数目或 SSD 头的特征提取块的数目。
函数build_ssd()返回每个特征点或区域的锚框数量,每个前类的特征形状,偏移量预测层以及 SSD 模型本身。
“列表 11.8.1”:model.py
def build_ssd(input_shape,
backbone,
n_layers=4,
n_classes=4,
aspect_ratios=(1, 2, 0.5)):
"""Build SSD model given a backbone
Arguments:
input_shape (list): input image shape
backbone (model): Keras backbone model
n_layers (int): Number of layers of ssd head
n_classes (int): Number of obj classes
aspect_ratios (list): annchor box aspect ratios
Returns:
n_anchors (int): Number of anchor boxes per feature pt
feature_shape (tensor): SSD head feature maps
model (Keras model): SSD model
"""
# number of anchor boxes per feature map pt
n_anchors = len(aspect_ratios) + 1
inputs = Input(shape=input_shape)
# no. of base_outputs depends on n_layers
base_outputs = backbone(inputs)
outputs = []
feature_shapes = []
out_cls = []
out_off = []
for i in range(n_layers):
# each conv layer from backbone is used
# as feature maps for class and offset predictions
# also known as multi-scale predictions
conv = base_outputs if n_layers==1 else base_outputs[i]
name = "cls" + str(i+1)
classes = conv2d(conv,
n_anchors*n_classes,
kernel_size=3,
name=name)
# offsets: (batch, height, width, n_anchors * 4)
name = "off" + str(i+1)
offsets = conv2d(conv,
n_anchors*4,
kernel_size=3,
name=name)
shape = np.array(K.int_shape(offsets))[1:]
feature_shapes.append(shape)
# reshape the class predictions, yielding 3D tensors of
# shape (batch, height * width * n_anchors, n_classes)
# last axis to perform softmax on them
name = "cls_res" + str(i+1)
classes = Reshape((-1, n_classes),
name=name)(classes)
# reshape the offset predictions, yielding 3D tensors of
# shape (batch, height * width * n_anchors, 4)
# last axis to compute the (smooth) L1 or L2 loss
name = "off_res" + str(i+1)
offsets = Reshape((-1, 4),
name=name)(offsets)
# concat for alignment with ground truth size
# made of ground truth offsets and mask of same dim
# needed during loss computation
offsets = [offsets, offsets]
name = "off_cat" + str(i+1)
offsets = Concatenate(axis=-1,
name=name)(offsets)
# collect offset prediction per scale
out_off.append(offsets)
name = "cls_out" + str(i+1)
#activation = 'sigmoid' if n_classes==1 else 'softmax'
#print("Activation:", activation)
classes = Activation('softmax',
name=name)(classes)
# collect class prediction per scale
out_cls.append(classes)
if n_layers > 1:
# concat all class and offset from each scale
name = "offsets"
offsets = Concatenate(axis=1,
name=name)(out_off)
name = "classes"
classes = Concatenate(axis=1,
name=name)(out_cls)
else:
offsets = out_off[0]
classes = out_cls[0]
outputs = [classes, offsets]
model = Model(inputs=inputs,
outputs=outputs,
name='ssd_head')
return n_anchors, feature_shapes, model
如前面所述,与 MNIST 和 CIFAR-10 等小型数据集不同,SSD 中使用的映像很大。 因此,不可能将图像加载到张量变量中。 在下一节中,我们将介绍一个多线程数据生成器,该生成器将使我们能够从文件系统并发加载图像,并避免内存瓶颈。
9. Keras 中的数据生成器模型
SSD 需要大量带标签的高分辨率图像来进行对象检测。 与之前的章节中使用的数据集可以加载到到内存中以训练模型不同,SSD 实现了多线程数据生成器。 多线程生成器的任务是加载图像的多个迷你批量及其相应的标签。 由于具有多线程,GPU 可以保持繁忙,因为一个线程向其提供数据,而其余的 CPU 线程处于队列中,准备从文件系统中馈入另一批数据或加载一批图像并计算基本真值 。“列表 11.9.1”显示了 Keras 中的数据生成器模型。
DataGenerator类继承自 Keras 的Sequence类,以确保它支持多处理。 DataGenerator保证在一个周期内使用整个数据集。
给定批量大小的整个周期的长度由__len__()方法返回。 对小批量数据的每个请求都可以通过__getitem__()方法来满足。 在每个周期之后,如果self.shuffle为True,则调用on_epoch_end()方法以随机播放整个批量。
“列表 11.9.1”:data_generator.py
class DataGenerator(Sequence):
"""Multi-threaded data generator.
Each thread reads a batch of images and their object labels
Arguments:
args: User-defined configuration
dictionary: Dictionary of image filenames and object labels
n_classes (int): Number of object classes
feature_shapes (tensor): Shapes of ssd head feature maps
n_anchors (int): Number of anchor boxes per feature map pt
shuffle (Bool): If dataset should be shuffled bef sampling
"""
def __init__(self,
args,
dictionary,
n_classes,
feature_shapes=[],
n_anchors=4,
shuffle=True):
self.args = args
self.dictionary = dictionary
self.n_classes = n_classes
self.keys = np.array(list(self.dictionary.keys()))
self.input_shape = (args.height,
args.width,
args.channels)
self.feature_shapes = feature_shapes
self.n_anchors = n_anchors
self.shuffle = shuffle
self.on_epoch_end()
self.get_n_boxes()
def __len__(self):
"""Number of batches per epoch"""
blen = np.floor(len(self.dictionary) / self.args.batch_size)
return int(blen)
def __getitem__(self, index):
"""Get a batch of data"""
start_index = index * self.args.batch_size
end_index = (index+1) * self.args.batch_size
keys = self.keys[start_index: end_index]
x, y = self.__data_generation(keys)
return x, y
def on_epoch_end(self):
"""Shuffle after each epoch"""
if self.shuffle == True:
np.random.shuffle(self.keys)
def get_n_boxes(self):
"""Total number of bounding boxes"""
self.n_boxes = 0
for shape in self.feature_shapes:
self.n_boxes += np.prod(shape) // self.n_anchors
return self.n_boxes
数据生成器的大部分工作都是通过__data_generation()方法完成的,如“列表 11.9.2”所示。 给定一个小批量,该方法执行:
imread()从文件系统读取图像。labels = self.dictionary[key]访问词典中存储的边界框和类标签。 前四个项目是边界框偏移量。 最后一个是类标签。anchor_boxes()生成锚框。iou()计算相对于地面真值边界框的每个锚点框的 IoU。get_gt_data()为每个锚框分配地面真实等级和偏移量。
样本数据扩充函数也包括在内,但此处不再讨论,例如添加随机噪声,强度重新缩放和曝光调整。 __data_generation()返回输入x和输出y对,其中张量x存储输入图像,而张量y捆绑类,偏移量 ,和面具一起。
“列表 11.9.2”:data_generator.py
import layer_utils
from skimage.io import imread
def __data_generation(self, keys):
"""Generate train data: images and
object detection ground truth labels
Arguments:
keys (array): Randomly sampled keys
(key is image filename)
Returns:
x (tensor): Batch images
y (tensor): Batch classes, offsets, and masks
"""
# train input data
x = np.zeros((self.args.batch_size, *self.input_shape))
dim = (self.args.batch_size, self.n_boxes, self.n_classes)
# class ground truth
gt_class = np.zeros(dim)
dim = (self.args.batch_size, self.n_boxes, 4)
# offsets ground truth
gt_offset = np.zeros(dim)
# masks of valid bounding boxes
gt_mask = np.zeros(dim)
for i, key in enumerate(keys):
# images are assumed to be stored in self.args.data_path
# key is the image filename
image_path = os.path.join(self.args.data_path, key)
image = skimage.img_as_float(imread(image_path))
# assign image to a batch index
x[i] = image
# a label entry is made of 4-dim bounding box coords
# and 1-dim class label
labels = self.dictionary[key]
labels = np.array(labels)
# 4 bounding box coords are 1st four items of labels
# last item is object class label
boxes = labels[:,0:-1]
for index, feature_shape in enumerate(self.feature_shapes):
# generate anchor boxes
anchors = anchor_boxes(feature_shape,
image.shape,
index=index,
n_layers=self.args.layers)
# each feature layer has a row of anchor boxes
anchors = np.reshape(anchors, [-1, 4])
# compute IoU of each anchor box
# with respect to each bounding boxes
iou = layer_utils.iou(anchors, boxes)
# generate ground truth class, offsets & mask
gt = get_gt_data(iou,
n_classes=self.n_classes,
anchors=anchors,
labels=labels,
normalize=self.args.normalize,
threshold=self.args.threshold)
gt_cls, gt_off, gt_msk = gt
if index == 0:
cls = np.array(gt_cls)
off = np.array(gt_off)
msk = np.array(gt_msk)
else:
cls = np.append(cls, gt_cls, axis=0)
off = np.append(off, gt_off, axis=0)
msk = np.append(msk, gt_msk, axis=0)
gt_class[i] = cls
gt_offset[i] = off
gt_mask[i] = msk
y = [gt_class, np.concatenate((gt_offset, gt_mask), axis=-1)]
return x, y
现在我们有一个多线程生成器,我们可以用它来从文件系统加载图像。 在下一节中,我们将演示如何通过拍摄目标对象的图像并对其进行标记来构建自定义数据集。
10. 示例数据集
使用便宜的 USB 相机(A4TECH PK-635G)收集了一个由 1,000 640 X 480 RGB 训练图像和 50 640 X 480 RGB 测试图像组成的小型数据集。 使用 VGG 图像标注器(VIA)[5]标记数据集图像,以检测三个对象:1)汽水罐,2)果汁罐和 3)水瓶。“图 11.10.1”显示了标记过程的示例 UI。
可以在GitHub存储库的utils/video_capture.py中找到用于收集图像的工具脚本。 该脚本每 5 秒自动捕获一次图像,因此可以加快数据收集过程。

图 11.10.1 使用 VGG 图像标注器(VIA)进行数据集标记的过程
数据收集和标记是一项耗时的活动。 在行业中,通常将其外包给第三方标注公司。 使用自动数据标记软件是加快数据标记任务的另一种选择。
有了这个示例数据集,我们现在可以训练我们的对象检测网络。
11. SSD 模型训练
可以从以下链接下载包含 csv 格式标签的 train 和测试数据集。
在顶层文件夹(即“第 11 章”,“对象检测”)中,创建数据集文件夹,将下载的文件复制到此处,然后运行以下命令将其解压缩:
mkdir dataset
cp drinks.tar.gz dataset
cd dataset
tar zxvf drinks.tar.gz
cd..
通过执行以下步骤,将 SSD 模型训练 200 个周期:
python3 ssd-11.6.1.py --train
可以根据 GPU 内存调整默认的批量大小--batch-size=4。 在 1080Ti 上,批量大小为 2。在 32GB V100 上,每个 GPU 可以为 4 或 8。 --train代表模型训练选项。
为了支持边界框偏移量的归一化,包含--normalize选项。 为了使用改进的损失函数,添加了--improved_loss选项。 如果仅需要平滑的 L1(无焦点损失),请使用–smooth-l1。 为了显示:
- L1,无规范化:
python3 ssd-11.1.1.py –-train
- 改进的损失函数,无规范化:
python3 ssd-11.1.1.py –-train --improved-loss
- 改进的损失函数,具有规范化:
python3 ssd-11.1.1.py –-train –improved-loss --normalize
- 平滑 L1,具有规范化:
python3 ssd-11.1.1.py –-train –-smooth-l1 --normalize
训练完 SSD 网络之后,我们需要解决另一个问题。 我们如何处理给定对象的多个预测? 在测试训练好的模型之前,我们将首先讨论非最大抑制(NMS)算法。
12. 非最大抑制(NMS)算法
模型训练完成后,网络将预测边界框偏移量和相应的类别。 在某些情况下,两个或更多边界框引用同一对象,从而创建冗余预测。 图 11.12.1 中的汽水罐表示了这种情况。 为了删除多余的预测,将调用 NMS 算法。 本书涵盖了经典 NMS 和软 NMS [6],如“算法 11.12.1”中所示。 两种算法都假定边界框和相应的置信度得分或概率是已知的。

图 11.12.1 网络预测了汽水罐对象的两个重叠边界框。 只选择一个有效的边界框,即得分为 0.99 的边界框。
在经典 NMS 中,基于概率选择最终边界框,并将其存储在列表D中,并带有相应的分数S。 所有边界框和相应的概率都存储在初始列表B和P中。 在第 3 行和第 4 行中,将具有最高分数p[m]的边界框用作参考,b[m]。
参考边界框被添加到最终选择的边界框D的列表中,并从列表B中删除,如第 5 行所示。 并且列表S从P中删除。 对于其余边界框,如果 IoU 与b[m]大于或等于设置的阈值Nₜ,将其从B中删除。 其相应的分数也从P中删除。
步骤在第 6 和 9-11 行中显示。 这些步骤将删除所有分数较小的冗余边界框。 在检查完所有其余的边界框之后,重复从第 3 行开始的过程。 该过程继续进行,直到边界框B的列表为空。 该算法返回选定的边界框D和相应的分数S。
经典 NMS 的问题是边界盒包含另一个对象,但其中的 IoU 和b[m]会从列表中删除。 Soft NMS [6]提出,与其从列表中彻底删除,不如以b[m],如第 8 行所示。
重叠的边界框具有第二次机会。 IoU 较小的边界框在将来的迭代中具有更高的生存机会。 在将来的选择中,它实际上可能证明它包含一个与b[m]不同的对象。 如“算法 11.12.1”中所示,Soft NMS 是传统 NMS 的便捷替代。 无需重新训练 SSD 网络。 与经典 NMS 相比,Soft NMS 具有更高的平均精度。
“列表 11.12.1”说明了经典 NMS 和软 NMS。 除了最终的边界框和相应的分数外,还返回相应的对象。 当其余边界框的最大分数小于某个阈值(例如:0.2)时,该代码将实现 NMS 的提前终止。
“算法 11.12.1”NMS 和软 NMS
要求:边界框预测:B = {b[1], b[2], …, b[n]}
要求:边界框类别的置信度或分数:B = {b[1], b[2], …, b[n]}
要求:最小 NMS IoU 阈值:Nₜ
-
D <- {};S <- {} -
当
B ≠ empty,执行 -
m <- argmax P -
M <- b[m];N <- p[m], -
D <- D ∪ M;B <- B - M;S <- S ∪ N;P <- P - N; -
对于步骤
b[i] ∈ B,执行 -
如果
soft_NMS = True -
p[i] = p[i] exp(-IOU(M, b[i])^2 / σ) -
否则如果
IOU(M, b[i]) >= N[t],那么 -
B = B - b[i];P = P - p[i] -
结束
-
结束
-
结束
-
返回
D, S
“列表 11.12.1”:boxes.py
def nms(args, classes, offsets, anchors):
"""Perform NMS (Algorithm 11.12.1).
Arguments:
args: User-defined configurations
classes (tensor): Predicted classes
offsets (tensor): Predicted offsets
Returns:
objects (tensor): class predictions per anchor
indexes (tensor): indexes of detected objects
filtered by NMS
scores (tensor): array of detected objects scores
filtered by NMS
"""
# get all non-zero (non-background) objects
objects = np.argmax(classes, axis=1)
# non-zero indexes are not background
nonbg = np.nonzero(objects)[0]
# D and S indexes in Line 1
indexes = []
while True:
# list of zero probability values
scores = np.zeros((classes.shape[0],))
# set probability values of non-background
scores[nonbg] = np.amax(classes[nonbg], axis=1)
# max probability given the list
# Lines 3 and 4
score_idx = np.argmax(scores, axis=0)
score_max = scores[score_idx]
# get all non max probability & set it as new nonbg
# Line 5
nonbg = nonbg[nonbg != score_idx]
# if max obj probability is less than threshold (def 0.8)
if score_max < args.class_threshold:
# we are done
break
# Line 5
indexes.append(score_idx)
score_anc = anchors[score_idx]
score_off = offsets[score_idx][0:4]
score_box = score_anc + score_off
score_box = np.expand_dims(score_box, axis=0)
nonbg_copy = np.copy(nonbg)
# get all overlapping predictions (Line 6)
# perform Non-Max Suppression (NMS)
for idx in nonbg_copy:
anchor = anchors[idx]
offset = offsets[idx][0:4]
box = anchor + offset
box = np.expand_dims(box, axis=0)
iou = layer_utils.iou(box, score_box)[0][0]
# if soft NMS is chosen (Line 7)
if args.soft_nms:
# adjust score: Line 8
iou = -2 * iou * iou
classes[idx] *= math.exp(iou)
# else NMS (Line 9), (iou threshold def 0.2)
elif iou >= args.iou_threshold:
# remove overlapping predictions with iou>threshold
# Line 10
nonbg = nonbg[nonbg != idx]
# Line 2, nothing else to process
if nonbg.size == 0:
break
# get the array of object scores
scores = np.zeros((classes.shape[0],))
scores[indexes] = np.amax(classes[indexes], axis=1)
return objects, indexes, scores
假设我们具有训练有素的 SSD 网络和一种抑制冗余预测的方法,则下一节将讨论对测试数据集的验证。 基本上,我们想知道我们的 SSD 是否可以对从未见过的图像执行对象检测。
13. SSD 模型验证
在对 SSD 模型进行 200 个周期的训练之后,可以验证表现。 用于评估的三个可能指标:1)IoU,2)精度和 3)召回。
第一个指标是平均 IoU(mIoU)。 给定真实情况测试数据集,计算真实情况边界框和预测边界框之间的 IoU。 在执行 NMS 之后,对所有真实情况和预测的边界框执行此操作。 所有 IoU 的平均值计算为 mIoU:
(Equation 11.13.1)
其中n_box是地面真值边界框bᵢ的数量和n_pred是预测边界框dⱼ的数量。 请注意,该度量标准无法验证两个重叠的边界框是否属于同一类。 如果需要,则可以轻松修改代码。“列表 11.13.1”显示了代码实现。
第二个度量是精度,如“公式 11.3.2”所示。 它是正确预测的对象类别的数量(真阳性或 TP)除以正确预测的对象类别的数量(真阳性或 TP)与错误预测的对象类别的数量(假阳性或 FP)之和。 精度是衡量 SSD 正确识别图像中对象的表现的指标。 精度越接近 1.0 越好。
(Equation 11.3.2)
第三个度量是召回,如“公式 11.3.3”所示。 它是正确预测的对象类别的数量(真阳性或 TP)除以正确预测的对象类别的数量(真阳性或 TP)加上错过的对象数量(假阴性或 FN)之和。 召回率是衡量 SSD 在不对图像中的对象进行错误分类方面有多出色的度量。 召回率越接近 1.0,则越好。
(Equation 11.3.3)
如果我们对测试数据集中的所有图像取均值,则它们称为平均精度和平均召回率。 在目标检测中,使用不同 mIoU 的精度和召回曲线来衡量表现。 为了简单起见,我们仅针对特定类别阈值(默认值为 0.5)计算这些指标的值。 感兴趣的读者可以参考 Pascal VOC [7]文章,以获取有关对象检测指标的更多详细信息。
评价结果示于“表 11.13.1”。 结果可以通过运行:
- 无规范化:
python3 ssd-11.6.1.py --restore-weights=ResNet56v2-4layer-extra_anchors-drinks-200.h5 --evaluate
- 无规范化,平滑 L1:
python3 ssd-11.6.1.py --restore-weights=ResNet56v2-4layer-smooth_l1-extra_anchors-drinks-200.h5 --evaluate
- 具有规范化:
python3 ssd-11.6.1.py --restore-weights=ResNet56v2-4layer-norm-extra_anchors-drinks-200.h5 --evaluate --normalize
- 具有规范化,平滑 L1:
python3 ssd-11.6.1.py --restore-weights=ResNet56v2-4layer-norm-smooth_l1-extra_anchors-drinks-200.h5 --evaluate --normalize
- 具有规范化,平滑 L1,焦点损失:
python3 ssd-11.6.1.py --restore-weights=ResNet56v2-4layer-norm-improved_loss-extra_anchors-drinks-200.h5 --evaluate --normalize
权重在 GitHub 上可用。
在 mIoU 上,最佳表现是非归一化偏移选项,而归一化偏移设置具有最高的平均精度和召回率。 考虑到训练数据集中只有 1,000 张图像,表现并不是最新技术。 也没有应用数据扩充。
从结果来看,使用损失函数的改进会降低表现。 使用平滑 L1 或焦距损失函数或同时使用两者时,会发生这种情况。“图 11.13.1”至“图 11.13.5”显示了样本预测。 可以通过执行以下操作获得图像上的对象检测:
python3 ssd-11.6.1.py –-restore-weights=<weights_file>
--image-file=<target_image_file> --evaluate
例如,要在dataset/drinks/0010050.jpg上运行对象检测:
python3 ssd-11.6.1.py --restore-weights=ResNet56v2-4layer-extra_anchors-drinks-200.h5 --image-file=dataset/drinks/0010050.jpg --evaluate
如果模型权重文件名中包含单词norm,请附加--normalize option。
“列表 11.13.1”:ssd-11.6.1.py
def evaluate_test(self):
# test labels csv path
path = os.path.join(self.args.data_path,
self.args.test_labels)
# test dictionary
dictionary, _ = build_label_dictionary(path)
keys = np.array(list(dictionary.keys()))
# sum of precision
s_precision = 0
# sum of recall
s_recall = 0
# sum of IoUs
s_iou = 0
# evaluate per image
for key in keys:
# ground truth labels
labels = np.array(dictionary[key])
# 4 boxes coords are 1st four items of labels
gt_boxes = labels[:, 0:-1]
# last one is class
gt_class_ids = labels[:, -1]
# load image id by key
image_file = os.path.join(self.args.data_path, key)
image = skimage.img_as_float(imread(image_file))
image, classes, offsets = self.detect_objects(image)
# perform nms
_, _, class_ids, boxes = show_boxes(args,
image,
classes,
offsets,
self.feature_shapes,
show=False)
boxes = np.reshape(np.array(boxes), (-1,4))
# compute IoUs
iou = layer_utils.iou(gt_boxes, boxes)
# skip empty IoUs
if iou.size ==0:
continue
# the class of predicted box w/ max iou
maxiou_class = np.argmax(iou, axis=1)
# true positive
tp = 0
# false positiove
fp = 0
# sum of objects iou per image
s_image_iou = []
for n in range(iou.shape[0]):
# ground truth bbox has a label
if iou[n, maxiou_class[n]] > 0:
s_image_iou.append(iou[n, maxiou_class[n]])
# true positive has the same class and gt
if gt_class_ids[n] == class_ids[maxiou_class[n]]:
tp += 1
else:
fp += 1
# objects that we missed (false negative)
fn = abs(len(gt_class_ids) - tp)
s_iou += (np.sum(s_image_iou) / iou.shape[0])
s_precision += (tp/(tp + fp))
s_recall += (tp/(tp + fn))
n_test = len(keys)
print_log("mIoU: %f" % (s_iou/n_test),
self.args.verbose)
print_log("Precision: %f" % (s_precision/n_test),
self.args.verbose)
print_log("Recall: %f" % (s_recall/n_test),
self.args.verbose)
结果如下,在“表 11.13.1”中:
| 未归一化的偏移 | 未归一化的偏移,平滑 L1 | 归一化的偏移 | 归一化偏移,平滑 L1 | 归一化偏移,平滑 L1,焦点损失 | |
|---|---|---|---|---|---|
| IoU | 0.64 | 0.61 | 0.53 | 0.50 | 0.51 |
| 平均精度 | 0.87 | 0.86 | 0.90 | 0.85 | 0.85 |
| 平均召回率 | 0.87 | 0.85 | 0.87 | 0.83 | 0.83 |
表 11.13.1 测试数据集上 SSD 的表现基准。

图 11.13.1 来自测试数据集的图像上的示例预测示例(未归一化的偏移量)。

图 11.13.2 来自测试数据集的图像上的示例预测示例(未归一化的偏移量,平滑 L1)。

图 11.13.3 来自测试数据集的图像预测示例(标准化偏移)。

图 11.13.4 对来自测试数据集的图像进行的预测示例(标准化偏移,平滑 L1)。

图 11.13.5 对来自测试数据集的图像进行的预测示例(归一化偏移,平滑 L1,聚焦损失)。
本节中的结果验证了我们的 SSD 模型。 一个重要的经验教训是,只要我们理解了问题,无论问题多么复杂,我们都可以逐步构建一个可行的解决方案。 SSD 是迄今为止我们在本书中介绍过的最复杂的模型。 它需要许多工具,模块以及大量数据准备和管理才能工作。
14. 总结
在本章中,讨论了多尺度单发对象检测的概念。 使用以接收场斑块的质心为中心的锚框,可以计算地面真值边界框偏移量。 代替原始像素误差,归一化像素误差会鼓励更适合优化的有限范围。
每个锚框都分配有地面实况类别标签。 如果锚点框不与对象重叠,则为其分配背景类,并且其偏移量不包括在偏移量损失计算中。 已经提出了焦点损失以改善类别损失函数。 可以使用平滑的 L1 损失函数代替默认的 L1 偏置损失函数。
对测试数据集的评估表明,使用默认损失函数的归一化偏移可实现平均精度和召回率方面的最佳表现,而当消除偏移归一化时,mIoU 会得到改善。 通过增加训练图像的数量和变化可以提高性能。
在“第 12 章”中,“语义分割”建立在本章中开发的概念的基础上。 特别是,我们重用 ResNet 骨干网络来构建分段网络和 IoU 指标进行验证。
15. 参考
Krizhevsky Alex, Ilya Sutskever, and Geoffrey E. Hinton. "Imagenet classification with deep convolutional neural networks." Advances in neural information processing systems. 2012.Liu Wei, et al. "SSD: Single Shot MultiBox Detector." European conference on computer vision. Springer, Cham, 2016.Girshick Ross. "Fast R-CNN." Proceedings of the IEEE international conference on computer vision. 2015.Lin Tsung-Yi, et al. "Focal loss for Dense Object Detection. "Proceedings of the IEEE international conference on computer vision. 2017.Dutta, et al. VGG Image Annotator http://www.robots.ox.ac.uk/~vgg/software/via/Bodla Navaneeth, et al. "Soft-NMS--Improving Object Detection With One Line of Code." Proceedings of the IEEE international conference on computer vision. 2017.Everingham Mark, et al. "The Pascal Visual Object Classes (VOC) challenge." International journal of computer vision 88.2 (2010): 303-338."Huber Loss." https://en.wikipedia.org/wiki/Huber_loss
十二、语义分割
在“第 11 章”,“对象检测”中,我们讨论了对象检测作为一种重要的计算机视觉算法,具有多种实际应用。 在本章中,我们将讨论另一种称为语义分割的相关算法。 如果对象检测的目的是对图像中的每个对象同时执行定位和标识,则在语义分割中,目的是根据每个像素的对象类别对它们进行分类。
进一步扩展类比,在对象检测中,我们使用边界框显示结果。 在语义分割中,同一对象的所有像素都属于同一类别。 在视觉上,同一对象的所有像素将具有相同的颜色。 例如,属于*汽水**类别的所有像素均为蓝色。 非苏打罐对象的像素将具有不同的颜色。
类似于对象检测,语义分割有许多实际应用。 在医学成像中,它可用于分离和测量正常细胞与异常细胞的区域。 在卫星成像中,语义分段可用于度量森林覆盖率或灾难期间的洪水程度。 通常,语义分割可用于识别属于同一类对象的像素。 识别每个对象的各个实例并不重要。
好奇的读者可能会想知道,一般而言,不同的分割算法与特别是语义分割算法之间有什么区别? 在以下部分中,我们将对不同的分割算法进行限定。
总而言之,本章的目的是为了提出:
- 不同类型的分割算法
- 全卷积网络(FCN)作为语义分割算法的实现
tf.keras中 FCN 的实现和评估
我们将从讨论不同的分割算法开始。
1. 分割
分割算法将图像划分为像素或区域集。 分区的目的是为了更好地理解图像表示的内容。 像素组可以表示图像中特定应用感兴趣的对象。 我们划分的方式区分了不同的分割算法。
在某些应用中,我们对给定图像中的特定可数对象感兴趣。 例如,在自主导航中,我们对车辆,交通标志,行人和道路上的其他物体的实例感兴趣。 这些可计数对象统称为,称为事物。 所有其他像素都集中在一起作为背景。 这种类型的细分称为实例细分。
在其他应用中,我们对可数对象不感兴趣,而对无定形的不可数区域感兴趣,例如天空,森林,植被,道路,草地,建筑物和水体。 这些对象统称为东西。 这种类型的分段称为语义分段。
大致上,事物和事物共同构成了整个图像。 如果算法可以识别事物像素和填充像素,则其称为全光分割,如 Kirilov 等人所定义 [1]。
但是,事物与事物之间的区别并不严格。 应用可能将可数对象统称为东西。 例如,在百货商店中,不可能识别机架上的服装实例。 它们可以作为布料一起集中在一起。
“图 12.1.1”显示了不同类型的细分之间的区别。 输入的图像在桌子的顶部显示了两个汽水罐和两个果汁罐。 背景杂乱无章。 假设我们只对汽水罐和果汁罐感兴趣,在实例细分中,我们为每个对象实例分配唯一的颜色以分别区分四个对象。 对于语义分割,我们假设将所有的汽水罐都塞在一起,将果汁罐作为另一罐塞在一起,将背景作为最后的罐塞在一起。 基本上,我们为每种物料分配了唯一的颜色。 最后,在全景分割中,我们假设只有背景才是背景,而我们只对苏打水和果汁罐感兴趣。
对于这本书,我们仅探讨语义分割。 按照“图 12.1.1”中的示例,我们将为“第 11 章”,“对象检测”中使用的对象分配唯一的填充类别:1)水瓶,2)汽水罐和 3)果汁罐。 第四个也是最后一个类别是背景。




图 12.1.1:显示不同分割算法的四幅图像。 彩色效果最佳。 原始图像可以在这个页面中找到。
2. 语义分割网络
从上一节中,我们了解到语义分割网络是一个像素级分类器。 网络框图显示在“图 12.2.1”中。 但是,与简单分类器不同(例如,“第 1 章”,“Keras 深度神经网络”和“第 2 章”,“MNIST 分类器简介”) 其中只有一个分类器生成one-hot vector作为输出,在语义分段中,我们有并行运行的并行分类器。 每个人都在生成自己的单热点向量预测。 分类器的数量等于输入图像中的像素数量或图像宽度与高度的乘积。 每个one-hot vector预测的维数等于感兴趣的填充对象类别的数量。

图 12.2.1:可以将语义分割网络视为按像素分类器。 彩色效果最佳。 原始图像可以在这个页面中找到
例如,假设我们对以下四个类别感兴趣:0)背景,1)水瓶,2)汽水罐和 3)果汁罐,我们可以在“图 12.2.2”中看到,每个对象类别有四个像素。
相应地,使用 4 维one-hot vector对每个像素进行分类。 我们使用阴影表示像素的类别。 利用这一知识,我们可以想象一个语义分割网络预测image_width x image_height 4 维一热向量作为输出,每个像素一个 4 维一热向量:

图 12.2.2:四个不同的样本像素。 使用 4 维一热向量,每个像素根据其类别进行分类。 彩色效果最佳。 原始图像可以在这个页面中找到
了解了语义分割的概念后,我们现在可以介绍神经网络像素级分类器。 Long 等人的《全卷积网络(FCN)》启发了我们的语义分段网络架构 [2]。FCN 的关键思想是在生成最终预测时使用多个比例的特征映射。
我们的语义分段网络显示在“图 12.2.3”中。 它的输入是 RGB 图像(例如640 x 480 x 3),并且输出具有类似尺寸的张量,但最后一个尺寸是填充类别的数量(例如,对于 4 种填充类别而言是640 x 480 x 4)。 出于可视化目的,我们通过为每种类别分配颜色来将输出映射到 RGB:

图 12.2.3:语义分割的网络架构。 除非另有说明,否则核大小为 3。 除非另有说明,否则跨步为 1。 彩色效果最佳。 原始图像可以在这个页面中找到
类似于“第 11 章”,“对象检测”中讨论的 SSD,我们采用骨干网作为特征提取器。 我们在 SSD 中使用类似的 ResNetv2 网络。 ResNet 主干网执行两次最大池化,以到达第一组特征映射,其尺寸为输入图像的 1/4。 通过使用连续的Conv2D(strides=2)-BN-ReLU层生成其他特征映射集,从而生成具有输入图像尺寸(1/8, 1/16, 1/32)的特征映射。
Zhao 等人的《金字塔场景解析网络(PSPNet)》进行了改进,进一步增强了我们的语义分割网络架构 [3]。 在 PSPNet 中,每个特征映射由另一个卷积层进一步处理。 此外,还使用了第一组特征映射。
FCN 和 PSPNet 都对特征金字塔进行了上采样,以达到与第一组特征映射相同的大小。 之后,使用Concatenate层将所有上采样特征融合在一起。 然后级联层通过步长等于 2 的转置卷积处理两次,以恢复原始图像的宽度和高度。 最后,使用核大小为 1 且过滤器等于 4(换句话说,类别数)和Softmax层的转置卷积生成按像素分类预测。
在下一节中,我们将讨论细分网络的tf.keras实现。 我们可以重用“第 11 章”,“对象检测”中的 SSD 中的某些网络块,以加快实现速度。
3. Keras 中的语义分割网络
如图“图 12.2.3”所示,我们已经有了语义细分网络的一些关键构建块。 我们可以重用“第 2 章”,“深度神经网络”中介绍的 ResNet 模型。 我们只需要构建特征的金字塔以及上采样和预测层。
借用我们在“第 2 章”,“深度神经网络”中开发的 ResNet 模型,并在“第 11 章”,“对象检测”中重用了该模型, 我们提取具有四个级别的特征金字塔。“列表 12.3.1”显示了从 ResNet 提取特征的金字塔。 conv_layer()只是创建Conv2D(strides=2)-BN-ReLU层的辅助函数。
“列表 12.3.1”:resnet.py:
特征的金字塔函数:
def features_pyramid(x, n_layers):
"""Generate features pyramid from the output of the
last layer of a backbone network (e.g. ResNetv1 or v2)
Arguments:
x (tensor): Output feature maps of a backbone network
n_layers (int): Number of additional pyramid layers
Return:
outputs (list): Features pyramid
"""
outputs = [x]
conv = AveragePooling2D(pool_size=2, name='pool1')(x)
outputs.append(conv)
prev_conv = conv
n_filters = 512
# additional feature map layers
for i in range(n_layers - 1):
postfix = "_layer" + str(i+2)
conv = conv_layer(prev_conv,
n_filters,
kernel_size=3,
strides=2,
use_maxpool=False,
postfix=postfix)
outputs.append(conv)
prev_conv = conv
return outputs
“列表 12.3.1”只是特征金字塔的一半。 剩下的一半是每组特征之后的卷积。 另一半显示在“列表 12.3.2”中,以及金字塔各层的上采样。 例如,图像尺寸为 1/8 的特征会被上采样 2 倍,以使其尺寸与图像尺寸为 1/4 的第一组特征相匹配。 在同一清单中,我们还建立了完整的分割模型,从骨干网络到特征金字塔,再连接上采样特征金字塔,最后进一步进行特征提取,上采样和预测。 我们在输出层使用n维(例如 4 维)Softmax层执行逐像素分类。
“列表 12.3.2”:model.py:
构建语义分割网络:
def build_fcn(input_shape,
backbone,
n_classes=4):
"""Helper function to build an FCN model.
Arguments:
backbone (Model): A backbone network
such as ResNetv2 or v1
n_classes (int): Number of object classes
including background.
"""
inputs = Input(shape=input_shape)
features = backbone(inputs)
main_feature = features[0]
features = features[1:]
out_features = [main_feature]
feature_size = 8
size = 2
# other half of the features pyramid
# including upsampling to restore the
# feature maps to the dimensions
# equal to 1/4 the image size
for feature in features:
postfix = "fcn_" + str(feature_size)
feature = conv_layer(feature,
filters=256,
use_maxpool=False,
postfix=postfix)
postfix = postfix + "_up2d"
feature = UpSampling2D(size=size,
interpolation='bilinear',
name=postfix)(feature)
size = size * 2
feature_size = feature_size * 2
out_features.append(feature)
# concatenate all upsampled features
x = Concatenate()(out_features)
# perform 2 additional feature extraction
# and upsampling
x = tconv_layer(x, 256, postfix="up_x2")
x = tconv_layer(x, 256, postfix="up_x4")
# generate the pixel-wise classifier
x = Conv2DTranspose(filters=n_classes,
kernel_size=1,
strides=1,
padding='same',
kernel_initializer='he_normal',
name="pre_activation")(x)
x = Softmax(name="segmentation")(x)
model = Model(inputs, x, name="fcn")
return model
给定分割网络模型,我们使用学习速度为1e-3的 Adam 优化器和分类交叉熵损失函数来训练网络。“列表 12.3.3”显示了模型构建和训练函数调用。 在 40 个周期之后,学习率每 20 个周期减半。 我们使用AccuracyCallback监视网络表现,类似于“第 11 章”,“对象检测”中的 SSD 网络。 回调使用类似于对象检测平均 IoU 的平均 IoU(mIoU)指标计算表现。 表现最佳的平均值 IoU 的权重保存在文件中。 通过调用fit_generator()将网络训练 100 个周期。
“列表 12.3.3”:fcn-12.3.1.py:
语义分割网络的初始化和训练:
def build_model(self):
"""Build a backbone network and use it to
create a semantic segmentation
network based on FCN.
"""
# input shape is (480, 640, 3) by default
self.input_shape = (self.args.height,
self.args.width,
self.args.channels)
# build the backbone network (eg ResNet50)
# the backbone is used for 1st set of features
# of the features pyramid
self.backbone = self.args.backbone(self.input_shape,
n_layers=self.args.layers)
# using the backbone, build fcn network
# output layer is a pixel-wise classifier
self.n_classes = self.train_generator.n_classes
self.fcn = build_fcn(self.input_shape,
self.backbone,
self.n_classes)
def train(self):
"""Train an FCN"""
optimizer = Adam(lr=1e-3)
loss = 'categorical_crossentropy'
self.fcn.compile(optimizer=optimizer, loss=loss)
log = "# of classes %d" % self.n_classes
print_log(log, self.args.verbose)
log = "Batch size: %d" % self.args.batch_size
print_log(log, self.args.verbose)
# prepare callbacks for saving model weights
# and learning rate scheduler
# model weights are saved when test iou is highest
# learning rate decreases by 50% every 20 epochs
# after 40th epoch
accuracy = AccuracyCallback(self)
scheduler = LearningRateScheduler(lr_scheduler)
callbacks = [accuracy, scheduler]
# train the fcn network
self.fcn.fit_generator(generator=self.train_generator,
use_multiprocessing=True,
callbacks=callbacks,
epochs=self.args.epochs,
workers=self.args.workers)
多线程数据生成器类DataGenerator与“第 11 章”,“对象检测”中使用的类类似。 如“列表 12.3.4”所示,对__data_generation(self, keys)签名方法进行了修改,以生成一对图像张量及其相应的按像素方向的真实情况标签或分割蒙版 。 在下一节中,我们将讨论如何生成基本事实标签。
“列表 12.3.4”:data_generator.py:
DataGenerator类用于语义分割的数据生成方法:
def __data_generation(self, keys):
"""Generate train data: images and
segmentation ground truth labels
Arguments:
keys (array): Randomly sampled keys
(key is image filename)
Returns:
x (tensor): Batch of images
y (tensor): Batch of pixel-wise categories
"""
# a batch of images
x = []
# and their corresponding segmentation masks
y = []
for i, key in enumerate(keys):
# images are assumed to be stored
# in self.args.data_path
# key is the image filename
image_path = os.path.join(self.args.data_path, key)
image = skimage.img_as_float(imread(image_path))
# append image to the list
x.append(image)
# and its corresponding label (segmentation mask)
labels = self.dictionary[key]
y.append(labels)
return np.array(x), np.array(y)
语义分割网络现已完成。 使用tf.keras,我们讨论了其架构实现,初始化和训练。
在运行训练程序之前,我们需要训练和测试带有地面真实性标签的数据集。 在的下一部分中,我们将讨论将在本章中使用的语义分割数据集。
4. 示例数据集
我们可以使用在“第 11 章”,“对象检测”中使用的数据集。 回想一下,我们使用了一个小型数据集,其中包含使用便宜的 USB 相机(A4TECH PK-635G)收集的 1,000 640 x 480 RGB 训练图像和 50 640 x 480 RGB 测试图像。 但是,我们没有使用边界框和类别进行标记,而是使用多边形形状跟踪了每个对象类别的边缘。 我们使用相同的数据集标注器 VGG 图像标注器(VIA)[4]手动跟踪边缘并分配以下标签:1)水瓶,2)汽水罐和 3)果汁罐。
“图 12.4.1”显示了标记过程的示例 UI。

图 12.4.1:使用 VGG 图像标注器(VIA)进行语义分割的数据集标记过程
威盛标签软件将标签保存在 JSON 文件中。 对于训练和测试数据集,这些是:
segmentation_train.json
segmentation_test.json
无法原样使用存储在 JSON 文件中的多边形区域。 每个区域都必须转换成分割蒙版,即张量,其尺寸为img_w x img_h x px – wise_category。 在此数据集中,分割蒙版的尺寸为640 x 480 x 4。类别 0 为背景,其余为 1)对于水瓶,2)对于苏打罐,以及 3)表示果汁罐。 在utils文件夹中,我们创建了一个generate_gt_segmentation.py工具,用于将 JSON 文件转换为分段掩码。 为了方便起见,用于训练和测试的地面真实数据存储在压缩数据集中,该数据集是从上一章下载的:
segmentation_train.npy
segmentation_test.npy
每个文件都包含image filename: segmentation mask格式的真实情况数据字典,该字典在训练和验证期间加载。“图 12.4.2”显示了使用彩色像素可视化的“图 12.4.1”中图像的分割蒙版的示例。

图 12.4.2:可视化图 12.4.1 中所做标注的分段蒙版
现在,我们准备训练和验证语义分割网络。 在下一节中,我们将显示在本节中标注的数据集上语义分割的结果。
5. 语义分割验证
要训练语义分段网络,请运行以下命令:
python3 fcn-12.3.1.py --train
在每个周期,也会执行验证以确定表现最佳的参数。 对于语义分割,可以使用两个度量。 首先是平均 IOU。 这类似于上一章中目标检测中的平均 IoU。 区别在于针对每个填充类别在真实情况分割掩码和预测的分割掩码之间计算 IoU。 这包括背景。 平均 IoU 只是测试数据集所有 IoU 的平均值。
“图 12.5.1”显示了在每个周期使用 mIoU 的语义分割网络的表现。 最大 mIoU 为 0.91。 这个比较高。 但是,我们的数据集只有四个对象类别:

图 12.5.1:使用 mIoU 进行测试数据集训练期间的语义分割表现
第二个指标是平均像素精度。 这类似于在分类器预测上计算准确率的方式。 不同之处在于,分割网络具有的预测数量等于图像中的像素数量,而不是具有一个预测。 对于每个测试输入图像,计算平均像素精度。 然后,计算所有测试图像的平均值。
“图 12.5.2”显示了在每个周期使用平均像素精度的语义分割网络的表现。 最大平均像素精度为 97.9%。 我们可以看到平均像素精度与 mIoU 之间的相关性:

图 12.5.2:使用测试数据集的平均像素精度在训练期间的语义分割表现
“图 12.5.3”显示了输入图像,地面实况语义分割掩码和预测的语义分割掩码的样本:



图 12.5.3:样本输入,基本事实和语义细分的预测。 我们将黑色分配为背景类,而不是紫色,如先前所用
总体而言,我们基于 FCN 并经过 PSPNet 的思想改进的语义分割网络的表现相对较好。 我们的语义分割网络绝不是最优化的。 可以减少特征金字塔中的过滤器数量,以最大程度地减少参数的数量,该参数约为 1110 万。 探索增加特征金字塔中的级别数也很有趣。 读者可以通过执行以下命令来运行验证:
python3 fcn-12.3.1.py --evaluate
--restore-weights=ResNet56v2-3layer-drinks-best-iou.h5
在下一章中,我们将介绍无监督的学习算法。 考虑到监督学习中所需的昂贵且费时的标签,强烈地开发了无监督学习技术。 例如,在本章的语义分割数据集中,一个人花了大约 4 天的手工标签。 如果深度学习始终需要人工标记,那么它就不会前进。
6. 总结
在本章中,讨论了分割的概念。 我们了解到细分有不同类别。 每个都有自己的目标应用。 本章重点介绍语义分段的网络设计,实现和验证。
我们的语义分割网络受到 FCN 的启发,FCN 已成为许多现代,最先进的分割算法(例如 Mask-R-CNN [5])的基础。 PSPNet 的构想进一步增强了我们的网络,该构想在 ImageNet 2016 解析挑战赛中获得第一名。
使用 VIA 标记工具,使用与“第 11 章”,“对象检测”中使用的相同图像集生成用于语义分割的新数据集标签。 分割蒙版标记属于同一对象类的所有像素。
我们使用平均 IoU 和平均像素准确率指标对语义分割网络进行了训练和验证。 测试数据集上的表现表明,它可以有效地对测试图像中的像素进行分类。
如本章最后一部分所述,由于所涉及的成本和时间,深度学习领域正在意识到监督学习的局限性。 下一章重点介绍无监督学习。 它利用了通信领域信息理论中使用的互信息概念。
7. 参考
Kirillov, Alexander, et al.: Panoptic Segmentation. Proceedings of the IEEE conference on computer vision and pattern recognition. 2019.Long, Jonathan, Evan Shelhamer, and Trevor Darrell: Fully Convolutional Networks for Semantic Segmentation. Proceedings of the IEEE conference on computer vision and pattern recognition. 2015.Zhao, Hengshuang, et al.: Pyramid Scene Parsing Network. Proceedings of the IEEE conference on computer vision and pattern recognition. 2017.Dutta, et al.: VGG Image Annotator http://www.robots.ox.ac.uk/~vgg/software/via/He Kaiming, et al.: Mask R-CNN. Proceedings of the IEEE international conference on computer vision. 2017.
十三、使用互信息的无监督学习
许多机器学习任务(例如分类,检测和分段)都依赖于标记的数据。 网络在这些任务上的表现直接受到标记质量和数据量的影响。 问题在于产生足够数量的高质量标注数据既昂贵又费时。
为了继续机器学习的发展,新算法应减少对人类标签的依赖。 理想情况下,网络应该从无标签数据中学习,由于互联网的发展以及诸如智能手机和物联网(IoT)。 从未标记的数据中学习是无监督学习的领域。 在某些情况下,无监督学习也称为自我监督学习,以强调使用纯净的未标记数据进行训练和缺乏人工监督。 在本文中,我们将使用术语无监督学习。
在机器学习中,有一些方法可以从未标记的数据中学习。 可以使用深度神经网络和无监督学习中的新思想来改善这些方法的表现。 当处理高度非结构化的数据(例如文本,图像,音频和视频)时,尤其如此。
在无监督学习中成功的方法之一是最大化给定神经网络中两个随机变量之间的互信息。 在信息论领域,互信息(MI)是两个随机变量之间依存性的量度。
MI 最近已成功地从未标记的数据中提取了有用的信息,可以帮助学习下游任务。 例如,MI 能够对潜在代码向量进行聚类,从而使分类任务成为简单的线性分离问题。
总之,本章的目的是介绍:
- 互信息的概念
- 使用神经网络估计 MI
- 下游任务的离散和连续随机变量上的 MI 最大化
- Keras 中 MI 估计网络的实现
我们将从介绍互信息的概念开始。
1. 互信息
互信息是对两个随机变量X和Y之间依赖性的度量。 有时,MI 也定义为通过观察Y得出的有关X的信息量。 MI 也被称为信息获取或观察Y时X不确定性的降低。
与相关性相反,MI 可以测量X和Y之间的非线性统计依赖性。 在深度学习中,MI 是一种合适的方法,因为大多数现实世界中的数据都是非结构化的,并且输入和输出之间的依赖关系通常是非线性的。 在深度学习中,最终目标是对输入数据和预先训练的模型执行特定任务,例如分类,翻译,回归或检测。 这些任务也称为下游任务。
由于 MI 可以发现输入,中间特征,表示和输出中的相关性的重要方面,这些方面本身就是随机变量,因此共享信息通常可以提高下游任务中模型的表现。
在数学上,两个随机变量X和Y之间的 MI 可以定义为:
(Equation 13.1.1)
哪里:
P(X,Y)是 X 和 Y 在样本空间XxY上的联合分布 。P(X)P(Y)是边际分布P(X)和P(Y)分别位于样本空间X和Y上。
换句话说,MI 是联合分布与边际分布乘积之间的 Kullback-Leibler(KL)散度。 回顾“第 5 章”,“改进的 GAN” ,KL 是两个分布之间距离的度量。 在 MI 的上下文中,KL 距离越大,两个随机变量X和Y之间的 MI 越高。 通过扩展,MI 越高,X对Y的依赖性越高。
由于 MI 等于边际分布的联合与乘积之间的 KL 散度,因此它暗示它大于或等于零:I(X; Y) > 0。 当X和Y是独立随机变量时,MI 完全等于零。 当X和Y是独立的时,观察一个随机变量(例如Y)不会提供关于另一个随机变量的信息(例如X)。 因此,MI 是X和Y独立程度的度量。
如果X和Y是离散随机变量,则通过扩展 KL 散度,MI 可以计算为:
(Equation 13.1.2)
哪里:
P(X,Y)是联合概率质量函数(PMF)。P(X)和P(Y)是边际 PMF。
如果联合和边际分布已知,则 MI 可以进行精确计算。
如果X和Y是连续随机变量,则通过扩展 KL 散度,MI 可以表示为:
(Equation 13.1.3)
哪里:
p(x,y)是联合概率密度函数(PDF)。p(x)和p(y)是边缘 PDF。
连续随机变量的 MI 通常很难处理,并且可以通过变分方法进行估计。 在本章中,我们将讨论估计两个连续随机变量之间的 MI 的技术。
在讨论用于计算互信息的技术之前,让我们首先解释一下 MI 与熵之间的关系。 熵在“第 6 章”,“纠缠表示 GAN”中非正式引入,并在 InfoGAN 中得到了应用。
2. 互信息和熵
MI 也可以用熵来解释。 回想一下“第 6 章”,“纠缠表示 GAN” ,熵H(X)是对预期信息量的度量。 随机变量X的:
(Equation 13.2.1)
“公式 13.2.1”表示熵还是不确定性的量度。 不确定事件的发生给我们带来了更多的惊喜或信息。 例如,有关员工意外晋升的新闻具有大量信息或熵。
使用“公式 13.2.1”,MI 可以表示为:



(Equation 13.2.2)
“公式 13.2.2”表示 MI 随着边际熵增加而增加,但随联合熵而减少。 就熵而言,MI 的一个更常见的表达式如下:


(Equation 13.2.3)
“公式 13.2.3”告诉我们,MI 随随机变量的熵增加而减小,而随另一个随机变量的条件熵而减小。 或者,如果我们知道Y,则 MI 是的信息减少量或X的不确定性。
等效地,

(Equation 13.2.4)
“公式 13.2.4”表示 MI 是对称的:
(Equation 13.2.5)
MI 也可以用X和Y的条件熵表示:
(Equation 13.2.6)
使用贝叶斯定理:



(Equation 13.2.7)
“图 13.2.1”总结了到目前为止我们讨论的 MI 与条件熵和边际熵之间的所有关系:

图 13.2.1 维恩图显示了 MI 与条件熵和边际熵之间的关系
MI 的另一种有趣解释是根据“公式 13.2.3”,可以将其重写为:
(Equation 13.2.8)
由于H(X | Y)是观察到Y时的X的不确定性,因此“公式 13.2.8”告诉我们, 如果可以最大化 MI,则可以确定X给定Y。 在“图 13.2.1”中,新月形H(X | Y)的面积随着代表 MI 的圆之间的交点增加而减小。
再举一个的具体例子,假设X是一个随机变量,表示观察到在给定随机字节中的 0 到 255 之间的数字。 假设分布均匀,则转换为P(X) = 1/256的概率。 以 2 为底的X的熵为:

假设随机变量Y代表随机字节的 4 个最高有效位。 如果我们观察到 4 个最高有效位全为零,则数字 0 到 15 包含P(X) = 1/16,其余数字具有P(X) = 0。条件熵在基数 2 中是:

这为我们提供了I(X; Y) = 8 - 4 = 4的 MI。 注意,随机变量X的不确定性或预期信息量在知道Y后降低。X和Y共享的互信息为 4,这也等于两个随机变量共享的位数。“图 13.2.2”说明了两种情况,其中所有位都是随机的,而四个最高有效位都为 0。

图 13.2.2 当所有位未知时与某些位已知时的熵
鉴于我们已经对 MI 和熵有了很好的了解,我们现在可以将此概念用作无监督学习的一种方法。
3. 通过最大化离散随机变量的互信息来进行无监督学习
深度学习中的经典问题是监督分类。 在“第 1 章”,“Keras 简介”和“第 2 章”,“深度神经网络”中,我们了解到,在监督分类下,我们需要标记输入图像。 我们对 MNIST 和 CIFAR10 数据集都进行了分类。 对于 MNIST,三层 CNN 和密集层可实现高达 99.3% 的精度。 对于使用 ResNet 或 DenseNet 的 CIFAR10,我们可以实现大约 93% 至 94% 的精度。 MNIST 和 CIFAR10 都被标记为数据集。
与监督学习不同,本章的目标是执行无监督学习。 我们的重点是没有标签的分类。 这个想法是,如果我们学习如何对所有训练数据的潜在代码向量进行聚类,那么线性分离算法可以对每个测试输入数据潜在向量进行分类。
为了学习没有标签的潜在代码向量的聚类,我们的训练目标是在输入图像X和其潜在代码Y之间最大化 MI。X和Y都是随机变量。 这个想法是外观相似的图像将具有聚集到相同区域的潜在向量。 线性分配问题可以很容易地将彼此远离的区域分开。 因此,可以以无监督的方式完成分类问题。 数学上,目标是最大化:
(Equation 13.2.3)
直观地,一旦我们观察到Y,我们对X充满信心。 “公式 13.2.3”的问题在于,我们无法很好地估计要测量的密度P(X | Y) H(X | Y)。
Ji 等人的不变信息聚类(IIC)[1] 建议从联合和边际分布直接测量I(X; Y)。 目的是使用“公式 13.1.2”测量引用同一输入的两个潜在代码随机变量之间的 MI。 假设输入X编码为Z:

将相同的输入X转换为X_bar = G(X),以便X仍可清晰地归类为与X相同的类别。 在图像处理中,G可以是常见的操作,例如小旋转,随机裁剪和剪切。 有时,只要结果图像的含义相同,就可以接受诸如对比度和亮度调整,边缘检测,少量噪声添加以及归一化之类的操作。 例如,如果X是狗的图像,则在G之后,X_bar显然仍是狗。
使用相同编码器网络的潜在代码向量为:

因此,我们可以用两个随机变量Z和Z_bar将“公式 13.1.2”重写为:
(Equation 13.3.1)
其中P(Z)和P(Z_bar)可以解释为Z和Z_bar的边际分布。 对于离散随机变量,Z和Z_bar都是P(Z)和P(Z_bar)都是分类分布。 我们可以想象,编码器输出是 softmax ,其维数等于训练和测试数据分布中的类数N。 例如,对于 MNIST,编码器输出是与训练和测试数据集中的 10 位数字相对应的 10 维一热向量。
为了确定“公式 13.3.1”中的每个项,我们首先估计P(Z, Z_bar)。 IIC 假设Z和Z_bar是独立的,因此联合分布可以估计为:
(Equation 13.3.2)
这将创建一个N x N矩阵P(Z, Z_bar),其中每个元素Z[ij]对应于同时观察两个随机变量(Z[i], Z_bar[j])的概率。 如果对大批量进行此估计,则大样本均值将估计联合概率。
由于我们将使用 MI 来估计密度函数,因此 IIC 将采样限制为(Z[i], Z_bar[i])。 本质上,对于每个样本xᵢ,我们计算其潜在代码P(Z[i]) = E(X[i])。 然后,我们将xᵢ转换,并计算其潜在代码P(Z_bar[i]) = E(X_bar[i])。 联合分布计算如下:
(Equation 13.3.3)
其中M是批量大小。 由于我们对xᵢ和x_bar[i]使用相同的编码器E,因此联合分布应该对称。 我们通过执行以下命令来增强对称性:
(Equation 13.3.4)
给定P(Z, Z_bar),边际分布可以计算为:
(Equation 13.3.5)
我们按行求和矩阵的所有条目。 类似地:
(Equation 13.3.6)
我们按矩阵汇总矩阵的所有条目。
给定“公式 13.3.1”中的所有项,我们可以训练神经网络编码器E,该编码器使用损失函数来最大化 MI 或最小化负 MI:
(Equation 13.3.7)
在实现无监督聚类之前,让我们再次反思目标–最大化I(Z; Z_bar)。 由于X和X_bar = G(X)及其对应的潜在代码向量Z和Z_bar共享相同的信息,因此神经网络编码器E应该学习映射X和X_bar成为潜在向量Z和Z_bar,它们具有几乎相同的值以最大化其 MI。 在 MNIST 的背景下,看起来相似的数字将具有潜在代码向量,它们聚集在空间的同一区域中。
如果潜在代码向量是 softmax 的输出,则表明我们正在执行无监督聚类,可以使用线性分配算法将其转换为分类器。 在本章中,我们将介绍两种可能的线性分配算法,这些算法可用于将无监督的聚类转换为无监督的分类。
在下一节中,我们将讨论可用于实现无监督聚类的编码器网络模型。 特别是,我们将介绍可用于估计P(Z)和P(Z_bar)的编码器网络。
4. 用于无监督聚类的编码器网络
图 13.4.1 中显示了用于无监督聚类的编码器网络实现。 它是一种编码器,具有类似 VGG 的[2]主干和Dense层,并具有 softmax 输出。 最简单的 VGG-11 具有主干,如“图 13.4.2”所示。
对于 MNIST,使用最简单的 VGG-11 骨干将特征映射大小从MaxPooling2D操作的 5 倍减至零。 因此,当在 Keras 中实现时,将使用按比例缩小的 VGG-11 主干版本,如图“图 13.4.3”所示。 使用同一组过滤器。

图 13.4.1 IIC 编码器网络E的网络实现。 输入的 MNIST 图像被中心裁剪为24 x 24像素。 在此示例中,X_bar = G(X)是随机的24 x 24像素裁剪操作。

图 13.4.2 VGG-11 分类器主干
在“图 13.4.3”中,有 4 个Conv2D-BN-ReLU Activation-MaxPooling2D层,其过滤器大小为(64, 128, 256, 512)。 最后的Conv2D层不使用MaxPooling2D。 因此,最后的Conv2D层针对24 x 24 x 1裁剪的 MNIST 输入输出(3, 3, 512)特征映射。

图 13.4.3 缩小的 VGG 用作编码器主干
“图 13.4.4”显示了“图 13.4.1”的 Keras 模型图。 为了提高性能,IIC 执行了超集群。 两个或更多编码器用于生成两个或更多个边际分布P(Z)和P(Z_bar)。 生成相应的联合分布。 就网络模型的而言,这是由具有两个或更多头的编码器实现的。

图 13.4.4 Keras 中 IIC 编码器E的网络实现
“图 13.4.4”是单头编码器,而“图 13.4.5”是双头编码器。 请注意,两个头共享相同的 VGG 主干。

图 13.4.5 Keras 中的两头编码器网络E
在以下两个部分的中,我们将研究[II]网络模型是如何实现,训练和评估的。 我们还将研究线性分配问题,作为为每个聚类指定标签的工具。
5. Keras 中的无监督聚类实现
Keras 中用于无监督聚类的网络模型实现在“列表 13.5.1”中显示。 仅显示初始化。 网络超参数存储在args中。 VGG 主干对象在初始化期间提供。 给定骨干,模型实际上只是具有 softmax 激活的Dense层,如build_model()方法所示。 有创建多个头的选项。
与“第 11 章”,“对象检测”相似,我们实现了DataGenerator类以多线程方式有效地提供输入数据。 DataGenerator对象生成由输入图像X及其变换后的图像X_bar组成的所需配对训练输入数据(即,连体输入图像)。 DataGenerator类中最关键的方法__data_generation()显示在“列表 13.5.2”中。 输入图像X从原始输入图像中央裁切。 对于 MNIST,这是24 x 24像素中心裁剪。 变换后的输入图像X_bar可以随机旋转±20范围内的某个角度,也可以从图像的任何部分随机裁剪16 x 16、18 x 18或20 x 20像素,然后将其调整为24 x 24像素。 作物尺寸存储在crop_sizes列表中。
注意,仅输入图像和变换图像在DataGenerator对象生成的数据中很重要。 同样,损失函数所需的配对数据沿批量轴连接。 这将使我们能够在单批配对数据中计算损失函数。
“列表 13.5.1”:iic-13.5.1.py。 显示初始化和模型创建的 IIC 类:IIC 类:
def __init__(self,
args,
backbone):
"""Contains the encoder model, the loss function,
loading of datasets, train and evaluation routines
to implement IIC unsupervised clustering via mutual
information maximization
Arguments:
args : Command line arguments to indicate choice
of batch size, number of heads, folder to save
weights file, weights file name, etc
backbone (Model): IIC Encoder backbone (eg VGG)
"""
self.args = args
self.backbone = backbone
self._model = None
self.train_gen = DataGenerator(args, siamese=True)
self.n_labels = self.train_gen.n_labels
self.build_model()
self.load_eval_dataset()
self.accuracy = 0
def build_model(self):
"""Build the n_heads of the IIC model
"""
inputs = Input(shape=self.train_gen.input_shape, name='x')
x = self.backbone(inputs)
x = Flatten()(x)
# number of output heads
outputs = []
for i in range(self.args.heads):
name = "z_head%d" % i
outputs.append(Dense(self.n_labels,
activation='softmax',
name=name)(x))
self._model = Model(inputs, outputs, name='encoder')
optimizer = Adam(lr=1e-3)
self._model.compile(optimizer=optimizer, loss=self.mi_loss)
“列表 13.5.2”:data_generator.py。 用于生成成对的输入数据以训练 IIC 编码器的DataGenerator类方法:
def __data_generation(self, start_index, end_index):
"""Data generation algorithm. The method generates
a batch of pair of images (original image X and
transformed imaged Xbar). The batch of Siamese
images is used to trained MI-based algorithms:
1) IIC and 2) MINE (Section 7)
Arguments:
start_index (int): Given an array of images,
this is the start index to retrieve a batch
end_index (int): Given an array of images,
this is the end index to retrieve a batch
"""
d = self.crop_size // 2
crop_sizes = [self.crop_size*2 + i for i in range(0,5,2)]
image_size = self.data.shape[1] - self.crop_size
x = self.data[self.indexes[start_index : end_index]]
y1 = self.label[self.indexes[start_index : end_index]]
target_shape = (x.shape[0], *self.input_shape)
x1 = np.zeros(target_shape)
if self.siamese:
y2 = y1
x2 = np.zeros(target_shape)
for i in range(x1.shape[0]):
image = x[i]
x1[i] = image[d: image_size + d, d: image_size + d]
if self.siamese:
rotate = np.random.randint(0, 2)
# 50-50% chance of crop or rotate
if rotate == 1:
shape = target_shape[1:]
x2[i] = self.random_rotate(image,
target_shape=shape)
else:
x2[i] = self.random_crop(image,
target_shape[1:],
crop_sizes)
# for IIC, we are mostly interested in paired images
# X and Xbar = G(X)
if self.siamese:
# If MINE Algorithm is chosen, use this to generate
# the training data (see Section 9)
if self.mine:
y = np.concatenate([y1, y2], axis=0)
m1 = np.copy(x1)
m2 = np.copy(x2)
np.random.shuffle(m2)
x1 = np.concatenate((x1, m1), axis=0)
x2 = np.concatenate((x2, m2), axis=0)
x = (x1, x2)
return x, y
x_train = np.concatenate([x1, x2], axis=0)
y_train = np.concatenate([y1, y2], axis=0)
y = []
for i in range(self.args.heads):
y.append(y_train)
return x_train, y
return x1, y1
为了实现 VGG 骨干,在 Keras 中实现了VGG类,如“列表 13.5.3”所示。 VGG类的灵活性在于可以用不同的方式(或 VGG 的不同样式)进行配置。 显示了用于 IIC VGG 主干配置cfg的选项'F'。 我们使用一个辅助函数来生成Conv2D-BN-ReLU-MaxPooling2D层。
“列表 13.5.3”:vgg.py。
Keras 中的VGG backbone类方法:
cfg = {
'F': [64, 'M', 128, 'M', 256, 'M', 512],
}
class VGG:
def __init__(self, cfg, input_shape=(24, 24, 1)):
"""VGG network model creator to be used as backbone
feature extractor
Arguments:
cfg (dict): Summarizes the network configuration
input_shape (list): Input image dims
"""
self.cfg = cfg
self.input_shape = input_shape
self._model = None
self.build_model()
def build_model(self):
"""Model builder uses a helper function
make_layers to read the config dict and
create a VGG network model
"""
inputs = Input(shape=self.input_shape, name='x')
x = VGG.make_layers(self.cfg, inputs)
self._model = Model(inputs, x, name='VGG')
@property
def model(self):
return self._model
@staticmethod
def make_layers(cfg,
inputs,
batch_norm=True,
in_channels=1):
"""Helper function to ease the creation of VGG
network model
Arguments:
cfg (dict): Summarizes the network layer
configuration
inputs (tensor): Input from previous layer
batch_norm (Bool): Whether to use batch norm
between Conv2D and ReLU
in_channel (int): Number of input channels
"""
x = inputs
for layer in cfg:
if layer == 'M':
x = MaxPooling2D()(x)
elif layer == 'A':
x = AveragePooling2D(pool_size=3)(x)
else:
x = Conv2D(layer,
kernel_size=3,
padding='same',
kernel_initializer='he_normal'
)(x)
if batch_norm:
x = BatchNormalization()(x)
x = Activation('relu')(x)
return x
回到IIC类,IIC的关键算法是使负 MI 最小的损失函数。 此方法显示在“列表 13.5.4”中。 为了评估单个批量中的损失,我们研究了y_pred,并将其分为上下两半,分别对应于输入图像X及其变换后的图像X_bar的编码器输出的。 回想一下,配对数据是通过将一批图像X和一批其变换后的图像X_bar连接在一起而制成的。
y_pred的下半部分为Z,而上半部分为Z_bar遵循“公式 10.3.2”至“公式 10.3.7”,联合分布P(Z, Z_bar)和边际分布被计算。 最后,返回负数 MI。 注意,每个头对总损失函数的贡献均等。 因此,损失是根据头部的数量来缩放的。
“列表 13.5.4”:iic-13.5.1.py。
Keras 中的IIC类损失函数。 损失函数使负 MI 最小化(即,使 MI 最大化):
def mi_loss(self, y_true, y_pred):
"""Mutual information loss computed from the joint
distribution matrix and the marginals
Arguments:
y_true (tensor): Not used since this is
unsupervised learning
y_pred (tensor): stack of softmax predictions for
the Siamese latent vectors (Z and Zbar)
"""
size = self.args.batch_size
n_labels = y_pred.shape[-1]
# lower half is Z
Z = y_pred[0: size, :]
Z = K.expand_dims(Z, axis=2)
# upper half is Zbar
Zbar = y_pred[size: y_pred.shape[0], :]
Zbar = K.expand_dims(Zbar, axis=1)
# compute joint distribution (Eq 10.3.2 & .3)
P = K.batch_dot(Z, Zbar)
P = K.sum(P, axis=0)
# enforce symmetric joint distribution (Eq 10.3.4)
P = (P + K.transpose(P)) / 2.0
# normalization of total probability to 1.0
P = P / K.sum(P)
# marginal distributions (Eq 10.3.5 & .6)
Pi = K.expand_dims(K.sum(P, axis=1), axis=1)
Pj = K.expand_dims(K.sum(P, axis=0), axis=0)
Pi = K.repeat_elements(Pi, rep=n_labels, axis=1)
Pj = K.repeat_elements(Pj, rep=n_labels, axis=0)
P = K.clip(P, K.epsilon(), np.finfo(float).max)
Pi = K.clip(Pi, K.epsilon(), np.finfo(float).max)
Pj = K.clip(Pj, K.epsilon(), np.finfo(float).max)
# negative MI loss (Eq 10.3.7)
neg_mi = K.sum((P * (K.log(Pi) + K.log(Pj) - K.log(P))))
# each head contribute 1/n_heads to the total loss
return neg_mi/self.args.heads
IIC 网络训练方法显示在“列表 13.5.5”中。 由于我们使用的是从Sequence类派生的DataGenerator对象,因此可以使用 Keras fit_generator()方法来训练模型。
我们使用学习率调度器,每 400 个周期将学习率降低 80%。 AccuracyCallback调用eval()方法,因此我们可以在每个周期之后记录网络的表现。
可以选择保存表现最佳的模型的权重。 在eval()方法中,我们使用线性分类器为每个聚类分配标签。 线性分类器unsupervised_labels()是一种匈牙利算法,它以最小的成本将标签分配给群集。
最后一步将无监督的聚类转换为无监督的分类。 unsupervised_labels()函数在“列表 13.5.6”中显示。
“列表 13.5.5”:iic-13.5.1.py。
IIC 网络训练和评估:
def train(self):
"""Train function uses the data generator,
accuracy computation, and learning rate
scheduler callbacks
"""
accuracy = AccuracyCallback(self)
lr_scheduler = LearningRateScheduler(lr_schedule,
verbose=1)
callbacks = [accuracy, lr_scheduler]
self._model.fit_generator(generator=self.train_gen,
use_multiprocessing=True,
epochs=self.args.epochs,
callbacks=callbacks,
workers=4,
shuffle=True)
def eval(self):
"""Evaluate the accuracy of the current model weights
"""
y_pred = self._model.predict(self.x_test)
print("")
# accuracy per head
for head in range(self.args.heads):
if self.args.heads == 1:
y_head = y_pred
else:
y_head = y_pred[head]
y_head = np.argmax(y_head, axis=1)
accuracy = unsupervised_labels(list(self.y_test),
list(y_head),
self.n_labels,
self.n_labels)
info = "Head %d accuracy: %0.2f%%"
if self.accuracy > 0:
info += ", Old best accuracy: %0.2f%%"
data = (head, accuracy, self.accuracy)
else:
data = (head, accuracy)
print(info % data)
# if accuracy improves during training,
# save the model weights on a file
if accuracy > self.accuracy \
and self.args.save_weights is not None:
self.accuracy = accuracy
folder = self.args.save_dir
os.makedirs(folder, exist_ok=True)
path = os.path.join(folder, self.args.save_weights)
print("Saving weights... ", path)
self._model.save_weights(path)
“列表 13.5.6”:utils.py。
匈牙利语算法将标签分配给具有最低成本的集群:
from scipy.optimize import linear_sum_assignment
def unsupervised_labels(y, yp, n_classes, n_clusters):
"""Linear assignment algorithm
Arguments:
y (tensor): Ground truth labels
yp (tensor): Predicted clusters
n_classes (int): Number of classes
n_clusters (int): Number of clusters
"""
assert n_classes == n_clusters
# initialize count matrix
C = np.zeros([n_clusters, n_classes])
# populate count matrix
for i in range(len(y)):
C[int(yp[i]), int(y[i])] += 1
# optimal permutation using Hungarian Algo
# the higher the count, the lower the cost
# so we use -C for linear assignment
row, col = linear_sum_assignment(-C)
# compute accuracy
accuracy = C[row, col].sum() / C.sum()
return accuracy * 100

图 13.5.1 在三个群集的简单场景中说明的线性分配算法,可以将其最佳地分配给三个类别
如图“图 13.5.1”所示,线性分配问题最好使用将三个群集分配给三个类别的简化方案来解释。 线性分配问题找到了类对类的一对一分配,从而使总成本最小。 在“图 13.5.1*”的左侧,显示了聚类结果和真实情况标签。
线性分配问题可以找到每个群集的类或类别,或者如何为每个群集分配标签。 还显示了成本矩阵C。 对于每个聚类-真实情况对,成本矩阵像元递减 1。该像元的行-列索引是聚类编号-真实情况标签索引。 使用成本矩阵,线性分配问题的工作是找到导致总成本最小的最优矩阵X:
(Equation 13.5.1)
其中c[ij]和x[ij]分别是矩阵C和X的元素 。i和j是索引。X的元素受的以下约束:
x[ij] ∈ {0, 1}
Σ[j] x[ij] = 1对于i = 1, 2, ..., N
Σ[i] x[ij] = 1对于j = 1, 2, ..., N
X是一个二进制矩阵。 每行仅分配给一列。 因此,线性分配问题是组合问题。 最佳解决方案的详细信息超出了本书的范围,此处不再讨论。
最佳权重矩阵X显示在“图 13.5.1”中。 群集 0 被分配了标签 1。群集 1 被分配了标签 2。群集 2 被分配了标签 0。这可以从成本矩阵中直观地进行验证,因为这导致最低成本为 -4,同时确保每行仅分配给一列。
使用此矩阵,群集类的分配显示在最右边的表中。 使用群集类分配时,第四行上只有一个错误。 结果精度为五分之四,即 80%。
我们可以将的线性分配问题扩展到为 10 个 MNIST 集群分配标签的问题。 我们在scipy包中使用linear_sum_assignment()函数。 该函数基于匈牙利算法。“列表 13.5.6”显示了群集标记过程的实现。 有关linear_sum_assignment()函数的更多详细信息,请参见这里。
要训练 1 头情况下的 IIC 模型,请执行:
python3 iic-13.5.1.py --heads=1 --train --save-weights=head1.h5
对于其他数量的打印头,应相应地修改选项--heads和--save-weights。 在下一部分中,我们将检查 IIC 作为 MNIST 分类器的表现。
6. 将 MNIST 用于验证
在本节中,我们将研究使用 MNIST 测试数据集对 IIC 进行验证之后的结果。 在测试数据集上运行聚类预测后,线性分配问题为每个聚类分配标签,从本质上将聚类转换为分类。 我们计算了分类精度,如“表 13.6.1”所示。 IIC 的准确率高于论文中报告的 99.3%。 但是,应该注意的是,并非每次训练都会导致高精度分类。
有时,由于优化似乎停留在局部最小值中,我们不得不多次运行训练。 此外,在多头 IIC 模型中,对于所有头部,我们都无法获得相同水平的表现。“表 13.6.1”报告了最佳表现的头部。
| 头部数 | 1 |
2 |
3 |
4 |
5 |
|---|---|---|---|---|---|
| 精度,% | 99.49 | 99.47 | 99.54 | 99.52 | 99.53 |
表 13.6.1 不同头数的 IIC 精度
权重在 GitHub 上可用。 例如,要在单头 IIC 上运行验证:
python3 iic-13.5.1.py --heads=1 --eval --restore-weights=head1-best.h5
总之,我们可以看到可以执行无监督分类。 结果实际上比我们在“第 2 章”,“深度神经网络”中检查的监督分类更好。 在以下各节中,我们将把注意力转向对连续随机变量的无监督学习。
7. 通过最大化连续随机变量的互信息进行无监督学习
在前面的章节中,我们了解到可以很好地估计离散随机变量的 MI。 我们还证明了借助线性分配算法,通过最大化 MI 来执行聚类的网络可以得出准确的分类器。
如果 IIC 是离散随机变量 MI 的良好估计者,那么连续随机变量又如何呢? 在本节的中,我们讨论 Belghazi 等人的互信息网络估计器(MINE)。 [3]作为连续随机变量 MI 的估计量。
MINE 在“公式 13.1.1”中提出了 KL 散度的另一种表示形式,以使用神经网络实现 MI 估计器。 在 MINE 中,使用 KL 散度的 Donsker-Varadhan(DV)表示:
(Equation 13.7.1)
在函数T的整个空间中占据最高位的位置。T是从输入空间(例如图像)映射到实数的任意函数。 回想一下,最高被粗略地解释为最大值。 对于T,我们可以从θ ∈ Θ参数化的函数T[θ] = X x Y -> R系列中进行选择。 因此,我们可以用估计 KL 散度的深度神经网络表示T[θ],因此代表T。
给定作为 MI 的精确(但难处理)表示I(X; Y)及其参数化的估计值I[θ](X; Y)作为易于处理的下限,我们可以安全地说:
(Equation 13.7.2)
其中参数化的 MI 估计为:
(Equation 13.7.3)
I[θ](X; Y)也称为神经信息测度。 在第一个期望中,样本(x, y) ~ P(X, Y)从联合分布P(X,Y)中获取。 在第二个期望中,样本x ~ P(X), y ~ P(Y)来自边际分布P(X)和P(Y)。
“算法 13.7.1”:MINE。
初始化所有网络参数θ。
θ尚未收敛时,请执行:
-
从联合分布
{(x^(1), y^(1)), (x^(2), y^(2)), ..., (x^(b), y^(b))} ~ P(X, Y)中抽取一个小批量的b -
从边际分布
{x^(1), x^(2), ..., x^(b)} ~ P(X)和{y^(1), y^(2), ..., y^(b)} ~ P(Y)中抽取一个小批量的b。 -
评估下界:
![]()
-
评估偏差校正后的梯度:
![]()
-
更新网络参数:
![]()
其中
ε是学习率。
“算法 13.7.1”总结了 MINE 算法。 来自边际分布的样本是来自联合分布的样本,另一个变量已删除。 例如,样本x只是简单的样本(x, y),变量y被丢弃。 在降为变量y的值之后,将x的样本进行混洗。 对y执行相同的采样方法。 为了清楚起见,我们使用符号x_bar和y_bar从边际分布中识别样本。
在下一部分中,在双变量高斯分布的情况下,我们将使用 MINE 算法估计 MI。 我们将展示使用解析方法估计的 MI 和使用 MINE 估计 MI 的方法。
8. 估计二元高斯的互信息
在本节中,我们将验证 MINE 的二元高斯分布。“图 13.8.1”显示具有均值和协方差的双变量高斯分布:
(Equation 13.8.1)
(Equation 13.8.2)

图 13.8.1 具有均值和协方差的二维高斯分布,如公式 13.8.1 和公式 13.8.2 所示
我们的目标是通过近似“公式 13.1.3”来估计 MI。 可以通过获得大量样本(例如 1 百万个)并创建具有大量箱子(例如 100 个箱子)的直方图来进行近似。“列表 13.8.1”显示了使用装仓对二元高斯分布的 MI 进行的手动计算。
“列表 13.8.1”:mine-13.8.1.py:
def sample(joint=True,
mean=[0, 0],
cov=[[1, 0.5], [0.5, 1]],
n_data=1000000):
"""Helper function to obtain samples
fr a bivariate Gaussian distribution
Arguments:
joint (Bool): If joint distribution is desired
mean (list): The mean values of the 2D Gaussian
cov (list): The covariance matrix of the 2D Gaussian
n_data (int): Number of samples fr 2D Gaussian
"""
xy = np.random.multivariate_normal(mean=mean,
cov=cov,
size=n_data)
# samples fr joint distribution
if joint:
return xy
y = np.random.multivariate_normal(mean=mean,
cov=cov,
size=n_data)
# samples fr marginal distribution
x = xy[:,0].reshape(-1,1)
y = y[:,1].reshape(-1,1)
xy = np.concatenate([x, y], axis=1)
return xy
def compute_mi(cov_xy=0.5, n_bins=100):
"""Analytic computation of MI using binned
2D Gaussian
Arguments:
cov_xy (list): Off-diagonal elements of covariance
matrix
n_bins (int): Number of bins to "quantize" the
continuous 2D Gaussian
"""
cov=[[1, cov_xy], [cov_xy, 1]]
data = sample(cov=cov)
# get joint distribution samples
# perform histogram binning
joint, edge = np.histogramdd(data, bins=n_bins)
joint /= joint.sum()
eps = np.finfo(float).eps
joint[joint<eps] = eps
# compute marginal distributions
x, y = margins(joint)
xy = x*y
xy[xy<eps] = eps
# MI is P(X,Y)*log(P(X,Y)/P(X)*P(Y))
mi = joint*np.log(joint/xy)
mi = mi.sum()
return mi
运行的结果:
python3 mine-13.8.1.py --gaussian
表示手动计算的 MI:
Computed MI: 0.145158
可以使用--cov_xy选项更改协方差。 例如:
python3 mine-13.8.1.py --gaussian --cov_xy=0.8
表示手动计算的 MI:
Computed MI: 0.510342

图 13.8.2 一个简单的 MINE 模型,用于估计双变量高斯分布的随机变量X和Y的 MI
“列表 13.8.2”:mine-13.8.1.py。
一个简单的 MINE 模型,用于估计双变量高斯分布的随机变量的 MI:
class SimpleMINE:
def __init__(self,
args,
input_dim=1,
hidden_units=16,
output_dim=1):
"""Learn to compute MI using MINE (Algorithm 13.7.1)
Arguments:
args : User-defined arguments such as off-diagonal
elements of covariance matrix, batch size,
epochs, etc
input_dim (int): Input size dimension
hidden_units (int): Number of hidden units of the
MINE MLP network
output_dim (int): Output size dimension
"""
self.args = args
self._model = None
self.build_model(input_dim,
hidden_units,
output_dim)
def build_model(self,
input_dim,
hidden_units,
output_dim):
"""Build a simple MINE model
Arguments:
See class arguments.
"""
inputs1 = Input(shape=(input_dim), name="x")
inputs2 = Input(shape=(input_dim), name="y")
x1 = Dense(hidden_units)(inputs1)
x2 = Dense(hidden_units)(inputs2)
x = Add()([x1, x2])
x = Activation('relu', name="ReLU")(x)
outputs = Dense(output_dim, name="MI")(x)
inputs = [inputs1, inputs2]
self._model = Model(inputs,
outputs,
name='MINE')
self._model.summary()
def mi_loss(self, y_true, y_pred):
""" MINE loss function
Arguments:
y_true (tensor): Not used since this is
unsupervised learning
y_pred (tensor): stack of predictions for
joint T(x,y) and marginal T(x,y)
"""
size = self.args.batch_size
# lower half is pred for joint dist
pred_xy = y_pred[0: size, :]
# upper half is pred for marginal dist
pred_x_y = y_pred[size : y_pred.shape[0], :]
# implentation of MINE loss (Eq 13.7.3)
loss = K.mean(pred_xy) \
- K.log(K.mean(K.exp(pred_x_y)))
return -loss
def train(self):
"""Train MINE to estimate MI between
X and Y of a 2D Gaussian
"""
optimizer = Adam(lr=0.01)
self._model.compile(optimizer=optimizer,
loss=self.mi_loss)
plot_loss = []
cov=[[1, self.args.cov_xy], [self.args.cov_xy, 1]]
loss = 0.
for epoch in range(self.args.epochs):
# joint dist samples
xy = sample(n_data=self.args.batch_size,
cov=cov)
x1 = xy[:,0].reshape(-1,1)
y1 = xy[:,1].reshape(-1,1)
# marginal dist samples
xy = sample(joint=False,
n_data=self.args.batch_size,
cov=cov)
x2 = xy[:,0].reshape(-1,1)
y2 = xy[:,1].reshape(-1,1)
# train on batch of joint & marginal samples
x = np.concatenate((x1, x2))
y = np.concatenate((y1, y2))
loss_item = self._model.train_on_batch([x, y],
np.zeros(x.shape))
loss += loss_item
plot_loss.append(-loss_item)
if (epoch + 1) % 100 == 0:
fmt = "Epoch %d MINE MI: %0.6f"
print(fmt % ((epoch+1), -loss/100))
loss = 0.
现在,让我们使用 MINE 估计此双变量高斯分布的 MI。“图 13.8.2”显示了一个简单的 2 层 MLP 作为T[θ]的模型。 输入层从联合分布中接收一批(x,y),从边缘分布中接收一批(x_bar, y_bar)。 该网络在build_model()中的“列表 13.8.2”中实现。 在同一清单中还显示了此简单 MINE 模型的训练例程。
实现“公式 13.7.3”的损失函数也在“列表 13.8.2”中显示。 请注意,损失函数不使用基本真值。 它只是最小化了 MI 的负估计(从而使 MI 最大化)。 对于此简单的 MINE 模型,未实现移动平均损失。 我们使用“列表 13.8.1”中的相同函数sample()来获得联合和边际样本。
现在,我们可以使用同一命令来估计双变量高斯分布的 MI:
python3 mine-13.8.1.py --gaussian
“图 13.8.3”显示了 MI 估计(负损失)与历时数的关系。 以下是每隔 100 个特定周期的定量结果。手动和 MINE 计算的结果接近。 这证明了 MINE 是连续随机变量 MI 的良好估计。
Epoch 100 MINE MI: 0.112297
Epoch 200 MINE MI: 0.141723
Epoch 300 MINE MI: 0.142567
Epoch 400 MINE MI: 0.142087
Epoch 500 MINE MI: 0.142083
Epoch 600 MINE MI: 0.144755
Epoch 700 MINE MI: 0.141434
Epoch 800 MINE MI: 0.142480
Epoch 900 MINE MI: 0.143059
Epoch 1000 MINE MI: 0.142186
Computed MI: 0.147247

图 13.8.3 MI 估计作为简单 MINE 模型的函数周期。
到目前为止,我们已经针对二元高斯分布情况证明了 MINE。 在下一节中,我们将使用 MINE 来处理与 IIC 相同的 MNIST 无监督聚类问题。
9. Keras 中的使用连续随机变量的无监督聚类
在 MNIST 数字的无监督分类中,我们使用 IIC,因为可以使用离散的联合和边际分布来计算 MI 。 我们使用线性分配算法获得了良好的准确率。
在此部分中,我们将尝试使用 MINE 进行聚类。 我们将使用来自 IIC 的相同关键思想:从一对图像及其转换后的版本(X, X_bar)中,最大化对应的编码潜向量(Z, Z_bar)的 MI。 通过最大化 MI,我们对编码的潜在向量进行聚类。 与 MINE 的不同之处在于,编码后的潜在向量是连续的,而不是 IIC 中使用的单热向量格式。 由于聚类的输出不是单热向量格式,因此我们将使用线性分类器。 线性分类器是没有诸如ReLU之类的非线性激活层的 MLP。 如果输出不是单热点向量格式,则使用线性分类器替代线性分配算法。
“图 13.9.1”显示了 MINE 的网络模型。 对于 MNIST,从 MNIST 训练数据集中采样了x。 与 IIC 相似,称为变量y的其他输入只是图像x的变换后的版本。 在测试过程中,输入图像x来自 MNIST 测试数据集。 从本质上讲,数据生成与 IIC 中的相同,如“列表 13.5.2”中所示。

图 13.9.1 使用编码器网络E的 MINE 的网络实现。 输入的 MNIST 图像被中心裁剪为24 x 24像素。 在此示例中,X_bar = Y = G(X)是随机的24 x 24像素裁剪操作。
当在 Keras 中实现时,“图 13.9.1”的编码器网络显示在“图 13.9.2”中。 我们在 Dense 输出中省略了维数,以便我们可以尝试不同的维数(例如 10、16 和 32)。

图 13.9.2 编码器网络E是 VGG 网络,类似于 IIC 中使用的网络
MINE 网络模型显示在“图 13.9.3”中,代码显示在“列表 13.9.1”中。 它在架构中与上一节中实现的简单 MINE 类似,不同之处在于,我们在 MLP 中使用了 1,024 个隐藏单元,而不是 16 个。
“列表 13.9.1”:mine-13.8.1.py。
MINE 网络模型用于无监督群集:
class MINE:
def __init__(self,
args,
backbone):
"""Contains the encoder, SimpleMINE, and linear
classifier models, the loss function,
loading of datasets, train and evaluation routines
to implement MINE unsupervised clustering via mutual
information maximization
Arguments:
args : Command line arguments to indicate choice
of batch size, folder to save
weights file, weights file name, etc
backbone (Model): MINE Encoder backbone (eg VGG)
"""
self.args = args
self.latent_dim = args.latent_dim
self.backbone = backbone
self._model = None
self._encoder = None
self.train_gen = DataGenerator(args,
siamese=True,
mine=True)
self.n_labels = self.train_gen.n_labels
self.build_model()
self.accuracy = 0
def build_model(self):
"""Build the MINE model unsupervised classifier
"""
inputs = Input(shape=self.train_gen.input_shape,
name="x")
x = self.backbone(inputs)
x = Flatten()(x)
y = Dense(self.latent_dim,
activation='linear',
name="encoded_x")(x)
# encoder is based on backbone (eg VGG)
# feature extractor
self._encoder = Model(inputs, y, name="encoder")
# the SimpleMINE in bivariate Gaussian is used
# as T(x,y) function in MINE (Algorithm 13.7.1)
self._mine = SimpleMINE(self.args,
input_dim=self.latent_dim,
hidden_units=1024,
output_dim=1)
inputs1 = Input(shape=self.train_gen.input_shape,
name="x")
inputs2 = Input(shape=self.train_gen.input_shape,
name="y")
x1 = self._encoder(inputs1)
x2 = self._encoder(inputs2)
outputs = self._mine.model([x1, x2])
# the model computes the MI between
# inputs1 and 2 (x and y)
self._model = Model([inputs1, inputs2],
outputs,
name='encoder')
optimizer = Adam(lr=1e-3)
self._model.compile(optimizer=optimizer,
loss=self.mi_loss)
self._model.summary()
self.load_eval_dataset()
self._classifier = LinearClassifier(\
latent_dim=self.latent_dim)

图 13.9.3 MINE 网络模型
如“列表 13.9.2”中所示,训练例程类似于 IIC 中的训练例程。 区别在于在每个周期之后执行的评估。 在这种情况下,我们针对个周期训练线性分类器,并将其用于评估聚类的潜在代码向量。 当精度提高时,可以选择保存模型权重。 损失函数和优化器与SimpleMINE中的类似,如“列表 13.8.2”中所示,此处不再赘述。
“列表 13.9.2”:mine-13.8.1.py。
矿山训练和评估职能:
def train(self):
"""Train MINE to estimate MI between
X and Y (eg MNIST image and its transformed
version)
"""
accuracy = AccuracyCallback(self)
lr_scheduler = LearningRateScheduler(lr_schedule,
verbose=1)
callbacks = [accuracy, lr_scheduler]
self._model.fit_generator(generator=self.train_gen,
use_multiprocessing=True,
epochs=self.args.epochs,
callbacks=callbacks,
workers=4,
shuffle=True)
def eval(self):
"""Evaluate the accuracy of the current model weights
"""
# generate clustering predictions fr test data
y_pred = self._encoder.predict(self.x_test)
# train a linear classifier
# input: clustered data
# output: ground truth labels
self._classifier.train(y_pred, self.y_test)
accuracy = self._classifier.eval(y_pred, self.y_test)
info = "Accuracy: %0.2f%%"
if self.accuracy > 0:
info += ", Old best accuracy: %0.2f%%"
data = (accuracy, self.accuracy)
else:
data = (accuracy)
print(info % data)
# if accuracy improves during training,
# save the model weights on a file
if accuracy > self.accuracy \
and self.args.save_weights is not None:
folder = self.args.save_dir
os.makedirs(folder, exist_ok=True)
args = (self.latent_dim, self.args.save_weights)
filename = "%d-dim-%s" % args
path = os.path.join(folder, filename)
print("Saving weights... ", path)
self._model.save_weights(path)
if accuracy > self.accuracy:
self.accuracy = accuracy

图 13.9.4 线性分类器模型
线性分类器模型显示在“图 19.3.4”中。 它是一个具有 256 个单元的隐藏层的 MLP。 由于此模型不使用诸如ReLU之类的非线性激活,因此可以将其用作线性分配算法的近似值,以对 VGG-Dense 编码器E的输出进行分类。“列表 13.9.3”显示了在 Keras 中实现的线性分类器网络模型构建器。
“列表 13.9.3”:mine-13.8.1.py。
线性分类器网络:
class LinearClassifier:
def __init__(self,
latent_dim=10,
n_classes=10):
"""A simple MLP-based linear classifier.
A linear classifier is an MLP network
without non-linear activations like ReLU.
This can be used as a substitute to linear
assignment algorithm.
Arguments:
latent_dim (int): Latent vector dimensionality
n_classes (int): Number of classes the latent
dim will be converted to.
"""
self.build_model(latent_dim, n_classes)
def build_model(self, latent_dim, n_classes):
"""Linear classifier model builder.
Arguments: (see class arguments)
"""
inputs = Input(shape=(latent_dim,), name="cluster")
x = Dense(256)(inputs)
outputs = Dense(n_classes,
activation='softmax',
name="class")(x)
name = "classifier"
self._model = Model(inputs, outputs, name=name)
self._model.compile(loss='categorical_crossentropy',
optimizer='adam',
metrics=['accuracy'])
self._model.summary()
可以通过执行以下命令来训练 MINE 非监督分类器:
python3 mine-13.8.1.py --train --batch-size=1024 --epochs=200
可以根据可用的 GPU 内存来调整批量的大小。 要使用其他潜在尺寸大小(例如 64),请使用--latent-dim选项:
python3 mine-13.8.1.py --train --batch-size=1024 --latent-dim=64 --epochs=200
在 200 个周期内,MINE 网络具有“图 13.9.5”中所示的精度:

图 13.9.5 MNIST 分类中的 MINE 准确率
如图“图 13.9.5”所示,在默认潜伏昏暗 10 时,类似于 IIC,使用线性分类器的 MINE 可以达到 93.86% 的精度。 精度随潜在尺寸的值而增加。 由于 MINE 是真实 MI 的近似值,因此预计其精度会低于 IIC。
到此结束本章和书。 无监督学习的领域是新生的。 鉴于人工智能发展的当前障碍之一是人工标签,这是一个巨大的研究机会,这既昂贵又费时。 我们预计在未来几年中,无监督学习将取得突破。
10. 总结
在本章中,我们讨论了 MI 及其在解决无监督任务中有用的方式。 各种在线资源提供了有关 MI 的其他背景信息[4]。 当用于聚类时,最大化 MI 会强制使用线性分配或线性分类器将潜在代码向量聚类在适合轻松标记的区域中。
我们介绍了 MI 的两种度量:IIC 和 MINE。 我们可以通过对离散随机变量使用 IIC 来近似逼近 MI,从而导致分类器以较高的精度执行。 IIC 适用于离散概率分布。 对于连续随机变量,MINE 使用 KL 散度的 Donsker-Varadhan 形式对估计 MI 的深度神经网络进行建模。 我们证明了 MINE 可以近似逼近双变量高斯分布的 MI。 作为一种无监督的方法,MINE 在对 MNIST 数字进行分类时显示出可接受的表现。
11. 参考
Ji, Xu, João F. Henriques, and Andrea Vedaldi. Invariant Information Clustering for Unsupervised Image Classification and Segmentation. International Conference on Computer Vision, 2019.Simonyan, Karen, and Andrew Zisserman. Very deep convolutional networks for large-scale image recognition. arXiv preprint arXiv:1409.1556 (2014).Belghazi, Mohamed Ishmael, et al. Mutual Information Neural Estimation. International Conference on Machine Learning. 2018.https://en.wikipedia.org/wiki/Mutual_information.

其中










































































浙公网安备 33010602011771号