0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ランド研究所の「機械学習による航空支配」を実装する(その17): 2D問題 mission_4

Last updated at Posted at 2021-07-10

#1. はじめに

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

Air Dominance Through Machine Learning: A Preliminary Exploration of Artificial Intelligence–Assisted Mission Planning, 2020

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

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

強化学習アルゴリズムは、これまでと同じく、SAC を用いました。

#2. Mission 4

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

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

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

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

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

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

#3. 相対観測量

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

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

relative_geometry_mission_4.jpg

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

relative observation table.jpg

なお、今回のミッションでは、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を撃破できた時に1、それ以外の時には−1を与えました。

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、約1分37秒に相当するので、現実問題としては長過ぎる値だと思います。

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

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

episode reward mean_trial.jpg

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

episode length mean_trail.jpg

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

ID_2_reward_hist.png

ID_2_reward.png

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

episode_length.png

ID_2_length.png

#6. 性能とロバストネス(汎化能力)

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

ID_2_history.png

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

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

success ration [%] over 1000 episodes.png

#7. 生成プラン例

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

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

SAM の射程に Decoy が進入すると、Decoy は撃墜され、その代わりに Cool down time の間だけ、SAM の射程が0になります。この間に、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

{
  "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

{
  "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

#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

先行して 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

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

(その18)に続く

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

過去記事へのリンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?