やってみた!

やってみた!

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

PyBulletでURDF(Unified Robot Description Format)  URDF解説編(2)

 前回はURDFの基本構造の紹介と、Colaboratoryを使ってPyBulletにhumanoid.urdfを読み込ませ、表示させる方法を紹介しました。

  今回はhumanoid.urdfの中身を見ていきます。

1.PyBulletのHumanoid.urdfの用途について

 PyBulletのプロジェクトにあるhumanoid.urdfは宙返り(backflip)学習サンプル用の人型モデルのようです。PyBulletサイトに簡単な紹介がありました。

DeepMimic: Example-Guided Deep Reinforcement Learning of Physics-Based Character Skills | Bullet Real-Time Physics Simulation

 基になっているサイトはこちらで、モーションキャプチャーデータをガイドとし、物理シミュレータPyBullet内のロボット制御を深層強化学習させるといった興味深いものです。宙返りだけでなく、さまざまな動きに挑戦しています。

DeepMimic: Example-Guided Deep Reinforcement Learning of Physics-Based Character Skills

2.humanoid.urdfの中身

 humanoid.urdfはinertialとcollisionのみを定義していてvisualは定義してませんが、前回の記事で紹介した様にPyBulletで読み込むとちゃんと表示されます。visualの代わりにcollisionのgeometryが表示されている様です。

 humanoid.urdfは次のリンク先にあります。リンク先を開いて画面右上のRawボタンを押すとhumanoid.urdfをダウンロードできます。

リンク :bullet3/humanoid.urdf at master · bulletphysics/bullet3 · GitHub

 urdfは前回の記事で紹介した通り、ロボットを部品であるlink(parent、child)をjointで結合して定義します。jointはparent linkの座標系をjoint座標系に変換します。child linkの座標系はjoint座標系と同じになります。

 humanoid.urdfを読み、linkを整理すると次の図になります。

f:id:akifukka:20200210220635j:plain

 各linkの慣性モーメント(Inertia)は、Inertiaマトリクスの要素9つのうち、同じ値になる3つを除いた6つで定義します。また、通常はInertiaマトリクスの対角要素以外は0になります。

 URDFの単位はmass(質量)はkg、長さはm、inertia(慣性モーメント)はkgm2です。

 次にhumanoid.urdfのjointの位置を整理します。

f:id:akifukka:20200211175944j:plain

 ロボットの基準位置は腰の中心で、脚の付け根のjointと同じ高さにあります。URDF読み込み時にスケール1/4としているのでhumanoidの高さは約1.62mです。重量は各linkのmassを単純に加算すると45kgでした。

 黒の線はjointのoriginで定義されている座標変換です。例えば次のように定義されていたとすると、

<link name="right_hip" >
</link>
<joint name="right_hip" type="spherical" >
  <parent link="root" />
  <child link="right_hip" />
</joint>
<link name="right_knee" >
  <collision>
    <origin rpy = "-1.570796 0 0" xyz = "0.000000 -0.800000 0.000000" />
    <geometry>
      <capsule length="1.240000" radius="0.200000"/>
    </geometry>
  </collision>

</link>
<joint name="right_knee" type="revolute" >
  <parent link="right_hip" />
  <child link="right_knee" />
  <origin rpy = "0 0 0" xyz = "0.0 -1.686184 0.0" />
</joint>

 link "right_knee"の座標原点はparent_linkであるright_hipの原点=joint "right_hip"からxyz = "0.0 -1.686184 0.0"だけ 移動します。上図の黒の線はこの移動量を示しています。同様に青の線はlinkの中で定義されているcollisionのorignと等しく、linkの原点=joint位置からの移動量-0.8を示しています。

 また、linkの円筒形の回転は単位がラジアンなので、-1.57=-2/π = 90度回転させています。

 humanoidを見ると、URDFでのロボット定義は基本的にlinkのinertial、collisionを与えてjointで結合するだけでよさそうです。

 なお、humanoidのjointはspherical×8、revloute×4の計12でした(固定であるfixedを除きます)。

つづく

 URDFの仕組みがある程度わかったところで、次回はオリジナルのロボットを定義してみようと思います。

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

PyBulletでURDF(Unified Robot Description Format)  URDF解説編(1)

 前回までは3D物理シミュレータBulletのpythonラッパーPyBulletで動くGym,HumanoidFlagrun(Harder)BulletEnv-v0を使い深層強化学習を試してみました。

 本記事では、オリジナルのロボットのシミュレーション環境を構築できる様、まずはURDFについて調べてみます。

 URDF(Unified Robot Description Format) はロボットの構造を示すテキストファイルで、ROS(Robot Operating System)で規定されています。PyBulletでもURDFで定義されたロボットモデルを扱うことができ、URDFはロボットシミュレーションモデルのデファクトスタンダードに近いものになりつつあります。

 なお、OPEN AI GymのモデルはMJCFと呼ばれるMuJoCo形式で記述されています。PyBulletはMJCFにも対応しているようです。

注意
 URDFに関するPyBulletとROSの互換性については確認していません。本記事のURDFファイルはPyBulletで動作確認しており、ROSでは動かない可能性があります。
 また、URDFのタグの解説はROSのチュートリアルを参考にしており、タグ個別にPyBulletで動作確認していません。そのためタグ解説で紹介していても、PyBulletで動かないタグがあるかも知れません。

 参考にしたROSのURDF(xml)のタグマニュアル、チュートリアルのページです。

urdf/XML - ROS Wiki

urdf/Tutorials - ROS Wiki

 Bullet(PyBullet)のホームぺージです。PyBulletのドキュメントにURDFタグの解説が見あたりませんでした。ただし、PyBulletのGithub上にサンプルのURDFがたくさんあります。

Bullet Real-Time Physics Simulation | Home of Bullet and PyBullet: physics simulation for games, visual effects, robotics and reinforcement learning.

 STL(3Dデータ)作成に使えそうなフリーの3D CADです。現時点では試せていませんが、忘れないようにリンクを貼っておきます。

1.URDFの初歩

 前書きが長くなってしまいましたが、ROSのチュートリアルのページの内容を参考にURDFのフォーマットを見ていきます。

urdf/Tutorials/Building a Visual Robot Model with URDF from Scratch - ROS Wiki

 まずは、実際に次のURDFを例に意味を調べます。


<?xml version="1.0"?>
<robot name="origins">
 <link name="base_link">
  <visual>
   <geometry>
    <cylinder length="0.6" radius="0.2"/>
   </geometry>
  </visual>
 </link>
<link name="right_leg"> <visual> <geometry> <box size="0.6 0.1 0.2"/> </geometry> <origin rpy="0 1.57075 0" xyz="0 0 -0.3"/> </visual> </link> <joint name="base_to_right_leg" type="fixed"> <parent link="base_link"/> <child link="right_leg"/> <origin xyz="0 -0.22 0.25"/> </joint> </robot>

