こちらのページでは、ML-Agentsの公式サンプルに入っています、「Tennis」についての解説ページになります。
「Tennis」は2つの「Agent」(青ラケット君と赤ラケット君)を左右移動+ジャンプさせてお互いにボールを打ち合っていこう!というゲームになります。今回のサンプルは、1つのBrainに紐づけられた2つのAgentがお互いに対戦することで機械学習を行うサンプルになっています。
なお、本ページはML-Agents(ver0.5)に基づいて解説していきます。2018年12月17日に、ML-Agents(ver0.6)が新規にリリースされました。ver0.6では、Brain Typeの設定方法が大きく変更されております。設定方法についてはこちらのページにまとめましたので、ML-Agents(ver0.6)でサンプルをいじる際は本ページと併せて参考にして下さい。
とりあえず、どんなゲームかを体感するために自分で遊んでみましょう。今回のゲームには2種類のBrain「PlayerBrain」と「TennisBrain」が用意されています(初期状態では両Agent共に「TennisBrain」が紐づいています)。それぞれの「Brain Type:Player」にすると分かりますが、下図の通り違う操作キーが割り当てられています。よって、両者のAgentを異なるBrainと紐づければ人vs人の対戦も可能なゲームとなります。
今回はAgentA(青ラケット君)に「PlayerBrain(Brain Type:Player)」を紐づけ、AgentB(赤ラケット君)に「TennisBrain(Brain Type:Internal)」を紐づけて、人vsAIの対戦をやってみましょう(「PlayerBrain」が非表示になっている場合は表示させて下さい、非表示のままだとエラーになります)。
「PlayerBrain」側の操作は左上図より、
・「←」:vectorAction[0]に値「-1」を返す
・「→」:vectorAction[0]に値「1」を返す
・「左Ctrl」:vectorAction[1]に値「1」を返す
です。後で解説しますようにスクリプト(TennisAgent.cs)を見ると、以下のように対応していることが分かります。
・「←」:AgentA(青ラケット君)が左に移動
・「→」:AgentA(青ラケット君)が右に移動
・「左Ctrl」:AgentA(青ラケット君)が上にジャンプ
ちなみにスクリプト全体を見渡すと、ボールを自陣に落下させると報酬-0.01、相手側にボールを返すと報酬+0.1、が入る仕組みになっており、これからいかにラリーを続けられるか、というゲームになっていることが分かります。相手側にボールを落とすこと以上に、いかに自陣にボールを落とさないかが大切なゲームになっているようですね。この辺り、スクリプト中の報酬値を変えてみるとまた違うタイプのAIが出来上がるのでしょうか?色々試してみるのも面白そうですね!
では次に、両者のAgentに「TennisBrain(Brain Type:External)」を紐づけてこのゲームを機械学習させてみましょう(実行のための詳細手順はコチラのページを参照)。今回、ゲーム内には18コート並んでいますので、3DBallのサンプルと同様、TennisBrainに18×2個のAgentが紐づき、効率良く機械学習出来るようになっております。
機械学習の最初の方では↓のように、両者のAgentは共にランダムにウロウロしているだけで、ボールを受け取ろうともしません。全くラリーが始まらない…。
(注:上の動画は機械学習のオプションに「--slow(通常速度で学習実行)」を入れて実行したものを撮影しています)
しかし、5万ステップ程学習した後の動きを見てみると、↓のようにちゃんとTennisしております!機械学習の効果により、両者のAgentはゲームの目的をしっかり理解して行動しているようです。本当機械学習ってスゴイですね!
この「Tennis」には、4つのスクリプト(TennisAcademy.cs、TennisAgent.cs、TennisArea.cs、HitWall.cs)が付いております。TennisArea.csは各コートの親オブジェクト「TennisArea」に、HitWall.csは「Ball」オブジェクトに、TennisAgent.csは両者の「Agent」に紐づいております。Academy、Agentスクリプトの概要についてはサンプル集解説0にて書いた通りですが、今回TennisAcademy.csには特に何も書かれておりません。よって、本ページではそれ以外の3スクリプトについて解説していきます。解説の都合上、「TennisAgent.cs」→「HitWall.cs」→「TennisArea.cs」の順に解説していきます。
これまでのサンプル解説ページではスクリプトの中身を極力全部解説していましたが、各ページかなり長い解説になっているため、今回以降は重要な箇所に絞って解説していきます(それでも長いですが…)。解説を省略した箇所については、基本これまでのサンプル解説(その1、その2、その3)で同様のスクリプトが登場しているハズ…ですので、一度ご自身で探してみて下さい。
サンプル集解説0にて書きましたが、「Agent」に付けるスクリプトは「Agent」クラスを継承しており、5つの(オーバーライド)メソッドを持っているんでしたね。では、各メソッドの詳細を見ていきましょう。
「Agent」を初期位置に戻し、速度も0に戻します。ただし、初期の左右位置(x座標)には少しだけランダム要素があるようです。
ちなみに、変数invertMultはpublic bool変数invertX(チェックあり:True、チェックなし:False)に依る変数で、以下のように値が入ります。
・AgentA:チェックなし→invertX=False→invertMult=+1f
・AgentB:チェックあり→invertX=True→invertMult=-1f
要するに、AgentAもAgentBも同じスクリプト「TennisAgent.cs」で紐づいていますが、net(x=0)を挟んで反対側にいますので、両者に対応するためにはx座標の±をひっくり返す必要があります。そこの役割を変数invertMultにさせております。
今回のゲームでは、Agent(自分自身)の状態(位置+速度)に加え、Ballの状態(位置+速度)をBrainに返しています。この辺は3DBallのサンプルと同様で、Agent(自分自身)の状態だけ分かっても、BallがどこにあるかでAgentがやるべき行動は全然変わります(基本、Ballの下に潜り込む必要がありますよね)ので、Ballの状態も知ることが機械学習には必須ですよね。
public override void CollectObservations() { AddVectorObs(invertMult * (transform.position.x - myArea.transform.position.x)); AddVectorObs(transform.position.y - myArea.transform.position.y); AddVectorObs(invertMult * agentRb.velocity.x); AddVectorObs(agentRb.velocity.y); AddVectorObs(invertMult * (ball.transform.position.x - myArea.transform.position.x)); AddVectorObs(ball.transform.position.y - myArea.transform.position.y); AddVectorObs(invertMult * ballRb.velocity.x); AddVectorObs(ballRb.velocity.y); }
具体的には、上のように「Agent(自分自身)の位置座標(x、y)、速度(x、y)」「Ballの位置座標(x、y)、速度(x、y)」で、float値を合計8成分返しています(今回のゲームは奥行方向に動かないのでz方向の情報は不要)。右下図の「Space Size」が「8」になっているのはこれが理由です。
var moveX = Mathf.Clamp(vectorAction[0], -1f, 1f) * invertMult; var moveY = Mathf.Clamp(vectorAction[1], -1f, 1f); if (moveY > 0.5 && transform.position.y - transform.parent.transform.position.y < -1.5f) { agentRb.velocity = new Vector3(agentRb.velocity.x, 7f, 0f); } agentRb.velocity = new Vector3(moveX * 30f, agentRb.velocity.y, 0f);
なお、以下のif文でAgentの位置座標に制約を課しています。要するにnet(x=0)に近付き過ぎはNGってことです。
if (invertX && transform.position.x - transform.parent.transform.position.x < -invertMult || !invertX && transform.position.x - transform.parent.transform.position.x > -invertMult) { transform.position = new Vector3(-invertMult + transform.parent.transform.position.x, transform.position.y, transform.position.z); }
次に、報酬やエピソードの完了…についての解説に行きたい所ですが、今回のゲームではこれらの工程は「HitWall.cs」(Ballに付いてるスクリプト)の方に書かれていますので、次にそちらの解説に行きましょう!
通常のMonoBehaviourクラスのスクリプトですが、Ballオブジェクトが他のオブジェクトと衝突・干渉した際にAgentA、AgentBに報酬を与える(、エピソード完了を通知する)役割を持っています。
スクリプト中ではまず、OnTriggerExitメソッド(当たってもすり抜け)内にて、overオブジェクトと干渉完了=netの上を越えたら直前にBallを打った(*)Agentに報酬+0.1が入ります。
また、OnCollisionEnterメソッド(当たったらはね返り)内にて、上図内のover以外のオブジェクトと衝突すると、勝ったAgentに報酬+0(代わりに点数+1)、負けたAgentに報酬-0.01が入ります。スクリプト中では、様々なシチュエーションに対して報酬をどうするか、が事細かに書かれていますが、要するに勝ち負け判定(*)は通常のテニスのルールに準拠しています。報酬・得点を支払った後は、Doneメソッドにてエピソード終了になります。
(*)変数lastAgentHitという、AgentAが打てば0、AgentBが打てば1になる変数により、直前に誰が打ったか分かる、という仕組みを取っています。勝敗判定する際にも、最後に誰がボールを打ったかは重要ですよね。
if (collision.gameObject.CompareTag("agent")) { lastAgentHit = collision.gameObject.name == "AgentA" ? 0 : 1; }
また、別スクリプト「TennisArea.cs」より、最初にボールが落下してきて誰も打ち返してない状態ではlastAgentHit=-1になっています。
同じく通常のMonoBehaviourクラスのスクリプトで、先程の「HitWall.cs」内でエピソードの終了を宣言した際、併せて本スクリプト中のMatchResetメソッドが発動します。本メソッド内に書かれているのは、要するに「Ball」を初期位置に戻し、速度も0に戻すだけです。ただし、初期の左右位置(x座標)には少しだけランダム要素があるようです。あと、左or右どちらのコートに落とすかもランダムです。