見出し画像

Unityプロジェクト「ML-Agents:Penguin」part4

0.はじめに

本記事では、Unityが無料で提供しているプロジェクト「ML-Agents:Penguin」の制作方法を日本語版にして全6回に分けてやっていきます。
第4回は、コードの記述(後半)です。
※第3回の続きとして話を進めていきます。
※赤ちゃんペンギンに魚をあげる表現で「吐き出す」とあります。これは、Regurgitateの意味からきており、よく逆流と翻訳されますが、魚を逆流という表現もややおかしく感じるので、今回は「吐き出す」と表現しております。


1.PenguinAgents.cs

"PenguinAgents"は、Agentsクラスから継承されており、素晴らしいことが発生します。環境の監視やアクションの実行、対話およびプレーヤー入力の受け入れを処理します。

(0)用語の整理
・エージェント=Penguin
・プレイヤー=これを作っている自分自身

(1)『PenguinAgents.cs』を「Visual Studio 2017」で開く
(2)次のコードを入力します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using MLAgents;

public class PenguinAgent : Agent
{

}

プログラム解説:
・"UnityEngine" と "MLAgents"のusing宣言を追加
・"Monobehaviour"ではなく"Agent"から継承するようクラスを定義

(3)PenguinAcademy クラス内({}間)に次のプログラムを入力する。

[Tooltip("How fast the agent moves forward")]
public float moveSpeed = 5f;

[Tooltip("How fast the agent turns")]
public float turnSpeed = 180f;

[Tooltip("Prefab of the heart that appears when the baby is fed")]
public GameObject heartPrefab;

[Tooltip("Prefab of the regurgitated fish that appears when the baby is fed")]
public GameObject regurgitatedFishPrefab;

プログラム解説:
・public変数を追加して、PenguinAgentの動きと回転速度、Heartと吐き出した魚のPrefabを追跡する。

(4)先ほどのプログラムの続きに、次のプログラムを入力する

private PenguinArea penguinArea;
private PenguinAcademy penguinAcademy;
new private Rigidbody rigidbody;
private GameObject baby;

private bool isFull; // If true, penguin has a full stomach
private float feedRadius = 0f;

プログラム解説:
・物体を追跡するprivate変数を追加

(5)さらに続けて、次のプログラムを入力する

/// Initial setup, called when the agent is enabled
public override void InitializeAgent()
{
   base.InitializeAgent();
   penguinArea = GetComponentInParent<PenguinArea>();
   penguinAcademy = FindObjectOfType<PenguinAcademy>();
   baby = penguinArea.penguinBaby;
   rigidbody = GetComponent<Rigidbody>();
}

プログラム解説:
・InitializeAgents関数をオーバーライドする
・InitializeAgents関数は、エージェントが起動したときに、自動的に1回呼び出されます。エージェントがリセットされるたびに呼び出されるわけではないため、個別のResetAgent関数があります。これを使用して、Scene内のいくつかのオブジェクトを探索します。

(6)さらに続けて、次のプログラムを入力する

/// Perform actions based on a vector of numbers
/// <param name="vectorAction">The list of actions to take</param>
public override void AgentAction(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 / agentParameters.maxStep);
}

プログラム解説:
・AgentAction関数をオーバーライドする。
・AgentsAction関数は、エージェントがコマンドを受信して応答する場所です。これらのコマンドは、ニューラルネットワークまたは人間のプレイヤーから発信される場合がありますが、この機能はそれらと同じに扱います。
・"vectorAction"パラメータは、エージェントが実行する必要があるアクションに対応する数値の配列です。このプロフェクトでは、「離散的(discrete)」アクションを使用しており、各整数値は選択肢に対応しています。代わりの方法は「連続的(continuous)」アクションで、-1から+1までの任意の小数値を選択できます。離散アクションでは、一度に1つの選択肢しか選択できませんが、その間の選択肢はありません。
この場合:
 〇vectorAction[0]は、0or1のいずれかで、所定の位置に留まる(0)か、フルスピードで前進(1)かを示します。
 〇vectorAction[1]は、0、1、2のいずれかで、回転しない(0)、負の方向に回転する(1)、、正の方向に回転する(2)かを示します。
・ニューラルネットワークは、訓練された時には、実際にこれらの動作が何をするのかの概念を持っていません。環境を一定方法で見た時に、ある行動がより多くの報酬ポイントをもたらしてくれる傾向があることを知っているだけです。このため、このスクリプトの後半では、環境の効果的な観察を作成することが非常に重要です。
・ベクトルアクションを解釈した後、AgentActuion関数は移動と回転を適用し、小さな負の報酬を追加します。この小さな負の報酬により、エージェントはできるだけ早くタスクを完了することができます。
・この場合、5,000stepsごとに-1 / 5000の報酬が与えられます。例えば、ペンギンが3,000stepsで早く終了した場合、このコード行から追加されるマイナス報酬は-3000 / 5000 = -0.6になります。ペンギンが5,000stepsすべて実行する場合、負の報酬の合計は
-5000 / 5000 = -1になります。

