Python 与 TensorFlow2 生成式 AI(二)

原文:zh.annas-archive.org/md5/d06d282ea0d9c23c57f0ce31225acf76

译者:飞龙

协议:CC BY-NC-SA 4.0

第四章:教授网络生成数字

在前一章中,我们涵盖了神经网络模型的构建基块。在这一章中,我们的第一个项目将重新创建深度学习历史上最具突破性的模型之一- 深度信念网络DBN)。DBN 是一个最早的多层网络,为其开发了一个可行的学习算法。除了具有历史意义外,该模型与本书主题有关,因为学习算法利用生成模型来预先将神经网络权重调整到合理配置,然后进行反向传播。

在本章中,我们将涵盖:

  • 如何加载修改的国家标准技术研究所MNIST)数据集并使用 TensorFlow 2 的数据集 API 进行转换。

  • 如何通过最小化类似于物理公式的"能量"方程来训练受限玻尔兹曼机RBM)- 一个简单的神经网络- 以生成图像。

  • 如何堆叠多个 RBM 来生成 DBN 并应用前向和后向传递来预训练此网络以生成图像数据。

  • 如何通过将这种预训练与使用 TensorFlow 2 API 的反向传播"微调"相结合来实现端到端的分类器。

MNIST 数据库

在开发 DBN 模型时,我们将使用之前讨论过的数据集 - MNIST 数据库,其中包含手绘数字 0 到 9 的数字图像¹。该数据库是两组早期图像的组合,分别来自国家标准技术研究所NIST): 特殊数据库 1(由美国高中学生书写)和特殊数据库 3(由美国人口普查局员工书写)²,总共分为 60,000 个训练图像和 10,000 个测试图像。

原始数据集中的图像全部为黑白,而修改后的数据集将其标准化以适应 20x20 像素的边界框,并使用抗锯齿技术去除锯齿状边缘,导致清洁图像中间灰度值;它们被填充以获得最终分辨率为 28x28 像素。

在原始的 NIST 数据集中,所有的训练图像都来自局务员,而测试数据集来自高中学生,修改后的版本将这两组人群混合在训练和测试集中,以为机器学习算法提供一个更少偏见的人口。

图 4.1:NIST 数据集中的数字(左)³ 和 MNIST(右)⁴

支持向量机SVMs)早期应用于此数据集的结果显示出了 0.8%的错误率,⁵而最新的深度学习模型的错误率低至 0.23%。⁶ 你应该注意到,这些数字的获得不仅是由于使用的判别算法,还有"数据增强"技巧,如创建额外的翻译图像,其中数字已经偏移了几个像素,从而增加了算法学习的数据示例数量。由于其广泛的可用性,这个数据集已经成为许多机器学习模型的基准,包括深度神经网络。

该数据集也是 2006 年多层神经网络训练突破的基准,该突破实现了 1.25%的错误率(与前述示例不同,没有图像翻译)。⁷ 在本章中,我们将详细研究如何使用生成模型实现这一突破,并探讨如何构建我们自己的 DBN,以生成 MNIST 数字。

检索和加载 TensorFlow 中的 MNIST 数据集

训练自己的 DBN 的第一步是构造我们的数据集。本节将向您展示如何将 MNIST 数据转换为一种方便的格式,以便您可以使用一些 TensorFlow 2 的内置函数来训练神经网络,以简化操作。

让我们从 TensorFlow 中加载 MNIST 数据集开始。由于 MNIST 数据已经用于许多深度学习基准测试,TensorFlow 2 已经为加载和格式化此数据提供了方便的实用程序。为此,我们首先需要安装tensorflow-datasets库:

py 复制代码
pip install tensorflow-datasets 

安装完软件包后,我们需要导入它以及所需的依赖项:

py 复制代码
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import matplotlib.pyplot as plt
import numpy as np
import tensorflow.compat.v2 as tf
import tensorflow_datasets as tfds 

现在我们可以使用构建器功能从Google Cloud StorageGCS)本地下载 MNIST 数据:

py 复制代码
mnist_builder = tfds.builder("mnist")
mnist_builder.download_and_prepare() 

现在数据集将在我们的计算机磁盘上可用。正如前面所述,这些数据被分为训练数据集和测试数据集,您可以通过查看info命令来验证:

py 复制代码
info = mnist_builder.info
print(info) 

这给出了以下输出:

py 复制代码
tfds.core.DatasetInfo(
    name='mnist',
    version=3.0.1
    description='The MNIST database of handwritten digits.',
    homepage='http://yann.lecun.com/exdb/mnist/',
    features=FeaturesDict({
        'image': Image(shape=(28, 28, 1), dtype=tf.uint8),
        'label': ClassLabel(shape=(), dtype=tf.int64, num_classes=10),
    }),
    total_num_examples=70000,
    splits={
        'test': 10000,
        'train': 60000,
    },
    supervised_keys=('image', 'label'),
    citation="""@article{lecun2010mnist,
      title={MNIST handwritten digit database},
      author={LeCun, Yann and Cortes, Corinna and Burges, CJ},
      journal={ATT Labs [Online]. Available: http://yann. lecun. com/exdb/mnist},
      volume={2},
      year={2010}
    }""",
    redistribution_info=,
) 

正如您所看到的,测试数据集有 10,000 个示例,训练数据集有 60,000 个示例,图像为 28x28 像素,具有 10 个类别中的一个标签(0 到 9)。

让我们首先来看看训练数据集:

py 复制代码
mnist_train = mnist_builder.as_dataset(split="train") 

我们可以使用show_examples函数可视化绘制一些示例:

py 复制代码
fig = tfds.show_examples(info, mnist_train) 

这给出了以下图表:

图 4.2:来自 TensorFlow 数据集的 MNIST 数字示例

在这里,您还可以更清楚地看到应用了抗锯齿处理的灰度边缘,以使原始数据集的边缘看起来不那么锯齿状(颜色也已从图 4.1中的原始示例翻转)。

我们还可以通过从数据集中取一个元素,将其重新塑形为 28x28 数组,将其强制转换为 32 位浮点数,并以灰度形式绘制来绘制单个图像:

py 复制代码
flatten_image = partial(flatten_image, label=True)

for image, label in mnist_train.map(flatten_image).take(1):
    plt.imshow(image.numpy().reshape(28,28).astype(np.float32), 
               cmap=plt.get_cmap("gray"))
    print("Label: %d" % label.numpy()) 

这给出了以下图表:

图 4.3:TensorFlow 中的 MNIST 数字

这对于视觉检查很好,但是在本章的实验中,我们实际上需要将这些图像展平成向量。为了做到这一点,我们可以使用map()函数,并验证数据集现在已经被展平;请注意,我们还需要将其转换为浮点数以便稍后在 RBM 中使用。RBM 还假设输入是二进制(0 或 1),所以我们需要重新缩放像素,范围从 0 到 256 到范围从 0 到 1:

py 复制代码
def flatten_image(x, label=True):
    if label:
        return (tf.divide(tf.dtypes.cast(tf.reshape(x["image"], (1,28*28)), tf.float32), 256.0) , x["label"])
    else:
        return (tf.divide(tf.dtypes.cast(tf.reshape(x["image"], (1,28*28)), tf.float32), 256.0))
for image, label in mnist_train.map(flatten_image).take(1):
    plt.imshow(image.numpy().astype(np.float32), cmap=plt.get_cmap("gray"))
    print("Label: %d" % label.numpy()) 

这得到了一个 784x1 的向量,这是数字"4"的"展平"版本的像素:

图 4.4: 在 TensorFlow 中将 MNIST 数字展平

现在我们已经将 MNIST 数据处理成一系列向量,我们准备开始实现一个 RBM 来处理这些数据,最终创建一个能够生成新图像的模型。

受限玻尔兹曼机:用统计力学生成像素

我们将应用于 MNIST 数据的神经网络模型的起源可以追溯到对哺乳动物大脑中的神经元如何一起传递信号并编码模式作为记忆的早期研究。通过使用物理学中的统计力学类比,本节将向您展示简单的网络如何"学习"图像数据的分布,并且可以用作更大网络的构建模块。

霍普菲尔德网络和神经网络的能量方程

正如我们在第三章 讨论的深度神经网络的基本组成部分 中所提到的,赫布学习法 陈述:"发射的神经元会产生联系"⁸,并且许多模型,包括多层感知器,都利用了这个想法来开发学习规则。其中一个模型就是霍普菲尔德网络,由几位研究人员在 1970-80 年代开发^(9 10)。在这个网络中,每个"神经元"都通过对称权重与其他所有神经元相连,但没有自连接(只有神经元之间的连接,没有自环)。

与我们在第三章学习的多层感知器和其他架构不同,霍普菲尔德网络是一个无向图,因为边是"双向的"。

图 4.5: 霍普菲尔德网络

霍普菲尔德网络中的神经元采用二进制值,要么是(-1, 1),要么是(0, 1),作为双曲正切或 Sigmoid 激活函数的阈值版本:

阈值(sigma)在训练过程中不会发生变化;为了更新权重,可以使用"赫布学习法 "来使用一组n个二进制模式(所有神经元的配置)进行更新:

其中n 是模式数,e 是特定配置中神经元ij的二进制激活。观察这个方程,你会发现如果神经元共享一个配置,它们之间的连接会被加强,而如果它们是相反的符号(一个神经元的符号为+1,另一个的符号为-1),它们之间的连接就会被削弱。按照这个规则迭代地加强或削弱连接,导致网络收敛到一个稳定的配置,类似于网络的特定激活的"记忆",给定一些输入。这代表了生物有机体中的联想记忆模型------将不相关的思想链接在一起的记忆,就像 Hopfield 网络中的神经元被链接在一起一样。^(11 12)

除了表示生物记忆外,Hopfield 网络还与电磁学有一个有趣的相似点。如果我们将每个神经元视为粒子或"电荷",我们可以用一个"自由能"方程描述该模型,表示该系统中的粒子如何相互排斥/吸引,以及系统在潜在配置分布上相对于平衡点的位置:

这里,w 是神经元ij 之间的权重,s 是这些神经元的"状态"(要么是 1,"开",要么是-1,"关"),sigma 是每个神经元的阈值(例如,它的总输入必须超过的值,才能将其设置为"开")。当 Hopfield 网络处于其最终配置中时,它还最小化了为网络计算的能量函数的值,其中具有相同状态的单元通过强连接(w )连接。与特定配置相关联的概率由Gibbs 测度给出:

这里,Z(B)是一个归一化常数,表示与"Chapter 1",生成 AI 的介绍:"从模型中"绘制"数据 中的贝叶斯概率函数中的归一化常数相同,表示网络的所有可能配置。

还要注意能量函数的定义中,神经元的状态只受到本地连接的影响(而不是受到所有网络中其他神经元的状态影响,无论它是否连接);这也被称为马尔科夫性质 ,因为状态是"无记忆"的,仅取决于其立即"过去"(邻居)。实际上,Hammersly-Clifford 定理表明,任何具有相同无记忆属性的分布都可以使用 Gibbs 测度来表示。¹³

用受限玻尔兹曼机建模不确定性数据

我们可能对其他种类的分布感兴趣吗?虽然 Hopfield 网络从理论角度来看很有用,但其缺点之一是无法纳入实际物理或生物系统中存在的不确定性;与确定性的打开或关闭不同,现实世界的问题通常涉及一定程度的偶然性 - 磁铁可能会翻转极性,或者神经元可能会随机发射。

这种不确定性,或者随机性 ,反映在Boltzmann 机器中¹⁴------这是 Hopfield 网络的变体,其中一半的神经元("可见"单元)从环境接收信息,而另一半("隐藏"单元)只从可见单元接收信息。

图 4.6:Boltzmann 机器

Boltzmann 机器通过抽样随机打开(1)或关闭(0)每个神经元,并在许多迭代中收敛到能量函数的最小值所代表的稳定状态。这在图 4.6中以示意图的形式显示,网络的白色节点为"关闭",蓝色节点为"开启";如果我们模拟网络中的激活,这些值将随时间波动。

从理论上讲,像这样的模型可以用来模拟图像的分布,例如使用隐藏节点作为表示图像中每个像素的基础概率模型的"条形码"。然而,在实践中,这种方法存在问题。首先,随着 Boltzmann 网络中单元的数量增加,连接的数量呈指数增长(例如,必须在 Gibbs 测度的归一化常数中考虑的潜在配置数量激增),同样需要采样网络到平衡状态所需的时间也随之增加。其次,具有中间激活概率的单元的权重往往会呈现随机行走模式(例如,概率会随机增加或减少,但永远不会稳定到平衡值),直到神经元收敛,这也延长了训练时间。¹⁵

一个实用的修改是删除 Boltzmann 机器中的一些连接,即可见单元之间的连接和隐藏单元之间的连接,仅保留两种类型神经元之间的连接。这种修改称为 RBM,如图 4.7所示¹⁶:

图 4.7:RBM

正如之前描述的那样,可见单元是来自 MNIST 数据集的输入像素,而隐藏单元是该图像的编码表示。通过来回采样直到收敛,我们可以创建一个图像的生成模型。我们只需要一个学习规则,告诉我们如何更新权重以使能量函数收敛到其最小值;这个算法就是对比散度CD)。为了理解为什么我们需要一个特殊的算法来处理 RBM,有助于重新思考能量方程以及我们如何采样获得网络的平衡。

对比散度:梯度的近似

如果我们回顾第一章 生成式人工智能简介:从模型中"生成"数据,使用 RBM 创建图像的生成模型本质上涉及找到图像的概率分布,使用能量方程¹⁷:

其中x 是一个图像,theta 是模型的参数(权重和偏置),Z是分区函数:

为了找到优化这个分布的参数,我们需要基于数据最大化似然(每个数据点在密度函数下的概率乘积):

在实践中,使用负对数似然稍微容易一些,因为它表示为一个和:

如果分布f 的形式简单,那么我们可以对f 的参数进行导数。例如,如果f 是一个单一的正态分布,那么最大化E 关于 mu(平均值)和 sigma(标准差)的值分别是样本均值和标准差;分区函数Z不会影响这个计算,因为积分是 1,一个常数,一旦我们取了对数,它就变成了 0。

如果分布代替一个正态分布的总和,则mu(i) (这些分布中的一个)关于f (所有N 个正态分布的总和)的偏导数同样涉及到每个其他分布的 mu 和 sigma。由于这种依赖关系,对于最优值没有封闭形式解法(例如,我们可以通过重新排列项或应用代数转换写出的解方程);相反,我们需要使用梯度搜索方法(例如我们在第三章 深度神经网络的构建基块 中讨论的反向传播算法)迭代地找到这个函数的最优值。同样,每个这些N 个分布的积分都是 1,意味着分区函数是常数log(N),使得导数为 0。

如果分布f 是正态分布的乘积而不是和,会发生什么?对于参数θ来说,分区函数Z不再是该方程中的常数;其值将取决于这些函数在计算积分时如何重叠和在何处重叠------它们可能通过相互排斥(0)或重叠(产生大于 1 的值)相互抵消。为了评估梯度下降步骤,我们需要能够使用数值方法计算此分区函数。在 RBM 示例中,这种 28x28 MNIST 数字配置的分区函数将具有 784 个逻辑单元和大量可能的配置(2⁷⁸⁴),使其在每次我们想要进行梯度计算时评估变得不方便。

除了采用完整梯度之外,我们还能优化此能量方程的值吗?回到能量方程,让我们明确地写出梯度:

分区函数Z 还可以进一步写成涉及Xf的参数的积分函数:

其中*< >表示对从x*的分布中采样的观察数据的平均值。换句话说,我们可以通过从数据中进行采样并计算平均值来近似积分,这使我们能够避免计算或近似高维积分。

