生成式深度学习(全)
原文: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 没有先验知识(这些库将在第二章中介绍)。
路线图
这本书分为三个部分。
第一部分是生成建模和深度学习的一般介绍,我们在其中探讨了贯穿本书后续部分所有技术的核心概念:
第二部分介绍了我们将用于构建生成模型的六种关键技术,每种技术都有实际示例:
-
在第三章,“变分自编码器”中,我们考虑变分自编码器(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 的概率——即它是不是梵高画的概率。
相比之下,生成建模不需要数据集被标记,因为它关注的是生成全新的图像,而不是试图预测给定图像的标签。
让我们正式定义这些类型的建模,使用数学符号:
条件生成模型
请注意,我们也可以构建一个生成模型来建模条件概率——观察到具有特定标签的观察的概率。
例如,如果我们的数据集包含不同类型的水果,我们可以告诉我们的生成模型专门生成一个苹果的图像。
一个重要的观点是,即使我们能够构建一个完美的判别模型来识别梵高的画作,它仍然不知道如何创作一幅看起来像梵高的画。它只能输出针对现有图像的概率,因为这是它被训练做的事情。我们需要训练一个生成模型,并从这个模型中采样,以生成具有高概率属于原始训练数据集的图像。
生成建模的崛起
直到最近,判别建模一直是机器学习中取得进展的主要驱动力。这是因为对于任何判别问题,相应的生成建模问题通常更难解决。例如,训练一个模型来预测一幅画是否是梵高的比起从头开始训练一个模型生成梵高风格的画作要容易得多。同样,训练一个模型来预测一段文本是否是查尔斯·狄更斯写的比起构建一个模型生成狄更斯风格的段落要容易得多。直到最近,大多数生成挑战都是难以实现的,许多人怀疑它们是否能够被解决。创造力被认为是一种纯粹的人类能力,无法被人工智能所匹敌。
然而,随着机器学习技术的成熟,这种假设逐渐削弱。在过去的 10 年中,该领域中最有趣的进展之一是通过将机器学习应用于生成建模任务的新颖应用。例如,图 1-3 展示了自 2014 年以来在面部图像生成方面已经取得的显著进展。
![]()
除了更容易处理外,辨别建模在历史上比生成建模更容易应用于跨行业的实际问题。例如,医生可能会从一个可以预测给定视网膜图像是否显示青光眼迹象的模型中受益,但不一定会从一个可以生成眼睛背面的新颖图片的模型中受益。
然而,这也开始发生变化,随着越来越多的公司提供针对特定业务问题的生成服务。例如,现在可以访问 API,根据特定主题生成原创博客文章,生成您产品在任何您想要的环境中的各种图片,或者撰写社交媒体内容和广告文案以匹配您的品牌和目标信息。生成人工智能在游戏设计和电影制作等行业也有明显的积极应用,训练用于输出视频和音乐的模型开始增加价值。
生成建模和人工智能
除了生成建模的实际用途(其中许多尚未被发现),还有三个更深层次的原因,可以认为生成建模是解锁一种更复杂形式的人工智能的关键,超越了辨别建模单独可以实现的范围。
首先,从理论角度来看,我们不应该将机器训练仅限于简单地对数据进行分类。为了完整性,我们还应该关注训练能够捕捉数据分布更完整理解的模型,超越任何特定标签。这无疑是一个更难解决的问题,因为可行输出空间的维度很高,我们将归类为数据集的创作数量相对较少。然而,正如我们将看到的,许多推动辨别建模发展的相同技术,如深度学习,也可以被生成模型利用。
其次,正如我们将在第十二章中看到,生成建模现在被用于推动其他领域的人工智能进步,如强化学习(通过试错来教导代理优化环境中的目标)。假设我们想训练一个机器人在给定地形上行走。传统方法是在环境中运行许多实验,其中代理尝试不同的策略,或者在地形的计算机模拟中尝试。随着时间的推移,代理将学会哪些策略比其他策略更成功,因此逐渐改进。这种方法的挑战在于它相当僵化,因为它被训练来优化一个特定任务的策略。最近开始流行的另一种方法是,代替训练代理人学习环境的世界模型,使用生成模型,独立于任何特定任务。代理可以通过在自己的世界模型中测试策略,而不是在真实环境中测试,来快速适应新任务,这通常在计算上更有效,并且不需要为每个新任务从头开始重新训练。
最后,如果我们真的要说我们已经建立了一台获得了与人类相媲美的智能形式的机器,生成建模肯定是解决方案的一部分。自然界中最好的生成模型之一就是正在阅读本书的人。花点时间考虑一下你是一个多么不可思议的生成模型。你可以闭上眼睛想象大象从任何可能的角度看起来是什么样子。你可以想象你最喜欢的电视节目有许多不同的可能结局,你可以通过在脑海中思考各种未来并相应地采取行动来计划下周。当前的神经科学理论表明,我们对现实的感知并不是一个高度复杂的辨别模型,它根据我们的感官输入产生我们正在经历的预测,而是一个从出生开始就接受训练以产生准确匹配未来的环境的模型。一些理论甚至暗示,这个生成模型的输出就是我们直接感知为现实的东西。显然,深入了解我们如何构建机器来获得这种能力将是我们继续了解大脑运作和普遍人工智能的核心。
我们的第一个生成模型
有了这个想法,让我们开始我们激动人心的生成建模之旅。首先,我们将看一个生成模型的玩具示例,并介绍一些将帮助我们理解本书后面将遇到的更复杂架构的想法。
你好,世界!
让我们从在只有两个维度中玩一个生成建模游戏开始。我选择了一个用于生成点集的规则,如图 1-4 所示。让我们称这个规则为。你的挑战是选择一个不同的点,看起来像是由相同规则生成的。
![二维空间中的两个点集]()
图 1-4。由未知规则生成的二维空间中的点集
你是如何选择的?你可能利用现有数据点的知识构建了一个心理模型,来估计点更有可能出现的位置。在这方面,是的估计。也许你决定应该看起来像图 1-5——一个矩形框,点可能出现在其中,框外则不可能找到任何点。
![]()
图 1-5。橙色框,,是真实数据生成分布的估计
要生成一个新的观察结果,您可以简单地在框内随机选择一个点,或者更正式地,从分布中采样。恭喜,您刚刚构建了您的第一个生成模型!您已经使用训练数据(黑点)构建了一个模型(橙色区域),您可以轻松地从中进行采样以生成其他似乎属于训练集的点。
现在让我们将这种思维形式化为一个框架,可以帮助我们理解生成建模试图实现的目标。
生成建模框架
我们可以在以下框架中捕捉建立生成模型的动机和目标。
现在让我们揭示真实的数据生成分布,看看这个框架如何适用于这个例子。正如我们从图 1-6 中看到的,数据生成规则只是世界陆地的均匀分布,没有机会在海洋中找到一个点。
![]()
图 1-6。橙色框是真实数据生成分布的估计(灰色区域)
显然,我们的模型是对的过度简化。我们可以检查点 A、B 和 C,以了解我们的模型在多大程度上准确模拟了的成功和失败:
-
点 A 是由我们的模型生成的观察结果,但似乎并不是由生成的,因为它位于海洋中间。
-
点 B 永远不可能由生成,因为它位于橙色框外。因此,我们的模型在产生观察结果的整个潜在可能性范围内存在一些缺陷。
-
点 C 是一个观察结果,可以由和生成。
尽管模型存在缺陷,但由于它只是一个橙色框内的均匀分布,因此很容易从中进行采样。我们可以轻松地随机选择框内的一个点,以便从中进行采样。
此外,我们可以肯定地说,我们的模型是对捕捉一些基本高级特征的底层复杂分布的简单表示。真实分布被分为有大量陆地(大陆)和没有陆地(海洋)的区域。这也是我们模型的一个高级特征,除了我们只有一个大陆,而不是很多。
这个例子展示了生成建模背后的基本概念。我们在本书中将要解决的问题将会更加复杂和高维,但我们处理问题的基本框架将是相同的。
表示学习
值得深入探讨一下我们所说的学习高维数据的表示是什么意思,因为这是本书中将会反复出现的一个主题。
假设你想向一个在人群中寻找你并不知道你长什么样的人描述你的外貌。你不会从描述你照片的像素 1 的颜色开始,然后是像素 2,然后是像素 3,依此类推。相反,你会合理地假设对方对一个普通人的外貌有一个大致的概念,然后用描述像素组的特征来修正这个基线,比如我有很金黄的头发或我戴眼镜。只需不超过 10 条这样的描述,对方就能将描述映射回像素,生成一个你的形象。这个形象可能不完美,但足够接近你的实际外貌,让对方在可能有数百人的人群中找到你,即使他们以前从未见过你。
这就是表示学习的核心思想。我们不是直接对高维样本空间建模,而是使用一些较低维度的潜在空间来描述训练集中的每个观察,并学习一个能够将潜在空间中的点映射到原始域中的点的映射函数。换句话说,潜在空间中的每个点都是某个高维观察的表示。
这在实践中意味着什么?假设我们有一个由饼干罐的灰度图像组成的训练集(图 1-7)。
![]()
图 1-7。饼干罐数据集
对我们来说,很明显有两个特征可以唯一代表这些罐子:罐子的高度和宽度。也就是说,我们可以将每个罐子的图像转换为一个仅有两个维度的潜在空间中的点,即使训练集中提供的图像是在高维像素空间中的。值得注意的是,这意味着我们也可以通过将适当的映射函数应用于潜在空间中的新点,生成训练集中不存在的罐子的图像,如图 1-8 所示。
意识到原始数据集可以用更简单的潜在空间来描述对于机器来说并不容易——它首先需要确定高度和宽度是最能描述这个数据集的两个潜在空间维度,然后学习能够将这个空间中的点映射到灰度饼干罐图像的映射函数。机器学习(特别是深度学习)赋予我们训练机器能够找到这些复杂关系的能力,而无需人类的指导。
![]()
图 1-8。饼干罐的 2D 潜在空间和将潜在空间中的点映射回原始图像域的函数
利用潜在空间训练模型的一个好处是,我们可以通过在更易管理的潜在空间内操作其表示向量,影响图像的高级属性。例如,要调整每个像素的阴影以使饼干罐的图像更高并不明显。然而,在潜在空间中,只需增加高度潜在维度,然后应用映射函数返回到图像域。我们将在下一章中看到一个明确的例子,不是应用于饼干罐而是应用于人脸。
将训练数据集编码到一个潜在空间中,以便我们可以从中进行采样并将点解码回原始域的概念对于许多生成建模技术是常见的,我们将在本书的后续章节中看到。从数学上讲,编码器-解码器技术试图将数据所在的高度非线性流形(例如,在像素空间中)转换为一个更简单的潜在空间,可以从中进行采样,因此很可能潜在空间中的任何点都是一个良好形成的图像的表示,如图 1-9 所示。
![]()
图 1-9. 在高维像素空间中的狗流形被映射到一个更简单的潜在空间,可以从中进行采样
核心概率理论
我们已经看到生成建模与概率分布的统计建模密切相关。因此,现在引入一些核心概率和统计概念是有意义的,这些概念将贯穿本书,用来解释每个模型的理论背景。
如果你从未学习过概率或统计学,不用担心。为了构建本书后面将看到的许多深度学习模型,不必对统计理论有深入的理解。然而,为了充分理解我们试图解决的任务,值得尝试建立对基本概率理论的扎实理解。这样,您将有基础来理解本章后面将介绍的不同类型的生成模型。
作为第一步,我们将定义五个关键术语,将每个术语与我们之前在二维世界地图中建模的生成模型的例子联系起来:
样本空间
样本空间是观察值 可以取的所有值的完整集合。
注意
在我们之前的例子中,样本空间包括世界地图上所有的纬度和经度点 。例如, = (40.7306, –73.9352) 是样本空间(纽约市)中属于真实数据生成分布的点。 = (11.3493, 142.1996) 是样本空间中不属于真实数据生成分布的点(在海里)。
概率密度函数
概率密度函数(或简称密度函数)是一个将样本空间中的点 映射到 0 到 1 之间的数字的函数 。密度函数在样本空间中所有点上的积分必须等于 1,以便它是一个明确定义的概率分布。
注意
在世界地图的例子中,我们生成模型的密度函数在橙色框之外为 0,在框内为常数,使得密度函数在整个样本空间上的积分等于 1。
虽然只有一个真实的密度函数被假定为生成可观测数据集,但有无限多个密度函数可以用来估计。
参数建模
参数建模是一种技术,我们可以用来构建我们寻找适当的方法。参数模型是一组密度函数,可以用有限数量的参数来描述。
注意
如果我们假设均匀分布作为我们的模型族,那么我们可以在图 1-5 上绘制的所有可能框的集合是参数模型的一个示例。在这种情况下,有四个参数:框的左下角坐标和右上角的坐标。
因此,这个参数模型中的每个密度函数(即每个框)可以用四个数字唯一表示。
可能性
参数集θ的可能性是一个函数,用于衡量给定一些观察点的θ的合理性。它的定义如下:
也就是说,给定一些观察点的θ的可能性被定义为由参数化的密度函数在点处的值。如果我们有一个完整的独立观测数据集,那么我们可以写成:
注意
在世界地图示例中,一个只覆盖地图左半部分的橙色框的似然为 0—它不可能生成数据集,因为我们观察到地图右半部分的点。在图 1-5 中的橙色框具有正的似然,因为在该模型下所有数据点的密度函数都为正。
由于 0 到 1 之间大量项的乘积可能会导致计算上的困难,我们通常使用log-likelihood ℓ代替:
有统计原因解释为什么似然以这种方式定义,但我们也可以看到这种定义在直觉上是有道理的。一组参数θ的似然被定义为如果真实的数据生成分布是由θ参数化的模型,则看到数据的概率。
警告
请注意,似然是参数的函数,而不是数据。它不应该被解释为给定参数集正确的概率—换句话说,它不是参数空间上的概率分布(即,它不会对参数求和/积分为 1)。
参数化建模的重点应该是找到最大化观察到的数据集𝐗的可能性的参数集的最优值^θ。
最大似然估计
最大似然估计是一种技术,它允许我们估计^θ——密度函数 pθ(𝐱)的参数集θ最有可能解释一些观察到的数据𝐗。更正式地说:
^θ也被称为最大似然估计(MLE)。
注意
在世界地图示例中,MLE 是仍然包含训练集中所有点的最小矩形。
神经网络通常最小化损失函数,因此我们可以等效地讨论找到最小化负对数似然的参数集:
生成建模可以被看作是一种最大似然估计的形式,其中参数θ是模型中包含的神经网络的权重。我们试图找到这些参数的值,以最大化观察到的给定数据的可能性(或等效地,最小化负对数似然)。
然而,对于高维问题,通常不可能直接计算 pθ(𝐱)—它是难以计算的。正如我们将在下一节中看到的,不同类型的生成模型采取不同的方法来解决这个问题。
生成模型分类
虽然所有类型的生成模型最终都旨在解决相同的任务,但它们在对密度函数 pθ(𝐱)建模时采取了略有不同的方法。广义上说,有三种可能的方法:
-
显式地对密度函数建模,但以某种方式限制模型,使得密度函数是可计算的。
-
显式地建模密度函数的可处理近似。
-
通过直接生成数据的随机过程隐式建模密度函数。
这些在图 1-10 中显示为分类法,与本书第 II 部分中将探索的六种生成模型家族并列。请注意,这些家族并不是相互排斥的—有许多模型是两种不同方法的混合体。您应该将这些家族视为生成建模的不同一般方法,而不是显式模型架构。
![]()
图 1-10. 生成建模方法的分类法
我们可以进行的第一个分割是在概率密度函数 被显式建模和被隐式建模的模型之间。
隐式密度模型并不旨在估计概率密度,而是专注于产生直接生成数据的随机过程。隐式生成模型的最著名例子是生成对抗网络。我们可以进一步将显式密度模型分为直接优化密度函数(可处理模型)和仅优化其近似值的模型。
可处理模型对模型架构施加约束,使得密度函数具有易于计算的形式。例如,自回归模型对输入特征进行排序,以便可以按顺序生成输出—例如,逐字或逐像素。归一化流模型将一系列可处理、可逆函数应用于简单分布,以生成更复杂的分布。
近似密度模型包括变分自动编码器,引入潜变量并优化联合密度函数的近似值。基于能量的模型也利用近似方法,但是通过马尔可夫链采样,而不是变分方法。扩散模型通过训练模型逐渐去噪给定的先前损坏的图像来近似密度函数。
贯穿所有生成模型家族类型的共同主题是深度学习。几乎所有复杂的生成模型都以深度神经网络为核心,因为它们可以从头开始训练,学习控制数据结构的复杂关系,而不必事先硬编码信息。我们将在第二章中探讨深度学习,提供实际示例,帮助您开始构建自己的深度神经网络。
生成式深度学习代码库
本章的最后一节将帮助您开始构建生成式深度学习模型,介绍伴随本书的代码库。
提示
本书中的许多示例都改编自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 中所示的网络,假设它已经被训练得可以准确预测给定输入脸部是否微笑:
-
单元 A 接收输入像素的单个通道的值。
-
单元 B 组合其输入值,使得当存在特定的低级特征,例如边缘时,它发射最强。
-
单元 C 组合低级特征,使得当图像中看到高级特征,例如牙齿时,它发射最强。
-
单元 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 所示。
![]()
默认情况下,图像数据由每个像素通道的 0 到 255 之间的整数组成。我们首先需要通过将这些值缩放到 0 到 1 之间来预处理图像,因为当每个输入的绝对值小于 1 时,神经网络的效果最好。
我们还需要将图像的整数标签更改为独热编码向量,因为神经网络的输出将是图像属于每个类的概率。如果图像的类整数标签是,那么它的独热编码是一个长度为 10 的向量(类的数量),除了第个元素为 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_train和x_test分别是形状为[50000, 32, 32, 3]和[10000, 32, 32, 3]的numpy数组。y_train和y_test分别是形状为[50000, 1]和[10000, 1]的numpy数组,包含每个图像类的范围为 0 到 9 的整数标签。
②
缩放每个图像,使像素通道值介于 0 和 1 之间。
③
对标签进行独热编码——y_train和y_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,我们使用了三种不同类型的层:Input、Flatten和Dense。
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激活函数是有用的;例如,对于每个观察结果只属于一个类的多类分类问题。它被定义为:
在这里,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 提供了许多内置的损失函数可供选择,或者你可以创建自己的损失函数。最常用的三个是均方误差、分类交叉熵和二元交叉熵。重要的是要理解何时适合使用每种损失函数。
如果你的神经网络旨在解决回归问题(即输出是连续的),那么你可能会使用均方误差损失。这是每个输出单元的实际值和预测值之间的平方差的平均值,其中平均值是在所有个输出单元上取得的:
如果你正在处理一个分类问题,其中每个观测只属于一个类,那么分类交叉熵是正确的损失函数。它定义如下:
最后,如果你正在处理一个具有一个输出单元的二元分类问题,或者一个每个观测可以同时属于多个类的多标签问题,你应该使用二元交叉熵:
优化器
优化器 是基于损失函数的梯度更新神经网络权重的算法。最常用和稳定的优化器之一是 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 个类别概率的向量。
②
我们将这个概率数组转换回一个单一的预测,使用numpy的argmax函数。这里,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层的两个参数——strides和padding。
步幅
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"的卷积层的输出形状是:
堆叠卷积层
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 |
|
让我们逐层走过我们的网络,注意张量的形状:
-
输入形状为(None, 32, 32, 3)—Keras 使用None表示我们可以同时通过网络传递任意数量的图像。由于网络只是执行张量代数运算,我们不需要单独通过网络传递图像,而是可以一起作为批次传递它们。
-
第一个卷积层中每个滤波器的形状是 4×4×3。这是因为我们选择每个滤波器的高度和宽度为 4(kernel_size=(4,4)),并且在前一层中有三个通道(红色、绿色和蓝色)。因此,该层中的参数(或权重)数量为(4×4×3+1)×10=490,其中+1 是由于每个滤波器附加了一个偏置项。每个滤波器的输出将是滤波器权重和它所覆盖的图像的 4×4×3 部分的逐像素乘积。由于strides=2和padding="same",输出的宽度和高度都减半为 16,由于有 10 个滤波器,第一层的输出是一批张量,每个张量的形状为[16,16,10]。
-
在第二个卷积层中,我们选择滤波器为 3×3,它们现在的深度为 10,以匹配前一层中的通道数。由于这一层中有 20 个滤波器,这给出了总参数(权重)数量为(3×3×10+1)×20=1,820。同样,我们使用strides=2和padding="same",所以宽度和高度都减半。这给出了一个总体输出形状为(None, 8, 8, 20)。
-
现在我们使用 Keras 的Flatten层展平张量。这会产生一组 8×8×20=1,280 个单元。请注意,在Flatten层中没有需要学习的参数,因为该操作只是对张量进行重组。
-
最后,我们将这些单元连接到一个具有 softmax 激活函数的 10 单元Dense层,表示 10 类分类任务中每个类别的概率。这会创建额外的 1,280×10=12,810 个参数(权重)需要学习。
这个例子演示了如何将卷积层链接在一起创建卷积神经网络。在我们看到这与我们密集连接的神经网络在准确性上的比较之前,我们将研究另外两种也可以提高性能的技术:批量归一化和 dropout。
批量归一化
训练深度神经网络时的一个常见问题是确保网络的权重保持在合理范围内的数值范围内 - 如果它们开始变得过大,这表明您的网络正在遭受所谓的梯度爆炸问题。当错误向后传播通过网络时,早期层中梯度的计算有时可能会呈指数增长,导致权重值出现剧烈波动。
警告
如果您的损失函数开始返回NaN,那么很有可能是您的权重已经变得足够大,导致溢出错误。
这并不一定会立即发生在您开始训练网络时。有时候,它可能在几个小时内愉快地训练,突然损失函数返回NaN,您的网络就爆炸了。这可能非常恼人。为了防止这种情况发生,您需要了解梯度爆炸问题的根本原因。
协变量转移
将输入数据缩放到神经网络的一个原因是确保在前几次迭代中稳定地开始训练。由于网络的权重最初是随机化的,未缩放的输入可能会导致立即产生激活值过大,从而导致梯度爆炸。例如,我们通常将像素值从 0-255 传递到输入层,而不是将这些值缩放到-1 到 1 之间。
因为输入被缩放,自然地期望未来所有层的激活也相对缩放。最初可能是正确的,但随着网络训练和权重远离其随机初始值,这个假设可能开始破裂。这种现象被称为协变量转移。
协变量转移类比
想象一下,你正拿着一摞高高的书,突然被一阵风吹袭。你将书向与风相反的方向移动以补偿,但在这样做的过程中,一些书会移动,使得整个塔比以前稍微不稳定。最初,这没关系,但随着每阵风,这摞书变得越来越不稳定,直到最终书移动得太多,整摞书倒塌。这就是协变量转移。
将这与神经网络联系起来,每一层就像堆叠中的一本书。为了保持稳定,当网络更新权重时,每一层都隐含地假设其来自下一层的输入分布在迭代中大致保持一致。然而,由于没有任何东西可以阻止任何激活分布在某个方向上发生显着变化,这有时会导致权重值失控和网络整体崩溃。
使用批量归一化进行训练
批量归一化是一种极大地减少这个问题的技术。解决方案出奇地简单。在训练期间,批量归一化层计算每个输入通道在批处理中的均值和标准差,并通过减去均值并除以标准差来进行归一化。然后,每个通道有两个学习参数,即缩放(gamma)和移位(beta)。输出只是归一化的输入,由 gamma 缩放并由 beta 移位。图 2-14 展示了整个过程。
![]()
我们可以在密集层或卷积层之后放置批量归一化层来归一化输出。
提示
参考我们之前的例子,这有点像用一小组可调节弹簧连接书层,以确保它们的位置随时间不会发生明显的整体移动。
使用批量归一化进行预测
您可能想知道这个层在预测时是如何工作的。在预测时,我们可能只想预测单个观测值,因此没有批次可以计算平均值和标准差。为了解决这个问题,在训练期间,批归一化层还会计算每个通道的平均值和标准差的移动平均值,并将这个值作为该层的一部分存储起来,以便在测试时使用。
批归一化层中包含多少参数?对于前一层中的每个通道,需要学习两个权重:比例(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 层类型:Conv2D、BatchNormalization和Dropout。让我们将这些部分组合成一个 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 |
|
提示
在继续之前,请确保您能够手工计算每一层的输出形状和参数数量。这是一个很好的练习,可以证明您已经完全理解了每一层是如何构建的,以及它是如何与前一层连接的!不要忘记包括作为Conv2D和Dense层的一部分包含的偏置权重。
训练和评估 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 中。输入图像被编码为潜在嵌入向量,然后解码回原始像素空间。
![]()
图 3-4。自动编码器架构图
自动编码器经过编码器和解码器后被训练重建图像。这一开始可能看起来很奇怪——为什么要重建一组已经可用的图像?然而,正如我们将看到的,自动编码器中有趣的部分是嵌入空间(也称为潜在空间),因为从这个空间中取样将允许我们生成新的图像。
首先定义一下我们所说的嵌入。嵌入()是原始图像压缩到较低维度潜在空间中。这个想法是通过选择潜在空间中的任意点,我们可以通过解码器生成新颖的图像,因为解码器已经学会如何将潜在空间中的点转换为可行的图像。
在我们的示例中,我们将图像嵌入到一个二维潜在空间中。这将帮助我们可视化潜在空间,因为我们可以轻松在 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:
这些观察实际上使得从潜在空间中抽样变得非常具有挑战性。如果我们将解码点的图像叠加在网格上的潜在空间上,如图 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_var( )之间的关系推导如下:
变分自动编码器的解码器与普通自动编码器的解码器相同,给出了图 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_mean和z_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_mean和z_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_mean和z_log_var层。
②
Sampling层从由参数z_mean和z_log_var定义的正态分布中对潜在空间中的点z进行采样。
③
定义编码器的 Keras Model——一个接受输入图像并输出z_mean、z_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_mean和z_log_var的正态分布与标准正态分布之间的差异。在这种特殊情况下,可以证明 KL 散度具有以下封闭形式:
kl_loss = -0.5 * sum(1 + z_log_var - z_mean ^ 2 - exp(z_log_var))
或者用数学符号表示:
总和取自潜在空间中的所有维度。当所有维度上的z_mean = 0和z_log_var = 0时,kl_loss被最小化为 0。当这两个项开始与 0 不同,kl_loss增加。
总的来说,KL 散度项惩罚网络将观测编码为与标准正态分布的参数明显不同的z_mean和z_log_var变量,即z_mean = 0和z_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_mean和z_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 显示了几幅已编码到潜在空间中的图像。然后,我们添加或减去某个向量的倍数(例如,Smiling、Black_Hair、Eyeglasses、Young、Male、Blond_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层堆叠在一起,中间夹有BatchNormalization、LeakyReLU激活和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层传递这些数据,其中夹在中间的是BatchNormalization和LeakyReLU层。
④
最终的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. 当鉴别器压倒生成器时的示例输出
如果发现鉴别器损失函数坍缩,需要找到削弱鉴别器的方法。尝试以下建议:
生成器压倒鉴别器
如果鉴别器不够强大,生成器将找到一种方法轻松欺骗鉴别器,只需少量几乎相同的图像样本。这被称为模式坍塌。
例如,假设我们在不更新鉴别器的情况下训练生成器多个批次。生成器会倾向于找到一个始终欺骗鉴别器的单个观察(也称为模式),并开始将潜在输入空间中的每个点映射到这个图像。此外,损失函数的梯度会坍缩到接近 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. 二元交叉熵损失
为了训练 GAN 鉴别器 D,我们计算了对比真实图像 x_i 的预测 p_i 与响应 y_i=1 以及对比生成图像 G(z_i)的预测 p_i 与响应 y_i=0 的损失。因此,对于 GAN 鉴别器,最小化损失函数可以写成方程 4-2 所示。
方程 4-2. GAN 鉴别器损失最小化
为了训练 GAN 生成器 G,我们计算了对比生成图像 G(z_i)的预测 p_i 与响应 y_i=1 的损失。因此,对于 GAN 生成器,最小化损失函数可以写成方程 4-3 所示。
方程 4-3. GAN 生成器损失最小化
现在让我们将其与 Wasserstein 损失函数进行比较。
首先,Wasserstein 损失要求我们使用 y_i=1 和 y_i=-1 作为标签,而不是 1 和 0。我们还从鉴别器的最后一层中移除了 sigmoid 激活,使得预测 p_i 不再受限于[0, 1]范围,而是可以是任何范围内的任意数字(负无穷,正无穷)。因此,在 WGAN 中,鉴别器通常被称为评论家,输出分数而不是概率。
Wasserstein 损失函数定义如下:
为了训练 WGAN 评论家,我们计算真实图像的预测与响应之间的损失与响应