游戏深度学习实用指南-全-
游戏深度学习实用指南(全)
原文:
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)本质上是通过我们的训练数据的一次传递。之所以需要多次传递,是为了让我们的权重在全局最小值而非局部最小值处收敛。在每个周期中,权重是通过以下方程训练的:

考虑以下内容:
= 权重
= 感知机学习的速率
= 标注的训练值
= 感知机返回的值
=
- 
偏差以类似的方式进行训练,但只需记住它是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,等等。一个一维张量将描述一个单一的数字,例如
,一个 2x2 张量将是
,或者你可能称之为矩阵。一个 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 函数,看看如何对一个单一的权重进行求导:
是我们描述的成本函数,表达如下:

- 我们可以通过如下方式,对这个函数进行关于单一变量权重的求导:


- 如果我们将所有这些偏导数合并在一起,我们就得到了我们的
Cost函数的向量梯度,
,表示为如下:

- 这个梯度定义了我们希望取反并用来最小化
Cost函数的向量方向。在我们之前的例子中,这个向量包含了超过 13,000 个分量。这些分量对应着网络中需要优化的超过 13,000 个权重。为了计算梯度,我们需要结合很多偏导数。幸运的是,微积分中的链式法则能够帮助我们简化这个数学过程。回想一下,链式法则的定义如下:

- 这使得我们能够使用链式法则为单一权重定义梯度,如下所示:


- 这里,
表示输入值,而
表示神经元位置。注意,我们现在需要对给定神经元的激活函数 a 进行偏导数,这一过程可以通过以下方式总结:

上标符号
表示当前层,而
表示上一层。
表示来自上一层的输入或输出。
表示激活函数,回想一下我们之前使用过 Step 和 ReLU 函数来扮演这个角色。
- 然后,我们对这个函数进行偏导数,如下所示:

为了方便起见,我们定义如下:

- 在这一点上,事情可能看起来比实际情况复杂得多。试着理解符号的所有细微之处,记住我们现在所看的本质上是激活函数对
Cost函数的偏导数。额外的符号仅仅是帮助我们索引各个权重、神经元和层。然后我们可以将其表达为如下:

- 再次强调,我们所做的只是为
^(th) 输入、
^(th) 神经元和层
的权重定义梯度 (
)。结合梯度下降,我们需要使用前面的基础公式将调整反向传播到权重中。对于输出层(最后一层),这现在可以总结如下:

- 对于内部或隐藏层,方程式的结果如下:

- 经过一些替换和对一般方程的操作,我们最终得到了这个结果:

在这里, f' 表示激活函数的导数。
上面的方程式使我们能够运行网络并通过以下过程进行误差反向传播:
-
首先,你需要计算每一层的激活值
和
,从输入层开始并向前传播。 -
然后我们使用
在输出层评估该项
。 -
我们通过使用余项来评估每一层,使用
,从输出层开始并向后传播。 -
再次强调,我们使用偏导数
来获得每一层所需的导数。
你可能需要多读几遍这一部分才能理解所有概念。另一个有用的做法是运行前面的示例并观察训练过程,尝试想象每个权重是如何被更新的。我们还没有完全完成这里的内容,接下来还有几个步骤——使用自动微分就是其中之一。除非你在开发自己的低级网络,否则仅仅对这些数学知识有基本的理解就能帮助你更好地理解训练神经网络的需求。在下一部分,我们将回到一些更基本的操作,并通过构建神经网络代理将我们的新知识付诸实践。
学习不应也很可能不应完全依赖于同一个来源。务必将学习扩展到其他书籍、视频和课程。这样不仅能让你在学习上更为成功,还能在过程中获得更多理解。
使用 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 代码,这次预计训练会花费几分钟。完成后,你应该会看到一张类似于下面的图片:
![]()
原始输入图像与编码和解码后的输出图像的示例
这就是我们构建一个简单的 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 层计算单词/字符之间如何关联的概率或上下文。你将经常遇到以下方程,它描述了这个转换过程:

请考虑以下内容:
-
= 输出序列 -
= 输入序列 -
= 向量表示
这个
表示 sigma 的乘法形式 (
),并用于将概率汇聚成思维向量。这是对整个过程的一个大大简化,感兴趣的读者可以自行在 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。
第二部分:深度强化学习
本节我们将详细研究深度强化学习,使用各种框架和技术,探索多个有趣的示例。
本节将涵盖以下章节:
-
第五章,深度强化学习简介
-
第六章,Unity ML-Agents
-
第七章,代理与环境
-
第八章,理解 PPO
-
第九章,奖励与强化学习
-
第十章,模仿与迁移学习
-
第十一章,构建多智能体环境
第五章:介绍 DRL
深度强化学习(DRL)目前正席卷全球,被视为机器学习技术中的“热点”,即达到某种形式的通用人工智能的目标。也许正是因为 DRL 接近了通用人工智能的边界,或者是我们所理解的通用智能。它也可能是你正在阅读本书的主要原因之一。幸运的是,本章以及本书大部分内容,都将深入探讨强化学习(RL)及其许多变体。在本章中,我们将开始学习 RL 的基础知识,以及它如何与深度学习(DL)相结合。我们还将探索OpenAI Gym环境,这是一个很棒的 RL 实验场,并学习如何利用一些简单的 DRL 技术进行实践。
请记住,这本书是一本实践性很强的书,因此我们将尽量减少技术理论的讨论,而是通过大量的工作实例进行探索。一些读者可能会觉得没有理论背景会感到迷茫,并且希望自己去深入探索 RL 的理论部分。
对于那些不熟悉 RL 理论背景的读者,我们将介绍几个核心概念,但这是简略版,因此建议您在准备好之后从其他来源学习更多的理论知识。
在本章中,我们将开始学习 DRL,这一主题将贯穿多个章节。我们将从基础开始,然后探索一些适应于 DL 的工作示例。以下是本章内容:
-
强化学习
-
Q 学习模型
-
运行 OpenAI gym
-
第一个深度 Q 网络(DQN)与 DRL
-
RL 实验
对于喜欢跳跃阅读的读者:是的,从本章开始阅读本书是完全可以的。不过,您可能需要回到前几章来完成一些练习。我们还假设您的 Python 环境已经配置好 TensorFlow 和 Keras,如果不确定,可以查看项目文件夹中的requirements.txt文件。
本书中的所有项目都是在 Visual Studio 2017(Python)中构建的,这是本书示例推荐的编辑器。如果您使用 VS 2017 与 Python,您可以通过打开章节解决方案文件轻松管理示例。当然,也有许多其他优秀的 Python 编辑器和工具,您可以使用自己习惯的工具。
强化学习
相较于其他机器学习方法,RL 目前在进展方面领先于其他技术。注意,这里使用的是“方法论”而非“技术”这个词。RL 是一种方法论或算法,它应用了一个我们可以与神经网络一起使用的原理,而神经网络是可以应用于多种方法论的机器学习技术。之前,我们讨论了与 DL 结合的其他方法论,但我们更专注于实际的实现。然而,RL 引入了一种新的方法论,它要求我们在理解如何应用之前,先了解更多的内在和外在机制。
强化学习(RL)由加拿大人理查德·萨顿(Richard Sutton)普及,他是阿尔伯塔大学的教授。萨顿还参与了 Google DeepMind 的 RL 开发,并且经常被视为 RL 的奠基人。
任何机器学习系统的核心都需要训练。通常,AI 代理/大脑一开始什么也不知道,我们通过某种自动化过程向其输入数据让它学习。正如我们所见,最常见的做法是监督训练。这是指我们首先给训练数据打上标签。我们还研究了无监督训练,在这种情况下,生成对抗网络(GANs)通过相互对抗进行训练。然而,这两种方法都没有复制我们在生物学中看到的那种学习或训练方式,这种方式通常被称为奖励或强化学习(RL):一种让你教会狗狗为零食叫唤、捡报纸并在户外排泄的学习方式,一种让代理探索自己的环境并自我学习的方式。这与我们期望通用 AI 所使用的学习方式并无太大不同;毕竟,RL 可能与我们所使用的系统类似,或者说我们是这么认为的。
大卫·西尔弗(David Silver),前萨顿教授的学生,现在是 DeepMind 的负责人,拥有一系列关于强化学习(RL)理论背景的优秀视频。前五个视频非常有趣,推荐观看,但后续内容较为深奥,可能并不适合所有人。以下是视频链接:www.youtube.com/watch?v=2pWv7GOvuf0
强化学习定义了一种名为自身的训练方式。这种基于奖励的训练在下图中显示:

强化学习
图中展示了一个代理在一个环境中。该代理读取环境的状态,然后决定并执行一个动作。这个动作可能会或不会带来奖励,而奖励可能是好是坏。在每次动作和可能的奖励后,代理会再次收集环境的状态。这个过程会重复,直到代理达到一个终结状态。也就是说,直到它达到目标;也许它死掉,或者只是累了。需要注意的是,前图中有一些细微的地方。首先,代理并不总是会获得奖励,这意味着奖励可能会延迟,直到某个未来的目标达到。这与我们之前探索的其他学习方式不同,后者会立即给训练网络反馈。奖励可以是好是坏,而负面训练代理同样有效,但对人类来说则不太适用。
现在,正如你所料,任何强大的学习模型,其数学可能会相当复杂,且对新手来说确实具有一定的挑战性。我们不会深入探讨理论细节,只会在下一节中描述一些强化学习的基础。
多臂老丨虎丨机
我们之前看到的图示描述了完整的强化学习(RL)问题,这也是我们在本书后续大部分内容中使用的模型。然而,我们经常讲授一个简化的单步变体问题,称为多臂老丨虎丨机。多臂老丨虎丨机这个名称源于拉斯维加斯的老丨虎丨机,而并非指某些其他不正当的含义。我们使用这些简化的场景来解释强化学习的基础知识,呈现为单步或单状态问题。
在多臂老丨虎丨机的情况下,可以想象一个虚构的多臂拉斯维加斯老丨虎丨机,根据拉动的臂不同奖励不同的奖品,但每个臂的奖品始终相同。智能体在这种情况下的目标是找出每次应拉动哪个臂。我们可以进一步将其建模为如下所示的方程:

考虑以下方程:
-
= 值的向量(1,2,3,4) -
= 行动 -
= α = 学习率 -
= 奖励
该方程计算智能体所采取每个行动的值(V),这是一个向量。然后,它将这些值反馈到自身,从奖励中减去并乘以学习率。计算得出的值可以用来决定拉动哪个臂,但首先智能体需要至少拉动每个臂一次。让我们快速在代码中建模这一过程,作为游戏/仿真程序员,我们可以看到它是如何工作的。打开 Chapter_5_1.py 代码并按照以下步骤操作:
- 本练习的代码如下:
alpha = .9
arms = [['bronze' , 1],['gold', 3], ['silver' , 2], ['bronze' , 1]]
v = [0,0,0,0]
for i in range(10):
for a in range(len(arms)):
print('pulling arm '+ arms[a][0])
v[a] = v[a] + alpha * (arms[a][1]-v[a])
print(v)
-
这段代码创建了所需的设置变量,
arms(gold、silver和bronze)以及值向量v(全为零)。然后,代码会进行多次迭代(10次),每次都拉动一个臂并根据方程计算并更新值v。请注意,奖励值被臂拉动的值所替代,即项arms[a][1]。 -
运行示例,你将看到生成的输出,显示每个行动的值,或者在这种情况下是一个臂的拉动。
正如我们所见,通过一个简单的方程式,我们能够建模多臂老丨虎丨机问题,并得出一个解决方案,使得智能体能够持续地选择正确的臂。这为强化学习奠定了基础,在接下来的章节中,我们将迈出下一步,探讨上下文老丨虎丨机。
上下文老丨虎丨机
现在,我们可以将单个多臂老丨虎丨机问题提升为包含多个多臂老丨虎丨机的问题,每个老丨虎丨机都有自己的一组臂。现在,我们的问题引入了上下文或状态到方程中。随着每个老丨虎丨机定义自己的上下文/状态,我们现在在质量和行动的角度来评估我们的方程。我们修改后的方程如下所示:

考虑以下方程:
= 值的表格/矩阵
[1,2,3,4
2,3,4,5
4,2,1,4]
-
= 状态 -
= 动作 -
= alpha = 学习率 -
= 回报
让我们打开Chapter_5_2.py并观察以下步骤:
- 打开代码,如下所示,并按照与之前示例所做的更改进行操作:
import random
alpha = .9
bandits = [[['bronze' , 1],['gold', 3], ['silver' , 2], ['bronze' , 1]],
[['bronze' , 1],['gold', 3], ['silver' , 2], ['bronze' , 1]],
[['bronze' , 1],['gold', 3], ['silver' , 2], ['bronze' , 1]],
[['bronze' , 1],['gold', 3], ['silver' , 2], ['bronze' , 1]]]
q = [[0,0,0,0],
[0,0,0,0],
[0,0,0,0],
[0,0,0,0]]
for i in range(10):
for b in range(len(bandits)):
arm = random.randint(0,3)
print('pulling arm {0} on bandit {1}'.format(arm,b))
q[b][arm] = q[b][arm] + alpha * (bandits[b][arm][1]-q[b][arm])
print(q)
-
这段代码设置了多个多臂老丨虎丨机,每个都有一组臂。然后,它会进行多次迭代,但这次在每次循环时,它还会循环遍历每个老丨虎丨机。在每次循环中,它会随机选择一个臂来拉动,并评估其质量。
-
运行示例并查看
q的输出。注意,即使在选择随机臂后,方程依然持续选择金臂,即回报最高的臂来拉动。
随意多玩这个示例,并查看练习以获取更多灵感。当我们讨论 Q-Learning 时,我们将扩展我们的 RL 问题的复杂度。然而,在进入那个部分之前,我们将稍作偏离,看看如何设置 OpenAI Gym,以便进行更多的 RL 实验。
使用 OpenAI Gym 进行强化学习(RL)
强化学习(RL)已经变得非常流行,现在有一场竞赛,专门开发帮助构建 RL 算法的工具。目前,这个领域的两个主要竞争者是OpenAI Gym和Unity。Unity 迅速成为了我们稍后将深入探索的 RL 竞速机器。现在,我们将启用辅助功能,使用 OpenAI Gym 来进一步探索 RL 的基本原理。
我们需要安装 OpenAI Gym 工具包才能继续,安装方法可能因操作系统的不同而有所差异。因此,我们将在这里专注于 Windows 的安装说明,其他操作系统用户可能会遇到较少的困难。按照以下步骤在 Windows 上安装 OpenAI Gym:
-
安装一个 C++编译器;如果你已经安装了 Visual Studio 2017,可能已经有一个推荐的编译器了。你可以在这里找到其他支持的编译器:
wiki.python.org/moin/WindowsCompilers。 -
确保已安装 Anaconda,并打开 Anaconda 命令提示符,然后运行以下命令:
conda create -n gym
conda activate gym
conda install python=3.5 # reverts Python, for use with TensorFlow later
pip install tensorflow
pip install keras pip install gym
- 就我们目前的目的而言,短期内不需要安装任何其他 Gym 模块。Gym 有很多示例环境,其中 Atari 游戏和 MuJoCo(机器人仿真器)是最有趣的几种。我们将在本章稍后查看 Atari 游戏模块。
这应该会为你的系统安装 Gym 环境。我们需要的大多数功能将通过最小设置即可工作。如果你决定深入使用 Gym,那么你可能需要安装其他模块;有很多模块。在下一节中,我们将测试这个新环境,边学习 Q-Learning。
一个 Q-Learning 模型
RL 深深地与几种数学和动态编程概念交织在一起,这些概念可以填满一本教科书,实际上也确实有几本类似的书。然而,对于我们的目的来说,我们只需要理解关键概念,以便构建我们的 DRL 智能体。因此,我们将选择不去过多纠结于数学细节,但仍有一些关键概念是你需要理解的,才能取得成功。如果你在第一章《深度学习与游戏》中覆盖了数学内容,那么这一部分将会非常轻松。对于那些没有覆盖的朋友,慢慢来,但这个部分你不能错过。
为了理解 Q-Learning 模型,这是 RL 的一种形式,我们需要回到基础知识。在接下来的部分,我们将讨论 马尔可夫决策过程 和 贝尔曼 方程的重要性。
马尔可夫决策过程和贝尔曼方程
在 RL 的核心是 马尔可夫决策过程 (MDP) 。一个 MDP 通常被描述为离散时间随机控制过程。简单来说,这意味着它是一个通过时间步来运作的控制程序,用于确定每个动作的概率,并且每个动作都会导致一个奖励。这个过程已经被广泛用于大多数机器人控制、无人机、网络控制,当然也包括 RL。我们通常通过下面的图示来描述这个过程:

马尔可夫决策过程
我们通过一个元组或向量来表示一个 MDP
,使用以下变量:
-
- 是一个有限的状态集合, -
- 是一个有限的动作集合, -
- 动作
在状态
时刻
导致状态
在时刻
的概率, -
- 是即时奖励 -
- gamma 是一个折扣因子,用来折扣未来奖励的重要性或为未来奖励提供重要性
该图通过将你自己想象成一个处于某个状态的智能体来工作。然后你根据概率确定动作,总是采取一个随机动作。当你移动到下一个状态时,该动作会给你一个奖励,你根据这个奖励更新概率。同样,David Silver 在他的讲座中非常好地涵盖了这一部分内容。
现在,前面的过程是有效的,但随后出现了另一种变体,通过引入 Bellman 方程 和策略/价值迭代的概念,提供了更好的未来奖励评估。之前我们有一个值,
,现在我们有一个策略(
)来表示一个值(
),这为我们提供了一个新的方程,如下所示:

我们不会进一步讲解这个方程,只是提醒大家牢记质量迭代的概念。在接下来的部分中,我们将看到如何将这个方程简化为每个动作的质量指标,并将其用于 Q-Learning。
Q-learning
随着质量迭代方法的引入,一个称为 Q-learning 或 质量学习 的有限状态方法被提出。Q 使用质量迭代技术来解决给定的有限状态问题,确定智能体的最佳行动。我们在前面看到的方程现在可以表示为以下形式:

考虑以下方程:
-
当前状态 -
当前动作 -
下一个动作 -
当前奖励 -
学习率(alpha) -
奖励折扣因子(gamma)
Q 值现在在智能体穿越其环境时被迭代更新。没有什么比一个例子更能展示这些概念了。打开 Chapter_5_3.py,并按照以下步骤操作:
- 我们从各种导入开始,并按照以下代码设置:
from collections import deque
import numpy as np
import os
clear = lambda: os.system('cls') #linux/mac use 'clear'
import time
import gym
from gym import wrappers, logger
-
这些导入仅加载了我们这个示例所需的基本库。记住,运行这个示例前,你需要安装
Gym。 -
接下来,我们设置一个新环境;在这个例子中,我们使用基础的
FrozenLake-v0示例,这是一个测试 Q-learning 的完美示例:
environment = 'FrozenLake-v0'
env = gym.make(environment)
- 然后我们设置 AI 环境(
env)以及其他一些参数:
outdir = os.path.join('monitor','q-learning-{0}'.format(environment))
env = wrappers.Monitor(env, directory=outdir, force=True)
env.seed(0)
env.is_slippery = False
q_table = np.zeros([env.observation_space.n, env.action_space.n])
#parameters
wins = 0
episodes = 40000
delay = 1
epsilon = .8
epsilon_min = .1
epsilon_decay = .001
gamma = .9
learning_rate = .1
-
在这部分代码中,我们设置了若干变量,稍后会详细说明。在这个示例中,我们使用了一个包装工具来监控环境,这对于确定潜在的训练问题非常有用。另一个需要注意的是
q_table数组的设置,这个数组由环境的observation_space(状态)和action_space(动作)定义;空间定义的是数组而不仅仅是向量。在这个例子中,action_space是一个向量,但它也可以是一个多维数组或张量。 -
跳过下一个函数部分,直接跳到最后一部分,在那里训练迭代发生并显示在以下代码中:
for episode in range(episodes):
state = env.reset()
done = False
while not done:
action = act(env.action_space,state)
next_state, reward, done, _ = env.step(action)
clear()
env.render()
learn(state, action, reward, next_state)
if done:
if reward > 0:
wins += 1
time.sleep(3*delay)
else:
time.sleep(delay)
print("Goals/Holes: %d/%d" % (wins, episodes - wins))
env.close()
-
前面的代码大部分都比较直接,应该容易理解。看看
env(环境)是如何使用从act函数生成的action;这会用于执行智能体的步骤或动作。step函数的输出是next_state,reward和done,我们用这些来通过learn函数来确定最佳的 Q 策略。 -
在我们深入研究动作和学习函数之前,先运行样本,观察智能体如何进行训练。训练可能需要一些时间,所以可以随时返回书本继续阅读。
以下是 OpenAI Gym FrozenLake 环境运行我们 Q-learning 模型的示例:

FrozenLake Gym 环境
在样本运行时,你将看到一个简单的文本输出,显示环境的状态。S 代表起点,G 代表目标,F 代表冻结区域,H 代表陷阱。智能体的目标是找到一条通过环境的路径,避免掉入陷阱,并到达目标。特别注意智能体是如何移动的,如何在环境中找到自己的路径。在下一节中,我们将深入分析 learn 和 act 函数,并理解探索的重要性。
Q-learning 和探索
我们在使用如 Q-learning 等策略迭代模型时,面临的一个问题是探索与利用的权衡。Q 模型方程假设通过最大化质量来决定一个行动,我们称之为利用(exploiting the model)。这个问题在于,它常常会将智能体局限于只追求最佳短期收益的解法。相反,我们需要允许智能体有一定的灵活性去探索环境并自主学习。我们通过引入一个逐渐消失的探索因子到训练中来实现这一点。让我们通过再次打开 Chapter_5_3.py 示例来看看这一过程是怎样的:
- 向下滚动,查看
act和is_explore函数,如下所示:
def is_explore():
global epsilon, epsilon_decay, epsilon_min
epsilon = max(epsilon-epsilon_decay,epsilon_min)
if np.random.rand() < epsilon:
return True
else:
return False
def act(action_space, state):
# 0 - left, 1 - Down, 2 - Right, 3 - Up
global q_table
if is_explore():
return action_space.sample()
else:
return np.argmax(q_table[state])
-
请注意,在
act函数中,首先测试智能体是否希望或需要进行探索,使用的是is_explore()。在is_explore函数中,我们可以看到,全局的epsilon值在每次迭代时都会通过epsilon_decay逐渐衰减,直到达到全局最小值epsilon_min。当智能体开始一个回合时,他们的探索epsilon值较高,这使得他们更可能进行探索。随着时间的推移,回合的进行,epsilon值会逐渐降低。我们这样做是基于假设,随着时间的推移,智能体将需要进行越来越少的探索。探索与利用之间的权衡非常重要,特别是在环境状态空间较大时,理解这一点至关重要。本书的后续内容将深入探讨这一权衡。请注意,智能体使用探索函数并随机选择一个动作。
-
最后,我们来到了
learn函数。这个函数是用来计算Q值的,具体如下:
def learn(state, action, reward, next_state):
# Q(s, a) += alpha * (reward + gamma * max_a' Q(s', a') - Q(s, a))
global q_table
q_value = gamma * np.amax(q_table[next_state])
q_value += reward
q_value -= q_table[state, action]
q_value *= learning_rate
q_value += q_table[state, action]
q_table[state, action] = q_value
- 在这里,方程被拆解并简化,但这是计算代理在利用时将使用的值的步骤。
保持代理继续运行,直到完成。我们刚刚完成了第一个完整的强化学习问题,尽管这是一个有限状态的问题。在下一节中,我们将大大扩展我们的视野,探索深度学习与强化学习的结合。
第一个深度强化学习与深度 Q-learning
现在我们已经详细了解了强化学习过程,我们可以着手将我们的 Q-learning 模型与深度学习结合。这正如你可能猜到的,是我们努力的巅峰所在,也是强化学习真正威力显现的地方。正如我们在前面的章节中所学,深度学习本质上是一个复杂的方程系统,可以通过非线性函数映射输入,从而生成训练输出。
神经网络只是一种更简单的求解非线性方程的方法。我们稍后会学习如何使用 DNN 来解决其他方程,但现在我们专注于使用它来解决我们在上一节看到的 Q-learning 方程。
我们将使用 OpenAI Gym 工具包中的CartPole训练环境。这个环境几乎是学习深度 Q-learning(DQN)的标准。
打开Chapter_5_4.py文件,按照以下步骤查看如何将我们的求解器转换为使用深度学习:
- 像往常一样,我们查看导入的模块和一些初始的起始参数,如下所示:
import random
import gym
import numpy as np
from collections import deque
from keras.models import Sequential
from keras.layers import Dense
from keras.optimizers import Adam
EPISODES = 1000
- 接下来,我们将创建一个类来包含 DQN 代理的功能。
__init__函数如下所示:
class DQNAgent:
def __init__(self, state_size, action_size):
self.state_size = state_size
self.action_size = action_size
self.memory = deque(maxlen=2000)
self.gamma = 0.95 # discount rate
self.epsilon = 1.0 # exploration rate
self.epsilon_min = 0.01
self.epsilon_decay = 0.995
self.learning_rate = 0.001
self.model = self._build_model()
-
大部分参数已经涵盖,但需要注意一个新参数叫做
memory,它是一个deque集合,保存了最近的 2,000 步。这使得我们能够在一种回放模式下批量训练神经网络。 -
接下来,我们查看神经网络模型是如何通过
_build_model函数构建的,如下所示:
def _build_model(self):
# Neural Net for Deep-Q learning Model
model = Sequential()
model.add(Dense(24, input_dim=self.state_size, activation='relu'))
model.add(Dense(24, activation='relu'))
model.add(Dense(self.action_size, activation='linear'))
model.compile(loss='mse',
optimizer=Adam(lr=self.learning_rate))
return model
-
这个模型比我们已经看到的其他模型要简单一些,包含三个dense层,为每个动作输出一个值。这个网络的输入是状态。
-
跳到文件的底部,查看训练迭代循环,如下所示:
if __name__ == "__main__":
env = gym.make('CartPole-v1')
state_size = env.observation_space.shape[0]
action_size = env.action_space.n
agent = DQNAgent(state_size, action_size)
# agent.load("./save/cartpole-dqn.h5")
done = False
batch_size = 32
for e in range(EPISODES):
state = env.reset()
state = np.reshape(state, [1, state_size])
for time in range(500):
# env.render()
action = agent.act(state)
env.render()
next_state, reward, done, _ = env.step(action)
reward = reward if not done else -10
next_state = np.reshape(next_state, [1, state_size])
agent.remember(state, action, reward, next_state, done)
state = next_state
if done:
print("episode: {}/{}, score: {}, e: {:.2}"
.format(e, EPISODES, time, agent.epsilon))
break
if len(agent.memory) > batch_size:
agent.replay(batch_size)
- 在这个示例中,我们的训练发生在一个实时
render循环中。代码中的重要部分被高亮显示,展示了状态的重塑和调用agent.remember函数。最后的agent.replay函数是网络进行训练的地方。remember函数如下所示:
def remember(self, state, action, reward, next_state, done):
self.memory.append((state, action, reward, next_state, done))
- 这个函数只是存储
state、action、reward、next_state和done参数,以供回放训练。向下滚动查看replay函数,具体如下:
def replay(self, batch_size):
minibatch = random.sample(self.memory, batch_size)
for state, action, reward, next_state, done in minibatch:
target = reward
if not done:
target = (reward+self.gamma*
np.amax(self.model.predict(next_state)[0]))
target_f = self.model.predict(state)
target_f[0][action] = target
self.model.fit(state, target_f, epochs=1, verbose=0)
if self.epsilon > self.epsilon_min:
self.epsilon *= self.epsilon_decay
-
replay函数是网络训练发生的地方。我们首先定义一个minibatch,它是通过从先前的经验中进行随机抽样并按batch_size分组定义的。然后,我们循环遍历批次,将reward设置为target,如果没有done,则根据模型对next_state的预测计算一个新的目标。之后,我们在state上使用model.predict函数来确定最终的目标。最后,我们使用model.fit函数将训练后的目标反向传播回网络。由于这一部分很重要,我们再强调一下。注意
target变量计算并设置的那一行。这些代码行可能看起来很熟悉,因为它们与我们之前看到的 Q 值方程一致。这个target值是当前动作应该预测的值。这就是当前动作的回报值,并由返回的reward设置。 -
运行示例并观察智能体训练如何使杆子在小车上保持平衡。以下是训练过程中环境的显示:

CartPole OpenAI Gym 环境
示例环境使用的是我们通常用于学习构建第一个 DRL 模型的典型第一个环境——CartPole。下一节中,我们将查看如何在其他场景和通过 Keras-RL API 提供的其他模型中使用 DQNAgent。
强化学习实验
强化学习正在迅速发展,我们刚刚看到的 DQN 模型很快就被更先进的算法所取代。RL 算法有许多变种和进展,足以填满几章内容,但大多数材料会被认为是学术性的。因此,我们将重点介绍 Keras RL API 提供的各种 RL 模型的更实际示例。
我们可以使用的第一个简单示例是将之前的示例修改为适应新的gym环境。打开Chapter_5_5.py并按照以下练习进行操作:
- 在下面的代码中更改环境名称:
if __name__ == "__main__":
env = gym.make('MountainCar-v0')
- 在本例中,我们将使用
MountainCar环境,如下所示:

MountainCar 环境示例
- 像平常一样运行代码,看看 DQNAgent 是如何解决爬山问题的。
你可以看到我们是如何快速切换环境并在另一个环境中测试 DQNAgent 的。在下一节中,我们将讨论如何使用 Keras-RL API 提供的不同 RL 算法来训练 Atari 游戏。
Keras RL
Keras 提供了一个非常有用的 RL API,它封装了几种变体,如 DQN、DDQN、SARSA 等。我们现在不会详细讨论这些不同的 RL 变体,但随着我们进入更复杂的模型,稍后会介绍重要的部分。不过,目前,我们将看看如何快速构建一个 DRL 模型来玩 Atari 游戏。打开Chapter_5_6.py并按照这些步骤操作:
- 我们首先需要通过
pip安装几个依赖项;打开命令行或 Anaconda 窗口,并输入以下命令:
pip install Pillow
pip install keras-rl
pip install gym[atari] # on Linux or Mac
pip install --no-index -f https://github.com/Kojoley/atari-py/releases atari_py # on Windows thanks to Nikita Kniazev
-
这将安装 Keras RL API、
Pillow(一个图像框架)以及gym的 Atari 环境。 -
按照平常的方式运行示例代码。这个示例确实需要脚本参数,但在这里我们不需要使用它们。以下是渲染的 Atari Breakout 环境示例:

