simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

対戦ゲームで一喜一憂するAI

強化学習でまるばつゲーム(3目ならべ)を作ったことがありましたが、ふと相手の手や自分の手に関して一喜一憂するAIが作れるのではないかと思ったわけです。
作ってみます。

AIには最近扱いに慣れてきたSDユニティちゃんを使わせてもらおうと思います。
感情表現としては、用意されている表情でやってみますか。

smile2, confuse, sad, scold, strain, surprise, damaged, relux の計 8 種類の感情表現とします。

状況に応じて、どの感情を表すかは後で決めることにしますか。
まずは対戦ゲームの方を完成させます。

簡単なユースケース駆動開発をしてみます。
まずは「リトライ(開始)ボタン」を押すとゲームが最初からやり直しとなります。
最初に「先攻後攻決めアニメ」が走ります。
自分の番になると、「青い Your Turn」のボードが一瞬出てきて消えます。
「盤面」の「マス」にホバーすると「半透明の〇 or ×マーク」がマス内に表示されます。
ホバーするマス目ごとに「SDユニティちゃん」が「感情表現」を変化させます。
配置したいマスをクリックすると、「配置音」と同時に「不透明の〇×マーク」がマス内に配置されます。
配置されたマス目ごとに「SDユニティちゃん」が「感情表現」を変化させます。
相手の番になると「赤い Enemy Turn」のボードが一瞬でてきて消えます。
「SDユニティちゃん」が感情表現しながら、マウスをホバーして「半透明の〇 or ×マーク」がマス内に表示され、そのマスごとに「感情表現」を変えます。
意思決定をしたら、SDユニティちゃんは、空いているマスに配置、「配置音」と同時に「不透明の〇×マーク」がマス内に配置されます。
配置されたマス目ごとに「SDユニティちゃん」が「感情表現」を変化させます。
「ゲームが終了、勝敗」が決まると、「SDユニティちゃん」が「感情表現」を変化させます。
「リトライ(開始)ボタン」を押すとゲームが最初からやり直しとなります。

ここで出てきたオブジェクトを列挙します。
・リトライ(開始)ボタン
・先攻後攻決めアニメ
・赤い Enemy Turn ボード、青い Your Turn ボード
・盤面/マス
・SDユニティちゃん
・感情
・配置音
・半透明、不透明の〇×マーク
・ゲーム結果、勝敗
まずはこれらのアセットを準備します。

…そろいました。
f:id:simplestar_tech:20170205202442j:plain

ではユースケースを実装してみましょう。
「リトライ(開始)ボタン」を押すとゲームが最初からやり直し
「先攻後攻決めアニメ」が走る
自分・相手の番になると、「青い・赤い Turn」のボードが一瞬出てきて消える
「盤面」の「マス」にマウスがホバーすると「半透明の〇 or ×マーク」がマス内に表示
ホバーするマス目ごとに「SDユニティちゃん」が「感情表現」を変化させます。
配置したいマスをクリックすると、「配置音」と同時に「不透明の〇×マーク」がマス内に配置される。
相手の番になると「赤い Enemy Turn」のボードが一瞬でてきて消える。

f:id:simplestar_tech:20170206004929j:plain

とりあえず、今ここまで実装を進めてます。

意思決定をしたら、SDユニティちゃんは、空いているマスに配置、「配置音」と同時に「不透明の〇×マーク」がマス内に配置されます。
配置されたマス目ごとに「SDユニティちゃん」が「感情表現」を変化させます。
「ゲームが終了、勝敗」が決まると、「SDユニティちゃん」が「感情表現」を変化させます。
「リトライ(開始)ボタン」を押すとゲームが最初からやり直しとなります。

f:id:simplestar_tech:20170227090030j:plain

をやりました。

次のだけ入っていない
「SDユニティちゃん」が感情表現しながら、マウスをホバーして「半透明の〇 or ×マーク」がマス内に表示され、そのマスごとに「感情表現」を変えます。

時間が空いてしまって、熱量が足りない…
気が向いたら、入れようと思います。

一喜一憂するAIは、わかっていても対戦していてなかなか楽しいことがわかりました。

f:id:simplestar_tech:20170227090243j:plain

ユニティちゃんライセンス

この作品はユニティちゃんライセンス条項の元に提供されています

Unity:mecanimのイロハ

