強化学習 in Unityで悪戦苦闘するお話

卒業論文でUnity内で強化学習をやることになったので、その一部始終をこの記事を収めることにしました。

# インストール等の下準備

## Unityのインストール

https://unity.com/ja/download

公式サイトの手順に従ってインストール。
この記事ではUnityHubはv3.6.1を、UnityEditorは2022.3.4f1を使用していきます。

## ML-Agentsのダウンロード

https://unity.com/ja/products/machine-learning-agents

GitHubに飛び最新のバージョンをダウンロード。
ml-agents-developという名前でzipがダウンロードされるので、解凍してみると、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623062/2d1252b2-44f9-12b1-ee1d-fab97c1cc9f0.png)

ここでUnityHubを開き、`追加 > ml-agents-develop > Project`を選択。
初回はエディタがインストールされていません!的なことが言われるのでVisualStudioのチェックマークを外してインストールします。
私の場合チェックを外さないとダウンロードが進まないとかいう謎状況が発生していたのですが、詳しくは下の記事をご覧ください。

https://inuinukaukau.com/unity-installation-never-finishes

# 実際にアセットで遊んでみる

インストール完了後、改めてプロジェクトを開くと、エディターが開きます(当たり前ですね)。
私の場合は既にすこし触っているので、Senceの見た目が少し違うかもしれません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623062/e39be748-994b-ff77-b214-eba3ff5b911d.png)

画像では左下のProjectウィンドウから、`Assets > ML-Agents > Examples > (任意のフォルダー) > Scenes > Unityマークのやつ` をダブルクリック。するとSceneが開くので、すでに訓練済みのAIくんの所作を堪能しましょう。
(このgifはPushBlockのsceneを選択しています)

![Movie_001.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623062/12cef6f5-ec0b-a3f5-9246-a490503c33c2.gif)


いくらでも見ていられますね。
また、2vs2のサッカーや歩行モデルくんなどいろいろ興味深いものもたくさんあります。

このままでも十分楽しめますが、私は環境構築して自分でさいきょうのえーあいを訓練してうはうはしたいので、その方法を探るべくネットでいろいろ探してみました。

# 強化学習の環境構築

やはり最初に引っかかるのは公式サイトですね。

https://github.com/Unity-Technologies/ml-agents/blob/release_18_docs/docs/Learning-Environment-Create-New.md

残念ながら私は英語弱者なのでそんなに理解できませんが、翻訳の力も借りながらなんとかして構築していこうと思います。

まずUnityHubからML-Agentsと同じバージョンの新しい3Dコアプロジェクトを作成します。名前は任意ですが、ここでは強化学習の略語の"RL"を使用します。

### パッケージのダウンロード

https://github.com/Unity-Technologies/ml-agents/blob/release_18_docs/docs/Installation.md#install-the-comunityml-agents-unity-package

https://docs.unity3d.com/Manual/upm-ui-install.html

作成したら、上のサイトに従ってML-AgentsのAssetをインポートしますが、英語で私はちんぷんかんぷんだったので一応メモっておきます。

#### Assetsのインポート(Package Managerを使う方法)

Unityエディターで ` winodw > Package Manager ` をクリックするとPackageManagerというウィンドウが開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623062/fb7b924f-cd89-51d0-1d1b-48fc10d707f4.png)

そこで、左上の`Packages: In Project`のところをクリックし`Unity Registry`を選択。
そして下に表示された大量の候補の中から`ML Agents`を選択してInstallすればパッケージの導入はひとまず完了です。
思ったより簡単でしたね。

### オブジェクト作成

[公式ガイド](https://github.com/Unity-Technologies/ml-agents/blob/release_21_docs/docs/Learning-Environment-Create-New.md)そのままやります。

床、ターゲット、エージェント(AIが操作するオブジェクト)を作成して空オブジェの下にまとめます。

簡単な操作なのでさすがにエラーは吐かないかと思います。

### エージェントのスクリプトの実装

`Rigid Body`コンポーネントを追加しておきます。
その後、スクリプトを公式ガイドからコピペして保存した途端にエラーを吐きました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623062/e270d474-8012-5987-3cea-0036254e026d.png)

