- Python 3.12をインストール
- USBポートを見つける
- モーターのセットアップ
- キャリブレーション
- 遠隔操作
- カメラの設定
- カメラ表示を準備する
- カメラで遠隔操作する
- データセットを記録する
- データセットのアップロード
- データセットの可視化
- エピソードを再生する
- ポリシーを訓練する
- Google Colabを使った学習
- 推論の実行とポリシーの評価
- 学習パラメータを変更する
- Weights & Biases(W&B)機械学習の実験管理サービス
- なぜ2カメラに移行したのか
- 既存データは使えない
- ハードウェア選定
- USBハブ検証:アームは60Hzで動いた
- 最終的な接続構成
- カメラの認識確認
- 2カメラ構成のコマンド
- 学んだこと・注意点まとめ
- 学習精度が上がらない原因を分析する
- エラーThe fields pretrained_revision are not valid for ACTConfig
- エラー:Record loop is running slower (8.1 Hz)
- Google Colabを卒業してAWS EC2でロボット学習
- なぜAWSなのか
- AWSのAI系サービスを検討する
- Colab vs EC2
- EC2で学習する全手順 ― 手動オペレーションからTerraformへ
- 「stop」と「terminate」を覚えておく時代の終わり
Python 3.12をインストール
手元の環境にはすでに conda, miniforge3 が設定されているので、環境を用意します。
https://huggingface.co/docs/lerobot/installation
を参考に、環境を構築していきます。
# 作業フォルダ準備
cd
mkdir -p development/lerobot-workspace
cd development/lerobot-workspace
# Python環境
conda create -y -n lerobot python=3.12
conda activate lerobot
# ffmpeg導入 7.1.1に固定する。
conda install ffmpeg=7.1.1 -c conda-forge
# LeRobotのインストール
git clone https://github.com/huggingface/lerobot.git
cd lerobot
# ライブラリのインストール
pip install -e .
pip install lerobot
# Feetech motor support
pip install 'lerobot[feetech]' ZshUSBポートを見つける
各アームに関連付けられているUSBポートを見つけます。
lerobot-find-portZsh上記を実行し、Remove the USB cable from your MotorsBus and press Enter when done.と表示されたら、フォロワーアーム用のUSBケーブルを抜いてEnterを押すと、フォロワーアームのUSBポート名が表示されますので控えておきます。
USBケーブルを元に戻して終了です。
続いて、リーダーアームも同じようにUSBポート名を確認します。
後で使いやすいように環境変数に登録しておきます。
export FOLLOWER_ARM='/dev/tty.usbmodem5A7A0186191'
export READER_ARM='/dev/tty.usbmodem5A4B0479511'
Zshモーターのセットアップ

モーターの初期化を実行します。
lerobot-setup-motors \
--robot.type=so101_follower \
--robot.port=$FOLLOWER_ARM
Zsh同様にして、リーダーアームも設定します。
lerobot-setup-motors \
--teleop.type=so101_leader \
--teleop.port=$READER_ARMZshキャリブレーション

robot.idはロボットの名前です。今回は「right_follower_arm」としました。
lerobot-calibrate --robot.type=so101_follower --robot.port=$FOLLOWER_ARM --robot.id=right_follower_armZshまずロボットを可動範囲の中央ポジションにし、Enterを押して、ロボットアームを可動域全体にわたって動かします。
同様の手順で、リーダーアームの校正を行います。
lerobot-calibrate --teleop.type=so101_leader --teleop.port=$READER_ARM --teleop.id=right_leader_armZshキャリブレーションデータは、.cacheフォルダ内に格納されています。
~/.cache/huggingface/lerobot/calibration/robots/so_follower/right_follower_arm.json
~/.cache/huggingface/lerobot/calibration/teleoperators/so_leader/right_leader_arm.json
遠隔操作
キャリブレーションが終わったら、テレオペレーションを試してみましょう。次のコマンドを実行して、リーダーアームを操作すると、フォロワーアームが同様の動きをすることを確認します。もし動きに違いがある場合は、再度キャリブレーションを実施してください。
lerobot-teleoperate \
--robot.type=so101_follower \
--robot.port=$FOLLOWER_ARM \
--robot.id=right_follower_arm \
--teleop.type=so101_leader \
--teleop.port=$READER_ARM \
--teleop.id=right_leader_armZsh
なかなか通信が安定せず、学習中にエラーが発生してしまうことが多発しましたので、しっかりしたUSBハブを購入することにしました。
JP-RSW-H7C4 ROSONWAY RSH-H7C4 おすすめポイント
スペック
- USB 3.2 Gen2(10Gbps)全ポート対応
- 7ポート(USB-C×4、USB-A×3)
- セルフパワー(ACアダプタ DC12V/3A、36W)
- 個別電源ボタン+LEDインジケータ付き
- ケーブル着脱式(約100cm)
SO-101の学習用途に合う理由
1. 全ポート10Gbps
例えばRealSense D435(5Gbps必要)のような多くの帯域を使うカメラを将来追加しても余裕。D435はUSB-Cコネクタなので、USB-Cポート(×4)に変換アダプタなしで直挿しできます。
2. セルフパワー36W
バスパワーではなくACアダプタで自己給電するので、カメラ複数台を同時接続しても電力不足による切断が起きにくい。過去のサーボ通信不安定の原因の一つが電力不足だったことを考えると、ここは重要です。
3. 個別スイッチ+LED
使うカメラだけONにできるので、カメラインデックスが安定します。以前苦労したカメラindex問題(0と1が入れ替わる)の対策になります。
では実際にこのUSBハブを使ってみましょう。
結果は60Hzで滑らかに動作しました!手持ちの古いAnkerハブではとても不安定で、時折8Hzに落ち込み、記録が途中で途切れてしまっていました。
Ankerハブ(USB2.0・STT)→ 8Hz・不安定
RSW-H7C4(VL822・MTT) → 60Hz・滑らか!
素晴らしい!これで問題解決です。おそらく搭載されているUSBコントローラーの能力だと思われます。調べてみた所、VL822のMTT(Multiple Transaction Translators)が効いているようです。 各ポートに独立した帯域を割り当てるので、サーボ通信のタイミングが乱れない。安定しています。

カメラの設定
次はカメラを設定しましょう。C920nを選択しました。なぜ C920n を選んだのか。ロボットアームにカメラを選ぶとき、最初に考えるべきことは「性能」ではありません。再現性です。HuggingFace上に公開されている1万6千を超えるLeRobotデータセット、その7割以上が640×480 / 30fpsで収録されています。高解像度は学習にとってノイズになり得ます。再現性を落とさないために、スタンダードに合わせておくという選択をしました。
C920系はLeRobotコミュニティでの実績がもっとも豊富なWebカメラシリーズだそうです。macOSのAVFoundationバックエンドにも対応しており、M1 Macでドライバーなしに即動作するのも便利です。USB 2.0接続なので、現在の構成(アーム×2、カメラ×1)では既存のアダプターだけで事足ります。
C920nはその中でもAmazon.co.jp限定モデル。C920sとの違いはプライバシーシャッターの有無だけ。センサーもレンズも同一ですし、不要な付属品を省いた分だけ安いんです。ロボット学習に三脚は要らないのでこれにしました。
選択肢 A
C920s
プライバシーシャッター付き。本体性能はC920nと完全同一。
選択肢 C
C922n
三脚付属、720p時60fps対応。マウント自作なら不要な付属品。
ただ、公式オーバーヘッドマウントのSTLがXPCAM HD 1080P専用設計だったことは誤算でした。BlenderでC920nの底面寸法に合わせたtopパーツを設計し、その日のうちに印刷・取り付けを完了しました。
オーバーヘッドカメラマウント ウェブカメラ C920n 3Dデータ https://makerworld.com/ja/models/2895012-overhead-cam-mount-webcam-c920n#profileId-3235267
カメラを検出するには、lerobot-find-cameras opencvコマンドを実行します。
$ lerobot-find-cameras opencv
OpenCV: out device of bound (0-2): 3
OpenCV: camera failed to properly initialize!
[ WARN:0@4.087] global cap_ffmpeg_impl.hpp:1217 open VIDEOIO/FFMPEG: Failed list devices for backend avfoundation
--- Detected Cameras ---
Camera #0:
Name: OpenCV Camera @ 0
Type: OpenCV
Id: 0
Backend api: AVFOUNDATION
Default stream profile:
Format: 16.0
Fourcc:
Width: 1920
Height: 1080
Fps: 5.0
--------------------
Camera #1:
Name: OpenCV Camera @ 1
Type: OpenCV
Id: 1
Backend api: AVFOUNDATION
Default stream profile:
Format: 16.0
Fourcc:
Width: 1920
Height: 1080
Fps: 30.0
--------------------
Camera #2:
Name: OpenCV Camera @ 2
Type: OpenCV
Id: 2
Backend api: AVFOUNDATION
Default stream profile:
Format: 16.0
Fourcc:
Width: 1280
Height: 720
Fps: 30.0
--------------------
Camera #0 として検出されたようです。
カメラ表示を準備する
rerun-sdk は、ロボットのセンサーデータやカメラ映像、関節角度などをリアルタイムで可視化するツールです。--display_data=true を指定したときに使われます。
テレオペレーション中にこういった情報をGUIで確認できます:
- カメラ映像
- 各サーボの現在角度
- ループ速度
次のコマンドでインストールします。
pip install 'lerobot[viz]'Zshカメラで遠隔操作する
次のコマンドで遠隔操作します。カメラ0を指定し、640×480 @ 30fps に設定しています。これLeRobotのデータ収集でも標準的な設定です。
lerobot-teleoperate \
--robot.type=so101_follower \
--robot.port=$FOLLOWER_ARM \
--robot.id=right_follower_arm \
--robot.cameras="{ front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}}" \
--teleop.type=so101_leader \
--teleop.port=$READER_ARM \
--teleop.id=right_leader_arm \
--display_data=trueZsh訓練データを収集する際には、対象物がカメラに写っている必要があります。アームで対象物が隠れたりすると、精度が落ちてしまうため、カメラを見ながら操作する事にも慣れておきましょう。
データセットを記録する
HaggingFaceハブの機能を利用してデータセットをアップロードします。まずは書き込みアクセストークンを取得します。
- HaggingFaceアカウントを作成します。
- 右上のメニューから、「Access Tokens」を選択します。
- 「Create New Token」ボタンをクリック。
Token typeでWriteを選択、Token nameに任意の名前を入力し「Create token」ボタンをクリック。- トークンが表示されます。このトークンは二度と表示されないので、確実に控えておきます。pCloud Passなどのパスワード管理ソフトに保存しておくのがおすすめです。