Atari Breakout 环境
不幸的是,你无法看到智能体玩游戏的过程,因为所有的动作都在后台进行,但可以让智能体运行直到它完成并保存模型。以下是我们运行示例的方法:
-
你可以使用
--mode test作为参数重新运行示例,让智能体在 10 回合中运行并查看结果。 -
在示例运行时,浏览代码并特别注意模型,如下所示:
model = Sequential()
if K.image_dim_ordering() == 'tf':
# (width, height, channels)
model.add(Permute((2, 3, 1), input_shape=input_shape))
elif K.image_dim_ordering() == 'th':
# (channels, width, height)
model.add(Permute((1, 2, 3), input_shape=input_shape))
else:
raise RuntimeError('Unknown image_dim_ordering.')
model.add(Convolution2D(32, (8, 8), strides=(4, 4)))
model.add(Activation('relu'))
model.add(Convolution2D(64, (4, 4), strides=(2, 2)))
model.add(Activation('relu'))
model.add(Convolution2D(64, (3, 3), strides=(1, 1)))
model.add(Activation('relu'))
model.add(Flatten())
model.add(Dense(512))
model.add(Activation('relu'))
model.add(Dense(nb_actions))
model.add(Activation('linear'))
print(model.summary())
- 注意我们的模型如何使用
Convolution(卷积),并且带有池化。这是因为这个示例将每一帧游戏画面作为输入(状态)并做出响应。在这种情况下,模型的状态非常庞大,这展示了深度强化学习(DRL)的真正威力。在本例中,我们仍在训练一个状态模型,但在未来的章节中,我们将研究如何训练一个策略,而不是模型。
这只是强化学习(RL)的简单介绍,我们省略了一些可能让新手困惑的细节。由于我们计划在接下来的章节中更详细地讨论 RL,特别是在第八章中深入讲解 近端策略优化(PPO)和 理解 PPO,因此不必过于担心策略和基于模型的 RL 等差异。
这里有一个很好的 TensorFlow DQN 示例,位于 GitHub 链接:github.com/floodsung/DQN-Atari-Tensorflow。代码可能有点过时,但它是一个简单且优秀的示例,值得一看。
我们不会进一步查看代码,但读者可以自由地深入研究。现在让我们尝试一些练习。
练习
一如既往,使用本节中的练习来更好地理解你学到的内容。尽量做至少两到三个本节中的练习:
-
返回
Chapter_5_1.py示例,并更改 alpha(learning_rate)变量,看看这对计算结果的影响。 -
返回
Chapter_5_2.py示例并更改各个老丨虎丨机的位置。 -
在示例
Chapter_5_2.py中更改学习率,并查看这对 Q 值结果输出的影响。 -
改变
Chapter_5_3.py示例中的 gamma 奖励折扣因子,并查看这对智能体训练的影响。 -
在
Chapter_5_3.py中改变探索的 epsilon 值并重新运行示例,看看更改各种探索参数对训练智能体的影响。 -
修改
Chapter_5_4.py示例中的各种参数(exploration,alpha,和gamma),观察这些参数对训练的影响。 -
修改
Chapter_5_4.py示例中内存的大小,增加或减少,观察这对训练的影响。 -
尝试在
Chapter_5_5.py中的 DQNAgent 示例中使用不同的 Gym 环境。你可以通过快速 Google 搜索查看其他可选的环境。 -
Chapter_5_6.py示例当前使用了一种叫做LinearAnnealedPolicy的形式探索策略;将策略更改为使用代码注释中提到的BoltzmannQPolicy策略。 -
一定要从
github.com/keras-rl/keras-rl下载并运行其他 Keras-RL 示例。同样,你可能需要安装其他 Gym 环境才能使它们正常工作。
还有大量关于 RL 的其他示例、视频和学习资料可供研究。尽可能多地学习,因为这些材料广泛且复杂,不是短时间内能够掌握的。
总结
RL(强化学习)是目前主导许多研究者兴趣的机器学习技术。它之所以吸引我们,通常是因为它非常适合游戏和仿真。在本章中,我们通过从多臂老丨虎丨机和上下文赌博机的基本入门问题开始,介绍了 RL 的一些基础知识。然后,我们快速了解了如何安装 OpenAI Gym RL 工具包。接着,我们研究了 Q 学习以及如何在代码中实现它并在 OpenAI Gym 环境中训练。最后,我们看到了如何通过加载其他环境(包括 Atari 游戏模拟器)来进行各种其他实验。
在下一章中,我们将探讨 Unity 目前正在开发的一个快速发展的前沿 RL 平台。
第六章:Unity ML-Agents
Unity 已经坚定并充满活力地拥抱了机器学习,尤其是深度强化学习(DRL),目标是为游戏和仿真开发者提供一个有效的深度强化学习(DRL)SDK。幸运的是,Unity 团队在 Danny Lange 的带领下,成功开发了一个强大的前沿 DRL 引擎,能够实现令人印象深刻的效果。这个引擎在多个方面超越了我们之前介绍的 DQN 模型,是顶级的。Unity 使用近端策略优化(PPO)模型作为其 DRL 引擎的基础。这个模型显著更为复杂,且可能在某些方面有所不同,但幸运的是,这只是许多章节的开始,我们会有足够的时间介绍这些概念——毕竟这是一本实践性强的书籍。
在本章中,我们介绍了Unity ML-Agents工具和 SDK,用于构建 DRL 代理来玩游戏和仿真。虽然这个工具既强大又前沿,但它也很容易使用,并且提供了一些帮助我们边学边用的工具。在本章中,我们将涵盖以下主题:
-
安装 ML-Agents
-
训练一个代理
-
大脑里有什么?
-
使用 TensorBoard 监控训练过程
-
运行一个代理
我们想感谢 Unity 团队成员们在 ML-Agents 方面的卓越工作;以下是写作时的团队成员名单:
-
Danny Lange (
arxiv.org/search/cs?searchtype=author&query=Lange%2C+D) -
Arthur Juliani (
arxiv.org/search/cs?searchtype=author&query=Juliani%2C+A) -
Vincent-Pierre Berges (
arxiv.org/search/cs?searchtype=author&query=Berges%2C+V) -
Esh Vckay (
arxiv.org/search/cs?searchtype=author&query=Vckay%2C+E) -
Yuan Gao (
arxiv.org/search/cs?searchtype=author&query=Gao%2C+Y) -
Hunter Henry (
arxiv.org/search/cs?searchtype=author&query=Henry%2C+H) -
Marwan Mattar (
arxiv.org/search/cs?searchtype=author&query=Mattar%2C+M) -
Adam Crespi (
arxiv.org/search/cs?searchtype=author&query=Crespi%2C+A) -
Jonathan Harper (
arxiv.org/search/cs?searchtype=author&query=Harper%2C+J)
在继续本章之前,请确保已按照第四章中的说明安装 Unity,构建深度学习游戏聊天机器人。
安装 ML-Agents
本节中,我们将概述成功安装 ML-Agents SDK 所需的高层步骤。这些材料仍然处于 beta 版本,并且在各版本之间已经发生了显著变化。因此,如果在执行这些高层步骤时遇到困难,只需返回到最新的 Unity 文档;它们写得非常清晰。
到你的计算机上操作并跟随这些步骤;可能会有很多子步骤,所以预计这会花费一些时间:
-
确保你的计算机已安装 Git;它可以通过命令行使用。Git 是一个非常流行的源代码管理系统,关于如何安装和使用 Git 的资源非常丰富,适用于你的平台。安装 Git 后,确保它正常工作,可以通过克隆一个仓库来测试,任意一个仓库都可以。
-
打开命令窗口或常规 shell。Windows 用户可以打开 Anaconda 窗口。
-
切换到一个工作文件夹,放置新代码的文件夹,并输入以下命令(Windows 用户可以选择使用
C:\ML-Agents):
git clone https://github.com/Unity-Technologies/ml-agents
- 这将把
ml-agents仓库克隆到你的计算机上,并创建一个同名的文件夹。你可能还想额外为文件夹名称添加版本号。Unity,以及整个 AI 领域,至少在当前阶段,是在不断变化的。这意味着新的变化和持续的更新始终在发生。写作时,我们将仓库克隆到名为ml-agents.6的文件夹中,如下所示:
git clone https://github.com/Unity-Technologies/ml-agents ml-agents.6
本书的作者曾经写过一本关于 ML-Agents 的书,并且在短时间内重新编写了几章,以适应重要的变更。实际上,本章也经历了几次重写,以应对更多的重大变化。
- 为
ml-agents创建一个新的虚拟环境并将其设置为3.6,像这样:
#Windows
conda create -n ml-agents python=3.6
#Mac
Use the documentation for your preferred environment
- 激活该环境,同样通过 Anaconda:
activate ml-agents
- 安装 TensorFlow。使用 Anaconda,我们可以通过以下命令进行安装:
pip install tensorflow==1.7.1
- 安装 Python 包。在 Anaconda 中,输入以下命令:
cd ML-Agents #from root folder
cd ml-agents or cd ml-agents.6 #for example
cd ml-agents
pip install -e . or pip3 install -e .
- 这将安装 Agents SDK 所需的所有软件包,可能需要几分钟时间。请确保保持此窗口打开,因为我们稍后会使用它。
这是 TensorFlow 的基本安装,不使用 GPU。请查阅 Unity 文档,了解如何安装 GPU 版本。这可能会对你的训练性能产生显著影响,具体取决于你的 GPU 性能。
这应该完成 Unity Python SDK for ML-Agents 的设置。在下一部分中,我们将学习如何设置并训练 Unity 提供的多个示例环境之一。
训练一个智能体
本书的大部分时间,我们都在研究代码以及 深度学习 (DL) 和 强化学习 (RL) 的内部知识。基于这些知识,我们现在可以进入并查看 深度强化学习 (DRL) 的实际应用。幸运的是,新的代理工具包提供了多个示例,展示了引擎的强大功能。打开 Unity 或 Unity Hub 并按照以下步骤操作:
-
点击项目对话框顶部的“打开项目”按钮。
-
定位并打开
UnitySDK项目文件夹,如下图所示:

打开 UnitySDK 项目
-
等待项目加载完成后,打开编辑器底部的项目窗口。如果系统提示更新项目,请确保选择“是”或“继续”。到目前为止,所有代理代码都已设计为向后兼容。
-
定位并打开如截图所示的 GridWorld 场景:

打开 GridWorld 示例场景
-
在层级窗口中选择 GridAcademy 对象。
-
然后将注意力集中在 Inspector 窗口,点击 Brains 旁边的目标图标以打开大脑选择对话框:

检查 GridWorld 示例环境
-
选择 GridWorldPlayer 大脑。这个大脑是 玩家 大脑,意味着玩家(即你)可以控制游戏。我们将在下一部分详细探讨这个大脑概念。
-
按下编辑器顶部的播放按钮,观察网格环境的形成。由于游戏当前设置为玩家控制,你可以使用 WASD 控制键来移动方块。目标与我们之前为 FrozenPond 环境构建的 DQN 类似。也就是说,你需要将蓝色方块移动到绿色的 + 符号处,并避免碰到红色的 X。
随时可以玩游戏,注意游戏运行的时间是有限的,并且不是回合制的。在下一部分中,我们将学习如何使用 DRL 代理运行这个示例。
大脑中有什么?
ML-Agents 平台的一个亮点是能够非常快速且无缝地从玩家控制切换到 AI/代理控制。为了实现这一点,Unity 引入了 大脑 的概念。大脑可以是玩家控制的,也可以是代理控制的,后者被称为学习大脑。亮点在于,你可以构建一个游戏并进行测试,之后玩家可以将游戏交给 RL 代理来控制。这一流程的额外好处是,让任何用 Unity 编写的游戏都能通过极少的努力由 AI 控制。事实上,这种强大的工作流让我们决定专门花一整章时间来讨论,第十二章,使用 DRL 调试/测试游戏,来学习如何使用 RL 测试和调试你的游戏。
使用 Unity 训练 RL 代理非常简单,设置和运行都很直接。Unity 在外部使用 Python 来构建学习脑模型。使用 Python 是更有意义的,因为正如我们之前所见,多个深度学习库都是基于 Python 构建的。按照以下步骤训练 GridWorld 环境中的代理:
- 重新选择 GridAcademy,并将 Brains 从 GridWorldPlayer 切换为 GridWorldLearning,如下所示:

切换脑部使用 GridWorldLearning
-
确保点击最后的 Control 选项。这个简单的设置告诉脑部它可以被外部控制。一定要再次确认该选项已启用。
-
在层级窗口中选择
trueAgent对象,然后在检查器窗口中,将 Grid Agent 组件下的 Brain 属性更改为 GridWorldLearning 脑:

将代理的脑设置为 GridWorldLearning
-
对于此示例,我们希望将 Academy 和 Agent 切换为使用相同的 brain,即 GridWorldLearning。在我们稍后探索的更高级用法中,情况不总是如此。当然,你也可以让一个玩家和一个代理脑同时运行,或者其他多种配置。
-
确保你已打开 Anaconda 或 Python 窗口,并设置为
ML-Agents/ml-agents文件夹或你的版本化ml-agents文件夹。 -
在 Anaconda 或 Python 窗口中,使用
ml-agents虚拟环境运行以下命令:
mlagents-learn config/trainer_config.yaml --run-id=firstRun --train
-
这将启动 Unity PPO 训练器,并根据配置运行代理示例。在某些时候,命令窗口将提示你运行已加载环境的 Unity 编辑器。
-
在 Unity 编辑器中按下播放按钮,运行 GridWorld 环境。不久之后,你应该能看到代理正在训练,且结果会输出到 Python 脚本窗口中:

在训练模式下运行 GridWorld 环境
-
注意
mlagents-learn脚本是构建 RL 模型以运行代理的 Python 代码。从脚本的输出可以看出,有多个参数,或者我们称之为超参数,需要进行配置。部分参数可能会让你感到熟悉,没错,但也有一些可能不太明了。幸运的是,在本章和本书的后续内容中,我们将详细探讨如何调整这些参数。 -
让代理训练几千次,并注意它学习的速度。这里的内部模型,称为PPO,已被证明在多种任务中非常有效,且非常适合游戏开发。根据你的硬件,代理可能在不到一小时的时间内就能完美完成此任务。
继续让代理训练,我们将在下一节中探讨更多检查代理训练进度的方法。
使用 TensorBoard 监控训练过程
使用强化学习(RL)或任何深度学习(DL)模型来训练智能体,虽然很有趣,但通常不是一件简单的任务,需要注意细节。幸运的是,TensorFlow 自带了一套名为TensorBoard的图形工具,我们可以用它来监控训练进度。按照以下步骤运行 TensorBoard:
-
打开一个 Anaconda 或 Python 窗口。激活
ml-agents虚拟环境。不要关闭运行训练器的窗口;我们需要保持它运行。 -
导航到
ML-Agents/ml-agents文件夹,并运行以下命令:
tensorboard --logdir=summaries
-
这将启动 TensorBoard 并运行它内置的 Web 服务器。你可以通过运行前述命令后显示的 URL 加载页面。
-
输入窗口中显示的 TensorBoard URL,或者在浏览器中使用
localhost:6006或machinename:6006。大约一个小时后,你应该会看到类似以下的内容:

TensorBoard 图形窗口
-
在上面的截图中,你可以看到每个不同的图表表示了训练的一个方面。理解这些图表对于了解智能体的训练过程非常重要,因此我们将逐个分析每个部分的输出:
- 环境:这一部分展示了智能体在环境中的整体表现。接下来的截图展示了每个图表的更详细查看及其优选趋势:

进一步查看环境部分的图表
-
累积奖励:这是智能体正在最大化的总奖励。通常你希望看到它上升,但也有可能出现下降的情况。最好将奖励最大化在 1 到-1 的范围内。如果你在图表中看到超出这个范围的奖励,也需要进行修正。
-
训练回合长度:如果这个值减小,通常是个好兆头。毕竟,较短的回合意味着更多的训练。然而,请记住,回合长度可能因需要而增加,因此这个值可能会有所波动。
-
课程:这表示智能体当前所在的课程,适用于课程学习。我们将在第九章中学习更多关于课程学习的内容,奖励和强化学习。
-
损失:这一部分显示了表示策略和价值计算损失或成本的图表。当然,我们并没有花太多时间解释 PPO 及其如何使用策略,所以在此时,只需理解训练时的优选方向。接下来是该部分的截图,箭头显示了最佳的偏好方向:

损失和优选训练方向
-
策略损失:这个值决定了策略随时间的变化程度。策略是决定行为的部分,通常这个图表应该显示出下降趋势,表明策略在做决策时越来越好。
-
值损失:这是
值函数的平均损失。它基本上表示代理对下一个状态的价值预测得有多准确。最初,这个值应增加,奖励稳定后它应减少。 -
策略:PPO 使用策略的概念,而不是模型来确定行动的质量。同样,我们将在第八章《理解 PPO》中花更多时间讨论这一点,并揭示 PPO 的更多细节。下一张截图展示了策略图及其优先趋势:

策略图和优先趋势
-
熵:表示代理探索的程度。随着代理对环境了解得更多,它需要探索的程度减少,因此该值应减少。
-
学习率:当前该值设置为随时间线性下降。
-
值估计:这是所有代理状态访问的平均值或均值。此值应增加,以代表代理知识的增长,然后稳定下来。
这些图表都旨在与 Unity 基于 PPO 方法的实现一起使用。暂时不用太担心理解这些新术语,我们将在第七章《代理与环境》中深入探讨 PPO 的基础知识。
-
让代理运行到完成,并保持 TensorBoard 运行。
-
返回到正在训练大脑的 Anaconda/Python 窗口,并运行以下命令:
mlagents-learn config/trainer_config.yaml --run-id=secondRun --train
- 你将再次被提示在编辑器中按下播放按钮;请确保这样做。让代理开始训练并运行几轮。在此过程中,监控 TensorBoard 窗口,并注意图表上如何显示
secondRun。你也可以让这个代理运行到完成,但如果你愿意,现在可以停止它。
在 ML-Agents 的早期版本中,你需要先构建一个 Unity 可执行文件作为游戏训练环境并运行它。外部 Python 大脑依旧按照相同的方式运行。这种方法使得调试代码问题或游戏中的问题变得非常困难。所有这些问题已经通过当前的方法得到解决;然而,对于某些自定义训练,我们可能需要以后再使用旧的可执行文件方法。
现在我们已经看到设置和训练代理是多么简单,接下来我们将通过下一部分来了解如何在没有外部 Python 大脑的情况下直接在 Unity 中运行该代理。
运行代理
使用 Python 进行训练效果很好,但它不是实际游戏中会使用的方式。理想情况下,我们希望能够构建一个 TensorFlow 图并在 Unity 中使用它。幸运的是,构建了一个名为 TensorFlowSharp 的库,它允许 .NET 使用 TensorFlow 图。这使我们能够构建离线的 TF 模型,并在之后将其注入到我们的游戏中。不幸的是,我们只能使用已训练的模型,暂时不能以这种方式进行训练。
让我们通过使用刚刚为 GridWorld 环境训练的图表并将其作为 Unity 中的内部大脑来看看它是如何工作的。请按下一个小节中的练习步骤来设置并使用内部大脑:
- 从此链接下载 TFSharp 插件:
s3.amazonaws.com/unity-ml-agents/0.5/TFSharpPlugin.unitypackage。
如果此链接无法使用,请查阅 Unity 文档或 Asset Store 获取新链接。当前版本被描述为实验性,并可能发生变化。
-
从编辑器菜单中选择 Assets | Import Package | Custom Package...
-
找到刚刚下载的资产包,并使用导入对话框将插件加载到项目中。如果您需要有关这些基础 Unity 任务的帮助,网上有大量的资料可以进一步指导您。
-
从菜单中选择 Edit | Project Settings。这将打开设置窗口(2018.3 新增功能)
-
在 Player 选项下找到 Scripting Define Symbols,并将文本设置为
ENABLE_TENSORFLOW,并启用 Allow Unsafe Code,如下图所示:

设置 ENABLE_TENSORFLOW 标志
-
在 Hierarchy 窗口中找到 GridWorldAcademy 对象,并确保它使用的是 Brains | GridWorldLearning。然后关闭 Grid Academy 脚本中 Brains 部分下的 Control 选项。
-
在
Assets/Examples/GridWorld/Brains文件夹中找到 GridWorldLearning 大脑,并确保在 Inspector 窗口中设置了 Model 参数,如下图所示:

为大脑设置使用的模型
-
Model应该已经设置为 GridWorldLearning 模型。在这个例子中,我们使用的是与 GridWorld 示例一起提供的 TFModel。您也可以通过将我们在前一个示例中训练的模型导入到项目中并将其设置为模型来轻松使用它。 -
按下 Play 运行编辑器并观察代理控制立方体。
目前,我们正在使用预训练的 Unity 大脑运行环境。在下一个小节中,我们将查看如何使用我们在前一节中训练的大脑。
加载已训练的大脑
所有 Unity 示例都带有预训练的大脑,您可以使用它们来探索示例。当然,我们希望能够将我们自己的 TF 图加载到 Unity 中并运行它们。请按照以下步骤加载已训练的图表:
- 找到
ML-Agents/ml-agents/models/firstRun-0文件夹。在此文件夹中,您应看到一个名为GridWorldLearning.bytes的文件。将该文件拖入 Unity 编辑器中的Project/Assets/ML-Agents/Examples/GridWorld/TFModels文件夹,如下所示:

将字节图表拖入 Unity
-
这将把图形导入 Unity 项目作为资源,并将其重命名为
GridWorldLearning 1。这样做是因为默认模型已经有了相同的名称。 -
在
brains文件夹中找到GridWorldLearning,在 Inspector 窗口中选择它,并将新的GridWorldLearning 1模型拖动到大脑参数下的模型插槽中:

在大脑中加载图形模型插槽。
-
在这一阶段,我们不需要更改其他参数,但请特别注意大脑的配置。默认设置目前是可行的。
-
在 Unity 编辑器中按下播放按钮,观察智能体成功地完成游戏。
-
训练智能体的时间将真正决定它在游戏中的表现。如果让它完成训练,智能体的表现应当与已经训练好的 Unity 智能体相当。
现在有很多 Unity 示例,您可以自行运行和探索。可以随意训练多个示例,或者按照下一节中的练习进行训练。
练习
使用本节中的练习来增强和巩固您的学习。至少尝试其中一些练习,记住这些练习对您有益:
-
设置并运行 3DBall 示例环境以训练一个有效的智能体。这个环境使用多个游戏/智能体来进行训练。
-
设置 3DBall 示例,让一半的游戏使用已经训练好的大脑,另一半使用训练或外部学习。
-
使用外部学习训练 PushBlock 环境中的智能体。
-
训练 VisualPushBlock 环境。注意这个示例如何使用视觉摄像头捕捉环境状态。
-
作为玩家运行 Hallway 场景,然后使用外部学习大脑训练该场景。
-
作为玩家运行 VisualHallway 场景,然后使用外部学习大脑训练该场景。
-
运行 WallJump 场景,然后在训练条件下运行它。这个示例使用了课程训练,稍后我们将在第九章中深入探讨,奖励与强化学习。
-
运行金字塔场景,然后为训练进行设置。
-
运行 VisualPyramids 场景并为训练进行设置。
-
运行 Bouncer 场景并为训练进行设置。
虽然您不必运行所有这些练习/示例,但熟悉它们是很有帮助的。它们往往可以作为创建新环境的基础,正如我们在下一章中所看到的那样。
总结
如你所学,Unity 中训练 RL 和 DRL 代理的工作流比在 OpenAI Gym 中更加集成和无缝。我们不需要写一行代码就能在网格世界环境中训练代理,而且视觉效果要好得多。对于本章,我们从安装 ML-Agents 工具包开始。然后我们加载了一个 GridWorld 环境,并设置它与 RL 代理进行训练。从那时起,我们查看了 TensorBoard 来监控代理的训练和进展。在训练完成后,我们首先加载了一个 Unity 预训练的大脑,并在 GridWorld 环境中运行它。接着,我们使用了一个刚刚训练好的大脑,并将其作为资产导入到 Unity 中,作为 GridWorldLearning 大脑的模型。
在下一章中,我们将探讨如何构建一个新的 RL 环境或游戏,我们可以使用代理来学习和玩耍。这将使我们进一步了解本章中略过的各种细节。
第七章:智能体与环境
玩转和探索实验性的强化学习环境是很有趣的,但最终,大多数游戏开发者希望开发自己的学习环境。为了做到这一点,我们需要更深入地了解训练深度强化学习环境,特别是一个智能体如何接收和处理输入。因此,在本章中,我们将仔细研究如何在 Unity 中训练一个更为复杂的示例环境。这将帮助我们理解输入和状态对训练智能体的重要性,以及 Unity ML-Agents 工具包中许多使我们能够探索多种选项的特性。本章对于任何希望在自己的游戏中构建环境并使用 ML-Agents 的人来说都至关重要。所以,如果你需要反复阅读本章以理解细节,请务必这样做。
在本章中,我们将涵盖许多与智能体如何处理输入/状态相关的细节,以及你如何调整这些内容以适应你的智能体训练。以下是本章内容的总结:
-
探索训练环境
-
理解状态
-
理解视觉状态
-
卷积与视觉状态
-
循环神经网络
确保你已经阅读、理解并运行了上一章的部分示例练习,第六章,Unity ML-Agents。在继续之前,确保你已经正确配置并运行了 Unity 和 ML-Agents 工具包。
探索训练环境
经常推动我们成功或推动我们学习的因素之一就是失败。作为人类,当我们失败时,通常会发生两件事:我们要么更加努力,要么选择放弃。有趣的是,这与强化学习中的负奖励很相似。在 RL 中,智能体如果获得负奖励,可能会因为看不到未来的价值或预测无法带来足够的好处而放弃探索某条路径。然而,如果智能体认为需要更多的探索,或者它没有完全探索完这条路径,它就会继续前进,并且通常这会引导它走上正确的道路。同样,这与我们人类的情况非常相似。因此,在本节中,我们将训练一个较为复杂的示例智能体,促使我们学习如何面对失败并修正训练中的问题。
Unity 目前正在构建一个多级基准塔环境,具有多个难度等级。这将允许深度强化学习爱好者、从业者和研究人员在基准环境上测试他们的技能和模型。作者获得的相对可靠的消息是,这个环境应该会在 2019 年年初或年中完成。
我们最终需要使用 Unity ML-Agents 工具包的许多高级功能来使这个示例正常工作。这要求你对本书的前五章有良好的理解。如果你跳过了这些章节来到这里,请根据需要回去复习。在本章的许多地方,我们提供了指向之前相关章节的有用链接。
我们将关注的训练示例环境是 VisualHallway,不要与标准的 Hallway 示例混淆。VisualHallway 的不同之处在于它使用摄像头作为模型的完整输入状态,而我们之前看到的其他 Unity 示例则使用某种形式的多传感器输入,通常允许代理在任何时候看到 90 度到 360 度的视角,并提供其他有用的信息。这对大多数游戏来说是可以接受的,事实上,许多游戏仍然允许这样的“作弊”或直觉作为 NPC 或计算机对手 AI 的一部分。将这些“作弊”加入游戏 AI 一直是一个被接受的做法,但也许这很快就会发生改变。
毕竟,好游戏是有趣的,并且对玩家来说是合乎逻辑的。过去不久的游戏可能能让 AI 作弊,但现在,玩家对 AI 的期望更高了,他们希望 AI 和他们遵循相同的规则。之前认为计算机 AI 受到技术限制的观念已经消失,现在游戏 AI 必须遵循与玩家相同的规则,这使得我们专注于使 VisualHallway 示例正常工作和训练变得更加有意义。
当然,教 AI 像玩家一样玩/学习还有另一个额外的好处,那就是能够将这种能力转移到其他环境中,这个概念叫做迁移学习。我们将在第十章《模仿与迁移学习》中探索迁移学习,学习如何调整预训练模型/参数,并将其应用于其他环境。
VisualHallway/Hallway 示例首先会将代理随机放入一个长房间或走廊。在这个空间的中央是一个彩色方块,每个走廊的两端角落都有一个覆盖地板的彩色方形区域。这个方块的颜色要么是红色,要么是金色(橙色/黄色),用来告诉代理目标方块的颜色与之相同。目标是让代理移动到正确的彩色方块。在标准的 Hallway 示例中,代理拥有 360 度的传感器感知。而在 VisualHallway 示例中,代理只能看到房间的摄像机视图,就像玩家在游戏中看到的一样。这使得我们的代理与玩家站在了同一起跑线。
在开始训练之前,让我们像玩家一样打开示例并玩一下,看看我们能做得怎么样。按照这个练习打开 VisualHallway 示例:
-
在继续之前,确保你已经正确安装了 ML-Agents,并且能够在 Python 中外部训练大脑。如果需要帮助,请参考上一章。
-
从项目窗口的 Assets | ML-Agents | Examples | Hallway | Scenes 文件夹中打开 VisualHallway 场景。
-
确保 Agent | Hallway Agent | Brain 设置为 VisualHallwayPlayer,如下图所示:

Hallway Agent | Brain 设置为 player
-
在编辑器中按下播放按钮运行场景,并使用 W、A、S 和 D 键来控制代理。记住,目标是移动到与中心方块颜色相同的方块。
-
玩游戏并移动到两个颜色方块,观察在进入奖励方块时,正向或负向奖励给予时会发生什么。游戏画面在进入奖励方块时会闪烁绿色或红色。
这个游戏环境典型地模拟了第一人称射击游戏,非常适合训练代理以第一人称视角进行游戏。训练代理以类似人类的方式玩游戏是许多 AI 从业者的目标,虽然这可能是你是否会在游戏中实现的功能。正如我们所见,根据你游戏的复杂性,这种学习/训练可能甚至不是一个可行的选项。此时,我们应该了解如何设置并通过视觉训练代理。
直观地训练代理
幸运的是,设置代理进行视觉训练相当简单,特别是如果你已经完成了上一章的练习。打开 Unity 编辑器并加载 VisualHallway 场景,准备好 Python 命令行或 Anaconda 窗口,我们就可以开始了:
- 在 Unity 中,将 Agent | Hallway Agent | Brain 更改为 VisualHallwayLearning,如下图所示:

将大脑更改为学习模式
-
点击 VisualHallwayLearning 大脑,在项目窗口中定位它。
-
点击 VisualHallwayLearning 大脑,在检查器窗口中查看其属性,如下图所示:

确认学习大脑的属性设置正确
-
确保大脑参数设置为接受分辨率为
84x84像素的单一视觉观察,并且不使用灰度。灰度仅是去除颜色通道,使输入变为一个通道,而非三个通道。回顾我们在第二章中讨论的卷积神经网络(CNN)层,卷积和递归网络。同时,确保 Vector Observation | Space Size 设置为 0,如前图所示。 -
在菜单中选择文件 | 保存和文件 | 保存项目,以保存所有更改。
-
切换到你的 Python 窗口或 Anaconda 提示符,确保你在
ML-Agents/ml-agents目录下,并运行以下命令:
mlagents-learn config/trainer_config.yaml --run-id=visualhallway --train
-
在命令执行后,等待提示以启动编辑器。然后,在提示时运行编辑器,并让示例运行完成,或者运行到你有耐心的时长为止。
-
在示例运行完成后,你应该会看到如下所示的内容:

完整的训练运行直到完成
假设你训练智能体直到运行结束,即训练了 500K 次迭代,那么你可以确认智能体确实什么也没学到。那么,为什么 Unity 会在他们的示例中加入这样的示例呢?嗯,你可以认为这是一个故意设计的挑战,或者只是他们的一次疏忽。无论如何,我们将其视为一个挑战,借此更好地理解强化学习。
在我们应对这个挑战之前,先回过头来重新确认我们对这个环境的理解,通过查看下一个部分中更易于训练的 Hallway 示例。
回归基础
当你在问题上卡住时,回到最基础的地方确认一切是否按预期工作是很有帮助的。公平地说,我们还没有深入探索 ML-Agents 的内部机制,也没有真正理解深度强化学习(DRL),因此我们实际上并没有从一开始就出发。但为了本示例的目的,我们将回过头来,详细查看 Hallway 示例。返回编辑器并执行以下操作:
-
在编辑器中打开 Hallway 示例场景。记住,场景位于 Assets | ML-Agents | Examples | Hallway | Scenes 文件夹中。
-
这个示例配置为使用多个并发训练环境。我们能够使用相同的大脑训练多个并发训练环境,因为 近端策略优化(PPO),支持这个智能体的强化学习算法,是基于策略进行训练,而不是基于模型。我们将在 第八章《理解 PPO》中详细讲解基于策略和基于模型的学习。为了简化操作,目前我们将暂时禁用这些额外的环境。
-
按下 Shift 键,然后在层级面板中选择所有编号的 HallwayArea(1-15)对象。
-
选中所有额外的 HallwayArea 对象,点击 "Active" 复选框将其禁用,如下图所示:

禁用所有额外的训练走廊
-
打开层级窗口中剩下的活动 HallwayArea 并选择 Agent。
-
将 Brain 智能体设置为使用 HallwayLearning brain。默认情况下,它可能设置为使用玩家的大脑。
-
在层级窗口中重新选择 Academy 对象,确保 Hallway Academy 组件的大脑设置为 Learning,并且启用了 Control 复选框。
-
打开 Python 或 Anaconda 窗口,进入
ML-Agents/ml-agents文件夹。确保你的 ML-Agents 虚拟环境已激活,并运行以下命令:
mlagents-learn config/trainer_config.yaml --run-id=hallway --train
- 让训练器启动并提示你点击编辑器中的 Play 按钮。观察代理运行,并将其表现与 VisualHallway 示例进行对比。
通常,在 50,000 次迭代之前,你会注意到代理有一些训练活动,但这可能会有所不同。所谓训练活动,是指代理的平均奖励(Mean Reward)大于-1.0,标准奖励(Standard Reward)不等于零。即使你让示例运行完成,即 500,000 次迭代,它也不太可能训练到正的平均奖励。我们通常希望奖励范围从-1.0 到+1.0,并且有一定的变化来展示学习活动。如果你还记得 VisualHallway 示例,代理在整个训练过程中没有显示任何学习活动。我们本可以延长训练迭代次数,但不太可能看到任何稳定的训练成果。原因在于状态空间的增加和奖励的处理。我们将在下一节扩展对状态的理解,并讨论它与强化学习的关系。
理解状态
Hallway 和 VisualHallway 示例本质上是相同的游戏问题,但提供了不同的视角,或者我们在强化学习中所说的环境或游戏状态。在 Hallway 示例中,代理通过传感器输入进行学习,这一点我们将很快讨论,而在 VisualHallway 示例中,代理通过摄像头或玩家视角进行学习。此时,理解每个示例如何处理状态,以及我们如何修改状态,将会非常有帮助。
在接下来的练习中,我们将修改 Hallway 输入状态并查看结果:
-
跳回到上一个练习结束时启用学习的 Hallway 场景。
-
我们需要修改几行 C#代码,没什么难的,但安装 Visual Studio(Community 版或其他版本)会比较有用,因为这是我们推荐的编辑器。当然,你也可以使用任何你喜欢的代码编辑器,只要它与 Unity 兼容。
-
在层级窗口中找到 Agent 对象,然后在检视窗口中点击 Hallway Agent 组件上的齿轮图标,如下图所示:

打开 HallwayAgent.cs 脚本
-
从上下文菜单中选择编辑脚本选项,如上图所示。这将会在你选择的代码编辑器中打开脚本。
-
在编辑器中找到以下 C#代码部分:
public override void CollectObservations()
{
if (useVectorObs)
{
float rayDistance = 12f;
float[] rayAngles = { 20f, 60f, 90f, 120f, 160f };
string[] detectableObjects = { "orangeGoal", "redGoal", "orangeBlock", "redBlock", "wall" };
AddVectorObs(GetStepCount() / (float)agentParameters.maxStep);
AddVectorObs(rayPer.Perceive(rayDistance, rayAngles, detectableObjects, 0f, 0f));
}
}
-
CollectObservations方法是智能体收集观察或输入状态的地方。在 Hallway 示例中,智能体将useVectorObs设置为true,意味着它通过if语句内部的代码块来检测状态。所有这些代码做的事情就是从智能体发射一束射线,角度分别为20f、60f、120f和160f度,距离由rayDistance定义,并检测在detectableObjects中定义的物体。这些射线感知是通过一个名为rayPer的辅助组件完成的,rayPer的类型是RayPerception,并执行rayPer.Percieve来收集它所感知到的环境状态。这些信息与步骤的比例一起,添加到智能体输入的向量观察或状态中。此时,状态是长度为 36 的向量。根据这个版本,必须在代码中构造它,但未来可能会有所变化。 -
修改
rayAngles这一行代码,使其与以下内容匹配:
float[] rayAngles = { 20f, 60f };
-
这样做的效果是显著缩小了智能体的视野或感知范围,从 180 度缩小到 60 度。换句话说,就是减少了输入状态。
-
完成编辑后,保存文件并返回 Unity。当你返回编辑器时,Unity 会重新编译代码。
-
在 Assets | ML-Agents | Examples | Hallway | Brains 文件夹中找到 HallwayLearning 大脑,并将 Vector Observation | Space Size 修改为
15,如以下截图所示:

设置向量观察空间大小
-
我们将其减少到 15 的原因是:现在的输入由两个角度输入加一个步骤输入组成。每个角度输入包括五个可检测的物体,再加上两个边界,总共七个感知或输入。因此,两个角度乘以七个感知,再加上一个步骤,等于 15。之前,我们有五个角度乘以七个感知,再加上一个步骤,等于 35。
-
在修改 Brain 可编程对象后,请确保保存项目。
-
再次运行示例进行训练,并观察智能体如何训练。花些时间关注智能体采取的动作以及它是如何学习的。务必让这个示例运行的时间与其他 Hallway 示例相同,希望能够完整运行。
结果让你感到惊讶吗?是的,我们的智能体在较小的视野下实际上训练得更快。这个结果可能看起来完全不合常理,但从数学角度考虑,小的输入空间或状态意味着智能体有更少的路径可供探索,因此应该训练得更快。这正是我们在减少输入空间超过一半后在这个示例中看到的情况。在此时,我们肯定需要观察在 VisualHallway 示例中,减少视觉状态空间会发生什么。
理解视觉状态
强化学习(RL)是一种非常强大的算法,但当我们开始处理大量状态输入时,计算复杂性会变得非常高。为了应对庞大的状态,许多强大的强化学习算法使用无模型或基于策略的学习概念,这一点我们将在后面的章节中讨论。如我们所知,Unity 使用基于策略的算法,允许它通过推广到策略来学习任何大小的状态空间。这使得我们可以轻松地将我们刚才运行的示例中的 15 个向量输入转变为更大规模的状态空间,就像在 VisualHallway 示例中那样。
让我们打开 Unity 到 VisualHallway 示例场景,并看看如何在接下来的练习中减少视觉输入空间:
-
在打开 VisualHallway 场景的同时,找到位于 Assets | ML-Agents | Examples | Hallway | Brains 文件夹中的 HallwayLearningBrain 并选择它。
-
将 Brain 参数 | 视觉观察的第一个相机可观察输入修改为
32x32灰度。如下截图所示:

设置代理的视觉观察空间
-
当视觉观察设置在大脑上时,每一帧都会以所选的分辨率从相机中捕捉。之前,捕捉的图像大小为 84 x 84 像素,虽然远不如玩家模式下的游戏屏幕那么大,但仍然明显大于 35 个向量输入。通过减小图像大小并将其转换为灰度,我们将输入框架从 84 x 84 x 3 = 20,172 个输入,减少到 32 x 32 x 1 = 1,024 个输入。反过来,这大大减少了所需的模型输入空间以及学习所需的网络复杂度。
-
保存项目和场景。
-
使用以下命令再次以学习模式运行 VisualHallway:
mlagents-learn config/trainer_config.yaml --run-id=vh_reduced --train
-
注意我们在每次运行时都在更改
--run-id参数。回想一下,如果我们要使用 TensorBoard,那么每次运行都需要一个唯一的名称,否则它会覆盖之前的运行。 -
让示例训练的时间与之前运行 VisualHallway 练习的时间一样,因为这样你可以很好的比较我们在状态上所做的变化。
结果是否如你所预期?是的,代理仍然没有学会,即使在减少了状态后。原因在于,较小的视觉状态实际上在这种情况下对代理是有害的。就像我们人类在尝试通过针孔看事物时的效果一样。然而,还有另一种方法可以通过卷积将视觉状态减少为特征集。正如你可能记得的,我们在第二章《卷积神经网络和循环神经网络》中详细讨论了卷积。在接下来的章节中,我们将研究如何通过添加卷积层来减少示例的视觉状态。
卷积与视觉状态
在 ML-Agents 工具包中,代理使用的视觉状态是通过一个过程定义的,这个过程在特定的分辨率下截取截图,然后将截图输入到卷积网络中,以训练某种形式的嵌入状态。在接下来的练习中,我们将打开 ML-Agents 的训练代码,并增强卷积代码以获得更好的输入状态:
-
使用文件浏览器打开 ML-Agents
trainers文件夹,路径为ml-agents.6\ml-agents\mlagents\trainers。在这个文件夹中,你会找到几个用于训练代理的 Python 文件。我们感兴趣的文件叫做models.py。 -
在你选择的 Python 编辑器中打开
models.py文件。Visual Studio 与 Python 数据扩展是一个非常好的平台,并且还提供了交互式调试代码的功能。 -
向下滚动文件,找到
create_visual_observation_encoder函数,其内容如下:
def create_visual_observation_encoder(self, image_input, h_size, activation, num_layers, scope,reuse):
#comments removed
with tf.variable_scope(scope):
conv1 = tf.layers.conv2d(image_input, 16, kernel_size=[8, 8], strides=[4, 4],activation=tf.nn.elu, reuse=reuse, name="conv_1")
conv2 = tf.layers.conv2d(conv1, 32, kernel_size=[4, 4], strides=[2, 2],activation=tf.nn.elu, reuse=reuse, name="conv_2")
hidden = c_layers.flatten(conv2)
with tf.variable_scope(scope + '/' + 'flat_encoding'):
hidden_flat = self.create_vector_observation_encoder(hidden, h_size, activation, num_layers, scope, reuse)
return hidden_flat
-
代码使用的是 Python 和 TensorFlow,但你应该能够识别
conv1和conv2卷积层。注意层的卷积核和步幅是如何定义的,以及缺少的池化层。Unity 没有使用池化,以避免丢失数据中的空间关系。然而,正如我们之前讨论的,这并非总是那么简单,实际上,这取决于你要识别的视觉特征类型。 -
在两个卷积层后添加以下代码行,并修改
hidden层的设置,如下所示:
conv1 = tf.layers.conv2d(image_input, 16, kernel_size=[8, 8], strides=[4, 4], activation=tf.nn.elu, reuse=reuse, name="conv_1")
conv2 = tf.layers.conv2d(conv1, 32, kernel_size=[4, 4], strides=[2, 2], activation=tf.nn.elu, reuse=reuse, name="conv_2")
conv3 = tf.layers.conv2d(image_input, 64, kernel_size=[2, 2], strides=[2, 2], activation=tf.nn.elu, reuse=reuse, name="conv_3")
hidden = c_layers.flatten(conv3)
-
这将产生在代理的游戏视图中添加另一个卷积层的效果,以提取更细节的内容。正如我们在第二章中看到的,卷积神经网络与递归网络,增加额外的卷积层会增加训练时间,但确实会提高训练表现——至少在图像分类器上是这样。
-
跳回你的命令行或 Anaconda 窗口,并使用以下命令以学习模式运行示例:
mlagents-learn config/trainer_config.yaml --run-id=vh_conv1 --train
- 观察训练过程并查看代理的表现——在示例运行时,一定要在游戏窗口中观察代理的动作。代理是否按你预期的方式执行?将结果与之前的运行进行比较,并注意其中的差异。
你一定会注意到,代理变得稍微更优雅,能够执行更精细的动作。虽然训练过程可能需要更长的时间,但这个代理能够观察到环境中的细微变化,因此会做出更精细的动作。当然,你也可以将整个 CNN 架构替换为使用更明确的架构。然而,需要注意的是,大多数图像分类网络忽略空间相关性,而正如我们在下一节中看到的,空间相关性对于游戏代理非常重要。
是否使用池化
正如我们在第二章中讨论的,卷积和递归网络,ML-Agents 不使用任何池化操作,以避免数据中的空间关系丢失。然而,正如我们在自动驾驶车辆的例子中看到的那样,实际上在更高特征级别的提取(卷积层)上,加入一个或两个池化层是有帮助的。虽然我们的例子在一个更复杂的网络上进行了测试,但它有助于了解这对更复杂的 ML-Agents CNN 嵌入的应用。让我们尝试一下,通过完成以下练习,在上一个例子中添加一个池化层:
-
打开你选择的 Python 编辑器中的
models.py文件。Visual Studio 配合 Python 数据扩展是一个很好的平台,同时也提供了交互式调试代码的功能。 -
找到以下代码块,这是我们在上一个练习中留下的样子:
conv1 = tf.layers.conv2d(image_input, 16, kernel_size=[8, 8], strides=[4, 4], activation=tf.nn.elu, reuse=reuse, name="conv_1")
conv2 = tf.layers.conv2d(conv1, 32, kernel_size=[4, 4], strides=[2, 2], activation=tf.nn.elu, reuse=reuse, name="conv_2")
conv3 = tf.layers.conv2d(image_input, 64, kernel_size=[2, 2], strides=[2, 2], activation=tf.nn.elu, reuse=reuse, name="conv_3")
hidden = c_layers.flatten(conv3)
- 现在,我们将通过修改代码块来注入一个池化层,代码如下:
conv1 = tf.layers.conv2d(image_input, 16, kernel_size=[8, 8], strides=[4, 4], activation=tf.nn.elu, reuse=reuse, name="conv_1")
#################### ADD POOLING
conv2 = tf.layers.conv2d(conv1, 32, kernel_size=[4, 4], strides=[2, 2], activation=tf.nn.elu, reuse=reuse, name="conv_2")
conv3 = tf.layers.conv2d(image_input, 64, kernel_size=[2, 2], strides=[2, 2], activation=tf.nn.elu, reuse=reuse, name="conv_3")
hidden = c_layers.flatten(conv3)
-
现在,这将设置我们的前一个示例,使用单层池化。你可以将其视为提取所有上层特征,比如天空、墙壁或地板,并将结果池化在一起。仔细想想,代理需要知道多少空间信息才能区分一个天空区域和另一个天空区域?代理实际上只需要知道天空总是在上面。
-
打开你的命令行窗口或 Anaconda 窗口,通过运行以下代码来训练示例:
mlagents-learn config/trainer_config.yaml --run-id=vh_conv_wpool1 --train
- 和往常一样,观察代理的表现,注意代理在训练过程中是如何移动的。观察训练直到完成,或者观察你之前看到的其他人的训练过程。
现在,根据你的机器或环境,你可能已经注意到训练时间有了显著的改进,但实际表现略有下降。这意味着每次训练迭代执行得更快了,可能快了两到三倍甚至更多,但代理需要更多的交互。在这种情况下,代理训练的时间会更短,但在其他环境中,高级别的池化可能会更具破坏性。最终,这取决于你环境中的视觉效果、你希望代理表现得有多好,以及你个人的耐心。
在接下来的部分,我们将探讨状态的另一个特征——记忆,或者序列。我们将了解如何使用递归网络来捕捉记住序列或事件系列的重要性。
记忆序列的递归网络
本章中我们运行的示例环境默认使用一种递归记忆形式来记住过去的事件序列。这种递归记忆由长短期记忆(LSTM)层构成,允许代理记住可能有助于未来奖励的有益序列。请记住,我们在第二章中深入讲解了 LSTM 网络,卷积与递归网络。例如,代理可能反复看到相同的帧序列,可能是朝着目标移动,然后将这一状态序列与增加的奖励关联起来。以下是摘自Khan Aduil 等人的论文Training an Agent for FPS Doom Game using Visual Reinforcement Learning and VizDoom中的图示,展示了这种网络的原始形式:

DQRN 架构
作者将该网络架构称为 DQRN,代表深度 Q 递归网络。可能有点奇怪的是,他们没有称之为 DQCRN,因为图示清楚地显示了卷积的加入。虽然 ML-Agents 的实现略有不同,但概念基本相同。无论如何,添加 LSTM 层对代理训练有很大帮助,但在这一阶段,我们还没有看到不使用 LSTM 的训练效果。
因此,在接下来的练习中,我们将学习如何禁用递归网络,并查看这对训练的影响:
-
打开标准的走廊示例场景,即没有视觉学习的那个,位置在
Assets/ML-Agents/Examples/Hallway/Scenes文件夹中。 -
打开命令行窗口或 Anaconda 窗口,并确保你的 ML-Agent 虚拟 Python 环境已激活。
-
找到并打开位于
ML-Agents/ml-agents/config文件夹中的trainer_config.xml文件,使用你喜欢的文本或 XML 编辑器。 -
找到如下所示的配置块:
HallwayLearning:
use_recurrent: true
sequence_length: 64
num_layers: 2
hidden_units: 128
memory_size: 256
beta: 1.0e-2
gamma: 0.99
num_epoch: 3
buffer_size: 1024
batch_size: 128
max_steps: 5.0e5
summary_freq: 1000
time_horizon: 64
-
名为
HallwayLearning的配置块与我们在场景中的 Academy 中设置的大脑名称相匹配。如果你需要确认这一点,可以继续检查。 -
我们通常将所有这些配置参数称为超参数,它们对训练有很大的影响,尤其是在设置不正确时。如果你滚动到文件的顶部,你会注意到一组默认参数,接着是每个命名大脑的例外设置。每个大脑的参数部分将覆盖默认设置。
-
通过如下修改代码来禁用
use_recurrent网络:
HallwayLearning:
use_recurrent: false
-
将
use_recurrent设置为false可以禁用递归编码的使用。现在我们可以看到这对训练的影响。 -
保存配置文件。
-
按照平常的方式运行学习示例。现在你应该已经能够轻松地运行一个训练示例了。
-
和往常一样,观察代理的表现,并确保关注代理的动作。
如你所见,在这个示例中,代理的表现显著较差,显然使用循环网络来捕捉重要动作序列起到了很大的作用。事实上,在大多数重复性游戏环境中,比如 Hallway 和 VisualHallway,增加循环状态非常有效。然而,也有一些环境可能不会受益,或者实际上会因使用状态序列而受到影响。那些需要广泛探索或包含新内容的环境,可能会受到影响。由于代理可能更倾向于使用较短的动作序列,这会受到为代理配置的内存量的限制。在开发新环境时,记得考虑这一点。
现在我们已经有了一个没有循环或 LSTM 层时样本运行的比较,我们可以通过在下一节调整一些相关的循环超参数,重新测试样本。
调整循环超参数
正如我们在讨论循环网络时了解到的,LSTM 层可能接收可变输入,但我们仍然需要定义希望网络记住的最大序列长度。使用循环网络时,我们需要调整两个关键的超参数。以下是这些参数的描述(截至本文撰写时,按 ML-Agents 文档中的列表):
-
sequence_length:C对应于在训练过程中传递通过网络的经验序列的长度。这个长度应该足够长,以捕捉代理可能需要记住的任何信息。例如,如果你的代理需要记住物体的速度,那么这个值可以是一个小数值。如果你的代理需要记住一条在回合开始时只给定一次的信息,那么这个值应该更大:- 典型范围:4 – 128
-
memory_size:对应于用于存储循环神经网络隐藏状态的浮点数数组的大小。该值必须是四的倍数,并且应根据你预期代理需要记住的任务信息量进行缩放:- 典型范围:64 – 512
循环的sequence_length和memory_size超参数的描述直接来自 Unity ML-Agents 文档。
如果我们查看trainer_config.yaml文件中的 VisualHallway 示例配置,可以看到这些参数定义如下:
VisualHallwayLearning:
use_recurrent: true
sequence_length: 64
num_layers: 1
hidden_units: 128
memory_size: 256
beta: 1.0e-2
gamma: 0.99
num_epoch: 3
buffer_size: 1024
batch_size: 64
max_steps: 5.0e5
summary_freq: 1000
time_horizon: 64
这实际上意味着我们的代理将使用 256 的内存大小记住 64 帧或输入状态。文档并未明确说明单个输入占用多少内存,因此我们只能假设默认的视觉卷积编码网络——原始的两层模型——每帧需要四个内存单元。我们可以假设,通过增加我们之前示例中的卷积编码,代理可能无法记住每一帧状态。因此,让我们修改 VisualHallway 示例中的配置,以适应这种内存增加,并查看它在以下练习中的效果:
-
打开 VisualHallway 示例,回到我们上次在之前的练习中离开的地方,无论是否启用池化。只要记住你是否启用了池化,因为这将影响所需的内存。
-
打开位于
ML-Agents/ml-agents/config文件夹中的trainer_config.yaml文件。 -
修改
VisualHallwayLearning配置部分,如下所示:
VisualHallwayLearning:
use_recurrent: true
sequence_length: 128
num_layers: 1
hidden_units: 128
memory_size: 2048 without pooling, 1024 with pooling
beta: 1.0e-2
gamma: 0.99
num_epoch: 3
buffer_size: 1024
batch_size: 64
max_steps: 5.0e5
summary_freq: 1000
time_horizon: 64
-
我们将代理的记忆从 64 个序列增加到 128 个序列,从而使其记忆翻倍。接着,当不使用池化时,我们将记忆增加到 2,048,而使用池化时为 1,024。记住,池化会收集特征并减少每次卷积步骤中生成的特征图数量。
-
编辑完成后,保存文件。
-
打开你的命令行或 Anaconda 窗口,使用以下命令开始训练:
mlagents-learn config/trainer_config.yaml --run-id=vh_recurrent --train
-
当提示时,通过按下播放按钮开始编辑器中的训练会话,并观看操作的展开。
-
等待代理训练完成,像我们之前运行的其他示例一样。你应该能注意到训练性能的再次提高,以及代理选择的动作,应该显得更加协调。
如我们所见,轻微调整超参数使得我们能够改善代理的性能。理解在训练中使用的众多参数的作用,对于你成功构建出色的代理至关重要。在下一部分,我们将介绍一些进一步的练习,帮助你提高理解和技能。
练习
一如既往,尽量独立完成两到三个练习,为了你自己的利益。虽然这是一本实践书籍,但花一些额外时间将你的知识应用于新问题总是有益的。
独立完成以下练习:
-
浏览并探索 VisualPushBlock 示例。这个示例与走廊示例非常相似,是一个不错的类比,可以进行尝试。
-
修改走廊示例中的 HallwayAgent 脚本,以使用更多的扫描角度,从而获得更多的向量观察。
-
修改走廊示例,使用组合的传感器扫描和视觉观察输入。这将要求你通过添加相机来修改学习大脑配置,并可能需要更新一些超参数。
-
修改其他视觉观测环境,以使用某种形式的向量观测。一个不错的例子是尝试在 VisualPushBlock 示例中应用此功能。
-
修改视觉观测摄像头的空间,使其比 84 x 84 像素更大或更小,并选择是否使用灰度化。这是测试更复杂或更简单的 CNN 网络架构时一个很好的练习。
-
修改
create_visual_observation_encoder卷积编码函数,使其能够使用不同的 CNN 架构。这些架构可以根据你的需求是简单还是复杂。 -
修改
create_visual_observation_encoder卷积编码函数,以使用不同级别和数量的池化层。尝试在每个卷积层后使用池化,探索其对训练的影响。 -
在其他一些示例环境中禁用并重新启用递归网络,探索其对结果的影响。
-
在启用递归网络的情况下,调整
sequence_length和memory_size参数,观察不同序列长度对智能体表现的影响。如果增加sequence_length,请务必相应地增加memory_size参数。 -
考虑为智能体添加额外的向量或视觉观测。毕竟,智能体不一定只能有单一形式的感官输入。智能体可以始终检测其所处的方向,或者可能有其他感官输入方式,例如能够听到声音。我们将在后续章节中为智能体提供听觉能力,但也可以尝试自己实现这一功能。
记住,这些练习是为你的利益和享受而提供的,因此确保至少尝试几个。
总结
在这一章中,我们详细探讨了 ML-Agents 中的智能体如何感知环境并处理输入。智能体对环境的感知完全由开发者控制,这通常是关于给予智能体多少或多少输入/状态的微妙平衡。在本章中,我们进行了许多示例,并从深入分析 Hallway 示例及智能体如何使用射线感知环境中的物体开始。接着,我们研究了智能体如何使用视觉观测作为输入或状态,类似于我们人类,从中学习。随后,我们探讨了 ML-Agents 使用的卷积神经网络(CNN)架构,该架构用于编码提供给智能体的视觉观测。我们学习了如何通过添加或删除卷积层或池化层来修改这一架构。最后,我们研究了记忆的作用,或者说如何通过输入状态的递归序列化来帮助智能体训练。递归网络使得智能体能够为提供奖励的动作序列增加更多的价值。
在下一章,我们将更详细地探讨强化学习(RL)以及智能体如何使用 PPO 算法。我们将在过程中深入学习 RL 的基础知识,并了解在训练中使用的许多超参数的重要性。
第八章:理解 PPO
我们避免深入探讨 近端策略优化(PPO)算法的更高级的内部工作原理,甚至避免讨论策略与模型的对比。如果你记得的话,PPO 是最早在 OpenAI 开发的 简化级别(RL)方法,支撑着 ML-Agents,并且是一种基于策略的算法。在本章中,我们将探讨基于策略和基于模型的强化学习算法之间的差异,以及 Unity 实现的更高级的内部工作原理。
以下是本章我们将覆盖的主要主题:
-
马拉松强化学习
-
部分可观察马尔可夫决策过程
-
Actor-Critic 和连续动作空间
-
理解 TRPO 和 PPO
-
使用超参数调优 PPO
本章内容属于高级内容,假设你已经学习过前几章和相关练习。为了本章的目的,我们还假设你能够在 Unity 中使用 ML-Agents 顺利打开并运行学习环境。
马拉松强化学习(RL)
到目前为止,我们的关注点一直在离散动作和情景化环境上,其中智能体通常学习解决谜题或完成某些任务。最典型的环境包括 GridWorld,以及当然的 Hallway/VisualHallway 示例,智能体在这些环境中离散地选择诸如上、左、下或右等动作,并且利用这些动作必须导航到某个目标。虽然这些是很好的环境,用来练习并学习强化学习的基本概念,但它们也可能是比较枯燥的学习环境,因为结果往往不是自动产生的,需要大量的探索。然而,在马拉松强化学习环境中,智能体始终通过控制反馈的奖励不断学习。事实上,这种形式的强化学习类似于机器人控制系统和仿真系统。由于这些环境充满了反馈奖励,因此当我们调整/调整超参数时,它们为我们提供了更好的即时反馈,这使得这些类型的环境非常适合我们的学习目的。
Unity 提供了几个马拉松强化学习(RL)环境示例,在撰写本文时,包含了 Crawler、Reacher、Walker 和 Humanoid 示例环境,但这些环境在未来可能会有所更改。
马拉松环境的构建方式不同,我们应该在深入之前了解这些差异。打开 Unity 编辑器和你选择的 Python 命令窗口,设置以运行 mlagents-learn,并完成以下练习:
-
打开
Assets/ML-Agents/Examples/Crawler/Scenes文件夹中的CrawlerDynamicTarget示例场景。这个示例展示了一个具有四个可移动肢体的智能体,每个肢体都有两个可以移动的关节。目标是让智能体朝着一个不断变化的动态目标移动。 -
在层级窗口中选择 DynamicPlatform | Crawler 对象,并注意爬行者代理组件和 CrawlerDynamicLearning 脑部,如下所示。
截图:

检查爬行者代理和脑部
-
请注意,脑部的空间大小为 129 个向量观察和 20 个连续动作。一个连续动作返回一个值,确定关节可能旋转的程度,从而让智能体学习如何将这些关节动作协调成能够让它爬行到目标的动作。
-
点击爬行者代理组件旁边的目标图标,在上下文菜单中选择编辑脚本。
-
打开脚本后,向下滚动并寻找
CollectObservations方法:
public override void CollectObservations()
{
jdController.GetCurrentJointForces();
AddVectorObs(dirToTarget.normalized);
AddVectorObs(body.transform.position.y);
AddVectorObs(body.forward);
AddVectorObs(body.up);
foreach (var bodyPart in jdController.bodyPartsDict.Values)
{
CollectObservationBodyPart(bodyPart);
}
}
-
再次提醒,代码是用 C#编写的,但智能体感知的输入应该是相当直观的。我们首先看到智能体接收目标方向、向上和向前的方向,以及每个身体部位的观察作为输入。
-
在场景中选择Academy,并确保Brain配置设置为Control(学习模式)。
-
从你之前准备好的命令窗口或 Anaconda 窗口,按如下方式运行
mlagents-learn脚本:
mlagents-learn config/trainer_config.yaml --run-id=crawler --train
- 训练开始后不久,你会看到智能体立即取得可度量的进展。
这个智能体能够非常快速地进行训练,并将在接下来的章节中极大地帮助我们测试对强化学习工作原理的理解。可以自由浏览和探索这个示例,但避免调整任何参数,因为我们将在下一部分开始做这件事。
部分可观察马尔可夫决策过程
在第五章《引入 DRL》中,我们了解到马尔可夫决策过程(MDP)用于定义智能体计算动作/价值所用的状态/模型。在 Q 学习的情况下,我们已经看到如何通过表格或网格来存储一个完整的 MDP,用于像 Frozen Pond 或 GridWorld 这样的环境。这些类型的强化学习是基于模型的,意味着它们完全建模环境中的每一个状态——例如,网格游戏中的每一个方格。然而,在大多数复杂的游戏和环境中,能够映射物理或视觉状态会变成一个部分可观察问题,或者我们可能称之为部分可观察马尔可夫决策过程(POMDP)。
POMDP 定义了一种过程,其中智能体永远无法完全看到其环境,而是学会基于派生的通用策略进行行动。这个过程在爬行者示例中得到了很好的展示,因为我们可以看到智能体只通过有限的信息——目标方向——来学习如何移动。下表概述了我们通常用于强化学习的马尔可夫模型定义:
| 否 | 是 | ||
|---|---|---|---|
| 所有状态可观察? | 否 | 马尔可夫链 | MDP |
| 是 | 隐马尔可夫模型 | POMDP |
由于我们通过动作控制智能体的状态,因此我们研究的马尔可夫模型包括 MDP 和 POMDP。同样,这些过程也常常被称为开模型或关模型,如果一个强化学习算法完全了解状态,我们称之为基于模型的过程。相反,POMDP 则指的是关模型过程,或者我们所说的基于策略的方法。基于策略的算法提供了更好的泛化能力,并且能够在具有未知或无限可观察状态的环境中进行学习。部分可观察状态的示例包括走廊环境、视觉走廊环境,以及当然还有爬行者。
马尔可夫模型为机器学习的许多方面提供了基础,你可能会在更先进的深度学习方法中遇到它们的应用,这些方法被称为深度概率编程。深度 PPL,正如它所称,是变分推断和深度学习方法的结合。
无模型方法通常使用经验缓冲区来存储一组经验,这些经验将在以后用于学习通用策略。这个缓冲区由一些超参数定义,称为time_horizon、batch_size和buffer_size。以下是从 ML-Agents 文档中提取的这些参数的定义:
-
time_horizon:这对应于每个智能体在将经验添加到经验缓冲区之前收集的步数。当在一个回合结束之前达到了此限制时,将使用一个值估计来预测智能体当前状态的整体预期奖励。因此,该参数在较长时间跨度(较少偏差但较高方差的估计)和较短时间跨度(较大偏差但较少变化的估计)之间进行权衡。在回合内存在频繁奖励,或者回合过大时,较小的数字可能更为理想。这个数字应该足够大,以捕捉智能体动作序列中的所有重要行为:- 典型范围:32 – 2,048
-
buffer_size:这对应于在我们更新模型或进行任何学习之前,需要收集的经验数量(智能体的观察、动作和奖励)。它应该是batch_size的倍数。通常,较大的buffer_size参数对应于更稳定的训练更新。- 典型范围:2,048 – 409,600
-
batch_size:这是用于一次梯度下降更新的经验数量。它应该始终是buffer_size参数的一部分。如果你使用的是连续动作空间,那么这个值应该很大(通常在千级别)。如果你使用的是离散动作空间,那么这个值应该较小(通常在十级别)。-
典型范围(连续型):512 – 5,120
-
典型范围(离散型):32 – 512
-
我们可以通过查看CrawlerDynamicLearning大脑配置来了解这些值是如何设置的,并通过修改它来观察它对训练的影响。打开编辑器并在正确配置的 Python 窗口中进入CrawlerDynamicTarget场景,按照以下步骤操作:
-
打开位于
ML-Agents/ml-agents/config文件夹中的trainer_config.yaml文件。 -
向下滚动到
CrawlerDynamicLearning大脑配置部分:
CrawlerDynamicLearning:
normalize: true
num_epoch: 3
time_horizon: 1000
batch_size: 2024
buffer_size: 20240
gamma: 0.995
max_steps: 1e6
summary_freq: 3000
num_layers: 3
hidden_units: 512
-
注意突出显示的行,显示了
time_horizon、batch_size和buffer_size参数。如果你还记得我们早期的 Hallway/VisualHallway 示例,time_horizon参数仅为 32 或 64。由于这些示例使用了离散动作空间,我们可以为time_horizon设置一个较低的值。 -
翻倍所有参数值,如以下代码片段所示:
time_horizon: 2000
batch_size: 4048
buffer_size: 40480
-
本质上,我们在这里做的是将智能体用于构建其周围环境策略的经验量翻倍。实际上,我们是在给智能体提供更多的经验快照来进行训练。
-
按照之前的操作,运行智能体进行训练。
-
让智能体训练的时间与运行之前的基础示例相同。这将为你提供一个良好的训练性能对比。
一件很明显的事情是,智能体训练的稳定性提高了,这意味着智能体的平均奖励将更加稳定地增长,且波动更小。回想一下,我们希望避免训练中的跳跃、尖峰或波动,因为这些可能表示网络优化方法的收敛性差。这意味着渐进的变化通常更好,并且表明训练表现良好。通过将time_horizon及相关参数翻倍,我们增加了智能体用于学习的经验量。反过来,这有助于稳定训练,但你可能会注意到,智能体需要更长的时间才能完成相同次数的迭代训练。
部分可观察的强化学习算法被归类为基于策略、无模型或离模型的算法,是 PPO 的基础。在接下来的章节中,我们将探讨强化学习中的改进,重点是更好地管理连续动作空间带来的额外复杂性。
Actor-Critic 和连续动作空间
我们在查看马拉松强化学习或控制学习时引入的另一个复杂性是连续动作空间的引入。连续动作空间表示智能体可以采取的无限可能动作的集合。在之前,我们的智能体可能会选择一个离散动作,比如是或否,现在它必须从一个无限的动作空间中为每个关节选择一个点作为动作。将无限动作空间映射到一个具体动作并不容易解决——然而,我们有神经网络可供使用,这为我们提供了一个非常好的解决方案,采用的架构与我们在第三章中看到的生成对抗网络(GAN)类似。
正如我们在生成对抗网络(GAN)章节中发现的那样,我们可以提出一种由两个竞争网络组成的网络架构。这些竞争网络将迫使每个网络通过相互竞争,寻找最佳解决方案,将一个随机空间映射到一个可信的伪造物。类似的概念也可以应用于这种情况,这被称为演员-评论家模型。该模型的示意图如下:

演员-评论家架构
这里发生的事情是,演员根据给定的状态从策略中选择一个动作。然后状态首先通过一个评论家,评论家根据当前状态评估最佳的动作,并给出一定的误差。更简单地说,评论家根据当前状态批评每一个动作,然后演员根据状态选择最佳动作。
这种动作选择方法最早在一种叫做对抗双 Q 网络(DDQN)的算法中进行了探索。现在它已成为大多数高级强化学习算法的基础。
演员-评论家模型本质上是为了解决连续动作空间问题,但鉴于其性能,这种方法也已被融入到一些高级离散算法中。ML-Agents 使用演员-评论家模型处理连续空间,但不使用离散动作空间的演员-评论家模型。
使用演员-评论家方法需要,或者在我们的网络中最有效的是,额外的层和神经元,这是我们可以在 ML-Agents 中配置的内容。这些超参数的定义来自 ML-Agents 文档,具体如下:
-
num_layers:这对应于在观测输入之后,或者在视觉观测的 CNN 编码之后存在的隐藏层数量。对于简单的问题,较少的层可能会训练得更快、更高效。对于更复杂的控制问题,可能需要更多的层:- 典型范围:1 – 3
-
hidden_units:这些对应于神经网络中每个全连接层的单元数。对于那些正确动作是观测输入的简单组合的问题,这个值应该较小。对于那些动作是观测变量间复杂交互的问题,这个值应该更大:- 典型范围:32 – 512
让我们打开一个新的 ML-Agents 马拉松或控制示例,看看修改这些参数对训练的影响。按照这个练习来理解向控制问题中添加层和神经元(单元)的效果:
-
打开
Assets/ML-Agents/Examples/Walker/Scenes文件夹中的 Walker 场景。这个示例展示了一个行走的类人动画。 -
在层次窗口中找到并选择 WalkerAgent 对象,然后查看检查器窗口并检查 Agent 和 Brain 设置,如下图所示:

