本記事の内容は2019年5月13日に更新されました。
やること
OpenAI Gymには二足歩行の学習用の環境が用意されています。vcoptを使って、GAで二足歩行を最適化してみましょう。
参考にさせていただいたサイト
本当にありがとうございます。
実行環境
Google Colaboratoryが利用可能です。
vcoptの使い方についてはチュートリアルをご参照ください。
vcoptの仕様については最新の仕様書をご参照ください。本記事執筆時とは仕様が異なる場合があります。
pip install
Google Colaboratoryはセッションが切れる度にpip installし直す必要があります。gymは最初から入っています。
#========================================================
#Colabに初期装備されていないパッケージのインストール
#========================================================
!pip install vcopt
!pip install box2d-py
次に、Colabでgym環境の画像表示(env.render()とか)をするためのおまじないその1です。
#========================================================
#おまじない1
#========================================================
!apt-get install xvfb
!pip install pyvirtualdisplay
おまじないその2です。
#========================================================
#おまじない2
#========================================================
from pyvirtualdisplay import Display
display = Display(visible=0, size=(1024, 768))
display.start()
import os
os.environ["DISPLAY"] = ":" + str(display.display) + "." + str(display.screen)
import
まずは、今回使うパッケージをインポートします。
#========================================================
#gym関連のimport
#========================================================
import Box2D
import gym
#========================================================
#プロット関連のimport
#========================================================
import matplotlib.pyplot as plt
from matplotlib import animation, rc
from PIL import Image
#========================================================
#その他のimport
#========================================================
import os
import numpy as np
import numpy.random as nr
#========================================================
#vcopt関連のimport
#========================================================
from vcopt import vcopt
二足歩行の動作確認
こんな感じで、動作確認できました。
#========================================================
#動作テスト
#========================================================
#環境の選択と初期化
env = gym.make('BipedalWalker-v2')
state = env.reset()
#actionの確認
print(env.action_space)
#stateの確認
print(state.shape)
#画像をスタックする配列の準備
fig = plt.figure()
ims = []
#初期画像をスタック
im = plt.imshow(env.render(mode='rgb_array'))
ims.append([im])
#ランダムにアクションしながらスタック
for i in range(50):
#[*, 0, 0, 0]: 左脚 +1なら前に回す -1なら後ろに回す
#[0, *, 0, 0]: 左膝 +1ならピンと伸ばす -1なら90°まで曲げる
#[0, 0, *, 0]: 右脚 +1なら前に出す -1なら後ろに出す
#[0, 0, 0, *]: 右膝 +1なら伸ばす -1なら90°くらいまで曲げる
action = nr.rand(4) * 2 - 1
#状態の更新
state, reward, done, info = env.step(action)
#画像をスタック
im = plt.imshow(env.render(mode='rgb_array'))
ims.append([im])
#終了判定
if done == True:
break
#アニメーション表示の準備
print('making animation......')
print('len(ims) = ' + str(len(ims)))
ani = animation.ArtistAnimation(fig, ims, interval=15, blit=True)
#ここにアニメを表示する場合はこちらを有効に
rc('animation', html='jshtml')
ani
#.gifや.mp4を保存する場合はこちらを有効に
#ani.save('walker.gif', writer='imagemagick')
#ani.save('walker.mp4', writer="ffmpeg")
Box(4,)
(24,)
making animation......
len(ims) = 51
action_space は Box(4, ) です。コード中にも注釈しましたが、[0, 0, 0, 0](各々-1~+1)という配列を env.step() に渡して状態を更新します。
actionの詳細は次のとおりです。
- [*, 0, 0, 0]: 左脚 +1なら前に回す -1なら後ろに回す
- [0, *, 0, 0]: 左膝 +1ならピンと伸ばす -1なら90°まで曲げる
- [0, 0, *, 0]: 右脚 +1なら前に出す -1なら後ろに出す
- [0, 0, 0, *]: 右膝 +1なら伸ばす -1なら90°くらいまで曲げる
state は (24, ) と表示されたとおり24個の数値列です。いろいろな部位の角度や、膝が地面についているかといった情報が含まれます。今回は使用しません。
rewardは、足を動かすとエネルギーを消費してマイナスになり、前に進むとプラスになるようです。その場でジタバタしているとマイナスになります。
何を最適化するのか
まず、簡単な離散的GAを適用するために、actionを8種の遺伝子に対応させます。
#pからactionへの変換用
p_action = [[+1, 0, 0, 0],
[-1, 0, 0, 0],
[0, +1, 0, 0],
[0, -1, 0, 0],
[0, 0, +1, 0],
[0, 0, -1, 0],
[0, 0, 0, +1],
[0, 0, 0, -1]]
遺伝子が0であれば、actionは [+1, 0, 0, 0] となり、左膝を前に出すことになります。あるいは遺伝子が2であれば、actionは [0, +1, 0, 0] となり、左膝を伸ばします。本来はこれら4つの関節は同時に動かせますが、簡単のために1フレームに1つの関節しか動かせないことにします。
さて、左右の足を交互に回してほしいのですが、足の1サイクルに何フレームを割り当てればよいかが分かりませんので、遺伝子配列の長さが決められません。そこで、遺伝子長を16とし、偶数番目の遺伝子はactionを、奇数番目の遺伝子はactionの継続フレーム数を表すことにします。例えば、遺伝子配列が[4, 1, 5, 3, 5, 8, 4, 10, 1, 8, 1, 2, 5, 5, 6, 6]であれば、
- action 4 を 1 フレーム
- action 5 を 3 フレーム
- action 5 を 8 フレーム
- action 4 を 10 フレーム
- action 1 を 8 フレーム
- action 1 を 2 フレーム
- action 5 を 5 フレーム
- action 6 を 6 フレーム
を1サイクルとして、ゲームオーバーまで繰り返します。注意点ですが、こいつは周りの状況をまったく見ずに、目を閉じてジタバタしていることを忘れないでください。できるだけ遠くまで走れるように、vcopt().dcGA()で遺伝子配列を最適化します。
(必須)評価関数
入力配列であるparaを受け取り、ゲームオーバーまでプレイします。スコアとして、rewardの積算値を返します。
#========================================================
#評価関数
#========================================================
def walker_score(para):
#ゲームの選択と初期化
env = gym.make('BipedalWalker-v2')
state = env.reset()
#pからactionへの変換用
p_action = [[+1, 0, 0, 0],
[-1, 0, 0, 0],
[0, +1, 0, 0],
[0, -1, 0, 0],
[0, 0, +1, 0],
[0, 0, -1, 0],
[0, 0, 0, +1],
[0, 0, 0, -1]]
#偶数番目をactionに、奇数番目をフレーム数に割り当てる
para_action = para[0::2]
para_frame = para[1::2]
#paraをゲームオーバーまでプレイ
score = 0
end = False
while 1:
for i in range(len(para_action)):
for _ in range(para_frame[i]):
#状態の更新
state, reward, done, info = env.step(p_action[para_action[i]])
#scoreの加算
score += reward
#終了判定
if done == True:
end = True
break
if end == True:
break
if end == True:
break
return score
(任意)poolの可視化関数
poolを受け取って、エリート個体のゲームオーバー時の1コマを見ることにしましょう。pool[best_index ]がparaに相当します。
#========================================================
#poolの可視化(エリートの終了時の画像表示)
#========================================================
def walker_show_pool(pool, **info):
#GA中の諸情報はinfoという辞書に格納されて渡されます
#これらを受け取って使用することができます
gen = info['gen']
best_index = info['best_index']
best_score = info['best_score']
mean_score = info['mean_score']
mean_gap = info['mean_gap']
time = info['time']
#ゲームの選択と初期化
env = gym.make('BipedalWalker-v2')
state = env.reset()
#pからactionへの変換用
p_action = [[+1, 0, 0, 0],
[-1, 0, 0, 0],
[0, +1, 0, 0],
[0, -1, 0, 0],
[0, 0, +1, 0],
[0, 0, -1, 0],
[0, 0, 0, +1],
[0, 0, 0, -1]]
#エリートの偶数番目をactionに、奇数番目をフレーム数に割り当てる
para_action = pool[best_index, 0::2]
para_frame = pool[best_index, 1::2]
#エリートのparaをゲームオーバーまでプレイ
score = 0
end = False
while 1:
for i in range(len(para_action)):
for _ in range(para_frame[i]):
#状態の更新
state, reward, done, info = env.step(p_action[para_action[i]])
#scoreの加算
score += reward
#終了判定
if done == True:
end = True
break
if end == True:
break
if end == True:
break
#世代情報の表示
print('gen={}, mean_score={}, best_score={}, time={}'.format(gen, mean_score, best_score, time))
print(",".join(map(str, pool[best_index])))
#エリートの最後の1コマだけ表示
plt.imshow(env.render(mode='rgb_array'))
plt.show()
GAで最適化
para_rangeを用意しますが、偶数番目はactionの選択肢である0~7を、奇数番目はフレーム数の選択肢として1~12を並べます。vcopt().dcGA()を実行します。スコアが大きい方向へ最適化しますので、第3引数は大きい数(999999など)にします。また、本来は個体数は自動設定されますが、ここでは時間短縮のためにpool_num=100のコマンドを追加しています。
#========================================================
#GAで最適化
#========================================================
#パラメータ範囲
para_range = []
for _ in range(8):
para_range.append([0, 1, 2, 3, 4, 5, 6, 7])
para_range.append([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
#GAの実行
para, score = vcopt().dcGA(para_range, #para_range
walker_score, #score_func
999999, #aim
show_pool_func=walker_show_pool, #show_para_func=None
seed=None, #seed=None
pool_num=100) #個体数を100に設定
#結果の表示
print(para)
print(score)
0世代目:スコア4.7(初期の100体の中に姑息な手段で進むやつがいた)
gen=0, mean_score=-94.3486, best_score=4.7131, time=8.51
4,1,5,3,5,8,4,10,1,8,1,2,5,5,6,6
100世代目:スコア85(足つった走法、安定感抜群)
gen=100, mean_score=-68.4066, best_score=84.7999, time=29.73
4,1,5,3,5,8,4,10,4,4,2,6,7,2,7,10
300世代目:スコア167(開始時はランダムな姿勢でランダムな地面に着地するので、たまたまハイスコアが出た個体らしい。基本的にエレガンスな転倒死)
gen=300, mean_score=-19.3135, best_score=167.0414, time=89.33
4,12,2,11,2,9,5,11,7,1,4,6,6,10,5,1
600世代目:スコア179(お馬さん走りを習得!)
gen=600, mean_score=36.0341, best_score=178.6304, time=228.83
4,12,2,9,2,9,5,11,3,1,4,9,6,3,3,10
5000世代目:スコア289(ほぼ収束、最終形態)
gen=5000, mean_score=260.7186, best_score=288.5554, time=2382.1
2,2,5,3,5,7,4,12,7,1,4,5,7,7,2,10
リプレイ用のコード
#========================================================
#特定のparaでリプレイ
#========================================================
#paraを指定
para = np.array([2,2,5,3,5,7,4,12,7,1,4,5,7,7,2,10])
#偶数番目をactionに、奇数番目をフレーム数に割り当てる
para_action = para[0::2]
para_frame = para[1::2]
#pからactionへの変換用
p_action = [[+1, 0, 0, 0],
[-1, 0, 0, 0],
[0, +1, 0, 0],
[0, -1, 0, 0],
[0, 0, +1, 0],
[0, 0, -1, 0],
[0, 0, 0, +1],
[0, 0, 0, -1]]
#ゲームの選択と初期化
env = gym.make('BipedalWalker-v2')
state = env.reset()
#画像をスタックする配列の準備
fig = plt.figure()
ims = []
#初期画像をスタック
im = plt.imshow(env.render(mode='rgb_array'))
ims.append([im])
#paraをゲームオーバーまでプレイ
score = 0
end = False
while 1:
for i in range(len(para_action)):
for _ in range(para_frame[i]):
#状態の更新
state, reward, done, info = env.step(p_action[para_action[i]])
#scoreの加算
score += reward
#画像をスタック
im = plt.imshow(env.render(mode='rgb_array'))
ims.append([im])
#終了判定
if done == True:
end = True
break
if end == True:
break
if end == True:
break
#アニメーション表示の準備
print('making animation......')
print('len(ims) = ' + str(len(ims)))
ani = animation.ArtistAnimation(fig, ims, interval=15, blit=True)
#ここにアニメを表示する場合はこちらを有効に
rc('animation', html='jshtml')
ani
#.gifや.mp4を保存する場合はこちらを有効に
#ani.save('walker.gif', writer='imagemagick')
#ani.save('walker.mp4', writer="ffmpeg")
結論
最適化の結果、2(12)→5(10)→4(17)→7(8)(action(frames))というサイクルを繰り返すことになったようです。日本語で言うと、左膝伸ばす→右脚後ろに出す→右脚前に出す→右膝曲げる、のサイクルです。
目を閉じて走るので、人間みたいな走り方だとすぐに転んでしまいます。よって、安定性重視のチキチキ走法になることは必然と言えるでしょう。ウサインボルトを目指すならば、自分の姿勢や地面の状況を見て判断するように、強化学習させたいところです。