SD Toon シェーダーの Unityちゃんで、ステート遷移によってアニメーションが変化する仕組み mecanim を勉強してみようと思います。
ほんとに今更ですが、基本的なところを触ってみます。

f:id:simplestar_tech:20170205134929j:plain

ボーンの入ったモデルと、そのボーンを動かすアニメーションクリップを用意します。
具体的にはモーションを設定した FBX ファイルをどこからか持ってくるという作業です。

今回は SD Toon Unity ちゃんとしました。
入手場所はこちら
unity-chan.com


手順をわかりやすく、まっさらなシーンに Unity ちゃんのモデルを配置してみます。

f:id:simplestar_tech:20170205135922j:plain

メッシュにはディフォルトのマテリアルが割り当てられているので Toon フォルダにある body 用、face 用 hair 用などをそれぞれのメッシュに割り当てます。
加えて、ディレクショナルライトを3つつくり、それぞれ別のレイヤーだけに影を落とすようにし
body, face, hair, head のレイヤーを変更し
Lighting にて Ambient を Skybox から Color White に設定します。
するとこんな見た目に変化します。

f:id:simplestar_tech:20170205142140j:plain

Model をそのまま配置しただけですが、ディフォルトで Animator と Avatar まで割り当たっていました。
これは便利なのでそのまま使いましょう。

次に AnimatorController を作ります。

最初はブレンドツリーというステートを一つ作ってみます。
このステートをダブルクリックすると、次のようなブレンドに関するUIが出てきます。(すでに別途 Walk と Run のモーションをブレンドするように追加した状態です。最初は何もないよ)

f:id:simplestar_tech:20170205145425j:plain

Blend パラメータを調整すれば、二つのモーションをその割合でブレンドするようになります。
そのほかのパラメータ設定は調整次第ですね。ここまで基本的なことができれば、モーションブレンドの基本は OK だと思います。

次に、アニメーションイベントをやってみます。

アニメーションクリップを見てみると、Walk に関しては次のようにアニメーションイベントが設定されています。

f:id:simplestar_tech:20170205150200j:plain

Animator が設定されている階層に、スクリプトコンポーネントを配置して、ここにある Function 名の関数を用意してみます。
すると、アニメーションが開始されたタイミングでこの関数が呼ばれます。

要は、モーションのあるタイミングで表情や音、エフェクトなどを発生させたいときなどに、これらのイベントをモーションに仕込んでおくと便利なことになりそうです。
何処にイベントを仕込んだのか、一覧表示できるとうれしいですが、そういうところ気が利かない機能かもしれません。(どこからも呼ばれないはずの関数が呼ばれてしまうというバグが残りそう…)

public class UnityChanBehaviour : MonoBehaviour {
    public AnimationClip[] animations;
    // Use this for initialization
    void Start () {
		
	}
	
	// Update is called once per frame
	void Update () {
		
	}

    public void OnCallChangeFace(string str)
    {
        int ichecked = 0;
        foreach (var animation in animations)
        {
            if (str == animation.name)
            {
                ChangeFace(str);
                break;
            }
            else if (ichecked <= animations.Length)
            {
                ichecked++;
            }
            else
            {
                //str指定が間違っている時にはデフォルトで
                str = "default@unitychan";
                ChangeFace(str);
            }
        }
    }

    void ChangeFace(string str)
    {
        //isKeepFace = true;
        //current = 1;
        //anim.CrossFade(str, 0);
    }
}

次に基本的な機能の、ブレンドマスクを見てみます。
例えば、先ほどのアニメーションイベントにて呼ばれた関数で、顔の表情だけを別のアニメーションにしてみたいと思います。

ブレンドマスクにはこんな感じで、faceだけ適用する旨を設定します。
f:id:simplestar_tech:20170205153913j:plain
あとは Face という、もう一つのレイヤーを AnimationController に追加して、そこに表情のステートを追加します。遷移をいちいち書かないのがポイント。
f:id:simplestar_tech:20170205154006j:plain

アニメーションイベントで呼ばれる関数にて、CrossFade 関数を呼べば、遷移をかかずとも、そのステートに遷移します。

    public void OnCallChangeFace(string str)
    {
        int ichecked = 0;
        foreach (var animation in animations)
        {
            if (str == animation.name)
            {
                ChangeFace(str);
                break;
            }
            else if (ichecked <= animations.Length)
            {
                ichecked++;
            }
            else
            {
                //str指定が間違っている時にはデフォルトで
                str = "default@unitychan";
                ChangeFace(str);
            }
        }
    }

    void ChangeFace(string str)
    {
        anim.CrossFade(str, 0.4f);
    }