トークンをCLIに追加してください。
export HF_WRITE_TOKEN=<控えておいたトークン>
hf auth login --token $HF_WRITE_TOKEN --add-to-git-credentialZshこのコマンドは~/.cache/huggingface/token にトークンを保存します。これにより huggingface_hub ライブラリがAPIを叩くときに自動認証されます。LeRobotがデータセットをHuggingFace Hubにアップロードするときに使います。
次に、Hugging Faceリポジトリ名を変数に格納します。
HF_USER=$(hf auth whoami --format quiet)
echo $HF_USERZshこれでデータセットを記録できます。
HuggingFaceの datasets ライブラリをインストールします。次のコマンドを実行してください。
pip install 'lerobot[dataset]'Zshdatasets ライブラリとは
HuggingFaceが開発したデータセット管理ライブラリです。LeRobotはロボットの録画データをこの形式で保存・管理します。次の機能を提供します:
- エピソードデータ(関節角度、カメラ画像)をParquet形式で保存
- HuggingFace Hubへのアップロード/ダウンロード
- データセットのバージョン管理
必要なモジュールをインストールします。
pip install pynputZsh5つのエピソードを記録してデータセットをHuggingFaceハブにアップロードするには、次のコマンドを実行します。
lerobot-record \
--robot.type=so101_follower \
--robot.port=$FOLLOWER_ARM \
--robot.id=right_follower_arm \
--robot.cameras="{ front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}}" \
--teleop.type=so101_leader \
--teleop.port=$READER_ARM \
--teleop.id=right_leader_arm \
--display_data=true \
--dataset.repo_id=${HF_USER}/record-test \
--dataset.num_episodes=5 \
--dataset.single_task="Grab the yellow lego block" \
--dataset.streaming_encoding=true \
--dataset.encoder_threads=2ZshデータのリポジトリIDを決めます。ここではrecord-testとしています。
このコマンドを実行すると音声案内が始まります。
- Recording episode zero → レゴブロックを掴んでケースに入れる操作をします。
- 操作終了の合図として、キーボードの右カーソルキー(→)を押します。
- Reset the environment → レゴブロックを元の位置に戻します。
- レゴブロック配置完了の合図として、キーボードの右カーソルキー(→)を押します。
- Recording episode one → レゴブロックを掴んでケースに入れる操作をします。
- 操作終了の合図として、キーボードの右カーソルキー(→)を押します。
- Reset the environment → レゴブロックを元の位置に戻します。
- レゴブロック配置完了の合図として、キーボードの右カーソルキー(→)を押します。
- もし現在のエピソードをキャンセルして最初から開始したいときは、左カーソル(
←)を押します。
このような手順でepisode zeroから、episode4までレゴブロックを掴んでケースに入れる操作を続けます。5回実施したところでStop recording、それから自動的にHaggingFaceへのアップロードが始まり、アップロード完了するとExitingの案内とともにプログラムが終了します。
データセットのアップロード
では作成されたデータを確認してみましょう。ローカルには~/.cache/huggingface/lerobot/${HF_USER}/内に、<リポジトリID>_yyyymmdd_hhMMddという名前で作成されます。
$ ls ~/.cache/huggingface/lerobot/${HF_USER}/record-test_20260606_114847
data meta videos
$ du -sh ~/.cache/huggingface/lerobot/${HF_USER}/record-test_20260606_114847
39M /Users/hiro/.cache/huggingface/lerobot/emboss369/record-test_20260606_114847Zshアップロードできているか確認しましょう。
echo https://huggingface.co/datasets/${HF_USER}/record-test_20260606_114847Zsh上記を実行し表示されたURLをブラウザで開きます。アップロードされたデータの詳細を確認することができます。
正常にアップロードされていか確認しましょう。次のような内容が記載されているはずです。
データセットの概要
- エピソード数:5
- 総フレーム数:4,034フレーム
- データサイズ:41.1MB(データ100MB + 動画200MB)
- FPS:30
- 動画解像度:640×480、AV1コーデック
- LeRobotバージョン:v3.0

データセットの可視化
visualize your dataset onlineを使うと、オンラインでデータを可視化することができます。

enter dataset id に、次のコマンドで得られるテキストを入力します。年月日日時は書き換えてください。
echo ${HF_USER}/so101_test_20260606_114847Zshアップロードされたデータのビデオ、3Dモデルによるリプレイなどを行うことができます。

