0.はじめに

MLPシリーズの『強化学習』をほとんど読み切ったので,次は強化学習をPythonで実装していきます.最終的にはスクラッチで書けるようになりたいな,とは思うのですがまずはStable Baselines3のような便利なライブラリに頼りきりで強化学習を動かして喜ぼうと思います.今回は,エージェントの行動の場となる環境を規定するのに必要なOpenAI Gymについて学んでいきます.

※MLPシリーズ『強化学習』は以下のブログ記事で解説しています.

1.OpenAI Gymの概要

OpenAI Gymは強化学習用のツールキットであり,学習に利用できる様々な環境が用意されている.いずれの環境も「行動」を渡すことで「状態,報酬,エピソード完了,情報」を返すシンプルな構造となっているため,強化学習アルゴリズムをどの環境に対してもコード改変なしに適用することができる.

環境の種類

OpenAI Gymの環境は,Environmentsのページで確認できる.環境は以下のカテゴリ別に提供されている.

  • Algorithmic:文字列の入力と出力からそれを実現するアルゴリズムを学習する環境.入力文字列の逆順の文字列を出力するタスクReverse-v0など.
  • Atari:ブロック崩しなどのAtari社のゲームでのスコアを競うアルゴリズムを学習する環境.
  • Box2D:2D物理エンジンBox2Dでの制御タスクを行う環境.
  • Classic control:CartPoleなどの古典的な制御タスクを行う環境.
  • MuJoCo:3D物理エンジンMuJoCoでに制御タスクを行う環境.
  • Robotics:ロボットアームやロボットハンドなどの環境.
  • Toy text:単純なテキストベースの古典的タスクを行う環境

2.インストール (macOS)

インストールにはXcodeとHomebrewが入っていることが前提となる.まずはターミナルで

$xcode-select -v
$brew --version

と入力し,いずれも入っていることを確認し,問題なければ仮想環境上で

$pip install gym
$pip install gym[atari]
$pip install gym[box2d]

とすることで,Alogorithmic, Classic control, Toy text, Atari, Box2Dの環境をインストールできる.MuJoCoとRoboticsについては商業ライセンスが必要となるのでここではスキップする.

3.Gymインターフェース

実際にGymを動かしてみる.「環境」の生成には,makeメソッドを使う.これにより,envはEnvオブジェクトとなる.


env = gym.make('CartPole-v1' # 環境ID.'CartPole-v1'は振り子倒立タスクの環境.
              )

Envクラスの主なメソッドは次の通り.

  • reset():環境の初期化
  • step():環境を1ステップ分更新
  • render():環境を描画
  • close():環境の終了
  • seed():乱数シードの指定

render()については,.pyファイルで動かす場合は正常に動作してくれるが,jupyter notebook上で実行するとウインドウがフリーズしてしまう,という不具合を確認した.

Envクラスの主なプロパティは次の通り.

  • action_space:行動空間
  • observation_space:状態空間

例として,CartPole環境でエージェントを動かしてみる.CartPole環境における状態は,次のように定義される.

  • カート位置 (-2.4~2.4)
  • カート速度 (-inf~inf)
  • 棒の角度 (-41.8~41.8)
  • 棒の角速度 (-inf~inf)

また,行動は

  • 0:カートの左移動
  • 1:カートの右移動

の2種類であり,報酬は「棒が倒れていなければ報酬1, 倒れればエピソード終了」と規定されている.


import gym

env = gym.make('CartPole-v1') # 環境の生成
state = env.reset()           # 環境の初期化.stateはエピソードの初期状態.

while True:
    env.render()                                 # 環境を描画
    action = env.action_space.sample()           # ランダム行動を取得
    print('action: ', action)
    state, reward, done, info = env.step(action) # 行動を環境に入力して1ステップ分更新
    print('state: ', state)
    print('reward: ', reward)
    if done: # 環境終了ならばループから離脱
        print('done')
        break

env.close()

###出力###
    action:  1
    state:  [ 0.00469949  0.20534073 -0.002991   -0.25300963]
    reward:  1.0
    action:  0
    state:  [ 0.0088063   0.01026162 -0.00805119  0.03872838]
    reward:  1.0
    action:  1
    state:  [ 0.00901154  0.20549809 -0.00727662 -0.25648387]
    reward:  1.0
    ...
    action:  1
    state:  [ 0.15868317  1.38526371 -0.22493786 -2.25842133]
    reward:  1.0
    done

