LoginSignup
19
7

More than 5 years have passed since last update.

逆強化学習でATARIに挑戦してみた(準備編)

Last updated at Posted at 2018-08-17

はじめに

初めまして、人工知能を学んでいる大学4年生です。
初めてのQiita投稿です。よろしくお願いします。

本記事の内容

タイトルの通り逆強化学習でATARIに挑戦するための準備を行いたいと思います。
具体的には

  1. Windows上でのATARIの環境構築
  2. データセット「The Atari Grand Challenge Dataset」の準備
  3. Behavior Cloning の実装

を行いたいと思います。
一つ目に関してはネット上に多くの記事がありましたが、少し詰まったので備忘録的に残しておきます。

動作環境

本記事における動作環境です。

  • Windows 10 HOME
  • anaconda 4.2.0
  • Python 3.5.5
  • Keras 2.1.6
  • tensorflow-gpu 1.8.0
  • gym 0.10.5

※ 詳しい動作環境は、githubでご覧ください.

逆強化学習とは

通常の強化学習では、エージェントが環境から報酬を受け取り、その報酬を最大化するように学習することによって、最適な行動ルールを学習します。
しかし、このような報酬の設計が困難である場合があります。
そのような場合に、上手な行動をするエキスパートの行動系列から報酬関数を推定する手法を逆強化学習といいます。

逆強化学習に関してはこちらのスライドが大変わかりやすいです。

TensorFlowで逆強化学習
https://www.slideshare.net/ohtaman/tensor-flow-63359654

ATARIの環境構築

こちらの記事の通りにすれば、OpenAIGymでのATARI環境が構築できます。

OpenAI Baselinesをwindows(winpython)で試す。
https://qiita.com/tmizu23/items/ff1d5c89bc99292410c0

Bash on Ubuntu on Windowsを使う方法もありますが、その場合は GPU が使用できなくなってしまうので注意が必要です。

手順を要約すると
1. makeを使えるようにする
2. pipでATARIをインストール
です。

makeを使えるようにする

先ほどの記事では msys2 をインストールしていました。
それでも問題ありませんが make さえ使えればいいので Make for Windows をインストールする方が簡単だと思います。

こちらの記事の通りにすれば、make が使えるようになります。

Windowsでmakeコマンドを使う
https://qiita.com/tokikaze0604/items/e13c04192762f8d4ec85

コマンドプロンプトの再起動は忘れないようにしましょう。

pipでATARIをインストール

まず、pip で gym をインストールします。

pip install gym

この時点では、まだATARIはインストールされていません。

続いて、pip で gym[atari] をインストールします。以下のコマンドを実行してください。

pip install -U git+https://github.com/Kojoley/atari-py.git

これでATARIの環境が構築できました。

※注意

pip install gym[atari]

としてしまうと、

Failed building wheel for atari-py

とエラーになります。(記事はちゃんと読まないとダメですね…)

動画保存

gym で動画を保存するには FFmpeg が必要なので

こちらからダウンロードしてインストールしてください。

こちらの記事に詳しい手順が解説されていました

ffmpegのインストール
http://simple.hatenablog.jp/entry/DTV/ffmpeg-Installation

The Atari Grand Challenge Dataset

The Atari Grand Challenge Datasetは、ATARIゲームのうち5つのゲームの状態(画像)と対応する行動のデータセットです。

こちらの本家サイトのエミュレータによって、プレイデータを収集したそうです。

2018年8月現在、このデータセットは v2 でした。

含まれているゲームは

  • Space Invaders
  • Q*bert
  • Ms. Pacman
  • Video Pinball
  • Montezuma's Revenge

です。

論文はこちらです。

The Atari Grand Challenge Dataset
https://arxiv.org/pdf/1705.10998.pdf

データセットのダウンロード

こちらのページの一段落目の download からダウンロードできます。
特定のゲームのデータだけをダウンロードしたい場合は、こちらのGitHubか先ほどのページの二段落目の here からダウンロードできます。(ただし、こちらのデータセットはどうやら v1 のようです。)

