1. はじめに
前回の記事 では、Unitree G1 を ROS 2 Humble + Gazebo Classic 11 で歩かせるところまで書きました。ただし上半身 17 関節を固定した構成での歩行であり、解放すると転倒するという状態でした。
今回はその続きです。29 自由度をすべて解放したまま歩けるようになったので、そこに至る過程を記事にしました。
👇:29 関節すべてを解放したままの歩行です (同梱ポリシー、40 秒・転倒なし)。前回記事の動画と同じ画角・同じ長さなので、並べると腕が振れている差が分かります
先に結論を書いておくと、効いたのは自前の再学習ではなく armature の設定ミスの修正でした。29 自由度のポリシー自体は必要でしたが、それは unitree_rl_mjlab に同梱されているもので足ります。10 時間かけて学習を回したあとで気づいたので、その顛末もそのまま書きます。
2. 前回からの流れ
前回使用していたポリシーは unitree_rl_gym の motion.pt です。これは脚 12 関節のみを制御するモデルで、観測は 47 次元でした。ここでいう「解放」は関節を放り出すことではなく、unitree_rl_gym の実機デプロイ設定 (deploy_real.py) と同じゲインで保持したうえで可動にすることを指します。
それでも転倒します。固定した構成では、上半身を学習時の MuJoCo モデルの上半身 (1 剛体・17.7 kg) に置き換えてありました。解放すると URDF 本来の 20.7 kg に戻り、ヨーまわりの慣性は 1.24 倍になります。歩行では脚を振り出すたびに胴体がヨー方向の反作用を受けるので、ここの差が効いていると見ています。これが前回の記事の結論でした (ただし 5 章の末尾に書くとおり、別の要因が残っている可能性があります)。
そこで 29 自由度で学習し直そう、というのが今回の出発点でした。
3. 学習環境: unitree_rl_mjlab
Unitree は 2026 年 1 月に unitree_rl_mjlab を公開しています。unitree_rl_gym が Isaac Gym ベースだったのに対し、こちらは MuJoCo Warp ベースです。
| unitree_rl_gym | unitree_rl_mjlab | |
|---|---|---|
| 学習基盤 | Isaac Gym (NVIDIA、登録が必要) | MuJoCo Warp (Apache-2.0) |
| G1 の対象関節 | 脚 12 | 29 |
| 観測次元 | 47 | 98 |
Isaac Gym は配布終了・要登録で、環境構築の敷居が高めです。MuJoCo Warp なら pip で入るので、こちらのほうが手を出しやすいと思います。私の環境 (RTX 2070) で 10001 反復・10 時間 18 分でした。
3.1 環境構築でのバージョン合わせ
素直に入れると動きませんでした。組み合わせを固定して解決しています。
| 症状 | 原因 | 対処 |
|---|---|---|
mjENBL_MULTICCD が無いと言われる |
mujoco 3.11 と mujoco-warp 3.5.0 で定数が食い違う |
mujoco==3.5.0 に固定 |
wp.context が無いと言われる |
warp-lang 1.16 で内部モジュール構成が変わった |
warp-lang==1.12.1 に固定 |
| 学習開始時に W&B のログインを求められる | 既定のロガーが wandb |
--agent.logger tensorboard を指定 |
環境そのものは Miniconda を使わず、素の venv で作りました。mjlab は Python 3.10 で動くので、これで問題ありません。
4. ハマったところ① 学習を回す前に deploy/ を見るべきだった
10 時間の学習が終わったあとで気づいたのですが、unitree_rl_mjlab には 29 自由度の学習済み ONNX ポリシーが最初から同梱されています。
deploy/robots/g1/config/policy/velocity/v0/exported/policy.onnx
同じディレクトリに deploy.yaml があり、観測の並び・ゲイン・既定関節角・アクションスケールがすべて書かれています。実機デプロイ用の設定ですが、シミュレータで動かすのに必要な情報はここに揃っています。
学習を始める前にまず deploy/ を覗く。それだけで、10 時間の学習は要りませんでした。
5. ハマったところ② armature はモータ型番ごとに違う
同梱の 29 DoF ポリシーを Gazebo に持っていったところ、1.5 秒で沈んで転倒しました。
t=0.00s z=0.783 grav_z=-0.999 未転倒
t=0.50s z=0.748 grav_z=-0.953 未転倒
t=1.00s z=0.652 grav_z=-0.959 未転倒
t=1.50s z=0.081 grav_z=-0.177 転倒
傾きは 20° 以内 (grav_z で -0.95 程度) に留まったまま、高さだけが落ちています。脚が体を支えきれずに潰れる動きです。
前回の記事で armature (減速機を介したモータロータの換算慣性) の話を書きましたが、そのときは Unitree の MJCF の <default> に書かれている一律 0.01 を、脚 12 関節にだけ入れていました。mjlab はそうではなく、モータの型番ごとにロータ慣性と減速比から計算した値を全関節に入れています。
| モータ | 適用関節 | armature |
|---|---|---|
| 7520-22 | 股 roll、膝 | 0.025102 |
| 7520-14 | 股 pitch、股 yaw、腰 yaw | 0.010178 |
| 5020 | 肩、肘、手首 roll | 0.003610 |
| 5020 × 2 | 足首 pitch/roll、腰 roll/pitch | 0.007219 |
| 4010 | 手首 pitch、手首 yaw | 0.004250 |
一律 0.01 では、膝と股 roll は 2.5 倍不足する一方、足首では逆に過大でした。そして腰と腕には何も入っていませんでした。
Gazebo 側での入れ方は前回と同じです。URDF に armature に相当する記述が無いので、関節軸 a に対して I += armature × a aᵀ を子リンクの慣性テンソルに加算します。値を型番別にし、対象を脚 12 関節から全 29 関節に広げた、という違いだけです。
これは近似である点に注意してください。MuJoCo の armature は関節空間の質量行列の対角にだけ載りますが、リンク慣性に足すと親関節から見た実効慣性まで増えます。膝の場合、加算先である脛リンクの <inertial> (重心まわりのテンソル) の膝軸方向の成分が 0.0364 kg·m² で、そこへ足す armature が 0.0251 kg·m² です。同じ基準どうしで 7 割ほど上乗せする計算になるので、無視できる大きさではありません。9 章で見る MuJoCo との差にはこの近似も含まれています。
これを直すと転倒しなくなったので、ここが効いていたのは間違いありません。ただし、なぜ振動ではなく「沈む」形で出たのかは分かっていません。腰と腕の実効慣性が不足したまま deploy.yaml のゲインを掛けていたので、前回の記事に書いた離散時間の安定限界を割っていたのではないか、とは考えています。ただし上のログは 0.5 秒間隔のサンプルなので、制御周期 50 Hz・物理 500 Hz の系で起きる振動はそもそも写りません。振動の有無は判定できておらず、仮説のまま置いてあります。
もう一つ未検証のことがあります。前回の記事で上半身を解放すると転倒していた件にも、腰と腕に armature が入っていなかったことが効いていた可能性があります。前回は上半身の質量差が原因だと結論づけましたが、armature を型番別に入れ直した状態で試してはいません。
5.1 値が正しいことの確認方法
この値は当てずっぽうではないことを確認できます。mjlab は PD ゲインを armature から決めており、固有振動数 10 Hz、減衰比 2.0 として
kp = armature × (2π × 10)²
kd = 2 × 2.0 × armature × (2π × 10)
としています。実際に計算すると、deploy.yaml に書かれている stiffness / damping と小数第 1 位まで一致します (deploy.yaml 側は 1 桁表記です)。
| モータ | armature | 計算した kp | deploy.yaml | 計算した kd | deploy.yaml |
|---|---|---|---|---|---|
| 7520-22 | 0.025102 | 99.098 | 99.1 | 6.310 | 6.3 |
| 7520-14 | 0.010178 | 40.179 | 40.2 | 2.558 | 2.6 |
| 5020 | 0.003610 | 14.251 | 14.3 | 0.907 | 0.9 |
| 5020 × 2 | 0.007219 | 28.501 | 28.5 | 1.815 | 1.8 |
| 4010 | 0.004250 | 16.778 | 16.8 | 1.068 | 1.1 |
厳密には、これは独立した検証ではありません。ゲインがこの式で生成されている以上、逆算して一致するのは当然だからです。確認できるのは「MJCF の armature と deploy.yaml のゲインが同じ式で整合している」ことまでです。それでも実用上は便利で、逆に言えば deploy.yaml の stiffness を (2π×10)² で割れば armature が求まります。MJCF を読まなくても値を復元できるので、覚えておくとよいと思います。
5.2 足首と腰は 2 倍
上の表で「5020 × 2」としている 4 関節には落とし穴があります。
私は最初これを 1 個ぶんの 0.003610 で入れていました。歩きはしたのですが、あとから検算して気づきました。この 4 関節の stiffness は deploy.yaml では 28.5 ですが、0.003610 から計算される kp は 14.251 で、ちょうど半分にしかなりません。
G1 の足首と腰 roll/pitch は 5020 を 2 個使う 4 節リンクで駆動されています。unitree_rl_mjlab のソースにはこうあります。
Due to the parallel linkage, the effective armature at the ankle and waist joints
is configuration dependent. Since the exact geometry of the linkage is unknown, we
assume a nominal 1:1 gear ratio.
厳密には姿勢によって変わるが、1:1 とみなして 2 個ぶんの和を使う、ということです。トルク上限も同様に 2 倍 (25 → 50 N·m) になります。
2 倍に直した結果、0.8 m/s 指令での速度追従が改善しました。足首・腰を 2 倍にする前後の比較です。
2 倍にする前 2 倍にした後
同梱ポリシー -0.031 -0.006 m/s
自前学習 -0.123 -0.078 m/s
ただし同梱ポリシー側の改善幅 0.025 m/s は、後述する実行ごとのばらつきと同じくらいの大きさです。明確に効いたと言えるのは自前学習側 (0.045 m/s) だけかもしれません。
なお、29 関節すべてを解放したまま転倒せずに歩くようになったのは、その手前の段階です。一律 0.01 を型番別の値に置き換えた時点で歩き始め、足首・腰の 2 倍はそこからの上積みでした。いずれにせよ、自前で学習し直す必要はありませんでした。29 自由度のポリシー自体は必要でしたが、それは同梱されているもので足ります。
6. ハマったところ③ チェックポイントを ONNX にする CLI が無い
自前で学習した重みを Gazebo に持っていこうとしたのですが、mjlab / rsl_rl 5.0.1 には「チェックポイント単体を ONNX に変換する」コマンドが見当たりませんでした。学習ループの中でしか書き出されません。
とはいえ推論経路は素直で、チェックポイントの中身を見ると
obs_normalizer._mean (1, 98)
obs_normalizer._std (1, 98)
mlp.0.weight (512, 98) # 以降 ELU
mlp.2.weight (256, 512)
mlp.4.weight (128, 256)
mlp.6.weight (29, 128)
distribution.std_param (29,)
だけです。std_param は学習時のサンプリング用で推論には使わないため、MLP の出力 (分布の平均) がそのまま action になります。
class ExportedActor(nn.Module):
def forward(self, obs):
return self.mlp((obs - self.mean) / (self.std + EPS))
正規化は (x - mean) / (std + eps) であって sqrt(var + eps) ではない点だけ注意です。eps は 1e-2 で、無視できる大きさではありません。
書き出したあとは、同じ入力を PyTorch と ONNX Runtime の両方に通して一致を確認しておくと安心です (私の場合は最大誤差 5e-06 でした)。
7. 同梱ポリシーと自前学習を比べる
せっかくなので、同じ条件で比較しました。指令速度 3 通り × 40 秒、Gazebo Classic 上での結果です。
| 指令 | ポリシー | 対地速 | 誤差 | 胴体高さの変動 |
|---|---|---|---|---|
| 0.3 | 同梱 (Unitree) | 0.293 | -0.007 | 0.014 |
| 0.3 | 自前学習 | 0.285 | -0.015 | 0.011 |
| 0.5 | 同梱 (Unitree) | 0.482 | -0.018 | 0.018 |
| 0.5 | 自前学習 | 0.454 | -0.046 | 0.012 |
| 0.8 | 同梱 (Unitree) | 0.794 | -0.006 | 0.027 |
| 0.8 | 自前学習 | 0.722 | -0.078 | 0.014 |
6 条件すべて転倒しませんでした。
速度の追従は同梱ポリシーのほうが上です。誤差はおおむね 4 % 以内に収まっています。自前学習は 5〜10 % 遅く、指令が速いほど差が開きます。
一方で胴体高さの変動は自前学習のほうが小さく、上下動の少ない歩き方をします。ただしこれは「遅く歩いているぶん揺れていない」だけの可能性もあり、優劣として読むのは早いと思っています。
なお、この表を含め本記事の数値はいずれも各条件 1 回の測定です。前回の記事に書いたとおり、同じ設定でも実行ごとに数 % はばらつくので、目安として読んでください。
率直なところ、10 時間かけて配布物より悪いものができたという結果です。
8. ハマったところ④ 学習中の指標の順位は別のシミュレータでは通用しない
TensorBoard を見ると、報酬が反復 4600 あたりで 40.9 のピークを打ち、そこから 28.7 まで落ちて戻っていませんでした。速度追従誤差も 0.53 → 0.83 と悪化しています。
反復 報酬 速度誤差
2000 38.69 0.570
4000 40.15 0.543
6000 26.31 0.892
8000 28.35 0.848
10000 28.70 0.833
指令速度の範囲は最初から ±1.0 m/s 固定でカリキュラムは入れていないので、「課題が難しくなったから下がった」わけではありません。純粋に後半で退行しています。
そこで「最良の 4600 反復目を使えばよいのでは」と思い、そちらも書き出して Gazebo で回してみました。チェックポイントの保存間隔は指定しておらず mjlab の既定のままで、そこに残っていた 4600 反復目のものを ONNX に変換しています。結果は 10000 反復目とほぼ同じか、むしろ少し悪いくらいでした。
学習中の指標で決めた順位が、別のシミュレータでの挙動を予測しませんでした。 最終的にどこで動かすかが決まっているなら、チェックポイントの選定もそこで評価するのが確実だと思います。
9. MuJoCo と Gazebo を並べて見る
sim2sim の差を見るために、同じ ONNX を両方のシミュレータで回して録画しました。
👇:同じ ONNX を MuJoCo と Gazebo の両方で回したものを 2×2 に並べています (左上 MuJoCo + 同梱、右上 MuJoCo + 自前学習、左下 Gazebo + 同梱、右下 Gazebo + 自前学習)
比較として成立させるため、観測ベクトルの組み立て・PD ゲイン・制御周期 (50 Hz)・物理ステップ (500 Hz)・armature を両方で共有しています。定数は 1 つのモジュールにまとめて双方から読む形にしました。とはいえ揃えられるのはここまでで、衝突形状や接触モデル、慣性諸元の出どころまでは一致していません。
| MuJoCo | Gazebo Classic | |
|---|---|---|
| 同梱ポリシー | 0.501 m/s | 0.482 m/s |
| 自前学習 | 0.476 m/s | 0.454 m/s |
前回はここを揃えるのに苦労しましたが、armature を型番ごとの実値にしたことで速度がかなり近づきました。ただし 2 条件とも Gazebo のほうが 4〜5 % 遅く、ずれの向きが揃っています。n=2 なので偶然の可能性も残りますが、5 章の近似 (armature をリンク慣性に足す) は Gazebo 側の実効慣性を過大にする方向に効くので、系統差だとすれば向きは合っています。
9.1 録画まわりで踏んだもの
動画を撮るだけでも 2 つ引っかかったので書いておきます。
MuJoCo のオフスクリーンバッファは既定 640×480 です。 960×600 で Renderer を作ろうとすると弾かれます。MJCF を書き換えなくても model.vis.global_.offwidth / offheight を設定すれば通ります。
ROS 2 の gazebo_ros_state に /gazebo/set_model_state トピックはありません。 ROS 1 にはあったのですが、ROS 2 ではサービス (/gazebo/set_entity_state) だけです。私はカメラをロボットに追従させるためにトピックへ publish していたのですが、当然何も起きず、カメラが原点に取り残されて被写体が点になっていました。エラーも警告も出ないので気づきにくいところです。
10. トラブルシューティングまとめ
| 症状 | 原因 | 対処 |
|---|---|---|
| 29 DoF ポリシーが 1.5 秒で沈んで転倒する | armature がモータ型番ごとの値になっていない (沈む機序自体は未解明) |
deploy.yaml の stiffness ÷ (2π×10)² で求める |
| 足首・腰まわりだけ挙動が硬い/柔らかい | この 4 関節は 5020 が 2 個の 4 節リンクなので armature・トルク上限とも 2 倍 | 2 倍にする |
| チェックポイントを ONNX にできない | mjlab / rsl_rl 5.0.1 に単体変換の CLI が無い | 正規化 + ELU MLP を組み直して torch.onnx.export
|
| ONNX の出力が学習時とずれる | 正規化が (x-mean)/(std+eps)、eps=1e-2 |
式をそのまま写す |
mjENBL_MULTICCD が無い |
mujoco と mujoco-warp のバージョン不整合 | mujoco==3.5.0 |
wp.context が無い |
warp-lang 1.16 の構成変更 | warp-lang==1.12.1 |
| MuJoCo の Renderer が作れない | オフスクリーンバッファ既定 640×480 |
model.vis.global_.offwidth を設定 |
| Gazebo のモデル姿勢を変えられない | ROS 2 の gazebo_ros_state はサービスのみ |
/gazebo/set_entity_state を使う |
11. まとめ
できたこと
- 29 自由度をすべて解放したまま、転倒せずに歩けるようになった
- 効いたのは armature をモータ型番ごとの値に直したことで、自前の再学習は必要なかった (同梱の 29 DoF ポリシーで足りる)
-
deploy.yamlのゲインから armature を逆算する手が使えると分かった - MuJoCo と Gazebo の速度差が 4〜5 % まで縮まり、sim2sim の比較ができる状態になった
- 学習環境を一通り動かせるようにした。同梱ポリシーは重みだけで報酬も観測も変えられないので、「物を持って歩く」のような課題をやるならここから作ることになる。順番を間違えただけで、投資そのものは無駄ではなかったと思うことにしています
残った課題
- 自前学習が同梱ポリシーに追いつかない。報酬が反復 4600 以降で退行した原因も未解明 (歩行だけなら同梱で足りるので、優先度は下げています)
- armature をリンク慣性に足す近似が残っており、Gazebo 側に系統差として出ている (5 章・9 章)
- 沈んで転倒した理由が説明できていない。前回記事の転倒に armature が効いていた可能性も未検証 (5 章)
- 「物を持って歩く」ような課題は、報酬設計から作る必要があるのと、それをした上で当初の目的である、タワークレーン環境と Open-RMF 環境への G1 の投入
参考リンク
- unitreerobotics/unitree_rl_mjlab — MuJoCo Warp ベースの学習環境と 29 DoF 学習済みポリシー (Apache-2.0)
- mujocolab/mjlab — 上記のベースになっているフレームワーク (Apache-2.0)
- unitreerobotics/unitree_ros — URDF / MJCF / メッシュ (BSD-3-Clause)
- unitreerobotics/unitree_rl_gym — 前回使用した脚 12 関節のポリシー (BSD-3-Clause)
ライセンス / クレジット
本記事は下記のオープンソースを利用しています。ライセンスは各リポジトリの LICENSE ファイルに従います。
-
unitree_rl_mjlab(29 DoF の学習済み ONNX ポリシー、
deploy.yaml、学習環境)— Apache License 2.0, Unitree Robotics -
mjlab(
unitree_rl_mjlabの基盤フレームワーク)— Apache License 2.0, mujocolab -
unitree_ros(
g1_descriptionの URDF / メッシュ)— BSD 3-Clause License, Copyright (c) HangZhou YuShu TECHNOLOGY CO.,LTD. ("Unitree Robotics") -
unitree_rl_gym(前回記事で使用した
motion.pt)— BSD 3-Clause License, Copyright (c) HangZhou YuShu TECHNOLOGY CO.,LTD. ("Unitree Robotics")
5.2 で引用した armature についてのコメントは unitree_rl_mjlab のソースからの引用です。