Unity、ML-Agentsで車載カメラの画像データを使い、ミニカーに自動運転させてみた話。

#目的
今回はUnityのML-Agentsを用いて、距離センサーを使わずに画像データを読み込みそれを使ってミニカーを自動運転させることに取り組んでいきたいと思います。

#学習の効率化
今回は進むべき道を青色、そのほかの壁、障害物を赤色に設定し、画像の読み込みで青色成分(RGBのB)だけを取り込むことで計算量を抑え、学習の効率化を行います。

#前提
この記事を読むにあたり、
・Unityがインストールされていること
・ML-Agentsがインストールされていること
・TFSharpPlugin.unitypackageがインストール、インポートされていること
を前提にしています。

#環境、使ったもの
・Unity 2017.3.0f3
・ML-Agents 0.7.0
・tensorflow 1.7.1
・ミニカー(https://assetstore.unity.com/packages/3d/vehicles/land/car-20128)

#Sceneの作成
Plane(床)を作ります。次に空のゲームオブジェクトを作り、そこにcubeをアタッチしてコースをつくり、またミニカー(jeep)もアタッチしておきます。
![bandicam 2019-03-18 12-16-46-815.jpg](https://qiita-image-store.s3.amazonaws.com/0/378819/dc6fc837-a48f-0d16-331d-a65ca7b24206.jpeg)

下の画像でミニカーのある位置を(0,0,0)とすると、1マス分の長さが10となっています(後に紹介するスクリプトでは修正を加えない場合、このコースの色、位置、形状、大きさでしか機能しないので注意してください。)jeepには二つのカメラ(一つは画像を読み込む用、もう一つは実行時にその映像を確認する用)をアタッチしておきましょう。
![bandicam 2019-03-18 12-27-27-227.jpg](https://qiita-image-store.s3.amazonaws.com/0/378819/fdffd02f-0770-8f38-9273-2350504cb260.jpeg)

またjeepに当たり判定をつけるためにjeepのInspectorの中のAddComponent>PhysicsでRigidbodyとBoxColliderをつけておきます。次に空のゲームオブジェクトを作り、名前をAutoDriveAcademyに変更します。

ML-Agents>Examples>Template>ScriptsにTemplateAcademy、TemplateAgentスクリプトがあるので、それを他の場所にコピーし名前をAutoDriveAcademy,JeepAgentに変更して、前者をオブジェクトのAutoDriveAcademyに後者をjeepにアタッチします。

Project>create>ML-Agents>LearningBrainでLearningBrainという名前でBrainを作成します。AutoDriveAcademyのAdd Brain to BroadcastHubをクリックして、BrainsにLearningBrainをアタッチします。
![bandicam 2019-03-18 12-45-55-756.jpg](https://qiita-image-store.s3.amazonaws.com/0/378819/4ae57136-65d9-23f3-78c1-ad79de8367b8.jpeg)

LearningBrainのInspectorでVectorObservation>SpaceSizeを1025に設定します。(VectorObservationはLearningBrainへの入力数を表しており、ここでは32×32の画像ピクセルデータとミニカーの受ける慣性を入力とするため、32×32+1=1025とします)
またVectorActionのSpace TypeをDiscrete、Branches Sizeを1にし、Branch 0 Sizeを3とします。(ここは出力についての設定で、このように設定することで0,1,2の値を返す出力が1つ得られます。今回はこの出力の値によって左に曲がるのか、まっすぐに進むのか、右に曲がるのかを決定していきます)

![bandicam 2019-03-18 16-32-10-667.jpg](https://qiita-image-store.s3.amazonaws.com/0/378819/845e0b5b-4fc3-63bd-423c-30e6cc9a98b8.jpeg)

#カメラの設定
Project>create>RenderTexture を選択して、名前をtextureとしておきます。textureのInspectorでsizeを32×32にします。
![bandicam 2019-03-18 16-26-00-683.jpg](https://qiita-image-store.s3.amazonaws.com/0/378819/11732eb0-e589-3018-18ae-9ff5d99ead59.jpeg)

先ほどjeepに取り付けたカメラの一方についてInspectorの中のTargetTextureにtextureをアタッチします。
![bandicam 2019-03-18 21-03-28-639.jpg](https://qiita-image-store.s3.amazonaws.com/0/378819/fd38fa58-0b4f-5473-a1b7-7755b7803113.jpeg)
![bandicam 2019-03-18 21-01-39-769.jpg](https://qiita-image-store.s3.amazonaws.com/0/378819/d3d00b77-fa21-bc5d-18db-c9f0bb039300.jpeg)

#JeepAgentスクリプト
JeepAgentスクリプトを下記のように書き換えます。細かい説明は省きますが、コードの説明としては
・CollectObserVationメゾットではAddVectorObs()の引数に値を入れることによって、その値を入力としてLearningBrainに渡すことができます。今回は画像の各ピクセルの青色成分と受けている慣性力を入力としています。
・AgentAction()メゾットの２つの引数のうち1つ目のものがLearningBrainから受け取る出力値となります。今回はこの値によってミニカーがどちらに進むのかをコントロールします。
・AddReward()メゾットについては、このメゾットの引数に値を入力することにより報酬を与えることができます。今回はミニカーが⑴コースのある地点に達したとき⑵その地点に近づいたときにプラスの報酬を与え、ミニカーが⑶その地点から遠ざかったとき⑷壁や障害物に衝突したときにマイナスの報酬を与えます。またそのある地点とはミニカーがコースに沿って楕円運動するようにstateが示すそのときの状態によって変えていきます。(衝突したときにマイナスの報酬を与えるのみだと、ひたすらその場でぐるぐる回ってしまいました。)

```Python:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using MLAgents;
using UnityStandardAssets.Vehicles.Car;
using System.IO;

public class JeepAgent : Agent
{
    System.Random r = new System.Random();
    public GameObject obj;
    public new Camera camera;
    private Texture2D targetTexture;
    GameObject[] cloneObj;
    Rigidbody rb;
    float[] action = new float[1];
    int size = 32;
    float state = 0;
    float place = 0;
    

    public override void InitializeAgent()
    {
        rb = this.GetComponent<Rigidbody>();
    }
    public override void CollectObservations()
    {
        //車載カメラの映像を取得
        CollectVideo();
        var cols = targetTexture.GetPixels();
        foreach (var col in cols)
        {
            //車載カメラの映像を入力
            AddVectorObs(col.b);
        }
        //慣性を入力
        AddVectorObs(rb.angularVelocity.y);
    }

    public void MoveAgent(float[] act)
    {
        //VectorAction>BranchSizeは入力の個数 ここでは1に設定してある
        action[0] = act[0] - 1;               //0<action<2なので-1<action<1に変換
        //車の進む向きを決める
        this.gameObject.transform.Rotate(0, action[0] * 4, 0);
        rb.velocity = this.gameObject.transform.localRotation * new Vector3(0, 0, 8);
    }
    
    void OnCollisionEnter(Collision collision)
    {
        //壁、障害物に衝突したとき報酬-1
        AddReward(-1f);
        Done();
    }

    void CollectVideo()
    {
        var tex = camera.targetTexture;
        targetTexture = new Texture2D(tex.width, tex.height, TextureFormat.ARGB32, false);
        // RenderTextureキャプチャ
        RenderTexture.active = camera.targetTexture;
        targetTexture.ReadPixels(new Rect(0, 0, tex.width, tex.height), 0, 0);
        targetTexture.Apply();
    }

    void Reward()
    {
        //コースをうまく回るように報酬を設定する
        float position = 0;
        if (state == 0)
        {
            position = transform.localPosition.z;
            if (position > place)
            {
                AddReward(0.01f);
                place = position;
            }
            else
            {
                AddReward(-0.02f);
            }

            if (position >= 20)
            {
                state = 1;
                place = this.transform.localPosition.x;
                AddReward(0.5f);
            }
        }
        else if (state == 1)
        {
            position = this.transform.localPosition.x;
            if (position < place)
            {
                AddReward(0.01f);
                place = position;
            }
            else
            {
                AddReward(-0.02f);
            }
            if (position <= -30)
            {
                state = 2;
                place = this.transform.localPosition.z;
                AddReward(0.5f);
            }
        }
        else if (state == 2)
        {
            position = this.transform.localPosition.z;
            if (position < place)
            {
                AddReward(0.01f);
                place = position;
            }
            else
            {
                AddReward(-0.02f);
            }

            if (position <= -20)
            {
                state = 3;
                place = this.transform.localPosition.x;
                AddReward(0.5f);
            }
        }
        else if (state == 3)
        {
            position = this.transform.localPosition.x;
            if (position > place)
            {
                AddReward(0.01f);
                place = position;
            }
            else
            {
                AddReward(-0.02f);
            }

            if (position >= -10)
            {
                state = 0;
                place = this.transform.localPosition.z;
                AddReward(0.5f);
            }
        }
    }
    public override void AgentAction(float[] vectorAction, string textAction)
    {
        //行動
        MoveAgent(vectorAction);
        //報酬
        Reward();
    }

    public void InitiateObj()
    {
        if (cloneObj != null)
        {
            for (int i = 0; i < cloneObj.Length; i++)
            {
                Destroy(cloneObj[i]);
            }
        }
        int objN = r.Next(5, 7);  //障害物の個数を決定
        int obj_x = -10;
        int obj_z = 0;

        cloneObj = new GameObject[objN];
        for (int i = 0; i < objN; i++)
        {
            obj_x = -10;
            obj_z = 0;
            while (((obj_x > -32 && obj_x < -8) && (-22 < obj_z && obj_z < 22)) || ((obj_x >= -1 && obj_x <= 1) && (obj_z >= -1 && obj_z <= 3)))
            {
                obj_x = r.Next(-48, 8);
                obj_z = r.Next(-38, 38);
            }
            cloneObj[i] = Instantiate(obj, transform.position + new Vector3(obj_x, 0, obj_z), Quaternion.identity);

        }
    }


    public override void AgentReset()
    {
        //エージェントの状態をリセット
        transform.localPosition = new Vector3(0f, 0.4f, 0f);
        transform.localRotation = Quaternion.Euler(0f, 0f, 0f);
        rb.velocity = new Vector3(0, 0, 15);
        rb.angularVelocity = Vector3.zero; //慣性
        state = 0;
        place = 0;
        //障害物を削除、新たにセット
        InitiateObj();
    }

    public override void AgentOnDone()
    {

    }
}
```

次にjeepのInspectorで、BrainにLearningBrainをアタッチし、Cameraに先ほどtextureをアタッチしたCameraをアタッチします。また障害物となるオブジェクト(青色成分を含まない色にしてください。今回は赤)を作成し、Objにアタッチしてください。
![bandicam 2019-03-18 22-12-25-368.jpg](https://qiita-image-store.s3.amazonaws.com/0/378819/f7d4cdfa-20c8-074b-fe41-ab5b023c31da.jpeg)

これで学習の準備ができました。

#学習
AutoDriveAcademyのControlにチェックをつけます。
![bandicam 2019-03-18 22-26-47-914.jpg](https://qiita-image-store.s3.amazonaws.com/0/378819/2b40228c-6565-b206-d0df-b28808763f7b.jpeg)
Edit>Project Setting>Player選択後、Inspector>Other Settings>Configuration>Scripting Defines SymbolsにENABLE_TENSORFLOWと入力します。
![bandicam 2019-03-18 22-27-54-217.jpg](https://qiita-image-store.s3.amazonaws.com/0/378819/b8d5b100-05cc-c981-e391-c64765c090f1.jpeg)

次にAnaconda Navigatorを開き、Environments>mlagentの再生ボタンからOpen Terminalを選択するとターミナルが立ち上がります。
![bandicam 2019-03-18 22-41-56-851.jpg](https://qiita-image-store.s3.amazonaws.com/0/378819/aa95203b-352f-886e-23a5-c1b3e77cc783.jpeg)

ml-agents-masterディレクトリまで移動し、下記のように打ち込みます。(mlagents-learn config/trainer_config.yaml --run-id=firstRun --train)
![bandicam 2019-03-18 22-48-44-627.jpg](https://qiita-image-store.s3.amazonaws.com/0/378819/105cc0a4-b3e0-a48e-a0c4-c148fc8e83c9.jpeg)
下の画像のようになったらUnityに戻って再生ボタンを押します。すると学習が始まります。
![bandicam 2019-03-18 23-24-10-972.jpg](https://qiita-image-store.s3.amazonaws.com/0/378819/105a3c3e-69b6-dce9-41fa-cbee89d3c731.jpeg)

ハイパーパラメータはml-agents-master>config>trainer_config.yamlをいじることで変更できます。Batch Sizeは入力の数でBuffer SizeはBatch Sizeの整数倍でないとならないそうです。(これは自信ないです)また学習の回数はMax Stepsをいじることで変更できます。

#実行
AutoDriveAcademyのチェックを外し、先ほどENABLE_TENSORFLOWと入力した部分を消します。学習が終了するとml-agents-master>models>firstRunの場所にLearningBrain.nnというファイルが作成されているのでそれをUnityに持ってきてLearningBrain>Modelにアタッチします。
![bandicam 2019-03-18 23-37-50-921.jpg](https://qiita-image-store.s3.amazonaws.com/0/378819/3c824d82-6a46-d010-598a-022119dcc388.jpeg)

最後に再生ボタンを押します。

#結果
自分の場合は50~60万回ほど学習させました。
![a.gif](https://qiita-image-store.s3.amazonaws.com/0/378819/5c5c8831-7958-299f-affd-f4514d9ce2ac.gif)

無事動きました。障害物の数を増やしてみましたがちゃんとよけて進んでいます！

#参考文献
1. 【Unity強化学習】自作ゲームで強化学習(https://qiita.com/God_KonaBanana/items/7aebdb411c99b059cc6f)
2. ML-Agents（ver0.5）の環境導入方法まとめ(http://enjoy-unity.net/ml-agents/ver0-5_matome/)
3. unity ml-agent 0.4 でml-agentを理解してみよ(https://qiita.com/oruchin1122/items/c6049d7c3a75c72e81e7)
4. Unity ML-Agentsをwindows10で使う　2018年11月版(http://am1tanaka.hatenablog.com/entry/2018/11/07/170029)