データセット全体で25GByte以上あるため、ダウンロードにかなりの時間を要するので注意してください。

このデータセットはtarとgzで圧縮されているので、7-Zipなどを用いて解凍してください。この解凍にもかなりの時間を要するので頑張ってください。解凍後のデータセットは90GByte近くあるため、解凍場所はきっちり指定しましょう。

データセットの概要

データセットの内容は下の表のようになっています。
hyou.png

論文に記載されている v1 よりもエピソード数が増加しています。しかし、ベストスコアがなぜか下がっていました。

スコアの分布はそれぞれ下の表のようになっていました。




おそらくネット上のエミュレータで収集しているためだと考えられますが、低い得点が多いように見られます。このデータセットを学習に用いる場合はスコアの高いものだけ用いるようにする必要があるでしょう。

データセットの構成

ディレクトリ構成

解凍後のディレクトリ構成は以下のようになっています。

/atari_v2_release/
├── screens
│    ├── mspacman
│    ├── pinball
│    ├── qbert
│    ├── revenge
│    └── spaceinvader
└── trajectories
     ├── mspacman
     ├── pinball
     ├── qbert
     ├── revenge
     └── spaceinvader

screens フォルダ内のそれぞれのフォルダには episode ごとのフォルダが入っており、その中にはフレーム番号がつけられた画像ファイルが入っています。trajectories フォルダ内には episode ごとの txt ファイルが入っています。

データフォーマット

trajectories フォルダ内の txt ファイルのフォーマットは csv 形式であり。以下のようになっています。

frame,reward,score,terminal,action

一行目はおそらくデータベースで割り振られたIDであり、関係ないので無視してかまわないでしょう。(何のためにあるのか分からないです…)

csv の要素はそれぞれ

  • frame はフレーム番号
  • reward はそのフレームで得られた報酬
  • score はそのフレームでの得点
  • terminal は終了状態なら1それ以外なら0
  • action はそのフレームの行動

です。

学習に用いるのは frame と action のみです。

screenはどのゲームであっても 160x210 の png ファイルです。
revenge

actionのエンコード

このデータセットの action のエンコードは

name action
NOOP 0
FIRE 1
UP 2
RIGHT 3
LEFT 4
DOWN 5
UPRIGHT 6
UPLEFT 7
DOWNRIGHT 8
DOWNLEFT 9
UPFIRE 10
RIGHTFIRE 11
LEFTFIRE 12
DOWNFIRE 13
UPRIGHTFIRE 14
UPLEFTFIRE 15
DOWNRIGHTFIRE 16
DOWNLEFTFIRE 17

となっています。

すでにお気づきの方も多いと思いますが、OpenAIGym の action のエンコードと一致していません。

例えば Ms. Pacman のエンコードは

name action
NOOP 0
UP 1
RIGHT 2
LEFT 3
DOWN 4
UPRIGHT 5
UPLEFT 6
DOWNRIGHT 7
DOWNLEFT 8

となっています。

actionのエンコードは下のコードを実行すると

import gym

env = gym.make('MsPacman-v0')

print(env.env.get_action_meanings())
['NOOP', 'UP', 'RIGHT', 'LEFT', 'DOWN', 'UPRIGHT', 'UPLEFT', 'DOWNRIGHT', 'DOWNLEFT']

のように取得できます。

さらに、Ms. Pacman のデータセット全体のactionの回数は以下の表のようになっています。

name action_count
NOOP 1430493
FIRE 2595
UP 300404
RIGHT 380917
LEFT 379688
DOWN 269034
UPRIGHT 14160
UPLEFT 13151
DOWNRIGHT 12775
DOWNLEFT 12612
UPFIRE 2977
RIGHTFIRE 3811
LEFTFIRE 3109
DOWNFIRE 2587
UPRIGHTFIRE 289
UPLEFTFIRE 0
DOWNRIGHTFIRE 356
DOWNLEFTFIRE 110

この表を見ればわかるように、データセットにはゲーム側に存在しないactionが含まれています。

これに気を付けないと正しく学習できない、もしくはエラーになる可能性があります(というかなりました)。

