強化学習で上昇気流を捉える（そこまで行き着けるか）

# 背景
パラグライダーは大気の影響を大きく受ける。それも含めて楽しみの一部だが、できることなら長く滞空していたい（状況により早く降りたいときも、、）、、
上手い人は地形、大気の状態、木の葉の揺れ、雲の発生状況などから理解できない脳内処理を行い長く、遠く飛びますが、普通の人には無理です、、
せめて、サーマル（上昇気流）にヒットした際に長くそこに留まる示唆が強化学習で得られないものか？と思った次第

# 前提
1. セルは6角形
1. 進行方向に向かって正面、右斜前、左斜め前しか進めない
1. 真ん中にサーマルエリア
1. サーマルエリアでないところからスタート（サーマルを通り過ぎた体）
1. サーマルエリアにたどり着いたらゴール（最終的にはセンタリングするところまでやりたい）

# 環境の描画

こんな感じのエリア。6角系のセルはゲームでよくあるので簡単に描画できる何かがあると思っていたものの、思い通りのものはなかった。
仕方がないのでMatplotlibで頑張って描画
真ん中の一番黄色い7つのセルがサーマルエリアの想定

![env.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328600/f02f4e08-f921-0619-0fb6-2bdf0247049b.png)

```python:6角形の環境表示
import matplotlib.pyplot as plt
markersize = 42
markershape = 'h'
color = 'red'
color2 = 'yellow'
plt.plot(0, 0, markershape, markersize=markersize, markerfacecolor=[0,0,0])
plt.plot(-2, 0, markershape, markersize=markersize, markerfacecolor=[0,0,0])
plt.plot(-4, 0, markershape, markersize=markersize, markerfacecolor=[0,0,0])

plt.plot(-1, 3, markershape, markersize=markersize, markerfacecolor=[0.5, 0, 0.5])
plt.plot(-3, 3, markershape, markersize=markersize, markerfacecolor=color)
plt.plot(-5, 3, markershape, markersize=markersize, markerfacecolor=color)

plt.plot(-1, -3, markershape, markersize=markersize, markerfacecolor=color2)
plt.plot(-3, -3, markershape, markersize=markersize, markerfacecolor=color2)
plt.plot(-5, -3, markershape, markersize=markersize, markerfacecolor=color2)

plt.xlim(-8, 8)
plt.ylim(-10, 10)
plt.show()
```
実行結果
![hexmap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328600/11c9673c-7092-14b7-6fda-0230ade5be9f.png)

あとは色と数値を計算結果に合わせて調整して表示すれば見やすいに違いない
面倒なのは
- マーカーの大きさを都度確認しなければならない（指定数値は全体に対する比率ではない）
- マップの作り方により、セル表示位置のずらし方・幅を調整しなければならない。（上記はY軸が奇数のときにちょっと左に寄せている）
- 色を表現するときに0から1の間に数値を変換しなければならない

# 状態の設定
普通のグリッドとは異なり、（X軸、Y軸、進行方向）で表現する。
その後で取れるアクションは、状態にある進行方向に依存して決まる（全6方向のうち3方向が選択可能なアクション）
以下がイメージ
![進行方向.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328600/e8513d75-e465-620c-8fab-d9ca784adc3c.png)


```python:状態(airspace)の設定
self.states = [(x, y, direc) for x in range(-4, 5, 1)
       for y in range(-4, 5, 1) for direc in range(6)]

```

```python:アクション,policyの定義とvalueの初期化
# 最初の要素は進行方向,2つ目が取りうる方向
self.actions  = {0:[5,0,1],1:[0,1,2],2:[1,2,3],3:[2,3,4],4:[3,4,5],5:[4,5,0]}

# 場所、進行方向、取りうる進行方向の三つがキー、選択可能性を値（ポリシー）として辞書を作成
        self.policy={}

for s in self.states:
  for a in self.actions.keys():
    self.policy[(s, (self.actions[a][0]))]=.3
    self.policy[(s, (self.actions[a][1]))]=.4
    self.policy[(s, (self.actions[a][2]))]=.3
        
self.value={}
  for s in self.states:
    self.value[s]=0
```
# アクションと報酬の定義

