面向机器学习的仿真实践指南-全-
面向机器学习的仿真实践指南(全)
原文:
zh.annas-archive.org/md5/71285a3e13b1589d6ffd1a8c3b53a8a2译者:飞龙
序言
欢迎来到机器学习实用模拟!这本书结合了我们最喜欢的两件事情:视频游戏引擎和人工智能。我们希望您在阅读时能像我们写作时那样享受它。
具体来说,本书探讨了 Unity 的使用,这是一个曾被称为游戏引擎但现在更喜欢称为用于创建和操作交互式、实时 3D 内容的平台的产品。这是很多词汇,但基本上可以归结为这样:Unity 是一个用于在 3D 中构建事物的平台,虽然传统上它被用于视频游戏开发,但也可以通过结合 3D 图形、物理模拟和某种输入来构建任何可以用 3D 表示的东西。
结合一个用于创建和操作交互式、实时 3D 内容的平台与机器学习工具,您可以使用您创建的 3D 世界来训练机器学习模型,有点像真实世界。事实上并不完全像真实世界,但想象起来很有趣,与真实世界有一些真正有用的联系(例如能够生成用于真实世界机器学习应用的数据,以及可以转换为物理真实世界对象的模型,比如机器人)。
提示
当我们说真实世界时,实际上是指物理世界。
将 Unity 与机器学习结合使用是创建模拟和合成数据的绝佳方式,这是本书涵盖的两个不同主题。
本书中使用的资源
我们建议您随着书中的进展逐章自己编写代码跟随学习。
如果您遇到困难,或者只想存档我们版本的代码副本,您可以通过我们的网站找到您需要的内容。
对于我们在书中涉及的某些活动,您将需要资源的副本以获取某些资产,因此我们建议您下载它。
受众和方法
我们为那些对机器学习感兴趣但不一定是机器学习工程师的程序员和软件工程师写了这本书。如果您对机器学习有一些兴趣,或者开始在机器学习领域工作,那么这本书适合您。如果您是一名游戏开发者,对 Unity 或其他游戏引擎有所了解,并希望学习机器学习(无论是为游戏还是其他应用),那么这本书也适合您。
如果你已经是一个机器学习专家,这本书也适合你,但方式有所不同:我们不会深入探讨机器学习的原理和方法。因此,如果你已经深入了解 PyTorch 和类似框架,那么在这里你也会感觉很舒适。如果你对机器学习世界的深层原理不是很了解,也不用担心,这里的内容非常易于理解。使用 Unity 进行仿真和合成的关键在于,你不需要详细了解其中的细节,它们都会“自动”运行(这是我们的最后一句名言,我们知道)。
无论如何,如果你来自软件、机器学习或游戏领域,本书都适合你。这里有适合每个人的内容。我们会教你足够使用 Unity 和机器学习的内容,同时也为你提供了进一步学习感兴趣领域的起点。
本书的组织结构
本书分为三部分。
第 I 部分,“仿真与合成基础”,介绍了仿真与合成的主题,并通过简单的活动逐步引导你进入。
第 II 部分,“为了乐趣与利益仿真世界”,致力于仿真。这是本书的最大部分,因为仿真比合成要复杂得多。在这一部分中,我们几乎一步一步地介绍了一系列仿真活动,随着我们的学习,逐渐建立了更多的概念和方法。到了本部分的结尾,你将接触到许多通过仿真可以走的不同道路。
第 III 部分,“合成数据,真实成果”,致力于合成。虽然比仿真要小得多,但仍然至关重要。你将学习使用 Unity 创建合成数据的基础知识,到最后你将能够制作几乎任何你可能需要的合成。
使用本书
我们将本书围绕活动进行了结构化。我们希望你能与我们一起完成这些活动,并在你感兴趣的地方添加自己的想法(但不要觉得你必须这么做)。
我们采取了基于活动的方法,因为我们认为这是学习 Unity 游戏引擎和机器学习方面所需知识的最佳方式。我们不希望把所有关于 Unity 的知识都教给你,也没有足够的空间来详细解释机器学习的所有细节。
通过活动之间的转换,我们可以根据需要引入或排除内容。我们真心希望你能喜欢我们选择的这些活动!
我们的任务
对于仿真,我们将构建:
-
一个可以将球自行滚动到目标的活动,在第二章中(我们知道,这听起来太不可思议了,但确实是真的!)
-
一个可以将方块推到目标区域的立方体,在第四章中。
-
一辆简单的自动驾驶汽车,驶入一条赛道,在第五章中。
-
通过模仿人类示范训练的追求硬币的球,在第六章
-
一个弹道发射器代理,可以使用课程学习将球发射到目标,第八章
-
一组一起工作以将块推向目标的立方体,在第九章
-
一个能够使用视觉输入(即摄像头)而不是精确测量来平衡球的代理,在第十章
-
一种连接并从 Python 操纵模拟的方式,在第十一章
并进行综合,我们将:
-
生成随机抛掷和放置的骰子图像,在第三章
-
改进骰子图像生成器,改变骰子的地板和颜色,在第十三章
-
生成超市产品图像,以便在复杂背景和随意位置上对 Unity 外图像进行训练,在第十四章
本书中使用的约定
本书使用以下排版约定:
斜体
表示新术语、URL、电子邮件地址、文件名和文件扩展名。
常宽度
用于程序清单,以及在段落中引用程序元素,如变量或函数名称、数据库、数据类型、环境变量、语句和关键字。也用于命令和命令行输出。
常粗体
显示用户应直接输入的命令或其他文本。
常斜体
显示应由用户提供值或由上下文确定的值替换的文本。
提示
这个元素表示一个提示或建议。
注意
这个元素表示一般性说明。
注意
这个元素表示警告或注意事项。
使用代码示例
补充资料(代码示例、练习、勘误等)可从http://secretlab.com.au/books/practical-simulations*下载。
本书的目的是帮助您完成工作。一般来说,如果本书提供示例代码,则可以在您的程序和文档中使用它。除非您复制了大量代码,否则无需联系我们以获得许可。例如,编写使用本书多个代码片段的程序不需要许可。出售或分发 O’Reilly 图书示例需要许可。回答问题并引用本书示例代码不需要许可。将本书示例代码的大量内容整合到产品文档中需要许可。
我们感谢您的使用,但不需要署名。署名通常包括标题、作者、出版商和 ISBN。例如:“Machine Learning 实用模拟,作者 Paris 和 Mars Buttfield-Addison、Tim Nugent 和 Jon Manning。版权 2022 Secret Lab,978-1-492-08992-6。”
如果您认为您使用的代码示例超出了合理使用范围或上述授权,请随时通过permissions@oreilly.com与我们联系。
致谢
Mars 要感谢她的家人和合作者们的支持,以及塔斯马尼亚大学 ICT 学院的人们和澳大利亚更广泛的科技社区为她提供的所有机会。
Jon 感谢他的母亲、父亲以及其他疯狂的大家庭成员们对他的巨大支持。
Paris 感谢他的母亲,没有她,他不可能做任何一件像写书这样有趣的事情,还有他的妻子(也是共同作者)Mars,以及所有和他一起幸运地撰写这本书的朋友们!
Tim 感谢他的父母和家人包容他那相对平淡的生活方式。
我们都想要感谢米歇尔·克罗宁,她真是太了不起了,她的技能和建议对完成这本书至关重要。帕里斯很抱歉我们会在会议中经常岔开话题,但这太有趣了,我们真的很期待未来与您合作更多项目!
特别感谢我们在 O’Reilly Media 的朋友和前编辑雷切尔·鲁梅利奥蒂斯。我们怀念我们一起参加会议喝咖啡的时光。
真的,我们要感谢在编写这本书的过程中与我们互动过的所有 O’Reilly Media 员工。特别要感谢克里斯·福歇尔,他不仅在工作中表现出色,而且对我们非常耐心。还要感谢我们出色的副本编辑伊丽莎白·奥利弗。你们都如此专业、有趣和才华横溢。这真是令人敬畏。
特别感谢托尼·格雷和苹果大学联盟,因为他们为我们和其他在这页上列出的人们提供了巨大的支持。如果不是他们,我们不会写这本书。现在轮到你写书了,托尼——对此我们感到抱歉!
还要感谢尼尔·戈德斯坦,他完全应该为把我们卷入写书这一行业中承担全部责任和/或指责。
我们感谢麦克拉伯的支持(他们知道他们是谁,并继续守望着海豚将军的不可避免的晋升),以及克里斯托弗·卢格教授、莱昂妮·埃利斯博士以及塔斯马尼亚大学的现任和前任工作人员对我们的包容。
还要特别感谢戴夫·J.、杰森·I.、亚当·B.、乔什·D.、安德鲁·B.、杰斯·L.以及所有激励我们并帮助我们的人。非常特别感谢在苹果工作的那群辛勤工作的工程师、作家、艺术家和其他工作人员,没有他们,这本书(以及许多类似的书)将没有存在的理由。
还要感谢我们的技术审阅者们!没有他们的彻底和专业精神以及对我们工作的热情,我们无法写出这本书。还有极高程度的吹毛求疵。我们真的很感激。真心的!
最后,非常感谢您购买我们的书籍——我们非常感激!如果您有任何反馈,请告诉我们。
第一部分: 模拟与合成基础
第一章:介绍合成和模拟
世界对数据需求迫切。机器学习和人工智能是最需要数据的领域之一。算法和模型不断增大,而现实世界的数据却是有限的。手动创建数据和现实世界系统并不可扩展,我们需要新的方法。这就是 Unity 以及传统用于视频游戏开发的软件发挥作用的地方。
本书关注合成和模拟,并利用现代视频游戏引擎在机器学习中的强大力量。表面上,将机器学习与模拟及合成数据结合起来似乎相对简单,但事实上,将视频游戏技术引入机器学习的严肃商业世界,却令许多公司和企业望而却步。
我们希望本书能引导你进入这个世界,并减少你的顾虑。本书的三位作者是具有丰富计算机科学背景的视频游戏开发者,还有一位是认真的机器学习和数据科学家。我们在多年间在各种行业和方法中积累的综合视角和知识,在这里呈现给你。
本书将带你探索使用 Unity 视频游戏引擎来构建和训练机器学习系统的方法和技术,以及使用和生成的数据。本书有两个明显的领域:模拟 和 合成。模拟指的是在你自己创建的虚拟世界中构建学习做某事的虚拟机器人(称为代理)。合成指的是构建虚拟对象或世界,输出有关这些对象和世界的数据,并将其用于在游戏引擎之外训练机器学习系统。
模拟和合成都是强大的技术,能够为以数据为中心的机器学习和人工智能带来新的和令人兴奋的方法。
一个全新的 ML 世界
不久后我们将进入本书的结构,但首先,这里是本章剩余部分的概述,分为四个部分:
-
在 “领域” 中,我们将介绍本书探索的机器学习领域:模拟与合成。
-
在 “工具” 中,我们将会介绍我们将要使用的工具——Unity 引擎,Unity ML-Agents Toolkit,PyTorch 和 Unity Perception,以及它们如何结合在一起。
-
在 “技术” 中,我们将探讨我们将用于机器学习的技术:近端策略优化(PPO),软演员-评论家(SAC),行为克隆(BC)和 生成对抗性模仿学习(GAIL)。
-
最后,在 “项目” 中,我们将总结我们将在本书中构建的项目,以及它们与领域和工具的关系。
在本章结束时,你将准备好深入探索模拟和合成的世界,你将了解游戏引擎的工作原理,并理解为什么它几乎是机器学习的完美工具。在本书结束时,你将准备好解决任何可能从游戏引擎驱动的模拟或合成中受益的问题。
领域
本书的双大支柱是模拟和合成。在本节中,我们将详细解释这两个术语的确切含义,以及本书将如何探索这些概念。
模拟和合成是人工智能和机器学习未来的核心部分。
许多应用立即显而易见:将模拟与深度强化学习结合起来,验证新机器人在建造物理产品之前的功能;在没有汽车的情况下创建你自动驾驶汽车的大脑;在没有仓库(或机器人)的情况下建造你的仓库并训练你的拾取和放置机器人。
其他用途更微妙:通过模拟来合成数据,而不是使用从真实世界记录的信息,然后训练传统的机器学习模型;利用行为克隆与模拟相结合,以真实用户活动为基础,在本来是完美的机器学习任务中添加生物或人类化的元素。
一个视频游戏引擎,比如 Unity,可以模拟足够接近真实世界的环境,并具备足够的逼真度,从而对基于模拟的机器学习和人工智能非常有用。游戏引擎不仅可以让你模拟足够大的城市和汽车来测试、训练和验证自动驾驶汽车深度学习模型,还可以模拟到引擎温度、剩余功率、激光雷达、声纳、X 射线等硬件的细节。想要在你的机器人中加入一个新的高端昂贵传感器?在你投资任何一分钱之前试一试,看看它是否能提高性能。节省金钱、时间、计算资源和工程资源,更好地了解你的问题空间。
是完全不可能的,还是可能不安全的,去获取足够多的你的数据?创建一个模拟并测试你的理论。便宜、无限的训练数据只隔着一个模拟。
模拟
当我们说模拟时,我们并不指代一个具体的东西。在这个背景下,模拟可以广泛指使用游戏引擎开发场景或环境,然后应用机器学习。在本书中,我们将模拟作为一个术语,广泛涵盖以下内容:
-
使用游戏引擎创建一个具有特定组件的环境,这些组件是代理或者代理们
-
赋予代理(们)移动的能力,或者与环境和/或其他代理进行互动或工作
-
将环境与机器学习框架连接起来,训练一个能够在环境中操作代理(们)的模型
-
使用训练好的模型来操作未来的环境,或将模型连接到其他同样配备的代理人(例如在真实世界中,与实际机器人)
合成
在本书的背景下,合成是一件相对容易明确的事情:合成是使用游戏引擎创建表面上看来是虚假的训练数据。例如,如果你为超市建立某种图像识别机器学习模型,你可能需要从多个角度和不同背景及环境中拍摄特定麦片品牌的盒子的照片。
使用游戏引擎,你可以创建和加载一个麦片盒的 3D 模型,然后在不同角度、背景和倾斜角度下生成数千张图像,将它们综合起来,并保存为标准图像格式(例如 JPG 或 PNG)。然后,利用你庞大的训练数据,你可以使用完全标准的机器学习框架和工具包(例如 TensorFlow、PyTorch、Create ML、Turi Create,或众多基于网络服务的训练系统之一),训练一个能识别你的麦片盒的模型。
然后可以将这种模式部署到例如某种购物车上的 AI 系统中,帮助人们购物,引导他们找到购物清单上的物品,或帮助店员正确放置货架并进行库存预测。
合成是使用游戏引擎创建训练数据,而游戏引擎本身通常与训练过程无关或关联很少。
工具
本章向您介绍了我们将在旅程中使用的工具。如果您不是游戏开发者,那么您将遇到的主要新工具是 Unity。Unity 传统上是一个游戏引擎,但现在被推广为实时 3D 引擎。
让我们逐一介绍本书中你将遇到的工具。
Unity
首先要明确,Unity 是一款游戏和视觉效果引擎。Unity Technologies 将 Unity 描述为实时 3D 开发平台。我们不会为你重复 Unity 网站上的营销材料,但如果你对公司如何定位感兴趣,你可以查看它。
提示
本书不旨在教授你 Unity 的基础知识。本书的一些作者已经从游戏开发的角度撰写了几本书籍,如果你感兴趣,你可以在 O'Reilly Media 找到它们。你不需要像游戏开发者那样学习 Unity,以便在模拟和机器学习合成中使用它;在本书中,我们将只学习足够的 Unity以在这方面取得效果。
Unity 用户界面看起来几乎与其他拥有 3D 功能的专业软件包相同。我们在 Figure 1-1 中包含了一个示例截图。该界面有可以操作的窗格,用于处理对象的 3D 画布以及许多设置。我们稍后会回到 Unity 用户界面的具体内容。你可以在Unity 文档中获取其不同元素的全面概述。
本书中你将同时使用 Unity 进行仿真和合成。

Figure 1-1. Unity 用户界面
Unity 引擎配备了一套强大的工具,允许你模拟重力、力量、摩擦、运动、各种传感器等。这些工具正是构建现代视频游戏所需的完整工具集。事实证明,这些工具也是创建仿真和合成数据所需的完全相同的工具集。但是,考虑到你正在阅读我们的书籍,你可能已经猜到了这一点。
注意
本书适用于 Unity 2021 及更新版本。如果你在 2023 年或之后阅读本书,Unity 的界面可能与我们的截图略有不同,但是概念和整体流程应该没有太大变化。游戏引擎通常会不断添加功能,而不是移除它们,所以你可能会看到的最常见的变化是图标看起来略有不同之类的事情。关于任何可能发生变化的最新注释,请访问我们的专用书籍网站。
通过 Unity ML-Agents 的 PyTorch
如果你身处机器学习领域,你可能已经听说过 PyTorch 开源项目。作为学术界和工业界最受欢迎的机器学习平台和生态系统之一,它几乎无所不在。在模拟和综合空间中,情况也一样:PyTorch 是首选框架之一。
在本书中,我们探索的基础机器学习大部分将通过 PyTorch 完成。我们不会深入探讨 PyTorch 的细枝末节,因为我们将大部分时间通过 Unity ML-Agents 工具包来使用 PyTorch。我们马上会讨论 ML-Agents 工具包,但你需要记住的是,PyTorch 是驱动 Unity ML-Agents 工具包所做工作的引擎。它一直在那里,在幕后运行,如果需要或者你知道自己在做什么,你可以对其进行调整,但大部分时间你根本不需要碰它。
小贴士
我们将在本节的其余部分讨论 Unity ML-Agents 工具包,所以如果你需要回顾 PyTorch,我们强烈推荐访问PyTorch 网站,或者 O’Reilly Media 出版的关于该主题的众多优秀书籍之一。
PyTorch 是一个库,支持使用数据流图进行计算。它支持使用 CPU 和 GPU(以及其他专用的机器学习硬件)进行训练和推理,并且可以在从严肃的 ML 优化服务器到移动设备的各种平台上运行。
注意
因为在本书中您将使用 PyTorch 的大部分工作都是抽象化的,所以我们很少会直接谈论 PyTorch 本身。因此,虽然它几乎是我们要探索的一切的背景,但您与它的主要接口将是通过 Unity ML-Agents 工具包和其他工具。
我们将使用 PyTorch,通过 Unity ML-Agents,进行本书中的所有模拟活动。
Unity ML-Agents 工具包
Unity ML-Agents 工具包(我们通常将其缩写为 UnityML 或 ML-Agents)是本书中您将进行的工作的支柱。ML-Agents 最初是作为一个简陋的实验项目发布的,然后逐渐扩展到包含一系列功能,使 Unity 引擎能够作为训练和探索智能代理和其他机器学习应用的模拟环境。
这是一个开源项目,提供了许多令人兴奋和经过深思熟虑的示例(如图 1-2 所示),并且可以通过其 GitHub 项目免费获取。

图 1-2. Unity ML-Agents 工具包的“主题图像”,展示了一些 Unity 的示例角色
如果不明显的话,我们将在本书中的所有模拟活动中使用 ML-Agents。我们将向您展示如何在您自己的系统上运行 ML-Agents,在第二章中。不要急着安装它!
Unity Perception
Unity Perception 包(我们通常将其缩写为 Perception)是我们将使用来生成合成数据的工具。Unity Perception 提供了一系列额外功能给 Unity 编辑器,允许您适当设置场景以创建 伪造 数据。
像 ML-Agents 一样,Perception 也是一个开源项目,您可以通过其 GitHub 项目找到它。
技术
ML-Agents 工具包支持使用 强化学习 和 模仿学习 技术进行训练。这两种技术都允许代理通过反复的试验和错误(或“强化”)来“学习”所需的行为,并最终在提供的成功标准下收敛到理想的行为。这些技术的区别在于用于评估和优化代理性能的标准。
强化学习
强化学习(RL)是指使用显式奖励进行学习过程。实施方负责为可取行为奖励“分数”,并为不良行为扣除分数。
在这一点上,你可能会想,“如果我不得不告诉它该做什么和不该做什么,机器学习的意义何在?” 但让我们来想象一下,例如,教一个双足代理行走。为每个步态变化的每个状态变化提供明确的一套指令——每个关节应该以多少度的旋转顺序进行——将是广泛且复杂的。
但是通过给代理几个点以向前移动,达到终点时给很多点,它跌倒时给负点,并尝试数十万次以获得正确结果,它将能够自行找出具体细节。因此,RL 的巨大优势在于能够给出以目标为中心的指令,需要复杂行为来实现。
ML-Agents 框架提供了两种不同的 RL 算法实现:近端策略优化(PPO)和 软演员-评论家(SAC)。
警告
注意这些技术和算法的缩写:RL、PPO 和 SAC。记住它们,我们将在整本书中经常使用它们。
PPO 是一个强大的通用 RL 算法,已经反复证明在各种应用中非常有效且通常稳定。PPO 是 ML-Agents 中使用的默认算法,并且将在本书的大部分内容中使用。稍后我们将更详细地探讨 PPO 的工作原理。
提示
近端策略优化是由 OpenAI 团队创建的,并于 2017 年首次亮相。如果您对细节感兴趣,可以阅读 arXiv 上的原始论文。
SAC 是一个 离线 RL 算法。稍后我们将详细讨论其含义,但目前来看,相对于像 PPO 这样的 在线 方法,它通常减少了所需的训练周期,但增加了内存需求。在本书中我们将会使用一两次 SAC,并在到达时更详细地探讨其工作原理。
提示
软演员-评论家是由伯克利人工智能研究组(BAIR)创建的,并于 2018 年 12 月首次亮相。您可以阅读 原始发布文档 以获取详细信息。
模仿学习
类似于 RL,模仿学习(IL)消除了需要定义复杂指令的需求,而是简单地设定目标。然而,IL 也消除了需要定义显式目标或奖励的必要性。相反,通常会提供一个演示——通常是一个由人手动控制的代理的录像——并根据代理模仿所展示行为来内在定义奖励。
这对于复杂领域尤其有用,其中期望的行为非常具体或者大多数可能的行为都是不可取的。对于多阶段目标(代理需要按特定顺序实现中间目标以获得奖励),使用 IL 进行训练也非常有效。
ML-Agents 框架内置了两种不同的 IL 算法的实现:行为克隆(BC)和生成对抗模仿学习(GAIL)。
BC是一种 IL 算法,用于训练代理精确模仿演示的行为。在这里,BC 仅负责定义和分配内在奖励;而现有的 RL 方法,如 PPO 或 SAC,则用于基础训练过程中。
GAIL是一种生成对抗方法,应用于 IL。在 GAIL 中,两个独立的模型在训练过程中互相对抗:一个是代理行为模型,它尽最大努力模仿给定的演示;另一个是鉴别器,它会反复接收人类驱动的演示者行为或代理驱动的模型行为的片段,并且必须猜测是哪一个。
提示
GAIL 源于 Jonathan Ho 和 Stefano Ermon 的论文“生成对抗模仿学习”。
随着鉴别器在识别模仿者方面变得更加熟练,代理模型必须改进以再次欺骗它。同样地,随着代理模型的改进,鉴别器必须建立越来越严格或更为微妙的内部标准来识别伪造的行为。在这种来回之间,每个人都被迫进行迭代改进。
提示
行为克隆通常是应用中最佳的方法,因为可以演示出代理可能遇到的所有或几乎所有条件。相比之下,GAIL 能够推广新的行为,这允许从有限的演示中学习模仿。
在早期训练中通常通过使用 BC,BC 和 GAIL 也可以一起使用,然后将部分训练好的行为模型分配为 GAIL 模型的代理部分。从 BC 开始,通常会使代理在早期训练中迅速改进,而在后期训练中切换到 GAIL 将允许其开发超出演示的行为。
混合学习
虽然单独使用 RL 或 IL 几乎总是可以达到目的,它们可以结合使用。然后,代理可以通过明确定义的达成目标的奖励和有效模仿的隐式奖励来获得奖励并且其行为得到指导。甚至可以调整每种奖励的权重,以便训练代理将其中一个作为主要目标或两者作为平等目标。
在混合训练中,IL 演示旨在在训练初期将代理放在正确的路径上,而显式的 RL 奖励则鼓励在此之内或之外的特定行为。这在理想的代理应该优于人类演示者的领域中是必要的。由于这种早期的辅助作用,同时使用 RL 和 IL 训练可以显著加快训练速度,让代理在解决复杂问题或在稀疏奖励情境中导航复杂环境时更快地训练出解决方案。
提示
稀疏奖励环境是那些代理特别少接收显式奖励的环境。在这样的环境中,代理“偶然”发现一个可奖励行为并因此收到其首次指示应该做什么,可能会浪费大部分可用的训练时间。但与 IL 结合,演示可以提供有关朝向显式奖励的理想行为的信息。
这些共同产生了一个复杂的奖励方案,可以鼓励代理从事高度特定的行为,但是需要代理达到成功的复杂程度的应用程序并不多。
技术总结
本章是概念和技术的入门调查,你将在本书的过程中接触和使用我们在这里看到的每一种技术。通过这样做,你将更加熟悉它们在实际操作中的工作方式。
其要义如下:
-
Unity ML-Agents Toolkit 目前提供了跨两个类别的一系列训练算法:
-
对于强化学习(RL):近端策略优化(PPO)和软演员-评论家(SAC)
-
对于模仿学习(IL):行为克隆(BC)和生成对抗性模仿学习(GAIL)
-
-
这些方法可以独立使用,也可以一起使用:
-
RL 可以单独使用 PPO 或 SAC,也可以与 BC 等 IL 方法结合使用。
-
BC 可以单独使用,作为使用 GAIL 方法或与 RL 结合的一步。
-
-
RL 技术需要一组定义好的奖励。
-
IL 技术需要提供一些演示。
-
RL 和 IL 通过实践学习。
在本书剩余的模拟主题探索过程中,我们将涉及或直接使用所有这些技术。
项目
本书是一本实际、务实的工作。我们希望你尽快使用模拟和合成开始工作,并且我们假设你在可能的时候更愿意专注于实施。
因此,虽然我们经常探索幕后情况,但本书的实质内容在于我们将共同构建的项目。
本书的实际基于项目的部分分为我们之前讨论的两个领域:模拟和合成。
模拟项目
我们的模拟项目将是多样的:当你在 Unity 中构建模拟环境时,代理可以通过多种方式观察和感知其世界。
一些模拟项目将使用一个代理来观察世界,使用向量观察:也就是说,数字。任何你想发送的数字。从字面上讲,任何你喜欢的数字。实际上,向量观察通常是如代理距离某物的距离或其他位置信息之类的东西。但实际上,任何数字都可以作为一种观察。
一些仿真项目将使用一个观察世界的代理,使用视觉观察,也就是图片!因为 Unity 是一个游戏引擎,而游戏引擎和电影一样有摄像机的概念,所以你可以简单地(虚拟地)在代理身上安装摄像机,并让它存在于游戏世界中。这些摄像机的视角可以输入到你的机器学习系统中,让代理根据摄像机的输入学习它的世界。
我们将使用 Unity、ML-Agents 和 PyTorch 的仿真示例包括:
-
一个可以自行滚动到目标的球,在第二章中(我们知道,听起来太神奇了,但这是真的!)
-
一个可以推动方块进入目标区域的立方体,在第四章中。
-
一辆简单的自动驾驶汽车在第五章中行驶。
-
一个通过模仿人类演示训练来寻找硬币的球,在第六章中。
-
一个弹道发射代理人,可以使用课程学习将球发射到目标位置,在第八章中。
-
一组立方体共同工作,将方块推向目标,在第九章中。
-
训练代理人使用视觉输入(即摄像机),而不是精确测量来平衡球顶部,在第十章中。
-
使用 Python 连接并操作 ML-Agents,在第十一章中。
综合项目
我们的综合项目会比仿真项目少一些,因为领域要简单一些。我们专注于利用 Unity 提供的材料来展示仿真的可能性。
我们将使用 Unity 和 Perception 的综合示例包括:
-
一个随机投掷和放置骰子图像生成器,在第三章中。
-
通过改变骰子图像生成器的地板和颜色来改进,在第十三章中。
-
生成超市产品图像,以允许在具有复杂背景和随意位置的图像上进行 Unity 之外的训练,在第十四章中。
一旦生成了合成数据,我们不会专注于实际的训练过程,因为关于这个主题有许多很好的书籍和在线文章,而我们在这本书中只有有限的页面。
总结与下一步
你已经迈出了第一步,本章包含了一些必要的背景材料。从这里开始,我们将通过实践来教你。这本书的标题中有实践这个词是有原因的,我们希望你通过构建自己的项目来感受仿真和综合。
注意
你可以在我们的书籍专用网站找到每个示例的代码 — 我们建议你在需要时才下载这些代码。我们也会及时更新网站,以便您了解任何需要注意的更改,请务必收藏该网址!
在下一章中,我们将探讨如何创建您的第一个模拟,实现一个代理程序在其中执行某些操作,并使用强化学习训练机器学习系统。
第二章:创建你的第一个模拟场景
我们将从一个简单的 模拟环境 开始:一个可以在平台上滚动的球代理。正如我们之前所说,我们知道这是一个很多处理的问题,但我们认为你能够应对这些激动人心的挑战,并且最终能够更好地理解 Unity 中的机器学习和模拟。
每个人都会记得他们的第一个模拟场景
在本章中,我们将使用 Unity 构建一个全新的模拟环境,创建一个代理,然后使用强化学习训练该代理在环境中完成任务。这将是一个非常简单的模拟环境,但它将展示许多重要内容:
-
如何通过使用少量简单对象的小集合在 Unity 中组装场景是多么简单。
-
如何使用 Unity Package Manager 将 Unity ML-Agents 工具包的 Unity 侧导入 Unity,并为机器学习设置一个 Unity 项目。
-
如何在你的模拟对象中设置一个简单的代理,以便使其能够完成任务
-
如何手动控制你的代理以测试模拟环境
-
如何使用 Unity ML-Agents Toolkit 命令行工具(CLI)开始训练运行,并如何启动 TensorBoard 监控训练进展
-
如何将训练好的模型文件带回 Unity 模拟环境并使用训练好的模型运行代理。
通过本章的学习,你将对 Unity 和使用 ML-Agents 工具包来解决更深层次、更复杂问题感到更加自如。
注
这一章和后面几章不会揭示底层的机器学习算法(请记住本书标题中的“实用”一词?),但我们将开始逐步了解机器学习算法的工作原理,我们承诺。
我们的模拟
我们的第一个模拟场景看似简单:一个小环境中放置了一个球,它位于虚空中的地面上。球可以自由滚动,包括从地面上掉落到虚空中。它是唯一可控制的元素:既可以由用户(即我们,用于测试目的)控制,也可以由强化学习 ML-Agents 系统控制。
因此,球将作为我们的 代理,其目标是在不掉落到 地面 之外尽快到达 目标。我们将构建的模拟环境在 Figure 2-1 中展示过。

图 2-1. 我们将要构建的模拟场景
广义上讲,创建任何模拟环境并训练一个或多个代理在其中运作的步骤如下:
-
在 Unity 中构建环境:这个环境是一个包含物体的物理模拟。
-
实现机器学习元素:即我们需要一个能在环境中运作的代理。
-
实现代码,告诉代理如何观察环境,如何在环境中执行动作,如何计算可能收到的奖励,并在成功或失败时如何重置自身或环境。
-
在环境中训练代理。
我们将在本章中执行这四个步骤。
设置
对于仿真和机器学习所需的工具及其讨论,请参考第一章。本节将为您提供关于完成此特定活动所需的各个部分的快速摘要。
具体来说,为了完成本章的活动并构建简单的模拟环境,您需要执行以下操作:
-
安装 Unity 2021 或更新版本. 本书不会教授您 Unity 的基础知识(如果您感兴趣,我们已经写了一本优秀的书籍),但值得注意的是,Unity 的安装方式经常变化,比这本书教授的基础材料更频繁,因此我们建议查看 Unity 网站上的Unity 安装指南以获取最新的安装信息。跳转到那里,安装适合的 Unity 版本,然后回来。我们还会在这里等您。
提示
虽然 Unity ML-Agents Toolkit 可与任何新于 2018.4 的 Unity 版本配合使用,但我们建议您安装最新的 2021 版本 Unity。您可能会找到 Unity 的 2021 LTS 版本。LTS 代表长期支持,这是 Unity 团队在指定时间内维护的版本,包括 bug 和安全修复。如果您正在进行生产工作,并且已经学完了学习(如果有“学完学习”这样的事情的话),以它为基础是一个安全的选择。您可以在Unity 文档中了解更多关于 Unity LTS 版本的信息。
-
安装 Python. 您需要安装一个新于或等于 Python 3.6.1 且旧于(但不包括)Python 3.8 的版本。如果您没有偏好或者没有现有的 Python 环境,我们建议安装 Python 3.7.8。正如我们在第一章中讨论的,Unity ML-Agents Toolkit 的大部分依赖于 Python。
警告
在撰写本文时,Unity ML-Agents Toolkit 不支持 Python 3.8。您需要使用 Python 3.6.1 或更新版本,或任何 Python 3.7 版本。如果您使用的是 Windows,则还需要 Python 的 x86-64 版本,因为该工具包不兼容 x86 版本。如果您在运行花哨的 Apple Silicon macOS 设备,则可能需要在 Rosetta 2 下运行 Python,但在 Apple Silicon 版本的 Python 下也可能正常工作。这方面的情况正在快速变化。查看本书的网站获取关于 Apple Silicon 和 Unity 仿真的最新信息。
要安装 Python,请访问Python 下载页面,获取适合你操作系统的安装程序。如果你不想直接以这种方式安装 Python,也可以使用操作系统的包管理器(如果有的话),或者一个全面的 Python 环境(我们非常喜欢 Anaconda),只要你安装的 Python 版本符合我们刚才提到的版本和架构版本。
注意
你还需要确保你的 Python 安装包含
pip(或pip3),即 Python 包管理器。如果安装出现问题,可以参考Python 文档。我们强烈建议你为 Unity ML-Agents 工作使用虚拟环境(“venv”)。 要了解更多关于创建 venv 的信息,可以参考Python 文档,或者按照我们下面概述的基本步骤进行操作。
注意
如果你有在你的机器上设置 Python 的首选方式,就用那个。我们不会告诉你如何生活。如果你对 Python 感到满意,那么实际上你只需要确保遵守 ML-Agents 的版本限制,安装正确的包,并在需要时运行它即可。在涉及多个版本时,Python 是出了名的不脆弱,对吧?(作者注:我们是澳大利亚人,所以这段话应该带有澳式口音,充满尊重的讽刺。)
你可以像这样创建一个虚拟环境:
python -m venv UnityMLVEnv提示
我们建议将其命名为
UnityMLVEnv或类似的名称。但是名称由你决定。你可以像这样激活它:
source UnityMLVEnv/bin/activate -
安装 Python
mlagents包。 一旦你安装了 Python,并设置了 Unity ML-Agents 的虚拟环境,可以通过以下命令安装 Pythonmlagents包:pip3 install mlagents提示
询问
pip,Python 包管理器,获取并安装mlagents也将安装所有mlagents所需的依赖项,包括 TensorFlow。 -
克隆或下载 Unity ML-Agents Toolkit GitHub 仓库。 你可以使用以下命令克隆该仓库:
git clone https://github.com/Unity-Technologies/ml-agents.git
我们主要假设你是你选择的操作系统的开发经验丰富的用户。如果你需要完成任何这些设置步骤的指导,请不要绝望!我们建议你查阅文档,以便快速上手。
在完成前面的四个步骤后,你已经完成了与 Python 相关的设置要求。接下来我们将看看 Unity 的要求。
创建 Unity 项目
创建模拟环境的第一步是创建一个全新的 Unity 项目。Unity 项目与任何其他开发项目非常相似:它是 Unity 声明为项目的文件、文件夹和东西的集合。
注意
我们的屏幕截图将来自 macOS,因为这是我们日常使用的主要环境。本书中使用的所有工具在 macOS、Windows 和 Linux 上均可使用,所以请使用您偏好的操作系统。我们会尽量在操作系统之间指出显著差异(但在我们所做的事情方面,并没有太多的差异)。我们在所有支持的平台上测试了所有活动,一切都在我们的机器上运行良好。
要创建项目,请确保您已完成所有设置步骤,然后执行以下操作:
-
打开 Unity Hub 并创建一个新的 3D 项目。如图 Figure 2-2 所示,我们将命名为“BallWorld”,但请随意发挥创意。
![psml 0202]()
Figure 2-2. 为我们的新环境创建 Unity 项目
-
选择菜单 Window → Package Manager,并使用 Unity Package Manager 安装 ML-Agents Toolkit 包 (
com.unity.ml-agents),如图 Figure 2-3 所示。![psml 0203]()
Figure 2-3. 安装 Unity ML-Agents Toolkit 包
注意
就像现在的所有东西一样,Unity 有一个包管理器。它实际上相当不错,但也像现在的所有东西一样,有时会有些脆弱。如果遇到问题,请重新启动 Unity 并重试,或者尝试下一个注意事项中解释的手动安装过程。
包下载和安装可能需要一些时间。下载完成后,您将看到 Unity 正在导入它,如图 Figure 2-4 所示。
![psml 0204]()
Figure 2-4. Unity 导入 ML-Agents 包
如果您想手动安装包,或者由于某些原因(如公司政策)无法使用 Unity 包管理器,则可以:
-
通过选择菜单 Window → Package Manager 打开包管理器。
-
在包管理器中点击 + 按钮,并选择“从磁盘添加包…”。
-
在您克隆的 Unity ML-Agents Toolkit 副本中找到 com.unity.ml-agents 文件夹,详见 “设置”。
-
选择 package.json 文件。
欲了解更多信息,请查看Unity 文档,了解如何从本地文件夹安装包。
-
-
确认在项目视图的 Packages 下有一个 ML Agents 文件夹,如图 Figure 2-5 所示。
![psml 0205]()
Figure 2-5. 如果您能看到 ML Agents 文件夹,则项目已准备就绪
您的项目已准备就绪。我们建议您在这一点上将其推送到某种源代码控制,或者将其复制为本书中所有 ML-Agents 工作的新起点。
提示
每次你想在 Unity 中创建一个新的模拟环境时,你都需要这个基本的 Unity 引擎起始点。换句话说,你需要打开一个带有安装了 Unity 包的新 Unity 项目,并且每个你想要工作的 ML-Agents 项目都需要进行这样的设置。Python 设置实际上只需要一次,它设置了你机器上可用的 Python 组件,但是 Unity 设置需要为你想要工作的每个项目都进行。
包罗万象
可能会令人稍感困惑,因为有许多名称相似的内容。在我们继续之前,让我们稍微解释一下。
在这里实际上有三组事物,你可以把它们都称为“ML-Agents 包”:
-
Python 包
mlagents。这是一个 Python 包,我们之前使用 Python 包管理器pip3安装了它。它是 Unity ML-Agents Toolkit 的一部分,并通过 Python 包索引 分发(这就是你之前使用pip3安装它的方式)。当我们谈论这个 Python 包时,我们会称之为mlagents。 -
Unity 包
com.unity.ml-agents。你可以通过 Unity 包管理器安装此包,就像我们在“创建 Unity 项目”中所做的那样。它还是 Unity ML-Agents Toolkit 的一部分,并通过 Unity 包管理器分发(可以通过自动化过程将其安装到 Unity 项目中,其中你可以从 Unity 包管理器界面的列表中选择它,或者手动方式,其中你可以给 Unity 包管理器一个正确形式的 Unity 包的 Git URL)。当我们谈论这个 Unity 包时,我们会将其称为com.unity.ml-agents、ML-Agents、Unity ML-Agents 包或 ML-Agents Toolkit。这个包允许你在 Unity 编辑器内使用 ML-Agents 的功能,并在你在 Unity 中编写的 C# 代码中使用。注意
你需要安装
mlagentsPython 包、安装 Unity ML-Agents 包,并克隆本地 ML-Agents 仓库的一个副本,因为每个安装都提供了不同的功能集。Python 包使你能够在终端中运行特定命令来训练代理,克隆的 Git 仓库提供了一些有用的示例代码和资源,Unity 包提供了在 Unity 场景中创建代理和其他 ML-Agents(或相关)组件所需的组件。每个都非常有用。 -
克隆的 Unity ML-Agents Toolkit GitHub 仓库 的副本。这是 GitHub 仓库内容的本地副本,我们建议你克隆或下载它,因为它包含了有用的文档、示例文件等。当我们提到这个时,我们会称其为“你的本地 ML-Agents 仓库副本”或类似的名称。
警告
如果您使用的是 Apple Silicon,可以使用 Apple Silicon 版本的 Unity,但在 Intel 环境下需要使用 Python。最简单和最懒的方法是在 Rosetta 下运行您的终端应用程序,但也有其他方法。讨论 Python 在 Apple Silicon 上的使用超出了本书的范围,但如果您处于这种情况下,网上有很多资源。请记住:在 macOS 上,Unity 可以是任何平台,但 ML-Agents 和 Python(对于这种用例)必须是 Intel。
环境
基本设置完成后,现在可以在 Unity 编辑器中开始构建您的模拟环境了。
这涉及在 Unity 中创建一个作为模拟环境的场景。我们正在构建的模拟环境具有以下要求:
-
一个地板,供代理在其上移动
-
一个代理要寻找的目标
我们还需要创建代理本身,但我们将在“代理”中进行讨论。
地板
地板是代理会移动的地方。它存在是因为我们的代理存在于 Unity 的物理模拟引擎内,如果没有地板,它将会掉落到地面上。
小贴士
在 Unity 中,地板概念并不特殊。我们只是选择使用平面作为地板,但我们可以使用任何存在于物理系统中并且足够大的物体作为地板。
要创建地板,请确保您已打开一个 Unity 场景,然后按照以下步骤操作:
-
打开 GameObject 菜单 → 3D Object → Plane。点击在 Hierarchy 视图中创建的新平面,在 Inspector 视图中,设置其名称为“Floor”或类似的名称,如 图 2-6 所示。这是我们的地板。
-
当地板被选中时,使用 Transform Inspector 将其位置设置为
(0, 0, 0),旋转设置为(0, 0, 0),缩放设置为(1, 1, 1),如 图 2-7 所示。我们这样做是为了确保地板处于合理的位置和方向,并且具有足够的比例。这些值可能已经是默认值了。

