ML-Agents(六)Tennis

ML-Agents(六)Tennis

喜歡的童靴但願你們多多點贊收藏哦~編程

此次Tennis示例研究費了我很多勁,倒不是由於示例的難度有多大,而重點是這個示例的訓練過程當中遇到了許多問題值得記錄下來,其次這個訓練是一個對抗訓練,也是比較有意思的示例。canvas

1、Tennis介紹

首先來看看效果~c#

teenis

OK,能夠看到畫面中有18個網球場,而後藍色的球拍和紫色的球拍互相對打。這裏注意一下,場景雖然都是3D的,但實際上球拍和球只在球場的中軸線上上下左右移動,也就是說其實換個相機位置的話,這裏實際上是個二維打球模擬。數組

teenis2

固然了,這樣算是簡化了訓練的過程,這個示例大部分所用到的內容和3D Ball差很少,主要有一個能夠深化學習的就是對抗訓練。下面咱們來先看一下官方對該示例的參數。dom

2、環境與訓練參數

老規矩,先來看一下官方文檔參數:ide

  • 設定:兩個agents控制球拍進行雙人遊戲,來回擊打球過球網函數

  • 目標:一方agent必須打擊球,以使對手沒法擊回球學習

  • Agent:在這個環境中,包含兩個擁有相同行爲參數(Behavior Parameters)的Agent。官方還建議,當你訓練好你的球拍Agent後,能夠把其中一個球拍調整爲Heuristic Only手動操做,嘗試一下被電腦虐的快感(固然官方不是這麼說的,可是確實有點困難= =)。這裏要設置成手動模式,還須要修改一下代碼,後面會提到命令行

  • Agent獎勵設定:設計

    • 若是agent贏下一球,則+1。固然因爲是對抗訓練,因此一方agent要經過防止另外一方agent贏球來贏取獎勵
    • 若是agent輸掉一球,則-1
  • 行爲參數:

    • 矢量觀測空間:一共9個變量,分別對應球和球拍的位置、速度以及方向
    • 矢量動做空間:Continuous類型,一共3個變量,對應球拍向網、遠離網的運動,跳躍和旋轉。這裏通俗點講就是球拍的x、y方向運動,以及球拍繞自身z軸的旋轉
    • 視覺觀測值:None
  • 可變參數:3個

    • 重力加速度:
      • Default:9.81
      • 推薦最小值:6
      • 推薦最大值:20
    • 網球比例:球三個維度上的比例(即x、y、z比例相同)
      • Default:5
      • 推薦最小值:0.2
      • 推薦最大值:5
    • 球拍初始角度:
      • Default:55(源碼裏這麼寫了)

    最後一個「球拍初始角度」官方漏寫了,我說爲啥明明寫了3個參數,卻只有兩個參數寫出來。可是!!!凡事有個可是,實際上經過源碼會發現,最後這個參數不管怎麼改,並不會改變訓練時的效果,由於訓練時會讓Agent自動調整球拍的旋轉角度。其實這個參數的惟一做用就是你在手動操做和你訓練出來的agent找虐時,會使球拍固定在一個角度來打球,固然,若是你還有餘力能夠調整球拍角度,那你很sei li~對於agent沒啥大影響,對於手動來講,那就是天差地別了。

3、場景基本結構

場景中包含18個球場,以下圖:

image-20200407230705102

相應的Hierarchy層級:

image-20200407230725819

場景中Camera、Canvas啥的都不用太多去介紹,TennisSettings能夠設置環境一些高級物理設置,例如Unity中的Physics.gravityTime.fixedDeltaTimePhysics.defaultSolverIterations等。

TennisArea是做爲一個訓練的基本單位。其中:

  • TennisArea

    父物體上原本就帶有一個TennisArea.cs腳本,這個腳本主要是重置比賽環境,例如每次開始球的位置、比例等,可是我認爲在這裏初始化球的比例有問題,這個咱們後面說。

  • Ball

    球,帶有剛體,存在HitWall.cs腳本,你會發現有意思的是,原來咱們看的示例agent的獎懲基本都在AgentAction()函數中,即隨時判斷agent是否獎懲,可是這裏將agent獎勵或失敗設置放到了球上,由球來決定究竟是agentA贏仍是agentB贏,而後重置agent。固然這也比較好理解,由於規則都在球上,球固然知道到底誰贏了(感受有點邪惡。。。。2333)。