歩いているときに飛んでくるアニメーションイベントでスマイルし
f:id:simplestar_tech:20170205154159j:plain

走っているときに飛んでくるアニメーションイベントで驚きます。
f:id:simplestar_tech:20170205154242j:plain

gif アニメをとるとこんなイメージです。

f:id:simplestar_tech:20170205155112g:plain

mecanimの基本的な機能はこんなところでしょうか。
もっと便利な機能を見つけましたら、またどこかで書こうと思います。

ユニティちゃんライセンス

この作品はユニティちゃんライセンス条項の元に提供されています

Unity:楽曲のビートに合わせてオブジェクトを振動させる

ちょっとテンションが上がるBGMをかけながら作業していたところ
ふと、Audio の低音と同期して振動するオブジェクトができないかな?
なんて思いついたので、ちょっと調べて解決したので、解決方法を書いておきます。

情報ソースはこちら
docs.unity3d.com

using UnityEngine;

public class WaveOutputter : MonoBehaviour
{
    float[] _spectrum = new float[256];
    float _lastLow = 0;

    [Range(0, 1)]
    public float t = 1;

    void Start()
    {
    }

    void Update()
    {
        AudioListener.GetSpectrumData(_spectrum, 0, FFTWindow.Rectangular);
        float low = 0;
        for (int i = 1; i < 3; i++)
            low += _spectrum[i];

        low = _lastLow * (1 - t) + low * t;
        transform.localScale = Vector3.one * 0.05f * low + Vector3.one;
        _lastLow = low;
    }
}

3目ならべ(〇×ゲーム)で最強のAIを作る

表題の通り、今回は3目ならべで最強のAIを作りました。
最も勝利確率の高いマスを赤く示してくれるツールとなっています。

f:id:simplestar_tech:20170129120257j:plain

具体的にどうしたかというと
前回作った強化学習のコードの一部を次の通り、変更してみました。
変更の意図としてはBellman方程式のModelの部分にて、着手可能なすべての手に対して均等に着手する確率を割り振ってみたらどうなるか、というものです。

    private void LearnAction(Action action)
    {
        LearnState(action.state);
        action.reward = action.state.reward + 0.999f * GetSumReward(action.state);
    }

    private float GetSumReward(State state)
    {
        if (0 == state.actions.Count)
        {
            return 0;
        }
        float sumReward = 0;
        for (int i = 0; i < state.actions.Count; i++)
        {
            sumReward += state.actions[i].reward / state.actions.Count;
        }
        return sumReward;
    }

何故最強と言えるのか、それを確かめる作業をしてみましょう。
今回作った行動の報酬の通りに打つ先手をAIとして、すべての着手を調べてみます。
ここでAIは常に同じ状況で同じ手を返すため、すべての着手を調べることができます。

作った行動と状態の樹形図をたどるようにして、先手をAIにした場合の後手の人間があらゆる手を打ったとしてその後の勝敗を調べました。
その時の全数検査のコードがこちら

結果は、後手の人間が勝利した回数 = 0 引き分けた回数 = 4 負けた回数 = 72 となりました。
対局パターン数は76ということでした。