URDFタグの解説

  • <?xml version="1.0"?>
    xmlバージョン1.0形式のファイルであることを宣言しています。
  • <robot name="origins">~</robot> urdf/XML/robot - ROS Wiki
     ロボットの定義を示すelement(要素)です。ロボットに含まれる各要素は全てこの間に記述します。ロボットは手、足、胴体などの構成部品linkと、接続部を示すjointで構成されます。
  • <link name="xxxx">~</link> urdf/XML/link - ROS Wiki
    linkの定義を示すelementです。
    <inertial>~</inertial>(optional) シミュレーション用の質量、慣性モーメント
     <origin>(optional) link座標系から見た重心位置、姿勢
     <inertia> 慣性モーメント(kgm2)
     <mass> 質量(kg)
    <visual>~</visual>(optional) 表示用データ
     <origin>(optional) link座標系から見た形状の中心位置、姿勢
     <geometry>(required) 形状
      <box>,<cylinder>,<sphere> 標準で用意されている形状
      <mesh> meshで定義される任意の形状、3DCAD等で作成します
     <material> (optional) 色、テクスチャ
    表示の外形、座標
    <collision>~</collision>(optional) 衝突判定用データ
     <origin> (optional) visualのoriginと同じ
     <geometry> visualのgeometryと同じ
  • <joint name="xxxx" type="aaaa">~</joint> urdf/XML/joint - ROS Wiki
    jointの定義を示すelementです。
    typeには次の種類があります(PyBullet)。
    ・revolute 軸方向に回転、上限、下限あり
    ・spherical 3軸自由回転(ボール接続)。ROSのマニュアルに本jointの説明を見つけられなかったのですがPyBulletで実際に使われています。互換性は?
    ・continuous 軸方向に回転、制限なし
    ・prismatic 軸方向にスライド、上限、下限あり
    ・fixed 固定
    ・floating 3軸方向、3軸回転
    ・planar  軸に垂直な平面上に移動可能
    <parent> (required) joint接続される側の親link
    <child> (required) joint接続する子link
    <origin> (optional) jointの座標系=子linkの座標系を与えます。親linkの座標系で指示(親link座標系→子link座標系への変換)します。省略すると子linkの座標系=親linkの座標系になります。
    <axis>(optional: defaults to (1,0,0)) jointの回転軸をjointの座標系(=子linkの座標系)で示します。軸の指定が必要無いtypeにはaxisは影響しません。
    <limit> (required only for revolute and prismatic joint) 

URDFでは次のようにロボットを定義します。
(1)linkで部品を定義し、jointで親linkと子linkを接続します。
(2)jointの座標系と子linkの座標系は同じです。joint定義中のorigin親linkの座標系からjointの座標系(=子linkの座標系)への変換定義します。
(3)jointが回転できる場合、回転中心はjoint座標系(=子link座標系)の原点です。回転軸はjoint座標系で別途設定します。

 上のURDF例は下図を記述しています。

f:id:akifukka:20200203215323j:plain

 2.PyBulletでhumanoidモデルを表示させてみる

 次にPyBulletのサンプルにあったhumanoidモデル(humanoid.urdf)をPyBulletで表示させてみます。ここではColaboratoryを使いますが、ローカル環境を使うとExample Browser等が使えるので、ローカル環境を構築して試してみてもいいかも。

フォルダ:bullet3/examples/pybullet/gym/pybullet_data/humanoid/humanoid.urdf

リンク :bullet3/humanoid.urdf at master · bulletphysics/bullet3 · GitHub

(1)humanoid.urdfのダウンロード
 リンク先を開いて画面右上のRawボタンを押しhumanoid.urdfをダウンロードして、ダウンロードフォルダからパソコン上の任意の場所に移動、保存します。

(2)Colaboratoryノートブックの作成
 ColaboratoryでPython3の新しいノートブックを作成、ランタイムのタイプはGPU無しで問題ありません。
 ノートブックに適当な名前をつけます。

(3)ライブラリをインストールします

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

 (4)humanoid.urdfをパソコンからColaboratoryにアップロードします
 なお、Colaboratoryのアップロードは上書きしてくれないので、事前にurdfファイルを削除しています。

!rm *.urdf
from google.colab import files
uploaded = files.upload()

 上記を実行すると表示される「ファイル選択」ボタンを押してパソコンに保存したhumanoid.urdfを選択します。

(5)PyBulletで表示させます

import matplotlib
import matplotlib.pyplot as plt
import numpy as np
from pyvirtualdisplay import Display
import pybullet as p
from pybullet_utils import bullet_client
import time

#表示の準備
display = Display(visible=0, size=(1024, 768))
display.start()
import os
os.environ["DISPLAY"] = ":" + str(display.display) + "." + str(display.screen)

#PyBullet開始
p.connect(p.DIRECT)
#PCの場合はp.GUI接続もあり
#p.connect(p.GUI)
#PyBulletで複数モデルをシミュレーションする場合はclientを作成し、
#以降はpの代わりに_pを使う(参考)。
#_p=bullet_client.BulletClient()

#urdfファイルをPyBulletに読み込む
p.loadURDF("humanoid.urdf")

#表示のためのカメラ設定
view_matrix = p.computeViewMatrixFromYawPitchRoll(
			cameraTargetPosition=[4,-4,3],
			distance=2.,
			yaw=30,
			pitch=-30,
			roll=0,
			upAxisIndex=2)

#表示のための投影マトリクス計算
proj_matrix = p.computeProjectionMatrixFOV(
			fov=60, aspect=320/240,
			nearVal=0.1, farVal=100.0)

#描画計算
(_, _, px, _, _) = p.getCameraImage(512,512,viewMatrix=view_matrix,projectionMatrix=proj_matrix,renderer=p.ER_BULLET_HARDWARE_OPENGL)

#pyplotで画面に表示させるためリスト形式をnumpy array形式に変換
#alphaを削除して表示
rgb_array = np.array(px)
rgb_array = rgb_array[:, :, :3]

plt.axis('off')
plt.imshow(rgb_array)

p.disconnect() 

 こんな図(humanoidが横たわった図)が表示されるはずです。

f:id:akifukka:20200209124618j:plain


 基本的動作についてはコメントを挿入しているので説明不要だと思います。若干補足します。

・p.connect(p.DIRECT)
 p.DIRECTはPyBulletへの接続(データ、通信授受の方法)を示す引数で、直接データ授受するという意味です。この他、GUI:グラフィカル環境経由でデータ授受、UDPUDP通信でデータ授受等いくつかのオプションがあります。
 ColaboratoryではGUIは使えませんが、ローカルPCで使う場合はGUIの方が便利かも(試していません・・・)。

・#_p=bullet_client.BulletClient()
 クライアント別にPyBulletを起動、独立した環境を構築できます。主にPyBulletサーバ用でしょうか。参考にこんな使い方もできますという意図としてコメントとして残していますが、基本的には必要ありません。

 PyBulletのコマンドの詳細は次を参照してください。

3.まとめ

  • 3D物理シミュレータのPythonラッパーPyBulletで使えるロボットモデル定義ファイルURDFの基本構造を解説しました。
     PythonベースであるPyBulletでロボットモデルを構築すれば、無理なく通常の深層学習フレームワークを使って学習させることができるはずです。
  • PyBulletのサンプルにあったロボットモデルhumanoid.urdfをPyBulletに読み込ませて表示させる方法を紹介しました。

つづく
 次回は実際のhumanoid.urdfの定義(ファイルの中身)を見てみます。

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

PyBullet-HumanoidFlagrunHarderBulletEnv-v0(4)

  次はHumanoidFlagrunを学習させてみます。Humanoidの学習に使ったソースコードのenvを定義している箇所を入れ替え(コメントアウト)て動かします。ソースは前々回のこちらの記事を見てください。

  ちなみに、学習を進めても全然報酬が増えず、Google Colaboratoryの時間制限がやっかいになってきたので、ローカルランタイムで動かすことにしました。深層強化学習のようにニューラルネットワークがさほど大きくなければ、GPU無しのノートPCでもそこそこ動きます。

 Colaboratoryをローカルランタイムで動かす手順はこちらまで。

1. HumanoidFlagrun

 PyBullet-HumanoidFlagrunHarderBulletEnv-v0(2)の記事中のソースコードのenvを定義している行のコメントアウトを入れ替えて学習を始めました。が、進捗はあまり良くありません。もしかするとBATCH_SIZEを256→512に増やすとか、パラメータを見直した方がいいのかも(ただしBATCH増やすと遅くなるかもしれません)。

