ML-Agents(十)Crawler

1、前言

今天是六一,先祝你們六一快樂!距上次發文章已通過了快一個月,工做有點忙,因此有點拖更,見諒~c#

咱們此次來研究一下Crawler(爬蟲)示例。官方其實還有個示例——Reacher,可是這個示例比較簡單,就是模擬一個帶兩個關節的手臂去跟隨目標物體,其核心就是讓咱們學會如何利用Configurable Joint來進行訓練,相比之下,Crawler就複雜得多,所以咱們跳過Reacher這個示例,感興趣的童靴能夠本身去研究一下。dom

先來看看Crawler示例的效果:ide

crawler1

能夠看到此次小藍變成了一隻四腳爬蟲,每隻腳上有兩個關節,其任務就是經過四肢協調運動找到綠色方塊。大概能夠預想到,這個示例在開始訓練的時候先要解決的問題就是小藍怎麼經過四肢協調站起來不摔倒,而後進行移動,最後纔是找到綠色方塊。函數

此外,該示例有兩個場景:CrawlerDynamicTarget和CrawlerStaticTarget,分別是動態目標物和靜態目標物,上圖展現的就是動態生成目標物,綠色的方塊會產生到隨機的位置,然後者是綠色方塊就在小藍的前方。所以咱們直接研究較難的CrawlerDynamicTarget示例,CrawlerStaticTarget也就迎刃而解了。學習

2、環境與訓練參數

  • 設定:有四隻胳膊和四隻前臂的生物。測試

  • 目標:Agent必須移動它的身體朝目標方向移動而不摔倒。ui

    • CrawlerStaticTarget:目標方向一直在前方。
    • CrawlerDynamicTarget:目標會隨機改變位置。
  • Agents:環境中包含8個相同行爲參數的agent。this

  • Agent獎勵設定(獨立的):spa

    • 若小藍的速度方向朝向目標方向,則+0.03*(速度與目標方向的點積)。
    • 若小藍面朝目標方向,則+0.01*(小藍向前方向與目標方向的點積)。
    • 隨着時間的增長,每一幀-0.001(PS.該獎勵是可勾選可不勾選的,官方示例默認未勾選)。

    Note:這裏來講一下,爲何獎勵這樣設定。首先要明白點積的概念:從幾何意義來說,如有兩個向量ab,則a·b = |a||b|cos(a, b),由此可推出:pwa

    ①當a·b>0,兩向量方向基本相同,夾角在0°到90°之間;

    ②當a·b=0,兩向量正交,相互垂直;

    ③當a·b<0,兩向量方向基本相反,夾角在90°到180°之間。

    所以,當小藍和目標速度或朝向大於90°時,實際上是在獎勵負數,由此來迫使小藍面朝目標物而且向目標物前進,這裏的設置其實還比較巧妙,能夠注意一下。

  • 行爲參數:

    • 矢量觀測空間:117個變量,分別對應於每一個肢節的位置、角度、速度、角速度,再加小藍body的加速度和角加速度。
    • 矢量動做空間:(Continuous)20個變量,對應關節的轉動。
    • 視覺觀察:無。
  • 可變參數:無。

  • 基準平均獎勵:

    CrawlerStaticTarget:2000。

    CrawlerDynamicTarget:400。

3、場景基本結構

這個示例的場景很簡單:

image-20200508212445189

其中,CrawlerSettings裏有一個AdjustTrainingTimescale腳本,該腳本就是經過數字鍵來改變Time.timeScale屬性的,說是能夠在訓練的時候用,本次訓練的時候我會試一下改變Time.timeScale可否加快訓練速度。

而後來詳細講一下訓練單元:

image-20200508213237779

訓練單元裏的Walls、Ground以及Target都如同直譯,沒啥特別說的,主要看一下Crawler:

image-20200508214745481

Crawler身上有5個腳本,BehaviorParameters是行爲參數腳本,只要有繼承Agent的腳本,則會自動附加該腳本;CrawlerAgent則是agent訓練腳本;DecisionRequester會按期自動爲agent請求決策,若是沒有該腳本,則須要手動調用Agent.RequestDecision()方法,不過以前的例子其實Agent上都有這個腳本,我之前忘記講了;JointDriveController控制各個關節,具體還有什麼做用一下子代碼解析的時候再來看;ModelOverrider這個腳本在0.15.0裏是沒有的,這個腳本能夠先不用管它,這個腳本的做用大概是在訓練前,在Console裏輸入指定的命令,容許在訓練期間覆蓋代理的.nn模型文件。

除此以外,Crawler的Body還有一個GroundContact腳本,該腳本是用來檢測Crawler是否摔倒,即頭部觸地,此時能夠對agent懲罰1,並從新開始新的Episode,這兩個都是可選的:

image-20200508222450705

Crawler有四隻前臂和四隻腿,再加一個Body:

image-20200508221933225

它們是以Configuration Joint(關節組件)兩兩相連的,以一部分爲例:

image-20200508222712491

這裏小提一下可配置關節 (Configurable Joint),該組件具備很是強的自定義性,具體能夠看下圖:

image-20200508223030033

能夠配置的參數很是多,但其實不少都是基礎的參數,各個參數是如何限制該關節的,建議你們本身下去後再去深刻研究,咱們這裏大概能夠看一下Crawler的四肢是如何運動的:
首先看前臂:

image-20200508223854107

前臂的繞X、Y角旋轉沒被鎖定(Locked),其他的都被鎖定了。

再看一下後臂:

image-20200508224118852

後臂的只有繞X軸是沒被鎖定的,其他的都被鎖定了。

這樣組合的結果就是(靈魂畫手,能看懂便可。。。):

image-20200508224446490

前臂能夠繞自身中心軸轉,能夠繞body上下轉,然後臂只能繞前臂上下。該關節配置構成了Crawler運動的基礎。

每一個後臂還有一個腳的子物體(就是那個小圓球),我看官方原本是想在腳接觸地面時令小球換個材質,不接觸地面時又是另外一種材質,可是示例中最終並無使用該方法,咱們能夠來試試這個功能:

創建兩個材質球Red和Green,而後分別將這兩個材質球拖到Crawler預製體的Agent腳本上,並勾選Use Foot Grounded Visualization選項:

image-20200508230453030

運行的話咱們就會有如下效果:

crawler2

腳基礎地面就變成綠色,不接觸地面就是紅色。

4、代碼分析

本示例的代碼首先須要理解Agent身上的JointDriveController腳本,該腳本用於設置Crawler的肢體關節協調(其餘三個示例Walker、Warm也同樣用到該腳本),同時該腳本中還包括BodyPart腳本,用於存儲agent中每一個肢體部分的相關信息。

咱們先來看一下BodyPart結構,是如何對Crawler的身體作存儲的。

BodyPart

/// <summary>
    /// 用於存儲agent每一個身體部位的行動和學習相關信息
    /// </summary>
    [System.Serializable]
    public class BodyPart
    {
        [Header("Body Part Info")] [Space(10)] public ConfigurableJoint joint;//身體的可配置關節組件
        public Rigidbody rb;//剛體
        [HideInInspector] public Vector3 startingPos;//起始位置
        [HideInInspector] public Quaternion startingRot;//起始角度

        [Header("Ground & Target Contact")]
        [Space(10)]
        public GroundContact groundContact;//檢測地面接觸
        public TargetContact targetContact;//檢測目標接觸
        
        [FormerlySerializedAs("thisJDController")]
        [HideInInspector] public JointDriveController thisJdController;//關節組件Controller

        [Header("Current Joint Settings")]
        [Space(10)]
        public Vector3 currentEularJointRotation;//關節當前歐拉角

        [HideInInspector] public float currentStrength;//當前做用力
        public float currentXNormalizedRot;
        public float currentYNormalizedRot;
        public float currentZNormalizedRot;

        [Header("Other Debug Info")]
        [Space(10)]
        public Vector3 currentJointForce;//當前關節做用力

        public float currentJointForceSqrMag;//當前關節做用力大小
        public Vector3 currentJointTorque;//當前關節轉矩
        public float currentJointTorqueSqrMag;//當前關節轉矩大小
        public AnimationCurve jointForceCurve = new AnimationCurve();//關節做用力曲線
        public AnimationCurve jointTorqueCurve = new AnimationCurve();//關節力矩曲線

        /// <summary>
        /// Reset body part to initial configuration.
        /// 身體關節初始化
        /// </summary>
        public void Reset(BodyPart bp)
        {
            bp.rb.transform.position = bp.startingPos;//位置
            bp.rb.transform.rotation = bp.startingRot;//角度
            bp.rb.velocity = Vector3.zero;//速度
            bp.rb.angularVelocity = Vector3.zero;//角速度
            if (bp.groundContact)
            {//地面接觸標誌置位
                bp.groundContact.touchingGround = false;
            }

            if (bp.targetContact)
            {//目標接觸標誌置位
                bp.targetContact.touchingTarget = false;
            }
        }

        /// <summary>
        /// 根據給定的x,y,z角度和力的大小計算扭矩
        /// </summary>
        public void SetJointTargetRotation(float x, float y, float z)
        {
            x = (x + 1f) * 0.5f;
            y = (y + 1f) * 0.5f;
            z = (z + 1f) * 0.5f;

            //Mathf.Lerp(from : float, to : float, t : float) 插值,t=0~1,返回(to-from)*t
            var xRot = Mathf.Lerp(joint.lowAngularXLimit.limit, joint.highAngularXLimit.limit, x);
            var yRot = Mathf.Lerp(-joint.angularYLimit.limit, joint.angularYLimit.limit, y);
            var zRot = Mathf.Lerp(-joint.angularZLimit.limit, joint.angularZLimit.limit, z);

            //Mathf.InverseLerp(from : float, to : float, value : float)反插值,返回value在from和to之間的比例值
            currentXNormalizedRot = Mathf.InverseLerp(joint.lowAngularXLimit.limit, joint.highAngularXLimit.limit, xRot);
            currentYNormalizedRot = Mathf.InverseLerp(-joint.angularYLimit.limit, joint.angularYLimit.limit, yRot);
            currentZNormalizedRot = Mathf.InverseLerp(-joint.angularZLimit.limit, joint.angularZLimit.limit, zRot);

            joint.targetRotation = Quaternion.Euler(xRot, yRot, zRot);//使關節轉向目標角度
            currentEularJointRotation = new Vector3(xRot, yRot, zRot);//當前關節歐拉角
        }
        /// <summary>
        /// 設置關節做用力大小
        /// </summary>
        /// <param name="strength"></param>
        public void SetJointStrength(float strength)
        {
            var rawVal = (strength + 1f) * 0.5f * thisJdController.maxJointForceLimit;
            var jd = new JointDrive
            {
                positionSpring = thisJdController.maxJointSpring,//關節最大彈力
                positionDamper = thisJdController.jointDampen,//關節彈性大小
                maximumForce = rawVal//施加的最大力
            };
            joint.slerpDrive = jd;
            currentStrength = jd.maximumForce;//當前施加的力
        }
    }