虽然我们不能直接从*p(x)中采样,但我们可以使用一种称为马尔可夫链蒙特卡洛MCMC)采样的技术从目标分布p(x')*生成数据。正如我们在讨论 Hopfield 网络时所描述的那样,"马尔可夫"属性意味着此采样仅使用上一个样本作为模拟中下一个数据点的概率的输入------这形成了一个"链",其中每个连续采样的数据点成为下一个数据点的输入。

这个技术名称中的"蒙特卡罗"是指摩纳哥公国的一个赌场,并表示,与赌博的结果一样,这些样本是通过随机过程生成的。通过生成这些随机样本,您可以使用N个 MCMC 步骤作为对难以或不可能积分的分布的平均值的近似。当我们把所有这些都放在一起时,我们得到以下梯度方程:

其中X 表示 MCMC 链中每一步的数据,其中X ⁰是输入数据。尽管在理论上您可能会认为需要大量步骤才能使链收敛,但实践中观察到,甚至N=1步就足以得到一个不错的梯度近似。¹⁸

注意,最终结果是输入数据和抽样数据之间的对比 ;因此,该方法被命名为对比散度,因为它涉及两个分布之间的差异。

将这一方法应用于我们的 RBM 示例中,我们可以按照以下步骤生成所需的样本:

  1. 取输入向量v

  2. 计算"隐藏"激活h

  3. 使用(2 )中的激活生成一个抽样的可见状态v'

  4. 使用(3 )生成一个抽样的隐藏状态h'

  5. 计算更新,这仅仅是可见和隐藏单元的相关性:

其中bc 分别是可见单元和隐藏单元的偏置项,e是学习率。

这种抽样被称为吉布斯抽样,这是一种方法,在这种方法中,我们一次只对分布的一个未知参数进行抽样,而将其他所有参数保持不变。在这里,我们在每一步中保持可见或隐藏的固定,并对单元进行抽样。

使用 CD,我们现在有了一种方法来执行梯度下降以学习我们的 RBM 模型的参数;事实证明,通过堆叠 RBM,我们可以潜在地计算出一个更好的模型,这就是所谓的 DBN。

堆叠受限玻尔兹曼机以生成图像:深度信念网络

你已经看到,具有单个隐藏层的 RBM 可用于学习图像的生成模型;事实上,理论工作表明,具有足够多的隐藏单元,RBM 可以近似表示任何具有二进制值的分布。¹⁹然而,在实践中,对于非常大的输入数据,添加额外的层可能比添加单个大层更有效,这允许对数据进行更"紧凑"的表示。

开发 DBNs 的研究人员还注意到,添加额外的层只会降低由生成模型重构的数据近似的下界的对数似然性。²⁰在这种情况下,第一层的隐藏层输出h 成为第二个 RBM 的输入;我们可以继续添加其他层来构建一个更深的网络。此外,如果我们希望使此网络能够学习不仅图像(x )的分布,还包括标签 - 它代表从 0 到 9 的哪个数字(y)-我们可以将另一个层添加到连接的 RBM 堆栈中,这是 10 个可能数字类的概率分布(softmax)。

训练非常深的图形模型,如堆叠 RBM,存在一个问题,即我们在第三章"深度神经网络的基本构件"中讨论过的"解释效果"。请注意,变量之间的依赖关系可能会使对隐藏变量状态的推断变得复杂:

图 4.8:贝叶斯网络中的解释效果²¹

图 4.8 中,知道路面潮湿可以被解释为打开了洒水器,以至于下雨与否变得无关紧要,这意味着我们无法有意义地推断下雨的概率。这相当于说隐藏单元的后验分布(第一章生成式人工智能简介:"从模型中抽取"数据)无法被可计算,因为它们是相关的,这会干扰对 RBM 的隐藏状态进行轻松抽样。

一种解决方案是在似然函数中将每个单元视为独立的,这被称为变分推断;虽然这在实践中有效,但鉴于我们知道这些单元实际上是相关的,这并不是一个令人满意的解决方案。

但这种相关性是从哪里来的呢?如果我们在单层 RBM 中对可见单元的状态进行抽样,我们会将每个隐藏单元的状态随机设置,因为它们是独立的;因此,隐藏单元的先验分布 是独立的。那么后验为何会相关呢?正如知道(数据)路面潮湿会导致洒水器和下雨天气的概率之间存在相关性一样,像素值之间的相关性导致隐藏单元的后验分布不是独立的。这是因为图像中的像素并非随机设置;根据图像代表的数字,像素组更有可能是明亮或黑暗的。在 2006 年的论文A Fast Learning Algorithm for Deep Belief Nets 中,作者假设可以通过计算一个互补先验来解决这个问题,该先验与似然完全相反,从而抵消这种相关性,并使后验也独立。

要计算这个互补先验 ,我们可以使用一个更高层次的隐藏单元的后验分布。生成这种分布的技巧在一个贪婪的、逐层的程序中,用于在多层生成模型中"初始化"堆叠的 RBM 网络,从而可以将权重微调为分类模型。例如,让我们考虑一个用于 MNIST 数据的三层模型(图 4.9):

图 4.9:基于 "A fast learning algorithm for deep belief nets" 的 DBN 架构由 Hinton 等人提出。

两个 500 单元层形成了 MNIST 数字的表示,而 2000 和 10 单元层是"关联记忆",将标签与数字表示相关联。前两层具有定向连接(不同的权重)用于上采样和下采样,而顶层具有无向权重(前向和后向传递使用相同的权重)。

这个模型可以分阶段学习。对于第一个 500 单元 RBM,我们会将其视为一个无向模型,强制前向和反向权重相等;然后我们将使用 CD 来学习这个 RBM 的参数。然后,我们会固定这些权重,并学习一个第二个(500 单元)RBM,它使用第一层的隐藏单元作为输入"数据",然后重复这个过程,直到 2000 层。

在我们"启动"网络之后,我们就不再需要强制底层的权重是绑定的,并且可以使用称为"wake-sleep"的算法来微调权重。²³

首先,我们接受输入数据(数字)并计算其他层的激活,一直到 2000 个单元和 10 个单元层之间的连接。我们使用先前给出的梯度方程计算指向下的"生成权重"(计算从网络生成图像数据的激活)的更新。这是"唤醒"阶段,因为如果我们将网络视为类似生物感知系统,则它通过这个前向传递从环境中接收输入。

对于 2000 个单元和 10 个单元的层,我们使用 CD 的采样过程,使用第二个 500 单元层的输出作为"数据"来更新无向权重。

然后我们取 2000 层单元的输出并向下计算激活,更新指向上的"识别权重"(计算激活以将图像分类为数字类别之一的权重)。这被称为"睡眠"阶段,因为它显示的是网络的"记忆",而不是从外部获取数据。

然后我们重复这些步骤直到收敛。

注意在实践中,我们可以在网络的顶层替换最后一层的无向权重为有向连接和 softmax 分类器。这个网络在技术上就不再是 DBN,而是一个可以用反向传播优化的普通深度神经网络。这是我们在自己的代码中要采取的方法,因为我们可以利用 TensorFlow 内置的梯度计算,并且它符合模型 API 的范例。

现在我们已经了解了 DBN 的训练方式以及预训练方法如何解决"解释"效应的问题的理论背景,我们将在代码中实现整个模型,展示如何利用 TensorFlow 2 的梯度带功能来实现 CD 作为自定义学习算法。

使用 TensorFlow Keras 层 API 创建 RBM

现在您已经了解了 RBM 的一些理论基础,让我们看看如何使用 TensorFlow 2.0 库实现它。为此,我们将使用 Keras 层 API 将 RBM 表示为自定义层类型。

本章的代码是从 deeplearning.net 的原始 Theano 代码转换到 TensorFlow 2 的。

首先,我们扩展tf.keras.layer

py 复制代码
from tensorflow.keras import layers
import tensorflow_probability as tfp
class RBM(layers.Layer):
    def __init__(self, number_hidden_units=10, number_visible_units=None, learning_rate=0.1, cd_steps=1):
        super().__init__()
        self.number_hidden_units = number_hidden_units
        self.number_visible_units = number_visible_units
        self.learning_rate = learning_rate
        self.cd_steps = cd_steps 

我们输入一定数量的隐藏单元、可见单元、用于 CD 更新的学习率以及每次 CD 传递中采取的步骤数。对于层 API,我们只需要实现两个函数:build()call()。当我们调用 model.compile() 时执行 build(),并用于初始化网络的权重,包括根据输入维度推断权重的正确大小:

py 复制代码
def build(self, input_shape):
    if not self.number_visible_units:
        self.number_visible_units = input_shape[-1]
        self.w_rec = self.add_weight(shape=(self.number_visible_units, self.number_hidden_units),
                          initializer='random_normal',
                          trainable=True)
        self.w_gen = self.add_weight(shape=(self.number_hidden_units, self.number_visible_units),
                           initializer='random_normal',
                           trainable=True)
        self.hb = self.add_weight(shape=(self.number_hidden_units, ),
                           initializer='random_normal',
                           trainable=True)
        self.vb = self.add_weight(shape=(self.number_visible_units, ),
                           initializer='random_normal',
                           trainable=True) 

我们还需要一种方法来执行模型的前向和反向采样。对于前向传播,我们需要从输入计算 S 型激活,然后根据由该 S 型激活给出的介于 1 和 0 之间的激活概率,随机打开或关闭隐藏单元:

py 复制代码
def forward(self, x):
    return tf.sigmoid(tf.add(tf.matmul(x, self.w), self.hb))
def sample_h(self, x):
    u_sample = tfp.distributions.Uniform().sample((x.shape[1], 
                                                   self.hb.shape[-1]))
    return tf.cast((x) > u_sample, tf.float32) 

同样,我们需要一种方式来为可见单元进行反向采样:

py 复制代码
def reverse(self, x):
    return tf.sigmoid(tf.add(tf.matmul(x, self.w_gen), self.vb))
def sample_v(self, x):
    u_sample = tfp.distributions.Uniform().sample((x.shape[1],
                                           self.vb.shape[-1]))
    return tf.cast(self.reverse(x) > u_sample, tf.float32) 

我们还在 RBM 类中实现了 call(),它提供了我们将在深度信念模型的微调中使用 fit() 方法时使用的前向传播:

py 复制代码
def call(self, inputs):
    return tf.sigmoid(tf.add(tf.matmul(inputs, self.w), self.hb)) 

要为每个受限玻尔兹曼机实际实现 CD 学习,我们需要创建一些额外的函数。第一个函数计算自由能,就像你在本章前面看到的 Gibbs 测度那样:

py 复制代码
def free_energy(self, x):
    return -tf.tensordot(x, self.vb, 1)\
    -tf.reduce_sum(tf.math.log(1+tf.math.exp(tf.add(tf.matmul(x, self.w), self.hb))), 1) 

请注意,我们本可以使用 tensorflow_probability 中的伯努利分布来执行此采样,使用 S 型激活作为概率;然而,这样做速度很慢,在进行 CD 学习时会导致性能问题。相反,我们使用了一种加速方法,在这种方法中,我们对与 S 型数组大小相同的均匀随机数数组进行采样,然后如果隐藏单元大于随机数,则将其设置为 1。因此,如果 S 型激活为 0.9,则它有 90% 的概率大于随机抽样的均匀数,并被设置为"打开"。这与以概率 0.9 采样伯努利变量的行为相同,但在计算上要高效得多。反向和可见样本的计算方式类似。最后,将这些放在一起允许我们执行前向和后向 Gibbs 采样:

py 复制代码
def reverse_gibbs(self, x):
    return self.sample_h(self.sample_v(x)) 

要执行 CD 更新,我们利用 TensorFlow 2 的即时执行和 第三章深度神经网络的构建块 中看到的 GradientTape API:

py 复制代码
def cd_update(self, x):
    with tf.GradientTape(watch_accessed_variables=False) as g:
        h_sample = self.sample_h(x)
        for step in range(self.cd_steps):
            v_sample = tf.constant(self.sample_v(h_sample))
            h_sample = self.sample_h(v_sample) 
        g.watch(self.w_rec)
        g.watch(self.hb)
        g.watch(self.vb)
        cost = tf.reduce_mean(self.free_energy(x)) - tf.reduce_mean(self.free_energy(v_sample))
        w_grad, hb_grad, vb_grad = g.gradient(cost, [self.w_rec, self.hb, self.vb])
        self.w_rec.assign_sub(self.learning_rate * w_grad)
        self.w_gen = tf.Variable(tf.transpose(self.w_rec)) # force
                                                           # tieing
        self.hb.assign_sub(self.learning_rate * hb_grad)
        self.vb.assign_sub(self.learning_rate * vb_grad)
        return self.reconstruction_cost(x).numpy() 

我们执行一步或多步样本,并使用数据的自由能与重建数据之间的差异计算成本(使用 tf.constant 将其转换为常数,以便在自动梯度计算期间不将其视为变量)。然后,我们计算三个权重矩阵的梯度并更新其值,然后返回我们的重建成本作为监视进度的一种方式。重建成本只是输入和重建数据之间的交叉熵损失:

py 复制代码
def reconstruction_cost(self, x):
        return tf.reduce_mean(
            tf.reduce_sum(tf.math.add(
            tf.math.multiply(x,tf.math.log(self.reverse(self.forward(x)))),
tf.math.multiply(tf.math.subtract(1,x),tf.math.log(tf.math.subtract(1,self.reverse(self.forward(x)))))
        ), 1),) 

这代表着公式:

这里 y 是目标标签,y-hat 是从 softmax 函数估计的标签,N 是数据集中元素的数量。

请注意,我们通过将更新(识别)权重的转置值复制到生成权重中来强制权重相等。在后续的唤醒-睡眠过程中,保持两组权重分开将会很有用,因为我们只会对识别(前向)或生成(反向)权重进行更新。

将所有这些放在一起,我们可以像在 Hinton 的论文 24 中那样初始化一个具有 500 个单位的 RBM,调用 build() 并传递 MNIST 数字的扁平化形状,并运行连续的训练周期:

py 复制代码
rbm = RBM(500)
rbm.build([784])
num_epochs=100
def train_rbm(rbm=None, data=mnist_train, map_fn=flatten_image, 
              num_epochs=100, tolerance=1e-3, batch_size=32, shuffle_buffer=1024):
    last_cost = None

    for epoch in range(num_epochs):
        cost = 0.0
        count = 0.0
        for datapoints in data.map(map_fn).shuffle(shuffle_buffer).batch(batch_size):
            cost += rbm.cd_update(datapoints)
            count += 1.0
        cost /= count
        print("epoch: {}, cost: {}".format(epoch, cost))
        if last_cost and abs(last_cost-cost) <= tolerance:
            break
        last_cost = cost

    return rbm

rbm = train_rbm(rbm, mnist_train, partial(flatten_image, label=False), 100, 0.5, 2000) 

大约 25 步后,模型应该会收敛,我们可以检查结果。一个感兴趣的参数是权重矩阵 w ;形状为 784(28x28)乘以 500,因此我们可以将每个"列"看作是一个 28x28 的滤波器,类似于我们在 第三章深度神经网络的构建块 中学习的卷积网络中的卷积核。我们可以可视化其中一些,看看它们在图像中识别出了什么样的模式:

py 复制代码
fig, axarr = plt.subplots(10,10)
plt.axis('off')
for i in range(10):
    for j in range(10):
        fig.axes[i*10+j].get_xaxis().set_visible(False)
        fig.axes[i*10+j].get_yaxis().set_visible(False)
        axarr[i,j].imshow(rbm.w_rec.numpy()[:,i*10+j].reshape(28,28), cmap=plt.get_cmap("gray")) 

这提供了一组滤波器:

图 4.10:训练后的 DBN 滤波器

我们可以看到这些滤波器似乎代表了我们在数字图像中找到的不同形状,比如曲线或线条。我们还可以通过从我们的数据中进行采样观察图像的重建:

py 复制代码
i=0
for image, label in mnist_train.map(flatten_image).batch(1).take(10):
    plt.figure(i)
    plt.imshow(rbm.forward_gibbs(image).numpy().reshape(28,28).astype(np.float32), cmap=plt.get_cmap("gray"))
    i+=1
    plt.figure(i)
    plt.imshow(image.numpy().reshape(28,28).astype(np.float32), 
               cmap=plt.get_cmap("gray"))
    i+=1 

图 4.11:DBN 中的原始(右)和重建(左)数字

我们可以在 图 4.11 中看到,网络已经很好地捕捉到了底层的数据分布,因为我们的样本代表了输入图像的可识别的二进制形式。现在我们已经有了一个工作层,让我们继续将多个 RBM 结合在一起以创建一个更强大的模型。

使用 Keras 模型 API 创建 DBN

现在你已经看到如何创建一个单层 RBM 来生成图像;这是创建一个完整的 DBN 所需的基本模块。通常情况下,对于 TensorFlow 2 中的模型,我们只需要扩展 tf.keras.Model 并定义一个初始化(其中定义了层)和一个 call 函数(用于前向传播)。对于我们的 DBN 模型,我们还需要一些自定义函数来定义其行为。

首先,在初始化中,我们需要传递一个包含我们的 RBM 层参数的字典列表(number_hidden_unitsnumber_visible_unitslearning_ratecd_steps):

py 复制代码
class DBN(tf.keras.Model):
    def __init__(self, rbm_params=None, name='deep_belief_network', 
                 num_epochs=100, tolerance=1e-3, batch_size=32, shuffle_buffer=1024, **kwargs):
        super().__init__(name=name, **kwargs)
        self._rbm_params = rbm_params
        self._rbm_layers = list()
        self._dense_layers = list()
        for num, rbm_param in enumerate(rbm_params):
            self._rbm_layers.append(RBM(**rbm_param))
            self._rbm_layers[-1].build([rbm_param["number_visible_units"]])
            if num < len(rbm_params)-1:
                self._dense_layers.append(
                    tf.keras.layers.Dense(rbm_param["number_hidden_units"], activation=tf.nn.sigmoid))
            else:
                self._dense_layers.append(
                    tf.keras.layers.Dense(rbm_param["number_hidden_units"], activation=tf.nn.softmax))
            self._dense_layers[-1].build([rbm_param["number_visible_units"]])
        self._num_epochs = num_epochs
        self._tolerance = tolerance
        self._batch_size = batch_size
        self._shuffle_buffer = shuffle_buffer 

与此同时请注意,我们还初始化了一组带有 softmax 的 sigmoid 密集层,我们可以在使用之前概述的生成过程训练模型后通过反向传播进行微调。要训练 DBN,我们开始一个新的代码块来启动 RBM 堆栈的生成学习过程:

py 复制代码
# pretraining:

        inputs_layers = []
        for num in range(len(self._rbm_layers)):
            if num == 0:
                inputs_layers.append(inputs)
                self._rbm_layers[num] = \
                    self.train_rbm(self._rbm_layers[num],
                                   inputs)
            else:  # pass all data through previous layer
                inputs_layers.append(inputs_layers[num-1].map(
                    self._rbm_layers[num-1].forward))
                self._rbm_layers[num] = \
                    self.train_rbm(self._rbm_layers[num],
                                   inputs_layers[num]) 

为了计算效率,我们通过使用 Dataset API 中的 map() 函数,在前向传递中将每个数据点通过前一层传递以生成除第一层以外每一层的输入,而不是反复生成这些前向样本。尽管这需要更多的内存,但大大减少了所需的计算量。预训练循环中的每一层都会回调到你之前看到的 CD 循环,它现在是 DBN 类的成员函数:

py 复制代码
def train_rbm(self, rbm, inputs,
              num_epochs, tolerance, batch_size, shuffle_buffer):
    last_cost = None
    for epoch in range(num_epochs):
        cost = 0.0
        count = 0.0
        for datapoints in inputs.shuffle(shuffle_buffer).batch(batch_size).take(1):
            cost += rbm.cd_update(datapoints)
            count += 1.0
        cost /= count
        print("epoch: {}, cost: {}".format(epoch, cost))
        if last_cost and abs(last_cost-cost) <= tolerance:
            break
        last_cost = cost
    return rbm 

一旦我们以贪婪的方式进行了预训练,我们就可以进行wake-sleep步骤。我们从向上传递开始:

py 复制代码
# wake-sleep:

    for epoch in range(self._num_epochs):
        # wake pass
        inputs_layers = []
        for num, rbm in enumerate(self._rbm_layers):
            if num == 0:
                inputs_layers.append(inputs)
            else:
                inputs_layers.append(inputs_layers[num-1].map(self._rbm_layers[num-1].forward))
        for num, rbm in enumerate(self._rbm_layers[:-1]):
            cost = 0.0
            count = 0.0
            for datapoints in inputs_layers[num].shuffle(
                self._shuffle_buffer).batch(self._batch_size):
                cost += self._rbm_layers[num].wake_update(datapoints)
                count += 1.0
            cost /= count
            print("epoch: {}, wake_cost: {}".format(epoch, cost)) 

再次注意,我们收集了在每个阶段转换的前向传递的列表,以便我们具有更新公式所需的必要输入。我们现在已经向 RBM 类添加了一个函数,wake_update,它将仅为生成(向下)权重计算更新,即除了最后一层(关联的,无向连接)之外的每一层:

py 复制代码
def wake_update(self, x):
    with tf.GradientTape(watch_accessed_variables=False) as g:
        h_sample = self.sample_h(x)
        for step in range(self.cd_steps):
            v_sample = self.sample_v(h_sample)
            h_sample = self.sample_h(v_sample)
        g.watch(self.w_gen)
        g.watch(self.vb)
        cost = tf.reduce_mean(self.free_energy(x)) - tf.reduce_mean(self.free_energy_reverse(h_sample))
    w_grad, vb_grad = g.gradient(cost, [self.w_gen, self.vb])

    self.w_gen.assign_sub(self.learning_rate * w_grad)
    self.vb.assign_sub(self.learning_rate * vb_grad)
    return self.reconstruction_cost(x).numpy() 

这与 CD 更新几乎相同,不同之处在于我们仅更新生成权重和可见单元偏置项。一旦我们计算了前向传递,然后对顶层的关联内存执行对比更新:

py 复制代码
# top-level associative:
        self._rbm_layers[-1] = self.train_rbm(self._rbm_layers[-1],
            inputs_layers[-2].map(self._rbm_layers[-2].forward), 
            num_epochs=self._num_epochs, 
            tolerance=self._tolerance, batch_size=self._batch_size, 
            shuffle_buffer=self._shuffle_buffer) 

然后我们需要计算wake-sleep算法的逆向传递数据;我们通过再次对最后一层的输入应用映射来实现这一点:

py 复制代码
reverse_inputs = inputs_layers[-1].map(self._rbm_layers[-1].forward) 

对于睡眠传递,我们需要反向遍历 RBM,仅更新非关联(无向)连接。我们首先需要逆向映射每一层所需的输入:

py 复制代码
reverse_inputs_layers = []
        for num, rbm in enumerate(self._rbm_layers[::-1]):
            if num == 0:
                reverse_inputs_layers.append(reverse_inputs)
            else:
                reverse_inputs_layers.append(
                    reverse_inputs_layers[num-1].map(
                    self._rbm_layers[len(self._rbm_layers)-num].reverse)) 

然后我们对层进行反向遍历,仅更新非关联连接:

py 复制代码
for num, rbm in enumerate(self._rbm_layers[::-1]):
            if num > 0:
                cost = 0.0
                count = 0.0
                for datapoints in reverse_inputs_layers[num].shuffle(
                    self._shuffle_buffer).batch(self._batch_size):
                    cost += self._rbm_layers[len(self._rbm_layers)-1-num].sleep_update(datapoints)
                    count += 1.0
                cost /= count
                print("epoch: {}, sleep_cost: {}".format(epoch, cost)) 

一旦我们对训练进展满意,我们就可以使用常规反向传播进一步调整模型。wake-sleep过程中的最后一步是将所有稠密层设置为来自 RBM 层的训练权重的结果:

py 复制代码
for dense_layer, rbm_layer in zip(dbn._dense_layers, dbn._rbm_layers):
    dense_layer.set_weights([rbm_layer.w_rec.numpy(), rbm_layer.hb.numpy()] 

我们已经在 DBN 类中使用 function() 调用包含了神经网络的前向传递:

py 复制代码
def call(self, x, training):
    for dense_layer in self._dense_layers:
        x = dense_layer(x)
    return x 

这可以在 TensorFlow API 中的 fit() 调用中使用:

py 复制代码
dbn.compile(loss=tf.keras.losses.CategoricalCrossentropy())
dbn.fit(x=mnist_train.map(lambda x: flatten_image(x, label=True)).batch(32), ) 

这开始使用反向传播来训练现在预先训练过的权重,以微调模型的判别能力。概念上理解这种微调的一种方式是,预训练过程引导权重到一个合理的配置,以捕捉数据的"形状",然后反向传播可以调整这些权重以适应特定的分类任务。否则,从完全随机的权重配置开始,参数距离捕捉数据中的变化太远,无法通过单独的反向传播有效地导航到最佳配置。

您已经了解了如何将多个 RBM 结合在层中创建 DBN,并如何使用 TensorFlow 2 API 在端到端模型上运行生成式学习过程;特别是,我们利用梯度磁带允许我们记录和重放梯度使用非标准优化算法(例如,不是 TensorFlow API 中的默认优化器之一),使我们能够将自定义梯度更新插入 TensorFlow 框架中。

摘要

在本章中,您了解了深度学习革命开始时最重要的模型之一,即 DBN。您看到 DBN 是通过堆叠 RBM 构建的,以及如何使用 CD 训练这些无向模型。

该章节随后描述了一种贪婪的逐层过程,通过逐个训练一堆 RBM 来启动 DBN,然后可以使用唤醒-睡眠算法或反向传播进行微调。然后,我们探讨了使用 TensorFlow 2 API 创建 RBM 层和 DBN 模型的实际示例,说明了使用GradientTape类计算使用 CD 更新的方法。

您还学习了如何根据唤醒-睡眠算法,将 DBN 编译为正常的深度神经网络,并对其进行监督训练的反向传播。我们将这些模型应用于 MNIST 数据,并看到 RBM 在训练收敛后如何生成数字,并具有类似于第三章深度神经网络的构建基块中描述的卷积滤波器的特征。

虽然本章中的示例显著扩展了 TensorFlow Keras API 的基本层和模型类,但它们应该能够让你了解如何实现自己的低级替代训练过程。未来,我们将主要使用标准的fit()predict()方法,从我们的下一个主题开始,变分自动编码器,这是一种复杂且计算效率高的生成图像数据的方式。

参考文献

  1. LeCun, Yann; Léon Bottou; Yoshua Bengio; Patrick Haffner (1998). 基于梯度的学习应用于文档识别。IEEE 会议录。86 (11): 2278--2324

  2. LeCun, Yann; Corinna Cortes; Christopher J.C. Burges。 MNIST 手写数字数据库,Yann LeCun,Corinna Cortes 和 Chris Burges

  3. NIST 的原始数据集:www.nist.gov/system/files/documents/srd/nistsd19.pdf

  4. upload.wikimedia.org/wikipedia/commons/thumb/2/27/MnistExamples.png/440px-MnistExamples.png

  5. LeCun, Yann; Léon Bottou; Yoshua Bengio; Patrick Haffner (1998). 基于梯度的学习应用于文档识别。IEEE 会议录。86 (11): 2278--2324

  6. D. Ciregan, U. Meier 和 J. Schmidhuber, (2012) 用于图像分类的多列深度神经网络 ,2012 年 IEEE 计算机视觉和模式识别会议,pp. 3642-3649. ieeexplore.ieee.org/document/6248110

  7. Hinton GE, Osindero S, Teh YW (2006) 深度信念网络的快速学习算法 。神经计算。18(7):1527-54. www.cs.toronto.edu/~hinton/absps/fastnc.pdf

  8. Hebb, D. O. (1949). 行为的组织:一个神经心理学理论。纽约:Wiley and Sons

  9. Gurney, Kevin (2002). 神经网络简介. Routledge

  10. Sathasivam, Saratha (2008). 霍普菲尔德网络中的逻辑学习.

  11. Hebb, D. O.。行为的组织:一个神经心理学理论。劳伦斯埃尔巴姆出版,2002 年

  12. Suzuki, Wendy A. (2005). 联想学习与海马体 . 心理科学日程。美国心理协会。www.apa.org/science/about/psa/2005/02/suzuki

  13. Hammersley, J. M.; Clifford, P. (1971),有限图和晶格上的马尔可夫场;Clifford, P. (1990),统计学中的马尔可夫随机场 ,在 Grimmett, G. R.; Welsh, D. J. A. (eds.),物理系统中的无序:纪念约翰 M. Hammersley 专著,牛津大学出版社,pp. 19-32

  14. Ackley, David H; Hinton, Geoffrey E; Sejnowski, Terrence J (1985),玻尔兹曼机的学习算法 (PDF),认知科学,9(1):147--169

  15. 玻尔兹曼机 . 维基百科. 检索日期:2021 年 4 月 26 日,来自en.wikipedia.org/wiki/Boltzmann_machine

  16. Smolensky, Paul (1986). 第六章:动力系统中的信息处理:谐和理论的基础 (PDF). 在 Rumelhart,David E.; McLelland, James L. (eds.) 平行分布式处理:认知微结构探索,第 1 卷:基础。麻省理工学院出版社。pp.194--281

  17. Woodford O. 对比散度笔记 . www.robots.ox.ac.uk/~ojw/files/NotesOnCD.pdf

  18. Hinton, G E. (2000). 通过最小化对比散度来训练专家乘积 . www.cs.utoronto.ca/~hinton/absps/nccd.pdf

  19. Roux, N L.,Bengio, Y. (2008). 受限玻尔兹曼机和深度信念网络的表示能力 . 在神经计算,卷 20,第 6 期,pp. 1631-1649. www.microsoft.com/en-us/research/wp-content/uploads/2016/02/representational_power.pdf

  20. Hinton, G E. (2000). 通过最小化对比散度来训练专家乘积 . www.cs.utoronto.ca/~hinton/absps/nccd.pdf

  21. Pearl J., Russell S. (2000). 贝叶斯网络 . ftp.cs.ucla.edu/pub/stat_ser/r277.pdf

  22. Hinton GE, Osindero S, Teh YW. (2006) 深信念网络的快速学习算法 . Neural Comput. 18(7):1527-54. www.cs.toronto.edu/~hinton/absps/fastnc.pdf

  23. Hinton GE, Osindero S, Teh YW. (2006) 深信念网络的快速学习算法 . Neural Comput. 18(7):1527-54. www.cs.toronto.edu/~hinton/absps/fastnc.pdf

  24. Hinton GE, Osindero S, Teh YW. (2006) 深信念网络的快速学习算法 . Neural Comput. 18(7):1527-54. www.cs.toronto.edu/~hinton/absps/fastnc.pdf

第五章:使用 VAEs 用神经网络绘制图片

正如您在 第四章 中所看到的,教网络生成数字,深度神经网络是创建复杂数据的生成模型的强大工具,允许我们开发一个网络,该网络可以从 MNIST 手写数字数据库生成图像。在那个例子中,数据相对简单;图像只能来自一组有限的类别(数字 0 到 9),并且是低分辨率灰度数据。

更复杂的数据呢,比如来自现实世界的彩色图像呢?这类"现实世界"的数据的一个例子是加拿大高级研究所 10 类数据集,简称为 CIFAR-10。¹ 它是来自 8000 万张图像更大数据集的 60,000 个样本的子集,分为十个类别------飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车。虽然在真实世界中我们可能会遇到的图像多样性方面仍然是一个极为有限的集合,但是这些类别具有一些特性,使它们比 MNIST 更复杂。例如,MNIST 数字可以在宽度、曲率和其他几个属性上变化;而 CIFAR-10 类别的动物或车辆照片有着更广泛的潜在变化范围,这意味着我们可能需要更复杂的模型来捕捉这种变化。

在本章中,我们将讨论一类称为变分自动编码器VAEs)的生成模型,这些模型旨在使生成这些复杂的现实世界图像更易于处理和调节。它们通过使用许多巧妙的简化方法来使得在复杂的概率分布上进行采样成为可能,从而可扩展。

我们将探讨以下主题以揭示 VAEs 的工作原理:

  • 神经网络如何创建数据的低维表示,以及这些表示的一些理想属性

  • 变分方法如何允许我们使用这些表示从复杂数据中进行采样

  • 如何使用重新参数化技巧来稳定基于变分采样的神经网络的方差 ------ 一个 VAE

  • 我们如何使用逆自回归流IAF)来调整 VAE 的输出

  • 如何在 TensorFlow 中实现 VAE/IAF

