Keras-高级深度学习-全-
Keras 高级深度学习(全)
原文:
annas-archive.org/md5/946fd2e9c806f075bc9bc101ff92dd6c译者:飞龙
前言
近年来,深度学习在视觉、语音、自然语言处理与理解以及其他数据丰富领域的难题中取得了前所未有的成功。企业、大学、政府和研究机构对该领域的关注加速了技术的进步。本书涵盖了深度学习中的一些重要进展。通过介绍原理背景、深入挖掘概念背后的直觉、使用 Keras 实现方程和算法,并分析结果,本书对高级理论进行了详细讲解。
人工智能(AI)现阶段仍然远未成为一个完全被理解的领域。作为 AI 子领域的深度学习,也处于同样的境地。虽然它远未成熟,但许多现实世界的应用,如基于视觉的检测与识别、产品推荐、语音识别与合成、节能、药物发现、金融和营销,已经在使用深度学习算法。未来还会发现并构建更多的应用。本书的目标是解释高级概念,提供示例实现,并让读者作为该领域的专家,识别出目标应用。
一个尚不完全成熟的领域是一把双刃剑。一方面,它为发现和开发提供了许多机会。深度学习中有很多尚未解决的问题,这为成为市场先行者——无论是在产品开发、出版还是行业认可上——提供了机会。另一方面,在任务关键型环境中,很难信任一个尚未完全理解的领域。我们可以放心地说,如果有人问,很少有机器学习工程师会选择乘坐由深度学习系统控制的自动驾驶飞机。要获得这种程度的信任,还需要做大量的工作。本书中讨论的高级概念很有可能在建立这种信任基础方面发挥重要作用。
每一本关于深度学习的书都不可能完全涵盖整个领域,本书也不例外。鉴于时间和篇幅的限制,我们本可以触及一些有趣的领域,如检测、分割与识别、视觉理解、概率推理、自然语言处理与理解、语音合成以及自动化机器学习。然而,本书相信,选取并讲解某些领域能够使读者能够进入未覆盖的其他领域。
当读者准备继续阅读本书时,需要牢记,他们选择了一个充满激动人心的挑战并且能够对社会产生巨大影响的领域。我们很幸运,拥有一份让我们每天早晨醒来都期待的工作。
本书适合谁阅读
本书面向希望深入理解深度学习高级主题的机器学习工程师和学生。每个讨论都附有 Keras 中的代码实现。本书适合那些希望了解如何将理论转化为在 Keras 中实现的工作代码的读者。除了理解理论外,代码实现通常是将机器学习应用于现实问题时最具挑战性的任务之一。
本书内容概览
第一章,使用 Keras 介绍高级深度学习,涵盖了深度学习的关键概念,如优化、正则化、损失函数、基础层和网络及其在 Keras 中的实现。本章还回顾了深度学习和 Keras 的使用,采用顺序 API。
第二章,深度神经网络,讨论了 Keras 的功能 API。探讨了两种广泛使用的深度网络架构,ResNet 和 DenseNet,并在 Keras 中通过功能 API 进行了实现。
第三章,自编码器,介绍了一个常见的网络结构——自编码器,用于发现输入数据的潜在表示。讨论并在 Keras 中实现了两个自编码器的应用示例:去噪和着色。
第四章,生成对抗网络(GANs),讨论了深度学习中的一项重要进展。GAN 用于生成看似真实的新合成数据。本章解释了 GAN 的原理。本文讨论并实现了两种 GAN 示例:DCGAN 和 CGAN,均在 Keras 中实现。
第五章,改进的 GANs,介绍了改进基础 GAN 的算法。这些算法解决了训练 GAN 时的难题,并提高了合成数据的感知质量。讨论并在 Keras 中实现了 WGAN、LSGAN 和 ACGAN。
第六章,解耦表示 GANs,讨论了如何控制 GAN 生成的合成数据的属性。如果潜在表示解耦,这些属性就可以被控制。介绍了两种解耦表示的技术:InfoGAN 和 StackedGAN,并在 Keras 中进行了实现。
第七章,跨域 GANs,涵盖了 GAN 的一个实际应用,即将图像从一个领域转移到另一个领域,通常称为跨域迁移。讨论了广泛使用的跨域 GAN——CycleGAN,并在 Keras 中实现。本章还展示了 CycleGAN 执行图像着色和风格迁移的过程。
第八章,变分自编码器(VAE),讨论了深度学习中的另一项重大进展。与 GAN 类似,VAE 是一种生成模型,用于生成合成数据。与 GAN 不同,VAE 侧重于可解码的连续潜在空间,适用于变分推断。VAE 及其变种 CVAE 和
-VAE 在 Keras 中进行了实现。
第九章,深度强化学习,解释了强化学习和 Q 学习的原理。介绍了实现 Q 学习的两种技术,Q 表更新和深度 Q 网络(DQN)。在 OpenAI gym 环境中,演示了使用 Python 和 DQN 实现 Q 学习。
第十章,策略梯度方法,解释了如何使用神经网络学习强化学习中的决策策略。涵盖了四种方法,并在 Keras 和 OpenAI gym 环境中实现,分别是 REINFORCE、带基线的 REINFORCE、演员-评论家和优势演员-评论家。本章中的示例展示了在连续动作空间中应用策略梯度方法。
为了最大限度地从本书中受益
-
深度学习与 Python:读者应具备基本的深度学习知识,并了解如何在 Python 中实现深度学习。虽然有使用 Keras 实现深度学习算法的经验会有所帮助,但并不是必须的。第一章,使用 Keras 介绍深度学习进阶,回顾了深度学习的概念及其在 Keras 中的实现。
-
数学:本书中的讨论假设读者具备大学水平的微积分、线性代数、统计学和概率论基础知识。
-
GPU:本书中的大多数 Keras 实现需要 GPU。如果没有 GPU,执行许多代码示例将不可行,因为所需时间过长(可能需要几个小时甚至几天)。本书中的示例尽量使用合理的数据大小,以减少高性能计算机的使用。本书假设读者至少可以访问 NVIDIA GTX 1060。
-
编辑:本书中的代码示例是在
vim编辑器下,使用 Ubuntu Linux 16.04 LTS、Ubuntu Linux 17.04 和 macOS High Sierra 操作系统编辑的。任何支持 Python 的文本编辑器都可以使用。 -
TensorFlow:Keras 需要一个后端。本书中的代码示例使用 Keras 和 TensorFlow 后端编写。请确保正确安装了 GPU 驱动和
tensorflow。 -
GitHub:我们通过示例和实验来学习。请从本书的 GitHub 仓库中
git pull或fork代码包。获取代码后,检查它。运行它。修改它。再次运行它。通过调整代码示例进行所有创造性的实验。这是理解章节中所有理论的唯一方法。我们也非常感激你在书籍 GitHub 仓库上给予星标。
下载示例代码文件
本书的代码包托管在 GitHub 上,地址是:
github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras
我们还在 github.com/PacktPublishing/ 提供了其他来自我们丰富书籍和视频目录的代码包,快去看看吧!
下载彩色图片
我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图片。你可以在这里下载:www.packtpub.com/sites/default/files/downloads/9781788629416_ColorImages.pdf。
使用的约定
本书中的代码示例使用 Python 编写,具体来说是 python3。配色方案基于 vim 语法高亮。考虑以下示例:
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()(x)
if activation == 'relu':
x = Activation('relu')(x)
else:
x = LeakyReLU(alpha=0.2)(x)
x = conv(x)
return x
尽可能包含文档字符串。至少使用文本注释来 最小化空间的使用。
所有命令行代码执行格式如下:
$ python3 dcgan-mnist-4.2.1.py
示例代码文件命名格式为:algorithm-dataset-chapter.section.number.py。命令行示例是第四章第二节的第一段代码,使用的是 DCGAN 算法和 MNIST 数据集。在某些情况下,执行命令行并未明确写出,但默认是:
$ python3 name-of-the-file-in-listing
The file name of the code example is included in the Listing caption.
联系我们
我们始终欢迎读者的反馈。
一般反馈:发送电子邮件至 <feedback@packtpub.com>,并在邮件主题中注明书名。如果你对本书的任何部分有疑问,请通过 <questions@packtpub.com> 给我们发送邮件。
勘误:尽管我们已尽最大努力确保内容的准确性,但错误还是难免发生。如果你在本书中发现错误,请告知我们。请访问,www.packtpub.com/submit-errata,选择你的书籍,点击“勘误提交表单”链接,并填写相关信息。
盗版:如果你在互联网上发现任何形式的非法复制品,我们将非常感激你提供相关网址或网站名称。请通过 <copyright@packtpub.com> 联系我们,并附上相关材料的链接。
如果你有兴趣成为作者:如果你对某个领域有专长,并且有兴趣编写或为书籍贡献内容,请访问 authors.packtpub.com。
评论
请留下评论。当你阅读并使用过这本书后,为什么不在你购买它的站点上留下评论呢?潜在的读者可以看到并参考你公正的意见做出购买决策,我们在 Packt 能够了解你对我们产品的看法,作者也可以看到你对他们书籍的反馈。谢谢!
了解更多关于 Packt 的信息,请访问 packtpub.com。
第一章。介绍使用 Keras 的高级深度学习
在本章中,我们将介绍本书中将使用的三种深度学习人工神经网络。这些深度学习模型是 MLP、CNN 和 RNN,它们是本书中所涵盖的高级深度学习主题(如自编码器和 GANs)的构建模块。
在本章中,我们将一起使用 Keras 库实现这些深度学习模型。我们将首先了解为什么 Keras 是我们使用的优秀工具。接下来,我们将深入探讨三种深度学习模型的安装和实现细节。
本章内容将包括:
-
解释为什么 Keras 库是用于高级深度学习的绝佳选择。
-
介绍 MLP、CNN 和 RNN —— 这是大多数高级深度学习模型的核心构建模块,我们将在本书中使用它们。
-
提供如何使用 Keras 和 TensorFlow 实现 MLP、CNN 和 RNN 的示例。
-
在过程中,我们将开始介绍一些重要的深度学习概念,包括优化、正则化和损失函数。
到本章结束时,我们将使用 Keras 实现基本的深度学习模型。在下一章中,我们将探讨基于这些基础的高级深度学习主题,如深度网络、自编码器和生成对抗网络(GANs)。
为什么 Keras 是完美的深度学习库?
Keras [Chollet, François. "Keras (2015)." (2017)] 是一个流行的深度学习库,写作时已有超过 250,000 名开发者使用,每年这个数字翻倍。超过 600 名贡献者积极维护它。本书中使用的一些示例已贡献至官方 Keras GitHub 仓库。谷歌的TensorFlow,一个流行的开源深度学习库,将 Keras 作为其高层 API。在工业界,Keras 被谷歌、Netflix、Uber 和 NVIDIA 等大公司广泛使用。本章中,我们将介绍如何使用 Keras Sequential API。
我们选择 Keras 作为本书中的工具,因为 Keras 是一个致力于加速深度学习模型实现的库。这使得 Keras 成为我们在实践和动手操作时的理想选择,尤其是在探索本书中的高级深度学习概念时。由于 Keras 与深度学习紧密相连,在最大化使用 Keras 库之前,了解深度学习的关键概念是至关重要的。
注意
本书中的所有示例都可以在 GitHub 上找到,链接如下:github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras。
Keras 是一个深度学习库,使我们能够高效地构建和训练模型。在该库中,层像乐高积木一样相互连接,生成的模型简洁且易于理解。模型训练非常直接,只需要数据、若干训练周期和用于监控的指标。最终结果是,大多数深度学习模型都可以通过显著更少的代码行数来实现。通过使用 Keras,我们能够提高生产力,通过节省在代码实现上的时间,将其投入到更重要的任务中,例如制定更好的深度学习算法。我们将 Keras 与深度学习结合使用,因为在引入本章接下来部分的三种深度学习网络时,它能提供更高的效率。
同样,Keras 非常适合快速实现深度学习模型,就像我们将在本书中使用的模型一样。典型的模型可以通过少量的代码行使用 Sequential Model API 构建。然而,不要被它的简洁性误导。Keras 也可以通过其 API 和 Model 和 Layer 类构建更先进和复杂的模型,这些模型可以定制以满足独特的需求。功能性 API 支持构建图形化的模型、层的重用以及表现像 Python 函数的模型。同时,Model 和 Layer 类提供了一个框架,用于实现不常见或实验性的深度学习模型和层。
安装 Keras 和 TensorFlow
Keras 不是一个独立的深度学习库。如 图 1.1.1 所示,它建立在其他深度学习库或后端之上。可以是 Google 的 TensorFlow,MILA 的 Theano 或 Microsoft 的 CNTK。对 Apache 的 MXNet 的支持几乎已经完成。我们将在本书中使用 TensorFlow 后端和 Python 3 进行测试。之所以选择 TensorFlow,是因为它的流行,使其成为一个常见的后端。
我们可以通过编辑 Linux 或 macOS 中的 Keras 配置文件 .keras/keras.json,轻松地在不同的后端之间切换。由于底层算法实现方式的不同,网络在不同的后端上可能会有不同的速度。
在硬件上,Keras 可以运行在 CPU、GPU 和 Google 的 TPU 上。在本书中,我们将使用 CPU 和 NVIDIA GPU(特别是 GTX 1060 和 GTX 1080Ti 型号)进行测试。

图 1.1.1:Keras 是一个高级库,建立在其他深度学习模型之上。Keras 支持在 CPU、GPU 和 TPU 上运行。
在继续阅读本书的其余内容之前,我们需要确保 Keras 和 TensorFlow 已正确安装。有多种方法可以进行安装;其中一种示例是使用 pip3 安装:
$ sudo pip3 install tensorflow
如果我们拥有支持的 NVIDIA GPU,并且已正确安装驱动程序,以及 NVIDIA 的 CUDA 工具包和 cuDNN 深度神经网络库,建议安装支持 GPU 的版本,因为它可以加速训练和预测:
$ sudo pip3 install tensorflow-gpu
我们接下来的步骤是安装 Keras:
$ sudo pip3 install keras
本书中的示例需要额外的包,如 pydot、pydot_ng、vizgraph、python3-tk 和 matplotlib。在继续进行本章后,请先安装这些包。
如果已安装 TensorFlow 和 Keras 及其依赖项,则以下内容不应产生任何错误:
$ python3
>>> import tensorflow as tf
>>> message = tf.constant('Hello world!')
>>> session = tf.Session()
>>> session.run(message)
b'Hello world!'
>>> import keras.backend as K
Using TensorFlow backend.
>>> print(K.epsilon())
1e-07
关于 SSE4.2 AVX AVX2 FMA 的警告信息(如下所示)可以安全忽略。要去除警告信息,你需要从 github.com/tensorflow/tensorflow 重新编译并安装 TensorFlow 源代码。
tensorflow/core/platform/cpu_feature_guard.cc:137] Your CPU supports instructions that this TensorFlow binary was not compiled to use: SSE4.2 AVX AVX2 FMA
本书不覆盖完整的 Keras API。我们只会介绍解释本书中高级深度学习主题所需的材料。如需更多信息,请参考官方 Keras 文档,网址为 keras.io。
实现核心深度学习模型 - MLPs、CNNs 和 RNNs
我们已经提到过,我们将使用三种高级深度学习模型,它们是:
-
MLPs:多层感知机
-
RNNs:递归神经网络
-
CNNs:卷积神经网络
这是我们在本书中将使用的三种网络。尽管这三种网络是独立的,但你会发现它们常常会结合在一起,以便充分利用每种模型的优势。
在本章的接下来的部分中,我们将逐一详细讨论这些构建模块。在随后的部分中,MLPs 将与其他重要主题一起讨论,如损失函数、优化器和正则化器。之后,我们将覆盖 CNNs 和 RNNs。
MLPs、CNNs 和 RNNs 之间的区别
多层感知机(MLPs)是一个全连接网络。在一些文献中,你常常会看到它们被称为深度前馈网络或前馈神经网络。从已知的目标应用角度理解这些网络将帮助我们深入理解设计高级深度学习模型的基本原因。MLPs 在简单的逻辑回归和线性回归问题中很常见。然而,MLPs 并不适合处理序列数据和多维数据模式。由于设计上的原因,MLPs 很难记住序列数据中的模式,并且需要大量的参数来处理多维数据。
对于顺序数据输入,RNNs 因其内部设计允许网络发现对预测有用的历史数据依赖关系,因此非常受欢迎。对于多维数据,如图像和视频,CNN 在提取特征图以进行分类、分割、生成等方面表现出色。在某些情况下,CNN 也以一维卷积的形式用于具有顺序输入数据的网络。然而,在大多数深度学习模型中,MLPs、RNNs 和 CNNs 会结合使用,以充分发挥每个网络的优势。
MLPs、RNNs 和 CNNs 并没有完整展示深度网络的全貌。我们还需要识别一个目标或损失函数,优化器,以及一个正则化器。目标是在训练过程中减少损失函数的值,因为这可以作为模型正在学习的良好指引。为了最小化这个值,模型使用优化器。这是一个算法,用于确定每个训练步骤中权重和偏差应该如何调整。一个训练好的模型不仅要在训练数据上工作,还要在测试数据甚至是未曾预见的输入数据上有效。正则化器的作用是确保训练好的模型能够泛化到新数据。
多层感知机(MLPs)
我们将要查看的三种网络中的第一种被称为多层感知机(MLPs)。假设目标是创建一个神经网络,用于基于手写数字来识别数字。例如,当网络的输入是手写数字 8 的图像时,相应的预测也必须是数字 8。这是分类器网络的经典任务,可以通过逻辑回归进行训练。为了训练和验证一个分类器网络,必须有一个足够大的手写数字数据集。修改版国家标准与技术研究院数据集(简称 MNIST)通常被认为是深度学习的Hello World!,并且是一个适合手写数字分类的数据集。
在我们讨论多层感知机模型之前,必须先理解 MNIST 数据集。书中大量的示例使用了 MNIST 数据集。MNIST 被用来解释和验证深度学习理论,因为它包含的 70,000 个样本虽然不大,却包含了足够丰富的信息:

图 1.3.1:MNIST 数据集中的示例图像。每张图像为 28 × 28 像素的灰度图。
MNIST 数据集
MNIST 是一个包含从 0 到 9 的手写数字集合。它有一个包含 60,000 张图像的训练集和 10,000 张测试图像,这些图像被分类到相应的类别或标签中。在一些文献中,目标或真实标签一词也用来指代标签。
在前面的图中可以看到 MNIST 数字的样本图像,每个图像的大小为 28×28 像素的灰度图。要在 Keras 中使用 MNIST 数据集,提供了一个 API 来自动下载和提取图像与标签。Listing 1.3.1展示了如何用一行代码加载 MNIST 数据集,使我们能够计算训练集和测试集的标签数量,并随机绘制数字图像。
Listing 1.3.1,mnist-sampler-1.3.1.py。Keras 代码展示了如何访问 MNIST 数据集,绘制 25 个随机样本,并计算训练集和测试集标签的数量:
import numpy as np
from 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.show()
plt.savefig("mnist-samples.png")
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所示。
在讨论多层感知机分类器模型之前,必须记住,虽然 MNIST 数据是二维张量,但应根据输入层的类型进行相应的重塑。下图展示了如何将一个 3×3 的灰度图像重塑为 MLP、CNN 和 RNN 的输入层:

图 1.3.2:一个类似于 MNIST 数据的输入图像,根据输入层的类型进行重塑。为简化起见,展示了如何将一个 3×3 的灰度图像进行重塑。
MNIST 数字分类器模型
如图 1.3.3所示,提出的 MLP 模型可用于 MNIST 数字分类。当单元或感知机被展示时,MLP 模型是一个完全连接的网络,如图 1.3.4所示。接下来还会展示如何根据输入计算感知机的输出,作为权重w[i]和偏置b[n](n 为第 n 个单元)的函数。相应的 Keras 实现请参见Listing 1.3.2。

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

图 1.3.4:图 1.3.3 中的 MLP MNIST 数字分类器由全连接层组成。为简化起见,激活函数和 dropout 未显示。同时也展示了一个单元或感知机。
Listing 1.3.2,mlp-mnist-1.3.2.py展示了使用 MLP 的 MNIST 数字分类器模型的 Keras 实现:
import numpy as np
from keras.models import Sequential
from keras.layers import Dense, Activation, Dropout
from keras.utils import to_categorical, plot_model
from 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 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)
# validate the model on test dataset to determine generalization
loss, acc = model.evaluate(x_test, y_test, batch_size=batch_size)
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 向量的格式,这是一个 10 维向量,所有元素为 0,除了数字类的索引。例如,如果标签是 2,则等效的 one-hot 向量为[0,0,1,0,0,0,0,0,0,0]。第一个标签的索引为0。
以下几行将每个标签转换为 one-hot 向量:
# convert to one-hot vector
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
在深度学习中,数据存储在张量中。张量一词适用于标量(0D 张量)、向量(1D 张量)、矩阵(2D 张量)和多维张量。从现在开始,除非标量、向量或矩阵能使解释更清晰,否则将使用张量一词。
剩余部分计算图像尺寸,第一层Dense的input_size,并将每个像素值从 0 到 255 的范围缩放到 0.0 到 1.0 之间。虽然可以直接使用原始像素值,但最好对输入数据进行归一化,以避免大梯度值,这可能会使训练变得困难。网络的输出也会进行归一化。训练完成后,有一个选项可以通过将输出张量乘以 255,将所有值恢复为整数像素值。
提出的模型基于 MLP 层。因此,输入预计为 1D 张量。因此,x_train和x_test分别被重塑为[60000, 28 * 28]和[10000, 28 * 28]。
# 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
使用 MLP 和 Keras 构建模型
数据准备好后,接下来是构建模型。提议的模型由三层 MLP 构成。在 Keras 中,MLP 层被称为Dense,即密集连接层。第一层和第二层 MLP 在结构上完全相同,每层有 256 个单元,后跟relu激活和dropout。选择 256 个单元是因为 128、512 和 1,024 个单元的性能指标较低。在 128 个单元时,网络收敛较快,但测试准确率较低。增加 512 或 1,024 个单元并未显著提高测试准确率。
单元数是一个超参数。它控制网络的容量。容量是网络能够逼近的函数复杂度的度量。例如,对于多项式,次数就是超参数。随着次数的增加,函数的容量也随之增加。
如下所示的模型,分类器模型是使用 Keras 的顺序模型 API 实现的。如果模型只需要一个输入和一个输出,并通过一系列层进行处理,这种方法已经足够简单。为了简单起见,我们暂时使用这个方法,但在第二章《深度神经网络》中,将介绍 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 中绘制:
relu(x) = max(0,x)

图 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 + e x) | 1.3.2 |
elu |
,其中 ,并且是一个可调超参数 |
1.3.3 |
selu |
selu(x) = k × elu(x,a),其中 k = 1.0507009873554804934193349852946 和 a = 1.6732632423543772848170429916717 | 1.3.4 |
表 1.3.1:常见非线性激活函数的定义
正则化
神经网络有记忆训练数据的倾向,特别是当它的容量足够大时。在这种情况下,网络在面对测试数据时会发生灾难性的失败。这是网络无法泛化的经典案例。为了避免这种倾向,模型使用了正则化层或正则化函数。一个常见的正则化层被称为 dropout。
dropout 的思想很简单。给定一个 dropout 率(这里设为 dropout=0.45),Dropout 层会随机移除该比例的神经元,使其不参与下一层的计算。例如,如果第一层有 256 个神经元,应用 dropout=0.45 后,只有 (1 - 0.45) * 256 = 140 个神经元会参与第二层的计算。Dropout 层使得神经网络能够应对不可预见的输入数据,因为网络经过训练后,即使部分神经元缺失,依然能够正确预测。值得注意的是,dropout 不会用于输出层,并且它只在训练过程中起作用,预测时不会使用 dropout。
除了 dropout 外,还有其他正则化器可以使用,如l1或l2。在 Keras 中,可以对每个层的偏置、权重和激活输出进行正则化。l1和l2通过添加惩罚函数来偏向较小的参数值。l1和l2通过参数值的绝对值和平方的和的分数来强制实施惩罚。换句话说,惩罚函数强迫优化器找到较小的参数值。具有较小参数值的神经网络对输入数据中的噪声更不敏感。
例如,l2权重正则化器与fraction=0.001可以实现为:
from keras.regularizers import l2
model.add(Dense(hidden_units,
kernel_regularizer=l2(0.001),
input_dim=input_size))
如果使用了l1或l2正则化,则不会添加额外的层。正则化会在Dense层内部强制执行。对于所提出的模型,dropout 仍然比l2正则化表现更好。
输出激活和损失函数
输出层有 10 个单元,并采用softmax激活函数。10 个单元对应着 10 个可能的标签、类别或分类。softmax激活函数可以通过以下方程表示:

(方程 1.3.5)
该方程适用于所有N = 10 个输出,x i for i = 0, 1 … 9 的最终预测。softmax的概念出奇地简单。它通过标准化预测,将输出压缩为概率。在这里,每个预测的输出是给定输入图像的正确标签的概率。所有输出的概率总和为 1.0。例如,当softmax层生成预测时,它将是一个 10 维的 1D 张量,输出可能类似于以下内容:
[ 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 之间时,将使用sigmoid。与softmax不同,预测张量中所有元素的总和不被限制为 1.0。例如,在情感预测(0.0 表示坏,1.0 表示好)或图像生成(0.0 表示 0,1.0 表示 255 像素值)中,sigmoid被用作最后一层。
tanh函数将其输入映射到-1.0 到 1.0 的范围。这在输出可以正负波动时非常重要。tanh函数更常用于循环神经网络的内部层,但也曾作为输出层激活函数使用。如果tanh替代sigmoid作为输出激活函数,所使用的数据必须进行适当的缩放。例如,代替在[0.0, 1.0]范围内缩放每个灰度像素

它被指定在范围[-1.0, 1.0]内

.
下图展示了sigmoid和tanh函数。从数学上讲,sigmoid可以通过以下方程式表示:

(方程式 1.3.6)

图 1.3.6:sigmoid与tanh函数图
预测张量与独热编码真实标签向量的差异被称为损失。一种损失函数是mean_squared_error(均方误差),即目标值与预测值之间差异的平方的平均值。在当前示例中,我们使用的是categorical_crossentropy。它是目标与预测值的乘积与预测值对数的总和的负值。Keras 中还提供了其他损失函数,如mean_absolute_error和binary_crossentropy。损失函数的选择并非随意,而应该是模型正在学习的标准。对于分类问题,categorical_crossentropy或mean_squared_error是softmax激活层之后的一个不错的选择。而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 的任意位置开始,梯度为

GD 算法要求 x 随后更新为

新的 x 值等于旧的值,加上梯度的相反方向并按比例缩放。

。这个小数字

指的是学习率。如果

,新值的 x = -0.48。
GD 通过迭代执行。在每一步中,y 将越来越接近其最小值。在 x = 0.5 时,

,GD 已经找到了 y = -1.25 的绝对最小值。梯度建议 x 不再改变。
学习率的选择至关重要。一个较大的值

可能无法找到最小值,因为搜索可能会在最小值附近来回摆动。另一方面,过小的值

可能需要进行大量的迭代才能找到最小值。在多重极小值的情况下,搜索可能会陷入局部最小值。

图 1.3.7:梯度下降类似于沿着函数曲线走下坡,直到到达最低点。在这个图中,全局最小值位于 x = 0.5。
多重极小值的例子可以在图 1.3.8中看到。如果出于某种原因搜索从图的左侧开始,且学习率非常小,则 GD 有很高的概率将 x = -1.51 作为 y 的最小值,而不会找到 x = 1.66 的全局最小值。一个足够的学习率将使梯度下降能够越过 x = 0.0 处的小山。在深度学习实践中,通常建议从较大的学习率(例如 0.1 到 0.001)开始,并随着损失接近最小值逐渐减小学习率。

图 1.3.8:具有两个最小值的函数图,x = -1.51 和 x = 1.66。图中还显示了该函数的导数。
梯度下降在深度神经网络中通常不使用,因为你经常会遇到需要训练的数百万个参数。执行完全的梯度下降在计算上效率低下。相反,使用了 SGD(随机梯度下降)。在 SGD 中,选择一个小批量样本来计算下降的近似值。参数(例如权重和偏置)通过以下公式进行调整:

(公式 1.3.7)
在这个公式中,

和

分别是损失函数的参数和梯度张量。g 是通过损失函数的偏导数计算得到的。为了优化 GPU 性能,建议将小批量大小设置为 2 的幂。在所提议的网络中,batch_size=128。
公式 1.3.7 计算了最后一层的参数更新。那么,如何调整前面层的参数呢?对于这种情况,应用微分的链式法则将导数传播到更低的层,并相应地计算梯度。这个算法在深度学习中被称为反向传播。反向传播的细节超出了本书的范围。不过,可以在neuralnetworksanddeeplearning.com找到一个很好的在线参考。
由于优化是基于微分的,因此损失函数的一个重要标准是它必须是平滑的或可微分的。当引入新的损失函数时,这个约束是非常重要的。
给定训练数据集、损失函数、优化器和正则化器的选择,现在可以通过调用 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 数据、训练的 epoch 数量和批量大小,fit() 就会处理剩下的部分。在其他深度学习框架中,这需要执行多个任务,比如将输入和输出数据转换为正确的格式、加载、监控等。所有这些都必须在 for 循环内完成!在 Keras 中,所有工作都在一行代码中完成。
在 fit() 函数中,epoch 是对整个训练数据集的完整采样。batch_size 参数是每次训练步骤处理的输入样本数量。要完成一个 epoch,fit() 需要用训练数据集的大小除以批量大小,再加 1 以补偿任何小数部分。
性能评估
到目前为止,MNIST 数字分类器的模型已经完成。性能评估将是下一个关键步骤,以确定所提出的模型是否提供了令人满意的解决方案。训练模型 20 个 epoch 足以获得可比的性能指标。
以下表格 表 1.3.2 显示了不同网络配置和相应的性能指标。在 Layers 列中,展示了第 1 到第 3 层的单元数。对于每个优化器,使用了 Keras 中的默认参数。可以观察到改变正则化器、优化器和每层单元数的效果。在 表 1.3.2 中的另一个重要观察结果是,较大的网络不一定会带来更好的性能。
增加网络深度对于训练和测试数据集的准确性没有带来额外的好处。另一方面,像 128 这样的较小单元数也可能会降低测试和训练的准确率。当去除正则化器并使用每层 256 个单元时,获得了 99.93%的最佳训练准确率。然而,由于网络过拟合,测试准确率要低得多,为 98.0%。
最高的测试准确率是在使用 Adam 优化器和 Dropout(0.45) 时达到 98.5%。从技术上讲,仍然存在一定程度的过拟合,因为其训练准确率为 99.39%。对于 256-512-256、Dropout(0.45) 和 SGD,训练和测试准确率都是 98.2%。去除 Regularizer 和 ReLU 层会导致最差的性能。通常,我们会发现 Dropout 层的性能优于 l2。
以下表格展示了在调优过程中典型的深度神经网络性能。该示例表明,网络架构需要改进。在接下来的部分中,另一个使用 CNN 的模型显示了测试准确率的显著提高:
| 层数 | 正则化器 | 优化器 | 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 | Dropout(0.4) | SGD | 是 | 98.23 | 98.1 |
| 256-256-256 | Dropout(0.45) | SGD | 是 | 98.07 | 98.1 |
| 256-256-256 | Dropout(0.5) | SGD | 是 | 97.68 | 98.1 |
| 256-256-256 | Dropout(0.6) | SGD | 是 | 97.11 | 97.9 |
| 256-512-256 | Dropout(0.45) | SGD | 是 | 98.21 | 98.2 |
| 512-512-512 | Dropout(0.2) | SGD | 是 | 99.45 | 98.3 |
| 512-512-512 | Dropout(0.4) | SGD | 是 | 98.95 | 98.3 |
| 512-1024-512 | Dropout(0.45) | SGD | 是 | 98.90 | 98.2 |
| 1024-1024-1024 | Dropout(0.4) | SGD | 是 | 99.37 | 98.3 |
| 256-256-256 | Dropout(0.6) | Adam | 是 | 98.64 | 98.2 |
| 256-256-256 | Dropout(0.55) | Adam | 是 | 99.02 | 98.3 |
| 256-256-256 | Dropout(0.45) | Adam | 是 | 99.39 | 98.5 |
| 256-256-256 | Dropout(0.45) | RMSprop | 是 | 98.75 | 98.1 |
| 128-128-128 | Dropout(0.45) | Adam | 是 | 98.70 | 97.7 |
模型总结
使用 Keras 库可以通过调用以下方法快速验证模型描述:
model.summary()
清单 1.3.2展示了提出的网络模型的总结。该模型需要总计 269,322 个参数。考虑到我们只是一个简单的 MNIST 数字分类任务,这个参数数量相当可观。MLP 模型并不高效。参数的数量可以通过图 1.3.4来计算,重点关注感知机如何计算输出。从输入到 Dense 层:784 × 256 + 256 = 200,960。第一个 Dense 到第二个 Dense:256 × 256 + 256 = 65,792。从第二个 Dense 到输出层:10 × 256 + 10 = 2,570。总数为 269,322。
清单 1.3.2 展示了 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) 2570
_________________________________________________________________
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()的结果类似,但以图形化的方式显示了每一层的相互连接和输入输出。

图 1.3.9:MLP MNIST 数字分类器的图形描述
卷积神经网络(CNNs)
现在我们将进入第二种人工神经网络——卷积神经网络(CNNs)。在本节中,我们将解决相同的 MNIST 数字分类问题,不过这次我们将使用 CNN。
图 1.4.1展示了我们将用于 MNIST 数字分类的 CNN 模型,模型的实现细节则在清单 1.4.1中进行了说明。为了实现 CNN 模型,之前模型的一些部分需要做出调整。输入向量不再是原来的形式,而是新的维度(高度、宽度、通道数),对于灰度的 MNIST 图像来说,输入形状是(image_size, image_size, 1) = (28, 28, 1)。因此,训练和测试图像需要重新调整大小以符合这一输入形状要求。

图 1.4.1:用于 MNIST 数字分类的 CNN 模型
清单 1.4.1,cnn-mnist-1.4.1.py展示了使用 CNN 进行 MNIST 数字分类的 Keras 代码:
import numpy as np
from keras.models import Sequential
from keras.layers import Activation, Dense, Dropout
from keras.layers import Conv2D, MaxPooling2D, Flatten
from keras.utils import to_categorical, plot_model
from 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)
loss, acc = model.evaluate(x_test, y_test, batch_size=batch_size)
print("\nTest accuracy: %.1f%%" % (100.0 * acc))
这里的主要变化是使用了Conv2D层。relu激活函数已经是Conv2D的一个参数。使用批量归一化层时,relu函数可以作为一个Activation层单独使用。批量归一化在深度 CNN 中非常常见,可以使得在训练过程中使用较大的学习率,而不会引起不稳定。
卷积
如果在 MLP 模型中,单元的数量表示Dense层的特点,那么卷积核则代表 CNN 操作的特点。如图 1.4.2所示,卷积核可以视为一个矩形的图像块或窗口,它从左到右、从上到下滑过整个图像。这一操作被称为卷积。它将输入图像转化为特征图,特征图代表了卷积核从输入图像中学习到的内容。特征图随后会被转化为下一层的特征图,依此类推。每个Conv2D生成的特征图的数量由filters参数控制。