以上代碼說難不難,說簡單也不簡單。。。主要是涉及到Joint組件的使用,這裏面牽扯到一些力學知識,我就不望文生義了,有興趣的同窗能夠深刻研究一下。

JointDriveController

/// <summary>
    /// Joint控制器
    /// </summary>
    public class JointDriveController : MonoBehaviour
    {
        [Header("Joint Drive Settings")]
        [Space(10)]
        public float maxJointSpring;//關節最大彈力大小
        public float jointDampen;//關節抵抗彈力的強度
        public float maxJointForceLimit;//最大做用力
        //float m_FacingDot;//該變量沒用到

        //身體部位字典
        [HideInInspector] public Dictionary<Transform, BodyPart> bodyPartsDict = new Dictionary<Transform, BodyPart>();

        /// <summary>
        /// 建立BodyPart對象並將其添加到字典中
        /// </summary>
        public void SetupBodyPart(Transform t)
        {
            var bp = new BodyPart
            {
                rb = t.GetComponent<Rigidbody>(),
                joint = t.GetComponent<ConfigurableJoint>(),
                startingPos = t.position,
                startingRot = t.rotation
            };
            bp.rb.maxAngularVelocity = 100;//最大角速度爲100

            //添加地面碰撞檢測腳本
            bp.groundContact = t.GetComponent<GroundContact>();
            if (!bp.groundContact)
            {
                bp.groundContact = t.gameObject.AddComponent<GroundContact>();
                bp.groundContact.agent = gameObject.GetComponent<Agent>();
            }
            else
            {
                bp.groundContact.agent = gameObject.GetComponent<Agent>();
            }

            //添加目標碰撞檢測腳本
            bp.targetContact = t.GetComponent<TargetContact>();
            if (!bp.targetContact)
            {
                bp.targetContact = t.gameObject.AddComponent<TargetContact>();
            }

            bp.thisJdController = this;
            bodyPartsDict.Add(t, bp);
        }
        /// <summary>
        /// 更新身體每一部分當前的做用力及轉矩
        /// </summary>
        public void GetCurrentJointForces()
        {
            foreach (var bodyPart in bodyPartsDict.Values)
            {//輪詢身體每部分
                if (bodyPart.joint)
                {
                    bodyPart.currentJointForce = bodyPart.joint.currentForce;//當前關節做用力
                    bodyPart.currentJointForceSqrMag = bodyPart.joint.currentForce.magnitude;//當前關節做用力大小
                    bodyPart.currentJointTorque = bodyPart.joint.currentTorque;//當前關節做用轉矩
                    bodyPart.currentJointTorqueSqrMag = bodyPart.joint.currentTorque.magnitude;//當前關節做用轉矩大小
                    if (Application.isEditor)
                    {//IDE下,建立關節做用力和關節力矩的曲線
                        if (bodyPart.jointForceCurve.length > 1000)
                        {
                            bodyPart.jointForceCurve = new AnimationCurve();
                        }

                        if (bodyPart.jointTorqueCurve.length > 1000)
                        {
                            bodyPart.jointTorqueCurve = new AnimationCurve();
                        }

                        bodyPart.jointForceCurve.AddKey(Time.time, bodyPart.currentJointForceSqrMag);
                        bodyPart.jointTorqueCurve.AddKey(Time.time, bodyPart.currentJointTorqueSqrMag);
                    }
                }
            }
        }
    }

這個腳本主要是將多個BodyPart進行管理的做用,同時能夠實時更新身體每一部分做用力及轉矩,用以Agent收集

BodyPart的相關信息。

以上兩個腳本我註釋的比較粗略,主要是對Joint組件的不熟悉形成的,該組件使用的細節我就不深刻講解了,咱們主要能弄清楚ml-agents是如何對這種多關節複雜的agent進行訓練的就能夠了。

GroundContact

/// <summary>
    /// 該腳本包含了agent可能與地面接觸的關節運動的邏輯。經過該腳本,能夠設置某些身體部位若是接觸地面後作出懲罰
    /// </summary>
    [DisallowMultipleComponent] //不可重複掛載特性
    public class GroundContact : MonoBehaviour
    {
        [HideInInspector] public Agent agent;//對應的agent

        //當接觸地面時,是否令agent置位
        [Header("Ground Check")] public bool agentDoneOnGroundContact;
        //是否在接觸地面時懲罰agent
        public bool penalizeGroundContact;
        //接觸地面懲罰的數值
        public float groundContactPenalty;
        //接觸地面標誌
        public bool touchingGround;
        //地面物體的tag
        const string k_Ground = "ground";

        /// <summary>
        /// 檢測碰撞是否爲地面
        /// </summary>
        void OnCollisionEnter(Collision col)
        {
            if (col.transform.CompareTag(k_Ground))
            {//碰撞到地面
                touchingGround = true;
                if (penalizeGroundContact)
                {//懲罰agent
                    agent.SetReward(groundContactPenalty);
                }
                if (agentDoneOnGroundContact)
                {//使得agent從新開始
                    agent.EndEpisode();
                }
            }
        }
        /// <summary>
        /// 檢查地面碰撞是否結束,並使其標誌復位
        /// </summary>
        void OnCollisionExit(Collision other)
        {
            if (other.transform.CompareTag(k_Ground))
            {
                touchingGround = false;
            }
        }
    }