创建图像的可分离编码

图 5.1 中,您可以看到 CIFAR-10 数据集的图像示例,以及一个可以根据随机数输入生成这些图像模糊版本的早期 VAE 算法示例:

图 5.1:CIFAR-10 样本(左),VAE(右)²

对 VAE 网络的最新工作已经使得这些模型能够生成更好的图像,正如您将在本章后面看到的那样。首先,让我们重新审视生成 MNIST 数字的问题以及我们如何将这种方法扩展到更复杂的数据。

第一章生成式人工智能简介:"从模型中"绘制数据第四章教网络生成数字 中回想起,RBM(或 DBN)模型本质上涉及学习给定一些潜在"代码"(z )的图像(x )的后验概率分布,由网络的隐藏层表示,x的"边际可能性":³

我们可以将z 视为图像x 的"编码"(例如,RBM 中二进制隐藏单元的激活),可以解码(例如,反向运行 RBM 以对图像进行采样)以获得x的重构。如果编码"好",重构将接近原始图像。因为这些网络对其输入数据的表示进行编码和解码,它们也被称为"自编码器"。

深度神经网络捕捉复杂数据的基本结构的能力是它们最吸引人的特征之一;正如我们在第四章教网络生成数字 中所看到的 DBN 模型一样,它使我们能够通过为数据的分布创建更好的基础模型来提高分类器的性能。它还可以用于简单地创建一种更好的方法来"压缩"数据的复杂性,类似于经典统计学中的主成分分析PCA )。在图 5.2中,您可以看到堆叠的 RBM 模型如何用作编码面部分布的一种方式,例如。

我们从"预训练"阶段开始,创建一个 30 单位的编码向量,然后通过强制它重构输入图像来校准它,然后使用标准反向传播进行微调:

图 5.2:使用 DBN 作为自编码器⁴

作为堆叠的 RBM 模型如何更有效地表示图像分布的示例,从图 5.2 派生的论文用神经网络减少数据的维数的作者演示了使用两个单位代码来对比 MNIST 数字的 PCA:

图 5.3:MNIST 数字的 PCA 与 RBM 自编码器对比⁵

在左边,我们看到使用二维 PCA 编码的数字 0-9(由不同的阴影和形状表示)。回想一下,PCA 是使用数据的协方差矩阵的低维分解生成的:

Cov(X) 的高度/宽度与数据相同(例如,在 MNIST 中为 28 x 28 像素),而 UV 都是较低维度的(M x kk x M ),其中 k 远小于 M 。由于它们在一个维度上具有较少的行/列数 kUV 是数据的低维表示,我们可以通过将其投影到这些 k 向量上来获得对单个图像的编码,从而给出了数据的 k 单位编码。由于分解(和投影)是线性变换(两个矩阵的乘积),PCA 组件有效区分数据的能力取决于数据是否线性可分(我们可以通过组之间的空间绘制一个超平面---该空间可以是二维的或 N 维的,例如 MNIST 图像中的 784 个像素)。

图 5.3 所示,PCA 为图像生成了重叠的代码,表明使用二分量线性分解来表示数字是具有挑战性的,其中表示相同数字的向量彼此靠近,而表示不同数字的向量明显分开。从概念上讲,神经网络能够捕捉更多表示不同数字的图像之间的变化,如其在二维空间中更清晰地分离这些数字的表示所示。

