OpenAI Gym 入門

# 概要
強化学習のシミュレーション環境「OpenAI Gym」について、簡単に使い方を記載しました。

類似記事はたくさんあるのですが、自分の理解のために投稿しました。

# 強化学習とは
ある環境において、自律エージェントが状況を観測しながら行動することを繰り返し試行し、目的を達成するための最適な意思決定を学習する、機械学習の方法。

教師あり学習とは違い、環境から得られる報酬を元に、行動の良し悪しを評価する。

<img width="450" alt="Picture1.png" src="https://qiita-image-store.s3.amazonaws.com/0/105335/631d177e-352b-eaa3-fa0e-dac19054b71b.png">

The Go gopher was designed by Renée French.


# [OpenAI Gym](https://gym.openai.com/docs/) とは
人工知能を研究する非営利企業 [OpenAI](https://openai.com/)が作った、強化学習のシミュレーション用プラットフォーム。
オープンソース https://github.com/openai/gym


# OpenAI Gym インストール方法

１. 基本パッケージのインストール

```bash
pip install gym
```

２. 依存ライブラリインストール
OSX:

```bash
brew install cmake boost boost-python sdl2 swig wget
```

Ubuntu 14.04:

```bash
apt-get install -y python-numpy python-dev cmake zlib1g-dev libjpeg-dev xvfb libav-tools xorg-dev python-opengl libboost-all-dev libsdl2-dev swig
```

３. Atari社のGameを動かすライブラリをインストール

```bash
pip install 'gym[atari]'
```

※ その他、全環境ライブラリ

```bash
pip install 'gym[all]'
```
[公式ドキュメント](https://github.com/openai/gym#installation)


# OpenAI Gym 使い方

## 基本的な使い方
### エージェントを動かす環境
makeの引数に環境名(ゲーム名)を指定し、環境インスタンス`env`を生成

```py:gym.py
import gym
env = gym.make('MountainCar-v0')
```

生成できる環境の一覧は以下で取得できる。

```py:gym.py
from gym import envs
envids = [spec.id for spec in envs.registry.all()]
```

それぞれが何の環境かは[公式サイト](https://gym.openai.com/envs/)[*]に記載されている


環境を初期化する

```py:gym.py
observation = env.reset()
```

`reset()`にて、初期状態の観測データ`observation`が取得できる。


現在の環境を描画

```py:gym.py
env.render()
```

### 観測データ
MountainCar-v0（山登り）の場合、以下のような配列が観測データとして取得できる。
それぞれの意味は[GitHub wiki](https://github.com/openai/gym/wiki/MountainCar-v0#observation)に記載されている。

```py:gym.py
print(observation)
>> [-0.56957656  0.        ]
```

|      |     内容 |    Min    |   Max    |
|:--------|:------:|:---------:|----:|
| 0      |    車の位置 |   $-1.2$  | $0.6$ |
| 1      |    車の速度 |   $-0.07$   |  $0.07$  |


観測データの最小値と最大値は以下で取得。

```py:gym.py
# 観測データの最大値
print(env.observation_space.high)
>> [0.6  0.07]

# 観測データの最小値
print(env.observation_space.low)
>> [-1.2  -0.07]
```

環境ごとに観測データが何を示すかは異なるため、それぞれ[公式ドキュメント](https://gym.openai.com/envs/)[*]を参照。

観測データの配列次元は以下で調べられる。

```py:gym.py
print(env.observation_space)
>> Box(2,)
```

### エージェントを動かす
環境インスタンス`env`の`step()`関数の引数にアクションデータを設定する。
MountainCar-v0（山登り）の場合、アクションの値はそれぞれ以下の通り。

|   値   |     内容 |  
|:--------|:------:|
| 0      |    左へ押す |
| 1      |    何もしない |
| 2      |    右へ押す |

[GitHub wiki](https://github.com/openai/gym/wiki/MountainCar-v0#actions)に記載されている。


```py:gym.py
# 左
action = 0
observation, reward, done, info = env.step(action)
```

行動を実施した戻り値として、行動後の観測データ・報酬・ゲーム終了フラグ・詳細情報が得られる。
山登りの場合、詳細情報はなく、報酬は各ステップごとに$-1$と固定。

環境ごとの行動の値は以下で調べられる。

```py:gym.py
print(env.action_space)
>> Discrete(3)
```
`Discrete(3)`は、3つの離散値`[0, 1, 2]`

## まとめ
1. 環境を生成　`gym.make(環境名)`
2. 環境をリセットして観測データ(状態)を取得 `env.reset()`
3. 状態から行動を決定　　<font color="Magenta">⬅︎ アルゴリズム考えるところ</font>
4. 行動を実施して、行動後の観測データ(状態)と報酬を取得 `env.step(行動)`
5. 今の行動を報酬から評価する　　<font color="Magenta">⬅︎ アルゴリズム考えるところ</font>
6. 3~5.を繰り返す

# Q-Learningで山登りをクリアする
観測データ(状態)から行動を決定し、車を山の頂上の旗まで動かすことをゴールとして、学習を行う。
これは未学習状態で、プレイしたところ。
200ステップを超えてしまうか、旗までたどり着いたら終了（１エピソード終了）。

<img width="350" alt="mountain_car_ini2.gif" src="https://qiita-image-store.s3.amazonaws.com/0/105335/3a79cab7-8d9a-b814-5701-1330a8a62aae.gif">

## Q-Learningアルゴリズム
各状態における、各行動の行動価値を定めた関数を行動価値関数といい、$Q(s,a)$であらわし、以下のアルゴリズムで$Q(s,a)$を更新する。

1. $Q(s,a)$を適当に定めて初期化
2. 以下を繰り返す<br>
　2-1. $s$を初期化($s_0$)<br>
　2-2. 以下を繰り返す<br>
　　　2-2-1. $Q(s,a)$からε-グリーディ法で行動$a$を選択する。<br>
　　　2-2-2. 行動$a$を行い、報酬$R(s,a)$と次状態$s'$を観測する。<br>
　　　2-2-3. $Q(s,a)$を更新する<br>
　　　　　　　$Q(s,a) \leftarrow Q(s,a) + \alpha \bigl(R(s,a) + \gamma \max_{a' \in A(s')}Q(s', a') -Q(s,a) \bigr)$ <br>
　　　　 　　　  ※$s′$が終端状態の場合、$ \max_{ a' \in A(s')} Q(s', a')$は $0$<br>
　　　2-2-4. $s\leftarrow s′$<br>
　　　2-2-5. $s$が終端状態なら、繰り返し終了。


詳細はこちら参照。
[Qiita: DQN（Deep Q Network）を理解したので、Gopherくんの図を使って説明](https://qiita.com/ishizakiiii/items/5eff79b59bce74fdca0d#q-learning)


## 実装

### 状態の取得
観測データから取得される、車の位置と速度は連続値であるため、離散値に変換する必要がある。
40個（適当）の離散値に振り分けることとする。

```py:mountain_car.py
def get_status(_observation):
    env_low = env.observation_space.low # 位置と速度の最小値
    env_high = env.observation_space.high #　位置と速度の最大値
    env_dx = (env_high - env_low) / 40 # 40等分
    # 0〜39の離散値に変換する
    position = int((_observation[0] - env_low[0])/env_dx[0])
    velocity = int((_observation[1] - env_low[1])/env_dx[1])
    return position, velocity
```

### Qテーブルの定義
車の位置(0, 1,..., 39) × 車の速度(0, 1,..., 39) × 行動(0, 1, 2)の多次元配列を作成し、行動価値の値を格納する。
shapeは`(40,40,3)`

初期値は全部の要素を$0$とする

```py:mountain_car.py
q_table = np.zeros((40, 40, 3))
```

### Qテーブルの更新

以下のようにQテーブルを更新する。

$Q(s,a) \leftarrow Q(s,a) + \alpha \bigl(R(s,a) + \gamma \max_{a' \in A(s')}Q(s', a') -Q(s,a) \bigr)$


```py:mountain_cat.py
def update_q_table(_q_table, _action,  _observation, _next_observation, _reward, _episode):

    alpha = 0.2 # 学習率
    gamma = 0.99　# 時間割引き率

    # 行動後の状態で得られる最大行動価値 Q(s',a')
    next_position, next_velocity = get_status(_next_observation)
    next_max_q_value = max(_q_table[next_position][next_velocity])

    # 行動前の状態の行動価値 Q(s,a)
    position, velocity = get_status(_observation)
    q_value = _q_table[position][velocity][_action]

    # 行動価値関数の更新
    _q_table[position][velocity][_action] = q_value + alpha * (_reward + gamma * next_max_q_value - q_value)

    return _q_table
```

### ε-グリーディ法の実装
基本的にはQテーブルを参照して、行動価値が最大になる行動を選択するが、一定の確率でランダムに行動するようにする。

```py:mountain_car.py
def get_action(_env, _q_table, _observation, _episode):
    epsilon = 0.002
    if np.random.uniform(0, 1) > epsilon:
        position, velocity = get_status(observation)
        _action = np.argmax(_q_table[position][velocity])
    else:
        _action = np.random.choice([0, 1, 2])
    return _action
```

### メイン処理

```py:mountain_car.py
if __name__ == '__main__':

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

    # Qテーブルの初期化
    q_table = np.zeros((40, 40, 3))

    observation = env.reset()
    rewards = []
    
    # 10000エピソードで学習する
    for episode in range(10000):

        total_reward = 0
        observation = env.reset()

        for _ in range(200):

            # ε-グリーディ法で行動を選択
            action = get_action(env, q_table, observation, episode)

            # 車を動かし、観測結果・報酬・ゲーム終了FLG・詳細情報を取得
            next_observation, reward, done, _ = env.step(action)

            # Qテーブルの更新
            q_table = update_q_table(q_table, action, observation, next_observation, reward, episode)
            total_reward += reward

            observation = next_observation

            if done:
                # doneがTrueになったら１エピソード終了
                if episode%100 == 0:
                    print('episode: {}, total_reward: {}'.format(episode, total_reward))
                rewards.append(total_reward)
                break
```

## 結果
10000回学習後の車の動き。

<img width="350" alt="mountain_car_clear.gif" src="https://qiita-image-store.s3.amazonaws.com/0/105335/a2555dd6-ee21-7b48-deb7-dedb2024892b.gif">

エピソードごとのトータル報酬の推移。

<img width="550" alt="bokeh_plot.png" src="https://qiita-image-store.s3.amazonaws.com/0/105335/4be29d00-b6bf-7764-242c-e94577690af2.png">

トータル報酬が-200より大きいと、山登りをクリアしていることになる。
1500エピソード以降は頻繁にクリアするようになっているが、必ずクリアできている訳ではない。

# DQNで山登りをクリアする
DQNでは行動価値関数をネットワーク化させて、学習します。

$R(s,a) + \gamma \max_{a' \in A(s')} Q(s', a')$を教師データと考えて$\theta$を更新することとします。

詳細はこちら[Qiita: DQN（Deep Q Network）を理解したので、Gopherくんの図を使って説明](https://qiita.com/ishizakiiii/items/5eff79b59bce74fdca0d#dqn)

## DQNの実装
[Keras-RL](https://keras-rl.readthedocs.io/en/latest/)というKerasで書ける、強化学習＋ディープラーニングのライブラリがあるので、これを使いました。[keras-rl GitHub](https://github.com/matthiasplappert/keras-rl)

```py:mountaincar_dqn.py
from keras.models import Sequential
from keras.layers import Dense, Activation, Flatten
from keras.optimizers import Adam
import gym
from rl.agents.dqn import DQNAgent
from rl.policy import EpsGreedyQPolicy
from rl.memory import SequentialMemory

env = gym.make('MountainCar-v0')
nb_actions = env.action_space.n

model = Sequential()
model.add(Flatten(input_shape=(1,) + env.observation_space.shape))
model.add(Dense(16))
model.add(Activation('relu'))
model.add(Dense(16))
model.add(Activation('relu'))
model.add(Dense(16))
model.add(Activation('relu'))
model.add(Dense(nb_actions))
model.add(Activation('linear'))

memory = SequentialMemory(limit=50000, window_length=1)

policy = EpsGreedyQPolicy(eps=0.001)
dqn = DQNAgent(model=model, nb_actions=nb_actions,gamma=0.99, memory=memory, nb_steps_warmup=10,
               target_model_update=1e-2, policy=policy)
dqn.compile(Adam(lr=1e-3), metrics=['mae'])

history = dqn.fit(env, nb_steps=50000, visualize=False, verbose=2)

dqn.test(env, nb_episodes=1, visualize=True)

```

## DQNの結果
Q-learningよりずっと少ないエピソードで攻略できてますね。
なぜか、ちょっと悔しいです。

<img width="350" alt="dqn_mountain.gif" src="https://qiita-image-store.s3.amazonaws.com/0/105335/6e405ad3-1904-4fd9-126f-f469b6a5919d.gif">



<img width="550" alt="bokeh_plot_DQN.png" src="https://qiita-image-store.s3.amazonaws.com/0/105335/be36686a-bc39-ab48-c51e-6be5d1d8d6cb.png">


# おわり
強化学習のシミュレーションが、ものすごく簡単にできて興奮しました。

理論だけで終わらせず、実装まですることで、より理論の理解が深まるなぁと思ったので、OpenAI Gymを使ってどんどん試していきたいです。

# 参考
[Qiita : ゲームでAIをトレーニングするジム「OpenAI Gym」の環境構築手順 on Mac OS X](https://qiita.com/shu223/items/a9cfe9107447b327b564)
[GitHub : sezan92/ReinforcementOpenA]
(https://github.com/sezan92/ReinforcementOpenAi)

*gymの[GitHub](https://github.com/openai/gym/wiki/MountainCar-v0#details)には[公式サイト](https://gym.openai.com/envs/MountainCar-v0/)がOld linksと記載されているので、今後はGitHub上で情報が更新されるのかもしれません。