強化学習事始め(Open AIのgymを使って手っ取り早く始める)

数式は、一切挟まない解説(と言うか主に実装)です。
強化学習の中でもQ-learningの一番単純な話です。

数式込みでちゃんと理解したい人は以下の記事をお勧めします。
強化学習とは?(What is Reinforcement Learning?)
また、最新の話題も知りたいよって人は以下の2nd editionが良いと思います。
http://webdocs.cs.ualberta.ca/~sutton/book/the-book.html

概要

強化学習とは、環境(Environment)との相互作用を通してエージェントを学習させる手法です。
ポイントは、プログラムの書き手が事細かにエージェントに指示を出さなくても、勝手に学習してくれる点です。

ざっくり書くと、エージェントの行動(action) に対して,状況(state)が変化し、報酬(reward)が支払われ、
エージェントは、試行錯誤の末、将来貰える報酬が最大になるように学習します。

例題(Open AIのFrozenLake)に基づく解説

今回題材として扱うのは、以下のFrozenLake-v0です。
gym.openai.com
環境(env)は、OpenAI側が引き取ってくれるので、エージェントの実装のみに注力できます。

FrozenLake-v0の問題設定

  • ペンギンがいて、碁盤の目状の氷の上からの脱出を目指します。
  • 碁盤の目は、縦4、横4マスです(state(s)は、16パターン)。
  • ペンギンは、上下左右へ1マス移動できます(=action(a)は、4パターン)。

エージェントの学習

  • Q(s,a)は、64パターン(16*4)になります。
    • Q(s,a)は、ある状態(s)の時にとった行動(a)により得られる(将来に渡る)報酬です。
  • ある状態(s)のもと、ある行動(a)をとることで、報酬が得られた場合は、次回もその行動を取りやすくするために、Q(s,a)は増加します。
  • 逆に、ある行動(a)をとった次の状態(s+1)で得られる報酬(Q(s+1,a))が、現在の状態(s)で得られる報酬(Q(s,a))よりも小さいのであれば、Q(s,a)を減じてその行動を慎むようにします。

実装 (ここを読んで頂くのが一番手っ取り早いかも)

# -*- coding: utf-8 -*-
#!/usr/bin/env python

import gym;
import numpy as np;
import matplotlib.pyplot as plt; #結果をグラフ描画しないなら不要

env = gym.make('FrozenLake-v0'); #環境の読み込み
"""
環境(Environment)は、以下の通り。

SFFF       (S: starting point, safe)
FHFH       (F: frozen surface, safe)
FFFH       (H: hole, fall to your doom)
HFFG       (G: goal, where the frisbee is located)
"""

#状態(state)は、16種、行動(action)は、4種 なので、16行4列
Q = np.zeros([env.observation_space.n,
              env.action_space.n]);

lr = 0.7; #学習率(=learning rate) (小さい(0.0)と遅いし、大きい(1.0)と局所解に陥りやすい)
disct= 0.99; #報酬のディスカウント(discount)

history = []; #グラフにプロットするためなので、グラフ描画しないなら不要

#state :s
#action : a
for i in range(2000): #2,000回ほど繰り返す。
    s = env.reset(); #環境を初期化し、初期状態を返却する (本問題では初期状態は、0)
    done = False;
    
    #学習
    for _ in range(300): # 1episode中の試行は、300回まで
        #選び方は、貪欲アルゴリズム
        #ランダム(np.random.randn)がエージェントの試行錯誤を表す。
        #学習が進むとrandomな行動をとることは慎むように(i+1)で割っている。
        a = np.argmax(Q[s,:] + np.random.randn(1,env.action_space.n)*(1.0/(i+1)));

        #試行が終了した場合は、done = True (goalにたどり着いた or 穴に落ちた)
        #ゴールできた場合は、reward = 1.0、その他は0.0
        s_1,reward,done,info = env.step(a); #今回、infoは利用しない

        #Qテーブルの更新 (ここが、学習の肝)
        Q[s,a] = (1-lr) * Q[s,a] + lr * (reward + disct * np.max(Q[s_1,:]));

        """
        以下のように展開した方が理解しやすい(かも知れない)
        Q[s,a] = Q[s,a] + lr * ( reward + disct * np.max(Q[s_1,:]) - Q[s,a]);

        成功していた場合は、rewardが1.0なので、次回もその行動を取りやすいくなるように、Q[s,a]は、加算される
        もし、とった行動(a)により、将来の報酬(Q[s_1,:])が減るようであれば、
        次回は、その行動は取りづらくなるように、Q[s,a]は減じられる
        (ただし、報酬が発生するようなら加算に転じることもある)
        """
 
        s = s_1;
        
        if done == True:
            break;
        
    history.append(reward); #ゴールできていれば1.0。その他は0.0

plt.plot(history);
plt.show();

結果

結果は、以下の通りです。
横軸は、試行回数(episodeの数)で、縦軸は、成功(1)or失敗(0)です。
はじめの方は、失敗ばかりですが、200回あたりからは、失敗はほぼなくなっていることがわかると思います。
また、700回付近で、スランプ(ちょっと失敗している)が見られますが、以降は安定しています。
f:id:marmarossa:20170326230638p:plain

ランダムな要素ありで学習しているので、得られるグラフは毎回異なります。