やってみた!

やってみた!

試したことを中心に、書評や興味のあること、思ったこととか

Open AI Gym Box2D BipedalWalkerをColaboratoryで動かしてみる(1)

 Open AI Gymをご存知の方も多いと思いますが、強化学習の開発用にOpen AIがフリーで提供しているテスト環境です。pythonで書かれています。

https://gym.openai.com/

 この中にはブロック崩し、インベーダゲームを始めとするAtariの様々なゲームや、2次元の月着陸、2足歩行(この記事)、3次元の人型ロボット等の様々な課題があります。当初、3次元はMujocoという有料の物理エンジンが使われていましたが、現在はPybulletというフリーのものが使えるようです。

 この記事ではOpen AI Gymの中でBox2D(2次元の物理エンジン)を使った2足歩行のBipedalWalker-v2を試してみたいと思います。 

1.Google Colaboratoryの準備

 GoogleアカウントでログインしてColaboratoryを起動、ファイルーPython3の新しいノートブックを開きます。

 

2.Box2Dのインストール

 次の2行でBox2D自身と、Box2D対応gymをインストールします。

!pip3 install box2d-py
!pip3 install 'gym[Box2D]'
 上記を実行したら必ず「ランタイムを再起動」を実行しましょう。実行しないとインストールしてもBox2Dを認識せずに、importしようとするとエラー(BipedalWalker-v2が無いと怒られます)が起きます。毎度ながらランタイムを再起動していないのが原因だと気づくまで1時間近くかかってしまいました。

3.画像表示の準備

 表示に必要なライブラリ類をインストールします。

!apt-get update
!apt-get -qq -y install xvfb freeglut3-dev ffmpeg
!pip3 -q install pyglet
!pip3 -q install pyopengl
!pip3 -q install pyvirtualdisplay

4.動かしてみます

  次のコードで動かします。この時点では絵は出ません。2足歩行BipedalWalker-v2を動かして、後で可視化するために次のデータを保存します。

・画像をリストframesに保存します。

・状態(observation)をリストobservationsに保存します。

・指令は action=[1,-1,1,-1]を与えます。
 両足のももを速度+1で動かし、両足の膝を速度ー1で動かします。#でコメントアウトしているのはサンプルで乱数で指令を与える関数です。

 BipedalWalker-v2はソースを見ると50FPS(0.02秒ステップで計算)しているので、グラフ描画用のリストts(横軸に使う)を1/50秒ステップで作っています。

import torch
import numpy as np
import gym
from pyvirtualdisplay import Display
display = Display(visible=0, size=(1024, 768))
display.start()
import os
#pyvirtualdisplayの仕様変更で次の文はエラーになるのでコメントアウトします(2021/6/5修正)
#os.environ["DISPLAY"] = ":" + str(display.display) + "." + str(display.screen)

#Bipedalwalkerのv2は無くなったのでv3に変更(2021/6/5)
env = gym.make('BipedalWalker-v3')

for i in range(1):
    obs = env.reset()
    done = False
    frames = []
    observations = []
    ts = []
    t = 0
    while not done and t < 20:
        frames.append(env.render(mode = 'rgb_array'))
#        action = env.action_space.sample()
        action = np.array([1,-1,1,-1])
        observation, reward, done, info = env.step(action)
        if t == 0 :
          for i in range(24):
            observations.append([observation[i]])
        else :
          for i in range(24):
            observations[i].append(observation[i])
        ts.append(t/50)
        t += 1

5.表示します

 次で4.項で計算した結果を表示します。

(1)observationsを使ってグラフを書きます。

 手直しできるよう簡単に使っている命令を解説します。
 fig1=plt.figure(figsize=(10,8)) 縦10、横8の図を生成
 sub1=fig1.add_subplot(421) サブプロット縦4、横2分割の場所1に追加。
 sub1.plot(ts,observations[0],label="hull angle") 横ts、縦observations[0]の折れ線追加
                     ラベル名はhull angle

 sub1.legend()       ラベルを表示

 後は上記の繰り返しです。

(2)frames内のimageを使って動画を作成します。

  fig2 = plt.figure(figsize=(6, 6))以降で図を生成して動画を作っています。

import matplotlib.pyplot as plt
import matplotlib.animation
import numpy as np
from IPython.display import HTML

fig1 = plt.figure(figsize=(10, 8))
sub1 = fig1.add_subplot(421)
sub1.plot(ts,observations[0],label="hull angle")
sub1.plot(ts,observations[1],label="hull angle Velocity")
sub1.legend()

sub2 = fig1.add_subplot(422)
sub2.plot(ts,observations[2],label="Velocity x")
sub2.plot(ts,observations[3],label="Velocity y")
sub2.legend()

sub3 = fig1.add_subplot(423)
sub3.plot(ts,observations[4],label="Hip1 Angle")
sub3.plot(ts,observations[9],label="Hip2 Angle")
sub3.legend()

sub4 = fig1.add_subplot(424)
sub4.plot(ts,observations[5],label="Hip1 Angle Velocity")
sub4.plot(ts,observations[10],label="Hip2 Angle Velocity")
sub4.legend()

sub5 = fig1.add_subplot(425)
sub5.plot(ts,observations[6],label="Knee1 Angle")
sub5.plot(ts,observations[11],label="Knee2 Angle")
sub5.legend()