エピソードを再生する
lerobot-replay コマンドを使うと、記録されたデータセットからエピソードを選んで、フォロワーアームで再現することができます。以下のコマンドを実行すると、フォロワーアームが自動的に動き出し、先ほど記録した動きと同様の動きを再現してくれます。
lerobot-replay \
--robot.type=so101_follower \
--robot.port=$FOLLOWER_ARM \
--robot.id=right_follower_arm \
--dataset.repo_id=${HF_USER}/record-test_20260606_114847 \
--dataset.episode=0Zshポリシーを訓練する
ではポリシーを訓練、つまり、ロボットに状況を見てどう動くかを決めるルール(戦略)を教えていく処理になります。ニューラルネットの学習を行います。
コマンドの基本構造
lerobot-train \
--dataset.repo_id=${HF_USER}/so101_test \ # 学習データ
--policy.type=act \ # アルゴリズム
--output_dir=outputs/train/act_so101_test \ # 保存先
--job_name=act_so101_test \ # ジョブ名
--policy.device=cuda \ # 学習デバイス
--wandb.enable=true \ # 可視化
--policy.repo_id=${HF_USER}/my_policy # モデルのアップロード先Zsh各引数の意味
--dataset.repo_idは学習に使うデータセットです。今回はemboss369/record-test_20260606_114847になります。--policy.type=actはACT(Action Chunking with Transformers)というアルゴリズムを指定しています。SO-ARM101の標準的な選択肢で、configuration_act.pyの設定が自動で読み込まれます。またデータセットに記録されたサーボ数やカメラ構成も自動で適応されます。--output_dirはチェックポイント(途中経過)の保存先です。学習は数時間かかるため、途中で止まっても--resume=trueで再開できます。--policy.device=cudaは学習をどのデバイスで行うかです。Google Colab(T4)を使う場合はcuda、M1 Macの場合はmpsです。--wandb.enable=trueはWeights & Biasesという学習進捗の可視化ツールです。lossの推移などをグラフで確認できます。使う場合は事前にwandb loginが必要です。任意なので不要なら省けます。--policy.repo_idは学習済みモデルのHuggingFace Hubへのアップロード先です。
ACT(Action Chunking with Transformers) はロボット模倣学習のアルゴリズムです。
一言で言うと
「人間のデモンストレーションを見て、同じ動作を再現できるようにロボットを訓練する手法」です。
名前の意味
Action Chunking(アクション・チャンキング) 1フレームずつ次の動作を予測するのではなく、複数フレーム分の動作をまとめて(chunk)予測します。例えば30フレーム先までの関節角度を一度に出力します。これにより動作がなめらかになります。
Transformers ChatGPTなどでも使われているTransformerアーキテクチャを使って、カメラ映像とアームの現在状態から次の動作を予測します。
Google Colabを使った学習
M1 Macには高性能なGPUが搭載されていません。このような場合はGoogle Colabを利用してACTトレーニングの手順で学習させることができます。
次に、ACTトレーニングの手順に従い、コマンドを実行します。
途中で、トークンを求められるので既に用意してあるWriteトークンを入力します。
Add token as git credential? [y/N]: にはNを入力します。
13:38 作業開始
13:43 lerobot-trainコマンドを開始。
14:50 61%まで進捗
学習自体は1時間49分で正常完了しました。
推論の実行とポリシーの評価
学習が完了したので、いよいよロボットに自律動作させてみます。使うコマンドはデータ収集時の lerobot-record とほぼ同じです。
lerobot-rollout \
--robot.type=so101_follower \
--robot.port=$FOLLOWER_ARM \
--robot.id=right_follower_arm \
--robot.cameras="{ front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}}" \
--policy.path=emboss369/act_policy_20260606 \
--display_data=trueZsh動作しました!ACTポリシーがロボットを自律動作させることができました。
しかしながら、成功率は0%。ロボットの動きは大変ぎこちないものです。
それもそのはず、他のAI同様、データセットは少なくとも50、理想的には200ぐらいないと安定動作しないのです。
では頑張って200データセット目指しましょう!といっても、50データセット録画するにも2時間くらいかかるので、4日間かけて取得します。1回で50データセットずつ取りましょう。幸い、LeRobotには既存データセットに追記する --resume オプションがあります。
1日目:新規作成(50エピソード)
cd ~/development/lerobot-workspace
conda activate lerobot
export FOLLOWER_ARM='/dev/tty.usbmodem5A7A0186191'
export READER_ARM='/dev/tty.usbmodem5A4B0479511'
export HF_USER=$(hf auth whoami --format quiet)
export DATASET_ID="so101_lego"
lerobot-record \
--robot.type=so101_follower \
--robot.port=$FOLLOWER_ARM \
--robot.id=right_follower_arm \
--robot.cameras="{ front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}}" \
--teleop.type=so101_leader \
--teleop.port=$READER_ARM \
--teleop.id=right_leader_arm \
--dataset.repo_id=${HF_USER}/${DATASET_ID} \
--dataset.single_task="Grab the yellow lego block" \
--dataset.num_episodes=50 \
--dataset.streaming_encoding=true \
--dataset.encoder_threads=2Zsh50回分の学習で、ブロックに近づくようになりましたが、まだうまくブロックを捕まえることができないようです。
2日目・3日目:追記(--resume=true)
1日目終了後にHuggingFace Hubで実際のデータセット名を確認してから、その名前をメモして、DATASET_IDに設定します。
DATASET_ID="so101_lego_20260616_175720"
lerobot-record \
--robot.type=so101_follower \
--robot.port=$FOLLOWER_ARM \
--robot.id=right_follower_arm \
--robot.cameras="{ front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}}" \
--teleop.type=so101_leader \
--teleop.port=$READER_ARM \
--teleop.id=right_leader_arm \
--dataset.repo_id=${HF_USER}/${DATASET_ID} \
--dataset.single_task="Grab the yellow lego block" \
--dataset.num_episodes=50 \
--dataset.streaming_encoding=true \
--dataset.encoder_threads=2 \
--resume=trueZshデータ数が100でも、まだまだ掴むことができませんでした。
4日目、ついにデータ数を200まで増やしました。しかし、相変わらずブロックをうまく掴めません。
学習パラメータを変更する
いままでは、サンプルの学習パラメータをそのまま使っていましたが、データ数に応じて学習ステップを増やすのがポイントです。データが増えたのに20,000ステップのままだと学習しきれません。データ量に応じて目安はこうです。
100エピソード → 50,000〜80,000ステップ
200エピソード → 100,000ステップ前後
前回まで20,000ステップだったのが、そもそも少なかった可能性があります。ここを増やすだけで「ブロック落としてしまう」が改善することは十分あり得ます。
もしくは、バッチサイズを上げます。バッチ32なら、25,000〜30,000ステップでも、バッチ8の10万ステップと同等のエポック数になります。学習時間も短縮できて一石二鳥です。
エポック数とは?
機械学習では「データセット全体を何周learnしたか」をエポックと呼びます。重要なのは総ステップ数そのものではなく、各データを何回見たかです。
関係式はこうです。
エポック数 = (ステップ数 × バッチサイズ) ÷ データの総フレーム数
今回のケースで計算してみる
実際のログにヒントがあります。前回の学習ログ(20,000ステップ)にこう出ていました。
step:20K smpl:160K ep:352 epch:7.04
epch:7.04 = 約7エポックです。smpl:160K(20,000ステップ × バッチ8 = 160,000サンプル)をデータ総数で割った値ですね。
200エピソードに増えると1エポックあたりのフレーム数が倍増するので、同じ20,000ステップだとエポック数が半分(約3.5)に落ちます。データが増えたらステップも増やさないと、学習が浅くなるわけです。
Weights & Biases(W&B)機械学習の実験管理サービス
--wandb.enable=true を付けて学習すると、lossの推移などがW&Bのウェブ画面にリアルタイムで記録され、グラフで見られるようになります。その際に「どのアカウントの記録か」を紐付けるために wandb login が必要です。
使い方
- https://wandb.ai でアカウント作成(無料)
- https://wandb.ai/authorize でAPIキーを取得
- ターミナルで:
bash
wandb login
# → APIキーの入力を求められるので貼り付け
一度ログインすれば認証情報が保存され、以降は不要です。
何が嬉しいか
「lossの下がり方を見てステップ数を決める」が、W&Bで可能になります。
- loss曲線がリアルタイムで見える
- まだ下がっている → ステップを増やす余地あり
- 下げ止まった → そのステップ数で十分
これがグラフで一目でわかるので、「200エピソードに何ステップが適正か」を経験則に頼らず判断できます。
Colabで使う場合
Colabのセルで先にログインしておきます。
import wandb
wandb.login()
# → APIキーを貼り付け
その後 --wandb.enable=true で学習すれば記録されます。
ちなみに必須ではありません。W&Bを使わない場合は今まで通り --wandb.enable=False でOKです。ただ、これから200エピソードで最適なステップ数を探るフェーズでは、loss曲線が見えると判断がかなり楽になるので、このタイミングで導入する価値はあります。
# === 設定 ===
HF_USER = "emboss369"
DATASET = "so101_lego_20260613_20260613_131100"
MODEL_NAME = "act_so101_lego_v6"
STEPS = 8000 # 底を打つあたりに設定
BATCH_SIZE = 32
SAVE_FREQ = 2000 # 2000ごとに保存
# ============
cmd = f"""lerobot-train \
--dataset.repo_id={HF_USER}/{DATASET} \
--policy.type=act \
--output_dir=outputs/train/{MODEL_NAME} \
--job_name={MODEL_NAME}_training_job \
--policy.device=cuda \
--wandb.enable=true \
--policy.repo_id={HF_USER}/{MODEL_NAME} \
--batch_size={BATCH_SIZE} \
--steps={STEPS} \
--save_freq={SAVE_FREQ}"""
print(cmd)
!{cmd}Zsh上記の通り変更して、再度学習を実行してみましょう。このパラメータだと、ColabのT4での学習時間は約2時間になります。ちなみに、このデータでは既に何度か実行し8000ステップあたりでtrain/lossが底を打つことがわかっているのでこのように設定しています。
学習を開始すると、Colabのログにリンクが出る
!{cmd} を実行すると、学習開始直後にこんな行が出力されます。
wandb: 🚀 View run at https://wandb.ai/<あなたのユーザー名>/lerobot/runs/xxxxx
この View run のリンクをクリックするとそのrun専用のページに直接飛べます。
2時間が経過した時点で、train/lossは底を打っています。

