はじめに
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
修正は、変更ガイドに書いてある内容に沿って実施しています。
Unityのチュ-トリアル ML-Agents:ペンギンをやってみたhttps://t.co/cp7g9ninZ8
— 高浜 (@SatoshiTakahama) March 21, 2020
アセットのバージョンはチュ-トリアルに合わせて以下を使用
ML-Agents Beta 0.13.1(最新版は0.15.0)
Barracuda 0.4.0(最新版は0.6.1) pic.twitter.com/mnEqkA4JRA
#開発環境
Unity 2019.3.5f1
#修正したところ
・UnityのPackage ManagerからBarracuda 0.6.1に更新します。
・ML-Agentsの0.15.0をダウンロードし、チュ-トリアルのAssets/ML-Agentsフォルダを差し替えます。
・インストールガイドに従い、com.unity.ml-agentsUnityパッケージをインストールします。
・namespaceを以下のように書き換えます。
using Unity.MLAgents;
using Unity.MLAgents.Sensors;
・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を以下のように書き換えます。
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のリリース通知にログ出力が表示されない問題を修正したとの記載があったので直っているかもしれません(未確認です)。
ML-Agents 1.0.0に更新した後は直っていました(2020/5/7追記)
#トレーニングの進捗状況の確認について
トレーニング実行するとsummariesフォルダにログが出力されるので、pythonのコンソールから(MLAgentsをアクティベートした状態で)以下のコマンドを実行することで、進捗状況をグラフで確認することができます。
tensorboard --logdir=./summaries
上記コマンドの後、ブラウザからlocalhost:6006を開くと以下のように表示されます。
各値の見方はここに説明があります。
#追記1
ペンギンが水中を泳げるように改造してみました。ペンギンが水中に入ったらUseGravityをfalseに、水中から出たらtrueにして、立体的な動きができるようにしました。
ペンギン達はいつのまにかコツをつかんだようだ pic.twitter.com/2fIngHGBQH
— 高浜 (@SatoshiTakahama) March 28, 2020
#追記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メソッドを以下のように書き換えます。
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メソッドを以下のように書き換えます。
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