つまり、先手において最強のAI(少なくとも負けることがないAI)ができていることを確認できました。
今度は後手用のAIも作ってみましょう。
次回へ続きます。

    int _humanWinCount = 0;
    int _humanDrawCount = 0;
    int _humanLoseCount = 0;
    int _gamePatternCount = 0;

    void Start ()
    {
        _globalState = new State() { reward = 0 };
        _currentState = _globalState;
        PutTrue(_globalState);
        LearnState(_globalState);
        AIPut(_globalState);

        Debug.Log("Human win = " + _humanWinCount + " draw = " + _humanDrawCount + " lose = " + _humanLoseCount);
        Debug.Log("Pattern Count = " + _gamePatternCount);
    }

    private void AIPut(State state)
    {
        float minmax = 0;
        int actionOffset = -1;
        if (_mark)
        {
            minmax = float.MinValue;
            for (int i = 0; i < state.actions.Count; i++)
            {
                if (minmax < state.actions[i].reward)
                {
                    minmax = state.actions[i].reward;
                    actionOffset = i;
                }
            }
        }
        else
        {
            minmax = float.MaxValue;
            for (int i = 0; i < state.actions.Count; i++)
            {
                if (minmax > state.actions[i].reward)
                {
                    minmax = state.actions[i].reward;
                    actionOffset = i;
                }
            }
        }
        if (-1 != actionOffset)
        {
            State nextState = state.actions[actionOffset].state;
            _mark = !_mark;
            PutTest(nextState);
        }
    }

    private void PutTest(State state)
    {
        if (0 == state.actions.Count)
        {
            bool win = state.reward > 0;

            if (win)
                ++_humanLoseCount;
            else
                ++_humanDrawCount;
            ++_gamePatternCount;
        }
        for (int i = 0; i < state.actions.Count; i++)
        {
            State nextState = state.actions[i].state;
            _mark = !_mark;

            if(nextState.reward < 0)
            {
                ++_humanWinCount;
                ++_gamePatternCount;
            }
            else
                AIPut(nextState);
        }
    }

3目ならべで強化学習すると、どうなる?→こうなる

前回の記事ははじめての強化学習ということで、Bellman方程式を使ってきわめて単純な経路分岐問題を解いてみました。
今回はもう少し複雑な経路問題を解いてみたいと思います。

お題は「3目ならべ」です。

先攻後攻が決まったら、まずは 3x3 のマス目の 9つのうち、いずれかに先行が〇を描き
その後は互いに余ったマスに×と〇を描いていき、3つ先に並べた方が勝利するゲームです。

理論的な話はもう理解できているので
さっそく、全パターンを走査するプログラムを書いてみましょう。

ということで、書きました。

    bool?[] _masume = new bool?[9];

    int[][] _finish = new int[8][];

	void Start ()
    {
        for (int i = 0; i < _masume.Length; i++)
        {
            _masume[i] = null;
        }
        _finish[0] = new int[3] { 0, 1, 2 };
        _finish[1] = new int[3] { 3, 4, 5 };
        _finish[2] = new int[3] { 6, 7, 8 };

        _finish[3] = new int[3] { 0, 3, 6 };
        _finish[4] = new int[3] { 1, 4, 7 };
        _finish[5] = new int[3] { 2, 5, 8 };

        _finish[6] = new int[3] { 0, 4, 8 };
        _finish[7] = new int[3] { 2, 4, 6 };

        PutTrue();
    }

    private bool IsFinish(int[] finish, bool ex)
    {
        return (ex == _masume[finish[0]] && ex == _masume[finish[1]] && ex == _masume[finish[2]]);
    }

    private void PutTrue()
    {
        for (int i = 0; i < _masume.Length; i++)
        {
            if (null == _masume[i])
            {
                bool ex = true;
                _masume[i] = ex;
                bool win = false;
                for (int k = 0; k < _finish.Length; k++)
                {
                    if (IsFinish(_finish[k], ex))
                    {
                        win = true;
                        break;
                    }
                }
                if (!win)
                    PutFalse();
                _masume[i] = null;
            }
        }
    }

    private void PutFalse()
    {
        for (int j = 0; j < _masume.Length; j++)
        {
            if (null == _masume[j])
            {
                bool ex = false;
                _masume[j] = ex;
                bool win = false;
                for (int k = 0; k < _finish.Length; k++)
                {
                    if (IsFinish(_finish[k], ex))
                    {
                        win = true;
                        break;
                    }
                }
                if (!win)
                    PutTrue();
                _masume[j] = null;
            }
        }
    }

勝利判定付きです。

では、強化学習するための、State と Action の樹形図をこの再帰処理に構築してもらいましょう。
そんなコードを書いてみます。

はい、ということで書きました。
ちゃんと樹形図を作ってくれています。