該腳本在小藍的每一個BodyPart上都有掛載,咱們能夠來詳細看一下:

image-20200525212333536

image-20200525212400364

image-20200525212417000

由上圖可知,當agent的leg以及Body接觸地面後,會使agent懲罰1,並使得agent從新開始新的一輪訓練;而foreLeg接觸地面則不作任何懲罰。

同時能夠留意一下腳本一開始的[DisallowMultipleComponent]特性,該特性可以使得腳本組件只在同一物體上存在一個。

下面咱們來看一下Crawler的Agent腳本。

CrawlerAgent

Agent初始化

/// <summary>
/// Crawler的Agent腳本
/// </summary>
[RequireComponent(typeof(JointDriveController))]//要求JointDriveController腳本同時存在
public class CrawlerAgent : Agent
{
    [Header("Target To Walk Towards")]
    [Space(10)]
    public Transform target;//目標方塊
    public Transform ground;//地面
    public bool detectTargets;//檢測目標標誌
    public bool targetIsStatic;//目標物是不是靜態的
    public bool respawnTargetWhenTouched;//當爲true時,到達目標後,目標會從新隨機到其餘地方
    public float targetSpawnRadius;//目標隨機位置半徑

    //各部分BodyPart
    [Header("Body Parts")] [Space(10)] public Transform body;
    public Transform leg0Upper;
    public Transform leg0Lower;
    public Transform leg1Upper;
    public Transform leg1Lower;
    public Transform leg2Upper;
    public Transform leg2Lower;
    public Transform leg3Upper;
    public Transform leg3Lower;

    [Header("Joint Settings")] [Space(10)] JointDriveController m_JdController;//Joint控制器
    Vector3 m_DirToTarget;//小藍到目標物的方向
    float m_MovingTowardsDot;//小藍速度方向與目標方向的點積
    float m_FacingDot;//小藍正方向與目標方向的點積

    [Header("Reward Functions To Use")]
    [Space(10)]
    public bool rewardMovingTowardsTarget;//速度方向與目標方向獎勵是否開啓
    public bool rewardFacingTarget;//小藍正方向與目標方向點積是否開啓
    public bool rewardUseTimePenalty;//是否隨時間流逝而懲罰

    [Header("Foot Grounded Visualization")]
    [Space(10)]
    public bool useFootGroundedVisualization;//是否使腳接觸地面改變材質
    
    public MeshRenderer foot0;
    public MeshRenderer foot1;
    public MeshRenderer foot2;
    public MeshRenderer foot3;
    public Material groundedMaterial;//接觸地面腳的材質
    public Material unGroundedMaterial;//未接觸地面腳的材質

    Quaternion m_LookRotation;//小藍到目標方向四元數
    Matrix4x4 m_TargetDirMatrix;//目標方向旋轉矩陣

    /// <summary>
    /// Agent初始化
    /// </summary>
    public override void Initialize()
    {
        m_JdController = GetComponent<JointDriveController>();//得到Joint控制器
        m_DirToTarget = target.position - body.position;//小藍到目標方向向量

        //Setup each body part
        //設置身體每一部分
        m_JdController.SetupBodyPart(body);
        m_JdController.SetupBodyPart(leg0Upper);
        m_JdController.SetupBodyPart(leg0Lower);
        m_JdController.SetupBodyPart(leg1Upper);
        m_JdController.SetupBodyPart(leg1Lower);
        m_JdController.SetupBodyPart(leg2Upper);
        m_JdController.SetupBodyPart(leg2Lower);
        m_JdController.SetupBodyPart(leg3Upper);
        m_JdController.SetupBodyPart(leg3Lower);
    }
}

這段代碼主要注意一下上面將要使用的變量。

Agent環境觀測值收集

