【強化学習でAIを避難させる】#3 AIが垂直避難する  -Unity ML-Agents

#0. はじめに
どうも、Yuulisです。今回は、強化学習でAIを垂直避難させるモデルを作成したので、それについてのお話。
今までの記事はこちら。初めての方はこちらを先に読むことをお勧めします。

https://qiita.com/Yuulis/items/0d9ea77bc47a73275f87

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


#1. 垂直避難とは?
そもそも、垂直避難とはどういう避難の形態なのでしょうか。
[内閣府の資料](http://www.bousai.go.jp/kaigirep/chousakai/saigaijihinan/3/pdf/shiryou_2.pdf)によると、「急激な降雨や浸水により**屋外での歩行等が危険な状態になった場合**は、浸水による建物の倒壊の危険がない場合には、自宅を立ち退き避難所へ避難することが必ずしも適切な行動ではなく、**自宅や隣接建物の2 階等へ緊急的に一時避難**し、救助を待つことも選択肢として考えられるべきである。」とあります。
つまり垂直避難を行うときというのは、**すでに屋外が危険な状態になっており、外に出て避難することが難しいときの緊急手段**ということです。

これを前提に、今回はこんな感じの学習環境を作っていきます。
![スクリーンショット 2021-06-22 231034.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285757/7231fb3f-8697-3e83-6900-f85c363dfede.png)
すでに床下まで水が迫ってきている状況ですね。

#2. 学習環境の準備
##2-1. 学習条件の整理
学習環境を作る前に、まず条件の整理をします。これをやっておかないと比較対照を行うときに大変なので、とても大切です。
###2-1-1. 環境の観察
今回は、周囲の環境の観察に、
・Vector Observation (オブジェクトの情報を数値で取得)
・Raycast Observation (レーザーでオブジェクトを検知)
の二通りの方法を組み合わせます。

###2-1-2. エージェントの行動
行動を記録するデータ型は、Discreteを使います。今回は、actという長さ3の配列に、次の3つの情報を変数で格納します。

```
act[0] = dirToGoForwardAction  // 前と後ろ
act[1] = dirToGoSideAction  // 右と左
act[2] = rotateDirAction  // 右回転と左回転
```

それぞれの変数には、次の値が格納されます。

| 変数 | 値 | 行動 |
|:-:|:-:|:-:|
| dirToGoForwardAction  | 0  | 動かない  |
|   | 1  | 前進  |
|   | 2  | 後進  |
| dirToGoSideAction  | 0  | 動かない  |
|   | 1  | 右  |
|   | 2  | 左  |
| rotateDirAction  | 0  | 動かない  |
|   | 1  | 右回転  |
|   | 2  | 左回転  |

例: ``[1, 0, 2], [2, 1, 0]``は、「前進して右に回転」の後、「後進して右に移動する」という行動になります。

また、エージェントは階段を使用して上下の階に移動できます。

###2-1-3. 学習環境平面図
**1F** (サイズ:　40*30 他の階も同様)
![スクリーンショット 2021-06-23 223112.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285757/f149717f-42fa-2213-ae3a-29882db32507.png)
**2F**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285757/27da0165-dc20-b4f4-936e-6437a0f34cfc.png)
**3F**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285757/6d1c564d-3e33-f22d-8856-cc3f6481d2c4.png)
学習環境の作成にはUnityのパッケージにあるProBuilderを使用しました。

###2-1-4. 報酬
**+の報酬**
・1回の行動ごと: (水とのy座標の差の10分の1 + 現在の階層 * 0.3) / MaxStep
※ここでいうMaxStepは、後述のパラメータ設定で設定するものです。正直ここの報酬は適当。
・上り階段を使用したとき: +0.3

**-の報酬**
・水の高さよりもエージェントのy座標が小さいとき: -1.0
・壁や障害物に触れたとき: -0.7
・下り階段を使用したとき: -0.5

