文 / Abe Haskins(Twitter,Github)
我们将在本文中探讨如何使用 Unity3D 和 TensorFlow 来教授 AI 执行简单的游戏任务:投球入篮。完整的源代码可于Github 上获取。如有任何疑问,请通过 Twitter 联系我。 注:Github 链接 https://github.com/abehaskins/tf-jam
游戏简介 在这款游戏中,玩家的主要目标就是将球投入篮筐中。乍一听,这不难做到,但当您热血沸腾、心跳加速,旁边又有观众在热情欢呼时,投中的难度就会加大。我现在谈的是经典美式篮球比赛吗?不,没这回事。我所讲的是 Midway 经典街机游戏 “NBA 嘉年华 (NBA Jam)”。
如果您玩过 “NBA 嘉年华 (NBA Jam)” 或由其激发灵感创作的任意一款游戏(包括真实版 NBA 联赛,我认为该联赛便取自于该款游戏),便会知道投篮技巧在玩家眼中就是小菜一碟。您只需按住投射按钮,然后等待最佳时机松开即可。但您有否想过这一投射动作 在游戏中 是如何进行的?所选球的运动弧线是怎样的?投球力度如何?计算机如何确定投射角度?
如果您脑瓜灵光、精通数学,或许可以用纸和笔计算出这些问题的答案,但本篇博文的作者在 8 年级代数考试中都不及格,更别提像那些 “聪明人” 一样计算出答案了。所以,我需要另辟蹊径。
我们不会采用更简单、快速且高效的数学方法来计算投篮问题,而是会探讨其中的原理,并学习一些 TensorFlow 基础知识,然后尝试向一些难搞的篮筐投球。
入门准备 开始项目之前,我们需要准备以下工具。
Unity:用于模拟篮球和创造物理环境 Node.js 和 TensorFlow.js:用于训练模型 TensorFlowSharp:用于通过机器学习代理 (ML-Agents) 资源软件包将模型嵌入 Unity tsjs-converter:用于将 TensorFlow.js 模型转换为可在 Unity 中使用的图表。 Google 表格:用于轻松直观地呈现我们的线性回归分析
即使您对以上任一技术均不擅长,也无妨!(我也不是都精通!)我会尽力阐明这些工具的协同运作方式。使用如此众多的技术会有一个弊端,那就是我在介绍时无法面面俱到,涵盖所有细节,但我会尽可能提供更多外部教育资源的链接!
下载项目 我不打算一步步重建该项目,所以建议您下载 Github 上的源代码,并紧跟我的讲述节奏。 注:Github 上的源代码链接 https://github.com/abehaskins/tf-jam
注意:您需要下载并导入 机器学习代理 (ML-Agents) Unity资源软件包,以便在 C# 中使用 Tensorflow。如果您收到无法在 Unity 中找到 Tensorflow 之类的错误,则需确保遵循 Unity 的 TensorflowSharp 安装文档。
注:机器学习代理 (ML-Agents) Unity 链接 https://github.com/Unity-Technologies/ml-agents Unity 的 TensorflowSharp 安装文档链接 https://github.com/Unity-Technologies/ml-agents/blob/master/docs/Using-TensorFlow-Sharp-in-Unity.md
我们的目标是什么? 为简便起见,我们为该项目设定的预期结果也很简单。我们想要解决如下问题:如果投球手与篮筐之间的距离为 X,则使用力度 Y 投球。就这么简单!我们不会试图盯住球或以任何其他花样为目标,我们只想弄清楚要使用多大力度投球才能投中。
如果您有意深究如何在 Unity 中训练更复杂的 AI,请前往 Unity 查看更多完整的机器学习代理 (ML-Agents) 项目。我在文中介绍的方法简单易行,但未必是最佳实践(因为我也正在学习中!)
我对 TensorFlow、机器学习和数学知识的掌握也很有限,谈不上精通。本文纯属自娱,诸君请勿尽信。
篮筐和球 前面提到过,我们的主要目标是投球入篮。要将球投入篮中,我们首先要有篮筐和球。这时,Unity 就可以派上用场了。
如果您不熟悉 Unity,只需知道这是一个游戏引擎,可为所有平台构建 2D 和 3D 游戏。Unity 内置物理引擎、基本的 3D 模型和出色的脚本运行时 (Mono),支持使用 C# 对游戏编程。
我并非艺术家,只是拖动了一些组块,拼凑起一个球场场景。
红色组块显然 代表 我们的球员。篮筐事先已设置隐形触发器,能够帮助我们检测物体(球)何时穿过篮筐。
在 Unity 编辑器中,可以看到以绿色轮廓表示的隐形触发器。图中显示有两个触发器。如此,我们可以确保仅计算从篮筐顶部落至底部的篮球数量。
如果看一下 /Assets/BallController.cs(每个篮球实例均有此脚本)中的 OnTriggerEnter 方法,便能了解如何将这两个触发器结合使用。
此函数会执行以下任务。首先,函数要确保顶部和底部的触发器均被击中;其次,它会改变篮球的材质,向我们直观展示球已入篮;最后,它会记录我们最关心的这两个关键变量:distance 和 force.y。
投篮 请打开 /Assets/BallSpawnerController.cs。这是一个依存于投球手的脚本,负责生成篮球并试图投中篮筐。您也可在 DoShoot() 方法的末尾处查看这段代码。
此代码会先实例化一个新的篮球实例,然后设定投篮力度以及投球者与篮板的距离(这样便于我们稍后记录,正如上段代码中所示)。
如果您仍未关闭 /Assets/BallController.cs,可以查看我们的 Start() 方法。我们在创建新篮球时将会调用此代码。
换言之,我们会创建一个新球,为其设定一定的力度,然后设定 30 秒后篮球自爆,这样我们就能够源源不断地处理新产生的球,并确保一切都在合理范围内运行。
下面我们尝试运行以上代码,看看这位全明星球员的投篮表现。您可以点击 Unity 编辑器中的 ▶️(“播放”)按钮,之后将会看到……
我们的球员(暂且称它为 “小红”)几乎百发百不中。
为什么小红的表现如此糟糕?答案就在于 Assets/BallController.cs 中有一行代码写的是 float force = 0.2f。这行代码大胆断定每次投篮动作都应完全相同。如您所见,Unity 会严格做到 “完全相同”。使用相同力度再三重复动作,同样的物体总是以完全相同的方式弹回。所有动作整齐划一。
这当然不是我们想要的结果。若不尝试新的突破,我们永不可能训练出堪比勒布朗 (Lebron) 的卓越投球手,所以我们要做一些改进。
随机投篮,收集数据 我们可以简单将力度改为随机大小来引入一些随机噪音。
这样,我们就会产生不同的投篮结果,最终发现投篮成功时的状况,当然,这可能也要花些时间才能猜对结果。
小红表现很差,虽然偶尔也会投中一球,但纯靠运气。没关系。此时,任何投篮都能作为可供使用的数据点。我们稍后会谈到这一点。
同时,我们不希望只能从一个位置投球。我们希望小红可以从任意距离成功投篮(如果足够幸运的话)。在 Assets/BallSpawnController.cs 中查找以下行,并取消对 MoveToRandomDistance() 的注释。
再次运行后,我们会看到小红每次投完篮后都会在球场上兴奋地跳来跳去。
随机移动与随机力度相结合会产生一种非常奇妙的东西:数据。如果此时查看 Unity 中的控制台,您会看到每次成功投篮的数据都有记录。
每次投中后,系统都会记录目前为止的进球数量、投射位置与篮筐的距离以及投射所需的力度。但这种进度太慢,我们需要进行加速。返回至先前添加 MoveToRandomDistance() 调用处,然后将 0.3f(每次投篮延迟 300 毫秒)改为 0.05f(延迟 50 毫秒)。
现在再投篮,我们会看到进球数大大增加。
这种训练方式效果不错!从后面的计数器中可以看到:进球率已达 6.4% 左右。但小红现在仍比不上斯蒂芬·库里。说到训练,我们真的从中学到什么了吗?TensorFlow 是如何起作用的?乐趣何在呢?这就要看下一步了。现在,我们准备从 Unity 中提取上述数据,并构建模型来预测所需力度。
预测、模型和回归 在 Google 表格中查看数据 在深入探讨 TensorFlow 之前,我想看看数据,所以我让 Unity 一直运行,直到小红成功投进大约 50 球。若您查看 Unity 项目的根目录,应该能看到新文件 successful_shots.csv。这是 Unity 中对每次成功进球产生的原始转储文件!我从 Unity 中导出此文件,以便在电子表格中轻松进行分析。
.csv 文件只有三行内容,分别为 index、distance 和 force。我已将此文件导入 Google 表格,并创建了一张绘有趋势线的散点图,此图为我们直观展示了数据的分布情况。
哇!快看。我是说,看看这张图。 我想说太赞了……好吧,我得承认,其实我刚开始的时候也看不懂这张图。我们下面就来具体分析一下这张图表中,Y 轴代表投射力度,X 轴代表投射距离,这些坐标点在两轴之间分布开来。图中显示了所需力度与投射距离之间具有十分明显的相关性(尽管会有个别严重离群的随机异常值)。
实际上,您也可以这样解读:“TensorFlow 对此非常擅长。”
该用例简单易懂,但 TensorFlow 还有一项卓越功能,即能够让我们使用类似代码构建更为复杂的模型(如果我们愿意)。例如,在完整的游戏中,我们可以加入更多特征,如另一方的投射位置及其过往的盖帽频率统计资料,以此来确定我们的球员应该投篮还是传球。
创建 TensorFlow.js 模型 在您最爱的编辑器中打开 tsjs/index.js 文件。该文件只是一个脚本,与 Unity 无关。我们可以使用该脚本基于 successful_shots.csv 中的数据来训练模型。
下面介绍这种训练及保存模型的完整方法……
如您所见,操作起来不是很复杂。我们从 .csv 文件中加载数据并创建一连串 X 和 Y 坐标点,听起来与上文的 Google 表格操作十分类似!在此处,我们要求模型 “拟合” 这些数据。完成后保存模型,以备日后使用!
很遗憾,TensorFlowSharp 并不接受 Tensorflow.js 所能保存的模型格式。为此,我们需要进行一些巧妙的转换,以便模型能够嵌入 Unity。我用了一些实用程序来帮助解决此问题。大致过程如下:将模型从 TensorFlow.js 格式转换为 Keras 格式,在此格式中创建检查点,并将检查点与 Protobuf Graph Definition 合并,得到 Frozen Graph Definition,然后即可将其嵌入 Unity。
幸运的是,如果您想快速开始,也可以跳过 这些步骤, 只运行 tsjs/build.sh;若一切顺利,它便能自动执行所有步骤并在 Unity 中填充冻结模型。
在 Unity 内部,我们能够通过 Assets/BallSpawnController.cs 中的 GetForceFromTensorFlow() 查看模型的交互情形。
在作出图表定义时,您将定义一个包含多个步骤的复杂系统。本例中,我们将自己的模型定义为单一密集层(具有隐式输入层),这表示我们的模型会接受单个输入并会生成相应输出。
在 TensorFlow.js 中使用 model.predict 时,它会将您的输入自动馈送至正确的输入图节点,并在计算完成后为您提供该节点的输出。然而,TensorFlowSharp 的运作方式与之不同,需要我们直接透过名称与图表节点交互。
鉴于此,我们便需将输入数据转换为图表可接受的格式,并将输出数据发送给小红。
游戏日! 我使用上述系统对模型作出了部分变动。该模型仅使用 500 次成功进球数据进行了训练,下图展示小红使用该模型的投篮效果。
我们可以看到进球率约提升了 10 倍!如果我们对小红训练数小时,然后收集 1 万或 10 万次成功进球数据,结果又会如何呢?他的投球表现无疑会进一步提升!这就交给您来实现吧。
强烈推荐您查看 Github 上的源代码,如果您训练的成功率超过 60%,欢迎在 Twitter 上给我留言(友情提示:60% 以上的成功率绝对能够实现,可以返回查看第一张 GIF 动画,看看您对小红的训练成效!)
|