ML-Agents(八)PushBlock

ML-Agents(八)PushBlock

一、前言

我们这次来学习一个新的实例——Push Block。这个示例的效果如下:

pushblock

如图可以看到,这个示例训练的效果就是让agent把白色的方块推到场景中绿色条形区域就算完成任务。注意每完成一次任务,重置的时候地面会显示绿色,表明此次agent成功将白色方块推到目标区域,若失败,则地面会显示一下红色。

我们可以先根据之前示例,想一下这个agent应该如何进行训练。首先可能想到的应该是利用Viusal Observations来对这个功能进行训练,其实本质上这个示例和Grid World有点相似,利用Render Texture Sensor或者Camera Sensor来对其进行训练。实际上官方在Push Block示例中,也有个场景叫VisualPushBlock,这个场景就是想尝试使用Camera Sensor来对agent进行训练,你还会发现它使用的是第一人称摄像机,当然官方并没有把这个训练完成(即没有训练模型)。

这个示例主要我们可以学习到的就是如何利用射线传感器进行数据采集,即Ray Perception Sensor。这个传感器主要分为2D3D两个类别,分别适用于二维和三维。Push Block则采用了Ray Perception Sensor 3D类型。

下面老规矩,先来看看官方对该示例的参数。

二、环境与训练参数

  • 设定:在一个平台环境中,agent可以推动方块移动。

  • 目标:Agent必须推动方块到目标位置。

  • Agents:一个训练环境只包含一个agent。

  • Agent奖励设定:

    • 每走一步惩罚0.0025。
    • 如果白色方块移动到目标位置,则奖励1。
  • 行为参数

    • 矢量观测空间:(Continusous)70个观测变量分别对应14个射线投射(ray-casts),每条射线探测三个可能的目标(墙、目标区域或方块)之一。即一条射线要么啥都没探测到,要么探测到的目标为墙、目标区域或方块三者之一。注意这里的目标区域其实也是有一个Box Collider使得射线可以检测到。

      这里有个疑问,70个观测变量怎么来的= =。有知道的小伙伴可以留言讨论一下~(在写后面的时候突然发现怎么算出来的了,具体在三、场景基本结构中解释Ray Perception Sensor Component 3D组件部分)

    • 矢量动作空间:Discrete Type,6个变量,分别对应小agent顺时针、逆时针方向旋转以及前、后、左、右四哥方向的移动。

    • 视觉观察(可选):使用第一人称摄像机,示例在VisualPushBlock场景中。但是官方文档也说了,Push Block的视觉观察版本并不使用提供的默认训练参数进行训练,也就是说如果想利用视觉观测值进行训练,需要自己调试一下训练参数。

  • 可变参数:4个

    • block_size:白方块在x、z方向上的比例,通俗来说就是白色方块的大小,越小的话肯定训练开始使越难找到。
      • 默认:2
      • 推荐最小值:0.5
      • 推荐最大值:4
    • dynamic_friction:地面的动摩擦系数,即运动物体与地面之间的摩擦系数
      • 默认:0
      • 推荐最小值:0
      • 推荐最大值:1
    • static_friction:地面的静摩擦系数,即静止物体与地面的摩擦系数
      • 默认:0
      • 推荐最小值:0
      • 推荐最大值:1
    • block_drag:空气阻力对方块的影响
      • 默认:0.5
      • 推荐最小值:0
      • 推荐最大值:2000

    以上参数中后三个:dynamic_friction、static_friction和block_drag其实都是调整agent小蓝块推动白色方块的难易程度,摩擦系数或者空气阻力越大,那么agent就越难推动白色方块,或者推动得越慢。

  • 基准平均奖励:4.5

三、场景基本结构

场景中包含了32个训练单元,如下图:

image-20200419222311272

我们先从上到下介绍一下简单的东西:SingleCam是场景渲染相机,即运行时观察的场景;PuashBlockSettings是设置环境一些高级物理设置以及专对PushBlock的设置,例如agent的速度,旋转速度等等;剩下就是UI、光源等,没必要详细介绍。