WalkerAgent 和 WalkerLearning 属性
-
在层级窗口中选择
WalkerAcademy,并确保为Brains参数启用了 Control 选项。 -
打开位于
ML-Agents/ml-agents/config文件夹中的trainer_config.yaml文件,向下滚动至WalkerLearning部分,如下所示:
WalkerLearning:
normalize: true
num_epoch: 3
time_horizon: 1000
batch_size: 2048
buffer_size: 20480
gamma: 0.995
max_steps: 2e6
summary_freq: 3000
num_layers: 3
hidden_units: 512
-
注意这个示例使用了多少层和单位。是更多还是更少于我们为离散动作问题使用的数量?
-
保存所有内容并为训练设置样本。
-
从 Python 控制台启动训练会话,使用以下命令:
mlagents-learn config/trainer_config.yaml --run-id=walker --train
- 这个代理可能需要更长的训练时间,但请尝试等待大约 100,000 次迭代,以便更好地了解它的训练进度。
现在我们更好地理解了 Actor-Critic 及其在连续动作空间中的应用,我们可以继续探索改变网络大小对训练这些更复杂网络的影响,接下来会讲到这一部分。
扩展网络架构
Actor-Critic 架构增加了问题的复杂性,因此也增加了解决这些问题所需的网络的复杂性和规模。这与我们之前对 PilotNet 的分析没有太大区别,PilotNet 是 Nvidia 用于自驾的多层 CNN 架构。
我们想要看到的是,增加网络大小对复杂示例(如 Walker 示例)产生的即时效果。打开 Unity 中的Walker示例并完成以下练习:
-
打开通常存放的
trainer_config.yaml文件。 -
修改
WalkerLearning配置,如下所示的代码:
WalkerLearning:
normalize: true
num_epoch: 3
time_horizon: 1000
batch_size: 2048
buffer_size: 20480
gamma: 0.995
max_steps: 2e6
summary_freq: 3000
num_layers: 1
hidden_units: 128
- 设置
num_layers: 1和hidden_units: 128。这些是我们在离散动作空间问题中常用的典型值。你可以通过查看另一个离散样本,如VisualHallwayLearning配置,来确认这一点,具体如下:
VisualHallwayLearning:
use_recurrent: false
sequence_length: 64
num_layers: 1
hidden_units: 128
memory_size: 256
beta: 1.0e-2
gamma: 0.99
num_epoch: 3
buffer_size: 1024
batch_size: 64
max_steps: 5.0e5
summary_freq: 1000
time_horizon: 64
-
这个样本使用了我们刚才为连续动作问题设置的相同设置。
-
编辑完成后,保存所有内容并准备开始训练。
-
启动一个新的训练会话,并使用新的
run-id参数。记住,每次运行时都更改run-id参数,这样在 TensorBoard 中更容易区分每次运行。 -
和往常一样,让样本运行的时间与之前未经修改的运行时间相同,以便进行良好的比较。
你可能会立即注意到运行这个示例时,训练的稳定性非常好。第二个你可能会注意到的是,虽然训练稳定性增加了,但性能略微下降。请记住,较小的网络有更少的权重,通常会更稳定且训练速度更快。然而,在这个问题中,虽然网络的训练更稳定且速度更快,但你可能会注意到训练会遇到瓶颈。现在,受限于网络规模,智能体能够更快地优化较小的网络,但却没有以前看到的精细控制。事实上,这个智能体永远不会像第一次未修改的运行那样好,因为它现在受限于一个较小的网络。这是构建深度强化学习(DRL)智能体时需要平衡的另一个权衡点,尤其是在游戏和仿真中。
在下一部分,我们将进一步探讨我们所说的优势函数,或者像演员-评论家中使用的那些,并首先探索 TRPO,当然还有 PPO。
理解 TRPO 和 PPO
有许多变体的策略和无模型算法已经变得流行,用于解决强化学习(RL)问题,优化未来奖励的预测。正如我们所见,许多算法使用优势函数,例如演员-评论家(Actor-Critic),其中有两方试图收敛到最优解。在这种情况下,优势函数试图找到最大期望的折扣奖励。TRPO 和 PPO 通过使用一种叫做最小化最大化(MM)的优化方法来实现这一目标。下面的图表展示了 MM 算法如何解决这个问题:

使用 MM 算法
这个图表摘自 Jonathon Hui 的一系列博客,这些博客优雅地描述了 MM 算法,并且详细讲解了 TRPO 和 PPO 方法。 详细来源请见以下链接:medium.com/@jonathan_hui/rl-proximal-policy-optimization-ppo-explained-77f014ec3f12。
本质上,MM 算法通过交互式地最大化和最小化函数参数,直到达到收敛的解,从而找到最优配对函数。在图表中,红线表示我们希望逼近的函数,蓝线表示收敛的函数。你可以看到算法如何通过选择最小值/最大值来找到一个解的过程。
使用 MM 时我们遇到的问题是函数逼近有时会偏离,或者掉入一个谷底。为了更好地理解这个问题,我们可以将其视为使用直线爬升不平的山丘。以下是这种情况的示例:

尝试使用线性方法爬升山丘
你可以看到,仅仅使用线性路径来尝试穿越这条危险的山脊实际上是非常危险的。虽然危险可能不那么明显,但当使用线性方法来解决 MM 问题时,它仍然是一个大问题,就像你在陡峭的山脊上只使用直线固定路径进行徒步旅行一样。
TRPO 通过使用二次方法解决了使用线性方法的问题,并通过限制每次迭代可以采取的步骤数,形成一个信任区域。也就是说,算法确保每个位置都是正的且安全的。如果我们再次考虑我们的爬坡示例,我们可以将 TRPO 看作是设置一条路径或信任区域,如下图所示:

一条信任区域路径沿着山坡
在前面的照片中,路径仅作为示例展示,表示一组连接的圆圈或区域;真实的信任路径可能靠近实际的峰顶或山脊,也可能不靠近。不管怎样,这种方式的效果是使代理能够以更渐进和逐步的速度学习。通过 TRPO,信任区域的大小可以调整,使其变大或变小,以配合我们偏好的策略收敛。TRPO 的问题在于它相当复杂,因为它需要对一些复杂方程进行二次导数计算。
PPO 通过限制或裁剪两种策略之间的 Kulbach-Leibler(KL)散度来解决这个问题,KL 散度在每次迭代中进行测量。KL 散度衡量的是概率分布之间的差异,可以通过以下图示描述:

理解 KL 散度
在前面的图示中,p(x) 和 q(x) 各自代表不同的策略,其中 KL 散度被衡量。算法随后使用这种散度的度量来限制或裁剪每次迭代中可能发生的策略变化量。ML-Agents 使用两个超参数,允许你控制应用于目标或确定每次迭代中策略变化量的函数的裁剪量。以下是 Unity 文档中描述的 beta 和 epsilon 参数的定义:
-
Beta:这对应于熵正则化的强度,使策略变得更加随机。这确保了代理在训练过程中能正确地探索动作空间。增加这个值将确保采取更多的随机动作。应该调整此值,使得熵(可以从 TensorBoard 中衡量)随着奖励的增加而缓慢减少。如果熵下降得太快,请增加 beta。如果熵下降得太慢,请减少 beta:
-
典型范围:1e-4 – 1e-2
-
Epsilon:这对应于梯度下降更新过程中,旧策略和新策略之间可接受的散度阈值。将此值设置为较小将导致更稳定的更新,但也会减慢训练过程:
-
典型范围:0.1 – 0.3
需要记住的关键点是,这些参数控制了一个策略从一个迭代到下一个迭代的变化速度。如果你注意到一个智能体的训练表现有些不稳定,可能需要将这些参数调小。epsilon的默认值是.2,beta的默认值是1.0e-2,但是,当然,我们需要探索这些值如何影响训练,无论是正面还是负面的方式。在接下来的练习中,我们将修改这些策略变化参数,并观察它们在训练中的效果:
-
对于这个示例,我们将打开
Assets/ML-Agents/Examples/Crawler/Scenes文件夹中的CrawlerDynamic场景。 -
打开位于
ML-Agents/ml-agents/config文件夹中的trainer_config.yaml文件。由于我们已经评估了这个样本的表现,有几种方法可以将训练配置恢复,并对 beta 和 epsilon 参数进行一些修改。 -
向下滚动到
CrawlerDynamicLearning配置部分,并按照以下方式修改:
CrawlerDynamicLearning:
normalize: true
num_epoch: 3
time_horizon: 1000
batch_size: 1024
buffer_size: 20240
gamma: 0.995
max_steps: 1e6
summary_freq: 3000
num_layers: 3
hidden_units: 512
epsilon: .1
beta: .1
-
我们将
epsilon和beta参数修改为更高的值,这意味着训练将变得不那么稳定。然而,如果你还记得,这些马拉松示例通常会以更稳定的方式进行训练。 -
打开一个配置正确的 Python 控制台,并运行以下命令以启动训练:
mlagents-learn config/trainer_config.yaml --run-id=crawler_policy --train
- 和往常一样,等待若干训练回合,以便从一个示例到下一个示例进行良好的比较。
你可能会发现意外的情况,那就是智能体似乎开始回退,实际上它确实在回退。这是因为我们将那些信任区间设置得太大(一个大的beta),而虽然我们允许变化速率较低(epsilon为.1),但是我们可以看到beta值对训练更为敏感。
请记住,Unity ML-Agents 实现使用了多个交叉特征,这些特征一起构成了一个强大的 RL 框架。在接下来的部分,我们将简要回顾 Unity 最近添加的一个优化参数。
泛化优势估计
RL 领域正在经历爆炸式增长,得益于不断推进的研究,推动了可能性的边界。每一次小的进展都会带来额外的超参数和小的调整,可以用来稳定和/或改善训练性能。Unity 最近添加了一个名为 lambda 的新参数,其定义来自文档,具体如下:
-
lambda:这对应于计算广义优势估计(GAE)时使用的 lambda 参数
arxiv.org/abs/1506.02438。可以将其看作代理在计算更新后的价值估计时,依赖当前价值估计的程度。较低的值对应更多地依赖当前价值估计(这可能导致较高的偏差),较高的值则对应更多地依赖环境中实际收到的奖励(这可能导致较高的方差)。该参数提供了两者之间的折中,正确的值可以带来更稳定的训练过程:- 典型范围:0.9 – 0.95
GAE 论文描述了一个名为 lambda 的函数参数,可以用于调整奖励估计函数,并且最适用于控制或马拉松型强化学习任务。我们不会深入探讨细节,感兴趣的读者应该下载论文并自行查阅。然而,我们将探索改变此参数如何影响控制样本,如接下来的Walker场景练习:
-
打开 Unity 编辑器,加载
Walker示例场景。 -
在层级结构中选择 Academy 对象,并确认场景仍然设置为训练/学习模式。如果是,你无需做其他操作。如果场景未设置为学习模式,你知道该怎么做。
-
打开
trainer_config.yaml文件,并按如下方式修改WalkerLearning:
WalkerLearning:
normalize: true
num_epoch: 3
time_horizon: 1000
batch_size: 2048
buffer_size: 20480
gamma: 0.995
max_steps: 2e6
summary_freq: 3000
num_layers: 3
hidden_units: 512
lambd: .99
-
注意我们如何设置
lambda参数,并确保num_layers和hidden_units被重置为原始值。论文中,作者描述了最优值在.95到.99之间,但这与 Unity 文档中的描述有所不同。 -
编辑完成后保存文件。
-
打开一个用于训练的 Python 控制台设置,并使用以下命令运行:
mlagents-learn config/trainer_config.yaml --run-id=walker_lambd --train
- 确保让样本运行的时间与之前一样,以便进行良好的比较。
经过大量训练后,你会注意到在这个示例中,代理的训练速度确实慢了大约 25%。这一结果告诉我们,通过增加 lambda,我们在要求代理更多地重视奖励。现在,这可能看起来有些反直觉,但在这个样本或这种类型的环境中,代理会收到持续的小的正向奖励。这导致每个奖励出现偏差,正如我们所见,这会扭曲训练并妨碍代理的进展。对于感兴趣的读者来说,尝试在 Hallway 环境中调整 lambda 参数可能会是一个有趣的练习,在该环境中代理只会收到一个正向的单次奖励。
强化学习优势函数有许多形式,并且它们的作用是解决许多与离线模型或策略驱动算法(如 PPO)相关的问题。在下一节中,我们将通过修改并创建一个新的样本控制/马拉松学习环境来结束本章内容。
学习调整 PPO
在这一部分,我们将学习如何调整一个修改过的/全新的控制学习环境。这将帮助我们深入了解 Unity 示例的内部工作原理,并向你展示如何稍后修改或创建新的示例。让我们首先打开 Unity 编辑器,开始进行以下练习:
-
打开
Reacher场景,将其设置为学习模式,并进行训练。你现在应该能轻松完成这一部分。让代理训练足够长的时间,以便像往常一样建立基准。 -
从菜单中选择
Assets/Import Package/Custom Package。从下载的源代码中的Chapter08文件夹找到Chapter_8_Assets.unitypackage。 -
打开
Assets/HoDLG/Scenes文件夹中的Reacher_3_joint场景。这是一个修改过的场景,我们将一起学习其构建过程。 -
首先,注意只有一个 Reacher 臂部是活动的,但现在有三个关节,如下图所示:

检查 Agent 游戏对象
-
注意现在这个手臂有三个部分,其中新的部分被称为 Capsule(2),并标识为 Pendulum C。关节的顺序现在是错乱的,这意味着 Pendulum C 实际上是中间的摆,而不是底部的摆。
-
选择每个 Capsule 对象,检查它们的配置和位置,如下图所示:

检查 Capsule 对象
-
请务必注意每个 Capsule 的 Configurable Joint | Connected Body 对象。此属性设置对象将连接或铰接的物体。Configurable Joint 组件上还有许多其他属性,可以让你模拟这种关节交互的任何形式,甚至是生物学上的。例如,你可能希望使这个手臂的关节更具人类特征,只允许某些角度的运动。同样,如果你设计的是一个动作受限的机器人,那么也可以通过这个关节组件来模拟。
-
在此阶段,我们可以设置并运行示例。打开并设置一个 Python 控制台或 Anaconda 窗口进行训练。
-
运行示例进行训练并观察代理的进展。让代理运行足够多的迭代,以便将训练性能与基准进行比较。
在这个阶段,我们已经启动了示例,并准备开始调整新的参数以优化训练。然而,在这之前,我们将回顾一下为了使上一个示例可行所需的 C# 代码更改。下一部分将介绍 C# 代码更改,对于不感兴趣代码的开发者来说是可选的。如果你打算在 Unity 中构建自己的控制或马拉松环境,你将需要阅读下一部分。
控制项目所需的编码更改
正如我们之前提到的,这一部分是可选的,适用于那些对使用 Unity C#构建自己控制样本的细节感兴趣的人。未来可能不再需要任何编码更改来修改这些类型的样本,这也是该部分是可选的另一个原因。
完成以下练习,了解在 Reacher 控制示例中添加关节所需的编码更改:
-
在层级窗口中选择 Agent 对象,然后在检查器窗口中注意到 Reacher Agent_3 组件。这是我们将要检查的修改后的脚本。
-
点击 Reach Agent_3 组件旁边的目标图标,从上下文菜单中选择编辑脚本。
-
这将会在你选择的 C#代码编辑器中打开
ReacherAgent_3.cs脚本。 -
在声明部分需要注意的第一件事是新增变量的添加,以下是以粗体显示的内容:
public GameObject pendulumA;
public GameObject pendulumB;
public GameObject pendulumC;
public GameObject hand;
public GameObject goal;
private ReacherAcademy myAcademy;
float goalDegree;
private Rigidbody rbA;
private Rigidbody rbB;
private Rigidbody rbC;
private float goalSpeed;
private float goalSize;
-
添加了两个新变量,
pendulumC和rbC,用于保存新的关节 GameObject 和 RigidBody。现在,Unity 物理中的Rigidbody表示可以被物理引擎移动或操作的物体。Unity 正在对其物理引擎进行升级,这将改变这里的一些教学内容。当前版本的 ML-Agents 使用的是旧的物理系统,因此这个示例也将使用旧系统。
-
接下来需要注意的重要事项是添加了额外的代理观察项,具体请参见以下
CollectObservations方法:
public override void CollectObservations()
{
AddVectorObs(pendulumA.transform.localPosition);
AddVectorObs(pendulumA.transform.rotation);
AddVectorObs(rbA.angularVelocity);
AddVectorObs(rbA.velocity);
AddVectorObs(pendulumB.transform.localPosition);
AddVectorObs(pendulumB.transform.rotation);
AddVectorObs(rbB.angularVelocity);
AddVectorObs(rbB.velocity);
AddVectorObs(pendulumC.transform.localPosition);
AddVectorObs(pendulumC.transform.rotation);
AddVectorObs(rbC.angularVelocity);
AddVectorObs(rbC.velocity);
AddVectorObs(goal.transform.localPosition);
AddVectorObs(hand.transform.localPosition);
AddVectorObs(goalSpeed);
}
- 粗体部分添加了新的观察项
pendulumC和rbC,它们总共增加了 13 个向量。回顾一下,这意味着我们还需要将大脑的观察数从 33 个向量切换到 46 个向量,具体如下图所示:

检查更新的 ReacherLearning_3 大脑
- 接下来,我们将查看
AgentAction方法;这是 Python 训练器代码调用代理并告诉它进行什么动作的地方,代码如下:
public override void AgentAction(float[] vectorAction, string textAction)
{
goalDegree += goalSpeed;
UpdateGoalPosition();
var torqueX = Mathf.Clamp(vectorAction[0], -1f, 1f) * 150f;
var torqueZ = Mathf.Clamp(vectorAction[1], -1f, 1f) * 150f;
rbA.AddTorque(new Vector3(torqueX, 0f, torqueZ));
torqueX = Mathf.Clamp(vectorAction[2], -1f, 1f) * 150f;
torqueZ = Mathf.Clamp(vectorAction[3], -1f, 1f) * 150f;
rbB.AddTorque(new Vector3(torqueX, 0f, torqueZ));
torqueX = Mathf.Clamp(vectorAction[3], -1f, 1f) * 150f;
torqueZ = Mathf.Clamp(vectorAction[4], -1f, 1f) * 150f;
rbC.AddTorque(new Vector3(torqueX, 0f, torqueZ));
}
-
在这种方法中,我们扩展了代码,允许代理以
rigidbody rbC的形式移动新的关节。你注意到新的学习大脑也添加了更多的动作空间吗? -
最后,我们查看
AgentReset方法,看看代理如何在新的肢体加入后重置自己,代码如下:
public override void AgentReset()
{
pendulumA.transform.position = new Vector3(0f, -4f, 0f) + transform.position;
pendulumA.transform.rotation = Quaternion.Euler(180f, 0f, 0f);
rbA.velocity = Vector3.zero;
rbA.angularVelocity = Vector3.zero;
pendulumB.transform.position = new Vector3(0f, -10f, 0f) + transform.position;
pendulumB.transform.rotation = Quaternion.Euler(180f, 0f, 0f);
rbB.velocity = Vector3.zero;
rbB.angularVelocity = Vector3.zero;
pendulumC.transform.position = new Vector3(0f, -6f, 0f) + transform.position;
pendulumC.transform.rotation = Quaternion.Euler(180f, 0f, 0f);
rbC.velocity = Vector3.zero;
rbC.angularVelocity = Vector3.zero;
goalDegree = Random.Range(0, 360);
UpdateGoalPosition();
goalSize = myAcademy.goalSize;
goalSpeed = Random.Range(-1f, 1f) * myAcademy.goalSpeed;
goal.transform.localScale = new Vector3(goalSize, goalSize, goalSize);
}
- 这段代码的作用仅仅是将手臂的位置重置为初始位置并停止所有运动。
这涵盖了本示例所需的唯一代码更改。幸运的是,只有一个脚本需要修改。未来可能完全不需要再修改这些脚本了。在下一部分,我们将通过调整额外参数并引入另一种训练优化方法来进一步完善样本训练。
多重代理策略
在这一部分,我们将探讨如何通过引入多个智能体来训练相同的策略,从而改进基于策略或非模型方法(如 PPO)。你将在这一部分使用的示例练习完全由你决定,应该是你熟悉的和/或感兴趣的内容。为了我们的目的,我们将探索一个我们已 extensively 研究过的示例——Hallway/VisualHallway。如果你已经跟随本书的大多数练习,你应该完全有能力适应这个示例。不过,请注意,在本次练习中,我们希望使用一个已设置为使用多个智能体进行训练的示例。
之前,我们避免讨论多个智能体;我们避免探讨这个训练方面是因为它可能会让关于模型内与模型外方法的讨论更加复杂。现在,你已经理解了使用基于策略的方法的差异和原因,你可以更好地理解,由于我们的智能体使用的是基于策略的方法,我们可以同时训练多个智能体针对同一个策略。然而,这可能会对其他训练参数和配置产生影响,正如你可能已经想象的那样。
打开 Unity 编辑器,进入Hallway/VisualHallway示例场景,或者你选择的其他场景,并完成以下练习:
-
打开一个 Python 或 Anaconda 控制台窗口,并准备开始训练。
-
选择并启用 HallwayArea,选择区域(1)到(19),使它们变为激活状态并可在场景中查看。
-
选择每个HallwayArea中的 Agent 对象,并确保Hallway Agent | Brain设置为 HallwayLearning,而不是 HallwayPlayer。这将启用所有额外的训练区域。
-
根据你之前的经验,你可能会选择是否将示例修改回原始状态。回想一下,在早期的练习中,我们修改了 HallwayAgent 脚本,使它只扫描一个较小的角度范围。这可能还需要你调整脑参数。
-
设置完场景后,保存它和项目。
-
使用唯一的
run-id运行场景并等待多个训练迭代。这个示例的训练速度可能会明显变慢,甚至加快,这取决于你的硬件配置。
现在我们已经为 Hallway 环境建立了一个新的基准线,我们可以确定修改一些超参数对离散动作样本的影响。我们将重新审视的两个参数是num_epochs(训练轮数)和batch_size(每个训练轮的经验数量),这些我们在之前的连续动作(控制)样本中也看过。在文档中,我们提到,在训练控制智能体时,更大的批处理大小更为理想。
在我们继续之前,打开trainer_config.yaml文件并检查如下的 HallwayLearning 配置部分:
HallwayLearning:
use_recurrent: true
sequence_length: 64
num_layers: 2
hidden_units: 128
memory_size: 256
beta: 1.0e-2
gamma: 0.99
num_epoch: 3
buffer_size: 1024
batch_size: 128
max_steps: 5.0e5
summary_freq: 1000
time_horizon: 64
在 Unity 文档中,专门提到只有在增加批量大小时才增加 epoch 数量,这是为了考虑额外的训练经验。我们了解到,控制示例通常从更大的批量大小中受益,因此需要更大的 epoch 数量。然而,我们还想确定的最后一件事是,在多个代理共同学习同一策略的离散动作示例中,修改batch_size和num_epoch参数的效果。
出于这个练习的目的,我们将只修改batch_size和num_epoch为如下值:
- 更新你正在使用的
HallwayLearning或大脑配置,使用以下参数:
HallwayLearning:
use_recurrent: true
sequence_length: 64
num_layers: 2
hidden_units: 128
memory_size: 256
beta: 1.0e-2
gamma: 0.99
num_epoch: 10
buffer_size: 1024
batch_size: 1000
max_steps: 5.0e5
summary_freq: 1000
time_horizon: 64
-
我们将
num_epoch设置为 10,batch_size设置为 1000。这些设置对于控制样本来说是典型的,正如我们之前看到的那样,但现在我们想要查看在多个代理训练相同策略的离散动作示例中的效果。 -
为训练准备样本,并准备好 Python 控制台并打开。
-
使用以下命令运行训练会话:
mlagents-learn config/trainer_config.yaml --run-id=hallway_e10b1000 --train
- 注意我们是如何使用帮助器前缀设置
run-id来命名迭代的。我们使用e10来表示num_epoch参数被设置为10,b1000代表batch_size值为1000。这种命名方案很有帮助,也是我们在本书中将继续使用的方式。
在代理进行训练时,尝试回答以下问题:
-
代理的训练效果是否比你预期的更好或更差?
-
你认为这是为什么?
你需要运行样本来学习这些问题的答案。在接下来的部分中,我们将讨论一些有助于你理解这些复杂主题的练习。
练习
尝试自行完成以下一两个练习:
-
运行 CrawlerStaticTarget 示例场景,并将其性能与动态示例进行比较。
-
将另一个控制示例中的
time_horizon、batch_size和buffer_size大脑超参数加倍:
time_horizon: 2000
batch_size: 4048
buffer_size: 40480
-
在另一个控制样本上执行相同的
time_horizon、batch_size和buffer_size的修改,并观察它们的联合效果。 -
修改
num_layers和hidden_units大脑超参数为我们在控制样本中使用的值,并将其应用于离散动作示例,如 Hallway 示例,代码如下。它对训练有什么影响?
num_layers: 3
hidden_units: 512
-
修改另一个连续或离散动作示例中的
num_layers和hidden_units超参数,并将其与其他参数修改结合使用。 -
将离散动作示例中的
lambd大脑超参数修改为.99。记住,这将加强奖励的效果:
lambd: .99
-
创建你自己的控制生物,带有关节和肢体。一个好的开始是使用爬行器示例并对其进行修改。
-
通过添加新肢体或关节来修改其中一个控制样本。
-
修改 Walker 控制示例,为代理添加武器和目标。你需要结合 Walker 和 Reacher 示例的元素。
-
运行修改过
num_epoch和batch_size参数的 VisualHallwayLearning 示例场景。结果如你所预期吗?
随着我们深入本书,这些练习可能会变得越来越繁琐,特别是在旧的、较慢的系统上运行时。然而,理解这些参数如何影响代理的训练是很重要的。
与深度学习和强化学习(RL)从业者交流时,他们常常将训练的微妙差别比作做饭的好与坏之间的区别。一个好的厨师可能能做出美味的菜肴,并提供一顿完全可以接受的饭菜,但只有伟大的厨师,凭借他们对细节的关注,才能做出一顿让你难以忘怀的杰出餐点。
总结
在这一章中,我们深入研究了强化学习(RL)的内部运作,通过理解基于模型和非基于模型和/或基于策略的算法之间的差异。正如我们所学,Unity ML-Agents 使用 PPO 算法,这是一种强大而灵活的策略学习模型,在训练控制任务时表现出色,有时这种任务被称为马拉松式强化学习。了解了更多基础知识后,我们跳入了其他形式的强化学习改进,例如 Actor-Critic(演员-评论家)或优势训练,并了解了 ML-Agents 所支持的选项。接下来,我们探讨了 PPO 的演变以及它的前身 TRPO 算法,了解了它们如何在基本层面上运作以及如何影响训练。在这一部分,我们学习了如何修改其中一个控制示例,创建 Reacher 手臂上的新关节。最后,我们通过调整超参数,探讨了如何改进多代理策略训练。
我们已经涵盖了强化学习(RL)的许多方面和细节,以及代理如何训练,但训练中最重要的部分——奖励,我们将留到下一章。在下一章中,我们将探讨奖励、奖励函数,以及奖励如何被模拟。
第九章:奖励和强化学习
奖励是强化学习的一个基本概念,且易于理解。毕竟,我们在一定程度上是通过奖励来训练和教导他人——比如训练狗或小孩。将奖励或reward函数实现到仿真中可能会有些困难,并且容易出现很多试错过程。这也是为什么我们要等到更后面、更高级的章节再讨论奖励、构建reward函数以及奖励辅助方法,如课程学习、反向播放、好奇心学习和模仿学习/行为克隆。
下面是本章将要讲解概念的简要总结:
-
奖励和
reward函数 -
奖励的稀疏性
-
课程学习
-
理解反向播放
-
好奇心学习
虽然本章内容较为高级,但它也是至关重要的一章,不容错过。同样,许多顶级的强化学习示范,如 DeepMind 的 AlphaStar,都是利用本章中的高级算法来教导代理执行以前认为不可能完成的任务。
奖励和奖励函数
我们常常面对奖励学习或训练的先入为主的观念,即任务完成后会有奖励,可能是好的也可能是坏的。虽然这种基于奖励的强化学习概念对于单一动作的任务完全适用,比如之前提到的经典多臂赌博机问题,或是教狗做一个动作,但请记住,强化学习实际上是关于代理通过一系列动作预测未来奖励,进而学习动作的价值。在每个动作步骤中,当代理不再进行探索时,它会根据所感知到的最佳奖励来决定下一步的行动。并不是总能清楚地知道这些奖励应该在数值上表示什么,以及这一点有多重要。因此,通常需要绘制出一组简单的reward函数,描述我们希望代理训练的学习行为。
让我们打开 Unity 编辑器中的 GridWorld 示例,并学习如何创建一组reward函数和映射,描述该训练过程,如下所示:
-
从 Assets | ML-Agents | Examples | GridWorld | Scenes 文件夹中打开
GridWorld示例。 -
在层级视图中选择 trueAgent 对象,然后将代理的脑部(Grid Agent | Brain)切换为 GridWorldLearning。
-
选择 GridAcademy 并将 Grid Academy | Brains | Control 选项设置为启用。
-
选择并禁用场景中的主摄像机。这样代理的摄像机将成为主要摄像机,且我们可以通过它来查看场景。
-
打开并准备一个 Python 或 Anaconda 窗口用于训练。如果需要记起如何操作,请查看前几章或 Unity 文档。
-
保存场景和项目。
-
使用以下命令在 Python/Anaconda 窗口中启动示例进行训练:
mlagents-learn config/trainer_config.yaml --run-id=gridworld --train
- 你会很快感受到这个示例的训练速度。记住,代理训练如此快速的主要原因是状态空间非常小;在这个例子中是 5x5。以下截图展示了模拟运行的一个例子:

GridWorld 示例运行在 5x5 网格上
- 运行示例直到完成。即使在较老的系统上运行,也不会花费太长时间。
注意,当代理学习将立方体放置到绿色+号上时,代理迅速从负奖励转变为正奖励。然而,你是否注意到代理从一个负的平均奖励开始训练?代理初始时的奖励值为零,所以让我们来看看负奖励是从哪里来的。在接下来的章节中,我们将通过查看代码来了解如何构建 reward 函数。
构建奖励函数
构建 reward 函数可以非常简单,像这个例子一样,或者非常复杂,正如你可能想象的那样。虽然在训练这些示例时这一步是可选的,但在你构建自己的环境时几乎是必需的。它还可以帮助你发现训练中的问题,以及提高或简化训练的方法。
打开 Unity 编辑器,并按照本练习构建这些示例 reward 函数:
-
在层级窗口中选择 trueAgent 对象,然后点击网格代理组件旁边的目标图标。
-
从联系菜单中选择编辑脚本。
-
在脚本在编辑器中打开后,向下滚动到
AgentAction方法,如下所示:
public override void AgentAction(float[] vectorAction, string textAction)
{
AddReward(-0.01f);
int action = Mathf.FloorToInt(vectorAction[0]);
... // omitted for brevity
Collider[] blockTest = Physics.OverlapBox(targetPos, new Vector3(0.3f, 0.3f, 0.3f));
if (blockTest.Where(col => col.gameObject.CompareTag("wall")).ToArray().Length == 0)
{
transform.position = targetPos;
if (blockTest.Where(col => col.gameObject.CompareTag("goal")).ToArray().Length == 1)
{
Done();
SetReward(1f);
}
if (blockTest.Where(col => col.gameObject.CompareTag("pit")).ToArray().Length == 1)
{
Done();
SetReward(-1f);
}
}
}
-
我们要关注高亮显示的行,
AddReward和SetReward:-
AddReward(-.1f):这一行表示步骤奖励。代理每走一步都会受到负奖励。这也是我们看到代理展示负奖励,直到它找到正奖励的原因。 -
SetReward(1f):这是代理收到的最终正奖励,并且设置为最大值1。在这类训练场景中,我们通常使用从 -1 到 +1 的奖励范围。 -
SetReward(-1f):这是死亡深渊奖励,也是最终的负奖励。
-
-
使用之前的每个语句,我们可以将这些映射到
reward函数,如下所示:-
AddReward(-.1f)=![]()
-
SetReward(1f)=![]()
-
SetReward(-1f)=![]()
-
-
这里需要注意的一点是,
AddReward是增量奖励,而SetReward设置最终值。所以,代理只有通过达到最终目标才能看到正奖励。
通过映射这些reward函数,我们可以看到,代理要想学习到正奖励,唯一的办法就是找到目标。这就是为什么代理一开始会收到负奖励的原因,代理本质上是先学会避免浪费时间或行动,直到它随机遇到目标。从那时起,代理可以根据之前获得的正奖励快速给状态赋值。问题在于,代理首先需要遇到正奖励,然后我们才能开始实际的训练。我们将在下一节讨论这个问题。
奖励稀疏
我们称代理没有得到足够的正奖励,或者根本没有正奖励的情况为奖励稀疏。展示奖励稀疏如何发生的最简单方法是通过示例,幸运的是,GridWorld 示例能够轻松地为我们演示这一点。打开编辑器中的 GridWorld 示例,并按照本练习操作:
-
打开上一个练习中我们离开的 GridWorld 示例场景。为了本次练习的目的,最好已经将原始示例训练完成。GridWorld 是一个紧凑的小示例,训练速度快,是测试基本概念甚至超参数的绝佳场所。
-
选择 GridAcademy 并将 Grid Academy | Reset Parameters | gridSize 更改为
25,如下图所示:

设置 GridAcademy 的 gridSize 参数
-
保存场景和项目。
-
从你的 Python/Anaconda 窗口使用以下命令启动示例进行训练:
mlagents-learn config/trainer_config.yaml --run-id=grid25x25 --train
- 这将启动示例,并且假设你仍然将 agentCam 作为主摄像机,你应该在游戏窗口中看到以下内容:

具有 25x25 网格大小的 GridWorld
-
我们已经将游戏的空间从 5x5 的网格扩展到 25x25 的网格,使得代理随机找到目标(+)符号变得更加困难。
-
你会很快注意到,在几轮训练后,代理在某些情况下的表现非常差,甚至报告的平均奖励小于-1。更糟糕的是,代理可能会继续这样训练很长时间。事实上,代理可能在 100、200、1,000 次甚至更多的迭代中都无法发现奖励。现在,这看起来像是状态的问题,从某种角度来看,你可能会这么认为。然而,记住,我们给代理的输入状态始终是相同的摄像机视图,84x84 像素的状态图像,我们并没有改变这一点。因此,在这个示例中,可以认为策略 RL 算法中的状态保持不变。因此,解决问题的最佳方法是增加奖励。
-
在 Python/Anaconda 窗口中通过输入Ctrl + C停止训练示例。为了公平起见,我们将平等地增加目标和死亡的奖励数量。
-
返回编辑器,选择 GridAcademy 并增加 Grid Academy | Reset Parameters 组件属性中的 numObstacles 和 numGoals,如下所示:

