OpenAI Gym を触ってみる

OpenAI Gymなる強化学習用プラットフォームを触ってみました(参考: PyConJPのプレゼンテーション)。

インストール自体はpip install gymで一発です(Atariゲームなどを扱いたい場合はpip install gym[atari]のようにサブパッケージをインストールする必要があるようです)。一応ドキュメントで使い方は説明されていますが、若干戸惑う点があったので随時補足します。

Atariゲームなど色々面白そうな環境がありますが、とりあえずFrozenLake(4x4, 8x8)というのが初心者向けっぽいので、これを試してみました。

ルールは非常に単純で、固定配置のマップ上でスタートから穴に落ちずにゴールに辿り着くだけです。成功時1点、失敗時0点の報酬が得られます。マップ上の記号の意味は以下の通り:

記号 意味
S スタート
F
H
G ゴール

ただしスタート地点を含む全ての床は凍っているため、必ずしも指定した方向に動いてくれるとは限りません。また、説明文には書かれていませんが実はターン制限があるため、あまりターンをかけすぎると報酬が得られなくなります。

OpenAI Gymはあくまで強化学習用という位置付けなので、この問題も強化学習によって解くことが期待されているようです。そのためか、環境における状態や行動の値の意味(例えば0が左への移動に相当するなど)も明示されてはいません。しかし入門ということで今回はそれも調べた上で色々試してみようと思います。

環境の状態空間および行動空間は以下のようにして調べられます:

>>> env = gym.make("FrozenLake-v0")
>>> env.observation_space
Discrete(16)
>>> env.action_space
Discrete(4)

Discrete(n)は {0, 1, ..., n-1} を表します。よって、ここでは状態空間が0から15までの16通り、行動空間が0から3までの4通りとなります。大体察しがついていると思いますが、状態はマス目のインデックス、行動は4通りの移動方向を表しています。またenv.render()を呼ぶと直前に指定した移動方向も含めて表示してくれるので、これを見れば値と移動方向の関係もわかります(0:L, 1:D, 2:R, 3:U)。参考までに4x4版をインタラクティブに遊べるスクリプトを置いておきます。

少し人手でプレイしてみるとわかりますが、指定した方向に動いてくれないことが多いです。法則がわかるまでは穴に落ちまくると思います。

ここで身も蓋もないですがソースコードを見て法則を調べてみます。すると:

if is_slippery:
    for b in [(a-1)%4, a, (a+1)%4]:
        newrow, newcol = inc(row, col, b)
        # ...
        li.append((1.0/3.0, newstate, rew, done))

というコード(is_slipperyは常に真)があるので、4を法として指定した方向の±1へずれる可能性があり、3方向とも確率は1/3であることがわかります(流し読みなので違ったらスミマセン)。言い換えると、指定方向から±90度ずれる可能性はありますが、180度ずれることはありません。つまり「行く方向は指定できないが、行かない方向は指定できる」ということになります(行きたくない方向の逆を指定すればよい)。

これを踏まえて4x4版のマップ配置を見てみます:

SFFF
FHFH
FFFH
HFFG

隣接する穴が1つだけなら必ず回避できるので、これなら割と高確率でクリアできそうです。ただし隣接する穴が2つ以上になると死ぬ可能性があるので、ターン制限を考えなくても100%クリアするのは無理と思われます。隣接する穴が2つ以上あるマスを「危険マス」と呼ぶことにします(ここでは0-basedで座標 (2,1) がそれにあたります)。

この程度であれば人力で解く(各マスでの最適解を求める)のも難しくなさそうなので、とりあえずそれを書いてみます。穴に落ちるのを回避することと、開幕は危険マスを避けるため下に進むことに注意して各マスでの最適解を書き出してみると:

LUUU
L_L_
UDL_
_RD_

こんな感じでしょうか(厳密に最適かどうかは自信ありませんが)。これをそのまま実装して提出してみたところ、とりあえず要求水準(いい所取り100エピソードで平均報酬0.78以上)はクリアしているようです*1

ここでターン制限について注意しておきます。問題文にはターン制限の説明がありませんが、モニタで提出用データを記録する際はターン制限を超過すると失敗とみなされます(提出したスクリプトではターン制限を考慮していないため、モニタなしの方が成績が良くなります)。ターン制限は環境ごとに異なり、以下のようにして調べられます:

>>> env.spec.timestep_limit
100

4x4版と8x8版でも制限ターンが異なるので、必ず上記のように取得する必要があるようです。

次は8x8版(制限ターン200)を考えてみます。マップはこのようになっています:

SFFFFFFF
FFFFFFFF
FFFHFFFF
FFFFFHFF
FFFHFFFF
FHHFFFHF
FHFFHFHF
FFFHFFFG

見た感じ、ターン制限を考えなければ右上からまっすぐ下へ進むルートで100%クリアできそうです。そこで適当に実装したものを提出してみましたが、これだとターン制限なしなら100%クリアできるものの、ターン制限ありだと微妙に足りないようです(いい所取り100エピソードで平均報酬0.99以上が必要だが、0.97止まり)。

他の人の結果を見ると(学習込みではありますが)クリアまでに短くても10000エピソード以上かかっているので、単純に試行回数不足というのも考えられますが、それだけではなさそうです。ターン制限を考慮した上でアルゴリズムを変えると成績が結構変わります。例えば開幕から右上に到達するまでずっと上を指定し、右上に到達したらずっと右を指定する、という方法でもターン制限がなければ100%クリアできますが、ターン制限を考慮すると10000エピソードでの平均報酬が0.71程度しか出なくなります(提出したものでは 0.88 程度)。

8x8版は解が自明とは言いがたいようなので、次回はQ学習による解法を試してみたいと思います。

*1:AIでもなんでもないので、こういうのを提出していいのかどうかはよくわかりませんが