loss曲線では4,000ステップで底でした。なので30,000まで回す意味は薄かったです。
しかし、このデータでもうまくブロックを掴めません。なぜでしょう?データが悪いのかもしれません。リーダーアームを200回データ取得したことで、操作にも慣れてきました。データ品質に気を使いながら、同じ動作を50回ほど繰り返してみましょう。
ポイントは「一貫性を持つこと。毎回同じやり方でタスクを行う。左で掴んだり右で掴んだり、やり方がバラバラだとポリシーが混乱する。まず50デモを確実に動かしてから、カメラ視点を増やす」
という点です。できるだけ同じ操作をするように心がけました。
2カメラ構成への移行
なぜ2カメラに移行したのか
SO-ARM101に黄色いレゴブロックを自律把持させるプロジェクトを進めてきました。データ収集を重ねるうちに、こんな壁にぶつかったわけです。
- 5エピソード → 見当違いの方向に動くだけ(成功率0%)
- 50エピソード → 全く掴めない
- 100エピソード → ブロックに近づくようになった
- 200エピソード → 掴めるが、落とす
「掴めるが落とす」の壁を超えられない。ステップ数を増やしても、データを増やしても、lossは下がるのに成功率が頭打ちになってきた。
原因を考えると、単一の俯瞰カメラではグリッパーとブロックの奥行き・接触の瞬間が見えにくいことが根本的な限界だと予測。ACTポリシーが見えている情報は俯瞰映像だけです。
コミュニティの実例を調べてみると、成功率が高い構成はほぼ例外なく俯瞰カメラ+手首カメラのデュアル構成みたい。「横から撮ったカメラ」「手首視点のカメラ」と、各自で工夫している。単一俯瞰カメラで成功しているケースはないのかもしれない。これは構造的な限界だと判断して、2カメラ構成への移行を決めました。
既存データは使えない
重要な現実として、2カメラ構成に変えると、今まで集めた俯瞰1台の250エピソードはそのままでは使えない。 残念。LeRobotのACTは「観測の形状(カメラ数・名前)」がモデルに焼き込まれるため、カメラ構成が変わったらデータを最初から取り直す必要があります。
250エピソードの手戻りは痛いが次への投資と割り切った。
ハードウェア選定
手首カメラ:InnoMaker 1080P USB2.0 UVCカメラ(B0CNCSFQC1)
手首カメラに求める条件は明確です。
- 32×32mm基板:TheRobotStudio公式の手首マウントSTLがこのサイズ前提で設計されている
- Mac OS対応:M1 MacでドライバなしのUVC認識が必須
- 俯瞰C920nと別機種:同一機種を2台使うと、USB認識でカメラインデックスがランダムに入れ替わるトラブルが報告されている
InnoMaker B0CNCSFQC1はこの3条件を満たし、日本のSO-101ユーザーの実績もあるようでした。Amazon.co.jpで入手可能です。