主要来看一下基本训练单元Area的结构,如下图:

image-20200419223002634

其中:

  • Agent

    是我们要训练的代理,蓝色的小方块。它身上不仅有Push Agent Basic训练脚本,还有两个Ray Perception Sensor Component 3D组件。我们可以先来看一下射线组件是怎么样的,如下图:

    image-20200419223742175

    首先我们的agent身上是有两个Ray Perception Sensor的,分为上层和下层,每一层上又有7条射线投射出。为什么分为两层呢?我们来看一下图就明白了:

    image-20200419224142032

    这里先忽略图中的字样,有绘制空心球的地方就是射线检测碰撞到的地方。如果小蓝只有底下一层Ray Sensor的话,当小蓝推动小白时,小蓝的前方射线势必会被小白遮挡几条,这就相当于小蓝被带了眼罩看不清前方。因此为了防止这种情况,我们给小蓝的上方又加了一层ray sensor,这一层射线是永远不会被小白遮挡的,所以小蓝才有了两层Ray Sensor的原因。

    除此之外,我发现官方做的Ray Sensor还有个小细节,就是在IDE下,射线检测碰撞的距离远近会影响到绘制的线条颜色变化,之前我以为只是单一的碰撞到检测即变为红色,仔细发现并不是。不过这个细节设计的难度应该不难,简单想一下,应该是显示的颜色=射线检测的距离/射线可检测的最大距离*颜色显示的阈值范围(RGB),我瞎猜的,估计差不多就这么算的。

    我们下面来看一下Ray Perception Sensor Component 3D组件的属性各代表什么意思:

    image-20200419225518869

    • Sensor Name:该Sensor的名字,类似于ID,注意在小蓝身上的两个名字不同哦,一个是RayPerceptionSensor,另一个是OffsetRayPerceptionSensor

    • Detectable Tags:设置射线能检测到物体的Tag集合。这里设置了3个Tag,分别代表了场景中的目标小白块、目标区域和墙体。

    • Rays Per Direction:射线的条数,为0的时候就只有一条向前的射线,详情看下图:

      pushblock1

    • Max Ray Degrees:射线覆盖的角度范围,可看下图:

      pushblock2

    • Sphere Cast Radius:设置碰撞空心线球的半径,如下图:

      pushblock3

    • Ray Length:设置射线投射的最远距离。

    • Ray Layer Mask:设置射线可以检测到的Layer。

    • Observation Stacks:堆叠之前观察的结果的数量,若设置为1则表示不堆叠以前的观察。

      这里插一下,在看这个属性的时候,突然发现前面环境与训练参数中为啥是70个观测变量参数了,具体在RayPerceptionSensorComponentBase脚本中有一个GetObservationShape()方法:

      image-20200419232015987

      这里numRays=7,numTags=3,则obsSize=(3+2)*7=35,而一个小蓝身上又有两个Sensor,则需要收集的观测变量为35*2=70个。

    • Start Vertical Offset:调整射线发出的角度,如下图:

      pushblock4

    • End Vertical Offset:相对于上面的属性,这里可以设置射线尾部的角度,如下图:

      pushblock5

    • Debug Gizmos中的Ray Hit Color和Ray Miss Color则是分别设置射线碰撞检测到物体线条的颜色以及未检测到时线条的颜色。

    好的,上面介绍了一下Ray Sensor 3D组件,我们继续回来,来看看一个训练单元Area上其他的物体。

  • Goal

    目标区域,注意该区域并不只是地面显示的颜色,而且还带有一个碰撞体,来使得小蓝辨认以及小白碰撞区域检测。

  • Block

    小白块,上面有一个Goal Detect.cs脚本,用来检测小白是否被推入目标区域。

  • Ground和WallsOuter

    墙体和地面,没什么好说的~

四、代码分析

这个示例相对来说比较简单,我们直接从小蓝身上的Push Agent Basic开始看起。

Agent脚本

Agent初始化