(7)さらに続けて、次のプログラムを入力する

/// Read inputs from the keyboard and convert them to a list of actions.
/// This is called only when the player wants to control the agent and has set
/// Behavior Type to "Heuristic Only" in the Behavior Parameters inspector.
/// <returns>A vectorAction array of floats that will be passed into <see cref="AgentAction(float[])"/></returns>
public override float[] Heuristic()
{
   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
   return new float[] { forwardAction, turnAction };
}

プログラム解説:
・Heuristic関数をオーバーライドする
・Heuristic関数を使用することで、ニューラルネットワークなしでエージェントを制御することができます。
・forwardaActionのデフォルトは0ですが、プレイヤーがキーボードの「w」を押すことで値が1となり、エージェントが前進します。
・turnActionのデフォルトは0ですが、プレイヤーがキーボードの「A」or「D」を押すと、値がそれぞれ1 or 2 に設定され、左or右に回転します。

(8)さらに続けて、次のプログラムを入力する

/// Reset the agent and area
public override void AgentReset()
{
   isFull = false;
   penguinArea.ResetArea();
   feedRadius = penguinAcademy.FeedRadius;
}

プログラム解説:
・AgentReset関数をオーバーライドする
・ベースエージェントクラスは、エージェントが赤ちゃんにすべての魚に餌を与えると、自動的にAgentReset関数を呼び出し、ペンギンの腹を空にし、領域をリセットします。

(9)補足説明
・エージェント(ペンギン)は、2つの方法で環境を観察します。
・1つ目は、"raycasts(レイキャスト)"です。これは、ペンギンからレーザーポインターの束を照らして、何かにぶつかったかどうかを確認するようなものです。自動運転車やロボットで使用されるLIDAR(ライダー)に似ています。レイキャスト観測は、「ML-Agents-0.12」以降『RayPerceptionSensor Component』を介して追加さています。
※この記事の表紙の画像にある、赤や白の線です。
・2つ目は、数値を使用する方法です。true / false値、距離、空間内のXYZ位置、四元数回転のいずれであっても、観測を数値のリストに変換し、エージェントの観測として追加できます。
・観測する対象を選択するときは、配慮する必要があります。エージェントに環境に関する十分な情報がない場合、エージェントはタスク(仕事)を完了できません。
・ここまで実装されているエージェント(ペンギン)には、メモリがありません。更新ステップごとに、どこに何があるのかを伝えることで、エージェントが判断できるようになる必要があります。
※ML-Agentでメモリを使うことは可能であるが、本記事の範囲外です。詳しくは、ML-Agents Recurrent Neural Network documentationを参照してください。

(10)さらに続けて、次のプログラムを入力する

/// Collect all non-Raycast observations
public override void CollectObservations()
{
   // Whether the penguin has eaten a fish (1 float = 1 value)
   AddVectorObs(isFull);

   // Distance to the baby (1 float = 1 value)
   AddVectorObs(Vector3.Distance(baby.transform.position, transform.position));

   // Direction to baby (1 Vector3 = 3 values)
   AddVectorObs((baby.transform.position - transform.position).normalized);

   // Direction penguin is facing (1 Vector3 = 3 values)
   AddVectorObs(transform.forward);

   // 1 + 1 + 3 + 3 = 8 total values
}

プログラム解説:
・CollectObservations関数をオーバーライドする

(11)さらに続けて、次のプログラムを入力する

private void FixedUpdate()
{
   // Test if the agent is close enough to to feed the baby
   if (Vector3.Distance(transform.position, baby.transform.position) < feedRadius)
   {
       // Close enough, try to feed the baby
       RegurgitateFish();
   }
}

プログラム解説:
・Fixed Update関数を追加
・これは、ペンギンが赤ちゃんに十分近いかを確認し、魚を吐き出して餌を与えようとします。実行する前に、RegurgitateFish関数内をチェックして、お腹がいっぱいかどうかを確認します。

(12)さらに続けて、次のプログラムを入力する

/// When the agent collides with something, take action
/// <param name="collision">The collision info</param>
private void OnCollisionEnter(Collision collision)
{
   if (collision.transform.CompareTag("fish"))
   {
       // Try to eat the fish
       EatFish(collision.gameObject);
   }
   else if (collision.transform.CompareTag("baby"))
   {
       // Try to feed the baby
       RegurgitateFish();
   }
}

プログラム解説:
・OnCollisionEnter関数を追加
・この関数を実装して、タグ「fish」または「baby」を持つアイテムとの衝突をテストし、それに応じた応答をします。

(13)さらに続けて、次のプログラムを入力する

/// Check if agent is full, if not, eat the fish and get a reward
/// <param name="fishObject">The fish to eat</param>
private void EatFish(GameObject fishObject)
{
   if (isFull) return; // Can't eat another fish while full
   isFull = true;

   penguinArea.RemoveSpecificFish(fishObject);

   AddReward(1f);
}