为了理解这一现象,可以将其类比为一个非常简单的二维数据集,由平行双曲线(二次多项式)组成(图 5.4):

图 5.4:平行双曲线和可分离性

在顶部,即使我们有两个不同的类别,我们也无法在二维空间中画一条直线将两个组分开;在神经网络中,单个层中的权重矩阵在 sigmoid 或 tanh 的非线性转换之前本质上是这种类型的线性边界。然而,如果我们对我们的 2D 坐标应用非线性变换,比如取超半径的平方根,我们可以创建两个可分离的平面(图 5.4底部)。

在 MNIST 数据中存在类似的现象:我们需要一个神经网络来将这些 784 位数的图像放置到不同的、可分离的空间区域中。这个目标通过对原始的、重叠的数据执行非线性变换来实现,其中的目标函数奖励增加编码不同数字图像的向量之间的空间分离。因此,可分离的表示增加了神经网络利用这些表示区分图像类别的能力。因此,在 图 5.3 中,我们可以看到右侧应用 DBN 模型创建所需的非线性变换以分离不同的图像。

现在我们已经讨论了神经网络如何将数据压缩为数值向量以及这些向量表示的一些理想特性,我们将探讨如何在这些向量中最佳压缩信息。为此,向量的每个元素应该从其他元素中编码出不同的信息,我们可以使用变分目标这一属性来实现。这个变分目标是创建 VAE 网络的基础。

变分目标

我们之前讨论了几个例子,展示了如何使用神经网络将图像压缩成数值向量。这一部分将介绍能够让我们从随机数值向量空间中采样新图像的要素,主要是有效的推理算法和适当的目标函数。让我们更加严谨地量化什么样的编码才能让其"好",并且能够很好地重现图像。我们需要最大化后验概率:

x 的概率非常高维时会出现问题,如你所见,在甚至是简单的数据中,如二元 MNIST 数字中,我们有2^ (像素数)可能的配置,我们需要对其进行积分(在概率分布意义上进行积分)以得到对单个图像概率的度量;换句话说,密度p (x )是棘手的,导致了依赖于p (x )的后验p (z |x)也同样不容易处理。

在某些情况下,正如你在第四章 中看到的,训练网络生成数字 ,我们可以使用简单的二元单元,例如对比散度来计算近似,这使我们可以计算梯度,即使我们无法计算封闭形式。然而,在处理非常大的数据集时也可能具有挑战性,我们需要对数据进行多次传递以计算使用对比散度CD )计算平均梯度,就像之前在第四章中看到的那样。

如果我们无法直接计算编码器p (z |x )的分布,也许我们可以优化一个足够"接近"的近似------让我们称之为q (z |x)。然后,我们可以使用一种度量来确定这两个分布是否足够接近。一个有用的接近度量是这两个分布是否编码了类似的信息;我们可以使用香农信息方程来量化信息:

考虑一下这为什么是一个很好的度量:随着p (x )的减少,事件变得更加罕见,因此事件的观察向系统或数据集传达更多信息,导致log (p (x ))的正值。相反,当事件的概率接近 1 时,该事件对数据集的编码信息变少,而log (p (x ))的值变为 0(图 5.5):

图 5.5:香农信息

因此,如果我们想要衡量两个分布pq中编码的信息之间的差异,我们可以使用它们的信息之差:

最后,如果我们想要找到分布在x 的所有元素上的信息差异的期望值,我们可以对p (x)取平均:

这个量被称为Kullback Leibler (KL ) 散度。它有一些有趣的特性:

  1. 它不是对称的:KL (p (x ), q (x ))一般来说不等于KL (q (x ), p (x)),所以"接近程度"是通过将一个分布映射到另一个分布的特定方向来衡量的。

  2. 每当q (x )和p (x )匹配时,这个项就是 0,意味着它们彼此之间的距离是最小的。同样,只有当pq 是相同的时候,KL (p (x ), q (x))才为 0。

  3. 如果q (x )为 0 或者p (x )为 0,那么KL 是未定义的;按照定义,它仅计算两个分布在x的范围内匹配的相对信息。

  4. KL始终大于 0。

如果我们要使用KL 散度来计算近似值q (z,x )对于我们不可计算的p (z |x)的拟合程度,我们可以写成:

和:

现在我们也可以写出我们不可计算的p (x )的表达式了:由于log (p (x ))不依赖于q (z |x ),对p (x )的期望值简单地是log (p (x ))。因此,我们可以用KL 散度表示 VAE 的目标,学习p (x)的边际分布:

第二项也被称为变分下限 ,也被称为证据下界ELBO );由于KL (q,p )严格大于 0,log (p(x ))严格大于或者(如果KL (q,p)为 0)等于这个值。

要解释这个目标在做什么,注意到期望引入了q (z |x )(编码 x )和p (x |z )p (z )(数据和编码的联合概率)之间的差异;因此,我们想要最小化一个下界,它实质上是编码的概率和编码与数据的联合概率之间的差距,误差项由KL (q,p )给出,这是一个可计算的近似和不可计算的编码器p (z |x )形式之间的差异。我们可以想象函数Q (z |x )和P (x |z )由两个深度神经网络表示;一个生成潜在代码zQ ),另一个从这个代码重建xP)。我们可以把这看作是一个自动编码器设置,就像上面的堆叠 RBM 模型一样,有一个编码器和解码器:

图 5.6:无再参数化 VAE 的自动编码器/解码器⁷

我们希望优化编码器 Q 和解码器 P 的参数,以最小化重构成本。其中一种方法是构造蒙特卡洛样本来使用梯度下降优化 Q 的参数:

我们从哪里抽样 z

然而,在实践中发现,可能需要大量的样本才能使这些梯度更新的方差稳定下来。

我们在这里也遇到了一个实际问题:即使我们可以选择足够的样本来获得对编码器的梯度的良好近似,但我们的网络包含一个随机的、不可微分的步骤(抽样 z ),我们无法通过反向传播来处理,就像我们无法通过反向传播来处理 第四章 中 RBN 中的随机单元一样。因此,我们的重构误差取决于 z 的样本,但我们无法通过生成这些样本的步骤进行端到端的网络调整。有没有办法我们可以创建一个可微分的解码器/编码器架构,同时减少样本估计的方差?VAE 的主要见解之一就是通过 "重新参数化技巧" 实现这一点。

重新参数化技巧

为了使我们能够通过我们的自编码器进行反向传播,我们需要将 z 的随机样本转换为一个确定性的、可微分的变换。我们可以通过将 z 重新参数化为一个噪声变量的函数来实现这一点:

一旦我们从 中抽样,z 中的随机性就不再取决于变分分布 Q (编码器)的参数,我们可以进行端到端的反向传播。我们的网络现在看起来像 图 5.7 ,我们可以使用 的随机样本(例如,标准正态分布)来优化我们的目标。这种重新参数化将 "随机" 节点移出了编码器/解码器框架,使我们能够通过整个系统进行反向传播,但它还有一个更微妙的优点;它减少了这些梯度的方差。请注意,在未重新参数化的网络中,z 的分布取决于编码器分布 Q 的参数;因此,当我们改变 Q 的参数时,我们也在改变 z 的分布,并且我们可能需要使用大量样本才能得到一个合理的估计。

通过重新参数化,z 现在仅取决于我们更简单的函数 g ,通过从标准正态分布中进行抽样引入随机性(这不依赖于 Q);因此,我们已经消除了一个有些循环的依赖,并使我们正在估计的梯度更加稳定:

图 5.7:重新参数化 VAE 的自编码器/解码器

现在你已经看到 VAE 网络是如何构建的,让我们讨论一种进一步改进这一算法的方法,使得 VAE 能够从复杂分布中取样:逆自回归流IAF)。

逆自回归流

在我们之前的讨论中,我们指出希望使用q (z |x )来近似"真实"的p (z |x ),这会允许我们生成数据的理想编码,并从中取样生成新的图像。到目前为止,我们假设q (z |x )有一个相对简单的分布,比如独立的高斯分布随机变量的向量(对角协方差矩阵上的非对角元素为 0)。这种分布有许多好处;因为它很简单,我们可以轻松地从随机正态分布中进行抽样生成新数据,并且因为它是独立的,我们可以分别调整潜在向量z的各个元素,以影响输出图像的各个部分。

然而,这样一个简单的分布可能不能很好地适应数据的期望输出分布,增加了p (z |x )和q (z |x )之间的KL 散度。我们能不能以某种方式保留q (z |x )的良好特性,但"变换"z ,以便它更多地捕捉表示x所需的复杂性呢?

一种方法是对z 应用一系列自回归变换,将其从一个简单分布转变为一个复杂分布;通过"自回归",我们指的是每个变换利用了前一次变换和当前数据来计算z 的更新版本。相比之下,我们上面介绍的基本 VAE 形式只有一个"变换":从z 到输出(虽然z 可能经过多层,但没有循环网络链接来进一步完善输出)。我们之前已经见过这样的变换,比如第三章中的 LSTM 网络,其中网络的输出是当前输入和先前时间步加权版本的组合。

我们之前讨论过的独立q (z |x )分布的吸引人之处是,例如独立的正态分布,它们在对数似然函数上有一个非常简单的表达式。这一特性对于 VAE 模型非常重要,因为其目标函数取决于对整个似然函数进行积分,而对于更复杂的对数似然函数来说,这可能是繁琐的。然而,通过约束一个经过一系列自回归变换的z ,我们得到了一个很好的特性,即第t 步的对数似然仅取决于t-1 ,因此雅可比矩阵(tt-1之间的偏导数的梯度矩阵)是下三角的,可以计算为一个和:

可以使用哪些种类的变换 f ?记住,在参数化技巧之后,z 是编码器 Q 输出的均值和标准差以及一个噪声元素 e 的函数:

如果我们应用连续的转换层,步骤 t 就变成了和前一层 z 与 sigmoid 输出 的逐元素乘积之和:

在实践中,我们使用神经网络转换来稳定每一步的均值估计:

图 5.8:IAF 网络⁶

再次注意,这种转换与第三章深度神经网络的基本构件 中讨论的 LSTM 网络的相似性。在图 5.8 中,除了均值和标准差之外,编码器 Q 还有另一个输出 (h ),用于对 z 进行采样。H 本质上是"辅助数据",它被传递到每一个连续的转换中,并且与在每一步计算的加权和一起,以一种类似 LSTM 的方式,表示网络的"持久记忆"。

导入 CIFAR

现在我们已经讨论了 VAE 算法的基本理论,让我们开始使用真实世界的数据集构建一个实际的例子。正如我们在介绍中讨论的,对于本章的实验,我们将使用加拿大高级研究所(CIFAR)10 数据集。¹⁰ 这个数据集中的图像是 8000 万个"小图像"数据集¹¹的一部分,其中大多数没有像 CIFAR-10 这样的类标签。对于 CIFAR-10,标签最初是由学生志愿者创建的¹²,而更大的小图像数据集允许研究人员为数据的部分提交标签。

像 MNIST 数据集一样,可以使用 TensorFlow 数据集的 API 下载 CIFAR-10:

py 复制代码
import tensorflow.compat.v2 as tf
import tensorflow_datasets as tfds
cifar10_builder = tfds.builder("cifar10")
cifar10_builder.download_and_prepare() 

这将把数据集下载到磁盘并使其可用于我们的实验。要将其拆分为训练集和测试集,我们可以使用以下命令:

py 复制代码
cifar10_train = cifar10_builder.as_dataset(split="train")
cifar10_test = cifar10_builder.as_dataset(split="test") 

让我们检查一下其中一幅图像,看看它是什么格式:

py 复制代码
cifar10_train.take(1) 

输出告诉我们数据集中每个图像的格式是 <DatasetV1Adapter shapes: {image: (32, 32, 3), label: ()}, types: {image: tf.uint8, label: tf.int64}>: 不像我们在第四章教网络生成数字中使用的 MNIST 数据集,CIFAR 图像有三个颜色通道,每个通道都有 32 x 32 个像素,而标签是一个从 0 到 9 的整数(代表 10 个类别中的一个)。我们也可以绘制图像来进行视觉检查:

py 复制代码
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
for sample in cifar10_train.map(lambda x: flatten_image(x, label=True)).take(1):
    plt.imshow(sample[0].numpy().reshape(32,32,3).astype(np.float32), 
               cmap=plt.get_cmap("gray")
              )
    print("Label: %d" % sample[1].numpy()) 

这给出了以下输出:

图 5.9:输出

像 RBM 模型一样,在这个示例中我们将构建的 VAE 模型的输出被缩放在 1 到 0 之间,并且接受图像的扁平版本,因此我们需要将每个图像转换为一个向量,并将其缩放到最大为 1:

py 复制代码
def flatten_image(x, label=False):
    if label:
        return (tf.divide(
            tf.dtypes.cast(
                tf.reshape(x["image"], (1, 32*32*3)), tf.float32), 
                    256.0),
                x["label"])
    else:
        return (
            tf.divide(tf.dtypes.cast(
                tf.reshape(x["image"], (1, 32*32*3)), tf.float32), 
                    256.0)) 

这导致每个图像都是长度为 3072 (32323) 的向量,在运行模型后,我们可以重塑它们以检查生成的图像。

从 TensorFlow 2 创建网络

现在我们已经下载了 CIFAR-10 数据集,将其拆分为测试和训练数据,并对其进行了重塑和重新缩放,我们已经准备好开始构建我们的 VAE 模型了。我们将使用 TensorFlow 2 中的 Keras 模块的相同 Model API。TensorFlow 文档中包含了使用卷积网络实现 VAE 的示例(www.tensorflow.org/tutorials/generative/cvae),我们将在此代码示例的基础上构建;然而,出于我们的目的,我们将使用基于原始 VAE 论文《自编码变分贝叶斯》(Auto-Encoding Variational Bayes)¹³ 的 MLP 层实现更简单的 VAE 网络,并展示如何将 TensorFlow 示例改进为也允许解码中的 IAF 模块。

在原始文章中,作者提出了两种用于 VAE 的模型,都是 MLP 前馈网络:高斯和伯努利,这些名称反映了 MLP 网络输出中使用的概率分布函数在它们的最终层中。伯努利 MLP 可以用作网络的解码器,从潜在向量 z 生成模拟图像 x。伯努利 MLP 的公式如下:

第一行是我们用于确定网络是否生成原始图像近似重建的交叉熵函数,而 y 是一个前馈网络,有两层:一个双曲正切变换,然后是一个 sigmoid 函数将输出缩放到 0 到 1 之间。回想一下,这种缩放是我们不得不将 CIFAR-10 像素从其原始值归一化的原因。

我们可以很容易地使用 Keras API 创建这个伯努利 MLP 网络:

py 复制代码
class BernoulliMLP(tf.keras.Model):
    def __init__(self, input_shape, name='BernoulliMLP', hidden_dim=10, latent_dim=10, **kwargs):
        super().__init__(name=name, **kwargs)
        self._h = tf.keras.layers.Dense(hidden_dim, 
                                        activation='tanh')
        self._y = tf.keras.layers.Dense(latent_dim, 
                                        activation='sigmoid')
    def call(self, x):
        return self._y(self._h(x)), None, None 

我们只需要指定单隐藏层和潜在输出 (z ) 的维度。然后,我们将前向传递指定为这两个层的组合。请注意,在输出中,我们返回了三个值,第二和第三个值均设置为 None。这是因为在我们的最终模型中,我们可以使用 BernoulliMLP 或 GaussianMLP 作为解码器。如果我们使用 GaussianMLP,则返回三个值,正如我们将在下文中看到的;本章中的示例利用了二进制输出和交叉熵损失,因此我们可以只使用单个输出,但我们希望两个解码器的返回签名匹配。

原始 VAE 论文中作者提出的第二种网络类型是高斯 MLP,其公式为:

此网络可以在网络中作为编码器(生成潜在向量z )或解码器(生成模拟图像x )使用。上述方程假定它用作解码器,对于编码器,我们只需交换xz变量。如您所见,这个网络有两种类型的层,一个隐藏层由输入的 tanh 变换给出,并且两个输出层,每个输出层由隐藏层的线性变换给出,这些输出层被用作对数正态似然函数的输入。像 Bernoulli MLP 一样,我们可以轻松使用 TensorFlow Keras API 实现这个简单网络:

py 复制代码
class GaussianMLP(tf.keras.Model):
    def __init__(self, input_shape, name='GaussianMLP', hidden_dim=10, latent_dim=10, iaf=False, **kwargs):
        super().__init__(name=name, **kwargs)
        self._h = tf.keras.layers.Dense(hidden_dim, 
                                        activation='tanh')
        self._mean = tf.keras.layers.Dense(latent_dim)
        self._logvar = tf.keras.layers.Dense(latent_dim)
        self._iaf_output = None
        if iaf:
            self._iaf_output = tf.keras.layers.Dense(latent_dim)
    def call(self, x):
        if self._iaf_output:
            return self._mean(self._h(x)), self._logvar(self._h(x)), 
                self._iaf_output(self._h(x))
        else:
            return self._mean(self._h(x)), self._logvar(self._h(x)), 
                None 

如您所见,要实现call函数,我们必须返回模型的两个输出(我们将用来计算zx 的正态分布的均值和对数方差)。然而,请注意,对于 IAE 模型,编码器必须具有额外的输出h,它被馈送到每一步的正规流中:

要允许额外的输出,我们在输出中包括了第三个变量,如果我们将 IAF 选项设置为True,它将被设置为输入的线性变换,如果为False,则为none,因此我们可以在具有和不具有 IAF 的网络中使用 GaussianMLP 作为编码器。

现在我们已经定义了我们的两个子网络,让我们看看如何使用它们来构建一个完整的 VAE 网络。像子网络一样,我们可以使用 Keras API 定义 VAE:

py 复制代码
class VAE(tf.keras.Model):    
    def __init__(self, input_shape, name='variational_autoencoder',
                 latent_dim=10, hidden_dim=10, encoder='GaussianMLP', 
                 decoder='BernoulliMLP', iaf_model=None,
                 number_iaf_networks=0,
                 iaf_params={},
                 num_samples=100, **kwargs):
        super().__init__(name=name, **kwargs)
        self._latent_dim = latent_dim
        self._num_samples = num_samples
        self._iaf = []
        if encoder == 'GaussianMLP':
            self._encoder = GaussianMLP(input_shape=input_shape, 
                                        latent_dim=latent_dim, 
                                        iaf=(iaf_model is not None), 
                                        hidden_dim=hidden_dim)
        else:
            raise ValueError("Unknown encoder type: {}".format(encoder))
        if decoder == 'BernoulliMLP':
            self._decoder = BernoulliMLP(input_shape=(1,latent_dim),
                                         latent_dim=input_shape[1], 
                                         hidden_dim=hidden_dim)
        elif decoder == 'GaussianMLP':
            self._encoder = GaussianMLP(input_shape=(1,latent_dim), 
                                        latent_dim=input_shape[1], 
                                        iaf=(iaf_model is not None), 
                                        hidden_dim=hidden_dim)
        else:
            raise ValueError("Unknown decoder type: {}".format(decoder))
        if iaf_model:
            self._iaf = []
            for t in range(number_iaf_networks):
                self._iaf.append(
                    iaf_model(input_shape==(1,latent_dim*2), 
                              **iaf_params)) 

如您所见,此模型被定义为包含编码器和解码器网络。此外,我们允许用户指定我们是否在模型中实现 IAF,如果是的话,我们需要一个由iaf_params变量指定的自回归变换的堆栈。因为这个 IAF 网络需要将zh 作为输入,输入形状是latent_dim (z)的两倍。我们允许解码器是 GaussianMLP 或 BernoulliMLP 网络,而编码器是 GaussianMLP。

此模型类还有一些其他函数需要讨论;首先是 VAE 模型类的编码和解码函数:

py 复制代码
def encode(self, x):
        return self._encoder.call(x)
    def decode(self, z, apply_sigmoid=False):
        logits, _, _ = self._decoder.call(z)
        if apply_sigmoid:
            probs = tf.sigmoid(logits)
            return probs
        return logits 

对于编码器,我们只需调用(运行前向传递)编码器网络。解码时,您会注意到我们指定了三个输出。介绍了 VAE 模型的文章《自编码变分贝叶斯》提供了解码器的例子,指定为高斯多层感知器MLP )或 Benoulli 输出。如果我们使用了高斯 MLP,解码器将为输出值、均值和标准差向量,我们需要使用 sigmoidal 变换将该输出转换为概率(0 到 1)。在伯努利情况下,输出已经在 0 到 1 的范围内,我们不需要这种转换(apply_sigmoid=False)。

一旦我们训练好了 VAE 网络,我们就需要使用抽样来产生随机潜在向量(z )并运行解码器来生成新图像。虽然我们可以将其作为 Python 运行时类的正常函数运行,但我们将使用@tf.function注释装饰这个函数,这样它就可以在 TensorFlow 图形运行时执行(就像任何 tf 函数一样,比如 reduce_summultiply),如果可用的话可以使用 GPU 和 TPU 等设备。我们从一个随机正态分布中抽取一个值,对于指定数量的样本,然后应用解码器来生成新图像:

py 复制代码
@tf.function
    def sample(self, eps=None):
        if eps is None:
            eps = tf.random.normal(shape=(self._num_samples, 
                                          self.latent_dim))
        return self._decoder.call(eps, apply_sigmoid=False) 

最后,回想一下,"重新参数化技巧"是用来使我们能够反向传播 z 的值,并减少 z 的似然的方差。我们需要实现这个变换,它的给定形式如下:

py 复制代码
def reparameterize(self, mean, logvar):
        eps = tf.random.normal(shape=mean.shape)
        return eps * tf.exp(logvar * .5) + mean 

在原始论文《Autoencoding Variational Bayes》中,给出了:

其中ix 中的一个数据点,l 是从随机分布中抽样的一个样本,这里是一个正常分布。在我们的代码中,我们乘以 0.5,因为我们在计算log variance (或标准差的平方),log ( )=log (s )2 ,所以 0.5 取消了 2,给我们留下了exp (log (s ))=s,正如我们在公式中需要的那样。

我们还将包括一个类属性(使用@property装饰器),这样我们就可以访问归一化变换的数组,如果我们实现了 IAF:

py 复制代码
@property
    def iaf(self):
        return self._iaf 

现在,我们将需要一些额外的函数来实际运行我们的 VAE 算法。第一个计算 lognormal 概率密度函数(pdf),用于计算变分下限或 ELBO:

py 复制代码
def log_normal_pdf(sample, mean, logvar, raxis=1):
    log2pi = tf.math.log(2\. * np.pi)
    return tf.reduce_sum(
          -.5 * ((sample - mean) ** 2\. * tf.exp(-logvar) + \
            logvar + log2pi), axis=raxis) 

现在我们需要利用这个函数作为在训练 VAE 的过程中每个小批量梯度下降传递的一部分来计算损失。和样本方法一样,我们将使用@tf.function注释装饰这个函数,以便它在图形运行时执行:

py 复制代码
@tf.function
def compute_loss(model, x):
    mean, logvar, h = model.encode(x)
    z = model.reparameterize(mean, logvar)
    logqz_x = log_normal_pdf(z, mean, logvar)
    for iaf_model in model.iaf:
        mean, logvar, _ = iaf_model.call(tf.concat([z, h], 2))
        s = tf.sigmoid(logvar)
        z = tf.add(tf.math.multiply(z,s), tf.math.multiply(mean,(1-s)))
        logqz_x -= tf.reduce_sum(tf.math.log(s))

    x_logit = model.decode(z)
    cross_ent = tf.nn.sigmoid_cross_entropy_with_logits(logits=x_logit, labels=x)
    logpx_z = -tf.reduce_sum(cross_ent, axis=[2])
    logpz = log_normal_pdf(z, 0., 0.)
    return -tf.reduce_mean(logpx_z + logpz - logqz_x) 

让我们来解开这里发生的一些事情。首先,我们可以看到我们在输入上调用编码器网络(在我们的情况下是扁平图像的小批量),生成所需的均值、logvariance,以及如果我们在网络中使用 IAF,我们将在归一化流变换的每一步传递的辅助输入h

我们对输入应用"重新参数化技巧",以生成潜在向量 z,并应用对得到的* logq*(z |x)的 lognormal pdf。

如果我们使用 IAF,我们需要通过每个网络迭代地变换z,并在每一步从解码器传入h(辅助输入)。然后我们将这个变换的损失应用到我们计算的初始损失上,就像在 IAF 论文中给出的算法中一样:¹⁴

一旦我们有了转换或未转换的z ,我们使用解码器网络解码它,得到重构数据x ,然后我们计算交叉熵损失。我们对小批量求和,并计算在标准正态分布(先验)处评估的z的对数正态 pdf,然后计算期望的下界。

请记住,变分下界或 ELBO 的表达式是:

因此,我们的小批量估计器是这个值的样本:

现在我们有了这些要素,我们可以使用GradientTape API 运行随机梯度下降,就像我们在第四章教授网络生成数字 中为 DBN 所做的那样,传入一个优化器、模型和数据的小批量(x):

py 复制代码
@tf.function
def compute_apply_gradients(model, x, optimizer):
    with tf.GradientTape() as tape:
        loss = compute_loss(model, x)
    gradients = tape.gradient(loss, model.trainable_variables)
    optimizer.apply_gradients(zip(gradients, model.trainable_variables)) 

要运行训练,首先我们需要指定一个使用我们构建的类的模型。如果我们不想使用 IAF,我们可以这样做:

py 复制代码
model = VAE(input_shape=(1,3072), hidden_dim=500, latent_dim=500) 

如果我们想要使用 IAF 变换,我们需要包含一些额外的参数:

py 复制代码
model = VAE(input_shape=(1,3072), hidden_dim=500, latent_dim=500, 
    iaf_model=GaussianMLP, number_iaf_networks=3, 
    iaf_params={'latent_dim': 500, 'hidden_dim': 500, 'iaf': False}) 

创建好模型后,我们需要指定一定数量的 epochs,一个优化器(在这个例子中,是 Adam,正如我们在第三章深度神经网络的构建基块中描述的那样)。我们将数据分成 32 个元素的小批量,并在每个小批量后应用梯度更新,更新的次数为我们指定的 epochs 数。定期输出 ELBO 的估计值以验证我们的模型是否在改善:

py 复制代码
import time as time
epochs = 100
optimizer = tf.keras.optimizers.Adam(1e-4)
for epoch in range(1, epochs + 1):
    start_time = time.time()
    for train_x in cifar10_train.map(
            lambda x: flatten_image(x, label=False)).batch(32):
        compute_apply_gradients(model, train_x, optimizer)
    end_time = time.time()
    if epoch % 1 == 0:
        loss = tf.keras.metrics.Mean()
        for test_x in cifar10_test.map(
            lambda x: flatten_image(x, label=False)).batch(32):
            loss(compute_loss(model, test_x))
    elbo = -loss.result()
    print('Epoch: {}, Test set ELBO: {}, '
          'time elapse for current epoch {}'.format(epoch,
                                                elbo,
                                                end_time - start_time)) 

我们可以通过查看更新来验证模型是否在改善,这些更新应该显示 ELBO 正在增加:

要检查模型的输出,我们可以首先查看重构误差;网络对输入图像的编码是否大致捕捉到了输入图像中的主要模式,从而使其能够从其向量z的编码中重构出来?我们可以将原始图像与通过编码器传递图像、应用 IAF,然后解码得到的重构进行比较:

py 复制代码
for sample in cifar10_train.map(lambda x: flatten_image(x, label=False)).batch(1).take(10):
    mean, logvar, h = model.encode(sample)
    z = model.reparameterize(mean, logvar)
    for iaf_model in model.iaf:
        mean, logvar, _ = iaf_model.call(tf.concat([z, h], 2))
        s = tf.sigmoid(logvar)
        z = tf.add(tf.math.multiply(z,s), tf.math.multiply(mean,(1-s)))    

    plt.figure(0)
    plt.imshow((sample.numpy().reshape(32,32,3)).astype(np.float32), 
               cmap=plt.get_cmap("gray"))
    plt.figure(1)
    plt.imshow((model.decode(z).numpy().reshape(32,32,3)).astype(np.float32), cmap=plt.get_cmap("gray")) 

对于前几个 CIFAR-10 图像,我们得到以下输出,显示我们已经捕捉到了图像的总体模式(尽管它是模糊的,这是 VAE 的一个普遍缺点,我们将在将来章节中讨论的**生成对抗网络(GANs)**中解决):

图 5.10:CIFAR-10 图像的输出

如果我们想要创建全新的图像怎么办?在这里,我们可以使用我们之前在从 TensorFlow 2 创建网络 中定义的"sample"函数,从随机生成的z向量而不是 CIFAR 图像的编码产品中创建新图像的批次:

py 复制代码
plt.imshow((model.sample(10)).numpy().reshape(32,32,3)).astype(np.float32), cmap=plt.get_cmap("gray")) 

此代码将生成类似于以下内容的输出,显示了从随机数向量生成的一组图像:

图 5.11:从随机数向量生成的图像

诚然,这些图像可能有些模糊,但您可以欣赏到它们显示的结构,并且看起来与您之前看到的一些"重建"CIFAR-10 图像相当。在这里的部分挑战,就像我们将在随后的章节中讨论的那样,是损失函数本身:交叉熵函数本质上对每个像素惩罚,以衡量其与输入像素的相似程度。虽然这在数学上可能是正确的,但它并不能捕捉到我们所认为的输入和重建图像之间的"相似性"的概念。例如,输入图像可能有一个像素设为无穷大,这将导致它与将该像素设为 0 的重建图像之间存在很大差异;然而,一个人观看这个图像时,会认为它们两者完全相同。GANs 所使用的目标函数,如第六章 中描述的用 GAN 生成图像,更准确地捕捉到了这种微妙之处。

总结

在本章中,您看到了如何使用深度神经网络来创建复杂数据的表示,例如图像,捕捉到比传统的降维技术如 PCA 更多的变化。这是通过 MNIST 数字进行演示的,其中神经网络可以在二维网格上更干净地分离不同数字,而不像这些图像的主成分那样。本章展示了如何使用深度神经网络来近似复杂的后验分布,例如图像,使用变分方法从不可约分布的近似中进行采样,形成了一种基于最小化真实和近似后验之间的变分下界的 VAE 算法。

您还学会了如何重新参数化这个算法生成的潜在向量,以降低方差,从而使随机小批量梯度下降收敛更好。您还看到了这些模型中编码器生成的潜在向量通常是相互独立的,可以使用 IAF 将其转换为更真实的相关分布。最后,我们在 CIFAR-10 数据集上实现了这些模型,并展示了它们如何用于重建图像和从随机向量生成新图像。

下一章将介绍 GANs,并展示我们如何使用它们为输入图像添加风格滤镜,使用 StyleGAN 模型。

参考文献

  1. Eckersley P., Nasser Y. 衡量 AI 研究的进展 。EFF。检索日期 2021 年 4 月 26 日,www.eff.org/ai/metrics#Measuring-the-Progress-of-AI-Research 和 CIFAR-10 数据集,www.cs.toronto.edu/~kriz/

  2. Malhotra P. (2018). 自编码器实现。GitHub 仓库。www.piyushmalhotra.in/Autoencoder-Implementations/VAE/

  3. Kingma, D P., Welling, M. (2014). 自动编码变分贝叶斯 . arXiv:1312.6114. arxiv.org/pdf/1312.6114.pdf

  4. Hinton G. E., Salakhutdinov R. R. (2006). 使用神经网络降低数据维度 . ScienceMag. www.cs.toronto.edu/~hinton/science.pdf

  5. Hinton G. E., Salakhutdinov R. R. (2006). 使用神经网络降低数据维度 . ScienceMag. www.cs.toronto.edu/~hinton/science.pdf

  6. Kingma, D P., Welling, M. (2014). 自动编码变分贝叶斯 . arXiv:1312.6114. arxiv.org/pdf/1312.6114.pdf

  7. Doersch, C. (2016). 变分自动编码器教程 . arXiv:1606.05908. arxiv.org/pdf/1606.05908.pdf

  8. Paisley, J., Blei, D., Jordan, M. (2012). 随机搜索的变分贝叶斯推断 . icml.cc/2012/papers/687.pdf

  9. Doersch, C. (2016). 变分自动编码器教程 . arXiv:1606.05908. arxiv.org/pdf/1606.05908.pdf

  10. Angelov, Plamen; Gegov, Alexander; Jayne, Chrisina; Shen, Qiang (2016-09-06). 计算智能系统的进展: 2016 年 9 月 7-9 日英国兰开斯特举办的第 16 届英国计算智能研讨会的贡献. Springer International Publishing. pp. 441--. ISBN 9783319465623. 检索于 2018 年 1 月 22 日。

  11. TinyImages: 麻省理工学院小图像

  12. Krizhevsky A. (2009). 从小图像中学习多层特征 . citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.222.9220&rep=rep1&type=pdf

  13. Kingma, D P., Welling, M. (2014). 自动编码变分贝叶斯 . arXiv:1312.6114. arxiv.org/pdf/1312.6114.pdf

  14. Kingma, D P., Salimans, T., Jozefowicz, R., Chen, X., Sutskever, I., Welling, M. (2016). 用逆自回归流改进变分推断 . arXiv:1606.04934. arxiv.org/pdf/1606.04934.pdf

第六章:使用 GAN 生成图像

生成建模是一个强大的概念,它为我们提供了巨大的潜力来逼近或建模生成数据的基本过程。在前几章中,我们涵盖了与深度学习一般以及更具体地与受限玻尔兹曼机RBMs )和变分自编码器VAEs )相关的概念。本章将介绍另一类生成模型家族,称为生成对抗网络GANs)。

在受博弈理论概念的强烈启发并吸取了先前讨论的一些最佳组件的基础上,GANs 为在生成建模空间中工作提供了一个强大的框架。自从它们于 2014 年由 Goodfellow 等人发明以来,GANs 受益于巨大的研究,并且现在被用于探索艺术、时尚和摄影等创造性领域。

以下是 GANs 的一个变体(StyleGAN)的两个惊人高质量样本(图 6.1 )。儿童的照片实际上是一个虚构的不存在的人物。艺术样本也是由类似的网络生成的。通过使用渐进增长的概念,StyleGANs 能够生成高质量清晰的图像(我们将在后面的部分中详细介绍)。这些输出是使用在数据集上训练的 StyleGAN2 模型生成的,如Flickr-Faces-HQFFHQ数据集。