图 2-6. 创建并命名地板

图 2-7. 设置地板的变换
小贴士
如果您需要帮助创建 Unity 场景,请查看Unity 文档。总体来说,一个场景包含对象,您可以使用 Assets → Create → Scene 菜单创建新的场景。
对于地板,我们不需要创建空虚空,因为 Unity 已经为我们提供了一个(即每个场景只是一个虚空,除非你添加了物体)。不要忘记使用文件菜单保存您正在工作的场景。
小贴士
您可以通过在 Project 视图中找到默认场景,右键单击它,然后选择重命名来将其名称改为不同于“SampleScene”的名称。
目标
目标是我们的代理将在地板上寻找的东西。再次强调,Unity 没有特殊的目标概念;我们只是称呼我们正在制作的立方体为目标,因为这在场景和模拟计划中是相关的。
要在你的场景中创建目标,请按照以下步骤操作:
-
打开游戏对象菜单 → 3D 对象 → 立方体。与地板类似,点击层级视图中的新立方体,并使用检视器将其命名为“目标”或类似名称,如 图 2-8 所示。这将成为目标,正如你所见,它是一个非常引人注目的目标!它可能部分嵌入在地板平面中。
![psml 0208]()
图 2-8. 创建并命名目标
-
当选择目标时,如同对地板操作一样,使用变换检视器设置其位置、旋转和缩放。在本例中,值应该类似于
(3, 0.5, 3)、(0, 0, 0)和(1, 1, 1),如 图 2-9 所示。![psml 0209]()
图 2-9. 至今为止的目标和环境变换
到目前为止,这就是我们为目标所需做的一切。此时,你的环境应该与 图 2-9 类似。不要忘记再次保存你的场景。
代理
下一步是创建代理。代理是在环境中移动的东西。它将是一个能够在周围滚动并寻找目标的球体。
按照以下步骤创建代理:
-
打开游戏对象菜单 → 3D 对象 → 球体。将球体命名为“代理”或类似名称。
-
使用检视器设置变换的位置、旋转和缩放为
(0, 0.5, 0)、(0, 0, 0)和(1, 1, 1)。你的代理应该与地板上的位置类似,如 图 2-10 所示。![psml 0210]()
图 2-10. 场景中的代理
-
在代理的检视器底部使用“添加组件”按钮,如 图 2-11 所示,并向代理添加一个刚体组件。你不需要在刚体组件上做任何更改。
![psml 0211]()
图 2-11. 添加组件按钮
这就是代理的物理方面的全部内容。接下来,我们需要给代理添加一些逻辑,并促使其连接到机器学习的一面:
-
选择代理,并通过检视器的“添加组件”按钮添加一个新的脚本组件,如 图 2-12 所示。将脚本命名为“BallAgent”,然后点击“创建并添加”。
![psml 0212]()
图 2-12. 为代理创建脚本
你应该在代理的检视器中看到一个新的脚本组件附加在上面,如 图 2-13 所示。
![psml 0213]()
图 2-13. 脚本已附加到代理
-
在 Unity 项目视图中双击球代理脚本以在代码编辑器中打开。
提示
基本上,使用 Unity ML-Agents 构建的代理需要附加一个告诉 Unity 其父类是
Agent的脚本。Agent是 ML-Agents Unity 包提供的一个类。作为Agent的子类意味着我们必须实现或覆盖 Unity ML-Agents 包提供的某些方法。这些方法允许我们控制和处理代理以进行机器学习。 -
一旦脚本打开,将以下代码添加到顶部,在
using UnityEngine;行下面:using Unity.MLAgents; using Unity.MLAgents.Sensors;这些行导入了我们导入的 ML-Agents Unity 包的适当部分,并允许我们在代码中使用它们。
-
找到
Update()方法并删除它。在这个模拟中我们不会使用它。 -
找到
BallAgent类的定义,并将其从这个状态更改为:public class BallAgent : MonoBehaviour到这里:
public class BallAgent : Agent
这使得我们的新类BallAgent成为Agent的子类(来自 ML-Agents),而不是默认的MonoBehaviour(大多数非 ML Unity 对象的父类)。
这些是在 Unity 中设置模拟环境中代理的基础知识。接下来我们需要添加一些逻辑,使我们的代理能够在场景中移动(嗯,地板上)。在这之前,我们将对到目前为止制作的三个对象进行分组。
将代码保存在代码编辑器中,然后切换回 Unity 编辑器,在场景中执行以下操作:
-
打开 GameObject 菜单 → 创建空物体以创建一个空的 GameObject。
-
选择空 GameObject 并使用检视面板将其命名为“训练区域”。
提示
当你重命名某物件时,可以在层次视图中选择它,而不是使用检视面板,然后按下回车键/Enter 键进入编辑模式。再次按下回车键/Enter 键保存新名称。
-
将训练区域的 GameObject 的位置、旋转和比例分别设置为
(0, 0, 0)、(0, 0, 0)和(1, 1, 1)。 -
在层次视图中,将地板、目标和代理拖放到训练区域中。此时你的层次视图应该像图 2-14 一样。这样做时,场景中的任何位置都不应该改变。

图 2-14. 训练区域
完成后别忘了保存场景。
启动和停止代理
我们将使用强化学习来训练这个代理。在 Unity 环境中进行强化学习训练涉及运行许多episode,在每个 episode 中,代理试图达到立方体。在每个 episode 中,如果代理做了我们希望它做的事情,我们希望通过奖励来强化这种行为,反之亦然。
一个 episode 运行直到代理失败任务——在本例中,可能是掉落到虚空中或者在预定时间内耗尽——或者成功完成任务,即到达目标。
在每个剧集的开始时,调用位于Agent上的 C#方法OnEpisodeBegin()来初始化新剧集的仿真环境。此方法设置剧集的环境。在大多数情况下,您将使用此方法随机化环境元素,以帮助代理在各种条件下学会成功完成任务。
对于我们的场景,在一个剧集开始时,需求是:
-
确保球代理位于地板上某处,而不是掉进深渊。
-
将目标移动到地板上的随机位置。
为了满足第一个要求,我们需要访问代理的 Rigidbody 组件。Rigidbody 组件是 Unity 模拟对象在物理系统中所必需的一部分。
提示
在Unity 手册中了解更多关于 Unity Rigidbody 组件的信息。
再次打开 BallAgent 脚本。由于我们需要重置代理的速度(如果它掉进深渊)并且最终移动它到地板上,我们需要访问它的Rigidbody:
-
我们这里需要的 Rigidbody 组件与此脚本将要附加到的相同对象上(不像刚才我们需要目标的变换),因此我们可以在脚本的
Start()方法内获取对它的引用。首先,当我们获取引用时,我们需要一个地方来存储它。在
Start()方法之前(在类内但在方法之上和之外),添加以下内容:Rigidbody rigidBody; -
接下来,在
Start()方法内部,添加以下代码来请求对附加到脚本的对象的 Rigidbody 的引用(并将其存储在我们之前创建的rigidBody变量中):rigidBody = GetComponent<Rigidbody>();为了满足第二个要求,我们需要确保代码可以访问目标的变换,以便我们可以将其移动到新的随机位置。
-
由于此脚本将附加到代理而不是目标,我们需要获取对目标变换的引用。
为此,在
BallAgent类的Start()方法之前,添加一个新的public类型为Transform的字段:public Transform Target;注意
Unity 组件中的
public字段将通过 Unity 编辑器显示在检查器中。这意味着您可以通过视觉或拖放选择要使用的对象。我们之前不需要对Rigidbody执行此操作,因为它不需要在 Unity 编辑器中公开。 -
保存脚本(并保持打开状态),然后切换回 Unity 编辑器。找到附加到代理的脚本组件,查找检查器中新创建的目标字段,并选择其旁边的小圆形按钮,如图 2-15 所示。
![psml 0215]()
图 2-15. 在脚本中更改目标
-
在打开的窗口中,双击目标对象,如图 2-16 所示。
![psml 0216]()
图 2-16. 选择目标
-
验证已附加到代理的脚本组件现在在目标字段中显示目标的变换,如图 2-17 图 所示。
提示
如果您愿意,您还可以从 Hierarchy 视图将目标对象拖动到检查器中的插槽中。
![psml 0217]()
图 2-17. 确认目标的变换在字段中显示
-
接下来,切换回脚本代码,并在类内部实现一个空的
OnEpisodeBegin()方法:public override void OnEpisodeBegin() { } -
在
OnEpisodeBegin()内部,添加以下代码来检查代理的Rigidbody的位置是否低于地板(这意味着代理正在下落),如果是,则重置其动量并将其移回地板上:if (this.transform.localPosition. y < 0) { this.rigidBody.angularVelocity = Vector3.zero; this.rigidBody.velocity = Vector3.zero; this.transform.localPosition = new Vector3(0, 0.5f, 0); } -
最后,在
if语句后添加一些代码,以将目标移动到新的随机位置:Target.localPosition = new Vector3(Random.value * 8 - 4, 0.5f, Random.value * 8 - 4);
不要忘记保存您的代码,以防万一。
让代理观察环境
接下来,我们需要设置代理以从模拟环境中收集观察。我们将假装我们的代理可以看到其目标,以便准确知道其位置(我们的目标不是弄清楚目标在哪里;而是到达目标):因此,它将有一个观察是目标位置的确切位置。这需要更多的编码。
观察是通过向我们的代理添加传感器来收集的。传感器可以通过代码添加,也可以通过将组件附加到 Unity 场景中的物体上添加。对于我们的第一个模拟,我们将完全通过代码完成。
在我们的代理 C# 代码中,我们需要执行以下操作:
-
创建一个空的
CollectObservations()方法:public override void CollectObservations(VectorSensor sensor) { } -
然后,在方法内部,为代理自身位置添加传感器观察:
sensor.AddObservation(this.transform.localPosition); -
我们还需要为代理自身的
x和z速度添加传感器观察(我们不关心y速度,因为代理不能上下移动):sensor.AddObservation(rigidBody.velocity.x); sensor.AddObservation(rigidBody.velocity.z); -
最后,我们需要为目标位置添加一个观察:
sensor.AddObservation(Target.localPosition);
观察到这些即可!
让代理在环境中采取行动
为了实现向目标移动的目标,代理需要能够移动。
-
首先,创建一个空的
OnActionReceived()方法:public override void OnActionReceived(ActionBuffers actions) { } -
然后,获取我们需要的两个连续动作的访问权限,一个用于
x,一个用于z,允许球在可以滚动的所有方向上进行控制:var actionX = actions.ContinuousActions[0]; var actionZ = actions.ContinuousActions[1]; -
创建一个零向量
Vector3作为控制信号:Vector3 controlSignal = Vector3.zero; -
然后,更改控制信号的
x和z分量,以从X和Z动作中获取它们的值:controlSignal.x = actionX; controlSignal.z = actionZ; -
最后,使用我们之前获取的
Rigidbody(附加到代理的组件),调用 UnityRigidbody上可用的AddForce()函数来应用相关力量:rigidBody.AddForce(controlSignal * 10);
暂时就这样!现在代理可以由机器学习系统控制。不要忘记保存您的代码。
注意
我们最初使用Vector3.zero创建controlSignal Vector3,因为我们希望y分量为0。我们可以通过创建一个完全空的Vector3,然后将0分配给controlSignal.y来实现相同的效果。
为其行为给予奖励
正如我们在第 1 章中提到的那样,奖励是强化学习的基本组成部分。强化学习需要奖励信号来引导代理执行最优策略,即我们希望它执行或尽可能接近的操作。强化学习的强化是通过使用奖励信号来引导代理朝着期望的行为(即最优策略)的方向发生的。
在您的OnActionReceived方法中,在您刚刚编写的现有代码之后,执行以下操作:
-
存储到目标的距离:
float distanceToTarget = Vector3.Distance (this.transform.localPosition, Target.localPosition); -
检查到目标的距离是否足够接近,以确认是否已到达目标位置,如果是,则分配奖励值为 1.0:
if (distanceToTarget < 1.42f) { SetReward(1.0f); } -
分配奖励后,在
if语句内,因为已达到目标,请调用EndEpisode()来完成当前的训练回合:EndEpisode(); -
现在检查代理是否已从平台上掉落,如果是,则同样结束此次训练回合(此情况下不适用奖励):
if (this.transform.localPosition.y < 0) { EndEpisode(); }
完成后,您将希望保存代码并返回 Unity 编辑器。
代理的最后一步
一个代理不仅需要扩展Agent脚本;还需要在 Unity 编辑器中进行一些支持脚本和设置。我们之前在 Unity 中安装的 ML-Agents 包已经带来了所需的脚本。
要将它们添加到您的代理中,在 Unity 编辑器中打开您的场景,执行以下操作:
-
在 Hierarchy 中选择代理,然后在其检视器底部点击“添加组件”按钮。
-
在图 2-18 中查找并添加决策请求组件。通过查看代理的检视器,验证组件是否正确添加,如图 2-19 所示。
![psml 0218]()
图 2-18. 添加决策请求器
![psml 0219]()
图 2-19. 决策请求器组件在代理的检视器中可见
-
使用滑块将决策周期更改为
10。 -
再次使用“添加组件”按钮,并向代理添加一个行为参数组件。
-
确认行为参数组件已成功添加,并将行为名称更新为“BallAgent”,向量观测空间大小设置为
8,连续动作设置为2,离散分支设置为0,如图 2-20 所示。
我们将向量观测空间大小设置为8,因为提供了八个值作为观测。它们是:
-
代表目标位置的向量的三个组成部分
-
代表代理位置的向量的三个组成部分
-
代表代理 X 速度的单个值
-
代表代理 Z 速度的单个值
回到“让代理观察环境”,当我们在代码中添加每个观察时,可以为八个值的复习提供帮助。
同样地,我们将 Continuous Actions 设置为2,因为有两个动作。它们是:
-
应用在 x 轴上的力
-
应用在 z 轴上的力
再次回到“让代理在环境中采取行动”,当我们为表示两个行动的代码添加了OnActionReceived方法时。

图 2-20. 设置行为参数
现在也是保存你的场景的好时机。
为代理提供手动控制系统
在使用游戏引擎构建机器学习仿真环境的乐趣之一是,你可以控制仿真中的代理并测试代理在环境中存在的能力,甚至测试目标是否可达。
为此,我们扩展Heuristic()方法。再次打开你的代理的 C#代码,并按照以下步骤操作:
-
实现一个空的
Heuristic()方法:public override void Heuristic(in ActionBuffers actionsOut) { }警告
在 C#中,
in关键字是一个参数修饰符。这意味着它使参数通过引用传递。在这种情况下,这有效地意味着你直接使用代理的动作,而不是传递到其他地方的副本。 -
在内部,添加以下代码,将
0索引动作映射到 Unity 输入系统的水平输入,并将1索引动作映射到垂直输入(与我们在“让代理在环境中采取行动”中早些时候映射x和z动作的方式匹配):var continuousActionsOut = actionsOut.ContinuousActions; continuousActionsOut[0] = Input.GetAxis("Horizontal"); continuousActionsOut[1] = Input.GetAxis("Vertical");就是这样。你会希望保存你的代码并返回 Unity 编辑器。你的手动控制系统已连接好。
如何使用这个控制系统?我们很高兴你问了。要使用手动控制系统,你需要按以下步骤操作:
-
在 Hierarchy 中选择代理,使用 Inspector 将行为类型设置为 Heuristic Only。
-
在 Unity 中按下播放按钮。现在,你可以使用键盘上的箭头键来控制代理。如果代理掉入虚空,代理应如预期地重置。
你可以通过 Unity 更改连接到动作箭头的Horizontal和Vertical轴的键:
-
打开 Edit 菜单 → Project Settings…并在 Project Settings 视图的侧边栏中选择 Input Manager。
-
找到 Horizontal 和 Vertical 轴以及相关的 Positive 和 Negative 按钮,并根据需要更改映射,如图 2-21 所示。
![psml 0221]()
图 2-21. 在 Unity 中更改映射到输入轴的键
现在应保存你的场景。
使用仿真进行训练
训练模拟是一个多步骤的过程,涉及使用一些提供的 Unity ML-Agents 脚本创建配置文件,并根据您的计算机性能需要一定的时间。为了训练,我们将使用 Python 和终端,但首先我们需要进行一些设置。
具体来说,我们需要创建一个 YAML 文件作为训练的超参数。然后,我们将使用mlagents Python 包中的mlagent-learn命令来运行训练。
提示
YAML 是一种有用的存储配置格式,旨在尽可能使人类可读。您可以从Wikipedia了解更多关于 YAML 的信息。
因此,要训练您的球体代理,请按照以下步骤操作:
-
创建一个名为BallAgent.yaml的新文件,并包含以下超参数和值:
behaviors: BallAgent: trainer_type: ppo hyperparameters: batch_size: 10 buffer_size: 100 learning_rate: 3.0e-4 beta: 5.0e-4 epsilon: 0.2 lambd: 0.99 num_epoch: 3 learning_rate_schedule: linear network_settings: normalize: false hidden_units: 128 num_layers: 2 reward_signals: extrinsic: gamma: 0.99 strength: 1.0 max_steps: 500000 time_horizon: 64 summary_freq: 10000 -
将新的 YAML 文件保存在一个明智的位置。我们喜欢把它放在靠近 Unity 项目的config/目录中,但不必把它放在 Unity 项目内(尽管如果您愿意的话也可以)。
提示
我们只使用非常小的批处理和缓冲区大小,因为这是一个非常简单的训练模拟:输入和输出不多,所以将批处理和缓冲区大小设得很小可以加快训练速度。更复杂的模拟环境、奖励系统和观测集合将需要不同的超参数值。稍后在第十二章中我们会更详细地讨论潜在的超参数。
-
紧接着,在您的终端内,进入我们在“设置”中创建的
venv,通过运行以下命令启动训练过程:mlagents-learn config/BallAgent.yaml --run-id=BallAgent注意
用config/BallAgent.yaml替换刚创建的配置文件的路径。
-
一旦命令开始运行,您应该会看到类似于图 2-22 的东西。此时,您可以在 Unity 中点击播放按钮。
![psml 0222]()
图 2-22. ML-Agents 开始训练过程
-
当您看到类似于图 2-23 的输出时,您将知道训练过程正在工作中。
提示
如果您正在运行一台装有高级 Apple Silicon 芯片的 macOS 机器,您可能希望在 Rosetta 2 下运行所有这些操作。
![psml 0223]()
图 2-23. 训练过程中的 ML-Agents 过程
使用 TensorBoard 监控训练
尽管可能看起来不像,mlagents-learn命令在幕后使用的是 PyTorch。PyTorch 的使用意味着您可以使用出色的一套 Python 机器学习工具:对于如此简单的模拟来说并没有太大意义,但至少我们可以讨论如何通过 TensorBoard 查看训练过程中的内部情况。
注意
尽管 TensorBoard 最初作为 TensorFlow 项目的一部分起源,这是一个与 PyTorch 不同的框架,并且最初由完全不同的团队开发,但 TensorBoard 已经成为一个更通用的机器学习框架支持工具,并与许多其他工具一起工作,包括 PyTorch。
按照以下步骤通过 TensorBoard 监控训练过程:
-
打开额外的终端,并切换到安装 Unity ML-Agents Toolkit 的位置。
-
激活你的
venv,并执行以下命令:tensorboard --logdir=results --port=6006提示
如果启动 TensorBoard 遇到问题,请尝试使用命令
pip3 install tensorboard安装最新版本(别忘了你需要在你的venv中执行!)。 -
一旦 TensorBoard 运行起来,你可以打开你的网页浏览器,并访问以下网址:http://localhost:6006。
-
从这个 TensorBoard 的浏览器实例中,你可以监控训练过程,如 图 2-24 所示。在你的模拟旅程中,特别相关的是
cumulative_reward和value_estimate统计信息,因为它们显示了代理执行任务的表现(基于其奖励)。如果cumulative_reward和value_estimate接近1.0,那么代理很可能已经解决了达到目标的问题(因为代理可以获得的最大奖励是1.0)。![psml 0224]()
图 2-24. TensorBoard 监控训练
训练完成时
最终,训练过程将完成,并在保存模型文件时显示 “Saved Model” 消息。
一旦模型文件保存完毕,请按照以下步骤将其导入 Unity 并使用模型运行模拟(而不是使用模拟进行训练),观察你的代理如何运作:
-
定位模型文件(它的名称可能是 BallAgent.onnx 或 BallAgent.nn 等),如 图 2-25 所示。
![psml 0225]()
图 2-25. 已保存的模型文件
-
将模型文件移动到 Unity 项目中,可以使用文件管理器或将其拖放到 Unity 项目视图中。你应该在 Unity 中看到 .onnx 文件,如 图 2-26 所示。
![psml 0226]()
图 2-26. Unity 中的模型
注意
在这里生成的 .onnx 文件(以及本书中许多其他章节中生成的文件)是一种开放的神经网络交换(ONNX)格式文件。ONNX 是一种用于存储机器学习模型的开放格式,并提供了一组通用的运算符和共同的文件格式,以使 AI 开发人员可以在各种框架、工具、运行时和编译器中使用模型。这是一个令人兴奋的开放机器学习标准,你在这里使用它!
-
在检查器中选择代理,并将项目视图中的.onnx或.nn文件从项目视图拖动到检查器中的模型组件。您的代理检查器应该看起来像图 2-27。
![psml 0227]()
图 2-27. 将训练好的模型附加到 Unity 中的代理
提示
您还可以在检查器中的模型字段旁边使用带有圆形按钮,从项目中可用的.onnx或.nn文件中进行选择(尽管现在只有一个文件可供选择!)。
-
通过在 Unity 中按下播放按钮来运行模拟。
您应该看到您的代理反复实现其目标!非常成功。
这一切意味着什么?
在本章中,您构建了一个非常简单的模拟环境,在这个环境中,单个代理可以学会如何将自己滚向目标(理想情况下是不要把自己滚到世界的边缘)。
这个代理能够观察其环境,因为BallAgent提供了关于以下内容的观察:
-
它自己的位置
-
它自己的
x速度 -
它自己的
z速度 -
目标的位置
值得记住的是,所提供的观察是代理对其环境以及其中状态变化的全部了解。
提示
我们通过向 Unity 的一个简单球体对象附加一个扩展Agent类的脚本组件,将其转换为一个代理。如果我们没有这样做,这个球就不会成为一个代理。
就像人类通过与世界的互动所获取的感官信息(视觉、嗅觉、触觉、听觉和味觉)以及自身身体状态(本体感知、内感知和前庭感觉)一样,代理需具备相同的能力,以确定它们在不同时间点的状态,并观察自身状态的变化如何影响与其目标相关的环境部分的状态。
对于一个需要寻找一个立方体的球来说,知道它的位置、速度和目标位置就足够了。例如,它不需要被告知地板在某一点结束。这是因为:
-
从地板上的任意其他点到目标的直接路径永远不会脱离边缘。
-
代理将通过经验学习,超出每个方向的一定距离将导致剧集结束而没有奖励。
其他类型的代理可能需要更多或更复杂的观察。正如稍后将看到的那样,ML-Agents 针对将代理与摄像机或类似激光雷达的深度传感器连接的一些内置实用工具,但您可以向代理传递任何您想要的信息。观察可以是您可以将其归结为数字或数字数组并传递到AddObservation方法中的任何东西。
警告
重要的是,代理只能获得足够的观察来理解它应该做什么。额外或冗余的信息可能看起来会帮助代理更快地解决其任务,但实际上它会让代理更加辛苦地找出哪些观察是重要的并与成功相关。
一个智能体最令人印象深刻的地方可能在于,它甚至不知道自己的观察结果是什么;它只是接收到一些没有字段名或上下文的数字。它通过观察这些数字在各种动作下如何变化来学习所需的内容。
动作是智能体可以执行以改变环境的指令。BallAgent能够添加或移除力量,从而在两个方向上进行滚动或制动。这使得在二维平面上完成完整的移动成为可能,如图 2-28 所示。

图 2-28. 在两个潜力力方向上的运动范围
但是动作可以是任何导致环境变化的事物,从智能体的物理移动(例如,滚球、移动关节、扑打翅膀、转动轮子)到高级动作(例如,向前走一米、站起来、转身 180 度),再到环境的变化(例如,拾起智能体所站的物体、打开智能体面前的门)。智能体应该获得的动作的粒度和范围将取决于您希望它学习的具体任务。
例如,如果您希望一个智能体学会走路,您可能不希望它的动作是“向前走”。因为那样您将不得不编写代码让它走路,它就不需要学习了。但是,如果您希望您的智能体学会如何完成迷宫,您可以提供移动的编码指令,这样它就不必学会如何走路,然后再学习迷宫的工作。
提示
回想一下我们如何在BallAgent上实现OnActionReceived方法,通过明确在所需方向上添加力量。它不需要学习如何向自己的RigidBody添加力量;相反,它学习何时适合决定沿着哪个轴移动。因此,您希望给智能体的任何动作将需要由您实现;智能体学习的是何时以及以何种顺序触发您给它选择的动作。
智能体的任务是利用其观察结果来评估环境的状态,以决定最佳响应,并执行必要的一系列动作。我们引导智能体选择对各种环境状态的所需响应,从而朝向我们对智能体的预期目标前进,使用奖励。在BallAgent的案例中,只有一个奖励:当智能体达到目标时为+1.0。
奖励只是在满足某些条件时授予或收回智能体的分数。有些奖励是内在的,例如在模仿学习中,奖励是自动给予的,用于与示例行为的相似性。在强化学习中,必须定义显式奖励。
这些奖励是代理程序将获得的唯一关于其行为的反馈,因此必须谨慎决定奖励和触发条件。随着时间的推移,代理程序通常会优化历史上给它带来最多分数的行为,这意味着设计不当的奖励方案可能导致意外的行为。
例如,假设我们决定通过奖励BallAgent 接近 目标来帮助它。这可能会加快训练,因为代理程序收到第一个奖励并得知应该做什么的时间会比仅奖励它达到目标要早得多。
因此,您决定对于这个起始距离目标一定距离的代理(比如说,10 个单位),您将根据它距离目标的接近程度来奖励它每一步。当它达到目标时,奖励为+10.0;当它距离目标一单位时,它将获得+9.0,依此类推。在第一步时,它还未移动,将收到+0.0 的奖励。
经过一些训练后,代理程序每个 episode 平均接收的奖励很高,因此您结束了训练。您将代理程序启动到推断模式,并发现在每个 episode 期间,代理程序会接近对象,然后围绕它旋转而不触及它,直到 episode 达到其长度限制并结束。出了什么问题?
您经过思考意识到,在奖励设计中掉入了一个简单的陷阱:您让代理程序优化了错误的事物。三个因素共同导致了这一混乱:
-
虽然代理程序确实考虑了每一步潜在行动的潜在收益,但它倾向于优化整个 episode 中可以获得的最高分数。
-
在理想的 episode 中,
BallAgent将直接向目标前进并达到它。假设它每步移动约一单位,那么代理将稳步增加奖励:第一步为+0.0,第二步为+1.0,直到代理到达目标时为+10.0,并且episode 在 10 步后以 55.0 的奖励结束。 -
如果
BallAgent代替接近目标一单位,它将在仿真的前九步中获得相同的奖励(总计 45.0)。然后,在此距离围绕目标旋转,它将每一步仿真允许继续获得+9.0。在我们的超参数文件中,max_steps=500000,此行为将在剩余的 499,991 步中继续,episode 将在达到半百万步后强制结束,并获得 4,499,964.0 的奖励。
您可以看到,只给予奖励点数的代理程序可能认为绕圈是优选的行为,而不是触摸目标。
因此,模拟是建立在三个主要概念上的:观察、行动和奖励。有了这三样东西,模拟代理可以学会各种智能行为。代理利用它收集到的观察经验、采取的行动以及获得的奖励来探索所处的环境。起初,行动会是随机的,随后随着获得奖励和学到的教训,行动将变得更加有针对性,适应当前场景中的理想行为。在训练过程中,代理的这种行为模型是不断变化的,随着奖励反馈的接收而更新,但一旦训练结束(当代理以推理模式运行时),它就被锁定了,那将是代理永远展示的行为。
这个模型,它将观察映射到在那一刻产生最高奖励的行动上,被称为策略。这是强化学习中行为模型的传统术语,但也是 ML-Agents 中的一个类名。Policy类以一种方式抽象出决策过程,使你可以切换决策方式。这就是允许在启发式控制之间切换的东西——之前允许你使用 Unity 的输入系统控制代理——以及通过神经网络控制,如在“训练完成时”中所见。
即将来临
本书的第二部分进一步探讨了基于模拟的机器学习与 ML-Agents,在第四章中开始我们下一个模拟活动。在这里,我们将建立在你所学的基础上,创建一个更复杂的模拟环境,其中的代理比这个例子中的滚球更了解它的情况。
你将学习如何将多种观察输入到你的代理中,并利用游戏引擎将视觉输入发送给你的代理,而不是原始数字。
我们还将探讨在更复杂的情况下如何加速训练过程,通过复制模拟环境并在许多类似代理之间并行训练,以便神经网络模型可以获得更多经验。令人兴奋,对吧?
第三章:创建您的第一个合成数据
本章介绍了合成,本书的第二支柱,正如第一章中所讨论的。这里重点介绍了用于机器学习数据合成的工具和流程,以及它与您到目前为止为模拟所做的工作如何关联,以及它与模拟工作的不同之处。
到本章结束时,您将生成世界上最令人失望的合成数据!但是您将准备好在未来的章节中生成更有趣的数据。我们承诺。请继续关注我们。
正如我们在“Unity”中提到的,我们在合成的初探中将主要使用一个名为感知的 Unity 包作为工具。
注意
在本书中,我们在合成方面的工作量不会像模拟那样多。这仅仅是因为需要学习的内容不多:模拟是一个非常广泛的领域,有许多不同的方法可以采用,而在 Unity 中进行合成主要是要根据需要执行不同类型的随机化,以生成所需的数据。我们会教你一切必要的知识,但活动会比较少。
Unity 感知
Unity 的感知包将 Unity 游戏引擎转变为一个工具,用于生成用于机器学习工作流程(主要不在 Unity 内部的工作流程)的合成数据集,主要是图像。
感知框架提供了一系列有用的工具,从数据集捕获、对象标记、图像捕获等等。您可以创建直观的对象-标签关联,并直接将它们馈送到您需要的机器学习工具链的任何部分。感知甚至可以帮助您生成边界框和语义分割蒙版,以及场景生成等等。它真的非常强大。
感知框架是一个开源项目,可以通过其 GitHub 项目免费获取。其功能示例显示在图 3-1 中。

图 3-1. Unity 的感知框架
我们将使用 Unity 感知包,它与 Unity 编辑器(就像 Unity ML-Agents 一样)配合使用,用于本章的所有内容,以及稍后在第十三章中使用。
过程
我们在本书中通过示例生成的所有合成数据的整体工作流程如下:
-
我们将确定一个场景,需要大量数据,通常用于训练。
-
我们将在 Unity 中创建一个场景,或者多个场景,布置我们希望参与我们模拟数据的对象。
-
我们将使用随机化程序来改变场景的参数,以根据需要变化数据。
-
最后,我们将为我们的数据指定地面真实性和标签,然后生成数据。
与仿真不同,Unity 并非你在合成工作中的始终。在仿真中,使用 ML-Agents 你会在 Unity 中建立一个场景作为你的仿真,与你的仿真相关的代理会存在并在那个场景中行动。而且,你训练后的代理的版本(希望完善你给予它们的任务)也会在那个场景中使用(当然,你可以将你和它们学到的东西放在其他地方的“大脑”中,但这超出了本书的范围)。
对于合成,我们只是使用 Unity 和 Perception 包作为生成大量数据的工具。实际上,由于 Unity 是一个视觉开发环境,最适合这种操作的数据类型是视觉数据(图像)。就像在仿真中一样,你将使用 Unity 构建某种环境或世界,然后使用 Unity 摄像头拍摄数以千计的该世界的图像并将它们导出到你的文件系统。一旦你有了这些图片,你将在其他地方进行实际的机器学习,使用 PyTorch、TensorFlow、Create ML 或你喜欢的任何训练系统。在本章中,我们将完成生成数据的设置以及前述工作流程的前两个步骤。
小贴士
ML-Agents 工具包的流程包括训练,感知流程负责训练。明白了吗?
使用 Unity Perception
为了探索 Unity 的 Perception 包,我们将完成一个突出显示工作流的简单活动。我们将生成的图像示例显示在 图 3-2 中。

