ML-Agentsを使って強化学習でBoidsモドキ

ML-Agentsの強化学習を把握するために、まずは移動制御だ、と思ってBoids（群体）モドキをやってみました。
結果がこちら。

<blockquote class="twitter-tweet" data-conversation="none" data-lang="ja"><p lang="ja" dir="ltr">強化学習の学習ステップをYoutubeにアップしてみました。<br>段々Boidsっぽくなりました。<a href="https://t.co/8HphXqT9lq">https://t.co/8HphXqT9lq</a></p>&mdash; foka (@foka22ok) <a href="https://twitter.com/foka22ok/status/1037953561580060672?ref_src=twsrc%5Etfw">2018年9月7日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


数十万ステップくらいからそれっぽい動きになってきた気がします。

# 学習条件
以下の条件で学習しました。
・正面に一定速度で進む
・赤玉の一定距離内に近づくと最大プラス報酬
・赤玉の方向を向けば向くほどプラス報酬
・体がひっくり返らなければプラス報酬
・正面上下左右の近距離に別の魚がいるとマイナス報酬

# BrainとAgent
## Brain
![ML-Brain.png](https://qiita-image-store.s3.amazonaws.com/0/16457/61e2e871-41b4-7371-2c7e-af6d9eec80b6.png)
※当初は魚群ではなく、飛行体でやろうとしていたのでFlyingBrainと名付けてしまいました...

## 学習で扱う状態
```c#:CollectObservations：MLAgents.Agentの機能

GameObject target;//追いかけるターゲット
GameObject enemy;//避ける敵（使わなかった）

public override void CollectObservations()
{
    AddVectorObs(transform.position); //自身の位置
    AddVectorObs(transform.rotation); //自身の向き
    AddVectorObs(target.transform.position - transform.position); //ターゲットまでの相対位置
    AddVectorObs(enemy.transform.position- transform.position); //敵までの相対位置（使わなかった）
    AddVectorObs(otherDistance); //正面上下左右の一定距離内の他の仲間との距離
}
```
※enemyは、当初敵キャラを置いて避けるようにしようと思っていた名残。

以下のパラメータ数を合計するとBrainのSpace Sizeで設定している18と同じ数になります。
AgentのAddVectorObsのパラメータ数はBrainのSpace Sizeと同数になる必要があります。
・自身の位置 x,y,z
・自身の向き x,y,z,w
・ターゲットまでの相対位置 x,y,z
・敵までの相対位置 x,y,z
・正面上下左右の他の仲間との距離 float[5]

# Agentの行動
```c#:AgentAction：MLAgents.Agentの機能

Rigidbody rb;

public override void AgentAction(float[] vectorAction, string textAction)
{
    float speed = 4f;

    rb.velocity = speed * transform.forward;//正面に一定速度で進む

    float actionX = speed * Mathf.Clamp(vectorAction[0], -1f, 1f); //左右回転情報として扱った
    float actionZ = speed * Mathf.Clamp(vectorAction[1], -1f, 1f); //上下回転情報として扱った

    //ここで強化学習の制御を反映している。今回は上下左右の回転制御にのみ利用。
    transform.rotation = transform.rotation* Quaternion.Euler(new Vector3(actionZ, actionX, 0)); 

    CheckOtherDistance(); //正面上下左右に他の仲間がいるか調べる。下記参照
    SetReward(); //条件に応じて報酬を設定する。下記参照
}
```

# 他の仲間との距離をチェックする
群体の数を後からでも増減できるようにしたかったので、全仲間の位置は監視せず、Physics.Raycastで一定距離内に反応があるかどうかだけを監視しました。

```c#:CheckOtherDistance：自作関数

[SerializeField]
LayerMask mask;//チェック対象レイヤーを限定しておく

 void CheckOtherDistance() {
    //初期化
    for(int i=0;i<otherDistance.Length; i++){
        otherDistance[i] = 0;
    }

    float rayDistance = 2.0f;

    //正面チェック
    Ray ray = new Ray(transform.position, transform.forward);
    RaycastHit hit;
    if (Physics.Raycast(ray, out hit, rayDistance, mask)) {
        otherDistance[0] = hit.distance;
    }

    //右方向チェック
    ray.direction = transform.right;
    if (Physics.Raycast(ray, out hit, rayDistance, mask)) {
        otherDistance[1] = hit.distance;
    }

    //左、上、下もPhysics.Raycastで同様にチェック

}
```

# 報酬を設定する
```c#:SetReward：自作関数
int flyCnt=0;//移動カウント。
float targetDist;//ターゲットとの距離。前フレームと比較して近づいているかどうかの判定に使う

void SetReward() {

    flyCnt++;

    //Bodyがひっくり返ってないかチェック。姿勢がちゃんとしてればより高報酬。
    float rDistance = Vector3.Distance(new Vector3(0,1,0), transform.up.normalized);
    if (rDistance < 1) {
        AddReward((1 - rDistance) * 0.01f);
    }

    //ターゲットを向いているかチェック。ターゲットを向いていればいるほど高報酬。
    Vector3 targetVector = (target.transform.position - transform.position).normalized;
    float vDistance = Vector3.Distance(targetVector, transform.forward);
    AddReward((1- vDistance) * 0.05f);

    float nowDist = Vector3.Distance(target.transform.position, transform.position);

    //前フレームよりターゲットに近づけば微報酬。もしかしたらなくても良かったかもしれない
    if (nowDist < targetDist) {
        AddReward(0.01f);
    }
    //flyCnt が一定以上経った状態で離れたところにいる場合は移動失敗とみなして最大マイナス報酬してDone!。
    else if (flyCnt > 300 && nowDist > 150) {
        AddReward(-1);
        Done();
    }
    targetDist = nowDist;

    //ターゲットに最接近したらゴールとして最大報酬してDone!。
    if (nowDist < 1f) {
        AddReward(1f);
        Done();
    }

    //正面の上下左右に他の仲間がいたらマイナス報酬。raycastで反応があればotherDist が0より大きくなるようにしているので0より大きいかどうかだけで判定した。
    foreach (float otherDist in otherDistance) {
        if (otherDist > 0) {
            AddReward(-0.01f);
        }
    }
}
```
最初は体がひっくり返ってるかのチェックしてなかったですが、それだとひっくり返ったままでも気にせず進むやつが多発...。
体の姿勢に応じて報酬を設定すると、それなりにうまくいきました。

# AgentReset時
```c#:AgentReset：MLAgents.Agentの機能
public override void AgentReset()
{
    //AgentResetの際に、位置と向きをランダムに指定して、どの向き・どの位置からでも対応できるようにした
    float positionRange = 20f;
    float angleRange = 180f;
    flyCnt = 0;
    transform.position = new Vector3(Random.Range(-positionRange, positionRange), Random.Range(-positionRange, positionRange), Random.Range(-positionRange, positionRange));
    transform.rotation = Quaternion.Euler(new Vector3(Random.Range(-angleRange, angleRange), Random.Range(-angleRange, angleRange), Random.Range(-angleRange, angleRange)));
    targetDist = Vector3.Distance(target.transform.position, transform.position);
}
```

# まとめ
手探りで試行錯誤しながらやってみたので、変なこともしてそうですが、ひとまずこれでBoidsっぽくなりました。

あわよくば普通にBoids実装するより処理が軽いと良いな、と期待したけど、強化学習どうのこうの言う前に、Raycastが重かったです...。

それなりの結果を出すために学習時間が数十分から1時間くらいかかることもあり、ちょっと調整して確認する、というだけでもかなりの時間がかかって心が折れそうになりました。

でも、とりあえずは、なんとなくML-Agentsの使い方がわかったので良しとします。