物理エンジンで機械学習① 物体追跡　2021年最新版

# はじめに

こんにちは。陰キャ大学生です。

前置きはさておき、Pythonを使って機械学習していると、「物理エンジン使ってやってみたいなぁ」なんて思う時があります。~~ありません~~
そこで今回は、Unityのために作られた機械学習ライブラリ__ML Agents__ を使って物理エンジンで機械学習してみました。
実はすでにQiita上には多くのMLAgentsの記事が上がっています。

[【ML-Agents】UnityとPythonのTensorFlowをつかって機械学習させてみた(v0.11β対応)](https://qiita.com/maruda/items/687f068d9d356f16dd90)

しかし、このMLAgents、__バージョンアップが頻繁なうえに、バージョン変わると関数やメソッド名も変わる__というなんともQiitaライター泣かせな仕様となっています。

そこで今回は、__現在(2021年2月25日)最新であるRelease13__ について記事を書きます。
おそらく数か月後には古くて使い物にならない記事になると思いますが、今この瞬間にMLAgentsを使いたいんだ！！という人に届いてほしいと思います。

#MLAgentsについて

こちらの記事が大変わかりやすく参考になりますので、ぜひご覧ください

[Unityで体感する強化学習【ML-Agents】](https://qiita.com/KazukiKawamura/items/e1d60ba2481c9b48b0a1)

# 環境

### MLAgentsの推奨環境

- Unity (2018.4以降)
- Python (3.6.1以降)

### 今回私が使用した環境

- __Unity (2019.4.15f1)__
- __Python (3.7.9) Anaconda__

推奨環境を満たしていれば大丈夫だと思いますが、Unityはバージョン等によりUIが変わる可能性があるのでご容赦ください。

# 前準備
### MLAgentsのダウンロード

[こちら](https://github.com/Unity-Technologies/ml-agents)のサイトから、__Release13__をダウンロードしてください。
ダウンロードされたZIPファイルを任意の場所に展開してください。

### PythonとUnityのダウンロード＆インストール

参考記事がわかりやすくまとめてくださったので引用します。参照されてください。

[【ML-Agents】UnityとPythonのTensorFlowをつかって機械学習させてみた(v0.11β対応)](https://qiita.com/maruda/items/687f068d9d356f16dd90)

# 準備
### Pythonの環境構築

#### Pytorchのインストール

ターミナルで以下を実行します

```
pip install torch~=1.7.1 -f https://download.pytorch.org/whl/torch_stable.html
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275720/f6b343d0-04a9-8437-e449-4bd91c469065.png)
完了

#### 関連ライブラリのインストール

__展開したディレクトリに移動__して、以下を実行

```
pip install -e ./ml-agents-envs
pip install -e ./ml-agents
```

実際、`pip install mlagents`でもインストールできますが、この際は常に最新版がインストールされますので、バージョンの整合性をとるためにもダウンロードしたSetup.pyから参照することをお勧めします。

__0.24.0__がインストールされたことを確認してください

# Unitでプロジェクト作成

Unity Hubを開いて、プロジェクトの作成をします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275720/8510f0af-c88f-7c84-bd1b-253af02f80df.png)

上のメニューから、`Window>Package Manager`の順に進んで、左上の＋マークから、`Add Package from disk...`を選択して、`<展開したフォルダ>/com.unity.ml-agents/package.json`を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275720/6eeddb1e-5c57-082a-2983-fd9b96dd4be2.png)

インポートが始まり、`In Project`に`ML Agents 1.8.0`があれば大丈夫です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275720/9f478161-25ec-df10-459b-96bed723fccb.png)

今回は、箱をボールが追いかけるAIを作成します。(詳しくは記事の最後を見てね)
Unityの詳しい説明等はここでは省略します。

### ステージの作成

`GameObject>3D Object>Plane`を選択し、__Stageに改名、Positionを(0, 0, 0)Scaleを(1, 1, 1)に設定__
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275720/4fc224fc-10aa-75e7-9ca5-2fc321213316.png)

### 箱の設定

`GameObject>3D Object>Cube`を選択し、__Targetに改名、Positionを(3, 0.5, 3)Scaleを(1, 1, 1)に設定__
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275720/13676df5-72c1-f109-1b46-dc650b217ec4.png)

### ボールの設定

`GameObject>3D Object>Sphere`を選択し、__RollerAgentに改名、Positionを(0, 0.5, 0)Scaleを(1, 1, 1)に設定__

__Inspector下部の`Add Component`から、`Physics>Rigidbody`を追加  👈これめちゃ大事です！！！__
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275720/1ba40283-35e8-359a-d391-c822bc29a07d.png)


後で複製できるように、グループ化しておきます。
`GameObject>Create Empty`を選択し、__Stage、Target、RollerAgentをドラッグしてグループ化します__名前は何でもいいです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275720/59ef2d5f-af92-a0d7-306f-ee770a758103.png)

### ボールのスクリプト作成

UnityのProject内に`Scripts`フォルダを作り、その中に`RollerAgent.cs`を作成します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275720/2c8165db-299d-15de-b2a8-e2c3e70c1063.png)

中身をお好きなエディタで以下のように書き換えてください。
解説は適宜コメントで挿入してあるので見てみてくださいね。

```c#:RollerAgent.cs

// 使用ライブラリのインポート
using System.Collections.Generic;
using UnityEngine;
using Unity.MLAgents;
using Unity.MLAgents.Sensors;
using Unity.MLAgents.Actuators;

// Agentクラスを継承し、必要なところだけ書き換えていきます
public class RollerAgent:Agent
{
    Rigidbody rBody;
    // スタートしたときに呼び出される関数
    void Start () {
        // ボールの物体を変数に格納
        rBody = GetComponent<Rigidbody>();
    }

    // Targetをグローバルに宣言
    public Transform Target;
    // エピソード(学習のステップ)が始まった時に呼び出される関数
    public override void OnEpisodeBegin()
    {
        // ボールのY座標が0＝落下したとき
        if (this.transform.localPosition.y < 0)
        {
            // 初期化
            this.rBody.angularVelocity = Vector3.zero;
            this.rBody.velocity = Vector3.zero;
            this.transform.localPosition = new Vector3( 0, 0.5f, 0);
        }

        // ターゲットをランダムな位置に
        Target.localPosition = new Vector3(Random.value * 8 - 4,
                                            0.5f,
                                            Random.value * 8 - 4);
    }

    // 観測データ(この場合で言うと学習に必要な数値)の取得
    public override void CollectObservations(VectorSensor sensor)
    {
        // ボールと箱の座標(x, y, z) x 2
        sensor.AddObservation(Target.localPosition);
        sensor.AddObservation(this.transform.localPosition);

        // ボールのスピード(x, z)
        sensor.AddObservation(rBody.velocity.x);
        sensor.AddObservation(rBody.velocity.z);
        
        // トータルで入力次元は8次元になる
    }

    // 力を加えるときの乗数
    public float forceMultiplier = 10;
    // アクションが起きたときに呼び出される関数
    public override void OnActionReceived(ActionBuffers actionBuffers)
    {
        // XとZ軸の入力に合わせてボールに力を加える
        Vector3 controlSignal = Vector3.zero;
        controlSignal.x = actionBuffers.ContinuousActions[0];
        controlSignal.z = actionBuffers.ContinuousActions[1];
        rBody.AddForce(controlSignal * forceMultiplier);

        // 箱とボールの距離
        float distanceToTarget = Vector3.Distance(this.transform.localPosition, Target.localPosition);

        // 箱にボールが到達したとき
        if (distanceToTarget < 1.42f)
        {
            // 報酬を1に設定
            SetReward(1.0f);
            // エピソードを終了
            EndEpisode();
        }

        // ボールが落下したとき
        else if (this.transform.localPosition.y < 0)
        {
            // エピソードを終了
            EndEpisode();
        }
    }

    // 手で動かす際の設定
    public override void Heuristic(in ActionBuffers actionsOut)
    {
        var continuousActionsOut = actionsOut.ContinuousActions;
        continuousActionsOut[0] = Input.GetAxis("Horizontal");
        continuousActionsOut[1] = Input.GetAxis("Vertical");
        
    }
}
```

### ファイルのアタッチ

作成したスクリプトを`RollerAgent`にドラッグします。

__`RollerAgent (Script)`の`Target`に、`Target(ゲームオブジェクト)`をドラッグ__

__`Add Component`から、`ML Agents>Behavior Parameters`を追加__
`Vector Observation>Space Size` を `8` に変更
`Actions>Continuous Actions` を `2` に変更
`Behavior name` を `RollerBall` に変更

__`Add Component`から、`ML Agents>Decision Requester`を追加__
`Decision Period` を `10` に変更

最後はこんな感じになります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275720/d61ef150-b323-f231-1ecb-d2d1be695e18.png)


# テスト
`Behavior Parameters`の`Behavior Type`を`Heuristic Only`に変更

中央上部のプレイボタンを押して、矢印キーで遊べるか確認してみてください
![test.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275720/a2825e75-df88-c9aa-4667-51930d53c96e.gif)


# 学習
__`<展開したディレクトリ>/config/rollerball.yaml`を作成__して、以下を書き込みます

```yaml:rollerball.yaml
behaviors:
  RollerBall:
    trainer_type: ppo
    hyperparameters:
      batch_size: 10
      buffer_size: 100
      learning_rate: 3.0e-4
      beta: 5.0e-4
      epsilon: 0.2
      lambd: 0.99
      num_epoch: 3
      learning_rate_schedule: linear
    network_settings:
      normalize: false
      hidden_units: 128
      num_layers: 2
    reward_signals:
      extrinsic:
        gamma: 0.99
        strength: 1.0
    max_steps: 500000
    time_horizon: 64
    summary_freq: 1000

```

`Behavior Parameters`の`Behavior Type`を`Default`に変更

__展開したディレクトリに移動__して、以下を実行

```
mlagents-learn config/rollerball.yaml --run-id=RollerBall
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275720/95735aa9-37c6-a2e2-ce26-6c8e89329562.png)

```
Listening on port 5004. Start training by pressing the Play button in the Unity Editor.
```

この文言がでたら、Unityに戻って、プレイボタンを押すと、学習が始まります。
最初のうちはボールがオロオロ動くのがなかなかに可愛いです笑

# 学習並列化

物理エンジンのいいところは、モデルを複製して同時に学習を進められるところです。
先ほどグループ化したものをプレハブ化し、複製してみましょう。(特にコード等変える必要はありません)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275720/2c9af7d3-f386-45e4-3f54-af31cd5bf426.png)

さて、改めて学習してみましょう。
![test2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275720/412a4018-4381-3a5c-53ee-031fbc40bc63.gif)

ログはこのようになります。`Mean Reward`が1に近づけばうまく学習が行われています。
終了したいときは、`CTRL+C`で止められます。その時のモデルデータを自動的に保存してくれます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275720/ed3e9a70-94c3-a45b-d11e-b7bacf1f119b.png)

また、バックボーンはTensorFlowなので、別ターミナルでコマンドを実行することで、TensorBoardも確認することができます。

```
tensorboard --logdir results --port 6006
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275720/443d8364-9a08-56fc-cb10-a20c15e693eb.png)

# 学習モデルでテスト

`<展開したディレクトリ>/results/RollerBall/RollerBall.onnx`を、Unityプロジェクト内の`Assets`にドロップしたのちに、RollerAgentの`Inspector`内のの`Behavior Parameters>Model`にドロップ。

プレイボタンを押すと、学習されたモデルでボールが動く様子を見ることができます。
床から落ちることなく箱を一生懸命追っている姿が確認できますね。~~可愛い。~~

![test3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275720/3cd695d8-31d1-8c99-af80-ed8631420305.gif)

# 最後に

たびたびバージョンの変わってしまうMLAgentsですが、使い方によっては機械学習の世界が広がると思います。
~~僕も早くコマンドラインで学習できるようにならないかなぁ~~