こちらが問題のスクリプトです。

```c#:RollerAgent.cs
using System.Collections.Generic;
using UnityEngine;
using Unity.MLAgents;
using Unity.MLAgents.Sensors;

public class RollerAgent : Agent
{
    Rigidbody rBody;
    void Start () {
        rBody = GetComponent<Rigidbody>();
    }

    public Transform Target;
    public override void OnEpisodeBegin()
    {
       // If the Agent fell, zero its momentum
        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);
        }

        // Move the target to a new spot
        Target.localPosition = new Vector3(Random.value * 8 - 4,
                                           0.5f,
                                           Random.value * 8 - 4);
    }
}
public override void CollectObservations(VectorSensor sensor)
{
    // Target and Agent positions
    sensor.AddObservation(Target.localPosition);
    sensor.AddObservation(this.transform.localPosition);

    // Agent velocity
    sensor.AddObservation(rBody.velocity.x);
    sensor.AddObservation(rBody.velocity.z);
}
public float forceMultiplier = 10;
public override void OnActionReceived(ActionBuffers actionBuffers)
{
    // Actions, size = 2
    Vector3 controlSignal = Vector3.zero;
    controlSignal.x = actionBuffers.ContinuousActions[0];
    controlSignal.z = actionBuffers.ContinuousActions[1];
    rBody.AddForce(controlSignal * forceMultiplier);

    // Rewards
    float distanceToTarget = Vector3.Distance(this.transform.localPosition, Target.localPosition);

    // Reached target
    if (distanceToTarget < 1.42f)
    {
        SetReward(1.0f);
        EndEpisode();
    }

    // Fell off platform
    else if (this.transform.localPosition.y < 0)
    {
        EndEpisode();
    }
}
```
残念ながら私はC#についても素人なのでさっぱりです。ということでchatGPT君の出番です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623062/d0cfcb89-17df-c5c3-2ab9-09e9a0d123c2.png)

なるほどわからん状態ですが、おそらく30行目の`public override void CollectObservations(VectorSensor sensor)`のところが`public class RollerAgent : Agent
{}`の下に入る必要があるということなのではないかと思ったので早速。

```diff_c_sharp
using System.Collections.Generic;
using UnityEngine;
using Unity.MLAgents;
using Unity.MLAgents.Sensors;

-public class RollerAgent : Agent
-{
-    ~
-}
-public override void CollectObservations(VectorSensor sensor)
-    {
-        ~
-    }

+public class RollerAgent : Agent
+{
+    public override void CollectObservations(VectorSensor sensor)
+    {
+        ~
+    }
+}
```
というような感じに修正したところ、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623062/2ce3b869-7c08-9d65-707f-e6cf5943221d.png)

エラーがいくつか消えましたね。やはり脳死コピペはよくないね てへぺろ(・ω<)
この調子でどんどんやっていきましょう。

```c#:RollerAgent
using System.Collections.Generic;
using UnityEngine;
using Unity.MLAgents;
using Unity.MLAgents.Sensors;

public class RollerAgent : Agent
{
    Rigidbody rBody;
    void Start () {
        rBody = GetComponent<Rigidbody>();
    }

    public Transform Target;
    public override void OnEpisodeBegin()
    {
       // If the Agent fell, zero its momentum
        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);
        }

        // Move the target to a new spot
        Target.localPosition = new Vector3(Random.value * 8 - 4,
                                           0.5f,
                                           Random.value * 8 - 4);
    }
    public override void CollectObservations(VectorSensor sensor)
    {
        // Target and Agent positions
        sensor.AddObservation(Target.localPosition);
        sensor.AddObservation(this.transform.localPosition);

        // Agent velocity
        sensor.AddObservation(rBody.velocity.x);
        sensor.AddObservation(rBody.velocity.z);
    }
    public float forceMultiplier = 10;
    public override void OnActionReceived(ActionBuffers actionBuffers)
    {
        // Actions, size = 2
        Vector3 controlSignal = Vector3.zero;
        controlSignal.x = actionBuffers.ContinuousActions[0];
        controlSignal.z = actionBuffers.ContinuousActions[1];
        rBody.AddForce(controlSignal * forceMultiplier);

        // Rewards
        float distanceToTarget = Vector3.Distance(this.transform.localPosition, Target.localPosition);

        // Reached target
        if (distanceToTarget < 1.42f)
        {
            SetReward(1.0f);
            EndEpisode();
        }

        // Fell off platform
        else if (this.transform.localPosition.y < 0)
        {
            EndEpisode();
        }
    }
}
```
一個だけ残ってしまいました。ということでまたもやchatGPT君のご登場です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623062/4bc33b68-ec0c-2251-7117-8b50aee1783e.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623062/d968f861-5da4-7727-0c5a-264a08eeaea8.png)

