夜は短し歩けよ未来大生

未来大生が人工知能を中心に勉強していく上で、学習メモや日記として書いていきます。

夏休みDay n(nは任意の整数)強化学習でFlappy_Birdやってみた

強化学習でFlappy_Birdやってみた(Q学習編)


Playing Flappy Bird by Q_Learning

↑2000回学習させたもの

忙しい人のためにコードはコチラ

記事を読む上での注意

  • 細かい強化学習の説明はできないため書いてありません
  • 勉強中のため色んな本やサイトを参考にコードを書いています
  • 下手な説明
  • 汚いコード
  • DeepLearningとかではないです

以上のことを許容できる方は読んでみてください。

目標

Flappy Birdを強化学習を使って攻略させること。
今回は強化学習の中でもQ学習を使って取り組みます。
以降DQNやDDQN、A3Cなどの他の手法も使って比較していこうと思っています。

概要

OpenAI Gymである程度遊んで飽きてしまい、自分でOpenAI Gym以外のタスクに取り組んでみたかったことからPLE(PyGame Learning Environment)に取り組もうと考えました。PLEとはOpenAI Gymのような強化学習用のシミュレーション環境のことです。今回はその中でもFlappy Birdというゲームに取り組みます。

Flappy Bird Nguyen Ha Dongによるスマートフォン向けゲームアプリ。 2013年5月に公開されて以来、その難易度の高さと中毒性から人気を博していたが、「収入は増えた代わりに日常生活が台無しになった」として削除された。

引用 : Hatena Keyword

使用した環境

環境 Version
OS mac OS X Sierra
Python 3.6.1
Anaconda Anaconda 4.4.0 (x86_64)
numpy 1.15.0
matplotlib 2.0.2
PLE 3.10
pygame 1.9.4
Pillow 4.1.1

PLEが公式にサポートしているのはPython2.7.6になります(2018年8月16日現在)。
他のバージョンでは動作は保証できません。

環境構築

Python, Anacondaの環境はある前提です。ない方は他のサイトなりを参考にしてください。

PLEのインストール

公式の説明はココ(英語)

PLEを実行する上で下記のライブラリが必要となります。

Anacondaを入れられている人はnumpyは入っているはずなので以下のライブラリをそれぞれインストールしましょう。

$ pip install Pillow
$ pip install pygame

以上のライブラリをインストールできたらGitHubリポジトリからcloneしましょう。

$ git clone https://github.com/ntasfi/PyGame-Learning-Environment

cloneが終わったら以下のコマンドを実行します。

$ cd PyGame-Learning-Environment
$ sudo pip install -e .

これでインストールが完了です!

公式のReferenceページにはQuickStartといってPLEのサンプルコードがありますが、Python3系ではライブラリ側に問題があって動きそうにないのでQuickStartするのは諦めましょう。
どうしてもFlappy Birdで遊びたい人はココからFlappy BirdのClone環境をインストールしてきましょう。

コードの解説

コードはココにあるので適宜参考にしてください。

ライブラリのインポート

import math
import copy
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
import os
os.environ["SDL_VIDEODRIVER"] = "dummy"  # ポップアウトウィンドウを表示しない
from ple.games.flappybird import FlappyBird
from ple import PLE
from collections import defaultdict
from ple.games.flappybird import FlappyBird

他のタスクを実行したい場合はここのFlappyBirdの部分を変えましょう。
他のタスク一覧

アニメの作成

moviepyというライブラリを使用しているため以下を実行してください。依存環境としてffmpegが必要です。

$ pip install moviepy
def make_anim(images, fps=60, true_image=False):
    duration = len(images) / fps
    import moviepy.editor as mpy

    def make_frame(t):
        try:
            x = images[int(len(images) / duration * t)]
        except:
            x = images[-1]

        if true_image:
            return x.astype(np.uint8)
        else:
            return ((x + 1) / 2 * 255).astype(np.uint8)

    clip = mpy.VideoClip(make_frame, duration=duration)
    clip.fps = fps
    return clip