图 1.4.2:一个 3 × 3 的卷积核与 MNIST 数字图像进行卷积。卷积过程通过步骤 t[n]和 t[n+1]展示,其中卷积核向右移动了 1 个像素的步幅。
卷积过程中涉及的计算如图 1.4.3所示。为了简便起见,假设输入图像(或输入特征图)是一个 5 × 5 的矩阵,应用了一个 3 × 3 的卷积核。卷积后的特征图也在图中展示。图中特征图中的一个元素已被阴影标出。你会注意到,卷积后的特征图比原始输入图像要小,这是因为卷积只在有效元素上进行,卷积核不能越过图像的边界。如果希望输入和输出特征图的尺寸保持一致,可以将Conv2D的参数设置为padding='same'。输入图像的边界会被零填充,从而在卷积后保持尺寸不变:

图 1.4.3:卷积操作展示了特征图中一个元素是如何被计算出来的
池化操作
最后的变化是添加了一个MaxPooling2D层,参数为pool_size=2。MaxPooling2D会压缩每个特征图。每个大小为pool_size × pool_size的区域都会被缩减为一个像素,值等于该区域内的最大像素值。以下图展示了两个区域的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 模型的输出层。
性能评估和模型总结
如列表 1.4.2所示,与使用 MLP 层时的 269,322 相比,列表 1.4.1 中的 CNN 模型所需参数较少,为 80,226。conv2d_1 层有 640 个参数,因为每个卷积核有 3 × 3 = 9 个参数,且每个 64 个特征图都有一个卷积核和一个偏置参数。其他卷积层的参数数量可以通过类似的方式计算。图 1.4.5 显示了 CNN MNIST 数字分类器的图形表示。
表 1.4.1 显示了使用 Adam 优化器和 dropout=0.2 的 3 层网络,每层 64 个特征图时,最大测试准确率为 99.4%。CNN 在参数效率上更高,并且具有比 MLP 更高的准确性。同样,CNN 也适合用于从顺序数据、图像和视频中学习表示。
列表 1.4.2 显示了 CNN MNIST 数字分类器的总结:
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d_1 (Conv2D) (None, 26, 26, 64) 640
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 13, 13, 64) 0
_________________________________________________________________
conv2d_2 (Conv2D) (None, 11, 11, 64) 36928
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 5, 5, 64) 0
_________________________________________________________________
conv2d_3 (Conv2D) (None, 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 数字分类器的图形描述
| 层数 | 优化器 | 正则化器 | 训练准确率,% | 测试准确率,% |
|---|---|---|---|---|
| 64-64-64 | SGD | Dropout(0.2) | 97.76 | 98.50 |
| 64-64-64 | RMSprop | Dropout(0.2) | 99.11 | 99.00 |
| 64-64-64 | Adam | Dropout(0.2) | 99.75 | 99.40 |
| 64-64-64 | Adam | Dropout(0.4) | 99.64 | 99.30 |
递归神经网络(RNNs)
现在我们将看看我们三个人工神经网络中的最后一个——递归神经网络(RNNs)。
RNNs 是一类适用于学习顺序数据表示的网络,例如自然语言处理(NLP)中的文本或仪器中的传感器数据流。虽然每个 MNIST 数据样本本身并非顺序数据,但不难想象,每个图像都可以被解释为一系列像素行或列的顺序。因此,基于 RNN 的模型可以将每个 MNIST 图像处理为 28 元素输入向量的序列,时间步等于 28。以下列表显示了图 1.5.1中 RNN 模型的代码:

图 1.5.1:用于 MNIST 数字分类的 RNN 模型
在以下列表中,列表 1.5.1,rnn-mnist-1.5.1.py 显示了使用 RNNs 进行 MNIST 数字分类的 Keras 代码:
import numpy as np
from keras.models import Sequential
from keras.layers import Dense, Activation, SimpleRNN
from keras.utils import to_categorical, plot_model
from 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)
loss, acc = model.evaluate(x_test, y_test, batch_size=batch_size)
print("\nTest accuracy: %.1f%%" % (100.0 * acc))
RNN 与前两种模型之间有两个主要区别。首先是input_shape = (image_size, image_size),实际上是input_shape = (timesteps, input_dim),即一个长度为timesteps的input_dim维度向量序列。其次是使用SimpleRNN层来表示一个具有units=256的 RNN 单元。units变量表示输出单元的数量。如果 CNN 的特点是卷积核在输入特征图上进行卷积,那么 RNN 的输出不仅是当前输入的函数,还与上一输出或隐藏状态有关。由于上一输出也是上一输入的函数,因此当前输出也是上一输出和输入的函数,依此类推。Keras 中的SimpleRNN层是 RNN 的简化版本。以下方程描述了 SimpleRNN 的输出:
ht = tanh(b + Wht-1 + Uxt) (1.5.1)
在此方程中,b是偏置项,W和U分别被称为递归核(上一输出的权重)和核(当前输入的权重)。下标t用于表示序列中的位置。对于SimpleRNN层,units=256时,总参数数量为 256 + 256 × 256 + 256 × 28 = 72,960,分别对应b、W和U的贡献。
以下图展示了在 MNIST 数字分类中使用的 SimpleRNN 和 RNN 的示意图。SimpleRNN比 RNN 更简单的原因是缺少在 softmax 计算之前的输出值Ot = Vht + c:

图 1.5.2:SimpleRNN 和 RNN 的示意图
与 MLP 或 CNN 相比,RNN 在初始阶段可能更难理解。在 MLP 中,感知机是基本单元。一旦理解了感知机的概念,MLP 就只是感知机的网络。在 CNN 中,卷积核是一个滑过特征图以生成另一个特征图的窗口。在 RNN 中,最重要的是自循环的概念。实际上,只有一个单元。
多个单元的错觉出现是因为每个时间步长都有一个单元,但实际上,除非网络被展开,否则它只是相同的单元被重复使用。RNN 的底层神经网络在单元之间是共享的。
列表 1.5.2中的总结指出,使用SimpleRNN需要较少的参数。图 1.5.3展示了 RNN MNIST 数字分类器的图形描述。该模型非常简洁。表 1.5.1显示,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) 0
=================================================================
Total params: 75,530
Trainable params: 75,530
Non-trainable params: 0

图 1.5.3:RNN MNIST 数字分类器的图形描述
| 层数 | 优化器 | 正则化器 | 训练准确率,% | 测试准确率,% |
|---|---|---|---|---|
| 256 | SGD | Dropout(0.2) | 97.26 | 98.00 |
| 256 | RMSprop | Dropout(0.2) | 96.72 | 97.60 |
| 256 | Adam | Dropout(0.2) | 96.79 | 97.40 |
| 512 | SGD | Dropout(0.2) | 97.88 | 98.30 |
表 1.5.1:不同的 SimpleRNN 网络配置和性能测量
在许多深度神经网络中,RNN 家族的其他成员更常被使用。例如,长短期记忆(LSTM)网络已广泛应用于机器翻译和问答问题。LSTM 网络解决了长期依赖问题,即将相关的过去信息记忆到当前输出。
与 RNN 或 SimpleRNN 不同,LSTM 单元的内部结构更为复杂。图 1.5.4展示了在 MNIST 数字分类上下文中的 LSTM 示意图。LSTM 不仅使用当前输入和过去的输出或隐藏状态,还引入了一个单元状态,st,用于将信息从一个单元传递到另一个单元。单元状态之间的信息流由三个门控控制,分别是ft、it 和qt。这三个门控的作用是决定哪些信息应被保留或替换,以及过去和当前输入中的哪些信息应对当前单元状态或输出做出贡献。本书中不讨论 LSTM 单元的内部结构的详细内容。但是,关于 LSTM 的直观指南可以参考:colah.github.io/posts/2015-08-Understanding-LSTMs。
LSTM()层可以作为SimpleRNN()的直接替代。如果 LSTM 对当前任务而言过于复杂,可以使用一个更简单的版本,称为门控循环单元(GRU)。GRU 通过将单元状态和隐藏状态结合来简化 LSTM。GRU 还通过减少一个门控来降低复杂度。GRU()函数也可以作为SimpleRNN()的直接替代。

图 1.5.4:LSTM 的示意图。为了清晰起见,参数未显示。
配置 RNN 的方式有很多种。一种方式是创建一个双向的 RNN 模型。默认情况下,RNN 是单向的,即当前输出仅受过去状态和当前输入的影响。而在双向 RNN 中,未来状态也可以通过允许信息反向流动来影响当前状态和过去状态。过去的输出会根据接收到的新信息进行更新。可以通过调用包装函数将 RNN 转为双向。例如,双向 LSTM 的实现是Bidirectional(LSTM())。
对于所有类型的 RNN,增加单元数也会增加模型的容量。然而,增加容量的另一种方式是通过堆叠 RNN 层。不过,作为一般经验法则,只有在必要时才应增加模型容量。过多的容量可能导致过拟合,从而导致训练时间延长并在预测时出现较慢的性能。
结论
本章概述了三种深度学习模型——MLP、RNN 和 CNN——并介绍了 Keras,这是一个用于快速开发、训练和测试这些深度学习模型的库。本章还讨论了 Keras 的顺序 API。在下一章中,将介绍功能性 API,它将使我们能够构建更复杂的模型,特别是针对高级深度神经网络。
本章还回顾了深度学习中的重要概念,如优化、正则化和损失函数。为了便于理解,这些概念是在 MNIST 数字分类的背景下呈现的。我们还讨论了使用人工神经网络(特别是 MLP、CNN 和 RNN)解决 MNIST 数字分类的不同方法,并讨论了它们的性能评估。MLP、CNN 和 RNN 是深度神经网络的重要构建块。
通过对深度学习概念的理解,以及如何将 Keras 用作这些概念的工具,我们现在已经准备好分析高级深度学习模型。在下一章讨论功能性 API 后,我们将进入流行深度学习模型的实现。后续章节将讨论一些高级主题,如自编码器、GAN、VAE 和强化学习。配套的 Keras 代码实现将对理解这些主题起到重要作用。
参考文献
- LeCun, Yann, Corinna Cortes, 和 C. J. Burges. MNIST 手写数字数据库。AT&T 实验室 [在线]. 可用链接:
yann. lecun. com/exdb/mnist 2 (2010)。
第二章:深度神经网络
在本章中,我们将研究深度神经网络。这些网络在像 ImageNet、CIFAR10 和 CIFAR100 等更具挑战性和高级的数据集上的分类准确度表现优秀。为了简洁起见,我们将重点关注两个网络:ResNet [2][4] 和 DenseNet [5]。虽然我们会进行更详细的讲解,但在深入之前,有必要简要介绍这两个网络:
ResNet 引入了残差学习的概念,通过解决深度卷积网络中的梯度消失问题,使得它能够构建非常深的网络。
DenseNet 通过允许每个卷积层直接访问输入和较低层的特征图,进一步改进了 ResNet 技术。它还通过利用 瓶颈 层和 过渡 层,在深度网络中成功地保持了较低的参数数量。
为什么选择这两个模型,而不是其他模型?自从它们问世以来,已有无数模型(如 ResNeXt [6] 和 FractalNet [7])受到了这两个网络使用的技术的启发。同样,通过理解 ResNet 和 DenseNet,我们能够利用它们的设计指南构建自己的模型。通过迁移学习,这也将使我们能够利用预训练的 ResNet 和 DenseNet 模型来为我们自己的目标服务。这些原因,加上它们与 Keras 的兼容性,使得这两个模型非常适合本书中探讨的高级深度学习内容。
虽然本章的重点是深度神经网络;我们将从讨论 Keras 中一个重要特性 功能性 API 开始。这个 API 作为构建网络的替代方法,可以帮助我们构建比顺序模型更复杂的网络。我们之所以如此关注这个 API,是因为它将在构建深度网络时变得非常有用,尤其是本章所聚焦的两个网络。建议在继续阅读本章之前,先完成第一章,介绍 Keras 高级深度学习,因为我们将在本章中使用该章的入门级代码和概念,并将其提升到更高级的水平。
本章的目标是介绍:
-
Keras 中的功能性 API,并探索运行此 API 的网络实例
-
深度残差网络(ResNet 版本 1 和 2)在 Keras 中的实现
-
将密集连接卷积网络(DenseNet)实现到 Keras 中
-
探索两个流行的深度学习模型:ResNet 和 DenseNet
功能性 API
在我们在第一章中首先介绍的顺序模型中,《深入了解 Keras 中的高级深度学习》,层是堆叠在另一个层之上的。通常,模型将通过其输入层和输出层进行访问。我们还了解到,如果我们想在网络中间添加一个辅助输入,或者在倒数第二层之前提取一个辅助输出,就没有简单的机制。
那个模型也有其不足之处,例如,它不支持图形化模型或像 Python 函数一样行为的模型。此外,也很难在两个模型之间共享层。函数式 API 解决了这些限制,这也是它成为任何想从事深度学习模型工作的人不可或缺的工具的原因。
函数式 API 遵循以下两个概念:
-
层是一个接受张量作为参数的实例。层的输出是另一个张量。为了构建模型,层实例是通过输入和输出张量相互链接的对象。这将产生与在顺序模型中堆叠多个层相似的最终结果。然而,使用层实例使得模型更容易具有辅助输入或多个输入输出,因为每个层的输入/输出都可以直接访问。
-
模型是一个在一个或多个输入张量和输出张量之间的函数。在模型的输入和输出之间,张量是通过层输入和输出张量相互链接的层实例。因此,模型是一个或多个输入层和一个或多个输出层的函数。模型实例规范了计算图,描述了数据如何从输入流向输出。
完成函数式 API 模型构建后,训练和评估是通过与顺序模型相同的函数来执行的。举个例子,在函数式 API 中,具有 32 个滤波器的 2D 卷积层Conv2D,以x作为层的输入张量,以y作为层的输出张量,可以写成:
y = Conv2D(32)(x)
我们还能够堆叠多个层来构建我们的模型。例如,我们可以像下面的代码示例那样,重写在上章创建的 CNN on MNIST 代码:
你将会看到以下列表 2.1.1,cnn-functional-2.1.1.py,它展示了我们如何使用函数式 API 转换cnn-mnist-1.4.1.py代码:
import numpy as np
from keras.layers import Dense, Dropout, Input
from keras.layers import Conv2D, MaxPooling2D, Flatten
from keras.models import Model
from keras.datasets import mnist
from keras.utils import to_categorical
# 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)
# 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
# image is processed as is (square grayscale)
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)
print("\nTest accuracy: %.1f%%" % (100.0 * score[1]))
默认情况下,MaxPooling2D使用pool_size=2,因此该参数已被移除。
在前面的代码中,每个层都是张量的函数。它们每个都会生成一个张量作为输出,这个输出会成为下一个层的输入。为了创建这个模型,我们可以调用Model()并提供inputs和outputs张量,或者提供张量的列表。其他部分保持不变。
相同的列表也可以使用 fit() 和 evaluate() 函数进行训练和评估,与顺序模型类似。sequential 类实际上是 Model 类的子类。我们需要记住在 fit() 函数中插入了 validation_data 参数以查看训练过程中验证准确度的进展。准确度在 20 个 epoch 中的范围从 99.3% 到 99.4%。
创建一个两输入一输出的模型
现在我们将要做一些非常激动人心的事情,创建一个具有两个输入和一个输出的高级模型。在我们开始之前,重要的是要知道,这对于顺序模型来说并不是一件简单的事情。
假设有一个新的 MNIST 数字分类模型被发明了,它被称为Y-Network,如 图 2.1.1 所示。Y-Network 使用相同的输入两次,分别在左右 CNN 分支上。网络使用 concatenate 层合并结果。合并操作 concatenate 类似于沿着连接轴堆叠两个形状相同的张量以形成一个张量。例如,沿着最后一个轴连接两个形状为 (3, 3, 16) 的张量将得到一个形状为 (3, 3, 32) 的张量。
concatenate 层之后的其他内容与之前的 CNN 模型保持不变,即 Flatten-Dropout-Dense:

图 2.1.1: Y-Network 接受相同的输入两次,但在两个卷积网络分支中处理输入。分支的输出通过 concatenate 层组合。最后一层的预测将与之前的 CNN 示例类似。
要改进 列表 2.1.1 中模型的性能,我们可以提出几个改变。首先,Y-Network 的分支正在将滤波器数量加倍,以补偿 MaxPooling2D() 后特征映射大小的减半。例如,如果第一个卷积的输出为 (28, 28, 32),经过最大池化后新形状为 (14, 14, 32)。接下来的卷积将有 64 个滤波器大小,并且输出尺寸为 (14, 14, 64)。
其次,尽管两个分支的卷积核尺寸都为 3,右分支使用了扩张率为 2。图 2.1.2 展示了在大小为 3 的卷积核上不同扩张率的效果。这个想法是通过增加扩张率增加卷积核的覆盖范围,CNN 将使右分支能够学习不同的特征映射。我们将使用 padding='same' 选项确保在使用扩张 CNN 时不会出现负张量维度。通过使用 padding='same',我们将保持输入的尺寸与输出特征映射的尺寸相同。这是通过填充输入以确保输出具有相同大小来完成的:

图 2.1.2: 通过增加扩张率从 1 开始,有效卷积核覆盖范围也会增加。
以下清单展示了 Y-网络的实现。两个分支是通过两个 for 循环创建的。两个分支期望相同的输入形状。这两个 for 循环将创建两个 3 层堆叠的 Conv2D-Dropout-MaxPooling2D。尽管我们使用了 concatenate 层来组合左右分支的输出,但我们也可以使用 Keras 的其他合并函数,例如 add、dot、multiply。合并函数的选择并非完全任意,而是必须基于合理的模型设计决策。
在 Y-网络中,concatenate 不会丢弃特征图的任何部分。相反,我们会让 Dense 层来处理拼接后的特征图。清单 2.1.2,cnn-y-network-2.1.2.py 展示了使用功能性 API 实现的 Y-网络:
import numpy as np
from keras.layers import Dense, Dropout, Input
from keras.layers import Conv2D, MaxPooling2D, Flatten
from keras.models import Model
from keras.layers.merge import concatenate
from keras.datasets import mnist
from keras.utils import to_categorical
from keras.utils import plot_model
# 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)
# 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-MaxPooling2D
# 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 layer
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)
print("\nTest accuracy: %.1f%%" % (100.0 * score[1]))
值得注意的是,Y-网络需要两个输入进行训练和验证。这两个输入是相同的,因此提供了[x_train, x_train]。
在 20 个周期中,Y-网络的准确率从 99.4% 上升到 99.5%。这相比于 3 层堆叠的 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 会超出本书的范围。
注意
读者可以访问keras.io/以获取有关功能性 API 的更多信息。
深度残差网络(ResNet)
深度网络的一个关键优势是它们能够从输入和特征图中学习不同层次的表示。在分类、分割、检测以及许多其他计算机视觉问题中,学习不同层次的特征通常能带来更好的性能。
然而,您会发现训练深度网络并不容易,因为在反向传播过程中,浅层的梯度会随着深度的增加而消失(或爆炸)。图 2.2.1 说明了梯度消失的问题。网络参数通过从输出层到所有前一层的反向传播进行更新。由于反向传播是基于链式法则的,因此梯度在到达浅层时会逐渐减小。这是因为小数的乘积,特别是当误差和参数的绝对值很小时。
乘法操作的次数将与网络的深度成正比。还值得注意的是,如果梯度退化,参数将无法得到适当的更新。
因此,网络将无法提升其性能:

图 2.2.1:深度网络中的一个常见问题是梯度在反向传播过程中传递到浅层时会消失。

图 2.2.2:典型 CNN 中的一个块与 ResNet 中的一个块的对比。为了防止反向传播过程中梯度退化,引入了快捷连接。
为了缓解深度网络中梯度的退化问题,ResNet 引入了深度残差学习框架的概念。让我们分析一下一个块,一个深度网络的小段。
上图显示了典型 CNN 块与 ResNet 残差块之间的对比。ResNet 的理念是,为了防止梯度退化,我们将让信息通过快捷连接流向浅层。
接下来,我们将进一步探讨这两个块之间差异的更多细节。图 2.2.3 显示了另一种常用深度网络 VGG[3] 和 ResNet 的 CNN 块的更多细节。我们将层特征图表示为 x。层 l 的特征图是

CNN 层中的操作是Conv2D-批量归一化(BN)-ReLU。
假设我们将这一组操作表示为 H() = Conv2D-批量归一化(BN)-ReLU,这将意味着:

(方程 2.2.1)

(方程 2.2.2)
换句话说,l - 2 层的特征图被转换为

由 H() = Conv2D-批量归一化(BN)-ReLU。相同的一组操作应用于转换

到

换句话说,如果我们有一个 18 层的 VGG,那么在输入图像转化为第 18 层特征图之前,会进行 18 次 H() 操作。
一般来说,我们可以观察到,层 l 输出的特征图仅受前一层特征图的直接影响。同时,对于 ResNet:

(方程 2.2.3)

(方程 2.2.4)

图 2.2.3:普通 CNN 块和残差块的详细层操作

由Conv2D-BN构成,这也被称为残差映射。+符号表示快捷连接与输出的张量元素逐一相加

快捷连接不会增加额外的参数或计算复杂度。
在 Keras 中,add 操作可以通过add()合并函数来实现。然而,两者

方程式和x应该具有相同的维度。如果维度不同,例如在更改特征图大小时,我们应该对x进行线性投影,以匹配

在原始论文中,当特征图大小减半时,线性投影是通过一个 1 × 1 卷积核和strides=2的Conv2D来实现的。
回到第一章,在介绍高级深度学习与 Keras时,我们讨论了stride > 1等价于在卷积过程中跳过像素。例如,如果strides=2,则在滑动卷积核时,每跳过一个像素。
前述的方程式 2.2.3和2.2.4都表示 ResNet 残差块的操作。它们暗示,如果更深层的网络可以训练得错误较少,那么浅层网络不应该有更多的错误。
了解了 ResNet 的基本构建块后,我们可以设计一个用于图像分类的深度残差网络。然而,这次我们将处理一个更具挑战性和高级的数据集。
在我们的示例中,我们将考虑 CIFAR10 数据集,这是原始论文验证过的其中一个数据集。在此示例中,Keras 提供了一个 API,可以方便地访问 CIFAR10 数据集,如下所示:
from keras.datasets import cifar10
(x_train, y_train), (x_test, y_test) = cifar10.load_data()
与 MNIST 类似,CIFAR10 数据集包含 10 个类别。该数据集由小型(32 × 32)RGB 真实世界图像组成,包含飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车,每个类别对应一个图像。图 2.2.4显示了 CIFAR10 的示例图像。
在该数据集中,有 50,000 个标记的训练图像和 10,000 个标记的测试图像用于验证:

图 2.2.4:CIFAR10 数据集的示例图像。完整数据集包含 50,000 个标记的训练图像和 10,000 个标记的测试图像用于验证。
对于 CIFAR10 数据集,ResNet 可以使用不同的网络架构来构建,如表 2.2.1所示。n的值和 ResNet 的相应架构已在表 2.2.2中验证过。表 2.2.1表示我们有三组残差块。每组有2n层,对应于n个残差块。在 32 × 32 的输入图像中,额外的层是第一层。
卷积核大小为 3,除了两个不同大小特征图之间的过渡层,它实现了线性映射。例如,Conv2D 的卷积核大小为 1,strides=2。为了与 DenseNet 保持一致,我们将在连接两个不同大小的残差块时使用过渡层(Transition Layer)一词。
ResNet 使用 kernel_initializer='he_normal' 来帮助反向传播时的收敛[1]。最后一层由 AveragePooling2D-Flatten-Dense 组成。值得注意的是,ResNet 不使用 dropout。并且看起来 add 合并操作和 1 × 1 卷积具有自我正则化效果。图 2.2.4 展示了 CIFAR10 数据集的 ResNet 模型架构,如 表 2.2.1 中所述。
以下代码片段展示了 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 在残差模块设计上有所改进,从而提高了性能。
| 层数 | 输出大小 | 卷积核大小 | 操作 |
|---|---|---|---|
| 卷积 | 32 × 32 | 16 | ![]() |
| 残差模块(1) | 32 × 32 | ![]() |
|
| 过渡层(1) | 32 × 32 | ![]() |
|
| 16 × 16 | |||
| 残差模块(2) | 16 × 16 | 32 | ![]() |
| 过渡层(2) | 16 × 16 | ![]() |
|
| 8 × 8 | |||
| 残差模块(3) | 8 × 8 | 64 | ![]() |
| 平均池化 | 1 × 1 | ![]() |
表 2.2.1:ResNet 网络架构配置

图 2.2.4:用于 CIFAR10 数据集分类的 ResNet 模型架构
| # 层数 | 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:用 CIFAR10 验证的 ResNet 架构
以下代码片段展示了 resnet-cifar10-2.2.1.py 的部分代码,这是 ResNet v1 的 Keras 实现:
def resnet_v1(input_shape, depth, num_classes=10):
if (depth - 2) % 6 != 0:
raise ValueError('depth should be 6n+2 (eg 20, 32, 44 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
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)
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 实现相比,有一些小的差异。特别是,我们不使用 SGD,而是使用 Adam。这是因为使用 Adam 时,ResNet 更容易收敛。我们还将使用学习率(lr)调度器lr_schedule(),在 80、120、160 和 180 个 epoch 时从默认的 1e-3 开始逐步减少lr。lr_schedule()函数将在训练的每个 epoch 后作为callbacks变量的一部分被调用。
另一个回调函数会在验证精度有进展时每次保存检查点。在训练深度网络时,保存模型或权重检查点是一个好习惯。因为训练深度网络需要大量时间。当你想使用你的网络时,只需要重新加载检查点,训练好的模型就会被恢复。这可以通过调用 Keras 的load_model()来实现。lr_reducer()函数也被包含在内。如果在调度减少学习率之前,验证损失没有改善,该回调函数将在patience=5个 epoch 后通过某个因子减少学习率。
当调用model.fit()方法时,会提供callbacks变量。与原始论文相似,Keras 实现使用数据增强ImageDataGenerator()来提供额外的训练数据,作为正则化方案的一部分。随着训练数据量的增加,泛化能力会有所提升。
例如,一个简单的数据增强是翻转狗的照片,如下图所示(horizontal_flip=True)。如果原图是狗的照片,那么翻转后的图像仍然是狗的照片。你也可以进行其他变换,如缩放、旋转、白化等,标签依然保持不变:

图 2.2.5:一个简单的数据增强是翻转原始图像
精确复制原始论文中的实现通常是困难的,特别是在使用的优化器和数据增强方面,因为本书中 Keras 实现的 ResNet 模型与原始论文中的模型在性能上存在轻微差异。
ResNet v2
在发布了关于 ResNet 的第二篇论文[4]之后,前一部分中介绍的原始模型被称为 ResNet v1。改进版的 ResNet 通常称为 ResNet v2。该改进主要体现在残差块中层的排列,如下图所示。
ResNet v2 的显著变化包括:
-
使用 1 × 1 - 3 × 3 - 1 × 1
BN-ReLU-Conv2D堆叠 -
批量归一化和 ReLU 激活在 2D 卷积之前