image-20200407231415130

  • Invisible Walls

    如英文註釋,就是透明碰撞體,場地兩邊地面各一個,場後各一個,就是爲了防止球掉落。固然我想吐槽一下這個碰撞體,設置的太粗曠了= =,兩邊的牆巨長無比。

image-20200407231943303

  • Scenery

    球場四周的碰撞體,網的碰撞體以及網上的碰撞體。

image-20200407233616918

  • AgentAAgentB

    兩個agent,分別表明兩對抗方,公用一套訓練參數進行訓練。

image-20200407234006882

這裏兩個agent你們注意,看一下有啥不同,首先看Behavior Parameters組件:

image-20200408205445361

image-20200408205513237

能夠看到兩個球拍的行爲參數,Team Id是不一樣的,表示了兩個不一樣的陣營,猜想若是有多方陣營也能夠訓練,還有這裏應該是使用Behavior Name來保證訓練的Brain一致,在前幾面的文章中也有說起。

再來看一下兩個球拍的Tennis Agent

image-20200408205707097

image-20200408205717457

會發現一個Invert X勾選,另外一個未勾選,這裏先賣個關子,一下子講爲何這麼設置。

下面直接開始分析代碼,走起。

4、代碼分析

環境初始化腳本

這個示例中環境初始化分爲兩個部分,一個部分是處於父節點的TennisArea腳本,另外一個是球上的HitWall腳本。前者主要在環境重置時初始化球的位置,以及限制球的速度;後者則是包含了打球規則以及初始化球拍、調用TennisArea來初始化比賽。

先來看一下TennisArea.cs腳本。

using UnityEngine;

public class TennisArea : MonoBehaviour
{
    public GameObject ball;//球
    public GameObject agentA;//球拍agentA
    public GameObject agentB;//球拍agentB
    Rigidbody m_BallRb;//球的剛體

    void Start()
    {
        m_BallRb = ball.GetComponent<Rigidbody>();
        MatchReset();//一開始重置比賽,注意這裏運行的順序後於Agent的InitializeAgent()
    }
    /// <summary>
    /// 重置比賽
    /// </summary>
    public void MatchReset()
    {
        var ballOut = Random.Range(6f, 8f);//球隨機x值
        var flip = Random.Range(0, 2);//隨機球出如今左邊仍是右邊
        if (flip == 0)
        {//令球在場地左邊隨機出現
            ball.transform.position = new Vector3(-ballOut, 6f, 0f) + transform.position;
        }
        else
        {//令球在場地右邊隨機出現
            ball.transform.position = new Vector3(ballOut, 6f, 0f) + transform.position;
        }
        m_BallRb.velocity = new Vector3(0f, 0f, 0f);//使球的速度變爲0,而後令其自由落體
        //注意:這裏修改小球的比例,我認爲在這裏修改球的比例是不合適的
        //ball.transform.localScale = new Vector3(.5f, .5f, .5f);
        ball.GetComponent<HitWall>().lastAgentHit = -1;//重置HitWall中判斷勝利參數
    }

    void FixedUpdate()
    {
        //這裏主要是控制球的速度不要太快
        var rgV = m_BallRb.velocity;
        m_BallRb.velocity = new Vector3(Mathf.Clamp(rgV.x, -9f, 9f), Mathf.Clamp(rgV.y, -9f, 9f), rgV.z);
        //Debug.Log(m_BallRb.velocity);
    }
}

這個腳本有幾個點來講一下:

  • 首先,一開始運行時,以上的Start()函數是晚於球拍Agent的初始化方法InitializeAgent()的,即MatchReset()方法是在InitializeAgent()以後的。在源碼裏,TennisArea腳本第32行設置了球的比例,而注意球的比例是可變參數(如今官方改爲Parameter Randomization了,即隨機化參數,原來是Generalized Reinforcement,便可變強化訓練,叫法改了用法同樣,就是可變參數)。所以,無論在Agent的InitializeAgent()裏如何設置球的比例,在後執行的MatchReset()方法裏都會將球的比例從新設置回(0.5f,0.5f,0.5f)。

    以上還只是列舉了訓練一開始狀況,蛋疼的是在訓練過程當中,也會出現MatchReset()在Agent的SetBall()以後的狀況,也就是若是你在訓練引入了可變參數球的size,在訓練過程當中應該是要必定步驟後改變球的比例來訓練,可是若是在MatchReset()中設置球的比例,會使得球的比例一直被固定爲0.5,所以應該是要去掉這一行的。

    固然,以上推測只是我初看代碼理解的,後面咱們能夠分別註釋這一句和加上這一句來進行可變size參數訓練,觀察tensorboard的曲線就立馬能看出來了。

  • 第二點是FixedUpdate()函數,這裏將網球的x方向速度和y方向速度限制到了-9到9,固然咱們能夠把這裏註釋掉,看看是什麼效果。

    tennis3

    這裏會發現球的速度過快,下面的Debug也會發現速度很容易就在十幾、二十幾,球太容易飛出場外。

