人工知能に関する断創録

このブログでは人工知能のさまざまな分野について調査したことをまとめています(更新停止: 2019年12月31日)

ニューラルネットによる逃避行動の学習

ニューラルネットによるパターン認識(2005/5/5)を紹介しましたが、これがゲームと何の関係があるんだと思った方が多いと思います。今回はニューラルネットをゲームに応用する方法を簡単なサンプルで解説します。ニューラルネットを持った獲物に追跡者から逃げる方法を学習させてみます。

nn_escape.jar

逃避行動の学習

ニューラルネットとはでも解説しましたが、ニューラルネット(以下、NN)は入力パターンと出力パターンの対応関係を学習できる方法です。前回のパターン認識では、描いた数字を01で表現した入力パターンと、認識した数字の場所が1で他は0とした出力パターンを使いました。NNはパターン認識だけではなく、入力と出力を0と1でパターン化できれば何でも学習させることができます。

(注)NNは0と1だけではなく0.57などの小数を入力や出力とすることもできます。あと何でも学習できるというのは言いすぎました。条件によってはできない場合もあります。

逃避行動の学習を例として説明します。赤い■が追跡者でプレイヤーが操作します。青い■が獲物でNNによって動きます。獲物が追跡者から逃げる方法をNNを使って訓練するのが目的です。

f:id:aidiary:20100518155649g:plain

入力パターン

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;
        }
    }

プログラム起動時に訓練してるので起動が少し遅いです。予め訓練したニューラルネットを組み込んだ方がいいかもしれません。

訓練済みニューラルネットの利用

訓練データを使って訓練したニューラルネットを使ってみます。流れは、

  1. 獲物の周囲9マスの状態を調べる
  2. それをNNの入力パターンとして与える
  3. 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の入力に与える)に応じて逃げるか逃げないかを決めるなんてこともできるはずです。いろいろ工夫すると面白いことできそうです。