【AI + pygame】pygameで作るインベーダー風ゲーム 第5回 強化学習編その2(ゲーム画面から学習させる)

2019年05月10日

(2019年5月20日更新:学習中の挙動について追記)

エンジニアのMです。
pygame によるインベーダー風ゲームの改造記事の第5回です。

今回のコードは、こちらからダウンロードできます。
-> pygame_invader_5.zip

ようやく強化学習を試す段階まで来ました。
ここでは「Deep Q-Network」によるゲーム攻略の学習をさせていくわけですが、まずは実験に使用したPCのスペックを上げておきたいと思います。

  • CPU:Core i5-7400(4コア4スレッド)
  • メモリ:24GB
  • GPU:Geforce GTX 1050 Ti(VRAM 4GB)
  • システムディスク:SSD(250GB)

このスペックは、GPU利用の機械学習を試す上での下限値に近い性能と言えると思います。
もし手持ちのPCが性能不足であれば、「Microsoft Azure」や「AWS」で仮想マシンを借りる手があります。
1時間100~120円程度でGPUインスタンスを借りられますので、ちょっと試すだけなら必要なパーツを買うより安上がりです。
こちらの解説は特にしませんので、インターネット等で調べて最適な方法を選んでください。

試しに仮想8コアの仮想マシンを借りて、CPUのみで学習を回したところ 1/3 ~ 1/4くらいの学習速度でした。
丸一日放置すればCPUでもいけそうですが、あまりおすすめはしません。

CNN(Convolution Neural Network)モデル

ゲーム画面を使って学習させるので、画像認識でお馴染みのCNN(Convolution Neural Network)を利用します。
keras を使ってモデルを記述したものを以下に示します。

    nb_actions = 3
    hidden_size = 128
    n_filters = 8
    kernel = (13, 13)
    strides = (3, 3)
    
    model = Sequential()
    model.add(Reshape((env.observation_space.shape), input_shape=(1, ) + env.observation_space.shape))
    model.add(Conv2D(n_filters, kernel, strides=strides, activation='relu', padding='same'))
    model.add(Conv2D(n_filters, kernel, strides=strides, activation='relu', padding='same'))
    model.add(Conv2D(n_filters, kernel, strides=strides, activation='relu', padding='same'))
    model.add(Flatten())
    model.add(Dense(hidden_size, kernel_initializer='he_normal', activation='relu',
                    kernel_regularizer=l2(0.01)))
    model.add(Dense(hidden_size, kernel_initializer='he_normal', activation='relu',
                    kernel_regularizer=l2(0.01)))
    model.add(Dense(hidden_size, kernel_initializer='he_normal', activation='relu',
                    kernel_regularizer=l2(0.01)))
    model.add(Dense(nb_actions, activation='linear'))

畳み込み層を3層、全結合層を3層重ねたモデルです。
位置の情報が失われるのでプーリング層は使用しません。
また画面の細部はそれほど重要ではないので、大きなカーネルを使用します。
つまりここでは細部の特徴を得るより、自機と弾、自機と敵、弾と敵の位置関係が得られることを期待しています。
strides でカーネルの移動距離を (3,3) に設定しているのは、パラメータ数を減らして高速に計算するためです。

全結合層には過学習回避のためL2正則化を適用しています。
学習を進めると、最終的に特定の行動を取り続けるようになることが多く、その対策です。
それでも学習を進めすぎると過学習に陥るようなので、ゲームプレイ中の挙動は常に確認するようにしましょう。
また、全結合層の初期値は ReLU と相性がいいと言われる 「He の初期値」を採用しています。

次に今回のモデルのパラメータ数について確認しておきます。
以下は keras の summary による出力を表にしたものです。

shape params
入力(Reshape) 113 x 160 x 3 0
畳み込み1(Conv2D) 38 x 54 x 8 4064
畳み込み2(Conv2D) 13 x 18 x 8 10824
畳み込み3(Conv2D) 5 x 6 x 8 10824
展開(Flatten) 240 0
全結合1(Dense) 128 30848
全結合2(Dense) 128 16512
全結合3(Dense) 128 16512
全結合4(Dense) 3 387

仮に strides を (1,1) とすると合計パラメータ数は1千万を超えます。
パラメータが多いとより多くの VRAM が必要となり、計算にかかる時間も相応に長くなります。
もちろんパラメータが多い(≒モデルが大規模な)ほうが高性能な可能性も十分にありますので、環境に合わせたモデルを検討しましょう。

学習用コード

実行に必要なものは以下の通りです。

  • python (3.68)
  • tensorflow-gpu (1.13.1)
  • Keras (2.2.4)
  • keras-rl (0.4.2)
  • CUDA toolkit (10.0)
  • cuDNN (7.5.1 for CUDA 10.0)

