#1. はじめに
本記事は、ランド研究所の「機械学習による航空支配」を実装する(その14)です。
(その13)では、ランド研究所のレポートに沿って強化学習プランナー(Agent)を強化学習させましたが、学習が上手く進みませんでした。この原因は、ネットワーク・サイズに比べて観測空間が大きくなりすぎたためと推定されます。この場合の王道(たぶんランド研究所が採った方法)は、ネットサイズを大きくして、学習を長く行うことなのですが、ここでは観測量を絶対量から相対量に変更することでネットサイズを大きくしなくても問題に対処できることを示します。
強化学習ツールとしては、1D 問題と同様に、Stable Baselinesを使用しました。強化学習アルゴリズムは、ランド研究所が PPO, 2017 と A3C, 2016 を使っていたので、それ以外の調べておいた方がよさそうな SAC: Soft Actor-Critic, 2018 を使いました。
使用した環境コードは、下記 GitHub の 'myenv_2D_R_v1' フォルダ、トレーニング用コードは 'training_sac-v1.py' です。
#2. 相対観測量
Fighter と SAM の関係は、下図に示すように Fighter と SAM の相対位置ベクトルと Fighter の速度ベクトル:
\vec{r}_{FS}, \quad \vec{v}_F \quad
を使って記述することができます(SAM は静止しているものとします)。
上図の関係は、下式に示す相対位置ベクトル、速度ベクトルの大きさと角度を使って観測することが出来ます。角度は、右手系でとっています。この観測量は、FighterとSAMの絶対位置が、絶対座標(より正確には、慣性座標)上で、同時に平行移動しても、回転移動しても、同一の観測量になります。したがって、絶対量で観測した場合に比べ、相対量で観測すると観測空間は大幅に小さくなることが期待できます。
|\vec{r}_{FS}|, \quad |\vec{v}_F|, \quad θ_F
この考え方に従って、Fighter, Jammer, SAM の幾何学関係を規定する相対観測量は下図としました。これにより、交戦の観測量を xy- 座標系とは無関係に、3 つのエンティティ(Fighter, Jammer, SAM)の相対的な観測量だけで表すことが出来ます。つまり、下図を並行移動しても回転移動させても、相対位置ベクトルと速度ベクトルの大きさと角度は変わらないので、観測量は全く同じになって、観測空間のサイズが絶対量で観測する場合に比べずっと小さくて済みます。
ランド研究所のレポートと同様に、速度は一定と仮定しているので、速度の大きさ自体は観測量には含めませんでした。
角度については、角度が 360 度のところで不連続に変わってしまうと嫌なので、sin( ), cos( ) で変換したものを観測量としました。これにより、連続的且つ一意的に角度を指定できます。(余談ですが、有名な OpenAI のMulti Agent Hide & Seak でも、Agent のベアリングを sin( ), cos( ) で表しています)。したがって、観測は以下の 20 要素のベクトルになります。この中には、alive や on のように厳密には実数値ではなく単なる flag もあるのですが、(連続観測空間と離散観測空間が混在した状態で学習させるのは、手間がかかりそうだったので)、全て実数観測値として扱っています。表の右端の列が、観測量の[最小値、最大値]です。
コードでは、以下の部分が、相対観測量に対応します。
def get_relative_observation_mission_1(self):
obs = []
r = (self.space_x ** 2 + self.space_y ** 2) ** 0.5
v_f = self.make_v(self.fighter_1)
r_fs = self.make_r(self.sam_1, self.fighter_1)
sin_f, cos_f = self.compute_heading_error(v_f, r_fs)
obs.append(sin_f)
obs.append(cos_f)
obs.append(np.linalg.norm(r_fs, ord=2) / r)
v_j = self.make_v(self.jammer_1)
r_js = self.make_r(self.sam_1, self.jammer_1)
sin_j, cos_j = self.compute_heading_error(v_j, r_js)
obs.append(sin_j)
obs.append(cos_j)
obs.append(np.linalg.norm(r_js, ord=2) / r)
r_fj = self.make_r(self.jammer_1, self.fighter_1)
sin_phai_f, cos_phai_f = self.compute_heading_error(v_f, r_fj)
sin_phai_j, cos_phai_j = self.compute_heading_error(v_j, r_fj)
obs.append(sin_phai_f)
obs.append(cos_phai_f)
obs.append(sin_phai_j)
obs.append(cos_phai_j)
obs.append(np.linalg.norm(r_fj, 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.jammer_1.alive)
obs.append(self.jammer_1.on)
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
#3. Mission 1 の学習条件
(その13)では、学習がちゃんと進むように、Fighter, Jammer の最大旋回率をかなり低く設定していましたが、本方法では常識的な値を設定しても大丈夫でした。
Fighter の最大旋回率は「第111回:空対空戦闘4 戦闘機の旋回性能と人間の限界」に、「F-15(機体重量35,000ポンド時)の維持旋回能力は海面高度上において480kt(マッハ0.73) のときに21度/秒であり、 燃料が切れない限り永久に毎秒 21 度で旋回することができます。ちなみにこの時に 9G が掛かります。恐らく機体構造の制限が無ければさらに高い旋回率を発揮できたでしょう。」と書いてあったので、(特に有人機を想定する必要はなかったのですが、他に適当な数字が無かったので)、きりの良い数値として 20 [deg/sec] としました。また、Jammer は、Fighter ほどの機体性能を有してはいないと思われるので、単純に半分の 10 [deg/sec] としました。
Fighter 最大旋回率: 20 deg/sec
Jammer 最大旋回率: 10 deg/sec
実際のヘディング角の変更量は、エージェントの出力を使って、下記で計算しています。(ここは、観測量が絶対量の時と同じです)。
if self.mission_id == 'mission_1':
self.fighter_1.action = actions[0] * self.fighter_1.max_heading_change_step
self.jammer_1.action = actions[1] * self.jammer_1.max_heading_change_step
学習のための Fighter, Jammer の初期位置は、下記のように幅 50±10 km のリングの中で、一様にランダム配置しました。また、初期ヘディング・エラーは、Fighter, Jammer ともに [-20, 20] [deg] の範囲で一様にランダム選択しました。
報酬は、ミッション成功時のみ 1、それ以外の時(ミッション失敗時やエンティティが境界外に出た時)は -1 を与えました。これは、1D 問題の時と同じです。コードでは以下(の 'w2' の部分)です。('w1', 'w3', 'l1', 'l2' は今回は使用しません)。
def get_reward_mission_1(self, done, fighter, jammer, sam):
reward = 0
# For done
if done:
if (self.mission_condition == 'w1') and (fighter.alive > .5) and \
(jammer.alive > .5) and (jammer.on < .5) and (sam.alive < .5):
reward = 1
elif (self.mission_condition == 'w2') and (fighter.alive > .5) and \
(jammer.alive > .5) and (jammer.on > .5) and (sam.alive < .5):
reward = 1
elif (self.mission_condition == 'w3') and (fighter.alive > .5) and \
(jammer.alive > .5) and (jammer.on < .5) and (sam.alive < .5):
reward = 1
elif (self.mission_condition == 'l1') and (fighter.alive > .5) and (jammer.alive > .5):
reward = 1
elif (self.mission_condition == 'l2') and (fighter.alive > .5) and (jammer.alive > .5):
reward = 1
else:
reward = -1
return reward
#4. 学習履歴と学習結果
学習履歴は下記に示します。赤い薄いラインは1000 テスト・エピソードの成功率の生値、濃いラインがその指数移動平均(EMA: Exponential Moving Average)です。
学習したエージェントを使って、学習時と同じランダム初期条件でテストしたところ、ミッション成功率は 100% (1000 success / 1000 test episodes)でした。
以下に、プランナーが生成したプランの例を示します。以下の動画では、濃紺の丸が Fighter、半透明で濃紺の大きな円が Fighter の射程、緑色の丸が Jammer、半透明で緑色の大きな円が Jammer の有効レンジ、赤の丸が SAM 配備位置 、半透明で赤色の大きな円が SAM の射程を表しています。SAMの射程を表す円は、ジャミングを受けると、それに対応した大きさの円に変わります。
##4.1 初期状態において、SAM に対し Jammer の方が Fighter よりも遠くにいる時
この生成プランでは、Fighter は、SAM の射程の外側で小さく旋回して待機し、Jammmer が進出して来るのを待ちます。そして、Jammer が SAM に近づいてくると、Jammer と Fighter を一緒に接近させます。若干、早目に SAM に接近した Fighter は、SAM を巻くように大きく旋回し、Jamming が有効になって SAM の射程が縮退すると、SAM に接近してSAMを撃破します。
{
"idx": 1,
"model_name": "./models/myenv_2D_R-v0-sac/sac_32_w2_2/best_model.zip",
"Mission": "Success with using Jammer",
"mission_condition": "w2",
"fighter_1": {
"alive": 1,
"initial (x,y)": [
16.290643905586187,
65.61297009004439
],
"final (x,y)": [
39.408720637496835,
48.54601353974328
],
"firing_range": 11.0
},
"jammer_1": {
"alive": 1,
"initial (x,y)": [
18.03895369665821,
111.98490336442276
],
"final (x,y)": [
39.23786310027692,
58.80075641275203
],
"jam_range": 18.0,
"jamming": 1
},
"sam_1": {
"alive": 0,
"(x,y)": [
50.0,
50.0
],
"firing_range": 14.5,
"jammed_firing_range": 10.149999999999999
}
}
##4.2 初期状態において、SAM に対しFighter の方が Jammer よりも遠くにいる時
Jammer は、SAM の周りを旋回して Fighter が進出してくるのを待ち、Fighter が近づいてくると、Jamming が有効になる距離まで SAM に近づきます。そこに Fighter が進出して SAM を撃破します。
{
"idx": 6,
"model_name": "./models/myenv_2D_R-v0-sac/sac_32_w2_2/best_model.zip",
"Mission": "Success with using Jammer",
"mission_condition": "w2",
"fighter_1": {
"alive": 1,
"initial (x,y)": [
42.487109271873486,
-17.343008309031475
],
"final (x,y)": [
44.2408859815987,
42.529923857539885
],
"firing_range": 9.5
},
"jammer_1": {
"alive": 1,
"initial (x,y)": [
84.14842296420865,
72.6804594067764
],
"final (x,y)": [
37.92792521824332,
51.10797464634628
],
"jam_range": 18.0,
"jamming": 1
},
"sam_1": {
"alive": 0,
"(x,y)": [
50.0,
50.0
],
"firing_range": 10.0,
"jammed_firing_range": 7.0
}
}
##4.3 初期状態において、SAM に対し Fighter と Jammer が同じくらいの距離にいる時
このプラン例では、先にSAMに近づいた Fighter が SAM の射程外を大きく巻いて、Jammer の進出を待っています。最終的なタイミングの微調整は、SAM に近づいてから行っています。
{
"idx": 25,
"model_name": "./models/myenv_2D_R-v0-sac/sac_32_w2_2/best_model.zip",
"Mission": "Success with using Jammer",
"mission_condition": "w2",
"fighter_1": {
"alive": 1,
"initial (x,y)": [
75.19214464426548,
106.3402063447949
],
"final (x,y)": [
54.205697685268404,
58.67277707553389
],
"firing_range": 10.0
},
"jammer_1": {
"alive": 1,
"initial (x,y)": [
66.49571033291458,
-5.172382601117313
],
"final (x,y)": [
64.68439365486097,
49.196253659443705
],
"jam_range": 18.0,
"jamming": 1
},
"sam_1": {
"alive": 0,
"(x,y)": [
50.0,
50.0
],
"firing_range": 11.5,
"jammed_firing_range": 8.049999999999999
}
}
#5. 学習したプランナーの汎化能力
学習したプランナー(エージェント)の未知の環境条件に対する汎化能力を見るために、環境条件を学習時の範囲外に振って、つまり外挿した条件で、プランナーの汎化能力(エージェントのロバスト性)を測ります。
##5.1 初期位置を遠くにする
学習時の Fighter, Jammer の初期位置は、SAM から[40, 60] km の範囲でした。これを、60km 以遠 に初期位置を設定して性能を確認したのが下記の図表です。横軸は、初期状態における Fighter, Jammer の SAM からの距離 (km) になります。縦軸は、1000 のテストエピソードでのミッション成功率です。初期位置が 90km ぐらいまでは、性能劣化はほとんどありませんでした。
Fighter, Jammer の初期位置を、SAM から r=160km とした場合の、プラン例を示します。
(1)成功例
先に Jammer が進出し、Jammer がある程度SAMに近づくと Fighter が進出します。Fighter が十分に進出してくるまで、Jammer は SAM の周りを大きく巻いて待機します。Fighter が十分に進出してきたら、Jammer は Jamming が有効になる距離まで SAM に近づきます。ほかの例も確認したのですが、Jammer がある程度 SAM に近づくのがトリガーとなって Fighter が進出し始めることが多いようです。理由は判りません(ゴメンナサイ)。
{
"idx": 8,
"model_name": "./models/myenv_2D_R-v0-sac/sac_32_w2_2/best_model.zip",
"Mission": "Success with using Jammer",
"mission_condition": "w2",
"fighter_1": {
"alive": 1,
"initial (x,y)": [
193.7781661857786,
120.19856785045286
],
"final (x,y)": [
62.92827477777151,
47.77256917960481
],
"firing_range": 13.5
},
"jammer_1": {
"alive": 1,
"initial (x,y)": [
36.995049465330574,
-109.47059685594331
],
"final (x,y)": [
51.421812117735186,
66.30860643726915
],
"jam_range": 18.0,
"jamming": 1
},
"sam_1": {
"alive": 0,
"(x,y)": [
50.0,
50.0
],
"firing_range": 17.5,
"jammed_firing_range": 12.25
}
}
(2)失敗例
Fighter, Jammer ともに進出せずに、旋回しているだけでした。
{
"idx": 5,
"model_name": "./models/myenv_2D_R-v0-sac/sac_32_w2_2/best_model.zip",
"Mission": "Failed",
"mission_condition": "w2",
"fighter_1": {
"alive": 1,
"initial (x,y)": [
83.33524195165572,
-106.48885469586827
],
"final (x,y)": [
175.31450770019796,
1.1123966959273262
],
"firing_range": 14.5
},
"jammer_1": {
"alive": 1,
"initial (x,y)": [
-34.144471665587375,
-86.0871334407451
],
"final (x,y)": [
-10.732119060928731,
-80.10759031621869
],
"jam_range": 18.0,
"jamming": 0
},
"sam_1": {
"alive": 1,
"(x,y)": [
50.0,
50.0
],
"firing_range": 17.0,
"jammed_firing_range": 11.899999999999999
}
}
##5.2 イニシャル・ヘディングエラーを大きくする。
学習時の Fighter, Jammer の初期ヘディングエラーは、SAM に対し[-20, 20] deg の範囲でした。これを、外挿方向に振って性能を確認したのが、下記の図表です。性能劣化は全くありませんでした。これは、少しばかり驚いたのですが、学習時に、いろいろなアクションを採る中で、様々なヘディングエラーに対応することを学ぶことが出来たためではないかと推測しています。(逆に言えば、イニシャル・ヘディングエラー無しで学習しても、十分な汎化能力が得られる可能性があります)。Fighter と Jammer の SAM からの初期離隔は 40km です。
この場合のプラン例を示します。初期ヘディングエラー = 180 deg で、SAM とは全く逆の方向を向いているのですが、よく見ると即座にヘディングを修正しているのが判ります。
{
"idx": 1,
"model_name": "./models/myenv_2D_R-v0-sac/sac_32_w2_2/best_model.zip",
"Mission": "Success with using Jammer",
"mission_condition": "w2",
"fighter_1": {
"alive": 1,
"initial (x,y)": [
38.30465022310934,
11.74795700101518
],
"final (x,y)": [
49.6074760540142,
35.30152161747661
],
"firing_range": 15.0
},
"jammer_1": {
"alive": 1,
"initial (x,y)": [
54.86767477485702,
89.70271706427937
],
"final (x,y)": [
48.03158294037904,
65.8783869065184
],
"jam_range": 18.0,
"jamming": 1
},
"sam_1": {
"alive": 0,
"(x,y)": [
50.0,
50.0
],
"firing_range": 17.5,
"jammed_firing_range": 12.25
}
}
##5.3 Fighter と Jammer の初期離隔距離を大きくする。
学習時の Fighter, Jammer の初期離隔距離は、[-10, 10] km の範囲内でランダムです。これを振って、Fighter, Jammer の初期離隔距離の変動に対するロバスト性を測ったのが、下図表です。Fighter(又は Jammer)初期位置を、SAMから 40 km に固定し、Jammer(又は Fighter)初期位置を、SAMから離隔させて性能劣化を確認しました。SAMからの初期離隔距離が 100 km のところまでチェックしましたが、Fighter を後方に下げても、Jammer を後方に下げても、成功率の劣化は全く見られませんでした。初期離隔として、これだけの距離が許容されるのであれば、古典的な経路プログラミングによって、SAM の近傍まで Fighter, Jammer を投入するようプランニングした後、強化学習プランナーに切り替えれば、かなりの状況をカバーできるのではないかと思います。
###5.3.1 Jammer の初期位置を SAM から遠ざけた時
生成プランの例を示します。
- Fighter 初期位置 = (SAMから r=40 km)
- Jammer 初期位置 = (SAMから r=100 km)
Fighter は Jammer が近づいてくるのを、小さく旋回して待ってから、Jammer と共に進出します。進出のタイミングの微調整は、SAM に近づいてから行います。
{
"idx": 4,
"model_name": "./models/myenv_2D_R-v0-sac/sac_32_w2_2/best_model.zip",
"Mission": "Success with using Jammer",
"mission_condition": "w2",
"fighter_1": {
"alive": 1,
"initial (x,y)": [
10.112963305829638,
53.00404789540828
],
"final (x,y)": [
38.887969549217736,
42.43441345353661
],
"firing_range": 13.5
},
"jammer_1": {
"alive": 1,
"initial (x,y)": [
-4.064458262297208,
134.12511130931313
],
"final (x,y)": [
40.06774679233228,
61.891315048779944
],
"jam_range": 18.0,
"jamming": 1
},
"sam_1": {
"alive": 0,
"(x,y)": [
50.0,
50.0
],
"firing_range": 14.5,
"jammed_firing_range": 10.149999999999999
}
}
###5.3.2 Fighter の初期位置を SAM から遠ざけた時
生成したプラン例を示します。ミッションの初期条件は以下です。
- Fighter 初期位置=SAM から r= 100 km
- Jammer 初期位置=SAM から r= 40 km
Jammer は SAM の周りを大きく旋回しつつ Fighter の進出を待ちます。Jammer は、Fighter が十分に進出したら、jamming が有効になるところまで進出します。
{
"idx": 4,
"model_name": "./models/myenv_2D_R-v0-sac/sac_32_w2_2/best_model.zip",
"Mission": "Success with using Jammer",
"mission_condition": "w2",
"fighter_1": {
"alive": 1,
"initial (x,y)": [
-49.30055056676437,
61.80680554330734
],
"final (x,y)": [
42.21938082760699,
56.5308450769031
],
"firing_range": 10.5
},
"jammer_1": {
"alive": 1,
"initial (x,y)": [
83.52658710910188,
71.81668986844255
],
"final (x,y)": [
44.37320638806996,
37.58358640664463
],
"jam_range": 18.0,
"jamming": 1
},
"sam_1": {
"alive": 0,
"(x,y)": [
50.0,
50.0
],
"firing_range": 12.5,
"jammed_firing_range": 8.75
}
}
#6. まとめ
以上をまとめると、
-
MDPを規定している状態空間に対し、どういった量を観測するかは学習の速度等に非常にインパクトがある。観測を絶対量から相対量にするだけで、観測空間がかなり小さくなるので、デフォルトの小さなネットワークでも学習ができる。
-
学習したエージェントは、Fighter, Jammer の初期位置変動、イニシャル・ヘディングエラー変動、初期離隔変動に対し非常にロバストである。特に、イニシャル・ヘディングエラー変動と初期離隔変動に対しては、極めてロバストである。
-
これらのことから、Fighter, Jammer を、例えば古典的な経路プログラミングで SAM の近傍まで持っていった後に、強化学習エージェントのプランに切り替えれば、かなりの状況に対応が出来るのではないかと思いました。
-
Fighter と Jammer では、異なる待機の仕方をする傾向が見られました。Jammer の初期位置が遠方の場合、Fighter は小さく旋回して Jammer を待つプランが生成される傾向があります。一方、Fighter の初期位置が遠方の場合は、Jammer は SAM を巻くように大きく旋回して待つようなプランが生成されます。この傾向が、今回の乱数系列に特有のものなのか、何らかの理由で必ずそうなるのかは不明です。
-
不満点は、なぜこういったプランを生成するのか全く判らない点です。
(その15)に続く
(その15)では、別のミッションに対してエージェントを強化学習します。
#過去記事へのリンク
- ランド研究所の「機械学習による航空支配」を実装する(その1):レポートのまとめ
- ランド研究所の「機械学習による航空支配」を実装する(その2):1次元問題について
- ランド研究所の「機械学習による航空支配」を実装する(その3): 1D simulator for GAN と Random mission planner の実装)
- ランド研究所の「機械学習による航空支配」を実装する(その4): conditional GAN の実装とトレーニング
- ランド研究所の「機械学習による航空支配」を実装する(その5):トレーニング結果の分析
- ランド研究所の「機械学習による航空支配」を実装する(その6):トレーニング・データの重要性と GAN の性能向上
- ランド研究所の「機械学習による航空支配」を実装する(その7):1D simulator for RL の実装
- ランド研究所の「機械学習による航空支配」を実装する(その8): Stable Baselines による強化学習
- ランド研究所の「機械学習による航空支配」を実装する(その9): 少し複雑な環境
- ランド研究所の「機械学習による航空支配」を実装する(その10):GAN / 強化学習プランナーの連携を考える
- ランド研究所の「機械学習による航空支配」を実装する(その11): 2次元問題の概要
- ランド研究所の「機械学習による航空支配」を実装する(その12): 2D simulator for mission_1 の実装
- ランド研究所の「機械学習による航空支配」を実装する(その13): 2D問題 mission_1 を強化学習する