# 歩行を学習させる(強化学習)
:::{sidebar} 余談: ポリシー?
:class: yodan
強化学習でいう**方策**(ポリシー、policy)は、いまの状態を受け取って、次に
どう動くかを返す関数のことです。
僕も最初に聞いたときは、ポリシーって自分が大事にしている考え方のことだと
思っていました。いまなら「このロボットのポリシーは?」と聞かれたら、
「関節の角度と速度を入れると各関節の目標角度が出てくる、3 層のニューラル
ネットワークです」と答えます。
:::
第13章の四脚は、**強化学習で学習した方策**で歩きます。紙面の都合で
本には載せられなかった学習まわりを、ここでまとめてサポートします。
強化学習とは何か、使っている道具(PPO・MuJoCo・mjlab)が何かは
{ref}`what-is-rl` にまとめました。
## ブラウザで歩かせてみる
学習済み方策を、物理シミュレータ **MuJoCo** の WebAssembly 版(ブラウザの中で
動く形にしたもの)で動かすシミュレーションです。
実機に書き込むものと同じニューラルネットワークが、ブラウザの中で
50 Hz の制御ループを回しています。前進・旋回の指令をスライダーで変えられます。
「押す」で胴体を横から押すこともできます。「RL 方策(IMU あり)」は
{doc}`imu` の方策です。
物理は**学習したときと同じ**にしてあります。接地するのは足先の球 4 つだけ、
サーボは位置制御(kp 25・kd 0.5)+ 逆起電力に相当する粘性減衰 + ギアの摩擦で、
指令は 20〜35 ms(実機で測った 27 ms を挟む幅)遅れてサーボに届きます。
```{raw} html
歩行シミュレーションを開く ↗
```
```{note}
初回は物理エンジン(WebAssembly 版の MuJoCo)とロボットモデルで約 16 MB ダウンロードします。
```
「制御」の切替で、**本文 13.5 の sin/cos 歩行**(状態機械 + トロット。対角の
2 本ずつを組にして交互に脚を出す歩き方)と、強化学習(RL, Reinforcement
Learning)の方策を比べられます。sin/cos 側は、遊脚を sin で山なりに持ち上げて、
2 リンクの逆運動学(IK, Inverse Kinematics。足先の位置から関節角を求める
計算、本文 13.3)で関節角に変換するもので、本のサンプルコード
`src/gait.cpp` + `src/leg_ik.cpp` をそのままブラウザに移植したものです。
ただ、この機体は前後が非対称なので、進む向きで歩き方がまるで変わります。
同じストライド 0.02 m(スライダー ±0.05)で、前へはほとんど進まず
(0.006 m/s)、後ろへは 0.10 m/s で進みます。前進はスライダー 0.12
(ストライド 0.048 m)で 0.06 m/s、0.15 まで上げると 8 回に 1 回は横倒しに
なります。係数を手で回してもどちらかにしか合わせられない——それが RL に
行く動機です。
(what-is-rl)=
## そもそも強化学習とは
**強化学習**は、正しい動かし方を人が教えるのではなく、ロボットに試行錯誤させて
覚えさせる方法です。うまくいったら点数(**報酬**)を与え、点数が多くなる
動き方を少しずつ身につけさせます。この四脚の学習では、たとえば次のような
報酬を決めています。
- 指令された速さで進めたら加点
- 立っていられたら少し加点。胴体が傾きすぎたり、低くなりすぎたりしたら、
その回はそこで打ち切り
- 胴体の高さが目標からずれたら減点
- 関節を必要以上に速く回したり、力を使いすぎたりしたら減点
実機でこれを何度も繰り返すと、時間がかかるうえに機体が壊れます。そこで学習は
物理シミュレーションの中で行い、4096 台のロボットを同時に歩かせます。
学習が終わって残るのが、観測から次の関節の目標角度を出す**方策**で、それを
Arduino に書き込みます。
使っている道具と用語は次の通りです。
| 名前 | 何か |
|---|---|
| **PPO** | Proximal Policy Optimization。方策を少しずつ直していく手順(アルゴリズム)の 1 つで、1 回の更新で方策を変えすぎないように抑えるのが特徴です([論文、2017 年](https://arxiv.org/abs/1707.06347)) |
| **MuJoCo** | Multi-Joint dynamics with Contact。関節でつながった体が、地面などと接触しながら動く様子を計算する物理シミュレータです。Roboti LLC が開発し、2021 年に Google DeepMind が取得して無償化、2022 年にオープンソースになりました([公式ドキュメント](https://mujoco.readthedocs.io/en/latest/overview.html))。上のブラウザのシミュレーションもこれです |
| **mjlab** | MuJoCo の上で強化学習の環境を組み立てるフレームワークです。GPU で MuJoCo をたくさん並べて動かす MuJoCo Warp を使い、環境の書き方は NVIDIA の Isaac Lab に合わせてあります([mujocolab/mjlab](https://github.com/mujocolab/mjlab))。mjlab での学習には NVIDIA の GPU が要ります(GPU が無い場合は {doc}`train`) |
| **unitree_rl_mjlab** | Unitree が公開している、mjlab で自社のロボットを学習させるコード([unitreerobotics/unitree_rl_mjlab](https://github.com/unitreerobotics/unitree_rl_mjlab))。本書はこれに、この機体の定義と報酬を差し込んで学習しました |
| **rsl_rl** | ETH チューリッヒの Robotic Systems Lab が公開している、ロボット向けの強化学習ライブラリです。PPO はこの実装を使っています([leggedrobotics/rsl_rl](https://github.com/leggedrobotics/rsl_rl)) |
| **sim2real** | シミュレーションで学習した方策を実機に持っていくこと。シミュレーションと実機は完全には一致しないので、ずれを見込んで学習します({ref}`下の「学習のコード」 `) |
## 方策の中身
小さな多層パーセプトロン(MLP, Multi-Layer Perceptron。全結合の層を重ねた、
いちばん素朴なニューラルネットワーク)1 つだけで歩いています。Arduino UNO R4
でも毎周期推論できるサイズです。
| | |
|---|---|
| 入力(観測) | **87 次元** = 指令 (vx, vy, wz) ・歩容位相 (sin/cos) ・関節角 8 ・関節速度 8 ・前回 action(方策の出力)8、それぞれ **3 ステップ分の履歴** |
| ネットワーク | 全結合 [96, 64]、活性化関数 ELU(Exponential Linear Unit) |
| 出力 | 8(各関節)。サーボ目標角 = home 角 + 0.25 × action。home 角は home 姿勢(立って歩き出す基準の姿勢)での関節角 |
| 制御周期 | 50 Hz(0.02 s)、歩容クロック 0.32 s |
| センサ | **関節角・関節速度のみ。IMU(慣性計測装置。胴体の傾きや回転の速さを測るセンサ)は使っていない**(この機体に無いため。胴体の姿勢は方策から見えていない。IMU を載せた版は {doc}`imu`) |
外転関節(脚を横に開く関節)が無いので横移動(vy)はできません。旋回は左右の歩幅差で作ります。
シミュレーションでの性能: 前進指令 0.12 m/s で 0.120 m/s(0.48 体長/秒)、
最大の指令 0.15 m/s で 0.148 m/s。指令にほぼ追従します。旋回は指令
0.3 rad/s で 0.213 rad/s。対角トロットは 3.13 Hz(歩容周期 0.32 s)で、
速度によらず一定です。いずれも上のシミュレーションと同じモデル・同じ制御で
20 秒ずつ 5 回歩かせ、後半 10 秒の平均を取った値です。遅いのはサーボの無負荷回転数で決まる
機体の性質で、学習の失敗ではありません。
(walk-training-code)=
## 学習のコード
学習は [mjlab (unitree_rl_mjlab)](https://github.com/unitreerobotics/unitree_rl_mjlab)
の PPO(rsl_rl)で行い、ロボット定義・報酬・環境設定を差し替える overlay(上流のコードはそのまま使い、この機体に必要なファイルだけを上から被せる書き方)として
書いてあります:
- コードを見る: [docs/os-on-arduino/code/13_quadruped/rl](https://github.com/iory/learning-os-from-arduino/tree/main/docs/os-on-arduino/code/13_quadruped/rl)
- ロボット記述一式(**URDF**(ROS などで使うロボットの記述形式)・
MJCF(MuJoCo のモデル形式)・メッシュ・RViz 設定、SolidWorks
エクスポートから MJCF を再現するスクリプト付き):
[docs/os-on-arduino/code/13_quadruped/arduino_os_quad_robot](https://github.com/iory/learning-os-from-arduino/tree/main/docs/os-on-arduino/code/13_quadruped/arduino_os_quad_robot)
sim2real のために、摩擦(0.4〜1.1)・付加質量(電装ぶん 0〜250 g)・
ゼロ点誤差(±1.7°)・関節角(±8.6°)などをランダム化して学習したあと、
リセット姿勢・速度をさらに乱した fine-tune(ロバスト化)を重ねています。
だから組み立てで数度ずれても歩きます。
**自分で学習し直す手順は {doc}`train` にまとめました。**
報酬や速度上限を変えて回してみたいとき、そして
**GPU が無い場合の Google Colab での通し方**はそちらです。
(walk-real)=
## 実機で歩かせる
使うのは第13章のサンプルコードで、{doc}`../chapters/index` の手順で入手した
`docs/os-on-arduino/code/13_quadruped/` です(このサイトの
[`code/13_quadruped/`](https://github.com/iory/build-your-own-arduino-rtos/tree/main/code/13_quadruped)
も同じものです)。**以下のコマンドは、`docs/os-on-arduino/code` にいる前提で
書いています。** 学習済み方策は `include/arduino_quad_policy.h` に入っていて、
**上のブラウザのシミュレーションと同じ方策**です。
機体ごとに合わせる値は 3 つあります。**サーボの ID** は
{ref}`組み立ての表 ` の通りに振り、**回る向き(sign)** は脚の組み方で
決まるので、本書の通りに組めば `include/quad_calib.h` に入っている値のままです。
残る **ゼロ点**は、脚をまっすぐにした姿勢でサーボの EEPROM(電源を切っても
消えない記憶領域)に書きます。
EEPROM に書くので、ファームウェアの値を書き換える必要はありません。
```{figure} ../_static/quadruped_walk.gif
:name: fig-walk-real
:width: 80%
本書の実機(自作 OS で 8 個のサーボを制御)が歩くところ(1.5 倍速)
```
### 1. 配線する
胴体の中に、ドライバーボードと Arduino UNO R4 WiFi を並べて置きます。8 個の
サーボはドライバーボードの 3 ピンコネクタ(D V G)へ数珠つなぎにします。
::::{grid} 1 2 2 2
:gutter: 2
:::{grid-item}
```{figure} ../_static/quadruped_wiring_overview.jpg
:target: ../_static/quadruped_wiring_overview.jpg
:alt: 胴体を上から見たところ。ID ラベルを貼ったサーボ 8 個、ドライバーボード、Arduino
胴体の中。ラベルの ID が {ref}`組み立ての表 ` の位置と合っていること
```
:::
:::{grid-item}
```{figure} ../_static/quadruped_cable_routing.jpg
:target: ../_static/quadruped_cable_routing.jpg
:alt: 脚のブラケットの爪にサーボのケーブルを通したところ
膝のサーボのケーブルは、脚のブラケットの爪に通して胴体へ引き込む
```
:::
::::
ドライバーボードの UART(シリアル通信)ヘッダと Arduino を 3 本でつなぎます。**TX 同士・RX 同士**
をつなぐのがこのボードの約束です({doc}`bom` の「配線で先に知っておくこと」)。
| ドライバーボード | Arduino |
|---|---|
| TX | D1(TX→1) |
| RX | D0(RX←0) |
| GND | GND |
::::{grid} 1 2 2 2
:gutter: 2
:::{grid-item}
```{figure} ../_static/quadruped_uart_wiring.jpg
:target: ../_static/quadruped_uart_wiring.jpg
:alt: ドライバーボードの UART ヘッダ(TX RX GND)から Arduino へ 3 本の線をつないだところ。ジャンパは A
ボード側。TX(青)・RX(緑)・GND(白)。この写真のジャンパは **A**(Arduino から動かす位置)
```
:::
:::{grid-item}
```{figure} ../_static/quadruped_uart_pins.jpg
:target: ../_static/quadruped_uart_pins.jpg
:alt: Arduino の D1 と D0 に青と緑の線、GND に白い線を差したところ
Arduino 側。青が D1(TX→1)、緑が D0(RX←0)、白が GND
```
:::
::::
```{figure} ../_static/quadruped_wiring_done.jpg
:target: ../_static/quadruped_wiring_done.jpg
:width: 50%
:alt: 配線を終えた胴体を上から見たところ。ボードに DC ジャックと USB Type-C のケーブルがつながっている
配線を終えたところ。ボードには電源(DC ジャック)と、PC から動かすときの
USB Type-C をつなぐ
```
### 2. PC からゼロ点を合わせる
ドライバーボードのジャンパを **B(USB-SERVO)** にして、ボードの USB Type-C を
PC につなぎます。Arduino は使わず、PC から直接サーボを動かします。
```bash
cd 13_quadruped/host
uv sync # numpy と feetech-cli が入る
uv run python quad_host.py scan # 8 個応答するか
uv run python quad_host.py zero # 脚をまっすぐにした姿勢をゼロ点にする
```
`zero` はトルクを切ってから、**4 本とも股から足先までまっすぐ真下に伸ばした姿勢**を
待ちます(**機体を手で支えるか吊るして**行う)。CAD はこの姿勢が 0 度になるように
描かれています。Enter を押すと 8 個のサーボの EEPROM にゼロ点を書き、8 個とも
2048 と読めることを確かめます。電源を切っても残ります。
::::{grid} 2
:gutter: 2
:::{grid-item}
```{figure} ../_static/quadruped_zero_side.jpg
:target: ../_static/quadruped_zero_side.jpg
:alt: 脚をまっすぐ真下に伸ばした姿勢を横から見たところ
ゼロ点の姿勢(横から)
```
:::
:::{grid-item}
```{figure} ../_static/quadruped_zero_front.jpg
:target: ../_static/quadruped_zero_front.jpg
:alt: 脚をまっすぐ真下に伸ばした姿勢を正面から見たところ
同じ姿勢を正面から。大腿と下腿が一直線
```
:::
::::
ゼロ点が入ったら、そのまま PC から立たせて歩かせられます。
```bash
uv run python quad_host.py stand # home 姿勢で保持。Ctrl-C で脱力
uv run python quad_host.py teleop # キーボードで歩かせる(PC が方策を回す)
```
`stand` は**まず吊るして**、4 本とも同じ格好で膝が後ろに引けていることを
確かめてください。1 本だけ鏡像になるなら、その脚の ID か組み方が違います。
`teleop` のキー操作は次の通りです。`w` を押すたびに前進速度が 0.02 m/s ずつ上がり、
space で止まります。
| キー | 動作 |
|---|---|
| `w` / `s` | 前進速度を ±0.02 m/s |
| `a` / `d` | 旋回速度を ±0.05 rad/s |
| space | 止めて home 姿勢を保持 |
| `0` | 全関節を 0 度(脚をまっすぐ)。吊るして使う |
| `x` / Ctrl-C | 止めて脱力し、終了 |
```{raw} html
PC が方策を回し、USB でサーボを直接動かして歩かせたところ(等倍)
```
### 3. Arduino に書き込んで歩かせる
ジャンパを **A(UART-SERVO)** に戻し、Arduino を PC に USB でつないで書き込みます。
```bash
cd ../.. # 2 で host に入ったので code に戻る
uv run pio run -d 13_quadruped -t upload
```
書き込みが終わると Arduino が起動し、2 秒かけて home 姿勢に立ち上がります。
ここからは**方策を Arduino が回し**、PC は速度の指令を送るだけです。キーボードで
操作するなら:
```bash
cd 13_quadruped/host
uv run python quad_host.py serial # Arduino の USB シリアルへ速度を送る
```
キー操作は `teleop` と同じです。違いは 2 つあります。`x` で終えても脱力せず、
home 姿勢で立ったまま止まります。そして指令が 0.3 秒途切れると、Arduino が自分で
止まって home 姿勢を保持します(PC 側が固まったり、ケーブルが抜けたりしたとき)。
```{raw} html
Arduino UNO R4 WiFi が方策を回して歩かせたところ。PC はキーボードの速度指令を送るだけ(等倍)
```
シリアルモニタから直接コマンドを打つこともできます
(`docs/os-on-arduino/code` で `uv run pio device monitor -b 115200`)。
| コマンド | 動作 |
|---|---|
| `iktest` | 順運動学と逆運動学(FK/IK)の往復テスト。脚は動かない |
| `stand` / `stop` | home 姿勢へ 2 秒で移って保持 |
| `trot` | 本文 13.5 の sin/cos 歩行(学習なしの歩き方)。吊るして試す |
| `rl ` | 学習済み方策で歩く。例 `rl 0.15 0`(前進 m/s・旋回 rad/s) |
| `zero` | 全関節を 0 度へ移して保持。吊るして使う |
| `free` | トルクを切る |
歩いているあいだ、毎秒こういう行が出ます(本書の実機で `rl 0.15 0` のとき):
```
loop avg 5770 us max 5797 us late 0/51 skipped 0 read_fail 0
```
`loop avg` は 1 周の処理時間で、制御周期 20 ms(50 Hz)に収まっていれば十分です。
`skipped` は制御周期を落とした回数、`read_fail` はサーボが応答しなかった回数で、
**どちらも 0 のまま**が正常です。増えるなら配線か電源を疑ってください。
```{warning}
- サーボ 8 個の電源は **12 V の AC アダプタかバッテリーから**。PC の USB からは
絶対に取らない(1 個あたりストール 2.7 A)
- `stand` と最初の歩行は**まず機体を吊るして**から。ID や組み方を間違えると、
下手に歩くのではなく暴れます
```
```{tip}
足裏にゴムかシリコンを貼ると滑りにくくなります。素の樹脂のままでも歩けますが、
フローリングのような硬くて滑らかな床では足が滑りやすくなります。
```
## FAQ
**転倒から起き上がれる?** — できません。IMU が無く自分の姿勢を観測できない
ためです(学習しても成功率 1%)。IMU を載せる価値が最も高いのはここです。
IMU を載せて押しに踏ん張る版は {doc}`imu` にありますが、起き上がりはまだ学習していません。
**もっと速くならない?** — 45 rpm のサーボに 0.19 m の脚という構成では
0.05〜0.2 m/s が素の実力です(シミュレーションでは最大の指令で 0.16 m/s)。律速はトルクではなく無負荷回転数なので、
速くしたければサーボか脚長の変更になります。