初めに
Isaac Labのウォークスルーを一通りこなしたので成果をまとめてみました。
環境デザインの背景
Isaac Labのオブジェクトは全て親子関係にあって、オブジェクトの位置は全て親との相対いちで表すことになります。こうしておくと、順運動学などの計算がしやすくなるためです。この際、色などのプロパティも親から子へと伝播されます。
クラスと設定
# imports
.
.
.
from .isaac_lab_tutorial_env_cfg import IsaacLabTutorialEnvCfg
class IsaacLabTutorialEnv(DirectRLEnv):
cfg: IsaacLabTutorialEnvCfg
def __init__(self, cfg: IsaacLabTutorialEnvCfg, render_mode: str | None = None, **kwargs):
super().__init__(cfg, render_mode, **kwargs)
. . .
def _setup_scene(self):
self.robot = Articulation(self.cfg.robot_cfg)
# add ground plane
spawn_ground_plane(prim_path="/World/ground", cfg=GroundPlaneCfg())
# add articulation to scene
self.scene.articulations["robot"] = self.robot
# clone and replicate
self.scene.clone_environments(copy_from_source=False)
# add lights
light_cfg = sim_utils.DomeLightCfg(intensity=2000.0, color=(0.75, 0.75, 0.75))
light_cfg.func("/World/Light", light_cfg)
def _pre_physics_step(self, actions: torch.Tensor) -> None:
. . .
def _apply_action(self) -> None:
. . .
def _get_observations(self) -> dict:
. . .
def _get_rewards(self) -> torch.Tensor:
total_reward = compute_rewards(...)
return total_reward
def _get_dones(self) -> tuple[torch.Tensor, torch.Tensor]:
. . .
def _reset_idx(self, env_ids: Sequence[int] | None):
. . .
@torch.jit.script
def compute_rewards(...):
. . .
return total_reward
isaac labではこのように学習したいオブジェクトや強化学習に使う損失関数を定義します。下にある_get_rewardsなどがわかりやすい部分です。基本的には、sceneをセットアップし、Mujocoのように単位時間あたりの物理ステップを計算。得られた結果をフィードバックして次のステップにうつるという処理を繰り返します。
ロボットの定義
import isaaclab.sim as sim_utils
from isaaclab.assets import ArticulationCfg
from isaaclab.actuators import ImplicitActuatorCfg
from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR
JETBOT_CONFIG = ArticulationCfg(
spawn=sim_utils.UsdFileCfg(usd_path=f"{ISAAC_NUCLEUS_DIR}/Robots/Jetbot/jetbot.usd"),
actuators={"wheel_acts": ImplicitActuatorCfg(joint_names_expr=[".*"], damping=None, stiffness=None)},
)
from isaac_lab_tutorial.robots.jetbot import JETBOT_CONFIG
from isaaclab.assets import ArticulationCfg
from isaaclab.envs import DirectRLEnvCfg
from isaaclab.scene import InteractiveSceneCfg
from isaaclab.sim import SimulationCfg
from isaaclab.utils import configclass
@configclass
class IsaacLabTutorialEnvCfg(DirectRLEnvCfg):
# env
decimation = 2
episode_length_s = 5.0
# - spaces definition
action_space = 2
observation_space = 3
state_space = 0
# simulation
sim: SimulationCfg = SimulationCfg(dt=1 / 120, render_interval=decimation)
# robot(s)
robot_cfg: ArticulationCfg = JETBOT_CONFIG.replace(prim_path="/World/envs/env_.*/Robot")
# scene
scene: InteractiveSceneCfg = InteractiveSceneCfg(num_envs=100, env_spacing=4.0, replicate_physics=True)
dof_names = ["left_wheel_joint", "right_wheel_joint"]
これがロボットの設定ファイルです。基本的に一番大事なのはusd_pathにオープンソースかローカルパスを書き込むとロボットの設定がインクルードされるという点です。
前の章での続きですが、このようにしてロボットを定義した上で、
def _pre_physics_step(self, actions: torch.Tensor) -> None:
self.actions = actions.clone()
def _apply_action(self) -> None:
self.robot.set_joint_velocity_target(self.actions, joint_ids=self.dof_idx)
各ロボットのアクションを定義し、更新を行います。この二つの関数はactionの適用と更新を分離して行っており、こうすることで処理を並列化して高速化できるということっぽいです。実際actionはロボットの数x一台の行動空間のテンソルで一括計算されるのですが、この辺りはGPUらしい計算方法だと感じました。
def _get_observations(self) -> dict:
self.velocity = self.robot.data.root_com_lin_vel_b
observations = {"policy": self.velocity}
return observations
def _get_rewards(self) -> torch.Tensor:
total_reward = torch.linalg.norm(self.velocity, dim=-1, keepdim=True)
return total_reward
これも同様に、結果を取得する部分と損失関数を計算するための部分が二つに分かれている高速化処理です。この関数を使うことで、ロボットの数分だけの報酬を得ることができ、この報酬をもとに次の学習を行います。
def _get_dones(self) -> tuple[torch.Tensor, torch.Tensor]:
time_out = self.episode_length_buf >= self.max_episode_length - 1
return False, time_out
def _reset_idx(self, env_ids: Sequence[int] | None):
if env_ids is None:
env_ids = self.robot._ALL_INDICES
super()._reset_idx(env_ids)
default_root_state = self.robot.data.default_root_state[env_ids]
default_root_state[:, :3] += self.scene.env_origins[env_ids]
self.robot.write_root_state_to_sim(default_root_state, env_ids)
ここら辺は理想的な挙動をしたり、逆に目的から大幅に外れてしまったロボットを初期状態に戻すための関数です。
Ground Truth && RL問題の探究
from isaaclab.markers import VisualizationMarkers, VisualizationMarkersCfg
from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR
import isaaclab.utils.math as math_utils
def define_markers() -> VisualizationMarkers:
"""Define markers with various different shapes."""
marker_cfg = VisualizationMarkersCfg(
prim_path="/Visuals/myMarkers",
markers={
"forward": sim_utils.UsdFileCfg(
usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/UIElements/arrow_x.usd",
scale=(0.25, 0.25, 0.5),
visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 1.0)),
),
"command": sim_utils.UsdFileCfg(
usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/UIElements/arrow_x.usd",
scale=(0.25, 0.25, 0.5),
visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.0, 0.0)),
),
},
)
return VisualizationMarkers(cfg=marker_cfg)
def _setup_scene(self):
self.robot = Articulation(self.cfg.robot_cfg)
# add ground plane
spawn_ground_plane(prim_path="/World/ground", cfg=GroundPlaneCfg())
# clone and replicate
self.scene.clone_environments(copy_from_source=False)
# add articulation to scene
self.scene.articulations["robot"] = self.robot
# add lights
light_cfg = sim_utils.DomeLightCfg(intensity=2000.0, color=(0.75, 0.75, 0.75))
light_cfg.func("/World/Light", light_cfg)
self.visualization_markers = define_markers()
# setting aside useful variables for later
self.up_dir = torch.tensor([0.0, 0.0, 1.0]).cuda()
self.yaws = torch.zeros((self.cfg.scene.num_envs, 1)).cuda()
self.commands = torch.randn((self.cfg.scene.num_envs, 3)).cuda()
self.commands[:,-1] = 0.0
self.commands = self.commands/torch.linalg.norm(self.commands, dim=1, keepdim=True)
# offsets to account for atan range and keep things on [-pi, pi]
ratio = self.commands[:,1]/(self.commands[:,0]+1E-8)
gzero = torch.where(self.commands > 0, True, False)
lzero = torch.where(self.commands < 0, True, False)
plus = lzero[:,0]*gzero[:,1]
minus = lzero[:,0]*lzero[:,1]
offsets = torch.pi*plus - torch.pi*minus
self.yaws = torch.atan(ratio).reshape(-1,1) + offsets.reshape(-1,1)
self.marker_locations = torch.zeros((self.cfg.scene.num_envs, 3)).cuda()
self.marker_offset = torch.zeros((self.cfg.scene.num_envs, 3)).cuda()
self.marker_offset[:,-1] = 0.5
self.forward_marker_orientations = torch.zeros((self.cfg.scene.num_envs, 4)).cuda()
self.command_marker_orientations = torch.zeros((self.cfg.scene.num_envs, 4)).cuda()
この時、マーカのヨー(ロールピッチヨーのヨー)がヨー角はasin(y/x)となるため、x,yの象限によっては違う向きなのに解が一致してしまうことから、オフセットを与えて1,3及び2,4象限を区別します。
このようにして各ロボットにマーカを付着させ、矢印の方向にうまくロボットが進めるように強化学習を行ってみます。
def _get_rewards(self) -> torch.Tensor:
forward_reward = self.robot.data.root_com_lin_vel_b[:,0].reshape(-1,1)
alignment_reward = torch.sum(self.forwards * self.commands, dim=-1, keepdim=True)
total_reward = forward_reward + alignment_reward
return total_reward
とりあえずx軸方向でのベクトルの内積の足し算を考えてみます。
実はこの計算手法では「観測空間が大きすぎる」という弱点があります。現在はコマンド、角速度、線速度で合計9もの状態量を観察する必要があります。そこで、これをドット積、クロス積のz方向、xの前進速度の3にしてみます。
クロス積は矢印の共線関係を非常にうまく表していて、二つの矢印がどの方向にどう動けば合致するかを内積と合わせて適切に示すことができます。
もう一つ、トレーニングは可能な限り報酬関数を削減し、単純化するのが良いとのことでした。今回は上のやつだとない席に加えて余計なx方向速度を足したりしていてよくわからないアイテムが出来上がっています。
そこで、
def _get_rewards(self) -> torch.Tensor:
forward_reward = self.robot.data.root_com_lin_vel_b[:,0].reshape(-1,1)
alignment_reward = torch.sum(self.forwards * self.commands, dim=-1, keepdim=True)
total_reward = forward_reward*alignment_reward
return total_reward
足し算ではなく掛け算にしてみます。式的には単純になった(要素が一つだけになった)のですが、全くの逆方向を向いていてもOK(-*- = +)なのでこのままではダメです。そこでexponentialを使い、
def _get_rewards(self) -> torch.Tensor:
forward_reward = self.robot.data.root_com_lin_vel_b[:,0].reshape(-1,1)
alignment_reward = torch.sum(self.forwards * self.commands, dim=-1, keepdim=True)
total_reward = forward_reward*torch.exp(alignment_reward)
return total_reward
このようにするとforward_rewardの負の値もしっかり反映されます。
この形は焼きなまし法などの機械学習分野でよく見る形で、exponentialの強みが出ている部分だと感じました。
参考
https://isaac-sim.github.io/IsaacLab/main/source/setup/walkthrough/index.html
原文はこちらにあります。