ランド研究所の「機械学習による航空支配」を実装する（その１７）： 2D問題 mission_4

#1. はじめに

本記事は、ランド研究所の「機械学習による航空支配」を実装する（その１７）です。

[Air Dominance Through Machine Learning: A Preliminary Exploration of Artificial Intelligence–Assisted Mission Planning, 2020](https://www.rand.org/pubs/research_reports/RR4311.html)


使用したコードは、下記GitHubの'mission_4'フォルダにあります。

> https://github.com/DreamMaker-Ai/AirDominance_2D_RL

強化学習アルゴリズムは、これまでと同じく、[SAC](https://arxiv.org/abs/1801.01290) を用いました。



#2. Mission 4

Mission 4 は、Fighter 3 機とDecoy 1 機を使って、SAM を攻撃するミッションです。SAM は Decoy を迎撃した後、次の迎撃ができない期間（Cool down）に入ります。このクールダウン・タイム期間中に、Fighter が搭載ウェポンの射程内に SAM を捉えることが出来る位置まで進出するプランニングができれば、SAM を撃破しミッションを達成できます。

これは、これまでのミッションと違って、タイミング制御が成功のために非常に重要となる問題です。

※　クールダウン・タイムが何で、どう設定したのかは、ランド研究所のレポートに記載されていないので判りません。SAMのミサイル・シーカが、赤外線シーカであれば、センサが冷却型の場合は、冷却に一定の時間が必要なはずですが、このことを言っているのでしょうか？

いずれにせよ、クールダウン・タイムは、いくら長くても２０〜３０秒程度だと思いますので、強化学習でこの精緻なタイミング制御をするのは、非常に難しいのではないかと予想しました（実際、難しかったです）。このため、非現実的に長いクールダウン・タイムから、次第に短くして限界性能を調べました。

結論から言うと、報酬シェーピング無しで、２０～３０秒のタイミング制御を実現することはできませんでした。ここは、３週間ほど試行錯誤しましたが、報酬シェーピングを工夫することで、なんとか所定のタイミング制御ができました。（それでも簡単ではないので、これ以上短いのであれば、発想を変えて、Decoy と Fighter１機を１パッケージで運用するような別のアプローチのほうが良いと思います）。

報酬シェーピングについては、（その１８）で記載することにして、今回は、報酬シェーピング無しで達成できた限界について記載します。


#3. 相対観測量

まず初めに相対観測量を定義します。

各エピソードに登場するエンティティは、3機のFighter、1機のDecoy、1基のSAMなので、MDP（Markov Decision Process）を構成するための相対観測量は以下の５８変数としました。

![relative_geometry_mission_4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/d78185d4-7f0a-fde6-2d22-8392bf1bbf44.jpeg)

表にまとめたのが下表です。観測 37 以降を、Mission 3 の観測に追加しました。

![relative observation table.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/25b8d2f1-b6a1-b0b9-674a-91bc6187f9b7.jpeg)

なお、今回のミッションでは、Decoy が SAM に撃破された以降の Decoy に関係する相対観測量をどう取り扱うか考えることが必要です。Decoy が SAM に撃破された場合、Decoy自体が存在しなくなるので、当然観測量はありません。したがって、観測空間を切り替えるのが王道のような気がします。ただ、そんなことがやれるのかどうかわからなかったし、仮にできても手間がかかりそうだったので、今回は、```Decoy が破壊されたらDecoy に関する観測量を全て 0 に設定する```ことにして様子を見ました。結果的には問題はありませんでしたが、ここは、実際の現象と観測量が異なっている訳なので、もう少し良いアイデアがあれば差し替えたい所です。

今回の相対観測量は、コードでは、以下になります。

    def get_relative_observation_mission_4(self):
        obs = []
        r = (self.space_x ** 2 + self.space_y ** 2) ** 0.5

        v_1 = self.make_v(self.fighter_1)
        r_1s = self.make_r(self.sam_1, self.fighter_1)
        sin_1, cos_1 = self.compute_heading_error(v_1, r_1s)
        obs.append(sin_1)
        obs.append(cos_1)
        obs.append(np.linalg.norm(r_1s, ord=2) / r)

        v_2 = self.make_v(self.fighter_2)
        r_2s = self.make_r(self.sam_1, self.fighter_2)
        sin_2, cos_2 = self.compute_heading_error(v_2, r_2s)
        obs.append(sin_2)
        obs.append(cos_2)
        obs.append(np.linalg.norm(r_2s, ord=2) / r)

        v_3 = self.make_v(self.fighter_3)
        r_3s = self.make_r(self.sam_1, self.fighter_3)
        sin_3, cos_3 = self.compute_heading_error(v_3, r_3s)
        obs.append(sin_3)
        obs.append(cos_3)
        obs.append(np.linalg.norm(r_3s, ord=2) / r)

        r_12 = self.make_r(self.fighter_2, self.fighter_1)
        r_13 = self.make_r(self.fighter_3, self.fighter_1)
        r_23 = self.make_r(self.fighter_3, self.fighter_2)

        sin_phai_12, cos_phai_12 = self.compute_heading_error(v_1, r_12)
        sin_phai_13, cos_phai_13 = self.compute_heading_error(v_1, r_13)
        sin_phai_21, cos_phai_21 = self.compute_heading_error(v_2, r_12)
        sin_phai_23, cos_phai_23 = self.compute_heading_error(v_2, r_23)
        sin_phai_31, cos_phai_31 = self.compute_heading_error(v_3, r_13)
        sin_phai_32, cos_phai_32 = self.compute_heading_error(v_3, r_23)
        obs.append(sin_phai_12)
        obs.append(cos_phai_12)
        obs.append(sin_phai_13)
        obs.append(cos_phai_13)
        obs.append(sin_phai_21)
        obs.append(cos_phai_21)
        obs.append(sin_phai_23)
        obs.append(cos_phai_23)
        obs.append(sin_phai_31)
        obs.append(cos_phai_31)
        obs.append(sin_phai_32)
        obs.append(cos_phai_32)
        obs.append(np.linalg.norm(r_12, ord=2) / r)
        obs.append(np.linalg.norm(r_13, ord=2) / r)
        obs.append(np.linalg.norm(r_23, ord=2) / r)

        obs.append(self.fighter_1.firing_range / self.fighter_1.max_firing_range)
        obs.append(self.fighter_1.weapon_count)
        obs.append(self.fighter_1.alive)

        obs.append(self.fighter_2.firing_range / self.fighter_2.max_firing_range)
        obs.append(self.fighter_2.weapon_count)
        obs.append(self.fighter_2.alive)

        obs.append(self.fighter_3.firing_range / self.fighter_3.max_firing_range)
        obs.append(self.fighter_3.weapon_count)
        obs.append(self.fighter_3.alive)

        obs.append(self.sam_1.firing_range / self.sam_1.max_firing_range)
        obs.append(self.sam_1.jammed_firing_range /
                   (self.sam_1.max_firing_range * self.jammer_1.jam_effectiveness))
        obs.append(self.sam_1.weapon_count)
        obs.append(self.sam_1.alive)

        if self.decoy_1.alive:
            v_d = self.make_v(self.decoy_1)
            r_ds = self.make_r(self.sam_1, self.decoy_1)
            sin_d1, cos_d1 = self.compute_heading_error(v_d, r_ds)
            obs.append(sin_d1)
            obs.append(cos_d1)
            obs.append(np.linalg.norm(r_ds, ord=2) / r)

            r_d1 = self.make_r(self.fighter_1, self.decoy_1)
            r_d2 = self.make_r(self.fighter_2, self.decoy_1)
            r_d3 = self.make_r(self.fighter_3, self.decoy_1)

            sin_phai_d1, cos_phai_d1 = self.compute_heading_error(v_d, r_d1)
            sin_phai_d2, cos_phai_d2 = self.compute_heading_error(v_d, r_d2)
            sin_phai_d3, cos_phai_d3 = self.compute_heading_error(v_d, r_d3)
            sin_phai_1d, cos_phai_1d = self.compute_heading_error(v_1, r_d1)
            sin_phai_2d, cos_phai_2d = self.compute_heading_error(v_2, r_d2)
            sin_phai_3d, cos_phai_3d = self.compute_heading_error(v_3, r_d3)
            obs.append(sin_phai_d1)
            obs.append(cos_phai_d1)
            obs.append(sin_phai_d2)
            obs.append(cos_phai_d2)
            obs.append(sin_phai_d3)
            obs.append(cos_phai_d3)
            obs.append(sin_phai_1d)
            obs.append(cos_phai_1d)
            obs.append(sin_phai_2d)
            obs.append(cos_phai_2d)
            obs.append(sin_phai_3d)
            obs.append(cos_phai_3d)
            obs.append(np.linalg.norm(r_d1, ord=2) / r)
            obs.append(np.linalg.norm(r_d2, ord=2) / r)
            obs.append(np.linalg.norm(r_d3, ord=2) / r)
        else:
            for _ in range(18):
                obs.append(0)

        obs.append(self.decoy_1.alive)

        return obs



#4. 学習条件とネット・アーキテクチャ
##4.1 初期配置

いつものようにSAMは戦場中心 (50,50) km に配置し、SAM を中心とした半径 [30, 50] km の円環状にランダムに Fighter 3 機と Decoy 1 機を配置しました。これまでのミッションで、初期ヘディングエラーに対しては、十分にロバストなものが学習できることがわかったので、初期ヘディングエラーは 0 deg のみで学習します。

Decoy のスピードは、Fighter と同じく 740 km /h としました。

##4.2 報酬
報酬シェーピングはしていないので、SAMを撃破できた時に１、それ以外の時には−１を与えました。

    def get_reward_mission_4(self, done, fighter_1, fighter_2, fighter_3, decoy, sam):
        reward = 0

        if done:
            if (fighter_1.alive > .5) and (fighter_2.alive > .5) and (fighter_3.alive > .5) and \
                    (sam.alive < .5):
                reward = 1
            else:
                reward = -1

        return reward

##4.3 ネットアーキテクチャ
相対観測量が増えたので、default モデルであるポリシー・ネットワーク、価値ネットワークがそれぞれ隠れ層ユニット数 [256,256] の2層構造では小さすぎるかもしれません。念のため、それぞれを [512, 512, 256] の3層構造にしました。重み共有は、（default モデルと同様）、用いませんでした。


#5. 学習履歴

報酬シェーピングしない場合、Cool down time は、40 time steps が限界で、それ以上短くすると、学習が進みませんでした。これは、97.297 sec、約１分３７秒に相当するので、現実問題としては長過ぎる値だと思います。

強化学習で精緻なタイミング・コントロールをやっている例は、あまり見かけないのですが、やはり、ある程度、報酬シェーピングを使って、学習を誘導してやらないと困難なようです。

学習は、下図の平均エピソード報酬の履歴ように、しばしの探索期間の後に、急に進む傾向がありました。

![episode reward mean_trial.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/9ae2a9b4-3dc4-50a8-79a4-fae55fe05f8f.jpeg)

学習中の平均エピソード長の履歴は下記になります。

![episode length mean_trail.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/5bff0b69-ea0e-1f4c-4930-912505cb25f9.jpeg)

トレーニング中のエピソード報酬の分布履歴は下図になります。

![ID_2_reward_hist.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/9d01fc1c-9d47-f8a9-eba5-2676eb4e9db3.png)

![ID_2_reward.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/9cb304f0-2242-8c80-bcb9-bc01dbc1559c.png)

また、トレーニング中のエピソード長の分布履歴は下図になります。

![episode_length.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/108d3e14-9990-a99c-ac77-39aefa4be07f.png)


![ID_2_length.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/09658c35-17f9-85b9-89e5-0360655b358b.png)




#6. 性能とロバストネス（汎化能力）

トレーニング中にランダムに生成した初期条件と同じ範囲内の初期条件で、１０エピソードのテストを実施し、成功率を評価したのが下図になります。テスト数が少ないのは、ビンテージマシンで試行錯誤するので、少しでも時間短縮を図りたかったからです。本当は、100ぐらいは欲しいところだと感じます。

![ID_2_history.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/b9ce2386-9b0a-67ee-ce51-41a9cacdc6a5.png)

学習時の条件範囲で1,000エピソードのシミュレーションを行ってノミナル能力を確認したのが下図です。成功率は 44.4% とこれまでのミッションに比べると格段に低い性能しか達成できませんでした。10エピソードでの評価に比べるとずっと悪い値になっていますが、理由は判りませんでした。もっとよく調べるべきなのですが、先に進みたいので、ここはペンディングとしました。

また、トレーニングで使用した初期条件を外装した初期条件で1,000エピソードのシミュレーションを行って汎化能力を確認しました。汎化能力自体は、それほど大きくは劣化しませんでした。

![success ration [%] over 1000 episodes.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/8596538f-b08e-1b9b-5673-9e6a37ace0ad.png)

#7. 生成プラン例

以下に生成プランの例を示します。

青い３つの丸が戦闘機、その周りの薄い円が搭載ウェポンの射程、水色の円が Decoy です。また、赤丸が SAM、赤い薄い円が SAM の射程です。

SAM の射程に Decoy が進入すると、Decoy は撃墜され、その代わりに Cool down time の間だけ、SAM の射程が０になります。この間に、Fighter 搭載ウェポンの射程内に SAM を捉えるプランが生成できれば成功です。

#7.1 失敗プランの例

Fighterは、SAMの射程外ギリギリまで近づいて、DecoyがSAMの射程に入って撃破されて、SAMがcool down time に入るのを待ちます。その上で、すかさず侵入しSAMを撃破しようとするのですが、進入タイミングが僅かに遅く、Cool down を終了したSAMに撃破されてしまいます。

後出の成功プランと比べると、このタイムスケールでは、タイミングの違いが判別できないぐらいしか違っていませんが、失敗は失敗です。強化学習で、このタイミングを制御するのは非常に難しいと感じました。

```
{
  "mission_id": "mission_4",
  "mission_condition": "d1",
  "result": "fail",
  "fighter_1": {
    "alive": 1,
    "firing_range": 18.0
  },
  "fighter_2": {
    "alive": 0,
    "firing_range": 15.5
  },
  "fighter_3": {
    "alive": 1,
    "firing_range": 16.5
  },
  "sam_1": {
    "alive": 1,
    "firing_range": 20.0,
    "cooldown_on": 0,
    "cooldown_max_count": 10,
    "cooldown_counter": 0
  }
}
```

![sac_fail_7.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/daca0df6-6c53-0167-e8ee-9995154bb51a.gif)

```
{
  "mission_id": "mission_4",
  "mission_condition": "d1",
  "result": "fail",
  "fighter_1": {
    "alive": 1,
    "firing_range": 13.0
  },
  "fighter_2": {
    "alive": 0,
    "firing_range": 11.0
  },
  "fighter_3": {
    "alive": 1,
    "firing_range": 10.5
  },
  "sam_1": {
    "alive": 1,
    "firing_range": 14.5,
    "cooldown_on": 0,
    "cooldown_max_count": 10,
    "cooldown_counter": 0
  }
}
```

![sac_fail_11.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/534601bf-26b7-acd9-2fca-7080bff7b434.gif)

```
{
  "mission_id": "mission_4",
  "mission_condition": "d1",
  "result": "fail",
  "fighter_1": {
    "alive": 0,
    "firing_range": 12.0
  },
  "fighter_2": {
    "alive": 0,
    "firing_range": 9.0
  },
  "fighter_3": {
    "alive": 1,
    "firing_range": 11.0
  },
  "sam_1": {
    "alive": 0,
    "firing_range": 13.0,
    "cooldown_on": 0,
    "cooldown_max_count": 10,
    "cooldown_counter": 0
  }
}
```

![sac_fail_17.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/1a90228a-17f0-91b0-e0d3-22461f415413.gif)

#7.2 成功プランの例

このタイムスケールでは、失敗プランと同じようなタイミングでFighterがSAMの射程に進出しているようにしか見えないのですが、わずかなタイミングの違いでこれらはミッションに成功しています。

```
{
  "mission_id": "mission_4",
  "mission_condition": "d1",
  "result": "success",
  "fighter_1": {
    "alive": 1,
    "firing_range": 17.0
  },
  "fighter_2": {
    "alive": 1,
    "firing_range": 17.5
  },
  "fighter_3": {
    "alive": 1,
    "firing_range": 15.5
  },
  "sam_1": {
    "alive": 0,
    "firing_range": 0,
    "cooldown_on": 1,
    "cooldown_max_count": 10,
    "cooldown_counter": 8
  }
}
```

![sac_success_3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/eca51c55-5bac-8e8a-c650-ba57cf462852.gif)

先行して SAM に近づいた Fighter が待機旋回して Decoy を待って進出します。

```
{
  "mission_id": "mission_4",
  "mission_condition": "d1",
  "result": "success",
  "fighter_1": {
    "alive": 1,
    "firing_range": 18.5
  },
  "fighter_2": {
    "alive": 1,
    "firing_range": 17.0
  },
  "fighter_3": {
    "alive": 1,
    "firing_range": 18.0
  },
  "sam_1": {
    "alive": 0,
    "firing_range": 0,
    "cooldown_on": 1,
    "cooldown_max_count": 10,
    "cooldown_counter": 10
  }
}
```

![sac_success_13.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/66150009-f3f9-3497-da30-1b012f93d0ce.gif)

Fighterの待機方法には、小旋回以外に、SAMの射程外を舐めるように巻く大旋回もあります。何が原因となって待機方法の違いが生じているのかは不明です。

```
{
  "mission_id": "mission_4",
  "mission_condition": "d1",
  "result": "success",
  "fighter_1": {
    "alive": 1,
    "firing_range": 11.5
  },
  "fighter_2": {
    "alive": 1,
    "firing_range": 11.0
  },
  "fighter_3": {
    "alive": 1,
    "firing_range": 13.0
  },
  "sam_1": {
    "alive": 0,
    "firing_range": 0,
    "cooldown_on": 1,
    "cooldown_max_count": 10,
    "cooldown_counter": 10
  }
}
```


![sac_success_16.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/d0e7f9de-e357-c068-10ea-bda1770b5295.gif)

# （その１８）に続く
今回達成できたノミナルなミッション成功率が 44% は低すぎます。また、Cool down time への要求が約１分３７秒というのも長すぎます。（その18）では、報酬シェーピングにより、他のミッション並みに成功率が向上し、数秒の Cool down time であっても、成功プランが生成できるようになることを示します。

# 過去記事へのリンク
- [ランド研究所の「機械学習による航空支配」を実装する（その1）：レポートのまとめ](https://qiita.com/DreamMakerAi/private/95e0aad16e450cb0c53d)
- [ランド研究所の「機械学習による航空支配」を実装する（その2）：1次元問題について](https://qiita.com/DreamMakerAi/items/72c90f9df5339dedd9cd)
- [ランド研究所の「機械学習による航空支配」を実装する（その3）： 1D simulator for GAN と Random mission planner の実装）](https://qiita.com/DreamMakerAi/private/1a0e25a9f673e531ee6f)
- [ランド研究所の「機械学習による航空支配」を実装する（その4）： conditional GAN の実装とトレーニング](https://qiita.com/DreamMakerAi/private/3a416d5cb64d9b7e86db)
- [ランド研究所の「機械学習による航空支配」を実装する（その5）：トレーニング結果の分析](https://qiita.com/DreamMakerAi/items/6c78181b71c470632fcf)
- [ランド研究所の「機械学習による航空支配」を実装する（その６）：トレーニング・データの重要性と GAN の性能向上](https://qiita.com/DreamMakerAi/private/b8ae6c9afd28e90b5b5c)
- [ランド研究所の「機械学習による航空支配」を実装する（その７）：1D simulator for RL の実装](https://qiita.com/DreamMakerAi/private/f008fa9981c3ad74c3cc)
- [ランド研究所の「機械学習による航空支配」を実装する（その８）： Stable Baselines による強化学習](https://qiita.com/drafts/37e6a18810b83d759d81/edit)
- [ランド研究所の「機械学習による航空支配」を実装する（その９）： 少し複雑な環境](https://qiita.com/DreamMakerAi/private/f4d3e35c07ab053608ea)
- [ランド研究所の「機械学習による航空支配」を実装する（その１０）：GAN / 強化学習プランナーの連携を考える](https://qiita.com/DreamMakerAi/private/6cb416e42fc35a7a425c)
- [ランド研究所の「機械学習による航空支配」を実装する（その１１）： ２次元問題の概要](https://qiita.com/DreamMakerAi/private/3572afde356ba5719123)
- [ランド研究所の「機械学習による航空支配」を実装する（その１２）： 2D simulator for mission_1 の実装](https://qiita.com/DreamMakerAi/private/af22d730bf8cbff23fdd)
- [ランド研究所の「機械学習による航空支配」を実装する（その１３）： 2D 問題 mission_1 を強化学習する](https://qiita.com/DreamMakerAi/private/2922e4ad82e8e8c2a059)
- [ランド研究所の「機械学習による航空支配」を実装する（その１４）： Relative Observation による mission planner の能力アップ](https://qiita.com/DreamMakerAi/private/fb2b9971c0894d4cc49b)
- [ランド研究所の「機械学習による航空支配」を実装する（その１５）： 2D問題 mission_2](https://qiita.com/DreamMakerAi/items/63f6313247cd778030dd)
- [ランド研究所の「機械学習による航空支配」を実装する（その１６）： 2D問題 mission_3](https://qiita.com/DreamMakerAi/private/815fc203798f4ea14701)