生成式深度学习-全-

生成式深度学习(全)

原文:Generative Deep Learning

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

这本书正在成为我的生活的一部分。当我在客厅找到一本副本时,我问我的儿子,“你什么时候拿到这本书的?”他回答说,“你给我的时候”,对我的疑惑表示困惑。我们一起翻阅各个部分时,我开始把《生成式深度学习》看作是生成式人工智能的《格雷解剖学》。

作者以令人难以置信的清晰度和令人放心的权威解剖了生成式人工智能的解剖学。他提供了一个真正非凡的快速发展领域的描述,其中包含了务实的例子、引人入胜的叙述和如此时下的参考资料,读起来就像是一部活生生的历史。

在他的解构过程中,作者对生成式人工智能的潜力保持着一种惊奇和兴奋的感觉,尤其在书中引人入胜的结尾部分中表现得尤为明显。在揭示了这项技术之后,他提醒我们,我们正处在一个新智能时代的黎明,一个生成式人工智能将我们的语言、艺术、创造力反映出来的时代;它不仅反映了我们已经创造的东西,还反映了我们可以创造的东西——只受“你自己的想象力”的限制。

人工智能中生成模型的中心主题深深地 resonates with me,因为我看到自然科学中出现了完全相同的主题;也就是说,我们将自己视为我们生活世界的生成模型。我怀疑在这本书的下一版中,我们将读到关于人工和自然智能融合的内容。在那之前,我会把这一版放在我的《格雷解剖学》副本旁边,以及我书架上的其他珍宝。

Karl Friston,FRS

神经科学教授

伦敦大学学院

前言

“我不能创造的东西,我就不理解。”

理查德·费曼

生成式人工智能是我们这个时代最具革命性的技术之一,改变了我们与机器互动的方式。它有潜力彻底改变我们生活、工作和娱乐的方式,已经成为无数对话、辩论和预测的主题。但如果这种强大技术还有更大的潜力呢?生成式人工智能的可能性是否超出了我们当前的想象?生成式人工智能的未来可能比我们想象的更令人兴奋...

自我们早期以来,我们一直在寻找创造原创和美丽作品的机会。对早期人类来说,这体现在洞穴壁画上,描绘着野生动物和抽象图案,用谨慎和有条不紊地放置在岩石上的颜料创作而成。浪漫主义时代赋予我们柴可夫斯基交响曲的掌握,通过声波激发胜利和悲剧感情的能力,编织在一起形成美丽的旋律和和谐。最近,我们发现自己在午夜赶往书店购买关于虚构巫师的故事,因为字母的组合创造了一个叙述,激励我们翻开页面,看看我们的英雄会发生什么。

因此,毫不奇怪,人类已经开始提出创造力的终极问题:我们能否创造出本身具有创造力的东西?

这就是生成式人工智能试图回答的问题。随着方法和技术的最新进展,我们现在能够构建能够以给定风格绘制原创艺术作品、写出有长期结构的连贯文本块、创作令人愉悦的音乐,以及通过生成虚构未来场景来制定复杂游戏的获胜策略的机器。这只是生成式革命的开始,将迫使我们不得不寻找关于创造力机制的一些最大问题的答案,最终,这意味着什么是人类。

简而言之,现在学习生成式人工智能再也没有比现在更好的时机了,所以让我们开始吧!

目标和方法

本书假设读者对生成式人工智能没有先前的知识。我们将从头开始逐步建立所有关键概念,以一种直观易懂的方式进行讲解,所以如果你对生成式人工智能没有经验也不用担心。你来对地方了!

本书不仅涵盖当前流行的技术,还作为生成建模的完整指南,涵盖了广泛的模型系列。没有一种技术在客观上比其他技术更好或更差——事实上,许多最先进的模型现在混合了来自生成建模各种方法的想法。因此,重要的是要及时了解生成式人工智能各个领域的发展,而不是专注于某一种特定的技术。有一点是肯定的:生成式人工智能领域发展迅速,你永远不知道下一个突破性想法将从何而来!

考虑到这一点,我将采取的方法是向您展示如何在自己的数据上训练自己的生成模型,而不是依赖预训练的现成模型。虽然现在有许多令人印象深刻的开源生成模型可以下载并在几行代码中运行,但本书的目的是从第一原则深入挖掘它们的架构和设计,以便您完全了解它们的工作原理,并可以使用 Python 和 Keras 从头开始编写每种技术的示例。

总的来说,这本书可以被看作是当前生成式人工智能领域的地图,涵盖了理论和实际应用,包括文献中关键模型的完整工作示例。我们将逐步演示每个步骤的代码,清晰地标明代码如何实现支撑每种技术的理论。这本书可以从头到尾阅读,也可以作为您随时查阅的参考书。最重要的是,我希望您觉得这本书有用且愉快!

注意

在整本书中,您将找到一些简短的寓言故事,帮助解释我们将构建的一些模型的机制。我认为教授新的抽象理论的最佳方法之一是首先将其转化为不那么抽象的东西,比如一个故事,然后再深入技术解释。故事和模型解释只是在两个不同领域中解释相同机制,因此在学习每个模型的技术细节时,参考相关故事可能会很有帮助!

先决条件

这本书假定您有 Python 编程经验。如果您对 Python 不熟悉,最好的起点是通过LearnPython.org开始。在线有许多免费资源,可以让您掌握足够的 Python 知识,以便使用本书中的示例。

另外,由于一些模型使用数学符号描述,对线性代数(例如矩阵乘法)和一般概率论有扎实的理解将会很有帮助。一本有用的资源是 Deisenroth 等人的书籍Mathematics for Machine Learning(剑桥大学出版社),可以免费获取。

本书假定您对生成建模(我们将在第一章中探讨关键概念)或 TensorFlow 和 Keras 没有先验知识(这些库将在第二章中介绍)。

路线图

这本书分为三个部分。

第一部分是生成建模和深度学习的一般介绍,我们在其中探讨了贯穿本书后续部分所有技术的核心概念:

  • 在第一章,“生成建模”中,我们定义生成建模,并考虑一个玩具示例,我们可以用来理解一些对所有生成模型都重要的关键概念。我们还列出了我们将在本书的第二部分中探索的生成模型家族的分类法。

  • 在第二章,“深度学习”中,我们通过使用 Keras 构建我们的第一个多层感知器(MLP)来开始探索深度学习和神经网络。然后,我们将其调整以包括卷积层和其他改进,以观察性能上的差异。

第二部分介绍了我们将用于构建生成模型的六种关键技术,每种技术都有实际示例:

  • 在第三章,“变分自编码器”中,我们考虑变分自编码器(VAE),看看它如何用于生成人脸图像,并在模型的潜在空间中在人脸之间进行变形。

  • 在第四章,“生成对抗网络”中,我们探索生成对抗网络(GAN)用于图像生成,包括深度卷积 GAN,条件 GAN 以及改进的 Wasserstein GAN,使训练过程更加稳定。

  • 在第五章,“自回归模型”中,我们将注意力转向自回归模型,从介绍循环神经网络(如长短期记忆网络(LSTM))开始,用于文本生成,以及用于图像生成的 PixelCNN。

  • 在第六章“归一化流模型”中,我们专注于归一化流,包括对该技术的直观理论探索以及如何构建 RealNVP 模型生成图像的实际示例。

  • 在第七章“基于能量的模型”中,我们涵盖了基于能量的模型,包括如何使用对比散度进行训练以及如何使用 Langevin 动力学进行采样等重要方法。

  • 在第八章“扩散模型”中,我们深入探讨了构建扩散模型的实用指南,这些模型驱动着许多最先进的图像生成模型,如 DALL.E 2 和 Stable Diffusion。

最后,在第三部分中,我们基于这些基础探索了最先进的图像生成、写作、音乐创作和基于模型的强化学习模型的内部运作:

  • 在第九章“Transformer”中,我们探讨了 StyleGAN 模型的渊源和技术细节,以及其他用于图像生成的最先进 GANs,如 VQ-GAN。

  • 在第十章“高级 GANs”中,我们考虑了 Transformer 架构,包括一个实用的指南,教你如何构建自己的 GPT 版本用于文本生成。

  • 在第十一章“音乐生成”中,我们将注意力转向音乐生成,包括如何处理音乐数据以及应用技术如 Transformers 和 MuseGAN 的指南。

  • 在第十二章“世界模型”中,我们看到生成模型如何在强化学习的背景下使用,应用世界模型和基于 Transformer 的方法。

  • 在第十三章“多模态模型”中,我们解释了四种最先进的多模态模型的内部运作,这些模型结合了多种类型的数据,包括 DALL.E 2、Imagen 和 Stable Diffusion 用于文本到图像生成以及 Flamingo,一种视觉语言模型。

  • 在第十四章“结论”中,我们回顾了迄今为止生成式人工智能的关键里程碑,并讨论了生成式人工智能将如何在未来几年彻底改变我们的日常生活方式。

第二版的变化

感谢所有阅读本书第一版的人们——我很高兴看到这么多人发现它是一个有用的资源,并提供了关于第二版中希望看到的内容的反馈。自 2019 年第一版出版以来,生成式深度学习领域取得了显著进展,因此除了更新现有内容外,我还添加了几个新章节,以使材料与当前的最新技术保持一致。

以下是关于各个章节和整体书籍改进的主要更新摘要:

  • 第一章现在包括了有关不同生成模型家族的部分以及它们之间关系的分类法。

  • 第二章包含了改进的图表和更详细的关键概念解释。

  • 第三章通过一个新的实例和相关解释进行了更新。

  • 第四章现在包括了对条件 GAN 架构的解释。

  • 第五章现在包括了有关图像自回归模型(例如 PixelCNN)的部分。

  • 第六章是一个全新的章节,描述了 RealNVP 模型。

  • 第七章也是一个新章节,重点介绍了诸如 Langevin 动力学和对比散度等技术。

  • 第八章是一个新撰写的章节,介绍了驱动当今许多最先进应用的去噪扩散模型。

  • 第九章是第一版结尾提供的材料的扩展,更深入地关注了各种 StyleGAN 模型的架构以及关于 VQ-GAN 的新材料。

  • 第十章是一个探索 Transformer 架构细节的新章节。

  • 第十一章包括现代 Transformer 架构,取代了第一版中的 LSTM 模型。

  • 第十二章包括更新的图表和描述,其中有一节介绍了这种方法如何影响当今最先进的强化学习。

  • 第十三章是一个新章节,详细解释了像 DALL.E 2、Imagen、Stable Diffusion 和 Flamingo 这样令人印象深刻的模型是如何工作的。

  • 第十四章已更新,以反映自第一版以来该领域取得的杰出进展,并更全面详细地展示生成式人工智能未来的发展方向。

  • 所有对第一版的反馈和发现的拼写错误都已经得到了解决(尽我所知!)。

  • 每章的开头都添加了章节目标,这样您在开始阅读之前就可以看到章节涵盖的关键主题。

  • 一些寓言故事已经被重新编写,更加简洁和清晰——我很高兴许多读者说这些故事帮助他们更好地理解关键概念!

  • 每章的标题和副标题都已对齐,以便清楚地显示章节中哪些部分是解释重点,哪些部分是关于构建自己模型的重点。

其他资源

我强烈推荐以下书籍作为机器学习和深度学习的一般介绍:

这本书中的大部分论文都是通过arXiv获取的,这是一个免费的科学研究论文库。现在,作者们通常会在论文完全经过同行评审之前将论文发布到 arXiv 上。查看最新提交的论文是了解该领域最前沿发展的绝佳方式。

我还强烈推荐网站Papers with Code,在那里您可以找到各种机器学习任务中最新的最先进结果,以及指向论文和官方 GitHub 存储库的链接。这是一个极好的资源,适合任何想快速了解当前哪些技术在各种任务中取得最高分数的人,并且肯定帮助我决定在本书中包含哪些技术。

本书中使用的约定

本书中使用了以下排版约定:

斜体

指示新术语、URL、电子邮件地址、文件名和文件扩展名。

等宽

用于命令和程序清单,以及在段落中引用程序元素,如变量或函数名。

等宽斜体

显示应该用用户提供的值或上下文确定的值替换的文本。

提示

这个元素表示一个提示或建议。

注意

这个元素表示一个一般性说明。

警告

这个元素表示一个警告或注意。

代码库

本书中的代码示例可以在 GitHub 存储库中找到。我特意确保没有任何模型需要大量的计算资源来训练,这样您就可以开始训练自己的模型,而不必花费大量时间或金钱购买昂贵的硬件。如果需要,存储库中有一个全面的指南,介绍如何使用 Docker 开始以及如何在 Google Cloud 上设置带有 GPU 的云资源。

自第一版以来,对代码库进行了以下更改:

  • 现在所有示例都可以在单个笔记本中运行,而不是从代码库中导入模块。这样您可以逐个单元格运行每个示例,并深入了解每个模型是如何逐步构建的。

  • 每个笔记本的部分现在在示例之间基本对齐。

  • 本书中的许多示例现在利用了来自惊人的开源 Keras 存储库的代码片段,这是为了避免创建一个完全独立的 Keras 生成 AI 示例的开源存储库,因为 Keras 网站已经有了优秀的实现。我在整本书和存储库中添加了参考和链接,指向我从 Keras 网站中利用的代码的原始作者。

  • 我添加了新的数据来源,并改进了第一版的数据收集过程——现在,有一个脚本可以轻松运行,从所需的来源收集数据,以便训练本书中的示例,使用诸如Kaggle API之类的工具。

使用代码示例

补充材料(代码示例、练习等)可在https://github.com/davidADSP/Generative_Deep_Learning_2nd_Edition下载。

如果您有技术问题或在使用代码示例时遇到问题,请发送电子邮件至bookquestions@oreilly.com

本书旨在帮助您完成工作。一般来说,如果本书提供了示例代码,您可以在程序和文档中使用它。除非您复制了代码的大部分内容,否则无需征得我们的许可。例如,编写一个使用本书中几个代码块的程序不需要许可。销售或分发 O’Reilly 图书中的示例需要许可。通过引用本书回答问题并引用示例代码不需要许可。将本书中大量示例代码合并到产品文档中需要许可。

我们感谢您的致谢,但不要求。致谢通常包括标题、作者、出版商和 ISBN。例如:“生成式深度学习,第 2 版,作者 David Foster(O’Reilly)。版权所有 2023 年应用数据科学合作伙伴有限公司,978-1-098-13418-1。”

如果您认为您使用的代码示例超出了合理使用范围或上述许可,请随时联系我们permissions@oreilly.com

致谢

有很多人我想要感谢他们帮助我写这本书。

首先,我想感谢所有抽出时间来技术审查这本书的人,特别是 Vishwesh Ravi Shrimali、Lipi Deepaakshi Patnaik、Luba Elliot 和 Lorna Barclay。还要感谢 Samir Bico 帮助审查和测试伴随这本书的代码库。你们的意见和建议是无价的。

此外,特别感谢我的同事们在Applied Data Science Partners,Ross Witeszczak、Amy Bull、Ali Parandeh、Zine Eddine、Joe Rowe、Gerta Salillari、Aleshia Parkes、Evelina Kireilyte、Riccardo Tolli、Mai Do、Khaleel Syed 和 Will Holmes。感谢你们在我完成这本书时对我的耐心,我非常期待我们将来一起完成的所有机器学习项目!特别感谢 Ross——如果我们没有决定一起创业,这本书可能永远不会成形,所以感谢你相信我作为你的商业伙伴!

我还要感谢任何曾经教过我任何数学知识的人——我在学校非常幸运地有出色的数学老师,他们培养了我对这门学科的兴趣,并鼓励我在大学进一步深造。我要感谢你们的奉献和不辞辛劳地与我分享你们对这门学科的知识。

非常感谢 O'Reilly 的工作人员在引导我写这本书的过程中。特别感谢 Michele Cronin,在每一步都在那里,提供有用的反馈并发送友好的提醒,让我继续完成章节!还要感谢 Nicole Butterfield、Christopher Faucher、Charles Roumeliotis 和 Suzanne Huston 将这本书制作出版,以及 Mike Loukides 首先联系我询问我是否有兴趣写书。你们从一开始就对这个项目非常支持,我要感谢你们为我提供一个平台,让我写我热爱的事物。

在写作过程中,我的家人一直是我不断鼓励和支持的源泉。非常感谢我的妈妈 Gillian Foster,她检查了每一行文字中的拼写错误,并教会我如何首先进行加法运算!你的注重细节在校对这本书时非常有帮助,我真的很感激你和爸爸给予我的所有机会。我的爸爸 Clive Foster 最初教会了我如何编程——这本书充满了实用示例,这要归功于他在我十几岁时在 BASIC 中摸索时的耐心,试图制作足球游戏。我的哥哥 Rob Foster 是你能找到的最谦逊的天才,尤其在语言学领域——和他聊天关于人工智能和基于文本的机器学习的未来一直非常有帮助。最后,我要感谢我的奶奶,她一直是我们所有人的灵感和乐趣源泉。她对文学的热爱是我第一次决定写书会是一件令人兴奋的事情的原因之一。

我还想感谢我的妻子 Lorna Barclay。在写作过程中,除了给我无尽的支持和茶水外,你还细致地检查了这本书的每一个字。没有你,我无法完成这本书。感谢你一直在我身边,让这段旅程变得更加愉快。我保证在书出版后的几天内,我不会在餐桌上谈论生成式人工智能。

最后,我要感谢我们可爱的宝贝女儿 Alina,在写书的漫长夜晚里给予无尽的娱乐。你可爱的笑声成为我打字时完美的背景音乐。谢谢你给予我的灵感,让我时刻保持警惕。你才是这次行动的真正智囊。

第一部分:生成式深度学习简介

第一部分是对生成建模和深度学习的一般介绍——这两个领域是我们需要理解的,以便开始进行生成式深度学习!

在第一章中,我们将定义生成建模,并考虑一个玩具示例,我们可以用来理解所有生成模型中重要的一些关键概念。我们还将列出我们将在本书的第二部分中探索的生成模型家族的分类法。

第二章提供了深度学习工具和技术的指南,我们将需要这些工具和技术来开始构建更复杂的生成模型。特别是,我们将构建我们的第一个深度神经网络示例——一个多层感知器(MLP)——使用 Keras。然后,我们将对其进行改进,包括卷积层和其他改进,以观察性能上的差异。

到第一部分结束时,您将对支撑本书后续部分所有技术的核心概念有很好的理解。

第一章:生成建模

本章是对生成建模领域的一般介绍。

我们将从一个温和的理论介绍生成建模开始,看看它是更广泛研究的判别建模的自然对应。然后我们将建立一个描述一个好的生成模型应该具有的理想特性的框架。我们还将阐明重要的概率概念,以便充分理解不同方法如何应对生成建模的挑战。

这将自然地引导我们到倒数第二部分,其中描述了如今主导该领域的六个广泛的生成模型家族。最后一部分解释了如何开始使用本书附带的代码库。

什么是生成建模?

生成建模可以被广泛定义如下:

生成建模是机器学习的一个分支,涉及训练一个模型来生成类似于给定数据集的新数据。

这在实践中意味着什么?假设我们有一个包含马照片的数据集。我们可以在这个数据集上训练一个生成模型,以捕捉马图片中像素之间复杂关系的规则。然后我们可以从这个模型中采样,创建出原始数据集中不存在的新颖、逼真的马的图片。这个过程在图 1-1 中有所说明。

图 1-1。一个生成模型被训练来生成逼真的马的照片

为了构建一个生成模型,我们需要一个包含我们试图生成的实体的许多示例的数据集。这被称为训练数据,一个这样的数据点被称为观察

每个观察结果由许多特征组成。对于图像生成问题,特征通常是单个像素值;对于文本生成问题,特征可以是单个单词或字母组合。我们的目标是构建一个能够生成新特征集的模型,看起来就好像它们是使用与原始数据相同的规则创建的。从概念上讲,对于图像生成来说,这是一个非常困难的任务,考虑到单个像素值可以被分配的方式数量庞大,而构成我们试图生成的实体图像的这种排列的数量相对较少。

一个生成模型必须是概率的,而不是确定的,因为我们希望能够对输出进行许多不同的变化采样,而不是每次都得到相同的输出。如果我们的模型仅仅是一个固定的计算,比如在训练数据集中每个像素的平均值,那么它就不是生成的。一个生成模型必须包括一个随机组件,影响模型生成的个体样本。

换句话说,我们可以想象存在某种未知的概率分布,解释为什么一些图像可能在训练数据集中找到,而其他图像则不可能。我们的工作是构建一个尽可能模拟这个分布的模型,然后从中采样生成新的、不同的观察结果,看起来就好像它们可能已经包含在原始训练集中。

生成模型与判别模型

为了真正理解生成建模的目标以及为什么这很重要,将其与其对应的判别建模进行比较是有用的。如果你学过机器学习,你所面对的大多数问题很可能是判别性的。为了理解区别,让我们看一个例子。

假设我们有一组绘画数据,一些是梵高画的,一些是其他艺术家的。有了足够的数据,我们可以训练一个判别模型来预测一幅给定的画是不是梵高画。我们的模型会学习到某些颜色、形状和纹理更有可能表明一幅画是荷兰大师的作品,对于具有这些特征的画作,模型会相应地增加其预测权重。图 1-2 展示了判别建模过程——注意它与图 1-1 中展示的生成建模过程的不同之处。

图 1-2。一个训练有素的判别模型,用于预测一幅给定图像是否是梵高的画。

在进行判别建模时,训练数据中的每个观察都有一个标签。对于像我们的艺术家判别器这样的二元分类问题,梵高的画作将被标记为 1,非梵高的画作将被标记为 0。然后我们的模型学习如何区分这两组,并输出一个新观察具有标签 1 的概率——即它是不是梵高画的概率。

相比之下,生成建模不需要数据集被标记,因为它关注的是生成全新的图像,而不是试图预测给定图像的标签。

让我们正式定义这些类型的建模,使用数学符号:

条件生成模型

请注意,我们也可以构建一个生成模型来建模条件概率p ( 𝐱 | y )——观察到具有特定标签y的观察𝐱的概率。

例如,如果我们的数据集包含不同类型的水果,我们可以告诉我们的生成模型专门生成一个苹果的图像。

一个重要的观点是,即使我们能够构建一个完美的判别模型来识别梵高的画作,它仍然不知道如何创作一幅看起来像梵高的画。它只能输出针对现有图像的概率,因为这是它被训练做的事情。我们需要训练一个生成模型,并从这个模型中采样,以生成具有高概率属于原始训练数据集的图像。

生成建模的崛起

直到最近,判别建模一直是机器学习中取得进展的主要驱动力。这是因为对于任何判别问题,相应的生成建模问题通常更难解决。例如,训练一个模型来预测一幅画是否是梵高的比起从头开始训练一个模型生成梵高风格的画作要容易得多。同样,训练一个模型来预测一段文本是否是查尔斯·狄更斯写的比起构建一个模型生成狄更斯风格的段落要容易得多。直到最近,大多数生成挑战都是难以实现的,许多人怀疑它们是否能够被解决。创造力被认为是一种纯粹的人类能力,无法被人工智能所匹敌。

然而,随着机器学习技术的成熟,这种假设逐渐削弱。在过去的 10 年中,该领域中最有趣的进展之一是通过将机器学习应用于生成建模任务的新颖应用。例如,图 1-3 展示了自 2014 年以来在面部图像生成方面已经取得的显著进展。

图 1-3。使用生成建模进行人脸生成在过去十年中取得了显著进展(改编自Brundage et al., 2018

除了更容易处理外,辨别建模在历史上比生成建模更容易应用于跨行业的实际问题。例如,医生可能会从一个可以预测给定视网膜图像是否显示青光眼迹象的模型中受益,但不一定会从一个可以生成眼睛背面的新颖图片的模型中受益。

然而,这也开始发生变化,随着越来越多的公司提供针对特定业务问题的生成服务。例如,现在可以访问 API,根据特定主题生成原创博客文章,生成您产品在任何您想要的环境中的各种图片,或者撰写社交媒体内容和广告文案以匹配您的品牌和目标信息。生成人工智能在游戏设计和电影制作等行业也有明显的积极应用,训练用于输出视频和音乐的模型开始增加价值。

生成建模和人工智能

除了生成建模的实际用途(其中许多尚未被发现),还有三个更深层次的原因,可以认为生成建模是解锁一种更复杂形式的人工智能的关键,超越了辨别建模单独可以实现的范围。

首先,从理论角度来看,我们不应该将机器训练仅限于简单地对数据进行分类。为了完整性,我们还应该关注训练能够捕捉数据分布更完整理解的模型,超越任何特定标签。这无疑是一个更难解决的问题,因为可行输出空间的维度很高,我们将归类为数据集的创作数量相对较少。然而,正如我们将看到的,许多推动辨别建模发展的相同技术,如深度学习,也可以被生成模型利用。

其次,正如我们将在第十二章中看到,生成建模现在被用于推动其他领域的人工智能进步,如强化学习(通过试错来教导代理优化环境中的目标)。假设我们想训练一个机器人在给定地形上行走。传统方法是在环境中运行许多实验,其中代理尝试不同的策略,或者在地形的计算机模拟中尝试。随着时间的推移,代理将学会哪些策略比其他策略更成功,因此逐渐改进。这种方法的挑战在于它相当僵化,因为它被训练来优化一个特定任务的策略。最近开始流行的另一种方法是,代替训练代理人学习环境的世界模型,使用生成模型,独立于任何特定任务。代理可以通过在自己的世界模型中测试策略,而不是在真实环境中测试,来快速适应新任务,这通常在计算上更有效,并且不需要为每个新任务从头开始重新训练。

最后,如果我们真的要说我们已经建立了一台获得了与人类相媲美的智能形式的机器,生成建模肯定是解决方案的一部分。自然界中最好的生成模型之一就是正在阅读本书的人。花点时间考虑一下你是一个多么不可思议的生成模型。你可以闭上眼睛想象大象从任何可能的角度看起来是什么样子。你可以想象你最喜欢的电视节目有许多不同的可能结局,你可以通过在脑海中思考各种未来并相应地采取行动来计划下周。当前的神经科学理论表明,我们对现实的感知并不是一个高度复杂的辨别模型,它根据我们的感官输入产生我们正在经历的预测,而是一个从出生开始就接受训练以产生准确匹配未来的环境的模型。一些理论甚至暗示,这个生成模型的输出就是我们直接感知为现实的东西。显然,深入了解我们如何构建机器来获得这种能力将是我们继续了解大脑运作和普遍人工智能的核心。

我们的第一个生成模型

有了这个想法,让我们开始我们激动人心的生成建模之旅。首先,我们将看一个生成模型的玩具示例,并介绍一些将帮助我们理解本书后面将遇到的更复杂架构的想法。

你好,世界!

让我们从在只有两个维度中玩一个生成建模游戏开始。我选择了一个用于生成点集𝐗的规则,如图 1-4 所示。让我们称这个规则为p data。你的挑战是选择一个不同的点𝐱 = ( x 1 , x 2 ),看起来像是由相同规则生成的。

二维空间中的两个点集

图 1-4。由未知规则生成的二维空间中的点集p data

你是如何选择的?你可能利用现有数据点的知识构建了一个心理模型p model,来估计点更有可能出现的位置。在这方面,p modelp data估计。也许你决定p model应该看起来像图 1-5——一个矩形框,点可能出现在其中,框外则不可能找到任何点。

图 1-5。橙色框,p model,是真实数据生成分布p data的估计

要生成一个新的观察结果,您可以简单地在框内随机选择一个点,或者更正式地,从分布p model采样。恭喜,您刚刚构建了您的第一个生成模型!您已经使用训练数据(黑点)构建了一个模型(橙色区域),您可以轻松地从中进行采样以生成其他似乎属于训练集的点。

现在让我们将这种思维形式化为一个框架,可以帮助我们理解生成建模试图实现的目标。

生成建模框架

我们可以在以下框架中捕捉建立生成模型的动机和目标。

现在让我们揭示真实的数据生成分布p data,看看这个框架如何适用于这个例子。正如我们从图 1-6 中看到的,数据生成规则只是世界陆地的均匀分布,没有机会在海洋中找到一个点。

图 1-6。橙色框p model是真实数据生成分布p data的估计(灰色区域)

显然,我们的模型p model是对p data的过度简化。我们可以检查点 A、B 和 C,以了解我们的模型在多大程度上准确模拟了p data的成功和失败:

  • 点 A 是由我们的模型生成的观察结果,但似乎并不是由p data生成的,因为它位于海洋中间。

  • 点 B 永远不可能由p model生成,因为它位于橙色框外。因此,我们的模型在产生观察结果的整个潜在可能性范围内存在一些缺陷。

  • 点 C 是一个观察结果,可以由p modelp data生成。

尽管模型存在缺陷,但由于它只是一个橙色框内的均匀分布,因此很容易从中进行采样。我们可以轻松地随机选择框内的一个点,以便从中进行采样。

此外,我们可以肯定地说,我们的模型是对捕捉一些基本高级特征的底层复杂分布的简单表示。真实分布被分为有大量陆地(大陆)和没有陆地(海洋)的区域。这也是我们模型的一个高级特征,除了我们只有一个大陆,而不是很多。

这个例子展示了生成建模背后的基本概念。我们在本书中将要解决的问题将会更加复杂和高维,但我们处理问题的基本框架将是相同的。

表示学习

值得深入探讨一下我们所说的学习高维数据的表示是什么意思,因为这是本书中将会反复出现的一个主题。

假设你想向一个在人群中寻找你并不知道你长什么样的人描述你的外貌。你不会从描述你照片的像素 1 的颜色开始,然后是像素 2,然后是像素 3,依此类推。相反,你会合理地假设对方对一个普通人的外貌有一个大致的概念,然后用描述像素组的特征来修正这个基线,比如我有很金黄的头发我戴眼镜。只需不超过 10 条这样的描述,对方就能将描述映射回像素,生成一个你的形象。这个形象可能不完美,但足够接近你的实际外貌,让对方在可能有数百人的人群中找到你,即使他们以前从未见过你。

这就是表示学习的核心思想。我们不是直接对高维样本空间建模,而是使用一些较低维度的潜在空间来描述训练集中的每个观察,并学习一个能够将潜在空间中的点映射到原始域中的点的映射函数。换句话说,潜在空间中的每个点都是某个高维观察的表示

这在实践中意味着什么?假设我们有一个由饼干罐的灰度图像组成的训练集(图 1-7)。

图 1-7。饼干罐数据集

对我们来说,很明显有两个特征可以唯一代表这些罐子:罐子的高度和宽度。也就是说,我们可以将每个罐子的图像转换为一个仅有两个维度的潜在空间中的点,即使训练集中提供的图像是在高维像素空间中的。值得注意的是,这意味着我们也可以通过将适当的映射函数f应用于潜在空间中的新点,生成训练集中不存在的罐子的图像,如图 1-8 所示。

意识到原始数据集可以用更简单的潜在空间来描述对于机器来说并不容易——它首先需要确定高度和宽度是最能描述这个数据集的两个潜在空间维度,然后学习能够将这个空间中的点映射到灰度饼干罐图像的映射函数f。机器学习(特别是深度学习)赋予我们训练机器能够找到这些复杂关系的能力,而无需人类的指导。

图 1-8。饼干罐的 2D 潜在空间和将潜在空间中的点映射回原始图像域的函数f

利用潜在空间训练模型的一个好处是,我们可以通过在更易管理的潜在空间内操作其表示向量,影响图像的高级属性。例如,要调整每个像素的阴影以使饼干罐的图像更高并不明显。然而,在潜在空间中,只需增加高度潜在维度,然后应用映射函数返回到图像域。我们将在下一章中看到一个明确的例子,不是应用于饼干罐而是应用于人脸。

将训练数据集编码到一个潜在空间中,以便我们可以从中进行采样并将点解码回原始域的概念对于许多生成建模技术是常见的,我们将在本书的后续章节中看到。从数学上讲,编码器-解码器技术试图将数据所在的高度非线性流形(例如,在像素空间中)转换为一个更简单的潜在空间,可以从中进行采样,因此很可能潜在空间中的任何点都是一个良好形成的图像的表示,如图 1-9 所示。

图 1-9. 在高维像素空间中的流形被映射到一个更简单的潜在空间,可以从中进行采样

核心概率理论

我们已经看到生成建模与概率分布的统计建模密切相关。因此,现在引入一些核心概率和统计概念是有意义的,这些概念将贯穿本书,用来解释每个模型的理论背景。

如果你从未学习过概率或统计学,不用担心。为了构建本书后面将看到的许多深度学习模型,不必对统计理论有深入的理解。然而,为了充分理解我们试图解决的任务,值得尝试建立对基本概率理论的扎实理解。这样,您将有基础来理解本章后面将介绍的不同类型的生成模型。

作为第一步,我们将定义五个关键术语,将每个术语与我们之前在二维世界地图中建模的生成模型的例子联系起来:

样本空间

样本空间是观察值 𝐱 可以取的所有值的完整集合。

注意

在我们之前的例子中,样本空间包括世界地图上所有的纬度和经度点 𝐱 = ( x 1 , x 2 )。例如,𝐱 = (40.7306, –73.9352) 是样本空间(纽约市)中属于真实数据生成分布的点。𝐱 = (11.3493, 142.1996) 是样本空间中不属于真实数据生成分布的点(在海里)。

概率密度函数

概率密度函数(或简称密度函数)是一个将样本空间中的点 𝐱 映射到 0 到 1 之间的数字的函数 p ( 𝐱 )。密度函数在样本空间中所有点上的积分必须等于 1,以便它是一个明确定义的概率分布。

注意

在世界地图的例子中,我们生成模型的密度函数在橙色框之外为 0,在框内为常数,使得密度函数在整个样本空间上的积分等于 1。

虽然只有一个真实的密度函数p data ( 𝐱 )被假定为生成可观测数据集,但有无限多个密度函数p model ( 𝐱 )可以用来估计p data ( 𝐱 )

参数建模

参数建模是一种技术,我们可以用来构建我们寻找适当p model ( 𝐱 )的方法。参数模型是一组密度函数p θ ( 𝐱 ),可以用有限数量的参数θ来描述。

注意

如果我们假设均匀分布作为我们的模型族,那么我们可以在图 1-5 上绘制的所有可能框的集合是参数模型的一个示例。在这种情况下,有四个参数:框的左下角坐标( θ 1 , θ 2 )和右上角( θ 3 , θ 4 )的坐标。

因此,这个参数模型中的每个密度函数p θ ( 𝐱 )(即每个框)可以用四个数字θ = ( θ 1 , θ 2 , θ 3 , θ 4 )唯一表示。

可能性

参数集θ的可能性 ( θ | 𝐱 )是一个函数,用于衡量给定一些观察点𝐱的θ的合理性。它的定义如下:

( θ | 𝐱 ) = p θ ( 𝐱 )

也就是说,给定一些观察点𝐱的θ的可能性被定义为由θ参数化的密度函数在点𝐱处的值。如果我们有一个完整的独立观测数据集𝐗,那么我们可以写成:

( θ | 𝐗 ) = 𝐱𝐗 p θ ( 𝐱 )

注意

在世界地图示例中,一个只覆盖地图左半部分的橙色框的似然为 0—它不可能生成数据集,因为我们观察到地图右半部分的点。在图 1-5 中的橙色框具有正的似然,因为在该模型下所有数据点的密度函数都为正。

由于 0 到 1 之间大量项的乘积可能会导致计算上的困难,我们通常使用log-likelihood ℓ代替:

( θ | 𝐗 ) = 𝐱𝐗 log p θ ( 𝐱 )

有统计原因解释为什么似然以这种方式定义,但我们也可以看到这种定义在直觉上是有道理的。一组参数θ的似然被定义为如果真实的数据生成分布是由θ参数化的模型,则看到数据的概率。

警告

请注意,似然是参数的函数,而不是数据。它不应该被解释为给定参数集正确的概率—换句话说,它不是参数空间上的概率分布(即,它不会对参数求和/积分为 1)。

参数化建模的重点应该是找到最大化观察到的数据集𝐗的可能性的参数集的最优值^θ。

最大似然估计

最大似然估计是一种技术,它允许我们估计^θ——密度函数 pθ(𝐱)的参数集θ最有可能解释一些观察到的数据𝐗。更正式地说:

θ ^ = argmax 𝐱 ( θ | 𝐗 )

^θ也被称为最大似然估计(MLE)。

注意

在世界地图示例中,MLE 是仍然包含训练集中所有点的最小矩形。

神经网络通常最小化损失函数,因此我们可以等效地讨论找到最小化负对数似然的参数集:

θ ^ = argmin θ - ( θ | 𝐗 ) = argmin θ - log p θ ( 𝐗 )

生成建模可以被看作是一种最大似然估计的形式,其中参数θ是模型中包含的神经网络的权重。我们试图找到这些参数的值,以最大化观察到的给定数据的可能性(或等效地,最小化负对数似然)。

然而,对于高维问题,通常不可能直接计算 pθ(𝐱)—它是难以计算的。正如我们将在下一节中看到的,不同类型的生成模型采取不同的方法来解决这个问题。

生成模型分类

虽然所有类型的生成模型最终都旨在解决相同的任务,但它们在对密度函数 pθ(𝐱)建模时采取了略有不同的方法。广义上说,有三种可能的方法:

  1. 显式地对密度函数建模,但以某种方式限制模型,使得密度函数是可计算的。

  2. 显式地建模密度函数的可处理近似。

  3. 通过直接生成数据的随机过程隐式建模密度函数。

这些在图 1-10 中显示为分类法,与本书第 II 部分中将探索的六种生成模型家族并列。请注意,这些家族并不是相互排斥的—有许多模型是两种不同方法的混合体。您应该将这些家族视为生成建模的不同一般方法,而不是显式模型架构。

图 1-10. 生成建模方法的分类法

我们可以进行的第一个分割是在概率密度函数 p ( 𝐱 )显式建模和被隐式建模的模型之间。

隐式密度模型并不旨在估计概率密度,而是专注于产生直接生成数据的随机过程。隐式生成模型的最著名例子是生成对抗网络。我们可以进一步将显式密度模型分为直接优化密度函数(可处理模型)和仅优化其近似值的模型。

可处理模型对模型架构施加约束,使得密度函数具有易于计算的形式。例如,自回归模型对输入特征进行排序,以便可以按顺序生成输出—例如,逐字或逐像素。归一化流模型将一系列可处理、可逆函数应用于简单分布,以生成更复杂的分布。

近似密度模型包括变分自动编码器,引入潜变量并优化联合密度函数的近似值。基于能量的模型也利用近似方法,但是通过马尔可夫链采样,而不是变分方法。扩散模型通过训练模型逐渐去噪给定的先前损坏的图像来近似密度函数。

贯穿所有生成模型家族类型的共同主题是深度学习。几乎所有复杂的生成模型都以深度神经网络为核心,因为它们可以从头开始训练,学习控制数据结构的复杂关系,而不必事先硬编码信息。我们将在第二章中探讨深度学习,提供实际示例,帮助您开始构建自己的深度神经网络。

生成式深度学习代码库

本章的最后一节将帮助您开始构建生成式深度学习模型,介绍伴随本书的代码库。

提示

本书中的许多示例都改编自Keras 网站上提供的优秀开源实现。我强烈建议您查看这个资源,因为不断添加新模型和示例。

克隆存储库

要开始,您首先需要克隆 Git 存储库。Git是一个开源版本控制系统,可以让您将代码本地复制,以便在自己的计算机上或在基于云的环境中运行笔记本。您可能已经安装了这个,但如果没有,请按照与您操作系统相关的说明进行操作。

要克隆本书的存储库,请导航到您想要存储文件的文件夹,并在终端中输入以下内容:

git clone https://github.com/davidADSP/Generative_Deep_Learning_2nd_Edition.git

您现在应该能够在计算机上的文件夹中看到文件。 `## 使用 Docker

如果您没有自己的 GPU,也没有问题!本书中的所有示例都将在 CPU 上训练,尽管这将比在启用 GPU 的机器上使用时间更长。README中还有关于设置 Google Cloud 环境的部分,该环境可以让您按需使用 GPU。

在 GPU 上运行

总结

本章介绍了生成建模领域,这是机器学习的一个重要分支,它补充了更广泛研究的判别建模。我们讨论了生成建模目前是人工智能研究中最活跃和令人兴奋的领域之一,近年来在理论和应用方面取得了许多进展。

我们从一个简单的玩具示例开始,看到生成建模最终关注的是对数据的基础分布进行建模。这带来了许多复杂和有趣的挑战,我们将这些总结为了一个框架,以理解任何生成模型的理想特性。

本书的代码库旨在与Docker一起使用,这是一种免费的容器化技术,可以使您轻松开始使用新的代码库,而不受架构或操作系统的限制。如果您从未使用过 Docker,不用担心——书籍存储库中有如何开始的描述在README文件中。

在第二章中,我们将开始探索深度学习,并看到如何使用 Keras 构建可以执行判别建模任务的模型。这将为我们提供必要的基础,以便在后面的章节中解决生成式深度学习问题。

我们随后讨论了关键的概率概念,这将有助于充分理解每种生成建模方法的理论基础,并列出了我们将在本书的第 II 部分中探索的六种不同的生成模型系列。我们还看到了如何开始使用Generative Deep Learning代码库,方法是克隆存储库。

第二章:深度学习

让我们从深度学习的基本定义开始:

深度学习是一类机器学习算法,它使用多个堆叠层的处理单元非结构化数据中学习高级表示。

要充分理解深度学习,我们需要进一步探讨这个定义。首先,我们将看一下深度学习可以用来建模的不同类型的非结构化数据,然后我们将深入研究构建多个堆叠层的处理单元来解决分类任务的机制。这将为我们未来关注生成任务的章节奠定基础。

深度学习数据

许多类型的机器学习算法需要结构化的表格数据作为输入,这些数据被排列成描述每个观察结果的特征列。例如,一个人的年龄、收入和上个月的网站访问次数都是可以帮助预测这个人是否会在接下来的一个月订阅特定在线服务的特征。我们可以使用这些特征的结构化表格来训练逻辑回归、随机森林或 XGBoost 模型来预测二元响应变量——这个人是否订阅了(1)还是没有订阅(0)?在这里,每个单独的特征都包含了关于观察结果的信息,模型将学习这些特征如何相互作用以影响响应。

非结构化数据指的是任何不自然地排列成特征列的数据,例如图像、音频和文本。当然,图像具有空间结构,录音或文本段落具有时间结构,视频数据既具有空间结构又具有时间结构,但由于数据不是以特征列的形式到达,因此被认为是非结构化的,如图 2-1 所示。

图 2-1。结构化数据和非结构化数据之间的区别

当我们的数据是非结构化的时候,单个像素、频率或字符几乎完全没有信息性。例如,知道图像的第 234 个像素是一种泥泞的褐色并不能真正帮助识别图像是房子还是狗,知道句子的第 24 个字符是一个e并不能帮助预测文本是关于足球还是政治。

像素或字符实际上只是画布上的凹痕,其中嵌入了高级信息性特征,例如烟囱的图像或前锋这个词。如果图像中的烟囱被放在房子的另一侧,图像仍然包含烟囱,但这些信息现在由完全不同的像素传递。如果文本中的前锋出现在稍早或稍晚的位置,文本仍然是关于足球的,但不同的字符位置会提供这些信息。数据的粒度与高度的空间依赖性破坏了像素或字符作为独立信息特征的概念。

因此,如果我们在原始像素值上训练逻辑回归、随机森林或 XGBoost 模型,那么训练好的模型通常只会在最简单的分类任务中表现不佳。这些模型依赖于输入特征具有信息性且不具有空间依赖性。另一方面,深度学习模型可以直接从非结构化数据中学习如何构建高级信息性特征。

深度学习可以应用于结构化数据,但其真正的力量,特别是在生成建模方面,来自于其处理非结构化数据的能力。通常,我们希望生成非结构化数据,例如新图像或原始文本字符串,这就是为什么深度学习对生成建模领域产生如此深远影响的原因。

深度神经网络

大多数深度学习系统是人工神经网络(ANNs,或简称神经网络)具有多个堆叠的隐藏层。因此,深度学习现在几乎已经成为深度神经网络的同义词。然而,任何使用多层学习输入数据的高级表示的系统也是一种深度学习形式(例如,深度信念网络)。

让我们首先详细解释一下神经网络的含义,然后看看它们如何用于从非结构化数据中学习高级特征。

什么是神经网络?

神经网络由一系列堆叠的组成。每一层包含通过一组权重连接到前一层单元的单元。正如我们将看到的,有许多不同类型的层,但其中最常见的是全连接(或密集)层,它将该层中的所有单元直接连接到前一层的每个单元。

所有相邻层都是全连接的神经网络称为多层感知器(MLPs)。这是我们将要学习的第一种神经网络。图 2-2 中显示了一个 MLP 的示例。

图 2-2。一个预测脸部是否微笑的多层感知器的示例

输入(例如,一张图像)依次通过网络中的每一层进行转换,直到达到输出层,这被称为网络的前向传递。具体来说,每个单元对其输入的加权和应用非线性变换,并将输出传递到后续层。最终的输出层是这个过程的结尾,单个单元输出一个概率,表明原始输入属于特定类别(例如,微笑)。

深度神经网络的魔力在于找到每一层的权重集,以获得最准确的预测。找到这些权重的过程就是我们所说的训练网络。

在训练过程中,一批图像通过网络传递,并将预测输出与真实值进行比较。例如,网络可能为一个真正微笑的人的图像输出 80%的概率,为一个真正不微笑的人的图像输出 23%的概率。对于这些示例,完美的预测将输出 100%和 0%,因此存在一定的误差。然后,预测中的误差通过网络向后传播,调整每组权重,使其朝着最显著改善预测的方向微调。这个过程被适当地称为反向传播。逐渐地,每个单元变得擅长识别一个特定的特征,最终帮助网络做出更好的预测。

学习高级特征

使神经网络如此强大的关键属性是它们能够从输入数据中学习特征,而无需人类指导。换句话说,我们不需要进行任何特征工程,这就是为什么神经网络如此有用!我们可以让模型决定如何安排其权重,只受其希望最小化预测误差的影响。

例如,让我们来解释一下图 2-2 中所示的网络,假设它已经被训练得可以准确预测给定输入脸部是否微笑:

  1. 单元 A 接收输入像素的单个通道的值。

  2. 单元 B 组合其输入值,使得当存在特定的低级特征,例如边缘时,它发射最强。

  3. 单元 C 组合低级特征,使得当图像中看到高级特征,例如牙齿时,它发射最强。

  4. 单元 D 结合高级特征,使得当原始图像中的人在微笑时它发射最强。

每个后续层中的单元能够通过结合来自前一层的低级特征来表示原始输入的越来越复杂的方面。令人惊讶的是,这是训练过程中自然产生的——我们不需要告诉每个单元要寻找什么,或者它应该寻找高级特征还是低级特征。

输入层和输出层之间的层被称为隐藏层。虽然我们的例子只有两个隐藏层,但深度神经网络可以有更多层。堆叠大量层允许神经网络逐渐构建信息,从先前层中的低级特征逐渐构建出更高级别的特征。例如,用于图像识别的 ResNet 包含 152 层。

接下来,我们将直接深入深度学习的实践方面,并使用 TensorFlow 和 Keras 进行设置,以便您可以开始构建自己的深度神经网络。

TensorFlow 和 Keras

TensorFlow是由谷歌开发的用于机器学习的开源 Python 库。TensorFlow 是构建机器学习解决方案中最常用的框架之一,特别强调张量的操作(因此得名)。它提供了训练神经网络所需的低级功能,例如计算任意可微表达式的梯度和高效执行张量操作。

Keras是一个用于构建神经网络的高级 API,构建在 TensorFlow 之上(图 2-3)。它非常灵活和用户友好,是开始深度学习的理想选择。此外,Keras 提供了许多有用的构建模块,可以通过其功能 API 组合在一起,创建高度复杂的深度学习架构。

图 2-3. TensorFlow 和 Keras 是构建深度学习解决方案的优秀工具

如果您刚开始学习深度学习,我强烈推荐使用 TensorFlow 和 Keras。这个设置将允许您在生产环境中构建任何您能想到的网络,同时还提供易于学习的 API,可以快速开发新的想法和概念。让我们从看看使用 Keras 构建多层感知器有多容易开始。

多层感知器(MLP)

在本节中,我们将使用监督学习训练一个 MLP 来对给定的图像进行分类。监督学习是一种机器学习算法,计算机在标记的数据集上进行训练。换句话说,用于训练的数据集包括带有相应输出标签的输入数据。算法的目标是学习输入数据和输出标签之间的映射,以便它可以对新的、未见过的数据进行预测。

MLP 是一种判别模型(而不是生成模型),但在本书后面的章节中,监督学习仍将在许多类型的生成模型中发挥作用,因此这是我们旅程的一个好起点。

运行此示例的代码

这个例子的代码可以在位于书籍存储库中的 Jupyter 笔记本中找到,位置为notebooks/02_deeplearning/01_mlp/mlp.ipynb

准备数据

在这个例子中,我们将使用CIFAR-10数据集,这是一个包含 60,000 个 32×32 像素彩色图像的集合,与 Keras 捆绑在一起。每个图像被分类为 10 个类别中的一个,如图 2-4 所示。

图 2-4. CIFAR-10 数据集中的示例图像(来源:Krizhevsky, 2009

默认情况下,图像数据由每个像素通道的 0 到 255 之间的整数组成。我们首先需要通过将这些值缩放到 0 到 1 之间来预处理图像,因为当每个输入的绝对值小于 1 时,神经网络的效果最好。

我们还需要将图像的整数标签更改为独热编码向量,因为神经网络的输出将是图像属于每个类的概率。如果图像的类整数标签是i,那么它的独热编码是一个长度为 10 的向量(类的数量),除了第i个元素为 1 之外,其他元素都为 0。这些步骤在示例 2-1 中显示。

示例 2-1。预处理 CIFAR-10 数据集
import numpy as np
from tensorflow.keras import datasets, utils

(x_train, y_train), (x_test, y_test) = datasets.cifar10.load_data() # ①

NUM_CLASSES = 10

x_train = x_train.astype('float32') / 255.0 # ②
x_test = x_test.astype('float32') / 255.0

y_train = utils.to_categorical(y_train, NUM_CLASSES) # ③
y_test = utils.to_categorical(y_test, NUM_CLASSES)

加载 CIFAR-10 数据集。x_trainx_test分别是形状为[50000, 32, 32, 3][10000, 32, 32, 3]numpy数组。y_trainy_test分别是形状为[50000, 1][10000, 1]numpy数组,包含每个图像类的范围为 0 到 9 的整数标签。

缩放每个图像,使像素通道值介于 0 和 1 之间。

对标签进行独热编码——y_trainy_test的新形状分别为[50000, 10][10000, 10]

我们可以看到训练图像数据(x_train)存储在形状为[50000, 32, 32, 3]张量中。在这个数据集中没有;相反,这是一个具有四个维度的张量。张量只是一个多维数组——它是矩阵向超过两个维度的自然扩展。这个张量的第一个维度引用数据集中图像的索引,第二和第三个维度与图像的大小有关,最后一个是通道(即红色、绿色或蓝色,因为这些是 RGB 图像)。

例如,示例 2-2 展示了如何找到图像中特定像素的通道值。

示例 2-2。图像 54 中位置为(12,13)的像素的绿色通道(1)值
x_train[54, 12, 13, 1]
# 0.36862746

构建模型

在 Keras 中,您可以将神经网络的结构定义为Sequential模型或使用功能 API。

Sequential模型适用于快速定义一系列层的线性堆叠(即一个层直接跟在前一个层后面,没有任何分支)。我们可以使用Sequential类来定义我们的 MLP 模型,如示例 2-3 所示。

示例 2-3。使用Sequential模型构建我们的 MLP
from tensorflow.keras import layers, models

model = models.Sequential([
    layers.Flatten(input_shape=(32, 32, 3)),
    layers.Dense(200, activation = 'relu'),
    layers.Dense(150, activation = 'relu'),
    layers.Dense(10, activation = 'softmax'),
])

本书中的许多模型要求从一层输出传递到多个后续层,或者反过来,一层接收来自多个前面层的输入。对于这些模型,Sequential类不适用,我们需要使用功能 API,这样更加灵活。

提示

我建议即使您刚开始使用 Keras 构建线性模型,也应该使用功能 API 而不是Sequential模型,因为随着您的神经网络变得更加复杂,功能 API 将在长远中为您提供更好的服务。功能 API 将为您提供对深度神经网络设计的完全自由。

示例 2-4 展示了使用功能 API 编码的相同 MLP。在使用功能 API 时,我们使用Model类来定义模型的整体输入和输出层。

示例 2-4。使用功能 API 构建我们的 MLP
from tensorflow.keras import layers, models

input_layer = layers.Input(shape=(32, 32, 3))
x = layers.Flatten()(input_layer)
x = layers.Dense(units=200, activation = 'relu')(x)
x = layers.Dense(units=150, activation = 'relu')(x)
output_layer = layers.Dense(units=10, activation = 'softmax')(x)
model = models.Model(input_layer, output_layer)

这两种方法提供相同的模型——架构的图表显示在图 2-5 中。

图 2-5。MLP 架构的图表

现在让我们更详细地看一下 MLP 中使用的不同层和激活函数。

为构建我们的 MLP,我们使用了三种不同类型的层:InputFlattenDense

Input层是网络的入口点。我们告诉网络每个数据元素的形状应该是一个元组。请注意,我们不指定批量大小;这是不必要的,因为我们可以同时将任意数量的图像传递到Input层中。我们不需要在Input层定义中明确指定批量大小。

接下来,我们将这个输入展平成一个向量,使用Flatten层。这将导致一个长度为 3072 的向量(= 32 × 32 × 3)。我们这样做的原因是因为后续的Dense层要求其输入是平坦的,而不是多维数组。正如我们将在后面看到的,其他类型的层需要多维数组作为输入,因此您需要了解每种层类型所需的输入和输出形状,以便了解何时需要使用Flatten

Dense层是神经网络中最基本的构建块之一。它包含一定数量的单元,这些单元与前一层密切连接,也就是说,层中的每个单元都与前一层中的每个单元连接,通过一个携带权重的单一连接(可以是正数或负数)。给定单元的输出是它从前一层接收的输入的加权和,然后通过非线性激活函数传递到下一层。激活函数对于确保神经网络能够学习复杂函数并且不仅仅输出其输入的线性组合至关重要。

激活函数

有许多种激活函数,但其中最重要的三种是 ReLU、sigmoid 和 softmax。

ReLU(修正线性单元)激活函数被定义为如果输入为负数则为 0,否则等于输入。LeakyReLU激活函数与 ReLU 非常相似,但有一个关键区别:ReLU 激活函数对于小于 0 的输入值返回 0,而 LeakyReLU 函数返回与输入成比例的一个小负数。如果 ReLU 单元总是输出 0,有时会出现死亡现象,因为存在对负值预激活的大偏差。在这种情况下,梯度为 0,因此没有错误通过该单元向后传播。LeakyReLU 激活通过始终确保梯度为非零来解决这个问题。基于 ReLU 的函数是在深度网络的层之间使用的最可靠的激活函数之一,以鼓励稳定的训练。

如果您希望从该层输出的结果在 0 和 1 之间缩放,那么sigmoid激活函数是有用的,例如,对于具有一个输出单元的二元分类问题或多标签分类问题,其中每个观察结果可以属于多个类。图 2-6 显示了 ReLU、LeakyReLU 和 sigmoid 激活函数并排进行比较。

图 2-6。ReLU、LeakyReLU 和 sigmoid 激活函数

如果您希望从该层输出的总和等于 1,则softmax激活函数是有用的;例如,对于每个观察结果只属于一个类的多类分类问题。它被定义为:

y i = e x i j=1 J e x j

在这里,J是层中单元的总数。在我们的神经网络中,我们在最后一层使用 softmax 激活,以确保输出是一组总和为 1 的 10 个概率,这可以被解释为图像属于每个类的可能性。

在 Keras 中,激活函数可以在层内定义(示例 2-5)或作为单独的层定义(示例 2-6)。

示例 2-5。作为Dense层的一部分定义的 ReLU 激活函数
x = layers.Dense(units=200, activation = 'relu')(x)
示例 2-6。作为自己的层定义的 ReLU 激活函数
x = layers.Dense(units=200)(x)
x = layers.Activation('relu')(x)

在我们的示例中,我们通过两个Dense层传递输入,第一个有 200 个单元,第二个有 150 个,两者都带有 ReLU 激活函数。

检查模型

我们可以使用model.summary()方法来检查每一层网络的形状,如表 2-1 所示。

表 2-1. model.summary()方法的输出

层(类型) 输出形状 参数 #
InputLayer (None, 32, 32, 3) 0
展平 (None, 3072) 0
Dense (None, 200) 614,600
Dense (None, 150) 30,150
Dense (None, 10) 1,510
总参数 646,260
可训练参数 646,260
不可训练参数 0

注意我们的Input层的形状与x_train的形状匹配,而我们的Dense输出层的形状与y_train的形状匹配。Keras 使用None作为第一维的标记,以显示它尚不知道将传递到网络中的观测数量。实际上,它不需要知道;我们可以一次通过 1 个观测或 1000 个观测通过网络。这是因为张量操作是使用线性代数同时在所有观测上进行的—这是由 TensorFlow 处理的部分。这也是为什么在 GPU 上训练深度神经网络而不是在 CPU 上时性能会提高的原因:GPU 针对大型张量操作进行了优化,因为这些计算对于复杂的图形处理也是必要的。

summary方法还会给出每一层将被训练的参数(权重)的数量。如果你发现你的模型训练速度太慢,检查摘要看看是否有任何包含大量权重的层。如果有的话,你应该考虑是否可以减少该层中的单元数量以加快训练速度。

提示

确保你理解每一层中参数是如何计算的!重要的是要记住,默认情况下,给定层中的每个单元也连接到一个额外的偏置单元,它总是输出 1。这确保了即使来自前一层的所有输入为 0,单元的输出仍然可以是非零的。

因此,200 单元Dense层中的参数数量为 200 * (3,072 + 1) = 614,600。

编译模型

在这一步中,我们使用一个优化器和一个损失函数来编译模型,如示例 2-7 所示。

示例 2-7. 定义优化器和损失函数
from tensorflow.keras import optimizers

opt = optimizers.Adam(learning_rate=0.0005)
model.compile(loss='categorical_crossentropy', optimizer=opt,
              metrics=['accuracy'])

现在让我们更详细地看一下我们所说的损失函数和优化器。

损失函数

损失函数被神经网络用来比较其预测输出与实际情况的差异。它为每个观测返回一个单一数字;这个数字越大,网络在这个观测中的表现就越差。

Keras 提供了许多内置的损失函数可供选择,或者你可以创建自己的损失函数。最常用的三个是均方误差、分类交叉熵和二元交叉熵。重要的是要理解何时适合使用每种损失函数。

如果你的神经网络旨在解决回归问题(即输出是连续的),那么你可能会使用均方误差损失。这是每个输出单元的实际值y i和预测值p i之间的平方差的平均值,其中平均值是在所有n个输出单元上取得的:

MSE = 1 n i=1 n (y i -p i ) 2

如果你正在处理一个分类问题,其中每个观测只属于一个类,那么分类交叉熵是正确的损失函数。它定义如下:

- i=1 n y i log ( p i )

最后,如果你正在处理一个具有一个输出单元的二元分类问题,或者一个每个观测可以同时属于多个类的多标签问题,你应该使用二元交叉熵

- 1 n i=1 n ( y i log ( p i ) + ( 1 - y i ) log ( 1 - p i ) )

优化器

优化器 是基于损失函数的梯度更新神经网络权重的算法。最常用和稳定的优化器之一是 Adam(自适应矩估计)。³ 在大多数情况下,您不需要调整 Adam 优化器的默认参数,除了 学习率。学习率越大,每个训练步骤中权重的变化就越大。虽然初始时使用较大的学习率训练速度更快,但缺点是可能导致训练不稳定,无法找到损失函数的全局最小值。这是您可能需要在训练过程中调整的参数。

另一个您可能遇到的常见优化器是 RMSProp(均方根传播)。同样,您不需要太多调整这个优化器的参数,但值得阅读Keras 文档以了解每个参数的作用。

我们将损失函数和优化器一起传递给模型的 compile 方法,还有一个 metrics 参数,我们可以在训练过程中指定任何额外的指标,如准确率。

训练模型

到目前为止,我们还没有向模型展示任何数据。我们只是设置了架构并使用损失函数和优化器编译了模型。

要针对数据训练模型,我们只需调用 fit 方法,如示例 2-8 所示。

示例 2-8. 调用 fit 方法来训练模型
model.fit(x_train # ①
          , y_train # ②
          , batch_size = 32 # ③
          , epochs = 10 # ④
          , shuffle = True # ⑤
          )

原始图像数据。

独热编码的类标签。

batch_size 确定每个训练步骤将传递给网络多少观察值。

epochs 确定网络将被展示完整训练数据的次数。

如果 shuffle = True,每个训练步骤将从训练数据中随机抽取批次而不重复。

这将开始训练一个深度神经网络,以预测来自 CIFAR-10 数据集的图像的类别。训练过程如下。

首先,网络的权重被初始化为小的随机值。然后网络执行一系列训练步骤。在每个训练步骤中,通过网络传递一个 batch 图像,并将错误反向传播以更新权重。batch_size 确定每个训练步骤批次中有多少图像。批量大小越大,梯度计算越稳定,但每个训练步骤越慢。

提示

使用整个数据集在每个训练步骤中计算梯度将耗费太多时间和计算资源,因此通常使用 32 到 256 之间的批量大小。现在推荐的做法是随着训练的进行增加批量大小。⁴

这将持续到数据集中的所有观察值都被看到一次。这完成了第一个 epoch。然后数据再次以批次的形式通过网络,作为第二个 epoch 的一部分。这个过程重复,直到指定的 epoch 数已经过去。

在训练过程中,Keras 会输出过程的进展,如图 2-7 所示。我们可以看到训练数据集已经被分成了 1,563 批次(每批包含 32 张图片),并且已经被展示给网络 10 次(即 10 个 epochs),每批大约需要 2 毫秒的时间。分类交叉熵损失从 1.8377 下降到 1.3696,导致准确率从第一个 epoch 后的 33.69%增加到第十个 epoch 后的 51.67%。

图 2-7. fit 方法的输出

评估模型

我们知道模型在训练集上的准确率为 51.9%,但它在从未见过的数据上表现如何?

为了回答这个问题,我们可以使用 Keras 提供的evaluate方法,如示例 2-9 所示。

示例 2-9。在测试集上评估模型性能
model.evaluate(x_test, y_test)

图 2-8 显示了这种方法的输出。

图 2-8。evaluate方法的输出

输出是我们正在监控的指标列表:分类交叉熵和准确率。我们可以看到,即使在它从未见过的图像上,模型的准确率仍然是 49.0%。请注意,如果模型是随机猜测的,它将达到大约 10%的准确率(因为有 10 个类别),因此 49.0%是一个很好的结果,考虑到我们使用了一个非常基本的神经网络。

我们可以使用predict方法查看测试集上的一些预测,如示例 2-10 所示。

示例 2-10。使用predict方法查看测试集上的预测
CLASSES = np.array(['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog'
                   , 'frog', 'horse', 'ship', 'truck'])

preds = model.predict(x_test) # ①
preds_single = CLASSES[np.argmax(preds, axis = -1)] # ②
actual_single = CLASSES[np.argmax(y_test, axis = -1)]

preds是一个形状为[10000, 10]的数组,即每个观测的 10 个类别概率的向量。

我们将这个概率数组转换回一个单一的预测,使用numpyargmax函数。这里,axis = -1告诉函数将数组折叠到最后一个维度(类别维度),因此preds_single的形状为[10000, 1]

我们可以使用示例 2-11 中的代码查看一些图像以及它们的标签和预测。如预期的那样,大约一半是正确的。

示例 2-11。显示 MLP 的预测与实际标签
import matplotlib.pyplot as plt

n_to_show = 10
indices = np.random.choice(range(len(x_test)), n_to_show)

fig = plt.figure(figsize=(15, 3))
fig.subplots_adjust(hspace=0.4, wspace=0.4)

for i, idx in enumerate(indices):
    img = x_test[idx]
    ax = fig.add_subplot(1, n_to_show, i+1)
    ax.axis('off')
    ax.text(0.5, -0.35, 'pred = ' + str(preds_single[idx]), fontsize=10
       , ha='center', transform=ax.transAxes)
    ax.text(0.5, -0.7, 'act = ' + str(actual_single[idx]), fontsize=10
        , ha='center', transform=ax.transAxes)
    ax.imshow(img)

图 2-9 显示了模型随机选择的一些预测,以及真实标签。

图 2-9。模型进行的一些预测,以及实际标签

恭喜!您刚刚使用 Keras 构建了一个多层感知器,并用它对新数据进行了预测。即使这是一个监督学习问题,但当我们在未来的章节中构建生成模型时,本章的许多核心思想(如损失函数、激活函数和理解层形状)仍然非常重要。接下来,我们将探讨通过引入一些新的层类型来改进这个模型的方法。

卷积神经网络(CNN)

我们的网络尚未表现得像它可能表现得那样好的原因之一是网络中没有考虑输入图像的空间结构。事实上,我们的第一步是将图像展平为一个单一向量,以便我们可以将其传递给第一个Dense层!

为了实现这一点,我们需要使用卷积层

卷积层

首先,我们需要了解在深度学习背景下卷积的含义。

图 2-10 显示了一个灰度图像的两个不同的 3×3×1 部分,与一个 3×3×1滤波器(或核心)进行卷积。卷积是通过将滤波器逐像素地与图像部分相乘,并将结果求和来执行的。当图像部分与滤波器紧密匹配时,输出更为正向,当图像部分与滤波器的反向匹配时,输出更为负向。顶部示例与滤波器强烈共振,因此产生一个较大的正值。底部示例与滤波器的共振不大,因此产生一个接近零的值。

图 2-10。应用于灰度图像两个部分的 3×3 卷积滤波器

如果我们将滤波器从左到右和从上到下移动到整个图像上,并记录卷积输出,我们将获得一个新的数组,根据滤波器中的值选择输入的特定特征。例如,图 2-11 显示了突出显示水平和垂直边缘的两个不同滤波器。

运行此示例的代码

您可以在位于书籍存储库中的notebooks/02_deeplearning/02_cnn/convolutions.ipynb的 Jupyter 笔记本中手动查看这个卷积过程。

图 2-11。应用于灰度图像的两个卷积滤波器

卷积层只是一组滤波器,其中存储在滤波器中的值是通过训练的神经网络学习的权重。最初这些是随机的,但逐渐滤波器调整它们的权重以开始选择有趣的特征,如边缘或特定的颜色组合。

在 Keras 中,Conv2D层将卷积应用于具有两个空间维度(如图像)的输入张量。例如,示例 2-12 中显示的代码构建了一个具有两个滤波器的卷积层,以匹配图 2-11 中的示例。

示例 2-12。应用于灰度输入图像的Conv2D
from tensorflow.keras import layers

input_layer = layers.Input(shape=(64,64,1))
conv_layer_1 = layers.Conv2D(
    filters = 2
    , kernel_size = (3,3)
    , strides = 1
    , padding = "same"
    )(input_layer)

接下来,让我们更详细地看一下Conv2D层的两个参数——stridespadding

步幅

strides参数是层用来在输入上移动滤波器的步长。增加步长会减小输出张量的大小。例如,当strides = 2时,输出张量的高度和宽度将是输入张量大小的一半。这对于通过网络传递时减小张量的空间大小,同时增加通道数量是有用的。

填充

padding = "same"输入参数使用零填充输入数据,以便当strides = 1时,从层的输出大小与输入大小完全相同。

图 2-12 显示了一个 3×3 的卷积核在一个 5×5 的输入图像上进行传递,其中padding = "same"strides = 1。这个卷积层的输出大小也将是 5×5,因为填充允许卷积核延伸到图像的边缘,使其在两个方向上都适合五次。没有填充,卷积核只能在每个方向上适合三次,从而给出一个 3×3 的输出大小。

图 2-12。一个 3×3×1 的卷积核(灰色)在一个 5×5×1 的输入图像(蓝色)上进行传递,其中padding = "same"strides = 1,生成 5×5×1 的输出(绿色)(来源:Dumoulin 和 Visin,2018)

设置padding = "same"是一种确保您能够轻松跟踪张量大小的好方法,因为它通过许多卷积层时。具有padding = "same"的卷积层的输出形状是:

( inputheight stride , inputwidth stride , f i l t e r s )

堆叠卷积层

Conv2D层的输出是另一个四维张量,现在的形状是(batch_size, height, width, filters),因此我们可以将Conv2D层堆叠在一起,以增加神经网络的深度并使其更强大。为了演示这一点,让我们想象我们正在将Conv2D层应用于 CIFAR-10 数据集,并希望预测给定图像的标签。请注意,这一次,我们不是一个输入通道(灰度),而是三个(红色、绿色和蓝色)。

示例 2-13 展示了如何构建一个简单的卷积神经网络,我们可以训练它成功完成这项任务。

示例 2-13。使用 Keras 构建卷积神经网络模型的代码
from tensorflow.keras import layers, models

input_layer = layers.Input(shape=(32,32,3))
conv_layer_1 = layers.Conv2D(
    filters = 10
    , kernel_size = (4,4)
    , strides = 2
    , padding = 'same'
    )(input_layer)
conv_layer_2 = layers.Conv2D(
    filters = 20
    , kernel_size = (3,3)
    , strides = 2
    , padding = 'same'
    )(conv_layer_1)
flatten_layer = layers.Flatten()(conv_layer_2)
output_layer = layers.Dense(units=10, activation = 'softmax')(flatten_layer)
model = models.Model(input_layer, output_layer)

这段代码对应于图 2-13 中显示的图表。

图 2-13。卷积神经网络的图表

请注意,现在我们正在处理彩色图像,第一个卷积层中的每个滤波器的深度为 3,而不是 1(即每个滤波器的形状为 4×4×3,而不是 4×4×1)。这是为了匹配输入图像的三个通道(红色、绿色、蓝色)。同样的想法也适用于第二个卷积层中的深度为 10 的滤波器,以匹配第一个卷积层输出的 10 个通道。

提示

一般来说,层中滤波器的深度总是等于前一层输出的通道数。

检查模型

从一个卷积层到下一个卷积层,数据流经过时张量形状如何变化真的很有启发性。我们可以使用model.summary()方法检查张量在网络中传递时的形状(表 2-2)。

表 2-2. CNN 模型摘要

层(类型) 输出形状 参数数量
输入层 (None, 32, 32, 3) 0
Conv2D (None, 16, 16, 10) 490
Conv2D (None, 8, 8, 20) 1,820
Flatten (None, 1280) 0
Dense (None, 10) 12,810
总参数 15,120
可训练参数 15,120
不可训练参数 0

让我们逐层走过我们的网络,注意张量的形状:

  1. 输入形状为(None, 32, 32, 3)—Keras 使用None表示我们可以同时通过网络传递任意数量的图像。由于网络只是执行张量代数运算,我们不需要单独通过网络传递图像,而是可以一起作为批次传递它们。

  2. 第一个卷积层中每个滤波器的形状是 4×4×3。这是因为我们选择每个滤波器的高度和宽度为 4(kernel_size=(4,4)),并且在前一层中有三个通道(红色、绿色和蓝色)。因此,该层中的参数(或权重)数量为(4×4×3+1)×10=490,其中+1 是由于每个滤波器附加了一个偏置项。每个滤波器的输出将是滤波器权重和它所覆盖的图像的 4×4×3 部分的逐像素乘积。由于strides=2padding="same",输出的宽度和高度都减半为 16,由于有 10 个滤波器,第一层的输出是一批张量,每个张量的形状为[16,16,10]

  3. 在第二个卷积层中,我们选择滤波器为 3×3,它们现在的深度为 10,以匹配前一层中的通道数。由于这一层中有 20 个滤波器,这给出了总参数(权重)数量为(3×3×10+1)×20=1,820。同样,我们使用strides=2padding="same",所以宽度和高度都减半。这给出了一个总体输出形状为(None, 8, 8, 20)

  4. 现在我们使用 Keras 的Flatten层展平张量。这会产生一组 8×8×20=1,280 个单元。请注意,在Flatten层中没有需要学习的参数,因为该操作只是对张量进行重组。

  5. 最后,我们将这些单元连接到一个具有 softmax 激活函数的 10 单元Dense层,表示 10 类分类任务中每个类别的概率。这会创建额外的 1,280×10=12,810 个参数(权重)需要学习。

这个例子演示了如何将卷积层链接在一起创建卷积神经网络。在我们看到这与我们密集连接的神经网络在准确性上的比较之前,我们将研究另外两种也可以提高性能的技术:批量归一化和 dropout。

批量归一化

训练深度神经网络时的一个常见问题是确保网络的权重保持在合理范围内的数值范围内 - 如果它们开始变得过大,这表明您的网络正在遭受所谓的梯度爆炸问题。当错误向后传播通过网络时,早期层中梯度的计算有时可能会呈指数增长,导致权重值出现剧烈波动。

警告

如果您的损失函数开始返回NaN,那么很有可能是您的权重已经变得足够大,导致溢出错误。

这并不一定会立即发生在您开始训练网络时。有时候,它可能在几个小时内愉快地训练,突然损失函数返回NaN,您的网络就爆炸了。这可能非常恼人。为了防止这种情况发生,您需要了解梯度爆炸问题的根本原因。

协变量转移

将输入数据缩放到神经网络的一个原因是确保在前几次迭代中稳定地开始训练。由于网络的权重最初是随机化的,未缩放的输入可能会导致立即产生激活值过大,从而导致梯度爆炸。例如,我们通常将像素值从 0-255 传递到输入层,而不是将这些值缩放到-1 到 1 之间。

因为输入被缩放,自然地期望未来所有层的激活也相对缩放。最初可能是正确的,但随着网络训练和权重远离其随机初始值,这个假设可能开始破裂。这种现象被称为协变量转移

协变量转移类比

想象一下,你正拿着一摞高高的书,突然被一阵风吹袭。你将书向与风相反的方向移动以补偿,但在这样做的过程中,一些书会移动,使得整个塔比以前稍微不稳定。最初,这没关系,但随着每阵风,这摞书变得越来越不稳定,直到最终书移动得太多,整摞书倒塌。这就是协变量转移。

将这与神经网络联系起来,每一层就像堆叠中的一本书。为了保持稳定,当网络更新权重时,每一层都隐含地假设其来自下一层的输入分布在迭代中大致保持一致。然而,由于没有任何东西可以阻止任何激活分布在某个方向上发生显着变化,这有时会导致权重值失控和网络整体崩溃。

使用批量归一化进行训练

批量归一化是一种极大地减少这个问题的技术。解决方案出奇地简单。在训练期间,批量归一化层计算每个输入通道在批处理中的均值和标准差,并通过减去均值并除以标准差来进行归一化。然后,每个通道有两个学习参数,即缩放(gamma)和移位(beta)。输出只是归一化的输入,由 gamma 缩放并由 beta 移位。图 2-14 展示了整个过程。

图 2-14。批量归一化过程(来源:Ioffe and Szegedy, 2015)⁶

我们可以在密集层或卷积层之后放置批量归一化层来归一化输出。

提示

参考我们之前的例子,这有点像用一小组可调节弹簧连接书层,以确保它们的位置随时间不会发生明显的整体移动。

使用批量归一化进行预测

您可能想知道这个层在预测时是如何工作的。在预测时,我们可能只想预测单个观测值,因此没有批次可以计算平均值和标准差。为了解决这个问题,在训练期间,批归一化层还会计算每个通道的平均值和标准差的移动平均值,并将这个值作为该层的一部分存储起来,以便在测试时使用。

批归一化层中包含多少参数?对于前一层中的每个通道,需要学习两个权重:比例(gamma)和偏移(beta)。这些是可训练参数。移动平均值和标准差也需要针对每个通道进行计算,但由于它们是从通过该层的数据派生而来,而不是通过反向传播进行训练,因此被称为不可训练参数。总共,这为前一层中的每个通道提供了四个参数,其中两个是可训练的,两个是不可训练的。

在 Keras 中,BatchNormalization层实现了批归一化功能,如例 2-14 所示。

例 2-14. Keras 中的BatchNormalization
from tensorflow.keras import layers
layers.BatchNormalization(momentum = 0.9)

在计算移动平均值和移动标准差时,momentum参数是给予先前值的权重。

Dropout

在备考考试时,学生通常会使用过去的试卷和样题来提高对学科材料的了解。一些学生试图记住这些问题的答案,但在考试中却因为没有真正理解学科内容而失败。最好的学生利用练习材料来进一步提高他们对学科的整体理解,这样当面对以前没有见过的新问题时,他们仍然能够正确回答。

相同的原则适用于机器学习。任何成功的机器学习算法必须确保它能泛化到未见过的数据,而不仅仅是记住训练数据集。如果一个算法在训练数据集上表现良好,但在测试数据集上表现不佳,我们称其为过拟合。为了解决这个问题,我们使用正则化技术,确保模型在开始过拟合时受到惩罚。

有许多方法可以对机器学习算法进行正则化,但对于深度学习来说,最常见的一种方法是使用dropout层。这个想法是由 Hinton 等人在 2012 年提出的⁷,并在 2014 年由 Srivastava 等人在一篇论文中提出⁸

Dropout 层非常简单。在训练期间,每个 dropout 层从前一层中选择一组随机单元,并将它们的输出设置为 0,如图 2-15 所示。

令人难以置信的是,这个简单的添加通过确保网络不会过度依赖某些单元或单元组而大大减少了过拟合,这些单元或单元组实际上只是记住了训练集中的观察结果。如果我们使用 dropout 层,网络就不能太依赖任何一个单元,因此知识更均匀地分布在整个网络中。

图 2-15. 一个 dropout 层

这使得模型在泛化到未见过的数据时更加出色,因为网络已经经过训练,即使在由于丢弃随机单元引起的陌生条件下,也能产生准确的预测。在 dropout 层内没有需要学习的权重,因为要丢弃的单元是随机决定的。在预测时,dropout 层不会丢弃任何单元,因此整个网络用于进行预测。

Dropout 类比

回到我们的类比,这有点像数学学生练习过去试卷,其中随机选择了公式书中缺失的关键公式。通过这种方式,他们学会了通过对核心原则的理解来回答问题,而不是总是在书中相同的地方查找公式。当考试时,他们会发现更容易回答以前从未见过的问题,因为他们能够超越训练材料进行泛化。

Keras 中的Dropout层实现了这种功能,rate参数指定了要从前一层中丢弃的单元的比例,如示例 2-15 所示。

示例 2-15. Keras 中的Dropout
from tensorflow.keras import layers
layers.Dropout(rate = 0.25)

由于密集层的权重数量较高,最容易过拟合,因此通常在密集层之后使用 Dropout 层,尽管也可以在卷积层之后使用。

提示

批量归一化也被证明可以减少过拟合,因此许多现代深度学习架构根本不使用 dropout,完全依赖批量归一化进行正则化。与大多数深度学习原则一样,在每种情况下都没有适用的黄金法则,唯一确定最佳方法的方式是测试不同的架构,看看哪种在保留数据集上表现最好。

构建 CNN

您现在已经看到了三种新的 Keras 层类型:Conv2DBatchNormalizationDropout。让我们将这些部分组合成一个 CNN 模型,并看看它在 CIFAR-10 数据集上的表现。

运行此示例的代码

您可以在书籍存储库中名为notebooks/02_deeplearning/02_cnn/cnn.ipynb的 Jupyter 笔记本中运行以下示例。

我们将测试的模型架构显示在示例 2-16 中。

示例 2-16. 使用 Keras 构建 CNN 模型的代码
from tensorflow.keras import layers, models

input_layer = layers.Input((32,32,3))

x = layers.Conv2D(filters = 32, kernel_size = 3
	, strides = 1, padding = 'same')(input_layer)
x = layers.BatchNormalization()(x)
x = layers.LeakyReLU()(x)

x = layers.Conv2D(filters = 32, kernel_size = 3, strides = 2, padding = 'same')(x)
x = layers.BatchNormalization()(x)
x = layers.LeakyReLU()(x)

x = layers.Conv2D(filters = 64, kernel_size = 3, strides = 1, padding = 'same')(x)
x = layers.BatchNormalization()(x)
x = layers.LeakyReLU()(x)

x = layers.Conv2D(filters = 64, kernel_size = 3, strides = 2, padding = 'same')(x)
x = layers.BatchNormalization()(x)
x = layers.LeakyReLU()(x)

x = layers.Flatten()(x)

x = layers.Dense(128)(x)
x = layers.BatchNormalization()(x)
x = layers.LeakyReLU()(x)
x = layers.Dropout(rate = 0.5)(x)

output_layer = layers.Dense(10, activation = 'softmax')(x)

model = models.Model(input_layer, output_layer)

我们使用四个堆叠的Conv2D层,每个后面跟一个BatchNormalization和一个LeakyReLU层。在展平结果张量后,我们通过一个大小为 128 的Dense层,再次跟一个BatchNormalization和一个LeakyReLU层。紧接着是一个用于正则化的Dropout层,网络最后是一个大小为 10 的输出Dense层。

提示

使用批量归一化和激活层的顺序是个人偏好的问题。通常情况下,批量归一化层放在激活层之前,但一些成功的架构会反过来使用这些层。如果选择在激活之前使用批量归一化,可以使用缩写 BAD(批量归一化,激活,然后是 dropout)来记住顺序!

模型摘要显示在表 2-3 中。

表 2-3. CIFAR-10 的 CNN 模型摘要

层(类型) 输出形状 参数 #
InputLayer (None, 32, 32, 3) 0
Conv2D (None, 32, 32, 32) 896
BatchNormalization (None, 32, 32, 32) 128
LeakyReLU (None, 32, 32, 32) 0
Conv2D (None, 16, 16, 32) 9,248
BatchNormalization (None, 16, 16, 32) 128
LeakyReLU (None, 16, 16, 32) 0
Conv2D (None, 16, 16, 64) 18,496
BatchNormalization (None, 16, 16, 64) 256
LeakyReLU (None, 16, 16, 64) 0
Conv2D (None, 8, 8, 64) 36,928
BatchNormalization (None, 8, 8, 64) 256
LeakyReLU (None, 8, 8, 64) 0
Flatten (None, 4096) 0
Dense (None, 128) 524,416
BatchNormalization (None, 128) 512
LeakyReLU (None, 128) 0
Dropout (None, 128) 0
Dense (None, 10) 1290
总参数 592,554
可训练参数 591,914
不可训练参数 640
提示

在继续之前,请确保您能够手工计算每一层的输出形状和参数数量。这是一个很好的练习,可以证明您已经完全理解了每一层是如何构建的,以及它是如何与前一层连接的!不要忘记包括作为Conv2DDense层的一部分包含的偏置权重。

训练和评估 CNN

我们编译和训练模型的方式与之前完全相同,并调用evaluate方法来确定其在留存集上的准确率(图 2-16)。

图 2-16。CNN 性能

正如您所看到的,这个模型现在的准确率达到了 71.5%,比之前的 49.0%有所提高。好多了!图 2-17 展示了我们新卷积模型的一些预测。

通过简单地改变模型的架构来包括卷积、批量归一化和丢弃层,已经实现了这一改进。请注意,我们新模型中的参数数量实际上比之前的模型更少,尽管层数要多得多。这表明了对模型设计进行实验和熟悉不同层类型如何利用优势的重要性。在构建生成模型时,更加重要的是要理解模型的内部工作原理,因为您最感兴趣的是网络的中间层,这些层捕捉了高级特征。

图 2-17。CNN 预测

总结

本章介绍了构建深度生成模型所需的核心深度学习概念。我们首先使用 Keras 构建了一个多层感知器(MLP),并训练模型来预测来自 CIFAR-10 数据集的给定图像的类别。然后,我们通过引入卷积、批量归一化和丢弃层来改进这个架构,创建了一个卷积神经网络(CNN)。

从本章中需要牢记的一个非常重要的观点是,深度神经网络在设计上是完全灵活的,在模型架构方面实际上没有固定的规则。有指导方针和最佳实践,但您应该随意尝试不同层和它们出现的顺序。不要感到受限于仅使用您在本书或其他地方阅读过的架构!就像一个拥有一套积木的孩子一样,您的神经网络的设计仅受您自己想象力的限制。

在下一章中,我们将看到如何使用这些积木来设计一个可以生成图像的网络。

¹ Kaiming He 等人,“用于图像识别的深度残差学习”,2015 年 12 月 10 日,https://arxiv.org/abs/1512.03385

² Alex Krizhevsky,“从微小图像中学习多层特征”,2009 年 4 月 8 日,https://www.cs.toronto.edu/~kriz/learning-features-2009-TR.pdf

³ Diederik Kingma 和 Jimmy Ba,“Adam:一种随机优化方法”,2014 年 12 月 22 日,https://arxiv.org/abs/1412.6980v8

⁴ Samuel L. Smith 等人,“不要降低学习率,增加批量大小”,2017 年 11 月 1 日,https://arxiv.org/abs/1711.00489

⁵ Vincent Dumoulin 和 Francesco Visin,“深度学习卷积算术指南”,2018 年 1 月 12 日,https://arxiv.org/abs/1603.07285

⁶ Sergey Ioffe 和 Christian Szegedy,“批量归一化:通过减少内部协变量转移加速深度网络训练”,2015 年 2 月 11 日,https://arxiv.org/abs/1502.03167

⁷ Hinton 等人,“通过防止特征探测器的共适应来构建网络”,2012 年 7 月 3 日,https://arxiv.org/abs/1207.0580

⁸ Nitish Srivastava 等人,“Dropout:防止神经网络过拟合的简单方法”,机器学习研究杂志 15 (2014): 1929–1958,http://jmlr.org/papers/volume15/srivastava14a/srivastava14a.pdf

第二部分:方法

在第二部分中,我们将深入探讨六种生成模型系列,包括它们工作原理的理论以及如何构建每种类型模型的实际示例。

在第三章中,我们将看一下我们的第一个生成式深度学习模型,变分自动编码器。这种技术不仅可以让我们生成逼真的人脸,还可以改变现有的图像,例如添加微笑或改变某人头发的颜色。

第四章探讨了近年来最成功的生成建模技术之一,生成对抗网络。我们将看到 GAN 训练如何被微调和调整,以不断推动生成建模能够实现的边界。

在第五章中,我们将深入探讨几个自回归模型的例子,包括 LSTMs 和 PixelCNN。这个模型系列将生成过程视为一个序列预测问题,它支撑着今天最先进的文本生成模型,并且也可以用于图像生成。

在第六章中,我们将涵盖归一化流模型系列,包括 RealNVP。这个模型基于变量变换公式,允许将简单分布(如高斯分布)转换为更复杂的分布,同时保持可处理性。

第七章介绍了基于能量的模型系列。这些模型训练一个标量能量函数来评分给定输入的有效性。我们将探讨一种用于训练基于能量模型的技术,称为对比散度,以及一种用于采样新观测的技术,称为 Langevin 动力学。

最后,在第八章中,我们将探索扩散模型系列。这种技术基于迭代地向图像添加噪声,然后训练模型去除噪声,使我们能够将纯噪声转换为逼真的样本。

到第二部分结束时,您将从每个生成建模系列中构建实际示例,并能够从理论角度解释每种模型的工作原理。

第三章:变分自动编码器

2013 年,Diederik P. Kingma 和 Max Welling 发表了一篇论文,奠定了一种称为变分自动编码器(VAE)的神经网络类型的基础。¹ 这现在是最基本和最知名的深度学习架构之一,用于生成建模,也是我们进入生成式深度学习旅程的绝佳起点。

在本章中,我们将首先构建一个标准的自动编码器,然后看看如何扩展这个框架以开发一个变分自动编码器。在这个过程中,我们将分析这两种模型,以了解它们在细粒度级别上的工作原理。通过本章的结束,您应该完全了解如何构建和操作基于自动编码器的模型,特别是如何从头开始构建一个变分自动编码器,以根据您自己的数据集生成图像。

介绍

让我们从一个简单的故事开始,这将有助于解释自动编码器试图解决的基本问题。

现在让我们探讨这个故事如何与构建自动编码器相关。

自动编码器

故事描述的过程的图表显示在图 3-2 中。您扮演编码器的角色,将每件服装移动到衣柜中的一个位置。这个过程称为编码。Brian 扮演解码器的角色,接受衣柜中的一个位置,并尝试重新创建该项目。这个过程称为解码

图 3-2. 无限衣柜中的服装项目-每个黑点代表一个服装项目

衣柜中的每个位置由两个数字表示(即一个 2D 向量)。例如,图 3-2 中的裤子被编码为点[6.3,-0.9]。这个向量也被称为嵌入,因为编码器试图将尽可能多的信息嵌入其中,以便解码器可以产生准确的重建。

自动编码器只是一个经过训练的神经网络,用于执行编码和解码项目的任务,使得这个过程的输出尽可能接近原始项目。关键是,它可以用作生成模型,因为我们可以解码 2D 空间中的任何点(特别是那些不是原始项目的嵌入)以生成新的服装项目。

现在让我们看看如何使用 Keras 构建自动编码器并将其应用于真实数据集!

运行此示例的代码

此示例的代码可以在位于书籍存储库中的 Jupyter 笔记本notebooks/03_vae/01_autoencoder/autoencoder.ipynb中找到。

Fashion-MNIST 数据集

在这个例子中,我们将使用Fashion-MNIST 数据集-一个由 28×28 像素大小的服装项目的灰度图像组成的集合。数据集中的一些示例图像显示在图 3-3 中。

图 3-3. Fashion-MNIST 数据集中的图像示例

数据集已经预先打包到 TensorFlow 中,因此可以按照示例 3-1 中所示进行下载。

示例 3-1. 加载 Fashion-MNIST 数据集
from tensorflow.keras import datasets
(x_train,y_train), (x_test,y_test) = datasets.fashion_mnist.load_data()

这些是 28×28 的灰度图像(像素值在 0 到 255 之间),我们需要对其进行预处理,以确保像素值在 0 到 1 之间。我们还将每个图像填充到 32×32,以便更容易地处理通过网络的张量形状,如示例 3-2 中所示。

示例 3-2. 数据预处理
def preprocess(imgs):
    imgs = imgs.astype("float32") / 255.0
    imgs = np.pad(imgs, ((0, 0), (2, 2), (2, 2)), constant_values=0.0)
    imgs = np.expand_dims(imgs, -1)
    return imgs

x_train = preprocess(x_train)
x_test = preprocess(x_test)

接下来,我们需要了解自动编码器的整体结构,以便我们可以使用 TensorFlow 和 Keras 对其进行编码。

自动编码器架构

自动编码器是由两部分组成的神经网络:

  • 一个编码器网络,将高维输入数据(如图像)压缩成较低维度的嵌入向量

  • 一个解码器网络,将给定的嵌入向量解压缩回原始域(例如,回到图像)

网络架构图显示在图 3-4 中。输入图像被编码为潜在嵌入向量z,然后解码回原始像素空间。

图 3-4。自动编码器架构图

自动编码器经过编码器和解码器后被训练重建图像。这一开始可能看起来很奇怪——为什么要重建一组已经可用的图像?然而,正如我们将看到的,自动编码器中有趣的部分是嵌入空间(也称为潜在空间),因为从这个空间中取样将允许我们生成新的图像。

首先定义一下我们所说的嵌入。嵌入(z)是原始图像压缩到较低维度潜在空间中。这个想法是通过选择潜在空间中的任意点,我们可以通过解码器生成新颖的图像,因为解码器已经学会如何将潜在空间中的点转换为可行的图像。

在我们的示例中,我们将图像嵌入到一个二维潜在空间中。这将帮助我们可视化潜在空间,因为我们可以轻松在 2D 中绘制点。实际上,自动编码器的潜在空间通常会有超过两个维度,以便更自由地捕获图像中更多的细微差别。

自动编码器作为去噪模型

自动编码器可以用于清理嘈杂的图像,因为编码器学习到捕获潜在空间中随机噪声的位置并不有用,以便重建原始图像。对于这样的任务,一个二维潜在空间可能太小,无法从输入中编码足够的相关信息。然而,正如我们将看到的,如果我们想将自动编码器用作生成模型,增加潜在空间的维度会很快导致问题。

现在让我们看看如何构建编码器和解码器。

编码器

在自动编码器中,编码器的任务是将输入图像映射到潜在空间中的嵌入向量。我们将构建的编码器的架构显示在表 3-1 中。

表 3-1。编码器的模型摘要

层(类型) 输出形状 参数 #
InputLayer (None,32,32,1) 0
Conv2D (None,16,16,32) 320
Conv2D (None,8,8,64) 18,496
Conv2D (None,4,4,128) 73,856
Flatten (None,2048) 0
Dense (None,2) 4,098
总参数 96,770
可训练参数 96,770
不可训练参数 0

为了实现这一点,我们首先为图像创建一个“输入”层,并依次通过三个Conv2D层,每个层捕获越来越高级的特征。我们使用步幅为 2,以减半每个层的输出大小,同时增加通道数。最后一个卷积层被展平,并连接到大小为 2 的Dense层,代表我们的二维潜在空间。

示例 3-3 展示了如何在 Keras 中构建这个模型。

示例 3-3。编码器
encoder_input = layers.Input(
    shape=(32, 32, 1), name = "encoder_input"
) # ①
x = layers.Conv2D(32, (3, 3), strides = 2, activation = 'relu', padding="same")(
    encoder_input
) # ②
x = layers.Conv2D(64, (3, 3), strides = 2, activation = 'relu', padding="same")(x)
x = layers.Conv2D(128, (3, 3), strides = 2, activation = 'relu', padding="same")(x)
shape_before_flattening = K.int_shape(x)[1:]

x = layers.Flatten()(x) # ③
encoder_output = layers.Dense(2, name="encoder_output")(x) # ④

encoder = models.Model(encoder_input, encoder_output) # ⑤

定义编码器(图像)的“输入”层。

顺序堆叠Conv2D层。

将最后一个卷积层展平为一个向量。

将这个向量连接到 2D 嵌入中的Dense层。

定义编码器的 KerasModel——一个将输入图像并将其编码为 2D 嵌入的模型。

提示

我强烈建议您尝试不同数量的卷积层和滤波器,以了解架构如何影响模型参数的总数、模型性能和模型运行时间。

解码器

解码器是编码器的镜像——我们使用卷积转置层,而不是卷积层,如表 3-2 所示。

表 3-2. 解码器的模型摘要

层(类型) 输出形状 参数 #
InputLayer (None, 2) 0
Dense (None, 2048) 6,144
重塑 (None, 4, 4, 128) 0
Conv2DTranspose (None, 8, 8, 128) 147,584
Conv2DTranspose (None, 16, 16, 64) 73,792
Conv2DTranspose (None, 32, 32, 32) 18,464
Conv2D (None, 32, 32, 1) 289
总参数 246,273
可训练参数 246,273
不可训练参数 0

示例 3-4 展示了我们如何在 Keras 中构建解码器。

示例 3-4. 解码器
decoder_input = layers.Input(shape=(2,), name="decoder_input") # ①
x = layers.Dense(np.prod(shape_before_flattening))(decoder_input) # ②
x = layers.Reshape(shape_before_flattening)(x) # ③
x = layers.Conv2DTranspose(
    128, (3, 3), strides=2, activation = 'relu', padding="same"
)(x) # ④
x = layers.Conv2DTranspose(
    64, (3, 3), strides=2, activation = 'relu', padding="same"
)(x)
x = layers.Conv2DTranspose(
    32, (3, 3), strides=2, activation = 'relu', padding="same"
)(x)
decoder_output = layers.Conv2D(
    1,
    (3, 3),
    strides = 1,
    activation="sigmoid",
    padding="same",
    name="decoder_output"
)(x)

decoder = models.Model(decoder_input, decoder_output) # ⑤

定义解码器(嵌入)的Input层。

将输入连接到Dense层。

将这个向量重塑成一个张量,可以作为输入传递到第一个Conv2DTranspose层。

Conv2DTranspose层堆叠在一起。

定义解码器的 Keras Model——一个模型,将潜在空间中的嵌入解码为原始图像域。

将编码器连接到解码器

为了同时训练编码器和解码器,我们需要定义一个模型,表示图像通过编码器流动并通过解码器返回。幸运的是,Keras 使这变得非常容易,如示例 3-5 所示。请注意我们如何指定自编码器的输出只是经过解码器传递后的编码器的输出的方式。

示例 3-5. 完整自编码器
autoencoder = Model(encoder_input, decoder(encoder_output)) # ①

定义完整自编码器的 Keras Model——一个模型,将图像通过编码器传递并通过解码器返回,生成原始图像的重建。

现在我们已经定义了我们的模型,我们只需要使用损失函数和优化器对其进行编译,如示例 3-6 所示。损失函数通常选择为原始图像的每个像素之间的均方根误差(RMSE)或二进制交叉熵。

示例 3-6. 编译自编码器
# Compile the autoencoder
autoencoder.compile(optimizer="adam", loss="binary_crossentropy")

现在我们可以通过将输入图像作为输入和输出来训练自编码器,如示例 3-7 所示。

示例 3-7. 训练自编码器
autoencoder.fit(
    x_train,
    x_train,
    epochs=5,
    batch_size=100,
    shuffle=True,
    validation_data=(x_test, x_test),
)

现在我们的自编码器已经训练好了,我们需要检查的第一件事是它是否能够准确重建输入图像。

重建图像

我们可以通过将测试集中的图像通过自编码器并将输出与原始图像进行比较来测试重建图像的能力。这个代码在示例 3-8 中展示。

示例 3-8. 使用自编码器重建图像
example_images = x_test[:5000]
predictions = autoencoder.predict(example_images)

在图 3-6 中,您可以看到一些原始图像的示例(顶部行),编码后的 2D 向量,以及解码后的重建物品(底部行)。

图 3-6. 服装项目的编码和解码示例

注意重建并不完美——解码过程中仍然有一些原始图像的细节没有被捕捉到,比如标志。这是因为将每个图像减少到只有两个数字,自然会丢失一些信息。

现在让我们来研究编码器如何在潜在空间中表示图像。

可视化潜在空间

我们可以通过将测试集通过编码器并绘制结果嵌入来可视化图像如何嵌入到潜在空间中,如示例 3-9 所示。

示例 3-9。使用编码器嵌入图像
embeddings = encoder.predict(example_images)

plt.figure(figsize=(8, 8))
plt.scatter(embeddings[:, 0], embeddings[:, 1], c="black", alpha=0.5, s=3)
plt.show()

结果绘图是图 3-2 中显示的散点图-每个黑点代表一个被嵌入到潜在空间中的图像。

为了更好地理解这个潜在空间的结构,我们可以利用 Fashion-MNIST 数据集中附带的标签,描述每个图像中的物品类型。总共有 10 组,如表 3-3 所示。

表 3-3。时尚 MNIST 标签

ID 服装标签
0 T 恤/上衣
1 裤子
2 套头衫
3 连衣裙
4 外套
5 凉鞋
6 衬衫
7 运动鞋
8
9 短靴

我们可以根据相应图像的标签对每个点进行着色,以生成图 3-7 中的绘图。现在结构变得非常清晰!尽管在训练期间模型从未展示过服装标签,但自动编码器自然地将外观相似的项目分组到潜在空间的相同部分。例如,潜在空间右下角的深蓝色点云都是不同的裤子图像,中心附近的红色点云都是短靴。

图 3-7。潜在空间的绘图,按服装标签着色

生成新图像

我们可以通过在潜在空间中抽样一些点并使用解码器将其转换回像素空间来生成新图像,如示例 3-10 所示。

示例 3-10。使用解码器生成新图像
mins, maxs = np.min(embeddings, axis=0), np.max(embeddings, axis=0)
sample = np.random.uniform(mins, maxs, size=(18, 2))
reconstructions = decoder.predict(sample)

一些生成的图像示例显示在图 3-8 中,以及它们在潜在空间中的嵌入。

图 3-8。生成的服装项目

每个蓝点映射到图表右侧显示的图像之一,下面显示嵌入向量。注意一些生成的项目比其他项目更真实。为什么呢?

为了回答这个问题,让我们首先观察一下潜在空间中点的整体分布,参考图 3-7:

  • 一些服装项目在一个非常小的区域内表示,而其他服装项目在一个更大的区域内表示。

  • 分布关于点(0, 0)不对称,也不是有界的。例如,具有正 y 轴值的点比负值更多,甚至有些点甚至延伸到 y 轴值> 8。

  • 颜色之间有很大的间隙,包含很少的点。

这些观察实际上使得从潜在空间中抽样变得非常具有挑战性。如果我们将解码点的图像叠加在网格上的潜在空间上,如图 3-9 所示,我们可以开始理解为什么解码器可能不总是生成令人满意的图像。

图 3-9。解码嵌入的网格,与数据集中原始图像的嵌入叠加,按项目类型着色

首先,我们可以看到,如果我们在我们定义的有界空间中均匀选择点,我们更有可能抽样出看起来像包(ID 8)而不是短靴(ID 9)的东西,因为为包(橙色)划定的潜在空间部分比短靴区域(红色)更大。

其次,我们应该如何选择潜在空间中的随机点并不明显,因为这些点的分布是未定义的。从技术上讲,我们可以选择 2D 平面上的任意点!甚至不能保证点会围绕(0, 0)中心。这使得从我们的潜在空间中抽样成为问题。

最后,我们可以看到潜在空间中存在一些空洞,原始图像没有被编码。例如,在域的边缘有大片白色空间——自动编码器没有理由确保这些点被解码为可识别的服装项目,因为训练集中很少有图像被编码到这里。

即使中心点也可能无法解码为形式良好的图像。这是因为自动编码器没有被迫确保空间是连续的。例如,即使点(-1,-1)可能被解码为一个令人满意的凉鞋图像,但没有机制来确保点(-1.1,-1.1)也产生一个令人满意的凉鞋图像。

在二维中,这个问题是微妙的;自动编码器只有少量维度可用,因此自然地必须将服装组合在一起,导致服装组之间的空间相对较小。然而,当我们开始在潜在空间中使用更多维度来生成更复杂的图像,如面孔时,这个问题变得更加明显。如果我们让自动编码器自由使用潜在空间来编码图像,那么相似点之间将会有巨大的间隙,而没有动机使之间的空间生成形式良好的图像。

为了解决这三个问题,我们需要将我们的自动编码器转换为变分自动编码器

变分自动编码器

为了解释,让我们重新审视无限衣柜并做一些改变...​

现在让我们尝试理解如何将我们的自动编码器模型转换为变分自动编码器,从而使其成为一个更复杂的生成模型。

我们需要更改的两个部分是编码器和损失函数。

编码器

在自动编码器中,每个图像直接映射到潜在空间中的一个点。在变分自动编码器中,每个图像实际上被映射到潜在空间中某一点周围的多变量正态分布,如图 3-10 所示。

图 3-10。自动编码器和变分自动编码器中编码器的区别

编码器只需要将每个输入映射到一个均值向量和一个方差向量,不需要担心潜在空间维度之间的协方差。变分自动编码器假设潜在空间中的维度之间没有相关性。

方差值始终为正,因此我们实际上选择将其映射到方差的对数,因为这可以取任何实数范围内的值( - , )。这样我们可以使用神经网络作为编码器,将输入图像映射到均值和对数方差向量。

总之,编码器将每个输入图像编码为两个向量,这两个向量一起定义了潜在空间中的多变量正态分布:

z_mean

分布的均值点

z_log_var

每个维度的方差的对数

我们可以使用以下方程从这些值定义的分布中采样一个点z

z = z_mean + z_sigma * epsilon

其中:

z_sigma = exp(z_log_var * 0.5)
epsilon ~ N(0,I)
提示

z_sigmaσ )和z_log_varlog ( σ 2 ) )之间的关系推导如下:

σ = exp ( log ( σ ) ) = exp ( 2 log ( σ ) / 2 ) = exp ( log ( σ 2 ) / 2 )

变分自动编码器的解码器与普通自动编码器的解码器相同,给出了图 3-12 所示的整体架构。

图 3-12。VAE 架构图

为什么对编码器进行这种小改变有帮助?

先前,我们看到潜在空间不需要连续——即使点(-2, 2)解码为一个良好形成的凉鞋图像,也没有要求(-2.1, 2.1)看起来相似。现在,由于我们从z_mean周围的区域对随机点进行采样,解码器必须确保同一邻域中的所有点在解码时产生非常相似的图像,以便重构损失保持较小。这是一个非常好的特性,确保即使我们选择一个解码器从未见过的潜在空间中的点,它也可能解码为一个良好形成的图像。

构建 VAE 编码器

现在让我们看看如何在 Keras 中构建这个编码器的新版本。

运行此示例的代码

此示例的代码可以在位于书籍存储库中的notebooks/03_vae/02_vae_fashion/vae_fashion.ipynb的 Jupyter 笔记本中找到。

该代码已经改编自由 Francois Chollet 创建的优秀VAE 教程,可在 Keras 网站上找到。

首先,我们需要创建一个新类型的Sampling层,这将允许我们从由z_meanz_log_var定义的分布中进行采样,如示例 3-11 所示。

示例 3-11. Sampling
class Sampling(layers.Layer): # ①
    def call(self, inputs):
        z_mean, z_log_var = inputs
        batch = tf.shape(z_mean)[0]
        dim = tf.shape(z_mean)[1]
        epsilon = K.random_normal(shape=(batch, dim))
        return z_mean + tf.exp(0.5 * z_log_var) * epsilon # ②

我们通过对 Keras 基础Layer类进行子类化来创建一个新层(请参阅“子类化 Layer 类”侧边栏)。

我们使用重参数化技巧(请参阅“重参数化技巧”侧边栏)来构建由z_meanz_log_var参数化的正态分布的样本。

包括新的Sampling层在内的编码器的完整代码显示在示例 3-12 中。

示例 3-12. 编码器
encoder_input = layers.Input(
    shape=(32, 32, 1), name="encoder_input"
)
x = layers.Conv2D(32, (3, 3), strides=2, activation="relu", padding="same")(
    encoder_input
)
x = layers.Conv2D(64, (3, 3), strides=2, activation="relu", padding="same")(x)
x = layers.Conv2D(128, (3, 3), strides=2, activation="relu", padding="same")(x)
shape_before_flattening = K.int_shape(x)[1:]

x = layers.Flatten()(x)
z_mean = layers.Dense(2, name="z_mean")(x) # ①
z_log_var = layers.Dense(2, name="z_log_var")(x)
z = Sampling()([z_mean, z_log_var]) # ②

encoder = models.Model(encoder_input, [z_mean, z_log_var, z], name="encoder") # ③

我们将Flatten层直接连接到 2D 潜在空间,而不是直接连接到z_meanz_log_var层。

Sampling层从由参数z_meanz_log_var定义的正态分布中对潜在空间中的点z进行采样。

定义编码器的 Keras Model——一个接受输入图像并输出z_meanz_log_var和由这些参数定义的正态分布中的采样点z的模型。

编码器的摘要显示在表 3-4 中。

表 3-4. VAE 编码器的模型摘要

Layer (type) 输出形状 参数 # 连接到
InputLayer (input) (None, 32, 32, 1) 0 []
Conv2D (conv2d_1) (None, 16, 16, 32) 320 [input]
Conv2D (conv2d_2) (None, 8, 8, 64) 18,496 [conv2d_1]
Conv2D (conv2d_3) (None, 4, 4, 128) 73,856 [conv2d_2]
Flatten (flatten) (None, 2048) 0 [conv2d_3]
Dense (z_mean) (None, 2) 4,098 [flatten]
Dense (z_log_var) (None, 2) 4,098 [flatten]
Sampling (z) (None, 2) 0 [z_mean, z_log_var]
总参数 100,868
可训练参数 100,868
不可训练参数 0

我们需要更改的原始自动编码器的唯一其他部分是损失函数。

损失函数

先前,我们的损失函数仅包括图像与通过编码器和解码器传递后的尝试副本之间的重构损失。重构损失也出现在变分自动编码器中,但现在我们需要一个额外的组件:Kullback-Leibler(KL)散度项。

KL 散度是衡量一个概率分布与另一个之间差异的一种方式。在 VAE 中,我们想要衡量我们的具有参数z_meanz_log_var的正态分布与标准正态分布之间的差异。在这种特殊情况下,可以证明 KL 散度具有以下封闭形式:

kl_loss = -0.5 * sum(1 + z_log_var - z_mean ^ 2 - exp(z_log_var))

或者用数学符号表示:

D KL [ N ( μ , σ N ( 0 , 1 ) ] = - 1 2 ( 1 + l o g ( σ 2 ) - μ 2 - σ 2 )

总和取自潜在空间中的所有维度。当所有维度上的z_mean = 0z_log_var = 0时,kl_loss被最小化为 0。当这两个项开始与 0 不同,kl_loss增加。

总的来说,KL 散度项惩罚网络将观测编码为与标准正态分布的参数明显不同的z_meanz_log_var变量,即z_mean = 0z_log_var = 0

为什么将这个损失函数的添加有助于什么?

首先,我们现在有一个明确定义的分布,可以用于选择潜在空间中的点——标准正态分布。其次,由于这个项试图将所有编码分布推向标准正态分布,因此大型间隙形成的机会较小。相反,编码器将尝试对称且高效地使用原点周围的空间。

在原始 VAE 论文中,VAE 的损失函数简单地是重建损失和 KL 散度损失项的加法。这个变体(β-VAE)包括一个因子,用于对 KL 散度进行加权,以确保它与重建损失平衡良好。如果我们过分权重重建损失,KL 损失将不会产生所需的调节效果,我们将看到与普通自动编码器相同的问题。如果 KL 散度项权重过大,KL 散度损失将占主导地位,重建图像将很差。在训练 VAE 时,这个权重项是需要调整的参数之一。

训练变分自动编码器

示例 3-13 展示了我们如何将整体 VAE 模型构建为抽象 Keras Model类的子类。这使我们能够在自定义的train_step方法中包含损失函数的 KL 散度项的计算。

示例 3-13。训练 VAE
class VAE(models.Model):
    def __init__(self, encoder, decoder, **kwargs):
        super(VAE, self).__init__(**kwargs)
        self.encoder = encoder
        self.decoder = decoder
        self.total_loss_tracker = metrics.Mean(name="total_loss")
        self.reconstruction_loss_tracker = metrics.Mean(
            name="reconstruction_loss"
        )
        self.kl_loss_tracker = metrics.Mean(name="kl_loss")

    @property
    def metrics(self):
        return [
            self.total_loss_tracker,
            self.reconstruction_loss_tracker,
            self.kl_loss_tracker,
        ]

    def call(self, inputs): # ①
        z_mean, z_log_var, z = encoder(inputs)
        reconstruction = decoder(z)
        return z_mean, z_log_var, reconstruction

    def train_step(self, data): # ②
        with tf.GradientTape() as tape:
            z_mean, z_log_var, reconstruction = self(data)
            reconstruction_loss = tf.reduce_mean(
                500
                * losses.binary_crossentropy(
                    data, reconstruction, axis=(1, 2, 3)
                )
            ) # ③
            kl_loss = tf.reduce_mean(
                tf.reduce_sum(
                    -0.5
                    * (1 + z_log_var - tf.square(z_mean) - tf.exp(z_log_var)),
                    axis = 1,
                )
            )
            total_loss = reconstruction_loss + kl_loss # ④

        grads = tape.gradient(total_loss, self.trainable_weights)
        self.optimizer.apply_gradients(zip(grads, self.trainable_weights))

        self.total_loss_tracker.update_state(total_loss)
        self.reconstruction_loss_tracker.update_state(reconstruction_loss)
        self.kl_loss_tracker.update_state(kl_loss)

        return {m.name: m.result() for m in self.metrics}

vae = VAE(encoder, decoder)
vae.compile(optimizer="adam")
vae.fit(
    train,
    epochs=5,
    batch_size=100
)

这个函数描述了我们希望在特定输入图像上返回的内容,我们称之为 VAE。

这个函数描述了 VAE 的一个训练步骤,包括损失函数的计算。

重建损失中使用了一个 beta 值为 500。

总损失是重建损失和 KL 散度损失的总和。

梯度带

TensorFlow 的梯度带是一种机制,允许在模型前向传递期间计算操作的梯度。要使用它,您需要将执行您想要区分的操作的代码包装在tf.GradientTape()上下文中。一旦记录了操作,您可以通过调用tape.gradient()计算损失函数相对于某些变量的梯度。然后可以使用这些梯度来更新变量与优化器。

这个机制对于计算自定义损失函数的梯度(就像我们在这里所做的那样)以及创建自定义训练循环非常有用,正如我们将在第四章中看到的。

变分自动编码器的分析

现在我们已经训练了我们的 VAE,我们可以使用编码器对测试集中的图像进行编码,并在潜在空间中绘制z_mean值。我们还可以从标准正态分布中进行采样,生成潜在空间中的点,并使用解码器将这些点解码回像素空间,以查看 VAE 的性能如何。

图 3-13 展示了新潜在空间的结构,以及一些采样点和它们的解码图像。我们可以立即看到潜在空间的组织方式发生了几处变化。

图 3-13。新的潜在空间:黑点显示每个编码图像的z_mean值,而蓝点显示潜在空间中的一些采样点(其解码图像显示在右侧)

首先,KL 散度损失项确保编码图像的z_meanz_log_var值永远不会偏离标准正态分布太远。其次,由于编码器现在是随机的而不是确定性的,因此现在的潜在空间更加连续,因此没有那么多形状不佳的图像。

最后,通过按服装类型对潜在空间中的点进行着色(图 3-14),我们可以看到没有任何一种类型受到优待。右侧的图显示了空间转换为p值——我们可以看到每种颜色大致上都有相同的表示。再次强调,重要的是要记住在训练过程中根本没有使用标签;VAE 已经自己学会了各种服装形式,以帮助最小化重构损失。

图 3-14。VAE 的潜在空间按服装类型着色

探索潜在空间

到目前为止,我们对自动编码器和变分自动编码器的所有工作都局限于具有两个维度的潜在空间。这帮助我们在页面上可视化 VAE 的内部工作原理,并理解我们对自动编码器架构所做的小调整是如何将其转变为一种更强大的网络类别,可用于生成建模。

现在让我们将注意力转向更复杂的数据集,并看看当我们增加潜在空间的维度时,变分自动编码器可以实现的惊人成就。

运行此示例的代码

此示例的代码可以在书籍存储库中的 Jupyter 笔记本中找到,位置为notebooks/03_vae/03_faces/vae_faces.ipynb

CelebA 数据集

我们将使用CelebFaces Attributes (CelebA)数据集来训练我们的下一个变分自动编码器。这是一个包含超过 200,000 张名人面孔彩色图像的集合,每张图像都带有各种标签(例如,戴帽子微笑等)。一些示例显示在图 3-15 中。

图 3-15。CelebA 数据集的一些示例(来源:Liu 等,2015

当我们开始探索这些特征如何在多维潜在空间中被捕获时,我们当然不需要标签来训练 VAE,但这些标签以后会很有用。一旦我们的 VAE 训练完成,我们就可以从潜在空间中进行采样,生成名人面孔的新示例。

CelebA 数据集也可以通过 Kaggle 获得,因此您可以通过在书籍存储库中运行 Kaggle 数据集下载脚本来下载数据集,如示例 3-14 所示。这将把图像和相关元数据保存到/data文件夹中。

示例 3-14。下载 CelebA 数据集
bash scripts/download_kaggle_data.sh jessicali9530 celeba-dataset

我们使用 Keras 函数image_dataset_from_directory来创建一个指向存储图像的目录的 TensorFlow 数据集,如示例 3-15 所示。这使我们能够在需要时(例如在训练期间)将图像批量读入内存,以便我们可以处理大型数据集,而不必担心将整个数据集装入内存。它还将图像调整大小为 64×64,在像素值之间进行插值。

示例 3-15。处理 CelebA 数据集
train_data = utils.image_dataset_from_directory(
    "/app/data/celeba-dataset/img_align_celeba/img_align_celeba",
    labels=None,
    color_mode="rgb",
    image_size=(64, 64),
    batch_size=128,
    shuffle=True,
    seed=42,
    interpolation="bilinear",
)

原始数据在范围[0, 255]内进行缩放以表示像素强度,我们将其重新缩放到范围[0, 1],如示例 3-16 所示。

示例 3-16。处理 CelebA 数据集

训练变分自动编码器

面部模型的网络架构与 Fashion-MNIST 示例类似,有一些细微差异:

  • 我们的数据现在有三个输入通道(RGB),而不是一个(灰度)。这意味着我们需要将解码器的最后一个卷积转置层中的通道数更改为 3。

  • 我们将使用一个具有 200 维而不是 2 维的潜在空间。由于面孔比时尚 MNIST 图像复杂得多,我们增加了潜在空间的维度,以便网络可以从图像中编码出令人满意的细节量。

  • 每个卷积层后都有批量归一化层以稳定训练。尽管每个批次运行时间较长,但达到相同损失所需的批次数量大大减少。

  • 我们将 KL 散度的β因子增加到 2,000。这是一个需要调整的参数;对于这个数据集和架构,这个值被发现可以产生良好的结果。

编码器和解码器的完整架构分别显示在表 3-5 和表 3-6 中。

表 3-5。VAE 面部编码器的模型摘要

层(类型) 输出形状 参数 # 连接到
输入层(input) (None, 32, 32, 3) 0 []
卷积层(conv2d_1) (None, 16, 16, 128) 3,584 [input]
批量归一化(bn_1) (None, 16, 16, 128) 512 [conv2d_1]
LeakyReLU(lr_1) (None, 16, 16, 128) 0 [bn_1]
卷积层(conv2d_2) (None, 8, 8, 128) 147,584 [lr_1]
批量归一化(bn_2) (None, 8, 8, 128) 512 [conv2d_2]
LeakyReLU(lr_2) (None, 8, 8, 128) 0 [bn_2]
卷积层(conv2d_3) (None, 4, 4, 128) 147,584 [lr_2]
批量归一化(bn_3) (None, 4, 4, 128) 512 [conv2d_3]
LeakyReLU(lr_3) (None, 4, 4, 128) 0 [bn_3]
卷积层(conv2d_4) (None, 2, 2, 128) 147,584 [lr_3]
批量归一化(bn_4) (None, 2, 2, 128) 512 [conv2d_4]
LeakyReLU(lr_4) (None, 2, 2, 128) 0 [bn_4]
展平(flatten) (None, 512) 0 [lr_4]
密集层(z_mean) (None, 200) 102,600 [flatten]
密集层(z_log_var) (None, 200) 102,600 [flatten]
采样(z) (None, 200) 0 [z_mean, z_log_var]
总参数 653,584
可训练参数 652,560
不可训练参数 1,024

表 3-6。VAE 面部解码器的模型摘要

层(类型) 输出形状 参数 #
输入层 (None, 200) 0
密集层 (None, 512) 102,912
批量归一化 (None, 512) 2,048
LeakyReLU (None, 512) 0
重塑 (None, 2, 2, 128) 0
转置卷积层 (None, 4, 4, 128) 147,584
批量归一化 (None, 4, 4, 128) 512
LeakyReLU (None, 4, 4, 128) 0
转置卷积层 (None, 8, 8, 128) 147,584
批量归一化 (None, 8, 8, 128) 512
LeakyReLU (None, 8, 8, 128) 0
转置卷积层 (None, 16, 16, 128) 147,584
批量归一化 (None, 16, 16, 128) 512
LeakyReLU (None, 16, 16, 128) 0
转置卷积层 (None, 32, 32, 128) 147,584
批量归一化 (None, 32, 32, 128) 512
LeakyReLU (None, 32, 32, 128) 0
转置卷积层 (None, 32, 32, 3) 3,459
总参数 700,803
可训练参数 698,755
不可训练参数 2,048

在训练大约五个时期后,我们的 VAE 应该能够生成名人面孔的新颖图像!

变分自动编码器的分析

首先,让我们看一下重建面孔的样本。图 3-16 中的顶行显示原始图像,底行显示它们通过编码器和解码器后的重建图像。

图 3-16。通过编码器和解码器传递后重建的面孔

我们可以看到 VAE 成功地捕捉了每张脸的关键特征-头部角度、发型、表情等。一些细节缺失,但重要的是要记住,构建变分自动编码器的目的不是为了实现完美的重构损失。我们的最终目标是从潜在空间中取样,以生成新的面孔。

为了实现这一点,我们必须检查潜在空间中的点的分布是否大致类似于多元标准正态分布。如果我们看到任何维度与标准正态分布明显不同,那么我们可能应该减少重构损失因子,因为 KL 散度项没有足够的影响。

我们潜在空间中的前 50 个维度显示在图 3-17 中。没有任何分布突出为与标准正态有显著不同,所以我们可以继续生成一些面孔!

图 3-17。潜在空间中前 50 个维度的点的分布

生成新面孔

要生成新的面孔,我们可以使用示例 3-17 中的代码。

示例 3-17。从潜在空间生成新面孔
grid_width, grid_height = (10,3)
z_sample = np.random.normal(size=(grid_width * grid_height, 200)) # ①

reconstructions = decoder.predict(z_sample) # ②

fig = plt.figure(figsize=(18, 5))
fig.subplots_adjust(hspace=0.4, wspace=0.4)
for i in range(grid_width * grid_height):
    ax = fig.add_subplot(grid_height, grid_width, i + 1)
    ax.axis("off")
    ax.imshow(reconstructions[i, :, :]) # ③

从具有 200 维度的标准多元正态分布中采样 30 个点。

解码采样点。

绘制图像!

输出显示在图 3-18 中。

图 3-18。新生成的面孔

令人惊讶的是,VAE 能够将我们从标准正态分布中采样的一组点转换为令人信服的人脸图像。这是我们第一次看到生成模型真正力量的一瞥!

接下来,让我们看看是否可以开始使用潜在空间对生成的图像执行一些有趣的操作。

潜在空间算术

将图像映射到较低维度的潜在空间的一个好处是,我们可以对这个潜在空间中的向量进行算术运算,当解码回原始图像域时具有视觉类比。

例如,假设我们想要拍摄一个看起来很伤心的人的图像,并给他们一个微笑。为此,我们首先需要找到潜在空间中指向增加微笑方向的向量。将这个向量添加到潜在空间中原始图像的编码中,将给我们一个新点,解码后应该给我们一个更加微笑的原始图像版本。

那么我们如何找到微笑向量呢?CelebA 数据集中的每个图像都带有属性标签,其中之一是Smiling。如果我们在具有属性Smiling的编码图像的潜在空间中取平均位置,并减去没有属性Smiling的编码图像的平均位置,我们将获得指向Smiling方向的向量,这正是我们需要的。

在潜在空间中,我们进行以下向量算术,其中alpha是一个确定添加或减去多少特征向量的因子:

z_new = z + alpha * feature_vector

让我们看看这个过程。图 3-19 显示了几幅已编码到潜在空间中的图像。然后,我们添加或减去某个向量的倍数(例如,SmilingBlack_HairEyeglassesYoungMaleBlond_Hair)以获得图像的不同版本,只改变相关特征。

图 3-19。向面孔添加和减去特征

令人惊奇的是,即使我们在潜在空间中移动点的距离相当大,核心图像仍然大致相同,除了我们想要操作的一个特征。这展示了变分自动编码器在捕捉和调整图像中的高级特征方面的能力。

在面孔之间变形

我们可以使用类似的想法在两个面孔之间变形。想象一下潜在空间中表示两个图像的两个点 A 和 B。如果您从点 A 开始沿直线走向点 B,解码沿途的每个点,您将看到从起始面孔到结束面孔的逐渐过渡。

数学上,我们正在遍历一条直线,可以用以下方程描述:

z_new = z_A * (1- alpha) + z_B * alpha

在这里,alpha是一个介于 0 和 1 之间的数字,确定我们离点 A 有多远。

图 3-20 展示了这一过程的实际操作。我们取两个图像,将它们编码到潜在空间中,然后在它们之间的直线上以固定间隔解码点。

图 3-20。在两个面孔之间变形

值得注意的是过渡的平滑性——即使同时有多个要同时更改的特征(例如,去除眼镜、头发颜色、性别),VAE 也能够流畅地实现这一点,显示出 VAE 的潜在空间确实是一个可以遍历和探索以生成多种不同人脸的连续空间。 # 摘要

在本章中,我们看到变分自动编码器是生成建模工具箱中的一个强大工具。我们首先探讨了如何使用普通自动编码器将高维图像映射到低维潜在空间,以便从单独无信息的像素中提取高级特征。然而,我们很快发现使用普通自动编码器作为生成模型存在一些缺点——例如,从学习的潜在空间中进行采样是有问题的。

变分自动编码器通过在模型中引入随机性并约束潜在空间中的点如何分布来解决这些问题。我们看到,通过一些微小的调整,我们可以将我们的自动编码器转变为变分自动编码器,从而赋予它成为真正生成模型的能力。

最后,我们将我们的新技术应用于面部生成问题,并看到我们如何简单地解码标准正态分布中的点以生成新的面孔。此外,通过在潜在空间内执行向量算术,我们可以实现一些惊人的效果,如面部变形和特征操作。

在下一章中,我们将探索一种不同类型的模型,这种模型仍然是生成图像建模的一种流行选择:生成对抗网络。

¹ Diederik P. Kingma 和 Max Welling,“自动编码变分贝叶斯”,2013 年 12 月 20 日,https://arxiv.org/abs/1312.6114

² Vincent Dumoulin 和 Francesco Visin,“深度学习卷积算术指南”,2018 年 1 月 12 日,https://arxiv.org/abs/1603.07285

³ Ziwei Liu 等,“大规模 CelebFaces 属性(CelebA)数据集”,2015 年,http://mmlab.ie.cuhk.edu.hk/projects/CelebA.html

第四章:生成对抗网络

2014 年,Ian Goodfellow 等人在蒙特利尔的神经信息处理系统会议(NeurIPS)上发表了一篇名为“生成对抗网络”的论文¹。生成对抗网络(或者更常见的称为 GAN)的引入现在被认为是生成建模历史上的一个关键转折点,因为这篇论文中提出的核心思想衍生出了一些最成功和令人印象深刻的生成模型。

本章将首先阐述 GAN 的理论基础,然后我们将看到如何使用 Keras 构建我们自己的 GAN。

介绍

让我们从一个简短的故事开始,以阐明 GAN 训练过程中使用的一些基本概念。

Brickki 砖块和伪造者的故事描述了生成对抗网络的训练过程。

GAN 是生成器和鉴别器之间的一场战斗。生成器试图将随机噪声转换为看起来像是从原始数据集中抽样的观察结果,而鉴别器试图预测一个观察结果是来自原始数据集还是生成器的伪造品之一。两个网络的输入和输出示例显示在图 4-2 中。

图 4-2. GAN 中两个网络的输入和输出

在过程开始时,生成器输出嘈杂的图像,鉴别器随机预测。GAN 的关键在于我们如何交替训练这两个网络,使得随着生成器变得更擅长欺骗鉴别器,鉴别器必须适应以保持其正确识别哪些观察结果是伪造的能力。这驱使生成器找到欺骗鉴别器的新方法,循环继续。

深度卷积生成对抗网络(DCGAN)

为了看到这个过程,让我们开始在 Keras 中构建我们的第一个 GAN,以生成砖块的图片。

我们将密切关注 GAN 的第一篇重要论文之一,“使用深度卷积生成对抗网络进行无监督表示学习”²。在这篇 2015 年的论文中,作者展示了如何构建一个深度卷积 GAN 来从各种数据集中生成逼真的图像。他们还引入了一些显著改进生成图像质量的变化。

运行此示例的代码

这个例子的代码可以在位于书籍存储库中的 Jupyter 笔记本中找到,路径为notebooks/04_gan/01_dcgan/dcgan.ipynb

砖块数据集

首先,您需要下载训练数据。我们将使用通过 Kaggle 提供的乐高砖块图像数据集。这是一个包含 40,000 张来自多个角度拍摄的 50 种不同玩具砖块的照片的计算机渲染集合。一些 Brickki 产品的示例图像显示在图 4-3 中。

图 4-3. 砖块数据集中的图像示例

您可以通过在书籍存储库中运行 Kaggle 数据集下载脚本来下载数据集,如示例 4-1 所示。这将把图像和相关元数据保存到/data文件夹中。

示例 4-1. 下载砖块数据集
bash scripts/download_kaggle_data.sh joosthazelzet lego-brick-images

我们使用 Keras 函数image_dataset_from_directory创建一个指向存储图像的目录的 TensorFlow 数据集,如示例 4-2 所示。这使我们能够在需要时(例如在训练期间)将图像批量读入内存,以便我们可以处理大型数据集而不必担心必须将整个数据集装入内存。它还将图像调整为 64×64 大小,插值像素值之间的差值。

示例 4-2. 从目录中的图像文件创建 TensorFlow 数据集
train_data = utils.image_dataset_from_directory(
    "/app/data/lego-brick-images/dataset/",
    labels=None,
    color_mode="grayscale",
    image_size=(64, 64),
    batch_size=128,
    shuffle=True,
    seed=42,
    interpolation="bilinear",
)

原始数据在范围[0, 255]内缩放以表示像素强度。在训练 GAN 时,我们将数据重新缩放到范围[-1, 1],以便我们可以在生成器的最后一层使用 tanh 激活函数,该函数提供比 sigmoid 函数更强的梯度(示例 4-3)。

示例 4-3。预处理砖块数据集
def preprocess(img):
    img = (tf.cast(img, "float32") - 127.5) / 127.5
    return img

train = train_data.map(lambda x: preprocess(x))

现在让我们看看如何构建鉴别器。## 鉴别器

鉴别器的目标是预测图像是真实的还是伪造的。这是一个监督图像分类问题,因此我们可以使用与我们在第二章中使用的类似架构:堆叠的卷积层,带有单个输出节点。

我们将构建的鉴别器的完整架构显示在表 4-1 中。

表 4-1。鉴别器的模型摘要

层(类型) 输出形状 参数 #
InputLayer (None, 64, 64, 1) 0
Conv2D (None, 32, 32, 64) 1,024
LeakyReLU (None, 32, 32, 64) 0
Dropout (None, 32, 32, 64) 0
Conv2D (None, 16, 16, 128) 131,072
BatchNormalization (None, 16, 16, 128) 512
LeakyReLU (None, 16, 16, 128) 0
Dropout (None, 16, 16, 128) 0
Conv2D (None, 8, 8, 256) 524,288
BatchNormalization (None, 8, 8, 256) 1,024
LeakyReLU (None, 8, 8, 256) 0
Dropout (None, 8, 8, 256) 0
Conv2D (None, 4, 4, 512) 2,097,152
BatchNormalization (None, 4, 4, 512) 2,048
LeakyReLU (None, 4, 4, 512) 0
Dropout (None, 4, 4, 512) 0
Conv2D (None, 1, 1, 1) 8,192
Flatten (None, 1) 0
总参数 2,765,312
可训练参数 2,763,520
不可训练参数 1,792

提供构建鉴别器的 Keras 代码在示例 4-4 中。

示例 4-4。鉴别器
discriminator_input = layers.Input(shape=(64, 64, 1)) # ①
x = layers.Conv2D(64, kernel_size=4, strides=2, padding="same", use_bias = False)(
    discriminator_input
) # ②
x = layers.LeakyReLU(0.2)(x)
x = layers.Dropout(0.3)(x)
x = layers.Conv2D(
    128, kernel_size=4, strides=2, padding="same", use_bias = False
)(x)
x = layers.BatchNormalization(momentum = 0.9)(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Dropout(0.3)(x)
x = layers.Conv2D(
    256, kernel_size=4, strides=2, padding="same", use_bias = False
)(x)
x = layers.BatchNormalization(momentum = 0.9)(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Dropout(0.3)(x)
x = layers.Conv2D(
    512, kernel_size=4, strides=2, padding="same", use_bias = False
)(x)
x = layers.BatchNormalization(momentum = 0.9)(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Dropout(0.3)(x)
x = layers.Conv2D(
    1,
    kernel_size=4,
    strides=1,
    padding="valid",
    use_bias = False,
    activation = 'sigmoid'
)(x)
discriminator_output = layers.Flatten()(x) # ③

discriminator = models.Model(discriminator_input, discriminator_output) # ④

定义鉴别器的Input层(图像)。

Conv2D层堆叠在一起,中间夹有BatchNormalizationLeakyReLU激活和Dropout层。

将最后一个卷积层展平-到这一点,张量的形状为 1×1×1,因此不需要最终的Dense层。

定义鉴别器的 Keras 模型-一个接受输入图像并输出介于 0 和 1 之间的单个数字的模型。

请注意,我们在一些Conv2D层中使用步幅为 2,以减少通过网络时张量的空间形状(原始图像中为 64,然后 32、16、8、4,最后为 1),同时增加通道数(灰度输入图像中为 1,然后 64、128、256,最后为 512),最终折叠为单个预测。

我们在最后一个Conv2D层上使用 sigmoid 激活函数,输出一个介于 0 和 1 之间的数字。

生成器

现在让我们构建生成器。生成器的输入将是从多元标准正态分布中抽取的向量。输出是与原始训练数据中的图像大小相同的图像。

这个描述可能让你想起变分自动编码器中的解码器。事实上,GAN 的生成器与 VAE 的解码器完全履行相同的目的:将潜在空间中的向量转换为图像。在生成建模中,从潜在空间映射回原始域的概念非常常见,因为它使我们能够操纵潜在空间中的向量以改变原始域中图像的高级特征。

我们将构建的生成器的架构显示在表 4-2 中。

表 4-2。生成器的模型摘要

层(类型) 输出形状 参数 #
InputLayer(无,100)0
Reshape(无,1,1,100)0
Conv2DTranspose(无,4,4,512)819,200
BatchNormalization(无,4,4,512)2,048
ReLU(无,4,4,512)0
Conv2DTranspose(无,8,8,256)2,097,152
BatchNormalization(无,8,8,256)1,024
ReLU(无,8,8,256)0
Conv2DTranspose(无,16,16,128)524,288
BatchNormalization(无,16,16,128)512
ReLU(无,16,16,128)0
Conv2DTranspose(无,32,32,64)131,072
BatchNormalization(无,32,32,64)256
ReLU(无,32,32,64)0
Conv2DTranspose(无,64,64,1)1,024
总参数 3,576,576
可训练参数 3,574,656
不可训练参数 1,920

构建生成器的代码在示例 4-5 中给出。

示例 4-5。生成器
generator_input = layers.Input(shape=(100,)) # ①
x = layers.Reshape((1, 1, 100))(generator_input) # ②
x = layers.Conv2DTranspose(
    512, kernel_size=4, strides=1, padding="valid", use_bias = False
)(x) # ③
x = layers.BatchNormalization(momentum=0.9)(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Conv2DTranspose(
    256, kernel_size=4, strides=2, padding="same", use_bias = False
)(x)
x = layers.BatchNormalization(momentum=0.9)(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Conv2DTranspose(
    128, kernel_size=4, strides=2, padding="same", use_bias = False
)(x)
x = layers.BatchNormalization(momentum=0.9)(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Conv2DTranspose(
    64, kernel_size=4, strides=2, padding="same", use_bias = False
)(x)
x = layers.BatchNormalization(momentum=0.9)(x)
x = layers.LeakyReLU(0.2)(x)
generator_output = layers.Conv2DTranspose(
    1,
    kernel_size=4,
    strides=2,
    padding="same",
    use_bias = False,
    activation = 'tanh'
)(x) # ④
generator = models.Model(generator_input, generator_output) # ⑤

定义生成器的Input层-长度为 100 的向量。

我们使用一个Reshape层来给出一个 1×1×100 的张量,这样我们就可以开始应用卷积转置操作。

我们通过四个Conv2DTranspose层传递这些数据,其中夹在中间的是BatchNormalizationLeakyReLU层。

最终的Conv2DTranspose层使用 tanh 激活函数将输出转换为范围[-1,1],以匹配原始图像域。

定义生成器的 Keras 模型-接受长度为 100 的向量并输出形状为[64,64,1]的张量。

请注意,我们在一些Conv2DTranspose层中使用步幅为 2,以增加通过网络传递时张量的空间形状(原始向量中为 1,然后为 4,8,16,32,最终为 64),同时减少通道数(512,然后为 256,128,64,最终为 1 以匹配灰度输出)。

训练 DCGAN

正如我们所看到的,在 DCGAN 中生成器和鉴别器的架构非常简单,并且与我们在第三章中看到的 VAE 模型并没有太大不同。理解 GAN 的关键在于理解生成器和鉴别器的训练过程。

我们可以通过创建一个训练集来训练鉴别器,其中一些图像是来自训练集的真实观察结果,一些是来自生成器的输出。然后我们将其视为一个监督学习问题,其中真实图像的标签为 1,假图像的标签为 0,损失函数为二元交叉熵。

我们应该如何训练生成器?我们需要找到一种评分每个生成的图像的方法,以便它可以优化到高分图像。幸运的是,我们有一个鉴别器正是这样做的!我们可以生成一批图像并将其通过鉴别器以获得每个图像的分数。然后生成器的损失函数就是这些概率与一个全为 1 的向量之间的二元交叉熵,因为我们希望训练生成器生成鉴别器认为是真实的图像。

至关重要的是,我们必须交替训练这两个网络,确保我们一次只更新一个网络的权重。例如,在生成器训练过程中,只有生成器的权重会被更新。如果我们允许鉴别器的权重也发生变化,那么鉴别器将只是调整自己,以便更有可能预测生成的图像是真实的,这不是期望的结果。我们希望生成的图像被预测接近 1(真实),因为生成器强大,而不是因为鉴别器弱。

鉴别器和生成器的训练过程的图示如图 4-5 所示。

图 4-5。训练 DCGAN-灰色框表示在训练过程中权重被冻结

Keras 提供了创建自定义train_step函数来实现这一逻辑的能力。示例 4-7 展示了完整的DCGAN模型类。

示例 4-7。编译 DCGAN
class DCGAN(models.Model):
    def __init__(self, discriminator, generator, latent_dim):
        super(DCGAN, self).__init__()
        self.discriminator = discriminator
        self.generator = generator
        self.latent_dim = latent_dim

    def compile(self, d_optimizer, g_optimizer):
        super(DCGAN, self).compile()
        self.loss_fn = losses.BinaryCrossentropy() # ①
        self.d_optimizer = d_optimizer
        self.g_optimizer = g_optimizer
        self.d_loss_metric = metrics.Mean(name="d_loss")
        self.g_loss_metric = metrics.Mean(name="g_loss")

    @property
    def metrics(self):
        return [self.d_loss_metric, self.g_loss_metric]

    def train_step(self, real_images):
        batch_size = tf.shape(real_images)[0]
        random_latent_vectors = tf.random.normal(
            shape=(batch_size, self.latent_dim)
        ) # ②

        with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape:
            generated_images = self.generator(
                random_latent_vectors, training = True
            ) # ③
            real_predictions = self.discriminator(real_images, training = True) # ④
            fake_predictions = self.discriminator(
                generated_images, training = True
            ) # ⑤

            real_labels = tf.ones_like(real_predictions)
            real_noisy_labels = real_labels + 0.1 * tf.random.uniform(
                tf.shape(real_predictions)
            )
            fake_labels = tf.zeros_like(fake_predictions)
            fake_noisy_labels = fake_labels - 0.1 * tf.random.uniform(
                tf.shape(fake_predictions)
            )

            d_real_loss = self.loss_fn(real_noisy_labels, real_predictions)
            d_fake_loss = self.loss_fn(fake_noisy_labels, fake_predictions)
            d_loss = (d_real_loss + d_fake_loss) / 2.0 # ⑥

            g_loss = self.loss_fn(real_labels, fake_predictions) # ⑦

        gradients_of_discriminator = disc_tape.gradient(
            d_loss, self.discriminator.trainable_variables
        )
        gradients_of_generator = gen_tape.gradient(
            g_loss, self.generator.trainable_variables
        )

        self.d_optimizer.apply_gradients(
            zip(gradients_of_discriminator, discriminator.trainable_variables)
        ) # ⑧
        self.g_optimizer.apply_gradients(
            zip(gradients_of_generator, generator.trainable_variables)
        )

        self.d_loss_metric.update_state(d_loss)
        self.g_loss_metric.update_state(g_loss)

        return {m.name: m.result() for m in self.metrics}

dcgan = DCGAN(
    discriminator=discriminator, generator=generator, latent_dim=100
)

dcgan.compile(
    d_optimizer=optimizers.Adam(
        learning_rate=0.0002, beta_1 = 0.5, beta_2 = 0.999
    ),
    g_optimizer=optimizers.Adam(
        learning_rate=0.0002, beta_1 = 0.5, beta_2 = 0.999
    ),
)

dcgan.fit(train, epochs=300)

生成器和鉴别器的损失函数是BinaryCrossentropy

为了训练网络,首先从多元标准正态分布中抽取一批向量。

接下来,通过生成器生成一批生成的图像。

现在让鉴别器预测一批真实图像的真实性...​

...​和一批生成的图像。

鉴别器损失是真实图像(标签为 1)和假图像(标签为 0)之间的平均二元交叉熵。

生成器损失是鉴别器对生成图像的预测与标签 1 之间的二元交叉熵。

分别更新鉴别器和生成器的权重。

鉴别器和生成器不断争夺主导地位,这可能使 DCGAN 训练过程不稳定。理想情况下,训练过程将找到一个平衡点,使生成器能够从鉴别器那里学习有意义的信息,图像的质量将开始提高。经过足够的 epochs,鉴别器往往最终占据主导地位,如图 4-6 所示,但这可能不是问题,因为生成器可能已经学会在这一点上生成足够高质量的图像。

图 4-6。训练过程中鉴别器和生成器的损失和准确率

向标签添加噪声

在训练 GAN 时的一个有用技巧是向训练标签添加少量随机噪声。这有助于改善训练过程的稳定性并增强生成的图像。这种标签平滑作为一种驯服鉴别器的方式,使其面临更具挑战性的任务,不会压倒生成器。

DCGAN 的分析

通过观察训练过程中特定时期生成器生成的图像(图 4-7),可以清楚地看到生成器越来越擅长生成可能来自训练集的图像。

图 4-7。训练过程中特定时期生成器的输出

神奇的是神经网络能够将随机噪声转换为有意义的东西。值得记住的是,我们除了原始像素之外没有提供模型任何额外的特征,因此它必须自行解决如何绘制阴影、立方体和圆等高级概念。

成功生成模型的另一个要求是它不仅仅是复制训练集中的图像。为了测试这一点,我们可以找到训练集中与特定生成示例最接近的图像。一个好的距离度量是L1 距离,定义为:

def compare_images(img1, img2):
    return np.mean(np.abs(img1 - img2))

图 4-8 显示了一些生成图像在训练集中最接近的观察结果。我们可以看到,虽然生成图像与训练集之间存在一定程度的相似性,但它们并不完全相同。这表明生成器已经理解了这些高级特征,并且能够生成与已经看到的图像不同的示例。

图 4-8. 从训练集中生成图像的最接近匹配

GAN 训练:技巧和窍门

虽然 GAN 是生成建模的重大突破,但训练起来也非常困难。在本节中,我们将探讨训练 GAN 时遇到的一些最常见问题和挑战,以及潜在的解决方案。在下一节中,我们将看一些更基本的调整 GAN 框架的方法,以解决许多这些问题。

鉴别器压倒生成器

如果鉴别器变得过于强大,损失函数的信号变得太弱,无法驱动生成器中的任何有意义的改进。在最坏的情况下,鉴别器完全学会区分真实图像和假图像,梯度完全消失,导致没有任何训练,如图 4-9 所示。

图 4-9. 当鉴别器压倒生成器时的示例输出

如果发现鉴别器损失函数坍缩,需要找到削弱鉴别器的方法。尝试以下建议:

  • 增加鉴别器中Dropout层的rate参数,以减少通过网络的信息量。

  • 降低鉴别器的学习率。

  • 减少鉴别器中的卷积滤波器数量。

  • 在训练鉴别器时向标签添加噪音。

  • 在训练鉴别器时,随机翻转一些图像的标签。

生成器压倒鉴别器

如果鉴别器不够强大,生成器将找到一种方法轻松欺骗鉴别器,只需少量几乎相同的图像样本。这被称为模式坍塌

例如,假设我们在不更新鉴别器的情况下训练生成器多个批次。生成器会倾向于找到一个始终欺骗鉴别器的单个观察(也称为模式),并开始将潜在输入空间中的每个点映射到这个图像。此外,损失函数的梯度会坍缩到接近 0,因此无法从这种状态中恢复。

即使我们尝试重新训练鉴别器以阻止它被这一点欺骗,生成器也会简单地找到另一个欺骗鉴别器的模式,因为它已经对其输入麻木,因此没有多样化其输出的动机。

模式坍塌的效果可以在图 4-10 中看到。

图 4-10. 当生成器压倒鉴别器时模式坍塌的示例

如果发现生成器遭受模式坍塌,可以尝试使用与前一节中列出的相反建议来加强鉴别器。此外,可以尝试降低两个网络的学习率并增加批量大小。

无信息损失

由于深度学习模型被编译为最小化损失函数,自然会认为生成器的损失函数越小,生成的图像质量就越好。然而,由于生成器只针对当前鉴别器进行评分,而鉴别器不断改进,我们无法比较在训练过程中不同点评估的损失函数。实际上,在图 4-6 中,生成器的损失函数随时间增加,尽管图像质量明显提高。生成器损失与图像质量之间的缺乏相关性有时使得 GAN 训练难以监控。

超参数

正如我们所看到的,即使是简单的 GAN,也有大量的超参数需要调整。除了鉴别器和生成器的整体架构外,还有控制批量归一化、丢弃、学习率、激活层、卷积滤波器、内核大小、步幅、批量大小和潜在空间大小的参数需要考虑。GAN 对所有这些参数的微小变化非常敏感,找到一组有效的参数通常是经过有教养的试错过程,而不是遵循一套已建立的指导方针。

这就是为什么重要理解 GAN 的内部工作原理并知道如何解释损失函数——这样你就可以识别出可能改善模型稳定性的超参数的合理调整。

解决 GAN 的挑战

近年来,一些关键进展大大提高了 GAN 模型的整体稳定性,并减少了一些早期列出的问题的可能性,比如模式崩溃。

在本章的其余部分,我们将研究带有梯度惩罚的 Wasserstein GAN(WGAN-GP),该模型对我们迄今为止探索的 GAN 框架进行了几个关键调整,以改善图像生成过程的稳定性和质量。 #带有梯度惩罚的 Wasserstein GAN(WGAN-GP)

在本节中,我们将构建一个 WGAN-GP 来从我们在第三章中使用的 CelebA 数据集中生成人脸。

运行此示例的代码

这个示例的代码可以在书库中的notebooks/04_gan/02_wgan_gp/wgan_gp.ipynb中找到。

这段代码是从由 Aakash Kumar Nain 创建的优秀的WGAN-GP 教程中改编而来,该教程可在 Keras 网站上找到。

Wasserstein GAN(WGAN)是由 Arjovsky 等人在 2017 年的一篇论文中引入的,是稳定 GAN 训练的第一步。通过一些改变,作者们能够展示如何训练具有以下两个特性的 GAN(引用自论文):

  • 一个与生成器的收敛和样本质量相关的有意义的损失度量

  • 优化过程的稳定性提高

具体来说,该论文为鉴别器和生成器引入了Wasserstein 损失函数。使用这个损失函数而不是二元交叉熵会导致 GAN 更稳定地收敛。

在本节中,我们将定义 Wasserstein 损失函数,然后看看我们需要对模型架构和训练过程做哪些其他更改以整合我们的新损失函数。

您可以在书库中的chapter05/wgan-gp/faces/train.ipynb中找到完整的模型类。

Wasserstein 损失

让我们首先回顾一下二元交叉熵损失的定义——我们目前用来训练 GAN 的函数(方程 4-1)。

方程 4-1. 二元交叉熵损失

- 1 n i=1 n ( y i log ( p i ) + ( 1 - y i ) log ( 1 - p i ) )

为了训练 GAN 鉴别器 D,我们计算了对比真实图像 x_i 的预测 p_i 与响应 y_i=1 以及对比生成图像 G(z_i)的预测 p_i 与响应 y_i=0 的损失。因此,对于 GAN 鉴别器,最小化损失函数可以写成方程 4-2 所示。

方程 4-2. GAN 鉴别器损失最小化

min D - ( 𝔼 xp X [ log D ( x ) ] + 𝔼 zp Z [ log ( 1 - D ( G ( z ) ) ) ] )

为了训练 GAN 生成器 G,我们计算了对比生成图像 G(z_i)的预测 p_i 与响应 y_i=1 的损失。因此,对于 GAN 生成器,最小化损失函数可以写成方程 4-3 所示。

方程 4-3. GAN 生成器损失最小化

min G - ( 𝔼 zp Z [ log D ( G ( z ) ) ] )

现在让我们将其与 Wasserstein 损失函数进行比较。

首先,Wasserstein 损失要求我们使用 y_i=1 和 y_i=-1 作为标签,而不是 1 和 0。我们还从鉴别器的最后一层中移除了 sigmoid 激活,使得预测 p_i 不再受限于[0, 1]范围,而是可以是任何范围内的任意数字(负无穷,正无穷)。因此,在 WGAN 中,鉴别器通常被称为评论家,输出分数而不是概率。

Wasserstein 损失函数定义如下:

- 1 n i=1 n ( y i p i )

为了训练 WGAN 评论家D,我们计算真实图像的预测与响应之间的损失p i = D ( x i )与响应y i = 1,以及生成图像的预测与响应之间的损失p i = D ( G ( z i ) )与响应y i = -1。因此,对于 WGAN 评论家,最小化损失函数可以写成如下形式:

min D - ( 𝔼 xp X [ D ( x ) ] - 𝔼 zp Z [ D ( G ( z ) ) ] )

换句话说,WGAN 评论家试图最大化其对真实图像和生成图像的预测之间的差异。

为了训练 WGAN 生成器,我们计算生成图像的预测与响应之间的损失p i = D ( G ( z i ) )与响应y i = 1。因此,对于 WGAN 生成器,最小化损失函数可以写成如下形式:

min G - ( 𝔼 zp Z [ D ( G ( z ) ) ] )

换句话说,WGAN 生成器试图生成评论家尽可能高分的图像(即,评论家被欺骗以为它们是真实的)。

利普希茨约束

也许让你惊讶的是,我们现在允许评论家输出范围内的任何数字(- ),而不是应用 Sigmoid 函数将输出限制在通常的[0,1]范围内。因此,Wasserstein 损失可能非常大,这令人不安——通常情况下,神经网络中的大数值应该避免!

事实上,WGAN 论文的作者表明,为了使 Wasserstein 损失函数起作用,我们还需要对评论家施加额外的约束。具体来说,评论家必须是1-Lipschitz 连续函数。让我们详细解释一下这意味着什么。

评论家是一个将图像转换为预测的函数D。如果对于任意两个输入图像x 1x 2,该函数满足以下不等式,则我们称该函数为 1-Lipschitz:

|D(x 1 )-D(x 2 )| |x 1 -x 2 | 1

在这里,| x 1 - x 2 |是两个图像之间的平均像素绝对差异,| D ( x 1 ) - D ( x 2 ) |是评论家预测之间的绝对差异。基本上,我们要求评论家的预测在两个图像之间变化的速率有限(即,梯度的绝对值必须在任何地方最多为 1)。我们可以看到这应用于利普希茨连续的一维函数中,如图 4-11 所示——在任何位置放置锥体时,线都不会进入锥体。换句话说,线在任何点上升或下降的速率都有限制。

图 4-11。利普希茨连续函数(来源:维基百科
提示

对于那些想深入了解为什么只有在强制执行这个约束时,Wasserstein 损失才有效的数学原理的人,Jonathan Hui 提供了一个优秀的解释

强制利普希茨约束

在原始的 WGAN 论文中,作者展示了通过在每个训练批次后将评论家的权重剪辑到一个小范围[-0.01, 0.01]内来强制执行利普希茨约束的可能性。

这种方法的批评之一是,评论家学习的能力大大降低,因为我们正在剪辑它的权重。事实上,即使在原始的 WGAN 论文中,作者们也写道,“权重剪辑显然是一种强制利普希茨约束的可怕方式。”强大的评论家对于 WGAN 的成功至关重要,因为没有准确的梯度,生成器无法学习如何调整其权重以生成更好的样本。

因此,其他研究人员寻找了其他方法来强制执行利普希茨约束,并提高 WGAN 学习复杂特征的能力。其中一种方法是带有梯度惩罚的 Wasserstein GAN。

在引入这种变体的论文中,作者展示了如何通过在评论家的损失函数中直接包含梯度惩罚项来强制执行利普希茨约束,如果梯度范数偏离 1,模型将受到惩罚。这导致了一个更加稳定的训练过程。

在下一节中,我们将看到如何将这个额外项构建到我们评论家的损失函数中。

梯度惩罚损失

图 4-12 是 WGAN-GP 评论家的训练过程的图表。如果我们将其与图 4-5 中原始鉴别器训练过程进行比较,我们可以看到的关键添加是梯度惩罚损失作为整体损失函数的一部分,与真实和虚假图像的 Wasserstein 损失一起。

图 4-12。WGAN-GP 评论家训练过程

梯度惩罚损失衡量了预测梯度的范数与输入图像之间的平方差异和 1 之间的差异。模型自然倾向于找到确保梯度惩罚项最小化的权重,从而鼓励模型符合利普希茨约束。

在训练过程中无法计算每个地方的梯度,因此 WGAN-GP 只在少数几个点处评估梯度。为了确保平衡混合,我们使用一组插值图像,这些图像位于连接真实图像批次和虚假图像批次的线上随机选择的点上,如图 4-13 所示。

图 4-13。图像之间的插值

在示例 4-8 中,我们展示了如何在代码中计算梯度惩罚。

示例 4-8。梯度惩罚损失函数
def gradient_penalty(self, batch_size, real_images, fake_images):
    alpha = tf.random.normal([batch_size, 1, 1, 1], 0.0, 1.0) # ①
    diff = fake_images - real_images
    interpolated = real_images + alpha * diff # ②

    with tf.GradientTape() as gp_tape:
        gp_tape.watch(interpolated)
        pred = self.critic(interpolated, training=True) # ③

    grads = gp_tape.gradient(pred, [interpolated])[0] # ④
    norm = tf.sqrt(tf.reduce_sum(tf.square(grads), axis=[1, 2, 3])) # ⑤
    gp = tf.reduce_mean((norm - 1.0) ** 2) # ⑥
    return gp

批次中的每个图像都会得到一个介于 0 和 1 之间的随机数,存储为向量alpha

计算一组插值图像。

批评家被要求对这些插值图像进行评分。

根据输入图像计算预测的梯度。

计算这个向量的 L2 范数。

该函数返回 L2 范数与 1 之间的平均平方距离。

训练 WGAN-GP

使用 Wasserstein 损失函数的一个关键优势是我们不再需要担心平衡批评家和生成器的训练——事实上,当使用 Wasserstein 损失时,必须在更新生成器之前将批评家训练到收敛,以确保生成器更新的梯度准确。这与标准 GAN 相反,标准 GAN 中重要的是不要让鉴别器变得太强。

因此,使用 Wasserstein GAN,我们可以简单地在生成器更新之间多次训练批评家,以确保它接近收敛。通常使用的比例是每次生成器更新三到五次批评家更新。

我们现在介绍了 WGAN-GP 背后的两个关键概念——Wasserstein 损失和包含在批评家损失函数中的梯度惩罚项。包含所有这些想法的 WGAN 模型的训练步骤显示在示例 4-9 中。

示例 4-9。训练 WGAN-GP
def train_step(self, real_images):
    batch_size = tf.shape(real_images)[0]

    for i in range(3): # ①
        random_latent_vectors = tf.random.normal(
            shape=(batch_size, self.latent_dim)
        )

        with tf.GradientTape() as tape:
            fake_images = self.generator(
                random_latent_vectors, training = True
            )
            fake_predictions = self.critic(fake_images, training = True)
            real_predictions = self.critic(real_images, training = True)

            c_wass_loss = tf.reduce_mean(fake_predictions) - tf.reduce_mean(
                real_predictions
            ) # ②
            c_gp = self.gradient_penalty(
                batch_size, real_images, fake_images
            ) # ③
            c_loss = c_wass_loss + c_gp * self.gp_weight # ④

        c_gradient = tape.gradient(c_loss, self.critic.trainable_variables)
        self.c_optimizer.apply_gradients(
            zip(c_gradient, self.critic.trainable_variables)
        ) # ⑤

    random_latent_vectors = tf.random.normal(
        shape=(batch_size, self.latent_dim)
    )
    with tf.GradientTape() as tape:
        fake_images = self.generator(random_latent_vectors, training=True)
        fake_predictions = self.critic(fake_images, training=True)
        g_loss = -tf.reduce_mean(fake_predictions) # ⑥

    gen_gradient = tape.gradient(g_loss, self.generator.trainable_variables)
    self.g_optimizer.apply_gradients(
        zip(gen_gradient, self.generator.trainable_variables)
    ) # ⑦

    self.c_loss_metric.update_state(c_loss)
    self.c_wass_loss_metric.update_state(c_wass_loss)
    self.c_gp_metric.update_state(c_gp)
    self.g_loss_metric.update_state(g_loss)

    return {m.name: m.result() for m in self.metrics}

执行三次批评家更新。

为批评家计算 Wasserstein 损失——虚假图像和真实图像的平均预测之间的差异。

计算梯度惩罚项(参见示例 4-8)。

批评家损失函数是 Wasserstein 损失和梯度惩罚的加权和。

更新批评家的权重。

为生成器计算 Wasserstein 损失。

更新生成器的权重。

WGAN-GP 中的批归一化

在训练 WGAN-GP 之前我们应该注意的最后一个考虑是批归一化不应该在批评家中使用。这是因为批归一化会在同一批次中的图像之间创建相关性,使得梯度惩罚损失效果不佳。实验证明,即使在批评家中没有批归一化,WGAN-GP 仍然可以产生出色的结果。

我们现在已经涵盖了标准 GAN 和 WGAN-GP 之间的所有关键区别。回顾一下:

  • WGAN-GP 使用 Wasserstein 损失。

  • WGAN-GP 使用标签 1 表示真实和-1 表示虚假进行训练。

  • 在批评家的最后一层没有 Sigmoid 激活。

  • 在评论者的损失函数中包含一个梯度惩罚项。

  • 对生成器进行多次更新之前多次训练评论者。

  • 评论中没有批量归一化层。

WGAN-GP 的分析

让我们看一下生成器在训练 25 个时期后的一些示例输出(图 4-14)。

图 4-14. WGAN-GP 面部示例

模型已经学会了面部的重要高级属性,没有出现模式坍塌的迹象。

我们还可以看到模型的损失函数随时间的演变(图 4-15)—评论者和生成器的损失函数都非常稳定和收敛。

如果我们将 WGAN-GP 的输出与上一章的 VAE 输出进行比较,我们可以看到 GAN 图像通常更清晰—特别是头发和背景之间的定义。这在一般情况下是正确的;VAE 倾向于产生模糊颜色边界的柔和图像,而众所周知,GAN 倾向于产生更清晰、更明确定义的图像。

图 4-15. WGAN-GP 损失曲线:评论者损失(epoch_c_loss)分解为 Wasserstein 损失(epoch_c_wass)和梯度惩罚损失(epoch_c_gp

同样,GAN 通常比 VAE 更难训练,并需要更长的时间达到令人满意的质量。然而,今天许多最先进的生成模型都是基于 GAN 的,因为在 GPU 上训练大规模 GAN 并花费更长时间的回报是显著的。

条件 GAN(CGAN)

到目前为止,在本章中,我们已经构建了能够从给定的训练集生成逼真图像的 GAN。然而,我们无法控制我们想要生成的图像类型—例如,男性或女性的面孔,或大砖或小砖。我们可以从潜在空间中随机采样一个点,但我们无法轻松地了解在选择潜在变量的情况下将产生什么样的图像。

在本章的最后部分,我们将把注意力转向构建一个能够控制输出的 GAN—所谓的条件 GAN。这个想法最早是在 2014 年由 Mirza 和 Osindero 在“条件生成对抗网络”中首次提出的,是对 GAN 架构的一个相对简单的扩展。

运行此示例的代码

此示例的代码可以在位于书籍存储库中的notebooks/04_gan/03_cgan/cgan.ipynb的 Jupyter 笔记本中找到。

代码已经从由 Sayak Paul 创建的优秀CGAN 教程中调整,该教程可在 Keras 网站上找到。

CGAN 架构

在这个例子中,我们将在面部数据集的金发属性上对我们的 CGAN 进行条件化。也就是说,我们可以明确指定我们是否想要生成一张有金发的图像。这个标签作为 CelebA 数据集的一部分提供。

高级 CGAN 架构如图 4-16 所示。

图 4-16. CGAN 中生成器和评论者的输入和输出

标准 GAN 和 CGAN 之间的关键区别在于,在 CGAN 中,我们向生成器和评论者传递与标签相关的额外信息。在生成器中,这只是作为一个独热编码向潜在空间样本附加。在评论者中,我们将标签信息作为额外通道添加到 RGB 图像中。我们通过重复独热编码向量来填充与输入图像相同形状的方式来实现这一点。

CGAN 之所以有效是因为评论者现在可以访问有关图像内容的额外信息,因此生成器必须确保其输出与提供的标签一致,以继续愚弄评论者。如果生成器生成了与图像标签不符的完美图像,评论者将能够简单地判断它们是假的,因为图像和标签不匹配。

提示

在我们的示例中,我们的独热编码标签长度为 2,因为有两个类别(金发和非金发)。但是,您可以有任意数量的标签——例如,您可以在 Fashion-MNIST 数据集上训练一个 CGAN,以输出 10 种不同的时尚物品之一,通过将长度为 10 的独热编码标签向量合并到生成器的输入中,并将 10 个额外的独热编码标签通道合并到评论家的输入中。

我们需要对架构进行的唯一更改是将标签信息连接到生成器和评论家的现有输入中,如示例 4-10 所示。

示例 4-10。CGAN 中的输入层
critic_input = layers.Input(shape=(64, 64, 3)) # ①
label_input = layers.Input(shape=(64, 64, 2))
x = layers.Concatenate(axis = -1)([critic_input, label_input])
...
generator_input = layers.Input(shape=(32,)) # ②
label_input = layers.Input(shape=(2,))
x = layers.Concatenate(axis = -1)([generator_input, label_input])
x = layers.Reshape((1,1, 34))(x)
...

图像通道和标签通道分别传递给评论家并连接。

潜在向量和标签类别分别传递给生成器,并在重塑之前连接。

训练 CGAN

我们还需要对 CGAN 的train_step进行一些更改,以匹配生成器和评论家的新输入格式,如示例 4-11 所示。

示例 4-11。CGAN 的train_step
def train_step(self, data):
    real_images, one_hot_labels = data # ①

    image_one_hot_labels = one_hot_labels[:, None, None, :] # ②
    image_one_hot_labels = tf.repeat(
        image_one_hot_labels, repeats=64, axis = 1
    )
    image_one_hot_labels = tf.repeat(
        image_one_hot_labels, repeats=64, axis = 2
    )

    batch_size = tf.shape(real_images)[0]

    for i in range(self.critic_steps):
        random_latent_vectors = tf.random.normal(
            shape=(batch_size, self.latent_dim)
        )

        with tf.GradientTape() as tape:
            fake_images = self.generator(
                [random_latent_vectors, one_hot_labels], training = True
            ) # ③

            fake_predictions = self.critic(
                [fake_images, image_one_hot_labels], training = True
            ) # ④
            real_predictions = self.critic(
                [real_images, image_one_hot_labels], training = True
            )

            c_wass_loss = tf.reduce_mean(fake_predictions) - tf.reduce_mean(
                real_predictions
            )
            c_gp = self.gradient_penalty(
                batch_size, real_images, fake_images, image_one_hot_labels
            ) # ⑤
            c_loss = c_wass_loss + c_gp * self.gp_weight

        c_gradient = tape.gradient(c_loss, self.critic.trainable_variables)
        self.c_optimizer.apply_gradients(
            zip(c_gradient, self.critic.trainable_variables)
        )

    random_latent_vectors = tf.random.normal(
        shape=(batch_size, self.latent_dim)
    )

    with tf.GradientTape() as tape:
        fake_images = self.generator(
            [random_latent_vectors, one_hot_labels], training=True
        ) # ⑥
        fake_predictions = self.critic(
            [fake_images, image_one_hot_labels], training=True
        )
        g_loss = -tf.reduce_mean(fake_predictions)

    gen_gradient = tape.gradient(g_loss, self.generator.trainable_variables)
    self.g_optimizer.apply_gradients(
        zip(gen_gradient, self.generator.trainable_variables)
    )

图像和标签从输入数据中解压缩。

独热编码向量被扩展为具有与输入图像相同空间大小(64×64)的独热编码图像。

现在,生成器被提供了两个输入的列表——随机潜在向量和独热编码标签向量。

评论家现在被提供了两个输入的列表——假/真实图像和独热编码标签通道。

梯度惩罚函数还需要将独热编码标签通道传递给评论家,因为它使用评论家。

对评论家培训步骤所做的更改也适用于生成器训练步骤。

CGAN 的分析

我们可以通过将特定的独热编码标签传递到生成器的输入来控制 CGAN 的输出。例如,要生成一个头发不是金色的脸,我们传入向量[1, 0]。要生成一个金发的脸,我们传入向量[0, 1]

可以在图 4-17 中看到 CGAN 的输出。在这里,我们保持示例中的随机潜在向量相同,只改变条件标签向量。很明显,CGAN 已经学会使用标签向量来控制图像的头发颜色属性。令人印象深刻的是,图像的其余部分几乎没有改变——这证明了 GAN 能够以这样一种方式组织潜在空间中的点,以便将各个特征解耦。

图 4-17。当BlondNot Blond向量附加到潜在样本时,CGAN 的输出
提示

如果您的数据集中有标签,通常最好将它们包含在 GAN 的输入中,即使您不一定需要将生成的输出条件化为标签,因为它们往往会提高生成的图像质量。您可以将标签视为像素输入的高度信息性扩展。

总结

在本章中,我们探讨了三种不同的生成对抗网络(GAN)模型:深度卷积 GAN(DCGAN)、更复杂的带有梯度惩罚的 Wasserstein GAN(WGAN-GP)和条件 GAN(CGAN)。

所有 GAN 都以生成器与鉴别器(或评论家)架构为特征,鉴别器试图“发现”真假图像之间的差异,生成器旨在欺骗鉴别器。通过平衡这两个对手的训练方式,GAN 生成器可以逐渐学习如何产生与训练集中的观察结果相似的图像。

我们首先看到如何训练 DCGAN 生成玩具积木的图像。它能够学习如何以图像形式真实地表示 3D 物体,包括阴影、形状和纹理的准确表示。我们还探讨了 GAN 训练可能失败的不同方式,包括模式坍塌和梯度消失。

然后,我们探讨了 Wasserstein 损失函数如何纠正了许多问题,并使 GAN 训练更加可预测和可靠。WGAN-GP 通过在损失函数中包含一个术语来将 1-Lipschitz 要求置于训练过程的核心,以将梯度范数拉向 1。

我们将 WGAN-GP 应用于人脸生成问题,并看到通过简单地从标准正态分布中选择点,我们可以生成新的人脸。这个采样过程与 VAE 非常相似,尽管 GAN 生成的人脸通常更加清晰,图像的不同部分之间的区别更大。

最后,我们构建了一个 CGAN,使我们能够控制生成的图像类型。这通过将标签作为输入传递给评论家和生成器来实现,从而为网络提供了所需的额外信息,以便根据给定的标签对生成的输出进行条件化。

总的来说,我们已经看到 GAN 框架非常灵活,能够适应许多有趣的问题领域。特别是,GAN 已经在图像生成领域取得了显著进展,有许多有趣的扩展到基础框架中,我们将在第十章中看到。

在下一章中,我们将探讨一种适合建模序列数据的不同生成模型家族——自回归模型。

¹ Ian J. Goodfellow 等人,“生成对抗网络”,2014 年 6 月 10 日,https://arxiv.org/abs/1406.2661

² Alec Radford 等人,“使用深度卷积生成对抗网络进行无监督表示学习”,2016 年 1 月 7 日,https://arxiv.org/abs/1511.06434

³ Augustus Odena 等人,“反卷积和棋盘伪影”,2016 年 10 月 17 日,https://distill.pub/2016/deconv-checkerboard

⁴ Martin Arjovsky 等人,“Wasserstein GAN”,2017 年 1 月 26 日,https://arxiv.org/abs/1701.07875

⁵ Ishaan Gulrajani 等人,“改进的 Wasserstein GANs 训练”,2017 年 3 月 31 日,https://arxiv.org/abs/1704.00028

⁶ Mehdi Mirza 和 Simon Osindero,“条件生成对抗网络”,2014 年 11 月 6 日,https://arxiv.org/abs/1411.1784

第五章:自回归模型

到目前为止,我们已经探讨了两种涉及潜变量的生成模型家族——变分自动编码器(VAEs)和生成对抗网络(GANs)。在这两种情况下,引入了一个新变量,其分布易于抽样,模型学习如何将此变量解码回原始领域。

现在我们将把注意力转向自回归模型——一类通过将生成建模问题简化为一个顺序过程的模型家族。自回归模型将预测条件放在序列中的先前值上,而不是在潜在随机变量上。因此,它们试图明确地对数据生成分布建模,而不是对其进行近似(如 VAEs 的情况)。

在本章中,我们将探讨两种不同的自回归模型:长短期记忆网络和 PixelCNN。我们将把 LSTM 应用于文本数据,将 PixelCNN 应用于图像数据。我们将在第九章中详细介绍另一个非常成功的自回归模型 Transformer。

介绍

为了理解 LSTM 的工作原理,我们将首先访问一个奇怪的监狱,那里的囚犯们组成了一个文学社团...​

Sopp 先生及其众包寓言的故事是对一种臭名昭著的用于文本等序列数据的自回归技术的类比:长短期记忆网络。

长短期记忆网络(LSTM)

LSTM 是一种特殊类型的循环神经网络(RNN)。RNN 包含一个循环层(或*单元),能够通过使其在特定时间步的输出成为下一个时间步的输入的一部分来处理序列数据。

当 RNN 首次引入时,循环层非常简单,仅包含一个 tanh 运算符,确保在时间步之间传递的信息在-1 和 1 之间缩放。然而,这种方法被证明存在梯度消失问题,并且在处理长序列数据时不具备良好的可扩展性。

LSTM 单元最初是在 1997 年由 Sepp Hochreiter 和 Jürgen Schmidhuber 的一篇论文中首次引入的。¹在这篇论文中,作者描述了 LSTM 不会像普通 RNN 那样遭受梯度消失问题,并且可以在数百个时间步长的序列上进行训练。自那时以来,LSTM 架构已经被改进和改良,变体如门控循环单元(本章后面讨论)现在被广泛应用并作为 Keras 中的层可用。

LSTM 已经应用于涉及序列数据的各种问题,包括时间序列预测、情感分析和音频分类。在本章中,我们将使用 LSTM 来解决文本生成的挑战。

运行此示例的代码

此示例的代码可以在位于书籍存储库中的 Jupyter 笔记本中找到,路径为notebooks/05_autoregressive/01_lstm/lstm.ipynb

食谱数据集

我们将使用通过 Kaggle 提供的Epicurious 食谱数据集。这是一个包含超过 20,000 个食谱的数据集,附带有营养信息和配料清单等元数据。

您可以通过在书籍存储库中运行 Kaggle 数据集下载脚本来下载数据集,如示例 5-1 所示。这将把食谱和相关元数据保存到本地的/data文件夹中。

示例 5-1。下载 Epicurious 食谱数据集
bash scripts/download_kaggle_data.sh hugodarwood epirecipes

`示例 5-2 展示了如何加载和过滤数据,以便只保留具有标题和描述的食谱。示例中给出了一个食谱文本字符串,详见示例 5-3。

示例 5-2。加载数据
with open('/app/data/epirecipes/full_format_recipes.json') as json_data:
    recipe_data = json.load(json_data)

filtered_data = [
    'Recipe for ' + x['title']+ ' | ' + ' '.join(x['directions'])
    for x in recipe_data
    if 'title' in x
    and x['title'] is not None
    and 'directions' in x
    and x['directions'] is not None
]
示例 5-3。来自食谱数据集的文本字符串
Recipe for Ham Persillade with Mustard Potato Salad and Mashed Peas  | Chop enough
parsley leaves to measure 1 tablespoon; reserve. Chop remaining leaves and stems
and simmer with broth and garlic in a small saucepan, covered, 5 minutes.
Meanwhile, sprinkle gelatin over water in a medium bowl and let soften 1 minute.
Strain broth through a fine-mesh sieve into bowl with gelatin and stir to dissolve.
Season with salt and pepper. Set bowl in an ice bath and cool to room temperature,
stirring. Toss ham with reserved parsley and divide among jars. Pour gelatin on top
and chill until set, at least 1 hour. Whisk together mayonnaise, mustard, vinegar,
1/4 teaspoon salt, and 1/4 teaspoon pepper in a large bowl. Stir in celery,
cornichons, and potatoes. Pulse peas with marjoram, oil, 1/2 teaspoon pepper, and
1/4 teaspoon salt in a food processor to a coarse mash. Layer peas, then potato
salad, over ham.

在看如何在 Keras 中构建 LSTM 网络之前,我们必须先快速了解文本数据的结构以及它与本书中迄今为止看到的图像数据有何不同。## 处理文本数据

文本和图像数据之间存在几个关键差异,这意味着许多适用于图像数据的方法并不适用于文本数据。特别是:

  • 文本数据由离散块(字符或单词)组成,而图像中的像素是连续色谱中的点。我们可以轻松地将绿色像素变成蓝色,但我们不清楚应该如何使单词“猫”更像单词“狗”,例如。这意味着我们可以轻松地将反向传播应用于图像数据,因为我们可以计算损失函数相对于单个像素的梯度,以确定像素颜色应该如何改变以最小化损失的方向。对于离散文本数据,我们不能明显地以同样的方式应用反向传播,因此我们需要找到解决这个问题的方法。

  • 文本数据具有时间维度但没有空间维度,而图像数据具有两个空间维度但没有时间维度。文本数据中单词的顺序非常重要,单词倒过来就没有意义,而图像通常可以翻转而不影响内容。此外,单词之间通常存在长期的顺序依赖关系,模型需要捕捉这些依赖关系:例如,回答问题或延续代词的上下文。对于图像数据,所有像素可以同时处理。

  • 文本数据对个体单位(单词或字符)的微小变化非常敏感。图像数据通常对个体像素单位的变化不太敏感——即使一些像素被改变,房子的图片仍然可以被识别为房子——但是对于文本数据,即使改变几个单词也可能极大地改变段落的含义,或使其毫无意义。这使得训练模型生成连贯文本非常困难,因为每个单词对段落的整体含义至关重要。

  • 文本数据具有基于规则的语法结构,而图像数据不遵循有关如何分配像素值的固定规则。例如,在任何情况下写“猫坐在上面”都没有语法意义。还有一些语义规则极其难以建模;即使从语法上讲,“我在海滩上”这个陈述没有问题,但意义上是不通顺的。

基于文本的生成式深度学习的进展

直到最近,大多数最复杂的生成式深度学习模型都集中在图像数据上,因为前面列表中提到的许多挑战甚至超出了最先进技术的范围。然而,在过去的五年中,在基于文本的生成式深度学习领域取得了惊人的进展,这要归功于 Transformer 模型架构的引入,我们将在第九章中探讨。

考虑到这些要点,让我们现在来看看我们需要采取哪些步骤,以便将文本数据整理成适合训练 LSTM 网络的形式。

标记化

第一步是清理和标记化文本。标记化是将文本分割成单独的单位,如单词或字符的过程。

如何对文本进行标记化取决于您尝试使用文本生成模型实现什么目标。使用单词和字符标记都有利弊,您的选择将影响您在建模之前需要如何清理文本以及模型输出。

如果使用单词标记:

  • 所有文本都可以转换为小写,以确保句子开头的大写单词与句子中间出现的相同单词以相同方式进行标记化。然而,在某些情况下,这可能不是理想的;例如,一些专有名词,如姓名或地点,可能受益于保持大写,以便它们被独立标记化。

  • 文本词汇(训练集中不同单词的集合)可能非常庞大,有些单词可能非常稀疏,甚至可能只出现一次。将稀疏单词替换为未知单词的标记可能是明智的选择,而不是将它们作为单独的标记包含在内,以减少神经网络需要学习的权重数量。

  • 单词可以进行词干处理,意味着它们被简化为最简单的形式,以便动词的不同时态保持标记化在一起。例如,browsebrowsingbrowsesbrowsed都将被词干处理为brows

  • 您需要将标点标记化,或者完全删除它。

  • 使用单词标记化意味着模型永远无法预测训练词汇表之外的单词。

如果您使用字符标记:

  • 模型可能生成字符序列,形成训练词汇表之外的新单词——在某些情况下,这可能是可取的,但在其他情况下则不是。

  • 大写字母可以转换为它们的小写对应词,也可以保留为单独的标记。

  • 使用字符标记时,词汇量通常较小。这对模型训练速度有益,因为最终输出层中需要学习的权重较少。

在这个示例中,我们将使用小写单词标记化,不进行词干处理。我们还将标记化标点符号,因为我们希望模型能够预测何时结束句子或使用逗号,例如。

示例 5-4 中的代码清理并标记文本。

示例 5-4。标记化
def pad_punctuation(s):
    s = re.sub(f"([{string.punctuation}])", r' \1 ', s)
    s = re.sub(' +', ' ', s)
    return s

text_data = [pad_punctuation(x) for x in filtered_data] # ①

text_ds = tf.data.Dataset.from_tensor_slices(text_data).batch(32).shuffle(1000) # ②

vectorize_layer = layers.TextVectorization( # ③
    standardize = 'lower',
    max_tokens = 10000,
    output_mode = "int",
    output_sequence_length = 200 + 1,
)

vectorize_layer.adapt(text_ds) # ④
vocab = vectorize_layer.get_vocabulary() # ⑤

填充标点符号,将它们视为单独的单词。

转换为 TensorFlow 数据集。

创建一个 Keras TextVectorization层,将文本转换为小写,为最常见的 10,000 个单词分配相应的整数标记,并将序列修剪或填充到 201 个标记长。

TextVectorization层应用于训练数据。

vocab变量存储一个单词标记列表。

在标记化后,一个配方的示例显示在示例 5-5 中。我们用于训练模型的序列长度是训练过程的一个参数。在这个示例中,我们选择使用长度为 200 的序列长度,因此我们将配方填充或裁剪到比这个长度多一个,以便我们创建目标变量(在下一节中详细介绍)。为了实现这个期望的长度,向量的末尾用零填充。

停止标记

0标记被称为停止标记,表示文本字符串已经结束。

示例 5-5。示例 5-3 中的配方进行了标记化
[  26   16  557    1    8  298  335  189    4 1054  494   27  332  228
  235  262    5  594   11  133   22  311    2  332   45  262    4  671
    4   70    8  171    4   81    6    9   65   80    3  121    3   59
   12    2  299    3   88  650   20   39    6    9   29   21    4   67
  529   11  164    2  320  171  102    9  374   13  643  306   25   21
    8  650    4   42    5  931    2   63    8   24    4   33    2  114
   21    6  178  181 1245    4   60    5  140  112    3   48    2  117
  557    8  285  235    4  200  292  980    2  107  650   28   72    4
  108   10  114    3   57  204   11  172    2   73  110  482    3  298
    3  190    3   11   23   32  142   24    3    4   11   23   32  142
   33    6    9   30   21    2   42    6  353    3 3224    3    4  150
    2  437  494    8 1281    3   37    3   11   23   15  142   33    3
    4   11   23   32  142   24    6    9  291  188    5    9  412  572
    2  230  494    3   46  335  189    3   20  557    2    0    0    0
    0    0    0    0    0]

在示例 5-6 中,我们可以看到一部分标记列表映射到它们各自的索引。该层将0标记保留为填充(即停止标记),将1标记保留为超出前 10000 个单词的未知单词(例如,persillade)。其他单词按频率顺序分配标记。要包含在词汇表中的单词数量也是训练过程的一个参数。包含的单词越多,您在文本中看到的未知标记就越少;但是,您的模型需要更大以容纳更大的词汇量。

示例 5-6。TextVectorization层的词汇表
0:
1: [UNK]
2: .
3: ,
4: and
5: to
6: in
7: the
8: with
9: a

创建训练集

我们的 LSTM 将被训练以预测序列中的下一个单词,给定此点之前的一系列单词。例如,我们可以向模型提供烤鸡配煮熟的的标记,期望模型输出一个合适的下一个单词(例如土豆,而不是香蕉)。

因此,我们可以简单地将整个序列向后移动一个标记,以创建我们的目标变量。

数据集生成步骤可以通过示例 5-7 中的代码实现。

示例 5-7。创建训练数据集
def prepare_inputs(text):
    text = tf.expand_dims(text, -1)
    tokenized_sentences = vectorize_layer(text)
    x = tokenized_sentences[:, :-1]
    y = tokenized_sentences[:, 1:]
    return x, y

train_ds = text_ds.map(prepare_inputs) # ①

创建包含食谱标记(输入)和相同向量向后移动一个标记(目标)的训练集。

LSTM 架构

整个 LSTM 模型的架构如表 5-1 所示。模型的输入是一系列整数标记,输出是 10,000 个词汇表中每个单词在序列中出现的概率。为了详细了解这是如何工作的,我们需要介绍两种新的层类型,即EmbeddingLSTM

表 5-1。LSTM 模型的摘要

层(类型) 输出形状 参数 #
InputLayer (None, None) 0
Embedding (None, None, 100) 1,000,000
LSTM (None, None, 128) 117,248
Dense (None, None, 10000) 1,290,000
总参数 2,407,248
可训练参数 2,407,248
不可训练参数 0

LSTM 的输入层

请注意,Input层不需要我们提前指定序列长度。批处理大小和序列长度都是灵活的(因此形状为(None, None))。这是因为所有下游层对通过的序列长度都是不可知的。

嵌入层

嵌入层本质上是一个查找表,将每个整数标记转换为长度为embedding_size的向量,如图 5-2 所示。模型通过权重学习查找向量。因此,该层学习的权重数量等于词汇表的大小乘以嵌入向量的维度(即 10,000 × 100 = 1,000,000)。

图 5-2。嵌入层是每个整数标记的查找表

我们将每个整数标记嵌入到连续向量中,因为这使得模型能够学习每个单词的表示,这些表示可以通过反向传播进行更新。我们也可以只对每个输入标记进行独热编码,但使用嵌入层更可取,因为它使得嵌入本身是可训练的,从而使模型在决定如何嵌入每个标记以提高性能时更加灵活。

因此,Input层将形状为[batch_size, seq_length]的整数序列张量传递给Embedding层,后者输出形状为[batch_size, seq_length, embedding_size]的张量。然后将其传递给LSTM层(图 5-3)。

图 5-3。单个序列在嵌入层中流动

LSTM 层

要理解 LSTM 层,我们首先必须看一下通用循环层的工作原理。

循环层具有特殊属性,能够处理顺序输入数据x 1 , , x n。随着序列中的每个元素x t逐个时间步通过,它会更新其隐藏状态h t

隐藏状态是一个向量,其长度等于细胞中的单元数——它可以被视为细胞对序列的当前理解。在时间步t,细胞使用先前的隐藏状态值h t-1,以及当前时间步的数据x t,产生一个更新的隐藏状态向量h t。这个循环过程持续到序列结束。一旦序列结束,该层输出细胞的最终隐藏状态h n,然后传递给网络的下一层。这个过程在图 5-4 中显示。

图 5-4。循环层的简单图示

为了更详细地解释这一点,让我们展开这个过程,这样我们就可以看到单个序列是如何通过该层传递的(图 5-5)。

细胞权重

重要的是要记住,这个图中的所有细胞共享相同的权重(因为它们实际上是相同的细胞)。这个图与图 5-4 没有区别;只是以不同的方式绘制了循环层的机制。

图 5-5。单个序列如何流经循环层

在这里,我们通过在每个时间步绘制细胞的副本来表示循环过程,并展示隐藏状态如何在流经细胞时不断更新。我们可以清楚地看到先前的隐藏状态如何与当前的顺序数据点(即当前嵌入的单词向量)混合以产生下一个隐藏状态。该层的输出是细胞的最终隐藏状态,在输入序列中的每个单词都被处理后。

警告

细胞的输出被称为隐藏状态是一个不幸的命名惯例——它并不真正隐藏,你不应该这样认为。事实上,最后一个隐藏状态是该层的整体输出,我们将利用这一点,稍后在本章中我们可以访问每个时间步的隐藏状态。

LSTM 细胞

现在我们已经看到了一个通用循环层是如何工作的,让我们来看看单个 LSTM 细胞的内部。

LSTM 细胞的工作是输出一个新的隐藏状态,h t,给定其先前的隐藏状态,h t-1,和当前的单词嵌入,x t。回顾一下,h t的长度等于 LSTM 中的单元数。这是在定义层时设置的一个参数,与序列的长度无关。

警告

确保不要混淆术语细胞单元。在 LSTM 层中有一个细胞,由它包含的单元数定义,就像我们早期故事中的囚犯细胞包含许多囚犯一样。我们经常将循环层绘制为展开的细胞链,因为这有助于可视化如何在每个时间步更新隐藏状态。

LSTM 单元格维护一个单元格状态,C t,可以被视为单元格对序列当前状态的内部信念。这与隐藏状态,h t,是不同的,隐藏状态最终在最后一个时间步输出。单元格状态与隐藏状态相同长度(单元格中的单元数)。

让我们更仔细地看一下单个单元格以及隐藏状态是如何更新的(图 5-6)。

隐藏状态在六个步骤中更新:

  1. 上一个时间步的隐藏状态,h t-1,和当前的单词嵌入,x t,被连接起来并通过遗忘门传递。这个门只是一个带有权重矩阵 W f,偏置 b f 和 sigmoid 激活函数的稠密层。得到的向量,f t,长度等于单元格中的单元数,并包含介于 0 和 1 之间的值,确定了应该保留多少先前的单元格状态,C t-1

    图 5-6. LSTM 单元格
    1. 连接的向量也通过一个输入门传递,类似于遗忘门,它是一个带有权重矩阵 W i,偏置 b i 和 sigmoid 激活函数的稠密层。这个门的输出,i t,长度等于单元格中的单元数,并包含介于 0 和 1 之间的值,确定了新信息将被添加到先前单元格状态,C t-1,的程度。

    2. 连接的向量也通过一个带有权重矩阵 W C,偏置 b C 和 tanh 激活函数的稠密层,生成一个向量 C ˜ t,其中包含单元格希望考虑保留的新信息。它的长度也等于单元格中的单元数,并包含介于-1 和 1 之间的值。

    3. f tC t-1 逐元素相乘并加到 i tC ˜ t 的逐元素乘积中。这代表了遗忘先前单元格状态的部分,并添加新的相关信息以生成更新后的单元格状态,C t

    4. 连接后的向量通过一个输出门传递:一个带有权重矩阵W o、偏置b o和 sigmoid 激活函数的稠密层。得到的向量o t的长度等于单元格中的单元数,并存储介于 0 和 1 之间的值,确定要从单元格中输出的更新后的单元格状态C t的多少。

    5. o t与更新后的单元格状态C t进行逐元素相乘,然后应用 tanh 激活函数产生新的隐藏状态h t

    6. Keras LSTM 层

      所有这些复杂性都包含在 Keras 的LSTM层类型中,因此您不必担心自己实现它!

      训练 LSTM

      构建、编译和训练 LSTM 的代码在 Example 5-8 中给出。

      Example 5-8. 构建、编译和训练 LSTM
      inputs = layers.Input(shape=(None,), dtype="int32") # ①
      x = layers.Embedding(10000, 100)(inputs) # ②
      x = layers.LSTM(128, return_sequences=True)(x) # ③
      outputs = layers.Dense(10000, activation = 'softmax')(x) # ④
      lstm = models.Model(inputs, outputs) # ⑤
      
      loss_fn = losses.SparseCategoricalCrossentropy()
      lstm.compile("adam", loss_fn) # ⑥
      lstm.fit(train_ds, epochs=25) # ⑦
      

      Input层不需要我们提前指定序列长度(可以是灵活的),所以我们使用None作为占位符。

      Embedding层需要两个参数,词汇量的大小(10,000 个标记)和嵌入向量的维度(100)。

      LSTM 层要求我们指定隐藏向量的维度(128)。我们还选择返回完整的隐藏状态序列,而不仅仅是最终时间步的隐藏状态。

      Dense层将每个时间步的隐藏状态转换为下一个标记的概率向量。

      整体的Model在给定一系列标记的输入序列时预测下一个标记。它为序列中的每个标记执行此操作。

      该模型使用SparseCategoricalCrossentropy损失进行编译——这与分类交叉熵相同,但在标签为整数而不是独热编码向量时使用。

      模型适合训练数据集。

      在 Figure 5-7 中,您可以看到 LSTM 训练过程的前几个时期——请注意随着损失指标下降,示例输出变得更加易懂。Figure 5-8 显示了整个训练过程中交叉熵损失指标的下降。

      Figure 5-7. LSTM 训练过程的前几个时期

      Figure 5-8. LSTM 训练过程中的交叉熵损失指标按时期

      LSTM 的分析

      现在我们已经编译和训练了 LSTM,我们可以开始使用它通过以下过程生成长文本字符串:

      1. 用现有的单词序列喂给网络,并要求它预测下一个单词。

      2. 将这个单词附加到现有序列并重复。

      网络将为每个单词输出一组概率,我们可以从中进行采样。因此,我们可以使文本生成具有随机性,而不是确定性。此外,我们可以引入一个温度参数到采样过程中,以指示我们希望过程有多确定性。

      温度参数

      接近 0 的温度使采样更加确定性(即,具有最高概率的单词很可能被选择),而温度为 1 意味着每个单词都以模型输出的概率被选择。

      这是通过在示例 5-9 中的代码实现的,该代码创建了一个回调函数,可以在每个训练周期结束时用于生成文本。

      示例 5-9。TextGenerator回调函数
      class TextGenerator(callbacks.Callback):
          def __init__(self, index_to_word, top_k=10):
              self.index_to_word = index_to_word
              self.word_to_index = {
                  word: index for index, word in enumerate(index_to_word)
              } # ①
      
          def sample_from(self, probs, temperature): # ②
              probs = probs ** (1 / temperature)
              probs = probs / np.sum(probs)
              return np.random.choice(len(probs), p=probs), probs
      
          def generate(self, start_prompt, max_tokens, temperature):
              start_tokens = [
                  self.word_to_index.get(x, 1) for x in start_prompt.split()
              ] # ③
              sample_token = None
              info = []
              while len(start_tokens) < max_tokens and sample_token != 0: # ④
                  x = np.array([start_tokens])
                  y = self.model.predict(x) # ⑤
                  sample_token, probs = self.sample_from(y[0][-1], temperature) # ⑥
                  info.append({'prompt': start_prompt , 'word_probs': probs})
                  start_tokens.append(sample_token) # ⑦
                  start_prompt = start_prompt + ' ' + self.index_to_word[sample_token]
              print(f"\ngenerated text:\n{start_prompt}\n")
              return info
      
          def on_epoch_end(self, epoch, logs=None):
              self.generate("recipe for", max_tokens = 100, temperature = 1.0)
      

      创建一个反向词汇映射(从单词到标记)。

      此函数使用temperature缩放因子更新概率。

      起始提示是您想要给模型以开始生成过程的一串单词(例如,recipe for)。首先将这些单词转换为标记列表。

      序列生成直到达到max_tokens长度或产生停止令牌(0)为止。

      模型输出每个单词成为序列中下一个单词的概率。

      概率通过采样器传递以输出下一个单词,由temperature参数化。

      我们将新单词附加到提示文本中,准备进行生成过程的下一次迭代。

      让我们看看这在实际中是如何运作的,使用两个不同的温度值(图 5-9)。

      图 5-9。在temperature = 1.0temperature = 0.2时生成的输出

      关于这两段文字有几点需要注意。首先,两者在风格上与原始训练集中的食谱相似。它们都以食谱标题开头,并包含通常语法正确的结构。不同之处在于,温度为 1.0 的生成文本更加冒险,因此比温度为 0.2 的示例不够准确。因此,使用温度为 1.0 生成多个样本将导致更多的变化,因为模型正在从具有更大方差的概率分布中进行抽样。

      为了证明这一点,图 5-10 显示了一系列提示的前五个具有最高概率的标记,对于两个温度值。

      图 5-10。在不同序列后的单词概率分布,对于温度值为 1.0 和 0.2

      该模型能够在一系列上下文中生成下一个最可能的单词的适当分布。例如,即使模型从未被告知过名词、动词或数字等词类,它通常能够将单词分为这些类别并以语法正确的方式使用它们。

      此外,该模型能够选择一个适当的动词来开始食谱说明,这取决于前面的标题。对于烤蔬菜,它选择preheatprepareheatputcombine作为最可能的可能性,而对于冰淇淋,它选择incombinestirwhiskmix。这表明该模型对于根据其成分而异的食谱之间的差异具有一定的上下文理解。

      还要注意temperature = 0.2示例的概率更加倾向于第一个选择标记。这就是为什么当温度较低时,生成的变化通常较少的原因。

      虽然我们的基本 LSTM 模型在生成逼真文本方面表现出色,但很明显它仍然难以理解所生成单词的一些语义含义。它引入了一些不太可能搭配在一起的成分(例如,酸味日本土豆、山核桃碎屑和果冻)!在某些情况下,这可能是可取的——比如,如果我们希望我们的 LSTM 生成有趣和独特的单词模式——但在其他情况下,我们需要我们的模型对单词如何组合在一起以及在文本中引入的想法有更深入的理解和更长的记忆。

      在下一节中,我们将探讨如何改进我们的基本 LSTM 网络。在第九章中,我们将看一看一种新型的自回归模型,Transformer,将语言建模提升到一个新的水平。

      前一节中的模型是一个简单的示例,展示了如何训练 LSTM 学习如何以给定风格生成文本。在本节中,我们将探讨这个想法的几个扩展。

      堆叠循环网络

      我们刚刚看到的网络包含一个单独的 LSTM 层,但我们也可以训练具有堆叠 LSTM 层的网络,以便从文本中学习更深层次的特征。

      为了实现这一点,我们只需在第一层之后引入另一层 LSTM。第二层 LSTM 可以使用第一层的隐藏状态作为其输入数据。这在图 5-11 中显示,整体模型架构在表 5-2 中显示。

      图 5-11。多层 RNN 的示意图:g[t]表示第一层的隐藏状态,h[t]表示第二层的隐藏状态

      表 5-2。堆叠 LSTM 的模型摘要

      层(类型) 输出形状 参数 #
      输入层 (None, None) 0
      嵌入 (None, None, 100) 1,000,000
      LSTM (None, None, 128) 117,248
      LSTM (None, None, 128) 131,584
      稠密 (None, None, 10000) 1,290,000
      总参数 2,538,832
      可训练参数 2,538,832
      不可训练参数 0

      构建堆叠 LSTM 的代码在示例 5-10 中给出。

      示例 5-10。构建堆叠 LSTM
      text_in = layers.Input(shape = (None,))
      embedding = layers.Embedding(total_words, embedding_size)(text_in)
      x = layers.LSTM(n_units, return_sequences = True)(x)
      x = layers.LSTM(n_units, return_sequences = True)(x)
      probabilites = layers.Dense(total_words, activation = 'softmax')(x)
      model = models.Model(text_in, probabilites)
      

      门控循环单元

      另一种常用的 RNN 层是门控循环单元(GRU)。² 与 LSTM 单元的主要区别如下:

      1. 遗忘输入门被重置更新门替换。

      2. 没有细胞状态输出门,只有从细胞输出的隐藏状态

      隐藏状态通过四个步骤更新,如图 5-12 所示。

      图 5-12。单个 GRU 单元

      过程如下:

      1. 上一个时间步的隐藏状态,h t-1,和当前单词嵌入,x t,被串联并用于创建重置门。这个门是一个密集层,带有权重矩阵W r和一个 sigmoid 激活函数。得到的向量,r t,长度等于细胞中的单元数,并存储介于 0 和 1 之间的值,确定应该将多少上一个隐藏状态,h t-1,传递到新信念的计算中。

      2. 重置门应用于隐藏状态,h t-1,并与当前单词嵌入x t连接。然后将该向量馈送到具有权重矩阵W和 tanh 激活函数的密集层,以生成一个向量h ˜ t,其中存储了细胞的新信念。它的长度等于细胞中的单元数,并存储在-1 和 1 之间的值。

      3. 前一个时间步的隐藏状态h t-1和当前单词嵌入x t的连接也用于创建更新门。该门是一个具有权重矩阵W z和 sigmoid 激活的密集层。生成的向量z t的长度等于细胞中的单元数,并存储在 0 和 1 之间的值,用于确定新信念h ˜ t的多少要混合到当前隐藏状态h t-1中。

      4. 细胞的新信念h ˜ t和当前隐藏状态h t-1按照更新门z t确定的比例混合,以产生更新后的隐藏状态h t,从细胞中输出。

        双向细胞

        对于预测问题,在推断时模型可以获得整个文本,没有理由只在正向方向处理序列 - 它同样可以被反向处理。Bidirectional层通过存储两组隐藏状态来利用这一点:一组是由序列在通常的正向方向处理时产生的,另一组是在序列被反向处理时产生的。这样,该层可以从给定时间步之前和之后的信息中学习。

        在 Keras 中,这被实现为对循环层的包装,如示例 5-11 所示。

        示例 5-11。构建双向 GRU 层
        layer = layers.Bidirectional(layers.GRU(100))
        

        隐藏状态

        结果层中的隐藏状态是长度等于包装细胞中单元数两倍的向量(正向和反向隐藏状态的连接)。因此,在此示例中,该层的隐藏状态是长度为 200 的向量。

        到目前为止,我们只将自回归模型(LSTMs)应用于文本数据。在下一节中,我们将看到如何使用自回归模型来生成图像。

        PixelCNN

        2016 年,van den Oord 等人³提出了一种通过预测下一个像素的可能性来逐像素生成图像的模型。该模型称为PixelCNN,可以训练以自回归方式生成图像。

        我们需要介绍两个新概念来理解 PixelCNN - 掩码卷积层残差块

        运行此示例的代码

        此示例的代码可以在位于书籍存储库中的 Jupyter 笔记本中找到,路径为notebooks/05_autoregressive/02_pixelcnn/pixelcnn.ipynb

        该代码改编自由 ADMoreau 创建的出色的PixelCNN 教程,可在 Keras 网站上找到。

        掩码卷积层

        正如我们在第二章中看到的,卷积层可以通过应用一系列滤波器从图像中提取特征。在特定像素处的层的输出是滤波器权重乘以围绕像素中心的小正方形上一层值的加权和。这种方法可以检测边缘和纹理,而在更深的层中,可以检测形状和更高级的特征。

        虽然卷积层在特征检测方面非常有用,但不能直接以自回归的方式使用,因为像素上没有顺序。它们依赖于所有像素都被平等对待的事实——没有像素被视为图像的开始结束。这与我们在本章中已经看到的文本数据形成对比,其中令牌有明确的顺序,因此可以轻松应用循环模型,如 LSTM。

        为了能够以自回归的方式将卷积层应用于图像生成,我们必须首先对像素进行排序,并确保滤波器只能看到在问题像素之前的像素。然后,我们可以通过将卷积滤波器应用于当前图像来一次生成一个像素,以预测下一个像素的值。

        我们首先需要为像素选择一个顺序——一个明智的建议是按照从左上到右下的顺序对像素进行排序,首先沿着行移动,然后沿着列向下移动。

        然后,我们对卷积滤波器进行掩码处理,以便每个像素处的层的输出仅受到在问题像素之前的像素值的影响。这是通过将一个由 1 和 0 组成的掩码与滤波器权重矩阵相乘来实现的,以便在目标像素之后的任何像素的值都被置为零。

        在 PixelCNN 中实际上有两种不同类型的掩码,如图 5-13 所示:

        • 类型 A,中心像素的值被掩码

        • 类型 B,中心像素的值被掩码

        图 5-13。左:卷积滤波器掩码;右:应用于一组像素以预测中心像素值分布的掩码(来源:van den Oord 等人,2016)

        初始的掩码卷积层(即直接应用于输入图像的层)不能使用中心像素,因为这正是我们希望网络猜测的像素!然而,后续层可以使用中心像素,因为这将仅根据原始输入图像中前面像素的信息计算出来。

        我们可以在示例 5-12 中看到如何使用 Keras 构建MaskedConvLayer

        示例 5-12。Keras 中的MaskedConvLayer
        class MaskedConvLayer(layers.Layer):
            def __init__(self, mask_type, **kwargs):
                super(MaskedConvLayer, self).__init__()
                self.mask_type = mask_type
                self.conv = layers.Conv2D(**kwargs) # ①
        
            def build(self, input_shape):
                self.conv.build(input_shape)
                kernel_shape = self.conv.kernel.get_shape()
                self.mask = np.zeros(shape=kernel_shape) # ②
                self.mask[: kernel_shape[0] // 2, ...] = 1.0 # ③
                self.mask[kernel_shape[0] // 2, : kernel_shape[1] // 2, ...] = 1.0 # ④
                if self.mask_type == "B":
                    self.mask[kernel_shape[0] // 2, kernel_shape[1] // 2, ...] = 1.0 # ⑤
        
            def call(self, inputs):
                self.conv.kernel.assign(self.conv.kernel * self.mask) # ⑥
                return self.conv(inputs)
        

        MaskedConvLayer基于普通的Conv2D层。

        掩码初始化为全零。

        前面行中的像素将被一个 1 解除掩码。

        前面列中在同一行中的像素将被一个 1 解除掩码。

        如果掩码类型为 B,则中心像素将被一个 1 解除掩码。

        掩码与滤波器权重相乘。

        请注意,这个简化的例子假设是灰度图像(即,只有一个通道)。如果是彩色图像,我们将有三个颜色通道,我们也可以对它们进行排序,例如,红色通道在蓝色通道之前,蓝色通道在绿色通道之前。

        残差块

        现在我们已经看到如何对卷积层进行掩码,我们可以开始构建我们的 PixelCNN。我们将使用的核心构建块是残差块。

        残差块是一组层,其中输出在传递到网络的其余部分之前添加到输入中。换句话说,输入有一条快速通道到输出,而无需经过中间层——这被称为跳跃连接。包含跳跃连接的理由是,如果最佳转换只是保持输入不变,这可以通过简单地将中间层的权重置零来实现。如果没有跳跃连接,网络将不得不通过中间层找到一个恒等映射,这要困难得多。

        我们在 PixelCNN 中的残差块的图示在图 5-14 中显示。

        图 5-14。一个 PixelCNN 残差块(箭头旁边是滤波器的数量,层旁边是滤波器大小)

        我们可以使用示例 5-13 中显示的代码构建一个ResidualBlock

        示例 5-13。一个ResidualBlock
        class ResidualBlock(layers.Layer):
            def __init__(self, filters, **kwargs):
                super(ResidualBlock, self).__init__(**kwargs)
                self.conv1 = layers.Conv2D(
                    filters=filters // 2, kernel_size=1, activation="relu"
                ) # ①
                self.pixel_conv = MaskedConv2D(
                    mask_type="B",
                    filters=filters // 2,
                    kernel_size=3,
                    activation="relu",
                    padding="same",
                ) # ②
                self.conv2 = layers.Conv2D(
                    filters=filters, kernel_size=1, activation="relu"
                ) # ③
        
            def call(self, inputs):
                x = self.conv1(inputs)
                x = self.pixel_conv(x)
                x = self.conv2(x)
                return layers.add([inputs, x]) # ④
        

        初始的Conv2D层将通道数量减半。

        Type B MaskedConv2D层,核大小为 3,仅使用来自五个像素的信息——上面一行中的三个像素,左边一个像素和焦点像素本身。

        最终的Conv2D层将通道数量加倍,以再次匹配输入形状。

        卷积层的输出与输入相加——这是跳跃连接。

        训练 PixelCNN

        在示例 5-14 中,我们组合了整个 PixelCNN 网络,大致遵循原始论文中的结构。在原始论文中,输出层是一个有 256 个滤波器的Conv2D层,使用 softmax 激活。换句话说,网络试图通过预测正确的像素值来重新创建其输入,有点像自动编码器。不同之处在于,PixelCNN 受到限制,以便不允许来自早期像素的信息流通过影响每个像素的预测,这是由于网络设计方式,使用MaskedConv2D层。

        这种方法的一个挑战是网络无法理解,比如说,像素值 200 非常接近像素值 201。它必须独立学习每个像素输出值,这意味着即使对于最简单的数据集,训练也可能非常缓慢。因此,在我们的实现中,我们简化输入,使每个像素只能取四个值之一。这样,我们可以使用一个有 4 个滤波器的Conv2D输出层,而不是 256 个。

        示例 5-14。PixelCNN 架构
        inputs = layers.Input(shape=(16, 16, 1)) # ①
        x = MaskedConv2D(mask_type="A"
                           , filters=128
                           , kernel_size=7
                           , activation="relu"
                           , padding="same")(inputs)# ②
        
        for _ in range(5):
            x = ResidualBlock(filters=128)(x) # ③
        
        for _ in range(2):
            x = MaskedConv2D(
                mask_type="B",
                filters=128,
                kernel_size=1,
                strides=1,
                activation="relu",
                padding="valid",
            )(x) # ④
        
        out = layers.Conv2D(
            filters=4, kernel_size=1, strides=1, activation="softmax", padding="valid"
        )(x) # ⑤
        
        pixel_cnn = models.Model(inputs, out) # ⑥
        
        adam = optimizers.Adam(learning_rate=0.0005)
        pixel_cnn.compile(optimizer=adam, loss="sparse_categorical_crossentropy")
        
        pixel_cnn.fit(
            input_data
            , output_data
            , batch_size=128
            , epochs=150
        ) # ⑦
        

        模型的Input是一个尺寸为 16×16×1 的灰度图像,输入值在 0 到 1 之间缩放。

        第一个 Type A MaskedConv2D层,核大小为 7,使用来自 24 个像素的信息——在焦点像素上面的三行中的 21 个像素和左边的 3 个像素(焦点像素本身不使用)。

        五个ResidualBlock层组被顺序堆叠。

        两个 Type B MaskedConv2D层,核大小为 1,作为每个像素通道数量的Dense层。

        最终的Conv2D层将通道数减少到四——本示例中的像素级别数。

        Model被构建为接受一幅图像并输出相同尺寸的图像。

        拟合模型——input_data在范围[0,1](浮点数)内缩放;output_data在范围[0,3](整数)内缩放。

        PixelCNN 的分析

        我们可以在我们在第三章中遇到的 Fashion-MNIST 数据集上训练我们的 PixelCNN。要生成新图像,我们需要要求模型根据所有先前像素预测下一个像素,逐个像素进行预测。与诸如变分自动编码器的模型相比,这是一个非常缓慢的过程!对于一幅 32×32 的灰度图像,我们需要使用模型进行 1,024 次顺序预测,而不是我们需要为 VAE 进行的单次预测。这是自回归模型如 PixelCNN 的主要缺点之一——由于采样过程的顺序性质,它们从中采样速度较慢。

        因此,我们使用图像尺寸为 16×16,而不是 32×32,以加快生成新图像的速度。生成回调类如示例 5-15 所示。

        示例 5-15。使用 PixelCNN 生成新图像
        class ImageGenerator(callbacks.Callback):
            def __init__(self, num_img):
                self.num_img = num_img
        
            def sample_from(self, probs, temperature):
                probs = probs ** (1 / temperature)
                probs = probs / np.sum(probs)
                return np.random.choice(len(probs), p=probs)
        
            def generate(self, temperature):
                generated_images = np.zeros(
                    shape=(self.num_img,) + (pixel_cnn.input_shape)[1:]
                ) # ①
                batch, rows, cols, channels = generated_images.shape
        
                for row in range(rows):
                    for col in range(cols):
                        for channel in range(channels):
                            probs = self.model.predict(generated_images)[
                                :, row, col, :
                            ] # ②
                            generated_images[:, row, col, channel] = [
                                self.sample_from(x, temperature) for x in probs
                            ] # ③
                            generated_images[:, row, col, channel] /= 4 # ④
                return generated_images
        
            def on_epoch_end(self, epoch, logs=None):
                generated_images = self.generate(temperature = 1.0)
                display(
                    generated_images,
                    save_to = "./output/generated_img_%03d.png" % (epoch)
                s)
        
        img_generator_callback = ImageGenerator(num_img=10)
        

        从一批空白图像(全零)开始。

        循环遍历当前图像的行、列和通道,预测下一个像素值的分布。

        从预测分布中抽取一个像素级别(对于我们的示例,范围在[0,3]内)。

        将像素级别转换为范围[0,1]并覆盖当前图像中的像素值,准备好进行下一次循环迭代。

        在图 5-15 中,我们可以看到原始训练集中的几幅图像,以及由 PixelCNN 生成的图像。

        图 5-15。训练集中的示例图像和由 PixelCNN 模型生成的图像

        该模型在重新创建原始图像的整体形状和风格方面做得很好!令人惊讶的是,我们可以将图像视为一系列令牌(像素值),并应用自回归模型如 PixelCNN 来生成逼真的样本。

        如前所述,自回归模型的一个缺点是它们从中采样速度较慢,这就是为什么本书中提供了它们应用的一个简单示例。然而,正如我们将在第十章中看到的,更复杂形式的自回归模型可以应用于图像以产生最先进的输出。在这种情况下,缓慢的生成速度是为了获得卓越质量输出而必须付出的代价。

        自原始论文发表以来,PixelCNN 的架构和训练过程已经进行了几项改进。以下部分介绍了其中一项变化——使用混合分布,并演示了如何使用内置的 TensorFlow 函数训练带有此改进的 PixelCNN 模型。

        混合分布

        对于我们之前的示例,我们将 PixelCNN 的输出减少到只有 4 个像素级别,以确保网络不必学习 256 个独立像素值的分布,这将减慢训练过程。然而,这远非理想——对于彩色图像,我们不希望我们的画布仅限于少数可能的颜色。

        为了解决这个问题,我们可以使网络的输出成为混合分布,而不是对 256 个离散像素值进行 softmax,遵循 Salimans 等人提出的想法。4 混合分布简单地是两个或更多其他概率分布的混合。例如,我们可以有五个具有不同参数的逻辑分布的混合分布。混合分布还需要一个离散分类分布,表示选择混合中包含的每个分布的概率。示例显示在图 5-16 中。

        图 5-16。三个具有不同参数的正态分布的混合分布——三个正态分布上的分类分布为[0.5, 0.3, 0.2]

        要从混合分布中抽样,我们首先从分类分布中抽样以选择特定的子分布,然后以通常的方式从中抽样。这样,我们可以用相对较少的参数创建复杂的分布。例如,图 5-16 中的混合分布仅需要八个参数——两个用于分类分布,以及三个正态分布的均值和方差。这与定义整个像素范围上的分类分布所需的 255 个参数相比要少。

        方便地,TensorFlow Probability 库提供了一个函数,允许我们用一行代码创建具有混合分布输出的 PixelCNN。示例 5-16 说明了如何使用此函数构建 PixelCNN。

        运行此示例的代码

        此示例的代码可以在书籍存储库中的 Jupyter 笔记本notebooks/05_autoregressive/03_pixelcnn_md/pixelcnn_md.ipynb中找到。

        示例 5-16。使用 TensorFlow 函数构建 PixelCNN
        import tensorflow_probability as tfp
        
        dist = tfp.distributions.PixelCNN(
            image_shape=(32, 32, 1),
            num_resnet=1,
            num_hierarchies=2,
            num_filters=32,
            num_logistic_mix=5,
            dropout_p=.3,
        ) # ①
        
        image_input = layers.Input(shape=(32, 32, 1)) # ②
        
        log_prob = dist.log_prob(image_input)
        
        model = models.Model(inputs=image_input, outputs=log_prob) # ③
        model.add_loss(-tf.reduce_mean(log_prob)) # ④
        

        将 PixelCNN 定义为一个分布——即,输出层是由五个逻辑分布组成的混合分布。

        输入是大小为 32×32×1 的灰度图像。

        Model以灰度图像作为输入,并输出在 PixelCNN 计算的混合分布下图像的对数似然。

        损失函数是输入图像批次上的平均负对数似然。

        该模型的训练方式与以前相同,但这次接受整数像素值作为输入,范围为[0, 255]。可以使用sample函数从分布中生成输出,如示例 5-17 所示。

        示例 5-17。从 PixelCNN 混合分布中抽样
        dist.sample(10).numpy()
        

        示例生成的图像显示在图 5-17 中。与以前的示例不同的是,现在正在利用完整的像素值范围。

        图 5-17。使用混合分布输出的 PixelCNN 的输出

        总结

        在本章中,我们看到了自回归模型,如循环神经网络如何应用于生成模仿特定写作风格的文本序列,以及 PixelCNN 如何以顺序方式生成图像,每次一个像素。

        我们探索了两种不同类型的循环层——长短期记忆(LSTM)和门控循环单元(GRU)——并看到这些单元如何可以堆叠或双向化以形成更复杂的网络架构。我们构建了一个 LSTM 来使用 Keras 生成逼真的食谱,并看到如何操纵采样过程的温度以增加或减少输出的随机性。

        我们还看到了如何以自回归方式生成图像,使用了 PixelCNN。我们使用 Keras 从头开始构建了一个 PixelCNN,编写了掩膜卷积层和残差块,以允许信息在网络中流动,从而只能使用前面的像素来生成当前的像素。最后,我们讨论了 TensorFlow Probability 库提供了一个独立的 PixelCNN 函数,实现了混合分布作为输出层,使我们能够进一步改进学习过程。

        在下一章中,我们将探讨另一种生成建模家族,明确地对数据生成分布进行建模—正规化流模型。

        ¹ Sepp Hochreiter 和 Jürgen Schmidhuber, “长短期记忆,” 神经计算 9 (1997): 1735–1780, https://www.bioinf.jku.at/publications/older/2604.pdf.

        ² Kyunghyun Cho 等人, “使用 RNN 编码器-解码器学习短语表示进行统计机器翻译,” 2014 年 6 月 3 日, https://arxiv.org/abs/1406.1078.

        ³ Aaron van den Oord 等人, “像素递归神经网络,” 2016 年 8 月 19 日, https://arxiv.org/abs/1601.06759.

        ⁴ Tim Salimans 等人, “PixelCNN++: 使用离散化逻辑混合似然和其他修改改进 PixelCNN,” 2017 年 1 月 19 日, http://arxiv.org/abs/1701.05517.

        第六章:正规化流模型

        到目前为止,我们已经讨论了三类生成模型家族:变分自动编码器、生成对抗网络和自回归模型。每种模型都提出了不同的方法来解决建模分布 p(x)的挑战,要么通过引入一个可以轻松采样的潜变量(并在 VAE 中使用解码器或在 GAN 中使用生成器进行转换),要么通过可处理地将分布建模为前面元素值的函数(自回归模型)。

        在本章中,我们将介绍一种新的生成模型家族——正规化流模型。正如我们将看到的,正规化流与自回归模型和变分自动编码器都有相似之处。像自回归模型一样,正规化流能够明确且可处理地建模数据生成分布 p(x)。像变分自动编码器一样,正规化流试图将数据映射到一个更简单的分布,比如高斯分布。关键区别在于,正规化流对映射函数的形式施加了约束,使其可逆,因此可以用来生成新的数据点。

        在本章的第一节中,我们将详细探讨这个定义,然后使用 Keras 实现一个名为 RealNVP 的正规化流模型。我们还将看到如何扩展正规化流以创建更强大的模型,如 GLOW 和 FFJORD。

        介绍

        我们将从一个简短的故事开始,以阐明正规化流背后的关键概念。

        雅各布和 F.L.O.W.机器的故事是对正规化流模型的描述。现在让我们更详细地探讨正规化流的理论,然后在使用 Keras 实现一个实际示例之前。

        正规化流

        正规化流模型的动机与我们在第三章中探讨的变分自动编码器的动机类似。简而言之,在变分自动编码器中,我们学习一个编码器映射函数,将一个复杂分布映射到一个我们可以从中采样的简单分布。然后我们还学习一个解码器映射函数,从简单分布到复杂分布,这样我们可以通过从简单分布中采样一个点 z 并应用学习到的转换来生成一个新的数据点。从概率上讲,解码器建模 p(x|z),但编码器只是真实 p(z|x)的近似—编码器和解码器是两个完全不同的神经网络。

        在正规化流模型中,解码函数被设计为编码函数的精确逆函数且计算迅速,使得正规化流具有可处理性质。然而,神经网络默认情况下不是可逆函数。这引发了一个问题,即我们如何创建一个可逆过程,将一个复杂分布(如一组水彩画的数据生成分布)转换为一个更简单的分布(如钟形高斯分布),同时仍然利用深度学习的灵活性和强大性。

        为了回答这个问题,我们首先需要了解一种称为变量变换的技术。在本节中,我们将使用一个简单的二维示例,这样你可以看到归一化流是如何详细工作的。更复杂的例子只是基本技术的扩展。

        变量变换

        假设我们有一个在二维矩形X上定义的概率分布p X ( x )x = ( x 1 , x 2 )),如图 6-2 所示。

        图 6-2。在二维空间中定义的概率分布p X ( x ),在 2D(左)和 3D(右)中显示

        这个函数在分布的定义域上积分为 1(即x 1在范围[1, 4],x 2在范围[0, 2]),因此它代表了一个明确定义的概率分布。我们可以写成如下形式:

        0 2 1 4 p X ( x ) d x 1 d x 2 = 1

        假设我们想要移动和缩放这个分布,使其在一个单位正方形Z上定义。我们可以通过定义一个新变量z = ( z 1 , z 2 )和一个将X中的每个点映射到Z中的一个点的函数f来实现这一点:

        z = f ( x ) z 1 = x 1 -1 3 z 2 = x 2 2

        请注意,这个函数是可逆的。也就是说,有一个函数g,它将每个z映射回其对应的x。这对于变量变换是必不可少的,否则我们无法在两个空间之间一致地进行前向和后向映射。我们可以通过简单地重新排列定义f的方程来找到g,如图 6-3 所示。

        图 6-3。在XZ之间改变变量

        现在我们需要看看从XZ的变量变换如何影响概率分布p X ( x )。我们可以通过将定义g的方程代入p X ( x )来将其转换为一个以z为变量的函数p Z ( z )

        p Z ( z ) = ((3z 1 +1)-1)(2z 2 ) 9 = 2z 1 z 2 3

        然而,如果我们现在在单位正方形上对 p_Z(z)进行积分,我们会发现有问题!

        0 1 0 1 2z 1 z 2 3 d z 1 d z 2 = 1 6

        现在,转换后的函数 p_Z(z)不再是一个有效的概率分布,因为它只积分到 1/6。如果我们想要将数据上的复杂概率分布转换为一个简单的我们可以从中抽样的分布,我们必须确保它积分到 1。

        缺少的因子 6 是因为我们转换后的概率分布的定义域比原始定义域小了六倍——原始矩形 X 的面积为 6,而这被压缩成了只有面积为 1 的单位正方形 Z。因此,我们需要将新的概率分布乘以一个归一化因子,该因子等于面积(或在更高维度中的体积)的相对变化。

        幸运的是,有一种方法可以计算给定变换的体积变化——即变换的雅可比行列式的绝对值。让我们来解释一下!

        雅可比行列式

        函数 z=f(x)的雅可比矩阵是其一阶偏导数的矩阵,如下所示:

        J = z x = z 1 x 1 z 1 x n z m x 1 z m x n

        最好的方法是用我们的例子来解释。如果我们对 z1 关于 x1 进行偏导数,我们得到 1/3。如果我们对 z1 关于 x2 进行偏导数,我们得到 0。同样,如果我们对 z2 关于 x1 进行偏导数,我们得到 0。最后,如果我们对 z2 关于 x2 进行偏导数,我们得到 1/2。

        因此,我们函数的雅可比矩阵如下:

        J = 1 3 0 0 1 2

        行列式仅对方阵有定义,并且等于通过将矩阵表示的变换应用于单位(超)立方体而创建的平行六面体的有符号体积。在二维中,这只是通过将矩阵表示的变换应用于单位正方形而创建的平行四边形的有符号面积。

        有一个用于计算具有 n 维的矩阵行列式的通用公式,其运行时间为𝒪(n³)。对于我们的例子,我们只需要二维的公式,如下所示:

        det a b c d = a d - b c

        因此,对于我们的示例,雅可比行列式的行列式是 1 3 × 1 2 - 0 × 0 = 1 6。这是我们需要确保变换后的概率分布仍然积分为 1 的缩放因子为 1/6!

        提示

        根据定义,行列式是有符号的——也就是说,它可以是负数。因此,我们需要取雅可比行列式的绝对值,以获得体积的相对变化。

        变量转换方程

        现在我们可以写下一个单一方程,描述在 XZ 之间变量转换的过程。这被称为变量转换方程(方程 6-1)。

        方程 6-1. 变量转换方程

        p X ( x ) = p Z ( z ) det z x

        这如何帮助我们构建一个生成模型?关键在于理解,如果 p Z ( z ) 是一个简单的分布,我们可以轻松地从中抽样(例如,高斯分布),那么理论上,我们所需要做的就是找到一个适当的可逆函数 f ( x ),可以将数据 X 映射到 Z,以及相应的逆函数 g ( z ),可以用来将抽样的 z 映射回原始域中的点 x。我们可以使用涉及雅可比行列式的前述方程找到数据分布 p ( x ) 的一个精确、可处理的公式。

        然而,在实践中应用时存在两个主要问题,我们首先需要解决!

        首先,计算高维矩阵的行列式在计算上是极其昂贵的——具体来说,是 𝒪 ( n 3 )。这在实践中是完全不切实际的,因为即使是小的 32×32 像素灰度图像也有 1024 个维度。

        其次,我们不清楚如何计算可逆函数 f ( x )。我们可以使用神经网络找到一些函数 f ( x ),但我们不能保证可以反转这个网络——神经网络只能单向工作!

        为了解决这两个问题,我们需要使用一种特殊的神经网络架构,确保变量转换函数 f 是可逆的,并且其行列式易于计算。

        我们将在接下来的部分中看到如何使用一种称为实值非体积保持(RealNVP)变换的技术来解决这个问题。

        RealNVP

        RealNVP 首次由 Dinh 等人在 2017 年提出。在这篇论文中,作者展示了如何构建一个神经网络,可以将复杂的数据分布转换为简单的高斯分布,同时具有可逆性和易于计算雅可比行列式的期望特性。

        运行此示例的代码

        此示例的代码可以在位于书籍存储库中的notebooks/06_normflow/01_realnvp/realnvp.ipynb的 Jupyter 笔记本中找到。

        该代码改编自由 Mandolini Giorgio Maria 等人创建的优秀RealNVP 教程,可在 Keras 网站上找到。

        两个 moons 数据集

        我们将在此示例中使用的数据集是由 Python 库sklearn中的make_moons函数创建的。这将创建一个嘈杂的 2D 点数据集,类似于两个新月形状,如图 6-4 所示。

        gdl2 0604

        图 6-4。二维中的两个 moons 数据集

        创建此数据集的代码在示例 6-1 中给出。

        示例 6-1。创建一个moons数据集
        data = datasets.make_moons(3000, noise=0.05)[0].astype("float32") # ①
        norm = layers.Normalization()
        norm.adapt(data)
        normalized_data = norm(data) # ②
        

        创建一个包含 3,000 个点的嘈杂、非标准化的 moons 数据集。

        将数据集归一化为均值为 0,标准差为 1。

        我们将构建一个 RealNVP 模型,可以生成遵循与两个 moons 数据集类似分布的 2D 点。虽然这是一个非常简单的例子,但它将帮助我们详细了解正规化流模型在实践中的工作方式。

        然而,首先,我们需要介绍一种新类型的层,称为耦合层。

        耦合层

        耦合层为其输入的每个元素产生一个比例和平移因子。换句话说,它产生两个与输入完全相同大小的张量,一个用于比例因子,一个用于平移因子,如图 6-5 所示。

        图 6-5。耦合层输出两个与输入相同形状的张量:一个缩放因子(s)和一个平移因子(t)

        为了为我们的简单示例构建自定义的Coupling层,我们可以堆叠Dense层以创建比例输出,并堆叠不同的Dense层以创建平移输出,如示例 6-2 所示。

        提示

        对于图像,Coupling层块使用Conv2D层而不是Dense层。

        示例 6-2。Keras 中的Coupling
        def Coupling():
            input_layer = layers.Input(shape=2) # ①
        
            s_layer_1 = layers.Dense(
                256, activation="relu", kernel_regularizer=regularizers.l2(0.01)
            )(input_layer) # ②
            s_layer_2 = layers.Dense(
                256, activation="relu", kernel_regularizer=regularizers.l2(0.01)
            )(s_layer_1)
            s_layer_3 = layers.Dense(
                256, activation="relu", kernel_regularizer=regularizers.l2(0.01)
            )(s_layer_2)
            s_layer_4 = layers.Dense(
                256, activation="relu", kernel_regularizer=regularizers.l2(0.01)
            )(s_layer_3)
            s_layer_5 = layers.Dense(
                2, activation="tanh", kernel_regularizer=regularizers.l2(0.01)
            )(s_layer_4) # ③
        
            t_layer_1 = layers.Dense(
                256, activation="relu", kernel_regularizer=regularizers.l2(0.01)
            )(input_layer) # ④
            t_layer_2 = layers.Dense(
                256, activation="relu", kernel_regularizer=regularizers.l2(0.01)
            )(t_layer_1)
            t_layer_3 = layers.Dense(
                256, activation="relu", kernel_regularizer=regularizers.l2(0.01)
            )(t_layer_2)
            t_layer_4 = layers.Dense(
                256, activation="relu", kernel_regularizer=regularizers.l2(0.01)
            )(t_layer_3)
            t_layer_5 = layers.Dense(
                2, activation="linear", kernel_regularizer=regularizers.l2(0.01)
            )(t_layer_4) # ⑤
        
            return models.Model(inputs=input_layer, outputs=[s_layer_5, t_layer_5]) # ⑥
        

        我们示例中Coupling层块的输入有两个维度。

        缩放流是一个大小为 256 的Dense层堆叠。

        最终的缩放层大小为 2,并具有tanh激活。

        平移流是一个大小为 256 的Dense层堆叠。

        最终的翻译层大小为 2,并具有linear激活。

        Coupling层被构建为一个 Keras Model,具有两个输出(缩放和平移因子)。

        请注意,临时增加通道数以允许学习更复杂的表示,然后将其折叠回与输入相同数量的通道。在原始论文中,作者还在每一层上使用正则化器来惩罚大的权重。

        通过耦合层传递数据

        耦合层的架构并不特别有趣——它的独特之处在于输入数据在通过层时如何被掩盖和转换,如图 6-6 所示。

        图 6-6。通过耦合层转换输入x的过程

        注意数据的前d维度被直接传递到第一个耦合层,剩下的D - d维度完全被遮蔽(即设为零)。在我们的简单示例中,选择d = 1意味着耦合层看到的不是两个值( x 1 , x 2 ),而是看到( x 1 , 0 )

        层的输出是比例和平移因子。这些再次被遮蔽,但这次是与之前的反向遮罩,只有后半部分被放行——即在我们的示例中,我们得到( 0 , s 2 )( 0 , t 2 )。然后这些被逐元素应用到输入的后半部分x 2,而输入的前半部分x 1则直接传递,完全不被更新。总之,对于维度为D的向量,其中d < D,更新方程如下:

        z 1:d = x 1:d z d+1:D = x d+1:D exp s ( x 1:d ) + t ( x 1:d )

        您可能想知道为什么我们要费力构建一个遮蔽了这么多信息的层。答案很明显,如果我们调查这个函数的雅可比矩阵的结构:

        z x = 𝐈 0 z d+1:D x 1:d diag ( exp [ s ( x 1:d ) ] )

        左上角的d × d子矩阵只是单位矩阵,因为z 1:d = x 1:d。这些元素直接传递而不被更新。因此,右上角的子矩阵为 0,因为z 1:d不依赖于x d+1:D

        左下角的子矩阵是复杂的,我们不寻求简化这个。右下角的子矩阵只是一个对角矩阵,填充有exp ( s ( x 1:d ) ) ,因为z d+1:D是线性相关于x d+1:D,梯度仅依赖于缩放因子(而不依赖于平移因子)。图 6-7 显示了这个矩阵形式的图表,只有非零元素被填充为彩色。

        注意对角线上方没有非零元素—因此,这种矩阵形式被称为下三角形。现在我们看到了以这种方式构造矩阵的好处—下三角矩阵的行列式就等于对角线元素的乘积。换句话说,行列式不依赖于左下子矩阵中的任何复杂导数!

        图 6-7。变换的雅可比矩阵——一个下三角矩阵,行列式等于对角线上元素的乘积

        因此,我们可以将这个矩阵的行列式写成如下形式:

        det ( J ) = exp j s (x 1:d ) j

        这是很容易计算的,这是构建归一化流模型的两个最初目标之一。

        另一个目标是函数必须易于反转。我们可以看到这是正确的,因为我们可以通过重新排列正向方程来写出可逆函数,如下所示:

        x 1:d = z 1:d x d+1:D = ( z d+1:D - t ( x 1:d ) ) exp - s ( x 1:d )

        等效图表显示在图 6-8 中。

        图 6-8。逆函数 x = g(z)

        现在我们几乎拥有构建 RealNVP 模型所需的一切。然而,仍然存在一个问题—我们应该如何更新输入的前个元素?目前,它们被模型完全保持不变!

        堆叠耦合层

        为了解决这个问题,我们可以使用一个非常简单的技巧。如果我们将耦合层堆叠在一起,但交替掩码模式,那么被一个层保持不变的层将在下一个层中更新。这种架构的额外好处是能够学习数据的更复杂表示,因为它是一个更深的神经网络。

        这些耦合层的雅可比矩阵仍然很容易计算,因为线性代数告诉我们,矩阵乘积的行列式是对角线上元素的乘积。同样,两个函数的复合的逆函数就是逆函数的复合,如下方程所示:

        det ( A · B ) = det ( A ) det ( B ) (f b f a ) -1 = f a -1 f b -1

        因此,如果我们堆叠耦合层,每次翻转掩码,我们可以构建一个神经网络,能够转换整个输入张量,同时保留具有简单雅可比行列式和可逆性的基本属性。图 6-9 显示了整体结构。

        图 6-9。堆叠耦合层,每层交替掩码

        训练 RealNVP 模型

        现在我们已经构建了 RealNVP 模型,我们可以训练它来学习两个月亮数据集的复杂分布。记住,我们希望最小化模型下数据的负对数似然 - log p X ( x ) 。使用方程 6-1,我们可以写成如下形式:

        - log p X ( x ) = - log p Z ( z ) - log det z x

        我们选择正向过程的目标输出分布 p Z ( z ) 为标准高斯分布,因为我们可以轻松从这个分布中采样。然后,我们可以通过应用逆过程将从高斯分布中采样的点转换回原始图像域,如图 6-10 所示。

        图 6-10。在 1D(中间行)和 2D(底部行)中,将复杂分布p X ( x )和简单高斯p Z ( z )之间的转换

        示例 6-3 展示了如何构建一个 RealNVP 网络,作为自定义的 Keras Model

        示例 6-3。在 Keras 中构建 RealNVP 模型
        class RealNVP(models.Model):
            def __init__(self, input_dim, coupling_layers, coupling_dim, regularization):
                super(RealNVP, self).__init__()
                self.coupling_layers = coupling_layers
                self.distribution = tfp.distributions.MultivariateNormalDiag(
                    loc=[0.0, 0.0], scale_diag=[1.0, 1.0]
                ) # ①
                self.masks = np.array(
                    [[0, 1], [1, 0]] * (coupling_layers // 2), dtype="float32"
                ) # ②
                self.loss_tracker = metrics.Mean(name="loss")
                self.layers_list = [
                    Coupling(input_dim, coupling_dim, regularization)
                    for i in range(coupling_layers)
                ] # ③
        
            @property
            def metrics(self):
                return [self.loss_tracker]
        
            def call(self, x, training=True):
                log_det_inv = 0
                direction = 1
                if training:
                    direction = -1
                for i in range(self.coupling_layers)[::direction]: # ④
                    x_masked = x * self.masks[i]
                    reversed_mask = 1 - self.masks[i]
                    s, t = self.layers_listi
                    s *= reversed_mask
                    t *= reversed_mask
                    gate = (direction - 1) / 2
                    x = (
                        reversed_mask
                        * (x * tf.exp(direction * s) + direction * t * tf.exp(gate * s))
                        + x_masked
                    ) # ⑤
                    log_det_inv += gate * tf.reduce_sum(s, axis = 1) # ⑥
                return x, log_det_inv
        
            def log_loss(self, x):
                y, logdet = self(x)
                log_likelihood = self.distribution.log_prob(y) + logdet # ⑦
                return -tf.reduce_mean(log_likelihood)
        
            def train_step(self, data):
                with tf.GradientTape() as tape:
                    loss = self.log_loss(data)
                g = tape.gradient(loss, self.trainable_variables)
                self.optimizer.apply_gradients(zip(g, self.trainable_variables))
                self.loss_tracker.update_state(loss)
                return {"loss": self.loss_tracker.result()}
        
            def test_step(self, data):
                loss = self.log_loss(data)
                self.loss_tracker.update_state(loss)
                return {"loss": self.loss_tracker.result()}
        
        model = RealNVP(
            input_dim = 2
            , coupling_layers= 6
            , coupling_dim = 256
            , regularization = 0.01
        )
        
        model.compile(optimizer=optimizers.Adam(learning_rate=0.0001))
        
        model.fit(
            normalized_data
            , batch_size=256
            , epochs=300
        )
        

        目标分布是标准的 2D 高斯分布。

        在这里,我们创建交替的掩码模式。

        定义 RealNVP 网络的Coupling层列表。

        在网络的主call函数中,我们遍历Coupling层。如果training=True,那么我们通过层向前移动(即从数据到潜在空间)。如果training=False,那么我们通过层向后移动(即从潜在空间到数据)。

        这行描述了正向和反向方程,取决于direction(尝试将direction = -1direction = 1代入以证明这一点!)。

        雅可比行列式的对数,我们需要计算损失函数,简单地是缩放因子的总和。

        损失函数是转换数据的对数概率的负和,根据我们的目标高斯分布和雅可比行列式的对数确定。

        RealNVP 模型的分析

        模型训练完成后,我们可以使用它将训练集转换为潜在空间(使用正向方向f),更重要的是,将潜在空间中的采样点转换为看起来可能是从原始数据分布中采样的点(使用反向方向g)。

        图 6-11 显示了在任何学习之前网络的输出 - 正向和反向方向只是直接传递信息,几乎没有任何转换。

        图 6-11。RealNVP 模型在训练前的输入(左)和输出(右),用于正向过程(顶部)和反向过程(底部)

        训练后(图 6-12),正向过程能够将训练集中的点转换为类似高斯分布的分布。同样,反向过程可以将从高斯分布中采样的点映射回类似原始数据的分布。

        图 6-12。RealNVP 模型在训练后的输入(左)和输出(右),用于正向过程(顶部)和反向过程(底部)

        训练过程的损失曲线显示在图 6-13 中。

        图 6-13。RealNVP 训练过程的损失曲线

        这完成了我们对 RealNVP 的讨论,这是正则化流生成模型的一个特定案例。在下一节中,我们将介绍一些现代正则化流模型,这些模型扩展了 RealNVP 论文中介绍的思想。

        其他正则化流模型

        另外两个成功且重要的正则化流模型是GLOWFFJORD。以下部分描述了它们所取得的关键进展。

        GLOW

        在 NeurIPS 2018 上展示的 GLOW 是第一个证明归一化流能够生成高质量样本并产生可遍历以操作样本的有意义潜在空间的模型之一。关键步骤是用可逆的 1×1 卷积层替换反向掩码设置。例如,在应用于图像的 RealNVP 中,通道的顺序在每一步之后都会翻转,以确保网络有机会转换所有的输入。而在 GLOW 中,应用了 1×1 卷积,这有效地作为一种通用方法来产生模型所需的任何通道排列。作者表明,即使加入了这一步骤,整体分布仍然是可处理的,具有易于大规模计算的行列式和逆。

        图 6-14。GLOW 模型的随机样本(来源:Kingma 和 Dhariwal,2018)²

        FFJORD

        RealNVP 和 GLOW 是离散时间归一化流模型,即它们通过一组离散的耦合层来转换输入。FFJORD(用于可扩展可逆生成模型的自由形式连续动力学),在 ICLR 2019 上展示了如何将转换建模为连续时间过程(即,通过将流中步数趋近于无穷大且步长趋近于零的极限来实现)。在这种情况下,动力学是使用由神经网络产生的普通微分方程(ODE)来建模的。使用黑盒求解器来解决在时间 t1 处的 ODE,即找到在时间 t0 处从高斯分布中采样的一些初始点 z0 的 z1,如下面的方程所描述的那样:

        z 0 p ( z 0 ) z(t) t = f θ ( x ( t ) , t ) x = z 1

        转换过程的图示在图 6-15 中展示。

        图 6-15。FFJORD 通过由神经网络参数化的普通微分方程模拟数据分布与标准高斯之间的转换(来源:Will Grathwohl 等人,2018)³

        总结

        在本章中,我们探讨了诸如 RealNVP、GLOW 和 FFJORD 等归一化流模型。

        归一化流模型是由神经网络定义的可逆函数,允许我们通过变量的改变直接建模数据密度。在一般情况下,变量改变方程要求我们计算一个高度复杂的雅可比行列式,这对于除了最简单的例子之外都是不切实际的。

        为了规避这个问题,RealNVP 模型限制了神经网络的形式,使其符合两个基本标准:可逆且雅可比行列式易于计算。

        它通过堆叠耦合层来实现,这些层在每一步产生尺度和平移因子。重要的是,耦合层在数据流经网络时会掩盖数据,以确保雅可比是下三角形的,因此具有易于计算的行列式。通过在每一层翻转掩码,实现了对输入数据的完全可见性。

        按设计,尺度和平移操作可以轻松地被反转,因此一旦模型训练完成,就可以通过网络反向运行数据。这意味着我们可以将正向转换过程定向到标准高斯分布,从中轻松采样。然后,我们可以通过网络反向运行采样点以生成新的观测数据。

        RealNVP 论文还展示了如何将这种技术应用于图像,通过在耦合层内部使用卷积,而不是密集连接层。GLOW 论文将这一思想扩展到消除任何硬编码排列掩模的必要性。FFJORD 模型引入了连续时间归一化流的概念,通过将转换过程建模为由神经网络定义的 ODE。

        总的来说,我们已经看到了归一化流是一个强大的生成建模家族,可以生成高质量的样本,同时保持能够可靠地描述数据密度函数的能力。

        ¹ Laurent Dinh 等人,“使用 Real NVP 进行密度估计”,2016 年 5 月 27 日,https://arxiv.org/abs/1605.08803v3

        ² Diedrick P. Kingma 和 Prafulla Dhariwal,“Glow: 具有可逆 1x1 卷积的生成流”,2018 年 7 月 10 日,https://arxiv.org/abs/1807.03039

        ³ Will Grathwohl 等人,“FFJORD: 用于可扩展可逆生成模型的自由形式连续动力学”,2018 年 10 月 22 日,https://arxiv.org/abs/1810.01367

        第七章:基于能量的模型

        基于能量的模型是一类广泛的生成模型,借鉴了建模物理系统的一个关键思想——即,事件的概率可以用玻尔兹曼分布来表示,这是一个特定函数,将实值能量函数归一化在 0 到 1 之间。这个分布最初是由路德维希·玻尔兹曼在 1868 年提出的,他用它来描述处于热平衡状态的气体。

        在本章中,我们将看到如何使用这个想法来训练一个生成模型,用于生成手写数字的图像。我们将探索几个新概念,包括用于训练 EBM 的对比散度和用于采样的朗之万动力学。

        介绍

        我们将从一个简短的故事开始,以说明基于能量的模型背后的关键概念。

        Diane Mixx 和 Long-au-Vin 跑步俱乐部的故事捕捉了基于能量建模的关键思想。现在让我们更详细地探讨理论,然后我们将使用 Keras 实现一个实际示例。

        基于能量的模型

        基于能量的模型试图使用玻尔兹曼分布(方程式 7-1)来建模真实的数据生成分布,其中 E ( x ) 被称为观察结果 x能量函数(或分数)。

        方程式 7-1。玻尔兹曼分布

        p ( 𝐱 ) = e -E(𝐱) 𝐱 ^𝐗 e -E(𝐱 ^)

        在实践中,这意味着训练一个神经网络 E ( x ),对可能的观察输出低分数(使得 p 𝐱 接近于 1),对不太可能的观察输出高分数(使得 p 𝐱 接近于 0)。

        用这种方式建模数据存在两个挑战。首先,我们不清楚如何使用我们的模型来采样新的观察结果——我们可以用它来生成给定观察结果的分数,但如何生成一个得分低的观察结果(即,一个可信的观察结果)?

        其次,方程式 7-1 的标准化分母包含一个对于除了最简单的问题外都难以计算的积分。如果我们无法计算这个积分,那么我们就无法使用最大似然估计来训练模型,因为这要求 p 𝐱 是一个有效的概率分布。

        基于能量的模型的关键思想是,我们可以使用近似技术来确保我们永远不需要计算难以计算的分母。这与标准化流形形成对比,标准化流形需要我们付出很大的努力,以确保我们对标准高斯分布应用的变换不会改变输出仍然是有效的概率分布的事实。

        我们通过使用一种称为对比散度(用于训练)和一种称为朗之万动力学(用于采样)的技术来避开棘手的难以计算的分母问题,这些技术遵循了杜和莫达奇在 2019 年的论文“基于能量的模型的隐式生成和建模”的思想。我们将在本章后面详细探讨这些技术,同时构建我们自己的 EBM。

        首先,让我们准备一个数据集并设计一个简单的神经网络,来表示我们的实值能量函数 E ( x )

        运行此示例的代码

        此示例的代码可以在书籍存储库中的notebooks/07_ebm/01_ebm/ebm.ipynb中找到。

        这段代码是根据 Philip Lippe 的优秀教程“深度基于能量的生成模型”进行调整的。

        MNIST 数据集

        我们将使用标准的MNIST 数据集,其中包含手写数字的灰度图像。数据集中的一些示例图像显示在图 7-2 中。

        图 7-2。MNIST 数据集中的图像示例

        数据集已经预先打包到 TensorFlow 中,因此可以按照示例 7-1 中所示下载。

        示例 7-1。加载 MNIST 数据集
        from tensorflow.keras import datasets
        (x_train, _), (x_test, _) = datasets.mnist.load_data()
        

        像往常一样,我们将像素值缩放到范围[-1, 1],并添加一些填充使图像大小为 32×32 像素。我们还将其转换为 TensorFlow 数据集,如示例 7-2 中所示。

        示例 7-2。预处理 MNIST 数据集
        def preprocess(imgs):
            imgs = (imgs.astype("float32") - 127.5) / 127.5
            imgs = np.pad(imgs , ((0,0), (2,2), (2,2)), constant_values= -1.0)
            imgs = np.expand_dims(imgs, -1)
            return imgs
        
        x_train = preprocess(x_train)
        x_test = preprocess(x_test)
        x_train = tf.data.Dataset.from_tensor_slices(x_train).batch(128)
        x_test = tf.data.Dataset.from_tensor_slices(x_test).batch(128)
        

        现在我们有了数据集,我们可以构建代表我们能量函数E ( x )的神经网络。

        能量函数

        能量函数E θ ( x )是一个具有参数θ的神经网络,可以将输入图像x转换为标量值。在整个网络中,我们使用了一个称为swish的激活函数,如下面的侧边栏所述。

        网络是一组堆叠的Conv2D层,逐渐减小图像的尺寸同时增加通道数。最后一层是一个具有线性激活的单个完全连接单元,因此网络可以输出范围内的值(- )。构建它的代码在示例 7-3 中给出。

        示例 7-3。构建能量函数E ( x )神经网络
        ebm_input = layers.Input(shape=(32, 32, 1))
        x = layers.Conv2D(
            16, kernel_size=5, strides=2, padding="same", activation = activations.swish
        )(ebm_input) # ①
        x = layers.Conv2D(
            32, kernel_size=3, strides=2, padding="same", activation = activations.swish
        )(x)
        x = layers.Conv2D(
            64, kernel_size=3, strides=2, padding="same", activation = activations.swish
        )(x)
        x = layers.Conv2D(
            64, kernel_size=3, strides=2, padding="same", activation = activations.swish
        )(x)
        x = layers.Flatten()(x)
        x = layers.Dense(64, activation = activations.swish)(x)
        ebm_output = layers.Dense(1)(x) # ②
        model = models.Model(ebm_input, ebm_output) # ③
        

        能量函数是一组堆叠的Conv2D层,带有 swish 激活。

        最后一层是一个单个完全连接单元,具有线性激活函数。

        一个将输入图像转换为标量能量值的 Keras Model

        使用 Langevin 动力学进行采样

        能量函数只为给定输入输出一个分数——我们如何使用这个函数生成能量分数低的新样本?

        我们将使用一种称为Langevin 动力学的技术,利用了我们可以计算能量函数相对于其输入的梯度的事实。如果我们从样本空间中的一个随机点开始,并朝着计算出的梯度的相反方向迈出小步,我们将逐渐减小能量函数。如果我们的神经网络训练正确,那么随机噪声应该在我们眼前转变成类似于训练集中的观察结果的图像!

        随机梯度 Langevin 动力学

        重要的是,当我们穿越样本空间时,我们还必须向输入添加少量随机噪声;否则,有可能陷入局部最小值。因此,该技术被称为随机梯度 Langevin 动力学。³

        我们可以将这种梯度下降可视化为 图 7-4 中所示,对于一个具有能量函数值的三维空间。路径是一个嘈杂的下降,沿着能量函数 E ( x ) 的负梯度相对于输入 x 下降。在 MNIST 图像数据集中,我们有 1,024 个像素,因此在一个 1,024 维空间中导航,但是相同的原则适用!

        图 7-4. 使用朗之万动力学的梯度下降

        值得注意的是,这种梯度下降与我们通常用来训练神经网络的梯度下降之间的区别。

        在训练神经网络时,我们使用反向传播计算损失函数相对于网络的参数(即权重)的梯度。然后我们将参数在负梯度方向上微调,这样经过多次迭代,我们逐渐最小化损失。

        使用朗之万动力学,我们保持神经网络权重固定,计算输出相对于输入的梯度。然后我们将输入在负梯度方向上微调,这样经过多次迭代,我们逐渐最小化输出(能量分数)。

        这两个过程都利用了相同的思想(梯度下降),但是应用于不同的函数,并且针对不同的实体。

        形式上,朗之万动力学可以用以下方程描述:

        x k = x k-1 - η x E θ ( x k-1 ) + ω

        其中 ω 𝒩 ( 0 , σ )x 0 𝒰(-1,1)。η 是必须调整的步长超参数——太大会导致步骤跳过最小值,太小则算法收敛速度太慢。

        提示

        x 0 𝒰(-1,1)是范围[-1,1]上的均匀分布。

        我们可以编写我们的朗之万采样函数,如 示例 7-4 所示。

        示例 7-4. 朗之万采样函数
        def generate_samples(model, inp_imgs, steps, step_size, noise):
            imgs_per_step = []
            for _ in range(steps): # ①
                inp_imgs += tf.random.normal(inp_imgs.shape, mean = 0, stddev = noise) # ②
                inp_imgs = tf.clip_by_value(inp_imgs, -1.0, 1.0)
                with tf.GradientTape() as tape:
                    tape.watch(inp_imgs)
                    out_score = -model(inp_imgs) # ③
                grads = tape.gradient(out_score, inp_imgs) # ④
                grads = tf.clip_by_value(grads, -0.03, 0.03)
                inp_imgs += -step_size * grads # ⑤
                inp_imgs = tf.clip_by_value(inp_imgs, -1.0, 1.0)
                return inp_imgs
        

        循环执行给定数量的步骤。

        向图像中添加少量噪音。

        通过模型传递图像以获得能量分数。

        计算输出相对于输入的梯度。

        向输入图像中添加少量梯度。

        使用对比散度进行训练

        现在我们知道如何从样本空间中采样一个新的低能量点,让我们将注意力转向训练模型。

        我们无法应用最大似然估计,因为能量函数不输出概率;它输出的是一个在样本空间中不积分为 1 的分数。相反,我们将应用 Geoffrey Hinton 在 2002 年首次提出的一种技术,称为对比散度,用于训练非归一化评分模型。⁴

        我们希望最小化的值(一如既往)是数据的负对数似然:

        = - 𝔼 x data log p θ ( 𝐱 )

        p θ ( 𝐱 ) 具有玻尔兹曼分布的形式,能量函数为 E θ ( 𝐱 ) ,可以证明该值的梯度可以写成以下形式(Oliver Woodford 的“对比散度笔记”进行完整推导):⁵

        θ = 𝔼 x data θ E θ ( 𝐱 ) - 𝔼 x model θ E θ ( 𝐱 )

        这在直觉上是有很多意义的-我们希望训练模型输出真实观察的大负能量分数,并为生成的假观察输出大正能量分数,以便这两个极端之间的对比尽可能大。

        换句话说,我们可以计算真实和假样本的能量分数之间的差异,并将其用作我们的损失函数。

        要计算假样本的能量分数,我们需要能够从分布 p θ ( 𝐱 ) 中精确抽样,但由于不可解的分母,这是不可能的。相反,我们可以使用 Langevin 采样过程生成一组能量分数较低的观察。这个过程需要运行无限多步才能产生完美样本(显然是不切实际的),因此我们运行一些小步数,假设这足以产生有意义的损失函数。

        我们还维护一个来自先前迭代的样本缓冲区,这样我们可以将其用作下一批的起点,而不是纯随机噪声。生成采样缓冲区的代码如示例 7-5 所示。

        示例 7-5。缓冲区
        class Buffer:
            def __init__(self, model):
                super().__init__()
                self.model = model
                self.examples = [
                    tf.random.uniform(shape = (1, 32, 32, 1)) * 2 - 1
                    for _ in range(128)
                ] # ①
        
            def sample_new_exmps(self, steps, step_size, noise):
                n_new = np.random.binomial(128, 0.05) # ②
                rand_imgs = (
                    tf.random.uniform((n_new, 32, 32, 1)) * 2 - 1
                )
                old_imgs = tf.concat(
                    random.choices(self.examples, k=128-n_new), axis=0
                ) # ③
                inp_imgs = tf.concat([rand_imgs, old_imgs], axis=0)
                inp_imgs = generate_samples(
                    self.model, inp_imgs, steps=steps, step_size=step_size, noise = noise
                ) # ④
                self.examples = tf.split(inp_imgs, 128, axis = 0) + self.examples # ⑤
                self.examples = self.examples[:8192]
                return inp_imgs
        

        采样缓冲区用一批随机噪声初始化。

        平均而言,每次有 5%的观察是从头开始生成的(即,随机噪声)。

        其余的随机从现有缓冲区中提取。

        这些观察被连接并通过 Langevin 采样器运行。

        生成的样本被添加到缓冲区中,缓冲区被修剪为最多 8,192 个观察。

        图 7-5 显示了对比散度的一个训练步骤。真实观察的分数被算法推低,而假观察的分数被拉高,每一步之后都不考虑对这些分数进行归一化。

        图 7-5。对比散度的一步

        我们可以编写对比散度算法的训练步骤,如示例 7-6 所示,在自定义 Keras 模型中。

        示例 7-6。使用对比散度训练的 EBM
        class EBM(models.Model):
            def __init__(self):
                super(EBM, self).__init__()
                self.model = model
                self.buffer = Buffer(self.model)
                self.alpha = 0.1
                self.loss_metric = metrics.Mean(name="loss")
                self.reg_loss_metric = metrics.Mean(name="reg")
                self.cdiv_loss_metric = metrics.Mean(name="cdiv")
                self.real_out_metric = metrics.Mean(name="real")
                self.fake_out_metric = metrics.Mean(name="fake")
        
            @property
            def metrics(self):
                return [
                    self.loss_metric,
                    self.reg_loss_metric,
                    self.cdiv_loss_metric,
                    self.real_out_metric,
                    self.fake_out_metric
                ]
        
            def train_step(self, real_imgs):
                real_imgs += tf.random.normal(
                    shape=tf.shape(real_imgs), mean = 0, stddev = 0.005
                ) # ①
                real_imgs = tf.clip_by_value(real_imgs, -1.0, 1.0)
                fake_imgs = self.buffer.sample_new_exmps(
                    steps=60, step_size=10, noise = 0.005
                ) # ②
                inp_imgs = tf.concat([real_imgs, fake_imgs], axis=0)
                with tf.GradientTape() as training_tape:
                    real_out, fake_out = tf.split(self.model(inp_imgs), 2, axis=0) # ③
                    cdiv_loss = tf.reduce_mean(fake_out, axis = 0) - tf.reduce_mean(
                        real_out, axis = 0
                    ) # ④
                    reg_loss = self.alpha * tf.reduce_mean(
                        real_out ** 2 + fake_out ** 2, axis = 0
                    ) # ⑤
                    loss = reg_loss + cdiv_loss
                grads = training_tape.gradient(loss, self.model.trainable_variables) # ⑥
                self.optimizer.apply_gradients(
                    zip(grads, self.model.trainable_variables)
                )
                self.loss_metric.update_state(loss)
                self.reg_loss_metric.update_state(reg_loss)
                self.cdiv_loss_metric.update_state(cdiv_loss)
                self.real_out_metric.update_state(tf.reduce_mean(real_out, axis = 0))
                self.fake_out_metric.update_state(tf.reduce_mean(fake_out, axis = 0))
                return {m.name: m.result() for m in self.metrics}
        
            def test_step(self, real_imgs): # ⑦
                batch_size = real_imgs.shape[0]
                fake_imgs = tf.random.uniform((batch_size, 32, 32, 1)) * 2 - 1
                inp_imgs = tf.concat([real_imgs, fake_imgs], axis=0)
                real_out, fake_out = tf.split(self.model(inp_imgs), 2, axis=0)
                cdiv = tf.reduce_mean(fake_out, axis = 0) - tf.reduce_mean(
                    real_out, axis = 0
                )
                self.cdiv_loss_metric.update_state(cdiv)
                self.real_out_metric.update_state(tf.reduce_mean(real_out, axis = 0))
                self.fake_out_metric.update_state(tf.reduce_mean(fake_out, axis = 0))
                return {m.name: m.result() for m in self.metrics[2:]}
        
        ebm = EBM()
        ebm.compile(optimizer=optimizers.Adam(learning_rate=0.0001), run_eagerly=True)
        ebm.fit(x_train, epochs=60, validation_data = x_test,)
        

        为真实图像添加少量随机噪声,以避免模型过度拟合训练集。

        一组假图像从缓冲区中抽样。

        真实和假图像通过模型运行以产生真实和假分数。

        对比散度损失简单地是真实和假观察的分数之间的差异。

        添加正则化损失以避免分数变得过大。

        通过反向传播计算网络权重相对于损失函数的梯度。

        test_step 用于在验证过程中计算一组随机噪声和训练集中的数据之间的对比散度。它可以作为衡量模型训练效果的指标(见下一节)。

        能量基模型的分析

        训练过程中的损失曲线和支持指标显示在 Figure 7-6 中。

        图 7-6. EBM 训练过程的损失曲线和指标

        首先,注意到在训练步骤中计算的损失在各个周期中大致保持不变且较小。虽然模型不断改进,但与训练集中的真实图像进行比较的缓冲区中生成的图像质量也在提高,因此我们不应该期望训练损失显著下降。

        因此,为了评估模型性能,我们还建立了一个验证过程,该过程不从缓冲区中采样,而是对一组随机噪声进行评分,并将其与训练集中的示例进行比较。如果模型正在改进,我们应该看到对比散度随着周期的增加而下降(即,它在区分随机噪声和真实图像方面变得更好),如 Figure 7-6 中所示。

        从 EBM 生成新样本只需运行 Langevin 采样器进行大量步骤,从一个静止状态(随机噪声)开始,如 Example 7-7 中所示。观测被迫 下坡,沿着相对于输入的评分函数的梯度,以便在噪声中出现一个合理的观测。

        示例 7-7. 使用 EBM 生成新观测
        start_imgs = np.random.uniform(size = (10, 32, 32, 1)) * 2 - 1
        gen_img = generate_samples(
            ebm.model,
            start_imgs,
            steps=1000,
            step_size=10,
            noise = 0.005,
            return_img_per_step=True,
        )
        

        在经过 50 个周期的训练后,采样器生成的一些观测示例显示在 Figure 7-7 中。

        图 7-7. 使用 EBM 模型的 Langevin 采样器生成的示例以指导梯度下降

        我们甚至可以展示单个观测是如何通过在 Langevin 采样过程中拍摄当前观测的快照生成的——这在 Figure 7-8 中展示。

        图 7-8. Langevin 采样过程中不同步骤的观测快照

        其他能量基模型

        在前面的例子中,我们使用了使用对比散度和 Langevin 动力学采样器训练的深度 EBM。然而,早期的 EBM 模型并没有使用 Langevin 采样,而是依赖于其他技术和架构。

        最早的能量基模型之一是 Boltzmann 机。⁶ 这是一个全连接的无向神经网络,其中二进制单元要么是 可见v),要么是 隐藏h)。网络的给定配置的能量定义如下:

        E θ ( v , h ) = - 1 2 v T L v + h T J h + v T W h

        其中 W , L , J 是模型学习的权重矩阵。训练通过对比散度实现,但使用 Gibbs 采样在可见层和隐藏层之间交替,直到找到平衡。实际上,这是非常缓慢的,不适用于大量隐藏单元。

        提示

        请查看 Jessica Stringham 的博客文章 “Python 中的 Gibbs 采样” 以获取 Gibbs 采样的优秀简单示例。

        这个模型的扩展,受限玻尔兹曼机(RBM),移除了相同类型单元之间的连接,因此创建了一个两层的二部图。这使得 RBM 可以堆叠成深度信念网络,以建模更复杂的分布。然而,使用 RBM 对高维数据进行建模仍然是不切实际的,因为仍然需要长混合时间的吉布斯采样。

        直到 2000 年代末,EBM 才被证明具有对更高维数据集进行建模的潜力,并建立了一个构建深度 EBM 的框架。⁷ Langevin 动力学成为 EBM 的首选采样方法,后来演变成一种称为得分匹配的训练技术。这进一步发展成一种称为去噪扩散概率模型的模型类,为 DALL.E 2 和 ImageGen 等最先进的生成模型提供动力。我们将在第八章中更详细地探讨扩散模型。

        摘要

        基于能量的模型是一类生成模型,利用能量评分函数——一个经过训练的神经网络,用于为真实观察输出低分数,为生成观察输出高分数。计算由该得分函数给出的概率分布需要通过一个难以处理的分母进行归一化。EBM 通过利用两个技巧来避免这个问题:对比散度用于训练网络,Langevin 动力学用于采样新观察。

        能量函数通过最小化生成样本得分与训练数据得分之间的差异来进行训练,这种技术称为对比散度。可以证明这等价于最小化负对数似然,这是最大似然估计所要求的,但不需要我们计算难以处理的归一化分母。在实践中,我们近似为假样本的采样过程,以确保算法保持高效。

        深度 EBM 的采样是通过 Langevin 动力学实现的,这是一种利用得分相对于输入图像的梯度逐渐将随机噪声转化为合理观察的技术,通过更新输入进行小步骤,沿着梯度下降。这改进了早期的方法,如受限玻尔兹曼机使用的吉布斯采样。

        ¹ 杜一伦和伊戈尔·莫达奇,“基于能量的模型的隐式生成和建模”,2019 年 3 月 20 日,https://arxiv.org/abs/1903.08689

        ² Prajit Ramachandran 等人,“搜索激活函数”,2017 年 10 月 16 日,https://arxiv.org/abs/1710.05941v2

        ³ Max Welling 和 Yee Whye Teh,“通过随机梯度 Langevin 动力学进行贝叶斯学习”,2011 年,https://www.stats.ox.ac.uk/~teh/research/compstats/WelTeh2011a.pdf

        ⁴ Geoffrey E. Hinton,“通过最小化对比散度训练专家乘积”,2002 年,https://www.cs.toronto.edu/~hinton/absps/tr00-004.pdf

        ⁵ Oliver Woodford,“对比散度笔记”,2006 年,https://www.robots.ox.ac.uk/~ojw/files/NotesOnCD.pdf

        ⁶ David H. Ackley 等人,“玻尔兹曼机的学习算法”,1985 年,认知科学 9(1), 147-165。

        ⁷ Yann Lecun 等人,“基于能量的学习教程”,2006 年,https://www.researchgate.net/publication/200744586_A_tutorial_on_energy-based_learning.

        第八章:扩散模型

        与 GANs 并驾齐驱,扩散模型是过去十年中引入的最具影响力和影响力的生成建模技术之一。在许多基准测试中,扩散模型现在胜过以前的最先进 GANs,并迅速成为生成建模从业者的首选选择,特别是对于视觉领域(例如,OpenAI 的 DALL.E 2 和 Google 的 ImageGen 用于文本到图像生成)。最近,扩散模型在广泛任务中的应用呈现爆炸性增长,类似于 2017 年至 2020 年间 GAN 的普及。

        许多支撑扩散模型的核心思想与本书中已经探索过的早期类型的生成模型(例如,去噪自动编码器,基于能量的模型)有相似之处。事实上,名称扩散灵感来自热力学扩散的深入研究:在 2015 年,这一纯物理领域与深度学习之间建立了重要联系。¹

        在基于分数的生成模型领域也取得了重要进展,²^,³这是能量基模型的一个分支,直接估计对数分布的梯度(也称为分数函数),以训练模型,作为使用对比散度的替代方法。特别是,杨松和斯特凡诺·厄尔蒙使用多个尺度的噪声扰动应用于原始数据,以确保模型-一个噪声条件分数网络(NCSN)在低数据密度区域表现良好。

        突破性的扩散模型论文于 2020 年夏天发表。⁴在前人的基础上,该论文揭示了扩散模型和基于分数的生成模型之间的深刻联系,作者利用这一事实训练了一个可以在几个数据集上与 GANs 匹敌的扩散模型,称为去噪扩散概率模型(DDPM)。

        本章将介绍理解去噪扩散模型工作原理的理论要求。然后,您将学习如何使用 Keras 构建自己的去噪扩散模型。

        介绍

        为了帮助解释支撑扩散模型的关键思想,让我们从一个简短的故事开始!

        DiffuseTV 故事描述了扩散模型背后的一般思想。现在让我们深入探讨如何使用 Keras 构建这样一个模型的技术细节。

        去噪扩散模型(DDM)

        去噪扩散模型背后的核心思想很简单-我们训练一个深度学习模型,在一系列非常小的步骤中去噪图像。如果我们从纯随机噪音开始,在理论上我们应该能够不断应用该模型,直到获得一个看起来好像是从训练集中绘制出来的图像。令人惊奇的是,这个简单的概念在实践中效果如此出色!

        让我们首先准备一个数据集,然后逐步介绍前向(加噪)和后向(去噪)扩散过程。

        运行此示例的代码

        此示例的代码可以在书籍存储库中位于notebooks/08_diffusion/01_ddm/ddm.ipynb的 Jupyter 笔记本中找到。

        该代码改编自 András Béres 在 Keras 网站上创建的优秀去噪扩散隐式模型教程

        花卉数据集

        我们将使用通过 Kaggle 提供的牛津 102 花卉数据集。这是一组包含各种花卉的 8000 多张彩色图像。

        您可以通过在书籍存储库中运行 Kaggle 数据集下载脚本来下载数据集,如示例 8-1 所示。这将把花卉图像保存到/data文件夹中。

        示例 8-1。下载牛津 102 花卉数据集
        bash scripts/download_kaggle_data.sh nunenuh pytorch-challange-flower-dataset
        

        “通常情况下,我们将使用 Keras 的image_dataset_from_directory函数加载图像,将图像调整为 64×64 像素,并将像素值缩放到范围[0, 1]。我们还将数据集重复五次,以增加时代长度,并将数据分成 64 张图像一组,如示例 8-2 所示。

        示例 8-2。加载牛津 102 花卉数据集
        train_data = utils.image_dataset_from_directory(
            "/app/data/pytorch-challange-flower-dataset/dataset",
            labels=None,
            image_size=(64, 64),
            batch_size=None,
            shuffle=True,
            seed=42,
            interpolation="bilinear",
        ) # ①
        
        def preprocess(img):
            img = tf.cast(img, "float32") / 255.0
            return img
        
        train = train_data.map(lambda x: preprocess(x)) # ②
        train = train.repeat(5) # ③
        train = train.batch(64, drop_remainder=True) # ④
        

        使用 Keras 的image_dataset_from_directory函数加载数据集(在训练期间需要时)。

        将像素值缩放到范围[0, 1]。

        将数据集重复五次。

        将数据集分成 64 张图像一组。

        数据集中的示例图像显示在图 8-2 中。

        图 8-2。牛津 102 花卉数据集中的示例图像

        现在我们有了数据集,我们可以探讨如何向图像添加噪声,使用前向扩散过程。 ##前向扩散过程

        假设我们有一幅图像𝐱 0,我们希望在大量步骤(比如,T = 1 , 000)中逐渐损坏,以至于最终与标准高斯噪声不可区分(即,𝐱 T应具有零均值和单位方差)。我们应该如何做到这一点呢?

        我们可以定义一个函数q,它向图像𝐱 t-1添加方差为β t的少量高斯噪声,以生成新图像𝐱 t。如果我们不断应用这个函数,我们将生成一系列逐渐嘈杂的图像(𝐱 0 , ... , 𝐱 T),如图 8-3 所示。

        图 8-3。前向扩散过程q

        我们可以将这个更新过程数学地表示如下(这里,ϵ t-1是具有零均值和单位方差的标准高斯分布):

        𝐱 t = 1 - β t 𝐱 t-1 + β t ϵ t-1

        请注意,我们还要缩放输入图像𝐱 t-1,以确保输出图像𝐱 t的方差随时间保持恒定。这样,如果我们将原始图像𝐱 0归一化为零均值和单位方差,那么𝐱 T将在足够大的T时逼近标准高斯分布,通过归纳,如下所示。

        如果我们假设𝐱 t-1具有零均值和单位方差,那么1 - β t 𝐱 t-1的方差将为1 - β t,而β t ϵ t-1的方差将为β t,使用V a r ( a X ) = a 2 V a r ( X )的规则。将这些加在一起,我们得到一个新的分布𝐱 t,均值为零,方差为1 - β t + β t = 1,使用V a r ( X + Y ) = V a r ( X ) + V a r ( Y )的规则,对于独立的XY。因此,如果𝐱 0被归一化为零均值和单位方差,那么我们保证对所有𝐱 t都成立,包括最终图像𝐱 T,它将近似为标准高斯分布。这正是我们需要的,因为我们希望能够轻松地对𝐱 T进行采样,然后通过我们训练过的神经网络模型应用反向扩散过程!

        换句话说,我们的前向噪声过程q也可以写成如下形式:

        q ( 𝐱 t | 𝐱 t-1 ) = 𝒩 ( 𝐱 t ; 1 - β t 𝐱 t-1 , β t 𝐈 )

        重新参数化技巧

        从图像𝐱 0直接跳转到图像的任何噪声版本𝐱 t也会很有用,而不必经过tq的应用。幸运的是,我们可以使用一种重新参数化技巧来实现这一点。

        如果我们定义α t = 1 - β tα ¯ t = i=1 t α i,那么我们可以写成以下形式:

        𝐱 t = α t 𝐱 t-1 + 1 - α t ϵ t-1 = α t α t-1 𝐱 t-2 + 1 - α t α t-1 ϵ = = α ¯ t 𝐱 0 + 1 - α ¯ t ϵ

        请注意,第二行使用了我们可以将两个高斯函数相加以获得一个新高斯函数的事实。因此,我们有一种方法可以从原始图像𝐱 0跳转到前向扩散过程的任何步骤𝐱 t。此外,我们可以使用α ¯ t值来定义扩散进度表,而不是原始的β t值,解释为α ¯ t是由信号(原始图像,𝐱 0)引起的方差,而1 - α ¯ t是由噪声(ϵ)引起的方差。

        前向扩散过程q也可以写成如下形式:

        q ( 𝐱 t | 𝐱 0 ) = 𝒩 ( 𝐱 t ; α ¯ t 𝐱 0 , ( 1 - α ¯ t ) 𝐈 )

        扩散进度表

        请注意,我们也可以在每个时间步长选择不同的β t——它们不必全部相同。β t(或α ¯ t)值随着t的变化被称为扩散进度表

        在原始论文中(Ho 等人,2020 年),作者选择了一个线性扩散进度表用于β t——即,β t随着t线性增加,从β 1 =0.0001 到β T =0.02。这确保在噪声过程的早期阶段,我们采取比在后期阶段更小的噪声步骤,当图像已经非常嘈杂时。

        我们可以编写一个线性扩散进度表,如示例 8-3 所示。

        示例 8-3。线性扩散进度表
        def linear_diffusion_schedule(diffusion_times):
            min_rate = 0.0001
            max_rate = 0.02
            betas = min_rate + tf.convert_to_tensor(diffusion_times) * (max_rate - min_rate)
            alphas = 1 - betas
            alpha_bars = tf.math.cumprod(alphas)
            signal_rates = alpha_bars
            noise_rates = 1 - alpha_bars
            return noise_rates, signal_rates
        
        T = 1000
        diffusion_times = [x/T for x in range(T)] # ①
        linear_noise_rates, linear_signal_rates = linear_diffusion_schedule(
            diffusion_times
        ) # ②
        

        扩散时间是 0 到 1 之间等间隔的步骤。

        线性扩散进度表应用于扩散时间以产生噪声和信号速率。

        在后续的一篇论文中发现,余弦扩散进度表优于原始论文中的线性进度表。余弦进度表定义了以下α ¯ t值:

        α ¯ t = cos 2 ( t T · π 2 )

        因此,更新的方程如下(使用三角恒等式 cos 2 ( x ) + sin 2 ( x ) = 1):

        𝐱 t = cos ( t T · π 2 ) 𝐱 0 + sin ( t T · π 2 ) ϵ

        这个方程是论文中使用的实际余弦扩散时间表的简化版本。作者还添加了一个偏移项和缩放,以防止扩散过程开始时噪声步骤太小。我们可以编写余弦和偏移余弦扩散时间表,如示例 8-4 所示。

        示例 8-4. 余弦和偏移余弦扩散时间表
        def cosine_diffusion_schedule(diffusion_times): # ①
            signal_rates = tf.cos(diffusion_times * math.pi / 2)
            noise_rates = tf.sin(diffusion_times * math.pi / 2)
            return noise_rates, signal_rates
        
        def offset_cosine_diffusion_schedule(diffusion_times): # ②
            min_signal_rate = 0.02
            max_signal_rate = 0.95
            start_angle = tf.acos(max_signal_rate)
            end_angle = tf.acos(min_signal_rate)
        
            diffusion_angles = start_angle + diffusion_times * (end_angle - start_angle)
        
            signal_rates = tf.cos(diffusion_angles)
            noise_rates = tf.sin(diffusion_angles)
        
            return noise_rates, signal_rates
        

        纯余弦扩散时间表(不包括偏移或重新缩放)。

        我们将使用的偏移余弦扩散时间表会调整时间表,以确保在扩散过程开始时噪声步骤不会太小。

        我们可以计算每个 tα ¯ t 值,以显示在线性、余弦和偏移余弦扩散时间表的每个阶段中有多少信号( α ¯ t )和噪声( 1 - α ¯ t )通过,如图 8-4 所示。

        图 8-4. 在扩散过程的每个步骤中的信号和噪声,对于线性、余弦和偏移余弦扩散时间表

        请注意,余弦扩散时间表中的噪声级别上升速度较慢。余弦扩散时间表将噪声逐渐添加到图像中,比线性扩散时间表更有效地提高了训练效率和生成质量。这也可以在被线性和余弦时间表破坏的图像中看到(图 8-5)。

        图 8-5. 一个图像被线性(顶部)和余弦(底部)扩散时间表破坏,从 0 到 T 的等间距值(来源:Ho 等人,2020

        反向扩散过程

        现在让我们看一下反向扩散过程。简而言之,我们要构建一个神经网络 p θ ( 𝐱 t-1 | 𝐱 t ),它可以撤销扩散过程,即近似反向分布 q ( 𝐱 t-1 | 𝐱 t )。如果我们能做到这一点,我们可以从 𝒩 ( 0 , 𝐈 ) 中随机采样噪声,然后多次应用反向扩散过程以生成新颖的图像。这在图 8-6 中可视化。

        图 8-6。反向扩散过程p θ . ( 𝐱 t-1 | 𝐱 t )试图撤消由正向扩散过程产生的噪声

        反向扩散过程和变分自动编码器的解码器之间存在许多相似之处。 在两者中,我们的目标都是使用神经网络将随机噪声转换为有意义的输出。 扩散模型和 VAE 之间的区别在于,在 VAE 中,正向过程(将图像转换为噪声)是模型的一部分(即,它是学习的),而在扩散模型中,它是非参数化的。

        因此,将与变分自动编码器中相同的损失函数应用是有意义的。 原始的 DDPM 论文推导出了这个损失函数的确切形式,并表明可以通过训练一个网络ϵ θ来预测已添加到给定图像𝐱 0的噪声ϵ在时间步t

        换句话说,我们对图像𝐱 0进行采样,并通过t个噪声步骤将其转换为图像𝐱 t = α ¯ t 𝐱 0 + 1 - α ¯ t ϵ。 我们将这个新图像和噪声率α ¯ t提供给神经网络,并要求它预测ϵ,采取梯度步骤来计算预测ϵ θ ( 𝐱 t )和真实ϵ之间的平方误差。

        我们将在下一节中查看神经网络的结构。 值得注意的是,扩散模型实际上维护了两个网络副本:一个是通过梯度下降主动训练的网络,另一个是权重的指数移动平均(EMA)网络,该网络是在先前的训练步骤中对主动训练网络的权重进行指数移动平均。 EMA 网络不太容易受到训练过程中的短期波动和峰值的影响,因此在生成方面比主动训练网络更稳健。 因此,每当我们想要从网络生成输出时,我们都会使用 EMA 网络。

        模型的训练过程如图 8-7 所示。

        图 8-7。去噪扩散模型的训练过程(来源:Ho 等人,2020

        在 Keras 中,我们可以将这个训练步骤编码为示例 8-5 所示。

        示例 8-5。Keras 扩散模型的train_step函数
        class DiffusionModel(models.Model):
            def __init__(self):
                super().__init__()
                self.normalizer = layers.Normalization()
                self.network = unet
                self.ema_network = models.clone_model(self.network)
                self.diffusion_schedule = cosine_diffusion_schedule
        
            ...
        
            def denoise(self, noisy_images, noise_rates, signal_rates, training):
                if training:
                    network = self.network
                else:
                    network = self.ema_network
                pred_noises = network(
                    [noisy_images, noise_rates**2], training=training
                )
                pred_images = (noisy_images - noise_rates * pred_noises) / signal_rates
        
                return pred_noises, pred_images
        
            def train_step(self, images):
                images = self.normalizer(images, training=True) # ①
                noises = tf.random.normal(shape=tf.shape(images)) # ②
                batch_size = tf.shape(images)[0]
                diffusion_times = tf.random.uniform(
                    shape=(batch_size, 1, 1, 1), minval=0.0, maxval=1.0
                ) # ③
                noise_rates, signal_rates = self.cosine_diffusion_schedule(
                    diffusion_times
                ) # ④
                noisy_images = signal_rates * images + noise_rates * noises # ⑤
                with tf.GradientTape() as tape:
                    pred_noises, pred_images = self.denoise(
                        noisy_images, noise_rates, signal_rates, training=True
                    ) # ⑥
                    noise_loss = self.loss(noises, pred_noises)  # ⑦
                gradients = tape.gradient(noise_loss, self.network.trainable_weights)
                self.optimizer.apply_gradients(
                    zip(gradients, self.network.trainable_weights)
                ) # ⑧
                self.noise_loss_tracker.update_state(noise_loss)
        
                for weight, ema_weight in zip(
                    self.network.weights, self.ema_network.weights
                ):
                    ema_weight.assign(0.999 * ema_weight + (1 - 0.999) * weight) # ⑨
        
                return {m.name: m.result() for m in self.metrics}
        
            ...
        

        我们首先将图像批次归一化为零均值和单位方差。

        接下来,我们对形状与输入图像匹配的噪声进行采样。

        我们还对随机扩散时间进行采样…​

        …并使用这些根据余弦扩散计划生成噪声和信号速率。

        然后我们将信号和噪声权重应用于输入图像以生成嘈杂的图像。

        接下来,我们通过要求网络预测噪声然后撤消添加噪声的操作,使用提供的noise_ratessignal_rates来去噪嘈杂的图像。

        然后我们可以计算预测噪声和真实噪声之间的损失(平均绝对误差)…​

        …​并根据这个损失函数采取梯度步骤。

        EMA 网络权重更新为现有 EMA 权重和训练后的网络权重在梯度步骤后的加权平均值。

        U-Net 去噪模型

        现在我们已经看到了我们需要构建的神经网络的类型(一个预测添加到给定图像的噪声的网络),我们可以看一下使这种可能的架构。

        DDPM 论文的作者使用了一种称为U-Net的架构类型。这个网络的图表显示在图 8-8 中,明确显示了张量在通过网络时的形状。

        图 8-8. U-Net 架构图

        类似于变分自动编码器,U-Net 由两部分组成:下采样部分,其中输入图像在空间上被压缩但在通道上被扩展,以及上采样部分,其中表示在空间上被扩展,而通道数量减少。然而,与 VAE 不同的是,在网络的上采样和下采样部分之间还有跳跃连接。VAE 是顺序的;数据从输入到输出依次通过网络的每一层。U-Net 不同,因为跳跃连接允许信息绕过网络的部分并流向后续层。

        当我们希望输出具有与输入相同的形状时,U-Net 特别有用。在我们的扩散模型示例中,我们希望预测添加到图像中的噪声,这个噪声与图像本身的形状完全相同,因此 U-Net 是网络架构的自然选择。

        首先让我们看一下在 Keras 中构建这个 U-Net 的代码,显示在示例 8-6 中。

        示例 8-6. Keras 中的 U-Net 模型
        noisy_images = layers.Input(shape=(64, 64, 3)) # ①
        x = layers.Conv2D(32, kernel_size=1)(noisy_images) # ②
        
        noise_variances = layers.Input(shape=(1, 1, 1)) # ③
        noise_embedding = layers.Lambda(sinusoidal_embedding)(noise_variances) # ④
        noise_embedding = layers.UpSampling2D(size=64, interpolation="nearest")(
            noise_embedding
        ) # ⑤
        
        x = layers.Concatenate()([x, noise_embedding]) # ⑥
        
        skips = [] # ⑦
        
        x = DownBlock(32, block_depth = 2)([x, skips]) # ⑧
        x = DownBlock(64, block_depth = 2)([x, skips])
        x = DownBlock(96, block_depth = 2)([x, skips])
        
        x = ResidualBlock(128)(x) # ⑨
        x = ResidualBlock(128)(x)
        
        x = UpBlock(96, block_depth = 2)([x, skips]) # ⑩
        x = UpBlock(64, block_depth = 2)([x, skips])
        x = UpBlock(32, block_depth = 2)([x, skips])
        
        x = layers.Conv2D(3, kernel_size=1, kernel_initializer="zeros")(x) # ⑪ 
        
        unet = models.Model([noisy_images, noise_variances], x, name="unet") # ⑫
        

        U-Net 的第一个输入是我们希望去噪的图像。

        这个图像通过一个Conv2D层传递,以增加通道数量。

        U-Net 的第二个输入是噪声方差(一个标量)。

        这是使用正弦嵌入编码的。

        这个嵌入被复制到空间维度以匹配输入图像的大小。

        两个输入流在通道上连接。

        skips列表将保存我们希望连接到下游UpBlock层的DownBlock层的输出。

        张量通过一系列DownBlock层传递,这些层减小了图像的大小,同时增加了通道的数量。

        然后,张量通过两个ResidualBlock层传递,这些层保持图像大小和通道数量恒定。

        接下来,张量通过一系列UpBlock层传递,这些层增加图像的大小,同时减少通道数。跳跃连接将输出与较早的DownBlock层的输出合并。

        最终的Conv2D层将通道数减少到三(RGB)。

        U-Net 是一个 Keras Model,它以嘈杂的图像和噪声方差作为输入,并输出预测的噪声图。

        要详细了解 U-Net,我们需要探索四个概念:噪声方差的正弦嵌入、ResidualBlockDownBlockUpBlock

        正弦嵌入

        正弦嵌入最初是由 Vaswani 等人在一篇论文中引入的。我们将使用 Mildenhall 等人在题为“NeRF: Representing Scenes as Neural Radiance Fields for View Synthesis”的论文中使用的这个原始想法的改编。

        我们希望能够将标量值(噪声方差)转换为一个不同的高维向量,能够提供更复杂的表示,以便在网络中下游使用。原始论文使用这个想法将句子中单词的离散位置编码为向量;NeRF 论文将这个想法扩展到连续值。

        具体来说,标量值x被编码如下方程所示:

        γ ( x ) = ( sin ( 2 π e 0f x ) , , sin ( 2 π e (L-1)f) x ) , cos ( 2 π e 0f x ) , , cos ( 2 π e (L-1)f x ) )

        其中我们选择L = 16,是我们期望的噪声嵌入长度的一半,f = ln(1000) L-1是频率的最大缩放因子。

        这产生了图 8-9 中显示的嵌入模式。

        图 8-9。噪声方差从 0 到 1 的正弦嵌入模式

        我们可以将这个正弦嵌入函数编码如示例 8-7 所示。这将一个单一的噪声方差标量值转换为长度为 32 的向量。

        示例 8-7。编码噪声方差的sinusoidal_embedding函数
        def sinusoidal_embedding(x):
            frequencies = tf.exp(
                tf.linspace(
                    tf.math.log(1.0),
                    tf.math.log(1000.0),
                    16,
                )
            )
            angular_speeds = 2.0 * math.pi * frequencies
            embeddings = tf.concat(
                [tf.sin(angular_speeds * x), tf.cos(angular_speeds * x)], axis=3
            )
            return embeddings
        

        残差块

        DownBlockUpBlock都包含ResidualBlock层,所以让我们从这些层开始。我们在第五章中构建 PixelCNN 时已经探讨过残差块,但为了完整起见,我们将在这里进行回顾。

        残差块是一组包含跳跃连接的层,将输入添加到输出中。残差块帮助我们构建更深的网络,可以学习更复杂的模式,而不会受到梯度消失和退化问题的严重影响。梯度消失问题是指随着网络变得更深,通过更深层传播的梯度很小,因此学习速度非常慢。退化问题是指随着神经网络变得更深,它们不一定像较浅的对应网络那样准确——准确性似乎在一定深度上饱和,然后迅速退化。

        退化

        退化问题有点反直觉,但在实践中观察到,因为更深的层至少必须学习恒等映射,这并不是微不足道的——尤其考虑到更深的网络面临的其他问题,比如梯度消失问题。

        这个解决方案最初在 2015 年 He 等人的 ResNet 论文中首次提出。通过在主要加权层周围包含一个跳跃连接高速公路,块可以选择绕过复杂的权重更新,简单地通过恒等映射传递。这使得网络可以在不牺牲梯度大小或网络准确性的情况下进行深度训练。

        ResidualBlock的图示如图 8-10 所示。请注意,在一些残差块中,我们还在跳跃连接上包含一个额外的具有核大小 1 的Conv2D层,以使通道数与块的其余部分保持一致。

        图 8-10。U-Net 中的ResidualBlock

        我们可以像示例 8-8 中所示的那样在 Keras 中编写ResidualBlock

        示例 8-8。U-Net 中的ResidualBlock代码
        def ResidualBlock(width):
            def apply(x):
                input_width = x.shape[3]
                if input_width == width: # ①
                    residual = x
                else:
                    residual = layers.Conv2D(width, kernel_size=1)(x)
                x = layers.BatchNormalization(center=False, scale=False)(x) # ②
                x = layers.Conv2D(
                    width, kernel_size=3, padding="same", activation=activations.swish
                )(x) # ③
                x = layers.Conv2D(width, kernel_size=3, padding="same")(x)
                x = layers.Add()([x, residual]) # ④
                return x
        
            return apply
        

        检查输入中的通道数是否与我们希望该块输出的通道数匹配。如果不匹配,可以在跳跃连接上包含额外的Conv2D层,以使通道数与块的其余部分保持一致。

        应用BatchNormalization层。

        应用两个Conv2D层。

        将原始块输入添加到输出中,以提供块的最终输出。

        DownBlocks 和 UpBlocks

        每个连续的DownBlock通过block_depth(在我们的示例中为 2)ResidualBlock增加通道数,同时还应用最终的AveragePooling2D层以将图像尺寸减半。每个ResidualBlock都添加到一个列表中,以便稍后由UpBlock层作为 U-Net 中的跳跃连接使用。

        UpBlock首先应用一个UpSampling2D层,通过双线性插值将图像大小加倍。每个连续的UpBlock通过block_depth(=2)ResidualBlock减少通道数,同时还通过 U-Net 中的跳跃连接将DownBlock的输出连接起来。这个过程的图示如图 8-11 所示。

        图 8-11。U-Net 中的DownBlock和相应的UpBlock

        我们可以使用 Keras 编写DownBlockUpBlock,如示例 8-9 所示。

        示例 8-9。U-Net 模型中的DownBlockUpBlock代码
        def DownBlock(width, block_depth):
            def apply(x):
                x, skips = x
                for _ in range(block_depth):
                    x = ResidualBlock(width)(x) # ①
                    skips.append(x) # ②
                x = layers.AveragePooling2D(pool_size=2)(x) # ③
                return x
        
            return apply
        
        def UpBlock(width, block_depth):
            def apply(x):
                x, skips = x
                x = layers.UpSampling2D(size=2, interpolation="bilinear")(x) # ④
                for _ in range(block_depth):
                    x = layers.Concatenate()([x, skips.pop()]) # ⑤
                    x = ResidualBlock(width)(x) # ⑥
                return x
        
            return apply
        

        DownBlock通过给定widthResidualBlock增加图像中的通道数…​

        …每个都保存在一个列表(skips)中,以便稍后由UpBlock使用。

        最终的AveragePooling2D层将图像的维度减半。

        UpBlock从一个UpSampling2D层开始,将图像大小加倍。

        DownBlock层的输出通过Concatenate层连接到当前输出。

        ResidualBlock用于在图像通过UpBlock时减少通道数。

        训练扩散模型

        现在我们已经准备好训练我们的去噪扩散模型了!示例 8-10 创建、编译和拟合扩散模型。

        示例 8-10。训练DiffusionModel的代码
        model = DiffusionModel() # ①
        model.compile(
            optimizer=optimizers.experimental.AdamW(learning_rate=1e-3, weight_decay=1e-4),
            loss=losses.mean_absolute_error,
        ) # ②
        
        model.normalizer.adapt(train) # ③
        
        model.fit(
            train,
            epochs=50,
        ) # ④
        

        实例化模型。

        编译模型,使用 AdamW 优化器(类似于 Adam,但带有权重衰减,有助于稳定训练过程)和平均绝对误差损失函数。

        使用训练集计算归一化统计数据。

        在 50 个时代内拟合模型。

        损失曲线(噪音平均绝对误差[MAE])显示在图 8-12 中。

        图 8-12。噪音平均绝对误差损失曲线,按时代

        从去噪扩散模型中采样

        为了从我们训练好的模型中采样图像,我们需要应用反向扩散过程-也就是说,我们需要从随机噪音开始,并使用模型逐渐消除噪音,直到我们得到一个可以识别的花朵图片。

        我们必须记住,我们的模型是经过训练的,用于预测在训练集中添加到给定嘈杂图像的总噪音量,而不仅仅是在噪音过程的最后一个时间步骤中添加的噪音。然而,我们不希望一次性消除所有噪音-在一次预测中从纯随机噪音中预测图像显然不会奏效!我们宁愿模仿正向过程,并在许多小步骤中逐渐消除预测的噪音,以使模型能够适应自己的预测。

        为了实现这一点,我们可以在两个步骤中从x t跳到x t-1,首先使用我们模型的噪音预测来计算原始图像x 0的估计,然后重新应用预测的噪音到这个图像,但只在t - 1个时间步骤内,产生x t-1。这个想法在图 8-13 中显示。

        图 8-13。扩散模型采样过程的一步

        如果我们重复这个过程多次,最终我们将得到一个经过许多小步骤逐渐引导的x 0的估计。实际上,我们可以自由选择采取的步数,关键是,它不必与训练噪音过程中的大量步数(即 1,000)相同。它可以小得多-在这个例子中,我们选择了 20。

        以下方程(Song 等,2020)数学上描述了这个过程:

        𝐱 t-1 = α ¯ t-1 𝐱 t -1-α ¯ t ϵ θ (t) (𝐱 t ) α ¯ t predicted𝐱 0 + 1-α ¯ t-1 -σ t 2 ·ϵ θ (t) (𝐱 t ) directionpointingto𝐱 t + σ t ϵ t randomnoise

        让我们来分解一下。方程式右侧括号内的第一个项是估计的图像 x 0,使用我们网络预测的噪声 ϵ θ (t) 计算得到。然后我们通过 t - 1 信号率 α ¯ t-1 缩放这个值,并重新应用预测的噪声,但这次是通过 t - 1 噪声率 1 - α ¯ t-1 - σ t 2 进行缩放。还添加了额外的高斯随机噪声 σ t ϵ t,其中 σ t 确定了我们希望生成过程有多随机。

        特殊情况 σ t = 0 对于所有的 t 对应于一种称为去噪扩散隐式模型(DDIM)的模型,由 Song 等人在 2020 年提出。⁹ 使用 DDIM,生成过程完全是确定性的—也就是说,相同的随机噪声输入将始终产生相同的输出。这是可取的,因为这样我们在潜在空间的样本和像素空间中生成的输出之间有一个明确定义的映射。

        在我们的示例中,我们将实现一个 DDIM,从而使我们的生成过程确定性。DDIM 采样过程(反向扩散)的代码显示在示例 8-11 中。

        示例 8-11. 从扩散模型中采样
        class DiffusionModel(models.Model):
        
        ...
        
            def reverse_diffusion(self, initial_noise, diffusion_steps):
                num_images = initial_noise.shape[0]
                step_size = 1.0 / diffusion_steps
                current_images = initial_noise
                for step in range(diffusion_steps): # ①
                    diffusion_times = tf.ones((num_images, 1, 1, 1)) - step * step_size # ②
                    noise_rates, signal_rates = self.diffusion_schedule(diffusion_times) # ③
                    pred_noises, pred_images = self.denoise(
                        current_images, noise_rates, signal_rates, training=False
                    ) # ④
                    next_diffusion_times = diffusion_times - step_size # ⑤
                    next_noise_rates, next_signal_rates = self.diffusion_schedule(
                        next_diffusion_times
                    ) # ⑥
                    current_images = (
                        next_signal_rates * pred_images + next_noise_rates * pred_noises
                    ) # ⑦
                return pred_images # ⑧
        

        观察固定数量的步骤(例如,20 步)。

        扩散时间都设置为 1(即在反向扩散过程开始时)。

        根据扩散计划计算噪声和信号率。

        U-Net 用于预测噪声,从而使我们能够计算去噪图像的估计。

        扩散时间减少一步。

        计算新的噪声和信号率。

        通过根据扩散计划率重新应用预测噪声到预测图像,计算出 t-1 图像。

        经过 20 步,最终的 𝐱 0 预测图像被返回。

        扩散模型的分析

        现在我们将看一下我们训练模型的三种不同用法:用于生成新图像,测试反向扩散步数如何影响质量,以及在潜在空间中两个图像之间的插值。

        生成图像

        为了从我们训练的模型中生成样本,我们只需运行逆扩散过程,确保最终去标准化输出(即,将像素值带回范围[0, 1])。我们可以在DiffusionModel类中使用示例 8-12 中的代码来实现这一点。

        示例 8-12。使用扩散模型生成图像
        class DiffusionModel(models.Model):
        
        ...
        
            def denormalize(self, images):
                images = self.normalizer.mean + images * self.normalizer.variance**0.5 # ①
                return tf.clip_by_value(images, 0.0, 1.0)
        
            def generate(self, num_images, diffusion_steps):
                initial_noise = tf.random.normal(shape=(num_images, 64, 64, 3)) # ①
                generated_images = self.reverse_diffusion(initial_noise, diffusion_steps) # ②
                generated_images = self.denormalize(generated_images) # ③
                return generated_images
        

        生成一些初始噪声图。

        应用逆扩散过程。

        网络输出的图像将具有零均值和单位方差,因此我们需要通过重新应用从训练数据计算得出的均值和方差来去标准化。

        在图 8-14 中,我们可以观察到训练过程中不同时期扩散模型的一些样本。

        图 8-14。训练过程中不同时期扩散模型的样本

        调整扩散步数

        我们还可以测试调整逆向过程中扩散步数如何影响图像质量。直观地,过程中步数越多,图像生成的质量就越高。

        我们可以在图 8-15 中看到,随着扩散步数的增加,生成的质量确实会提高。从初始抽样的噪声中一次性跳跃,模型只能预测出一个朦胧的颜色斑块。随着步数的增加,模型能够改进和锐化生成物。然而,生成图像所需的时间与扩散步数成线性关系,因此存在权衡。在 20 和 100 个扩散步之间的改进很小,因此在这个例子中我们选择 20 作为质量和速度之间的合理折衷。

        图 8-15。随着扩散步数的增加,图像质量提高

        在图像之间进行插值

        最后,正如我们之前在变分自动编码器中看到的那样,我们可以在高斯潜在空间中的点之间进行插值,以便在像素空间中平滑过渡。在这里,我们选择使用一种球面插值的形式,确保方差在混合两个高斯噪声图之间保持恒定。具体来说,每一步的初始噪声图由a sin ( π 2 t ) + b cos ( π 2 t )给出,其中t从 0 平滑地变化到 1,ab是我们希望在其间插值的两个随机抽样的高斯噪声张量。

        生成的图像显示在图 8-16 中。

        图 8-16。使用去噪扩散模型在图像之间进行插值 # 总结

        在本章中,我们探索了近期最令人兴奋和有前途的生成建模领域之一:扩散模型。特别是,我们实现了一篇关于生成扩散模型的关键论文(Ho 等人,2020)中介绍的原始去噪扩散概率模型(DDPM)的思想。然后,我们借鉴了去噪扩散隐式模型(DDIM)论文中的思想,使生成过程完全确定性。

        我们已经看到扩散模型由前向扩散过程和逆扩散过程组成。 前向扩散过程通过一系列小步骤向训练数据添加噪声,而逆扩散过程包括试图预测添加的噪声的模型。

        我们利用重新参数化技巧,以便在前向过程的任何步骤中计算带噪声的图像,而无需经历多个加噪步骤。 我们已经看到,用于向数据添加噪声的参数选择计划在模型的整体成功中起着重要作用。

        逆扩散过程由一个 U-Net 参数化,试图在每个时间步预测噪声,给定在该步骤的噪声图像和噪声率。 U-Net 由DownBlock组成,它们增加通道数同时减小图像的大小,以及UpBlock,它们减少通道数同时增加大小。 噪声率使用正弦嵌入进行编码。

        从扩散模型中进行采样是在一系列步骤中进行的。 使用 U-Net 来预测添加到给定噪声图像的噪声,然后用于计算原始图像的估计。 然后使用较小的噪声率重新应用预测的噪声。 从标准高斯噪声分布中随机抽取的随机点开始,重复这个过程一系列步骤(可能明显小于训练过程中使用的步骤数),以获得最终生成。

        我们看到,在逆过程中增加扩散步骤的数量会提高图像生成质量,但会降低速度。 我们还执行了潜在空间算术,以在两个图像之间插值。

        ¹ Jascha Sohl-Dickstein 等,“使用非平衡热力学进行深度无监督学习”,2015 年 3 月 12 日,https://arxiv.org/abs/1503.03585

        ² 杨松和 Stefano Ermon,“通过估计数据分布的梯度进行生成建模”,2019 年 7 月 12 日,https://arxiv.org/abs/1907.05600

        ³ 杨松和 Stefano Ermon,“改进训练基于分数的生成模型的技术”,2020 年 6 月 16 日,https://arxiv.org/abs/2006.09011

        ⁴ Jonathon Ho 等,“去噪扩散概率模型”,2020 年 6 月 19 日,https://arxiv.org/abs/2006.11239

        ⁵ Alex Nichol 和 Prafulla Dhariwal,“改进去噪扩散概率模型”,2021 年 2 月 18 日,https://arxiv.org/abs/2102.09672

        ⁶ Ashish Vaswani 等,“注意力就是一切”,2017 年 6 月 12 日,https://arxiv.org/abs/1706.03762

        ⁷ Ben Mildenhall 等,“NeRF:将场景表示为神经辐射场进行视图合成”,2020 年 3 月 1 日,https://arxiv.org/abs/2003.08934

        ⁸ Kaiming He 等,“用于图像识别的深度残差学习”,2015 年 12 月 10 日,https://arxiv.org/abs/1512.03385

        ⁹ 宋嘉明等,“去噪扩散隐式模型”,2020 年 10 月 6 日,https://arxiv.org/abs/2010.02502`

        第三部分:应用

        在第三部分中,我们将探索迄今为止所见的生成建模技术在图像、文本、音乐和游戏等领域的一些关键应用。我们还将看到如何使用最先进的多模态模型穿越这些领域。

        在第九章中,我们将把注意力转向 Transformers,这是一种现代文本生成模型的先进架构。特别是,我们将探索 GPT 的内部工作原理,并使用 Keras 构建我们自己的版本,我们将看到它如何构建了诸如 ChatGPT 之类的工具的基础。

        在第十章中,我们将看一些对图像生成产生影响的最重要的 GAN 架构,包括 ProGAN、StyleGAN、StyleGAN2、SAGAN、BigGAN、VQ-GAN 和 ViT VQ-GAN。我们将探索每个架构的关键贡献,并了解这种技术如何随着时间的推移而发展。

        第十一章探讨音乐生成,这带来了额外的挑战,比如对音乐音高和节奏进行建模。我们将看到许多适用于文本生成的技术(如 Transformers)也可以应用于这个领域,但我们还将探索一种称为 MuseGAN 的深度学习架构,该架构应用了基于 GAN 的方法来生成音乐。

        第十二章展示了生成模型如何在其他机器学习领域中使用,比如强化学习。我们将重点关注“世界模型”论文,该论文展示了如何将生成模型用作代理训练的环境,使其能够在幻想的梦境版本的环境中进行训练,而不是真实环境。

        在第十三章中,我们将探索跨越图像和文本等领域的最先进的多模态模型。这包括文本到图像模型,如 DALL.E 2、Imagen 和 Stable Diffusion,以及视觉语言模型,如 Flamingo。

        最后,在第十四章中总结了迄今为止的生成人工智能之旅,当前的生成人工智能格局,以及我们未来可能走向何方。我们将探讨生成人工智能如何改变我们的生活和工作方式,以及考虑它是否有潜力在未来几年解锁更深层次的人工智能形式。

        第九章:Transformer

        我们在第五章中看到,我们可以使用循环神经网络(RNNs)(如 LSTM 和 GRU)在文本数据上构建生成模型。这些自回归模型一次处理一个令牌的顺序数据,不断更新一个捕获输入当前潜在表示的隐藏向量。可以设计 RNN 以通过在隐藏向量上应用密集层和 softmax 激活来预测序列中的下一个单词。直到 2017 年,这被认为是生成文本的最复杂方式,当一篇论文永久改变了文本生成的格局。

        介绍

        谷歌 Brain 的论文,自信地命名为“注意力就是一切”¹,因推广注意力的概念而闻名,这个概念现在驱动着大多数最先进的文本生成模型。

        作者展示了如何创建称为Transformer的强大神经网络,用于顺序建模,而不需要复杂的循环或卷积架构,而只依赖于注意机制。这种方法克服了 RNN 方法的一个关键缺点,即难以并行化,因为它必须一次处理一个令牌的序列。Transformer 是高度可并行化的,使它们能够在大规模数据集上进行训练。

        在本章中,我们将深入探讨现代文本生成模型如何利用 Transformer 架构在文本生成挑战中达到最先进的性能。特别是,我们将探索一种称为生成式预训练 Transformer(GPT)的自回归模型,它驱动着 OpenAI 的 GPT-4 模型,被广泛认为是当前文本生成领域的最先进技术。

        GPT

        OpenAI 于 2018 年 6 月推出了 GPT,在论文“通过生成式预训练改进语言理解”中²,几乎与原始 Transformer 论文出现一年后完全一致。

        在本文中,作者展示了如何训练 Transformer 架构以预测序列中的下一个单词,然后随后对特定下游任务进行微调。

        GPT 的预训练过程涉及在名为 BookCorpus 的大型文本语料库上训练模型(来自不同流派的 7,000 本未发表书籍的 4.5 GB 文本)。在预训练期间,模型被训练以预测给定前面单词的序列中的下一个单词。这个过程被称为语言建模,用于教导模型理解自然语言的结构和模式。

        在预训练之后,GPT 模型可以通过提供较小的、特定于任务的数据集来进行微调以适应特定任务。微调涉及调整模型的参数以更好地适应手头的任务。例如,模型可以针对分类、相似性评分或问题回答等任务进行微调。

        自 GPT 架构推出以来,OpenAI 通过发布后续模型如 GPT-2、GPT-3、GPT-3.5 和 GPT-4 对其进行了改进和扩展。这些模型在更大的数据集上进行训练,并具有更大的容量,因此可以生成更复杂和连贯的文本。研究人员和行业从业者广泛采用了 GPT 模型,并为自然语言处理任务的重大进展做出了贡献。

        在本章中,我们将构建我们自己的变体 GPT 模型,该模型在较少数据上进行训练,但仍利用相同的组件和基本原则。

        运行此示例的代码

        此示例的代码可以在位于书籍存储库中的 Jupyter 笔记本中找到,位置为notebooks/09_transformer/01_gpt/gpt.ipynb

        该代码改编自由 Apoorv Nandan 创建的优秀GPT 教程,该教程可在 Keras 网站上找到。

        葡萄酒评论数据集

        我们将使用通过 Kaggle 提供的Wine Reviews 数据集。这是一个包含超过 130,000 条葡萄酒评论的数据集,附带元数据,如描述和价格。

        您可以通过在书库中运行 Kaggle 数据集下载脚本来下载数据集,如示例 9-1 所示。这将把葡萄酒评论和相关元数据保存在本地的/data文件夹中。

        示例 9-1. 下载葡萄酒评论数据集
        bash scripts/download_kaggle_data.sh zynicide wine-reviews
        

        `数据准备步骤与第五章中用于准备输入到 LSTM 的数据的步骤是相同的,因此我们不会在这里详细重复它们。如图 9-1 所示,步骤如下:

        1. 加载数据并创建每种葡萄酒的文本字符串描述列表。

        2. 用空格填充标点符号,以便每个标点符号被视为一个单独的单词。

        3. 通过TextVectorization层将字符串传递,对数据进行标记化,并将每个字符串填充/裁剪到固定长度。

        4. 创建一个训练集,其中输入是标记化的文本字符串,输出是预测的相同字符串向后移动一个标记。

        图 9-1. Transformer 的数据处理 ## 注意力

        了解 GPT 如何工作的第一步是了解注意力机制的工作原理。这个机制是使 Transformer 架构与循环方法在语言建模方面独特和不同的地方。当我们对注意力有了扎实的理解后,我们将看到它如何在 GPT 等 Transformer 架构中使用。

        当您写作时,句子中下一个词的选择受到您已经写过的其他单词的影响。例如,假设您开始一个句子如下:

        The pink elephant tried to get into the car but it was too
        

        显然,下一个词应该是与big同义的。我们怎么知道这一点?

        句子中的某些其他单词对帮助我们做出决定很重要。例如,它是大象而不是树懒,意味着我们更喜欢big而不是slow。如果它是游泳池而不是汽车,我们可能会选择scared作为big的一个可能替代。最后,getting into汽车的行为意味着大小是问题所在——如果大象试图压扁汽车,我们可能会选择fast作为最后一个词,现在it指的是汽车。

        句子中的其他单词一点都不重要。例如,大象是粉红色这个事实对我们选择最终词汇没有影响。同样,句子中的次要单词(thebutit等)给句子以语法形式,但在这里并不重要,以确定所需形容词。

        换句话说,我们正在关注句子中的某些单词,而基本上忽略其他单词。如果我们的模型也能做同样的事情,那不是很好吗?

        Transformer 中的注意力机制(也称为注意力头)旨在做到这一点。它能够决定从输入的哪个位置提取信息,以有效地提取有用信息而不被无关细节混淆。这使得它非常适应各种情况,因为它可以在推断时决定在哪里寻找信息。

        相比之下,循环层试图建立一个捕捉每个时间步输入的整体表示的通用隐藏状态。这种方法的一个弱点是,已经合并到隐藏向量中的许多单词对当前任务(例如,预测下一个单词)并不直接相关,正如我们刚刚看到的。注意力头不会遇到这个问题,因为它们可以选择如何从附近的单词中组合信息,具体取决于上下文。

        查询、键和值

        那么,注意力头如何决定在哪里查找信息呢?在深入细节之前,让我们以高层次的方式探讨它是如何工作的,使用我们的粉色大象示例。

        想象一下,我们想预测跟在单词too后面的是什么。为了帮助完成这个任务,其他前面的单词发表意见,但他们的贡献受到他们对自己预测跟在too后面的单词的信心程度的加权。例如,单词elephant可能自信地贡献说,它更有可能是与大小或响度相关的单词,而单词was没有太多可以提供来缩小可能性。

        换句话说,我们可以将注意力头视为一种信息检索系统,其中一个“查询”(“后面跟着什么词?”)被转换为一个键/值存储(句子中的其他单词),输出结果是值的加权和,权重由查询和每个键之间的共鸣决定。

        我们现在将详细介绍这个过程(图 9-2),再次参考我们的粉色大象句子。

        图 9-2。注意力头的机制

        查询Q)可以被视为当前任务的表示(例如,“后面跟着什么词?”)。在这个例子中,它是从单词too的嵌入中导出的,通过将其通过权重矩阵W Q传递来将向量的维度从d e更改为d k

        向量(K)是句子中每个单词的表示——您可以将这些视为每个单词可以帮助的预测任务的描述。它们以类似的方式导出查询,通过将每个嵌入通过权重矩阵W K传递来将每个向量的维度从d e更改为d k。请注意,键和查询具有相同的长度(d k)。

        在注意力头内部,每个键与查询之间的向量对之间使用点积进行比较(Q K T)。这就是为什么键和查询必须具有相同的长度。对于特定的键/查询对,这个数字越高,键与查询的共鸣就越强,因此它可以更多地对注意力头的输出做出贡献。结果向量被缩放为d k,以保持向量和的方差稳定(大约等于 1),并且应用 softmax 以确保贡献总和为 1。这是一个注意力权重向量。

        向量(V)也是句子中单词的表示——您可以将这些视为每个单词的未加权贡献。它们通过将每个嵌入通过权重矩阵W V传递来导出,以将每个向量的维度从d e更改为d v。请注意,值向量不一定要与键和查询具有相同的长度(但通常为了简单起见)。

        值向量乘以注意力权重,给出给定QKV注意力,如方程 9-1 所示。

        方程 9-1。注意力方程

        A t t e n t i o n ( Q , K , V ) = s o f t m a x ( QK T d k ) V

        从注意力头中获取最终输出向量,将注意力求和得到长度为d v的向量。这个上下文向量捕捉了句子中单词对于预测接下来的单词是什么的任务的混合意见。

        多头注意力

        没有理由只停留在一个注意力头上!在 Keras 中,我们可以构建一个MultiHeadAttention层,将多个注意力头的输出连接起来,使每个头学习不同的注意力机制,从而使整个层能够学习更复杂的关系。

        连接的输出通过一个最终的权重矩阵W O传递,将向量投影到所需的输出维度,这在我们的情况下与查询的输入维度相同(d e),以便层可以顺序堆叠在一起。

        图 9-3 展示了一个MultiHeadAttention层的输出是如何构建的。在 Keras 中,我们可以简单地写下示例 9-2 中显示的代码来创建这样一个层。

        示例 9-2。在 Keras 中创建一个MultiHeadAttention
        layers.MultiHeadAttention(
            num_heads = 4, # ①
            key_dim = 128, # ②
            value_dim = 64, # ③
            output_shape = 256 # ④
            )
        

        这个多头注意力层有四个头。

        键(和查询)是长度为 128 的向量。

        值(因此也是每个头的输出)是长度为 64 的向量。

        输出向量的长度为 256。

        图 9-3。一个具有四个头的多头注意力层

        因果掩码

        到目前为止,我们假设我们的注意力头的查询输入是一个单一的向量。然而,在训练期间为了效率,我们理想情况下希望注意力层能够一次操作输入中的每个单词,为每个单词预测接下来的单词。换句话说,我们希望我们的 GPT 模型能够并行处理一组查询向量(即一个矩阵)。

        您可能会认为我们可以将向量批量处理成一个矩阵,让线性代数处理剩下的部分。这是正确的,但我们需要一个额外的步骤——我们需要对查询/键的点积应用一个掩码,以避免未来单词的信息泄漏。这被称为因果掩码,在图 9-4 中显示。

        图 9-4。对一批输入查询计算注意力分数的矩阵,使用因果注意力掩码隐藏对查询不可用的键(因为它们在句子中后面)

        如果没有这个掩码,我们的 GPT 模型将能够完美地猜测句子中的下一个单词,因为它将使用单词本身的键作为特征!创建因果掩码的代码显示在示例 9-3 中,结果的numpy数组(转置以匹配图表)显示在图 9-5 中。

        示例 9-3。因果掩码函数
        def causal_attention_mask(batch_size, n_dest, n_src, dtype):
            i = tf.range(n_dest)[:, None]
            j = tf.range(n_src)
            m = i >= j - n_src + n_dest
            mask = tf.cast(m, dtype)
            mask = tf.reshape(mask, [1, n_dest, n_src])
            mult = tf.concat(
                [tf.expand_dims(batch_size, -1), tf.constant([1, 1], dtype=tf.int32)], 0
            )
            return tf.tile(mask, mult)
        
        np.transpose(causal_attention_mask(1, 10, 10, dtype = tf.int32)[0])
        

        图 9-5。作为numpy数组的因果掩码——1 表示未掩码,0 表示掩码
        提示

        因果掩码仅在解码器 Transformer(如 GPT)中需要,其中任务是根据先前的标记顺序生成标记。在训练期间屏蔽未来标记因此至关重要。

        其他类型的 Transformer(例如编码器 Transformer)不需要因果掩码,因为它们不是训练来预测下一个标记。例如,Google 的 BERT 预测给定句子中的掩码单词,因此它可以使用单词之前和之后的上下文。³

        我们将在本章末尾更详细地探讨不同类型的 Transformer。

        这结束了我们对存在于所有 Transformer 中的多头注意力机制的解释。令人惊讶的是,这样一个有影响力的层的可学习参数仅由每个注意力头的三个密集连接权重矩阵(W QW KW V)和一个进一步的权重矩阵来重塑输出(W O)。在多头注意力层中完全没有卷积或循环机制!

        接下来,我们将退一步,看看多头注意力层如何形成更大组件的一部分,这个组件被称为Transformer 块

        Transformer 块

        Transformer 块是 Transformer 中的一个单一组件,它应用一些跳跃连接、前馈(密集)层和在多头注意力层周围的归一化。Transformer 块的示意图显示在图 9-6 中。

        图 9-6。一个 Transformer 块

        首先,注意到查询是如何在多头注意力层周围传递并添加到输出中的——这是一个跳跃连接,在现代深度学习架构中很常见。这意味着我们可以构建非常深的神经网络,不会受到梯度消失问题的困扰,因为跳跃连接提供了一个无梯度的高速公路,允许网络将信息向前传递而不中断。

        其次,在 Transformer 块中使用层归一化来提供训练过程的稳定性。我们已经在本书中看到了批归一化层的作用,其中每个通道的输出被归一化为均值为 0,标准差为 1。归一化统计量是跨批次和空间维度计算的。

        相比之下,在 Transformer 块中,层归一化通过计算跨通道的归一化统计量来归一化批次中每个序列的每个位置。就归一化统计量的计算方式而言,它与批归一化完全相反。显示批归一化和层归一化之间差异的示意图显示在图 9-7 中。

        图 9-7。层归一化与批归一化——归一化统计量是跨蓝色单元计算的(来源:Sheng 等人,2020)⁴

        层归一化与批归一化

        层归一化在原始 GPT 论文中使用,并且通常用于基于文本的任务,以避免在批次中的序列之间创建归一化依赖关系。然而,最近的工作,如 Shen 等人的挑战了这一假设,显示通过一些调整,一种形式的批归一化仍然可以在 Transformer 中使用,胜过更传统的层归一化。

        最后,在 Transformer 块中包含了一组前馈(即密集连接)层,以允许组件在网络深入时提取更高级别的特征。

        在 Keras 中展示了一个 Transformer 块的实现,详见示例 9-4。

        示例 9-4。Keras 中的TransformerBlock
        class TransformerBlock(layers.Layer):
            def __init__(self, num_heads, key_dim, embed_dim, ff_dim, dropout_rate=0.1): # ①
                super(TransformerBlock, self).__init__()
                self.num_heads = num_heads
                self.key_dim = key_dim
                self.embed_dim = embed_dim
                self.ff_dim = ff_dim
                self.dropout_rate = dropout_rate
                self.attn = layers.MultiHeadAttention(
                    num_heads, key_dim, output_shape = embed_dim
                )
                self.dropout_1 = layers.Dropout(self.dropout_rate)
                self.ln_1 = layers.LayerNormalization(epsilon=1e-6)
                self.ffn_1 = layers.Dense(self.ff_dim, activation="relu")
                self.ffn_2 = layers.Dense(self.embed_dim)
                self.dropout_2 = layers.Dropout(self.dropout_rate)
                self.ln_2 = layers.LayerNormalization(epsilon=1e-6)
        
            def call(self, inputs):
                input_shape = tf.shape(inputs)
                batch_size = input_shape[0]
                seq_len = input_shape[1]
                causal_mask = causal_attention_mask(
                    batch_size, seq_len, seq_len, tf.bool
                ) # ②
                attention_output, attention_scores = self.attn(
                    inputs,
                    inputs,
                    attention_mask=causal_mask,
                    return_attention_scores=True
                ) # ③
                attention_output = self.dropout_1(attention_output)
                out1 = self.ln_1(inputs + attention_output) # ④
                ffn_1 = self.ffn_1(out1) # ⑤
                ffn_2 = self.ffn_2(ffn_1)
                ffn_output = self.dropout_2(ffn_2)
                return (self.ln_2(out1 + ffn_output), attention_scores) # ⑥
        

        构成TransformerBlock层的子层在初始化函数中定义。

        因果掩码被创建用来隐藏查询中的未来键。

        创建了多头注意力层,并指定了注意力掩码。

        第一个加和归一化层。

        前馈层。

        第二个加和归一化层。

        位置编码

        在我们能够将所有内容整合在一起训练我们的 GPT 模型之前,还有一个最后的步骤要解决。您可能已经注意到,在多头注意力层中,没有任何关心键的顺序的内容。每个键和查询之间的点积是并行计算的,而不是像递归神经网络那样顺序计算。这是一种优势(因为并行化效率提高),但也是一个问题,因为我们显然需要注意力层能够预测以下两个句子的不同输出:

        • 狗看着男孩然后…(叫?)

        • 男孩看着狗然后…(微笑?)

        为了解决这个问题,我们在创建初始 Transformer 块的输入时使用一种称为位置编码的技术。我们不仅使用标记嵌入对每个标记进行编码,还使用位置嵌入对标记的位置进行编码。

        标记嵌入是使用标准的Embedding层创建的,将每个标记转换为一个学习到的向量。我们可以以相同的方式创建位置嵌入,使用标准的Embedding层将每个整数位置转换为一个学习到的向量。

        提示

        虽然 GPT 使用Embedding层来嵌入位置,但原始 Transformer 论文使用三角函数——我们将在第十一章中介绍这种替代方法,当我们探索音乐生成时。

        为构建联合标记-位置编码,将标记嵌入加到位置嵌入中,如图 9-8 所示。这样,序列中每个单词的含义和位置都被捕捉在一个向量中。

        图 9-8. 将标记嵌入添加到位置嵌入以给出标记位置编码

        定义我们的TokenAndPositionEmbedding层的代码显示在示例 9-5 中。

        示例 9-5. TokenAndPositionEmbedding
        class TokenAndPositionEmbedding(layers.Layer):
            def __init__(self, maxlen, vocab_size, embed_dim):
                super(TokenAndPositionEmbedding, self).__init__()
                self.maxlen = maxlen
                self.vocab_size =vocab_size
                self.embed_dim = embed_dim
                self.token_emb = layers.Embedding(
                    input_dim=vocab_size, output_dim=embed_dim
                ) # ①
                self.pos_emb = layers.Embedding(input_dim=maxlen, output_dim=embed_dim) # ②
        
            def call(self, x):
                maxlen = tf.shape(x)[-1]
                positions = tf.range(start=0, limit=maxlen, delta=1)
                positions = self.pos_emb(positions)
                x = self.token_emb(x)
                return x + positions # ③
        

        标记使用Embedding层进行嵌入。

        标记的位置也使用Embedding层进行嵌入。

        该层的输出是标记和位置嵌入的总和。

        训练 GPT

        现在我们准备构建和训练我们的 GPT 模型!为了将所有内容整合在一起,我们需要将输入文本通过标记和位置嵌入层,然后通过我们的 Transformer 块。网络的最终输出是一个简单的具有 softmax 激活函数的Dense层,覆盖词汇表中的单词数量。

        提示

        为简单起见,我们将只使用一个 Transformer 块,而不是论文中的 12 个。

        整体架构显示在图 9-9 中,相应的代码在示例 9-6 中提供。

        图 9-9. 简化的 GPT 模型架构
        示例 9-6. 在 Keras 中的 GPT 模型
        MAX_LEN = 80
        VOCAB_SIZE = 10000
        EMBEDDING_DIM = 256
        N_HEADS = 2
        KEY_DIM = 256
        FEED_FORWARD_DIM = 256
        
        inputs = layers.Input(shape=(None,), dtype=tf.int32) # ①
        x = TokenAndPositionEmbedding(MAX_LEN, VOCAB_SIZE, EMBEDDING_DIM)(inputs) # ②
        x, attention_scores = TransformerBlock(
            N_HEADS, KEY_DIM, EMBEDDING_DIM, FEED_FORWARD_DIM
        )(x) # ③
        outputs = layers.Dense(VOCAB_SIZE, activation = 'softmax')(x) # ④
        gpt = models.Model(inputs=inputs, outputs=[outputs, attention]) # ⑤
        gpt.compile("adam", loss=[losses.SparseCategoricalCrossentropy(), None]) # ⑥
        gpt.fit(train_ds, epochs=5)
        

        输入被填充(用零填充)。

        文本使用TokenAndPositionEmbedding层进行编码。

        编码通过TransformerBlock传递。

        转换后的输出通过具有 softmax 激活的Dense层传递,以预测后续单词的分布。

        Model以单词标记序列作为输入,并输出预测的后续单词分布。还返回了 Transformer 块的输出,以便我们可以检查模型如何引导其注意力。

        模型使用预测的单词分布上的SparseCategoricalCrossentropy损失进行编译。

        GPT 的分析

        现在我们已经编译并训练了我们的 GPT 模型,我们可以开始使用它生成长文本字符串。我们还可以询问从TransformerBlock输出的注意权重,以了解 Transformer 在生成过程中不同点处寻找信息的位置。

        生成文本

        我们可以通过以下过程生成新文本:

        1. 将现有单词序列馈送到网络中,并要求它预测接下来的单词。

        2. 将此单词附加到现有序列并重复。

        网络将为每个单词输出一组概率,我们可以从中进行抽样,因此我们可以使文本生成具有随机性,而不是确定性。

        我们将使用在第五章中引入的相同TextGenerator类进行 LSTM 文本生成,包括指定采样过程的确定性程度的temperature参数。让我们看看这在两个不同的温度值(图 9-10)下是如何运作的。

        图 9-10。在temperature = 1.0temperature = 0.5时生成的输出。

        关于这两段文字有几点需要注意。首先,两者在风格上与原始训练集中的葡萄酒评论相似。它们都以葡萄酒的产地和类型开头,而葡萄酒类型在整个段落中保持一致(例如,它不会在中途更换颜色)。正如我们在第五章中看到的,使用温度为 1.0 生成的文本更加冒险,因此比温度为 0.5 的示例不够准确。因此,使用温度为 1.0 生成多个样本将导致更多的变化,因为模型正在从具有更大方差的概率分布中进行抽样。

        查看注意力分数

        我们还可以要求模型告诉我们在决定句子中的下一个单词时,每个单词放置了多少注意力。TransformerBlock输出每个头的注意权重,这是对句子中前面单词的 softmax 分布。

        为了证明这一点,图 9-11 显示了三个不同输入提示的前五个具有最高概率的标记,以及两个注意力头的平均注意力,针对每个前面的单词。根据其注意力分数对前面的单词进行着色,两个注意力头的平均值。深蓝色表示对该单词放置更多的注意力。

        图 9-11。各种序列后单词概率分布

        在第一个示例中,模型密切关注国家(德国),以决定与地区相关的单词。这是有道理的!为了选择一个地区,它需要从与国家相关的单词中获取大量信息,以确保它们匹配。它不需要太关注前两个标记(葡萄酒评论),因为它们不包含有关地区的任何有用信息。

        在第二个例子中,它需要参考葡萄(雷司令),因此它关注第一次提到它的时间。它可以通过直接关注这个词来提取这个信息,无论这个词在句子中有多远(在 80 个单词的上限内)。请注意,这与递归神经网络非常不同,后者依赖于隐藏状态来维护整个序列的所有有趣信息,以便在需要时可以利用——这是一种效率低下得多的方法。

        最终的序列展示了我们的 GPT 模型如何基于信息的组合选择适当的形容词的例子。这里的注意力再次集中在葡萄(雷司令)上,但也集中在它含有残留糖的事实上。由于雷司令通常是一种甜酒,而且已经提到了糖,因此将其描述为略带甜味而不是略带泥土味是有道理的。

        以这种方式询问网络非常有启发性,可以准确了解它从哪里提取信息,以便对每个后续单词做出准确的决策。我强烈建议尝试玩弄输入提示,看看是否可以让模型关注句子中非常遥远的单词,以说服自己关注模型的注意力模型比传统的递归模型更具有力量! # 其他 Transformer

        我们的 GPT 模型是一个解码器 Transformer——它一次生成一个标记的文本字符串,并使用因果屏蔽只关注输入字符串中的先前单词。还有编码器 Transformer,它不使用因果屏蔽——相反,它关注整个输入字符串以提取输入的有意义的上下文表示。对于其他任务,比如语言翻译,还有编码器-解码器 Transformer,可以将一个文本字符串翻译成另一个;这种模型包含编码器 Transformer 块和解码器 Transformer 块。

        表 9-1 总结了三种 Transformer 的类型,以及每种架构的最佳示例和典型用例。

        表 9-1。三种 Transformer 架构

        类型 示例 用例
        编码器 BERT(谷歌) 句子分类、命名实体识别、抽取式问答
        编码器-解码器 T5(谷歌) 摘要、翻译、问答
        解码器 GPT-3(OpenAI) 文本生成

        一个众所周知的编码器 Transformer 的例子是谷歌开发的双向编码器表示来自 Transformer(BERT)模型,它可以根据缺失单词的上下文预测句子中的缺失单词(Devlin 等,2018)。

        编码器 Transformer

        编码器 Transformer 通常用于需要全面理解输入的任务,比如句子分类、命名实体识别和抽取式问答。它们不用于文本生成任务,因此我们不会在本书中详细探讨它们——有关更多信息,请参阅 Lewis Tunstall 等人的使用 Transformer 进行自然语言处理(O'Reilly)。

        在接下来的章节中,我们将探讨编码器-解码器 Transformer 的工作原理,并讨论 OpenAI 发布的原始 GPT 模型架构的扩展,包括专门为对话应用设计的 ChatGPT。

        T5

        一个使用编码器-解码器结构的现代 Transformer 的例子是谷歌的 T5 模型。这个模型将一系列任务重新构建为文本到文本的框架,包括翻译、语言可接受性、句子相似性和文档摘要,如图 9-12 所示。

        图 9-12。T5 如何将一系列任务重新构建为文本到文本框架的示例,包括翻译、语言可接受性、句子相似性和文档摘要(来源:Raffel et al., 2019

        T5 模型架构与原始 Transformer 论文中使用的编码器-解码器架构非常相似,如图 9-13 所示。关键区别在于 T5 是在一个庞大的 750GB 文本语料库(Colossal Clean Crawled Corpus,或 C4)上进行训练的,而原始 Transformer 论文仅关注语言翻译,因此它是在 1.4GB 的英德句对上进行训练的。

        图 9-13。编码器-解码器 Transformer 模型:每个灰色框是一个 Transformer 块(来源:Vaswani et al., 2017

        这个图表中的大部分内容对我们来说已经很熟悉了——我们可以看到 Transformer 块被重复,并且使用位置嵌入来捕捉输入序列的顺序。这个模型与我们在本章前面构建的 GPT 模型之间的两个关键区别如下:

        • 在左侧,一组编码器Transformer 块对待翻译的序列进行编码。请注意,注意力层上没有因果屏蔽。这是因为我们不生成更多文本来扩展要翻译的序列;我们只想学习一个可以提供给解码器的整个序列的良好表示。因此,编码器中的注意力层可以完全不加屏蔽,以捕捉单词之间的所有交叉依赖关系,无论顺序如何。

        • 在右侧,一组解码器Transformer 块生成翻译文本。初始注意力层是自指的(即,键、值和查询来自相同的输入),并且使用因果屏蔽确保来自未来标记的信息不会泄漏到当前要预测的单词。然而,我们可以看到随后的注意力层从编码器中提取键和值,只留下查询从解码器本身传递。这被称为交叉引用注意力,意味着解码器可以关注输入序列的编码器表示。这就是解码器知道翻译需要传达什么含义的方式!

        图 9-14 展示了一个交叉引用注意力的示例。解码器层的两个注意力头能够共同提供单词the的正确德语翻译,当它在the street的上下文中使用时。在德语中,根据名词的性别有三个定冠词(der, die, das),但 Transformer 知道选择die,因为一个注意力头能够关注单词street(德语中的一个女性词),而另一个关注要翻译的单词(the)。

        图 9-14。一个示例,展示一个注意力头关注单词“the”,另一个关注单词“street”,以便正确将单词“the”翻译为德语单词“die”,作为“Straße”的女性定冠词
        提示

        这个例子来自Tensor2Tensor GitHub 存储库,其中包含一个 Colab 笔记本,让您可以玩转一个经过训练的编码器-解码器 Transformer 模型,并查看编码器和解码器的注意力机制如何影响将给定句子翻译成德语。

        GPT-3 和 GPT-4

        自 2018 年 GPT 的原始出版以来,OpenAI 已发布了多个更新版本,改进了原始模型,如表 9-2 所示。

        表 9-2。OpenAI 的 GPT 系列模型的演变

        模型 日期 注意力头 词嵌入大小 上下文窗口 参数数量 训练数据
        GPT 2018 年 6 月 12 12 768 512 120,000,000 BookCorpus:来自未发表书籍的 4.5 GB 文本
        GPT-2 2019 年 2 月 48 48 1,600 1,024 1,500,000,000 WebText:来自 Reddit 外链的 40 GB 文本
        GPT-3 2020 年 5 月 96 96 12,888 2,048 175,000,000,000 CommonCrawl,WebText,英文维基百科,书籍语料库等:570 GB
        GPT-4 2023 年 3 月 - - - - - -

        GPT-3 的模型架构与原始 GPT 模型非常相似,只是规模更大,训练数据更多。在撰写本文时,GPT-4 处于有限的测试阶段——OpenAI 尚未公开发布模型的结构和规模的详细信息,尽管我们知道它能够接受图像作为输入,因此首次跨越成为多模态模型。GPT-3 和 GPT-4 的模型权重不是开源的,尽管这些模型可以通过商业工具和 API获得。

        GPT-3 也可以根据您自己的训练数据进行微调——这使您可以提供多个示例,说明它应该如何对特定风格的提示做出反应,通过物理更新网络的权重。在许多情况下,这可能是不必要的,因为 GPT-3 可以通过在提示本身提供几个示例来告诉它如何对特定风格的提示做出反应(这被称为few-shot learning)。微调的好处在于,您不需要在每个单独的输入提示中提供这些示例,从长远来看可以节省成本。

        给定系统提示句子的 GPT-3 输出示例显示在图 9-15 中。

        图 9-15。GPT-3 如何扩展给定系统提示的示例

        诸如 GPT 之类的语言模型在规模上受益巨大——无论是模型权重的数量还是数据集的大小。大型语言模型能力的上限尚未达到,研究人员继续推动着使用越来越大的模型和数据集所能实现的边界。

        ChatGPT

        在 GPT-4 的测试版发布几个月前,OpenAI 宣布了ChatGPT——这是一个允许用户通过对话界面与其一系列大型语言模型进行交互的工具。2022 年 11 月的原始版本由GPT-3.5提供支持,这个版本比 GPT-3 更强大,经过微调以进行对话回应。

        示例对话显示在图 9-16 中。请注意,代理能够在输入之间保持状态,理解第二个问题中提到的attention指的是 Transformer 上下文中的注意力,而不是一个人的专注能力。

        图 9-16。ChatGPT 回答有关 Transformer 的问题的示例

        在撰写本文时,尚无描述 ChatGPT 工作详细信息的官方论文,但根据官方博客文章,我们知道它使用一种称为reinforcement learning from human feedback(RLHF)的技术来微调 GPT-3.5 模型。这种技术也在 ChatGPT 小组早期的论文⁶中使用,该论文介绍了InstructGPT模型,这是一个经过微调的 GPT-3 模型,专门设计用于更准确地遵循书面说明。

        ChatGPT 的训练过程如下:

        1. 监督微调:收集人类编写的对话输入(提示)和期望输出的演示数据集。这用于使用监督学习微调基础语言模型(GPT-3.5)。

        2. 奖励建模:向人类标记者展示提示的示例和几个抽样的模型输出,并要求他们将输出从最好到最差进行排名。训练一个奖励模型,预测给定对话历史的每个输出的得分。

        3. 强化学习:将对话视为一个强化学习环境,其中策略是基础语言模型,初始化为从步骤 1 中微调的模型。给定当前的状态(对话历史),策略输出一个动作(一系列标记),由在步骤 2 中训练的奖励模型评分。然后可以训练一个强化学习算法——近端策略优化(PPO),通过调整语言模型的权重来最大化奖励。

        强化学习

        有关强化学习的介绍,请参阅第十二章,在那里我们探讨了生成模型如何在强化学习环境中使用。

        RLHF 过程如图 9-17 所示。

        图 9-17。ChatGPT 中使用的强化学习来自人类反馈微调过程的示意图(来源:OpenAI

        虽然 ChatGPT 仍然存在许多限制(例如有时“产生”事实不正确的信息),但它是一个强大的示例,展示了 Transformers 如何用于构建生成模型,可以产生复杂、长期和新颖的输出,往往难以区分是否为人类生成的文本。像 ChatGPT 这样的模型迄今取得的进展证明了人工智能的潜力及其对世界的变革性影响。

        此外,显而易见的是,基于人工智能的沟通和互动将继续在未来快速发展。像Visual ChatGPT⁷这样的项目现在正在将 ChatGPT 的语言能力与 Stable Diffusion 等视觉基础模型相结合,使用户不仅可以通过文本与 ChatGPT 互动,还可以通过图像。在像 Visual ChatGPT 和 GPT-4 这样的项目中融合语言和视觉能力,有望开启人机交互的新时代。

        总结

        在本章中,我们探讨了 Transformer 模型架构,并构建了一个 GPT 的版本——用于最先进文本生成的模型。

        GPT 利用一种称为注意力的机制,消除了循环层(例如 LSTM)的需求。它类似于信息检索系统,利用查询、键和值来决定它想要从每个输入标记中提取多少信息。

        注意力头可以组合在一起形成所谓的多头注意力层。然后将它们包装在一个 Transformer 块中,其中包括围绕注意力层的层归一化和跳过连接。Transformer 块可以堆叠以创建非常深的神经网络。

        因果屏蔽用于确保 GPT 不能从下游标记泄漏信息到当前预测中。此外,还使用一种称为位置编码的技术,以确保输入序列的顺序不会丢失,而是与传统的词嵌入一起嵌入到输入中。

        在分析 GPT 的输出时,我们看到不仅可以生成新的文本段落,还可以审查网络的注意力层,以了解它在句子中查找信息以改善预测的位置。GPT 可以在不丢失信号的情况下访问远处的信息,因为注意力分数是并行计算的,不依赖于通过网络顺序传递的隐藏状态,这与循环神经网络的情况不同。

        我们看到了 Transformer 有三个系列(编码器、解码器和编码器-解码器)以及每个系列可以完成的不同任务。最后,我们探讨了其他大型语言模型的结构和训练过程,如谷歌的 T5 和 OpenAI 的 ChatGPT。

        ¹ Ashish Vaswani 等人,“注意力就是一切”,2017 年 6 月 12 日,https://arxiv.org/abs/1706.03762

        ² Alec Radford 等人,“通过生成式预训练改进语言理解”,2018 年 6 月 11 日,https://openai.com/research/language-unsupervised

        ³ Jacob Devlin 等人,“BERT: 深度双向 Transformer 的语言理解预训练”,2018 年 10 月 11 日,https://arxiv.org/abs/1810.04805

        ⁴ Sheng Shen 等人,“PowerNorm: 重新思考 Transformer 中的批归一化”,2020 年 6 月 28 日,https://arxiv.org/abs/2003.07845

        ⁵ Colin Raffel 等人,“探索统一文本到文本 Transformer 的迁移学习极限”,2019 年 10 月 23 日,https://arxiv.org/abs/1910.10683

        ⁶ Long Ouyang 等人,“使用人类反馈训练语言模型遵循指令”,2022 年 3 月 4 日,https://arxiv.org/abs/2203.02155

        ⁷ Chenfei Wu 等人,“Visual ChatGPT: 使用视觉基础模型进行对话、绘画和编辑”,2023 年 3 月 8 日,https://arxiv.org/abs/2303.04671

        第十章:高级 GANs

        第四章介绍了生成对抗网络(GANs),这是一类生成模型,在各种图像生成任务中取得了最先进的结果。模型架构和训练过程的灵活性导致学术界和深度学习从业者找到了设计和训练 GAN 的新方法,从而产生了许多不同的高级架构,我们将在本章中探讨。

        介绍

        详细解释所有 GAN 发展及其影响可能需要另一本书。GitHub 上的GAN Zoo 代码库包含了 500 多个不同的 GAN 示例,涵盖了从 ABC-GAN 到 ZipNet-GAN 的各种 GAN,并附有相关论文链接!

        在本章中,我们将介绍对该领域产生影响的主要 GANs,包括对每个模型的模型架构和训练过程的详细解释。

        我们将首先探讨 NVIDIA 推动图像生成边界的三个重要模型:ProGAN、StyleGAN 和 StyleGAN2。我们将对每个模型进行足够详细的分析,以理解支撑架构的基本概念,并看看它们如何各自建立在早期论文的想法基础上。

        我们还将探讨另外两种重要的 GAN 架构,包括引入注意力机制的 Self-Attention GAN(SAGAN)和 BigGAN,后者在 SAGAN 论文中的许多想法基础上构建。我们已经在第九章中看到了注意力机制在变换器中的威力。

        最后,我们将介绍 VQ-GAN 和 ViT VQ-GAN,它们融合了变分自动编码器、变换器和 GAN 的思想。VQ-GAN 是谷歌最先进的文本到图像生成模型 Muse 的关键组成部分。我们将在第十三章中更详细地探讨所谓的多模型。

        训练您自己的模型

        为了简洁起见,我选择不在本书的代码库中直接构建这些模型的代码,而是将尽可能指向公开可用的实现,以便您可以根据需要训练自己的版本。

        ProGAN

        ProGAN 是 NVIDIA 实验室在 2017 年开发的一种技术,旨在提高 GAN 训练的速度和稳定性。ProGAN 论文建议,不要立即在全分辨率图像上训练 GAN,而是首先在低分辨率图像(例如 4×4 像素)上训练生成器和鉴别器,然后在训练过程中逐步添加层以增加分辨率。

        让我们更详细地了解渐进式训练的概念。

        训练您自己的 ProGAN

        Bharath K 在Paperspace 博客上提供了一个关于使用 Keras 训练自己的 ProGAN 的优秀教程。请记住,训练 ProGAN 以达到论文中的结果需要大量的计算能力。

        渐进式训练

        与 GANs 一样,我们构建两个独立的网络,生成器和鉴别器,在训练过程中进行统治之争。

        在普通的 GAN 中,生成器总是输出全分辨率图像,即使在训练的早期阶段也是如此。可以合理地认为,这种策略可能不是最佳的——生成器可能在训练的早期阶段学习高级结构较慢,因为它立即在复杂的高分辨率图像上操作。首先训练一个轻量级的 GAN 以输出准确的低分辨率图像,然后逐渐增加分辨率,这样做会更好吗?

        这个简单的想法引导我们进入渐进式训练,这是 ProGAN 论文的一个关键贡献。ProGAN 分阶段训练,从一个已经通过插值压缩到 4×4 像素图像的训练集开始,如图 10-1 所示。

        图 10-1。数据集中的图像可以使用插值压缩到较低分辨率

        然后,我们可以最初训练生成器,将潜在输入噪声向量z(比如长度为 512)转换为形状为 4×4×3 的图像。匹配的鉴别器需要将大小为 4×4×3 的输入图像转换为单个标量预测。这第一步的网络架构如图 10-2 所示。

        生成器中的蓝色框表示将特征图转换为 RGB 图像的卷积层(toRGB),鉴别器中的蓝色框表示将 RGB 图像转换为一组特征图的卷积层(fromRGB)。

        图 10-2。ProGAN 训练过程的第一阶段的生成器和鉴别器架构

        在论文中,作者训练这对网络,直到鉴别器看到了 800,000 张真实图像。现在我们需要了解如何扩展生成器和鉴别器以处理 8×8 像素图像。

        为了扩展生成器和鉴别器,我们需要融入额外的层。这在两个阶段中进行,过渡和稳定,如图 10-3 所示。

        图 10-3。ProGAN 生成器训练过程,将网络从 4×4 图像扩展到 8×8(虚线代表网络的其余部分,未显示)

        让我们首先看一下生成器。在过渡阶段中,新的上采样和卷积层被附加到现有网络中,建立了一个残差连接以保持现有训练过的toRGB层的输出。关键的是,新层最初使用一个参数α进行掩蔽,该参数在整个过渡阶段逐渐从 0 增加到 1,以允许更多新的toRGB输出通过,减少现有的toRGB层。这是为了避免网络在新层接管时出现冲击

        最终,旧的toRGB层不再有输出流,网络进入稳定阶段——进一步的训练期间,网络可以微调输出,而不经过旧的toRGB层。

        鉴别器使用类似的过程,如图 10-4 所示。

        图 10-4。ProGAN 鉴别器训练过程,将网络从 4×4 图像扩展到 8×8(虚线代表网络的其余部分,未显示)

        在这里,我们需要融入额外的降采样和卷积层。同样,这些层被注入到网络中——这次是在网络的开始部分,就在输入图像之后。现有的fromRGB层通过残差连接连接,并在过渡阶段逐渐淡出,随着新层在过渡阶段接管时逐渐淡出。稳定阶段允许鉴别器使用新层进行微调。

        所有过渡和稳定阶段持续到鉴别器已经看到了 800,000 张真实图像。请注意,即使网络是渐进训练的,也没有层被冻结。在整个训练过程中,所有层都保持完全可训练。

        这个过程继续进行,将 GAN 从 4×4 图像扩展到 8×8,然后 16×16,32×32,依此类推,直到达到完整分辨率(1,024×1,024),如图 10-5 所示。

        图 10-5。ProGAN 训练机制,以及一些示例生成的人脸(来源:Karras 等人,2017

        完整渐进训练过程完成后,生成器和鉴别器的整体结构如图 10-6 所示。

        图 10-6. 用于生成 1,024×1,024 像素 CelebA 面孔的 ProGAN 生成器和鉴别器的结构(来源:Karras 等人,2018)

        该论文还提出了其他几个重要贡献,即小批量标准差、均衡学习率和像素级归一化,以下部分将简要描述。

        小批量标准差

        小批量标准差层是鉴别器中的额外层,附加了特征值的标准差,跨所有像素和整个小批量平均作为额外(常数)特征。这有助于确保生成器在输出中创建更多的变化——如果整个小批量中的变化较小,则标准差将很小,鉴别器可以使用此特征来区分假批次和真实批次!因此,生成器被激励确保它生成与真实训练数据中存在的变化量相似的数量。

        均衡学习率

        ProGAN 中的所有全连接和卷积层都使用均衡学习率。通常,神经网络中的权重是使用诸如He 初始化之类的方法进行初始化的——这是一个高斯分布,其标准差被缩放为与层的输入数量的平方根成反比。这样,具有更多输入的层将使用与零的偏差较小的权重进行初始化,通常会提高训练过程的稳定性。

        ProGAN 论文的作者发现,当与 Adam 或 RMSProp 等现代优化器结合使用时,这会导致问题。这些方法会对每个权重的梯度更新进行归一化,使得更新的大小与权重的规模(幅度)无关。然而,这意味着具有较大动态范围(即具有较少输入的层)的权重将比具有较小动态范围(即具有更多输入的层)的权重需要更长时间来调整。发现这导致了 ProGAN 中生成器和鉴别器不同层的训练速度之间的不平衡,因此他们使用均衡学习率来解决这个问题。

        在 ProGAN 中,权重使用简单的标准高斯进行初始化,而不管层的输入数量如何。归一化是动态应用的,作为对层的调用的一部分,而不仅仅是在初始化时。这样,优化器会将每个权重视为具有大致相同的动态范围,因此会应用相同的学习率。只有在调用层时,权重才会按照 He 初始化器的因子进行缩放。

        像素级归一化

        最后,在 ProGAN 中,生成器中使用像素级归一化,而不是批归一化。这将每个像素中的特征向量归一化为单位长度,并有助于防止信号在网络中传播时失控。像素级归一化层没有可训练的权重。

        输出

        除 CelebA 数据集外,ProGAN 还应用于大规模场景理解(LSUN)数据集的图像,并取得了出色的结果,如图 10-7 所示。这展示了 ProGAN 相对于早期 GAN 架构的强大之处,并为未来的迭代(如 StyleGAN 和 StyleGAN2)铺平了道路,我们将在接下来的部分中探讨。

        图 10-7. 在 LSUN 数据集上渐进训练的 ProGAN 生成的示例,分辨率为 256×256(来源:Karras 等人,2017

        StyleGAN

        StyleGAN³是 2018 年的一个 GAN 架构,建立在 ProGAN 论文中的早期思想基础上。实际上,鉴别器是相同的;只有生成器被改变。

        通常在训练 GAN 时,很难将潜在空间中对应于高级属性的向量分离出来——它们经常是纠缠在一起,这意味着调整潜在空间中的图像以使脸部更多雀斑,例如,可能也会无意中改变背景颜色。虽然 ProGAN 生成了极其逼真的图像,但它也不例外。我们理想情况下希望完全控制图像的风格,这需要在潜在空间中对特征进行分离。

        StyleGAN 通过在网络的不同点显式注入风格向量来实现这一点:一些控制高级特征(例如,面部方向)的向量,一些控制低级细节(例如,头发如何落在额头上)的向量。

        StyleGAN 生成器的整体架构如图 10-8 所示。让我们逐步走过这个架构,从映射网络开始。

        图 10-8。StyleGAN 生成器架构(来源:Karras et al., 2018

        训练您自己的 StyleGAN

        Soon-Yau Cheong 在Keras 网站上提供了一个关于使用 Keras 训练自己的 StyleGAN 的优秀教程。请记住,要实现论文中的结果,训练 StyleGAN 需要大量的计算资源。

        映射网络

        映射网络 f 是一个简单的前馈网络,将输入噪声 𝐳 𝒵 转换为不同的潜在空间 𝐰 𝒲。这使得生成器有机会将嘈杂的输入向量分解为不同的变化因素,这些因素可以被下游的风格生成层轻松捕捉到。

        这样做的目的是将图像的风格选择过程(映射网络)与生成具有给定风格的图像的过程(合成网络)分开。

        合成网络

        合成网络是生成具有给定风格的实际图像的生成器,由映射网络提供。如图 10-8 所示,风格向量 𝐰 被注入到合成网络的不同点,每次通过不同的密集连接层 A i,生成两个向量:一个偏置向量 𝐲 b,i 和一个缩放向量 𝐲 s,i。这些向量定义了应该在网络中的这一点注入的特定风格,也就是告诉合成网络如何调整特征图以使生成的图像朝着指定的风格方向移动。

        通过自适应实例归一化(AdaIN)层实现这种调整。

        自适应实例归一化

        AdaIN 层是一种神经网络层,通过参考风格偏差𝐲 b,i和比例𝐲 s,i调整每个特征图𝐱 i的均值和方差。这两个向量的长度等于合成网络中前一卷积层输出的通道数。自适应实例归一化的方程如下:

        AdaIN ( 𝐱 i , 𝐲 ) = 𝐲 s,i 𝐱 i -μ(𝐱 i ) σ(𝐱 i ) + 𝐲 b,i

        自适应实例归一化层确保注入到每一层的风格向量只影响该层的特征,防止任何风格信息在层之间泄漏。作者表明,这导致潜在向量𝐰比原始𝐳向量更具解耦性。

        由于合成网络基于 ProGAN 架构,因此是逐步训练的。在合成网络中较早层的风格向量(当图像分辨率最低时为 4×4、8×8)将影响比网络后期(64×64 到 1,024×1,024 像素分辨率)更粗糙的特征。这意味着我们不仅可以通过潜在向量𝐰完全控制生成的图像,还可以在合成网络的不同点切换𝐰向量以改变各种细节级别的风格。

        风格混合

        作者使用一种称为风格混合的技巧,确保生成器在训练过程中不能利用相邻风格之间的相关性(即,每层注入的风格尽可能解耦)。不仅仅是采样单个潜在向量𝐳,而是采样两个( 𝐳 1 , 𝐳 2 ),对应两个风格向量( 𝐰 1 , 𝐰 2 )。然后,在每一层,随机选择( 𝐰 1𝐰 2 ),以打破可能存在的向量之间的任何相关性。

        随机变化

        合成器网络在每个卷积后添加噪音(通过一个学习的广播层B传递),以考虑诸如单个头发的放置或面部背后的背景等随机细节。再次强调,噪音注入的深度会影响对图像的影响粗糙程度。

        这也意味着合成网络的初始输入可以简单地是一个学习到的常量,而不是额外的噪音。在风格输入和噪音输入中已经存在足够的随机性,以生成图像的足够变化。

        StyleGAN 的输出

        图 10-9 展示了 StyleGAN 的工作原理。

        图 10-9. 在不同细节级别上合并两个生成图像的风格(来源:Karras 等人,2018

        这里,两个图像,源 A 和源 B,是从两个不同的 𝐰 向量生成的。为了生成一个合并的图像,源 A 的 𝐰 向量通过合成网络,但在某个时刻,被切换为源 B 的 𝐰 向量。如果这个切换发生得很早(4 × 4 或 8 × 8 分辨率),则从源 B 传递到源 A 的是粗略的风格,如姿势、脸型和眼镜。然而,如果切换发生得更晚,只有来自源 B 的细粒度细节被传递,比如脸部的颜色和微结构,而来自源 A 的粗略特征被保留。

        StyleGAN2

        在这一系列重要的 GAN 论文中的最终贡献是 StyleGAN2。这进一步构建在 StyleGAN 架构之上,通过一些关键改变提高了生成输出的质量。特别是,StyleGAN2 生成不会像 伪影 那样受到严重影响——在 StyleGAN 中发现的图像中的水滴状区域,这些伪影是由于 StyleGAN 中的自适应实例归一化层引起的,如 图 10-10 所示。

        图 10-10. 一个 StyleGAN 生成的人脸图像中的伪影(来源:Karras et al., 2019

        StyleGAN2 中的生成器和鉴别器与 StyleGAN 不同。在接下来的章节中,我们将探讨这两种架构之间的关键区别。

        训练您自己的 StyleGAN2

        使用 TensorFlow 训练您自己的 StyleGAN 的官方代码可在 GitHub 上找到。请注意,为了实现论文中的结果,训练一个 StyleGAN2 需要大量的计算资源。

        权重调制和去调制

        通过删除生成器中的 AdaIN 层并将其替换为权重调制和去调制步骤,解决了伪影问题,如 图 10-11 所示。 𝐰 代表卷积层的权重,在 StyleGAN2 中通过调制和去调制步骤直接在运行时更新。相比之下,StyleGAN 的 AdaIN 层在图像张量通过网络时操作。

        StyleGAN 中的 AdaIN 层只是一个实例归一化,后面跟着样式调制(缩放和偏置)。StyleGAN2 中的想法是在运行时直接将样式调制和归一化(去调制)应用于卷积层的权重,而不是卷积层的输出,如 图 10-11 所示。作者展示了这如何消除了伪影问题,同时保持对图像样式的控制。

        图 10-11. StyleGAN 和 StyleGAN2 样式块之间的比较

        在 StyleGAN2 中,每个密集层 A 输出一个单一的样式向量 s i,其中 i 索引了相应卷积层中的输入通道数。然后将这个样式向量应用于卷积层的权重,如下所示:

        w i,j,k ' = s i · w i,j,k

        这里,j 索引了层的输出通道,k 索引了空间维度。这是过程的 调制 步骤。

        然后,我们需要归一化权重,使它们再次具有单位标准差,以确保训练过程的稳定性。这是 去调制 步骤:

        w i,j,k '' = w i,j,k ' i,k w i,j,k ' 2 +ε

        其中 ϵ 是一个小的常数值,用于防止除以零。

        在论文中,作者展示了这个简单的改变足以防止水滴状伪影,同时通过样式向量保持对生成图像的控制,并确保输出的质量保持高水平。

        路径长度正则化

        StyleGAN 架构的另一个变化是在损失函数中包含了额外的惩罚项——这被称为路径长度正则化

        我们希望潜在空间尽可能平滑和均匀,这样在任何方向上潜在空间中的固定大小步长会导致图像的固定幅度变化。

        为了鼓励这一属性,StyleGAN2 旨在最小化以下术语,以及通常的 Wasserstein 损失和梯度惩罚:

        𝔼 𝑤,𝑦 𝐉 𝑤 𝑦 2 -a 2

        在这里,𝑤是由映射网络创建的一组样式向量,𝑦是从𝒩 ( 0 , 𝐈 )中绘制的一组嘈杂图像,𝐉 𝑤 = g 𝑤是生成器网络相对于样式向量的雅可比矩阵。

        术语𝐉 𝑤 𝑦 2测量了经雅可比矩阵给出的梯度变换后图像𝑦的幅度。我们希望这个值接近一个常数a,这个常数是动态计算的,作为训练进行时𝐉 𝑤 𝑦 2的指数移动平均值。

        作者发现,这个额外的术语使探索潜在空间更可靠和一致。此外,损失函数中的正则化项仅在每 16 个小批次中应用一次,以提高效率。这种技术称为懒惰正则化,不会导致性能的明显下降。

        没有渐进增长

        StyleGAN2 训练的另一个重大更新是在训练方式上。StyleGAN2 不再采用通常的渐进式训练机制,而是利用生成器中的跳过连接和鉴别器中的残差连接来将整个网络作为一个整体进行训练。它不再需要独立训练不同分辨率,并将其作为训练过程的一部分混合。

        图 10-12 展示了 StyleGAN2 中的生成器和鉴别器块。

        图 10-12。StyleGAN2 中的生成器和鉴别器块

        我们希望能够保留的关键属性是,StyleGAN2 从学习低分辨率特征开始,并随着训练的进行逐渐完善输出。作者表明,使用这种架构确实保留了这一属性。在训练的早期阶段,每个网络都受益于在较低分辨率层中细化卷积权重,而通过跳过和残差连接将输出传递到较高分辨率层的方式基本上不受影响。随着训练的进行,较高分辨率层开始占主导地位,因为生成器发现了更复杂的方法来改善图像的逼真度,以欺骗鉴别器。这个过程在图 10-13 中展示。

        图 10-13。每个分辨率层对生成器输出的贡献,按训练时间(改编自Karras 等人,2019

        StyleGAN2 的输出

        一些 StyleGAN2 输出的示例显示在图 10-14 中。迄今为止,StyleGAN2 架构(以及诸如 StyleGAN-XL 这样的扩展变体)仍然是 Flickr-Faces-HQ(FFHQ)和 CIFAR-10 等数据集上图像生成的最先进技术,根据基准网站Papers with Code

        图 10-14。FFHQ 人脸数据集和 LSUN 汽车数据集的未筛选 StyleGAN2 输出(来源:Karras 等人,2019)

        其他重要的 GAN

        在这一部分中,我们将探讨另外两种架构,它们也对 GAN 的发展做出了重大贡献——SAGAN 和 BigGAN。

        自注意力生成对抗网络(SAGAN)

        自注意力生成对抗网络(SAGAN)是 GAN 的一个重要发展,因为它展示了如何将驱动序列模型(如 Transformer)的注意机制也纳入到基于 GAN 的图像生成模型中。图 10-15 展示了介绍这种架构的论文中的自注意力机制。

        图 10-15。SAGAN 模型中的自注意机制(来源:Zhang 等人,2018

        不包含注意力的基于 GAN 的模型的问题在于,卷积特征图只能在局部处理信息。连接图像一侧的像素信息到另一侧需要多个卷积层,这会减小图像的尺寸,同时增加通道数。在这个过程中,精确的位置信息会被减少,以捕捉更高级的特征,这使得模型学习远距离像素之间的长距离依赖性变得计算上低效。SAGAN 通过将我们在本章前面探讨过的注意力机制纳入到 GAN 中来解决这个问题。这种包含的效果在图 10-16 中展示。

        图 10-16。SAGAN 生成的一幅鸟的图像(最左侧单元格)以及由最终基于注意力的生成器层生成的像素的注意力图(右侧单元格)(来源:Zhang 等人,2018)

        红点是鸟身体的一部分,因此注意力自然地集中在周围的身体细胞上。绿点是背景的一部分,这里注意力实际上集中在鸟头的另一侧,即其他背景像素上。蓝点是鸟的长尾的一部分,因此注意力集中在其他尾部像素上,其中一些与蓝点相距较远。对于没有注意力的像素来说,尤其是对于图像中的长、细结构(例如这种情况下的尾巴),要维持这种长距离依赖性将会很困难。

        训练您自己的 SAGAN

        使用 TensorFlow 训练自己的 SAGAN 的官方代码可在GitHub上找到。请注意,要实现论文中的结果,训练 SAGAN 需要大量的计算资源。

        BigGAN

        BigGAN,由 DeepMind 开发,扩展了 SAGAN 论文中的思想。图 10-17 展示了一些由 BigGAN 生成的图像,该模型在 ImageNet 数据集上进行了训练,分辨率为 128×128。

        图 10-17。由 BigGAN 生成的图像示例(来源:Brock 等人,2018)

        除了对基本 SAGAN 模型进行一些增量更改外,论文中还概述了将模型提升到更高层次的几项创新。其中一项创新是所谓的“截断技巧”。这是指用于采样的潜在分布与训练期间使用的 z 𝒩 ( 0 , 𝐈 ) 分布不同。具体来说,采样期间使用的分布是“截断正态分布”(重新采样具有大于一定阈值的 z 值)。截断阈值越小,生成样本的可信度越高,但变异性降低。这个概念在图 10-18 中展示。

        图 10-18. 截断技巧:从左到右,阈值设置为 2、1、0.5 和 0.04(来源:Brock 等人,2018

        正如其名称所示,BigGAN 在某种程度上是对 SAGAN 的改进,仅仅是因为它更“大”。BigGAN 使用的批量大小为 2,048,比 SAGAN 中使用的 256 的批量大小大 8 倍,并且每一层的通道大小增加了 50%。然而,BigGAN 还表明,通过包含共享嵌入、正交正则化以及将潜在向量 z 包含到生成器的每一层中,而不仅仅是初始层,可以在结构上改进 SAGAN。

        要全面了解 BigGAN 引入的创新,我建议阅读原始论文和相关演示材料

        使用 BigGAN

        TensorFlow 网站上提供了一个使用预训练的 BigGAN 生成图像的教程。

        VQ-GAN

        另一种重要的 GAN 类型是 2020 年推出的 Vector Quantized GAN(VQ-GAN)。这种模型架构建立在 2017 年的论文“神经离散表示学习”中提出的一个想法之上,即 VAE 学习到的表示可以是离散的,而不是连续的。这种新型模型,即 Vector Quantized VAE(VQ-VAE),被证明可以生成高质量的图像,同时避免了传统连续潜在空间 VAE 经常出现的一些问题,比如“后验坍缩”(学习到的潜在空间由于过于强大的解码器而变得无信息)。

        提示

        OpenAI 在 2021 年发布的文本到图像模型 DALL.E 的第一个版本(参见第十三章)使用了具有离散潜在空间的 VAE,类似于 VQ-VAE。

        通过“离散潜在空间”,我们指的是一个学习到的向量列表(“码书”),每个向量与相应的索引相关联。VQ-VAE 中编码器的工作是将输入图像折叠到一个较小的向量网格中,然后将其与码书进行比较。然后,将每个网格方格向量(通过欧氏距离)最接近的码书向量传递给解码器进行解码,如图 10-19 所示。码书是一个长度为 d(嵌入大小)的学习向量列表,与编码器输出和解码器输入中的通道数相匹配。例如,e 1 是一个可以解释为“背景”的向量。

        图 10-19. VQ-VAE 的示意图

        代码本可以被看作是一组学习到的离散概念,这些概念由编码器和解码器共享,以描述给定图像的内容。VQ-VAE 必须找到一种方法,使这组离散概念尽可能具有信息量,以便编码器可以准确地用特定的代码向量标记每个网格方块,这对解码器是有意义的。因此,VQ-VAE 的损失函数是重构损失加上两个项(对齐和承诺损失),以确保编码器的输出向量尽可能接近代码本中的向量。这些项取代了典型 VAE 中编码分布和标准高斯先验之间的 KL 散度项。

        然而,这种架构提出了一个问题——我们如何对新颖的代码网格进行采样,以传递给解码器生成新的图像?显然,使用均匀先验(为每个网格方块均等概率选择每个代码)是行不通的。例如,在 MNIST 数据集中,左上角的网格方块很可能被编码为背景,而靠近图像中心的网格方块不太可能被编码为这样。为了解决这个问题,作者使用了另一个模型,一个自回归的 PixelCNN(参见第五章),来预测网格中下一个代码向量,给定先前的代码向量。换句话说,先验是由模型学习的,而不是像普通 VAE 中的标准高斯先验那样静态的。

        训练您自己的 VQ-VAE

        有一篇由 Sayak Paul 撰写的优秀教程,介绍如何使用 Keras 在Keras 网站上训练自己的 VQ-VAE。

        VQ-GAN 论文详细介绍了 VQ-VAE 架构的几个关键变化,如图 10-20 所示。

        图 10-20。VQ-GAN 的图表:GAN 鉴别器通过额外的对抗损失项帮助 VAE 生成更清晰的图像

        首先,正如名称所示,作者包括一个 GAN 鉴别器,试图区分 VAE 解码器的输出和真实图像,损失函数中还有一个对抗项。众所周知,GAN 生成的图像比 VAE 更清晰,因此这个添加改善了整体图像质量。请注意,尽管名称中有 VAE,但 VAE 仍然存在于 VQ-GAN 模型中——GAN 鉴别器是一个额外的组件,而不是 VAE 的替代品。将 VAE 与 GAN 鉴别器(VAE-GAN)结合的想法首次由 Larsen 等人在他们 2015 年的论文中提出。

        其次,GAN 鉴别器预测图像的小块是否真实或伪造,而不是一次性预测整个图像。这个想法(PatchGAN)被应用在 2016 年由 Isola 等人介绍的成功的pix2pix图像到图像模型中,并且也成功地作为CycleGAN的一部分应用,另一个图像到图像的风格转移模型。PatchGAN 鉴别器输出一个预测向量(每个块的预测),而不是整个图像的单个预测。使用 PatchGAN 鉴别器的好处在于,损失函数可以衡量鉴别器在基于风格而不是内容来区分图像方面的表现如何。由于鉴别器预测的每个单独元素基于图像的一个小方块,它必须使用块的风格而不是内容来做出决定。这是有用的,因为我们知道 VAE 生成的图像在风格上比真实图像更模糊,因此 PatchGAN 鉴别器可以鼓励 VAE 解码器生成比其自然产生的更清晰的图像。

        第三,与使用单个 MSE 重建损失不同,该损失将输入图像像素与 VAE 解码器输出像素进行比较,VQ-GAN 使用感知损失项,计算编码器中间层的特征图与解码器相应层之间的差异。这个想法来自于侯等人 2016 年的论文,¹⁴作者在其中展示了这种对损失函数的改变导致更逼真的图像生成。

        最后,模型的自回归部分使用 Transformer 而不是 PixelCNN,训练以生成代码序列。Transformer 在 VQ-GAN 完全训练后的一个单独阶段中进行训练。作者选择仅使用在要预测的令牌周围的滑动窗口内的令牌,而不是完全自回归地使用所有先前的令牌。这确保了模型可以扩展到需要更大潜在网格大小和因此需要 Transformer 生成更多令牌的更大图像。

        ViT VQ-GAN

        Yu 等人在 2021 年的论文“Vector-Quantized Image Modeling with Improved VQGAN”中对 VQ-GAN 进行了最后一个扩展。¹⁵ 在这里,作者展示了如何将 VQ-GAN 的卷积编码器和解码器替换为 Transformer,如图 10-21 所示。

        对于编码器,作者使用Vision Transformer(ViT)。¹⁶ ViT 是一种神经网络架构,将最初设计用于自然语言处理的 Transformer 模型应用于图像数据。ViT 不使用卷积层从图像中提取特征,而是将图像分成一系列补丁,对其进行标记化,然后将其作为输入馈送到编码器 Transformer 中。

        具体来说,在 ViT VQ-GAN 中,非重叠的输入补丁(每个大小为 8×8)首先被展平,然后投影到低维嵌入空间中,位置嵌入被添加。然后,这个序列被馈送到标准编码器 Transformer 中,生成的嵌入根据学习的码书进行量化。这些整数代码然后由解码器 Transformer 模型处理,最终输出是一系列补丁,可以被拼接在一起形成原始图像。整体的编码器-解码器模型被作为自动编码器端到端训练。

        图 10-21。ViT VQ-GAN 的图表:GAN 鉴别器通过额外的对抗损失项帮助 VAE 生成更清晰的图像(来源:Yu and Koh, 2022)¹⁷

        与原始 VQ-GAN 模型一样,训练的第二阶段涉及使用自回归解码器 Transformer 生成代码序列。因此,在 ViT VQ-GAN 中总共有三个 Transformer,另外还有 GAN 鉴别器和学习的码书。论文中 ViT VQ-GAN 生成的图像示例显示在图 10-22 中。

        图 10-22。ViT VQ-GAN 在 ImageNet 上训练生成的示例图像(来源:Yu et al., 2021

        总结

        在本章中,我们回顾了自 2017 年以来一些最重要和有影响力的 GAN 论文。特别是,我们探讨了 ProGAN、StyleGAN、StyleGAN2、SAGAN、BigGAN、VQ-GAN 和 ViT VQ-GAN。

        我们从 2017 年 ProGAN 论文中首创的渐进训练概念开始探索。2018 年 StyleGAN 论文引入了几个关键改变,使对图像输出有更大的控制,例如用于创建特定样式向量的映射网络和允许在不同分辨率注入样式的合成网络。最后,StyleGAN2 用权重调制和解调制步骤替换了 StyleGAN 的自适应实例归一化,同时还进行了额外的增强,如路径正则化。该论文还展示了如何保留渐进分辨率细化的可取属性,而无需逐步训练网络。

        我们还看到了如何将注意力的概念构建到 GAN 中,2018 年引入了 SAGAN。这使网络能够捕捉长距离依赖关系,例如图像相对两侧的相似背景颜色,而无需依赖深度卷积映射将信息传播到图像的空间维度。BigGAN 是这个想法的延伸,进行了几个关键改变,并训练了一个更大的网络以进一步提高图像质量。

        在 VQ-GAN 论文中,作者展示了如何将几种不同类型的生成模型结合起来产生很好的效果。在最初引入具有离散潜在空间的 VAE 概念的 VQ-VAE 论文的基础上,VQ-GAN 还包括一个鼓励 VAE 通过额外的对抗损失项生成更清晰图像的鉴别器。自回归 Transformer 用于构建一个新颖的代码令牌序列,可以由 VAE 解码器解码以生成新颖图像。ViT VQ-GAN 论文进一步扩展了这个想法,通过用 Transformer 替换 VQ-GAN 的卷积编码器和解码器。

        ¹ Huiwen Chang 等人,“Muse: 通过遮罩生成 Transformer 进行文本到图像生成”,2023 年 1 月 2 日,https://arxiv.org/abs/2301.00704

        ² Tero Karras 等人,“用于改善质量、稳定性和变化的 GAN 的渐进增长”,2017 年 10 月 27 日,https://arxiv.org/abs/1710.10196

        ³ Tero Karras 等人,“用于生成对抗网络的基于样式的生成器架构”,2018 年 12 月 12 日,https://arxiv.org/abs/1812.04948

        ⁴ Xun Huang 和 Serge Belongie,“使用自适应实例归一化实时进行任意风格转移”,2017 年 3 月 20 日,https://arxiv.org/abs/1703.06868

        ⁵ Tero Karras 等人,“分析和改进 StyleGAN 的图像质量”,2019 年 12 月 3 日,https://arxiv.org/abs/1912.04958

        ⁶ Axel Sauer 等人,“StyleGAN-XL: 将 StyleGAN 扩展到大型多样数据集”,2022 年 2 月 1 日,https://arxiv.org/abs/2202.00273v2

        ⁷ Han Zhang 等人,“自注意力生成对抗网络”,2018 年 5 月 21 日,https://arxiv.org/abs/1805.08318

        ⁸ Andrew Brock 等人,“用于高保真自然图像合成的大规模 GAN 训练”,2018 年 9 月 28 日,https://arxiv.org/abs/1809.11096

        ⁹ Patrick Esser 等人,“驯服 Transformer 以进行高分辨率图像合成”,2020 年 12 月 17 日,https://arxiv.org/abs/2012.09841

        ¹⁰ Aaron van den Oord 等人,“神经离散表示学习”,2017 年 11 月 2 日,https://arxiv.org/abs/1711.00937v2

        ¹¹ Anders Boesen Lindbo Larsen 等人,“超越像素的自动编码:使用学习的相似度度量”,2015 年 12 月 31 日,https://arxiv.org/abs/1512.09300

        ¹² Phillip Isola 等人,“带条件对抗网络的图像到图像翻译”,2016 年 11 月 21 日,https://arxiv.org/abs/1611.07004v3

        ¹³ Jun-Yan Zhu 等人,“使用循环一致性对抗网络进行无配对图像到图像翻译”,2017 年 3 月 30 日,https://arxiv.org/abs/1703.10593

        ¹⁴ Xianxu Hou 等人,“深度特征一致变分自动编码器”,2016 年 10 月 2 日,https://arxiv.org/abs/1610.00291

        ¹⁵ Jiahui Yu 等人,“改进的 VQGAN 进行矢量量化图像建模”,2021 年 10 月 9 日,https://arxiv.org/abs/2110.04627

        ¹⁶ Alexey Dosovitskiy 等人,“一幅图像价值 16x16 个词:规模化图像识别的 Transformer”,2020 年 10 月 22 日,https://arxiv.org/abs/2010.11929v2

        ¹⁷ Jiahui Yu 和 Jing Yu Koh,“改进的 VQGAN 进行矢量量化图像建模”,2022 年 5 月 18 日,https://ai.googleblog.com/2022/05/vector-quantized-image-modeling-with.html

        第十一章:音乐生成

        音乐作曲是一个复杂而创造性的过程,涉及将不同的音乐元素(如旋律、和声、节奏和音色)结合在一起。虽然传统上认为这是一种独特的人类活动,但最近的进展使得生成既能让耳朵愉悦又具有长期结构的音乐成为可能。

        音乐生成最流行的技术之一是 Transformer,因为音乐可以被视为一个序列预测问题。这些模型已经被调整为通过将音符视为一系列标记(类似于句子中的单词)来生成音乐。Transformer 模型学会根据先前的音符预测序列中的下一个音符,从而生成一段音乐。

        MuseGAN 采用了一种完全不同的方法来生成音乐。与 Transformer 逐音符生成音乐不同,MuseGAN 通过将音乐视为一个图像,由音高轴和时间轴组成,一次生成整个音乐曲目。此外,MuseGAN 将不同的音乐组成部分(如和弦、风格、旋律和节奏)分开,以便可以独立控制它们。

        在本章中,我们将学习如何处理音乐数据,并应用 Transformer 和 MuseGAN 来生成与给定训练集风格相似的音乐。

        介绍

        为了让机器创作出我们耳朵愉悦的音乐,它必须掌握我们在第九章中看到的与文本相关的许多技术挑战。特别是,我们的模型必须能够学习并重新创建音乐的顺序结构,并能够从一系列可能性中选择后续音符。

        然而,音乐生成面临着文本生成所没有的额外挑战,即音高和节奏。音乐通常是复调的,即有几条音符流同时在不同乐器上演奏,这些音符组合在一起形成既不和谐(冲突)又和谐(和谐)的和声。文本生成只需要我们处理一条文本流,与音乐中存在的并行和弦流相比。

        此外,文本生成可以逐字处理。与文本数据不同,音乐是一种多部分、交织在一起的声音织锦,不一定同时传递——听音乐的乐趣很大程度上来自于整个合奏中不同节奏之间的相互作用。例如,吉他手可能弹奏一连串更快的音符,而钢琴家则弹奏一个较长的持续和弦。因此,逐音符生成音乐是复杂的,因为我们通常不希望所有乐器同时改变音符。

        我们将从简化问题开始,专注于为单一(单声部)音乐线生成音乐。许多来自第九章关于文本生成的技术也可以用于音乐生成,因为这两个任务有许多共同的主题。我们将首先训练一个 Transformer 来生成类似于 J.S.巴赫大提琴组曲风格的音乐,并看看注意机制如何使模型能够专注于先前的音符,以确定最自然的后续音符。然后,我们将处理复调音乐生成的任务,并探讨如何使用基于 GAN 的架构来为多声部创作音乐。

        用于音乐生成的 Transformer

        我们将在这里构建的模型是一个解码器 Transformer,灵感来自于 OpenAI 的MuseNet,它也利用了一个解码器 Transformer(类似于 GPT-3),训练以预测给定一系列先前音符的下一个音符。

        在音乐生成任务中,随着音乐的进行,序列的长度N变得很大,这意味着每个头部的N × N注意力矩阵变得昂贵且难以存储和计算。我们理想情况下不希望将输入序列剪切为少量标记,因为我们希望模型围绕长期结构构建乐曲,并重复几分钟前的主题和乐句,就像人类作曲家一样。

        为了解决这个问题,MuseNet 利用了一种称为Sparse Transformer的 Transformer 形式。注意矩阵中的每个输出位置仅计算一部分输入位置的权重,从而减少了训练模型所需的计算复杂性和内存。MuseNet 因此可以在 4,096 个标记上进行全注意力操作,并可以学习跨多种风格的长期结构和旋律结构。 (例如,查看 OpenAI 在 SoundCloud 上的肖邦莫扎特的录音。)

        要看到音乐短语的延续通常受几个小节前的音符影响,看看巴赫大提琴组曲第 1 号前奏的开头小节吧(图 11-1)。

        图 11-1。巴赫的大提琴组曲第 1 号(前奏)

        小节

        小节(或节拍)是包含固定数量的拍子的音乐小单位,并由穿过五线谱的垂直线标记出来。如果你能够数 1、2、1、2,那么每个小节有两拍,你可能在听进行曲。如果你能够数 1、2、3、1、2、3,那么每个小节有三拍,你可能在听华尔兹。

        你认为接下来会是什么音符?即使你没有音乐训练,你可能仍然能猜到。如果你说是 G(与乐曲的第一个音符相同),那么你是正确的。你是怎么知道的?你可能能够看到每个小节和半小节都以相同的音符开头,并利用这些信息来做出决定。我们希望我们的模型能够执行相同的技巧——特别是,我们希望它能够关注前半小节中的特定音符,当前一个低 G 被记录时。基于注意力的模型,如 Transformer,将能够在不必在许多小节之间保持隐藏状态的情况下,合并这种长期回顾。

        任何尝试音乐生成任务的人首先必须对音乐理论有基本的了解。在下一节中,我们将介绍阅读音乐所需的基本知识以及如何将其数值化,以便将音乐转换为训练 Transformer 所需的输入数据。

        运行此示例的代码

        此示例的代码可以在位于书存储库中的 Jupyter 笔记本notebooks/11_music/01_transformer/transformer.ipynb中找到。

        巴赫大提琴组曲数据集

        我们将使用的原始数据集是 J.S.巴赫的大提琴组曲的一组 MIDI 文件。您可以通过在书的存储库中运行数据集下载脚本来下载数据集,如示例 11-1 所示。这将把 MIDI 文件保存到本地的/data文件夹中。

        示例 11-1。下载 J.S.巴赫大提琴组曲数据集
        bash scripts/download_music_data.sh
        

        要查看并听取模型生成的音乐,您需要一些能够生成乐谱的软件。[MuseScore](https://musescore.org)是一个很好的工具,可以免费下载。 `## 解析 MIDI 文件

        我们将使用 Python 库music21将 MIDI 文件加载到 Python 中进行处理。示例 11-2 展示了如何加载一个 MIDI 文件并可视化它(图 11-2),既作为乐谱又作为结构化数据。

        图 11-2。音乐符号
        示例 11-2。导入 MIDI 文件
        import music21
        
        file = "/app/data/bach-cello/cs1-2all.mid"
        example_score = music21.converter.parse(file).chordify()
        

        八度

        每个音符名称后面的数字表示音符所在的八度——因为音符名称(A 到 G)重复,这是为了唯一标识音符的音高。例如,G2是低于G3的一个八度。

        现在是时候将乐谱转换成更像文本的东西了!我们首先循环遍历每个乐谱,并将乐曲中每个元素的音符和持续时间提取到两个单独的文本字符串中,元素之间用空格分隔。我们将乐曲的调号和拍号编码为特殊符号,持续时间为零。

        单声部与复调音乐

        在这个第一个例子中,我们将把音乐视为单声部(一条单独的线),只取任何和弦的最高音。有时我们可能希望保持各声部分开,以生成复调性质的音乐。这带来了我们将在本章后面解决的额外挑战。

        这个过程的输出显示在图 11-3 中——将其与图 11-2 进行比较,以便看到原始音乐数据如何转换为这两个字符串。

        图 11-3。音符文本字符串和持续时间文本字符串的示例,对应于图 11-2

        这看起来更像我们之前处理过的文本数据。单词是音符-持续时间组合,我们应该尝试构建一个模型,根据先前音符和持续时间的序列来预测下一个音符和持续时间。音乐和文本生成之间的一个关键区别是,我们需要构建一个可以同时处理音符和持续时间预测的模型——即,我们需要处理两个信息流,而不是我们在第九章中看到的单一文本流。

        标记化

        为了创建将训练模型的数据集,我们首先需要像之前为文本语料库中的每个单词所做的那样,对每个音符和持续时间进行标记化。我们可以通过使用TextVectorization层,分别应用于音符和持续时间,来实现这一点,如示例 11-3 所示。

        示例 11-3。标记化音符和持续时间
        def create_dataset(elements):
            ds = (
                tf.data.Dataset.from_tensor_slices(elements)
                .batch(BATCH_SIZE, drop_remainder = True)
                .shuffle(1000)
            )
            vectorize_layer = layers.TextVectorization(
                standardize = None, output_mode="int"
            )
            vectorize_layer.adapt(ds)
            vocab = vectorize_layer.get_vocabulary()
            return ds, vectorize_layer, vocab
        
        notes_seq_ds, notes_vectorize_layer, notes_vocab = create_dataset(notes)
        durations_seq_ds, durations_vectorize_layer, durations_vocab = create_dataset(
            durations
        )
        seq_ds = tf.data.Dataset.zip((notes_seq_ds, durations_seq_ds))
        

        完整的解析和标记化过程显示在图 11-4 中。

        图 11-4。解析 MIDI 文件并对音符和持续时间进行标记化

        创建训练集

        预处理的最后一步是创建我们将馈送给 Transformer 的训练集。

        我们通过将音符和持续时间字符串分成 50 个元素的块来实现这一点,使用滑动窗口技术。输出只是输入窗口向后移动一个音符,这样 Transformer 就被训练来预测未来一个时间步的元素的音符和持续时间,给定窗口中的先前元素。这个示例(仅用四个元素的滑动窗口进行演示)显示在图 11-5 中。

        图 11-5。音乐 Transformer 模型的输入和输出——在这个例子中,使用宽度为 4 的滑动窗口创建输入块,然后将其移动一个元素以创建目标输出

        我们将在 Transformer 中使用的架构与我们在第九章中用于文本生成的架构相同,但有一些关键的区别。

        正弦位置编码

        首先,我们将介绍一种不同类型的令牌位置编码。在第九章中,我们使用了一个简单的Embedding层来编码每个令牌的位置,有效地将每个整数位置映射到模型学习的不同向量。因此,我们需要定义一个最大长度(N),该序列可以是,并在这个序列长度上进行训练。这种方法的缺点是无法推断出比这个最大长度更长的序列。您将不得不将输入剪切到最后的N个令牌,如果您试图生成长篇内容,则这并不理想。

        为了避免这个问题,我们可以转而使用一种称为sine position embedding的不同类型的嵌入。这类似于我们在第八章中用来编码扩散模型噪声方差的嵌入。具体来说,以下函数用于将输入序列中单词的位置(p o s)转换为长度为d的唯一向量:

        P E pos,2i = sin ( pos 10,000 2i/d ) P E pos,2i+1 = cos ( pos 10,000 (2i+1)/d )

        对于较小的i,这个函数的波长很短,因此函数值沿着位置轴快速变化。较大的i值会产生更长的波长。因此,每个位置都有自己独特的编码,这是不同波长的特定组合。

        提示

        请注意,此嵌入是为所有可能的位置值定义的。它是一个确定性函数(即,模型不会学习它),它使用三角函数来为每个可能的位置定义一个唯一的编码。

        Keras NLP模块具有一个内置层,为我们实现了这种嵌入 - 因此,我们可以定义我们的TokenAndPositionEmbedding层,如示例 11-4 所示。

        示例 11-4。对音符和持续时间进行标记化
        class TokenAndPositionEmbedding(layers.Layer):
            def __init__(self, vocab_size, embed_dim):
                super(TokenAndPositionEmbedding, self).__init__()
                self.vocab_size = vocab_size
                self.embed_dim = embed_dim
                self.token_emb = layers.Embedding(input_dim=vocab_size, output_dim=embed_dim)
                self.pos_emb = keras_nlp.layers.SinePositionEncoding()
        
            def call(self, x):
                embedding = self.token_emb(x)
                positions = self.pos_emb(embedding)
                return embedding + positions
        

        图 11-6 显示了如何将这两种嵌入(令牌和位置)相加以产生序列的整体嵌入。

        图 11-6。TokenAndPositionEmbedding层将令牌嵌入添加到正弦位置嵌入中,以产生序列的整体嵌入

        多个输入和输出

        现在我们有两个输入流(音符和持续时间)和两个输出流(预测音符和持续时间)。因此,我们需要调整我们的 Transformer 架构以适应这一点。

        处理双输入流的方法有很多种。我们可以创建代表每个音符-持续时间对的令牌,然后将序列视为单个令牌流。然而,这样做的缺点是无法表示在训练集中未见过的音符-持续时间对(例如,我们可能独立地看到了G#2音符和1/3持续时间,但从未一起出现过,因此没有G#2:1/3的令牌)。

        相反,我们选择分别嵌入音符和持续时间令牌,然后使用连接层创建输入的单一表示,该表示可以被下游 Transformer 块使用。类似地,Transformer 块的输出传递给两个独立的密集层,代表了预测的音符和持续时间概率。整体架构如图 11-7 所示。层输出形状显示了批量大小b和序列长度l

        图 11-7。音乐生成 Transformer 的架构

        另一种方法是将音符和持续时间标记交错到一个单一的输入流中,并让模型学习输出应该是一个音符和持续时间标记交替的单一流。这增加了确保当模型尚未学会如何正确交错标记时,输出仍然可以解析的复杂性。

        提示

        设计您的模型没有的方式——其中一部分乐趣就是尝试不同的设置,看看哪种对您最有效!

        音乐生成 Transformer 的分析

        我们将从头开始生成一些音乐,通过向网络提供一个START音符标记和0.0持续时间标记(即,我们告诉模型假设它是从乐曲的开头开始)。然后我们可以使用与我们在第九章中用于生成文本序列的相同迭代技术来生成一个音乐段落,如下所示:

        1. 给定当前序列(音符和持续时间),模型预测两个分布,一个是下一个音符的分布,另一个是下一个持续时间的分布。

        2. 我们从这两个分布中进行采样,使用一个temperature参数来控制在采样过程中我们希望有多少变化。

        3. 选择的音符和持续时间被附加到相应的输入序列中。

        4. 这个过程会重复进行,对于我们希望生成的元素数量,会有新的输入序列。

        图 11-8 展示了在训练过程的各个时期由模型从头开始生成的音乐示例。我们对音符和持续时间使用了 0.5 的温度。

        图 11-8。当仅使用一个START音符标记和0.0持续时间标记作为种子时,模型生成的乐段示例

        在本节中,我们大部分的分析将集中在音符预测上,而不是持续时间,因为对于巴赫的大提琴组曲来说,和声的复杂性更难捕捉,因此更值得研究。然而,您也可以将相同的分析应用于模型的节奏预测,这对于您可能用来训练该模型的其他音乐风格可能特别相关(比如鼓声)。

        关于在图 11-8 中生成的乐段有几点需要注意。首先,看到随着训练的进行,音乐变得越来越复杂。一开始,模型通过坚持使用相同的音符和节奏来保险。到了第 10 个时期,模型已经开始生成小段音符,到了第 20 个时期,它产生了有趣的节奏,并且牢固地确立在一个固定的调(E ♭大调)中。

        其次,我们可以通过绘制每个时间步的预测分布的热图来分析随时间变化的音符分布。图 11-9 展示了在图 11-8 中第 20 个时期的示例的热图。

        图 11-9。随着时间推移可能的下一个音符的分布(在第 20 个时期):方块越暗,模型对下一个音符在这个音高的确定性就越高

        这里需要注意的一个有趣的点是,模型显然已经学会了哪些音符属于特定的,因为在不属于该调的音符处存在分布中的间隙。例如,在音符 54(对应于 G ♭/F ♯)的行上有一个灰色间隙。在 E ♭大调的音乐作品中,这个音符极不可能出现。模型在生成过程的早期就确立了调,并且随着乐曲的进行,模型选择更有可能出现在该调中的音符,通过关注代表它的标记。

        值得一提的是,模型还学会了巴赫特有的风格,即在大提琴上降到低音结束一个乐句,然后又反弹回来开始下一个乐句。看看大约在第 20 个音符附近,乐句以低音 E♭结束——在巴赫大提琴组曲中,通常会回到乐器更高、更响亮的音域开始下一个乐句,这正是模型的预测。在低音 E♭(音高编号 39)和下一个音符之间有一个很大的灰色间隙,预测下一个音符将在音高编号 50 左右,而不是继续在乐器的低音区域漂浮。

        最后,我们应该检查我们的注意力机制是否按预期工作。图 11-10 中的水平轴显示了生成的音符序列;垂直轴显示了网络在预测水平轴上的每个音符时所关注的位置。每个方块的颜色显示了在生成序列的每个点上所有头部中的最大注意力权重。方块越暗,表示在序列中这个位置上应用的注意力越多。为简单起见,我们在这个图表中只显示了音符,但网络也会关注每个音符的持续时间。

        我们可以看到,在初始调号、拍号和休止符中,网络选择几乎全部注意力放在START标记上。这是有道理的,因为这些特征总是出现在音乐片段的开头——一旦音符开始流动,START标记基本上就不再受到关注。

        当我们超过最初的几个音符时,我们可以看到网络主要关注大约最后两到四个音符,并很少对四个音符之前的音符给予重要权重。再次,这是有道理的;前四个音符中可能包含足够的信息,以了解乐句可能如何继续。此外,一些音符更强烈地回到 D 小调的调号上——例如E3(乐曲的第 7 个音符)和B-2(B♭-乐曲的第 14 个音符)。这很有趣,因为这些正是依赖 D 小调调号来消除任何模糊的确切音符。网络必须回顾调号才能知道调号中有一个 B♭(而不是 B 自然音),但调号中没有一个 E♭(必须使用 E 自然音)。

        图 11-10。矩阵中每个方块的颜色表示在水平轴上预测音符时,对垂直轴上每个位置给予的注意力量

        还有一些例子表明,网络选择忽略附近的某些音符或休止符,因为它们对理解乐句并没有提供额外信息。例如,倒数第二个音符(A2)对三个音符前的B-2并不特别关注,但对四个音符前的A2稍微更关注。对于模型来说,看位于节拍上的A2比看位于节拍外的B-2更有趣,后者只是一个过渡音。

        请记住,我们并没有告诉模型哪些音符相关,哪些音符属于哪个调号——它通过研究巴赫的音乐自己弄清楚了这一点。

        多声部音乐的标记化

        我们在本节中探讨的 Transformer 对单线(单声部)音乐效果很好,但它能够适应多线(复调)音乐吗?

        挑战在于如何将不同的音乐线表示为单个令牌序列。在前一节中,我们决定将音符和音符持续时间分成网络的两个不同输入和输出,但我们也看到我们可以将这些令牌交错成一个单一流。我们可以使用相同的想法来处理复调音乐。这里将介绍两种不同的方法:网格标记化基于事件的标记化,正如 2018 年的论文“音乐 Transformer:生成具有长期结构的音乐”中所讨论的那样。¹

        网格标记化

        考虑 J.S.巴赫赞美诗中的两小节音乐。有四个不同的声部(女高音[S],中音[A],男高音[T],低音[B]),分别写在不同的五线谱上。

        图 11-11。J.S.巴赫赞美诗的前两小节

        我们可以想象在网格上绘制这段音乐,其中 y 轴表示音符的音高,x 轴表示自作品开始以来经过的 16 分音符(四分音符)的数量。如果网格方块被填充,那么在那个时间点有音符在播放。所有四个声部都绘制在同一个网格上。这个网格被称为钢琴卷,因为它类似于一卷纸上打孔的物理卷,这在数字系统发明之前被用作记录机制。

        我们可以通过首先沿着四个声部,然后沿着时间步骤顺序移动,将网格序列化为令牌流。这将产生一个令牌序列S 1 , A 1 , T 1 , B 1 , S 2 , A 2 , T 2 , B 2 , ...,其中下标表示时间步骤,如图 11-12 所示。

        图 11-12。为巴赫赞美诗的前两小节创建网格标记化

        然后,我们将训练我们的 Transformer 模型以预测给定先前令牌的下一个令牌。我们可以通过将序列在时间上以四个音符一组(每个声部一个)展开来将生成的序列解码回网格结构。尽管同一个音符经常被分割成多个令牌,并且在其他声部的令牌之间有令牌,但这种技术效果出奇地好。

        然而,也存在一些缺点。首先,请注意,模型无法区分一个长音符和相同音高的两个较短相邻音符。这是因为标记化并没有明确编码音符的持续时间,只是在每个时间步是否存在音符。

        其次,这种方法要求音乐具有可以分成合理大小块的规则节拍。例如,使用当前系统,我们无法编码三连音(在一个拍子中演奏的三个音符)。我们可以将音乐分成每个四分音符(四分音符)12 个步骤,而不是 4 个步骤,这将使表示相同音乐段所需的令牌数量增加三倍,增加训练过程的开销,并影响模型的回溯能力。

        最后,我们不清楚如何将其他组件添加到标记化中,比如动态(每个声部的音乐是大声还是安静)或速度变化。我们被钢琴卷的二维网格结构所限制,这提供了一种方便的表示音高和节奏的方式,但不一定是一种容易融入使音乐听起来有趣的其他组件的方式。

        基于事件的标记化

        更灵活的方法是使用基于事件的令牌化。这可以被看作是一个词汇表,字面上描述了音乐是如何作为一系列事件创建的,使用丰富的令牌集。

        例如,在图 11-13 中,我们使用三种类型的令牌:

        • NOTE_ON<*音高*>(开始播放给定音高的音符)

        • NOTE_OFF<*音高*>(停止播放给定音高的音符)

        • TIME_SHIFT<*步骤*>(按给定步骤向前移动时间)

        这个词汇表可以用来创建一个描述音乐构造的序列,作为一组指令。

        图 11-13。巴赫赞美诗第一小节的事件令牌化

        我们可以轻松地将其他类型的令牌纳入这个词汇表中,以表示后续音符的动态和速度变化。通过使用TIME_SHIFT<0.33>令牌,这种方法还提供了一种在四分音符背景下生成三连音的方法。总的来说,这是一个更具表现力的令牌化框架,尽管对于 Transformer 来说,学习训练集音乐中固有模式可能更复杂,因为它在定义上比网格方法更少结构化。

        提示

        我鼓励您尝试实施这些复调技术,并使用您在本书中迄今为止积累的所有知识在新的令牌化数据集上训练 Transformer。我还建议查看我们的 Tristan Behrens 博士关于音乐生成研究的指南,可在GitHub上找到,该指南提供了关于使用深度学习进行音乐生成的不同论文的全面概述。

        在下一节中,我们将采用完全不同的方法来进行音乐生成,使用 GAN。# MuseGAN

        您可能认为图 11-12 中显示的钢琴卷看起来有点像现代艺术品。这引发了一个问题——我们实际上是否可以将这个钢琴卷视为图片,并利用图像生成方法而不是序列生成技术?

        正如我们将看到的,对于这个问题的答案是肯定的,我们可以直接将音乐生成视为图像生成问题。这意味着我们可以应用对图像生成问题非常有效的基于卷积的技术,特别是 GAN。

        MuseGAN 是在 2017 年的论文“MuseGAN:用于符号音乐生成和伴奏的多轨序列生成对抗网络”中引入的。作者展示了通过一种新颖的 GAN 框架训练模型生成复调、多轨、多小节音乐是可能的。此外,他们展示了通过将喂给生成器的噪声向量的责任分解,他们能够对音乐的高级时间和基于轨道的特征进行精细控制。

        让我们首先介绍 J.S.巴赫赞美诗数据集。

        运行此示例的代码

        此示例的代码可以在书籍存储库中的notebooks/11_music/02_musegan/musegan.ipynb中找到。

        巴赫赞美诗数据集

        要开始这个项目,您首先需要下载我们将用于训练 MuseGAN 的 MIDI 文件。我们将使用包含四声部的 229 首 J.S.巴赫赞美诗数据集。

        您可以通过在书籍存储库中运行巴赫赞美诗数据集下载器脚本来下载数据集,如示例 11-5 所示。这将把 MIDI 文件保存到本地的/data文件夹中。

        示例 11-5。下载巴赫赞美诗数据集
        bash scripts/download_bach_chorale_data.sh
        

        数据集由每个时间步长的四个数字数组组成:每个声部的 MIDI 音符音高。在这个数据集中,一个时间步长等于一个 16 分音符(半音符)。因此,例如,在 4 个四分音符拍的单个小节中,有 16 个时间步长。数据集会自动分成训练验证测试集。我们将使用训练数据集来训练 MuseGAN。

        首先,我们需要将数据整理成正确的形状以供 GAN 使用。在这个示例中,我们将生成两小节音乐,因此我们将提取每个赞美诗的前两小节。每小节包括 16 个时间步长,四个声部中有潜在的 84 个音高。

        提示

        从现在开始,声部将被称为轨道,以保持术语与原始论文一致。

        因此,转换后的数据将具有以下形状:

        [BATCH_SIZE, N_BARS, N_STEPS_PER_BAR, N_PITCHES, N_TRACKS]
        

        其中:

        BATCH_SIZE = 64
        N_BARS = 2
        N_STEPS_PER_BAR = 16
        N_PITCHES = 84
        N_TRACKS = 4
        

        为了将数据整理成这种形状,我们将音高数字进行独热编码,转换为长度为 84 的向量,并将每个音符序列分成两小节,每小节包括 16 个时间步长。我们在这里做出的假设是数据集中的每个赞美诗每小节有四拍,这是合理的,即使不是这种情况,也不会对模型的训练产生不利影响。

        图 11-14 展示了两小节原始数据如何转换为我们将用来训练 GAN 的转换后的钢琴卷帘数据集。

        图 11-14。将两小节原始数据处理成我们可以用来训练 GAN 的钢琴卷帘数据 ## MuseGAN 生成器

        像所有 GAN 一样,MuseGAN 由一个生成器和一个评论家组成。生成器试图用其音乐创作愚弄评论家,评论家试图通过确保能够区分生成器伪造的巴赫赞美诗和真实的赞美诗来阻止这种情况发生。

        MuseGAN 的不同之处在于生成器不仅接受单个噪声向量作为输入,而是有四个单独的输入,分别对应音乐的四个不同特征:和弦、风格、旋律和节奏。通过独立操纵这些输入中的每一个,我们可以改变生成音乐的高级属性。

        生成器的高级视图显示在图 11-15 中。

        MuseGAN 生成器的高级图表

        图表显示和弦和旋律输入首先通过一个时间网络,输出一个维度等于要生成的小节数的张量。风格和节奏输入不会以这种方式在时间上拉伸,因为它们在整个乐曲中保持不变。

        然后,为了为特定轨道的特定小节生成特定小节,来自和弦、风格、旋律和节奏部分的相关输出被连接起来形成一个更长的向量。然后将其传递给小节生成器,最终输出指定轨道的指定小节。

        通过连接所有轨道的生成小节,我们创建了一个可以与评论家的真实分数进行比较的分数。

        让我们首先看看如何构建一个时间网络。

        时间网络

        时间网络的工作是将长度为Z_DIM = 32的单个输入噪声向量转换为每个小节的不同噪声向量(长度也为 32)的神经网络,由卷积转置层组成。构建这个网络的 Keras 代码在示例 11-6 中显示。

        示例 11-6。构建时间网络
        def conv_t(x, f, k, s, a, p, bn):
            x = layers.Conv2DTranspose(
                        filters = f
                        , kernel_size = k
                        , padding = p
                        , strides = s
                        , kernel_initializer = initializer
                        )(x)
            if bn:
                x = layers.BatchNormalization(momentum = 0.9)(x)
        
            x = layers.Activation(a)(x)
            return x
        
        def TemporalNetwork():
            input_layer = layers.Input(shape=(Z_DIM,), name='temporal_input') # ①
            x = layers.Reshape([1,1,Z_DIM])(input_layer) # ②
            x = conv_t(
                x, f=1024, k=(2,1), s=(1,1), a = 'relu', p = 'valid', bn = True
            ) # ③
            x = conv_t(
                x, f=Z_DIM, k=(N_BARS - 1,1), s=(1,1), a = 'relu', p = 'valid', bn = True
            )
            output_layer = layers.Reshape([N_BARS, Z_DIM])(x) # ④
            return models.Model(input_layer, output_layer)
        

        时间网络的输入是长度为 32 的向量(Z_DIM)。

        我们将这个向量重塑为一个具有 32 个通道的 1×1 张量,以便我们可以对其应用二维卷积转置操作。

        我们应用Conv2DTranspose层来沿一个轴扩展张量的大小,使其与N_BARS的长度相同。

        我们使用 Reshape 层去除不必要的额外维度。

        我们使用卷积操作而不是要求两个独立的向量进入网络的原因是,我们希望网络学习如何以一种一致的方式让一个小节跟随另一个小节。使用神经网络沿着时间轴扩展输入向量意味着模型有机会学习音乐如何跨越小节流动,而不是将每个小节视为完全独立于上一个的。

        和弦、风格、旋律和 groove

        现在让我们更仔细地看一下喂给生成器的四种不同输入:

        和弦

        和弦输入是一个长度为 Z_DIM 的单一噪声向量。这个向量的作用是控制音乐随时间的总体进展,跨越轨道共享,因此我们使用 TemporalNetwork 将这个单一向量转换为每个小节的不同潜在向量。请注意,虽然我们称这个输入为和弦,但它实际上可以控制音乐中每个小节变化的任何内容,比如一般的节奏风格,而不是特定于任何特定轨道。

        风格

        风格输入也是长度为 Z_DIM 的向量。这个向量在不经过转换的情况下传递,因此在所有小节和轨道上都是相同的。它可以被视为控制乐曲整体风格的向量(即,它会一致地影响所有小节和轨道)。

        旋律

        旋律输入是一个形状为 [N_TRACKS, Z_DIM] 的数组—也就是说,我们为每个轨道提供长度为 Z_DIM 的随机噪声向量。

        这些向量中的每一个都通过轨道特定的 TemporalNetwork,其中轨道之间的权重不共享。输出是每个轨道的每个小节的长度为 Z_DIM 的向量。因此,模型可以使用这些输入向量来独立地微调每个小节和轨道的内容。

        Groove

        groove 输入也是一个形状为 [N_TRACKS, Z_DIM] 的数组,即每个轨道的长度为 Z_DIM 的随机噪声向量。与旋律输入不同,这些向量不通过时间网络,而是直接传递,就像风格向量一样。因此,每个 groove 向量将影响轨道的整体属性,跨越所有小节。

        我们可以总结每个 MuseGAN 生成器组件的责任,如 表 11-1 所示。

        表 11-1. MuseGAN 生成器的组件

        输出在小节之间不同吗? 输出在部分之间不同吗?
        风格
        Groove
        和弦
        旋律

        MuseGAN 生成器的最后一部分是 小节生成器—让我们看看如何使用它来将和弦、风格、旋律和 groove 组件的输出粘合在一起。

        小节生成器

        小节生成器接收四个潜在向量——来自和弦、风格、旋律和 groove 组件。这些被连接起来产生长度为 4 * Z_DIM 的输入向量。输出是单个轨道的单个小节的钢琴卷表示—即,形状为 [1, n_steps_per_bar, n_pitches, 1] 的张量。

        小节生成器只是一个使用卷积转置层来扩展输入向量的时间和音高维度的神经网络。我们为每个轨道创建一个小节生成器,轨道之间的权重不共享。构建 BarGenerator 的 Keras 代码在 示例 11-7 中给出。

        示例 11-7. 构建 BarGenerator
        def BarGenerator():
        
            input_layer = layers.Input(shape=(Z_DIM * 4,), name='bar_generator_input') # ①
        
            x = layers.Dense(1024)(input_layer) # ②
            x = layers.BatchNormalization(momentum = 0.9)(x)
            x = layers.Activation('relu')(x)
            x = layers.Reshape([2,1,512])(x)
        
            x = conv_t(x, f=512, k=(2,1), s=(2,1), a= 'relu',  p = 'same', bn = True) # ③
            x = conv_t(x, f=256, k=(2,1), s=(2,1), a= 'relu', p = 'same', bn = True)
            x = conv_t(x, f=256, k=(2,1), s=(2,1), a= 'relu', p = 'same', bn = True)
            x = conv_t(x, f=256, k=(1,7), s=(1,7), a= 'relu', p = 'same', bn = True) # ④
            x = conv_t(x, f=1, k=(1,12), s=(1,12), a= 'tanh', p = 'same', bn = False) # ⑤
        
            output_layer = layers.Reshape([1, N_STEPS_PER_BAR , N_PITCHES ,1])(x) # ⑥
        
            return models.Model(input_layer, output_layer)
        

        bar 生成器的输入是长度为 4 * Z_DIM 的向量。

        通过一个 Dense 层后,我们重新塑造张量以准备进行卷积转置操作。

        首先我们沿着时间步长轴扩展张量…​

        …​然后沿着音高轴。

        最终层应用了 tanh 激活,因为我们将使用 WGAN-GP(需要 tanh 输出激活)来训练网络。

        张量被重塑以添加两个大小为 1 的额外维度,以准备与其他小节和轨道连接。

        将所有内容整合在一起

        最终,MuseGAN 生成器接受四个输入噪声张量(和弦、风格、旋律和节奏),并将它们转换为一个多轨多小节乐谱。构建 MuseGAN 生成器的 Keras 代码在示例 11-8 中提供。

        示例 11-8。构建 MuseGAN 生成器
        def Generator():
            chords_input = layers.Input(shape=(Z_DIM,), name='chords_input') # ①
            style_input = layers.Input(shape=(Z_DIM,), name='style_input')
            melody_input = layers.Input(shape=(N_TRACKS, Z_DIM), name='melody_input')
            groove_input = layers.Input(shape=(N_TRACKS, Z_DIM), name='groove_input')
        
            chords_tempNetwork = TemporalNetwork() # ②
            chords_over_time = chords_tempNetwork(chords_input)
        
            melody_over_time = [None] * N_TRACKS
            melody_tempNetwork = [None] * N_TRACKS
            for track in range(N_TRACKS):
                melody_tempNetwork[track] = TemporalNetwork() # ③
                melody_track = layers.Lambda(lambda x, track = track: x[:,track,:])(
                    melody_input
                )
                melody_over_time[track] = melody_tempNetworktrack
        
            barGen = [None] * N_TRACKS
            for track in range(N_TRACKS):
                barGen[track] = BarGenerator() # ④
        
            bars_output = [None] * N_BARS
            c = [None] * N_BARS
            for bar in range(N_BARS): # ⑤
                track_output = [None] * N_TRACKS
        
                c[bar] = layers.Lambda(lambda x, bar = bar: x[:,bar,:])(chords_over_time)
                s = style_input
        
                for track in range(N_TRACKS):
        
                    m = layers.Lambda(lambda x, bar = bar: x[:,bar,:])(
                        melody_over_time[track]
                    )
                    g = layers.Lambda(lambda x, track = track: x[:,track,:])(
                        groove_input
                    )
        
                    z_input = layers.Concatenate(
                        axis = 1, name = 'total_input_bar_{}_track_{}'.format(bar, track)
                    )([c[bar],s,m,g])
        
                    track_output[track] = barGentrack
        
                bars_output[bar] = layers.Concatenate(axis = -1)(track_output)
        
            generator_output = layers.Concatenate(axis = 1, name = 'concat_bars')(
                bars_output
            ) # ⑥
        
            return models.Model(
                [chords_input, style_input, melody_input, groove_input], generator_output
            ) # ⑦
        
        generator = Generator()
        

        定义生成器的输入。

        通过时间网络传递和弦输入。

        通过时间网络传递旋律输入。

        为每个轨道创建一个独立的小节生成器网络。

        循环遍历轨道和小节,为每种组合创建一个生成的小节。

        将所有内容连接在一起形成单个输出张量。

        MuseGAN 模型接受四个不同的噪声张量作为输入,并输出一个生成的多轨多小节乐谱。

        MuseGAN 评论家

        与生成器相比,评论家的架构要简单得多(这在 GAN 中经常是这样)。

        评论家试图区分生成器创建的完整多轨多小节乐谱和巴赫赞美诗的真实节选。它是一个卷积神经网络,主要由将乐谱折叠成单个输出预测的Conv3D层组成。

        Conv3D 层

        到目前为止,在本书中,我们只使用了适用于三维输入图像(宽度、高度、通道)的Conv2D层。在这里,我们必须使用Conv3D层,它们类似于Conv2D层,但接受四维输入张量(n_barsn_steps_per_barn_pitchesn_tracks)。

        我们在评论家中不使用批量归一化层,因为我们将使用 WGAN-GP 框架来训练 GAN,这是不允许的。

        构建评论家的 Keras 代码在示例 11-9 中给出。

        示例 11-9。构建 MuseGAN 评论家
        def conv(x, f, k, s, p):
            x = layers.Conv3D(filters = f
                        , kernel_size = k
                        , padding = p
                        , strides = s
                        , kernel_initializer = initializer
                        )(x)
            x = layers.LeakyReLU()(x)
            return x
        
        def Critic():
            critic_input = layers.Input(
                shape=(N_BARS, N_STEPS_PER_BAR, N_PITCHES, N_TRACKS),
                name='critic_input'
            ) # ①
        
            x = critic_input
            x = conv(x, f=128, k = (2,1,1), s = (1,1,1), p = 'valid') # ②
            x = conv(x, f=128, k = (N_BARS - 1,1,1), s = (1,1,1), p = 'valid')
        
            x = conv(x, f=128, k = (1,1,12), s = (1,1,12), p = 'same') # ③
            x = conv(x, f=128, k = (1,1,7), s = (1,1,7), p = 'same')
        
            x = conv(x, f=128, k = (1,2,1), s = (1,2,1), p = 'same') # ④
            x = conv(x, f=128, k = (1,2,1), s = (1,2,1), p = 'same')
            x = conv(x, f=256, k = (1,4,1), s = (1,2,1), p = 'same')
            x = conv(x, f=512, k = (1,3,1), s = (1,2,1), p = 'same')
        
            x = layers.Flatten()(x)
        
            x = layers.Dense(1024, kernel_initializer = initializer)(x)
            x = layers.LeakyReLU()(x)
        
            critic_output = layers.Dense(
                1, activation=None, kernel_initializer = initializer
            )(x) # ⑤
        
            return models.Model(critic_input, critic_output)
        
        critic = Critic()
        

        评论家的输入是一个多轨多小节乐谱数组,每个形状为[N_BARS, N_STEPS_PER_BAR, N_PITCHES, N_TRACKS]

        首先,我们沿着小节轴折叠张量。由于我们使用的是 4D 张量,所以在评论家中应用Conv3D层。

        接下来,我们沿着音高轴折叠张量。

        最后,我们沿着时间步轴折叠张量。

        输出是一个具有单个单元且没有激活函数的Dense层,这是 WGAN-GP 框架所需的。

        MuseGAN 的分析

        我们可以通过生成一个乐谱,然后调整一些输入噪声参数来查看对输出的影响来进行一些实验。

        生成器的输出是一个值范围在[-1, 1]的数组(由于最终层的 tanh 激活函数)。为了将其转换为每个轨道的单个音符,我们选择每个时间步的所有 84 个音高中具有最大值的音符。在原始的 MuseGAN 论文中,作者使用了阈值 0,因为每个轨道可以包含多个音符;然而,在这种情况下,我们可以简单地取最大值来确保每个时间步每个轨道恰好有一个音符,这与巴赫赞美诗的情况相同。

        图 11-16 显示了模型从随机正态分布的噪声向量生成的乐谱(左上角)。我们可以通过欧几里德距离找到数据集中最接近的乐谱,并检查我们生成的乐谱是否是数据集中已经存在的音乐片段的副本——最接近的乐谱显示在其正下方,我们可以看到它与我们生成的乐谱并不相似。

        现在让我们玩弄输入噪声来微调我们生成的乐谱。首先,我们可以尝试改变和弦噪声向量——图 11-16 中左下角的乐谱显示了结果。我们可以看到每个轨道都已经改变,正如预期的那样,而且两个小节展现出不同的特性。在第二小节中,贝斯线更加动态,顶部音乐线的音高比第一小节更高。这是因为影响两个小节的潜在向量是不同的,因为输入和弦向量通过了一个时间网络。

        图 11-16。MuseGAN 预测乐谱的示例,显示训练数据中最接近的真实乐谱以及通过改变输入噪声而影响生成乐谱的情况

        总结

        当我们改变风格向量(右上角)时,两个小节以类似的方式改变。整个乐段的风格已经从原始生成的乐谱中改变,以一种一致的方式(即,相同的潜在向量被用来调整所有轨道和小节)。

        我们还可以通过旋律和节奏输入单独改变轨道。在图 11-16 中间右侧的乐谱中,我们可以看到仅改变顶部音乐线的旋律噪声输入的效果。所有其他部分保持不变,但顶部音符发生了显著变化。此外,我们可以看到顶部音乐线两个小节之间的节奏变化:第二小节比第一小节更动态,包含比第一小节更快的音符。

        最后,图中右下角的乐谱显示了当我们仅改变贝斯的节奏输入参数时预测的乐谱。同样,所有其他部分保持不变,但贝斯部分是不同的。此外,贝斯的整体模式在小节之间保持相似,这是我们所期望的。

        这展示了如何每个输入参数可以直接影响生成的音乐序列的高级特征,就像我们之前能够调整 VAE 和 GAN 的潜在向量以改变生成图像的外观一样。模型的一个缺点是必须预先指定要生成的小节数。为了解决这个问题,作者展示了模型的一个扩展,允许将先前的小节作为输入馈入,使模型能够通过不断将最近预测的小节作为额外输入来生成长形乐谱。

        在本章中,我们探讨了两种不同类型的音乐生成模型:Transformer 和 MuseGAN。

        Transformer 的设计类似于我们在第九章中看到的用于文本生成的网络。音乐和文本生成有很多共同点,通常可以同时用于两者的类似技术。我们通过将两个输入和输出流(音符和持续时间)纳入 Transformer 架构来扩展了 Transformer 架构。我们看到模型能够通过准确生成巴赫音乐来学习关于调式和音阶等概念。

        我们还探讨了如何调整标记化过程以处理多声部(多轨)音乐生成。网格标记化将乐谱的钢琴卷表示序列化,使我们能够在描述每个音轨中存在哪个音符的令牌的单个流上训练 Transformer,在离散的、等间隔的时间步长间隔内。基于事件的标记化产生了一个配方,描述了如何以顺序方式创建多行音乐,通过一系列指令的单个流。这两种方法都有优缺点——Transformer 基于的音乐生成方法的成功或失败往往严重依赖于标记化方法的选择。

        我们还看到生成音乐并不总是需要顺序方法——MuseGAN 使用卷积来生成具有多轨的多声部乐谱,将乐谱视为图像,其中轨道是图像的各个通道。MuseGAN 的新颖之处在于四个输入噪声向量(和弦、风格、旋律和节奏)的组织方式,使得可以对音乐的高级特征保持完全控制。虽然底层的和声仍然不像巴赫的那样完美或多样化,但这是对一个极其难以掌握的问题的良好尝试,并突显了 GAN 处理各种问题的能力。

        ¹ 黄成志安娜等人,“音乐 Transformer:生成具有长期结构的音乐”,2018 年 9 月 12 日,https://arxiv.org/abs/1809.04281

        ² 董浩文等人,“MuseGAN:用于符号音乐生成和伴奏的多轨序列生成对抗网络”,2017 年 9 月 19 日,https://arxiv.org/abs/1709.06298

        第十二章:世界模型

        本章介绍了近年来生成模型最有趣的应用之一,即它们在所谓的世界模型中的使用。

        介绍

        2018 年 3 月,David Ha 和 Jürgen Schmidhuber 发表了他们的“World Models”论文。该论文展示了如何通过在自己生成的梦境环境中进行实验来训练一个模型,而不是在真实环境中。这是一个很好的例子,说明了当与强化学习等其他机器学习技术一起应用时,生成建模如何解决实际问题。

        架构的一个关键组件是一个生成模型,它可以构建给定当前状态和动作的下一个可能状态的概率分布。通过随机移动建立对环境基础物理的理解后,模型能够在自己对环境的内部表示中完全自行训练新任务。这种方法导致了在测试时两个任务的世界最佳得分。

        在本章中,我们将详细探讨论文中的模型,特别关注一个需要代理学习如何尽可能快地在虚拟赛道上驾驶汽车的任务。虽然我们将使用 2D 计算机模拟作为我们的环境,但相同的技术也可以应用于在现实环境中测试策略昂贵或不可行的情况。

        提示

        在本章中,我们将引用“World Models”论文的优秀 TensorFlow 实现,该实现公开在 GitHub 上,我鼓励您克隆并运行!

        在我们开始探索模型之前,我们需要更仔细地了解强化学习的概念。

        强化学习

        强化学习可以定义如下:

        强化学习(RL)是一种机器学习领域,旨在训练一个代理在给定环境中以达到特定目标的最佳表现。

        虽然判别建模和生成建模都旨在在观察数据集上最小化损失函数,但强化学习旨在最大化给定环境中代理的长期奖励。它通常被描述为机器学习的三大分支之一,与监督学习(使用标记数据进行预测)和无监督学习(从未标记数据中学习结构)并列。

        让我们首先介绍一些与强化学习相关的关键术语:

        环境

        代理操作的世界。它定义了规则集,这些规则管理游戏状态更新过程和奖励分配,考虑到代理的先前动作和当前游戏状态。例如,如果我们正在教一个强化学习算法下棋,环境将包括规定给定动作(例如,兵的移动e2e4)如何影响下一个游戏状态(棋盘上棋子的新位置)的规则,并且还会指定如何评估给定位置是否为将军,并在获胜移动后为获胜玩家分配奖励 1。

        代理

        在环境中采取行动的实体。

        游戏状态

        代理可能会遇到的特定情况的数据(也称为状态)。例如,具有伴随游戏信息的特定棋盘配置,例如哪个玩家将进行下一步移动。

        行动

        代理可以采取的可行移动。

        奖励

        环境在采取行动后向代理返回的值。代理的目标是最大化其奖励的长期总和。例如,在国际象棋游戏中,将对手的国王将军的奖励为 1,而其他每一步的奖励为 0。其他游戏在整个 episode 中不断授予奖励(例如,在Space Invaders游戏中的得分)。

        Episode

        环境中代理的一次运行;这也被称为rollout

        时间步

        对于离散事件环境,所有状态、动作和奖励都被标注以显示它们在时间步t的值。

        这些概念之间的关系在图 12-1 中显示。

        图 12-1. 强化学习图表

        环境首先使用当前游戏状态s 0进行初始化。在时间步t,代理接收当前游戏状态s t并使用它来决定下一个最佳动作a t,然后执行。给定这个动作,环境然后计算下一个状态s t+1和奖励r t+1,并将它们传递回代理,以便循环再次开始。这个循环会持续直到 episode 的结束条件满足(例如,经过给定数量的时间步或代理赢得/输掉)。

        我们如何设计一个代理来最大化在给定环境中的奖励总和?我们可以构建一个包含一组规则的代理,用于如何响应任何给定的游戏状态。然而,随着环境变得更加复杂,这很快变得不可行,并且永远不允许我们构建一个在特定任务中具有超人能力的代理,因为我们正在硬编码规则。强化学习涉及创建一个代理,通过反复游戏在复杂环境中学习最佳策略。

        现在让我们来看一下模拟汽车在赛道上行驶的CarRacing环境。

        赛车环境

        CarRacing是通过Gymnasium包提供的环境。Gymnasium 是一个用于开发强化学习算法的 Python 库,其中包含几个经典的强化学习环境,如CartPolePong,以及提出更复杂挑战的环境,比如训练代理在不平坦地形上行走或赢得 Atari 游戏。

        Gymnasium

        Gymnasium 是 OpenAI 的 Gym 库的维护分支——自 2021 年以来,Gym 的进一步开发已转移到 Gymnasium。因此,在本书中,我们将 Gymnasium 环境称为 Gym 环境。

        所有环境都提供了一个step方法,通过该方法您可以提交一个给定的动作;环境将返回下一个状态和奖励。通过反复调用代理选择的动作来调用 step 方法,您可以在环境中玩出一个 episode。还有一个reset方法,用于将环境恢复到初始状态,以及一个render方法,允许您观看您的代理在给定环境中执行。这对于调试和找到代理可以改进的地方非常有用。

        让我们看看CarRacing环境中游戏状态、动作、奖励和 episode 是如何定义的:

        游戏状态

        一个 64×64 像素的 RGB 图像,描绘了赛道和汽车的俯视图。

        动作

        一组三个值:方向盘方向(-1 到 1)、加速度(0 到 1)和刹车(0 到 1)。代理必须在每个时间步设置这三个值。

        奖励

        每个时间步骤都会受到-0.1 的负惩罚,如果访问了新的赛道瓷砖,则会获得 1000/N的正奖励,其中N是构成赛道的瓷砖总数。

        剧集

        当汽车完成赛道或驶出环境边缘,或者经过了 3000 个时间步骤时,剧集结束。

        这些概念在图 12-2 中的游戏状态的图形表示中显示。

        图 12-2. CarRacing环境中一个游戏状态的图形表示

        视角

        我们应该想象代理人漂浮在赛道上方,从鸟瞰视角控制汽车,而不是从驾驶员的视角看赛道。

        世界模型概述

        我们现在将对整个世界模型架构和训练过程进行高层概述,然后深入研究每个组件。

        架构

        解决方案由三个不同部分组成,如图 12-3 所示,它们分别进行训练:

        V

        变分自动编码器(VAE)

        M

        带有混合密度网络(MDN-RNN)的递归神经网络

        C

        一个控制器

        图 12-3. 世界模型架构图

        VAE

        当您驾驶时做出决策时,您并不会主动分析视野中的每个像素—而是将视觉信息压缩成较少数量的潜在实体,例如道路的直线程度、即将到来的弯道以及您相对于道路的位置,以指导您的下一个动作。

        我们在第三章中看到,VAE 可以将高维输入图像压缩成一个潜在随机变量,该变量近似遵循标准高斯分布,通过最小化重构误差和 KL 散度。这确保了潜在空间是连续的,我们能够轻松从中进行采样以生成有意义的新观察。

        在汽车赛道示例中,VAE 将 64×64×3(RGB)输入图像压缩成一个 32 维正态分布的随机变量,由两个变量mulogvar参数化。这里,logvar是分布方差的对数。我们可以从该分布中采样以产生代表当前状态的潜在向量z。这将传递给网络的下一个部分,MDN-RNN。

        MDN-RNN

        当您驾驶时,每个后续观察对您来说并不是完全意外的。如果当前观察表明前方道路左转,您向左转动方向盘,您期望下一个观察显示您仍然与道路保持一致。

        如果您没有这种能力,您的汽车可能会在道路上蛇行,因为您无法看到稍微偏离中心会在下一个时间步骤中变得更糟,除非您现在采取措施。

        这种前瞻性是 MDN-RNN 的任务,它试图根据先前的潜在状态和先前的动作来预测下一个潜在状态的分布。

        具体来说,MDN-RNN 是一个具有 256 个隐藏单元的 LSTM 层,后面跟着一个混合密度网络(MDN)输出层,允许下一个潜在状态实际上可以从几个正态分布中的任何一个中抽取。

        “世界模型”论文的一位作者 David Ha 也将相同的技术应用于手写生成任务,如图 12-4 所示,描述了下一个笔尖可能落在几个不同红色区域中的事实。

        图 12-4. 用于手写生成的 MDN

        在汽车赛道示例中,我们允许下一个观察到的潜在状态的每个元素都可以从五个正态分布中的任何一个中抽取。

        控制器

        到目前为止,我们还没有提到选择动作的事情。这个责任在于控制器。控制器是一个密集连接的神经网络,其中输入是z(从 VAE 编码的分布中采样的当前潜在状态)和 RNN 的隐藏状态的串联。三个输出神经元对应于三个动作(转向、加速、刹车),并且被缩放以落入适当的范围内。

        控制器使用强化学习进行训练,因为没有训练数据集会告诉我们某个动作是还是。相反,代理通过反复实验自己发现这一点。

        正如我们将在本章后面看到的那样,“世界模型”论文的关键在于它展示了如何在代理的环境生成模型中进行强化学习,而不是在 Gym 环境中。换句话说,它发生在代理对环境行为的幻想版本中,而不是真实的环境中。

        为了理解三个组件的不同角色以及它们如何共同工作,我们可以想象它们之间的对话:

        VAE(查看最新的 64×64×3 观察):这看起来像一条笔直的道路,接近一个轻微的左弯,汽车面向道路的方向(z)。

        RNN:基于那个描述(z)和控制器选择在上一个时间步加速的事实(action),我将更新我的隐藏状态(h),以便下一个观察被预测为仍然是一条笔直的道路,但在视野中有稍微更多的左转。

        控制器:基于来自 VAE 的描述(z)和来自 RNN 的当前隐藏状态(h),我的神经网络输出[0.34, 0.8, 0]作为下一个动作。

        然后,控制器的动作传递给环境,环境返回更新后的观察结果,循环再次开始。

        训练

        训练过程包括五个步骤,按顺序运行,概述如下:

        1. 收集随机回滚数据。在这里,代理不关心给定任务,而是简单地使用随机动作探索环境。多个剧集被模拟,每个时间步的观察状态、动作和奖励被存储。这个想法是建立一个关于环境物理工作方式的数据集,然后 VAE 可以从中学习以有效地捕捉状态作为潜在向量。MDN-RNN 随后可以学习潜在向量随时间的演变方式。

        2. 训练 VAE。使用随机收集的数据,我们在观察图像上训练 VAE。

        3. 收集数据以训练 MDN-RNN。一旦我们有了训练好的 VAE,我们使用它将每个收集到的观察编码为mulogvar向量,并将其保存在当前动作和奖励旁边。

        4. 训练 MDN-RNN。我们获取一批批次的剧集,并在每个时间步加载在步骤 3 生成的mulogvaractionreward变量。然后我们从mulogvar向量中采样一个z向量。给定当前的z向量、actionreward,然后训练 MDN-RNN 来预测随后的z向量和reward

        5. 训练控制器。通过训练好的 VAE 和 RNN,我们现在可以训练控制器,以输出一个动作,给定当前的z和 RNN 的隐藏状态h。控制器使用进化算法 CMA-ES 作为其优化器。该算法奖励生成导致任务整体得分较高的动作的矩阵权重,以便未来的代际也可能继承这种期望的行为。

        让我们现在更详细地看看每个步骤。

        收集随机回滚数据

        第一步是从环境中收集 rollout 数据,使用一个代理器执行随机动作。这可能看起来很奇怪,因为我们最终希望我们的代理器学会如何采取智能动作,但这一步将提供代理器将用于学习世界运作方式以及其动作(尽管起初是随机的)如何影响随后观察的数据。

        我们可以通过启动多个 Python 进程并行捕获多个 episode,每个进程运行环境的单独实例。每个进程将在单独的核心上运行,因此如果您的计算机有很多核心,您可以比只有几个核心时更快地收集数据。

        这一步使用的超参数如下:

        parallel_processes

        要运行的并行进程数(例如,如果您的计算机有≥8 个核心,则为8

        max_trials

        每个进程应总共运行多少个 episode(例如,125,因此 8 个进程将总共创建 1,000 个 episode)

        max_frames

        每个 episode 的最大时间步数(例如,300

        图 12-5 显示了一个 episode 的第 40 到 59 帧的摘录,汽车驶向一个拐角,同时显示了随机选择的动作和奖励。请注意,随着汽车经过新的赛道瓷砖,奖励变为 3.22,但其他情况下为-0.1。

        一个 episode 的第 40 到 59 帧

        图 12-5。一个 episode 的第 40 到 59 帧

        训练 VAE

        现在我们在收集的数据上构建一个生成模型(VAE)。请记住,VAE 的目的是让我们将一个 64×64×3 的图像折叠成一个正态分布的随机变量z,其分布由两个向量mulogvar参数化。这两个向量的长度均为 32。这一步的超参数如下:

        vae_batch_size

        训练 VAE 时使用的批量大小(每批次观察数量)(例如,100

        z_size

        潜在z向量的长度(因此mulogvar变量)(例如,32

        vae_num_epoch

        训练时的 epoch 数量(例如,10

        VAE 架构

        正如我们之前看到的,Keras 允许我们不仅定义将进行端到端训练的 VAE 模型,还可以定义额外的子模型,分别定义训练网络的编码器和解码器。例如,当我们想要对特定图像进行编码或解码给定的z向量时,这些将非常有用。我们将定义 VAE 模型和三个子模型,如下所示:

        vae

        这是经过训练的端到端 VAE。它接受一个 64×64×3 的图像作为输入,并输出一个重建的 64×64×3 的图像。

        encode_mu_logvar

        这接受一个 64×64×3 的图像作为输入,并输出与该输入对应的mulogvar向量。多次通过该模型运行相同的输入图像将每次产生相同的mulogvar向量。

        encode

        这接受一个 64×64×3 的图像作为输入,并输出一个采样的z向量。多次通过该模型运行相同的输入图像将每次产生不同的z向量,使用计算出的mulogvar值来定义采样分布。

        decode

        这接受一个z向量作为输入,并返回重建的 64×64×3 图像。

        模型和子模型的图表显示在图 12-6 中。

        《World Models》论文中的 VAE 架构

        图 12-6。《World Models》论文中的 VAE 架构

        探索 VAE

        现在我们将查看 VAE 和每个子模型的输出,然后看看 VAE 如何用于生成全新的赛道观察。

        VAE 模型

        如果我们将一个观察输入到 VAE 中,它能够准确重建原始图像,如图 12-7 所示。这对于直观检查 VAE 是否正常工作非常有用。

        VAE 模型的输入和输出

        图 12-7。VAE 模型的输入和输出

        编码器模型

        如果我们用一个观察来喂encode_mu_logvar模型,输出将是描述多元正态分布的生成mulogvar向量。encode模型进一步采样特定的z向量。显示两个编码器模型输出的图表在图 12-8 中。

        来自编码器模型的输出

        图 12-8。编码器模型的输出

        潜变量z是从由mulogvar定义的高斯分布中采样的,通过从标准高斯中采样,然后缩放和移位采样的向量(示例 12-1)。

        示例 12-1。从由mulogvar定义的多元正态分布中采样z
        eps = tf.random_normal(shape=tf.shape(mu))
        sigma = tf.exp(logvar * 0.5)
        z = mu + eps * sigma
        

        解码器模型

        decode模型接受一个z向量作为输入,并重构原始图像。在图 12-9 中,我们线性插值z的两个维度,以展示每个维度似乎编码轨道的特定方面——在这个例子中,z[4]控制了最接近汽车的轨道的左右方向,z[7]控制了即将到来的左转的急剧程度。

        这表明 VAE 学习到的潜在空间是连续的,可以用来生成代理以前从未观察过的新轨迹段。

        z 的两个维度的线性插值

        图 12-9。z的两个维度的线性插值

        收集数据以训练 MDN-RNN

        现在我们有了一个经过训练的 VAE,我们可以用它来为我们的 MDN-RNN 生成训练数据。

        在这一步中,我们通过encode_mu_logvar模型传递所有随机回滚观察,并存储与每个观察相对应的mulogvar向量。这些编码数据,以及已经收集的actionrewarddone变量,将用于训练 MDN-RNN。这个过程在图 12-10 中显示。

        创建 RNN 训练数据集

        图 12-10。创建 MDN-RNN 训练数据集

        训练 MDN-RNN

        现在我们可以训练 MDN-RNN 来预测下一个z向量的分布,并在未来一个时间步骤内奖励,给定当前的z向量、当前的动作和先前的奖励。然后我们可以使用 RNN 的内部隐藏状态(可以被视为模型对环境动态的当前理解)作为控制器的输入之一,控制器最终将决定最佳的下一步动作。

        这个过程的超参数如下:

        rnn_batch_size

        训练 MDN-RNN 时使用的批量大小(每批次多少个序列)(例如,100

        rnn_num_steps

        训练的总迭代次数(例如,4000

        MDN-RNN 架构

        MDN-RNN 的架构在图 12-11 中显示。

        RNN 架构

        图 12-11。MDN-RNN 架构

        MDN-RNN 由一个 LSTM 层(RNN)组成,后面是一个密集连接层(MDN),将 LSTM 的隐藏状态转换为混合分布的参数。让我们逐步走过网络。

        LSTM 层的输入是一个长度为 36 的向量,是从 VAE 的编码z向量(长度为 32)、当前动作(长度为 3)和先前奖励(长度为 1)连接而成的。

        LSTM 层的输出是一个长度为 256 的向量,每个 LSTM 单元在该层中有一个值。这被传递给 MDN,MDN 只是一个密集连接层,将长度为 256 的向量转换为长度为 481 的向量。

        为什么是 481?图 12-12 解释了从 MDN-RNN 的输出组成。混合密度网络的目的是模拟我们的下一个z可能从几个可能的分布中以一定概率抽取的事实。在汽车赛车示例中,我们选择了五个正态分布。我们需要多少参数来定义这些分布?对于这 5 个混合物,我们需要一个mu和一个logvar(来定义分布)以及被选择的这个混合物的对数概率(logpi),对于z的每个 32 个维度。这使得 5 × 3 × 32 = 480 个参数。额外的一个参数是用于奖励预测。

        从混合密度网络的输出

        图 12-12。混合密度网络的输出

        从 MDN-RNN 中抽样

        我们可以从 MDN 输出中抽样,通过以下过程生成下一个z和下一个时间步的奖励的预测:

        1. 将 481 维输出向量分割为 3 个变量(logpimulogvar)和奖励值。

        2. logpi进行指数化和缩放,以便将其解释为 5 个混合索引上的 32 个概率分布。

        3. 对于z的 32 个维度中的每一个,从由logpi创建的分布中抽样(即选择哪个分布应该用于z的每个维度)。

        4. 获取此分布的相应mulogvar的值。

        5. 从由所选参数mulogvar参数化的正态分布中为z的每个维度抽样一个值。

        MDN-RNN 的损失函数是z向量重构损失和奖励损失的总和。z向量重构损失是 MDN-RNN 预测的分布的负对数似然,给定z的真实值,奖励损失是预测奖励和真实奖励之间的均方误差。

        训练控制器

        最后一步是使用协方差矩阵适应进化策略(CMA-ES)来训练控制器(输出选择的动作的网络)。

        该步骤的超参数如下:

        controller_num_worker

        将以并行方式测试解决方案的工作者数量

        controller_num_worker_trial

        每个工作者在每一代将被给予测试的解决方案数量

        controller_num_episode

        每个解决方案将被测试的情节数量,以计算平均奖励

        controller_eval_steps

        评估当前最佳参数集之间的代数数量

        控制器架构

        控制器的架构非常简单。它是一个没有隐藏层的密集连接神经网络。它将输入向量直接连接到动作向量。

        输入向量是当前z向量(长度 32)和 LSTM 当前隐藏状态(长度 256)的串联,得到长度为 288 的向量。由于我们将每个输入单元直接连接到 3 个输出动作单元,所以要调整的权重总数为 288 × 3 = 864,再加上 3 个偏置权重,总共为 867。

        我们应该如何训练这个网络?请注意,这不是一个监督学习问题——我们不是在尝试预测正确的动作。没有正确动作的训练集,因为我们不知道对于环境的给定状态来说最佳动作是什么。这就是将这个问题区分为强化学习问题的原因。我们需要代理通过在环境中进行实验并根据接收到的反馈更新其权重来发现权重的最佳值。

        进化策略是解决强化学习问题的流行选择,因为它们简单、高效且可扩展。我们将使用一种特定的策略,称为 CMA-ES。

        CMA-ES

        进化策略通常遵循以下过程:

        1. 创建一组代理并随机初始化每个代理要优化的参数。

        2. 循环以下步骤:

          1. 评估环境中的每个代理,返回多个周期的平均奖励。

          2. 繁殖得分最高的代理,以创建种群的新成员。

          3. 为新成员的参数添加随机性。

          4. 通过添加新创建的代理和删除表现不佳的代理来更新种群池。

        这类似于动物在自然界中进化的过程 - 因此称为进化策略。在这种情况下,“繁殖”简单地意味着结合现有的得分最高的代理,使得下一代更有可能产生高质量的结果,类似于它们的父母。与所有强化学习解决方案一样,需要在贪婪地寻找局部最优解和探索参数空间中未知区域以寻找潜在更好解决方案之间找到平衡。这就是为什么向种群中添加随机性很重要,以确保我们的搜索领域不会太狭窄。

        CMA-ES 只是进化策略的一种形式。简而言之,它通过维护一个正态分布来采样新代理的参数。在每一代中,它更新分布的均值以最大化从上一个时间步采样高分代理的可能性。同时,它更新分布的协方差矩阵以最大化在给定先前均值的情况下采样高分代理的可能性。它可以被视为一种自然产生的梯度下降形式,但它的优势在于它是无导数的,这意味着我们不需要计算或估计昂贵的梯度。

        在图 12-13 中展示了算法在一个玩具示例上的一个代的演示。在这里,我们试图找到一个高度非线性函数在二维空间中的最小点 - 图像中红/黑区域的函数值大于图像中白/黄区域的函数值。

        CMA-ES 算法的一个代更新

        图 12-13. CMA-ES 算法的一个更新步骤(来源:Ha, 2017

        步骤如下:

        1. 我们从随机生成的 2D 正态分布开始,并从中采样候选人种群,如图 12-13 中的蓝色所示。

        2. 然后我们计算每个候选者的函数值,并将最佳 25%孤立出来,如图 12-13 中的紫色所示 - 我们将这组点称为P

        3. 我们将新正态分布的均值设置为P中点的均值。这可以被视为繁殖阶段,在这个阶段我们只使用最佳候选者来生成分布的新均值。我们还将新正态分布的协方差矩阵设置为P中点的协方差矩阵,但在协方差计算中使用现有的均值而不是P中点的当前均值。现有均值与P中点的均值之间的差异越大,下一个正态分布的方差就越大。这会自然地在寻找最佳参数的过程中产生动量效应。

        4. 然后我们可以从具有更新均值和协方差矩阵的新正态分布中采样一个新的候选人种群。

        图 12-14 展示了该过程的几代。请看均值向最小值大步移动时协方差如何扩大,但当均值稳定在真实最小值时,协方差如何变窄。

        CMA-ES

        图 12-14. CMA-ES(来源:维基百科)

        对于汽车赛车任务,我们没有一个明确定义的函数来最大化,而是一个环境,其中要优化的 867 个参数决定了代理的得分如何。最初,一些参数集将以随机方式生成比其他参数更高的得分,算法将逐渐将正态分布移向在环境中得分最高的那些参数的方向。

        并行化 CMA-ES

        CMA-ES 的一个巨大优势是它可以很容易地并行化。算法中最耗时的部分是计算给定参数集的得分,因为它需要在环境中模拟具有这些参数的代理。然而,这个过程可以并行化,因为个别模拟之间没有依赖关系。有一个协调器进程,它将要测试的参数集并行发送给许多节点进程。节点将结果返回给协调器,协调器累积结果,然后将该代的整体结果传递给 CMA-ES 对象。该对象根据图 12-13 更新正态分布的均值和协方差矩阵,并为协调器提供一个新的人口进行测试。然后循环重新开始。图 12-15 在图表中解释了这一点。

        并行化 CMA-ES

        图 12-15. 并行化 CMA-ES——这里有一个人口规模为八个和四个节点(因此 t = 2,每个节点负责的试验次数)

        协调器向 CMA-ES 对象(es)请求一组要试验的参数。

        协调器将参数分成可用节点的数量。在这里,每个四个节点进程都会得到两组参数进行试验。

        节点运行一个工作进程,循环遍历每组参数,并为每组参数运行几集。在这里,我们为每组参数运行三集。

        每集剧集的奖励被平均以给出每组参数的单个得分。

        每个节点将其得分列表返回给协调器。

        协调器将所有得分组合在一起,并将此列表发送给es对象。

        es对象使用这个奖励列表来计算新的正态分布,如图 12-13 所示。

        大约经过 200 代,训练过程为汽车赛车任务实现了约 840 的平均奖励分数,如图 12-16 所示。

        并行化 CMA-ES

        图 12-16. 控制器训练过程的平均剧集奖励,按代数(来源:Zac Wellmer,“World Models”

        梦中训练

        到目前为止,控制器训练是使用 Gym 的CarRacing环境来实现将模拟从一个状态移动到下一个状态的步骤方法。该函数根据环境的当前状态和选择的动作计算下一个状态和奖励。

        注意步骤方法在我们模型中执行的功能与 MDN-RNN 非常相似。从 MDN-RNN 中采样输出了下一个z和奖励的预测,给出了当前z和选择的动作。

        事实上,MDN-RNN 可以被视为一个独立的环境,但是在z空间中运行,而不是在原始图像空间中。令人难以置信的是,这意味着我们实际上可以用 MDN-RNN 的副本替换真实环境,并在 MDN-RNN 启发的梦境中完全训练控制器,以模拟环境应该如何行为。

        换句话说,MDN-RNN 已经从原始随机移动数据集中学到了关于真实环境的一般物理知识,因此可以在训练控制器时作为真实环境的代理使用。这是非常了不起的——这意味着代理可以通过思考如何在梦境环境中最大化奖励来训练自己学习新任务,而无需在真实世界中测试策略。然后,它可以在第一次尝试任务时表现良好,而实际上从未尝试过这项任务。

        接下来是在真实环境和梦境中进行训练的架构比较:真实世界架构显示在图 12-17 中,梦境训练设置在图 12-18 中说明。

        图 12-17。在 Gym 环境中训练控制器

        请注意,在梦境架构中,控制器的训练完全在z空间中进行,而无需将z向量解码回可识别的轨道图像。当然,我们可以这样做,以便视觉检查代理的性能,但这并不是训练所必需的。

        图 12-18。在 MDN-RNN 梦境环境中训练控制器

        在 MDN-RNN 梦境环境中完全训练代理的一个挑战是过拟合。当代理在梦境环境中找到一种有益的策略,但在真实环境中泛化能力不强时,就会发生这种情况,这是因为 MDN-RNN 没有完全捕捉到在某些条件下真实环境的行为方式。

        原始论文的作者强调了这一挑战,并展示了如何包含一个温度参数来控制模型的不确定性可以帮助缓解问题。增加这个参数会放大通过 MDN-RNN 对z进行采样时的方差,导致在梦境环境中训练时出现更多波动。控制器对于遇到已知状态的更安全策略会获得更高的奖励,因此往往更容易泛化到真实环境。然而,增加温度需要平衡,以免使环境变得太波动,以至于控制器无法学习任何策略,因为梦境环境在时间上的演变不够一致。

        在原始论文中,作者展示了这种技术成功应用于不同的环境:DoomTakeCover,基于电脑游戏Doom。图 12-19 显示了改变温度参数如何影响虚拟(梦境)得分和真实环境中的实际得分。

        图 12-19。使用温度控制梦境环境波动性(来源:Ha and Schmidhuber, 2018

        在真实环境中,最佳温度设置为 1.15,在发表时超过了当前 Gym 领导者的得分 1,092。这是一个惊人的成就——请记住,控制器从未在真实环境中尝试过这项任务。它只是在真实环境中随机行动(用于训练 VAE 和 MDN-RNN 模型),然后使用梦境环境来训练控制器。

        使用生成世界模型作为强化学习方法的一个关键优势是,在梦境环境中的每一代训练比在真实环境中的训练要快得多。这是因为 MDN-RNN 对z和奖励预测比 Gym 环境中的z和奖励计算更快。

        总结

        在本章中,我们看到了如何在强化学习环境中利用生成模型(VAE)使代理能够通过在自己生成的梦境中测试策略来学习有效策略,而不是在真实环境中进行测试。

        VAE 被训练来学习环境的潜在表示,然后作为输入传递给一个递归神经网络,该网络在潜在空间内预测未来轨迹。令人惊讶的是,代理可以使用这个生成模型作为伪环境,通过演化方法迭代地测试策略,以便在真实环境中得到良好的泛化。

        有关该模型的更多信息,请参阅原始论文作者编写的出色互动解释,可在在线获取。

        ¹ 大卫·哈和尤尔根·施密德胡伯,“世界模型”,2018 年 3 月 27 日,https://arxiv.org/abs/1803.10122

        ² 大卫·哈,“演化策略的视觉指南”,2017 年 10 月 29 日,https://blog.otoro.net/2017/10/29/visual-evolution-strategies

        第十三章:多模态模型

        到目前为止,我们已经分析了专注于单一数据模态的生成学习问题:文本、图像或音乐。我们已经看到了 GAN 和扩散模型如何生成最先进的图像,以及 Transformer 如何引领文本和图像生成的方式。然而,作为人类,我们没有跨模态的困难——例如,描述给定照片中正在发生的事情,创作数字艺术来描绘书中虚构的幻想世界,或将电影配乐与给定场景的情感相匹配。我们能训练机器做同样的事吗?

        介绍

        多模态学习涉及训练生成模型以在两种或更多种不同类型的数据之间进行转换。在过去两年中引入的一些最令人印象深刻的生成模型具有多模态性质。在本章中,我们将详细探讨它们的工作原理,并考虑未来的生成建模将如何受到大型多模态模型的影响。

        我们将探讨四种不同的视觉语言模型:来自 OpenAI 的 DALL.E 2;来自 Google Brain 的 Imagen;来自 Stability AI、CompVis 和 Runway 的 Stable Diffusion;以及来自 DeepMind 的 Flamingo。

        提示

        本章的目的是简明扼要地解释每个模型的工作原理,而不深入探讨每个设计决策的细节。有关更多信息,请参考每个模型的各自论文,其中详细解释了所有设计选择和架构决策。

        文本到图像生成侧重于从给定的文本提示生成最先进的图像。例如,给定输入“用造型粘土制成的一颗西兰花头,在阳光下微笑”,我们希望模型能够输出一个与文本提示精确匹配的图像,如图 13-1 所示。

        这显然是一个极具挑战性的问题。文本理解和图像生成本身就很难解决,正如我们在本书的前几章中所看到的。这样的多模态建模提出了额外的挑战,因为模型还必须学习如何跨越两个领域之间的鸿沟,并学习一个共享表示,使其能够准确地将一段文本转换为高保真图像而不丢失信息。

        图 13-1。DALL.E 2 进行文本到图像生成的示例

        此外,为了取得成功,模型必须能够结合可能从未见过的概念和风格。例如,没有米开朗基罗的壁画中有人们戴着虚拟现实头盔,但我们希望我们的模型能够在我们要求时创建这样的图像。同样,模型准确推断生成图像中的对象如何与彼此相关,基于文本提示。例如,“宇航员骑着甜甜圈穿越太空”的图片应该与“宇航员在拥挤的空间里吃甜甜圈”的图片看起来截然不同。模型必须学习单词如何通过上下文赋予意义,以及如何将实体之间的明确文本关系转换为暗示相同含义的图像。

        DALL.E 2

        我们将要探索的第一个模型是DALL.E 2,这是由 OpenAI 设计用于文本到图像生成的模型。该模型的第一个版本,DALL.E,是在 2021 年 2 月发布的,引发了对生成多模态模型的新一波兴趣。在本节中,我们将调查该模型的第二次迭代,DALL.E 2,于 2022 年 4 月发布,距离第一个版本发布仅一年多一点。

        DALL.E 2 是一个非常令人印象深刻的模型,进一步增进了我们对 AI 解决这类多模态问题能力的理解。它不仅在学术上具有影响力,还迫使我们提出与 AI 在创造性过程中的角色有关的重大问题,这些问题以前被认为是人类独有的。我们将从探索 DALL.E 2 的工作方式开始,建立在本书前面已经探讨过的关键基本思想之上。

        架构

        要理解 DALL.E 2 的工作原理,我们必须首先了解其整体架构,如图 13-2 所示。

        图 13-2. DALL.E 2 架构

        有三个不同的部分需要考虑:文本编码器先验解码器。文本首先通过文本编码器传递,以产生文本嵌入向量。然后,该向量通过先验进行转换,以产生图像嵌入向量。最后,这通过解码器传递,连同原始文本,以生成图像。我们将依次逐个步骤地介绍每个组件,以全面了解 DALL.E 2 在实践中的工作方式。

        文本编码器

        文本编码器的目的是将文本提示转换为表示文本提示概念含义的嵌入向量,该向量位于潜在空间内。正如我们在前几章中所看到的,将离散文本转换为连续潜在空间向量对于所有下游任务都是至关重要的,因为我们可以根据特定目标进一步操纵向量。

        在 DALL.E 2 中,作者并不是从头开始训练文本编码器,而是利用了一个名为对比语言-图像预训练(CLIP)的现有模型,也是由 OpenAI 制作的。因此,要理解文本编码器,我们必须首先了解 CLIP 的工作原理。

        CLIP

        CLIP³是 OpenAI 于 2021 年 2 月发布的一篇论文中公布的(就在第一篇 DALL.E 论文发布几天后),该论文将其描述为“一种能够有效地从自然语言监督中学习视觉概念的神经网络。”

        它使用一种称为对比学习的技术将图像与文本描述进行匹配。该模型在从互联网上抓取的 4 亿个文本-图像对数据集上进行训练——一些示例对显示在图 13-3 中。作为比较,ImageNet 中有 1400 万个手动注释的图像。给定一幅图像和一组可能的文本描述,它的任务是找到实际与图像匹配的描述。

        图 13-3. 文本-图像对的示例

        对比学习背后的关键思想很简单。我们训练两个神经网络:一个文本编码器,将文本转换为文本嵌入,以及一个图像编码器,将图像转换为图像嵌入。然后,给定一批文本-图像对,我们使用余弦相似度比较所有文本和图像嵌入组合,并训练网络,以最大化匹配文本-图像对之间的分数,并最小化不正确的文本-图像对之间的分数。这个过程在图 13-4 中显示。

        CLIP 不是生成模型

        请注意,CLIP 本身不是生成模型——它不能生成图像或文本。它更接近于判别模型,因为最终输出是关于给定图像最接近哪个文本描述(或反之亦然,哪个图像最接近给定文本描述)的预测。

        图 13-4. CLIP 训练过程

        文本编码器和图像编码器都是 Transformer——图像编码器是 Vision Transformer(ViT),在“ViT VQ-GAN”中介绍,它将注意力的相同概念应用于图像。作者测试了其他模型架构,但发现这种组合产生了最好的结果。

        CLIP 特别有趣的地方在于它可以用于对从未接触过的任务进行零样本预测。例如,假设我们想使用 CLIP 来预测 ImageNet 数据集中给定图像的标签。我们可以首先通过使用模板(例如“一张<标签>的照片”)将 ImageNet 标签转换为句子,如图 13-5 所示。

        图 13-5. 将新数据集中的标签转换为标题,以生成 CLIP 文本嵌入

        为了预测给定图像的标签,我们可以通过 CLIP 图像编码器传递图像,并计算图像嵌入与所有可能文本嵌入之间的余弦相似度,以找到得分最高的标签,如图 13-6 所示。

        图 13-6. 使用 CLIP 预测图像内容

        请注意,我们无需重新训练 CLIP 神经网络,即可将其应用于新任务。它使用语言作为一个通用领域,通过它可以表达任何一组标签。

        使用这种方法,可以证明 CLIP 在各种图像数据集标签挑战中表现良好(图 13-7)。其他模型通常在应用于具有相同标签的不同数据集时失败,因为它们高度优化于它们训练的个别数据集。CLIP 更加稳健,因为它学习了对完整文本描述和图像的深刻概念理解,而不仅仅擅长于将单个标签分配给给定数据集中的图像的狭窄任务。

        图 13-7. CLIP 在各种图像标签数据集上表现良好(来源:Radford 等人,2021

        如前所述,CLIP 是根据其区分能力来衡量的,那么它如何帮助我们构建生成模型,如 DALL.E 2 呢?

        答案是,我们可以将训练好的文本编码器作为 DALL.E 2 等更大模型的一部分,冻结权重。训练好的编码器只是一个将文本转换为文本嵌入的通用模型,对于生成图像等下游任务应该是有用的。文本编码器能够捕捉文本的丰富概念理解,因为它经过训练,使其尽可能与其匹配的图像嵌入对应物相似,后者仅由配对图像产生。因此,它是我们需要能够从文本领域跨越到图像领域的桥梁的第一部分。

        先验

        下一阶段的过程涉及将文本嵌入转换为 CLIP 图像嵌入。DALL.E 2 的作者尝试了两种不同的方法来训练先验模型:

        • 自回归模型

        • 扩散模型

        他们发现扩散方法优于自回归模型,并且在计算效率上更高。在本节中,我们将看看两者的区别。

        自回归先验

        自回归模型按顺序生成输出,通过对输出标记(例如单词、像素)进行排序,并将下一个标记的生成条件放在前面的标记上。我们已经在之前的章节中看到了这在循环神经网络(例如 LSTMs)、Transformer 和 PixelCNN 中的应用。

        DALL.E 2 的自回归先验是一个编码器-解码器 Transformer。它经过训练,可以在给定 CLIP 文本嵌入的情况下重现 CLIP 图像嵌入,如图 13-8 所示。请注意,原始论文中提到了一些自回归模型的附加组件,为了简洁起见,我们在这里省略了。

        图 13-8. DALL.E 2 的自回归先验的简化图

        该模型在 CLIP 文本-图像对数据集上进行训练。您可以将其视为我们需要的桥梁的第二部分,以便从文本领域跳转到图像领域:我们正在将一个向量从文本嵌入潜在空间转换为图像嵌入潜在空间。

        输入文本嵌入由 Transformer 的编码器处理,产生另一个表示,传递给解码器,同时传递当前生成的输出图像嵌入。输出是逐个元素生成的,使用教师强制来比较预测的下一个元素与实际的 CLIP 图像嵌入。

        生成的顺序性意味着自回归模型在计算效率上不如作者尝试的其他方法,接下来我们将看一下这些方法。

        扩散先验

        正如我们在第八章中看到的,扩散模型正迅速成为生成建模从业者的首选之一,与 Transformer 并列。在 DALL.E 2 中,一个仅使用解码器的 Transformer 作为先验,通过扩散过程进行训练。

        训练和生成过程如图 13-9 所示。再次强调,这是一个简化版本;原始论文包含了扩散模型结构的所有细节。

        图 13-9。DALL.E 2 扩散先验训练和生成过程的简化图示

        在训练过程中,每个 CLIP 文本和图像嵌入对首先被连接成一个单一向量。然后,图像嵌入在 1,000 个时间步长内被加入噪声,直到它与随机噪声无法区分。然后扩散先验被训练以预测上一个时间步长的去噪图像嵌入。先验在整个过程中都可以访问文本嵌入,因此能够根据这些信息对其预测进行条件化,逐渐将随机噪声转换为预测的 CLIP 图像嵌入。损失函数是去噪步骤中的平均均方误差。

        为了生成新的图像嵌入,我们随机采样一个向量,将相关文本嵌入前置,并通过训练好的扩散先验多次传递。

        解码器

        DALL.E 2 的最后部分是解码器。这是模型的一部分,根据文本提示和先验输出的预测图像嵌入生成最终图像。

        解码器的架构和训练过程借鉴了早前 OpenAI 发表的一篇论文,该论文于 2021 年 12 月发表,介绍了一种名为 Guided Language to Image Diffusion for Generation and Editing (GLIDE)的生成模型。⁴

        GLIDE 能够从文本提示中生成逼真的图像,这与 DALL.E 2 的工作方式非常相似。不同之处在于 GLIDE 不使用 CLIP 嵌入,而是直接使用原始文本提示进行训练,从头开始训练整个模型,如图 13-10 所示。

        图 13-10。DALL.E 2 和 GLIDE 之间的比较—GLIDE 从头开始训练整个生成模型,而 DALL.E 2 利用 CLIP 嵌入将信息从初始文本提示传递下去

        让我们先看看 GLIDE 是如何工作的。

        GLIDE

        GLIDE 作为一个扩散模型进行训练,使用 U-Net 架构作为去噪器,使用 Transformer 架构作为文本编码器。它学会了根据文本提示消除添加到图像中的噪声。最后,一个上采样器被训练以将生成的图像缩放到 1,024×1,024 像素。

        GLIDE 从头开始训练 35 亿(B)参数模型—模型的视觉部分(U-Net 和上采样器)有 23 亿参数,Transformer 有 12 亿参数。它在 2.5 亿文本-图像对上进行训练。

        扩散过程如图 13-11 所示。使用 Transformer 创建输入文本提示的嵌入,然后用于引导 U-Net 进行去噪过程。我们在第八章中探讨了 U-Net 架构;当图像的整体大小应保持不变时(例如,用于风格转移、去噪等),这是一个完美的模型选择。

        图 13-11。GLIDE 扩散过程

        DALL.E 2 解码器仍然使用 U-Net 去噪器和 Transformer 文本编码器架构,但另外还有预测的 CLIP 图像嵌入来进行条件。这是 GLIDE 和 DALL.E 2 之间的关键区别,如图 13-12 所示。

        图 13-12。DALL.E 2 解码器还额外依赖于先验产生的图像嵌入

        与所有扩散模型一样,要生成新图像,我们只需对一些随机噪声进行多次 U-Net 去噪,条件是 Transformer 文本编码和图像嵌入。输出是一个 64×64 像素的图像。

        上采样器

        解码器的最后部分是上采样器(两个单独的扩散模型)。第一个扩散模型将图像从 64×64 转换为 256×256 像素。第二个再次转换,从 256×256 到 1,024×1,024 像素,如图 13-13 所示。

        上采样很有用,因为这意味着我们不必构建处理高维图像的大型上游模型。我们可以在整个过程的最后阶段之前使用小图像,然后应用上采样器。这节省了模型参数,并确保更高效的上游训练过程。

        图 13-13。第一个 Upsampler 扩散模型将图像从 64×64 像素转换为 256×256 像素,而第二个将图像从 256×256 像素转换为 1,024×1,024 像素

        这就是 DALL.E 2 模型的解释!总之,DALL.E 2 利用预训练的 CLIP 模型立即生成输入提示的文本嵌入。然后使用称为先验的扩散模型将其转换为图像嵌入。最后,它实现了一个 GLIDE 风格的扩散模型,以生成输出图像,条件是预测的图像嵌入和 Transformer 编码的输入提示。

        DALL.E 2 的示例

        可以在官方网站上找到 DALL.E 2 生成的更多图像示例。该模型能够以令人惊讶的方式将复杂、不同的概念结合在一起,以一种现实、可信的方式,这代表了 AI 和生成建模的重大进步。

        在论文中,作者展示了该模型可以用于除文本到图像生成之外的其他目的。其中一个应用是创建给定图像的变化,我们将在下一节中探讨。

        图像变化

        如前所述,使用 DALL.E 2 解码器生成图像时,我们对由纯随机噪声组成的图像进行采样,然后逐渐减少噪声量,使用依赖于提供的图像嵌入的去噪扩散模型。选择不同的初始随机噪声样本将导致不同的图像。

        为了生成给定图像的变化,我们只需要建立其图像嵌入以供解码器使用。我们可以使用原始的 CLIP 图像编码器来获得这个,它专门设计用于将图像转换为其 CLIP 图像嵌入。这个过程如图 13-14 所示。

        图 13-14。DALL.E 2 可用于生成给定图像的变化

        先验的重要性

        作者探索的另一条途径是建立先验的重要性。先验的目的是为解码器提供一个有用的图像表示,利用预训练的 CLIP 模型。然而,这一步骤可能是不必要的——也许我们可以直接将文本嵌入传递给解码器,而不是图像嵌入,或者完全忽略 CLIP 嵌入,只根据文本提示进行条件化。这会影响生成的质量吗?

        为了测试这一点,作者尝试了三种不同的方法:

        1. 只将文本提示(以及图像嵌入的零向量)提供给解码器。

        2. 将文本提示和文本嵌入(就像它是图像嵌入一样)提供给解码器。

        3. 将文本提示和图像嵌入(即完整模型)提供给解码器。

        示例结果显示在图 13-15 中。我们可以看到,当解码器缺乏图像嵌入信息时,它只能产生文本提示的粗略近似,缺少关键信息,如计算器。将文本嵌入视为图像嵌入稍微好一些,尽管它无法捕捉刺猬和计算器之间的关系。只有带有先验的完整模型才能产生准确反映提示中包含的所有信息的图像。

        图 13-15。先验为模型提供了额外的上下文,并帮助解码器产生更准确的生成物(来源:Ramesh 等人,2022

        限制

        在 DALL.E 2 论文中,作者还强调了模型的几个已知限制。其中两个(属性绑定和文本生成)显示在图 13-16 中。

        图 13-16。DALL.E 2 的两个限制在于其将属性绑定到对象和再现文本信息的能力——顶部提示:“一个红色立方体放在一个蓝色立方体上”;底部提示:“一个写着深度学习的标志”(来源:Ramesh 等人,2022

        属性绑定是模型理解给定文本提示中单词之间关系的能力,特别是属性如何与对象相关联。例如,提示“一个红色立方体放在一个蓝色立方体上”在视觉上必须与“一个蓝色立方体放在一个红色立方体上”有明显区别。与之前的模型(如 GLIDE)相比,DALL.E 在这方面有些困难,尽管生成的整体质量更好且更多样化。

        此外,DALL.E 2 无法准确再现文本——这可能是因为 CLIP 嵌入不捕捉拼写,而只包含文本的更高级表示。这些表示可以部分成功地解码为文本(例如,单个字母大多正确),但没有足够的组合理解来形成完整的单词。

        Imagen

        在 OpenAI 发布 DALL.E 2 一个多月后,Google Brain 团队发布了他们自己的文本到图像模型称为 Imagen。我们在本章中已经探讨的许多核心主题也与 Imagen 相关:例如,它使用文本编码器和扩散模型解码器。

        在接下来的部分中,我们将探讨 Imagen 的整体架构,并将其与 DALL.E 2 进行比较。

        架构

        Imagen 架构的概述显示在图 13-17 中。

        图 13-17。Imagen 架构(来源:Saharia 等人,2022

        冻结文本编码器是预训练的 T5-XXL 模型,一个大型编码器-解码器 Transformer。与 CLIP 不同,这个模型仅在文本上进行训练,而不是图像,因此它不是一个多模态模型。然而,作者发现它仍然在 Imagen 中作为文本编码器表现非常出色,并且扩展这个模型对整体性能的影响比扩展扩散模型解码器更大。

        与 DALL.E 2 一样,Imagen 的解码扩散模型基于 U-Net 架构,以文本嵌入为条件。对标准 U-Net 架构进行了几项架构改进,以产生作者称之为高效 U-Net的模型。该模型使用更少的内存,收敛更快,并且比以前的 U-Net 模型具有更好的样本质量。

        将生成的图像从 64×64 像素升级到 1,024×1,024 像素的上采样器超分辨率模型也是扩散模型,继续使用文本嵌入来指导上采样过程。

        DrawBench

        Imagen 论文的另一个贡献是DrawBench——一个包含 200 个文本提示的套件,用于文本到图像的评估。文本提示涵盖 11 个类别,如计数(生成指定数量的对象的能力)、描述(生成描述对象的复杂和长文本提示的能力)和文本(生成引用文本的能力)。为了比较两个模型,DrawBench 文本提示通过每个模型,并将输出交给一组人类评分员进行评估,评估涵盖两个指标:

        对齐

        哪张图更准确地描述了标题?

        保真度

        哪张图更逼真(看起来更真实)?

        DrawBench 人类评估的结果显示在图 13-18 中。

        DALL.E 2 和 Imagen 都是在文本到图像生成领域做出了重大贡献的显著模型。虽然 Imagen 在许多 DrawBench 基准测试中表现优于 DALL.E 2,但 DALL.E 2 提供了 Imagen 中没有的额外功能。例如,因为 DALL.E 2 利用了 CLIP(一个多模态文本-图像模型),它能够接受图像作为输入来生成图像嵌入。这意味着 DALL.E 2 能够提供图像编辑和图像变化的功能。这在 Imagen 中是不可能的;文本编码器是一个纯文本模型,因此无法输入图像。

        图 13-18. 在对齐和图像保真度方面比较 Imagen 和 DALL.E 2(来源:Saharia 等人,2022

        Imagen 的示例

        示例 Imagen 生成显示在图 13-19 中。

        图 13-19. 示例 Imagen 生成(来源:Saharia 等人,2022

        稳定扩散

        我们将探讨的最后一个文本到图像扩散模型是稳定扩散,由Stability AI于 2022 年 8 月发布,与慕尼黑路德维希·马克西米利安大学计算机视觉与学习研究小组Runway合作。它与 DALL.E 2 和 Imagen 不同,因为它的代码和模型权重已经通过Hugging Face公开发布。这意味着任何人都可以在自己的硬件上与模型互动,而无需使用专有 API。

        架构

        稳定扩散和之前讨论的文本到图像模型之间的主要架构差异在于它使用潜在扩散作为其基础生成模型。潜在扩散模型(LDMs)是由 Rombach 等人在 2021 年 12 月提出的,在论文“使用潜在扩散模型进行高分辨率图像合成”中。⁶ 该论文的关键思想是将扩散模型包装在一个自动编码器中,使得扩散过程在图像的潜在空间表示上运行,而不是在图像本身上运行,如图 13-20 所示。

        图 13-20. 稳定扩散架构

        这一突破意味着去噪 U-Net 模型相对轻量化,与操作完整图像的 U-Net 模型相比。自动编码器处理将图像细节编码到潜在空间并将潜在空间解码回高分辨率图像的繁重工作,使扩散模型纯粹在潜在的概念空间中工作。这为训练过程带来了显著的速度和性能提升。

        去噪过程也可以选择由通过文本编码器传递的文本提示引导。稳定扩散的第一个版本使用了 OpenAI 的预训练 CLIP 模型(与 DALL.E 2 中相同),但稳定扩散 2 使用了一个名为OpenCLIP的自定义训练的 CLIP 模型,该模型是从头开始训练的。

        稳定扩散的示例

        图 13-21 展示了稳定扩散 2.1 的一些示例输出—您可以通过Hugging Face上托管的模型尝试自己的提示。

        图 13-21. 稳定扩散 2.1 的示例输出

        探索潜在空间

        如果您想探索稳定扩散模型的潜在空间,我强烈推荐在 Keras 网站上进行的演练

        Flamingo

        到目前为止,我们已经看过三种不同类型的文本到图像模型。在本节中,我们将探索一种多模态模型,它可以根据文本和视觉数据流生成文本。Flamingo 是 DeepMind 在 2022 年 4 月发表的一篇论文中介绍的,⁷是一系列视觉语言模型(VLMs),作为预训练的仅视觉和仅语言模型之间的桥梁。

        在这一部分,我们将介绍 Flamingo 模型的架构,并将其与我们迄今为止看到的文本到图像模型进行比较。

        架构

        Flamingo 的整体架构显示在图 13-22 中。为了简洁起见,我们将仅探讨该模型的核心组件—视觉编码器、感知器重采样器和语言模式—以足够的细节来突出使 Flamingo 独特的关键思想。我强烈建议阅读原始研究论文,对模型的每个部分进行彻底审查。

        图 13-22. Flamingo 架构(来源:Alayrac 等人,2022

        视觉编码器

        Flamingo 模型与纯文本到图像模型(如 DALL.E 2 和 Imagen)之间的第一个区别是,Flamingo 可以接受交错的文本和视觉数据的组合。这里,视觉数据包括视频和图像。

        视觉编码器的工作是将输入中的视觉数据转换为嵌入向量(类似于 CLIP 中的图像编码器)。Flamingo 中的视觉编码器是一个预训练的无归一化 ResNet(NFNet),由 Brock 等人在 2021 年介绍⁸—具体来说,是一个 NFNet-F6(NFNet 模型从 F0 到 F6,大小和功率逐渐增加)。这是 CLIP 图像编码器和 Flamingo 视觉编码器之间的一个关键区别:前者使用 ViT 架构,而后者使用 ResNet 架构。

        视觉编码器是使用与 CLIP 论文中引入的对比目标相同的图像-文本对进行训练的。训练后,权重被冻结,以便对 Flamingo 模型的任何进一步训练不会影响视觉编码器的权重。

        视觉编码器的输出是一个特征的 2D 网格,然后在传递给 Perceiver Resampler 之前被展平为 1D 向量。视频通过每秒采样 1 帧处理,并将每个快照独立通过视觉编码器传递以产生几个特征网格;然后在展平特征之前添加了学习的时间编码,并将结果连接成一个单一向量。

        Perceiver Resampler

        传统编码器 Transformer(例如 BERT)中的内存需求随着输入序列长度呈二次方增长,这就是为什么输入序列通常被限制在一定数量的标记上(例如 BERT 中的 512 个)。然而,视觉编码器的输出是一个长度可变的向量(由于可变的输入图像分辨率和可变的视频帧数),因此可能非常长。

        Perceiver 架构专门设计用于高效处理长输入序列。它不是对整个输入序列执行自注意力,而是使用固定长度的潜在向量,并仅将输入序列用于交叉注意力。具体来说,在 Flamingo Perceiver Resampler 中,keyvalue是输入序列和潜在向量的串联,而query仅是潜在向量本身。图 13-23 显示了视频数据的视觉编码器和 Perceiver Resampler 过程的图示。

        图 13-23。应用于视频输入的 Perceiver Resampler(来源:Alayrac 等人,2022

        Perceiver Resampler 的输出是一个固定长度的潜在向量,传递给语言模型。

        语言模型

        语言模型由几个堆叠的块组成,以解码器 Transformer 的风格输出预测的文本延续。事实上,语言模型的大部分来自一个名为Chinchilla的预训练 DeepMind 模型。2022 年 3 月发表的 Chinchilla 论文⁹展示了一个设计得比同行要小得多的语言模型(例如,Chinchilla 的参数为 70B,而 GPT-3 的参数为 170B),同时在训练中使用了更多标记。作者表明,该模型在一系列任务上优于更大的模型,突出了在训练更大的模型和在训练期间使用更多标记之间优化权衡的重要性。

        Flamingo 论文的一个关键贡献是展示了 Chinchilla 如何适应与插入语言数据(Y)一起工作的额外视觉数据(X)。让我们首先探讨语言和视觉输入是如何结合以产生语言模型的输入的(图 13-24)。

        首先,文本通过用<image>标记替换视觉数据(例如图像),并使用<EOC>(块结束)标记将文本分成。每个块最多包含一个图像,该图像始终位于块的开头,即假定后续文本仅与该图像相关。序列的开头也用<BOS>(句子开头)标记。

        接下来,序列被标记化,每个标记被赋予一个索引(phi),对应于前面的图像索引(如果在块中没有前置图像,则为0)。这样,文本标记(Y)可以被强制只与对应于其特定块的图像标记(X)进行交叉关注,通过掩蔽。例如,在图 13-24 中,第一个块不包含图像,因此 Perceiver Resampler 的所有图像标记都被掩盖。第二个块包含图像 1,因此这些标记可以与图像 1 的图像标记进行交互。同样,最后一个块包含图像 2,因此这些标记可以与图像 2 的图像标记进行交互。

        图 13-24。掩蔽的交叉关注(XATTN),结合视觉和文本数据——浅蓝色条目被掩盖,深蓝色条目未被掩盖(来源:Alayrac 等人,2022)

        现在我们可以看到这个掩蔽的交叉关注组件如何融入语言模型的整体架构中(图 13-25)。

        蓝色的 LM 层组件是来自 Chinchilla 的冻结层,这些层在训练过程中不会更新。紫色的GATED XATTN-DENSE层作为 Flamingo 的一部分进行训练,包括混合语言和视觉信息的掩蔽交叉关注组件,以及随后的前馈(密集)层。

        该层是门控的,因为它通过两个不同的 tanh 门传递来自交叉关注和前馈组件的输出,这两个门都初始化为零。因此,当网络初始化时,GATED XATTN-DENSE层没有贡献——语言信息直接通过。alpha门控参数由网络学习,随着训练的进行逐渐混合视觉数据的信息。

        图 13-25。Flamingo 语言模型块,包括来自 Chinchilla 的冻结语言模型层和一个GATED XATTN-DENSE层(来源:Alayrac 等人,2022

        来自 Flamingo 的例子

        Flamingo 可以用于各种目的,包括图像和视频理解,对话提示和视觉对话。在图 13-26 中,我们可以看到 Flamingo 的一些示例。

        图 13-26。从 80B 参数 Flamingo 模型获得的输入和输出示例(来源:Alayrac 等人,2022

        请注意,在每个示例中,Flamingo 以真正的多模式风格混合文本和图像信息。第一个示例使用图像代替文字,并能够建议一个适当的书籍来继续提示。第二个示例展示了视频中的帧,Flamingo 正确地识别了行动的后果。最后三个示例都展示了 Flamingo 如何交互使用,通过对话提供额外信息或通过进一步提问进行探究。

        看到一台机器能够回答如此广泛的模态和输入任务范围内的复杂问题,真是令人惊讶。在论文中,作者们量化了 Flamingo 在一组基准任务上的能力,并发现在许多基准测试中,Flamingo 能够超越专门针对特定任务的模型的性能。这突显了大型多模型可以迅速适应各种任务,并为开发不仅仅局限于单一任务的 AI 代理铺平了道路,而是真正可以在推理时由用户引导的通用代理。

        总结

        在本章中,我们探讨了四种不同的最先进多模型:DALL.E 2,Imagen,Stable Diffusion 和 Flamingo。

        DALL.E 2 是来自 OpenAI 的大规模文本到图像模型,可以根据文本提示生成各种风格的逼真图像。它通过将预训练模型(例如 CLIP)与先前作品(GLIDE)中的扩散模型架构相结合来工作。它还具有额外的功能,例如能够通过文本提示编辑图像并提供给定图像的变体。尽管它存在一些限制,例如不一致的文本渲染和属性绑定,但 DALL.E 2 是一个非常强大的 AI 模型,已经帮助推动生成建模领域进入一个新时代。

        另一个超越先前基准的模型是 Google Brain 的 Imagen。这个模型与 DALL.E 2 有许多相似之处,例如文本编码器和扩散模型解码器。两个模型之间的一个关键区别是 Imagen 文本编码器是在纯文本数据上训练的,而 DALL.E 2 文本编码器的训练过程涉及图像数据(通过对比 CLIP 学习目标)。作者表明,这种方法在各种任务中取得了最先进的性能,通过他们的 DrawBench 评估套件。

        稳定扩散是来自 Stability AI、CompVis 和 Runway 的开源产品。这是一个文本到图像的模型,其模型权重和代码都是免费提供的,因此您可以在自己的硬件上运行它。稳定扩散特别快速和轻量,因为它使用了一个在自动编码器的潜在空间上运行的潜在扩散模型,而不是图像本身。

        最后,DeepMind 的 Flamingo 是一种视觉语言模型,即它接受交错的文本和视觉数据流(图像和视频),并能够继续通过附加文本提示的方式进行文本输出,类似解码器 Transformer 的风格。其关键贡献在于展示了如何通过视觉编码器和感知器重采样器将视觉信息输入到 Transformer 中,将视觉输入特征编码为少量的视觉标记。语言模型本身是 DeepMind 早期 Chinchilla 模型的扩展,经过调整以融入视觉信息。

        所有这四个都是多模态模型强大性能的显著例子。未来,生成建模很可能会变得更加多模态化,AI 模型将能够通过交互式语言提示轻松跨越模态和任务。

        阿迪蒂亚·拉梅什等人,“零样本文本到图像生成”,2021 年 2 月 24 日,https://arxiv.org/abs/2102.12092。

        阿迪蒂亚·拉梅什等人,“具有 CLIP 潜在特征的分层文本条件图像生成”,2022 年 4 月 13 日,https://arxiv.org/abs/2204.06125。

        亚历克斯·拉德福德等人,“从自然语言监督中学习可转移的视觉模型”,2021 年 2 月 26 日,https://arxiv.org/abs/2103.00020。

        亚历克斯·尼科尔等人,“GLIDE: 朝向逼真图像生成和编辑的文本引导扩散模型”,2021 年 12 月 20 日,https://arxiv.org/abs/2112.10741。

        奇特万·萨哈里亚等人,“具有深度语言理解的逼真文本到图像扩散模型”,2022 年 5 月 23 日,https://arxiv.org/abs/2205.11487。

        罗宾·隆巴赫等人,“使用潜在扩散模型进行高分辨率图像合成”,2021 年 12 月 20 日,https://arxiv.org/abs/2112.10752。

        让-巴蒂斯特·阿拉拉克等人,“Flamingo: 一种用于少样本学习的视觉语言模型”,2022 年 4 月 29 日,https://arxiv.org/abs/2204.14198。

        ⁸ Andrew Brock 等人,“无归一化的高性能大规模图像识别”,2021 年 2 月 11 日,https://arxiv.org/abs/2102.06171

        ⁹ Jordan Hoffmann 等人,“训练计算优化的大型语言模型”,2022 年 3 月 29 日,https://arxiv.org/abs/2203.15556v1

        第十四章:结论

        2018 年 5 月,我开始着手第一版这本书的工作。五年后,我对生成 AI 的无限可能性和潜在影响感到比以往任何时候都更加兴奋。

        在这段时间里,我们看到了这个领域的惊人进步,对真实世界应用有着看似无限的潜力。我对我们迄今为止所取得的成就感到敬畏和惊叹,并迫不及待地期待着生成 AI 未来几年将对世界产生的影响。生成深度学习有能力以我们无法想象的方式塑造未来。

        此外,随着我为这本书研究内容,我越来越清楚地意识到这个领域不仅仅是关于创建图像、文本或音乐。我相信生成深度学习的核心是智能本身的秘密。

        本章的第一部分总结了我们在生成 AI 之旅中达到这一点的过程。我们将按时间顺序浏览自 2014 年以来的生成 AI 发展时间轴,以便您可以看到每种技术在生成 AI 历史中的位置。第二部分解释了我们目前在最先进的生成 AI 方面的位置。我们将讨论生成深度学习方法的当前趋势以及普通公众可以使用的当前现成模型。接下来,我们将探讨生成 AI 的未来以及前方的机遇和挑战。我们将考虑未来五年生成 AI 可能会是什么样子,以及它对社会和商业的潜在影响,并解决一些主要的伦理和实际问题。

        生成 AI 时间轴

        图 14-1 是我们在本书中一起探索的生成建模关键发展的时间轴。颜色代表不同的模型类型。

        生成 AI 领域建立在深度学习早期发展的基础上,比如反向传播和卷积神经网络,这些技术解锁了模型在大规模数据集上学习复杂关系的可能性。在本节中,我们将研究生成 AI 的现代历史,从 2014 年开始,这一历史发展速度惊人。

        为了帮助我们理解所有内容如何相互关联,我们可以大致将这段历史分为三个主要时代:

        1. 2014 年至 2017 年:VAE 和 GAN 时代

        2. 2018 年至 2019 年:Transformer 时代

        3. 2020 年至 2022 年:大模型时代

        图 14-1。从 2014 年到 2023 年的生成 AI 简史(注意:一些重要的发展,如 LSTM 和早期基于能量的模型[例如,玻尔兹曼机]在这个时间轴之前)

        2014 年至 2017 年:VAE 和 GAN 时代

        VAE 的发明可以说是点燃生成 AI 火药桶的火花。这篇论文展示了不仅可以生成简单的图像,如 MNIST 数字,还可以生成更复杂的图像,如面孔,而且可以在一个可以平滑遍历的潜在空间中生成。2014 年,GAN 的引入紧随其后,这是一种全新的对抗性框架,用于解决生成建模问题。

        接下来的三年被逐渐更令人印象深刻的 GAN 系列扩展所主导。除了对 GAN 模型架构(DCGAN,2015)、损失函数(Wasserstein GAN,2017)和训练过程(ProGAN,2017)的基本改变外,还使用 GAN 处理了新的领域,如图像到图像的转换(pix2pix,2016,和 CycleGAN,2017)和音乐生成(MuseGAN,2017)。

        在这个时代,还引入了重要的 VAE 改进,如 VAE-GAN(2015)和后来的 VQ-VAE(2017),并且在“世界模型”论文中看到了对强化学习的应用。

        在这段时间内,已建立的自回归模型,如 LSTMs 和 GRUs,仍然是文本生成的主导力量。相同的自回归思想也被用于生成图像,PixelRNN(2016 年)和 PixelCNN(2016 年)被引入作为思考图像生成的新方法。还在测试其他图像生成方法,例如 RealNVP 模型(2016 年),为后来的各种归一化流模型铺平了道路。

        在 2017 年 6 月,一篇开创性的论文《注意力就是一切》发表,开启了以 Transformer 为中心的生成 AI 的下一个时代。

        2018 年至 2019 年:Transformer 时代

        Transformer 的核心是注意力机制,它消除了旧的自回归模型(如 LSTMs)中存在的循环层的需求。Transformer 随着 2018 年 GPT(仅解码器 Transformer)和 BERT(仅编码器 Transformer)的推出迅速崭露头角。接下来的一年,逐渐建立了更大的语言模型,通过将它们视为纯文本到文本生成问题,擅长各种任务,其中 GPT-2(2018 年,15 亿参数)和 T5(2019 年,110 亿参数)是杰出的例子。

        Transformer 也开始成功应用于音乐生成,例如 Music Transformer(2018 年)和 MuseNet(2019 年)模型的引入。

        在这两年里,也发布了几个令人印象深刻的 GAN,巩固了该技术作为图像生成的最先进方法的地位。特别是,SAGAN(2018 年)和更大的 BigGAN(2018 年)将注意力机制与 GAN 框架结合起来,取得了令人难以置信的结果,而 StyleGAN(2018 年)和后来的 StyleGAN2(2019 年)展示了如何以惊人的细粒度控制生成图像的风格和内容。

        另一个正在积聚动力的生成 AI 领域是基于分数的模型(NCSN,2019 年),最终为生成 AI 领域的下一个重大变革——扩散模型铺平了道路。

        2020 年至 2022 年:大模型时代

        这个时代见证了几个模型的推出,这些模型融合了不同生成建模家族的思想,并加速了现有架构。例如,VQ-GAN(2020 年)将 GAN 鉴别器引入 VQ-VAE 架构,Vision Transformer(2020 年)展示了如何训练 Transformer 在图像上运行的可能性。2022 年发布了 StyleGAN-XL,这是对 StyleGAN 架构的进一步更新,可以生成 1024×1024 像素的图像。

        2020 年推出了两个模型,为所有未来大型图像生成模型奠定了基础:DDPM 和 DDIM。突然之间,扩散模型在图像生成质量方面成为 GAN 的竞争对手,正如 2021 年的论文标题“扩散模型在图像合成方面击败了 GAN”所明确说明的那样。扩散模型的图像质量令人难以置信地好,它们只需要训练一个单一的 U-Net 网络,而不是 GAN 的双网络设置,使训练过程更加稳定。

        大约在同一时间,GPT-3(2020 年)发布了——这是一个庞大的 1750 亿参数的 Transformer,可以以一种几乎难以理解的方式生成几乎任何主题的文本。该模型通过一个网络应用程序和 API 发布,允许公司在其基础上构建产品和服务。ChatGPT(2022 年)是一个围绕 OpenAI 最新版本的 GPT 的网络应用程序和 API 封装器,允许用户与 AI 就任何主题进行自然对话。

        在 2021 年和 2022 年,一大批其他大型语言模型相继发布,以与 GPT-3 竞争,包括微软和英伟达的 Megatron-Turing NLG(2021 年),DeepMind 的 Gopher(2021 年)和 Chinchilla(2022 年),谷歌的 LaMDA(2022 年)和 PaLM(2022 年),以及 Aleph Alpha 的 Luminous(2022 年)。还发布了一些开源模型,如 EleutherAI 的 GPT-Neo(2021 年),GPT-J(2021 年)和 GPT-NeoX(2022 年);Meta 的 66B 参数 OPT 模型(2022 年);谷歌的 Fine-tuned Flan-T5 模型(2022 年);Hugging Face 的 BLOOM(2022 年)等等。这些模型都是 Transformer 的变体,训练在大量数据语料库上。

        强大的 Transformer 用于文本生成和最先进的扩散模型用于图像生成的迅速崛起意味着过去两年生成 AI 发展的重点大部分集中在多模态模型上,即在超过一个领域(例如文本到图像模型)上运行的模型。

        这一趋势始于 2021 年,当 OpenAI 发布了 DALL.E,这是一个基于离散 VAE(类似于 VQ-VAE)和 CLIP(一种预测图像/文本对的 Transformer 模型)的文本到图像模型。随后是 GLIDE(2021 年)和 DALL.E 2(2022 年),更新了模型的生成部分,使用扩散模型而不是离散 VAE,取得了真正令人印象深刻的结果。这一时代还见证了谷歌发布的三个文本到图像模型:Imagen(2022 年,使用 Transformer 和扩散模型),Parti(2022 年,使用 Transformers 和 ViT-VQGAN 模型),以及后来的 MUSE(2023 年,使用 Transformers 和 VQ-GANs)。DeepMind 也发布了 Flamingo(2022 年),这是一个视觉语言模型,建立在他们的大型语言模型 Chinchilla 的基础上,允许图像作为提示数据的一部分。

        2021 年引入的另一个重要扩散进展是潜在扩散,其中扩散模型在自动编码器的潜在空间内进行训练。这一技术推动了 Stable Diffusion 模型的诞生,该模型由 Stability AI、CompVis 和 Runway 在 2022 年联合合作发布。与 DALL.E 2、Imagen 和 Flamingo 不同,Stable Diffusion 的代码和模型权重是开源的,这意味着任何人都可以在自己的硬件上运行该模型。

        生成 AI 的当前状态

        当我们结束对生成 AI 历史的探索时,现在重要的是反思我们在当前最先进应用和模型方面的立足点。让我们花一点时间评估我们在这一领域迄今取得的进展和关键成就。

        大型语言模型

        现在,文本生成的生成 AI 几乎完全集中在构建大型语言模型(LLMs)上,它们的唯一目的是直接从大量文本语料库中建模语言,即它们被训练来预测下一个词,以解码器 Transformer 的风格。

        大型语言模型方法被广泛采用,因为它具有灵活性和在各种任务上表现出色的能力。同一模型可以用于问答、文本摘要、内容创作等多种示例,因为最终每个用例都可以被构建为一个文本到文本问题,其中特定任务指令(提示)作为模型输入的一部分给出。

        让我们以GPT-3为例。图 14-2 展示了同一模型如何用于文本摘要和内容创作。

        图 14-2。来自 GPT-3 的输出——未突出显示的文本是提示,绿色突出显示的文本是 GPT-3 的输出

        请注意,在这两种情况下,提示包含相关的指令。GPT-3 的任务只是逐个标记地继续提示。它没有一个可以查找信息的事实数据库,也没有可以复制到答案中的文本片段。它只被要求预测接下来最有可能跟随现有标记的标记,然后将这个预测附加到提示中以生成下一个标记,依此类推。

        令人难以置信的是,这种简单的设计足以使语言模型在各种任务中表现出色,如图 14-2 所示。此外,它赋予了语言模型令人难以置信的灵活性,可以根据任何提示生成逼真的文本作为回应——想象力通常是限制因素!

        图 14-3 显示自 2018 年原始 GPT 模型发布以来,大型语言模型的规模如何增长。参数数量呈指数增长,直到 2021 年底,Megatron-Turing NLG 达到 5300 亿参数。最近,更多的重点放在构建更高效的语言模型上,这些模型使用更少的参数,因为更大的模型在生产环境中更昂贵且速度较慢。

        图 14-3. 大型语言模型(橙色)和多模型(粉色)的参数数量随时间变化

        许多人仍认为 OpenAI 的 GPT 系列(GPT-3、GPT-3.5、GPT-4 等)是目前个人和商业使用中最强大的最新语言模型套件。它们可以通过网络应用API使用。

        大型语言模型家族的另一个最新成员是 Meta 推出的大型语言模型 Meta AI(LLaMA),¹,这是一套从 7B 到 65B 参数大小的模型系列,纯粹基于公开可用的数据集进行训练。

        今天存在的一些最强大的 LLM 的摘要显示在表 14-1 中。有些模型,如 LLaMA,是不同规模模型的系列—在这种情况下,最大模型的规模显示在这里。一些模型的预训练权重是完全开源的,这意味着任何人都可以免费使用和构建。

        表 14-1. 大型语言模型

        模型 日期 开发者 # 参数 开源
        GPT-3 2020 年 5 月 OpenAI 1750 亿
        GPT-Neo 2021 年 3 月 EleutherAI 27 亿
        GPT-J 2021 年 6 月 EleutherAI 60 亿
        Megatron-Turing NLG 2021 年 10 月 微软和英伟达 5300 亿
        Gopher 2021 年 12 月 DeepMind 2800 亿
        LaMDA 2022 年 1 月 谷歌 1370 亿
        GPT-NeoX 2022 年 2 月 EleutherAI 200 亿
        Chinchilla 2022 年 3 月 DeepMind 700 亿
        PaLM 2022 年 4 月 谷歌 5400 亿
        Luminous 2022 年 4 月 Aleph Alpha 700 亿
        OPT 2022 年 5 月 Meta 1750 亿 是(660 亿)
        BLOOM 2022 年 7 月 Hugging Face 合作 1750 亿
        Flan-T5 2022 年 10 月 谷歌 110 亿
        GPT-3.5 2022 年 11 月 OpenAI 未知
        LLaMA 2023 年 2 月 Meta 650 亿
        GPT-4 2023 年 3 月 OpenAI 未知

        尽管大型语言模型有令人印象深刻的应用,但仍然存在重大挑战需要克服。最值得注意的是,它们容易虚构事实,无法可靠地应用逻辑思维过程,如图 14-4 所示。

        图 14-4. 虽然大型语言模型在某些任务上表现出色,但也容易出现与事实或逻辑推理相关的错误(显示了 GPT-3 的输出)

        重要的是要记住,LLMs 只是被训练来预测下一个单词。它们与现实没有其他联系,无法可靠地识别事实或逻辑谬误。因此,在生产中使用这些强大的文本预测模型时,我们必须非常谨慎——它们尚不能可靠地用于需要精确推理的任何事情。

        文本到代码模型

        大型语言模型的另一个应用是代码生成。2021 年 7 月,OpenAI 推出了一个名为 Codex 的模型,这是一个在 GitHub 上的代码上进行了微调的 GPT 语言模型。该模型能够成功地为一系列问题编写新颖的编码解决方案,只需根据要解决的问题的评论或函数名称进行提示。这项技术如今驱动着 GitHub Copilot,这是一个可以在您输入时实时建议代码的 AI 对编程师。Copilot 是一个基于订阅的付费服务,提供免费试用期。

        图 14-5 显示了两个自动生成的完成示例。第一个示例是一个从给定用户那里获取推文的函数,使用 Twitter API。给定函数名称和参数,Copilot 能够自动完成函数定义的其余部分。第二个示例要求 Copilot 解析一组费用,还包括在 docstring 中包含一个自由文本描述,解释输入参数的格式以及与任务相关的具体说明。Copilot 能够仅通过描述自动完成整个函数。

        这项引人注目的技术已经开始改变程序员处理特定任务的方式。程序员通常会花费相当大的时间搜索现有解决方案的示例,阅读社区问答论坛,如 Stack Overflow,并查阅包文档中的语法。这意味着离开交互式开发环境(IDE),切换到 Web 浏览器,并从 Web 上复制和粘贴代码片段,以查看它们是否解决了您的特定问题。在许多情况下,Copilot 消除了这样做的必要性,因为您只需在 IDE 中写下您希望实现的简要描述后,就可以通过 AI 生成的潜在解决方案进行选项卡切换。

        图 14-5. GitHub Copilot 功能的两个示例(来源:GitHub Copilot)

        文本到图像模型

        目前,最先进的图像生成主要由将给定文本提示转换为图像的大型多模态模型主导。文本到图像模型非常有用,因为它们允许用户通过自然语言轻松地操纵生成的图像。这与诸如 StyleGAN 之类的模型形成对比,后者虽然非常令人印象深刻,但没有通过您可以描述要生成的图像的文本界面。

        目前可供商业和个人使用的三个重要的文本到图像生成模型是 DALL.E 2、Midjourney 和 Stable Diffusion。

        OpenAI 的 DALL.E 2 是一项按需付费服务,可通过 Web 应用程序和 API 获得。Midjourney 通过其 Discord 频道提供基于订阅的文本到图像服务。DALL.E 2 和 Midjourney 都为那些加入平台进行早期实验的用户提供免费积分。

        Midjourney

        Midjourney 是用于本书第 II 部分故事插图的服务!

        Stable Diffusion 不同,因为它是完全开源的。用于训练模型的模型权重和代码都可以在 GitHub 上找到,因此任何人都可以在自己的硬件上运行该模型。用于训练 Stable Diffusion 的数据集也是开源的。这个名为 LAION-5B 的数据集包含了 58.5 亿个图像文本对,目前是世界上最大的公开可访问的图像文本数据集。

        这种方法的一个重要推论是,基线稳定扩散模型可以被构建并适应不同的用例。ControlNet 就是这一点的一个很好的演示,它是一种神经网络结构,允许通过添加额外条件对稳定扩散的输出进行细粒度控制。例如,输出图像可以根据给定输入图像的Canny 边缘图进行条件化,如图 14-6 所示。

        图 14-6。使用 Canny 边缘图和 ControlNet 对稳定扩散输出进行条件化(来源:Lvmin Zhang, ControlNet

        ControlNet 包含一个可训练的稳定扩散编码器副本,以及一个完整的稳定扩散模型的锁定副本。这个可训练的编码器的任务是学习如何处理输入条件(例如,Canny 边缘图),而锁定副本保留了原始模型的功能。这样,稳定扩散可以仅使用少量图像对进行微调。零卷积简单地是所有权重和偏置都为零的 1×1 卷积,因此在训练之前,ControlNet 没有任何效果。

        图 14-7。ControlNet 架构,可训练的稳定扩散编码器块用蓝色突出显示(来源:Lvmin Zhang, ControlNet

        稳定扩散的另一个优点是,它能够在仅具有 8 GB VRAM 的单个中等大小 GPU 上运行,这使得它可以在边缘设备上运行,而不是通过调用云服务。随着文本到图像服务包含在下游产品中,生成速度变得越来越重要。这也是为什么多模态模型的大小通常趋向于减小的原因之一(参见图 14-3)。

        三种模型的示例输出可以在图 14-8 中看到。所有这些模型都非常出色,能够捕捉给定描述的内容和风格。

        图 14-8。稳定扩散 v2.1、Midjourney 和 DALL.E 2 对相同提示的输出

        今天存在的一些最强大的文本到图像模型的摘要显示在表 14-2 中。

        表 14-2。文本到图像模型

        模型 日期 开发者 # 参数 开源
        DALL.E 2 2022 年 4 月 OpenAI 35 亿
        Imagen 2022 年 5 月 谷歌 46 亿
        Parti 2022 年 6 月 谷歌 200 亿
        稳定扩散 2022 年 8 月 Stability AI、CompVis 和 Runway 8.9 亿
        MUSE 2023 年 1 月 谷歌 30 亿

        使用文本到图像模型的技巧之一是创建一个提示,既描述您想要生成的图像的内容,又使用鼓励模型生成特定风格或类型图像的关键词。例如,诸如令人惊叹获奖之类的形容词通常可以用来提高生成的质量。然而,并不总是同一个提示在不同模型上都能很好地工作——这取决于用于训练模型的特定文本-图像数据集的内容。发现适合特定模型的提示的艺术被称为提示工程

        其他应用

        生成式人工智能正在迅速在各种新领域中找到应用,从强化学习到其他种类的文本到 X多模态模型。

        例如,2022 年 11 月,Meta 发表了一篇关于 CICERO 的论文,这是一个训练有素的 AI 代理人,用于玩《外交》这个棋盘游戏。在这个游戏中,玩家代表第一次世界大战前欧洲的不同国家,必须与彼此进行谈判和欺骗,以控制整个大陆。对于 AI 代理人来说,这是一个非常复杂的游戏,因为其中有一个沟通元素,玩家必须与其他玩家讨论他们的计划,以获得盟友、协调行动并提出战略目标。为了实现这一点,CICERO 包含一个能够发起对话并回应其他玩家消息的语言模型。至关重要的是,对话与代理人的战略计划一致,这些计划由模型的另一部分生成,以适应不断变化的情景。这包括代理人在与其他玩家交谈时虚张声势,即说服另一个玩家与代理人合作,然后在后续回合中对该玩家采取激进的行动。值得注意的是,在一个匿名的外交联盟中,涉及 40 场比赛,CICERO 的得分超过了人类玩家的平均水平的两倍以上,并且在参与多场比赛的参与者中排名前 10%。这是一个很好的例子,展示了生成式 AI 如何成功地与强化学习相结合。

        体现大型语言模型的发展是一个令人兴奋的研究领域,谷歌的 PaLM-E 模型进一步证明了这一点。该模型将强大的语言模型 PaLM 与 Vision Transformer 相结合,将视觉和传感器数据转换为可以与文本指令交错的标记,使机器人能够根据文本提示和来自其他感官模式的持续反馈执行任务。PaLM-E 网站展示了该模型的能力,包括控制机器人根据文本描述排列方块和取物品。

        文本到视频模型涉及从文本输入创建视频。这个领域建立在文本到图像建模的概念基础上,还有一个额外的挑战,即融入时间维度。例如,2022 年 9 月,Meta 发布了 Make-A-Video,这是一个生成模型,可以仅通过文本提示作为输入创建一个短视频。该模型还能在两个静态图像之间添加动作,并生成给定输入视频的变体。有趣的是,它仅在配对的文本-图像数据和无监督视频素材上进行训练,而不是直接在文本-视频对上进行训练。无监督的视频数据足以让模型学习世界如何移动;然后它使用文本-图像对学习如何映射文本图像模态,然后将其动画化。Dreamix 模型能够进行视频编辑,根据给定的文本提示转换输入视频,同时保留原始视频的摄像机角度、背景和照明元素。

        同样,文本到 3D模型将传统的文本到图像方法扩展到第三维。2022 年 9 月,Google 发布了DreamFusion,这是一个扩散模型,根据输入的文本提示生成 3D 资产。关键是,该模型不需要标记的 3D 资产进行训练。作者使用一个预先训练的 2D 文本到图像模型(Imagen)作为先验,然后训练一个 3D 神经辐射场(NeRF),使其能够在随机角度渲染时产生良好的图像。另一个例子是 OpenAI 的Point-E,于 2022 年 12 月发布。Point-E 是一个纯扩散系统,能够根据给定的文本提示生成一个 3D 点云。虽然其输出质量不如 DreamFusion,但这种方法的优势在于比基于 NeRF 的方法快得多——它可以在单个 GPU 上在一到两分钟内产生输出,而不需要多个 GPU 小时。

        鉴于文本和音乐之间的相似性,不足为奇的是也有人尝试创建文本到音乐模型。Google 于 2023 年 1 月发布的MusicLM是一种语言模型,能够将音乐片段的文本描述(例如“一段由失真吉他伴奏的平静小提琴旋律”)转换为准确反映描述的音频,时长数分钟。它建立在早期工作AudioLM的基础上,通过添加模型能够由文本提示引导的功能;您可以在 Google 研究网站上找到可听的示例。

        生成 AI 的未来

        在这最后一部分中,我们将探讨强大的生成 AI 系统可能对我们生活的世界产生的潜在影响——在我们的日常生活中、工作场所以及教育领域。我们还将阐明生成 AI 将面临的关键实际和伦理挑战,如果它要成为一个使社会获得显著净正面贡献的无处不在的工具。

        生成 AI 在日常生活中的应用

        毫无疑问,未来生成 AI 将在人们的日常生活中扮演越来越重要的角色,特别是大型语言模型。通过 OpenAI 的ChatGPT,已经可以使用生成 AI 为求职申请生成完美的求职信,为同事生成专业的电子邮件回复,或者在特定主题上生成有趣的社交媒体帖子。这项技术真正是互动的:它能够包含您请求的具体细节,回应反馈,并在某些地方不清楚时提出自己的问题。这种个人助手AI 的风格应该是科幻小说的内容,但它并不是——它已经出现了,任何选择使用它的人都可以使用。

        这种应用成为主流的后果是什么?最直接的影响可能是书面沟通质量的提高。使用具有用户友好界面的大型语言模型将使人们能够在几秒钟内将一个想法的草图转化为连贯、高质量的段落。电子邮件写作、社交媒体帖子,甚至短格式即时通讯都将因此技术而发生变革。它不仅消除了与拼写、语法和可读性相关的常见障碍,而且直接将我们的思维过程与可用输出联系起来,通常无需参与构建句子的过程。

        生成良好文本只是大型语言模型的一个用途。人们将开始使用这些模型进行创意生成、建议和信息检索。我相信我们可以将这视为作为一个物种获取、分享、检索和综合信息能力的第四阶段。我们开始通过获取周围人的信息或亲自前往新地点来获取信息。印刷术的发明使书籍成为传播思想的主要载体。最后,互联网的诞生使我们能够在触摸按钮时即时搜索和检索信息。生成 AI 开启了一个新的信息综合时代,我相信它将取代今天搜索引擎的许多当前用途。

        例如,OpenAI 的 GPT 系列模型可以提供定制的假日目的地推荐,如图 14-9 所示,或者如何应对困难情况的建议,或者对一个晦涩概念的详细解释。使用这项技术更像是向朋友询问,而不是在搜索引擎中输入查询,因此,人们迅速涌向这项技术。ChatGPT 是发展最快的技术平台;在推出后的 5 天内获得了 100 万用户。为了对比,Instagram 花了 2.5 个月才达到相同数量的用户,Facebook 花了 10 个月。

        图 14-9。来自 GPT-3 的输出,提供定制的假日推荐

        工作场所中的生成 AI

        除了一般用途外,生成 AI 还将在需要创造力的特定工作中找到应用。以下是一些可能受益的职业的非尽头列表:

        广告

        生成 AI 可以用来创建针对特定人群的个性化广告活动,基于他们的浏览和购买历史。

        音乐制作

        生成 AI 可以用来创作和制作原创音乐曲目,为无限的可能性提供可能。

        建筑学

        生成 AI 可以用来设计建筑和结构,考虑因素如风格和布局约束。

        时尚设计

        生成 AI 可以用来创建独特多样的服装设计,考虑到潮流和穿着者的喜好。

        汽车设计

        生成 AI 可以用来设计和开发新的车型,并自动找到特定设计的有趣变化。

        电影和视频制作

        生成 AI 可以用来创建特效和动画,以及为整个场景或故事情节生成对话。

        制药研究

        生成 AI 可以用来生成新的药物化合物,有助于开发新的治疗方法。

        创意写作

        生成 AI 可以用来生成书面内容,如小说故事、诗歌、新闻文章等。

        游戏设计

        生成 AI 可以用来设计和开发新的游戏关卡和内容,创造无限种游戏体验。

        数字设计

        生成 AI 可以用来创建原创数字艺术和动画,以及设计和开发新的用户界面和网页设计。

        人们经常说 AI 对这些领域的工作构成存在威胁,但我并不认为事实就是如此。对我来说,AI 只是这些创意角色工具箱中的另一个工具(尽管是一个非常强大的工具),而不是角色本身的替代品。选择拥抱这项新技术的人会发现他们能够更快地探索新想法,并以以前不可能的方式迭代概念。

        教育中的生成 AI

        我相信最终将受到显著影响的另一个日常生活领域是教育。生成式人工智能挑战了教育的基本公理,这是我们自互联网诞生以来从未见过的。互联网使学生能够即时和明确地检索信息,使纯粹测试记忆和回忆的考试显得过时和无关紧要。这促使了一种以测试学生能够以新颖方式综合思想为重点的方法转变,而不仅仅是测试事实知识。

        我相信生成式人工智能将在教育领域引起另一场变革性转变,需要重新评估和调整当前的教学方法和评估标准。如果每个学生现在都可以在口袋里拥有一个可以对问题生成新颖回答的论文写作机器,那么基于论文的课程的目的是什么?

        许多人呼吁禁止使用这种人工智能工具,就像禁止剽窃一样。然而,情况并不那么简单,因为检测人工智能生成的文本比检测剽窃要困难得多,甚至更难以无疑地证明。此外,学生可以使用人工智能工具为论文生成一个骨架草稿,然后根据需要添加额外细节或更新事实不正确的信息。在这种情况下,是学生的原创作品,还是人工智能的?

        显然,这些是需要解决的重大问题,以便教育和认证保持其完整性。在我看来,抵制人工智能工具在教育中的传播是毫无意义的-任何这样的方法注定会失败,因为它们将在日常生活中变得如此普遍,以至于试图限制它们的使用将是徒劳的。相反,我们需要找到方法来拥抱这项技术,并询问如何设计开放式人工智能课程,就像我们允许开卷考试课程一样,并鼓励学生使用互联网和人工智能工具公开研究材料。

        生成式人工智能在辅助学习过程本身方面的潜力也是巨大且深刻的。一个由人工智能驱动的导师可以帮助学生学习新主题(如图 14-10 所示),克服误解,或生成完全个性化的学习计划。从生成的虚构中过滤真相的挑战与我们目前在互联网上可用信息所面临的挑战并无二致,这是一个需要跨学科进一步关注的生活技能。

        图 14-10。GPT-3 的输出-展示了大型语言模型如何用于学习的示例

        生成式人工智能可以是一个非常强大的工具,可以在那些有机会接触优秀教师和最佳学习材料的人与那些没有这种机会的人之间拉平竞争场。我对这一领域的进展感到兴奋,因为我相信它可以释放全球范围内的巨大潜力。

        生成式人工智能的伦理和挑战

        尽管在生成式人工智能领域取得了令人难以置信的进展,但仍然有许多挑战需要克服。其中一些挑战是实际的,另一些是伦理的。

        例如,大型语言模型的一个主要批评是,当询问一个陌生或矛盾的主题时,它们很容易生成错误信息,如图 14-4 所示。这种危险在于很难知道生成的回应中包含的信息是否真实准确。即使您要求 LLM 解释其推理或引用来源,它可能会编造参考文献或说出一系列逻辑上不相连的陈述。这不是一个容易解决的问题,因为 LLM 只是一组权重,准确捕捉给定一组输入标记时最可能的下一个词-它没有可以用作参考的真实信息库。

        解决这个问题的一个潜在方案是为大型语言模型提供调用结构化工具的能力,如计算器、代码编译器和在线信息源,用于需要精确执行或事实的任务。例如,图 14-11 展示了 Meta 于 2023 年 2 月发布的名为Toolformer的模型的输出。⁴

        图 14-11. Toolformer 能够自主调用不同的 API 以在必要时获取精确信息的示例(来源:Schick 等人,2023

        Toolformer 能够明确调用 API 以获取信息,作为其生成式响应的一部分。例如,它可能使用维基百科 API 来检索有关特定人物的信息,而不是依赖于这些信息被嵌入在其模型权重中。这种方法特别适用于精确的数学运算,其中 Toolformer 可以说明它想要输入计算器 API 的哪些操作,而不是试图以有用的方式自动生成答案。

        生成式 AI 的另一个突出的伦理关注点在于,大公司使用从网络上抓取的大量数据来训练他们的模型,而原始创作者并没有明确同意这样做。通常这些数据甚至没有公开发布,因此无法知道您的数据是否被用来训练大型语言模型或多模态文本到图像模型。显然,这是一个合理的担忧,特别是对于艺术家来说,他们可能会认为这是对他们的艺术作品的使用,而他们并没有得到任何版税或佣金。此外,艺术家的名字可能被用作提示,以生成更多风格类似于原作的艺术作品,从而降低内容的独特性并将风格商品化。

        这个问题的一个解决方案是由 Stability AI 开创的,他们的多模态模型 Stable Diffusion 是在开源 LAION-5B 数据集的一个子集上进行训练的。他们还推出了网站Have I Been Trained?,任何人都可以在训练数据集中搜索特定的图像或文本段落,并选择退出未来的模型训练过程。这将控制权交还给原始创作者,并确保用于创建强大工具如此的数据具有透明度。然而,这种做法并不普遍,许多商业可用的生成式 AI 模型并不公开其数据集或模型权重,也不提供任何选择退出训练过程的选项。

        总之,虽然生成式 AI 是一个强大的工具,可用于日常生活、工作场所和教育领域的沟通、生产力和学习,但其广泛使用既有优势也有劣势。重要的是要意识到使用生成式 AI 模型的输出的潜在风险,并始终确保负责任地使用它。尽管如此,我对生成式 AI 的未来充满乐观,并迫不及待地想看到企业和人们如何适应这项新的令人兴奋的技术。

        最后思考

        在本书中,我们通过过去十年的生成建模研究之旅,从 VAEs、GANs、自回归模型、正规化流模型、基于能量的模型和扩散模型的基本思想开始,建立在这些基础上,了解 VQ-GAN、Transformers、世界模型和多模态模型等最新技术如何推动生成模型在各种任务中所能实现的边界。

        我相信,在未来,生成建模可能是一种更深层次的人工智能的关键,超越任何特定任务,使机器能够有机地制定自己的奖励、策略,甚至在环境中产生意识。我的信念与 Karl Friston 最初开创的“主动推理”原则密切相关。主动推理背后的理论可以轻松填满另一本完整的书籍——并且确实填满了,就像 Thomas Parr 等人在《主动推理:心智、大脑和行为中的自由能量原则》(麻省理工学院出版社)中所做的那样,我强烈推荐——所以我只会在这里尝试简短解释。

        作为婴儿,我们不断地探索周围环境,建立起可能未来的心智模型,看似没有明显目的,只是为了更深入地理解世界。我们接收到的数据没有标签——从出生那一刻起就不断轰击我们感官的光和声波似乎是随机的。即使有人指着一个苹果说“苹果”,我们年幼的大脑也没有理由将这两个输入联系起来,学习到光线进入眼睛的方式与声波进入耳朵的方式有某种关联。没有声音和图像的训练集,没有气味和味道的训练集,也没有行为和奖励的训练集;只有一个无休止的极其嘈杂的数据流。

        然而,此刻你正在阅读这句话,也许正在享受嘈杂咖啡馆里一杯咖啡的味道。你专注于将视网膜上的微小部分的光缺失转化为一系列抽象概念,这些概念单独来看几乎没有意义,但结合起来,会在你的脑海中引发一波平行的表征——图像、情感、想法、信念和潜在行动都涌入你的意识,等待你的认知。对于你的婴儿大脑来说基本无意义的同样嘈杂的数据流现在不再那么嘈杂。一切对你来说都是有意义的。你在任何地方都看到结构。你对日常生活的物理现象从不感到惊讶。世界是因为你的大脑决定它应该是这样。在这个意义上,你的大脑是一个极其复杂的生成模型,具有关注输入数据特定部分、在神经通路的潜在空间内形成概念表征、并随时间处理序列数据的能力。

        主动推理是一个基于这一思想的框架,用来解释大脑如何处理和整合感官信息以做出决策和行动。它指出,一个生物体对其所处世界有一个生成模型,并利用这个模型对未来事件进行预测。为了减少模型与现实之间的差异所带来的惊讶,生物体相应地调整其行动和信念。Friston 的关键思想是,行动和感知优化可以被看作是同一个硬币的两面,两者都旨在最小化一个称为“自由能量”的量。

        这个框架的核心是一个环境的生成模型(在大脑中捕获),它不断地与现实进行比较。关键是,大脑不是事件的被动观察者。在人类中,它连接着一条脖子和一套腿,可以将其核心输入传感器相对于输入数据源放置在多种位置。因此,可能未来的生成序列不仅取决于其对环境物理的理解,还取决于其对自身及其行为方式的理解。行动和感知的这种反馈循环对我来说非常有趣,我相信我们只是触及了具有行动推理原则的具体环境中能够采取行动的具体生成模型的潜力表面。

        这是我认为将在未来十年继续推动生成建模走向聚光灯下的核心理念之一,作为解锁人工通用智能的关键之一。

        在这个基础上,我鼓励您继续从在线和其他书籍中提供的优质材料中学习更多关于生成模型的知识。感谢您抽出时间阅读本书至此,希望您和我一样享受阅读的乐趣!

        ¹ Hugo Touvron 等人,“LLaMA: 开放高效的基础语言模型”,2023 年 2 月 27 日,https://arxiv.org/abs/2302.13971

        ² Mark Chen 等人,“评估在代码上训练的大型语言模型”,2021 年 7 月 7 日,https://arxiv.org/abs/2107.03374

        ³ 张旅民和 Maneesh Agrawala,“向文本到图像扩散模型添加条件控制”,2023 年 2 月 10 日,https://arxiv.org/abs/2302.05543

        ⁴ Timo Schick 等人,“Toolformer: 语言模型可以自学使用工具”,2023 年 2 月 9 日,https://arxiv.org/abs/2302.04761

posted @ 2025-11-23 09:26  绝不原创的飞龙  阅读(9)  评论(0)    收藏  举报