https://blog.tensorflowcn.cn/2018/07/tf-jam-shooting-hoops-with-machine-learning.html
https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEidh9BkWMiTFJkVp4ICLPMzxjxOKiqpmgo6rOBuuXIptvI81IsHq6rT8kVvB0ypkHk-ktQCof4fAmpRjpaCPvqU5Z38jvd7sdTLGTRF9rlD2QIzC-5ql77_9AyKaeobrOietJQRvcBr0S8/s1600/0_VTFbD4rcGt1ySzQS.gif
作者:Abe Haskins (Twitter,Github)
在本文中,我们将深入研究使用
Unity3D 和
TensorFlow 来教 AI 执行一项简单的游戏内任务:将球投进篮筐。完整的源代码可以在
Github 上找到,如果您有任何问题,请在
Twitter 上联系我。
.
游戏介绍
有一个游戏,玩家的主要目标是:将球投进篮筐。这听起来并不难,但当你的血液沸腾,你的心跳加速,人群欢呼的时候——好吧,投篮就变得相当困难。我是在说经典的美国篮球游戏
篮球 吗?不,从未听说过。我指的是经典的 Midway 街机游戏
NBA Jam。
如果你玩过 NBA Jam 或任何受其启发的游戏(包括现实中的 NBA 联盟,我认为它是在 NBA Jam 之后出现的),那么你就会知道从玩家的角度来看,投篮的机制相当简单。你按住并释放
投篮按钮,并以完美的时机。但是你有没有想过从
游戏的角度来看,这个投篮是如何进行的呢?球的弧线是如何选择的?球被投掷的力度是多少?计算机如何知道投篮的角度?
如果你是一个聪明、喜欢数学的人,你也许可以用笔和纸算出这些答案,然而这篇文章的作者在八年级代数就挂科了,所以……这些“聪明人”的答案是不可能的。我需要用不同的方式来解决这个问题。
我们不会采取更简单、更快、更有效的方式来实际进行投篮所需的数学运算,而是会看看这个兔子洞有多深,学习一些简单的 TensorFlow,并尝试投一些该死的篮筐。
入门
我们需要一些东西来完成这个项目。
如果你不是这些技术的专家,这完全没问题!(我肯定不是所有这些技术的专家!)我会尽力解释这些部分是如何组合在一起的。使用这么多不同的技术的缺点之一是,我将无法详细解释所有内容,但我将尽力尽可能多地链接到教育资源!
下载项目
我不会尝试一步一步地重新创建这个项目,所以我建议你从
Github 上获取源代码,并在解释发生的事情时跟着做。
注意:你需要下载并导入
ML-Agents Unity 资产包才能在 C# 中使用 Tensorflow。如果你在 Unity 中遇到了关于 Tensorflow 找不到的错误,请确保你已按照
Unity TensorflowSharp 设置文档 进行操作。
我们的目标是什么?
为了简单起见,我们希望这个项目的最终结果非常简单。我们想要解决的问题是:
如果射手距离篮筐 X 距离,就用 Y 力投球。就这样!我们不会尝试瞄准球或做任何花哨的动作。我们只是想弄清楚投球的力度,以便投篮命中。
如果你对如何在 Unity 中制作更复杂的 AI 感兴趣,你应该查看 Unity 的更完整项目
ML-Agents。我在这里讨论的方法旨在简单、易懂,不一定代表最佳实践(我也是在学习!)。
我对 TensorFlow、机器学习和数学的有限知识不会很微妙。所以请带着批判的眼光来看待它,并理解这一切都是为了好玩。
篮筐和球
我们已经讨论了我们的目标的核心:投篮得分。要将球投进篮筐,你需要一个篮筐和……好吧,一个球。这就是 Unity 的用武之地。
如果你不熟悉 Unity,只要知道它是一个游戏引擎,它允许你为所有平台构建 2D 和 3D 游戏。它内置了物理引擎、基本的 3D 建模,以及一个很棒的脚本运行时 (
Mono),它允许我们用
C# 编写我们的游戏。
我不是艺术家,但我拖动了一些方块,并拼凑了这个场景。
那块红色的方块
显然是我们的球员。篮筐已经设置了不可见的
触发器,允许我们检测到物体(球)何时穿过篮筐。
在 Unity 编辑器中,你可以看到用绿色描绘的不可见触发器。你会注意到有两个触发器。这是为了确保我们只计算球从上到下落入篮筐的得分。
如果我们看一下
/Assets/BallController.cs
中的
OnTriggerEnter
方法(每个篮球实例都将拥有的脚本),你可以看到这两个触发器是如何一起使用的。
这个函数做了一些事情。首先,它确保顶部和底部的触发器都被触发,然后它改变球的材质,以便我们可以直观地看到球是否投篮命中,最后它记录了我们关心的两个关键变量:
distance
和
force.y
。
投篮
打开
/Assets/BallSpawnerController.cs
。这个脚本位于我们的射手身上,负责生成篮球并尝试投篮。查看
DoShoot()
方法结尾附近的代码片段。
这段代码
Instantiates
了一个新的球实例,然后设置了我们投球的力和距离目标的距离(这样我们就可以在稍后更轻松地记录下来,正如我们在上一段代码片段中所展示的那样)。
如果你仍然打开了
/Assets/BallController.cs
,你可以看一下我们的
Start()
方法。这段代码是在我们创建新的篮球时调用的。
换句话说,我们创建了一个新的球,赋予它一些力,然后在 30 秒后自动销毁这个球,因为我们将处理很多球,我们希望确保一切都保持在合理的范围内。
让我们尝试运行所有这些,看看我们这位全明星射手表现如何。你可以在 Unity 编辑器中点击 ▶️(播放)按钮,我们就会看到……
我们的球员,昵称“红”,几乎准备与史蒂芬·库里一决高下了。
那么为什么红这么烂呢?答案在于
Assets/BallController.cs
中的一行代码,它写着
float force = 0.2f
。这一行代码断言每球的投篮方式都应该完全一样。你会注意到 Unity 对这个“完全一样”的概念非常重视。相同的物体,相同的力,一次又一次地复制,将始终以完全相同的方式反弹。很酷。
当然,这不是我们想要的。如果我们从来不尝试任何新的东西,我们永远不可能像勒布朗那样投篮,所以让我们来增添一些乐趣。
随机投篮,收集数据
我们可以通过简单地将力改为随机值来引入一些随机噪声。
这混淆了我们的投篮,所以我们终于可以看看篮球成功得分是什么样子,即使它需要一段时间才能猜对。
红非常愚蠢,他会偶尔投进一球,但这纯粹是运气。不过没关系。在这一点上,任何投进的球都是我们可以利用的数据点。我们将在稍后讨论这个问题。
同时,我们不希望只能从一个位置投篮得分。我们希望红能从任何距离成功投篮(只要他足够幸运)。在
Assets/BallSpawnController.cs
中,找到这些代码行并取消注释
MoveToRandomDistance()
。
如果我们运行它,我们会看到 Red 在每次投篮后都会兴奋地在球场周围跳跃。
这种随机运动和随机力的组合创造了一件非常奇妙的事情:数据。如果您查看 Unity 中的控制台,您将看到数据在每次投篮时被记录下来,因为成功的尝试不断涌入。
每次成功的投篮都会记录到目前为止成功的投篮次数、距离篮筐的距离以及投篮所需的力。不过这很慢,让我们加速。回到我们添加 `MoveToRandomDistance()` 调用的地方,将 0.3f(每次投篮延迟 300 毫秒)更改为 `0.05f`(每次投篮延迟 50 毫秒)。现在点击播放,看着我们成功的投篮像雨点一样涌现。
现在 **这** 才是有效的训练方案!我们可以从后面的计数器中看到,我们成功地投进了大约 6.4% 的球。库里,他可比不了。说到训练,我们真的从中学到了什么吗? TensorFlow 在哪里?为什么这很有趣?好吧,那是下一步。我们现在准备将这些数据从 Unity 中提取出来,并建立一个模型来预测所需的力。
预测、模型和回归
在 Google 表格中检查我们的数据
在我们深入 TensorFlow 之前,我想看一下数据,所以让 Unity 运行直到 Red 成功完成了大约 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 图定义` 合并,以获得一个 `
冻结的图定义`,我们可以将其拉入 Unity。
幸运的是,如果您想参与其中,您可以跳过所有这些操作,只需运行 `tsjs/build.sh`,如果一切顺利,它将自动完成所有步骤,并将冻结的模型放入 Unity。
在 Unity 内部,我们可以查看 `Assets/BallSpawnController.cs` 中的 `GetForceFromTensorFlow()`,看看与模型交互的样子。
当您创建图定义时,您正在定义一个具有多个步骤的复杂系统。在我们的例子中,我们将模型定义为一个单层密集层(带有隐式输入层),这意味着我们的模型接收单个输入,并为我们提供一些输出。
当您在 TensorFlow.js 中使用
model.predict 时,它会自动将您的输入提供给正确的输入图节点,并在计算完成后为您提供来自正确节点的输出。但是,TensorFlowSharp 的工作方式有所不同,它要求我们通过节点名称直接与图节点交互。
考虑到这一点,我们只需要将输入数据转换为图期望的格式,并将输出发送回 Red。
比赛开始了!
使用上面的系统,我在我们的模型上创建了一些变体。这是 Red 使用仅在 500 次成功投篮上训练的模型进行投篮。
我们看到投篮命中率提高了约 10 倍!如果我们让 Red 训练几个小时,并收集 10,000 次或 100,000 次成功的投篮,会发生什么呢?当然,这会进一步提高他的水平!好吧,我留给你们自己去探索。
我强烈建议您查看
Github 上的源代码 并
在 Twitter 上给我发推,如果您能打出 60% 的命中率(剧透:打出 60% 的命中率是 100% 可行的,回去看看第一个 GIF,看看你能够训练 Red 到何种程度!)。