OK,TennisArea腳本應該沒什麼問題了,咱們來分析一下網球上的HitWall腳本。

HitWall.cs

using UnityEngine;

public class HitWall : MonoBehaviour
{
    public GameObject areaObject;//父節點
    public int lastAgentHit;//最後一次哪一個agent擊球,0表明agentA擊球,1表明agentB擊球
    public bool net;//判斷是否過網

    public enum FloorHit
    {
        Service,//從空中發球
        FloorHitUnset,//成功回擊球以後,使用該標誌位
        FloorAHit,//在A地面彈起
        FloorBHit//在B地面彈起
    }

    public FloorHit lastFloorHit;//最後一次地板擊中狀態

    TennisArea m_Area;
    TennisAgent m_AgentA;//代理A
    TennisAgent m_AgentB;//代理B

    void Start()
    {
        m_Area = areaObject.GetComponent<TennisArea>();
        m_AgentA = m_Area.agentA.GetComponent<TennisAgent>();
        m_AgentB = m_Area.agentB.GetComponent<TennisAgent>();
    }

    /// <summary>
    /// 比賽重置,包括agentA、agentB、網球置位等
    /// </summary>
    void Reset()
    {
        m_AgentA.Done();
        m_AgentB.Done();
        m_Area.MatchReset();
        lastFloorHit = FloorHit.Service;
        net = false;
    }
    /// <summary>
    /// agentA贏
    /// </summary>
    void AgentAWins()
    {
        m_AgentA.SetReward(1);
        m_AgentB.SetReward(-1);
        m_AgentA.score += 1;
        Reset();

    }
    /// <summary>
    /// agentB贏
    /// </summary>
    void AgentBWins()
    {
        m_AgentA.SetReward(-1);
        m_AgentB.SetReward(1);
        m_AgentB.score += 1;
        Reset();

    }
    void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.CompareTag("iWall"))
        {//若是球碰到牆(Tag=="iWall"),主要是"InvisibleWalls"和"Scenery"物體下的透明碰撞體
            if (collision.gameObject.name == "wallA")
            {//若是球碰到A這邊的牆
                if (lastAgentHit == 0 || lastFloorHit == FloorHit.FloorAHit)
                {//A本身擊球碰到A牆出界||球通過A這邊的地面彈起碰到A牆出界(A沒有接到球),則B贏
                    AgentBWins();
                }
                else
                {//B擊球的狀況下:若在發球時,B第一球未碰到A地面直接出界||回球時球碰到了B本身邊的地面||回球時,球出A邊界,則A贏
                    AgentAWins();
                }
            }
            else if (collision.gameObject.name == "wallB")
            {//同上
                if (lastAgentHit == 1 || lastFloorHit == FloorHit.FloorBHit)
                {
                    AgentAWins();
                }
                else
                {
                    AgentBWins();
                }
            }
            else if (collision.gameObject.name == "floorA")
            {//若是球碰到A這邊的地面
                if (lastAgentHit == 0 || lastFloorHit == FloorHit.FloorAHit || lastFloorHit == FloorHit.Service)
                {//A擊球碰到本身的地面||最後一次也是在A地面彈起,即球在A地面彈了兩次||最後一次是B從空中發的球,A未接到球,則B贏
                    AgentBWins();
                }
                else
                {//以上狀況都不是,則A接到球併成功回擊
                    lastFloorHit = FloorHit.FloorAHit;
                    if (!net)
                    {//A成功接球並回擊
                        net = true;
                    }
                }
            }
            else if (collision.gameObject.name == "floorB")
            {//同上
                if (lastAgentHit == 1 || lastFloorHit == FloorHit.FloorBHit || lastFloorHit == FloorHit.Service)
                {
                    AgentAWins();
                }
                else
                {
                    lastFloorHit = FloorHit.FloorBHit;
                    if (!net)
                    {
                        net = true;
                    }
                }
            }
            else if (collision.gameObject.name == "net" && !net)
            {//若是球碰到網,且未斷定過網(即net=false)
                if (lastAgentHit == 0)
                {//若是上次擊球爲A,則B贏
                    AgentBWins();
                }
                else if (lastAgentHit == 1)
                {//若是上次擊球爲B,則A贏
                    AgentAWins();
                }
            }
        }
        else if (collision.gameObject.name == "AgentA")
        {//球碰到球拍A
            if (lastAgentHit == 0)
            {//若是上一次A已經擊打過,即A擊球兩次,則B贏
                AgentBWins();
            }
            else
            {//A成功回球
                if (lastFloorHit != FloorHit.Service && !net)
                {//A在空中直接回球||球在A地面彈起後A回球(即lastFloorHit=FloorAHit||FloorHitUnset)
                    //則斷定過網
                    net = true;
                }

                lastAgentHit = 0;//使最後一次擊球爲A
                lastFloorHit = FloorHit.FloorHitUnset;//重置擊球過程
            }
        }
        else if (collision.gameObject.name == "AgentB")
        {//同上
            if (lastAgentHit == 1)
            {
                AgentAWins();
            }
            else
            {
                if (lastFloorHit != FloorHit.Service && !net)
                {
                    net = true;
                }

                lastAgentHit = 1;
                lastFloorHit = FloorHit.FloorHitUnset;
            }
        }
    }
}

