Isaac Labで強化学習を使ってUnitree Go2にバク転を学習させる

Isaac Labで強化学習を使ってUnitree Go2にバク転を学習させる

2026.04.20

はじめに

本記事では、四足歩行ロボットのUnitree Go2に、強化学習を使ってバク転を学習させるトライをした記録を共有します。

強化学習は初めて触る方にも読めるように書いたつもりですが、Isaac Sim / Isaac Labの基本的な操作やインストールについては過去記事をご覧いただければと思います。

過去記事
https://dev.classmethod.jp/articles/install-isaac-sim/
https://dev.classmethod.jp/articles/isaac-lab-unitree-g1-simulation-setup/
https://dev.classmethod.jp/articles/unitree-g1-dex3-hand-isaac-sim/
https://dev.classmethod.jp/articles/isaac-sim-basic-gui-operations/
https://dev.classmethod.jp/articles/unitree-g1-inspire-hand-demo-deploy/

そもそも強化学習とは何か

バク転のような動的な動作は、人間が関節角度を1つずつ指定して作るには複雑すぎます。そこで強化学習という手法を使います。

強化学習はものすごくざっくり言うと「学習の主体者自身が、報酬を最大化する行動を試行錯誤で獲得する」枠組みです。ロボットが環境に対して行動を取ると、環境は次の状態と報酬を返します。これを大量に繰り返す中で、ロボットは「どういう状態でどういう行動を取れば報酬が最大になるか」を学習していきます。

今回の場合、ロボットは関節角度の目標値を出力し、環境(物理シミュレーション)が次のロボットの姿勢と報酬を返します。報酬は「高く跳べたら+」「空中で後方回転したら+」「転倒したら-」のように設計者が定義します。

環境と前提条件

項目 バージョン・構成
OS Ubuntu 24.04
GPU NVIDIA RTX 5090
Isaac Sim 5.1.0
Isaac Lab v2.3.2
rsl-rl-lib 5.0.1

学習用環境の骨格はUnitree公式のunitree_rl_labをベースにしています。このリポジトリには歩行タスクのサンプルが含まれていますが、バク転タスクは含まれていないため、新規に追加する形で実装します。

セットアップ

unitree_rl_labの導入

cd ~
git clone https://github.com/unitreerobotics/unitree_rl_lab.git
cd unitree_rl_lab

# Isaac Labのcondaを有効化
conda activate env_isaaclab

# rsl-rl 5.0.1をインストール
pip install rsl-rl-lib==5.0.1

# unitree_rl_lab本体をインストール
./unitree_rl_lab.sh -i

Go2のURDFを読み込むための設定

URDF(Unified Robot Description Format)はロボットの物理構造を記述するXMLフォーマットです。リンク(体の各パーツ)、ジョイント(関節)、慣性モーメント、衝突形状などが定義されており、シミュレータはこれを読み込んでロボットをセットアップします。Unitreeは公式でGo2のURDFを配布しており、Isaac Sim側でこれを読み込む形になります。

unitree_rl_lab/source/unitree_rl_lab/unitree_rl_lab/assets/robots/unitree.pyで、Go2のURDFパスを環境に応じて調整します。

私の環境ではUnitree公式のROSリポジトリを別途クローンしてあるため、以下のように環境変数経由でパスを指定しています。

export UNITREE_ROS_DIR=/home/my-name/unitree_ros

rsl-rl 5.0.1互換のための修正

rsl-rl 5.0.1ではActor/Critic APIが変更されており、unitree_rl_labの既存configとの互換性対応が必要です。
具体的にはrsl_rl_ppo_cfg.pyをactor/critic分離型に書き換え、train.py/play.pyで
stochastic, distribution_cfg等のキーをOnPolicyRunnerに渡す前に除去する必要があります。

バク転タスクの登録

新規タスクとしてbackflipディレクトリを追加します。

cd ~/unitree_rl_lab/source/unitree_rl_lab/unitree_rl_lab/tasks
mkdir backflip

タスクを自動登録できるように、scripts/list_envs.pyにパッケージを追加します。

# scripts/list_envs.py
def import_packages():
    ...
    for package in ["locomotion.robots", "mimic.robots", "backflip"]:
        ...

タスクの実装

報酬関数の設計