图 6.1:GAN(StyleGAN2)想象出的图像(2019 年 12 月)- Karras 等人和 Nvidia²

本章将涵盖:

  • 生成模型的分类

  • 一些改进的 GAN,如 DCGAN、条件-GAN 等。

  • 渐进式 GAN 设定及其各个组件

  • 与 GANs 相关的一些挑战

  • 实例操作

生成模型的分类

生成模型是无监督机器学习领域的一类模型。它们帮助我们对生成数据集的潜在分布进行建模。有不同的方法/框架可以用来处理生成模型。第一组方法对应于用显式密度函数表示数据的模型。在这里,我们明确定义一个概率密度函数,,并开发一个增加从这个分布中采样的最大似然的模型。

在显式密度方法中,还有两种进一步的类型,即可计算近似 密度方法。 PixelRNNs 是可计算密度方法的一个活跃研究领域。当我们试图对复杂的现实世界数据分布建模时,例如自然图像或语音信号,定义参数函数变得具有挑战性。为了克服这一点,你在第四章"教网络生成数字"和第五章"使用 VAEs 绘制图像"中分别学习了关于 RBMs 和 VAEs。这些技术通过明确地逼近概率密度函数来工作。VAEs 通过最大化下界的似然估计来工作,而 RBMs 则使用马尔可夫链来估计分布。生成模型的整体景观可以使用图 6.2来描述:

图 6.2:生成模型的分类³

GANs 属于隐式密度建模方法。隐式密度函数放弃了明确定义潜在分布的属性,但通过定义方法来从这些分布中抽样的方法来工作。 GAN 框架是一类可以直接从潜在分布中抽样的方法。这减轻了到目前为止我们所涵盖的方法的某些复杂性,例如定义底层概率分布函数和输出的质量。现在你已经对生成模型有了高层次的理解,让我们深入了解 GAN 的细节。

生成对抗网络

GANs 有一个非常有趣的起源故事。一切始于在酒吧里讨论/争论与伊恩·古德费洛和朋友们讨论使用神经网络生成数据相关的工作。争论以每个人都贬低彼此的方法而告终。古德费洛回家编写了现在我们称之为 GAN 的第一个版本的代码。令他惊讶的是,代码第一次尝试就成功了。古德费洛本人在接受《连线》杂志采访时分享了一份更详细的事件链描述。

如前所述,GANs 是隐式密度函数,直接从底层分布中抽样。它们通过定义一个双方对抗的两人游戏来实现这一点。对抗者在定义良好的奖励函数下相互竞争,每个玩家都试图最大化其奖励。不涉及博弈论的细节,该框架可以解释如下。

判别器模型

这个模型代表了一个可微分函数,试图最大化从训练分布中抽样得到样本的概率为 1。这可以是任何分类模型,但我们通常偏爱使用深度神经网络。这是一种一次性模型(类似于自动编码器的解码器部分)。

鉴别器也用于分类生成器输出是真实的还是假的。这个模型的主要作用是帮助开发一个强大的生成器。我们将鉴别器模型表示为D ,其输出为D (x )。当它用于分类生成器模型的输出时,鉴别器模型被表示为D (G (z )),其中G (z)是生成器模型的输出。

图 6.3:鉴别器模型

生成器模型

这是整个游戏中主要关注的模型。这个模型生成样本,意图是与我们的训练集中的样本相似。该模型将随机的非结构化噪声作为输入(通常表示为z),并尝试创建各种输出。生成器模型通常是一个可微分函数;它通常由深度神经网络表示,但不局限于此。

我们将生成器表示为G ,其输出为G (z )。通常相对于原始数据x 的维度,我们使用一个较低维度的z ,即。这是对真实世界信息进行压缩或编码的一种方式。

图 6.4:生成器模型

简单来说,生成器的训练是为了生成足够好的样本以欺骗鉴别器,而鉴别器的训练是为了正确地区分真实(训练样本)与假的(输出自生成器)。因此,这种对抗游戏使用一个生成器模型G ,试图使D (G (z ))尽可能接近 1。而鉴别器被激励让D (G (z))接近 0,其中 1 表示真实样本,0 表示假样本。当生成器开始轻松地欺骗鉴别器时,生成对抗网络模型达到均衡,即鉴别器达到鞍点。虽然从理论上讲,生成对抗网络相对于之前描述的其他方法有几个优点,但它们也存在自己的一系列问题。我们将在接下来的章节中讨论其中一些问题。

训练生成对抗网络

训练生成对抗网络就像玩这个两个对手的游戏。生成器正在学习生成足够好的假样本,而鉴别器正在努力区分真实和假的。更正式地说,这被称为极小极大游戏,其中价值函数V (G , D)描述如下:

这也被称为零和游戏,其均衡点与纳什均衡相同。我们可以通过分离每个玩家的目标函数来更好地理解价值函数V (G , D)。以下方程描述了各自的目标函数:

其中是传统意义上的鉴别器目标函数,是生成器目标等于鉴别器的负值,是训练数据的分布。其余项具有其通常的含义。这是定义博弈或相应目标函数的最简单方式之一。多年来,已经研究了不同的方式,其中一些我们将在本章中讨论。

目标函数帮助我们理解每个参与者的目标。如果我们假设两个概率密度在任何地方都非零,我们可以得到D (x)的最优值为:

我们将在本章后面重新讨论这个方程。现在,下一步是提出一个训练算法,其中鉴别器和生成器模型分别朝着各自的目标进行训练。训练 GAN 的最简单但也是最广泛使用的方法(迄今为止最成功的方法)如下。

重复以下步骤N 次。N是总迭代次数:

  1. 重复k次步骤:

    • 从生成器中抽取大小为m 的小批量:{z [1], z [2], ... z [m]} = p [model](z)

    • 从实际数据中抽取大小为m 的小批量:{x [1], x [2], ... x [m]} = p [data](x)

    • 更新鉴别器损失,

  2. 将鉴别器设为不可训练

  3. 从生成器中抽取大小为m 的小批量:{z [1], z [2], ... z [m]} = p [model](z)

  4. 更新生成器损失,

在他们的原始论文中,Goodfellow 等人使用了k=1,也就是说,他们交替训练鉴别器和生成器模型。有一些变种和技巧,观察到更频繁地训练鉴别器比生成器有助于更好地收敛。

下图(图 6.5 )展示了生成器和鉴别器模型的训练阶段。较小的虚线是鉴别器模型,实线是生成器模型,较大的虚线是实际训练数据。底部的垂直线示意从z 的分布中抽取数据点,即x =p [model](z) 。线指出了生成器在高密度区域收缩而在低密度区域扩张的事实。部分**(a)展示了训练阶段的初始阶段,此时鉴别器 (D)是部分正确的分类器。部分 (b) ©展示了D 的改进如何引导G的变化。最后,在部分 (d)中,你可以看到p[model] = p [data],鉴别器不再能区分假样本和真样本,即

图 6.5:GAN 的训练过程⁴

非饱和生成器成本

在实践中,我们不训练生成器最小化log (1 -- D (G (z ))),因为该函数不能提供足够的梯度用于学习。在G 表现较差的初始学习阶段,鉴别器能够以高置信度区分假的和真实的。这导致log (1 -- D (G (z )))饱和,阻碍了生成模型的改进。因此,我们调整生成器改为最大化log (D (G (z))):

这为生成器提供了更强的梯度进行学习。这在图 6.6 中显示。x 轴表示D (G (z))。顶部线显示目标,即最小化鉴别器正确的可能性。底部线(更新的目标)通过最大化鉴别器错误的可能性来工作。

图 6.6:生成器目标函数⁵

图 6.6说明了在训练的初始阶段,轻微的变化如何帮助实现更好的梯度。

最大似然游戏

极小极大游戏可以转化为最大似然游戏,其目的是最大化生成器概率密度的可能性。这是为了确保生成器的概率密度类似于真实/训练数据的概率密度。换句话说,游戏可以转化为最小化p [z]和p [data]之间的离散度。为此,我们利用Kullback-Leibler 散度KL 散度)来计算两个感兴趣的分布之间的相似性。总的价值函数可以表示为:

生成器的成本函数转化为:

一个重要的要点是 KL 散度不是对称度量,也就是说,。模型通常使用来获得更好的结果。

迄今为止讨论过的三种不同成本函数具有略有不同的轨迹,因此在训练的不同阶段具有不同的特性。这三个函数可以如图 6.7所示可视化:

图 6.7:生成器成本函数⁶

基本 GAN

我们已经在理解 GAN 的基础知识方面取得了相当大的进展。在本节中,我们将应用这些理解,从头开始构建一个 GAN。这个生成模型将由一个重复的块结构组成,类似于原始论文中提出的模型。我们将尝试使用我们的网络复制生成 MNIST 数字的任务。

整体的 GAN 设置可以在图 6.8 中看到。图中概述了一个以噪声向量z作为输入的生成器模型,并且利用重复块来转换和扩展向量以达到所需的尺寸。每个块由一个密集层后跟一个 Leaky ReLU 激活和一个批量归一化层组成。我们简单地将最终块的输出重新塑造以将其转换为所需的输出图像大小。

另一方面,判别器是一个简单的前馈网络。该模型以图像作为输入(真实图像或来自生成器的伪造输出)并将其分类为真实或伪造。这两个竞争模型的简单设置有助于我们训练整体的 GAN。

图 6.8:Vanilla GAN 架构

我们将依赖于 TensorFlow 2 并尽可能使用高级 Keras API。第一步是定义判别器模型。在此实现中,我们将使用一个非常基本的多层感知器MLP)作为判别器模型:

py 复制代码
def build_discriminator(input_shape=(28, 28,), verbose=True):
    """
    Utility method to build a MLP discriminator
    Parameters:
        input_shape:
            type:tuple. Shape of input image for classification.
                        Default shape is (28,28)->MNIST
        verbose:
            type:boolean. Print model summary if set to true.
                        Default is True
    Returns:
        tensorflow.keras.model object
    """
    model = Sequential()
    model.add(Input(shape=input_shape))
    model.add(Flatten())
    model.add(Dense(512))
    model.add(LeakyReLU(alpha=0.2))
    model.add(Dense(256))
    model.add(LeakyReLU(alpha=0.2))
    model.add(Dense(1, activation='sigmoid'))
    if verbose:
        model.summary()
    return model 

我们将使用顺序 API 来准备这个简单的模型,仅含有四层和具有 sigmoid 激活的最终输出层。由于我们有一个二元分类任务,因此最终层中只有一个单元。我们将使用二元交叉熵损失来训练判别器模型。

生成器模型也是一个多层感知器,其中含有多层将噪声向量z扩展到所需的尺寸。由于我们的任务是生成类似于 MNIST 的输出样本,最终的重塑层将把平面向量转换成 28x28 的输出形状。请注意,我们将利用批次归一化来稳定模型训练。以下代码片段显示了构建生成器模型的实用方法:

py 复制代码
def build_generator(z_dim=100, output_shape=(28, 28), verbose=True):
    """
    Utility method to build a MLP generator
    Parameters:
        z_dim:
            type:int(positive). Size of input noise vector to be
                        used as model input.
                        Default value is 100
        output_shape:   type:tuple. Shape of output image .
                        Default shape is (28,28)->MNIST
        verbose:
            type:boolean. Print model summary if set to true.
                        Default is True
    Returns:
        tensorflow.keras.model object
    """
    model = Sequential()
    model.add(Input(shape=(z_dim,)))
    model.add(Dense(256, input_dim=z_dim))
    model.add(LeakyReLU(alpha=0.2))
    model.add(BatchNormalization(momentum=0.8))
    model.add(Dense(512))
    model.add(LeakyReLU(alpha=0.2))
    model.add(BatchNormalization(momentum=0.8))
    model.add(Dense(1024))
    model.add(LeakyReLU(alpha=0.2))
    model.add(BatchNormalization(momentum=0.8))
    model.add(Dense(np.prod(output_shape), activation='tanh'))
    model.add(Reshape(output_shape))
    if verbose:
        model.summary()
    return model 

我们简单地使用这些实用方法来创建生成器和判别器模型对象。以下代码片段还使用这两个模型对象来创建 GAN 对象:

py 复制代码
discriminator = build_discriminator()
discriminator.compile(loss='binary_crossentropy',
                      optimizer=Adam(0.0002, 0.5),
                      metrics=['accuracy'])
generator=build_generator()
z_dim = 100 #noise
z = Input(shape=(z_dim,))
img = generator(z)
# For the combined model we will only train the generator
discriminator.trainable = False
# The discriminator takes generated images as input 
# and determines validity
validity = discriminator(img)
# The combined model  (stacked generator and discriminator)
# Trains the generator to fool the discriminator
gan_model = Model(z, validity)
gan_model.compile(loss='binary_crossentropy', optimizer=Adam(0.0002, 0.5)) 

最后一部分是定义训练循环。正如前一节中所描述的,我们将交替训练(判别器和生成器)模型。通过高级 Keras API,这样做非常简单。以下代码片段首先加载 MNIST 数据集并将像素值缩放到-1 到+1 之间:

py 复制代码
# Load MNIST train samples
(X_train, _), (_, _) = datasets.mnist.load_data()
# Rescale to [-1, 1]
  X_train = X_train / 127.5 -- 1 

对于每次训练迭代,我们首先从 MNIST 数据集中随机选择实际图像,数量等于我们定义的批量大小。下一步涉及对相同数量的z 向量进行采样。我们使用这些采样的z向量来从我们的生成器模型中生成输出。最后,我们计算真实样本和生成样本的判别器损失。这些步骤在下面的代码片段中有详细说明:

py 复制代码
idx = np.random.randint(0, X_train.shape[0], batch_size)
real_imgs = X_train[idx]
# pick random noise samples (z) from a normal distribution
noise = np.random.normal(0, 1, (batch_size, z_dim))
# use generator model to generate output samples
fake_imgs = generator.predict(noise)
# calculate discriminator loss on real samples
disc_loss_real = discriminator.train_on_batch(real_imgs, real_y)

# calculate discriminator loss on fake samples
disc_loss_fake = discriminator.train_on_batch(fake_imgs, fake_y)

# overall discriminator loss
discriminator_loss = 0.5 * np.add(disc_loss_real, disc_loss_fake) 

训练生成器非常简单。我们准备了一个堆叠模型对象,类似于我们以前讨论过的 GAN 架构。简单地使用train_on_batch帮助我们计算生成器损失并改善它,如下面的代码片段所示:

py 复制代码
# train generator
# pick random noise samples (z) from a normal distribution
noise = np.random.normal(0, 1, (batch_size, z_dim))
# use trained discriminator to improve generator
gen_loss = gan_model.train_on_batch(noise, real_y) 

我们训练我们的普通 GAN 大约 30,000 次迭代。以下(图 6.9)是训练不同阶段的模型输出。您可以清楚地看到随着我们从一个阶段移到另一个阶段,样本质量是如何提高的。

图 6.9:普通 GAN 在训练的不同阶段的输出

普通 GAN 的结果令人鼓舞,但也留下了进一步改进的空间。在下一节中,我们将探讨一些改进的架构,以增强 GAN 的生成能力。

改进的 GAN

普通 GAN 证明了对抗网络的潜力。建立模型的简易性和输出的质量引发了该领域的很多兴趣。这导致了对改进 GAN 范式的大量研究。在本节中,我们将涵盖几个主要的改进,以发展 GAN。

深度卷积 GAN

2016 年发表的这项由 Radford 等人完成的工作引入了几项关键改进,以改善 GAN 输出,除了关注卷积层之外,还讨论了原始 GAN 论文。2016 年的论文强调使用更深的架构。图 6.10 展示了深度卷积 GANDCGAN)的生成器架构(如作者所提出的)。生成器将噪声向量作为输入,然后通过一系列重复的上采样层、卷积层和批量归一化层来稳定训练。

图 6.10:DCGAN 生成器架构⁷

直到 DCGAN 的引入,输出图像的分辨率相当有限。提出了拉普拉斯金字塔或 LAPGAN 来生成高质量的图像,但它在输出中也存在一定程度的模糊。DCGAN 论文还利用了另一个重要的发明,即批量归一化层。批量归一化是在原始 GAN 论文之后提出的,并且通过将每个单元的输入归一化为零均值和单位方差来稳定整体训练。为了获得更高分辨率的图像,它利用了大于 1 的步长移动卷积滤波器。

让我们首先准备鉴别器模型。基于 CNN 的二元分类器是简单的模型。我们在这里做的一个修改是在层之间使用比 1 更长的步长来对输入进行下采样,而不是使用池化层。这有助于为生成器模型的训练提供更好的稳定性。我们还依赖批量归一化和泄漏整流线性单元来实现相同的目的(尽管这些在原始 GAN 论文中未被使用)。与普通 GAN 鉴别器相比,这个鉴别器的另一个重要方面是没有全连接层。

生成器模型与普通 GAN 所见的截然不同。这里我们只需要输入向量的维度。我们利用重塑和上采样层将向量修改为二维图像,并增加其分辨率。类似于 DCGAN 的判别器,我们除了输入层被重塑为图像外,没有任何全连接层。以下代码片段展示了如何为 DCGAN 构建生成器模型:

py 复制代码
def build_dc_generator(z_dim=100, verbose=True):
    model = Sequential()
    model.add(Input(shape=(z_dim,)))
    model.add(Dense(128 * 7 * 7, activation="relu", input_dim=z_dim))
    model.add(Reshape((7, 7, 128)))
    model.add(UpSampling2D())
    model.add(Conv2D(128, kernel_size=3, padding="same"))
    model.add(BatchNormalization(momentum=0.8))
    model.add(Activation("relu"))
    model.add(UpSampling2D())
    model.add(Conv2D(64, kernel_size=3, padding="same"))
    model.add(BatchNormalization(momentum=0.8))
    model.add(Activation("relu"))
    model.add(Conv2D(1, kernel_size=3, padding="same"))
    model.add(Activation("tanh"))
    if verbose:
        model.summary()
    return model 
Figure 6.11).

图 6.11:DCGAN 在不同训练阶段的输出

结果显示,DCGAN 能够在较少的训练周期内生成所需的输出。虽然很难从生成的图像质量中得出太多结论(考虑到 MNIST 数据集的性质),但原则上,DCGAN 应该能够生成比普通 GAN 更高质量的输出。

向量算术

通过加法、减法等操作操纵潜在向量以生成有意义的输出变换是一种强大的工具。DCGAN 论文的作者们表明,生成器的Z 表示空间确实具有如此丰富的线性结构。类似于 NLP 领域的向量算术,word2vec 在执行"国王" - "男人" + "女人"的操作后生成类似于"女王"的向量,DCGAN 在视觉领域也能实现相同的功能。以下是 DCGAN 论文的一个例子(图 6.12):

图 6.12:DCGAN 向量算术⁸

这个例子显示,我们可以通过执行"带眼镜的男人" - "不带眼镜的男人" + "不带眼镜的女人"的简单操作来生成"带眼镜的女人"的示例。这打开了无需大量训练数据就能生成复杂样本的可能性。尽管不像 word2vec 那样只需一个向量就足够,但在这种情况下,我们需要平均至少三个样本才能实现稳定的输出。

条件 GAN

GAN 是可以从训练领域生成逼真样本的强大系统。在前面的章节中,您看到普通 GAN 和 DCGAN 可以从 MNIST 数据集生成逼真样本。这些架构也被用于生成类似人脸甚至真实世界物品的样本(从对 CIFAR10 的训练等)。但它们无法控制我们想要生成的样本。

简单来说,我们可以使用训练过的生成器生成任意数量所需的样本,但我们不能控制它生成特定类型的示例。条件 GANCGANs)是提供我们精确控制生成特定类别示例所需的 GAN 类别。由 Mirza 等人于 2014 年⁹开发,它们是对 Goodfellow 等人原始 GAN 架构的最早改进之一。

CGAN 的工作方式是通过训练生成器模型生成伪造样本,同时考虑所需输出的特定特征。另一方面,鉴别器需要进行额外的工作。它不仅需要学会区分真实和伪造的样本,还需要在生成的样本和其条件特征不匹配时标记出伪造的样本。

在他们的工作 Conditional Adversarial Networks 中,Mirza 等人指出了使用类标签作为生成器和鉴别器模型的附加条件输入。我们将条件输入标记为 y,并将 GAN 极小极大游戏的价值函数转换如下:

log log D (x |y ) 是对真实样本 x 在条件 y 下的鉴别器输出,而 log log (1 - D (G (z |y ))) 则是对伪造样本 G (z ) 在条件 y 下的鉴别器输出。请注意,价值函数与普通 GAN 的原始极小极大方程仅略有变化。因此,我们可以利用改进的生成器成本函数以及我们在前几节讨论过的其他增强功能来加强生成器。条件信息 y (例如,类标签)作为两个模型的额外输入,GAN 设置本身负责处理其余部分。图 6.13 展示了条件 GAN 的架构设定。

图:6.13 CGAN 生成器架构¹⁰

尽可能保持实现与原始 CGAN 实现的接近,现在我们将开发条件生成器和鉴别器模型作为 MLPs。建议您尝试基于类标签的 DCGAN 类似的架构。由于每个成分模型都将有多个输入,我们将使用 Keras 函数 API 来定义我们的模型。我们将开发 CGAN 以生成 MNIST 数字。

py 复制代码
z and the class label *y*'s embedding output, using the multiply layer. Please note that this is different from the original implementation, which concatenates vectors *z* and *y*. Changes as compared to vanilla GAN's generator have been highlighted for ease of understanding:
py 复制代码
def build_conditional_generator(z_dim=100, output_shape=(28, 28),
                                **num_classes=****10**, verbose=True):
    """
    Utility method to build a MLP generator
    Parameters:
        z_dim:
            type:int(positive). Size of input noise vector to be
                        used as model input.
                        Default value is 100
        output_shape:   type:tuple. Shape of output image .
                        Default shape is (28,28)->MNIST
        num_classes:    type:int. Number of unique class labels.
                        Default is 10->MNIST digits
        verbose:
            type:boolean. Print model summary if set to true.
                        Default is True
    Returns:
        tensorflow.keras.model object
    """
    **noise = Input(shape=(z_dim,))**
    **label = Input(shape=(****1****,), dtype=****'int32'****)**
    **label_embedding = Flatten()(Embedding(num_classes, z_dim)(label))**
    **model_input = multiply([noise, label_embedding])**
    mlp = Dense(256, input_dim=z_dim)(model_input)
    mlp = LeakyReLU(alpha=0.2)(mlp)
    mlp = BatchNormalization(momentum=0.8)(mlp)
    mlp = Dense(512)(mlp)
    mlp = LeakyReLU(alpha=0.2)(mlp)
    mlp = Dense(1024)(mlp)
    mlp = LeakyReLU(alpha=0.2)(mlp)
    mlp = BatchNormalization(momentum=0.8)(mlp)
    mlp = Dense(np.prod(output_shape), activation='tanh')(mlp)
    mlp = Reshape(output_shape)(mlp)
    **model = Model([noise, label], mlp)**
    if verbose:
        model.summary()
    return model 
 network. Changes as compared to vanilla GAN's discriminator have been highlighted:
py 复制代码
def build_conditional_discriminator(input_shape=(28, 28,),
                                    **num_classes=****10**, verbose=True):
    """
    Utility method to build a conditional MLP discriminator
    Parameters:
        input_shape:
            type:tuple. Shape of input image for classification.
                        Default shape is (28,28)->MNIST
        num_classes:    type:int. Number of unique class labels.
                        Default is 10->MNIST digits
        verbose:
            type:boolean. Print model summary if set to true.
                        Default is True
    Returns:
        tensorflow.keras.model object
    """
    **img = Input(shape=input_shape)**
    **flat_img = Flatten()(img)**
    **label = Input(shape=(****1****,), dtype=****'int32'****)**
    **label_embedding = Flatten()(Embedding(num_classes,**
                                      **np.prod(input_shape))(label))**
    **model_input = multiply([flat_img, label_embedding])**
    mlp = Dense(512, input_dim=np.prod(input_shape))(model_input)
    mlp = LeakyReLU(alpha=0.2)(mlp)
    mlp = Dense(512)(mlp)
    mlp = LeakyReLU(alpha=0.2)(mlp)
    mlp = Dropout(0.4)(mlp)
    mlp = Dense(512)(mlp)
    mlp = LeakyReLU(alpha=0.2)(mlp)
    mlp = Dropout(0.4)(mlp)
    mlp = Dense(1, activation='sigmoid')(mlp)
    **model = Model([img, label], mlp)**
    if verbose:
        model.summary()
    return model 
training loop:
py 复制代码
def train(generator=None,discriminator=None,gan_model=None,
          epochs=1000, batch_size=128, sample_interval=50,
          z_dim=100):
    # Load MNIST train samples
    **(X_train, y_train), (_, _) = datasets.mnist.load_data()**
    # Rescale -1 to 1
    X_train = X_train / 127.5 - 1
    X_train = np.expand_dims(X_train, axis=3)
    **y_train = y_train.reshape(****-1****,** **1****)**
    # Prepare GAN output labels
    real_y = np.ones((batch_size, 1))
    fake_y = np.zeros((batch_size, 1))
    for epoch in range(epochs):
        # train disriminator
        # pick random real samples from X_train
        idx = np.random.randint(0, X_train.shape[0], batch_size)
        **real_imgs, labels = X_train[idx], y_train[idx]**
        # pick random noise samples (z) from a normal distribution
        noise = np.random.normal(0, 1, (batch_size, z_dim))
        # use generator model to generate output samples
        **fake_imgs = generator.predict([noise, labels])**
        # calculate discriminator loss on real samples
        **disc_loss_real = discriminator.train_on_batch([real_imgs, labels], real_y)**

        # calculate discriminator loss on fake samples
        **disc_loss_fake = discriminator.train_on_batch([fake_imgs, labels], fake_y)**

        # overall discriminator loss
        discriminator_loss = 0.5 * np.add(disc_loss_real, disc_loss_fake)

        # train generator
        # pick random noise samples (z) from a normal distribution
        noise = np.random.normal(0, 1, (batch_size, z_dim))

        # pick random labels for conditioning
        **sampled_labels = np.random.randint(****0****,** **10****, batch_size).reshape(****-1****,** **1****)**
        # use trained discriminator to improve generator
        **gen_loss = gan_model.train_on_batch([noise, sampled_labels], real_y)**
        # training updates
        print ("%d [Discriminator loss: %f, acc.: %.2f%%] [Generator loss: %f]" % (epoch, discriminator_loss[0], 
              100*discriminator_loss[1], gen_loss))
        # If at save interval => save generated image samples
        if epoch % sample_interval == 0:
            sample_images(epoch,generator) 

训练完成后,可以要求 CGAN 生成特定类别的示例。图 6.14 展示了横跨训练周期的不同类别标签的输出。

图 6.14: CGAN 在不同训练阶段的输出

图 6.14 明显可见的一个主要优势是 CGAN 提供给我们的额外控制功能。如讨论所述,通过使用额外输入,我们能够轻松控制生成器生成特定的数字。这开启了长长的用例列表,其中一些将在本书的后续章节中介绍。

Wasserstein GAN

到目前为止,我们所涵盖的改进 GAN 主要集中在增强架构以改进结果。GAN 设置的两个主要问题是极小极大游戏的稳定性和生成器损失的不直观性。这些问题是由于我们交替训练鉴别器和生成器网络,并在任何给定时刻,生成器损失表明鉴别器到目前为止的表现。

沃瑟斯坦 GAN(或 W-GAN)是 Arjovsky 等人为克服 GAN 设置中的一些问题而提出的尝试。这是一些深度学习论文中深深植根于理论基础以解释其工作影响的论文之一(除了经验证据)。典型 GAN 和 W-GAN 之间的主要区别在于 W-GAN 将判别器视为评论家(来源于强化学习;参见第十一章《用生成模型创作音乐》)。因此,W-GAN 判别器(或评论家)不仅仅将输入图像分类为真实或伪造,还生成一个分数来告诉生成器输入图像的真实性或伪造性。

我们在本章的初始部分讨论的最大似然游戏解释了这样一个任务,我们试图通过 KL 散度来最小化p [z]和p [data]之间的差异,即。除了是非对称的,KL 散度在分布相距太远或完全不相交时也存在问题。为了克服这些问题,W-GAN 使用地球移动者EM )距离或 Wasserstein 距离。简单地说,EM 距离是从分布pq 移动或转运质量的最小成本。对于 GAN 设置,我们可以将其想象为从生成器分布(p [z])移动到实际分布(p [data])的最小成本。在数学上,这可以被陈述为任何传输计划(表示为Wsourcedestination ))的下确界(或最大下界,表示为inf)的方式:

由于这是无法计算的,作者使用了康托洛维奇-鲁宾斯坦二元性来简化计算。简化形式表示为:

其中sup 是最大值或最小上界,f 是一个 1-Lipschitz 函数,施加了一定的约束条件。要完全理解使用 Wasserstein 距离的细节和影响,需要许多细节。建议您阅读论文以深入了解相关概念,或参考vincentherrmann.github.io/blog/wasserstein/

为了简洁起见,我们将重点放在实现层级的改变上,以帮助实现稳定的可训练体系结构。图 6.15展示了 GAN 和 W-GAN 之间的梯度更新对比:

图 6.15:W-GAN 对比 GAN¹¹

该图解释了当输入为双峰高斯分布时,GAN 判别器中的梯度消失,而 W-GAN 评论家的梯度始终平滑。

为了将这种理解转化为实现层级的细节,W-GAN 的关键变化如下:

  • 判别器被称为评论家,它生成并输出真实性或伪造性的分数。

  • 评论家/判别器中的最后一层是一个线性层(而不是 sigmoid)。

  • -1 表示真标签,而 1 表示假标签。这在文献中被表达为积极和消极的评论家。否则,我们分别使用 1 和 0 表示真和假标签。

  • 我们用 Wasserstein 损失替换了分类损失(二元交叉熵)。

  • 与生成器模型相比,评论家模型被允许进行更多次的训练周期。这是因为在 W-GANs 的情况下,稳定的评论家更好地指导生成器;梯度要平稳得多。作者每个生成器周期训练了评论家模型五次。

  • 评论家层的权重被剪切在一个范围内。这是为了保持 1-李普希兹约束所必需的。作者使用了-0.01 到 0.01 的范围。

  • RMSProp 是推荐的优化器,以保证稳定的训练。这与典型情况下使用 Adam 作为优化器的情况形成对比。

经过这些改变,作者注意到训练稳定性有了显著的改善,并且生成器得到了更好的反馈。图 6.16(来自论文)展示了生成器如何从稳定的评论家中获得提示来进行更好的训练。随着训练轮数的增加,结果也会得到改善。作者们尝试了基于 MLP 的生成器和卷积生成器,发现了类似的结果。

图 6.16:W-GAN 生成器损失和输出质量¹²

由于我们可以对任何生成器和鉴别器进行微小修改,让我们来看一些实现细节。首先,最重要的是 Wasserstein 损失。我们通过取评论家评分和真实标签的平均值来计算它。这在下面的片段中显示:

py 复制代码
def wasserstein_loss(y_true, y_pred):
    """
    Custom loss function for W-GAN
    Parameters:
        y_true: type:np.array. Ground truth
        y_pred: type:np.array. Predicted score
    """
    return K.mean(y_true * y_pred) 

对鉴别器的主要改变是它的最后一层和它的权重裁剪。虽然最后一层的激活函数的变化很直接,但权重裁剪在开头可能有些挑战。通过 Keras API,这可以通过两种方式来完成:通过对Constraint类进行子类化,并将其作为所有层的附加参数使用,或者在训练循环期间遍历层。虽然第一种方法更清晰,但我们将使用第二种方法,因为更容易理解。

py 复制代码
# Clip critic weights
for l in discriminator.layers:
       weights = l.get_weights()
       weights = [np.clip(w, -clip_value, clip_value) for w in weights]
       l.set_weights(weights) 

经过这些改变,我们训练我们的 W-GAN 来生成 MNIST 数字。以下(图 6.17)是训练不同阶段的输出样本:

图 6.17:W-GAN 训练不同阶段的输出

承诺的稳定训练受到理论证明的支持,但也并非没有自身一套问题。大部分问题是由于保持计算的可处理性的限制。其中一些问题是在 Gulrajani 等人于 2017 年撰写的一篇名为改良的 Wasserstein GAN 的训练(13)的最近工作中得到解决。该工作提出了一些技巧,最重要的是梯度惩罚(或者,如作者称呼它的,W-GAN-GP)。你也被鼓励去阅读这个有趣的工作,以更好地理解其贡献。

现在我们已经涵盖了相当多的改进,让我们转向一个稍微更复杂的设置,称为 Progressive GAN。在下一节中,我们将详细介绍这种高效的架构,以生成高质量的输出。

Progressive GAN

GAN 是生成高质量样本的强大系统,我们在前几节中已经看到了一些例子。不同的工作已经利用了这种对抗性设置来从不同的分布生成样本,比如 CIFAR10,celeb_a,LSUN-bedrooms 等(我们使用 MNIST 的例子来解释)。有一些工作集中于生成更高分辨率的输出样本,比如 Lap-GANs,但它们缺乏感知输出质量,并且提出了更多的训练挑战。Progressive GANs 或 Pro-GANs 或 PG-GANs 是由 Karras 等人在他们的名为用于改善质量、稳定性和变化的 GAN(14)的 ICLR-2018 工作中提出的,是一种生成高质量样本的高效方法。

本文提出的方法不仅缓解了早期工作中存在的许多挑战,而且还提出了一个非常简单的解决方案来解决生成高质量输出样本的问题。该论文还提出了一些非常有影响力的贡献,其中一些我们将在接下来的子章节中详细介绍。

整体方法

解决技术难题的软件工程方法通常是将其分解为更简单的颗粒任务。Pro-GANs 也针对生成高分辨率样本的复杂问题,通过将任务拆分为更小更简单的问题来解决。高分辨率图像的主要问题是具有大量模式或细节。这使得很容易区分生成的样本和真实数据(感知质量问题)。这本质上使得构建一个具有足够容量在这样的数据集上训练良好并具有内存需求的生成器的任务非常艰巨。