特に説明はなし。サイトから丸パクリしました。

グラフの作成

累積報酬とプレイできた時間をグラフに表示します。

def make_graph(reward_per_epoch, lifetime_per_epoch):
    fig, (axL, axR) = plt.subplots(ncols=2, figsize=(10,4))
    axL.set_title('lifetime')
    axL.grid(True)
    axL.plot(lifetime_per_epoch)
    axR.set_title('reward')
    axR.grid(True)
    axR.plot(reward_per_epoch)
    fig.show()

定数の宣言

ETA = 0.5
GAMMA = 0.99
GOAL_FRAME = 1200 

学習率 ETA 0.5
時間割引率 GAMMA 0.99
目標フレーム数 GOAL_FRAME 1200

Agentクラス

実際にゲームをプレイするAgentクラスを宣言します。

class Agent:
    def __init__(self, num_actions):
        self.brain = Brain(num_actions)
    
    def update_Q_function(self, state, action, reward, state_prime):
        self.brain.update_policy(state, action, reward, state_prime)
    
    def get_action(self, state, episode):
        action = self.brain.decide_action(state, episode)
        return action

コンストラクタでAgentの脳みそとなるBrainクラスをインスタンス化します。
update_Q_functionではQ値を更新するupdate_policyを呼び出します。
get_actionではQ値から行動を選択します。

Brainクラス

Agentの脳みその役割を担うBrainクラスを宣言します。

class Brain:
    
    def __init__(self, num_actions):
        self.num_actions = num_actions
        self.q_table = defaultdict(lambda: np.zeros(num_actions))

    def decide_action(self, state, episode):
        #  ε-greedy
        state_idx = self.get_state_idx(state)  # 相対位置の取得
        epsilon = 0.5 * (1 / (episode + 1))
        if epsilon <= np.random.uniform(0, 1):
            action = np.argmax(self.q_table[state_idx])  # Q値が最大の行動を選択する
        else:
            action = np.random.choice(self.num_actions)  # ランダムな行動を選択する
        return action

    def update_policy(self, state, action, reward, state_prime):
        state_idx = self.get_state_idx(state)
        state_prime_idx = self.get_state_idx(state_prime)
        # Q学習を用いてQ値を更新する
        best_q = np.max(self.q_table[state_prime_idx])
        self.q_table[state_idx][action] += ETA * (
            reward + GAMMA * best_q - self.q_table[state_idx][action])
    
    bucket_range_per_feature = {
        'next_next_pipe_bottom_y': 40,
        'next_next_pipe_dist_to_player': 512,
        'next_next_pipe_top_y': 40,
        'next_pipe_bottom_y': 20,
        'next_pipe_dist_to_player': 20,
        'next_pipe_top_y': 20,
        'player_vel': 4,
        'player_y': 16
    }
    
    def get_state_idx(self, state):
        # パイプの絶対位置の代わりに相対位置を使用する
        state = copy.deepcopy(state)
        state['next_next_pipe_bottom_y'] -= state['player_y']
        state['next_next_pipe_top_y'] -= state['player_y']
        state['next_pipe_bottom_y'] -= state['player_y']
        state['next_pipe_top_y'] -= state['player_y']

        # アルファベット順に並び替える
        state_key = [k for k, v in sorted(state.items())]

        # 相対位置を返す
        state_idx = []
        for key in state_key:
            state_idx.append(int(state[key] / self.bucket_range_per_feature[key]))
        return tuple(state_idx)

コンストラクタでは行動の数を引数として、Qテーブルを宣言します。

self.q_table = defaultdict(lambda: np.zeros(num_actions))

ではdafultdictの引数にlambda: np.zeros(num_actions)を取っています。
今回のactionnの数は「ボタンを押してジャンプする」の一つだけなので、num_actionsは1となります。