強化学習の成否は報酬関数の設計にかかっています。何度も試行錯誤した結果、最終的には以下の構成になりました。

報酬・マイナス報酬 重み 目的
rew_height +10.0 高く跳ぶほど+
rew_upright +2.0 地上で直立姿勢を維持
rew_vert_vel +2.0 上方向の速度に+
rew_airborne_rotation +15.0 空中での後方回転積分に+
rew_ang_vel +0.5 後方回転角速度に+
rew_landing_stable +5.0 バク転完了後に直立・低速で+
pen_wrong_direction -0.5 前方回転にマイナス報酬
pen_roll_yaw -0.1 ロール・ヨー方向の回転にマイナス
pen_torque -0.00002 トルクの2乗和にマイナス報酬
pen_action_rate -0.005 アクションの急変にマイナス報酬

各項目を足し算してtotalにしています。

total = (rew_height + rew_upright + rew_vert_vel + rew_rotation + rew_ang_vel
         + pen_wrong_direction + pen_roll + pen_yaw + rew_stable
         + pen_torque + pen_action_rate + pen_warmup_action)

この構成に至るまでの経緯は記事の後半で詳しく紹介します。

エピソード終了条件

def _get_dones(self):
    base_pos = self.robot.data.root_pos_w
    height = base_pos[:, 2]
    time_out = self.episode_length_buf >= self.max_episode_length - 1
    too_low = height < self.cfg.termination_height  # 0.08m
    past_warmup = self.episode_length_buf > 20
    terminated = too_low & past_warmup
    return terminated, time_out

最初の20ステップは「ウォームアップ期間」としてターミネーション判定を無効化しています。リセット直後にロボットが姿勢を安定させるまでの期間中に早すぎるtermが入るのを防ぐためです。

段階的に学習させる

バク転をいきなり学習させようとしてもロボットは何も掴めず、報酬がゼロのまま学習が進みません。そこで、段階的に難易度を上げていくことにしました。

Phase 1: ジャンプ学習(5000イテレーション)

最初は「高く跳んで直立姿勢を維持する」だけの課題にします。回転報酬はまだ入れません。

# Phase 1の報酬構成
rew_height = 10.0
rew_upright = 2.0
# rew_airborne_rotation は使わない

4096並列環境で5000イテレーション回したところ、ロボットはジャンプを獲得しました。

Mean reward: 574.95
Mean episode length: 99.00
Mean action std: 0.3付近

Phase 2: 空中回転を追加(10000イテレーション)

Phase 1の学習済みモデルを起点に、空中で後方回転したら報酬を与える項を追加します。

is_airborne = height > 0.35  # 空中にいる判定
pitch_vel_world = base_ang_vel_w[:, 1]

# 空中にいる時のピッチ角速度(後方回転方向)を積分
airborne_pitch_delta = (-pitch_vel_world) * dt * is_airborne.float()
self.airborne_pitch += airborne_pitch_delta

# 積分値に比例する報酬
pitch_frac = self.airborne_pitch / (2 * math.pi)
rew_rotation = 15.0 * pitch_frac.clamp(0, 1.2)

ここでロボットが「空中でどう動けば後方回転になるか」を学習していきます。

10000イテレーション後、ロボットは立派なバク転をできるようになりました。Mean rewardは2000近くまで上がり、GUI上で実際に後転する動作が確認できます。

Phase 3: 動作の洗練(3000イテレーション)

この時点でバク転自体はできていますが、よく見ると粗が目立ちます。

  • 回転方向が個体によってブレる(前転する個体もいる)
  • 空中でロール・ヨー方向にもグルグル回る
  • 着地が不安定で、そのまま次のバク転を始める

https://youtu.be/ZPbVpX-nWC4?si=9QuMK7T8w2UosnKL

そこで動作を洗練させるために、細かいマイナス報酬を追加しました。

# 前方回転(間違った方向)にマイナス報酬
forward_pitch_vel = pitch_vel_world.clamp(min=0, max=20) * is_airborne.float()
pen_wrong_direction = -0.5 * forward_pitch_vel

# ロール・ヨー方向の角速度の2乗にマイナス報酬
pen_roll = -0.1 * (roll_vel_world ** 2)
pen_yaw = -0.1 * (yaw_vel_world ** 2)