Gymの状態行動空間はgym.spacesパッケージで定義されており,次の四つの型が用意されている.

  • Boxクラス:範囲[low,high]の連続値の要素を持つ多次元配列.例えば,210×160のRGB画像を状態空間とする場合は,Box(low=0, high=255, shape=(210,160,3)と定義される.
  • Discreteクラス:範囲[0,n-1]の整数.例えば,CartPoleのように「0:左移動,1:右移動」と2つの行動を表現する場合はDiscrete(2)となる.
  • MultiBinaryクラス:0または1の要素を持つ配列.「上,下,左,右」の4つのボタンを同時に押すようなシチュエーションでは,行動空間は長さ4の配列であり,例えば「上,右」ボタンを押す,という行動は[1,0,0,1]と表現される.この場合は行動空間はMultiBinary(4)と定義される.
  • MultiDiscreteクラス:範囲[0,n-1]の整数の要素を持つ配列.複数の種類の行動を必要とする場合に用いる.例えば,カーソルの移動を「0:左移動,1:右移動」,書き込む文字を「0:A, 1:B, 2:C, 3:D, 4:E, 5:なし」とするようなシチュエーションでは,行動空間は長さ2の配列であり,「右に移動しながらDを書き込む」行動は[1,3]と表現される.この場合は行動空間はMultiDiscrete([2,6])と定義される.

env = gym.make('CartPole-v1')
print('行動空間: ', env.action_space)
print('状態空間: ', env.observation_space)

###出力###
    行動空間:  Discrete(2)
    状態空間:  Box(-3.4028234663852886e+38, 3.4028234663852886e+38, (4,), float32)

4.自作の環境を作成

ここでは,Gymインターフェースの形式に準拠した自作の環境を定義する方法を学ぶ.一度方法が分かれば,自分であらゆる環境を作って強化学習を実行できるようになる.

まずはGym環境の骨組を理解するため簡単な環境を作ってみる.GoLeftEnvは,エージェントが数直線上で常に左に動くことを学ぶための環境とする.

エージェントは数直線上において0~grid_size-1までの実数値の状態を取る.初期状態は最右であるとする.例えば,grid_size=10の時,環境は以下のような数直線であり,エージェントの初期状態は9.0である.

                         (Agent)
0  1  2  3  4  5  6  7  8  9
|--|--|--|--|--|--|--|--|--|

またエージェントは,環境において「0:LEFT,1:RIGHT」の二通りの行動を選択し,エージェントの行動によって座標が1.0だけ変化することとする.ゆえに,grid_sizeが10の時,状態空間,行動空間はそれぞれBox(low=0,high=9,shape=(1,)),Discrete(2)で表現することができる.(状態は整数しか取らないので状態空間はDiscrete(9)でも良いような気がするが一般的に状態空間はBoxで表すらしい.)

では実際に,GoLeftEnvクラスを書いていく.gym.Envクラスを継承し,reset,step,renderといったメソッドを記述すれば良い.


import numpy as np
import gym
from gym import spaces

class GoLeftEnv(gym.Env):

    #---
    # Gymインターフェースに沿った自作環境.
    # エージェントが常に左に進むことを学習する環境.
    #---

    LEFT = 0
    RIGHT = 1

    def __init__(self, grid_size=10):
        super(GoLeftEnv, self).__init__()
        self.grid_size = grid_size   
        self.agent_pos = grid_size - 1 # エージェントの状態を定義.
        n_actions = 2                  # 行動は「0:LEFT, 1:RIGHT」の二通り.
        self.action_space = spaces.Discrete(n_actions)
                                       # 行動空間はDiscrete(2)
        self.observation_space = spaces.Box(low=0, high=self.grid_size,
                                            shape=(1,), dtype=np.float32)
                                       # 状態空間はBox(low=0,high=9,shape=(1,))

    def reset(self):

        #---
        # 環境を初期化するメソッド.
        # サイズ(1,)のNumPy配列を返す
        #---

        self.agent_pos = self.grid_size - 1 # 状態を初期化.最右とする.
        return np.array([self.agent_pos]).astype(np.float32)

    def step(self, action):

        #---
        # エージェントの行動(0:LEFT/1:RIGHT)を受けて環境を更新するメソッド,
        # 出力は,状態(NumPy配列),報酬,終了判定,情報の4つ.
        #---

        if action == self.LEFT:    # 0
            self.agent_pos -= 1    # 左に動く場合は座標が1だけ減る.
        elif actcion == self.RIGHT: # 1
            self.agent_pos += 1
        else:
            raise ValueError(f'Received invalid action={action}' + 
                             f' is not part of the action space.')

        # 状態を[0,grid_size-1]区間に収める.
        # 座標9にいる時に行動1:RIGHTを選択すると状態空間にない10に移動してしまうため,
        # そのような場合は9から動かないこととする.
        self.agent_pos = np.clip(self.agent_pos, 0, self.grid_size)
        done = bool(self.agent_pos == 0)         # 終了判定.左端(0)に着いたら終了とする.
        reward = 1 if self.agent_pos == 0 else 0 # 報酬.左端に着いたら報酬1を受け取る.
        info = {}                                # infoはなくても良いので空辞書にしておく.
        return np.array([self.agent_pos]).astype(np.float32), reward, done, info

    def render(self, mode='console'):

        #---
        # 環境をコンソール上に描画するメソッド.
        #---

        if mode != 'console':
            raise NotImplementedError()
        print('.' * self.agent_pos, end='')
        print('x', end='')
        print("." * (self.grid_size - self.agent_pos))

    def close(self):
        pass

実際に作った環境でエージェントを動かしてみる.


import gym

env = GoLeftEnv()  # 環境の生成
state = env.reset() # 環境の初期化.stateはエピソードの初期状態.

while True:
    env.render()                                 # 環境を描画
    action = env.action_space.sample()           # ランダム行動を取得
    print('action: ', action)
    state, reward, done, info = env.step(action) # 行動を環境に入力して1ステップ分更新
    print('state: ', state)
    print('reward: ', reward)
    if done: # 環境終了ならばループから離脱
        print('done')
        break

env.close()

###出力###
.........x.
action:  0
state:  [8.]
reward:  0
........x..
action:  0
state:  [7.]
reward:  0
.......x...
action:  1
state:  [8.]
reward:  0
........x..

...

.x.........
action:  1
state:  [2.]
reward:  0
..x........
action:  0
state:  [1.]
reward:  0
.x.........
action:  0
state:  [0.]
reward:  1
done

以上より,自作の環境でエージェントを訓練させることができるようになった.

5.参考