
SO-ARM101 に LeRobot の ACT で模倣学習させてみた
はじめに
こんにちは、クラスメソッド製造ビジネステクノロジー部の森茂です。
前回の記事では、SO-ARM101 Pro キットを組み立てて LeRobot のテレオペレーションで動かしました。リーダーアームを手で操作するとフォロワーがリアルタイムで追従する、あの体験はなかなかのものでした。
今回は第三弾として、テレオペで集めたデータを使って ACT(Action Chunking with Transformers)で模倣学習を行い、ロボットアームに自律的に動いてもらいます。学習は DGX Spark の GPU で、評価は Mac と DGX Spark の両方で実施しました。この「どこで推論するか」が成功率に直結するという、予想外の結果になりました。
今回のゴール
4 ステップで進めます。
- カメラ接続と配置の調整
- テレオペレーションで 50 エピソードのデータ収集
- DGX Spark で ACT ポリシーの学習
- 学習済みモデルで自律動作の評価

前提環境
データ収集は Mac、学習と評価は DGX Spark と、2 台で役割分担しています。
| 項目 | データ収集(Mac) | 評価・学習(DGX Spark) |
|---|---|---|
| マシン | MacBook Pro(M4) | NVIDIA DGX Spark |
| OS | macOS Tahoe 26.3 | Ubuntu 24.04 |
| Python | 3.12 | 3.12 |
| LeRobot | v0.4.4 | v0.4.4 |
| PyTorch | 2.10.0(MPS) | 2.10.0+cu130(CUDA) |
| GPU | Apple M4(MPS) | NVIDIA GB10(128GB UMA) |
ACT とは
ACT(Action Chunking with Transformers)は、模倣学習のためのポリシーアーキテクチャです。テレオペで収集したデモデータから、カメラ画像と関節角度を入力にとり、次のアクション列(action chunk)を予測します。
第一弾で取り組んだ強化学習(RL)では報酬関数を設計する必要がありましたが、模倣学習(IL)ではお手本の動作を見せるだけです。「報酬をどう定義するか」で悩む代わりに、「良いデモデータをどう集めるか」が勝負どころになります。
LeRobot では ACT が SO-ARM101 の第一推奨ポリシーとして位置づけられています。パラメータ数は約 52M で、2 台のカメラ入力に対応しています。
タスク設計
今回のタスクは「スポンジ製のカラーキューブを掴んでケースに入れる」というシンプルな PickPlace です。

赤いキューブ(2.5cm x 3cm)を 1 つ使い、同じ動作を繰り返します。タスクをこれに決めた理由はいくつかあります。SO-ARM101 のグリッパーで掴みやすいサイズであること、成功と失敗の判定が明確なこと(ケースに入ったか入らないか)、そして第一弾の RL 記事で挑戦した PickOrange(シミュレーション)と自然に対比できることです。50 回のデモ収集が苦にならない程度の複雑さ、というのも地味に大事でした。
Step 1: カメラ接続と配置
カメラは 2 台構成で、作業エリア全体を映す front に Logitech C920、グリッパー付近を映す wrist に小型の InnoMaker U20CAM-1080P を割り当てました。
まずはカメラの認識を確認します。
uv run lerobot-find-cameras opencv
macOS ではカメラのインデックスが接続順やリブートで変わることがあるので、念の為毎回確認するのが確実です。自分の環境では front がインデックス 1、wrist がインデックス 0 でした。
カメラ付きのテレオペで画角と配置を調整します。
uv run lerobot-teleoperate \
--robot.type=so101_follower --robot.port=/dev/tty.usbmodemXXXX --robot.id=my_awesome_follower_arm \
--robot.cameras='{"front": {"type": "opencv", "index_or_path": 1, "width": 640, "height": 480, "fps": 30}, "wrist": {"type": "opencv", "index_or_path": 0, "width": 640, "height": 480, "fps": 30}}' \
--teleop.type=so101_leader --teleop.port=/dev/tty.usbmodemYYYY --teleop.id=my_awesome_leader_arm \
--display_data=true
--display_data=true にすると rerun でカメラ映像をリアルタイム表示できます。ただし、後述のとおり描画負荷で FPS が下がるため、このステップでの画角確認用と割り切って使うのがよさそうです。
確認したいのは、front カメラにキューブの初期位置とケースの両方が映っていること、wrist カメラにグリッパー付近が映っていることの 2 点です。