using System.Collections;
using UnityEngine;
using MLAgents;

public class PushAgentBasic : Agent
{
    //地面,需要更根据任务完成情况更换地面材质
    public GameObject ground;

    //整个训练单元,开局时可能随机改变角度
    public GameObject area;

    [HideInInspector]
    public Bounds areaBounds;//地面碰撞体的边界

    //Push Block示例的设置,设置小蓝的速度、旋转角度等
    PushBlockSettings m_PushBlockSettings;

    public GameObject goal;//目标区域
    public GameObject block;//小白块

    [HideInInspector]
    public GoalDetect goalDetect;//小白块上判定进入目标区域脚本

    public bool useVectorObs;//该选项应该是之前留下的,此代码中无用

    Rigidbody m_BlockRb;  //小白块刚体
    Rigidbody m_AgentRb;  //小蓝刚体
    Material m_GroundMaterial; //地面原始材质

    //地面Renderer
    Renderer m_GroundRenderer;

    void Awake()
    {
        //找到Push Block初始配置
        m_PushBlockSettings = FindObjectOfType<PushBlockSettings>();
    }
    /// <summary>
    /// 初始化Agent
    /// </summary>
    public override void InitializeAgent()
    {
        base.InitializeAgent();
        //设置小白脚本里的agent为小蓝
        goalDetect = block.GetComponent<GoalDetect>();
        goalDetect.agent = this;

        m_AgentRb = GetComponent<Rigidbody>();
        m_BlockRb = block.GetComponent<Rigidbody>();
        areaBounds = ground.GetComponent<Collider>().bounds;
        
        //拿到地面的的renderer,使得当小蓝完成任务或未完成任务时替换地面材质
        m_GroundRenderer = ground.GetComponent<Renderer>();
        //缓存地面原始材质
        m_GroundMaterial = m_GroundRenderer.material;
        //设置可变参数
        SetResetParameters();
    }
    /// <summary>
    /// 设置可变参数
    /// </summary>
    public void SetResetParameters()
    {
        //设置地面的两个可变参数:动摩擦系数+静摩擦系数
        SetGroundMaterialFriction();
        //设置小白块的两个可变参数:小白的X、Z方向的比例大小以及小白的所受空气阻力
        SetBlockProperties();
    }
    /// <summary>
    /// 设置地面可变参数
    /// </summary>
    public void SetGroundMaterialFriction()
    {
        var resetParams = Academy.Instance.FloatProperties;
        var groundCollider = ground.GetComponent<Collider>();

        groundCollider.material.dynamicFriction = resetParams.GetPropertyWithDefault("dynamic_friction", 0);
        groundCollider.material.staticFriction = resetParams.GetPropertyWithDefault("static_friction", 0);
    }
	/// <summary>
    /// 设置小白可变参数
    /// </summary>
    public void SetBlockProperties()
    {
        var resetParams = Academy.Instance.FloatProperties;
        var scale = resetParams.GetPropertyWithDefault("block_scale", 2);
        //小白的比例
        m_BlockRb.transform.localScale = new Vector3(scale, 0.75f, scale);
        //小白的空气摩擦阻力
        m_BlockRb.drag = resetParams.GetPropertyWithDefault("block_drag", 0.5f);
    }
}

以上代码没什么特别要讲的,只用注意之前没详细讲过的一个函数GetPropertyWithDefault(string key,float defaultValue),这个函数一般用于可变参数的设置,第一个是你要设置的可变参数的key,这个与你要训练的可变参数配置文件中的部分一一对应,如下图是当时在ML-Agents(四)3DBall补充の引入泛化中对3D Ball里的可变参数训练文件:

resampling-interval: 5000

mass:
    sampler-type: "uniform"
    min_value: 0.5
    max_value: 10

gravity:
    sampler-type: "uniform"
    min_value: 7
    max_value: 12

scale:
    sampler-type: "uniform"
    min_value: 0.75
    max_value: 3