f:id:simplestar_tech:20170122173848j:plain

    class Action
    {
        public int i = -1;
        public float reward = 0;
        public State state = null;
    }

    class State
    {
        public float reward = 0;
        public List<Action> actions = new List<Action>();
    }

    bool?[] _masume = new bool?[9];

    int[][] _finish = new int[8][];

    int _gameover = 0;

	void Start ()
    {
        for (int i = 0; i < _masume.Length; i++)
        {
            _masume[i] = null;
        }
        _finish[0] = new int[3] { 0, 1, 2 };
        _finish[1] = new int[3] { 3, 4, 5 };
        _finish[2] = new int[3] { 6, 7, 8 };

        _finish[3] = new int[3] { 0, 3, 6 };
        _finish[4] = new int[3] { 1, 4, 7 };
        _finish[5] = new int[3] { 2, 5, 8 };

        _finish[6] = new int[3] { 0, 4, 8 };
        _finish[7] = new int[3] { 2, 4, 6 };

        State state0 = new State() { reward = 0 };
        PutTrue(state0);
    }

    private bool IsFinish(int[] finish, bool ex)
    {
        bool isGameOver = (ex == _masume[finish[0]] && ex == _masume[finish[1]] && ex == _masume[finish[2]]);
        if (isGameOver)
            _gameover++;
        return isGameOver;
    }

    private void PutTrue(State state)
    {
        for (int i = 0; i < _masume.Length; i++)
        {
            if (null == _masume[i])
            {
                State newState = new State();
                state.actions.Add(new Action() { i = i, reward = 0, state = newState });
                bool ex = true;
                _masume[i] = ex;
                bool win = false;
                for (int k = 0; k < _finish.Length; k++)
                {
                    if (IsFinish(_finish[k], ex))
                    {
                        newState.reward = 1;
                        win = true;
                        break;
                    }
                }
                if (!win)
                    PutFalse(newState);
                _masume[i] = null;
            }
        }
    }

    private void PutFalse(State state)
    {
        for (int j = 0; j < _masume.Length; j++)
        {
            if (null == _masume[j])
            {
                State newState = new State();
                state.actions.Add(new Action() { i = j, reward = 0, state = newState });
                bool ex = false;
                _masume[j] = ex;
                bool win = false;
                for (int k = 0; k < _finish.Length; k++)
                {
                    if (IsFinish(_finish[k], ex))
                    {
                        newState.reward = -1;
                        win = true;
                        break;
                    }
                }
                if (!win)
                    PutTrue(newState);
                _masume[j] = null;
            }
        }
    }

ゲームオーバー数をざっと数えてみたところ21万回だったので21万の枝葉ができる樹形図となっている模様。
一応先攻だったときの報酬として、〇で勝てば+1、×で勝てば-1という形で State の reward を決めさせてもらいました。

あとは、この樹形図を使って学習するコードを書く必要がありますね。
書いてみます。

書きました。

    private void LearnState(State state)
    {
        for (int i = 0; i < state.actions.Count; i++)
        {
            Action action = state.actions[i];
            LearnAction(action);
        } 
    }

    private void LearnAction(Action action)
    {
        LearnState(action.state);
        action.reward = action.state.reward + 0.9f * GetMaxReward(action.state);
    }

    private float GetMaxReward(State state)
    {
        if (0 == state.actions.Count)
        {
            return 0;
        }
        float maxReward = float.MinValue;
        for (int i = 0; i < state.actions.Count; i++)
        {
            if (maxReward < state.actions[i].reward)
                maxReward = state.actions[i].reward;
        }
        return maxReward;
    }

学習結果を表示すると次の通りです。

f:id:simplestar_tech:20170129112556j:plain

期待どおりでしたか?

そう、実は相手の着手も自分が打てるという条件と等しい学習方法ですので
相手が最弱手を打つことを想定して行動の報酬を決定しています。
つまり何処に打つにしても、必ず5手で勝利するため、報酬は同じとなってしまいました。

ということで
3目ならべで強化学習すると、どうなる?→こうなる
を示しました。

ちゃんとしたAIにするには、いつか勉強した MinMax法 を使うのがよさそうです。
記事は次に続きます。

とっても簡単!強化学習の実践

強化学習とは?
それは、行動の結果得られる報酬が最も大きくなる行動を選択する仕組みにおいて
その報酬を計算して求めることです。

例えば「働けばお金がもらえる」という場合は「お金」が報酬となります。
報酬の設定のしかたは問題によって、また設定する人それぞれですので、デザイナーとしての腕の見せ所となるでしょう。

今回はお金を報酬にして、強化学習を行う非常に簡単なゲームを作ります。

まず最初に理論的な話を進めます。
お付き合いください。

最初に述べた、行動の結果得られる報酬が最も大きくなる行動を選択する仕組みとは、いったいどういったものなのでしょうか?

