百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

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

bigegpt 2024-08-20 10:56 2 浏览

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

游戏介绍

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

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

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

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

让我们开始吧!

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

  • 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/)用于将线性回归可视化。

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

下载项目

我不想手把手建立这个项目,所以我建议从 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)完成了设置。

目标是什么?

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

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

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

篮筐和篮球

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

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

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

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

在 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。

投篮

打开 /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 按钮,我们就会看到……

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

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

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

随机投篮,收集数据

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

1float force = Random.Range(0f, 1f);

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

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

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

1yield return new WaitForSeconds(0.3f);

2// MoveToRandomDistance();

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

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

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

1yield return new WaitForSeconds(0.05f);

2MoveToRandomDistance();

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

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

预测,模型和回归

看看 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),这样就能大概知道数据的分布。

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

这张图显示的是一系列点,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({units: 1, inputShape: [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, {epochs: 100});

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[0, 0] / 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 的工作原理不同,需要你自己去根据节点名称操作节点。

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

游戏时间!

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

我们看到命中率提高了 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

译者:弯月,责编:屠敏

“征稿啦!”

CSDN 公众号秉持着「与千万技术人共成长」理念,不仅以「极客头条」、「畅言」栏目在第一时间以技术人的独特视角描述技术人关心的行业焦点事件,更有「技术头条」专栏,深度解读行业内的热门技术与场景应用,让所有的开发者紧跟技术潮流,保持警醒的技术嗅觉,对行业趋势、技术有更为全面的认知。

如果你有优质的文章,或是行业热点事件、技术趋势的真知灼见,或是深度的应用实践、场景方案等的新见解,欢迎联系 CSDN 投稿,联系方式:微信(guorui_1118,请备注投稿+姓名+公司职位),邮箱(guorui@csdn.net)。

相关推荐

悠悠万事,吃饭为大(悠悠万事吃饭为大,什么意思)

新媒体编辑:杜岷赵蕾初审:程秀娟审核:汤小俊审签:周星...

高铁扒门事件升级版!婚宴上‘冲喜’老人团:我们抢的是社会资源

凌晨两点改方案时,突然收到婚庆团队发来的视频——胶东某酒店宴会厅,三个穿大红棉袄的中年妇女跟敢死队似的往前冲,眼瞅着就要扑到新娘的高额钻石项链上。要不是门口小伙及时阻拦,这婚礼造型团队熬了三个月的方案...

微服务架构实战:商家管理后台与sso设计,SSO客户端设计

SSO客户端设计下面通过模块merchant-security对SSO客户端安全认证部分的实现进行封装,以便各个接入SSO的客户端应用进行引用。安全认证的项目管理配置SSO客户端安全认证的项目管理使...

还在为 Spring Boot 配置类加载机制困惑?一文为你彻底解惑

在当今微服务架构盛行、项目复杂度不断攀升的开发环境下,SpringBoot作为Java后端开发的主流框架,无疑是我们手中的得力武器。然而,当我们在享受其自动配置带来的便捷时,是否曾被配置类加载...

Seata源码—6.Seata AT模式的数据源代理二

大纲1.Seata的Resource资源接口源码2.Seata数据源连接池代理的实现源码3.Client向Server发起注册RM的源码4.Client向Server注册RM时的交互源码5.数据源连接...

30分钟了解K8S(30分钟了解微积分)

微服务演进方向o面向分布式设计(Distribution):容器、微服务、API驱动的开发;o面向配置设计(Configuration):一个镜像,多个环境配置;o面向韧性设计(Resista...

SpringBoot条件化配置(@Conditional)全面解析与实战指南

一、条件化配置基础概念1.1什么是条件化配置条件化配置是Spring框架提供的一种基于特定条件来决定是否注册Bean或加载配置的机制。在SpringBoot中,这一机制通过@Conditional...

一招解决所有依赖冲突(克服依赖)

背景介绍最近遇到了这样一个问题,我们有一个jar包common-tool,作为基础工具包,被各个项目在引用。突然某一天发现日志很多报错。一看是NoSuchMethodError,意思是Dis...

你读过Mybatis的源码?说说它用到了几种设计模式

学习设计模式时,很多人都有类似的困扰——明明概念背得滚瓜烂熟,一到写代码就完全想不起来怎么用。就像学了一堆游泳技巧,却从没下过水实践,很难真正掌握。其实理解一个知识点,就像看立体模型,单角度观察总...

golang对接阿里云私有Bucket上传图片、授权访问图片

1、为什么要设置私有bucket公共读写:互联网上任何用户都可以对该Bucket内的文件进行访问,并且向该Bucket写入数据。这有可能造成您数据的外泄以及费用激增,若被人恶意写入违法信息还可...

spring中的资源的加载(spring加载原理)

最近在网上看到有人问@ContextConfiguration("classpath:/bean.xml")中除了classpath这种还有其他的写法么,看他的意思是想从本地文件...

Android资源使用(android资源文件)

Android资源管理机制在Android的开发中,需要使用到各式各样的资源,这些资源往往是一些静态资源,比如位图,颜色,布局定义,用户界面使用到的字符串,动画等。这些资源统统放在项目的res/独立子...

如何深度理解mybatis?(如何深度理解康乐服务质量管理的5个维度)

深度自定义mybatis回顾mybatis的操作的核心步骤编写核心类SqlSessionFacotryBuild进行解析配置文件深度分析解析SqlSessionFacotryBuild干的核心工作编写...

@Autowired与@Resource原理知识点详解

springIOCAOP的不多做赘述了,说下IOC:SpringIOC解决的是对象管理和对象依赖的问题,IOC容器可以理解为一个对象工厂,我们都把该对象交给工厂,工厂管理这些对象的创建以及依赖关系...

java的redis连接工具篇(java redis client)

在Java里,有不少用于连接Redis的工具,下面为你介绍一些主流的工具及其特点:JedisJedis是Redis官方推荐的Java连接工具,它提供了全面的Redis命令支持,且...