q, qnet, gru, lstm のコーディングに間違いはないのでしょうか?その確認をするためには、Q値が正しい値に収束するかをチェックすることが大切です。
また、短期記憶ユニット(gru/lstm)の短期記憶の能力はどれくらいなのでしょうか。定量的に知りたいです。
このようなことを調べるためには、タスクはできるだけシンプルな方がよいでしょう。sawamptour はかなりシンプルですがまだQ値を調べるには状態数が多く複雑です。そこで、状態数が究極に少ない「廊下タスク」を考えました。sim_corridor.py です。
この章では、この廊下タスクを使って各アルゴリズムのパフォーマンスをチェックします。lstmとgruについては、この両者のパフォーマンスは類似していましたが、感覚的にパフォーマンスがよいと思われるgruのみをチェックしました。
sim_corridor の使い方
使い方は、sim_swamptour と同じです。
[code]
> python sim_corridor.py
[/code]
で、実行方法が表示されます。
[code]
—- 使い方 —————————————
3つのパラメータを指定して実行します
> python sim_corridor.py [agt_type] [task_type] [process_type]
[agt_type] :q, qnet, lstm, gru
[task_type] : L8g23v, L8g23, L8g34, L8g67, L8g34567
[process_type] :learn/L, more/M, graph/G, anime/A
例 > python sim_corridor.py q L8g23v L
—————————————————
[/code]
タスクは、L8g23v, L8g23, L8g34, L8g67, L8g34567, の5種類がすぐに使えます。
全てのタスクは、1次元のフィールドでプレイします。ゴールの位置(青)を覚えて、そこまで移動し[右に移動]し、[脱出]すると報酬がもらえます。L8g23v 以外は、ゴールの位置が表示されるのは開始時のみなので、位置を覚えなくてはなりません。タスクによってゴールの出現位置が変わります。
以下は、L8g23v をq に学習させたときのアニメーションです。うまく青のゴールで脱出できていることが分かります。
以下は、L8g23 をgru に学習させたときのアニメーションです。ゴールは途中で消えてしまいますが、エージェントはその場所を覚えて脱出していることが分かります。
sim_corridor.py と同様、以下の(1)と(2)で、env_corridor.py に新しいタスクを追加することもできます。
(1) env_coridor.py の列挙型class TaskType に mytask を追加(名前は任意)。
class TaskType(Enum): """ タスクタイプの列挙型 """ L8g23v = auto() L8g23 = auto() L8g34 = auto() L8g67 = auto() L8g34567 = auto() # mytask = auto() # オリジナルタスクを追加
(2) mytask のパラメータを、class Env の set_task_type() に追加。
class Env(core.coreEnv): def __init__(): --- 省略 elif task_type == TaskType.L8g34567: self.field_length = 8 self.goal_candidate = (3, 4, 5, 6, 7) self.pos_start = 0 self.reward_fail = 0 self.reward_move = 0 self.reward_goal = 1 self.step_until_goal_hidden = 1 # 以下のように追加 """ elif task_type == TaskType.mytask: # mytask の設定追加 self.field_length = 20 self.goal_candidate = list(range(10, 20)) self.pos_start = 0 self.reward_fail = 0 self.reward_move = 0 self.reward_goal = 1 self.step_until_goal_hidden = 1 """
シミュレーションのパラメータ(一回で行うステップ数 N_STEP、終了条件 EARY_STOP_STEP 等)もデフォルト値以外で設定した場合には、sim_corridor.py の以下のコードの後に、myTaskの設定を追加します。
--- 省略 elif task_type == TaskType.L8g34567: N_STEP = 5000 SHOW_Q_INTERVAL =200 EARY_STOP_STEP = None EARY_STOP_REWARD = 1 AGT_EPSILON = 0.4 AGT_ANIME_EPSILON = 0.0 # 以下のように追加 """ elif task_type == TaskType.mytask: # mytaskのシミュレーションパラメータ追加 N_STEP = 5000 SHOW_Q_INTERVAL =200 EARY_STOP_STEP = None EARY_STOP_REWARD = 1 AGT_EPSILON = 0.4 AGT_ANIME_EPSILON = 0.0 """
Q値の理論値
L8g23v のQの理論値を考えてみます。[math]\gamma=0.9[/math]を想定します。まず、ゴールの位置が2に出た時のエピソードを考えます。復習ですが、Q値 [math]Q(x, a)[/math] は、観測[math]x[/math]を受け取って行動[math]a[/math]を出力した後から受け取る報酬の和です。報酬の和は、割引報酬率を乗算したそのエピソード内での和となります。
[math]
\begin{eqnarray}
Q(x(t), a(t)) & \approx & r(t) \\
& & + \gamma r(t+1) \\
& & + \gamma^2 r(t+2) + \cdots
\end{eqnarray}
[/math]
これは次のx(t + 1)のQ値を使って以下のように表すことができます。
[math]
\begin{eqnarray}
Q(x(t), a(t)) & \approx & r(t) \\
& & + \gamma \max_{a’} Q(x(t+1), a’)
\end{eqnarray}
[/math]
では、L8g23v を考えましょう。下の図のように、[math]t=0, 1, 2[/math] でロボットが位置2のところまで進んだことを想定します。
t=2 の観測xの時に「脱出」を選ぶと報酬 1 を受け取って終了しますので Q(x, 脱出)=1 です。「右へ」を選んだ場合には、これ以降報酬は出ないので Q(x, 右へ)=0 となります。
この1ステップ前の t=1 の観測xでは、「脱出」を選ぶと Q(x, 脱出)=0 です。「右へ」を選んだ場合には、図に示したように、Q(x, 右へ)= [math]r(t=1) + \gamma r(t=2) [/math]となり、0.9 となります。
このように考えて、2ステップ前では、Q(x, 脱出)=0で、Q(x, 右へ)=0.81 となります。
ゴールの位置が3だったときもこのように考えて、以下のように数値が求まります。
では、L8g23での理論値はどうなるでしょうか。L8g23ではゴールが見えなくなりますが、エージェントがゴールの位置を区別することができるなら、Q値の理論値はL8g23v と同じになるはずです。
Q値を学習できるか L8g23v
q のパフォーマンス
それではQの収束を見てみます。
[code]
(mRL) python sim_corridor.py q L8g23v L
[/code]
でL8g23v のシミュレーションを開始します。
シミュレーションの間、以下のようにQ値が表示されます。
左の[ ]の中がobservation を表しており、その中の”1″は、ロボットの位置を示します。”3″がゴール、”0″が何もないセルです。右側がQ(0)とQ(1)の値です。矢印は大きい方を指しています。
Qの値を見ると、完全に理論値と同じ値に収束していることが分かります。Q学習のアルゴリズムはよさそうですね。
[code]
observation : Q(0), Q(1)
[1 0 3 0 0 0 0 0] : 0.00 –> 0.81 # (0.00, 0.81)
[0 1 3 0 0 0 0 0] : 0.00 –> 0.90 # (0.00, 0.90)
[0 0 1 0 0 0 0 0] : 1.00 <– 0.00 # (1.00, 0.00)
[0 0 3 1 0 0 0 0] : 0.00 –> 0.00 # (0.00, 0.00)
[1 0 0 3 0 0 0 0] : 0.00 –> 0.73 # (0.00, 0.73)
[0 1 0 3 0 0 0 0] : 0.00 –> 0.81 # (0.00, 0.81)
[0 0 1 3 0 0 0 0] : 0.00 –> 0.90 # (0.00, 0.90)
[0 0 0 1 0 0 0 0] : 1.00 <– 0.00 # (1.00, 0.00)
[/code]
qnet のパフォーマンス
それではqnet はどうでしょうか。
[code]
(mRL) python sim_corridor.py qnet L8g23v L
[/code]
でL8g23v のシミュレーションを開始します。
[code]
observation : Q(0), Q(1)
[1 0 3 0 0 0 0 0] : -0.00 –> 0.81 # (0.00, 0.81)
[0 1 3 0 0 0 0 0] : -0.00 –> 0.91 # (0.00, 0.90)
[0 0 1 0 0 0 0 0] : 1.00 <– 0.01 # (1.00, 0.00)
[0 0 3 1 0 0 0 0] : -0.00 –> 0.01 # (0.00, 0.00)
[1 0 0 3 0 0 0 0] : -0.01 –> 0.73 # (0.00, 0.73)
[0 1 0 3 0 0 0 0] : -0.00 –> 0.81 # (0.00, 0.81)
[0 0 1 3 0 0 0 0] : 0.00 –> 0.90 # (0.00, 0.90)
[0 0 0 1 0 0 0 0] : 1.00 <– 0.02 # (1.00, 0.00)
[/code]
# は正しい値です。比較のために記載しています。
0.01程度の誤差は見られますが、正しいQ値に収束していると言ってよいでしょう。
ニューラルネットを使っているので、学習によってデータにない入力に対する出力も変化します。その影響が出ているのではないかと思います。qのQ-table ではそのような影響はありませんでした。
gru のパフォーマンス
gruとlstm はほぼ同じなので、ここでは gruの結果を見ていきたいと思います。
[code]
observation : Q(0), Q(1)
[1 0 3 0 0 0 0 0] : -0.00 –> 0.81 # (0.00, 0.81)
[0 1 3 0 0 0 0 0] : -0.09 –> 0.86 # (0.00, 0.90)
[0 0 1 0 0 0 0 0] : 0.92 <– 0.01 # (1.00, 0.00)
[0 0 3 1 0 0 0 0] : -0.00 <– -0.06 # (0.00, 0.00)
[1 0 0 3 0 0 0 0] : 0.01 –> 0.68 # (0.00, 0.73)
[0 1 0 3 0 0 0 0] : -0.05 –> 0.80 # (0.00, 0.81)
[0 0 1 3 0 0 0 0] : 0.01 –> 0.92 # (0.00, 0.90)
[0 0 0 1 0 0 0 0] : 0.97 <– -0.24 # (1.00, 0.00)
[/code]
# は正しい値です。比較のために記載しています。
完全には一致していませんが、それらしい値にはなっています。qnet に比べると、過去の履歴にも影響されるせいか、若干不安定なところがあるようです。
短期記憶のテスト L8g23
L8g23 は、ゴールの位置が初めにしか提示されません。そのため同じ観測値に対しても、提示されたゴールの位置によって行動を変える必要があります。
q のパフォーマンス
まず、q でこのタスクを試したときのQ値です。qは短期記憶の機能がないのでうまくできません。
[code]
observation : Q(0), Q(1)
位置0 1 2 3 4 5 6 7
A0[1 0 3 0 0 0 0 0] : 0.00 –> 0.37 # (0.00, 0.81)
B0[0 1 0 0 0 0 0 0] : 0.00 –> 0.45 # (0.00, 0.90)
C0[0 0 1 0 0 0 0 0] : 0.59 <– 0.40 # (1.00, 0.00)
D0[0 0 0 1 0 0 0 0] : 0.57 <– 0.00 # (0.00, 0.00)
A1[1 0 0 3 0 0 0 0] : 0.00 –> 0.37 # (0.00, 0.73)
B1[0 1 0 0 0 0 0 0] : 0.00 –> 0.45 # (0.00, 0.81)
C1[0 0 1 0 0 0 0 0] : 0.59 <– 0.40 # (0.00, 0.90)
D1[0 0 0 1 0 0 0 0] : 0.57 <– 0.00 # (1.00, 0.00)
[/code]
例えば、C0とC1は、同じ観測ですがC0は2の位置にゴールが提示された後であり、C1は3の位置にゴールが提示された後なのでC0の時には、「脱出」を選ぶと報酬が1であり、C1の時には「脱出」を選んでしまうと報酬が0になります。しかし、C0とC1のQ値は同じ値となっています。
同様に、B0とB1、D0とD1でも観測が同じ値ですのでQ値も同じになります。A0とA1の観測値はゴールが見えているので違うのですが、そこから先の未来が同じになるので、同じQ値に収束しています。
gru のパフォーマンス
それでは、短期記憶ユニットを使っているgru はどうでしょうか。15000ステップ学習を行った後のQ値です。
[code]
observation : Q(0), Q(1)
位置0 1 2 3 4 5 6 7
A0[1 0 3 0 0 0 0 0] : -0.01 –> 0.80 # (0.00, 0.81)
B0[0 1 0 0 0 0 0 0] : -0.00 –> 0.92 # (0.00, 0.90)
C0[0 0 1 0 0 0 0 0] : 1.02 <– -0.05 # (1.00, 0.00)
D0[0 0 0 1 0 0 0 0] : -0.00 –> 0.01 # (0.00, 0.00)
A1[1 0 0 3 0 0 0 0] : -0.08 –> 0.71 # (0.00, 0.73)
B1[0 1 0 0 0 0 0 0] : 0.02 –> 0.82 # (0.00, 0.81)
C1[0 0 1 0 0 0 0 0] : 0.00 –> 0.85 # (0.00, 0.90)
D1[0 0 0 1 0 0 0 0] : 1.02 <– -0.07 # (1.00, 0.00)
[/code]
同じ観測情報であるB0とB1, C0とC1, D0とD1で異なるQ値が出力されています。そして、それらの値は、かなり理論値に近い値に収束していることが分かります。gru が想定していたように機能していると言えるでしょう。
gru の限界、L8g34, L8g67
L8g23を学習することができたgru でしたが、L8g34, L8g67 ではどうなのでしょうか?L8g23と比べて、記憶を保持する時間が少し長くなるので難易度は少し上がるはずです。
実際にやってみると、できたりできなかったりと、はっきりしない結果になります。
そこで、全ての条件で10000ステップの学習を50回行い、学習が成功する確率をステップ毎にプロットしました(このプロットはmemoryRLに含まれていません。また、L8g45, L8g56はタスクに含まれていません。独自の別なプログラムで計算しました)。
その結果、L8g23では成功する確率は0.8以上であるのに対して、1ステップだけ記憶する時間が増えたL8g34では、成功確率が0.5まで下がってしまうことが分かりました。さらに、L8g45だと0.1程度になってしまい、L8g56, L8g57 だと成功確率はほぼ0となってしまいました。
この時のシミュレーションは、入力層の次の中間層は20個のLeLUユニット、次の層はgruユニット20個、そして次が出力となっていました。
この数を多くしたら成功確率が上がるかもしれないとL8g45でユニットを多くした条件で試しましたが、成功確率は上がりませんでした。
※n20r20: LeLU 20個、gru 20個, n40r40: LeLU 40個、gru 40個, n40r80: LeLU 40個、gru 80個 を表す。
以上のことから、この最終層をgruに置き換えただけのモデルは、短期記憶はできるけれどもかなり簡単な場合にしかできないということが分かりました。これはlstm でも同じでした。
タスクには、L8g34567 というL8g67よりももっと難しいものも準備してありますが、当然、これを解くことはできません。
さて、最後にまとめです。
この記事では、通常のQ学習、ニューラルネットワークを使ったQ学習、短期記憶付きの強化学習を紹介し、それぞれができるタスク、できないタスクを明らかにしました。
特に短期記憶付き強化学習gru, lstm では、従来の強化学習できないTmaze_bothとTmaze_either ができることを示しました。
そして、短期記憶を保持しなくてはいけないステップ数が伸びると、急激に成功確率が減ることを示しました。L8g34567 や ruin_2swamp のようなタスクを解くかが今後の課題です。みなさんもぜひチャレンジしてみてください。
おわり