Python-与-TensorFlow2-生成式-AI-全-

Python 与 TensorFlow2 生成式 AI(全)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

“想象力比知识更重要。”

– 阿尔伯特·爱因斯坦,《爱因斯坦关于宇宙宗教和其他见解与格言》(2009)

在本书中,我们将探索生成式人工智能,这是一种使用先进的机器学习算法生成合成(但惊人逼真)数据的尖端技术。生成模型一直以来都引起了跨领域研究人员的兴趣。随着机器学习和更具体地说是深度学习领域的最新进展,生成式建模在研究作品数量和它们在不同领域的应用中迅速增长。从艺术作品和音乐作曲到合成医学数据集,生成建模正在推动想象力和智能的界限。理解、实现和利用这些方法所需的思考和努力量简直令人惊叹。一些较新的方法(如 GANs)非常强大,但难以控制,使得整个学习过程既令人兴奋又令人沮丧。

使用 Python 和 TensorFlow 2 进行生成式人工智能 是我们作者和 Packt Publishing 的才华横溢团队数小时辛勤工作的结果,帮助你理解这个生成式建模领域的 深度广度狂野 空间。本书的目标是成为生成式建模空间的万花筒,并涵盖广泛的主题。本书带你走向旅程,在这个过程中,你不仅仅是读懂理论和了解基础知识,还通过示例发现了这些模型的潜力和影响。我们将使用各种开源技术来实现这些模型——Python 编程语言、用于深度神经网络开发的 TensorFlow 2 库,以及云计算资源,如 Google Colab 和 Kubeflow 项目。

对本书中各种主题、模型、架构和示例的理解将帮助你轻松探索更复杂的主题和尖端研究。

本书适合对象

使用 Python 和 TensorFlow 2 进行生成式人工智能 面向数据科学家、机器学习工程师、研究人员和对生成式建模以及将最先进的架构应用于真实数据集感兴趣的开发者。这本书也适合那些具有中级深度学习相关技能的 TensorFlow 初学者,他们希望扩展自己的知识基础。

开始阅读本书只需要基本的 Python 和深度学习技能。

本书内容包括

第一章生成式人工智能简介:从模型中“提取”数据,介绍了生成式人工智能领域,从概率论基础到最近的这些方法的应用产品。

第二章建立一个 TensorFlow 实验室,描述了如何使用开源工具——Python、Docker、Kubernetes 和 Kubeflow——为使用 TensorFlow 开发生成式人工智能模型设置计算环境,以便在云中运行可扩展的代码实验室。

第三章深度神经网络的构建模块,介绍了深度神经网络的基础概念,这些概念将在本卷的其余部分中被利用——它们是如何受生物研究启发的,研究人员在开发越来越大规模和复杂模型时克服了哪些挑战,以及网络架构、优化器和正则化器的各种构建模块,这些构建模块在本书其余部分的生成式人工智能示例中被利用。

第四章教网络生成数字,演示了如何实现深度置信网络,这是一种突破性的神经网络架构,通过生成式人工智能方法在分类手写数字图像方面取得了最先进的结果,这种方法教会网络在学习对其进行分类之前生成图像。

第五章使用 VAE 用神经网络绘制图片,描述了变分自动编码器(VAEs),这是从深度置信网络发展而来的一种先进技术,通过巧妙地使用贝叶斯统计学中的客观函数来创建复杂对象的更清晰图像。读者将实现一个基本和高级的 VAE,后者利用了逆自回归流(IAF),这是一种递归变换,可以将随机数映射到复杂的数据分布以创建引人注目的合成图像。

第六章使用 GAN 进行图像生成,介绍了生成对抗网络,或 GANs,作为强大的生成建模深度学习架构。从 GANs 的基本构建模块和其他基本概念开始,本章介绍了许多 GAN 架构以及它们如何用于从随机噪声生成高分辨率图像。

第七章使用 GAN 进行风格转移,专注于生成建模的创造性应用,特别是称为风格转移的 GAN。应用例如将黑白图像转换为彩色图像,航拍地图转换为类似谷歌地图的输出,以及去除背景,都可以通过风格转移实现。我们涵盖了许多成对和非成对的架构,如 pix2pix 和 CycleGAN。

第八章使用 GAN 进行深度伪造,介绍了 GAN 的一个有趣且有争议的应用,称为深度伪造。该章节讨论了深度伪造的基本构建模块,例如特征和不同的操作模式,以及一些关键架构。它还包括了一些实际示例,以基于所涵盖的关键概念生成虚假照片和视频,这样读者就可以创建自己的深度伪造流水线。

第九章文本生成方法的兴起,介绍了与文本生成任务相关的概念和技术。我们从深度学习模型中不同的文本向量表示方式入手,讲述了语言生成的基本知识。然后我们介绍了不同的架构选择和解码机制,以实现高质量的输出。本章为后续更复杂的文本生成方法奠定了基础。

第十章NLP 2.0:使用 Transformer 生成文本,介绍了 NLP 领域最新最先进的技术,重点介绍了一些基于 Transformer 架构(如 GPT-x)的最先进的文本生成能力,以及它们如何彻底改变了语言生成和 NLP 领域。

第十一章使用生成模型创作音乐,介绍了使用生成模型创作音乐。这是一种有趣但具有挑战性的生成模型应用,涉及理解与音乐相关的许多细微差别和概念。本章涵盖了许多不同的生成音乐的方法,从基本的 LSTMs 到简单的 GANs,最终到用于多声部音乐生成的 MuseGAN。

第十二章使用生成式 AI 玩游戏:GAIL,描述了生成式 AI 和强化学习之间的联系,强化学习是一种机器学习的分支,教授“代理”在执行指定任务时在真实或虚拟“环境”中导航。通过 GAN 和强化学习之间的联系,读者将通过模仿跳跃运动的专家示例,教会一个跳跃形象在 3D 环境中导航。

第十三章生成式 AI 的新兴应用,描述了最近在生成式 AI 领域的研究,涵盖了生物技术、流体力学、视频和文本合成等各个方面。

要最大限度地发挥本书的效益

要跟上本书的代码,请推荐以下要求:

  • 硬件(用于本地计算):

    • 128GB 硬盘

    • 8GB 内存

    • Intel Core i5 处理器或更高版本

    • NVIDIA 8GB 及以上显卡(GTX1070 及以上)

  • 软件:

    • Python 3.6 及以上版本

    • TensorFlow 2.x

    • Chrome/Safari/Firefox 浏览器(如果在云中训练,则通过 Google Colab 或 Kubeflow 直接执行代码)

下载示例代码文件

本书的代码包托管在 GitHub 上,网址为 github.com/PacktPublishing/Hands-On-Generative-AI-with-Python-and-TensorFlow-2。我们还有其他书籍和视频的代码包,请访问github.com/PacktPublishing/。一并查看吧!

下载彩色图片

我们还提供了该书中所使用的屏幕截图/图表的彩色图像的 PDF 文件。您可以在此处下载:static.packt-cdn.com/downloads/9781800200883_ColorImages.pdf

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词,数据库表名,文件夹名,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 用户名。例如:"我们可以使用show_examples()函数直观地绘制一些示例。"

一段代码块设置如下:

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) 

当我们希望引起您对代码块特定部分的注意时,相关行或项目将用粗体显示:

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) 

任何命令行输入或输出如下所示:

pip install tensorflow-datasets 

粗体:表示一个新术语,一个重要词汇,或者您在屏幕上看到的单词,在菜单或对话框中也会以这种方式呈现在文本中。例如:"从管理面板中选择系统信息"。

警告或重要说明会出现在此处。

提示和技巧会出现在此处。

联系我们

我们的读者的反馈总是受欢迎的。

一般反馈:如果您对本书的任何方面有疑问,请在消息主题中提及书名,发送电子邮件至customercare@packtpub.com

勘误:尽管我们已尽一切努力确保内容的准确性,但错误是无法避免的。如果您在本书中发现了错误,我们将不胜感激。请访问www.packtpub.com/support/errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

盗版:如果您在互联网上发现我们作品的任何非法复制形式,我们将不胜感激,如果您提供给我们位置地址或网站名称。请联系我们,发送邮件至copyright@packtpub.com并附上链接到该材料的链接。

如果您有兴趣成为作者:如果您在某个专题上有专业知识,并且有兴趣撰写或为一本书作出贡献,请访问authors.packtpub.com

评论

请留下评论。一旦您阅读并使用了本书,为什么不在您购买的网站上留下评论呢?潜在读者可以看到并使用您的客观意见来做出购买决定,我们在 Packt 可以了解到您对我们产品的看法,我们的作者也能看到您对他们书籍的反馈。谢谢!

有关 Packt 的更多信息,请访问packtpub.com

第一章:生成人工智能简介:“从模型中“勾画”数据

在本章中,我们将深入探讨生成模型的各种应用。在此之前,我们将退后一步,详细研究生成模型与其他类型的机器学习的区别。区别在于任何机器学习算法的基本单位:概率以及我们用数学量化我们遇到的数据的形状和分布的各种方法。

在本章的其余部分,我们将 covers:

  • AI 的应用

  • 判别和生成模型

  • 实施生成模型

  • 概率的规则

  • 为什么使用生成模型?

  • 生成模型的独特挑战

AI 的应用

在 2018 年 10 月的纽约,国际拍卖行佳士得在印刷品与多重品展销会上以 43.25 万美元的价格出售了埃德蒙·贝拉米的肖像(图 1.1)。这次销售之所以引人注目,既因为销售价格比这件作品的初步估计高出 45 倍,也因为这幅肖像的非同寻常的起源。与佳士得自 18 世纪以来出售的大多数其他艺术品不同,埃德蒙·贝拉米的肖像既不是用油画或水彩画完成的,其创作者甚至不是人类;相反,它是由一个复杂的机器学习算法完全产生的数字图像。创作者——一个名为 Obvious 的巴黎集体利用了从 14 世纪到 20 世纪创作的 1.5 万幅肖像的集合来调整一个能够生成美学上类似但合成的图像的人工神经网络模型。

图 1.1:埃德蒙·贝拉米的肖像¹

画像远非唯一一个展示机器学习惊人成果的领域。事实上,如果你在过去几年关注新闻,你可能已经看到许多关于现代 AI 系统应用于各种问题的开创性成果的故事,从硬科学到数字艺术。像 Obvious 创建的深度神经网络模型现在可以在训练有素的医生水平上对人体解剖的 X 光图像进行分类,²在传统棋类游戏如围棋(一种类似国际象棋的亚洲游戏)³和多人游戏⁴中战胜人类大师,并且对法语翻译成英语时对语法细微差别表现出惊人的敏感性。⁵

判别模型和生成模型

这些人工智能的其他示例与生成埃德蒙·贝拉米的肖像的模型在一个重要的方面有所不同。在所有这些其他应用中,模型被呈现一组输入数据,例如英文文本、X 射线图像或游戏棋盘上的位置,这些数据与目标输出配对,例如翻译句子中的下一个词、X 射线图的诊断分类或游戏中的下一步。事实上,这可能是您在以往预测建模经验中最熟悉的 AI 模型类型;它们被广泛称为判别模型,其目的是在一组输入变量和目标输出之间创建映射。目标输出可以是一组离散类别(例如在翻译中下一个出现的英文单词),也可以是连续结果(例如预期一个客户在接下来的 12 个月内在在线商店中的消费金额)。

应该注意的是,这种模型,其中数据被标记评分,仅代表了现代机器学习能力的一半。另一类算法,比如生成了在佳士得拍卖会上出售的人造肖像的算法,不会从输入变量中计算得分或标签,而是生成新数据。与判别模型不同,输入变量通常是与现实世界值无关的数字向量,甚至经常是随机生成的。这种模型——被称为生成模型——可以从随机噪声中产生复杂的输出,例如文本、音乐或图像,并且是本书的主题。

即使您当时不知道,您可能也曾在新闻中看到其他生成模型的实例,与前面提到的判别示例并列。一个突出的例子是深度伪造,这是一种视频,其中一个人的脸被系统地替换为另一个人的脸,通过使用神经网络重新映射像素。⁶

图 1.2:一个深度伪造图像⁷

也许您还看到过关于 AI 模型生成虚假新闻的报道,最初由 OpenAI 公司的科学家因担心其可能被用于在线制造宣传和误导而感到恐慌。⁸

图 1.3:使用 GPT-2 创建的聊天机器人对话⁹

在这些以及其他应用中,例如谷歌的语音助手 Duplex,它可以通过与人类实时动态创建对话来进行餐厅预订¹⁰,或者可以生成原创音乐作品的软件¹¹,我们被环绕着生成式人工智能算法的输出。

图 1.4:使用生成对抗网络(GANs)进行风格迁移的示例¹²

这些模型能够处理各种领域的复杂信息:创建逼真的图像或照片上的滤镜 (图 1.4),合成声音,对话文本,甚至是最佳玩视频游戏的规则。你可能会问,这些模型从哪里来?我如何能自己实现它们?我们将在下一节中更多地讨论这个问题。

实现生成模型

尽管生成模型理论上可以使用各种机器学习算法来实现,但在实践中,它们通常是通过深度神经网络构建的,这些网络非常适合捕捉图像或语言等数据的复杂变化。

在本书中,我们将专注于使用TensorFlow 2.0来实现这些深度生成模型的许多不同应用。TensorFlow 是一个 C++框架,有 Python 编程语言的 API,用于开发和应用深度学习模型。它是谷歌在 2013 年开源的,并且已经成为研发和部署神经网络模型的最受欢迎的库之一。

随着 2.0 版的发布,以前版本中开发的那些样板代码得到了清理,使用了高级抽象层,使我们能够专注于模型而不是计算过程的其它部分。最新版本还引入了即时执行的概念,允许网络计算按需运行,这将是我们实现某些模型的重要好处。

在接下来的章节中,你将学习不仅是这些模型背后的理论,还有在流行的编程框架中实现它们所需的实际技能。在第二章建立一个 TensorFlow 实验室中,你将学习如何设置一个云环境,以便你可以使用Kubeflow框架来分布式运行 TensorFlow,并记录你的实验。

实际上,正如我将在第三章深度神经网络的构建模块中更详细地描述的那样,自 2006 年以来,大规模神经网络模型的深度学习研究已经产生了各种各样的生成模型应用。其中第一个是受限玻尔兹曼机,它被堆叠在多个层中以创建深度信念网络。我将在第四章教网络生成数字中描述这两种模型。后来的创新包括变分自动编码器VAEs),它们可以有效地从随机数生成复杂的数据样本,我将在第五章使用 VAEs 用神经网络绘制图片中描述这些技术。

我们还将在本书的第六章使用 GAN 生成图像中更详细地讨论用于创建埃德蒙·贝拉米的肖像的算法 GAN。从概念上讲,GAN 模型在两个神经网络之间创建竞争。一个(称为生成器)从一组随机数开始生成逼真的(或者在 Obvious 的实验中,艺术性的)图像,并应用数学变换。在某种意义上,生成器就像一名艺术学生,从笔触和创意灵感中创作新的绘画。

第二个网络,被称为判别器,试图分类一幅图片是否来自一组真实世界的图片,还是由生成器创建。因此,判别器就像一个老师,评分学生是否产生了与他们试图模仿的绘画相媲美的作品。随着生成器变得越来越擅长欺骗判别器,它的输出越来越接近于它被设计来复制的历史示例。

有许多类 GAN 模型,附加变体在第七章使用 GAN 进行风格迁移,和第十一章使用生成模型创作音乐,在我们讨论高级模型时涵盖。生成模型中的另一个关键创新是在自然语言数据领域。通过以一种计算可扩展的方式代表句子中单词之间的复杂相互关系,Transformer 网络及其基于其之上构建的双向编码器 TransformerBERT)模型呈现了在应用中生成文本数据的强大构建模块,如聊天机器人,在第九章文本生成方法的崛起,和第十章NLP 2.0:使用 Transformer 生成文本中我们将更详细地讨论。

第十二章使用生成式 AI 玩视频游戏:GAIL 中,您还将看到诸如 GAN 和 VAE 等模型如何被用于生成不仅仅是图像或文本,而是一组允许使用强化学习算法开发的游戏网络更高效地处理和导航其环境的规则集——本质上,学会学习。生成模型是一个不断增长的巨大研究领域,所以遗憾的是,我们无法在本书中涵盖每一个主题。对于感兴趣的读者,进一步主题的参考资料将在第十三章生成式 AI 的新兴应用中提供。

要开始一些背景信息,让我们讨论一下概率规则。

概率规则

在最简单的层面上,模型,无论是用于机器学习还是更经典的方法,如线性回归,都是关于各种数据如何相互关联的数学描述。

在建模任务中,我们通常考虑将数据集的变量分成两大类:

  1. 独立数据,主要是指模型的 输入,用 X 表示。这些可以是分类特征(例如某学生所在的六所学校中的“0”或“1”),连续的(例如相同学生的身高或考试成绩),或序数的(班级中学生的排名)。

  2. 依赖数据 相反,则是我们模型的输出,用 Y 表示。(请注意,在某些情况下,Y 是一个 标签,可用于条件产生输出,例如在条件 GAN 中。)与自变量一样,这些可以是连续的、分类的或序数的,它们可以是数据集每个元素的个体元素或多维矩阵(张量)。

那么如何使用统计描述我们模型中的数据呢?换句话说,我们如何定量描述我们可能看到的值,以及频率如何,哪些值更有可能一起出现?一种方式是询问数据中观察特定值的可能性,或者该值的概率。例如,如果我们想知道掷六面骰子出现 4 的概率是多少,答案是平均来说,我们会在六次掷骰子中看到一次 4。我们写为:

P(X=4) = = 16.67%

其中 P 表示 概率为

什么定义了特定数据集的允许概率值?如果我们想象数据集所有可能值的集合,比如掷骰子的所有值,那么概率将每个值映射到 0 和 1 之间的数。最小值是 0,因为我们不可能有看到结果的负概率;最不可能的结果是我们永远不会看到特定值的机会,或者是 0%的概率,比如在六面骰子上掷出 7。同样,我们也不可能有大于 100%的观察结果概率,其值为 1;概率为 1 的结果是绝对确定的。与数据集相关联的这组概率值属于离散类(例如骰子的面)或无限潜在值的集合(例如身高或体重的变化)。不过,在任一情况下,这些值必须遵循某些规则,这些规则是由数学家安德烈·科尔莫戈洛夫在 1933 年描述的 概率公理

  1. 观察的概率(掷骰子点数、特定身高等)是 0 到 1 之间的非负有限数。

  2. 在所有可能观察空间中至少出现一项观察结果的概率是 1。

  3. 不同、互斥事件的联合概率是各个事件概率的总和。

尽管这些规则可能看起来抽象,但你将会在第三章深度神经网络的基础组件中看到,它们与开发神经网络模型直接相关。例如,规则 1 的一个应用是在预测目标类别的softmax函数中生成介于 1 和 0 之间的概率。规则 3 用于将这些结果归一化到 0-1 范围内,保证它们是深度神经网络的相互独立的预测(换句话说,实际世界中的图像逻辑上不能同时被分类为狗和猫,而只能是狗或猫,这两个结果的概率是可加的)。最后,第二条规则提供了我们可以使用这些模型生成数据的理论保证。

然而,在机器学习和建模的背景下,我们通常不只是对观察到一条输入数据的概率X感兴趣;我们更想知道,基于这些数据,Y条件概率是多少。换句话说,我们想知道基于数据对一组数据的标签有多大可能性。我们将此表示为给定 X 的 Y 的概率,或者给定 X 条件下的 Y 的概率

P(Y|X)

我们还可以询问关于YX的另一个问题,即它们一起发生或它们的联合概率有多大,这可以使用前面的条件概率表达式表示如下:

P(X, Y) = P(Y|X)P(X) = P(X|Y)(Y)

此公式表示X 和 Y 的概率。在XY完全独立的情况下,这就是它们的乘积:

P(X|Y)P(Y) = P(Y|X)P(X) = P(X)P(Y)

你会发现,这些表达式在我们讨论第四章教授网络生成数字中的补充先验受限玻尔兹曼机模拟独立数据样本的能力中变得重要。它们也是贝叶斯定理的构建模块,我们将在下一节中讨论。

判别建模和生成建模以及贝叶斯定理

现在让我们考虑这些条件和联合概率规则如何与我们为各种机器学习应用构建的预测模型相关联。在大多数情况下——比如预测电子邮件是否欺诈性或客户未来生命周期价值的美元金额——我们对条件概率P(Y|X=x)感兴趣,其中Y是我们试图建模的结果集,X表示输入特征,x是输入特征的特定值。如前所述,这种方法被称为判别建模。¹⁴判别建模试图学习数据X与结果Y之间的直接映射。

另一种理解判别建模的方法是在贝叶斯定理的背景下,¹⁵它关联了数据集的条件和联合概率:

P(Y|X) = P(X|Y)P(Y)/P(X) = P(X, Y)/P(X)

在贝叶斯公式中,表达式P(X|Y)/P(X)被称为似然或观察到的X给出Y观察概率的支持证据。P(Y)X先验或结果的合理性,P(Y|X)X后验或给出到目前为止与结果相关的所有独立数据的观察概率。概念上,贝叶斯定理表明结果的概率是其基线概率与此结果的输入数据条件概率的乘积。

这个定理是作者去世两年后发表的,在前言中理查德·普赖斯描述它为上帝存在的一个数学论证,这或许是适当的,因为托马斯·贝叶斯在生前是一位牧师。

在判别学习的背景下,我们可以看到判别模型直接计算后验概率;我们可以有似然或先验模型,但在这种方法中并不需要。即使你可能没有意识到,你可能在机器学习工具包中使用的大多数模型都是判别模型,例如以下模型:

  • 线性回归

  • 逻辑回归

  • 随机森林

  • 梯度提升决策树(GBDT)

  • 支持向量机(SVM)

前两种(线性回归和逻辑回归)是在数据X的条件下模型结果Y,使用正态或高斯函数(线性回归)或 S 型函数(逻辑回归)。相比之下,最后三种没有正式的概率模型—它们计算将X映射到Y的函数(一组树用于随机森林或 GDBT,或者 SVM 的内积分布),使用损失或错误函数来调整这些估计值。鉴于这种非参数性质,一些作者认为这构成了一类非模型判别算法。(16)

相比之下,生成模型试图学习标签和输入数据的联合分布P(Y, X)。回想一下通过联合概率的定义:

P(X, Y) = P(X|Y)P(Y)

我们可以将贝叶斯定理重写如下:

P(Y|X) = P(X, Y)/P(X)

与判别情况不同,我们的目标不是使用P(Y|X)直接映射XY,而是模拟XY的联合概率P(X, Y)。虽然我们可以使用XY的联合分布来计算后验概率P(Y|X)并学习一个目标模型,但我们也可以使用此分布来通过联合采样新的元组(x, y)或使用目标标签Y采样新的数据输入,使用以下表达式:

P(X|Y=y) = P(X, Y)/P(Y)

生成模型的例子包括以下内容:

  • 朴素贝叶斯分类器

  • 高斯混合模型

  • 潜在狄利克雷分配(LDA)

  • 隐马尔可夫模型

  • 深度波尔兹曼机

  • 变分自动编码器(VAEs)

  • 生成对抗网络(GANs)

朴素贝叶斯分类器,虽然被称为判别模型,但利用贝叶斯定理来学习XY的联合分布,假设X变量是独立的。同样,高斯混合模型描述了数据点属于一组正态分布之一的可能性,使用标签和这些分布的联合概率。

LDA 将文档表示为单词和一组潜在关键字列表(主题)的联合概率,这些关键字列表在文档中使用。隐马尔可夫模型表示数据的状态和下一个状态的联合概率,例如一周中连续几天的天气。正如你将在第四章中看到的,教网络生成数字,深度波尔兹曼机学习标签和与之相关的数据向量的联合概率。我们将在第 5、6、7 和 11 章中涵盖的 VAE 和 GAN 模型也利用联合分布来映射复杂的数据类型。这种映射允许我们从随机向量生成数据,或者将一种数据转换为另一种。

如前所述,生成模型的另一个观点是,如果我们知道一个结果Y,它们允许我们生成X的样本。在前述列表中的前四个模型中,这种条件概率只是模型公式的一个组成部分,而后验估计仍然是最终目标。然而,在最后三个示例中,它们都是深度神经网络模型,学习关于一个隐藏或潜在变量ZX的条件是实际上的主要目标,以便生成新的数据样本。利用多层神经网络所允许的丰富结构,这些模型可以近似表示复杂数据类型的分布,如图像、自然语言和声音。此外,Z不再是目标值,而在这些应用中通常是一个随机数,仅用作从中生成大量假设数据点的输入。在我们有标签的程度上(比如一个生成的图像应该是狗还是海豚,或者一个生成的歌曲的流派),模型就是P(X|Y=y, Z=z),其中标签Y控制着除了Z的随机性外其他数据的生成。

为什么使用生成模型?

现在我们已经回顾了生成模型的内容,并在概率语言中更正式地定义了它们,为什么我们首先需要这样的模型呢?它们在实际应用中提供了什么价值?为了回答这个问题,让我们简要地浏览一下我们将在本书的其余部分更详细地讨论的主题。

深度学习的承诺

正如已经指出的,我们将在本书中调查的许多模型都是深度的、多级的神经网络。过去 15 年来,深度学习模型在图像分类、自然语言处理和理解以及强化学习方面取得了复兴。这些进展是由于在调整和优化非常复杂的模型方面的传统挑战的突破,再加上对更大的数据集、分布式计算能力的访问以及诸如 TensorFlow 这样的框架,使得原型设计和复制研究变得更加容易。

构建更好的数字分类器

用于在机器学习和计算机视觉中基准算法的一个经典问题是对来自 MNIST 数据集的像素化图像中表示的 0-9 之间的哪个手写数字进行分类的任务。¹⁷ 在这个问题上取得的一个重大突破发生在 2006 年,当时多伦多大学和新加坡国立大学的研究人员发现了一种训练深度神经网络执行此任务的方法。¹⁸

他们的一个关键观察是,与其训练一个网络直接预测给定图像(X)的最可能的数字(Y),不如首先训练一个可以生成图像的网络,然后作为第二步对它们进行分类。在第四章教网络生成数字中,我将描述这个模型是如何改进过去的尝试的,并且如何创建自己的受限玻尔兹曼机深度玻尔兹曼机模型,这些模型可以生成新的 MNIST 数字图像。

生成图像

使用 MNIST 数据集的方法生成像Edmond Belamy 的肖像这样的图像的一个挑战是,图像通常没有标签(如数字);相反,我们想要使用一个潜在向量Z将随机数空间映射到一组人工图像,就像我在本章中早些时候描述的那样。

另一个限制是我们希望促进这些图像的多样性。如果我们输入在某个范围内的数字,我们希望知道它们生成不同的输出,并且能够调整生成的图像特征。为此,VAE 被开发出来生成多样化和逼真的图像(图 1.5)。

图 1.5:来自 VAE 的样本图像¹⁹

在图像分类任务的背景下,能够生成新图像可以帮助我们增加现有数据集中的示例数量,或者如果我们现有数据集严重偏向于特定类型的照片,则减少偏差。应用可能包括为时尚电子商务网站的产品照片生成替代姿势(角度、色调或透视镜头)(图 1.6):

图 1.6:使用深度生成模型模拟替代姿势²⁰

风格转移和图像变换

除了将人工图像映射到随机数空间之外,我们还可以使用生成模型学习将一种图像映射到另一种图像。这种模型可以用于将一幅马的图像转换为斑马的图像(图 1.7),创建深度伪造视频,其中一个演员的脸被另一个演员的脸替换,或将照片转换为绘画(图 1.21.4):²¹

图 1.7:CycleGAN 将条纹应用于马身上以生成斑马²²

应用生成建模的另一个引人注目的例子是一项研究,该研究发现了艺术家巴勃罗·毕加索的失落杰作被另一幅图像所覆盖的情况。在对《老吉他手》《蹲着的乞丐》进行 X 射线成像后,显示了一个女人和一个风景的早期图像位于其下面(图 1.8),研究人员使用了毕加索蓝色时期的其他绘画或其他彩色照片(图 1.8)来训练神经风格转移模型,该模型将黑白图像(覆盖绘画的 X 射线放射图)转换为原始艺术作品的着色。然后,将这种转换模型应用于隐藏图像使他们能够重建着色版本的丢失绘画:

图 1.8:深度学习被用于给绘画场景的 X 射线图着色(中),通过从示例中学习颜色模式(列 d)生成失落艺术品的着色版本(极右)²³

所有这些模型都使用了之前提到的 GAN,这是一种在 2014 年提出的深度学习模型²⁴。除了改变图像的内容(如前面斑马的例子中),这些模型还可以用于将一幅图像映射到另一幅图像,比如成对的图像(例如具有相似面部特征的狗和人,如图 1.9),或者从图像生成文本描述(图 1.10):

图 1.9:Sim-GAN 用于将人类映射到动物或动漫脸部²⁵

图 1.10:Caption-GAN 用于从图像生成描述²⁶

我们还可以根据一些辅助信息如标签来调节生成图像的属性,这是 GAN-Gogh 算法中采用的方法,它通过将期望的艺术家作为输入提供给生成模型来合成不同艺术家风格的图像(图 1.4)。²⁷我将在第六章使用 GAN 生成图像第七章使用 GAN 进行风格转移中描述这些应用。

虚假新闻和聊天机器人

人类一直想要与机器交谈;第一个聊天机器人,ELIZA,²⁸是在 1960 年代在麻省理工学院编写的,它使用一个简单的程序来转换用户的输入并生成一个回应,以心理治疗师的方式频繁地以问题形式回应。

更复杂的模型可以生成全新的文本,例如谷歌的 BERT 和 GPT-2,^(29 30)它们使用一种称为Transformers的单元。神经网络中的Transformers模块允许网络在文本中的前导词汇上提出新词汇,在生成合理的语言片段时强调更相关的词汇。然后,BERT 模型将Transformers单元结合成自然语言模式和上下文重要性的强大多维编码。此方法可用于自然语言处理NLP)任务的文档创建,或用于聊天机器人对话系统(图 1.3)。

声音合成

声音,如图像或文本,是一种复杂的、高维度的数据。音乐尤其复杂:它可能涉及一个或多个音乐家,具有时间结构,并且可以分为主题相关的段落。所有这些组成部分都被纳入到先前提到的模型中,比如 MuseGAN,该模型使用 GAN 生成这些各种组件,并将它们合成为逼真但合成的音乐曲目。我将在第十一章“用生成模型创作音乐”中描述 MuseGAN 及其变种的实施。

游戏规则

前述应用涉及我们可以看到、听到或阅读的数据类型。然而,生成模型还可以用于生成规则。这在深度学习的一种流行应用中很有用:使用算法玩棋盘游戏或 Atari 视频游戏。³¹

虽然这些应用传统上使用强化学习RL)技术来训练网络以在这些游戏中采用最佳策略,但新的研究表明使用 GAN 来提出新规则作为训练过程的一部分,³²或生成合成数据来激发整体学习过程。³³我们将在第十二章“用生成 AI 玩视频游戏:GAIL”中研究这两种应用。

生成模型的独特挑战

鉴于生成模型具有的强大应用,实施它们的主要挑战是什么?正如所述,这些模型大多利用复杂数据,需要我们拟合大型模型来捕捉其特征和分布的所有细微差别。这对于我们必须收集的示例数量以充分代表我们试图生成的数据类型,以及构建模型所需的计算资源都有影响。我们将在第二章“设置 TensorFlow 实验室”中讨论使用云计算框架和图形处理单元GPU)并行训练这些模型的技术。

由于数据复杂性及我们试图生成数据而不是数字标签或值的事实,我们对模型准确度的概念变得更加复杂:我们不能简单地计算到单个标签或分数的距离。

我们将在第五章使用 VAE 进行神经网络绘图,以及第六章使用 GAN 进行图像生成中讨论深度生成模型(如 VAE 和 GAN 算法)采用不同方法来确定生成图像是否与真实世界图像可比。最后,正如提到的,我们的模型需要允许我们生成大量和多样化的样本,而我们将讨论的各种方法采用不同的方法来控制数据的多样性。

摘要

在本章中,我们讨论了生成建模是什么,以及它如何适应更熟悉的机器学习方法的格局。我使用概率论和贝叶斯定理来描述这些模型如何以与判别式学习相反的方式进行预测。

我们审查了生成式学习的用例,既针对特定类型的数据,也针对一般性的预测任务。最后,我们还研究了构建这些模型所面临的一些专门挑战。

在下一章中,我们将通过探索如何使用 Docker 和 Kubeflow 为 TensorFlow 2.0 设置开发环境,开始实际实现这些模型。

参考文献

  1. www.christies.com/features/A-collaboration-between-two-artists-one-human-one-a-machine-9332-1.aspx

  2. Baltruschat, I.M., Nickisch, H., Grass, M.等(2019)。多标签胸部 X 射线分类的深度学习方法比较。Sci Rep 9, 6381。doi.org/10.1038/s41598-019-42294-8

  3. AlphaGo(无日期)。DeepMind。2021 年 4 月 20 日检索自deepmind.com/research/case-studies/alphago-the-story-so-far

  4. AlphaStar 团队(2019 年 10 月)。AlphaStar:使用多智能体强化学习在《星际争霸 II》中达到大师级水平。DeepMind。deepmind.com/blog/article/AlphaStar-Grandmaster-level-in-StarCraft-II-using-multi-agent-reinforcement-learning

  5. Devlin, J., Chang, M., Lee, K., Toutanova, K.(2019)。BERT:用于语言理解的深度双向转换器的预训练。arXiv。arxiv.org/abs/1810.04805v2

  6. Brandon, J.(2018 年 2 月 16 日)。可怕的高科技色情:令人毛骨悚然的“深度假视频”正在兴起。福克斯新闻。www.foxnews.com/tech/terrifying-high-tech-porn-creepy-deepfake-videos-are-on-the-rise

  7. seanbmcgregor.com/DeepfakeDetectionGame.html

  8. 更好的语言模型及其影响。(2019 年 2 月 14 日)。OpenAI。openai.com/blog/better-language-models/

  9. devopstar.com/static/2293f764e1538f357dd1c63035ab25b0/d024a/fake-facebook-conversation-example-1.png

  10. Leviathan Y., Matias Y. (2018 年 5 月 8 日)。Google Duplex:用于电话实现真实世界任务的 AI 系统。Google AI 博客。 ai.googleblog.com/2018/05/duplex-ai-system-for-natural-conversation.html

  11. Hao-Wen Dong, Wen-Yi Hsiao, Li-Chia Yang 和 Yi-Hsuan Yang。MuseGANsalu133445.github.io/musegan/

  12. neurohive.io/wp-content/uploads/2018/06/neural-style-transfer-example-e1530287419338.jpg

  13. Kolmogorov A. N., (1956). 概率论基础.(第 2 版)。纽约:切尔西出版公司。 www.york.ac.uk/depts/maths/histstat/kolmogorov_foundations

  14. Jebara, Tony., (2004). 机器学习:判别式与生成式。Kluwer Academic (Springer)。www.springer.com/gp/book/9781402076473

  15. Bayes Thomas, (1763) LII. 解决机会命理学问题的尝试。由已故牧师 Bayes 先生 F.R.S.与约翰坎顿先生的信交流。R. Soc.53370–418. royalsocietypublishing.org/doi/10.1098/rstl.1763.0053

  16. Jebara, Tony., (2004). 机器学习:判别式与生成式。Kluwer Academic (Springer)。www.springer.com/gp/book/9781402076473

  17. yann.lecun.com/exdb/mnist/

  18. G. Hinton, S. Osindero, & Y.-W. Teh. (2005). 用于深度信念网络的快速学习算法www.cs.toronto.edu/~fritz/absps/ncfast.pdf

  19. jaan.io/images/variational-autoencoder-faces.jpgmiro.medium.com/max/2880/1*jcCjbdnN4uEowuHfBoqITQ.jpeg

  20. Esser, P., Haux, J., Ommer, B., (2019). 用于图像合成的无监督稳健潜在特征分解. arXiv。arxiv.org/abs/1910.10223

  21. CycleGAN。TensorFlow Core。2021 年 4 月 26 日检索自 www.tensorflow.org/tutorials/generative/cyclegan

  22. www.tensorflow.org/tutorials/generative/images/horse2zebra_2.png

  23. Bourached, A., Cann, G. (2019). 失落的艺术品. arXiv:1909.05677. arxiv.org/pdf/1909.05677.pdf

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

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

  26. Gorti, S. K., Ma, Jeremy (2018). 使用循环一致性对抗网络的文本-图像-文本翻译. arXiv. arxiv.org/abs/1808.04538

  27. rkjones4, adam-hanna, erincr & rodrigobdz (2020). GANGogh. GitHub 代码库. github.com/rkjones4/GANGogh

  28. Weizenbaum Joseph. (1976) 计算机与人类的思维. W. H. Freeman and company. blogs.evergreen.edu/cpat/files/2013/05/Computer-Power-and-Human-Reason.pdf

  29. Schwartz B., (2019, October 25). 欢迎 BERT:谷歌最新的搜索算法,以更好地理解自然语言. Search Engine Land. searchengineland.com/welcome-bert-google-artificial-intelligence-for-understanding-search-queries-323976

  30. 更好的语言模型及其影响. (2019, February 14). OpenAI. openai.com/blog/better-language-models/

  31. Mnih V., Kavukcuoglu K., Silver D., Graves A., Antonoglou I., Wierstra D., Riedmiller M. (2013, January 01). 使用深度强化学习玩 Atari 游戏. DeepMind. deepmind.com/research/publications/playing-atari-deep-reinforcement-learning

  32. Liu, Yang; Zeng, Yifeng; Chen, Yingke; Tang, Jing; Pan, Yinghui (2019). 自我改进的生成对抗强化学习. AAMS 2019. www.ifaamas.org/Proceedings/aamas2019/pdfs/p52.pdf

  33. Kasgari, A T Z, Saad, W., Mozaffari, M., Poor, H V (2020). 经验丰富的深度生成对抗强化学习用于无模型超可靠低延迟通信. arXiv. arxiv.org/abs/1911.03264

第二章:设置 TensorFlow 实验室

现在你已经在 第一章 中看到了生成模型的所有令人惊叹的应用,你可能想知道如何开始实施这些使用这些算法的项目。在本章中,我们将介绍一些工具,这些工具将在本书的其余部分中用于实现各种生成式 AI 模型中使用的深度神经网络。我们的主要工具是由 Google^(1 2) 开发的 TensorFlow 2.0 框架;然而,我们还将使用其他一些资源来简化实现过程(在 2.1 表格 中总结)。

我们可以广泛分类这些工具:

  • 用于可复制的依赖管理的资源(Docker,Anaconda)

  • 数据整理和算法开发的探索性工具(Jupyter)

  • 部署这些资源到云端并管理其生命周期的工具(Kubernetes,Kubeflow,Terraform)

工具 项目网址 用途
Docker www.docker.com/ 应用程序运行时依赖封装
Anaconda www.anaconda.com/ Python 语言包管理
Jupyter jupyter.org/ 交互式 Python 运行时和绘图/数据探索工具
Kubernetes kubernetes.io/ Docker 容器编排和资源管理
Kubeflow www.kubeflow.org/ 基于 Kubernetes 开发的机器学习工作流引擎
Terraform www.terraform.io/ 用于可配置和一致部署 Kubeflow 和 Kubernetes 的基础设施脚本语言
VSCode code.visualstudio.com/ 集成开发环境(IDE)

表格 2.1:用于生成对抗模型开发的技术栈

在本章中,我们将在我们将我们的代码从笔记本电脑移到云端的旅程中,首先描述一些有关 TensorFlow 在本地运行时的背景知识。然后,我们将描述一系列软件工具,这些工具将使在本地或云端运行全面的 TensorFlow 实验更加容易,如笔记本、容器和集群管理器等。最后,我们将通过一个简单的实例来介绍如何建立一个可重现的研究环境,运行本地和分布式训练,并记录我们的结果。我们还将探讨如何在一台机器内的多个 CPU/GPU 单元(纵向扩展)和云端的多台机器(横向扩展)上并行化 TensorFlow 以加速训练。通过本章的学习,我们将准备好扩展这个实验室框架,用各种生成式 AI 模型来实施项目。

首先,让我们深入了解 TensorFlow 的细节,这是本书剩余部分将用于开发模型的库。 TensorFlow 解决了神经网络模型开发中的哪些问题?它采用了哪些方法?它在多年来如何发展?为了回答这些问题,让我们回顾一些促成 TensorFlow 发展的深度神经网络库的历史。

深度神经网络的发展和 TensorFlow

正如我们将在 第三章 深度神经网络的构建模块 中看到的那样,深度神经网络本质上由矩阵运算(加法、减法、乘法)、非线性变换和使用这些组件的导数进行的基于梯度的更新组成。

在学术界,研究人员通常使用诸如 MATLAB³ 这样的高效原型工具来运行模型和准备分析。虽然这种方法允许进行快速实验,但它缺乏工业软件开发的元素,例如 面向对象OO)开发,它可以实现可重现性和干净的软件抽象,从而使工具可以被大型组织所采用。这些工具对于大型数据集的扩展也存在困难,并且对于此类工业用途可能会带来繁重的许可费用。然而,在 2006 年之前,这种计算工具在大多数情况下仍然足够。然而,随着应用深度神经网络算法处理的数据集的增长,取得了突破性的成果,如:

  • ImageNet 数据集上的图像分类⁴

  • 在 YouTube 视频中大规模无监督地发现图像模式⁵

  • 创造了能够像人类一样能够玩雅达利视频游戏和围棋的人工智能代理^(6 7)

  • 通过 Google 开发的 BERT 模型实现的最先进的语言翻译⁸

在这些研究中开发的模型随着它们应用到的数据集的规模而变得复杂起来(请参阅表 2.2 以了解其中一些模型的巨大规模)。由于工业用例需要稳健且可扩展的框架来开发和部署新的神经网络,一些学术团体和大型技术公司投资于通用工具包的开发,用于实现深度学习模型。这些软件库将常见模式编码为可重用的抽象,使即使复杂的模型通常也可以体现在相对简单的实验脚本中。

模型名称 年份 # 参数
AlexNet 2012 61M
YouTube CNN 2012 1B
Inception 2014 5M
VGG-16 2014 138M
BERT 2018 340M
GPT-3 2020 175B

表 2.2:按年份分类的模型参数数量

这些框架的早期示例包括 Theano,⁹ 蒙特利尔大学开发的 Python 包,以及 Torch,¹⁰ 由 Facebook 的研究人员在 Lua 语言中编写,后来被转移到 Python,以及由 Google¹¹开发的具有 Python 绑定的 C++运行时的 TensorFlow。

在本书中,我们将主要使用 TensorFlow 2.0,因为它被广泛采用,并且它具有方便的高级界面Keras,可以抽象出大部分关于定义常规层和模型架构的重复工作。

TensorFlow 是 Google 内部称为DistBelief的工具的开源版本。¹²DistBelief 框架由分布式工作者(在一组机器上运行的独立计算过程)组成,这些工作者会对网络进行正向和反向梯度下降处理(我们将在第三章深度神经网络的构建基块中讨论训练神经网络的常见方式),并将结果发送给参数服务器进行更新。DistBelief 框架中的神经网络被表示为一个有向无环图DAG),以损失函数结束,产生与观察目标(如图像类别或代表翻译模型中句子中最可能的下一个词的词汇概率分布)进行比较的标量(数值)。

DAG 是一种软件数据结构,由节点(操作)和数据()组成,信息仅沿着边的单向流动(因此有向),而且没有循环(因此无环)。

尽管 DistBelief 允许 Google 生产出多个大型模型,但它有一些限制:

  • 首先,Python 脚本接口是使用一组对应 C++中底层实现的预定义层开发的;添加新颖的层类型需要在 C++中编码,这对生产力构成了障碍。

  • 其次,虽然该系统非常适合使用基本的随机梯度下降SGD)(我们将在第三章深度神经网络的构建基块中更详细地描述的算法)在大规模数据上训练前馈网络,但它缺乏灵活性,无法容纳循环、强化学习或对抗学习范式 —— 后者对于我们在本书中实现的许多算法贯穿全文非常重要。

  • 最后,这个系统难以缩小规模,比如在具有 GPU 的台式机和具有多个核心的分布式环境中运行相同的作业,部署还需要不同的技术栈。

这些考虑共同促使 TensorFlow 作为一种通用的深度学习计算框架的发展:一种可以让科学家灵活地尝试新的层架构或前沿的训练范式,同时还允许这种实验在笔记本电脑(用于早期工作)和计算集群(用于扩展更成熟的模型)上使用相同的工具运行,同时还通过为两者提供一个公共运行时来简化研究和开发代码之间的过渡。

尽管这两个库都共享计算图的概念(将网络表示为操作(节点)和数据(边缘)的图形)和数据流编程模型(其中矩阵操作通过图的有向边缘传递并对其应用操作),但 TensorFlow 与 DistBelief 不同,设计为图的边缘是张量(n 维矩阵),图的节点是原子操作(加法、减法、非线性卷积或队列和其他高级操作),而不是固定的层操作——这允许在定义新计算时具有更大的灵活性,甚至允许进行突变和有状态更新(这些仅是图中的附加节点)。

数据流图本质上充当一个“占位符”,其中数据被插入到定义的变量中,并可以在单台或多台机器上执行。TensorFlow 在 C++运行时优化构建的数据流图,允许优化,例如向 GPU 发送命令。图的不同计算也可以在多台机器和硬件上执行,包括 CPU、GPU 和 TPU(Google 开发的自定义张量处理芯片,在 Google Cloud 计算环境中可用)¹¹,因为在 TensorFlow 中以高层次描述的相同计算被实现为在多个后端系统上执行。

因为数据流图允许可变状态,本质上,不再像 DistBelief 那样有一个集中的参数服务器(尽管 TensorFlow 也可以以参数服务器配置的方式分布式运行),因为持有状态的不同节点可以执行与任何其他工作节点相同的操作。此外,控制流操作(例如循环)允许对可变长度输入进行训练,例如在循环网络中(参见第三章深度神经网络的构建模块)。在训练神经网络的情况下,每一层的梯度简单地表示为图中的附加操作,允许使用相同框架包含像速度这样的优化(如 RMSProp 或 ADAM 优化器,在第三章深度神经网络的构建模块中描述)而不是修改参数服务器逻辑。在分布式训练的情况下,TensorFlow 还具有几个检查点和冗余机制(“备份”工作节点以防单个任务失败),使其适用于在分布式环境中进行稳健的训练。

TensorFlow 2.0

尽管在数据流图中表示操作原语可以灵活定义 Python 客户端 API 内的新层,但也可能导致大量“样板”代码和重复的语法。出于这个原因,高级 API Keras¹⁴被开发出来提供高级抽象;层使用 Python 类表示,而特定运行环境(例如 TensorFlow 或 Theano)是执行该层的“后端”,就像原子 TensorFlow 运算符可以在 CPU、GPU 或 TPU 上具有不同的底层实现一样。尽管作为与框架无关的库开发,Keras 已包含在 TensorFlow 2.0 版本的主要发布中。为了可读性的目的,我们将在本书中大部分模型使用 Keras 来实现,而在需要实现特定操作或突出基础逻辑时,将恢复到底层的 TensorFlow 2.0 代码。请参阅表 2.3以比较这些库在低(TensorFlow)或高(Keras)级别上如何实现各种神经网络算法概念。

对象 TensorFlow 实现 Keras 实现
神经网络层 张量计算 Python 层类
梯度计算 图运行操作符 Python 优化器类
损失函数 张量计算 Python 损失函数
神经网络模型 图运行会话 Python 模型类实例

表 2.3:TensorFlow 和 Keras 比较

为了向您展示 Keras 和 TensorFlow 1.0 在实现基本神经网络模型时所做的抽象的区别,让我们看一下使用这两个框架编写卷积层的示例(请参阅第三章深度神经网络的构建块)。在第一种情况下,在 TensorFlow 1.0 中,您可以看到很多代码涉及显式指定变量、函数和矩阵操作,以及梯度函数和运行时会话来计算网络的更新。

这是 TensorFlow 1.0 中的多层感知器¹⁵:

X = tf.placeholder(dtype=tf.float64)
Y = tf.placeholder(dtype=tf.float64)
num_hidden=128
# Build a hidden layer
W_hidden = tf.Variable(np.random.randn(784, num_hidden))
b_hidden = tf.Variable(np.random.randn(num_hidden))
p_hidden = tf.nn.sigmoid( tf.add(tf.matmul(X, W_hidden), b_hidden) )
# Build another hidden layer
W_hidden2 = tf.Variable(np.random.randn(num_hidden, num_hidden))
b_hidden2 = tf.Variable(np.random.randn(num_hidden))
p_hidden2 = tf.nn.sigmoid( tf.add(tf.matmul(p_hidden, W_hidden2), b_hidden2) )
# Build the output layer
W_output = tf.Variable(np.random.randn(num_hidden, 10))
b_output = tf.Variable(np.random.randn(10))
p_output = tf.nn.softmax( tf.add(tf.matmul(p_hidden2, W_output), 
           b_output) )
loss = tf.reduce_mean(tf.losses.mean_squared_error(
        labels=Y,predictions=p_output))
accuracy=1-tf.sqrt(loss)
minimization_op = tf.train.AdamOptimizer(learning_rate=0.01).minimize(loss)
feed_dict = {
    X: x_train.reshape(-1,784),
    Y: pd.get_dummies(y_train)
}
with tf.Session() as session:
    session.run(tf.global_variables_initializer())
    for step in range(10000):
        J_value = session.run(loss, feed_dict)
        acc = session.run(accuracy, feed_dict)
        if step % 100 == 0:
            print("Step:", step, " Loss:", J_value," Accuracy:", acc)
            session.run(minimization_op, feed_dict)
    pred00 = session.run([p_output], feed_dict={X: x_test.reshape(-1,784)}) 

相比之下,Keras 中相同卷积层的实现通过使用在 Python 类中体现的抽象概念(如层、模型和优化器)大大简化了。这些类封装了计算的底层细节,使代码逻辑更易读。

还要注意,在 TensorFlow 2.0 中,运行会话的概念(惰性执行,即仅在显式编译和调用时才计算网络)已经被弃用,而采用了渴望执行的方式,在调用网络函数(如 callcompile)时动态调用会话和图,网络行为就像任何其他 Python 类一样,而无需显式创建 session 作用域。使用 tf.Variable() 声明变量的全局命名空间概念也已被替换为默认垃圾收集机制

这是 Keras 中的多层感知器层¹⁵:

import TensorFlow as tf
from TensorFlow.keras.layers import Input, Dense
from keras.models import Model
l = tf.keras.layers
model = tf.keras.Sequential([
    l.Flatten(input_shape=(784,)),
    l.Dense(128, activation='relu'),
    l.Dense(128, activation='relu'),
    l.Dense(10, activation='softmax')
])
model.compile(loss='categorical_crossentropy', 
              optimizer='adam',
              metrics = ['accuracy'])
model.summary()
model.fit(x_train.reshape(-1,784),pd.get_dummies(y_train),nb_epoch=15,batch_size=128,verbose=1) 

现在我们已经了解了 TensorFlow 库的一些细节以及它为深度神经网络模型的开发(包括我们将在本书中实现的生成模型)而非常合适的原因,让我们开始建立我们的研究环境。虽然我们可以简单地使用像 pip 这样的 Python 包管理器在我们的笔记本电脑上安装 TensorFlow,但我们希望确保我们的过程尽可能健壮和可复制 - 这将使我们更容易将我们的代码打包到不同的机器上运行,或者通过指定我们在实验中使用的每个 Python 库的确切版本来保持我们的计算一致。我们将首先安装一个集成开发环境IDE),这将使我们的研究更容易 - VSCode。

VSCode

Visual Studio CodeVSCode)是由微软公司开发的开源代码编辑器,可用于许多编程语言,包括 Python。它允许调试,并与诸如 Git 等版本控制工具集成; 我们甚至可以在 VSCode 中运行 Jupyter 笔记本(我们将在本章后面描述)。安装说明因您使用的是 Linux、macOS 还是 Windows 操作系统而异:请查看您系统的单独说明,网址为 code.visualstudio.com。安装完成后,我们需要使用 Git 克隆本书项目的源代码副本,命令如下:

git clone git@github.com:PacktPublishing/Hands-On-Generative-AI-with-Python-and-TensorFlow-2.git 

此命令将把本书项目的源代码复制到我们的笔记本电脑上,允许我们本地运行和修改代码。一旦您复制了代码,请使用 VSCode 打开此书的 GitHub 存储库(图 2.1)。我们现在准备开始安装我们将需要的一些工具;打开install.sh文件。

图 2.1:VSCode IDE

对于我们来说特别有用的一个功能是,VSCode 具有集成(图 2.2)终端,我们可以在其中运行命令:您可以通过选择View,然后从下拉列表中选择Terminal来访问此功能,这将打开一个命令行提示:

图 2.2:VSCode 终端

选择TERMINAL选项卡,并选择解释器为bash;现在您应该能够输入正常的命令。将目录更改为Chapter_2,我们将在其中运行我们的安装脚本,您可以在 VSCode 中打开该脚本。

我们将运行的安装脚本将下载并安装我们在最后几章中将使用到的各种组件;我们将使用的全面框架是Kubeflow库,它处理我们在本卷的后几章中将使用到的各种数据和培训流水线。在本章的其余部分,我们将介绍 Kubeflow 是如何构建在 Docker 和 Kubernetes 之上的,以及如何在几个流行的云提供商上设置 Kubeflow。

Kubernetes(Kubeflow 基于此技术)本质上是一种管理使用Docker创建的容器化应用程序的方式,它允许创建和持久化可重现、轻量级的执行环境以适用于各种应用。虽然我们将使用 Docker 创建可重复的实验运行时,以了解其在虚拟化解决方案整体景观中的位置以及为什么它对现代应用程序开发如此重要,让我们稍微偏离一下,详细描述 Docker 的背景。

Docker:一个轻量级的虚拟化解决方案

开发强大的软件应用程序的一个持续挑战是使它们在与开发它们的机器不同的机器上运行相同。这些环境上的差异可能涵盖多个变量:操作系统、编程语言库版本和硬件,如 CPU 型号。

在处理这种异构性时,传统上一种方法是使用虚拟机VM)。虽然虚拟机能够在多样化的硬件和操作系统上运行应用程序,但它们也受到资源密集型的限制(图 2.3):每个运行在主机上的虚拟机都需要资源开销来运行完全独立的操作系统,以及所有来宾系统中的应用程序或依赖项。

图 2.3:虚拟机与容器¹⁶

然而,在某些情况下,这是一种不必要的开销级别;我们不一定需要运行一个完全独立的操作系统,而只需要一个一致的环境,包括一个操作系统内的库和依赖项。对于指定运行时环境的轻量级框架的需求促使了 2013 年Docker 项目的创建,用于容器化。本质上,容器是运行应用程序的环境,包括所有依赖项和库,允许可重现部署 Web 应用程序和其他程序,例如数据库或机器学习流水线中的计算。对于我们的用例,我们将使用它提供一个可重现的 Python 执行环境(Python 语言版本和库)来运行我们生成式机器学习流水线中的步骤。

本章余下部分的许多示例和本书中的项目需要安装 Docker。有关如何为您的特定操作系统安装 Docker 的说明,请参阅此处的指南。要验证您已成功安装该应用程序,请在终端上运行以下命令,该命令将打印出可用的选项:

docker run hello-world 

重要的 Docker 命令和语法

要了解 Docker 的工作原理,了解用于所有 Docker 容器的模板Dockerfile是有用的。作为示例,我们将使用 Kubeflow 项目中的 TensorFlow 容器笔记本示例(链接)。

此文件是 Docker 应如何采用基本操作环境、添加依赖项并在打包后执行软件的一组说明:

FROM public.ecr.aws/j1r0q0g6/notebooks/notebook-servers/jupyter-tensorflow:master-abf9ec48
# install - requirements.txt
COPY --chown=jovyan:users requirements.txt /tmp/requirements.txt
RUN python3 -m pip install -r /tmp/requirements.txt --quiet --no-cache-dir \
 && rm -f /tmp/requirements.txt 

虽然容器之间的确切命令会有所不同,但这将让您了解我们可以如何使用容器来管理应用程序——在这种情况下,使用一致的库集运行 Jupyter 笔记本进行交互式机器学习实验。一旦我们为我们的特定操作系统安装了 Docker 运行时,我们将通过运行以下命令来执行这样一个文件:

Docker build -f <Dockerfilename> -t <image name:tag> 

当我们这样做时,会发生一些事情。首先,我们从远程存储库中检索base文件系统或 image,这有点类似于我们在使用 Java 构建工具(如 Gradle 或 Maven)或 Python 的 pip 安装程序时,从 Artifactory 收集 JAR 文件的方式。有了这个文件系统或 image,然后我们为 Docker build 命令设置所需的变量,比如用户名和 TensorFlow 版本,以及容器的运行时环境变量。我们确定将用于运行命令的 shell 程序,然后安装我们需要运行 TensorFlow 和笔记本应用程序的依赖项,并指定在启动 Docker 容器时要运行的命令。然后,我们使用由基本 image 名称和一个或多个 tags(比如版本号,或者在许多情况下,简单地用时间戳来唯一标识这个 image)组成的标识符保存这个快照。最后,要实际启动运行这个容器的笔记本服务器,我们将发出以下命令:

Docker run <image name:tag> 

默认情况下,Docker 会运行在 Dockerfile 文件中的可执行命令;在我们目前的示例中,这是启动笔记本服务器的命令。然而,并非一定如此;我们也可以有一个 Dockerfile,它只是为应用程序构建一个执行环境,并发出在该环境内运行的命令。在这种情况下,命令看起来会像这样:

Docker run <image name:tag> <command> 

Docker run 命令允许我们测试我们的应用程序是否可以成功在 Dockerfile 指定的环境中运行;然而,通常我们希望在云中运行此应用程序,以便利用分布式计算资源或能够托管向全球公开的 Web 应用程序,而不是在本地。要做到这一点,我们需要将构建的镜像移到一个远程存储库,使用 push 命令,这个远程存储库可能与我们最初拉取初始镜像的存储库相同,也可能不同。

Docker push <image name:tag> 

注意,image 名称可以包含对特定注册表的引用,比如本地注册表或在主要云提供商(如 AWS 的 弹性容器服务(ECS),Azure 的 Azure Kubernetes 服务(AKS) 或 Google 的容器注册表)上托管的注册表。将镜像发布到远程注册表允许开发人员共享镜像,并使我们可以在云中部署容器。

使用 Docker-compose 连接 Docker 容器

到目前为止,我们只讨论了一些基本的 Docker 命令,这些命令可以让我们在单个容器中运行单个服务。然而,你也许能够理解,在“现实世界”中,我们通常需要同时运行一个或多个应用程序 – 例如,一个网站将同时有一个获取和处理用户活动数据的网络应用程序和一个用于记录信息的数据库实例。在复杂的应用程序中,网站甚至可能由多个专门用于特定用例的小型网络应用程序或微服务组成,比如前端、用户数据或订单管理系统。对于这些类型的应用程序,我们需要多个容器彼此通信。Docker-compose 工具(docs.docker.com/compose/)就是为此类应用程序而设计的:它允许我们使用YAML格式在应用文件中指定多个 Docker 容器。例如,一个具有 Redis 数据库实例的网站配置可能如下:

version: '3'
services:
  web:
    build: .
    ports:
    - "5000:5000"
    volumes:
    - .:/code
    - logvolume01:/var/log
    links:
    - redis
  redis:
    image: redis
volumes:
  logvolume01: {} 

代码 2.1:Docker Compose 的 yaml 输入文件

这里的两个应用程序容器分别是webredis数据库。文件还指定了与这两个应用程序相关联的卷(磁盘)。使用这个配置,我们可以运行以下命令:

docker-compose up 

这将启动 YAML 文件中指定的所有容器,并允许它们彼此通信。然而,即使 Docker 容器和 docker-compose 允许我们使用一致的执行环境构建复杂的应用程序,当我们将这些服务部署到云端时,我们可能会遇到鲁棒性问题。例如,在一个网站应用程序中,我们无法保证应用程序运行的虚拟机会持续长时间,因此我们需要管理自愈和冗余的进程。这也与分布式机器学习流水线有关,其中我们不希望因为集群中的一个节点出现问题就不得不终止整个作业,因此我们需要备份逻辑来重新启动工作的一部分。此外,虽然 Docker 具有 docker-compose 功能来链接应用程序中的几个容器,但它没有健壮的规则来控制这些容器之间的通信,或者如何将它们作为一个单元进行管理。出于这些目的,我们转向 Kubernetes 库。

Kubernetes:强大的多容器应用程序管理

Kubernetes 项目-有时缩写为 k8s-诞生于谷歌内部称为Borg的容器管理项目。Kubernetes 来自希腊词 navigator,如项目标识的七条辐射轮所示。¹⁸ Kubernetes 使用 Go 编程语言编写,提供了一个强大的框架,在由云提供商管理的底层资源上部署和管理 Docker 容器应用程序(例如亚马逊网络服务AWS)、Microsoft Azure 和Google 云平台GCP))。

Kubernetes 基本上是用来控制由一个或多个部署在云中的 Docker 容器组成的应用程序的工具;这个容器的集合称为Pod。每个 Pod 可以有一个或多个副本(以实现冗余),这称为资源副本集。Kubernetes 部署的两个主要组件是控制平面节点。控制平面承载了部署和管理 Pod 的集中逻辑,由(图 2.4)组成:

图 2.4:Kubernetes 组件¹⁸

  • Kube-api-server:这是主要的应用程序,它侦听用户的命令以部署或更新 Pod,或通过ingress管理对 Pod 的外部访问。

  • Kube-controller-manager:管理每个 Pod 副本数量等功能的应用程序。

  • Cloud-controller-manager:管理特定于云提供商的功能。

  • Etcd:维护不同 Pod 的环境和状态变量的键值存储。

  • Kube-scheduler:负责找到运行 Pod 的工作进程的应用程序。

虽然我们可以设置自己的控制平面,但在实践中,通常我们会将此功能由我们的云提供商管理,例如谷歌的Google Kubernetes 引擎GKE)或亚马逊的弹性 Kubernetes 服务EKS)。Kubernetes 节点-集群中的各个单独的机器-每个都运行一个名为kubelet的应用程序,该应用程序监视运行在该节点上的 Pod。

现在,我们已经对 Kubernetes 系统有了一个高层次的了解,接下来让我们来看一下你将需要与 Kubernetes 集群进行交互、更新其组件以及启动和停止应用程序的重要命令。

重要的 Kubernetes 命令

为了与在云中运行的 Kubernetes 集群进行交互,我们通常使用Kubernetes 命令行工具kubectl)。有关在您的操作系统上安装 kubectl 的说明可以在kubernetes.io/docs/tasks/tools/install-kubectl/找到。要验证您是否成功安装了 kubectl,可以在终端中再次运行help命令:

kubectl --help 

与 Docker 一样,kubectl 有许多命令;我们将使用的一个重要命令是apply命令,它与docker-compose类似,它将一个 YAML 文件作为输入并与 Kubernetes 控制平面通信以启动、更新或停止 Pod:

kubectl apply -f <file.yaml> 

作为 apply 命令运行方式的示例,让我们看一下用于部署 web 服务器 (nginx) 应用程序的 YAML 文件:

apiVersion: v1
kind: Service
metadata:
  name: my-nginx-svc
  labels:
    app: nginx
spec:
  type: LoadBalancer
  ports:
  - port: 80
  selector:
    app: nginx
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80 

此文件中指定的资源将按照文件中列出的顺序在 Kubernetes 集群节点上创建。首先,我们创建负载均衡器,它在nginx web 服务器的副本之间路由外部流量。metadata 用于为这些应用程序打标签,以便稍后使用 kubectl 进行查询。其次,我们使用一致的容器(镜像为 1.7.9)创建一组 3nginx pod 的副本,它们分别使用其容器上的端口 80

Kubernetes 集群的同一组物理资源可以在多个虚拟集群中共享,使用命名空间 – 这使我们可以将资源分隔到多个用户或组之间。例如,这可以让每个团队运行自己的一组应用程序,并在逻辑上表现得好像他们是唯一的用户。稍后,在我们对 Kubeflow 的讨论中,我们将看到如何使用此功能在同一 Kubeflow 实例上逻辑分区项目。

用于配置管理的 Kustomize

像大多数代码一样,我们最终可能希望将用于向 Kubernetes 发出命令的 YAML 文件存储在版本控制系统中,例如 Git。这导致一些情况下这种格式可能不理想:例如,在机器学习管道中,我们可能执行超参数搜索,其中相同的应用程序以稍微不同的参数运行,导致大量重复的命令文件。

或者,我们可能有一些参数,例如 AWS 账户密钥,出于安全原因,我们不希望将其存储在文本文件中。我们还可能希望通过将我们的命令拆分为 base 和附加部分来增加重用性;例如,在 代码 2.1 中显示的 YAML 文件中,如果我们想要在不同的数据库中运行 ngnix,或者指定 Amazon、Google 和 Microsoft Azure 提供的不同云对象存储中的文件存储。

对于这些用例,我们将使用 Kustomize 工具(kustomize.io),也可通过 kubectl 使用:

kubectl apply -k <kustomization.yaml> 

或者,我们可以使用 Kustomize 命令行工具。kustomization.yaml 是一个 Kubernetes 应用程序的模板;例如,考虑以下模板,用于 Kubeflow 示例存储库中的训练作业(github.com/kubeflow/pipelines/blob/master/manifests/kustomize/sample/kustomization.yaml):

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
  # Or
# github.com/kubeflow/pipelines/manifests/kustomize/env/gcp?ref=1.0.0
  - ../env/gcp
  # Kubeflow Pipelines servers are capable of 
  # collecting Prometheus metrics.
  # If you want to monitor your Kubeflow Pipelines servers 
  # with those metrics, you'll need a Prometheus server 
  # in your Kubeflow Pipelines cluster.
  # If you don't already have a Prometheus server up, you 
  # can uncomment the following configuration files for Prometheus.
  # If you have your own Prometheus server up already 
  # or you don't want a Prometheus server for monitoring, 
  # you can comment the following line out.
  # - ../third_party/prometheus
  # - ../third_party/grafana
# Identifier for application manager to apply ownerReference.
# The ownerReference ensures the resources get garbage collected
# when application is deleted.
commonLabels:
  application-crd-id: kubeflow-pipelines
# Used by Kustomize
configMapGenerator:
  - name: pipeline-install-config
    env: params.env
    behavior: merge
secretGenerator:
  - name: mysql-secret
    env: params-db-secret.env
    behavior: merge
# !!! If you want to customize the namespace,
# please also update 
# sample/cluster-scoped-resources/kustomization.yaml's 
# namespace field to the same value
namespace: kubeflow
#### Customization ###
# 1\. Change values in params.env file
# 2\. Change values in params-db-secret.env 
# file for CloudSQL username and password
# 3\. kubectl apply -k ./
#### 

我们可以看到此文件引用了位于相对路径 ../base 的单独的 kustomization.yaml 文件中的 base 配置集。要编辑此文件中的变量,例如,要更改应用程序的命名空间,我们将执行:

kustomize edit set namespace mykube 

我们还可以添加配置映射以传递给训练作业,使用键-值格式,例如:

kustomize edit add configmap configMapGenerator --from-literal=myVar=myVal 

最后,当我们准备在 Kubernetes 上执行这些命令时,我们可以动态地build并应用所需的kubectl命令,假设kustomization.yaml在当前目录中。

kustomize build . |kubectl apply -f - 

希望这些示例演示了 Kustomize 如何提供一种灵活的方式来使用模板生成我们在本书后面的工作流程中经常需要的 kubectl YAML;我们将经常利用它来参数化我们的工作流程。

现在我们已经了解了 Kubernetes 如何在云中管理 Docker 应用程序,以及 Kustomize 如何允许我们灵活地重用kubectl yaml命令,让我们看看这些组件如何在 Kubeflow 中联系在一起,以运行我们稍后将进行的创建 TensorFlow 生成式 AI 模型的实验。

Kubeflow:一个端到端的机器学习实验室

正如本章开始时所描述的,端到端机器学习研究和开发的lab有许多组件(表 2.1),例如:

  • 管理和版本化库依赖,例如 TensorFlow,并将其打包为可复现的计算环境

  • 可视化数据并尝试不同设置的交互式研究环境

  • 指定管道步骤的系统化方式 – 数据处理、模型调优、评估和部署

  • 分布式运行建模过程所需资源的供应

  • 具有快照历史版本的研究过程的强大机制

正如我们在本章前面所描述的,TensorFlow 被设计用于利用分布式资源进行训练。为了利用这一能力,我们将使用 Kubeflow 项目。Kubeflow 建立在 Kubernetes 之上,具有几个在管理端到端机器学习应用程序过程中有用的组件。要安装 Kubeflow,我们需要拥有现有的 Kubernetes 控制平面实例,并使用 kubectl 启动 Kubeflow 的各个组件。设置步骤会略有不同,取决于我们是使用本地实例还是主要云服务提供商之一。

通过 MiniKF 在本地运行 Kubeflow

如果我们想快速开始或在本地原型化我们的应用程序,我们可以避免设置云账户,而是使用虚拟机模拟我们在云中配置资源的方式。要在本地设置 Kubeflow,我们首先需要安装 VirtualBox (www.virtualbox.org/wiki/Downloads) 以运行虚拟机,以及 Vagrant 以在 VirtualBox 虚拟机上运行配置,用于设置 Kubernetes 控制平面和 Kubeflow(www.vagrantup.com/downloads.html)。

安装了这两个依赖项后,创建一个新目录,切换到该目录并运行:

vagrant init arrikto/minikf
vagrant up 

这将初始化 VirtualBox 配置并启动应用程序。现在,您可以导航到http://10.10.10.10/并按照说明启动 Kubeflow 和 Rok(Arrikto 创建,用于 Kubeflow 实验中使用的数据的存储卷)。一旦这些被提供,你应该看到一个像这样的屏幕(图 2.5):

图 2.5:在虚拟盒子中的 MiniKF 安装界面¹⁹

登录到 Kubeflow 查看各个组件的仪表板(图 2.6):

图 2.6:MiniKF 中的 Kubeflow 仪表板

我们将在后面回到这些组件,并了解 Kubeflow 提供的各种功能,但首先,让我们一起看看如何在云中安装 Kubeflow。

在 AWS 中安装 Kubeflow

在 AWS 上运行 Kubeflow,我们需要在云中提供一个 Kubernetes 控制平面。幸运的是,亚马逊提供了一个名为 EKS 的托管服务,它可以方便地提供一个控制平面来部署 Kubeflow。按照以下步骤在 AWS 上部署 Kubeflow:

  1. 注册 AWS 账户并安装 AWS 命令行界面

    这是与各种 AWS 服务进行交互所需的,根据您平台上的说明位于docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html。安装完成后,输入:

    aws configure 
    

    为了设置您的账户和密钥信息来提供资源。

  2. 安装 eksctl

    这个命令行实用程序允许我们从命令行在亚马逊中提供一个 Kubernetes 控制平面。按照docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html上的说明进行安装。

  3. 安装 iam-authenticator

    为了允许 kubectl 与 EKS 进行交互,我们需要使用 IAM 验证器提供正确的权限来修改我们的 kubeconfig。请参考docs.aws.amazon.com/eks/latest/userguide/install-aws-iam-authenticator.html上的安装说明。

  4. 下载 Kubeflow 命令行工具

    链接位于 Kubeflow 发布页面(github.com/kubeflow/kubeflow/releases/tag/v0.7.1)。下载其中一个目录,并使用以下命令解压 tarball:

    tar -xvf kfctl_v0.7.1_<platform>.tar.gz 
    
  5. 构建配置文件

    输入 Kubeflow 应用程序目录(${KF_DIR})、部署名称(${KF_NAME})和部署的基本配置文件的路径(${CONFIG_URI})的环境变量,位于raw.githubusercontent.com/kubeflow/manifests/v0.7-branch/kfdef/kfctl_aws.0.7.1.yaml用于 AWS 部署,运行以下命令生成配置文件:

    mkdir -p ${KF_DIR}
    cd ${KF_DIR}
    kfctl build -V -f ${CONFIG_URI} 
    

    这将在本地生成一个名为kfctl_aws.0.7.1.yaml的本地配置文件。如果这看起来像 Kustomize,那是因为kfctl在内部使用 Kustomize 来构建配置。我们还需要为本地配置文件的位置添加一个环境变量${CONFIG_FILE},在这种情况下是:

    export CONFIG_FILE=${KF_DIR}/kfctl_aws.0.7.1.yaml 
    
  6. 在 EKS 上启动 Kubeflow

    使用以下命令启动 Kubeflow:

    cd ${KF_DIR}
    rm -rf kustomize/ 
    kfctl apply -V -f ${CONFIG_FILE} 
    

    所有 Kubeflow 组件变为可用将需要一些时间;您可以通过使用以下命令来检查进度:

    kubectl -n kubeflow get all 
    

    一旦它们都可用,我们可以使用以下命令获取 Kubeflow 仪表板的 URL 地址:

    kubectl get ingress -n istio-system 
    

这将带我们到上面的 MiniKF 示例中显示的仪表盘视图。请注意,在默认配置中,此地址对公众开放;对于安全应用程序,我们需要按照www.kubeflow.org/docs/aws/authentication/中的说明添加身份验证。

在 GCP 中安装 Kubeflow

像 AWS 一样,Google 云平台GCP)提供了一个托管的 Kubernetes 控制平面 GKE。我们可以使用以下步骤在 GCP 中安装 Kubeflow:

  1. 注册 GCP 账户并在控制台上创建一个项目

    该项目将是与 Kubeflow 相关的各种资源所在的位置。

  2. 启用所需服务

    在 GCP 上运行 Kubeflow 所需的服务包括:

    • 计算引擎 API

    • Kubernetes 引擎 API

    • 身份和访问管理(IAM)API

    • 部署管理器 API

    • 云资源管理器 API

    • 云文件存储 API

    • AI 平台培训和预测 API

  3. 设置 OAuth(可选)

    如果您希望进行安全的部署,那么,与 AWS 一样,您必须按照说明添加身份验证到您的安装中,位于(www.kubeflow.org/docs/gke/deploy/oauth-setup/)。或者,您可以只使用 GCP 账户的用户名和密码。

  4. 设置 GCloud CLI

    这类似于前一节中涵盖的 AWS CLI。安装指南可在cloud.google.com/sdk/找到。您可以通过运行以下命令来验证您的安装:

    gcloud --help 
    
  5. 下载 Kubeflow 命令行工具

    链接位于 Kubeflow 发行版页面(github.com/kubeflow/kubeflow/releases/tag/v0.7.1)。下载其中一个目录并使用以下命令解压 tar 文件:

    tar -xvf kfctl_v0.7.1_<platform>.tar.gz 
    
  6. 登录 Google 云并创建用户凭据

    接下来,我们需要创建一个登录账户和凭据令牌,用于与我们的账户中的资源进行交互。

    gcloud auth login
    gcloud auth application-default login 
    
  7. 设置环境变量并部署 Kubeflow

    与 AWS 一样,我们需要为一些关键环境变量输入值:包含 Kubeflow 配置文件的应用程序(${KF_DIR}),Kubeflow 部署的名称(${KF_NAME}),基本配置 URI 的路径(${CONFIG_URI}- 对于 GCP,这是raw.githubusercontent.com/kubeflow/manifests/v0.7-branch/kfdef/kfctl_gcp_iap.0.7.1.yaml),Google 项目的名称(${PROJECT})以及它所在的区域(${ZONE})。

  8. 启动 Kubeflow

    与 AWS 一样,我们使用 Kustomize 构建模板文件并启动 Kubeflow:

    mkdir -p ${KF_DIR}
    cd ${KF_DIR}
    kfctl apply -V -f ${CONFIG_URI} 
    

    一旦启动了 Kubeflow,您可以使用以下命令获取仪表板的 URL:

    kubectl -n istio-system get ingress 
    

在 Azure 上安装 Kubeflow

Azure 是微软公司的云服务,和 AWS 和 GCP 一样,我们可以利用它来安装 Kubeflow,利用在 Azure 云中驻留的 Kubernetes 控制平面和计算资源。

  1. 在 Azure 上注册账户

    azure.microsoft.com注册账号-可用于实验的免费层。

  2. 安装 Azure 命令行实用程序

    请参阅docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest上平台的安装说明。您可以通过在本地计算机的命令行上运行以下命令来验证安装:

    az 
    

    这应该会打印出您可以在控制台上使用的命令列表。首先,通过以下命令登录您的帐户:

    az login 
    

    并输入您在步骤 1中注册的帐户凭据。您将被重定向到浏览器以验证您的帐户,之后您应该会看到类似以下的响应:

    "You have logged in. Now let us find all the subscriptions to which you have access": …
    [
    { 
        "cloudName": …
        "id" ….
    …
        "user": {
    …
    }
    }
    ] 
    
  3. 为新集群创建资源组

    我们首先需要创建新应用所在的资源组,使用以下命令:

    az group create -n ${RESOURCE_GROUP_NAME} -l ${LOCATION} 
    
  4. 在 AKS 上创建 Kubernetes 资源

    现在,在您的资源组上部署 Kubernetes 控制平面:

    az aks create -g ${RESOURCE_GROUP_NAME} -n ${NAME} -s ${AGENT_SIZE} -c ${AGENT_COUNT} -l ${LOCATION} --generate-ssh-keys 
    
  5. 安装 Kubeflow

    首先,我们需要获取凭据以在我们的 AKS 资源上安装 Kubeflow:

    az aks get-credentials -n ${NAME}  -g ${RESOURCE_GROUP_NAME} 
    
  6. 安装 kfctl

    安装并解压缩 tarball 目录:

    tar -xvf kfctl_v0.7.1_<platform>.tar.gz 
    
  7. 设置环境变量

    与 AWS 一样,我们需要为一些关键环境变量输入值:包含 Kubeflow 配置文件的应用程序(${KF_DIR}),Kubeflow 部署的名称(${KF_NAME}),和基本配置 URI 的路径(${CONFIG_URI}- 对于 Azure,这是raw.githubusercontent.com/kubeflow/manifests/v0.7-branch/kfdef/kfctl_k8s_istio.0.7.1.yaml)。

  8. 启动 Kubeflow

    与 AWS 一样,我们使用 Kustomize 构建模板文件并启动 Kubeflow:

    mkdir -p ${KF_DIR}
    cd ${KF_DIR}
    kfctl apply -V -f ${CONFIG_URI} 
    

    一旦启动 Kubeflow,您可以使用端口转发将本地端口8080的流量重定向到集群中的端口80,以使用以下命令在localhost:8080上访问 Kubeflow 仪表板:

    kubectl port-forward svc/istio-ingressgateway -n istio-system 8080:80 
    

使用 Terraform 安装 Kubeflow

对于这些云提供商,你可能会注意到我们有一套共同的命令;创建一个 Kubernetes 集群,安装 Kubeflow,并启动应用程序。虽然我们可以使用脚本来自动化这个过程,但想要像我们的代码一样,有一种方法来版本控制和持久化不同的基础设施配置,允许创建运行 Kubeflow 所需资源集合的可重现的配方,这将是可取的。这也有助于我们在不完全重写安装逻辑的情况下,潜在地在不同的云提供商之间移动。

模板语言Terraform (www.terraform.io/)是由 HashiCorp 创建的一种用于基础设施即服务IaaS)的工具。就像 Kubernetes 有一个 API 来更新集群上的资源一样,Terraform允许我们使用 API 和模板语言来抽象不同的底层云提供商的交互,使用命令行工具和用 GoLang 编写的核心组件。Terraform 可以使用用户编写的插件进行扩展。

图 2.7:Terraform 架构²⁰

让我们以安装 Kubeflow 在 AWS 上使用 Terraform 指南的一个例子,位于github.com/aws-samples/amazon-eks-machine-learning-with-terraform-and-kubeflow上。一旦你在 EC2 容器上建立所需的 AWS 资源并安装了 terraform,aws-eks-cluster-and-nodegroup.tf Terraform 文件用于使用命令创建 Kubeflow 集群:

terraform apply 

这个文件中有一些关键组件。一个是指定部署方面的变量:

variable "efs_throughput_mode" {
   description = "EFS performance mode"
   default = "bursting"
   type = string
} 

另一个是指定我们正在使用的云提供商的规范:

provider "aws" {
  region                  = var.region
  shared_credentials_file = var.credentials
resource "aws_eks_cluster" "eks_cluster" {
  name            = var.cluster_name
  role_arn        = aws_iam_role.cluster_role.arn
  version         = var.k8s_version

  vpc_config {
    security_group_ids = [aws_security_group.cluster_sg.id]
    subnet_ids         = flatten([aws_subnet.subnet.*.id])
  }

  depends_on = [
    aws_iam_role_policy_attachment.cluster_AmazonEKSClusterPolicy,
    aws_iam_role_policy_attachment.cluster_AmazonEKSServicePolicy,
  ]

  provisioner "local-exec" {
    command = "aws --region ${var.region} eks update-kubeconfig --name ${aws_eks_cluster.eks_cluster.name}"
  }

  provisioner "local-exec" {
    when    = destroy
    command = "kubectl config unset current-context"
  }

}
  profile   = var.profile
} 

还有另一个是诸如 EKS 集群这样的资源:

resource "aws_eks_cluster" "eks_cluster" {
  name     = var.cluster_name
  role_arn = aws_iam_role.cluster_role.arn
  version  = var.k8s_version

  vpc_config {
    security_group_ids = [aws_security_group.cluster_sg.id]
    subnet_ids         = flatten([aws_subnet.subnet.*.id])
  }

  depends_on = [
    aws_iam_role_policy_attachment.cluster_AmazonEKSClusterPolicy,
    aws_iam_role_policy_attachment.cluster_AmazonEKSServicePolicy,
  ]

  provisioner "local-exec" {
    command = "aws --region ${var.region} eks update-kubeconfig --name ${aws_eks_cluster.eks_cluster.name}"
  }

  provisioner "local-exec" {
    when    = destroy
    command = "kubectl config unset current-context"
  }

} 

每次运行 Terraform apply 命令时,它都会遍历这个文件,确定要创建哪些资源,调用哪些底层 AWS 服务来创建它们,以及他们应该使用哪组配置进行配置。这为编排诸如 Kubeflow 之类的复杂安装提供了一种清晰的方式,这是一种版本化的、可扩展的模板语言。

现在我们已经成功地在本地或在云端的托管 Kubernetes 控制面板上安装了 Kubeflow,让我们看看平台上有哪些可用的工具。

Kubeflow 组件简介

现在我们已经在本地或云端安装了 Kubeflow,让我们再次看看 Kubeflow 仪表板(图 2.8):

图 2.8:Kubeflow 仪表板

让我们来看看这个工具包提供了什么。首先,注意到在上面的面板中,我们有一个下拉菜单,其中指定了名称为anonymous – 这是前面提到的 Kubernetes 的namespace。虽然我们的默认值是anonymous,但我们可以在我们的 Kubeflow 实例上创建多个命名空间,以容纳不同的用户或项目。这可以在登录时完成,我们在那里设置一个个人资料(图 2.9):

图 2.9:Kubeflow 登录页面

或者,与 Kubernetes 中的其他操作一样,我们可以使用 YAML 文件应用一个命名空间:

apiVersion: kubeflow.org/v1beta1
kind: Profile
metadata:
  name: profileName  
spec:
  owner:
    kind: User
    name: userid@email.com 

使用kubectl命令:

kubectl create -f profile.yaml 

一旦我们有了命名空间,我们可以做些什么呢?让我们看看可用的工具。

Kubeflow 笔记本服务器

我们可以使用 Kubeflow 在一个命名空间中启动一个 Jupyter 笔记本服务器,在这里我们可以运行实验性的代码;我们可以通过用户界面中的Notebook Servers选项卡并选择NEW SERVER来启动笔记本(图 2.10):

图 2.10:Kubeflow 笔记本创建

然后我们可以指定参数,比如要运行哪个容器(可能包括我们在之前关于 Docker 讨论中检查过的 TensorFlow 容器),以及分配多少资源(图 2.11)。

图 2.11:Kubeflow Docker 资源面板

您还可以指定一个持久卷PV)来存储数据,即使笔记本服务器被关闭,数据仍然保留,以及特殊资源,比如 GPU。

一旦启动,如果您已经指定了一个包含 TensorFlow 资源的容器,您可以在笔记本服务器中开始运行模型。

Kubeflow 流水线

对于笔记本服务器,我们举了一个单一容器(笔记本实例)应用的例子。Kubeflow 还通过pipelines功能为我们提供了运行多容器应用工作流(如输入数据、训练和部署)的能力。Pipelines 是遵循领域特定语言DSL)的 Python 函数,用于指定将编译为容器的组件。

如果我们在用户界面上点击 pipelines,我们会被带到一个仪表盘(图 2.12):

图 2.12:Kubeflow 流水线仪表盘

选择其中一个流水线,我们可以看到组件容器的视觉概览(图 2.13)。

图 2.13:Kubeflow 流水线可视化

创建新的运行之后,我们可以为该流水线的特定实例指定参数(图 2.14)。

图 2.14:Kubeflow 流水线参数

一旦流水线创建完毕,我们可以使用用户界面来可视化结果(图 2.15):

图 2.15:Kubeflow 流水线结果可视化

在幕后,用于生成此流水线的 Python 代码是使用流水线 SDK 编译的。我们可以指定组件来自具有 Python 代码的容器:

@kfp.dsl.component
def my_component(my_param):
  ...
  return kfp.dsl.ContainerOp(
    name='My component name',
    image='gcr.io/path/to/container/image'
  )
or a function written in Python itself:
@kfp.dsl.python_component(
  name='My awesome component',
  description='Come and play',
)
def my_python_func(a: str, b: str) -> str: 

对于纯 Python 函数,我们可以使用编译器将其转换为一个操作:

my_op = compiler.build_python_component(
  component_func=my_python_func,
  staging_gcs_path=OUTPUT_DIR,
  target_image=TARGET_IMAGE) 

然后我们使用 dsl.pipeline 装饰器将此操作添加到流水线中:

@kfp.dsl.pipeline(
  name='My pipeline',
  description='My machine learning pipeline'
)
def my_pipeline(param_1: PipelineParam, param_2: PipelineParam):
  my_step = my_op(a='a', b='b') 

我们使用以下代码进行编译:

kfp.compiler.Compiler().compile(my_pipeline, 'my-pipeline.zip') 

运行此代码:

client = kfp.Client()
my_experiment = client.create_experiment(name='demo')
my_run = client.run_pipeline(my_experiment.id, 'my-pipeline', 
  'my-pipeline.zip') 

我们还可以将此 ZIP 文件上传到流水线 UI,在那里 Kubeflow 可以使用编译生成的 YAML 实例化作业。

现在你已经看到了生成单个流水线结果的过程,我们下一个问题是如何生成这样一个流水线的最佳参数。正如你将在第三章深度神经网络的构建模块中看到的那样,神经网络模型通常具有多个配置,称为超参数,它们管理着它们的体系结构(例如层数、层大小和连接性)和训练范式(例如学习率和优化器算法)。Kubeflow 具有用于优化此类参数网格的内置实用程序,称为Katib

使用 Kubeflow Katib 优化模型超参数

Katib 是一个框架,用于使用不同的输入运行同一作业的多个实例,例如神经架构搜索(用于确定神经网络中正确的层数和大小)和超参数搜索(例如为算法找到正确的学习率)。与我们见过的其他 Kustomize 模板一样,TensorFlow 作业指定了一个通用的 TensorFlow 作业,并为参数留有占位符:

apiVersion: "kubeflow.org/v1alpha3"
kind: Experiment
metadata:
  namespace: kubeflow
  name: tfjob-example
spec:
  parallelTrialCount: 3
  maxTrialCount: 12
  maxFailedTrialCount: 3
  objective:
    type: maximize
    goal: 0.99
    objectiveMetricName: accuracy_1
  algorithm:
    algorithmName: random
  metricsCollectorSpec:
    source:
      fileSystemPath:
        path: /train
        kind: Directory
    collector:
      kind: TensorFlowEvent
  parameters:
    - name: --learning_rate
      parameterType: double
      feasibleSpace:
        min: "0.01"
        max: "0.05"
    - name: --batch_size
      parameterType: int
      feasibleSpace:
        min: "100"
        max: "200"
  trialTemplate:
    goTemplate:
        rawTemplate: |-
          apiVersion: "kubeflow.org/v1"
          kind: TFJob
          metadata:
            name: {{.Trial}}
            namespace: {{.NameSpace}}
          spec:
           tfReplicaSpecs:
            Worker:
              replicas: 1 
              restartPolicy: OnFailure
              template:
                spec:
                  containers:
                    - name: tensorflow 
                      image: gcr.io/kubeflow-ci/tf-mnist-with-
                             summaries:1.0
                      imagePullPolicy: Always
                      command:
                        - "python"
                        - "/var/tf_mnist/mnist_with_summaries.py"
                        - "--log_dir=/train/metrics"
                        {{- with .HyperParameters}}
                        {{- range .}}
                        - "{{.Name}}={{.Value}}"
                        {{- end}}
                        {{- end}} 

我们可以使用熟悉的 kubectl 语法来运行它:

kubectl apply -f https://raw.githubusercontent.com/kubeflow/katib/master/examples/v1alpha3/tfjob-example.yaml 

或通过 UI(图 2.16):

图 2.16:Kubeflow 上的 Katib UI

在这里你可以看到这些多参数实验的结果可视化,或者一个表格(图 2.172.18)。

图 2.17:Kubeflow 多维参数优化的可视化

图 2.18:Kubeflow 多结果实验的 UI

总结

在本章中,我们概述了 TensorFlow 是什么,以及它如何作为深度学习研究的改进,我们还探讨了设置 IDE、VSCode 和可重现应用程序的基础,Docker 容器。为了编排和部署 Docker 容器,我们讨论了 Kubernetes 框架,以及如何使用其 API 扩展容器组。最后,我描述了 Kubeflow,一个建立在 Kubernetes 上的机器学习框架,它允许我们运行端到端的流水线、分布式训练和参数搜索,并为训练后的模型提供服务。然后,我们使用 Terraform,一种 IaaS 技术,设置了 Kubeflow 部署。

在深入具体项目之前,我们将介绍神经网络理论的基础知识以及你需要编写基本训练作业的 TensorFlow 和 Keras 命令,在 Kubeflow 上。

参考资料

  1. Abadi, Martín 等(2016 年)TensorFlow:异构分布式系统上的大规模机器学习。arXiv:1603.04467。arxiv.org/abs/1603.04467

  2. 谷歌。TensorFlow。检索日期为 2021 年 4 月 26 日,网址:www.tensorflow.org/

  3. MATLAB,马萨诸塞州南提克:The MathWorks Inc。www.mathworks.com/products/matlab.html

  4. Krizhevsky A., Sutskever I., & Hinton G E. 使用深度卷积神经网络的 ImageNet 分类papers.nips.cc/paper/4824-imagenet-classification-with-deepconvolutional-neural-networks.pdf

  5. Dean J., Ng A. (2012 年 6 月 26 日)。利用大规模脑模拟进行机器学习和 AI。Google | The Keyword。blog.google/technology/ai/using-large-scale-brain-simulations-for/

  6. Mnih, V., Kavukcuoglu, K., Silver, D., Graves, A., Antonoglou, I., Wierstra, D., Riedmiller, M. (2013)。使用深度强化学习玩 Atari 游戏。arXiv:1312.5602。arxiv.org/abs/1312.5602

  7. Silver D, Schrittwieser J, Simonyan K, Antonoglou I, Huang A, Guez A, Hubert T, Baker L, Lai M, Bolton A, Chen Y, Lillicrap T, Hui F, Sifre L, van den Driessche G, Graepel T, Hassabis D. (2017)。在没有人类知识的情况下掌握围棋自然。550(7676):354-359。pubmed.ncbi.nlm.nih.gov/29052630/

  8. Devlin, J., Chang, M. W., Lee, K., & Toutanova, K. (2018)。Bert:用于语言理解的深度双向 transformers 的预训练。arXiv:1810.04805。arxiv.org/abs/1810.04805

  9. Al-Rfou, R.,等人 (2016)。Theano:快速计算数学表达的 Python 框架。arXiv。arxiv.org/pdf/1605.02688.pdf

  10. Collobert R., Kavukcuoglu K., & Farabet C. (2011)。Torch7:一个类似 Matlab 的机器学习环境ronan.collobert.com/pub/matos/2011_torch7_nipsw.pdf

  11. Abadi M.,等人 (2015)。TensorFlow:异构分布式系统上的大规模机器学习download.tensorflow.org/paper/whitepaper2015.pdf

  12. Abadi, Martín,等人 (2016)。TensorFlow:异构分布式系统上的大规模机器学习。arXiv:1603.04467。arxiv.org/abs/1603.04467

  13. Jouppi, N P,等人 (2017)。数据中心张量处理单元的性能分析。arXiv:1704.04760。arxiv.org/abs/1704.04760

  14. van Merriënboer, B., Bahdanau, D., Dumoulin, V., Serdyuk, D., Warde-Farley, D., Chorowski, J., Bengio, Y. (2015)。Blocks 和 Fuel:深度学习框架。arXiv:1506.00619。arxiv.org/pdf/1506.00619.pdf

  15. stackoverflow.com/questions/57273888/keras-vs-TensorFlow-code-comparison-sources

  16. Harris M. (2016). Docker vs. 虚拟机. Nvidia developer blog. developer.nvidia.com/blog/nvidia-docker-gpu-server-application-deployment-made-easy/vm_vs_docker/

  17. 一个视觉双关语 - 该项目的原始代码名称为 Seven of Nine,来自电视剧星际迷航:航海家号中的博格角色。

  18. Kubernetes 组件。 (2021 年 3 月 18 日) Kubernetes. kubernetes.io/docs/concepts/overview/components/

  19. Pavlou C. (2019). 在本地端到端的 ML 管道:Notebooks 和 Kubeflow Pipelines 在新 MiniKF 上. Medium | Kubeflow. medium.com/kubeflow/an-end-to-end-ml-pipeline-on-prem-notebooks-kubeflow-pipelines-on-the-new-minikf-33b7d8e9a836

  20. Vargo S. (2017). 使用 Terraform 管理 Google 日历. HashiCorp. www.hashicorp.com/blog/managing-google-calendar-with-terraform

第三章:深度神经网络的构建模块

在本书中,我们将实现的广泛范围的生成式人工智能模型都是建立在过去十年来在深度学习和神经网络方面的进步基础上的。虽然在实践中我们可以在不参考历史发展的情况下实现这些项目,但追溯它们的基本组成部分将使您对这些模型如何为什么工作有更深入的理解。在本章中,我们将深入探讨这一背景,向您展示生成式人工智能模型是如何从基础构建的,如何将较小的单元组装成复杂的架构,这些模型中的损失函数是如何优化的,以及一些当前的理论解释为什么这些模型如此有效。掌握了这些背景知识,您应该能够更深入地理解从本书的第四章开始的更高级模型和主题背后的推理,《教网络生成数字》。一般来说,我们可以将神经网络模型的构建模块分为一些关于模型如何构建和训练的选择,我们将在本章中进行介绍:

使用哪种神经网络架构:

  • 感知器

  • 多层感知器MLP)/ 前馈

  • 卷积神经网络CNNs

  • 循环神经网络RNNs

  • 长短期记忆网络LSTMs

  • 门控循环单元GRUs

在网络中使用哪些激活函数:

  • 线性

  • Sigmoid

  • Tanh

  • ReLU

  • PReLU

使用什么优化算法来调整网络参数:

  • 随机梯度下降SGD

  • RMSProp

  • AdaGrad

  • ADAM

  • AdaDelta

  • 无 Hessian 优化

如何初始化网络的参数:

  • 随机

  • Xavier 初始化

  • He 初始化

正如你所理解的,这些决策的产物可能导致大量潜在的神经网络变种,开发这些模型的一个挑战之一是确定每个选择中的正确搜索空间。在描述神经网络历史的过程中,我们将更详细地讨论这些模型参数的影响。我们对这个领域的概述始于这一学科的起源:谦逊的感知器模型。

感知器——一个功能中的大脑

最简单的神经网络架构——感知器——受生物研究的启发,旨在理解心理加工的基础,试图用数学公式表示大脑的功能。在本节中,我们将涵盖一些早期研究,以及它是如何激发了现在的深度学习和生成式人工智能领域的。

从组织到 TLUs

AI 算法的近期受欢迎可能会给人一种错误的印象,认为这个领域是新的。许多近期的模型基于几十年前的发现,这些发现因云端的大规模计算资源以及用于并行矩阵计算的定制硬件(如图形处理单元GPUs)、张量处理单元TPUs)和可编程门阵列FPGAs))而得到了重振。如果我们认为神经网络的研究包括其生物启发和计算理论,那么这个领域已经有上百年的历史了。事实上,19 世纪科学家 Santiago Ramón y Cajal 详细解剖插图中描述的其中一个最早的神经网络,这些插图基于对相互连接的神经细胞层的实验观察,启发了神经元学说—即大脑是由单独的、物理上不同且专门的细胞组成,而不是一个连续的网络。¹ Cajal 观察到的视网膜的不同层也启发了特定的神经网络架构,比如我们将在本章后面讨论的 CNN。

图 3.1:由 Santiago Ramón y Cajal 绘制的神经元相互连接的网络³

这种简单神经细胞相互连接的观察使得计算研究人员推测精神活动可能如何由简单的逻辑运算表示,进而产生复杂的精神现象。最初的“自动机理论”通常被追溯到麻省理工学院的 Warren McCulloch 和 Walter Pitts 于 1943 年发表的一篇文章。³ 他们描述了一个简单的模型,即阈值逻辑单元TLU),其中二进制输入根据阈值转换为二进制输出:

其中,I 代表输入值,W 代表权重范围为 (0, 1) 或 (-1, 1),而 f 是一个阈值函数,根据输入是否超过阈值 T 将这些输入转换成二进制输出:⁴

在视觉上和概念上,McCulloch 和 Pitts 的模型与启发它的生物神经元(图 3.2)之间存在一定的相似性。他们的模型将输入整合成输出信号,就像神经元的自然树突(神经元的短输入“臂”,从其他细胞接收信号)将输入通过轴突(细胞的长“尾巴”,将从树突接收到的信号传递给其他神经元)合成一个单一的输出。我们可以想象,就像神经细胞被组成网络以产生复杂的生物学电路一样,这些简单的单元可能被连接起来以模拟复杂的决策过程。

图 3.2:TLU 模型和生物神经元^(5 6)

实际上,使用这个简单的模型,我们已经可以开始表示几个逻辑操作。 如果我们考虑一个带有一个输入的简单神经元的情况,我们可以看到 TLU 可以解决恒等或否定函数(表 3.13.2)。

对于一个简单地返回输入作为输出的恒等操作,权重矩阵在对角线上会有 1(或者对于单个数字输入,权重矩阵会简单地是标量 1,如表 1所示):

恒等
输入
1
0

Table 3.1:恒等操作的 TLU 逻辑

同样地,对于否定操作,权重矩阵可以是负对角线矩阵,在阈值为 0 时翻转输出的符号:

否定
输入
1
0

Table 3.2:否定操作的 TLU 逻辑

给定两个输入,TLU 也可以表示诸如 AND 和 OR 等操作。 在这里,可以设置一个阈值,使得组合输入值必须超过2(对应 AND 操作的输出为1)或者1(对应 OR 操作的输出为1,如果两个输入中任意一个为1)。

AND
输入 1
0
1
0
1

Table 3.3:AND 操作的 TLU 逻辑

OR
输入 1
0
1
0
1

Table 3.4:OR 操作的 TLU 逻辑

然而,TLU 无法捕获诸如“异或”(XOR)的模式,它只有在OR条件为真时才会输出1

异或
输入 1
0
1
0
1

Table 3.5:XOR 操作的 TLU 逻辑

要看到这为什么是真的,考虑一个有两个输入和正权重值为1的 TLU。 如果阈值值T1,那么输入(0, 0)(1, 0)(0, 1)将产生正确的值。 然而,(1, 1)会发生什么? 因为阈值函数对于任何求和大于1的输入都返回1,它无法表示 XOR(表 3.5),因为 XOR 要求一旦超过不同的、更高的值,就要计算不同的输出。 改变一个或两个权重为负值也没有帮助; 问题在于决策阈值只能单向操作,不能对更大的输入进行反转。

同样地,TLU 不能表示“异或”的否定,即XNOR表 3.6)。

XNOR
输入 1
0
1
0
1

Table 3.6:XNOR 操作的 TLU 逻辑

XOR操作类似(表 3.5),通过考虑一个含有两个 1 的权重矩阵,可以说明无法通过 TLU 函数来表示XNOR操作(表 3.6);对于两个输入(1, 0)或(0, 1),如果我们设置输出 1 的阈值为 2,则获得了正确的值。与XOR操作类似,当输入为(0, 0)时会遇到问题,因为我们无法设置第二个阈值来使和为 0 时输出 1。

从 TLUs 到调谐感知器

除了这些有关表示XORXNOR操作的限制外,还有一些附加的简化会限制 TLU 模型的表达能力;权重是固定的,输出只能是二进制(0 或 1)。显然,对于像神经元这样的系统来说,“学习”需要对环境做出响应,并根据先前的经验反馈确定不同输入的相关性。这个观点在加拿大心理学家唐纳德·赫布(Donald Hebb)1949 年的著作《行为的组织》中有所体现,他提出,附近的神经细胞的活动随着时间会趋同,有时被简化为赫布定律:“放电在一起联结在一起”^(7 8)。基于赫布的权重随时间变化的提议,康奈尔航空实验室的研究员弗兰克·罗森布拉特(Frank Rosenblatt)在 1950 年代提出了感知器(perceptron)模型。⁹ 他用自适应权重替代了 TLU 模型中的固定权重,并增加了偏置项,得到了一个新的函数:

我们注意到,输入I已被标记为X以突显它们可以是任何值,而不仅仅是二进制01。将赫布的观察与 TLU 模型相结合,感知器的权重将根据简单的学习规则进行更新:

  1. 从一组 J 个样本x(1) …. x(j)出发。这些样本都有标签 y,可以是 0 或 1,提供有标记的数据(y, x)(1) …. (y, x)(j)。这些样本可以是单个值,此时感知器有单个输入,也可以是长度为N且具有 i的多值输入的向量。

  2. 初始化所有权重w为小的随机值或 0。

  3. 使用感知器函数计算所有示例x的估计值y_hat

  4. 使用学习速率r更新权重,以更接近于每一步t中训练的期望输出值:

    ,对于所有的J个样本和N个特征。概念上,需要注意如果y为 0 且目标值为 1,我们希望通过一定的增量r增加权重的值;同样,如果目标值为 0 且估计值为 1,我们希望减小权重,使得输入值不超过阈值。

  5. 重复步骤 3-4,直到预测输出y和实际输出yhat之间的差值低于某个期望的阈值。在有非零偏置项b的情况下,也可以使用类似的公式来计算更新。

尽管简单,你可以理解这样的分类器可以学习到许多模式,但仍然不能学习到XOR函数。然而,通过将几个感知机组合成多个层,这些单元可以表示任何简单的布尔函数,¹⁰而且麦卡洛克和皮茨此前已经推测过将这些简单单元组合成一个通用计算引擎或图灵机,可以表示标准编程语言中的任何操作。然而,前述学习算法对每个单元独立操作,这意味着它可以扩展到由许多层感知机组成的网络(图 3.3)。

图 3.3:一个多层感知机¹¹

然而,麻省理工学院的计算机科学家马文·明斯基和西摩·帕珀特在 1969 年的书籍《感知机》中表明,一个三层前馈网络需要至少有一个这些单元(在第一层)与所有输入之间的完全(非零权重)连接才能计算所有可能的逻辑输出¹²。这意味着,与生物神经元只连接到少数邻居相比,这些计算模型需要非常密集的连接。

虽然后来的架构中已经融入了连接的稀疏性,比如 CNNs,但这种密集的连接仍然是许多现代模型的特征,特别是在通常形成模型倒数第二层隐藏层的全连接层中。除了这些模型在当时的硬件上计算上不便利外,对于稀疏模型无法计算所有逻辑运算的观察被研究界更广泛地解释为感知机无法计算 XOR。虽然是错误的,¹³但这个观点导致了 AI 在随后的几年里资金的枯竭,有时这段时期被称为AI 冬季¹⁴。

神经网络研究的下一次革命将需要一种更有效的方法来计算复杂模型中更新所需的参数,这种技术将被称为反向传播

多层感知机和反向传播

尽管自从《感知机》出版后,直到 1980 年代,神经网络的大规模研究资金都在下降,但研究人员仍然认识到这些模型有价值,特别是当它们被组装成由多个感知机单元组成的多层网络时。事实上,当输出函数的数学形式(即模型的输出)被放宽为多种形式(如线性函数或 Sigmoid 函数)时,这些网络可以解决回归和分类问题,理论结果表明,3 层网络可以有效逼近任何输出。¹⁵然而,这项工作没有解决这些模型的计算解的实际限制,而之前描述的感知机学习算法等规则对它们的应用造成了很大的限制。

对神经网络的重新关注始于反向传播算法的普及,该算法尽管在 20 世纪 60 年代已经被发现,但直到 20 世纪 80 年代才被广泛应用于神经网络,此前的多项研究强调了它在学习这些模型中的权重方面的有用性。¹⁶ 正如你在感知机模型中所看到的,更新权重的学习规则在没有“隐藏”层的情况下是相对容易推导出来的。输入只被感知机一次性地转换为输出值,意味着可以直接调整权重以产生期望的输出。当输入和输出之间有隐藏层时,问题就变得更加复杂:我们何时改变内部权重以计算输入权重遍历到最终输出的激活值?我们如何根据输入权重来修改它们?

反向传播技术的见解在于,我们可以利用微积分中的链式法则来高效地计算网络中每个参数相对于损失函数的导数,并且结合学习规则,这为训练多层网络提供了一种可扩展的方法。

让我们用一个例子来说明反向传播:考虑一个像图 3.3中所示的网络。假设最终层中的输出是使用 S 形函数计算的,这将产生一个值在 0 到 1 之间:

此外,值y,即最终神经元的输入之和,是隐藏单元的 S 形输入的加权和:

我们还需要一个概念,来判断网络在完成任务时是表现良好还是不良好。在这里可以使用的一个直观的误差函数是平方损失:

其中yhat是估计值(来自模型输出),y是所有输入示例J和网络K的输出的实际值的总和(其中K=1,因为只有一个输出值)。 反向传播开始于“前向传递”,在这一步中我们计算内层和外层所有输出的值,从而得到yhat的估计值。然后我们进行后向传递来计算梯度来更新权重。

我们的总体目标是计算每个神经元的权重w和偏置项 b 的偏导数:∂E/∂w∂E/∂b,这将使我们能够计算出bw的更新。为了实现这个目标,让我们从计算最终神经元输入的更新规则开始;我们希望使用链式规则来计算误差E对于每个这些输入的偏导数(在本例中有五个,对应于五个隐藏层神经元):

我们可以通过对损失函数求导来得到值∂E/∂y

对于单个示例,这只是输入和输出值之间的差异。对于∂y/∂x,我们需要对 Sigmoid 函数进行偏导数:

综上所述,我们有:

如果我们想要计算特定参数x(如权重w或偏置项b)的梯度,我们需要多做一步:

我们已经知道第一项,且x仅通过来自下层y的输入依赖于w,因为这是一个线性函数,所以我们得到:

如果我们想为隐藏层中的一个神经元计算此导数,我们以同样的方式对这个输入yᵢ进行偏导数计算,这很简单:

因此,我们总共可以对所有输入到这个隐藏层的单元求和:

我们可以递归地重复这个过程以获得所需的更新规则,因为我们现在知道如何在任何层计算yw的梯度。这使得更新权重的过程变得高效,因为一旦我们通过反向传播计算了梯度,我们就可以结合连续的梯度通过层来得到网络任何深度所需的梯度。

现在,我们已经得到了每个w(或其他需要计算的神经元参数)的梯度,我们如何制定"学习规则"来更新权重?在他们的论文中,Hinton 等人指出,我们可以在每个样本批处理上计算梯度后应用更新,但建议在所有样本上计算平均值后应用更新。梯度表示误差函数相对于参数发生最大变化的方向;因此,为了更新,我们希望将权重推向相反的方向,e是一个小值(步长):

然后在训练过程中的每个时间点t,我们使用计算出的梯度更新权重:

扩展这个方法,Hinton 等人提出了一个当前梯度的指数加权更新加上先前更新的方法:

其中 alpha 是一个衰减参数,用于加权先前更新的贡献,取值范围从 0 到 1。根据这个过程,我们将使用一些小的随机值初始化网络中的权重,选择步长e,并通过前向和后向传播以及参数更新进行迭代,直到损失函数达到某个期望值。

现在我们已经描述了反向传播背后的形式数学,让我们看看它在实践中如何在 TensorFlow 2 等软件包中实现。

实践中的反向传播

虽然通过这种推导来理解深度神经网络的更新规则是有用的,但对于大型网络和复杂架构来说,这显然会很快变得难以管理。因此,幸运的是,TensorFlow 2 可以自动处理这些梯度的计算。在模型初始化期间,每个梯度都被计算为图中张量和操作之间的中间节点:例如,参见图 3.4

图 3.4:将梯度操作插入到 TensorFlow 图中¹⁸

在上述图的左侧显示了一个成本函数 C,它是从修正线性单元ReLU)的输出中计算得到的(一种我们将在本章后面介绍的神经元函数),而这个输出又是通过将一个权重向量乘以输入 x 并添加一个偏置项 b 计算得到的。在右侧,你可以看到 TensorFlow 已经扩展了这个图,以计算作为整个控制流一部分所需的所有中间梯度。

在存储了这些中间值之后,通过递归操作将它们组合成完整的梯度的任务交给了 GradientTape API。在幕后,TensorFlow 使用一种称为反向模式自动微分的方法来计算梯度;它将依赖变量(输出y)固定,并且从网络的末端递归地向前计算所需的梯度。

例如,让我们考虑以下形式的神经网络:

图 3.5:反向模式自动微分¹⁹

如果我们想要计算输出 y 关于输入 x 的导数,我们需要重复地代入最外层的表达式²⁰:

因此,为了计算所需的梯度,我们只需从上到下遍历图,当我们计算时存储每个中间梯度。这些值被存储在一个记录上,被称为磁带,这是一个对早期计算机的参考,其中信息存储在磁带上,²¹然后用于重放值以进行计算。另一种方法是使用前向模式自动微分,从下到上计算。这需要两次而不是一次传递(对于每个馈入到最终值的分支),但在概念上更容易实现,不需要反向模式的存储内存。然而,更重要的是,反向模式模仿了我之前描述的反向传播的推导。

这个磁带(也称为Wengert Tape,以其开发者之一命名)实际上是一个数据结构,你可以在 TensorFlow Core API 中访问到。例如,导入核心库:

from __future__ import absolute_import, division, print_function, unicode_literals
import tensorflow as tf 

然后,可以使用 tf.GradientTape() 方法来获取这个磁带,在其中你可以评估与图中间值相关的梯度²²:

x = tf.ones((2, 2))
with tf.GradientTape() as t:
  t.watch(x)
  y = tf.reduce_sum(x)
  z = tf.multiply(y, y)
# Use the tape to compute the derivative of z with respect to the
# intermediate value y.
dz_dy = t.gradient(z, y)
# note that the resulting derivative, 2*y, = sum(x)*2 = 8
assert dz_dy.numpy() == 8.0 

默认情况下,GradientTape() 使用的内存资源在调用 gradient() 后被释放;但是,你也可以使用 persistent 参数来存储这些结果²³:

x = tf.constant(3.0)
with tf.GradientTape(persistent=True) as t:
  t.watch(x)
  y = x * x
  z = y * y
dz_dx = t.gradient(z, x)  # 108.0 (4*x³ at x = 3)
dy_dx = t.gradient(y, x)  # 6.0 

现在你已经看到 TensorFlow 如何实际计算梯度以评估反向传播,让我们回顾一下反向传播技术是如何随着时间的推移而发展,以应对实际实现中的挑战的细节。

反向传播的缺陷

虽然反向传播过程提供了一种以合理方式更新网络内部权重的方法,但它存在几个缺点,使得深度网络在实践中难以使用。其中一个是 梯度消失 的问题。在我们推导反向传播公式时,你看到网络中更深层次的权重的梯度是来自更高层的连续偏导数的乘积。在我们的例子中,我们使用了 Sigmoid 函数;如果我们绘制出 Sigmoid 的值及其一阶导数,我们可以看到一个潜在的问题:

图 3.6:Sigmoid 函数及其梯度²⁴

随着 Sigmoid 函数的值向极端值(0 或 1,代表“关闭”或“打开”)增加或减少,梯度的值趋近于零。这意味着从隐藏激活函数 y 的这些梯度得到的更新值 wb 会趋向于零,使得权重在迭代之间变化很小,使得反向传播过程中隐藏层神经元的参数变化非常缓慢。很显然,这里的一个问题是 Sigmoid 函数饱和;因此,选择另一个非线性函数可能会规避这个问题(这确实是作为 ReLU 提出的解决方案之一,我们稍后会讨论)。

另一个问题更微妙,与网络如何利用其可用的自由参数有关。正如你在 第一章生成型 AI 简介:“从模型中“绘制”数据 中看到的,变量的后验概率可以计算为似然和先验分布的乘积。我们可以将深度神经网络看作是这种概率的图形表示:神经元的输出,取决于其参数,是所有输入值和这些输入上的分布(先验)的乘积。当这些值变得紧密耦合时就会出现问题。举个例子,考虑一下头痛的竞争性假设:

图 3.7:解释逆效应

如果一个病人患有癌症,那么关于他们是否感冒的证据是如此压倒性,以至于没有提供额外价值;实际上,两个先前的假设的价值由于其中一个的影响而变得耦合。这使得计算不同参数的相对贡献变得棘手,特别是在深层网络中;我们将在我们关于《第四章,教网络生成数字》中讨论受限玻尔兹曼机和深度信念网络的问题。正如我们在该章节中将更详细地描述的那样,一项 2006 年的研究²⁵展示了如何抵消这种效应,这是对深度神经网络中可行推断的最早的一次突破,这一突破依赖于产生手绘数字图像的生成模型。

除了这些问题之外,在 20 世纪 90 年代和 21 世纪初,神经网络更广泛应用的其他挑战还包括像支持矢量机²⁶、梯度和随机梯度提升模型²⁷、随机森林²⁸甚至是惩罚回归方法如 LASSO²⁹和 Elastic Net³⁰这样的方法,用于分类和回归任务。

虽然理论上,深度神经网络的表征能力可能比这些模型更强,因为它们通过连续层构建输入数据的分层表示,与通过单一转换给出的“浅”表示如回归权重或决策树相反,但在实践中,训练深层网络的挑战使得这些“浅”方法对实际应用更有吸引力。这也与较大网络需要调整成千上万甚至是百万参数的事实相搭上了较大计算资源的事实,使这些实验在云供应商提供的廉价计算资源的爆炸之前是不可行的,包括 GPU 和 TPU 特别适用于快速矩阵计算。

现在我们已经介绍了训练简单网络架构的基础知识,让我们转向更复杂的模型,这些模型将构成书中许多生成模型的基础:CNNs 和序列模型(RNNs,LSTMs 等)。

网络的种类:卷积和递归

到目前为止,我们主要通过引用前馈网络来讨论神经网络的基础知识,其中每个输入都连接到每个层的每个输出。虽然这些前馈网络有助于说明深层网络的训练方式,但它们只是现代应用中使用的一类更广泛架构的一部分,包括生成模型。因此,在讨论使训练大型网络变得实用的一些技术之前,让我们回顾一下这些替代的深度模型。

视觉网络:卷积架构

正如本章开头所指出的,深度神经网络模型的灵感之一是生物神经系统。当研究人员试图设计可以模仿视觉系统功能的计算机视觉系统时,他们转向了视网膜的结构,这是在 20 世纪 60 年代神经生物学家 David Huber 和 Torsten Weisel 的生理学研究中揭示的。³¹ 正如以前所描述的,生理学家 Santiago Ramon Y Cajal 提供了神经结构如视网膜被安排在垂直网络中的视觉证据。

图 3.8:视网膜的“深层神经网络”^(32 33)

Huber 和 Weisel 研究了猫的视网膜系统,展示了它们对形状的知觉是由排列在一列中的单个细胞的活动所组成的。每一列细胞都被设计用来检测输入图像中边缘的特定方向;复杂形状的图像是由这些简单图像拼接在一起的。

早期的 CNNs

这种列的概念启发了对 CNN 结构的早期研究³⁴。与前馈网络中学习单元之间的个体权重不同,这种结构(图 3.9)使用了专门用于检测图像中特定边缘的一组神经元中的共享权重。网络的初始层(标记为 H1)由每个 64 个神经元的 12 组组成。这些组中的每个都是通过在 16 x 16 像素的输入图像上传递一个 5 x 5 的网格来得到的;这个组中的每一个 64 个 5 x 5 的网格共享相同的权重,但与输入的不同空间区域相关联。你可以看到,如果它们的接受域重叠了两个像素,那么每个组中必须有 64 个神经元来覆盖输入图像。

当这 12 组神经元在 H1 层中结合在一起时,它们形成了 12 个表示图像中特定边缘的存在或不存在的 8 x 8 网格(图 3.9)。这种权重共享在直观上是有意义的,因为由权重表示的卷积核被指定用来检测图像中的不同颜色和/或形状,不管它出现在图像的哪个位置。这种降采样的效果是一定程度上的位置不变性;我们只知道边缘发生在图像某个区域内,但由于降采样导致的分辨率降低,我们无法知道确切位置。因为它们是通过将一个 5 x 5 的矩阵(卷积核)与图像的一部分相乘得到的,这种操作被用在图像模糊和其他转换中,这 5 x 5 的输入特征被称为 卷积核,也给网络起了名字。

图 3.9:卷积神经网络³⁵

当我们有了这 12 个缩小了的 8 x 8 图像时,下一层(H2)还有 12 组神经元;在这里,卷积核是 5 x 5 x 8——它们横跨从H1上的一个 8 x 8 地图,遍及 12 个中的 8 个组。由于一个 5 x 5 的网格可以在 8 x 8 的网格上上下移动四次以覆盖 8 x 8 网格中的所有像素,我们需要 16 个这样的 5 x 5 x 8 组的神经元。

就像视觉皮层中更深层的细胞一样,网络中的更深层对来自不同边缘检测器的多个列进行整合,将信息组合在一起。

最后,该网络的第三个隐藏层(H3)包含 30 个隐藏单元和H2中的 12 x 16 个单元之间的全全连接,就像在传统的前馈网络中一样;最终的 10 个输出单元将输入图像分类为 10 个手写数字之一。

通过权重共享,在该网络中的自由参数总数得到了减少,虽然在绝对术语中仍然很大。虽然反向传播成功地用于此任务,但需要为一组成员受限的图像设计精心的网络,这些图像具有局限性的结果——对于如检测来自数百或数千个可能类别的对象等实际应用,需要采用其他方法。

AlexNet 和其他 CNN 创新技术

2012 年的一篇文章产生了最先进的结果,使用一个被称为 AlexNet 的模型将 ImageNet 中的 130 万张图像分类为 1000 种分类。这些模型要实现训练,需要采用一些后来的创新技术。(36)如我之前提到的一样,一个是使用 ReLU(37)替代 sigmoid 或双曲正切函数。ReLU 是以下形式的函数:

与 sigmoid 函数或 tanh 相比,在函数饱和时,其导数会缩小至 0,而 ReLU 函数具有恒定的梯度和 0 处的不连续性(图 3.10)。这意味着梯度不会饱和,导致网络的深层训练更慢,导致优化困难。

图 3.10:替代激活函数的梯度(38)

虽然 ReLU 函数具有非消失梯度和低计算要求的优势(因为它们只是阈值线性变换),但缺点是如果输入低于 0,则它们可能会“关闭”,导致再次出现 0 梯度。这个问题在之后的工作中得到解决,在 0 以下引入了一个“泄漏”。(39)

进一步的改进是使此阈值自适应,具有斜率为a参数化泄漏 ReLUPReLU)。(40)

AlexNet 使用的另一个技巧是辍学。⁴¹ 辍学的想法受到合奏方法的启发,在合奏方法中,我们对许多模型的预测进行平均,以获得更稳健的结果。显然,对于深度神经网络来说,这是不可行的;因此,一个妥协方案是以 0.5 的概率随机将某些神经元的值设为 0。这些值在每次反向传播的前向传递中被重置,允许网络有效地对不同的架构进行采样,因为“辍学”的神经元在该传递中不参与输出。

图 3.11:辍学

AlexNet 中使用的另一个增强是局部响应归一化。尽管 ReLU 不像其他单元那样饱和,模型的作者仍然发现限制输出范围有价值。例如,在一个单个卷积核中,他们使用相邻卷积核的值对输入进行归一化,这意味着总体响应被重新缩放⁴²:

其中a是图像上给定xy位置处的非标准化输出,j的总和是在相邻卷积核上,Bk和 alpha 是超参数。这种重新缩放让人想起后来被广泛应用于卷积和其他神经网络架构中的一种创新,批量归一化⁴³。批量归一化还对网络内部的“原始”激活应用转换:

其中x是非标准化输出,By是尺度和偏移参数。这种转换被广泛应用于许多神经网络架构,以加速训练,尽管它的有效原因仍然是争论的话题。⁴⁴

现在你对使大型 CNN 训练成为可能的一些方法论进步有了一些了解,让我们来研究 AlexNet 的结构,看看我们将在后面章节中实现的生成模型中使用的一些额外的架构组件。

AlexNet 架构

尽管图 3.12中的 AlexNet 架构看起来令人生畏,但一旦我们将这个大型模型分解为单独的处理步骤,就不那么难理解了。让我们从输入图像开始,跟踪通过每个后续神经网络层的一系列转换为每个图像计算输出分类的方法。

图 3.12:AlexNet

输入到 AlexNet 的图像大小为 224 x 224 x 3(对于 RGB 通道)。第一层由 96 个单元和 11 x 11 x 3 卷积核组成;输出经过响应归一化(如前所述)和最大化池化。最大化池化是一种采取n x n网格上的最大值来记录输入中是否“任何位置”出现模式的操作;这又是一种位置不变性的形式。

第二层也是一组规模为 5 x 5 x 8 的卷积,以 256 个为一组。第三层到第五层都有额外的卷积,没有规范化,接着是两个全连接层和一个输出大小为 1,000 表示 ImageNet 中可能的图像类。AlexNet 的作者使用了几个 GPU 来训练模型,这种加速对输出非常重要。

图 3.13:来自 AlexNet 的图像核

在初始的 11 x 11 x 3 卷积中,即训练过程中学到的特征中(图 3.13),我们可以看到可识别的边缘和颜色。虽然 AlexNet 的作者没有展示出网络中更高层次的神经元合成这些基本特征的例子,但另一项研究提供了一个示例,在该研究中,研究人员训练了一个大型的 CNN 来对 YouTube 视频中的图像进行分类,得到了网络最上层的一个神经元,它似乎是一个猫探测器(图 3.14)。

图 3.14:从 YouTube 视频中学习到的猫探测器⁴⁵

这个概述应该让你明白 CNN 架构看起来的样子,以及什么样的发展使得它们随着时间的推移而成为图像分类器或基于图像的生成模型的基础更加可行。现在我们将转向另一类更专业的架构——RNN,这种架构用于开发时间或基于序列的模型。

序列数据的网络

除了图像数据,自然语言文本也一直是神经网络研究中的一个热门话题。然而,与我们迄今为止检查的数据集不同,语言有一个重要的顺序与其含义相关。因此,为了准确地捕捉语言或时间相关数据中的模式,有必要使用专门设计用于此目的的网络。

RNN 和 LSTM

让我们想象一下,我们试图预测句子中的下一个词,给定到目前为止的词。试图预测下一个词的神经网络不仅需要考虑当前词,还需要考虑可变数量的先前输入。如果我们只使用一个简单的前馈 MLP,该网络实际上会将整个句子或每个词都处理为一个向量。这引入了这样一个问题:要么必须将可变长度的输入填充到一个共同的长度,并且不保留任何相关性的概念(也就是说,在生成下一个预测时,句子中哪些单词比其他单词更相关),或者在每一步中只使用上一个词作为输入,这样会丢失句子其余部分的上下文和提供的所有信息。这种问题激发了“原生”RNN⁴⁶,它在计算一个神经元的输出时,不仅考虑当前输入,还考虑前一步的隐藏状态:

可以将这个过程想象为每一层递归地馈送到下一个时间步骤的序列中。实际上,如果我们“展开”序列的每个部分,我们最终得到一个非常深的神经网络,其中每一层共享相同的权重。⁴⁷

图片

图 3.15:展开的 RNN⁴⁸

训练深度前馈网络所具有的困难也同样适用于循环神经网络;使用传统激活函数时,梯度往往在长距离上衰减(或者如果梯度大于 1,则爆炸)。

然而,与前馈网络不同,RNNs 不是用传统的反向传播进行训练,而是用一种称为时间反向传播BPTT)的变体:网络被展开,如前所述,使用反向传播,对每个时间点的误差进行平均处理(因为每一步都有一个“输出”,即隐藏状态)。⁴⁹此外,在 RNNs 的情况下,我们遇到的问题是网络的记忆非常短暂;它只包含最近单元的信息,而当前单元之前的信息则难以保持长期上下文。对于翻译等应用来说,这显然是一个问题,因为句子末尾的单词的解释可能依赖于句子开头的术语,而不仅仅是直接前面的术语。

LSTM 网络的开发是为了使 RNNs 能够在长序列上保持上下文或状态。⁵⁰

图片

图 3.16:LSTM 网络

在传统的 RNN 中,我们只保留来自前一步隐藏单元激活的短期记忆h。除了这个短期记忆外,LSTM 架构引入了一个额外的层c,即“长期”记忆,它可以持续多个时间步长。从某种意义上说,这种设计让人想起了电容器,它可以使用c层来储存或保持“电荷”,一旦达到某个阈值就释放它。为了计算这些更新,一个 LSTM 单元由许多相关的神经元或门组成,这些门在每个时间步骤上一起作用来转换输入。

给定输入向量x和前一时刻t-1的隐藏状态h,在每个时间步长,LSTM 首先计算了一个值,从 0 到 1 表示c的每个元素中“遗忘”了多少信息:

图片

我们进行第二次类似的计算来确定要保留输入值的哪些部分:

图片

现在我们知道了c的哪些元素被更新了;我们可以计算这个更新如下:

图片

其中o是一个 Hadamard 积(逐元素乘法)。本质上,这个方程告诉我们如何使用 tanh 变换计算更新,使用输入门过滤它们,并使用忘记门将它们与前一个时间步的长期记忆结合起来,以潜在地过滤掉旧值。

要计算每个时间步的输出,我们计算另一个输出门:

图片

并且在每一步计算最终输出时(隐藏层作为下一步的短期记忆提供给下一步),我们有:

提出了许多这种基本设计的变体;例如,“窥视孔”LSTM 用c(t-1)替代了h(t-1)(因此每个操作都可以“窥视”长期记忆单元),⁵¹而 GRU⁵²通过删除输出门简化了整体设计。这些设计的共同之处在于,它们避免了训练 RNN 时出现的梯度消失(或爆炸)困难,因为长期记忆充当缓冲区,以维持梯度并在许多时间步骤上传播神经元激活。

构建更好的优化器

到目前为止,在本章中,我们已经讨论了几个例子,其中更好的神经网络架构实现了突破;然而,与此同样(甚至更加)重要的是用于在这些问题中最小化误差函数的优化过程,通过选择产生最低误差的参数来“学习”网络的参数。回顾我们对反向传播的讨论,这个问题有两个组成部分:

  • 如何初始化权重:在许多历史应用中,我们看到作者使用了一定范围内的随机权重,并希望通过反向传播的使用从这个随机起始点至少得到一个局部最小化的损失函数。

  • 如何找到局部最小损失:在基本的反向传播中,我们使用梯度下降和固定学习率以及一阶导数更新来遍历权重矩阵的潜在解空间;然而,有充分的理由相信可能存在更有效的方法来找到局部最小值。

事实上,这两者都被证明是深度学习研究进展的关键考虑因素。

梯度下降到 ADAM

正如我们在反向传播的讨论中看到的那样,1986 年提出的用于训练神经网络的原始版本在获取梯度并更新权重之前对整个数据集进行了损失平均。显然,这相当慢,并且使模型的分发变得困难,因为我们无法分割输入数据和模型副本;如果我们使用它们,每个副本都需要访问整个数据集。

相比之下,SGD 在n个样本后计算梯度更新,其中n可以是从 1 到N(数据集的大小)的范围。在实践中,我们通常执行小批量梯度下降,其中n相对较小,而且我们在每个 epoch(数据的一次遍历)后随机分配数据给n批次。

但是,SGD 可能会很慢,导致研究人员提出加速搜索最小值的替代方案。正如在原始反向传播算法中所见,一个想法是使用一种记住先前步骤并在前进方向继续的指数加权动量形式。已经有提出了各种变体,如Nesterov Momentum,它增加了一个项来增加这种加速^(53)。

与原始反向传播算法中使用的动量项相比,将当前动量项加到梯度中有助于保持动量部分与梯度变化保持一致。

另一种优化方法,称为自适应梯度Adagrad)^(54),通过该参数梯度的平方和(G)来缩放每次更新的学习率;因此,经常更新的元素被降采样,而不经常更新的元素被推动以更大的幅度进行更新:

这种方法的缺点是,随着我们继续训练神经网络,总和G将无限增加,最终将学习率缩小到一个非常小的值。为了解决这个缺点,提出了两种变体方法,RMSProp^(55)(经常应用于 RNN)和 AdaDelta^(56),在计算G时加入固定宽度窗口的 n 步。

自适应动量估计ADAM)^(57)可以看作是一种尝试将动量和 AdaDelta 结合起来;动量计算用于保留过去梯度更新的历史,而在 AdaDelta 中使用的固定更新窗口内的衰减平方梯度总和用于调整结果梯度的大小。

这里提到的方法都具有一阶的特性:它们只涉及损失对输入的一阶导数。虽然计算简单,但这可能导致在神经网络参数的复杂解空间中导航时出现实际挑战。如图 3.17所示,如果我们将权重参数的景观视为一条沟壑,那么一阶方法要么在曲率快速变化的区域移动得太快(顶部图像),超调极小值,要么在曲率较低的极小值“沟壑”中移动得太慢。理想的算法将考虑曲率和曲率变化的变化速率,允许优化器顺序方法在曲率变化特别缓慢时采用更大的步长,反之亦然(底部图像)。

图 3.17:复杂的景观和二阶方法^(58)

因为它们利用了导数的改变速率(二阶导数),这些方法被称为二阶,并且在优化神经网络模型中已经取得了一定的成功^(59)。

然而,每次更新所需的计算量比一阶方法大,因为大多数二阶方法涉及大型矩阵求逆(因此内存利用率高),需要近似来使这些方法可扩展。然而,最终,实际优化网络的突破之一不仅来自于优化算法,还包括我们如何初始化模型中的权重。

Xavier 初始化

正如之前所述,在早期研究中,常常用一定范围的随机值初始化神经网络的权重。2006 年在深度置信网络的训练中取得的突破,正如您将在第四章教授网络生成数字中看到的那样,使用了预训练(通过生成建模方法)来在执行标准反向传播之前初始化权重。

如果您曾经在 TensorFlow Keras 模块中使用过一个层,您会注意到层权重的默认初始化是从截断的正态分布或均匀分布中抽取的。这个选择是从哪里来的?正如我之前描述的,使用 S 型或双曲线激活函数的深度网络的一个挑战是,它们倾向于变得饱和,因为这些函数的值受到非常大或负的输入的限制。我们可以解释初始化网络的挑战是保持权重在这样一个范围内,以至于它们不会使神经元的输出饱和。另一种理解方法是假设神经元的输入和输出值具有类似的方差;信号在通过神经元时不会被大幅放大或减小。

在实践中,对于一个线性神经元,y = wx + b,我们可以计算输入和输出的方差为:

b是常数,因此我们剩下:

由于权重矩阵中有N个元素,并且我们希望var(y)等于var(x),这给出了:

因此,对于权重矩阵w,我们可以使用方差为 1/N(输入和输出单元的平均数量,因此权重的数量)的截断正态分布或均匀分布。⁶⁰变体也已经应用于 ReLU 单元:⁶¹这些方法被称为它们原始作者的名字,如 Xavier 或 He 初始化。

总的来说,我们回顾了 TensorFlow 2 中底层使用的几种常见优化器,并讨论了它们如何改进基本的 SGD 形式。我们还讨论了聪明的权重初始化方案如何与这些优化器共同作用,使我们能够训练越来越复杂的模型。

摘要

在本章中,我们涵盖了深度学习的基本词汇 - 如起始对感知器和多层感知器的研究导致了简单的学习规则被放弃,而采用反向传播。我们还研究了专门的神经网络架构,如基于视觉皮层的卷积神经网络(CNNs),以及专门用于序列建模的循环网络。最后,我们检查了最初为反向传播提出的梯度下降算法的变体,这些变体的优点包括动量,并描述了将网络参数放在更容易导航到局部最小值范围的权重初始化方案。

在这种背景下,我们将着手进行生成模型的项目,首先是使用深度信念网络生成 MNIST 数字的项目,见第四章教授网络生成数字

参考文献

  1. López-Muñoz F., Boya J., Alamo C. (2006). 神经元理论,神经科学的基石,颁给圣地亚哥·拉蒙·伊·卡哈尔的诺贝尔奖 100 周年。《大脑研究公报》. 70 (4–6):391–405. pubmed.ncbi.nlm.nih.gov/17027775/

  2. Ramón y Cajal, Santiago (1888). 鸟类中枢神经中枢结构

  3. McCulloch, W.S., Pitts, W. (1943). 神经活动中所固有的思想的逻辑演算。数理生物物理学通报5, 115–133. doi.org/10.1007/BF02478259

  4. 请注意:Rashwan M., Ez R., reheem G. (2017). 阿拉伯语言语音识别的计算智能算法.《开罗大学工程领域杂志》. 12. 886-893. 10.21608/auej.2017.19198. wwwold.ece.utep.edu/research/webfuzzy/docs/kk-thesis/kk-thesis-html/node12.html

  5. Rashwan M., Ez R., reheem G. (2017). 阿拉伯语言语音识别的计算智能算法.《开罗大学工程领域杂志》. 12. 886-893. 10.21608/auej.2017.19198. wwwold.ece.utep.edu/research/webfuzzy/docs/kk-thesis/kk-thesis-html/node12.html

  6. 人工神经元. 维基百科. 检索日期:2021 年 4 月 26 日,网址:en.wikipedia.org/wiki/Artificial_neuron

  7. Shackleton-Jones Nick. (2019 年 5 月 3 日).人们如何学习:设计教育和培训,以提高绩效。Kogan Page。英国伦敦

  8. Hebb, D. O. (1949). 行为组织:神经心理学理论。纽约:Wiley 和 Sons 出版社

  9. Rosenblatt, Frank (1957). 感知器-一个认知和识别自动装置。报告 85-460-1. 康奈尔航空实验室。

  10. Marvin Minsky 和 Seymour Papert,1972 年(第二版,第一版 1969 年)《感知器:计算几何的介绍》,MIT 出版社,剑桥,马萨诸塞州

  11. Hassan, Hassan & Negm, Abdelazim & Zahran, Mohamed & Saavedra, Oliver. (2015). 利用高分辨率卫星图像评估人工神经网络进行浅水湖泊水深估计:以 El Burullus Lake 为例. 国际水技术期刊. 5.

  12. Marvin Minsky 和 Seymour Papert, 1972 (第二版带有更正,第一版 1969) 感知机:计算几何简介, The MIT Press, 剑桥 MA

  13. Pollack, J. B. (1989). "无意伤害:感知机扩展版评论". 数学心理学杂志. 33 (3): 358–365.

  14. Crevier, Daniel (1993), AI:人工智能的动荡探索, 纽约,纽约: BasicBooks.

  15. Cybenko, G. 通过 S 型函数的叠加进行逼近. 数学. 控制信号系统 2, 303–314 (1989). doi.org/10.1007/BF02551274

  16. Goodfellow, Ian; Bengio, Yoshua; Courville, Aaron (2016). 6.5 反向传播和其他差分算法. 深度学习. MIT 出版社. pp. 200–220

  17. Rumelhart, D., Hinton, G. & Williams, R. (1986) 通过反向传播误差学习表示. 自然 323, 533–536. doi.org/10.1038/323533a0

  18. Guess A R., (2015 年 11 月 10 日). Google 开源机器学习库 TensorFlow. DATAVERSITY. www.dataversity.net/google-open-sources-machine-learning-library-tensorflow/

  19. Berland (2007). ReverseaccumulationAD.png. 维基百科. 可从: commons.wikimedia.org/wiki/File:ReverseaccumulationAD.png

  20. 自动微分. 维基百科. en.wikipedia.org/wiki/Automatic_differentiation

  21. R.E. Wengert (1964). 一个简单的自动导数评估程序. Comm. ACM. 7 (8): 463–464.;Bartholomew-Biggs, Michael; Brown, Steven; Christianson, Bruce; Dixon, Laurence (2000). 算法的自动微分. 计算与应用数学杂志. 124 (1–2): 171–190.

  22. TensorFlow 作者 (2018). automatic_differentiation.ipynb. 可从: colab.research.google.com/github/tensorflow/tensorflow/blob/r1.9/tensorflow/contrib/eager/python/examples/notebooks/automatic_differentiation.ipynb#scrollTo=t09eeeR5prIJ

  23. TensorFlow 作者. 梯度和自动微分简介. TensorFlow. 可从: www.tensorflow.org/guide/autodiff

  24. Thomas (2018). 梯度消失问题和 ReLU – TensorFlow 调查. 机器学习冒险。查阅:adventuresinmachinelearning.com/vanishing-gradient-problem-tensorflow/

  25. Hinton, Osindero, Yee-Whye (2005). 深度信念网络的快速学习算法. 多伦多大学,计算机科学。查阅:www.cs.toronto.edu/~fritz/absps/ncfast.pdf

  26. Cortes, C., Vapnik, V. 支持向量网络. 机器学习 20, 273–297 (1995). doi.org/10.1007/BF00994018

  27. Friedman, J. H. (February 1999). 贪婪函数逼近:梯度增强机 (PDF)

  28. Breiman, L. 随机森林. 机器学习 45, 5–32 (2001). doi.org/10.1023/A:1010933404324

  29. Tibshirani R. (1996). 通过套索实现回归收缩和选择. 英国皇家统计学会杂志。Wiley. 58 (1): 267–88.

  30. Zou H., Hastie T. (2005). 通过弹性网络实现正规化和变量选择. 英国皇家统计学会杂志 B 系列:301–320

  31. Hubel D. H., Wiesel T. N. (1962) 感觉野,视交互及猫脑视觉皮层功能体系结构. 生理学杂志,1962, 160: 106-154。doi.org/10.1113/jphysiol.1962.sp006837

  32. charlesfrye.github.io/FoundationalNeuroscience/img/corticalLayers.gif

  33. Wolfe, Kluender, Levy (2009). 感知和知觉. 坎伯兰:Sinauer Associates Inc.。

  34. LeCun, Yann, et al. 反向传播应用于手写邮政编码识别. 神经计算,1.4 (1989): 541-551.

  35. LeCun, Yann, et al. 反向传播应用于手写邮政编码识别. 神经计算,1.4 (1989): 541-551.

  36. 使用深度卷积神经网络进行 ImageNet 分类www.nvidia.cn/content/tesla/pdf/machine-learning/imagenet-classification-with-deep-convolutional-nn.pdf

  37. Nair V., Hinton G E. (2010). 修正线性单元改进限制玻尔兹曼机. 机器学习国际会议论文集,2010 年,以色列海法。

  38. Agarap A F. (2019, September 5). 通过梯度噪音添加来避免伴随梯度消失的问题. 朝着数据科学。towardsdatascience.com/avoiding-the-vanishing-gradients-problem-96183fd03343

  39. Maas A L., Hannun A Y., Ng A Y. (2013). 修正线性非线性改进神经网络声学模型. 机器学习国际会议论文集,2013 年,美国佐治亚州亚特兰大市。

  40. He,K.,Zhang,X.,Ren,S.,Sun,J.(2015)。 深入挖掘整流器:在 ImageNet 分类上超越人类水平性能。 arXiv:1502.01852。arxiv.org/abs/1502.01852

  41. Hinton,G E.,Srivastava,N.,Krizhevsky,A.,Sutskever,I.,Salakhutdinov,R R.(2012)。 通过防止特征检测器的协同适应来改进神经网络。 arXiv:1207.0580。arxiv.org/abs/1207.0580

  42. Krizhevsky A.,Sutskever I.,Hinton G E.(2012)。 使用深度卷积神经网络的 ImageNet 分类。神经信息处理系统 25(NIPS 2012)的一部分。papers.nips.cc/paper/2012/file/c399862d3b9d6b76c8436e924a68c45b-Paper.pdf

  43. Ioffe,S.,Szegedy,C.(2015)。 批量归一化:通过减少内部协变量转移加速深层网络训练。 arXiv:1502.03167。 arxiv.org/abs/1502.03167

  44. Santurkar,S。,Tsipras,D。,Ilyas,A。,Madry,A.(2019)。 批量归一化如何帮助优化? arXiv:1805.11604。 arxiv.org/abs/1805.11604

  45. Dean J.,Ng,A Y.(2012)。 使用大规模脑模拟进行机器学习和人工智能。The Keyword | Google。blog.google/technology/ai/using-large-scale-brain-simulations-for/

  46. Rumelhart,D.,Hinton,G.和 Williams,R.(1986 年)通过反向传播错误学习表示自然 323,533–536。 doi.org/10.1038/323533a0

  47. LeCun,Y.,Bengio,Y.和 Hinton G.(2015)。 深度学习自然 521,436–444。 www.nature.com/articles/nature14539.epdf

  48. Olah(2015 年)。 理解 LSTM 网络. colah 的博客。可从colah.github.io/posts/2015-08-Understanding-LSTMs/获取。

  49. Mozer,M. C.(1995)。 用于时间模式识别的聚焦反向传播算法。在 Chauvin,Y .; Rumelhart,D.(eds。)。 反向传播:理论,体系结构和应用。 ResearchGate。 Hillsdale,NJ:劳伦斯 Erlbaum 凯斯。第 137-169 页。

  50. Greff K.,Srivastava,R K。,Koutník,J.,Steunebrink,B R。,Schmidhuber,J.(2017)。 LSTM:搜索空间奥德赛。 arXiv:1503.04069v2。 arxiv.org/abs/1503.04069v2

  51. Gers FA, Schmidhuber E. LSTM 循环网络学习简单的无上下文和有上下文的语言. IEEE 交易神经网络。 2001 年;12(6):1333-40. doi:10.1109/72.963769。 PMID:18249962。

  52. Cho, K., van Merrienboer, B., Gulcehre, C., Bahdanau, D., Bougares, F., Schwenk, H., Bengio, Y. (2014). 使用 RNN 编码器-解码器学习短语表示用于统计机器翻译。arXiv:1406.1078。arxiv.org/abs/1406.1078

  53. Sutskever, I., Martens, J., Dahl, G. & Hinton, G. (2013). 初始化和动量在深度学习中的重要性。第 30 届国际机器学习大会论文集, PMLR 28(3):1139-1147.

  54. Duchi J., Hazan E., Singer Y. (2011). 用于在线学习和随机优化的自适应次梯度方法。机器学习研究杂志 12 (2011) 2121-2159.

  55. Hinton, Srivastava, Swersky. 神经网络用于机器学习,第 6a 讲。可从:www.cs.toronto.edu/~tijmen/csc321/slides/lecture_slides_lec6.pdf

  56. Zeiler, M D. (2012). ADADELTA:一种自适应学习率方法。arXiv:1212.5701。arxiv.org/abs/1212.5701

  57. Kingma, D P., Ba, J. (2017). Adam:一种随机优化方法。arXiv:1412.6980。arxiv.org/abs/1412.6980

  58. Martens J. (2010). 通过无 Hessian 优化的深度学习。ICML. Vol. 27. 2010.

  59. Martens J. (2010). 通过无 Hessian 优化的深度学习。ICML. Vol. 27. 2010.

  60. Glorot X., Bengio Y., (2010). 理解训练深度前馈神经网络的困难。第十三届人工智能与统计国际会议论文集。

  61. He, K., Zhang, X., Ren, S., Sun, J. (2015). 深入研究整流器:在 ImageNet 分类上超越人类水平性能。arXiv:1502.01852。arxiv.org/abs/1502.01852

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

在前一章中,我们涵盖了神经网络模型的构建基块。在这一章中,我们的第一个项目将重新创建深度学习历史上最具突破性的模型之一- 深度信念网络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库:

pip install tensorflow-datasets 

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

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 数据:

mnist_builder = tfds.builder("mnist")
mnist_builder.download_and_prepare() 

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

info = mnist_builder.info
print(info) 

这给出了以下输出:

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)。

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

mnist_train = mnist_builder.as_dataset(split="train") 

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

fig = tfds.show_examples(info, mnist_train) 

这给出了以下图表:

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

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

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

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:

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

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(),并用于初始化网络的权重,包括根据输入维度推断权重的正确大小:

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 之间的激活概率,随机打开或关闭隐藏单元:

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) 

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

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() 方法时使用的前向传播:

def call(self, inputs):
    return tf.sigmoid(tf.add(tf.matmul(inputs, self.w), self.hb)) 

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

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 采样:

def reverse_gibbs(self, x):
    return self.sample_h(self.sample_v(x)) 

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

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 将其转换为常数,以便在自动梯度计算期间不将其视为变量)。然后,我们计算三个权重矩阵的梯度并更新其值,然后返回我们的重建成本作为监视进度的一种方式。重建成本只是输入和重建数据之间的交叉熵损失:

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 数字的扁平化形状,并运行连续的训练周期:

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 的滤波器,类似于我们在 第三章深度神经网络的构建块 中学习的卷积网络中的卷积核。我们可以可视化其中一些,看看它们在图像中识别出了什么样的模式:

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 滤波器

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

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):

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 堆栈的生成学习过程:

# 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 类的成员函数:

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步骤。我们从向上传递开始:

# 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,它将仅为生成(向下)权重计算更新,即除了最后一层(关联的,无向连接)之外的每一层:

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

# 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算法的逆向传递数据;我们通过再次对最后一层的输入应用映射来实现这一点:

reverse_inputs = inputs_layers[-1].map(self._rbm_layers[-1].forward) 

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

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

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

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 层的训练权重的结果:

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() 调用包含了神经网络的前向传递:

def call(self, x, training):
    for dense_layer in self._dense_layers:
        x = dense_layer(x)
    return x 

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

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 数字的问题以及我们如何将这种方法扩展到更复杂的数据。

第一章生成式人工智能简介:“从模型中”绘制数据z第四章教网络生成数字中回想起,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:

import tensorflow.compat.v2 as tf
import tensorflow_datasets as tfds
cifar10_builder = tfds.builder("cifar10")
cifar10_builder.download_and_prepare() 

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

cifar10_train = cifar10_builder.as_dataset(split="train")
cifar10_test = cifar10_builder.as_dataset(split="test") 

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

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 个类别中的一个)。我们也可以绘制图像来进行视觉检查:

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:

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 网络:

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 实现这个简单网络:

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:

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 模型类的编码和解码函数:

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 等设备。我们从一个随机正态分布中抽取一个值,对于指定数量的样本,然后应用解码器来生成新图像:

@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 的似然的方差。我们需要实现这个变换,它的给定形式如下:

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(s²)=2log(s),所以 0.5 取消了 2,给我们留下了exp(log(s))=s,正如我们在公式中需要的那样。

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

@property
    def iaf(self):
        return self._iaf 

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

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注释装饰这个函数,以便它在图形运行时执行:

@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):

@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,我们可以这样做:

model = VAE(input_shape=(1,3072), hidden_dim=500, latent_dim=500) 

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

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 的估计值以验证我们的模型是否在改善:

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,然后解码得到的重构进行比较:

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 图像的编码产品中创建新图像的批次:

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,即z_dim <= x_dim。这是对真实世界信息进行压缩或编码的一种方式。

文本说明自动生成

图 6.4:生成器模型

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

训练生成对抗网络

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

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

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

目标函数帮助我们理解每个参与者的目标。如果我们假设两个概率密度在任何地方都非零,我们可以得到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)

    • 更新鉴别器损失,Jᵈ

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

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

  4. 更新生成器损失,Jᵍ

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

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

A close up of a map  Description automatically generated

图 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 散度不是对称度量,也就是说,KL(p[g]||p_data) ≠ KL(p_data||p[g])。模型通常使用KL(p[g]||p_data)来获得更好的结果。

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

手机屏幕截图 由计算机自动生成的描述

图 6.7:生成器成本函数⁶

基本 GAN

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

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

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

图 6.8:Vanilla GAN 架构

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

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 的输出形状。请注意,我们将利用批次归一化来稳定模型训练。以下代码片段显示了构建生成器模型的实用方法:

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 对象:

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 之间:

# Load MNIST train samples
(X_train, _), (_, _) = datasets.mnist.load_data()
# Rescale to [-1, 1]
  X_train = X_train / 127.5 – 1 

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

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帮助我们计算生成器损失并改善它,如下面的代码片段所示:

# 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 构建生成器模型:

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 数字。

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:
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:
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:
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]之间的差异,即θ = argmin D[KL](p_data(x)||p[g](z))。除了是非对称的,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 损失。我们通过取评论家评分和真实标签的平均值来计算它。这在下面的片段中显示:

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类进行子类化,并将其作为所有层的附加参数使用,或者在训练循环期间遍历层。虽然第一种方法更清晰,但我们将使用第二种方法,因为更容易理解。

# 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) 来初始化权重,然后在运行时明确进行了缩放。缩放是以w_hat[i] = w[i] / c的形式进行的,其中 c 是来自 He's 初始化器的每层归一化常数。他们还指出了动量优化器(如 Adam 和 RMSProp)存在的问题,这些问题通过这种等化的学习率方法得以缓解。

逐像素归一化

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

其中eps = 10⁻⁸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 的是:

import tensorflow_hub as hub 

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

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

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

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

第七章:使用 GAN 进行风格转移

神经网络在涉及分析和语言技能的各种任务中正在取得进步。创造力是人类一直占有优势的领域,艺术不仅是主观的,没有明确定义的边界,而且很难量化。然而,这并没有阻止研究人员探索算法的创造能力。多年来有过几次成功试图创建、理解甚至模仿艺术或艺术风格的尝试,例如深梦¹神经风格转移²。

生成模型非常适合于想象和创作相关的任务。特别是,生成对抗网络(GAN)在多年来的风格转移任务中得到了深入研究和探索。一个典型的例子是在图 7.1中展示的,CycleGAN 架构成功地使用莫奈和梵高等著名艺术家的风格将照片转换成绘画。

图 7.1:基于 CycleGAN³使用四位著名画家艺术风格的风格转移

图 7.1给我们展示了风格转移的视觉效果。样本表明 CycleGAN 模型能够保留输入图像的细节和结构,同时以模仿著名画家作品的方式进行转换。换句话说,风格转移是一种技术,它能够改变输入图像,使其采用另一个/参考图像的视觉风格。

在本章中,我们将介绍使用不同 GAN 架构进行风格转移的方法。我们将重点关注以下方面:

  • 图像对图像配对风格转移技术

  • 图像对图像无配对风格转移技术

  • 相关工作

我们将涵盖不同 GAN 架构的内部工作原理和使风格转移设置得以实现的重要贡献。我们还将从头开始构建和训练这些架构,以更好地理解它们的工作原理。

本章中介绍的所有代码片段都可以直接在 Google Colab 中运行。出于篇幅的考虑,未包含依赖项的导入语句,但读者可以参考 GitHub 存储库获取完整的代码:github.com/PacktPublishing/Hands-On-Generative-AI-with-Python-and-TensorFlow-2

让我们开始看配对风格转移吧。

使用 pix2pix GAN 进行配对风格转移

第六章《使用 GAN 生成图像》中,我们讨论了与 GAN 架构相关的多项创新,这些创新导致了改进的结果和更好的输出类别控制。其中之一是条件 GAN。将这个简单但强大的添加到 GAN 设置中,使我们能够浏览潜在的向量空间,并控制生成器生成特定的输出。我们尝试了一个简单的 MNIST 条件 GAN,在那里我们能够生成我们选择的输出。

在本节中,我们将在风格迁移的背景下介绍条件 GAN 的一种变体。我们将详细讨论 pix2pix 架构的重要组件,并训练我们自己的成对风格迁移网络。我们将结束本节,介绍一些这种能力的惊人而创新的用例。

风格迁移是一个引人入胜的研究领域,将创造力和深度学习推向极限。在他们的作品《具有条件对抗网络的图像到图像翻译》中,Isola 和 Zhu 等人提出了一个有条件的 GAN 网络,能够学习任务特定的损失函数,因此可以跨数据集工作。正如其名称所示,这个 GAN 架构接受特定类型的图像作为输入,并将其转换为不同的域。它被称为成对的风格迁移,因为训练集需要同时具有来自源域和目标域的匹配样本。这种通用方法已被证明能够有效地从标签映射和边缘映射中合成高质量的图像,甚至着色图像。作者强调了开发一种能够理解手头数据集并学习映射函数的架构的重要性,而无需手工工程(通常情况下是这样)。

本文在条件 GAN 架构的基础上提出了一些贡献。其中一些贡献也被其他作品采用,并在其作品中引用了必要的参考文献。我们鼓励读者仔细阅读这些内容以深入理解。现在我们将详细介绍 pix2pix GAN 设置的每个组件。

U-Net 生成器

深度卷积生成器是作为 DC-GAN 设置的一部分在《第六章,使用 GAN 生成图像》中探索的。由于 CNN 针对计算机视觉任务进行了优化,因此将它们用于生成器和鉴别器架构具有许多优势。本文专注于生成器设置的两种相关架构。两种选择是基本的编码器-解码器架构和带有跳跃连接的编码器-解码器架构。具有跳跃连接的架构与 U-Net 模型比基本的编码器-解码器设置更相似。因此,pix2pix GAN 中的生成器被称为 U-Net 生成器。参见图 7.2作为参考:

包含时钟的图片 自动生成的描述

图 7.2:(左)编码器-解码器生成器。 (右)带有跳跃连接的编码器-解码器,或者是 U-Net 生成器。

典型的编码器(在编码-解码设置中)接受输入并通过一系列下采样层生成一个紧凑的向量形式。这个紧凑的向量被称为瓶颈特征。解码器部分然后将瓶颈特征上采样到生成最终输出。这种设置在许多场景中非常有用,比如语言翻译和图像重建。瓶颈特征将整体输入压缩到较低维度的空间。

理论上,瓶颈特征捕获所有所需信息,但当输入空间足够大时,实际上变得困难。

此外,对于我们的图像到图像翻译任务,存在许多需要在输入和输出图像之间保持一致的重要特征。例如,如果我们正在训练我们的 GAN 从轮廓地图生成航拍照片,与道路、水体和其他低级信息相关的信息需要在输入和输出之间保持一致,如图 7.3所示:

包含文字的图片  自动生成的描述

图 7.3:U-Net 架构使生成器能够确保输入和生成的输出之间的特征一致

U-Net 架构使用跳跃连接在输入和输出之间传递重要特征(见图 7.2i图 7.3)。在 pix2pix GAN 的情况下,跳跃连接被添加在每个第 i 个下采样层和(n - i)个上采样层之间,其中 n 是生成器中的总层数。跳跃连接导致从第 i 层到(n - i)层的所有通道被串联在一起,i 层被追加到(n - i)层:

手机屏幕截图  自动生成的描述

图 7.4:U-Net 生成器的编码器和解码器块

文中提到的生成器针对编码器和解码器部分采用了重复的块结构。每个编码器块由卷积层、后跟批量归一化层、辍学层和泄漏的 ReLU 激活组成。每个这样的块通过步幅为 2 下采样 2 倍。

解码器块使用转置卷积层,后跟批量归一化和泄漏的 ReLU 激活。每个块上采样 2 倍。编码器和解码器块的简化设置如图 7.4所示。 如前所述,每个块之间也使用跳跃连接。掌握了生成器的这些知识后,让我们进入实现细节。

首先,让我们为 U-Net 生成器的下采样和上采样块准备一些实用的方法。下采样块使用由卷积层组成的堆栈,后跟泄漏的 ReLU 激活,最后是可选的批量归一化层。

下面的downsample_block辅助函数接受所需的滤波器数量、内核大小以及是否需要批归一化的输入参数:

def downsample_block(incoming_layer,
                     num_filters,
                     kernel_size=4,
                     batch_normalization=True):
    downsample_layer = Conv2D(num_filters,
                              kernel_size=kernel_size,
                              strides=2, padding='same')(incoming_layer)
    downsample_layer = LeakyReLU(alpha=0.2)(downsample_layer)
    if batch_normalization:
        downsample_layer = BatchNormalization(momentum=0.8)(downsample_layer)
    return downsample_layer 

下一个辅助函数是上采样块。每个上采样块都是由一个上采样层、一个二维卷积层、一个可选的丢失层和最后一个批归一化层组成的堆栈。

upsample_block function:
def upsample_block(incoming_layer,
                   skip_input_layer,
                   num_filters,
                   kernel_size=4,
                   dropout_rate=0):
    upsample_layer = UpSampling2D(size=2)(incoming_layer)
    upsample_layer = Conv2D(num_filters,
                            kernel_size=kernel_size,
                            strides=1,
                            padding='same',
                            activation='relu')(upsample_layer)
    if dropout_rate:
        upsample_layer = Dropout(dropout_rate)(upsample_layer)
    upsample_layer = BatchNormalization(momentum=0.8)(upsample_layer)
    upsample_layer = Concatenate()([upsample_layer, skip_input_layer])
    return upsample_layer 
downsample_block(). We stack seven such blocks with an increasing number of filters.

最后一部分的关键是准备解码器。为此,我们使用upsample_block()函数堆叠了七个解码器块,并从编码器层获取跳跃连接。以下代码片段实现了这一点:

def build_generator(img_shape,channels=3,num_filters=64):
    # Image input
    input_layer = Input(shape=img_shape)
    # Downsampling
    down_sample_1 = downsample_block(input_layer, 
                                     num_filters, 
                                     batch_normalization=False)
    # rest of the downsampling blocks have batch_normalization=true
    down_sample_2 = downsample_block(down_sample_1, num_filters*2)
    down_sample_3 = downsample_block(down_sample_2, num_filters*4)
    down_sample_4 = downsample_block(down_sample_3, num_filters*8)
    down_sample_5 = downsample_block(down_sample_4, num_filters*8)
    down_sample_6 = downsample_block(down_sample_5, num_filters*8)
    down_sample_7 = downsample_block(down_sample_6, num_filters*8)
    # Upsampling blocks with skip connections
    upsample_1 = upsample_block(down_sample_7, down_sample_6, 
                                               num_filters*8)
    upsample_2 = upsample_block(upsample_1, down_sample_5, 
                                            num_filters*8)
    upsample_3 = upsample_block(upsample_2, down_sample_4, 
                                            num_filters*8)
    upsample_4 = upsample_block(upsample_3, down_sample_3, 
                                            num_filters*8)
    upsample_5 = upsample_block(upsample_4, down_sample_2, 
                                            num_filters*2)
    upsample_6 = upsample_block(upsample_5, down_sample_1, num_filters)
    upsample_7 = UpSampling2D(size=2)(upsample_6)
    output_img = Conv2D(channels, 
                        kernel_size=4, 
                        strides=1, 
                        padding='same', 
                        activation='tanh')(upsample_7)
    return Model(input_layer, output_img) 

这展示了我们可以利用构建模块轻松形成复杂架构,如 U-Net 生成器。现在让我们了解与 pix2pix 的鉴别器相关的详细信息。

补丁-GAN 鉴别器

典型的鉴别器通过获取输入图像并将其分类为假或真来工作,即生成单个输出标量。在有条件的鉴别器的情况下,有两个输入,第一个是条件输入,第二个是用于分类的生成样本(来自生成器)。对于我们的图像到图像转换用例,鉴别器提供了源图像(条件输入)以及生成的样本,并且其目的是预测生成的样本是否是源的合理转换。

pix2pix 的作者提出了一个 Patch-GAN 设置用于鉴别器,它接受两个所需输入并生成大小为N x N的输出。图 7.5以简化的方式说明了 Patch-GAN 的概念。

N x `N` input:

展示的图片,照片,不同,屏幕自动生成的描述

图 7.5: 简化的图示,以了解 Patch-GAN 鉴别器的工作原理

文章中提出的配置使用了三个 Patch-GAN 层,使用了 4 x 4 的内核大小和 2 的步幅。最后两层使用了 4 x 4 的内核大小和 1 的步幅。这导致了一个 70 x 70 的 Patch-GAN 设置,也就是说,N x N输出矩阵中的每个输出像素/单元/元素对应于输入图像的 70 x 70 补丁。每个这样的 70 x 70 补丁具有高度的重叠,因为输入图像的大小为 256 x 256。为了更好地理解这一点,让我们通过以下代码片段计算有效感受野的计算:

def get_receptive_field(output_size, ksize, stride):
    return (output_size - 1) * stride + ksize
last_layer = get_receptive_field(output_size=1, ksize=4, stride=1)
# Receptive field: 4
fourth_layer = get_receptive_field(output_size=last_layer, ksize=4, stride=1)
# Receptive field: 7
third_layer = get_receptive_field(output_size=fourth_layer, ksize=4, stride=2)
# Receptive field: 16
second_layer = get_receptive_field(output_size=third_layer, ksize=4, stride=2)
# Receptive field: 34
first_layer = get_receptive_field(output_size=second_layer, ksize=4, stride=2)
# Receptive field: 70
print(first_layer) 

代码片段显示了理解每个输出像素如何对应于初始输入图像中大小为 70 x 70 的补丁的计算。

直观理解这一点的方式是假设模型准备了输入图像的多个重叠补丁,并试图将每个补丁分类为假或真,然后对其进行平均以准备整体结果。这已被证明能够提高生成图像的整体输出质量。

作者尝试了不同的补丁大小,范围从 1 x 1(Pixel-GAN)到 256 x 256(Image-GAN),但他们报告 70 x 70 配置(Patch-GAN)取得了最佳结果,而且在这之后几乎没有改进。

直观上,我们或许可以推断:在样式转移中,目标是从源图像复制局部特征到目标图像,因此补丁大小需要最好地为此目标服务;像素级补丁大小太窄,失去了更大特征的视野,而图像级补丁大小对图像内部的局部变化不敏感。

现在让我们使用 TensorFlow 2 来准备我们的 Patch-GAN 鉴别器。第一步是准备一个用于定义鉴别器模块的实用程序,包括卷积层、泄漏 ReLU 和可选的批量标准化层:

def discriminator_block(incoming_layer,
                        num_filters,
                        kernel_size = 4,
                        batch_normalization=True):

    disc_layer = Conv2D(num_filters,
                        kernel_size = kernel_size,
                        strides=2,
                        padding='same')(incoming_layer)
    disc_layer = LeakyReLU(alpha = 0.2)(disc_layer)
    if batch_normalization:
        disc_layer = BatchNormalization(momentum = 0.8)(disc_layer)
    return disc_layer 

我们将使用这些模块来准备 Patch-GAN 鉴别器,步骤如下。下面的片段准备了一个鉴别器模型,它接受两个输入(生成器的输出和调节图像),然后是四个鉴别器模块,带有越来越多的滤波器:

def build_discriminator(img_shape,num_filters=64):
    input_img = Input(shape=img_shape)
    cond_img = Input(shape=img_shape)
    # Concatenate input and conditioning image by channels 
    # as input for discriminator
    combined_input = Concatenate(axis=-1)([input_img, cond_img])
    # First discriminator block does not use batch_normalization
    disc_block_1 = discriminator_block(combined_input, 
                                       num_filters, 
                                       batch_normalization=False)
    disc_block_2 = discriminator_block(disc_block_1, num_filters*2)
    disc_block_3 = discriminator_block(disc_block_2, num_filters*4)
    disc_block_4 = discriminator_block(disc_block_3, num_filters*8)
    output = Conv2D(1, kernel_size=4, strides=1, padding='same')(disc_block_4)
    return Model([input_img, cond_img], output) 

与生成器类似,我们现在有一个构建所需的 Patch-GAN 鉴别器的函数。下一步是了解用于训练整体设置的目标函数。

损失

第六章《基于 GAN 的图像生成》中,我们详细讨论了条件 GAN,介绍了总体条件 GAN 目标函数。在这里再次提到:

作者观察到,利用 L1 和 L2 正则化方法改进输出质量的典型方式仅通过捕捉低频率的局部结构,即对生成图像整体清晰度的贡献。与 L2 正则化相比,L1 正则化有助于防止模糊。因此,我们可以将 L1 正则化公式化为:

其中x是源图像,y是条件输入,z是噪音向量。将 U-Net 设置与 L1 正则化相结合可以生成清晰的输出图像,其中 GAN 处理高频率,而 L1 协助低频率。更新后的目标函数可以表述为:

与原始 GAN 论文中建议的改进类似,pix2pix 也最大化log(D(G(z|y))),而不是最小化log(1 – D(G(z|y)))。这会导致梯度曲线反馈更好(参见第六章《基于 GAN 的图像生成》的训练 GANs部分)。

训练 pix2pix

我们现在已经准备好所有必需的组件。拼图的最后一块就是将生成器和鉴别器组合成一个训练循环,为准备好的 pix2pix GAN 网络做准备。

我们还将相关的损失函数附加到每个组件网络上:

def train(generator, 
          discriminator, 
          gan, 
          patch_gan_shape, 
          epochs,
          path='/content/maps',
          batch_size = 1, 
          sample_interval = 50):

    # Ground truth shape/Patch-GAN outputs
    real_y = np.ones((batch_size,) + patch_gan_shape)
    fake_y = np.zeros((batch_size,) + patch_gan_shape)
    for epoch in range(epochs):
      print("Epoch={}".format(epoch))
      for idx, (imgs_source, imgs_cond) in enumerate(batch_generator(path=path, batch_size=batch_size,
                     img_res=[IMG_HEIGHT, IMG_WIDTH])):
            # train discriminator
            # generator generates outputs based on 
            # conditioned input images
            fake_imgs = generator.predict([imgs_cond])
            # calculate discriminator loss on real samples
            disc_loss_real = discriminator.train_on_batch([imgs_source,
                                                           imgs_cond], 
                                                           real_y)
            # calculate discriminator loss on fake samples
            disc_loss_fake = discriminator.train_on_batch([fake_imgs, 
                                                           imgs_cond], 
                                                           fake_y)
            # overall discriminator loss
            discriminator_loss = 0.5 * np.add(disc_loss_real, 
                                              disc_loss_fake)
            # train generator
            gen_loss = gan.train_on_batch([imgs_source, imgs_cond],
                                          [real_y, imgs_source])
            # training updates every 50 iterations
            if idx % 50 == 0:
              print ("[Epoch {}/{}] [Discriminator loss: {}, accuracy: {}] [Generator loss: {}]".format(epoch, epochs,                                         discriminator_loss[0],                                         100*discriminator_loss[1],
                                        gen_loss[0]))
            # Plot and Save progress every few iterations
            if idx % sample_interval == 0:
              plot_sample_images(generator=generator,
                                 path=path,
                                 epoch=epoch,
                                 batch_num=idx,
                                 output_dir='images') 

上述函数接受生成器、鉴别器和组合 pix2pix GAN 模型对象作为输入。根据 Patch-GAN 鉴别器的大小,我们定义用于保存假和真输出预测的 NumPy 数组。

与我们在前一章中训练 GAN 的方式类似,我们通过首先使用生成器生成假样本,然后使用这个样本来获取鉴别器输出来循环多次迭代。最后,这些输出用于计算损失并更新相应的模型权重。

现在我们有了训练循环,以下片段准备了鉴别器和 GAN 网络:

IMG_WIDTH = 256
IMG_HEIGHT = 256
# build discriminator
discriminator = build_discriminator(img_shape=(IMG_HEIGHT,IMG_WIDTH,3),
                                    num_filters=64)
discriminator.compile(loss='mse',
                      optimizer=Adam(0.0002, 0.5),
                      metrics=['accuracy'])
# build generator and GAN objects
generator = build_generator(img_shape=(IMG_HEIGHT,IMG_WIDTH,3),
                            channels=3,
                            num_filters=64)
source_img = Input(shape=(IMG_HEIGHT,IMG_WIDTH,3))
cond_img = Input(shape=(IMG_HEIGHT,IMG_WIDTH,3))
fake_img = generator(cond_img)
discriminator.trainable = False
output = discriminator([fake_img, cond_img])
gan = Model(inputs=[source_img, cond_img], outputs=[output, fake_img])
gan.compile(loss=['mse', 'mae'],
            loss_weights=[1, 100],
            optimizer=Adam(0.0002, 0.5)) 

训练循环简单且类似于我们在前一章中使用的内容:每个时代,我们在训练鉴别器和生成器之间交替。使用的超参数如 pix2pix 论文中所述。在训练的不同阶段模型的输出在图 7.6中展示:

图 7.6:不同训练阶段的 pix2pix 生成输出

与我们在第六章中训练的更简单的架构不同,尽管更复杂,但 pix2pix GAN 的训练速度更快,而且在更少的迭代中稳定到更好的结果。在图 7.6中展示的输出显示了模型学习映射并从第一个时代开始生成高质量输出的能力。所有这些都可以归因于前几节讨论的一些创新。

现在我们已经看到了如何为配对风格转换设置和训练 pix2pix GAN,让我们看看它可以用于哪些事情。

使用案例

图像到图像翻译设置在现实世界中开启了许多用例和应用。pix2pix 设置提供了一个通用框架,可应用于许多图像到图像翻译用例,而无需专门设计架构或损失函数。在他们的工作中,Isola 和 Zhu 等人展示了许多有趣的研究来展示这些功能。

这个 pix2pix GAN 的有条件 GAN 设置能够执行诸如:

  • 从标签输入生成建筑立面

  • 黑白图像的彩色化

  • 将卫星/航拍地图输入图像转换为类似 Google 地图的输出

  • 语义分割任务,如街景到分割标签

  • 将草图转换为图像任务,如草图转照片,草图转肖像,草图转猫,草图转彩色宝可梦,甚至轮廓转时尚物品,如鞋子、包等

  • 背景去除

  • 修补或图像完成

  • 热图到 RGB 图像的转换

  • 白天到夜晚场景和夏季到冬季场景转换

论文中执行的一些翻译类型在图 7.7中供参考:

包含照片的图片,自动生成不同的描述

图 7.7:使用 pix2pix 进行不同图像到图像翻译任务的几个示例

如前所述,pix2pix 架构经过高度优化,即使在非常小的数据集上也能训练。这使社区和其他研究人员尝试了更多创造性的用例;作者为论文开发了一个展示此类用例的网站。我们鼓励读者访问网站了解更多细节:phillipi.github.io/pix2pix/

讨论了配对样式转移后,接下来我们将看看不配对样式转移。

使用 CycleGAN 进行不配对样式转移

配对样式转移是一个强大的设置,有许多用例,我们在前一节中讨论了其中一些。它提供了在给定一对源和目标领域数据集的情况下执行跨域转换的能力。pix2pix 设置还展示了 GAN 理解和学习所需的损失函数的能力,无需手动指定。

虽然相对于手工制作的损失函数和以往的作品而言,配对样式转移是一个巨大的改进,但它受到配对数据集的限制。配对样式转移要求输入和输出图像在结构上完全相同,即使领域不同(从航空到地图,从标签到场景等)。在本节中,我们将重点关注一种名为 CycleGAN 的改进样式转移架构。

CycleGAN 通过放宽输入和输出图像的约束来改进配对样式转移架构。CycleGAN 探索了模型尝试学习源和目标域之间风格差异的不配对样式转移范式,而不需要明确地对输入和输出图像进行配对。

朱和朴等人将这种不配对样式转移描述为类似于我们能够想象梵高或莫奈会如何画一个特定场景,而没有确实看到并列示例。引用自该论文³:

相反,我们了解到了莫奈绘画集和风景照片集。我们可以推理出这两组之间的风格差异,因此可以想象如果我们将一个场景从一组转换到另一组会是什么样子。

这不仅提供了一个很好的优势,也开拓了额外的用例,其中源和目标领域的精确配对要么不可用,要么我们没有足够的训练示例。

CycleGAN 的整体设置

在配对样式转移的情况下,训练数据集由配对样本组成,表示为{xᵢyᵢ},其中xᵢyᵢ之间存在对应关系。如参考图 7.8(a)所示:

图 7.8:(a)配对训练示例。(b)不配对训练示例。(来源:朱和朴等人《使用循环一致对抗网络进行不配对图像到图像转换》,图 2)

对于 CycleGAN,训练数据集包括来自源集的不成对样本,表示为{x[i]}, i = 1 -> N},以及目标集{y[j]}, j = 1 -> M,没有关于哪个xᵢ匹配哪个yⱼ的具体信息。 参考图 7.8 (b)

在前一章中,我们讨论了 GAN 是如何学习一个映射G: X -> Y,使得输出y' = G(X)y ∈ Y无法区分。 尽管这对其他场景有效,但对于无配对的图像到图像转换任务却不太好。 由于缺乏配对样本,我们无法像以前那样使用 L1 损失来学习G,因此我们需要为无配对风格转换制定不同的损失。 一般来说,当我们学习函数G(x)时,这是学习Y的众多可能性之一。 换句话说,对于给定的XY,存在无限多个G会在y'上具有相同的分布。

为了减少搜索空间并在寻找最佳生成器G时增加更多约束,作者引入了一种称为循环一致性的特性。 从数学上讲,假设我们有两个生成器GF,使得G: X -> YF: Y -> X。 在最佳设置中,GF将相互逆转并且应该是双射,即一对一。 对于 CycleGAN,作者同时训练两个生成器GF,以促进对抗损失以及循环一致性约束以鼓励F(G(x)) ≈ xG(F(y)) ≈ y。 这导致成功训练无配对风格转换 GAN 设置。

请注意,与生成器类似,我们在这个设置中也有两组判别器,D[Y]用于GD[X]用于F。 这个设置背后的直觉是,只有在能够以相反顺序执行相同的操作时,我们才能从源域到目标域学习出最佳的翻译。 图 7.9生动地演示了循环一致性的概念:

包含时钟的图片,自动生成的描述

图 7.9:CycleGAN³的高级示意图

图 7.9的第一部分(最左侧)描述了 CycleGAN 的设置。 这个设置显示了两对生成器和判别器,GD[Y]FD[X]

图 7.9的中间部分显示了 CycleGAN 的前向循环训练。 输入x通过G转换为Y_hat,然后F试图再生原始输入为x_hat。 这一步更新GD[Y]。循环一致性损失有助于减少x和其再生形式x_hat之间的距离。

同样,图 7.9的第三部分(最右边)展示了向后传递,其中y被转换为X,然后G尝试再生原始输入,如y_hat

为了更好地理解无配对训练设置是如何工作的,让我们通过一个通用示例来走一遍。假设任务是从英语到法语的翻译。一个模型已经学会了从英语到法语的最佳映射的设置将是当它被反转(即,法语到英语)时产生原始句子的结果。

现在让我们深入了解并详细了解接下来的小节中的每个组件。

对抗损失

典型的 GAN 使用对抗损失来训练一个足够聪明的生成器以欺骗鉴别器。在 CycleGAN 的情况下,由于我们有两组生成器和鉴别器,我们需要对对抗损失进行一些调整。让我们一步一步来。

对于我们的 CycleGAN 中的第一组生成器-鉴别器,即G: X -> Y,对抗损失可以定义为:

同样,第二个生成器-鉴别器F: Y -> X集合给出为:

这两个目标一起形成了 CycleGAN 的总体目标的前两项。对于生成器-鉴别器的两组,还有一个额外的变化是最小化部分。选择不是使用标准的负对数似然,而是选择最小二乘损失。表示为:

最小二乘损失被观察到更稳定,并导致更好质量的输出样本。

循环损失

我们之前介绍了循环一致性的概念;现在我们将看到如何明确实现它。在 CycleGAN 的论文中,作者朱和朴等人强调,对抗损失对于无配对图像到图像的翻译任务是不够的。不仅搜索空间太广,而且有足够的容量,生成器可以陷入模式坍塌而不学习源域和目标域的实际特征。

为了减少搜索空间并确保学习到的映射足够好,CycleGAN 设置应该能够生成原始输入x经过GF处理后,即x -> G(x) -> F(G(x)) ≈ x以及y -> F(y) -> G(F(y)) ≈ y的反向路径。这些分别称为前向和后向循环一致性。总循环一致性损失是定义为 L1 损失:

此损失确保从生成的输出中重建原始输入尽可能接近。

身份损失

CycleGAN 的作者们还观察到了关于彩色对象的整体设置的特定问题。在没有特定颜色约束的情况下,GF 生成器在前向和后向循环中会引入不同的色调,而这并不是必要的。为了减少这种不必要的行为,引入了一项称为身份损失的正则化项。查看 Figure 7.10 展示了这种特定效果的实际情况:

A picture containing photo, different, showing, show  Description automatically generated

Figure 7.10 身份损失对 CycleGAN 性能的影响。输出对应于生成器 G(x) 的输出。(来源:Zhu 和 Park 等人。使用循环一致对抗网络进行非配对图像到图像的翻译,图 9)

正如在 Figure 7.10 中间列所显示的那样,没有身份损失的附加约束,CycleGAN 在其输出中引入了不必要的色调。因此,身份损失,定义为 L_identity,可以表述为:

简单来说,这个损失将生成器正则化为当来自目标域的真实样本被用作生成的输入时的近似身份映射。

整体损失

CycleGAN 的整体目标简单地是前面子节中讨论的不同损失的加权和,即对抗损失、循环一致性损失和身份损失。整体目标定义为:

论文强调了不同实验中λη的不同值。当我们从头开始准备我们的模型时,我们将明确说明这些正则化项的值。

实践操作:使用 CycleGAN 进行非配对式风格转换

我们讨论了 CycleGAN 的整体设置及其关键创新,即循环一致性损失和身份损失,这些使得非配对式风格转换成为可能。在本节中,我们将逐部分实施它,并训练一对 CycleGANs 将苹果转换为橙子,并将照片转换为梵高的绘画作品。

生成器设置

让我们从生成器开始。与 pix2pix GAN 类似,CycleGAN 也使用 U-Net 生成器(注意,此设置中有两个生成器)。

第一步是准备上采样和下采样块的实用方法。这里的一个重要区别是使用 实例归一化 替代批量归一化层。实例归一化的工作方式是对每个训练样本中的每个通道进行归一化。这与批量归一化相反,批量归一化是在整个小批量和所有输入特征上进行归一化的。有关实例归一化的更多详细信息,请参阅 第六章使用 GAN 生成图像

downsample_block() prepares a stack composed of a convolutional layer followed by leaky ReLU activation and an instance normalization layer. The function takes the number of filters and kernel size as inputs:
def downsample_block(incoming_layer,
                     num_filters,
                     kernel_size=4):
    downsample_layer = Conv2D(num_filters,
                              kernel_size=kernel_size,
                              strides=2, padding='same')(incoming_layer)
    downsample_layer = LeakyReLU(alpha=0.2)(downsample_layer)
    downsample_layer = InstanceNormalization()(downsample_layer)
    return downsample_layer 
upsample_block() function. This function prepares a stack consisting of an upsampling layer followed by a convolutional layer, optional dropout, and instance normalization layer. Each upsampling block takes input from the previous layer as well as a skip connection as input:
def upsample_block(incoming_layer,
                   skip_input_layer,
                   num_filters,
                   kernel_size=4,
                   dropout_rate=0):

    upsample_layer = UpSampling2D(size=2)(incoming_layer)
    upsample_layer = Conv2D(num_filters,
                            kernel_size=kernel_size,
                            strides=1,
                            padding='same',
                            activation='relu')(upsample_layer)
    if dropout_rate:
        upsample_layer = Dropout(dropout_rate)(upsample_layer)
    upsample_layer = InstanceNormalization()(upsample_layer)
    upsample_layer = Concatenate()([upsample_layer, skip_input_layer])
    return upsample_layer 

这里使用的 U-Net 生成器与 pix2pix 设置相比要较浅,但表现同样出色(请参阅 Cycle loss 部分)。以下代码片段演示了构建生成器的方法:

def build_generator(img_shape, channels=3, num_filters=32):
    # Image input
    input_layer = Input(shape=img_shape)
    # Downsampling
    down_sample_1 = downsample_block(input_layer, num_filters)
    down_sample_2 = downsample_block(down_sample_1, num_filters*2)
    down_sample_3 = downsample_block(down_sample_2,num_filters*4)
    down_sample_4 = downsample_block(down_sample_3,num_filters*8)
    # Upsampling
    upsample_1 = upsample_block(down_sample_4, down_sample_3, 
                                               num_filters*4)
    upsample_2 = upsample_block(upsample_1, down_sample_2, 
                                            num_filters*2)
    upsample_3 = upsample_block(upsample_2, down_sample_1, num_filters)
    upsample_4 = UpSampling2D(size=2)(upsample_3)
    output_img = Conv2D(channels, 
                        kernel_size=4, 
                        strides=1, 
                        padding='same', 
                        activation='tanh')(upsample_4)
    return Model(input_layer, output_img) 

如我们所见,生成器由四个下采样和四个上采样块组成,然后是一个输出目标图像的 Conv2D 层。现在让我们构建辨别器。

辨别器设置

与生成器一样,CycleGAN 中使用的辨别器也利用了来自 pix2pix 论文的内容。辨别器是 Patch-GAN,并且以下代码清单展示了构建辨别器块的方法以及构建辨别器的方法:

def discriminator_block(incoming_layer,
                        num_filters,
                        kernel_size=4,
                        instance_normalization=True):

    disc_layer = Conv2D(num_filters,
                        kernel_size=kernel_size,
                        strides=2,
                        padding='same')(incoming_layer)
    disc_layer = LeakyReLU(alpha=0.2)(disc_layer)
    if instance_normalization:
        disc_layer = InstanceNormalization()(disc_layer)
    return disc_layer
def build_discriminator(img_shape,num_filters=64):
    input_layer = Input(shape=img_shape)
    disc_block_1 = discriminator_block(input_layer, 
                                       num_filters, 
                                       instance_normalization=False)
    disc_block_2 = discriminator_block(disc_block_1, num_filters*2)
    disc_block_3 = discriminator_block(disc_block_2, num_filters*4)
    disc_block_4 = discriminator_block(disc_block_3, num_filters*8)
    output = Conv2D(1, kernel_size=4, strides=1, padding='same')(disc_block_4)
    return Model(input_layer, output) 

现在我们已经准备好了构建模块。让我们使用它们来建立整体的 CycleGAN 架构。

GAN 设置

我们使用这些方法准备了两套生成器和辨别器,用于从域 A 映射到 B,然后再从 B 回映射到 A。以下代码片段正是如此:

generator_filters = 32
discriminator_filters = 64
# input shape
channels = 3
input_shape = (IMG_HEIGHT, IMG_WIDTH, channels)
# Loss weights
lambda_cycle = 10.0            
lambda_identity = 0.1 * lambda_cycle
optimizer = Adam(0.0002, 0.5)
patch = int(IMG_HEIGHT / 2**4)
patch_gan_shape = (patch, patch, 1)
# Discriminators
disc_A = build_discriminator(input_shape,discriminator_filters)
disc_A.compile(loss='mse',
    optimizer=optimizer,
    metrics=['accuracy'])
disc_B = build_discriminator(input_shape,discriminator_filters)
disc_B.compile(loss='mse',
    optimizer=optimizer,
    metrics=['accuracy'])
# Generators
gen_AB = build_generator(input_shape,channels, generator_filters)
gen_BA = build_generator(input_shape, channels, generator_filters)
# CycleGAN
img_A = Input(shape=input_shape)
img_B = Input(shape=input_shape)
# generate fake samples from both generators
fake_B = gen_AB(img_A)
fake_A = gen_BA(img_B)
# reconstruct original samples from both generators
reconstruct_A = gen_BA(fake_B)
reconstruct_B = gen_AB(fake_A)
# generate identity samples
identity_A = gen_BA(img_A)
identity_B = gen_AB(img_B)
# disable discriminator training
disc_A.trainable = False
disc_B.trainable = False
# use discriminator to classify real vs fake
output_A = disc_A(fake_A)
output_B = disc_B(fake_B)
# Combined model trains generators to fool discriminators
gan = Model(inputs=[img_A, img_B],
            outputs=[output_A, output_B,
                     reconstruct_A, reconstruct_B,
                     identity_A, identity_B ])
gan.compile(loss=['mse', 'mse','mae', 'mae','mae', 'mae'],
            loss_weights=[1, 1,
                          lambda_cycle, lambda_cycle,
                          lambda_identity, lambda_identity ],
            optimizer=optimizer) 

我们刚刚为两对生成器和辨别器创建了对象。通过定义必须的输入和输出,我们将它们组合在 gan 对象中。接下来实现训练循环。

训练循环

最后完整的画面是编写自定义训练循环。该循环首先使用两个生成器生成假样本,然后用它们更新两个方向的辨别器(即,ABBA)。最后使用更新后的辨别器来训练整体的 CycleGAN。以下代码片段展示了训练循环:

def train(gen_AB, 
          gen_BA, 
          disc_A, 
          disc_B, 
          gan, 
          patch_gan_shape, 
          epochs, 
          path='/content/{}'.format(dataset_name),
          batch_size=1, 
          sample_interval=50):
    # Adversarial loss ground truths
    real_y = np.ones((batch_size,) + patch_gan_shape)
    fake_y = np.zeros((batch_size,) + patch_gan_shape)
    for epoch in range(epochs):
        print("Epoch={}".format(epoch))
        for idx, (imgs_A, imgs_B) in enumerate(batch_generator(path,
                                                         batch_size,
                                image_res=[IMG_HEIGHT, IMG_WIDTH])):
            # train discriminators
            # generate fake samples from both generators
            fake_B = gen_AB.predict(imgs_A)
            fake_A = gen_BA.predict(imgs_B)
            # Train the discriminators 
            # (original images = real / translated = Fake)
            disc_A_loss_real = disc_A.train_on_batch(imgs_A, real_y)
            disc_A_loss_fake = disc_A.train_on_batch(fake_A, fake_y)
            disc_A_loss = 0.5 * np.add(disc_A_loss_real, 
                                       disc_A_loss_fake)
            disc_B_loss_real = disc_B.train_on_batch(imgs_B, real_y)
            disc_B_loss_fake = disc_B.train_on_batch(fake_B, fake_y)
            disc_B_loss = 0.5 * np.add(disc_B_loss_real, 
                                       disc_B_loss_fake)
            # Total disciminator loss
            discriminator_loss = 0.5 * np.add(disc_A_loss, disc_B_loss)
            # train generator
            gen_loss = gan.train_on_batch([imgs_A, imgs_B],
                                          [
                                           real_y, real_y,
                                           imgs_A, imgs_B,
                                           imgs_A, imgs_B
                                           ]
                                          )
            # training updates every 50 iterations
            if idx % 50 == 0:
              print ("[Epoch {}/{}] [Discriminator loss: {}, accuracy: {}][Generator loss: {}, Adversarial Loss: {}, Reconstruction Loss: {}, Identity Loss: {}]".format(idx, 
                           epoch,
                           discriminator_loss[0], 
                           100*discriminator_loss[1],
                           gen_loss[0],
                           np.mean(gen_loss[1:3]),
                           np.mean(gen_loss[3:5]),
                           np.mean(gen_loss[5:6])))

            # Plot and Save progress every few iterations
            if idx % sample_interval == 0:
              plot_sample_images(gen_AB,
                                 gen_BA,
                                 path=path,
                                 epoch=epoch,
                                 batch_num=idx,
                                 output_dir='images') 

CycleGAN 的训练循环与 pix2pix 的大致相似,但有一些补充。由于我们有两对生成器和辨别器,该函数将所有四个模型作为输入,以及一个合并的 gan 对象。训练循环从两个生成器生成的假样本开始,然后使用它们来更新相应的辨别器的权重。然后将它们合并以训练整体的 GAN 模型。

使用本节介绍的组件,我们对两组风格转移数据集进行了实验,将苹果转变成橙子并将照片转变为梵高画作。图 7.11 展示了苹果转变成橙子实验在不同训练阶段的输出:

图 7.11:循环生成对抗网络在苹果到橙子实验不同训练阶段生成的输出

类似地,Figure 7.12 展示了 CycleGAN 如何学习将照片转换为梵高风格的艺术作品:

图 7.12:循环生成对抗网络在将照片转变为梵高风格画作实验不同训练阶段生成的输出

正如上述示例所示(图 7.117.12),CycleGAN 似乎已经从两个域中学会了一些细微差别,而没有匹配的训练样本。这在难以获得匹配样本的情况下是一个很好的进展。

从这两个实验中另一个重要的观察是所需的训练量。虽然两个实验都使用了完全相同的设置和超参数,但与将照片转换为梵高风格绘画设置相比,苹果转橘子实验的训练速度要快得多。这可能归因于第二个实验中模式的数量很大,以及训练样本的多样性。

这结束了我们关于无配对风格迁移的部分。现在我们将探讨一些与配对和无配对风格迁移相关并有所分支的工作。

相关工作

风格迁移是一个有趣的领域,不同研究组之间正在进行大量的并行研究,以改进现有技术。迄今为止,在配对和无配对风格迁移领域中,最有影响力的两项工作已在本章中讨论过。在这个空间中还有一些其他相关工作值得讨论。

在本节中,我们将简要讨论无配对图像到图像转换领域中另外两个与 CycleGAN 类似的工作。具体而言,我们将涉及 DiscoGAN 和 DualGAN 设置,因为它们提出了类似的想法,但存在细微的变化。

重要的是要注意,同一领域还有许多其他工作。为了完整性和一致性,我们将我们的讨论限制在其中的几个上。鼓励读者也探索其他有趣的架构。

DiscoGAN

Kim 和 Cha 等人提出了一种使用 GANs 发现跨域关系的模型,称为 DiscoGAN。⁶ 将黑白图像转换为彩色图像、卫星图像转换为地图样式图像等任务也可以称为跨域转换,以及风格迁移。

正如我们已经看到的,跨域转换在现实世界中具有许多应用。诸如自动驾驶和医疗保健等领域已经开始利用深度学习技术,然而许多用例因为缺乏更大的数据集而受阻。无配对跨域转换工作,如 DiscoGAN(和 CycleGAN),可以在这些领域提供巨大帮助。

包含时钟的图片 自动生成的描述

图 7.13:DiscoGAN 设置⁶

与 CycleGAN 几乎同时发布的 DiscoGAN 在执行无配对风格迁移时具有许多相似之处,以及一些轻微的差异。与 CycleGAN 一样,DiscoGAN 利用两对生成器和判别器来实现跨域转换。第一对生成器将图像从域 A 转换为 B(表示为 G[AB]),而判别器(表示为 D[B])则分类生成的输出(表示为 x[A][B])是真实的还是伪造的。

第二个生成器-判别器对是论文的关键。通过强制系统从生成的输出(x[A][B])中再生生成原始输入图像,DiscoGAN 能够学习所需的跨域表示,而无需明确的配对。使用第二个生成器(标记为 G[B][A])及其对应的判别器(D[A])实现了对原始样本的重构。整体设置如图 7.13所示,仅供参考。

图 7.13所示,为了学习跨域表示,DiscoGAN 不仅将图像从域A转换为x[AB],然后重构回来,还对域B中的图像做同样的操作(即,Bx[BA]然后重构回来)。这种双向映射,也称为双射,以及重构损失和对抗损失,有助于实现最先进的结果。作者指出,仅依赖重构损失而没有附加管道(Bx[BA]和重构)的设置仍会导致模式崩溃等故障模式。

与 CycleGAN 不同,我们注意到重构损失是 L1 损失和前向、后向重构的加权和,DiscoGAN 以略微不同的方式探索和使用重构损失。DiscoGAN 论文提到,重构损失可以是任何距离度量,例如均方误差、余弦距离或铰链损失。如下方方程所示,生成器然后单独在训练中使用重构损失:

其中L_GAN[i]代表原始的对抗损失,L_CONST[i]是每个 GAN 对的重构损失。

生成器使用具有卷积和反卷积层(或转置卷积)的编码器-解码器设置来对中间表示/特征图进行下采样和上采样。另一方面,解码器类似于生成器的编码器部分,由卷积层组成,最后一层是用于分类的 sigmoid 函数。

DiscoGAN 的作者提出了许多经过充分记录的实验,以了解他们提出的架构如何处理模式崩溃。其中一项实验是汽车到汽车的映射实验。在这个设置中,作者探索了原始 GAN、带重构损失的 GAN 和 DiscoGAN 这三种架构如何处理各种模式。映射实验将具有特定旋转(方位角)的汽车输入图像转换为不同的旋转角度。

手机屏幕截图 自动生成的描述

图 7.14:汽车到汽车的映射实验,以了解不同设置下的模式崩溃情况。(a) 原始 GAN,(b) 带重构损失的 GAN,(c) DiscoGAN 设置。

图 7.14(a)(b)所示,无论是 GAN 还是带重建损失的 GAN 都遭受模式崩溃;在这两种情况下,聚类的点表示这些架构只能学习少数模式,或者是汽车方向(线是真实情况)。相反,图 7.14(c)显示 DiscoGAN 学习了各种模式的更好表示,点分布在线附近。

设置 DiscoGAN 相当简单。利用本章介绍的工具,我们训练了一个 DiscoGAN 设置,学习边缘到鞋子的映射。

图形用户界面 自动创建的说明

图 7.15:DiscoGAN 在边缘到鞋子实验中的训练过程

图 7.15显示了我们模型的训练进展。从随机映射开始,以了解边界形状和一些颜色。训练更长时间可以获得更好的结果,作者在他们的论文中展示了这一点。

DualGAN

DualGAN 是非配对图像到图像转换架构家族中的最新成员。与 DiscoGAN 和 CycleGAN 相比,它以略微不同的角度处理这个任务。作者 Yi 和 Zhang 等人发布了名为DualGAN: 图像到图像转换的无监督双学习⁷的作品,灵感来自一篇关于机器翻译的双学习的重要论文。⁸从论文中引用,将图像到图像转换视为双学习任务的想法如下:

我们的方法受到自然语言处理中的双学习的启发。双学习同时训练两个“相反的”语言翻译器(例如,英语到法语和法语到英语),通过最小化由两个翻译器的嵌套应用产生的重建损失。这两个翻译器代表原始-双对,并且嵌套应用形成一个闭环,允许应用强化学习。具体来说,在单语数据(英语或法语)上测量的重建损失将生成信息反馈,以训练双语翻译模型。

尽管 CycleGAN 论文中没有明确引用,但这些想法似乎有相当多的重叠。如预期的那样,DualGAN 的设置也使用了两对生成器-判别器。这两对被称为原始 GAN 和双 GAN。图 7.16展示了 DualGAN 的整体设置:

手机屏幕截图 自动创建的说明

图 7.16:DualGAN 设置⁷

原始 GAN(表示为G[A])从域 U 接收输入 u 并学习将其转换为来自域 V 的v,而双 GAN 进行相反操作。这个设置的两个反馈信号称为重建错误和成员评分。成员评分类似于 CycleGAN 中的对抗损失,其中G[A]的目标是生成足够优秀的输出G[A] 以欺骗D[A]

重建损失表示了 G[B] 从生成的输出 G[A] 中重构原始输入 u 的学习能力。重建损失类似于 CycleGAN 的循环一致性损失,除了在问题制定方面与 CycleGAN 的差异之外,训练设置也略有不同。DualGAN 设置使用 Wasserstein 损失进行训练。他们报告说使用 Wasserstein 损失有助于轻松获得稳定的模型。图 7.17 显示了 DualGAN 论文中的一些实验:

一个包含食物的图片  自动生成的描述

图 7.17:使用 DualGAN 进行非配对图像到图像的翻译⁷

DualGAN 设置还利用了 U-Net 风格的生成器和 Patch-GAN 风格的鉴别器,这些技巧对输出质量产生了影响。

摘要

在本章中,我们通过图像到图像的翻译任务的视角探索了 GAN 研究的创新一面。尽管创新的影响是显而易见的,但这些技术也为改进计算机视觉模型的研究和开发开辟了途径,尤其是在数据集难以获取的领域。

我们从理解配对图像到图像的翻译任务开始了本章。这个任务提供了训练数据,其中源域和目标域有配对的训练样本。我们使用 pix2pix GAN 架构探索了这个任务。通过这种架构,我们探索了编码器-解码器架构如何有助于开发可以产生高保真输出的生成器。pix2pix 论文通过利用跳跃连接或 U-Net 风格生成器将编码器-解码器架构推进了一步。

这种设置还提出了另一个强大的概念,称为 Patch-GAN 鉴别器,它可以优雅地帮助整体 GAN 获得更好的反馈信号,适用于不同的风格转换用例。我们利用这些概念从零开始构建和训练了我们自己的 pix2pix GAN,将卫星图像转换为类似 Google 地图的输出。我们的训练结果是使用很少的训练样本和训练迭代得到的高质量输出。观察到这种更快速和稳定的训练是这项工作的作者们贡献的不同创新直接影响的结果。我们还探索了使用 pix2pix 风格架构可以实现的各种其他用例。

在本章的第二部分中,我们将图像到图像的翻译任务扩展到了非配对的设置中。非配对的训练设置无疑是一个更加复杂的问题,但它开辟了更多的可能性。配对设置适用于在源域和目标域中都有明确样本对的情况,但大多数现实场景下并没有这样的数据集。

我们通过 CycleGAN 架构探索了非配对图像到图像的翻译设置。CycleGAN 的作者提出了许多直观但强大的贡献,使得非配对设置能够工作。我们讨论了循环一致性损失和身份损失作为整体对抗损失的正则化项的概念。我们特别讨论了身份损失如何改善样本的整体重构,从而提高输出的整体质量。利用这些概念,我们使用 TensorFlow-Keras API 从头开始构建了 CycleGAN 设置。我们对两个数据集进行了实验,一个是苹果到橙子,另一个是照片到梵高风格的绘画。在这两种情况下,结果都非常好。

在本章的最后一节中,我们讨论了一些相关工作,即 DiscoGAN 和 DualGAN 架构。与 CycleGAN 一起,这两种架构构成了整体非配对图像到图像翻译 GAN 的家族。我们讨论了这些架构如何从略有不同的角度呈现相似的思想。我们还讨论了问题表述和整体架构中细微差异如何影响最终结果。

在这一章中,我们在 第六章使用 GAN 生成图像 中讨论的与 GAN 及特别是条件 GAN 相关的概念基础上进行了拓展。我们讨论了许多创新和贡献,利用简单的构建模块实现了一些惊人的用例。接下来的一系列章节将进一步将生成模型的边界推向文本和音频等领域。系好安全带吧!

参考文献

  1. Mordvintsev,A.,麦克唐纳,K.,鲁道夫,L.,金,J-S.,李,J.,以及 daviga404。 (2015)。deepdream。GitHub 代码库。github.com/google/deepdream

  2. Gatys,L.A.,Ecker,A.S.,以及 Bethge,M.(2015)。一种艺术风格的神经算法。arXiv。arxiv.org/abs/1508.06576

  3. 朱,J-Y.,帕克,T.,伊索拉,P.,以及埃弗罗斯,A.A.(2017)。使用循环一致性对抗网络进行非配对图像到图像的翻译。arXiv。arxiv.org/abs/1703.10593

  4. 伊索拉,P.,朱,J-Y.,周,T.,以及埃弗罗斯,A.A.(2017)。带条件对抗网络的图像到图像翻译。2017 年 IEEE 计算机视觉与模式识别会议(CVPR),2017,pp. 5967-5976。ieeexplore.ieee.org/document/8100115

  5. Ronneberger,O.,Fisher,P.,以及 Brox,T.(2015)。U-net:用于生物医学图像分割的卷积网络。MICCAI,2015。arxiv.org/abs/1505.04597

  6. 金,T.,查,M.,金,H.,李,J.K.,以及金,J.(2017)。用生成对抗网络学习发现跨域关系。2017 年国际机器学习会议(ICML)。arxiv.org/abs/1703.05192

  7. Yi, Z., Zhang, H., Tan, P., & Gong, M. (2017). DualGAN: 无监督的图像到图像翻译的双向学习. ICCV 2017. arxiv.org/abs/1704.02510v4

  8. Xia, Y., He, D., Qin, T., Wang, L., Yu, N., Liu, T-Y., & Ma, W-Y. (2016). 机器翻译的双向学习. NIPS 2016. arxiv.org/abs/1611.00179

第八章:使用 GANs 进行 deepfakes

对视频和照片进行编辑以编辑文物已经有很长一段时间了。如果你看过《阿甘正传》或《速度与激情 7》,很有可能你甚至没有注意到这些电影中约翰·F·肯尼迪或保罗·沃克的场景是假的,是根据需要编辑到电影中的。

你可能还记得电影《阿甘正传》中的一个场景,阿甘会见约翰·F·肯尼迪。该场景是使用复杂的视觉效果和档案素材创建的,以确保高质量的结果。好莱坞制片厂、世界各地的间谍机构和媒体机构一直在利用诸如 Photoshop、After Effects 和复杂的自定义视觉效果/CGI(计算机生成图像)流水线等编辑工具来获得如此引人入胜的结果。虽然在大多数情况下结果或多或少是可信的,但要编辑每一个细节,如场景光线、面部、眼睛和唇部运动以及每一帧的阴影,需要大量的人工工作和时间。

同样,在 BuzzFeed 的一个视频中,你很有可能见过前美国总统巴拉克·奥巴马说“基尔蒙格是对的”(基尔蒙格是漫威电影宇宙的一个反派角色)。虽然显然是假的,但从视觉和音频方面看,视频似乎是真实的。还有许多其他例子,突出人物可以被看到发表他们通常不会说的评论。

不考虑伦理问题,阿甘见到约翰·F·肯尼迪和巴拉克·奥巴马谈论基尔蒙格之间有一个主要区别。如前所述,前者是通过使用复杂的视觉效果/CGI 进行的繁琐手工工作的结果。而后者,则是一种名为deepfakes的技术的结果。deep learningfake的混成词,deepfake是一个广泛的术语,用于描述生成我们讨论的示例的 AI 能力技术。

在本章中,我们将涵盖与 deepfakes 相关的不同概念、架构和组件。我们将重点关注以下主题:

  • 深度伪造技术景观概览

  • Deepfaking 的不同形式:替换、重新演绎和编辑

  • 不同架构利用的关键特性

  • 高级 deepfakes 工作流程

  • 使用自动编码器交换面孔

  • 使用 pix2pix 重新演绎奥巴马的面部动作

  • 挑战和道德问题

  • 关于现成实现的简要讨论

我们将介绍不同 GAN 架构的内部工作原理和使 deepfakes 成为可能的主要贡献。我们还将从头构建和训练这些架构,以更好地理解它们。Deepfakes 不仅限于视频或照片,还用于生成假文本(新闻文章,书籍)甚至语音(语音片段,电话录音)。在本章中,我们只关注视频/图像,术语deepfakes指相关用例,除非另有说明。

本章中呈现的所有代码片段都可以直接在 Google Colab 中运行。出于空间原因,依赖项的导入语句未包含在内,但读者可以参考 GitHub 存储库获取完整的代码:github.com/PacktPublishing/Hands-On-Generative-AI-with-Python-and-TensorFlow-2

让我们从 deepfakes 的概述开始。

Deepfakes 概述

Deepfakes 是一个总括性术语,代表使用人工智能(特别是深度学习)生成的内容,对人类看来真实和可信。生成假内容或操纵现有内容以适应参与方的需求和议程并不是什么新鲜事。在前言中,我们讨论了一些通过 CGI 和费力的手工努力来生成逼真结果的电影。随着深度学习和更具体地说,生成模型的进步,越来越难区分真实与虚假。

生成对抗网络GANs)在这一领域发挥了非常重要的作用,使得能够生成清晰、高质量的图像和视频。诸如thispersondoesnotexist.com等基于 StyleGAN 的作品真正推动了生成高质量逼真内容的界限。其他一些关键架构(我们在第六章使用 GAN 生成图像第七章使用 GAN 进行风格转换中讨论过)已成为不同 deepfake 设置的重要构建基块。

Deepfakes 有许多应用,可以分类为创造性、生产性和不道德或恶意的用例。以下是几个例子,突出了 deepfakes 的不同用例。

创造性和生产性用例:

  • 重现历史和著名人物:有许多历史人物,我们很乐意与之互动并学习。有能力操纵和生成逼真内容的深度伪造技术正是这种用例所需的技术。一项大规模的此类实验是为了让著名超现实主义画家萨尔瓦多·达利重生。达利博物馆与广告公司 GS&P 合作,开发了一个名为《达利永生》的展览。² 该展览利用存档的视频素材和访谈来训练一个深度伪造设置,观看者被达利所迎接,然后与他分享了他的一生故事。最后,达利甚至提出与观众自拍,输出的照片确实是逼真的自拍照。

  • 电影翻译:随着 Netflix 等平台变得越来越普遍,观众们观看跨语言内容的次数比以往任何时候都要多。虽然字幕和手动配音是可行的选择,但它们还有很多需要改进的地方。利用深度伪造技术,使用人工智能自动生成任何视频的配音翻译比以往任何时候都更加容易。被称为 疟疾必须消灭 的社会倡议利用了类似的技术,创建了一个强大的运动,帮助著名足球运动员大卫·贝克汉姆以九种不同的语言传播意识。³ 同样地,深度伪造技术已经被印度的一个政党使用,其中候选人在竞选活动中被看到使用不同的语言发言。⁴

  • 时尚:利用 GANs 和其他生成模型创建新的风格和时尚内容并不是什么新鲜事。随着深度伪造技术的出现,研究人员、博客作者和时尚品牌将时尚产业推向了新的高度。现在我们有了由人工智能生成的数字模特,她们穿着新的时尚系列,并帮助降低成本。这项技术甚至被用来创建可以个性化模仿买家体型的模特渲染,以提高购买的机会。⁵

  • 视频游戏角色:多年来,视频游戏已经取得了很大进步,许多现代游戏展现了电影级的图形。传统上,人类演员被利用来在这些游戏中扮演角色。然而,现在有一个越来越流行的趋势,就是利用深度伪造和相关技术来开发角色和故事情节。游戏 使命召唤 的开发者发布了一段预告片,展示了前美国总统罗纳德·里根在游戏中扮演一个角色。⁶

  • 库存图像:营销传单、广告和官方文件有时需要把某些人放在其他内容旁边。传统上,使用实际的演员和模特。也有库存图像服务授权此类内容进行商业使用。有了thispersondoesnotexist.com这样的作品,现在非常容易根据我们的需求生成一个新的面孔或个性,而不需要任何实际的演员或模特。

恶意使用案例:

  • 色情内容:根据我们的需求生成虚假内容具有严重后果。事实上,deepfakes 引起公众注意时,是因为在 2017 年,一位 Reddit 用户发布了一个臭名昭著的伪造色情视频,视频中一位名人的面孔被替换了⁷。此后,已经有整个社区致力于生成这样的虚假视频,这对他们描绘的人物的公众形象可能造成非常严重的破坏。

  • 冒充:我们已经讨论过一个前美国总统巴拉克·奥巴马演讲的伪造视频,他谈论了许多他通常会避免的话题和事物。制作这样的视频来冒充公众人物、政治家等可能会产生巨大的后果。

Deepfakes 包括类似真实的内容,可以被归类为多个子类别。在下一节中,我们将讨论不同类别,以更好地理解整体情况。

操作模式

生成可信的虚假内容需要注意多个方面,以确保结果尽可能地真实。典型的 deepfake 设置需要一个一个目标生成的内容

  • 源(用下标s表示)是控制所需输出的驱动身份。

  • 目标(用下标t表示)是正在伪造的身份。

  • 生成的内容(用下标g表示)是通过将源转换为目标得到的结果。

现在我们已经了解了一些基本术语,让我们深入了解生成虚假内容的不同方式。

替换

这是生成虚假内容的最常用形式。目的是用源(xₛ)的特定内容替换目标(xₜ)的内容。面部替换已经是一个长期以来的研究领域。图 8.1显示了唐纳德·特朗普的面孔被尼古拉斯·凯奇的面孔替换的例子。图中展示了源(xₛ)和目标(xₜ)的身份,而生成的内容(x[g])显示在最后一列:

图 8.1:面部替换⁸

替换技术可以大致分为以下几类:

  • 转移:这是一种基本的替换形式,其中xₛ的内容(例如,脸部替换的情况下)转移到xₜ。转移方法在粗略的上下文中大多被利用,换句话说,替换并不如人们所期望的那样干净或平滑。例如,对于购物服装,用户可能有兴趣在不同的服装中进行可视化。这类应用可以省略非常详细的信息但仍然为用户提供所需的体验。

  • 交换:这是一种略微复杂一点的替换类型,其中对xₜ的转移受到xₜ本身特定特征的引导。例如,在图 8.1中,底部一行显示了尼古拉斯·凯奇的脸被换到唐纳德·特朗普的脸上。替换图像保持了特朗普(目标图像)的头发、姿势等特征。

替换模式,尽管听起来很琐碎,但并不简单,因为任何模型都需要关注与图像照明、肤色、遮挡和阴影相关的许多因素。本章后面的部分将讨论其中一些方面的处理。

重新表演

替换方法产生令人印象深刻的结果,但生成的内容仍有改进的空间。再演绎方法被用来捕捉目标的姿势、表情和凝视等特征,以改进生成内容的可信度。再演绎技术侧重于以下方面以提高虚假内容的质量:

  • 凝视:重点是关注眼睛和眼皮的位置。该领域的技术试图根据源的眼部运动/凝视重新演绎生成输出的凝视。这对于改进照片或保持视频中的眼神联系非常有用。

  • 口部:重新演绎面部的嘴唇和口部区域可以提高生成内容的可信度。在这种情况下,xₜ的口部运动取决于xₛ的口部运动。在某些情况下,源输入xₛ可能是语音或其他音频。口部重新演绎方法也被称为 Bol 方法。

  • 表情:这是再演绎的一种更通用形式,通常包括其他再演绎方面,如眼睛、嘴巴和姿势。它们被用来根据xₛ驱动xₜ的表达。

  • 姿势:姿势再现,无论是头部还是身体,都是一种全面考虑头部和整个身体定位的方法。在这种情况下,源驱动目标,并产生更有信服力的结果。

这些再现在 图 8.2 中得到了更好的描述,在图的左侧我们有源(xₛ)和目标(xₜ),图的右侧显示了源的不同方面如何影响生成的内容。请注意,图 8.2 仅用于举例说明,结果并不仅仅是目标内容的简单复制粘贴。随着我们在本章中的深入,我们还会看到更进化的例子。

图 8.2: 再现方法。受影响的区域在每次再现中都有所突出

特定地区虚拟不同类型的再现在 图 8.2 中被特别突出。 正如前面提到的,很显然表情再现包括眼部和口部区域。

编辑

深伪造并不一定涉及替换或再现。深伪造的另一个应用是为了实现特定目标而添加、删除或更改目标实体的某些方面。编辑可能涉及对服装、年龄、种族、性别、头发等进行操纵。以下图示了一些可能的编辑:

图 8.3: 编辑模式下的深伪造。左图是变换的基础输入。右图展示了三种不同的编辑: 头发、眼镜和年龄。

图 8.3 右侧的编辑展示了如何将输入图像的某些属性转换为生成虚假内容。有许多良性的用例,要么是为了好玩(如FaceAppREFACE这样的应用程序),要么具有商业价值(眼镜和化妆品品牌)。然而,也有许多恶意的应用程序(色情作品、虚假身份等),这些恶意应用程序削弱并引发了对此类工具的使用引发的问题。

我们已经介绍了生成虚假内容的不同模式的基础知识,并讨论了每种模式的主要关注领域。在下一节中,我们将讨论在训练此类模型中发挥作用的特征以及我们如何利用它们。

关键特征集

人脸和身体是虚假内容生成任务中的关键实体。尽管深度学习架构通常不需要手工制作的特征,在处理复杂实体时,小小的推动会产生深远影响。特别是在处理人脸时,除了在给定图像或视频中检测整体脸部之外,深伪造解决方案还需要关注眼睛、嘴巴和其他特征。在上一节中,我们讨论了不同的操作模式,强调了脸部不同部分的重要性以及它们对改善所生成虚假内容的可信度的影响。

在本节中,我们将简要介绍一些不同深伪造解决方案利用的重要特征。这些特征包括:

  • 面部行为编码系统 (FACS)

  • 三维可塑模型 (3DMM)

  • 面部标志

我们还将进行一些实际操作,以更好地理解这些特征集。

面部动作编码系统(FACS)

胡尔特绍在 1969 年开发了 FACS,并在 1978 年由埃克曼等人采用和完善。面部动作编码系统,或 FACS,是一种基于解剖学的系统,用于理解面部运动。它是用于分析面部肌肉以理解表情和情绪的最详尽和准确的编码系统之一。

图 8.4描述了一些特定的肌肉动作及其相关含义。

一个手机截图  自动生成的描述

图 8.4:使用 FACS 进行动作标记的样本集

FACS 包括一份详细的手册,由人类编码员用来手动编码每个面部表情。肌肉活动被分为被称为行动单元(AU)的组。这些 AU 代表与面部表情对应的肌肉活动。 图 8.4 描述了一些示例 AU,指向眉毛、嘴唇和面部其他部分的运动。

尽管最初的 FACS 系统需要人工编码员,现在已经有自动化系统可计算确定正确的 AU。像下面这样的作品利用自动的 AU 生成逼真的结果:

  • GANimation:解剖学感知的 来自单个图像的面部动画

  • 视觉效果的高分辨率 人脸换装¹⁰

  • 3D 引导的细粒度人脸操作¹¹

尽管 FACS 可以对给定面部表情提供细粒度的理解,但整个系统的复杂性限制了它在专业动画/CGI/VFX 工作室之外的使用。

3D 可塑模型

三维可塑模型,简称 3DMM,是一种从 2D 图像推断完整三维面部表面的方法。最初由 Blanz、Vetter 等人在其名为用于合成 3D 面部的可塑模型的作品中提出¹²,这是一种强大的统计方法,可以模拟人脸形状和质地以及姿势和照明。

该技术通过将输入图像转换为面部网格来工作。面部网格由确定面部每个部分的形状和质地的顶点和边组成。网格有助于使用一组向量和矩阵对姿势和表情进行参数化。然后,这些向量或 3D 重建本身可以用作我们的伪造内容生成模型的输入特征。

面部标记

基于 FACS 和 3DMM 的特征在定义人脸(以及总体身体)特征方面具有高度准确性和表现力。然而,这些方法在计算方面是昂贵的,有时甚至需要人类干预(例如,FACS 编码)才能得到良好的结果。面部标记是另一种特征集,简单而强大,并且被一些最近的作品使用以取得最先进的结果。

面部标记是一系列重要的面部特征,如鼻子、眉毛、嘴巴和眼睛的角落。 目标是使用某种回归模型检测这些关键特征。 最常见的方法是利用预定义的一组在面部或身体上可以使用训练模型进行有效跟踪的位置。

面部标记检测任务可以分解为以下两步骤:

  • 第一步涉及定位给定图像中的一个或多个面孔。

  • 第二步需要细化,识别已识别的面孔的关键面部结构。

这两个步骤可以看作是形状预测的特殊案例。 我们可以使用几种不同的方法来检测面部标记作为生成虚假内容任务的特征。 在接下来的小节中,我们将介绍三种最广泛使用的方法:OpenCV、dlib 和 MTCNN。

使用 OpenCV 进行面部标记检测

OpenCV 是一个旨在处理实时任务的计算机视觉库。 它是最受欢迎和广泛使用的库之一,可在许多语言中使用包装器,包括 Python。 它包括许多扩展和 contrib-packages,例如用于人脸检测、文本处理和图像处理的扩展。 这些包增强了它的整体功能。

使用 OpenCV 可以以几种不同的方式执行面部标记检测。 其中一种方法是利用 Haar Cascade 过滤器,它们利用直方图然后是 SVM 进行对象检测。 OpenCV 还支持基于 DNN 的执行相同任务的方法。

使用 dlib 进行面部标记检测

Dlib 是另一个跨平台库,提供的功能与 OpenCV 大体相似。 Dlib 的主要优势在于提供了一系列用于人脸和标记的预训练检测器。 在我们深入实现细节之前,让我们更多地了解一下标记特征。

面部标记是给定面部的细节。 即使每张脸都是独特的,但有些属性可以帮助我们识别给定形状为面孔。 这个精确的常见特征列表被编码成所谓的 68 坐标68 点系统。 此点系统被设计用于注释 iBUG-300W 数据集。 这个数据集是 dlib 提供的许多标记检测器的基础。 每个特征都被赋予一个特定的索引(在 68 个之中)并且有它自己的 (x, y) 坐标。 在 图 8.5 中指示了这 68 个索引。

一部手机的屏幕截图  自动生成的描述

图 8.5:来自 iBUG-300W 数据集的 68 个点标注

如图所示,每个索引对应于特定的坐标,一组索引标记了面部标记。 例如,索引 28-31 对应于鼻子的桥梁,检测器尝试检测并预测这些索引的相应坐标。

设置 dlib 是一个有点复杂的过程,特别是如果你使用的是 Windows 机器。请参考以下设置指南:

现在,让我们利用这个 68 个面部地标的坐标系来开发一个简短的演示应用程序,用于检测面部特征。我们将利用 dlib 和 OpenCV 中的预训练检测器来构建这个演示。以下代码片段显示了如何用几行代码轻松地识别不同的面部地标:

detector = dlib.get_frontal_face_detector() 
predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")
image = cv2.imread('nicolas_ref.png')
# convert to grayscale
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 
faces = detector(gray)
# identify and mark features
for face in faces:  
    x1 = face.left() 
    y1 = face.top() 
    x2 = face.right() 
    y2 = face.bottom() 
    landmarks = predictor(gray, face) 
    for n in range(0, 68): 
        x = landmarks.part(n).x 
        y = landmarks.part(n).y 
        cv2.circle(image, (x, y), 2, (255, 0, 0), -1) 

上述代码接受面部图像作为输入,将其转换为灰度,并使用 dlib 检测器和预测器在脸上标出上述 68 个点。一旦我们准备好这些功能,就可以执行整个脚本。该脚本弹出一个视频捕获窗口。视频输出叠加了面部地标,如图 8.6所示:

图 8.6:使用预训练检测器进行面部地标检测的样本视频捕获

如你所看到的,预训练的面部地标检测器似乎做得很好。通过几行代码,我们能够获得具体的面部特征。在本章的后续部分,我们将利用这些特征来训练我们自己的深度伪造架构。

使用 MTCNN 进行面部地标检测

对于面部和面部地标检测任务,OpenCV 和 dlib 有很多替代方案。其中最突出、表现最好的之一叫做MTCNN,全称多任务级联卷积网络。由张, 张等¹³开发,MTCNN 是一个由三个级联网络组成的复杂深度学习架构。这三个网络共同帮助完成面部和地标识别的任务。对于 MTCNN 的详细讨论超出了本书的范围,但我们将简要介绍其显著的方面并构建一个快速演示。有兴趣的读者请阅读原始引用的工作了解详情。

如前所述,MTCNN 设置使用了三个级联网络,称为 P-Net、R-Net 和 O-Net。不多说,设置首先建立了输入图像的金字塔,即将输入图像缩放到不同的分辨率。然后,提议网络(P-Net)将其作为输入,并输出可能包含面部的潜在边界框。在中间进行一些预处理步骤后,精化网络(R-Net)通过将其缩小到最可能的边界框来精化结果。

最终输出由 Output-Net 或 O-Net 生成。O-Net 输出包含面部的最终边界框,以及眼睛、鼻子和嘴巴的地标坐标。

现在让我们尝试一下这个最先进的架构来识别脸部和相应的标志。幸运的是,MTCNN 可以作为一个 pip 软件包,非常容易使用。在下面的代码清单中,我们将构建一个实用函数来利用 MTCNN 进行我们所需的任务:

def detect_faces(image, face_list):
    plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
    ax = plt.gca()
    for face in face_list:
        # mark faces
        x, y, width, height = face['box']
        rect = Rectangle((x, y), width, height, fill=False, 
                                                color='orange')
        ax.add_patch(rect)
        # mark landmark features
        for key, value in face['keypoints'].items():
            dot = Circle(value, radius=12, color='red')
            ax.add_patch(dot)
    plt.show()
# instantiate the detector
detector = MTCNN()
# load sample image
image = cv2.imread('trump_ref.png')
# detect face and facial landmarks
faces = detector.detect_faces(image)
# visualize results
detect_faces(image, faces) 

正如代码清单所展示的,MTCNN 检测器的预测为每个检测到的脸部输出两个项目 - 脸部的边界框和每个面部标志的五个坐标。利用这些输出,我们可以利用 OpenCV 在输入图像上添加标记以可视化预测。

图 8.7展示了这次练习的样本输出。

图 8.7:基于 MTCNN 的人脸和面部标志检测

正如图所示,MTCNN 似乎已经正确地检测出图像中的所有面部以及面部标志。我们只需几行代码,就能够使用最先进的复杂深度学习网络快速生成所需的输出。类似于上一节中的 dlib/OpenCV 练习,我们可以利用 MTCNN 识别关键特征,这些特征可以用作我们虚假内容生成模型的输入。

另一个易于使用的基于深度学习的人脸检测和识别库名为face_recognition。这是一个可以通过 pip 进行安装的软件包,为这两项任务提供了直接的 API。对于人脸识别的任务(主要目的是识别一个人而不仅仅是检测脸部),它使用了 VGGFace。VGGFace 是牛津大学视觉几何组开发的深度学习架构。它使用了 VGG 风格的主干来提取面部特征。这些特征然后可以用于相似性匹配。我们将在本章的后续部分使用这个软件包。

现在我们已经了解了不同模式以及识别和提取相关特征的不同方式,让我们开始从头开始构建自己的一些这样的架构。在接下来的部分中,我们将讨论构建深伪造模型的高级流程以及为此目的使用的常见架构,接着从头开始操作训练其中几个。

高级工作流程

虚假内容生成是一个复杂的任务,包含许多组件和步骤来帮助生成可信的内容。虽然这个领域正在经历很多改进整体结果的研究和技巧,但整体设置主要可以用一些常见的构造块来解释。在本节中,我们将讨论一个描述深伪造设置如何使用数据来训练和生成虚假内容的常见高级流程。我们还将简要介绍一些在许多作品中用作基础构造块的常见架构。

正如前面讨论的,深度伪造设置需要一个源身份(xₛ),它驱动目标身份(xₜ)生成虚假内容(x[g])。为了理解高级流程,我们将继续使用这个符号,以及与前一节讨论的关键特征集相关的概念。步骤如下:

  • 输入处理

    • 使用识别和裁剪面孔的面部检测器处理输入图像(xₛxₜ)。

    • 然后使用裁剪的面孔来提取中间表示或特征。

  • 生成

    • 中间表示(xₛ或另一张脸)与驱动信号一起用于生成新的面孔。
  • 混合

    • 然后,混合函数将生成的面孔尽可能清晰地合并到目标中。

相应的工作采用额外的中间或后处理步骤来改善整体结果。图 8.8详细描述了主要步骤:

图 8.8:创建深度伪造的高级流程

如图所示,我们使用尼古拉斯·凯奇的照片作为输入,并将其转换为一个类似于唐纳德·特朗普的假照片。每个步骤中使用的关键组件可以是本章中已经介绍过的各种组件中的任何一个。例如,面部裁剪步骤可以利用 dlib 或 MTCNN,同样,用于生成过程的关键特征可以是 FACS AUs、面部标志或 3DMM 向量中的任何一个。

到目前为止,我们已经涵盖了与面部裁剪和关键特征相关的方面,这些特征可以在这个虚假内容生成过程中使用。深度伪造的下一步是最终输出图像或视频的生成。生成建模是我们在以前的章节中已经相当深入讨论过的内容,从变分自编码器到不同类型的 GANs。对于虚假内容生成的任务,我们将建立在这些架构之上。读者应该注意,deepfakes 任务是我们在这些先前章节中涵盖的不同模型的特殊情况,或者说是受限制的用例。

现在让我们来看一下一些不同 deepfake 作品中常用的一些架构。

常见架构

大多数 deepfake 设置利用已知的架构并进行一定的调整,作为生成虚假内容的构建模块。我们在第 4、5、6 和 7 章中详细讨论了大多数这些架构。以下是用于生成图像或视频的最常用架构的简要重述。

编码器-解码器(ED)

编码器-解码器架构包括两个组件,编码器和解码器。编码器组件由一系列层组成,从实际的高维输入(如图像)开始。然后将输入缩小到较低维度空间或向量表示,称为瓶颈特征。解码器组件需要瓶颈特征,并将其解码或转换为不同或相同的向量空间作为输入。典型的 ED 架构如下所示:

包含显示器、屏幕、钟表的图片  自动生成描述

图 8.9:典型的编码器-解码器架构

编码器解码器架构的特殊情况被称为自编码器。自编码器将输入转换为瓶颈特征,然后将原始输入重构为输出。这样的网络在学习输入特征表示上很有用。编码器-解码器架构的另一种变体称为变分自编码器,或者 VAE。VAE 学习给定输入空间的解码器的后验分布。在第五章使用 VAE 的神经网络绘画图像中,我们看到 VAE 在学习和解开表示方面更好,并且在总体上生成内容更好(与自编码器相比)。

生成对抗网络(GANs)

GANs 是隐式密度建模网络,在最近的研究中被用来生成非常高质量的输出。不详细介绍,GAN 的设置包括两个竞争模型,一个生成器和一个鉴别器。生成器的任务是根据驱动信号(噪音向量,条件输入等)生成看起来真实的内容。另一方面,鉴别器的任务是区分假的和真的。两个网络进行最小最大博弈,直到达到均衡状态,生成器能够生成足够好的样本来愚弄鉴别器。

典型的 GAN 如图 8.10所示:

包含绘画的图片  自动生成描述

图 8.10:典型的 GAN 架构

GANs 在生成高质量输出方面非常有效,并且多年来一直是重要研究的对象。改进已经带来了一些真正强大的变体,使其进一步推动了边界。在深度伪造的上下文中,两个最广泛使用的变体是CycleGANpix2pix。两种架构都是为图像到图像的转换任务设计的。Pix2pix 是一个配对翻译网络,而 CycleGAN 不需要对训练样本进行任何配对。这两种架构的有效性和简单性使它们成为深度伪造任务的完美候选者。我们在第七章使用 GANs 进行风格转移中详细讨论了这两种架构;我们鼓励你快速浏览前一章,以更好地理解本章剩余部分。

到目前为止,我们已经相当详细地涵盖了所有所需的基本组件。现在让我们利用这种理解,从头开始实现几个深度伪造的设置。

使用自动编码器进行替换

深度伪造是技术的一个有趣而强大的应用,既有用又危险。在前几节中,我们讨论了可以利用的不同操作模式和关键特性,以及常见的架构。我们还简要涉及了实现最终结果所需的不同任务的高层流程。在这一节中,我们将专注于使用自动编码器作为我们的主要架构来开发面部交换设置。让我们开始吧。

任务定义

本次练习的目标是开发一个面部交换设置。如前所述,面部交换是深度伪造术语下的一种替换模式操作。在此设置中,我们将专注于将好莱坞演员尼古拉斯·凯奇 (Nicolas Cage) 变身为前美国总统唐纳德·J·特朗普 (Donald J. Trump)。在接下来的章节中,我们将介绍为准备数据、训练模型以及最终生成交换的假图片而需要完成的每个子任务。

数据集准备

首要任务是数据准备。由于目标是为尼古拉斯·凯奇和唐纳德·特朗普开发一个面部交换器,我们需要包含每个人图像的数据集。出于许多原因,这个数据收集任务本身可能非常耗时和具有挑战性。首先,照片可能受到许可和隐私问题的限制。其次,公开可用的高质量数据集很难找到。最后,在照片中识别特定面孔也是一个具有挑战性的任务,因为在给定照片中可能存在属于不同人的多张面孔。

由于版权原因,我们无法发布用于获取本章精确输出的训练数据集,因为它们是从各种在线来源中获取的。但可能会对获取类似数据集有用的网站是:

假设我们已经收集了原始数据集,我们可以继续进行下一组任务:面部检测和识别。

第一个任务是定义一个实体类来保存与面部相关的对象。我们需要这样一个类,因为我们需要通过管道传递图像、提取的脸部和面部标志,以及变换。我们定义一个类,DetectedFace,如下面的代码片段所示:

# Entity class
class DetectedFace(object):
    def __init__(self, image, x, w, y, h, landmarks):
        self.image = image
        self.x = x
        self.w = w
        self.y = y
        self.h = h
        self.landmarks = landmarks
    def landmarksAsXY(self):
        return [(p.x, p.y) for p in self.landmarks.parts()] 

将这些经常使用的属性抽象成一个对象(类)使我们能够减少在不同的实用程序之间需要传递的单独参数的数量。我们在关键特征集部分讨论了face_recognition库。我们将利用这个库中的姿势预测模型来使用 dlib 的shape_predictor来预测面部位置。下面的代码片段实例化了预测器对象:

predictor_68_point_model = face_recognition_models.pose_predictor_model_location()
pose_predictor = dlib.shape_predictor(predictor_68_point_model) 
detect_faces utility method:
def detect_faces(frame):
    face_locations = face_recognition.face_locations(frame)
    landmarks = _raw_face_landmarks(frame, face_locations)
    for ((y, right, bottom, x), landmarks) in zip(face_locations, landmarks):
        yield DetectedFace(frame[y: bottom, x: right], 
                           x, right - x, y, bottom - y, landmarks) 

这种方法接受图像作为输入,并生成DetectedFace对象作为输出。读者应该注意,我们正在产生DetectedFace类型的对象。yield关键字确保了延迟执行,这意味着在需要时创建对象。这可以确保较小的内存需求。另一方面,DetectedFace对象将提取的面部和相应的标志抽象化。

detect_faces function to extract all the faces in the input image:
# Load Sample Image
image = cv2.imread('sample_image.jpg')
plt.imshow(cv2.cvtColor(image , cv2.COLOR_BGR2RGB))
plt.axis('off');
# Detect faces and visualize them all
detected_faces = [face for face in detect_faces(image)] 

我们迭代由detect_faces方法返回的生成器对象,以可视化所有识别到的面部。在下面的代码片段中,我们执行这个可视化:

for face in detected_faces:
  plt.imshow(cv2.cvtColor(face.image , cv2.COLOR_BGR2RGB))
  plt.axis('off');
  plt.show() 

用于面部识别和提取的示例图像如下图所示:

图 8.11:用于面部识别和提取的示例图像

正如图 8.11所示,有两张脸对应于唐纳德·特朗普和纳伦德拉·莫迪。使用detect_faces实用方法提取的面部显示在图 8.12中:

一张人的特写 描述是由低置信度自动生成的

图 8.12:从示例图像中提取的面部

FaceFilter, which helps us to do so:
class FaceFilter():
    def __init__(self, reference_file_path, threshold=0.65):
        """
        Works only for single face images
        """
        image = face_recognition.load_image_file(reference_file_path)
        self.encoding = face_recognition.face_encodings(image)[0]
        self.threshold = threshold
    def check(self, detected_face):
        encodings = face_recognition.face_encodings(detected_face.image)
        if len(encodings) > 0:
            encodings = encodings[0]
            score = face_recognition.face_distance([self.encoding],                                                    encodings)
        else:
            print("No faces found in the image!")
            score = 0.8
        return score <= self.threshold 

在前面的代码中,FaceFilter类需要一个参考图像作为输入。这是直观的;这个参考图像用作对比的基准,以确认我们是否找到了正确的面部。正如前面提到的,face_recognition包使用 VGGFace 为任何图像生成编码。我们对参考图像执行这个操作,并提取其矢量表示。然后在FaceFilter类中使用check函数来执行新图像与参考图像之间的相似性检查(使用欧氏距离或余弦相似性等度量)。如果相似度低于一定阈值,则返回False

FaceFilter using a reference image. We then iterate through the list of detected_faces to see which faces actually belong to Donald Trump:
face_filter = FaceFilter('trump_ref.png')
for face in detected_faces:
  if face_filter.check(face):
    plt.title("Matching Face")
    plt.imshow(cv2.cvtColor(face.image , cv2.COLOR_BGR2RGB))
    plt.axis('off');
    plt.show() 

示例图像、参考图像和识别的面部显示在图 8.13中:

图 8.13:示例图像,参考图像,匹配的脸和不匹配的脸

正如图中所示,我们的FaceFilter类能够识别哪个脸属于唐纳德·特朗普,哪个脸不属于他。这对于创建我们的数据集非常有用。

Extract class, which takes in the extracted face as input and generates an aligned output; in other words, we align the orientation of the cropped/extracted face with that of the reference image:
class Extract(object):
    def extract(self, image, face, size):
        if face.landmarks is None:
            print("Warning! landmarks not found. Switching to crop!")
            return cv2.resize(face.image, (size, size))
        alignment = get_align_mat(face)
        return self.transform(image, alignment, size, padding=48)
    def transform(self, image, mat, size, padding=0):
        mat = mat * (size - 2 * padding)
        mat[:, 2] += padding
        return cv2.warpAffine(image, mat, (size, size)) 

Extract类中的extract方法将整个图像以及DetectFace对象作为输入。它还接受一个大小参数,以将图像调整为所需的尺寸。我们利用了 cv2 的warpAffine和 skimage 的transform方法来执行对齐。感兴趣的读者请查阅这些库的官方文档以获取更多详情。目前,我们可以将这些视为帮助函数,允许我们提取和对齐检测到的面部。图 8.14显示了对齐后的输出:

图 8.14:从输入图像到提取的面部,最后到对齐的面部的变换

图中所示的变换突显了原始提取的面部与对齐后的面部之间的细微差别。这个变换适用于任何面部姿势,并有助于对齐面部以获得更好的结果。

现在我们了解了逐步任务,让我们将所有内容整理起来以生成所需的数据集。以下代码片段将所有这些步骤合并为一个单一方法以便使用:

def get_faces(reference_image,image,extractor,debug=False):
    faces_count = 0
    facefilter = FaceFilter(reference_image)
    for face in detect_faces(image):
        if not facefilter.check(face):
            print('Skipping not recognized face!')
            continue
        resized_image = extractor.extract(image, face, 256)
        if debug:
          imgplot = plt.imshow(cv2.cvtColor(resized_image,                                             cv2.COLOR_BGR2RGB))
          plt.show()
        yield faces_count, face
        faces_count +=1 

接下来,我们使用get_faces方法编写一个高级函数,该函数以原始图像作为输入,以及其他所需对象,来提取并将相关面部转储到输出目录。如下所示:

def create_face_dataset(reference_face_filepath,
                        input_dir,
                        output_dir,
                        extractor,
                        included_extensions=included_extensions):
  image_list = [fn for fn in glob.glob(input_dir+"/*.*") \
              if any(fn.endswith(ext) for ext in included_extensions)]
  print("Total Images to Scan={}".format(len(image_list)))
  positive_ctr = 0
  try:
    for filename in image_list:
        image = cv2.imread(filename)
        for idx, face in get_faces(reference_face_filepath,image,extractor):
            resized_image = extractor.extract(image, face, 256)
            output_file = output_dir+"/"+str(filename).split("/")[-1]
            cv2.imwrite(output_file, resized_image)
            positive_ctr += 1
  except Exception as e:
      print('Failed to extract from image: {}. Reason: {}'.format(filename, e))
  print("Images with reference face={}".format(positive_ctr)) 

我们使用create_face_dataset扫描唐纳德·特朗普和尼古拉斯·凯奇的原始图像,为我们创建所需的数据集。

自编码器架构

我们使用前一节中提供的工具为唐纳德·特朗普和尼古拉斯·凯奇准备了数据集。现在让我们朝着一个学习人脸交换任务的模型架构努力。

我们在本书的前几节介绍了一些常见的架构。编码器-解码器设置是其中一个广泛用于深度伪造任务的设置。对于我们当前的人脸交换任务,我们将开发一个自编码器设置来学习和交换面部。一如既往,我们将利用 TensorFlow 和 Keras 来准备所需的模型。

在我们深入实际架构代码之前,让我们简要回顾一下这个设置是如何工作的。一个典型的自编码器有两个组件,编码器和解码器。编码器将图像作为输入并将其压缩到一个较低维度的空间。这个压缩表示被称为嵌入,或者瓶颈特征。解码器以相反的方式工作。它以嵌入向量作为输入,并试图将图像重建为输出。简而言之,自编码器可以描述为:

自编码器以x作为输入,并尝试生成一个重建x',使得x ≈ x'

通过对自编码器架构的简要概述,让我们开始为两个编码器和解码器开发所需的函数。以下片段显示了用于编码器部分创建下采样块的函数:

def conv(x, filters):
    x = Conv2D(filters, kernel_size=5, strides=2, padding='same')(x)
    x = LeakyReLU(0.1)(x)
    return x 

下采样块使用一个二维卷积层,后跟泄漏的 ReLU 激活。编码器将利用多个这样的重复块,然后是全连接和重塑层。最后,我们使用上采样块将输出转换为具有 512 个通道的 8x8 图像。以下片段显示了上采样块:

def upscale(x, filters):
    x = Conv2D(filters * 4, kernel_size=3, padding='same')(x)
    x = LeakyReLU(0.1)(x)
    x = UpSampling2D()(x)
    return x 

上采样块由二维卷积,LeakyReLU,最后是一个UpSampling2D层组成。我们使用上采样块和下采样块来创建编码器架构,如下片段所示:

def Encoder(input_shape, encoder_dim):
    input_ = Input(shape=input_shape)
    x = input_
    x = conv(x, 128)
    x = conv(x, 256)
    x = conv(x, 512)
    x = conv(x, 1024)
    x = Dense(encoder_dim)(Flatten()(x))
    x = Dense(4 * 4 * 1024)(x)
    # Passed flattened X input into 2 dense layers, 1024 and 1024`4`4
    x = Reshape((4, 4, 1024))(x)
    # Reshapes X into 4,4,1024
    x = upscale(x, 128)
    return Model(input_, x) 

另一方面,解码器具有更简单的设置。我们使用几个上采样块,然后是一个卷积层来重构输入图像作为其输出。以下片段显示了解码器的函数:

def Decoder(input_shape=(8, 8, 512)):
    input_ = Input(shape=input_shape)
    x = input_
    x = upscale(x, 256)
    x = upscale(x, 128)
    x = upscale(x, 64)
    x = Conv2D(3, kernel_size=5, padding='same', activation='sigmoid')(x)
    return Model(input_, x) 

对于我们的人脸交换任务,我们开发了两个自编码器,一个用于每个身份,换句话说,一个用于唐纳德·特朗普,一个用于尼古拉斯·凯奇。唯一的技巧是两个自编码器共享相同的编码器。是的,这种架构设置要求我们开发具有特定解码器但共同编码器的两个自编码器。

这个技巧之所以有效是因为有几个简单的原因。我们稍微讨论一下这一点。假设我们有两个自编码器,Autoencoder-A 和 Autoencoder-B,由一个公共编码器组成,但分别具有 Decoder-A 和 Decoder-B 的解码器。这个设置如下图所示:

图 8.15:使用自编码器进行替换

关于这个设置如何工作的详细信息如下:

  • 两个自编码器在训练过程中使用反向传播学习重构它们各自的输入。

  • 每个自编码器都试图最小化重构误差。在我们的情况下,我们将使用平均绝对误差MAE)作为我们的度量标准。

  • 由于两个自编码器具有相同的编码器,编码器学习理解两种类型的人脸并将它们转换为嵌入空间。

  • 通过对输入图像进行对齐和变形,确保编码器能够学习两种类型人脸的表示。

  • 另一方面,相应的解码器经过训练,利用嵌入来重构图像。

一旦两个编码器都训练得令我们满意,我们就进行人脸交换。让我们考虑这样一个场景:我们要将人物 B 的脸换到人物 A 的脸上:

  • 我们从一个人物 B 的图像开始。输入由编码器编码为低维空间。现在,我们不再使用 B 的解码器,而是将其与 A 的解码器交换,即 Decoder-A。这本质上是使用了来自人物 B 数据集的 Autoencoder-A 的输入。

  • 使用 B 作为 Autoencoder-A 的输入进行面部交换是可行的,因为 Autoencoder-A 将 B 的面部视为 A 本身的扭曲版本(因为存在公共编码器)。

  • 因此,Autoencoder-A 的解码器生成一个看起来像 A 的外观,但具有 B 的特征的输出图像。

让我们利用这一理解来创建所需的自动编码器。以下代码片段展示了两种类型面孔的自动编码器:

ENCODER_DIM = 1024
IMAGE_SHAPE = (64, 64, 3)
encoder = Encoder(IMAGE_SHAPE,ENCODER_DIM)
decoder_A = Decoder()
decoder_B = Decoder()
optimizer = Adam(lr=5e-5, beta_1=0.5, beta_2=0.999) #orig adam 5e-5
x = Input(shape=IMAGE_SHAPE)
autoencoder_A = Model(x, decoder_A(encoder(x)))
autoencoder_B = Model(x, decoder_B(encoder(x)))
autoencoder_A.compile(optimizer=optimizer, loss='mean_absolute_error')
autoencoder_B.compile(optimizer=optimizer, loss='mean_absolute_error') 

我们有两个接受 3 通道输入图像的自动编码器,每个图像大小为 64x64。编码器将这些图像转换为大小为 8x8x512 的嵌入,而解码器使用这些嵌入来重建形状为 64x64x3 的输出图像。在下一节中,我们将训练这些自动编码器。

训练我们自己的面部交换程序

现在我们已经准备好了自动编码器,我们需要准备一个自定义训练循环来一起训练这两个网络。然而,在进行训练循环之前,我们需要定义一些其他实用程序。

我们为两种个性创建的输入数据集包含它们在不同的光照条件、面部位置和其他设置下的面部。然而这些并不是穷尽的。为了确保我们捕获每种面孔的更大变化,我们将使用一些增强方法。以下代码片段展示了一个向输入图像施加随机变换的函数:

def random_transform(image,
                     rotation_range,
                     zoom_range,
                     shift_range,
                     random_flip):
    h, w = image.shape[0:2]
    rotation = np.random.uniform(-rotation_range, rotation_range)
    scale = np.random.uniform(1 - zoom_range, 1 + zoom_range)
    tx = np.random.uniform(-shift_range, shift_range) * w
    ty = np.random.uniform(-shift_range, shift_range) * h
    mat = cv2.getRotationMatrix2D((w // 2, h // 2), rotation, scale)
    mat[:, 2] += (tx, ty)
    result = cv2.warpAffine(image, mat, (w, h), 
                            borderMode=cv2.BORDER_REPLICATE)
    if np.random.random() < random_flip:
        result = result[:, ::-1]
    return result 

random_transform 函数帮助我们生成同一输入面部的不同扭曲。这种方法确保我们有足够的变化来训练我们的网络。

下一个需要的函数是批量生成器。由于我们处理图像和大型网络,务必记住资源要求。我们利用诸如yield这样的延迟执行实用程序来尽可能保持内存/GPU 要求低。以下代码片段显示了我们训练过程的批量生成器:

def minibatch(image_list, batchsize):
    length = len(image_list)
    epoch = i = 0
    shuffle(image_list)
    while True:
        size = batchsize
        if i + size > length:
            shuffle(image_list)
            i = 0
            epoch += 1
        images = np.float32([read_image(image_list[j])
                             for j in range(i, i + size)])
        warped_img, target_img = images[:, 0, :, :, :],                                  images[:, 1, :, :, :]
        i += size
        yield epoch, warped_img, target_img
def minibatchAB(image_list, batchsize):
    batch = minibatch(image_list, batchsize)
    for ep1, warped_img, target_img in batch:
        yield ep1, warped_img, target_img 

现在我们有了批量生成器和增强函数,让我们准备一个训练循环。下面是展示的:

def train_one_step(iter,batch_genA,batch_genB,autoencoder_A,autoencoder_B):
    epoch, warped_A, target_A = next(batch_genA)
    epoch, warped_B, target_B = next(batch_genB)
    loss_A = autoencoder_A.train_on_batch(warped_A, target_A)
    loss_B = autoencoder_B.train_on_batch(warped_B, target_B)
    print("[#{0:5d}] loss_A: {1:.5f}, loss_B: {2:.5f}".format(iter, loss_A, loss_B))
ctr = 10000
batchsize = 64
save_interval = 100
model_dir = "models"
fn_imgA = get_image_paths('nicolas_face')
fn_imgB = get_image_paths('trump_face')
batch_genA = minibatchAB(fn_imgA, batchsize)
batch_genB = minibatchAB(fn_imgB, batchsize)
for epoch in range(0, ctr):
    save_iteration = epoch % save_interval == 0
    train_one_step(epoch,batch_genA,batch_genB,autoencoder_A,autoencoder_B)
    if save_iteration:
        print("{}/{}".format(epoch,ctr))
        save_weights('models',encoder,decoder_A,decoder_B) 

我们训练两个自动编码器大约 10,000 步,或者直到损失稳定。我们使用批量大小为 64 并且每 100 个周期保存检查点权重。读者可根据其基础架构设置自由调整这些参数。

结果和局限性

现在,我们已经为尼古拉斯·凯奇(自动编码器-A)和唐纳德·特朗普(自动编码器-B)分别训练了对应的自动编码器。最后一步是将尼古拉斯·凯奇转换为唐纳德·特朗普。我们之前描述了这些步骤;我们将使用唐纳德·特朗普的自动编码器,输入为尼古拉斯·凯奇,从而生成一个看起来像尼古拉斯·凯奇版本的唐纳德·特朗普的输出。

但在进入输出生成任务之前,我们需要一些额外的实用程序。我们讨论了一个称为混合的额外步骤。这一步是在输出生成后执行的,以确保生成的替换和原始面孔无缝地融合成一幅图像。回头看图 8.8,对于混合概念的视觉提醒。对于我们的任务,我们准备了一个名为Convert的混合类。该类在以下片段中呈现:

class Convert():
    def __init__(self, encoder,
                 blur_size=2,
                 seamless_clone=False,
                 mask_type="facehullandrect",
                 erosion_kernel_size=None,
                 **kwargs):
        self.encoder = encoder
        self.erosion_kernel = None
        if erosion_kernel_size is not None:
            self.erosion_kernel = cv2.getStructuringElement(
                cv2.MORPH_ELLIPSE, (erosion_kernel_size, erosion_kernel_size))
        self.blur_size = blur_size
        self.seamless_clone = seamless_clone
        self.mask_type = mask_type.lower()
    def patch_image(self, image, face_detected):

        size = 64
        image_size = image.shape[1], image.shape[0]
        # get face alignment matrix
        mat = np.array(get_align_mat(face_detected)).reshape(2, 3) * size
        # perform affine transformation to 
        # transform face as per alignment matrix
        new_face = self.get_new_face(image, mat, size)
        # get face mask matrix
        image_mask = self.get_image_mask(image, new_face, 
                                         face_detected, mat, 
                                         image_size)

        return self.apply_new_face(image,
                                   new_face,
                                   image_mask,
                                   mat,
                                   image_size,
                                   size) 

patch_image方法依赖于该类中还定义的几个实用函数,get_new_faceapply_new_faceget_image_mask

 def apply_new_face(self,
                       image,
                       new_face,
                       image_mask,
                       mat,
                       image_size,
                       size):
        base_image = np.copy(image)
        new_image = np.copy(image)
        # perform affine transformation for better match
        cv2.warpAffine(new_face, mat, image_size, new_image,
                       cv2.WARP_INVERSE_MAP, cv2.BORDER_TRANSPARENT)
        outimage = None
        if self.seamless_clone:
            masky, maskx = cv2.transform(np.array([size / 2, size / 2]).reshape(1, 1, 2), cv2.invertAffineTransform(mat)).reshape(2).astype(int)
            outimage = cv2.seamlessClone(new_image.astype(np.uint8), base_image.astype(np.uint8), (image_mask * 255).astype(np.uint8), (masky, maskx), cv2.NORMAL_CLONE)
        else:            
            # apply source face on the target image's mask
            foreground = cv2.multiply(image_mask, 
                                      new_image.astype(float))
            # keep background same 
            background = cv2.multiply(1.0 - image_mask, 
                                      base_image.astype(float))
            # merge foreground and background components
            outimage = cv2.add(foreground, background)
        return outimage
    def get_new_face(self, image, mat, size):
        # function to align input image based on
        # base image face matrix
        face = cv2.warpAffine(image, mat, (size, size))
        face = np.expand_dims(face, 0)
        new_face = self.encoder(face / 255.0)[0]
        return np.clip(new_face * 255, 0, 255).astype(image.dtype)
    def get_image_mask(self, image, new_face, face_detected, mat, image_size):
        # function to get mask/portion of image covered by face
        face_mask = np.zeros(image.shape, dtype=float)
        if 'rect' in self.mask_type:
            face_src = np.ones(new_face.shape, dtype=float)
            cv2.warpAffine(face_src, mat, image_size, face_mask,
                           cv2.WARP_INVERSE_MAP, cv2.BORDER_TRANSPARENT)
        hull_mask = np.zeros(image.shape, dtype=float)
        if 'hull' in self.mask_type:
            hull = cv2.convexHull(np.array(face_detected.landmarksAsXY()).reshape(
                (-1, 2)).astype(int)).flatten().reshape((-1, 2))
            cv2.fillConvexPoly(hull_mask, hull, (1, 1, 1))
        if self.mask_type == 'rect':
            image_mask = face_mask
        elif self.mask_type == 'faceHull':
            image_mask = hull_mask
        else:
            image_mask = ((face_mask * hull_mask))
        # erode masked image to improve blending
        if self.erosion_kernel is not None:
            image_mask = cv2.erode(image_mask, self.erosion_kernel, 
                                   iterations=1)
        # blur masked image to improve blending
        if self.blur_size != 0:
            image_mask = cv2.blur(image_mask, (self.blur_size, 
                                               self.blur_size))
        return image_mask 

该类接收多个参数以改善混合结果。这些参数控制诸如模糊核大小、补丁类型(如矩形或多边形)和侵蚀核大小等方面。该类还接收编码器作为输入。类方法patch_image在实例化期间使用来自 cv2 库的变换函数和我们设置的参数来进行其神奇操作。我们使用以下convert函数来处理每个 A 类型输入面孔并将其转换为 B 类型:

def convert(converter, item,output_dir):
    try:
        (filename, image, faces) = item
        image1 = None
        for idx, face in faces:
            image1 = converter.patch_image(image, face)
        if np.any(image1):
          output_file = output_dir+"/"+str(filename).split("/")[-1]
          cv2.imwrite(str(output_file), image1)
    except Exception as e:
        print('Failed to convert image: {}. Reason: {}'.format(filename, e)) 
Convert class and an inference loop to generate output:
conv_name = "Masked"
swap_model = False
blur_size = 2
seamless_clone = False
mask_type = "facehullandrect"
erosion_kernel_size = None
smooth_mask = True
avg_color_adjust = True
faceswap_converter = Convert(model_swapper(False,autoencoder_A,autoencoder_B),
    blur_size = blur_size,
    seamless_clone = seamless_clone,
    mask_type = mask_type,
    erosion_kernel_size = erosion_kernel_size,
    smooth_mask = smooth_mask,
    avg_color_adjust = avg_color_adjust
)
list_faces=get_list_images_faces('nicolas_face',                                 'nicolas_ref.png',extractor)
for item in list_faces:
    #print(item)
    convert(faceswap_converter, item,'face_swaps_trump') 

生成的输出如下图所示:

图 8.16: 尼古拉斯·凯奇变成唐纳德·特朗普

交换后的输出面孔令人鼓舞,但并不像我们预期的那样无缝。但我们可以看到模型已学会识别和交换面部的正确部分。混合步骤还尝试匹配肤色、面部姿势和其他方面,使结果尽可能真实。

这似乎是一个良好的开始,但留下了很多需要改进的地方。以下是我们设置的几个限制:

  • 交换输出的质量直接与训练的自动编码器的能力相关。由于没有组件来跟踪重建输出的真实性,很难将自动编码器引导到正确的方向。使用 GAN 可能是提供正面反馈以增强整个训练过程的一种可能的增强。

  • 输出有点模糊。这是由于实际输入分辨率和生成的输出(64x64)之间的差异。造成模糊输出的另一个原因是使用 MAE 作为简单的损失函数。研究表明,复合和复杂的损失有助于提高最终的输出质量。

  • 有限的数据集是输出质量受限的另一个原因。我们利用增强技术来解决限制,但这并不能替代更大的数据集。

在本节中,我们从零开始开发了一个人脸交换 deepfake 架构。我们以逐步的方式来理解将尼古拉斯·凯奇与唐纳德·特朗普交换的整体流程中的每个组件和步骤。在下一节中,我们将使用更复杂的设置来尝试不同的操作模式。

本节中的代码基于原始 deepfake 工作以及 Olivier Valery 的代码的简化实现,该代码可在 GitHub 的以下链接找到:github.com/OValery16/swap-face

现在我们已经训练好了自己的人脸交换器,演示了替换模式的操作,我们可以继续进行再现模式。

使用 pix2pix 进行再现

再现是深度假像设置的另一种操作模式。与替换模式相比,它据说更擅长生成可信的虚假内容。在前面的章节中,我们讨论了执行再现的不同技术,即通过关注凝视、表情、嘴巴等。

我们还在第七章使用 GAN 进行风格转移中讨论了图像到图像翻译体系结构。特别是,我们详细讨论了 pix2pix GAN 是一种强大的架构,可以实现成对翻译任务。在本节中,我们将利用 pix2pix GAN 从零开始开发一个人脸再现设置。我们将努力构建一个网络,我们可以使用我们自己的面部、嘴巴和表情来控制巴拉克·奥巴马(前美国总统)的面部。我们将逐步进行每一步,从准备数据集开始,到定义 pix2pix 架构,最后生成输出再现。让我们开始吧。

数据集准备

我们将使用 pix2pix GAN 作为我们当前再现任务的骨干网络。虽然 pix2pix 是一个训练样本非常少的强大网络,但有一个限制,即需要训练样本成对出现。在本节中,我们将利用这个限制来达到我们的目的。

由于目标是分析目标面孔并使用源面孔进行控制,我们可以利用不同面孔之间的共同之处来为我们的用例开发数据集。不同面孔之间的共同特征是面部地标的存在和位置。在关键特征集部分,我们讨论了如何使用诸如 dlib、cv2 和 MTCNN 等库构建简单易用的面部地标检测模块。

对于我们当前的用例,我们将准备成对的训练样本,包括一对地标和其对应的图像/照片。要生成再现内容,我们只需提取源脸部/控制实体的面部地标,然后使用 pix2pix 基于目标人物生成高质量的实际输出。

在我们的情况下,源/控制人格可以是您或任何其他人,而目标人格是巴拉克·奥巴马。

为了准备我们的数据集,我们将从视频中提取每帧的帧和相应的标志。由于我们希望训练我们的网络能够基于标志输入生成高质量的彩色输出图像,我们需要一段 Barack Obama 的视频。您可以从互联网上的各种不同来源下载此视频。请注意,此练习再次仅用于学术和教育目的。请谨慎使用任何视频。

生成一个标志和视频帧的配对数据集是面部标志 部分给出的代码片段的直接应用。为了避免重复,我们将其留给读者作为练习。请注意,本书的代码仓库中提供了完整的代码。我们从 Barack Obama 的一次演讲中生成了近 400 个配对样本。图 8.17 展示了其中的一些样本:

包含照片、不同、人的图片,描述自动生成

图 8.17:由面部标志和相应视频帧组成的配对训练样本

我们可以看到标志如何捕捉头部的位置以及嘴唇、眼睛和其他面部标志的移动。因此,我们几乎可以立即生成配对的训练数据集。现在让我们继续进行网络设置和训练。

Pix2pix GAN 设置和训练

我们在第七章 用 GAN 进行风格转移 中详细讨论了 pix2pix 架构及其子组件和目标函数。在本节中,我们将简要介绍它们以确保完整性。

build_generator, that prepares the generator network:
def build_generator(img_shape,channels=3,num_filters=64):
    # Image input
    input_layer = Input(shape=img_shape)
    # Downsampling
    down_sample_1 = downsample_block(input_layer, 
                                     num_filters, 
                                     batch_normalization=False)
    # rest of the downsampling blocks have batch_normalization=true
    down_sample_2 = downsample_block(down_sample_1, num_filters*2)
    down_sample_3 = downsample_block(down_sample_2, num_filters*4)
    down_sample_4 = downsample_block(down_sample_3, num_filters*8)
    down_sample_5 = downsample_block(down_sample_4, num_filters*8)
    down_sample_6 = downsample_block(down_sample_5, num_filters*8)
    down_sample_7 = downsample_block(down_sample_6, num_filters*8)
    # Upsampling blocks with skip connections
    upsample_1 = upsample_block(down_sample_7, down_sample_6, 
                                               num_filters*8)
    upsample_2 = upsample_block(upsample_1, down_sample_5, 
                                            num_filters*8)
    upsample_3 = upsample_block(upsample_2, down_sample_4, 
                                            num_filters*8)
    upsample_4 = upsample_block(upsample_3, down_sample_3, 
                                            num_filters*8)
    upsample_5 = upsample_block(upsample_4, down_sample_2, 
                                            num_filters*2)
    upsample_6 = upsample_block(upsample_5, down_sample_1, num_filters)
    upsample_7 = UpSampling2D(size=2)(upsample_6)
    output_img = Conv2D(channels, 
                        kernel_size=4, 
                        strides=1, 
                        padding='same', 
                        activation='tanh')(upsample_7)
    return Model(input_layer, output_img) 

请注意,我们正在重用作为第七章 用 GAN 进行风格转移 的一部分准备的实用函数。与具有特定设置的生成器不同,pix2pix 的判别器网络是一个相当简单的实现。我们在以下片段中介绍了判别器网络:

def build_discriminator(img_shape,num_filters=64):
    input_img = Input(shape=img_shape)
    cond_img = Input(shape=img_shape)
    # Concatenate input and conditioning image by channels 
    # as input for discriminator
    combined_input = Concatenate(axis=-1)([input_img, cond_img])
    # First discriminator block does not use batch_normalization
    disc_block_1 = discriminator_block(combined_input, 
                                       num_filters, 
                                       batch_normalization=False)
    disc_block_2 = discriminator_block(disc_block_1, num_filters*2)
    disc_block_3 = discriminator_block(disc_block_2, num_filters*4)
    disc_block_4 = discriminator_block(disc_block_3, num_filters*8)
    output = Conv2D(1, kernel_size=4, strides=1, padding='same')(disc_block_4)
    return Model([input_img, cond_img], output) 
discriminator network uses repeating blocks consisting of convolutional, LeakyReLU, and batch normalization layers. The output is a *patch-GAN* kind of setup that divides the whole output into several overlapping patches to calculate fake versus real. The patch-GAN ensures high-quality outputs that feel more realistic.

我们使用这两个函数来准备我们的生成器、判别器和 GAN 网络对象。对象的创建如下片段所示:

IMG_WIDTH = 256
IMG_HEIGHT = 256
discriminator = build_discriminator(img_shape=(IMG_HEIGHT,IMG_WIDTH,3),
                                    num_filters=64)
discriminator.compile(loss='mse',
                      optimizer=Adam(0.0002, 0.5),
                      metrics=['accuracy'])
generator = build_generator(img_shape=(IMG_HEIGHT,IMG_WIDTH,3),
                            channels=3,
                            num_filters=64)
source_img = Input(shape=(IMG_HEIGHT,IMG_WIDTH,3))
cond_img = Input(shape=(IMG_HEIGHT,IMG_WIDTH,3))
fake_img = generator(cond_img)
discriminator.trainable = False
output = discriminator([fake_img, cond_img])
gan = Model(inputs=[source_img, cond_img], outputs=[output, fake_img])
gan.compile(loss=['mse', 'mae'],
            loss_weights=[1, 100],
            optimizer=Adam(0.0002, 0.5)) 

训练循环很简单;我们利用三个网络对象(判别器、生成器和整体 GAN 模型),并交替训练生成器和判别器。请注意,面部标志数据集用作输入,而视频帧则是此训练过程的输出。训练循环如下片段所示:

def train(generator, 
          discriminator, 
          gan, 
          patch_gan_shape, 
          epochs,
          path='/content/data',
          batch_size=1, 
          sample_interval=50):

    # Ground truth shape/Patch-GAN outputs
    real_y = np.ones((batch_size,) + patch_gan_shape)
    fake_y = np.zeros((batch_size,) + patch_gan_shape)
    for epoch in range(epochs):
      print("Epoch={}".format(epoch))
      for idx, (imgs_source, imgs_cond) in enumerate(batch_generator(path=path,
          batch_size=batch_size,
          img_res=[IMG_HEIGHT, IMG_WIDTH])):
            # train discriminator
            # generator generates outputs based on 
            # conditioned input images
            fake_imgs = generator.predict([imgs_cond])
            # calculate discriminator loss on real samples
            disc_loss_real = discriminator.train_on_batch([imgs_source, 
                                                           imgs_cond], 
                                                           real_y)
            # calculate discriminator loss on fake samples
            disc_loss_fake = discriminator.train_on_batch([fake_imgs, 
                                                           imgs_cond], 
                                                           fake_y)
            # overall discriminator loss
            discriminator_loss = 0.5 * np.add(disc_loss_real, disc_loss_fake)
            # train generator
            gen_loss = gan.train_on_batch([imgs_source, imgs_cond], [real_y, imgs_source])
            # training updates every 50 iterations
            if idx % 50 == 0:
              print ("[Epoch {}/{}] [Discriminator loss: {}, accuracy: {}] [Generator loss: {}]".format(epoch, epochs, 
                                        discriminator_loss[0], 
                                        100*discriminator_loss[1],
                                        gen_loss[0]))
            # Plot and Save progress every few iterations
            if idx % sample_interval == 0:
              plot_sample_images(generator=generator,
                                 path=path,
                                 epoch=epoch,
                                 batch_num=idx,
                                 output_dir='images') 
with paired training examples. Pix2pix is a highly optimized GAN which requires very few resources overall. With only 400 samples and 200 epochs, we trained our landmarks-to-video frame GAN. 

图 8.188.19 展示了此设置的训练进度:

图 8.18:用于面部再现的 pix2pix GAN 的训练进度(第 1 个纪元)

图 8.19:用于面部再现的 pix2pix GAN 的训练进度(第 40 个纪元)

正如我们在前面的图中所看到的,模型能够捕捉关键的面部特征及其位置,以及背景细节。在初始迭代中(图 8.18),模型似乎在生成嘴部区域方面遇到了困难,但随着训练的进行,它学会了用正确的一组细节填充它(图 8.19)。

现在我们已经为所需任务训练了我们的 GAN,让我们在下一节中进行一些再现。

结果和局限性

到目前为止,我们在本章中主要处理图像或照片作为输入。由于 pix2pix GAN 是一个非常高效的实现,它可以用来在几乎实时地生成输出。因此,这种能力意味着我们可以使用这样一个训练好的模型来使用实时视频来进行再现。换句话说,我们可以使用自己的实时视频来再现巴拉克·奥巴马的面部动作和表情。

get_landmarks and get_obama functions:
CROP_SIZE = 256
DOWNSAMPLE_RATIO = 4
def get_landmarks(black_image,gray,faces):
    for face in faces:
        detected_landmarks = predictor(gray, face).parts()
        landmarks = [[p.x * DOWNSAMPLE_RATIO, p.y * DOWNSAMPLE_RATIO] for p in detected_landmarks]
        jaw = reshape_for_polyline(landmarks[0:17])
        left_eyebrow = reshape_for_polyline(landmarks[22:27])
        right_eyebrow = reshape_for_polyline(landmarks[17:22])
        nose_bridge = reshape_for_polyline(landmarks[27:31])
        lower_nose = reshape_for_polyline(landmarks[30:35])
        left_eye = reshape_for_polyline(landmarks[42:48])
        right_eye = reshape_for_polyline(landmarks[36:42])
        outer_lip = reshape_for_polyline(landmarks[48:60])
        inner_lip = reshape_for_polyline(landmarks[60:68])
        color = (255, 255, 255)
        thickness = 3
        cv2.polylines(black_image, [jaw], False, color, thickness)
        cv2.polylines(black_image, [left_eyebrow], False, color, 
                      thickness)
        cv2.polylines(black_image, [right_eyebrow], False, color, 
                      thickness)
        cv2.polylines(black_image, [nose_bridge], False, color, 
                      thickness)
        cv2.polylines(black_image, [lower_nose], True, color, 
                      thickness)
        cv2.polylines(black_image, [left_eye], True, color, thickness)
        cv2.polylines(black_image, [right_eye], True, color, thickness)
        cv2.polylines(black_image, [outer_lip], True, color, thickness)
        cv2.polylines(black_image, [inner_lip], True, color, thickness)
    return black_image
def get_obama(landmarks):
    landmarks = (landmarks/127.5)-1
    landmarks = tf.image.resize(landmarks, [256,256]).numpy()
    fake_imgs = generator.predict(np.expand_dims(landmarks,axis=0))
    return fake_imgs 

这些函数帮助从给定帧中提取并绘制面部标志,并使用这些标志来使用 pix2pix GAN 生成彩色帧的输出。

下一步是使用这些函数处理实时视频并生成再现的输出样本。这种操作足够快速,以增强虚假内容的可信度。以下代码段展示了操作循环:

cap = cv2.VideoCapture(0)
fps = video.FPS().start()
k = 0
display_plots = True
display_cv2 = True
while True:
    k += 1
    ret, frame = cap.read(0)
    if np.all(np.array(frame.shape)):
        frame_resize = cv2.resize(frame, None, fx=1 / DOWNSAMPLE_RATIO, fy=1 / DOWNSAMPLE_RATIO)
        gray = cv2.cvtColor(frame_resize, cv2.COLOR_BGR2GRAY)
        faces = detector(gray, 1)
        black_image = np.zeros(frame.shape, np.uint8)
        landmarks = get_landmarks(black_image.copy(),gray,faces)
        img_tgt = (landmarks/127.5)-1
        img_tgt = tf.image.resize(img_tgt, [256,256]).numpy()
        obama = generator.predict(np.expand_dims(img_tgt,axis=0))[0]
        try:
            obama = 0.5 * obama + 0.5
            gen_imgs = np.concatenate([np.expand_dims(cv2.cvtColor(rescale_frame(frame_resize), cv2.COLOR_RGB2BGR),axis=0), 
                     np.expand_dims(rescale_frame(obama),axis=0), 
                     np.expand_dims(rescale_frame(landmarks),axis=0)])
            if display_plots:
                titles = ['Live', 'Generated', 'Landmarks']
                rows, cols = 1, 3
                fig, axs = plt.subplots(rows, cols)
                for j in range(cols):
                    if j!=1:
                        axs[j].imshow(gen_imgs[j].astype(int))
                    else:
                        axs[j].imshow(gen_imgs[j])
                    axs[j].set_title(titles[j])
                    axs[j].axis('off')
                plt.show()
            if display_cv2:
                cv2.imshow('synthetic obama', cv2.cvtColor(gen_imgs[1], cv2.COLOR_BGR2RGB))
                #cv2.imshow('landmark', rescale_frame(landmarks))
        except Exception as ex:
            print(ex)
        fps.update()
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
fps.stop()
print('[INFO] elapsed time (total): {:.2f}'.format(fps.elapsed()))
print('[INFO] approx. FPS: {:.2f}'.format(fps.fps()))
cap.release()
cv2.destroyAllWindows() 
using the pix2pix GAN. Upon executing the video capture and manipulation loop, we are able to generate some promising results. Some of the re-enactments are depicted in the following figure:

图 8.20: 使用实时视频作为源,奥巴马作为目标,使用 pix2pix GAN 进行再现

图 8.20展示了整体设置如何无缝工作。我们能够捕捉到实时视频,将其转换为面部标志,然后使用 pix2pix GAN 生成再现。在实时视频中,背景中没有物体,但我们的网络能够正确生成美国国旗。样本还展示了模型如何很好地捕捉表情和头部倾斜。

尽管结果令人鼓舞,但远远不能被认为是真实或可信的。以下是我们在本节讨论的方法中所涉及的一些限制:

  • 图 8.20中的输出有点模糊。如果头部倾斜得太多,或者直播视频中的人离摄像头太近或太远,它们会完全变成空白或难以理解。这个问题主要是因为 pix2pix GAN 学会了相对大小和位置的面部标志物,相对于训练数据集。通过进行面部对齐并在输入和推理阶段使用更紧凑的裁剪,可以改善这一问题。

  • 模型生成的内容高度依赖于训练数据。由于我们的训练数据集来自演讲,因此头部移动有限,面部表情也非常有限。因此,如果你试图移动头部太多或展示训练数据集中不存在的表情,模型会做出非常糟糕的猜测。更大的数据集和更多的变化可以帮助解决这个问题。

我们已经看到了一个强大的图像到图像翻译 GAN 架构可以被重新用于再现任务。

在前两节中,我们介绍了一些有趣的从零开始开发替换和再现架构的实际练习。我们讨论了我们的设置中的一些问题以及如何改进它们。在接下来的一节中,我们将讨论与深度伪造系统相关的一些挑战。

挑战

在本节中,我们将讨论与深度伪造架构相关的一些常见挑战,首先简要讨论与这项技术相关的道德问题。

道德问题

尽管生成虚假内容并不是一个新概念,但“deepfake”一词在 2017 年成为众所关注的焦点,那时 Reddit 用户 u/deepfakes 发布了用深度学习将名人面孔叠加在色情视频上的虚假视频。这些视频的质量和用户能够轻松生成它们的方式在全球新闻频道上掀起了轩然大波。很快,u/deepfakes 发布了一个名为FakeApp的易于设置的应用程序,使用户能够在对深度学习工作原理几乎一无所知的情况下生成此类内容。这导致了大量虚假视频和令人反感的内容。结果,人们开始对涉及身份盗用、冒充、假新闻等问题产生关注。

很快,学术界对此产生了浓厚兴趣,这不仅有助于改善技术,还坚持其道德使用。尽管一些恶意和令人反感的内容创作者利用这些技术,但也有许多工业和研究项目正在进行中,以检测此类虚假内容,如微软的深度伪造检测工具和 Deepware。^(14 15)

技术挑战

尽管道德问题暂且不提,让我们也讨论一下典型深度伪造设置中明显存在的一些挑战:泛化、遮挡和时间问题。

泛化

深度伪造架构本质上是生成式模型,高度依赖于使用的训练数据集。这些架构通常也需要大量的训练样本,这可能很难获得,特别是对于目标(或在恶意使用的情况下的受害者)而言。另一个问题是配对的训练设置。通常针对一个源和目标配对训练的模型不易用于另一对源和目标人物。

研究的一个活跃领域是致力于全新高效架构的开发,这些架构能够使用更少的训练数据。CycleGAN 和其他无配对翻译架构的发展也有助于克服配对训练的瓶颈。

遮挡

源或目标输入可能围绕它们存在妨碍某些特征的残留物。这可能是由于手部运动、头发、眼镜或其他物体造成的。另一种遮挡发生在口部和眼部区域的动态变化上。这可以导致不一致的面部特征或奇怪的裁剪图像。某些作品正在致力于通过使用分割、修补和其他相关技术来避免这些问题。其中一项作品的例子是 Siarohin 等人的First Order Motion Model for Image Generation¹⁶

时间问题

深度伪造架构基于逐帧处理(对于视频输入)。这导致了视频帧之间的抖动、闪烁,或者完全不连贯。我们在上一节使用 pix2pix GAN 进行再现练习时看到了一个例子。该模型无法为未见过的场景生成连贯的输出。为了改进这一点,一些研究人员正尝试使用带有 GANs 的 RNNs(循环神经网络)来生成连贯的输出。这方面的例子包括:

  • MoCoGAN:分解运动和内容以进行视频生成¹⁷

  • Video-to-Video Synthesis¹⁸

现成的实现

在本章中,我们介绍了一种逐步开发用于替换和再现的两种不同深度伪造架构的方法。尽管这些实现易于理解和执行,但它们需要相当多的理解和资源来生成高质量的结果。

自 2017 年发布 u/deepfakes 内容以来,已经推出了许多开源实现以简化这项技术的使用。尽管危险,这些项目大多强调了道德意义和警告开发人员以及普通用户不要恶意采用这些项目。虽然这超出了本章的范围,但我们在本节列举了一些设计良好并受欢迎的实现。鼓励读者查看特定项目以获取更多细节。

  • FaceSwap¹⁹ 该项目的开发人员声称这一实现接近 u/deepfakes 的原始实现,并经过多年的改进以提高输出内容质量。该项目提供了详细的文档和逐步指南,用于准备训练数据集和生成虚假内容。他们还分享了用于加速培训过程的预训练网络。该项目拥有一个图形界面,适合完全新手用户。

  • DeepFaceLab²⁰ 这是互联网上最全面、详细和受欢迎的深伪造项目之一。该项目基于 2020 年 5 月提出的同名论文。该项目包括详细的用户指南、视频教程、非常成熟的 GUI、预训练模型、Colab 笔记本、数据集,甚至是用于快速部署的 Docker 镜像。

  • FaceSwap-GAN²¹ 采用了一种简单而有效的技术,使用了 ED+GAN 设置。该项目提供了用于快速训练自己模型的实用程序和现成的笔记本。该项目还提供了预训练模型,供直接使用或迁移学习。

有许多 Android 和 iOS 应用程序都以同样的方式工作,并将入门门槛降到最低。如今,几乎任何拥有智能手机或一点技术概念的人都可以轻松使用或训练这些设置。

摘要

Deepfakes在伦理和技术上都是一个复杂的课题。在本章中,我们首先讨论了 deepfake 技术总体情况。我们概述了 deepfakes 的内容以及简要介绍了一些有益的和恶意的使用案例。我们详细讨论了不同 deepfake 设置的不同操作模式以及这些操作模式如何影响生成内容的整体可信度。虽然 deepfakes 是一个与视频、图像、音频、文本等等相关的全面术语,但在本章中我们只关注视觉使用案例。

在我们的范围内,我们讨论了在这一领域中不同作品中利用的各种特征集。特别是,我们讨论了面部表情编码系统(FACS)、3D 可塑模型(3DMM)和面部标志。我们还讨论了如何使用诸如 dlib 和 MTCNN 之类的库进行面部标志检测。然后,我们介绍了一种要在 deepfakes 管道中执行的高级任务流程。与此同时,我们还讨论了一些开发这种系统所广泛使用的常见架构。

本章的第二部分利用这一理解,提出了两个实际操作的练习,从零开始开发 deepfake 管道。我们首先致力于开发基于自动编码器的人脸交换架构。通过这项练习,我们逐步进行了准备数据集、训练网络,并最终生成交换输出的步骤。第二个练习涉及使用 pix2pix GAN 执行重新演绎,将实时视频用作源,将巴拉克·奥巴马用作目标。我们讨论了每个实施的问题以及克服这些问题的方法。

在最后一节中,我们提出了有关 deepfake 架构相关的伦理问题和挑战的讨论。我们还简要介绍了一些流行的现成项目,这些项目允许任何拥有计算机或智能手机的人生成虚假内容。

我们在本章中涵盖了很多内容,并且涉及一些非常令人兴奋的使用案例。重要的是要再次强调,当我们使用这样强大的技术时,要非常小心。所涉及的影响和后果对涉及的实体可能非常危险,因此我们应该注意如何使用这些知识。

虽然本章主要关注视觉方面,但我们将转变方向,接下来的两章将讨论文本内容。自然语言处理领域涌现了一些激动人心的研究和应用案例。我们将重点关注一些开创性的文本生成作品。敬请期待。

参考文献

  1. BuzzFeedVideo. (2018 年 4 月 17 日)。 你绝对不会相信这个视频中奥巴马说了什么!😉 [视频]. YouTube。 www.youtube.com/watch?v=cQ54GDm1eL0&ab_channel=BuzzFeedVideo

  2. Lee, D. (2019 年 5 月 10 日)。 Deepfake 萨尔瓦多·达利与博物馆参观者自拍. The Verge. www.theverge.com/2019/5/10/18540953/salvador-dali-lives-deepfake-museum

  3. Malaria Must Die. (2020 年)。 没有疟疾的世界. Malaria Must Die. malariamustdie.com/

  4. Lyons, K. (2020 年 2 月 18 日)。 一名印度政治家使用 AI 将演讲翻译成其他语言,以吸引更多选民. The Verge. www.theverge.com/2020/2/18/21142782/india-politician-deepfakes-ai-elections

  5. Dietmar, J. (2019 年 5 月 21 日)。 GAN 和深假技术可能会引领时尚业的革命。福布斯。 www.forbes.com/sites/forbestechcouncil/2019/05/21/gans-and-deepfakes-could-revolutionize-the-fashion-industry/?sh=2502d4163d17

  6. Statt, N. (2020 年 8 月 27 日)。 罗纳德·里根在最新的使命召唤:黑色行动冷战预告片中派你去犯战争罪。The Verge。 www.theverge.com/2020/8/27/21403879/call-of-duty-black-ops-cold-war-gamescom-2020-trailer-ronald-reagan

  7. Cole, S. (2017 年 12 月 11 日)。 AI 辅助的深假色情已经出现,我们都完蛋了。Vice。 www.vice.com/en/article/gydydm/gal-gadot-fake-ai-porn

  8. dfaker & czfhhh. (2020 年). df. GitHub 代码库. github.com/dfaker/df

  9. Pumarola, A., Agudo, A., Martinez, A.M., Sanfeliu, A., & Moreno-Noguer, F. (2018 年). GANimation: 来自单个图像的解剖学感知面部动画。ECCV 2018。arxiv.org/abs/1807.09251

  10. Naruniec, J., Helminger, L., Schroers, C., & Weber, R.M. (2020 年). 用于视觉效果的高分辨率神经面部交换。Eurographics 渲染研讨会 2020s3.amazonaws.com/disney-research-data/wp-content/uploads/2020/06/18013325/High-Resolution-Neural-Face-Swapping-for-Visual-Effects.pdf

  11. Geng, Z., Cao, C., & Tulyakov, S. (2019). 3D 引导的细粒度面部操作。arXiv。arxiv.org/abs/1902.08900

  12. Blanz, V., & Vetter, T. (1999). 用于合成 3D 面部的可塑模型。SIGGRAPH '99:第 26 届计算机图形学和交互技术年会。187-194。cseweb.ucsd.edu/~ravir/6998/papers/p187-blanz.pdf

  13. Kaipeng,Z.,Zhang,Z.,Li,Z.和 Qiao,Y. (2016). 使用多任务级联卷积网络进行联合面部检测和对齐。IEEE 信号处理通信(SPL),第 23 卷,第 10 期,pp. 1499-1503,2016。kpzhang93.github.io/MTCNN_face_detection_alignment/

  14. Burt, T., & Horvitz, E. (2020 年 9 月 1 日). 打击虚假信息的新步骤。微软博客。blogs.microsoft.com/on-the-issues/2020/09/01/disinformation-deepfakes-newsguard-video-authenticator/

  15. Deepware。 (2021). Deepware - 使用简单工具扫描和检测深度伪造视频deepware.ai/

  16. Siarohin, A., Lathuiliere, S., Tulyakov, S., Ricci, E., & Sebe, N. (2019). 图像动画的一阶运动模型。NeurIPS 2019。aliaksandrsiarohin.github.io/first-order-model-website/

  17. Tulyakov, S., Liu, M-Y., Yang, X., & Kautz, J. (2017). MoCoGAN:视频生成的运动和内容分解。arXiv。arxiv.org/abs/1707.04993

  18. Wang, T-C., Liu, M-Y., Zhu, J-Y., Liu, G., Tao, A., Kautz, J., Catanzaro, B. (2018). 视频到视频的合成。NeurIPS,2018。arxiv.org/abs/1808.06601

  19. torzdf 和 77 位其他贡献者。 (2021). faceswap。GitHub 仓库。github.com/Deepfakes/faceswap

    1. iperov 和其他 18 位贡献者。 (2021). DeepFaceLab。GitHub 仓库。github.com/iperov/DeepFaceLab
  20. shaoanlu,silky,clarle 和 Ja1r0。 (2019). faceswap-GAN。GitHub 仓库。github.com/shaoanlu/faceswap-GAN

第九章:文本生成方法的崛起

在前几章中,我们讨论了不同的方法和技术来开发和训练生成模型。特别是在第六章“使用 GAN 生成图像”中,我们讨论了生成模型的分类以及介绍了显式和隐式类。在整本书中,我们的重点一直是在视觉空间中开发生成模型,利用图像和视频数据集。深度学习在计算机视觉领域的发展以及易于理解性是引入这样一个专注介绍的主要原因。

然而,在过去几年中,自然语言处理NLP)或文本数据处理受到了极大的关注和研究。文本不只是另一种无结构类型的数据;其背后还有更多东西超出了表面所见。文本数据代表了我们的思想、想法、知识和交流。

在本章和下一章中,我们将专注于理解与 NLP 和文本数据的生成模型相关的概念。我们将在本章中涵盖与文本数据生成模型相关的不同概念、架构和组件,重点关注以下主题:

  • 传统表示文本数据方式的简要概述

  • 分布式表示方法

  • 基于 RNN 的文本生成

  • LSTM 变体和文本卷积

我们将介绍不同架构的内部工作原理和使文本生成用例成为可能的关键贡献。我们还将构建和训练这些架构,以更好地理解它们。读者还应该注意,虽然我们将在第九章“文本生成方法的崛起”和第十章“NLP 2.0:使用 Transformer 生成文本”中深入研究关键贡献和相关细节,但这些模型中的一些非常庞大,无法在常规硬件上进行训练。我们将在必要时利用某些高级 Python 包,以避免复杂性。

本章中呈现的所有代码片段都可以直接在 Google Colab 中运行。由于篇幅原因,未包含依赖项的导入语句,但读者可以参考 GitHub 存储库获取完整的代码:github.com/PacktPublishing/Hands-On-Generative-AI-with-Python-and-TensorFlow-2

在我们深入建模方面之前,让我们先了解如何表示文本数据。

表示文本

语言是我们存在中最复杂的方面之一。我们使用语言来传达我们的思想和选择。每种语言都有一个叫做字母表的字符列表,一个词汇表和一组叫做语法的规则。然而,理解和学习一门语言并不是一项微不足道的任务。语言是复杂的,拥有模糊的语法规则和结构。

文本是语言的一种表达形式,帮助我们交流和分享。这使得它成为研究的完美领域,以扩展人工智能可以实现的范围。文本是一种无结构数据,不能直接被任何已知算法使用。机器学习和深度学习算法通常使用数字、矩阵、向量等进行工作。这又引出了一个问题:我们如何为不同的与语言相关的任务表示文本?

词袋

正如我们之前提到的,每种语言都包括一个定义的字符列表(字母表),这些字符组合在一起形成单词(词汇表)。传统上,词袋BoW)一直是表示文本信息的最流行方法之一。

词袋是将文本转换为向量形式的一种简单灵活的方法。这种转换不仅有助于从原始文本中提取特征,还使其适用于不同的算法和架构。正如其名称所示,词袋表示模型将每个单词作为一种基本的度量单位。词袋模型描述了在给定文本语料库中单词的出现情况。为了构建一个用于表示的词袋模型,我们需要两个主要的东西:

  • 词汇表:从要分析的文本语料库中已知单词的集合。

  • 出现的度量:根据手头的应用/任务选择的东西。例如,计算每个单词的出现次数,称为词频,就是一种度量。

与词袋模型相关的详细讨论超出了本章的范围。我们正在呈现一个高级概述,作为在本章后面引入更复杂主题之前的入门。

词袋模型被称为“袋子”,以突显其简单性和我们忽略出现次数的任何排序的事实。换句话说,词袋模型舍弃了给定文本中单词的任何顺序或结构相关信息。这听起来可能是一个很大的问题,但直到最近,词袋模型仍然是表示文本数据的一种流行和有效的选择。让我们快速看几个例子,了解这种简单方法是如何工作的。

"有人说世界将在火中终结,有人说在冰中。从我尝到的欲望中,我同意那些赞成火的人。"

Fire and Ice by Robert Frost. We'll use these few lines of text to understand how the BoW model works. The following is a step-by-step approach:
  1. 定义词汇表

    首先且最重要的步骤是从我们的语料库中定义一个已知单词列表。为了便于理解和实际原因,我们现在可以忽略大小写和标点符号。因此,词汇或唯一单词为 {some, say, the, world, will, end, in, fire, ice, from, what, i, have, tasted, of, desire, hold, with, those, who, favour}。

    这个词汇表是一个包含 26 个词中的 21 个唯一单词的语料库。

  2. 定义出现的度量

    一旦我们有了词汇集,我们需要定义如何衡量词汇中每个单词的出现次数。正如我们之前提到的,有很多种方法可以做到这一点。这样的一个指标就是简单地检查特定单词是否存在。如果单词不存在,则使用 0,如果存在则使用 1。因此,句子“some say ice”可以得分为:

    • some: 1

    • say: 1

    • the: 0

    • world: 0

    • will: 0

    • end: 0

    • in: 0

    • fire: 0

    • ice: 1

    因此,总向量看起来像[1, 1, 0, 0, 0, 0, 0, 0, 1]。

    多年来已经发展了一些其他指标。最常用的指标是:

    • 词频

    • TF-IDF,如第七章使用 GAN 进行风格转移

    • 哈希化

这些步骤提供了词袋模型如何帮助我们将文本数据表示为数字或向量的高层次概述。诗歌摘录的总体向量表示如下表所示:

表的描述是自动生成的

图 9.1:词袋表示

矩阵中的每一行对应诗歌中的一行,而词汇表中的唯一单词构成了列。因此,每一行就是所考虑文本的向量表示。

改进此方法的结果涉及一些额外的步骤。这些优化与词汇和评分方面有关。管理词汇非常重要;通常,文本语料库的规模会迅速增大。处理词汇的一些常见方法包括:

  • 忽略标点符号

  • 忽略大小写

  • 移除常见单词(或停用词)如 a, an, the, this 等

  • 使用单词的词根形式的方法,如stop代替stopping。词干提取和词形还原是两种这样的方法

  • 处理拼写错误

我们已经讨论了不同的评分方法以及它们如何帮助捕捉某些重要特征。词袋模型简单而有效,是大多数自然语言处理任务的良好起点。然而,它存在一些问题,可以总结如下:

  • 缺失的上下文

    正如我们之前提到的,词袋模型不考虑文本的排序或结构。通过简单地丢弃与排序相关的信息,向量失去了捕捉基础文本使用上下文的机会。例如,“我肯定”和“我怀疑我肯定”这两句话将具有相同的向量表示,然而它们表达了不同的思想。扩展词袋模型以包括 n-gram(连续词组)而不是单个词确实有助于捕捉一些上下文,但在非常有限的范围内。

  • 词汇和稀疏向量

    随着语料库的规模增加,词汇量也在增加。管理词汇量大小所需的步骤需要大量的监督和手动工作。由于这种模型的工作方式,大量的词汇导致非常稀疏的向量。稀疏向量对建模和计算需求(空间和时间)造成问题。激进的修剪和词汇管理步骤在一定程度上确实有所帮助,但也可能导致重要特征的丢失。

在这里,我们讨论了词袋模型如何帮助将文本转换为向量形式,以及这种设置中的一些问题。在下一节,我们将转向一些更多涉及的表示方法,这些方法缓解了一些这些问题。

分布式表示

词袋模型是将单词转换为向量形式的易于理解的方法。这个过程通常被称为向量化。虽然这是一种有用的方法,但在捕获上下文和与稀疏相关的问题方面,词袋模型也有它的局限性。由于深度学习架构正在成为大多数空间的事实上的最先进系统,显而易见的是我们应该在 NLP 任务中也利用它们。除了前面提到的问题,词袋模型的稀疏和大(宽)向量是另一个可以使用神经网络解决的方面。

一种处理稀疏问题的简单替代方案是将每个单词编码为唯一的数字。继续上一节的示例,“有人说冰”,我们可以将“有人”赋值为 1,“说”赋值为 2,“冰”赋值为 3,以此类推。这将导致一个密集的向量,[1, 2, 3]。这是对空间的有效利用,并且我们得到了所有元素都是完整的向量。然而,缺失上下文的限制仍然存在。由于数字是任意的,它们几乎不能单独捕获任何上下文。相反,将数字任意映射到单词并不是非常可解释的。

可解释性是 NLP 任务的重要要求。对于计算机视觉用例,视觉线索足以成为理解模型如何感知或生成输出的良好指标(尽管在那方面的量化也是一个问题,但我们现在可以跳过它)。对于 NLP 任务,由于文本数据首先需要转换为向量,因此重要的是理解这些向量捕获了什么,以及模型如何使用它们。

在接下来的章节中,我们将介绍一些流行的向量化技术,尝试捕捉上下文,同时限制向量的稀疏性。请注意,还有许多其他方法(例如基于 SVD 的方法和共现矩阵)也有助于向量化文本数据。在本节中,我们将只涉及那些有助于理解本章后续内容的方法。

Word2vec

英国牛津词典大约有 60 万个独特的单词,并且每年都在增长。然而,这些单词并非独立的术语;它们彼此之间存在一些关系。word2vec 模型的假设是学习高质量的向量表示,以捕获上下文。这更好地总结了 J.R. 菲斯的著名引文:“你可以通过它搭配的伙伴来认识一个词”。

在他们名为“Vector Space 中单词表示的高效估计”的工作中,Mikolov 等人¹介绍了两种学习大型语料库中单词向量表示的模型。Word2Vec 是这些模型的软件实现,属于学习这些嵌入的迭代方法。与一次性考虑整个语料库不同,这种方法尝试迭代地学习编码每个单词的表示及其上下文。学习词表示作为密集上下文向量的这一概念并不新鲜。这最初是由 Rumelhart 等人于 1990²年提出的。他们展示了神经网络如何学习表示,使类似的单词最终处于相同的聚类中。拥有捕获某种相似性概念的单词向量形式的能力是非常强大的。让我们详细看看 word2vec 模型是如何实现这一点的。

连续词袋 (CBOW) 模型

连续词袋模型是我们在上一节讨论的词袋模型的扩展。该模型的关键方面是上下文窗口。上下文窗口被定义为沿着句子移动的固定大小的滑动窗口。中间的词称为目标,窗口内的左右术语称为上下文术语。CBOW 模型通过给定其上下文术语来预测目标术语。

例如,让我们考虑一句参考句子,“some say the world will end in fire”。如果我们的窗口大小为 4,目标术语为world,那么上下文术语将会是{say, the}和{will, end}。模型的输入是形式为(上下文术语,目标术语)的元组,然后将其通过神经网络学习嵌入向量。

这个过程如下图所示:

图表,图解 自动生成描述

图 9.2:连续词袋模型设置

如前面的图表所示,上下文术语,表示为w[t ± i],被作为输入传递给模型,以预测目标术语,表示为wₜ。CBOW 模型的整体工作可以解释如下:

  1. 对于大小为V的词汇表,定义了大小为C的上下文窗口。C可以是 4、6 或任何其他大小。我们还定义了两个矩阵WW'来生成输入和输出向量。矩阵WVxN,而W'VNxV的维度。N是嵌入向量的大小。

  2. 上下文术语(w[t ± i])和目标术语(y)被转化为独热编码(或标签编码),并且训练数据以元组的形式准备:(w[t ± i]y)。

  3. 我们对上下文向量进行平均以得到v' = Σw[t ± k] / 2C

  4. 最终的输出评分向量 z 是平均向量 v' 和输出矩阵 W' 的点积。

  5. 输出评分向量经过 softmax 函数转换为概率值;也就是说,y' = softmax(z),其中 y' 应该对应词汇表中的一个术语。

  6. 最终的目标是训练神经网络,使得 y' 和实际目标 y 尽可能接近。

作者建议使用诸如交叉熵之类的成本函数来训练网络并学习这样的嵌入。

skip-gram 模型

skip-gram 模型是该论文中用于学习词嵌入的第二个变体。本质上,该模型的工作方式与 CBOW 模型完全相反。换句话说,在 skip-gram 的情况下,我们输入一个词(中心/目标词),预测上下文术语作为模型的输出。让我们用之前的例子进行说明,“some say the world will end in fire”。在这里,我们将用 world 作为输入术语,并训练一个模型以高概率预测 {say, the, will, end},作为上下文术语。

下图显示了 skip-gram 模型;如预期的那样,这是我们在 图 9.2 中讨论的 CBOW 设置的镜像:

图表,图解  自动生成描述

图 9.3:skip-gram 模型设置

skip-gram 模型的逐步工作可以解释如下:

  1. 对于一个大小为 V 的词汇表,定义一个大小为 C 的上下文窗口。C 可以是 4、6 或其他任意大小。我们还定义了两个矩阵 WW',分别用于生成输入向量和输出向量。矩阵 WVxN 的,而 W' 的维度是 NxVN 是嵌入向量的大小。

  2. 生成中心词 x 的独热编码表示。

  3. 通过 xW 的点积来获取 x 的词嵌入表示。嵌入表示为 v = W.x

  4. 我们通过将 W'v 的点积得到输出评分向量 z;也就是说,z = W'.v

  5. 评分向量通过 softmax 层转换为输出概率,生成 y'

  6. 最终的目标是训练神经网络,使得 y' 和实际上的上下文 y 尽可能接近。

在 skip-gram 的情况下,对于任何给定的中心词,我们有多个输入-输出训练对。该模型将所有上下文术语都视为同等重要,无论它们与上下文窗口中的中心词之间的距离如何。这使我们能够使用交叉熵作为成本函数,并假设具有强条件独立性。

为了改善结果并加快训练过程,作者们引入了一些简单但有效的技巧。负采样、噪声对比估计分层 softmax等概念是一些被利用的技术。要详细了解 CBOW 和 skip-gram,请读者阅读 Mikolov 等人引用的文献¹,作者在其中详细解释了每个步骤。

nltk to clean up this dataset and prepare it for the next steps. The text cleanup process is limited to lowercasing, removing special characters, and stop word removal only:
# import statements and code for the function normalize_corpus 
# have been skipped for brevity. See corresponding 
# notebook for details.
cats = ['alt.atheism', 'sci.space']
newsgroups_train = fetch_20newsgroups(subset='train', 
                                      categories=cats,
                                      remove=('headers', 'footers', 
                                              'quotes'))
norm_corpus = normalize_corpus(newsgroups_train.data) 
gensim to train a skip-gram word2vec model:
# tokenize corpus
tokenized_corpus = [nltk.word_tokenize(doc) for doc in norm_corpus]
# Set values for various parameters
embedding_size = 32  # Word vector dimensionality
context_window = 20  # Context window size
min_word_count = 1   # Minimum word count
sample = 1e-3        # Downsample setting for frequent words
sg = 1               # skip-gram model
w2v_model = word2vec.Word2Vec(tokenized_corpus, size=embedding_size, 
                              window=context_window, 
                              min_count =min_word_count,
                              sg=sg, sample=sample, iter=200) 

只需几行代码,我们就可以获得我们词汇表的 word2vec 表示。检查后,我们发现我们的词汇表中有 19,000 个独特单词,并且我们为每个单词都有一个向量表示。以下代码片段显示了如何获得任何单词的向量表示。我们还将演示如何获取与给定单词最相似的单词:

# get word vector
w2v_model.wv['sun'] 
array([ 0.607681, 0.2790227, 0.48256198, 0.41311446, 0.9275479,
       -1.1269532, 0.8191313, 0.03389674, -0.23167856, 0.3170586,
        0.0094937, 0.1252524, -0.5247988, -0.2794391, -0.62564677,
       -0.28145587, -0.70590997, -0.636148, -0.6147065, -0.34033248,
        0.11295943, 0.44503215, -0.37155458, -0.04982868, 0.34405553,
        0.49197063, 0.25858226, 0.354654, 0.00691116, 0.1671375,
        0.51912665,  1.0082873 ], dtype=float32) 
# get similar words
w2v_model.wv.most_similar(positive=['god']) 
[('believe', 0.8401427268981934),
 ('existence', 0.8364629149436951),
 ('exists', 0.8211747407913208),
 ('selfcontradictory', 0.8076522946357727),
 ('gods', 0.7966105937957764),
 ('weak', 0.7965559959411621),
 ('belief', 0.7767481803894043),
 ('disbelieving', 0.7757835388183594),
 ('exist', 0.77425217628479),
 ('interestingly', 0.7742466926574707)] 

前述输出展示了sun这个单词的 32 维向量。我们还展示了与单词god最相似的单词。我们可以清楚地看到,诸如 believe、existence 等单词似乎是最相似的,这是合乎逻辑的,考虑到我们使用的数据集。对于感兴趣的读者,我们在对应的笔记本中展示了使用 TensorBoard 的 3 维向量空间表示。TensorBoard 表示帮助我们直观地理解嵌入空间,以及这些向量是如何相互作用的。

GloVe

word2vec 模型有助于改进各种自然语言处理任务的性能。在相同的动力下,另一个重要的实现叫做 GloVe 也出现了。GloVe 或全局词向量表示于 2014 年由 Pennington 等人发表,旨在改进已知的单词表示技术。³

正如我们所见,word2vec 模型通过考虑词汇表中单词的局部上下文(定义的窗口)来工作。即使这是非常有效的,但还不够完善。单词在不同上下文中可能意味着不同的东西,这要求我们不仅要理解局部上下文,还要理解全局上下文。GloVe 试图在学习单词向量的同时考虑全局上下文。

有一些经典的技术,例如潜在语义分析LSA),这些技术基于矩阵分解,在捕获全局上下文方面做得很好,但在向量数学等方面做得不太好。

GloVe 是一种旨在学习更好的词表示的方法。GloVe 算法包括以下步骤:

  1. 准备一个词共现矩阵X,使得每个元素xᵢ[j]表示单词i在单词j上下文中出现的频率。GloVe 使用了两个固定尺寸的窗口,这有助于捕捉单词之前和之后的上下文。

  2. 共现矩阵X使用衰减因子进行更新,以惩罚上下文中距离较远的术语。衰减因子定义如下:α = 1 / offset,其中 offset 是考虑的单词的距离。

  3. 然后,我们将准备 GloVe 方程如下软约束条件:

    这里,wᵢ是主要单词的向量,wⱼ是上下文单词的向量,bᵢbⱼ是相应的偏差项。

  4. 最后一步是使用前述约束条件来定义成本函数,其定义如下:

    这里,f是一个加权函数,定义如下:

    该论文的作者使用γ = 3/4获得了最佳结果。

类似于 word2vec 模型,GloVe 嵌入也取得了良好的结果,作者展示了 GloVe 胜过 word2vec 的结果。他们将此归因于更好的问题表述和全局上下文的捕捉。

在实践中,这两种模型的性能差不多。由于需要更大的词汇表来获得更好的嵌入(对于 word2vec 和 GloVe),对于大多数实际应用情况,预训练的嵌入是可用且被使用的。

预训练的 GloVe 向量可以通过多个软件包获得,例如spacy。感兴趣的读者可以探索spacy软件包以获得更多详情。

FastText

Word2Vec 和 GloVe 是强大的方法,在将单词编码为向量空间时具有很好的特性。当涉及到获取在词汇表中的单词的向量表示时,这两种技术都能很好地工作,但对于词汇表之外的术语,它们没有明确的答案。

在 word2vec 和 GloVe 方法中,单词是基本单位。这一假设在 FastText 实现中受到挑战和改进。FastText 的单词表示方面基于 2017 年 Bojanowski 等人的论文使用子词信息丰富化词向量。⁴ 该工作将每个单词分解为一组 n-grams。这有助于捕捉和学习字符组合的不同向量表示,与早期技术中的整个单词不同。

例如,如果我们考虑单词“India”和n=3用于 n-gram 设置,则它将将单词分解为{, <in, ind, ndi, dia, ia>}。符号“<”和“>”是特殊字符,用于表示原始单词的开始和结束。这有助于区分,它代表整个单词,和<in,它代表一个 n-gram。这种方法有助于 FastText 生成超出词汇表的术语的嵌入。这可以通过添加和平均所需 n-gram 的向量表示来实现。

FastText 在处理可能有大量新/词汇表之外术语的用例时,被显示明显提高性能。FastText 是由Facebook AI ResearchFAIR)的研究人员开发的,这应该不足为奇,因为在 Facebook 等社交媒体平台上生成的内容是巨大且不断变化的。

随着它的改进,也有一些缺点。由于这种情况下的基本单位是一个 n-gram,因此训练/学习这种表示所需的时间比以前的技术更长。 n-gram 方法还增加了训练这种模型所需的内存量。然而,论文的作者指出,散列技巧在一定程度上有助于控制内存需求。

为了便于理解,让我们再次利用我们熟悉的 Python 库gensim。我们将扩展上一节中 word2vec 模型练习所使用的相同数据集和预处理步骤。以下片段准备了 FastText 模型对象:

# Set values for various parameters
embedding_size = 32    # Word vector dimensionality
context_window = 20    # Context window size
min_word_count = 1   # Minimum word count
sample = 1e-3        # Downsample setting for frequent words
sg = 1               # skip-gram model
ft_model = FastText(tokenized_corpus, size=embedding_size, 
                     window=context_window, min_count = min_word_count, sg=sg, sample=sample, iter=100) 

word2vec 模型无法返回单词"sunny"的矢量表示,因为它不在训练词汇表中。以下片段显示了 FastText 仍能生成矢量表示的方法:

# out of vocabulary
ft_model.wv['sunny'] 
array([-0.16000476, 0.3925578, -0.6220364, -0.14427347, -1.308504,
        0.611941, 1.2834805, 0.5174112, -1.7918613, -0.8964722,
       -0.23748468, -0.81343293, 1.2371198 , 1.0380564, -0.44239333,
        0.20864521, -0.9888209, 0.89212966, -1.1963437, 0.738966,
       -0.60981965, -1.1683533, -0.7930039, 1.0648874, 0.5561004,
       -0.28057176, -0.37946936, 0.02066167, 1.3181996, 0.8494686,
       -0.5021836, -1.0629338], dtype=float32) 

这展示了 FastText 如何改进基于 word2vec 和 GloVe 的表示技术。我们可以轻松处理词汇表之外的术语,同时确保基于上下文的密集表示。

现在让我们利用这个理解来开发文本生成模型。

文本生成和 LSTM 的魔法

在前几节中,我们讨论了不同的表示文本数据的方法,以使其适合不同的自然语言处理算法使用。在本节中,我们将利用这种对文本表示的理解来构建文本生成模型。

到目前为止,我们已经使用由不同种类和组合的层组成的前馈网络构建了模型。这些网络一次处理一个训练示例,这与其他训练样本是独立的。我们说这些样本是独立同分布的,或IID。语言,或文本,有点不同。

正如我们在前几节中讨论的,单词根据它们被使用的上下文而改变它们的含义。换句话说,如果我们要开发和训练一种语言生成模型,我们必须确保模型理解其输入的上下文。

循环神经网络RNNs)是一类允许先前输出用作输入的神经网络,同时具有记忆或隐藏单元。对先前输入的意识有助于捕捉上下文,并使我们能够处理可变长度的输入序列(句子很少长度相同)。下图显示了典型的 RNN,既实际形式又展开形式:

Diagram  Description automatically generated

图 9.4:一个典型的 RNN

图 9.4所示,在时间 t₁,输入 x₁ 生成输出 y₁。在时间 t₂x₂y₁(前一个输出)一起生成输出 y₂,以此类推。与 typcial feedforward 网络不同,其中的每个输入都独立于其他输入,RNN 引入了前面的输出对当前和将来的输出的影响。

RNN 还有一些不同的变体,即门控循环单元GRUs)和长短期记忆LSTMs)。之前描述的原始 RNN 在自回归环境中工作良好。但是,它在处理更长上下文窗口(梯度消失)时存在问题。GRUs 和 LSTMs 通过使用不同的门和记忆单元来尝试克服此类问题。LSTMs 由 Hochreiter 和 Schmidhuber 于 1997 年引入,可以记住非常长的序列数据中的信息。LSTMs 由称为输入、输出和遗忘门的三个门组成。以下图表显示了这一点。

图 9.5:LSTM 单元的不同门

有关 LSTMs 的详细了解,请参阅colah.github.io/posts/2015-08-Understanding-LSTMs/

现在,我们将重点介绍更正式地定义文本生成任务。

语言建模

基于 NLP 的解决方案非常有效,我们可以在我们周围看到它们的应用。最突出的例子是手机键盘上的自动完成功能,搜索引擎(Google,Bing 等)甚至文字处理软件(如 MS Word)。

自动完成是一个正式概念称为语言建模的常见名称。简单来说,语言模型以某些文本作为输入上下文,以生成下一组单词作为输出。这很有趣,因为语言模型试图理解输入的上下文,语言结构和规则,以预测下一个单词。我们经常在搜索引擎,聊天平台,电子邮件等上使用它作为文本完成工具。语言模型是 NLP 的一个完美实际应用,并展示了 RNN 的威力。在本节中,我们将致力于建立对 RNN 基于语言模型的文本生成的理解以及训练。

让我们从理解生成训练数据集的过程开始。我们可以使用下面的图像来做到这一点。该图像描绘了一个基于单词的语言模型,即以单词为基本单位的模型。在同样的思路下,我们可以开发基于字符,基于短语甚至基于文档的模型:

Timeline  Description automatically generated

图 9.6:用于语言模型的训练数据生成过程

正如我们之前提到的,语言模型通过上下文来生成接下来的单词。这个上下文也被称为一个滑动窗口,它在输入的句子中从左到右(从右到左对于从右往左书写的语言)移动。 图 9.6中的滑动窗口跨越三个单词,作为输入。每个训练数据点的对应输出是窗口后面紧跟的下一个单词(或一组单词,如果目标是预测下一个短语)。因此,我们准备我们的训练数据集,其中包括({上下文词汇}, 下一个单词)这种形式的元组。滑动窗口帮助我们从训练数据集中的每个句子中生成大量的训练样本,而无需显式标记。

然后使用这个训练数据集来训练基于 RNN 的语言模型。在实践中,我们通常使用 LSTM 或 GRU 单元来代替普通的 RNN 单元。我们之前讨论过 RNN 具有自动回归到先前时间步的数值的能力。在语言模型的上下文中,我们自动回归到上下文词汇,模型产生相应的下一个单词。然后我们利用时间反向传播BPTT)通过梯度下降来更新模型权重,直到达到所需的性能。我们在 第三章深度神经网络的构建模块中详细讨论了 BPTT。

现在我们对语言模型以及准备训练数据集和模型设置所涉及的步骤有了一定的了解。现在让我们利用 TensorFlow 和 Keras 来实现其中一些概念。

实践操作:字符级语言模型

我们在之前的部分中讨论了语言建模的基础知识。在这一部分中,我们将构建并训练自己的语言模型,但是有一点不同。与之前部分的讨论相反,在这里,我们将在字符级别而不是词级别工作。简单来说,我们将着手构建一个模型,将少量字符作为输入(上下文)来生成接下来的一组字符。选择更细粒度的语言模型是为了便于训练这样的模型。字符级语言模型需要考虑的词汇量(或独特字符的数量)要比词级语言模型少得多。

为了构建我们的语言模型,第一步是获取一个数据集用作训练的来源。古腾堡计划是一项志愿者工作,旨在数字化历史著作并提供免费下载。由于我们需要大量数据来训练语言模型,我们将选择其中最大的一本书,列夫·托尔斯泰的 战争与和平。该书可在以下网址下载:

www.gutenberg.org/ebooks/2600

以下代码片段载入了作为我们源数据集的书籍内容:

datafile_path = r'warpeace_2600-0.txt'
# Load the text file
text = open(datafile_path, 'rb').read().decode(encoding='utf-8')
print ('Book contains a total of {} characters'.format(len(text))) 
Book contains a total of 3293673 characters 
vocab = sorted(set(text))
print ('{} unique characters'.format(len(vocab))) 
108 unique characters 

下一步是准备我们的数据集用于模型。正如我们在表示文本部分讨论的那样,文本数据被转换为向量,使用词表示模型。一种方法是首先将它们转换为独热编码向量,然后使用诸如 word2vec 之类的模型将其转换为密集表示。另一种方法是首先将它们转换为任意数值表示,然后在 RNN-based 语言模型的其余部分中训练嵌入层。在这种情况下,我们使用了后一种方法,即在模型的其余部分一起训练一个嵌入层。

下面的代码片段准备了单个字符到整数映射的映射:

char2idx = {u:i for i, u in enumerate(vocab)}
idx2char = np.array(vocab)
text_as_int = np.array([char2idx[c] for c in text])
print('{')
for char,_ in zip(char2idx, range(20)):
    print('  {:4s}: {:3d},'.format(repr(char), char2idx[char]))
print('  ...\n}') 
{
  '\n':   0,
  '\r':   1,
  ' ' :   2,
  '!' :   3,
... 

如你所见,每个唯一的字符都映射到一个整数;例如,\n映射为 0,!映射为 3,依此类推。

为了最佳的内存利用,我们可以利用tf.data API 将我们的数据切片为可管理的片段。我们将我们的输入序列限制在 100 个字符长度,并且这个 API 帮助我们创建这个数据集的连续片段。这在下面的代码片段中展示:

seq_length = 100
examples_per_epoch = len(text)//(seq_length+1)
# Create training examples / targets
char_dataset = tf.data.Dataset.from_tensor_slices(text_as_int)
for i in char_dataset.take(10):
    print(idx2char[i.numpy()]) 
B
O
O
K

O
N
E
... 
split_input_target to prepare the target output as a one-position-shifted transformation of the input itself. In this way, we will be able to generate consecutive (input, output) training pairs using just a single shift in position:
def split_input_target(chunk):
    """
    Utility which takes a chunk of input text and target 
    as one position shifted form of input chunk.
    Parameters:
        chunk: input list of words
    Returns:
        Tuple-> input_text(i.e. chunk minus 
        last word),target_text(input chunk minus the first word)
    """
    input_text = chunk[:-1]
    target_text = chunk[1:]
    return input_text, target_text
dataset = sequences.map(split_input_target)
for input_example, target_example in  dataset.take(1):
    print ('Input data: ', repr(''.join(idx2char[input_example.numpy()])))
    print ('Target data:', repr(''.join(idx2char[target_example.numpy()]))) 
Input data:  '\r\nBOOK ONE: 1805\r\n\r\n\r\n\r\n\r\n\r\nCHAPTER I\r\n\r\n"Well, Prince, so Genoa and Lucca are now just family estat'
Target data: '\nBOOK ONE: 1805\r\n\r\n\r\n\r\n\r\n\r\nCHAPTER I\r\n\r\n"Well, Prince, so Genoa and Lucca are now just family estate' 
build_model that prepares a single layer LSTM-based language model:
def build_model(vocab_size, embedding_dim, rnn_units, batch_size):
    """
    Utility to create a model object.
    Parameters:
        vocab_size: number of unique characters
        embedding_dim: size of embedding vector. 
        This typically in powers of 2, i.e. 64, 128, 256 and so on
        rnn_units: number of GRU units to be used
        batch_size: batch size for training the model
    Returns:
        tf.keras model object
    """
    model = tf.keras.Sequential([
    tf.keras.layers.Embedding(vocab_size, embedding_dim,
                              batch_input_shape=[batch_size, None]),
    tf.keras.layers.LSTM(rnn_units,
                        return_sequences=True,
                        stateful=True,
                        recurrent_initializer='glorot_uniform'),
    tf.keras.layers.Dense(vocab_size)
  ])
    return model
# Length of the vocabulary in chars
vocab_size = len(vocab)
# The embedding dimension
embedding_dim = 256
# Number of RNN units
rnn_units = 1024
model = build_model(
  vocab_size = len(vocab),
  embedding_dim=embedding_dim,
  rnn_units=rnn_units,
  batch_size=BATCH_SIZE) 

我们已经创建了模型对象。从代码片段中可以看出,模型是一堆嵌入层、LSTM 层和稠密层。嵌入层有助于将原始文本转换为向量形式,然后是 LSTM 和稠密层,它们学习上下文和语言语义。

下一组步骤涉及定义损失函数和编译模型。我们将使用稀疏分类交叉熵作为我们的损失函数。下面的代码片段定义了损失函数和编译模型;我们使用 Adam 优化器进行最小化:

def loss(labels, logits):
    return tf.keras.losses.sparse_categorical_crossentropy(labels, logits, from_logits=True)
model.compile(optimizer='adam', loss=loss) 

由于我们使用 TensorFlow 和高级 Keras API,训练模型就像调用fit函数一样简单。我们只训练了 10 个时代,使用ModelCheckpoint回调来保存模型的权重,如下面的代码片段所示:

# Directory where the checkpoints will be saved
checkpoint_dir = r'data/training_checkpoints'
# Name of the checkpoint files
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt_{epoch}")
checkpoint_callback=tf.keras.callbacks.ModelCheckpoint(
    filepath=checkpoint_prefix,
    save_weights_only=True)
EPOCHS = 10
history = model.fit(dataset, epochs=EPOCHS, callbacks=[checkpoint_callback]) 
Epoch 1/10
254/254 [==============================] - 38s 149ms/step - loss: 2.4388
Epoch 2/10
254/254 [==============================] - 36s 142ms/step - loss: 1.7407
.
.
.
Epoch 10/10
254/254 [==============================] - 37s 145ms/step - loss: 1.1530 

恭喜,你已经训练了你的第一个语言模型。现在,我们将使用它来生成一些假文本。在我们做到这一点之前,我们需要了解如何解码我们模型生成的输出。

解码策略

早些时候,我们将所有文本数据转换为适合训练和推理的向量形式。现在我们有了一个训练好的模型,下一步是输入一些上下文词,以及生成下一个词作为输出。这个输出生成步骤正式被称为解码步骤。它被称为"解码",因为模型输出一个向量,必须经过处理才能得到实际的词作为输出。有一些不同的解码技术;让我们简要讨论一下流行的:贪婪解码、束搜索和抽样。

贪婪解码

这是最简单和最快的解码策略。正如其名,贪婪解码是一种在每次预测步骤中选择最高概率项的方法。

尽管这样快速高效,但贪婪会在生成文本时产生一些问题。通过仅关注最高概率的输出,模型可能会生成不一致或不连贯的输出。在字符语言模型的情况下,这甚至可能导致非词典词的输出。贪婪解码还限制了输出的差异性,这也可能导致重复的内容。

波束搜索

波束搜索是广泛使用的贪婪解码的替代方法。该解码策略不是选择最高概率的术语,而是在每个时间步长跟踪n个可能的输出。下图说明了波束搜索解码策略。它展示了从步骤 0 开始形成的多个波束,创建了一个树状结构:

Diagram  Description automatically generated

图 9.7:基于波束搜索的解码策略

图 9.7所示,波束搜索策略通过在每个时间步长跟踪n个预测,并最终选择具有总体最高概率的路径来工作,在图中用粗线突出显示。让我们逐步分析在上述图中使用的波束搜索解码示例,假设波束大小为 2。

在时间步骤t₀

  1. 模型预测以下三个单词(带概率)为(the, 0.3),(when, 0.6)和(and, 0.1)。

  2. 在贪婪解码的情况下,我们将选择"when",因为它的概率最高。

  3. 在这种情况下,由于我们的波束大小为 2,我们将跟踪前两个输出。

在时间步骤t₂

  1. 我们重复相同的步骤;即,我们跟踪两个波束中的前两个输出。

  2. 通过沿着分支计算分支的概率,计算波束分数如下:

    • (when, 0.6) –> (the, 0.4) = 0.60.4 = 0.24*

    • (the, 0.3) –> (war, 0.9) = 0.30.9 = 0.27*

根据上述讨论,生成的最终输出是"It was July, 1805 the war"。与"它是 1805 年 7 月,当时的战争"这样的输出相比,它的最终概率为 0.27,而"它是 1805 年 7 月,当时的"这样的输出的分数为 0.24,这是贪婪解码给我们的结果。

这种解码策略大大改进了我们在前一节讨论的天真贪婪解码策略。这在某种程度上为语言模型提供了额外的能力,以选择最佳的可能结果。

抽样

抽样是一个过程,在此过程中从更大的总体中选择了预定义数量的观察结果。作为对贪婪解码的改进,可以采用随机抽样解码方法来解决变化/重复问题。一般来说,基于抽样的解码策略有助于根据迄今为止的上下文选择下一个词,即:

在这里,wₜ是在时间步t上的输出,已经根据在时间步t-1之前生成的单词进行了条件化。延续我们之前解码策略的示例,以下图像突出显示了基于采样的解码策略将如何选择下一个单词:

图,漏斗图 描述自动生成

图 9.8:基于采样的解码策略

正如图 9.8所示,该方法在每个时间步从给定的条件概率中随机选择一个单词。在我们的示例中,模型最终通过随机选择in,然后选择Paris作为随后的输出。如果你仔细观察,在时间步t₁,模型最终选择了概率最低的单词。这带来了与人类使用语言方式相关的非常必要的随机性。 Holtzman 等人在其题为神经文本退化的奇特案例的作品⁵中通过陈述人类并不总是简单地使用概率最高的单词来提出了这个确切的论点。他们提出了不同的场景和示例,以突显语言是单词的随机选择,而不是由波束搜索或贪婪解码形成的典型高概率曲线。

这将引入一个重要的参数称为温度

温度

正如我们之前讨论的,基于采样的解码策略有助于改善输出的随机性。然而,过多的随机性也不理想,因为它可能导致无意义和不连贯的结果。为了控制这种随机性的程度,我们可以引入一个可调参数称为温度。该参数有助于增加高概率项的概率,同时减少低概率项的概率,从而产生更尖锐的分布。高温度导致更多的随机性,而较低的温度则带来可预测性。值得注意的是,这可以应用于任何解码策略。

Top-k 采样

波束搜索和基于采样的解码策略都有各自的优缺点。Top-k 采样是一种混合策略,它兼具两者的优点,提供了更复杂的解码方法。简单来说,在每个时间步,我们不是选择一个随机单词,而是跟踪前 k 个条目(类似于波束搜索),并在它们之间重新分配概率。这给了模型生成连贯样本的额外机会。

实践操作:解码策略

现在,我们对一些最广泛使用的解码策略有了足够的理解,是时候看看它们的实际效果了。

第一步是准备一个实用函数generate_text,根据给定的解码策略生成下一个单词,如下面的代码片段所示:

def generate_text(model, mode='greedy', context_string='Hello',
   num_generate=1000, 
                   temperature=1.0):
    """
    Utility to generate text given a trained model and context
    Parameters:
        model: tf.keras object trained on a sufficiently sized corpus
        mode: decoding mode. Default is greedy. Other mode is
              sampling (set temperature)
        context_string: input string which acts as context for the                         model
        num_generate: number of characters to be generated
        temperature: parameter to control randomness of outputs
    Returns:
        string : context_string+text_generated
    """
    # vectorizing: convert context string into string indices
    input_eval = [char2idx[s] for s in context_string]
    input_eval = tf.expand_dims(input_eval, 0)
    # String for generated characters
    text_generated = []
    model.reset_states()
    # Loop till required number of characters are generated
    for i in range(num_generate):
        predictions = model(input_eval)
        predictions = tf.squeeze(predictions, 0)
        if mode == 'greedy':
          predicted_id = np.argmax(predictions[0])

        elif mode == 'sampling':
          # temperature helps control the character 
          # returned by the model.
          predictions = predictions / temperature
          # Sampling over a categorical distribution
          predicted_id = tf.random.categorical(predictions, 
                                           num_samples=1)[-1,0].numpy()
        # predicted character acts as input for next step
        input_eval = tf.expand_dims([predicted_id], 0)
        text_generated.append(idx2char[predicted_id])
    return (context_string + ''.join(text_generated)) 

代码首先将原始输入文本转换为整数索引。然后我们使用模型进行预测,根据所选择的模式进行操作,贪婪或采样。我们已经从前面的练习中训练了一个字符语言模型,以及一个辅助工具,帮助我们根据所选择的解码策略生成下一个词。我们在以下片段中使用了这两者来理解使用不同策略生成的不同输出:

# greedy decoding
print(generate_text(model, context_string=u"It was in July, 1805
",num_generate=50,mode="greedy"))
# sampled decoding with different temperature settings
print(generate_text(model, context_string=u"It was in July, 1805
",num_generate=50, mode="sampling", temperature=0.3))
print(generate_text(model, context_string=u"It was in July, 1805
",num_generate=50, mode="sampling",temperature=0.9)) 

使用相同种子与不同解码策略的结果在以下屏幕截图中展示:

图形用户界面,文本,应用程序,信件,电子邮件 说明自动生成

图 9.9:基于不同解码策略的文本生成。粗体文本是种子文本,后面是模型生成的输出文本。

这个输出突显了我们迄今讨论的所有解码策略的一些问题以及显著特征。我们可以看到温度的增加如何使模型更具表现力。我们还可以观察到模型已经学会了配对引号甚至使用标点符号。模型似乎还学会了如何使用大写字母。温度参数的增加表现力是以牺牲模型稳定性为代价的。因此,通常在表现力和稳定性之间存在权衡。

这就是我们生成文本的第一种方法的总结;我们利用了 RNNs(特别是 LSTMs)来使用不同的解码策略生成文本。接下来,我们将看一些 LSTM 模型的变体,以及卷积。

LSTM 变体和文本的卷积

当处理序列数据集时,RNNs 非常有用。我们在前一节中看到,一个简单的模型有效地学会了根据训练数据集学到的内容生成文本。

多年来,我们在对 RNNs 建模和使用的方式方面取得了许多改进。在本节中,我们将讨论前一节中讨论的单层 LSTM 网络的两种广泛使用的变体:堆叠和双向 LSTMs。

堆叠的 LSTMs

我们非常清楚神经网络的深度在处理计算机视觉任务时如何帮助其学习复杂和抽象的概念。同样,一个堆叠的 LSTM 架构,它有多个 LSTMs 层依次堆叠在一起,已经被证明能够带来相当大的改进。堆叠的 LSTMs 首次由格雷夫斯等人在他们的工作中提出使用深度循环神经网络进行语音识别⁶。他们强调了深度-多层 RNNs-与每层单位数目相比,对性能的影响更大。

尽管没有理论证明可以解释这种性能提升,经验结果帮助我们理解影响。这些增强可以归因于模型学习复杂特征甚至输入的抽象表示的能力。由于 LSTM 和 RNNs 一般具有时间成分,更深的网络学习在不同时间尺度上运行的能力。⁷

build_model function to do just that:
def build_model(vocab_size, embedding_dim, rnn_units, batch_size,is_bidirectional=False):
    """
    Utility to create a model object.
    Parameters:
        vocab_size: number of unique characters
        embedding_dim: size of embedding vector. This typically in                        powers of 2, i.e. 64, 128, 256 and so on
        rnn_units: number of LSTM units to be used
        batch_size: batch size for training the model
    Returns:
        tf.keras model object
    """
    model = tf.keras.Sequential()
    model.add(tf.keras.layers.Embedding(vocab_size, embedding_dim,
                              batch_input_shape=[batch_size, None]))
    if is_bidirectional:
      model.add(tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(rnn_units,
                        return_sequences=True,
                        stateful=True,
                        recurrent_initializer='glorot_uniform')))
    else:
      model.add(tf.keras.layers.LSTM(rnn_units,
                        return_sequences=True,
                        stateful=True,
                        recurrent_initializer='glorot_uniform'))
      model.add(tf.keras.layers.LSTM(rnn_units,
                        return_sequences=True,
                        stateful=True,
                        recurrent_initializer='glorot_uniform'))
      model.add(tf.keras.layers.Dense(vocab_size))
    return model 

数据集、训练循环,甚至推理工具保持不变。为了简洁起见,我们跳过了再次展示这些代码摘录。我们不久将讨论我们在这里引入的双向参数。

现在,让我们看看这个更深的基于 LSTM 的语言模型的结果是如何的。下面的截图展示了这个模型的结果:

花朵的近照  自动生成描述

图 9.10: 基于不同解码策略的堆叠 LSTM 的语言模型的文本生成

我们可以清楚地看到,生成的文本如何更好地捕捉到书中的书写风格、大写、标点等方面,比图 9.9中显示的结果更好。这突出了我们讨论过的关于更深的 RNN 结构的一些优势。

双向 LSTM

现在非常广泛使用的第二个变体是双向 LSTM。我们已经讨论过 LSTMs 和 RNNs 生成它们的输出通过利用之前的时间步。当涉及到文本或任何序列数据时,这意味着 LSTM 能够利用过去的上下文来预测未来的时间步。虽然这是一个非常有用的特性,但这并不是我们可以达到的最好水平。让我们通过一个例子来说明为什么这是一个限制:

图形用户界面、文本、应用、PowerPoint  自动生成描述

图 9.11: 查看给定单词的过去和未来上下文窗口

从这个例子可以明显看出,不看目标单词“Teddy”右侧的内容,模型无法正确地获取上下文。为了处理这种情况,引入了双向 LSTM。它们的背后的想法非常简单和直接。双向 LSTM(或者 biLSTM)是两个 LSTM 层同时工作的组合。第一个是通常的前向 LSTM,它按照原始顺序接受输入序列。第二个被称为后向 LSTM,它接受倒置的一份复制作为输入序列。下面的图表展示了一个典型的双向 LSTM 设置:

图表  自动生成描述

图 9.12: 双向 LSTM 设置

图 9.12所示,前向和后向 LSTM 协作处理输入序列的原始和反转副本。由于在任何给定的时间步上有两个 LSTM 单元在不同的上下文上工作,我们需要一种定义输出的方式,以供网络中的下游层使用。输出可以通过求和、乘积、连接,甚至隐藏状态的平均值来组合。不同的深度学习框架可能会设置不同的默认值,但最常用的方法是双向 LSTM 输出的连接。请注意,与双向 LSTM 类似,我们可以使用双向 RNN 或甚至双向 GRU(门控循环单元)。

与普通 LSTM 相比,双向 LSTM 设置具有优势,因为它可以查看未来的上下文。当无法窥视未来时,这种优势也变成了限制。对于当前的文本生成用例,我们利用双向 LSTM 在编码器-解码器类型的架构中。我们利用双向 LSTM 来学习更好地嵌入输入,但解码阶段(我们使用这些嵌入去猜测下一个单词)只使用普通的 LSTM。与早期的实践一样,我们可以使用相同的一套工具来训练这个网络。我们把这留给你来练习;现在我们将继续讨论卷积。

卷积和文本

RNN 在序列对序列任务(例如文本生成)方面非常强大和表现出色。但它们也面临一些挑战:

  1. 当上下文窗口非常宽时,RNN 会受到消失梯度的影响。虽然 LSTM 和 GRU 在一定程度上克服了这一问题,但是与我们在正常用法中看到的非局部交互的典型情况相比,上下文窗口仍然非常小。

  2. RNN 的反复出现使其变得顺序且最终在训练和推断时变得缓慢。

  3. 我们在上一节介绍的架构试图将整个输入语境(或种子文本)编码成单个向量,然后由解码器用于生成下一组单词。这在种子/语境非常长时会受到限制,正如 RNN 更多地关注上下文中最后一组输入的事实一样。

  4. 与其他类型的神经网络架构相比,RNN 具有更大的内存占用空间;也就是说,在实现过程中需要更多的参数和更多的内存。

另一方面,我们有卷积网络,在计算机视觉领域经过了战斗的检验。最先进的架构利用 CNN 提取特征,在不同的视觉任务上表现良好。CNN 的成功使研究人员开始探索它们在自然语言处理任务中的应用。

使用 CNN 处理文本的主要思想是首先尝试创建一组单词的向量表示,而不是单个单词。更正式地说,这个想法是在给定句子中生成每个单词子序列的向量表示。

让我们考虑一个示例句子,“流感爆发迫使学校关闭”。首先的目标将是将这个句子分解为所有可能的子序列,比如“流感爆发迫使”,“爆发迫使学校”,…,“学校关闭”,然后为每个子序列生成一个向量表示。虽然这样的子序列可能或可能不带有太多含义,但它们为我们提供了一种理解不同上下文中的单词以及它们的用法的方式。由于我们已经了解如何准备单词的密集向量表示(参见Distributed representation一节),让我们在此基础上了解如何利用 CNNs。

继续前面的例子,图 9.13(A) 描述了每个单词的向量形式。为了方便理解,向量仅为 4 维:

自动生成的日历描述

图 9.13:(A)示例句子中每个单词的向量表示(1x4)。 (B)两个大小为 3 的内核/过滤器。 (C)取 Hadamard 乘积后的每个内核的维度为 1x2 的短语向量,然后进行步幅为 1 的求和。

两个大小为 3 的内核分别显示在图 9.13(B) 中。在文本/NLP 用例中,内核的选择为单词向量维度的宽度。大小为 3 表示每个内核关注的上下文窗口。由于内核宽度与单词向量宽度相同,我们将内核沿着句子中的单词移动。这种尺寸和单向移动的约束是这些卷积滤波器被称为 1-D 卷积的原因。输出短语向量显示在图 9.13(C) 中。

类似于用于计算机视觉用例的深度卷积神经网络,上述设置也使我们能够为自然语言处理用例堆叠 1-D 卷积层。更大的深度不仅允许模型捕获更复杂的表示,还允许捕获更广泛的上下文窗口(这类似于增加视觉模型的感受野随深度增加)。

使用 CNNs 用于自然语言处理用例还提高了计算速度,同时减少了训练这种网络所需的内存和时间。事实上,这些都是以下研究中探索的一些使用 1-D CNNs 的自然语言处理任务的优势:

  • 自然语言处理(几乎)从零开始,Collobert 等⁸

  • 用于文本分类的字符级卷积网络,Zhang 等⁹

  • 用于句子分类的卷积神经网络,Kim¹⁰

  • 用于文本分类的循环卷积神经网络,Lai 和 Xu 等¹¹

到目前为止,我们已经讨论了 CNNs 如何用于提取特征并为自然语言处理用例捕获更大的上下文。与语言相关的任务,特别是文本生成,与之相关的有一定的时间方面。因此,下一个显而易见的问题是,我们是否可以利用 CNNs 来理解时间特征,就像 RNNs 一样?

研究人员已经探索使用 CNN 进行时间或序贯处理已经有一段时间了。虽然我们讨论了 CNN 如何是捕捉给定单词上下文的好选择,但这对于某些用例来说存在问题。例如,像语言建模/文本生成这样的任务需要模型理解上下文,但只需来自一侧。简单来说,语言模型通过查看已处理的单词(过去上下文)来生成未来单词。但 CNN 也可以覆盖未来的时间步。

从 NLP 领域稍微偏离,Van den Oord 等人关于 PixelCNNs¹²和 WaveNets¹³的作品对于理解 CNN 在时间设置中的应用特别重要。他们提出了因果卷积的概念,以确保 CNN 只利用过去而不是未来上下文。这一概念在下图中得到了突出:

一个链条围栏  自动生成的描述

图 9.14:基于 Van den Oord 等人¹³的 CNN 的因果填充。 图 2

因果卷积确保模型在任何给定的时间步t都会进行p(x[t+1] | x[1:][t])类型的预测,并且不依赖于未来的时间步x[t+1],x[t+2] … x[t+][T],正如 图 9.14所示。在训练过程中,可以并行地为所有时间步进行条件预测;但生成/推理步骤是顺序的;每个时间步的输出都会反馈给模型以进行下一个时间步的预测。

由于这个设置没有任何循环连接,模型训练速度更快,哪怕是更长序列也一样。因果卷积的设置最初来源于图像和音频生成用例,但也已扩展到 NLP 用例。WaveNet 论文的作者此外利用了一个称为dilated convolutions的概念,以提供更大的感知域,而不需要非常深的架构。

这种利用 CNN 捕捉和使用时间组件的想法已经为进一步探索打开了大门。

在我们进入下一章涉及更复杂的注意力和变换器架构之前,有必要强调一些先前的重要作品:

  • Kalchbrenner 等人¹⁴的时间内神经机器翻译介绍了基于编码器-解码器架构的 ByteNet 神经翻译模型。总体设置利用 1-D 因果卷积,以及扩张核,以在英德翻译任务上提供最先进的性能。

  • Dauphin 等人在他们名为具有门控卷积的语言建模的作品中提出了一个基于门控卷积的语言模型。(15) 他们观察到他们的门控卷积提供了显着的训练加速和更低的内存占用。

  • Gehring 等人¹⁶和 Lea 等人¹⁷的作品进一步探讨了这些想法,并提供了更好的结果。

  • 感兴趣的读者还可以探索 Bai 等人的一篇题为基于序列建模的通用卷积和循环网络的实证评估的论文¹⁸。该论文为基于循环神经网络(RNN)和卷积神经网络(CNN)的架构提供了一个很好的概述,用于序列建模任务。

这结束了我们对语言建模旧架构的基本要素的讨论。

总结

祝贺你完成了涉及大量概念的复杂章节。在本章中,我们涵盖了处理文本数据以进行文本生成任务的各种概念。我们首先了解了不同的文本表示模型。我们涵盖了大多数广泛使用的表示模型,从词袋到 word2vec 甚至 FastText。

本章的下一部分重点讨论了发展对基于循环神经网络(RNN)的文本生成模型的理解。我们简要讨论了什么构成了语言模型以及我们如何为这样的任务准备数据集。之后我们训练了一个基于字符的语言模型,生成了一些合成文本样本。我们涉及了不同的解码策略,并用它们来理解我们 RNN 模型的不同输出。我们还深入探讨了一些变种,比如堆叠 LSTM 和双向 LSTM 的语言模型。最后,我们讨论了在 NLP 领域使用卷积网络的情况。

在下一章中,我们将重点关注 NLP 领域一些最新和最强大架构的基本构件,包括注意力和Transformers。

参考文献

  1. Mikolov, T., Chen, K., Corrado, G., & Dean, J. (2013). 词向量的高效估计. arXiv. arxiv.org/abs/1301.3781

  2. Rumelhart, D.E., & McClelland, J.L. (1987). 分布表示, in 并行分布式处理: 认知微结构探索:基础, pp.77-109. MIT Press. web.stanford.edu/~jlmcc/papers/PDP/Chapter3.pdf

  3. Pennington, J., Socher, R., & Manning, C.D. (2014). GloVe: 全局词向量表示. Proceedings of the 2014 Conference on Empirical Methods in Natural Language Processing (EMNLP). nlp.stanford.edu/pubs/glove.pdf

  4. Bojanowski, P., Grave, E., Joulin, A., & Mikolov, T. (2017). 使用子词信息丰富词向量. arXiv. arxiv.org/abs/1607.04606

  5. Holtzman, A., Buys, J., Du, L., Forbes, M., & Choi, Y. (2019). 神经文本退化的好奇案例. arXiv. arxiv.org/abs/1904.09751

  6. Graves, A., Mohamed, A., & Hinton, G. (2013). 深度循环神经网络语音识别. arXiv. arxiv.org/abs/1303.5778

  7. Pascanu, R., Gulcehre, C., Cho, K., & Bengio, Y. (2013). 如何构建深度循环神经网络. arXiv. arxiv.org/abs/1312.6026

  8. Collobert, R., Weston, J., Karlen, M., Kavukcuoglu, K., & Kuksa, P. (2011). 几乎从零开始的自然语言处理. arXiv. arxiv.org/abs/1103.0398

  9. Zhang, X., Zhao, J., & LeCun, Y. (2015). 用于文本分类的字符级卷积网络. arXiv. arxiv.org/abs/1509.01626

  10. Kim, Y. (2014). 用于句子分类的卷积神经网络. arXiv. arxiv.org/abs/1408.5882

  11. Lai, S., Xu, L., Liu, K., & Zhao, J. (2015). 用于文本分类的循环卷积神经网络. 第二十九届 AAAI 人工智能大会论文集。zhengyima.com/my/pdfs/Textrcnn.pdf

  12. van den Oord, A., Kalchbrenner, N., Vinyals, O., Espeholt, L., Graves, A., & Kavukcuoglu, K. (2016). 具有 PixelCNN 解码器的条件图像生成. arXiv. arxiv.org/abs/1606.05328

  13. van den Oord, A., Dieleman, S., Simonyan, K., Vinyals, O., Graves, A., Kalchbrenner, N., Senior, A., Kavukcuoglu, K. (2016). WaveNet:用于原始音频的生成模型. arxiv.org/abs/1609.03499

  14. Kalchbrenner, N., Espeholt, L., Simonyan, K., van den Oord, A., Graves, A., & Kavukcuoglu, K. (2016). 线性时间内的神经机器翻译. arXiv. arxiv.org/abs/1609.03499

  15. Dauphin, Y.N., Fan, A., Auli, M., & Grangier, D. (2016). 带门卷积网络的语言建模. arXiv. arxiv.org/abs/1612.08083

  16. Gehring, J., Auli, M., Grangier, D., Yarats, D., & Dauphin, Y.N. (2017). 卷积序列到序列学习. arXiv. arxiv.org/abs/1705.03122

  17. Lea, C., Flynn, M.D., Vidal, R., Reiter, A., & Hager, G.D. (2016). 用于动作分割和检测的时态卷积网络. arXiv. arxiv.org/abs/1611.05267

  18. Bai, S., Kolter, J.Z., & Koltun, V. (2018). 对序列建模的通用卷积和循环网络的经验评估. arXiv. arxiv.org/abs/1803.01271

第十章:NLP 2.0:使用 Transformers 生成文本

正如我们在上一章中看到的,NLP 领域在我们理解、表示和处理文本数据的方式上取得了一些显著的飞跃。从使用 LSTMs 和 GRUs 处理长距离依赖/序列到使用 word2vec 和相关技术构建密集向量表示,该领域整体上取得了显著的改进。随着词嵌入几乎成为事实上的表示方法,以及 LSTMs 成为 NLP 任务的主力军,我们在进一步增强方面遇到了一些障碍。将嵌入与 LSTM 结合使用的这种设置充分利用了编码器-解码器(以及相关体系结构)风格模型。

我们在上一章中简要地看到了由于基于 CNN 的体系结构在 NLP 用例中的研究和应用而实现的某些改进。在本章中,我们将涉及导致当前最先进的 transformer 架构开发的下一组增强功能。我们将重点关注:

  • 注意力的概述以及 transformers 如何改变 NLP 领域

  • GPT 系列模型,提供基于 GPT-2 的文本生成流程的逐步指南

我们将涵盖注意力、自注意力、上下文嵌入,最后是 transformer 架构等主题。

本章中呈现的所有代码片段都可以直接在 Google Colab 中运行。由于空间原因,未包含依赖项的导入语句,但读者可以参考 GitHub 存储库获取完整的代码:github.com/PacktPublishing/Hands-On-Generative-AI-with-Python-and-TensorFlow-2

让我们首先把注意力转向注意力。

注意力

我们用于准备第一个文本生成语言模型的基于 LSTM 的架构存在一个主要限制。RNN 层(一般来说,可能是 LSTM 或 GRU 等)以定义大小的上下文窗口作为输入,并将其全部编码为单个向量。在解码阶段可以使用它开始生成下一个标记之前,这个瓶颈向量需要在自身中捕获大量信息。

注意力是深度学习空间中最强大的概念之一,真正改变了游戏规则。注意力机制背后的核心思想是在解码阶段使用之前利用 RNN 的所有中间隐藏状态来决定要关注哪个。更正式地表达注意力的方式是:

给定一组值的向量(所有 RNN 的隐藏状态)和一个查询向量(这可能是解码器的状态),注意力是一种计算值的加权和的技术,依赖于查询。

加权和作为隐藏状态(值向量)中包含的信息的选择性摘要,而查询决定了要关注哪些值。注意机制的根源可以在与神经机器翻译NMT)架构相关的研究中找到。NMT 模型特别在对齐问题上遇到困难,而注意力在这方面大大帮助了。例如,从英语翻译成法语的句子可能不是单词一对一匹配的。注意力不仅限于 NMT 用例,而且广泛应用于其他 NLP 任务,如文本生成和分类。

这个想法非常简单,但我们如何实现和使用它呢?图 10.1展示了注意力机制的工作示例。图表展示了时间步t上展开的 RNN。

自动生成的图表描述

图 10.1:带有注意机制的简单 RNN

提到图表,我们来逐步了解注意力是如何计算的:

  1. 让 RNN 编码器隐藏状态表示为h₁h₂…、hₙ,当前输出向量为sₜ

  2. 首先,我们计算时间步t注意分数 eᵗ 如下:

    这一步也被称为对齐步骤。

  3. 然后,我们将这个分数转换成注意力分布

  4. 使用 softmax 函数帮助我们将分数转换为总和为 1 的概率分布。

  5. 最后一步是计算注意力向量aₜ,也称为上下文向量,方法是将编码器隐藏状态加权求和aₜ

一旦我们得到了注意向量,我们就可以简单地将其与先前时间步的解码器状态向量连接起来,并像以前一样继续解码向量。

到目前为止,各种研究人员已经探索了注意机制的不同变体。需要注意的一些重要点包括:

  • 注意计算的前述步骤在所有变体中都相同。

  • 然而,区别在于计算注意力分数(表示为eᵗ)的方式。

广泛使用的注意力评分函数有基于内容的注意力,加法注意力,点积和缩放的点积。鼓励读者进一步探索以更好地了解这些。

上下文嵌入

从基于 BoW 的文本表示模型到无监督的密集表示技术(如 word2vec、GloVe、fastText 等)的大跨越是改善深度学习模型在 NLP 任务上表现的秘密武器。然而,这些表示方法也有一些局限性,我们会提醒自己:

  • 单词的含义取决于使用的上下文。这些技术导致无论上下文如何,都会得到相同的向量表示。虽然可以通过使用非常强大的词义消歧方法(例如使用监督学习算法消除单词歧义)来解决这个问题,但从本质上来讲,这并没有被任何已知技术所捕捉到。

  • 单词可以有不同的用法、语义和句法行为,但单词表示保持不变。

仔细思考一下,我们在上一章中使用 LSTMs 准备的架构正试图在内部解决这些问题。为了进一步阐述,让我们快速回顾一下我们建立的架构:

  • 我们开始将输入文本转换为字符或单词嵌入。

  • 然后,这些嵌入向量通过一个 LSTM 层(或者一组 LSTM 层,甚至是双向 LSTM 层),最终的隐藏状态被转换和解码以生成下一个标记。

虽然起始点利用了预训练嵌入,这些嵌入在每个上下文中具有相同的表示,但 LSTM 层引入了上下文。一组 LSTM 层分析令牌序列,每一层都试图学习与语言句法语义等相关的概念。这为每个令牌(单词或字符)的表示提供了非常重要的上下文。

Peters 等人在 2017 年提出的TagLM架构是第一批提供见解的工作之一,说明了如何将预训练的词嵌入与预训练的神经语言模型结合起来,为下游 NLP 任务生成具有上下文意识的嵌入向量。

改变了 NLP 领域的巨大突破是ELMo,即来自语言模型的嵌入。ELMo 架构由 Peters 等人在他们 2018 年的作品Deep Contextualized Word Representations中提出。不详细展开,ELMo 架构的主要亮点是:

  • 模型使用基于双向 LSTM 的语言模型。

  • Character CNNs 被用来生成嵌入向量,取代了预训练的词向量,这些向量利用了 4096 个 LSTM 单元,但通过前向传播层转换成了更小的 512 大小的向量。

  • 模型利用剩余层来帮助在架构的深层之间传递梯度。这有助于防止梯度消失等问题。

  • 主要创新之处在于利用所有隐藏的双向 LSTM 层来生成输入表示。与以前的作品不同,在以前的作品中,只使用最终的 LSTM 层来获取输入表示,这项工作对所有隐藏层的隐藏状态进行加权平均。这有助于模型学习上下文词嵌入,其中每一层都有助于语法和语义等方面。

ELMo 备受关注的原因并不是它帮助提高了性能,而是 ELMo 学习的上下文嵌入帮助它在以往的架构上改进了最先进的性能,不仅在几个 NLP 任务上,而且几乎所有的任务上(详见论文)。

Howard 和 Ruder 在 2018 年提出的ULMFiT模型基于类似的概念,并帮助推广了在 NLP 领域的迁移学习的广泛应用。³

自注意力

我们已经简要讨论了注意力机制及其对改进 NLP 模型的影响。在本节中,我们将讨论关于注意力机制的后续改进,即自注意力。

自注意力是由程先生等人在他们 2016 年题为用于机器阅读的长短期记忆网络的论文中提出的。⁴ 自注意力概念是建立在注意力的一般思想之上的。自注意力使得模型能够学习当前标记(字符、单词或句子等)与其上下文窗口之间的相关性。换句话说,它是一种注意力机制,相关于给定序列的不同位置,以生成相同序列的表示。可以将其想象为一种将单词嵌入转换为给定句子/序列的方式。原始论文中呈现的自注意力概念如同 图 10.2 所示。

表描述是自动生成的

图 10.2:自注意力⁴

让我们尝试理解这张图中展示的自注意力输出。每一行/句子表示模型在每个时间步的状态,当前单词用红色突出显示。蓝色表示模型的注意力,其集中程度由蓝色的深浅表示。因此,上下文中的每个单词都有一定程度地影响当前单词的嵌入。

感兴趣的读者可以探索 Google Brain 团队制作的一个展示名为 Tensor2Tensor(现已废弃,改用 JAX)的框架的 notebook,这个 notebook 呈现了一个交互式可视化,帮助理解自注意力的概念:colab.research.google.com/github/tensorflow/tensor2tensor/blob/master/tensor2tensor/notebooks/hello_t2t.ipynb

这个概念是我们即将讨论的 Transformer 架构的核心构建模块之一。

Transformer

注意、上下文嵌入和无循环体系结构等概念的结合导致了我们现在所谓的Transformers体系结构的诞生。Transformers体系结构是由瓦斯瓦尼等人于 2017 年在具有里程碑意义的论文注意力就是你所需要的中提出的。⁵ 这项工作代表了自然语言处理领域的完全范式转变;它不仅提出了一个强大的体系结构,还巧妙地利用了一些最近发展的概念,帮助它在不同基准测试中击败了最先进的模型。

我们将简要介绍Transformers体系结构的内部。欲了解逐步说明,请参阅 Jay Alammar 的插图Transformersjalammar.github.io/illustrated-transformer/

在核心部分,Transformers是一种无循环和无卷积的基于注意力的编码器-解码器架构。它完全依赖于注意机制来学习局部和全局依赖关系,从而实现了大规模并行化。现在让我们来看看这项工作的主要贡献。

总体架构

如前所述,Transformers在其核心是一个编码器-解码器架构。但与 NLP 领域已知的编码器-解码器体系结构不同,这项工作呈现了一个堆叠的编码器-解码器设置。

图 10.3 展示了高级Transformers设置。

自动生成的图表描述

图 10.3:Transformers体系结构的高级示意图

如图所示,该架构利用多个堆叠在一起的编码器块。解码器本身由堆叠的解码块组成,并且最后一个编码器块馈送到每个解码器块中。这里要注意的重要一点是,编码器和解码器块都不包含循环或卷积层。图 10.4 (A) 概述了编码器块,而 图 10.4 (B) 概述了解码器块。虚线表示不同层之间的残差连接。原始论文以相同的 6 个编码器块和解码器块呈现了Transformers体系结构。

自动生成的图表描述

图 10.4:A) 编码器块,B) 在Transformers体系结构中使用的解码器块

编码器块,如图 10.4 (A)所示,包括一个用于计算自注意力的层,然后是归一化和前馈层。这些层之间有跳跃连接。解码器块与编码器块几乎相同,只是多了一个由自注意力和归一化层组成的子块。这个额外的子块从最后一个编码器块获取输入,以确保编码器的注意力传播到解码块。

解码器块中的第一层进行了轻微修改。这个多头自注意力层对未来的时间步/上下文进行了屏蔽。这确保了模型在解码当前标记时不会关注目标的未来位置。让我们花点时间来理解多头自注意力组件。

多头自注意力

我们在上一节讨论了自注意力的概念。在本节中,我们将讨论变换器架构如何实现自注意力及其相关参数。在介绍注意力概念时,我们将其描述为查询向量(解码器状态,表示为q)和值向量(编码器的隐藏状态,表示为v)。

对于变换器而言,这有所修改。我们使用编码器状态或输入标记作为查询和值向量(自注意力),以及一个额外的向量称为k向量(表示为k)。在这种情况下,键、值和查询向量的维度相同。

变换器架构使用缩放点积作为其注意力机制。这个评分函数定义如下:

其中注意力输出首先计算为查询向量Q和键向量K(实际上这些是矩阵,但我们稍后会解释)的点积QKᐪ。点积试图捕捉查询与编码器状态的相似性,然后乘以输入向量的维度n的平方根进行缩放。引入这个缩放因子是为了确保梯度能够正确传播,因为对于大的嵌入向量观察到了梯度消失。Softmax 操作将分数转换为总和为 1 的概率分布。最后一步是计算编码器状态(这次是值向量V)的加权和与 Softmax 输出的乘积。整个操作在图 10.5中表示:

图 说明自动生成

图 10.5:(左)缩放点积注意力,(右)多头自注意力,将几个自注意力层并行组合在一起。

在每个编码器块中使用单个注意头的位置,模型使用多个注意头并行进行操作(如 图 10.5(右) 所示)。作者在论文中提到,“多头注意力使模型能够同时关注不同位置的不同表示子空间的信息。使用单个注意头,平均会抑制这种情况。”换句话说,多头注意力使模型能够学习输入中每个单词的不同方面,即,一个注意头可能捕捉与介词的关系的影响,另一个可能专注于它与动词的交互作用,依此类推。由于每个注意头都有自己的 QKV 向量集,实际上这些被实现为矩阵,每行对应于一个特定的头。

这里提供了多头自注意的高度直观的可视化解释,供参考:www.youtube.com/watch?v=-9vVhYEXeyQ&ab_channel=Peltarion

有人可能会认为,由于多头设置,参数数量会突然激增并减慢训练过程。为了抵消这一点,作者们首先将较大的输入嵌入投影到较小的维度向量(大小为 64),然后使用 8 个头在原始实现中进行操作。这导致最终的连接向量(来自所有注意头)与具有较大输入嵌入向量的单个注意头的维度相同。这个巧妙的技巧帮助模型在相同的空间中捕捉更多的语义,而不会对整体训练速度产生影响。整体Transformers架构使用多个这样的编码器块,每个编码器块都包含多头注意层。

位置编码

Transformers模型不包含任何循环或卷积层,因此为了确保模型理解输入序列的重要性,使用了“位置嵌入”的概念。作者选择使用以下方法生成位置编码:

其中 pos 是输入令牌的位置,i 是维度,d[model] 是输入嵌入向量的长度。作者在偶数位置使用正弦,奇数位置使用余弦。位置编码向量的维度与输入向量相同,并且在输入编码器或解码器块之前将两个向量相加。

多头自注意力机制与位置编码的结合帮助Transformers网络构建输入序列的高度上下文表示。这个结合着完全基于注意力机制的架构,使得Transformers不仅能够在许多基准测试中超越现有模型,还能够构建一整个基于Transformers的模型系列。在下一节中,我们将简要介绍这一系列Transformers。

BERT-ology

Transformers架构在自然语言处理领域引入了完全前所未有的性能基准。这种无循环设置导致了对基于Transformers的完整系列架构的研究和开发。最初和最成功的其中之一是 BERT 模型。BERT,或者来自Transformers的双向编码器表示,是由 Google AI 的 Devlin 等人于 2018 年提出的。⁶

该模型在Transformers模型设定的基准上取得了显著的改进。BERT 还通过展示如何对预训练模型进行微调以提供最先进的性能,推动了自然语言处理领域的迁移学习的发展。在计算机视觉用例中,我们可以使用类似 VGG 或 ResNet 这样的大型预训练网络作为特征提取器并带有分类层,或者我们可以对给定任务微调整个网络。我们也可以使用 BERT 来实现相同的功能。

BERT 模型采用具有不同数量的编码器块的Transformers式编码器。作者提出了两个模型,BERT-base 包含 12 个块,BERT-large 包含 24 个块。这两个模型相比原始Transformers设置具有更大的前馈网络(分别为 768 和 1024)和更多的注意力头(分别为 12 和 16)。

与原始Transformers实现的另一个主要变化是双向掩码语言模型目标。典型的语言模型确保因果关系,即解码过程只关注过去的上下文而不是未来的时间步。BERT 的作者调整了这个目标,以从两个方向构建上下文,即 预测掩码词下一个句子预测。这在 图 10.6 中有所描述:

Graphical user interface, text, application  Description automatically generated

图 10.6:BERT 训练目标,包括掩码语言模型和下一个句子预测

如图所示,掩码语言模型随机掩码了训练过程中的 15% 的标记。他们在大规模语料库上训练模型,然后在 GLUE (gluebenchmark.com/) 和其他相关基准上对其进行微调。据报告,该模型在性能上明显优于所有先前的架构。

BERT 的成功导致一系列改进模型,通过调整嵌入、编码器层等方面的某些方面来提供渐进性能提升。像 RoBERTa⁷、ALBERT⁸、DistilBERT⁹、XLNet¹⁰ 等模型共享核心思想并在此基础上提供改进。

由于 BERT 不符合因果关系,因此不能用于典型的语言建模任务,如文本生成。在接下来的章节中,我们将讨论 OpenAI 提出的Transformers并行架构系列。

GPT 1, 2, 3…

OpenAI 是一个人工智能研究团体,由于他们的具有新闻价值的作品,如 GPT、GPT-2 和最近发布的 GPT-3,一直备受关注。在本节中,我们将讨论与这些架构及其新颖贡献相关的简要讨论。最后,我们将使用 GPT-2 的预训练版本来执行我们的文本生成任务。

生成式预训练: GPT

这个系列中的第一个模型被称为GPT,或生成式预训练。它于 2018 年发布,大约与 BERT 模型同时。该论文¹¹ 提出了一个基于Transformers和无监督学习思想的任务无关架构。GPT 模型被证明在 GLUE 和 SST-2 等多个基准测试中取得了胜利,虽然其性能很快被 BERT 超越,后者在此后不久发布。

GPT 本质上是基于我们在上一章节中提出的transformer-decoder的语言模型(参见Transformers部分)。由于语言模型可以以无监督方式进行训练,该模型的作者利用了这种无监督方法在非常大的语料库上进行训练,然后针对特定任务进行了微调。作者使用了包含超过 7,000 本不同流派的独特未发表书籍的BookCorpus数据集¹²。作者声称,该数据集使得模型能够学习到长距离信息,这是由于存在着长串连续文本。这被认为比之前使用的 1B Word Benchmark 数据集更好,后者由于句子被打乱而丧失了长距离信息。GPT 的整体设置如下图所示:

图形用户界面 自动生成描述

图 10.7: GPT 架构(左), 使用 GPT 的基于任务的设置(右)

图 10.7 (左)所示,GPT 模型与原始Transformers-解码器类似。作者使用了 12 个解码器块(而不是原始Transformers中的 6 个),每个块具有 768 维状态和 12 个自注意头。由于该模型使用了掩码自注意力,它保持了语言模型的因果属性,因此还可以用于文本生成。对于图 10.7 (右)展示的其余任务,基本上使用相同的预训练语言模型,并对输入进行最小的任务特定预处理和最终任务特定的层/目标。

GPT-2

GPT 被一个更加强大的模型 GPT-2 取代。Radford 等人在 2019 年的论文Language Models are Unsupervised Multi-task Learners中展示了 GPT-2 模型。¹³ 最大的 GPT-2 变体是一个庞大的 15 亿参数的基于 transformer 的模型,在各种 NLP 任务上表现出色。

这项工作最引人注目的是,作者展示了一个以无监督方式训练的模型(语言建模)如何在few-shot设置中实现了最先进的性能。这一点特别重要,因为与 GPT 甚至 BERT 相比,GPT-2 在特定任务上不需要进行任何微调。

与 GPT 类似,GPT-2 的秘密武器是它的数据集。作者们通过对一个名为 Reddit 的社交网站的 4500 万个外链进行爬取,准备了一个庞大的 40 GB 数据集。他们进行了一些基于启发式的清理、去重和移除维基百科文章(当然,为什么不呢?)最终得到了大约 800 万个文档。这个数据集被称为WebText数据集。

GPT-2 的总体结构与 GPT 相同,只是稍微更改了一些地方,比如在每个次块的开头放置层归一化,并在最终的自注意力块之后添加了额外的层归一化。模型的四个变体分别利用了 12、24、36 和 48 层。词汇量也扩展到了 50000 个词,并且上下文窗口扩展到了 1024 个标记(而 GPT 为 512)。

GPT-2 作为一种语言模型表现非常出色,以至于作者最初决定不释放预训练模型以造福大众¹⁴。最终他们还是释放了它,理由是迄今为止还没有发现恶意使用。需注意的是,这不仅仅是道德问题。数据和模型的庞大规模使得大多数人甚至无法想象训练这样的模型的可能性。Figure 10.8 描述了一些最近的 NLP 模型的规模和训练所需的计算量:

Table  Description automatically generated

Figure 10.8: NLP 模型的规模¹⁵

TPU 的速度比典型的 GPU 快多次,如图所示,GPT-2 需要在报告的数据集上进行 2048 TPU 天的训练。与大型 BERT 模型的 256 TPU 天相比是相当大的差距。

有兴趣的读者可以在这里探索 GPT-2 的官方实现:github.com/openai/gpt-2.

虽然官方实现是基于 TensorFlow 1.14,但此链接提供了使用 TensorFlow 2.x 的非官方实现:

akanyaani.github.io/gpt-2-tensorflow2.0/

幸运的是,由于预训练模型的发布,Hugging Face 的研究人员决定致力于使Transformers架构民主化。来自 Hugging Face 的 transformer 包是一个高级包装器,使我们能够使用几行代码就可以使用这些大规模的自然语言处理模型。该库还提供了一个 Web 应用程序,用于探索不同的基于Transformers的模型。图 10.9 是当提供了一个 "GPT is" 种子时,由该 Web 应用程序生成的段落的快照:

自动生成的文本描述

图 10.9:使用基于 Hugging Face transformer 包的 GPT-2 的示例输出¹⁶

图中生成的文本显示了 GPT-2 的惊人效果。在第一句中,模型表现出色,甚至遵循了正确引用先前工作的惯例(尽管引用本身并不正确)。内容并不是很有意义,但在语法上是准确的,相对于我们提供的最小种子文本而言,它是相当连贯的。

现在,我们将利用 transformers 包来构建一个基于 GPT-2 的文本生成流水线,看看我们的模型能做多好。

亲身体验 GPT-2

保持与之前某些章节主题的一致性,在那些章节中,我们使用各种复杂的架构生成了一些虚假内容,让我们使用 GPT-2 生成一些虚假新闻标题。百万条新闻标题数据集包含了澳大利亚广播公司的一百多万条新闻标题,历时 17 年收集。该数据集可在以下链接处获得:

dataverse.harvard.edu/dataset.xhtml?persistentId=doi:10.7910/DVN/SYBGZL

www.kaggle.com/therohk/million-headlines

我们将使用 Hugging Face 的 transformers 库来对这个数据集上的 GPT-2 进行微调。从高层次来看,这个假新闻生成任务与我们在 第九章 的初始部分中进行的语言建模任务是一样的。由于我们使用的是 transformers 包,与训练数据集的创建、标记化和最终训练模型相关的步骤都是使用高级 API 进行抽象化的。

transformers 库与 TensorFlow 和 PyTorch 后端都兼容。对于这种特殊情况,我们使用基于 PyTorch 的默认重新训练设置。该库不断改进,在撰写本文时,稳定版本 3.3.1 在使用 TensorFlow 对 GPT-2 进行微调时存在问题。由于 transformers 是一个高级库,读者在以下代码片段中不会注意到太大的差异。

如往常一样,第一步是读取手头的数据集,并将其转换为所需的格式。我们不需要自己准备单词到整数和反向映射。transformers库中的Tokenizer类会为我们处理这些。以下代码片段准备了数据集和所需的对象:

import pandas as pd
from sklearn.model_selection import train_test_split
from transformers import AutoTokenizer
from transformers import TextDataset,DataCollatorForLanguageModeling
# Get dataset
news = pd.read_csv('abcnews-date-text.csv')
X_train, X_test= train_test_split(news.headline_text.tolist(),test_size=0.33, random_state=42)
# Write the headlines from training dataset
with open('train_dataset.txt','w') as f:
  for line in X_train:
    f.write(line)
    f.write("\n")
# Write the headlines from testing dataset
with open('test_dataset.txt','w') as f:
  for line in X_test:
    f.write(line)
    f.write("\n")
# Prepare tokenizer object
tokenizer = AutoTokenizer.from_pretrained("gpt2",pad_token='<pad>')
train_path = 'train_dataset.txt'
test_path = 'test_dataset.txt'
# Utility method to prepare DataSet objects
def load_dataset(train_path,test_path,tokenizer):
    train_dataset = TextDataset(
          tokenizer=tokenizer,
          file_path=train_path,
          block_size=4)

    test_dataset = TextDataset(
          tokenizer=tokenizer,
          file_path=test_path,
          block_size=4)   

    data_collator = DataCollatorForLanguageModeling(
        tokenizer=tokenizer, mlm=False,
    )
    return train_dataset,test_dataset,data_collator
train_dataset,test_dataset,data_collator = load_dataset(train_path, test_path, tokenizer) 
sklearn to split our dataset into training and test segments, which are then transformed into usable form using the TextDataset class. The train_dataset and test_dataset objects are simple generator objects that will be used by the Trainer class to fine-tune our model. The following snippet prepares the setup for training the model:
from transformers import Trainer,TrainingArguments,AutoModelWithLMHead
model = AutoModelWithLMHead.from_pretrained("gpt2")
training_args = TrainingArguments(
    output_dir="./headliner",  # The output directory
    overwrite_output_dir=True, # overwrite the content of 
                               # the output directory
    num_train_epochs=1,        # number of training epochs
    per_device_train_batch_size=4, # batch size for training
    per_device_eval_batch_size=2,  # batch size for evaluation
    eval_steps = 400, # Number of update steps 
                      # between two evaluations.
    save_steps=800,   # after # steps model is saved 
    warmup_steps=500, # number of warmup steps for 
                      # learning rate scheduler
    )
trainer = Trainer(
    model=model,
    args=training_args,
    data_collator=data_collator,
    train_dataset=train_dataset,
    eval_dataset=test_dataset,
    prediction_loss_only=True,
) 

我们使用AutoModelWithLMHead类作为对 GPT-2 的高级封装,具有语言模型目标。Trainer类根据TrainingArguments类设置的参数简单地迭代训练步骤。

下一步只需调用train函数,让微调开始。以下代码片段展示了 GPT-2 的训练步骤:

trainer.train() 
{'loss': 6.99887060546875, 'learning_rate': 5e-05, 'epoch': 0.0010584004182798454, 'total_flos': 5973110784000, 'step': 500}
{'loss': 6.54750146484375, 'learning_rate': 4.994702390916932e-05, 'epoch': 0.0021168008365596907, 'total_flos': 11946221568000, 'step': 1000}
{'loss': 6.5059072265625, 'learning_rate': 4.989404781833863e-05, 'epoch': 0.003175201254839536, 'total_flos': 17919332352000, 'step': 1500}
{'loss': 6.46778125, 'learning_rate': 4.9841071727507945e-05, 'epoch': 0.0042336016731193814, 'total_flos': 23892443136000, 'step': 2000}
{'loss': 6.339587890625, 'learning_rate': 4.978809563667726e-05, 'epoch': 0.005292002091399226, 'total_flos': 29865553920000, 'step': 2500}
{'loss': 6.3247421875, 'learning_rate': 4.973511954584657e-05, 'epoch': 0.006350402509679072, 'total_flos': 35838664704000, 'step': 3000} 
pipeline object along with the utility function get_headline, which we need to generate headlines using this fine-tuned model:
from transformers import pipeline
headliner = pipeline('text-generation',
                model='./headliner', 
                tokenizer='gpt2',
                config={'max_length':8})
# Utility method
def get_headline(headliner_pipeline, seed_text="News"):
  return headliner_pipeline(seed_text)[0]['generated_text'].split('\n')[0] 

现在让我们生成一些虚假的新闻标题,看看我们的 GPT-2 模型的表现好坏。图 10.10展示了使用我们的模型生成的一些虚假新闻标题:

一朵花的特写  自动生成的说明

图 10.10:使用微调 GPT-2 生成的虚假新闻标题。粗体文本是种子文本。

生成的输出展示了 GPT-2 和基于 transformer 的架构的潜力。读者应该将此与我们在《第九章 文本生成方法的兴起》的初始部分中训练的基于 LSTM 的变体进行比较。这里展示的模型能够捕捉到与新闻标题相关的一些细微差别。例如,它生成了简短而精练的句子,捕捉到了像袋鼠土著甚至墨尔本这样在澳大利亚环境中都相关的词语,这些都存在于我们的训练数据集的领域。所有这些都是模型在仅经过几个 epoch 的训练后所捕获的。可能性是无穷无尽的。

猛犸 GPT-3

OpenAI 小组并未在 GPT-2 取得巨大成功后就止步不前。相反,GPT-2 展示了模型容量(参数大小)和更大数据集如何可能导致令人印象深刻的结果。Brown 等人在 2020 年 5 月发布了题为语言模型是少样本学习者的论文。¹⁷此论文介绍了巨大的 1750 亿参数 GPT-3 模型。

GPT-3 比以往任何语言模型都要庞大(大约 10 倍),并且将 transformer 架构发挥到了极限。在这项工作中,作者展示了模型的 8 个不同变体,从一个拥有 1.25 亿参数,12 层的“GPT-3 小”到一个拥有 1750 亿参数,96 层的 GPT-3 模型。

模型架构与 GPT-2 相同,但有一个主要变化(除了嵌入尺寸、注意头和层数的增加之外)。这个主要变化是在Transformers块中使用交替的密集和局部带状稀疏注意力模式。这种稀疏注意技术类似于为稀疏变换器(参见使用稀疏变换器生成长序列,Child 等人¹⁸))提出的技术。

与之前的 GPT 模型类似,作者们不得不为这第三次迭代准备一个更大的数据集。他们基于类似 Common Crawl(过滤了更好的内容)、WebText2(WebText 的更大版本,用于 GPT-2)、Books1 和 Books2,以及维基百科数据集准备了一个 3000 亿标记的数据集。他们按各自数据集的质量比例抽样。

作者比较了 NLP 模型和总体机器学习的学习范式与人类学习方式。尽管语言模型在这些年来的性能和容量上有所改进,但最先进模型仍需要特定于任务的精细调整。为展示 GPT-3 的能力,他们评估了该模型的 少例学习一例学习零例学习 模式。精细调整模式暂时留给未来练习。

这三种评估模式可以总结如下:

  • 零-shot:仅凭任务的自然语言描述,即在没有展示任何正确输出示例的情况下,模型就能预测答案。

  • 一例:除了任务描述外,模型还展示了一个任务示例。

  • 少例学习:除了任务描述外,模型还展示了一些任务示例。

在每种情况下,都不进行梯度更新(因为我们只是评估模型,在任何这些模式中都不是在训练)。图 10.11 显示了每种评估模式的示例设置,任务是将文本从英语翻译成西班牙语。

自动生成的文本描述

图 10.11: GPT-3 的评估模式

如图所示,在零-shot 模式下,模型展示了任务描述和一个用于翻译的提示。类似地,在一例和少例模式下,模型分别展示了一个和一些示例,然后展示了实际翻译提示。作者观察到 GPT-3 在零-shot 和一例设置下取得了有希望的结果。在少例设置中,该模型大多数情况下是竞争性的,甚至在某些任务中超过了当前的最先进水平。

除了通常的 NLP 任务外,GPT-3 似乎展示了一些在其他情况下需要快速适应或即兴推理的非凡能力。作者观察到 GPT-3 能够在一些任务上表现良好,如拼词、进行三位数的算术,甚至在看到一次定义后就能在句子中使用新词。作者还观察到,在少例学习设置下,GPT-3 生成的新闻文章足够好,以至于在区分它们与人为生成的文章时会给人类评估者带来困难。有趣的是,在之前部分准备的 GPT-2 上测试一下 GPT-3。

该模型足够庞大,需要一个专门的高性能集群来训练,正如论文中所描述的。作者就训练这个巨大模型所需的计算量和能量进行了讨论。在当前状态下,这个模型对我们大多数人来说还是难以企及的。OpenAI 计划以 API 的形式展示这个模型,但在撰写本章时,细节尚不明朗。

摘要

在本章中,我们介绍了近期 NLP 模型中一些主要的概念,如attention机制、contextual embeddingsself-attention。然后我们使用这个基础来学习transformer架构及其内部组件。我们简要地讨论了 BERT 及其系列架构。

在本章的下一节中,我们对 OpenAI 的基于 transformer 的语言模型进行了讨论。我们讨论了 GPT 和 GPT-2 的架构和数据集相关的选择。我们还使用了 Hugging Face 的transformer包来开发我们自己的基于 GPT-2 的文本生成流水线。最后,我们对最新、最尖端的语言模型 GPT-3 进行了简要讨论。我们讨论了开发这样一个巨大模型的各种动机以及它超越传统测试基准列表的功能清单。

本章和第九章 文本生成方法的兴起 展示了自然语言处理(NLP)是自己的研究领域。然而,来自计算机视觉和深度学习/机器学习的概念通常相互交叉,以推动技术边界。

在下一章中,我们将把我们的焦点转移到理解音频领域以及生成模型在音频领域中的工作。

参考文献

  1. Peters, M.E., Ammar, W., Bhagavatula, C., & Power, R. (2017). Semi-supervised sequence tagging with bidirectional language models. arXiv. arxiv.org/abs/1705.00108

  2. Peters, M.E., Neumann, M., Iyyer, M., Gardner, M., Clark, C., Lee, K., & Zettlemoyer, L. (2018). Deep contexualized word representations. arXiv. arxiv.org/abs/1802.05365

  3. Howard, J., & Ruder, S. (2018). Universal Language Model Fine-tuning for Text Classification. arXiv. arxiv.org/abs/1801.06146

  4. Cheng, J., Dong, L., & Lapata, M. (2016). Long Short-Term Memory-Networks for Machine Reading. arXiv. arxiv.org/abs/1601.06733

  5. Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A.N., Kaiser, L., & Polosukhin, I. (2017). Attention Is All You Need. arXiv. arxiv.org/abs/1706.03762

  6. Devlin, J., Chang, M-W., Lee, K., & Toutanova, K. (2018). BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding. arXiv. arxiv.org/abs/1810.04805

  7. Liu, Y., Ott, M., Goyal, N., Du, J., Joshi, M., Chen, D., Levy, O., Lewis, M., Zettlemoyer, L., & Stoyanov, V. (2019)。RoBERTa: A Robustly Optimized BERT Pretaining Approach。arXiv。arxiv.org/abs/1907.11692

  8. Lan, Z., Chen, M., Goodman, S., Gimpel, K., Sharma, P., & Soricut, R. (2019)。ALBERT: A Lite BERT for Self-supervised Learning of Language Representations。arXiv。arxiv.org/abs/1909.11942

  9. Sanh, V., Debut, L., Chaumond, J., & Wolf, T. (2019)。DistilBERT, a distilled version of BERT: smaller, faster, cheaper and lighter。arXiv。arxiv.org/abs/1910.01108

  10. Yang, Z., Dai, Z., Yang, Y., Carbonell, J., Salakhutdinov, R., & Le, Q.V. (2019)。XLNet: Generalized Autoregressive Pretraining for Language Understanding。arXiv。arxiv.org/abs/1906.08237

  11. Radford, A. (2018 年 6 月 11 日)。Improving Language Understanding with Unsupervised Learning。OpenAI。openai.com/blog/language-unsupervised/

  12. Zhu, Y., Kiros, R., Zemel, R., Salakhutdinov, R., Urtasun, R., Torralba, A., & Fidler, S. (2015)。Aligning Books and Movies: Towards Story-like Visual Explanations by Watching Movies and Reading Books。arXiv。arxiv.org/abs/1506.06724

  13. Radford, A., Wu, J., Child, R., Luan, D., Amodei, D., & Sutskever, I. (2019)。Language Models are Unsupervised Multitask Learners。OpenAI。cdn.openai.com/better-language-models/language_models_are_unsupervised_multitask_learners.pdf

  14. Radford, A., Wu, J., Amodei, D., Amodei, D., Clark, J., Brundage, M., & Sutskever, I. (2019 年 2 月 14 日)。Better Language Models and Their Implications。OpenAI。openai.com/blog/better-language-models/

  15. stanfordonline. (2019 年 3 月 21 日)。斯坦福 CS224N: NLP with Deep Learning | Winter 2019 | Lecture 13 – Contextual Word Embeddings [视频]。YouTube。www.youtube.com/watch?v=S-CspeZ8FHc&ab_channel=stanfordonline

  16. Hugging Face。 (无日期)。gpt2 abstract。检索于 2021 年 4 月 22 日,来源:transformer.huggingface.co/doc/arxiv-nlp/ByLHXHhnBJtBLOpRENZmulqc/edit

  17. Brown, T.B., Mann, B., Ryder, N., Subbiah, M., Kaplan, J., Dhariwal, P., Neelakantan, A., Shyam, P., Sastry, G., Askell, A., Agarwal, S., Herbert-Voss, A., Krueger, G., Henighan, T., Child, R., Ramesh, A., Ziegler, D., Wu, J., Winter, C., Hesse, C., Chen, M., Sigler, E., Litwin, M., Gray, S., Chess, B., Clark, J., Berner, C., McCandlish, S., Radford, A., Sutskever, I., & Amodei, D. (2020). "语言模型是少样本学习器". arXiv. arxiv.org/abs/2005.14165

  18. Child, R., Gray, S., Radford, A., & Sutskever, I. (2019). 使用稀疏 Transformer 生成长序列. arXiv. arxiv.org/abs/1904.10509

第十一章:使用生成模型作曲音乐

在之前的章节中,我们讨论了许多以图像、文本和视频生成为重点的生成模型。从非常基本的 MNIST 数字生成到像模仿巴拉克·奥巴马这样更复杂的任务,我们探讨了许多复杂的作品及其新颖的贡献,并花时间了解所涉及的任务和数据集的细微差别。

我们看到,在之前关于文本生成的章节中,计算机视觉领域的改进如何帮助促进自然语言处理领域的显著进步。同样,音频是另一个领域,在这个领域,来自计算机视觉和自然语言处理领域的思想交叉已经扩大了视角。音频生成并不是一个新领域,但由于深度学习领域的研究,这个领域近年来也取得了一些巨大的进步。

音频生成有许多应用。如今最突出和流行的是一系列智能助手(谷歌助手、苹果 Siri、亚马逊 Alexa 等等)。这些虚拟助手不仅试图理解自然语言查询,而且还以非常人性化的声音作出回应。音频生成还在辅助技术领域找到应用,在那里,文本到语音引擎用于为阅读障碍者阅读屏幕上的内容。

在音乐生成领域利用这样的技术越来越受到关注。字节跳动(社交网络 TikTok 的母公司)收购了基于人工智能的免版税音乐生成服务 Jukedeck,这一举动充分展示了这个领域的潜在价值和影响力。¹

实际上,基于人工智能的音乐生成是一个不断增长的趋势,有许多竞争性的解决方案和研究工作。诸如苹果的 GarageBand² 等商业化产品提供了许多易于使用的界面,供新手只需点击几下即可创作出高质量音乐曲目。谷歌的 Magenta³ 项目的研究人员正在通过尝试不同的技术、工具和研究项目,将音乐生成的边界推向新的极限,使对这些复杂主题几乎没有知识的人也能够自己生成令人印象深刻的音乐作品。

在本章中,我们将介绍与音频数据生成模型相关的不同概念、架构和组件。特别是,我们将把焦点限制在音乐生成任务上。我们将专注于以下主题:

  • 音乐表示的简要概述

  • 基于 RNN 的音乐生成

  • 一个简单的设置,以了解如何利用 GANs 进行音乐生成

  • 基于 MuseGAN 的复音音乐生成

本章中介绍的所有代码片段都可以直接在 Google Colab 中运行。出于空间原因,依赖项的导入语句没有包含在内,但读者可以参考 GitHub 存储库获取完整的代码:github.com/PacktPublishing/Hands-On-Generative-AI-with-Python-and-TensorFlow-2

我们将从音乐生成任务的介绍开始。

开始学习音乐生成

音乐生成是一个固有复杂且困难的任务。通过算法(机器学习或其他方式)进行这样的任务甚至更具挑战性。尽管如此,音乐生成是一个有趣的研究领域,有许多待解决的问题和令人着迷的作品。

在本节中,我们将建立对这一领域的高层次理解,并了解一些重要且基础的概念。

计算机辅助音乐生成或更具体地说是深度音乐生成(由于使用深度学习架构)是一个由生成乐谱和表现生成两个主要组成部分组成的多层学习任务。让我们简要讨论每个组件:

  • 生成乐谱:乐谱是音乐的符号表示,可供人类或系统使用/阅读以生成音乐。类比一下,我们可以将乐谱与音乐之间的关系安全地视为文本与言语之间的关系。音乐乐谱由离散符号组成,可以有效地传达音乐。一些作品使用术语AI 作曲家来表示与生成乐谱任务相关的模型。

  • 表现生成:延续文本-语音类比,表现生成(类似于言语)是表演者使用乐谱以其自己的节奏、韵律等特征生成音乐的地方。与表现生成任务相关的模型有时也被称为AI 表演者

我们可以根据目标组件针对不同的用例或任务来实现。图 11.1强调了在音乐生成的上下文中正在研究的一些任务:

图 11.1:音乐生成的不同组件及相关任务列表

如图所示,通过仅关注乐谱生成,我们可以致力于诸如旋律生成和旋律和声化以及音乐修补(与填补音乐中缺失或丢失的信息相关联)等任务。除了作曲家和表演者之外,还有研究正在进行中,旨在构建 AI DJ。与人类唱片骑师(DJ)类似,AI DJ 利用现有的音乐组件创建串烧、混搭、混音,甚至高度个性化的播放列表。

在接下来的章节中,我们将主要致力于构建我们自己的乐谱生成模型或 AI 作曲家。既然我们对整体音乐生成的景观有了高层次的理解,让我们专注于理解音乐的表示方式。

表示音乐

音乐是代表情绪、节奏、情感等的艺术作品。类似于文本,文本是字母和语法规则的集合,音乐谱有自己的符号和一套规则。在前几章中,我们讨论了如何在任何自然语言处理任务之前,将文本数据首先转换为可用的向量形式。在音乐的情况下,我们也需要做类似的事情。

音乐表示可以分为两大类:连续和离散。连续表示也称为音频领域表示。它将音乐数据处理为波形。如图 11.2(a)所示,音频领域表示捕捉丰富的声学细节,如音色和发音。

图形用户界面 自动生成的描述

图 11.2:音乐的连续或音频领域表示。a)1D 波形是音频信号的直接表示。b)音频数据的二维表示可以是以时间为一轴,频率为第二轴的频谱图形式。

如图所示,音频领域表示可以直接是 1D 波形或 2D 频谱图:

  • 一维波形是音频信号的直接表示,其中x轴表示时间,y轴表示信号的变化。

  • 二维频谱图将时间作为x轴,频率作为y轴。

我们通常使用短时傅里叶变换STFT)将一维波形转换为二维频谱图。根据我们如何获得最终的频谱图,有不同的变体,如梅尔频谱图或幅度频谱图。

另一方面,离散或符号表示使用离散符号来捕获与音高、持续时间、和弦等相关的信息。尽管不如音频域表示那样具有表现力,但符号表示被广泛应用于不同的音乐生成工作中。这种流行程度主要是由于易于理解和处理这种表示形式。图 11.3展示了音乐谱的一个示例符号表示:

图 11.3:音乐的离散或符号表示

如图所示,符号表示使用各种符号/位置捕获信息。MIDI,或音乐乐器数字接口,是音乐家用来创建、谱写、演奏和分享音乐的可互操作格式。它是各种电子乐器、计算机、智能手机甚至软件用来读取和播放音乐文件的常用格式。

符号表示可以设计成捕捉诸如note-onnote-off时间偏移小节轨道等许多事件。为了理解即将出现的部分和本章的范围,我们只会关注两个主要事件,即note-onnote-off。MIDI 格式捕捉了 16 个通道(编号为 0 到 15)、128 个音符和 128 个响度设置(也称为速度)。还有许多其他格式,但为了本章的目的,我们将仅使用基于 MIDI 的音乐文件,因为它们被广泛使用、富有表现力、可互操作且易于理解。

music21. We then use its utility function to visualize the information in the file:
from music21 import converter
midi_filepath = 'Piano Sonata No.27.mid'
midi_score = converter.parse(midi_filepath).chordify()
# text-form
print(midi_score.show('text'))
# piano-roll form
print(midi_score.show()) 

到目前为止,我们在理解整体音乐生成格局和一些重要的表示技术方面已经取得了相当大的进展。接下来,我们将开始进行音乐生成本身。

使用 LSTM 进行音乐生成

歌曲是连续信号,由各种乐器和声音的组合构成,这一点我们在前一节已经看到。另一个特点是结构性的循环模式,我们在听歌时要注意。换句话说,每首音乐都有其独特的连贯性、节奏和流畅性。

这样的设置与我们在 第九章 文本生成方法的兴起 中看到的文本生成情况类似。在文本生成的情况下,我们看到了基于 LSTM 的网络的力量和有效性。在本节中,我们将扩展堆叠 LSTM 网络来执行音乐生成任务。

为了保持简单和易于实现,我们将专注于单个乐器/单声部音乐生成任务。让我们先看看数据集,然后考虑如何为我们的音乐生成任务准备它。

数据集准备

MIDI 是一种易于使用的格式,可以帮助我们提取文件中包含的音乐的符号表示。在本章的实践练习中,我们将利用 reddit 用户u/midi_man收集并分享的大规模公共 MIDI 数据集的子集,该数据集可以在以下链接中找到:

www.reddit.com/r/WeAreTheMusicMakers/comments/3ajwe4/the_largest_midi_collection_on_the_inte

基于贝多芬、巴赫、巴托克等伟大音乐家的古典钢琴作品。该子集可以在压缩文件midi_dataset.zip中找到,并且连同本书的代码一起放在 GitHub 存储库中。

正如前面提到的,我们将利用 music21 来处理此数据集的子集,并准备我们的数据来训练模型。由于音乐是各种乐器和声音/歌手的集合,因此为了本练习的目的,我们将首先使用 chordify() 函数从歌曲中提取和弦。以下代码片段可以帮助我们以所需格式获取 MIDI 分数的列表:

from music21 import converter
data_dir = 'midi_dataset'
# list of files
midi_list = os.listdir(data_dir)
# Load and make list of stream objects
original_scores = []
for midi in tqdm(midi_list):
    score = converter.parse(os.path.join(data_dir,midi))
    original_scores.append(score)
# Merge notes into chords
original_scores = [midi.chordify() for midi in tqdm(original_scores)] 

一旦我们有了分数列表,下一步就是提取音符及其对应的时间信息。为了提取这些细节,music21具有诸如element.pitchelement.duration之类的简单易用的接口。

以下代码片段帮助我们从 MIDI 文件中提取这样的信息,并准备两个并行的列表:

# Define empty lists of lists
original_chords = [[] for _ in original_scores]
original_durations = [[] for _ in original_scores]
original_keys = []
# Extract notes, chords, durations, and keys
for i, midi in tqdm(enumerate(original_scores)):
    original_keys.append(str(midi.analyze('key')))
    for element in midi:
        if isinstance(element, note.Note):
            original_chords[i].append(element.pitch)
            original_durations[i].append(element.duration.quarterLength)
        elif isinstance(element, chord.Chord):
            original_chords[i].append('.'.join(str(n) for n in element.pitches))
            original_durations[i].append(element.duration.quarterLength) 
C major key:
# Create list of chords and durations from songs in C major
major_chords = [c for (c, k) in tqdm(zip(original_chords, original_keys)) if (k == 'C major')]
major_durations = [c for (c, k) in tqdm(zip(original_durations, original_keys)) if (k == 'C major')] 
mapping and presents a sample output as well:
def get_distinct(elements):
    # Get all pitch names
    element_names = sorted(set(elements))
    n_elements = len(element_names)
    return (element_names, n_elements)
def create_lookups(element_names):
    # create dictionary to map notes and durations to integers
    element_to_int = dict((element, number) for number, element in enumerate(element_names))
    int_to_element = dict((number, element) for number, element in enumerate(element_names))
    return (element_to_int, int_to_element)
# get the distinct sets of notes and durations
note_names, n_notes = get_distinct([n for chord in major_chords for n in chord])
duration_names, n_durations = get_distinct([d for dur in major_durations for d in dur])
distincts = [note_names, n_notes, duration_names, n_durations]
with open(os.path.join(store_folder, 'distincts'), 'wb') as f:
    pickle.dump(distincts, f)
# make the lookup dictionaries for notes and durations and save
note_to_int, int_to_note = create_lookups(note_names)
duration_to_int, int_to_duration = create_lookups(duration_names)
lookups = [note_to_int, int_to_note, duration_to_int, int_to_duration]
with open(os.path.join(store_folder, 'lookups'), 'wb') as f:
    pickle.dump(lookups, f)
print("Unique Notes={} and Duration values={}".format(n_notes,n_durations)) 
Unique Notes=2963 and Duration values=18 

我们现在准备好映射。在以下代码片段中,我们将训练数据集准备为长度为 32 的序列,并将它们的对应目标设为序列中紧接着的下一个标记:

# Set sequence length
sequence_length = 32
# Define empty array for training data
train_chords = []
train_durations = []
target_chords = []
target_durations = []
# Construct train and target sequences for chords and durations
# hint: refer back to Chapter 9 where we prepared similar 
# training data
# sequences for an LSTM-based text generation network
for s in range(len(major_chords)):
    chord_list = [note_to_int[c] for c in major_chords[s]]
    duration_list = [duration_to_int[d] for d in major_durations[s]]
    for i in range(len(chord_list) - sequence_length):
        train_chords.append(chord_list[i:i+sequence_length])
        train_durations.append(duration_list[i:i+sequence_length])
        target_chords.append(chord_list[i+1])
        target_durations.append(duration_list[i+1]) 

正如我们所看到的,数据集准备阶段除了与处理 MIDI 文件相关的一些细微差别之外,大部分都是直截了当的。生成的序列及其对应的目标在下面的输出片段中供参考:

print(train_chords[0]) 
array([ 935, 1773, 2070, 2788,  244,  594, 2882, 1126,  152, 2071, 
        2862, 2343, 2342,  220,  221, 2124, 2123, 2832, 2584, 939, 
        1818, 2608, 2462,  702,  935, 1773, 2070, 2788,  244, 594,
        2882, 1126]) 
print(target_chords[0]) 
1773 
print(train_durations[0]) 
array([ 9,  9,  9, 12,  5,  8,  2,  9,  9,  9,  9,  5,  5,  8,  2,
        5,  5,  9,  9,  7,  3,  2,  4,  3,  9,  9,  9, 12,  5,  8,
        2,  9]) 
print(target_durations[0]) 
9 

转换后的数据集现在是一系列数字,就像文本生成的情况一样。列表中的下一项是模型本身。

用于音乐生成的 LSTM 模型

如前所述,我们的第一个音乐生成模型将是第九章文本生成方法的崛起中基于 LSTM 的文本生成模型的扩展版本。然而,在我们可以将该模型用于这项任务之前,有一些注意事项需要处理和必要的变更需要进行。

不像文本生成(使用 Char-RNN)只有少数输入符号(小写和大写字母、数字),音乐生成的符号数量相当大(~500)。在这个符号列表中,还需要加入一些额外的符号,用于时间/持续时间相关的信息。有了这个更大的输入符号列表,模型需要更多的训练数据和学习能力(学习能力以 LSTM 单元数量、嵌入大小等方面来衡量)。

我们需要处理的下一个明显变化是模型能够在每个时间步骤上接受两个输入的能力。换句话说,模型应能够在每个时间步骤上接受音符和持续时间信息,并生成带有相应持续时间的输出音符。为此,我们利用功能性的tensorflow.keras API,构建一个多输入多输出的架构。

正如在第九章文本生成方法的崛起中详细讨论的那样,堆叠的 LSTM 在能够学习更复杂特征方面具有明显优势,这超过了单个 LSTM 层网络的能力。除此之外,我们还讨论了注意机制以及它们如何帮助缓解 RNN 所固有的问题,比如难以处理长距离依赖关系。由于音乐由在节奏和连贯性方面可感知的局部和全局结构组成,注意机制肯定可以起作用。下面的代码片段按照所讨论的方式准备了一个多输入堆叠的 LSTM 网络:

def create_network(n_notes, n_durations, embed_size = 100,                                          rnn_units = 256):
    """ create the structure of the neural network """
    notes_in = Input(shape = (None,))
    durations_in = Input(shape = (None,))
    x1 = Embedding(n_notes, embed_size)(notes_in)
    x2 = Embedding(n_durations, embed_size)(durations_in) 
    x = Concatenate()([x1,x2])
    x = LSTM(rnn_units, return_sequences=True)(x)
    x = LSTM(rnn_units, return_sequences=True)(x)
    # attention
    e = Dense(1, activation='tanh')(x)
    e = Reshape([-1])(e)
    alpha = Activation('softmax')(e)
    alpha_repeated = Permute([2, 1])(RepeatVector(rnn_units)(alpha))
    c = Multiply()([x, alpha_repeated])
    c = Lambda(lambda xin: K.sum(xin, axis=1), output_shape=(rnn_units,))(c)

    notes_out = Dense(n_notes, activation = 'softmax', name = 'pitch')(c)
    durations_out = Dense(n_durations, activation = 'softmax', name = 'duration')(c)

    model = Model([notes_in, durations_in], [notes_out, durations_out])
    model.compile(loss=['sparse_categorical_crossentropy', 
                        'sparse_categorical_crossentropy'], optimizer=RMSprop(lr = 0.001))
    return model 
network (one input each for notes and durations respectively). Each of the inputs is transformed into vectors using respective embedding layers. We then concatenate both inputs and pass them through a couple of LSTM layers followed by a simple attention mechanism. After this point, the model again diverges into two outputs (one for the next note and the other for the duration of that note). Readers are encouraged to use keras utilities to visualize the network on their own.

训练这个模型就像在 keras 模型对象上调用 fit() 函数一样简单。我们将模型训练约 100 个周期。图 11.4 描述了模型在不同周期下的学习进展:

图形用户界面,文本说明自动生成

图 11.4:模型输出随着训练在不同周期下的进展

如图所示,模型能够学习一些重复模式并生成音乐。在这里,我们使用基于温度的抽样作为我们的解码策略。正如在 第九章文本生成方法的兴起 中讨论的,读者可以尝试诸如贪婪解码、纯抽样解码等技术,以了解输出音乐质量如何变化。

这是使用深度学习模型进行音乐生成的一个非常简单的实现。我们将之前两章学到的概念与之进行了类比,那两章是关于文本生成的。接下来,让我们使用对抗网络进行一些音乐生成。

使用 GAN 进行音乐生成

在前一节中,我们尝试使用一个非常简单的基于 LSTM 的模型进行音乐生成。现在,让我们提高一点标准,看看如何使用 GAN 生成音乐。在本节中,我们将利用我们在前几章学到的与 GAN 相关的概念,并将它们应用于生成音乐。

我们已经看到音乐是连续且序列化的。LSTM 或 RNN 等模型非常擅长处理这样的数据集。多年来,已经提出了各种类型的 GAN,以有效地训练深度生成网络。

Mogren 等人于 2016 年提出了 连续循环神经网络与对抗训练:C-RNN-GAN⁴,结合了 LSTM 和基于 GAN 的生成网络的能力,作为音乐生成的方法。这是一个直接但有效的音乐生成实现。与前一节一样,我们将保持简单,并且只关注单声道音乐生成,尽管原始论文提到了使用音调长度、频率、强度和音符之间的时间等特征。论文还提到了一种称为 特征映射 的技术来生成复调音乐(使用 C-RNN-GAN-3 变体)。我们将只关注理解基本架构和预处理步骤,而不试图按原样实现论文。让我们开始定义音乐生成 GAN 的各个组件。

生成器网络

tensorflow.keras to prepare our generator model:
def build_generator(latent_dim,seq_shape):
  model = Sequential()
  model.add(Dense(256, input_dim=latent_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(seq_shape), activation='tanh'))
  model.add(Reshape(seq_shape))
  model.summary()
  noise = Input(shape=(latent_dim,))
  seq = model(noise)
  return Model(noise, seq) 

生成器模型是一个相当简单的实现,突显了基于 GAN 的生成模型的有效性。接下来,让我们准备判别器模型。

判别器网络

在 GAN 设置中,判别器的任务是区分真实和生成的(或虚假的)样本。在这种情况下,由于要检查的样本是一首音乐作品,所以模型需要有处理序列输入的能力。

为了处理顺序输入样本,我们使用一个简单的堆叠 RNN 网络。第一个递归层是一个具有 512 个单元的 LSTM 层,后面是一个双向 LSTM 层。第二层的双向性通过查看特定和弦或音符之前和之后的内容来帮助判别器更好地学习上下文。递归层后面是一堆密集层和一个用于二元分类任务的最终 sigmoid 层。判别器网络如下代码片段所示:

def build_discriminator(seq_shape):
  model = Sequential()
  model.add(LSTM(512, input_shape=seq_shape, return_sequences=True))
  model.add(Bidirectional(LSTM(512)))
  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'))
  model.summary()
  seq = Input(shape=seq_shape)
  validity = model(seq)
  return Model(seq, validity) 

如代码片段所示,判别器也是一个非常简单的模型,由几个递归和密集层组成。接下来,让我们将所有这些组件组合起来并训练整个 GAN。

训练与结果

第一步是使用我们在前几节介绍的实用程序实例化生成器和判别器模型。一旦我们有了这些对象,我们将生成器和判别器组合成一个堆栈,形成整体的 GAN。以下片段展示了三个网络的实例化:

rows = 100
seq_length = rows
seq_shape = (seq_length, 1)
latent_dim = 1000
optimizer = Adam(0.0002, 0.5)
# Build and compile the discriminator
discriminator = build_discriminator(seq_shape)
discriminator.compile(loss='binary_crossentropy', optimizer=optimizer, metrics=['accuracy'])
# Build the generator
generator = build_generator(latent_dim,seq_shape)
# The generator takes noise as input and generates note sequences
z = Input(shape=(latent_dim,))
generated_seq = 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(generated_seq)
# The combined model  (stacked generator and discriminator)
# Trains the generator to fool the discriminator
gan = Model(z, validity)
gan.compile(loss='binary_crossentropy', optimizer=optimizer) 

就像我们在前几章中所做的那样,在堆叠到 GAN 模型对象之前,首先将鉴别器训练设置为false。这确保只有在生成周期期间更新生成器权重,而不是鉴别器权重。我们准备了一个自定义训练循环,就像我们在之前的章节中多次介绍的那样。

为了完整起见,我们在此提供参考:

def train(latent_dim, 
          notes, 
          generator, 
          discriminator, 
          gan,
          epochs, 
          batch_size=128, 
          sample_interval=50):
  disc_loss =[]
  gen_loss = []
  n_vocab = len(set(notes))
  X_train, y_train = prepare_sequences(notes, n_vocab)
  # ground truths
  real = np.ones((batch_size, 1))
  fake = np.zeros((batch_size, 1))
  for epoch in range(epochs):
      idx = np.random.randint(0, X_train.shape[0], batch_size)
      real_seqs = X_train[idx]
      noise = np.random.normal(0, 1, (batch_size, latent_dim))
      # generate a batch of new note sequences
      gen_seqs = generator.predict(noise)
      # train the discriminator
      d_loss_real = discriminator.train_on_batch(real_seqs, real)
      d_loss_fake = discriminator.train_on_batch(gen_seqs, fake)
      d_loss = 0.5 * np.add(d_loss_real, d_loss_fake)
      #  train the Generator
      noise = np.random.normal(0, 1, (batch_size, latent_dim))
      g_loss = gan.train_on_batch(noise, real)
      # visualize progress
      if epoch % sample_interval == 0:
        print ("%d [D loss: %f, acc.: %.2f%%] [G loss: %f]" % (epoch, d_loss[0],100*d_loss[1],g_loss))
        disc_loss.append(d_loss[0])
        gen_loss.append(g_loss)
  generate(latent_dim, generator, notes)
  plot_loss(disc_loss,gen_loss) 

我们使用了与上一节相同的训练数据集。我们将我们的设置训练了大约 200 个时代,批量大小为 64。图 11.5展示了鉴别器和生成器在训练周期中的损失以及在不同时间间隔内的一些输出:

图 11.5:a)随着训练的进行,鉴别器和生成器损失。b)在不同训练间隔内生成器模型的输出

图中显示的输出突显了基于 GAN 的音乐生成设置的潜力。读者可以选择尝试不同的数据集,甚至是 Mogren 等人在 C-RNN-GAN 论文中提到的细节。生成的 MIDI 文件可以使用 MuseScore 应用程序播放。

与上一节中基于 LSTM 的模型相比,基于这个 GAN 的模型的输出可能会感觉更加精致一些(尽管这纯粹是主观的,考虑到我们的数据集很小)。这可能归因于 GAN 相对于基于 LSTM 的模型更好地建模生成过程的固有能力。有关生成模型拓扑结构及其各自优势的更多细节,请参阅第六章使用 GAN 生成图像

现在我们已经看到了两种单声部音乐生成的变体,让我们开始尝试使用 MuseGAN 进行复声音乐生成。

MuseGAN – 复声音乐生成

到目前为止,我们训练的两个模型都是音乐实际感知的简化版本。虽然有限,但基于注意力的 LSTM 模型和基于 C-RNN-GAN 的模型都帮助我们很好地理解了音乐生成过程。在本节中,我们将在已学到的基础上进行拓展,朝着准备一个尽可能接近实际音乐生成任务的设置迈出一步。

在 2017 年,Dong 等人在他们的作品《MuseGAN: 多轨序列生成对抗网络用于符号音乐生成和伴奏》中提出了一种多轨音乐生成的 GAN 类型框架⁵。这篇论文详细解释了各种与音乐相关的概念,以及 Dong 和他的团队是如何应对它们的。为了使事情保持在本章的范围内,又不失细节,我们将涉及这项工作的重要贡献,然后继续进行实现。在我们进入“如何”部分之前,让我们先了解 MuseGAN 工作试图考虑的与音乐相关的三个主要属性:

  • 多轨互依性:大多数我们听的歌曲通常由多种乐器组成,如鼓,吉他,贝斯,人声等。在这些组件的演奏方式中存在着很高的互依性,使最终用户/听众能够感知到连贯性和节奏。

  • 音乐结构:音符常常被分成和弦和旋律。这些分组以高度重叠的方式进行,并不一定是按照时间顺序排列的(这种对时间顺序的简化通常适用于大多数与音乐生成相关的已知作品)。时间顺序的排列不仅是出于简化的需要,也是从 NLP 领域,特别是语言生成的概括中得出的。

  • 时间结构:音乐具有分层结构,一首歌可以看作是由段落组成(在最高级别)。段落由各种短语组成,短语又由多个小节组成,如此类推。图 11.6以图像方式描述了这种层级结构:

Table 说明会自动生成

图 11.6:一首歌的时间结构

  • 如图所示,一根小节进一步由节拍组成,在最低的级别上,我们有像素。MuseGAN 的作者们提到小节作为作曲的单位,而不是音符,这是为了考虑多轨设置中的音符分组。

MuseGAN 通过基于三种音乐生成方法的独特框架来解决这三个主要挑战。这三种基本方法分别采用即兴演奏,混合和作曲家模型。我们现在简要解释一下这些方法。

即兴演奏模型

如果我们将前一节中的简化单声部 GAN 设置外推到多声部设置,最简单的方法是利用多个发电机-鉴别器组合,每个乐器一个。干扰模型正是这个设定,其中M个独立的发电机从各自的随机向量准备音乐。每个发电机都有自己的评论家/鉴别器,有助于训练整体 GAN。此设置如图 11.7所示:

图形用户界面 自动生成说明

图 11.7: 由 M 个发电机和鉴别器对组成的干扰模型,用于生成多轨道输出

如上图所示,干扰设置模拟了一群音乐家的聚合,他们通过独立即兴创作音乐,没有任何预定义的安排。

作曲家模型

如其名称所示,此设置假设发生器是一个典型的能够创建多轨钢琴卷的人类作曲家,如图 11.8所示:

图 11.8: 单发电机组成的作曲家模型,能够生成 M 轨道,一个用于检测假样本和真实样本的鉴别器

如图所示,这个设置只有一个鉴别器来检测真实或假的(生成的)样本。与前一个干扰模型设置中的M个随机向量不同,这个模型只需要一个公共随机向量z

混合模型

这是通过将干扰和作曲家模型结合而产生的有趣想法。混合模型有M个独立的发电机,它们利用各自的随机向量,也被称为轨内随机向量。每个发电机还需要另一个称为轨间随机向量的额外随机向量。这个额外的向量被认为是模仿作曲家并帮助协调独立的发电机。图 11.9描述了混合模型,每个发电机都需要轨内和轨间随机向量作为输入:

图形用户界面,徽标 自动生成说明

图 11.9: 由 M 个发电机和一个单一鉴别器组成的混合模型。每个发电机需要两个输入,即轨间和轨内随机向量的形式。

如图所示,混合模型的M发电机仅与一个鉴别器一起工作,以预测一个样本是真实的还是假的。将演奏和作曲家模型结合的优势在于生成器端的灵活性和控制。由于我们有M个不同的发电机,这个设定允许在不同的轨道上选择不同的架构(不同的输入大小、过滤器、层等),以及通过轨间随机向量的额外控制来管理它们之间的协调。

除了这三个变体,MuseGAN 的作者还提出了一个时间模型,我们将在下面讨论。

临时模型

音乐的时间结构是我们讨论的 MuseGAN 设置的三个重要方面之一。我们在前几节中解释的三个变体(即即兴、作曲家和混合模型)都在小节级别上工作。换句话说,每个模型都是一小节一小节地生成多音轨音乐,但可能两个相邻小节之间没有连贯性或连续性。这与分层结构不同,分层结构中一组小节组成一个乐句等等。

为了保持生成歌曲的连贯性和时间结构,MuseGAN 的作者提出了一个时间模型。在从头开始生成时(作为其中一种模式),该额外的模型通过将小节进行为一个附加维度来生成固定长度的乐句。该模型由两个子组件组成,时间结构生成器 G[时间] 和小节生成器 G[小节]。该设置在 图 11.10 中呈现:

图 11.10:时间模型及其两个子组件,时间结构生成器 G[时间] 和小节生成器 G[小节]

时间结构生成器将噪声向量 z 映射到一个潜在向量序列 z_vec = {z_vecᵗ}, t = 1 -> T。这个潜在向量z_vec携带时间信息,然后由 G[小节] 用于逐小节生成音乐。时间模型的整体设置如下所示:

作者指出,该设置类似于一些关于视频生成的作品,并引用了进一步了解的参考文献。作者还提到了另一种情况,其中呈现了一个条件设置,用于通过学习来生成由人类生成的音轨序列的时间结构。

我们已经介绍了 MuseGAN 设置的具体构建块的细节。现在让我们深入了解这些组件如何构成整个系统。

MuseGAN

MuseGAN 的整体设置是一个复杂的架构,由多个活动部分组成。为了使时间结构保持连贯,该设置使用了我们在前一节中讨论的两阶段时间模型方法。图 11.11 展示了 MuseGAN 架构的简化版本:

图表 11.11 的自动生成说明(中等置信度)

图 11.11:简化的 MuseGAN 架构,由 M 个生成器和一个判别器组成,以及一个用于生成短语连贯输出的两阶段时间模型。

如图所示,该设置使用时间模型用于某些音轨和直接的随机向量用于其他音轨。时间模型和直接输入的输出然后在传递给小节生成器模型之前进行连接(或求和)。

然后小节生成器逐小节创建音乐,并使用评论者或鉴别器模型进行评估。在接下来的部分,我们将简要触及生成器和评论者模型的实现细节。

请注意,本节介绍的实现与原始工作接近,但并非完全复制。为了简化并便于理解整体架构,我们采取了某些捷径。有兴趣的读者可以参考官方实现详情和引文工作中提到的代码库。

生成器

如前一节所述,生成器设置取决于我们是使用即兴演奏、作曲家还是混合方法。为简单起见,我们只关注具有多个生成器的混合设置,其中每个音轨都有一个生成器。

一组生成器专注于需要时间连贯性的音轨;例如,旋律这样的组件是长序列(超过一小节长),它们之间的连贯性是一个重要因素。对于这样的音轨,我们使用如下片段所示的时间架构:

def build_temporal_network(z_dim, n_bars, weight_init):
    input_layer = Input(shape=(z_dim,), name='temporal_input')
    x = Reshape([1, 1, z_dim])(input_layer)
    x = Conv2DTranspose(
        filters=512,
        kernel_size=(2, 1),
        padding='valid',
        strides=(1, 1),
        kernel_initializer=weight_init
    )(x)
    x = BatchNormalization(momentum=0.9)(x)
    x = Activation('relu')(x)
    x = Conv2DTranspose(
        filters=z_dim,
        kernel_size=(n_bars - 1, 1),
        padding='valid',
        strides=(1, 1),
        kernel_initializer=weight_init
    )(x)
    x = BatchNormalization(momentum=0.9)(x)
    x = Activation('relu')(x)
    output_layer = Reshape([n_bars, z_dim])(x)
    return Model(input_layer, output_layer) 

如图所示,时间模型首先将随机向量重塑为所需的维度,然后通过转置卷积层将其传递,以扩展输出向量,使其跨越指定小节的长度。

对于我们不需要小节间连续性的音轨,我们直接使用随机向量 z。在实践中,与节奏或节拍相关的信息涵盖了这些音轨。

时序生成器和直接随机向量的输出首先被连结在一起,以准备一个更大的协调向量。然后,这个向量作为输入传递给下面片段所示的小节生成器 G[bar]

def build_bar_generator(z_dim, n_steps_per_bar, n_pitches, weight_init):
    input_layer = Input(shape=(z_dim * 4,), name='bar_generator_input')
    x = Dense(1024)(input_layer)
    x = BatchNormalization(momentum=0.9)(x)
    x = Activation('relu')(x)
    x = Reshape([2, 1, 512])(x)
    x = Conv2DTranspose(
        filters=512,
        kernel_size=(2, 1),
        padding='same',
        strides=(2, 1),
        kernel_initializer=weight_init
    )(x)
    x = BatchNormalization(momentum=0.9)(x)
    x = Activation('relu')(x)
    x = Conv2DTranspose(
        filters=256,
        kernel_size=(2, 1),
        padding='same',
        strides=(2, 1),
        kernel_initializer=weight_init
    )(x)
    x = BatchNormalization(momentum=0.9)(x)
    x = Activation('relu')(x)
    x = Conv2DTranspose(
        filters=256,
        kernel_size=(2, 1),
        padding='same',
        strides=(2, 1),
        kernel_initializer=weight_init
    )(x)
    x = BatchNormalization(momentum=0.9)(x)
    x = Activation('relu')(x)
    x = Conv2DTranspose(
        filters=256,
        kernel_size=(1, 7),
        padding='same',
        strides=(1, 7),
        kernel_initializer=weight_init
    )(x)
    x = BatchNormalization(momentum=0.9)(x)
    x = Activation('relu')(x)
    x = Conv2DTranspose(
        filters=1,
        kernel_size=(1, 12),
        padding='same',
        strides=(1, 12),
        kernel_initializer=weight_init
    )(x)
    x = Activation('tanh')(x)
    output_layer = Reshape([1, n_steps_per_bar, n_pitches, 1])(x)
    return Model(input_layer, output_layer) 
shows that the bar generator consists of a dense layer followed by batch-normalization, before a stack of transposed convolutional layers, which help to expand the vector along time and pitch dimensions.

评论者

评论者模型相对于我们在前一节中构建的生成器来说更简单。评论者基本上是一个卷积 WGAN-GP 模型(类似于 WGAN,在 第六章 使用 GAN 生成图像 中涵盖的),它从小节生成器的输出以及真实样本中获取信息,以检测生成器输出是伪造的还是真实的。以下片段呈现了评论者模型:

def build_critic(input_dim, weight_init, n_bars):
    critic_input = Input(shape=input_dim, name='critic_input')
    x = critic_input
    x = conv_3d(x,
                num_filters=128,
                kernel_size=(2, 1, 1),
                stride=(1, 1, 1),
                padding='valid',
                weight_init=weight_init)
    x = conv_3d(x,
                num_filters=64,
                kernel_size=(n_bars - 1, 1, 1),
                stride=(1, 1, 1),
                padding='valid',
                weight_init=weight_init)
    x = conv_3d(x,
                num_filters=64,
                kernel_size=(1, 1, 12),
                stride=(1, 1, 12),
                padding='same',
                weight_init=weight_init)
    x = conv_3d(x,
                num_filters=64,
                kernel_size=(1, 1, 7),
                stride=(1, 1, 7),
                padding='same',
                weight_init=weight_init)
    x = conv_3d(x,
                num_filters=64,
                kernel_size=(1, 2, 1),
                stride=(1, 2, 1),
                padding='same',
                weight_init=weight_init)
    x = conv_3d(x,
                num_filters=64,
                kernel_size=(1, 2, 1),
                stride=(1, 2, 1),
                padding='same',
                weight_init=weight_init)
    x = conv_3d(x,
                num_filters=128,
                kernel_size=(1, 4, 1),
                stride=(1, 2, 1),
                padding='same',
                weight_init=weight_init)
    x = conv_3d(x,
                num_filters=256,
                kernel_size=(1, 3, 1),
                stride=(1, 2, 1),
                padding='same',
                weight_init=weight_init)
    x = Flatten()(x)
    x = Dense(512, kernel_initializer=weight_init)(x)
    x = LeakyReLU()(x)
    critic_output = Dense(1,
                          activation=None,
                          kernel_initializer=weight_init)(x)
    critic = Model(critic_input, critic_output)
    return critic 

一个需要注意的重点是使用 3D 卷积层。对于大多数任务,我们通常使用 2D 卷积。在这种情况下,由于我们有 4 维输入,需要使用 3D 卷积层来正确处理数据。

我们使用这些实用工具来为四个不同的音轨准备一个通用的生成器模型对象。在下一步中,我们准备训练设置并生成一些示例音乐。

训练和结果

所有组件都准备就绪。最后一步是将它们组合在一起,并按照典型 WGAN-GP 的训练方式进行训练。论文的作者提到,如果他们每更新 5 次鉴别器,就更新一次生成器,模型将达到稳定的性能。我们遵循类似的设置来实现 图 11.12 中显示的结果:

图 11.12:从 MuseGAN 设置中得到的结果展示了多轨输出,这在各个小节之间似乎是连贯的,并且具有一致的节奏。

如图所示,MuseGAN 产生的多轨多声部输出确实令人印象深刻。我们鼓励读者使用 MIDI 播放器(甚至是 MuseScore 本身)播放生成的音乐样本,以了解输出的复杂性及其相较于前几节中准备的简单模型的改进。

总结

恭喜你完成了另一个复杂的章节。在本章中,我们覆盖了相当多的内容,旨在建立对音乐作为数据源的理解,然后使用生成模型生成音乐的各种方法。

在本章的第一部分,我们简要讨论了音乐生成的两个组成部分,即乐谱表演生成。我们还涉及了与音乐生成相关的不同用例。下一部分集中讨论了音乐表示的不同方法。在高层次上,我们讨论了连续和离散的表示技术。我们主要关注1D 波形2D 频谱图作为音频或连续域中的主要表示形式。对于符号或离散表示,我们讨论了基于音符/和弦的乐谱。我们还使用music21库进行了一个快速的动手练习,将给定的 MIDI 文件转换成可读的乐谱。

当我们对音乐如何表示有了基本的了解后,我们开始构建音乐生成模型。我们首先研究的最简单方法是基于堆叠的 LSTM 架构。该模型利用注意力机制和符号表示来生成下一组音符。这个基于 LSTM 的模型帮助我们窥探了音乐生成的过程。

下一部分集中使用 GAN 设置来生成音乐。我们设计的 GAN 类似于 Mogren 等人提出的C-RNN-GAN。结果非常鼓舞人心,让我们深入了解了对抗网络如何被用于音乐生成任务。

在前两个动手练习中,我们将我们的音乐生成过程仅限于单声音乐,以保持简单。在本章的最后一节,我们的目标是理解生成复音轨/多轨音乐所需的复杂性和技术。我们详细讨论了* MUSEGAN,这是 2017 年由 Dong 等人提出的基于 GAN 的复音轨/多轨音乐生成架构。Dong 和他的团队讨论了多轨相互依赖音乐纹理时间结构三个主要方面,这些方面应该由任何多轨音乐生成模型处理。他们提出了音乐生成的三种变体,即即兴演奏作曲家混合模型。他们还讨论了时间小节*生成模型,以便更好地理解这些方面。MUSEGAN 论文将混音音乐生成模型作为这些更小组件/模型的复杂组合来处理多轨/复音轨音乐的生成。我们利用了这一理解来构建这项工作的简化版本,并生成了我们自己的复音轨音乐。

本章让我们进一步了解了可以使用生成模型处理的另一个领域。在下一章中,我们将升级并专注于令人兴奋的强化学习领域。使用 RL,我们也将构建一些很酷的应用程序。请继续关注。

参考

  1. Butcher, M. (2019 年 7 月 23 日). 看起来 TikTok 已经收购了英国创新音乐人工智能初创公司 Jukedeck。 TechCrunch. techcrunch.com/2019/07/23/it-looks-like-titok-has-acquired-jukedeck-a-pioneering-music-ai-uk-startup/

  2. Apple. (2021). GarageBand for Mac - Apple. www.apple.com/mac/garageband/

  3. Magenta. (未知发布日期) 使用机器学习创作音乐和艺术. magenta.tensorflow.org/

  4. Mogren, O. (2016). C-RNN-GAN:带对抗性训练的连续循环神经网络. NIPS 2016 年 12 月 10 日,在西班牙巴塞罗那举办的建设性机器学习研讨会(CML)。arxiv.org/abs/1611.09904

  5. Dong, H-W., Hsiao, W-Y., Yang, L-C., & Yang, Y-H. (2017). MuseGAN:用于符号音乐生成和伴奏的多轨序列生成对抗网络. 第 32 届 AAAI 人工智能会议(AAAI-18)。salu133445.github.io/musegan/pdf/musegan-aaai2018-paper.pdf

第十二章:用生成式人工智能玩视频游戏:GAIL

在之前的章节中,我们已经看到如何使用生成式人工智能来生成简单的(受限玻尔兹曼机器)和复杂的(变分自动编码器,生成式对抗模型)图像,音符(MuseGAN)和新颖文本(BERT,GPT-3)。

在所有先前的示例中,我们着重于使用深度神经网络生成复杂数据。然而,神经网络也可以用于学习实体(如视频游戏角色或车辆)如何响应环境以优化奖励的规则;正如我们将在本章描述的那样,这个领域被称为强化学习RL)。虽然 RL 并不是与深度学习或生成式人工智能有内在联系,但这些领域的结合已经创造出了一套优化复杂行为函数的强大技术。

在本章中,我们将向您展示如何将 GANs 应用于学习不同角色在 OpenAI 模拟环境中导航的最佳策略。为了理解这些方法与 RL 传统方法的强大组合,我们将首先回顾 RL 要解决的更一般问题:如何确定给定 状态 的实体的正确 动作,产生一个新的 状态 和一个 奖励。优化这些奖励的规则称为 策略。我们将讨论以下主题:

  • 深度神经网络如何用于学习高维数据(如 Atari 视频游戏的原始像素)的复杂策略。

  • 逆强化学习IRL)的问题 – 如何从观察“专家”智能体给出的策略示例中学习奖励函数 – 这种算法因此也被称为模仿学习,我们将更详细地描述。

  • 我们如何使用 GAN 训练函数来区分专家和非专家行为(就像我们在以前的示例中区分模拟数据和自然数据)来优化奖励函数。

强化学习:动作,智能体,空间,策略和奖励

第一章 《生成式人工智能导论:“从模型中“绘制”数据》》中回忆起,多数判别式人工智能示例都涉及将连续或离散标签应用于一条数据。在本书中我们讨论的图像示例中,这可以是应用深度神经网络来确定 MNIST 图像代表的数字,或者 CIFAR-10 图像是否包含一匹马。在这些情况下,模型产生了一个单一输出,一个最小误差的预测。在强化学习中,我们也希望进行这种点预测,但要经过多个步骤,并且在重复使用中优化总误差。

图 12.1:Atari 视频游戏示例¹

以一个具体的例子来说明,考虑一个玩家控制太空船击落外星飞船的视频游戏。在这个例子中,玩家操纵的太空船是代理;游戏中任意时间点屏幕上的像素集合就是状态。基于这个环境,玩家需要做出正确的动作(例如向右移动,按下开火),这将最大化奖励 – 这里确实是游戏的得分。我们不只需要考虑接下来的即时动作,而是所有动作直到游戏结束,因为只要我们不用完生命值,我们就可以累积额外的分数。专业的视频游戏玩家学会如何在不同情况下做出反应,一个策略,用以面对游戏过程中不同的情况。RL 的问题是确定一个机器学习算法,能够通过接收一组输入(游戏的当前状态)并输出最优动作来增加胜率,来复制这样一个人类专家的行为。

要使用数学符号正式描述这一情况,我们可以将环境,比如视频游戏,在其中代理作为ϵ来表示,这个代理在每个时间点输出一组数据(像素)作为状态 xₜ。在游戏的每个时间点,玩家(算法)从动作集合中选择 aₜ,该集合包括n个动作 A = {1, 2, …. N};这也被称为“动作集”或“动作空间”。虽然为了清晰起见,我们在本章中限制了对离散动作空间的讨论,但理论上并没有这样的限制,这个理论同样适用于连续动作(尽管由此产生的 RL 问题会更加复杂,因为有无穷无尽的可能性)。对于这些动作中的每一个,代理会获得奖励 rₜ,这可能会改变游戏得分。

如果我们只考虑当前屏幕 xₜ 作为系统的“状态”,那么我们的决策只依赖于当前,并且 RL 问题就成为了马尔可夫决策过程MDP),因为下一步动作的选择只依赖于即时可用的数据,而不依赖于历史(图 12.2)。然而,在上述给定的视频游戏例子中,仅仅考虑当前屏幕可能不足以确定最优动作,因为它只是局部可观测 – 我们不知道敌方星际飞船是否已经移出屏幕(因此它可能会重新出现)。我们也不知道我们的飞船移动的方向,而不经过比较之前的例子我们就无法得知,这可能会影响我们是否需要改变方向。如果环境的当前状态包含了我们需要知道的游戏的所有信息 – 比如一场扑克游戏,所有玩家都展示自己的牌 – 那么我们就说这个环境是完全可观测的。²

图 12.2:马尔可夫决策过程(MDP)³。通过具有一定概率的行动(橙色圆圈)之间的状态(绿色圆圈)的转移(黑色箭头)得到奖励(橙色箭头)。

的确,人类的视频游戏玩家并不仅仅依赖游戏的当前状态来确定下一步该做什么;他们还依赖游戏之前的提示,比如敌人离开屏幕的时间点,以便预料它们再次出现。

类似地,我们的算法将受益于使用导致当前状态s={x[1]…,x[t];a[1]...a[t-1]}的一系列状态和行动作为当前决策的输入。换句话说,“状态”变成了以前状态和行动的这个序列,并且我们仍然可以使用为 MDP 开发的方法来解决这个 RL 问题。

在每个时间点,基于这个状态(历史),我们希望做出决定,以最大化游戏结束时的未来奖励 R。直觉上,我们通常更擅长估计我们即将采取的行动的结果,而不是它们对未来的长期影响,因此我们需要使用一个贴现项,γ,在计算预期奖励时对近期和长期采取行动的影响进行估计:

这里,t'是当前时间点t和游戏结束T之间的时间点。我们可以看到基于γ的值,未来奖励函数有三种潜在的解释:

  • γ = 0. 未来的奖励对我们的决策没有影响,我们只关心当前的奖励。

  • 0 < γ < 1. 随着t't之间的距离增加,这个乘法因子的指数会变得更大,贴现项变得更小,将未来奖励缩小到无限远处为 0。在这种情况下,我们更加重视近期目标而不是远期目标。

  • γ = 1. 在这种情况下,我们的环境可能是确定性的,因此我们不需要贴现未来的奖励,因为结果没有不确定性,但即使在随机环境中,使用γ < 1γ = 1可以看作是在奖励计算之间进行正则化的选择(或者选择不进行正则化)。当贴现因子<1 时,该算法受到(潜在稀疏的)长期数据的影响较小,因此可以帮助解决行动空间非常大且在没有进行贴现的情况下训练很多未来步骤可能导致在该空间中单个路径上过拟合的情况。

我们在这里描述的 RL 问题的目标是学习一个值函数Q,它在给定一个序列和一个策略π的情况下最大化奖励-将这写成"Q"函数的约定来自于这个函数评估 RL 算法做出的决策的“质量”。

换句话说,给定一个状态(或者,在我们的情况下,一系列状态)和一个提议的动作,Q 函数根据最大的总未来奖励 R 对提议的动作进行评分。这个 Q 函数通常是未知的,我们需要解决它 - 然而,最优的 Q 函数遵循一条基于动态规划的重要规则,称为最优性原则

“最优策略具有这样的性质:无论初始状态和初始决策是什么,剩余的决策必须构成一个对于第一个决策产生的状态而言的最优策略。”⁶

换句话说,无论起始状态如何,一旦我们由于第一个动作而转移到新状态,所有后续动作都必须在最优策略下是最优的。如果我们将这个原则以数学形式写出,它将值函数分成两部分,形成一个递归表达式,称为贝尔曼方程

其中 Q* 是“最优”的 Q 函数,E 是期望值,而ζ是从中采样未来状态 s' 的环境。这个表达式表示最优的 Q 函数应该给出当前序列和提议动作的期望值,作为当前奖励 r 和下一系列动作的折现未来值 Q*(s', a') 之和的期望值。这个表达式也被称为函数表达式,因为解决方案是函数 Q*,因此这个一般类问题称为“Q 学习”。⁷ 在 Q 学习中解决 Q* 的一种方法是通过值迭代

其中α是一个学习速率,决定了如何快速地根据新信息修改函数。如果我们对足够大的is运行此更新,我们可以收敛到Q的最优值,Q*。 ^(8, 9) 对于简单问题,我们只有少量的动作和状态,我们可以创建一个表格,给出任何潜在动作的Q值。该表中的条目对于每个Q(s, a)都是随机初始化的,并使用值迭代公式进行更新:

状态/动作 A1 A2 A3 A4
S1 Q(S1, A1)
S2
S3

表 12.1:Q 学习的查找表。每个单元格包含特定状态(S)和动作(A)对的 Q 值。值是随机初始化的,并使用值迭代进行更新。

然而,正如之前的作者所指出的¹,对于我们所描述的视频游戏场景这样的问题,几乎不可能枚举所有潜在的序列和动作。此外,由于任何特定序列-动作对的可能性非常低,我们需要采样大量示例才能准确地估计使用值迭代来估计 Q 值。

为了使这个问题更具有计算可行性,我们需要将 Q 从查找表转变为一个可以对它没有见过的状态-动作对的值进行推广估计的函数。为了做到这一点, Q 可以用一些参数 θ表示:该函数可以是线性的,树模型的,或者在现代应用程序中甚至是我们在本书的其他部分中学习的深度神经网络。在这种情况下,我们的目标更像是一个经典的学习算法,我们尝试最小化:

使用

p 是代理在环境中执行的潜在非常大空间的行动和序列的分布ζ。由于 L 是可微分的,我们可以像 第三章 中讨论过的深度学习算法一样使用随机梯度下降来优化它:

通过固定参数并优化 L,然后从 ps 构建新的样本,我们得到了一个类似于上面给出的值迭代 Q-学习的迭代更新算法。¹⁰

这种方法在 RL 方法的层次结构中属于哪个位置?一般来说,RL 方法根据以下几点进行分类:

  • 是否使用静态(离线)数据集,还是持续交付给系统的新数据进行训练(在线)。¹¹

  • 策略函数是否使用 Q 进行更新(on-policy)或独立更新(off-policy)。¹²

  • 过渡方程是否明确建模(基于模型)或不是(无模型)。¹²

在所有可能的 RL 算法变体中,上面描述的 Q-学习版本被分类为off-policy, online, and model-free。虽然我们可以使用 Q 的值作为选择下一个行动的策略,我们从 p(]) 的样本不需要遵循这个策略。

在实践中,一个 epsilon-贪婪分布通常用于想要为算法引入一些随机性以防止其陷入局部最小值的问题,比如深度 Q-学习(稍后介绍),它选择“贪婪”(最大化 Q)的行动概率为 e,以及随机行动概率为 1-e。因此,我们正在学习的策略(Q)不严格用于选择具有这种随机性的行动(a)。这种方法是无模型的,因为神经网络近似了过渡模型,并且是在线的,因为它是在动态生成的数据集上学习的,尽管也可以使用静态离线视频游戏会话历史进行训练。

相比之下,在on-policy的 Q 学习算法中,比如状态-动作-奖励-状态-动作SARSA)¹³,我们使用直接更新 Q 函数的方法,就像上面描述的值迭代步骤那样。与离策略示例不同,我们不是根据从分布p(有时称为行为策略)生成的样本或探索性选择的动作来计算最优Q;相反,我们是根据策略选择动作,并可能在学习 Q 的过程中更新该策略。

深度 Q 学习

尽管深度学习领域独立于强化学习方法,如 Q 学习算法,但这两种方法的强大组合被应用于训练算法以接近人类水平地玩街机游戏。¹ 在这项研究中的一个重要见解是将深度神经网络应用于从视频游戏的原始像素生成向量表示,而不是试图显式地表示游戏状态的一些特征;这个神经网络是这个 RL 算法的 Q 函数。

另一个关键的发展是一种称为经验回放的技术,其中状态的历史(在这里,是游戏中的视频帧像素)、行动和奖励被存储在一个固定长度的列表中,并且反复随机重新采样,通过使用上述的ε-贪心方法来以一定的随机可能性选择一个非最优结果。结果是,对于相同数据的许多样本,值函数的更新被平均化,并且连续样本之间的相关性(可能使算法仅探索解空间的有限集合)被打破。此外,这种“深度”Q 学习算法是离策略实现的,以避免使用联合优化策略函数生成最优样本的潜在循环反馈。

深度 Q 学习也是无模型的,即我们没有环境E的表示或模型(比如一个可以模拟游戏新帧的生成模型)。事实上,就像视频游戏的例子一样,它可能只是表示被玩家观察到的游戏的“内部状态”的历史数据样本。

将这些部分组合在一起,深度 Q 学习算法使用以下步骤来学习玩 Atari 游戏¹⁴:

  1. 创建一个列表,以存储(当前状态、动作、奖励、下一个状态)的样本作为“回放内存”。

  2. 随机初始化表示 Q 函数的神经网络中的权重。

  3. 对于一定数量的游戏序列,初始化一个起始游戏屏幕(像素)和对这个输入的转换(比如最后四个屏幕)。这个固定长度历史的“窗口”很重要,因为否则 Q 网络将需要容纳任意大小的输入(非常长或非常短的游戏屏幕序列),这个限制使得将卷积神经网络应用于这个问题变得更容易。

  4. 对于游戏中的一定步骤(屏幕),使用ε-greedy 采样来选择给定当前屏幕和通过 Q 计算的奖励函数的下一个动作。

  5. 更新状态后,将这个转换(当前状态,动作,奖励,动作,下一个状态)保存到重播记忆中。

  6. 从重播记忆中随机选择一组(当前状态,动作,奖励,下一个状态)的转换,并使用 Q 函数计算它们的奖励。使用随机梯度下降来基于这些转换更新 Q。

  7. 对许多游戏和游戏步骤继续步骤 3-6,直到 Q 网络中的权重收敛。

虽然 Q-learning 的其他应用与其特定领域有关,但是使用深度神经网络来近似大量可能结果的 Q 函数的一般方法(而不是可以在表中表示的一小部分状态和动作)在许多情况下都被证明是有效的。其他应用深度 Q-learning 的例子包括:

  • 使用 CNN 处理围棋(一种类似于国际象棋的东亚游戏)游戏板上的位置,并应用 Q-learning 来确定游戏中基于人类玩家的历史例子的下一个最佳移动;2015 年发布了名为“AlphaGo”的模型。¹⁵

  • 对 2017 年 AlphaGo 发布的创新,命名为“AlphaGo Zero”,该程序完全是从合成游戏(两个 RL 代理相互对弈)中学习,而不是从人类玩家的历史示例中学习。¹⁶

  • AlphaGo Zero 的更一般形式“AlphaZero”,也使用自我对弈掌握了国际象棋和将棋。¹⁷

  • AlphaStar 是一种 RL 算法,可以在多人实时战略游戏星际争霸中击败人类大师。¹⁸

  • 一个名为“AlphaFold”的模型,可以根据其 2D 序列预测蛋白质的 3D 结构 - 我们将在第十三章生成 AI 的新兴应用中更详细地描述 AlphaFold。

我们迄今为止描述了 Q-learning 及其变体,以及如何通过深度学习增强 Q-learning 以应对复杂的环境和数据集。然而,我们描述的问题都共享一个共同特征,即我们能够用定义明确的数学术语表达奖励函数(例如游戏中的得分)。在许多现实场景中 - 例如训练 RL 代理驾驶汽车 - 这个奖励函数并不容易定义。在这些情况下,我们可能不是编写奖励函数,而是使用人类驾驶员的示例作为奖励的隐式表示 - 这是一种称为逆强化学习的方法,我们将在下面更详细地描述。

逆强化学习:从专家中学习

上面的深度 Q 学习示例依赖于一个明确的奖励函数——游戏中的分数。然而,我们并不总是能够访问明确的奖励函数,包括在重要的现实场景中,比如自动驾驶汽车。在驾驶员根据道路上的环境选择导航的情况下,我们会给予什么“奖励”值呢?虽然我们对“正确”决定有直观的感觉,但为这样的奖励函数量化详尽的规则将是具有挑战性的。

我们可以不必尝试确定奖励函数,而是观察专业驾驶员执行复杂的操作,比如在多车道交通中合并,并优化一个行为模仿专家的代理。这是一个更一般的问题,被称为模仿学习。模仿学习的一种形式是行为克隆,¹⁹它遵循以下算法:

  1. 从专家行为中收集一组状态、动作对!

  2. 学习一个策略函数a = π(s, a),其中π是一种监督分类算法。

虽然这种方法很直接,但需要大量数据才能创建一个能够推广到未知环境的分类器,特别是当潜在环境的数量很大时(例如自动驾驶汽车的情况)。²⁰

特别是,行为克隆的成功取决于未来环境的分布与训练数据的相似性,当模型的结果能够影响后续观察结果的分布时,这一点尤其困难。例如,自动驾驶汽车在道路上的选择成为重新训练模型的进一步数据,导致潜在的复合错误和数据漂移的反馈循环。²¹

另一种方法是学习一个代理,试图匹配专家的整个轨迹的结果,而不是单个动作,就像行为克隆中一样。然后,该算法的输出是一个在轨迹上将"专家行为"评分高于新手行为的函数。这种方法被称为逆强化学习IRL),因为它颠倒了常见的模式(图 12.3)-在左侧,我们看到了一个典型的反馈循环,如我们为 Atari 玩深度 Q 网络描述的那样,其中一个代理(蓝色)观察到一个状态(s)并使用奖励函数(R)选择一个动作(a)产生一个转移(T)到一个新状态和一个奖励(r)。相比之下,在右侧,从这些状态、动作和转移中产生的奖励隐含地由来自专家(E)的例子表示,代理(蓝色)学习复制这个序列通过一个学习到的奖励函数(R[E]),而不是显式地"在算法的循环中"。换句话说,我们不是从显式奖励函数中学习策略,而是观察专家的行为并推断出一个会导致他们观察到的动作的奖励函数。

图 12.3:强化学习(a)和逆强化学习(b)²²

如何通过从头开始学习奖励函数来量化"专家"的行为呢?如果我们重新审视我们之前的视频游戏示例,我们可以检查专家人类游戏的像素屏幕 x 和动作 ax₁a₁x₂a₂...)序列,这些序列构成了一个完整的会话,并尝试找到一个函数 f,该函数会为给定的游戏给出总奖励:

然后,我们可以询问给定的函数 f 是否倾向于复制专家玩家的行为,而不是其他替代方案。然而,存在固有的问题-多个 f 可能会给出相同的奖励结果,使得不清楚哪一个(可能性很多)最能泛化到新数据。²³

或者,我们可以尝试优化一个代理,使其产生的观察轨迹与专家的概率相同;换句话说,我们从跟随该代理产生的序列中看到的分布与从专家行为中随机抽取的序列相同,而优化算法基于最小化提出的和从专家处观察到的序列的经验分布之间的差异。²⁴ 这个预期分布(无论是观察到的还是由代理生成的)可以用以下方式表示:

这里P是轨迹(ζ)的概率,f是状态的特征,例如视频游戏示例中的观察像素。我们已经解决了解决模糊奖励函数的问题,但我们仍然面临着许多代理可能导致相同行为的可能性。我们甚至可能需要混合不同的策略或奖励函数来模拟给定的专家行为,并且目前尚不清楚如何选择这些。

图 12.4:来自(非)确定性 MDP 的路径轨迹。(b)是确定性 MDP(a)的特定路径(轨迹),而(d)是来自非确定性 MDP(c)的样本路径,其中对于状态动作a₄可能导致的情况存在歧义。(23)

当研究受限玻尔兹曼机时,我们可以引用我们在第四章教网络生成数字中看到的分区函数和玻尔兹曼分布。如果我们将由 RL 代理表示的 MDP 并“展开”沿着对一组状态作出反应的一组特定动作的轨迹,我们将得到一组树形图中的变长路径,如图 12.4所示。

通过遵循不同策略,可以获得对这些路径发生的不同分布,即使具有相同的像素特征分布,某些策略可能也会根据特定的奖励函数更喜欢一条路径而不是另一条路径。为了解决这种歧义,我们可以优化具有参数Θ的奖励函数,以使基于这些特征的相同奖励函数值的路径获得相同的偏好,但我们指数地偏爱更高的奖励:

这种方法被称为最大熵;从第四章教网络生成数字中回忆,Z是分区函数,将P归一化为关于由线性奖励函数θᐪ评估的步骤sⱼ组成的平均轨迹f的概率密度。(25)即使在非确定性结果(图 12.4,(b)和(d))的情况下,这也可能是可能的,例如,在计算机随机生成敌方太空船行为的视频游戏中,我们可以将此方程参数化为过渡分布T的条件。(23)

这里,分布与以前相似,但我们增加了基于行动a和状态s使用过渡模型T观察结果o的概率PₜI表示指示函数,当ζ[ε0] 时评估为 1,否则为 0。然后,我们可以优化此方程以找到最大化此函数概率的奖励函数参数:

此似然的梯度仅是期望的特征计数(例如,像素分布)来自专家游戏和通过遵循提出的代理人获得的特征计数之间的差异,根据访问给定状态D的频率:

这给了我们一个优化的目标,但是我们可以选择使用什么样的P。 我们将研究的是最大因果熵,在其中给定行动的概率(因此,状态和路径分布)条件于先前的状态和行动集,就像我们先前的视频游戏示例中一样^(26 27):

因为如果路径永远不终止,这个熵值可能是无限的,所以我们可以应用一个折现因子使其有限²⁶:

其中折现因子B应用于每个时间步t,期望值为初始状态P₀的分布和随后状态P的分布,受到策略π的影响;H(A || S)表示因果熵H(A || S) = E[-logP(A || S)];而P(A || S)是因果条件概率= ΠP(A[t]|s[0:t], A[0:t-1]), t = 0 -> T(在序列中先前行动的条件概率)。

因此,在这一点上,我们有一个优化目标(折现因果熵),以及计算为特定代理人制定奖励函数的概念,使其行为类似于专家。 这个目标与生成式人工智能有什么联系呢? 答案在于我们如何在区分专家和学习者之间进行类比,并从学习者中生成样本——与我们在第 6第 7第 8章中研究过的 GAN 类似!

对抗学习和模仿

给定一组专家观察(驾驶员或冠军电子游戏玩家),我们希望找到一个奖励函数,为与专家行为匹配的代理人分配高奖励,并为不匹配的代理人分配低奖励。 同时,我们希望在此奖励函数下选择这样一个代理人的策略π,使其尽可能地信息丰富,通过最大化熵和偏好专家而不是非专家的选择。 我们将展示如何通过一种名为生成对抗性模仿学习GAIL)的算法在 2016 年发表的方法来实现这两点。²⁰

在接下来的内容中,我们使用“成本”函数而不是“奖励”函数来匹配此主题的参考文献中使用的约定,但它只是奖励函数的负值。

将这些约束放在一起,我们得到²⁰:

在内部项中,我们试图找到一种最大化折现熵的策略:

因此导致一个大的负值,并最小化成本项。我们希望通过选择一个不仅满足内部约束而且提供低成本专家样式行为的成本函数来最大化不同潜在成本函数的整体表达式。请注意,该内部项也等价于一个寻求其行为优化目标的 RL 问题:

在可能策略空间上π 上,记为 Π。然而,为了限制 c 的可能选择空间,我们应用一个正则化函数Ψ(c),它在许多可能选择中寻找低复杂度的 c,并将此约束添加到整体目标中:

我们可以看到我们可以在给定策略(IRL)下交替优化 c,并在该 c 下优化策略(RL) - 这正是我们将要采取的方法。我们可以用前面描述的方法表达最优策略,作为对在该策略下不同状态的 占用度 (ρ) 的度量:

然后,RL 和 IRL 问题的最优点等价于最小化学习和专家占用之间的差异:

其中o表示函数合成,f o g(x) = f(g(x)),这里表示 RL 代理将 IRL 输出作为其参数之一。然而,我们收到的专家行为示例通常是有限的,因此我们不希望完全模仿它们的分布,因为那可能导致代理过度拟合。相反,我们希望使用距离度量,例如我们在 第六章 中看到的 KL 散度或 JS 散度,来量化观察到的专家行为分布与试图近似该行为的 IRL 代理采取的动作分布之间的差异:

在这个算法中应该使用什么样的正则化函数,Ψ(c)?GAIL 算法的作者提出了一个函数,如果成本低则分配低惩罚,反之则分配高惩罚。在论文《生成对抗模仿学习》中给出了满足此属性的正则化函数的完整推导:

其中

这个函数的直观解释是,它最小化了策略 H 的因果熵与惩罚项λ(例如,学习策略的可能性)之间的差异,以及专家和学习策略状态的占用差异D_JS(ρ[π], ρ[πE])

这个D_JS(ρ[π], ρ[πE])Ψ*_GA(ρ[π], ρ[πE]) 可以写成一个二元分类器的负对数损失,用于区分专家(0)和学习(1)的行动状态对:

这个表达式可能看起来非常熟悉,因为它类似于 第六章 使用 GAN 生成图像 的目标函数,也用于区分生成和真实数据!将这些术语组合在一起(正则化器和因果熵),我们可以看到完整的目标函数是:

要优化这个目标函数,GAIL 算法利用以下步骤:

  1. 准备一组专家轨迹并随机初始化鉴别器和策略参数。

  2. 为 RL 代理生成一组轨迹。

  3. 使用随机梯度下降步骤更新鉴别器参数。

  4. 使用 2015 年发布的名为Trust Region Policy OptimizationTRPO)的算法,使用基于梯度的更新来更新策略参数 - 请参阅原始文章"Trust Region Policy Optimization"了解有关此梯度更新形式的更多详细信息。²⁸

  5. 重复算法的 步骤 2-4 直到策略和鉴别器参数的值收敛。

现在我们已经解释了使用生成式 AI 进行强化学习的直觉,让我们深入到在虚拟环境中训练步行人形的一个实际示例中。

在 PyBullet Gym 上运行 GAIL

在本章的代码示例中,我们将训练一个虚拟代理在一个模拟环境中导航 - 在许多 RL 论文中,这个环境是使用 Mujoco 框架 (www.mujoco.org/) 进行模拟的。Mujoco 代表 Multi joint dynamics with contacts - 这是一个物理“引擎”,可以让您创建一个人工代理(如摆锤或双足人形),其中“奖励”可能是通过模拟环境移动的能力。

尽管它是用于开发强化学习基准的流行框架,例如由研究小组 OpenAI 使用(请参阅github.com/openai/baselines以了解其中一些实现),但它也是闭源的,并且需要许可证才能使用。对于我们的实验,我们将使用 PyBullet Gymperium (github.com/benelot/pybullet-gym),这是一个用于模拟 Mujoco 环境中的代理的物理模拟器和导入代理的替代品。

图 12.5: 来自 Mujoco 模拟环境的示例(http://www.mujoco.org/)

要安装 Pybullet-Gym,您需要先安装 OpenAI Gym,使用以下命令:

git clone https://github.com/openai/gym.git 
cd gym 
pip install -e . 

然后安装 Pybullet:

git clone https://github.com/benelot/pybullet-gym.git 
cd pybullet-gym 
pip install -e . 

为了展示这个模拟环境是如何工作的,让我们创建一个“hopper”,这是您可以使用库实例化的许多虚拟代理之一:

import gym  
import pybulletgym  
env = gym.make('HopperMuJoCoEnv-v0')
env.render("human")
env.reset() 

如果命令执行正确,我们将看到以下输出,即给出行走者当前观察(11 维向量)的数组。

图 12.6:行走者的观察向量

render("human") 的调用将创建一个窗口,显示“蹦床”,这是一个简单的单脚人物,在模拟的 3D 环境中移动(图 12.7):

图 12.7:PyGym 蹦床

我们可以运行几次原始的蹦床迭代,以了解其外观。在这个模拟中,我们最多进行 1,000 步,并使用弹出窗口进行可视化:

env.reset() 
for t in range(1000):
    action = env.action_space.sample()
    _, _, done, _ = env.step(action)
    env.render("human")
    if done:
        break 

我们首先使用 reset() 清除环境。然后,在最多 1,000 个时间步长内,我们会对动作空间进行采样(例如,表示行走图形在虚拟环境中移动的 xyz 坐标)。然后,我们使用该动作获取更新的奖励和观察,并渲染结果,直到移动完成。

这个演示来自完全未经训练的蹦床。对于我们的 GAIL 实现,我们将需要一个已成功训练行走的蹦床,作为算法的“专家”轨迹样本。为此,我们将从 OpenAI 网站下载一组蹦床数据,网址为:

drive.google.com/drive/folders/1h3H4AY_ZBx08hz-Ct0Nxxus-V1melu1U

这些包含一组 NumPy 文件,例如deterministic.trpo.Hopper.0.00.npz,其中包含使用前面我们讨论过的 GAIL 算法中的 TRPO 算法训练的增强学习代理的数据样本。

如果我们加载这些数据,我们还可以使用 Pybullet 模拟器进行可视化,但这次我们将看到专家的步骤,而不是随机基线代理:

import numpy as np
mujoco_hopper_np = np.load('deterministic.trpo.Hopper.0.00.npz')
for i_episode in range(20):
    observation = env.reset()
    episode = np.random.choice(mujoco_hopper_np['acs'].shape[0])
    for t in range(1000):
        env.render("human")
        action = mujoco_hopper_np = \
        np.load('deterministic.trpo.Hopper.0.00.npz')['acs'][episode][t]
        observation, reward, done, info = env.step(action)
        if done:
            print("Episode finished after {} timesteps".format(t+1))
            break
env.close() 

此代码加载了预先训练的蹦床,初始化了虚拟环境,并在其中最多进行 1,000 步,在这些步骤中,动作(蹦床的下一个移动)是使用蹦床的训练策略函数确定的,根据该动作更新环境状态(蹦床的位置)。注意,这里的策略函数是确定性的,导致在任何给定时间 t 的任何给定动作都会产生相同的结果。您现在可以看到蹦床迈出了许多步:

图 12.8:由 TRPO 训练的专家策略的蹦床移动

让我们更仔细地看一下我们加载的这个 NumPy 对象中有哪些数据。您会注意到,格式 .npz 是一组压缩文件的 gzip 存档。我们可以通过使用对象 mujoco_hopper_np 的 files 参数来查看这些存档的名称:

print(mujoco_hopper_np.files) 

得到:

['ep_rets', 'obs', 'rews', 'acs'] 

观察是 11 维对象,您可以通过查看 obs 的维度来验证:

print(mujoco_hopper_np['obs'].shape) 

该数组有 1,500 个示例,每个 1,000 个时间步长,每个时间步长有 11 个表示不同物理量(铰链位置、扭矩等)的维度。蹦跶者任务的目标是尽可能快地向前移动,所以当代理学会向前移动时,奖励函数会更高。如果我们检查acs数据,我们会发现它有三个维度,对应于三维空间中的点。这是一个连续的动作空间,不同于我们之前讨论的离散示例。

print(mujoco_hopper_np ['acs'].shape) 

ep_rets对应于在时间 t 执行动作的未来预测奖励,而奖励rews是奖励函数的输出。

代理:演员-评论家网络

要创建我们的 GAIL 实现,首先我们需要指定一个代理。(29)这是演员-评论家架构,由两个网络组成:一个学习观察结果的“值”(评论家),另一个(演员)基于观察结果进行动作采样。这些网络可以是独立的,也可以共享参数;对于我们的实验,我们让它们共享隐藏层和输入层,但有单独的输出层(图 12.9)。

图 12.9:演员-评论家架构

注意,本章 GAIL 实现的代码基于github.com/fangihsiao/GAIL-Tensorflow/blob/master/tf_gail.ipynb

下面我们定义ActorCritic类:

import tensorflow_probability as tfp
import tensorflow as tf
tfd = tfp.distributions
class ActorCritic(tf.keras.Model):
    def __init__(self, name='actor_critic', dim_actions=3, num_layers=2, input_shape=(11), num_units=100, **kwargs):
        super().__init__(name=name, **kwargs)
        self._num_layers = num_layers
        self._num_units = num_units
        self._dim_actions = dim_actions
        self._layers = list()
        for n, l in enumerate(range(self._num_layers)):
            self._layers.append(tf.keras.layers.Dense(
                                          self._num_units,
                                          activation=tf.nn.relu))
            if n == 0:
                self._layers[-1].build(input_shape)
            else:
                self._layers[-1].build((num_units))
            self._layers.append(tf.keras.layers.BatchNormalization())
        self._value_output = tf.keras.layers.Dense(1,activation=None)
        self._value_output.build((num_units))
        self._action_output = tf.keras.layers.Dense(
                                          self._dim_actions,
                                          activation=tf.nn.tanh)
        self._action_output.build((num_units))
        self._action_dist_std = tf.Variable([1., 1, 1], 
                                            trainable=False)
        self._action_dist =  None
    def get_params(self):
        weights = []
        for layer in self.layers:
            weights += layer.trainable_weights
        return weights+\
            self._action_output.trainable_weights + \
            self._value_output.trainable_weights + \
            [self._action_dist_std]
    def call(self, inputs):
        x = self._layers0
        for layer in self._layers[1:self._num_layers]:
            x = layer(x)
        return self._value_output(x)
    def log_prob(self, x):
        return self._action_dist.log_prob(x)
    def sample(self, inputs, output='action'):
        x = self._layers0
        for layer in self._layers[1:self._num_layers]:
            x = layer(x)
        self._action_dist = tfd.Normal(self._action_output(x),
                                       [1,1,1])
        if output == 'action':
            return self._action_dist.sample()
        elif output == 'entropy':
            return tf.reduce_mean(self._action_dist.entropy())
        else:
            raise ValueError("unknown sample type: {}".format(output)) 

该类初始化一个接受输入状态和动作对并生成两个输出的网络-一个生成新动作(代表虚拟空间中蹦跶者的下一步移动的 3D 坐标)-演员-另一个生成值(表示蹦跶者在虚拟空间中移动的成功程度)-评论家。值输出是一个单一标量,随着蹦跶者运动质量的提高而增加,而动作是一个 3 单元向量,表示在 3D 空间中移动的每个坐标的均值和标准偏差。

因为我们的网络有多个输出,所以在设置输入层和初始化它们时需要小心。请注意,我们明确调用了两个输出层的build,而不是让它们在前向传递中自动实例化,因为这将导致模型编译错误。我们还实例化了一个变量_action_dist_std,包含动作维度的标准偏差,我们将在模型中用它来采样新坐标。我们还包括了BatchNormalization层,以防止我们网络中的梯度爆炸或消失。(29)

我们还需要能够返回模型中可训练的参数,以便进行梯度计算,使用Actor-Critic网络的get_params方法:

 def get_params(self):
        weights = []
        for layer in self.layers:
            weights += layer.trainable_weights
        return weights+\
            self._action_output.trainable_weights + \
            self._value_output.trainable_weights + \
            [self._action_dist_std] 

在我们的前向传递中,我们计算了评论家的输出:

 def call(self, inputs):
        x = self._layers0
        for layer in self._layers[1:self._num_layers]:
            x = layer(x)
        return self._value_output(x) 

为了从 Actor 中抽样新的动作(3D 移动),我们使用参数 'action' 运行样本函数 - 如果我们提供 'entropy' 替代,则返回动作分布的熵:

def sample(self, inputs, output='action'):
        x = self._layers0
        for layer in self._layers[1:self._num_layers]:
            x = layer(x)
        self._action_dist = tfd.Normal(self._action_output(x), [1,1,1])

        if output == 'action':
            return self._action_dist.sample()
        elif output == 'entropy':
            return tf.reduce_mean(self._action_dist.entropy())
        else:
            raise ValueError("unknown sample type: {}".format(output)) 

最后,我们需要能够返回 PPO 网络中动作分布的对数概率(用于我们的损失函数),如下所述:

 def log_prob(self, x):
        return self._action_dist.log_prob(x) 

我们的 IRL 代理 - 我们将使用 Proximal Policy Optimization (PPO) 策略更新,这是 2017 年发表的 TRPO 的改进²⁹ - 利用这个 Actor-Critic 网络作为“策略”函数。

class PPO(tf.keras.Model):

    def __init__(self, name='ppo', dim_actions=3, num_layers=2, num_units=100, eps=0.2, v_coeff=0.5, ent_coeff=0.01, lr=3e-2, **kwargs):
        super().__init__(name=name, *kwargs)
        self._dim_actions = dim_actions
        self._num_layers = num_layers
        self._num_units = num_units
        self._eps = eps
        self._v_coeff = v_coeff
        self._ent_coeff = ent_coeff
        self._policy = ActorCritic(num_layers=self._num_layers, 
                                   num_units=self._num_units,
                                   dim_actions=self._dim_actions)
        self._new_policy = ActorCritic(num_layers=self._num_layers, 
                                       num_units=self._num_units,
                                       dim_actions=self._dim_actions)
        self._policy.compile(run_eagerly=True)
        self._new_policy.compile(run_eagerly=True)
        self._optimizer = tf.keras.optimizers.Adam(lr) 

这个类初始化一个神经网络(_policy)并为该网络的更新提供了一个占位符(_new_policy),以便在算法的每一步中,我们都可以参照其相对于上一策略的改进来更新新策略。

train_policy 循环内的损失函数使用梯度下降算法进行优化,其中梯度的大小被限制在一个固定范围内(“剪辑”),以便大梯度不会导致损失函数(和权重)在训练轮次之间发生 drastical 改变:

def loss(self, actions, observations, advantages, returns):
        ratio = tf.exp(self._new_policy.log_prob(actions) - 
                       self._policy.log_prob(actions))
        surr = ratio * advantages
        actor_loss = tf.reduce_mean(
                tf.minimum(surr, tf.clip_by_value(ratio, 1 - self._eps,
                                    1 + self._eps) * advantages))
        critic_loss = tf.reduce_mean(tf.square(returns - self._new_policy.call(observations)))
        return -1*actor_loss - self._ent_coeff * \
    tf.reduce_mean(self._new_policy.sample(observations, 'entropy'))\
                         + self._v_coeff * critic_loss 

在这个损失函数中,我们首先取旧策略(Actor-Critic 网络的当前参数)与潜在更新(新策略)之间的比率 - 两者的对数概率差的指数给出了一个比率(这是观察到的动作在每个网络的动作分布下的概率)。如果新的提议网络是一个改进(其参数更好地适应了观察到的动作序列),比率大于 1。否则,建议的质量保持不变(比率为 1)或比当前的 Actor-Critic 参数更差(比率小于 1)。

我们将这个比率乘以“优势”,优势是回报(我们之前描述的 Q 函数)与 Actor-Critic 现有状态的当前值之间的差异。在这个 GAIL 实现中,我们通过广义优势估计²⁹来计算优势,它使用 Q 函数的指数平滑估计,其中 gamma(系数)和 tau(指数)控制了未来奖励估计在未来的衰减程度相对于没有未来信息(tau = 0)或未来数据与现在数据相比的重要性没有衰减(tau = 1):

def compute_gae(next_value, rewards, masks, values, gamma=0.99, tau=0.95):
    values = values + [next_value]
    gae = 0
    returns = []
    for step in reversed(range(len(rewards))):
        delta = rewards[step] + gamma * values[step + 1] * \ 
                    masks[step] - values[step]
        gae = delta + gamma * tau * masks[step] * gae
        returns.insert(0, gae + values[step])
    return returns 

上面的损失函数然后使用优势乘以surr("替代"项),并计算两个值——第一个是演员损失,它将由优势给出的损失项约束在给定范围内,由clip_by_value函数表示。这可以防止新旧策略的概率比率的极端值(远小于 1 或大于 1)使损失函数不稳定。除此之外,我们还加上评论者的损失,评论者值与我们上面计算的优势函数之间的平方差。对演员和评论者函数以及行动概率分布的熵(它是否为位置子集分配高值,从而包含关于潜在行动分布的“信息”)进行加权求和,得到损失函数的总体策略质量作为目标。

请注意actor_loss乘以负 1(因为它是旧策略概率与新策略概率之间的比率——因此如果它改善了,它大于 1,但损失函数应该最小化,因此一个更大的负值)。同样,熵项如果更大,它含有更多信息,我们也要取它的负值,因为我们要最小化损失函数。评论者的损失越接近 0,它就变得更好,所以我们保留这个项为正。

要使用这个损失函数,我们定义了一个名为train_policy的自定义训练函数:

def train_policy(self, actions, observations, advantages, returns):
    params = self._new_policy.get_params()
    with tf.GradientTape(watch_accessed_variables=False) as g:
        g.watch(params)
        def loss(actions, observations, advantages, returns):
            ...

        cost = loss(actions, observations, advantages, returns)
    grads = g.gradient(cost, params)
    grads = [grad if grad is not None else tf.zeros_like(var)
        for var, grad in zip(params, grads)]
    self._optimizer.apply_gradients(zip(grads, params),
        experimental_aggregate_gradients=False) 

我们使用get_params()函数提取 PPO 策略网络的可训练参数,使用GradientTape对它们进行“监视”,并使用上面的损失函数计算损失。此外,由于演员-评论者有两个输出(动作和值),只有一个输出(值)受到奖励更新的影响,我们可能会有不存在的梯度,所以我们用 0 替换任何空的梯度。

在我们上面描述的 GAIL 内循环的每一步中,我们还需要能够用深拷贝(创建一个具有相同值的新变量,而不是指向原始变量的指针)来替换旧策略为新策略:

def update_policy(self):
    self._policy = copy.deepcopy(self._new_policy) 

最后,我们可以使用对演员-评论者策略网络的调用来获得价值(奖励)估计和采样新的行动:

def get_action(self, x):
    return self._new_policy.sample(x, output='action')
def get_value(self, x):
    return self._new_policy.call(x) 

鉴别器

通过我们的 PPO 算法,我们有了需要教会表现得像一个专家的“代理人”。我们可以从我们以“生成器”形式下载的 TRPO 训练示例的弹跳者中抽样。

现在我们只需要一个鉴别器网络,它旨在区分专家行为和我们正在训练的代理者:

class Discriminator(tf.keras.Model):

    def __init__(self, name='discriminator', dim_actions=3, num_layers=2, num_units=100, lr=3e-2, **kwargs):
        super().__init__(name=name, **kwargs)
        self._dim_actions = dim_actions
        self._num_layers = num_layers
        self._num_units = num_units
        self._layers = list()
        for l in range(self._num_layers):
            self._layers.append(tf.keras.layers.Dense(
                                          self._num_units,
                                          activation=tf.nn.relu))
            self._layers.append(tf.keras.layers.BatchNormalization())
        self._layers.append(tf.keras.layers.Dense(1,activation=None))
        self._optimizer = tf.keras.optimizers.Adam(lr) 
        return self._new_policy.call(x) 

就像演员-评论者一样,这是一个 3 层神经网络,层与层之间应用了BatchNormalization。它的单个输出指示输入的质量(就像演员-评论者的价值函数),当网络更像“专家”时,输出应该更低。请注意,为了使“奖励”输出与演员-评论者值输出的符号匹配,我们反转了鉴别器的符号,因为它对专家观察预测得更接近 0:

def get_reward(self, x):
    return -1 *tf.squeeze(tf.math.log(tf.sigmoid(self.call(x)))) 

此转换应用于我们之前为 Actor-Critic 网络看到的相同 call 函数。

 def call(self, x):
        for layer in self._layers:
            x = layer(x)
        return x 

训练和结果

训练网络时,我们应用一个损失函数,试图将专家(观察,动作)对分类为 0,并将代理(观察,动作)对分类为 1。当代理学会生成高质量的(观察,动作)对,类似于专家时,鉴别器将越来越难区分来自代理和专家的样本,并且也会将代理样本标记为 0:

def loss(self, x):
    expert_out, policy_out = tf.sigmoid(tf.split(self.call(x), 
                               num_or_size_splits=2, axis=0))
    return (tf.nn.sigmoid_cross_entropy_with_logits(tf.ones_like(policy_out), policy_out) + tf.nn.sigmoid_cross_entropy_with_logits(tf.zeros_like(expert_out), expert_out)) 

与之前一样,我们通过 get_params() 提取网络的参数:

 def get_params(self):
        weights = []
        for layer in self.layers:
            weights += layer.trainable_weights
        return weights 

然后我们使用 train_discriminator 将我们的损失函数应用于这些参数:

def train_discriminator(self, x):
    params = self.get_params()
    with tf.GradientTape(watch_accessed_variables=False) as g:
        g.watch(params)
        cost = self.loss(x)
    grads = g.gradient(cost, params)
    grads = [grad if grad is not None else tf.zeros_like(var)
          for var, grad in zip(params, grads)]
    self._optimizer.apply_gradients(zip(grads, params),
        experimental_aggregate_gradients=False) 

最后,我们需要一个更新函数用于我们的 PPO 小批量步骤,在 GAIL 算法的每个内循环中从代理中随机采样观察:

def ppo_iteration(mini_batch_size, observations, actions, returns, advantages):
    batch_size = observations.shape[0]
    for _ in range(batch_size // mini_batch_size):
        rand_ids = np.random.randint(0, batch_size, mini_batch_size)
        yield (observations[rand_ids, :], actions[rand_ids, :],
               returns[rand_ids, :], advantages[rand_ids, :]) 

我们还希望能够绘制我们训练代理的进展,为此,我们将使用模型从环境中取样,并绘制平均奖励和鉴别器的性能(代理和专家的鉴别器奖励匹配程度):

from IPython.display import clear_output
import matplotlib.pyplot as plt
def plot(frame_idx, rewards, policy_ob_ac_rew, expert_ob_ac_rew):
    clear_output(True)
    plt.figure(figsize=(20,5))
    plt.subplot(131)
    plt.title('frame %s. reward: %s' % (frame_idx, rewards[-1]))
    plt.ylabel('Agent Reward')
    plt.xlabel('Step in Training')
    plt.plot(rewards)
    plt.subplot(132)
    plt.title('frame %s.' % (frame_idx))
    plt.plot(policy_ob_ac_rew)
    plt.plot(expert_ob_ac_rew)
    plt.legend(['Agent','Expert'])
    plt.xlabel('Steps in Test Simulation')
    plt.ylabel('Discriminator Reward')
    plt.show() 

此函数生成两个图表,显示在图 12.10中。左侧是代理的一组测试观察的奖励,随着代理在移动跳跃器方面变得更好,奖励应该增加。右侧绘制了每个 n 步样本代理和专家跳跃器运动的鉴别器如何区分两者的情况(橙色和蓝色线是否重叠,这是最优的,或者彼此之间是否很远,这种情况下 GAIL 算法尚未收敛):

图 12.10:一系列测试观察的代理奖励(左),鉴别器奖励(右)。图由 plot() 生成。

测试样本是使用 test_env 函数生成的,该函数类似于我们上面看到的 Pybullet 仿真 - 它使用当前代理并计算当前策略下仿真的 n 步,返回平均奖励:

def test_env(model, vis=False):
    ob = env.reset()
    ob = tf.reshape(tf.convert_to_tensor(ob), (1,11))
    done = False
    total_reward = 0
    while not done:
        if vis:
            env.render()
        ac = model.get_action(ob)[0]
        ac = tf.reshape(tf.convert_to_tensor(ac), (3, 1))
        next_ob, reward, done, _ = env.step(ac)
        ob = next_ob
        ob = tf.reshape(tf.convert_to_tensor(ob), (1,11))
        total_reward += reward
    return total_reward 

对于我们的主函数,我们将设置最大仿真步数和算法的超参数,包括 ADAM 优化器的学习率 (lr),每个网络隐藏层中的单位数,每个步数在代理中运行每个跳跃仿真的步数 (num_steps),在每个代理更新中选择的样本数量(小批量大小),在每个内部循环更新代理时运行的步数 (ppo_epochs),算法中的总最大步数 (max_frames),以及一个容器来保存我们上面展示如何绘制的外样本奖励估计 (test_rewards):

ppo_hidden_size           = 32
discriminator_hidden_size = 32
lr                        = 3e-4
num_steps                 = 1000
mini_batch_size           = 50
ppo_epochs                = 5
max_frames = 100000000
frame_idx  = 0
test_rewards = [] 

首先,我们初始化鉴别器和 PPO 网络,设置内部循环计数器以进行代理更新 (i_update),并设置 Pybullet 环境:

ob = env.reset()
ppo = PPO(lr=lr, num_units=ppo_hidden_size)
discriminator = Discriminator(lr=lr, num_units=discriminator_hidden_size)
i_update = 0 

在每一步,我们将使用当前策略计算一定数量的时间步,并创建这些观察、动作和奖励的列表。在固定的间隔内,我们将使用我们上面描述的函数绘制结果:

while frame_idx < max_frames:
    i_update += 1

    values = []
    obs = []
    acs = []
    rewards = []
    masks = []
    entropy = 0
    for _ in range(num_steps):
        ob = tf.reshape(tf.convert_to_tensor(ob), (1,11))
        ac = ppo.get_action(ob)
        ac = tf.reshape(tf.convert_to_tensor(ac), (3, 1))
        next_ob, _, done, _ = env.step(ac)
        reward = discriminator.get_reward(np.concatenate([ob, 
                     tf.transpose(ac)], axis=1))
        value = ppo.get_value(ob)
        values.append(value)
        rewards.append(reward)
        masks.append((1-done))
        obs.append(ob)
        acs.append(np.transpose(ac))
        ob = next_ob
        frame_idx += 1
        if frame_idx % 1000 == 0 and i_update > 1:
            test_reward = np.mean([test_env(ppo) for _ in range(10)])
            test_rewards.append(test_reward)
            plot(frame_idx, test_rewards, 
                 discriminator.get_reward(policy_ob_ac), 
                 discriminator.get_reward(expert_ob_ac))

    next_ob = tf.reshape(tf.convert_to_tensor(next_ob), (1,11))

    next_value = ppo.get_value(next_ob)
    returns = compute_gae(next_value, rewards, masks, values)
    returns = np.concatenate(returns)
    values = np.concatenate(values)
    obs = np.concatenate(obs)
    acs = np.concatenate(acs)
    advantages = returns - values 

请注意,如果我们的仿真被终止,我们会“屏蔽”完成后的任何观察,以便它们不影响我们的梯度计算。还要注意,我们正在使用 tf.convert_to_tensortf.reshape 函数将样本中的 NumPy 数据转换为张量。我们使用上述描述的 compute_gae 函数平滑估计的优势函数。

然后,我们周期性地(这里是每 3 个周期)使用小批量更新计算 PPO 策略的更新。

 # Policy Update
    if i_update % 3 == 0:
        ppo.update_policy()
        for _ in range(ppo_epochs):
            for ob_batch, ac_batch, return_batch, adv_batch in ppo_iteration(mini_batch_size, obs, acs, returns, advantages):
                ppo.train_policy(ac_batch, ob_batch, adv_batch,                     return_batch) 

最后,我们从之前加载的 TRPO 策略中对专家轨迹进行采样,将专家和新策略的观察和动作连接起来,并对鉴别器进行梯度更新:

 # Discriminator Update
    expert_samples = np.random.randint(0, mujoco_hopper_np['acs'].\
                                       shape[0], 1)
    expert_ob_ac = np.concatenate([
mujoco_hopper_np['obs'][expert_samples,:num_steps,:].reshape(num_steps,11), 
mujoco_hopper_np['acs'][expert_samples,:num_steps,:].reshape(num_steps,3)],1)
    policy_ob_ac = np.concatenate([obs, acs], 1)
    discriminator.train_discriminator(np.concatenate([expert_ob_ac, policy_ob_ac], axis=0)) 

请注意,您可以在算法运行时观察仿真窗口中的跳跃器正在训练 - 找到“奖励”的数学定义的难度表现在于,我们可以看到跳跃器进行创造性运动(例如爬行,图 12.11),以便在仿真空间中移动(从而获得奖励),但不像专家的“跳跃”动作。

图 12.11: GAIL 训练期间跳跃器“爬行”

摘要

在本章中,我们探讨了生成模型在强化学习中的另一个应用。首先,我们描述了强化学习如何允许我们学习环境中代理的行为,以及深度神经网络如何使 Q-learning 能够适用于具有极大观察和动作空间的复杂环境。

然后,我们讨论了逆强化学习,并且它通过“反转”问题并尝试“通过示例学习”与强化学习有所不同。我们讨论了如何使用熵来评分尝试与专家网络进行比较的问题,以及这种熵损失的特定、正则化版本如何与我们在第六章中学习的 GAN 问题具有类似的形式,称为 GAIL(生成对抗性模仿学习)。我们看到 GAIL 只是这个通用想法的许多可能表述之一,使用不同的损失函数。最后,我们使用 bullet-gym 物理模拟器和 OpenAI Gym 实现了 GAIL。

在最后一章,我们将通过探索生成式人工智能在不同问题领域的最新研究来总结,包括生物信息学和流体力学,为您继续探索这一发展领域提供进一步阅读的参考文献。

参考文献

  1. Mnih, V., Kavukcuoglu, K., Silver, D., Graves, A., Antonoglou, I., Wierstra, D., & Riedmiller, M. (2013). 使用深度强化学习玩雅达利. arXiv. arxiv.org/abs/1312.5602

  2. Bareketain, P. (2019 年 3 月 10 日)。理解用于深度多智能体强化学习的稳定经验重播。Medium。medium.com/@parnianbrk/understanding-stabilising-experience-replay-for-deep-multi-agent-reinforcement-learning-84b4c04886b5

  3. 维基百科用户 waldoalverez,采用 CC BY-SA 4.0 许可证(creativecommons.org/licenses/by-sa/4.0/)。

  4. Amit, R., Meir, R., & Ciosek, K. (2020)。折扣因子作为强化学习的正则化器。第 37 届国际机器学习大会论文集,维也纳,奥地利,PMLR 119,2020。proceedings.mlr.press/v119/amit20a/amit20a.pdf

  5. Matiisen, T. (2015 年 12 月 19 日)。揭秘深度强化学习。计算神经科学实验室。neuro.cs.ut.ee/demystifying-deep-reinforcement-learning/

  6. Bellman, R.E. (2003 年)[1957]。动态规划。多佛。

  7. Watkins, C.J.C.H. (1989)。从延迟奖励中学习(PDF)(博士论文),剑桥大学,www.cs.rhul.ac.uk/~chrisw/new_thesis.pdf

  8. Sutton, R., & Barto, A. (1998). 强化学习:一种介绍。麻省理工学院出版社。

  9. Melo, F.S. Q-learning 的收敛性:一个简单的证明users.isr.ist.utl.pt/~mtjspaan/readingGroup/ProofQlearning.pdf

  10. Watkins, C.J.C.H., & Dayan, P. (1992). Q-learning。机器学习,8(3-4):279-292。

  11. Nair, A., Dalal, M., Gupta, A., & Levine, S. (2020)。利用离线数据集加速在线强化学习。arXiv。arxiv.org/abs/2006.09359

  12. Sutton, R.S. & Barto A.G. (2018)。强化学习:一种介绍(第 2 版)。麻省理工学院出版社。

  13. Rummery, G.A., & Niranjan, M. (1994)。使用连接系统的在线 Q-learning。剑桥大学工程系。

  14. Mnih, V., Kavukcuoglu, K., Silver, D., Graves, A., Antonoglou, I., Wierstra, D., & Riedmiller, M. (2013)。使用深度强化学习玩 Atari。arXiv。arxiv.org/abs/1312.5602

  15. Silver, D., Huang, A., Maddison, C.J., Guez, A., Sifre, L., van den Driessche, G., Schrittwieser, J., Antonoglou, I., Panneershelvam, V., Lanctot, M., Dieleman, S., Grewe, D., Nham, J., Kalchbrenner, N., Sutskever, I., Lillicrap, T., Leach, M., Kavukcuoglu, K., Graepel, T., & Hassabis, D. (2016). 用深度神经网络和树搜索掌握围棋游戏自然 529, 484–489。www.nature.com/articles/nature16961

  16. Silver, D., Schrittwieser, J., Simonyan, K., Antonoglou, I., Huang, A., Guez, A., Hubert, T., Baker, L., Lai, M., Bolton, A., Chen, Y., Lillicrap, T., Hui, F., Sifre, L., van den Driessche, G., Graepel, T., & Hassabis, D. (2017). 无需人类知识的围棋掌握。《自然》550, 354-359。www.nature.com/articles/nature24270

  17. Silver, D., Hubert, T., Schrittwieser, J., Antonoglou, I., Lai, M., Guez, A., Lanctot, M., Sifre, L., Kumaran, D., Graepel, T., Lillicrap, T., Simonyan, K., & Hassabis, D. (2017). 通过自我对弈掌握国际象棋和将棋的通用强化学习算法。arXiv。arxiv.org/abs/1712.01815

  18. Vinyals, O., Babuschkin, I., Silver. D. et al. (2019). 使用多智能体强化学习在星际争霸 II 中达到国际大师级。《自然》575, 350-354。www.nature.com/articles/s41586-019-1724-z

  19. Pomerleau, D.A. (1991). 自主导航的人工神经网络高效训练。《神经计算》, 3(1):88-97。

  20. Ho, J., & Ermon, S. (2016). 生成对抗学习。arXiv。arxiv.org/abs/1606.03476

  21. Ross, S., & Bagnell, J.A. (2010). Imitation Learning 的高效约简。《第十三届国际人工智能与统计学会议论文集》, PMLR 9:661-668

  22. 基于 Arora, S., & Doshi, P. (2018). 逆强化学习概述:挑战、方法和进展,arXiv 一图。arxiv.org/abs/1806.06877

  23. 基于 Ziebart, B.D., Maas, A., Bagnell, J.A., & Dey, A.K. (2008). 最大熵逆强化学习。《第二十三届 AAAI 人工智能会议论文集》。

  24. Abbeel, P., & Ng, A. Y. (2004). 逆强化学习的学徒学习。《ICML 会议录》, 1–8。

  25. Jaynes, E. T. (1957). 信息理论与统计力学。《物理评论》106:620–630。

  26. M. Bloem and N. Bambos, 无限时间范围的最大因果熵逆强化学习, 2014 年第 53 届 IEEE 决策与控制会议, pp. 4911-4916, doi: 10.1109/CDC.2014.7040156。citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.720.5621&rep=rep1&type=pdf

  27. Kramer, G. (1998). 有反馈通道的信道的指向信息。瑞士苏黎世联邦理工学院博士论文。citeseerx.ist.psu.edu/viewdoc/download;jsessionid=91AB2ACCC7B3CD1372C2BC2EA267ECEF?doi=10.1.1.728.3888&rep=rep1&type=pdf

  28. Schulman, J., Levine, S., Moritz, P., Jordan, M.I., & Abbeel, P. (2015). 信任区域策略优化. arXiv. arxiv.org/abs/1502.05477

  29. Ioffe, S., & Szegedy, C. (2015). 批量归一化:通过减少内部协变量转移加速深度网络训练. arXiv. arxiv.org/abs/1502.03167

  30. Schulman, J., Wolski, F., Dhariwal, P., Radford, A., & Klimov, O. (2017). 近端策略优化算法. arXiv. arxiv.org/abs/1707.06347

  31. Schulman, J., Moritz, P., Levine, S., Jordan, M., & Abbeel, P. (2015). 使用广义优势估计进行高维连续控制. arXiv. arxiv.org/abs/1506.02438

第十三章:新兴的生成型人工智能应用

在前面的章节中,我们研究了使用生成型人工智能的大量应用,包括生成图片和文字,甚至音乐。然而,这是一个庞大且不断扩张的领域;在 Google 学术搜索中,与“生成对抗网络”匹配的论文数量为 27,200 篇,其中有 16,200 篇发表于 2020 年!对于一个从根本上始于 2014 年的领域来说,这是令人惊讶的指数增长,这也可以在 Google n-gram 查看器中得到体现(图 13.1):

图 13.1:Google n-gram of "generative adversarial networks"

正如我们在本卷中看到的,生成对抗网络只是更广泛的生成型人工智能领域中一类模型,该领域还包括变分自动编码器、BERT 和 GPT-3 等模型。因为单个书籍无法覆盖所有这些领域,所以我们在这一卷中以讨论该领域中的一些新兴主题来结束本卷:药物发现和蛋白质折叠、求解数学方程、从图像生成视频,以及生成食谱。鼓励感兴趣的读者参考所引用的文献,以获得对每个主题的更详细讨论。

利用生成模型发现新药物

我们在本卷中未涉及的一个领域,即生成型人工智能正在对生物技术研究产生巨大影响。我们讨论两个领域:药物发现和预测蛋白质结构。

用生成分子图网络搜索化学空间

在根本上,一种药物——无论是药店里的阿司匹林还是医生开的抗生素——都是由节点(原子)和边(键)组成的化学图图 13.2)。与用于文本数据的生成模型(第 3、9 和 10 章)一样,图形具有不固定长度的特殊属性。有许多方法可以对图进行编码,包括基于单个片段的数值代码的二进制表示法(图 13.2)以及"SMILES"字符串,它们是 3D 分子的线性表示法(图 13.3)。你可能会意识到,化学图中潜在特征的数量是非常庞大的;事实上,已经估计¹化学结构的潜在数量达到了 10⁶⁰,这甚至比生成模型的论文数量还要大;作为参考,可观测宇宙中分子的数量²约为 10⁷⁸。

图 13.2:化学图³

人们因此可以理解,药物发现的一大挑战——为现有和新出现的疾病寻找新药物——是需要搜索潜在空间的规模之大。虽然实验性方法"药物筛选"——在高通量实验中测试成千上万甚至数十亿的化合物,以寻找具有潜在治疗特性的化学"稻草堆"中的化学针——已经被用了几十年,但是机器学习等计算方法的发展为规模更大的"虚拟筛选"打开了大门。使用现代计算方法,科学家可以测试完全虚拟化合物库,看它们是否具有与疾病研究中感兴趣的蛋白质相互作用的能力。那么,如何生成这样一个大型的虚拟分子库呢?

图 13.3:小分子的生成模型⁶

回到化学图的编码,也许不足为奇,我们可以使用基于循环神经网络(如 LSTM)的生成模型,从可能的分子图构型的巨大空间中采样。因为分子遵循特定的结构主题,这个问题比简单地独立采样原子集合更复杂,因为它们必须按照化学结构约束形成一个连贯的分子。图 13.3说明了这个过程可能是什么样子;将 2D 结构编码成二进制特征向量(有点类似我们在早期章节看到的文本表示),然后通过循环神经网络运行这些向量,训练一个根据先前的原子/键预测下一个原子或键的模型。一旦模型在输入数据上训练完成,它就可以被用来通过从这个生成器中一次次采样新结构来生成新的分子。变分自动编码器(第五章使用 VAE 画图)也已经被用来生成分子⁴(图 13.4),同样生成对抗网络⁵(图 13.5),我们在第六章使用 GAN 生成图像中介绍过。

图 13.4:小分子的变分自动编码器⁶

图 13.5:小分子的生成对抗模型⁶

使用生成模型折叠蛋白质

一旦我们从这样的模型得到了虚拟分子集合,下一个挑战是弄清楚它们当中是否有潜在成为药物的可能。科学家们通过"虚拟对接"进行测试,这需要测试一大库模拟分子,看它们是否适配在计算机中表示的蛋白质结构的口袋中。

分子动力学模拟用于近似评估潜在药物和可能影响体内疾病的蛋白质之间的能量吸引力/排斥力,其基于蛋白质和化学物质的相对化学结构。一个挑战是,在这些模拟中,药物“对接”到的蛋白质结构通常是通过 X 射线晶体学实验获得的,这是一种通过蛋白质结构衍射 X 射线以获得其三维形态表示的繁琐过程。

此外,这个过程仅适用于在液体悬浮液中稳定的一部分蛋白质,这排除了许多与疾病相关的分子,这些分子在细胞表面表达,在那里它们被细胞膜中的脂质粒子包围和稳定。

在这里,生成式人工智能也可以帮助:DeepMind(谷歌的研究子公司,也负责 AlphaGo 和其他突破性成果)的研究人员最近发布了一个名为AlphaFold的程序,它可以直接从蛋白质的遗传序列解决蛋白质的三维结构,使研究人员可以绕过实验晶体学的繁琐过程。图 13.6 说明了这是如何实现的:首先,一个网络被训练来从蛋白质的线性序列代码中预测蛋白质内的三维氨基酸(蛋白质的组成单位)之间的距离,表示折叠结构中各部分之间最可能的角度和距离:

图 13.6:训练神经网络预测蛋白质序列中氨基酸之间的距离⁷

然后,对于没有结构的蛋白质,AlphaFold 使用生成模型提出新的蛋白质片段(图 13.7),并评分哪些片段具有形成稳定三维结构的高可能性:

图 13.7:生成模型预测蛋白质构象⁷

这些示例展示了如何使用生成式人工智能在人体中模拟药物和其潜在靶点的结构,将现实世界中难以或不可能的实验转化为潜在发现新药物的新模型的大规模计算能力。

使用生成建模求解偏微分方程

另一个深度学习在一般领域以及生成学习在特定领域引领最新突破的领域是偏微分方程PDEs),这是一种用于各种应用的数学模型,包括流体动力学、天气预测和理解物理系统行为。更正式地说,偏微分方程对函数的偏导数施加某些条件,问题是找到一个满足这个条件的函数。通常在函数上放置一些初始条件或边界条件,以限制在特定网格内的搜索空间。例如,考虑 Burger 方程⁸,它控制着诸如给定位置和时间的流体速度之类的现象(图 13.8):

其中 u 是速度,t 是时间,x 是位置坐标,而v是流体的黏性("油腻度")。如果黏性为 0,则简化为无黏方程:

此方程的解取决于函数 u 的形式及其在 t=0 时的初始条件;例如,如果 u 是一个线性函数,则它有一个封闭的解析解(换句话说,可以仅使用数学公式的操作和变量替换来导出解,而无需使用数值方法来最小化误差函数,以选择潜在的解算法进行解析)。在大多数情况下,然而,需要数值方法,一个流行的方法是使用有限元方法FEMs)将输出(xt)划分为一个"网格",并在每个网格内数值求解 u。这类似于使用一组"基础"函数,如余弦和正弦(傅立叶分析)或小波将复杂函数分解为更简单的基础函数的总和。¹⁰

图 13.8:二维中 Burger 方程的可视化¹¹

然而,现在考虑我们所面临的问题:对于一组给定条件(边界条件),我们希望在网格上输出一个函数值,从而在空间和时间的每个点上创建一个函数值的热图,不完全像我们之前在生成应用中看到的图像!事实上,卷积生成模型已被用于将边界条件映射到输出网格(图 13.9)¹²:

图 13.9:使用 FEM 方法解决 Burger 的无黏方程(右)和深度学习(左)¹²

如前所述,使用 FEM 的典型策略是在网格中对每个元素数值求解u,其中大部分的计算负担来自于重复计算解而不是检查解¹²;因此,生成式 AI 可以通过抽样大量可能的解(该集合受初始条件或每个变量的数值范围的限制)并检查它们是否满足 PDE 的条件,完全规避了需要在网格的每个元素中求解函数的需要。图 13.10显示了将一组边界条件(b)和粘度变量(v)输入到卷积网络中,然后生成 PDE 的解。

图 13.10:DiffNet 用于对 Burger 方程的潜在解进行抽样¹²

该示例中的网络称为DiffNet。其误差函数有两个项;一个(L[p],方程 4)鼓励生成的网格与 PDE 曲面匹配(误差较小),而另一个则强制解在xt上再现所需的边界条件(L[b],方程 5)。

这两个约束共同类似于我们在过去章节中看到的其他对抗生成示例;一个项试图最小化误差(如鉴别器),而另一个试图近似一个分布(这里是变量上的边界条件)。

从图像创建视频的少样本学习方式

在之前的章节中,我们已经看到 GANs 在接受一组示例照片的训练后可以生成新颖的逼真图像。这种技术也可以用来创建图像的变体,无论是应用“滤镜”还是基础图像的新姿势或角度。将这种方法推向逻辑极限,我们是否可以从单个或有限数量的图像中创建一个“说话的头”?这个问题非常具有挑战性——对图像应用“扭曲”转换的传统(或深度学习)方法会产生明显的伪影,从而降低输出的逼真度^(13,14)。另一种替代方法是使用生成模型从输入图像(图 13.11)中抽样潜在的角度和位置变化,正如 Zakharov 等人在他们的论文Few Shot Adversarial Learning of Realistic Neural Talking Head Models中所做的那样。¹⁵

图 13.11:用于从单个图像创建运动帧的生成架构(基于 Zakharov 等人的第 2 张图。)

这个架构有三个处理一组输入视频的网络。第一个生成地标特征的嵌入(面部的简化表示,例如轮廓和重要特征的位置,如眼睛和鼻子的位置),以及原始图像的像素,生成数值向量。第二个使用这个嵌入和图像和地标数据作为输入生成新的视频帧。最后,通过鉴别器将下一帧视频与这个生成的图像进行比较。

这被称为few shot学习,因为它只使用少量视频帧来学习生成新的视频序列,这些序列的长度可能是无限的。然后,这个模型可以应用于仅有一个示例的输入数据,例如静止照片,从历史照片生成“说话的头像”,甚至是像《蒙娜丽莎》这样的绘画¹⁶。读者可以参考引用的论文了解示例输出。

这些“活肖画像”在某种程度上是 deepfakes 的演变 - 不是将一个移动的面部图像复制到另一个图像中,而是从没有与之相关的运动数据的图片中模拟出栩栩如生的动作。就像我们在第一章中讨论的画像拍卖,生成 AI 简介:“从模型中提取”数据,这是一个真正将生成 AI 带入新生活的例子。

使用深度学习生成食谱

我们将讨论的最后一个例子与本书中较早的例子有关,即使用 GAN 生成图像的文本描述。这个问题的一个更复杂的版本是生成图像的结构化描述,该图像具有多个组件,例如图像中所描述的食物的配方。这个描述也更复杂,因为它依赖于这些组件(说明)的特定顺序才能连贯(图 13.12):

图 13.12: 从食物图像生成的食谱¹⁷

图 13.13所示,这个“逆向烹饪”问题也已经使用生成模型¹⁷(Salvador 等人)进行了研究。

图 13.13: 逆向烹饪生成模型的架构¹⁷

像我们在前几章看到的许多例子一样,“编码器”网络接收图像作为输入,然后使用序列模型“解码”成食物成分的文本表示,这些成分与图像嵌入组合在一起,创建由网络的第三层解码的一组指令。这个“指令解码器”使用了我们在第十章中描述的 transformer 架构,NLP 2.0: 使用 Transformer 生成文本,在我们讨论 BERT 时,对图像的不同部分应用加权相关性,并输出成分清单。

摘要

在本章中,我们研究了许多生成模型的新兴应用。其中之一是在生物技术领域,它们可以用于创建大量新的潜在药物结构。同样在生物技术领域,生成模型被用于创建可以用于计算药物发现的潜在蛋白质折叠结构。

我们探讨了生成模型如何可以用于解决数学问题,特别是偏微分方程,通过将流体力学方程组一系列边界条件映射到解决方案网格。我们还研究了一个具有挑战性的问题,即如何从有限的输入图像集生成视频,最后从食物图像生成复杂的文本描述(组件和说明的顺序),用于制作食谱。

恭喜您到达本书的最后。作为最终总结,让我们回顾一下我们学到的所有内容:

  • 第一章生成式 AI 简介:从模型“绘制”数据:生成模型是什么

  • 第二章建立一个 TensorFlow 实验室:如何在云端建立 TensorFlow 2 的环境

  • 第三章深度神经网络的基本构件:生成式 AI 中使用的神经网络模型的基本构件

  • 第四章教网络生成数字:限制玻尔兹曼机(最早的生成模型之一)如何生成手写数字

  • 第五章使用 VAEs 利用神经网络绘画图片:变分自编码器如何从随机数据生成图像

  • 第六章使用 GAN 生成图片:了解 GAN 的基本组成以及它们如何用于从随机噪声生成高分辨率图像

  • 第七章使用 GAN 进行风格迁移:如何利用 GAN(CycleGAN 和 pix2pix)将风格从一个领域转移到另一个领域

  • 第八章使用 GAN 进行 Deepfakes:生成深假的基本构件,生成虚假照片和视频

  • 第九章文本生成方法的兴起:使用深度学习模型进行语言生成的基础知识

  • 第十章NLP 2.0:使用变形金刚生成文本:变形金刚以及不同的最先进架构(如 GPT-x)如何改变了语言生成和 NLP 领域的革命

  • 第十一章利用生成模型进行音乐作曲:如何利用生成模型从随机数据生成音乐

  • 第十二章用生成式 AI 玩视频游戏:GAIL:生成模型如何用于训练可以通过强化学习在虚拟环境中导航的“代理”

  • 第十三章生成式 AI 的新兴应用:生成式 AI 的一些令人兴奋的新兴应用

我们希望本书展示了传统和尖端用例中生成建模的多样性,并且您对通过阅读引用的背景或甚至尝试自己实施一些模型更加感兴趣!

参考资料

  1. Kirkpatrick, P., & Ellis, C. (2004)。化学空间自然 432,823。www.nature.com/articles/432823a

  2. Villanueva, J.C. (2009 年 7 月 30 日)。宇宙中有多少个原子? 《今日宇宙》。www.universetoday.com/36302/atoms-in-the-universe/#:~:text=At%20this%20level%2C%20it%20is,hundred%20thousand%20quadrillion%20vigintillion%20atoms

  3. 基于 Akutsu, T., & Nagamochi, H. (2013 年)的一幅图。化学图的比较与枚举。计算与结构生物技术杂志,Vol. 5 Issue 6

  4. Gómez-Bombarelli R., Wei, J. N., Duvenaud, D., Hernández-Lobato, J. M., Sánchez-Lengeling, B., Sheberla, D., Aguilera-Iparraguirre, J., Hirzel, T. D., Adams, R. P., Aspuru-Guzik, A. (2018)。使用数据驱动的分子连续表示进行自动化学设计。ACS 中央科学 2018,4,268-276。

  5. Blaschke, T., Olivecrona, M., Engkvist, O., Bajorath, J., Chen, H.。应用生成自动编码器进行全新分子设计。分子信息学 2018,37,1700123。

  6. Bian Y., & Xie, X-Q. (2020)。生成化学:用深度学习生成模型进行药物发现。arXiv。arxiv.org/abs/2008.09000

  7. Senior, A., Jumper, J., Hassabis, D., & Kohli, P. (2020 年 1 月 15 日)。AlphaFold:利用 AI 进行科学发现。DeepMind 博客。deepmind.com/blog/article/AlphaFold-Using-AI-for-scientific-discovery

  8. Cameron, M. Burgers 方程笔记。www.math.umd.edu/~mariakc/burgers.pdf

  9. Chandrasekhar, S. (1943)。平面激波衰变论(No. 423)。弹道研究实验室。

  10. COMSOL Multiphysics 百科全书。(2016 年 3 月 15 日)。有限元法(FEM)www.comsol.com/multiphysics/finite-element-method

  11. Wikipedia. (2021 年 4 月 14 日)。Burgers 方程en.wikipedia.org/wiki/Burgers%27_equation#%20Inviscid_Burgers'_%20equation

  12. Botelho, S., Joshi, A., Khara, B., Sarkar, S., Hegde, C., Adavani, S., & Ganapathysubramanian, B. (2020)。解 PDE 的深度生成模型:用于训练大型无数据模型的分布式计算。arXiv。arxiv.org/abs/2007.12792

  13. Averbuch-Elor, H., Cohen-Or, D., Kopf, J., & Cohen, M.F. (2017)。给肖像栩栩如生的魔法。《ACM 图形学交易(TOG)》,36(6):196

  14. Ganin, Y., Kononenko, D., Sungatullina, D., & Lempitsky, V. (2016)。DeepWarp:用于凝视操作的照片逼真图像合成。欧洲计算机视觉会议,311-326。Springer。

  15. Zakharov,E.,Shysheya,A.,Burkov,E.,& Lempitsky,V. (2019)。Few-Shot Adversarial Learning of Realistic Neural Talking Head Models。 arXiv。arxiv.org/abs/1905.08233

  16. Hodge,M. (2019 年 5 月 24 日)。【真正的】谈论三星的'深度伪造'视频显示了假新闻的可怕新前沿。 The Sun。www.thesun.co.uk/news/9143575/deepfake-talking-mona-lisa-samsung/

  17. Salvador,A.,Drozdzal,M.,Giro-i-Nieto,X.,& Romero,A. (2019)。反向烹饪:从食物图像生成食谱。 arXiv。arxiv.org/abs/1812.06164

分享您的经验感谢您抽出时间阅读本书。如果您喜欢这本书,请帮助其他人找到它。在www.amazon.com/dp/1800200889上留下评论。

packt.com

订阅我们的在线数字图书馆,以完全访问超过 7,000 本书籍和视频,以及行业领先的工具,帮助您规划个人发展并推动您的职业发展。有关更多信息,请访问我们的网站。

为什么订阅?

  • 花更少的时间学习,更多的时间编码,使用来自超过 4,000 名行业专业人士的实用电子书和视频

  • 通过专门为您建立的技能计划学习更好

  • 每月获取一个免费电子书或视频

  • 完全可搜索,轻松访问重要信息

  • 复制并粘贴,打印并书签内容

www.Packt.com,您还可以阅读一系列免费的技术文章,注册一系列免费的通讯,以及获得 Packt 图书和电子书的独家折扣和优惠。

posted @ 2026-03-25 10:32  布客飞龙II  阅读(75)  评论(0)    收藏  举报