如何让 AI 像 NBA 球星一样投篮?

640?wx_fmt=gif

640?wx_fmt=jpeg

本文旨在使用 Unity3D 和 TensorFlow 来教 AI 怎样玩一个简单的游戏:把球投进篮筐。

640?wx_fmt=gif


640?wx_fmt=png

游戏介绍


我们说的这个游戏里玩家只有一个主要目标:把球投进篮筐里。听起来貌似不难,但当你血液上涌、心跳加速、观众们呐喊时,嗯,想投进还是挺困难的。这是不是北美的经典游戏——篮球?不是,没听说过。我说的是 Midway 出品的经典街机游戏 NBA Jam。

如果你玩过 NBA Jam 或任何受到它启发的游戏(包括真实世界中的 NBA 大联盟,我记得应该是在 NBA Jam 之后诞生的),那你肯定知道从玩家的角度来看,投篮的原理是非常简单的。只需按住投球键,然后在正确的时机松开即可。但你有没有想过,从游戏的角度来看,投篮的过程是什么?球的弧线怎样确定?投球的力度多大?计算机怎样知道投球的角度?

扫描二维码关注公众号,回复: 2608683 查看本文章

聪明并且喜欢数学的你肯定能用纸笔得到答案,但笔者八年级的代数不及格……所以这种“聪明人”的答案就免了吧。我需要用更难的办法解决。

我不想用简单、快捷、有效的方式,用数学解决投篮的问题,而是想学一些简单的 TensorFlow,然后试着投篮就好了。


640?wx_fmt=png

让我们开始吧!