1月28日
 ここまでで、既に4日くらい学習させたところです。10episodeで約1000stepくらい、報酬は数10程度ですが、7,8回に1回くらい400~500の報酬を得ることがあります。昨日は数百の報酬を得ることが無かったので少しは進歩しているかも。

 episode,step,reward,entropy= 61920 4624222 78.53 -15.45
 episode,step,reward,entropy= 61930 4625295 528.01 -18.26
 episode,step,reward,entropy= 61940 4626463 47.31 -13.27

 1月29日
 episode,step,reward,entropy= 65630 5051473 81.35 -19.37
 episode,step,reward,entropy= 65640 5052775 88.59 -18.67
 episode,step,reward,entropy= 65650 5053755 30.02 -23.14

 これだけ報酬が増えないと毎stepに学習する必要はなくて、1eisodeごとまとめて学習した方が同じpolicyでの経験データを複数とれ、いいような気がしてきました。

 一旦とめて、学習をまとめて行うよう修正して再開しようとしたらJupyter動かず・・。結局 pip install --upgrade ipykernel でipykernelをアップグレードしたら動くようになり、Jupyter自体を再起動して実行。リプレイメモリがクリアされるためか、再開直後はエントロピーが-17目標のところ-80ぐらいになってしまいます・・・・。

 しばらくこれ(学習を1episode後まとめて実施)で続けてみます。

1月30日
 報酬、1epsodeあたりのstepともにあまり進捗が見られません。ノートPCローカルランタイムだと1日60万stepぐらいしか進みませんね。

 episode,step,reward,entropy= 71540 5656722 25.3 -22.67
 episode,step,reward,entropy= 71550 5657850 57.45 -24.81
 episode,step,reward,entropy= 71560 5659017 8.71 -28.54

1月31日
 夜中にchromeが再起動したみたいで、ローカルランタイムとの接続が切れてしまいました。再接続しようとしても接続できず・・・、途中経過がわかりません。ランタイムを再立ち上げするとリプレイメモリがまたクリアされてしまうので、このまま継続します。

現状はこんな感じ。まともに方向転換できません。
episode,step,reward,entropy= 77180 6371987 23.72 -24.85
f:id:akifukka:20200201093140g:plain

2月1日
 若干ですが続くようになったようです。
episode,step,reward,entropy= 78070 6533278 662.51 -23.91
episode,step,reward,entropy= 78080 6534833 25.39 -39.83
episode,step,reward,entropy= 78090 6536899 -169.43 -27.97

f:id:akifukka:20200201233005g:plain
2月7日
 2月1日は10episodeでstep1500程度でした。今日は1episodeごとに表示させるようにしましたが、1epsodeでstep700~1000くらい転ばないようになりました。ただ動きはあいかわらず遅くて、その場でまわっている感じで2月1日とあまり印象は変わりません。動画は容量が大きいので掲載は控えます。
episode,step,reward,entropy= 85001 8354165 21.44 -38.86
episode,step,reward,entropy= 85002 8355165 952.15 -25.06
episode,step,reward,entropy= 85003 8355819 785.67 -33.10

2月12日
 あまり変化は感じません・・・。ちょっと動きが小さくなったかも。
episode,step,reward,entropy= 88007 9524293 119.69 -43.60
episode,step,reward,entropy= 88008 9525049 972.15 -25.15
episode,step,reward,entropy= 88009 9525964 330.92 -29.31

・・・学習が進みましたら更新します。

2. HumanoidFlagrunHarder

 少し並行させてGoogleホストで学習をさせて様子を見ていました。数日学習させたところ、転倒後に何とかもがいて起き上がろうとし始めました。12時間ごとにリプレイメモリがクリアされるので、今一つちゃんと学習できるか半信半疑でしたが学習進んでいるようです。
episode,step,reward,entropy= 30005 5358689 -622.11 -167.97

f:id:akifukka:20200201234355g:plain

2月12日
 学習が進んだら転んだ後、動かなくなっちゃいました。ダメかも。
 episode,step,reward,entropy= 66003 12217441 -320.21 -176.93

f:id:akifukka:20200212220507g:plain

gifが重いけどご容赦を。

ーーーーーーーーーーーーーーーーーーーーーーーーーーーー
HumanoidFlagrunHarderの記事は、この(4)でおしまいです!
学習継続中なので、目立った進捗があればこのページを逐次更新します。

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

Colabortoryをローカルランタイムで使ってみる

2020年1月30日 改訂1
 エラーが表示されjupyterがうまく実行されず、ipykernelをアップグレードした件について、最後に追記しました。

-------------------------------------

 Google Colaboratoryは大変便利なのですが、90分と12時間の時間制限がたまに傷です。実はColaboratoryはGoogleが用意しているホストの他、ローカルのjupyterランタイムに接続して使うことができ、この場合は時間制限はありません(当たり前か)。

 ちょっと試してみて、画像認識のように大量のデータを使うわけではなければ、GPUを持たないノートPCでも結構使い物になることがわかりましたので、使い方を紹介します。

1.注意事項

 手っ取り早く環境構築するためWindows10ベースでjupyterローカルランタイムを構築しました。そのためGoogleのホスト(Linux)とは一部互換性がありません。例えば、以下のような違いがあります。

  • Googleドライブをマウントできません。Googleドライブをマウントするライブラリgoogle.colabがLinuxでしか使えないライブラリを使っているためです。
  • Open AI gymの拡張env、gym[Box2D]、gym[classic_control]等を使えません。Open AI gymの拡張envがWindows用に実装されていないためです(Open AI gymのWindows実装自体が実験的実装とされていて拡張部は実装されていません)。
  • その他、Linux特有の処理は実行できません。Colaboratoryで実行している深層強化学習のソースも若干修正が必要になります。

2.インストール

(1)anaconda

 pythonの実行環境anacondaをインストールします。jupyter、pythonの各種ライブラリも同時にインストールされます。pythonバージョン3.x用をインストールします。

 ユーザ全員か、自分だけかを聞かれるので自分だけ(Just Me)としました。 

(2)Pytorch

 スタート-Anaconda-Anaconda Promptを起動します。Pytorhのホームぺージでインストール設定を選択するとインストールコマンドが表示されます。
 例えばWindows、conda(Anacondaのこと)、GPU無しの場合は以下のコマンドをAnaconda Promptで実行します。

 conda install pytorch torchvision cpuonly -c pytorch

(3)Open AI gym

 以下のコマンドでインストールします。

pip install gym

(4)PyBullet

 以下のコマンドでインストールします。

!pip install pybullet

(5)GoogleのJupyter 拡張機能

 以下のコマンドでインストールします。

pip install jupyter_http_over_ws
jupyter serverextension enable --py jupyter_http_over_ws

 参考 Colaboratory – Google

3.ローカルランタイム起動

  • スタート-Anaconda-Anaconda Promptを起動します。
  • 以下のコマンドでjupyterを起動します。googleからのアクセスを許可します。
jupyter notebook --NotebookApp.allow_origin='https://colab.research.google.com' --port=8888 --NotebookApp.port_retries=0

  Prompt画面にColaboratoryからの接続用キーが表示されるのでコピーします。

f:id:akifukka:20200127215341j:plain

 コピーできない場合はPromptを右クリックしてプロパティーオプションー簡易編集モードにチェックをいれます。

4.Colaboratoryから接続する

 Golaboratoryの接続-ローカルランタイムに接続

f:id:akifukka:20200127215733j:plain

バックエンドURLに先ほどコピーした接続先をペーストして接続を押します。

5.Colaboratoryのソースリスト修正

深層強化学習のソースリストを例に修正箇所を記します。

(1)ファイルシステム

 Googleドライブは使えないのでファイルの読み書きはローカルパソコン上に行います。カレントディレクトリは「ローカルディスク」-「ユーザ」-「ユーザ名」になります。
 私はここにjupyterといったフォルダを作って使っています。この場合、ソースリスト中のファイル指定先を例えば次のように変更します。