图 2.3.1:ResNet v1 与 ResNet v2 之间残差块的对比
ResNet v2 也在与resnet-cifar10-2.2.1.py相同的代码中实现:
def resnet_v2(input_shape, depth, num_classes=10):
if (depth - 2) % 9 != 0:
raise ValueError('depth should be 9n+2 (eg 56 or 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
if res_block == 0: # first layer and first stage
activation = None
batch_normalization = False
else:
num_filters_out = num_filters_in * 2
if res_block == 0: # 1st layer but not 1st stage
strides = 2 # downsample
# 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:
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 的准确度在以下表格中展示:
| # 层数 | n | CIFAR10 精度(原文) | CIFAR10 精度(本书) |
|---|---|---|---|
| ResNet56 | 9 | 无 | 93.01 |
| ResNet110 | 18 | 93.63 | 93.15 |
在 Keras 应用包中,ResNet50 也已经实现,并配有相应的检查点以便重用。这是一种替代实现,但绑定于 50 层 ResNet v1。
密集连接卷积网络 (DenseNet)

图 2.4.1:DenseNet 中的 4 层 Dense 块。每一层的输入由所有前面生成的特征图组成。
DenseNet 通过一种不同的方法解决了梯度消失问题。与使用快捷连接不同,所有之前的特征图将作为下一层的输入。前面的图展示了 Dense 块中的密集互连示例。
为简便起见,在此图中我们只显示了四层。请注意,第 l 层的输入是所有先前特征图的拼接。如果我们将 BN-ReLU-Conv2D 视为操作 H(x),则第 l 层的输出为:

(方程 2.4.1)
Conv2D 使用 3×3 的卷积核。每层生成的特征图数量称为增长率,k。通常,k = 12,但在论文《Densely Connected Convolutional Networks》中,Huang 等人(2017)也使用 k = 24 [5]。因此,如果特征图数量为

是

,那么在 图 2.4.1 的 4 层 Dense 块结束时,特征图的总数将是

。
DenseNet 还建议 Dense 块前面应使用 BN-ReLU-Conv2D,并且特征图的数量应为增长率的两倍,

。因此,在 Dense 块结束时,特征图的总数将是 72。我们还将使用相同的卷积核大小,即 3。在输出层,DenseNet 建议我们在 Dense() 和 softmax 分类器之前执行一次平均池化。如果未使用数据增强,必须在 Dense 块的 Conv2D 后添加一个 dropout 层:

图 2.4.2:DenseNet 中一个 Dense 块的层,包含和不包含瓶颈层 BN-ReLU-Conv2D(1)。为清晰起见,我们将卷积核大小作为 Conv2D 的一个参数。
随着网络加深,两个新问题将出现。首先,由于每一层都贡献k个特征图,因此在层l的输入数量为

。因此,特征图在深层中会迅速增长,导致计算变得缓慢。例如,对于一个 101 层的网络,当k = 12 时,这个数值为 1200 + 24 = 1224。
其次,与 ResNet 类似,随着网络深度的增加,特征图的尺寸会被缩小,以增加卷积核的覆盖范围。如果 DenseNet 在合并操作中使用了拼接,它必须解决尺寸不匹配的问题。
为了防止特征图数量增加到计算效率低下的程度,DenseNet 引入了瓶颈层,如图 2.4.2所示。其思路是,每次拼接后,应用一个大小为 4k的 1 × 1 卷积。这种维度减少技术可以防止由Conv2D(3)处理的特征图数量迅速增加。
然后,瓶颈层将 DenseNet 层修改为BN-ReLU-Conv2D(1)-BN-ReLU-Conv2D(3),而不仅仅是BN-ReLU-Conv2D(3)。为了清晰起见,我们在Conv2D中包含了卷积核大小作为参数。使用瓶颈层后,每个Conv2D(3)只处理 4k个特征图,而不是

对于层l。例如,对于 101 层的网络,最后一个Conv2D(3)的输入仍然是 48 个特征图,当k = 12 时,而不是之前计算的 1224:

图 2.4.3:两个 Dense 块之间的过渡层
为了解决特征图尺寸不匹配的问题,DenseNet 将深度网络划分为多个密集块,这些密集块通过过渡层连接,如前图所示。在每个密集块内,特征图的尺寸(即宽度和高度)保持不变。
过渡层的作用是过渡从一个特征图尺寸到另一个较小的特征图尺寸,位于两个密集块之间。尺寸的减小通常是原来的一半。这个过程是通过平均池化层完成的。例如,默认pool_size=2的AveragePooling2D将尺寸从(64, 64, 256)减少到(32, 32, 256)。过渡层的输入是上一个密集块中最后一个拼接层的输出。
然而,在将特征图传递给平均池化之前,它们的数量将通过一定的压缩因子减少,

,使用Conv2D(1)。DenseNet 使用

在他们的实验中。例如,如果上一个密集块的最后一次连接的输出是(64, 64, 512),那么在Conv2D(1)后,特征图的新维度将是(64, 64, 256)。当压缩和维度降低结合在一起时,过渡层由BN-Conv2D(1)-AveragePooling2D层组成。在实际中,批量归一化位于卷积层之前。
构建一个 100 层 DenseNet-BC 用于 CIFAR10
我们现在将构建一个用于 CIFAR10 数据集的DenseNet-BC(瓶颈压缩)模型,具有 100 层,使用我们之前讨论的设计原则。
下表展示了模型配置,图 2.4.3展示了模型架构。列表 2.4.1展示了 100 层 DenseNet-BC 的部分 Keras 实现。我们需要注意的是,我们使用RMSprop,因为它在使用 DenseNet 时比 SGD 或 Adam 收敛得更好。
| 层 | 输出大小 | DenseNet-100 BC |
|---|---|---|
| 卷积 | 32 x 32 | ![]() |
| 密集块(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 | ![]() |
| 分类层 | Flatten-Dense(10)-softmax |
表 2.4.1:用于 CIFAR10 分类的 100 层 DenseNet-BC

图 2.4.3:用于 CIFAR10 分类的 100 层 DenseNet-BC 模型架构
列表 2.4.1,densenet-cifar10-2.4.1.py:如表 2.4.1所示,100 层 DenseNet-BC 的部分 Keras 实现:
# 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()
在列表 2.4.1中的 Keras 实现训练 200 个 epochs 后,准确率为 93.74%,而论文中报告的是 95.49%。使用了数据增强。我们在 DenseNet 中使用了与 ResNet v1/v2 相同的回调函数。
对于更深的层,需要使用 Python 代码中的表格更改growth_rate和depth变量。然而,训练一个深度为 250 或 190 的网络将需要相当长的时间,正如论文中所做的那样。为了给我们一个训练时间的概念,每个 epoch 大约需要在 1060Ti GPU 上运行一个小时。尽管 Keras 应用程序模块中也有 DenseNet 的实现,但它是在 ImageNet 上训练的。
结论
在本章中,我们介绍了作为构建复杂深度神经网络模型的高级方法——功能性 API,并展示了如何使用功能性 API 构建多输入单输出的 Y 型网络。与单一分支 CNN 网络相比,该网络的精度更高。接下来,本书中的其他章节,我们将发现功能性 API 在构建更复杂和高级模型时是不可或缺的。例如,在下一章,功能性 API 将帮助我们构建模块化的编码器、解码器和自编码器。
我们还花费了大量时间探索两个重要的深度网络——ResNet 和 DenseNet。这两个网络不仅在分类中应用广泛,还应用于其他领域,如分割、检测、跟踪、生成以及视觉/语义理解。我们需要记住,理解 ResNet 和 DenseNet 中的模型设计决策,比单纯跟随原始实现更为重要。通过这种方式,我们能够将 ResNet 和 DenseNet 的关键概念应用于我们的实际需求。
参考文献
-
Kaiming He 和其他人. 深入探讨整流器:超越人类级别的 ImageNet 分类性能。IEEE 国际计算机视觉会议论文集,2015(
www.cv-foundation.org/openaccess/content_iccv_2015/papers/He_Delving_Deep_into_ICCV_2015_paper.pdf?spm=5176.100239.blogcont55892.28.pm8zm1&file=He_Delving_Deep_into_ICCV_2015_paper.pdf)。 -
Kaiming He 和其他人. 用于图像识别的深度残差学习。IEEE 计算机视觉与模式识别会议论文集,2016a(
openaccess.thecvf.com/content_cvpr_2016/papers/He_Deep_Residual_Learning_CVPR_2016_paper.pdf)。 -
Karen Simonyan 和 Andrew Zisserman. 用于大规模图像识别的非常深的卷积网络。ICLR,2015(
arxiv.org/pdf/1409.1556/)。 -
Kaiming He 和其他人. 深度残差网络中的身份映射。欧洲计算机视觉会议。Springer 国际出版,2016b(
arxiv.org/pdf/1603.05027.pdf)。 -
Gao Huang 和其他人. 密集连接卷积网络。IEEE 计算机视觉与模式识别会议论文集,2017(
openaccess.thecvf.com/content_cvpr_2017/papers/Huang_Densely_Connected_Convolutional_CVPR_2017_paper.pdf)。 -
谢赛宁(Saining Xie)等人。深度神经网络的聚合残差变换。计算机视觉与模式识别(CVPR),2017 年 IEEE 会议。IEEE,2017(
openaccess.thecvf.com/content_cvpr_2017/papers/Xie_Aggregated_Residual_Transformations_CVPR_2017_paper.pdf)。 -
古斯塔夫·拉尔松(Gustav Larsson)、迈克尔·梅尔(Michael Maire)和格雷戈里·沙赫纳罗维奇(Gregory Shakhnarovich)。Fractalnet: 无残差的超深神经网络。arXiv 预印本 arXiv:1605.07648,2016 年(
arxiv.org/pdf/1605.07648.pdf)。
第三章 自编码器
在上一章,第二章,深度神经网络中,您已介绍了深度神经网络的概念。现在我们将继续研究自编码器,这是一种神经网络架构,旨在找到给定输入数据的压缩表示。
与前几章类似,输入数据可以是多种形式,包括语音、文本、图像或视频。自编码器将尝试找到一种表示或编码,以便对输入数据执行有用的变换。例如,在去噪自编码器中,神经网络将尝试找到一个可以将噪声数据转换为干净数据的编码。噪声数据可能是带有静态噪音的音频录音,然后将其转换为清晰的声音。自编码器将自动从数据中学习编码,无需人工标注。因此,自编码器可以归类为无监督学习算法。
在本书的后续章节中,我们将介绍生成对抗网络(GANs)和变分自编码器(VAEs),它们也是无监督学习算法的代表形式。这与我们在前几章讨论的监督学习算法不同,后者需要人工标注。
在最简单的形式中,自编码器将通过尝试将输入复制到输出的方式来学习表示或编码。然而,使用自编码器并不是简单地将输入复制到输出。否则,神经网络将无法揭示输入分布中的隐藏结构。
自编码器将输入分布编码成低维张量,通常表现为一个向量。这将近似于通常称为潜在表示、编码或向量的隐藏结构。这个过程构成了编码部分。然后,潜在向量将通过解码器部分被解码,以恢复原始输入。
由于潜在向量是输入分布的低维压缩表示,因此应当预期通过解码器恢复的输出只能近似输入。输入和输出之间的不相似度可以通过损失函数来度量。
那么,为什么我们要使用自编码器呢?简单来说,自编码器在其原始形式或作为更复杂神经网络的一部分都有实际应用。它们是理解深度学习高级主题的关键工具,因为它们提供了一个低维的潜在向量。此外,它可以高效处理,用于对输入数据执行结构性操作。常见的操作包括去噪、着色、特征级运算、检测、跟踪和分割,仅举几例。
总结来说,本章的目标是呈现:
-
自编码器的原理
-
如何将自编码器实现到 Keras 神经网络库中
-
去噪和颜色化自编码器的主要特征
自编码器原理
在本节中,我们将介绍自编码器的原理。在本节中,我们将查看使用 MNIST 数据集的自编码器,这是我们在前几章中首次介绍的。
首先,我们需要意识到自编码器有两个操作符,它们是:
-
编码器:它将输入 x 转换为低维潜在向量 z = f(x)。由于潜在向量维度较低,编码器被迫仅学习输入数据的最重要特征。例如,在 MNIST 数字的情况下,重要的特征可能包括书写风格、倾斜角度、笔画圆度、粗细等。本质上,这些是表示数字零到九所需的最重要信息。
-
解码器:它尝试从潜在向量恢复输入,
![自编码器原理]()
。尽管潜在向量维度较低,但它具有足够的大小,使解码器能够恢复输入数据。
解码器的目标是使

尽可能接近 x。通常,编码器和解码器都是非线性函数。z 的维度是它能表示的显著特征数量的度量。为了提高效率并限制潜在编码仅学习输入分布的最显著特性,维度通常远小于输入维度[1]。
当潜在编码的维度明显大于 x 时,自编码器倾向于记住输入。
适当的损失函数,

,是输入 x 和输出(即恢复的输入)之间不相似度的度量,

如下方的方程所示,均方误差(MSE)是这种损失函数的一个例子:

(方程 3.1.1)
在此示例中,m 是输出维度(例如,在 MNIST 中 m = 宽度 × 高度 × 通道 = 28 × 28 × 1 = 784)。

和

是 x 的元素,并且

分别。由于损失函数是输入和输出之间不相似度的度量,我们可以使用其他重建损失函数,如二元交叉熵或结构相似性指数(SSIM)。

图 3.1.1:自编码器的框图

图 3.1.2:带有 MNIST 数字输入和输出的自编码器。潜在向量是 16 维的。
为了使自编码器具有上下文,x 可以是一个 MNIST 数字,其维度为 28 × 28 × 1 = 784。编码器将输入转换为一个低维度的 z,它可以是一个 16 维的潜在向量。解码器将尝试以以下形式恢复输入

从 z 中获取。直观地看,每个 MNIST 数字 x 将与

. 图 3.1.2 向我们展示了这个自编码过程。我们可以观察到,解码后的数字 7,虽然不是完全相同,但足够接近。
由于编码器和解码器都是非线性函数,我们可以使用神经网络来实现它们。例如,在 MNIST 数据集中,可以通过 MLP 或 CNN 来实现自编码器。自编码器可以通过最小化损失函数并通过反向传播进行训练。与其他神经网络类似,唯一的要求是损失函数必须是可微的。
如果我们将输入视为一个分布,我们可以将编码器解释为分布的编码器,

编码器和解码器作为分布的解码器,

. 自编码器的损失函数表示如下:

(公式 3.1.2)
损失函数简单地意味着我们希望在给定潜在向量分布的情况下,最大化恢复输入分布的机会。如果假设解码器输出分布是高斯分布,那么损失函数就简化为 MSE,因为:

(公式 3.1.3)
在这个例子中,

表示均值为的高斯分布

和方差为

. 假设常数方差。解码器输出

假设是独立的。m 是输出维度。
使用 Keras 构建自编码器
接下来我们将进行一个非常激动人心的内容,使用 Keras 库构建一个自编码器。为了简化起见,我们将使用 MNIST 数据集作为第一组示例。自编码器将从输入数据中生成一个潜在向量,并通过解码器恢复输入。第一个示例中的潜在向量是 16 维的。
首先,我们将通过构建编码器来实现自动编码器。清单 3.2.1 显示了编码器,它将 MNIST 数字压缩为一个 16 维的潜在向量。编码器是由两层 Conv2D 堆叠而成。最后一个阶段是一个有 16 个单元的 Dense 层,用于生成潜在向量。图 3.2.1 显示了 plot_model() 生成的架构模型图,它与 encoder.summary() 生成的文本版本相同。最后一个 Conv2D 层输出的形状被保存下来,用于计算解码器输入层的维度,以便轻松重建 MNIST 图像。
以下的代码清单 3.2.1 显示了 autoencoder-mnist-3.2.1.py。这是一个使用 Keras 实现的自动编码器。潜在向量是 16 维的:
from keras.layers import Dense, Input
from keras.layers import Conv2D, Flatten
from keras.layers import Reshape, Conv2DTranspose
from keras.models import Model
from keras.datasets import mnist
from keras.utils import plot_model
from 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 filters per CNN 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 funtion, 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:编码器模型由 Conv2D(32)-Conv2D(64)-Dense(16) 组成,用于生成低维潜在向量。
清单 3.2.1 中的解码器将潜在向量解压缩,以恢复 MNIST 数字。解码器的输入阶段是一个 Dense 层,用于接受潜在向量。单元的数量等于编码器中 Conv2D 输出维度的乘积。这样做是为了方便将 Dense 层的输出调整为 Conv2DTranspose 的输入,最终恢复原始的 MNIST 图像维度。
解码器由三层 Conv2DTranspose 堆叠而成。在我们的例子中,我们将使用 反向卷积神经网络(有时称为反卷积),这种结构在解码器中更为常见。我们可以把反向卷积神经网络(Conv2DTranspose)想象成卷积神经网络的反向过程。在一个简单的例子中,如果卷积神经网络将图像转换为特征图,反向卷积神经网络则会根据特征图生成图像。图 3.2.2 显示了解码器模型。

图 3.2.2:解码器模型由 Dense(16)-Conv2DTranspose(64)-Conv2DTranspose(32)-Conv2DTranspose(1) 组成。输入是潜在向量,被解码以恢复原始输入。
通过将编码器和解码器结合在一起,我们可以构建自动编码器。图 3.2.3 说明了自动编码器的模型图。编码器的张量输出也是解码器的输入,解码器生成自动编码器的输出。在这个示例中,我们将使用 MSE 损失函数和 Adam 优化器。在训练过程中,输入和输出相同,即 x_train。我们应该注意,在这个示例中,只有少数几层就足够使验证损失在一轮训练中降到 0.01。对于更复杂的数据集,可能需要更深的编码器、解码器以及更多的训练轮次。

图 3.2.3:自动编码器模型是通过将编码器模型和解码器模型结合在一起构建的。这个自动编码器有 178k 个参数。
在对自编码器训练一个周期后,验证损失为 0.01,我们能够验证它是否可以编码和解码之前未见过的 MNIST 数据。图 3.2.4展示了来自测试数据的八个样本及其对应的解码图像。除了图像中的轻微模糊外,我们可以轻松识别出自编码器能够以良好的质量恢复输入数据。随着训练轮数的增加,结果将会得到改善。

图 3.2.4:从测试数据中预测的自编码器输出。前两行是原始输入的测试数据,后两行是预测的数据。
此时,我们可能会想,如何可视化潜在向量空间呢?一种简单的可视化方法是强迫自编码器通过使用 2 维潜在向量来学习 MNIST 数字的特征。这样,我们就能够将这个潜在向量投影到 2D 空间中,从而看到 MNIST 编码的分布情况。通过在 autoencoder-mnist-3.2.1.py 代码中设置 latent_dim = 2,并使用 plot_results() 绘制 MNIST 数字与 2 维潜在向量的关系,图 3.2.5 和 图 3.2.6 展示了 MNIST 数字在潜在编码上的分布情况。这些图形是在训练 20 个周期后生成的。为了方便起见,程序已保存为 autoencoder-2dim-mnist-3.2.2.py,其部分代码在代码清单 3.2.2中显示。
以下是代码清单 3.2.2,autoencoder-2dim-mnist-3.2.2.py,它展示了用于可视化 MNIST 数字在 2 维潜在编码上的分布的函数。其余代码实际上与代码清单 3.2.1类似,这里不再展示。
def plot_results(models,
data,
batch_size=32,
model_name="autoencoder_2dim"):
"""Plots 2-dim latent values as color gradient
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
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))
plt.scatter(z[:, 0], z[:, 1], c=y_test)
plt.colorbar()
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(-4, 4, n)
grid_y = np.linspace(-4, 4, 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.2.5:MNIST 数字分布与潜在编码维度 z[0] 和 z[1] 的关系。原始彩色照片可在书籍的 GitHub 仓库中找到,https://github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras/blob/master/chapter3-autoencoders/README.md。

图 3.2.6:当导航 2 维潜在向量空间时生成的数字
在图 3.2.5中,我们能够看到特定数字的潜在编码在空间中的某个区域聚集。例如,数字 0 位于左下象限,而数字 1 位于右上象限。这种聚集在图 3.2.6中得到了呈现。事实上,同一图形展示了从潜在空间中导航或生成新数字的结果,正如在图 3.2.5中所示。
例如,从中心开始,并改变一个二维潜在向量的值朝向左下象限,显示出数字从 2 变为 0。这是可以预期的,因为从图 3.2.5,我们可以看到数字 2 的代码簇接近中心,而数字 0 的代码簇则位于左下象限。如图 3.2.6所示,我们只探索了每个潜在维度在-4.0 到+4.0 之间的区域。
如在图 3.2.5所示,潜在代码分布是不连续的,并且超出了

。理想情况下,它应该像一个圆形,那里每个地方都有有效的值。由于这种不连续性,存在一些区域,如果我们解码潜在向量,可能会生成几乎无法识别的数字。
去噪自编码器(DAE)
我们现在要构建一个具有实际应用的自编码器。首先,让我们画个图,假设 MNIST 数字图像被噪声污染,这样人类阅读起来会更困难。我们可以构建一个去噪自编码器(DAE)来去除这些图像中的噪声。图 3.3.1展示了三组 MNIST 数字。每组的顶部行(例如,MNIST 数字 7、2、1、9、0、6、3、4、9)是原始图像。中间行显示了 DAE 的输入,即被噪声污染的原始图像。底部行显示了 DAE 的输出:

图 3.3.1:原始 MNIST 数字(顶部行),被污染的原始图像(中间行)和去噪图像(底部行)

图 3.3.2:去噪自编码器的输入是被污染的图像,输出是干净的或去噪的图像。假设潜在向量为 16 维。
如图 3.3.2所示,去噪自编码器的结构实际上与我们在前一部分中介绍的 MNIST 自编码器相同。输入定义为:

(方程式 3.3.1)
在这个公式中,

表示被噪声污染的原始 MNIST 图像。
编码器的目标是发现如何生成潜在向量z,使得解码器能够恢复

通过最小化失真损失函数(如均方误差 MSE),如图所示:

(方程式 3.3.2)
在这个例子中,m 是输出维度(例如,在 MNIST 中,m = 宽度 × 高度 × 通道 = 28 × 28 × 1 = 784)。

和

是…的元素

和

,分别。
为了实现 DAE,我们需要对上一节中的自编码器做一些修改。首先,训练输入数据应该是损坏的 MNIST 数字。训练输出数据与原始清晰的 MNIST 数字相同。这就像是告诉自编码器正确的图像应该是什么,或者要求它在给定损坏图像的情况下找出如何去除噪声。最后,我们必须在损坏的 MNIST 测试数据上验证自编码器。
图 3.3.2左侧展示的 MNIST 数字 7 是一个实际的损坏图像输入。右侧的是经过训练的去噪自编码器输出的清晰图像。
清单 3.3.1展示了已贡献至 Keras GitHub 仓库的去噪自编码器。使用相同的 MNIST 数据集,我们能够通过添加随机噪声来模拟损坏的图像。添加的噪声是一个高斯分布,均值为,

和标准差

。由于添加随机噪声可能会将像素数据推向无效值(小于 0 或大于 1),因此像素值会被裁剪到[0.1, 1.0]范围内。
其他部分将与上一节中的自编码器几乎相同。我们将使用相同的 MSE 损失函数和 Adam 优化器。然而,训练的 epoch 数增加到了 10。这是为了允许足够的参数优化。
图 3.3.1展示了实际的验证数据,包含损坏和去噪后的 MNIST 测试数字。我们甚至能够看到,人类会发现很难读取损坏的 MNIST 数字。图 3.3.3展示了 DAE 在噪声水平提高时的某种鲁棒性。

到

和

。在

,DAE 仍然能够恢复原始图像。然而,在

,在第二组和第三组中,像数字 4 和 5 这样的几个数字已经无法正确恢复。

图 3.3.3:去噪自编码器在噪声水平增加时的表现
如清单 3.3.1 所示,denoising-autoencoder-mnist-3.3.1.py向我们展示了一个去噪自编码器:
from keras.layers import Dense, Input
from keras.layers import Conv2D, Flatten
from keras.layers import Reshape, Conv2DTranspose
from keras.models import Model
from keras import backend as K
from 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.4.1所示,如果我们得到一张前景为稻田、背景为火山、顶部为天空的灰度照片,我们能够为其添加适当的颜色。

图 3.4.1:为梅雁火山的灰度照片添加颜色。颜色化网络应该模仿人类的能力,通过给灰度照片添加颜色。左图是灰度图,右图是彩色图。原始的彩色照片可以在本书的 GitHub 仓库中找到,链接为 https://github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras/blob/master/chapter3-autoencoders/README.md。
一个简单的自动颜色化算法似乎是自编码器的合适问题。如果我们可以用足够多的灰度照片作为输入,并将对应的彩色照片作为输出进行训练,它可能会发现隐藏的结构并正确地应用颜色。大致来说,它是去噪的反向过程。问题是,自编码器能否为原始灰度图像添加颜色(良好的噪声)?
列表 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,展示了一个使用 CIFAR10 数据集的颜色化自编码器:
from keras.layers import Dense, Input
from keras.layers import Conv2D, Flatten
from keras.layers import Reshape, Conv2DTranspose
from keras.models import Model
from keras.callbacks import ReduceLROnPlateau, ModelCheckpoint
from keras.datasets import cifar10
from keras.utils import plot_model
from keras import backend as K
import numpy as np
import matplotlib.pyplot as plt
import os
# convert from color image (RGB) to grayscale
# source: opencv.org
# grayscale = 0.299*red + 0.587*green + 0.114*blue
def rgb2gray(rgb):
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 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 = clr_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 维,以便增加它可以表示的显著属性的数量,如自编码器部分所讨论的那样。最后,输出滤波器的大小增加到 3,即与期望的彩色输出中的 RGB 通道数相等。
颜色化自编码器现在使用灰度图像作为输入,原始 RGB 图像作为输出进行训练。训练将需要更多的 epoch,并使用学习率调整器,在验证损失没有改善时缩小学习率。这可以通过在 Keras 的fit()函数中告诉callbacks参数调用lr_reducer()函数来轻松实现。
图 3.4.2 演示了 CIFAR10 测试数据集中的灰度图像颜色化。图 3.4.3 比较了地面真实值与颜色化自编码器的预测。自编码器完成了一个可接受的颜色化任务。海洋或天空被预测为蓝色,动物具有不同的棕色阴影,云是白色的,等等。
有一些明显的错误预测,比如红色车辆变成了蓝色,或者蓝色车辆变成了红色,偶尔绿色的田野被误判为蓝天,暗色或金色的天空被转换成了蓝色天空。

图 3.4.2:使用自编码器进行自动灰度到彩色图像转换。CIFAR10 测试灰度输入图像(左)和预测的彩色图像(右)。原始彩色照片可以在书籍的 GitHub 仓库中找到,https://github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras/blob/master/chapter3-autoencoders/README.md。

图 3.4.3:实地对比真实的彩色图像和预测的上色图像。原始彩色照片可以在书籍的 GitHub 仓库中找到,https://github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras/blob/master/chapter3-autoencoders/README.md。
结论
在本章中,我们介绍了自编码器,它是一种神经网络,将输入数据压缩成低维度的编码,以便高效地执行结构性转换,如去噪和上色。我们为更高级的话题——生成对抗网络(GANs)和变分自编码器(VAEs)奠定了基础,这些将在后续章节中介绍,同时仍然探讨自编码器如何利用 Keras。我们展示了如何从两个基本模型(编码器和解码器)实现自编码器。我们还学会了如何提取输入分布的隐藏结构,这是 AI 中的常见任务之一。
一旦潜在编码被揭示出来,就可以对原始输入分布执行许多结构性操作。为了更好地理解输入分布,可以通过低级嵌入方式(类似于我们在本章中所做的)或通过更复杂的降维技术,如 t-SNE 或 PCA,来可视化潜在向量形式的隐藏结构。
除了去噪和上色外,自编码器还用于将输入分布转换为低维度潜在编码,这些编码可以进一步处理用于其他任务,如分割、检测、跟踪、重建、视觉理解等。在第八章"),变分自编码器(VAEs)中,我们将讨论与自编码器结构相同,但通过具有可解释的潜在编码来区分的 VAEs,这些编码能够生成连续的潜在编码投影。在下一章,我们将介绍 AI 领域最近最重要的突破之一——生成对抗网络(GANs),在其中我们将学习 GANs 的核心优势及其合成看似真实的数据或信号的能力。
参考文献
- Ian Goodfellow 等人。深度学习。第 1 卷。剑桥:MIT 出版社,2016 年 (
www.deeplearningbook.org/)。
第四章 生成对抗网络(GANs)
在本章中,我们将研究生成对抗网络(GANs)[1],这是我们将要研究的三种人工智能算法中的第一个。GAN 属于生成模型的范畴。然而,与自编码器不同,生成模型能够根据任意编码生成新的、有意义的输出。
本章将讨论 GAN 的工作原理。我们还将回顾 Keras 中几个早期 GAN 的实现。稍后,我们将展示实现稳定训练所需的技术。本章的范围涵盖了两种流行的 GAN 实现示例,深度卷积生成对抗网络(DCGAN)[2]和条件生成对抗网络(CGAN)[3]。
总结一下,本章的目标是:
-
介绍生成对抗网络(GAN)的原理
-
如何在 Keras 中实现 GAN,如 DCGAN 和 CGAN
GAN 概述
在深入研究 GAN 的更高级概念之前,我们先来回顾一下 GAN,并介绍它们的基本概念。GAN 非常强大;这一简单的说法通过它们能够生成虚拟名人面孔来证明这一点,这些面孔并非真实人物,而是通过进行潜空间插值生成的。
GAN 的高级特性[4]的一个绝佳示例可以通过这个 YouTube 视频(youtu.be/G06dEcZ-QTg)看到。视频展示了如何利用 GAN 生成逼真的面孔,这显示了它们的强大功能。这个话题比我们在本书中之前讨论的任何内容都要复杂。例如,上述视频是自编码器无法轻易实现的,正如我们在第三章中讨论的,自编码器。
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.1)
该方程就是标准的二元交叉熵损失函数。损失是正确识别真实数据的期望值的负和,
,以及正确识别合成数据的 1.0 减去期望值,
。对数不会改变局部最小值的位置。训练时,判别器会提供两小批次数据:
-
,来自采样数据的真实数据(即,
),标签为 1.0 -
,来自生成器的假数据,标签为 0.0
为了最小化损失函数,判别器的参数,
,将通过反向传播来更新,通过正确识别真实数据,
,以及合成数据,
。正确识别真实数据等同于
,而正确分类假数据则等同于
或者
。在这个方程中,
是生成器用来合成新信号的任意编码或噪声向量。两者共同作用于最小化损失函数。
为了训练生成器,GAN 将判别器和生成器的损失视为零和博弈。生成器的损失函数只是判别器损失函数的负值:

(公式 4.1.2)
这可以更恰当地重新写成一个价值函数:

(公式 4.1.3)
从生成器的角度来看,公式 4.1.3 应该被最小化。从判别器的角度来看,价值函数应该被最大化。因此,生成器的训练准则可以写成一个最小最大问题:

(公式 4.1.4)
有时,我们会通过伪装合成数据为真实数据(标签为 1.0)来试图欺骗判别器。通过相对于
的最大化,优化器向判别器参数发送梯度更新,使其将该合成数据视为真实数据。与此同时,通过相对于
的最小化,优化器将训练生成器的参数,教其如何欺骗判别器。然而,实际上,判别器在将合成数据分类为假数据时非常自信,因此不会更新其参数。此外,梯度更新很小,并且在传播到生成器层时已经大大减弱。因此,生成器未能收敛:

图 4.1.4:训练生成器就像使用二元交叉熵损失函数训练一个网络。生成器产生的假数据被当作真实数据展示。
解决方案是将生成器的损失函数重新构建为以下形式:

(方程式 4.1.5)
损失函数的作用是通过训练生成器来最大化判别器将合成数据认为是现实数据的可能性。新的公式不再是零和的,而是纯粹基于启发式的驱动。图 4.1.4 展示了训练中的生成器。在这个图中,生成器的参数仅在整个对抗网络训练时更新。这是因为梯度从判别器传递给生成器。然而,实际上,在对抗训练过程中,判别器的权重只是暂时被冻结。
在深度学习中,生成器和判别器都可以使用合适的神经网络架构来实现。如果数据或信号是图像,生成器和判别器网络将使用 CNN。对于像自然语言处理(NLP)中的一维序列,两个网络通常是递归网络(RNN、LSTM 或 GRU)。
Keras 中的 GAN 实现
在前一部分中,我们了解到 GAN 的原理是直观的。我们还学会了如何通过熟悉的网络层,如 CNN 和 RNN,来实现 GAN。使 GAN 与其他网络不同的是,它们通常很难训练。即便是简单的层变化,也可能导致网络训练的不稳定。
在这一部分中,我们将研究使用深度 CNN 实现的早期成功的 GAN 实现之一。它被称为 DCGAN [3]。
图 4.2.1 显示了用于生成假 MNIST 图像的 DCGAN。DCGAN 推荐以下设计原则:
-
使用步幅 > 1 的卷积,而不是
MaxPooling2D或UpSampling2D。通过步幅 > 1,卷积神经网络(CNN)学会了如何调整特征图的大小。 -
避免使用
Dense层。在所有层中使用 CNN。Dense层仅用作生成器的第一层,用于接受z向量。Dense层的输出经过调整大小后,成为后续 CNN 层的输入。 -
使用批量归一化(BN)来通过标准化每一层的输入,使其均值为零,方差为单位,从而稳定学习。在生成器的输出层和判别器的输入层中不使用 BN。在这里展示的实现示例中,判别器中不使用批量归一化。
-
修正线性单元(ReLU)在生成器的所有层中使用,除了输出层,输出层使用tanh激活函数。在这里展示的实现示例中,输出层使用sigmoid而不是tanh,因为它通常能使 MNIST 数字的训练更加稳定。
-
在鉴别器的所有层中使用 Leaky ReLU。与 ReLU 不同,当输入小于零时,Leaky ReLU 不会将所有输出置为零,而是生成一个小的梯度,等于 alpha × input。在以下示例中,alpha = 0.2。

图 4.2.1:一个 DCGAN 模型
生成器学习从 100 维输入向量生成假图像([-1.0, 1.0] 范围的 100 维均匀分布随机噪声)。鉴别器将真实图像与假图像区分开,但在对抗网络训练过程中,无意中指导生成器如何生成真实图像。我们在 DCGAN 实现中使用的卷积核大小为 5,目的是增加卷积的覆盖范围和表达能力。
生成器接受由均匀分布生成的 100 维 z 向量,范围为 -1.0 到 1.0。生成器的第一层是一个 7 × 7 × 128 = 6,272 个 单元 的 Dense 层。单元的数量是根据输出图像的最终尺寸(28 × 28 × 1,28 是 7 的倍数)以及第一层 Conv2DTranspose 的滤波器数量(等于 128)来计算的。我们可以将反卷积神经网络(Conv2DTranspose)看作是卷积神经网络(CNN)过程的反向过程。简单地说,如果 CNN 将图像转化为特征图,则反卷积 CNN 会根据特征图生成图像。因此,反卷积 CNN 在上一章的解码器和本章的生成器中都得到了应用。
在经过两次 Conv2DTranspose(strides = 2)处理后,特征图的大小将为 28 × 28 × 滤波器数量。每个 Conv2DTranspose 前都进行批归一化和 ReLU 激活。最终层使用 sigmoid 激活函数,生成 28 × 28 × 1 的假 MNIST 图像。每个像素被归一化到 [0.0, 1.0],对应于 [0, 255] 的灰度级别。以下列表显示了在 Keras 中实现生成器网络的代码。我们定义了一个函数来构建生成器模型。由于整个代码较长,我们将仅列出与讨论相关的部分。
注意
完整代码可在 GitHub 上获取:github.com/PacktPublishing/Advanced-Deep- Learning-with-Keras。
列表 4.2.1,dcgan-mnist-4.2.1.py 向我们展示了 DCGAN 的生成器网络构建函数:
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: Target size of one side (assuming square image)
# Returns
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 × 28 × 1 的 MNIST 图像,分类为真实(1.0)或虚假(0.0)。共有四个 CNN 层。除了最后一个卷积层外,每个Conv2D都使用strides = 2来对特征图进行下采样。每个Conv2D层后面都接一个 Leaky ReLU 层。最终的滤波器大小是 256,而初始滤波器大小是 32,并且每个卷积层的滤波器大小都会翻倍。最终的滤波器大小为 128 也能工作,但我们会发现,使用 256 时生成的图像效果更好。最终输出层会将特征图展平,并通过一个单元的Dense层在经过 sigmoid 激活层缩放后输出介于 0.0 到 1.0 之间的预测值。输出被建模为伯努利分布。因此,使用二元交叉熵损失函数。
在构建生成器和判别器模型后,通过连接生成器和判别器网络来构建对抗模型。判别器和对抗网络都使用 RMSprop 优化器。判别器的学习率为 2e-4,而对抗网络的学习率为 1e-4。应用 RMSprop 衰减率,判别器为 6e-8,对抗网络为 3e-8。将对抗网络的学习率设置为判别器的一半将使训练更加稳定。我们从图 4.1.3和4.1.4中回忆到,GAN 训练有两个部分:判别器训练和生成器训练,这是对抗训练,期间冻结判别器权重。
清单 4.2.2展示了在 Keras 中实现判别器的代码。定义了一个函数来构建判别器模型。在清单 4.2.3中,我们将展示如何构建 GAN 模型。首先构建判别器模型,然后是生成器模型的实例化。对抗模型只是将生成器和判别器组合在一起。许多 GAN 模型中,批处理大小 64 似乎是最常见的。网络参数显示在清单 4.2.3中。
如清单 4.2.1和4.2.2所示,DCGAN 模型是直观的。构建它的难点在于网络设计中的微小变化会轻易导致训练无法收敛。例如,如果在判别器中使用了批归一化,或者将生成器中的strides = 2转移到后面的 CNN 层,DCGAN 将无法收敛。
清单 4.2.2,dcgan-mnist-4.2.1.py展示了我们 DCGAN 判别器网络构建函数:
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
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,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.4 显示了专门用于训练判别器和对抗网络的函数。由于自定义训练,通常的 fit() 函数不会被使用。相反,调用 train_on_batch() 来对给定的数据批次进行单次梯度更新。接着,通过对抗网络来训练生成器。训练过程首先从数据集中随机选择一批真实图像,并将其标记为真实(1.0)。然后,生成器生成一批假图像,并将其标记为假(0.0)。这两批图像被连接在一起,用于训练判别器。
完成此步骤后,生成器将生成新的假图像,并标记为真实(1.0)。这一批图像将用于训练对抗网络。两个网络会交替训练约 40,000 步。在规律的间隔中,基于某个噪声向量生成的 MNIST 数字会保存在文件系统中。在最后的训练步骤中,网络已经收敛。生成器模型也会保存在文件中,以便我们可以轻松地重用已训练的模型来生成未来的 MNIST 数字。然而,只有生成器模型会被保存,因为它才是 GAN 在生成新 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 Discriminaor 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 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:
if (i + 1) == train_steps:
show = True
else:
show = False
# plot generator images on a periodic basis
plot_images(generator,
noise_input=noise_input,
show=show,
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.1 展示了生成器生成的假图像随着训练步骤的变化而演化的过程。在 5000 步时,生成器已经开始生成可识别的图像。这就像拥有一个能够画数字的代理一样。值得注意的是,一些数字会从一种可识别的形式(例如,最后一行第二列的 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)的 GAN 变种来解决[4]。
使用相同的 GAN,对生成器和判别器的输入都施加一个条件。这个条件是数字的 one-hot 向量形式。这与要生成的图像(生成器)或是否被分类为真实或假(判别器)相关联。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 所示。
根据 图 4.3.2,更合适的写法是将损失函数写作:

和

.

(方程式 4.3.1)

(方程式 4.3.2)
判别器的新损失函数旨在最小化预测来自数据集的真实图像与来自生成器的假图像的错误,给定它们的独热标签。图 4.3.2 显示了如何训练判别器。

图 4.3.2:训练 CGAN 判别器与训练 GAN 判别器类似。唯一的区别是生成的假图像和数据集的真实图像都使用它们相应的独热标签进行条件化。
生成器的新损失函数最小化判别器对根据指定的独热标签条件化的假图像的正确预测。生成器学会生成特定的 MNIST 数字,给定其独热向量,可以欺骗判别器。以下图像展示了如何训练生成器:

图 4.3.3:通过对抗网络训练 CGAN 生成器与训练 GAN 生成器类似。唯一的区别是生成的假图像是通过独热标签进行条件化的。
以下清单突出显示了判别器模型中所需的小修改。代码使用 Dense 层处理独热向量,并将其与图像输入连接。Model 实例被修改以适应图像和独热向量输入。
清单 4.3.1,cgan-mnist-4.3.1.py 向我们展示了 CGAN 判别器。高亮部分显示了 DCGAN 中做出的更改。
def build_discriminator(inputs, y_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)
y_labels (Layer): Input layer for one-hot vector to condition
the inputs
image_size: Target size of one side (assuming square image)
# Returns
Model: Discriminator Model
"""
kernel_size = 5
layer_filters = [32, 64, 128, 256]
x = inputs
y = Dense(image_size * image_size)(y_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 y_labels
discriminator = Model([inputs, y_labels],
x,
name='discriminator')
return discriminator
以下列表突出显示了为了在生成器构建函数中加入条件化 one-hot 标签而做的代码更改。Model实例已针对z-向量和 one-hot 向量输入进行了修改。
列表 4.3.2,cgan-mnist-4.3.1.py 显示了 CGAN 生成器。突出显示了 DCGAN 中所做的更改:
def build_generator(inputs, y_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)
y_labels (Layer): Input layer for one-hot vector to condition
the inputs
image_size: Target size of one side (assuming square image)
# Returns
Model: Generator Model
"""
image_resize = image_size // 4
# network parameters
kernel_size = 5
layer_filters = [128, 64, 32, 1]
x = concatenate([inputs, y_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 y_labels
generator = Model([inputs, y_labels], x, name='generator')
return generator
列表 4.3.3 突出显示了为适应判别器和生成器的条件化 one-hot 向量而对train()函数所做的更改。首先,CGAN 判别器使用一批真实数据和假数据进行训练,数据根据它们各自的 one-hot 标签进行条件化。然后,通过训练对抗网络,更新生成器的参数,给定条件化的假数据,假数据伪装成真实数据。与 DCGAN 类似,在对抗训练过程中,判别器的权重被冻结。
列表 4.3.3,cgan-mnist-4.3.1.py 显示了 CGAN 训练过程。突出显示了 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
# 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)]
# 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
y_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, y_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:
if (i + 1) == train_steps:
show = True
else:
show = False
# plot generator images on a periodic basis
plot_images(generator,
noise_input=noise_input,
noise_class=noise_class,
show=show,
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):
cgan-mnist-4.3.1.py --generator=cgan_mnist.h5 --digit=8
使用 CGAN 就像拥有一个代理,我们可以要求它绘制类似人类书写的数字。CGAN 相较于 DCGAN 的主要优势在于,我们可以指定代理绘制哪个数字。
结论
本章讨论了生成对抗网络(GANs)背后的基本原理,为我们接下来的更高级话题奠定了基础,这些话题包括改进型 GAN、解耦表示 GAN 和跨域 GAN。我们从了解 GAN 是由两个网络组成——生成器和判别器开始。判别器的作用是区分真实信号和假信号。生成器的目标是欺骗判别器。生成器通常与判别器结合形成一个对抗网络。通过训练对抗网络,生成器学会如何生成能够欺骗判别器的假信号。
我们还了解到,虽然 GANs 很容易构建,但训练起来极为困难。文中展示了两个 Keras 中的示例实现。DCGAN 展示了如何训练 GAN 生成假图像,这些假图像是 MNIST 数字。然而,DCGAN 生成器无法控制应生成哪个具体数字。CGAN 通过将生成器条件化为绘制特定数字来解决这个问题。条件是以 one-hot 标签的形式出现的。如果我们想构建一个能够生成特定类别数据的代理,CGAN 是非常有用的。
在下一章中,将介绍 DCGAN 和 CGAN 的改进。特别关注如何稳定 DCGAN 的训练以及如何提高 CGAN 的感知质量。这将通过引入新的损失函数和稍微不同的模型架构来实现。
参考文献
-
Ian Goodfellow。NIPS 2016 教程:生成对抗网络。arXiv 预印本 arXiv:1701.00160,2016 (
arxiv.org/pdf/1701.00160.pdf). -
Alec Radford、Luke Metz 和 Soumith Chintala。深度卷积生成对抗网络的无监督表示学习。arXiv 预印本 arXiv:1511.06434,2015 (
arxiv.org/pdf/1511.06434.pdf). -
Mehdi Mirza 和 Simon Osindero。条件生成对抗网络。arXiv 预印本 arXiv:1411.1784,2014 (
arxiv.org/pdf/1411.1784.pdf). -
Tero Karras 等人。渐进增长的生成对抗网络:提高质量、稳定性和变化性。ICLR,2018 (
arxiv.org/pdf/1710.10196.pdf).
第五章:改进的 GAN
自从 2014 年生成对抗网络(GAN)[1]被提出以来,它的普及速度迅速增加。GAN 已被证明是一种有效的生成模型,能够合成看起来真实的新数据。随后的许多深度学习研究论文都提出了应对原始 GAN 困难和局限性的措施。
如我们在前几章所讨论的那样,GAN 训练 notoriously 难以进行,并且容易发生模式崩塌。模式崩塌是指生成器即使在损失函数已优化的情况下,也会生成看起来相同的输出。在 MNIST 数字的情境中,若发生模式崩塌,生成器可能只会生成数字 4 和 9,因为它们看起来相似。瓦瑟斯坦 GAN(WGAN)[2]通过认为可以通过简单地替换基于 Wasserstein 1 或地球搬运距离(EMD)的 GAN 损失函数来避免稳定性训练和模式崩塌,解决了这些问题。
然而,稳定性问题并不是 GAN 唯一的难题。还有一个日益迫切的需求是提升生成图像的感知质量。最小二乘 GAN(LSGAN)[3]提出了同时解决这两个问题的方法。基本前提是 sigmoid 交叉熵损失在训练过程中会导致梯度消失,这会导致图像质量差。最小二乘损失不会引发梯度消失。与传统的 GAN 生成图像相比,采用最小二乘损失生成的图像在感知质量上有显著提高。
在上一章中,CGAN 介绍了一种对生成器输出进行条件控制的方法。例如,如果我们想要得到数字 8,我们会在输入生成器时加入条件标签。受 CGAN 启发,辅助分类器 GAN(ACGAN)[4]提出了一种改进的条件算法,从而使得输出的感知质量和多样性得到了更好的提升。
总结来说,本章的目标是介绍这些改进的 GAN 并展示:
-
WGAN 的理论公式
-
理解 LSGAN 的原理
-
理解 ACGAN 的原理
-
知道如何使用 Keras 实现改进的 GAN——WGAN、LSGAN 和 ACGAN
瓦瑟斯坦 GAN
如我们之前提到的,GAN 的训练是非常困难的。两个网络——判别器和生成器的对立目标很容易导致训练不稳定。判别器试图正确区分真假数据,而生成器则尽力欺骗判别器。如果判别器学习得比生成器快,生成器的参数就无法得到优化。另一方面,如果判别器学习得较慢,那么梯度可能在到达生成器之前就消失了。在最糟糕的情况下,如果判别器无法收敛,生成器将无法获得任何有用的反馈。
距离函数
训练 GAN 的稳定性可以通过检查其损失函数来理解。为了更好地理解 GAN 的损失函数,我们将回顾两个概率分布之间的常见距离或散度函数。我们关心的是 p[data](真实数据分布)与 p[g](生成器数据分布)之间的距离。GAN 的目标是使 p[g] → p[data]。表 5.1.1 显示了这些散度函数。
在大多数最大似然任务中,我们将使用 Kullback-Leibler (KL) 散度或 D[KL] 作为损失函数中的度量,来衡量我们神经网络模型预测与真实分布函数之间的差距。如 方程 5.1.1 所示,D[KL] 是不对称的,因为
。
Jensen-Shannon (JS) 或 D[JS] 是基于 D[KL] 的散度。然而,与 D[KL] 不同,D[JS] 是对称的,并且是有限的。在这一部分,我们将展示优化 GAN 损失函数等价于优化 D[JS]。
| 散度 | 表达式 |
|---|---|
| Kullback-Leibler (KL) 5.1.1 | ![]() ![]() |
| Jensen-Shannon (JS) 5.1.2 | ![]() |
| Earth-Mover Distance (EMD) 或 Wasserstein 15.1.3 | ,其中 是所有联合分布 y(x,y) 的集合,其边际分布为 p[data] 和 p[g]。 |
表 5.1.1:两个概率分布函数 p[data] 和 p[g] 之间的散度函数

图 5.1.1:EMD 是从 x 到目标分布 y 所需运输的质量的加权量
EMD(地球搬运距离)的直觉是,它衡量了概率分布 p[data] 在与概率分布 p[g] 匹配时,需要通过 d = ||x - y|| 运输的质量量!距离函数。
是所有可能的联合分布空间中的一个联合分布!距离函数。
也称为运输计划,用于反映将质量运输到以匹配两个概率分布的策略。给定这两个概率分布,有许多可能的运输计划。粗略来说,inf 表示具有最小成本的运输计划。
例如,图 5.1.1展示了两个简单的离散分布
和
。
在位置 x i(i = 1, 2, 3 和 4)上有质量 m i,同时
在位置 y i(i = 1 和 2)上有质量 m i。为了匹配分布
,箭头展示了将每个质量 x i 移动 d i 的最小运输方案。EMD 计算公式为:

(方程 5.1.4)
在图 5.1.1中,EMD 可以被解释为将土堆
移到填满孔洞
所需的最少工作量。虽然在这个例子中,inf也可以从图中推导出来,但在大多数情况下,特别是对于连续分布,穷举所有可能的运输方案是不可行的。我们将在本章稍后回到这个问题。与此同时,我们将展示 GAN 损失函数实际上是在最小化詹森-香农(JS)散度。
GAN 中的距离函数
我们现在将根据上一章中的损失函数,计算给定任何生成器的最优判别器。我们将回顾以下方程:
(方程 4.1.1)
除了从噪声分布进行采样,前述方程还可以表示为从生成器分布进行采样:
(方程 5.1.5)
为了找到最小值
:
(方程 5.1.6)
(方程 5.1.7)
积分内部的项呈 y → a log y + b log(1 - y) 形式,该式在
处达到已知的最大值,对于任何
,不包括 {0,0} 的
都成立。由于积分不会改变该表达式的最大值位置(或
的最小值),因此最优判别器为:
(方程 5.1.8)
因此,损失函数给出了最优判别器:
(方程 5.1.9)
(方程 5.1.10)
(方程 5.1.11)
(方程 5.1.12)
我们可以从方程 5.1.12中观察到,最优判别器的损失函数是一个常数减去真实分布p[data]与任何生成器分布p[g]之间的两倍 Jensen-Shannon 散度。最小化
意味着最大化
,或者判别器必须正确区分假数据和真实数据。
与此同时,我们可以合理地认为,最优生成器是当生成器分布等于真实数据分布时:
(方程 5.1.13)
这是有道理的,因为生成器的目标是通过学习真实数据分布来欺骗判别器。实际上,我们可以通过最小化D[JS],,或者通过使p[g] → p[data]来得到最优生成器。给定最优生成器,最优判别器是
,并且
。

图 5.1.2:没有重叠的两个分布示例。
适用于p[g]
问题是,当两个分布没有重叠时,没有平滑的函数可以帮助缩小它们之间的差距。通过梯度下降训练 GAN 将无法收敛。例如,假设:
p[data] = (x, y) 其中
(方程 5.1.14)
p[g] = (x, y) 其中
(方程 5.1.15)
如图 5.1.2所示,U(0,1)是均匀分布。每个距离函数的散度如下:
由于D[JS]是一个常数,GAN 将没有足够的梯度来驱动p[g] → p[data]。我们还会发现D[KL]或反向D[KL]也无济于事。然而,通过W(p[data],p[g])我们可以得到一个平滑的函数,以便通过梯度下降使p[g] → p[data]。EMD 或 Wasserstein 1 似乎是优化 GAN 的更合适的损失函数,因为D[JS]在两个分布几乎没有重叠的情况下无法发挥作用。
为了进一步理解,关于距离函数的精彩讨论可以在lilianweng.github.io/lil-log/2017/08/20/from-GAN-to-W GAN.html找到。
Wasserstein 损失的使用
在使用 EMD 或 Wasserstein 1 之前,还需要克服一个问题。穷举
空间以找到
是不可行的。提出的解决方案是使用其 Kantorovich-Rubinstein 对偶:

(方程 5.1.16)
等价地,EMD,
,是所有K-Lipschitz 函数的上确界(大致为最大值):
。K-Lipschitz 函数满足以下约束:
(方程 5.1.17)
对于所有
,K-Lipschitz 函数具有有界导数,且几乎总是连续可微(例如,f(x) = |x|具有有界导数且连续,但在x = 0 处不可微)。
方程 5.1.16可以通过找到一系列K-Lipschitz 函数来求解
:
(方程 5.1.18)
在 GAN 的上下文中,方程 5.1.18 可以通过从z-噪声分布采样并将f[w]替换为判别器函数D[w]来重写:
(方程 5.1.19)
我们使用粗体字母来突出显示多维样本的通用性。我们面临的最终问题是如何找到函数系列
。我们将要讨论的提议解决方案是,在每次梯度更新时,判别器的权重w在下界和上界之间裁剪(例如,-0.0, 1 和 0.01):
(方程 5.1.20)
w的较小值限制了判别器在紧凑的参数空间中,从而确保了 Lipschitz 连续性。
我们可以使用方程 5.1.19作为我们新 GAN 损失函数的基础。EMD 或 Wasserstein 1 是生成器试图最小化的损失函数,而判别器试图最大化(或最小化-W(p[数据], p[生成])):

(方程 5.1.21)

(方程 5.1.22)
在生成器损失函数中,第一个项会消失,因为它并没有直接与真实数据进行优化。
以下表格展示了 GAN 和 WGAN 的损失函数之间的差异。为简洁起见,我们简化了
和
的符号。这些损失函数用于训练 WGAN,如算法 5.1.1所示。图 5.1.3展示了 WGAN 模型实际上与 DCGAN 模型相同,唯一的区别在于假数据/真实数据标签和损失函数:
| 网络 | 损失函数 | 方程 |
|---|---|---|
| GAN | ![]() ![]() |
4.1.14.1.5 |
| WGAN | ![]() ![]() ![]() |
5.1.215.1.225.1.20 |
表 5.1.1:GAN 与 WGAN 的损失函数比较
算法 5.1.1 WGAN
参数的值是
,c = 0.01,m = 64,n[critic] = 5。
需要:
,学习率。c,裁剪参数。m,批量大小。n[critic],每个生成器迭代中的判别器(鉴别器)迭代次数。
需要:w[0],初始判别器(鉴别器)参数。
,初始生成器参数
-
while![Wasserstein 损失的使用]()
尚未收敛
do -
fort = 1, …, n[critic]do -
从均匀噪声分布中采样一个批次
![Wasserstein 损失的使用]()
来自真实数据
-
从均匀噪声分布中采样一个批次
![Wasserstein 损失的使用]()
来自均匀噪声分布
-
![Wasserstein 损失的使用]()
,计算鉴别器梯度
-
![Wasserstein 损失的使用]()
,更新鉴别器参数
-
![Wasserstein 损失的使用]()
,裁剪鉴别器权重
-
end for -
从均匀噪声分布中采样一个批次
![Wasserstein 损失的使用]()
来自均匀噪声分布
-
![Wasserstein 损失的使用]()
,计算生成器梯度
-
![Wasserstein 损失的使用]()
,更新生成器参数
-
end while

图 5.1.3:上图:训练 WGAN 鉴别器需要来自生成器的假数据和来自真实分布的真实数据。下图:训练 WGAN 生成器需要来自生成器的假数据,这些数据假装是来自真实分布。
类似于 GAN,WGAN 交替训练鉴别器和生成器(通过对抗)。然而,在 WGAN 中,鉴别器(也称为评论员)训练n[critic]次迭代(第 2 到第 8 行),然后再训练生成器一次迭代(第 9 到第 11 行)。与 GAN 相比,WGAN 在训练过程中对鉴别器和生成器的训练次数不同。训练鉴别器意味着学习鉴别器的参数(权重和偏置)。这需要从真实数据中采样一个批次(第 3 行)和从假数据中采样一个批次(第 4 行),然后在将采样的数据传入鉴别器网络后计算鉴别器参数的梯度(第 5 行)。鉴别器参数使用 RMSProp 进行优化(第 6 行)。第 5 行和第 6 行是对方程 5.1.21的优化。研究表明,在 WGAN 中,Adam 优化器表现不稳定。
最后,EM 距离优化中的 Lipschitz 约束通过裁剪判别器参数(第 7 行)来施加。第 7 行实现了方程 5.1.20。经过n[批评者]迭代的判别器训练后,判别器参数被冻结。生成器训练从采样一批假数据开始(第 9 行)。采样的数据被标记为真实(1.0),试图欺骗判别器网络。生成器的梯度在第 10 行计算,并在第 11 行使用 RMSProp 进行优化。第 10 行和第 11 行执行梯度更新,以优化方程 5.1.22。
在训练完生成器后,判别器参数会被解冻,开始另一轮n[批评者]判别器训练迭代。需要注意的是,在判别器训练期间无需冻结生成器参数,因为生成器仅参与数据的生成。与 GAN 类似,判别器可以作为一个独立的网络进行训练。然而,训练生成器始终需要判别器的参与,因为损失是从生成器网络的输出计算的。
与 GAN 不同,在 WGAN 中,真实数据标记为 1.0,而假数据标记为-1.0,这是为了在第 5 行计算梯度时作为一种解决方法。第 5-6 行和第 10-11 行执行梯度更新,分别优化方程 5.1.21和5.1.22。第 5 行和第 10 行中的每一项都被建模为:

(方程 5.1.23)
其中,y[标签] = 1.0 表示真实数据,y[标签] = -1.0 表示假数据。为了简化符号,我们移除了上标(i)。对于判别器,WGAN 增加了
,以在使用真实数据进行训练时最小化损失函数。当使用假数据进行训练时,WGAN 减少了
,以最小化损失函数。对于生成器,当假数据在训练过程中被标记为真实时,WGAN 增加了
,以最小化损失函数。请注意,y[标签]在损失函数中的直接贡献仅限于它的符号。在 Keras 中,方程 5.1.23实现为:
def wasserstein_loss(y_label, y_pred):
return -K.mean(y_label * y_pred)
使用 Keras 实现 WGAN
要在 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)
Keras 中的整体网络模型类似于图 4.2.1中显示的 DCGAN。
列表 5.1.1 强调了使用 RMSprop 优化器和 Wasserstein 损失函数。算法 5.1.1 中的超参数在训练中使用。列表 5.1.2 是紧密跟随该 算法 的训练函数。然而,在训练判别器时有一个小的调整。我们不再在一个包含真实和虚假数据的单一批次中训练权重,而是先使用一批真实数据进行训练,然后再使用一批虚假数据进行训练。这种调整将防止由于真实和虚假数据标签的符号相反以及由于裁剪导致的权重幅度较小而导致梯度消失。
注意
完整代码可在 GitHub 上找到:
github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras
图 5.1.4 展示了 WGAN 在 MNIST 数据集上的输出演变。
列表 5.1.1,wgan-mnist-5.1.2.py。WGAN 模型实例化和训练。判别器和生成器都使用 Wasserstein 1 损失,wasserstein_loss():
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 = "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,wgan-mnist-5.1.2.py。WGAN 的训练过程严格遵循 算法 5.1.1。判别器每训练一次生成器,需要进行 n [批判] 次迭代:
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 labeled 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:
if (i + 1) == train_steps:
show = True
else:
show = False
# plot generator images on a periodic basis
gan.plot_images(generator,
noise_input=noise_input,
show=show,
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:WGAN 的样本输出与训练步骤的对比。WGAN 在训练和测试过程中没有遭遇模式崩溃。
即使在网络配置发生变化时,WGAN 也依然稳定。例如,众所周知,当批量归一化插入到判别器网络中的 ReLU 激活之前时,DCGAN 会变得不稳定。而相同的配置在 WGAN 中是稳定的。
下图展示了在判别器网络上应用批量归一化时,DCGAN 和 WGAN 的输出:

图 5.1.5:在判别器网络中,将批量归一化插入到 ReLU 激活之前时,DCGAN(左)和 WGAN(右)输出的对比
类似于上一章中的 GAN 训练,训练好的模型会在 40,000 次训练步骤后保存到文件中。我鼓励你运行训练好的生成器模型,查看生成的新 MNIST 数字图像:
python3 wgan-mnist-5.1.2.py --generator=wgan_mnist.h5
最小二乘 GAN(LSGAN)
正如上一节所讨论的,原始 GAN 很难训练。当 GAN 优化其损失函数时,实际上是在优化 Jensen-Shannon 散度,D[JS]。当两个分布函数之间几乎没有重叠时,优化 D[JS] 是非常困难的。
WGAN 提出了使用 EMD 或 Wasserstein 1 损失函数来解决这个问题,即使在两个分布几乎没有重叠的情况下,它也具有平滑的可微函数。然而,WGAN 并不关注生成图像的质量。除了稳定性问题外,原始 GAN 生成的图像在感知质量方面仍有提升空间。LSGAN 理论认为,这两个问题可以同时得到解决。
LSGAN 提出了最小二乘损失。图 5.2.1 说明了为何在 GAN 中使用 sigmoid 交叉熵损失会导致生成的数据质量较差。理想情况下,假样本的分布应尽可能接近真实样本的分布。然而,对于 GAN,一旦假样本已经处于正确的决策边界一侧,梯度就会消失。
这防止了生成器有足够的动力来提升生成假数据的质量。远离决策边界的假样本将不再尝试接近真实样本的分布。使用最小二乘损失函数时,只要假样本分布远离真实样本分布,梯度就不会消失。即使假样本已经处于正确的决策边界一侧,生成器也会努力提高其对真实密度分布的估计:

图 5.2.1:真实样本和假样本的分布被各自的决策边界划分:Sigmoid 和最小二乘
| 网络 | 损失函数 | 方程 |
|---|---|---|
| GAN | ![]() ![]() |
4.1.14.1.5 |
| LSGAN | ![]() ![]() |
5.2.15.2.2 |
表 5.2.1:GAN 和 LSGAN 的损失函数比较
上表展示了 GAN 和 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 中相同。LSGAN 在 Keras 中的网络模型与图 4.2.1 类似,唯一不同的是没有线性或输出激活。训练过程与 DCGAN 中看到的相似,并由实用函数提供:
gan.train(models, x_train, params)
清单 5.2.1,lsgan-mnist-5.2.1.py 显示了判别器和生成器在 DCGAN 中是相同的,除了判别器输出激活和使用了 MSE 损失函数:
def build_and_train_models():
# 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 converges
# easily 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)
接下来的图展示了使用 MNIST 数据集进行 40,000 步训练后,LSGAN 生成的样本。与 DCGAN 中的图 4.2.1相比,输出图像具有更好的感知质量:

图 5.2.2:LSGAN 与训练步数的样本输出
我鼓励你运行训练好的生成器模型,查看新合成的 MNIST 数字图像:
python3 lsgan-mnist-5.2.1.py --generator=lsgan_mnist.h5
辅助分类器 GAN (ACGAN)
ACGAN 在原理上与我们在上一章讨论的 条件 GAN (CGAN) 相似。我们将比较 CGAN 和 ACGAN。对于 CGAN 和 ACGAN,生成器的输入是噪声和标签。输出是属于输入类标签的假图像。对于 CGAN,判别器的输入是图像(假图像或真实图像)及其标签。输出是图像为真实的概率。对于 ACGAN,判别器的输入是图像,而输出是图像为真实的概率及其类别标签。接下来的图突出显示了 CGAN 和 ACGAN 在生成器训练中的区别:

图 5.3.1:CGAN 与 ACGAN 生成器训练。主要区别在于判别器的输入和输出。
本质上,在 CGAN 中,我们为网络提供边信息(标签)。在 ACGAN 中,我们尝试使用辅助类解码器网络来重建边信息。ACGAN 认为,强迫网络执行额外任务已被证明能提高原始任务的性能。在这种情况下,额外的任务是图像分类。原始任务是生成假图像。
| 网络 | 损失函数 | 数量 |
|---|---|---|
| CGAN | ![]() ![]() |
4.3.14.3.2 |
| ACGAN | 

表 5.3.1:CGAN 与 ACGAN 损失函数的比较
| 5.3.15.3.2 |
|---|
上表展示了与 CGAN 相比的 ACGAN 损失函数。ACGAN 的损失函数与 CGAN 相同,唯一不同的是额外的分类器损失函数。除了原本识别真假图像的任务 (
),判别器的Equation 5.3.1还有一个额外任务,即正确分类真假图像 (
)。生成器的Equation 5.3.2意味着除了通过假图像来欺骗判别器 (
),它还要求判别器正确分类这些假图像 (
)。
从 CGAN 代码开始,只需要修改判别器和训练函数以实现 ACGAN。判别器和生成器构建函数也由gan.py提供。为了查看在判别器上做出的修改,以下listing展示了构建函数,其中突出了执行图像分类的辅助解码器网络和双输出。
Listing 5.3.1,gan.py展示了判别器模型构建与 DCGAN 相同,用于预测图像是否为真实,作为第一个输出。添加了一个辅助解码器网络来执行图像分类并产生第二个输出:
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:
# 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)
生成器与 ACGAN 中的生成器相同。回顾一下,生成器构建函数在以下listing中展示。我们需要注意的是,Listings 5.3.1和5.3.2是 WGAN 和 LSGAN 在前面章节中使用的相同构建函数。
Listing 5.3.2,gan.py展示了生成器模型构建与 CGAN 中的相同:
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)
以下图展示了 Keras 中 ACGAN 的网络模型:

图 5.3.2:ACGAN 的 Keras 模型
如Listing 5.3.3所示,判别器和对抗模型已被修改以适应判别器网络中的变化。现在我们有了两个损失函数。第一个是原始的二元交叉熵,用于训练判别器估计输入图像是否真实。第二个是图像分类器,预测类别标签。输出是一个 10 维的独热向量。
参考Listing 5.3.3,acgan-mnist-5.3.1.py,其中突出了为适应判别器网络的图像分类器,在判别器和对抗模型中实施的变化。两个损失函数分别对应判别器的两个输出:
def build_and_train_models():
# 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 converges easily 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)
在Listing 5.3.4中,我们突出了训练过程中实现的变化。与 CGAN 代码相比,主要的区别在于输出标签必须在判别器和对抗训练期间提供。
如Listing 5.3.4中所示,acgan-mnist-5.3.1.py,train 函数中实现的更改已突出显示:
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, 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, 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:
if (i + 1) == train_steps:
show = True
else:
show = False
# plot generator images on a periodic basis
gan.plot_images(generator,
noise_input=noise_input,
noise_label=noise_label,
show=show,
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")
结果发现,增加了这个额外任务后,ACGAN 相比我们之前讨论过的所有 GAN,性能有了显著提升。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.4 展示了由 CGAN 和 ACGAN 分别生成的每个 MNIST 数字的并排比较。数字 2 到 6 在 ACGAN 中的质量优于 CGAN。
我鼓励你运行训练好的生成器模型,以查看新的合成 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.3:ACGAN 在训练步骤下生成的样本输出,标签为[0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5]

图 5.3.4:由 CGAN 和 ACGAN 生成的 0 至 9 数字输出并排比较
结论
在本章中,我们展示了对 GAN 原始算法的多种改进,这些算法在上一章中首次介绍。WGAN 提出了一种算法,通过使用 EMD 或 Wasserstein 1 损失来提高训练的稳定性。LSGAN 认为 GAN 原始的交叉熵函数容易导致梯度消失,而最小二乘损失则不同。LSGAN 提出了一种算法,实现了稳定的训练和高质量的输出。ACGAN 通过要求判别器除了判断输入图像是否为假图像或真实图像外,还需要执行分类任务,从而显著提高了 MNIST 数字条件生成的质量。
在下一章,我们将学习如何控制生成器输出的属性。尽管 CGAN 和 ACGAN 能够指示所需的数字进行生成,但我们尚未分析能够指定输出属性的 GAN。例如,我们可能希望控制 MNIST 数字的书写风格,如圆度、倾斜角度和粗细。因此,本章的目标是介绍具有解耦表示的 GAN,以控制生成器输出的特定属性。
参考文献
-
Ian Goodfellow 等人,生成对抗网络。神经信息处理系统进展,2014(
papers.nips.cc/paper/5423-generative-adversarial-nets.pdf)。 -
Martin Arjovsky、Soumith Chintala 和 Léon Bottou,Wasserstein GAN。arXiv 预印本,2017(
arxiv.org/pdf/1701.07875.pdf)。 -
Xudong Mao 等人。最小二乘生成对抗网络。2017 年 IEEE 计算机视觉国际会议(ICCV)。IEEE 2017 (
openaccess.thecvf.com/content_ICCV_2017/papers/Mao_Least_Squares_Generative_ICCV_2017_paper.pdf)。 -
Augustus Odena、Christopher Olah 和 Jonathon Shlens。带有辅助分类器 GAN 的条件图像合成。ICML,2017 (
proceedings.mlr.press/v70/odena17a/odena17a.pdf)。
第六章 解耦表示的 GANs
正如我们所探讨的,GAN 通过学习数据分布可以生成有意义的输出。然而,对于生成的输出属性并没有控制。一些 GAN 的变体,如条件 GAN(CGAN)和辅助分类器 GAN(ACGAN),如前一章所讨论的,能够训练一个受条件限制的生成器来合成特定的输出。例如,CGAN 和 ACGAN 都能引导生成器生成特定的 MNIST 数字。这是通过使用一个 100 维的噪声代码和相应的独热标签作为输入来实现的。然而,除了独热标签之外,我们没有其他方法来控制生成输出的属性。
注意
关于 CGAN 和 ACGAN 的回顾,请参见第四章,生成对抗网络(GANs),以及第五章,改进的 GANs。
在本章中,我们将介绍一些能够修改生成器输出的 GAN 变体。在 MNIST 数据集的背景下,除了生成哪个数字,我们可能还希望控制书写风格。这可能涉及到所需数字的倾斜度或宽度。换句话说,GAN 也可以学习解耦的潜在代码或表示,我们可以使用这些代码或表示来改变生成器输出的属性。解耦的代码或表示是一个张量,它可以改变输出数据的特定特征或属性,而不会影响其他属性。
本章的第一部分,我们将讨论InfoGAN:通过信息最大化生成对抗网络进行可解释表示学习 [1],这是一种 GAN 的扩展。InfoGAN 通过最大化输入代码和输出观察之间的互信息,以无监督的方式学习解耦的表示。在 MNIST 数据集上,InfoGAN 将书写风格与数字数据集解耦。
在本章的后续部分,我们还将讨论堆叠生成对抗网络(StackedGAN)[2],这是 GAN 的另一种扩展。StackedGAN 使用预训练的编码器或分类器来帮助解耦潜在代码。StackedGAN 可以视为一堆模型,每个模型由一个编码器和一个 GAN 组成。每个 GAN 通过使用相应编码器的输入和输出数据,以对抗的方式进行训练。
总结来说,本章的目标是展示:
-
解耦表示的概念
-
InfoGAN 和 StackedGAN 的原理
-
使用 Keras 实现 InfoGAN 和 StackedGAN
解耦表示
原始的 GAN 能够生成有意义的输出,但其缺点是无法进行控制。例如,如果我们训练一个 GAN 来学习名人面孔的分布,生成器会生成新的名人样貌的人物图像。然而,无法控制生成器生成我们想要的面孔的特定特征。例如,我们无法要求生成器生成一张女性名人面孔,长黑发,皮肤白皙,棕色眼睛,正在微笑。根本原因是我们使用的 100 维噪声代码将生成器输出的所有显著特征都缠结在一起。我们可以回忆起在 Keras 中,100 维代码是通过从均匀噪声分布中随机抽样生成的:
# generate 64 fake images from 64 x 100-dim uniform noise
noise = np.random.uniform(-1.0, 1.0, size=[64, 100])
fake_images = generator.predict(noise)
如果我们能够修改原始 GAN,使其能够将代码或表示分离为缠结和解耦的可解释潜在代码,我们将能够告诉生成器生成我们所需的内容。
接下来的图像展示了一个具有缠结代码的 GAN 及其结合缠结和解耦表示的变化。在假设的名人面孔生成背景下,使用解耦代码,我们可以指定我们希望生成的面孔的性别、发型、面部表情、肤色和眼睛颜色。n–dim 的缠结代码仍然用于表示我们尚未解耦的所有其他面部特征,例如面部形状、面部毛发、眼镜,仅举三个例子。缠结和解耦代码的拼接作为生成器的新输入。拼接代码的总维度不一定是 100:

图 6.1.1:具有缠结代码的 GAN 以及其结合缠结和解耦代码的变化。此示例展示了名人面孔生成的背景。
从前面的图像来看,具有解耦表示的 GAN 也可以像普通的 GAN 一样进行优化。这是因为生成器的输出可以表示为:

(方程式 6.1.1)
代码

由两个元素组成:
-
类似于 GAN 的不可压缩缠结噪声代码 z 或噪声向量。
-
潜在代码,c**1,c**2,…,c**L,表示数据分布的可解释解耦代码。所有潜在代码统一表示为 c。
为了简化起见,假设所有潜在代码都是独立的:

(方程式 6.1.2)
生成器函数

提供了不可压缩的噪声代码和潜在代码。从生成器的角度来看,优化

之间的互信息与优化 z 相同。生成器网络在得出解决方案时会忽略由解耦代码施加的约束。生成器学习分布

。这将实际破坏解耦表示的目标。
InfoGAN
为了加强代码的解耦,InfoGAN 向原始损失函数中提出了一个正则化项,该项最大化潜在代码 c 和

:

(方程 6.1.3)
该正则化项迫使生成器在构建合成假图像的函数时考虑潜在代码。在信息论领域,潜在代码 c 和

定义为:

(方程 6.1.4)
其中 H(c) 是潜在代码 c 的熵,和

是观察到生成器输出后的 c 的条件熵,

。熵是衡量随机变量或事件不确定性的一个度量。例如,像 太阳从东方升起 这样的信息熵较低。而 中彩票中大奖 的熵则较高。
在 方程 6.1.4 中,最大化互信息意味着最小化

或者通过观察生成的输出减少潜在代码的不确定性。这是有道理的,例如,在 MNIST 数据集中,如果 GAN 看到它观察到数字 8,生成器会对合成数字 8 更有信心。
然而,估计它是很难的

因为它需要了解后验知识

,而这是我们无法访问的。解决方法是通过估计辅助分布 Q(c|x) 来估算互信息的下界。InfoGAN 通过以下方式估算互信息的下界:

(方程 6.1.5)
在 InfoGAN 中,H(c)被假定为常数。因此,最大化互信息就是最大化期望值。生成器必须确信它已经生成了具有特定属性的输出。我们应该注意到,这个期望值的最大值是零。因此,互信息下界的最大值是 H(c)。在 InfoGAN 中,离散潜在代码的 Q(c|x) 可以通过 softmax 非线性表示。期望值是 Keras 中负的 categorical_crossentropy 损失。
对于单维连续代码,期望是对 c 和 x 的双重积分。这是因为期望从解缠代码分布和生成器分布中采样。估计期望的一种方法是假设样本是连续数据的良好度量。因此,损失被估计为 c log Q(c|x)。
为了完成 InfoGAN 网络,我们应该有一个 Q(c|x) 的实现。为了简单起见,网络 Q 是附加在判别器倒数第二层的辅助网络。因此,这对原始 GAN 的训练影响最小。下图展示了 InfoGAN 的网络图:

图 6.1.2:展示 InfoGAN 中判别器和生成器训练的网络图
下表展示了 InfoGAN 相对于原始 GAN 的损失函数。InfoGAN 的损失函数相比原始 GAN 多了一个额外的项

其中

是一个小的正常数。最小化 InfoGAN 的损失函数意味着最小化原始 GAN 的损失并最大化互信息

.
| 网络 | 损失函数 | 数字 |
|---|---|---|
| GAN | ![]() ![]() |
4.1.14.1.5 |
| InfoGAN | 
对于连续代码,InfoGAN 推荐一个值为
。在我们的示例中,我们设置为
。对于离散代码,InfoGAN 推荐
表 6.1.1:GAN 和 InfoGAN 损失函数的比较
. | 6.1.16.1.2 |
如果应用于 MNIST 数据集,InfoGAN 可以学习解缠的离散和连续代码,从而修改生成器的输出属性。例如,像 CGAN 和 ACGAN 一样,离散代码以 10 维独热标签的形式用于指定要生成的数字。然而,我们可以添加两个连续代码,一个用于控制书写风格的角度,另一个用于调整笔画宽度。下图展示了 InfoGAN 中 MNIST 数字的代码。我们保留较小维度的纠缠代码来表示所有其他属性:

图 6.1.3:在 MNIST 数据集背景下 GAN 和 InfoGAN 的代码
InfoGAN 在 Keras 中的实现
为了在 MNIST 数据集上实现 InfoGAN,需要对 ACGAN 的基础代码进行一些修改。如以下列表所示,生成器将纠缠的(z 噪声代码)和解缠的代码(独热标签和连续代码)拼接起来作为输入。生成器和判别器的构建函数也在 lib 文件夹中的 gan.py 中实现。
注意
完整的代码可以在 GitHub 上找到:
github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras
列表 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')
上面的列表展示了带有原始默认 GAN 输出的判别器和Q网络。突出了与离散代码(用于一热标签)softmax预测和给定输入 MNIST 数字图像的连续代码概率相对应的三个辅助输出。
列表 6.1.2,infogan-mnist-6.1.1.py。InfoGAN 判别器和Q网络:
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展示了 Keras 中的 InfoGAN 模型。构建判别器和对抗模型还需要一些更改。更改主要体现在所使用的损失函数上。原始的判别器损失函数是binary_crossentropy,用于离散代码的categorical_crossentropy,以及针对每个连续代码的mi_loss函数,构成了整体损失函数。每个损失函数的权重为 1.0,除了mi_loss函数,其权重为 0.5,适用于
连续代码。
列表 6.1.3突出了所做的更改。然而,我们应该注意到,通过使用构建器函数,判别器的实例化方式如下:
# call discriminator builder with 4 outputs: source, label,
# and 2 codes
discriminator = gan.discriminator(inputs, num_labels=num_labels, with_codes=True)
生成器是通过以下方式创建的:
# 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.4:InfoGAN Keras 模型
列表 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 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 的正态分布。对于假数据,我们将使用随机采样的标签,而对于真实数据,我们将使用数据集类标签来表示离散潜在代码。以下列表突出了在训练函数中所做的更改。与之前的所有 GAN 类似,判别器和生成器(通过对抗)交替训练。在对抗训练期间,判别器的权重被冻结。每 500 步间隔使用gan.py plot_images()函数保存生成器输出的样本图像。
列表 6.1.4,infogan-mnist-6.1.1.py展示了 InfoGAN 的训练函数如何类似于 ACGAN。唯一的区别是我们提供从正态分布中采样的连续代码:
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])
# 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:
if (i + 1) == train_steps:
show = True
else:
show = False
# 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=show,
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 的生成器输出
类似于我们之前介绍的所有 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 -
检查第一个连续编码的效果,了解哪个属性受到了影响。我们将第一个连续编码从 -2.0 变化到 2.0,数字从 0 到 9. 第二个连续编码设置为 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.7 显示第二个连续编码控制书写风格的旋转角度(倾斜):
python3 infogan-mnist-6.1.1.py --generator=infogan_mnist.h5 --digit=0 --code1=0 --code2=0 --p2

图 6.1.5:InfoGAN 生成的图像,当离散编码从 0 变化到 9 时。两个连续编码都设置为零。

图 6.1.6:InfoGAN 生成的图像,当第一个连续编码从 -2.0 变化到 2.0 时,数字从 0 到 9. 第二个连续编码设置为零。第一个连续编码控制数字的粗细。

图 6.1.7:InfoGAN 生成的图像,当第二个连续编码从 -2.0 变化到 2.0 时,数字从 0 到 9. 第一个连续编码设置为零。第二个连续编码控制书写风格的旋转角度(倾斜)。
从这些验证结果中,我们可以看到,除了能够生成类似 MNIST 的数字外,InfoGAN 扩展了条件 GAN(如 CGAN 和 ACGAN)的能力。网络自动学习了两个任意编码,可以控制生成器输出的特定属性。如果我们将连续编码的数量增加到超过 2 个,看看还能控制哪些其他属性,将会很有趣。
StackedGAN
与 InfoGAN 同样的理念,StackedGAN 提出了通过分解潜在表示来调整生成器输出条件的方法。然而,StackedGAN 采用了不同的方式来解决这一问题。StackedGAN 并非学习如何调整噪声以产生所需的输出,而是将 GAN 拆解成一堆 GAN。每个 GAN 都以通常的鉴别器对抗方式独立训练,并拥有自己的潜在编码。
图 6.2.1 向我们展示了 StackedGAN 如何在假设的名人面部生成背景下工作。假设 编码器 网络经过训练,能够分类名人面孔。
编码器 网络由一堆简单的编码器组成,编码器 i 其中 i = 0 … n - 1 对应于 n 个特征。每个编码器提取某些面部特征。例如,编码器[0] 可能是用于发型特征的编码器,特征1。所有简单的编码器共同作用,使得整个 编码器 能正确预测。
StackedGAN 背后的思想是,如果我们想要构建一个生成虚假名人面孔的 GAN,我们应该简单地反转编码器。StackedGAN 由一堆简单的 GAN 组成,GAN[i],其中 i = 0 … n - 1 对应n个特征。每个 GAN[i]学习反转其对应编码器编码器[i]的过程。例如,GAN[0]从虚假的发型特征生成虚假的名人面孔,这是编码器[0]过程的反转。
每个GAN[i]使用一个潜在代码z[i],它决定生成器的输出。例如,潜在代码z[0]可以将发型从卷发改变为波浪发型。GAN 堆叠也可以作为一个整体,用来合成虚假的名人面孔,完成整个编码器的反向过程。每个GAN[i]的潜在代码z[i]可用于改变虚假名人面孔的特定属性:

图 6.2.1:在生成名人面孔的背景下,StackedGAN 的基本思想。假设存在一个假设的深度编码器网络,能够对名人面孔进行分类,StackedGAN 只是反转编码器的过程。
在 Keras 中实现 StackedGAN
StackedGAN 的详细网络模型可以在下图中看到。为了简洁起见,每个堆叠中仅显示了两个编码器-GAN。图看起来可能很复杂,但它只是编码器-GAN 的重复。换句话说,如果我们理解了如何训练一个编码器-GAN,那么其他的也遵循相同的概念。在接下来的部分中,我们假设 StackedGAN 是为 MNIST 数字生成设计的:

图 6.2.2:StackedGAN 由编码器和 GAN 的堆叠组成。编码器经过预训练,用于执行分类任务。生成器[1],G[1],学习基于虚假标签y[f]和潜在代码z[1][f]合成f[1][f]特征。生成器[0],G[0],使用虚假特征f[1][f]和潜在代码z[0][f]生成虚假图像。
StackedGAN 以编码器开始。它可以是一个经过训练的分类器,用于预测正确的标签。中间特征向量f[1][r]可用于 GAN 训练。对于 MNIST,我们可以使用类似于第一章中讨论的基于 CNN 的分类器,使用 Keras 介绍深度学习。下图显示了编码器及其在 Keras 中的网络模型实现:

图 6.2.3:StackedGAN 中的编码器是一个简单的基于 CNN 的分类器
列表 6.2.1展示了前图的 Keras 代码。它类似于第一章中的基于 CNN 的分类器,Keras 的高级深度学习介绍,除了我们使用Dense层提取 256 维特征。这里有两个输出模型,Encoder[0]和Encoder[1]。两者都将用于训练 StackedGAN。
Encoder[0]的输出,f[0][r],是我们希望Generator[1]学习合成的 256 维特征向量。它作为Encoder[0]的辅助输出,E[0]。整体的Encoder被训练用于分类 MNIST 数字,x [r]。正确的标签,y [r],由Encoder[1],E[1]预测。在此过程中,中间特征集,f[1]r,被学习并可用于Generator[0]的训练。在训练 GAN 时,子脚本r用于强调并区分真实数据与假数据。
列表 6.2.1,stackedgan-mnist-6.2.1.py展示了在 Keras 中实现的编码器:
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 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
enc1 = Model(inputs=feature1, outputs=labels, name="encoder1")
# return both enc0 and enc1
return enc0, enc1
| 网络 | 损失函数 | 编号 |
|---|---|---|
| GAN | ![]() ![]() |
4.1.14.1.5 |
| StackedGAN | ![]() ![]() ![]() ![]() 其中 是权重,![]() |
6.2.16.2.26.2.36.2.46.2.5 |
表 6.2.1:GAN 与 StackedGAN 损失函数的比较。~p [data]表示从相应的编码器数据(输入、特征或输出)中采样。
给定Encoder输入(x[r])中间特征(f1r)和标签(y r),每个 GAN 按照常规的鉴别器—对抗性方式进行训练。损失函数由方程 6.2.1至6.2.5在表 6.2.1中给出。方程6.2.1和6.2.2是通用 GAN 的常规损失函数。StackedGAN 有两个额外的损失函数,条件和熵。
条件损失函数,

在方程 6.2.3中,确保生成器在合成输出f[i]时不会忽略输入f[i+1],即使在输入噪声代码z[i]的情况下。编码器,编码器[i],必须能够通过逆转生成器生成器[i]的过程来恢复生成器输入。生成器输入与通过编码器恢复的输入之间的差异由L2或欧几里得距离均方误差(MSE)衡量。图 6.2.4展示了参与计算的网络元素!StackedGAN 在 Keras 中的实现:

图 6.2.4:图 6.2.3 的简化版本,仅显示参与计算的网络元素!StackedGAN 在 Keras 中的实现
然而,条件损失函数引入了一个新问题。生成器忽略输入的噪声代码,z i,并仅依赖于f [i+1]。熵损失函数,

在方程 6.2.4中,确保生成器不忽略噪声代码,z i。Q网络从生成器的输出中恢复噪声代码。恢复的噪声与输入噪声之间的差异也通过L2或均方误差(MSE)进行测量。下图展示了参与计算的网络元素

:

图 6.2.5:图 6.2.3 的简化版本,仅展示了参与计算的网络元素

最后一个损失函数与通常的 GAN 损失相似。它由一个判别器损失组成

以及一个生成器(通过对抗)损失

下图展示了我们 GAN 损失中涉及的元素:

图 6.2.6:图 6.2.3 的简化版本,仅展示了参与计算的网络元素

和

在方程 6.2.5中,三个生成器损失函数的加权和是最终的生成器损失函数。在我们将要展示的 Keras 代码中,所有权重都设置为 1.0,除了熵损失设置为 10.0。方程 6.2.1至方程 6.2.5中,i表示编码器和 GAN 组 ID 或层级。在原始论文中,网络首先独立训练,然后进行联合训练。在独立训练期间,先训练编码器。在联合训练期间,使用真实数据和伪造数据。
StackedGAN 的生成器和判别器的实现仅需对 Keras 作少量修改,以便提供辅助点以访问中间特征。图 6.2.7展示了生成器 Keras 模型。列表 6.2.2阐明了构建两个生成器的函数(gen0 和 gen1),它们分别对应 生成器0 和 生成器1。gen1生成器由三个Dense层组成,标签和噪声编码 z1f 作为输入。第三层生成伪造的 f[1]f 特征。gen0生成器与我们之前介绍的其他 GAN 生成器相似,可以通过gan.py中的生成器构建器进行实例化:
# gen0: feature1 + z0 to feature0 (image)
gen0 = gan.generator(feature1, image_size, codes=z0)
gen0 输入是 f 特征和噪声编码 z。[0] 输出是生成的伪造图像,x[f]:

图 6.2.7:Keras 中的 StackedGAN 生成器模型
列表 6.2.2,stackedgan-mnist-6.2.1.py展示了我们在 Keras 中实现生成器的代码:
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): discrete 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展示了判别器 Keras 模型。我们提供了构建 判别器[0] 和 判别器[1] (dis0 和 dis1) 的函数。dis0 判别器与 GAN 判别器相似,只是输入是特征向量,并且有辅助网络 Q[0] 来恢复 z[0]。gan.py 中的构建器函数用于创建 dis0:
dis0 = gan.discriminator(inputs, num_codes=z_dim)
dis1判别器由三层 MLP 组成,如列表 6.2.3所示。最后一层用于区分真实与伪造的 f[1]。Q[1] 网络共享 dis1 的前两层。其第三层恢复 z[1]:

图 6.2.8:Keras 中的 StackedGAN 判别器模型
列表 6.2.3,stackedgan-mnist-6.2.1.py展示了判别器[1]在 Keras 中的实现:
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-Network 与判别器模型共享一些公共层。因此,它的损失函数也会在判别器模型训练中包含。
列表 6.2.4,stackedgan-mnist-6.2.1.py。在 Keras 中构建 StackedGAN:
def build_and_train_models():
# 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 = 40000
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[1] 然后是 GAN[0])。代码显示在列表 6.2.5中。值得注意的是,训练顺序是:
-
判别器[1] 和 Q[1] 网络通过最小化判别器和熵损失
-
判别器[0] 和 Q[0] 网络通过最小化判别器和熵损失
-
对抗性网络通过最小化对抗性、熵和条件损失
-
对抗性网络通过最小化对抗性、熵和条件损失
列表 6.2.5,stackedgan-mnist-6.2.1.py展示了我们在 Keras 中训练 StackedGAN 的代码:
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 (fr dis1.metrics_names)
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 (fr 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 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 image 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:
if (i + 1) == train_steps:
show = True
else:
show = False
generators = (gen0, gen1)
plot_images(generators,
noise_params=noise_params,
show=show,
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")
StackedGAN 的生成器输出
经过 10,000 步的训练后,StackedGAN 的Generator[0]和Generator[1]模型被保存到文件中。将Generator[0]和Generator[1]堆叠在一起,可以基于标签和噪声代码z[0]和z[1]合成虚假图像。
可以通过以下方式定性验证 StackedGAN 生成器:
-
让离散标签从 0 到 9 变化,同时两个噪声代码,z[0]和z[1],从均值为 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到
-
让第一个噪声代码,z[0],作为常量向量从-4.0 变化到 4.0,用于数字 0 到 9,如下所示。第二个噪声代码,z[0],设置为零向量。图 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 -
让第二个噪声代码,z[1],作为常量向量从-1.0 变化到 1.0,用于数字 0 到 9,如下所示。第一个噪声代码,z[0],设置为零向量。图 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.9:当离散代码从 0 变动到 9 时,由 StackedGAN 生成的图像。两个图像
和
都来自于一个均值为 0,标准差为 0.5 的正态分布。

图 6.2.10:使用 StackedGAN 生成的图像,当第一个噪声代码,z[0],从常量向量-4.0 变化到 4.0 时,适用于数字 0 到 9。z[0]似乎控制每个数字的厚度。

图 6.2.11:当第二个噪声代码,z[1],从常量向量-1.0 到 1.0 变化时,由 StackedGAN 生成的图像。z[1]似乎控制每个数字的旋转(倾斜)和笔画厚度。
图 6.2.9到图 6.2.11展示了 StackedGAN 提供了更多的控制,可以控制生成器输出的属性。控制和属性包括(标签,数字类型),(z0,数字厚度),和(z1,数字倾斜度)。从这个例子来看,我们还可以控制其他可能的实验,例如:
-
增加堆叠元素的数量,从当前的 2 开始
-
降低代码z0 和z1 的维度,像在 InfoGAN 中一样
接下来的图展示了 InfoGAN 和 StackedGAN 的潜在代码差异。解耦代码的基本思想是对损失函数施加约束,使得只有特定的属性会被一个代码所影响。从结构上来看,InfoGAN 比 StackedGAN 更容易实现。InfoGAN 的训练速度也更快:

图 6.2.12:不同 GAN 的潜在表示
结论
在本章中,我们讨论了如何解开 GAN 的潜在表示。我们在本章的早期讨论了 InfoGAN 如何通过最大化互信息来迫使生成器学习解耦的潜在向量。在 MNIST 数据集的例子中,InfoGAN 使用了三个表示和一个噪声编码作为输入。噪声表示其余的属性,以纠缠表示的形式出现。StackedGAN 以不同的方式处理这个问题。它使用一堆编码器 GAN 来学习如何合成虚假的特征和图像。首先训练编码器以提供一个特征数据集。然后,编码器 GANs 被联合训练,学习如何利用噪声编码来控制生成器输出的属性。
在下一章中,我们将介绍一种新的 GAN 类型,它能够在另一个领域生成新数据。例如,给定一张马的图片,该 GAN 可以自动转换为斑马的图片。这种类型的 GAN 的有趣之处在于,它可以在无监督的情况下进行训练。
参考文献
-
Xi Chen 等人。InfoGAN:通过信息最大化生成对抗网络进行可解释的表示学习。《神经信息处理系统进展》,2016(
papers.nips.cc/paper/6399-infogan-interpretable-representation-learning-by-information-maximizing-generative-adversarial-nets.pdf)。 -
Xun Huang 等人。堆叠生成对抗网络。IEEE 计算机视觉与模式识别会议(CVPR)。第 2 卷,2017(
openaccess.thecvf.com/content_cvpr_2017/papers/Huang_Stacked_Generative_Adversarial_CVPR_2017_paper.pdf)。
第七章 跨领域 GAN
在计算机视觉、计算机图形学和图像处理领域,许多任务涉及将图像从一种形式转换为另一种形式。例如,灰度图像的上色、将卫星图像转换为地图、将一位艺术家的作品风格转换为另一位艺术家的风格、将夜间图像转换为白天图像、将夏季照片转换为冬季照片,这些都是例子。这些任务被称为跨领域转换,将是本章的重点。源领域中的图像被转换到目标领域,从而生成一个新的转换图像。
跨领域转换在现实世界中有许多实际应用。例如,在自动驾驶研究中,收集道路场景驾驶数据既费时又昂贵。为了尽可能覆盖多种场景变化,在这个例子中,车辆将会在不同的天气条件、季节和时间下行驶,获取大量多样的数据。利用跨领域转换,能够通过转换现有图像生成看起来逼真的新合成场景。例如,我们可能只需要从一个地区收集夏季的道路场景,从另一个地方收集冬季的道路场景。然后,我们可以将夏季图像转换为冬季图像,将冬季图像转换为夏季图像。这样,可以将需要完成的任务数量减少一半。
生成逼真合成图像是生成对抗网络(GANs)擅长的领域。因此,跨领域转换是 GAN 的一种应用。在本章中,我们将重点介绍一种流行的跨领域 GAN 算法——CycleGAN [2]。与其他跨领域转换算法(如pix2pix [3])不同,CycleGAN 不需要对齐的训练图像就能工作。在对齐图像中,训练数据应由一对图像组成,即源图像及其对应的目标图像。例如,一张卫星图像及其相应的地图。CycleGAN 只需要卫星数据图像和地图。地图可能来自其他卫星数据,并不一定是之前从训练数据生成的。
在本章中,我们将探讨以下内容:
-
CycleGAN 的原理,包括其在 Keras 中的实现
-
CycleGAN 的示例应用,包括使用 CIFAR10 数据集进行灰度图像上色和在 MNIST 数字及街景房屋号码(SVHN)[1]数据集上进行风格转换
CycleGAN 的原理

图 7.1.1:对齐图像对的示例:左侧为原始图像,右侧为使用 Canny 边缘检测器转换后的图像。原始照片由作者拍摄。
从一个领域到另一个领域的图像转换是计算机视觉、计算机图形学和图像处理中的常见任务。前面的图示了边缘检测,这是一个常见的图像转换任务。在这个例子中,我们可以将左侧的真实照片视为源域中的一张图像,而右侧的边缘检测图像视为目标域中的一个样本。还有许多其他跨领域转换过程具有实际应用,例如:
-
卫星图像转换为地图
-
面部图像转换为表情符号、漫画或动漫
-
身体图像转换为头像
-
灰度照片的着色
-
医学扫描图像转换为真实照片
-
真实照片转换为艺术家画作
在不同领域中有许多类似的例子。例如,在计算机视觉和图像处理领域,我们可以通过发明一个提取源图像特征并将其转换为目标图像的算法来执行转换。Canny 边缘检测算子就是这样一个算法的例子。然而,在许多情况下,转换过程非常复杂,手动设计几乎不可能找到合适的算法。源域和目标域的分布都是高维且复杂的:

图 7.1.2:未对齐的图像对示例:左侧是菲律宾大学大学大道上的真实向日葵照片,右侧是伦敦国家美术馆的文森特·梵高的《向日葵》。原始照片由作者拍摄。
解决图像转换问题的一种方法是使用深度学习技术。如果我们拥有来自源域和目标域的足够大的数据集,我们可以训练神经网络来建模转换。由于目标域中的图像必须根据源图像自动生成,因此它们必须看起来像目标域中的真实样本。GANs 是适合此类跨领域任务的网络。pix2pix [3]算法就是一个跨领域算法的例子。
pix2pix 类似于我们在第四章")中讨论的条件 GAN(CGAN)[4],生成对抗网络 (GANs)。我们可以回顾一下,在条件 GAN 中,除了噪声输入 z 外,一个条件(如一-hot 向量)会限制生成器的输出。例如,在 MNIST 数字中,如果我们希望生成器输出数字 8,则条件是一-hot 向量[0, 0, 0, 0, 0, 0, 0, 0, 1, 0]。在 pix2pix 中,条件是待转换的图像。生成器的输出是转换后的图像。pix2pix 通过优化条件 GAN 损失进行训练。为了最小化生成图像中的模糊,还包括了L1损失。
类似于 pix2pix 的神经网络的主要缺点是训练输入和输出图像必须对齐。图 7.1.1 是一个对齐图像对的示例。样本目标图像是从源图像生成的。在大多数情况下,对齐的图像对无法获得,或者从源图像生成对齐图像的成本较高,或者我们不知道如何从给定的源图像生成目标图像。我们拥有的是来自源领域和目标领域的样本数据。图 7.1.2 是一个示例,展示了同一向日葵主题的源领域(真实照片)和目标领域(梵高艺术风格)的数据。源图像和目标图像不一定对齐。
与 pix2pix 不同,CycleGAN 只要有足够的源数据和目标数据的变换和多样性,就能学习图像翻译。无需对齐。CycleGAN 学习源分布和目标分布,并从给定的样本数据中学习如何从源分布翻译到目标分布。不需要监督。在 图 7.1.2 的背景下,我们只需要成千上万的真实向日葵照片和成千上万的梵高向日葵画作照片。训练完 CycleGAN 后,我们就能将一张向日葵照片翻译成一幅梵高的画作:

图 7.1.3:CycleGAN 模型由四个网络组成:生成器 G、生成器 F、判别器 D[y] 和判别器 D[x]
CycleGAN 模型
图 7.1.3 显示了 CycleGAN 的网络模型。CycleGAN 的目标是学习以下函数:
y' = G(x) (公式 7.1.1)
这会生成目标领域的虚假图像,y ', 作为真实源图像 x 的函数。学习是无监督的,仅利用源领域和目标领域中可用的真实图像 x 和 y 进行学习。
与常规的 GAN 不同,CycleGAN 强加了循环一致性约束。正向循环一致性网络确保能够从虚假的目标数据中重建真实的源数据:
x' = F(G(x)) (公式 7.1.2)
通过最小化正向循环一致性 L1 损失来实现这一目标:

(公式 7.1.3)
网络是对称的。反向循环一致性网络也试图从虚假的源数据中重建真实的目标数据:
y ' = G(F(y)) (公式 7.1.4)
通过最小化反向循环一致性 L1 损失来实现这一目标:

(公式 7.1.5)
这两个损失的和被称为循环一致性损失:


(公式 7.1.6)
循环一致性损失使用 L1 或 均值绝对误差 (MAE),因为与 L2 或 均方误差 (MSE) 相比,它通常能得到更少模糊的图像重建。
与其他 GAN 类似,CycleGAN 的最终目标是让生成器G学习如何合成能够欺骗正向循环判别器D[y]的伪造目标数据y '。由于网络是对称的,CycleGAN 还希望生成器F学习如何合成能够欺骗反向循环判别器D[x]的伪造源数据x '。受最小二乘 GAN(LSGAN)[5]的更好感知质量启发,如第五章中所述,改进的 GAN,CycleGAN 也使用 MSE 作为判别器和生成器的损失。回想一下 LSGAN 与原始 GAN 的不同之处在于,LSGAN 使用 MSE 损失而不是二元交叉熵损失。CycleGAN 将生成器-判别器损失函数表示为:

(方程式 7.1.7)

(方程式 7.1.8)

(方程式 7.1.9)

(方程式 7.1.10)

(方程式 7.1.11)

(方程式 7.1.12)
CycleGAN 的总损失如下所示:

(方程式 7.1.13)
CycleGAN 推荐以下权重值:

和

以便更重视循环一致性检查。
训练策略类似于原始 GAN。算法 7.1.1 总结了 CycleGAN 的训练过程。
重复进行n次训练步骤:
-
最小化
![CycleGAN 模型]()
通过使用真实的源数据和目标数据训练正向循环判别器。一个真实目标数据的小批量,y,被标记为 1.0。一个伪造目标数据的小批量,y ' = G(x),被标记为 0.0。
-
最小化
![CycleGAN 模型]()
通过使用真实的源数据和目标数据训练反向循环判别器。一个真实源数据的小批量,x,被标记为 1.0。一个伪造源数据的小批量,x ' = F(y),被标记为 0.0。
-
最小化
![CycleGAN 模型]()
和
![CycleGAN 模型]()
通过在对抗网络中训练正向循环和反向循环生成器。一个伪造目标数据的小批量,y ' = G(x),被标记为 1.0。一个伪造源数据的小批量,x ' = F(y),被标记为 1.0。判别器的权重被冻结。

图 7.1.4:在风格迁移过程中,颜色组成可能无法成功迁移。为了解决这个问题,加入了身份损失到总损失函数中。

图 7.1.5:包含身份损失的 CycleGAN 模型,如图像左侧所示
在神经风格迁移问题中,颜色组成可能无法从源图像成功传递到假目标图像中。这个问题如图 7.1.4所示。为了解决这个问题,CycleGAN 提出了包含前向和反向循环身份损失函数的方案:

(方程 7.1.14)
CycleGAN 的总损失为:

(方程 7.1.15)
与

身份损失也会在对抗训练过程中得到优化。图 7.1.5展示了带有身份损失的 CycleGAN。
使用 Keras 实现 CycleGAN
让我们解决一个 CycleGAN 可以处理的简单问题。在第三章中,自动编码器,我们使用一个自动编码器对 CIFAR10 数据集中的灰度图像进行上色。我们可以回想起,CIFAR10 数据集由 50,000 个训练数据和 10,000 个测试数据样本组成,所有图像都是 32 × 32 的 RGB 图像,属于十个类别。我们可以使用 rgb2gray(RGB) 将所有彩色图像转换为灰度图像,如第三章中讨论的自动编码器。
继承之前,我们可以使用灰度训练图像作为源领域图像,原始彩色图像作为目标领域图像。值得注意的是,尽管数据集是对齐的,但我们输入到 CycleGAN 中的是一组随机的彩色图像样本和一组随机的灰度图像样本。因此,我们的 CycleGAN 不会将训练数据视为对齐的。训练完成后,我们将使用测试灰度图像来观察 CycleGAN 的性能:

图 7.1.6:前向循环生成器 G,Keras 中的实现。该生成器是一个由编码器和解码器构成的 U-Net 网络。
正如上一节所讨论的,要实现 CycleGAN,我们需要构建两个生成器和两个判别器。CycleGAN 的生成器学习源输入分布的潜在表示,并将该表示转换为目标输出分布。这正是自动编码器所做的。然而,类似于第三章中讨论的典型自动编码器,自动编码器,使用一个编码器,该编码器将输入下采样直到瓶颈层,在该层之后的过程在解码器中被反转。这种结构在某些图像翻译问题中并不适用,因为编码器和解码器层之间共享了许多低级特征。例如,在上色问题中,灰度图像的形状、结构和边缘与彩色图像中的相同。为了解决这个问题,CycleGAN 生成器采用了U-Net [7]结构,如图 7.1.6所示。
在 U-Net 结构中,编码器层 e n-i 的输出与解码器层 d i 的输出进行拼接,其中 n = 4 是编码器/解码器层的数量,i = 1, 2 和 3 是共享信息的层编号。
我们应该注意到,尽管示例中使用了n = 4,但具有更高输入/输出维度的问题可能需要更深的编码器/解码器。U-Net 结构允许编码器和解码器之间自由流动特征级别的信息。编码器层由 Instance Normalization(IN)-LeakyReLU-Conv2D 组成,而解码器层由 IN-ReLU-Conv2D 组成。编码器/解码器层的实现见 Listing 7.1.1,生成器的实现见 Listing 7.1.2。
注意
完整的代码可以在 GitHub 上找到:
github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras
实例归一化(IN)是每个数据样本的 批归一化(BN)(即 IN 是每个图像或每个特征的 BN)。在风格迁移中,归一化对比度是按每个样本进行的,而不是按批次进行。实例归一化等同于对比度归一化。同时,批归一化会破坏对比度归一化。
注意
在使用实例归一化之前,请记得安装 keras-contrib:
$ sudo pip3 install git+https://www.github.com/keras-team/keras-contrib.git
Listing 7.1.1,cyclegan-7.1.1.py 展示了 Keras 中编码器和解码器层的实现:
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()(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()(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
Listing 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 判别器。输入图像被下采样多次(在本示例中,下采样了三次)。最后一层是一个 Dense(1) 层,用来预测输入图像是真实的概率。每一层与生成器的编码器层相似,只是没有使用 IN。然而,在处理大图像时,使用单一的数值来判断图像是“真实”还是“假”在参数上效率较低,且会导致生成器生成的图像质量较差。
解决方案是使用 PatchGAN [6],它将图像划分为一个补丁网格,并使用标量值网格来预测这些补丁是否真实。普通 GAN 判别器与 2 × 2 PatchGAN 判别器的比较见 图 7.1.7。在本示例中,补丁之间没有重叠,且在边界处相接。然而,通常情况下,补丁可能会有重叠。
我们应该注意到,PatchGAN 在 CycleGAN 中并没有引入一种新的 GAN 类型。为了提高生成图像的质量,若使用 2 × 2 PatchGAN,我们不再只有一个输出去进行判别,而是有四个输出进行判别。损失函数没有变化。直观来说,这是有道理的,因为如果图像的每个补丁或部分看起来都是真实的,那么整张图像看起来也会更真实。

图 7.1.7:GAN 和 PatchGAN 判别器的比较
以下图示展示了在 Keras 中实现的判别器网络。图示展示了判别器判断输入图像或图像块是否为彩色 CIFAR10 图像的概率。由于输出图像仅为 32×32 RGB 的小图像,使用一个标量表示图像是否真实就足够了。然而,我们也评估了使用 PatchGAN 时的结果。清单 7.1.3展示了判别器的函数构建器:

图 7.1.8:目标判别器D[y]在 Keras 中的实现。PatchGAN 判别器显示在右侧。
清单 7.1.3,cyclegan-7.1.1.py展示了在 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=1,
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 和循环一致性损失。与前几章的 GAN 类似,我们使用学习率为 2e-4、衰减率为 6e-8 的 RMSprop 优化器来优化判别器。对抗网络的学习率和衰减率是判别器的一半。
清单 7.1.4,cyclegan-7.1.1.py展示了我们在 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'])
# freeze the discriminator weights in the adversarial model
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的训练程序。以下清单展示了 CycleGAN 训练。这与传统 GAN 训练的细微区别在于,CycleGAN 有两个判别器需要优化。然而,只有一个对抗模型需要优化。每 2000 步,生成器保存预测的源图像和目标图像。我们使用批次大小为 32。我们也尝试过批次大小为 1,但输出质量几乎相同,只是训练时间更长(批次大小为 1 时每张图像 43 毫秒,批次大小为 32 时每张图像 3.6 毫秒,使用 NVIDIA GTX 1060)。
清单 7.1.5,cyclegan-7.1.1.py展示了我们在 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:
if (step + 1) == train_steps:
show = True
else:
show = False
test_generator((g_source, g_target),
(test_source_data, test_target_data),
step=step+1,
titles=titles,
dirs=dirs,
show=show)
# 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 的训练数据和测试数据。有关这两个文件的详细信息,请参考源代码。加载数据后,训练和测试图像将被转换为灰度图像,以生成源数据和测试源数据。
以下代码段展示了如何使用 CycleGAN 构建和训练一个生成器网络(g_target)来对灰度图像进行着色。由于 CycleGAN 是对称的,我们还构建并训练了第二个生成器网络(g_source),将彩色图像转化为灰度图像。两个 CycleGAN 着色网络已经训练完成。第一个使用类似普通 GAN 的标量输出判别器;第二个使用 2 × 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 的生成器输出
图 7.1.9展示了 CycleGAN 的着色结果。源图像来自测试数据集。为了进行比较,我们展示了地面真实图像以及使用简单自动编码器(第三章,自动编码器)进行着色的结果。总体而言,所有着色图像在视觉上都是可以接受的。总体来看,每种着色技术都有其优缺点。所有着色方法在天空和车辆的真实颜色上都有不一致的地方。
例如,飞机背景中的天空(第 3 行,第 2 列)是白色的。自动编码器正确预测了这一点,但 CycleGAN 认为它是浅棕色或蓝色的。对于第 6 行,第 6 列,海上船只的灰暗天空被自动编码器着色为蓝天蓝海,而 CycleGAN(没有 PatchGAN)则预测为蓝海白天。两种预测在现实世界中都有其合理性。同时,使用 PatchGAN 的 CycleGAN 的预测接近真实值。在倒数第二行和第二列,任何方法都未能预测出汽车的红色。在动物图像上,CycleGAN 的两种变体都接近真实值的颜色。
由于 CycleGAN 是对称的,它也能根据彩色图像预测灰度图像。图 7.1.10展示了两种 CycleGAN 变体执行的彩色转灰度转换。目标图像来自测试数据集。除了某些图像灰度色调的细微差异外,预测结果通常是准确的:

图 7.1.9:使用不同技术进行的着色。展示了地面真实图像、使用自动编码器(第三章,自动编码器)进行的着色、使用带有普通 GAN 判别器的 CycleGAN 进行的着色,以及使用 PatchGAN 判别器的 CycleGAN 进行的着色。最佳观看效果为彩色。原始彩色照片可在本书的 GitHub 库中找到,网址为:https://github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras/blob/master/chapter7-cross-domain-gan/README.md。

图 7.1.10:CycleGAN 的彩色(图 7.1.9 中的内容)到灰度转换
读者可以通过使用预训练的带 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 在 MNIST 和 SVHN 数据集上的应用
我们现在要解决一个更具挑战性的问题。假设我们使用灰度的 MNIST 数字作为源数据,并希望借用 SVHN [1](我们的目标数据)中的风格。每个领域的示例数据如图 7.1.11所示。我们可以重用上一节中讨论的所有构建和训练 CycleGAN 的函数来执行风格迁移。唯一的区别是我们需要为加载 MNIST 和 SVHN 数据添加例程。
我们引入了模块mnist_svhn_utils.py来帮助我们完成这项任务。列表 7.1.7展示了用于跨领域迁移的 CycleGAN 的初始化和训练。CycleGAN 结构与前一节相同,只是我们使用了 5 的核大小,因为这两个领域之间有很大的差异:

图 7.1.11:两个不同领域的数据未对齐。原始彩色照片可以在书籍的 GitHub 库中找到,网址为 https://github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras/blob/master/chapter7-cross-domain-gan/README.md。
注意
在使用实例归一化之前,请记得安装keras-contrib:
$ sudo pip3 install git+https://www.github.com/keras-team/keras-contrib.git
列表 7.1.7 中的cyclegan-7.1.1.py展示了 MNIST 和 SVHN 之间跨领域风格迁移的 CycleGAN:
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)
# genrate 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.12所示。生成的图像具有 SVHN 的风格,但数字没有完全迁移。例如,在第 4 行中,数字 3、1 和 3 被 CycleGAN 进行了风格化。然而,在第 3 行中,数字 9、6 和 6 分别被 CycleGAN 风格化为 0、6、01、0、65 和 68,分别在没有 PatchGAN 和使用 PatchGAN 时的结果不同。
向后循环的结果如图 7.1.13所示。在这种情况下,目标图像来自 SVHN 测试数据集。生成的图像具有 MNIST 的风格,但数字没有正确转换。例如,在第 1 行中,数字 5、2 和 210 被 CycleGAN 分别转换为 7、7、8、3、3 和 1,其中不使用 PatchGAN 和使用 PatchGAN 时的结果不同。
对于 PatchGAN,输出 1 是可以理解的,因为预测的 MNIST 数字被限制为一个数字。在 SVHN 数字的第 2 行最后 3 列中,像 6、3 和 4 这样的数字被 CycleGAN 转换为 6、3 和 6,但没有 PatchGAN 的情况下。然而,CycleGAN 的两个版本的输出始终是单一数字并且具有可识别性。
从 MNIST 转换到 SVHN 时出现的问题,其中源域中的一个数字被转换为目标域中的另一个数字,称为 标签翻转 [8]。尽管 CycleGAN 的预测是循环一致的,但它们不一定是语义一致的。数字的意义在转换过程中丧失。为了解决这个问题,Hoffman [8] 提出了改进版的 CycleGAN,称为 CyCADA(循环一致对抗领域适配)。其区别在于额外的语义损失项确保了预测不仅是循环一致的,而且是语义一致的:

图 7.1.12:将测试数据从 MNIST 域进行风格迁移到 SVHN。原始彩色照片可以在本书的 GitHub 仓库中找到,网址为 https://github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras/blob/master/chapter7-cross-domain-gan/README.md。

图 7.1.13:将测试数据从 SVHN 域进行风格迁移到 MNIST。原始彩色照片可以在本书的 GitHub 仓库中找到,网址为 https://github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras/blob/master/chapter7-cross-domain-gan/README.md。

图 7.1.14:CycleGAN 与 PatchGAN 在 MNIST(源)到 SVHN(目标)的前向循环。重建后的源图像与原始源图像相似。原始彩色照片可以在本书的 GitHub 仓库中找到,网址为 https://github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras/blob/master/chapter7-cross-domain-gan/README.md。

图 7.1.15:CycleGAN 与 PatchGAN 在 MNIST(源)到 SVHN(目标)的反向循环。重建后的目标图像与原始目标图像不完全相似。原始彩色照片可以在本书的 GitHub 仓库中找到,网址为 https://github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras/blob/master/chapter7-cross-domain-gan/README.md。
在 图 7.1.3 中,CycleGAN 被描述为是循环一致的。换句话说,给定源 x,CycleGAN 在前向循环中重建源为 x '。此外,给定目标 y,CycleGAN 在反向循环中重建目标为 y '。
图 7.1.14 显示了 CycleGAN 在前向循环中重建 MNIST 数字。重建后的 MNIST 数字几乎与源 MNIST 数字完全相同。图 7.1.15 显示了 CycleGAN 在反向循环中重建 SVHN 数字。许多目标图像被重建。某些数字是完全相同的,例如第二行最后两列(3 和 4)。而有些数字相同但模糊,如第一行前两列(5 和 2)。有些数字被转换为另一个数字,尽管风格保持不变,例如第二行前两列(从 33 和 6 变为 1 和一个无法识别的数字)。
在个人层面上,我建议你使用 CycleGAN 的预训练模型与 PatchGAN 进行图像翻译:
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 中,源数据和目标数据不一定是对齐的。我们展示了两个例子,灰度 ↔ 彩色,和 MNIST ↔ SVHN。虽然 CycleGAN 可以执行许多其他可能的图像翻译任务。
在下一章中,我们将探讨另一类生成模型,变分自编码器(VAE)。VAE 的目标与生成新图像(数据)相似,重点在于学习作为高斯分布建模的潜在向量。我们还将展示 GAN 所解决问题的其他相似之处,表现为条件 VAE 和 VAE 中潜在表示的解耦。
参考文献
-
Yuval Netzer 等人. 使用无监督特征学习读取自然图像中的数字. NIPS 深度学习与无监督特征学习研讨会. Vol. 2011. No. 2. 2011 (
www-cs.stanford.edu/~twangcat/papers/nips2011_housenumbers.pdf). -
Zhu, Jun-Yan 等人. 使用循环一致生成对抗网络进行无配对图像到图像翻译. 2017 IEEE 国际计算机视觉大会 (ICCV). IEEE, 2017 (
openaccess.thecvf.com/content_ICCV_2017/papers/Zhu_Unpaired_Image-To-Image_Translation_ICCV_2017_paper.pdf). -
Phillip Isola 等人. 使用条件生成对抗网络的图像到图像翻译. 2017 IEEE 计算机视觉与模式识别大会 (CVPR). IEEE, 2017 (
openaccess.thecvf.com/content_cvpr_2017/papers/Isola_Image-To-Image_Translation_With_CVPR_2017_paper.pdf). -
Mehdi Mirza 和 Simon Osindero. 条件生成对抗网络. arXiv 预印本 arXiv:1411.1784, 2014 (
arxiv.org/pdf/1411.1784.pdf). -
Xudong Mao 等人. 最小二乘生成对抗网络. 2017 IEEE 国际计算机视觉大会 (ICCV). IEEE, 2017 (
openaccess.thecvf.com/content_ICCV_2017/papers/Mao_Least_Squares_Generative_ICCV_2017_paper.pdf). -
Chuan Li 和 Michael Wand. 使用马尔可夫生成对抗网络的预计算实时纹理合成. 欧洲计算机视觉会议. Springer, Cham, 2016 (
arxiv.org/pdf/1604.04382.pdf). -
Olaf Ronneberger, Philipp Fischer 和 Thomas Brox. U-Net: 用于生物医学图像分割的卷积网络. 国际医学图像计算与计算机辅助干预会议。Springer,Cham,2015 (
arxiv.org/pdf/1505.04597.pdf)。 -
Judy Hoffman 等人. CyCADA: 循环一致性对抗域适应. arXiv 预印本 arXiv:1711.03213,2017 (
arxiv.org/pdf/1711.03213.pdf)。
第八章 变分自编码器(VAE)
与我们在前几章讨论的生成对抗网络(GANs)类似,变分自编码器(VAE)[1]属于生成模型家族。VAE 的生成器能够在其连续的潜在空间中生成有意义的输出。解码器输出的可能属性通过潜在向量进行探索。
在 GAN 中,重点是如何得到一个能够逼近输入分布的模型。VAE 则试图通过可解码的连续潜在空间来建模输入分布。这是 GAN 与 VAE 生成更逼真信号的一个可能的根本原因。例如,在图像生成中,GAN 能够生成更逼真的图像,而 VAE 则生成较为模糊的图像。
在 VAE 中,重点是潜在代码的变分推断。因此,VAE 为学习和高效的贝叶斯推断提供了一个合适的框架。例如,具有解耦表示的 VAE 可以使潜在代码在迁移学习中得到重用。
从结构上看,VAE 与自编码器相似。它们也由编码器(也称为识别或推断模型)和解码器(也称为生成模型)组成。VAE 和自编码器都试图在学习潜在向量的同时重建输入数据。然而,与自编码器不同,VAE 的潜在空间是连续的,解码器本身被用作生成模型。
与前几章讨论的生成对抗网络(GANs)类似,VAE 的解码器也可以进行条件化。例如,在 MNIST 数据集中,我们可以指定给定一个 one-hot 向量时要生成的数字。这类条件 VAE 被称为 CVAE [2]。VAE 的潜在向量也可以通过在损失函数中加入正则化超参数来进行解耦。这被称为

-VAE [5]。例如,在 MNIST 中,我们可以分离出决定每个数字的厚度或倾斜角度的潜在向量。
本章的目标是介绍:
-
VAE 的原理
-
理解重新参数化技巧,帮助在 VAE 优化中使用随机梯度下降
-
条件 VAE(CVAE)的原理和
![变分自编码器(VAE)]()
-VAE
-
理解如何在 Keras 库中实现 VAE
VAE 原理
在生成模型中,我们通常感兴趣的是使用神经网络来逼近输入的真实分布:

(公式 8.1.1)
在前面的公式中,

是在训练过程中确定的参数。例如,在名人面孔数据集的背景下,这相当于找到一个可以生成面孔的分布。类似地,在 MNIST 数据集中,这个分布可以生成可识别的手写数字。
在机器学习中,为了进行某种层次的推理,我们感兴趣的是找到

,是输入x与潜在变量z之间的联合分布。潜在变量不是数据集的一部分,而是编码从输入中可观察到的某些属性。在名人面孔的背景下,这些可能是面部表情、发型、发色、性别等。在 MNIST 数据集中,潜在变量可能表示数字和书写风格。

实际上是输入数据点及其属性的分布。P θ(x)可以从边际分布中计算出来:

(方程 8.1.2)
换句话说,考虑所有可能的属性,我们最终得到了描述输入的分布。在名人面孔的例子中,如果我们考虑所有的面部表情、发型、发色、性别,那么描述名人面孔的分布就能被恢复出来。在 MNIST 数据集中,如果我们考虑所有可能的数字、书写风格等,最终我们得到了手写数字的分布。
问题是方程 8.1.2是不可解的。这个方程没有解析形式或有效的估计器,无法对其参数进行微分。因此,使用神经网络进行优化是不可行的。
使用贝叶斯定理,我们可以为方程 8.1.2找到另一种表达方式:

(方程 8.1.3)
P(z)是z的先验分布。它不依赖于任何观察。如果z是离散的,并且

是高斯分布,那么

是高斯混合分布。如果z是连续的,

它是高斯分布的无限混合。
实际上,如果我们尝试构建一个神经网络来逼近

如果没有合适的损失函数,它将忽略z并得出一个琐碎的解

=

因此,方程 8.1.3不能为我们提供一个好的估计

.
另外,方程 8.1.2也可以表达为:

(方程 8.1.4)
然而,

也是不可解的。VAEs 的目标是找到一个可解的分布,能紧密地估计

.
变分推断
为了使

为了可处理性,VAE 引入了变分推断模型(编码器):

(方程式 8.1.5)

提供了一个对的良好估计

. 它既是参数化的,又是可处理的。

可以通过深度神经网络优化参数来近似

.
通常,

被选为多元高斯分布:

(方程式 8.1.6)
两者的含义是,

,以及标准差,

,由编码器神经网络使用输入数据点计算得出。对角矩阵意味着 z 的元素是独立的。
核心方程
推断模型

从输入 x 生成潜在向量 z。

它类似于自编码器模型中的编码器。另一方面,

从潜在代码 z 重构输入数据。

其作用类似于自编码器模型中的解码器。为了估计

,我们必须确定它与的关系

和

.
如果

是对的估计

,Kullback-Leibler(KL)散度决定了这两个条件概率分布之间的距离:

(方程式 8.1.7)
使用贝叶斯定理,

(方程式 8.1.8)
在 方程式 8.1.7 中,

(方程式 8.1.9)

可以将期望值提出,因为它不依赖于

. 重新排列前面的方程并认识到

:

(方程式 8.1.10)
方程式 8.1.10 是变分自编码器(VAE)的核心。左边是

我们正在最大化的是由于距离造成的误差较小

来自真实

. 我们可以回忆起对数运算不会改变极大值(或极小值)的位置。给定一个能够提供良好估计的推断模型

,

近似为零。第一项,

,右侧看起来像一个解码器,它从推理模型中提取样本来重建输入。第二项是另一个距离。这一次,它是在

先验

。
方程 8.1.10的左侧也被称为变分下界或证据下界 (ELBO)。由于 KL 总是正值,ELBO 是

。通过优化参数来最大化 ELBO

和

神经网络的意义是:
-
或推理模型在编码x的属性到z时变得更好 -
在方程 8.1.10的右侧被最大化,或者解码器模型在从潜在向量z重建x方面变得更好
优化
方程 8.1.10的右侧包含关于 VAE 损失函数的两个重要信息。解码器项

意味着生成器从推理模型的输出中获取z样本来重建输入。最大化这一项意味着我们最小化重建损失,

。如果假设图像(数据)分布是高斯分布,则可以使用 MSE。如果每个像素(数据)被视为伯努利分布,则损失函数是二元交叉熵。
第二项,

,结果证明可以直接评估。从方程 8.1.6,

是一个高斯分布。通常,

也是一个均值为零、标准差为 1.0 的高斯分布。KL 项简化为:

(方程 8.1.11)
其中

是z的维度。两者

和

是通过推理模型计算的x的函数。为了最大化

,

和

。选择

来源于各向同性单位高斯的属性,通过适当的函数可以将其变形成任意分布。从方程 8.1.11,KL 损失

只是

。
注
例如,之前[6]已证明,可以使用该函数将各向同性高斯分布变形成环形分布

。
读者可以进一步探讨 Luc Devroye 的理论,基于样本的非均匀随机变异生成 [7]。
总结来说,VAE 损失函数被定义为:

(方程式 8.1.12)
重参数化技巧

图 8.1.1:带有和不带有重参数化技巧的 VAE 网络
在前图的左侧展示了 VAE 网络。编码器接受输入x,并估计均值,

,以及标准差,

,多元高斯分布的潜在向量z。解码器从潜在向量z中采样,重建输入为

。这看起来很简单,直到反向传播时梯度更新发生。
反向传播的梯度不会通过随机采样块。虽然神经网络可以有随机输入,但梯度无法通过随机层。
解决此问题的方法是将采样过程作为输入推送出去,如图 8.1.1右侧所示。然后,计算样本为:

(方程式 8.1.13)
如果

和

以向量格式表达,然后

是逐元素相乘。使用方程式 8.1.13,看起来就像采样直接来自原本设想的潜在空间。这个技巧被称为重参数化技巧。
现在采样发生在输入端,VAE 网络可以使用熟悉的优化算法进行训练,如 SGD、Adam 或 RMSProp。
解码器测试
在训练完 VAE 网络后,推理模型,包括加法和乘法操作符,可以丢弃。为了生成新的有意义的输出,从生成所使用的高斯分布中采样。

。接下来的图展示了我们如何测试解码器:

图 8.1.2:解码器测试设置
Keras 中的 VAE
VAE 的结构与典型的自编码器相似。区别主要在于重参数化技巧中的高斯随机变量采样。列表 8.1.1展示了使用 MLP 实现的编码器、解码器和 VAE。这个代码也已经贡献到官方的 Keras GitHub 库中。为了简化讨论,潜在向量z是二维的。
编码器只是一个两层的 MLP,第二层生成均值和对数方差。使用对数方差是为了简化KL 损失和重参数化技巧的计算。编码器的第三个输出是使用重参数化技巧对z的采样。我们需要注意,在采样函数中,

由于

给定它是高斯分布的标准差。
解码器也是一个两层的 MLP,它接受 z 的样本来近似输入。编码器和解码器都使用一个大小为 512 的中间维度。
VAE 网络实际上是编码器和解码器的组合。图 8.1.3 到 图 8.1.5 显示了编码器、解码器和 VAE 模型。损失函数是 重建损失 和 KL 损失 的总和。VAE 网络在默认的 Adam 优化器下表现良好。VAE 网络的总参数数目为 807,700。
Keras 中的 VAE MLP 代码已经预训练权重。要进行测试,我们需要运行:
$ python3 vae-mlp-mnist-8.1.1.py --weights=vae_mlp_mnist.h5
注意
完整代码可以在以下链接中找到:github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras。
清单 8.1.1,vae-mlp-mnist-8.1.1.py 显示了使用 MLP 层的 VAE Keras 代码:
# reparameterization trick
# instead of sampling from Q(z|X), sample eps = N(0,I)
# z = z_mean + sqrt(var)*eps
def sampling(args):
z_mean, z_log_var = args
batch = K.shape(z_mean)[0]
# K is the keras backend
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
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')
encoder.summary()
plot_model(encoder, to_file='vae_mlp_encoder.png', show_shapes=True)
# 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')
decoder.summary()
plot_model(decoder, to_file='vae_mlp_decoder.png', show_shapes=True)
# instantiate vae model
outputs = decoder(encoder(inputs)[2])
vae = Model(inputs, outputs, name='vae_mlp')
if __name__ == '__main__':
parser = argparse.ArgumentParser()
help_ = "Load h5 model trained weights"
parser.add_argument("-w", "--weights", help=help_)
help_ = "Use mse loss instead of binary cross entropy (default)"
parser.add_argument("-m",
"--mse",
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.mse:
reconstruction_loss = mse(inputs, outputs)
else:
reconstruction_loss = binary_crossentropy(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')
vae.summary()
plot_model(vae,
to_file='vae_mlp.png',
show_shapes=True)
if args.weights:
vae = vae.load_weights(args.weights)
else:
# train the autoencoder
vae.fit(x_train,
epochs=epochs,
batch_size=batch_size,
validation_data=(x_test, None))
vae.save_weights('vae_mlp_mnist.h5')
plot_results(models,
data,
batch_size=batch_size,
model_name="vae_mlp")

图 8.1.3:VAE MLP 的编码器模型

图 8.1.4:VAE MLP 的解码器模型

图 8.1.5:使用 MLP 的 VAE 模型
图 8.1.6 显示了使用 plot_results() 在 50 个 epoch 后的潜在向量连续空间。为简化起见,函数在此未显示,但可以在 vae-mlp-mnist-8.1.1.py 的其余代码中找到。该函数绘制了两个图像,分别是测试数据集标签(图 8.1.6)和生成的数字样本(图 8.1.7),两者均为 z 的函数。这两个图展示了潜在向量如何决定生成数字的特征。
在连续空间中导航总是会得到一个与 MNIST 数字相似的输出。例如,数字 9 的区域靠近数字 7 的区域。从接近中心的 9 向左移动将数字变为 7。从中心向下移动将生成的数字从 3 变为 8,最终变为 1。数字的形态变化在 图 8.1.7 中更为明显,这也是对 图 8.1.6 的另一种解释。
在 图 8.1.7 中,生成器输出显示了潜在空间中数字的分布,而不是色条。可以观察到所有数字都被表示出来。由于分布在中心附近较为密集,因此在中间区域的变化较快,而在均值较大的地方变化较慢。我们需要记住,图 8.1.7 是 图 8.1.6 的反映。例如,数字 0 在两个图中的右上象限,而数字 1 在右下象限。
在 图 8.1.7 中有一些无法识别的数字,尤其是在左上方的象限。从下面的图中可以观察到,该区域大多为空,并且远离中心:

图 8.1.6:测试数据集的潜在向量均值(VAE MLP)。颜色条显示了与 z 对应的 MNIST 数字。彩色图像可在书籍的 GitHub 仓库中找到:https://github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras/tree/master/chapter8-vae。

图 8.1.7:作为潜在向量均值函数生成的数字(VAE MLP)。为了便于解释,均值的值范围与图 8.1.6 类似。
使用 CNNs 进行 VAE
在原始论文 Auto-encoding Variational Bayes [1] 中,VAE 网络是使用 MLP 实现的,这与我们在前一节中介绍的类似。在本节中,我们将展示使用 CNN 会显著提高生成的数字质量,并且将参数数量大幅减少至 134,165。
Listing 8.1.3 展示了编码器、解码器和 VAE 网络。该代码也已贡献到官方的 Keras GitHub 仓库。为了简洁起见,类似 MLP 的一些代码行不再显示。编码器由两层 CNN 和两层 MLP 组成,用于生成潜在代码。编码器输出结构类似于前一节中看到的 MLP 实现。解码器由一层 MLP 和三层反向卷积 CNN 组成。图 8.1.8 到 8.1.10 展示了编码器、解码器和 VAE 模型。对于 VAE CNN,RMSprop 会比 Adam 得到更低的损失。
VAE CNN 的 Keras 代码已包含预训练权重。要进行测试,我们需要运行:
$ python3 vae-cnn-mnist-8.1.2.py --weights=vae_cnn_mnist.h5
Listing 8.1.3, vae-cnn-mnist-8.1.2.py 显示了使用 CNN 层的 VAE Keras 代码:
# network parameters
input_shape = (image_size, image_size, 1)
batch_size = 128
kernel_size = 3
filters = 16
latent_dim = 2
epochs = 30
# VAE mode = 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')
encoder.summary()
plot_model(encoder, to_file='vae_cnn_encoder.png', show_shapes=True)
# 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')
decoder.summary()
plot_model(decoder, to_file='vae_cnn_decoder.png', show_shapes=True)
# instantiate vae model
outputs = decoder(encoder(inputs)[2])
vae = Model(inputs, outputs, name='vae')

图 8.1.8:VAE CNN 的编码器

图 8.1.9:VAE CNN 的解码器

图 8.1.10:使用 CNNs 的 VAE 模型

图 8.1.11:测试数据集的潜在向量均值(VAE CNN)。颜色条显示了与 z 对应的 MNIST 数字。彩色图像可在书籍的 GitHub 仓库中找到:https://github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras/tree/master/chapter8-vae。
前述图像展示了使用 CNN 实现的 VAE 在训练 30 个 epoch 后的连续潜在空间。每个数字分配的区域可能不同,但分布大致相同。下图展示了生成模型的输出。定性上,与 图 8.1.7 中的 MLP 实现相比,模糊的数字较少:

图 8.1.12:作为潜在向量均值函数生成的数字(VAE CNN)。为了便于解释,均值的值范围与图 8.1.11 类似。
条件 VAE (CVAE)
条件变分自编码器 [2] 类似于 CGAN 的思想。在 MNIST 数据集的背景下,如果潜在空间是随机采样的,VAE 无法控制生成哪个数字。CVAE 通过包含一个条件(数字的独热标签)来解决这个问题,从而生成特定的数字。这个条件被强加到编码器和解码器的输入上。
从形式上讲,VAE 的核心方程 方程 8.1.10 被修改,以包括条件 c:

(方程 8.2.1)
与 VAE 类似,方程 8.2.1 表示,如果我们想要最大化基于 c 的输出,

,那么两个损失项必须最小化:
-
在给定潜在向量和条件的情况下,解码器的重建损失。
-
编码器在给定潜在向量和条件的情况下与先验分布之间的 KL 损失。与 VAE 类似,我们通常选择
![条件变分自编码器(CVAE)]()
.
列表 8.2.1,cvae-cnn-mnist-8.2.1.py 展示了使用 CNN 层的 CVAE 的 Keras 代码。在突出显示的代码中展示了为支持 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')
encoder.summary()
plot_model(encoder, to_file='cvae_cnn_encoder.png', show_shapes=True)
# build decoder model
latent_inputs = Input(shape=(latent_dim,), name='z_sampling')
x = keras.layers.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')
decoder.summary()
plot_model(decoder, to_file='cvae_cnn_decoder.png', show_shapes=True)
# instantiate vae model
outputs = decoder([encoder([inputs, y_labels])[2], y_labels])
cvae = Model([inputs, y_labels], outputs, name='cvae')
if __name__ == '__main__':
parser = argparse.ArgumentParser()
help_ = "Load h5 model trained weights"
parser.add_argument("-w", "--weights", help=help_)
help_ = "Use mse loss instead of binary cross entropy (default)"
parser.add_argument("-m", "--mse", help=help_, action='store_true')
help_ = "Specify a specific digit to generate"
parser.add_argument("-d", "--digit", type=int, help=help_)
help_ = "Beta in Beta-CVAE. Beta > 1\. Default is 1.0 (CVAE)"
parser.add_argument("-b", "--beta", type=float, help=help_)
args = parser.parse_args()
models = (encoder, decoder)
data = (x_test, y_test)
if args.beta is None or args.beta < 1.0:
beta = 1.0
print("CVAE")
model_name = "cvae_cnn_mnist"
else:
beta = args.beta
print("Beta-CVAE with beta=", beta)
model_name = "beta-cvae_cnn_mnist"
# VAE loss = mse_loss or xent_loss + kl_loss
if args.mse:
reconstruction_loss = mse(K.flatten(inputs), K.flatten(outputs))
else:
reconstruction_loss = binary_crossentropy(K.flatten(inputs),
K.flatten(outputs))
reconstruction_loss *= image_size * image_size
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_loss = K.mean(reconstruction_loss + kl_loss)
cvae.add_loss(cvae_loss)
cvae.compile(optimizer='rmsprop')
cvae.summary()
plot_model(cvae, to_file='cvae_cnn.png', show_shapes=True)
if args.weights:
cvae = cvae.load_weights(args.weights)
else:
# train the autoencoder
cvae.fit([x_train, to_categorical(y_train)],
epochs=epochs,
batch_size=batch_size,
validation_data=([x_test, to_categorical(y_test)], None))
cvae.save_weights(model_name + '.h5')
if args.digit in range(0, num_labels):
digit = np.array([args.digit])
else:
digit = np.random.randint(0, num_labels, 1)
print("CVAE for digit %d" % digit)
y_label = np.eye(num_labels)[digit]
plot_results(models,
data,
y_label=y_label,
batch_size=batch_size,
model_name=model_name)

图 8.2.1:CVAE CNN 中的编码器。输入现在是 VAE 输入和条件标签的拼接。

图 8.2.2:CVAE CNN 中的解码器。输入现在是 z 采样和条件标签的拼接。

图 8.2.3:使用 CNN 的 CVAE 模型。输入现在是 VAE 输入和条件标签的拼接。
实现 CVAE 需要对 VAE 的代码进行一些修改。对于 CVAE,使用了 VAE CNN 的实现。列表 8.2.1 突出显示了对 MNIST 数字的 VAE 原始代码所做的修改。编码器输入现在是原始输入图像和其独热标签的拼接。解码器输入现在是潜在空间采样和它应该生成的图像的独热标签的组合。总参数数量为 174,437。与代码相关的部分

-VAE 将在本章的下一节中讨论。
损失函数没有变化。然而,在训练、测试和绘制结果时提供了独热标签。图 8.2.1 到 8.2.3 向我们展示了编码器、解码器和 CVAE 模型。指明了条件标签作为独热向量的作用。

图 8.2.4:测试数据集的潜在向量均值(CVAE CNN)。颜色条显示了与 z 相关的对应 MNIST 数字。彩色图像可以在书籍的 GitHub 仓库找到: https://github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras/tree/master/chapter8-vae。

图 8.2.5:根据潜在向量均值和独热标签(CVAE CNN)生成的数字 0 到 5。为了便于理解,均值的取值范围与图 8.2.4 相似。

图 8.2.6:根据潜在向量均值和独热标签(CVAE CNN)生成的数字 6 到 9。为了便于理解,均值的取值范围与图 8.2.4 相似。
在图 8.2.4中,展示了经过 30 个 epoch 后每个标签的均值分布。与前面章节中的图 8.1.6和8.1.11不同,每个标签并没有集中在某个区域,而是分布在整个图中。这是预期中的结果,因为在潜在空间中的每次采样应当生成一个特定的数字。导航潜在空间会改变该特定数字的属性。例如,如果指定的数字是 0,那么导航潜在空间仍会产生 0,但其属性(如倾斜角度、粗细以及其他书写风格特征)会有所不同。
这些变化在图 8.2.5和8.2.6中表现得更加清晰。为了便于比较,潜在向量的取值范围与图 8.2.4中的一致。使用预训练的权重,可以通过执行以下命令生成一个数字(例如,0):
$ python3 cvae-cnn-mnist-8.2.1.py --weights=cvae_cnn_mnist.h5 --digit=0
在图 8.2.5和8.2.6中,可以注意到每个数字的宽度和圆度(如果适用)随着z[0]从左到右的变化而变化。同时,随着z[1]从上到下的变化,每个数字的倾斜角度和圆度(如果适用)也会发生变化。当我们离分布的中心越来越远时,数字的图像开始退化。这是预期中的结果,因为潜在空间是一个圆形。
其他显著的属性变化可能是针对特定数字的。例如,数字 1 的水平笔画(臂)会出现在左上方区域。数字 7 的水平笔画(横杆)则只出现在右侧区域。
-VAE:具有解耦潜在表示的 VAE
在第六章中,讨论了解耦表示 GAN的概念以及潜在编码的解耦表示的重要性。我们可以回顾一下,解耦表示指的是单一潜在单元对单个生成因子的变化敏感,同时对其他因子的变化保持相对不变[3]。改变潜在编码会导致生成输出中某一属性的变化,而其他属性保持不变。
在同一章节中,InfoGANs [4]向我们展示了在 MNIST 数据集中,可以控制生成的数字和倾斜和书写风格的厚度。观察前一节中的结果,可以注意到 VAE 在某种程度上本质上正在分离潜在向量维度。例如,查看图 8.2.6中的数字 8,导航z[1]从顶部到底部减少宽度和圆度,同时将数字顺时针旋转。增加z[0]从左到右也减少了宽度和圆度,同时将数字逆时针旋转。换句话说,z[1]控制顺时针旋转,z[0]影响逆时针旋转,两者都改变宽度和圆度。
在本节中,我们将展示 VAE 损失函数中的简单修改如何进一步迫使潜在代码分离。该修改是正常数加权,

,作为 KL 损失的正则化器:

(方程 8.3.1)
这种 VAE 的变体被称为

-VAE [5]。隐式效果

是更紧密的标准偏差。换句话说,

强制后验分布中的潜在代码,

独立。
实施是很直接的

-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 with

。其他都相同。然而,确定

需要一些试验和错误。必须在重建误差和潜在代码独立性的正则化之间进行仔细平衡。在约

. 当值为

,

-VAE 被迫仅学习一个分离的表示,同时静音另一个潜在维度:

图 8.3.1:测试数据集的潜在向量均值(

-VAE with

)彩色图像可以在本书的 GitHub 仓库中找到: https://github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras/tree/master/chapter8-vae。
图 8.3.1 和 图 8.3.2 展示了

-VAE 与

和

。随着

,与 CVAE 相比,该分布的标准差较小。随着

,只有学习到的潜在编码。分布实际上被缩小到 1D,第一潜在编码 z[0] 被编码器和解码器忽略:

图 8.3.2:测试数据集的潜在向量均值(

-VAE 与

)彩色图像可以在本书的 GitHub 仓库中找到: https://github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras/tree/master/chapter8-vae。
这些观察结果在 图 8.3.3 中有所体现。

-VAE 与

有两个潜在编码是相对独立的。z[0] 决定书写风格的倾斜角度。同时,z[1] 指定数字的宽度和圆润度(如果适用)。

-VAE 与

,z[0] 被静音。增加 z[0] 不会显著改变数字。z[1] 决定了书写风格的倾斜角度和宽度。

图 8.3.3:数字 0 到 3 作为潜在向量均值和 one-hot 标签的函数生成(

-VAE

)为了便于理解,均值的值范围与图 8.3.1 类似。
Keras 代码

-VAE 具有预训练权重。要测试

-VAE 与

生成数字 0,我们需要运行:
$ python3 cvae-cnn-mnist-8.2.1.py --beta=7 --weights=beta-cvae_cnn_mnist.h5 --digit=0
结论
在本章中,我们介绍了变分自编码器(VAE)的基本原理。正如我们在 VAE 原理中所学到的,它们与 GAN 在某些方面相似,因为它们都试图从潜在空间生成合成输出。然而,可以注意到,相较于 GAN,VAE 网络更加简单且易于训练。条件 VAE 和

-VAE 在概念上与条件 GAN 和解缠表示 GAN 相似。
VAE 具有解缠潜在向量的内在机制。因此,构建一个

-VAE 非常直观。然而,我们应该注意到,可解释的和解缠的编码在构建智能代理时是重要的。
在下一章中,我们将重点介绍强化学习。在没有任何先验数据的情况下,代理通过与环境的互动来学习。我们将讨论如何根据正确的行动奖励代理,并对错误的行动进行惩罚。
参考文献
-
Diederik P. Kingma 和 Max Welling。自编码变分贝叶斯。arXiv 预印本 arXiv:1312.6114,2013(
arxiv.org/pdf/1312.6114.pdf)。 -
Kihyuk Sohn、Honglak Lee 和 Xinchen Yan。使用深度条件生成模型学习结构化输出表示。神经信息处理系统进展,2015(
papers.nips.cc/paper/5775-learning-structured-output-representation-using-deep-conditional-generative-models.pdf)。 -
Yoshua Bengio、Aaron Courville 和 Pascal Vincent。表示学习:综述与新视角。IEEE 模式分析与机器智能事务 35.8,2013:1798-1828(
arxiv.org/pdf/1206.5538.pdf)。 -
Xi Chen 等人。Infogan: 通过信息最大化生成对抗网络进行可解释的表示学习。神经信息处理系统进展,2016(
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 和 A. Lerchner。
![参考文献]()
-VAE: 使用受限变分框架学习基础视觉概念。ICLR,2017(
openreview.net/pdf?id=Sy2fzU9gl)。 -
Carl Doersch。变分自编码器教程。arXiv 预印本 arXiv:1606.05908,2016(
arxiv.org/pdf/1606.05908.pdf)。 -
Luc Devroye. 基于样本的非均匀随机变量生成. 第 18 届冬季仿真会议论文集. ACM, 1986(
www.eirene.de/Devroye.pdf).
第九章:第 9 章 深度强化学习
强化学习 (RL) 是一个框架,由代理用于决策。代理不一定是软件实体,比如在视频游戏中。相反,它可以体现在硬件中,如机器人或自动驾驶汽车。体现代理可能是充分理解和利用强化学习的最佳方式,因为物理实体与现实世界互动并接收响应。
代理位于一个环境中。环境具有部分或完全可观察的状态。代理有一组行动,可以用来与其环境交互。行动的结果会将环境转换到一个新的状态。执行行动后将收到相应的标量奖励。代理的目标是通过学习一个策略,在给定状态下决定采取哪个行动来最大化累积的未来奖励。
强化学习与人类心理学有很强的相似性。人类通过经验世界来学习。错误的行动会导致一定形式的惩罚,并应在未来避免,而正确的行动则受到奖励并应该鼓励。这种与人类心理学的强烈相似性已经使许多研究人员相信,强化学习可以引领我们走向人工智能 (AI)。
强化学习已经存在几十年了。然而,超越简单世界模型,RL 在扩展方面一直存在困难。这就是深度学习 (DL) 发挥作用的地方。它解决了这个可扩展性问题,开启了深度强化学习 (DRL) 的时代,这也是本章重点讨论的内容。DRL 中的一个显著例子是 DeepMind 在能够超越不同视频游戏中最佳人类表现的代理工作。在本章中,我们讨论了 RL 和 DRL 两者。
总之,本章的目标是提供:
-
RL 的原理
-
强化学习技术 Q 学习
-
包括深度 Q 网络 (DQN) 和 双 Q 学习 (DDQN) 在内的高级主题
-
在 Python 上实施 RL 和在 Keras 中实现 DRL 的指南
强化学习(RL)的原理
图 9.1.1 显示了用于描述 RL 的感知-行动-学习循环。环境是一个放在地板上的苏打罐。代理是一个移动机器人,其目标是拾取苏打罐。它观察周围的环境,并通过机载摄像头跟踪苏打罐的位置。观察结果以一种状态的形式总结,机器人将使用该状态来决定采取哪些行动。它采取的行动可能涉及低级控制,如每个轮子的旋转角度/速度,每个机械臂关节的旋转角度/速度,以及夹持器是否打开或关闭。
或者,动作可能是高层次的控制动作,例如让机器人前进/后退,按照某个角度转向,抓取/释放。任何使抓手远离可乐的动作都会得到负奖励。任何缩小抓手位置与可乐之间距离的动作都会得到正奖励。当机器人臂成功拾起可乐罐时,会得到一个较大的正奖励。RL 的目标是学习最优策略,帮助机器人根据状态决定采取何种动作,以最大化累积的折扣奖励:

图 9.1.1:强化学习中的感知-行动-学习循环
正式来说,RL 问题可以描述为马尔可夫决策过程(MDP)。为简化起见,我们假设是一个确定性环境,其中在给定状态下的某个动作将始终导致已知的下一个状态和奖励。在本章稍后的部分,我们将探讨如何考虑随机性。在时间步t时:
-
环境处于状态s[t],来自状态空间
,状态可能是离散的或连续的。起始状态是s[0],而终止状态是s[t]。 -
代理根据策略从动作空间中采取动作a[t],
。
可能是离散的或连续的。 -
环境使用状态转移动态
转移到新的状态s[t+1]。下一个状态只依赖于当前的状态和动作。
对代理不可知。 -
代理使用奖励函数 r[t+1] = R(s[t],a[t])接收一个标量奖励,其中
。奖励仅依赖于当前的状态和动作。R对代理不可知。 -
未来的奖励由
折扣,其中
,k是未来的时间步。 -
Horizon,H,是完成一次从s[0]到s[t]的回合所需的时间步数T。
环境可以是完全可观察的,也可以是部分可观察的。后者通常被称为部分可观察的 MDP或POMDP。大多数情况下,完全观察环境是不现实的。为了提高可观察性,除了当前观察之外,过去的观察也会被纳入考虑。状态包括关于环境的足够观察信息,供策略决定采取何种行动。在图 9.1.1中,这可以是机器人抓手与苏打罐相对的 3D 位置,由机器人摄像头估算得出。
每次环境过渡到新状态时,智能体会收到一个标量奖励,r[t+1]。在图 9.1.1中,每当机器人靠近苏打罐时,奖励为+1;每当远离时,奖励为-1;当机器人关闭抓手并成功拿起苏打罐时,奖励为+100。智能体的目标是学习最优策略
,以最大化所有状态的回报:
(方程 9.1.1)
回报被定义为折扣累积奖励,
。从方程 9.1.1可以观察到,未来的奖励相较于即时奖励权重较低,因为通常情况下
,其中
。在极端情况下,当
,只有即时奖励才重要;当
时,未来的奖励和即时奖励有相同的权重。
回报可以被解释为通过跟随某一任意策略,某一状态的价值,
:
(方程 9.1.2)
换个角度来看强化学习(RL)问题,智能体的目标是学习最优策略,以最大化所有状态s的
:
(方程 9.1.3)
最优策略的价值函数简单地表示为V**。在图 9.1.1*中,最优策略是产生最短行动序列的策略,这个序列使得机器人越来越接近苏打罐,直到将其抓取。状态距离目标状态越近,其价值越高。
导致目标(或终止状态)事件序列可以被建模为策略的轨迹或展开:
轨迹 = (s0a0r1s1,s1a1r2s2,...,sT-1aT-1r T s[t]) (方程 9.1.4)
如果 MDP 是有终止的,当代理到达终止状态s[T']时,状态会被重置为s[0]。如果T是有限的,我们就有一个有限的视野。否则,视野是无限的。在图 9.1.1中,如果 MDP 是有终止的,那么在收集完可乐罐后,机器人可能会寻找另一个可乐罐来捡起来,RL 问题就会重复。
Q 值
一个重要的问题是,如果 RL 问题是找到
,那么代理通过与环境交互是如何学习的?方程 9.1.3并没有明确指出要尝试的动作和计算回报的下一个状态。在 RL 中,我们发现通过使用Q值来学习
会更容易:
(方程 9.2.1)
其中:
(方程 9.2.2)
换句话说,方程 9.2.1不是寻找最大化所有状态的值的策略,而是寻找最大化所有状态的质量(Q)值的动作。在找到Q值函数之后,V值和因此得出的
分别由方程 9.2.2和9.1.3确定。
如果对于每个动作,都能观察到奖励和下一个状态,我们可以制定以下迭代或试错算法来学习Q值:
(方程 9.2.3)
为了简化符号,s ' 和 a ' 分别表示下一个状态和动作。方程 9.2.3被称为贝尔曼方程,它是 Q 学习算法的核心。Q 学习尝试将回报或价值的一级展开(方程 9.1.2)近似为当前状态和动作的函数。
从对环境动态的零知识开始,代理尝试一个动作a,以奖励r和下一个状态s '的形式观察发生了什么。
选择下一个合乎逻辑的动作,从而为下一个状态提供最大的Q值。所有在方程* 9.2.3中已知的项,当前状态-动作对的Q值就被更新。通过迭代地进行更新,最终会学习到Q值函数。
Q 学习是一种脱离策略的 RL 算法。它通过不直接从该策略中采样经验来学习改进策略。换句话说,Q值是独立于代理使用的底层策略进行学习的。当Q值函数收敛时,只有通过方程 9.2.1才能确定最优策略。
在给出 Q-Learning 的使用示例之前,我们需要注意,智能体必须不断探索其环境,同时逐渐利用到目前为止所学到的内容。这是强化学习中的一个问题——如何在 探索 和 利用 之间找到正确的平衡。通常,在学习初期,动作是随机的(探索)。随着学习的进行,智能体利用 Q 值(利用)。例如,刚开始时,90% 的动作是随机的,10% 是基于 Q 值函数的,而在每一轮结束时,这个比例逐渐减小。最终,动作是 10% 随机的,90% 基于 Q 值函数。
Q-Learning 示例
为了说明 Q-Learning 算法,我们需要考虑一个简单的确定性环境,如下图所示。环境中有六个状态。允许转移的奖励如图所示。只有在两种情况下,奖励才为非零。转移到 目标 (G) 状态时奖励 +100,而进入 洞 (H) 状态时奖励为 -100。这两个状态是终止状态,构成从 起始 状态结束一个回合的条件:

图 9.3.1:简单确定性世界中的奖励
为了正式化每个状态的标识,我们需要使用(行,列)标识符,如下图所示。由于智能体尚未了解其环境,因此下图中显示的 Q-表的初始值为零。在此示例中,折扣因子,
。回顾一下,在当前 Q 值的估算中,折扣因子决定了未来 Q 值的权重,权重是根据步数的函数,
。在 方程式 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.5:假设智能体选择的动作是向右移动,状态 (1, 1) 的 Q 值更新如图所示。
假设下一步随机选择的动作是向下移动。图 9.3.4 显示向下移动时,状态 (0, 1) 的 Q 值没有变化。在 图 9.3.5 中,智能体的第三个随机动作是向右移动。它遇到了 H 状态并收到了 -100 的惩罚。这次更新非零。状态 (1, 1) 向右移动的 Q 值为 -100。一次试验刚刚结束,智能体返回到 Start 状态。

图 9.3.6:假设智能体选择的动作是连续两步向右移动,状态 (0, 1) 的 Q 值更新如图所示。
假设智能体仍处于探索模式,如 图 9.3.6 所示。它在第二次试验的第一步选择了向右移动。正如预期的那样,更新值为 0。然而,它选择的第二个随机动作仍然是向右移动。智能体达到了 G 状态并获得了 +100 的奖励。状态 (0, 1) 向右移动的 Q 值变为 100。第二次试验完成,智能体返回到 Start 状态。

图 9.3.7:假设智能体选择的动作是向右移动,状态 (0, 0) 的 Q 值更新如图所示。

图 9.3.8:在此情况下,智能体的策略决定利用 Q 表来确定状态 (0, 0) 和 (0, 1) 的动作。Q 表建议两个状态都向右移动。
在第三次试验开始时,智能体随机选择的动作是向右移动。状态 (0, 0) 的 Q 值现在被更新为非零值,因为下一状态的可能动作的最大 Q 值为 100。图 9.3.7 展示了相关的计算过程。下一状态 (0, 1) 的 Q 值回传到之前的状态 (0, 0),这就像是对帮助找到 G 状态的早期状态进行的奖励。
在 Q 表中的进展是显著的。事实上,在下一次试验中,如果出于某种原因,策略决定利用 Q 表而不是随机探索环境,那么根据 图 9.3.8 中的计算,第一步将是向右移动。在 Q 表的第一行中,导致最大 Q 值的动作是向右移动。对于下一状态 (0, 1),Q 表的第二行建议下一个动作仍然是向右移动。智能体成功地达到了目标。策略引导智能体采取正确的动作集来实现其目标。
如果 Q-Learning 算法继续无限运行,Q-表格将会收敛。收敛的假设条件是 RL 问题必须是确定性 MDP,奖励有界,并且所有状态都被无限次访问。
Python 中的 Q-Learning
环境和上一节讨论的 Q-Learning 可以在 Python 中实现。由于策略只是一个简单的表格,因此目前不需要 Keras。列表 9.3.1 显示了 q-learning-9.3.1.py,这是使用 QWorld 类实现简单确定性世界(环境、智能体、动作和 Q-表格算法)的代码。为了简洁起见,处理用户界面的函数未显示。
在此示例中,环境动态通过 self.transition_table 表示。在每次执行动作时,self.transition_table 决定下一个状态。执行动作的奖励存储在 self.reward_table 中。每次执行动作时,这两个表格都会被查询。Q-Learning 算法通过 update_q_table() 函数实现。每当智能体需要决定执行哪一个动作时,它会调用 act() 函数。动作可以是随机选择的,也可以是通过 Q-表格的策略决定的。选择随机动作的概率存储在 self.epsilon 变量中,并通过 update_epsilon() 函数使用固定的 epsilon_decay 进行更新。
在执行 列表 9.3.1 中的代码之前,我们需要运行:
$ sudo pip3 install termcolor
安装 termcolor 包。该包有助于在终端中可视化文本输出。
注意
完整代码可以在 GitHub 上找到:github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras。
列表 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):
# 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
# start of episode
def reset(self):
self.state = 0
return self.state
# agent wins when the goal is reached
def is_in_win_state(self):
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
# execute the action on the environment
def step(self, action):
# 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
# determine the next action
def act(self):
# 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
return np.argmax(self.q_table[self.state])
# Q-Learning - update the Q Table using Q(s, a)
def update_q_table(self, state, action, reward, next_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
# UI to dump Q Table contents
def print_q_table(self):
print("Q-Table (Epsilon: %0.2f)" % self.epsilon)
print(self.q_table)
# update Exploration-Exploitation mix
def update_epsilon(self):
if self.epsilon > self.epsilon_min:
self.epsilon *= self.epsilon_decay
列表 9.3.2,q-learning-9.3.1.py。主要的 Q-Learning 循环。每次状态、动作、奖励和下一个状态的迭代都会更新智能体的 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
print(scores)
q_world.print_q_table()
感知-动作-学习循环在 列表 9.3.2 中有所说明。在每一轮中,环境重置为 开始 状态。选择并应用要执行的动作到环境中。观察到奖励和下一个状态,并用于更新 Q-表格。当达到 目标 或 洞 状态时,回合结束(done = True)。对于此示例,Q-Learning 执行 100 轮或 10 次胜利,以先达到的为准。由于 self.epsilon 变量在每一轮中减少,智能体开始倾向于利用 Q-表格来确定给定状态下的执行动作。为了查看 Q-Learning 模拟,我们只需要运行:
$ python3 q-learning-9.3.1.py

图 9.3.9:显示智能体经过 2000 次胜利后的 Q-表格截图
上图显示了在运行时 maxwins = 2000(达到 2000x 目标 状态)和 delay = 0(仅查看最终 Q-表格)的截图:
$ python3 q-learning-9.3.1.py --train
Q-Table 已经收敛,并显示了在给定状态下代理可以采取的逻辑行动。例如,在第一行或状态 (0, 0) 中,策略建议向右移动。第二行的状态 (0, 1) 也是如此。第二个动作到达 目标 状态。scores 变量转储显示,随着代理从策略中获得正确的动作,采取的最小步骤数逐渐减少。
从 图 9.3.9 中,我们可以通过 方程式 9.2.2 来计算每个状态的值,
。例如,对于状态 (0, 0),V**(s*) = max(81.0,72.9,90.0,81.0) = 90.0。以下图显示了每个状态的值:

图 9.3.10:来自图 9.3.9 和方程式 9.2.2 的每个状态的值
非确定性环境
如果环境是非确定性的,那么奖励和动作都是概率性的。新系统是一个随机的 MDP。为了反映非确定性奖励,新的值函数为:
(方程式 9.4.1)
贝尔曼方程被修改为:
(方程式 9.4.2)
时间差学习
Q-Learning 是一种更广泛的 时间差学习 或 TD-Learning 的特例
。更具体来说,它是一步 TD-Learning TD(0) 的特例:
(方程式 9.5.1)
在方程式中
是学习率。我们应该注意,当
时,方程式 9.5.1 类似于贝尔曼方程。为简便起见,我们将 方程式 9.5.1 称为 Q-Learning 或广义 Q-Learning。
之前,我们将 Q-Learning 称为一种离策略 RL 算法,因为它在不直接使用正在优化的策略的情况下学习 Q 值函数。一种 在策略 的一步 TD-Learning 算法的示例是 SARSA,类似于 方程式 9.5.1:
(方程式 9.5.2)
主要的区别在于使用正在优化的策略来确定 a**'。术语 s、a、r、s**' 和 a**'(因此称为 SARSA)必须已知,以便在每次迭代时更新 Q 值函数。Q-Learning 和 SARSA 都在 Q 值迭代中使用现有的估计,这一过程称为 自举法。在自举法中,我们通过奖励和随后的 Q 值估计来更新当前的 Q 值估计。
OpenAI gym 上的 Q-Learning
在展示另一个示例之前,似乎需要一个合适的 RL 模拟环境。否则,我们只能在非常简单的问题上运行 RL 模拟,就像前面的示例一样。幸运的是,OpenAI 创建了 Gym,gym.openai.com。
Gym 是一个开发和比较强化学习算法的工具包。它可以与大多数深度学习库(包括 Keras)一起使用。可以通过运行以下命令来安装 Gym:
$ sudo pip3 install gym
Gym 提供了多个可以用来测试强化学习算法的环境,比如玩具文本、经典控制、算法、Atari 和 2D/3D 机器人等。例如,FrozenLake-v0 (图 9.5.1) 是一个玩具文本环境,类似于 Python 示例中的简单确定性世界。FrozenLake-v0 有 12 个状态。标记为 S 的是起始状态,F 是冰冻的湖面,安全的区域,H 是应该避免的洞穴状态,G 是目标状态,飞盘的位置。转移到目标状态的奖励为 +1,其他所有状态的奖励为 0。
在 FrozenLake-v0 中,还有四个可用的动作(左、下、右、上),称为动作空间。然而,与之前的简单确定性世界不同,实际的移动方向仅部分依赖于所选的动作。FrozenLake-v0 环境有两种变化:滑动和非滑动。如预期,滑动模式更具挑战性。

图 9.5.1:OpenAI Gym 中的冰冻湖环境
在 FrozenLake-v0 上应用一个动作会返回观察(相当于下一个状态)、奖励、完成(回合是否结束)以及调试信息的字典。环境的可观察属性,称为观察空间,通过返回的观察对象来捕捉。
广义 Q 学习可以应用于 FrozenLake-v0 环境。表 9.5.1 显示了滑动和非滑动环境中性能的改进。衡量策略性能的一种方法是执行的回合中,达到目标状态的比例。这个比例越高,效果越好。从纯探索(随机动作)约 1.5% 的基线开始,策略可以在非滑动环境中达到 ~76% 的目标状态,而在滑动环境中则为 ~71%。如预期,控制滑动环境更具挑战性。
由于只需要一个 Q 表,代码仍然可以在 Python 和 NumPy 中实现。列表 9.5.1 展示了 QAgent 类的实现,而 列表 9.5.2 展示了智能体的感知-行动-学习循环。除了使用 OpenAI Gym 中的 FrozenLake-v0 环境外,最重要的变化是实现了由 方程式 9.5.1 所定义的广义 Q 学习,该实现位于 update_q_table() 函数中。
qagent 对象可以在滑动或非滑动模式下操作。该代理经过 40,000 次迭代训练。训练后,代理可以利用 Q-表来选择执行任何策略下的动作,如 表 9.5.1 的测试模式所示。使用学习到的策略后,性能大幅提升,正如 表 9.5.1 中所示。通过使用 gym,构建环境的许多代码都被简化了。
这将帮助我们专注于构建一个有效的强化学习(RL)算法。要使代码以慢动作或每个动作延迟 1 秒运行:
$ python3 q-frozenlake-9.5.1.py -d -t=1
| 模式 | 运行 | 目标近似百分比 |
|---|---|---|
| 训练非滑动 |
python3 q-frozenlake-9.5.1.py
| 26.0 |
|---|
| 测试非滑动 |
python3 q-frozenlake-9.5.1.py -d
| 76.0 |
|---|
| 纯随机非滑动动作 |
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.0 |
|---|
| 纯随机滑动 |
python3 q-frozenlake-9.5.1.py -s -e
| 1.5 |
|---|
表 9.5.1:在
FrozenLake-v0环境上使用学习率 = 0.5 的广义 Q-Learning 的基准和性能
列表 9.5.1,q-frozenlake-9.5.1.py 显示了在 FrozenLake-v0 环境中实现 Q-Learning:
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,
decay=0.99):
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_decay = decay
self.epsilon_min = 0.1
# 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
# determine the next action
# if random, choose from random action space
# else use the Q Table
def act(self, state, is_explore=False):
# 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
return np.argmax(self.q_table[state])
# TD(0) learning (generalized Q-Learning) with learning rate
def update_q_table(self, state, action, reward, 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
# dump Q Table
def print_q_table(self):
print(self.q_table)
print("Epsilon : ", self.epsilon)
# save trained Q Table
def save_q_table(self):
np.save(self.filename, self.q_table)
# load trained Q Table
def load_q_table(self):
self.q_table = np.load(self.filename)
# adjust epsilon
def update_epsilon(self):
if self.epsilon > self.epsilon_min:
self.epsilon *= self.epsilon_decay
列表 9.5.2,q-frozenlake-9.5.1.py。FrozenLake-v0 环境的主要 Q-Learning 循环:
# 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)
print("-------%0.2f%% Goals in %d Episodes---------"
% (percent_wins, episode))
if done:
time.sleep(5 * delay)
else:
time.sleep(delay)
深度 Q-网络 (DQN)
在小型离散环境中,使用 Q-表来实现 Q-Learning 是可行的。然而,当环境有大量状态或像大多数情况那样是连续的时,Q-表就不再可行或实际。例如,如果我们观察的是由四个连续变量组成的状态,那么表的大小是无限的。即使我们尝试将这四个变量每个离散化为 1000 个值,表中行的总数将是令人吃惊的 1000⁴ = 1e¹²。即使在训练后,表仍然是稀疏的——表中的大多数单元格都是零。
解决这个问题的方法被称为 DQN [2],它使用深度神经网络来逼近 Q-表。如 图 9.6.1 所示。构建 Q-网络有两种方法:
-
输入是状态-动作对,预测是 Q 值
-
输入是状态,预测是每个动作的Q值
第一个选项不是最优的,因为网络将根据动作的数量被调用若干次。第二个是首选方法。Q-网络仅被调用一次。
最理想的动作是拥有最大 Q 值的动作:

图 9.6.1:深度 Q-网络
训练 Q-网络所需的数据来自代理的经验:
。每个训练样本是一个经验单元!深度 Q-网络 (DQN)。在给定的状态下,在时间步 t,s = s[t],动作 a = a[t] 是使用 Q-Learning 算法确定的,类似于前一节所述:
(方程 9.6.1)
为了简化符号,我们省略了下标和粗体字母的使用。需要注意的是,Q(s,a) 是 Q 网络。严格来说,它是 Q(a|s),因为动作被移到预测中,如 图 9.6.1 右侧所示。具有最高 Q 值的动作是应用于环境中的动作,以获得奖励 r = r [t+1],下一个状态 s ' = s[t+1],以及一个布尔值 done,表示下一个状态是否为终止状态。从 方程 9.5.1 中的广义 Q 学习,可以通过应用所选动作确定 MSE 损失函数:
(方程 9.6.2)
其中所有项都来自前面的 Q 学习讨论,Q(a|s) → Q(s,a)。项
。换句话说,使用 Q 网络预测给定下一个状态下每个动作的 Q 值,并从中选择最大值。注意,在终止状态 s',
。
算法 9.6.1,DQN 算法:
要求:将回放记忆 D 初始化至容量 N
要求:用随机权重初始化动作值函数 Q 
要求:初始化目标动作值函数 Q[target],并赋予权重 
要求:探索率,
和折扣因子,
-
forepisode = 1, …,Mdo: -
给定初始状态 s
-
forstep = 1,…, Tdo: -
选择动作
![深度 Q 网络 (DQN)]()
-
执行动作 a,观察奖励 r 和下一个状态 s'
-
将转移 (s, a, r, s**') 存储在 D 中
-
更新状态,s = s**'
-
//经验回放
-
从 D 中采样一个小批量的经验(s[j], a[j], r[j+1], s[j+1])
-
![深度 Q 网络 (DQN)]()
-
对
执行梯度下降步骤,更新参数 ![深度 Q 网络 (DQN)]()
-
// 定期更新目标网络
-
每 C 步 Q[target] = Q,即设置为
![深度 Q 网络 (DQN)]()
-
结束
然而,事实证明,训练 Q 网络是不稳定的。导致不稳定的原因有两个问题:
-
样本之间存在高度相关性
-
非平稳目标
高相关性是由于采样经验的顺序性。DQN 通过创建经验缓冲区解决了这个问题。训练数据从该缓冲区中随机采样。此过程称为 经验回放。
非平稳目标问题来源于每次训练的小批量后,目标网络 Q(s ',a ') 的更新。目标网络的微小变化可能会对策略、数据分布以及当前 Q 值与目标 Q 值之间的关联产生显著影响。通过在 C 次训练步骤内冻结目标网络的权重来解决这个问题。换句话说,创建了两个相同的 Q-网络。每 C 次训练步骤,目标 Q-网络的参数都会从正在训练的 Q-网络复制过来。
DQN 算法在 算法 9.6.1 中做了总结。
DQN 在 Keras 上
为了说明 DQN,使用了 OpenAI Gym 的 CartPole-v0 环境。CartPole-v0 是一个杆子平衡问题,目标是保持杆子不倒。该环境是二维的。动作空间由两个离散动作(左移和右移)组成。然而,状态空间是连续的,包含四个变量:
-
线性位置
-
线性速度
-
旋转角度
-
角速度
CartPole-v0 如 图 9.6.1 所示。
最初,杆子是竖直的。每个保持杆子竖直的时间步都会获得 +1 的奖励。当杆子偏离竖直超过 15 度或偏离中心超过 2.4 单位时,回合结束。如果在 100 次连续试验中,平均奖励为 195.0,则认为 CartPole-v0 问题已解决:

图 9.6.1:CartPole-v0 环境
Listing 9.6.1 展示了 CartPole-v0 的 DQN 实现。DQNAgent 类表示使用 DQN 的代理。创建了两个 Q-网络:
-
算法 9.6.1 中的 Q-网络或 Q
-
目标 Q-网络或 Q[target] 在 算法 9.6.1 中
两个网络都是具有三层隐藏层,每层 256 单元的 MLP。Q-网络在经验回放期间训练,使用 replay() 方法。在每 C = 10 次训练步骤的常规间隔中,Q-网络的参数通过 update_weights() 复制到目标 Q-网络中。这实现了 算法 9.6.1 中的 13 行,Q[target] = Q。每个回合结束后,探索-利用比例通过 update_epsilon() 被降低,以利用已学习的策略。
为了在经验回放期间实现 算法 9.6.1 中的 10 行,replay(),对于每个经验单元 (s[j], a[j], r[j+1], s[j+1]),动作 a[j] 的 Q 值被设置为 Q[max]。所有其他动作的 Q 值保持不变。
下面的代码实现了这一点:
# 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
只有动作 a[j] 的损失值不为零,且等于
,如 算法 9.6.1 中 11 行所示。请注意,经验回放是在 Listing 9.6.2 中的感知-动作-学习循环调用的,在每个回合结束后,假设缓冲区中有足够的数据(即缓冲区大小大于或等于批量大小)。在经验回放期间,随机采样一批经验单元并用于训练 Q-网络。
类似于 Q 表,act() 实现了
-贪婪策略,方程式 9.6.1。经验由 remember() 存储在回放缓冲区中。Q 的计算由 get_target_q_value() 函数完成。在 10 次运行的平均值中,DQN 在 822 次训练中解决了 CartPole-v0。需要注意的是,结果在每次训练运行时可能会有所不同。
列表 9.6.1,dqn-cartpole-9.6.1.py 向我们展示了 Keras 中的 DQN 实现:
from keras.layers import Dense, Input
from keras.models import Model
from keras.optimizers import Adam
from collections import deque
import numpy as np
import random
import argparse
import gym
from gym import wrappers, logger
class DQNAgent():
def __init__(self, state_space, action_space, args, episodes=1000):
self.action_space = action_space
# experience buffer
self.memory = []
# discount rate
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))
# 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
if self.ddqn:
print("----------Double DQN--------")
else:
print("-------------DQN------------")
# Q Network is 256-256-256 MLP
def build_model(self, n_inputs, n_outputs):
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
# save Q Network params to a file
def save_weights(self):
self.q_model.save_weights(self.weights_file)
def update_weights(self):
self.target_q_model.set_weights(self.q_model.get_weights())
# eps-greedy policy
def act(self, state):
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
return np.argmax(q_values[0])
# store experiences in the replay buffer
def remember(self, state, action, reward, next_state, done):
item = (state, action, reward, next_state, done)
self.memory.append(item)
# compute Q_max
# use of target Q Network solves the non-stationarity problem
def get_target_q_value(self, next_state):
# 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
# experience replay addresses the correlation issue between samples
def replay(self, batch_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)
# 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
# decrease the exploration, increase exploitation
def update_epsilon(self):
if self.epsilon > self.epsilon_min:
self.epsilon *= self.epsilon_decay
列表 9.6.2,dqn-cartpole-9.6.1.py。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 % win_trials == 0:
print("Episode %d: Mean survival = %0.2lf in %d episodes" %
(episode, mean_score, win_trials))
双重 Q 学习(DDQN)
在 DQN 中,目标 Q 网络选择并评估每个动作,导致 Q 值的过度估计。为了解决这个问题,DDQN [3] 提议使用 Q 网络来选择动作,并使用目标 Q 网络来评估该动作。
在 算法 9.6.1 总结的 DQN 中,第 10 行中的 Q 值估算为:

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

术语
让 Q 来选择动作。然后这个动作由 Q[target] 进行评估。
在列表 9.6.1 中,DQN 和 DDQN 都得到了实现。具体来说,对于 DDQN,get_target_q_value() 函数在计算 Q 值时所做的修改被突出显示:
# compute Q_max
# use of target Q Network solves the non-stationarity problem
def get_target_q_value(self, next_state):
# 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
为了进行对比,在 10 次运行的平均值中,CartPole-v0 通过 DDQN 在 971 次训练中解决。要使用 DDQN,运行:
$ python3 dqn-cartpole-9.6.1.py -d
结论
在本章中,我们介绍了深度强化学习(DRL)。许多研究人员认为这是通向人工智能最有前途的技术。我们一起回顾了强化学习(RL)的原理。强化学习能够解决许多简单问题,但 Q 表无法扩展到更复杂的实际问题。解决方案是使用深度神经网络来学习 Q 表。然而,由于样本相关性和目标 Q 网络的非平稳性,在 RL 上训练深度神经网络非常不稳定。
DQN 提出了使用经验回放和将目标网络与正在训练的 Q 网络分开的方法来解决这些问题。DDQN 建议通过将动作选择与动作评估分开,进一步改进算法,从而减少 Q 值的过度估计。DQN 还有其他改进建议。优先经验回放 [6] 认为,经验缓冲区不应均匀采样。相反,基于 TD 误差的重要经验应该被更加频繁地采样,以实现更高效的训练。[7] 提出了对抗性网络架构,用于估计状态值函数和优势函数。两者均用于估算 Q 值,从而加速学习。
本章节介绍的方法是值迭代/拟合。通过找到最优值函数来间接学习策略。下一章将直接学习最优策略,使用一类被称为策略梯度方法的算法。学习策略具有许多优势。特别是,策略梯度方法可以处理离散和连续的动作空间。
参考文献
-
Sutton 和 Barto. 强化学习:一种介绍, 2017 (
incompleteideas.net/book/bookdraft2017nov5.pdf). -
Volodymyr Mnih 和其他人, 通过深度强化学习实现人类水平控制. Nature 518.7540, 2015: 529 (
www.davidqiu.com:8888/research/nature14236.pdf) -
Hado Van Hasselt, Arthur Guez, 和 David Silver 双 Q 学习的深度强化学习. AAAI. Vol. 16, 2016 (
www.aaai.org/ocs/index.php/AAAI/AAAI16/paper/download/12389/11847). -
Kai Arulkumaran 和其他人 深度强化学习简要调查. arXiv 预印本 arXiv:1708.05866, 2017 (
arxiv.org/pdf/1708.05866.pdf). -
David Silver 强化学习讲义, (
www0.cs.ucl.ac.uk/staff/d.silver/web/Teaching.html). -
Tom Schaul 和其他人. 优先经验重放. arXiv 预印本 arXiv:1511.05952, 2015 (
arxiv.org/pdf/1511.05952.pdf). -
Ziyu Wang 和其他人. 深度强化学习的对抗网络架构. arXiv 预印本 arXiv:1511.06581, 2015 (
arxiv.org/pdf/1511.06581.pdf).
第十章 策略梯度方法
在本书的最后一章,我们将介绍直接优化策略网络的强化学习算法。这些算法统称为策略梯度方法。由于策略网络在训练过程中是直接优化的,因此策略梯度方法属于在策略(on-policy)强化学习算法家族。像我们在第九章讨论的基于值的方法一样,深度强化学习,策略梯度方法也可以作为深度强化学习算法来实现。
研究策略梯度方法的一个基本动机是解决 Q-Learning 的局限性。我们回顾一下,Q-Learning 是通过选择能够最大化状态值的动作来进行学习的。通过 Q 函数,我们能够确定一种策略,使得代理能够根据给定的状态决定采取哪种动作。所选择的动作就是给代理带来最大值的那个动作。在这方面,Q-Learning 只适用于有限数量的离散动作。它无法处理连续动作空间环境。此外,Q-Learning 并不是在直接优化策略。最终,强化学习的目标是找到那种最优策略,代理可以利用它来决定采取哪种行动以最大化回报。
相比之下,策略梯度方法适用于具有离散或连续动作空间的环境。此外,本章将介绍的四种策略梯度方法是直接优化策略网络的性能度量。这导致了一个训练好的策略网络,代理可以使用该网络在其环境中进行最优行动。
总结来说,本章的目标是呈现:
-
策略梯度定理
-
四种策略梯度方法:REINFORCE,带基线的 REINFORCE,演员-评论家(Actor-Critic),以及优势演员-评论家(Advantage Actor-Critic,A2C)
-
关于如何在 Keras 中实现策略梯度方法的指南,适用于连续动作空间环境
策略梯度定理
如第九章中所讨论的,深度强化学习,在强化学习中,智能体位于一个处于状态s[t']的环境中,这是状态空间
的一个元素。状态空间
可以是离散的,也可以是连续的。智能体从动作空间
中采取动作a[t],遵循策略
。
可以是离散的,也可以是连续的。由于执行动作a[t],智能体获得奖励r[t+1],并且环境转移到一个新的状态s[t+1]。新状态仅依赖于当前状态和动作。智能体的目标是学习一个最优策略
,以最大化所有状态的回报:
(方程 9.1.1)
回报,
,被定义为从时间 t 到剧集结束或达到终止状态时的折扣累计奖励:
(方程 9.1.2)
从方程 9.1.2可以看出,回报也可以解释为通过遵循策略
得到的给定状态的值。从方程 9.1.1可以观察到,与即时奖励相比,未来奖励的权重较低,因为通常
,其中
。
到目前为止,我们只考虑了通过优化基于值的函数Q(s,a)来学习策略。本章的目标是通过对
进行参数化来直接学习策略。通过参数化,我们可以使用神经网络来学习策略函数。学习策略意味着我们要最大化一个特定的目标函数
,该目标函数是相对于参数
的性能度量。在情节强化学习中,性能度量是起始状态的值。在连续情况下,目标函数是平均奖励率。
通过执行梯度上升,可以最大化目标函数
。在梯度上升中,梯度更新是朝着被优化函数的导数方向进行的。到目前为止,我们所有的损失函数都是通过最小化或执行梯度下降来优化的。稍后,在 Keras 实现中,我们可以看到梯度上升通过简单地将目标函数取负并执行梯度下降来完成。
直接学习策略的优势在于它可以应用于离散和连续的动作空间。对于离散动作空间:
(方程 10.1.1)
在该公式中,a[i] 是第 i 个动作。a[i] 可以是神经网络的预测或状态-动作特征的线性函数:
(方程 10.1.2)
是任何将状态-动作转换为特征的函数,例如编码器。
确定每个 a[i] 的概率。例如,在上一章中的平衡摆杆问题中,目标是通过沿二维轴向左或向右移动小车来保持摆杆竖直。在这种情况下,a[0] 和 a[1] 分别是向左和向右移动的概率。一般来说,代理选择具有最高概率的动作,
。
对于连续动作空间,
会根据状态从概率分布中采样一个动作。例如,如果连续动作空间是范围
,那么
通常是一个高斯分布,其均值和标准差由策略网络预测。预测的动作是从这个高斯分布中采样得到的。为了确保不生成无效的预测,动作会在 -1.0 和 1.0 之间截断。
正式地,对于连续动作空间,策略是从高斯分布中采样:
(方程 10.1.3)
均值,
,和标准差,
,都是状态特征的函数:
(方程 10.1.4)
(方程 10.1.5)
是任何将状态转换为其特征的函数。
是 softplus 函数,它确保标准差为正值。实现状态特征函数的一个方法是使用自编码器网络的编码器。在本章结束时,我们将训练一个自编码器,并使用编码器部分作为状态特征函数。因此,训练策略网络就是优化参数
的问题。
给定一个连续可微的策略函数,
,可以计算策略梯度:
(方程 10.1.6)
方程 10.1.6 也称为 策略梯度定理。它适用于离散和连续的动作空间。相对于参数的梯度
是通过策略动作采样的自然对数并按 Q 值缩放计算得到的。方程 10.1.6 利用了自然对数的性质,
。
策略梯度定理在直观上是合理的,因为性能梯度是从目标策略样本中估计的,并且与策略梯度成正比。策略梯度通过 Q 值进行缩放,以鼓励那些有助于状态价值提升的动作。梯度还与动作概率成反比,以惩罚那些频繁发生但对性能度量的提升没有贡献的动作。
在下一节中,我们将展示估计策略梯度的不同方法。
注意
关于策略梯度定理的证明,请参见[2]和 David Silver 关于强化学习的讲义,www0.cs.ucl.ac.uk/staff/d.silver/web/Teaching_files/pg.pdf
策略梯度方法具有一些微妙的优势。例如,在某些基于卡片的游戏中,基于价值的方法在处理随机性时没有直接的程序,而策略方法则不同。在策略方法中,随着参数的变化,动作的概率会平滑变化。与此同时,基于价值的动作可能会因参数的微小变化而遭遇剧烈的波动。最后,策略方法对参数的依赖促使我们采用不同的方式来执行性能度量上的梯度上升。这些就是接下来章节中将介绍的四种策略梯度方法。
策略方法也有其自身的缺点。它们通常更难训练,因为有趋向局部最优解的倾向,而不是全局最优解。在本章末尾将介绍的实验中,代理容易变得舒适,并选择那些不一定给出最高价值的动作。策略梯度还具有高方差的特点。
梯度更新经常被高估。此外,训练策略方法是费时的。训练通常需要成千上万的回合(即样本效率低)。每个回合只提供少量样本。在本章末尾提供的实现中,典型的训练需要在 GTX 1060 GPU 上大约一个小时来进行 1,000 回合。
在接下来的章节中,我们将讨论四种策略梯度方法。虽然讨论主要集中在连续动作空间上,但这一概念通常也适用于离散动作空间。由于四种策略梯度方法中策略网络和价值网络的实现方式相似,我们将在本章结束时演示如何将其实现到 Keras 中。
蒙特卡洛策略梯度(REINFORCE)方法
最简单的策略梯度方法叫做 REINFORCE [5],它是一种蒙特卡洛策略梯度方法:
(方程 10.2.1)
其中 R[t] 是在 方程 9.1.2 中定义的回报。R[t] 是策略梯度定理中
的无偏样本。
算法 10.2.1 总结了 REINFORCE 算法 [2]。REINFORCE 是一种蒙特卡洛算法。它不需要环境动态的知识(即无模型)。只需要经验样本,
,就能优化地调整策略网络的参数,
。折扣因子,
,考虑到奖励随着步数增加而减少的价值。梯度被折扣,
。在后期步骤中计算的梯度贡献较小。学习率,
,是梯度更新的缩放因子。
参数通过执行使用折扣梯度和学习率的梯度上升法进行更新。作为一种蒙特卡洛算法,REINFORCE 要求智能体完成一个回合后才会处理梯度更新。由于其蒙特卡洛性质,REINFORCE 的梯度更新具有高方差的特点。在本章结束时,我们将把 REINFORCE 算法实现到 Keras 中。
算法 10.2.1 REINFORCE
需要:一个可微分的参数化目标策略网络,
。
需要:折扣因子,
和学习率
。例如,
和
。
需要:
,初始策略网络参数(例如,
)。
-
重复
-
生成一个 episode,
,通过遵循 ![蒙特卡洛策略梯度(REINFORCE)方法]()
-
对于步骤
,执行 -
计算回报,
![蒙特卡洛策略梯度(REINFORCE)方法]()
-
计算折扣性能梯度,
![蒙特卡洛策略梯度(REINFORCE)方法]()
-
执行梯度上升,
![蒙特卡洛策略梯度(REINFORCE)方法]()

图 10.2.1:策略网络
在 REINFORCE 中,参数化的策略可以通过神经网络建模,如图 10.2.1所示。如前一节所讨论,对于连续动作空间的情况,状态输入会被转换为特征。状态特征是策略网络的输入。表示策略函数的高斯分布具有均值和标准差,二者都是状态特征的函数。策略网络,
,可以是 MLP、CNN 或 RNN,具体取决于状态输入的性质。预测的动作只是从策略函数中采样得到的。
带基准的 REINFORCE 方法
REINFORCE 算法可以通过从回报中减去一个基准来进行泛化,
。基准函数,B(s t ) 可以是任何函数,只要它不依赖于 a[t]。基准不会改变性能梯度的期望值:
(公式 10.3.1)
公式 10.3.1 表明,
,因为
不是
的函数。
虽然引入基准不会改变期望值,但它降低了梯度更新的方差。方差的减少通常会加速学习。在大多数情况下,我们使用价值函数,
作为基准。如果回报被高估,缩放因子将按比例通过价值函数减小,从而降低方差。价值函数也是参数化的,
,并与策略网络共同训练。在连续动作空间中,状态值可以是状态特征的线性函数:
(公式 10.3.2)
算法 10.3.1 总结了带基线的 REINFORCE 方法[1]。这与 REINFORCE 相似,唯一的不同是返回值被
替代。不同之处在于我们现在训练两个神经网络。如图 10.3.1所示,除了策略网络
,还同时训练价值网络
。策略网络参数通过性能梯度
更新,而价值网络参数则通过价值梯度
调整。由于 REINFORCE 是一种蒙特卡洛算法,因此可以推测,价值函数的训练也是蒙特卡洛算法。
学习率不一定相同。请注意,价值网络也在执行梯度上升。我们将在本章末尾展示如何使用 Keras 实现带基线的 REINFORCE 方法。
算法 10.3.1 带基线的 REINFORCE
需要:一个可微分的参数化目标策略网络
。
需要:一个可微分的参数化价值网络
。
需要:折扣因子
,性能梯度的学习率
和价值梯度的学习率
。
需要:
,初始策略网络参数(例如,
)。
,初始价值网络参数(例如,
)。
-
重复
-
通过跟随
生成一个回合![带基线的 REINFORCE 方法]()
-
对于步骤
,执行 -
计算回报
![带基线的 REINFORCE 方法]()
-
减去基线
![带基线的 REINFORCE 方法]()
-
计算折扣后的价值梯度
![带基线的 REINFORCE 方法]()
-
执行梯度上升
![带基线的 REINFORCE 方法]()
-
计算折扣后的性能梯度
![带基线的 REINFORCE 方法]()
-
执行梯度上升
![带基线的 REINFORCE 方法]()

图 10.3.1:策略和价值网络
演员-评论员方法
在带基线的 REINFORCE 方法中,值被用作基线。它不是用来训练值函数的。在本节中,我们将介绍一种带基线的 REINFORCE 变种,称为演员-评论家方法。策略网络和值网络分别扮演演员和评论家的角色。策略网络是演员,负责在给定状态下决定采取何种行动。与此同时,值网络则评估演员或策略网络做出的决策。值网络作为评论家,量化演员所做的选择的好坏。值网络通过将当前状态值与收到的奖励的总和以及观察到的下一个状态的折现值进行比较,来评估状态值,
。这种差异,
,表示为:
(方程式 10.4.1)
其中为了简便起见,我们省略了
和
的下标。方程式 10.4.1 类似于第九章中讨论的 Q-Learning 中的时序差分方法,深度强化学习。下一个状态值被折现了
。估计远期奖励是困难的。因此,我们的估计仅基于近期的未来,
。这被称为自举技术。自举技术和在方程式 10.4.1 中对状态表示的依赖通常能够加速学习并减少方差。从方程式 10.4.1中我们可以看到,值网络评估的是当前状态,
,这是由于策略网络的上一个动作,
。与此同时,策略梯度是基于当前动作,
。从某种意义上说,评估延迟了一个步骤。
算法 10.4.1 总结了演员-评论家方法[2]。除了用于训练策略和值网络的状态值评估外,训练是在线进行的。在每一步中,两个网络都会进行训练。这与 REINFORCE 和带基线的 REINFORCE 不同,后者在训练之前需要完成一个回合。值网络被调用了两次。首先是在当前状态的值估计过程中,其次是在下一个状态的值估计过程中。两个值都用于梯度计算。图 10.4.1展示了演员-评论家网络。我们将在本章末尾使用 Keras 实现演员-评论家方法。
算法 10.4.1 演员-评论家
要求:一个可微分的参数化目标策略网络,
。
需要:一个可微分的参数化价值网络,
。
需要:折扣因子,
,性能梯度的学习率,
,以及价值梯度的学习率,
。
需要:
,初始策略网络参数(例如,
)。
,初始价值网络参数(例如,
)。
-
重复
-
对于步骤
执行 -
采样一个动作
![演员-评论员方法]()
-
执行动作并观察奖励
和下一个状态 ![演员-评论员方法]()
-
评估状态值估计,
![演员-评论员方法]()
-
计算折扣价值梯度,
![演员-评论员方法]()
-
执行梯度上升,
![演员-评论员方法]()
-
计算折扣性能梯度,
![演员-评论员方法]()
-
执行梯度上升,
![演员-评论员方法]()
-
![演员-评论员方法]()

图 10.4.1:演员-评论员网络
优势演员-评论员 (A2C) 方法
在前一部分中的演员-评论员方法中,目标是让价值函数正确评估状态值。还有其他技术可以训练价值网络。一种显而易见的方法是使用 均方误差 (MSE) 在价值函数优化中,类似于 Q-Learning 中的算法。新的价值梯度等于回报的 MSE 的偏导数,
,和状态值之间:
(方程 10.5.1)
随着
,价值网络的预测变得更加准确。我们将这种变种的演员-评论员算法称为 A2C。A2C 是 异步优势演员-评论员(A3C)的单线程或同步版本,[3]提出。这个量
被称为 优势。
算法 10.5.1 概述了 A2C 方法。A2C 与行动者-评论员有一些区别。行动者-评论员是在线的,或者在每个经验样本上进行训练。A2C 类似于蒙特卡洛算法 REINFORCE 和带基线的 REINFORCE。它是在一个回合完成后进行训练的。行动者-评论员从第一个状态训练到最后一个状态,而 A2C 训练从最后一个状态开始,最终到达第一个状态。此外,A2C 的策略和价值梯度不再被折扣因子
折扣。
A2C 的对应网络类似于图 10.4.1,因为我们只改变了梯度计算的方法。为了在训练过程中鼓励智能体探索,A3C 算法[3]建议将策略函数加权熵值的梯度添加到梯度函数中,
。回忆一下,熵是事件的信息量或不确定性的度量。
算法 10.5.1 优势行动者-评论员(A2C)
需要: 一个可微的参数化目标策略网络,
。
需要: 一个可微的参数化值网络,
。
需要: 折扣因子,
,性能梯度的学习率,
,值梯度的学习率,
和熵权重,
。
需要:
,初始策略网络参数(例如,
)。
,初始值网络参数(例如,
)。
-
重复
-
通过遵循
生成一个回合 ![优势行动者-评论员(A2C)方法]()
-
![优势行动者-评论员(A2C)方法]()
-
对于步骤
,执行 -
计算回报,
![优势行动者-评论员(A2C)方法]()
-
计算值梯度,
![优势行动者-评论员(A2C)方法]()
-
累积梯度,
![优势行动者-评论员(A2C)方法]()
-
计算性能梯度,
![优势行动者-评论员(A2C)方法]()
-
执行梯度上升,
![优势行动者-评论员(A2C)方法]()
使用 Keras 的策略梯度方法
前面讨论的四种策略梯度方法(算法 10.2.1 到 10.5.1)使用相同的策略和价值网络模型。图 10.2.1 到 10.4.1 中的策略和价值网络配置相同。四种策略梯度方法的区别仅在于:
-
性能和价值梯度公式
-
训练策略
在本节中,我们将讨论在 Keras 中实现 算法 10.2.1 到 10.5.1 的代码,因为它们共享许多共同的例程。
注意
完整代码可以在 github.com/PacktPublishing/Advanced-Deep-Learning-with-Keras 找到。
但在讨论实现之前,先简要了解一下训练环境。

图 10.6.1 MountainCarContinuous-v0 OpenAI Gym 环境
与 Q-Learning 不同,策略梯度方法适用于离散和连续的动作空间。在我们的示例中,我们将在一个连续动作空间的案例中演示四种策略梯度方法,MountainCarContinuous-v0 环境来自 OpenAI Gym,gym.openai.com。如果你不熟悉 OpenAI Gym,请参考第九章,深度强化学习。
MountainCarContinuous-v0 二维环境的快照如 图 10.6.1 所示。在这个二维环境中,一辆引擎不太强大的汽车位于两座山之间。为了到达右侧山顶的黄色旗帜,它必须前后行驶以获得足够的动能。施加到汽车上的能量(即,动作的绝对值)越大,奖励就越小(或者说,奖励变得更负)。奖励始终为负,只有到达旗帜时才为正。在那时,汽车会获得 +100 的奖励。然而,每个动作都会受到以下代码的惩罚:
reward-= math.pow(action[0],2)*0.1
有效动作值的连续范围是[-1.0, 1.0]。超出该范围时,动作会被截断为其最小值或最大值。因此,应用大于 1.0 或小于-1.0 的动作值是没有意义的。MountainCarContinuous-v0 环境的状态包含两个元素:
-
汽车位置
-
汽车速度
状态通过编码器转换为状态特征。预测的动作是策略模型在给定状态下的输出。价值函数的输出是状态的预测值:

图 10.6.2 自动编码器模型

图 10.6.3 编码器模型

图 10.6.4 解码器模型
如图 10.2.1到图 10.4.1所示,在构建策略和价值网络之前,我们必须首先创建一个将状态转换为特征的函数。这个函数是通过类似于第三章中实现的自编码器的编码器来实现的,自编码器。图 10.6.2展示了由编码器和解码器组成的自编码器。在图 10.6.3中,编码器是一个 MLP,由Input(2)-Dense(256, activation='relu')-Dense(128, activation='relu')-Dense(32)组成。每个状态都被转换为一个 32 维的特征向量。在图 10.6.4中,解码器也是一个 MLP,但由Input(32)-Dense(128, activation='relu')-Dense(256, activation='relu')-Dense(2)组成。自编码器经过 10 个 epoch 的训练,使用MSE损失函数和 Keras 默认的 Adam 优化器。我们为训练和测试数据集随机采样了 220,000 个状态,并应用了 200k/20k 的训练-测试集划分。训练后,编码器权重被保存以便在未来的策略和价值网络训练中使用。列表 10.6.1展示了构建和训练自编码器的方法。
列表 10.6.1,policygradient-car-10.1.1.py展示了我们构建和训练自编码器的方法:
# autoencoder to convert states into features
def build_autoencoder(self):
# 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')
# training the autoencoder using randomly sampled
# states from the environment
def train_autoencoder(self, x_train, x_test):
# 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)

图 10.6.5:策略模型(行为者模型)
给定MountainCarContinuous-v0环境,策略(或行为者)模型预测必须施加在汽车上的动作。正如本章第一部分关于策略梯度方法的讨论,对于连续动作空间,策略模型从高斯分布中采样一个动作,
。在 Keras 中,这是通过以下方式实现的:
# given mean and stddev, sample an action, clip and return
# we assume Gaussian distribution of probability of selecting an
# action given a state
def action(self, args):
mean, stddev = args
dist = tf.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
动作在其最小值和最大值之间被裁剪。
策略网络的作用是预测高斯分布的均值和标准差。图 10.6.5展示了策略网络建模的过程
。值得注意的是,编码器模型具有冻结的预训练权重。只有均值和标准差的权重会接收性能梯度更新。
策略网络基本上是公式10.1.4 和10.1.5的实现,这些公式为了方便起见在此重复:
(公式 10.1.4)
(公式 10.1.5)
其中
是编码器,
是均值Dense(1)层的权重,
是标准差Dense(1)层的权重。我们使用了修改过的softplus函数,
,以避免标准差为零:
# some implementations use a modified softplus to ensure that
# the stddev is never zero
def softplusk(x):
return K.softplus(x) + 1e-10
政策模型生成器如下列表所示。此列表还包括我们接下来将讨论的对数概率、熵和值模型。
列表 10.6.2,policygradient-car-10.1.1.py向我们展示了从编码状态特征构建策略(演员)、logp、熵和值模型的方法:
def build_actor_critic(self):
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()

图 10.6.6:策略的高斯对数概率模型

图 10.6.7:熵模型
除了策略网络,
,我们还必须有动作对数概率(logp)网络
,因为这实际上计算梯度。如图 10.6.6所示,logp网络只是策略网络,其中额外的Lambda(1)层计算给定动作、均值和标准差的高斯分布的对数概率。logp网络和演员(策略)模型共享相同的参数集。Lambda层没有任何参数。它由以下函数实现:
# given mean, stddev, and action compute
# the log probability of the Gaussian distribution
def logp(self, args):
mean, stddev, action = args
dist = tf.distributions.Normal(loc=mean, scale=stddev)
logp = dist.log_prob(action)
return logp
训练logp网络也训练了演员模型。在本节讨论的训练方法中,只训练了logp网络。
如图 10.6.7所示,熵模型还与策略网络共享参数。输出Lambda(1)层使用以下函数计算高斯分布的熵,给定均值和标准差:
# given the mean and stddev compute the Gaussian dist entropy
def entropy(self, args):
mean, stddev = args
dist = tf.distributions.Normal(loc=mean, scale=stddev)
entropy = dist.entropy()
return entropy
熵模型仅由 A2C 方法使用:

图 10.6.8:值模型
前面的图显示了值模型。该模型还使用冻结权重的预训练编码器来实现以下方程,这里为了方便重复列出:
(方程 10.3.2)
是Dense(1)层的权重,唯一接收值梯度更新的层。图 10.6.8代表了
在算法 10.3.1到10.5.1中。值模型可以用几行代码构建:
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中,我们通过梯度上升执行目标函数的最大化。在 Keras 中,我们通过梯度下降执行损失函数的最小化。损失函数简单地是要最大化的目标函数的负值。梯度下降是梯度上升的负数。列表 10.6.3显示了logp和值损失函数。
我们可以利用损失函数的共同结构,将 算法 10.2.1 到 10.5.1 的损失函数统一起来。性能和值的梯度仅在常数因子上有所不同。所有的性能梯度都有共同项
。这在策略日志概率损失函数 logp_loss() 中由 y_pred 表示。共同项的因子
取决于所使用的算法,并通过 y_true 实现。表 10.6.1 显示了 y_true 的值。剩余项是熵的加权梯度
。它在 logp_loss() 函数中实现为 beta 和 entropy 的乘积。只有 A2C 使用此项,因此默认情况下,beta=0.0。对于 A2C,beta=0.9。
列表 10.6.3,policygradient-car-10.1.1.py:logp 和值网络的损失函数。
# logp loss, the 3rd and 4th variables (entropy and beta) are needed
# by A2C so we have a different loss function structure
def logp_loss(self, entropy, beta=0.0):
def loss(y_true, y_pred):
return -K.mean((y_pred * y_true) + (beta * entropy), axis=-1)
return loss
# typical loss function structure that accepts 2 arguments only
# this will be used by value loss of all methods except A2C
def value_loss(self, y_true, y_pred):
return -K.mean(y_pred * y_true, axis=-1)
| 算法 | logp_loss 的 y_true |
value_loss 的 y_true |
|---|---|---|
| 10.2.1 REINFORCE | ![]() |
不适用 |
| 10.3.1 带基准的 REINFORCE | ![]() |
![]() |
| 10.4.1 演员-评论员 | ![]() |
![]() |
| 10.5.1 A2C | ![]() |
![]() |
表 10.6.1:
logp_loss和value_loss的y_true值。
类似地,算法 10.3.1 和 10.4.1 的值损失函数具有相同的结构。这些值损失函数在 Keras 中实现为 value_loss(),如 列表 10.6.3 所示。共同的梯度因子
由张量 y_pred 表示。剩余的因子由 y_true 表示。y_true 的值也显示在 表 10.6.1 中。REINFORCE 不使用值函数。A2C 使用 MSE 损失函数来学习值函数。在 A2C 中,y_true 表示目标值或真实值。
列表 10.6.4,policygradient-car-10.1.1.py 向我们展示了,REINFORCE、带基准的 REINFORCE 和 A2C 都是通过每个回合进行训练的。计算合适的回报后,再调用 列表 10.6.5 中的主要训练例程:
# train by episode (REINFORCE, REINFORCE with baseline
# and A2C use this routine to prepare the dataset before
# the step by step training)
def train_by_episode(self, last_value=0):
if self.args.actor_critic:
print("Actor-Critic must be trained per step")
return
elif self.args.a2c:
# 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)
return
# only REINFORCE and REINFORCE with baseline
# use the ff codes
# 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)
列表 10.6.5,policygradient-car-10.1.1.py 向我们展示了所有策略梯度算法使用的主要 train 例程。演员-评论员在每个经验样本时调用此例程,而其他算法则在每个回合的训练例程中调用此例程,见 列表 10.6.4:
# main routine for training as used by all 4 policy gradient
# methods
def train(self, item, gamma=1.0):
[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
# actor-critic: delta = reward - value + discounted_next_value
# a2c: delta = discounted_reward - value
delta = reward - self.value(state)[0]
# only REINFORCE does not use a critic (value network)
critic = False
if self.args.baseline:
critic = True
elif self.args.actor_critic:
# since this function is called by Actor-Critic
# directly, evaluate the value function here
critic = True
if not done:
next_value = self.value(next_state)[0]
# add the discounted next value
delta += gamma*next_value
elif self.args.a2c:
critic = True
else:
delta = reward
# 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)
# in A2C, the target value is the return (reward
# replaced by return in the train_by_episode function)
if self.args.a2c:
discounted_delta = reward
discounted_delta = np.reshape(discounted_delta, [-1, 1])
# train the value network (critic)
if critic:
self.value_model.fit(np.array(state),
discounted_delta,
batch_size=1,
epochs=1,
verbose=verbose)
所有网络模型和损失函数就绪后,最后一部分是训练策略,每个算法的训练策略不同。如 列表 10.6.4 和 10.6.5 中所示,使用了两个训练函数。算法 10.2.1、10.3.1 和 10.5.1 在训练前等待完整剧集完成,因此同时运行 train_by_episode() 和 train()。完整的剧集保存在 self.memory 中。Actor-Critic 算法 10.4.1 每步训练,仅运行 train()。
每个算法处理其剧集轨迹的方式不同。
| 算法 | y_true 公式 |
Keras 中的 y_true |
|---|---|---|
| 10.2.1 REINFORCE | ![]() |
reward * discount_factor |
| 10.3.1 带基线的 REINFORCE | ![]() |
(reward - self.value(state)[0]) * discount_factor |
| 10.4.1 Actor-Critic | ![]() |
(reward - self.value(state)[0] + gamma*next_value) * discount_factor |
| 10.5.1 A2C | 和 ![]() |
(reward - self.value(state)[0]) 和 reward |
表格 10.6.2:表格 10.6.1 中的 y_true 值
对于 REINFORCE 方法和 A2C,reward 实际上是 train_by_episode() 中计算的返回值。discount_factor = gamma**step。
两种 REINFORCE 方法通过替换内存中的奖励值来计算返回值
:
# only REINFORCE and REINFORCE with baseline
# use the ff codes
# 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_
然后,训练策略(演员)和价值模型(仅带基线)从第一步开始,对每一步进行训练。
A2C 的训练策略不同,它从最后一步到第一步计算梯度。因此,返回值从最后一步的奖励或下一个状态值开始累积:
# 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)
列表中的 reward 变量也被返回值替代。如果到达终止状态(即汽车触及旗帜)或非终止状态的下一个状态值,则初始化为 reward:
v = 0 if reward > 0 else agent.value(next_state)[0]
在 Keras 实现中,我们提到的所有程序都作为方法在 PolicyAgent 类中实现。PolicyAgent 的作用是表示实施策略梯度方法的智能体,包括构建和训练网络模型以及预测动作、对数概率、熵和状态值。
以下列表展示了智能体执行并训练策略与价值模型时一个剧集的展开方式。for 循环执行 1000 个剧集。一个剧集在达到 1000 步或汽车触及旗帜时终止。智能体在每一步执行策略预测的动作。在每个剧集或步骤后,训练程序被调用。
列表 10.6.6,policygradient-car-10.1.1.py:智能体运行 1000 个剧集,每一步都执行策略预测的动作并进行训练:
# 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
v = 0 if reward > 0 else agent.value(next_state)[0]
agent.train_by_episode(last_value=v)
# accumulate reward
total_reward += reward
# next state is the new state
state = next_state
step += 1
策略梯度方法的性能评估
这四种策略梯度方法通过训练智能体 1,000 回合来进行评估。我们将 1 次训练定义为 1,000 回合的训练。第一个性能度量标准是通过累计智能体在 1,000 回合中到达旗帜的次数来衡量的。图 10.7.1 到 10.7.4 显示了每种方法的五次训练会话。
在这个度量标准中,A2C 以最大的次数到达旗帜,其次是带基线的 REINFORCE、演员-评论家方法和 REINFORCE 方法。使用基线或评论家加速了学习。请注意,这些是智能体不断改进其性能的训练会话。在实验中,确实有一些情况,智能体的表现没有随时间提升。
第二个性能度量标准是基于以下要求:如果每个回合的总奖励至少为 90.0,那么MountainCarContinuous-v0被认为是已解决的。通过每种方法的五次训练,我们选择了最后 100 个回合(第 900 到 999 回合)中总奖励最高的一次训练。图 10.7.5 到 10.7.8 显示了四种策略梯度方法的结果。带基线的 REINFORCE 是唯一能够在 1,000 回合训练后持续获得约 90 总奖励的方法。A2C 表现第二好,但无法持续达到至少 90 的总奖励。

图 10.7.1:使用 REINFORCE 方法山地车到达旗帜的次数

图 10.7.2:使用带基线的 REINFORCE 方法山地车到达旗帜的次数

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

图 10.7.4:使用 A2C 方法山地车到达旗帜的次数

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

图 10.7.6:使用带基线的 REINFORCE 方法每回合获得的总奖励。

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

图 10.7.8:使用 A2C 方法每回合获得的总奖励
在实验中,我们对对数概率和价值网络的优化使用相同的学习率1e-3。折扣因子设置为 0.99,除了 A2C 方法,它在 0.95 的折扣因子下更容易训练。
鼓励读者通过执行以下命令来运行训练好的网络:
$ python3 policygradient-car-10.1.1.py
--encoder_weights=encoder_weights.h5 --actor_weights=actor_weights.h5
以下表格显示了运行 policygradient-car-10.1.1.py 的其他模式。权重文件(即 *.h5)可以用你自己的预训练权重文件替换。请参考代码查看其他潜在选项:
| 目的 | 运行 |
|---|---|
| 从头开始训练 REINFORCE |
python3 policygradient-car-10.1.1.py
--encoder_weights=encoder_weights.h5
|
| 从头开始训练带基线的 REINFORCE |
|---|
python3 policygradient-car-10.1.1.py
--encoder_weights=encoder_weights.h5 -b
|
| 从头开始训练 Actor-Critic |
|---|
python3 policygradient-car-10.1.1.py
--encoder_weights=encoder_weights.h5 -a
|
| 从头开始训练 A2C |
|---|
python3 policygradient-car-10.1.1.py
--encoder_weights=encoder_weights.h5 -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
|
| 从之前保存的权重训练 Actor-Critic |
|---|
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 时的不同选项
最后需要注意的是,在 Keras 中实现策略梯度方法存在一些局限性。例如,训练 actor 模型需要重新采样动作。动作首先被采样并应用到环境中以观察奖励和下一个状态。然后,再采样一次用于训练对数概率模型。第二次采样不一定与第一次相同,但用于训练的奖励来自第一次采样的动作,这可能在梯度计算中引入随机误差。
好消息是,Keras 在 tf.keras 中得到了 TensorFlow 大力支持。从 Keras 过渡到更灵活、更强大的机器学习库(如 TensorFlow)变得更加容易。如果你是从 Keras 开始,并且想要构建低级自定义机器学习例程,那么 Keras 和 tf.keras 的 API 有很强的相似性。
在 TensorFlow 中使用 Keras 有一定的学习曲线。此外,在 tf.keras 中,你可以利用 TensorFlow 新的易用 Dataset 和 Estimators API。这简化了大量的代码和模型重用,最终形成一个干净的管道。随着 TensorFlow 新的急切执行模式的出现,实现和调试 Python 代码在 tf.keras 和 TensorFlow 中变得更加容易。急切执行允许代码在不构建计算图的情况下执行,这与本书中所做的方式不同。它还允许代码结构类似于典型的 Python 程序。
结论
在本章中,我们介绍了策略梯度方法。从策略梯度定理开始,我们制定了四种方法来训练策略网络。我们详细讨论了四种方法:REINFORCE、带基线的 REINFORCE、Actor-Critic 和 A2C 算法。我们探索了这四种方法如何在 Keras 中实现。接着,我们通过检查代理成功达到目标的次数以及每个回合收到的总奖励来验证这些算法。
与我们在上一章讨论的 Deep Q-Network [3] 类似,基本的策略梯度算法可以进行一些改进。例如,最显著的一种是 A3C [4],它是 A2C 的多线程版本。这使得智能体能够同时接触到不同的经验,并异步地优化策略和值网络。然而,在 OpenAI 进行的实验中,blog.openai.com/baselines-acktr-a2c/,A3C 并没有比 A2C 强大的优势,因为前者无法充分利用如今强大的 GPU。
鉴于这是本书的结尾,值得注意的是,深度学习领域非常广阔,要在一本书中涵盖所有的进展几乎是不可能的。我们所做的是精心挑选了那些我认为在广泛应用中会有用的高级话题,并且这些话题是你,读者,可以轻松构建的。本书中展示的 Keras 实现将允许你继续进行,并将这些技术应用到你自己的工作和研究中。
参考文献
-
Sutton 和 Barto。强化学习:导论,
incompleteideas.net/book/bookdraft2017nov5.pdf,(2017)。 -
Mnih, Volodymyr,及其他人。通过深度强化学习实现人类水平的控制,自然 518.7540 (2015): 529。
-
Mnih, Volodymyr,及其他人。深度强化学习的异步方法,国际机器学习会议,2016。
-
Williams 和 Ronald J. 简单统计梯度跟踪算法用于连接主义强化学习,机器学习 8.3-4 (1992): 229-256。
第十一章:你可能会喜欢的其他书籍
如果你喜欢这本书,你可能对 Packt 出版的其他书籍感兴趣:

深度强化学习实战
Maxim Lapan
ISBN: 978-1-78883-424-7
-
理解强化学习的深度学习背景,并实现复杂的深度学习模型
-
学习强化学习的基础:马尔可夫决策过程
-
评估包括交叉熵、DQN、Actor-Critic、TRPO、PPO、DDPG、D4PG 等强化学习方法
-
了解如何在各种环境中处理离散和连续的动作空间
-
使用价值迭代法击败 Atari 街机游戏
-
创建你自己的 OpenAI Gym 环境来训练股票交易代理
-
教你的代理使用 AlphaGo Zero 玩 Connect4
-
探索最新的深度强化学习研究,主题包括 AI 驱动的聊天机器人

使用 TensorFlow 进行深度学习
Giancarlo Zaccone, Md. Rezaul Karim
ISBN: 978-1-78883-110-9
-
使用 TensorFlow 应用深度机器智能和 GPU 计算
-
访问公共数据集,使用 TensorFlow 加载、处理和转换数据
-
了解如何使用高级 TensorFlow API 构建更强大的应用程序
-
使用深度学习进行可扩展的物体检测和移动计算
-
通过探索强化学习技术,快速训练机器从数据中学习
-
探索深度学习研究和应用的活跃领域
留下评论——让其他读者知道你的想法
请通过在你购买书籍的网站上留下评论,与他人分享你对本书的看法。如果你是从亚马逊购买的书籍,请在本书的亚马逊页面上留下真实的评论。这一点至关重要,因为其他潜在读者可以通过你的公正意见做出购买决策,我们可以了解客户对我们产品的反馈,而作者也能看到他们与 Packt 合作创作的书籍的反馈。只需要几分钟时间,但对其他潜在客户、我们的作者和 Packt 都非常有价值。谢谢!


,其中
,并且是一个可调超参数














,来自采样数据的真实数据(即,
),标签为 1.0
,来自生成器的假数据,标签为 0.0

,其中
是所有联合分布 y(x,y) 的集合,其边际分布为 p[data] 和 p[g]。































其中
是权重,





或推理模型在编码x的属性到z时变得更好
在方程 8.1.10的右侧被最大化,或者解码器模型在从潜在向量z重建x方面变得更好
-VAE:具有解耦潜在表示的 VAE
,状态可能是离散的或连续的。起始状态是s[0],而终止状态是s[t]。
。
可能是离散的或连续的。
转移到新的状态s[t+1]。下一个状态只依赖于当前的状态和动作。
对代理不可知。
。奖励仅依赖于当前的状态和动作。R对代理不可知。
折扣,其中
,k是未来的时间步。

执行梯度下降步骤,更新参数 

,通过遵循 
,执行

















,执行






浙公网安备 33010602011771号