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

#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_3' フォルダです。強化学習アルゴリズムは、[SAC](https://arxiv.org/abs/1801.01290) を用いました。

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

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

#2. Mission 3

ミッション３は、Fighter 3 機を使って、SAM 1 基を攻撃するミッションです。ただし、SAM の射程よりも長いウェポンを搭載している Fighter は 3 機の内 1 機のみとします。したがって、プランナー（エージェント）は、適切な 1 機を 3 機の中から選んで、SAM を攻撃するプランを生成しなければなりません。

SAM の射程よりも長いウェポンを搭載している Fighter は、下記コードで、エピソード毎に3機の内からランダムに設定します。

    def condition_f1(self, fighter_1, fighter_2, fighter_3, sam):
        sam_firing_range_list = np.arange(sam.min_firing_range,
                                          sam.max_firing_range + self.resolution, self.resolution)
        sam_firing_range_list = sam_firing_range_list[1:-1]
        sam.firing_range = np.random.choice(sam_firing_range_list)

        long_weapon_fighter = np.random.choice(['fighter_1', 'fighter_2', 'fighter_3'])
        if long_weapon_fighter == 'fighter_1':
            fighter_1.long_weapon = 1
            fighter_2.long_weapon = 0
            fighter_3.long_weapon = 0
        elif long_weapon_fighter == 'fighter_2':
            fighter_1.long_weapon = 0
            fighter_2.long_weapon = 1
            fighter_3.long_weapon = 0
        elif long_weapon_fighter == 'fighter_3':
            fighter_1.long_weapon = 0
            fighter_2.long_weapon = 0
            fighter_3.long_weapon = 1

        condition_1 = self.condition_f1_fighter(self.fighter_1, sam)
        condition_2 = self.condition_f1_fighter(self.fighter_2, sam)
        condition_3 = self.condition_f1_fighter(self.fighter_3, sam)
        condition = condition_1 or condition_2 or condition_3

        if not condition:
            raise Exception('Error in condition mission f1!')


#3. 相対観測量とアクション
#3.1 相対観測量
マルコフ決定過程（MDP: Markov Decision Process）を規定するためのミッション３の相対観測量は下図の距離（ベクトルのノルム）、速度（ベクトルのノルム）、角度で規定できます。全部で 37 観測量です。これにより、下図を慣性空間で並行移動しても回転移動しても、同じ観測量が観測されるので、絶対量で観測した場合に比べ、ずっと小さな観測空間で済みます。この結果、ネットワークサイズが小さくて済み、学習も早くなることが期待できます。

![relative_geometry_mission_3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/84694fde-0f7b-a3d6-2466-4e43101916b6.jpeg)

これは、表で書くと以下になります。プランナーは、この観測量に含まれている SAM の射程と3機の Fighter の射程情報をもとに適切なアクション（プラン）を生成することを学習します。

![observation table mission 3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/d0289f45-9019-79e3-a19c-0063f1fe0587.jpeg)

コードでは以下の通りです。

    def get_relative_observation_mission_3(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)

        return obs

観測量が多くなってきたので、RLLIB の default ネットワーク（unit数 256, 256 の２層 MLP）で大丈夫かと少し心配しましたが、問題はありませんでした。


#3.2 アクション
アクションは、3機の Fighter のヘディングの１タイムステップでの変化量（の正規化値）ですので、コードでは以下となります。

```
self.fighter_1.action = actions[0] * self.fighter_1.max_heading_change_step
self.fighter_2.action = actions[1] * self.fighter_2.max_heading_change_step
self.fighter_3.action = actions[2] * self.fighter_3.max_heading_change_step
```


#4. 学習条件

エピソードの初期状態として、SAM を戦場中心 (50, 50) km に配置し、その周りの円環上に、SAM に対してヘディングエラーを持った 3 機の Fighter をランダムに配置します。具体的には、以下の設定でプランナーの学習を行いました。

- Fighters initial position: SAM から半径 [40, 50] km の円環上
- Fighters initial heading error: SAM に対し [-20, 20] deg のずれ

コードでは、下記になります。

```
    def set_initial_condition_fighter(self, fighter):
        r = 40 + 10 * np.random.rand()  # default
        # r = 40 + 50 * np.random.rand()

        c = np.random.rand()
        if c < .5:
            theta = - np.pi * np.random.rand()
            fighter.x = 50 + r * np.sin(theta)
            fighter.y = 50 + r * np.cos(theta)
            fighter.heading = - (-np.pi - theta)
        else:
            theta = np.pi * np.random.rand()
            fighter.x = 50 + r * np.sin(theta)
            fighter.y = 50 + r * np.cos(theta)
            fighter.heading = - (np.pi - theta)

        heading_error = 20 * np.pi / 180 * (np.random.rand() - .5) * 2  # default
        # heading_error = 180 * np.pi / 180 * (np.random.rand() - .5) * 2
        fighter.heading += heading_error
        fighter.heading_sin = np.sin(fighter.heading)
        fighter.heading_cos = np.cos(fighter.heading)
```