sub6 = fig1.add_subplot(426)
sub6.plot(ts,observations[7],label="Knee1 Angle Velocity")
sub6.plot(ts,observations[12],label="Knee2 Angle Velocity")
sub6.legend()

sub7 = fig1.add_subplot(427)
sub7.plot(ts,observations[8],label="Leg1 Touch Ground")
sub7.plot(ts,observations[13],label="Leg2 Touch Ground")
sub7.legend()

plt.show()

fig2 = plt.figure(figsize=(6, 6))
plt.axis('off')

images = []
for f in frames:
  image = plt.imshow(f)
  images.append([image])
ani = matplotlib.animation.ArtistAnimation(fig2, images, interval=30, repeat_delay=1)

HTML(ani.to_jshtml())

 こんな感じでしゃがみます。

f:id:akifukka:20191206232803j:plain

f:id:akifukka:20191207125142j:plain

 

 足の指令値として同じ値を与えていても、地面からの反力等の影響で必ずしも同じにはなりません。動かしてみて、足の指令(角度の速度指令)は次の方向だとわかりました。

腿(HIP)   前に出す方が +

膝(KNEE) しゃがむ方向が -

6.observation

githubでgym/envs/box2d/bipedal_walker.pyを見てみます。

    def step(self, action):
        #self.hull.ApplyForceToCenter((0, 20), True) -- Uncomment this to receive a bit of stability help
        control_speed = False  # Should be easier as well
        if control_speed:
            self.joints[0].motorSpeed = float(SPEED_HIP  * np.clip(action[0], -1, 1))
            self.joints[1].motorSpeed = float(SPEED_KNEE * np.clip(action[1], -1, 1))
            self.joints[2].motorSpeed = float(SPEED_HIP  * np.clip(action[2], -1, 1))
            self.joints[3].motorSpeed = float(SPEED_KNEE * np.clip(action[3], -1, 1))
        else:
            self.joints[0].motorSpeed     = float(SPEED_HIP     * np.sign(action[0]))
            self.joints[0].maxMotorTorque = float(MOTORS_TORQUE * np.clip(np.abs(action[0]), 0, 1))
            self.joints[1].motorSpeed     = float(SPEED_KNEE    * np.sign(action[1]))
            self.joints[1].maxMotorTorque = float(MOTORS_TORQUE * np.clip(np.abs(action[1]), 0, 1))
            self.joints[2].motorSpeed     = float(SPEED_HIP     * np.sign(action[2]))
            self.joints[2].maxMotorTorque = float(MOTORS_TORQUE * np.clip(np.abs(action[2]), 0, 1))
            self.joints[3].motorSpeed     = float(SPEED_KNEE    * np.sign(action[3]))
            self.joints[3].maxMotorTorque = float(MOTORS_TORQUE * np.clip(np.abs(action[3]), 0, 1))

        self.world.Step(1.0/FPS, 6*30, 2*30)

        pos = self.hull.position
        vel = self.hull.linearVelocity

        for i in range(10):
            self.lidar[i].fraction = 1.0
            self.lidar[i].p1 = pos
            self.lidar[i].p2 = (
                pos[0] + math.sin(1.5*i/10.0)*LIDAR_RANGE,
                pos[1] - math.cos(1.5*i/10.0)*LIDAR_RANGE)
            self.world.RayCast(self.lidar[i], self.lidar[i].p1, self.lidar[i].p2)

        state = [
            self.hull.angle,        # Normal angles up to 0.5 here, but sure more is possible.
            2.0*self.hull.angularVelocity/FPS,
            0.3*vel.x*(VIEWPORT_W/SCALE)/FPS,  # Normalized to get -1..1 range
            0.3*vel.y*(VIEWPORT_H/SCALE)/FPS,
            self.joints[0].angle,   # This will give 1.1 on high up, but it's still OK (and there should be spikes on hiting the ground, that's normal too)
            self.joints[0].speed / SPEED_HIP,
            self.joints[1].angle + 1.0,
            self.joints[1].speed / SPEED_KNEE,
            1.0 if self.legs[1].ground_contact else 0.0,
            self.joints[2].angle,
            self.joints[2].speed / SPEED_HIP,
            self.joints[3].angle + 1.0,
            self.joints[3].speed / SPEED_KNEE,
            1.0 if self.legs[3].ground_contact else 0.0
            ]
        state += [l.fraction for l in self.lidar]
        assert len(state)==24
        .
        .
        .return np.array(state), reward, done, {}

 最後の方にdef stepという記述があり、この関数が実際のモデルの中枢です。上の方で指令値actionを実際のモデルに適用して、最後の方でstate(observationに相当)を作っています。

 上記ソースコードからobservationの中身は次の通りです。

  0:胴体角度
 1:胴体角速度
 2:x方向移動速度
 3:y方向移動速度
 4:足1のHIP(腿)角度
 5:足1のHIP(腿)角速度
 6:足1のKNEE(膝)角度
 7:足1のKNEE(膝)角速度
 8:足1接地で1
 9:足2のHIP(腿)角度
10:足2のHIP(腿)角速度
11:足2のKNEE(膝)角度
12:足2のKNEE(膝)角速度
13:足2接地で1
14~23:Lidar(レーザレーダ、障害物検知用)、ここでは省略します。

actionの値を変更する等、試してみるといいと思います。

つづく

  強化学習 カテゴリーの記事一覧 - やってみた!