原文:
annas-archive.org/md5/0ce7869756af78df5fbcd0d4ae8d5259
译者:飞龙
前言
随着我们进入 21 世纪,人工智能和机器学习技术正在迅速显现出它们将深刻改变我们未来生活方式的趋势。从对话助手到搜索引擎中的智能推荐,我们现在每天都在体验 AI,普通用户/消费者现在期望他们在做任何事情时,界面都能更加智能化。这无疑包括游戏,这也很可能是你作为一名游戏开发者正在考虑阅读本书的原因之一。
本书将通过动手实践,带领你构建深度学习模型,用于简单编码,目的是构建自动驾驶算法、生成音乐以及创建对话机器人,最后深入探讨深度强化学习(DRL)。我们将从强化学习(RL)的基础开始,逐步结合深度学习(DL)和强化学习(RL)来创建深度强化学习(DRL)。接下来,我们将深入探讨如何优化强化学习,训练代理执行复杂任务,从在走廊中导航到与僵尸踢足球。过程中,我们将通过实际的试验和错误学习调参的细节,并学习如何使用前沿算法,包括好奇心学习、课程学习、回放学习和模仿学习,以优化代理的训练。
本书适用对象
本书适合任何对在下一款游戏项目中应用深度学习感兴趣的游戏开发者或有志于成为游戏开发者的人。为了成功掌握这些材料,你应该掌握 Python 编程语言以及其他基于 C 的语言,如 C#、C、C++或 Java。此外,基础的微积分、统计学和概率学知识将有助于你理解材料并促进学习,但这些并非必需。
本书涵盖内容
第一章,游戏中的深度学习,介绍了深度学习在游戏中的背景,然后通过构建基本感知机来讲解基础知识。之后,我们将学习网络层的概念并构建一个简单的自编码器。
第二章,卷积神经网络与递归神经网络,探讨了卷积和池化等高级层,并说明如何将其应用于构建自动驾驶深度网络。接着,我们将研究如何使用递归层在深度网络中学习序列的概念。
第三章,用于游戏的生成对抗网络(GAN),概述了生成对抗网络(GAN)的概念,它是一种将两个对立网络对抗的架构模式。接下来,我们将探讨并使用各种 GAN 生成游戏纹理和原创音乐。
第四章,构建深度学习游戏聊天机器人,详细介绍了递归网络,并开发了几种形式的对话型聊天机器人。最后,我们将通过 Unity 与聊天机器人进行对话。
第五章,引言:深度强化学习(DRL),首先介绍强化学习的基本概念,然后讲解多臂老虎机问题和 Q 学习。接下来,我们将迅速进入深度学习的整合,并通过 Open AI Gym 环境探索深度强化学习。
第六章,Unity ML-Agents,首先介绍了 ML-Agents 工具包,这是一个基于 Unity 构建的强大深度强化学习平台。然后,我们将学习如何设置和训练工具包提供的各种示例场景。
第七章,智能体与环境,探讨了从环境中捕获的输入状态如何影响训练。我们将研究通过为不同的视觉环境构建多种输入状态编码器来改进这些问题。
第八章,理解 PPO,解释了学习如何训练智能体需要对强化学习中使用的各种算法有一些深入的背景知识。在这一章中,我们将深入探索 ML-Agents 工具包中的核心算法——近端策略优化算法(PPO)。
第九章,奖励与强化学习,解释了奖励在强化学习中的基础性作用,探讨了奖励的重要性以及如何建模奖励函数。我们还将探索奖励的稀疏性,以及如何通过课程学习(Curriculum Learning)和反向播放(backplay)克服强化学习中的这些问题。
第十章,模仿与迁移学习,进一步探索了模仿学习和迁移学习等高级方法,作为克服奖励稀疏性和其他智能体训练问题的方式。接着,我们将研究迁移学习的其他应用方式。
第十一章,构建多智能体环境,探索了多个智能体相互竞争或合作的多种场景。
第十二章,使用 DRL 调试/测试游戏,解释了如何使用 ML-Agents 构建一个测试/调试框架,以便在你的下一个游戏中使用,这是深度强化学习中一个较少涉及的新领域。
第十三章,障碍塔挑战及未来,探讨了你接下来的发展方向。你是否准备好接受 Unity 障碍塔挑战并构建自己的游戏,还是你需要进一步的学习?
为了从本书中获得最大收益
对 Python 有一定了解,并且接触过机器学习会有所帮助,了解 C 风格语言(如 C、C++、C# 或 Java)也会有帮助。对微积分的基本理解虽然不是必需的,但也会有所帮助,理解概率和统计也会有益。
下载示例代码文件
您可以从您的帐户在 www.packt.com 下载本书的示例代码文件。如果您从其他地方购买了本书,可以访问 www.packt.com/support 并注册以便直接通过电子邮件获取文件。
您可以按照以下步骤下载代码文件:
-
请在 www.packt.com 登录或注册。
-
选择 SUPPORT 标签。
-
点击“代码下载与勘误”。
-
在搜索框中输入书名,并按照屏幕上的指示操作。
文件下载后,请确保使用最新版本的工具解压或提取文件夹:
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
-
7-Zip/PeaZip for Linux
本书的代码包也托管在 GitHub 上,链接为 github.com/PacktPublishing/Hands-On-Deep-Learning-for-Games
。若代码有更新,将会在现有的 GitHub 仓库中更新。
我们还从我们丰富的书籍和视频目录中提供其他代码包,您可以在 github.com/PacktPublishing/
上查看!快来看看吧!
下载彩色图片
我们还提供了一个包含本书中使用的截图/图表彩色图片的 PDF 文件。您可以在此下载: www.packtpub.com/sites/default/files/downloads/9781788994071_ColorImages.pdf
.
使用的约定
本书中使用了许多文本约定。
CodeInText
:表示文本中的代码词汇、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。举个例子:“将下载的 WebStorm-10*.dmg
磁盘映像文件作为另一个磁盘挂载到您的系统中。”
代码块以如下形式设置:
html, body, #map {
height: 100%;
margin: 0;
padding: 0
}
当我们希望引起您对代码块中特定部分的注意时,相关行或项目将以粗体显示:
[default]
exten => s,1,Dial(Zap/1|30)
exten => s,2,Voicemail(u100)
exten => s,102,Voicemail(b100)
exten => i,1,Voicemail(s0)
任何命令行输入或输出将以如下形式书写:
$ mkdir css
$ cd css
粗体:表示新术语、重要单词或屏幕上显示的单词。例如,菜单或对话框中的单词会以这种形式出现在文本中。举个例子:“从管理面板中选择系统信息。”
警告或重要说明以这种形式出现。
提示和技巧以这种形式出现。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果你对本书的任何方面有疑问,请在邮件主题中注明书名,并通过customercare@packtpub.com
与我们联系。
勘误:尽管我们已经尽力确保内容的准确性,但错误仍然会发生。如果你在本书中发现任何错误,我们将非常感激你向我们报告。请访问 www.packt.com/submit-errata,选择你的书籍,点击“勘误提交表单”链接,并填写相关信息。
盗版:如果你在互联网上遇到任何我们作品的非法复制版本,我们将非常感激你能提供该材料的地址或网站名称。请通过copyright@packt.com
与我们联系,并附上相关链接。
如果你有兴趣成为作者:如果你在某个领域有专业知识,并且有兴趣编写或贡献一本书,请访问 authors.packtpub.com。
评论
请留下评论。阅读并使用完本书后,为什么不在你购买该书的网站上留下评论呢?潜在读者可以通过你的公正评价做出购买决策,我们 Packt 也能了解你对我们产品的看法,而我们的作者也能看到你对他们作品的反馈。谢谢!
想了解更多关于 Packt 的信息,请访问 packt.com。
第一部分:基础知识
本书的这一部分涵盖了神经网络和深度学习的基本概念。我们将从最简单的自编码器、生成对抗网络(GANs)、卷积神经网络和递归神经网络讲起,直到构建一个可运行的真实世界聊天机器人。本节将为你提供构建神经网络和深度学习知识的基本基础。
本节将包含以下章节:
-
第一章,深度学习与游戏
-
第二章,卷积神经网络与递归神经网络
-
第三章,生成对抗网络与游戏
-
第四章,构建深度学习游戏聊天机器人
第一章:游戏中的深度学习
欢迎来到*《游戏中的深度学习实践》*。本书适用于任何希望以极具实践性的方式学习游戏中的深度学习(DL)的人。值得注意的是,本书讨论的概念不仅仅局限于游戏。我们在这里学到的许多内容将轻松地转移到其他应用或模拟中。
强化学习(RL),这是我们在后续章节中将讨论的核心内容之一,正迅速成为主流的机器学习(ML)技术。它已经应用于从服务器优化到预测零售市场客户活动等方方面面。本书的旅程将主要集中在游戏开发上,我们的目标是构建一个可运行的冒险游戏。请始终记住,您在本书中发现的相同原理也可以应用于其他问题,比如模拟、机器人技术等。
在本章中,我们将从神经网络和深度学习的基本知识开始。我们将讨论神经网络的背景,并逐步构建一个能够玩简单文本游戏的神经网络。具体来说,本章将涉及以下主题:
-
深度学习的过去、现在与未来
-
神经网络 – 基础
-
在TensorFlow(TF)中实现多层感知机
-
理解 TensorFlow
-
使用反向传播训练神经网络
-
在 Keras 中构建自动编码器
本书假设您具备 Python 的基础知识。您应该能够设置并激活虚拟环境。后续章节将使用 Unity 3D,该软件仅限于 Windows 和 macOS(对硬核 Linux 用户表示歉意)。
如果您已经掌握了深度学习,您可能会倾向于跳过这一章。然而,无论如何,这一章非常值得一读,并将为我们在全书中使用的术语奠定基础。至少做一下动手练习——您稍后会感谢自己的!
深度学习的过去、现在与未来
虽然深度学习这一术语最早是由 Igor Aizenberg 及其同事在 2000 年与神经网络相关联的,但它在过去五年中才真正流行开来。在此之前,我们称这种类型的算法为人工神经网络(ANN)。然而,深度学习所指的是比人工神经网络更广泛的内容,涵盖了许多其他领域的互联机器。因此,为了澄清,我们将在本书的后续部分主要讨论 ANN 形式的深度学习。不过,我们也会在第五章中讨论一些可以在游戏中使用的其他形式的深度学习,介绍 DRL。
过去
多层感知器(MLP)网络的第一个形式,或者我们现在称之为人工神经网络(ANN),是由 Alexey Ivakhnenko 于 1965 年提出的。Ivakhnenko 等了好几年才在 1971 年写下关于多层感知器的文章。这个概念花了一些时间才被理解,直到 1980 年代才开始有更多的研究。这一次,尝试了图像分类和语音识别,虽然失败了,但进展已经开始。又过了 10 年,到了 90 年代末,人工神经网络再次流行起来。流行的程度甚至让 ANN 进入了某些游戏,直到更好的方法出现。之后局势平静下来,又过了大约十年。
然后,在 2012 年,Andrew Ng 和 Jeff Dean 使用人工神经网络(ANN)来识别视频中的猫咪,深度学习的兴趣爆发了。他们的进展是若干微不足道(但有趣的)突破之一,使得人们开始关注深度学习。接着,在 2015 年,谷歌的DeepMind团队开发了 AlphaGo,这一次全世界都注意到了。AlphaGo 被证明能够轻松战胜世界上最顶尖的围棋选手,这改变了一切。很快,其他技术也跟进,深度强化学习(DRL)就是其中之一,证明了在以前被认为不可能的领域,人类的表现可以被持续超越。
在教授学生们神经网络时,教授们喜欢分享一个幽默且贴切的故事:美国陆军在 80 年代做过早期研究,使用人工神经网络识别敌方坦克。这个算法 100%有效,陆军还组织了一个大型演示来展示其成功。不幸的是,在演示中什么都没能正常工作,每个测试都惨败。回去分析之后,陆军才意识到这个人工神经网络根本没有识别敌方坦克。相反,它是经过在多云天拍摄的图像进行训练的,它做的只是识别云层。
现在的情况
目前,至少在写作时,我们仍处于深度学习爆炸的中期,充满了碎片和混乱,作为开发者,我们的任务就是理清这一切。神经网络目前是许多深度学习技术的基础,其中几项我们将在本书中讲解。只是,似乎每天都有新的、更强大的技术出现,研究人员争先恐后地去理解它们。实际上,这种思想的激增可能会使一项技术陷入停滞,因为研究人员花费越来越多的时间试图复制结果。这无疑是先前人工神经网络(深度学习)停滞不前的主要原因之一。事实上,行业中许多怀疑者预测,这种情况可能会再次发生。那么,你应该担心吗?读这本书值得吗?简短的回答是值得。长答案是可能不值得,这一次的情况非常不同,许多深度学习概念现在已经在创造收入,这是一个好兆头。深度学习技术现在是经过验证的赚钱工具,这让投资者感到放心,并且鼓励新的投资和增长。究竟增长会有多大还未可知,但机器和深度学习领域现在充满了各行业的机会和增长。
那么,游戏行业是否还可能再次抛弃游戏?这也不太可能,通常是因为许多最近的重要进展,如强化学习,都是为了玩经典的 Atari 游戏而构建的,并以游戏为问题。这只会促使更多的研究通过游戏来进行深度学习。游戏平台 Unity 3D 已经对强化学习在游戏中的应用做出了重大投资。实际上,Unity 正在开发一些最前沿的强化学习技术,我们稍后会与这个平台合作。Unity 确实使用 C#进行脚本编写,但使用 Python 来构建和训练深度学习模型。
未来
预测任何事物的未来都是极其困难的,但如果你足够仔细地观察,可能会对事物的发展方向、发展地点或发展方式有所洞察。当然,拥有一个水晶球或训练有素的神经网络肯定会有所帮助,但许多流行的事物往往依赖于下一个伟大的成就。没有任何预测的能力,我们可以观察到深度学习研究和商业开发中当前的趋势是什么吗?嗯,目前的趋势是使用机器学习(ML)来生成深度学习(DL);也就是说,一台机器基本上会自己组装一个神经网络,解决一个问题。谷歌目前正在大量投资建设一项名为AutoML的技术,它可以生成一个神经网络推理模型,能够识别图像中的物体/活动、语音识别或手写识别等。Geoffery Hinton,通常被誉为人工神经网络的教父,最近展示了复杂的深度网络系统可以被分解成可重用的层。基本上,你可以使用从各种预训练模型中提取的层来构建一个网络。这无疑会发展成更有趣的技术,并且在深度学习的探索中发挥重要作用,同时也为计算的下一个阶段铺平道路。
现在,编程代码在某些时候将变得过于繁琐、困难和昂贵。我们已经能看到这种情况的爆发,许多公司正在寻找最便宜的开发人员。现在估计代码的平均成本是每行$10-$20,是的,每行。那么,开发人员在什么时候会开始以人工神经网络(ANN)或TensorFlow(TF)推理图的形式构建他们的代码呢?嗯,在本书的大部分内容中,我们开发的深度学习(DL)代码将生成到 TF 推理图,也可以说是一个大脑。我们将在书的最后一章使用这些“大脑”来构建我们冒险游戏中的智能。构建图模型的技术正迅速成为主流。许多在线机器学习应用程序现在允许用户通过上传训练内容并按下按钮来构建可以识别图像、语音和视频中的物体的模型。这是否意味着将来应用程序可以这样开发而不需要编程?答案是肯定的,而且这种情况已经在发生。
现在我们已经探索了深度学习的过去、现在和未来,接下来可以开始深入研究更多的术语以及神经网络是如何工作的,下一部分将会展开讨论。
神经网络 – 基础
神经网络或多层感知器的灵感来源于人类的大脑和神经系统。我们神经系统的核心是上图所示的类比计算机的神经元,它就是一个感知器:
人类神经元与感知器的示意图
我们大脑中的神经元会收集输入,进行处理,然后像计算机的 感知器 一样输出响应。感知器接受一组输入,将它们加总,并通过激活函数处理。激活函数决定是否输出,以及在激活时以什么水平输出。让我们仔细看看感知器,具体如下:
感知器
在前面图表的左侧,你可以看到一组输入被推送进来,并加上一个常数偏置。稍后我们会详细讨论这个偏置。然后,输入会被一组单独的权重相乘,并通过激活函数处理。在 Python 代码中,它就像 Chapter_1_1.py
中的那样简单:
inputs = [1,2]
weights = [1,1,1]
def perceptron_predict(inputs, weights):
activation = weights[0]
for i in range(len(inputs)-1):
activation += weights[i] * input
return 1.0 if activation >= 0.0 else 0.0
print(perceptron_predict(inputs,weights))
请注意,weights
列表比 inputs
列表多一个元素;这是为了考虑偏置(weights[0]
)。除此之外,你可以看到我们只是简单地遍历 inputs
,将它们与指定的权重相乘并加上偏置。然后,将 activation
与 0.0
进行比较,如果大于 0,则输出。在这个非常简单的示例中,我们只是将值与 0 进行比较,本质上是一个简单的阶跃函数。稍后我们会花时间多次回顾各种激活函数,可以认为这个简单模型是执行这些函数的基本组成部分。
上述示例代码的输出是什么?看看你能否自己找出答案,或者采取更简单的方式,复制粘贴到你最喜欢的 Python 编辑器中并运行。代码将直接运行,无需任何特殊库。
在前面的代码示例中,我们看到的是一个输入数据点 [1,2]
,但在深度学习中,这样的单一数据点几乎没什么用处。深度学习模型通常需要数百、数千甚至数百万个数据点或数据集来进行有效的训练和学习。幸运的是,通过一个感知器,我们所需的数据量不到 10 个。
让我们扩展前面的例子,并通过打开你喜欢的 Python 编辑器,按照以下步骤将一个包含 10 个点的训练集输入到 perceptron_predict
函数中:
我们将在本书的后续章节中使用 Visual Studio Code 来处理大部分主要的编码部分。当然,你可以使用你喜欢的编辑器,但如果你是 Python 新手,不妨试试这段代码。代码适用于 Windows、macOS 和 Linux。
- 在你喜欢的 Python 编辑器中输入以下代码块,或者打开从下载的源代码中提取的
Chapter_1_2.py
:
train = [[1,2],[2,3],[1,1],[2,2],[3,3],[4,2],[2,5],[5,5],[4,1],[4,4]]
weights = [1,1,1]
def perceptron_predict(inputs, weights):
activation = weights[0]
for i in range(len(inputs)-1):
activation += weights[i+1] * inputs[i]
return 1.0 if activation >= 0.0 else 0.0
for inputs in train:
print(perceptron_predict(inputs,weights))
-
这段代码只是扩展了我们之前看到的例子。在这个例子中,我们正在测试定义在
train
列表中的多个数据点。然后,我们只需遍历列表中的每个项目,并打印出预测值。 -
运行代码并观察输出。如果你不确定如何运行 Python 代码,确保先学习相关课程再继续深入。
你应该看到一个输出,重复显示 1.0,这意味着所有输入值都被识别为相同的。这并不是很有用。原因在于我们没有训练或调整输入权重以匹配已知的输出。我们需要做的是训练这些权重以识别数据,接下来我们将看看如何做到这一点。
在 Python 中训练感知机
完美!我们创建了一个简单的感知机,它接受输入并输出结果,但实际上并没有做任何事情。我们的感知机需要训练它的权重,才能真正发挥作用。幸运的是,有一种已定义的方法,叫做梯度下降,我们可以用它来调整这些权重。重新打开你的 Python 编辑器,更新或输入以下代码,或者从代码下载中打开Chapter_1_3.py
:
def perceptron_predict(inputs, weights):
activation = weights[0]
for i in range(len(inputs)-1):
activation += weights[i + 1] * inputs[i]
return 1.0 if activation >= 0.0 else 0.0
def train_weights(train, learning_rate, epochs):
weights = [0.0 for i in range(len(train[0]))]
for epoch in range(epochs):
sum_error = 0.0
for inputs in train:
prediction = perceptron_predict(inputs, weights)
error = inputs[-1] - prediction
sum_error += error**2
weights[0] = weights[0] + learning_rate * error
for i in range(len(inputs)-1):
weights[i + 1] = weights[i + 1] + learning_rate * error * inputs[i]
print('>epoch=%d, learning_rate=%.3f, error=%.3f' % (epoch, learning_rate, sum_error))
return weights
train = [[1.5,2.5,0],[2.5,3.5,0],[1.0,11.0,1],[2.3,2.3,1],[3.6,3.6,1],[4.2,2.4,0],[2.4,5.4,0],[5.1,5.1,1],[4.3,1.3,0],[4.8,4.8,1]]
learning_rate = 0.1
epochs = 10
weights = train_weights(train, learning_rate, epochs)
print(weights)
train_weights
函数是新的,将用于通过迭代误差最小化来训练感知机,并且为我们在更复杂的网络中使用梯度下降打下基础。这里有很多内容,我们将逐步解析。首先,我们用这一行将weights
列表初始化为0.0
:
weights = [0.0 for i in range(len(train[0]))]
然后我们开始在for
循环中训练每个周期。**周期(epoch)**本质上是通过我们的训练数据的一次传递。之所以需要多次传递,是为了让我们的权重在全局最小值而非局部最小值处收敛。在每个周期中,权重是通过以下方程训练的:
考虑以下内容:
https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-gm/img/d9d34cbd-58a9-4f2a-b685-5c0997c5c4fc.png = https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-gm/img/5937c8c0-55a8-459d-aa32-fe722b51b262.png - https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-gm/img/3ec50e08-1a20-4317-b539-4bb85fc26986.png
偏差以类似的方式进行训练,但只需记住它是weight
。还需要注意的是,我们现在如何在train
列表中标注数据点,结束值为0.0
或1.0
。0.0
表示不匹配,而1.0
表示完全匹配,如下代码片段所示:
train = [[1.5,2.5,0.0],[2.5,3.5,0.0],[1.0,11.0,1.0],[2.3,2.3,1.0],[3.6,3.6,1.0],[4.2,2.4,0.0],[2.4,5.4,0.0],[5.1,5.1,1.0],[4.3,1.3,0.0],[4.8,4.8,1.0]]
这种数据标注在训练神经网络中非常常见,被称为监督训练。我们将在后续章节中探索其他无监督和半监督的训练方法。如果你运行之前的代码,你将看到以下输出:
示例输出来自样本训练运行
现在,如果你有一些机器学习经验,你会立即识别出训练过程中围绕某些局部最小值的波动,这导致我们的训练无法收敛。你可能在深度学习的过程中多次遇到这种波动,所以了解如何解决它是非常有帮助的。
在这种情况下,我们的问题很可能出在激活
函数的选择上,正如你可能记得的那样,它只是一个简单的步进函数。我们可以通过输入一个新的函数来解决这个问题,称为修正线性单元(ReLU)。下图展示了step
和ReLU
函数并排的示例:
步进函数与 ReLU 激活函数的对比
要更改激活函数,请打开之前的代码清单并跟随操作:
- 定位以下代码行:
return 1.0 if activation >= 0.0 else 0.0
- 像这样修改它:
return 1.0 if activation * (activation>0) >= 0.0 else 0.0
-
如果激活函数的值大于 0,则通过自身相乘的微妙差异即为
ReLU
函数的实现。是的,它看起来就这么简单。 -
运行代码并观察输出的变化。
当你运行代码时,数值迅速收敛并保持稳定。这是我们训练的巨大进步,也是将激活函数更改为ReLU
的原因。原因在于,现在我们的感知器权重可以更慢地收敛到全局最大值,而之前使用step
函数时,它们仅在局部最小值附近波动。接下来,我们将在本书的过程中测试许多其他激活函数。在下一节中,我们将讨论当我们开始将感知器组合成多个层时,情况变得更加复杂。
TensorFlow 中的多层感知器
到目前为止,我们一直在看一个简单的单一感知器示例以及如何训练它。这对我们的小数据集来说很有效,但随着输入数量的增加,网络的复杂性也在增加,这也反映在数学计算上。下图展示了一个多层感知器,或者我们通常所称的 ANN:
多层感知器或 ANN
在图示中,我们看到一个具有一个输入层、一个隐藏层和一个输出层的网络。输入现在通过一个神经元输入层共享。第一层神经元处理输入,并将结果输出到隐藏层进行处理,依此类推,直到最终到达输出层。
多层网络可能变得相当复杂,这些模型的代码通常通过高级接口如 Keras、PyTorch 等进行抽象化。这些工具非常适合快速探索网络架构和理解深度学习概念。然而,当涉及到性能时,尤其是在游戏中,确实需要在 TensorFlow 或支持低级数学运算的 API 中构建模型。在本书中,我们将在引导性的深度学习章节中从 Keras(一个高级 SDK)切换到 TensorFlow,并来回切换。这将帮助你看到使用这两种接口时的差异与相似之处。
Unity ML-Agents 最初是使用 Keras 原型开发的,但后来已迁移到 TensorFlow。毫无疑问,Unity 团队以及其他团队这样做是出于性能和某种程度上的控制考虑。与 TensorFlow 一起工作类似于编写你自己的着色器。虽然编写着色器和 TensorFlow 代码都非常困难,但定制你自己的渲染和现在的学习能力将使你的游戏独一无二,脱颖而出。
接下来有一个非常好的 TensorFlow 多层感知机示例,供你参考,列出在 Chapter_1_4.py
中。为了使用 TensorFlow 运行此代码,请按照以下步骤操作:
在下一节之前,我们不会涉及 TensorFlow 的基础知识。这是为了让你先看到 TensorFlow 的实际应用,再避免因细节让你感到乏味。
- 首先,通过以下命令在 Windows 或 macOS 上的 Python 3.5/3.6 环境中安装 TensorFlow。你也可以使用带有管理员权限的 Anaconda Prompt:
pip install tensorflow
OR
conda install tensorflow //using Anaconda
- 确保安装适用于默认 Python 环境的 TensorFlow。我们稍后会关注创建更结构化的虚拟环境。如果你不确定什么是 Python 虚拟环境,建议先离开书本,立即参加 Python 课程。
在这个练习中,我们正在加载 MNIST 手写数字数据库。如果你读过任何关于机器学习(ML)和深度学习(DL)的资料,你很可能已经见过或听说过这个数据集。如果没有,只需快速 Google MNIST,就可以了解这些数字的样子。
- 以下的 Python 代码来自
Chapter_1_4.py
列表,每个部分将在接下来的步骤中解释:
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("/tmp/data/", one_hot=True)
- 我们首先加载
mnist
训练集。mnist
数据集包含了 28 x 28 像素的图像,显示了手绘的 0-9 数字,或者我们称之为 10 个类别:
import tensorflow as tf
# Parameters
learning_rate = 0.001
training_epochs = 15
batch_size = 100
display_step = 1
# Network Parameters
n_hidden_1 = 256 # 1st layer number of neurons
n_hidden_2 = 256 # 2nd layer number of neurons
n_input = 784 # MNIST data input (img shape: 28*28)
n_classes = 10 # MNIST total classes (0-9 digits)
- 然后我们导入
tensorflow
库并指定为tf
。接着,我们设置一些稍后将使用的参数。注意我们如何定义输入和隐藏层参数:
# tf Graph input
X = tf.placeholder("float", [None, n_input])
Y = tf.placeholder("float", [None, n_classes])
# Store layers weight & bias
weights = {
'h1': tf.Variable(tf.random_normal([n_input, n_hidden_1])),
'h2': tf.Variable(tf.random_normal([n_hidden_1, n_hidden_2])),
'out': tf.Variable(tf.random_normal([n_hidden_2, n_classes]))
}
biases = {
'b1': tf.Variable(tf.random_normal([n_hidden_1])),
'b2': tf.Variable(tf.random_normal([n_hidden_2])),
'out': tf.Variable(tf.random_normal([n_classes]))
}
- 接下来,我们使用
tf.placeholder
设置一些 TensorFlow 占位符,用来存储输入数量和类别,数据类型为'float'
。然后,我们使用tf.Variable
创建并初始化变量,首先是权重,再是偏置。在变量声明中,我们使用tf.random_normal
初始化正态分布的数据,将其填充到一个二维矩阵或张量中,矩阵的维度等于n_input
和n_hidden_1
,从而填充一个随机分布的数据张量:
# Create model
def multilayer_perceptron(x):
# Hidden fully connected layer with 256 neurons
layer_1 = tf.add(tf.matmul(x, weights['h1']), biases['b1'])
# Hidden fully connected layer with 256 neurons
layer_2 = tf.add(tf.matmul(layer_1, weights['h2']), biases['b2'])
# Output fully connected layer with a neuron for each class
out_layer = tf.matmul(layer_2, weights['out']) + biases['out']
return out_layer
# Construct model
logits = multilayer_perceptron(X)
- 然后我们通过对每一层操作的权重和偏置进行乘法运算来创建模型。我们在这里做的基本上是将我们的激活方程转化为一个矩阵/张量方程。现在,我们不再进行单次传递,而是通过矩阵/张量乘法在一次操作中进行多次传递。这使得我们可以一次性处理多个训练图像或数据集,这是我们用来更好地推广学习的技术。
对于我们神经网络中的每一层,我们使用tf.add
和tf.matmul
将矩阵乘法操作加入到我们通常所说的TensorFlow 推理图中。从我们创建的代码中可以看到,我们的模型有两个隐藏层和一个输出层:
# Define loss and optimizer
loss_op = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=Y))
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)
train_op = optimizer.minimize(loss_op)
- 接下来,我们定义一个
loss
函数和优化器。loss_op
用于计算网络的总损失。然后,AdamOptimizer
根据loss
或cost
函数进行优化。我们稍后会详细解释这些术语,因此如果现在不太明白也不用担心:
# Initializing the variables
init = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)
# Training cycle
for epoch in range(training_epochs):
avg_cost = 0.
total_batch = int(mnist.train.num_examples/batch_size)
# Loop over all batches
for i in range(total_batch):
batch_x, batch_y = mnist.train.next_batch(batch_size)
# Run optimization op (backprop) and cost op (to get loss value)
_, c = sess.run([train_op, loss_op], feed_dict={X: batch_x,Y: batch_y})
# Compute average loss
avg_cost += c / total_batch
- 然后我们通过创建一个新会话并运行它来初始化一个新的 TensorFlow 会话。我们再次使用周期性迭代训练方法,对每一批图像进行循环训练。记住,一整个图像批次会同时通过网络,而不仅仅是一张图像。然后,我们在每个周期中循环遍历每一批图像,并优化(反向传播和训练)成本,或者说,最小化成本:
# Display logs per epoch step
if epoch % display_step == 0:
print("Epoch:", '%04d' % (epoch+1), "cost={:.9f}".format(avg_cost))
print("Optimization Finished!")
- 然后,我们输出每个周期运行的结果,展示网络如何最小化误差:
# Test model
pred = tf.nn.softmax(logits) # Apply softmax to logits
correct_prediction = tf.equal(tf.argmax(pred, 1), tf.argmax(Y, 1))
- 接下来,我们实际运行前面的代码进行预测,并使用我们之前选择的优化器来确定
logits
模型中正确值的百分比:
# Calculate accuracy
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
print("Accuracy:", accuracy.eval({X: mnist.test.images, Y: mnist.test.labels}))
- 最后,我们计算并输出模型的
accuracy
。如果你运行这个练习,不要只是关注模型的准确度,还要思考如何提高准确度的方法。
在前面的参考示例中有很多内容,我们将在接下来的章节中进一步讲解。希望你此时已经能够看到事情变得复杂的程度。这也是为什么在本书的大部分基础章节中,我们会先通过 Keras 讲解概念。Keras 是一个强大且简单的框架,能够帮助我们迅速构建复杂的网络,同时也使我们教学和学习变得更加简单。我们还将提供在 TensorFlow 中开发的重复示例,并在书中进展过程中展示一些关键的差异。
在下一节中,我们将解释 TensorFlow 的基本概念,它是什么,以及我们如何使用它。
TensorFlow 基础
TensorFlow(TF)正在迅速成为许多深度学习应用的核心技术。虽然还有像 Theano 这样的其他 API,但它是最受关注的,且对我们来说最适用。像 Keras 这样的高层框架提供了部署 TF 或 Theano 模型的能力。例如,这对于原型设计和快速验证概念非常有帮助,但作为游戏开发者,你知道,对于游戏而言,最重要的要求总是性能和控制。TF 比任何高层框架(如 Keras)提供更好的性能和更多的控制。换句话说,要成为一名认真的深度学习开发者,你很可能需要并且希望学习 TF。
正如其名所示,TF(TensorFlow)就是围绕张量展开的。张量是一个数学概念,描述的是一个在n维度中组织的数据集合,其中n可以是 1、2x2、4x4x4,等等。一个一维张量将描述一个单一的数字,例如https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-gm/img/6fa7dde2-1b00-44af-8f4c-041e252a6be9.png,一个 2x2 张量将是https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-gm/img/7077b58d-f2f0-4f9e-843e-fe60c0e44cfe.png,或者你可能称之为矩阵。一个 3x3x3 的张量将描述一个立方体形状。本质上,你可以将任何应用于矩阵的操作应用于张量,而且在 TF 中,一切都是张量。当你刚开始使用张量时,作为一个有游戏开发背景的人,将它们视为矩阵或向量是很有帮助的。
张量不过是多维数组、向量或矩阵,许多示例展示在下面的图表中:
多种形式的张量(占位符)
让我们回到Chapter_1_4.py
并按照接下来的步骤操作,以更好地理解 TF 示例是如何运行的:
- 首先,再次检查顶部部分,特别注意占位符和变量的声明;这在下面的代码片段中再次展示:
tf.placeholder("float", [None, n_input])
...
tf.Variable(tf.random_normal([n_input, n_hidden_1]))
-
placeholder
用于定义输入和输出张量。Variable
设置一个变量张量,可以在 TF 会话或程序执行时进行操作。在这个例子中,一个名为random_normal
的辅助方法将隐藏权重填充为正态分布的数据集。还有其他类似的辅助方法可以使用;有关更多信息,请查看文档。 -
接下来,我们构建
logits
模型,它是一个名为multilayer_perceptron
的函数,如下所示:
def multilayer_perceptron(x):
layer_1 = tf.add(tf.matmul(x, weights['h1']), biases['b1'])
layer_2 = tf.add(tf.matmul(layer_1, weights['h2']), biases['b2'])
out_layer = tf.matmul(layer_2, weights['out']) + biases['out']
return out_layer
logits = multilayer_perceptron(X)
-
在函数内部,我们看到定义了三个网络层,其中两个是输入层,一个是输出层。每个层使用
add
或+
函数来将matmul(x, weights['h1'])
和biases['b1']
的结果相加。Matmul
做的是每个权重与输入x的简单矩阵乘法。回想一下我们第一个例子中的感知机;这就像将所有权重乘以输入,然后加上偏差。注意,结果张量(layer_1, layer_2)
作为输入传递到下一个层。 -
跳到第 50 行左右,注意我们是如何获取
loss
、optimizer
和initialization
函数的引用:
loss_op = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=Y))
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)
train_op = optimizer.minimize(loss_op)
init = tf.global_variables_initializer()
-
重要的是要理解,我们这里只是存储了对函数的引用,并没有立即执行它们。损失和优化器函数已经详细讨论过了,但也要特别注意
global_variables_initializer()
函数。这个函数是所有变量初始化的地方,我们必须先运行这个函数。 -
接下来,滚动到会话初始化和启动的开始,如下所示:
with tf.Session() as sess:
sess.run(init)
-
我们在 TF 中构建
Session
,作为执行的容器或所谓的图。这个数学图描述了节点和连接,与我们模拟的网络有相似之处。TF 中的所有操作都需要在一个 session 内进行。然后我们运行第一个函数init
,通过run
来执行。 -
正如我们已经详细介绍了训练过程,接下来我们将查看的下一个元素是通过以下代码执行的下一个函数
run
:
_, c = sess.run([train_op, loss_op], feed_dict={X: batch_x,Y: batch_y})
run
函数中发生了很多事情。我们通过当前的feed_dict
字典作为输入,将训练和损失函数train_op
和loss_op
作为一组输入。结果输出值c
等于总成本。请注意,输入函数集被定义为train_op
然后是loss_op
。在这种情况下,顺序定义为train
/loss
,但如果你选择,也可以将其反转。你还需要反转输出值,因为输出顺序与输入顺序相匹配。
代码的其余部分已经在某种程度上定义了,但理解在使用 TF 构建模型时的一些关键区别还是很重要的。如你所见,现在我们已经能够相对快速地构建复杂的神经网络。然而,我们仍然缺少一些在构建更复杂的网络时将会非常有用的关键知识。我们缺少的正是训练神经网络时使用的基础数学,这部分内容将在下一节进行探讨。
使用反向传播训练神经网络
计算神经元的激活、前向传播,或者我们称之为 前馈传播,是相当简单的过程。我们现在遇到的复杂性是将误差反向传播通过网络。当我们现在训练网络时,我们从最后的输出层开始,确定总误差,就像我们在单个感知机中做的那样,但现在我们需要总结所有输出层的误差。然后我们需要使用这个值将误差反向传播回网络,通过每个权重更新它们,基于它们对总误差的贡献。理解一个在成千上万甚至数百万个权重的网络中,单个权重的贡献可能会非常复杂,幸运的是,通过微分和链式法则的帮助,这一过程变得可行。在我们进入复杂的数学之前,我们需要先讨论 Cost
函数以及如何计算误差,下一节会详细讲解。
虽然反向传播的数学原理很复杂,可能让人感到有些困难,但总有一天,你会希望或者需要理解它。尽管如此,对于本书的目的,你可以忽略这一部分,或者根据需要再回顾这部分内容。我们在后面的章节中开发的所有网络都会自动为我们处理反向传播。当然,你也不能完全避免数学的部分;它是深度学习中无处不在的。
成本函数
Cost
函数描述了我们整个网络中一个批次的误差的平均和,通常由以下方程定义:
输入定义为每个权重,输出是我们在处理的批次中遇到的总平均成本。可以将这个成本看作是误差的平均和。现在,我们的目标是将这个函数或误差成本最小化到尽可能低的值。在前面几个示例中,我们已经看到一种名为梯度下降的技术被用来最小化这个成本函数。梯度下降的原理是通过对Cost
函数进行微分,并确定相对于每个权重的梯度。然后,对于每个权重,或者说每个维度,算法会根据计算出的梯度调整权重,以最小化Cost
函数。
在我们深入讨论解释微分的复杂数学之前,先来看一下梯度下降在二维空间中的工作原理,参考下图:
梯度下降找到全局最小值的示例
简单来说,算法所做的就是通过缓慢渐进的步骤寻找最小值。我们使用小步伐来避免超过最小值,正如你之前看到的那样,这种情况可能会发生(记住“摆动”现象)。这时学习率的概念就出现了,它决定了我们训练的速度。训练越慢,你对结果的信心就越高,但通常会以时间为代价。另一种选择是通过提高学习率来加快训练速度,但正如你现在看到的,可能很容易超过任何全局最小值。
梯度下降是我们将要讨论的最简单形式,但请记住,还有许多其他优化算法的高级变体,我们将进一步探讨。例如,在 TF 示例中,我们使用了AdamOptimizer
来最小化Cost
函数,但还有许多其他变体。不过,暂时我们将专注于如何计算Cost
函数的梯度,并在下一节中了解梯度下降的反向传播基础。
偏导数和链式法则
在我们深入计算每个权重的细节之前,让我们稍微回顾一下微积分和微分。如果你还记得你最喜欢的数学课——微积分,你可以通过微分来确定函数中任何一点的变化斜率。下面的图示是一个微积分复习:
基本微积分方程的回顾
在图中,我们有一个非线性函数f,它描述了蓝色线条的方程。通过对 f’进行微分并求解,我们可以确定任何一点的斜率(变化率)。回想一下,我们也可以利用这个新函数来确定局部和全局的最小值或最大值,正如图中所示。简单的微分使我们能够求解一个变量,但我们需要求解多个权重,因此我们将使用偏导数或对一个变量进行微分。
如你所记得的,偏微分使我们能够对一个变量进行求导,同时将其他变量视为常数。让我们回到我们的 Cost
函数,看看如何对一个单一的权重进行求导:
- https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-gm/img/540eed90-acc5-4e90-9e5f-5a4a2da7a90f.png 是我们描述的成本函数,表达如下:
- 我们可以通过如下方式,对这个函数进行关于单一变量权重的求导:
- 如果我们将所有这些偏导数合并在一起,我们就得到了我们的
Cost
函数的向量梯度, https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-gm/img/0fb9abc8-223f-4fa3-96a3-b025e13774ac.png,表示为如下:
- 这个梯度定义了我们希望取反并用来最小化
Cost
函数的向量方向。在我们之前的例子中,这个向量包含了超过 13,000 个分量。这些分量对应着网络中需要优化的超过 13,000 个权重。为了计算梯度,我们需要结合很多偏导数。幸运的是,微积分中的链式法则能够帮助我们简化这个数学过程。回想一下,链式法则的定义如下:
- 这使得我们能够使用链式法则为单一权重定义梯度,如下所示:
- 这里,https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-gm/img/53d1990c-21bb-4d13-97a4-d69c40d87558.png 表示输入值,而 https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-gm/img/d7bf0016-5adf-4f73-88c6-a93ebacf1caa.png 表示神经元位置。注意,我们现在需要对给定神经元的激活函数 a 进行偏导数,这一过程可以通过以下方式总结:
上标符号 https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-gm/img/674a1fc1-17c6-4627-a679-e4a020bc511c.png 表示当前层,而 https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-gm/img/7768eb39-9ec9-40af-b61a-bd226e3544ca.png 表示上一层。https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-gm/img/b73cb21f-571d-4923-a76b-f6227090187d.png 表示来自上一层的输入或输出。https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-gm/img/5b84bf4a-5db8-43b0-bcb0-d8a9531484ee.png 表示激活函数,回想一下我们之前使用过 Step
和 ReLU
函数来扮演这个角色。
- 然后,我们对这个函数进行偏导数,如下所示:
为了方便起见,我们定义如下:
- 在这一点上,事情可能看起来比实际情况复杂得多。试着理解符号的所有细微之处,记住我们现在所看的本质上是激活函数对
Cost
函数的偏导数。额外的符号仅仅是帮助我们索引各个权重、神经元和层。然后我们可以将其表达为如下:
- 再次强调,我们所做的只是为 https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-gm/img/625ab8a1-2e70-4937-95b8-1ac620c209c5.png^(th) 输入、https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-gm/img/aa2eb4de-35af-441a-b21a-eedb4d796372.png^(th) 神经元和层 https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-gm/img/3fb99529-b043-4057-8400-e4d5924454df.png的权重定义梯度 (https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-gm/img/c7be7304-2015-4fb0-bce6-d80275794496.png)。结合梯度下降,我们需要使用前面的基础公式将调整反向传播到权重中。对于输出层(最后一层),这现在可以总结如下:
- 对于内部或隐藏层,方程式的结果如下:
- 经过一些替换和对一般方程的操作,我们最终得到了这个结果:
在这里, f’ 表示激活函数的导数。
上面的方程式使我们能够运行网络并通过以下过程进行误差反向传播:
-
首先,你需要计算每一层的激活值 https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-gm/img/12a18170-2428-4904-bbc5-71fcfc00cffd.png 和 https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-gm/img/c475dd78-297d-41ea-99f8-582ae07a5009.png,从输入层开始并向前传播。
-
然后我们使用https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-gm/img/abb13af1-a57e-435b-95b2-e8bd980a33ab.png在输出层评估该项 https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-gm/img/4cad7f38-ff20-4529-a65a-046d1ca4dd6d.png。
-
我们通过使用余项来评估每一层,使用 https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-gm/img/3a68e2e9-7e67-4bdc-9919-75611c86fe29.png,从输出层开始并向后传播。
-
再次强调,我们使用偏导数 https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-gm/img/3fcd1213-41e8-4d5d-b7b9-2a737e0b210c.png 来获得每一层所需的导数。
你可能需要多读几遍这一部分才能理解所有概念。另一个有用的做法是运行前面的示例并观察训练过程,尝试想象每个权重是如何被更新的。我们还没有完全完成这里的内容,接下来还有几个步骤——使用自动微分就是其中之一。除非你在开发自己的低级网络,否则仅仅对这些数学知识有基本的理解就能帮助你更好地理解训练神经网络的需求。在下一部分,我们将回到一些更基本的操作,并通过构建神经网络代理将我们的新知识付诸实践。
学习不应也很可能不应完全依赖于同一个来源。务必将学习扩展到其他书籍、视频和课程。这样不仅能让你在学习上更为成功,还能在过程中获得更多理解。
使用 Keras 构建自动编码器
尽管我们已经涵盖了理解深度学习所需的许多重要内容,但我们还没有构建出能够真正执行任何操作的东西。当我们开始学习深度学习时,第一个要解决的问题之一就是构建自编码器来编码和重构数据。通过完成这个练习,我们可以确认网络中的输入也能从网络中输出,基本上能让我们确信人工神经网络并不是一个完全的黑箱。构建和使用自编码器还允许我们调整和测试各种参数,以便理解它们的作用。让我们开始吧,首先打开 Chapter_1_5.py
文件,并按照以下步骤操作:
- 我们将逐节讲解这个代码。首先,我们输入基础层
Input
和Dense
,然后是Model
,这些都来自tensorflow.keras
模块,具体的导入如下:
from tensorflow.keras.layers import Input, Dense
from tensorflow.keras.models import Model
- 我们在 Keras 中定义深度学习模型时,使用的是层或神经元,而非单独的神经元。
Input
和Dense
层是我们最常用的层,但我们也会看到其他层。如其名称所示,Input
层处理输入,而Dense
层则是典型的完全连接神经元层,我们之前已经看过这一点。
我们在这里使用的是嵌入式版本的 Keras。原始示例来自 Keras 博客,并已转换为 TensorFlow。
- 接下来,我们通过以下代码设置
encoding
维度的数量:
encoding_dim = 32
- 这是我们希望将样本降维到的维度数量。在这种情况下,它只是 32,即对一个有 784 输入维度的图像进行大约 24 倍的压缩。记住,我们得到
784
输入维度是因为我们的输入图像是 28 x 28,并且我们将其展平为一个长度为 784 的向量,每个像素代表一个单独的值或维度。接下来,我们使用以下代码设置具有 784 输入维度的Input
层:
input_img = Input(shape=(784,))
- 那一行代码创建了一个形状为 784 输入的
Input
层。然后,我们将使用以下代码将这 784 个维度编码到我们的下一个Dense
层:
encoded = Dense(encoding_dim, activation='ReLU')(input_img)
encoder = Model(input_img, encoded)
-
上述代码简单地创建了我们完全连接的隐藏层(
Dense
),包含 32 个(encoding_dim
)神经元,并构建了编码器。可以看到,input_img
,即Input
层,被用作输入,我们的激活函数是ReLU
。下一行使用Input
层(input_img
)和Dense
(encoded
)层构建了一个Model
。通过这两层,我们将图像从 784 维编码到 32 维。 -
接下来,我们需要使用更多的层来解码图像,代码如下:
decoded = Dense(784, activation='sigmoid')(encoded)
autoencoder = Model(input_img, decoded)
encoded_input = Input(shape=(encoding_dim,))
decoder_layer = autoencoder.layers[-1]
decoder = Model(encoded_input, decoder_layer(encoded_input))
autoencoder.compile(optimizer='adadelta', loss='binary_crossentropy')
-
下一组层和模型将用于将图像解码回 784 维度。底部的最后一行代码是我们使用
adadelta
优化器调用来编译autoencoder
模型,并使用binary_crossentropy
作为loss
函数。我们稍后会更多地讨论损失函数和优化参数的类型,但现在只需要注意,当我们编译一个模型时,实际上就是在为反向传播和优化算法设置模型。记住,所有这些操作都是自动完成的,我们无需处理这些复杂的数学问题。 -
这设置了我们模型的主要部分,包括编码器、解码器和完整的自动编码器模型,我们随后会编译这些模型以便进行训练。在接下来的部分,我们将处理模型的训练和预测。
训练模型
接下来,我们需要用一组数据样本来训练我们的模型。我们将再次使用 MNIST 手写数字数据集;这个数据集既简单、免费又方便。回到代码列表,按照以下步骤继续练习:
- 从我们上次停下的地方继续,找到以下代码段:
from tensorflow.keras.datasets import mnist import numpy as np (x_train, _), (x_test, _) = mnist.load_data()
-
我们首先导入
mnist
库和numpy
,然后将数据加载到x_train
和x_test
数据集。作为数据科学和机器学习中的一般规则,通常我们会使用一个训练集来进行学习,然后使用评估集来进行测试。这些数据集通常通过随机拆分数据来生成,通常是将80%
用于训练,20%
用于测试。 -
然后我们使用以下代码进一步定义我们的训练和测试输入:
x_train = x_train.astype('float32') / 255. x_test = x_test.astype('float32') / 255. x_train = x_train.reshape((len(x_train), np.prod(x_train.shape[1:]))) x_test = x_test.reshape((len(x_test), np.prod(x_test.shape[1:]))) print( x_train.shape) print( x_test.shape)
-
前两行代码将我们的输入灰度像素颜色值从
0
到255
进行归一化,方法是除以255
。这会将值转换为从0
到1
的范围。通常,我们希望尝试对输入数据进行归一化处理。接下来,我们将训练和测试集重塑为输入的Tensor
。 -
模型都已经构建并编译好,现在是时候开始训练了。接下来的几行代码将是网络学习如何编码和解码图像的部分:
autoencoder.fit(x_train, x_train, epochs=50, batch_size=256, shuffle=True, validation_data=(x_test, x_test)) encoded_imgs = encoder.predict(x_test) decoded_imgs = decoder.predict(encoded_imgs)
- 你可以看到我们的代码中,我们正在设置使用
x_train
作为输入和输出来拟合数据。我们使用50
个epochs
和256
张图像的batch size
。稍后可以自行调整这些参数,看看它们对训练的影响。之后,encoder
和decoder
模型被用来预测测试图像。
这完成了我们为这个模型(或者如果你愿意的话,多个模型)所需的模型和训练设置。记住,我们正在将一个 28 x 28 的图像解压缩为本质上的 32 个数字,然后使用神经网络重建图像。现在我们的模型已经完成并训练好了,接下来我们将回顾输出结果,下一节我们将进行这个操作。
检查输出
我们这次的最后一步是查看图像实际发生了什么。我们将通过输出一小部分图像来获得我们的成功率,完成这个练习。继续下一个练习,完成代码并运行自编码器:
- 继续上一个练习,找到以下代码的最后一部分:
import matplotlib.pyplot as plt n = 10 # how many digits we will display plt.figure(figsize=(20, 4)) for i in range(n): # display original ax = plt.subplot(2, n, i + 1) plt.imshow(x_test[i].reshape(28, 28)) plt.gray() ax.get_xaxis().set_visible(False) ax.get_yaxis().set_visible(False) # display reconstruction ax = plt.subplot(2, n, i + 1 + n) plt.imshow(decoded_imgs[i].reshape(28, 28)) plt.gray() ax.get_xaxis().set_visible(False) ax.get_yaxis().set_visible(False) plt.show()
-
在这段代码中,我们只是输出所有训练完成后输入和结果自编码图像。代码从导入
mathplotlib
开始,用于绘图,然后我们循环遍历多张图像来显示结果。其余的代码只是输出图像。 -
像往常一样运行 Python 代码,这次预计训练会花费几分钟。完成后,你应该会看到一张类似于下面的图片:https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-gm/img/57c7b3de-945e-42fb-af0a-cf5258f7f7f5.png
原始输入图像与编码和解码后的输出图像的示例
这就是我们构建一个简单的 Keras 模型的过程,该模型可以对图像进行编码然后解码。通过这个过程,我们可以看到多层神经网络的每个小部分是如何用 Keras 函数编写的。在最后一节中,我们邀请你,读者,进行一些额外的练习以便进一步学习。
练习
使用这些额外的练习来帮助你学习并进一步测试你的知识。
回答以下问题:
-
列举三种不同的激活函数。记住,Google 是你的好朋友。
-
偏置的作用是什么?
-
如果你减少了某个章节示例中的 epoch 数量,你预期会发生什么?你试过了吗?
-
反向传播的作用是什么?
-
解释代价函数的作用。
-
在 Keras 自编码器示例中,增加或减少编码维度的数量会发生什么情况?
-
我们输入的层类型叫什么名字?
-
增加或减少批量大小会发生什么?
-
Keras 示例中输入的
Tensor
形状是什么?提示:我们已经有一个打印语句显示了这一点。 -
在上一个练习中,我们使用了多少个 MNIST 样本进行训练和测试?
随着我们在书中的进展,额外的练习肯定会变得更加困难。不过现在,花些时间回答这些问题并测试你的知识。
总结
在本章中,我们探讨了深度学习的基础,从简单的单层感知器到更复杂的多层感知器模型。我们从深度学习的过去、现在和未来开始,从那里构建了一个基本的单层感知器参考实现,以帮助我们理解深度学习的基本简单性。接着,我们通过将多个感知器添加到一个多层实现中,进一步扩展了我们的知识,使用了 TensorFlow(TF)。使用 TF 使我们能够看到如何表示和训练一个原始内部模型,并用一个更复杂的数据集 MNIST 进行训练。然后,我们经历了数学的长篇旅程,尽管很多复杂的数学被 Keras 抽象化了,我们还是深入探讨了梯度下降和反向传播的工作原理。最后,我们通过 Keras 的另一个参考实现结束了这一章,其中包括了一个自动编码器。自动编码让我们能够训练一个具有多重用途的网络,并扩展了我们对网络架构不必线性的理解。
在下一章中,我们将在现有知识的基础上,探索卷积神经网络和循环神经网络。这些扩展为神经网络的基础形式提供了额外的功能,并在我们最近的深度学习进展中发挥了重要作用。
在下一章中,我们将开始构建游戏组件的旅程,探索被认为是深度学习(DL)基础的另一个元素——生成对抗网络(GAN)。GAN 在深度学习中就像是一把瑞士军刀,正如我们将在下一章中看到的那样,它为我们提供了很多用途。
第二章:卷积与递归网络
人脑通常是我们在构建 AI 时的主要灵感来源和比较对象,深度学习研究人员经常从大脑中寻找灵感或获得确认。通过更详细地研究大脑及其各个部分,我们经常发现神经子过程。一个神经子过程的例子是我们的视觉皮层,这是大脑中负责视觉的区域或部分。我们现在了解到,大脑的这个区域的连接方式与反应输入的方式是不同的。这正好与我们之前在使用神经网络进行图像分类时的发现相似。现在,人脑有许多子过程,每个子过程在大脑中都有特定的映射区域(视觉、听觉、嗅觉、语言、味觉、触觉以及记忆/时间性),但在本章中,我们将探讨如何通过使用高级形式的深度学习来模拟视觉和记忆,这些高级形式被称为卷积神经网络和递归网络。视觉和记忆这两个核心子过程在许多任务中被广泛应用,包括游戏,它们也成为了许多深度学习研究的重点。
研究人员常常从大脑中寻找灵感,但他们构建的计算机模型通常与生物大脑的对应结构并不完全相似。然而,研究人员已经开始识别出大脑内几乎完美对应于神经网络的类比。例如,ReLU 激活函数就是其中之一。最近发现,我们大脑中神经元的兴奋程度绘制出来后,与 ReLU 图形完全匹配。
在本章中,我们将详细探讨卷积神经网络和递归神经网络。我们将研究它们如何解决在深度学习中复制准确视觉和记忆的问题。这两种新的网络或层类型是相对较新的发现,但它们在某些方面促进了深度学习的诸多进展。本章将涵盖以下主题:
-
卷积神经网络
-
理解卷积
-
构建自驾 CNN
-
记忆和递归网络
-
用 LSTM 玩石头剪子布
在继续之前,确保你已经较好地理解了前一章中概述的基本内容。这包括运行代码示例,安装本章所需的依赖项。
卷积神经网络
视觉是最常用的子过程。你现在就正在使用它!当然,研究人员早期尝试通过神经网络来模拟这一过程,然而直到引入卷积的概念并用于图像分类,才真正有效。卷积的概念是检测、分组和隔离图像中常见特征的想法。例如,如果你遮住了一张熟悉物体的图片的四分之三,然后展示给某人,他们几乎肯定能通过识别部分特征来认出这张图。卷积也以同样的方式工作,它会放大图像然后隔离特征,供之后识别使用。
卷积通过将图像分解成其特征部分来工作,这使得训练网络变得更加容易。让我们进入一个代码示例,该示例基于上一章的内容,并且现在引入了卷积。打开Chapter_2_1.py
文件并按照以下步骤操作:
- 看一下导入部分的前几行:
import tensorflow as tf
from tensorflow.keras.layers import Input, Dense, Conv2D, MaxPooling2D, UpSampling2D
from tensorflow.keras.models import Model
from tensorflow.keras import backend as K
-
在这个示例中,我们导入了新的层类型:
Conv2D
、MaxPooling2D
和UpSampling2D
。 -
然后我们设置
Input
并使用以下代码构建编码和解码网络部分:
input_img = Input(shape=(28, 28, 1)) # adapt this if using `channels_first` image data format
x = Conv2D(16, (3, 3), activation='relu', padding='same')(input_img)
x = MaxPooling2D((2, 2), padding='same')(x)
x = Conv2D(8, (3, 3), activation='relu', padding='same')(x)
x = MaxPooling2D((2, 2), padding='same')(x)
x = Conv2D(8, (3, 3), activation='relu', padding='same')(x)
encoded = MaxPooling2D((2, 2), padding='same')(x)
x = Conv2D(8, (3, 3), activation='relu', padding='same')(encoded)
x = UpSampling2D((2, 2))(x)
x = Conv2D(8, (3, 3), activation='relu', padding='same')(x)
x = UpSampling2D((2, 2))(x)
x = Conv2D(16, (3, 3), activation='relu')(x)
x = UpSampling2D((2, 2))(x)
decoded = Conv2D(1, (3, 3), activation='sigmoid', padding='same')(x)
- 首先需要注意的是,我们现在保持图像的维度,在这个例子中是 28 x 28 像素宽,且只有 1 层或 1 个通道。这个示例使用的是灰度图像,所以只有一个颜色通道。这与之前完全不同,以前我们只是将图像展开成一个 784 维的向量。
第二点需要注意的是使用了Conv2D
层(即二维卷积层)和随后的MaxPooling2D
或UpSampling2D
层。池化层或采样层用于收集或反过来解开特征。注意我们如何在图像编码后使用池化或下采样层,在解码图像时使用上采样层。
- 接下来,我们使用以下代码块构建并训练模型:
autoencoder = Model(input_img, decoded)
autoencoder.compile(optimizer='adadelta', loss='binary_crossentropy')
from tensorflow.keras.datasets import mnist
import numpy as np
(x_train, _), (x_test, _) = mnist.load_data()
x_train = x_train.astype('float32') / 255.
x_test = x_test.astype('float32') / 255.
x_train = np.reshape(x_train, (len(x_train), 28, 28, 1))
x_test = np.reshape(x_test, (len(x_test), 28, 28, 1))
from tensorflow.keras.callbacks import TensorBoard
autoencoder.fit(x_train, x_train,
epochs=50,
batch_size=128,
shuffle=True,
validation_data=(x_test, x_test),
callbacks=[TensorBoard(log_dir='/tmp/autoencoder')])
decoded_imgs = autoencoder.predict(x_test)
-
上面代码中的模型训练与上一章末尾我们所做的相似,但现在请注意训练集和测试集的选择。我们不再压缩图像,而是保持其空间属性作为卷积层的输入。
-
最后,我们通过以下代码输出结果:
n = 10
plt.figure(figsize=(20, 4))
for i in range(n):
ax = plt.subplot(2, n, i)
plt.imshow(x_test[i].reshape(28, 28))
plt.gray()
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
ax = plt.subplot(2, n, i + n)
plt.imshow(decoded_imgs[i].reshape(28, 28))
plt.gray()
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
plt.show()
- 如你之前所做,运行代码,你会立刻注意到训练速度大约慢了 100 倍。这可能需要你等待,具体取决于你的机器;如果需要等待,去拿一杯饮料或三杯,或者来一顿饭吧。
现在,训练我们的简单示例需要大量时间,这在旧硬件上可能会非常明显。在下一节中,我们将详细介绍如何开始监控训练过程。
使用 TensorBoard 监控训练过程
TensorBoard 本质上是一个数学图形或计算引擎,在处理数字时表现非常出色,因此我们在深度学习中使用它。该工具本身仍然相当不成熟,但它具有一些非常有用的功能,可以用于监控训练过程。
按照以下步骤开始监控我们的样本训练:
- 您可以通过在与运行样本相同的目录/文件夹中,在新的 Anaconda 或命令窗口中输入以下命令来监控训练会话:
//first change directory to sample working folder
tensorboard --logdir=/tmp/autoencoder
- 这将启动一个 TensorBoard 服务器,您可以通过将浏览器导航到以斜体显示的 URL 来查看输出,正如您运行
TensorBoard
所在的窗口中显示的那样。它通常会像下面这样:
TensorBoard 1.10.0 at ***http://DESKTOP-V2J9HRG:6006*** (Press CTRL+C to quit)
or use
http://0.0.0.0:6000
-
请注意,URL 应使用您的机器名称,但如果无法工作,可以尝试第二种形式。如果提示,请确保允许端口
6000
和6006
以及 TensorBoard 应用程序通过您的防火墙。 -
当样本运行完毕时,您应该会看到以下内容:
使用卷积进行自动编码数字
- 返回并对比本示例和第一章《深度学习游戏》中的最后一个示例的结果。请注意性能的提升。
您可能会立即想到,“我们经历的训练时间是否值得付出这么多努力?”毕竟,在前一个示例中,解码后的图像看起来非常相似,而且训练速度要快得多,除了请记住,我们是通过在每次迭代中调整每个权重来缓慢训练网络权重,这些权重然后可以保存为模型。这个模型或“大脑”可以用来以后再次执行相同的任务,而无需重新训练。效果惊人地有效!在我们学习本章时,请始终牢记这个概念。在第三章《游戏中的 GAN》中,我们将开始保存并移动我们的“大脑”模型。
在接下来的部分中,我们将更深入地探讨卷积的工作原理。当您第一次接触卷积时,它可能比较难以理解,所以请耐心些。理解它的工作原理非常重要,因为我们稍后会广泛使用它。
理解卷积
卷积 是从图像中提取特征的一种方式,它可能使我们根据已知特征更容易地对其进行分类。在深入探讨卷积之前,让我们先退一步,理解一下为什么网络以及我们的视觉系统需要在图像中孤立出特征。请看下面的内容:这是一张名为 Sadie 的狗的样本图像,应用了各种图像滤镜:
应用不同滤镜的图像示例
上面展示了四种不同的版本,分别应用了没有滤波器、边缘检测、像素化和发光边缘滤波器。然而,在所有情况下,作为人类的你都能清晰地识别出这是一张狗的图片,不论应用了什么滤波器,除了在边缘检测的情况下,我们去除了那些对于识别狗不必要的额外图像数据。通过使用滤波器,我们只提取了神经网络识别狗所需要的特征。这就是卷积滤波器的全部功能,在某些情况下,这些滤波器中的一个可能只是一个简单的边缘检测。
卷积滤波器是一个由数字组成的矩阵或核,定义了一个单一的数学操作。这个过程从将其与左上角的像素值相乘开始,然后将矩阵操作的结果求和并作为输出。该核沿着图像滑动,步幅称为步幅,并演示了此操作:
应用卷积滤波器
在上图中,使用了步幅为 1 的卷积。应用于卷积操作的滤波器实际上是一个边缘检测滤波器。如果你观察最终操作的结果,你会看到中间部分现在被填充了 OS,这大大简化了任何分类任务。我们的网络需要学习的信息越少,学习速度越快,所需的数据也越少。现在,有趣的部分是,卷积学习滤波器、数字或权重,它需要应用这些权重以提取相关特征。这一点可能不太明显,可能会让人困惑,所以我们再来讲解一遍。回到我们之前的例子,看看我们如何定义第一个卷积层:
x = Conv2D(16, (3, 3), activation='relu', padding='same')(input_img)
在那行代码中,我们将第一个卷积层定义为具有16
个输出滤波器,意味着这一层的输出实际上是 16 个滤波器。然后我们将核大小设置为(3,3)
,这表示一个3x3
矩阵,就像我们在例子中看到的那样。请注意,我们没有指定各种核滤波器的权重值,因为毕竟这就是网络正在训练去做的事情。
让我们看看当所有内容组合在一起时,这在以下图示中是怎样的:
完整的卷积操作
卷积的第一步输出是特征图。一个特征图表示应用了单个卷积滤波器,并通过应用学习到的滤波器/核生成。在我们的例子中,第一层生成16 个核,从而生成16 个特征图;请记住,16
是指滤波器的数量。
卷积后,我们应用池化或子采样操作,以便将特征收集或聚集到一起。这种子采样进一步创建了新的集中的特征图,突出显示了我们正在训练的图像中的重要特征。回顾一下我们在之前的例子中如何定义第一个池化层:
x = MaxPooling2D((2, 2), padding='same')(x)
在代码中,我们使用 pool_size
为 (2,2)
进行子采样。该大小表示图像在宽度和高度方向下采样的因子。所以一个 2 x 2 的池化大小将创建四个特征图,其宽度和高度各减半。这会导致我们的第一层卷积和池化后总共生成 64 个特征图。我们通过将 16(卷积特征图)x 4(池化特征图) = 64 特征图来得到这个结果。考虑一下我们在这个简单示例中构建的特征图总数:
那就是 65,536 个 4 x 4 图像的特征图。这意味着我们现在在 65,536 张更小的图像上训练我们的网络;对于每张图像,我们尝试对其进行编码或分类。这显然是训练时间增加的原因,但也要考虑到我们现在用于分类图像的额外数据量。现在,我们的网络正在学习如何识别图像的部分或特征,就像我们人类识别物体一样。
例如,如果你仅仅看到了狗的鼻子,你很可能就能认出那是一只狗。因此,我们的样本网络现在正在识别手写数字的各个部分,正如我们现在所知道的,这大大提高了性能。
正如我们所看到的,卷积非常适合识别图像,但池化过程可能会破坏空间关系的保持。因此,当涉及到需要某种形式的空间理解的游戏或学习时,我们倾向于限制池化或完全消除池化。由于理解何时使用池化以及何时不使用池化非常重要,我们将在下一节中详细讨论这一点。
构建自驾车卷积神经网络(CNN)
Nvidia 在 2017 年创建了一个名为PilotNet的多层卷积神经网络(CNN),它通过仅仅展示一系列图像或视频,就能控制车辆的方向。这是神经网络,特别是卷积网络强大功能的一个引人注目的演示。下图展示了 PilotNet 的神经网络架构:
PilotNet 神经网络架构
图中显示了网络的输入从底部开始,上升到单个输入图像的结果输出到一个神经元,表示转向方向。由于这是一个很好的示例,许多人已在博客中发布了 PilotNet 的示例,其中一些实际上是有效的。我们将查看这些博客中的一个代码示例,看看如何用 Keras 构建类似的架构。接下来是来自原始 PilotNet 博客的一张图,展示了我们的自驾网络将用于训练的一些图像类型:
PilotNet 训练图像示例
这个例子的训练目标是输出方向盘应该转动的角度,以保持车辆行驶在道路上。打开Chapter_2_2.py
中的代码列表,并按照以下步骤操作:
- 我们将转而使用 Keras 进行一些样本处理。虽然 TensorFlow 内嵌版的 Keras 一直表现良好,但有一些功能我们需要的仅在完整版 Keras 中才有。要安装 Keras 和其他依赖项,打开 Shell 或 Anaconda 窗口,并运行以下命令:
pip install keras
pip install pickle
pip install matplotlib
- 在代码文件(
Chapter_2_2.py
)的开始部分,我们首先进行一些导入操作,并使用以下代码加载示例数据:
import os
import urllib.request
import pickle
import matplotlib
import matplotlib.pyplot as plt
***#downlaod driving data (450Mb)***
data_url = 'https://s3.amazonaws.com/donkey_resources/indoor_lanes.pkl'
file_path, headers = urllib.request.urlretrieve(data_url)
print(file_path)
with open(file_path, 'rb') as f:
X, Y = pickle.load(f)
-
这段代码只是做一些导入操作,然后从作者的源数据中下载示例驾驶帧。这篇博客的原文是由Roscoe’s Notebooks编写的,链接可以在
wroscoe.github.io/keras-lane-following-autopilot.html
找到。pickle
是一个解压库,用于解压前面列表底部数据集X
和Y
中的数据。 -
然后我们会将帧的顺序打乱,或者说本质上是对数据进行随机化。我们通常这样随机化数据以增强训练效果。通过随机化数据顺序,网络需要学习图像的绝对转向值,而不是可能的相对或增量值。以下代码完成了这个打乱过程:
import numpy as np
def unison_shuffled_copies(X, Y):
assert len(X) == len(Y)
p = np.random.permutation(len(X))
return X[p], Y[p]
shuffled_X, shuffled_Y = unison_shuffled_copies(X,Y)
len(shuffled_X)
-
这段代码的作用仅仅是使用
numpy
随机打乱图像帧的顺序。然后它会输出第一个打乱数据集shuffled_X
的长度,以便我们确认训练数据没有丢失。 -
接下来,我们需要创建训练集和测试集数据。训练集用于训练网络(权重),而测试集或验证集用于验证在新数据或原始数据上的准确性。正如我们之前所看到的,这在使用监督式训练或标注数据时是一个常见的主题。我们通常将数据划分为 80%的训练数据和 20%的测试数据。以下代码执行了这一操作:
test_cutoff = int(len(X) * .8) # 80% of data used for training
val_cutoff = test_cutoff + int(len(X) * .2) # 20% of data used for validation and test data
train_X, train_Y = shuffled_X[:test_cutoff], shuffled_Y[:test_cutoff]
val_X, val_Y = shuffled_X[test_cutoff:val_cutoff], shuffled_Y[test_cutoff:val_cutoff]
test_X, test_Y = shuffled_X[val_cutoff:], shuffled_Y[val_cutoff:]
len(train_X) + len(val_X) + len(test_X)
- 在创建了训练集和测试集后,我们现在想要增强或扩展训练数据。在这个特定的案例中,作者通过翻转原始图像并将其添加到数据集中来增强数据。我们将在后续章节中发现许多其他增强数据的方法,但这种简单有效的翻转方法是你可以加入到机器学习工具库中的一个技巧。执行这个翻转的代码如下:
X_flipped = np.array([np.fliplr(i) for i in train_X])
Y_flipped = np.array([-i for i in train_Y])
train_X = np.concatenate([train_X, X_flipped])
train_Y = np.concatenate([train_Y, Y_flipped])
len(train_X)
- 现在进入了重要部分。数据已经准备好,现在是构建模型的时候,如下代码所示:
from keras.models import Model, load_model
from keras.layers import Input, Convolution2D, MaxPooling2D, Activation, Dropout, Flatten, Dense
img_in = Input(shape=(120, 160, 3), name='img_in')
angle_in = Input(shape=(1,), name='angle_in')
x = Convolution2D(8, 3, 3)(img_in)
x = Activation('relu')(x)
x = MaxPooling2D(pool_size=(2, 2))(x)
x = Convolution2D(16, 3, 3)(x)
x = Activation('relu')(x)
x = MaxPooling2D(pool_size=(2, 2))(x)
x = Convolution2D(32, 3, 3)(x)
x = Activation('relu')(x)
x = MaxPooling2D(pool_size=(2, 2))(x)
merged = Flatten()(x)
x = Dense(256)(merged)
x = Activation('linear')(x)
x = Dropout(.2)(x)
angle_out = Dense(1, name='angle_out')(x)
model = Model(input=[img_in], output=[angle_out])
model.compile(optimizer='adam', loss='mean_squared_error')
model.summary()
-
目前构建模型的代码应该比较容易理解。注意架构的变化以及代码是如何与我们之前的示例不同的。还要注意两个高亮的行。第一行使用了一种新的层类型,叫做
Flatten
。这个层的作用就是将 2 x 2 的图像展平为一个向量,然后输入到一个标准的Dense
全连接隐藏层。第二行高亮的代码引入了另一种新的层类型,叫做Dropout
。这个层类型需要更多的解释,将在本节末尾进行更详细的讲解。 -
最后是训练部分,这段代码进行如下设置:
import os
from keras import callbacks
model_path = os.path.expanduser('~/best_autopilot.hdf5')
save_best = callbacks.ModelCheckpoint(model_path, monitor='val_loss', verbose=1,
save_best_only=True, mode='min')
early_stop = callbacks.EarlyStopping(monitor='val_loss', min_delta=0, patience=5,
verbose=0, mode='auto')
callbacks_list = [save_best, early_stop]
model.fit(train_X, train_Y, batch_size=64, epochs=4, validation_data=(val_X, val_Y), callbacks=callbacks_list)
-
这段代码设置了一组
callbacks
来更新和控制训练。我们已经使用过 callbacks 来更新 TensorBoard 服务器的日志。在这种情况下,我们使用 callbacks 在每个检查点(epoch)后重新保存模型并检查是否提前退出。请注意我们保存模型的形式——一个hdf5
文件。这个文件格式表示的是一种层次化的数据结构。 -
像你之前一样运行代码。这个示例可能需要一些时间,因此再次请保持耐心。当你完成后,将不会有输出,但请特别注意最小化的损失值。
在你深度学习的这段职业生涯中,你可能意识到你需要更多的耐心,或者更好的电脑,或者也许是一个支持 TensorFlow 的 GPU。如果你想尝试后者,可以下载并安装 TensorFlow GPU 库以及与你的操作系统相对应的其他必需库,这些会有所不同。网上可以找到大量文档。在安装了 TensorFlow 的 GPU 版本后,Keras 将自动尝试使用它。如果你有支持的 GPU,你应该会注意到性能的提升,如果没有,考虑购买一个。
虽然这个示例没有输出,为了简化,试着理解正在发生的事情。毕竟,这同样可以设置为一款驾驶游戏,网络仅通过查看截图来控制车辆。我们省略了作者原始博客文章中的结果,但如果你想进一步查看其表现,请返回并查看源链接。
作者在他的博客文章中做的一件事是使用了池化层,正如我们所见,当处理卷积时,这是相当标准的做法。然而,池化层的使用时机和方式现在有些争议,需要进一步详细讨论,这将在下一节中提供。
空间卷积和池化
Geoffrey Hinton 及其团队最近强烈建议,使用池化与卷积会去除图像中的空间关系。Hinton 则建议使用CapsNet,或称为胶囊网络。胶囊网络是一种保留数据空间完整性的池化方法。现在,这并非在所有情况下都是问题。对于手写数字,空间关系并不那么重要。然而,对于自动驾驶汽车或从事空间任务的网络——比如游戏——使用池化时,性能往往不如预期。事实上,Unity 团队在卷积后并不使用池化层;让我们来了解原因。
池化或下采样是通过将数据的共同特征聚集在一起的方式来增强数据。这样做的问题是,数据中的任何关系通常会完全丢失。下图演示了在卷积图上进行**MaxPooling(2,2)**的效果:
最大池化的工作原理
即便是在简单的前图中,你也能迅速理解池化操作会丢失角落(上左、下左、下右和上右)的空间关系。需要注意的是,经过几层池化后,任何空间关系将完全消失。
我们可以通过以下步骤测试从模型中移除池化层的效果,并再次进行测试:
- 打开
Chapter_2_3.py
文件,并注意我们注释掉了几个池化层,或者你也可以像下面这样删除这些行:
x = Convolution2D(8, 3, 3)(img_in)
x = Activation('relu')(x)
x = MaxPooling2D(pool_size=(2, 2))(x)
x = Convolution2D(16, 3, 3)(x)
x = Activation('relu')(x)
#x = MaxPooling2D(pool_size=(2, 2))(x)
x = Convolution2D(32, 3, 3)(x)
x = Activation('relu')(x)
#x = MaxPooling2D(pool_size=(2, 2))(x)
-
注意我们没有注释掉(或删除)所有的池化层,而是保留了一个。在某些情况下,你可能仍然希望保留一些池化层,可能是为了识别那些空间上不重要的特征。例如,在识别数字时,空间关系对于整体形状的影响较小。然而,如果我们考虑识别面孔,那么人的眼睛、嘴巴等之间的距离,就是区分面孔的关键特征。不过,如果你只想识别一个面孔,包含眼睛、嘴巴等,单纯使用池化层也完全可以接受。
-
接下来,我们还会在
Dropout
层上增加丢弃率,代码如下:
x = Dropout(.5)(x)
-
我们将在下一节中详细探讨丢弃层。现在,只需明白这个更改将对我们的模型产生更积极的影响。
-
最后,我们将
epochs
的数量增加到10
,代码如下:
model.fit(train_X, train_Y, batch_size=64, epochs=10, validation_data=(val_X, val_Y), callbacks=callbacks_list)
-
在我们之前的运行中,如果你在训练时观察损失率,你会发现最后一个例子大约在四个 epoch 时开始收敛。由于去掉了池化层也减少了训练数据,我们还需要增加 epoch 的数量。记住,池化或下采样增加了特征图的数量,特征图更少意味着网络需要更多的训练轮次。如果你不是在 GPU 上训练,这个模型将需要一段时间,所以请耐心等待。
-
最后,再次运行这个示例,应用那些小的修改。你会注意到的第一件事是训练时间剧烈增加。记住,这是因为我们的池化层确实加速了训练,但代价也不小。这也是我们允许只有单个池化层的原因之一。
-
当示例运行完毕后,比较一下我们之前运行的
Chapter_2_2.py
示例的结果。它达到了你预期的效果吗?
我们之所以专注于这篇博客文章,是因为它展示得非常好,写得也很出色。作者显然非常懂行,但这个示例也展示了在尽可能详细的情况下理解这些概念基础的重要性。面对信息的泛滥,这不是一件容易的事,但这也再次强调了开发有效的深度学习模型并非一项简单的任务,至少目前还不是。
现在我们已经理解了池化层的成本/惩罚,我们可以进入下一部分,回到理解Dropout
的内容。它是一个非常有效的工具,你将一次又一次地使用它。
Dropout 的必要性
现在,让我们回到我们非常需要讨论的Dropout
。在深度学习中,我们使用 Dropout 作为在每次迭代过程中随机切断层之间网络连接的一种方式。下面的示意图展示了 Dropout 在三层网络中应用的一次迭代:
Dropout 前后的变化
需要理解的重要一点是,并非所有连接都会被切断。这样做是为了让网络变得不那么专注于特定任务,而是更加通用。使模型具备通用性是深度学习中的一个常见主题,我们通常这么做是为了让模型能更快地学习更广泛的问题。当然,有时将网络通用化也可能限制了网络的学习能力。
如果我们现在回到之前的示例,并查看代码,我们可以看到这样使用了Dropout
层:
x = Dropout(.5)(x)
这一行简单的代码告诉网络在每次迭代后随机丢弃或断开 50%的连接。Dropout 仅对全连接层(Input -> Dense -> Dense)有效,但作为提高性能或准确性的一种方式非常有用。这可能在某种程度上解释了之前示例中性能提升的原因。
在下一部分,我们将探讨深度学习如何模仿记忆子过程或时间感知。
记忆和递归网络
记忆通常与递归神经网络(RNN)相关联,但这并不完全准确。RNN 实际上只是用来存储事件序列或你可能称之为时间感知的东西,如果你愿意的话,它是“时间的感觉”。RNN 通过在递归或循环中将状态保存回自身来实现这一点。下面是这种方式的一个示例:
展开式递归神经网络
图示展示了一个循环神经元的内部表示,该神经元被设置为跟踪若干时间步或迭代,其中x表示某一时间步的输入,h表示状态。W、U和V的网络权重在所有时间步中保持不变,并使用一种叫做**时间反向传播(BPTT)**的技术进行训练。我们不会深入讨论 BPTT 的数学原理,留给读者自己去发现,但要明白,循环神经网络中的网络权重使用一种成本梯度方法来进行优化。
循环神经网络(RNN)允许神经网络识别元素序列并预测通常接下来会出现的元素。这在预测文本、股票和当然是游戏中有巨大的应用。几乎任何能够从对时间或事件序列的理解中受益的活动,都可以通过使用 RNN 来获益,除了标准的 RNN,前面展示的类型,由于梯度问题,无法预测更长的序列。我们将在下一节中进一步探讨这个问题及其解决方案。
LSTM 拯救了梯度消失和爆炸问题
RNN 所面临的问题是梯度消失或爆炸。这是因为,随着时间的推移,我们尝试最小化或减少的梯度变得非常小或非常大,以至于任何额外的训练都不会产生影响。这限制了 RNN 的实用性,但幸运的是,这个问题已经通过**长短期记忆(LSTM)**块得到解决,如下图所示:
LSTM 块示例
LSTM 块使用一些技术克服了梯度消失问题。在图示中,您会看到一个圈内有一个x,它表示由激活函数控制的门控。在图示中,激活函数是σ和tanh。这些激活函数的工作方式类似于步长函数或 ReLU,我们可能会在常规网络层中使用任一函数作为激活。大多数情况下,我们会将 LSTM 视为一个黑箱,您只需要记住,LSTM 克服了 RNN 的梯度问题,并能够记住长期序列。
让我们看一个实际的例子,看看如何将这些内容组合在一起。打开Chapter_2_4.py
并按照以下步骤操作:
- 我们像往常一样,首先导入我们需要的各种 Keras 组件,如下所示:
这个例子取自machinelearningmastery.com/understanding-stateful-lstm-recurrent-neural-networks-python-keras/
。这个网站由Jason Brownlee 博士主办,他有许多出色的例子,解释了 LSTM 和循环神经网络的使用。
import numpy
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
from keras.utils import np_utils
-
这次我们引入了两个新的类,
Sequential
和LSTM
。当然我们知道LSTM
的作用,那Sequential
呢?Sequential
是一种模型形式,按顺序定义层级,一个接一个。我们之前对这个细节不太关注,因为我们之前的模型都是顺序的。 -
接下来,我们将随机种子设置为一个已知值。这样做是为了使我们的示例能够自我复制。你可能在之前的示例中注意到,并非每次运行的结果都相同。在许多情况下,我们希望训练的一致性,因此我们通过以下代码设置一个已知的种子值:
numpy.random.seed(7)
-
需要意识到的是,这只是设置了
numpy
的随机种子值。其他库可能使用不同的随机数生成器,并需要不同的种子设置。我们将在未来尽可能地识别这些不一致之处。 -
接下来,我们需要确定一个训练的序列;在此示例中,我们将使用如下代码中的
alphabet
:
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
char_to_int = dict((c, i) for i, c in enumerate(alphabet))
int_to_char = dict((i, c) for i, c in enumerate(alphabet))
seq_length = 1
dataX = []
dataY = []
for i in range(0, len(alphabet) - seq_length, 1):
seq_in = alphabet[i:i + seq_length]
seq_out = alphabet[i + seq_length]
dataX.append([char_to_int[char] for char in seq_in])
dataY.append(char_to_int[seq_out])
print(seq_in, '->', seq_out)
-
前面的代码构建了我们的字符序列,并构建了每个字符序列的映射。它构建了
seq_in
和seq_out
,展示了正向和反向的位置。由于序列长度由seq_length = 1
定义,因此我们只关心字母表中的一个字母及其后面的字符。当然,你也可以使用更长的序列。 -
构建好序列数据后,接下来是使用以下代码对数据进行形状调整和归一化:
X = numpy.reshape(dataX, (len(dataX), seq_length, 1))
# normalize
X = X / float(len(alphabet))
# one hot encode the output variable
y = np_utils.to_categorical(dataY)
- 前面的代码的第一行将数据重塑为一个张量,其大小长度为
dataX
,即步骤数或序列数,以及要识别的特征数。然后我们对数据进行归一化。归一化数据的方式有很多种,但在此我们将值归一化到 0 到 1 之间。接着,我们对输出进行独热编码,以便于训练。
独热编码是将数据或响应的位置值设置为 1,其它位置设置为 0。在此示例中,我们的模型输出是 26 个神经元,它也可以用 26 个零表示,每个神经元对应一个零,像这样:
00000000000000000000000000
每个零代表字母表中匹配的字符位置。如果我们想表示字符A,我们会输出如下的独热编码值:
10000000000000000000000000
- 然后,我们构建模型,使用与之前略有不同的代码形式,如下所示:
model = Sequential()
model.add(LSTM(32, input_shape=(X.shape[1], X.shape[2])))
model.add(Dense(y.shape[1], activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
model.fit(X, y, epochs=500, batch_size=1, verbose=2)
scores = model.evaluate(X, y, verbose=0)
print("Model Accuracy: %.2f%%" % (scores[1]*100))
-
前面代码中的关键部分是高亮显示的那一行,展示了
LSTM
层的构建。我们通过设置单元数来构建LSTM
层,在这个例子中是32
,因为我们的序列长度为 26 个字符,我们希望通过2
来禁用单元。然后我们将input_shape
设置为与之前创建的张量X
相匹配,X
用于保存我们的训练数据。在这种情况下,我们只是设置形状以匹配所有字符(26 个)和序列长度,在这种情况下是1
。 -
最后,我们用以下代码输出模型:
for pattern in dataX:
x = numpy.reshape(pattern, (1, len(pattern), 1))
x = x / float(len(alphabet))
prediction = model.predict(x, verbose=0)
index = numpy.argmax(prediction)
result = int_to_char[index]
seq_in = [int_to_char[value] for value in pattern]
print(seq_in, "->", result)
- 像平常一样运行代码并检查输出。你会注意到准确率大约为 80%。看看你能否提高模型预测字母表下一个序列的准确率。
这个简单的示例展示了使用 LSTM 块识别简单序列的基本方法。在下一部分,我们将看一个更复杂的例子:使用 LSTM 来玩石头、剪刀、布。
使用 LSTM 玩石头、剪刀、布
记住,数据序列的记忆在许多领域有着广泛的应用,尤其是在游戏中。当然,制作一个简单、清晰的示例是另一回事。幸运的是,互联网上有很多示例,Chapter_2_5.py
展示了一个使用 LSTM 来玩石头、剪刀、布的例子。
打开那个示例文件并按照以下步骤进行操作:
这个示例来自github.com/hjpulkki/RPS
,但是代码需要在多个地方进行调整才能适应我们的需求。
- 让我们像往常一样开始导入。在这个示例中,确保像上次练习那样安装 Keras:
import numpy as np
from keras.utils import np_utils
from keras.models import Sequential
from keras.layers import Dense, LSTM
- 然后,我们设置一些常量,如下所示:
EPOCH_NP = 100
INPUT_SHAPE = (1, -1, 1)
OUTPUT_SHAPE = (1, -1, 3)
DATA_FILE = "data.txt"
MODEL_FILE = "RPS_model.h5"
- 然后,我们构建模型,这次有三个 LSTM 层,分别对应于序列中的每个元素(石头、剪刀和布),如下所示:
def simple_model():
new_model = Sequential()
new_model.add(LSTM(output_dim=64, input_dim=1, return_sequences=True, activation='sigmoid'))
new_model.add(LSTM(output_dim=64, return_sequences=True, activation='sigmoid'))
new_model.add(LSTM(output_dim=64, return_sequences=True, activation='sigmoid'))
new_model.add(Dense(64, activation='relu'))
new_model.add(Dense(64, activation='relu'))
new_model.add(Dense(3, activation='softmax'))
new_model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy', 'categorical_crossentropy'])
return new_model
- 然后,我们创建一个函数,从
data.txt
文件中提取数据。该文件使用以下代码保存了训练数据的序列:
def batch_generator(filename):
with open('data.txt', 'r') as data_file:
for line in data_file:
data_vector = np.array(list(line[:-1]))
input_data = data_vector[np.newaxis, :-1, np.newaxis]
temp = np_utils.to_categorical(data_vector, num_classes=3)
output_data = temp[np.newaxis, 1:]
yield (input_data, output_data)
-
在这个示例中,我们将每个训练块通过 100 次 epoch 进行训练,顺序与文件中的顺序一致。更好的方法是以随机顺序训练每个训练序列。
-
然后我们创建模型:
# Create model
np.random.seed(7)
model = simple_model()
- 使用循环训练数据,每次迭代从
data.txt
文件中获取一个批次:
for (input_data, output_data) in batch_generator('data.txt'):
try:
model.fit(input_data, output_data, epochs=100, batch_size=100)
except:
print("error")
- 最后,我们使用验证序列评估结果,如以下代码所示:
print("evaluating")
validation = '100101000110221110101002201101101101002201011012222210221011011101011122110010101010101'
input_validation = np.array(list(validation[:-1])).reshape(INPUT_SHAPE)
output_validation = np_utils.to_categorical(np.array(list(validation[1:]))).reshape(OUTPUT_SHAPE)
loss_and_metrics = model.evaluate(input_validation, output_validation, batch_size=100)
print("\n Evaluation results")
for i in range(len(loss_and_metrics)):
print(model.metrics_names[i], loss_and_metrics[i])
input_test = np.array([0, 0, 0, 1, 1, 1, 2, 2, 2]).reshape(INPUT_SHAPE)
res = model.predict(input_test)
prediction = np.argmax(res[0], axis=1)
print(res, prediction)
model.save(MODEL_FILE)
del model
- 像平常一样运行示例。查看最后的结果,并注意模型在预测序列时的准确性。
一定要多次运行这个简单示例,理解 LSTM 层是如何设置的。特别注意参数及其设置方式。
这就结束了我们快速了解如何使用递归,也就是 LSTM 块,来识别和预测数据序列。我们当然会在本书的其他章节中多次使用这一多功能的层类型。
在本章的最后一部分,我们再次展示了一些练习,鼓励你们为自己的利益进行尝试。
练习
完成以下练习,以便在自己的时间里提高学习体验。加深你对材料的理解会让你成为一个更成功的深度学习者,也能让你更享受本书的内容:
-
在
Chapter_2_1.py
示例中,将Conv2D
层的过滤器大小更改为不同的值。再次运行示例,看看这对训练性能和准确度有何影响。 -
注释掉或删除
Chapter_2_1.py
示例中的几个MaxPooling层和相应的UpSampling层。记住,如果你删除了第 2 层和第 3 层之间的池化层,你也需要删除上采样层以保持一致性。重新运行示例,观察这对训练时间、准确度和性能的影响。 -
修改
Chapter_2_2.py
示例中的Conv2D层,使用不同的滤波器大小。观察这对训练的影响。 -
修改
Chapter_2_2.py
示例中的Conv2D层,使用步幅值为2。你可能需要参考Keras文档来完成此操作。观察这对训练的影响。 -
修改
Chapter_2_2.py
示例中的MaxPooling层,改变池化的维度。观察这对训练的影响。 -
删除或注释掉
Chapter_2_3.py
示例中使用的所有MaxPooling层。如果所有池化层都被注释掉,会发生什么?现在需要增加训练周期数吗? -
修改本章中使用的各个示例中的Dropout使用方式。这包括添加 dropout。测试使用不同 dropout 比例的效果。
-
修改
Chapter_2_4.py
示例,使模型提高准确率。你需要做什么来提高训练性能? -
修改
Chapter_2_4.py
示例,以便预测序列中的多个字符。如果需要帮助,请回顾原始博客文章,获取更多信息。 -
如果你改变
Chapter_2_5.py
示例中三个LSTM层使用的单元数,会发生什么?如果将其增加到 128、32 或 16 会怎样?尝试这些值,了解它们的影响。
可以自行扩展这些练习。尝试自己写一个新的示例,即使只是一个简单的例子。没有什么比写代码更能帮助你学习编程了。
总结
在本章以及上一章中,我们深入探讨了深度学习和神经网络的核心元素。尽管我们在过去几章中的回顾不够全面,但它应该为你继续阅读本书的其他部分打下良好的基础。如果你在前两章的任何内容上遇到困难,现在就回去复习这些内容,花更多时间进行复习。理解神经网络架构的基本概念和各种专用层的使用非常重要,如本章所讨论的(CNN 和 RNN)。确保你理解 CNN 的基础知识以及如何有效地使用它来选择特征,并了解使用池化或子采样时的权衡。同时,理解 RNN 的概念,以及在预测或检测时何时使用 LSTM 块。卷积层和 LSTM 块现在是深度学习的基础组件,接下来我们将在构建的多个网络中使用它们。
在下一章,我们将开始为本书构建我们的示例游戏,并介绍 GANs,即生成对抗网络。我们将探讨 GANs 以及它们如何用于生成游戏内容。
第三章:GAN 在游戏中的应用
到目前为止,在我们的深度学习探索中,我们所有的网络训练都采用了一种叫做监督训练的技术。当你花时间标记和识别数据时,这种训练方法非常有效。我们之前的所有示例练习都使用了监督训练,因为它是最简单的教学方法。然而,监督学习往往是最繁琐和冗长的方法,主要因为它在训练前需要一定的标签或数据识别。在机器学习或深度学习在游戏和仿真中的应用尝试中,尽管有人尝试使用这种训练方式,但结果都证明是失败的。
这就是为什么在本书的大部分内容中,我们将探讨其他形式的训练,首先从一种无监督训练方法开始,称为生成对抗网络(GAN)。GAN 通过本质上是一个双人游戏的方式进行自我训练,这使得它们成为我们学习的理想下一步,并且是实际开始为游戏生成内容的完美方法。
在本章中,我们将探索生成对抗网络(GAN)及其在游戏内容开发中的应用。在这个过程中,我们还将学习更多深度学习技术的基础知识。本章将涵盖以下内容:
-
介绍 GAN
-
在 Keras 中编写 GAN 代码
-
Wasserstein GAN
-
GAN 用于创建纹理
-
使用 GAN 生成音乐
-
练习
GAN 以其训练和构建的难度著称。因此,建议你花时间仔细阅读本章内容,并在需要时多做几次练习。我们学习的制作有效 GAN 的技术将帮助你更好地理解训练网络的整体概念以及其他可用的选项。同时,我们仍然需要涵盖许多关于训练网络的基础概念,所以请认真完成本章的内容。
介绍 GAN
GAN 的概念通常通过一个双人游戏类比来介绍。在这个游戏中,通常有一位艺术专家和一位艺术伪造者。艺术伪造者的目标是制作出足够逼真的假画来欺骗艺术专家,从而赢得游戏。以下是最早通过神经网络展示这一过程的示例:
Ian 及其他人提出的 GAN
在上面的图示中,生成器(Generator)扮演着艺术伪造者的角色,试图超越被称为判别器(Discriminator)的艺术专家。生成器使用随机噪声作为来源来生成图像,目标是让图像足够逼真,以至于能够欺骗判别器。判别器在真实和虚假图像上进行训练,任务就是将图像分类为真实或虚假。然后,生成器被训练去制作足够逼真的假图像来欺骗判别器。虽然这个概念作为一种自我训练网络的方式看起来简单,但在过去几年里,这种对抗技术的实现已在许多领域表现出卓越的效果。
GAN 最早由伊恩·古德费洛(Ian Goodfellow)及其团队在蒙特利尔大学于 2014 年开发。仅仅几年时间,这项技术已经迅速扩展到众多广泛而多样的应用领域,从生成图像和文本到为静态图像添加动画,几乎在短短几年内就完成了突破。以下是目前在深度学习领域引起关注的几项令人印象深刻的 GAN 改进/实现的简短总结:
-
深度卷积 GAN(DCGAN):这是我们刚才讨论的标准架构的首次重大改进。我们将在本章的下一部分中,探讨它作为我们学习的第一个 GAN 形式。
-
对抗自编码器 GAN:这种自编码器变体利用对抗性 GAN 技术来隔离数据的属性或特征。它在发现数据中的潜在关系方面具有有趣的应用,例如能够区分手写数字的风格与内容之间的差异。
-
辅助分类器 GAN:这是另一种增强型 GAN,与条件 GAN 相关。它已被证明能够合成更高分辨率的图像,尤其在游戏领域非常值得进一步探索。
-
CycleGAN:这是一个变体,其令人印象深刻之处在于它允许将一种图像的风格转换为另一种图像。许多使用这种形式的 GAN 示例都很常见,例如将一张图片的风格转换成梵高的画风,或者交换名人的面孔。如果本章激发了你对 GAN 的兴趣,并且你想进一步探索这一形式,可以查看这篇文章:
hardikbansal.github.io/CycleGANBlog/
。 -
条件 GAN:这些 GAN 使用一种半监督学习的形式。这意味着训练数据被标记,但带有元数据或属性。例如,不是将 MNIST 数据集中的手写数字标记为“9”,而是标记其书写风格(草书或印刷体)。然后,这种新的条件 GAN 形式不仅可以学习数字,还可以学习它们是草书还是印刷体。这种 GAN 形式已经展现出一些有趣的应用,并且我们将在探讨游戏领域的具体应用时进一步讨论。
-
DiscoGAN:这又是一种 GAN,展示了有趣的结果,从交换名人发型到性别转换。这种 GAN 提取特征或领域,并允许将它们转移到其他图像或数据空间。这在游戏中有很多应用,值得对感兴趣的读者进一步探索。
-
DualGAN:这使用双重 GAN,通过训练两个生成器与两个判别器进行对抗,以将图像或数据转换为其他风格。这对于重新设计多个资产非常有用,尤其是在为游戏生成不同风格的艺术内容时表现出色。
-
最小二乘 GAN(LSGAN):这种 GAN 使用不同的损失计算方式,并且已被证明比 DCGAN 更有效。
-
pix2pixGAN:这是对条件 GAN 的扩展,使其能够从一张图片转换或生成多种特征到另一张图片。这允许将物体的草图转换为该物体的真实 3D 渲染图像,反之亦然。虽然这是一个非常强大的 GAN,但它仍然是研究驱动的,可能还不适合用于游戏开发。或许你得再等六个月或一年。
-
InfoGANs:这些类型的 GAN 迄今为止被广泛用于探索训练数据中的特征或信息。例如,它们可以用来识别 MNIST 数据集中数字的旋转方向。此外,它们通常被用作识别条件 GAN 训练属性的一种方式。
-
Stacked 或 SGAN:这是一种将自身分解为多个层次的 GAN,每个层都是一个生成器和判别器相互对抗。这使得整个 GAN 更容易训练,但也要求你理解每个阶段或层的细节。如果你是刚开始学习,这可能不是适合你的 GAN,但随着你构建更复杂的网络,稍后可以再次回顾这个模型。
-
Wasserstein GANs:这是一种最先进的 GAN,它将在本章的专门章节中获得关注。损失的计算是这种形式的 GAN 的改进之处。
-
WassGANs:这使用 Wasserstein 距离来确定损失,从而显著帮助模型的收敛。
在本章中,我们将继续探索具体 GAN 实现的其他实例。在这里,我们将学习如何使用 GAN 生成游戏纹理和音乐。暂时,我们先跳到下一部分,学习如何在 Keras 中编写 GAN。
在 Keras 中编写一个 GAN
当然,最好的学习方式是通过实践,所以让我们跳进来,开始编写第一个 GAN。在这个例子中,我们将构建基础的 DCGAN,并稍后根据我们的需求进行修改。打开Chapter_3_2.py
,并按照以下步骤进行:
这段代码最初来自github.com/eriklindernoren/Keras-GAN
,它是 Keras 中最好的 GAN 表示,感谢 Erik Linder-Norén 的辛勤工作。做得好,感谢你的努力,Erik。
一个备用的普通 GAN 列表已被添加为Chapter_3_1.py
,供你学习使用。
- 我们首先导入所需的库:
from __future__ import print_function, division
from keras.datasets import mnist
from keras.layers import Input, Dense, Reshape, Flatten, Dropout
from keras.layers import BatchNormalization, Activation, ZeroPadding2D
from keras.layers.advanced_activations import LeakyReLU
from keras.layers.convolutional import UpSampling2D, Conv2D
from keras.models import Sequential, Model
from keras.optimizers import Adam
import matplotlib.pyplot as plt
import sys
import numpy as np
-
在前面的代码中引入了一些新的重要类型:
Reshape
、BatchNormalization
、ZeroPadding2D
、LeakyReLU
、Model
和Adam
。我们将更详细地探讨这些类型。 -
我们之前的大多数示例使用的是基本的脚本。现在我们进入了一个阶段,需要为将来的进一步使用创建自定义的类型(类)。这意味着我们现在开始像这样定义我们的类:
class DCGAN():
-
因此,我们创建了一个新的类(类型),命名为
DCGAN
,用于实现深度卷积 GAN。 -
接下来,我们通常会按照 Python 的约定定义
init
函数。然而,为了我们的目的,让我们先看看generator
函数:
def build_generator(self):
model = Sequential()
model.add(Dense(128 * 7 * 7, activation="relu", input_dim=self.latent_dim))
model.add(Reshape((7, 7, 128)))
model.add(UpSampling2D())
model.add(Conv2D(128, kernel_size=3, padding="same"))
model.add(BatchNormalization(momentum=0.8))
model.add(Activation("relu"))
model.add(UpSampling2D())
model.add(Conv2D(64, kernel_size=3, padding="same"))
model.add(BatchNormalization(momentum=0.8))
model.add(Activation("relu"))
model.add(Conv2D(self.channels, kernel_size=3, padding="same"))
model.add(Activation("tanh"))
model.summary()
noise = Input(shape=(self.latent_dim,))
img = model(noise)
return Model(noise, img)
-
build_generator
函数构建了伪造艺术模型,意味着它接受那一组噪声样本,并尝试将其转换为判别器认为是真的图像。在这个过程中,它利用卷积原理提高效率,然而在这种情况下,它生成了一张噪声特征图,然后将其转化为一张真实图像。从本质上讲,生成器做的是与识别图像相反的工作,它并不是识别图像,而是尝试基于特征图生成图像。在前面的代码块中,注意输入是如何以
128, 7x7
的噪声特征图开始,然后使用Reshape
层将其转换为我们想要创建的正确图像布局。接着,它通过上采样(即池化或下采样的逆过程)将特征图放大到 2 倍大小(14 x 14),并训练另一个卷积层,之后继续进行更多的上采样(2 倍至 28 x 28),直到生成正确的图像尺寸(MNIST 的 28x28)。我们还看到了一个新的层类型BatchNormalization
的使用,稍后我们会详细讨论它。 -
接下来,我们将像这样构建
build_discriminator
函数:
def build_discriminator(self):
model = Sequential()
model.add(Conv2D(32, kernel_size=3, strides=2, input_shape=self.img_shape, padding="same"))
model.add(LeakyReLU(alpha=0.2))
model.add(Dropout(0.25))
model.add(Conv2D(64, kernel_size=3, strides=2, padding="same"))
model.add(ZeroPadding2D(padding=((0,1),(0,1))))
model.add(BatchNormalization(momentum=0.8))
model.add(LeakyReLU(alpha=0.2))
model.add(Dropout(0.25))
model.add(Conv2D(128, kernel_size=3, strides=2, padding="same"))
model.add(BatchNormalization(momentum=0.8))
model.add(LeakyReLU(alpha=0.2))
model.add(Dropout(0.25))
model.add(Conv2D(256, kernel_size=3, strides=1, padding="same"))
model.add(BatchNormalization(momentum=0.8))
model.add(LeakyReLU(alpha=0.2))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(1, activation='sigmoid'))
model.summary()
img = Input(shape=self.img_shape)
validity = model(img)
return Model(img, validity)
-
这次,判别器正在测试图像输入,并判断它们是否为伪造图像。它使用卷积来识别特征,但在这个示例中,它使用
ZeroPadding2D
将一层零填充放置在图像周围,以帮助识别。该层的相反形式是Cropping2D
,它会裁剪图像。注意,模型没有在卷积中使用下采样或池化层。我们将在接下来的部分中探讨其他新的特殊层LeakyReLU
和BatchNormalization
。注意,我们在卷积中没有使用任何池化层。这是为了通过分数步幅卷积增加空间维度。看看在卷积层内部我们是如何使用奇数大小的卷积核和步幅的。 -
现在,我们将回过头来像这样定义
init
函数:
def __init__(self):
self.img_rows = 28
self.img_cols = 28
self.channels = 1
self.img_shape = (self.img_rows, self.img_cols, self.channels)
self.latent_dim = 100
optimizer = Adam(0.0002, 0.5)
self.discriminator = self.build_discriminator()
self.discriminator.compile(loss='binary_crossentropy',
optimizer=optimizer, metrics=['accuracy'])
self.generator = self.build_generator()
z = Input(shape=(self.latent_dim,))
img = self.generator(z)
self.discriminator.trainable = False
valid = self.discriminator(img)
self.combined = Model(z, valid)
self.combined.compile(loss='binary_crossentropy', optimizer=optimizer)
-
这段初始化代码设置了我们输入图像的大小(28 x 28 x 1,表示一个通道的灰度图像)。然后设置一个
Adam
优化器,这是我们将在优化器章节中进一步回顾的内容。之后,它构建了discriminator
,然后是generator
。接着,它将这两个模型或子网络(generator
和discriminator
)组合在一起,使得网络能够协同工作,并在整个网络上优化训练。这个概念我们将在优化器部分更详细地讨论。 -
在我们深入之前,花点时间运行这个示例。这个示例可能需要相当长的时间来运行,所以启动后可以回到书本,保持运行状态。
-
在样本运行过程中,你将能够看到生成的输出被保存到与运行的 Python 文件同一个文件夹下的
images
文件夹中。可以观察到,每经过 50 次迭代,都会保存一张新图像,如下图所示:
GAN 生成的输出示例
上图展示了大约经过 3900 次迭代后的结果。当你开始训练时,会需要一些时间才能获得如此好的结果。
这涵盖了模型设置的基础知识,除了训练过程中所有的工作,这部分将在下一节中讲解。
训练一个 GAN
训练一个 GAN 需要更多的细节关注以及对更高级优化技术的理解。我们将详细讲解该函数的每个部分,以便理解训练过程的复杂性。让我们打开Chapter_3_1.py
,查看train
函数并按照以下步骤进行操作:
- 在
train
函数的开头,你会看到以下代码:
def train(self, epochs, batch_size=128, save_interval=50):
(X_train, _), (_, _) = mnist.load_data()
X_train = X_train / 127.5 - 1.
X_train = np.expand_dims(X_train, axis=3)
valid = np.ones((batch_size, 1))
fake = np.zeros((batch_size, 1))
-
数据首先从 MNIST 训练集加载,然后重新缩放到
-1
到1
的范围。我们这样做是为了更好地将数据围绕 0 进行中心化,并且适配我们的激活函数tanh
。如果你回去查看生成器函数,你会看到底部的激活函数是tanh
。 -
接下来,我们构建一个
for
循环来迭代整个训练周期,代码如下:
for epoch in range(epochs):
- 然后,我们使用以下代码随机选择一半的真实训练图像:
idx = np.random.randint(0, X_train.shape[0], batch_size)
imgs = X_train[idx]
- 然后,我们采样
noise
并使用以下代码生成一组伪造图像:
noise = np.random.normal(0, 1, (batch_size, self.latent_dim))
gen_imgs = self.generator.predict(noise)
-
现在,图像中一半是真实的,另一半是我们
generator
生成的伪造图像。 -
接下来,
discriminator
会对图像进行训练,产生错误预测的伪造图像损失和正确识别的真实图像损失,如下所示:
d_loss_real = self.discriminator.train_on_batch(imgs, valid)
d_loss_fake = self.discriminator.train_on_batch(gen_imgs, fake)
d_loss = 0.5 * np.add(d_loss_real, d_loss_fake)
-
记住,这段代码是针对一个批次的数据运行的。这就是为什么我们使用
numpy np.add
函数来将d_loss_real
和d_loss_fake
相加的原因。numpy
是我们常用的一个库,用于处理数据集或张量。 -
最后,我们使用以下代码训练生成器:
g_loss = self.combined.train_on_batch(noise, valid)
print ("%d [D loss: %f, acc.: %.2f%%] [G loss: %f]" % (epoch, d_loss[0], 100*d_loss[1], g_loss))
if epoch % save_interval == 0:
self.save_imgs(epoch)
- 请注意,
g_loss
是如何基于训练合并模型来计算的。正如你可能记得的,合并模型会将真实和伪造图像的输入传入,并将训练的误差反向传播到整个模型中。这使得我们能够将generator
和discriminator
一起训练,作为一个合并模型。接下来展示了这一过程的一个示例,但请注意图像大小与我们不同:
DCGAN 的层架构图
现在我们对架构有了更好的理解,我们需要回过头来理解一些关于新层类型和合并模型优化的细节。我们将在下一节中探讨如何优化像我们的 GAN 这样的联合模型。
优化器
优化器实际上只是另一种通过网络训练误差反向传播的方式。正如我们在第一章*《深度学习与游戏》*中学到的那样,我们用于反向传播的基本算法是梯度下降法,以及更高级的随机梯度下降法(SGD)。
SGD 通过在每次训练迭代中随机选择批次顺序来改变梯度的评估。尽管 SGD 在大多数情况下表现良好,但它在生成对抗网络(GAN)中表现不佳,因为它存在一个被称为梯度消失/ 梯度爆炸的问题,这通常出现在训练多个但组合的网络时。记住,我们将生成器的结果直接输入判别器中。为此,我们转向更高级的优化器。以下图示展示了典型最佳优化器的性能:
各种优化器的性能比较
图中的所有方法都源自 SGD,但你可以清楚地看到,在这个实例中,Adam是赢家。当然,也有例外情况,但目前最受欢迎的优化器是 Adam。你可能已经注意到,我们以前广泛使用过它,并且未来你可能会继续使用它。不过,接下来我们会更详细地了解每个优化器,如下所示:
-
SGD:这是我们最早研究的模型之一,通常它将是我们用来作为训练基准的模型。
-
带有 Nesterov 的 SGD:SGD 常面临的问题是我们在早期训练示例中看到的网络损失中的晃动效应。记住,在训练过程中,我们的网络损失会在两个值之间波动,几乎就像一个球在山坡上下滚动。实质上,这正是发生的情况,但我们可以通过引入一个我们称之为动量的项来纠正这一点。以下图示展示了动量对训练的影响:
带有和不带动量的 SGD
所以,现在,我们不再只是让球盲目地滚动,而是控制它的速度。我们给它一点推力,让它越过那些恼人的颠簸或晃动,更高效地到达最低点。
正如你可能记得,从反向传播的数学中我们控制 SGD 中的梯度,以训练网络最小化误差或损失。通过引入动量,我们试图通过近似值来更高效地控制梯度。Nesterov 技术,或者可以称之为动量,使用加速的动量项来进一步优化梯度。
-
AdaGrad:这种方法根据更新的频率优化个别训练参数,因此非常适合处理较小的数据集。另一个主要优点是它不需要调节学习率。然而,这种方法的一个大缺点是平方梯度导致学习率变得过小,从而使得网络停止学习。
-
AdaDelta:这种方法是 AdaGrad 的扩展,用于处理平方梯度和消失的学习率。它通过将学习率窗口固定为一个特定的最小值来解决这一问题。
-
RMSProp:由深度学习的祖师爷 Geoff Hinton 开发,这是一种用于解决 AdaGrad 中消失学习率问题的技术。如图所示,它与 AdaDelta 的表现相当。
-
自适应矩估计(Adam):这是一种尝试通过更控制版本的动量来控制梯度的技术。它通常被描述为动量加上 RMSProp,因为它结合了两者的优点。
-
AdaMax:这种方法未在性能图表中显示,但值得一提。它是 Adam 的扩展,对每次更新迭代应用于动量进行了推广。
-
Nadam:这是一种未出现在图表中的方法,它是 Nesterov 加速动量和 Adam 的结合。普通的 Adam 仅使用了一个未加速的动量项。
-
AMSGrad:这是 Adam 的一种变种,在 Adam 无法收敛或出现震荡时效果最佳。这是由于算法未能适应学习率,通过取之前平方梯度的最大值而非平均值来修正这一问题。这个区别较为微妙,且更倾向于较小的数据集。在使用时可以将这个选项作为未来可能的工具记在心中。
这就完成了我们对优化器的简要概述;务必参考章节末尾的练习,以进一步探索它们。在下一节中,我们将构建自己的 GAN,生成可以在游戏中使用的纹理。
Wasserstein GAN
正如你现在可以非常清楚地理解的,GAN 有着广泛而多样的应用,其中许多在游戏中应用得非常好。其中一个应用是生成纹理或纹理变化。我们经常需要对纹理做些微小变化,以便给我们的游戏世界增添更具说服力的外观。这可以通过着色器来完成,但出于性能考虑,通常最好创建静态资源。
因此,在本节中,我们将构建一个 GAN 项目,允许我们生成纹理或高度图。你也可以使用我们之前简要提到的其他一些酷炫的 GAN 扩展这个概念。我们将使用 Erik Linder-Norén 的 Wasserstein GAN 的默认实现,并将其转换为我们的用途。
在首次接触深度学习问题时,你将面临的一个主要障碍是如何将数据调整为所需的格式。在原始示例中,Erik 使用了 MNIST 数据集,但我们将把示例转换为使用 CIFAR100 数据集。CIFAR100 数据集是一组按类别分类的彩色图像,如下所示:
CIFAR 100 数据集
现在,让我们打开 Chapter_3_wgan.py
并按照以下步骤操作:
- 打开 Python 文件并查看代码。大部分代码与我们之前查看的 DCGAN 相同。然而,我们想要查看几个关键的不同点,具体如下:
def train(self, epochs, batch_size=128, sample_interval=50):
(X_train, _), (_, _) = mnist.load_data()
X_train = (X_train.astype(np.float32) - 127.5) / 127.5
X_train = np.expand_dims(X_train, axis=3)
valid = -np.ones((batch_size, 1))
fake = np.ones((batch_size, 1))
for epoch in range(epochs):
for _ in range(self.n_critic):
idx = np.random.randint(0, X_train.shape[0], batch_size)
imgs = X_train[idx]
noise = np.random.normal(0, 1, (batch_size, self.latent_dim))
gen_imgs = self.generator.predict(noise)
d_loss_real = self.critic.train_on_batch(imgs, valid)
d_loss_fake = self.critic.train_on_batch(gen_imgs, fake)
d_loss = 0.5 * np.add(d_loss_fake, d_loss_real)
for l in self.critic.layers:
weights = l.get_weights()
weights = [np.clip(w, -self.clip_value, self.clip_value) for
w in weights]
l.set_weights(weights)
g_loss = self.combined.train_on_batch(noise, valid)
print ("%d [D loss: %f] [G loss: %f]" % (epoch, 1 - d_loss[0], 1
- g_loss[0]))\
if epoch % sample_interval == 0:
self.sample_images(epoch)
- Wasserstein GAN 使用一种距离函数来确定每次训练迭代的成本或损失。除此之外,这种形式的 GAN 使用多个评判器而不是单一的判别器来确定成本或损失。训练多个评判器可以提高性能,并解决我们通常在 GAN 中看到的梯度消失问题。另一种 GAN 训练方式的示例如下:
GAN 实现的训练性能对比 (arxiv.org/pdf/1701.07875.pdf
)
-
WGAN 通过管理成本来克服梯度问题,采用距离函数来确定移动的成本,而不是依赖错误值的差异。一个线性成本函数可能像字符拼写一个单词所需的移动次数那样简单。例如,单词 SOPT 的成本为 2,因为 T 字符需要移动两次才能正确拼写成 STOP。单词 OTPS 的距离成本为 3 (S) + 1 (T) = 4,才能正确拼写成 STOP。
-
Wasserstein 距离函数本质上确定了将一个概率分布转换为另一个概率分布的成本。如你所想,理解这些数学可能相当复杂,因此我们将这一部分留给对数学更感兴趣的读者。
-
运行示例。这个示例可能需要较长时间才能运行,所以请耐心等待。此外,已知这个示例在某些 GPU 硬件上可能无法顺利训练。如果你遇到这种情况,只需禁用 GPU 的使用即可。
-
当示例运行时,打开与 Python 文件相同文件夹中的
images
文件夹,查看训练图像的生成过程。
运行示例,直到你理解它是如何工作的为止。即使在高级硬件上,这个示例也可能需要几个小时。完成后,继续进行下一部分,我们将看到如何修改这个示例以生成纹理。
使用 GAN 生成纹理
在深度学习的高级书籍中,很少涉及如何调整数据以输入到网络中的具体细节。除了数据调整外,还需要修改网络内部结构以适应新的数据。这个示例的最终版本是 Chapter_3_3.py
,但本练习从 Chapter_3_wgan.py
文件开始,并按以下步骤操作:
- 我们将通过交换导入语句来将训练数据集从 MNIST 切换到 CIFAR,代码如下:
from keras.datasets import mnist #remove or leave
from keras.datasets import cifar100 #add
- 在类的开始,我们将把图像尺寸的参数从 28 x 28 的灰度图像更改为 32 x 32 的彩色图像,代码如下:
class WGAN():
def __init__(self):
self.img_rows = 32
self.img_cols = 32
self.channels = 3
- 现在,移动到
train
函数并按如下方式修改代码:
#(X_train, _), (_, _) = mnist.load_data() or delete me
(X_train, y), (_, _) = cifar100.load_data(label_mode='fine')
Z_train = []
cnt = 0
for i in range(0,len(y)):
if y[i] == 33: #forest images
cnt = cnt + 1
z = X_train[i]
Z_train.append(z)
#X_train = (X_train.astype(np.float32) - 127.5) / 127.5 or delete me
#X_train = np.expand_dims(X_train, axis=3)
Z_train = np.reshape(Z_train, [500, 32, 32, 3])
Z_train = (Z_train.astype(np.float32) - 127.5) / 127.5
#X_train = (X_train.astype(np.float32) - 127.5) / 127.5
#X_train = np.expand_dims(X_train, axis=3)
-
这段代码加载 CIFAR100 数据集中的图像,并按标签进行分类。标签存储在
y
变量中,代码会遍历所有下载的图像,并将它们隔离到特定的集合中。在这个例子中,我们使用标签33
,对应的是森林图像。CIFAR100 中有 100 个类别,我们选择其中一个类别,这个类别包含 500 张图像。你也可以尝试生成其他类别的纹理。其余的代码相当简单,除了
np.reshape
调用部分,在那里我们将数据重塑为包含 500 张32x32
像素并且有三个通道的图像。你可能还需要注意,我们不再像之前那样需要扩展轴到三个通道,因为我们的图像已经是三通道的。 -
接下来,我们需要返回生成器和判别器模型,并稍微修改代码。首先,我们将按如下方式修改生成器:
def build_generator(self):
model = Sequential()
model.add(Dense(128 * 8 * 8, activation="relu", input_dim=self.latent_dim))
model.add(Reshape((8, 8, 128)))
model.add(UpSampling2D())
model.add(Conv2D(128, kernel_size=4, padding="same"))
model.add(BatchNormalization(momentum=0.8))
model.add(Activation("relu"))
model.add(UpSampling2D())
model.add(Conv2D(64, kernel_size=4, padding="same"))
model.add(BatchNormalization(momentum=0.8))
model.add(Activation("relu"))
model.add(Conv2D(self.channels, kernel_size=4, padding="same"))
model.add(Activation("tanh"))
model.summary()
noise = Input(shape=(self.latent_dim,))
img = model(noise)
return Model(noise, img)
-
粗体代码表示所做的更改。我们对这个模型所做的所有操作就是将
7x7
的原始特征图转换为8x8
。回想一下,原始图像的完整尺寸是28x28
。我们的卷积从7x7
的特征图开始,经过两次放大,得到28x28
的图像。由于我们的新图像尺寸是32x32
,我们需要将网络调整为从8x8
的特征图开始,经过两次放大得到32x32
,与 CIFAR100 图像的尺寸相同。幸运的是,我们可以保持判别器模型不变。 -
接下来,我们添加一个新函数来保存原始 CIFAR 图像的样本,代码如下:
def save_images(self, imgs, epoch):
r, c = 5, 5
gen_imgs = 0.5 * imgs + 1
fig, axs = plt.subplots(r, c)
cnt = 0
for i in range(r):
for j in range(c):
axs[i,j].imshow(gen_imgs[cnt, :,:,0],cmap='gray')
axs[i,j].axis('off')
cnt += 1
fig.savefig("images/cifar_%d.png" % epoch)
plt.close()
save_images
函数输出原始图像样本,并通过以下代码在train
函数中调用:
idx = np.random.randint(0, Z_train.shape[0], batch_size)
imgs = Z_train[idx]
if epoch % sample_interval == 0:
self.save_images(imgs, epoch)
- 新代码是粗体部分,输出的是原始图像的一个样本,如下所示:
原始图像示例
- 运行示例并再次查看
images
文件夹中的输出,文件夹名称为cifar
,展示训练结果。再次提醒,这个示例可能需要一些时间来运行,因此请继续阅读下一部分。
在样本运行时,你可以观察到 GAN 是如何训练以匹配这些图像的。这里的好处是,你可以通过多种技术轻松生成不同的纹理。你可以将这些纹理或高度图用作 Unity 或其他游戏引擎中的素材。在完成本节之前,我们先来讨论一些归一化和其他参数。
批量归一化
批量归一化,顾名思义,它会将一层中权重的分布标准化,使其围绕均值 0。这样可以让网络使用更高的学习率,同时避免梯度消失或爆炸的问题。这是因为权重被标准化,从而减少了训练过程中的偏移或震荡,正如我们之前看到的那样。
通过标准化一层中的权重,我们使网络能够使用更高的学习率,从而加速训练。此外,我们还可以避免或减少使用DropOut
的需要。你会看到我们使用标准术语来标准化这些层,如下所示:
model.add(BatchNormalization(momentum=0.8))
从我们对优化器的讨论中回忆一下,动量控制着我们希望训练梯度减少的快慢。在这里,动量指的是标准化分布的均值或中心变化的程度。
在接下来的部分,我们将讨论另一种特殊的层——LeakyReLU。
Leaky 和其他 ReLU
LeakyReLU添加了一个激活层,允许负值有一个小的斜率,而不是像标准 ReLU 激活函数那样为 0。标准 ReLU 通过只允许正激活的神经元激活,鼓励网络的稀疏性。然而,这也会导致死神经元的状态,网络的某些部分实际上会“死亡”或变得无法训练。为了解决这个问题,我们引入了一种名为 LeakyReLU 的 ReLU 激活形式。以下是这种激活方式的示例:
Leaky 和参数化 ReLU 的示例
在前面的图示中是参数化 ReLU,它类似于 Leaky,但允许网络自行训练参数。这使得网络能够自我调整,但训练时间会更长。
你可以使用的其他 ReLU 变体总结如下:
- 指数线性 (ELU, SELU): 这些 ReLU 形式的激活如图所示:
ELU 和 SELU
-
连接 ReLU (CReLU): 这一层将常规 ReLU 和 LeakyReLU 结合在一起,提供一种新的功能,生成两个输出值。对于正值,它产生* [0,x]*,而对于负值,它返回 [x,0]。需要注意的是,这一层的输出翻倍,因为每个神经元会生成两个值。
-
ReLU-6:6 的值是任意的,但它允许网络训练稀疏的神经元。稀疏性具有价值,因为它鼓励网络学习或建立更强的权重或连接。已经有研究表明,人脑在稀疏状态下工作,通常只有少数几个神经元同时被激活。你经常会听到一个神话,说我们大脑最多一次只使用 10%。这可能是真的,但其中的原因更多的是数学上的问题,而不是我们无法使用大脑的全部功能。我们确实使用大脑的全部,只不过不是同时使用所有部分。稀疏性鼓励的更强的单个权重,使网络能够做出更好、更强的决策。更少的权重也能减少过拟合或数据记忆的情况。这种情况常发生在具有成千上万神经元的深度网络中。
正则化是我们经常使用的另一种技术,用于修剪或减少不需要的权重,创建稀疏网络。在接下来的章节中,我们将有机会更深入地了解正则化和稀疏性。
在下一节中,我们将利用所学知识构建一个能生成游戏音乐的工作型音乐 GAN。
创建音乐的 GAN
在本章的最终示例中,我们将研究使用 GAN 为游戏生成音乐。音乐生成本身并不特别困难,但它让我们看到了使用 LSTM 层的 GAN 变体,该变体能够识别音乐中的序列和模式。然后,它尝试将随机噪音重建成可接受的音符序列和旋律。当你听到那些生成的音符并意识到这段旋律来自计算机的大脑时,这个示例会显得非常虚幻。
这个示例的来源来自 GitHub,github.com/megis7/musegen
,并由 Michalis Megisoglou 开发。我们查看这些代码示例的原因是为了看到别人最优秀的作品,并从中学习。在某些情况下,这些示例接近原始版本,而在其他情况下则不完全相同。我们确实做了一些调整。Michalis 还在 GitHub 上发布了他为实现museGAN(基于 GAN 的音乐生成)编写的代码的详细 README。如果你有兴趣进一步扩展这个示例,务必查看 GitHub 网站。不同的库有几个 museGAN 的实现,其中之一是 TensorFlow。
在这个示例中,我们使用 Keras,目的是使示例更易于理解。如果你对使用 TensorFlow 非常认真,那么一定要查看 museGAN 的 TensorFlow 版本。
这个示例分别训练判别器和生成器,这意味着需要先训练判别器。对于我们的第一次运行,我们将使用作者之前生成的模型来运行这个示例,但我们仍然需要一些设置;让我们按照以下步骤进行:
- 我们首先需要安装一些依赖项。以管理员身份打开 Anaconda 或 Python 窗口,并运行以下命令:
pip install music21
pip install h5py
-
Music21
是一个用于加载 MIDI 文件的 Python 库。MIDI是一种音乐交换格式,用于描述音乐/音符,正如你所猜测的那样。原始模型是通过一组描述巴赫 300 首合唱音乐的 MIDI 文件进行训练的。你可以通过导航到musegen
文件夹并运行脚本来找到该项目。 -
导航到项目文件夹,并执行运行先前训练的模型的脚本,如下所示:
cd musegen
python musegen.py or python3 musegen.py
-
这将加载先前保存的模型,并使用这些模型来训练生成器并生成音乐。当然,您稍后可以根据需要使用您选择的其他 MIDI 文件训练这个 GAN。对于 MIDI 文件,有许多免费的来源,包括古典音乐、电视主题音乐、游戏和现代流行音乐。我们在这个例子中使用的是作者的原始模型,但可能性是无穷无尽的。
-
加载音乐文件和训练可能会非常耗时,因为训练通常需要较长时间。所以,趁此机会查看一下代码。打开项目文件夹中的
musegen.py
文件。查看大约第 39 行,如下所示:
print('loading networks...')
dir_path = os.path.dirname(os.path.realpath(__file__))
generator = loadModelAndWeights(os.path.join(dir_path, note_generator_dir, 'model.json'),
os.path.join(dir_path, note_generator_dir, 'weights-{:02d}.hdf5'.format(generator_epoch)))
-
这一段代码加载了从
hdf5
或分层数据文件中训练的模型。前面的代码设置了多个变量,用于定义我们将在生成新音符时使用的音符词汇。 -
找到项目文件夹中名为
notegenerator.py
的文件。查看模型创建的代码,如下所示:
x_p = Input(shape=(sequence_length, pitch_dim,), name='pitches_input')
h = LSTM(256, return_sequences=True, name='h_lstm_p_1')(x_p)
h = LSTM(512, return_sequences=True, name='h_lstm_p_2')(h)
h = LSTM(256, return_sequences=True, name='h_lstm_p_3')(h)
# VAE for pitches
z_mean_p = TimeDistributed(Dense(latent_dim_p, kernel_initializer='uniform'))(h)
z_log_var_p = TimeDistributed(Dense(latent_dim_p, kernel_initializer='uniform'))(h)
z_p = Lambda(sampling)([z_mean_p, z_log_var_p])
z_p = TimeDistributed(Dense(pitch_dim, kernel_initializer='uniform', activation='softmax'))(z_p)
x_d = Input(shape=(sequence_length, duration_dim, ), name='durations_input')
h = LSTM(128, return_sequences=True)(x_d)
h = LSTM(256, return_sequences=True)(h)
h = LSTM(128, return_sequences=True)(h)
# VAE for durations
z_mean_d = TimeDistributed(Dense(latent_dim_d, kernel_initializer='uniform'))(h)
z_log_var_d = TimeDistributed(Dense(latent_dim_d, kernel_initializer='uniform'))(h)
z_d = Lambda(sampling)([z_mean_d, z_log_var_d])
z_d = TimeDistributed(Dense(duration_dim, kernel_initializer='uniform', activation='softmax'))(z_d)
conc = Concatenate(axis=-1)([z_p, z_d])
latent = TimeDistributed(Dense(pitch_dim + duration_dim, kernel_initializer='uniform'))(conc)
latent = LSTM(256, return_sequences=False)(latent)
o_p = Dense(pitch_dim, activation='softmax', name='pitches_output', kernel_initializer='uniform')(latent)
o_d = Dense(duration_dim, activation='softmax', name='durations_output', kernel_initializer='uniform')(latent)
-
注意我们如何从使用
Conv2D
层改为使用LSTM
层,因为我们已经从图像识别转向了序列或音符模式识别。我们还从使用更直接的层次结构转向了复杂的时间分布架构。此外,作者使用了一种称为变分自编码的概念,用于确定序列中音符的分布。这个网络是我们迄今为止看到的最复杂的,内容非常丰富。对于这个例子,不必过于担心,只需看看代码的流向。我们将在第四章*《构建深度学习游戏聊天机器人》*中详细探讨更多这种类型的高级时间分布网络. -
让示例运行并生成一些音乐样本到
samples/note-generator
文件夹。随着我们进入更复杂的问题,训练时间将从几个小时变成几天,甚至更长。很可能你会轻松生成一个网络,但却没有足够的计算能力在合理时间内完成训练。 -
打开文件夹,双击其中一个示例文件来听听生成的 MIDI 文件。记住,这段音乐是由计算机“大脑”生成的。
在这个示例中,我们没有涵盖很多代码。所以,请务必返回并查看musegen.py
文件,以更好地理解用于构建网络生成器的流程和层类型。在下一部分,我们将探讨如何训练这个 GAN。
训练音乐 GAN
在开始训练这个网络之前,我们将查看作者原始 GitHub 源码中展示的整体架构:
museGAN 网络架构概述
这些网络几乎完全相同,直到你仔细观察并发现 LSTM 层的细微差异。注意,某一模型使用的单元数是另一个模型的两倍。
我们可以通过在 Python 或 Anaconda 提示符下运行以下命令来生成音乐模型:
python note-generator.py
or
python3 note-generator.py
这个脚本加载示例数据并生成我们在稍后创建原创音乐时会在musegen.py
文件中使用的模型。打开note-generator.py
文件,主要部分如下所示:
代码已经从原始版本进行了修改,以使其更加兼容 Windows 并支持跨平台。再次强调,这绝不是对作者出色工作的批评。
def loadChorales():
notes = []
iterator = getChoralesIterator()
# load notes of chorales
for chorale in iterator[1:maxChorales]: # iterator is 1-based
transpose_to_C_A(chorale.parts[0])
notes = notes + parseToFlatArray(chorale.parts[0])
notes.append((['end'], 0.0)) # mark the end of the piece
return notes
该代码使用 Music21 库来读取 MIDI 音符和其他音乐形式,来自您可以用于自己测试的音乐语料库。这个训练数据集是生成其他音乐来源的一个很好的方式,包含以下内容:web.mit.edu/music21/doc/moduleReference/moduleCorpus.html
。
您可以通过修改config.py
文件中的内容或添加额外的配置选项来进一步修改此示例,文件示例如下:
# latent dimension of VAE (used in pitch-generator)
latent_dim = 512
# latent dimensions for pitches and durations (used in note-generator)
latent_dim_p = 512
latent_dim_d = 256
# directory for saving the note embedding network model --- not used anymore
note_embedding_dir = "models/note-embedding"
# directory for saving the generator network model
pitch_generator_dir = 'models/pitch-generator'
# directory for saving the note generator network model
note_generator_dir = 'models/note-generator'
# directory for saving generated music samples
output_dir = 'samples'
上一个示例非常适合探索音乐生成。一个更实用且潜在有用的示例将在下一部分介绍。
通过另一种 GAN 生成音乐
另一个音乐生成示例也包含在Chapter_3
源文件夹中,名为Classical-Piano-Composer,其源代码位于github.com/Skuldur/Classical-Piano-Composer
,由 Sigurður Skúli 开发。这个示例使用了完整的《最终幻想》MIDI 文件作为音乐生成的灵感来源,是一个生成自己音乐的极好实用示例。
为了运行这个示例,您需要先运行lstm.py
,并使用以下命令从Classical-Piano-Composer
项目文件夹中执行:
python lstm.py
or
python3 lstm.py
这个示例可能需要相当长的时间来训练,所以请确保打开文件并阅读它的功能。
模型训练完成后,您可以通过运行以下命令来运行生成器:
python predict.py
or
python3 predict.py
这个脚本加载了训练好的模型并生成音乐。它通过将 MIDI 音符编码为网络输入,按序列或音符集的形式进行处理。我们在这里做的就是将音乐文件分解成短序列,或者如果你愿意,可以称之为音乐快照。你可以通过调整代码文件中的sequences_length
属性来控制这些序列的长度。
这个第二个示例的一个优点是可以下载你自己的 MIDI 文件并将其放入适当的输入文件夹进行训练。更有趣的是,两个项目都使用了类似的三层 LSTM 结构,但在其他执行方式上差异较大。
如果你想深入了解游戏中的音频或音乐开发,尤其是在 Unity 中的开发,可以阅读 Micheal Lanham 的书籍《Game Audio Development with Unity 5.x》。这本书可以向你展示更多在游戏中处理音频和音乐的技巧。
这两个音乐样本的训练和生成音乐可能需要一些时间,但毫无疑问,通过运行这两个示例并理解它们的工作原理,绝对值得付出努力。GAN 技术革新了我们对神经网络训练的理解,并改变了它们能够产生的输出类型。因此,它们在生成游戏内容方面无疑具有重要地位。
练习
花些时间通过进行以下练习来巩固你的学习:
-
你会使用哪种类型的 GAN 来在图像上转移风格?
-
你会使用哪种类型的 GAN 来隔离或提取风格?
-
修改 Wasserstein GAN 示例中使用的评论者数量,看看它对训练的影响。
-
修改第一个 GAN,即 DCGAN,使用你在本章中学到的任何技巧来提高训练性能。你是如何提高训练性能的?
-
修改 BatchNormalization 动量参数,看看它对训练的影响。
-
修改一些样本,将激活函数从 LeakyReLU 更改为另一种更高级的激活形式。
-
修改 Wasserstein GAN 示例,使用你自己的纹理。在章节下载的代码示例中有一个示例数据加载器可供使用。
-
从
github.com/eriklindernoren/Keras-GAN
下载其他参考 GAN 之一,并修改它以使用你自己的数据集。 -
修改第一个音乐生成 GAN,使用不同的语料库。
-
使用你自己的 MIDI 文件来训练第二个音乐生成 GAN 示例。
-
(附加题)哪个音乐 GAN 生成的音乐更好?它是你预期的吗?
你当然不必完成所有这些练习,但可以尝试做几个。立刻将这些知识应用到实践中,能够大大提高你对材料的理解。毕竟,实践才能完美。
总结
在本章中,我们探讨了生成对抗网络(GAN),它是一种构建深度神经网络(DNN)的方法,可以通过复制或提取其他内容的特征来生成独特的内容。这也使我们能够探索无监督学习,这是一种无需先前数据分类或标记的训练方法。在上一章中,我们使用了监督学习。我们从研究当前在深度学习社区产生影响的各种 GAN 变种开始。然后,我们用 Keras 编写了一个深度卷积 GAN,接着介绍了最先进的 Wasserstein GAN。随后,我们探讨了如何利用样本图像生成游戏纹理或高度图。最后,我们通过研究两个能够从样本音乐生成原创 MIDI 音乐的音乐生成 GAN,结束了本章的内容。
在最后的示例中,我们研究了依赖于 RNN(LSTM)的生成对抗网络(GAN)在音乐生成中的应用。我们将在接下来的章节中继续探讨 RNN,重点讲解如何为游戏构建深度学习聊天机器人。
第四章:构建深度学习游戏聊天机器人
聊天机器人,或称对话代理,是人工智能领域一个迅速发展的趋势,被视为与计算机互动的下一个人类界面。从 Siri、Alexa 到 Google Home,这一领域的商业增长势不可挡,你很可能已经以这种方式与计算机进行了互动。因此,讨论如何为游戏构建对话代理似乎是理所当然的。然而,出于我们的目的,我们将关注一类被称为神经对话代理的机器人。它们的名字来源于它们是通过神经网络开发的。现在,聊天机器人不仅仅是聊天;我们还将探讨对话机器人在游戏中可以采取的其他应用方式。
在本章中,我们将学习如何构建神经对话代理,并将这些技术应用于游戏中。以下是我们将讨论的主要内容摘要:
-
神经对话代理
-
序列到序列学习
-
DeepPavlov
-
构建机器人服务器
-
在 Unity 中运行机器人
-
练习
我们现在将开始构建更实际的、能够在现实中工作的项目示例。虽然并非所有的训练都已完成,但现在是时候开始构建你可以使用的部分了。这意味着我们将在本章开始使用 Unity,事情可能很快变得复杂。只要记得慢慢来,如果需要的话,可以多看几遍材料。同样,本章末尾的练习是一个非常好的额外学习资源。
在接下来的章节中,我们将探讨神经对话代理的基础知识。
神经对话代理
通过自然语言与计算机进行交流的概念早在《星际迷航》(1966 至 1969 年)时期就已流行。在该系列中,我们经常看到柯克、斯科蒂(Scotty)等人向计算机发出指令。从那时起,许多人试图构建能够与人类自然对话的聊天机器人。在这条常常不成功的道路上,几种语言学方法被开发了出来。这些方法通常被归类为自然语言处理,或称NLP。现在,NLP 仍然是大多数聊天机器人的基础,包括我们稍后将介绍的深度学习类型。
我们通常根据目的或任务将对话代理分组。目前,我们将聊天机器人分为两种主要类型:
-
目标导向:这些机器人是柯克(Kirk)可能使用的那种,或者是你每天可能与之沟通的机器人,一个很好的例子是 Siri 或 Alexa。
-
通用对话者:这些聊天机器人旨在与人类就广泛的话题进行对话,一个好的例子是微软 Tay。不幸的是,Tay 机器人可能有些太容易受影响,学会了不良语言,类似于一个两岁孩子的行为。
游戏与聊天机器人并不陌生,已经尝试使用这两种形式,并取得了不同程度的成功。虽然你可能认为目标导向型的机器人非常合适,但实际上,语音/文本对于大多数重复性的游戏任务来说,速度太慢且枯燥。即使是简单的语音命令(咕哝或呻吟)也太慢了,至少现在是这样。因此,我们将探讨那些经常未被充分利用的对话型聊天机器人,以及它们如何在游戏中发挥作用。
以下是这些机器人可以承担的游戏任务总结:
-
非玩家角色(NPCs):这是一个显而易见的首选。NPC 通常是预设脚本,容易变得单调重复。那么,如何设计一个能够自然对话的 NPC 呢?也许在使用正确的词语或短语组合时,它能透露信息?这里的可能性是无穷的,实际上,游戏中已经开始使用一些自然语言处理(NLP)技术来实现这一点。
-
玩家角色:如果有一个游戏,你可以和自己对话怎么样?也许角色失去了记忆,正在努力回忆信息或了解背景故事。
-
推广/提示:也许作为推广你的游戏的一种方式,你可以创建一个机器人,提示如何完成一些困难任务,或者仅仅作为一种谈论游戏的方式。
-
MMO 虚拟角色:如果在你离开你最喜欢的 MMO 游戏时,你的角色依然留在游戏中,无法进行任何操作,但仍能像你一样与人对话,那会怎么样?这是我们将在本章讨论的示例,我们稍后会介绍如何实现动作部分,当我们探讨强化学习时。
随着时间的推移,可能会出现更多的用途,但现在前面的列表应该能给你一些关于如何在游戏中使用聊天机器人的好点子。在下一部分,我们将讨论构建对话型机器人背后的背景。
一般的对话模型
对话型聊天机器人可以进一步分为两种主要形式:生成型和选择型。我们将要讨论的方法是生成型。生成型模型通过输入一系列带有上下文和回复的词语和对话来学习。内部,这些模型使用 RNN(LSTM)层来学习并预测这些序列,并将它们返回给对话者。以下是该系统如何工作的一个示例:
生成型对话模型的示例
请注意,图示中的每个块代表一个 LSTM 单元。每个单元会记住文本所在的序列。前面图示中可能不太清楚的是,对话文本的双方在训练前都被输入到模型中。因此,这个模型与我们在第三章*《游戏中的 GAN》*中讨论的 GAN 有些相似。在下一部分,我们将深入讨论如何搭建这种类型的模型。
序列到序列学习
在上一节中,我们看到我们的网络模型的概述。在这一节中,我们将介绍一个使用序列到序列学习的生成式对话模型的 Keras 实现。在我们深入探讨这种生成模型的理论之前,让我们先运行一个示例,因为这可能需要一些时间。我们将探索的示例是 Keras 提供的序列到序列机器翻译的参考示例。它当前配置为执行英法翻译。
打开 Chapter_4_1.py
示例代码并按照以下步骤运行:
- 打开一个 shell 或 Anaconda 窗口。然后运行以下命令:
python3 Chapter_4_1.py
- 这将运行示例,可能需要几个小时才能完成。该示例还可能消耗大量内存,这可能会导致低内存系统进行内存分页。将内存分页到磁盘将花费额外的训练时间,特别是如果你没有使用 SSD。如果你发现无法完成这个示例的训练,可以减少
epochs
和/或num_samples
参数,如下所示:
batch_size = 64 # Batch size for training.
epochs = 100 # Number of epochs to train for.
latent_dim = 256 # Latent dimensionality of the encoding space.
num_samples = 10000 # Number of samples to train on.
-
如果你无法使用原始值进行训练,可以减少
epochs
或num_samples
参数。 -
当示例完成训练后,它将运行一个测试数据集。在此过程中,它会输出结果,你可以看到它从英语翻译到法语的效果如何。
-
打开位于章节源代码中的
fra-eng
文件夹。 -
打开
fra.txt
文件,前几行如下:
Go. Va !
Hi. Salut !
Run! Cours !
Run! Courez !
Wow! Ça alors !
Fire! Au feu !
Help! À l'aide !
Jump. Saute.
Stop! Ça suffit !
Stop! Stop !
Stop! Arrête-toi !
Wait! Attends !
Wait! Attendez !
Go on. Poursuis.
Go on. Continuez.
Go on. Poursuivez.
Hello! Bonjour !
Hello! Salut !
- 请注意训练文本(英语/法语)是如何根据标点符号和空格进行拆分的。同时,也要注意序列的长度是如何变化的。我们输入的序列不必与输出的长度匹配,反之亦然。
我们刚才看的示例使用了序列到序列字符编码将文本从英语翻译成法语。通常,聊天生成是通过逐词编码完成的,但这个示例使用了更细粒度的字符到字符模型。这在游戏中有一个优势,因为我们尝试生成的语言不一定总是人类语言。请记住,尽管在这个示例中我们只是生成翻译文本,任何与输入配对的文本都可以是你认为合适的任何响应。在下一节中,我们将逐步解析代码,深入理解这个示例的工作原理。
逐步解析代码
随着我们继续深入本书,我们将开始只专注于重要的代码部分,这些部分有助于我们理解一个概念或方法是如何实现的。这样,你需要更重视自己打开代码并至少独立进行探索。在下一个练习中,我们将关注示例代码中的重要部分:
- 打开
Chapter_4_1.py
并向下滚动到注释Vectorize the data
,如下所示:
# Vectorize the data.
input_texts = []
target_texts = []
input_characters = set()
target_characters = set()
with open(data_path, 'r', encoding='utf-8') as f:
lines = f.read().split('\n')
for line in lines[: min(num_samples, len(lines) - 1)]:
input_text, target_text = line.split('\t')
# We use "tab" as the "start sequence" character
# for the targets, and "\n" as "end sequence" character.
target_text = '\t' + target_text + '\n'
input_texts.append(input_text)
target_texts.append(target_text)
for char in input_text:
if char not in input_characters:
input_characters.add(char)
for char in target_text:
if char not in target_characters:
target_characters.add(char)
input_characters = sorted(list(input_characters))
target_characters = sorted(list(target_characters))
num_encoder_tokens = len(input_characters)
num_decoder_tokens = len(target_characters)
max_encoder_seq_length = max([len(txt) for txt in input_texts])
max_decoder_seq_length = max([len(txt) for txt in target_texts])
print('Number of samples:', len(input_texts))
print('Number of unique input tokens:', num_encoder_tokens)
print('Number of unique output tokens:', num_decoder_tokens)
print('Max sequence length for inputs:', max_encoder_seq_length)
print('Max sequence length for outputs:', max_decoder_seq_length)
-
这一部分代码输入训练数据,并将其编码成用于向量化的字符序列。请注意,这里设置的
num_encoder_tokens
和num_decoder_tokens
参数依赖于每个字符集中的字符数,而非样本数。最后,编码和解码序列的最大长度是根据这两者中编码字符的最大长度来设置的。 -
接下来,我们要看看输入数据的向量化。数据的向量化减少了每个响应匹配的字符数量,同时也是内存密集型的部分,但当我们对齐这些数据时,我们希望保持响应或目标比原始输入提前一步。这一微妙的差异使得我们的序列学习 LSTM 层能够预测序列中的下一个模式。接下来是这一过程如何运作的示意图:
序列到序列模型
-
在图示中,我们可以看到HELLO的开始是如何被翻译为与响应短语SALUT(法语中的hello)相对应的,这一步是在响应短语之前发生的。请注意在前面的代码中这如何实现。
-
然后我们构建将映射到我们网络模型的层,代码如下:
# Define an input sequence and process it.
encoder_inputs = Input(shape=(None, num_encoder_tokens))
encoder = LSTM(latent_dim, return_state=True)
encoder_outputs, state_h, state_c = encoder(encoder_inputs)
# We discard `encoder_outputs` and only keep the states.
encoder_states = [state_h, state_c]
# Set up the decoder, using `encoder_states` as initial state.
decoder_inputs = Input(shape=(None, num_decoder_tokens))
# We set up our decoder to return full output sequences,
# and to return internal states as well. We don't use the
# return states in the training model, but we will use them in inference.
decoder_lstm = LSTM(latent_dim, return_sequences=True, return_state=True)
decoder_outputs, _, _ = decoder_lstm(decoder_inputs,
initial_state=encoder_states)
decoder_dense = Dense(num_decoder_tokens, activation='softmax')
decoder_outputs = decoder_dense(decoder_outputs)
# Define the model that will turn
# `encoder_input_data` & `decoder_input_data` into `decoder_target_data`
model = Model([encoder_inputs, decoder_inputs], decoder_outputs)
# Run training
model.compile(optimizer='rmsprop', loss='categorical_crossentropy')
model.fit([encoder_input_data, decoder_input_data], decoder_target_data,
batch_size=batch_size,
epochs=epochs,
validation_split=0.2)
# Save model
model.save('s2s.h5')
- 注意我们是如何创建编码器和解码器输入以及解码器输出的。此代码构建并训练了
model
,然后将其保存以供后续推理使用。我们使用术语推理来表示模型正在推断或生成对某些输入的答案或响应。接下来是该序列到序列模型在层级结构中的示意图:
编码器/解码器推理模型
- 这个模型相当复杂,涉及了许多内容。我们刚刚讲解了模型的第一部分。接下来,我们需要讲解构建思维向量和生成采样模型。生成这部分代码如下:
encoder_model = Model(encoder_inputs, encoder_states)
decoder_state_input_h = Input(shape=(latent_dim,))
decoder_state_input_c = Input(shape=(latent_dim,))
decoder_states_inputs = [decoder_state_input_h, decoder_state_input_c]
decoder_outputs, state_h, state_c = decoder_lstm(
decoder_inputs, initial_state=decoder_states_inputs)
decoder_states = [state_h, state_c]
decoder_outputs = decoder_dense(decoder_outputs)
decoder_model = Model(
[decoder_inputs] + decoder_states_inputs,
[decoder_outputs] + decoder_states)
# Reverse-lookup token index to decode sequences back to
# something readable.
reverse_input_char_index = dict(
(i, char) for char, i in input_token_index.items())
reverse_target_char_index = dict(
(i, char) for char, i in target_token_index.items())
查看这段代码,看看你是否能理解其结构。我们仍然缺少一个关键部分,接下来将在下一节中讨论。
思维向量
在编码和解码文本处理的中间部分是生成思维向量。思维向量由“教父”Geoffrey Hinton 博士推广,代表了一个向量,显示了一个元素与许多其他元素之间的上下文关系。
例如,单词hello可能与许多单词或短语有较高的关联上下文,比如hi、how are you?、hey、goodbye等等。同样,单词如red、blue、fire和old在与单词hello关联时的上下文会较低,至少在日常口语中是如此。单词或字符的上下文是基于我们在机器翻译文件中的配对。在这个例子中,我们使用的是法语翻译配对,但这些配对可以是任何语言的。
这个过程是将第一个编码模型转换为思维向量的一部分,或者在这个例子中是一个概率向量。LSTM 层计算单词/字符之间如何关联的概率或上下文。你将经常遇到以下方程,它描述了这个转换过程:
请考虑以下内容:
这个 https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-gm/img/63dfa677-e45e-441c-a9ea-28c0a85f887b.png 表示 sigma 的乘法形式 (https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-gm/img/b107c7b6-9f98-40a1-b8c9-ebe14b24cffd.png),并用于将概率汇聚成思维向量。这是对整个过程的一个大大简化,感兴趣的读者可以自行在 Google 上查找更多关于序列到序列学习的资料。对于我们的目的,关键是要记住,每个词/字符都有一个概率或上下文,将其与另一个词/字符关联起来。生成这个思维向量可能会非常耗时且占用大量内存,正如你可能已经注意到的那样。因此,在我们的应用中,我们将查看一组更全面的自然语言工具,以便在接下来的章节中创建一个神经对话机器人。
DeepPavlov
DeepPavlov 是一个全面的开源框架,用于构建聊天机器人和其他对话代理,适用于各种目的和任务。虽然这个机器人是为目标导向型的机器人设计的,但它非常适合我们,因为它功能全面,并且包含几种序列到序列模型的变体。接下来让我们看看如何在以下步骤中构建一个简单的模式(序列到序列)识别模型:
- 到目前为止,我们一直保持 Python 环境比较宽松,但这必须改变。我们现在想要隔离我们的开发环境,以便稍后可以轻松地将其复制到其他系统。做到这一点的最佳方式是使用 Python 虚拟环境。创建一个新环境,然后在 Anaconda 窗口中使用以下命令激活它:
#Anaconda virtual environment
conda create --name dlgames
#when prompted choose yes
activate dlgames
- 如果你没有使用 Anaconda,过程就会稍微复杂一些,步骤如下:
#Python virtual environment
pip install virtualenv
virutalenv dlgames
#on Mac
source dlgames/bin/activate
#on Windows
dlgames\Scripts\activate
- 然后,我们需要在 shell 或 Anaconda 窗口中使用以下命令安装 DeepPavlov:
pip install deeppavlov
-
这个框架将尝试安装几个库,并可能会干扰现有的 Python 环境。这也是我们现在使用虚拟环境的另一个原因。
-
对于我们的目的,我们将只查看基本的
Hello World
示例,现在我们已经涵盖了背景知识,它非常容易理解。我们首先按照标准导入库,代码如下:
from deeppavlov.skills.pattern_matching_skill import PatternMatchingSkill
from deeppavlov.agents.default_agent.default_agent import DefaultAgent
from deeppavlov.agents.processors.highest_confidence_selector import HighestConfidenceSelector
-
目前,DeepPavlov 是基于 Keras 的,但正如你所见,我们在这里使用的类型包装了一个序列到序列的模式匹配模型的功能。
PatternMatchingSkill
代表了我们想要赋予聊天机器人代理的序列到序列模型。接下来,我们导入DefaultAgent
类型,它是一个基本代理。之后,我们引入一个名为HighestConfidenceSelector
的置信度选择器。记住,我们生成的思维向量是一个概率向量。HighestConfidenceSelector
选择器始终选择与相应词匹配的最高值关系或上下文。 -
接下来,我们生成三组模式和对应的响应,如下代码所示:
hello = PatternMatchingSkill(responses=['Hello world!'], patterns=["hi", "hello", "good day"])
bye = PatternMatchingSkill(['Goodbye world!', 'See you around'], patterns=["bye", "ciao", "see you"])
fallback = PatternMatchingSkill(["I don't understand, sorry", 'I can say "Hello world!"'])
-
每个
PatternMatchingSkill
代表一组模式/响应上下文对。注意,每个模式可能对应多个响应和模式。这个框架的另一个优点是可以互换和添加技能。在这个例子中,我们只使用了模式匹配,但读者可以探索更多其他技能。 -
最后,我们通过简单地打印结果来构建代理并运行它,代码如下:
HelloBot = DefaultAgent([hello, bye, fallback], skills_selector=HighestConfidenceSelector())
print(HelloBot(['Hello!', 'Boo...', 'Bye.']))
-
这段代码的最后部分创建了一个
DefaultAgent
,包含了三个技能(hello
、bye
和fallback
),并使用HighestConfidenceSelector
。然后,通过将三组输入嵌套在print
语句中,运行该代理。 -
像往常一样运行代码并查看输出结果。它是你预期的结果吗?
DeepPavlov 的简洁性使它成为构建各种对话聊天机器人(用于游戏或其他目的)的一款优秀工具。该框架本身功能非常强大,并提供了多种自然语言处理工具,适用于多种任务,包括面向目标的聊天机器人。整个书籍可能会、也应该会围绕 Pavlov 写;如果你对此感兴趣,可以进一步查找 NLP 和 DeepPavlov 相关的资料。
有了新的工具,我们现在需要一个平台来提供具有出色对话能力的机器人。在接下来的部分,我们将探讨如何为我们的机器人构建一个服务器。
构建聊天机器人服务器
Python 是一个很棒的框架,提供了许多用于游戏开发的优秀工具。然而,我们将专注于使用 Unity 来满足我们的需求。Unity 是一个出色且非常用户友好的游戏引擎,它将使得在后续章节中设置复杂的示例变得轻松。即使你不懂 C#(Unity 的语言),也不用担心,因为我们在很多情况下将通过 Python 来操作引擎。这意味着我们希望能够在 Unity 之外运行我们的 Python 代码,并且希望能够在服务器上执行。
如果你在用 Python 开发游戏,那么是否使用服务器变得可选,除非有非常强烈的理由将你的 AI 机器人设置为服务或微服务。微服务是自包含的简洁应用或服务,通常通过一些知名的通信协议进行交互。AI 微服务 或 AI 即服务(AIaaS)正在迅速超过其他形式的 SaaS,并且这一商业模式迟早会扩展到游戏领域。无论如何,目前我们从将聊天机器人创建为微服务中获得的好处是 解耦。解耦将使你未来能够轻松地将这个机器人迁移到其他平台。
微服务还引入了一种新的通信模式。通常,当客户端应用连接到服务器时,通信是直接且即时的。但如果你的连接中断,或者需要对通信进行筛选、复制或存储以便以后分析或重用呢?这时,使用直接的通信协议就会被附加的功能所拖累,实际上这些功能本不需要在直接协议中实现。相反,微服务引入了 消息中心 的概念。它本质上是一个容器或邮局,所有的消息流量都通过这里。这带来了极大的灵活性,并将我们的通信协议从管理额外任务的负担中解放出来。接下来的部分,我们将看看如何安装一个非常易用的消息中心。
消息中心(RabbitMQ)
如果你之前从未接触过微服务或消息中心的概念,接下来的内容可能会让你有些担忧。但不必担心。消息中心和微服务的设计目的是使多个需要互相通信的服务更容易连接、路由和排除故障。因此,这些系统的设计是易于设置和使用的。让我们看看接下来的练习中,如何轻松地设置一个叫做 RabbitMQ 的优秀消息队列平台:
-
打开浏览器访问
www.rabbitmq.com/#getstarted
。 -
下载并为你的平台安装 RabbitMQ。通常,页面顶部会有一个下载按钮。你可能会被提示安装 Erlang,如下所示:
Erlang 警告对话框
-
Erlang 是一种并发函数式编程语言,非常适合编写消息中心。如果你的系统没有安装它,只需下载并安装适合你平台的版本;接下来,重新启动 RabbitMQ 的安装。
-
大部分情况下,按照默认选项进行安装,除了安装路径。确保将安装路径设置得简短且易于记忆,因为稍后我们需要找到它。以下是在 Windows 上的安装程序中设置路径的示例:
在 Windows 上设置安装路径的示例
-
RabbitMQ 将作为一个服务安装在您的平台上。根据您的系统,您可能会收到一些安全提示,要求防火墙或管理员权限。只需允许所有这些例外,因为该中心需要完全访问权限。当安装完成后,RabbitMQ 应当在您的系统上运行。如果您对配置或设置有任何疑问,请务必查看您平台的文档。RabbitMQ 设计为使用安全通信,但为了开发目的,它保持了相当开放的状态。请避免在生产系统中安装该中心,并准备进行一些安全配置。
-
接下来,我们需要激活 RabbitMQ 管理工具,以便我们能全面了解该中心的工作方式。打开命令提示符,导航到
RabbitMQ
安装服务器文件夹(标记为 server)。然后导航到sbin
文件夹。当您到达那里时,运行以下命令以安装管理插件(Windows 或 macOS):
rabbitmq-plugins enable rabbitmq_management
- 以下是 Windows 命令提示符中显示的示例:
安装 RabbitMQ 管理插件
这完成了您系统上中心的安装。在下一部分中,我们将看到如何使用管理界面检查该中心。
管理 RabbitMQ
RabbitMQ 是一个功能齐全的消息中心,具有非常强大和灵活的功能。RabbitMQ 有很多特性,可能会让一些对网络不太熟悉的用户感到有些畏惧。幸运的是,我们现在只需要使用其中的一些功能,未来我们将探索更多的功能。
目前,请打开浏览器,并按照以下步骤探索该中心的管理界面:
-
在浏览器中导航到
http://localhost:15672/
,您应该能看到一个登录对话框。 -
输入用户名
guest
和密码guest
。这些是默认的凭据,除非您做了其他配置,否则应该有效。 -
登录后,您将看到 RabbitMQ 界面:
RabbitMQ 管理界面
- 这里有很多内容要了解,所以现在只需点击并探索各种选项。避免更改任何设置,至少在现在和未被要求之前不要更改。RabbitMQ 非常强大,但我们都知道,强大的能力伴随着巨大的责任。
目前,您的消息队列是空的,因此不会看到很多活动,但我们很快将在下一部分解决这个问题,届时我们将学习如何向队列发送和接收消息。
向 MQ 发送和接收消息
RabbitMQ 使用一种名为高级消息队列协议(AMQP)的协议进行通信,这是一种所有消息中间件的标准。这意味着我们可以在未来有效地将 RabbitMQ 替换为更强大的系统,例如 Kafka。也就是说,大部分我们在这里讲解的概念也适用于类似的消息系统。
我们要做的第一件事是通过一个非常简单的 Python 客户端将一条消息放到队列中。打开源文件Chapter_4_3.py
,并按照以下步骤操作:
- 打开源代码文件并查看:
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.queue_declare(queue='hello')
channel.basic_publish(exchange='',
routing_key='hello',
body='Hello World!')
print(" [x] Sent 'Hello World!'")
connection.close()
-
这段代码来自 RabbitMQ 参考教程,展示了如何进行连接。它首先连接到中心并打开一个名为
hello
的queue
(队列)。队列就像一个邮箱或一堆消息。一个中心可以有多个不同的队列。然后,代码将一条消息发布到hello
队列,并且消息内容为Hello World!
。 -
在我们运行示例之前,首先需要安装
Pika
。Pika 是一个 AMQP 连接库,可以使用以下命令进行安装:
pip install pika
-
然后像平常一样运行代码文件,并观察输出结果。其实并不是很激动人心,是吗?
-
再次访问 RabbitMQ 管理界面,地址为
http://localhost:15672/
,你会看到现在中心有一个消息,如下所示:
RabbitMQ 界面,显示添加了一条消息
- 我们刚刚发送的消息会留在中心,直到我们稍后取回。这一单一特性将使我们能够运行独立的服务,并确保它们能正确地进行通信,而无需担心其他消费者或发布者。
对于 RabbitMQ 而言,我们刚刚写了一个发布者。在某些情况下,你可能只希望某个服务或应用发布消息,而在其他情况下,你可能希望它们去消费消息。在接下来的练习中,Chapter_4_4_py
,我们将编写一个中心消费者或客户端:
- 打开源文件
Chapter_4_4.py
,查看代码:
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.queue_declare(queue='hello')
def callback(ch, method, properties, body):
print(" [x] Received %r" % body)
channel.basic_consume(callback,
queue='hello',
no_ack=True)
print(' [*] Waiting for messages. To exit press CTRL+C')
channel.start_consuming()
-
上面的代码几乎与之前的示例相同,不同之处在于这次它仅通过内部的
callback
函数来接收响应并消费队列中的消息。在这个示例中,还可以注意到脚本如何阻塞自身并等待消息。在大多数情况下,客户端会注册一个回调函数到队列中,以注册一个事件。当新消息进入特定队列时,该事件会被触发。 -
像平常一样运行代码,观察到第一个
Hello World
消息从队列中被取出并在客户端窗口输出。 -
保持客户端运行,并运行另一个
Chapter_4_3.py
(发布)脚本,注意到客户端如何迅速消费并将其输出到窗口。
这就完成了与消息中心的简单发送和接收通信。正如你所看到的,代码相当简单,大多数配置开箱即用。如果在此设置过程中遇到任何问题,请务必查阅 RabbitMQ 教程,这是另一个极好的额外帮助资源。在下一部分,我们将学习如何构建一个工作的聊天机器人服务器示例。
编写消息队列聊天机器人
我们想要创建的聊天机器人服务器本质上是前面三个示例的结合体。打开Chapter_4_5.py
,并按照接下来的练习进行操作:
- 完整的服务器代码如下:
import pika
from deeppavlov.skills.pattern_matching_skill import PatternMatchingSkill
from deeppavlov.agents.default_agent.default_agent import DefaultAgent
from deeppavlov.agents.processors.highest_confidence_selector import HighestConfidenceSelector
hello = PatternMatchingSkill(responses=['Hello world!'], patterns=["hi", "hello", "good day"])
bye = PatternMatchingSkill(['Goodbye world!', 'See you around'], patterns=["bye", "chao", "see you"])
fallback = PatternMatchingSkill(["I don't understand, sorry", 'I can say "Hello world!"'])
HelloBot = DefaultAgent([hello, bye, fallback], skills_selector=HighestConfidenceSelector())
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channelin = connection.channel()
channelin.exchange_declare(exchange='chat', exchange_type='direct', durable=True)
channelin.queue_bind(exchange='chat', queue='chatin')
channelout = connection.channel()
channelout.exchange_declare(exchange='chat', durable=True)
def callback(ch, method, properties, body):
global HelloBot, channelout
response = HelloBot([str(body)])[0].encode()
print(body,response)
channelout.basic_publish(exchange='chat',
routing_key='chatout',
body=response)
print(" [x] Sent response %r" % response)
channelin.basic_consume(callback,
queue='chatin',
no_ack=True)
print(' [*] Waiting for messages. To exit press CTRL+C')
channelin.start_consuming()
-
我们基本上在不到 25 行代码的情况下完成了一个完整的
Hello World
聊天机器人服务器。当然,功能仍然有限,但到现在你肯定能理解如何为机器人添加其他模式匹配功能。这里需要注意的重点是,我们正在从一个名为
chatin
的队列中消费消息,并将其发布到一个名为chatout
的队列中。这些队列现在被包装在一个名为chat
的交换机中。你可以把交换机看作是一个路由服务。交换机为队列提供了额外的功能,最棒的是,它们是可选的。不过,出于使用上的考虑,我们希望使用交换机,因为它们提供了更好的全局控制。RabbitMQ 中有四种类型的交换机,下面总结了它们:-
Direct:消息直接发送到消息传输中标记的队列。
-
Fanout:将消息复制到交换机所包装的所有队列。这在你想添加日志记录或历史归档时非常有用。
-
主题:这允许你将消息发送到通过匹配消息队列标识的队列。例如,你可以将消息发送到
chat
队列,任何包含chat单词的队列都能收到这条消息。主题交换允许你将类似的消息分组。 -
头部:这与主题交换类似,但它是基于消息本身的头部进行过滤。这是一个很好的交换方式,用于动态路由带有适当头部的消息。
-
-
运行
Chapter_4_5.py
服务器示例并保持它在运行。 -
接下来,打开
Chapter_4_6.py
文件并查看显示的代码:
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channelin = connection.channel()
channelin.exchange_declare(exchange='chat')
chat = 'boo'
channelin.basic_publish(exchange='chat',
routing_key='chatin',
body=chat)
print(" [x] Sent '{0}'".format(chat))
connection.close()
- 上面的代码只是一个示例客户端,我们可以用它来测试聊天机器人服务器。注意变量
chat
被设置为'boo'
。当你运行代码时,检查聊天机器人服务器的输出窗口;这就是我们之前运行的Chapter_4_5.py
文件。你应该能在窗口中看到一个响应消息,它与我们刚发送的聊天消息相匹配。
此时,你可以编写一个完整的聊天客户端,使用 Python 与我们的聊天机器人进行通信。然而,我们想将我们的机器人连接到 Unity,并在下一节中看看如何将我们的机器人作为微服务使用。
在 Unity 中运行聊天机器人
Unity正迅速成为学习开发游戏、虚拟现实和增强现实应用的标准游戏引擎。现在,它也正在迅速成为开发 AI 和 ML 应用的标准平台,部分原因是 Unity 团队构建了出色的强化学习平台。这个 Unity ML 平台是我们希望使用该工具的关键组成部分,因为它目前处于游戏领域的先进 AI 技术前沿。
Unity 的 AI 团队,由丹尼·兰奇博士和高级开发者阿瑟·朱利安尼博士领导,直接和间接地为本书的内容创意提供了许多建议和贡献。当然,这对本书中大量使用 Unity 的部分产生了巨大影响。
安装 Unity 相当简单,但我们希望确保第一次安装时就能正确完成。因此,请按照以下步骤在系统上安装 Unity 版本:
-
在浏览器中访问
store.unity.com/download
,接受条款后,下载 Unity 下载助手。这是用于下载和安装所需组件的工具。 -
运行下载助手并选择以下最低配置组件进行安装,如下图所示:
选择 Unity 安装组件
-
只需确保安装最新版本的 Unity,并选择与你的操作系统相匹配的组件,如前述截图所示。当然,你可以根据自己的需求选择其他组件,但这些是本书所需的最低配置。
-
接下来,将 Unity 安装路径设置为一个众所周知的文件夹。一个不错的选择是将文件夹名称设置为版本号。这样,你可以在同一系统上安装多个版本的 Unity,且能够轻松找到它们。以下截图展示了在 Windows 上如何操作:
设置 Unity 安装路径
-
这些就是安装过程中的关键部分,之后你可以使用默认设置继续安装软件。
-
安装完后启动 Unity 编辑器,你将被提示登录。无论你使用的是免费版本,Unity 都要求你有一个账户。返回 unity.com,创建一个账户。完成账户设置后,返回并登录编辑器。
-
登录后,创建一个名为
Chatbot
的空项目,并让编辑器打开一个空白场景。
Unity 是一个功能齐全的游戏引擎,如果这是你第一次使用,可能会让你感到有些不知所措。网上有许多教程和视频,可以帮助你快速掌握界面。我们会尽量简单地演示概念,但如果你迷茫了,可以放慢节奏,反复练习几次。
安装完 Unity 后,我们需要安装一些组件或资源,以便轻松连接到刚才创建的聊天机器人服务器。在下一节中,我们将为 Unity 安装 AMQP 资源。
为 Unity 安装 AMQP
RabbitMQ 提供了丰富的跨平台库资源,允许您轻松连接到中心。C# 库在 Unity 外部运行良好,但设置起来有些问题。幸运的是,Cymantic Labs 的开发者们在 GitHub 上构建并开源了适用于 Unity 的版本。让我们在下一个练习中学习如何安装这段代码:
- 使用
git
或 ZIP 文件下载并解压代码,地址为github.com/CymaticLabs/Unity3D.Amqp
:
git clone https://github.com/CymaticLabs/Unity3D.Amqp.git
-
从菜单切换到 Unity,选择 文件 | 打开项目,并导航到您安装代码的
Unity3D.Amqp\unity\CymaticLabs.UnityAmqp
文件夹。这将打开该资源的独立项目。等待项目加载。 -
在项目窗口中打开
Assets/CymanticLabs/Amqp/Scenes
文件夹(通常位于底部)。 -
双击 AmqpDemo 场景以在编辑器中打开它。
-
在编辑器顶部按下播放按钮运行场景。运行场景后,您应该会看到以下内容:
设置 Amqp 连接并发送消息
-
按下连接按钮以连接到本地 RabbitMQ。
-
接下来,在订阅下,将交换设置为 chat,将队列设置为 chatout,并点击订阅。这将订阅队列,以便我们可以在 Unity 控制台窗口中看到任何返回的消息。
-
最后,在发布下,将交换设置为 chat,将队列设置为 chatin,并输入一条消息,例如
hello
。点击发送按钮,您应该会在控制台窗口中看到机器人的回应。
这为我们的聊天机器人设置了工作环境。当然,这仅仅是可能性的开始,读者也应鼓励进行更深入的探索,但请记住,我们将在后续部分重新访问这段代码并加以利用。
本章内容已完成,现在您可以在下一部分继续学习并充分利用它。
练习
使用以下练习来扩展您的学习,并在本章中更加自信地掌握内容:
-
返回到第一个练习并加载另一组翻译。对这些翻译进行训练,并查看训练后生成的回应。还有很多其他语言文件可以用来进行训练。
-
使用英文/法文翻译文件作为示例,设置您自己的对话训练文件。记住,匹配的回应可以是任何内容,而不仅仅是翻译文本。
-
向 DeepPavlov 机器人添加额外的模式匹配技能。可以选择简单的测试技能或聊天机器人服务器技能。
-
DeepPavlov 聊天机器人使用最高价值选择标准来选择回应。DeepPavlov 也有一个随机选择器。将聊天机器人的回应选择器更改为使用随机选择。
-
将示例中的交换类型更改为使用 Fanout,并创建一个日志队列来记录消息。
-
将交换类型更改为 Topic,并查看如何对消息进行分组。警告:这可能会导致示例出现问题;看看你是否能够修复它。
-
编写一个 Python 的 RabbitMQ 发布者,向一个或多个不同类型的队列发布消息。
-
使用模式匹配技能创建一整套对话技能。然后,看看你的机器人与您对话的表现如何。
-
为聊天机器人服务器添加其他类型的技能。这可能需要你做一些额外的功课。
-
在 RabbitMQ 上编写或运行两个聊天机器人,并观察它们如何相互对话。
至少完成其中两个或三个练习。
总结
在本章中,我们探讨了使用神经网络和深度学习构建聊天机器人或神经对话代理。我们首先了解了什么构成了聊天机器人以及当前使用的主要形式:目标导向型和对话型机器人。然后,我们研究了如何构建一个基本的机器翻译对话聊天机器人,使用了序列到序列学习。
在了解了序列学习的背景后,我们研究了开源工具 DeepPavlov。DeepPavlov 是一个强大的聊天平台,基于 Keras 构建,旨在处理多种形式的神经网络代理对话和任务。这使得它成为我们使用聊天机器人服务器作为基础的理想选择。接着,我们安装了 RabbitMQ,这是一种微服务消息中心平台,未来将允许我们的机器人和其他各种服务进行交互。
最后,我们安装了 Unity,并迅速安装了 AMQP 插件资源,并连接到我们的聊天机器人服务器。
这完成了我们关于深度学习的介绍部分,在下一部分,我们将通过深入探讨深度强化学习,开始更加专注于游戏 AI。