はじめに
今回は、いよいよML-Agentsを用いて強化学習の環境を実装していきます。
エージェントの実装
実はここまでのコードで、環境の実装はほとんど終わっています。ここからはその2までの記事で作ってきたコードをML-Agentsの仕組みに乗せるだけです。そのために、Agentクラスを継承した新しいエージェントである、JumpAgentクラスを作成します。Agentは、MonoBehaviourのサブクラスで、Agentクラスの仮想メソッドをオーバーライドすることによりML-Agentsにおけるエージェントを実装できます。今回オーバーライドを行うメソッドは以下の4つです。
OnEpisodeBegin
CollectObservation(VectorSensor sensor)
OnActionReceived(ActionBuffers actionBuffers)
WriteDiscreteActionMask(IDiscreteActionMask actionMask)
OnEpisodeBegin
このメソッドはエピソード開始時に呼ばれ、エージェントの初期化を行います。また、Training Areaに属するエージェント以外の初期化も同時に行います。このメソッドの特徴は、初期化の前の状態は、エピソード終了直後であるということです。例えば、エージェントは落下中かもしれませんし、敵キャラと衝突中かもしれません。
public ControlPlayer ControlPlayer;
public ControlEnemy ControlEnemy;
public override void OnEpisodeBegin()
{
float enemyX = Random.Range(-5f, 5f);
// enemyXから1以上離れた座標にプレイヤーを配置する
float playerX = enemyX;
while(Mathf.Abs(playerX - enemyX) < 1)
{
playerX = Random.Range(-4.5f, 4.5f);
}
int enemyDirection = 2 * Random.Range(0, 2) - 1;
// 初期化
ControlEnemy.transform.localPosition = new Vector3(enemyX ,0.5f, 0);
ControlEnemy.SetDirection(enemyDirection);
ControlPlayer.transform.localPosition = new Vector3(playerX, 0.5f, 0);
}
CollectObservation
環境を観測する(環境の状態を取得する)メソッドです。このメソッドは、stepのうち観測のフェーズに入ったときに呼び出され、ここで設定された情報がBrainに送信され、Brainでまとめられた情報がPythonに送信されます。
なお、観測を行う方法は他に2種類存在しますが、今回のような場合にはCollectObservation
メソッドを使うのが一般的ですので、解説は後の記事に回します。
引数のVectorSensor
は、観測したデータを特定の順番で覚えることができる構造体です。sensor.AddObservation
メソッドを呼び出すことで観測したデータを記録できます。引数は多数の型に対応しており、int,float,bool,Vector3,Vector2,Quaternion
を使うことができます。なお、AddObservation
メソッドにわたされる観測は、どのような状態でCollectObservation
が呼ばれても常に同じ型、順番でかつ同じ数の要素を含んでいないといけません。これはML-Agents側の制約というよりかは、このようにしないと、ニューラルネットワークに与えられる入力が一定の形状を保てないことが原因となっている制約です。
public override void CollectObservations(VectorSensor sensor)
{
// 観測で使われる変数の数は6である
// 自分のxy座標(連続):2
sensor.AddObservation(ControlPlayer.GetPosition());
// 敵のx座標(連続):1
sensor.AddObservation(ControlEnemy.GetPosition());
// 自分のxy速度(連続):2
sensor.AddObservation(ControlPlayer.GetVelocity());
// 敵の進行方向(-1と1で離散):1
sensor.AddObservation(ControlEnemy.GetDirection());
}
このような実装を行ったら、ML-Agent側にどのような観測をするのかを設定します。これにより、Python側でも「6個の値が得られるらしい」などの情報が得られるようになります。
この設定にはインスペクタでの操作が必要です。現在作成しているJumpAgent
コンポーネントをAgent
ゲームオブジェクトにアタッチします。すると、自動的にBehaviorParameters
コンポーネントがAgent
ゲームオブジェクトにアタッチされます。これはエージェントに関連する設定と、決定に関連する設定を保持しているコンポーネントです。設定すべき項目はたくさんありますが、いったんVector Observation
に話を絞ります。
今回は6つの値を観測するので、Space Sizeを6にしましょう。これで設定完了です。なお、Stacked Vector3
は、直前n回の入力を保持しておくというもので、直前の入力と今回の入力との差分が重要な情報になる場合や、リカレントニューラルネットワークを用いた学習手法を利用する場合には1以外の値を設定することになりますが、今回は1にします。
OnActionReceivedその1:行動の実行
このメソッドは、stepのうちBrainが行動を決定したときに呼ばれます。このメソッドの責務は割と大きめで、以下の3つの処理を行います。
- 決定に基づき行動を行う
- エピソードの終了判定を行う
- 報酬の取得を行う
引数のActionBuffersには決定された内容が保存されています。ここで重要になってくるのは「離散的な行動」と「連続的な行動」の概念です。
- 離散的な行動:やるorやらない、4つのだせる技のなかから1つ選ぶ、などの有限個の選択肢をもつ行動
- 連続的な行動:力xで押す、距離d進む、などの無限個の選択肢をもつ行動
今回は「ジャンプするかどうか」と「どの方向に進むか、もしくは進まないか」の2つの離散的な行動をとります。そこで、そのような引数を受け取ることができるようにBehavior Parameters
コンポーネントの設定を行います。今回はActionsという設定項目に注目してください。
ここでは、アクションの種類と数についての設定ができます。Continuous Actionsは連続的な行動の個数、Discrete Branchesは離散的な行動の個数を示しています。今回は画像のようにそれぞれ0と2を設定しています。Discrete Branchesに値を設定すると、Branch n Sizeという項目が現れます。これは、それぞれの行動が何個の選択肢を持っているのかという設定です。今回は「ジャンプをするorしないの2通り」「右に動く、左に動く、そのままの3通り」なので、それぞれ2と3を設定しています。
設定が終われば、実装に移りましょう。
public override void OnActionReceived(ActionBuffers actions)
{
// Behavior Parameters画面で設定した順番に行動が取得できる
int jumpAction = actions.DiscreteActions[0]; // 0,1のうちどれか
int moveAction = actions.DiscreteActions[1]; // 0,1,2のうちどれか
// エージェントの行動処理
if(jumpAction == 1)
{
ControlPlayer.Jump();
}
ControlPlayer.Move(moveAction - 1); // -1,0,1のどれかにすることによってMoveの引数に対応させている
// 報酬の確認
if (ControlPlayer.Rewards.Count > 0)
{
foreach(var reward in ControlPlayer.Rewards)
{
AddReward(reward);
}
ControlPlayer.Rewards.Clear();
}
// エピソード終了判定
if (ControlPlayer.IsEpisodeEnd)
{
ControlPlayer.IsEpisodeEnd = false;
EndEpisode();
}
}
actions引数のDiscreteプロパティでは、指定した数の離散的な行動の内容がリスト形式(正確にはActionSegment<int>
)で保存されていて、インデクサを用いてint型の値として取得できます。
OnActionReceivedその2:報酬の決定とエピソード管理
Agentクラスは報酬取得用に2つのメソッドを提供しています。SetRewardとAddRewardです。どちらもfloat型の値をとり、大きな違いはありません。AddRewardが連続して呼ばれた場合、報酬を足し算して最終的な報酬とすることに対して、SetRewardが最後に設定された報酬で報酬を上書きするという細かい差があるだけです。
このようにして、「行動」と「報酬」をひとつのメソッドで実装して、CollectObservationと交互に一定間隔で呼び出し続けることによって、経験データを収集していきます。そして、最終的にはエピソードが何らかの理由で終了します。今回の場合は、「敵と接触」「落下」「一定時間の経過(後述)」のうちのどれかの原因により終了するのですが、エピソード終了をBrainに知らせるためにEndEpisode
メソッドを呼び出す必要があります。今回の場合、終了判定自体はControlPlayerで行われているので、単にその値を判定するだけの実装になっています。
なお、今回は物理シミュレーション系のタスクなのでOnActionReceivedにて報酬決定とエピソード管理を行っていますが、ターン制ゲームなどのタスクの場合は別のタイミングでSetRewardなどを呼び出すことになります。
WriteDiscreteActionMask
離散的な行動の場合、特定の行動を禁止したくなる場合があります。例えば、今回の例では二段ジャンプはできないので、高さが一定以上の場合はエージェントがジャンプという行動をとるのを禁止したいです。このような場合に使えるのがWriteDiscreteActionMask
メソッドです。
public override void WriteDiscreteActionMask(IDiscreteActionMask actionMask)
{
// ジャンプできるかどうかを指定する
actionMask.SetActionEnabled(0, 1, ControlPlayer.CanJump);
}
引数のIDiscreteActionMask
インターフェースは、SetActionEnabled
だけを持ちます。これは、第一引数にどの行動を禁止するのか、第二引数にどの選択肢を禁止するのか、第三引数に禁止するかどうか、を指定します。サンプルコードでは、OnActionReceived
でaction.DiscreteActions[0]
で取得できるジャンプに関する行動について指定したいので、第一引数には0を指定しています。また、禁止したいのは「ジャンプする」に対応する1なので、第二引数には1を指定しています。そして、第三引数にfalseを指定すると行動を禁止できます。なお、trueを指定しても特に意味はありません。
おわりに
今回はAgent
を継承したJumpAgent
コンポーネントを実装しました。ここはML-Agentsのお作法というべきところで、覚えるべきインターフェースやルールが多いです。しかしながら、その実装のほとんどはControlPlayer
やControlEnemy
などで強化学習を想定して作成したメソッドを使いまわせています。繰り返しになりますが、重要なのは強化学習であってツールの使い方ではないです。