USBハブ:ROSONWAY RSH-H7C4
M1 MacのUSB-Cポートは2つしかありません。1つは電源(充電)に使うため、実質1ポートしか使えないのです。アーム2本+カメラ2台の計4台を1ポートに集約するためにセルフパワーUSBハブが必要になりました。
選定で重要だったのはチップセットです!以前ANKERのUSBハブ(USB2.0コントローラー)経由でアームを動かしたとき、テレオペのループ速度が45Hz→8Hzに激落ちして There is no status packet! エラーが頻発した経験があります。ハブのチップ品質がサーボ通信の安定性に直結することを身をもって知っていました。
RSH-H7C4の仕様:
- USB 3.2 Gen2(10Gbps)全ポート対応
- 7ポート(USB-C×4、USB-A×3)
- セルフパワー(ACアダプタ DC12V/3A、36W)
- 個別電源ボタン+LEDインジケータ
届いてすぐに system_profiler SPUSBHostDataType でチップを確認した。
USB Vendor ID: 0x2109 → VIA Labs(確定)
USB Product ID: 0x0822 → VL822(確定)
Link Speed: 10 Gb/s
VIA Labs VL822が確定。このチップはMTT(Multiple Transaction Translators)対応で、複数デバイスへの帯域を個別に割り当てる設計です。Macドライバも不要で使いやすい。
3Dプリントマウント:Wrist_Cam_Mount_32x32_UVC_Module_SO101.stl
TheRobotStudio公式リポジトリにある手首カメラマウントです。既存のWrist Rollコンポーネントを丸ごと置き換える統合設計で、別途アダプターが不要なシンプルな構成だ。
印刷設定:
- インフィル:40%
- サポート:ツリーサポート
- フィラメント:PLA
- 向き:STLのデフォルトのまま
インフィル40%と多めにしているのは強度が重要だからです。負荷がかかる場所なので。壁面数も6に上げています。
ダウンロード先:
TheRobotStudio/SO-ARM100
└ Optional/Wrist_Cam_Mount_32x32_UVC_Module/stl/
└ Wrist_Cam_Mount_32x32_UVC_Module_SO101.stl
取り付けネジ:
- M2×4本(カメラモジュール固定・Feetchサーボ付属品の流用可)
- M3×2本(アームへの固定)
USBハブ検証:アームは60Hzで動いた
最大の懸念は「RSH-H7C4経由でアームが安定して動くか」だった。以前のAnkerハブ(USB2.0コントローラー)で8Hzになったトラウマがある。
結果は予想を上回るものだった。
Ankerハブ(USB2.0・STT)→ 8Hz・不安定・エラー頻発
RSH-H7C4(VL822・MTT) → 60Hz・滑らか
60Hz。速い。 VL822のMTTが各ポートへの帯域を独立して管理するため、サーボ通信のタイミングが乱れないどころか、以前より安定した結果になった。
チップ選定の重要性を改めて実感した瞬間だった。
最終的な接続構成
Macポート① → 電源(充電)
Macポート② → RSH-H7C4(1台のみ)
├ USB-A① → leaderアーム
├ USB-A② → followerアーム
├ USB-A③ → C920n(俯瞰・front)
└ USB-C①(変換アダプタ)→ InnoMaker(手首・wrist)
ポートが1台に全部収まった。
カメラの認識確認
lerobot-find-cameras opencv
結果:
- index 0 → InnoMaker(手首・wrist)
- index 1 → C920n(俯瞰・front)
カメラインデックスは接続順や起動タイミングで変わることがあるため、毎回 lerobot-find-cameras opencv で確認してからコマンドを実行する必要があります。
2カメラ構成のコマンド
テレオペ(映像確認)
lerobot-teleoperate \
--robot.type=so101_follower \
--robot.port=$FOLLOWER_ARM \
--robot.id=right_follower_arm \
--robot.cameras='{
wrist: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30},
front: {type: opencv, index_or_path: 1, width: 640, height: 480, fps: 30}
}' \
--teleop.type=so101_leader \
--teleop.port=$READER_ARM \
--teleop.id=right_leader_arm \
--display_data=true
データ収集(lerobot-record)1日目
HF_USER=$(hf auth whoami --format quiet)
DATASET="so101_lego_2cam_v1"
lerobot-record \
--robot.type=so101_follower \
--robot.port=$FOLLOWER_ARM \
--robot.id=right_follower_arm \
--robot.cameras='{
wrist: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30},
front: {type: opencv, index_or_path: 1, width: 640, height: 480, fps: 30}
}' \
--teleop.type=so101_leader \
--teleop.port=$READER_ARM \
--teleop.id=right_leader_arm \
--dataset.repo_id=${HF_USER}/${DATASET} \
--dataset.root=$HOME/.cache/huggingface/lerobot/${HF_USER}/${DATASET} \
--dataset.single_task="Grab the yellow lego block" \
--dataset.num_episodes=50 \
--dataset.streaming_encoding=false \
--dataset.encoder_threads=2 \
--display_data=falseZshデータ収集(lerobot-record)2日目以降(–resume=trueで継続)
HF_USER=$(hf auth whoami --format quiet)
DATASET="so101_lego_2cam_v1"
DATASET_ID="so101_lego_2cam_v1_20260617_192936"
lerobot-record \
--robot.type=so101_follower \
--robot.port=$FOLLOWER_ARM \
--robot.id=right_follower_arm \
--robot.cameras='{
wrist: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30},
front: {type: opencv, index_or_path: 1, width: 640, height: 480, fps: 30}
}' \
--teleop.type=so101_leader \
--teleop.port=$READER_ARM \
--teleop.id=right_leader_arm \
--dataset.root=$HOME/.cache/huggingface/lerobot/${HF_USER}/${DATASET} \
--dataset.repo_id=${HF_USER}/${DATASET_ID} \
--dataset.single_task="Grab the yellow lego block" \
--dataset.num_episodes=50 \
--dataset.streaming_encoding=false \
--dataset.encoder_threads=2 \
--display_data=false \
--resume=trueZshDATASET="so101_lego_2cam_v1"と、DATASET_ID="so101_lego_2cam_v1_20260617_192936"を分けているのはHubへのアップロード時に、lerobot-record が内部でタイムスタンプ付きのリポジトリ名を生成してアップロードするためです。
Hagging Faceにログインして、DATASET_IDを確認します。
ローカルの保存先はDATASETを使っています。これは --dataset.root を指定したことでローカルの保存先はタイムスタンプなしになりました。--dataset.rootを使わない場合はローカルの保存先もタイムスタン付きのフォルダ名になります。
確定した設定
M1 Mac + 2カメラ構成での正解設定はこれです。
--dataset.streaming_encoding=false
--dataset.encoder_threads=2
--display_data=false
2カメラのencoder_threads設定:M1 Macでの正解は streaming_encoding=false
これは何かというと、streaming_encoding=true にしてしまうと、M1の8コアを食い尽くすようなのです。そこでstreaming_encoding=false にするとエピソード終了後にまとめてエンコードするため、録画中のCPU負荷がゼロになりデータを30Hzで完全に維持できる。エピソード間の待ち時間が長くなる(10〜30秒)のが代償だが、データ品質優先なら明確にこちらが正解。これで、安定して30Hzでのデータ取得ができるようになった。もっと高速なCPUならばstreaming_encoding=true にして効率的にデータ収集できるのだろうが手持ちのM1 Macではこれが限界だ。
学習
# === 設定 ===
HF_USER = "emboss369"
DATASET = "so101_lego_2cam_v1_20260617_192936"
MODEL_NAME = "act_so101_lego_2cam_v1_150"
STEPS = 12000
BATCH_SIZE = 32
SAVE_FREQ = 1000
# ============
cmd = f"""lerobot-train \
--dataset.repo_id={HF_USER}/{DATASET} \
--policy.type=act \
--output_dir=outputs/train/{MODEL_NAME} \
--job_name={MODEL_NAME}_training_job \
--policy.device=cuda \
--wandb.enable=true \
--policy.repo_id={HF_USER}/{MODEL_NAME} \
--batch_size={BATCH_SIZE} \
--steps={STEPS} \
--save_freq={SAVE_FREQ}"""
print(cmd)
!{cmd}ZshMODEL_NAME="act_so101_lego_2cam_v1_100"
lerobot-rollout \
--robot.type=so101_follower \
--robot.port=$FOLLOWER_ARM \
--robot.id=right_follower_arm \
--robot.cameras='{
wrist: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30},
front: {type: opencv, index_or_path: 1, width: 640, height: 480, fps: 30}
}' \
--policy.path=emboss369/${MODEL_NAME} \
--display_data=trueZsh学んだこと・注意点まとめ
USBハブはチップで選べ :安物のバスパワーハブはサーボ通信でエラー発生。VIA Labs VL822(MTT対応)のセルフパワーハブ一択だ。
カメラは別機種を選べ :同一機種を2台使うと、USBパスがランダムに入れ替わってクラッシュすることもあるそうだ。俯瞰(C920n)と手首(InnoMaker)で意図的に機種を変えることで、これを回避した。
カメラインデックスは毎回確認 :lerobot-find-cameras opencv を実行して、カメラインデックスを確認してからコマンドを組む。学習時と推論時でwristとfrontが入れ替わると、モデルが破綻する。
既存データは取り直し :カメラ構成を変えたら、既存の単一カメラデータは使えない。250エピソード手放したが、「掴めるが落とす」を超えるための必要コストと割り切った。
学習精度が上がらない原因を分析する
タスクの定義
今回のタスクは、小さな黄色のレゴブロックを掴んで白いケースに入れる動作です。ロボットからすると対象物はかなり小さく、精密な動作が求められます。
このタスクを150エピソード繰り返してデータを収集しました。条件はこのようになっています。
- 黄色いブロックの位置:毎回ランダム
- 白い箱の位置:毎回ランダム
一見すると豊富なバリエーションのあるデータに見えます。しかし、ここに大きな落とし穴がありました。
問題の核心:ランダム性の二重化
「ブロック位置がランダム」「箱の位置もランダム」というのは、実は2つの独立した変数を同時に扱う必要があるタスクです。
ACTのような模倣学習は、基本的に「見たものに似た状況を再現する」仕組みです。つまり、学習時に見たことがない状況には対応できません。
ブロック位置と箱位置の組み合わせを考えると、空間は二次元的に広がります。たとえばブロックが取り得る位置が10通り、箱が取り得る位置が10通りあるとすると、理論上の組み合わせは100通りです。150エピソードという数は、この組み合わせ空間を十分にカバーできているとは言えません。
ブロック位置:A
箱位置:B
正しい軌道 = f(A, B)
この関数 f(A, B) を学習するためには、A と B の組み合わせを広くカバーしたデータが必要です。片方だけのランダム性なら同じエピソード数でも密度は格段に高くなりますが、両方がランダムだとデータ密度が二乗的に薄まります。
実際の症状
実機でrolloutを実行すると、このような症状が見られました。
- ブロックの方向には動く(観測→行動のマッピング自体は機能している)
- しかし2cmほどズレた目標へ向かってしまう
- ズレの方向はランダム(毎回同じ方向ではない)
この「ランダムな2cmのズレ」は、視覚的精度の不足を示しています。ブロックがどこにあるかは大まかに分かっているが、ピンポイントまで絞り込めていない状態です。
これはキャリブレーションの問題でも、視差の問題でもなく、データ密度の問題である可能性が高いと判断しました。仮説の立て方
原因を切り分けるために、変数を1つに絞る実験を設計しました。
実験条件
- ブロック位置:狭い範囲でランダム(従来より大幅に絞る)
- 箱の位置:固定
この条件で50エピソードを収集して学習した場合に、2cmのズレが改善するかどうかを確認します。
判定基準
- 改善した → 「ランダム範囲に対するデータ密度不足」が原因だったと確定。今後は段階的にランダム範囲を広げながらデータを追加する戦略を取ります。
- 改善しない → ランダム性が原因ではなく、別の要因(カメラ解像度、n_obs_stepsなど)を疑います。
データ収集のコツ:変数を1つずつ増やす
この実験から得られる教訓は、タスクの難易度を段階的に上げることの重要性です。
Step 1:ブロック固定・箱固定(最もシンプル)
↓ 成功したら
Step 2:ブロックランダム・箱固定
↓ 成功したら
Step 3:ブロックランダム・箱ランダム(最終目標)
最初から「両方ランダム」のデータを集めても、モデルがそれを学習できる段階に達していなければ意味がありません。まず「掴む」を確実にできるようにしてから、「運ぶ先」のランダム性を加えるというアプローチが、データ効率の面でも診断のしやすさの面でも優れています。
1回目
DATASET="so101_lego_2cam_narrow_v1"
lerobot-record \
--robot.type=so101_follower \
--robot.port=$FOLLOWER_ARM \
--robot.id=right_follower_arm \
--robot.cameras='{
wrist: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30},
front: {type: opencv, index_or_path: 1, width: 640, height: 480, fps: 30}
}' \
--teleop.type=so101_leader \
--teleop.port=$READER_ARM \
--teleop.id=right_leader_arm \
--dataset.repo_id=${HF_USER}/${DATASET} \
--dataset.single_task="Grab the yellow lego block and put it in the box" \
--dataset.num_episodes=50 \
--dataset.streaming_encoding=false \
--dataset.encoder_threads=2 \
--display_data=falseZsh2回目以降
DATASET="so101_lego_2cam_narrow_v1"
DATASET_ID="so101_lego_2cam_narrow_v1_20260627_120624"
lerobot-record \
--robot.type=so101_follower \
--robot.port=$FOLLOWER_ARM \
--robot.id=right_follower_arm \
--robot.cameras='{
wrist: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30},
front: {type: opencv, index_or_path: 1, width: 640, height: 480, fps: 30}
}' \
--teleop.type=so101_leader \
--teleop.port=$READER_ARM \
--teleop.id=right_leader_arm \
--dataset.root=$HOME/.cache/huggingface/lerobot/${HF_USER}/${DATASET} \
--dataset.repo_id=${HF_USER}/${DATASET_ID} \
--dataset.single_task="Grab the yellow lego block" \
--dataset.num_episodes=50 \
--dataset.streaming_encoding=false \
--dataset.encoder_threads=2 \
--display_data=false \
--resume=trueZsh現在の状況
箱固定・ブロック範囲を絞った条件でのデータ(narrow_v1)を49エピソード収集し、学習を開始しました。このデータで「2cmのズレ」が改善するかどうかが、次の判断分岐点です。
結果次第で、段階的にランダム性を加えていく戦略か、別のアプローチ(カメラ解像度の向上、データ収集方法の見直しなど)に切り替えるかを決定します。
学習
# === 設定 ===
HF_USER = "emboss369"
DATASET = "so101_lego_2cam_v1_20260617_192936"
MODEL_NAME = "so101_lego_2cam_narrow_v1_49"
STEPS = 5000
BATCH_SIZE = 32
SAVE_FREQ = 1000
# ============
cmd = f"""lerobot-train \
--dataset.repo_id={HF_USER}/{DATASET} \
--policy.type=act \
--output_dir=outputs/train/{MODEL_NAME} \
--job_name={MODEL_NAME}_training_job \
--policy.device=cuda \
--wandb.enable=true \
--policy.repo_id={HF_USER}/{MODEL_NAME} \
--batch_size={BATCH_SIZE} \
--steps={STEPS} \
--save_freq={SAVE_FREQ}"""
print(cmd)
!{cmd}Zshようやく2時間が経過し進捗率90%まできたこころで、Colabとの接続が突然切れました!
あわってて、次のコマンドで、途中から再開します。なんとか再開して学習時間が(お金が)無駄にならないですみましたが、焦りました。
cmd = f"""lerobot-train \
--dataset.repo_id={HF_USER}/{DATASET} \
--policy.type=act \
--output_dir=outputs/train/{MODEL_NAME} \
--job_name={MODEL_NAME}_training_job \
--policy.device=cuda \
--wandb.enable=true \
--policy.repo_id={HF_USER}/{MODEL_NAME} \
--batch_size={BATCH_SIZE} \
--steps={STEPS} \
--resume=true \
--config_path=outputs/train/so101_lego_2cam_narrow_v1_49/checkpoints/005000/pretrained_model/train_config.json \
--save_freq={SAVE_FREQ}"""Zshポイントは、–resume=true を指定して、–config_path=<train_config.json>を指定することです。こうすることで、途中まで保存したセーブポイントから再開できるのです!
なお、SAVE_FREQ = 1000 と指定している通り、–save_freqで小さめの数を指定することで、頻繁にセーブしてくれます。2000とか1000とかに設定しておくと安心です。
学習済みデータのrollout
MODEL_NAME="so101_lego_2cam_narrow_v1_49"
lerobot-rollout \
--robot.type=so101_follower \
--robot.port=$FOLLOWER_ARM \
--robot.id=right_follower_arm \
--robot.cameras='{
wrist: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30},
front: {type: opencv, index_or_path: 1, width: 640, height: 480, fps: 30}
}' \
--policy.path=emboss369/${MODEL_NAME} \
--display_data=trueZshエラーThe fields pretrained_revision are not valid for ACTConfig
ここで、次のようなエラーが発生しました。
The fields `pretrained_revision` are not valid for ACTConfig
このエラーはモデルのHubに保存されている設定ファイル(config.json)に pretrained_revision というフィールドが含まれているが、現在インストールされているLeRobotのバージョンの ACTConfig クラスにそのフィールドが存在しないために起きます。 Yahoo! Shopping
つまり、モデルを学習したColabのLeRobotバージョンと、ローカルのLeRobotバージョンが食い違っているのが原因です。
ローカルのLeRobotを最新版にアップデートします。
LeRobotのアップデート
cd ~/development/lerobot-workspace/lerobot
git pull
pip install -e ".[feetech]" --quietZshエラー:Record loop is running slower (8.1 Hz)
ターミナルにエラーが出ています。
WARNING 2026-06-30 05:05:35 gies/base.py:75 Record loop is running slower (8.1 Hz) than the target FPS (30.0 Hz). Dataset frames might be dropped and robot control might be unstable. Common causes are: 1) Camera FPS not keeping up 2) Policy inference taking too long 3) CPU starvation
警告 2026-06-30 05:05:35 gies/base.py:75 記録ループの実行速度(8.1 Hz)が、目標FPS(30.0 Hz)を下回っています。データセットのフレームが欠落したり、ロボットの制御が不安定になったりする可能性があります。一般的な原因としては、1) カメラのFPSが追いついていない、2) ポリシー推論に時間がかかりすぎている、3) CPUリソース不足、などが挙げられます。
このエラーが出ると、一瞬ロボットの動作がリセットされた感じになり、それまでの動作を少し忘れている?ようにに見えます。
何が起きているか
ACTは n_action_steps=100 のチャンク予測をします。つまり「100ステップ分の動作を一度に予測して、順番に実行する」仕組みです。
理想:30Hz × 100steps = 約3.3秒分の動作を予測 → なめらかに実行
現実:8Hz で動いている → 推論が間に合わず途中でリセット → カクカク
推論が30Hzに追いつかないと、チャンクの途中でバッファが切れて新しい予測を始めるサイクルが崩れ、連続性が失われます。
M1 MacのMPSでACTのリアルタイム推論は難しいというのが現実です。学習はColab(T4 GPU)でやっていますが、推論もGPUが必要なフェーズに来ています。
Google Colabを卒業してAWS EC2でロボット学習
無料のGoogle Colabには大変お世話になりました。しかし無料枠には限りがあります。1回学習すればそれで使い切ってしまいます。そのためGoogle Colab Proを契約しました。
それでも、AWSのAI系サービスを一通り検討した上で、なぜEC2を選んだのか、そして実際にSO-ARM101のACTポリシーをEC2で学習させる全手順をまとめます。
今回、当初の計画は「AWSコンソールを手でポチポチ操作し、SSHで入って手動でコマンドを叩く」というものでしたが、実際に手を動かす中で「これは毎回手作業でやるものじゃないな」と気づき、最終的にはTerraformで全体をコード化し、Spotインスタンス前提の使い捨てインフラとして構築し直しました。この記事では、当初の検討内容に加えて、実際に組み上がった仕組みと、その過程で踏んだ数々の地雷についても記録しておきます。
なぜAWSなのか
これまでのワークフローはこうでした。
M1 Macでテレオペ・データ収集
↓
HuggingFace Hubにデータセット保存
↓
Google ColabでACT学習 ← ここが無料枠終了で詰まった
ローカルにGPU搭載のBTO PCを導入する計画はあるものの、到着はまだ先です。それまでのつなぎとして、クラウドGPUを使うことにしました。
また、Colab Proという選択肢もありますが、データも集まり、学習時間が長くなってくると、Colab Proの最大連続12時間という制限が地味に不便です。学習させている間、Colabのタブを閉じることができないわけですから。
AWSのAI系サービスを検討する
「AWSでAI学習」と一口に言っても、選択肢はたくさんあります。一通り調べて、今回の用途(カスタムPyTorchモデルであるACTの学習)に合うかどうかを評価しました。
① EC2 GPU インスタンス → 採用
LinuxマシンをクラウドにレンタルしてLeRobotを直接動かす、最もシンプルな方法です。環境構築が自由で、Colabと同じT4 GPUが使え、スポットインスタンスなら格安です。結論:今回はこれを採用。
② SageMaker → 見送り
マネージドなML学習基盤です。ジョブ管理やログが自動化されて便利ですが、同等のEC2インスタンスに比べて2〜4割ほど割高になります。個人の実験レベルではオーバースペックでした。
見送りの理由
- ワークフロー自体が単純(1ステップ):今回は「ACT/SmolVLAを学習する」という単一ステップのみで、前処理→評価→モデルレジストリ登録→デプロイのような多段パイプラインではありません。Pipelinesの本領(DAG管理・ステップキャッシュ・リネージ)がほぼ活きない規模感です。
- 対話的なデバッグのしやすさを優先:SageMaker Trainingはジョブをマネージドコンテナ内で実行する「ブラックボックス」型で、SSH/SSMで直接入って
nvidia-smiや中間ファイルを見る、といった今回の運用スタイルとは相性が悪いです。素のEC2ならインスタンスに直接入って自由に触れます。 - カスタム環境の持ち込みコスト:LeRobotの学習環境(conda、ffmpeg、独自のtrain/dataset extras)をSageMakerで使うには、これらを含んだ独自コンテナイメージを作ってECRにpushする一手間が必要になり、
user_data一発で完結させる今の設計方針とは逆方向です。 - 個人利用でのコスト構造:SageMaker Trainingインスタンスは同スペックの素のEC2より単価が上乗せされており(マネージドである分の手数料)、個人の検証用途では素のEC2 Spotの方が単純に安上がりです。
面白いのは、SageMakerにはManaged Spot Trainingという機能があり、checkpoint_s3_uriを指定するだけでSpot中断時のチェックポイントS3保存・再開をAWS側が自動でやってくれます。つまり、前回resume_or_start.shで手作りした「S3への定期sync+resume」の仕組みは、SageMakerを使えば標準機能としてタダで手に入る部分ではあります。ただし上記の理由(対話性・単純さ・コスト)から、今回は「自分でTerraform+EC2で作る」方針を優先した形です。
③ Amazon Bedrock → 用途違い
AIといえばAmazon Bedrockを最初に思い浮かべるでしょう。私もそうでした。Bedrockは既製の基盤モデル(Claude、Llama等)にAPIでアクセスするサービスです。カスタムモデルをゼロから学習する用途には使えません。
④ AWS Trainium → 将来の宿題
AWS独自のAI学習チップで、GPU比でコスト削減が可能です。ただしPyTorchをそのまま動かせず、専用のNeuron SDKへの対応が必要なため、今回は見送りました。コストは魅力ですがそもそもNVIDIAのcudaエコシステムの利点が活かせないため実現は難しいと判断しました。
⑤ AWS Batch → 今はまだ早い
DockerとCDKを組み合わせた本格的なジョブパイプラインです。AWS公式がLeRobot × SO-ARM101の構築ガイドを出しているほど相性は良いのですが、インフラ構築の知識が必要で、今の段階では複雑そう。今後の改題とします。
Colab vs EC2
もちろん Colab Proを使うという手もあります。
| Google Colab無料 | Google Colab Pro | AWS EC2 スポット | |
|---|---|---|---|
| GPU | T4 | T4/A100 | T4 |
| 時間制限 | あり(不安定) | 緩め | なし |
| コスト | 無料 | 約1,200円/月 | 約170円/回 |
| 手間 | 少ない | 少ない | 中程度(→今回コード化してほぼゼロに) |
| 安定性 | 低い | 中 | 高い |
無料で気軽に試すならColab、繰り返し回すならEC2スポット、という棲み分けです。そしてBTO PC(いまGPU搭載パソコンを発注したところです)が届いたら、ローカルGPUが待ち時間ゼロ・追加コストゼロの最終回答になる予定です。
Colab Pro と EC2スポットの損益分岐
定額のColab Proと従量制のEC2スポット、どちらが得かは「月に何回学習を回すか」で決まります。Colab Proが月約1,200円、EC2スポットが1回約170円なので、割り算するとおおよそ次のようになります。
1,200円 ÷ 170円 ≒ 7回
つまり月7回以上学習を回すならColab Pro、6回以下ならEC2スポットが目安です。
ただし数字だけでは見えない差もあります。Colab ProはA100が使えて1回の学習が1.5時間と速く、ブラウザを閉じるだけで済むので気楽です。一方EC2は起動・終了を自分でやる手間と、terminateし忘れて課金が続くリスクがあります。その代わり時間制限がなく、長時間の学習やデータ前処理も中断されません。
データ収集を増やして何度も学習を回す今のフェーズでは、A100の速さも込みでColab Proの方が体験が良いかもしれません。EC2は「Colab ProでもGPUが足りない」「環境を細かく制御したい」ときの選択肢として持っておく、という位置づけが現実的です。まずはお安くEC2で試してみて、ガンガン使うようになってきたらColab Proでも良さそうです。
Colab Proは100CU/月の従量的な仕組みです。T4は1時間あたり約1.96ユニットで、月100ユニットなら約51時間使えます。ACT学習はT4だと4〜6時間。仮に5時間とすると:
100 CU ÷ 1.96 CU/h ÷ 5h ≒ 約10回
「月10回まではColab Pro(T4)が最安」だと思われます。
採用:EC2 g4dn.xlarge スポットインスタンス
とはいえ、最終的に選んだのは g4dn.xlarge のスポットインスタンスです。最初のうちは月にそんなに学習させないと想定しています。
| 項目 | 内容 |
|---|---|
| GPU | NVIDIA T4(16GB) |
| vCPU / メモリ | 4 / 16GB |
| オンデマンド料金(us-east-1) | 約$0.526/時間 |
| スポット料金(us-east-1) | 約$0.23〜0.24/時間(時期・AZで変動) |
| ストレージ(EBS gp3 150GB) | 約$0.08/GB・月 |
リージョンは当初「東京(ap-northeast-1)」も検討しましたが、最終的には us-east-1(バージニア北部) に固定しました。
実際の学習コスト(実測ベース)
今回、実際に完走した学習(ACTポリシー、5,000ステップ、バッチサイズ32)の実績はこうでした。
インスタンス稼働時間:約3時間(セットアップ+自動リブート+学習2時間37分+最終sync)
GPU(スポット、g4dn.xlarge):約$0.23/時間 × 3時間 ≒ $0.7
ストレージ・S3・DynamoDB等:ごくわずか(無視できる額)
─────────────────────────────────
合計:概算で $1前後(150円前後)
コンビニのコーヒーより安く1回の学習が回せています。※AWSのCost Explorerは当日分の反映に24〜48時間ほどラグがあるため、確定額は翌日以降のBilling Consoleで確認し、判明次第この記事にも追記します。
EC2で学習する全手順 ― 手動オペレーションからTerraformへ
当初の計画は「AWSコンソールで毎回ポチポチ設定→SSH接続→手動でコマンド実行→Discord通知→手動でterminate」というものでした。しかし思ったのは「これを学習のたびに毎回手でやるのは事故のもとだし面倒すぎる」ということです。特に懸念していたのは次の2点です。
- コンソールでのポチポチ設定は毎回微妙に条件(AMI・インスタンスタイプ・セキュリティグループ)がブレる可能性がある
- terminateし忘れる、あるいはstopと間違えるという「うっかり」に対して人間の記憶力に頼っている
そこで、Terraform(IaC=Infrastructure as Code)で環境全体をコードとして定義し、「毎回まっさらな状態から作り、学習が終わったら跡形もなく壊す」という Immutable Infrastructure の考え方に切り替えました。以下、実際に組み上がった仕組みです。
Phase 1:手動コンソール操作から卒業する
AWSコンソールで毎回設定していた「リージョン→AMI→インスタンスタイプ→スポットリクエスト→ストレージ→セキュリティグループ→キーペア」の一連の設定を、すべて.tfファイルとして書き下ろしました。これにより:
terraform planで「何が作られるか」を実行前に必ず確認できるterraform apply一発で、毎回まったく同じ条件でインフラが再現できる- 設定の変更履歴がGitで残る(「なぜこの設定にしたか」が追跡できる)
AMIは当初検討していた「Deep Learning OSS Nvidia Driver AMI GPU PyTorch」ではなく、素の Ubuntu AMI を採用しました。GPUドライバ・Docker・Miniconda・LeRobot一式のインストールを、EC2起動時に自動実行されるuser_data(cloud-init)スクリプト側で完結させる設計にしたためです。既製のDeep Learning AMIに乗っかるより、「何が入っているか」を全部自分のコードで把握・再現できる方を優先しました。
SSHも使っていません。代わりに AWS Systems Manager Session Manager を使っています。.pemキーファイルの管理も、22番ポートを自分のIPだけに開ける設定も不要になり、IAMロールさえ付与しておけばブラウザ/CLIから直接シェルに入れます。
Phase 2:terraform apply一発で「起動→環境構築→認証→学習開始」まで自動
実際の自動化フローはこうなりました。
terraform apply
→ EC2起動(Spotインスタンス)
→ user_data実行:NVIDIAドライバ/Docker/Miniconda(python3.12)/LeRobotのセットアップ
→ SSM Parameter StoreからHuggingFace/WandBのトークンを取得しノンインタラクティブでログイン
(トークンをコード・tfvarsに書かない。SecureStringとして事前登録するだけ)
→ NVIDIAドライバ有効化のため1分後に自動リブート
→ systemdサービスが学習(lerobot-train)を自動起動
→ 学習中は10分ごとに出力ディレクトリをまるごとS3へ同期(Spot中断に備える)
→ 正常終了時:lerobot-train自身がHuggingFace Hubへモデルを自動push
(push_to_hubはデフォルトTrueなので、Discord通知のような追加の一手間は不要)
→ 最終syncと完了マーカーをS3に書き込み
「学習が終わったらHubにアップロード」を自分でcurlを叩いて実装する必要はありませんでした。lerobot-train自体が、policy.repo_idさえ指定しておけば正常終了時に自動でHuggingFace Hubへpushしてくれる仕様だったからです(これは実際にコードを読んで確認するまで気づきませんでした)。
Phase 3:実際にハマった壁(今回いちばんの学び)
きれいに設計したつもりでも、実際にterraform applyを叩くと現実の壁にぶつかります。今回遭遇した問題を記録しておきます。次に同じ作業をする人(未来の自分も含め)への申し送りです。
① vCPUクォータが「0」問題、そして「4」問題
新規アカウントではGPUインスタンス系のクォータが初期値0になっていることがあり、これは当初の計画通りService Quotasから緩和申請が必要でした(Running On-Demand G and VT instances)。
EC2を作成しようとすると下記のようなメッセージが表示されました。
Your request for accessing resources in this region is being validated, and you will not be able to launch additional resources in this region until the validation is complete. We will notify you by email once your request has been validated. While normally resolved within minutes, please allow up to 4 hours for this process to complete. If the issue still persists, then open a support case. [https://support.console.aws.amazon.com/support/home?region=us-east-1#/case/create?issueType=customer-service&serviceCode=account-management&categoryCode=account-verification]
これはAWS側のアカウント検証メッセージで、エラーではありません。
新しいAWSアカウントや、初めてGPUインスタンス(g4dn等)を立てようとすると出ることがある、正常な検証プロセスです。不正利用防止のため、AWSがアカウントを確認しています。

You have requested more vCPU capacity than your current vCPU limit of 0 allows for the instance bucket that the specified instance type belongs to. Please visit http://aws.amazon.com/contact-us/ec2-request to request an adjustment to this limit.
これはAWSのvCPUクォータ制限です。GPUインスタンス(g4dn等)の上限が0に設定されているため、起動できません。
新規アカウントでは、GPUインスタンス系のクォータが初期値0になっていることがよくあります。先ほどのアカウント検証とは別の、これは明示的な上限緩和申請が必要な制限です。
AWSコンソールで「Service Quotas」を検索して開く

左メニュー「AWSのサービス」→「Amazon EC2」を選択

検索窓に 「Running On-Demand G and VT instances」 と入力、この項目を選び「アカウントレベルでの引き上げをリクエスト」

値に 4(g4dn.xlargeは4 vCPU)以上を入力して申請。

申請には数時間〜2営業日かかることがあります。
同様に、スポットインスタンスの方の項目、All G and VT Spot Instance Requests のクォーターも申請を行います。
ここで見落としがちなのが、Spot版のクォータ(All G and VT Spot Instance Requests)は別枠だということです。今回申請して確保できたのはわずか4vCPU。g4dn.xlargeもg6.xlargeもちょうど4vCPUなので、これは「常にインスタンス1台分ぴったり、余裕ゼロ」という状態を意味します。そのため、インスタンスを再作成(destroy→create)する際、AWS側のクォータ会計処理が反映されるまでのわずかな間に次のcreateが走ってしまい、MaxSpotInstanceCountExceededで失敗することが何度もありました。対策は「数十秒〜数分待って再実行」。自動化スクリプト側にも待機時間を仕込んで緩和しています。
② Spot容量は時間帯・AZでかなり変動する
最初に選んだg6.xlarge(L4 GPU)が、あるAZでは起動できても数時間後には同じAZで容量不足(InsufficientInstanceCapacity)になる、ということが実際に起きました。aws ec2 get-spot-placement-scoresというAPIで、インスタンスタイプ×AZごとの確保しやすさ(スコア1〜10)を事前に見られることを知り、これを使ってAZを選び直しました。最終的には、コストも安く容量にも余裕があるg4dn.xlargeに切り替えて安定させています。冒頭で紹介した価格・性能はこの経緯を経た結論です。
③ Ubuntuのawscliパッケージが入らない
apt-get install awscliが「no installation candidate」で失敗しました。Ubuntuのawscliはuniverseリポジトリに依存しており、環境によっては解決できません。AWS公式のCLI v2インストーラ(zip配布)に切り替えて解決しました。
④ condaの利用規約同意
conda createが「Terms of Serviceに同意していない」で失敗。Anacondaのデフォルトチャンネルは最近利用規約への同意が必須になっており、conda tos acceptを事前に実行する必要がありました。
⑤ Pythonバージョンの不一致
LeRobotの最新版はpython>=3.12を要求しますが、当初condaの環境はpython=3.10で作っていました。依存解決の途中で「Python 3.10.20は3.12以上という要件に合わない」と判明し、修正しました。
⑥ NVIDIAドライバは再起動しないと有効にならない
ドライバをインストールしても、nvidia-smiが実際に有効になるには再起動が必要な場合があります。同じ起動シーケンス内で即座に学習を始めるとドライバ未反映のまま走ってしまうリスクがあったため、セットアップ完了後に自動で1回リブートし、その後systemdが学習を自動起動する形に直しました。
⑦ データセット用S3バケットは「使い捨てインフラのstate」と分離する
学習インフラ(EC2・IAM・セキュリティグループ)は毎回作り直す前提ですが、データセット・チェックポイント用のS3バケットはそうはいきません。これらを同じTerraform stateに混ぜると、インフラを壊す操作のたびに大事なデータまで巻き込まれる事故リスクを抱えます。そこで、Terraformのstate自体を「毎回作り直す本体」と「永続データを持つ別state」に分離し、後者にはprevent_destroyという誤削除防止のガードも付けました。
Phase 4:学習完了 → 自動でHubへpush → 後片付けも自動
当初の計画ではDiscord Webhookで完了通知を送り、手動でインスタンスをterminateする想定でした。今回はここも仕組み化しています。
- 学習の進捗はリアルタイムで Weights & Biases のダッシュボードから確認(インスタンスにSSHで入らなくても、ブラウザで見られる)
- 学習完了を検知するオーケストレータースクリプトが、完了マーカー(S3上の目印ファイル)を見つけたら自動で
terraform destroyを実行(terminateし忘れる心配がそもそもない) - モデルはHuggingFace Hubへpush済みなので、S3に残ったチェックポイント(resume用の一時データ)はお役御免。完了マーカーの存在を確認した上で自動的に削除するようにしました
- Spot中断(インスタンスが完了マーカーなしに消えた場合)は、待機を挟んで自動で
terraform applyをやり直し、S3上の直近チェックポイントから学習を再開
「stop」と「terminate」を覚えておく時代の終わり
当初の記事では、次の違いをしっかり覚えておくことを重要な注意点として挙げていました。
停止(stop) → EBSストレージの課金は継続 ← 油断ポイント
終了(terminate)→ EBSも削除され課金ゼロ ← これを使う
これは今でも正しい知識ですが、今回たどり着いた答えは一段上のレイヤーにあります。「terminateを覚えておく」という人間の記憶力に頼るのをやめ、そもそも毎回破棄する前提の設計にすることで、覚えておくべきことをゼロにする、というアプローチです。
- 学習用インスタンスは毎回まっさらに作り、学習が終わったら(あるいはSpot中断されたら)跡形もなく壊す
- 壊して困る永続データ(データセット・チェックポイント)は最初からインフラの外(別管理のS3)に置く
- 学習済みモデルの本当の置き場所はHuggingFace Hub。EC2にもS3にも「唯一のコピー」を残さない
学習完了後、実際にAWS上を一通り点検し、EC2インスタンス・EBS・Elastic IP・IAMロール・セキュリティグループ・Spotリクエストがすべて跡形もなく消えていることを確認しました。課金対象として残っているのはSSM Parameter Store(トークン2件、無料枠)のみです。
今回、当初想定していた「手動SSH運用+Discord通知」から、Terraformによる完全自動化・Immutable Infrastructure・Spot中断耐性・HuggingFace Hub自動pushという構成にたどり着きました。実際にSO-ARM101のACTポリシー学習(5,000ステップ、約2時間37分)も無事完走し、モデルはHuggingFace Hub(emboss369/so101_lego_2cam_narrow_v1_49)に自動でアップロードされています。
コストの確定額は翌日以降のBilling Consoleで確認でき次第、この記事に追記します。
この記事はLeRobot、SO-ARM101を使った実際の作業記録に基づいています。AWSの料金やサービス仕様は変わることがあるので、最新情報は公式ドキュメントでご確認ください。

コメント