里面的mass、gravity和scale就是在程序中设置的key一一对应。当然如果没有训练时,这个key所对应的值为空,则GetPropertyWithDefault(string key,float defaultValue)会取第二个默认参数。

Agent重置

重置代码有一点有意思的地方,先上源码:

    /// <summary>
    /// Agent重置,在Done()时会自动调用
    /// </summary>
    public override void AgentReset()
    {
        //使整个平台(训练单元)随机旋转角度0,90,180,270
        var rotation = Random.Range(0, 4);
        var rotationAngle = rotation * 90f;
        area.transform.Rotate(new Vector3(0f, rotationAngle, 0f));
        //重置小白
        ResetBlock();
        //重置小蓝
        transform.position = GetRandomSpawnPos();
        m_AgentRb.velocity = Vector3.zero;
        m_AgentRb.angularVelocity = Vector3.zero;
        //重置可变参数
        SetResetParameters();
    }
	/// <summary>
    /// 当小蓝推动小白到目标区域时调用,注意这里调用是小白的脚本GoalDetect.cs调用的
    /// </summary>
    public void ScoredAGoal()
    {
        //到达目标区域,奖励5
        AddReward(5f);
        //调用Agent的Done()函数,此时AgentReset()函数会自动执行
        Done();
        //设置地面亮绿色0.5秒
        StartCoroutine(GoalScoredSwapGroundMaterial
                       (m_PushBlockSettings.goalScoredMaterial, 0.5f));
    }
    /// <summary>
    /// 设置地面材质为绿色
    /// </summary>
    IEnumerator GoalScoredSwapGroundMaterial(Material mat, float time)
    {
        m_GroundRenderer.material = mat;//地面设置为绿色
        yield return new WaitForSeconds(time); //等待0.5s后,换回原先材质
        m_GroundRenderer.material = m_GroundMaterial;
    }
    /// <summary>
    /// 重置小白的位置和速度
    /// </summary>
    void ResetBlock()
    {
        //小白的位置随机重置
        block.transform.position = GetRandomSpawnPos();
        //使小白的速度置零
        m_BlockRb.velocity = Vector3.zero;
        //使小白的角速度置零
        m_BlockRb.angularVelocity = Vector3.zero;
    }
	/// <summary>
    /// 利用地面的Bounds范围以及碰撞关系来随机生成小白以及小蓝出现的位置
    /// </summary>
    public Vector3 GetRandomSpawnPos()
    {
        var foundNewSpawnLocation = false;
        var randomSpawnPos = Vector3.zero;
        while (foundNewSpawnLocation == false)
        {
            //随机找两个值X、Z,注意这里的m_PushBlockSettings.spawnAreaMarginMultiplier系数
            var randomPosX = Random.Range(-areaBounds.extents.x * m_PushBlockSettings.spawnAreaMarginMultiplier,
                areaBounds.extents.x * m_PushBlockSettings.spawnAreaMarginMultiplier);
            var randomPosZ = Random.Range(-areaBounds.extents.z * m_PushBlockSettings.spawnAreaMarginMultiplier,
                areaBounds.extents.z * m_PushBlockSettings.spawnAreaMarginMultiplier);
            //判断随机的位置附近是否存在碰撞体,若存在则继续随机生成位置,
            //直到生成的位置附近没有其他碰撞体
            randomSpawnPos = ground.transform.position + new Vector3(randomPosX, 1f, randomPosZ);
            if (Physics.CheckBox(randomSpawnPos, new Vector3(2.5f, 0.01f, 2.5f)) == false)
            {
                foundNewSpawnLocation = true;
            }
        }
        return randomSpawnPos;
    }

以上代码大部分都很简单,每次训练完成的条件就是小白被推入目标区域,然后调用小蓝身上的ScoredAGoal()函数,使得整个训练重置。

有意思的是最后一部分GetRandomSpawnPos()方法,即随机生成小蓝和小白位置方法。首先先看这个代码前半部分,随机的X、Z值除了随机生成,还乘以了m_PushBlockSettings.spawnAreaMarginMultiplier参数,该参数是PushBlockSettings.cs脚本中的,有了它的相乘,则规定了小白和小蓝出现的位置占场地的百分比,可以参考下图来理解:

image-20200420231830033

图中的数字大概就是m_PushBlockSettings.spawnAreaMarginMultiplier参数的取值,源码中该值设置为0.5,则小蓝和小白出现的位置就在场地中心50%方形区域范围内。这里如果值越大,相应训练的时间也就越长,也符合常理。

其次,在该代码中还有一个while()循环,这个循环主要是判断随机生成的位置周围是否有其它碰撞体(包括墙体等)。

该段代码使用了Physics.CheckBox(Vector3 center, Vector3 halfExtents, Quaternion orientation = Quaternion.identity, int layermask = DefaultRaycastLayers, QueryTriggerInteraction queryTriggerInteraction = QueryTriggerInteraction.UseGlobal)函数,当然这个函数参数很多,源码中就设置了前两个值,意思就是在center位置生成一个碰撞盒,长宽高分别为halfExtents(x、y、z)的两倍。如下图:

image-20200420232704686

如上图,碰撞盒的尺寸为(5,0.02,5),若小白在图示位置,而小蓝一次随机的位置在(0,0,0)点处,利用Physics.CheckBox()方法就检测出了在(0,0,0)点处周围有小白,因此此次生成的位置(0,0,0)并不能符合要求,然后就继续while()去重新生成位置,知道小蓝和小白离得足够远。

这里比较坑的时,注意不能把m_PushBlockSettings.spawnAreaMarginMultiplier这个值设置的太小,太小的话你会发现程序一运行就卡死。也可以想到,如果太小,while会变成死循环,因为没有足够的距离使得小白和小蓝的距离足够远而结束循环。

Agent动作反馈

Agent动作反馈代码较为简单:

	/// <summary>
    /// Agent每步动作反馈
    /// </summary>
    public override void AgentAction(float[] vectorAction)
    {
        //利用矢量空间向量移动Agent
        MoveAgent(vectorAction);
        //每步都给予惩罚使得Agent迅速完成任务,这里是-1/5000每步
        AddReward(-1f / maxStep);
    }
    /// <summary>
    /// 根据矢量动作空间action[]来使得Agent作出反应
    /// </summary>
    public void MoveAgent(float[] act)
    {
        var dirToGo = Vector3.zero;//向前向量
        var rotateDir = Vector3.zero;//转向向量
        var action = Mathf.FloorToInt(act[0]);//act[]取值范围为0~6,则action取值范围为0~6

        switch (action)
        {
            case 1:
                dirToGo = transform.forward * 1f;//向后
                break;
            case 2:
                dirToGo = transform.forward * -1f;//向前
                break;
            case 3:
                rotateDir = transform.up * 1f;//向右转
                break;
            case 4:
                rotateDir = transform.up * -1f;//向左转
                break;
            case 5:
                dirToGo = transform.right * -0.75f;//向右平移
                break;
            case 6:
                dirToGo = transform.right * 0.75f;//向左平移
                break;
        }
        transform.Rotate(rotateDir, Time.fixedDeltaTime * 200f);//旋转方向
        m_AgentRb.AddForce(dirToGo * m_PushBlockSettings.agentRunSpeed,
            ForceMode.VelocityChange);//移动
    }

至此,Agent脚本分析完毕。

其他

剩下的脚本主要是在PushBlockSettings物体上的PushBlockSettings脚本,该脚本主要可以设置本示例的公共参数。

using UnityEngine;

public class PushBlockSettings : MonoBehaviour
{
    /// <summary>
    /// Agent的速度
    /// </summary>
    public float agentRunSpeed;
    /// <summary>
    /// Agent的旋转速度
    /// </summary>
    public float agentRotationSpeed;
    /// <summary>
    /// 使用场地比例乘数,具体作用已在上一节中讲述
    /// </summary>
    public float spawnAreaMarginMultiplier;
    /// <summary>
    /// 任务达成时,地面要更换的材质
    /// </summary>
    public Material goalScoredMaterial;
    /// <summary>
    /// 任务失败时,地面要更换的材质(工程中未用到)
    /// </summary>
    public Material failMaterial;
}

