夏休み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を実行する上で下記のライブラリが必要となります。
- numpy
- pillow
- pygame
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)。
また、εの値はエピソードが増えるにつれて小さくすることで、ランダムに行動を起こす確率を低くします。
関数update_policy()
ではQ学習の更新式にしたがってQ値を更新します。($$\eta$$は学習率, は時間割引率)
[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)より長いフレーム数プレイしたら学習をストップします。
それと同時に報酬の和とプレイ時間を学習グラフとして出力します。
グラフは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
気軽にフォローしてください。泣いて喜びます。
インターン探しています
現在リモートで働かせていただけるインターン先を探しております。
機械学習分野について勉強中です。
しっかりと扱える言語はPythonのみですが、タスクを与えられれば自分で調べながら完成まで頑張ることをモットーにしています。
以前はインターンにてwebの知識ゼロの状態からFlaskを使ってwebAPIの作成を行いました。
もし興味を持っていただけましたらご連絡ください。
参考文献
つくりながら学ぶ! 深層強化学習 ~PyTorchによる実践プログラミング~
OpenAI Gym
PLE
Q学習
強化学習のキホン
PythonのDefaultDictについて
PLEのインストール
FlappyBirdの動かしかたとQ学習の参考
夏休み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という手法があって、
そいつがどう更新に関わってるのかよく分かっていなかったですね。
具体的には
(引用:
第14回 深層強化学習DQN(Deep Q-Network)の解説|Tech Book Zone Manatee
)
この式の右辺にある
という項が少し前の時間のQ-Networkをつかって出力するんですけど、ちゃんとわかっていなかったですね
他にもmaxがついている項のaは なのに、maxがついていない方の項のaは ってtがついてるのかとか
基本がわかっていないとやっぱりできないんだなって思いました。
あと、心のどこかでDQNは行動を出力するもんだと思ってたんですけど、あくまでQ値を出力するんですね。DQNって思いっきり書いてあるのに勘違いしてました。それも理解の妨げになっていた気もする。
理論が少し理解できてきたから明日はDDQNやってもっと余裕があればA2CかDQNの論文を読んでみようかと思います!!!
ではでは
DL4US Lesson1やったった
この記事は東京大学松尾研究室主催DL4US(DL4US | Deep Learning for All of Us)を受講した感想を書いているものです。
先程、DL4USのLesson1を受けてきました。
詳しいことは書かないほうが良いと思うので書きませんが、Lesson1ならこんなもんかなと思うような内容でした。 あと、仕方のないことなのかなとは思いますが、少々詰め込み過ぎじゃないのかなとは思いました。対象がエンジニア向けだからいいのかな?本当の初学者だったら、少しわけがわからないと思う部分もあると思います。(ただまぁオンラインテスト通過してるから初学者はほとんどいないか)
最後には、課題として精度が90%以上のモデルを作るというものが出たのですがこれがどうも難しい。
上位者はリーダーボードに名前が載るというから、これはもう載りたいよね。うん。
ただまぁ簡単にはいかないんですよね、結構なめてました。頑張らねば。。。。(7/28現在 精度:0.902)
期限(8/2 23:59)
ひとこと ブログとかって書くの難しい、文章を書く能力の低さが伺える
DL4US第二期を受講することになった
東京大学松尾研究室主催 DL4US( DL4US | Deep Learning for All of Us ) の抽選を無事通過し、受講することになりました。
受講までの流れとしては、
応募締め切り 6/30 -> 受講オンラインテスト 7/13~//17 -> 結果 7/19
といった流れで受講するための選考がありました。
オンラインテストでは、Numpyに関する基本的な使い方や文法が問われました。記憶が正しければ全部で20問出題されていたように思います。
Twitterでオンラインテストについて述べている人が結構いましたが、見ている限りでは普段からNumpyを使っていれば簡単に解けるみたいです。(自分は普段からあまり使っている方ではないので3問ばかり間違えてしましましたが。。。)
7/19に結果がメールで通知され、またTwitterを眺めていると4問間違えて当選しなかった人もいるようでした。アブナイアブナイ。。。。
選考基準もよく分かっておらず、自分がなぜ通過できたのか疑問ですが、運良く選考を通過しDL4US第二期を受講できることを嬉しく思います。
Pythonのパスの結合について-忘れないためのメモ
Pythonのパスの結合で何回でもやりそうな失敗したからメモ
Pythonには標準ライブラリの os ライブラリに環境に合わせていい感じにパスを繋げてくれるモジュールがある
具体的には
import os os.path.join('User', 'desktop')
なんかにすると /User/desktop みたいにして返してくれる。(記事内のディレクトリ名は適当です。)
os.path.dirname(os.path.abspath('__file__'))
こうすると得られる
今回はファイルの保存先をconfigファイルとしてメインの実行ファイルとは別のファイルで管理しようとした。
ファイルの保存先を実行ディレクトリと同じ階層に保存ディレクトリを作って、その中に入れるつもりだったのだが
# 完成予想図 |--main.py | |--hozon1 | |-- a.txt | |-- b.txt | |--hozon2 |--1.csv |--2.csv
最初は上のようになることを期待して書いたコードがこんな感じ
import os MAIN_DIR = os.path.dirname(os.path.abspath('__file__')) TXT_DIR = os.path.join(MAIN_DIR, '/hozon1') CSV_DIR = os.path.join(MAIN_DIR, '/hozon2')
こんなコードを書いたのだけど、こんなコードでは予想したとおりにパスを取得できない それぞれprint文で簡単に確認してみると
MAIN_DIR では /User/desktop
TXT_DIR では /hozon1
CSV_DIR では /hozon2
が出力されてしまった。 全然結合がされていなかった。
では正しくはどうすべきだったのかと言うと
import os MAIN_DIR = os.path.dirname(os.path.abspath('__file__')) TXT_DIR = os.path.join(MAIN_DIR, 'hozon1') CSV_DIR = os.path.join(MAIN_DIR, 'hozon2')
バックスラッシュを取ればよかっただけ。
たったこのためだけに30分も使ってしまった。 しょうもないミスをしたらまた公開しようと思う。