LoginSignup
19
14

More than 3 years have passed since last update.

強化学習でスーパーマリオをクリアする

Last updated at Posted at 2019-12-28

https://vigne-cla.com/3-4/
の記事によると人口知能の学習に使えるスーパーマリオの環境は
Kautenjaさんが作成したgym_super_mario_brosと
ppaquetteさんが作成したppaquette_gym_super_mario
がある。

ここではppaquette_gym_super_marioを使用している
https://qiita.com/tsunaki/items/ec2556dc147c6d263b9d
のコードを使用させていただき、スーパーマリオの強化学習を進めていきます。
(tsunaki様、こちらのページありがとうございます)

基本情報

  • 環境はgymをベースにしている。
    gymの情報:
    https://github.com/openai/gym/blob/master/README.rst

  • 画面データはもともと256x224でred、blue、green (RGB)の情報を持つ。

  • データ減らすために画面を縦13個、横16個のtileという単位に直して処理している。

  • タイルには以下の4種類のうちの何かが定義されている

    • 0: empty space
    • 1: object (e.g. platform, coins, flagpole, etc.)
    • 2: enemy
    • 3: Mario

準備

https://github.com/tsunaki00/super_mario
からzipファイルをPCに保存して展開する。

requirements.txtに
gym>=0.2.3
gym_pull
numpy
と記載されており、これらをインストールする必要がある。

gym>=0.2.3はgymのバージョン0.2.3以上という意味と思われるが、バージョンが新しすぎると
/home/(username)/.local/lib/python3.6/site-packages/gym
以下に
configuration.py
がなくてエラーになってしまう。

ここでは0.2.3をインストールして進める。

pip3 install gym==0.2.3

しかし、このままzipファイルを展開した中にあるstart.pyを

python3 start.py 

によって実行すると
Traceback (most recent call last):
File "start.py", line 23, in
from gym.wrappers import Monitor
ImportError: cannot import name 'Monitor'
というエラーになるので、start.py内の
from gym.wrappers import Monitor

from gym.monitoring import Monitor
と書き換える。

(これはmonitor.pyが
/home/(username)/.local/lib/python3.6/site-packages/gym/wrappers
ではなく
/home/(username)/.local/lib/python3.6/site-packages/gym/monitoring
にあるためである。)

また、

pip3 install gym_pull
pip3 install numpy

としてgym_pullとnumpyもインストールする。

CNN

  def create_network(self, action_size):
    #2層のレイヤーにする
    W_conv1 = self.weight_variable([8, 8, 1, 16])
    b_conv1 = self.bias_variable([16])
    W_conv2 = self.weight_variable([4, 4, 16, 32])
    b_conv2 = self.bias_variable([32])
    W_conv3 = self.weight_variable([4, 4, 32, 64])
    b_conv3 = self.bias_variable([64])
    W_fc1 = self.weight_variable([512, action_size])
    b_fc1 = self.bias_variable([action_size])
    s = tf.placeholder("float", [None, 13, 16, 1])
    # hidden layers
    h_conv1 = tf.nn.relu(self.conv2d(s, W_conv1, 2) + b_conv1)
    h_conv2 = tf.nn.relu(self.conv2d(h_conv1, W_conv2, 2) + b_conv2)
    h_conv3 = tf.nn.relu(self.conv2d(h_conv2, W_conv3, 1) + b_conv3)
    h_conv3_flat = tf.reshape(h_conv3, [-1, 512])
    readout = tf.matmul(h_conv3_flat, W_fc1) + b_fc1
    return s, readout 

weight_variableの[]内は[フィルタ高さ、フィルタ幅、チャンネル数、フィルタ数]を表す。
ただし全結合層では[入力ユニット数、出力ユニット数]となる。

CNNの入力はsであり、後にscreen(画面)情報が代入される。ここではサイズが[?, 13, 16, 1]のplaceholderである。
以下の処理がされる。
1層目:[8, 8, 1, 16]のフィルタがかかり、16個のバイアスがかかる。
2層目:[4, 4, 16, 32]のフィルタがかかり、32個のバイアスがかかる。
3層目:[4, 4, 32, 64]のフィルタがかかり、64個のバイアスがかかる。
全結合層:512個のユニットをaction_size個のユニットに全結合する。

出力readoutのユニットの数はactionのパターン数(action_size)であり、
それぞれのactionの良さそう度合いを出力しているようである。
もとのコードでは2^6=64通りであるが、後の改良版では2通りである。

学習

ディープラーニングの関数をcreate_networkとして設定し、readoutを出力している。
以下のようにcostを定義し、最小化している。

    s, readout = self.create_network(len(action_list))
    a = tf.placeholder("float", [None, len(action_list)])
    y = tf.placeholder("float", [None, 1])
    readout_action = tf.reduce_sum(tf.multiply(readout, a), reduction_indices = 1)
    cost = tf.reduce_mean(tf.square(y - readout_action))
    train_step = tf.train.AdamOptimizer(1e-6).minimize(cost)