f:id:simplestar_tech:20170114185111j:plain

それは、上図の構成において、エージェントと呼ばれるキャラクターが行動Aか行動Bを報酬の大きい方を選んで行動する仕組みのことを指します。

ここで出てくる報酬Aと報酬Bを計算で求めることが強化学習です。

数学の記号を使わず報酬=~という形の計算式を立てると

報酬A=宝箱ゲット、お金いっぱい
報酬B=トラップによるダメージ、ケガしてお金もらえない

と表せます。
次に記号を使って表してみましょう。

f:id:simplestar_tech:20170114225045j:plain

U()で示した値は行動の後、状態S'a, 状態S'b, になったときに得られる金額の値を示しています。
つまり、行動によって得られる報酬です。
この値は今回のデザイナーである私が決めました。(とにかく宝箱側のスコアが高ければ良いという考えで設定しています。)

強化学習で出てくる式で、よく読者の理解を妨げるのが、ここでいう「報酬の大きい値を選択する」という処理まで計算式に含めるところです。
ではその式を見てみましょう。

f:id:simplestar_tech:20170115100038j:plain

πで表した値は戦略と呼ばれるもので、つまりは行動Aか行動Bいずれかを出力する関数を表します。
argmaxというのは報酬を最大にするactionを選ぶ操作を意味し、つまりは行動Aか行動Bいずれか報酬の大きいactionを選択するということを意味します。
戦略を決めるのは私たち人間です。
今回の私のデザインではactionにaが入ることになるでしょう。
エージェントとなるキャラクター性を示す戦略にすると、きっと愛着のあるAIが作れると思います。

ここまで見てきた計算式について、報酬は人の手によってあらかじめデザインされていますので、代入のみで完結しています。
では、もし報酬がデザインされていない行動が出てきた場合は、どのようにその報酬を設定すればよいのでしょうか?

次の図を見てください。

f:id:simplestar_tech:20170115102015j:plain

状態Sになる前に、状態S0が存在するとします。
状態S0から状態S0へと進むための行動Xの結果得られる報酬Xはどのようにして求めるか?という問題を今私たちは解こうとしています。

ここで、報酬X=~という数式を立ててみたいと思います。

報酬X=走る、疲れてお金もらえない

確かに、今までの報酬の決め方に従えばこれで完成なのですが、大半の読者は報酬設定が間違っていると指摘できると思います。
そう、状態Sは状態S0に比べてお金持ちになれる可能性が高くなっています。
この期待の高まりを式に反映させたのが次の式です。

報酬X=走る、疲れてお金もらえない + 宝箱ゲット、お金いっぱい or トラップによるダメージ、ケガしてお金もらえない

これが強化学習の説明で最初に出てくる方程式 Bellman equation の原始的な姿です。

そういえば

報酬A=宝箱ゲット、お金いっぱい
報酬B=トラップによるダメージ、ケガしてお金もらえない

と定義していましたので、この原始的な式は次の形で表せます。

報酬X=走る、疲れてお金もらえない + 報酬A or 報酬B

さて、戦略πを採用すると、必ず報酬の大きい方を選ぶことになりますから、今回の私のデザインに従えば、式は次の形になります。

報酬X=走る、疲れてお金もらえない + 報酬A

そろそろ記号を使った数式で報酬Xを表してみます。

f:id:simplestar_tech:20170115105156j:plain