這個腳本,比較複雜的部分就是判斷誰贏的邏輯,其實就是將網球的規則編程代碼了,熟悉網球規則的童靴應該看一下再想一想邏輯就明白了。固然這裏也能夠作了解,畢竟屬於業務部分的東西。

Agent腳本

Agent初始化與重置

首先來看一下Agent腳本中對於agent的初始化、重置部分以及變量參數。

public class TennisAgent : Agent
{
    [Header("Specific to Tennis")]
    public GameObject ball;//網球對象
    public bool invertX;//鏡像標誌位
    public int score;//得分
    public GameObject myArea;//平臺
    public float angle;//球拍角度
    public float scale;//網球比例

    Text m_TextComponent;//得分板Text
    Rigidbody m_AgentRb;//球拍剛體
    Rigidbody m_BallRb;//網球剛體
    float m_InvertMult;//鏡像翻轉乘數
    IFloatProperties m_ResetParams;//可變參數(可變參數)

    const string k_CanvasName = "Canvas";
    const string k_ScoreBoardAName = "ScoreA";
    const string k_ScoreBoardBName = "ScoreB";

    public override void InitializeAgent()
    {
        m_AgentRb = GetComponent<Rigidbody>();
        m_BallRb = ball.GetComponent<Rigidbody>();
        //找球拍本身對應的得分板,不贅述
        var canvas = GameObject.Find(k_CanvasName);
        GameObject scoreBoard;
        m_ResetParams = Academy.Instance.FloatProperties;
        if (invertX)
        {
            scoreBoard = canvas.transform.Find(k_ScoreBoardBName).gameObject;
        }
        else
        {
            scoreBoard = canvas.transform.Find(k_ScoreBoardAName).gameObject;
        }
        m_TextComponent = scoreBoard.GetComponent<Text>();
        SetResetParameters();//設置可變參數
    }
    /// <summary>
    /// 設置球拍角度
    /// </summary>
    public void SetRacket()
    {
        angle = m_ResetParams.GetPropertyWithDefault("angle", 55f);
        gameObject.transform.eulerAngles = new Vector3(
            gameObject.transform.eulerAngles.x,
            gameObject.transform.eulerAngles.y,
            m_InvertMult * angle
        );
    }
    /// <summary>
    /// 設置網球比例
    /// </summary>
    public void SetBall()
    {
        scale = m_ResetParams.GetPropertyWithDefault("scale", .5f);
        ball.transform.localScale = new Vector3(scale, scale, scale);
    }
    /// <summary>
    /// 設置可變參數
    /// </summary>
    public void SetResetParameters()
    {
        SetRacket();
        SetBall();
    }
    /// <summary>
    /// Agent重置
    /// </summary>
    public override void AgentReset()
    {
        //根據inverX值來將m_InverMul置爲1或-1
        m_InvertMult = invertX ? -1f : 1f;
        //球拍位置重置,X隨機(先後),Y(高度)與Z(左右)位置固定
        transform.position = new Vector3(-m_InvertMult * Random.Range(6f, 8f), -1.5f, -1.8f) + transform.parent.transform.position;
        //球拍速度置0
        m_AgentRb.velocity = new Vector3(0f, 0f, 0f);
        //設置可變參數
        SetResetParameters();
    }
}

