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

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という単位に直して処理している。

- タイルには以下の４種類のうちの何かが定義されている
      - 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 <module>
    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が得られる。

##メモ

<!-- 
以下はnes_env.pyに記載されている。

- "total reward"はx軸の距離である。episode_rewardはエピソードごと（１ゲーム）ごとのtotal rewardである。_get_episode_reward関数はself.episode_rewardを返す。
- get_total_reward関数は"total_reward"を返す。(しかしget_total_reward関数の出力をみるとtotal_scoreになっている？これが全レベルの平均の合計のreward？) 
- - "reward"はアクションごとに定義される。_reset関数によって0になる。_get_reward関数はself.rewardを返す。
- reward = total_reward - prev_total_reward

-下記のようにstd_rewardはepisode_rewardを0から1000までに制限したものである。

```
    def _get_standard_reward(self, episode_reward):
        # Can be overridden
        std_reward = episode_reward
        std_reward = min(1000, std_reward)                                  # Cannot be more than 1,000
        std_reward = max(0, std_reward)                                     # Cannot be less than 0
        return std_reward
```
-->

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のバージョンによっては

```
ModuleNotFoundError: No module named 'gym.configuration'
```
や

```
AttributeError: module 'gym.monitoring' has no attribute 'monitor'
```

というエラーが出た。
これは
/home/(username)/.local/lib/python3.6/site-packages/gym
以下に
configuration.py
や
monitoring\monitor.py
がないときに発生する。

これらを含むバージョンを探したところ、0.4.0だった。

gymをいったんインストールし、バージョン指定（ここでは0.4.0）してインストールするには

```
pip3 uninstall gym 
pip3 install gym==0.4.0
```
とする。

試行錯誤した結果、gym==0.4.0以外にも
gym==0.10.5
gym==0.15.4
をインストールする必要があった。

gymをインストールした後、

```
pip3 install ppaquette_gym_super_mario
```
でppaquette_gym_super_marioをインストールする必要がある。


※
ppaquette/gym-super-marioは
gym==0.10.5、
tsunaki00/super_marioは
gym>=0.2.3　（0.2.3以上のバージョンという意味だろう）と
requirements.txtに記載されている。
)

gym/configuration.py -> gym==0.4.0あたり
gym/monitoring/monitor.py -> gym==0.9.0以上

---
-->


```
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'
```
というエラーは
上記の「準備」に記載した手順で環境を作ると出なくなった。

<!-- 

---

```
  File "mario.py", line 108, in <module>
    game.play_game()
  File "mario.py", line 103, in play_game
    self.env.render()
  File "/home/okada/.local/lib/python3.6/site-packages/gym/core.py", line 284, in render
    return self.env.render(mode)
  File "/home/okada/.local/lib/python3.6/site-packages/gym/core.py", line 111, in render
    raise NotImplementedError
NotImplementedError
```
このエラーは
  def play_game(self) :
を
  def play_game(self, render=False) :
に、
self.env.render()
を
if render:
  self.env.render()
に変更すると解決した。

-->

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

修正前：

```
    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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/543160/4209ed2a-5245-8523-6bad-e675e257c283.png)

---

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