五、训练

训练配置参数

基于上一篇ML-Agents(七)训练指令与训练配置文件,我们来大概研究一下Push Block的设置:

trainer_config.yaml

PushBlock:
    max_steps: 1.5e7
    batch_size: 128
    buffer_size: 2048
    beta: 1.0e-2
    hidden_units: 256
    summary_freq: 60000
    time_horizon: 64
    num_layers: 2

默认的参数就先不看,主要看PushBlock独有的设置。

  • max_steps:设置的很大,说明该次训练应该还是比较费时间的,不过场景中也相应有32个训练单元,所以也相应加快了训练速度。
  • batch_size:相比默认1024设置的较小,因为此次训练的矢量动作空间是离散的(Discrete Type),所以设置范围为32-512
  • buffer_size:是batch_size的倍数,范围为2048-409600
  • beta:相比默认设置5.0e-3,该值设置更大,也就意味着在训练过程中agent的行动更具随机性,不过如果entropy的下降太慢,则要减小该值。
  • hidden_units:默认设置为128,该值设置为256,原因是此示例相对来说观察变量之间的交互复杂度更高,因此该值需要更大。
  • summary_freq:该值设置多久保存一次统计数据,主要决定在tensorboard中显示数据点的数量。由于该训练max_steps较大,因此该值也相对默认值10000设置的大。
  • time_horizon:如果在一个episode里,agent频繁获得奖励或episode数量非常大的情况下,该值需要更小。对应于本示例训练,由于每一步都惩罚agent,要使其尽快完成训练,因此该值设置较小(PS.该值范围为32-2048)。
  • num_layers:与默认相同,定义在观察值输入之后或在视觉观察的CNN编码后存在多少个隐藏层。对于简单的问题,更少的层数可使得训练更加迅速和高效。对于更加复杂的控制问题,可能需要更多的层(PS.该值的范围为1-3)。

关于配置文件更多的配置项可以看我上一篇文章~

训练过程

我们先来使用官方不带可变参数的配置来训练一下,在命令行中输入熟悉的训练命令:

mlagents-learn config/trainer_config.yaml --run-id=PushBlock_Normal --train

进行训练,如下图:

pushblock6

刚开始小蓝蠢的要死~还不知道自己来到这个世界上要干啥。。。训练一会儿,看它能不能醒悟自己的任务。

等到一段时间后,会发现有些小蓝好像明白了自己push小白的任务,但其实大多数小蓝都只是把小白推到某个墙角就卡住了。这种情况下,初期的小蓝就只能等到这一次的最大动作数5000步后,重置来进行下一次尝试。

pushblock7

OK,我们还是等到训练一段时间后在来看效果。

随着时间的退役,我们可以观察到越来越多的训练单元达成了任务:

pushblock9

同时也可以从命令行中的Mean Reward值逐渐增加来看到学习的效果越来愈好。

image-20200426232008127

训练效果越来越好,表明参数基本设置没啥大问题,现在只需要等待训练完毕即可。训练大概3小时后,训练完毕,可以同时查看此次训练的tensorboard。

image-20200427063742264

将训练模型放入Unity中,发现可以使小蓝正确push小白到目标区域中。

pushblock10

六、总结

此次示例总体来说较为简单,主要就是学习关于射线传感器的应用。本篇文章就此结束,欢迎大家留言交流~

写文不易~因此做以下申明:

1.博客中标注原创的文章,版权归原作者 煦阳(本博博主) 所有;

2.未经原作者允许不得转载本文内容,否则将视为侵权;

3.转载或者引用本文内容请注明来源及原作者;

4.对于不遵守此声明或者其他违法使用本文内容者,本人依法保留追究权等。

posted @ 2020-04-27 20:58  煦阳  阅读(2805)  评论(3编辑  收藏  举报