为了解决这些问题,Karras 等人提出了一种方法,逐渐从低分辨率向高分辨率发展的过程中,使生成器和判别器模型逐渐增长。这在图 6.18中有所展示。他们指出这种模型的渐进增长具有各种优点,例如能够生成高质量的样本,训练速度更快,通常所需的内存要求更少(与直接训练 GAN 生成高分辨率输出相比)。

图 6.18:逐步增加判别器和生成器模型的分辨率¹⁵

逐步生成高分辨率图像并不是一个全新的想法。许多先前的工作使用了类似的技术,但作者指出他们的工作与自动编码器的逐层训练最相似

系统通过从低分辨率样本和反映图像的生成器-判别器设置(在架构上)学习。在较低的分辨率下(比如 4x4),训练要简单得多且稳定,因为要学习的模式较少。然后我们逐步增加分辨率,为两个模型引入额外的层。逐步增加分辨率的步骤限制了所面临任务的复杂性,而不是迫使生成器一次性学习所有模式。这最终使得 Pro-GAN 能够生成百万像素大小的输出,这些都是从非常低分辨率的初始点开始的。

在接下来的小节中,我们将涵盖重要贡献和实现层面的细节。需要注意的是,尽管 Pro-GAN 的训练时间和计算需求有所改进,但仍然很庞大。作者提到在多个 GPU 上生成所述的百万像素输出可能需要长达一周的训练时间。在满足要求的基础上,我们将涵盖组件层面的细节,但使用 TensorFlow Hub 来呈现经过训练的模型(而不是从头开始训练)。这将使我们能够专注于重要细节,并根据需要利用预构建的模块。

渐进增长-平滑淡入

Pro-GAN 被引入为逐步增加分辨率的网络,通过向生成器和判别器模型添加额外的层来实现。但实际上是怎么工作的呢?下面是逐步说明:

  • 生成器和判别器模型的起始分辨率分别为 4x4。两个网络执行它们指定的生成和鉴别预缩放样本的任务。

  • 我们对这些模型进行多个轮次的训练,直到性能达到饱和。在这一点上,两个网络都添加了额外的层。

  • 生成器在获得额外的上采样层以生成 8x8 的样本,而判别器则获得额外的下采样层。

  • 从一个步骤到下一个步骤(从 4x4 到 8x8)是逐渐进行的,使用了一个覆盖因子,图 6.19 以图表形式展示了这个过渡。

图 6.19:平滑淡入¹⁷

  • 现有的层通过乘以 1- 进行扩大,并且逐渐过渡到新增层;而新增的层则乘以 进行缩小。 的值在 0 和 1 之间,逐渐从 0 增加到 1,以增加新增层的贡献。

  • 使用同样的过程对判别器进行操作,其中过渡逐渐将其从现有设置过渡到新增的层。

  • 需要注意的是,在整个训练过程中,所有层都会被训练(现有的增加和新增的层)。

作者从 4x4 分辨率开始,逐步增加到最终达到百万像素级别。

小批量标准偏差

现有的方法依赖于诸如批量归一化、虚拟归一化等归一化技术。这些技术使用可训练参数来计算小批量级别的统计数据,以保持样本之间的相似性。除了增加额外的参数和计算负载外,这些归一化方法并不能完全缓解问题。

Pro-GAN 的作者提出了一种简化的解决方案,不需要任何可训练参数。提出的小批量标准偏差方法旨在提高小批量的多样性。从判别器的最后一层开始,该方法计算每个空间位置(像素位置 xy )的标准偏差。对于大小为 B 的批次,图像的形状为 H x W x C (高度、宽度和通道),计算了共 B * H * W * C 个标准偏差。下一步包括对这些标准偏差进行平均,并将它们连接到层的输出。这是为了保证每个示例在小批量中都相同。

等化学习率

Pro-GAN 的作者简要提到,他们专注于比当前流行的自定义初始化方法更简单的权重初始化方法。他们使用标准正态分布 N (0,1) 来初始化权重,然后在运行时明确进行了缩放。缩放是以的形式进行的,其中 c 是来自 He's 初始化器的每层归一化常数。他们还指出了动量优化器(如 Adam 和 RMSProp)存在的问题,这些问题通过这种等化的学习率方法得以缓解。

逐像素归一化

到目前为止提到的增强功能要么专注于判别器,要么是整个 GAN 训练。这种归一化技术是应用于生成器模型的。作者指出,这种方法有助于防止训练过程中的不稳定以及模式崩溃问题。正如名称所示,他们建议对每个空间位置(或每个像素,表示为 (x , y))应用归一化。归一化方程如下:

其中 N 是特征图的数量,ab 分别是原始特征向量和归一化特征向量。这个看起来奇怪的归一化方程有助于有效地防止幅度的巨大随机变化。

TensorFlow Hub 实现

正如前面提到的,尽管 Pro-GAN 在产生高质量结果方面有着长列表的有效贡献,但需要大量计算资源。GitHub 的官方实现¹⁸提到在 CelebA-HQ 数据集上单个 GPU 训练两周时间。这已经超出了大多数人可用的时间和精力。以下(图 6.20)是生成器和鉴别器模型架构的快照;每个模型约有 2300 万参数!

图 6.20:生成器和鉴别器模型摘要¹⁹

因此,我们将专注于通过 TensorFlow Hub 可用的预训练 Pro-GAN 模型。TensorFlow Hub 是一个包含大量深度学习模型的存储库,可以轻松下载并在 TensorFlow 生态系统中用于各种下游任务。以下是一个小例子,展示了我们如何使用 Pro-GAN 模型。

第一步是加载所需的库。使用 TensorFlow Hub,唯一需要额外 import 的是:

py 复制代码
import tensorflow_hub as hub 

我们使用 TensorFlow Hub 版本 0.12.0 和 TensorFlow 版本 2.4.1. 确保您的版本同步,否则语法可能会有轻微变化。下一步是加载模型。我们为 TensorFlow 会话设置了一个种子,以确保结果的可重现性:

py 复制代码
tf.random.set_seed(12)
pro_gan = hub.load("https://tfhub.dev/google/progan-128/1").signatures['default'] 

使用 TensorFlow Hub 加载预训练模型与前述代码一样简单。下一步是从正态分布中随机采样潜在向量 (z)。模型要求潜在向量的大小为 512. 一旦我们有了潜在向量,我们就将其传递给生成器以获得输出:

py 复制代码
vector = tf.random.normal([1, 512])
sample_image = pro_gan(vector)['default'][0]
np_img = sample_image.numpy()
plt.figure(figsize=(6,6))
plt.imshow(np_img) 

以下是预训练的 Pro-GAN 模型生成的样本人脸(图 6.21):

图 6.21:使用 TensorFlow Hub 中的预训练 Pro-GAN 生成的样本人脸

我们编写了一个简单的采样函数,类似于本章一直在使用的函数,以生成一些额外的面孔。这个额外的实验帮助我们了解这个模型能够捕捉到的人脸多样性,当然,它在解决模式崩溃等问题上取得了成功(更多内容将在下一节介绍)。以下图像(图 6.22)是 25 张这样的面孔样本:

图 6.22:使用 Pro-GAN 生成的 25 张面孔

如果你好奇,TensorFlow Hub 提供了一个训练机制,可以从头开始训练这样的模型。此外,Pro-GAN 的作者已经开源了他们的实现。建议你去研究一下。

我们已经涵盖了很多内容来了解不同的架构及其生成图像的能力。在下一节中,我们将涵盖与 GANs 相关的一些挑战。

挑战

GANs 提供了一种开发生成模型的替代方法。它们的设计固有地有助于缓解我们使用其他技术时讨论的问题。然而,GANs 并不是没有自己一套问题。使用博弈论概念开发模型的选择令人着迷,但难以控制。我们有两个试图优化对立目标的代理/模型,这可能导致各种问题。与 GANs 相关的一些最常见的挑战如下。

训练不稳定

GANs 通过对立的目标进行极小极大博弈。难怪这导致生成器和判别器模型在批次间产生振荡的损失。一个训练良好的 GAN 设置通常最初会有更高的损失变化,但最终会稳定下来,两个竞争模型的损失也会如此。然而,GANs(特别是原始的 GANs)很常见地会失控。很难确定何时停止训练或估计一个平衡状态。

模式坍塌

模式坍塌是指生成器找到一个或仅有少数足以愚弄鉴别器的样本的失败状态。为了更好地理解这一点,让我们以两个城市的温度的假设数据集为例,城市A 和城市B 。我们还假设城市A 位于较高的海拔处,大部分时间保持寒冷,而城市B 位于赤道附近,气温较高。这样的数据集可能具有如图 6.23 所示的温度分布。该分布是双峰的,即有两个峰值:一个是城市A 的,另一个是城市B的(由于它们不同的天气条件)。

图 6.23:两个城市温度的双峰分布

现在我们有了我们的数据集,假设我们的任务是训练一个能够模仿这个分布的 GAN。在完美的情况下,我们将有 GAN 生成来自城市A 和城市B 的温度样本,其概率大致相等。然而,一个常见的问题是模式坍塌:生成器最终只生成来自一个模式(比如,只有城市B)。当:

  • 生成器通过仅从城市B生成看起来逼真的样本来愚弄鉴别器

  • 鉴别器试图通过学习所有城市A 的输出都是真实的,并试图将城市B的样本分类为真实或伪造来抵消这一点

  • 生成器然后转向城市A ,放弃城市B的模式

  • 现在,鉴别器假定所有城市B 的样本都是真实的,并试图代替城市A的样本进行分类

  • 这个周期不断重复

这种循环重复,因为生成器永远没有足够的动力来覆盖两种模式。这限制了生成器的实用性,因为它展示了样本输出的贫乏多样性。在实际应用中,模式崩溃会从完全崩溃(即,所有生成的样本都相同)到部分崩溃(即,捕捉到一些模式)不等。

到目前为止,在本章中我们已经训练了不同的 GAN 架构。MNIST 数据集也是多模态的特性。对于这样的数据集,完全崩溃将导致 GAN 生成的只有一个数字,而部分崩溃意味着只生成了一些数字(共 10 个)。图 6.24 展示了普通 GAN 的两种情况:

图 6.24:GAN 的失败模式 - 模式崩溃

图 6.24 展示了模式崩溃如何导致 GAN 能够生成的样本的多样性受限。

无信息量的损失和评估指标

神经网络使用梯度下降进行训练,并改善损失值。然而在 GAN 的情况下(除了 W-GAN 和相关架构),损失值大多无信息量。我们会假设随着训练的进行,生成器损失会持续减少,而鉴别器会达到一个鞍点,但事实并非如此。主要原因是交替训练生成器和鉴别器模型。在任何给定点上,生成器的损失与到目前为止已经训练的鉴别器进行比较,因此很难在训练周期内比较生成器的性能。需要注意的是,在 W-GAN 的情况下,批评者的损失尤其是用来指导改进生成器模型的信号。

除了这些问题,GAN 还需要一个严格的评估指标来了解样本输出的质量。Inception 分数就是计算输出质量的一种方式,然而在这一领域还有识别更好的评估指标的空间。

总结

在本章中,我们介绍了一种名为生成对抗网络(GAN)的新生成模型。受博弈论概念的启发,GAN 提出了一种隐式的建模数据生成概率密度的方法。我们首先将 GAN 放在生成模型的总体分类中,并对比了这些与我们在早期章节介绍过的其他一些方法的不同之处。然后,我们继续通过涵盖极小极大博弈的价值函数以及一些变种,如非饱和生成器损失和最大似然博弈,来了解 GAN 实际上是如何工作的。我们使用 TensorFlow Keras API 开发了基于多层感知器的普通 GAN 来生成 MNIST 数字。

在下一节中,我们触及了一些改进的 GAN,如深度卷积 GAN、条件 GAN 和最后的 Wasserstein GAN。我们不仅探讨了主要的贡献和增强,还建立了一些代码库来训练这些改进的版本。下一节涉及一个称为渐进式 GAN 的高级变体。我们深入讨论了这个高级设置的细节,并使用预训练模型生成了假面孔。在最后一节中,我们讨论了与 GAN 相关的一些常见挑战。

这一章是我们在后续章节中跳入更高级架构之前所需的基础。我们将涵盖计算机视觉领域的其他主题,如风格转移方法、人脸交换/深度伪造等。我们还将涵盖文本和音频等领域的主题。请继续关注!

参考文献

  1. Goodfellow, I J., Pouget-Abadie, J., Mirza, M., Xu, B., Warde-Farley, D., Ozair, S., Courville, A., Bengio, Y. (2014). 生成对抗网络 . arXiv:1406.2661. arxiv.org/abs/1406.2661

  2. 样本:thispersondoesnotexist.com/(左)和 thisartworkdoesnotexist.com/(右)

  3. 改编自 Ian Goodfellow, 2017 年生成对抗网络教程

  4. Goodfellow, I J., Pouget-Abadie, J., Mirza, M., Xu, B., Warde-Farley, D., Ozair, S., Courville, A., Bengio, Y. (2014). 生成对抗网络 . arXiv:1406.2661. arxiv.org/abs/1406.2661

  5. 改编自 CS231 讲座 13: cs231n.stanford.edu/slides/2017/cs231n_2017_lecture13.pdf

  6. Goodfellow, I J., Pouget-Abadie, J., Mirza, M., Xu, B., Warde-Farley, D., Ozair, S., Courville, A., Bengio, Y. (2014). 生成对抗网络 . arXiv:1406.2661. arxiv.org/abs/1406.2661

  7. Radford, A., Metz, L., Chintala, S. (2015). 深度卷积生成对抗网络的无监督表示学习 . arXiv:1511.06434. arxiv.org/abs/1511.06434

  8. Radford, A., Metz, L., Chintala, S. (2015). 深度卷积生成对抗网络的无监督表示学习 . arXiv:1511.06434. arxiv.org/abs/1511.06434

  9. Mirza, M., Osindero, S. (2014). 条件生成对抗网络 . arXiv:1411.1784. arxiv.org/abs/1411.1784

  10. Mirza, M., Osindero, S. (2014). 条件生成对抗网络 . arXiv:1411.1784. arxiv.org/abs/1411.1784

  11. Arjovsky, M., Chintala, S., Bottou, L. (2017). Wasserstein GAN . arXiv:1701.07875. arxiv.org/abs/1701.07875

  12. Arjovsky, M., Chintala, S., Bottou, L. (2017). Wasserstein GAN 。arXiv:1701.07875。arxiv.org/abs/1701.07875

  13. Gulrajani, I., Ahmed, F., Arjovsky, M., Courville, A. (2017). 改善 Wasserstein GANs 的训练 。arXiv:1704.00028。arxiv.org/abs/1704.00028

  14. Karras, T., Aila, T., Laine, S., Lehtinen, J. (2017). "渐进增长的 GANs 以提高质量、稳定性和变化 "。arXiv:1710.10196。arxiv.org/abs/1710.10196

  15. Karras, T., Aila, T., Laine, S., Lehtinen, J. (2017). 渐进增长的 GANs 以提高质量、稳定性和变化 。arXiv:1710.10196。arxiv.org/abs/1710.10196

  16. Bengio Y., Lamblin P., Popovici D., Larochelle H. (2006). 深度网络的贪婪逐层训练 。在第 19 届国际神经信息处理系统会议论文集(NIPS'06)中。MIT 出版社,剑桥,MA,美国,153-160。dl.acm.org/doi/10.5555/2976456.2976476

  17. Karras, T., Aila, T., Laine, S., Lehtinen, J. (2017). 渐进增长的 GANs 以提高质量、稳定性和变化 。arXiv:1710.10196。arxiv.org/abs/1710.10196

  18. 渐进式 GAN 官方实现:github.com/tkarras/progressive_growing_of_gans

  19. Karras, T., Aila, T., Laine, S., Lehtinen, J. (2017). 渐进增长的 GANs 以提高质量、稳定性和变化 。arXiv:1710.10196。arxiv.org/abs/1710.10196

相关推荐
搏博12 分钟前
神经网络问题之一:梯度消失(Vanishing Gradient)
人工智能·机器学习
z千鑫12 分钟前
【人工智能】深入理解PyTorch:从0开始完整教程!全文注解
人工智能·pytorch·python·gpt·深度学习·ai编程
YRr YRr21 分钟前
深度学习:神经网络的搭建
人工智能·深度学习·神经网络
威桑23 分钟前
CMake + mingw + opencv
人工智能·opencv·计算机视觉
爱喝热水的呀哈喽27 分钟前
torch张量与函数表达式写法
人工智能·pytorch·深度学习
肥猪猪爸1 小时前
使用卡尔曼滤波器估计pybullet中的机器人位置
数据结构·人工智能·python·算法·机器人·卡尔曼滤波·pybullet
LZXCyrus1 小时前
【杂记】vLLM如何指定GPU单卡/多卡离线推理
人工智能·经验分享·python·深度学习·语言模型·llm·vllm
我感觉。2 小时前
【机器学习chp4】特征工程
人工智能·机器学习·主成分分析·特征工程
YRr YRr2 小时前
深度学习神经网络中的优化器的使用
人工智能·深度学习·神经网络
DieYoung_Alive2 小时前
一篇文章了解机器学习(下)
人工智能·机器学习