こちらのページでは、ML-Agentsの公式サンプルに入っています、「PushBlock」についての解説ページになります。
「PushBlock」は「Agent」(青小立方体)を上下左右移動させて橙大立方体の「Block」をゴールまで押していこう!というゲームになります。今回のサンプルは、「RayPerception」というML-Agents特有の機能を用いて機械学習を行うサンプルになっています。
なお、本ページは、2018年12月17日に新規リリースされたML-Agents(ver0.6)に基づいて解説していきます。ver0.6では、1つ前のver0.5と比べてBrain Typeの設定方法が大きく変更されました。ver0.5とver0.6の違いについてはこちらのページにまとめましたので、ML-Agents(ver0.5)で本サンプルをいじる際は本ページと併せて参考にして下さい。
・「W」:vectorAction[0]に値「1」を返す
・「S」:vectorAction[0]に値「2」を返す
・「D」:vectorAction[0]に値「3」を返す
・「A」:vectorAction[0]に値「4」を返す
です。いつもの通り、これだけでは何のことか全然分かりませんが、後で解説しますようにスクリプト(PushAgentBasic.cs)を見ると、以下のように対応していることが分かります。
・「W」:Agentが前に進む
・「S」:Agentが後ろに下がる
・「D」:Agentが(上から見て)反時計回りに回転
・「A」:Agentが(上から見て)時計回りに回転
ちなみに、ゲーム上には全32個のAgentがありますが、先程全てのAgentのBrainを「PushBlockPlayer」Brainアセットと紐づけましたので、32個のAgentを同時操作することになります。ただ、スクリプトを読むと分かりますが、Agentの初期位置、Blockの初期位置、ゴールの位置(上下左右端のいずれか)は完全にランダムで、初期状態はどのフィールドもバラバラな状態になります。よって、人力では32個のフィールド同時にゴールを目指すのは不可能です。なので、本気で遊ぶ場合も1個のフィールドに対してだけ集中して遊びましょう(笑)。
ちなみにスクリプトより、Blockをゴールまで押し込むと報酬+5、各ステップ報酬-0.0002、が入る仕組みになっており、これからいかに素早くBlockをゴールまで押し込めるか、というゲームになっていることが分かります。
実際プレイしてみた感じはいかがでしょうか?人間が遊ぶ分にはそこまで難しくないゲームですが、Agentは連続的に動きますし、自由度がある分AIにプレイさせるのは難しいかも…??
では次に、Brainに機械学習させてみましょう。ver0.6での設定方法はこちらにも書きましたが、AgentのPrefabに「PushBlockLearning」Brainアセットを紐づけ、「Academy」内のBrainsに「PushBlockLearning」設定→control欄にチェック入れる、(ver0.5では「Brain Type:External」に設定)でしたね。
今回、ゲーム内には32個のフィールドが並んでいますので、3DBallのサンプルと同様、「PushBlockLearning」Brainアセットに32個のAgentが紐づき、効率良く機械学習出来るようになっております。
機械学習の最初の方では↓のように、Agentは意味もなくただフィールド上をウロウロしているだけで、ゲームの目的もよく分かっていない感じがします。「何か橙色の箱置いてあるけどコレで何すんの…?」って感じの動きですねぇ…。
(注:上の動画は機械学習のオプションに「--slow(通常速度で学習実行)」を入れて実行したものを撮影しています)
しかし、5万ステップ程学習した後の動きを見てみると、↓のようにほぼ最適ルートでBlockをゴールまで押し込んでいます!(機械学習の過程を見た感じ、2万ステップ位でほぼ↓のような動きが出来ていました!早い!)機械学習の効果により、Agentはゲームの目的をしっかり理解して行動しているようです。機械学習ってスゴイですよね~。
この「PushBlock」には、3つのスクリプト(GoalDetect.cs、PushAgentBasic.cs、PushBlockAcademy.cs)が付いております。GoalDetect.csはBlockオブジェクトに、PushAgentBasic.csはAgentオブジェクトに紐づいております。Academy、Agentスクリプトの概要についてはサンプル集解説0にて書いた通りですが、今回「PushBlockAcademy.cs」では変数定義を行っている程度で特に難しいことは書かれておりません。また、「GoalDetect.cs」は「PushAgentBasic.cs」のオマケ的な位置付けになっています。そこで、本ページでは主にスクリプト「PushAgentBasic.cs」の中身を解説し、ついでに「GoalDetect.cs」の中身も紹介していきます。
今回もページ内の解説を少し簡略化するため、重要な箇所に絞って解説していきます。解説を省略した箇所については、基本これまでのサンプル解説(その1、その2、その3、その4)で同様のスクリプトが登場しているハズ…ですので、一度ご自身で探してみて下さい。
サンプル集解説0にて書きましたが、「Agent」に付けるスクリプトは「Agent」クラスを継承しており、5つの(オーバーライド)メソッドを持っているんでしたね。今回のスクリプト「PushAgentBasic.cs」は、それ以外にも幾つかのメソッドがありますが、主要となるメソッドは5つの(オーバーライド)メソッドであることに変わりはありませんので、今回もこれらの(オーバーライド)メソッドに焦点を当てて、詳細を見ていきましょう。
(オーバーライド)メソッドに行く前に、まずはこのAwakeメソッドが一番最初に呼び出されます。内部では、AcademyオブジェクトをFindメソッドで検索しています。今回のAgentスクリプトでは、Academy中で定義されている変数を呼び出すので、そのためにまず最初にAcademyオブジェクトを検索しています。
フィールド中の各オブジェクトを定義しています。あまり見ない物を中心に解説します。
・rayPer = GetComponent<RayPerception>();
・areaBounds = ground.GetComponent<Collider>().bounds;
groundオブジェクト(地面)のCollider(当たり判定)サイズを記憶させておきます。後程、AgentとBlockの初期位置をランダムに決定する際、両者共にgroundオブジェクト範囲内に着地出来るよう、ランダム範囲を設定する時に備えて、あらかじめ本変数を定義しておきます。
・groundRenderer = ground.GetComponent<Renderer>();
groundMaterial = groundRenderer.material;
これら2行を使って、初期状態の地面(groundオブジェクト)の色を記憶しておきます。ゲームを遊ぶと分かりますが、Blockをゴールまで持って行くと、地面の色が数秒間緑に変化し、その後元の色に戻ります。この手続きを後々行うために、初期状態の地面色を覚えさせておく必要があります。
フィールドの向き、Agent(青小立方体)とBlock(橙大立方体)の初期位置、初期速度を設定します。
フィールドの向きは元向き・90°回転向き・180°回転向き・270°回転向きのどれかからランダムで選ばれます。また、Agent(青小立方体)とBlock(橙大立方体)の初期状態については、まず初期位置はGetRandomSpawnPosメソッドにより、groundオブジェクト範囲内でのランダム位置に設定され、そして初期速度は0で設定されます。
ランダム位置(座標)の設定方法は、先程①で定義した変数「areaBounds」を用いて、以下のように設定します(Z座標も同様)。
X座標 = Random.Range(-areaBounds.extents.x*0.5, areaBounds.extents.x*0.5) ;
Bounds.extentsでgroundオブジェクトのColliderの半分サイズの値になりますので、更に0.5を掛けることで初期位置はフィールド中心付近になります。
今回のゲームでは、RayPerceptionの機能を用いてAgentの周辺にどんなオブジェクトが置いてあるか、の情報をBrainに返して機械学習を行います。そのため、以下のようにPerceiveメソッドを用います。
public override void CollectObservations() { if (useVectorObs) //デフォルトではチェックあり=「true」 { var rayDistance = 12f; float[] rayAngles = { 0f, 45f, 90f, 135f, 180f, 110f, 70f }; var detectableObjects = new[] { "block", "goal", "wall" }; AddVectorObs(rayPer.Perceive(rayDistance, rayAngles, detectableObjects, 0f, 0f)); AddVectorObs(rayPer.Perceive(rayDistance, rayAngles, detectableObjects, 1.5f, 0f)); } }
Perceive(Rayの距離,Rayの角度(xz平面内)の配列,衝突判定を行うオブジェクトのタグ配列,Rayのスタートy座標,Rayのゴールy座標)という構成です。
Perceiveメソッドを用いた場合Brainに返す値の数はfloat値(Rayの角度数)×(判定を行うオブジェクト数+2)成分になります。今回の場合、7×(3+2)=35個のfloat値を返します(Perceiveメソッドを2回使用しているので、合計35×2=70個)。
Rayの角度 | blockと衝突 | goalと衝突 | wallと衝突 | 衝突なし | 距離 |
0°光線 | 0 | 0 | 0 | 1 | 0 |
45°光線 | 0 | 1 | 0 | 0 | 0.8 |
70°光線 | 1 | 0 | 0 | 0 | 0.4 |
90°光線 | 0 | 1 | 0 | 0 | 0.7 |
110°光線 | 0 | 1 | 0 | 0 | 0.9 |
135°光線 | 0 | 0 | 1 | 0 | 0.6 |
180°光線 | 0 | 0 | 1 | 0 | 0.7 |
0がNo、1がYesを意味します。また、距離は光線全体長を1として、Agentから衝突物間の距離を示します(上表の値は参考のために書いており、正確な値ではありません、悪しからず…m(_ _)m)。
・transform.forward/up/right:Agent正面方向/上方向/右方向の単位ベクトル
・AddForce(ベクトル,ForceMode.VelocityChange):瞬間的に速度付与される
最後に、Blockに紐づいているスクリプト「GoalDetect.cs」の解説を簡単に。
本スクリプトでは、Blockとgoalが衝突し始めたらPushAgentBasic.cs中のIScoredAGoalメソッドを呼び出します。本メソッドに入ったら報酬+5が入り、さらにコルーチンGoalScoredSwapGroundMaterialを呼び出して、地面の色を数秒間緑に変化させ、その後元の色に戻ります。