ニューラルネットによる逃避行動の学習
ニューラルネットによるパターン認識(2005/5/5)を紹介しましたが、これがゲームと何の関係があるんだと思った方が多いと思います。今回はニューラルネットをゲームに応用する方法を簡単なサンプルで解説します。ニューラルネットを持った獲物に追跡者から逃げる方法を学習させてみます。
逃避行動の学習
ニューラルネットとはでも解説しましたが、ニューラルネット(以下、NN)は入力パターンと出力パターンの対応関係を学習できる方法です。前回のパターン認識では、描いた数字を01で表現した入力パターンと、認識した数字の場所が1で他は0とした出力パターンを使いました。NNはパターン認識だけではなく、入力と出力を0と1でパターン化できれば何でも学習させることができます。
(注)NNは0と1だけではなく0.57などの小数を入力や出力とすることもできます。あと何でも学習できるというのは言いすぎました。条件によってはできない場合もあります。
逃避行動の学習を例として説明します。赤い■が追跡者でプレイヤーが操作します。青い■が獲物でNNによって動きます。獲物が追跡者から逃げる方法をNNを使って訓練するのが目的です。
入力パターン
NNへの入力は獲物の周囲9マスにおける追跡者の存在パターンです(上図では場所に0〜9の番号をつけてます)。各マスは追跡者がいたら1、いなかったら0としてます。たとえば、獲物の左、3の場所に追跡者がいたら
000100000
が、真下、7の場所に追跡者がいたら
000000010
がNNへ与える入力パターンになります。今回は追跡者は1人しかいませんが2人以上いても同じです。右(5)と右下(8)にいたら
000001001
となります。
出力パターン
出力パターンは逃げる場所だけが1で他は0というパターンです。たとえば、真下へ右下(8)へ逃げるなら
000000001
がNNの出力パターンとなります。この出力パターンをもとにして実際に獲物を移動させてます。ここまでの説明でわかったと思いますが、入力層、出力層のノード数はともに9個(0〜8)です。中間層は適当に50としました。
訓練データ
次にNNを訓練するのに必要な訓練データを見てみます。「逃避」行動を学習させたいので追跡者がいる方向と反対方向に逃げるよう訓練すればいいですね。つまり、訓練データは下のようになります。
方向 012 345 678
訓練データ 入力パターン 教師信号 100000000 000000001 追跡者が0にいたら8へ逃げる 010000000 000000010 追跡者が1にいたら7へ逃げる 001000000 000000100 追跡者が2にいたら6へ逃げる 000100000 000001000 追跡者が3にいたら5へ逃げる 000000000 000010000 追跡者がいなかったら動かない 000001000 000100000 追跡者が5にいたら3へ逃げる 000000100 001000000 追跡者が6にいたら2へ逃げる 000000010 010000000 追跡者が7にいたら1へ逃げる 000000001 100000000 追跡者が8にいたら0へ逃げる
プログラム中では、
// 訓練データセット // 周囲9マスで追跡者がいる場所を表している // 012 // 345 // 678 private static final int[][] trainingSet = new int[][] { {1,0,0,0,0,0,0,0,0}, // 0 {0,1,0,0,0,0,0,0,0}, // 1 {0,0,1,0,0,0,0,0,0}, // 2 {0,0,0,1,0,0,0,0,0}, // 3 {0,0,0,0,0,0,0,0,0}, // 周りにいない {0,0,0,0,0,1,0,0,0}, // 5 {0,0,0,0,0,0,1,0,0}, // 6 {0,0,0,0,0,0,0,1,0}, // 7 {0,0,0,0,0,0,0,0,1}, // 8 }; // 教師信号 // 周囲9マスの逃げる場所を表している private static final int[][] teacherSet = new int[][] { {0,0,0,0,0,0,0,0,1}, // 8 {0,0,0,0,0,0,0,1,0}, // 7 {0,0,0,0,0,0,1,0,0}, // 6 {0,0,0,0,0,1,0,0,0}, // 5 {0,0,0,0,1,0,0,0,0}, // 4(移動しない) {0,0,0,1,0,0,0,0,0}, // 3 {0,0,1,0,0,0,0,0,0}, // 2 {0,1,0,0,0,0,0,0,0}, // 1 {1,0,0,0,0,0,0,0,0}, // 0 };
追跡者がいなかったら(入力パターンが全部0)だったら動かない(4に移動すれば動かない)ことに注意してください。あと追跡者が2人以上いるパターンは訓練データとして与えてません。このパターンまで与えていたら訓練データを入力するのがだるい。ここはNNがロバスト性を発揮するのを期待しましょう。訓練データを学習させてるのがlearn()です。
/** * 訓練データを学習する */ private void learn() { // 訓練データを学習 double error = 1.0; int count = 0; while ((error > 0.0001) && (count < 50000)) { error = 0; count++; // 各訓練データを誤差が小さくなるまで繰り返し学習 for (int i=0; i<trainingSet.length; i++) { // 入力値を設定 for (int j=0; j<trainingSet[i].length; j++) { brain.setInput(j, trainingSet[i][j]); } // 教師信号を設定 for (int j=0; j<teacherSet[i].length; j++) { brain.setTeacherValue(j, teacherSet[i][j]); } // 学習開始 brain.feedForward(); error += brain.calculateError(); brain.backPropagate(); } error /= trainingSet.length; } }
プログラム起動時に訓練してるので起動が少し遅いです。予め訓練したニューラルネットを組み込んだ方がいいかもしれません。
訓練済みニューラルネットの利用
訓練データを使って訓練したニューラルネットを使ってみます。流れは、
- 獲物の周囲9マスの状態を調べる
- それをNNの入力パターンとして与える
- NNの出力パターンをもとに獲物を動かす
となります。この処理はescape()にまとめてあります。
/** * ニューラルネットの出力に基づいて逃げる */ public void escape() { // 周囲を観察してセンサーデータを取得 // sensor[]に格納される sense(); // ニューラルネットにsensorの値をセット for (int i=0; i<sensor.length; i++) { brain.setInput(i, sensor[i]); } // 出力を計算 brain.feedForward(); // 最大出力を持つノード番号を取得 int dir = brain.getMaxOutputID(); // その方向に逃げる move(dir); }
まんまですね。sense()は周囲9マスの状態をsensor[]配列に格納するメソッドです。
/** * センサーデータをセットする */ private void sense() { // 0で初期化 for (int i=0; i<sensor.length; i++) { sensor[i] = 0; } // 追跡者の位置を取得 int px = predator.x; int py = predator.y; // 追跡者のいる場所のセンサーを1にセット if (x-1 == px && y-1 == py) { sensor[0] = 1; } else if (x == px && y-1 == py) { sensor[1] = 1; } else if (x+1 == px && y-1 == py) { sensor[2] = 1; } else if (x-1 == px && y == py) { sensor[3] = 1; } else if (x+1 == px && y == py) { sensor[5] = 1; } else if (x-1 == px && y+1 == py) { sensor[6] = 1; } else if (x ==px && y+1 == py) { sensor[7] = 1; } else if (x+1 == px && y+1 == py) { sensor[8] = 1; } }
ニューラルネットの利点
今回のプログラムを見てNNなんて使わなくても有限状態機械やif-thenルール(プロダクションルール)で書けるじゃんって思った方いませんか?こんな感じに
if 左に敵がいる then 右に逃げろ else if 上に敵がいる then 下に逃げろ else if 右に敵がいる then 左に逃げろ ・・・ else 動くな
ニューラルネットを使う利点はそのロバスト性(頑健性)にあります。ロバスト性とは入力パターンにノイズがあったり、未知(訓練データにない)パターンが入ってきても適切な出力を出せる性質を言います。今回の訓練データでは追跡者が2人以上いる場合のパターンは与えてませんでした。それにもかかわらず、NNは2人以上いる場合でも適切な方向に逃げられます。たとえば、左上(0)と上(1)左(3)に追跡者がいる訓練データは与えていませんが獲物はちゃんと右下(8)に逃げようとします。プログラマーが指示してないのにちゃんと動けるんですよ。すごいですねー。これはプログラマーがあらゆる規則を定める有限状態機械やif-thenルールでは実現できない機能です。
(注)この実験は付属のNNTest.javaで試すことができるのでやってみてください。
(注)ただし、プログラマーが想定した方向に逃げてくれない場合もあります。ロバスト性ってのはかなりいい加減なものですが獲物の個性があるようで面白いものです。
ゲームの状態は何でも0と1を使ったパターンに変換することができます(コード化と呼びます)。NNを応用する方法を探すのは面白いかもしれません。今回のサンプルは逃避じゃなくて磁石じゃないかって思った方・・・正解(笑)獲物は周囲9マスしか観測してないんで隣接してないと逃げられないんですね。観測範囲をもっと拡げるといいかもしれません。あと自分の体力(これもコード化してNNの入力に与える)に応じて逃げるか逃げないかを決めるなんてこともできるはずです。いろいろ工夫すると面白いことできそうです。