OK,以上代碼其實大部分都很簡單,可是關於兩個變量invertXm_InvertMult,我給它們分別取名爲「鏡像標誌位」和「鏡像翻轉乘數」。從名字上也能夠看出些許貓膩,簡單來說,invertX區分了兩個球拍,且決定了m_InvertMult的值是1仍是-1,而m_InvertMult則是一個使得兩個對立的球拍方向統一化的乘數,這樣就可使得兩個球拍雖然是對手,可是通過鏡像翻轉乘數,實際上轉換後,能夠當作兩個球拍都是向同一個方向訓練。

固然在球拍位置初始化、角度初始化時,m_InvertMult也有做用,以下圖:

image-20200409234454878

image-20200409234521430

能夠看到兩個球拍的TransformX與RotationZ爲正負相反,這也是爲何在上述代碼中,對於球拍初始化位置和角度要乘以m_InvertMult

矢量觀測空間

咱們在以前知道了該示例的觀測空間是Continous類型的,且變量有9個,下面來看一下。

/// <summary>
    /// 矢量觀測空間
    /// </summary>
    /// <param name="sensor"></param>
    public override void CollectObservations(VectorSensor sensor)
    {
        //球拍與場地的x值相對位置(先後)
        sensor.AddObservation(m_InvertMult * (transform.position.x - myArea.transform.position.x));
        //球拍與場地的y值相對位置(高低)
        sensor.AddObservation(transform.position.y - myArea.transform.position.y);
        //球拍x方向的速度
        sensor.AddObservation(m_InvertMult * m_AgentRb.velocity.x);
        //球拍y方向的速度
        sensor.AddObservation(m_AgentRb.velocity.y);

        //球與場地的x值相對位置
        sensor.AddObservation(m_InvertMult * (ball.transform.position.x - myArea.transform.position.x));
        //球與場地的y值相對位置
        sensor.AddObservation(ball.transform.position.y - myArea.transform.position.y);
        //球x方向的速度
        sensor.AddObservation(m_InvertMult * m_BallRb.velocity.x);
        //球y方向的速度
        sensor.AddObservation(m_BallRb.velocity.y);

        //球拍的旋轉角度
        sensor.AddObservation(m_InvertMult * gameObject.transform.rotation.z);
    }

整體來講,這裏收集了球拍和場地、球和場地的相對位置以及它們各自的速度信息,這裏再利用圖來說一下鏡像翻轉乘數。

image-20200410000108289

這樣經過m_InverMult,則可使得兩個球拍輸出的觀察參數變爲同向,使得對抗訓練對於一個訓練單元的訓練效果變爲double。對於球拍的旋轉角度也是一樣的道理。

Agent動做反饋

下面咱們來看代理的AgentAction()方法。

/// <summary>
    /// agent動做反饋
    /// </summary>
    /// <param name="vectorAction"></param>
    public override void AgentAction(float[] vectorAction)
    {
        //限制球拍x、y方向(左右、上下)乘積係數爲-1到1
        var moveX = Mathf.Clamp(vectorAction[0], -1f, 1f) * m_InvertMult;
        var moveY = Mathf.Clamp(vectorAction[1], -1f, 1f);
        //限制球拍每次旋轉角度乘積係數爲-1到1
        var rotate = Mathf.Clamp(vectorAction[2], -1f, 1f) * m_InvertMult;

        //當球拍在較低位置時,且moveY>0.5,則改變球拍向上的速度
        if (moveY > 0.5 && transform.position.y - transform.parent.transform.position.y < -1.5f)
        {
            m_AgentRb.velocity = new Vector3(m_AgentRb.velocity.x, 7f, 0f);
        }
        //改變球拍x(左、右)方向上的速度
        m_AgentRb.velocity = new Vector3(moveX * 30f, m_AgentRb.velocity.y, 0f);
        //改變球拍角度
        m_AgentRb.transform.rotation = Quaternion.Euler(0f, -180f, 55f * rotate + m_InvertMult * 90f);

        //限制球拍向前移動時不要越過網
        if (invertX && transform.position.x - transform.parent.transform.position.x < -m_InvertMult ||
            !invertX && transform.position.x - transform.parent.transform.position.x > -m_InvertMult)
        {
            transform.position = new Vector3(-m_InvertMult + transform.parent.transform.position.x,
                transform.position.y,
                transform.position.z);
        }

        m_TextComponent.text = score.ToString();//計分牌刷新
    }