/// <summary>
    /// 觀測值收集
    /// </summary>
    /// <param name="sensor"></param>
    public override void CollectObservations(VectorSensor sensor)
    {
        m_JdController.GetCurrentJointForces();//更新身體每一部分當前的做用力及轉矩

        //更新小藍到目標的方向
        m_DirToTarget = target.position - body.position;//向量agent到target
        m_LookRotation = Quaternion.LookRotation(m_DirToTarget);//獲取小藍正向到目標向量的四元數
        m_TargetDirMatrix = Matrix4x4.TRS(Vector3.zero, m_LookRotation, Vector3.one);//將上述四元數轉換爲旋轉矩陣

        //Body到地面的高度(下方測量值)
        RaycastHit hit;
        if (Physics.Raycast(body.position, Vector3.down, out hit, 10.0f))
        {
            sensor.AddObservation(hit.distance);
        }
        else
            sensor.AddObservation(10.0f);

        //前方、上方測量值收集
        //獲取body的正向到目標方向轉換的相對向量
        var bodyForwardRelativeToLookRotationToTarget = m_TargetDirMatrix.inverse.MultiplyVector(body.forward);
        sensor.AddObservation(bodyForwardRelativeToLookRotationToTarget);
        //獲取body的上方到目標方向轉換的相對向量
        var bodyUpRelativeToLookRotationToTarget = m_TargetDirMatrix.inverse.MultiplyVector(body.up);
        sensor.AddObservation(bodyUpRelativeToLookRotationToTarget);

        foreach (var bodyPart in m_JdController.bodyPartsDict.Values)
        {//收集身體每一部分的測量值
            CollectObservationBodyPart(bodyPart, sensor);
        }
    }
    /// <summary>
    /// 將每一個身體部位的相關信息添加到觀察中
    /// </summary>
    public void CollectObservationBodyPart(BodyPart bp, VectorSensor sensor)
    {
        var rb = bp.rb;
        //是否接觸地面
        sensor.AddObservation(bp.groundContact.touchingGround ? 1 : 0); 
        //bp速度方向相對於目標方向的相對矢量,即速度與目標方向關係
        var velocityRelativeToLookRotationToTarget = m_TargetDirMatrix.inverse.MultiplyVector(rb.velocity);
        sensor.AddObservation(velocityRelativeToLookRotationToTarget);
        //bp角加速度方向相對於目標方向的相對矢量,即角速度與目標方向關係
        var angularVelocityRelativeToLookRotationToTarget = m_TargetDirMatrix.inverse.MultiplyVector(rb.angularVelocity);
        sensor.AddObservation(angularVelocityRelativeToLookRotationToTarget);

        if (bp.rb.transform != body)
        {//除了body以外的部分,獲取每一部分(肢體)的相對位置,x、y、z當前角度以及當前做用力
            var localPosRelToBody = body.InverseTransformPoint(rb.position);
            sensor.AddObservation(localPosRelToBody);
            sensor.AddObservation(bp.currentXNormalizedRot); // Current x rot
            sensor.AddObservation(bp.currentYNormalizedRot); // Current y rot
            sensor.AddObservation(bp.currentZNormalizedRot); // Current z rot
            sensor.AddObservation(bp.currentStrength / m_JdController.maxJointForceLimit);
        }
    }

這部分代碼我認爲是Crawler示例中比較重要的部分,由於從中能夠學習到如何對於多關節的複雜問題進行數據收集,這裏面又涉及到一些角度轉換的問題,例如四元數、轉換矩陣操做等。

整體來,這部分數據主要以下:小藍forward到目標的相對旋轉關係,小藍up到目標的相對關係,每一部分速度方向、角速度相對於目標方向關係,每部分肢節的做用力及旋轉角度等。