'/content/drive/My Drive/model_Humanoid_SAC'
 → 'jupyter/model_Humanoid_SAC'

(2)ライブラリインストール

  • apt-getは使えませんのでコメントアウトします。
  • pip3をpipに変更します。pip3はpython3用pipの意味でpython2のpipと区別するためのものです。今回はpython3しかインストールされておらずpipがpython3にあてがわれていてpip3は使えません。
  • 'gym[Box2D]'、'gym[classic_control]'は使えないのでコメントアウトします。

(3)Colaboratoryでの画像表示設定をコメントアウト

 Linux用の命令でディスプレイドライバを持たないGoogleホスト用なのでコメントアウトします。Windowsではこれらの命令を実行しなくても画像データを生成してくれます。

#display = Display(visible=0, size=(1024, 768))
#display.start()
#import os
#os.environ["DISPLAY"] = ":" + str(display.display) + "." + str(display.screen)

 

 これで時間を気にすることなく思う存分学習させることができます。
 最近Colaboratoryの使い過ぎでGPUが割り当てられなくなっていたところですし、深層強化学習ならGPUなくてもたいして変わらないし。

6.追記(2020年1月30日)

 jupyterにうまく接続できず、コンソールに次のようなエラー表示がされたことがあります。パソコンを再起動しても効果なし。

 ValueError: signal only works in main thread

 以下の通りipykernelをアップグレードしたら解消しました。

 pip install --upgrade ipykernel

 

Humanoid flag runについて

 ちなみにHumanoid flag run 全然報酬が上昇しません・・・。1日200万stepづつ学習は進んでるんですが・・・。いったい何日かかることやら。

では!

 

PyBullet-HumanoidFlagrunHarderBulletEnv-v0(3)

 今回はSoft Actor-Critic(SAC)について備忘録ということで解説します。以下の論文は初期のSACをさらに改良したものです。

1.深層強化学習の理解に必要な事項の整理

 まずは、各記号、考え方の整理など。

 f:id:akifukka:20200125111703j:plain

 行動価値関数が最大になる行動を出力する方策関数を学習するのが目的です。
行動価値関数と方策関数をニューラルネットワークで表現します。

①実際に乱数で試行錯誤した経験(状態st、行動at、報酬r、次の状態st+1)から、Bellman方程式を使って行動価値関数Qを学習させます。この時にBellman方程式中で経験記録に無い値、すなわち次回の行動at+1が出てくるため、これはその時最新の方策関数を使って推定します。

②行動価値関数を更新したら、その時の経験を使ってQを最大化する方策関数を学習させます。

①と②を繰り返して行動価値関数Qと方策関数πのニューラルネットワークを学習させます。

2.Soft Actor Critic(SAC)

 SACは初期の論文のもの(Open AIのSpining Upのやり方)と、後から発表された改良型のものがありますが、ここでは改良型について説明します。

  • 報酬と方策のエントロピー(ばらつきの大きさ)の将来に渡る総合計を最大化する方策を求めます。エントロピーが加わるところが従来と異なるところです。より大きなエントロピーを持つ方策(方策がばらついても結果(報酬)が良い方策分布)を学習させます。不安定な極所的最適解が排除され、学習が安定して進むと思われます。

    f:id:akifukka:20200125113831j:plain 

  • 行動価値関数Qを報酬の将来総和と次回ステップ以降のエントロピー将来総和で定義すると、確率的なBellman方程式は次のように書けます。

    f:id:akifukka:20200125120343j:plain

     学習時は0.5×(左辺-右辺)の2乗をLoss関数として定義し、Loss関数が最小になる様、ネットワークの重みを更新します。ソースリストの次の箇所で計算しています。なお後述するテクニックを使っています。

    ①右辺の計算 next_actions:as+1、next_logp_pis:t+1のエントロピー×ー1
    #Qターゲット

        #Qターゲット
        next_mus,next_actions,next_logp_pis = policy_net(next_state_batch)
        next_q1s = q1_target_net(torch.cat([next_state_batch, next_actions], 1))
        next_q2s = q2_target_net(torch.cat([next_state_batch, next_actions], 1))
        next_qs = torch.min(next_q1s, next_q2s)
    
        q_targets = reward_batch + GAMMA * (1.0-done_batch) * (next_qs - alpha * next_logp_pis)
    

    ②Loss関数の計算

    
        #Q1ネット
        #loss関数
    #    q1_loss = 0.5 * F.mse_loss(q1_net(torch.cat([state_batch, action_batch],1)),q_targets) 中身がわかるような記述に変更
        q1_loss = 0.5 * torch.mean((q1_net(torch.cat([state_batch, action_batch],1)) - q_targets)**2)
        #ネットの学習
        q1_optimizer.zero_grad()
        #誤差逆伝搬
        q1_loss.backward(retain_graph=True)
        #重み更新
        q1_optimizer.step()
    
  • DDPGの改良型であるTD3で使われたテクニックclipped double-Qを使います。Qネットワークを2つ(Q1、Q2)それぞれ学習させて、小さい方を使うことでQの過大学習によるpolicyの破壊を予防します。
  • Bellman方程式を使ってQを学習する際Qが不安定にならないように、右辺の計算に使うQは別に定義(ターゲット関数)したものを使います。Qのターゲット関数はQに遅れて追従するようにします。
       tau = 0.005
        #q1,2 targetネットのソフトアップデート
        #学習の収束を安定させるためゆっくり学習するtarget netを作りloss関数の計算に使う。
        #学習後のネット重み×tau分を反映させ、ゆっくり追従させる。
        for target_param, local_param in zip(q1_target_net.parameters(), q1_net.parameters()):
          target_param.data.copy_(tau*local_param.data + (1.0-tau)*target_param.data)
    
        for target_param, local_param in zip(q2_target_net.parameters(), q2_net.parameters()):
          target_param.data.copy_(tau*local_param.data + (1.0-tau)*target_param.data)
  • 方策(policy)はSquashed Gaussian Policyを使います(tanhで-1~1の範囲に押しつぶしたGaussian Policy関数の意味です)。最初に通常のGaussian policiesを計算し、その後squashします。

    ある状態stを入力値として、行動の平均 μ(st)、log分散 log σ(st)をニューラルネットワークで推定、平均にノイズを加算して行動を導きます。

    f:id:akifukka:20200125112832j:plain
    エントロピーを次の式で計算した後、squashします。

    f:id:akifukka:20200125144804j:plain

  • Qの学習結果をpolicyに反映します。Qは最初のエントロピーHを含んでいないので、Qにエントロピーを加えたものが最大になるようなpolicy πを学習させます。実際はLoss関数 Jπ =-1×(Qにエントロピーを加えたもの)と定義し、Loss関数が最小になるようにpolicy πを学習させて上記と同じことをしています。

    f:id:akifukka:20200125160941j:plain
        #policyネットのloss関数
        mus,actions,logp_pis = policy_net(state_batch)
        q1s = q1_net(torch.cat([state_batch,actions],1))
        q2s = q2_net(torch.cat([state_batch,actions],1))
        qs = torch.min(q1s, q2s)
        p_loss = torch.mean(alpha * logp_pis - qs)
    
        #policyネットの学習
        p_optimizer.zero_grad()
        #誤差逆伝搬
        p_loss.backward(retain_graph=True)
        #重み更新
        p_optimizer.step()
    
  • あらかじめエントロピーの目標値を決め、その値になるようにtemperature parameter α(alpha)を自動調整します。エントロピーの目標値は、以下の論文によると単純にactionの次元1つあたりー1として、-1×actionの要素数(次元)としたとのことです。

    [1812.11103] Learning to Walk via Deep Reinforcement Learning

    Loss関数Jαを次のように定義し、Jαが小さくなるようαを更新します。

    f:id:akifukka:20200125175648j:plain

    ①Ht > H (目標エントロピーの方が大)
    αを増やすとJαが減るのでαの更新でαが増加します。αが増加すると、Qの学習時にエントロピーの影響が大きくなり、エントロピーは増加します。
    ②Ht < H (目標エントロピーの方が小)
    αを減らすとJαが減るのでαの更新でαが減少します。αが減少するとQにおけるエントロピーの影響が小さくなり、学習が進むとエントロピーは減少していきます。
        alpha_loss = -torch.mean(log_alpha*(target_entropy + logp_pis))
        #log_alpha更新
        alpha_optimizer.zero_grad()
        #誤差逆伝搬
        alpha_loss.backward(retain_graph=True)
        #log_alpha更新
        alpha_optimizer.step()
        #alpha更新
        alpha = log_alpha.exp()