aにactions、yにrewards、sにimagesが代入される。

        rewards.append([float(info['distance'])])

から、rewardsは距離の値の時系列データとなっている。

actionは[up, left, down, right, A, B]の6種あり、それぞれ0または1なので、actionは2^6=64通りある。

    for i in range(64) :
      command = format(i, 'b')
      command = '{0:06d}'.format(int(command))
      actions = []
      for cmd in list(command) :
        actions.append(int(cmd))
      action_list.append(actions)
  • formatは書式を指定する関数で、command = format(i, 'b')によって変数iに入る数字(1~64)が2進法に変換されてcommandに入力される。
  • command = '{0:06d}'.format(int(command))によって6桁の2進数になる。
  • actionsに6bitの情報が入り、64通りのactionsのデータがaction_listに入る。
  • actionsはaction_list[action_index]として得ることができる。

action_indexは以下より、episodeが9まではランダムに得られ、それ以降はscreen情報を入れたCNNの学習の結果から得られる。

        if episode < 10 :
          action_index = random.randint(0, len(action_list) - 1)
        else :
          readout_t = readout.eval(feed_dict = {s : [screen]})[0]
          action_index = np.argmax(readout_t)
obs, reward, is_finished, info = self.env.step(action_list[action_index])

によってaction_list[action_index]の結果としてobs, reward, is_finished, infoが得られる。

メモ

ppaquette_gym_super_mario/super_mario_bros.pyから以下がわかる。

  • self.launch_vars['mode'] = 'algo'
     のalgoをhumanとすると、人がマリオを操作できるようになる。
     (ppaquette_gym_super_mario/wrappers/control.pyでhumanモードとalgoモードを区別をしている。)

  • lua/super-mario-bros.luaにはメモリーアドレスや、敵の位置情報の取得など、ゲーム進行上必要なことが書かれている。

PC環境

試行錯誤の結果、Ubuntuを使用

エラー解決集

gym.error.DependencyNotInstalled: fceux is required. Try installing with apt-get install fceux.

というエラーが出た。これはWindowsでは解決できていないが、Ubuntuでは

sudo apt-get install fceux
sudo apt --fix-broken install

で解決した。


dpkg-deb: error: paste subprocess was killed by signal (Broken pipe)
Errors were encountered while processing:
 /var/cache/apt/archives/nvidia-cuda-dev_9.1.85-3ubuntu1_amd64.deb
E: Sub-process /usr/bin/dpkg returned an error code (1)

というエラーは

sudo dpkg -i --force-overwrite /var/cache/apt/archives/nvidia-cuda-dev_9.1.85-3ubuntu1_amd64.deb

とnvidia-cuda-dev_9.1.85-3ubuntu1_amd64.debを上書きしたら解決した。


Traceback (most recent call last):
  File "mario.py", line 105, in <module>
    game.play_game()
  File "mario.py", line 59, in play_game
    sess = tf.InteractiveSession()
AttributeError: module 'tensorflow' has no attribute 'InteractiveSession'

というエラーは

pip3 uninstall tensorflow
pip3 install tensorflow==1.14.0

とtensorflowのバージョンを下げたら解決した。


AttributeError: 'TimeLimit' object has no attribute 'tiles'

というエラーは
上記の「準備」に記載した手順で環境を作ると出なくなった。

アクションの選定

マリオのアクションとしては左へ移動などは不要と考えられ、選定した方が良いと思われる。
ここでは以下のように、Bダッシュ右移動とBダッシュ右ジャンプの2アクションだけに絞ってみた。