AgentAction()代碼內容也不算太難,主要仍是注意對於對抗的兩方,經過invertX和m_InvertMult來使得對抗兩方同向化。

小小提一下,在最後「限制球拍向前移動時不要越過網」部分,若是以agentA爲例,此時invertX=false,m_InvertMult=1,則當知足

(!invertX && transform.position.x - transform.parent.transform.position.x > -m_InvertMult)時,有以下狀況:

image-20200410195902358

能夠看到,當球拍再向前的話,就會碰到網上,所以須要限制球拍不能越過網。agentB紫色球拍也是同樣的狀況,只是數值相反。

在這裏深刻思考一下,爲何只有限制球拍向前移動,而不用去限制球拍向後移動?可能你們也有相應的疑問,其實這裏利用兩個碰撞體就能夠限制球拍的移動了,即網的碰撞體以及場地後方碰撞體:

image-20200410200640970

可是你會發現網的碰撞體只有在球拍在低處時才能限制球拍向前移動,而將代碼註釋以後,球拍在空中時就會發生:

tennis4

能夠看到,你能夠控制球拍去對面胖揍對手= =||||。因此這裏只用對球拍在自身向前的方向進行限制便可。

Agent手動操控

下面來看一下Heristic()函數:

/// <summary>
    /// 手動操控
    /// </summary>
    /// <returns></returns>
    public override float[] Heuristic()
    {
        var action = new float[2];

        action[0] = Input.GetAxis("Horizontal");//左右移動控制
        action[1] = Input.GetKey(KeyCode.Space) ? 1f : 0f;//空格使球拍飛起
        return action;
    }

OK,以上代碼很簡單,可是有一個問題,當你想操做球拍和電腦對打時,將一個球拍的Behavior Type置爲Heuristic Only後,開始遊戲,你會發現如下錯誤:

image-20200410201722187

這裏是說agent的返回的矢量動做空間數量有問題,若是你比較熟悉ML-Agents以後,你會發如今AgentAction(float[] vectorAction)中,vectorAction[]數組有三個元素,分別控制了球拍的x、y方向移動以及球拍的繞z軸的旋轉。而在以上Huristic()代碼中,action[]數組只有兩個元素,只控制了球拍的x、y方向移動,缺乏繞z軸的旋轉的行爲參數。

所以,這裏只須要將AgentAction()方法中的兩句代碼註釋:

var rotate = Mathf.Clamp(vectorAction[2], -1f, 1f) * m_InvertMult;

以及

m_AgentRb.transform.rotation = Quaternion.Euler(0f, -180f, 55f * rotate + m_InvertMult * 90f);

註釋後,就能夠進行手動操做了,固然若是你也相同時操做球拍旋轉,也不是不能夠,你可修改手動操控代碼以下:

/// <summary>
    /// 手動操控
    /// </summary>
    /// <returns></returns>
    public override float[] Heuristic()
    {
        var action = new float[3];

        action[0] = Input.GetAxis("Horizontal");//左右移動控制
        action[1] = Input.GetKey(KeyCode.Space) ? 1f : 0f;//空格使球拍飛起
        action[2] = Input.GetAxis("Vertical");//控制球拍旋轉
        return action;
    }

5、訓練

咱們此次訓練Tennis的模型,主要包含如下幾種:正常訓練(不帶可變參數)、帶兩個可變參數(scale、gravity)訓練、只帶一個可變參數(scale)不註釋代碼、只帶一個可變參數(scale)註釋代碼。

你們應該還記得咱們在上述4、代碼分析中的環境初始化腳本一小節,提到在MatchReset()函數中重置小球比例是不合適的,所以咱們來驗證一下這個想法是否正確。

接下來,咱們先進行一組正常訓練;而後在利用兩個可變參數訓練以前,先驗證MatchReset()中設置小球比例會不會使得小球比例的可變參數設置失效;最後,咱們再進行帶兩個可變參數的訓練。

普通訓練(不帶可變參數)

咱們先來普通訓練一次,以前已經重複過不少次的操做~咱們cd到ml-agent的目錄,而後輸入如下命令(固然這裏面有一些配置文件、訓練結果的路徑,能夠自行修改):

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

