查看原文
其他

Unity机器学习 | 编写代理及创建游戏AI

2018-01-08 Unity官方 Unity官方平台


自2017年9月Unity推出了机器学习代理工具, 机器学习的开发者持续增长并在社区中获得认可。Unity机器学习代理项目入选mybridge评选2017年最佳30个机器学习开源项目,并在Github中获得1710星。Unity机器学习社区挑战赛也在如火如荼的进行中。


今天的文章中,我们将会学习如何设置一个基本代理,它能利用强化机器学习达到随机选择的数字目标。我们将使用到最新的Unity机器学习代理工具创建并训练代理,完成指定任务,并探讨将这个简单的代理优化为真正的游戏AI。


设置Unity机器学习代理工具和TensorFlow

开始之前,我们需要设置好机器学习代理工具。设置请参考以下文章:

说明:本文的代码和运行环境基于Windows 10环境。


机器学习代理(ML-Agents)场景设置

当我们完成设置后,打开Unity项目,创建新场景。


首先新建一个游戏对象,命名为“NumberAcademy“。添加“TemplateAcademy”组件到“NumberAcademy“游戏对象中。

 

TemplateAcademy组件脚本可从Github下载:

https://github.com/Unity-Technologies/ml-agents/blob/master/unity-environment/Assets/ML-Agents/Template/Scripts/TemplateAcademy.cs

 

场景设置过程并不需要这个组件做太多事,所以我们只用模板中的基本内容即可。

 

NumberAcademy游戏对象

 

在这个组件下,创建子游戏对象,命名为“NumberBrain“。


添加一个Brain组件。将状态数量(State Size)和动作数量(Action Size)的值设为2。将动作空间类型(Action Space Type)设为离散(Discrete)。

 

在这项目中,我们将会用到两个独立的动作,也就是“上”和“下”。之所以使用离散类型,是因为这些动作会以整数值形式呈现。

 

将状态空间类型(State Space Type)设为连续(Continuous)。我们将跟踪两个浮点数,所以使用连续类型。


 NumberBrain游戏对象

 

将大脑类型(Brain Type)设为玩家(Player),并加入两个动作。选择任意两个键位,在此我们选择的是A和B,并分别赋值为0和1。绑定0的键位将减小当前值,而绑定1的键位将增大它。

 

Brain脚本组件

  

现在我们来创建新脚本,命名为NumberDemoAgent.cs,将其基本类设置为Agent,替换原有的“: MonoBehaviour with : Agent”。代码如下:

public class NumberDemoAgent : Agent

{

                       [SerializeField]

                       private float currentNumber;

                       [SerializeField]

                       private float targetNumber;

                       [SerializeField]

                       private Text text;

                       [SerializeField]

                       private Transform cube;

                       [SerializeField]

                       private Transform sphere;

 

                       int solved;

 

currentNumber和targetNumber字段在这段代码里最重要。其它的代码都是用作调试和可视化。我们的代理将挑选一个随机的targetNumber,试着通过使用增长和减小指令,将currentNumber的值调整到目标值。


下面将重写CollectState方法。代码如下:

public override List<float> CollectState()

                       {

                            List<float> state = new List<float>();

                            state.Add(currentNumber);

                            state.Add(targetNumber);

                            return state;

                       }

 

在上面这段段代码里,我们会返回两个浮点数给currentNumber和targetNumber,作为代理的状态。


注意:这里是如何匹配二个状态变量的,而且它们都是浮点数,这就是为什么我们要将状态空间类型设为连续而不是分离。

 

为了训练我们的代理,需要选取随机目标数,所以要重写AgentReset()方法。代码如下:

public override void AgentReset()

                       {

                            targetNumber = UnityEngine.Random.RandomRange(-1f, 1f);

                            sphere.position = new Vector3(targetNumber * 5, 0, 0);

                            currentNumber = 0f;

                       }

 

最后也是最重要的一部分是AgentReset()方法。这是我们的输入动作,处理任务,也就是回应动作,如果代理回报(reward)结果则为成功。

 

代码如下:

public override void AgentStep(float[] action)

                       {

                            if (text != null)

                                   text.text = string.Format("C:{0} / T:{1} [{2}]", currentNumber, targetNumber, solved);

 

                            switch ((int)action[0])

                            {

                                   case 0:

                                          currentNumber -= 0.01f;

                                          break;

                                   case 1:

                                          currentNumber += 0.01f;

                                          break;

                                   default:

                                          return;

                            }

 

                            cube.position = new Vector3(currentNumber * 5f, 0f, 0f);

 

                            if (currentNumber < -1.2f || currentNumber > 1.2f)

                            {

                                   reward = -1f;

                                   done = true;

                                   return;

                            }

 

                            float difference = Mathf.Abs(targetNumber - currentNumber);

                            if (difference <= 0.01f)

                            {

                                   solved++;

                                   reward = 1;

                                   done = true;

                                   return;

                            }

                       }

 

首先我们会看到文字的变化。这只是用于调试或可视化,它让我们能看到当前值,还有目标值,以及我们所需要解决问题(即达到目标值)所测试的数值次数。

 

下一步是切换查看动作和完成任务的地方。在示例中,脚本要么通过减小当前值回应动作0,要么增大当前值回应动作1。除此之外,输入的其它值都无效,但若是脚本得到了其它值,脚本会忽略这个值并返回。

 

现在我们基于currentNumber移动立方体,这个值会用作x轴偏移值。该立方体在此用于可视化,对实际的逻辑或训练过程没有任何影响。

 

然后检查currentNumber对大小限制的反应。因为我们选取的随机数是在-1和1之间的,如果随机数为-1.2或是1.2,将视作失败,因为数值超过了极限。本示例中,我们给代理回报-1,表示失败,然后给done变量赋值为true,以让代理能重置并再次尝试。

 

最后,我们会检查currentNumber和目标值的差值是否小于0.01。若小于,则认为是匹配值,将回报(reward)值设为1.0,表示成功,给done赋值为true表示完成。我们还会再次期间使用solved变量作为计数器,用于调试,这样我们就能知道成功的次数了。

 

下面是完整的脚本代码:

using System.Collections.Generic;

using UnityEngine;

using UnityEngine.UI;

 

public class NumberDemoAgent : Agent

{