3.パラメータについて

 パラメータについて気付いたことをまとめておきます。

  • BATCH_SIZE = 256
    学習に使う経験数。以外に影響が大きく、少ないと学習が安定せず進まなく(報酬が飽和)します。humanoidの場合、128では少なすぎでした。少ないとQが滑らかに更新されず、いびつになってしまうのではないかと想像しています。
  • GAMMA = 0.99
    報酬に対する割引率。具体的な影響については未経験。
  • lr=3e-4(optim.Adam)
    ニューラルネットワーク更新時の大きさの係数。大きいと更新後のネットワークがいびつになり、学習が不安定になりやすい。小さいと学習に時間がかかる。
  • tau = 0.005
    ターゲット関数の更新係数。この係数分だけ最新のネットワークの重みに変わる。変更したことがないので影響については何とも言えないが、大きすぎると学習が不安定になりやすいと考えられます。

以上、今回はSACについてまとめてみました。

つづく

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

PyBullet-HumanoidFlagrunHarderBulletEnv-v0(2)

2020/1/25改正
 学習継続時に早くalphaが収束するようalpha、log_alpha、alpha_optimizerを保存するように変更しました。gpu有、無しの両環境で保存データを共有できるようモデル読み込み時にmap_location=deviceを追加しました。

2020/1/23改正
 BATCH_SIZEを128から論文と同じ256に変更したら学習が飛躍的に改善しました。あわせてSIGMA_DECAY を 3000から10に、num_episodesを9000に変更しました。

ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー

 DDPGではHumonoidFlagrunをうまく学習させられなかったので、SAC(Soft Actor-Critic)で学習させてみました。以下まとめ。

  •  SAC動かすのにかなり手間取ったので、まだHumanoidFlagrunHarderを学習させることができていません(できないかも・・・汗)。本記事は、やっと学習するようになったHumanoidBulletEnv-v0を例に紹介します。
  • 学習に8時間かけて、やっと、すぐには転ばず、足を出す動作を覚えました。さらに時間をかければもっと安定するようになるでしょう。学習しているのを確認するだけなら2,3時間だけでもいいと思いますが、なにせ時間がかかります。

1.SAC(Soft Actor-Critic) 

 SAC(Soft Actor-Critic)は2018年に発表された比較的新しい方式で、DDPGと同様連続値を扱うことができる強化学習法のひとつです。以下の論文は初期のSACの計算手順の改良版について書かれたもので、今回はこれをベースにしました。本論文にはSACはDDPGや他の手法と比較して学習の進みが早いといった特長があることが示されています。

 本論文はGoogleカルフォルニアバークレー校の共同作業のようです。

 こちらも参考にさせてもらいました。なおSpinning Upの内容は改良前のSAC計算方法なので若干異なりますが、SACの式の解説等、基本的考え方は参考になります。

Soft Actor-Critic — Spinning Up documentation

GitHub - openai/spinningup: An educational resource to help anyone learn deep reinforcement learning.

GitHub - ku2482/soft-actor-critic.pytorch: A PyTorch Implementation of Soft Actor-Critic.

 次の章でとにかくコードを動かしてみますが、その前にSACのイメージだけでも。

  • 方策policyの出力(行動)を確率的なものとします。policyの出力は中心値に対してgauss分布でばらつくものとし、中心値とばらつきの大きさエントロピーニューラルネットワークで推定します。
  • 価値関数は、報酬に加え方策のエントロピー(ばらつきの大きさ)の大きさを加えます。これにより、より大きなエントロピーを持つ行動を学習するようになります。不安定な極所的最適解が学習から排除され、学習が安定して進むものと思われます。

     例えば水上の飛び石の上を渡っているとすると、DDPGでは単純に乱数で選んで学習するので小さな石を選び、うまくいったらそれを学習してしまうのに対し、SACはある幅をもった行動の価値を高いと考えるので、常に大きな石を選ぶように学習します。

    f:id:akifukka:20200121192727j:plain
     この積み重ねでSACはより安定した行動を学習し、極所最適解にはまったりせず学習が進むということだと思われます。

2.とにかく動かしてみる

 SACの細かい話は次回以降にして、とにかく動かしてみましょう。なお、ある程度動きが改善されてるなと感じるくらいまで学習させるには8~10時間くらいの覚悟が必要です。

(1) Colaboratoryを立ち上げる

 ランタイム-ランタイムのタイプを変更でGPUにします。

(2)ライブラリをインストールします。

 MountainCarContinuousも動かせるようBox2Dもインストールしています。

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

!pip3 install 'gym[Box2D]'
!pip3 install  'gym[classic_control]'

(3)Colaboratoryのランタイムを再起動します。

 インストールしたライブラリを有効にするためランタイムを再起動します。

 ランタイム-ランタイムの再起動

(4)Googleドライブをマウントします。

 学習したモデルを保存するためGoogleドライブをマウントします。
 実行するとリンクが表示されるのでリンクをクリックして、アカウントを選択して表示されたコードをCtrl-Cでコピーして、Enter your authorization code:の四角のエリアにCtrl-Pでペーストして、Entキーを押します。

from google.colab import drive
drive.mount('/content/drive')

(5) ネットワーク、オプティマイザ、リプレイメモリ等を生成し初期化します。

#初期化コード
import gym
import pybullet_envs
import math
import random
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from collections import namedtuple
#from itertools import count

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import torchvision.transforms as T

from pyvirtualdisplay import Display
from scipy.interpolate import interp1d

display = Display(visible=0, size=(1024, 768))
display.start()
import os
os.environ["DISPLAY"] = ":" + str(display.display) + "." + str(display.screen)

env = gym.make('HumanoidBulletEnv-v0')
#env = gym.make('HumanoidFlagrunBulletEnv-v0')
#env = gym.make('HumanoidFlagrunHarderBulletEnv-v0')
#env = gym.make('MountainCarContinuous-v0')

# if gpu is to be used
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

#リプレイメモリー
Transition = namedtuple('Transition',
                        ('state', 'action', 'next_state', 'reward','done'))

class ReplayMemory(object):
    def __init__(self, capacity):
        self.capacity = capacity
        self.memory = []
        self.position = 0

    def push(self, *args):
        """Saves a transition."""
        if len(self.memory) < self.capacity:
            self.memory.append(None)
        self.memory[self.position] = Transition(*args)
        self.position = (self.position + 1) % self.capacity

    def sample(self, batch_size):
        return random.sample(self.memory, batch_size)

    def __len__(self):
        return len(self.memory)
   
    def reset(self):
        self.memory = []
        self.position = 0   

#Actor(Polycy) Network squashed Gaussian Policy
LOG_STD_MAX = 2
LOG_STD_MIN = -20
EPS = 1e-8

class Policy_net(nn.Module):
    def __init__(self, NumObs, NumAct,NumHidden):
        super(Policy_net, self).__init__()
        self.ln1 = nn.Linear(NumObs, NumHidden)
        self.ln2 = nn.Linear(NumHidden, NumHidden)
        self.ln3 = nn.Linear(NumHidden, NumHidden)

        #mu 中心値算出用
        self.muln = nn.Linear(NumHidden, NumAct)

        #log_std エントロピー×ー1算出用
        self.log_stdln = nn.Linear(NumHidden, NumAct)

    def forward(self, x):
        x = F.relu(self.ln1(x))
        x = F.relu(self.ln2(x))