#5. 学習履歴

この学習は割と簡単らしく、収束は早いです。

![episode reward mean_trial.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/09026a2e-41fc-597e-e341-9952a211f2ef.jpeg)

学習が進むのに合わせて、エピソード長も短くなります。

![episode length mean_trail.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/2869deae-3bc6-24f5-cc15-064a2cc2df2e.jpeg)

ランダムに生成した初期条件の下で、1000エピソードで成功率を計算したのが下図です。安定して100％近い成功率を達成できています。

![learning_history.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/4ab093b2-f6fa-6aea-aa57-20937f33d26f.png)




#6. 学習結果と汎化能力

初期位置が、学習時の円環から外側に離れていくと、未経験の初期状態になるので成功率が低くなって行きます。下図に示すように、この低下が思ったよりも大きかったのですが、原因は、（後出の生成プラン例に示しますが）、SAM の攻撃に使わない２機が SAM から離反する動きをするため、攻撃機が SAM に到達する前に、SAM の攻撃に使わない２機が戦場エリアを逸脱し、エピソード終了となってしまうエピソードが増えるためでした。射程の短い Fighter を SAM の攻撃に使うようなミスは僅かでした（後出の生成プラン例に示すように、ゼロではありませんでした）。したがって、戦場をもっと広くとってシミュレーション時間を確保すれば成功率はもっと高くなる（つまり、実際の成功率はもっと高い）はずですが、ここでは学習時と同じ値に設定しています。

![Mission 3_ Robustness to initial position of Fighters.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/86fa9b80-4d29-1a25-8085-82b2e865de49.png)

![Mission 3_ Robustness to initial heading error.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/32f0ebe1-0d0b-0d79-732e-6d012fb61c74.png)

##6.1 Nominal Condition での生成プラン例

以下の動画では、3つの青い丸がFighterを、その周りの薄い青い円が搭載ウェポンの射程を表します。また、赤い丸がSAM、その周りの薄い赤い円がSAMの射程を表します。

###6.1.1 Success

典型的なプランは、３機の Fighter の内、SAM の射程よりも長いウェポンを搭載した Fighter 1機だけを SAM に対して進出させ、残りの二機は SAM から離隔したところで待機させるプランです。

```
{
  "mission_id": "mission_3",
  "mission_condition": "f1",
  "result": "success",
  "fighter_1": {
    "alive": 1,
    "firing_range": 15.5
  },
  "fighter_2": {
    "alive": 1,
    "firing_range": 18.5
  },
  "fighter_3": {
    "alive": 1,
    "firing_range": 7.5
  },
  "sam_1": {
    "alive": 0,
    "firing_range": 16.5
  }
}
```
![sac_success_1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/eb1a03b2-2531-562c-ed83-2d16073fbed9.gif)

```
{
  "mission_id": "mission_3",
  "mission_condition": "f1",
  "result": "success",
  "fighter_1": {
    "alive": 1,
    "firing_range": 16.5
  },
  "fighter_2": {
    "alive": 1,
    "firing_range": 7.5
  },
  "fighter_3": {
    "alive": 1,
    "firing_range": 8.0
  },
  "sam_1": {
    "alive": 0,
    "firing_range": 12.0
  }
}
```

![sac_success_19.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/96e2e623-d663-75ee-0617-91b7271bd512.gif)



SAM を攻撃できるウェポンを搭載した Fighter と近い位置に別の Fighter がいるような初期条件では、最初は両機を SAM に接近させますが、最終的に正しい Fighter のみを進出させるプランを生成することがありました。

```
{
  "mission_id": "mission_3",
  "mission_condition": "f1",
  "result": "success",
  "fighter_1": {
    "alive": 1,
    "firing_range": 12.0
  },
  "fighter_2": {
    "alive": 1,
    "firing_range": 10.0
  },
  "fighter_3": {
    "alive": 1,
    "firing_range": 20.0
  },
  "sam_1": {
    "alive": 0,
    "firing_range": 19.0
  }
}
```

![sac_success_11.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/395ab37a-b55c-781e-b282-ff84e425c5b0.gif)


###6.1.2 Fail

典型的な失敗プランの例です。SAM を攻撃できるウェポンを搭載した Fighter が、SAM に接近する前に、他の Fighter が戦域を離脱して、エピソード終了となっています。SAM を攻撃できるウェポンを搭載した Fighter は、SAM に近づいて行っている途中なので、戦域がもっと広ければ、このプランは成功した可能性があります。