                       [SerializeField]

                       private float currentNumber;

                       [SerializeField]

                       private float targetNumber;

                       [SerializeField]

                       private Text text;

                       [SerializeField]

                       private Transform cube;

                       [SerializeField]

                       private Transform sphere;

 

                       int solved;

 

                       public override List<float> CollectState()

                       {

                            List<float> state = new List<float>();

                            state.Add(currentNumber);

                            state.Add(targetNumber);

                            return state;

                       }

 

                       public override void AgentStep(float[] action)

                       {

                            if (text != null)

                                   text.text = string.Format("C:{0} / T:{1} [{2}]", currentNumber, targetNumber, solved);

 

                            switch ((int)action[0])

                            {

                                   case 0:

                                          currentNumber -= 0.01f;

                                          break;

                                   case 1:

                                          currentNumber += 0.01f;

                                          break;

                                   default:

                                          return;

                            }

 

                            cube.position = new Vector3(currentNumber * 5f, 0f, 0f);

 

                            if (currentNumber < -1.2f || currentNumber > 1.2f)

                            {

                                   reward = -1f;

                                   done = true;

                                   return;

                            }

 

                            float difference = Mathf.Abs(targetNumber - currentNumber);

                            if (difference <= 0.01f)

                            {

                                   solved++;

                                   reward = 1;

                                   done = true;

                                   return;

                            }

                       }

 

                       public override void AgentReset()

                       {

                            targetNumber = UnityEngine.Random.RandomRange(-1f, 1f);

                            sphere.position = new Vector3(targetNumber * 5, 0, 0);

                            currentNumber = 0f;

                       }

}

设置代理

现在脚本完成了,我们需要新建游戏对象,命名为“NumberDemoAgent”。


将刚刚写好的NumberDemoAgent.cs脚本附加到该游戏对象上,并将NumberBrain添加到该组件的Brain部分。

 

NumberDemoAgent游戏对象

 

下一步新建文本(Text)对象,将其放到明显的位置,理想位置是在屏幕的中央,字号要大。将该文本对象附加到NumberDemoAgent上。

 

新建立方体(Cube)对象,将它们也添加到NumberDemoAgent上,这样我们可以看到过程的变化,比阅读一个个数值更轻松。

 

在运行模式下测试

点击play,就能用设置好的键位A和B,左右移动立方体。当我们将方块向球体移动时,它将增大solved计数器,并重置。如果朝错误的方向移动的太远,它也会重置(请记住1.2的范围值限制)。

 

训练

当它在运行模式下成功运行后,选择brain并把Brain Type设为外部(External)。保存场景,并构建一个可执行文件,文件中只包含了一个启用调试模式的场景。


 

关于输出文件夹,选择python文件夹作为存放这个机器学习代理项目的文件夹。例如,我的文件夹的地址是这个:C:\ml-agents\python。别忘了该可执行文件的名字,因为后续我们就需要它了。

 

Anaconda / Jupyter

启动Anaconda命令指示符。将目录定位到刚刚的构建目录,也就是那个python文件夹下,命令示例:cd c:\ml-agents\python

 

输入命令“jupyter notebook”(提示:这里可能要打两次回车),自动打开浏览器界面。会得到这样一个网页界面:

 

提示:这里要修改高亮部分。对于env_name变量,要输入可执行文件的名字。对于buffer_size和batch_size,你可以使用截图里的值。要注意,这里面的当前值部分只在测试的时候会看到。

 

当编辑好Hyperparameters部分后,按照步骤运行。从第一步和第二步开始。在第三步时,你会看到一个打开的游戏窗口。第一次打开时,你也许会得到窗口许可对话框,记得要选择允许。

 

在进行到第四步时,先注意得到的结果,第一次运行时也许会需要几分钟。

 

 

当它保存多次后,点击stop按钮。然后继续第五步,并运行。这将导出你的训练数据到一个与可执行文件同名的.bytes文件中,导出位置在上述python目录的子目录下。示例:python/models/ppo。

 


复制.bytes文件,(再次提醒,它的名字会和你的可执行文件名字一样)将这个粘贴到Unity项目里的某个位置。


选择Brain脚本组件,将Brain Type设为Internal。把.byetes文件添加到Graph Model字段上。


 

保存并点击运行,就这样我们就完成创建自定义代理。


结语

这个示例相当简单,只是用来让你理解这个系统的运作方式。我们很期待能看到这个项目你能够继续开发,并构建成更大更有趣的项目,用来控制游戏AI,从而制作出有意思的游戏机制和AI。更多精彩的机器学习文章欢迎访问 Unity官方社区(Unitychina.cn)! 更多Unity机器学习社区挑战赛的信息请访问Unity Connect!


参考信息

Unity ML Agents GitHub

https://github.com/Unity-Technologies/ml-agents


HyperParameters Doc

https://github.com/Unity-Technologies/ml-agents/blob/master/docs/best-practices-ppo.md


2017年最佳30个机器学习开源项目

https://medium.mybridge.co/30-amazing-machine-learning-projects-for-the-past-year-v-2018-b853b8621ac7


Unity机器学习系列文章

官方活动


点击“阅读原文”访问Unity官方社区!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存