#層数が多いと収束が遅いことがあるので層数減らした。収束が早くなったかは未確認。
#        x = F.relu(self.ln3(x))

        mu = self.muln(x)
        log_std = torch.tanh(self.log_stdln(x))
        log_std = LOG_STD_MIN + (LOG_STD_MAX - LOG_STD_MIN) * (log_std + 1) * 0.5

        log_std_exp = log_std.exp()
        #πの計算(action) ノイズはガウス分布
        noise = torch.randn_like(mu)
        pi = mu + noise * log_std_exp
        #Gaussian Policy類推度の計算(Log-Likelihood)
        pre_sum = -0.5 * (((pi-mu)/(log_std_exp + EPS))**2 + 2 * log_std + np.log(2 * np.pi))
        logp_pi = torch.sum(pre_sum,1,keepdim=True)

        #squash tanhで範囲をー1~1につぶす
        mu, pi, logp_pi = apply_squashing_func(mu, pi, logp_pi)

        return mu,pi,logp_pi

#傾きを保持したままクリッピングだけを行う
def clip_but_pass_gradient(x, low=-1., high=1.):
    clip_high = (x > high).float()
    clip_low = (x < low).float()
    return x + ((high - x)*clip_high + (low - x)*clip_low).detach()

def apply_squashing_func(mu, pi, logp_pi):
    mu = mu.tanh()
    pi = pi.tanh()

    logp_pi = logp_pi - torch.sum((clip_but_pass_gradient(1 - pi**2,low = 0, high = 1) + 1e-6).log(),1,keepdim=True)
    return mu, pi, logp_pi

#Critic(Q) Network Q値の計算
class Q_net(nn.Module):
    def __init__(self, NumInput, NumHidden):
        super(Q_net, self).__init__()
        self.ln1 = nn.Linear(NumInput, NumHidden)
        self.ln2 = nn.Linear(NumHidden, NumHidden)
        self.ln3 = nn.Linear(NumHidden, NumHidden)
        self.ln4 = nn.Linear(NumHidden, 1)
        self.ln4.weight.data.uniform_(-3e-3, 3e-3)

    def forward(self, input):
        x = F.leaky_relu(self.ln1(input))
        x = F.leaky_relu(self.ln2(x))
#層数が多いと収束が遅いことがあるので層数減らした。収束が早くなったかは未確認。
#        x = F.leaky_relu(self.ln3(x))
        y = self.ln4(x)
        return y

def optimize_model():
    global alpha

    if len(memory) < BATCH_SIZE:
        return
    transitions = memory.sample(BATCH_SIZE)
    batch = Transition(*zip(*transitions))

    #batchを一括して処理するためtensorのリストを2次元テンソルに変換
    state_batch = torch.stack(batch.state,dim=0)
    action_batch = torch.stack(batch.action,dim=0)
    reward_batch = torch.stack(batch.reward,dim=0)
    next_state_batch = torch.stack(batch.next_state,dim=0)
    done_batch = torch.stack(batch.done,dim=0)

    #Qターゲット
    next_mus,next_actions,next_logp_pis = policy_net(next_state_batch)
    next_q1s = q1_target_net(torch.cat([next_state_batch, next_actions], 1))
    next_q2s = q2_target_net(torch.cat([next_state_batch, next_actions], 1))
    next_qs = torch.min(next_q1s, next_q2s)

    q_targets = reward_batch + GAMMA * (1.0-done_batch) * (next_qs - alpha * next_logp_pis)

    #Q1ネット
    #loss関数
#    q1_loss = 0.5 * F.mse_loss(q1_net(torch.cat([state_batch, action_batch],1)),q_targets) 中身がわかるような記述に変更
    q1_loss = 0.5 * torch.mean((q1_net(torch.cat([state_batch, action_batch],1)) - q_targets)**2)
    #ネットの学習
    q1_optimizer.zero_grad()
    #誤差逆伝搬
    q1_loss.backward(retain_graph=True)
    #重み更新
    q1_optimizer.step()

    #Q2ネット
    #loss関数
#    q2_loss = 0.5 * F.mse_loss(q2_net(torch.cat([state_batch, action_batch],1)),q_targets) 中身がわかるような記述に変更
    q2_loss = 0.5 * torch.mean((q2_net(torch.cat([state_batch, action_batch],1)) - q_targets)**2)
    #ネットの学習
    q2_optimizer.zero_grad()
    #誤差逆伝搬
    q2_loss.backward(retain_graph=True)
    #重み更新
    q2_optimizer.step()

    #policyネットのloss関数
    mus,actions,logp_pis = policy_net(state_batch)
    q1s = q1_net(torch.cat([state_batch,actions],1))
    q2s = q2_net(torch.cat([state_batch,actions],1))
    qs = torch.min(q1s, q2s)
    p_loss = torch.mean(alpha * logp_pis - qs)

    #policyネットの学習
    p_optimizer.zero_grad()
    #誤差逆伝搬
    p_loss.backward(retain_graph=True)
    #重み更新
    p_optimizer.step()

    alpha_loss = -torch.mean(log_alpha*(target_entropy + logp_pis))
    #log_alpha更新
    alpha_optimizer.zero_grad()
    #誤差逆伝搬
    alpha_loss.backward(retain_graph=True)
    #log_alpha更新
    alpha_optimizer.step()
    #alpha更新
    alpha = log_alpha.exp()

    tau = 0.005
    #q1,2 targetネットのソフトアップデート
    #学習の収束を安定させるためゆっくり学習するtarget netを作りloss関数の計算に使う。
    #学習後のネット重み×tau分を反映させ、ゆっくり追従させる。
    for target_param, local_param in zip(q1_target_net.parameters(), q1_net.parameters()):
      target_param.data.copy_(tau*local_param.data + (1.0-tau)*target_param.data)

    for target_param, local_param in zip(q2_target_net.parameters(), q2_net.parameters()):
      target_param.data.copy_(tau*local_param.data + (1.0-tau)*target_param.data)

#action,observationの要素数取得
n_actions = env.action_space.shape[0]
n_observations = env.observation_space.shape[0]
target_entropy = -torch.Tensor([n_actions]).to(device)
print('num actions,observations,target_entropy',n_actions,n_observations,target_entropy)

#ネットワーク
#policyネットとそのtargetネット
policy_net = Policy_net(n_observations, n_actions,256).to(device)
#Qネット,Vネットとそのtargetネット
q1_net = Q_net(n_observations + n_actions, 256).to(device)
q2_net = Q_net(n_observations + n_actions, 256).to(device)
q1_target_net = Q_net(n_observations + n_actions, 256).to(device)
q2_target_net = Q_net(n_observations + n_actions, 256).to(device)
#Qターゲットネット初期化(コピー)
q1_target_net.load_state_dict(q1_net.state_dict())
q2_target_net.load_state_dict(q1_net.state_dict())

#学習用optimizer生成
p_optimizer = optim.Adam(policy_net.parameters(),lr=3e-4)
q1_optimizer = optim.Adam(q1_net.parameters(),lr=3e-4)
q2_optimizer = optim.Adam(q2_net.parameters(),lr=3e-4)
log_alpha = torch.zeros(1,requires_grad=True,device=device)
alpha = log_alpha.exp()
alpha_optimizer = optim.Adam([log_alpha], lr=3e-4)

#学習用リプレイメモリ生成
memory = ReplayMemory(1000000)
memory.reset()