```
{
  "mission_id": "mission_3",
  "mission_condition": "f1",
  "result": "fail",
  "fighter_1": {
    "alive": 1,
    "firing_range": 19.5
  },
  "fighter_2": {
    "alive": 1,
    "firing_range": 20.5
  },
  "fighter_3": {
    "alive": 1,
    "firing_range": 21.0
  },
  "sam_1": {
    "alive": 1,
    "firing_range": 20.5
  }
}
```

![sac_fail_9.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/4fd1f068-e391-aabb-2662-9d5a48871594.gif)




##6.2 Robustness to Initial Position and heading error of Fighters の生成プラン例

Fighter ３機の初期位置を、学習時の [40,50] km から外挿方向の [60, 90] km、ヘディングエラーを [-20, 20] deg から [-180, 180] deg にした時の生成プランの例を示します。

以下の動画では、3つの青い丸が Fighter を、その周りの薄い青い円が搭載ウェポンの射程を表します。また、赤い丸が SAM、その周りの薄い赤い円が SAM の射程を表します。

###6.2.1 Success

```
{
  "mission_id": "mission_3",
  "mission_condition": "f1",
  "result": "success",
  "fighter_1": {
    "alive": 1,
    "firing_range": 19.5
  },
  "fighter_2": {
    "alive": 1,
    "firing_range": 11.5
  },
  "fighter_3": {
    "alive": 1,
    "firing_range": 9.5
  },
  "sam_1": {
    "alive": 0,
    "firing_range": 11.5
  }
}
```

![sac_success_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/a642c9a6-aa0b-c843-e08d-9aa6e3abae66.gif)

```
{
  "mission_id": "mission_3",
  "mission_condition": "f1",
  "result": "success",
  "fighter_1": {
    "alive": 1,
    "firing_range": 9.5
  },
  "fighter_2": {
    "alive": 1,
    "firing_range": 21.0
  },
  "fighter_3": {
    "alive": 1,
    "firing_range": 8.0
  },
  "sam_1": {
    "alive": 0,
    "firing_range": 18.5
  }
}
```

![sac_success_24.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/c842f4d1-ba02-e06c-4b0a-81844694d3f0.gif)

```
{
  "mission_id": "mission_3",
  "mission_condition": "f1",
  "result": "success",
  "fighter_1": {
    "alive": 1,
    "firing_range": 12.5
  },
  "fighter_2": {
    "alive": 1,
    "firing_range": 10.5
  },
  "fighter_3": {
    "alive": 1,
    "firing_range": 20.5
  },
  "sam_1": {
    "alive": 0,
    "firing_range": 19.0
  }
}
```

![sac_success_39.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/84fbc877-9ba7-8913-1fba-7f3c2f8a3b67.gif)


###6.2.2 Fail

これは、失敗というよりも、SAM の攻撃に使わない Fighter が戦場離脱したために、ミッション途中でエピソードが終了してしまい、結果として、ミッション失敗に分類されている例です。

```
{
  "mission_id": "mission_3",
  "mission_condition": "f1",
  "result": "fail",
  "fighter_1": {
    "alive": 1,
    "firing_range": 17.0
  },
  "fighter_2": {
    "alive": 1,
    "firing_range": 7.5
  },
  "fighter_3": {
    "alive": 1,
    "firing_range": 21.0
  },
  "sam_1": {
    "alive": 1,
    "firing_range": 19.5
  }
}
```

![sac_fail_9.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/9f73a3a3-98cf-3cc8-60ba-9e863df59002.gif)

これは、珍しく、誤った Fighter を選択して SAM に近づけてしまった失敗例です。初期条件が、学習時の条件を外挿する方向の場合、このような失敗がわずかに発生するようになります。

```
{
  "mission_id": "mission_3",
  "mission_condition": "f1",
  "result": "fail",
  "fighter_1": {
    "alive": 1,
    "firing_range": 11.5
  },
  "fighter_2": {
    "alive": 1,
    "firing_range": 20.0
  },
  "fighter_3": {
    "alive": 0,
    "firing_range": 8.5
  },
  "sam_1": {
    "alive": 1,
    "firing_range": 16.5
  }
}
```

![sac_fail_21.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366937/ceacdaae-a733-192b-0149-99602e3c38a5.gif)


#7. まとめ
複数の Fighter から適切な Fighter を選択して SAM を攻撃するプランナーで学習できました。


#（その１７）へ続く
（その１７）では、最後のミッションとして、デコイを使うミッション・プランナーを強化学習します。

# 過去記事へのリンク
- [ランド研究所の「機械学習による航空支配」を実装する（その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)