だそうです。コピペだから宣言で間違うはずなどないはずだなどと思いながらガイドを見返した結果、

```c#
using Unity.MLAgents;
using Unity.MLAgents.Sensors;
using Unity.MLAgents.Actuators;
```

が勝手に後半のコピペ用のところで

```c#
using System.Collections.Generic;
using UnityEngine;
using Unity.MLAgents;
using Unity.MLAgents.Sensors;
```

となっていました。そこで`using Unity.MLAgents.Actuators;`を追加すると、コンソールからエラーがすべて消え去りました。ということでこちらが最終的なコードです。

```c#:RollerAgent
using System.Collections.Generic;
using UnityEngine;
using Unity.MLAgents;
using Unity.MLAgents.Sensors;
using Unity.MLAgents.Actuators;

public class RollerAgent : Agent
{
    Rigidbody rBody;
    void Start () {
        rBody = GetComponent<Rigidbody>();
    }

    public Transform Target;
    public override void OnEpisodeBegin()
    {
       // If the Agent fell, zero its momentum
        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);
        }

        // Move the target to a new spot
        Target.localPosition = new Vector3(Random.value * 8 - 4,
                                           0.5f,
                                           Random.value * 8 - 4);
    }
    public override void CollectObservations(VectorSensor sensor)
    {
        // Target and Agent positions
        sensor.AddObservation(Target.localPosition);
        sensor.AddObservation(this.transform.localPosition);

        // Agent velocity
        sensor.AddObservation(rBody.velocity.x);
        sensor.AddObservation(rBody.velocity.z);
    }
    public float forceMultiplier = 10;
    public override void OnActionReceived(ActionBuffers actionBuffers)
    {
        // Actions, size = 2
        Vector3 controlSignal = Vector3.zero;
        controlSignal.x = actionBuffers.ContinuousActions[0];
        controlSignal.z = actionBuffers.ContinuousActions[1];
        rBody.AddForce(controlSignal * forceMultiplier);

        // Rewards
        float distanceToTarget = Vector3.Distance(this.transform.localPosition, Target.localPosition);

        // Reached target
        if (distanceToTarget < 1.42f)
        {
            SetReward(1.0f);
            EndEpisode();
        }

        // Fell off platform
        else if (this.transform.localPosition.y < 0)
        {
            EndEpisode();
        }
    }
}
```
さて、これでようやく次のステップに行けますね。

### エディター画面での最終設定

ヒエラルキーウィンドウで`RollerAgent`オブジェクトを選択したら(公式ガイドの通りであれば、この時`RollerAgent`は`TrainingArea`という空オブジェクトの子オブジェクトに設定されているはずです)、インスペクターウィンドウのRollerAgentスクリプトの所はこうなっているはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623062/320302e7-2ea4-da54-746f-8ebac5426873.png)


そこにヒエラルキーウィンドウから`Target`オブジェクトを選択して`Target`のフィールド(ここでは`None(Transform)`となっている所)にドラッグ&ドロップします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623062/4ea09c25-1584-af22-801d-586399b89e1f.png)

そしてコンポーネント`Decision Requester`と`Behavior Parameters`を追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623062/82cddc32-12f0-86a0-f945-f5a7d263e4d5.png)

`Decision Requester`の

`Decision Period`を`10`に設定し、