# バク転完了後、直立・低速な状態で+
rew_landing_stable = 5.0 * is_stable.float()

3000イテレーション後、動作の方向性と安定性がかなり改善されました。

失敗の記録

実はPhase 3にたどり着くまで、何度も失敗してやり直しをしています。

失敗その1: 報酬の急激な変更で学習が発散

Phase 2の学習済みモデルから、いきなり巨大な着地報酬(rew_landing = 50)と「空中回転後は回転報酬ゼロ」というロジックを同時に追加したところ、数百イテレーションで学習が崩壊しました。

# 崩壊時の指標
Mean action std: 1.27 → 3.76  (約3倍に上昇)
Mean value loss: 130 → 2403  (18倍以上に爆発)

Action stdとはロボットが取る行動のランダム性で、学習が進むと小さく収束していくのが正常です。それが逆に大きくなっているということは、以前学習したバク転の能力が失われて、ポリシーがランダムに近い行動に戻ってしまったことを意味します。

原因は報酬スケールの急激な変更です。Phase 2では存在しなかった巨大な報酬項が新たに入ったことで、Critic(報酬の予測モデル)の誤差が拡大し、連動してActor(ポリシー)も破壊されました。

学び: 既存のポリシーから継続学習する場合、報酬スケールを大きく変えないこと。

失敗その2: 関節速度へのマイナス報酬を入れたら着地できなくなった

実機デプロイを見据えて、関節のおかしな動きを抑えるマイナス報酬を追加しました。

pen_joint_vel = -0.0001 * torch.sum(joint_vel ** 2, dim=-1)
pen_joint_acc = -2.5e-8 * torch.sum(joint_acc ** 2, dim=-1)

学習後の数値上はMean reward 2344、Action std 0.34と綺麗に収束していましたが、実際の挙動を見ると着地できなくなっていました

これは報酬ハッキングの典型例です。着地は本質的に関節を大きく動かす必要があるため、関節速度・加速度のマイナス報酬を嫌うロボットは、「そもそも着地できる姿勢に入らない」という選択をしました。数値上は学習が進んでいるように見えても、タスクの本質を捉えていません。

学び: 動作全体に一律のマイナス報酬を設定すると、必要な動作まで抑制されてしまう。

失敗その3: 着地安定報酬を上げすぎて「地味なジャンプ」に退化

連続バク転を防ぐために、バク転完了後の直立・低速状態に大きな報酬(rew_landing_stable = 15.0)を与えたところ、ロボットはバク転をやめて「その場で小さく跳ねるだけ」の動作に退化しました。

報酬設計上、以下のように最適戦略が変わってしまったためです。

  • バク転する: 空中回転報酬を稼ぐが、着地が難しく安定報酬は不確実
  • その場で跳ねる: 空中回転報酬は無いが、常に直立・低速なので安定報酬が確実に入る

総合すると後者のほうが期待報酬が高くなってしまっていました。

学び: ある報酬項のスケールを上げると、その報酬だけを効率よく稼ぐ別の戦略に乗り換えてしまうことがある。

失敗その4: has_flippedフラグで回転報酬を無効化したら退化

連続バク転対策として、1回バク転が完了したら以降は回転報酬・ジャンプ報酬を全てオフにするフラグを入れました。結果、失敗その3と同じ「地味なジャンプ」に退化しました。

  • バク転する: 数ステップのバク転中は報酬が入るが、完了後は着地・直立報酬のみ
  • 跳ねるだけ: 常に着地・直立報酬を稼げる

こちらも跳ねるだけの戦略に乗り換えた方が得、となってしまいました。

学び: 報酬のオン/オフを条件付きで切り替えると、予期しない戦略が最適解になる。

成功パターン: エピソードの時間を短縮して物理的に制約

連続バク転を抑制しつつ報酬設計を崩さない方法として、エピソード時間を1.5秒に短縮するというシンプルな対処に落ち着きました。

episode_length_s = 1.5

バク転1回は約1秒かかるので、1.5秒では2回目を完了することが物理的に不可能です。報酬設計はそのままで、「時間切れで2回目は諦める」しかない状況をシミュレータ側で作ることで、意図した動作を得ました。

