4/14(日) 足・靴・木型研究会「第2回研究集会」を開催します☆彡

9-4. 遺伝的アルゴリズム(vcopt)で二足歩行を最適化する

本記事の内容は2019年5月13日に更新されました。

やること

OpenAI Gymには二足歩行の学習用の環境が用意されています。vcoptを使って、GAで二足歩行を最適化してみましょう。

参考にさせていただいたサイト

本当にありがとうございます。

https://gym.openai.com/envs/BipedalWalker-v2/
BipedalWalker v2
A toolkit for developing and comparing reinforcement learning algorithms. - openai/gym
BipedalWalker/BipedalWalker.ipynb at master · mayurmadnani/BipedalWalker
BipedalWalker - AI teaching itself to walk. Contribute to mayurmadnani/BipedalWalker development by creating an account on GitHub.

実行環境

Google Colaboratoryが利用可能です。

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))というサイクルを繰り返すことになったようです。日本語で言うと、左膝伸ばす→右脚後ろに出す→右脚前に出す→右膝曲げる、のサイクルです。

目を閉じて走るので、人間みたいな走り方だとすぐに転んでしまいます。よって、安定性重視のチキチキ走法になることは必然と言えるでしょう。ウサインボルトを目指すならば、自分の姿勢や地面の状況を見て判断するように、強化学習させたいところです。

タイトルとURLをコピーしました