#総数カウント用
total_episode = 0
total_step = 0

 (6)学習

 学習します。1000エピソードごとにネットワークをGoogleドライブに保存します。ファイル名はmodel_Humanoid_SACxxxxで、xxxxはエピソード数です。

 num_epsode=20000を書き換えて学習episode数を変更します。最初は1000~5000くらいにして、様子を見た方がいいかも知れません。この学習コードだけ繰り返して実行すると、前の状態から学習を継続してくれます。

 また、後で説明するモデル読み込んだ後、学習コードを実行するとそこから継続して学習できます。

 学習を途中で止めても、再実行で継続できます(ごくたまに失敗しますが)。

 (5)の初期化コードを実行すると直前までの学習モデルや、ファイルから読み込んだモデルが初期化されてしまうので注意が必要です。

 なおColaboratoryは90分セッションが切れているとリセットされてしまうということで、次の記事の通りChromeのアドオンAuto Refreshを使います。

Google Colaboratoryの90分セッション切れ対策【自動接続】 - Qiita

#初期化コードを再実行せず、このコードだけを繰り返し実行すると直前の学習状態、
#もしくはファイルから読み込んだモデルを続けて学習できる。

#BATCH_SIZE = 128
BATCH_SIZE = 256
#Qネット学習時の報酬の割引率
GAMMA = 0.99
#alphaは自動計算のためコメントアウト
#alpha = 0.05

#episode数に対するノイズの減少係数
#OUNoise オリジナルのSACでは使わないが試行錯誤でいれてみた。
#ちゃんと確認できていないがMountainCarでは効果あるかも。
SIGMA_START = 1.0 #最初
SIGMA_END = 0.0 #最後
SIGMA_DECAY = 10 #このepisode回数で約30%まで減衰
SIGMA_DECAY_END = SIGMA_DECAY * 1.2 #このepisode以降はOUNoiseを使用しない
theta = 0.08

#50 フレーム/sec グラフ描画用
FPS = 50
#学習エピソード数。num_epsode=20000を変更する。
num_episodes = 9000
display_on = 1 # num_episodeの最後の回の動画データ保存
save_model_on = 1 # 0 モデル自動セーブしない 1:モデル自動セーブする
t_max = 1400 # 1エピソードでの最大ステップ数。ステップ数がこの値以上になったら強制的にenvをリセットして次のエピソードに移行

noise =np.array([random.uniform(-0.5,0.5) for i in range(n_actions)],dtype = np.float)

for i_episode in range(num_episodes):
    #whileループ内初期化
    observation = env.reset()
    done = False
    reward_total = 0.0
    logp_pi_total = 0.0
    t = 0
    #グラフ用データ保存領域初期化
    frames = []
    observations = []
    actions = []
    ts = []
    for i in range(n_observations):
      observations.append([])
    for i in range(n_actions):
      actions.append([])

    sigma = SIGMA_END + (SIGMA_START - SIGMA_END) * math.exp(-1. * total_episode / SIGMA_DECAY)

    total_episode += 1

    while not done and t < t_max:

        with torch.no_grad():
          mu,action,logp_pi = policy_net(torch.from_numpy(observation).float().reshape(1,-1).to(device))

          mu = mu.cpu().data.numpy().reshape(-1)
          action = action.cpu().data.numpy().reshape(-1)
          logp_pi = logp_pi.cpu().data.numpy().reshape(-1)[0]
          logp_pi_total += logp_pi

          #OUNoise 最初だけ。total_episode > SIGMA_DECAY_ENDではOUNoiseを加算しない。
          noise = noise - theta * noise + sigma * np.array([random.uniform(-1.0,1.0) for i in range(len(noise))])
          if total_episode < SIGMA_DECAY_END :
            action = mu + noise

          #episodeの最後は純粋にネットワークの性能を取得するためノイズ無し(中心値mu)-------------------
          if (i_episode == num_episodes -1 ):
            action = mu
          action = np.clip(action, -1, 1)

        #物理モデル1ステップ---------------------------
        next_observation, reward, done, info = env.step(action)
        reward_total = reward_total + reward

        #学習用にメモリに保存--------------------------
        tensor_observation = torch.tensor(observation,device=device,dtype=torch.float)
        tensor_action = torch.tensor(action,device=device,dtype=torch.float)
        tensor_next_observation = torch.tensor(next_observation,device=device,dtype=torch.float)
        tensor_reward = torch.tensor([reward],device=device,dtype=torch.float)
        tensor_done =  torch.tensor([done],device=device,dtype=torch.float)
        memory.push(tensor_observation, tensor_action, tensor_next_observation, tensor_reward,tensor_done)

        #observation(state)更新------------------------
        observation = next_observation

        #学習 現バージョンは毎ステップごとに学習。
        #episode終了毎にまとめて学習させることもできるが計算が遅くなったような気がしたので。
#        if ((t == t_max -1) or done) and (i_episode != num_episodes -1):
#          for j in range(t):
        optimize_model()

        #データ保存------------------------------------
        if (i_episode == num_episodes -1) and (display_on == 1) :
#          print(i_episode,observation,mu,action,_logp_pi)
          #動画
          frames.append(env.render(mode = 'rgb_array'))
          #グラフ 現状未使用
          for i in range(n_observations):
            observations[i].append(observation[i])
          for i in range(n_actions):
            actions[i].append(action[i])
          ts.append(t/FPS)

        #時間を進める----------------------------------
        t += 1
        total_step += 1
        # end while loop ------------------------------

    #10episode毎に、最後のepisodeの報酬、エントロピーを表示
    if (total_episode % 10 == 0) or (i_episode == num_episodes -1 ):
      print('episode,step,reward,entropy=',total_episode,total_step,reward_total,-logp_pi_total/t)
    
    #1000episodeごとにモデルをGoogleドライブに保存
    if (total_episode % 1000 == 0) and (save_model_on == 1):
      torch.save({
        'epoch':total_episode,
        'step':total_step,
        'log_alpha':log_alpha,
        'alpha':alpha,
        'policy':policy_net.state_dict(),
        'q1':q1_net.state_dict(),
        'q2':q2_net.state_dict(),
        'q1_target':q1_target_net.state_dict(),
        'q2_target':q2_target_net.state_dict(),
        'p_optimizer':p_optimizer.state_dict(),
        'q1_optimizer':q1_optimizer.state_dict(),
        'q2_optimizer':q2_optimizer.state_dict(),
        'alpha_optimizer':alpha_optimizer.state_dict(),
      },'/content/drive/My Drive/model_Humanoid_SAC' + str(total_episode) )
# end for loop --------------------------------------

実行すると10episodeごとに次のように表示されます。

episode,step,reward,entropy= 10 188 -63.53772342780692 0.4764575527773963
episode,step,reward,entropy= 20 374 -53.021724941738086 10.449289242426554
episode,step,reward,entropy= 30 581 -67.98715715128392 8.465339357202703
 

 ここで、各数値の意味は次の通りです。

episode:総エピソード数、step:総ステップ数、entropy:エントロピー

 最初はすぐに転倒してしまうため、1episodeのstepが少ないので、stepはあまり増えません(上の例では、10episodeで200stepしか進まないので平均すると1episode=20step程度で転倒しています)。

 なお、humanoidでは、エントロピーが-17になるようalpha(エントロピー調整係数(Qを求める際に、報酬にエントロピーを加算する時にエントロピーに乗ずる係数)、 inverse of reward scaleとも呼ばれる)を自動調整しているので、学習が進むとエントロピーもー17近辺に近づいていきます。

 (7)画像を表示

 画像を生成します。またgif動画ファイルを作ってGoogleドライブに保存します。

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

fig = plt.figure(figsize=(5, 4))
plt.axis('off')
plt.subplots_adjust(left=0, right=1, bottom=0, top=1)

images = []
for f in frames:
  image = plt.imshow(f)
  images.append([image])
ani = matplotlib.animation.ArtistAnimation(fig, images, interval=30, repeat_delay=1)
ani.save("/content/drive/My Drive/images.gif", writer="pillow")