這裏有興趣的童靴能夠仔細研究一下,我這裏主要來搞一些四元數的相關用法。

  • 四元數(Quaternion)

    網上關於四元數的文章應該不少了,我按我得理解寫一下,有錯誤請指正。

    首先說到四元數,主要用到的地方就是三維世界中物體的旋轉,對於三維世界中描述物體的旋轉,咱們通常有三種方法表示:旋轉矩陣、歐拉角、四元數。

    以一個點p爲例,以上述三種方法旋轉獲得p',則有:

    • 旋轉矩陣

      旋轉矩陣乘以點p的齊次座標,獲得旋轉後的的點p':

      image-20200531195201386

      另,繞x,y,z軸旋轉θ的矩陣爲:

      image-20200531200229579

    • 歐拉角

      歐拉角描述旋轉,是咱們一般用的方式,例以下圖,能夠將其旋轉分解爲三步(藍色爲起始座標系,紅色爲旋轉後的座標系):

      image-20200531200756335

      先繞z軸旋轉α,再繞x軸旋轉β,最後繞z軸旋轉γ。固然這裏的旋轉順序並非規定死的,在Unity中,旋轉的順序是ZXY順序。

      那麼對應於以上歐拉角的旋轉矩陣爲:

      image-20200531201207752

      不過歐拉角有個解決不了的問題,即「萬向節死鎖」問題,同時使用歐拉角也不能進行平滑插值。

    • 四元數

      四元數實際上是一種高階複數,它能夠很方便的描述物體繞任意軸的旋轉,四元數q能夠表示爲:

      image-20200531201637816

      其中,i、j、k知足:

      image-20200531201657359

      同時四元數又能夠寫成一個向量和一個實數的組合形式:

      image-20200531201738536

      四元數能夠看做是向量和實數的更加通常的形式,咱們普通用的向量能夠視爲實部爲0的四元數,而實數能夠視爲虛部爲0的四元數,由此能夠獲得一些四元數符合實數或者向量的運算性質(感興趣的同窗能夠本身去查,例如四元數的乘法、共軛四元數、四元數的逆等)。

      利用四元數來刻畫三維空間中的旋轉,令點p繞單位向量(x,y,z)表示的軸旋轉θ,則可申明一個四元數q:

      image-20200531202214278

      再令咱們要旋轉的p點寫成四元數的形式p(P,0)(至關於虛部爲p,實部爲0),則旋轉後的p'能夠用如下公式計算:

      image-20200531202545007

      固然這個公式的右邊能夠看到,是三個四元數的乘法,最後獲得的p'也是一個四元數。

  • Unity中四元數的API

    上面咱們講了三種旋轉方式,都只是比較簡單的講解,有許多細節其實並無涉及到,其實能夠分別利用三種方法對一個點去計算旋轉後的位置,這樣能夠更加深入的加深印象。

    在Unity中,咱們大多數使用的是歐拉角來描述物體的旋轉,但其實四元數更加方便,功能更增強大。可是四元數的實部和虛部若是你不是很瞭解,則不要去修改它們,這裏咱們只是解析一下Quaternion的一些API用法。

    • Quaternion.AngleAxis(float angle, Vector3 axis)

      這個方法其實就是四元數的原本用法,即繞某軸axis旋轉angle角度,例如,使得一個Cube繞Unity中x軸旋轉45度,則有

      q = Quaternion.AngleAxis(45, Vector3.right);Cube.transform.rotation = q;

      上式中q爲任意四元數,效果以下圖:

      初始位置,藍色線爲物體自身z軸,綠色爲y軸,紅色爲x軸。

      image-20200531204445451

      變換後:

      image-20200531204737582

      固然,咱們也能夠利用該函數使得Cube繞任意軸旋轉angle角度,咱們在場景中放置一個軸:

      image-20200531205038521

      image-20200531205509261

      而後讓Cube繞這這根軸旋轉45度,既有:

      q = Quaternion.AngleAxis(45, Axis.transform.up);Cube.transform.rotation = q;

      image-20200531205547070

    • Quaternion.LookRotation(Vector3 forward, Vector3 upwards = Vector3.up)

      這個函數其實就是讓物體的前方指向forward方向,物體的上方指向upwards方向(可不賦值)。例如,我如今要讓Cube的前方指向下,上方指向前,則有:

      q = Quaternion.LookRotation(Vector3.down, Vector3.forward);Cube.transform.rotation = q;

      image-20200531210335029

      固然這個函數就能夠衍生出一些還玩的用法,例如同步兩個物體的旋轉,咱們引入一個Target球體:

      image-20200531210737980

      如今使得方塊的前方與球體的上方一致,方塊的上方與球體的後方一致,將代碼在Update中執行:

      q = Quaternion.LookRotation(Target.transform.up, -Target.transform.forward);Cube.transform.rotation = q;

      則有:

      crawler3

      能夠看到上圖中方塊的前方(藍色軸)一直與球體的上方(綠色軸)一致,而方塊的上方(綠色軸)一直與球體的後方一致。

      除此以外,咱們還能夠利用該方法使得方塊一直面向球體:

      q = Quaternion.LookRotation(Target.transform.position);Cube.transform.rotation = q;

      crawler4

      固然這樣寫有個弊端,就是方塊的位置不能移動,若是移動的話,則該方法失效:

      crawler5

      能夠看到將方塊上移一些,則不能看向目標球體了,所以咱們能夠對這段代碼改造一下:

      var vec = Target.transform.position - Cube.transform.position;q = Quaternion.LookRotation(vec);Cube.transform.rotation = q;

      crawler6

      這樣就能夠一直使得方塊的前方指向目標球體了。

    • Quaternion.FromToRotation(Vector3 fromDirection, Vector3 toDirection)

      這個函數主要是將某個方向fromDirection指向另外一個方向toDirection,例如將Cube的前方指向Cube的下方,則有:

      q = Quaternion.FromToRotation(Cube.transform.forward, -Cube.transform.up);

      固然,除此以外,咱們會發現若是使用Quaternion.LookRotation能夠令方塊前方一直看向目標球體,那若是想讓方塊的上方一直看向目標球體怎麼辦呢?這裏就須要使用Quaternion.FromToRotation來操做了,一開始你可能會寫出如下代碼:

      q = Quaternion.FromToRotation(Cube.transform.up, Target.transform.position);

      可是這段代碼是有問題的,會使得Cube產生抖動:

      crawler7

      因此這裏實際上是應該這麼寫:

      q = Quaternion.FromToRotation(Vector3.up, Target.transform.position);

      這樣就能實現方塊的上方一直指向目標球體,與上面同理,再次改造一下,使得方塊移動位置也能夠指向球體,則有:

      var vec= Target.transform.position - Cube.transform.position;q = Quaternion.FromToRotation(Vector3.up, vec);

      crawler8

      OK,到這裏咱們對於四元數這幾個函數就講解到這,算是拋磚引玉,若是理解有什麼不正確的地方還請留言指正。附上測試的代碼,註釋能夠本身進行撤銷去測試,基本都是上面講解中涉及到的代碼:

      public class QuaTest : MonoBehaviour
      {
          public GameObject Cube;
          public GameObject Axis;
          public GameObject Target;
      
          private Quaternion q;
      
          void Update()
          {
              //向右(x)
              Debug.DrawLine(Cube.transform.position, Cube.transform.localPosition + Cube.transform.right, Color.red);
              Debug.DrawLine(Target.transform.position, Target.transform.localPosition + Target.transform.right, Color.red);
              //向前(z)
              Debug.DrawLine(Cube.transform.position, Cube.transform.localPosition + Cube.transform.forward, Color.blue);
              Debug.DrawLine(Target.transform.position, Target.transform.localPosition + Target.transform.forward, Color.blue);
              //向上(y)
              Debug.DrawLine(Cube.transform.position, Cube.transform.localPosition + Cube.transform.up, Color.green);
              Debug.DrawLine(Target.transform.position, Target.transform.localPosition + Target.transform.up, Color.green);
      
              if (Input.GetKeyDown(KeyCode.Q))
              {
                  //Cube繞x軸旋轉45度
                  //q = Quaternion.AngleAxis(45, Vector3.right);
      
                  //Cube繞Axis自定義軸旋轉45度
                  //q = Quaternion.AngleAxis(45, Axis.transform.up);
      
                  //繞x軸旋轉90度
                  //q = Quaternion.Euler(90, 0, 0);//歐拉角實現
                  //q = Quaternion.LookRotation(Vector3.down, Vector3.forward);//令物體的前方指向下,上方指向前
                  //q = Quaternion.AngleAxis(90, Vector3.right);//令物體繞右軸(x軸)旋轉90度
                  //q = Quaternion.FromToRotation(Vector3.forward, Vector3.down);//令物體的前方指向物體的下方,不能使用自身座標系
                  //q = Quaternion.FromToRotation(Cube.transform.forward, -Cube.transform.up);//令物體的前方指向物體的下方,不能使用自身座標系
      
                  Cube.transform.rotation = q;
              }
              //令方塊前方與球體上方一致,方塊上方與球體後方一致,即令方塊的旋轉與球的旋轉同步
              //q = Quaternion.LookRotation(Target.transform.up, -Target.transform.forward);
      
              //令方塊一直面向目標球體,若Cube自身座標變了,則失效
              //q = Quaternion.LookRotation(Target.transform.position);
              //令方塊一直面向目標球體,Cube自身座標變也一直面向
              //var vec = Target.transform.position - Cube.transform.position;
              //q = Quaternion.LookRotation(vec);
      
              //令方塊上方一直看向目標球體
              //q = Quaternion.FromToRotation(Cube.transform.up, Target.transform.position);//若是使用自身的向上向量,會產生抖動
              //q = Quaternion.FromToRotation(Vector3.up, Target.transform.position);
              var vec= Target.transform.position - Cube.transform.position;
              q = Quaternion.FromToRotation(Vector3.up, vec);//注,這裏須要使用世界座標向上,而不能使用自身座標系的向上向量
      
              Cube.transform.rotation = q;
          }
      }