修正前:

    action_list = []
    for i in range(64) :
      command = format(i, 'b')
      command = '{0:06d}'.format(int(command))
      actions = []
      for cmd in list(command) :
        actions.append(int(cmd))
      action_list.append(actions)"""

修正後:

    action_list=[[0, 0, 0, 1, 0, 1], [0, 0, 0, 1, 1, 1]]

※[]の中は[up, left, down, right, A, B]を表す。

CNNの改良

3層から6層にレイヤーを増やし、pooling層を入れました。

修正前:

  def create_network(self, action_size):
    W_conv1 = self.weight_variable([8, 8, 1, 16])
    b_conv1 = self.bias_variable([16])
    W_conv2 = self.weight_variable([4, 4, 16, 32])
    b_conv2 = self.bias_variable([32])
    W_conv3 = self.weight_variable([4, 4, 32, 64])
    b_conv3 = self.bias_variable([64])
    W_fc1 = self.weight_variable([512, action_size])
    b_fc1 = self.bias_variable([action_size])
    s = tf.placeholder("float", [None, 13, 16, 1])
    # hidden layers
    h_conv1 = tf.nn.relu(self.conv2d(s, W_conv1, 2) + b_conv1)
    h_conv2 = tf.nn.relu(self.conv2d(h_conv1, W_conv2, 2) + b_conv2)
    h_conv3 = tf.nn.relu(self.conv2d(h_conv2, W_conv3, 1) + b_conv3)
    h_conv3_flat = tf.reshape(h_conv3, [-1, 512])
    readout = tf.matmul(h_conv3_flat, W_fc1) + b_fc1
    return s, readout 

修正後:

  def create_network(self, action_size):
    W_conv1 = self.weight_variable([8, 8, 1, 16])
    b_conv1 = self.bias_variable([16])
    W_conv2 = self.weight_variable([4, 4, 16, 32])
    b_conv2 = self.bias_variable([32])
    W_conv3 = self.weight_variable([4, 4, 32, 64])
    b_conv3 = self.bias_variable([64])
    W_conv4 = self.weight_variable([4, 4, 64, 64])
    b_conv4 = self.bias_variable([64])
    W_conv5 = self.weight_variable([4, 4, 64, 64])
    b_conv5 = self.bias_variable([64])
    W_conv6 = self.weight_variable([4, 4, 64, 64])
    b_conv6 = self.bias_variable([64])
    W_fc1 = self.weight_variable([8, action_size])
    b_fc1 = self.bias_variable([action_size])
    s = tf.placeholder("float", [None, 13, 16, 1])
    # hidden layers
    h_conv1 = tf.nn.relu(self.conv2d(s, W_conv1, 2) + b_conv1)
    h_pool1 = self.max_pool_2x2(h_conv1)
    h_conv2 = tf.nn.relu(self.conv2d(h_pool1, W_conv2, 2) + b_conv2)
    h_pool2 = self.max_pool_2x2(h_conv2)
    h_conv3 = tf.nn.relu(self.conv2d(h_pool2, W_conv3, 1) + b_conv3)
    h_pool3 = self.max_pool_2x2(h_conv3)
    h_conv4 = tf.nn.relu(self.conv2d(h_pool3, W_conv4, 1) + b_conv4)
    h_pool4 = self.max_pool_2x2(h_conv4)
    h_conv5 = tf.nn.relu(self.conv2d(h_pool4, W_conv5, 1) + b_conv5)
    h_pool5 = self.max_pool_2x2(h_conv5)
    h_conv6 = tf.nn.relu(self.conv2d(h_pool5, W_conv6, 1) + b_conv6)
    h_pool6 = self.max_pool_2x2(h_conv6)
    h_conv6_flat = tf.reshape(h_pool6, [-1, 8])
    readout = tf.matmul(h_conv6_flat, W_fc1) + b_fc1
    return s, readout 

現段階の改良コード

import tensorflow as tf
import gym
import gym_pull
import ppaquette_gym_super_mario
#from gym.wrappers import Monitor
from gym.monitoring import Monitor
import random
import numpy as np

class Game :

  def __init__(self, episode_count=2000, eps=1.0, eps_min=1e-4):
    self.episode_count = episode_count
    self.eps = eps
    self.eps_min = eps_min
    self.eps_decay = (eps-eps_min) / episode_count;
    ## select stage
    self.env = gym.make('ppaquette/SuperMarioBros-1-1-Tiles-v0')

  def weight_variable(self, shape):
    initial = tf.truncated_normal(shape, stddev = 0.01)
    return tf.Variable(initial)

  def bias_variable(self, shape):
    initial = tf.constant(0.01, shape = shape)
    return tf.Variable(initial)

  def conv2d(self, x, W, stride):
    return tf.nn.conv2d(x, W, strides = [1, stride, stride, 1], padding = "SAME")

  def max_pool_2x2(self, x):
    return tf.nn.max_pool(x, ksize = [1, 2, 2, 1], strides = [1, 2, 2, 1], padding = "SAME")

  def create_network(self, action_size):
    W_conv1 = self.weight_variable([8, 8, 1, 16])
    b_conv1 = self.bias_variable([16])
    W_conv2 = self.weight_variable([4, 4, 16, 32])
    b_conv2 = self.bias_variable([32])
    W_conv3 = self.weight_variable([4, 4, 32, 64])
    b_conv3 = self.bias_variable([64])
    W_conv4 = self.weight_variable([4, 4, 64, 64])
    b_conv4 = self.bias_variable([64])
    W_conv5 = self.weight_variable([4, 4, 64, 64])
    b_conv5 = self.bias_variable([64])
    W_conv6 = self.weight_variable([4, 4, 64, 64])
    b_conv6 = self.bias_variable([64])
    W_fc1 = self.weight_variable([8, action_size])
    b_fc1 = self.bias_variable([action_size])
    s = tf.placeholder("float", [None, 13, 16, 1])
    # hidden layers
    h_conv1 = tf.nn.relu(self.conv2d(s, W_conv1, 2) + b_conv1)
    h_pool1 = self.max_pool_2x2(h_conv1)
    h_conv2 = tf.nn.relu(self.conv2d(h_pool1, W_conv2, 2) + b_conv2)
    h_pool2 = self.max_pool_2x2(h_conv2)
    h_conv3 = tf.nn.relu(self.conv2d(h_pool2, W_conv3, 1) + b_conv3)
    h_pool3 = self.max_pool_2x2(h_conv3)
    h_conv4 = tf.nn.relu(self.conv2d(h_pool3, W_conv4, 1) + b_conv4)
    h_pool4 = self.max_pool_2x2(h_conv4)
    h_conv5 = tf.nn.relu(self.conv2d(h_pool4, W_conv5, 1) + b_conv5)
    h_pool5 = self.max_pool_2x2(h_conv5)
    h_conv6 = tf.nn.relu(self.conv2d(h_pool5, W_conv6, 1) + b_conv6)
    h_pool6 = self.max_pool_2x2(h_conv6)
    h_conv6_flat = tf.reshape(h_pool6, [-1, 8])
    readout = tf.matmul(h_conv6_flat, W_fc1) + b_fc1
    return s, readout 

  def play_game(self) :
    action_list=[[0, 0, 0, 1, 0, 1], [0, 0, 0, 1, 1, 1]]
    sess = tf.InteractiveSession()
    #sess = tf.compat.v1.InteractiveSession
    s, readout = self.create_network(len(action_list))
    a = tf.placeholder("float", [None, len(action_list)])
    y = tf.placeholder("float", [None, 1])
    readout_action = tf.reduce_sum(tf.multiply(readout, a), reduction_indices = 1)
    cost = tf.reduce_mean(tf.square(y - readout_action))
    train_step = tf.train.AdamOptimizer(1e-6).minimize(cost)
    saver = tf.train.Saver()
    #sess.run(tf.global_variables_initializer())
    sess.run(tf.initialize_all_variables())
    checkpoint = tf.train.get_checkpoint_state("./saved_networks/checkpoint")

    if checkpoint and checkpoint.model_checkpoint_path:
      saver.restore(sess, checkpoint.model_checkpoint_path)
      print ("Successfully loaded:", checkpoint.model_checkpoint_path)
    else:
      print ("Could not find old network weights")
    for episode in range(self.episode_count):
      self.env.reset()
      total_score = 0
      distance = 0
      is_finished = False
      actions, rewards, images = [], [] ,[]
      Y = []
      self.eps = max(self.eps - self.eps_decay, self.eps_min)
      while is_finished == False :
        screen = np.reshape(self.env.tiles, (13, 16, 1))
        readout_t = readout.eval(feed_dict = {s : [screen]})[0]   # moved

        ##### added #####
        print('esp:', self.eps)
        #print('esp_decay:', self.eps_decay)
        if np.random.random() < self.eps:
          action_index = random.randint(0, len(action_list) - 1)
        else:
        #################
          action_index = np.argmax(readout_t)

        obs, reward, is_finished, info = self.env.step(action_list[action_index])
        action_array = np.zeros(len(action_list))
        action_array[action_index] = 1
        actions.append(action_array)
        rewards.append([float(info['distance'])])
        images.append(screen)

        train_step.run(feed_dict = {a : actions, y : rewards, s : images})
        print('Episode : ', episode, 'action_index : ', action_index, 'rewards:', rewards, 'readout_t:', readout_t)
        actions, rewards, images = [], [] ,[]

        self.env.render()
      saver.save(sess, 'saved_networks/model-dqn', global_step = episode)

if __name__ == '__main__' :
  game = Game()
  game.play_game()


アクションの選定だけでなく、林様のサイト
https://datumstudio.jp/blog/deepmind%E3%81%AEdqn%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0%E3%82%92%E5%86%8D%E7%8F%BE%E3%81%97%E3%81%A6%E3%81%BF%E3%81%9F
を参考に、試行ごとにランダムの割合epsを1から少しずつ減らしていく仕様にしました。

試行は2000回にしてみました。学習はできてきた感じで、epsが0.5くらいのときにめでたくゴールしました。
しかし、まだすぐゲームオーバーになったりもします。
林様のサイトのように経験再生を入れたりしてまだまだ改良できそうです。
CNNにGlobal Average Poolingを使うのも試してみたいです。

image.png


また進捗があり次第更新していきます。

19
14
4

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
14