動作確認に使ったバージョンも同時に示しています。
CUDA と cuDNN 以外は pip コマンドで導入可能です。
tensorflow-gpuCUDA のバージョンとの整合性に厳しいので、他のバージョンを使う場合は公式のマニュアルをよく確認してください。
CUDAcuDNN も同様です。cuDNN のダウンロードには NVIDIA Developer のアカウントが必要です。
tensorflow のCPU版を使う場合は CUDA と cuDNN は不要です。

from keras.models import Sequential
from keras.layers import Dense, Flatten,  Conv2D, Reshape
from keras.optimizers import Adam
from keras.regularizers import l2

from rl.agents.dqn import DQNAgent
from rl.policy import EpsGreedyQPolicy
from rl.memory import SequentialMemory
import invader07_part4

env = invader07_part4.Invader(step=True, image=True)
nb_actions = 3

hidden_size = 128
n_filters = 8
kernel = (13, 13)
strides = (3, 3)

model = Sequential()
model.add(Reshape((env.observation_space.shape), input_shape=(1, ) + env.observation_space.shape))
model.add(Conv2D(n_filters, kernel, strides=strides, activation='relu', padding='same'))
model.add(Conv2D(n_filters, kernel, strides=strides, activation='relu', padding='same'))
model.add(Conv2D(n_filters, kernel, strides=strides, activation='relu', padding='same'))
model.add(Flatten())
model.add(Dense(hidden_size, kernel_initializer='he_normal', activation='relu',
                kernel_regularizer=l2(0.01)))
model.add(Dense(hidden_size, kernel_initializer='he_normal', activation='relu',
                kernel_regularizer=l2(0.01)))
model.add(Dense(hidden_size, kernel_initializer='he_normal', activation='relu',
                kernel_regularizer=l2(0.01)))
model.add(Dense(nb_actions, activation='linear'))
print(model.summary())

memory = SequentialMemory(limit=100000, window_length=1)
policy = EpsGreedyQPolicy(eps=0.001)

dqn = DQNAgent(model=model, nb_actions=nb_actions,gamma=0.99, memory=memory, nb_steps_warmup=100,
               target_model_update=1e-2, policy=policy)

dqn.compile(Adam(lr=1e-3), metrics=['mae'])

fname = "invader_model_image_160x120_act3_h128_f8_k13_13_st3_c3_d3.bin"
try:
    dqn.load_weights(fname)
    print("Weights are loaded.")
except:
    print("Weights are NOT loaded.")

history = dqn.fit(env, nb_steps=200000, verbose=2)

dqn.save_weights(fname, overwrite=True)

dqn.test(env, nb_episodes=50)

invader07_part4 は前回で配布したコードです(今回の zip ファイルにも同梱しています)。

このコードを実行すると、インベーダー風ゲームが起動されて自動でのゲームプレイが開始されます。
nb_steps で指定したステップ数に到達するまで、ゲームプレイと学習を繰り返します。
今回使っているパラメータは、keras-rl のサンプルとほぼ一緒で、モデルと与える報酬の調整だけでどうにか動くようにしました。
DQNAgent の引数 target_model_update などの学習に関わるパラメータを調整して、さらにうまく学習するように工夫してみてください。
現状のクリア率は30%程度でしょうか。モデルの規模などを考えれば悪くない数字かと思いますが、改良の余地は多分にあると思います。

ちなみに、dqn.py から呼び出されたインベーダー風ゲームの挙動は大変不安定です。
ゲーム画面をクリックしたり、他のアプリをいじったりしているだけで画面がフリーズします。
学習自体は進むようなので問題はありませんが、テスト中に固まると、それ以降の処理は進まなくなります。
Pygame 本来の使用法から離れているので、仕方のないところです。

学習結果だけ見たい人向けに、200,000steps 学習済みのファイルと、AIによるプレイ動画も用意しました。活用してください。
-> invader_model_data

まとめ

前回で必要なメソッドを全て揃えておいたので、今回の記事は非常に簡素なものになりました。
画像から学習させるのは思いの外難しく、正直なところ今のモデルが良いものなのかどうかもわかっていません。
非常に粗い画面に基づいてプレイさせていることを思えば十分なのかもしれませんが、まだまだ詰められそうなので他の作業の裏ででも学習させ続けたいところです。
次回は、画像ではなく、こちらで設定した情報に基づいた学習について解説します。

今回のデータはこちらから(再掲) -> pygame_invader_5.zip

この記事が気に入ったら
いいね ! しよう

Twitter で
フォームズ編集部
オフィスで働く方、ネットショップやホームページを運営されている皆様へ、ネットを使った仕事の効率化、Webマーケティングなど役立つ情報をお送りしています。