由於在訓練配置文件trainer_config.yaml中能夠看到Tennis的max_steps爲5.0e7,即五千萬步,是全部例子中訓練最大步數最大的。而以前咱們的3D Ball才五十萬步,前者是後者的100倍,因此訓練時間至關長。

實際我訓練過程當中,大概到100萬步的時候的效果就很不錯了,所以我將Tennis的max_steps改爲了5.0e6。

這次訓練次數較多,訓練時間比較長,放一張訓練的過程截圖:

tennis7

能夠大概看到兩邊打的有來有回,同時能夠在屏幕下方看到雙方的得分。

同時觀察命令行輸出:

image-20200411000943322

在以前訓練中,Mean RewardStd of Reward是衡量訓練效果的很重要的兩個標準,通常來說是逐漸上升的,而這裏一直是0和1,相應的有兩個其餘的參數代替:Mean Opponent ELOStd Opponent ELO

通過查閱資料,首先了解一下ELO是什麼:ELO等級分制度是指由匈牙利裔美國物理學家Elo建立的一個衡量各種對弈活動水平的評價方法,是當今對弈水平評估的公認的權威方法。

其實咱們用簡單的話來說,例如早先英雄聯盟有排位分,你的排位分就是利用ELO計算出來的。若是你贏了比你分數更高的對手,你的排位分就會增長更多;若是輸給比你分數更少的對少,那你排位分就會減小的更多。更詳細的計算方法你們能夠去查資料,這個知識點還挺有意思的,能夠了解排位分大概是怎麼算出來的。

經過對ELO的瞭解,也能夠發現,這裏由於用到了對抗訓練,因此採用Mean Opponent ELOStd Opponent ELO來看訓練的效果,其中咱們會發現命令行中有一行是Tennis?team=1 ELO:1615.145,這裏其實就表明了team爲1的agent(即球拍B),如今它的ELO(能夠簡單理解爲排位分)是1615.145,你會發現它的ELO會隨着訓練的進行逐漸升高,至關於一直打排位,訓練本身上王者。

【Warning】等待了大約48分鐘,訓練到大概三百萬步時,忽然發現以下狀況:兩個球拍不對打了,罷工了!大概的情形就是兩個球拍一開始就一塊兒移動到網前,讓球直接落地,而後立馬開始新的一局,能夠以後的截圖。此時立馬Ctrl+C中止訓練。咱們能夠看一下tensorboard的狀況:

image-20200411003841174

首先是對抗訓練中會存在ELO圖表,其實你就能夠當作是排位分的變化趨勢,能夠明顯觀察ELO在大概在3百萬步時忽然下跌,包括Reawd也是,在3百萬步處有異常數據。將訓練到一半的Tennis_Normal.nn文件放到Unity中:

tennis8

發現兩個球拍在一開始就往網前跑,給對手送分。看來利用Ctrl+C仍是沒能將上一次的訓練模型拯救下來= =。

這裏我懷疑是這樣的,由於在代碼中,沒有設計在訓練過程當中,若是對抗雙方在一局裏長時間來回打球,而給雙方同時獎勵的機制。當接近3百萬時,雙方的球技都挺好了,能打的有來有回的時間愈來愈長,致使長時間Brain沒有收到獎勵,使得Brain發現若是一直打可能一直都不能得分,本身的排位也上不去,可是若是丟球,還有機會拿獎勵分,導致Brain產生消極比賽送分的決策= =||||,這裏也是很逗了。因此我將tennis的訓練最大步驟設置爲2.5e6差很少到達此時狀況下ELO的最大值,正所謂是實踐出真知啊。

從這也能夠看到,訓練的最大步數並非越大越好,還要基於你考慮的是否周全。並且在訓練前,應該竟可能設置好矢量觀測空間以及其餘條件,運行一段時間後及時查看訓練效果,不要訓練太長時間發現沒有效果還硬着頭皮訓練,也不要一開始訓練只訓練較少步數沒有效果就換參數。

再次訓練一次最大訓練步數爲2.5e6的:

tennis9

會發現訓練出來的模型仍是有問題,而後我繼續調整最大訓練步數爲2.0e6,對比一下tensorboard:

image-20200411081316991

咱們從圖表裏發現,2.5e6s的訓練模型在大概2.1M步驟的時候Brian又罷工了,訓練的隨機性仍是比較大,在最大訓練步驟5.0e7時明明在3M左右纔開始罷工。。。。

咱們把2.0e6的訓練模型放到Unity中去,訓練效果以下:

tennis10

其實還蠻奇怪的,從訓練效果來看,2.0e6是比5.0e7及2.5e6正常一些,可是若是訓練步驟太長又會出現Brain罷工的狀況,因此我也比較好奇官方Tennis給出的訓練模型是如何訓練出來的,是否是有一些什麼設置咱們漏了。這裏要是有人知道的話也能夠留言討論交流~下面咱們仍是繼續別的訓練。

可變參數設置

之前的文章裏咱們稱可變參數爲泛化參數,是由於ml-agents官方改了,所以咱們之後就把「泛化參數」統一稱做「可變參數」。可變參數咱們此次按官方推薦的來設置,先來看一下Tennis的可變參數配置:

tennis_generalize.yaml

resampling-interval: 20000

scale:
    sampler-type: "uniform"
    min_value: 0.2
    max_value: 5
    
gravity:
    sampler-type: "uniform"
    min_value: 6
    max_value: 20

注意gravity參數的設置是在ProjectSettingOverrides.cs腳本里的:Academy.Instance.FloatProperties.RegisterCallback("gravity", f => { Physics.gravity = new Vector3(0, -f, 0); });來設置的。

此外,咱們將resampling-interval設置爲20000,是因爲在trainer_config.yaml配置文件中,Tennis的訓練步驟咱們設置的是2.0e6步,參照3D Ball,5.0e5次,可變訓練間隔爲5000,依此參照,設置Tennis的這個參數爲20000。固然這樣參照設置不必定適合,在訓練過程當中若是想將某參數改變引入可變,那麼在改變前的參數對應的訓練效果應該是基本成型的,若是訓練還麼有成型就改變參數,有可能形成由於一直改參數使得訓練效果一直不佳。就和作軟件同樣,若是軟件需求一直更改,那麼等到軟件成型出產品基本就等到猴年馬月了。

一個可變參數訓練

爲了驗證MatchReset()中是否須要設置小球比例的問題,咱們新增可變參數配置文件:

tennis_generalize_1.yaml

resampling-interval: 20000

scale:
    sampler-type: "uniform"
    min_value: 0.2
    max_value: 5

將gravity參數去掉,用來消除gravity改變帶來的影響。咱們先試驗註釋代碼後的效果,在命令行中輸入:

mlagents-learn config/trainer_config.yaml --sampler=config/tennis_generalize_1.yaml --run-id=Tennis_Gen_1_DeleteScale --train

訓練截圖以下:

tennis11

能夠明顯看到一開始球的大小有改變。訓練結束後,再將代碼取消註釋,命令行中輸入以下命令進行訓練:

mlagents-learn config/trainer_config.yaml --sampler=config/tennis_generalize_1.yaml --run-id=Tennis_Gen_1_ModifyScale --train

會發現小球的大小在訓練過程當中,有時候一開始會變大或者變小,可是會立馬變爲正常大小進行訓練,這也符合個人想法,即MatchReset()會在SetResetParameters()以後將小球的比例復位成0.5。在相同訓練配置下,代碼註釋後的訓練相比代碼註釋前的訓練,會使得小球比例改變,從而也就證實了MatchReset()中設置小球比例在可變參數訓練中是不該該的的。

兩個可變參數訓練

咱們利用兩個可變參數的配置文件,輸入如下命令(固然這裏面有一些配置文件、訓練結果的路徑,能夠自行修改):

mlagents-learn config/trainer_config.yaml --sampler=config/tennis_generalize.yaml --run-id=Tennis_Gen --train

等待訓練完成後,查看tensorboard圖表:

image-20200411165831643

能夠看出,與不帶可變參數的訓練,帶可變參數的訓練其中仍是有一些波折的,波折基本就表明了參數改變使得agent的ELO降低,這裏的訓練數據僅僅做爲一個參考吧。把訓練模型放到Unity中效果依然不是很好,這裏就不展現了。

總結

此次的Tennis示例研究,主要時間都消耗到訓練上,最終也沒有訓練出和官方同樣的效果,這點仍是有點遺憾,我認爲應該是獎勵規則有漏洞的緣由,具體在第五節已經寫了,固然也有多是我哪裏設置錯了。不過在這個過程當中已經有挺多經驗值得學習和記錄了,過程仍是比較有意思,所以這個示例的研究先到這,說不定後面研究更多的示例就會豁然開朗了。

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

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

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

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

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

相關文章
相關標籤/搜索