##2-2. 強化学習プロジェクトを作成
[ここ](https://qiita.com/Yuulis/items/98c5e8d2a34a32dbd60d#4-unity%E3%81%ABmlagents%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8%E3%82%92%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)を参考に進めました。

#3. 実装
##3-1. 学習環境の作成
学習環境をPrefab化したものがこちら。
![スクリーンショット 2021-06-23 230923.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285757/3704c3a6-24b1-6d2a-7b07-c4186b883874.png)![スクリーンショット 2021-06-23 230951.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285757/12c8183f-15c4-14d2-af63-c9ed3e074a83.png)
詳細なものについては割愛します。[2-3. 学習環境平面図](https://qiita.com/drafts/0c521eadcd1364c1efd1/edit#2-1-3-%E5%AD%A6%E7%BF%92%E7%92%B0%E5%A2%83%E5%B9%B3%E9%9D%A2%E5%9B%B3)を参考にしてみてください。
ここでは、上昇する水のシェーダーを紹介します。今回使用したのはこちらのアセット。
[Unity-WaterBuoyancy](https://github.com/dbrizov/NaughtyWaterBuoyancy)
こちらは、水のシェーダーとともに実際に浮力の計算も行ってくれるすぐれもの。
Githubからリポジトリをクローン(ダウンロード)して、Unityのプロジェクトタブに入れるだけで使用可能です。

##3-2. スクリプト
今回書いたのはこのスクリプトだけ。Agentオブジェクトにアタッチします。
<details><summary>スクリプトはこちら</summary><div>

```C#:AgentControl_3Floor_Lv1_3.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.MLAgents;
using Unity.MLAgents.Sensors;
using Unity.MLAgents.Actuators;

public class AgentControl_3Floor_Lv1_3 : Agent
{
    // General
    private bool onEpisode;

    // Agent
    Rigidbody rBody;
    private int nowFloor;
    public float AgentSpeed = 0.5f;

    // Water
    public Transform Water;

    // At Initializing
    public override void Initialize()
    {
        this.rBody = GetComponent<Rigidbody>();
    }

    // Agent Spawn
    public void SpawnAgent()
    {
        this.transform.localPosition = new Vector3(Random.Range(-19.5f, 19.5f), 1.5f, Random.Range(-14.5f, 14.5f));
    }

    // When Episode begins
    public override void OnEpisodeBegin()
    {
        onEpisode = false;

        // Reset Agent's status
        this.rBody.angularVelocity = Vector3.zero;
        this.rBody.velocity = Vector3.zero;
        this.transform.rotation = Quaternion.identity;

        SpawnAgent();
        nowFloor = 1;

        Water.localPosition = new Vector3(-22.5f, -10.0f, -17.5f);

        onEpisode = true;
    }

    // Get observations (size = 4)
    public override void CollectObservations(VectorSensor sensor)
    {
        sensor.AddObservation(this.transform.localPosition);  // Agent(x, y, z)
        sensor.AddObservation(Water.localPosition.y);  // Water(y)
    }

    // When Agent moves (size = 3)
    public void MoveAgent(ActionSegment<int> act)
    {
        var dirToGo = Vector3.zero;
        var rotateDir = Vector3.zero;
        var dirToGoForwardAction = act[0];
        var dirToGoSideAction = act[1];
        var rotateDirAction = act[2];

        if (dirToGoForwardAction == 1) dirToGo = 1.0f * this.transform.forward;  // Forward
        else if (dirToGoForwardAction == 2) dirToGo = -1.0f * this.transform.forward;  // Back

        if (dirToGoSideAction == 1) dirToGo = 1.0f * this.transform.right;  // Right
        else if (dirToGoSideAction == 2) dirToGo = -1.0f * this.transform.right;  // Left

        if (rotateDirAction == 1) rotateDir = 1.0f * this.transform.up;  // Rotate right
        else if (rotateDirAction == 2) rotateDir = -1.0f * this.transform.up;  // Rotate left

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

    // Agent's action
    public override void OnActionReceived(ActionBuffers actions)
    {
        MoveAgent(actions.DiscreteActions);
        if (Water.localPosition.y < 14.0f) Water.Translate(0f, 0.01f, 0f);

        // When Water comes
        if (this.transform.localPosition.y < Water.localPosition.y)
        {
            AddReward(-1.0f);
            EndEpisode();
        }

        // Punish step by step
        float DistanceToWater = this.transform.localPosition.y - Water.localPosition.y;
        AddReward(((DistanceToWater / 10f) + (0.3f * nowFloor)) / MaxStep);
    }

    // Human Controling
    public override void Heuristic(in ActionBuffers actionsOut)
    {
        var discreteActionsOut = actionsOut.DiscreteActions;

        if (Input.GetKey(KeyCode.W)) discreteActionsOut[0] = 1;  // Forward
        if (Input.GetKey(KeyCode.S)) discreteActionsOut[0] = 2;  // Back
        if (Input.GetKey(KeyCode.D)) discreteActionsOut[2] = 1;  // Rotate right
        if (Input.GetKey(KeyCode.A)) discreteActionsOut[2] = 2;  // Rotate left
    }

    // When touching something
    void OnCollisionEnter(Collision col)
    {
        // During Episode?
        if (onEpisode)
        {
            // Wall or Obstacle
            if (col.gameObject.CompareTag("Wall") || col.gameObject.CompareTag("Obstacle"))
            {
                AddReward(-0.7f);
                EndEpisode();
            }

            // UpStair
            else if (col.gameObject.CompareTag("UpStair"))
            {
                this.rBody.angularVelocity = Vector3.zero;
                this.rBody.velocity = Vector3.zero;
                this.transform.rotation = Quaternion.identity;

                if (this.transform.localPosition.x > 0 && this.transform.localPosition.z > 0) { this.transform.Translate(-1.0f, 7.0f, 4.0f); this.transform.Rotate(0f, -90.0f, 0f); }
                else if (this.transform.localPosition.x < 0 && this.transform.localPosition.z > 0) { this.transform.Translate(1.0f, 7.0f, -4.0f); this.transform.Rotate(0f, 90.0f, 0f); }
                else if (this.transform.localPosition.x < 0 && this.transform.localPosition.z < 0) { this.transform.Translate(1.0f, 7.0f, -4.0f); this.transform.Rotate(0f, 90.0f, 0f); }
                else if (this.transform.localPosition.x > 0 && this.transform.localPosition.z < 0) { this.transform.Translate(-1.0f, 7.0f, 4.0f); this.transform.Rotate(0f, -90.0f, 0f); }

                nowFloor++;
                AddReward(0.3f);
            }

            // DownStair
            else if (col.gameObject.CompareTag("DownStair"))
            {
                nowFloor--;

                this.rBody.angularVelocity = Vector3.zero;
                this.rBody.velocity = Vector3.zero;
                this.transform.rotation = Quaternion.identity;

                if (this.transform.localPosition.x > 0 && this.transform.localPosition.z > 0) { this.transform.Translate(-1.0f, -7.0f, -4.0f); this.transform.Rotate(0f, -90.0f, 0f); }
                else if (this.transform.localPosition.x < 0 && this.transform.localPosition.z > 0) { this.transform.Translate(1.0f, -7.0f, 4.0f); this.transform.Rotate(0f, 90.0f, 0f); }
                else if (this.transform.localPosition.x < 0 && this.transform.localPosition.z < 0) { this.transform.Translate(1.0f, -7.0f, 4.0f); this.transform.Rotate(0f, 90.0f, 0f); }
                else if (this.transform.localPosition.x > 0 && this.transform.localPosition.z < 0) { this.transform.Translate(-1.0f, -7.0f, -4.0f); this.transform.Rotate(0f, -90.0f, 0f); }

                AddReward(-0.5f);
            }
        }

        else
        {
            SpawnAgent();
        }
    }
}
```

</div></details>

学習用パラメータはこんな感じ。

<details><summary>学習用パラメータ</summary><div>

```EL_3Floor_Lv1_3.yaml
behaviors:
  3Floor_Lv1_3:
    trainer_type: ppo
    hyperparameters:
      batch_size: 128
      buffer_size: 2048
      learning_rate: 0.0003
      beta: 0.01
      epsilon: 0.2
      lambd: 0.95
      num_epoch: 3
      learning_rate_schedule: linear
    network_settings:
      normalize: true
      hidden_units: 512
      num_layers: 3
      vis_encode_type: simple
    reward_signals:
      extrinsic:
        gamma: 0.95
        strength: 1.0
      curiosity:
        gamma: 0.99
        strength: 0.05 
    max_steps: 5000000
    keep_checkpoints: 100
    checkpoint_interval: 200000
    time_horizon: 64
    summary_freq: 10000
    threaded: true

```

</div></details>
今回は、curiosityという、AIに好奇心を持たせる項目を追加しました。こうすることで、学習の効率を上げることができます。

#4. 学習結果
今回は、300万step学習させました。こんな感じで動きます。

https://youtu.be/COQWgpnAP58

報酬グラフはこんな感じ。
![スクリーンショット 2021-06-25 132325.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1285757/4b833733-c8ee-2f39-b8cc-31a699ef826e.png)
右肩上がりの増加傾向にあり、獲得報酬は1を超えています。おおむね成功といったところでしょうか。

#5. 終わりに
今回は、強化学習で垂直避難を学習させてみました。
次回、エージェントの動きや学習環境の構成をもう少し見直してみるつもりです。

_2021/07/03追記:_

https://qiita.com/Yuulis/items/10516f5e1b58041b03d7