プログラム解説:
・EatFish関数を追加
・これで、ペンギンがお腹いっぱいになっていなければ、魚を食べる機能を追加して、エリアからその魚を取り除き、報酬を獲得します。

(14)さらに続けて、次のプログラムを入力する

/// Check if agent is full, if yes, feed the baby
private void RegurgitateFish()
{
   if (!isFull) return; // Nothing to regurgitate
   isFull = false;

   // Spawn regurgitated fish
   GameObject regurgitatedFish = Instantiate<GameObject>(regurgitatedFishPrefab);
   regurgitatedFish.transform.parent = transform.parent;
   regurgitatedFish.transform.position = baby.transform.position;
   Destroy(regurgitatedFish, 4f);

   // Spawn heart
   GameObject heart = Instantiate<GameObject>(heartPrefab);
   heart.transform.parent = transform.parent;
   heart.transform.position = baby.transform.position + Vector3.up;
   Destroy(heart, 4f);

   AddReward(1f);

   if (penguinArea.FishRemaining <= 0)
   {
       Done();
   }
}

プログラム解説:
・RegurgitateFish関数を追加
・これは、魚を吐き出して赤ちゃんに餌を与える機能です。地面には魚の魂が、空中にはハートが浮かんで、赤ちゃんがどれだけ親に愛されているのかを表現します。
・また、自動破壊タイマーを設定します。エージェントは報酬を得て、魚が残っていなければDone関数を呼び出し、自動的にAgentReset関数を呼び出します。


2.Fish.cs

"Fish"は、各魚に泳ぎを取り付けます。Unityには、水の物理演算が組み込まれていないため、コードを単純化するために、ターゲットの宛先に向かって直線で移動するだけです。

(1)『Fish.cs』を「Visual Studio 2017」で開く
(2)次のコードを入力します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Fish : MonoBehaviour
{
   [Tooltip("The swim speed")]
   public float fishSpeed;

   private float randomizedSpeed = 0f;
   private float nextActionTime = -1f;
   private Vector3 targetPosition;
}

プログラム解説:
・"UnityEngine" のusing宣言を追加
・「FishSpeed」は、魚の平均速度を制御します。
・「randomizedSpeed」は、新しい泳ぐ目的地が選択されるたびに、ランダムに変更される速度
・「nextActionTime」は、新しい泳ぎ先を選択するトリガーとして使用
・「targetPosition」は、魚が泳いでいる宛先の位置

(3)PenguinAcademy クラス内({}間)に次のプログラムを入力する。

/// Called every timestep
private void FixedUpdate()
{
   if (fishSpeed > 0f)
   {
       Swim();
   }
}

プログラム解説:
・FixedUpdate関数を追加
・FixedUpdate関数は、0.02秒の定期的な間隔で呼び出され(フレームレートとは関係ない)、ML-Agentのトレーニングでよくある、ゲーム速度を上げてトレーニングしている時でも対話できます。その中で、魚がどう泳ぐべきかチェックし、泳ぐ場合はSwim関数を呼び出します。

(4)先ほどのプログラムの続きに、次のプログラムを入力する

/// Swim between random positions
private void Swim()
{
   // If it's time for the next action, pick a new speed and destination
   // Else, swim toward the destination
   if (Time.fixedTime >= nextActionTime)
   {
       // Randomize the speed
       randomizedSpeed = fishSpeed * UnityEngine.Random.Range(.5f, 1.5f);

       // Pick a random target
       targetPosition = PenguinArea.ChooseRandomPosition(transform.parent.position, 100f, 260f, 2f, 13f);

       // Rotate toward the target
       transform.rotation = Quaternion.LookRotation(targetPosition - transform.position, Vector3.up);

       // Calculate the time to get there
       float timeToGetThere = Vector3.Distance(transform.position, targetPosition) / randomizedSpeed;
       nextActionTime = Time.fixedTime + timeToGetThere;
   }
   else
   {
       // Make sure that the fish does not swim past the target
       Vector3 moveVector = randomizedSpeed * transform.forward * Time.fixedDeltaTime;
       if (moveVector.magnitude <= Vector3.Distance(transform.position, targetPosition))
       {
           transform.position += moveVector;
       }
       else
       {
           transform.position = targetPosition;
           nextActionTime = Time.fixedTime;
       }
   }
}

プログラム解説:
・Swim関数を追加
・魚は更新されるたびに、新しい速度と目的地を選択するか、現在の目的に向かって移動します。
・新しい行動を取る時には、次のように魚が行動します。
 〇魚の平均速度の50%~150%の間で、ランダムな新しい速度を選択
 〇新しいランダムなターゲットの位置(水中)を選んで泳ぐ
 〇魚を回転させて、ターゲットに向ける
 〇そこに到達するために必要な時間を計算する
・そうでなければ、魚はターゲットに向かって移動し、それを通り過ぎて泳がないことを確認します。


3.最後に

これで、ペンギンが魚を捕まえて、赤ちゃんに餌を与える訓練のための必要なコードがすべて揃いました。
次は、コードを使用するようにSceneを設定します。


この記事が気に入ったらサポートをしてみませんか?