※注意

score の値はゲーム画面に表示されていた値と 1 ずれていることがありました(エミュレータのバグ?)。
最終フレームであっても terminal は 1 になっていませんでした(この要素いらないのでは…?)。

データセットの読み込み

scoreでソート

先ほど述べた通り、このデータセットを用いる時には、score の高いものだけを選んで使う方が良いと考えられるので、最終フレームの scoreで trajectories のファイルをソートしました。

最終フレームのスコアを読み込むにはtrajectoriesの最終行だけ読めばいいので、

Atari_Traj_Util.py
traj_score = []
for traj in os.listdir(path + '/trajectories/' + GAME_NAME):
    f = open(path + '/trajectories/' + GAME_NAME + '/' + traj, 'r')
    end_data = f.readlines()[-1].split(",")
    #[フォルダ名, 最終スコア]
    traj_score.append([os.path.splitext(traj)[0], int(end_data[2])])
    f.close()

# スコアでソート
traj_score = sorted(traj_score, key=lambda x:x[1], reverse=True)

として、scoreで降順にソートすればいいでしょう。
pathはデータセットのフォルダのパス、GAME_NAMEはゲームのフォルダ名です。

trajectories読み込み

trajectories フォルダの txt ファイルは csv 形式なので pandas 等を使って簡単に読み込むことができます。

先ほどscoreで降順に並び替えたので、データセットの使用する割合を p に格納して

Atari_Traj_Util.py
for traj_num, _ in traj_score[:int(len(traj_score)*p)]:
    print("Now Loading : %s" % traj_num)
    # データロード
    df = pd.read_csv(path + '/trajectories/' + GAME_NAME + '/' + traj_num + '.txt', skiprows=1)

としました。先ほど説明した通り、一行目は無視しています。

screens読み込み

screensのフォルダ名とtrajectoriesのファイル名が対応しており、画像名はフレーム番号なので先ほど読み込んだデータフレームを使えばいいのですが、少し問題が生じました。

私のPCのメモリは 16GB なのですが、生の画像データをすべて読み込んでから処理を行おうとするとメモリリークが発生し、処理速度が著しく低下してしまいました。

なので、読みこむ段階で学習のための前処理を行うことで、データサイズを小さくしました。

前処理

前処理はこちらのサイトを参考にして行いました。

DQNをKerasとTensorFlowとOpenAI Gymで実装する
http://elix-tech.github.io/ja/2016/06/29/dqn-ja.html

行った前処理は

  1. 84x84 にリサイズ
  2. グレースケールに変換
  3. 4フレームを結合

です。

1と2は計算コストとメモリ削減のための前処理です。

3を行う理由はATARIにはスプライト数の上限があり、偶数フレームか奇数フレームのどちらかにしか出現しないオブジェクトがあるからです。

コードは以下になりました。

Atari_Traj_Util.py
def preprocess(status, tof=True, tol=True):
    """状態の前処理"""

    def _preprocess(observation):
        """画像への前処理"""
        # 画像化
        img = Image.fromarray(observation)
        # サイズを入力サイズへ
        img = img.resize(INPUT_SHAPE)
        # グレースケールに
        img = img.convert('L') 
        # 配列に追加
        return np.array(img)

    # 状態は4つで1状態
    assert len(status) == FRAME_SIZE

    state = np.empty((*INPUT_SHAPE, FRAME_SIZE), 'int8')

    for i, s in enumerate(status):
        # 配列に追加
        state[:, :, i] = _preprocess(s)

    if tof:    
        # 画素値を0~1に正規化
        state = state.astype('float32') / 255.0

    if tol:
        state = state.tolist()

    return state

最終的なコード

前処理も含めた最終的な読み込みのコードです。

結合したフレームに対応するactionは結合したフレームの内で一番最後のフレームのactionとしました。