関数decide_action()ではマルコフ性に従い現在の状態から行動を選択します。
基本的にはQテーブルから行動を決定します。ですが、初期の状態は良いQテーブルができているわけではないので、たまたまQ値が大きくなってしまっている場合もあります。それを避けるためにある確率εでランダムに行動をします(ε-greedy)。 また、εの値はエピソードが増えるにつれて小さくすることで、ランダムに行動を起こす確率を低くします。
\epsilon = 0.5 \times \frac{1}{episode + 1}

関数update_policy()ではQ学習の更新式にしたがってQ値を更新します。($$\eta$$は学習率, \gammaは時間割引率)
[tex:\displaystyle Q{best} = max(Q{前状態})]
[tex:\displaystyle \Q{new-value} += \eta * (reward + \gamma * Q_best - Q{current-value})]

関数get_state_idx()では学習を安定させるために自分(鳥)とパイプとの距離を相対距離とします。
こうすることで状態変数の数を減らすことができ、値の最大値と最小値の差も小さくなります。

Environmentクラス

ゲームをプレイする盤面を表現するEnvironmentクラスを宣言します。

class Environment:
    
    def __init__(self, graph=True):
        self.game = FlappyBird()
        self.env = PLE(self.game, fps=30, display_screen=False)
        self.num_actions = len(self.env.getActionSet()) # 1
        self.agent = Agent(self.num_actions)
        self.graph=graph
    
    def run(self):
        
        from IPython.display import Image, display

        reward_per_epoch = []
        lifetime_per_epoch = []
        PRINT_EVERY_EPISODE = 500
        SHOW_GIF_EVERY_EPISODE = 5000
        NUM_EPISODE = 50000
        for episode in range(0, NUM_EPISODE):
            # 環境のリセット
            self.env.reset_game()
            # record frame
            frames = [self.env.getScreenRGB()]

            # 状態の初期化
            state = self.game.getGameState()
            cum_reward = 0  # このエピソードにおける累積報酬の和
            t = 0

            while not self.env.game_over():

                # 行動の選択
                action = self.agent.get_action(state, episode)

                # 行動を実行し、報酬を得る
                reward = self.env.act(
                        self.env.getActionSet()[action])  
                        # パイプを超えれば、reward +=1 失敗したら reward  -= 5

                frames.append(self.env.getScreenRGB())

                # 累積報酬
                cum_reward += reward

                # 次状態を得る
                state_prime = self.game.getGameState() 

                # Agentの更新
                self.agent.update_Q_function(state, action, reward, state_prime)

                # 次のイテレーションの用意
                state = state_prime
                t += 1
            
            # 500エピソード毎にlogを出力
            if episode % PRINT_EVERY_EPISODE == 0:
                print("Episode %d finished after %f time steps" % (episode, t))
                print("cumulated reward: %f" % cum_reward)
                reward_per_epoch.append(cum_reward)
                lifetime_per_epoch.append(t)
                if len(frames) > GOAL_FRAME:
                    print("len frames:", len(frames))
                    clip = make_anim(frames, fps=60, true_image=True).rotate(-90)
                    display(clip.ipython_display(fps=60, autoplay=1, loop=1))
                    if self.graph == True:
                        make_graph(reward_per_epoch, lifetime_per_epoch)
                    break
                

            # 5000エピソード毎にアニメーションを作成
            if episode % SHOW_GIF_EVERY_EPISODE == 0:
                print("len frames:", len(frames))
                clip = make_anim(frames, fps=60, true_image=True).rotate(-90)
                display(clip.ipython_display(fps=60, autoplay=1, loop=1))

コンストラクタでゲームやAgentのインスタンス化を行います。

関数run()ではメインの学習ループを実行します。
学習グラフを表示するためのlistである、PRINT_EVERY_EPISODEとSHOW_GIF_EVERY_EPISODEを宣言します。 SHOW_GIF_EVERY_EPISODEとNUM_EPISODEはlogやアニメーションを出力するタイミングの設定です。

forループの中では主に環境と状態の初期化、ゲームのプレイを行っています。
ゲームのプレイの簡単な流れとしては、行動を選択->行動から報酬を得る->次状態を受け取り->Agentの更新を行っています。
また、初期設定では500ループ毎に何Stepプレイしたか、累積報酬の2つを出力します。 そのときに目標フレーム数(60fpsなので、20秒なら1200frame)より長いフレーム数プレイしたら学習をストップします。 それと同時に報酬の和とプレイ時間を学習グラフとして出力します。