Agent動做反饋

/// <summary>
    /// 動做反饋
    /// </summary>
    /// <param name="vectorAction"></param>
    public override void OnActionReceived(float[] vectorAction)
    {
        //獲取全部部分
        var bpDict = m_JdController.bodyPartsDict;

        var i = -1;
        //設置每一部分的角度
        bpDict[leg0Upper].SetJointTargetRotation(vectorAction[++i], vectorAction[++i], 0);
        bpDict[leg1Upper].SetJointTargetRotation(vectorAction[++i], vectorAction[++i], 0);
        bpDict[leg2Upper].SetJointTargetRotation(vectorAction[++i], vectorAction[++i], 0);
        bpDict[leg3Upper].SetJointTargetRotation(vectorAction[++i], vectorAction[++i], 0);
        bpDict[leg0Lower].SetJointTargetRotation(vectorAction[++i], 0, 0);
        bpDict[leg1Lower].SetJointTargetRotation(vectorAction[++i], 0, 0);
        bpDict[leg2Lower].SetJointTargetRotation(vectorAction[++i], 0, 0);
        bpDict[leg3Lower].SetJointTargetRotation(vectorAction[++i], 0, 0);

        //設置每一部分的做用力
        bpDict[leg0Upper].SetJointStrength(vectorAction[++i]);
        bpDict[leg1Upper].SetJointStrength(vectorAction[++i]);
        bpDict[leg2Upper].SetJointStrength(vectorAction[++i]);
        bpDict[leg3Upper].SetJointStrength(vectorAction[++i]);
        bpDict[leg0Lower].SetJointStrength(vectorAction[++i]);
        bpDict[leg1Lower].SetJointStrength(vectorAction[++i]);
        bpDict[leg2Lower].SetJointStrength(vectorAction[++i]);
        bpDict[leg3Lower].SetJointStrength(vectorAction[++i]);
    }

注意這裏Action Space Type爲Continuous,且Space Size爲20

Agent重置

/// <summary>
    /// Agent重置,使得身體各個部分初始化等
    /// </summary>
    public override void OnEpisodeBegin()
    {
        if (m_DirToTarget != Vector3.zero)
        {//讓小藍正向面對目標物
            transform.rotation = Quaternion.LookRotation(m_DirToTarget);
        }
        transform.Rotate(Vector3.up, Random.Range(0.0f, 360.0f));//使得小藍隨機旋轉一個角度

        foreach (var bodyPart in m_JdController.bodyPartsDict.Values)
        {//身體各部分置位
            bodyPart.Reset(bodyPart);
        }
        if (!targetIsStatic)
        {//若是開啓動態目標物,則隨機重置目標物位置
            GetRandomTargetPos();
        }
    }
    /// <summary>
    /// 使得目標方塊位置隨機生成
    /// </summary>
    public void GetRandomTargetPos()
    {
        //Random.insideUnitSphere:返回半徑爲1的球體內的一個隨機點
        var newTargetPos = Random.insideUnitSphere * targetSpawnRadius;
        newTargetPos.y = 5;
        target.position = newTargetPos + ground.position;
    }