学び: 報酬設計で無理やり制約をかけるより、環境側の物理制約で誘導する方がシンプルで壊れにくい。

その他の細かい調整

リセット時の初期高さ

学習を進める中で「ロボットが毎回シミュレーション開始時に上から落ちてくる」という現象に気がつきました。調べると、URDFで定義された初期位置(z=0.4m)が実際の立ち姿勢(z=0.17m)より高く、その差分を毎回落下していたのが原因でした。

default_root[:, 2] = 0.17  # 実際の立ち姿勢の高さに変更

ウォームアップペナルティ

リセット直後の数ステップは姿勢制御が不安定になりがちなので、最初の20ステップは小さなマイナス報酬を入れて静止を促しました。

is_warmup = self.episode_length_buf < 20
pen_warmup_action = -0.05 * torch.sum(self.actions ** 2, dim=-1) * is_warmup.float()

学習を実行する

最終的なbackflipタスクは以下のように3フェーズで学習します。

# Phase 1: ジャンプ学習
conda activate env_isaaclab
cd ~/unitree_rl_lab

python scripts/rsl_rl/train.py \
    --headless \
    --task Unitree-Go2-Backflip-v0 \
    --num_envs 4096 \
    --max_iterations 5000

Phase 2以降は前段のチェックポイントを指定して継続学習します。

# Phase 2: Phase 1のチェックポイントから回転報酬を追加して学習
python scripts/rsl_rl/train.py \
    --headless \
    --task Unitree-Go2-Backflip-v0 \
    --num_envs 4096 \
    --max_iterations 10000 \
    --checkpoint logs/rsl_rl/unitree_go2_backflip_v0/PHASE1_RUN/model_4999.pt

Phase 1が50分弱、Phase 2が1時間40分程度で完了しました。

学習したポリシーで動作確認

python scripts/rsl_rl/play.py \
    --task Unitree-Go2-Backflip-v0 \
    --num_envs 1 \
    --checkpoint logs/rsl_rl/unitree_go2_backflip_v0/FINAL_RUN/model_2999.pt

num_envsを16などにすると同じポリシーでも個体によって動作が微妙に異なることが観察でき、ポリシーの頑健性が分かります。

動作をゆっくり確認したい場合、scripts/rsl_rl/play.pyのシミュレーションループにtime.sleepを挟むことでスロー再生になります。

# 該当箇所
slow_factor = 5.0
sleep_time = dt * slow_factor - (time.time() - start_time)
if sleep_time > 0:
    time.sleep(sleep_time)

https://youtu.be/j9MWjt2mTTA?si=IXotZFcU2d8DAWYO

残課題

今回のポリシーはシミュレータ上でバク転できるようになりましたが、いくつか課題が残っています。

  • 着地の安定性: 足からではなく腹から着地気味になる
  • 関節の不自然な動き: 空中で関節が実機では不可能な角度になる
  • 連続バク転の抑制: エピソード時間の短縮で対処しましたが、根本的な解決ではなく時間切れで強制終了しているだけです。しかも解決しきれていません。
  • Sim-to-Realギャップ: 実機Go2でそのまま動かすには物理モデルの差分が大きく、Domain Randomization等の対策が必要です

Domain Randomizationを試したが失敗した話

実機デプロイに向けて、Domain Randomizationを試しました。これは学習時に、シミュレータ内のロボットの質量や関節の特性(硬さ・粘性)をエピソードごとに少しずつランダムに変動させる手法です。狙いは、シミュレータと実機の物理的な誤差を吸収し、「多少条件が変わっても動けるポリシー」を学習させることです。
しかし、既存のバク転ポリシーから継続学習する形では上手く学習が進まず、バク転の動作自体が崩れてしまいました。

Sim-to-Real対応は学習をやり直す必要があると思われ、大規模な作業です。次回の記事で改めて取り組みます。

まとめ

強化学習を使ってUnitree Go2にバク転を学習させることができました。
最終的には目的達成までいけませんでしたが、その過程で多くの学びがありました。
ここでの学びをもとに成功に辿り着きたいと思います。

次回の記事では、Domain Randomizationを入れた学習と、学習したポリシーを実機Go2にデプロイするSim-to-Real対応に取り組みます。

この記事をシェアする

関連記事