f:id:tkr1205:20180818234602p:plain
学習グラフ

グラフは500回に一回データを取得しているので、x軸が10の場合5000回目の学習の値です。\n また、lifetimeはframe数なので、y軸÷60をすればプレイ時間(sec)となります。

そして、5000ループ毎にアニメーションを作成します。

実行

特に説明はなし。

flappybird_env = Environment()
flappybird_env.run()

結果

学習0回目

学習10000回目

学習20000回目


Playing Flappy Bird by Q_Learning

コードはGitHub

Jupyter Notebook形式でGitHubにあげているので見てみてね

まとめ・感想

今回OpenAI Gym以外のタスクに初めて挑戦してみました。
最初Q学習でFlappyBirdをプレイするのは難しいと思っていましたが、やってみると案外いけるものだなと思いました。一度20000回まで学習させたときにはメモリが足りなくなってしまい途中で学習が止まっていました。もっとメモリの使用をおさえたり学習の速度を高速にする事が今後の課題です。 強化学習教師あり学習と違って複雑なものが多いように思います。ですが、その分とても勉強していて楽しいです。 まだまだ強化学習も勉強し始めたばかりなので、色んな本屋サイトを参考にしましたがとても良い勉強になったと思います。今度はDQNで同じFlappyBirdのタスクに挑戦してみようかと考えています。

SNS

気軽にフォローしてください。泣いて喜びます。

Twitter:@tkr12051

インターン探しています

現在リモートで働かせていただけるインターン先を探しております。
機械学習分野について勉強中です。
しっかりと扱える言語はPythonのみですが、タスクを与えられれば自分で調べながら完成まで頑張ることをモットーにしています。 以前はインターンにてwebの知識ゼロの状態からFlaskを使ってwebAPIの作成を行いました。
もし興味を持っていただけましたらご連絡ください。

参考文献

つくりながら学ぶ! 深層強化学習 ~PyTorchによる実践プログラミング~
OpenAI Gym
PLE
Q学習
強化学習のキホン
PythonのDefaultDictについて
PLEのインストール
FlappyBirdの動かしかたとQ学習の参考

夏休みDay8,9

昨日完全に記事を書くことを失念してました。

いやーもう完全に忘れてたね、なんならさっきまで忘れてたね(9/11 23:54現在)

ここ2日目はゆっくりPRMLを読んでました

もうね序論から時間かかりまくり

確率密度関数とか共分散行列とか講義でやったのに全然覚えてない。自分の記憶力に呆れるね

そんなんだからめちゃくちゃ時間かかってる。一つの事に時間かかりすぎてやる気が削がれちゃう。 ただでさえ高校時代に数学から逃げてた人間だから基礎がないせいでツライ

 

ただの報告

話は変わって、大学3年生となり周りの人は結構インターンをするわけで、自分もやりたいなーと思ったわけで。夏休み前から結構探してたんですね、でもひとつも採用してもらえる所がなかったんですが昨日やっと見つかりましたぁぁぁ!!!嬉しい!!!

でも時給は発生しません。お金ないです。はい。

はぁ…リモートのアルバイトもないし、ツライです。収入ありません。リモート始まるまで生きてられるかな…ははは…

夏休みDay6

今日は特に何もできなかったやらなかった。

でも、夏休みの明確な目標を立てたからここに書いておくことにする。

  • PRMLを読み切り、出てくる式を自分で導けるようになる。
  • 強化学習を用いて自分よりも強いFlapy Birdを作る 
  • 自分より強いFlapy Birdを攻略するモデルをDQN, DDQN, A3C, A2C, Ape-Xで実装する

今日はお試しに自分だけでDQNを実装してOpenAI Gymに取り組もうかと思ってたんですけど、どうも何から手をつければいいのかわからず、進捗を生み出せませんでした。
どこから手をつければいいのか、何を考えながら実装すればいいのか。。。
やっぱり強化学習教師あり学習とかと違って難しいですね