在以前的版本中,Agent重置函數爲AgentReset(),如今版本改名爲OnEpisodeBegin()

此外,以上代碼段中能夠看一下隨機產生目標物的方法,其使用了UnityEngine.Random.insideUnitSphere屬性,該值會返回一個半徑爲1的球體內的一個隨機點,除此以外,該類中還提供onUnitSphere(在球上隨機位置),insideUnitCircle(在平面圓內隨機位置)兩個屬性。

其餘

void FixedUpdate()
    {
        if (detectTargets)
        {//開啓檢測碰撞目標獎勵
            foreach (var bodyPart in m_JdController.bodyPartsDict.Values)
            {//每幀遍歷身體的每一個部分,是否碰撞到目標
                if (bodyPart.targetContact && bodyPart.targetContact.touchingTarget)
                {//碰撞到目標,則獎勵1,並根據自選項重置目標位置
                    TouchedTarget();
                }
            }
        }
        
        if (useFootGroundedVisualization)
        {//是否開啓碰撞地板腳變材質功能
            foot0.material = m_JdController.bodyPartsDict[leg0Lower].groundContact.touchingGround
                ? groundedMaterial
                : unGroundedMaterial;
            foot1.material = m_JdController.bodyPartsDict[leg1Lower].groundContact.touchingGround
                ? groundedMaterial
                : unGroundedMaterial;
            foot2.material = m_JdController.bodyPartsDict[leg2Lower].groundContact.touchingGround
                ? groundedMaterial
                : unGroundedMaterial;
            foot3.material = m_JdController.bodyPartsDict[leg3Lower].groundContact.touchingGround
                ? groundedMaterial
                : unGroundedMaterial;
        }

        if (rewardMovingTowardsTarget)
        {//是否開啓速度方向與目標方向獎勵懲罰機制
            RewardFunctionMovingTowards();
        }

        if (rewardFacingTarget)
        {//是否開啓小藍前方與目標方向獎勵懲罰機制
            RewardFunctionFacingTarget();
        }

        if (rewardUseTimePenalty)
        {//是否開啓隨時間流失懲罰機制
            RewardFunctionTimePenalty();
        }
    }

	/// <summary>
    /// 計算小藍速度方向與目標方向的點積,以此來獎勵或懲罰
    /// </summary>
    void RewardFunctionMovingTowards()
    {
        m_MovingTowardsDot = Vector3.Dot(m_JdController.bodyPartsDict[body].rb.velocity, m_DirToTarget.normalized);
        AddReward(0.03f * m_MovingTowardsDot);
    }

    /// <summary>
    /// 計算小藍正向與目標方向的點積,以此來懲罰獲獎勵
    /// </summary>
    void RewardFunctionFacingTarget()
    {
        m_FacingDot = Vector3.Dot(m_DirToTarget.normalized, body.forward);
        AddReward(0.01f * m_FacingDot);
    }

    /// <summary>
    /// 隨時間流失,懲罰小藍,促使其快速完成任務
    /// </summary>
    void RewardFunctionTimePenalty()
    {
        AddReward(-0.001f);
    }

此段代碼主要是對各類自選項進行設置判斷,主要用途就是設置在何時給予小藍懲罰或獎勵。

image-20200531215440750

至此,咱們將Crawler的主要代碼都解析了一遍,可能有一些地方沒有解析的很清楚,也算是拋磚引玉,主要借鑑一下里面的用法便可,實現仍是要根據具體需求具體分析,除此以外引入了一些四元數的內容,此次也算是對四元數知識的空缺進行了必定程度的彌補。

5、訓練

在命令行中輸入如下命令:

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

進行訓練,以下圖:

crawler9

有點像魔鬼的步伐。。。

還記得AdjustTrainingTimescale腳本麼,是設置Time.Scale的腳本,此時咱們若是按數字鍵1~9,會發現Crawler的動做確實能夠減慢或加速,就不作動圖了,動圖幀數必定的,也看不出來加速或減速,因此本身能夠去試一下。這個值應該是確實能影響訓練速度的,我打印了一下,普通訓練的話這個值是20,Time.Scale最大爲100,可是也應該不能設置的太大,太大的話會形成Update卡頓,反而影響訓練速度,不過這個是我猜的。。。

訓練一段時間就發現小藍以飛快速度奔向目標:

crawler11

得飄得飄得意的飄~

順帶附上訓練後的TesorBoard:

image-20200601185720849

能夠看到,最終訓練的Cumulative Reward大概在650左右,比官方的數據400還要好不少。

放到Unity中來看一下訓練效果:

crawler12

效果和官方訓練的模型同樣,沒什麼問題。

6、總結

本次的案例主要是展現對於複雜的多關節對象如何訓練,而且說起到了一些四元數的知識,歡迎你們點贊留言共同探討。

寫文不易~所以作如下申明:

1.博客中標註原創的文章,版權歸原做者 煦陽(本博博主) 全部;

2.未經原做者容許不得轉載本文內容,不然將視爲侵權;

3.轉載或者引用本文內容請註明來源及原做者;

4.對於不遵照此聲明或者其餘違法使用本文內容者,本人依法保留追究權等。

相關文章
相關標籤/搜索