更新障碍物和目标的数量
-
保存场景和项目。
-
使用以下代码启动训练会话:
mlagents-learn config/trainer_config.yaml --run-id=grid25x25x5 --train
-
这表示我们正在用五倍数量的障碍物和目标运行示例。
-
让智能体训练 25,000 次迭代,并观察性能的提高。让智能体训练直到完成,并将结果与我们的第一次训练进行比较。
奖励稀疏性问题通常更常见于离散动作任务中,例如 GridWorld/Hallway 等,因为reward函数通常是绝对的。在连续学习任务中,reward函数往往更为渐进,通常通过达到某个目标的进展来衡量,而不仅仅是目标本身。
通过增加障碍物和目标的数量——即负奖励和正奖励——我们能够更快速地训练智能体,尽管你可能会看到非常不稳定的训练周期,而且智能体的表现永远不会真正达到最初的水平。实际上,训练在某个点可能会发生偏离。造成这种情况的原因部分是由于其有限的视野,并且我们仅部分解决了稀疏奖励问题。当然,我们可以通过简单地增加目标和障碍物的数量来解决这个稀疏奖励问题。你可以返回并尝试将障碍物和奖励的数量设置为 25,看看能否得到更稳定的长期结果。
当然,在许多强化学习问题中,增加奖励的数量并不是一个选项,我们需要寻找更巧妙的方法,正如我们在下一节中将看到的那样。幸运的是,很多方法在非常短的时间内相继出现,旨在解决稀疏或困难奖励的问题。Unity 作为领先者,迅速采纳并实施了多种方法,其中我们将讨论的第一个方法叫做课程学习(Curriculum Learning),我们将在下一节中详细讨论。
课程学习
课程学习允许智能体通过逐步提升reward函数来逐步学习一个困难的任务。当奖励保持绝对时,智能体以更简单的方式找到或实现目标,从而学习到奖励的目的。然后,随着训练的进行和智能体的学习,获得奖励的难度增加,这反过来迫使智能体进行学习。
当然,Unity 有一些此类示例,我们将在接下来的练习中查看WallJump示例,了解如何设置一个课程学习样本:
-
打开 WallJump 场景,路径为 Assets | ML-Agents | Examples | WallJump | Scenes 文件夹。
-
在层次窗口中选择 Academy 对象。
-
如下所示,点击 Wall Jump Academy | Brains | Control 参数中的两个控制选项:

设置多个大脑进行学习
-
这个示例使用多个大脑来更好地按任务分离学习。实际上,所有大脑将同时进行训练。
-
课程学习使用第二个配置文件来描述代理将经历的课程或学习步骤。
-
打开
ML-Agents/ml-agents/config/curricul/wall-jump文件夹。 -
在文本编辑器中打开
SmallWallJumpLearning.json文件。文件如下所示:
{
"measure" : "progress",
"thresholds" : [0.1, 0.3, 0.5],
"min_lesson_length": 100,
"signal_smoothing" : true,
"parameters" :
{
"small_wall_height" : [1.5, 2.0, 2.5, 4.0]
}
}
-
这个 JSON 文件定义了 SmallWallJumpLearning 大脑作为其课程或学习步骤的一部分所采取的配置。这些参数的定义在 Unity 文档中有详细说明,但我们将按照文档中的参数进行查看:
-
measure– 衡量学习进展和课程进度的指标:-
reward – 使用衡量标准接收到的奖励。
-
progress – 使用步骤与最大步骤数的比率。
-
-
thresholds(浮动数组)– 衡量标准值的点,在这些点上课程应当提升。 -
min_lesson_length(整数)– 在课程可以更改之前应完成的最小回合数。如果设置了奖励衡量标准,则将使用最近min_lesson_length回合的平均累积奖励来决定是否应更改课程。必须是非负数。
-
-
通过阅读此文件我们可以看到,设定了三个课程,通过
progress的measure来定义,progress由回合数来表示。回合边界定义为.1或 10%,.3或 30%,和.5或 50%总回合数。每个课程我们都通过边界定义参数,在这个例子中,参数是small_wall_height,第一个课程的边界是1.5到2.0,第二个课程的边界是2.0到2.5,第三个课程的边界是2.5到4.0。 -
打开一个 Python/Anaconda 窗口并准备好进行训练。
-
使用以下命令启动训练会话:
mlagents-learn config/trainer_config.yaml --curriculum=config/curricula/wall-jump/ --run-id=wall-jump-curriculum --train
-
高亮的额外部分将文件夹添加到辅助课程配置中。
-
你需要等待至少一半的完整训练步骤才能看到所有三个训练阶段。
这个例子介绍了一种我们可以用来解决稀疏或难以获得奖励问题的技术。在接下来的部分中,我们将讨论一种专门的课程训练形式,叫做 Backplay。
理解 Backplay
2018 年末,Cinjon Resnick 发布了一篇创新论文,题为Backplay: Man muss immer umkehren,(arxiv.org/abs/1807.06919),其中介绍了一种叫做 Backplay 的课程学习改进方法。基本前提是,训练时将代理从目标开始,然后逐步将其移回。这种方法可能并不适用于所有情况,但我们将使用这种方法结合课程训练,看看如何在以下练习中改进 VisualHallway 示例:
-
从 Assets | ML-Agents | Examples | Hallway | Scenes 文件夹中打开 VisualHallway 场景。
-
确保场景重置为默认的起始点。如果需要,可以再次从 ML-Agents 拉取源代码。
-
使用 VisualHallwayLearning 大脑设置学习场景,并确保智能体仅使用默认的 84x84 视觉观察。
-
选择 Academy 对象,在检查器窗口中添加一个新的 Hallway Academy | Reset Parameter,命名为
distance,如下所示:

在 Academy 上设置新的重置参数
-
你可以将重置参数用于不仅仅是课程学习,因为它们可以帮助你轻松配置编辑器中的训练参数。我们在这里定义的参数将设置智能体距离目标区域的距离。此示例旨在展示 Backplay 的概念,为了正确实现它,我们需要将智能体移动到目标的正前方——但我们暂时不进行这一操作。
-
选择 VisualHallwayArea | Agent,并在你喜欢的代码编辑器中打开 Hallway Academy 脚本。
-
向下滚动到
AgentReset方法,并将顶部代码行调整为如下所示:
public override void AgentReset()
{
float agentOffset = academy.resetParameters["distance"];
float blockOffset = 0f;
// ... rest removed for brevity
-
这一行代码将调整智能体的起始偏移量,以适应现在预设的 Academy 重置参数。同样,当 Academy 在训练过程中更新这些参数时,智能体也会看到更新的值。
-
保存文件并返回编辑器。编辑器会重新编译你的代码更改,并告知你一切是否正常。如果控制台出现红色错误,通常意味着有编译错误,可能是由语法错误引起的。
-
打开一个准备好的 Python/Anaconda 窗口,并运行以下命令来开始训练:
mlagents-learn config/trainer_config.yaml --run-id=vh_backplay --train
- 这将以常规模式运行会话,不使用课程学习,但它会将智能体的起始位置调整得更接近目标区域。让这个示例运行,并观察智能体在如此接近目标时的表现如何。
让训练运行一段时间,并观察与原始训练的区别。你会注意到的一点是,智能体现在不自觉地跑向奖励区域,这正是我们所期望的。接下来我们需要实现的是课程学习部分,即智能体在学习如何在下一部分找到奖励的过程中逐渐向后移动。
通过课程学习实现 Backplay
在上一节中,我们实现了 Backplay 的第一部分,即让智能体起始于目标附近或非常接近目标的位置。接下来我们需要完成的部分是使用课程学习(Curriculum Learning)将智能体逐步移回到它的预定起始点。请再次打开 Unity 编辑器并进入 VisualHallway 场景,按照以下步骤操作:
-
使用文件浏览器或命令行打开
ML-Agents/ml-agents/config文件夹。 -
创建一个名为
hallway的新文件夹并进入该文件夹。 -
打开文本编辑器或在新目录下创建一个名为
VisualHallwayLearning.json的新 JSON 文本文件。JavaScript 对象表示法(JSON)用于描述 JavaScript 中的对象,它也成为了一种配置设置的标准。 -
在新文件中输入以下 JSON 文本:
{
"measure" : "rewards",
"thresholds" : [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7],
"min_lesson_length": 100,
"signal_smoothing" : true,
"parameters" :
{
"distance" : [12, 8, 4, 2, -2, -4, -8, -12]
}
-
这个配置文件定义了我们将在 Backplay 上训练代理的课程。该文件定义了一个
measure,包括rewards和thresholds,用于确定代理何时会晋级到下一个训练阶段。当奖励阈值达到最低100步长时,训练将进入下一个distance参数。注意,我们如何将距离参数定义为12,表示距离目标较近,然后逐渐减少。当然,你也可以创建一个函数来映射不同的范围值,但这部分留给你自己完成。 -
编辑完成后保存文件。
-
从 Python/Anaconda 窗口启动训练会话,使用以下命令:
mlagents-learn config/trainer_config.yaml --curriculum=config/curricula/hallway/ --run-id=hallway-curriculum --train
- 训练开始后,注意 Python/Anaconda 窗口中课程如何被设置,如下图所示:

观看训练中课程参数的设置
- 等待代理训练完成,看看它能在会话结束前完成多少个训练级别。
现在,我们需要澄清的一点是,这个示例更像是一个创新示例,而不是 Backplay 的真实示例。实际的 Backplay 是将代理放在目标位置,然后向后工作。在这个示例中,我们几乎将代理放置在目标位置并向后工作。这个差异是微妙的,但到现在为止,希望你能理解,从训练的角度来看,这可能是有意义的。
好奇心学习
到目前为止,我们只考虑了代理在环境中可能获得的外部奖励。例如,走廊示例当代理到达目标时会给出 +1 的外部奖励,而当它达到错误的目标时会给予 -1 的外部奖励。然而,像我们这样的真实动物其实可以基于内在动机进行学习,或者使用内在的 reward 函数。一个很好的例子是一个婴儿(猫、人类,或者其他任何东西),它通过玩耍有明显的自然动机去保持好奇心。玩耍的好奇心为婴儿提供了一个内在或固有的奖励,但实际上,这一行为本身会带来负面的外部奖励。毕竟,婴儿在消耗能量,这是一个负面的外部奖励,但它依然会继续玩耍,从而学习更多关于环境的一般信息。这反过来使它能够探索更多的环境,并最终达到一些非常困难的目标,比如打猎或上班。
这种内部或内在奖励建模属于强化学习(RL)的一类子类别,称为动机强化学习(Motivated Reinforcement Learning)。正如你可以想象的,这种学习模式在游戏中有着巨大的应用前景,从创建 NPC 到构建更具可信度的对手,这些对手实际上会受到某种性格特征或情绪的驱动。试想一下,拥有一个能生气甚至表现出同情心的电脑对手?当然,我们离这个目标还很远,但在此期间,Unity 已经添加了一个内在奖励系统,用于模拟智能体的好奇心,这就是所谓的好奇心学习。
好奇心学习(CL)最初由加利福尼亚大学伯克利分校的研究人员在一篇名为由**自监督预测驱动的好奇心探索的论文中提出,您可以在pathak22.github.io/noreward-rl/中找到该论文。论文进一步描述了一个使用正向和反向神经网络解决稀疏奖励问题的系统。研究人员将该系统称为内在好奇心模块(ICM),旨在将其作为其他强化学习(RL)系统之上的一层或模块使用。这正是 Unity 所做的,他们将其作为模块添加到 ML-Agents 中。
Unity 的首席研究员 Arthur Juliani 博士在他们的实现上有一篇很棒的博客文章,您可以在blogs.unity3d.com/2018/06/26/solving-sparse-reward-tasks-with-curiosity/找到。
ICM 通过使用反向神经网络工作,该网络通过当前和下一个观察到的智能体状态进行训练。它使用编码器对两种状态之间的动作进行预测编码,当前状态和下一个状态。然后,正向网络在当前观察和动作的基础上进行训练,将其编码为下一个观察状态。接着,从反向和正向模型中分别计算实际和预测编码之间的差异。在这种情况下,差异越大,惊讶感越强,内在奖励也就越多。以下是从 Arthur Juliani 博士的博客中提取的图示,描述了这一过程如何工作:

好奇心学习模块的内部工作原理
该图表显示了两个模型和层的表示,分别是蓝色的正向和反向,蓝色线条表示网络流动,绿色框表示内在模型计算,而奖励输出则以绿色虚线的形式呈现。
好了,理论部分讲得差不多了,接下来是时候看看这个 CL 如何在实践中运作了。幸运的是,Unity 有一个非常完善的环境,展示了这个名为 Pyramids 的新模块。让我们打开 Unity 并按照接下来的练习查看这个环境的实际效果:
-
打开 Assets | ML-Agents | Examples | Pyramids | Scenes 文件夹中的 Pyramid 场景。
-
在层级视图窗口中选择 AreaPB(1)到 AreaPB(15),然后在检查器窗口中禁用这些对象。
-
离开玩家模式下的场景。第一次,我们希望你自己来体验这个场景并弄清楚目标。即使你已经阅读过博客或玩过这个场景,也请再试一次,不过这次需要思考应该设置哪些奖励函数。
-
在编辑器中按下播放按钮,并开始以玩家模式玩游戏。如果你之前没有玩过游戏或者不了解前提,不要感到惊讶,如果你花了一段时间才能解开这个谜题。
对于那些没有提前阅读或玩过的同学,下面是前提。场景开始时,代理被随机放置在一个有多个房间的区域,里面有石质金字塔,其中一个有开关。代理的目标是激活开关,然后生成一个沙箱金字塔,上面放着一个大金盒子。开关在被激活后会从红色变为绿色。金字塔出现后,代理需要把金字塔推倒并取回金盒子。这并不是最复杂的谜题,但确实需要一些探索和好奇心。
假设我们尝试用一组reward函数来模拟这种好奇心或探索的需求。我们将需要一个用于激活按钮的reward函数,一个用于进入房间的函数,一个用于推倒积木的函数,当然还有一个用于获得金盒子的函数。然后我们需要确定每个目标的价值,或许可以使用某种形式的逆向强化学习(IRL)。然而,使用好奇心学习,我们可以只为获取盒子的最终目标创建奖励函数(+1),并可能设置一个小的负步长目标(.0001),然后利用内在的好奇心奖励让代理学习其余步骤。这个技巧相当巧妙,我们将在下一部分看到它是如何工作的。
好奇心内在模块正在运行
在我们理解金字塔任务的难度后,我们可以继续在接下来的练习中训练具有好奇心的代理:
-
在编辑器中打开金字塔场景。
-
在层次视图中选择 AreaRB | 代理对象。
-
将金字塔代理 | 大脑切换为 PyramidsLearning 大脑。
-
在层次视图中选择 Academy 对象。
-
启用 Academy | Pyramid Academy | 大脑 | 控制属性中的控制选项,如下截图所示:

设置 Academy 为控制模式
-
打开一个 Python 或 Anaconda 控制台,并为训练做准备。
-
打开位于
ML-Agents/ml-agents/config文件夹中的trainer_config.yaml文件。 -
向下滚动到
PyramidsLearning配置部分,如下所示:
PyramidsLearning:
use_curiosity: true
summary_freq: 2000
curiosity_strength: 0.01
curiosity_enc_size: 256
time_horizon: 128
batch_size: 128
buffer_size: 2048
hidden_units: 512
num_layers: 2
beta: 1.0e-2
max_steps: 5.0e5
num_epoch: 3
-
这里有三个新的配置参数,已用粗体标出:
-
use_curiosity:将其设置为true以使用该模块,但默认情况下通常为false。 -
curiosity_strength:这是代理对好奇心内在奖励与外在奖励的重视程度。 -
curiosity_enc_size:这是我们将网络压缩到的编码层的大小。如果你回想一下自编码器,你会发现 256 的大小相当大,但也要考虑你可能要编码的状态空间或观察空间的大小。
-
保持参数为已设置的值。
- 使用以下命令启动训练会话:
mlagents-learn config/trainer_config.yaml --run-id=pyramids --train
虽然这次训练会话可能需要一些时间,但观察代理如何探索也许会很有趣。即使在当前的设置下,使用仅一个训练区域,你也许能够看到代理在几次迭代后解决这个难题。
由于 ICM 是一个模块,它可以快速激活到任何我们想要查看效果的其他示例中,这也是我们在下一部分将要做的。
在 Hallway/VisualHallway 上尝试 ICM
与我们训练的代理类似,我们通过试错学习得很好。这也是我们为何要不断练习那些非常困难的任务,比如跳舞、唱歌或演奏乐器。强化学习(RL)也不例外,要求从业者通过试验、错误和进一步探索的严峻考验中学习其中的技巧。因此,在接下来的练习中,我们将把 Backplay(课程学习)和好奇心学习结合在一起,应用到我们熟悉的 Hallway 场景中,看看它会带来什么效果,如下所示:
-
打开 Hallway 或 VisualHallway 场景(你可以选择其一),如我们上次所留,启用了课程学习并设置为模拟 Backplay。
-
打开
trainer_config.yaml配置文件,位置在ML-Agents/ml-agents/config文件夹中。 -
向下滚动至
HallwayLearning或VisualHallwayLearning脑网络配置参数,并添加以下附加配置行:
HallwayLearning:
use_curiosity: true
curiosity_strength: 0.01
curiosity_enc_size: 256
use_recurrent: true
sequence_length: 64
num_layers: 2
hidden_units: 128
memory_size: 256
beta: 1.0e-2
gamma: 0.99
num_epoch: 10
buffer_size: 1024
batch_size: 1000
max_steps: 5.0e5
summary_freq: 1000
time_horizon: 64
-
这将启用本示例的好奇心模块。我们使用与上一个金字塔示例相同的好奇心设置。
-
确保这个示例已为我们在该部分中配置的课程 Backplay 做好准备。如果需要,回去复习那部分内容,并在继续之前将其能力添加到此示例中。
这可能需要你创建一个新的课程文件,使用与我们之前相同的参数。记住,课程文件需要与其所使用的脑网络名称相同。
- 打开一个准备好进行训练的 Python/Anaconda 窗口,并使用以下命令开始训练:
mlagents-learn config/trainer_config.yaml --curriculum=config/curricula/hallway/ --run-id=hallway_bp_cl --train
- 让训练运行直到完成,因为结果可能会很有趣,并展示叠加学习增强对外部和内部奖励的一些强大可能性。
这个练习展示了如何通过课程学习模拟 Backplay 以及好奇心学习为学习过程添加代理动机来运行一个代理。正如你可以想象的那样,内在奖励学习和整个动机强化学习领域可能会对我们的深度强化学习(DRL)带来一些有趣的进展和改进。
在下一节中,我们将回顾一些有助于你学习这些概念的练习。
练习
虽然你阅读本书的动机可能各不相同,但希望到目前为止你已经能够意识到亲自实践的价值。正如以往,我们呈现这些练习供你享受与学习,并希望你在完成它们时玩得开心:
-
选择另一个使用离散动作的示例场景,并编写相应的奖励函数。是的,这意味着你需要打开代码并查看它。
-
选择一个连续动作场景,并尝试为其编写奖励函数。虽然这可能有点困难,但如果你想要构建自己的控制训练智能体,这是必不可少的。
-
在我们探索的另一个离散动作示例中添加课程学习。决定如何将训练分解成不同的难度等级,并创建控制训练进程的参数。
-
在一个连续动作示例中添加课程学习。这更具挑战性,你可能希望先完成第二个练习。
-
在 Hallway 环境中实际实现 Backplay,通过将智能体从目标位置开始,并在智能体训练过程中,使用课程学习将其移动回期望的起始位置。
-
在另一个离散动作示例中实现 Backplay,并查看它对训练的影响。
-
在 VisualPyramids 示例中实施好奇心学习,并注意训练中的差异。
-
在一个连续动作示例中实施好奇心学习,并观察它对训练的效果。这是你预期的吗?
-
禁用 Pyramids 示例中的好奇心学习,观察这对智能体训练的影响。
-
想一想,如何将 Backplay 添加到 VisualPyramids 示例中。如果你真的实现了它,你将获得额外的积分。
如你所见,随着我们逐步深入本书,练习的难度也在增加。记住,即使只完成一两个练习,也会对你所掌握的知识产生重要影响。
摘要
在这一章,我们探讨了强化学习(RL)中的一个基础组成部分,那就是奖励。我们了解到,在构建训练环境时,最好定义一套奖励函数,让智能体遵循这些规则。通过理解这些方程式,我们能够更好地理解频繁或稀疏的奖励如何对训练产生负面影响。接着,我们介绍了几种方法,其中第一种叫做课程学习(Curriculum Learning),可以用来缓解或逐步推进智能体的外部奖励。之后,我们探讨了另一种技术,叫做反向播放(Backplay),它使用反向播放技术和课程训练来增强智能体的训练。最后,我们介绍了内在奖励或内在强化学习的概念。我们还了解到,最初开发的内在奖励系统是为了赋予智能体好奇心的动机。我们查看了如何在一些例子中应用好奇心学习,并通过课程学习将其与反向播放结合使用。
在下一章,我们将探索更多奖励辅助解决方案,形式为模仿学习和迁移学习,在这一章中,我们将学习如何将人类的游戏体验映射到一种叫做模仿学习或行为克隆的学习形式。
第十章:模仿与迁移学习
在本文撰写时,一种名为 AlphaStar 的新 AI,深度强化学习(DRL)代理,使用模仿学习(IL)在实时战略游戏《星际争霸 II》中以五比零击败了人类对手。AlphaStar 是 David Silver 和 Google DeepMind 为建立更智能、更强大 AI 的工作的延续。AlphaStar 使用的具体技术可以写成一本书,而 IL 和模仿人类游戏的学习方法如今受到高度关注。幸运的是,Unity 已经在离线和在线训练场景的形式中实现了 IL。尽管我们在这一章中不会达到 AlphaStar 的水平,但我们仍将了解 IL 和其他形式的迁移学习的基础技术。
在这一章中,我们将研究 ML-Agents 中 IL 的实现,然后探讨其他迁移学习的应用。我们将在本章中涵盖以下内容:
-
IL 或行为克隆
-
在线训练
-
离线训练
-
迁移学习
-
模仿迁移学习
尽管 AlphaStar 在一场实时战略游戏中对人类职业玩家取得了惊人的战术胜利,但它仍然因所使用的游戏方式和动作类型而受到审视。许多玩家表示,AI 的战术能力显然优于人类,但整体的战略规划则非常糟糕。看看 Google DeepMind 如何应对这一批评应该会很有趣。
这将是一个令人兴奋的章节,并将为你的未来开发提供大量的训练可能性,一切从下一节开始。
IL 或行为克隆
IL 或行为克隆是通过从人类或可能是另一种 AI 捕捉观察和动作,并将其作为输入用于训练代理的过程。代理本质上由人类引导,并通过他们的动作和观察进行学习。一组学习观察可以通过实时游戏(在线)接收,或者从已保存的游戏中提取(离线)。这提供了捕捉多个代理的游戏并同时或单独训练它们的能力。IL 提供了训练代理的能力,或者实际上是为你可能无法通过常规 RL 训练的任务编程代理,正因如此,它很可能成为我们在不久的将来用于大多数任务的关键 RL 技术。
很难评估某个东西给你带来的价值,直到你看到没有它的情况。考虑到这一点,我们首先将通过一个没有使用 IL,但显然可以受益于 IL 的例子开始。打开 Unity 编辑器并按照以下步骤进行练习:
-
打开位于 Assets | ML-Agents | Examples | Tennis | Scenes 文件夹中的 Tennis 场景。
-
选择并禁用额外的代理训练区域,TennisArea(1) 到 TennisArea(17)。
-
选择 AgentA,并确保 Tennis Agent | Brain 设置为 TennisLearning。我们希望每个代理在这个例子中互相对抗。
-
选择 AgentB 并确保 Tennis Agent | Brain 设置为 TennisLearning。
在这个示例中,短时间内我们正在同一环境中训练多个代理。我们将在第十一章《构建多代理环境》中讨论更多代理与其他代理进行学习的场景。
-
选择 Academy 并确保 Tennis Academy | Brains 设置为 TennisLearning,并且控制选项已启用,如下图所示:

在 Academy 上将控制设置为启用
- 打开 Python/Anaconda 窗口并为训练做准备。我们将通过以下命令启动训练:
mlagents-learn config/trainer_config.yaml --run-id=tennis --train
- 观看训练过程几千次,足以让你确信代理不会轻易学会这个任务。当你确信之后,停止训练并继续进行。
仅通过查看这个第一个示例,你就可以发现普通训练以及我们所讨论的其他高级方法,如课程学习和好奇心学习,都会很难实现,而且在这种情况下可能会适得其反。在接下来的部分中,我们将展示如何在在线训练模式下使用 IL 运行这个示例。
在线训练
在线模仿学习是指你教代理实时学习一个玩家或另一个代理的观察。它也是训练代理或机器人最有趣且吸引人的方式之一。接下来,我们将跳入并为在线模仿学习设置网球环境:
-
选择 TennisArea | AgentA 对象,并将 Tennis Agent | Brain 设置为 TennisPlayer。在这个 IL 场景中,我们有一个大脑作为教师,即玩家,另一个大脑作为学生,即学习者。
-
选择 AgentB 对象并确保 Tennis Agent | Brain 设置为 TennisLearning。这将是学生大脑。
-
打开
ML-Agents/ml-agents/config文件夹中的online_bc_config.yaml文件。IL 使用的配置与 PPO 不同,因此这些参数的名称可能相似,但可能不会像你所习惯的那样响应。 -
在文件中向下滚动,找到
TennisLearning大脑配置,如以下代码片段所示:
TennisLearning:
trainer: online_bc
max_steps: 10000
summary_freq: 1000
brain_to_imitate: TennisPlayer
batch_size: 16
batches_per_epoch: 5
num_layers: 4
hidden_units: 64
use_recurrent: false
sequence_length: 16
-
仔细查看超参数,我们可以看到有两个新的参数需要关注。以下是这些参数的总结:
-
trainer:online_或offline_bc—使用在线或离线行为克隆。在这种情况下,我们正在进行在线训练。 -
brain_to_imitate:TennisPlayer—这设置了学习大脑应尝试模仿的目标大脑。此时我们不会对文件进行任何更改。
-
-
打开你准备好的 Python/Anaconda 窗口,并通过以下命令启动训练:
mlagents-learn config/online_bc_config.yaml --run-id=tennis_il --train --slow
- 在编辑器中按下播放按钮后,你将能够用W、A、S、D键控制左边的挡板。玩游戏时,你可能会惊讶于代理学习的速度,它可能变得相当优秀。以下是游戏进行中的一个示例:

使用 IL 进行代理的播放和教学
- 如果愿意,继续玩完示例。也可以有趣的是在游戏过程中切换玩家,甚至训练大脑并使用训练好的模型进行后续对战。你记得怎么运行训练好的模型吗?
在完成上一个练习的过程中,您可能会想,为什么我们不以这种方式训练所有的 RL 代理。这是一个很好的问题,但正如您可以想象的那样,这取决于情况。虽然 IL 非常强大,且是一个相当能干的学习者,但它并不总是会按预期工作。而且,IL 代理仅会学习它所看到的搜索空间(观察),并且只会停留在这些限制内。以 AlphaStar 为例,IL 是训练的主要输入,但团队也提到,AI 确实有很多时间进行自我对战,这可能是它许多获胜策略的来源。所以,虽然 IL 很酷且强大,但它并不是解决所有 RL 问题的“金蛋”。然而,在完成本练习后,您很可能会对 RL,尤其是 IL,产生新的更深的理解。在下一部分,我们将探索使用离线 IL。
离线训练
离线训练是通过玩家或代理在游戏中进行游戏或执行任务时生成的录制游戏文件,然后将其作为训练观察反馈给代理,帮助代理进行后续学习。虽然在线学习当然更有趣,在某些方面更适用于网球场景或其他多人游戏,但它不太实际。毕竟,通常您需要让代理实时玩好几个小时,代理才会变得优秀。同样,在在线训练场景中,您通常只能进行单代理训练,而在离线训练中,可以将演示播放提供给多个代理,以实现更好的整体学习。这还允许我们执行有趣的训练场景,类似于 AlphaStar 的训练,我们可以教一个代理,让它去教其他代理。
我们将在第十一章中深入学习多代理游戏玩法,构建多代理环境。
在接下来的练习中,我们将重新访问我们老朋友“Hallway/VisualHallway”示例。再次这么做是为了将我们的结果与之前使用该环境运行的示例练习进行比较。请按照本练习的步骤设置一个新的离线训练会话:
-
克隆并下载 ML-Agents 代码到一个新的文件夹,可能选择
ml-agents_b、ml-agents_c或其他名称。这样做的原因是为了确保我们在干净的环境中运行这些新练习。此外,有时返回到旧环境并回忆可能忘记更新的设置或配置也能有所帮助。 -
启动 Unity 并打开UnitySDK项目以及 Hallway 或 VisualHallway 场景,您可以选择其中之一。
-
场景应设置为以播放器模式运行。只需确认这一点。如果需要更改,请进行更改。
-
如果场景中有其他活动的代理训练环境,请禁用它们。
-
在层级视图中选择 HallwayArea | Agent。
-
点击 Inspector 窗口底部的 Add Component 按钮,输入
demo,并选择演示录制组件,如下图所示:

添加演示录制器
-
如前面的截图所示,点击新建的演示录制组件上的“Record”按钮,确保检查所有选项。同时,填写录制的“演示名称”属性,如图所示。
-
保存场景和项目。
-
按下 Play 按钮并播放场景一段时间,至少几分钟,可能不到几个小时。当然,你的游戏表现也会决定代理的学习效果。如果你玩的不好,代理也会学得差。
-
当你认为足够的时间已经过去,并且你已经尽力完成了游戏后,停止游戏。
在游戏播放结束后,你应该会看到一个名为 Demonstrations 的新文件夹,在项目窗口的 Assets 根文件夹中创建。文件夹内将包含你的演示录制。这就是我们在下一部分将喂入代理的数据。
设置训练环境
现在我们已经有了演示录制,可以继续进行训练部分。然而,这一次,我们将把观察文件回放给多个代理,在多个环境中进行训练。打开 Hallway/VisualHallway 示例场景,并按照以下练习设置训练:
-
选择并启用所有 HallwayArea 训练环境,HallwayArea(1)到 HallwayArea(15)。
-
在层级视图中选择 HallwayArea | Agent,然后将 Hallway Agent | Brain 切换为 HallwayLearning,如下图所示:

设置代理组件
-
同时,选择并禁用演示录制组件,如前面的屏幕截图所示。
-
确保场景中的所有代理都使用 HallwayLearning 大脑。
-
在层级视图中选择 Academy,然后启用 Hallway Academy | Brains | Control 选项,如下图所示:

启用 Academy 控制大脑
- 保存场景和项目
现在,我们已经为代理学习配置了场景,可以进入下一部分,开始喂入代理数据。
喂入代理数据
在执行在线 IL 时,我们每次只给一个代理喂入数据,场景是网球场。然而,这次我们将从同一个演示录制中训练多个代理,以提高训练效果。
我们已经为训练做好了准备,现在开始在接下来的练习中喂入代理数据:
-
打开一个 Python/Anaconda 窗口,并从新的
ML-Agents文件夹中设置训练环境。你已经重新克隆了源代码,对吧? -
从
ML-Agents/ml-agents_b/config文件夹中打开offline_bc_config.yaml文件。文件内容如下,供参考:
default:
trainer: offline_bc
batch_size: 64
summary_freq: 1000
max_steps: 5.0e4
batches_per_epoch: 10
use_recurrent: false
hidden_units: 128
learning_rate: 3.0e-4
num_layers: 2
sequence_length: 32
memory_size: 256
demo_path: ./UnitySDK/Assets/Demonstrations/<Your_Demo_File>.demo
HallwayLearning:
trainer: offline_bc
max_steps: 5.0e5
num_epoch: 5
batch_size: 64
batches_per_epoch: 5
num_layers: 2
hidden_units: 128
sequence_length: 16
use_recurrent: true
memory_size: 256
sequence_length: 32
demo_path: ./UnitySDK/Assets/Demonstrations/demo.demo
- 将
HallwayLearning或VisualHallwayLearning大脑的最后一行更改为以下内容:
HallwayLearning:
trainer: offline_bc
max_steps: 5.0e5
num_epoch: 5
batch_size: 64
batches_per_epoch: 5
num_layers: 2
hidden_units: 128
sequence_length: 16
use_recurrent: true
memory_size: 256
sequence_length: 32
demo_path: ./UnitySDK/Assets/Demonstrations/AgentRecording.demo
-
请注意,如果你使用的是
VisualHallwayLearning大脑,你还需要在前面的配置脚本中更改相应的名称。 -
完成编辑后,保存你的更改。
-
返回你的 Python/Anaconda 窗口,使用以下命令启动训练:
mlagents-learn config/offline_bc_config.yaml --run-id=hallway_il --train
- 当提示时,在编辑器中按下 Play 并观看训练过程。你会看到代理使用与自己非常相似的动作进行游戏,如果你玩的不错,代理将很快开始学习,你应该会看到一些令人印象深刻的训练成果,这一切都得益于模仿学习。
强化学习可以被看作是一种蛮力学习方法,而模仿学习和通过观察训练的改进无疑将主导未来的代理训练。当然,难道这真的是令人惊讶的事情吗?毕竟,我们这些简单的人类就是这样学习的。
在下一部分,我们将探讨深度学习的另一个令人兴奋的领域——迁移学习,以及它如何应用于游戏和深度强化学习(DRL)。
迁移学习
模仿学习,按定义属于迁移学习(TL)的一种类型。我们可以将迁移学习定义为一个代理或深度学习网络通过将经验从一个任务转移到另一个任务来进行训练的过程。这可以像我们刚才进行的观察训练那样简单,或者像在代理的大脑中交换层/层权重,或者仅仅在一个相似的任务上训练代理那样复杂。
在迁移学习中,我们需要确保我们使用的经验或先前的权重是可以泛化的。通过本书的基础章节(第 1-3 章),我们学习了使用诸如 Dropout 和批量归一化等技术进行泛化的价值。我们了解到,这些技术对于更通用的训练非常重要;这种训练方式使得代理/网络能够更好地推理测试数据。这与我们使用一个在某个任务上训练的代理去学习另一个任务是一样的。一个更通用的代理,实际上比一个专门化的代理更容易转移知识,甚至可能完全不同。
我们可以通过一个快速的示例来演示这一点,开始训练以下简单的练习:
-
在 Unity 编辑器中打开 VisualHallway 场景。
-
禁用任何额外的训练区域。
-
确认 Academy 控制大脑。
-
从 Hallway/Brains 文件夹中选择 VisualHallwayLearning 大脑,并将 Vector Action | Branches Size | Branch 0 Size 设置为
7,如下面的截图所示:

增加代理的向量动作空间
-
我们增加了大脑的动作空间,使其与我们的迁移学习环境所需的动作空间兼容,稍后我们会详细介绍。
-
保存场景和项目。
-
打开一个准备好的 Python/Anaconda 窗口以进行训练。
-
使用以下代码启动训练会话:
mlagents-learn config/trainer_config.yaml --run-id=vishall --train --save-freq=10000
-
在这里,我们引入了一个新的参数,用于控制模型检查点创建的频率。目前,默认值设置为 50,000,但我们不想等这么久。
-
在编辑器中运行代理进行训练,至少保存一个模型检查点,如下图所示:

ML-Agents 训练器正在创建一个检查点。
-
检查点是一种获取大脑快照并将其保存以供后用的方法。这允许你返回并继续从上次停止的地方进行训练。
-
让代理训练到一个检查点,然后通过按Ctrl + C(在 Python/Anaconda 窗口中)或在 Mac 上按command + C来终止训练。
当你终止训练后,是时候在下一个部分尝试将这个已保存的大脑应用到另一个学习环境中了。
转移大脑。
我们现在想将刚刚训练过的大脑带入一个新的、但相似的环境中重新使用。由于我们的代理使用视觉观察,这使得任务变得更简单,但你也可以尝试用其他代理执行这个示例。
让我们打开 Unity,进入 VisualPushBlock 示例场景并按照这个练习操作:
-
选择 Academy 并启用它来控制大脑。
-
选择代理并设置它使用 VisualPushBlockLearning 大脑。你还应该确认这个大脑的配置与我们刚才运行的 VisualHallwayLearning 大脑相同,即视觉观察和向量动作空间相匹配。
-
在文件资源管理器或其他文件浏览器中打开
ML-Agents/ml-agents_b/models/vishall-0文件夹。 -
将文件和文件夹的名称从
VisualHallwayLearning更改为VisualPushBlockLearning,如以下截图所示:

手动更改模型路径。
-
通过更改文件夹的名称,我们实际上是在告诉模型加载系统将我们的 VisualHallway 大脑恢复为 VisualPushBlockBrain。这里的技巧是确保两个大脑具有相同的超参数和配置设置。
-
说到超参数,打开
trainer_config.yaml文件,确保 VisualHallwayLearning 和 VisualPushBlockLearning 参数相同。以下代码片段显示了这两个配置的参考示例:
VisualHallwayLearning:
use_recurrent: true
sequence_length: 64
num_layers: 1
hidden_units: 128
memory_size: 256
beta: 1.0e-2
gamma: 0.99
num_epoch: 3
buffer_size: 1024
batch_size: 64
max_steps: 5.0e5
summary_freq: 1000
time_horizon: 64
VisualPushBlockLearning:
use_recurrent: true
sequence_length: 64
num_layers: 1
hidden_units: 128
memory_size: 256
beta: 1.0e-2
gamma: 0.99
num_epoch: 3
buffer_size: 1024
batch_size: 64
max_steps: 5.0e5
summary_freq: 1000
time_horizon: 64
-
编辑完成后,保存配置文件。
-
打开你的 Python/Anaconda 窗口,使用以下代码启动训练:
mlagents-learn config/trainer_config.yaml --run-id=vishall --train --save-freq=10000 --load
-
之前的代码不是打印错误;它是我们用来运行 VisualHallway 示例的完全相同的命令,只是在末尾加上了
--load。这应该会启动训练并提示你运行编辑器。 -
随时可以运行训练,只要你喜欢,但请记住,我们几乎没有训练原始代理。
现在,在这个示例中,即使我们已经训练了代理完成 VisualHallway,这可能也不太有效地将知识转移到 VisualPushBlock。为了这个示例,我们选择了这两个,因为它们非常相似,将一个训练好的大脑转移到另一个上要简单一些。对于你自己,能够转移训练过的大脑可能更多的是关于在新的或修改过的关卡上重新训练代理,甚至允许代理在逐渐更难的关卡上进行训练。
根据你使用的 ML-Agents 版本,这个示例可能效果不一。具体问题在于模型的复杂性、超参数的数量、输入空间以及我们正在运行的奖励系统。保持这些因素一致也需要非常注意细节。在接下来的章节中,我们将稍作偏离,探讨这些模型的复杂性。
探索 TensorFlow 检查点
TensorFlow 正迅速成为支撑大多数深度学习基础设施的底层图计算引擎。尽管我们没有详细介绍这些图引擎是如何构建的,但从视觉上查看这些 TensorFlow 模型是很有帮助的。我们不仅能更好地理解这些系统的复杂性,而且一个好的图像往往胜过千言万语。让我们打开浏览器,进行下一个练习:
-
使用你最喜欢的搜索引擎在浏览器中搜索短语
netron tensorflow。Netron 是一个开源的 TensorFlow 模型查看器,完美符合我们的需求。 -
找到指向 GitHub 页面的链接,在页面中找到下载二进制安装程序的链接。选择适合你平台的安装程序并点击下载。这将带你到另一个下载页面,你可以选择下载的文件。
-
使用适合你平台的安装程序安装 Netron 应用程序。在 Windows 上,下载安装 exe 安装程序并运行即可。
-
运行 Netron 应用程序,启动后,你将看到以下内容:

Netron 应用程序
-
点击窗口中间的“打开模型...”按钮
-
使用文件资源管理器定位到
ML-Agents/ml-agents/models/vishall-0\VisualHallwayLearning文件夹,并找到raw_graph.def文件,如下图所示:

选择要加载的模型图定义
- 加载图形后,使用右上角的 - 按钮将视图缩放到最大,类似于以下截图:

我们代理大脑的 TensorFlow 图模型
-
如插图所示,这个图形极其复杂,我们不太可能轻易理解它。然而,浏览并观察这个模型/图形是如何构建的还是很有趣的。
-
向图表顶部滚动,找到一个名为 advantages 的节点,然后选择该节点,并查看图表和输入的模型属性,如下图所示:

优势图模型的属性
- 在这个模型的属性视图中,你应该能够看到一些非常熟悉的术语和设置,例如 visual_observation_0,它显示该模型输入是形状为 [84,84,3] 的张量。
完成后,可以随意查看其他模型,甚至探索 Unity 之外的其他模型。虽然这个工具还不完全能够总结像我们这样的复杂模型,但它展示了这些工具变得越来越强大的潜力。更重要的是,如果你能找到合适的方式,你甚至可以导出变量以供以后检查或使用。
模仿迁移学习
模仿学习的一个问题是,它常常将代理引导到一个限制其未来可能行动的路径上。这和你被教导错误的方式执行任务,然后按照那个方式做,可能没有多想,最后才发现其实有更好的方法并无太大区别。事实上,人类在历史上一直容易犯这种问题。也许你小时候学到吃完饭后游泳很危险,但后来通过自己的实验,或只是凭常识,你才知道那只是个神话,这个神话曾经被认为是事实很长时间。通过观察训练代理也没有什么不同,它会限制代理的视野,将其狭隘化,只能局限于它所学的内容。然而,有一种方法可以让代理回到部分的蛮力或试错探索,从而扩展它的训练。
使用 ML-Agents,我们可以将 IL 与某种形式的迁移学习结合起来,让代理先通过观察学习,然后通过从曾经的学生身上继续学习进一步训练。这种 IL 链式学习,若你愿意,可以让你训练一个代理来自动训练多个代理。让我们打开 Unity,进入 TennisIL 场景,并按照下一个练习操作:
- 选择 TennisArea | Agent 对象,在检查器中禁用 BC Teacher Helper 组件,然后添加一个新的演示记录器,如下图所示:

检查 BC Teacher 是否附加到代理上
-
BC Teacher Helper 是一个记录器,功能与演示记录器类似。BC 记录器允许你在代理运行时打开和关闭录制,非常适合在线训练,但在编写本文时,该组件无法使用。
-
确保 Academy 设置为控制 TennisLearning 大脑。
-
保存场景和项目。
-
打开一个 Python/Anaconda 窗口,并使用以下命令启动训练:
mlagents-learn config/online_bc_config.yaml --run-id=tennis_il --train --slow
-
当提示时点击播放,在编辑器中运行游戏。使用 W, A, S, D 键控制蓝色球拍,并玩几秒钟来热身。
-
热身后,按 R 键开始录制演示观察。玩几分钟游戏,让智能体变得更有能力。在智能体能够回球后,停止训练。
这不仅会训练智能体,效果很好,而且还会创建一个演示录制回放,我们可以用它进一步训练智能体,让它们学习如何像 AlphaStar 训练一样互相对战。接下来,我们将在下一个部分设置我们的网球场景,以便在离线训练模式下运行多个智能体。
使用一个演示训练多个智能体
现在,通过录制我们打网球的过程,我们可以将此录制用于训练多个智能体,所有智能体的反馈都汇入一个策略。打开 Unity 到网球场景,即那个拥有多个环境的场景,并继续进行下一个练习:
- 在层级窗口的过滤栏中输入
agent,如以下截图所示:

搜索场景中的所有智能体
-
选择场景中所有智能体对象,并批量更改它们的大脑,使用 TennisLearning 而不是 TennisPlayer。
-
选择 Academy 并确保启用它以控制智能体大脑。
-
打开
config/offline_bc_config.yaml文件。 -
在底部为
TennisLearning大脑添加以下新部分:
TennisLearning:
trainer: offline_bc
max_steps: 5.0e5
num_epoch: 5
batch_size: 64
batches_per_epoch: 5
num_layers: 2
hidden_units: 128
sequence_length: 16
use_recurrent: true
memory_size: 256
sequence_length: 32
demo_path: ./UnitySDK/Assets/Demonstrations/TennisAgent.demo
-
保存场景和项目。
-
打开 Python/Anaconda 窗口并使用以下代码进行训练:
mlagents-learn config/offline_bc_config.yaml --run-id=tennis_ma --train
-
你可能希望添加
--slow参数来观看训练过程,但这不是必需的。 -
让智能体训练一段时间,并注意其进步。即使只有短暂的观察录制输入,智能体也能很快成为一个有能力的玩家。
有多种方法可以执行这种类型的 IL 和迁移学习链,使智能体在训练时具有一定的灵活性。你甚至可以不使用 IL,而是直接使用已训练的模型的检查点,并像我们之前那样通过迁移学习来运行智能体。可能性是无限的,最终哪些做法会成为最佳实践仍然有待观察。
在下一个部分,我们将提供一些练习,你可以用它们来进行个人学习。
练习
本章结尾的练习可能会提供数小时的乐趣。尽量只完成一到两个练习,因为我们还需要完成本书:
-
设置并运行 PyramidsIL 场景以进行在线 IL 训练。
-
设置并运行 PushBlockIL 场景以进行在线 IL 训练。
-
设置并运行 WallJump 场景以进行在线 IL 训练。这需要你修改场景。
-
设置并运行 VisualPyramids 场景以使用离线录制。录制训练过程,然后训练智能体。
-
设置并运行 VisualPushBlock 场景以使用离线录制。使用离线 IL 训练智能体。
-
设置 PushBlockIL 场景以录制观察演示。然后,使用此离线训练来训练多个代理在常规的 PushBlock 场景中。
-
设置 PyramidsIL 场景以录制演示。然后,使用该数据进行离线训练,以训练多个代理在常规的 Pyramids 场景中。
-
在 VisualHallway 场景中训练一个代理,使用任何你喜欢的学习方式。训练后,修改 VisualHallway 场景,改变墙壁和地板的材质。在 Unity 中更改物体材质非常容易。然后,使用交换模型检查点的技术,将之前训练好的大脑迁移到新环境中。
-
完成第八个练习,但使用 VisualPyramids 场景。你也可以在此场景中添加其他物体或方块。
-
完成第八个练习,但使用 VisualPushBlock 场景。尝试添加其他方块或代理可能需要避开的其他物体。
只需记住,如果你正在尝试任何迁移学习练习,匹配复杂图表时要特别注意细节。在下一节中,我们将总结本章所涵盖的内容。
摘要
在本章中,我们介绍了一种新兴的强化学习技术,叫做模仿学习(Imitation Learning)或行为克隆(Behavioral Cloning)。正如我们所学,这种技术通过捕捉玩家玩游戏时的观察数据,然后在在线或离线环境中使用这些观察数据进一步训练代理。我们还了解到,IL 只是迁移学习的一种形式。接着,我们介绍了使用 ML-Agents 的一种技术,它可以让你在不同的环境中迁移大脑。最后,我们探讨了如何将 IL 和迁移学习结合起来,作为激励代理自主开发新策略的训练方法。
在下一章中,我们将通过研究多个代理训练场景,进一步加深对游戏中深度强化学习(DRL)的理解。
第十一章:构建多代理环境
在完成单代理的经验后,我们可以进入更加复杂但同样有趣的多代理环境中,在该环境中训练多个代理以合作或竞争的方式工作。这也为训练具有对抗性自我对抗、合作性自我对抗、竞争性自我对抗等的新机会打开了大门。在这里,可能性变得无穷无尽,这也许就是 AI 的真正“圣杯”。
在本章中,我们将介绍多代理训练环境的多个方面,主要的章节主题如下所示:
-
对抗性和合作性自我对抗
-
竞争性自我对抗
-
多大脑游戏
-
通过内在奖励增加个体性
-
个体化的外部奖励
本章假设你已经完成了前三章并做了一些练习。在下一节中,我们将开始介绍各种自我对抗场景。
最好从 ML-Agents 仓库的一个新克隆开始本章内容。我们这样做是为了清理我们的环境,确保没有不小心保存的错误配置。如果你需要帮助,可以查阅前面的章节。
对抗性和合作性自我对抗
术语自我对抗当然对不同的人来说有不同的含义,但在此案例中,我们的意思是大脑通过操控多个代理来与自己进行竞争(对抗性)或合作。在 ML-Agents 中,这可能意味着一个大脑在同一环境中操控多个代理。在 ML-Agents 中有一个很好的示例,所以打开 Unity 并按照下一个练习准备好这个场景以进行多代理训练:
-
从 Assets | ML-Agents | Examples | Soccer | Scenes 文件夹中打开 SoccerTwos 场景。该场景默认设置为玩家模式运行,但我们需要将其转换回学习模式。
-
选择并禁用所有 SoccerFieldTwos(1)到 SoccerFieldTwos(7)区域。我们暂时不使用这些区域。
-
选择并展开剩余的活动 SoccerFieldTwos 对象。这将显示一个包含四个代理的游戏区,其中两个标记为 RedStriker 和 BlueStriker,另外两个标记为 RedGoalie 和 BlueGoalie。
-
检查代理并将每个代理的大脑设置为 StrikerLearning 或 GoalieLearning,具体设置请参见下图:

在代理上设置学习大脑
- 在这个环境中,我们有四个代理,由大脑控制,这些大脑既在合作又在竞争。说实话,这个示例非常出色,极好地展示了合作性和竞争性自我对抗的概念。如果你还在努力理解一些概念,可以参考这个图示,它展示了如何将这些内容结合起来:

SoccerTwos 的大脑架构
-
如我们所见,我们有两个大脑控制四个代理:两个前锋和两个守门员。前锋的任务是进攻守门员,当然,守门员的任务是防守进球。
-
选择“Academy”并启用“Soccer Academy | Brains | Control”以控制两个大脑,如下所示:

在 Academy 中设置大脑控制
- 同时,注意一下“Striker”、“Goalie Reward”和“Punish”设置,这些都位于“Soccer Academy”组件的底部。还需要注意的是,每个大脑的
reward函数是如何运作的。以下是该示例中reward函数的数学描述:




-
这意味着,当进球时,每个四个代理会根据其位置和队伍获得奖励。因此,如果红队进球,红队的前锋将获得
+1奖励,蓝队前锋将获得-0.1奖励,红队守门员将获得+0.1奖励,而可怜的蓝队守门员将获得-1奖励。现在,你可能会认为这可能会导致重叠,但请记住,每个代理对一个状态或观察的看法是不同的。因此,奖励将根据该状态或观察应用于该代理的策略。实质上,代理正在基于其当前对环境的看法进行学习,这种看法会根据哪个代理发送该观察而变化。 -
编辑完毕后保存场景和项目。
这为我们的场景设置了多代理训练,使用两个大脑和四个代理,既包括竞争性又包括合作性的自我对战。在接下来的部分中,我们完成外部配置并开始训练场景。
训练自我对战环境
训练这种类型的自我对战环境不仅为增强训练提供了更多可能性,还为有趣的游戏环境开辟了新的可能性。在某些方面,这种类型的训练环境看起来和观看游戏一样有趣,正如我们将在本章结束时所看到的。
但现在,我们将回到前面,继续设置我们需要的配置,以便在下一步的练习中训练我们的 SoccerTwos 多代理环境:
- 打开
ML-Agents/ml-agents/config/trainer_config.yaml文件,查看StrikerLearning和GoalieLearning配置部分,如下所示:
StrikerLearning:
max_steps: 5.0e5
learning_rate: 1e-3
batch_size: 128
num_epoch: 3
buffer_size: 2000
beta: 1.0e-2
hidden_units: 256
summary_freq: 2000
time_horizon: 128
num_layers: 2
normalize: false
GoalieLearning:
max_steps: 5.0e5
learning_rate: 1e-3
batch_size: 320
num_epoch: 3
buffer_size: 2000
beta: 1.0e-2
hidden_units: 256
summary_freq: 2000
time_horizon: 128
num_layers: 2
normalize: false
-
显而易见的想法是大脑应该有类似的配置,你可能会从这种方式开始,没错。然而,请注意,即使在这个示例中,
batch_size参数对于每个大脑也设置得不同。 -
打开 Python/Anaconda 窗口,切换到 ML-Agents 虚拟环境,然后从
ML-Agents/ml-agents文件夹中启动以下命令:
mlagents-learn config/trainer_config.yaml --run-id=soccer --train
- 当提示时按下播放,你应该能看到以下训练会话正在运行:

在训练模式下运行的 SoccerTwos 场景
-
如前所述,这可以是一个非常有趣的示例,观看时很有娱乐性,并且训练速度惊人地快。
-
在进行了一些训练后,打开 Python/Anaconda 控制台,并注意到现在你得到了两个大脑的统计信息,分别是 StrikerLearning 和 GoalieLearning,如下图所示:

控制台输出显示来自两个大脑的统计信息
-
注意到 StrikerLearning 和 GoalieLearning 互相返回相反的奖励。这意味着,为了训练这些代理,它们必须使两者的平均奖励都平衡到 0。当代理们进行训练时,你会注意到它们的奖励开始收敛到 0,这是这个示例的最佳奖励。
-
让示例运行至完成。观看这些环境时很容易迷失其中,因此你甚至可能没有注意到时间流逝。
这个示例展示了我们如何通过自我游戏利用多代理训练的力量,同时教两个大脑如何同时进行竞争和合作。在接下来的部分,我们将看看多个代理如何在自我游戏中相互竞争。
对抗性自我游戏
在前面的示例中,我们看到了一个既有合作又有竞争的自我游戏示例,其中多个代理几乎是共生地运作的。虽然这是一个很好的示例,但它仍然将一个大脑的功能与另一个大脑通过奖励函数联系起来,因此我们观察到代理们几乎处于奖励对立的情境中。相反,我们现在想要查看一个能够仅通过对抗性自我游戏来训练大脑与多个代理的环境。当然,ML-Agents 就有这样一个环境,称为 Banana,它包括几个随机游走在场景中并收集香蕉的代理。这些代理还有一个激光指示器,如果击中对手,可以使其禁用几秒钟。接下来的练习中,我们将查看这个场景:
-
打开位于 Assets | ML-Agents | Examples | BananaCollectors | Scenes 文件夹中的 Banana 场景。
-
选择并禁用额外的训练区域 RLArea(1) 到 RLArea(3)。
-
选择 RLArea 中的五个代理(Agent、Agent(1)、Agent(2)、Agent(3)、Agent(4))。
-
将 Banana Agent | Brain 从 BananaPlayer 切换到 BananaLearning。
-
选择学院并将 Banana Academy | Brains | Control 属性设置为启用。
-
在编辑器中选择 Banana Agent 组件(脚本),并在你选择的代码编辑器中打开。如果你向下滚动到页面底部,你会看到
OnCollisionEnter方法,如下所示:
void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.CompareTag("banana"))
{
Satiate();
collision.gameObject.GetComponent<BananaLogic>().OnEaten();
AddReward(1f);
bananas += 1;
if (contribute)
{
myAcademy.totalScore += 1;
}
}
if (collision.gameObject.CompareTag("badBanana"))
{
Poison();
collision.gameObject.GetComponent<BananaLogic>().OnEaten();
AddReward(-1f);
if (contribute)
{
myAcademy.totalScore -= 1;
}
}
}
- 阅读上述代码后,我们可以将
reward函数总结为以下内容:


这仅仅意味着代理们只会因吃香蕉而获得奖励。有趣的是,禁用对手(使用激光或被禁用)并没有奖励。
-
保存场景和项目。
-
打开准备好的 Python/Anaconda 控制台,并使用以下命令开始训练:
mlagents-learn config/trainer_config.yaml --run-id=banana --train
- 当提示时,按下编辑器中的 Play 按钮,并观察接下来截图中展示的动作:

香蕉收集器代理正在执行任务
- 让场景运行尽可能长的时间。
这个场景是一个很好的例子,展示了代理如何学习使用一个不返回奖励的次要游戏机制,但像激光一样,它仍然被用来使对抗性的收集者无法动弹,从而获得更多的香蕉,同时仅仅因吃香蕉才获得奖励。这个例子展示了强化学习(RL)的真正力量,以及如何利用它来发现次要策略以解决问题。虽然这是一个非常有趣的方面,观看起来也很有趣,但请考虑其更深远的影响。研究表明,RL 已经被用来优化从网络到推荐系统的所有内容,通过对抗性自我游戏,因此未来看强化学习这种学习方法能够达成什么目标将会非常有趣。
多脑模式游戏
ML-Agents 工具包的一个真正伟大的特点是能够快速添加由多个大脑驱动的多个代理。这使我们能够构建更复杂的游戏环境或场景,拥有有趣的代理/人工智能,既可以与其互动也可以对抗。让我们看看将我们的足球示例转换为让所有代理使用独立大脑是多么容易:
-
打开我们之前查看的 SoccerTwos 场景的编辑器。
-
定位到示例中的
Brains文件夹,路径为 Assets | ML-Agents | Examples | Soccer | Brains。 -
点击窗口右上角的 Create 菜单,在上下文菜单中选择 ML-Agents | Learning Brain:

创建一个新的学习大脑
-
将新大脑命名为
RedStrikerLearning。在同一文件夹中创建三个新的大脑,分别命名为RedGoalieLearning、BlueGoalieLearning和BlueStrikerLearning。 -
选择 RedStrikerLearning。然后选择并拖动 StrikerLearning 大脑,将其放入“从槽复制大脑参数”位置:

从另一个大脑复制大脑参数
-
对于 BlueStrikerLearning,复制 StrikerLearning 的参数。然后对 RedGoalieLearning 和 BlueGoalieLearning 执行相同操作,复制 GoalieLearning 的参数。
-
在 Hierarchy 窗口中选择 RedAgent,并将 Agent Soccer | Brain 设置为 RedStrikerLearning。对其他每个代理执行相同操作,将颜色与位置匹配。BlueGoalie -> BlueGoalieLearning。
-
选择 Academy,并从 Soccer Academy | Brains 列表中移除当前所有的大脑。然后使用添加新按钮将所有我们刚创建的新大脑添加回列表,并设置为控制:

将新的大脑添加到 Academy
-
保存场景和项目。现在,我们只是将示例从使用两个并行大脑的自我游戏模式切换为让代理们分别在不同的队伍中。
-
打开一个设置好用于训练的 Python/Anaconda 窗口,并用以下内容启动:
mlagents-learn config/trainer_config.yaml --run-id=soccer_mb --train
- 让训练运行并注意观察代理的表现,看看他们是否像之前一样发挥得那么好。同时也查看控制台输出。你会看到现在它为四个代理提供报告,但代理之间仍然有些共生关系,因为红色前锋与蓝色守门员相对。然而,现在他们的训练速度要慢得多,部分原因是每个大脑现在只能看到一半的观察数据。记得之前我们有两个前锋代理将数据输入到一个大脑,而正如我们所学,这种额外的状态输入可以显著加速训练。
此时,我们有四个代理,四个独立的大脑正在进行一场足球比赛。当然,由于代理仍通过共享奖励函数进行共生式训练,我们不能真正把它们称为独立个体。除了,如我们所知,队伍中的个体往往会受到其内在奖励系统的影响。我们将在下一部分中查看内在奖励的应用如何使最后的这个练习更加有趣。
通过内在奖励增加个体性
正如我们在第九章《奖励与强化学习》中学到的,内在奖励系统和代理动机的概念目前在 ML-Agents 中只是作为好奇心学习实现的。应用内在奖励或动机与强化学习结合的这一领域,在游戏和人际应用中有广泛的应用,例如仆人代理。
在下一个练习中,我们将为一些代理添加内在奖励,并观察这对游戏产生什么影响。打开上一个练习的场景并按以下步骤操作:
-
打开
ML-Agents/ml-agents/config/trainer_config.yaml文件,用文本编辑器进行编辑。我们之前没有为我们的代理添加任何专门的配置,但现在我们将纠正这一点并添加一些额外的配置。 -
将以下四个新的大脑配置添加到文件中:
BlueStrikerLearning:
max_steps: 5.0e5
learning_rate: 1e-3
batch_size: 128
num_epoch: 3
buffer_size: 2000
beta: 1.0e-2
hidden_units: 256
summary_freq: 2000
time_horizon: 128
num_layers: 2
normalize: false
BlueGoalieLearning:
use_curiosity: true
summary_freq: 1000
curiosity_strength: 0.01
curiosity_enc_size: 256
max_steps: 5.0e5
learning_rate: 1e-3
batch_size: 320
num_epoch: 3
buffer_size: 2000
beta: 1.0e-2
hidden_units: 256
time_horizon: 128
num_layers: 2
normalize: false
RedStrikerLearning:
use_curiosity: true
summary_freq: 1000
curiosity_strength: 0.01
curiosity_enc_size: 256
max_steps: 5.0e5
learning_rate: 1e-3
batch_size: 128
num_epoch: 3
buffer_size: 2000
beta: 1.0e-2
hidden_units: 256
time_horizon: 128
num_layers: 2
normalize: false
RedGoalieLearning:
max_steps: 5.0e5
learning_rate: 1e-3
batch_size: 320
num_epoch: 3
buffer_size: 2000
beta: 1.0e-2
hidden_units: 256
summary_freq: 2000
time_horizon: 128
num_layers: 2
normalize: false
-
注意我们已经在
BlueGoalieLearning和RedStrikerLearning大脑上启用了use_curiosity: true。你可以从文件中原有的GoalieLearning和StrikerLearning大脑配置中复制并粘贴大部分内容;只需注意细节即可。 -
编辑完成后保存文件。
-
打开你的 Python/Anaconda 控制台并使用以下命令开始训练:
mlagents-learn config/trainer_config.yaml --run-id=soccer_icl --train
- 让代理训练一段时间,你会注意到,尽管它们确实表现得像更独立的个体,但它们的训练能力仍然较差,任何在训练中看到的进步很可能是因为给了几个代理好奇心奖励。
通过内在奖励或动机为代理添加个性化的能力,随着深度强化学习(DRL)在游戏和其他潜在应用中的发展,肯定会逐渐成熟,并希望能够提供其他不完全专注于学习的内在奖励模块。然而,内在奖励确实能鼓励个性化,因此,在下一节中,我们将为修改后的示例引入外在奖励。
迁移学习的另一个优秀应用是,在代理已经完成一般任务的训练后,能够添加内在奖励模块。
个性化的外在奖励
我们已经在多个章节中广泛讨论了外部或外在奖励,以及如何使用技术来优化和鼓励它们对代理的作用。现在,看似通过修改代理的行为来调整其外在奖励或本质上的奖励函数,似乎是一种简便的方法。然而,这可能会带来困难,并且通常会导致训练表现变差,这就是我们在前一节中为几个代理添加课程学习(CL)时所观察到的情况。当然,即使训练变差,我们现在手头上有许多技巧,比如迁移学习(TL),也叫做模仿学习(IL);好奇心;和 CL,来帮助我们纠正问题。
在接下来的练习中,我们将通过添加额外的外在奖励来为我们的代理增添更多个性。打开我们刚才正在操作的前一个练习示例并跟着做:
-
从菜单中选择窗口 | 资产商店。这将带你进入 Unity 资产商店,这是一个非常优秀的辅助资源库。虽然大多数这些资源是付费的,但老实说,与同类开发者工具相比,价格非常低廉,而且有几个免费的、非常优秀的资源,你可以开始使用它们来增强你的训练环境。资产商店是 Unity 最好与最糟糕的地方之一,所以如果你购买了资源,记得查看评论和论坛帖子。任何好的资源通常都会有自己的开发者论坛,而艺术资源则较少。
-
在搜索栏中输入
toony tiny people并按 Enter 键或点击搜索按钮。这将显示搜索结果。
我们要感谢Polygon Blacksmith,感谢他们的支持,使我们能够将他们的 Toony Tiny People Demo 资源与本书的源文件一起分发。此外,他们的角色资源包做得非常好,且易于使用。如果你决定构建一个完整的游戏或增强版演示,他们的一些较大资源包的价格也是一个很好的起点。
- 选择名为 Toony Tiny People Demo 的结果,由 Polygon Blacksmith 提供,并点击选择它。它将显示在此截图中:

Polygon Blacksmith 的 Toony Tiny People Demo 资源
- 点击红色的下载按钮,下载完成后,按钮会变为导入,如前面的截图所示。点击导入按钮导入资产。当导入对话框弹出时,确保选中所有内容,然后点击导入。
这些类型的低多边形或卡通资产非常适合让简单的游戏或模拟更具娱乐性和观看乐趣。虽然看起来不多,但你可以花费大量时间观看这些训练模拟的运行,若它们看起来更吸引人,那将大有帮助。
-
选择并展开层级中的所有代理对象。这包括 RedStriker、BlueStriker、RedGoalie 和 BlueGoalie。
-
打开项目窗口中的 Assets | TooyTinyPeople | TT_demo | prefabs 文件夹。
-
从前面的文件夹中选择并拖动 TT_demo_Female 预设体,并将其拖放到层级窗口中的 RedStriker 代理对象上。选择位于代理对象下方的立方体对象,并在检查器中禁用它。继续按以下列表对其他代理执行相同操作:
-
TT_demo_female -> RedStriker
-
TT_demo_male_A -> BlueStriker
-
TT_demo_police -> BlueGoalie
-
TT_demo_zombie -> RedGoalie
-
这一点在下图中得到了进一步展示:

设置新的代理模型
- 确保也将新代理模型的 Transform 位置和朝向重置为
[0,0,0],如以下截图所示:

重置拖动的预设体的朝向和位置
- 保存场景和项目。
此时,你可以在训练中运行场景,观看新的代理模型移动,但这并没有多大意义。代理的行为仍然是一样的,所以接下来我们需要做的是基于某些任意的个性设置额外的外在奖励,我们将在下一节定义这些个性。
通过自定义奖励函数创建独特性
尽管结果可能没有我们预期的那样独特,我们通过加入内在奖励成功地使我们的代理变得更具个性。这意味着我们现在希望通过修改代理的外在奖励来使其行为更具个性,最终使游戏更具娱乐性。
我们开始实现这一点的最佳方式是查看我们之前描述的 SoccerTwos 奖励函数;这些奖励函数在这里列出,供参考:




我们现在想做的是基于当前角色对奖励函数进行一些个性化修改。我们将通过简单地将函数链与基于角色类型的修改进行组合来实现,如下所示:
或 
或 
或 
或 
我们在这些奖励函数中所做的只是通过某些个性修改来调整奖励值。对于女孩,我们给她 1.25 倍的奖励,反映出她可能很兴奋。男孩则不那么兴奋,因此我们将他的奖励调整为 0.95 倍,稍微减少奖励。警察则始终冷静且掌控自如,奖励保持不变。最后,我们引入了一个变数——半死的僵尸。为了表现它是半死不活,我们还将它的奖励减少一半。
当然,你可以根据游戏机制修改这些函数,但需要注意的是,你所应用的个性修改可能会妨碍训练。在我们开始训练这个示例时,务必记住这一点。
一个女孩,一个男孩,一个僵尸和一个警察走进了足球场。
现在我们理解了新的奖励函数,我们想要在示例中添加一些内容,说明是时候打开 Unity 并编写代码了。这个示例将需要对 C# 文件做一些轻微的修改,但代码非常简单,任何有 C 语言经验的程序员都应该能轻松理解。
打开 Unity,进入我们在上一个示例中修改的场景,并跟随下一个练习:
-
在层级窗口中找到 RedStriker 代理并选择它。
-
从 Inspector 面板中,点击 Agent Soccer 组件旁边的齿轮图标,然后在上下文菜单中选择“编辑脚本”。这将会在你的编辑器中打开脚本和解决方案。
-
在文件顶部的当前
enum AgentRole后添加一个新的enum,名为PersonRole,如代码所示:
public enum AgentRole
{
striker,goalie
} *//after this line*
public enum PersonRole
{
girl, boy, police, zombie
}
-
这创建了一个新的角色,实际上是我们希望应用到每个大脑的个性。
-
向类中添加另一个新变量,如下所示:
public AgentRole agentRole; *//after this line*
public PersonRole playerRole;
- 这将
PersonRole新角色添加到代理中。现在,我们还想通过向InitializeAgent方法添加一行代码来将新类型添加到设置中,如下所示:
public override void InitializeAgent()
{
base.InitializeAgent();
agentRenderer = GetComponent<Renderer>();
rayPer = GetComponent<RayPerception>();
academy = FindObjectOfType<SoccerAcademy>();
PlayerState playerState = new PlayerState();
playerState.agentRB = GetComponent<Rigidbody>();
agentRB = GetComponent<Rigidbody>();
agentRB.maxAngularVelocity = 500;
playerState.startingPos = transform.position;
playerState.agentScript = this;
area.playerStates.Add(playerState);
playerIndex = area.playerStates.IndexOf(playerState);
playerState.playerIndex = playerIndex;
playerState.personRole = personRole; *//add this line*
}
- 你现在可能会看到一行错误。这是因为我们还需要将新的
personRole属性添加到PlayerState中。打开PlayerState类并按如下所示添加属性:
[System.Serializable]
public class PlayerState
{
public int playerIndex;
public Rigidbody agentRB;
public Vector3 startingPos;
public AgentSoccer agentScript;
public float ballPosReward;
public string position;
public AgentSoccer.PersonRole personRole { get; set; } *//add me*
}
- 你现在应该已经进入了
SoccerFieldArea.cs文件。滚动到RewardOrPunishPlayer方法,并按如下所示修改:
public void RewardOrPunishPlayer(PlayerState ps, float striker, float goalie)
{
if (ps.agentScript.agentRole == AgentSoccer.AgentRole.striker)
{
RewardOrPunishPerson(ps, striker); *//new line*
}
if (ps.agentScript.agentRole == AgentSoccer.AgentRole.goalie)
{
RewardOrPunishPerson(ps, striker); *//new line*
}
ps.agentScript.Done(); //all agents need to be reset
}
- 我们在这里做的是注入另一个奖励函数,
RewardOrPunishPerson,以便添加我们外部的个性奖励。接下来,添加一个新的RewardOrPunishPerson方法,如下所示:
private void RewardOrPunishPerson(PlayerState ps, float reward)
{
switch (ps.personRole)
{
case AgentSoccer.PersonRole.boy:
ps.agentScript.AddReward(reward * .95f);
break;
case AgentSoccer.PersonRole.girl:
ps.agentScript.AddReward(reward*1.25f);
break;
case AgentSoccer.PersonRole.police:
ps.agentScript.AddReward(reward);
break;
case AgentSoccer.PersonRole.zombie:
ps.agentScript.AddReward(reward * .5f);
break;
}
}
- 这段代码的功能与我们之前定制的奖励函数完全相同。编辑完成后,保存所有文件并返回到 Unity 编辑器。如果有任何错误或编译警告,它们将在控制台中显示。如果需要返回并修复任何(红色)错误,进行修正即可。
如你所见,凭借很少的代码,我们就能够添加外在的个性奖励。当然,你可以以任何方式增强这个系统,甚至让它更通用、参数化。在接下来的部分,我们将把所有这些内容整合起来,开始训练我们的代理。
配置代理的个性
所有代码设置好后,我们现在可以继续回到编辑器,设置代理以匹配我们想要应用的个性。再次打开编辑器,按照接下来的练习将个性应用到代理上并开始训练:
- 在层级视图中选择 RedStriker,并将我们刚刚创建的 Agent Soccer | Person Role 参数设置为 Girl,如下所示:

为每个代理设置个性
-
更新所有代理,使其具备与我们之前分配的模型匹配的相关个性:BlueStriker -> 男孩,BlueGoalie -> 警察,RedGoalie -> 僵尸,如前面的截图所示。
-
保存场景和项目。
-
现在,在这一点上,如果你希望更详细些,你可能想回去更新每个代理的大脑名称以反映它们的个性,比如 GirlStrikerLearning 或 PoliceGoalieLearning,并且可以省略团队颜色。务必将新的大脑配置设置添加到你的
trainer_config.yaml文件中。 -
打开你的 Python/Anaconda 训练控制台,并使用以下命令开始训练:
mlagents-learn config/trainer_config.yaml --run-id=soccer_peeps --train
- 现在,这会非常有趣,正如你在下面的截图中看到的:

观看不同个性在踢足球
-
请注意,我们保留了团队颜色的立方体,以显示每个代理所属的团队。
-
让代理训练几千次迭代后,再打开控制台;注意代理们现在看起来不再那么共生了。在我们的示例中,它们仍然是成对出现的,因为我们仅对奖励应用了简单的线性变换。当然,你也可以应用更复杂的非线性函数,这些函数不是反相关的,可以描述代理的其他动机或个性。
-
最后,让我们打开 TensorBoard,查看我们多代理训练的更好比较。在你当前工作的
ML-Agents/ml-agents文件夹中,再次打开一个 Python/Anaconda 控制台,并运行以下命令:
tensorboard --logdir=summaries
- 使用浏览器打开 TensorBoard 界面并检查结果。确保禁用所有额外的结果,只专注于我们当前训练中四个大脑的表现。我们要关注的三个主要图表已合并在这个图示中:

TensorBoard 绘图,显示四个大脑训练的结果
从 TensorBoard 的结果中可以看出,代理的训练效果不佳。我们当然可以通过增加额外的训练区域并提供更多的观测值来改善这一点,从而训练策略。然而,如果你查看策略损失图,结果表明,代理之间的竞争导致了最小的策略变化,这在训练初期是一个不好的现象。如果有的话,僵尸代理似乎是从这些结果中学到最多的代理。
当然,你可以通过很多其他方式修改外部奖励函数,以鼓励在多智能体训练场景中某些行为方面的表现。这些技术中有些效果很好,有些则效果不佳。我们仍处于开发这项技术的初期阶段,最佳实践仍在逐步形成。
在下一部分中,我们将探讨你可以做的进一步练习,以巩固我们在本章中所涵盖的所有内容。
练习
和往常一样,尝试至少一个或两个以下练习,来获得个人的乐趣和学习:
-
打开 BananaCollectors 示例中的 Banana 场景,并在训练模式下运行。
-
修改 BananaCollectors | Banana 场景,使其使用五个独立的学习大脑,然后在训练模式下运行。
-
修改最后一个 SoccerTwos 练习中的奖励函数,使用指数或对数函数。
-
修改最后一个 SoccerTwos 练习中的奖励函数,使用非逆相关和非线性函数。这样,正负奖励的调整方式对于每个个性来说都是不同的。
-
修改 SoccerTwos 场景,使用不同的角色和个性。也要建立新的奖励函数,然后训练代理。
-
修改 BananaCollectors 示例中的 Banana 场景,使其使用与 SoccerTwos 示例相同的个性和自定义奖励函数。
-
用 BananaCollectors 示例做练习 3。
-
用 BananaCollectors 示例做练习 4。
-
用 BananaCollectors 示例做练习 5。
-
使用当前示例中的一个作为模板,或创建自己的,构建一个新的多智能体环境。这个最后的练习很有可能变成你自己的游戏。
你可能已经注意到,随着我们在书中的进展,练习变得更加耗时和困难。为了你自己的个人利益,请尽量完成至少几个练习。
总结
在本章中,我们探讨了多智能体训练环境中的无限可能性。我们首先研究了如何通过自我对弈设置环境,在这种环境中,一个大脑可以控制多个大脑,它们既相互竞争又相互合作。接着,我们探讨了如何通过使用 ML-Agents 好奇心学习系统,结合内在奖励的方式,增加个性化元素,激发智能体的好奇心。然后,我们研究了如何使用外在奖励来塑造智能体的个性并影响训练。我们通过添加免费的风格资产,并通过奖励函数链应用自定义的外部奖励来实现这一点。最后,我们训练了环境,并被男孩智能体彻底击败僵尸的结果逗乐;如果你观看完整的训练过程,你将看到这一点。
在下一章,我们将探讨深度强化学习(DRL)在调试和测试已构建游戏中的另一种新颖应用。
第三部分:构建游戏
在最后这一部分,我们将探讨深度学习目前如何在游戏中应用,并展望深度学习在游戏中的未来。
在本节中,我们将包括以下章节:
-
第十二章,使用 DRL 调试/测试游戏
-
第十三章,障碍塔挑战及其扩展
第十二章:使用 DRL 调试/测试游戏
虽然 ML-Agents 框架为构建游戏中的 AI 代理提供了强大的功能,但它也为调试和测试提供了自动化工具。任何复杂软件的开发都需要与广泛的产品测试和优秀的质量保证团队的审查相结合。测试每个方面、每种可能的组合和每个级别可能非常耗时且昂贵。因此,在本章中,我们将探讨使用 ML-Agents 作为自动化方式来测试一个简单的游戏。当我们修改或更改游戏时,我们的自动化测试系统可以通知我们是否存在问题或可能已经破坏了测试的变更。我们还可以进一步利用 ML-Agents,例如,评估训练性能。
以下是本章将涵盖内容的简要总结:
-
介绍游戏
-
设置 ML-Agents
-
重写 Unity 输入系统
-
通过模仿进行测试
-
分析测试过程
本章假设你对 ML-Agents 工具包有一定的了解,并且对 Unity 游戏引擎有一定的熟悉程度。你还应该对奖励函数以及如何使用 ML-Agents 进行模仿学习有较好的掌握。
在接下来的部分中,我们将从下载并导入游戏开始;我们将在接下来的部分中教你如何让 ML-Agents 玩游戏。即使是对于有经验的 Unity 用户来说,这一章也应视为进阶内容。因此,如果你对 Unity 和/或 C#相对较新,只需慢慢来,逐步完成练习。本章结束时,如果你完成了所有练习,你应该已经朝着成为 Unity 高手的方向迈进。
介绍游戏
我们将要介绍的游戏是一个免费的示范样本资产,它是典型游戏的优秀示例。我们测试的游戏将采用离散控制机制和第一人称视角,类似于我们过去看过的游戏。我们将在这里展示的技术是如何将游戏的控制器映射/破解到 ML-Agents 中,以便它可以由 ML-Agents 驱动。使用这种技术应该能让你将 ML-Agents 附加到任何现有的游戏中,尽管不同的控制器,比如第三人称或俯视视角,可能需要稍微调整方法。
如果你认为自己是有经验的 Unity 用户,并且有自己的项目使用 FPS 系统,那么你可以尝试将这个示例适应到自己的游戏或示例中。
由于某些被称为资源翻转的可疑技术,你通常很难找到好的 Unity 示范游戏项目。实际上,一些开发者会拿到一个示范项目,并快速为其换皮,作为他们自己的游戏进行转售。由于这一行为通常会给 Unity 这个优秀的游戏引擎带来负面影响,Unity 社区普遍对这种做法表示反对。这些快速制作的游戏通常质量很差,且没有任何支持,更不用说这些开发者通常仅使用免费许可证,这意味着这些设计不佳的游戏会标注上Made with Unity字样。
我们希望展示如何将 ML-Agents 集成到一个正在运行的游戏中,用于测试、调试和/或作为 AI 增强功能。让我们从导入基础项目并设置游戏在编辑器中运行开始。在此过程中,我们可能需要对一些内容进行调整,以确保一切正常运行,但这是我们的目标。打开 Unity 编辑器并按照下一部分中的练习设置基础游戏项目:
-
创建一个名为
HoDLG的新项目(或使用你自己喜欢的名字)。等待空项目加载完成。如果你觉得自己有足够资格,可以使用你自己的项目。 -
从菜单中选择Window | Asset Store。
-
在搜索面板中,输入
ms vehicle system并按Enter或点击Search按钮。我们将查看一个免费的资源包,名为 MS Vehicle System,它有一个有趣的小环境可以玩耍。通常,像这样的免费环境比较难找到(如前所述),但一般来说,制作精良的商业(非免费)资源包会提供良好的演示环境,比如这个。Unity 也有一些教程环境,但它们通常会迅速过时,而且更新也不一定容易。 -
点击MS Vehicle System卡片,等待资源页面加载,如下图所示:

选择要下载的资源包
-
点击下载按钮下载资源,然后点击导入将资源导入项目中。按照导入对话框的提示将所有资源导入项目中。
-
在Assets | MSVehicleSystem (FreeVersion) 文件夹中找到MainScene场景并打开它。
-
按下Play按钮在编辑器中运行场景,使用控制来驾驶车辆。注意如何切换车辆和相机控制。当测试(游戏)完成后,通过按下 Play 停止场景。
-
在Hierarchy筛选框中输入
canvas,然后选择场景中的所有Canvas对象,如下图所示:

禁用场景中的 Canvas UI
-
这将禁用场景中的 UI,我们在测试时不需要它,而且在这种情况下它并不重要。如果这是一个真正的游戏,可能会有更多颜色鲜艳的视觉效果来表示分数,当然,你也可以随时添加这些。
-
点击过滤器输入框旁的X,清除它并将场景恢复正常。
-
再次播放场景并探索多个区域。寻找一个你认为可能适合作为目标的地方;记住,最初不要设置太难的目标。以下是一个可能成为有趣目标的位置示例;看看你能否找到这个位置:

寻找适合放置目标的位置
即使你找不到具体的地方,也要找到一个不容易到达的区域。这样,代理必须广泛地探索该关卡才能找到目标(或目标点)。在我们的例子中,我们将随机放置目标方块在关卡中,并鼓励代理去寻找这些目标。这样,我们也可以通过探索的频率来绘制出探索的区域,然后决定如何覆盖其他区域进行测试。在进入下一部分之前,我们将添加 ML-Agents。
设置 ML-Agents
在写这本书时,ML-Agents 是作为一个 GitHub 项目进行开发和发布的。随着产品的成熟,可能会将其作为独立的资源包发布,但目前并不是这样。
因此,我们首先需要将 ML-Agents 导出为资源包。打开一个新的 Unity 编辑器会话,进入 ML-Agents 或 Unity SDK 项目,并按照以下步骤操作:
-
定位到项目窗口中的ML-Agents文件夹,并选择它。
-
从菜单中选择资源 | 导出包。
-
确保所有文件夹内容都已高亮显示,如下所示的导出包对话框摘录:

将 ML-Agents 导出为资源包
-
确保取消选中包括依赖项复选框,如上文摘录所示。只要选择了正确的根文件夹,所有我们需要的依赖项应该都会被打包。
-
在对话框中点击导出...按钮,然后选择并保存资产文件到一个你稍后容易找到的位置。
-
打开 Unity 编辑器,进入我们在上一个练习中开始的项目。
-
从菜单中选择资源 | 导入包 | 自定义包。定位我们刚刚导出的包,并将其导入到新的测试项目中。
-
定位到项目窗口,在资源根目录下创建一个名为
HoDLG的新文件夹,然后在该新文件夹内创建名为Brains、Prefabs、和Scripts的新文件夹,如下图所示:

创建新的项目文件夹
- 创建这些文件夹是为新的资源、示例或项目打基础的标准方式。现在你可以关闭旧的 ML-Agents Unity SDK 项目,因为我们不再需要它。
现在我们已经导入了 ML-Agents 并为测试游戏奠定了基础,接下来我们可以开始添加 ML-Agents 的学习部分进行测试。
向游戏中引入奖励
目前场景没有明确的目标。许多开放世界和探索类型的游戏目标定义较为松散。然而,对于我们的目的,我们只希望代理能够测试整个游戏关卡,并尽可能识别出任何游戏缺陷,或许还能发现一些我们从未预见的策略。当然,这并不意味着如果汽车驾驶代理变得足够强大,我们也能把它们作为游戏对手使用。底线是,我们的代理需要学习,而它通过奖励来实现这一点;因此,我们需要制定一些奖励函数。
让我们首先为目标定义一个奖励函数,如下所示:

这个过程非常简单;每当代理遇到目标时,它们将获得一个奖励值 1。为了避免代理花费过长时间,我们还会引入一个标准的步骤奖励,具体如下:

这意味着我们为每个代理的行动应用一个奖励,奖励值为-1 除以最大步骤数。这是相当标准的做法(例如我们的 Hallway 代理就使用了它),所以这里没有什么新东西。因此,我们的奖励函数将非常简单,这很好。
在许多情况下,您的游戏可能有明确的目标,您可以基于这些目标给予奖励。例如,一款驾驶游戏会有一个明确的目标,我们可以为代理设定目标。在这种情况下,在我们的开放世界游戏中,为代理设定目标是有意义的。当然,如何实现奖励结构非常重要,但请根据您的实际情况选择合适的方式。
在定义了奖励函数后,接下来是将目标的概念引入游戏中。我们希望保持这个系统的通用性,因此我们将在一个名为TestingAcademy的新对象中构建一个目标部署系统。这样,您可以将这个学院对象拖放到任何类似的 FPS 或第三人称控制的世界中,它就能正常工作。
第一人称射击游戏(FPS)指的是一种游戏类型,也是一种控制/摄像头系统。我们感兴趣的是后者,因为它是我们控制汽车的方式。
打开新的合并项目的编辑器,接着按照下一个练习来构建TestingAcademy对象:
-
在层级窗口中点击,然后从菜单中选择游戏对象 | 创建空物体。将新对象命名为
TestingAcademy。 -
找到并点击HoDLG | Scripts文件夹,然后在项目窗口中打开创建子菜单。
-
在创建菜单中,选择C# 脚本。将脚本重命名为
TestingAcademy。 -
打开新的TestingAcademy脚本并输入以下代码:
using MLAgents;
using UnityEngine;
namespace Packt.HoDLG
{
public class TestingAcademy : Academy
{
public GameObject goal;
public int numGoals;
public Vector3 goalSize;
public Vector3 goalCenter;
public TestingAgent[] agents;
public GameObject[] goals;
}
}
本章练习的所有代码都包含在Chapter_12_Code.assetpackage中,该包随书籍的源代码一同提供。
- 这段代码通过使用所需的命名空间来定义我们的类和导入内容。然后,我们定义了自己的命名空间
Packt.HoDLG,并且类继承自 ML-Agents 的基类Academy。接下来声明了几个变量来定义目标部署的立方体。可以把它想象成一个虚拟的空间立方体,用来生成目标。其基本思路是让物理引擎处理剩余部分,让目标直接掉到地面上。
命名空间在 Unity 中是可选的,但强烈建议将代码放入命名空间中,以避免大多数命名问题。如果你使用了很多资源或修改了现有资源,就像我们在这里所做的那样,这些问题会很常见。
- 接下来,我们将定义标准的
Academy类设置方法InitializeAcademy。该方法会自动调用,具体如下所示:
public override void InitializeAcademy()
{
agents = FindObjectsOfType<TestingAgent>();
goals = new GameObject[numGoals];
}
- 这个方法是作为 ML-Agents 设置的一部分被调用的,它实际上启动了整个 SDK。通过添加
Academy(TestingAcademy),我们实际上启用了 ML-Agents。接下来,我们将添加最后一个方法,该方法会在所有代理回合结束时重置学院,如下所示:
public override void AcademyReset()
{
if (goalSize.magnitude > 0)
{
for(int i = 0; i < numGoals; i++)
{
if(goals[i] != null && goals[i].activeSelf)
Destroy(goals[i]);
}
for(int i = 0; i < numGoals; i++)
{
var x = Random.Range(-goalSize.x / 2 + goalCenter.x, goalSize.x / 2 + goalCenter.x);
var y = Random.Range(-goalSize.y / 2 + goalCenter.y, goalSize.y / 2 + goalCenter.y);
var z = Random.Range(-goalSize.z / 2 + goalCenter.z, goalSize.z / 2 + goalCenter.z);
goals[i] = Instantiate(goal, new Vector3(x, y, z), Quaternion.identity, transform);
}
}
}
-
这段代码会随机生成目标,并将其放置在虚拟立方体的范围内。然而,在此之前,它首先使用
Destroy方法清除旧的目标。Destroy会将对象从游戏中移除。然后,代码再次循环并在虚拟立方体内的随机位置创建新的目标。实际创建目标的代码行被高亮显示,并使用了Instantiate方法。Instantiate会在指定的位置和旋转角度创建游戏中的对象。 -
保存文件并返回编辑器。此时不必担心任何编译错误。如果你是从头开始编写代码,可能会缺少一些类型,稍后我们会定义这些类型。
创建好新的TestingAcademy脚本后,我们可以继续在下一节中将该组件添加到游戏对象并设置学院。
设置 TestingAcademy
创建好TestingAcademy脚本后,接下来通过以下步骤将其添加到游戏对象中:
-
从脚本文件夹中拖动新的TestingAcademy脚本文件,并将其放到层次结构窗口中的TestingAcademy对象上。这会将该组件添加到对象中。在完成学院设置之前,我们还需要创建其他一些组件。
-
在层次结构窗口中点击,菜单中选择Game Object | 3D Object | Cube。将新对象重命名为
goal。 -
选择该对象并将Tag更改为
goal。然后,通过点击Target图标并选择v46或其他闪亮材质,来更换其材质,如下图所示:

更换目标对象的材质
-
在菜单中选择goal对象,接着选择Component | Physics | Rigidbody。这将添加一个名为 Rigidbody 的物理系统组件。通过将Rigidbody添加到对象上,我们允许它受物理系统的控制。
-
将goal对象拖放到Project窗口中的HoDLG | Prefabs文件夹中。这将使目标对象成为一个Prefab。预制件是自包含的对象,包含自己的层级结构。预制件可以包含一个完整的场景,或者只是一个对象,就像我们这里所做的一样。
-
在Hierarchy窗口中选择并删除goal对象。未来,我们将通过使用它的 Prefab 从 Academy 中以编程方式实例化goal。
-
点击HoDLG | Brains文件夹,点击打开Create菜单。在菜单中选择ML-Agents | LearningBrain。将新建的大脑命名为
TestingLearningBrain,然后创建一个名为TestingPlayerBrain的新玩家大脑。暂时无需配置这些大脑。 -
在Hierarchy窗口中选择TestingAcademy对象,然后更新Testing Academy组件的值,如下图所示:

设置 TestingAcademy
-
请注意,我们正在TestingAcademy脚本中设置以下属性:
-
Brains: TestingLearningBrain
-
Max Steps: 3000
-
Goal: 通过从文件夹中拖动预制件来设置目标
-
Num Goals: 3(从盒子中丢出的目标数)
-
Goal Size: (50, 50, 50)(确定目标框的最大边界)
-
Goal Center: (85, 110, -37)(目标框的中心点)
-
此时你可能会想要运行项目;如果你刚刚下载了代码,倒是可以先运行,但等我们在下一节中定义TestingAgent时再开始吧。
编写 TestingAgent 脚本
当然,如果没有代理来与环境交互并进行学习,我们的测试(或者说我们想要推进的模拟程度)是没有意义的。在接下来的练习中,我们将定义描述TestingAgent组件的脚本:
-
点击HoDLG | Scripts文件夹,点击Create按钮以打开菜单。
-
在菜单中选择C# Script并将脚本命名为
TestingAgent。 -
在编辑器中打开脚本,并开始用以下代码进行编写:
using MLAgents;
using UnityEngine;
namespace Packt.HoDLG
{
public class TestingAgent : Agent
{
public string[] axisAction;
protected Vector3 resetPos;
protected Quaternion resetRot;
}
}
-
这开始了我们的类;这次它是从
Agent这个基类扩展而来。接着,我们定义了一些基本字段,用于设置变量和记录代理的起始位置和旋转。 -
接下来,我们定义
InitializeAgent方法。该方法会被调用一次,用于设置代理并确保动作长度一致;我们很快就会讲到。我们记住代理开始时的位置/旋转,以便稍后恢复。代码如下:
public override void InitializeAgent()
{
base.InitializeAgent();
if (axisAction.Length != brain.brainParameters.vectorActionSize[0])
throw new MLAgents.UnityAgentsException("Axis actions must match agent actions");
resetPos = transform.position;
resetRot = transform.rotation;
}
- 接下来,我们定义一个空的方法,名为
CollectObservations。通常,这是代理观察环境的地方;由于我们计划使用视觉观察,因此可以将其留空。代码如下:
public override void CollectObservations(){ }
- 接下来,我们定义另一个必需的方法:
AgentAction。这是我们添加负面步骤奖励并移动代理的地方,如下方代码片段所示:
public override void AgentAction(float[] vectorAction, string textAction)
{
AddReward(-1f / agentParameters.maxStep);
MoveAgent(vectorAction);
}
public void MoveAgent(float[] act)
{
for(int i=0;i<act.Length;i++)
{
var val = Mathf.Clamp(act[i], -1f, 1f);
TestingInput.Instance.setAxis(val,axisAction[i]);
}
}
-
这里的代码解读来自大脑的动作,并将它们注入到一个新的类中(我们稍后将构建它),叫做
TestingInput。TestingInput是一个辅助类,我们将用它来覆盖游戏的输入系统。 -
保存脚本,并且再次忽略任何编译器错误。我们有一个新的依赖项,
TestingInput,我们将在稍后定义它。
有了新的脚本,我们可以在下一部分开始设置TestingAgent组件。
设置 TestingAgent
现在,我们正在构建的系统是相当通用的,旨在用于多个环境。在设置过程中请记住这一点,特别是当某些概念看起来有点抽象时。打开编辑器,让我们将TestingAgent脚本添加到一个对象中:
-
选择场景中的Vehicle1、Vehicle3、Vehicle4和Vehicle5,并禁用它们。我们当前只希望给代理提供驾驶能力,而不是切换车辆;因此,我们只需要默认的Vehicle2。
-
从HoDLG | Scripts文件夹中选择TestingAgent脚本并将其拖动到Vehicle2对象上。这将把TestingAgent组件添加到我们的Vehicle2,使其成为一个代理(嗯,差不多)。
-
打开Vehicle2 | Cameras在Hierarchy窗口中,并选择你希望代理使用的视图。我们将选择Camera2进行此练习,但每个五个摄像头的选项如以下截图所示:

选择作为输入的视觉观察摄像头
-
最佳选择是Camera1或Camera5,如上面截图所示。请注意,摄像头的顺序是反向的,从右到左,编号从 1 开始。当然,这也留给我们很多机会在未来尝试其他视觉输入。
-
选择Vehicle2并将选中的TestingPlayerBrain和Camera1拖到需要的插槽中,如下方截图所示:

设置 TestingAgent 组件
-
你还需要定义其他属性,具体如下:
-
大脑:TestingPlayerBrain。
-
摄像头 1:点击添加摄像头以添加一个新摄像头,然后从Vehicle2的摄像头中选择Camera1。
-
决策频率:
10(这决定了代理做决策的频率;10是这个游戏的一个不错的起始值。它会有所变化,可能需要根据需要进行调整) -
轴向动作:2:
-
元素 0:垂直(表示我们将要覆盖的轴,允许代理控制游戏。稍后我们会更详细地介绍轴的描述)
-
元素 1:水平(与之前相同)
-
-
-
保存项目和场景,并再次忽略任何编译错误。
这完成了 TestingAgent 的设置;正如你所看到的,启动这个功能所需的配置或代码并不多。未来,你可能会看到更多高级的测试/调试或构建代理的方式。不过,目前我们需要通过注入 Unity 输入系统来完成我们的示例,下一部分我们将会这样做。
覆盖 Unity 输入系统
Unity 最具吸引力的特点之一是其跨平台能力,能够在任何系统上运行,而这也带来了多个有助于我们将代码注入其中的抽象层。然而,所讨论的游戏需要遵循 Unity 的最佳实践,以便轻松实现这种注入。并不是说我们不能通过覆盖游戏的输入系统来实现,只是那样做起来不那么简单。
在我们开始描述注入如何工作之前,先回顾一下使用 Unity 输入系统的最佳实践。多年来,Unity 输入系统已经从最初的简单查询设备输入的方式,演变成现在更具跨平台特性的系统。然而,包括 Unity 本身在内的许多开发者,仍然使用查询特定按键代码的输入方法。最佳实践是定义一组轴(输入通道),来定义游戏的输入。
我们可以通过以下练习轻松查看它在游戏中的当前定义:
-
从编辑器菜单中选择 编辑 | 项目设置。
-
选择“输入”标签,然后展开 轴 | 水平 和 轴 | 垂直,如下面的截图所示:

检查输入轴设置
- 垂直 和 水平 轴定义了将用于控制游戏的输入。通过在这个标签中定义它们,我们可以通过查询轴来跨平台控制输入。请注意,轴输入允许我们定义按钮和摇杆(触摸)输入。对输入系统的查询(使用
getAxis)返回一个从-1到+1的值,或者是连续的输出。这意味着我们可以将任何离散形式的输入(比如按键)立即转换为连续值。例如,当用户按下 W 键时,输入系统将其转换为 垂直轴 上的正 1 值,相反,按下 S 键则会生成负 1 值,同样是在 垂直轴 上。类似地,A 和 D 键控制 水平轴。
正如你在本书的几章中所看到的,使用.6 版本的 ML-Agents 时,当前的离散动作解决方案远不如连续动作。因此,未来我们更倾向于使用连续动作。
到这个阶段,你可能会想,为什么我们使用了离散动作?这是一个很好的问题。如何在未来处理这个二分法,Unity 还未明确。在下一节中,我们将探讨如何将其注入到输入系统中。
构建 TestingInput
我们将使用一种名为单例模式的设计模式,以实现一个可以在代码的任何地方访问的类,类似于当前使用的 Unity 输入类。Unity 的优点是能够让输入完全静态,但对于我们的目的,我们将使用定义良好的脚本版本。打开编辑器并按照接下来的练习构建TestingInput脚本和对象:
-
选择HoDLG | Scripts文件夹并打开创建菜单。
-
从创建菜单中,选择C#脚本。将新脚本命名为
Singleton。这个脚本是来自wiki.unity3d.com/index.php/Singleton的标准模式脚本;脚本如下所示:
using UnityEngine;
namespace Packt.HoDLG
{
/// <summary>
/// Inherit from this base class to create a singleton.
/// e.g. public class MyClassName : Singleton<MyClassName> {}
/// </summary>
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
// Check to see if we're about to be destroyed.
private static bool m_ShuttingDown = false;
private static object m_Lock = new object();
private static T m_Instance;
/// <summary>
/// Access singleton instance through this propriety.
/// </summary>
public static T Instance
{
get
{
if (m_ShuttingDown)
{
Debug.LogWarning("[Singleton] Instance '" + typeof(T) +
"' already destroyed. Returning null.");
return null;
}
lock (m_Lock)
{
if (m_Instance == null)
{
// Search for existing instance.
m_Instance = (T)FindObjectOfType(typeof(T));
// Create new instance if one doesn't already exist.
if (m_Instance == null)
{
// Need to create a new GameObject to attach the singleton to.
var singletonObject = new GameObject();
m_Instance = singletonObject.AddComponent<T>();
singletonObject.name = typeof(T).ToString() + " (Singleton)";
// Make instance persistent.
DontDestroyOnLoad(singletonObject);
}
}
return m_Instance;
}
}
}
private void OnApplicationQuit()
{
m_ShuttingDown = true;
}
private void OnDestroy()
{
m_ShuttingDown = true;
}
}
}
-
输入上述代码,或者直接使用从书籍源代码下载的代码。单例模式允许我们定义一个线程安全的特定类实例,所有对象都可以引用。典型的静态类不是线程安全的,可能会导致数据损坏或内存问题。
-
在HoDLG | Scripts文件夹中创建一个名为
TestingInput的新脚本,并打开它进行编辑。 -
我们将从以下代码开始该类:
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace Packt.HoDLG
{
public class TestingInput : Singleton<TestingInput>
{
public string[] axes;
public bool isPlayer;
}
}
- 请注意高亮显示的那一行,以及我们如何声明该类继承自
Singleton类型,该类型包装了TestingInput类型。这个使用泛型的递归类型非常适合单例模式。如果这一点有点不清楚,别担心;你需要记住的唯一一点是,我们现在可以在代码的任何地方访问该类的实例。请注意我们提到的是实例而不是类,这意味着我们还可以在TestingInput类中保持状态。我们在这里声明的变量,axes和isPlayer,要么在编辑器中设置,要么在Start方法中定义,如下所示:
void Start()
{
axisValues = new Dictionary<string, float>();
//reset the axes to zero
foreach(var axis in axes)
{
axisValues.Add(axis, 0);
}
}
-
在
Start方法中,我们定义了一个Dictionary来保存我们希望该组件覆盖的轴和值。这使我们能够控制希望覆盖哪些输入。然后,我们构建名称/值对集合。 -
接下来,我们将定义几个方法,使我们能够模拟并设置输入系统的轴值。Unity 没有直接设置轴值的方式。目前,
Input系统直接查询硬件以读取输入状态,并且没有提供覆盖此状态进行测试的方式。虽然这是社区长期以来的一个需求,但是否最终会实现仍然有待观察。 -
然后我们输入
setAxis和getAxis方法,如下所示:
public void setAxis(float value, string axisName)
{
if (isPlayer == false && axes.Contains(axisName)) //don't if player mode
{
axisValues[axisName] = value;
}
}
public float getAxis(string axisName)
{
if (isPlayer)
{
return Input.GetAxis(axisName);
}
else if(axes.Contains(axisName))
{
return axisValues[axisName];
}
else
{ return 0; }
}
- 这完成了脚本的编写;如果你在过程中已经添加了代码,保存文件并返回 Unity。此时,你应该看不到编译错误,因为所有必需的类型应该已经存在并且是完整的。
这将设置TestingInput脚本;现在,我们需要继续下一部分,将它添加到场景中。
将 TestingInput 添加到场景
单例可以在任何地方被调用,实际上,它们并不需要场景中的游戏对象。然而,通过将对象添加到场景中,我们更清晰地意识到所需的依赖关系,因为它使我们能够为特定的场景设置所需的参数。打开 Unity 编辑器,按照接下来的练习将TestingInput组件添加到场景中:
-
点击层级窗口,然后从菜单中选择游戏对象 | 创建空对象。重命名该对象为
TestingInput。 -
将TestingInput脚本从HoDLG | 脚本文件夹拖到层级窗口中的新TestingInput对象上。
-
选择TestingInput对象,然后设置所需的轴,如下图所示:

设置要重写的轴
-
我们需要定义两个要重写的轴。在这个例子中,我们只重写垂直(S和W)和水平(A和D)键。当然,你也可以重写任何你想要的轴,但在这个例子中,我们只重写了两个。
-
保存项目和场景。
到此为止,你无法运行项目,因为实际的输入系统还没有重写任何内容。我们将在下一部分完成最终的注入。
重写游戏输入
到此为止,我们已经建立了一个完整的测试系统;接下来只需要完成注入的最后部分。这部分的操作可能需要敏锐的眼光和一些代码的挖掘。幸运的是,有一些清晰的指示器,可以帮助你找到注入的位置。打开编辑器,按照以下步骤完成注入:
-
在层级窗口中选择Control对象。
-
在检查器窗口中找到MS Scene Controller Free组件,然后使用上下文菜单打开代码编辑器中的脚本。
-
找到以下代码块,大约在286行(大约中间位置),如下所示:
case ControlTypeFree.windows:
verticalInput = Input.GetAxis (_verticalInput);
horizontalInput = Input.GetAxis (_horizontalInput);
mouseXInput = Input.GetAxis (_mouseXInput);
mouseYInput = Input.GetAxis (_mouseYInput);
mouseScrollWheelInput = Input.GetAxis (_mouseScrollWheelInput);
break;
}
-
这里是游戏查询
GetAxis方法的位置,用来返回相应输入轴的值。正如我们所讨论的,我们这里只关心垂直和水平轴。你当然可以根据需要重写其他轴。 -
修改设置
verticalInput和horizontalInput的代码行,如下所示:
verticalInput = TestingInput.Instance.getAxis(_verticalInput);
horizontalInput = TestingInput.Instance.getAxis(_horizontalInput);
-
请注意,我们调用
TestingInput.Instance,以便访问我们类的单例实例。这使我们能够查询该类当前的输入值。现在,TestingInput对象可以作为该类在输入方面的真实来源。 -
之前,我们快速浏览了设置输入的代理代码,但这里再次提供供参考:
public void MoveAgent(float[] act)
{
for(int i=0;i<act.Length;i++)
{
var val = Mathf.Clamp(act[i], -1f, 1f);
TestingInput.Instance.setAxis(val,axisAction[i]);
}
}
-
注意
TestingAgentMoveAgent方法中的高亮行。这里是我们通过代理重写输入并将值注入到游戏中的地方。 -
保存代码并返回编辑器。确保现在修复任何编译问题。
不幸的是,我们仍然无法运行场景,因为我们还有最后一个配置步骤需要处理。在下一部分,我们将通过设置脑模型来完成配置。
配置所需的脑模型
最后一块拼图是配置我们之前快速构建的脑模型。ML-Agents 要求脑模型配置所需的输入和观察空间,以便正常工作。我们将在下一个练习中设置 TestingPlayerBrain 和 TestingLearningBrain:
-
打开 Unity 编辑器并从 HoDLG | Brains 文件夹中选择 TestingLearningBrain,以在 Inspector 中打开它。
-
设置 Brain 参数,如下图所示:

设置 TestingPlayerBrain 的参数
-
需要设置几个参数,它们总结如下:
-
视觉观察:
84x84,无灰度 -
向量动作:
-
空间类型:连续
-
空间大小:
2 -
动作描述:
-
大小:
2 -
元素 0:垂直
-
元素 1:水平
-
-
-
轴向连续玩家动作:
-
大小:
2 -
垂直:
-
轴向:垂直
-
索引:
0 -
缩放:
1
-
-
水平:
-
轴向:水平
-
索引:
1 -
缩放:
1
-
-
-
-
选择 TestingLearningBrain 并以相同方式进行配置,但这是用于学习,如下图所示:

配置 TestingLearningBrain
-
学习脑的配置要简单得多,但即使在以玩家模式运行示例时(如你记得的,它已被设置为这样做),这也是必须的。
-
保存场景和项目。最终,我们已经完成了所需的配置。
-
按下播放按钮运行场景并以玩家模式玩游戏。我们通过 ML-Agents 系统控制游戏。几秒钟后,你应该能看到附近掉落一些目标。
-
控制车辆并驶入目标,如下图所示:

驾驶进入目标
- 当你完成游戏后,停止游戏。
现在我们能够通过配置好的玩家脑模型通过 ML-Agents 玩游戏,我们将在下一部分切换到学习脑,让代理控制游戏。
训练时间
无论我们决定如何使用这个平台,无论是用于训练还是测试,我们现在需要做最后的脑配置步骤,以便设置任何我们可能决定用于训练的自定义超参数。打开 Python/Anaconda 控制台并准备进行训练,然后按照以下步骤进行:
-
打开位于
ML-Agents/ml-agents/config文件夹中的trainer_config.yaml文件。 -
我们将向配置文件中添加一个新的配置部分,模仿其他视觉环境中的配置。按照以下方式添加新的配置:
TestingLearningBrain:
use_recurrent: true
sequence_length: 64
num_layers: 1
hidden_units: 128
memory_size: 256
beta: 1.0e-2
gamma: 0.99
num_epoch: 3
buffer_size: 1024
batch_size: 64
max_steps: 5.0e5
summary_freq: 1000
time_horizon: 64
-
注意,我们添加了
brain这个词,以便与其他脑区分开来。这个脑是基于我们之前花时间探索的VisualHallwayBrain模型制作的。然而,请记住,我们现在运行的是一个连续动作问题,这可能会影响某些参数。 -
保存文件并返回到 Unity 编辑器。
-
定位到
TestingAcademy对象,将其Brains替换为TestingLearningBrain,并将其设置为Control,就像你之前做过的那样。 -
保存场景和项目,并返回到 Python/Anaconda 控制台。
-
通过运行以下命令开始训练/学习/测试会话:
mlagents-learn config/trainer_config.yaml --run-id=testing --train
- 观看训练过程和代理玩游戏的表现。代理将开始运行,取决于你训练的时间长度,它可能会变得擅长于寻找目标。
到此为止,你可以让代理自行运行并探索你的关卡。然而,我们要做的是通过使用模仿学习来控制或引导测试代理走向正确的路径,我们将在下一节中讨论这一方法。
通过模仿进行测试
在学习的这一阶段,你已经掌握了几种策略,我们可以应用它们来帮助测试代理学习并找到目标。我们可以轻松地使用好奇心或课程学习,我们将把这作为读者的练习。我们想要的是一种控制某些测试过程的方法,而且我们不希望代理随机地测试所有内容(至少在这个阶段不希望)。当然,有些地方完全随机测试效果很好。(顺便提一下,这种随机测试被称为猴子测试,因为它类似于猴子乱按键盘或输入。)然而,在像我们的游戏这样的空间中,探索每一个可能的组合可能需要很长时间。因此,最好的替代方法是捕捉玩家的录制并将其用作我们测试代理的模仿学习来源。
一切设置好后,并且我们现在能够通过 ML-Agents 将输入事件传递过去,我们可以以代理需要学习的形式捕捉玩家输入。接下来,让我们打开一个备份的 Unity 并设置场景来捕捉玩家的录制,步骤如下:
-
在Hierarchy窗口中选择Vehicle2对象。回忆一下,这里是TestingAgent脚本附加的地方。
-
使用Inspector窗口底部的Add Component按钮将Demonstration Recorder组件添加到代理上。
-
将Demonstration Recorder设置为Record,并将Demonstration Name设置为Testing,然后将大脑切换到TestingPlayerBrain,如下面的截图所示:

向智能体添加演示记录器
-
选择TestingAcademy对象,确保禁用Brain上的Control选项。我们希望玩家在录制时控制智能体。
-
按下 Play 键并运行游戏。使用键盘上的WASD控制键驾驶车辆穿越目标。玩一会儿,以生成一个不错的录制。
-
完成后,检查
Assets文件夹中是否有一个名为Demonstrations的新文件夹,其中包含你的Testing.demo录制文件。
现在,随着玩家录制功能启用,我们可以设置并运行智能体,使用模仿学习来测试关卡。
配置智能体使用 IL
我们已经完成了设置和运行离线模仿学习 (IL) 会话的过程,但让我们在接下来的练习中回顾一下这个过程:
-
打开 Unity 编辑器,进入相同项目,找到包含智能体的Vehicle2对象。
-
将智能体的大脑从TestingPlayerBrain切换到TestingLearningBrain。
-
选择TestingAcademy并启用Testing Academy | Brains组件上的Control属性。
-
保存场景和项目。
-
打开
config/offline_bc_config.yaml文件,在文本或代码编辑器中编辑。 -
添加以下部分(
HallwayLearning的修改版):
TestingLearningBrain:
trainer: offline_bc
max_steps: 5.0e5
num_epoch: 5
batch_size: 64
batches_per_epoch: 5
num_layers: 2
hidden_units: 128
sequence_length: 16
use_recurrent: true
memory_size: 256
sequence_length: 32
demo_path: ./UnitySDK/Assets/Demonstrations/Testing.demo
-
编辑完成后保存文件。
-
打开一个准备好进行训练的 Python/Anaconda 控制台,并输入以下命令:
mlagents-learn config/offline_bc_config.yaml --run-id=testing_il --train
-
注意几个修改,已用粗体标出。训练开始后,观察智能体以你训练它的方式驾驶汽车(或者至少,它会尽力去做)。
-
让智能体玩游戏,观察它的表现以及是否遇到问题。
这个演示/游戏相当稳定,不容易出现明显问题,这使得测试明显问题变得困难。然而,希望你能理解,如果在游戏早期实现这种类型的系统,即使只是为了测试,它也能提供快速发现 bug 和其他问题的能力。当然,目前我们唯一的识别问题的方法是观察智能体的游戏表现,这并没有节省时间。我们需要的是一种追踪智能体活动的方法,并确定智能体是否(以及何时)遇到问题。幸运的是,我们可以通过添加分析功能轻松地增加这种追踪方式,接下来我们将介绍这一部分。
分析测试过程
ML-Agents 当前缺少的一个关键功能是额外的训练分析(超出控制台和 TensorBoard 提供的内容)。一个可能至关重要的功能(而且不难添加)是训练分析。可以通过 Unity Analytics 服务来实现这个功能,所有游戏都可以免费试用。由于这不是 ML-Agents 的现有功能,因此我们将在下一个练习中通过添加我们自己的训练分析系统来实现:
-
打开 Unity 编辑器,从菜单中选择Window | General | Services。这将打开一个名为Services的新窗口,通常会出现在Inspector窗口上方。
-
点击新打开的服务窗口中的Analytics服务。你需要通过几屏设置,询问你的偏好和确认,如下图所示:

为你的项目设置分析
-
点击按钮启用Google Analytics。然后,选择Discover玩家洞察开关,你将被提示按下编辑器中的Play。
-
在编辑器中按下Play,让游戏运行几秒钟。
-
返回到服务窗口和 Analytics 页面,在顶部,你应该看到一个叫做Go to Dashboard的按钮。点击该按钮,如下图所示:

使用仪表板探索你的数据
- 这将打开你的默认网页浏览器,带你到你的项目分析页面,你应该能看到一些事件,如appStart和appStop。
这就完成了分析服务的设置,正如你所看到的,它其实非常简单。然而,像所有事情一样,我们需要自定义一些我们将要发送到分析服务的数据。你将在下一节学习如何发送你自己的自定义分析。
发送自定义分析
如果你之前使用过分析服务,你可能已经有了自己追踪游戏使用情况的最佳实践;如果是这样,随时可以使用这些方法。我们在这里展示的方法是作为起点,帮助你设置并发送自定义分析数据用于训练,或者用于跟踪玩家使用情况。
让我们开始,打开 Unity 编辑器并进行下一个练习:
-
在
HoDLG的Scripts文件夹中创建一个名为TestingAnalytics的新 C#脚本。 -
打开并编辑
TestingAnalytics脚本,在编辑器中输入以下代码:
using UnityEngine;
namespace Packt.HoDLG
{
public class TestingAnalytics : Singleton<TestingAnalytics>
{
private TestingAcademy academy;
private TestingAgent[] agents;
private void Start()
{
academy = FindObjectOfType<TestingAcademy>();
agents = FindObjectsOfType<TestingAgent>();
}
public string CurrentGameState
{
get
{
var state = string.Empty;
foreach (var agent in agents)
{
foreach (var goal in academy.goals)
{
var distance = Vector3.Distance(goal.transform.position, agent.transform.position);
state += agent.name + " distance to goal " + distance + "/n";
}
}
return state;
}
}
}
}
-
这段代码所做的就是收集目标的当前位置以及它们与代理的接近程度。那是我们目前关心的内容。此外,注意我们将其设为public property,以便像方法一样调用,而不仅仅是一个字段。这在后面会很重要。
-
保存文件并返回编辑器。确认没有编译错误。
-
在场景中创建一个新的空游戏对象,并命名为
TestingAnalytics。将新的TestingAnalytics脚本拖到该对象上,将其设置为场景组件。虽然这个类是单例,但我们仍然希望将其作为场景的依赖项添加(本质上是作为一个提醒)。然而,我们还可以使用另一个技巧来编程预制体。 -
将 TestingAnalytics 对象拖入 HoDLG | Prefabs 文件夹。这将使该对象成为预制体,其他所有预制体现在都可以访问它。
-
双击 HoDLG | Prefabs 文件夹中的 goal 预制体,以在其自己的迷你编辑器中打开该对象。
-
使用 添加组件 按钮为对象添加 Analytics Event Tracker 组件并进行配置,如下图所示:

设置分析事件跟踪器
-
配置组件如下:
-
何时:生命周期
-
生命周期事件:销毁时
-
发送事件:
-
名称:目标摧毁事件
-
参数:1/10:
-
名称:状态
-
值:动态
-
对象:TestingAnalytics(预制体)
-
方法:CurrentGameState
-
-
-
-
通过更改学院和代理配置,将场景切换回玩家模式。
-
保存场景和项目。
-
通过按 播放 运行场景,驾车经过一个目标。当你碰到目标时,查看 Analytics 仪表板并注意事件是如何被追踪的。
在这个阶段,分析数据只有在目标被摧毁时才会报告,并报告每个代理与目标的距离。因此,对于一个代理和三个目标,当目标被撞毁或物体重置时,它们会报告三个距离。通过这些统计数据,你可以大致了解每个代理测试会话的整体情况,不管是好是坏。当然,你可以添加任何你想要的分析数据;这很容易让人过度投入。谁知道呢;未来,Unity 可能会提供一个由 ML-Agents 驱动的自测平台,提供测试分析数据。
本章即将结束,当然,我们也接近你最喜欢的部分——练习。
练习
本章的练习是结合了使用 ML-Agents 和构建你自己的测试分析平台。因此,从下面的列表中选择一到两个你可以自己完成的练习:
-
配置 TestingAgent 使用不同的相机进行视觉观察输入。
-
启用代理大脑上的 好奇心学习。
-
设置 TestingAgent 控制另一辆车。
-
设置 TestingAgent 在另一辆车上运行,让 ML-Agents 同时控制两个代理。
-
为代理添加额外的追踪分析自定义事件。也许可以跟踪代理的移动距离与其生命周期的关系。这将提供一个速度因子,也能表示代理的效率。一个更快撞到目标的代理会有更好的速度因子。
-
通过添加带有学习代理的第二辆车,启用在线模仿学习。如果需要,回顾一下网球场景的设置。
-
设置学院使用课程学习。也许允许虚拟目标部署框在训练迭代中增长(按 10%或其他因素)。这将使目标分散更远,使得代理更难找到。
-
修改大脑使用的视觉观察输入为
184x184,这是新的标准,看看这对代理训练有何影响。 -
修改视觉观察卷积编码网络,就像我们在第七章中所做的那样,Agents and the Environment,使用更多层和/或不同的过滤器。
-
将这个测试框架应用到你自己的游戏中。确保也添加分析功能,以便跟踪培训和玩家使用情况。
这些练习比前几章更复杂,因为这是一个重要的大章节。在下一节中,我们将回顾你在本章学到和涵盖的内容。
摘要
在本书的所有章节中,如果你正在开发自己的游戏,这可能是最有用的章节之一。游戏测试是需要大量时间和注意力的事情,必须部分自动化。虽然深度强化学习在几乎任何游戏中都表现良好是有意义的,但尚不清楚这是否是这种新学习现象的一个利基。然而,有一点可以肯定的是,ML-Agents 完全可以作为一个测试工具,并且我们确信随着时间的推移它会变得更加出色。
在本章中,我们看了建立一个通用测试平台,由 ML-Agents 提供支持,可以自动测试任何游戏。我们首先看了我们需要调整的每个组件,学院和代理,以及它们如何被泛化用于测试。然后,我们看了如何注入到 Unity 输入系统中,并使用我们的TestingAgent来覆盖游戏的输入并学习如何独立控制它。之后,我们看了如何通过使用离线 IL 来更好地设置我们的测试,并记录一个演示文件,以便稍后用来训练代理。最后,为了查看我们的测试效果如何,我们添加了分析功能,并根据我们的需求进行了定制。
下一章将是我们的最后一章,也是我们对游戏深度学习的最后讨论;适当地,我们将展望 ML-Agents 和 DRL 的未来。
第十三章:障碍塔挑战及其后续
在本章中,我们的最后一章,我们将审视游戏中深度学习(DL)和深度强化学习(DRL)的当前和未来状态。我们诚实而坦率地看待这些技术是否已经准备好投入商业游戏,或者它们只是新奇玩意。几年后,我们是否会看到 DRL 代理在每一款游戏中击败人类玩家?尽管这还有待观察,而且事情变化迅速,但真正的问题是:DL 是否准备好为您的游戏服务?这可能是您此刻正在问自己的问题,希望我们在本章中能够回答。
本章将是一些实际练习和一般讨论的结合,不幸的是没有练习。好吧,有一个大练习,但我们很快就会谈到。以下是本章将涵盖的内容:
-
Unity 障碍塔挑战
-
您的游戏的深度学习?
-
制作你的游戏
-
更多学习的基础知识
本章假定您已经完成了本书中的众多练习,以便理解上下文。我们将提到这些部分以提醒读者,请不要跳到本章。
Unity 障碍塔挑战
Unity 障碍塔挑战于 2019 年 2 月引入,作为一个离散的视觉学习问题。正如我们之前所见,这是游戏、机器人和其他模拟学习的圣杯。更有趣的是,这一挑战是在 ML-Agents 之外引入的,并且要求挑战者从头开始编写他们自己的 Python 代码来控制游戏——这是我们在本书中接近学习如何做到的,但我们省略了技术细节。相反,我们专注于调整超参数、理解奖励和代理状态的基础知识。如果您决定挑战这个塔,所有这些基础知识都将派上用场。
在撰写本书时,用于开发的 ML-Agents 版本是0.6。如果您已经完成了所有的练习,您会注意到,所有使用离散动作空间的视觉学习环境都存在梯度消失或梯度爆炸问题。您将看到的情况是代理基本上学不到东西,并执行随机动作;通常需要数十万次迭代才能看到结果。但在使用矢量观察的状态空间较小的环境中,我们并不会看到这个问题。然而,在具有大输入状态的视觉环境中,这个问题经常会出现。这意味着,基本上在撰写本书时,您不会希望使用 Unity 代码;它目前是离散动作的可视学习者。
在写这篇文章时,Unity Obstacle Tower Challenge 刚刚启动,早期的度量指标已经开始报告。目前,谷歌 DeepMind 提出的领先算法毫不奇怪,就是一个名为Rainbow的算法。简而言之,Rainbow 是许多不同的深度强化学习(DRL)算法和技术的结合,旨在更好地学习障碍塔所定义的离散动作视觉学习空间。
既然我们已经确认你可能想要编写自己的代码,那么接下来我们将理解你的代理需要解决的高层关键问题。解释如何编写代码以及其他技术细节可能需要另一本书,因此我们将讨论整体挑战和你需要解决的关键要素。此外,获胜者更可能需要使用更多的概率方法来解决问题,而这一点目前在任何地方的讨论都不充分。
让我们在接下来的练习中设置挑战并启动它:
-
从
github.com/Unity-Technologies/obstacle-tower-env下载 Obstacle Tower 环境的二进制文件。 -
按照指示操作,下载适合你环境的压缩文件。在大多数系统上,这只需要下载并解压到一个文件夹,稍后你将在该文件夹中执行文件。
-
将文件解压到一个常用的文件夹中。
-
通过双击程序(Windows)或在控制台中输入名称来启动程序。启动挑战后,你实际上可以像人类一样参与其中。玩这个游戏,看看你能爬到多少楼层。以下截图展示了正在运行的挑战示例:

玩家模式下的 Obstacle Tower 挑战
你在游戏过程中会学到的第一件事之一是,游戏开始时相对简单,但在后面的楼层,难度会增加,甚至对人类来说也很困难。
如前所述,解决这个挑战超出了本书的范围,但希望你现在能理解一些目前制约深度强化学习领域的复杂性。我们已经在下表中回顾了你在进行此方法时将面临的主要挑战:
| 问题 | 章节 | 当前 状态 | 未来 |
|---|---|---|---|
| 视觉观测状态——你需要构建一个足够复杂的卷积神经网络(CNN),并可能需要递归神经网络(RNN)来编码视觉状态中的足够细节。 | 第七章,代理与环境 | 当前的 Unity 视觉编码器远未达标。 | 幸运的是,CNN 和递归网络在视频分析中已有大量研究。记住,你不仅仅是想捕捉静态图像;你还需要编码图像的序列。 |
| DQN, DDQN 或 Rainbow | 第五章, 介绍深度强化学习 | Rainbow 目前是最好的,并且可以在 GCP 上使用。 | 正如我们在本书中看到的,PPO 仅在连续动作空间上表现良好。为了应对离散动作空间的问题,我们回顾了更基础的方法,如 DQN 或新兴的 Rainbow,它是所有基本方法的汇总。我们还将讨论未来可能通过进一步使用深度概率方法来解决当前问题的途径。 |
| 内在奖励 | 第九章, 奖励与强化学习 | 使用内在奖励系统在探索方面表现出色。 | 引入像好奇心学习这样的内在奖励系统,可以让智能体根据某种对状态的期望来探索新环境。这种方法将对任何计划达到塔楼更高层次的算法至关重要。 |
| 理解 | 第六章, Unity ML-Agents | Unity 提供了一个出色的示范环境,用于构建和测试模型。 | 你可以很容易地在 Unity 中快速构建并独立测试一个类似的环境。难怪 Unity 从未发布过原始的 Unity 环境作为项目。这很可能是因为这会吸引许多初学者,他们以为仅凭训练就能解决问题。但有时候,训练并不是答案。 |
| 稀疏奖励 | 第九章, 奖励与强化学习 第十章, 模仿与迁移学习 | 可以实施课程学习或模仿学习。 | 我们已经讨论了许多管理稀疏奖励问题的示例。看看获胜者是否依赖这些方法中的一种,如模仿学习(IL)来取得胜利,将会非常有趣。 |
| 离散动作 | 第八章, 理解 PPO | 我们学会了如何利用 PPO 通过随机方法解决连续动作问题。 | 正如我们之前提到的,可能需要通过深度概率方法和技术来解决当前的一些问题。这可能需要新算法的开发,而开发所需的时间仍然需要观察。 |
前表中突出的每个问题可能需要部分或全部解决,才能让智能体从 1 层到 100 层,完成整个挑战。如何在 Unity、DRL 以及整个深度强化学习领域中发挥作用,还需要进一步观察。在接下来的部分,我们将讨论深度学习和深度强化学习的实际应用,以及它们如何用于你的游戏。
深度学习在你的游戏中的应用?
你可能是因为希望通过学习深度学习(DL)和深度强化学习(DRL)在游戏中的应用,进而获得理想的工作或完成理想的游戏,才开始阅读这本书。无论如何,你会面临一个问题:决定这项技术是否值得加入自己的游戏,以及在什么程度上加入。以下是十个问题,可以帮助你判断深度学习(DL)是否适合你的游戏:
-
你是否已经决定并需要使用深度学习(DL)或深度强化学习(DRL)来构建游戏?
-
是的 – 10 分
-
不是 – 0 分
-
-
你的游戏是否能从某种形式的自动化中受益,无论是通过测试还是管理重复性的玩家任务?
-
是的 – 10 分
-
不是 – 0 分
-
-
你是否希望将训练、人工智能或其他类似活动作为游戏的一部分?
-
是的 – (-5)分。你可能更适合使用一种更强大的人工智能来模拟训练。训练 DRL 需要太多的迭代和样本,至少目前,它作为游戏内训练工具并不高效。
-
不是 – 0 分。
-
-
你是否希望在你的游戏中加入前沿的人工智能技术?
-
是的 – 10 分。确实有很多方法可以将人工智能技术叠加,并让深度强化学习(DRL)解决方案奏效。谈到当前的人工智能技术,真的没有比这更前沿的技术了。
-
不是 – 0 分。
-
-
你是否有足够的时间来训练人工智能?
-
是的 – 10 分
-
不是 – (-10)分
-
-
你是否阅读了这本书的很大一部分,并完成了至少一些练习?
-
是的 – 10 分,若完成超过 50%则加 5 分
-
不是 – (-10)分;感谢你的诚实
-
-
你是否有数学背景或对数学感兴趣?
-
是的 – 10 分
-
不是 – (-10)分
-
-
你在学术层面阅读了多少关于强化学习的论文?
-
10+ – 25 分
-
5–10 – 10 分
-
1–5 – 5 分
-
0 – 0 分
-
-
你的完成时间表是什么?
-
1–3 个月 – (-10)分
-
3–6 个月 – 0 分
-
6–12 个月 – 10 分
-
1–2 年 – 25 分
-
-
你的团队规模是多少?
-
单打独斗 – (-10)分
-
2–5 – 0 分
-
6–10 – 10 分
-
11+ – 25 分
-
回答所有问题并评分,以确定你是否完全准备好。请参阅以下内容,了解你和/或你的团队的准备情况:
-
<0 分 - 你是怎么走到这本书的这一部分的?你还没有准备好,最好放下这本书。
-
0-50 - 你显然有潜力,但你还需要更多帮助;查看下一步和进一步学习领域的部分。
-
50-100 - 你显然在构建知识基础和实现有趣的深度强化学习(DRL)方面有了进展,但你可能仍然需要一些帮助。查看下一步和进一步学习领域的部分。
-
100+ - 你已经完全准备好了,非常感谢你抽出时间阅读这本书。也许可以利用一些个人时间,将你的知识传递给你认识的人或团队成员。
当然,前述测试结果没有绝对的规则,您可能会发现自己的得分很低,但随后可能会做出下一个伟大的人工智能游戏。您如何看待结果由您决定,下一步如何进行也完全由您决定。
在下一部分,我们将探讨您可以采取的下一步措施,以便深入了解 DRL,并如何在游戏中构建更好的自动化和人工智能。
构建您的游戏
现在您已经决定为您的游戏使用深度学习和/或深度强化学习,是时候确定如何在游戏中实现各种功能了。为了做到这一点,我们将通过一个表格,概述您需要经过的步骤来构建游戏的人工智能代理:
| 步骤 | 行动 | 总结 |
|---|---|---|
| 启动 | 确定您希望游戏中的人工智能在什么层次上操作,从基础层次(也许只是用于测试和简单的自动化)到高级层次(人工智能与玩家对抗)。 | 确定人工智能的层次。 |
| 资源配置 | 确定资源的数量。基本的人工智能或自动化可以由团队内部处理,而更复杂的人工智能可能需要一个或多个经验丰富的团队成员。 | 团队需求。 |
| 知识 | 确定团队所拥有的知识水平以及所需的知识。显然,任何实施新人工智能的团队都需要学习新技能。 | 知识差距分析。 |
| 演示 | 始终从构建一个简单但可行的概念验证开始,展示系统的所有关键方面。 | 演示团队能够完成基本前提。 |
| 实施 | 以简洁且可维护的方式构建实际系统。保持所有已知的内容简单清晰。 | 构建系统。 |
| 测试 | 一遍又一遍地测试系统。系统必须彻底测试,当然,最好的测试方法就是使用 DRL 自动化测试系统。 | 测试系统。 |
| 修复 | 正如任何开发过软件超过几周的人所告诉你的那样,过程是构建、测试、修复并重复。这本质上就是软件开发的过程,因此尽量不要增加太多其他无关的功能,以免分散注意力。 | 修复系统。 |
| 发布 | 向用户/玩家发布软件对成功的游戏或任何类型的软件产品至关重要。您始终希望尽早发布并频繁发布,这意味着必须鼓励玩家进行测试并提供反馈。 | 发布错误。 |
| 重复 | 这一过程是无止境的,只要您的产品/游戏能带来利润,它就会持续进行。 | 支持系统。 |
前述过程是基本前提,适用于大多数开发需求。在大多数情况下,您可能希望在工作或任务板上跟踪单个工作项,如功能或错误。您可能希望使用更明确的流程,例如 Scrum,但通常保持简洁是最好的行动方式。
Scrum 及其他软件开发流程是很好的学习范例,但除非你有经过正式培训的员工,否则最好避免自己去实施这些流程。这些流程中往往有一些微妙的规则,需要执行才能像它们所声称的那样有效。即使是经过培训的 Scrum Master,也可能需要在许多组织中每天进行斗争,才能落实这些规则,最终它们的价值变得更多是由管理驱动,而非开发者主导。可以参考前面的表格作为你构建下一个游戏时的步骤指南,始终记住“构建、发布、修复、重复”是做好软件的关键。
在下一部分,我们将介绍你可以用来扩展学习的其他内容。
更多的学习基础
目前有着日益增长的学习机器学习、深度学习和深度学习回归(DLR)的资源。这个资源库正在不断扩大,选择材料的余地也越来越多。因此,我们现在总结出我们认为对游戏 AI 和深度学习最具前景的领域:
-
基础数据科学课程:如果你从未学习过数据科学的基础课程,那么你肯定应该学习一门。这些课程可以帮助你理解数据的特性、统计学、概率和变异性,这些基础知识多得无法一一列举。务必先打好这个基础。
-
概率编程:这是通过变分推断方法的结合,来回答给定事件的概率及某事件可能发生的概率。这些类型的模型和语言已经用于多年来分析金融信息和风险,但它们现在在机器学习技术中逐渐崭露头角。
-
深度概率编程:这是变分推断与深度学习模型的结合。变分推断是一个过程,你通过给定多个可能概率的输入来回答一个带有概率的问题。因此,我们不是用一系列权重来训练网络,而是使用一系列概率分布。这种方法已经证明非常有效,最近它已经用修改后的概率 CNN 模型执行了视觉图像分类任务。
-
视觉状态分类与编码:深度学习系统的一个关键方面是开发卷积神经网络(CNN)模型来分类图像。为了为你的游戏环境构建网络,你需要深入理解这个领域。请记住,不同的环境可能需要使用 CNN 模型。
-
记忆:记忆当然可以有多种形式,但最值得关注的主要形式是递归神经网络(RNN)。在本书的早期,我们介绍了目前使用的标准递归网络模型——长短时记忆(LSTM)块。即使在写作时,关于门控递归单元(GRU)的兴趣也在重新升温,这是一种更复杂的递归网络,已被证明能更好地解决梯度消失问题。人们始终对云技术或其他支持的技术以及它们如何与新的深度学习技术互动充满兴趣。
-
深度学习即服务:像 Google、Amazon、Microsoft、OpenAI 等公司,虽然声称注重开放性,但通常远非如此。在大多数情况下,如果你想将这些技术融入你的游戏,你需要订阅他们的服务——当然,这也有其优缺点。主要问题在于,如果你的游戏变得非常流行,并且你过度依赖深度学习服务,你的利润就会与其挂钩。幸运的是,Unity 至今还没有采取这种方式,但这一切还得看社区如何顺利解决障碍塔挑战。
-
数学:一般来说,无论你是否打算深入构建自己的模型,你都需要不断提升自己的数学技能。最终,你对数学的直觉理解将为你提供解决这些复杂技术所需的洞察力。
-
毅力:学会失败,然后继续前行。这是至关重要的,也是许多新开发者常常感到沮丧的地方,他们会选择转向更简单、更容易、更少回报的事情。当你失败时要高兴,因为失败是学习和理解的过程。如果你从未失败过,那你其实从未真正学习过,所以学会去失败。
硬编码的学习资源列表很可能在这本书还没有印刷或发布之前就已经过时。请利用前面的列表来概括你的学习,拓宽你在基础机器学习和数据科学方面的知识。最重要的是,深度学习是一项数据科学追求,必须尊重数据;永远不要忘记这一点。
在下一节的最终章中,我们将总结本章内容和整本书。
总结
在本章中,我们简要介绍了与深度学习(DL)和深度强化学习(DRL)相关的许多基本概念;也许你会决定参与 Unity 障碍塔挑战并完成它,或者仅在自己的项目中使用 DRL。我们通过简单的测验来评估你是否有潜力深入学习并在游戏中应用 DRL。之后,我们探讨了开发的下一步,并最终看到了可能想要专注的其他学习领域。
本书是一次了解深度学习(DL)在未来应用于游戏项目时如何有效的练习。我们一开始探讨了许多基础的 DL 原理,并研究了更具体的网络类型,如 CNN 和 LSTM。接着,我们考察了这些基础网络形式如何应用于自动驾驶和构建聊天机器人等应用。之后,我们研究了当前机器学习算法的“王者”——强化学习和深度强化学习。然后,我们研究了当前的领导者之一——Unity ML-Agents,并通过多个章节讲解如何实现这一技术,逐步从简单的环境构建到更复杂的多智能体环境。这也使我们有机会探索不同形式的内在/外在奖励和学习系统,包括课程学习、好奇心、模仿学习和迁移学习。
最后,在完成本章之前,我们进行了一个关于使用深度强化学习(DRL)进行自动化测试和调试的长期练习,并额外提供了使用内在学习(IL)增强测试的选项。


是我们描述的成本函数,表达如下:
,表示为如下:
表示输入值,而
表示神经元位置。注意,我们现在需要对给定神经元的激活函数 a 进行偏导数,这一过程可以通过以下方式总结:
^(th) 输入、
^(th) 神经元和层
的权重定义梯度 (
)。结合梯度下降,我们需要使用前面的基础公式将调整反向传播到权重中。对于输出层(最后一层),这现在可以总结如下:
和
,从输入层开始并向前传播。
在输出层评估该项
。
,从输出层开始并向后传播。
来获得每一层所需的导数。
= 输出序列
= 输入序列
= 向量表示
= 值的向量(1,2,3,4)
= 行动
= α = 学习率
= 奖励
= 值的表格/矩阵
= 状态
= 动作
= alpha = 学习率
= 回报
- 是一个有限的状态集合,
- 是一个有限的动作集合,
- 动作
在状态
时刻
导致状态
在时刻
的概率,
- 是即时奖励
- gamma 是一个折扣因子,用来折扣未来奖励的重要性或为未来奖励提供重要性
当前状态
当前动作
下一个动作
当前奖励
学习率(alpha)
奖励折扣因子(gamma)


浙公网安备 33010602011771号