Atari_Traj_Util.py
# 軌道ごとに記録する配列
status_ary = []
action_ary = []
for traj_num, _ in traj_score[:int(len(traj_score)*p)]:
    print("Now Loading : %s" % traj_num)
    # データロード
    df = pd.read_csv(path + '/trajectories/' + GAME_NAME + '/' +traj_num + '.txt', skiprows=1)
    traj_list = [np.array(Image.open(path + '/screens/' + GAME_NAME + '/' + traj_num + '/' +img_file + '.png', 'r')) 
                    for img_file in tqdm(df['frame'].astype('str').values.tolist())]
    act_list = df['action'].astype('int8').values.tolist()

    # 前処理
    print("Now Preprocess : %s" % traj_num)

    status = np.concatenate([preprocess(traj_list[i:i+frame_size], frame_size, False, False)[np.newaxis, :, :, :] 
                    for i in tqdm(range(len(traj_list) // frame_size))], axis=0)
    action = [act_trans_list[act_list[i+(frame_size-1)]] for i in tqdm(range(len(traj_list) // frame_size))]


    status_ary.append(status)
    action_ary.append(np.array(action))

    #メモリ対策
    del traj_list
    del act_list
    del status
    del action

軌道ごとに読み込みlistに格納していますが、画像をlistで格納するとメモリリークを起こしたので、データサイズを比較的小さくできるint8型のnumpyで格納するようにしています。del でメモリ開放していますが気休め程度だと思います。

tqdmはプログレスバーを出力するためのパッケージです。

そして、最後に以下のコードによってすべてのepisodesの軌道を結合し、float32型に変換した上で正規化します。

Atari_Traj_Util.py
# numpy展開
print("Now Concatenate")
status = np.concatenate(status_ary, axis=0)
del status_ary
action = np.concatenate(action_ary, axis=0)
del action_ary

# 状態正規化
status = status.astype('float32') / 255.0
print("End Concatenate")

これで、データセットの読み込みができました。

Behavior Cloning

データセットの有効性を確認するために、このデータセットを用いて学習を行いました。

実装の容易さから、デモンストレーションのデータのstateを入力、actionを出力として教師あり学習を行う、Behavior Cloningを実装しました。

Atari環境

今回扱うゲームは「Q*bert」にしました。(特に理由はありません)

Q*bert の action のエンコードは

name action
NOOP 0
FIRE 1
UP 2
RIGHT 3
LEFT 4
DOWN 5

でした。

環境の名前について

OpenAIGym においてAtari環境を利用する場合、同じゲームに異なる名前の環境が存在します。

基本的には {}NoFrameskip-v4 という名前になっている環境を使うべきだそうです。

そうしないと

  • 環境側でフレームスキップが発生する
  • 行動の繰り返しが行われる

らしいです。

こちらの記事に詳しい解説がありました

OpenAI Gym の Atari Environment の命名規則と罠について
https://qiita.com/keisuke-nakata/items/141fc53f419b102d942c

モデル

モデルの構造は以下のようにしました。

Layer Filter size Num Filters Stride Activation
Conv2D 8x8 64 4x4 ReLU
Conv2D 4x4 64 2x2 ReLU
Conv2D 3x3 64 1x1 ReLU
Dense 512 ReLU
Output num actions SoftMax

学習パラメータなど

学習に用いたパラメータ・学習則は以下の通りです。

  • BatchSize 128
  • Epoch 20
  • Adam
    • lr 0.001
    • beta_1 0.9
    • beta_2 0.999
    • epsilon None
    • decay 0.0

実験

データセットを使用する割合が1%と5%それぞれにして、学習を行いました。

10%で学習を行おうとしたところ、メモリリークが発生して学習できませんでした。学習を行ったコンピュータのメモリは16GBです。

action の変換は以下の表で行いました。(これでいいかは不明)

name action_count
NOOP NOOP
FIRE FIRE
UP UP
RIGHT RIGHT
LEFT LEFT
DOWN DOWN
UPRIGHT UP
UPLEFT UP
DOWNRIGHT RIGHT
DOWNLEFT LEFT
UPFIRE UP
RIGHTFIRE RIGHT
LEFTFIRE LEFT
DOWNFIRE DOWN
UPRIGHTFIRE UP
UPLEFTFIRE UP
DOWNRIGHTFIRE RIGHT
DOWNLEFTFIRE LEFT

試行ごとのブレが大きかったので、それぞれ3回の実験結果を載せました。

データセット1%利用

学習に用いたデータの分布は以下のようになっていました。

最上位層のみのデータを利用している形になっています。

学習した結果は以下のようになりました。

  

400 0 125

試行ごとにブレていますが、飛び降りたりせず得点を得ているので、学習はできているように見えます。

データセット5%利用

学習に用いたデータの分布は以下のようになっていました。

1%の時よりデータ数は増えていますが、比較的得点の低いデータも学習に用いる形になっています。

学習した結果は以下のようになりました。

||||

3775 0 0

1%の時と比べて、高い得点を取れている場合もありますが、全く動かない場合も増えてしまいました。

原因

動かなくなってしまう原因を調べるために学習したデータのactionの回数を表にしました。

NOOP FIRE UP RIGHT LEFT DOWN
103628 4 10277 14548 9227 13928

この表を見ればすぐ分かるように、何もしない行動である「NOOP」が突出して多いのが動かなくなってしまった原因だと考えられます。

Class Weight 使用

偏りのあるデータで学習するためにactionの回数が少ないほど大きな重みをかけるようにしました。

以下のコードでそれぞれの重みを計算しました。

Atari_Traj_Util.py
unique, count = np.unique(action, return_counts=True)
weight = np.max(count) / count
weight = dict(zip(unique, weight))

Class_Weightを使って学習した結果は以下のようになりました。

  

450 4275 500

動かないことはなくなり、ちゃんと学習できているようです。

データセット1%利用(Class_Weight有り)

1%の時でもactionに偏りがあるはずなので調べてみると

NOOP FIRE UP RIGHT LEFT DOWN
26454 0 2820 2502 1594 2989

このように大きく偏っていたので先ほどと同様に重みを付けて学習しました。

  

5%の時と大きな差が無いような結果になりました。

まとめ

使用する割合を増やせば良くなるかと思っていましたが、必ずしもそんなことは無いようです。それよりも試行によるブレの方が大きいように感じました。

やはり単純に教師あり学習をするだけでは、何もないのに動かなくなったり、敵に自分から突っ込んでいってしまったりするようです。

まとめと感想

ここまで読んでいただいた方、本当にありがとうございました。

Qiitaどころか記事を書くのも初めてなのでかなり大変でした。(Markdownとかgifとか)

本当は逆強化学習までやってから1つの記事としてまとめるつもりだったんですが、バグを直したりいろいろしていたら脱線が長くなってしまったので、ここで一度切らせていただきます。

続きは近いうちに投稿できたらと思っています。(実装もまだですが)

この記事が何かしらのお役に立てれば幸いです。

おまけ

ここまで、Q*bert を対象に学習を行ってきましたがネット上には、Q*bert を対象にした強化学習の記事が無かったのでDQNで Q*bert をやりたいと思います。

と言ってもkeras-rlのexampleにAtariをDQNで解くサンプルがあったので、ほとんどそれを使うだけです。

keras-rl

kerasを用いて、DQNなどの深層強化学習を実装したライブラリです。

本家のGitHubはこちらです。

DQN以外にもDDPGなども使えるそうです。詳しくはこちら

インストール

pipでインストールするだけです。

pip install keras-rl

動作環境

  • keras-rl 0.4.2

他はと同じです。

ソースコード

ソースコードは一応こちらに上げてあります。

このコードは、こちらにあるkeras-rlのexampleにコメントと動画の保存を追加しただけのものです。

特にソースコードの説明はしません。(するなら記事にします。)

結果

結果は以下のようになりました。

out.gif

あまり、得点が取れていませんね……

学習に8時間くらいかかっているのですが、まだまだ学習が足りていないのかもしれないです。

Behavior Cloningと比べると機敏に動いている感じがします。Behavior Cloningはやはり多すぎる"NOOP"に学習が引っ張られているのかもしれません。

これで本当に終わりです。ありがとうございました。

19
7
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
7