HTML(ani.to_jshtml())

  (8)モデルを読み込み

 Googleドライブに自動保存されたモデルを読み込みます。そこから学習を継続することも可能です。ただしリプレイメモリの内容は保存されていないので、過去の試行例は引き継がれないので、純粋な意味では条件は変わります。

#Googleドライブからモデルを読み込む。ファイル名は都度変更すること。
checkpoint = torch.load('/content/drive/My Drive/model_Humanoid_SAC16000', map_location=device)
total_episode = checkpoint['epoch']
total_step = checkpoint['step']
log_alpha = checkpoint['log_alpha']
alpha = checkpoint['alpha']
policy_net.load_state_dict(checkpoint['policy'])
q1_net.load_state_dict(checkpoint['q1'])
q2_net.load_state_dict(checkpoint['q2'])
q1_target_net.load_state_dict(checkpoint['q1_target'])
q2_target_net.load_state_dict(checkpoint['q2_target'])
p_optimizer.load_state_dict(checkpoint['p_optimizer'])
q1_optimizer.load_state_dict(checkpoint['q1_optimizer'])
q2_optimizer.load_state_dict(checkpoint['q2_optimizer'])
alpha_optimizer.load_state_dict(checkpoint['alpha_optimizer'])

3.結果です。

  約連続98時間学習した結果です。論文では100万stepの学習で報酬5000だとか・・・。当方、137万stepで報酬291・・・。
 BATCH_SIZEを128から256に変更したところ大きく改善しました。9000 episodesで約10時間かかります。ちなみに最大ステップ数1400を変更するとrewardも増えます。

BATCH_SIZEが128:episode:16352、step:1378063 reward:291
BATCH_SIZEが256:episode:9002、step:1626911 reward:563

f:id:akifukka:20200125075100g:plain
つづく
 SACについて少し解説を書いた後、HumanoidFlagrunに挑戦する予定です。まだ全く手が付いていないので時間かかりそうですが・・。

 なにせ今回、最初はSACの初期の論文ベースで実装して、いきなりFlagrunをやってみたのですが全然ダメで。バグなのか課題が難しすぎるのか判断つかず2週間ほど迷走して、やっと現状までたどりついたところなので・・・。では!

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

PyBullet-HumanoidFlagrunHarderBulletEnv-v0(1)

 次は3Dの物理シミュレータを使ってみます。以前はOpen AI Gymで使える3D物理環境は有料のMuJoCo用だけでしたが、今では無料で使えるPyBullet用環境(env)もあるということなので、こちらを使ってみます。

 PyBulletはErwin Cumansさんらが開発したオープンソースの3D物理シミュレーションツールです。ロボット業界で広く使われている物理モデル記述法であるURDFに対応しており応用範囲は広そうです。Erwin Cumansさんは今はGoogleで働いているそうな。

 もともとMuJoCo用に用意されたOpen AI Gymのenvが、PyBullet用に書き直されGithub上のPyBulletの以下のフォルダに収納されています。

bullet3/examples/pybullet/gym/pybullet_envs/

 手始め?に、あらかじめ用意されているenv、HumanoidFlagrunHarderBulletEnv-v0を試してみます。実は、DDPGで1週間ほど試行錯誤しながら学習させてみたのですが、いつまでたっても歩けるようにならず・・・。ということで今回は学習済みサンプルを使っての紹介です。

1.HumanoidFlagrunHarderBulletEnv-v0

 ソースコードはbullet3/examples/pybullet/gym/pybullet_envs/gym_locomotion_envs.py

です。人型のロボットが赤い球状のFlag(旗)を走って追いかける動作を学習するための環境です。

(1)動かしてみる

 PyBulletの以下フォルダに学習済みactorを使ったデモプログラムが用意されています。このままだとColaboratoryで絵を描けないので改造して動かします。

bullet3/examples/pybullet/gym/pybullet_envs/examples/enjoy_TF_HumanoidFlagrunHarderBulletEnv_v1_2017jul.py

①Colaboratoryノートブックの作成

 ColaboratoryでPython3の新しいノートブックを作成、ランタイム-ランタイムのタイプを変更でハードウェアアクセラレータにGPUをセットします。

 ノートブックに適当な名前をつけます。

②ライブラリをインストールします

 今までと違い、最後にpybulletをインストールする行が追加されています。

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

③Runtimeを再起動

 インストールしたライブラリを有効にするためランタイムを再起動します。

 ランタイム-ランタイムの再起動

④学習済みactorのクラス定義ファイルをGithubからコピー

!wget https://raw.githubusercontent.com/bulletphysics/bullet3/master/examples/pybullet/gym/pybullet_envs/examples/enjoy_TF_HumanoidFlagrunHarderBulletEnv_v1_2017jul.py

⑤環境を動かします

from enjoy_TF_HumanoidFlagrunHarderBulletEnv_v1_2017jul import SmallReactivePolicy
import gym
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.animation
from PIL import Image
from IPython.display import HTML
from pyvirtualdisplay import Display

display = Display(visible=0, size=(1024, 768))
display.start()

import os
os.environ["DISPLAY"] = ":" + str(display.display) + "." + str(display.screen)

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

pi = SmallReactivePolicy(env.observation_space, env.action_space)

frames = []
t = 0
done = False
reward_total = 0.0

observation = env.reset()

while not done and t < 1400:

  action = pi.act(observation)

  #物理モデル1ステップ---------------------------
  observation, reward, done, info = env.step(action)
  reward_total = reward_total + reward

  #動画
  frames.append(env.render(mode = 'rgb_array'))

  #時間を進める----------------------------------
  t += 1
  # end while loop ------------------------------

print('reward=',reward_total)

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

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

HTML(ani.to_jshtml())

f:id:akifukka:20200125081754g:plain
(2)報酬について

 ソースコード、bullet3/examples/pybullet/gym/pybullet_envs/gym_locomotion_envs.pyを読むと報酬は次の総和で与えられるようです。詳しくはWalkerBaseBulletEnvのdef step内のself.rewardsから内容を追いかけることができます。

  • self._alive
    倒れている:-1
    立っている:1~2の間でbodyのz座標による。通常歩行している高さなら2。
    robot_locomotors.pyのclass HumanoidFlagrunHarder、def alive_bonus参照
  • progress(進捗)
    potentialの差(進捗分)。potentialは次の値で与えられる。ロボットが立ち上がるのは姿勢の報酬があるため。
    姿勢      :立っている200、寝ている100
    目標との距離  :距離/step時間 potentialの進捗が目標接近速度になる
    class HumanoidFlagrunHarder、def calc_potential参照。
  • electricity_cost
    関節の速度とトルクから計算したDCモータに対するコスト(負の値になる)
    gym_locomotion_envs.pyのWalkerBaseBulletEnvのdef step参照。
  • joints_at_limit_cost
    関節が限界まで曲がっている、伸びきっている時の減点。
    gym_locomotion_envs.pyのWalkerBaseBulletEnvのdef step参照。
  • feet_collision_cost
    たぶん未使用で0のまま

 

今回のまとめ

 3Dの物理シミュレータPyBulletを使ったOpen AI Gym用env、HumanoidFlagrunHarderBulletEnv-v0を学習済みサンプルを使い、Google Colaboratoryで動かしてみました。

 また、DDPGで学習に挑戦してみましたが、いつまでたっても歩けるようになりませんでした(うまくいかなかったので記事にはしていません)。

 ということで、単純なDDPGではなくて、その改良型

 Twin Delayed DDPG — Spinning Up documentation

 Soft Actor-Critic — Spinning Up documentation

といったあたりに挑戦する必要がありそうです・・・。時間かかりそうですね。DDPGでもふらふら歩けるようになると思っていましたが甘かった。では!

つづく

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