我们需要一堆东西来完成这个项目。

  • Unity 模拟篮球和物理运动;

  • Node.js 和 TensorFlow.js 用于训练模型;

  • TensorFlowSharp 用于将模型通过 ML-Agents asset 包集成到 Unity 中;

  • tsjs-converter(https://github.com/tensorflow/tfjs-converter)将 TensorFlow.js 模型转换为图,以便在 Unity 使用;

  • Google Sheets(http://sheets.google.com/)用于将线性回归可视化。

你不懂某个技术也完全没关系!(其实我也不是完全懂!)我会尽力解释这些东西怎样合作的。使用这么多技术的缺点之一就是我没法详细解释每一种技术,但我会尽可能多地提供资源供大家学习。


640?wx_fmt=png

下载项目


我不想手把手建立这个项目,所以我建议从 GitHub 上下载(https://github.com/abehaskins/tf-jam)代码,然后随着我的解释一步步做。

注意:你需要下载并导入 ML-Agents(https://github.com/Unity-Technologies/ml-agents)这个 Unity asset 包,才能在 C# 中使用TensorFlow。如果在 Unity 中看到任何 TensorFlow 找不到的错误,请确保你按照 Unity 的 TensorflowSharp 文档(https://github.com/Unity-Technologies/ml-agents/blob/master/docs/Using-TensorFlow-Sharp-in-Unity.md)完成了设置。


640?wx_fmt=png

目标是什么?


简单来说,我们希望的输出很简单。我们想要解决的问题是:如果投篮者与篮筐的距离为 X,那么就以力度 Y 投篮。就这么简单!我们不考虑瞄准或其他任何东西。我们只想找出要多大力气投篮才能命中。

如果你想知道怎样才能在 Unity 中制作更复杂的 AI,可以参考 Unity 的更完整的 ML-Agents(https://github.com/Unity-Technologies/ml-agents)项目。我这里介绍的项目本身非常简单易行,而且不一定使用了最佳实践(我也在学习呀!)。

我有限的关于 TensorFlow、机器学习和数学的知识并不是障碍。所以就把这个项目当做娱乐吧。


640?wx_fmt=png

篮筐和篮球


我们已经说过这个项目的目标了——投篮。要把球投进篮筐里,首先我们需要一个篮筐,还有一个球。这就该 Unity 上场了。

如果你不熟悉 Unity,那只需要记住它是个游戏引擎,可以在任何平台上制作二维和三维游戏。它有内置的物理引擎,基本的三维建模,和一个非常好用的脚本运行时(Mono:https://www.mono-project.com/),所以我们可以利用它用 C# 来写游戏。

我不是艺术家,所以我简单地拖了几个立方体放在了场景里。

640?wx_fmt=png

红色立方体显然是玩家。篮筐上有个看不见的触发器(https://unity3d.com/learn/tutorials/topics/physics/colliders-triggers),用于检测物体(篮球)通过篮筐。

640?wx_fmt=png

在 Unity 编辑器中可以看到隐形的触发器的绿色边框。你可以看到我们放了两个触发器。这样可以保证我们只统计那些从顶部一直落到底部的球。

看一下 /Assets/BallController.cs(这个文件是每个篮球上的脚本)中的 OnTriggerEnter方法,会发现这两个触发器是同时使用的。

 1private void OnTriggerEnter(Collider other)
2
{
3  if (other.name == "TriggerTop")
4  {
5     hasTriggeredTop = true;
6  } else if (other.name == "TriggerBottom") {
7     if (hasTriggeredTop  && !hasBeenScored)
8     {
9        GetComponent<Renderer>().material = MaterialBallScored;
10        Debug.Log(String.Format("{0}, {1}, {2}", SuccessCount++, Distance, Force.y));
11     }
12     hasBeenScored = true;
13  }
14}

这个函数做了几件事情。首先,它确保顶部和底部的两个触发器都触发了,然后改变球的材质,这样我们就能直观地看到球进了篮筐,最后输出我们关 心的两个变量:distance 和 force.y。


640?wx_fmt=png

投篮


打开 /Assets/BallSpawnerController.cs。这个脚本运行在投篮运动员上,负责生成篮球,并尝试投篮。看一下末尾处的 DoShoot() 方法。

1var ball = Instantiate(PrefabBall, transform.position, Quaternion.identity);
2var bc = ball.GetComponent<BallController>();
3bc.Force = new Vector3(
4  dir.x * arch * closeness,
5  force,
6  dir.y * arch * closeness
7);
8bc.Distance = dist;

在这段代码中,Instantiates 初始化一个新的篮球实例,然后设置投篮的力度,以及与篮筐的距离(这样后面输出就会更容易,如前一段代码所示)。

如果你还没关 /Assets/BallController.cs,你可以看看它的 Start() 方法。每次创建新的篮球时都会调用这个方法。

1void Start ()
2
{
3  var scaledForce = Vector3.Scale(Scaler, Force);
4  GetComponent<Rigidbody>().AddForce(scaledForce);
5  StartCoroutine(DoDespawn(30));
6}

换句话说,我们建立一个新的球,给它一些力量,然后在 30 秒之后自动销毁这个球,因为我们要处理很多很多球,我们希望场景能干净一些。

试着运行一下项目,看看我们的全明星投篮运动员投得怎么样。点击 Unity 编辑器中的 Play 按钮,我们就会看到……

640?wx_fmt=gif

玩家(我们叫他“小红”)要向斯蒂芬·库里挑战了!

为啥小红投篮这么差?答案是 Assets/BallController.cs 中的一行,float force = 0.2f。这一行说每次投篮都应该是完全一样的力度。我们发现 Unity 忠实地执行了这个“完全一样”。同一个对象,同样的力度,不断重复,弹跳方式都完全一样。真棒。

当然这并不是我们希望的结果。不尝试新的东西就永远不可能成为詹姆斯。所以我们来尝试下吧。


640?wx_fmt=png

随机投篮,收集数据


我们简单地改变力度为随机数,来引入一些随机的噪声。

1float force = Random.Range(0f1f);

这样投篮就是随机的了,我们终于看到有球命中的样子了,尽管需要花上好长一段时间才能命中。

640?wx_fmt=gif

小红很笨,他偶尔会投进,但完全是靠蒙。不过没关系。现在,任何投进的球都是我们需要的数据点。我们一会儿就会用到。

同时,我们不想从一个固定的位置投篮。我们希望小红能从任意距离投进篮筐(如果他运气足够好的话)。在 Assets/BallSpawnController.cs 中找到这几行,然后去掉 MoveToRandomDistance() 前面的注释。

1yield return new WaitForSeconds(0.3f);
2// MoveToRandomDistance();

运行之后,我们会发现小红充满活力地一边投篮一边跳来跳去。

640?wx_fmt=gif

随机运动和随机力度的组合能创建出非常有用的东西:数据。看看Unity的控制台,就会看到每次投进后都会显示出数据。

640?wx_fmt=gif

每次成功投进都会输出目前的投中次数、到篮筐的距离,和投篮的力度。这个模拟很慢,我们来加快一些。回到我们添加 MoveToRandomDistance()的地方,把 0.3f(两次投篮之间300毫秒延迟)改成 0.05f(50毫秒延迟)。

1yield return new WaitForSeconds(0.05f);
2MoveToRandomDistance();

再点击 Play 按钮看看能投中多少。

640?wx_fmt=gif

这个训练不错!我们可以从后面的计数器看到,成功投中的次数大概是 6.4%。库里也做不到这么高吧?不过说起训练,我们从这里学到什么了吗?说好的 TensorFlow 呢?这有什么意思?好吧,TensorFlow 是下一步的十二。我们现在要把 Unity 中的数据拿出来,建立一个模型来预测力度。


640?wx_fmt=png

预测,模型和回归


看看 Google Sheets 里收集到的数据。

在进入 TensorFlow 之前,我想先看看数据,所以我让 Unity 一直运行,直到小红投中 50 个球。这会在 Unity 项目的根目录下生成一个文件 successful_shots.csv。这是从 Unity 中保存下的投篮命中的原始数据!我让 Unity 导出这些数据,这样就可以在工作表中进行分析了。

.csv 文件只有三列:index、distance 和 force。我把这个文件(https://support.google.com/docs/answer/40608?co=GENIE.Platform%3DDesktop&hl=en)导入到 Google Sheets 中,然后建了个散点图(https://support.google.com/docs/answer/190718?hl=en)并画上趋势线(https://support.google.com/docs/answer/6075154?hl=en&co=GENIE.Platform%3DDesktop),这样就能大概知道数据的分布。

640?wx_fmt=png

哇!看这个图。我是说,看图中的那条线。嗯,好吧,我承认,一开始我也不知道这条线是啥意思。来一步步看看。

这张图显示的是一系列点,Y 轴是投篮力度,X 轴是投篮距离。我们可以看到力度和距离之间有很明显的相关性(除了一些不正常的弹跳引起的随机异常之外)。

用正常的话说就是“TensorFlow很会处理这种数据”。

虽然这个例子很简单,但 TensorFlow 很棒的一点就是,如果有需要,我们可以用极其简单的代码做出非常复杂的模型。例如,在完整的游戏中,我们可以包含许多特征——如其他玩家的位置,以及他们以前的阻挡投篮的频率等,来确定玩家应该选择投篮还是传球。

用 TensorFlow.js 建立模型

用你喜欢的编辑器打开 tsjs/index.js 文件。这个文件跟 Unity 没关系,它只是用来根据 successful_shots.csv 的数据训练模型的。

下面是所有训练并保存模型的代码。

 1(async () => {
2   /*
3       Load our csv file and get it into a properly shaped array of pairs likes...
4       [
5           [distanceA, forceB],
6           [distanceB, forceB],
7           ...
8       ]
9    */

10   var pairs = getPairsFromCSV();
11   console.log(pairs);
12
13   /*
14       Train the model using the data.
15    */

16   var model = tf.sequential();
17   model.add(tf.layers.dense({units1inputShape: [1]}));
18   model.compile({loss'meanSquaredError'optimizer'sgd'});
19
20   const xs = tf.tensor1d(pairs.map((p) => p[0] / 100));
21   const ys = tf.tensor1d(pairs.map((p) => p[1]));
22
23   console.log(`Training ${pairs.length}...`);
24   await model.fit(xs, ys, {epochs100});
25
26   await model.save("file://../Assets/shots_model");
27})();

可以看到,代码没多少。首先从 .csv 文件中加载数据,然后建立一系列 X 和 Y 点(就像 Google Sheets 一样)。然后我们要求模型去“拟合”数据。之后把模型存下来供以后使用。

很可惜,TensorFlowSharp 要求的模型的格式跟 Tensorflow.js 能保存的格式不一样。所以我们得做一些转换才能把模型导入到 Unity。我用了一些工具来做这项工作。基本的流程就是把模型从 TensorFlow.js 格式转换成 Keras 格式,这样我们就能设置保存点(https://www.tensorflow.org/get_started/checkpoints),然后将Protobuf图形定义(https://www.tensorflow.org/extend/tool_developers/)合并进去,得到冻结的图形定义(https://github.com/tensorflow/tensorflow/blob/master/tensorflow/python/tools/freeze_graph.py),这样就能导入到 Unity 中了。

幸运的是,你可以跳过这一切,只需运行 tsjs/build.sh,如果一切正常,它会自动完成所有步骤,然后把冻结的模型导入到 Unity 中。

在 Unity 中,看看 Assets/BallSpawnController.cs 文件中的 GetForceFromTensorFlow(),看看怎样使用我们的模型。

 1float GetForceFromTensorFlow(float distance)
2
{
3  var runner = session.GetRunner ();
4
5  runner.AddInput (
6     graph["shots_input"][0],
7     new float[1,1]{{distance}}
8  );
9  runner.Fetch (graph ["shots/BiasAdd"] [0]);
10  float[,] recurrent_tensor = runner.Run () [0].GetValue () as float[,];
11  var force = recurrent_tensor[00] / 10;
12  Debug.Log(String.Format("{0}, {1}", distance, force));
13  return force;
14}

在制作图形定义时,需要定义一个包含多个步骤的复杂系统。在这个例子中,我们把模型定义成单密集层(以及一个隐含的输入层),意思就是我们的模型接受一个输入,然后给出一个输出。

如果在 TensorFlow.js 中调用model.predict(https://js.tensorflow.org/api/0.11.7/#tf.Model.predict),它会自动把输入放到正确的输入图节点上,然后在计算完成之后,从正确的节点上拿到输出。但 TensorFlowSharp 的工作原理不同,需要你自己去根据节点名称操作节点。

知道这些之后,我们只需把输入转换成图期待的格式,然后把输出发回给小红即可。


640?wx_fmt=png

游戏时间!


使用上面的系统,我根据模型做了几个不同的版本。下面是小红在一个根据 500 次命中数据训练过的模型上的投篮结果。

640?wx_fmt=gif

我们看到命中率提高了 10 倍!如果我们训练小红几个小时,收集一万条,或者十万条命中数据会怎样?肯定会让他提高更多!这个工作就留给读者了。

我强烈推荐你看看 GitHub 上的源代码

  • https://github.com/abehaskins/tf-jam

如果能超过 60% 的命中率,欢迎在 Twitter 上告诉我:

  • https://twitter.com/abeisgreat

(剧透:超过 60% 是完全可能的,回到本文开头看看那张图,小红能被训练得很好!)

原文:https://medium.com/tensorflow/tf-jam-shooting-hoops-with-machine-learning-7a96e1236c32?linkId=54634097

作者:TensorFlow

译者:弯月,责编:屠敏

640?wx_fmt=gif

640?wx_fmt=gif

猜你喜欢

转载自blog.csdn.net/csdnnews/article/details/81463936