图 3-2. 我们将生成的骰子图像的示例
最终,我们将生成骰子的图像,摄像头从不同角度拍摄,背景色彩不同,骰子的颜色组合也不同(我们在 “过程” 中提到的随机器)。在本章中,我们将在添加随机器之前完成所有操作。
然而,现在我们将设置一个场景并准备添加随机化。尽管还不会添加实际的随机器,那将在 第十三章 中进行。
创建 Unity 项目
与我们许多实际场景一样,使用 Unity 创建合成数据的第一步是创建一个全新的 Unity 项目:
-
打开 Unity Hub 并创建一个新的 3D “URP” 项目。如 图 3-3 所示,我们将其命名为 “SimpleDice”,但名称对功能并不重要。项目模板选择 (“Universal Render Pipeline” 或 “URP”) 才是重要的。
![psml 0303]()
图 3-3. 在 Unity Hub 中创建一个 URP 项目
警告
我们不会像前一章那样创建“3D 项目”,因为我们需要使用 Unity 的通用渲染管线(URP)。通用渲染管线(URP)是一个可编程的图形管线,为游戏开发人员提供了不同的工作流程。
因为生成合成数据的核心工作之一是输出图像,我们将使用 URP。Perception 使用 URP 生成帧完成渲染时的事件,以项目为单位,我们将使用该事件来输出图像。
不想考虑?别担心!我们只需要从不同的渲染管线中获取某些功能,以便使用 Unity Perception。对于 ML-Agents,我们不需要这些功能。
请记住,当您围绕 Unity 的 Perception 功能构建项目时,您可能希望使用 URP,而最简单的方法是从 Unity Hub 提供的 URP 模板开始。
如果您想了解 Unity 的不同渲染管线,请访问Unity 文档。
-
项目加载完成后,您需要删除由 URP 模板添加的示例资产,如图 3-4 所示。仅删除名为“示例资产”的父对象及其下的子对象。保留相机、灯光和“后处理体积”。
注意
由于某种原因,创建新的 URP 项目最简单的方法是使用带有示例环境的项目。我们也不确定为什么会这样。
![psml 0304]()
图 3-4. 删除示例资产
-
接下来,我们需要安装 Perception 包。选择窗口菜单 → 包管理器,使用 Unity 包管理器通过选择“+”菜单 → “从 git URL 添加包”,并输入
com.unity.perception进行安装,如图 3-5 和 3-6 所示。![psml 0305]()
图 3-5. 从 Git 添加包
![psml 0306]()
图 3-6. Unity Perception 包的包名称
包的下载和安装可能需要一些时间,请耐心等待。下载完成后,Unity 将导入该包,如图 3-7 所示。然后可以关闭包管理器窗口。
![psml 0307]()
图 3-7. Unity Perception 包由 Unity 加载
-
接下来,在项目窗格中选择 ForwardRenderer 资源,如图 3-8 所示(您会在设置文件夹中找到它)。
![psml 0308]()
图 3-8. ForwardRenderer 资源
-
在其检查器中,单击“添加渲染器功能”,然后单击“真实数据渲染器功能”,如图 3-9 所示。
![psml 0309]()
图 3-9. 添加真实数据渲染器
在这一点上,你的项目基本准备就绪了。这是使用 Unity 感知框架进行所有工作的一个良好而干净的起点,因此我们建议将其推送到某种源代码控制中,或者复制一份,以便每次都有一个新的起点。
创建一个场景
有时候创建一个场景是件好事!这就是其中一个时机。我们将要构建的场景非常简单:就是一些骰子!这些骰子将放置在一个平面上,并且我们将在引擎中拍摄骰子的图像来生成我们的合成数据(也就是骰子的合成图像)。
让我们开始吧!
获取骰子模型
首先,我们需要用到的骰子模型。你可以自己制作,但是在书籍资源中,你可以下载一个包含我们为你制作的骰子模型的 Unity 资源包:
-
通过双击Dice.unitypackage 文件并在 Unity 中点击“全部导入”来下载并导入它。
-
模型导入后,验证它们在 Unity 编辑器的项目面板中是否可见,如图 3-10 所示。
![psml 0310]()
图 3-10. Unity 编辑器中的骰子资源
就是这样!你已经准备好创建一个场景了。
一个非常简单的场景
打开场景后,首先我们需要添加一个地板和一些骰子:
-
通过在层次视图中添加一个平面并将其重命名为“Floor”,如图 3-11 所示,创建一个地板。
![psml 0311]()
图 3-11. 初始场景,带有地板
-
将一些骰子从项目面板中的“Dice”文件夹(在名为“Prefabs”的子文件夹中)拖放到场景或层次视图中,并将它们放置在地板上。现在绝对具体的位置并不是特别重要,但是如果你想要复制我们的场景,你可以在图 3-12 中看到它。
![psml 0312]()
图 3-12. 场景中的骰子
-
将相机位置调整为从稍微高角度显示骰子。你可以在重新定位相机时查看游戏视图来验证这一点。我们的示例在图 3-13 中展示。
![psml 0313]()
图 3-13. 骰子的良好视角
-
使用游戏视图顶部的下拉菜单,如图 3-14 所示,添加一个命名分辨率(我们称之为“Perception”)并将相机的分辨率设置为
480x480。因为我们将使用主摄像机(也是唯一的摄像机)来渲染图像,这里的分辨率控制着我们将渲染并保存到磁盘的图像的大小。提示
如果找不到下拉菜单,请确保你正在游戏视图上查看。场景视图没有你需要的菜单。
![psml 0314]()
图 3-14. 设置分辨率
在继续之前保存你的场景。
接下来,我们需要创建一种方法来控制我们的合成场景。我们将通过在我们的场景中创建一个“空的”游戏对象,并附加 Unity 感知框架提供的一些特殊组件来实现这一点。以下是执行此操作的步骤:
-
在层次视图中创建一个新的空游戏对象,并将其命名为“场景”或类似的名称。
提示
我们的“场景游戏对象(Scenario GameObject)”在某种意义上是“空的”,意味着它不映射到你可以在场景中看到的视觉组件。它存在于场景中,但在场景中不可见。如果我们向其添加一个视觉组件(例如像立方体这样的网格),它将变得可见。但是要控制一个场景,你并不需要任何可见的组件。
-
在层次视图中选择新的场景对象,然后在其检查器中使用“添加组件”按钮,如图 3-15 所示,向此新游戏对象添加固定长度场景组件,如图 3-16 所示。
![psml 0315]()
图 3-15. 添加组件按钮
![psml 0316]()
图 3-16. 添加固定长度场景组件
-
现在暂时不要更改固定长度场景的参数和设置。总迭代参数实际上是我们场景中将保存到磁盘的图像数量。
注意
如果您没有可用的固定长度场景组件,请查看前面导入感知包的步骤。此资产来自感知包。
固定长度场景组件用于通过协调所有必要的随机元素来控制场景的执行流程。
现在我们需要修改主摄像机,以使其用于感知:
-
选择主摄像机,并使用其检查器添加感知摄像机组件。
-
将感知摄像机的参数保持默认设置,如图 3-17 所示。稍后我们将详细介绍这些内容,但如果您愿意,您可以修改输出基础文件夹,以指定您希望保存渲染图像的位置。
![psml 0317]()
图 3-17. 新的感知摄像机组件
警告
如果在添加感知摄像机组件时,在 Unity 编辑器的控制台窗格中看到与异步着色器编译相关的错误或警告,请不要过于担心!如果发生这种情况,请选择编辑菜单 → 项目设置… → 编辑器,在着色器编译设置中找到并禁用异步着色器编译。
感知摄像机组件允许我们修改和控制从摄像机捕获的合成帧的参数,以及它们如何被注释,以及我们最终提供的标签如何与真实数据相关联。
别忘了再次保存你的场景。
准备合成
当您生成一个合成图像时,您也可以生成不同类型的真实数据。
感知提供一系列不同的标签器,它们控制你可以在每张捕获图像旁生成的地面真相的类型:
-
3D 边界框
-
2D 边界框
-
物体计数
-
物体元数据/信息
-
语义分割地图
提示
地面真相指我们知道是真实的信息。例如,因为我们正在制作骰子的图像,我们知道它们肯定是骰子。它们是骰子就是地面真相的一部分。
因为我们将生成不同面数的骰子图像,我们对物体元数据/信息标签器感兴趣。在 Unity 编辑器中,Unity 将其称为渲染对象信息标签器。
要为此项目添加一个标签器,请在场景中执行以下操作:
-
在层次结构面板中选择主摄像机,找到附加到其上的感知摄像机组件。
-
点击感知摄像机的相机标签器部分中的“+”按钮,如图 3-18(#fig:plusbutton)所示。
![psml 0318]()
图 3-18。相机标签器部分中的“+”按钮
-
从出现的列表中选择渲染对象信息标签器,如图 3-19(#fig:renderedobjectinf)所示。
![psml 0319]()
图 3-19。添加一个渲染对象信息标签器
-
确认已添加标签器,如图 3-20(#fig:addedroil)所示。
![psml 0320]()
图 3-20。我们主摄像机上的渲染对象信息标签器
要使用标签器,我们需要创建一些标签:
-
在项目面板中,右键点击并选择创建 → 感知 → ID 标签配置,如图 3-21(#fig:addingasset)所示。
![psml 0321]()
图 3-21。创建一个新的 ID 标签配置
-
找到新创建的资产(通常会被命名为
IdLabelConfig或类似的名称),然后将其重命名为DiceLabels或类似的明显名称。 -
在项目面板中选择这个资产,然后在其检视器面板中使用“添加新标签”按钮来创建六个标签。完成后,你的标签列表应该类似于我们的示例,如图 3-22 所示。
![psml 0322]()
图 3-22。已创建六个标签
-
再次在层次结构中选择主摄像机,找到附加到其上的感知摄像机组件中的 Id 标签配置字段,然后从项目面板将刚刚创建的
DiceLabels资产拖动到字段中,或者点击字段并选择该资产。一旦设置好,它应该看起来像图 3-23(#fig:finpercam)。![psml 0323]()
图 3-23。感知摄像机已设置
再次,在继续之前,你需要保存场景。
测试场景
现在是测试场景的好时机,不涉及任何随机元素。按照以下步骤测试场景,并检查我们迄今为止所做的一切是否正确:
-
使用 Unity 的播放按钮运行场景。这可能需要一段时间。
-
场景应该生成与我们添加到场景 GameObject 中的 Fixed Length Scenario 组件的 Total Iterations 参数指定的图片数量相同,然后会自动退出播放模式。再次强调,可能需要一段时间,并且 Unity 编辑器可能会显得卡住。
-
要验证一切是否正常工作,在 Unity 再次响应并且播放模式已结束后,选择层次结构中的主摄像机,找到感知摄像机组件。它将具有一个新的“显示文件夹”按钮,如图 3-24 所示。
-
点击“显示文件夹”按钮。这将打开存储在您本地计算机上的图像位置。
此时,您应该找到一个包含从场景摄像机生成的 100 张图片的文件夹。它们都是相同的,如图 3-25 所示。如果您已经走到这一步并且一切都正常工作,您可以继续了!

图 3-24. 成功运行后的“显示文件夹”按钮

图 3-25. 骰子图片
很好!您实际上已经合成了一些数据——每次保存图像时,所有合成的图像都是相同的,我们的标签实际上并没有被使用。
设置我们的标签
我们创建的标签表示骰子的哪一面朝上。为了使这些信息以与我们输出的图像匹配的格式可用,我们需要将其附加到预制体上:
-
通过在项目面板中双击 Dice-Black-Side1(黑色骰子,显示一个点朝上的面)来打开 Dice-Black-Side1 的预制体。
-
当预制体打开时,如图 3-26 所示,选择其层次结构中的根对象(在本例中为“Dice-Black-Side1”),并使用其检视面板中的“添加组件”按钮添加一个标签组件,如图 3-27 所示。
![psml 0326]()
图 3-26. 打开一个预制体
![psml 0327]()
图 3-27. 添加一个标签组件
-
展开检视面板中新组件的 DiceLabels 部分,并在代表朝上面的标签旁边点击“添加到标签”按钮(这个应该是“one”),如图 3-28 所示。
-
退出骰子预制体,并为所有骰子预制体重复此过程(总共应该有 30 个,由 5 种不同颜色组成,每种颜色都有 6 个点朝上)。您应该为每个骰子应用对应面朝上的标签号码。
![psml 0328]()
图 3-28. 添加特定标签
检查标签
后续我们将更多地使用标签,但您可以通过以下步骤快速检查它们是否正常工作:
-
选择层次结构中的主摄像机,在其中添加一个新的 BoundingBox2D 标签,并将其连接到我们之前创建的标签集,如图 3-29 所示。
![psml 0329]()
图 3-29. 向相机添加新的标签器
-
运行项目并查看游戏视图。除了将图像文件保存为正常格式外,您还将看到它在每个标记对象周围绘制边界框,如图 3-30 所示。
![psml 0330]()
图 3-30. 绘制边界框
在后续章节中,当我们开始使用随机化器时,我们将更多地利用这些标签。
接下来做什么?
到目前为止,我们已经建立了一个场景,并连接了所有必要的管道以使 Unity Perception 工作:
-
我们使用 URP 管线创建了一个项目,这是使用 Unity Perception 创建模拟图像数据所必需的。
-
我们向场景中的相机添加了一个感知相机组件,我们希望使用它来生成图像。
-
我们在场景中的一个空对象上添加了一个固定长度场景组件,允许我们管理图像的整体生成。
-
我们已经确定了当运行图像合成过程时,图像保存在哪里。
-
我们已经为我们的骰子合成创建了一些标签,并将它们应用到我们使用的相关骰子预设上。
稍后,在第十三章中,我们将通过向场景添加随机化器进一步进行探索,这将改变骰子的位置、大小和其他元素,以及场景本身的元素,从而使我们生成的每幅图像都是独特的。再稍后,在第十四章中,我们将探讨探索已生成的合成数据以及如何利用它。
第二部分:为了娱乐和利润而模拟世界
第四章:创建更高级的模拟
到目前为止,您已经了解了模拟的基础知识和综合的基础知识。现在是时候深入一些,进行更多的模拟了。回到第二章,我们建立了一个简单的模拟环境,向您展示了在 Unity 中组装场景并用它来训练代理是多么容易。
在本章中,我们将基于您已经学到的东西,并使用相同的基本原理创建一个稍微更高级的模拟。我们将要构建的模拟环境如图 4-1 所示。

图 4-1. 我们将构建的模拟
这个模拟将由一个立方体组成,它将再次充当我们的代理。代理的目标是尽快将一个块推入一个目标区域。
到本章结束时,您将继续巩固 Unity 技能,用于组装模拟环境,并更好地掌握 ML-Agents Toolkit 的组件和特性。
设置推块代理
要获取关于模拟和机器学习工具的全面介绍和讨论,请参考第一章。本节将为您提供完成这个特定活动所需的各种工具的快速总结。
具体来说,这里我们将执行以下操作:
-
创建一个新的 Unity 项目并为其配置 ML-Agents 的使用。
-
在 Unity 项目的场景中创建我们的推块环境。
-
实现必要的代码,使我们的推块代理在环境中运行并可以使用强化学习进行训练。
-
最后,在环境中训练我们的代理并查看其运行情况。
创建 Unity 项目
再次,我们将为此模拟创建一个全新的 Unity 项目:
-
打开 Unity Hub 并创建一个新的 3D 项目。我们将其命名为“BlockPusher”。
-
安装 ML-Agents Toolkit 包。请参考第二章获取说明。
就这些!您已经准备好为推块代理创建生存环境。
环境
有了我们准备好的空白 Unity 项目和 ML-Agents,下一步是创建模拟环境。除了代理本身之外,本章的模拟环境还具备以下要求:
-
一个地板,供代理移动
-
一个块,供代理推动
-
一组外部墙壁,防止代理掉入虚空
-
一个目标区域,供代理将块推入其中
在接下来的几个部分中,我们将在 Unity 编辑器中创建每个部分。
地板
地板是我们的代理和它推动的方块的生活场所。地板与第二章中创建的相似,但这里我们还将围绕它建立墙壁。在编辑器中打开新的 Unity 项目后,我们将创建一个新场景,并为我们的代理(以及它推动的方块)创建地板:
-
打开 GameObject 菜单 → 3D Object → 立方体。单击在层次视图中创建的立方体,并像之前一样将其名称设置为“地板”或类似的名称。
-
选择新创建的地板后,将其位置设置为合适的值,比如
(20, 0.35, 20)或类似的值,这样它就是一个平坦而略带厚度的大地板,如 图 4-2 所示。![psml 0402]()
图 4-2. 我们模拟用的地板
提示
地板是这个世界存在的中心。通过将世界的中心定位在地板上,地板的位置实际上并不重要。
这次我们希望地板有更多特色,所以我们打算给它上色:
-
打开 Assets 菜单 → 创建 → 材质以在项目中创建一个新的材质资源(可以在项目视图中看到)。通过右键单击并选择重命名(或选中材质后按返回键),将材质重命名为“Material_Floor”或类似的名称。
-
确保在项目视图中选择了新的材料,并使用检查器将反射颜色设置为一些花哨的颜色。我们推荐一个漂亮的橙色,但任何颜色都可以。你的检查器应该看起来像 图 4-3 一样。
![psml 0403]()
图 4-3. 地板材质
-
在层次视图中选择地板,并从项目视图直接将新材料拖放到地板在项目视图中的条目或地板检查器底部的空白处。场景视图中地板的颜色应该会改变,并且地板的检查器应该有一个新的组件,如 图 4-4 所示。
![psml 0404]()
图 4-4. 地板的检查器,显示新的材料组件
地板就这样了!确保在继续之前保存场景。
墙壁
接下来,我们需要在地板周围创建一些墙壁。与第二章不同,我们不希望代理有可能从地板上掉下来。
要创建墙壁,我们将再次使用我们旧朋友——立方体。回到刚才创建地板的 Unity 场景中,进行以下操作:
-
在场景中创建一个新的立方体。使其在 x 轴上与地板相同的比例(大约是
20),在 y 轴上高1单位,在 z 轴上约0.25单位。它应该看起来像 图 4-5 一样。![psml 0405]()
图 4-5. 第一面墙
-
为墙壁创建新材质,给它一个漂亮的颜色,并将其应用到您创建的墙壁上。我们的示例显示在图 4-6 中。
![psml 0406]()
图 4-6. 新的墙壁材质
-
将立方体重命名为“墙壁”或类似的名称,并再次复制它。这些将是我们在一个轴上的墙壁。目前不要担心将它们放置在正确的位置。
-
再次复制其中一堵墙,并使用检查器在 y 轴上旋转
90度。旋转完成后,再次复制它。提示
您可以按键盘上的 W 键切换到移动工具。
-
使用移动工具调整墙壁的位置,选择每个墙壁时(无论是在场景视图还是层次视图中),按住键盘上的 V 键进入顶点捕捉模式。在按住 V 键的同时,将鼠标悬停在墙壁网格的不同顶点上。将鼠标悬停在一个墙壁外底角的顶点上,然后单击并拖动移动手柄,以将其捕捉到地板上适当的上角顶点。该过程显示在图 4-7 中。
![psml 0407]()
图 4-7. 在角落上进行顶点捕捉
提示
您可以使用场景视图右上角的小部件切换到不同视图,如图 4-8 所示。
![psml 0408]()
图 4-8. 场景小部件
-
对每个墙段重复这个步骤。一些墙段会重叠和相交,这没关系。
完成后,您的墙壁应该看起来像图 4-9。在继续之前,务必保存您的场景。
![psml 0409]()
图 4-9. 四个最终墙壁
方块
在这个阶段,方块是我们需要在编辑器中创建的最简单元素。像我们大多数人一样,它存在于被推动的状态(在这种情况下,由代理推动)。我们将在 Unity 场景中添加这个方块:
-
向场景中添加一个新的立方体,并将其重命名为“方块”。
-
使用检查器给代理添加刚体组件,将其质量设置为
10,阻力设置为4,并且在所有三个轴上冻结其旋转,如图 4-10 所示。![psml 0410]()
图 4-10. 方块的参数
-
将方块放置在地板上的任意位置。任何位置都可以。
提示
如果您在精确定位方块的位置时遇到困难,可以像处理墙壁一样使用顶点捕捉模式的移动工具,并将代理捕捉到地板的一个角落(它将与墙壁相交)。然后使用方向移动工具(在移动模式下点击并拖动代理箭头上的箭头)或检查器将其移动到所需位置。
目标
目标是场景中代理需要推动方块的位置。它不仅仅是一个物理事物,更是一个概念。但是概念在视频游戏引擎中无法表示,那么我们如何实现它呢?这是一个很棒的问题,亲爱的读者!我们创建一个平面——一个平坦的区域——并将其设置为特定的颜色,以便观察者(也就是我们)可以知道目标区域在哪里。这种颜色不会被代理使用,只是为了我们自己。
代理将使用我们添加的碰撞器,这是一个大的空间体,位于有色地面区域的上方,使用 C# 代码,我们可以知道某物体是否在这个空间体内(因此称为“碰撞器”)。
按照以下步骤创建目标及其碰撞器:
-
在场景中创建一个新平面并将其重命名为“目标”或类似的名称。
-
为目标创建新材料并应用它。我们建议您使用一种显眼的颜色,因为这是我们希望代理将方块推入的目标区域。将新材料应用于目标。
-
使用顶点捕捉与您之前在“墙壁”中使用的相同技巧将目标使用 Rect 工具(可以通过键盘上的 T 键访问)或通过工具选择器定位,如图 4-11 所示。大致按图 4-12 的显示位置放置目标。
![psml 0411]()
图 4-11. 工具选择器
![psml 0412]()
图 4-12. 目标位置
-
使用检查器,从目标中删除 Mesh Collider 组件,并使用“添加组件”按钮添加 Box Collider 组件。
-
选择 Hierarchy 中的目标,在目标的 Inspector 中的 Box Collider 组件中点击“编辑碰撞器”按钮(如图 4-13 所示)。
![psml 0413]()
图 4-13. 编辑碰撞器按钮
-
使用小绿色方形手柄调整目标的碰撞器大小,以便它覆盖更多环境的体积,这样如果代理进入碰撞器,则会被检测到。我们的示例显示在图 4-14,但这不是一门科学;你只需把它做大!你可能会发现,只需使用检查器中的 Box Collider 组件增加其 Y 轴上的大小会更容易。
![psml 0414]()
图 4-14. 我们的大碰撞器,显示手柄
如前所述,请不要忘记保存场景。
代理
最后(差不多了),我们需要创建代理本身。我们的代理将是一个立方体,并附加适当的脚本(我们也会创建),就像我们在第二章中使用球代理一样。
仍然在 Unity 编辑器中,在你的场景中执行以下操作:
-
创建一个新的立方体并命名为“代理”或类似的名称。
-
在代理的检查器中,选择“添加组件”按钮并添加一个新的脚本。命名为类似“BlockSorterAgent”之类的名称。
-
打开新创建的脚本,并添加以下导入语句:
using Unity.MLAgents; using Unity.MLAgents.Actuators; using Unity.MLAgents.Sensors; -
将类更新为
Agent的子类。 -
现在你需要一些属性,首先是地板和环境的处理(我们稍后会回到分配这些)。这些放在任何方法之前的类内部:
public GameObject floor; public GameObject env; -
你还需要一些东西来表示地板的边界:
public Bounds areaBounds; -
你需要一些东西来代表目标区域和需要推到目标的块:
public GameObject goal; public GameObject block; -
现在添加一些
Rigidbody来存储块和代理的主体:Rigidbody blockRigidbody; Rigidbody agentRigidbody;
当代理初始化时,我们需要做一些事情,所以我们首先要做的就是Initialize()函数:
-
添加
Initialize()函数:public override void Initialize() { } -
在内部,获取代理和块的
Rigidbody:agentRigidbody = GetComponent<Rigidbody>(); blockRigidbody = block.GetComponent<Rigidbody>(); -
最后,对于
Initialize()函数,获取地板边界的处理:areaBounds = floor.GetComponent<Collider>().bounds;
接下来,我们想要能够在代理生成时(以及每次训练运行时)在地板上随机放置代理,因此我们会创建一个GetRandomStartPosition()方法。这个方法完全是我们自己的,不是实现 ML-Agents(像我们重写的方法)的必需部分:
-
添加
GetRandomStartPosition()方法:public Vector3 GetRandomStartPosition() { }每当我们想在模拟中的地板上随机放置东西时,我们将调用这个方法。它会返回地板上一个随机可用的位置。
-
在
GetRandomStartPosition()内部,获取地板和目标的边界:Bounds floorBounds = floor.GetComponent<Collider>().bounds; Bounds goalBounds = goal.GetComponent<Collider>().bounds; -
现在创建一个地板上新点的存储位置(稍后我们会回到这一点):
Vector3 pointOnFloor; -
现在,制作一个计时器,这样你就可以看到如果出于某种原因这个过程花费了太长时间:
var watchdogTimer = System.Diagnostics.Stopwatch.StartNew(); -
接下来,添加一个变量来存储边缘。我们将使用这个变量来从随机位置中添加和移除一个小缓冲区:
float margin = 1.0f; -
现在开始一个
do-while循环,继续选择一个随机点,如果选择的点在目标边界内,则继续:do { } while (goalBounds.Contains(pointOnFloor)); -
在
do内部,检查计时器是否花费了太长时间,如果是,则抛出异常:if (watchdogTimer.ElapsedMilliseconds > 30) { throw new System.TimeoutException ("Took too long to find a point on the floor!"); } -
然后,在
do内部的if语句下面,选择地板顶面上的一个点:pointOnFloor = new Vector3( Random.Range(floorBounds.min.x + margin, floorBounds.max.x - margin), floorBounds.max.y, Random.Range(floorBounds.min.z + margin, floorBounds.max.z - margin) );添加和移除
margin,这样箱子总是在地板上,而不是在墙壁或空间中: -
在
do-while之后,return你创建的pointOnFloor:return pointOnFloor;
对于GetRandomStartPosition()就是这样。接下来,我们需要一个函数,当代理将块移到目标位置时调用。我们将使用这个函数奖励代理做正确的事情,加强我们想要的策略:
-
创建
GoalScored()函数:public void GoalScored() { } -
添加一个调用
AddReward():AddReward(5f); -
并添加一个调用
EndEpisode():EndEpisode();
接下来,我们将实现OnEpisodeBegin(),这是在每个训练或推断episode开始时调用的函数:
-
首先,我们会放置函数在适当的位置:
public override void OnEpisodeBegin() { } -
我们将获取一个随机的旋转和角度:
var rotation = Random.Range(0, 4); var rotationAngle = rotation * 90f; -
现在我们将为块获取一个随机的起始位置,使用我们创建的函数:
block.transform.position = GetRandomStartPosition(); -
我们将设置块的速度和角速度,使用它的
Rigidbody:blockRigidbody.velocity = Vector3.zero; blockRigidbody.angularVelocity = Vector3.zero; -
我们将为代理获取一个随机的起始位置:
transform.position = GetRandomStartPosition(); -
并且我们将设置代理的速度和角速度,同样使用它的
Rigidbody:agentRigidbody.velocity = Vector3.zero; agentRigidbody.angularVelocity = Vector3.zero; -
最后,我们将旋转整个环境。我们这样做是为了让代理不会只学习总是有目标的那一边:
//env.transform.Rotate(new Vector3(0f, rotationAngle, 0f));
这就是 OnEpisodeBegin() 函数的全部内容。保存你的代码。
接下来,我们将实现 Heuristic() 函数,这样我们就可以手动控制代理:
-
创建
Heuristic()函数:public override void Heuristic(in ActionBuffers actionsOut) { }注意
这里对代理的手动控制与训练过程完全无关。它只是存在以便我们可以验证代理在环境中的移动是否恰当。
-
获取 Unity ML-Agents Toolkit 发送的动作,并设置动作为
0,以便最终在调用Heuristic()结束时你知道你总是会得到一个有效的动作或0:var discreteActionsOut = actionsOut.DiscreteActions; discreteActionsOut[0] = 0; -
然 然后,对于每个键(D、W、A 和 S),检查它是否被使用,并发送适当的动作:
if(Input.GetKey(KeyCode.D)) { discreteActionsOut[0] = 3; } else if(Input.GetKey(KeyCode.W)) { discreteActionsOut[0] = 1; } else if (Input.GetKey(KeyCode.A)) { discreteActionsOut[0] = 4; } else if (Input.GetKey(KeyCode.S)) { discreteActionsOut[0] = 2; }提示
这些数字完全是任意的。只要它们保持一致并且不重叠,它们是无所谓的。一个数字始终代表一个方向(在人类控制下对应一个按键)。
这就是 Heuristic() 函数的全部内容。
接下来,我们需要实现 MoveAgent() 函数,这将允许 ML-Agents 框架控制代理进行训练和推理:
-
首先,我们将实现这个函数:
public void MoveAgent(ActionSegment<int> act) { } -
然后,在内部,我们将清零用于移动的方向和旋转:
var direction = Vector3.zero; var rotation = Vector3.zero; -
然后,我们将从 Unity ML-Agents Toolkit 接收到的动作分配到更可读的东西上:
var action = act[0]; -
现在我们将根据该动作进行切换,并相应地设置方向或旋转:
switch (action) { case 1: direction = transform.forward * 1f; break; case 2: direction = transform.forward * -1f; break; case 3: rotation = transform.up * 1f; break; case 4: rotation = transform.up * -1f; break; case 5: direction = transform.right * -0.75f; break; case 6: direction = transform.right * 0.75f; break; } -
然后,在
switch外部,我们将根据任何旋转来行动:transform.Rotate(rotation, Time.fixedDeltaTime * 200f); -
我们还将根据任何方向对代理的
Rigidbody应用力:agentRigidbody.AddForce(direction * 1, ForceMode.VelocityChange);
MoveAgent() 就这些了。再次保存你的代码。
最后,现在,我们需要实现 OnActionReceived() 函数,它不做更多的事情,只是将接收到的动作传递给我们的 MoveAgent() 函数:
-
创建函数:
public override void OnActionReceived(ActionBuffers actions) { } -
调用你自己的
MoveAgent()函数,传递离散动作:MoveAgent(actions.DiscreteActions); -
并且根据步数设置负奖励来惩罚代理:
SetReward(-1f / MaxStep);这个负奖励希望能鼓励代理节约其移动并尽可能少地进行移动,以便最大化其奖励并实现我们从中期望的目标。
现在一切就绪了。确保在继续之前保存你的代码。
环境
在继续之前,我们需要在设置环境方面做一些行政工作,所以切换回 Unity 编辑器中的场景。我们将首先创建一个 GameObject 来容纳墙壁,只是为了保持 Hierarchy 的整洁:
-
在 Hierarchy 视图上右键点击,选择 Create Empty。将空 GameObject 重命名为 “Walls”,如 Figure 4-15 所示。
![psml 0415]()
图 4-15. 命名为 Walls 的墙体对象
-
选择所有四个墙壁(你可以依次点击它们,或者在点击第一个后按住 Shift 再点击最后一个),然后将它们拖放到新的墙壁对象下。此时应该看起来像 图 4-16 这样。
![psml 0416]()
图 4-16. 墙壁被很好地封装
现在我们将创建一个空的 GameObject 来容纳整个环境:
-
在层次视图中右键单击,并选择创建空对象。将空的 GameObject 重命名为“环境”。
-
在层次视图中,将我们刚刚创建的墙壁对象,以及代理、地板、块和目标,拖放到新的环境对象中。此时应该看起来像 图 4-17 这样。

图 4-17. 环境被封装
接下来,我们需要在我们的代理上配置一些东西:
-
在层次视图中选择代理,并在检视器视图中向下滚动到你添加的脚本。从层次视图中将地板对象拖放到检视器中的 Floor 槽中。
-
对整体环境 GameObject、目标和块执行相同操作。在编辑器中将 Max Steps 设置为
5000,以便代理不会花费太长时间将块推向目标。你的检视器应该看起来像 图 4-18 这样。![psml 0418]()
图 4-18. 代理脚本属性
-
现在,在代理的检视器中使用“添加组件”按钮,添加一个 DecisionRequester 脚本,并将其决策周期设置为 5,如 图 4-19 所示。
![psml 0419]()
图 4-19. 决策请求者组件已添加到代理并适当配置
-
添加两个 Ray Perception Sensor 3D 组件,每个组件都具有三个可检测标签:block、goal 和 wall,并使用 图 4-20 中显示的设置。
回到 “让代理观察环境” ,我们说过你可以通过代码或组件添加观察。那时我们全都通过代码实现。而这次我们将全部通过组件实现。所涉及的组件是我们刚刚添加的 Ray Perception Sensor 3D 组件。
![psml 0420]()
图 4-20. 两个 Ray Perception 传感器
提示
这次我们的代理甚至没有
CollectObservations方法,因为所有的观察都是通过我们在编辑器中添加的 Ray Perception Sensor 3D 组件收集的。 -
我们需要将刚刚使用的标签添加到我们实际想要标记的对象中。标签允许我们根据它们的标记引用对象,因此,如果某物被标记为“wall”,我们可以将其视为墙壁等等。在层次结构中选择块,并使用检视器添加一个新标签,如 图 4-21 所示。
![psml 0421]()
图 4-21. 添加一个新标签
-
将新标签命名为“block”,如 图 4-22 所示。
![psml 0422]()
图 4-22. 命名一个新标签
-
最后,如图 4-23 所示,将新标签附加到块上。
![psml 0423]()
图 4-23. 将标签附加到对象
-
对于目标,使用“goal”标签重复此操作,并对所有墙组件使用“wall”标签。有了这些,我们添加的射线感知传感器 3D 组件将仅“看到”使用“block”、“goal”或“wall”标记的物体。如图 4-24 所示,我们添加了两层射线感知器,它们从附加到它们的对象中发出一条线,并回报该线首次碰到的东西(在本例中,仅在它是墙、目标或块时)。我们添加了两个,它们被安置在不同的角度。它们只在 Unity 编辑器中可见。
![psml 0424]()
图 4-24. 射线感知传感器 3D 组件
-
最后,使用“添加组件”按钮添加一个行为参数组件。将行为命名为“Push”,并按图 4-25 设置参数。
![psml 0425]()
图 4-25. 代理的行为参数
在 Unity 编辑器中保存你的场景。现在我们将在我们的块上进行一些配置:
-
添加一个新的脚本到块中,命名为“GoalScore”。
-
打开脚本,并添加一个属性以引用代理:
public Block_Sorter_Agent agent;在这里创建的属性类型应该与附加到代理的类的类名匹配。
提示
这次不需要将父项更改为
Agent或导入任何 ML-Agents 组件,因为这个脚本不是一个代理。它只是一个普通的脚本。 -
添加一个
OnCollisionEnter()函数:private void OnCollisionEnter(Collision collision) { } -
在
OnCollisionEnter()内部,添加以下代码:if(collision.gameObject.CompareTag("goal")) { agent.GoalScored(); } -
保存脚本并返回到 Unity,在 Hierarchy 中选中块,将其从 Hierarchy 拖动到新的 GoalScore 脚本中的 Agent 槽位中。如图 4-26 所示。
![psml 0426]()
图 4-26. GoalScore 脚本
不要忘记再次保存场景。
训练和测试
当在 Unity 和 C# 脚本中构建了所有内容后,现在是训练代理并查看模拟工作的时候了。我们将按照 “使用模拟进行训练” 中的相同过程进行操作:创建一个新的 YAML 文件,用作训练的超参数。
这里是如何设置超参数的:
-
创建一个新的 YAML 文件,用作训练的超参数。我们的称为 Push.yaml,包括以下超参数和值:
behaviors: Push: trainer_type: ppo hyperparameters: batch_size: 10 buffer_size: 100 learning_rate: 3.0e-4 beta: 5.0e-4 epsilon: 0.2 lambd: 0.99 num_epoch: 3 learning_rate_schedule: linear network_settings: normalize: false hidden_units: 128 num_layers: 2 reward_signals: extrinsic: gamma: 0.99 strength: 1.0 max_steps: 500000 time_horizon: 64 summary_freq: 10000 -
接下来,在之前在 “设置” 中创建的
venv中,在终端中运行以下命令来启动训练过程:mlagents-learn _config/Push.yaml_ --run-id=PushAgent1注意
将
*config/Push.yaml*替换为你刚创建的配置文件的路径。 -
一旦命令启动并运行,你应该会看到类似图 4-27 的东西。此时,你可以在 Unity 中按下播放按钮。
![psml 0222]()
图 4-27. ML-Agents 进程开始训练
当你看到类似于图 4-28 的输出时,说明训练过程正在进行。
![psml 0223]()
图 4-28. 训练期间的 ML-Agents 过程
当训练完成后,请参考“训练完成后”章节,了解如何找到生成的.nn或.onnx文件的详细信息。
使用模型来运行代理程序,看看它的表现!
第五章:创建自动驾驶汽车
到目前为止,您构建的两个模拟都是相当抽象的概念——在虚空中滚动的球,推动其他立方体的立方体等——但是用于机器学习的模拟确实是真正实用的(我们保证)。 在本章中,我们将在 Unity 中制作一个非常简单的自动驾驶汽车,并使用强化学习来训练它驾驶。 从实用的角度来说,您无法将随后训练的模型加载到真正的物理汽车中,但是它表明了您在模拟环境中可以超越抽象的能力。
现在是你的时候了! 基本上,为了世界的利益,您将建造您自己的自动驾驶汽车(见图 5-1)。

图 5-1。 自动驾驶汽车在其赛道上。 在他的毡房中。 用他的 iPad。
我们的自动驾驶车辆将仅存在于其自身的美丽虚空中,而不是烦人的现实世界——因此我们可以避开所有那些讨厌的伦理困境,比如如果面前有一个人该怎么办(我们稍后会在本书中详细讨论)。
我们的汽车将学会如何驾驶,但除此之外并不多。 准备好了吗?
创建环境
我们需要做的第一件事是我们汽车存在的美丽虚空。 它将包括几个部分,其中最重要的是汽车将要驾驶的赛道。 在我们建造赛道之后,我们将创建汽车本身,然后设置所有内容以与机器学习系统一起工作。
世界的关键部分是赛道,这是汽车绕行的东西,以及汽车本身。 就这些了。 我们承认这是一辆相当简单的自动驾驶汽车。
我们将在 Unity 编辑器中进行这项工作,就像我们迄今为止完成的模拟一样。 它将涉及组装我们汽车生活的世界以及汽车本身。 与之前的活动之间的一个关键区别是,我们将提供一组可下载的资产,用于构建汽车的赛道。
在继续之前,请执行以下操作:
-
在 Unity Hub 中创建一个新的 3D Unity 项目(我们的项目名为“SimpleCar”),如图 5-2 所示。
![psml 0502]()
图 5-2。 在 Unity Hub 中创建新的 3D 项目
-
将 Unity ML-Agents 包导入 Unity 项目(参见“创建 Unity 项目”)。
-
确保您的 Python 环境已准备就绪(参见“设置”)。
赛道
我们要做的第一件事是赛道。 我们的赛道将非常简单,因为我们希望确保我们的自动驾驶汽车如何工作非常明显。 将有两个基本部分:如图 5-3 所示的直道部分,以及如图 5-4 所示的转角部分。

图 5-3。 我们赛道的直道部分

图 5-4。 我们赛道的角落块
赛道的每一部分由地板和一些隔离墙组成。现在让我们制作直道部件:
-
创建一个新平面。命名为“Track”,并且使用“track”标签标记它。
-
在项目视图中创建一个材质,命名为类似“TrackMaterial”的名称,以便知道它的用途,并给它一个适合道路的漂亮颜色。我们的是黑色,但你可以随意创意。将此材质分配给赛道平面。
-
创建一个新的立方体,并在检视器中将其缩放设置为
(1, 1, 10),使其变长而细。使用之前使用过的对齐工具将立方体沿平面的一条边放置。 -
在项目视图中创建一个材质,命名为类似“WallMaterial”的名称,并给它一个漂亮的颜色。将此材质分配给立方体。
-
复制立方体,并将其移动到平面的另一侧。你的部件应该看起来像图 5-5。
![psml 0503]()
图 5-5. 直道部件
-
将这两个墙片命名为“Wall”的某个变种,并将它们分配到“wall”标签。
-
创建一个空的 GameObject,命名为类似“Track_Piece”的变种,并将其设置为赛道平面和两堵墙的父对象,如图 5-6 所示。
![psml 0506]()
图 5-6. 层次结构中的赛道部件
-
接下来,选择赛道部件的父对象,然后选择“Assets”菜单 → 创建 → Prefab,如图 5-7 所示。
![psml 0507]()
图 5-7. 使用“Assets”菜单创建预制体
现在在你的项目面板中将会有一个赛道预制体,这是一个可复制的赛道部件。你可以修改一次预制体,然后更新所有使用它的地方。我们很快就会用这个预制体来组装我们的赛道。
提示
你也可以从层次结构中的父对象中拖动到项目面板中创建一个预制体。
接下来,我们将制作角落部件:
-
在场景中创建另一个新平面。命名为“CornerTrack”,并且使用“track”标签标记它(使用与之前部件相同的赛道标签)。将之前创建的赛道材质分配给它。
-
创建一个新的立方体,并在检视器中将其缩放设置为
(1, 1, 10),使其变长而细。使用之前使用过的对齐工具将立方体沿平面的一条边放置。将之前创建的墙体材质分配给它。 -
复制立方体并将其移动到平面的一侧,形成一个角落。你的部件应该看起来像图 5-8。
![psml 0508]()
图 5-8. 到目前为止的角落部件
-
创建一个新的立方体并放置在对角线上,如图 5-9 所示。
![psml 0504]()
图 5-9. 对角线上的对立角,最终角落部件
-
将这三个墙片命名为“Wall”的某个变种,并且将它们全部分配到“wall”标签,与之前部件中的墙壁相同。
-
创建一个空的 GameObject,命名为类似“Corner Piece”的变种,并将其设置为赛道平面和三堵墙的父对象,如图 5-10 所示。
![psml 0510]()
图 5-10. 角落部件的层级结构
-
接下来,选择角落部件的父对象,然后选择 Assets 菜单 → 创建 → Prefab。
现在你将在项目面板中看到一个角落预制件,以及轨道预制件。我们的显示在图 5-11 中,并附带它们使用的材料。

图 5-11. 两个轨道预制件及其材料
注意
如果你对 Unity 比较熟悉,可以按照自己的想法制作轨道部件!这本书不涵盖这方面的内容,但这是一个很好的学习练习。我们建议尝试 Blender,这是一个出色的开源 3D 建模工具。如果你在同一台计算机上安装了 Blender 和 Unity,并且将 .blend 文件拖入 Unity,你可以直接在 Unity 中使用该文件,并且在 Blender 中进行的任何更改并保存将自动反映在 Unity 中。
使用 Unity 的工具,就像你在早些活动中使用捕捉工具一样,将部件放置在一起以铺设赛道。我们的赛道显示在图 5-12 中,但你的外观目前并不重要。你只需要制作一个与我们类似复杂度的赛道即可。

图 5-12. 我们的训练赛道
注意
如果你不太愿意自己制作赛道,可以在本书提供的资产中找到预制赛道。下载资产后,你可以通过打开 CarPremadeTrack.unitypackage 文件并将其导入到 Unity 项目中来使用该赛道。
汽车
接下来,自然地,我们需要一辆汽车。我们的汽车不需要很复杂,并且不会以任何方式进行装配或动画(即,车轮不会转动,灯光不会工作)。它甚至可以是立方体形状,但为了好玩,我们将其设计成汽车样式。
要获取汽车并将其放入场景中,请按照以下步骤操作:
-
从 Sketchfab 下载一辆漂亮的汽车,如图 5-13 所示。我们使用的那辆汽车可以在这里找到,但任何汽车都可以。
![psml 0513]()
图 5-13. 我们将要使用的汽车
-
将汽车导入到 Unity 项目中,方法是将 .blend 文件拖放到 Assets 视图中。
-
接下来,在层级中创建一个空的 GameObject,并命名为“Car”。
-
将一个 Rigidbody 组件添加到汽车上,如图 5-14 所示。
![psml 0514]()
图 5-14. 汽车的 Rigidbody 组件
-
将一个盒型碰撞体添加到汽车上,如图 5-15 所示。
![psml 0515]()
图 5-15. 汽车的盒型碰撞体
-
将新添加的汽车模型拖入汽车 GameObject 中,如图 5-16 所示。确保位于 GameObject 中的汽车模型位于
(0,0,0)位置。![psml 0516]()
图 5-16. 汽车的 GameObject,包含其中的模型
警告
确保 Rigidbody 和 Box Collider 组件附加到最上层的汽车 GameObject 上(即你创建的那个),而不是添加模型到场景时创建的内部 GameObject 上。
-
将汽车 GameObject 定位在赛道上的中心位置,作为你的起始点(具体位置不重要)。我们的位置如 图 5-17 所示。

图 5-17. 赛道上的汽车,处于起始位置
这就是场景中环境的全部内容,所以不要忘记保存。
设置机器学习环境
接下来,我们需要设置项目为一个机器学习模拟。我们会像之前一样,通过安装 ML-Agents Unity 包,并在 Unity 编辑器中的 GameObject 上添加一些组件来实现这一点:
-
使用 Unity Package Manager 安装 Unity ML-Agents Toolkit。如果需要提醒,请参考 第二章。
-
安装完成后,向汽车的 GameObject 添加一个 Behavior Parameters 组件,如 图 5-18 所示。
![psml 0518]()
图 5-18. 汽车的新行为参数组件
-
接下来,添加一个决策请求器组件到汽车的 GameObject 上,如 图 5-19 所示。
![psml 0519]()
图 5-19. 决策请求器组件,添加到汽车上
在这一点上,你可能应该保存你的 Unity 场景。完成后,现在是时候制作汽车的脚本了:
-
通过在汽车的 GameObject 上添加组件在 Unity 编辑器中创建一个新的脚本。命名为“CarAgent”或类似的名称。
-
打开新创建的 CarAgent 脚本资源,在内部导入适当的 Unity ML-Agents Toolkit 部分,除了样板导入之外:
using Unity.MLAgents; using Unity.MLAgents.Sensors; -
更新
CarAgent,使其继承自Agent,移除 Unity 提供的所有样板代码:public class CarAgent : Agent { }和之前的模拟一样,为了成为一个智能体,我们的汽车需要继承自来自 Unity ML-Agents 包的
Agent。 -
添加一些变量:
public float speed = 10f; public float torque = 10f; public int progressScore = 0; private Transform trackTransform;我们将存储汽车的
speed和torque,以便我们可以调整它们,还有一个progressScore。这允许我们显示和使用分数(沿着赛道的进度),如果我们希望的话。我们还将创建一个地方来存储
trackTransform,我们将根据汽车所在的赛道位置来更新它。Transform类型表示你创建的 3D 场景中的位置,并且是 Unity 中的一种类型。 -
实现
Heuristic()方法,以便根据需要由你作为人类进行测试和控制汽车:public override void Heuristic(float[] actionsOut) { actionsOut[0] = Input.GetAxis("Horizontal"); actionsOut[1] = Input.GetAxis("Vertical"); }这个
Heuristic()方法与我们在之前的模拟中所做的非常相似:它允许我们将actionsOut数组的两个元素分配给 Unity 输入系统的Horizontal和Vertical轴。无论在 Unity 输入系统中为Horizontal和Vertical轴分配了什么键,都将控制输入到actionsOut。提示
我们在这里使用的是 Unity 的“Classic”输入系统。技术上已被新输入系统取代,但是像游戏引擎中的大多数事物一样,从不会删除任何东西,并且在这里使用新输入系统的增加复杂性没有任何优势。您可以在 Unity 文档中了解关于经典输入系统的信息链接,并了解 Input Manager 链接,该管理器允许您配置分配给轴的键。
您可以通过选择编辑菜单 → 项目设置,并从对话框的侧边栏中选择输入管理器,来选择为轴分配哪些键。默认情况下,键盘上的箭头键已分配。
-
创建一个
PerformMove()方法,该方法接受三个浮点数——水平移动、垂直移动和增量时间——并适当地进行车辆的平移和旋转:private void PerformMove(float h, float v, float d) { float distance = speed * v; float rotation = h * torque * 90f; transform.Translate(distance * d * Vector3.forward); transform.Rotate(0f, rotation * d, 0f); }
我们将使用这个PerformMove()方法来移动车辆,无论是通过人工控制还是机器学习的brain。这里发生的唯一事情就是在车辆的变换上调用Translate和Rotate(因为此脚本作为组件附加到场景中的车辆代理),以便移动它。
-
覆盖
OnActionReceived()方法是 Unity ML-Agents 框架中Agent所必需的一部分:public override void OnActionReceived(float[] vectorAction) { float horizontal = vectorAction[0]; float vertical = vectorAction[1]; PerformMove(horizontal, vertical, Time.fixedDeltaTime); }我们的
OnActionReceived()方法使用从 ML-Agents 框架接收的两个vectorAction动作,映射到水平和垂直方向,获取上一个或当前的预移动位置,并调用我们之前创建的PerformMove()函数来执行移动操作。我们很快将在这个方法中添加一些奖励功能,但现在我们将其保持不变。
-
接下来,实现
CollectObservations(),这是 ML-Agents 框架方法的另一个覆盖:public override void CollectObservations(VectorSensor vectorSensor) { float angle = Vector3.SignedAngle (trackTransform.forward, transform.forward, Vector3.up); vectorSensor.AddObservation(angle / 180f); }
CollectObservations()用于向 ML-Agents 系统提供关于环境的信息。观察是代理所知道的关于其所处世界的信息,您可以决定提供多少信息。
对于车辆代理,我们在CollectObservations()中唯一要做的就是比较车辆的方向与赛道的方向。这使用我们之前创建的trackTransform,该变量保存车辆的当前位置。这个初始观察给了车辆需要处理的东西:它需要最小化这个角度以便跟随赛道。这是一个带符号的角度,范围在-180到180之间,用于告诉车辆是否需要向左或向右转向。
现在我们将暂时返回 Unity 编辑器,通过组件添加一些额外的观察。正如我们之前所说,不是所有的观察都必须通过代码到达;有些可以通过 Unity 编辑器中的组件添加。保存您的代码,然后返回 Unity 场景:
-
在层次结构中选择代理的父对象,并在检视器中使用“添加组件”按钮添加两个 Ray Perception Sensor 3D 组件。
-
给它们取一个合理的名字(例如,“RayPerceptionSensor1”,“RayPerceptionSensor2”)。
-
将其中一个设置为图 5-20 中显示的参数。
![psml 0520]()
图 5-20。两个三维射线感知传感器组件中的第一个
这个传感器从汽车两侧各发送四条射线,从前方发送一条,如图 5-21 所示。
![psml 0521]()
图 5-21。第一个传感器的射线
-
将另一个三维射线感知传感器组件设置为图 5-22 中显示的参数。
![psml 0522]()
图 5-22。两个三维射线感知传感器组件中的第二个
注意
重要的是这两个传感器都设置为只检测标记为“wall”的物体。
这个传感器从汽车前方发送一条射线和从后方直接发送一条射线,如图 5-23 所示。
提示
如果我们没有重写
CollectObservations()并在代码中实现它,如果我们愿意,我们仍然可以通过编辑器中的组件专门为我们的代理提供观察。![psml 0523]()
图 5-23。第二个传感器的射线
-
保存场景,并返回到代码编辑器中的代理脚本。
现在我们将实现自己的函数,名为TrackProgress()。它将用于制定奖励系统:
private int TrackProgress()
{
int reward = 0;
var carCenter = transform.position + Vector3.up;
// Where am I?
if (Physics.Raycast(carCenter, Vector3.down, out var hit, 2f))
{
var newHit = hit.transform;
// Am I on a new spot?
if (trackTransform != null && newHit != trackTransform)
{
float angle = Vector3.Angle
(trackTransform.forward, newHit.position - trackTransform.position);
reward = (angle < 90f) ? 1 : -1;
}
trackTransform = newHit;
}
return reward;
}
TrackProgress()如果我们向前移动到道路的新部分,将返回1,如果向后移动,则返回-1,其他情况返回0。
它通过采用以下逻辑来实现:
-
它从汽车对象的中心向下投射一条射线到地面。
-
利用从那个射线获取的信息,它知道汽车当前在赛道的哪个瓷砖上。
-
如果当前的瓷砖与上一个不同,它会计算瓷砖方向与汽车位置(相对于瓷砖)之间的角度。
-
如果那个角度小于 90 度,它就向前移动;否则,它就向后移动。
重要的是要让汽车具备判断是否在前进的能力;否则,它就不会知道何时应该受到奖励。这就是这个函数的作用。接下来,我们需要创建一些新方法:
-
首先,我们需要实现
OnEpisodeBegin(),这是 ML-Agents 框架的另一个重写:public override void OnEpisodeBegin() { transform.localPosition = Vector3.zero; transform.localRotation = Quaternion.identity; }我们在这里做的不多:只是将汽车的本地位置和旋转设置为
zero和identity。这个函数在每个 episode 开始时被调用,因此我们在这里用它来重置汽车的本地位置和旋转。 -
接下来要实现的方法是
OnCollisionEnter(),我们将用它来确保汽车代理在与墙碰撞时受到相应的惩罚:private void OnCollisionEnter(Collision collision) { if (collision.gameObject.CompareTag("wall")) { SetReward(-1f); EndEpisode(); } }OnCollisionEnter()是 Unity 对象的标准部分,并在场景中的物体碰撞时调用。在这种情况下,我们检查碰撞的物体是否被标记为 “wall”。我们将在 Unity 编辑器中很快为环境中的物体添加 “wall” 和其他一些有用的标记。如果汽车代理与墙壁相撞,它会受到-1奖励的惩罚,并且调用了属于 ML-Agents 的EndEpisode()函数以开始新的一集。 -
接下来,我们将添加一个
Initialize()方法,该方法首次调用TrackProgress():public override void Initialize() { TrackProgress(); }Initialize()是 Unity 的一部分,在对象首次实例化时调用。 -
回到
OnActionReceived(),在我们之前编写的代码结尾,在调用PerformMove()之后,我们将添加一些代码:var lastPos = transform.position; int reward = TrackProgress(); var dirMoved = transform.position - lastPos; float angle = Vector3.Angle(dirMoved, trackTransform.forward); float bonus = (1f - angle / 90f) * Mathf.Clamp01(vertical) * Time.fixedDeltaTime; AddReward(bonus + reward); progressScore += reward;这段代码首先在汽车移动前存储位置,然后调用
TrackProgress()检查我们所在的瓦片是否发生了改变。利用这两个信息,我们计算一个代表我们移动方向的向量
dirMoved,然后用它来获取当前赛道片段与代理之间的角度。因为我们获取的角度在 0 到 180 度之间,如果我们将其映射到更小的范围 0–2,会更容易:我们通过 90 进行除法。从 1 中减去这个值会给出一个小的奖励(当角度增加时会减小)。如果我们的角度大于 90 度,则会变成负数。
结果乘以垂直速度(为正),因此我们有了一个奖励。我们将整个结果乘以时间 (
Time.fixedDeltaTime),以便我们每秒最多只获得一个奖励。
不要忘记在代码编辑器中保存代码和在 Unity 编辑器中保存场景。
训练仿真
在一切都建好之后,我们将设置训练环境,然后看看我们简单的自动驾驶汽车在实践中是如何工作的。第一步是将行为设置为启发式,这样我们就可以使用键盘控制汽车进行测试,然后我们将进行训练。
要将汽车代理的行为类型设置为启发式,请在 Unity 编辑器中打开场景,选择 Hierarchy 中的代理并将行为类型更改为启发式,如 图 5-24 中所示,然后在 Unity 中运行场景。

图 5-24. 将行为类型设置为启发式
您可以使用键盘(箭头和 WASD 键,很可能会使用这些键,除非您在 Unity 输入系统中修改了它们)来驾驶汽车在赛道上行驶。很神奇,对吧?
警告
驾驶可能会非常困难。
训练
与前几章一样,训练仿真需要一个配置文件和一些 ML-Agents 脚本来读取它们:
-
首先,我们需要一个传统的 YAML 文件作为我们的训练超参数。创建一个名为 CarAgent.yaml 的新文件,并包含以下超参数和值:
behaviors: CarDrive: trainer_type: ppo hyperparameters: batch_size: 1024 buffer_size: 10240 learning_rate: 3.0e-4 beta: 5.0e-3 epsilon: 0.2 lambd: 0.95 num_epoch: 3 learning_rate_schedule: linear network_settings: normalize: false hidden_units: 128 num_layers: 2 vis_encode_type: simple reward_signals: extrinsic: gamma: 0.99 strength: 1.0 keep_checkpoints: 5 max_steps: 1.0e6 time_horizon: 64 summary_freq: 10000 threaded: true -
接下来,在层级面板中选择汽车代理,然后在行为参数组件的行为类型下拉菜单中选择默认,如图 5-25 所示。
![psml 0525]()
图 5-25. 将行为类型设置为默认
-
准备好开始训练了。启动之前创建的虚拟环境,并通过在终端中运行以下命令开始训练过程:
mlagents-learn config/CarAgent.yaml --run-id=CarAgent1注意
你需要用刚刚创建的配置文件的路径替换到 YAML 文件的路径。
一旦系统执行了 mlagents-learn,你应该会看到类似于图 5-26 的东西。在 Unity 编辑器中按下播放按钮。

图 5-26. 准备开始训练汽车
训练将运行 1.0e6 步(也就是 1,000,000 步)。如果愿意,你可以使用 TensorBoard 监控训练进度。有关详细信息,请参阅“使用 TensorBoard 监控训练”。
当训练完成时
最终,训练将完成,你将获得一个 .onnx 或 .nn 文件,如图 5-27 所示。

图 5-27. 当训练完成时,会生成 .onnx 文件。
接下来,我们需要将新训练的机器学习模型(存储在 .nn 或 .onnx 文件中)附加到代理程序:
-
将新模型文件拖到 Unity 编辑器中的项目窗格中,然后将其附加到你的代理程序,如图 5-28 所示。
![psml 0528]()
图 5-28. 在 Unity 编辑器中附加的模型文件
-
运行项目,看着你的自动驾驶汽车在赛道上自动驾驶!令人惊叹。
第六章:介绍模仿学习
在这一章中,我们将讨论模仿学习(IL)。与其他形式的机器学习略有不同,IL 的目的并不是为了达到特定的目标。相反,它的目的是复制其他事物的行为。那个其他事物?可能是人类。
为了探索 IL,我们将创建另一个基于球的代理程序,它可以在周围滚动,并且我们将训练它去寻找和捡起一个硬币(经典视频游戏风格的捡取)。但与其通过奖励信号强化行为来训练它相反,我们将使用我们自己的大脑来训练它。
这意味着,最初,我们将自己移动代理程序,使用键盘,就像我们在前几章中使用启发式行为来控制代理程序一样。不同之处在于,这一次当我们驱动代理程序时,ML-Agents 将观察我们,一旦完成,我们将使用 IL 让代理程序学会如何复制我们的行为。
注意
IL 不仅可以让您创建更像人类的行为,还可以用来启动训练。一些任务有非常高的初始学习曲线,克服这些早期障碍的训练可能相当缓慢。如果人类能向代理程序展示如何完成任务,代理程序可以将其作为起点并优化方法。幸运的是,人类在很多事情上都做得很好,IL 让您能够利用这一点。IL 的一个缺点是,它不擅长发现新颖的方法,通常比其他方法更早达到顶峰。它的效果取决于所展示的演示。
IL 的主要优势是,您可以比其他机器学习技术更快地获得结果,而无需设置任何奖励结构。这种情况下,奖励信号将自动是它与我们行为匹配程度的衡量,而不是更显式的东西。我们将在第七章中探讨如何与 IL 一起使用奖励。
在这一章中,我们将使用一种特定的 IL 方法,称为行为克隆(BC),这种方法在 Unity 和 ML-Agents 中相对容易实现,但比其他技术有更多限制。我们将在遇到这些限制时详细讨论它们。
提示
Unity 还支持一种称为生成对抗性模仿学习(GAIL)的 IL 技术。我们将在第七章中使用 GAIL。
仿真环境
我们的 IL 仿真环境将是相当简单和抽象的。你可以在图 6-1 中看到我们版本的图像。我们的环境将有一个用于地面的大平面,一个球作为我们的代理程序,以及一个扁平的圆柱体作为我们的目标硬币(相信我们,这是一个硬币!)。

图 6-1. 我们将要构建的仿真
要构建仿真环境,我们需要:
-
制造地面
-
制作目标。
-
制作代理。
让我们开始吧:
-
使用 Unity Hub 创建一个新的 3D Unity 项目。我们的项目名为“ImitationLearningBall”。
-
导入 Unity ML-Agents 包。
-
确保你的 Python 环境准备就绪。
完成此操作后,继续构建环境。
创建地面。
首先,我们需要我们的地面存在,因为没有地面,我们的球将很难滚动到任何地方。在创建新的 Unity 项目、导入 Unity ML-Agents 包并进入空场景之后,你需要:
-
创建一个平面,命名为“地面”,并确保其位置为
(0, 0, 0)。现在我们的地面位置正确,我们将快速地给它一个不同的外观,以便我们稍后在环境中能更容易地区分它和其他即将存在的元素。
-
在项目视图中创建一个材质,命名为“地面材质”或类似名称,并通过更改反照率属性给它一个漂亮的颜色,看起来像地面(我们推荐一个漂亮的草绿色)。
-
将材料分配给地面平面,通过将其拖动到上面。
当完成时,你应该有类似于图 6-2 的东西。

图 6-2。我们 IL 场景的地面平面
我们的地面已经准备好了,现在是时候制作目标了。在继续之前,请不要忘记保存你的场景。
创建目标。
我们此场景的目标将是一个大金币。对于一个滚动的球来说,还能想要什么呢?(如果它足够好用于马里奥,对我们的球来说也足够好!)
要创建硬币,请打开 Unity 场景并按照以下步骤操作:
-
创建一个新的圆柱体,并命名为“目标”。
-
使用检视器,将目标的位置更改为
(0, 0.75, 0),其旋转更改为(0, 0, 90),并将其缩放更改为(1.5, 0.1, 1.5)。
这给了我们一个漂亮的平面圆盘,但看起来并不像一个硬币,所以让我们通过给它一点材质来改变这一点:
-
创建一个新材质,并命名为与目标硬币相关的名称。
-
使用检视器,将新材质的反射率颜色设置为漂亮的金黄色。
-
仍然在材料检视器中,将金属滑块拖到最右边,直到读数为
1.0,并将光滑度滑块拖动到约0.3。 -
将新材质拖到场景中目标物体或层级上以应用它。
当完成时,你应该有一个看起来类似于图 6-3 的硬币。视频游戏真是不可思议吧?

图 6-3。我们 IL 场景的目标硬币
当然,如果不慢慢旋转,它就不是一个真正的视频游戏硬币了。这与任何机器学习或模拟的任何方面都无关,但我们觉得这非常重要:
-
在层级中选择硬币。
-
在检视器中,点击“添加组件”按钮。
-
在下拉菜单中选择“新建脚本”选项。
-
将脚本命名为“CoinSpin”,然后按回车。
-
在你的代码编辑器中打开CoinSpin.cs脚本。
-
用以下代码替换文件中的代码:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class CoinSpin : Monobehavior { public float speed = 10; void Update() { var rotationRate = Time.deltaTime * speed; transform.Rotate(Vector3.left * rotationRate); } }
在 Unity 编辑器中检查器中暴露了一个名为speed的浮点变量(因为在代码中设置为public),如果您想让硬币旋转得更快或更慢,可以使用它。
现在,如果在 Unity 中播放场景,目标硬币将会缓慢旋转。完美。
注意
让硬币旋转或使其变成闪亮的金色对于培训来说并不是必要的。然而,因为最初需要人类来驾驶球,所以值得花一点时间使事物更加清晰和有趣。人类不喜欢看抽象的白色形状,稍微增加一些乐趣很少是一个坏主意,即使计算机并不在乎。
我们最后需要做的是设置硬币处理碰撞球的情况。
当您创建 Unity 的预定义多面体之一(例如我们将其制成的圆柱体),它会带有碰撞器,让我们知道何时发生碰撞。默认情况下,Unity 将碰撞器视为实体对象,但我们不希望如此。我们不希望球弹开硬币,我们希望球穿过硬币,并通知我们发生了碰撞。
为此,我们需要将我们的碰撞器转换为触发体积:
-
在 Hierarchy 中选择硬币。
-
在检查器中,找到胶囊碰撞器部分。
-
勾选“是触发器”复选框以使其成为触发器。
现在当东西与硬币碰撞时,不会像撞墙那样,我们会被通知有东西碰到了,但不会有物理效果。
我们仍然需要一个简单的方法来让球知道它碰到了硬币而不是其他东西。就像我们在第五章中所做的那样,我们将使用 Unity 编辑器的标签来实现这一点。标签可以让您快速地用附加元数据标记场景中的某些对象,虽然有其他方法可以实现相同的效果,但标签非常轻便和方便:
-
在 Hierarchy 中选择硬币。
-
在检查器中,选择标签下拉菜单,如图 6-4 所示。
![psml 0604]()
图 6-4。初始标签列表
有几个预定义的标签,但我们不想要这些,所以我们将自己制作:
-
从下拉菜单中选择“添加标签”选项。这将打开标签编辑器,允许我们创建新标签。
-
点击
+按钮创建一个新标签。 -
将标签命名为“目标”。
-
在 Hierarchy 中选择硬币,并从检查器中打开标签下拉菜单。
-
选择新创建的“目标”标签,如图 6-5 所示。
![psml 0605]()
图 6-5。修改后的标签列表
现在我们的硬币被标记为目标,稍后可以用来区分物体。我们的目标硬币完成了!别忘了保存。
名称为 Ball,Agent Ball
是时候制作我们的球体代理了。我们知道,思考任何形式的球体总是令人兴奋的,而我们的球体代理也不例外。此时,我们只是设置我们球体的物理属性,而不涉及 ML 元素。
在 Unity 编辑器中,执行以下操作:
-
在层次视图中创建一个新的球体,并将其命名为 “Ball”。
-
使用检视面板,将球体的位置设置为
(0, 0.25, 0),其缩放设置为(0.5, 0.5, 0.5)。 -
使用球体的检视面板添加一个 Rigidbody 组件。你不需要修改其任何参数。
现在我们已经创建并放置了一个球体在场景中,并且给它添加了 Rigidbody,因此它存在于物理系统中。
提示
Rigidbody 是 Unity 的物理系统中允许球体参与的组件,它赋予了球体各种物理属性,比如质量。
最后,我们希望给球体代理赋予一个不同的外观:
-
在资源窗口中创建一个新的材质。
-
将其重命名为 “Ball_Mat。”
-
从 书籍网站 下载书籍的资源,并找到 ball_texture.png 文件。将该文件拖放到 Unity 的项目窗口中。
-
选择球体材质。
-
将球体纹理从资源区域拖拽到检视面板中的 Albedo 字段中。
-
将材质从资源拖放到场景中的球体上。
现在我们的球体在 图 6-6 中已经准备就绪,看起来不错。如果你喜欢,可以自己制作球体的纹理。

图 6-6. 我们的球体应用了新的材质
继续操作前别忘了保存场景。
摄像机
尽管摄像机在训练中没有使用(我们稍后将在 第十章 中讨论),作为人类,在驾驶球体时我们需要能够看到环境,因此我们需要将摄像机放置在我们感到舒适的位置。
这里没有真正的规则,任何你认为最适合你的摄像机角度和位置都可以。但是,如果你希望在任何时候都能看到整个地面,请在 Unity 编辑器中使用以下设置:
-
在层次视图中选择主摄像机。
-
在检视面板中,将摄像机的位置更改为
0, 5, 0。 -
在检视面板中,将旋转设置为
90, 0, 0。 -
在检视面板中找到摄像机组件部分。
-
将视野设置为
90。
现在你应该能够从上方俯视整个地面和其上的任何物体,就像 图 6-7 中展示的那样。

图 6-7. 俯视全视角摄像机
再次,不要忘记保存场景。
构建模拟
随着大部分环境的设置和配置完成,我们现在可以转向模拟和训练方面。
我们将在这里进行多项操作,其中大部分与我们的球体相关(即将成为一个代理)。
我们将要采取的步骤应该开始变得熟悉了,但是如果你感觉有些生疏,我们将会:
-
配置球体成为一个代理
-
编写启发式控制代码,使我们能够驱动球。
-
编写代码以生成观察结果。
-
编写代码以在成功或失败时重置环境。
因为我们使用的是 IL 而不是 RL,所以在这种情况下我们不会使用奖励。这意味着你将看不到我们提供任何奖励,无论是正面的还是负面的。
注意
如果我们愿意,我们可以包括奖励,但这对这个特定的场景没有任何影响,所以我们不打算这么做。在下一章中,当我们研究用于模仿学习的 GAIL 方法时,我们将进一步探讨如何结合奖励和模仿。
代理组件。
我们的球将需要成为一个代理,但目前它只是一个带有自卑情结的球,所以让我们来修复它。在你的场景中,在 Unity 编辑器中执行以下操作:
-
在层次结构中选择球。
-
在检视器中,单击“添加组件”按钮。
-
添加一个决策请求器组件。
-
将决策周期更改为
10。
这将添加许多其他组件。有些是必需的,但其他一些则不需要,所以我们将做一些调整:
-
在行为参数组件内部检视器中,将行为名称更改为“RollingBall”。
-
将向量观察空间大小更改为
8,而不是1。 -
将连续动作更改为
2。
这些意味着我们已经设置了我们的代理以具有八个观察结果和两个控制动作,但这并不允许启发式控制。现在让我们添加它。这些将是与过去所做的不同的新步骤:
-
在球代理的检视器中,单击“添加组件”按钮。
-
添加演示记录器组件。
-
将演示名称设置为“RollerDemo”。
-
将演示指导者设置为 Assets/Demos。
演示记录器是一个组件,允许 ML-Agents 系统观察我们驱动球的行为,并将其记录到我们之前设置的目录中的文件中。该文件将用于训练。
最后,我们需要设置一个代理脚本。因为决策请求组件需要一个代理脚本,所以它为我们添加了一个默认版本,但我们希望自定义一个:
-
在检视器中,单击“添加组件”按钮。
-
在球上添加一个名为“Roller”的新脚本。
-
在你的代码编辑器中打开Roller.cs。
现在我们将配置球以成为一个代理。我们将在这里进行一些基本设置,然后在后续部分添加更多内容:
-
在Roller.cs文件的顶部添加以下导入:
using UnityEngine; using Unity.MLAgents; using Unity.MLAgents.Sensors;这些为我们提供了我们将使用的基本 ML 组件,以及访问 UnityEngine 库的能力,我们将需要它来生成一些随机数。
-
修改
Roller的类定义:public class Roller : Agent -
向
Roller添加以下实例变量:public float speed = 10; public Transform goal; private Rigidbody body; private bool victory = false; -
用以下内容替换
Start方法:void Start() { body = GetComponent<Rigidbody>(); }我们在这里做了几件事情:首先,我们将
Roller作为Agent的子类,这意味着我们可以得到 Unity 为我们添加的默认代理。然后我们设置了我们将需要的四个不同属性。speed和goal是公共的,意在在检查器中设置,我们马上就会这样做。它们控制着球的移动速度以及它应该以什么GameObject作为目标。body跟踪物理 Rigidbody 组件,以便我们可以根据需要添加和移除力。并且
victory将用于确定是否已达到目标。 -
在检查器中,将场景中的目标硬币拖到 Roller 组件的目标槽中。
-
在检查器中,删除 Unity 添加的默认代理组件。
完成以上步骤后,您现在应该在您的球代理上有类似于图 6-8 的 ML-Agents 组件。

图 6-8. 我们的球,其代理组件正确配置完成。
注意
根据您的 ML-Agents 版本,可能不会自动安装相同的组件。如果是这种情况,您将需要一个决策请求器、一个行为参数以及一个演示记录器组件。您还需要我们的自定义代理脚本 Roller。
别忘了保存一切。
添加启发式控制。
由于我们将驱动球以生成训练行为,我们需要一种直接移动球的方式,通常我们可以使用启发式控制来做到这一点:
-
打开Roller.cs。
-
在类中添加以下方法:
public override void Heuristic(in ActionBuffers actionsOut) { var continuousActionsOut = actionsOut.ContinuousActions; continuousActionsOut[0] = Input.GetAxis("Horizontal"); continuousActionsOut[1] = Input.GetAxis("Vertical"); }当 ML-Agents 需要代理执行操作时,此方法将被调用,但现在我们拦截了这一过程,并提供了我们自己的操作。
我们正在使用默认的 Unity 输入系统获取
0-1归一化的水平和垂直值,以控制我们的球。默认情况下,这些值映射到游戏中的标准 WASD 或箭头键控制方案,非常适合我们。现在,我们需要能够为每个集进行配置代理及其目标。
-
在类中添加以下方法:
public override void OnEpisodeBegin() { victory = false; body.angularVelocity = Vector3.zero; body.velocity = Vector3.zero; this.transform.position = new Vector3(0, 0.25f, 0); var position = UnityEngine.Random.insideUnitCircle * 3; goal.position = new Vector3(position.x, 0.75f, position.y); }这做了几件事情。首先,我们将胜利标志重置为 false,尽管它尚未改变。然后,我们从球体中移除所有力,并将其重置到地面的中心。
最后,在半径为 3 的圆上生成一个随机位置(这与我们的地面的大小很匹配),并将目标设置为该位置。在我们自己驱动球之前,唯一剩下的就是添加操作代码。
-
在类中添加以下方法:
public override void OnActionReceived(ActionBuffers actions) { var continuousActions = actions.ContinuousActions; Vector3 controlSignal = Vector3.zero; controlSignal.x = continuousActions[0]; controlSignal.z = continuousActions[1]; body.AddForce(controlSignal * speed); if (victory) { EndEpisode(); } else if (this.transform.localPosition.y < 0) { EndEpisode(); } }在这里,我们获取动作值的水平和垂直分量,并使用它们向我们球的物理体添加一个小力量。这实质上是将球推向动作值的方向,使用了 Unity 的物理系统。
提示。
您可以在Unity 文档中了解更多关于
AddForce()的信息。然后我们快速检查一下是否已经获胜(目前还不能),或者是否已经掉到了边缘。如果是这样,我们就结束本集并将一切重置回之前的状态。
完成这些操作后,保存工作并返回 Unity 编辑器,以便我们可以测试我们的代码。
-
在检视器中,在行为参数脚本中,将行为类型设置更改为启发式。
-
播放场景。
您现在可以使用键盘驾驶球。如果您从世界的边缘掉落,环境应该会重置。万岁!
警告
您可能注意到 Unity 警告您关于观察数与设置值不匹配的问题,我们将在下一步修复。这是因为我们尚未提供我们打算提供的所有观察,而我们在配置 Unity 编辑器中代理的行为参数组件的向量观察空间大小时告诉 Unity 预期有八个观察。
如往常一样,请记得保存场景。
观察和目标
尽管我们可以完美地驾驭我们的球,但它对自己的世界一无所知,所以它没有学习的能力。
为了使球体代理理解世界,它需要观察。与我们迄今为止构建的每个模拟代理一样,观察是代理了解所处世界的内容。
在这样做的同时,我们还应处理当我们击中硬币时发生的事情以及我们如何击中硬币:
-
打开 Roller.cs。
-
添加以下方法:
public override void CollectObservations(VectorSensor sensor) { sensor.AddObservation(goal.position); sensor.AddObservation(this.transform.position); sensor.AddObservation(body.velocity.x); sensor.AddObservation(body.velocity.z); }当 ML-Agents 需要为代理收集观察时,将调用此方法。对于这种情况,我们的观察相当简单:我们传递目标位置和自身位置 (
x和z坐标)。我们还传递球的水平和垂直速度。这就结束了观察,现在是时候加入碰撞了。
-
在 Roller.cs 中添加以下方法:
void OnTriggerEnter(Collider other) { if (other.CompareTag("Goal")) { victory = true; } }OnTriggerEnter是 Unity 中的内置函数,当物体进入触发器体积(例如我们的硬币的触发器)时调用。
在这里,我们只是检查我们碰撞的物体是否标记为“目标”,我们的硬币就是这样,并且如果是,我们告诉它设置一个已达成目标的标志。
保存您的脚本。现在,如果我们返回 Unity 并播放场景,我们可以驾驶球,拾取硬币,并重置这一事件。
我们准备好开始生成一些训练数据并进行一些学习。
警告
当我们使用 Unity 物理系统让我们知道何时发生碰撞时,这可能会在更复杂的场景中导致一些奇怪的结果。事件的定时让代理能够关联行动、观察和奖励,这意味着您不能总是依靠 OnTriggerEnter 在需要时触发训练。在本例中,我们的示例很简单,这使我们能够更深入地了解 Unity 物理系统,因此我们认为这是值得的。然而,对于大多数情况,建议进行距离检查。
生成数据和训练
配置正确的场景后,我们可以生成一些训练数据。现在是向机器人展示如何将球开到硬币的时候了。
创建训练数据
令人欣慰的是,Unity 已经让我们轻而易举地记录我们的动作:我们只需要设置一个标志。困难的部分将是善于驾驶球。
注意
在这里很重要的是你尽力玩好游戏。代理将直接从你学习,所以如果你表现不好,代理也会表现不好。ML-Apple离You-Tree不远。
-
在层次视图中选择球。
-
在检视器中找到演示记录器组件。
-
将记录开关设为打开状态。我们准备开始录制。
-
播放场景。
-
使用键盘驱动小球,并确保多次捡起硬币。
-
当你对场景满意时停止它。我们建议尝试大约 20 次捡起硬币。
一旦完成,你应该在Assets目录下看到一个名为Demos的新文件夹。在该文件夹内,你应该能看到一个名为RollerDemo的文件。如果你在 Unity 编辑器的检视器中选择它,它将告诉你相关信息,比如记录了什么样的动作和观察结果,如图 6-9 所示。

图 6-9. 检查我们的演示录制
如果进行多次训练运行,你将看到多个演示文件,它们的命名方式是数字递增的,比如RollerDemo_0、RollerDemo_1等。这些将是我们的训练数据。
配置训练
完成了我们的训练数据编写后,现在需要创建我们的 YAML 配置文件。每个人都喜欢 YAML。
本文件的大部分内容将基于第二章,所以我们不会在这里详细讨论。不过,我们会讨论与 IL 特定部分相关的内容:
-
在项目根目录创建一个 config 文件夹。
-
在该文件夹内,创建一个新的文本文件。
-
将其命名为RollerBallIL.yaml。
-
将以下 YAML 添加到该文件中:
behaviors: RollerBall: trainer_type: ppo hyperparameters: batch_size: 10 buffer_size: 100 learning_rate: 3.0e-4 beta: 5.0e-4 epsilon: 0.2 lambd: 0.99 num_epoch: 3 learning_rate_schedule: linear network_settings: normalize: false hidden_units: 128 num_layers: 2 reward_signals: extrinsic: gamma: 0.99 strength: 1.0 max_steps: 500000 time_horizon: 64 summary_freq: 10000 # behavior cloning behavioral_cloning: demo_path: ./Assets/Demos/RollerDemo.demo strength: 0.5 steps: 150000 batch_size: 512 num_epoch: 3
大部分内容与我们之前创建的 YAML 文件相同,主要区别在于behavioral_cloning部分。这些是我们正在使用的行为克隆(或 BC)技术的具体参数。
目前它们都非常通用,因为我们的模拟非常简单。然而,特别感兴趣的是strength设置,它控制 BC 相对于正常的 PPO 训练的学习率。实质上,strength表示 BC 对训练的影响和控制程度;如果设置得太高,可能会导致过拟合,但如果设置得太低,则学习不足。
与所有配置参数一样,了解它们的影响最好的方法是改变它们并观察对训练的影响。
另一个有趣的属性是demo_path。它指向我们之前制作的演示录制。如果你改变了你的演示名称,或者想要使用不同于第一次运行的演示,确保修改demo_path变量以匹配。根据你的系统,可能需要给它一个绝对路径(例如/Volumes/Work/Sims/IL/RollerDemo.demo)。
小贴士
在 YAML 文件中很容易打错这些参数。直接从我们的代码中复制它们可能会更容易一些,我们的代码在GitHub上。
开始训练
差不多可以进行机器学习的机器部分了,终于。但首先,我们必须在 Unity 编辑器中进行一些设置:
-
在 Hierarchy 中选择球体。
-
在检视器中找到行为参数组件。
-
将行为类型设置为默认。
-
在检视器中找到演示录制器组件。
-
关闭记录设置。现在我们的代理已经可以由 Python 控制。
-
打开终端。
-
导航至 Unity 项目根目录。
-
运行以下命令:
mlagents-learn config/rollerball_config.yaml --run-id=RollerBall过了一会儿,你应该看到类似于图 6-10 的东西,这意味着我们可以在 Unity 中开始工作了。
![psml 0610]()
图 6-10。Python ML-Agents 已经准备好进行训练。
-
返回 Unity。
-
播放场景。
你应该看到球独自迅速移动,试图学习你的驾驶技能。
-
去拿一杯很高的咖啡,或者可能读一会儿书。没关系,我们会等你的。
一旦这个过程结束,我们的训练就完成了,我们可以测试它了。
小贴士
如果你需要一个预训练的机器学习模型,请查看我们在GitHub上制作的模型。
使用我们训练好的模型运行。
是时候让我们的 IL 训练模型尝试一下了。
首先,像往常一样,我们需要找到 ML-Agents 为我们创建的神经网络:默认情况下,它将位于项目根目录中的 results 文件夹中。在里面会有一个名为Roller Ball的文件夹,里面有一个名为Rolling Ball.onnx的文件,这是我们训练好的神经网络。
按照以下步骤使用训练好的模型运行球体代理:
-
将.onnx文件拖放到 Unity 的资源面板中。
-
在 Hierarchy 中选择球体。
-
在检视器中找到行为参数组件。
-
将.onnx文件添加到模型槽中。
-
将行为类型设置为仅推断。
-
在 Unity 中播放场景。
现在你应该看到球在四处滚动,收集硬币,并且如果你像我们一样开车,经常会从边缘掉下去。希望你比我们更擅长教球。
恭喜你,使用模仿学习训练了一个代理!
理解和使用模仿学习。
模仿学习对于使代理像人类一样行为是有用的(在某种程度上);然而,在实践中,它更可能作为多阶段、多技术训练过程的一部分,帮助代理尤其是在早期训练中前进。
小贴士
如需进一步了解多种可能的技术,请参考“技术”章节。
当你考虑进行模拟时,你试图创建一个高效的模拟:你希望早期的训练尽可能成功。
在强化学习中,代理在获得第一个奖励之前完全不知道自己在做什么,甚至稍微的。IL 允许你快速跳过这一过程,使用人类来展示一个“良好的行为”开始,然后继续使用 IL 或切换到 RL 进行训练,尽可能快速地完成困难的早期训练。
在早期训练之后,您可以继续使用 IL 生成具有更有机、“类人”的行为的代理(在您模拟的背景下,这意味着什么),或者转向另一种技术,如前述的 RL,以快速生成新的经验并改进演示的人类行为。
例如,我们在第五章中创建的汽车,最初使用 RL 训练,可以改为首先使用 IL 进行初始训练阶段,其中使用人类驾驶赛道的演示来指导其驾驶行为,然后进行第二个训练阶段——几乎与我们在第五章实际使用的相同——在此基础上使用 RL。这种方法很可能会大大缩短总体训练时间,并使汽车在驾驶课程时显得稍微更具人性化。
第七章:高级模仿学习
在本章中,我们将探讨使用生成对抗仿真学习(GAIL)的模仿学习(IL)。我们可以像我们使用行为克隆(BC)时那样几乎相同地使用 GAIL,但这不会展示给您任何新内容,除了更改配置 YAML 文件外。
到目前为止,通过我们的模拟,我们已经完成了基础工作,建立在此基础上,并使用强化学习创建了一个简单的自动驾驶汽车。在上一章中,我们使用了 IL 来训练一个代理人,使用人类行为。我们用于行为克隆的 IL 试图最大化其与我们提供的训练数据的相似性。
IL 并不是我们能够使用的唯一的 BC 技术。这一次,我们将使用 GAIL。GAIL 可以帮助改善我们代理的训练,使其基本上跳过学习过程中的早期障碍,并让它专注于从那时起的改进。
提示
BC 和 GAIL 也可以结合使用,以便您可以希望提取两者的优点并减少任何一者的弱点。在本章末尾,我们将介绍如何结合 GAIL 和 BC,但目前的重点将放在 GAIL 上。
遇见 GAIL
在我们开始使用 Unity 和 ML-Agents 进行基于 GAIL 的活动之前,我们将解析一些使 GAIL 运行的要点。
GAIL 正如其名称所示,是一种对抗性的模仿学习方法,基于一种称为GAN的机器学习网络:生成对抗网络。GAN 有效地将两个训练好的模型,称为鉴别器和生成器,相互对抗。鉴别器模型评估生成器复制所需的训练数据或行为的能力,来自鉴别器的反馈被生成器用来指导和希望改进其行动。
这些行动和行为然后被反馈给鉴别器,以便它更多地了解场景。鉴别器根据生成器采取的行动和提供的演示学习场景的规则和奖励。
提示
GAIL 是一种比 BC 更新得多的模仿学习方法,但这并不一定意味着它更好;它只是不同而已。机器学习作为一个领域处于不断变化之中。
然后自然出现的问题是我应该何时使用 GAIL,何时使用 BC?和大多数机器学习中的事物一样,答案并不简单。一般来说,选择使用哪种更多地取决于您打算使用的场景。令人困惑的是,您还可以将它们结合起来(通常)比单独使用它们效果更好。
注意
学术研究围绕 GAIL 经常讨论逆强化学习和无模型学习,以及其他听起来非常花哨的术语。
这基本意味着 GAIL 没有对世界的固有理解;它必须找出场景的规则和最大化场景奖励的行动。
因此,当它被抛入深水区并且几乎没有帮助的情况下,它表现得相当不错。
如果您拥有大量覆盖环境中可能变化的人类生成的训练数据,则 BC 与 IL 结合的效果通常会优于 GAIL。
如果您只有少量的人类生成数据,GAIL 将能够更好地推断出最佳方法。在与由人类定义的外部奖励结合时(使用 ML-Agents 中的AddReward函数),GAIL 的表现也往往优于 BC。
在使用强化学习时,为模拟设置正确的奖励结构通常非常棘手。使用 GAIL 可以帮助解决这个问题,因为它在不知道具体场景的情况下运作,并且在某种程度上试图弄清楚您的期望。它通过依赖示范数据中包含的信息来实现这一点。在设计良好的奖励结构困难的复杂场景中,即使您不能本质上解释您的行为为何有效,您也可以使用 GAIL 来基于您的行为模式工作出解决方案。
GAIL 比 BC 与 IL 更加灵活,但这并不是我们在这里使用它的原因;我们之所以使用它,是因为 GAIL 在与外部奖励结合时效果更好。当您只提供部分示范信息时,GAIL 的表现比 BC 与 IL 更好。
提示
本质上,在 GAIL 内部有两只“狼”:第一只“狼”致力于更好地理解它所处的世界,而第二只“狼”则执行希望能够取悦第一只“狼”的行动。
按我说的去做和做
有一句古老的谚语,“说到做到”,每当训练 ML-Agent 代理时我就会想起它。
我们基本上只设置了一些奖励,然后告诉代理从那里开始解决问题。如果我们在孩子成长过程中这样做,那会被认为是一种非常糟糕的知识传授方式,所以我们倾向于展示他们如何做几次,然后给他们规则并让他们在此基础上改进。
几乎任何时候,作为人类,当您接受训练时,通常会在您独立操作之前展示正确的操作方式几次。
这就是我们在这里尝试复现的内容;我们希望使用 GAIL 来启动我们代理的训练。我们希望展示正确的方法几次,然后让它从那时开始找出最佳方法。
一个 GAIL 场景
对于这种情况,我们将使用类似于我们之前在第六章中使用 IL 进行 BC 训练的问题和环境。我们的活动涉及一个环境,具有以下特征:
-
一个目标区域
-
一个充当代理的球,需要移动到目标位置
它看起来像图 7-1。

图 7-1. 我们的 IL 环境,在修改为 GAIL 之前
如果球掉出世界,这将以失败结束本集;如果球达到目标,这将以成功结束本集。
对于我们与 GAIL 的活动,我们将使用同一个环境,并进行小幅添加:
-
代理需要先触摸“key”,然后才能解锁目标。
-
在没有触摸到“key”之前触摸目标将不会有任何效果。
在这一点上,您可以复制您为第六章创建的 Unity 项目,或者直接进行修改。我们选择在项目内复制场景,因此我们打开 Unity 编辑器并执行以下操作:
-
如图 7-2 所示,在项目窗格中选择场景。
![psml 0702]()
图 7-2. 在项目窗格中选择场景
-
选择“编辑”菜单 → 复制,如图 7-3 所示。
![psml 0703]()
图 7-3. 选择复制
-
将复制的场景重命名为“GAIL”或类似名称。
确保新场景已经打开并准备就绪。然后是添加 key 的时间:
-
在项目层次结构中添加一个新的立方体。
-
将此立方体重命名为“key”。
立方体将部分嵌入在地面中,但目前这没有问题。接下来的步骤是修改代理和代理的脚本。
注意
如果您还没有完成第六章,我们强烈建议您在尝试这之前完成。
修改代理的动作
我们当前的代理仅在我们为其录制的演示中使用训练数据——否则没有设置奖励结构。
对于 BC with IL 来说,没有奖励是很好的,但对于我们在 GAIL 活动中所追求的不是这样。我们希望代理使用训练数据来帮助开始学习,然后从奖励中获得值,作为代理优化的组成部分。
注意
因为我们在与基于 BC 的 IL 相同的项目中工作,所以我们将直接修改roller agent类(这将影响我们在上一章中创建的场景的功能),但如果您希望保持原样,可以复制该文件或创建一个新的 C# 文件作为新的代理。
只需记住将其连接到场景中的代理并移除旧代理。
-
打开 Roller.cs。
-
在类中添加以下成员变量:
public Transform key; private bool hasKey = false;这两个变量中的第一个变量
key将被用于在场景中引用key对象,第二个变量将用于判断我们是否已经拿起了key。现在我们可以在
key本身上使用一些特定于 GameObject 的信息,以了解它是否被击中,而不是让另一个变量在那里浪费空间,但这并没有节省多少,以至于不值得被打扰。 -
用以下代码替换
OnActionReceived方法:var continuousActions = actions.ContinuousActions; Vector3 control = Vector3.zero; control.x = continuousActions[0]; control.z = continuousActions[1]; body.AddForce(control * speed); if (transform.position.y < 0.4f) { AddReward(-1f); EndEpisode(); } var keyDistance = Vector3.Distance(transform.position, key.position); if (keyDistance < 1.2f) { hasKey = true; key.gameObject.SetActive(false); } if (hasKey) { if (Vector3.Distance(transform.position, goal.position) < 1.2f) { AddReward(1f); EndEpisode(); } }
这部分的工作方式与我们之前的代码类似:它基于移动动作值施加力。如果代理人滚出平面边缘,我们仍然会重置环境,但现在我们会因此惩罚它。
接下来,我们要判断是否触碰到了关键物体。如果我们触碰到了它,我们会使关键物体失效(因此它不再出现在场景中),并标记关键物体已找到。
最后,我们做了类似的事情,但是针对的是目标而不是关键物体;如果我们有了关键物体,我们会给予奖励并结束这一集。
我们在这里使用一个距离值,1.2 单位,来判断我们是否足够接近。我们选择这个数字是因为它比单位球体与单位立方体之间的中心距离要稍微大一点。我们这样做是因为这段代码简单明了,很好地展示了这个概念。然而,它并不完美:我们粗略地在代理周围画了一个半径为 0.6 的球体,并查看是否有东西在里面。
注意
Unity 有一个内置方法可以做到这一点:Physics.OverlapSphere,它允许你定义一个中心点和半径,并查看在这个想象的球体内有哪些collider。我们没有使用这种方法,因为它看起来有点笨拙,为了正确地确定你击中了什么,你应该使用标签,这需要我们设置起来。因此,我们保持简单,进行距离检查,但是内置方法在让你定义碰撞层蒙版方面有很大的灵活性,如果我们有一个更复杂的示例,那就是我们要做的。
如果你感兴趣,这里是一个OverlapSphere调用的基础。弄清楚你击中了什么或者将碰撞过滤到只有相关的碰撞是一个练习,正如他们所说,留给读者自己去做:
var colliders = Physics.OverlapSphere(transform.position,
0.5f);
foreach(var collider in colliders)
{
Debug.Log($"Hit {collider.gameObject.name}");
}
那些是我们修改后的动作;现在是关于观察结果的内容。别忘了保存你的代码。
修改观察结果
现在对观察结果的处理可能已经非常熟悉了,我们将通过将观察结果传递给 ML-Agents 的CollectObservations()函数来完成所有操作。与 IL 版本相比,对观察结果的核心更改是添加关于关键信息和关键状态的信息。
在你的代码打开时,用以下代码替换CollectObservations方法:
sensor.AddObservation(body.velocity.x);
sensor.AddObservation(body.velocity.z);
Vector3 goalHeading = goal.position - transform.position;
var goalDirection = goalHeading / goalHeading.magnitude;
sensor.AddObservation(goalDirection.x);
sensor.AddObservation(goalDirection.z);
sensor.AddObservation(hasKey);
if (hasKey)
{
sensor.AddObservation(0);
sensor.AddObservation(0);
}
else
{
Vector3 keyHeading = key.position - this.transform.position;
var keyDirection = keyHeading / keyHeading.magnitude;
sensor.AddObservation(keyDirection.x);
sensor.AddObservation(keyDirection.z);
}
从概念上讲,这与以前并没有太大的不同;我们只是跟踪更多的事物。
我们仍然拥有我们的速度和朝向目标的方向,但我们正在添加一个新的观察值来表明我们是否有关键物体,以及朝向关键物体的方向。如果关键物体已被捡起,我们就不再计算它的方向;我们只发送零,这几乎等同于不发送观察结果。
我们这样做的原因是因为我们必须每次发送相同数量的观察结果。
所有这些代码都改变了发送给代理的观察数量,与以前的基于 IL 的版本相比。我们很快会修复这个问题。
重置代理
对于我们最后的一段代码,我们需要更新OnEpisodeBegin()函数以适当地重置一切。具体来说,我们现在需要重置关键状态和状态。
替换OnEpisodeBegin方法体的代码如下:
body.angularVelocity = Vector3.zero;
body.velocity = Vector3.zero;
transform.position = new Vector3(0, 0.5f, 0);
transform.rotation = Quaternion.identity;
hasKey = false;
key.gameObject.SetActive(true);
var keyPos = UnityEngine.Random.insideUnitCircle * 3.5f;
key.position = new Vector3(keyPos.x, 0.5f, keyPos.y);
var goalPos = UnityEngine.Random.insideUnitCircle * 3.5f;
goal.position = new Vector3(goalPos.x, 0.5f, goalPos.y);
与观察一样,这与以前并没有太大不同:我们仍然将代理重置到中心并移除其所有力量,并且我们仍然选择一个随机点并将目标移动到那里。然而,我们还标记了我们没有钥匙,确保钥匙游戏对象在场景中处于活动状态,并最终将其移动到一个随机位置。
通过这个改变,我们的代码就完成了。我们不需要触及启发式代码,因为那里的内容没有改变。在返回 Unity 编辑器之前别忘了保存。
更新代理属性
我们的代理代码发生了相当大的变化,因此检查器中设置的许多组件值对于该代理不再正确;让我们来修复这个问题。在 Unity 编辑器中,打开您的场景:
-
在层次结构中选择代理。
-
在检查器中找到代理组件。
-
将钥匙游戏对象从层次结构拖放到检查器中的键字段中。
-
在检查器中找到行为参数组件。
-
将观察的空间大小设置为 7。
有了这些,我们的代理现在已经正确编码和配置。接下来让我们给它一些训练数据。
演示时间
这个略微修改过的世界不再与我们之前使用的相同,所以我们应该为代理创建一些新的演示数据:
-
在层次结构中选择代理。
-
在检查器中找到行为组件。
-
将类型从默认更改为启发式。
-
在启发式记录器组件中,将其设置为记录。
-
播放场景。
-
尽力记录一些演示数据。
注意
您可能会想知道为什么我们要记录新的演示数据,考虑到我们在使用 BC 时已经这样做了。我们这样做是因为这些数据没有奖励作为其一部分,这意味着 GAIL 将无法将动作与奖励关联起来。如果我们使用旧数据,我们将会在没有外部奖励的情况下训练 GAIL。这样做是有效的,但不是本章的重点,很可能不会给您想要的结果。
一旦您觉得已经记录足够的数据,请停止场景。现在您应该有一些数据可以用来输入到 GAIL 中。
提示
如果您选择了创建的演示文件,在 Unity 检查器中可以看到我们获得的平均奖励。它还显示一些其他信息,但在这里我们关心的主要是平均奖励。如果太低,这可能不是一个特别好的演示文件。
接下来是训练。
使用 GAIL 进行训练
如果你猜想,“我只需在 YAML 文件中设置一些奇怪的设置来启用 GAIL 吗?” 那么你是正确的,所以我们再次迎来令人兴奋的一轮让我们在 YAML 文件中编辑一些魔法数字。对于 GAIL 来说,我们想要调整的相关部分都是奖励设置的一部分。
对于这种情况,我们将使用早期使用 BC 时使用的相同训练配置文件,但我们会做一些更改。首先,我们需要创建一个新的配置文件:
-
复制 rollerball_config.yaml 并将其命名为 rollerball_gail_config.yaml。
接下来,您需要移除配置中与 BC 相关的部分。
-
删除
behavioral_cloning行以及其下和缩进的所有行。最后,我们希望添加 GAIL。
-
在
reward_signals部分下,添加一个新的 GAIL 部分:gail: strength: 0.01 demo_path: ./Assets/Demos/RollerDemoGail.demo
我们可以调整几个不同的 GAIL 参数;在这里,我们只设置了两个,而且只有一个是必需的。
必需的是 demo_path,它指向我们刚刚创建的演示文件。我们还设置了 strength,其默认值为 1,我们将其设置得远低于默认值,因为 strength 被 GAIL 用来调整奖励信号的比例。
我们将其设置得很低,因为我们的演示数据不太理想,并且计划外部奖励信号是确定采取何种行动的主要指标。如果我们给它一个更强的信号,它会更像我们的演示文件学习,而不是像场景的最佳玩法。
我们还可以在这里配置 GAIL 的其他设置(但保持它们的默认值),包括鉴别器的大小、学习率和伽马等。
我们这里不需要任何这些设置,所以我们将它们保持在默认设置,但如果您对它们感兴趣,官方文档中对它们都有描述,如果 GAIL 不按照您希望的方式工作。
警告
由于 GAIL 的设计方式,它有引入各种偏见到代理中的习惯;也就是说,即使这与场景目标直接冲突,它经常试图延长情节长度。
由于这个原因,在训练过程中,如果您发现您的代理基本上只是闲逛而不完成手头的任务,很可能需要降低 GAIL 奖励信号,以防止其压倒外部奖励。
完成后,完成的 YAML 文件应该如下所示:
behaviors:
rolleragent_gail:
trainer_type: ppo
hyperparameters:
batch_size: 10
buffer_size: 100
learning_rate: 3.0e-4
beta: 5.0e-4
epsilon: 0.2
lambd: 0.99
num_epoch: 3
learning_rate_schedule: linear
network_settings:
normalize: false
hidden_units: 128
num_layers: 2
reward_signals:
extrinsic:
gamma: 0.99
strength: 1.0
gail:
strength: 0.01
demo_path: ./Assets/Demos/RollerDemoGail.demo
use_actions: true
max_steps: 500000
time_horizon: 64
summary_freq: 10000
配置文件配置完成后,现在是开始实际训练的时候了:
-
在 Unity 中选择代理程序。
-
在检视器中,在行为参数组件内,将行为类型更改为默认。
-
在检视器中,在演示录制器中,取消选中记录框。
-
在命令行中,运行以下命令:
mlagents-learn config/rolleragent_gail_config.yaml --run-id=rolleragent_gail -
一旦开始,返回 Unity 并按 Play 按钮。
代理现在应该正在训练中。快速(或不那么快)地喝杯咖啡,让我们在训练完成后再继续。
运行及其后续
一旦我们的训练完成,我们可以像之前一样运行它:
-
将训练好的 .onnx 模型文件添加到 Unity 中。
-
在 Unity 中选择代理。
-
在 Inspector 中,在行为参数组件内,将行为类型更改为仅推理。
-
将模型文件拖放到模型槽中。
-
点击播放,坐下来享受观看你的代理在周围滚动,捡起方块。
对于我们的代理来说,在训练了 500,000 次迭代后,它的平均奖励分数达到了 0.99,这几乎是完美的表现。
与我们的演示文件相比,其平均奖励为 0.95,所以代理已经超越了我们,这正是我们预期的:学生已经成为了大师。
现在我们已经介绍了将 GAIL 与外部奖励因素结合的基础知识,但在我们继续其他主题和章节之前,现在是时候谈谈如何结合 GAIL 了。在这个例子中,我们将 GAIL 与外部奖励结合在一起,但我们也可以将其与模仿学习和行为克隆结合起来。为此,我们只需将 BC 配置元素添加回 YAML 配置文件中即可。
然而,诀窍在于平衡外部奖励、GAIL 和 BC 奖励的相对强度值。
对于这种情况,我们尝试了各种不同的三个值,调整了其他配置设置,甚至尝试将 BC 限制在训练的前半部分,但我们并没有看到训练有任何显著改善。在某些情况下,当试图最佳地融合各种元素时,我们最终得到的代理表现非常糟糕,其平均奖励为 -0.4,这意味着大部分时间它只是干脆从地面边缘掉下去;我们的 GAIL 或仅 BC 都表现出色。
或许在这种情况下,这些调整并没有提供足够的价值,或者也许我们只是还没有找到合适的值让一切顺利运行。
Unity 在其 金字塔示例 中发现,当结合不同的技术进行训练时,与单独使用任何其他方法相比,代理训练速度更快且效果更好。
结合不同方法确实有其合理之处;毕竟,这与我们学习的方式并没有太大不同。我们尝试结合许多不同的技术以获得尽可能好的结果,那么为什么代理就应该有所不同呢?模仿学习有着巨大的潜力,并且因为它相对容易添加到你的训练中,所以非常值得一试。
第八章:引入课程学习
回想一下你在学校的头几天。那是多么奇怪的时光……老师站在班级前面向你们展示一个二次方程,要求你们解决它。
“x的值是多少?”你发现自己被问到。
困惑的是,你不知道发生了什么;毕竟,这是你的第一天。
但你仍然要猜测:“三。”老师瞪视着你然后宣布你非常错误。你被送回家。
第二天这种情况重复。老师再次给你一个二次方程;你再次失败并被送回家。日复一日,这种情况发生:你出现并得到一个方程式,你猜测,得到一个异常错误的答案,然后被送回家。
有一天你猜测,老师说:“错了,但接近了。”
终于,有些进展了。
你仍然被送回家。
第二天这种情况重复,再次重复,每次你都更接近猜测。每次你被送回家,每次你第二天再次出现并猜测。
最终你开始把它们拼在一起,你开始理解构成方程式的个别部分,它们相互作用的方式,它们如何影响x的值。这次当被问及时,情况有所不同。你回答说,“x等于-1 加或减根号 2”,并且你对你的答案感到自信。你的老师慢慢点头。“正确。”
你现在已经在学校待了 600 年,但是最终你知道如何解决一个二次方程。你被送回家。
当然,这听起来像是一种糟糕的学习方式和一种极其残酷的教学方式,然而这就是我们要求 ML 工作的方式。
实际上人们是按阶段教授的。
我们从需要解决更复杂问题的基础开始,一旦掌握了它们,我们就转向更困难的问题和更复杂的信息。我们像金字塔一样建立在我们以前的知识之上,一层一层地添加,直到我们能够解决我们关心的实际问题。
我们以这种方式教导人们是因为已经证明这种方法有效;事实证明,仅仅让孩子们解决二次方程并不是非常有效的,但是教授数字的基础知识,然后是数学能力,接着是代数和公式,这样有一天你可以让他们解决一个二次方程并且他们能够做到。各种文化在所有知识领域中以各种形式采取这种方法。
机器学习中的课程学习(CL)提出了这样一个问题:“如果它对人类有效,那么它对 ML 也有效吗?”在本章中,我们将看看如何使用课程学习通过分阶段地构建来解决问题。
机器学习中的课程学习
使用课程学习(Curriculum Learning)在你的 ML 模型中的主要原因与人类使用它的原因相同:通常在移动到任务的更高级部分之前,掌握某事物的基础知识会更容易。
注意
当课程学习成功时,它是非常成功的。例如,在 Unity 中,当教导一个代理人去达到一个目标(比如学习如何越过篱笆)时,课程学习模型学习速度和效果都比传统模型更快更好。其他与模拟相关的领域也显示了类似的有希望的结果。然而,这并不意味着答案总是“只使用课程学习”。
像许多机器学习中的事情一样,知道何时使用课程学习并不是一成不变的。如果您试图解决的问题具有明确定义的难度元素,或者任务本身具有明显的阶段,那么它可能是课程学习的一个极好候选者。不幸的是,在尝试并查看之前,您实际上无法确定 CL 是否合适。
例如,假设您想训练一个代理人去追逐一个会四处移动的目标;也许您正在制作一只追逐松鼠的狗的模拟。
注意
我们,作者,来自澳大利亚,我们这里没有松鼠,所以我们假设狗追赶它们是完全无害的原因。此外,这些松鼠不能爬树;毕竟,我们从未见过有松鼠这么做过!
我们可以让狗对抗一个在空间中自己移动的松鼠,并让代理人自己想出解决方法,或者我们可以使用课程学习。
要开始我们的课程学习狗代理,我们将从让我们的狗朝一个恰好超过两米宽的松鼠移动开始。
注意
再次强调,我们这里没有松鼠,所以我们很确定它们可以长到这么大。
此外,松鼠不会移动,这对我们的狗来说更容易。然后,松鼠可能开始缩小,迫使狗更精确地移动以达到它。
一旦狗掌握了达到一个静止的松鼠大小的松鼠,我们可以开始移动目标。我们甚至可以让松鼠的速度从非常慢逐渐加快到松鼠速度,最终达到超松鼠的速度。
因此,我们这里的课程是先教我们的代理人移动,然后是追随,然后是追随更快和更快的目标,基本上教它如何追逐。¹
通常,课程学习被展示为在非常复杂的场景中使用,使其几乎感觉像是一种魔法弹药。一些它用于的问题如果没有课程学习的魔法棒可能几乎不可能实现,但要记住的是,从根本上说,它是一种改进训练的手段,而不是做不可能的事情。一般来说,任何您可以用课程学习解决的问题,都可以不用它解决,只是通常需要更长的时间。
结果表明,仅仅通过增加计算能力来解决问题,虽然有点不够优雅,但确实有效。在我们看来,课程学习在最佳状态下时,是用来加快或改善你的代理训练的,而在这个角色中,它很可能是模型训练的未来。
课程学习场景
让我们使用课程学习创建并解决一个问题。我们要解决的问题是教代理如何向目标扔球。
虽然作为人类,我们天生擅长扔球,但这实际上是一个非常复杂的任务。如果你想要击中目标,你必须考虑距离、投掷力量、角度以及弹道弧线。
代理始终会从房间中心开始,但目标会随机散布在空间中。在投掷球之前,代理将不得不确定要多大力量扔球,以及垂直角度和投掷方向。
这引出了一个问题,即我们的课程将是什么,以及我们如何增加这种情景的难度。
像所有强化学习方法一样,我们将有一个奖励结构,通过给予接近未中的小奖励来鼓励代理改进。
这种奖励结构将是我们课程的基础。我们将从一个非常大的半径开始,这被视为“接近未中”,随着时间的推移,该半径会缩小,从而鼓励代理变得更加准确以继续获得奖励。就像我们之前的例子(松鼠从一个极大的尺寸开始,然后随着代理理解其意图而不断缩小)一样,这里也是一样的。
因此,我们的课程将分为几个难度逐渐增加的课程,其中代理必须成功地将球扔到靠近目标的距离缩小。
我们可以根据需要设定多少难度级别,但我们的课程的核心方法每次都将是相同的。
在 Unity 中构建
让我们从在 Unity 中构建环境开始。完成的环境将类似于图 8-1。
与我们在本书中创建的所有其他例子不同,我们将在模拟侧面做一些略有不同的事情。
我们不会扔出物体然后等待 Unity 物理引擎移动它。相反,我们会即时计算着陆点并使用该计算点。

图 8-1. 我们场景中的环境,准备进行课程学习
一旦模型训练完毕,我们将使物体四处飞溅,因为这看起来很酷,但不是为了训练。我们不需要以我们能看到的方式扔物体,因为我们可以计算它是否会击中我们的目标。在训练期间模拟实际抛掷的视觉组件会不必要地减慢速度,我们不需要这样做。一旦训练好模型,我们可以添加适当的视觉效果,映射到底层发生的事情。
这样做的原因是,否则我们必须在我们的代理中添加记忆,以便它学会将它采取的投掷动作与稍后得到的奖励联系起来。因此,我们可以进行一些测试,并连接启发式(用于手动人类控制、测试,如以前所做的那样),我们将可视化抛物线和投掷结束点。
注意
ML-Agents 框架确实支持为您的代理添加记忆,但因为显著增加了复杂性和训练时间,所以我们尽可能地避免使用它。
弹道数学是众所周知的,因此我们不必真正进行步骤;相反,我们可以进行数学建模。
当我们稍后使我们的代理真正投掷游戏对象时,它们将准确地落在数学上说它们将落在的地方。在这种情况下不可能的情况下,将需要记忆,但在这里不需要。
创建一个新的 Unity 项目,添加 Unity ML-Agents 包,并准备一个空场景继续。我们的项目名叫“CurriculumLearning”,相当有创意。
创建地面
首先,我们需要一些地面;对于这个环境,我们将创建一个单一的平面:
-
在层次结构中创建一个新的平面。
-
使用检查器将其命名为“地面”,将其位置设置为
(0, 0, 0),并将其比例设置为(20, 1, 20)。
创建好我们的地面后,我们将给它涂上颜色(通常情况下),这样可以更容易地在视觉上区分模拟中的不同部分(对于我们,人类来说):
-
创建一个新的材料,并给它取名为“GroundGrass_Mat”。
-
使用检查器将材料的反射率颜色设置为漂亮的绿色。
-
从项目面板将材料拖动到层次结构或场景本身的地平面上。
现在我们的地面有了材料,帮助我们区分一切。完成后,我们将继续制作我们的目标。不要忘记保存你的场景。
创建目标
在这个场景的这一部分中,我们的目标将只是一个简单的立方体。在训练期间,它将用于提取一个位置。但是,在推理期间,它将被我们的代理射击并根据物理法则四处飞溅,所以我们需要设置目标来涵盖这两种情况。
注意
“推理阶段”是指您正在使用经过训练的模型时。您的代理从模型推断出动作,因此是推理阶段。
-
在层次结构中创建一个新的立方体。
-
使用检查器将其命名为“目标”,并将其位置设置为
(0, 0.5, 0)。 -
向其添加刚体组件。
当我们在推理模式下运行模拟时,我们的目标需要有一个刚体,因为我们希望它对被投掷的东西作出反应。这对训练没有影响,但我们现在需要设置它。
完成这些后,我们可以开始工作我们的代理。在继续之前保存场景。
代理
现在是构建我们的代理的时候了:我们的代理的基础非常简单,因为它将基于一个立方体(很像我们的目标)。真是个惊喜!
-
在层级中创建一个新的立方体。
-
将立方体命名为“Agent”,并将其位置设置为
(0, 0, 0)。这很简单,但在这里有可视化的东西是很有用的。然而,有一个问题:它有一个碰撞器,因为默认情况下立方体有一个。在后面推断时,这只会妨碍产生抛射物。
-
在检视器中,从代理中移除框碰撞器组件。
现在,我们为代理使用网格的整个原因是帮助我们可视化它正在做的事情,但不是所有弹道轨迹的方面都能轻松显示。
因为代理是一个立方体,我们可以看到它朝向的方向,但我们希望有另一个元素来显示它瞄准的俯仰角。
-
在层级中添加一个新的圆柱体。
-
将圆柱体拖到代理的下方(在层级中),使其成为代理的子对象。
-
使用检视器将圆柱体的位置设置为
(0, 1, 0),并将其旋转设置为(0.2, 1, 0.2)。 -
在检视器中,从圆柱体中移除碰撞器组件。
这给了我们一个小圆柱体放在箱子上方,我们可以用它来视觉上判断高度。
虽然这将以一种奇怪的方式关联并穿过盒子,但对于启发阶段来说没问题,对于训练或推理阶段,它对模拟没有影响。
大部分场景已完成。在继续之前保存它。
构建模拟
随着场景的基本结构准备就绪,是时候开始构建模拟部分了。具体来说,正如你现在希望的那样,我们将创建代理需要的动作、观察和奖励。
使代理成为代理
虽然我们可能已经在场景中准备好了代理,但它没有代码;至少,它将需要成为一个Agent子类。
-
创建一个新的 C#文件,并将其命名为Launcher.cs。
-
打开Launcher.cs并用以下内容替换它:
using System.Collections; using System.Collections.Generic; using UnityEngine; using Unity.MLAgents; using Unity.MLAgents.Actuators; using Unity.MLAgents.Sensors; public class Launcher : Agent { public float elevationChangeSpeed = 45f; [Range(0, 90)] public float elevation = 0f; public float powerChangeSpeed = 5f; public float powerMax = 20f; public float power = 10f; public float maxTurnSpeed = 90f; public float hitRadius = 1f; public float rewardRadius = 20f; public float firingThreshold = 0.9f; public Transform target; public Transform pitchCylinder; }
在上述代码中,我们声明了一些控制最小和最大值的变量,以及一些稍后用于调整奖励的额外值。最后,还有两个引用:一个用于目标,一个用于圆柱体。我们很快就需要这些元素,所以现在就处理它们吧。
这些数字变量每个与弹道抛掷的不同元素相关:
-
Elevation 是垂直抛掷角度;它被限制在 0 到 90 之间,Elevation Change Speed 是俯仰角速度。
-
力量是投掷时施加的力量大小,最大力量和力量变化速度限制了最大力量和代理可以加速和减速其力量的速度。
-
最大转向速度是代理能够旋转其面向的速度;我们不需要追踪面向本身,因为与仰角不同,我们只会向前投掷。
-
击中半径和奖励半径用于给予奖励,所以我们很快会详细讨论它们。
-
发射阈值 限制了代理允许投掷投射物的频率。
现在我们需要连接这些各种组件:
-
将 Launcher.cs 文件拖放到场景中的代理对象上。
-
将目标对象从层级拖放到检查器中的
Launcher组件的target字段中。 -
将圆柱体对象从层级中拖放到检查器中
Launcher组件的cylinder字段中。
最后,我们需要添加并配置代理的其他必要组件:
-
从层级中选择代理。
-
在检查器中,点击“添加组件”按钮,然后选择 ML Agents → 决策请求者。
-
在检查器中,点击“添加组件”按钮,然后选择 ML Agents → 行为参数。
-
在行为参数组件部分,将行为重命名为“Launcher”。
-
将向量观察空间大小设置为
9。 -
将连续动作的数量设置为
4。 -
在代理组件中,将最大步数设置为
2000。
现在我们的代理基础就绪,我们可以开始添加动作了。别忘了保存所有内容。
动作
我们的代理的动作非常简单。它将能够旋转其朝向(或偏航角),旋转其垂直瞄准方向(或俯仰角),增加或减少其投掷力量,并最终投掷或发射其投射物。
这意味着动作缓冲区将有四个值:偏航变化、俯仰变化、力量变化 和 是否开火的决定。
提示
在这里,我们将根据阈值 (firingThreshold) 限制发射动作的频率,从而将连续动作强制转换为离散动作。
离散动作 是代理可以执行的一种动作,要么发生,要么不发生,而 连续动作 是代理响应的可能值范围。
Unity 支持在单个代理中同时使用连续和离散动作;但是,在这里我们不这样做。
能够动态调整此值非常有用,这样我们可以通过调整释放投射物的频率来使模型更具侵略性。只需知道,您可以混合和匹配连续和离散动作。
在创建动作代码之前,我们需要创建几个辅助函数:
-
在
Launcher类中添加以下代码:public Vector3 LocalImpactPoint { get { var range = (power * power * Mathf.Sin(2.0f * elevation * Mathf.Deg2Rad)) / -Physics.gravity.y; return new Vector3(0, 0, range); } } public static Vector2 GetDisplacement (float gravity, float speed, float angle, float time) { float xDisp = speed * time * Mathf.Cos(2f * Mathf.Deg2Rad * angle); float yDisp = speed * time * Mathf.Sin(2f * Mathf.Deg2Rad * angle) - .5f * gravity * time * time; return new Vector2(xDisp, yDisp); }这些方法中的第一个,
LocalImpactPoint,将给出一个地面平面上的点,投射物一旦释放,将会着陆在这个点上。第二个方法GetDisplacement根据弹道弧线上的当前时间点返回投射物所在的具体空间点。因为弹道投射的物理学非常被理解,这两个函数将为我们提供与直接使用 Unity 物理仿真相同的确切结果,只是不必等待所有那些讨厌的时间过去。
注意
GetDisplacement方法是Freya Holmer 的轨迹类的稍作修改版本,可在 MIT 许可下使用。它是一组您可能会发现有用的数学函数库的一部分;有关详细信息和许可信息,请参阅存储库。我们只需要这一个函数,所以我们没有包括所有内容,但这是一个非常棒的代码库,您应该去看看。接下来,我们希望能够可视化弹道弧线,以便在测试系统时,我们可以检查其是否正常工作,并帮助我们启发式地测试系统:
-
将以下方法添加到
Launcher.cs类中:private void OnDrawGizmos() { var resolution = 100; var time = 10f; var increment = time / resolution; Gizmos.color = Color.yellow; for (int i = 0; i < resolution - 1; i++) { var t1 = increment * i; var t2 = increment * (i + 1); var displacement1 = Launcher.GetDisplacement (-Physics.gravity.y, power, elevation * Mathf.Deg2Rad, t1); var displacement2 = Launcher.GetDisplacement (-Physics.gravity.y, power, elevation * Mathf.Deg2Rad, t2); var linePoint1 = new Vector3(0, displacement1.y, displacement1.x); var linePoint2 = new Vector3(0, displacement2.y, displacement2.x); linePoint1 = transform.TransformPoint(linePoint1); linePoint2 = transform.TransformPoint(linePoint2); Gizmos.DrawLine(linePoint1, linePoint2); } var impactPoint = transform.TransformPoint(LocalImpactPoint); Gizmos.DrawSphere(impactPoint, 1.5f); }此方法将绘制出轨迹的弧线以及它将与地面平面相交的点处的小球。
这通过迭代想象的投掷过程实现,每次迭代后绘制新的线段,并最终在终点处仅绘制一个球体。
OnDrawGizmos方法是内置于 Unity 中的,并在每一帧调用。它可以绘制各种有用的调试和辅助信息(大多数辅助视觉,如场景视图中的翻译箭头,都是使用 gizmos 绘制的)。这些 gizmos 仅在场景视图中显示,不会在游戏或构建中显示。现在我们准备好扩展我们的动作。
-
将以下方法添加到
Launcher.cs类中:public override void OnActionReceived(ActionBuffers actions) { int i = 0; var turnChange = actions.ContinuousActions[i++]; var elevationChange = actions.ContinuousActions[i++]; var powerChange = actions.ContinuousActions[i++]; var shouldFire = actions.ContinuousActions[i++] > firingThreshold; transform.Rotate(0f, turnChange * maxTurnSpeed * Time.fixedDeltaTime, 0, Space.Self); elevation += elevationChange * elevationChangeSpeed * Time.fixedDeltaTime; elevation = Mathf.Clamp(elevation, 0f, 90); pitchCylinder.rotation = Quaternion.Euler(elevation, 0, 0); power += powerChange * powerChangeSpeed * Time.fixedDeltaTime; power = Mathf.Clamp(power, 0, powerMax); if (shouldFire) { var impactPoint = transform.TransformPoint(LocalImpactPoint); var impactDistanceToTarget = Vector3.Distance (impactPoint, target.position); var launcherDistanceToTarget = Vector3.Distance (transform.position, target.position); var reward = Mathf.Pow(1 - Mathf.Pow (Mathf.Clamp(impactDistanceToTarget, 0, rewardRadius) / rewardRadius, 2), 2); if (impactDistanceToTarget < hitRadius) { AddReward(10f); } AddReward(reward); EndEpisode(); } }在这种方法中有很多内容。首先我们从缓冲区中获取动作,然后检查释放动作是否超过其阈值。
然后,我们开始通过动作调整俯仰、偏航和抛出力。
真正的魔力发生在
if(shouldFire)部分。这是我们授予奖励的地方。首先,我们确定我们的抛射物距离目标有多远,然后根据该距离提供一个比例奖励。距离因子符合 sigmoid 形状。
提示
我们决定按照这种方式缩放奖励,因为在测试过程中,我们发现它比线性奖励效果更好。尽管线性仍然有效,但需要更长时间。很多强化学习都涉及这种试错。
接下来,如果我们在
hitRadius范围内,即直接击中箱子,我们给代理人一个非常大的奖励。最后,我们结束剧集。注意
因为代理人只有在释放投掷后结束剧集并获得奖励,有时可能需要一段时间才能获得任何分数。
你可以等待此过程,尝试调整抛出阈值,或取消训练并重新启动,希望其随机启动时会更愿意释放。
所有选项都可以使用:只需选择您最喜欢的那个。
观察
下一步是让我们的代理通过观察感知世界。我们之前提到会有九个观察值,所以现在让我们来创建它们。在这个活动中,我们将通过代码在一个重写的CollectObservations方法中提供所有观察值,并且我们不会在 Unity 编辑器中添加任何传感器。
将以下方法添加到Launcher.cs中:
public override void CollectObservations(VectorSensor sensor)
{
sensor.AddObservation(transform.InverseTransformDirection
(target.position - transform.position));
sensor.AddObservation(elevation);
sensor.AddObservation(power);
sensor.AddObservation(LocalImpactPoint);
sensor.AddObservation(Vector3.Distance
(transform.InverseTransformPoint(target.position), LocalImpactPoint));
}
我们添加的前三个观察值代表了与目标的朝向,投掷的仰角和投掷的力量。
接下来的两个是如果投掷,它将击中的位置和击中点相对目标的距离。
因此,如果我们假装代理是一只手臂,实质上我们告诉代理其手臂的设置及基于当前设置抛出的位置有多偏离。
尽管这看起来像是大量信息,但这与我们(作为人类)思考如何投掷物体并没有太大不同。我们非常擅长在投掷之前估计着陆点,并使用这些信息进行调整。
我们的代理只是得到了一点额外的帮助,因为他们的“估计”将是完美的。然而,作为人类,我们也可以估计旅行时间,甚至可以调整我们的投掷来击中一个移动的目标以弥补其运动—我们的代理不知道如何处理这一点。
人类的启发式控制
这是一个相当复杂的情景,所以我们希望先测试一下,以确保在我们的代理开始训练之前没有犯任何巨大的错误。
让我们添加一些启发式控制,以便我们可以快速测试场景基础知识。
提示
而且,这样做很有趣。
与之前的所有活动一样,我们这样做是为了让我们作为人类在测试期间控制事物,并不是为了任何将用于训练的东西(不像为 BC 记录演示):
-
将以下方法添加到
Launcher类中:private bool heuristicFired = false; public override void Heuristic(in ActionBuffers actionsOut) { var continuousActions = actionsOut.ContinuousActions; var input = new Vector3(); var keysToVectors = new (KeyCode, Vector3)[] { (KeyCode.A, new Vector3( 0, -1, 0)), (KeyCode.D, new Vector3( 0, 1, 0)), (KeyCode.W, new Vector3(-1, 0, 0)), (KeyCode.S, new Vector3( 1, 0, 0)), (KeyCode.Q, new Vector3( 0, 0, -1)), (KeyCode.E, new Vector3( 0, 0, 1)), }; foreach (var e in keysToVectors) { if (Input.GetKey(e.Item1)) { input += e.Item2; } } var turnChange = input.y; var elevationChange = input.x; var powerChange = input.z; int i = 0; continuousActions[i++] = turnChange; continuousActions[i++] = elevationChange; continuousActions[i++] = powerChange; if (Input.GetKey(KeyCode.Space)) { if (heuristicFired == false) { continuousActions[i++] = 1; heuristicFired = true; } else { continuousActions[i++] = 1; } continuousActions[i++] = 1; } else { heuristicFired = false; continuousActions[i++] = 0; } }这看起来比实际复杂得多。虽然这里有一大块代码,但它只是检测是否按住了指定的 QWEASD 键之一,如果是,则在动作缓冲区中增加相应的动作(W 和 S 用于俯仰,A 和 D 用于旋转,Q 和 E 用于力量)。
这使我们完全控制了朝向、投掷的角度和力量。
然后,如果按下空格键,我们还将发射阈值设置为
1,以确保代理会释放其投掷。让我们试一试,看看它如何运作。
-
在层次结构中选择代理。
-
在检视器中找到行为参数组件。
-
将行为类型属性从默认更改为启发式。
-
运行场景,尽力击中目标。
因为显然我们没有犯任何错误,也没有必要修复任何东西(对吧?),所以我们可以继续进行课程方面的工作。
创建课程
我们之前说过,课程将通过减小奖励半径的大小来增加难度,从而迫使代理人靠近目标以继续获得奖励。这意味着我们需要做几件事情:定义一个课程,确定映射到难度的值,并使环境重置基于课程值。让我们先从重置数值开始。
重置环境
我们实际上还没有做任何重置环境的工作。正如前面提到的,我们希望根据课程改变环境,但这并不是我们需要改变的全部。
在我们的模拟中有三个元素在起作用:代理、目标和奖励信号。
我们希望这三个元素在每次重置时都被修改,但只有其中一个会受到课程的影响。这意味着我们可以在不关心课程本身的情况下重置大部分环境。
将以下方法添加到 Launcher 类中:
public override void OnEpisodeBegin()
{
power = Random.Range(0, powerMax);
elevation = Random.Range(0f, 90f);
transform.eulerAngles = new Vector3(0, Random.Range(0, 360f), 0);
var spawn = Random.insideUnitCircle * 100f;
target.position = new Vector3(spawn.x, 0, spawn.y);
rewardRadius =
Academy.Instance.EnvironmentParameters.GetWithDefault
("rewardRadius", 25f);
}
这将在每次新的训练周期开始时由训练调用。现在它没有太多的代码,但它确实包含了所有与我们的课程学习相关的代码。
在这里,我们随机设置初始方向、仰角和投掷力量。然后我们在地平面上随机选取一个点并将目标移动到那里。最后,我们设置我们的 rewardRadius,这是决定我们必须接近目标才能获得任何奖励的元素。
对于课程学习来说,这最后一步是关键。rewardRadius 值将根据从 Academy 环境变量中获取的值进行设置,具体来说是从环境变量 rewardRadius 获取的值,我们尚未设置,但很快会设置。这里的环境变量已经设置了一个默认值,在我们的情况下是 25,如果找不到环境变量,则使用此值。
完成这些后,如果你按照目前的配置运行模拟并检查代理的 rewardRadius 值,你会发现它始终是 25,因为我们实际上还没有创建配置,所以它正在使用默认值。尽管如此,这是我们的重置工作完成了,所以现在我们可以继续创建课程的一面。
课程配置
现在我们的环境已经正确配置为使用课程提供的值来增加难度,但我们实际上还没有创建课程,让我们来修正一下这个问题。
要创建我们的课程,我们将使用 YAML 配置文件的一个我们尚未深入探讨的部分:环境参数。在这里,我们可以配置我们的课程以逐步增加场景的难度。
我们所有的课程本质上都归结为我们为训练添加到 YAML 文件中的额外一组值:
-
创建一个新的 YAML 文件,并将其命名为“launcher.yaml”。
-
将以下文本添加到 YAML 文件中:
behaviors: Launcher: trainer_type: ppo hyperparameters: batch_size: 2048 buffer_size: 20480 learning_rate: 3.0e-4 beta: 1.0e-2 epsilon: 0.2 lambd: 0.95 num_epoch: 3 learning_rate_schedule: linear network_settings: normalize: false hidden_units: 256 memory_size: 256 num_layers: 2 vis_encode_type: simple reward_signals: extrinsic: gamma: 0.995 strength: 1.0 keep_checkpoints: 5 max_steps: 10000000 time_horizon: 120 summary_freq: 10000这是一个基本上直接的设置,使用 PPO 来训练一个代理,如果我们不展示课程学习,那么这将是我们停止的地方。
现在,我们将添加适用于我们课程的相关部分。
-
将以下内容添加到 YAML 文件底部:
environment_parameters: rewardRadius: curriculum: - name: Lesson0 completion_criteria: measure: reward behavior: Launcher signal_smoothing: true min_lesson_length: 100 threshold: 1.0 require_reset: true value: 100在这里,我们声明了一个名为
rewardRadius的新环境参数,这与我们之前在代码中使用的相同,然后设置它以便由课程修改。目前我们的课程只有一节:我们将其命名为
Lesson0,但我们可以随意更改。我们稍后将添加更多内容,但现在让我们单独看看这一节。首先,我们有
name。正如我们之前提到的,我们不会明确地按名称引用它,但日志将会使用它,因此设定一个是值得的。接下来我们有两个不同的属性,
completion_criteria和value。completion_criteria负责处理何时结束当前课程并开始下一课。特别是,两个最重要的元素是
measure和min_lesson_length。measure可以是reward或progress。而不是从奖励信号中获取变化时机,progress使用的是步骤数与最大步骤数的比率。在我们的情况下,我们希望奖励本身成为我们进步的过程,因此我们正在使用它。
接下来,
min_lesson_length是一个控制,防止幸运的开端将代理置于尚未准备好的更难环境中。值为
100意味着代理必须在达到或超过threshold值的情况下执行至少 100 次迭代,然后下一课才会开始。最后,
value属性是我们可以控制环境参数实际值的地方。在我们的情况下,我们将其设置为
100的值,这使其在获取奖励的大初始区域内。提示
而不是单个值,您可以将
value设置为具有最小和最大值范围,并让课程从该范围内随机选择。您甚至可以配置它是在范围内均匀采样还是高斯采样。
现在,我们已经完成了第一课,是时候完成我们的课程了。
-
将以下内容添加到 YAML 文件的课程部分:
- name: Lesson1 completion_criteria: measure: reward behavior: Launcher signal_smoothing: true min_lesson_length: 100 threshold: 3.0 require_reset: true value: 75 - name: Lesson2 completion_criteria: measure: reward behavior: Launcher signal_smoothing: true min_lesson_length: 100 threshold: 6.0 require_reset: true value: 50 - name: Lesson3 value: 25我们在这里模型中添加了另外三节课,每节课基本上与第一节相同——只是重置时箱子距离的值已经增加。
注意
在这里设置的 YAML 文件中的环境参数不仅限于课程学习。Unity ML-Agents 还将它们用于任何环境随机化。它们只是 ML-Agents 可以访问并注入到模拟中的变量,您可以根据需要使用它们。
唯一的区别是在最后一课中,我们将奖励半径设置得非常小,并且没有完成标准,因为我们希望代理受到充分挑战。重要的是要提到课程中的所有课程都是列表的一部分,这就是为什么在 YAML 文件中命名的课程旁边有一个小短线(
-)。如果没有这个,课程将无法正常工作。提示
如果您计划或需要在课程中包含大量课程,可以将它们声明为数组,而不是像我们一样完全构建每个课程。我们只有几节课,所以像我们这样全部写出来是可以的。
现在我们的课程已经编写完成,我们终于可以开始训练我们的模型了。
注意
我们只修改了一个变量,即rewardRadius,但您可以修改尽可能多的变量,或者您可以使用它们来在环境中进行更根本性的改变,远远超出我们所做的范围。对于这种情况的另一个很好的选择可能是减小hitDistance的半径,以便完美命中需要更高的精度。我们试图保持简单,以便您可以看到如何使用课程学习,但无论您在课程中修改多少变量,其原理始终如一。正如在所有 ML 中一样,确定何时使用一种技术的正确时机通常更多地是“猜测和检查”。
训练
一切准备就绪,现在是时候开始我们的训练了:
-
在检查器中,在代理的行为参数组件中,将行为类型从启发式更改为默认。
-
运行以下命令:
mlagents-learn config/launcher.yaml --run-id=launcher -
坐下来,放松一下,等待训练完成。
注意
有许多不同的变量可以调整代理的设置,以调整弹道弧线的感觉。您应该在启发式模式下尝试各种设置,看看您是否能找到一些您喜欢的设置。但如果您只想使用我们使用的设置,这里是它们:
-
海拔变化速度 = 45
-
功率变化速度 = 5
-
最大功率 = 50
-
最大转向速度 = 90
-
命中半径 = 3
-
奖励半径 = 100
-
发射阈值 = 0.9
-
在我们的情况下,我们发现训练大约需要整整一天时间,所以绝对不能等待完成。但一旦完成,您将拥有一个非常精确地向目标投掷抛射物的小而整洁的代理。
运行它
因为我们正在训练代理而不实际在场景中抛出虚拟岩石,所以观看训练代理会相当无聊。
如果您将新训练的模型添加到 Unity 中并将其连接到代理,您将看到它旋转并调整角度,但您实际上能看到的过程只有我们添加为 gizmo 的黄线弧形的变化。
不过,我们想看到的是在将虚拟岩石抛出远处之前,代理调整俯仰和偏航;黄色抛物线简直不足以描述这个过程。
另外,因为情节在发射抛射物后立即结束,所以即使它生成了抛射物,你也看不到目标实际被击中,因为它会突然被传送到世界的另一个部分,所以我们还需要改变这一点。
然而,本章已经相当长了,如果要添加所有必要的步骤来展示代理实际执行这些步骤,将会使它变得非常庞大。
因此,我们将跳过这一方面,并且以各地烹饪节目的传统,说:“这是我们之前准备好的一份”,并引导您访问我们的网站,如果您想查看我们创建的场景。
这个场景与我们为训练创建的场景并没有太大的不同,但设计它使观看代理执行其动作更加令人兴奋。
这个场景的核心与本章早些时候讨论的先前训练类似。当重置情节时,我们会在地面平面上随机放置一个目标。与训练环境不同的是,当代理发射时我们不会结束情节。相反,我们将生成一个具有代理确定的物理属性的抛射物。然后,这个抛射物从代理处飞出,希望能击中目标。
因为它可能会大部分时间击中目标,我们不会立即重置环境。相反,我们通过对目标施加爆炸力将其抛向空中。在目标被击中三次或者掉落到世界的边缘后,我们然后重置情节。
提示
我们随意选择了三次命中,因为给予太多的奖励往往会使情况变得复杂。试验以找出对每个构建的情景最佳的解决方案。反复试验!同样,在这种情况下,“世界的边缘”指的是落在平面所在的 y 轴下方(换句话说,抛射物没有击中任何物体,并且继续下落)。
现在,我们不限制它可以发射多少个抛射物,所以有时它会不断地发射它们出去,但如果抛射物掉落到世界的边缘或者击中某物时,我们会将它们删除。
如果您想增加或减少它释放的抛射物的数量,最简单的参数调整是firingThreshold。增加它会使生成抛射物的可能性较小,而减小它则会增加可能性。
我们发现0.6是一个很好的释放大量抛射物的阈值;尝试一些不同的值,看看哪个对您有效。
修改代码以仅支持单个抛射物并不太困难,留作读者的练习。
如果您感兴趣,大部分更改都在InferenceLauncher.cs和Projectile.cs中,这两个文件包含了管理代理和抛射物本身的所有代码。
然而,从代理的角度来看,它与我们之前编写的原始发射器代码完全相同。唯一的真正区别在于,我们这里没有任何奖励,因为它们是不必要的,所以我们把它们拿掉了。
其他所有更改都是视觉上的微调。你可以在该书的资源中找到这些文件,这些资源可在该书的特定网站上找到。
课程学习与其他方法
课程训练的整个目的是改善训练;也就是说,要么增加训练的速度,要么提高训练的整体分数(即质量),要么两者兼而有之。如果你开始设计一个类似于我们用作初始课程的场景,奖励半径为 100 可能会感觉相当大,因为它涵盖了地面的大部分。设计一个奖励半径为 25 的东西更有可能更适合您的需求。事实上,我们还创建了一个奖励半径较小的 25 的训练,您可以在图 8-2 中看到差异。

图 8-2. TensorBoard 显示课程学习(上方线)与传统训练(下方线)的奖励
当你将这些图并排显示时,你会发现课程学习不仅更快,而且学习效果更好;也就是说,它获得了更高的平均奖励,并且在更少的步骤内达到了更高的奖励。实际上,我们在接近 700 万次迭代时停止了课程学习的训练,因为它基本上已经获得了可能的最大奖励,而即使在 1000 万次迭代后,传统学习方法也只获得了可能最大奖励的约 90%。值得指出的是,我们也遇到了一些情况,其中课程学习在相同设置下的速度显著较慢(如图 8-3 所示),尽管它仍然获得了更高的总体得分。

图 8-3. TensorBoard 显示较慢的课程学习(较低的弯曲线)与传统训练(较高的直线)的奖励
我们的猜测是,在神经网络中初始随机加权时,偶然地导致了低发射阈值,使其在投掷投射物时犹豫不决。我们认为这是因为在我们课程的Lesson0完成后,它并没有努力学习;它似乎只是在学习那个第一部分时遇到了困难。如果你将更慢的课程学习线移到左边,基本上与第一个相同,结果是几乎相同的形状和相同的总奖励。所以,这表明课程学习并不是每种情况的万能药,在训练的早期阶段确实会对整体训练产生重大影响,但即使如此,在复杂场景中它仍然具有优势。
接下来做什么?
完成这些后,我们已经解析了一个简单的课程学习示例。你可以将课程学习应用于几乎任何需要分阶段解决的问题。
Unity 的文档中有几个很棒的课程学习示例,如果你想进一步探索,我们在书籍的在线材料中提供了一些最佳起点链接。
¹ 编者注:这是一个笑话,澳大利亚人告诉我们这很搞笑。
第九章:合作学习
在本章中,我们将进一步推进我们的仿真和强化学习,创建一个仿真环境,其中多个代理必须共同努力实现共同的目标。这些类型的仿真涉及合作学习,代理通常会作为一个组接收奖励,而不是个别地—包括可能没有为导致奖励的行动做出贡献的代理。
在 Unity ML-Agents 中,用于合作学习的首选训练算法和方法称为多代理后期信用分配(或简称 MA-POCA)。MA-POCA 涉及对一组代理进行集中的评论或指导训练。MA-POCA 方法意味着代理仍然可以学习他们需要做什么,即使奖励是给整个组的。
提示
在合作学习环境中,如果你愿意,仍然可以给个体代理人奖励。我们稍后会简要涉及这一点。你也可以使用其他算法,或者像通常一样使用 PPO,但是 MA-POCA 具有特殊的功能,可以使合作学习更好。你可以将一组经过 PPO 训练的代理人联合在一起以获得类似的结果。不过我们不建议这样做。
合作模拟
让我们建立一个仿真环境,其中包含需要共同工作的一组代理人。这个环境有很多部分,所以请慢慢来,逐步进行,并在需要时做笔记。
我们的环境将涉及三个相同大小的代理人,以及三种不同大小的方块(总共六个)。代理人需要共同努力将目标有效地移动到目标区域,特别是大的方块,它们需要多个代理的推力。
在 Unity 中构建环境
创建一个新的 Unity 项目,添加 Unity ML-Agents 包,并在编辑器中打开一个新场景。我们的项目名为“Coop”。
阅读项目后,我们需要做的第一件事是创建合作学习仿真的物理元素,所以我们需要:
-
一个地板
-
一些墙壁
-
一个目标区域
-
一些不同大小的方块
-
代理人
现在我们来做这件事。
组装地板和墙壁
和通常一样,我们的墙壁和地板将是缩放后的立方体。在 Unity 编辑器中,执行以下操作:
-
在 Hierarchy 中创建一个立方体,命名为“Floor”,并将其缩放为
(25, 0.35, 25),使其成为一个大的正方形。 -
在项目面板中创建一个新的材质,指定一种颜色(我们的是浅棕色),并将该材质分配给地板。
-
在 Hierarchy 中创建四个立方体,分别命名为“Wall1,” “Wall2,” “Wall3,” 和 “Wall4,” 并将它们缩放到
(25, 1, 0.25),以便足够长,覆盖每一侧的地板。 -
将墙壁旋转并定位到地板的两侧,如图 9-1 所示。
![psml 0901]()
图 9-1. 地板上的墙壁位置
-
在项目面板中创建一个新材质,并分配一个颜色(我们使用的是浅蓝色)。将此材质分配给所有四个墙体对象。
-
在层次结构中创建一个空的
GameObject,命名为“墙壁”或类似名称,并将四个墙体对象作为子对象拖动到其中。
此时你的世界应该看起来像图 9-1。保存场景后,你可以继续操作。
添加目标
接下来我们需要一个目标区域。正如我们之前看到的,目标区域将是地板的一部分,代理器必须把块推到其中。它将被明亮地着色,以便我们作为人类在 Unity 中查看时能够看到它,并且将具有一个大的 Box Collider 体积用于代理器:
-
在层次结构中创建一个新平面,命名为“目标”,并将其缩放到
(0.5, 1.16, 2.5)。 -
在项目面板中创建一个新材质,并分配一个鲜明而分散注意力的颜色(我们使用的是红色)。将此材质分配给目标。
注意
提醒一下,在这种情况下,代理器没有任何传感器可以告诉它目标的颜色。它根据传感器获得的信息来学习目标的位置,这部分内容将很快涵盖。它看不到颜色。颜色是给人类观察用的。
-
将目标放置在地板上的一侧,如图 9-2 所示。
![psml 0415]()
图 9-2. 目标区域
-
在目标的检视器中,点击“编辑碰撞体”按钮,并调整目标的盒碰撞体,使其覆盖一个大的体积,如图 9-3 所示。这将用于检测当代理器成功将一块块推入目标区域时。
![psml 0414]()
图 9-3. 目标的碰撞体
-
同样,在目标的检视器中,勾选“是触发器”按钮。我们不希望代理器或块实际与目标的体积碰撞,我们只想知道它们是否在目标的体积内。
这就是关于目标区域的所有内容。现在你的场景应该看起来像图 9-4。在继续之前保存场景。

图 9-4. 带有目标的区域
不同尺寸的块
现在我们将为合作代理器创建一些块,推动到目标中。我们希望有三种不同类型的块,如图 9-5 所示:
-
小块
-
中等大小的块
-
大块
我们将创建每种块的副本,以便有两个每种类型的块。每种类型将获得不同的奖励金额,整个代理器组在将块推入目标时将获得这些奖励。
另外,通过物理系统,一些块会比其他块更重,这意味着代理器需要共同努力将它们推入目标,从而整个代理器组将获得更高的分数。

图 9-5. 三种类型的块
按照以下步骤创建这些块:
-
向层级添加一个新的立方体,并命名为“小块 1”或类似的名称。我们将保持默认比例。
-
向其检查器中的 Add Component 按钮添加一个刚体组件。
-
按照图 9-6 的设置设置刚体组件。
![psml 0906]()
图 9-6. 小块上的刚体组件
-
复制小块 1,创建(并命名)小块 2。
提示
你可能注意到我们已经为我们的块添加了一个显示一些文本的画布。如果你想的话,你也可以这样做。这不与 ML 组件连接,仅供人类可见。
-
将它们都放置在场景中,如图 9-7 所示。
![psml 0907]()
图 9-7. 两个小块
-
接下来,复制其中一个小块,并命名为“中等块 1”。
-
将中等块 1 的比例改为
(2, 1, 2),使其比小块 1 稍大。 -
使用检查器,将中等块 1 的质量在 Rigidbody 组件中设置为
100,使其相当重。 -
复制中等块 1,并创建(并命名)中等块 2。
-
将两个中等块都放置在场景中,如图 9-8 所示,与小块一起。
![psml 0908]()
图 9-8. 添加了中等块
-
最后,复制其中一个中等块,并命名为大块 1。
-
将大块 1 的比例改为
(2.5, 1, 2.5)。 -
使用检查器,将大块 1 的质量在 Rigidbody 组件中设置为
150,这样它就非常重了。 -
与小块和中等块一样,复制大块 1,创建(并命名)大块 2。
-
在场景中将两个大块放置如图 9-9 所示,与所有其他块一起。
![psml 0909]()
图 9-9. 添加了大块
现在关于块的部分就到这里了,稍后我们会回来添加一些代码。保存场景。
代理
这些代理非常简单,它们所有的合作行为都来自我们马上要编写的脚本,而不是编辑器设置中的任何特殊内容:
-
在层级中创建一个新的立方体,并将其重命名为“代理 1”。保持默认比例。
-
向其添加一个刚体组件,设置如图 9-10 所示。
![psml 0910]()
图 9-10. 代理的刚体组件
-
再次复制一次,并将两个新副本重命名为代理 2 和代理 3。
-
创建三种新的材料——每种代理一种——颜色不同,并分配给代理。
-
将它们放置得与我们的位置相似,如图 9-11 所示。
![psml 0911]()
图 9-11. 三个代理
现在,关于代理就是这些了。接下来我们将添加一些代码,然后需要回到 Unity 场景中实现更多功能。别忘了保存场景。
编写代理
大部分场景已经构建完成,现在是为代理编写代码的时候了。实际上,这个代理的代码相当简单,因为很多逻辑都已经移到别的地方了——我们很快就会讲到。
对于代理,我们只需要实现以下方法:
-
Initialize() -
MoveAgent() -
OnActionReceived()
让我们开始吧:
-
在项目窗口中创建一个新的 C#脚本资源,并命名为“合作块推动者”或类似的名称。
-
在代码编辑器中打开新脚本,删除整个样板内容,然后添加以下导入项:
using UnityEngine; using Unity.MLAgents; using Unity.MLAgents.Actuators; -
接下来,实现类的框架,确保它是从
Agent继承而来:public class CoopBlockPusher : Agent { } -
添加一个成员变量来保存对代理
Rigidbody的引用:private Rigidbody agentRigidbody; -
覆盖代理的
Initialize()函数来获取对那个Rigidbody的引用:public override void Initialize() { agentRigidbody = GetComponent<Rigidbody>(); } -
接下来,创建一个
MoveAgent()方法:public void MoveAgent(ActionSegment<int> act) { }此方法接受一个数组作为参数(
act),使用 ML-Agents 的ActionSegment数据结构。我们很快将在OnActionReceived()中调用它,传入离散动作的数组以移动代理。提示
每次只会传递一个具体的离散动作,因此我们只查看数组的第一个条目。这个代理将有七种不同的可能动作(什么也不做、向一边旋转、向另一边旋转、向前、向后、向左、向右)。
-
现在,在新方法中,将一些临时变量(用于方向和旋转)清零:
var direction = Vector3.zero; var rotation = Vector3.zero; -
获取我们正在处理的数组的第一个(也是唯一一个)条目的引用,它表示此刻的具体动作:
var action = act[0]; -
现在,在这个
action上执行switch:switch(action) { case 1: direction = transform.forward * 1f; break; case 2: direction = transform.forward * -1f; break; case 3: rotation = transform.up * 1f; break; case 4: rotation = transform.up * -1f; break; case 5: direction = transform.right * -0.75f; break; case 6: direction = transform.right * 0.75f; break; }这个
switch语句设置了旋转或方向的一个临时变量,具体取决于传递的动作。注意
到底哪个动作对应于代理的哪个实际动作,这完全是任意的。我们只是事先决定了,并坚持这样做。机器学习系统学习如何将应用于何处,并处理它。
-
接下来,根据设置的动作实际实现旋转代理的
transform,并为代理的Rigidbody施加力,分别用于旋转和方向:transform.Rotate(rotation, Time.fixedDeltaTime * 200f); agentRigidbody.AddForce(direction * 3, ForceMode.VelocityChange); -
对于
MoveAgent()方法来说就是这些了,现在我们转向OnActionReceived(),在那里实际调用它:public override void OnActionReceived(ActionBuffers actionBuffers) { MoveAgent(actionBuffers.DiscreteActions); }
这只是将它接收到的ActionBuffers的离散部分取出,并传递给我们刚刚编写的单独的MoveAgent()方法。
提示
我们本来可以直接将MoveAgent()中的代码放在OnActionReceived()中,但因为OnActionReceived()技术上是处理动作而不是具体的移动,所以调用我们的MoveAgent()方法更清晰,即使唯一可能的动作都与移动相关。
我们的代理代码就这些了。保存脚本,然后返回 Unity 编辑器。
编写环境管理器
现在我们需要制作一个脚本,该脚本将负责环境本身。这个脚本将执行一些重要的工作,以便我们可以拥有协作代理:
-
在项目面板中创建一个新的 C#脚本资源,并命名为“CooperativeBlockPusherEnvironment”或类似的名称。
-
打开新的脚本并删除模板内容。
-
添加以下导入项:
using System.Collections; using System.Collections.Generic; using Unity.MLAgents; using UnityEngine; -
创建一个类来存储其他所有内容:
public class CoopBlockPusherEnvironment : MonoBehaviour { }这个类不需要是
Agent的子类,而是可以直接是默认的 UnityMonoBehaviour的子类。 -
创建一个类来存储所有将一起工作的代理及其起始位置:
[System.Serializable] public class Agents { public CoopBlockPusher Agent; [HideInInspector] public Vector3 StartingPosition; [HideInInspector] public Quaternion StartingRotation; [HideInInspector] public Rigidbody RigidBody; } -
现在为它们将要推入目标的块创建一个类似的类:
[System.Serializable] public class Blocks { public Transform BlockTransform; [HideInInspector] public Vector3 StartingPosition; [HideInInspector] public Quaternion StartingRotation; [HideInInspector] public Rigidbody RigidBody; } -
我们需要创建一个好的
int,用来存储我们希望环境执行的最大步数,这样我们可以从编辑器轻松配置它:[Header("Max Environment Steps")] public int MaxEnvironmentSteps = 25000; -
我们还需要创建一个成员变量的集合,用于存储有用的东西的句柄,例如地面、整体区域、目标、在需要重置时检查的内容以及剩余未交付到目标的块的数量:
[HideInInspector] public Bounds areaBounds; // The ground (we use this to spawn the things that need to be placed) public GameObject ground; public GameObject area; public GameObject goal; private int resetTimer; // blocks left private int blocksLeft; -
我们需要创建两个列表,一个是
Agents,另一个是Blocks,使用我们刚写的类:// List of all the agents public List<Agents> ListOfAgents = new List<Agents>(); // List of all blocks public List<Blocks> ListOfBlocks = new List<Blocks>(); -
最后,我们创建一个
SimpleMultiAgentGroup,用于将代理分组,以便它们可以共同工作:private SimpleMultiAgentGroup agentGroup;稍后会详细讨论
SimpleMultiAgentGroup。 -
接下来,我们需要实现一个
Start()方法,用于在模拟即将开始时设置一切:void Start() { } -
在
Start()方法内部,我们将执行所有必要的操作,以确保一切准备就绪:-
获取地面边界的句柄。
-
迭代
Blocks列表(场景中的所有块),并存储它们的起始位置、旋转和它们的Rigidbody。 -
初始化一个新的
SimpleMultiAgentGroup。 -
迭代
Agents 列表(场景中的所有代理),并存储它们的起始位置、旋转和它们的Rigidbody,然后在我们创建的SimpleMultiAgentGroup上调用RegisterAgent(),通知它存在每个我们想要一起合作的代理。 -
调用
ResetScene(),我们马上就会编写这个方法。
-
-
在
Start()方法内部,添加以下代码来完成前述所有操作:areaBounds = ground.GetComponent<Collider>().bounds; foreach (var item in ListOfBlocks) { item.StartingPosition = item.BlockTransform.transform.position; item.StartingRotation = item.BlockTransform.rotation; item.RigidBody = item.BlockTransform.GetComponent<Rigidbody>(); } agentGroup = new SimpleMultiAgentGroup(); foreach (var item in ListOfAgents) { item.StartingPosition = item.Agent.transform.position; item.StartingRotation = item.Agent.transform.rotation; item.RigidBody = item.Agent.GetComponent<Rigidbody>(); agentGroup.RegisterAgent(item.Agent); } ResetScene(); -
接下来,我们将实现
FixedUpdate()方法,在 Unity 中会定期调用:void FixedUpdate() { resetTimer += 1; if(resetTimer >= MaxEnvironmentSteps && MaxEnvironmentSteps > 0) { agentGroup.GroupEpisodeInterrupted(); ResetScene(); } agentGroup.AddGroupReward(-0.5f / MaxEnvironmentSteps); }在这里,我们每次增加重置计时器
1,并检查重置计时器是否大于或等于最大环境步数(且最大环境步数大于0),如果是,则通过在代理组上调用GroupEpisodeInterrupted()中断代理组,并调用ResetScene()。如果未达到最大环境步数,我们只需在代理组上调用
AddGroupReward(),给予组的惩罚为-0.5除以最大环境步数,以惩罚它的存在。希望这样可以确保代理尽快完成任务。SimpleMultiAgentGroup协调了一组代理,并允许代理共同努力以最大化分配给整个组的奖励。通常的奖励和结束事件发生在SimpleMultiAgentGroup上,而不是在单个代理上。注意
SimpleMultiAgentGroup是 Unity ML-Agent 实现的 MA-POCA 的一个特性,因此它只在使用 MA-POCA 训练代理时才起作用。 -
现在我们将创建一个相当大的
GetRandomSpawnPos()方法,根据需要随机放置区块和代理:public Vector3 GetRandomSpawnPos() { Bounds floorBounds = ground.GetComponent<Collider>().bounds; Bounds goalBounds = goal.GetComponent<Collider>().bounds; // Stores the point on the floor that we'll end up returning Vector3 pointOnFloor; // Start a timer so we have a way // to know if we're taking too long var watchdogTimer = System.Diagnostics.Stopwatch.StartNew(); do { if (watchdogTimer.ElapsedMilliseconds > 30) { // This is taking too long; throw an exception to bail // out, avoiding an infinite loop that hangs Unity! throw new System.TimeoutException ("Took too long to find a point on the floor!"); } // Pick a point that's somewhere on the top face of the floor pointOnFloor = new Vector3( Random.Range(floorBounds.min.x, floorBounds.max.x), floorBounds.max.y, Random.Range(floorBounds.min.z, floorBounds.max.z) ); // Try again if this point is inside the goal bounds } while (goalBounds.Contains(pointOnFloor)); // All done, return the value! return pointOnFloor; } -
接下来,我们将创建一个
ResetBlock()方法,接受一个我们之前创建的Blocks类型,并给它一个随机的生成位置(使用我们之前编写的GetRandomSpawnPos()方法),将速度和角速度设为零:void ResetBlock(Blocks block) { block.BlockTransform.position = GetRandomSpawnPos(); block.RigidBody.velocity = Vector3.zero; block.RigidBody.angularVelocity = Vector3.zero; } -
现在我们需要一个方法,用于记录代理或代理组成功将区块交付到目标的时候:
public void Scored(Collider collider, float score) { blocksLeft--; // check if it's done bool done = blocksLeft == 0; collider.gameObject.SetActive(false); agentGroup.AddGroupReward(score); if (done) { // reset everything agentGroup.EndGroupEpisode(); ResetScene(); } }这可能看起来有点神秘,但实际上我们的
Scored()方法接受一个Collider和一个float(表示分数),因为这个方法只有在区块确实被交付到目标时才被调用,它:-
将剩余区块的计数减少
1 -
检查是否剩余
0个区块,如果是,则将名为done的布尔值设置为true -
停用传入的
Collider所属的游戏对象(换句话说,消除推入目标的区块) -
向
SimpleMultiAgentGroup添加奖励,根据传入的分数 -
然后检查
done的布尔值是否为true,如果是,则在SimpleMultiAgentGroup上调用EndGroupEpisode(),然后调用ResetScene()
-
-
接下来,我们将创建一个快速的辅助方法来返回一个随机旋转:
Quaternion GetRandomRot() { return Quaternion.Euler(0, Random.Range(0.0f, 360.0f), 0); } -
而且,对于环境脚本,我们将编写经常调用的
ResetScene():public void ResetScene() { resetTimer = 0; var rotation = Random.Range(0,4); var rotationAngle = rotation * 90f; area.transform.Rotate(new Vector3(0f, rotationAngle, 0f)); // first reset all the agents foreach (var item in ListOfAgents) { var pos = GetRandomSpawnPos(); var rot = GetRandomRot(); item.Agent.transform.SetPositionAndRotation(pos,rot); item.RigidBody.velocity = Vector3.zero; item.RigidBody.angularVelocity = Vector3.zero; } // next, reset all the blocks foreach (var item in ListOfBlocks) { var pos = GetRandomSpawnPos(); var rot = GetRandomRot(); item.BlockTransform.transform.SetPositionAndRotation(pos,rot); item.RigidBody.velocity = Vector3.zero; item.RigidBody.angularVelocity = Vector3.zero; item.BlockTransform.gameObject.SetActive(true); } blocksLeft = ListOfBlocks.Count; }这个函数:
-
将
resetTimer设回0。 -
然后旋转整个区域,这样目标不总是在同一侧。
-
迭代
ListOfAgents中的所有Agent,使用我们的辅助方法给它们随机位置和随机旋转,并将它们的速度和角速度设为零。 -
迭代
ListOfBlocks中的所有Block,使用我们的辅助方法给它们随机位置和随机旋转,将它们的速度和角速度设为零,并激活它们。
-
提示
我们将每个区块设置为活动状态,因为它们可能在重置后从非活动状态回来,因为模拟可能已经运行,并且一些区块可能已经被推入目标(根据之前的代码,这意味着它们被设置为非活动状态)。
环境管理脚本到此结束。保存并返回 Unity。
编码区块
我们需要完成的最后一部分编码是区块本身:
-
在项目面板中创建一个新的 C#脚本资产,并命名为“GoalScore”或类似的名称。
-
在代码编辑器中打开脚本并删除样板内容。
-
添加以下导入:
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; -
实现一个名为
GoalScore的类,它是 Unity 默认MonoBehaviour的子类:public class GoalScore : MonoBehaviour { } -
内部,添加一些成员变量以存储我们要检测的特定 Unity 标签,推送该脚本将附加到的特定块的价值,并且块的
Collider:public string tagToDetect = "goal"; //collider tag to detect public float GoalValue = 1; private Collider blockCollider; -
接下来,在
GoalScore类内部实现名为TriggerEvent的类,如下所示:[System.Serializable] public class TriggerEvent : UnityEvent<Collider, float> { }需要使用 Unity 的事件系统来使用这个类。稍后会详细介绍。
-
在
TriggerEvent类之后,但仍然在GoalScore类内部,添加以下触发回调函数:[Header("Trigger Callbacks")] public TriggerEvent onTriggerEnterEvent = new TriggerEvent(); public TriggerEvent onTriggerStayEvent = new TriggerEvent(); public TriggerEvent onTriggerExitEvent = new TriggerEvent();这些表示某物进入所讨论对象的碰撞体、停留在其中和退出其中的事件。
-
现在,创建每个这些被调用的函数:
private void OnTriggerEnter(Collider col) { if (col.CompareTag(tagToDetect)) { onTriggerEnterEvent.Invoke(blockCollider, GoalValue); } } private void OnTriggerStay(Collider col) { if (col.CompareTag(tagToDetect)) { onTriggerStayEvent.Invoke(blockCollider, GoalValue); } } private void OnTriggerExit(Collider col) { if (col.CompareTag(tagToDetect)) { onTriggerExitEvent.Invoke(blockCollider, GoalValue); } }
这些每个都映射到我们创建的触发回调之一,接受一个Collider,如果该Collider具有我们要查找的标签(这在我们之前创建的某些成员变量中定义),则触发回调事件,传递Collider和GoalValue(这是我们刚刚创建的某个成员变量之一)。
保存脚本,并返回 Unity。
完成环境和代理的最后配置
我们已经编写了三个脚本,现在我们需要将它们连接起来。
首先,进行以下操作:
-
从 Project 窗口中的代理脚本拖动到 Hierarchy 中的每个代理上(总共三个代理)。
-
从 Project 窗口中的环境脚本拖动到 Hierarchy 中的环境(父对象)上。
-
并且从 Project 窗口中的 GoalScore 脚本拖动到 Hierarchy 中的每个块上(总共六个块)。
接下来,我们需要配置所有内容。我们将从代理开始。对每个 Hierarchy 中的代理执行以下操作:
-
选择代理,并使用 Inspector 添加一个行为参数组件。
-
配置行为参数组件如图 9-12 所示。
![psml 0912]()
图 9-12. 已配置的行为参数组件
-
使用 Inspector 向代理添加决策请求器组件,保留其设置为默认值。
-
使用 Inspector 向代理添加刚体传感器组件,确保将根体分配为代理的刚体组件,将虚拟根分配为代理本身,并按照图 9-13 中显示的方式勾选框。
![psml 0913]()
图 9-13. 刚体传感器组件
-
接下来,在 Hierarchy 中的代理中添加一个空的子对象,并命名为“网格传感器”或类似的名称。
-
在 Hierarchy 中的代理的网格传感器子对象上选择,并使用其 Inspector 中的“添加组件”按钮添加网格传感器组件。
-
使用 Inspector 配置网格传感器组件如图 9-14 所示。
![psml 0914]()
图 9-14. 已配置的网格传感器组件
网格传感器组件创建一个网格传感器。网格传感器是一种非常灵活的传感器,它围绕代理创建一个网格形状,并根据它设置为查找的对象类型(由标签定义),在代理周围的顶部视角的特定单元格中检测对象的存在。所选代理附加的网格传感器示例显示在图 9-15 中。
注意
Eidos,视频游戏开发工作室,向 Unity ML-Agents 项目贡献了网格传感器组件。网格传感器将射线投射数据提取的通用性与卷积神经网络(CNNs——处理图像的神经网络)的计算效率结合起来。网格传感器通过查询物理属性收集模拟数据,然后将数据结构化为“高度 x 宽度 x 通道”的矩阵。这个矩阵在很多方面类似于图像,并且可以输入到 CNN 中。另一个好处是,网格可以比实际图像具有更低的分辨率,这可以提高训练时间。

图 9-15. 网格传感器组件的操作
接下来,对层次结构中的每个块执行以下操作:
-
选择块,并使用检视器将目标分数组件(我们刚刚编写并拖动到其上的脚本)设置为检测带有“goal”标签的物体。
-
设置适当的目标值:小块为 1,中块为 2,大块为 3。
-
接下来,单击触发回调部分下方的 + 按钮,并将下拉列表设置为仅运行时,如图 9-16 所示。
![psml 0916]()
图 9-16. 仅设置为运行时
-
从层次结构中拖动环境对象到下拉列表下方的字段中,然后将右侧的下拉列表设置为指向我们先前创建的环境脚本中的
Scored()方法,如图 9-17 所示。![psml 0917]()
图 9-17. 选择
Scored()方法
接下来,对环境父对象执行以下操作:
-
在层次结构中选择环境父对象。
-
在其检视器中,在属于我们编写并拖动的脚本的环境组件下,将最大环境步骤设置为
5000。 -
然后将地板、环境父对象和目标拖入相应的字段中,如图 9-18 所示。
![psml 0918]()
图 9-18. 配置环境
-
更新组件中显示的代理列表以包含
3,并拖入每个三个代理,如图 9-19 所示。![psml 0919]()
图 9-19. 代理列表
-
更新组件中显示的块列表以包含
6,并拖入每个六个块,如图 9-20 所示。![psml 0920]()
图 9-20. 块列表
就这样!保存场景。
为合作进行培训
几乎是时候训练我们的合作环境了。通常,首先我们需要创建一个 YAML 文件:
behaviors:
CoopBlockPush:
trainer_type: poca
hyperparameters:
batch_size: 1024
buffer_size: 10240
learning_rate: 0.0003
beta: 0.01
epsilon: 0.2
lambd: 0.95
num_epoch: 3
learning_rate_schedule: constant
network_settings:
normalize: false
hidden_units: 256
num_layers: 2
vis_encode_type: simple
reward_signals:
extrinsic:
gamma: 0.99
strength: 1.0
keep_checkpoints: 5
max_steps: 15000000
time_horizon: 64
summary_freq: 60000
现在,将其保存在某个地方!要运行训练,请启动您的终端并运行以下内容:
mlagents-learn CoopBlockPush.yaml --run-id=BlockCoop1
就这样。您的训练可能需要几个小时——在我们的测试 MacBook Pro 上,花了大约 18 小时才达到某个位置。
拿到生成的 .onnx 文件,将其拖入 Unity 中的项目面板,然后分配到所有代理人的适当字段中,如 图 9-21 所示,并运行您的模拟以观察您的小伙伴代理共同推动方块。

图 9-21. 该模型附加到代理人上
合作代理或一个大代理
有时很难概念化您可能希望采用合作多代理方法的情况,在这种方法中,每个代理都是真正独立的,但它们通过本章讨论的系统受到奖励和指导,而不是制作一个在模拟中表现为各种实体的单个代理。
换句话说,您可以通过制作一个在世界中有三个立方体实体的单个“巨大”代理来复制您在此处构建的模拟的行为。
制作单独的代理通常更容易,因为每个代理将有一个更简单的模型,这将导致更可靠的训练,并且您将更快地收敛于解决方案。每个代理的模型中存储的神经网络比理论上的巨大代理要小得多,并且行为更可预测,可能也更可靠。但是,是的,您可以制作一个巨大的代理。
注意
一个代理人与其他代理人“合作”与孤立代理人没有任何不同,因为不同代理人之间不能进行通信(它们不能共享意图,关于其他代理人状态的信息只能通过对它们的外部观察得到)。存在于模拟中的其他代理人只是这个代理人必须考虑和观察的环境的另一部分——尽管可能是一个潜在混乱的部分。
一个合法的个体集合,但协作学习代理人的问题空间,而不是一个巨大代理人的部分,比试图处理其控制下实体之间可能发生的指数级交互的单个代理人小得多。因此,如果需要的话,通常最好制作一个协作集合的代理人。
提示
如果您想更详细地探索合作代理,请查看 Unity 的精彩躲避球环境。
第十章:在模拟中使用相机
现在是时候获得一些真正的视觉了。我们不是指闪亮的机器人,而是指相机(还有灯光和行动)。在这一章中,我们将看看如何使用一台相机将你的模拟世界作为代理的观察结果。
你不再需要通过代码向代理输入数字或从传感器输入观测结果!相反,你将受限于你选择设置的相机能够看到的内容。(从技术上讲,这也是数字,但我们离题了。)
观察结果和相机传感器
到目前为止,我们使用的所有观察结果基本上都是数字——通常是Vector3——我们通过CollectObservations方法向代理提供,或者通过某种类型的传感器收集,观察环境来测量事物,或者使用基于网格的观察结果进行二维空间表示。
我们在Agent中要么实现了CollectObservations(),并传入了向量和其他形式的数字,要么在 Unity 编辑器中向代理添加了组件,这些组件在幕后创建了射线投射(完美的激光用于测量距离和其击中的内容),并自动将这些数字传递给 ML-Agents 系统。
还有另一种方法可以向我们的代理提供观测结果:通过使用CameraSensor和RenderTextureSensor。这些允许我们传递图像信息,以 3D 张量的形式,给代理策略的卷积神经网络(CNN)。所以,基本上是更多的数字。但从我们的角度来看,这是一幅图片。
提示
卷积神经网络通常用作描述处理图像的任何形式的神经网络的术语。
使用图像作为观察结果允许代理从图像中的空间规律中学习,以形成策略。
提示
你可以将视觉观察结果与已经使用的向量观察结果结合起来。我们稍后再讨论这个。
广义上说,向你的代理添加CameraSensor非常简单,和在 Unity 中的许多其他操作一样,涉及在检视面板中添加一个组件。
我们马上就会通过一个完整的例子来详细讨论,但典型的步骤如下:
-
在 Unity 编辑器中,定位到场景层次结构中的代理并选择它。
-
在代理的检视面板中,使用“添加组件”按钮添加一个相机传感器组件。
-
在添加的相机传感器组件中,将相机(从层次结构中的任何相机)分配给相机字段。
-
你还可以为相机传感器命名,并指定宽度、高度以及神经网络处理的图像是否为灰度。
注意
每个传感器组件(无论是相机还是其他)必须在每个代理上具有唯一的传感器名称。
我们很快会回到相机传感器,并讨论如何连接相机到其中。
提示
当难以用向量数值化表示您希望代理程序处理的状态时,视觉观察是很有用的,但可能会使您的代理训练速度变慢。
构建仅相机代理
为了演示相机传感器的使用,本章的活动是创建一个非常简单的模拟,仅依赖于相机进行观察。
我们即将构建的模拟是一个立方体代理(亲爱的读者,它存在于虚空中),必须在其顶部保持一个球(也称为球,但不是代理球)平衡。
首先,像以前做过几次一样,在 Unity 中创建一个新的空项目,并导入 ML-Agents 包。然后,在一个新场景中,执行以下操作:
-
在层次结构中创建一个新的立方体,并命名为“代理”。
-
在层次结构中创建一个新的球体,并命名为“球”。
-
将代理立方体的比例设置为
(5, 5, 5)。 -
将球体移动到代理上方,如图 10-1 所示。大致位置即可;您只需让球在立方体上方的空间中浮动即可。
![psml 1001]()
图 10-1. 立方体上方的球
-
接下来,在层次结构中创建一个新的空对象,命名为“平衡球”,并将代理和球拖到其下作为子对象。这代表整个模拟环境。
目前就这些了。我们保证我们正在做某些事情。
编写仅相机代理的代码
现在,我们将编写驱动我们简单代理的代码。要开始编码,像往常一样,在项目视图中创建一个新的脚本资产作为代理的组件。我们将其命名为“BalancingBallAgent”。双击项目视图中的新资产文件以在您的代码编辑器中打开它。
当您在代码编辑器中打开文件后,请按照以下步骤操作:
-
添加以下导入,以便获取 Unity 所需的所有部分:
using UnityEngine; using Unity.MLAgents; using Unity.MLAgents.Actuators; using Unity.MLAgents.Sensors; using Random = UnityEngine.Random;我们显然需要从 ML-Agents 中获取大量内容,但我们也想要 Unity 的随机数系统,以便我们可以生成随机数。
-
接下来,删除为您提供的整个类,并替换为:
public class BalancingBallAgent : Agent { }请注意,您需要确保类名与您创建的资产文件相同。当然,它将从
Agent继承。 -
添加一些成员变量,一个用于存储作为球体的
GameObject引用,另一个用于该球体的Rigidbody:public GameObject ball; Rigidbody ball_rigidbody; -
接下来,覆盖
Initialize()方法,这来自Agent,在代理首次启用时调用一次:public override void Initialize() { ball_rigidbody = ball.GetComponent<Rigidbody>(); }在
Initialize()内部,我们获取球体的Rigidbody的句柄,并可以进行其他设置(但目前不需要)。 -
覆盖
Heuristic()方法,这也来自Agent,允许我们手动控制代理:public override void Heuristic(in ActionBuffers actionsOut) { var continuousActionsOut = actionsOut.ContinuousActions; continuousActionsOut[0] = -Input.GetAxis("Horizontal"); continuousActionsOut[1] = Input.GetAxis("Vertical"); }像往常一样,
Heuristic()允许代理使用自定义启发式选择动作。这意味着您可以提供一些与任何机器学习分开的自定义决策逻辑。最常见的用途是由人类提供对代理的手动控制,这正是我们在这里所做的。我们将用它来测试代理,而不是训练它,尽管(这次我们不进行 IL 或 GAIL)。到目前为止,如果你按顺序阅读本书,我们的代码应该对你来说已经很熟悉了,但本质上它:
-
获取传递给该方法的
ActionBuffers的连续部分。 -
获取连续动作数组中的第一个条目,并将水平输入的负值赋给它。
-
获取连续动作数组中的第二个条目,并将垂直输入的值赋给它。
注意
有关 Unity 输入管理器的信息,请查阅Unity 文档。
-
-
接下来,我们将实现
Agent中的OnEpisodeBegin()函数:public override void OnEpisodeBegin() { gameObject.transform.rotation = new Quaternion(0f, 0f, 0f, 0f); gameObject.transform.Rotate (new Vector3(1, 0, 0), Random.Range(-10f, 10f)); gameObject.transform.Rotate (new Vector3(0, 0, 1), Random.Range(-10f, 10f)); ball_rigidbody.velocity = new Vector3(0f, 0f, 0f); ball.transform.position = new Vector3 (Random.Range(-1.5f, 1.5f), 4f, Random.Range(-1.5f, 1.5f)) + gameObject.transform.position; }在这个函数中,我们需要做的是在训练周期开始时设置代理和环境。对于我们的平衡球代理,我们需要:
-
将代理的旋转设置为默认位置。
-
在 x 轴上随机旋转代理,在
-10到10之间。 -
在 z 轴上随机旋转代理,在
-10到10之间。 -
将球的
Rigidbody速度设为零。 -
将球本身的位置随机设置在 x 轴和 z 轴之间的
-1.5到1.5之间,并且在 y 轴上为4(这大致是您之前放置的高度),这样它始终位于代理的正上方,但是在不同的位置上。
-
-
最后,对于代码,我们实现
OnActionReceived()。我们将分段实现,因为这是相当多的代码。首先,我们将实现框架:public override void OnActionReceived(ActionBuffers actionBuffers) { var action_z = 2f * Mathf.Clamp(actionBuffers.ContinuousActions[0], -1f, 1f); var action_x = 2f * Mathf.Clamp(actionBuffers.ContinuousActions[1], -1f, 1f); }该方法被调用以允许代理执行一些动作。它执行的操作基于传入的
ActionBuffers的内容。提示
Agent系统中的ActionBuffers位元特别来自我们之前导入的Unity.MLAgents.Actuators组件的连续部分。到目前为止,我们实现了一些临时变量来保存我们代理的 z 轴和 x 轴动作。具体来说,我们使用了
Clamp,并传入了连续动作组件的每个部分的内容,将其限制在-1到1之间,然后将结果乘以 2 以增强效果。注意
我们在这里使用的
Clamp函数,在过去的几次使用中,接受一个值(在本例中来自ActionBuffers数组),如果它在后续的两个值之间(在本例中为-1和1),则返回该值。否则,如果初始值小于它,则返回较小的值;如果初始值大于它,则返回较大的值。 -
接下来,在这个初始代码下方但仍在
OnActionReceived()内,添加:if ((gameObject.transform.rotation.z < 0.25f && action_z > 0f) || (gameObject.transform.rotation.z > -0.25f && action_z < 0f)) { gameObject.transform.Rotate(new Vector3(0, 0, 1), action_z); } if ((gameObject.transform.rotation.x < 0.25f && action_x > 0f) || (gameObject.transform.rotation.x > -0.25f && action_x < 0f)) { gameObject.transform.Rotate(new Vector3(1, 0, 0), action_x); }此代码检查我们代理的旋转的 z 轴是否小于
0.25,并且传入的 z 轴动作是否大于0,或者我们代理的旋转的 z 轴是否大于-0.25,并且传入的 z 轴动作是否小于0。如果其中任一条件为真,则调用Rotate,请求按照我们早些时候创建的action_z变量中指定的数量在 z 轴上旋转。接下来,我们对 x 轴执行相同的操作。
-
接下来,在方法内部,在刚刚编写的代码下面,添加以下内容:
if ((ball.transform.position.y - gameObject.transform.position.y) < -2f || Mathf.Abs (ball.transform.position.x - gameObject.transform.position.x) > 3f || Mathf.Abs (ball.transform.position.z - gameObject.transform.position.z) > 3f) { SetReward(-1f); EndEpisode(); } else { SetReward(0.1f); }此代码检查球在 y 轴上的位置与代理在 y 轴上的位置之差是否小于
-2,或者类似地在 x 和 z 轴上检查差值是否大于3。为什么?任何这些情况可能表明球已经离开了代理的顶部并掉下去或者做了其他奇怪的事情。这意味着模拟应该结束本轮,并且代理应该收到处罚(在这种情况下是-1)。否则,作为小奖励,球很可能仍然在代理的顶部表面,一切都很顺利,提供了
0.1的奖励。
就这些代码!在切换回 Unity 编辑器之前别忘了保存。
为代理添加新摄像头
接下来,我们需要为代理添加一个额外的摄像头,用作其观察。我们将通过在 Unity 编辑器中向场景中添加对象来实现这一点。摄像头默认情况下不是由您编写的,它们是我们向 Unity 世界添加的物理但不可见的东西。
摄像头确实有坐标(即它们有一个transform),我们可以在 Unity 编辑器中看到它们(这帮助我们定位它们和它们的朝向),但是如果在场景中有两个相互对视的摄像头,它们不会“看见”对方。这并不存在实体物理。
要添加摄像头,请按照以下步骤进行:
-
在层级视图中使用,将一个新摄像头创建为平衡球对象(代理和球的父级)的子对象,如图 10-2 所示。
![psml 1002]()
图 10-2. 添加新摄像头
-
将新摄像头重命名为合适的名称,比如“代理摄像头”。
-
将新的代理摄像头定位,使其朝向代理和球,如图 10-3 所示。
注意
场景中已经有一个摄像头,因为每个场景都会有一个。不要移除它。这是您作为人类观看模拟运行的摄像头。
![psml 1003]()
图 10-3. 调整新代理摄像头的位置
-
在层级视图中选择代理,并使用“添加组件”按钮添加一个摄像机传感器组件。
-
现在,使用检视面板将新代理摄像头分配给摄像机传感器组件中的摄像机字段,如图 10-4 所示。
![psml 1004]()
图 10-4. 分配摄像头
-
在代理检视器中的“球体”字段中分配层级中的球体对象,如图 10-5 所示。
![psml 1005]()
图 10-5. 在脚本中分配球
-
使用“添加组件”按钮,确保已将决策请求器和行为参数组件附加到代理上。
-
确保你的行为参数组件在向量观察中具有
0的空间大小(即没有观察),并具有2个连续动作。 -
你还需要为这个行为命名。我们建议使用“平衡球”或类似的名称。
需要添加相机的全部操作都已完成。在继续之前保存你的场景。
查看代理相机所见内容
有几种方法可以查看代理相机的视图。第一种方法非常明显,你可能已经通过它来适当地定位相机:
-
在层级中选择代理相机。
-
场景视图将在右下角显示相机所见的预览,如图 10-6 所示。
![psml 1006]()
图 10-6. 显示代理相机视图
你还可以创建一个视图,展示特殊代理相机的视图,并在游戏视图的顶部显示,如图 10-7 所示。
![psml 1007]()
图 10-7. 特殊视图显示新相机的视图
-
在 Unity 编辑器中创建此视图,可以在项目视图中创建一个新的自定义渲染纹理资源,如图 10-8 所示。
![psml 1008]()
图 10-8. 创建一个自定义渲染纹理资源
-
设置如图 10-9 所示。默认设置应该是正确的。
![psml 1009]()
图 10-9. 新的渲染纹理资源
-
给它一个合理的名称,比如“cameraRenderTexture”。
-
在层级中选择代理相机,在其检视器中的目标纹理字段中,分配刚刚创建的渲染纹理资源,如图 10-10 所示。
![psml 1010]()
图 10-10. 分配给代理相机的渲染纹理资源
提示
渲染纹理是一个游戏开发术语,在所有游戏引擎中都很常见,由引擎创建并在运行时更新。因此,渲染纹理在你希望将场景中相机的视图放置在场景中或在场景顶部显示时非常有用。例如,在视频游戏中,常用于显示游戏内屏幕的内容:屏幕视图是一个渲染纹理,显示了摄像机在场景其他位置(玩家视角之外)看到的内容。了解更多关于渲染纹理的内容,可以参考Unity 文档。
-
接下来,在层级中创建一个画布,如图 10-11 所示,保持所有默认设置。
![psml 1011]()
图 10-11. 在层次结构中创建画布
画布是 Unity 提供的用于屏幕渲染的对象;这意味着它通常用于显示位于场景顶部的事物(屏幕与场景),如用户界面。我们将使用它来制作一个简易的非交互界面:显示摄像机所见的内容。
-
作为画布的子对象,在层次结构中添加一个空对象并命名为“摄像机视图”或类似的名称,如图 10-12 所示。
![psml 1012]()
图 10-12. 层次结构,显示新画布和摄像机视图
-
在此新对象的检视器中,使用“添加组件”按钮并添加一个原始图像组件。
-
然后,将您之前创建的渲染纹理资产(从项目视图中)分配给原始图像组件的纹理字段,如图 10-13 所示。
![psml 1013]()
图 10-13. 将纹理分配给原始图像组件
-
现在,使用矩形工具(如图 10-14 所示),在场景视图中调整原始图像组件的大小并将其放置在角落里,如图 10-15 所示。
![psml 1014]()
图 10-14. 矩形工具
![psml 1015]()
图 10-15. 将原始图像组件放置在角落里
现在当您运行模拟时,您将会看到主摄像机的视图和代理摄像机的小视图,就像往常一样。您可以使用此技术为您自己对任何您创建的模拟添加视图。即使代理不使用摄像机,也没有阻止您添加摄像机以捕捉不同的视角。
训练基于摄像机的代理
要训练代理,您通常需要一个用于超参数的 YAML 文件(你会说:“这真是个巨大的惊喜!”)。这是我们建议使用的文件,但请随意尝试:
behaviors:
BalancingBall:
trainer_type: ppo
hyperparameters:
batch_size: 64
buffer_size: 12000
learning_rate: 0.0003
beta: 0.001
epsilon: 0.2
lambd: 0.99
num_epoch: 3
learning_rate_schedule: linear
network_settings:
normalize: true
hidden_units: 128
num_layers: 2
vis_encode_type: simple
reward_signals:
extrinsic:
gamma: 0.99
strength: 1.0
keep_checkpoints: 5
max_steps: 500000
time_horizon: 1000
summary_freq: 12000
使用准备好的 YAML 文件后,通过在命令行上执行mlagents-learn来运行训练:
mlagents-learn BalancingBall.yaml --run-id=Ball1
警告
您在代理的行为参数组件上的行为名称需要像往常一样与 YAML 文件中的行为名称匹配。
仅使用视觉观察进行训练将比使用向量观察进行训练需要更长的时间。我们的训练过程,使用前述的 YAML 文件,在最近的 MacBook Pro 上大约花费了两个小时。
当训练完成时,使用生成的.onnx文件来运行您的代理,看看效果如何。
提示
探索将视觉观察与向量和其他观察类型结合使用。看看是否可以将它们结合起来以快速训练代理。
摄像机和您
一直使用摄像机并把它们用于所有事情非常诱人。我们明白!它们很令人兴奋,给您的代理虚拟眼睛并让它们解决您可能需要解决的任何问题,这种感觉有点神奇。但这很少是最佳的方法。
当你在构建一个模拟仿真,这个仿真代表某个你随后可能在现实世界中建造的东西时,摄像头显然是最有用的。当那某个东西实际上会使用摄像头时,比如构建一个复杂的自动驾驶汽车、无人机或者拾取放置机器人的仿真,并且计划在实际中使用你生成的模型的一部分或全部时,使用摄像头当然是有道理的。
注意
如果一个代理程序仅仅配备了摄像头,那就像一个只有视觉信息的人类一样——只能接收到我们环境的视觉信息是一个非常复杂的问题。它更好地与其他感官输入配对或互补。代理程序通常有一些动作的目标,但不一定会在物理上改变环境。经常有一些代理程序可以执行的动作不会改变摄像头的输入,这意味着它们可能不会因为环境中没有可见的状态变化而得到任何反馈。如果代理程序仅仅接收视觉观察结果,那么它需要知道由于它的动作而环境发生了某种变化(除了其奖励以外)。
如果你在建造一个纯粹用于模拟的东西,那么在使用摄像头时应该要审慎选择。通常情况下,同时使用摄像头和矢量观察来观察模拟中的相同元素是比较不寻常的,但也有一些情况是合理的:矢量可能会为摄像头增加背景信息,或者提供关于环境的额外信息,这些信息仅从摄像头中无法获取。
例如,如果你正在建造一个拾取放置机器人,它通过摄像头可以看到放在它前面的一袋面条,那么同时使用一个射线投射观察来检测并识别被标记的面条袋,似乎有些奇怪。仅仅图像识别系统就足以处理识别面条袋的任务,而来自射线投射的额外信息,在最好的情况下是多余的,在最坏的情况下,会妨碍训练过程。
基本上,如果你的代理程序在引擎之外(在现实世界中)使用,你应尽可能地模仿它实际上会有的输入;如果你在进行的是纯虚拟的活动,你可以给它一个摄像头,这样可以得到计算机视觉模型作为结果。
你可以给任何代理程序配备摄像头。但这并不意味着它一定会有用。任何不能为代理程序提供更多关于如何实现其目标的信息的观察结果只会让事情变得复杂,导致神经网络的训练变得更慢更困难。你应该尽可能地减少观察结果。
相机非常有趣,当你探索制作自己的模拟时,你绝对应该和它们玩耍。但是当事情变得严肃时,你要小心不要使用过多的相机,或者不要仅仅依赖相机,如果有更好的选择的话。
第十一章:使用 Python 进行工作
在本章中,我们将探索更多使用 Unity 的 ML-Agents 与 Python 结合的可能性。迄今为止,我们所做的一切都集中在 Unity 和(通过 ML-Agents)Python 的组合上,但我们采取了 Unity 为中心的通过 ML-Agents)Python 的组合上,但我们采取了以 Unity 为中心的方法。
本章将介绍几种从 Python 中心化方法完成任务的方式。具体来说,我们将探讨如何使用 Python 与 Unity 和 ML-Agents 交互。
Python 无处不在
我们在整本书中一直在使用 Python,但我们还使用了一个运行训练并将其连接到 Unity 的脚本。深层次来看,PyTorch 也被用于训练。除此之外,并不重要我们使用的是 Python。碰巧我们运行的脚本是由 Python 提供支持的。这可能是任何语言。
在本章中,我们将更多地使用 Python,并掌握将 Python 与 ML-Agents 结合生成的能力,超越提供的脚本。我们将证明,当您运行 mlagents-learn 来训练一个代理时,实际上是在使用 Python,并稍微超出提供的脚本。
在本章中,我们将主要探讨 Python 中的 GridWorld 环境,这是一个简单的 3x3 网格,代理是一个蓝色的正方形,需要触碰绿色的 + 号,但不能触碰红色的 x 号。它采用纯视觉观测系统:一个朝向网格的摄像头。
它的动作是五个离散选项之一:
-
什么也不做
-
向上移动
-
向下移动
-
向右移动
-
向左移动
如果代理触碰到目标(绿色加号),奖励1.0,如果触碰到红色 x,惩罚-1.0。每步还会有一个存在惩罚-0.01。
您可以在 图 11-1 中看到 GridWorld 的样子。

图 11-1。GridWorld,数字前沿
尝试环境
要使用 Python,自然需要设置一个新的 Python 环境。这几乎与我们在“设置”中使用的 Python 环境完全相同,只有一些小的区别。
为了准备好,您需要完成以下步骤:
-
按照 “设置” 中的说明,设置一个新的 Python 环境。
-
配置完成后,按照以下规格安装一些额外的内容。首先,安装 matplotlib:
pip install matplotlib我们将使用 matplotlib 在屏幕上展示一些图像,同时探索 Python 和 ML-Agents。这本书不涵盖 matplotlib 的详细讨论,但如果你搜索 O'Reilly 的学习平台或 DuckDuckGo,会有大量资料。还有一个网站。
-
接下来,安装 Jupyter Lab:
pip install jupyterlab -
点 启动 Jupyter Lab:
jupyterlabJupyter Lab 是一个创建“笔记本”的工具,它允许您以易于运行和编写的形式执行 Python。它通常用于科学计算,您可能已经接触过它,例如 Jupyter Notebooks、IPython 或 Google 的品牌版本 Google Colab。
-
一旦运行,创建一个空白的笔记本,如 图 11-2 所示。
![psml 1102]()
图 11-2. 空白的 Jupyter 笔记本
就这样。
接下来,我们将开始编码,并且我们将在进行时解释情况:
-
立即,我们需要导入 ML-Agents:
import mlagents这将在我们的笔记本中引入基于 Python 的 ML-Agents 软件包。
-
接下来,我们将导入
matplotlib.pyplot,这样我们就可以显示绘图:import matplotlib.pyplot as plot -
我们告诉
matplotlib我们希望它内联显示:%matplotlib inline这确保
matplotlib将在笔记本中内联显示图像。 -
现在我们要询问 ML-Agents 它的默认注册表:
from mlagents_envs.registry import default_registry这提供了一个预构建的 Unity 环境数据库(称为“Unity 环境注册表”),基于 ML-Agents 的示例。这些环境可用于通过 Python API 进行实验,而无需将环境构建为二进制文件,也无需同时运行 Unity。
-
导入默认注册表后,我们可以快速查看我们得到了什么:
environment_names = list(default_registry.keys()) for name in environment_names: print(name)如果您此时运行笔记本(使用“运行”菜单 → 运行所有单元格),您将看到一个环境列表,如 图 11-3 所示。
![psml 1103]()
图 11-3. 环境列表
-
接下来,我们将加载其中一个提供的环境:
env = default_registry["GridWorld"].make()这将从默认环境中加载 GridWorld 环境。
-
载入 GridWorld 环境后,我们首先要做的是询问其行为:
behavior = list(env.behavior_specs)[0] # get the first behavior print(f"The behavior is named: {behavior}") spec = env.behavior_specs[behavior]这会获取环境行为的处理(对应于环境中附加到代理的“行为参数”组件),并打印其名称和团队 ID(因为环境不使用团队,所以团队 ID 将为 0)。
-
接下来,我们将找出它有多少观察结果:
print("The number of observations is: ", len(spec.observation_specs)) -
接着我们将查看是否有视觉观察:
vis_obs = any(len(spec.shape) == 3 for spec in spec.observation_specs) print("Visual observations: ", vis_obs) -
我们还将检查连续和离散动作的数量:
if spec.action_spec.continuous_size > 0: print(f"There are {spec.action_spec.continuous_size} continuous actions") if spec.action_spec.is_discrete(): print(f"There are {spec.action_spec.discrete_size} discrete actions") -
我们将检查离散分支的选项:
if spec.action_spec.discrete_size > 0: for action, branch_size in enumerate(spec.action_spec.discrete_branches): print(f"Action number {action} has {branch_size} different options")
我们可以再次运行笔记本以获取有关其的一些信息。您应该会看到类似于 图 11-4 的内容。

图 11-4. 探索环境
接着,我们将按步骤执行环境:
-
首先,我们将存储环境中的步骤数:
decision_steps, terminal_steps = env.get_steps(behavior) -
我们将设置代理行为的动作,传入我们想要使用的行为和一个维度为 2 的张量:
env.set_actions (behavior, spec.action_spec.empty_action(len(decision_steps))) -
然后,我们将使仿真向前推进一步:
env.step() -
仿真推进一次后,现在是时候看看它能看到什么了。首先,我们将检查是否有任何视觉观察:
for index, obs_spec in enumerate(spec.observation_specs): if len(obs_spec.shape) == 3: print("Here is the first visual observation") plot.imshow(decision_steps.obs[index][0,:,:,:]) plot.show()这将从环境中的一个代理获取第一个视觉观察,并使用
matplotlib显示它。 -
接下来,我们将检查任何向量观察:
for index, obs_spec in enumerate(spec.observation_specs): if len(obs_spec.shape) == 1: print("First vector observations : ", decision_steps.obs[index][0,:]) -
在此时运行笔记本应该会显示代理的第一个视觉观察图像,如图 11-5 所示。
![psml 1105]()
图 11-5. 第一个视觉观察
-
现在我们将使环境通过三个情节:
for episode in range(3): env.reset() decision_steps, terminal_steps = env.get_steps(behavior) tracked_agent = -1 # -1 indicates not yet tracking done = False # For the tracked_agent episode_rewards = 0 # For the tracked_agent while not done: # Track the first agent we see if not tracking # len(decision_steps) = [number of agents that requested a decision] if tracked_agent == -1 and len(decision_steps) >= 1: tracked_agent = decision_steps.agent_id[0] # Generate an action for all agents action = spec.action_spec.random_action(len(decision_steps)) # Set the actions env.set_actions(behavior, action) # Move the simulation forward env.step() # Get the new simulation results decision_steps, terminal_steps = env.get_steps(behavior) if tracked_agent in decision_steps: # The agent requested a decision episode_rewards += decision_steps[tracked_agent].reward if tracked_agent in terminal_steps: # Agent terminated its episode episode_rewards += terminal_steps[tracked_agent].reward done = True print(f"Total rewards for episode {episode} is {episode_rewards}") -
再次运行整个笔记本,您应该看到一些看起来熟悉的训练信息,如图 11-6 所示。
![psml 1106]()
图 11-6. 笔记本中的训练
-
最后,关闭环境:
env.close()
如何使用 Python?
我们使用的mlagents Python 包与我们用来在模拟中训练代理的mlagents-learn脚本相同。
提示
对于本书来说,探索mlagents Python API 的全部内容是超出范围并且完全不必要的。里面有很多东西,我们只是在这里以教程形式为您介绍亮点。但如果您感兴趣,可以在在线文档中找到所有mlagents Python API 的文档。
您可以使用 Python API 来控制、与 Unity 模拟环境交互并获取信息。这意味着您可以开发完全定制的训练和学习算法,而不是依赖通过mlagents-learn脚本使用的提供的算法。
稍后,在第十二章中,我们将探讨如何将 Unity 构建的模拟环境连接到 OpenAI Gym。
使用您自己的环境
当然,您可以使用自己的环境,而不是 Unity 提供的注册表中的示例之一。
在使用您自己的环境之前,您需要构建它。我们将建立 Unity 的一个示例项目,即我们之前使用过的 GridWorld,作为一个示例:
-
打开作为 ML-Agents GitHub 仓库一部分的 ML-Agents Unity 项目,如图 11-7 所示。
![psml 1107]()
图 11-7. ML-Agents 存储库中的项目
-
一旦您进入项目,从项目面板中打开 GridWorld 场景,如图 11-8 所示。
![psml 1108]()
图 11-8. GridWorld 场景
-
为简化操作,请选择并从层次结构视图中删除所有编号区域,如图 11-9 所示。
![psml 1109]()
图 11-9. 您需要删除的区域
这些是用于通过同时训练多个代理以加快训练速度的主区域的副本。因为我们将在 Python 中尝试这个环境,所以我们只想要一个区域。
-
接下来,通过编辑菜单 → 项目设置 → 玩家打开玩家设置。找到分辨率和呈现部分,如图 11-10 所示,并选中“在后台运行”。关闭玩家设置。
![psml 1110]()
图 11-10. 玩家设置
-
打开文件菜单中的“构建设置”从 File → Build Settings,如 图 11-11 所示。选择您要在其上运行此程序的平台(我们在 MacBook Pro 上进行截图),并确保 GridWorld 场景是列表中唯一选中的场景。
提示
如果场景列表为空,则只会构建当前打开的场景。这也没关系。
![psml 1111]()
图 11-11. 构建设置
-
点击构建按钮,并将结果输出保存到您熟悉的路径,如 图 11-12 所示。
注意
在 macOS 上,保存的输出将是一个标准的 .app 文件。在 Windows 上,输出将是一个包含可执行文件的文件夹。
![psml 1112]()
图 11-12. 选择构建位置
-
Unity 将构建环境,如 图 11-13 所示。
![psml 1113]()
图 11-13. 构建正在进行
完全自定义训练
因为 Python 包实际上只是一个 API,用来控制 Unity 模拟环境中发生的进程,我们实际上可以用它来替换 mlagents-learn 脚本提供的训练过程。在本节中,我们将快速查看一个主要基于 Unity 示例之一的示例。
在开始之前,您需要设置好 Python 环境,如 “设置” 所述。完成后,请确保 Jupyter Lab 正在运行,并准备继续。
在这个示例中,我们将再次使用 Unity 的 GridWorld 示例环境,而不是使用 Unity 提供的训练算法之一来训练它,而是使用 Q-learning 进行训练。Q-learning 是一种无模型的强化学习算法,旨在学习特定状态下行动的价值。
提示
Q-learning 的“无模型”方面指的是 Q-learning 不需要环境的模型。一切都与状态和动作相关,而不是对环境特定理解的具体模型。Unity ML-Agents 使用的标准算法 PPO 也是无模型的。探索 Q-learning 的具体细节超出了本书的范围。
我们将在这里通过提供一些代码来启动这个过程,因为代码有点多。在您下载的书籍资源中找到 PracticalSims_CustomTraining.ipynb 笔记本,并将其加载到 Jupyter Lab 中。
让我们来看看这个笔记本中的代码都做了些什么:
-
导入语句引入了
mlagents,如往常一样,以及torch(PyTorch)、一些 Python 的math和typing(Python 的类型提示库),以及numpy。 -
然后,创建一个表示将进行训练的神经网络的类:
VisualQNetwork。这个类定义了一个神经网络,它以图像作为输入,并输出一组数字。 -
接下来,我们创建一个
Experience类,它存储一个动作、观察、奖励组合。将存储为Experience的经验将用于训练将要制作的神经网络。 -
现在,笔记本的主要部分,
Trainer类,将从我们的 Unity 环境中获取数据,并生成Experience对象的缓冲区,使用VisualQNetwork的策略。
有了提供的设置,我们将一起编写训练循环。和往常一样,我们将逐步进行,因为了解发生的情况很重要:
-
首先,我们将确保关闭任何现有的环境:
try: env.close() except: pass -
然后,我们将从 Unity 默认注册表获取一个 GridWorld:
env = default_registry["GridWorld"].make() -
现在,我们将创建前面讨论过的
VisualQNetwork的实例(使用笔记本中早期定义的类):qnet = VisualQNetwork((64, 84, 3), 126, 5) -
我们将创建一个
Buffer(早期定义,使用Experience)来存储经验:experiences: Buffer = [] -
我们还将创建一个优化器,这只是一个标准的 Adam 优化器,直接来自 PyTorch:
optim = torch.optim.Adam(qnet.parameters(), lr= 0.001) -
我们将创建一个浮点数列表来存储累积奖励:
cumulative_rewards: List[float] = [] -
接下来,我们将定义一些环境变量,如我们希望的训练步数,我们希望每个训练步收集的新经验数量,以及缓冲区的最大大小:
NUM_TRAINING_STEPS = int(os.getenv('QLEARNING_NUM_TRAINING_STEPS', 70)) NUM_NEW_EXP = int(os.getenv('QLEARNING_NUM_NEW_EXP', 1000)) BUFFER_SIZE = int(os.getenv('QLEARNING_BUFFER_SIZE', 10000)) -
最后(当然不是最后一个),我们将编写训练循环:
for n in range(NUM_TRAINING_STEPS): new_exp,_ = Trainer.generate_trajectories(env, qnet, NUM_NEW_EXP, epsilon=0.1) random.shuffle(experiences) if len(experiences) > BUFFER_SIZE: experiences = experiences[:BUFFER_SIZE] experiences.extend(new_exp) Trainer.update_q_net(qnet, optim, experiences, 5) _, rewards = Trainer.generate_trajectories(env, qnet, 100, epsilon=0) cumulative_rewards.append(rewards) print("Training step ", n+1, "\treward ", rewards)我们的训练循环迭代到我们定义的最大训练步数,每一步创建一个新的经验,将其存储在缓冲区中,更新模型,更新奖励,然后继续。这是一个非常标准的训练循环。
-
最后,我们关闭环境,并使用
matplotlib绘制一个漂亮的训练图表:env.close() plt.plot(range(NUM_TRAINING_STEPS), cumulative_rewards)
有了它,我们可以运行笔记本,并等待一段时间,因为它在训练。
Python 的意义是什么?
ML-Agents Toolkit 实际上是完全通过 Python 控制模拟的强大方法。如果不想,您完全不需要将其用于机器学习。
当然,您可以将其用于机器学习。ML-Agents 的 Python API 组件在与定义超参数的 YAML 文件配合使用时非常有用。如果您想要超越 mlagents-learn 命令自动执行的内容(与算法和场景的限制),您可以创建完全定制的训练管道,使用 PyTorch(或 TensorFlow,或您能找到的任何东西)的所有出色功能来训练生活在 Unity 模拟中的实体。
您还可以使用 Python API 添加额外的步骤来训练和学习过程中,根据需要在引擎观察发生之前或之后注入对专业领域库的调用。
第十二章:在引擎盖和更远的地方
在本章中,我们将涉及一些我们在前几章中用于模拟的方法。
我们已经概述了这个要点:在基于仿真的代理学习中,代理经历一个训练过程,为其行为开发一个策略。策略充当了从先前的观察到其响应的动作及相应奖励的映射。训练发生在大量的回合中,期间累积奖励应随着代理在给定任务中的改进而增加,部分受超参数的控制,这些超参数控制训练期间代理行为的各个方面,包括生成行为模型的算法。
一旦训练完成,推理被用来查询训练代理模型以响应给定刺激(观察)的适当行为(动作),但学习已经停止,因此代理不再会在给定任务上改进。
我们已经讨论了大部分这些概念:
-
我们了解观察、动作和奖励以及它们之间映射如何用于建立策略。
-
我们知道训练阶段会在大量的回合中进行,一旦完成,代理就会转向推理(仅查询模型,不再更新它)。
-
我们知道我们将超参数文件传递给
mlagents -learn过程,但我们在这部分有点草率。 -
我们知道在训练过程中有不同的算法可供选择,但也许并不清楚为什么会选择其中的特定选项。
因此,本节将进一步探讨 ML-Agents 中可用的超参数和算法,以及何时以及为何选择使用它们中的每一个,以及它们如何影响您在训练中的其他选择,如奖励方案的选择。
超参数(和参数)
当使用 ML-Agents 开始训练时,需要传递一个 YAML 文件,其中包含必要的超参数。这在机器学习世界通常被称为超参数文件,但其中不仅包含这些,因此 ML-Agents 文档更倾向于将其称为配置文件。其中包含在学习过程中使用的变量,其值将执行以下操作之一:
-
指定训练过程的各个方面(“参数”)
-
更改代理或模型本身在学习过程中的行为(“超参数”)
参数
常见配置的训练参数包括以下内容:
trainer_type
训练中使用的算法,从ppo、sac或poca中选择
max_steps
在一个回合结束之前,代理可以接收的最大观察次数或执行的动作数量,无论是否已实现目标
checkpoint_interval和keep_checkpoints
训练过程中输出重复/备份模型的频率以及保留的最新模型数量
summary_freq
如何定期输出(或发送到 TensorBoard)有关训练进展的详细信息。
network_settings及其对应的子参数
允许您指定代表代理策略的神经网络的一些大小、形状或行为。
选择trainer_type将取决于您的代理和环境的各个方面。我们将在接下来的章节中深入讨论这些算法的内部工作原理。
定义max_steps很重要,因为如果模拟运行了数千次或数百万次——如在模型训练过程中发生的那样——很可能在某个时刻代理会陷入无法恢复的状态。如果没有强制限制,代理将保持在这种状态,并持续污染其行为模型,使用不相关或不具代表性的数据,直到 Unity 耗尽可用内存或用户终止进程。这不是理想的情况。相反,应使用经验或估计来确定一个数字,允许一个缓慢前进的代理实现目标,但不允许过度挣扎。
例如,假设您正在训练一辆自动驾驶车辆,需要穿过一个赛道并达到目标位置,先前训练过的模型或测试运行告诉您,理想的运行需要大约 5,000 步才能到达目标位置。如果将max_steps设置为500,000,那么一个无用的情节,即代理未能达成任何成就,将导致信息量增加十倍,这很可能会混淆模型的过程。但是,如果将max_steps设置为5,000或更低,模型将永远没有机会取得中等结果,而是每次都会在很短的时间内结束尝试,直到最终(也许)偶然达到了完美的无先验知识情节。这种可能性极小。在这些数字之间选择最佳的策略;例如,在这个例子中大约为10,000步。对于您自己的代理,理想值将取决于其执行任务的复杂性。
检查点功能允许在每次checkpoint_interval更改时保存中间模型,始终保留最后keep_checkpoints个模型。这意味着,如果您发现代理在训练结束前表现良好,您可以终止训练过程,仅使用最新的检查点。还可以从检查点恢复训练,允许从一种算法开始训练,然后切换到另一种——例如在第一章中提到的从 BC 到 GAIL 的示例。
访问network_settings可用于设置决定行为模型神经网络形状的子参数。如果要在存储或计算资源有限的环境(例如边缘设备)中使用生成的模型,则这对于减小模型大小非常有帮助,例如通过network_settings→num_layers或network_settings->hidden_units。其他设置可以更精细地控制特定方面,如用于解释视觉输入的方法,或者是否对连续观测进行归一化。
奖励参数
接下来需要定义进一步的参数来指定如何在训练期间处理奖励。这些参数属于reward_signals。
在使用明确奖励时,必须设置reward_signals->extrinsic->strength和extrinsic->gamma。在这里,strength只是一个应用于通过AddReward或SetReward调用发送的奖励的比例,如果您正在尝试混合学习方法或其他外在和内在奖励的组合,则可能希望减少这一比例。
同时,gamma是应用于奖励估计的比例,基于达到该奖励所需的时间。当代理考虑下一步该做什么时会使用这一值,基于它认为对每个选项将会收到的奖励。gamma可以被视为一个衡量代理应该在长期获益与短期获益之间优先考虑多少的指标。代理是否应该放弃未来几步的奖励,以希望在最后实现更大的目标并获得更大的奖励?还是应该立即做出能立即获得奖励的选择?这个选择将取决于您的奖励方案以及代理要完成任务的复杂性,但通常较高的值(表明更多的长期思考)倾向于产生更智能的代理。
还可以启用其他类型的奖励,这些奖励直接来自训练过程,并被称为内在奖励——类似于模仿学习方法使用的那些奖励。但是,IL 奖励代理与展示行为的相似性,而这些奖励鼓励了一般属性,几乎像某种特定代理的个性。
最普遍适用的内在奖励是curiosity,由reward_signals->curiosity定义。代理的好奇心意味着优先级别映射未知(即尝试新事物并查看其得分)超过已知良好动作(即过去给他们带来得分的动作)。通过基于动作的新颖程度或其结果的意外性的奖励来鼓励好奇心,这有助于避免稀疏奖励环境中常见的局部最大化问题。
例如,一个代理可能被设计成寻找并站在一个平台上以打开一个门,然后穿过打开的门到达目标。为了激励每一步并加快训练速度,你可以给予代理站在平台上的奖励,并对达到目标后给予指数级增长的奖励。但一个不好奇的代理可能会意识到它在第一步获得了奖励,并决定最佳行动是不断地在平台上站立直至每一集结束,最终结束训练。这是因为它知道站在平台上是好的,而它认为其他任何行动的结果(在它看来可能是无限的)是未知的。因此,它将坚持自己擅长的行为。这就是为什么多阶段目标通常需要引入人工好奇心,使代理更愿意尝试新事物。
要启用好奇心,只需传递与外在奖励所需的相同的超参数:
reward_signals->curiosity->strength
当试图平衡好奇奖励与其他奖励(如外在奖励)时,需要按比例缩放好奇奖励的数字(必须在0.0和1.0之间)。
reward_signals->curiosity->gamma
第二个尺度用于根据实现所需时间来调节奖励的感知价值,与extrinsic->gamma中的相同(同样在0.0和1.0之间)。
其他不太常用的内在奖励信号可以用来引入其他代理倾向,例如随机网络蒸馏,或者启用特定的学习类型,例如 GAIL。
超参数
常见配置的模型超参数包括:
batch_size
控制模型每次更新时模拟执行的迭代次数。如果你完全使用连续动作,batch_size应该很大(数千),如果只使用离散动作,则应该很小(十几)。
buffer_size
取决于算法的不同,控制不同的事物。对于 PPO 和 MA-POCA,它控制在更新模型之前收集的经验数量(应该是batch_size的几倍)。对于 SAC,buffer_size对应于经验缓冲区的最大大小,因此 SAC 可以从旧的和新的经验中学习。
learning_rate
对模型的影响每次更新的程度。通常介于1e-5和1e-3之间。
提示
如果训练不稳定(换句话说,奖励不一致增加),尝试降低learning_rate。较大的buffer_size也将对训练的稳定性有所帮助。
一些超参数是特定于所使用的训练器的。让我们从您可能经常使用的训练器(PPO)的重要参数开始:
-
beta鼓励探索。在这里,较高的beta值将导致与好奇心内在奖励类似的结果,因为它鼓励代理尝试新事物,即使早期已发现了奖励。在简单环境中更倾向于较低的beta值,因此代理将倾向于限制其行为在过去曾有益的行为上,这可能会减少训练时间。 -
epsilon决定了行为模型对变化的接受程度。在这里,较高的epsilon值将允许代理快速采纳新行为一旦发现奖励,但这也意味着代理的行为容易改变,甚至在训练后期也是如此。较低的epsilon值意味着代理需要更多尝试才能从经验中学习,可能会导致更长的训练时间,但会确保在训练后期行为一致。 -
调度超参数如
beta_schedule和epsilon_schedule可用于在训练过程中更改其他超参数的值,例如在早期训练中优先考虑beta(好奇心),或在后期减少epsilon(善变性)。
小贴士
POCA/MA-POCA 使用与 PPO 相同的超参数,但是SAC 有一些专门的超参数。
欲知当前 ML-Agents 支持的参数和超参数的完整列表,请参阅ML-Agents 文档。
小贴士
如果你不太了解(或者不想了解)你选择的模型所需的超参数是什么意思,你可以查看 GitHub 上每种训练器类型的ML-Agents 示例文件。一旦你看到训练的过程,如果你遇到像这里描述的一些特定于超参数的问题,你可以选择调整这些具体的数值。
算法
Unity ML-Agents 框架允许代理通过优化定义在以下方式之一的奖励来学习行为:
明确地(外在奖励)由你
在我们使用AddReward或SetReward方法的 RL 方法中:当我们知道代理做对了某事时(或者做错了时)我们给予奖励(或惩罚)。
隐含地(内在奖励)由选择的算法
在 IL 方法中,基于与提供的行为演示的相似性来给予奖励:我们向代理展示行为,它试图克隆它,并根据其克隆的质量自动获得奖励。
隐含地由训练过程
我们讨论的超参数设置,其中代理可以因展现某些属性(如好奇心)而受到奖励。在这本书中,我们并没有深入讨论这个,因为这超出了本书的范围。
但这并不是 ML-Agents 中可用算法之间的唯一区别。
近端策略优化(PPO) 可能是在使用 Unity 进行 ML 工作时最明智的默认选择。PPO 试图逼近一个理想函数,将代理的观察映射到给定状态下可能的最佳行动。它被设计为通用算法。它可能不是最有效的,但通常可以完成任务。这就是为什么 Unity ML-Agents 将其作为默认选项的原因。
软策演员-评论者(SAC) 是一种离策略的强化学习算法。这基本上意味着可以单独定义最佳训练行为和最佳结果代理行为。这可以减少代理达到最佳行为所需的训练时间,因为可以在训练期间鼓励一些可能在训练中可取但在最终代理行为模型中不可取的特征。
这种属性的最佳例子是好奇心。在训练期间,好奇心的探索是很好的,因为你不希望你的代理只发现一个给它点数的东西,然后再也不尝试其他任何事物。但是一旦模型训练完毕,这种探索就不那么理想了,因为如果训练如期进行,它已经发现了所有理想的行为。
因此,SAC 在训练速度上可能更快,但相比于像 PPO 这样的在策略方法,需要更多的内存来存储和更新单独的行为模型。
注意
有关于 PPO 是在策略还是离策略的争论。我们倾向于将其视为在策略上,因为它基于遵循当前策略进行更新。
多代理人死后信用分配(POCA 或 MA-POCA) 是一种多代理算法,使用集中的评论者来奖励和惩罚一组代理。奖励类似于基本的 PPO,但是奖励给评论者。代理应该学会如何最好地贡献以获得奖励,但也可以单独奖励。它被认为是死后的,因为代理在学习过程中可以从代理组中移除,但仍然会学习其行为对组获得奖励的贡献,即使在被移除后也是如此。这意味着代理可以采取对组有益的行动,即使这些行动会导致它们自身的死亡。
我们在第九章中使用了MA-POCA。
Unity 推理引擎和集成
在代理训练期间,代表代理行为的神经网络会随着代理执行动作并接收奖励形式的反馈而不断更新。这通常是一个漫长的过程,因为神经网络图可能非常庞大,调整所需的计算量会随其大小而增加。同样,使代理在所需任务中持续成功所需的剧集数量通常在数十万甚至数百万。
因此,在 ML-Agents 中训练一个中等复杂度的代理可能会占用个人电脑数小时甚至数天。然而,一个训练过的代理可以轻松地包含在 Unity 游戏中或导出用于简单的应用程序中。那么,他们在训练后的使用如何变得更加可行呢?
提示
如果您想要在 ML-Agents 外部训练用于 ML-Agents 的模型,请首先了解Tensor 名称和Barracuda 模型参数。这超出了本书的范围,但非常有趣!
答案在于训练期间所需的性能与推理期间的差异。在训练阶段后,代理行为的神经网络被锁定在一个位置;随着代理执行动作,它将不再更新,奖励也不再作为反馈发送。相反,将以与训练期间相同的方式向代理提供相同的观察,但是定义哪些观察将与哪些动作响应的规则已经定义好了。
弄清楚对应的反应就像追踪一个图形一样简单。因此,推理是一个高效的过程,即使在计算资源有限的应用程序中也可以包含。所需的只是一个推理引擎,它知道如何接受输入,追踪网络图,并输出适当的操作。
注意
Unity ML-Agents 推理引擎是使用计算着色器实现的,这些着色器是在 GPU 上运行的小型专用程序(也称为图形卡),但不用于图形处理。这意味着它们可能无法在所有平台上工作。
幸运的是,Unity ML-Agents 自带一个称为Unity 推理引擎(有时称为 Barracuda)的推理引擎。因此,你不需要制作自己的引擎,或者在训练时使用的底层框架(如 PyTorch 或 TensorFlow)。
提示
您可以在Unity 文档中了解更多关于 Barracuda 的信息。
如果你在 ML-Agents 外部训练了一个模型,你将无法使用它与 Unity 的推理引擎。理论上,你可以创建一个符合 ML-Agents 期望的常量和张量名称的模型,但这并没有得到官方支持。
警告
机器学习代理生成的模型,使用 CPU 运行推理速度可能比使用 GPU 更快,这可能有点违反直觉(或者对于你的背景来说可能很直观),除非你的代理有大量的视觉观察。
使用 ML-Agents Gym 包装器
OpenAI Gym 是一个(几乎成为事实上的标准)用于开发和探索强化学习算法的开源库。在本节中,我们将快速了解使用 ML-Agents Gym 包装器来探索强化学习算法。
在开始使用 ML-Agents Gym Wrapper 之前,您需要设置好 Python 和 Unity 环境。因此,如果您还没有这样做,请通过“设置”的步骤创建一个新的环境。完成这些步骤后,继续执行以下操作:
-
激活您的新环境,然后安装
gym_unityPython 包:pip install gym_unity -
然后,您可以从任何 Python 脚本中启动 Unity 仿真环境作为 gym:
from gym_unity.envs import UnityToGymWrapper env = UnityToGymWrapper (unity_env, uint8_visual, flatten_branched, allow_multiple_obs)
在这种情况下,unity_env是要包装并作为 gym 呈现的 Unity 环境。就是这样!
Unity 环境和 OpenAI Baselines
OpenAI 项目中最有趣的组成部分之一是 OpenAI Baselines,这是一组高质量的强化学习算法实现。现在处于维护模式,但它仍然提供了一系列非常有用的算法,供您探索强化学习。
方便的是,您可以通过 Unity ML-Agents Gym Wrapper 与 Unity 仿真环境一起使用 OpenAI Baselines。
作为一个快速示例,我们将使用 OpenAI 的 DQN 算法来训练我们在第十一章中使用的 GridWorld。
首先,您需要构建 GridWorld 环境的一个副本:
-
在您克隆或下载的 ML-Agents GitHub 存储库的副本中,打开项目文件夹(参见“设置”),作为 Unity 项目使用 Unity Hub 打开,并使用项目视图打开 GridWorld 场景。
-
然后打开“文件”菜单 → “构建设置”,选择您当前的平台。
-
确保“场景构建列表”中仅选中了 GridWorld 场景。
-
点击“构建”并选择一个您熟悉的位置保存构建。
提示
您可能会想知道为什么我们不能使用默认的注册表来获取 GridWorld 的副本,因为我们在这里专门使用 Python。原因是 ML-Agents Gym Wrapper 只支持存在单个代理的环境。所有预构建的默认注册表环境都有多个区域,以加快训练速度。
接下来,我们将转到 Python:
-
在您的 Python 环境中,您需要安装 Baselines 包:
pip install git+git://github.com/openai/baselines警告
您可能需要在执行此操作之前安装 TensorFlow,通过
pip install tensorflow==1.15。您将需要这个特定版本的 TensorFlow 以保持与 OpenAI Baselines 的兼容性:特别是它使用了 TensorFlow 的contrib模块,该模块不是 TensorFlow 2.0 的一部分。这就是 Python 的乐趣所在。 -
接下来,启动 Jupyter Lab,按照我们在“尝试环境”中使用的过程创建一个新的笔记本。
-
添加以下
import行:import gym from baselines import deepq from baselines import logger from mlagents_envs.environment import UnityEnvironment from gym_unity.envs import UnityToGymWrapper -
接下来,获取一下我们刚刚构建的 Unity 环境,并将其转换为 gym 环境:
unity_env = UnityEnvironment("/Users/parisba/Downloads/GridWorld.app", 10000, 1) env = UnityToGymWrapper(unity_env, uint8_visual=True) logger.configure('./logs') # Change to log in a different directory注意,
/Users/parisba/Downloads/GridWorld.app的等效部分应该指向一个.app或.exe或其他可执行文件(取决于您的平台),这是我们刚刚制作的 GridWorld 的构建副本。 -
最后,运行训练:
act = deepq.learn( env, "cnn", # For visual inputs lr=2.5e-4, total_timesteps=1000000, buffer_size=50000, exploration_fraction=0.05, exploration_final_eps=0.1, print_freq=20, train_freq=5, learning_starts=20000, target_network_update_freq=50, gamma=0.99, prioritized_replay=False, checkpoint_freq=1000, checkpoint_path='./logs', # Save directory dueling=True ) print("Saving model to unity_model.pkl") act.save("unity_model.pkl")
你的环境将启动,并将使用 OpenAI Baselines DQN 算法进行训练。
Side Channels
Unity 的 Python ML-Agents 组件提供了一个名为“side channels”的功能,允许你在运行在 Unity 中的 C#代码和 Python 代码之间双向共享任意信息。具体来说,ML-Agents 提供了两个可供使用的 side channels:EngineConfigurationChannel和EnvironmentParametersChannel。
引擎配置通道
引擎配置通道允许你变化与引擎相关的参数:时间尺度、图形质量、分辨率等。它旨在通过变化质量来提高训练性能,或者在推断期间使事物更漂亮、更有趣或更有用以供人类审查。
按照以下步骤创建一个EngineConfigurationChannel:
-
确保以下内容包含在你的
import语句中:from mlagents_envs.environment import UnityEnvironment from mlagents_envs.side_channel.engine_configuration_channel import EngineConfigurationChannel -
创建一个
EngineConfigurationChannel:channel = EngineConfigurationChannel() -
将通道传递给你正在使用的
UnityEnvironment:env = UnityEnvironment(side_channels=[channel]) -
根据需要配置通道:
channel.set_configuration_parameters(time_scale = 2.0)
在这种情况下,这个EngineConfigurationChannel的配置将time_scale设置为2.0。
就这样!有一系列可能用于set_configuration_parameters的参数,比如用于分辨率控制的width和height,quality_level和target_frame_rate。
环境参数通道
环境参数通道比引擎配置通道更通用;它允许你在 Python 和仿真环境之间传递任何需要的数值值。
按照以下步骤创建一个EnvironmentParametersChannel:
-
确保你拥有以下的
import语句:from mlagents_envs.environment import UnityEnvironment from mlagents_envs.side_channel.environment_parameters_channel import EnvironmentParametersChannel -
创建一个
EnvironmentParametersChannel并将其传递给UnityEnvironment,就像我们对引擎配置通道所做的那样:channel = EnvironmentParametersChannel() env = UnityEnvironment(side_channels=[channel]) -
接下来,在 Python 端使用该通道,命名一个参数为
set_float_parameter:channel.set_float_parameter("myParam", 11.0)在这种情况下,参数被命名为
myParam。 -
这允许你从 Unity 中的 C#访问相同的参数:
var environment_parameters = Academy.Instance.EnvironmentParameters; float myParameterValue = envParameters.GetWithDefault("myParam", 0.0f);
这里调用中的0.0f是一个默认值。
到此为止,我们完成了本章的内容,也基本完成了书籍中的模拟。在代码下载中提供了一些下一步操作;如果你对强化学习感兴趣并希望进一步探索,请打开资源包中书籍网站的 Next_Steps 文件夹。
第三部分:合成数据,真实结果
第十三章:创建更高级的合成数据
在本章中,我们将回顾合成,并在 Unity 的感知使用中进行深入讨论,这是我们在第三章中进行的介绍的延续。
具体来说,我们将使用随机器人向我们的骰子生成的图像添加一个随机元素,并学习如何探索我们正在合成的数据,利用我们之前添加的标签。
向场景添加随机元素
要生成有用的合成数据,我们需要向场景添加随机元素。我们要添加的随机元素包括:
-
一个随机的地板颜色
-
一个随机的摄像机位置
通过随机改变地板的颜色和摄像机的位置,我们能够生成各种随机的骰子图像,这些图像可以用于在 Unity 之外的图像识别系统中训练,以识别各种情况下的骰子。
我们将继续使用我们在第三章结束时得到的同一个项目,因此在继续之前,请复制它或从头开始重新创建。我们复制了它并将其重命名为“SimpleDiceWithRandomizers”。
提示
切记这 记住,项目必须是一个 3D URP 项目,这与你在第二部分中制作的仿真项目不同。如果需要提醒,请参考“创建 Unity 项目”。
随机化地板颜色
要随机化地板颜色,我们首先需要一个随机器。要添加随机器,请打开 Unity 场景并执行以下操作:
-
找到附加到 Scenario 对象的 Scenario 组件,并单击图 13-1 中显示的添加随机器按钮。
![psml 1301]()
图 13-1. 添加一个随机器
-
选择 g:perceptioncategory)所示,选择感知类别,并如图 13-3 所示,选择颜色随机器。
![psml 1302]()
图 13-2. 选择感知类别
![psml 1303]()
图 13-3. 选择颜色随机器
颜色随机器需要知道它应该改变颜色的对象。为此,我们需要在地板平面上(我们希望改变颜色的对象)添加一个颜色随机器标签组件:
-
在 Hierarchy 面板中选择地板,并使用其 Inspector 添加一个颜色随机器标签组件。
-
确保已将其添加到对象中,如图 13-4 所示。
![psml 1304]()
图 13-4. 颜色随机器标签
就这些。为了测试随机器是否起作用,请运行项目,并检查“测试场景”中记录的文件系统位置。
如果一切正常,你会发现骰子图片有各种不同颜色的背景,如图 13-5 所示。

图 13-5. 随机平面颜色(如果您正在阅读印刷版本,则将以灰度显示)
随机化摄像机位置
接下来,我们将在捕获保存图像的摄像机位置上添加一个随机元素。
要随机化摄像机的位置,我们需要一个不随 Unity Perception 包提供的随机器。为此,我们将编写我们自己的随机器。
注意
随机器是附加到场景的脚本。随机器封装了在环境执行期间执行的特定随机化活动。每个随机器向检视器公开特定参数。
可以通过创建一个新的脚本,该脚本派生自Randomizer类,并根据需要实现该类中的方法来创建一个新的随机器。
您可以重写的一些方法包括:
-
OnCreate(),在场景加载随机器时调用 -
OnIterationStart(),在场景启动迭代时调用 -
OnIterationEnd(),在场景完成迭代时调用 -
OnScenarioComplete(),在场景完成时调用 -
OnStartRunning(),在启用随机器的第一帧时调用 -
OnUpdate(),该方法在每一帧都会被调用
例如,这是我们刚才使用的ColorRandomizer的代码,它是作为 Unity Perception 包的一部分创建和提供的:
[AddRandomizerMenu("Perception/Color Randomizer")]
public class ColorRandomizer : Randomizer
{
static readonly int k_BaseColor = Shader.PropertyToID("_BaseColor");
public ColorHsvaParameter colorParameter;
protected override void OnIterationStart()
{
var taggedObjects = tagManager.Query<ColorRandomizerTag>();
foreach (var taggedObject in taggedObjects)
{
var renderer = taggedObject.GetComponent<Renderer>();
renderer.material.SetColor(k_BaseColor, colorParameter.Sample());
}
}
}
注意
每个随机器都必须有[Serializable]标签,以便 Unity 编辑器可以将其自定义并保存为其 UI 的一部分。您可以在 Unity 的文档中了解有关此标签的更多信息。
重要的是要包含[AddRandomizerMenu]属性,并为随机器指定一个路径,使其显示为 Figure 13-6 中所示。

图 13-6. 再次选择颜色随机器
遵循以下步骤来创建您自己的随机器:
-
通过右键单击项目窗格中的空白处,选择创建 → C# 脚本来创建一个新的脚本。
-
将新脚本命名为 CamRandomizer.cs,然后打开它,删除除提供的导入行以外的所有内容。
-
添加以下导入项:
using UnityEngine.Experimental.Perception.Randomization.Parameters; using UnityEngine.Experimental.Perception.Randomization.Randomizers; -
在类之外并在任何方法之上添加上述属性,使其出现在子菜单中:
[AddRandomizerMenu("Perception/Cam Randomizer")] -
添加从
Randomizer派生的类:public class CamRandomizer : Randomizer { } -
创建一个存储场景摄像机引用的位置,以便您可以使用随机器进行移动:
public Camera cam; -
创建一个
FloatParameter,以便在 Unity 编辑器中定义摄像机x位置的范围:public FloatParameter camX; -
接下来,覆盖前面提到的
OnIterationStart()方法,使用它来Sample()刚刚创建的camX参数,并定位摄像机:protected override void OnIterationStart() { cam.transform.position = new Vector3(camX.Sample(),18.62f,0.72f); }
脚本编写完成后,您需要将其添加到场景中:
-
从层级中选择场景,再次使用“添加随机器”按钮,但这次要找到你新创建的相机随机器,如图 13-7 所示。
![psml 1307]()
图 13-7. 新创建的相机随机器
-
找到相机随机器的设置,将范围设置在
-7到7之间,如图 13-8 所示。![psml 1308]()
图 13-8. 相机随机器设置
-
将主摄像机拖入相机随机器的摄像机字段中。
通过运行场景来测试随机器。这一次,相机的位置以及地板的颜色将是随机的,如图 13-9 所示。

图 13-9. 随机生成的图像,具有随机颜色和相机位置
接下来是什么?
我们已经在两个与模拟相关的章节中涵盖了 Unity 合成的所有基本原理。接下来,我们将结合我们所有的新知识,并构建一个用于训练图像识别系统的数据集(再次强调,这种训练将在 Unity 之外进行,详细步骤超出本书的范围)。
第十四章:合成购物
您已经初步了解了如何使用 Unity 生成自定义合成数据集,但仅仅是触及到了皮毛。
在本章中,我们将结合迄今学到的内容,进一步探索 Unity Perception 的可能性和特性,并讨论如何将其应用到您自己的项目中。
具体来说,我们将使用 Unity 和 Perception 创建一个完整功能的合成数据集:一个在超市可能找到的项目集,精心注释和标记。
想象一下,一个由人工智能驱动的购物车,它知道您触摸的物品,当您从货架上拿出它们时(您不必费力去想象它,因为它是真实存在的!)。为了训练这样一个东西,您需要一个大量的数据集,显示超市里的产品包装。您需要各种角度的包装图像,以及它们背后各种物品的图像,并且需要对它们进行标记,以便在使用它们训练模型时,能够准确地进行训练。
我们将在本章中创建该数据集。
创建 Unity 环境
首先,我们需要在 Unity 中构建一个世界,用于创建我们的随机商店图像。在这种情况下,世界将是一个场景,我们向其中添加随机器,以创建我们需要的图像范围。
要启动和运行 Unity 环境,请按照以下步骤操作:
-
创建一个全新的 Unity 项目,再次选择 Universal Render Pipeline(URP)模板,如图 14-1 所示。我们的项目名为“SyntheticShopping”,但您可以自由发挥创意。
![psml 1401]()
图 14-1. 一个新的 URP 项目
-
项目打开后,使用 Unity Package Manager 安装 Unity Perception 包,如图 14-2 所示。
![psml 1402]()
图 14-2. 添加 Perception 包
提示
您可以按名称添加该包,
com.unity.perception,浏览包存储库,或手动下载并安装。 -
在包管理器窗口中,单击教程文件旁边的导入按钮,同时选中 Unity Perception 包。这将导入一系列有用的图像和模型到项目中。本章我们将使用它们。如图 14-3 所示。
![psml 1403]()
图 14-3. 导入教程文件
-
在项目窗口中创建一个新场景,如图 14-4 所示。将其命名为“SyntheticShop”或类似的名称。
![psml 1404]()
图 14-4. 一个新场景
-
打开新的空场景。您的 Unity 屏幕应该看起来像图 14-4。
-
然后,在项目窗口中找到 ForwardRenderer 资产,如图 14-5 所示。
![psml 1405]()
图 14-5. ForwardRenderer 资产
-
选择 ForwardRenderer 资产,在 Inspector 中点击添加渲染特性按钮,并选择 Ground Truth Renderer Feature,如 图 14-6 所示。
![psml 1406]()
图 14-6. 配置前向渲染器
到此为止我们所需的一切;接下来我们需要添加一个感知摄像机。
一个感知摄像机
为了允许地面真实标注,我们需要将感知摄像机添加到 SyntheticShop 场景中的主摄像机。
感知摄像机是生成图像所用的相机或视图。感知摄像机所见的内容最终将呈现为为您生成的每个图像。
要在继续之前测试标签器,请在 Unity 中执行以下步骤:
-
在 SyntheticShop 场景的 Hierarchy 中选择 Main Camera,并在其 Inspector 中使用添加组件按钮添加 Perception Camera 组件,如 图 14-7 所示。
![psml 1407]()
图 14-7. 将感知摄像机添加到主摄像机
-
接下来,在 Perception Camera 组件的 Inspector 中,在 Camera Labelers 部分下选择 + 按钮,并添加一个 BoundingBox2DLabeler,如图 14-8 和 14-9 所示。
![psml 1408]()
图 14-8. 添加 BoundingBox2DLabeler
![psml 1409]()
图 14-9. 标签器
-
现在我们需要创建一个新的资产来命名这些标签。在 Project 面板中创建一个新的 ID Label Config 资产,如 图 14-10 所示。我们命名为“SyntheticShoppingLabels”。
![psml 1410]()
图 14-10. 创建 ID Label Config 资产
-
在 Project 窗格中选择新资产,并在 Inspector 中找到添加所有标签到配置按钮(显示在 图 14-11)以将之前导入的样本数据中的标签添加到配置。
![psml 1411]()
图 14-11. 标签和添加所有标签到配置按钮
注意
我们刚刚添加的标签来自导入资产上的标签组件。因为资产有标签,但我们没有一个 ID Label Config 资产来确认和包含这些标签,所以我们需要创建一个并将它们添加进去。
-
确认标签已移动到已添加标签部分,如 图 14-12 所示。
![psml 1412]()
图 14-12. 标签已添加
-
再次在 Hierarchy 中选择 Main Camera,回到 Perception Camera 组件。将 SyntheticShoppingLabels 资产拖放到 Id Label Config 字段中(或使用按钮,如 图 14-13 所示)。
提示
确保在 BoundBox2DLabeler 部分选中 Enabled 复选框。
![psml 1413]()
图 14-13. 指定 ID Label Config 资产
到此为止。接下来,我们需要测试标签。
测试标签器
为了测试标签器在继续之前是否正常工作:
-
找到作为示例资产的一部分导入的前景对象预制件,如图 14-14 所示。
![psml 1414]()
图 14-14. 前景预制件
-
将项目窗格中的任意一个预制件拖动到层次结构中。
-
选择新添加的预制件,并在场景视图处于活动状态时,按键盘上的 F 键将视图聚焦在它上面,如图 14-15 所示。
![psml 1415]()
图 14-15. 聚焦在一些意大利面上
-
移动主摄像机,在场景视图中,直到在游戏视图中很好地显示新添加的预制件为止。
提示
而不是手动对齐摄像机,您可以选择预制件,在场景视图中聚焦它,然后通过右键单击层次结构中的预制件并选择“与视图对齐”来要求主摄像机复制场景视图的透视图。
-
使用播放按钮运行场景。您应该看到一个围绕预制件所代表的物品适当显示的边界框,如图 14-16 所示。如果是这样,这意味着到目前为止一切正常!
![psml 1416]()
图 14-16. 测试标签
如果一切正常工作,请从场景中删除预制对象。
添加随机化器
接下来,我们需要向环境中添加一些随机化器。随机化器将随机放置前景(以及最终的背景)对象,以生成一系列不同的图像。
提示
通过创建随机放置图像的图像(还可以随机做其他事情),我们正在帮助可能最终使用这些数据训练的机器学习模型更有效地找到我们希望它找到的图像中的对象。
正如我们之前讨论的,Unity Perception 提供了许多不同的随机化器,并允许根据需要创建自己的随机化器。对于我们的合成商店,我们希望随机化许多不同的事物:
-
事物的质地
-
我们感兴趣的事物背后的对象(背景对象)
-
背景对象的颜色
-
对象的放置(前景和背景)
-
对象(前景和背景)的旋转
按照以下步骤添加随机化器:
-
在层次结构中创建一个空的游戏对象,并命名为“场景”或类似的名称。
-
选择场景对象,并使用其检视器通过“添加组件”按钮添加 Fixed Length Scenario 组件,如图 14-17 所示。
![psml 1417]()
图 14-17. 添加固定长度场景组件
-
使用“添加随机化器”按钮添加 BackgroundObjectPlacementRandomizer。
-
在新的 BackgroundObjectPlacementRandomizer 中,点击“添加文件夹”按钮,然后导航到教程资产的 Background Objects 文件夹中的 Prefabs 文件夹,如图 14-18 所示。
![psml 1418]()
图 14-18. 添加一个 BackgroundObjectPlacementRandomizer
-
添加了背景对象文件夹后,您可能需要调整深度、层计数、分离距离和放置区域设置:我们的设置如图 14-19 所示。
![psml 1419]()
图 14-19. 背景对象放置随机器设置
提示
现在可以再次运行模拟,你会发现相机前方出现了一堆随机形状(颜色相同)。尽管如此,前景对象还没有出现。
-
接下来,添加一个 TextureRandomizer(在场景对象中的 Fixed Length Scenario 组件的“添加随机器”按钮中使用)。
-
添加 TextureRadomizer 后,选择“添加文件夹”按钮,并从教程资产中找到背景纹理文件夹,如图 14-20 所示。
![psml 1420]()
图 14-20. TextureRandomizer 设置
-
接下来我们将添加一个 HueOffsetRandomizer,如图 14-21 所示。我们将使用其默认设置。
![psml 1421]()
图 14-21. 添加一个 HueOffsetRandomizer
-
现在我们需要添加一个 ForegroundObjectPlacementRandomizer,并使用“添加文件夹”按钮指向前景对象预制件文件夹(杂货)。我们的设置如图 14-22 所示。
![psml 1422]()
图 14-22. 前景对象放置随机器
-
对于最后的随机器,我们需要一个 RotationRandomizer,如图 14-23 所示。
![psml 1423]()
图 14-23. RotationRandomizer
这就是所有的随机器。为了确定随机器影响的对象,我们需要给这些对象添加一些额外的组件:
-
在项目面板中打开背景对象预制件文件夹,并选择所有预制件(仍然在项目面板中),如图 14-24 所示。
![psml 1424]()
图 14-24. 选择背景预制件资源
-
在检查员(选择所有背景对象预制件时),点击“添加组件”按钮,依次添加一个 TextureRandomizerTag 组件、一个 HueOffsetRandomizerTag 组件和一个 RotationRandomizerTag 组件,如图 14-25 所示。
![psml 1425]()
图 14-25. 添加到资源的组件
-
导航到项目面板中的前景对象预制件文件夹,在该面板中选择所有这些预制件,并使用检查员将 RotationRandomizerTag 添加到所有前景对象中。
就是这样!
看起来成功即成
我们准备生成一些假超市数据。
注
您可能需要调整摄像机的位置,以便生成良好框架的图像。
运行环境,Unity 将重复运行我们设置的随机器,并在每次保存图像时显示其保存位置。Unity 控制台会显示这些位置,如图 14-26 所示。

图 14-26. 图像输出的路径
如果您在系统上导航到此文件夹,您会发现大量图像,以及一些 Unity Perception JSON 文件,描述物体的标签,如图 14-27 所示。

图 14-27。随机图像示例
你可以使用这一组数据来训练 Unity 之外的机器学习系统。要使用这些数据训练机器学习系统,你可以采用许多方法之一。
如果你感兴趣,我们建议首先使用 Faster R-CNN 模型,并使用在 ImageNet 上预训练的 ResNet50 骨干。你可以在 PyTorch 的 torchvision 包中找到所有这些内容的实现。
小贴士
我们建议如果您想进一步了解,请找一本关于 PyTorch 或 TensorFlow 的好书。与此同时,Unity 的 GitHub 上的 datasetinsights 仓库 是一个很好的起点。
使用合成数据
本书中的合成章节专注于使用模拟环境生成合成数据,这是机器学习领域日益流行的趋势。这是因为创建流行的热门机器学习领域如计算机视觉中所需的检测或分类模型,需要大量代表您希望模型能够识别或区分的对象类型的数据。
通常这意味着由数百万张照片组成的数据集,每张照片都单独标记了其中出现的对象。有时甚至需要标记每张图像中特定对象出现的区域。如果针对您尝试解决的问题不存在这样的数据集,这将是一个不可行的工作量。
这导致了数据集共享的流行,这是一个很好的做法,但由于机器学习模型在如何做出关键决策方面可能会显得不透明,了解其基于的数据很少,这只会加剧机器学习领域中责任和理解不足的问题。因此,如果您为重要事项或学习练习训练模型,仍然希望创建自己的训练数据集。
数据合成可以通过允许某人定义数据中应该出现的规则及其各个方面的变化方式,来减少创建数据集所需的工作量。然后可以使用模拟环境在给定规格内生成任意数量的随机变化,并以指定形式输出,如标记图像。这可用于创建以下数据集:
-
通过在虚拟场景中从不同角度生成对象的图片,与其他对象混合显示,部分遮挡,并在不同光照条件下展示,来识别特定对象。
-
预测 2D 图像中的距离或深度 —— 通过生成视觉图像和相应的深度地图,这些图像和地图由仿真生成(仿真了解物体与摄像机之间的距离)
-
场景中的分区 —— 类似于预测 2D 图像中的深度,但输出可以允许像自动驾驶汽车这样的设备识别与其驾驶相关的物体,如标志或行人(如图 14-28 所示)
-
你可以在虚拟场景中生成任何其他具有随机变化的内容

图 14-28. 视觉图像示例(左)及表示场景中识别的物体类别的对应地图(右)
一旦数据合成完成,您可以根据需要处理数据。从图像数据集中摄取并学习所需的通用机器学习超出了本书的范围。这里我们关注仿真部分以及仿真引擎如何实现独特类型的机器学习。
对于超出仿真的机器学习,您可能希望查看 O'Reilly Media 的另一本书,如使用 Swift 实现实用人工智能,该书的作者与本书相同,或者使用 Scikit-Learn、Keras 和 TensorFlow 进行实战机器学习,该书由 Aurélien Géron 撰写。







































































































































































浙公网安备 33010602011771号