U(S)は状態Sへ進むための行動の報酬Xを表しています。
R(S)は走る、疲れてお金もらえないという状態Sそのものから得られる報酬を表しています。今回の私のデザインであればR(S)=0です。
maxは最も大きな値を選択して返す関数を表しています。
U(S'action)は、報酬Aまたは報酬Bいずれかを示しています。

ここで、報酬Xに報酬Aが減衰することなくそのままの値として代入されてしまいますが、これで本当に良いのでしょうか?
たとえば行動Xを行っている間に、誰かが先に宝箱を開けてお金を持っていってしまっているかもしれません。

そこで、割引率を考慮しようという発想が生まれます。
次の式が広く知れわたっている Bellman equation の姿です。

f:id:simplestar_tech:20170115110909j:plain

より詳細を学んで、この式が Bellman equation の形と違うと指摘する人が出てくると思いますが
Bellman equation に出てくる Model は、今回の私のデザインでは 1 ですので、確率表記は省略されています。
その方が最初は理解しやすいのでそうしています。

私たちはここまで、「もし報酬がデザインされていない行動が出てきた場合は、どのようにその報酬を設定すればよいのでしょうか?」
という問題に対して考えてきました。
今ならば答えられます。

Bellman equation を使って求めればよいのです。

それでは強化学習を実践してみましょう。
実践編の最初の作業としては、行動の結果得られる報酬が最も大きくなる行動を選択する仕組みというものから作ってみましょう。

f:id:simplestar_tech:20170115115027j:plain

作りました。

実装の方も概念を共有した後、コードを示したいと思います。

学習過程にて変動する行動に付随する報酬と、固定された状態に設定される報酬があります。
状態には行動リストがあり、行動には結果の状態があります。
それをつなぎ合わせて、Bellman equation を解く式をプログラムすると次の通り

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public struct StateInfo
{
    public float reward;
    public ActionInfo[] actions;
}

public struct ActionInfo
{
    public float reward;
    public StateInfo targetState;
}

public class AgentBehaviour : MonoBehaviour {

    public float _gamma = 0.99f;
    public StateInfo[] _states = new StateInfo[4];

	void Start () {
        StateInfo state0 = new StateInfo();
        state0.reward = 0;
        state0.actions = new ActionInfo[1];

        StateInfo state = new StateInfo();
        state.reward = 0;
        state.actions = new ActionInfo[2];

        StateInfo stateA = new StateInfo();
        stateA.reward = 1000000;
        stateA.actions = null;

        StateInfo stateB = new StateInfo();
        stateB.reward = 0;
        stateB.actions = null;

        state0.actions[0].targetState = state;
        state.actions[0].targetState = stateA;
        state.actions[1].targetState = stateB;

        _states[0] = state0;
        _states[1] = state;
        _states[2] = stateA;
        _states[3] = stateB;

        int count = 3;
        while (0 < --count)
        {
            Learn();
        }
    }

    private void Learn()
    {
        for (int i = 0; i < _states.Length; i++)
        {
            LearnState(ref _states[i]);
        }
    }

    private void LearnState(ref StateInfo state)
    {
        if (null != state.actions)
        {
            for (int j = 0; j < state.actions.Length; j++)
            {
                LearnAction(ref state.actions[j]);
            }
        }
    }

    private void LearnAction(ref ActionInfo action)
    {
        action.reward = action.targetState.reward + _gamma * GetMaxReward(ref action.targetState);
    }

    private static float GetMaxReward(ref StateInfo targetState)
    {
        if (null == targetState.actions)
        {
            return 0;
        }

        float maxReward = 0;
        for (int i = 0; i < targetState.actions.Length; i++)
        {
            if (maxReward < targetState.actions[i].reward)
            {
                maxReward = targetState.actions[i].reward;
            }
        }
        return maxReward;
    }
}

繰り返し計算を2回すると、行動の報酬が収束することを確認しました。
これが、最も単純な強化学習の例だと思います。

あとは、この学習結果を参照して、最も大きな報酬の行動を選択して行動する仕組みを書けば、ゲームの完成です。
ゲームの実装はハンザツなので省略します。

ユニティちゃんライセンス

この作品はユニティちゃんライセンス条項の元に提供されています

強化学習の Value Iteration と Q-Learning の違い

強化学習には Value Iteration と Q-Learning の2通りの学習方法が存在します。

Value Iteration で登場する Model が既知か、未知かを見て
未知の場合は、試行錯誤を繰り返す Q-Learning しか選べません。

Value Iteration にて登場する報酬は人間のさじ加減で決めます。
行動と結果の組み合わせを示す Model もまた、人間のさじ加減で決めます。
Model の見当がつかない場合は Q-Learning に逃げます。
Q-Learning とは試行錯誤を続けて、ずっとずっと試行錯誤を続けて、いつの日か人間のさじ加減で決めた報酬が得られたなら、それまでの行動経路の評価値をちょっとだけ上げるという学習方法です。
このときの評価値のちょっとした上げ方も人間のさじ加減で決めます。

いくつか解説を見てきましたが、ハンザツなままとらえている人が多いですね。
私はこんな風に、すっきりととらえてみました。

間違えたら、その行動を避けるようになり
当たったら、それまでの行動を取りやすくなる
これなら確かに、経験から学ぶ人にそっくりな行動を取るのかもしれませんね。