【強化学習でAIを避難させる】#1 学習環境の実装  -Unity ML-Agents  

#0. はじめに
初めまして、Yuulisです。今年の3月ぐらいから、Unityの強化学習ライブラリである、[ML-Agents](https://github.com/Unity-Technologies/ml-agents)を使って強化学習をして遊んでいる高校生です。

Qiita初投稿ではありますが、ML-Agentsの日本語解説記事を増やすという目的も兼ねて、今回から強化学習でAIに避難行動を学習させていこうと思います。

#1. ML-Agents環境を用意する
ML-Agentsを使う上で環境構築は重要です。しかし、うまく書ける自信がないので、自分の中で分かりやすかったサイトを貼っておきます。

[Unity ML-Agentsのインストールから学習まで](https://www.suzu6.net/posts/270-isntall-unity-ml-agents/)

また、以下の書籍もかなり参考になりました。誤植が一部ありますが、気づける範囲です。これからML-Agentsを始めたい、という方には是非おすすめです。

[Unity ML-Agents 実践ゲームプログラミング v1.1対応版 (Unityではじめる機械学習・強化学習)](https://www.amazon.co.jp/Unity-ML-Agents-%E5%AE%9F%E8%B7%B5%E3%82%B2%E3%83%BC%E3%83%A0%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0-v1-1%E5%AF%BE%E5%BF%9C%E7%89%88-Unity%E3%81%A7%E3%81%AF%E3%81%98%E3%82%81%E3%82%8B%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92%E3%83%BB%E5%BC%B7%E5%8C%96%E5%AD%A6%E7%BF%92/dp/4862464823)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285757/3de1bdb7-a1d6-3be8-7480-7d98bf4859dc.png)


※ML-Agentsの最新バージョンは、2021/05/04時点でRelease 17となっていますが、私の環境はUnity 2019.4.23f1, ML-Agents Release 12です。

**追記 (2021/08/29)** ML-Agentsパッケージが2.0.0に更新されました。

https://qiita.com/Yuulis/items/98c5e8d2a34a32dbd60d


#2. 今回作るもの
初回の今回は、4つの非常口の中から一番近いものに避難するAIを作ることを目標とします。
![スクリーンショット 2021-05-05 171927.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285757/042083d1-0d94-6cd1-a40a-9fa8664d617a.png)


##2-1. 学習環境全般
長方形のエリアの四隅に、非常口を設置します。なお、エリアには壁は存在せず、下に落ちると失敗です。

##2-2. エージェントの設定
####環境の観察
環境の観察は、Raycast Observationという方法を使います。エージェントの周囲からレーザーを発射し、そのレーザーに当たったものを検知します。

####行動
行動を記録するデータ型は、Discreteというものを使います。これは、離散値(0, 1, 2...)の要素を持つ整数配列です。
今回は、1回の行動の配列の長さは1で、配列1つの要素に入り得る要素の数は次の5つです。

| 数値 | 行動 |
|:-:|:-:|
|0   |移動しない   |
|1   |前   |
|2   |後ろ   |
|3   |右回転   |
|4   |左回転   |

※例えば、`[1],[3],[2]`というデータがあれば、「前、右回転、後ろ」を意味します。

####報酬
生成時に最も近かった非常口に触れれば+1.0。それ以外の場合は+0.3。
エリアから落下したら-0.2。
1行動ごとに-1.0 / MaxStep

※MaxStepというのは、1回の学習で行動できる回数のことです。例えばMaxStep = 5000ならば、エージェントは5000回行動できます。

#3. 実装
##3-1. ML-AgentsライブラリをUnityに追加
では、いよいよ実装していきます。まずは、ML-AgentsライブラリをUnityに追加します。
Unityの新しいプロジェクト「EvacuationLearning」を作成したら、「ウィンドウ」から「Package Maneger」を選択してください。
![packageManeger.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285757/6ea843d7-cba1-63bd-cf75-2a3ec891a1d4.png)
Package Manegerの読み込みが終わったら（少し時間がかかる）、左上のボタンから「Add package from disk...」を選択してください。
![addPackageFromDisk.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285757/619e6cef-793e-7faf-ad07-d21c2b7498d0.png)
[1. ML-Agents環境を用意する](https://qiita.com/drafts#1-ml-agents%E7%92%B0%E5%A2%83%E3%82%92%E7%94%A8%E6%84%8F%E3%81%99%E3%82%8B) でクローン（ダウンロード）したML-Agents toolkitのディレクトリに移動し、`com.unity.ml-agents/package.json`と`com.unity.ml-agents.extensions/package.json`を追加します。
![スクリーンショット 2021-05-05 144724.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285757/2ffa37b2-16d0-09b0-2d13-eec237bcc10b.png)
パッケージを追加したら、今度は「ファイル」から「ビルド設定」を選択し、左下の「プレイヤー設定」を選択します。
![スクリーンショット 2021-05-05 144958.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285757/14d5ed69-e0ae-53a0-b91e-3e336f3fd25c.png)
下にスクロールして、「API 互換性レベル」を「.NET 4.x」に変更します。
![スクリーンショット 2021-05-05 145135.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285757/43e2018e-529f-876f-752b-71f5ce917a6f.png)
以上でML-AgentsライブラリをUnityに追加することができました。

##3-2. 学習環境の作成
次に、エージェントが学習する環境を作成します。
以下のオブジェクトを追加し、ヒエラルキーの「Transform」タブを以下のように設定してください。
Areaは「Plane」、その他は「Cube」のオブジェクトを使います。

| オブジェクト名 | 座標 | 角度 | サイズ |
|:-:|:-:|:-:|:-:|
| Area  | (0, 0, 0)  | (0, 0, 0)  | (1.5, 1, 3)  |
| Agent  | (0, 0.5, 0)  | (0, 0, 0)  | (1, 1, 1)  |
| Target1  | (-6.5, 0.5, -12.5)  | (0, 0, 0)  | (2, 1, 5)  |
| Target2  | (6.5, 0.5, -12.5)  | (0, 0, 0)  | (2, 1, 5)  |
| Target3  | (6.5, 0.5, 12.5)  | (0, 0, 0)  | (2, 1, 5)  |
| Target4  | (-6.5, 0.5, 12.5)  | (0, 0, 0)  | (2, 1, 5)  |

「Agent」には「Rigidbody」を追加します。以下のように設定してください。
![スクリーンショット 2021-05-05 150855.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285757/25e91312-a74d-b79e-b3e0-57277d050edb.png)
さらに、Agentの向いている方向が分かるよう、顔のパーツも追加します。この辺はお好みです。私は以下のように設定しました。
目は「Cylinder」、口は「Cube」です。

| オブジェクト名 | 座標 | 回転 | サイズ |
|:-:|:-:|:-:|:-:|
| Eye  | (0.25, 0.2, 0.35)  | (90, 0, 0)  | (0.2, 0.2, 0.2)  |
| Eye(1)  | (-0.25, 0.2, 0.35)  | (90, 0, 0)  | (0.2, 0.2, 0.2)  |
| Mouth  | (0, -0.2, 0.5)  | (0, 0, 90)  | (0.1, 0.5, 0.1)  |

これらのオブジェクトは「Agent」の子オブジェクトにしてください。

次はそれぞれにMaterialを付けます。この辺もお好みです。別にMaterial用のディレクトリを作っておくとよいでしょう。
ちなみに私は[Blockout](https://assetstore.unity.com/packages/tools/level-design/blockout-100388?locale=ja-JP)という無料アセットを使っています。アセットストアからダウンロードできます。
エージェント本体には赤色のマテリアル、エージェントの顔には黒色のマテリアルを適用しました。
それ以外のオブジェクトにはBlockoutのマテリアルを適用。

最後に、「TrainingArea」という空のオブジェクトを追加し、すべてのオブジェクトを子オブジェクト化します。
![スクリーンショット 2021-05-05 152344.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285757/ab810877-32f3-6ef6-fa87-a32e573b5eae.png)
これで、「TrainingArea」を複製することで並行学習が簡単になりました。
以上で学習環境の作成が完了です。

##3-3. 学習のためのスクリプトを作成
今度は、「Agent」に強化学習のスクリプトを適用します。
その前に、今後のために、「Target」にtagを付けておきます。
インスペクターウィンドウの「タグ」を選択し、「タグを追加...」をクリック（画像ではすでに「target」と「obstacle」というタグができていますが無視して結構です）。
![スクリーンショット 2021-05-05 152849.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285757/0c65d615-025b-27eb-58af-49b52d9fa090.png)
以下のような画面になるので、「タグ」の右下の+ボタンをクリックし、新しいタグの名前「target」を入力して「Save」。
![スクリーンショット 2021-05-05 153048.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285757/9b819150-c988-2b8b-efbc-82eba8631d91.png)
Target1~4のそれぞれに「target」タグをつけます。
![スクリーンショット 2021-05-05 153343.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285757/dbef8e38-7cff-1a1b-3a77-264dfd57df5b.png)

では、スクリプトを書いていきましょう。
エージェントのインスペクターウィンドウの「コンポーネントを追加」から、新しいスクリプト「AgentControl.cs」を作成。
以下のように書き換えます。

```AgentControl.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// ML-Agentsパッケージの追加
using Unity.MLAgents;
using Unity.MLAgents.Sensors;
using Unity.MLAgents.Actuators;

public class AgentControl : Agent  // 基底クラスをAgentに変更
{
    public Transform target1;
    public Transform target2;
    public Transform target3;
    public Transform target4;
    Rigidbody rBody;
    private bool onEpisode = false;  // エピソード実行中かどうか
    private string closestTarget;  // 生成時に最も近かったターゲット名を格納

    // 初期化時
    public override void Initialize()
    {
        this.rBody = GetComponent<Rigidbody>();
        onEpisode = false;
    }

    // エピソード開始時
    public override void OnEpisodeBegin()
    {
        // エージェントの状態をリセット
        this.rBody.angularVelocity = Vector3.zero;
        this.rBody.velocity = Vector3.zero;
        this.transform.rotation = Quaternion.identity;

        // エリアのランダムな位置に生成
        this.transform.localPosition = new Vector3(Random.Range(-6.5f, 6.5f), 0.5f, Random.Range(-14.0f, 14.0f));

        // エピソード開始
        onEpisode = true;

        // スポーン時の最近のターゲットを取得
        float dis1 = Vector3.Distance(this.transform.localPosition, target1.localPosition);
        float dis2 = Vector3.Distance(this.transform.localPosition, target2.localPosition);
        float dis3 = Vector3.Distance(this.transform.localPosition, target3.localPosition);
        float dis4 = Vector3.Distance(this.transform.localPosition, target4.localPosition);

        float leastDis = Mathf.Min(dis1, dis2, dis3, dis4);

        if (leastDis == dis1) closestTarget = "Target1";
        if (leastDis == dis2) closestTarget = "Target2";
        if (leastDis == dis3) closestTarget = "Target3";
        if (leastDis == dis4) closestTarget = "Target4";
    }

    // 行動実行時  size = 5
    public override void OnActionReceived(float[] vectorAction)
    {
        Vector3 dirToGo = Vector3.zero;
        Vector3 rotateDir = Vector3.zero;

        // 離散値ごとの行動設定
        int action = (int)vectorAction[0];
        if (action == 1) dirToGo = transform.forward;  // 前
        if (action == 2) dirToGo = transform.forward * -1.0f;  // 後ろ
        if (action == 3) rotateDir = transform.up;  // 右
        if (action == 4) rotateDir = transform.up * -1.0f;  // 左

        this.transform.Rotate(rotateDir, Time.deltaTime * 200f);
        this.rBody.AddForce(dirToGo * 0.6f, ForceMode.VelocityChange);

        // エリアから落下したとき
        if (this.transform.localPosition.y < 0)
        {
            AddReward(-0.3f);  // 罰
            EndEpisode();
        }

        // ステップごとの罰
        AddReward(-1.0f / MaxStep);
    }

    // 目標にぶつかったとき
    public void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.tag == "target")
        {
            // エピソード中なら
            if (onEpisode)
            {
                // 生成時に最も近かったターゲットなら
                if (collision.gameObject.name == closestTarget)
                {
                    AddReward(1.0f);  // 最大報酬
                    EndEpisode();
                }
                
                // それ以外なら
                else
                {
                    AddReward(0.3f);  // 小さな報酬
                    EndEpisode();
                }

            }

            // エピソード中でなければ
            else
            {
                // ランダムな位置に生成のやり直し
                this.transform.localPosition = new Vector3(Random.Range(-6.5f, 6.5f), 0.5f, Random.Range(-14.0f, 14.0f));
            }
        }
    }

    // 人間が操作するとき
    public override void Heuristic(float[] actionsOut)
    {
        actionsOut[0] = 0;
        if (Input.GetKey(KeyCode.UpArrow)) actionsOut[0] = 1;
        if (Input.GetKey(KeyCode.DownArrow)) actionsOut[0] = 2;
        if (Input.GetKey(KeyCode.RightArrow)) actionsOut[0] = 3;
        if (Input.GetKey(KeyCode.LeftArrow)) actionsOut[0] = 4;
    }
}
```
大まかな説明はスクリプト内にコメントしておきました。

では、Agentのインスペクターウィンドウに戻り、設定を行っていきます。
まずは先ほど書いたスクリプトの設定。
Target1~4には、[3-2. 学習環境の作成](https://qiita.com/drafts#3-2-%E5%AD%A6%E7%BF%92%E7%92%B0%E5%A2%83%E3%81%AE%E4%BD%9C%E6%88%90) で作ったTargetをドラッグアンドドロップ。
![スクリーンショット 2021-05-05 155029.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285757/b4d68bef-6548-d4d7-624d-cbb8484c2dac.png)

次に、「Behavior Parameters」という、エージェントの行動にかかわるスクリプトを追加。すでにある場合もありますが、追加されてなければ「コンポーネントを追加」から追加できます。
以下のように設定。
今回はRayCast Observationを使うので、「Vector Observation」の「Space Size」は0にセット。
また、「Discrete Branches」は[2-2. エージェントの設定](https://qiita.com/drafts#%E8%A1%8C%E5%8B%95) の行動の欄を参考に。
![スクリーンショット 2021-05-05 155933.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285757/cbff03cd-915f-1a80-7d87-277f911045d2.png)

最後に、エージェントの観察をするためのセンサーを追加します。
「コンポーネントを追加」から、「Ray Perception Sensor 3D」を追加し、以下のように設定します。
![スクリーンショット 2021-05-05 160939.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285757/4b08282f-ea0a-5075-9c5d-0e74773835ee.png)
今回は、エージェントの周囲360°から16方向にセンサーを発射します。

そして、学習を効率化させるために、並行学習をします。「TrainingArea」を9個ほど複製し、互いの学習環境に影響を与えないよう、四方50ぐらい間をあけます。
![スクリーンショット 2021-05-05 161406.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285757/2c8fa001-ad7b-a574-b7e3-8275fdee7de3.png)
こんな感じ。

以上で学習環境の実装は終了です。

#4. 強化学習をやってみる
強化学習を始める前に、学習用パラメータの設定をします。
`EvacuationLearning\config\base.yaml`を新しく作成します。
以下のようなコードを書きます。

```base.yaml
behaviors:
  AgentManeger:
    trainer_type: ppo
    hyperparameters:
      batch_size: 128
      buffer_size: 2048
      learning_rate: 0.0003
      beta: 0.005
      epsilon: 0.2
      lambd: 0.95
      num_epoch: 3
      learning_rate_schedule: linear
    network_settings:
      normalize: true
      hidden_units: 256
      num_layers: 3
      vis_encode_type: simple
    reward_signals:
      extrinsic:
        gamma: 0.95
        strength: 1.0
    max_steps: 1000000
    keep_checkpoints: 5
    checkpoint_interval: 200000
    time_horizon: 64
    summary_freq: 10000
    threaded: true
```
ここの詳しい説明は書籍にも載っていますが、次回説明しようと思います。

いよいよ学習です！
Anaconda Promptを起動し、以下のコマンドを順に実行します。

1. 仮想環境の起動 
`conda activate (作成した仮想環境名)`
※`(仮想環境名) >>>` となれば成功。

2. プロジェクトのディレクトリに移動
`cd (プロジェクトのディレクトリのパス)`

3. 学習開始コマンド
`mlagents-learn .\config\base.yaml --run-id=1st`
`1st`の部分は重複は許されません。学習ごとに変える必要があります。

※こんな感じのアスキートアートと、`Listening on port (ランダムな値). Start training by pressing the Play button in the Unity Editor.`
というコードが出たら成功。

```

                        ▄▄▄▓▓▓▓
                   ╓▓▓▓▓▓▓█▓▓▓▓▓
              ,▄▄▄m▀▀▀'  ,▓▓▓▀▓▓▄                           ▓▓▓  ▓▓▌
            ▄▓▓▓▀'      ▄▓▓▀  ▓▓▓      ▄▄     ▄▄ ,▄▄ ▄▄▄▄   ,▄▄ ▄▓▓▌▄ ▄▄▄    ,▄▄
          ▄▓▓▓▀        ▄▓▓▀   ▐▓▓▌     ▓▓▌   ▐▓▓ ▐▓▓▓▀▀▀▓▓▌ ▓▓▓ ▀▓▓▌▀ ^▓▓▌  ╒▓▓▌
        ▄▓▓▓▓▓▄▄▄▄▄▄▄▄▓▓▓      ▓▀      ▓▓▌   ▐▓▓ ▐▓▓    ▓▓▓ ▓▓▓  ▓▓▌   ▐▓▓▄ ▓▓▌
        ▀▓▓▓▓▀▀▀▀▀▀▀▀▀▀▓▓▄     ▓▓      ▓▓▌   ▐▓▓ ▐▓▓    ▓▓▓ ▓▓▓  ▓▓▌    ▐▓▓▐▓▓
          ^█▓▓▓        ▀▓▓▄   ▐▓▓▌     ▓▓▓▓▄▓▓▓▓ ▐▓▓    ▓▓▓ ▓▓▓  ▓▓▓▄    ▓▓▓▓`
            '▀▓▓▓▄      ^▓▓▓  ▓▓▓       └▀▀▀▀ ▀▀ ^▀▀    `▀▀ `▀▀   '▀▀    ▐▓▓▌
               ▀▀▀▀▓▄▄▄   ▓▓▓▓▓▓,                                      ▓▓▓▓▀
                   `▀█▓▓▓▓▓▓▓▓▓▌
                        ¬`▀▀▀█▓


 Version information:
  ml-agents: 0.23.0,
  ml-agents-envs: 0.23.0,
  Communicator API: 1.3.0,
  PyTorch: 1.7.0+cu110
2021-05-05 12:47:13 INFO [learn.py:275] run_seed set to 7390
2021-05-05 12:47:14 INFO [environment.py:205] Listening on port 5004. Start training by pressing the Play button in the Unity Editor.
```

このコードが出たら、すぐにUnityの画面に戻って「Play」ボタンを押してください。ある程度時間が経過すると、エラーになって学習開始コマンドの打ち直しになります。

学習中は`2021-05-05 12:47:48 INFO [stats.py:139] AgentManeger. Step: 10000. Time Elapsed: 35.025 s. Mean Reward: -0.017. Std of Reward: 0.632. Training.`
のようなログがプロンプトに表示されます。
今回は100万Step学習することになっているので、1時間程度かかります。

ちなみに、`Mean Reward`はStepごとの報酬平均、`Std of Reward`は標準偏差を表しています。
今回の場合では、前者が1に近づき、後者の値が0.4以下ぐらいになれば成功だといえるでしょう。

私のケースでは、70万Stepあたりで変化がほぼなくなったので、学習を終了しました。

#5. 学習結果の確認
学習が終わったら、結果を確認しましょう。
`EvacuationLearing\results\1st`に、`AgentManeger.onnx`というファイルができているはずです。
それをUnityのプロジェクトウィンドウにドラッグアンドドロップ。
さらにそれを、「Agent」のヒエラルキーウィンドウの「Behavior Parameters」の「モデル」の部分にアタッチします。
![スクリーンショット 2021-05-05 170108.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285757/0c780b2a-1fa4-8a46-249d-020ad8b53713.png)

「Play」ボタンを押してみましょう。こんな感じに動きましたか？（さらっとTwitterの宣伝）

https://twitter.com/Yuulis04/status/1389589140996431879?s=20

報酬グラフや詳しい情報は、以下のコマンドをAnaconda Promptで実行すると得られます。
`tensorboard --logdir results`

すると、localhostのリンクが出てくるので、コピペしてアクセス。
![スクリーンショット 2021-05-05 171037.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285757/58d735e6-70a1-2eb4-6ec3-f412cdcace22.png)

うん、うまい具合に学習できました。

#6. 終わりに
いかがでしたでしょうか。
強化学習に興味を持っていろいろ調べた結果、UnityのML-Agentsを発見したのですが、やはり解説が少ないです（あったとしてもほぼ同じ内容）。これからこの分野を開拓していきたいと思っています。
初投稿ということもあり、執筆に時間がかかってしまいました。誤字や不明な点があれば、ご指摘ください。
また、この記事を読んで、少しでも強化学習に興味を持っていただけたら幸いです。


ありがとうございました。