6角形の座標なので、X軸方向、Y軸方向の動きがトリッキーになる。
今回はY軸が偶数、奇数でX軸の加算方法を変える

```python:アクションと報酬の定義
def move(self, s, a):
    s_place=(s[0],s[1])
    # とりあえずアップストリームに入ったらゴールとする
    # 稼働確認したら4回以上アップストリームに入ったらゴールにする
    if s_place in self.upstreams:
        return 0,s
    
    # Y軸が偶数の場合、右上はX軸＋1、Y軸＋1
    # Y軸が奇数の場合、右上はX軸＋0、Y軸＋1
    if s[1]%2==0:
        x_shift_right=1
        x_shift_left=0
    else:
        x_shift_right=0
        x_shift_left=1

    if a==0:
        s_new=(s[0]+x_shift_right,s[1]+1,a)
    elif a==1:
        s_new=(s[0]+1,s[1],a)
    elif a==2:
        s_new=(s[0]+x_shift_right,s[1]-1,a)
    elif a==3:
        s_new=(s[0]-x_shift_left,s[1]-1,a)
    elif a==4:
        s_new=(s[0]-1,s[1],a)
    elif a==5:
        s_new=(s[0]-x_shift_left,s[1]+1,a)

    # 空域内にいなくなった場合、動かず
    if s_new not in self.states:
        return 0,s
    # 空域がダウンストリームだった場合は今回設定せず
    
    # 上記のどの場合でもない場合、報酬はマイナスで移動
    return -1,s_new
```
# ポリシーの設定
参考書読んでもゆっくりついていかないと迷子になる辺り、、
すべての状態に対して、次の動きに対する報酬を計算
計算する際に、動きに対して設定した掛け目を（正面が0.4、それ以外が0.3）、元の動きに対する報酬(sに対するr)と新しい状態(s_new)の価値に対してgamma(今回は1）を乗じたものを加算したものに乗じる（文字にするとわかりにくく、あっているかどうかもわからない、、）
状態の価値の変化量がdelta(今回は0.01）よりも小さくなったら終了

```python:状態価値関数
while True:
    delta_max=0
    for s in air_space.states:
        v_new=0
        # 進行方向に合わせたアクションを取得
        cell_actions=air_space.actions[s[2]]
        for a in cell_actions:
            r,s_new=air_space.move(s,a)
            v_new += air_space.policy[(s,a)]*(r+gamma*air_space.value[s_new])
        delta_max=max(delta_max,abs(air_space.value[s]-v_new))
        air_space.value[s]=v_new
    if delta_max < delta:
        break
```
これで出てくるはず（祈り）
# 結果
お、出た！
![2021411161331544953.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328600/1184312e-3db8-54c2-c420-43794e33eea2.png)

スタートを(2,2,0)（2,2座標で進行方向が０、サーマルから飛び出して2セル進んでしまった想定）とすると、移動方向は5が良さげ（値は-88.67)。
状態は(2,3,5)になるので、次の状態は(1,3,4)(値は-84.87)
すると次は(0,2,3)(値は-72.9)がよくて、(0,1,3)でサーマルに入ってゴール！と思えば直感的にまっとうな気がするが、この解釈で良いのか？これが動的計画法ってことでよいのか？はゆっくり確認する

![結果_Inked2021411161331544953_LI.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328600/61c7b820-bbb7-c6a1-dd99-d5ad013454a4.jpeg)

# このあと、、
- できればベストの方向を表示させたい
- サーマルエリアに4回以上滞空出来たらゴールとしたい（サーマルエリアを突っ切って出てしまわないように
- いつの日か実際の距離感に合わせてみたい
- そしてさらにいつの日か携帯デバイスとして飛びながらベストな方向を教えてくれるようにしたい


# 参考
あちこちつまみ食いのように見ながらも、以下の書籍をメインで参照
まだ入り口なのでもう少し追って見る予定
比較的最近の本なのでコードがそのまま動いて良かった
[ITエンジニアのための強化学習理論入門](https://amzn.to/3fXoAAh)