夏休みDay5(DL4US課題)

今日は明後日までに期限が迫ってきたDL4USの課題に取り組んでいました。

詳しいことは自分で調べてもらうとDL4US( DL4US | Deep Learning for All of Us )が何なのかわかりやすい説明が出てくると思います。

DL4USの資料や課題の詳しい内容はもちろん言ってはいけないと思うので、言える範囲でやったことを言っていこうかなと思います。

今回の内容

今回の内容は画像認識なんかによく使われる技術の解説がメインでした。
具体的には畳み込み層やData Augmentationとか学習のテクニックとかですね。

課題の内容

そして、気になる課題の内容はよくある多クラスの画像分類問題でした。
深層学習ライブラリのチュートリアルとかにもでてくるような有名なデータを使って画像の分類をしました。

自分はまず、何も考えずに畳み込み層と活性化関数だけの単純なネットワークでやってみました。
何も考えずにやると大体50%くらいが限界でした。

そこから自分はRes-Netを参考にネットワークを構築しようと考えました。
具体的にはSkip Connectionを使った、勾配消失問題への対策を考えたネットワークを構築しました。

途中なぜかフィルタの数でエラー吐いてました。。。。issueを見てると全く同じ悩みを抱えてる人がいましたが、自分がどうやって解決できたのかいまいち分かってないので助けてさしあげられません。。。ごめんなさい!!!

学習させた結果は大体70%くらいでした。
ですが、これはエポック数が30で、もう少し精度が伸びそうだったんで今もう一度最大エポック50回で学習しなおしてます。時間がかかりますね。。。。

社会人の人が土日にしかできないーって言ってる人もいましたが、暇な夏休みなのを良いことにばんばん時間を使っていこうかと思います!!!

おまけ

パラメータ 数値
最適化関数 SGD+Momentum
学習率 0.01
Momentum 0.9
weight decay 0.0001

全てRes-Netの論文を参考にしました。
[1512.03385] Deep Residual Learning for Image Recognition

ここら辺もまだまだ弄りがいがありそうなところですね。

夏休みDay4(DQNがだんだんわかってきた)

ずっとDQNがわからなかったんですけど
今日なんとなく理解できた気がしました。
いやーやっぱり、説明のメモ取りながら理解していくってのは重要ですね。
具体的に何が理解できていなかったのか考えてみると、DQNって学習を安定させるために色んな技術を使うんですが
Q-Networkを更新するときに少し前のQ-Networkを使うってゆうTarget Q-Netwotkという手法があって、
そいつがどう更新に関わってるのかよく分かっていなかったですね。

具体的には
f:id:tkr1205:20180806223610j:plain
(引用: 第14回 深層強化学習DQN(Deep Q-Network)の解説|Tech Book Zone Manatee )

この式の右辺にある
\gamma \max Q(S_{t+1}, a)
という項が少し前の時間のQ-Networkをつかって出力するんですけど、ちゃんとわかっていなかったですね
他にもmaxがついている項のaは a なのに、maxがついていない方の項のaは a_t ってtがついてるのかとか
基本がわかっていないとやっぱりできないんだなって思いました。
あと、心のどこかでDQNは行動を出力するもんだと思ってたんですけど、あくまでQ値を出力するんですね。DQNって思いっきり書いてあるのに勘違いしてました。それも理解の妨げになっていた気もする。

理論が少し理解できてきたから明日はDDQNやってもっと余裕があればA2CDQNの論文を読んでみようかと思います!!!

ではでは

夏休みDay3

今日はPRML(パターン認識と機械学習 上  )の序章やってました。

確率論の加法定理とからへんまで進めました。

まぁ、この辺はまだ難しいとかはなく大学でもやった内容なんでサクサクいけますね。

明日は先輩ならこれからの強化学習を借りれる事になったので借りてきまっす!

 

あ、DL4USもやらなきゃ。。。