Step 2: 50 エピソードのデータ収集
画角が決まったら、本番のデータ収集に入ります。
uv run lerobot-record \
--robot.type=so101_follower --robot.port=/dev/tty.usbmodemXXXX --robot.id=my_awesome_follower_arm \
--robot.cameras='{"front": {"type": "opencv", "index_or_path": 1, "width": 640, "height": 480, "fps": 30}, "wrist": {"type": "opencv", "index_or_path": 0, "width": 640, "height": 480, "fps": 30}}' \
--teleop.type=so101_leader --teleop.port=/dev/tty.usbmodemYYYY --teleop.id=my_awesome_leader_arm \
--display_data=false \
--dataset.repo_id=${HF_USER}/so101_pickplace \
--dataset.num_episodes=50 \
--dataset.single_task="Pick up the red cube and place it into the case"
キーボード操作は → でエピソード終了、← でやり直し、ESC で全体終了です。
データ収集のコツ
50 エピソードを約 45 分かけて収集しました。データサイズは 840MB です。
キューブの初期位置は毎回少しずつ変えています。同じ場所に置き続けると、その位置でしか動作しないポリシーになってしまうためです。一方、ケースの位置は固定しました。変数を減らして学習を安定させる狙いです。
動作の速度と軌道はなるべく揃えるようにしました。テレオペはリーダーアームを手で操作するので、どうしてもブレが出ます。「上からまっすぐ掴んで、まっすぐケースに運ぶ」というパターンを意識しながら、お手本の一貫性を保つようにしました。
Step 3: DGX Spark で ACT 学習
DGX Spark への環境構築
DGX Spark 側に LeRobot の学習環境を構築します。ここで一つハマりポイントがありました。
普通に uv add lerobot すると PyTorch の CPU 版がインストールされます。PyPI の CPU 版が優先されてしまうためです。CUDA 版を確実に入れるには、pyproject.toml で PyTorch のインデックスを明示指定しました。
[[tool.uv.index]]
name = "pytorch-cu130"
url = "https://download.pytorch.org/whl/cu130"
explicit = true
[tool.uv.sources]
torch = { index = "pytorch-cu130" }
torchvision = { index = "pytorch-cu130" }
この設定で PyTorch 2.10.0+cu130 が入り、CUDA が正しく認識されるようになりました。
データの転送
Mac で収集したデータを DGX Spark に rsync で転送します。
rsync -av \
~/.cache/huggingface/lerobot/${HF_USER}/so101_pickplace \
ciel:~/.cache/huggingface/lerobot/${HF_USER}/
学習の実行
uv run lerobot-train \
--dataset.repo_id=${HF_USER}/so101_pickplace \
--policy.type=act \
--output_dir=outputs/train/act_so101_pickplace \
--policy.device=cuda \
--policy.push_to_hub=false \
--wandb.enable=false
学習の実測データ
| 項目 | 値 |
|---|---|
| パラメータ数 | 52M |
| バッチサイズ | 8 |
| ステップ数 | 100,000 |
| 学習速度 | 約 3.9 step/s |
| 所要時間 | 約 7 時間 |
| データ | 50 エピソード / 40,075 フレーム |
| データサイズ | 840MB |
DGX Spark の 128GB 統合メモリのおかげか、メモリ周りで困ることはありませんでした。学習中は GPU メモリを約 18GB 使用していました。寝る前に回しておいて翌朝確認する、という手軽さがローカル GPU のいいところです。
Step 4: 評価
学習済みモデルを Mac に転送して、実機で評価します。
# チェックポイントを Mac に転送
rsync -av \
ciel:~/works/robotics/lerobot-train/outputs/train/act_so101_pickplace/checkpoints/last/pretrained_model \
./act_model/
ポートとカメラのインデックスは接続のたびに変わる可能性があるので、評価の前に確認しておきます。
uv run lerobot-find-port
uv run lerobot-find-cameras opencv
Mac MPS での評価
uv run lerobot-record \
--robot.type=so101_follower --robot.port=/dev/tty.usbmodem5B420765431 --robot.id=my_awesome_follower_arm \
--robot.cameras='{"front": {"type": "opencv", "index_or_path": 1, "width": 640, "height": 480, "fps": 30}, "wrist": {"type": "opencv", "index_or_path": 0, "width": 640, "height": 480, "fps": 30}}' \
--dataset.repo_id=${HF_USER}/eval_act_so101_pickplace \
--dataset.num_episodes=10 \
--dataset.push_to_hub=false \
--dataset.single_task="Pick up the red cube and place it into the case" \
--display_data=false \
--policy.path=./act_model/pretrained_model
評価結果(Mac MPS)
10 エピソードを手動で実行しました。
| 結果 | 件数 | 割合 |
|---|---|---|
| 成功(ピック&プレイス完了) | 4 | 40% |
| 掴み損ね(キューブ認識はOK) | 1 | 10% |
| 失敗(キューブ未到達・挙動不審) | 5 | 50% |
成功率 40% という数字だけ見ると微妙に思えますが、成功したエピソードの動きはかなりスムーズでした。赤いキューブを視覚的に捉えてからの到達動作はきれいに学習できています。問題は初動のガクガクした動きで、これが FPS のミスマッチに起因していそうだというのが検証を通じて見えてきました。
DGX Spark CUDA での評価
同じモデルを DGX Spark に接続したアームで評価すると、成功率がほぼ 100% まで上がりました。Mac で見ていたガクガクした初動がなくなり、滑らかにキューブへ到達してピックアップする様子は、正直「同じモデルなのか?」と疑うほどでした。
DGX Spark 側のセットアップ
カメラは Logitech C920(front、/dev/video2)と InnoMaker U20CAM(wrist、/dev/video0)を使い、アームは /dev/ttyACM0 に直接接続しています。ユーザーを video、dialout、input グループに追加し、feetech-servo-sdk(scservo_sdk)もインストールしました。
一つ注意点として、DGX Spark の Wayland 環境では pynput が動かないため、キーボードによるエピソード送り(→ ← ESC)も reset_time_s の自動進行も使えませんでした。1 エピソードずつコマンドを手動で実行・停止する地味な作業でしたが、結果を見た瞬間にそんな手間は吹き飛びました。
推論 FPS と成功率の関係
| デバイス | 推論 FPS | 学習時 FPS との比 | 成功率 |
|---|---|---|---|
| Mac MPS | 約 15 Hz | 50% | 40%(4/10) |
| Mac CPU | 約 8 Hz | 27% | 未計測 |
| DGX Spark CUDA | 約 30 Hz | 100% | 90%(9/10) |
DGX Spark での唯一の失敗はキューブの掴み損ねで、FPS 起因の問題ではありませんでした。
データ収集時が 30Hz なので、ポリシーも 30Hz の時間感覚でアクションを出力します。推論が半分の 15Hz しか出ないと、1 ステップあたりの時間間隔がずれてアームの動きがぎこちなくなります。Mac MPS では ACT の推論と 2 台のカメラキャプチャ(640x480 x 30fps x 2)の同時処理がボトルネックで、15Hz が上限でした。
「学習と同じ速度で動かす」がこれほど成功率に直結するとは、正直やってみるまで実感がありませんでした。
ハマりポイントまとめ
検証中に遭遇したトラブルと対処法をまとめました。
| 問題 | 原因 | 対処 |
|---|---|---|
display_data=true で FPS 9.8Hz に低下 |
rerun の描画負荷(2 カメラ同時表示) | データ収集時は false に |
HF_USER 未設定で OSError |
repo_id が /so101_pickplace になりパス不正 |
export HF_USER=himorishige で設定 |
--policy.path vs --policy.type |
type + pretrained_path では前後処理が初期化されない | --policy.path で完全なパイプラインを構築 |
| Mac MPS で推論 15Hz が上限 | ACT 推論 + 2 カメラキャプチャの同時処理負荷 | DGX Spark(CUDA)での推論に切り替え |
| PyTorch CPU 版が uv で入る | PyPI の CPU 版が優先される | pyproject.toml で CUDA index を explicit 指定 |
| rsync でディレクトリ構造がネスト | pretrained_model/ がサブディレクトリに入る |
--policy.path=./act_model/pretrained_model |
| DGX Spark Wayland で pynput 非対応 | キーボード操作不可 | 1 エピソードずつ手動で実行 |
RL と IL、どちらが「楽」だったか
第一弾の Isaac Sim での強化学習(RL)と、今回の模倣学習(IL)を体験して、両方のアプローチを比較できる立場になりました。
RL で一番苦労したのは報酬関数の設計です。「キューブに近づいたら +1」「掴んだら +10」といった報酬を組み合わせるわけですが、重みのバランスが崩れると学習が進まなかったり、意図しない裏技を見つけたりします。一方で、環境さえ整えてしまえばデータ収集は自動です。シミュレーション内でエージェントが勝手に試行錯誤してくれます。
IL ではその逆でした。報酬関数を考える必要はありませんが、お手本のデータ収集が人力です。50 エピソードの収集に 45 分かかりました。しかもデモの品質がそのまま学習結果に反映されるので、雑な操作をすると雑な動作を学習してしまいます。「お手本の一貫性を保つ」というのが、実は報酬設計とは別種の難しさだなと感じました。
個人的な感想としては、今回のような単純なタスクでは IL の方が手軽でした。報酬関数の設計は試行錯誤の繰り返しで、特にシミュレーション環境の構築も含めると立ち上がりに時間がかかります。IL はテレオペでお手本を見せるだけなので、「動くものを見る」までの距離が短いです。ただ、タスクが複雑になったり、バリエーションが増えたりすると話は変わりそうです。そうなると RL のようにデータ収集を自動化できるアプローチの方が強いかなと思います。
まとめ
SO-ARM101 の模倣学習を、データ収集から学習、評価まで一通り進めました。50 エピソードのデモデータを Mac でテレオペ収集し、DGX Spark で ACT を 7 時間学習させ、実機で評価するという流れです。
検証を通じて得られた一番の知見は、推論 FPS と学習時のフレームレートの一致が成功率に直結するということでした。Mac MPS(15Hz)では 40% だった成功率が、DGX Spark CUDA(30Hz)ではほぼ 100% まで向上しました。ACT のようなアクションチャンクベースのポリシーでは、時間軸の一貫性がそのまま動作品質に効いてきます。
今回のシリーズを通じて、シミュレーションでの強化学習、実機の組み立てとテレオペ、そして模倣学習と、ロボット学習の基本的な流れを一通り体験できました。次回は Foundation Model(VLA)を使った GR00T N1.6 のファインチューニングに挑戦予定です。今回のデモデータがそのまま活用できるか、試してみたいですね。