`Behavior Parameters`の

`Behavior Name`を`RollerBall`、
`Vector Observation`>`Space Size`を8、
`Actions`>`Continuous Actions`を2に設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623062/f28647bc-55da-2361-dcaa-efefa989395c.png)

これで、トレーニング前に環境をテストする準備が整いました。

### 環境のテスト

公式ガイドが環境のテストを勧めているのでやっておきます。
`RollerAgent`の`public class RollerAgent : Agent
{}`の内側に、以下のコードを挿入しておきます。

```c#
public override void Heuristic(in ActionBuffers actionsOut)
{
    var continuousActionsOut = actionsOut.ContinuousActions;
    continuousActionsOut[0] = Input.GetAxis("Horizontal");
    continuousActionsOut[1] = Input.GetAxis("Vertical");
}
```

シーンを実行すると、矢印キーでボールを操作でき、ターゲットに設定されているキューブに触れるとランダムな位置に再出現しているのがわかります。また、プラットフォームから落ちるとリセットされる様子が確認できるはずです。

これでテストは完了です。

**そして公式ガイドによると、なんとこれで環境構築はもう終了しているらしいです。**

せっかくなのでトレーニングもしちゃいましょう。

# AIのトレーニング

https://github.com/Unity-Technologies/ml-agents/blob/release_21_docs/docs/Getting-Started.md

## インストール

https://github.com/Unity-Technologies/ml-agents/blob/release_21_docs/docs/Installation.md

これによると、全体的な手順としては、

1. Unityをインストール(2022.3以上)
1. Phytonをインストール(3.10.12以上)
1. リポジトリのクローンを作成(任意)
1. `com.unity.ml-agents`というUnityパッケージをインストールする
1. `com.unity.ml-agents.extensions`というUnityパッケージもインストールする(任意)
1. `mlagents-envs`をインストール
1. `mlagents`Pythonパッケージをインストールする

とインストールだらけです。なお、3番目のやつは調べた感じサンプルシーンのダウンロードらしいのでこの記事では飛ばしていきます。ではやっていきましょう。

### 1. Unityをインストール(2022.3以上)

[もうできているはずです。](https://qiita.com/drafts#unity%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)

### 2. Pythonをインストール

https://www.python.org/downloads/

https://www.anaconda.com/download

この記事では3.10.12を使用していきます。また、condaを使って仮想環境を管理することを推奨しているのでインストールしておきます。

インストールが完了すると、Anaconda Navigatorというランチャーが開くので、
`CMD.exe Prompt`を起動し以下のコマンドを入力します。

```
conda create -n mlagents python=3.10.12 && conda activate mlagents
```
個々の構文はこういう感じです。
```
conda create -n 環境名 python=バージョン
&&
activate 環境名
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623062/ef944fee-0154-1fb1-cb43-732c4825ed78.png)


このような画面が出るのでyを押してEnter。
こういう感じになり仮想環境が作成されます。コマンドプロンプトは開いたままにしておきます。

### 3. リポジトリのクローン

こちらで環境構築はすでに終えているのでスキップしたいところですが、後ろのほうでerror吐いてたのでやむを得ずクローンしておきます。
Anacondaのコマンドプロンプトから、
```
git clone --branch release_21 https://github.com/Unity-Technologies/ml-agents.git
```
と入力。これで完了です。

### 4. `com.unity.ml-agents`のインストール

[既に完了しています。](https://qiita.com/drafts#%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8%E3%81%AE%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89)

### 5. `com.unity.ml-agents.extensions`のインストール

今回の企画とは関係ないのでスキップです。

### 6. `mlagents` Python packageのインストール

先ほどAnaconda Navigatorで開いたコマンドプロンプトを閉じた場合は、再度開き、`activate mlagents`と入力してください。

それではいよいよMLagents(Python package)をインストールします。まずクローンしたリポジトリ`mlagents`のパスを調べましょう。
```
cd パス(筆者の場合はC:\Users\user\ml-agents)
```
その後、
```
python -m pip install ./ml-agents-envs
python -m pip install ./ml-agents
```
と入力します。