Unityのチュ－トリアル ML-Agents：ペンギンを動かしてみた

# はじめに
Unityのチュ－トリアルML-Agents：ペンギンを動かしてみました。
https://learn.unity.com/project/ml-agents-penguins
最初、アセットのバージョンをチュ－トリアルに合わせて以下を使用したところ、特に引っかかるところもなく動かすことができました。
ML-Agents 0.13.1
Barracuda 0.4.0
これを、2020/3/21時点で最新版の以下に更新したところ、いろいろ修正が必要で引っかかるところもあったので書いておきます。
ML-Agents 0.15.0
Barracuda 0.6.1
修正は、[変更ガイド](https://github.com/Unity-Technologies/ml-agents/blob/master/docs/Migrating.md)に書いてある内容に沿って実施しています。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Unityのチュ－トリアル ML-Agents：ペンギンをやってみた<a href="https://t.co/cp7g9ninZ8">https://t.co/cp7g9ninZ8</a><br>アセットのバージョンはチュ－トリアルに合わせて以下を使用<br>ML-Agents Beta 0.13.1(最新版は0.15.0)<br>Barracuda 0.4.0(最新版は0.6.1) <a href="https://t.co/mnEqkA4JRA">pic.twitter.com/mnEqkA4JRA</a></p>&mdash; 高浜 (@SatoshiTakahama) <a href="https://twitter.com/SatoshiTakahama/status/1241173484895522816?ref_src=twsrc%5Etfw">March 21, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

#開発環境
Unity 2019.3.5f1

#修正したところ
・UnityのPackage ManagerからBarracuda 0.6.1に更新します。
・ML-Agentsの0.15.0をダウンロードし、チュ－トリアルのAssets/ML-Agentsフォルダを差し替えます。
・[インストールガイド](https://github.com/Unity-Technologies/ml-agents/blob/master/docs/Installation.md)に従い、com.unity.ml-agentsUnityパッケージをインストールします。
・namespaceを以下のように書き換えます。

```
using Unity.MLAgents;
using Unity.MLAgents.Sensors;
```

・DogAgent.csを以下のように書き換えます。

```C#:DogAgent.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.MLAgents;
using Unity.MLAgents.Sensors;
using Unity.MLAgentsExamples;

public class DogAgent : Agent {

    [HideInInspector]
    // The target the dog will run towards.
    public Transform target; 

    // These items should be set in the inspector
    [Header("Body Parts")] 
    public Transform mouthPosition;
    public Transform body;
    public Transform leg0_upper;
    public Transform leg1_upper;
    public Transform leg2_upper;
    public Transform leg3_upper;
    public Transform leg0_lower;
    public Transform leg1_lower;
    public Transform leg2_lower;
    public Transform leg3_lower;

    // These determine how the dog should be able to rotate around the y axis
    [Header("Body Rotation")] 
    public float maxTurnSpeed;
    public ForceMode turningForceMode;

    [Header("Sounds")]
    // If true, the dog will bark. 
    // Note : This should be turned off during training...unless you want to hear a dozen dogs barking for hours
    public bool canBark;
    // The clips to use for the barks of the dog
    public List<AudioClip> barkSounds = new List <AudioClip>();
	AudioSource audioSourceSFX;

    JointDriveController jdController;
    
    [HideInInspector]
    // This vector gives the position of the target relative to the position of the dog
	public Vector3 dirToTarget;
    // This float determines how much the dog will be rotating around the y axis
    float rotateBodyActionValue; 
    // Counts the number of steps until the next agent's decision will be made
    int decisionCounter;

    // [HideInInspector]
    public bool runningToItem;
    // [HideInInspector]
    public bool returningItem;

    void Awake()
    {
        // Audio Setup
        audioSourceSFX = body.gameObject.AddComponent<AudioSource>();
        audioSourceSFX.spatialBlend = .75f;
        audioSourceSFX.minDistance = .7f;
        audioSourceSFX.maxDistance = 5;
        if(canBark)
        {
            StartCoroutine(BarkBarkGame());
        }

        //Joint Drive Setup
        jdController = GetComponent<JointDriveController>();
        jdController.SetupBodyPart(body);
        jdController.SetupBodyPart(leg0_upper);
        jdController.SetupBodyPart(leg0_lower);
        jdController.SetupBodyPart(leg1_upper);
        jdController.SetupBodyPart(leg1_lower);
        jdController.SetupBodyPart(leg2_upper);
        jdController.SetupBodyPart(leg2_lower);
        jdController.SetupBodyPart(leg3_upper);
        jdController.SetupBodyPart(leg3_lower);

    }

    /// <summary>
    /// Add relevant information on each body part to observations.
    /// </summary>
    public void CollectObservationBodyPart(BodyPart bp, VectorSensor sensor)
    {
        var rb = bp.rb;
        sensor.AddObservation(bp.groundContact.touchingGround ? 1 : 0); // Is this bp touching the ground
        if(bp.rb.transform != body)
        {
            sensor.AddObservation(bp.currentXNormalizedRot);
            sensor.AddObservation(bp.currentYNormalizedRot);
            sensor.AddObservation(bp.currentZNormalizedRot);
            sensor.AddObservation(bp.currentStrength/jdController.maxJointForceLimit);
        }
    }

    /// <summary>
    /// The method the agent uses to collect information about the environment
    /// </summary>
    public override void CollectObservations(VectorSensor sensor)
    {
        sensor.AddObservation(dirToTarget.normalized);
        sensor.AddObservation(body.localPosition);
        sensor.AddObservation(jdController.bodyPartsDict[body].rb.velocity);
        sensor.AddObservation(jdController.bodyPartsDict[body].rb.angularVelocity);
        sensor.AddObservation(body.forward); //the capsule is rotated so this is local forward
        sensor.AddObservation(body.up); //the capsule is rotated so this is local forward
        foreach (var bodyPart in jdController.bodyPartsDict.Values)
        {
            CollectObservationBodyPart(bodyPart, sensor);
        }
    }


    /// <summary>
    /// Rotates the body of the agent around the y axis
    /// </summary>
    /// <param name="act"> The amount by which the agent must rotate</param>
    void RotateBody(float act)
    {
        float speed = Mathf.Lerp(0, maxTurnSpeed, Mathf.Clamp(act, 0, 1));
        Vector3 rotDir = dirToTarget; 
        rotDir.y = 0;
        // Adds a force on the front of the body
        jdController.bodyPartsDict[body].rb.AddForceAtPosition(
            rotDir.normalized * speed * Time.deltaTime, body.forward, turningForceMode); 
        // Adds a force on the back od the body
        jdController.bodyPartsDict[body].rb.AddForceAtPosition(
            -rotDir.normalized * speed * Time.deltaTime, -body.forward, turningForceMode); 
    }

    /// <summary>
    /// Allows the dog to bark
    /// </summary>
    /// <returns></returns>
    public IEnumerator BarkBarkGame()
    {   

        while(true)
        {
            //When we're returning the stick we should not bark because we have
            //a stick in our mouth :|>
            if(!returningItem)
            {
                //Choose one of the barking clips at random and play it.
                audioSourceSFX.PlayOneShot(barkSounds[Random.Range( 0, barkSounds.Count)], 1);
            }
            //Wait for a random amount of time (between 1 & 10 sec) until we bark again.
            yield return new WaitForSeconds(Random.Range(1, 10)); 
        }
    }

    /// <summary>
    /// The agent's action method. Is called at each decision and allows the agent to move
    /// </summary>
    /// <param name="vectorAction"> The actions that were determined by the policy</param>
    /// <param name="textAction"> The text action given by the policy</param>
	public override void OnActionReceived(float[] vectorAction)
    {

        var bpDict = jdController.bodyPartsDict;
      
        // Update joint drive target rotation
        bpDict[leg0_upper].SetJointTargetRotation(vectorAction[0], vectorAction[1], 0);
        bpDict[leg1_upper].SetJointTargetRotation(vectorAction[2], vectorAction[3], 0);
        bpDict[leg2_upper].SetJointTargetRotation(vectorAction[4], vectorAction[5], 0);
        bpDict[leg3_upper].SetJointTargetRotation(vectorAction[6], vectorAction[7], 0);
        bpDict[leg0_lower].SetJointTargetRotation(vectorAction[8], 0, 0);
        bpDict[leg1_lower].SetJointTargetRotation(vectorAction[9], 0, 0);
        bpDict[leg2_lower].SetJointTargetRotation(vectorAction[10], 0, 0);
        bpDict[leg3_lower].SetJointTargetRotation(vectorAction[11], 0, 0);

        // Update joint drive strength
        bpDict[leg0_upper].SetJointStrength(vectorAction[12]);
        bpDict[leg1_upper].SetJointStrength(vectorAction[13]);
        bpDict[leg2_upper].SetJointStrength(vectorAction[14]);
        bpDict[leg3_upper].SetJointStrength(vectorAction[15]);
        bpDict[leg0_lower].SetJointStrength(vectorAction[16]);
        bpDict[leg1_lower].SetJointStrength(vectorAction[17]);
        bpDict[leg2_lower].SetJointStrength(vectorAction[18]);
        bpDict[leg3_lower].SetJointStrength(vectorAction[19]);

        rotateBodyActionValue = vectorAction[20];
  
    }

    /// <summary>
    /// Update the direction vector to the current target;
    /// </summary>
    public void UpdateDirToTarget()
    {
        dirToTarget = target.position - jdController.bodyPartsDict[body].rb.position;

    }

    void FixedUpdate()
    {
        UpdateDirToTarget();

        if (decisionCounter == 0)
        {
            decisionCounter = 3;
            RequestDecision();
        }
        else
        {
            decisionCounter--;
        }

        RotateBody(rotateBodyActionValue); 

        // Energy Conservation
        // The dog is penalized by how strongly it rotates towards the target.
        // Without this penalty the dog tries to rotate as fast as it can at all times.
        var bodyRotationPenalty = -0.001f * rotateBodyActionValue;
        AddReward(bodyRotationPenalty);

        // Reward for moving towards the target
        RewardFunctionMovingTowards();
        // Penalty for time
        RewardFunctionTimePenalty();
    }
	
    /// <summary>
    /// Reward moving towards target & Penalize moving away from target.
    /// This reward incentivizes the dog to run as fast as it can towards the target,
    /// and decentivizes running away from the target.
    /// </summary>
    void RewardFunctionMovingTowards()
    {

		float movingTowardsDot = Vector3.Dot(
		    jdController.bodyPartsDict[body].rb.velocity, dirToTarget.normalized); 
        AddReward(0.01f * movingTowardsDot);
    }


    /// <summary>
    /// Time penalty
    /// The dog gets a pentalty each step so that it tries to finish 
    /// as quickly as possible.
    /// </summary>
    void RewardFunctionTimePenalty()
    {
        AddReward(- 0.001f);  //-0.001f chosen by experimentation.
    }

}
```
・DogAcademy.csを以下のように書き換えます。

```C#:DogAcademy.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.MLAgents;
using Unity.MLAgentsExamples;

public class DogAcademy : MonoBehaviour
{

    public void AcademyReset()
    {
        Monitor.verticalOffset = 1f;
        Physics.defaultSolverIterations = 12;
        Physics.defaultSolverVelocityIterations = 12;
        Time.captureFramerate = 0;
    }

    public void AcademyStep()
    {
    }

}

```
・Agentクラスのメソッド変更等に伴いPenguinAgent.csを以下のように書き換えます。

・Penginプレファブを開いて、DecisionRequesteコンポーネントを付加します。付加しないとビルドは通るけれど初期配置から全く動かない状態になってしまいます。自分はこれになかなか気が付かなかったので結構はまりました。

・トレーニング用のパラメータは、マニュアルに記載されていた通りで動作しました。

```
CorgiLearning:
    normalize: true
    num_epoch: 3
    time_horizon: 1000
    batch_size: 2048
    buffer_size: 20480
    gamma: 0.995
    max_steps: 2e6
    summary_freq: 3000
    num_layers: 3
    hidden_units: 512
```
・トレーニング開始は以下コマンドです。

```

mlagents-learn config/trainer_config.yaml --curriculum config/curricula/penguin/PenguinLearning.yaml --run-id penguin_01 --train
```
・トレーニング開始した後、本来は"Start training by pressing the Play button in the Unity Editor."
が表示されたあと、Unity側でPlayを実行するのですが、なぜかログ出力されていなくなっているので、
"Instructions for updating:
non-resource variables are not supported in the long term"が表示されたあたりでPlayを実行します。
(2020/4/1追記)
~~0.15.1の[リリース通知](https://github.com/Unity-Technologies/ml-agents/releases)にログ出力が表示されない問題を修正したとの記載があったので直っているかもしれません(未確認です)。~~
ML-Agents 1.0.0に更新した後は直っていました(2020/5/7追記)

#トレーニングの進捗状況の確認について
トレーニング実行するとsummariesフォルダにログが出力されるので、pythonのコンソールから(MLAgentsをアクティベートした状態で)以下のコマンドを実行することで、進捗状況をグラフで確認することができます。

```
tensorboard --logdir=./summaries
```
上記コマンドの後、ブラウザからlocalhost:6006を開くと以下のように表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215278/f278e39f-38fe-8a3a-eb18-9569986ae1c8.png)

各値の見方は[ここ](https://github.com/Unity-Technologies/ml-agents/blob/master/docs/Getting-Started.md#observing-training-progress)に説明があります。

#追記1
ペンギンが水中を泳げるように改造してみました。ペンギンが水中に入ったらUseGravityをfalseに、水中から出たらtrueにして、立体的な動きができるようにしました。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ペンギン達はいつのまにかコツをつかんだようだ <a href="https://t.co/2fIngHGBQH">pic.twitter.com/2fIngHGBQH</a></p>&mdash; 高浜 (@SatoshiTakahama) <a href="https://twitter.com/SatoshiTakahama/status/1243724384004526080?ref_src=twsrc%5Etfw">March 28, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

#追記2(2020/05/04)
ML-Agentsを最新版に更新したところ、いくつか修正が必要でした。
ML-Agents 1.0.0
Barracuda 0.7.0
Unity 2019.3.12f1
・namespaceを以下のように書き換えます。

```
using Unity.MLAgents;
using Unity.MLAgents.Sensors;
```
・PenguinAgent.csのOnActionReceivedメソッドとHeuristicメソッドを以下のように書き換えます。

```C#:PenguinAgent.cs
    public override void OnActionReceived(float[] vectorAction)
    {
        // Convert the first action to forward movement
        float forwardAmount = vectorAction[0];

        // Convert the second action to turning left or right
        float turnAmount = 0f;
        if (vectorAction[1] == 1f)
        {
            turnAmount = -1f;
        }
        else if (vectorAction[1] == 2f)
        {
            turnAmount = 1f;
        }

        // Apply movement
        rigidbody.MovePosition(transform.position + transform.forward * forwardAmount * moveSpeed * Time.fixedDeltaTime);
        transform.Rotate(transform.up * turnAmount * turnSpeed * Time.fixedDeltaTime);

        // Apply a tiny negative reward every step to encourage action
        AddReward(-1f / MaxStep);
    }

    public override void Heuristic(float[] actionsOut)
    {
        float forwardAction = 0f;
        float turnAction = 0f;
        if (Input.GetKey(KeyCode.W))
        {
            // move forward
            forwardAction = 1f;
        }
        if (Input.GetKey(KeyCode.A))
        {
            // turn left
            turnAction = 1f;
        }
        else if (Input.GetKey(KeyCode.D))
        {
            // turn right
            turnAction = 2f;
        }

        // Put the actions into an array and return
        actionsOut[0] = forwardAction;
        actionsOut[1] = turnAction;
    }
```

・PenguinAcademy.csのOnAwakeメソッドを以下のように書き換えます。

```C#:PenguinAcademy.cs
    void OnAwake()
    {
        FishSpeed = 0f;
        FeedRadius = 0f;

        // Set up code to be called every time the fish_speed parameter changes 
        // during curriculum learning
        Academy.Instance.EnvironmentParameters.RegisterCallback("fish_speed", f =>
        {
            FishSpeed = f;
        });

        // Set up code to be called every time the feed_radius parameter changes 
        // during curriculum learning
        Academy.Instance.EnvironmentParameters.RegisterCallback("feed_radius", f =>
        {
            FeedRadius = f;
        });
    }
```

・トレーニング開始のコマンドは--trainの指定が不要になりました。

```
mlagents-learn config/trainer_config.yaml --curriculum config/curricula/penguin/PenguinLearning.yaml --run-id penguin_01
```