フィジカルAI研究室を作る Log#5 | π0.5 を mechArm 270 の成功19本で2,000ステップ学習 ― 予測した最初の1秒が、お手本の平均とほぼ重なった

机の上の小さなロボットアームに、「赤いブロックをつまんで白い箱に入れる」お手本を19回分見せて、AI に4時間かけて覚えさせました。覚えたあとの AI に、アームの上のカメラの画像と、今の関節の角度を渡すと、「次の1秒でどう動くか」を返してきます。その1秒を、お手本19回の平均と並べると、肩の関節は1秒後に 32.0° と 30.5°、手首は −27.6° と −26.6° でした。どの関節も、差は数度以内です。アームにはまだ1度も送っていません。画面に予測を出して、お手本と比べただけです。

使った AI は、Physical Intelligence 社が公開しているロボット用のモデル π0.5(パイゼロごー)です。カメラの画像と指示文を受け取って、ロボットの関節をどう動かすかを出すモデルで、別の大きなロボットのデータで事前に学習されています。手元のアーム mechArm 270(関節6つの小型アーム)とは、関節の数もカメラの位置も違うので、そのままでは動かせません。そこで、mechArm で記録したお手本を使って、π0.5 に追加で学ばせました。追加で学ばせることを、ファインチューニング(追加学習)と呼びます。

お手本は、人が手で動かしたものではありません。これまで使ってきた「ブロックの位置をカメラで見つけて、決まった順番で動く」プログラム(スクリプト)を何度も動かして、成功した回だけを集めました。つまり π0.5 には、スクリプトの動きをまねさせています。まねができたら、次はスクリプトと π0.5 を同じ条件で並べて比べます。

お手本を集める途中で、アームが「置く」直前に止まるようになりました。調べると、置く高さを自分で直す仕組み(成功のたびに少しずつ学ぶ補正)が、成功するたびに 5mm ずつ高さを上げ続けていました。カメラが「白い箱が 20cm 動いた」と毎回測り違えていて、それを「ブロックが箱に当たった」と受け取っていたためです。高さが上がり続けた結果、手首の角度が安全範囲の外になり、送る前の安全チェックが止めていました。お手本を集めている間は、この補正を更新しないようにして続けています。

やったことの全体は次のとおりです。

やったことなぜ結果
スクリプトを回して成功のお手本を集めたπ0.5 に mechArm での動き方を覚えさせるため合計20本。修正後は12回中9回成功
置く高さの補正の暴走を止めた成功のたびに 5mm 上がり、手首が安全範囲の外に出ていた補正を 15mm に戻し、記録中は更新しない
19本で π0.5 を学習した1本(つかみ直した回)を除き、そろった動きだけにしたGPU(NVIDIA A40)を借りて4時間3分。loss 0.1155 → 0.0054
予測を画面に出して、お手本と比べたアームに送る前に、変な動きをしないか確かめるため最初の1秒は、お手本の平均と関節ごとに数度以内

GPU は、Runpod というサービスで時間単位で借りました。A40 を1時間 0.62 ドルで借りて、学習そのものに4時間3分かかっています。学習が終わったのは夜中の1時過ぎで、そのあと朝まで GPU を止めずに置いていたため、使わない時間の料金も払っています。

この回の作業は、Claude(Anthropic の AI)と一緒に進めました。プログラムの修正と手順書は Claude が書き、アームの操作、カメラとケーブルの接続、Runpod の画面操作、成功したかどうかの目視の判定はこちらで行っています。ここから下は、使ったコマンド・コード・出力を全量で載せた作業ログです。

作業ログ ― デーモンの再起動コマンドが、起動する前に自分自身を止めていた

mechArm は、Mac から Raspberry Pi 上のデーモン(常駐して指令を待つプログラム、ポート5555)に TCP でつないで動かしています。Pi の電源を入れ直すと、デーモンは自動では起動しません。前回までの成功は10本です。

最初に Claude が出した起動コマンドは、「古いデーモンを止める」と「新しく起動する」を1行にまとめたものでした。

(base) shimazueiichi@shimadzueiichinoMacBook-Pro ~ % ssh -i ~/.ssh/id_rsa_pi ubuntu@192.168.2.1 "pkill -f '[p]ython3 /home/ubuntu/mecharm_daemon.py'; setsid nohup python3 /home/ubuntu/mecharm_daemon.py </dev/null >>/home/ubuntu/daemon.log 2>&1 &"
sleep 5
ssh -i ~/.ssh/id_rsa_pi ubuntu@192.168.2.1 'tail -3 ~/daemon.log; ss -ltn | grep 5555'
[DAEMON 14:57:36] cmd received: get_angles
[DAEMON 14:57:36] cmd received: set_fresh_mode
[DAEMON 14:57:36] 切断: ('192.168.2.2', 53935)
(base) shimazueiichi@shimadzueiichinoMacBook-Pro ~ %

ログの3行は前回のもので、5555 で待ち受けている行も出ていません。デーモンは起動していませんでした。

pkill -f は、プロセスのコマンド行全体を検索します。[p]ython3 /home/ubuntu/mecharm_daemon.py は「python3 /home/ubuntu/mecharm_daemon.py」という文字列に一致する書き方で、検索するコマンド自身には一致しないようにする定番の形です。ところがこの1行には、後ろの起動部分に python3 /home/ubuntu/mecharm_daemon.py がそのまま入っています。ssh が Pi の上で動かすシェルのコマンド行にもこの文字列が含まれるので、pkill は起動部分を実行する前に、そのシェル自身を止めていました。

5555 で待ち受けている相手はいなかったので、止める部分を外して起動だけを実行しました。

(base) shimazueiichi@shimadzueiichinoMacBook-Pro ~ % ssh -i ~/.ssh/id_rsa_pi ubuntu@192.168.2.1 "setsid nohup python3 /home/ubuntu/mecharm_daemon.py </dev/null >>/home/ubuntu/daemon.log 2>&1 &"
sleep 5
ssh -i ~/.ssh/id_rsa_pi ubuntu@192.168.2.1 'tail -3 ~/daemon.log; ss -ltn | grep 5555'
[DAEMON 14:37:27] ✓ MyCobot 初期化完了
[DAEMON 14:37:27] startup_warmup angles=[-0.08, 92.9, 3.69, -2.28, -98.52, -29.79]
[DAEMON 14:37:27] ポート 5555 で待ち受け中...
LISTEN0      1                                 0.0.0.0:5555        0.0.0.0:*    
(base) shimazueiichi@shimadzueiichinoMacBook-Pro ~ % 

Pi の時計は合っていないので(ネットにつながらない有線接続のみ)、ログの時刻は実際の時刻と違います。起動直後の姿勢 j1=92.9°・j4=−98.52° は、電源を入れる前のたたんだ姿勢のままです。

続けて、記録係(上カメラの映像と関節角を記録するプログラム)と、アームを動かす本線を起動します。

pkill -f 'place_modules/recorder.py'; sleep 1
cd ~/r2r2r-scripts && python3 dynamic_tracking_limits/place/place_modules/recorder.py --top --no-side
cd ~/r2r2r-scripts && ./run_pick_place.sh --human-judge --record-demo

1回目の試行は、ブロックをつかんだまま「置く」直前の安全チェックで止まった

1回目の試行で、アームはブロックをつかんで箱の上まで運び、そこで止まりました。そのときの本線の出力です。

画像: /Volumes/KIOXIA/r2r2r-data/runs/run_pick_place/run_0001_20261016_174936/run_0001_20261016_174936_pick.jpg
  VLM : verdict=yes  confidence=0.82  reason=2枚目の画像でグリッパーの爪の間に赤ブロックが挟まれており、1枚目と比較してブロックの位置がグリッパーと共に上方向にシフトしていることが確認できる。床面との接触が見られない。
掴めている? [y/n/u(unknown)] > y
[JUDGE ] pick attempt=1 adopted=yes (vlm=yes human=yes dist_override=False) confidence=0.82 reason=2枚目の画像でグリッパーの爪の間に赤ブロックが挟まれており、1枚目と比較してブロックの位置がグリッパーと共に上方向にシフトしていることが確認できる。床面との接触が見られない。
[LOOKUP] stage7_approach_place  phase=approach_place  target_xz=(-0.1955,+0.1961)  → j0=+38.597°  method=interp  x_resid=0.00000m  z_resid=0.03330m  joint_ok=True
[SEND] stage7_approach_place: send_angles (single) [ +38.60,  +57.39,  +17.49,   +0.08,  -89.20,  -29.44]  speed=20 wait=1.0s  (pre max|Δ|=68.21°, post-send timeout 12.2s)
[SEND] arrival OK: max|Δ|=2.46° (tol 3.0°)
[MOVE  ] stage7_approach_place  arrived=True retried=False post_max|Δ|=2.460°
[CORR ] PLACE measure mode: apply/update skipped
[HGT  ] PLACE apply: cell=place_x-200_z200 dee_y=29.0mm → target_ee_y=0.1290m
[PLACE_LOOKUP_OBS] tag=stage8 method=fk_j4_priority_j4safe_absent ee_y_selected=+0.1144m d_ee_y_vs_prev=(first) max_dj=+11.800° target_angles=[+41.22,+67.00,+15.00,-7.00,-101.00,-30.00]
[LOOKUP] stage8_place  phase=place_withblock  target_xz=(-0.1955,+0.1961)  → j0=+41.215°  method=fk_j4_priority_j4safe_absent  x_resid=0.00536m  z_resid=0.01251m  joint_ok=True  max|Δj|=11.80°  ee_y=0.1144m
[ABORT ] stage=8  reason=送信前ガード NG: j4=-101.00 が J4_SAFE[-90.0,60.0] 外
  [WARN] fresh_mode切替不可: daemon error: set_fresh_mode is not available in this pymycobot environment
[LOG   ] /Volumes/KIOXIA/r2r2r-data/runs/run_pick_place/run_0001_20261016_174936/run_0001_20261016_174936.json
[LEDGER] append OK: /Volumes/KIOXIA/r2r2r-data/runs/pick_place_ledger_v3.csv (in_box_geom= dist=)
[LOOP] === 試行 #1 終了: success=False log=/Volumes/KIOXIA/r2r2r-data/runs/run_pick_place/run_0001_20261016_174936/run_0001_20261016_174936.json ===

[LOOP] 次の操作 [n=次試行 (Enter可) / h=HOME / q=終了]: 

止めたのは、送る前の安全チェック(J4_SAFE、手首の関節 j4 は −90°〜+60° の範囲でしか送らない)です。置く姿勢として選ばれた関節角が j4=−101° で、範囲の外でした。2回目の試行(run_0001_20261016_175123)も、同じ stage8 で同じ理由で止まっています。

アームはブロックをつかんだまま箱の上で止まっているので、ブロックの下に手を添えて h(HOME に戻す)を送り、ブロックを手で受け取りました。次に n を押すと、最初にグリッパーが開いてブロックが落ちるためです。

出力の中の [HGT ] PLACE apply: cell=place_x-200_z200 dee_y=29.0mm が手がかりでした。置く高さの補正(dee_y)が 29mm になっていて、目標の高さ(target_ee_y=0.1290m)を押し上げています。

置く高さの補正が、成功するたびに 5mm ずつ上がっていた

前回うまくいっていた試行と、止まった試行で、置く姿勢(stage8)と高さ補正を並べました。

cd $HOME/mnt/KIOXIA/r2r2r-data/runs/run_pick_place && python3 - <<'PY'
import json,glob
runs=["run_0001_20261016_174936","run_0001_20261015_193907","run_0001_20261015_194135","run_0001_20261015_194420","run_0001_20261015_185536"]
for d in runs:
    j=json.load(open(f"{d}/{d}.json"))
    print("==",d)
    for k in ("place_target_xz","place_xz","box_xz_m"):
        if k in j: print(k,j[k])
    for m in j.get("moves",[]):
        if m.get("label","").startswith(("stage7","stage8")):
            print(" ",m.get("label"),[round(a,1) for a in (m.get("target_angles") or m.get("angles") or [])], m.get("method"), m.get("arrived"))
    det=j.get("detect")
    if isinstance(det,list) and det: print(" detect keys", list(det[-1].keys())[:15])
PY
== run_0001_20261016_174936
  stage7_approach_place [38.6, 57.4, 17.5, 0.1, -89.2, -29.4] None True
 detect keys ['stage', 'source', 'block_xz_m', 'box_xz_m', 'block_score', 'box_score', 'n_captures', 'top_camera_index']
== run_0001_20261015_193907
  stage7_approach_place [38.8, 57.4, 17.5, 0.1, -89.2, -29.4] None True
  stage8_place [41.2, 57.0, 15.0, -7.0, -81.0, -30.0] None True
  stage8_place_zservo_i1 [36.2, 57.0, 20.0, -12.0, -86.0, -30.0] None True
  stage8_place_zservo_i2 [36.2, 57.0, 20.0, -12.0, -86.0, -30.0] None True
  stage8_place_zservo_i3 [36.2, 57.0, 20.0, -12.0, -86.0, -30.0] None True
 detect keys ['stage', 'source', 'block_xz_m', 'box_xz_m', 'block_score', 'box_score', 'n_captures', 'top_camera_index']
== run_0001_20261015_194135
  stage7_approach_place [38.8, 57.4, 17.5, 0.1, -89.2, -29.4] None True
  stage8_place [41.2, 57.0, 15.0, -7.0, -81.0, -30.0] None True
  stage8_place_zservo_i1 [41.2, 57.0, 20.0, -7.0, -86.0, -30.0] None True
  stage8_place_zservo_i2 [41.2, 57.0, 20.0, -7.0, -86.0, -30.0] None True
  stage8_place_zservo_i3 [41.2, 57.0, 15.0, -12.0, -81.0, -30.0] None True
 detect keys ['stage', 'source', 'block_xz_m', 'box_xz_m', 'block_score', 'box_score', 'n_captures', 'top_camera_index']
== run_0001_20261015_194420
  stage7_approach_place [38.8, 57.4, 17.5, 0.1, -89.2, -29.4] None True
  stage8_place [41.6, 57.4, 20.0, 0.1, -89.2, -29.4] None True
  stage8_place_zservo_i1 [44.6, 57.4, 20.0, 0.1, -89.2, -29.4] None True
  stage8_place_zservo_i2 [45.6, 59.1, 17.5, 0.1, -89.2, -29.4] None True
  stage8_place_zservo_i3 [44.6, 57.4, 20.0, 0.1, -89.2, -29.4] None True
 detect keys ['stage', 'source', 'block_xz_m', 'box_xz_m', 'block_score', 'box_score', 'n_captures', 'top_camera_index']
== run_0001_20261015_185536
  stage7_approach_place [38.6, 57.4, 17.5, 0.1, -89.2, -29.4] None True
  stage8_place [41.2, 57.0, 15.0, -7.0, -81.0, -30.0] None True
  stage8_place_zservo_i1 [41.2, 57.0, 20.0, -7.0, -86.0, -30.0] None True
  stage8_place_zservo_i2 [41.2, 57.0, 20.0, -12.0, -86.0, -30.0] None True
  stage8_place_zservo_i3 [36.2, 57.0, 20.0, -12.0, -86.0, -30.0] None True
 detect keys ['stage', 'source', 'block_xz_m', 'box_xz_m', 'block_score', 'box_score', 'n_captures', 'top_camera_index']

うまくいった回の stage8 は [41.2, 57.0, 15.0, -7.0, -81.0, -30.0] で、j1(肩)は 57°、j4(手首)は −81° です。止まった回は [41.22, 67.00, 15.00, -7.00, -101.00, -30.00] で、j1 が 67°、j4 が −101° になっていました。肩を倒して手首を大きく曲げ、手先を高い位置に保とうとする姿勢です。

高さ補正の値を、各試行の記録から並べました。

cd $HOME/mnt/KIOXIA/r2r2r-data/runs/run_pick_place && python3 - <<'PY'
import json,glob,os
rows=[]
for d in sorted(glob.glob("run_0001_2026101[56]_*")):
    p=f"{d}/{d}.json"
    if not os.path.exists(p): continue
    try: j=json.load(open(p))
    except: continue
    h=((j.get("correction") or {}).get("place_height_last")) or {}
    jd=j.get("judgments") or {}
    print(d[-15:], h.get("cell_key"), h.get("dee_y_mm"), h.get("j4_selected_deg"), "place:",(jd.get("place") or {}).get("verdict"),(jd.get("place") or {}).get("human_verdict"))
PY
grep -rl "place_x-200_z200" $HOME/mnt/KIOXIA/r2r2r-data --include=*.json -l 2>/dev/null | grep -v run_pick_place | head
20261015_102101 None None None place: None None
20261015_102402 None None None place: None None
20261015_102449 None None None place: None None
20261015_102528 place_x-180_z160 10.0 -86.0 place: unknown no
20261015_102709 None None None place: None None
20261015_102723 None None None place: None None
20261015_102815 None None None place: None None
20261015_103512 place_x-180_z160 10.0 -86.0 place: yes yes
20261015_104156 place_x-200_z180 25.0 -89.2 place: yes yes
20261015_104405 place_x-180_z220 0.0 -81.0 place: yes yes
20261015_104625 place_x-180_z220 5.0 -81.0 place: no no
20261015_104907 place_x-180_z180 15.0 -86.0 place: yes yes
20261015_105115 place_x-180_z160 9.0 -86.0 place: no no
20261015_165551 place_x-200_z180 24.0 -89.2 place: yes yes
20261015_165825 place_x-200_z180 23.0 -86.0 place: yes yes
20261015_170115 place_x-200_z180 28.0 -86.0 place: yes yes
20261015_173058 None None None place: None None
20261015_173315 place_x-180_z180 20.0 -81.0 place: yes yes
20261015_173928 place_x-180_z180 19.0 -81.0 place: unknown None
20261015_174159 place_x-180_z180 19.0 -81.0 place: no no
20261015_174402 place_x-180_z180 18.0 -86.0 place: no no
20261015_174847 place_x-180_z180 17.0 -86.0 place: no no
20261015_175150 None None None place: None None
20261015_175333 place_x-180_z180 22.0 -89.2 place: yes yes
20261015_175544 place_x-200_z220 0.0 -76.0 place: no no
20261015_175856 place_x-200_z220 0.0 -76.0 place: unknown None
20261015_180056 place_x-200_z200 5.0 -81.0 place: no no
20261015_180309 place_x-220_z160 15.0 -86.0 place: no no
20261015_181752 place_x-220_z180 0.0 -81.0 place: yes yes
20261015_182056 place_x-220_z180 5.0 -81.0 place: yes yes
20261015_182552 place_x-200_z200 5.0 -81.0 place: yes yes
20261015_183310 place_x-200_z200 10.0 -81.0 place: yes yes
20261015_185536 place_x-200_z200 15.0 -81.0 place: yes yes
20261015_193907 place_x-200_z200 14.0 -81.0 place: yes yes
20261015_194135 place_x-200_z200 19.0 -81.0 place: yes yes
20261015_194420 place_x-200_z200 24.0 -89.2 place: yes yes
20261016_174936 place_x-200_z200 29.0 -101.0 place: None None
/sessions/rcw-013hbkstcrwnqtywv99gx9us/mnt/KIOXIA/r2r2r-data/archive/correction_memory_preclean_20260705/correction_memory.json
/sessions/rcw-013hbkstcrwnqtywv99gx9us/mnt/KIOXIA/r2r2r-data/archive/correction_memory_premerge_20260711.json
/sessions/rcw-013hbkstcrwnqtywv99gx9us/mnt/KIOXIA/r2r2r-data/archive/correction_memory_preplacereset_DAY278/place_height_memory.json
/sessions/rcw-013hbkstcrwnqtywv99gx9us/mnt/KIOXIA/r2r2r-data/archive/place_height_memory_premerge_20260711.json
/sessions/rcw-013hbkstcrwnqtywv99gx9us/mnt/KIOXIA/r2r2r-data/archive/place_learning_premerge_20260712/place_release_offset.json
/sessions/rcw-013hbkstcrwnqtywv99gx9us/mnt/KIOXIA/r2r2r-data/diag/place_height_memory.json
/sessions/rcw-013hbkstcrwnqtywv99gx9us/mnt/KIOXIA/r2r2r-data/diag/place_height_memory.json

(/sessions/rcw-…/mnt/KIOXIA は、Claude が Mac 連携で KIOXIA を開いたときのパスで、Mac の /Volumes/KIOXIA と同じ場所です。最後の2行は、同じファイルが2回出ています。)

箱が置かれているセル place_x-200_z200 の補正は、成功した回のたびに 5mm ずつ上がっていました(14mm の回は、試行の間に一度だけ 1mm 下がったためです)。

試行高さ補正置くときの j4
10-15 18:255mm−81°
10-15 18:3310mm−81°
10-15 18:5515mm−81°
10-15 19:4119mm−81°
10-15 19:4424mm−89.2°
10-16 17:4929mm−101°(安全範囲の外)

補正の記憶ファイルの中身です。

cd $HOME/mnt/KIOXIA/r2r2r-data/diag && ls -la place_height_memory.json && python3 -c "
import json;d=json.load(open('place_height_memory.json'))
print(type(d), list(d.keys())[:10] if isinstance(d,dict) else len(d))
c=d.get('cells',d)
for k,v in c.items():
    if 'x-200_z200' in k or 'x-200_z180' in k: print(k, json.dumps(v,ensure_ascii=False)[:600])
"
-rwx------ 1 rcw-013hbkstcrwnqtywv99gx9us rcw-013hbkstcrwnqtywv99gx9us 2662 Oct 15 10:46 place_height_memory.json
<class 'dict'> ['place_x-180_z200', 'place_x-200_z180', 'place_x-220_z160', 'place_x-160_z260', 'place_x-200_z260', 'place_x-160_z160', 'place_x-180_z160', 'place_x-160_z180', 'place_x-180_z180', 'place_x-200_z200']
place_x-200_z180 {"dee_y_mm": 33.0, "n": 9, "last_box_shift_dist_mm": 186.417251216113}
place_x-200_z200 {"dee_y_mm": 29.0, "n": 9, "last_box_shift_dist_mm": 200.3678028156783}

last_box_shift_dist_mm(置く前と置いたあとで、白い箱の位置がどれだけずれたか)が 200mm になっています。箱は 20cm も動いていません。

この補正は、スクリプトの run_pick_place.py の中で、次の関数が更新しています。

def _height_update_ratchet(mem, key, box_shift_dist_mm):
    """ラチェット則で 1 セルの dee_y_mm を更新して mem に書き戻す.
    - box_shift_dist_mm > HEIGHT_HIT_THRESH_MM   → dee_y += HEIGHT_STEP_MM   (速い)
    - box_shift_dist_mm < HEIGHT_CLEAR_THRESH_MM → dee_y -= HEIGHT_DECAY_MM  (遅い)
    - 中間帯 (CLEAR ≤ dist ≤ HIT) は現状維持 (ヒステリシス)
    - dee_y は [0.0, HEIGHT_MAX_MM] にクランプ.
    box_shift_dist_mm が None (検出失敗) の場合は上位で呼ばず, 呼ばれても現状維持.
    戻り値: 更新後エントリ dict {'dee_y_mm', 'n'}.
    """
    if box_shift_dist_mm is None:
        # 上位が守るはず (呼ばない) だが二重に安全側.
        e = mem.get(key)
        if isinstance(e, dict):
            return e
        mem[key] = {"dee_y_mm": 0.0, "n": 0}
        return mem[key]
    e = mem.get(key)
    if e is not None and not isinstance(e, dict):
        print("[HGT  ] [WARN] mem[{}] が dict 以外 ({}). 破棄して再初期化"
              .format(key, type(e).__name__))
        e = None
    if e is None:
        old_dee_y = 0.0
        n = 0
    else:
        try:
            old_dee_y = float(e.get("dee_y_mm", 0.0))
        except (TypeError, ValueError):
            old_dee_y = 0.0
        try:
            n = int(e.get("n", 0))
        except (TypeError, ValueError):
            n = 0
    d = float(box_shift_dist_mm)
    if d > HEIGHT_HIT_THRESH_MM:
        new_dee_y = old_dee_y + HEIGHT_STEP_MM
    elif d < HEIGHT_CLEAR_THRESH_MM:
        new_dee_y = old_dee_y - HEIGHT_DECAY_MM
    else:
        new_dee_y = old_dee_y
    new_dee_y = max(0.0, min(HEIGHT_MAX_MM, new_dee_y))
    mem[key] = {"dee_y_mm": new_dee_y, "n": n + 1,
                "last_box_shift_dist_mm": d}
    return mem[key]

使っている定数です。

# box_shift bimodal(10mm 非当たり / 216mm 当たり) の谷を HEIGHT_HIT_THRESH に置く.
# HEIGHT_MAX_MM=40 は箱縁+ブロック厚(24mm)+余裕. これ以上は上げない.
PLACE_HEIGHT_MEM_PATH = Path(
    "/Volumes/KIOXIA/r2r2r-data/diag/place_height_memory.json")
HEIGHT_HIT_THRESH_MM   = 50.0    # box_shift_dist > これ で「当たった」= 上げる
HEIGHT_CLEAR_THRESH_MM = 20.0    # box_shift_dist < これ で「明確に非当たり」= 下げる
HEIGHT_STEP_MM         = 5.0     # 一段の上げ (速い). 当たり続ければ +5mm 積む
HEIGHT_DECAY_MM        = 1.0     # 一段の下げ (遅い. STEP の 1/5)
HEIGHT_MAX_MM          = 40.0    # 上限. 箱縁+ブロック厚+余裕. これ以上は上げない

箱のずれが 50mm を超えると「ブロックが箱に当たって、箱を押した」とみなして 5mm 上げ、20mm 未満なら 1mm 下げる、という決まりです。上げるのは速く、下げるのは遅くしてあります。

台帳(全試行の記録を1行ずつ残している CSV)で、箱のずれの記録を見ました。

cd $HOME/mnt/KIOXIA/r2r2r-data/runs && python3 - <<'PY'
import csv
r=list(csv.DictReader(open("pick_place_ledger_v3.csv")))
for x in r[-30:]:
    print(x.get("run_stem","")[-15:], "shift", x.get("box_shift_dx"), x.get("box_shift_dz"), x.get("box_shift_dist"), "hgt", x.get("place_height_dee_y_mm"), x.get("box_shift_reason"), "in_box", x.get("in_box_geom"))
PY
20261015_104156 shift 4.72 -0.04 4.72 hgt 25.00 ok in_box False
20261015_104405 shift -57.33 -216.41 223.87 hgt 0.00 ok in_box True
20261015_104625 shift -61.01 -213.96 222.49 hgt 5.00 ok in_box False
20261015_104907 shift -57.93 -187.39 196.14 hgt 15.00 ok in_box True
20261015_105115 shift -43.97 18.37 47.66 hgt 9.00 ok in_box False
20261015_165551 shift 11.65 -7.30 13.75 hgt 24.00 ok in_box True
20261015_165825 shift -30.71 -184.06 186.61 hgt 23.00 ok in_box True
20261015_170115 shift -31.06 -183.81 186.42 hgt 28.00 ok in_box True
20261015_173058 shift    hgt  place_img_none in_box 
20261015_173315 shift 1.35 -4.94 5.12 hgt 20.00 ok in_box True
20261015_173928 shift    hgt 19.00 place_img_none in_box 
20261015_174159 shift 1.97 -0.03 1.97 hgt 19.00 ok in_box False
20261015_174402 shift 1.98 0.46 2.03 hgt 18.00 ok in_box False
20261015_174847 shift -49.06 -174.76 181.52 hgt 17.00 ok in_box False
20261015_175150 shift    hgt  place_img_none in_box 
20261015_175333 shift 1.87 -3.47 3.94 hgt 22.00 ok in_box True
20261015_175544 shift 14.77 1.70 14.87 hgt 0.00 ok in_box False
20261015_175856 shift    hgt 0.00 place_img_none in_box 
20261015_180056 shift 4.92 40.07 40.37 hgt 5.00 ok in_box True
20261015_180309 shift -25.21 26.63 36.67 hgt 15.00 ok in_box False
20261015_181752 shift -20.81 -177.63 178.84 hgt 0.00 ok in_box True
20261015_182056 shift -17.90 -174.91 175.82 hgt 5.00 ok in_box True
20261015_182552 shift -42.64 -196.42 201.00 hgt 5.00 ok in_box True
20261015_183310 shift -40.00 -196.58 200.61 hgt 10.00 ok in_box True
20261015_185536 shift 1.49 0.49 1.56 hgt 15.00 ok in_box False
20261015_193907 shift -41.69 -197.02 201.38 hgt 14.00 ok in_box True
20261015_194135 shift -46.14 -197.28 202.60 hgt 19.00 ok in_box True
20261015_194420 shift -40.01 -196.33 200.37 hgt 24.00 ok in_box True
20261016_174936 shift    hgt 29.00 place_img_none in_box 
20261016_175123 shift    hgt 29.00 place_img_none in_box 

ブロックが箱に入った回(in_box True)のほとんどで、箱が奥行き方向(z)に約 −175〜−215mm ずれたと記録されています。実物の箱は固定してあり、動いていません。ブロックが入ったあとの画像で、白い箱の位置を別の場所として測っていると考えています。つまり「箱に入った」ほど「箱に当たった」と判定され、高さが上がる、という逆向きの学習になっていました。

箱の位置の測り違いそのものは、まだ直していません。白い箱を見つける処理(06f_top_white_box_detect_v7.py)にも、この回とは別の、まだコミットされていない変更が入っています。お手本を集め終わってから調べます。

高さ補正を 15mm に戻し、お手本を記録している間は更新しないようにした

直し方は2つ組み合わせました。

  1. 補正の記憶を、前の日に5回続けて成功した 15mm に戻す。箱のセルが隣の place_x-200_z180(33mm)と判定されることもあるので、2つとも 15mm にする
  2. お手本を記録している間(--record-demo を付けたとき)は、補正を読むだけにして、書き換えない

1だけだと、3回成功したところでまた同じ所で止まります。2は、置く高さが毎回そろうので、π0.5 の学習データとしても扱いやすくなります。アームの動かし方そのものには触れていません。

記憶ファイルは、先に KIOXIA の archive/ へ写してから書き換えました。

set -e
D=$HOME/mnt/KIOXIA/r2r2r-data
mkdir -p $D/archive/place_height_memory_predemo_20261016
cp -n $D/diag/place_height_memory.json $D/archive/place_height_memory_predemo_20261016/place_height_memory.json
python3 - <<'PY'
import json, os
p=os.path.expanduser("~/mnt/KIOXIA/r2r2r-data/diag/place_height_memory.json")
m=json.load(open(p))
for k in ("place_x-200_z200","place_x-200_z180"):
    print(k, m[k]["dee_y_mm"], "→ 15.0"); m[k]["dee_y_mm"]=15.0
json.dump(m, open(p,"w"), ensure_ascii=False, indent=2)
PY
place_x-200_z200 29.0 → 15.0
place_x-200_z180 33.0 → 15.0

run_pick_place.py の変更は7行です。変更前のファイルは run_pick_place.py.bak_20261016_pre_freeze_height に残しました。_DEMO_TAP_INSTALLED は、--record-demo を付けて起動したときに True になる変数です(前回 Log#4 で追加)。

diff --git a/dynamic_tracking_limits/place/place_modules/run_pick_place.py b/dynamic_tracking_limits/place/place_modules/run_pick_place.py
index 6cf41fc..febc2be 100644
--- a/dynamic_tracking_limits/place/place_modules/run_pick_place.py
+++ b/dynamic_tracking_limits/place/place_modules/run_pick_place.py
@@ -4658,6 +4658,13 @@ def run(dry_run=False, dummy_block_xz=None, dummy_box_xz=None,
                 _dx = (float(_bxl[0]) - float(box_xz[0])) * 1000.0
                 _dz = (float(_bxl[1]) - float(box_xz[1])) * 1000.0
                 _box_shift_dist_mm = (_dx * _dx + _dz * _dz) ** 0.5
+            # 2026-10-16: --record-demo 中は高さ補正記憶を読むだけで更新しない.
+            #   箱検出の測り違い (box_shift≈200mm) で成功のたびに +5mm 積み上がり,
+            #   j4 が J4_SAFE 外になって stage8 で止まったため. 置く高さをそろえる目的も兼ねる.
+            if _box_shift_dist_mm is not None and _DEMO_TAP_INSTALLED:
+                print("[HGT  ] record-demo: 高さ補正の更新を停止中 (dist={:.1f}mm, 記憶は不変)"
+                      .format(_box_shift_dist_mm))
+            elif _box_shift_dist_mm is not None:
                 try:
                     _hmem = _height_load()
                     _hkey = _height_key(float(box_xz[0]), float(box_xz[1]))

run() 関数は 5,000 行を超えるので、変更した箇所を含むまとまり(置いたあとの撮影から、置く判定まで)を、変更後の形で全部載せます。

        else:
            place_img = _capture_for_vlm(
                "place", frames_dir, log_record, dry_run, run_stem=run_stem)
            _observe_capture_push(log_record)
            _OBSERVE_STATE["cap_place"] = (str(place_img) if place_img else None)
            # 観測: 同じ画像に赤ブロック HSV 検出を脇でかける(既存判定/分岐は不変)
            _OBSERVE_STATE["block_place_xz"] = _detect_block_in_place_img(place_img)
            # DAY274: 同じ画像に白箱検出も脇でかける (stage2 と同一検出器 06f).
            # 撮影・送角・グリッパー・sleep は追加しない (画像は既に手元にある).
            # 検出失敗 (None) は _append_ledger 側で box_shift 3 列を空欄にする.
            _OBSERVE_STATE["box_xz_last"] = _detect_white_box_in_place_img(place_img)
            # DAY274 層2: box_shift_dist を計算し, 高さ補正記憶をラチェット更新.
            # box_xz (stage2 検出) と box_xz_last (退避後) 両方揃った時のみ.
            # 検出失敗 (None) や box_xz 欠損時は更新スキップ (現状維持).
            # PLACE 補正 (XZ) と独立記憶 (別 json). USE_CORRECTION=False で完全停止.
            _bxl = _OBSERVE_STATE.get("box_xz_last")
            _box_shift_dist_mm = None
            if (USE_CORRECTION
                    and isinstance(box_xz, (list, tuple)) and len(box_xz) == 2
                    and isinstance(box_xz[0], (int, float))
                    and isinstance(box_xz[1], (int, float))
                    and isinstance(_bxl, (list, tuple)) and len(_bxl) == 2
                    and isinstance(_bxl[0], (int, float))
                    and isinstance(_bxl[1], (int, float))):
                _dx = (float(_bxl[0]) - float(box_xz[0])) * 1000.0
                _dz = (float(_bxl[1]) - float(box_xz[1])) * 1000.0
                _box_shift_dist_mm = (_dx * _dx + _dz * _dz) ** 0.5
            # 2026-10-16: --record-demo 中は高さ補正記憶を読むだけで更新しない.
            #   箱検出の測り違い (box_shift≈200mm) で成功のたびに +5mm 積み上がり,
            #   j4 が J4_SAFE 外になって stage8 で止まったため. 置く高さをそろえる目的も兼ねる.
            if _box_shift_dist_mm is not None and _DEMO_TAP_INSTALLED:
                print("[HGT  ] record-demo: 高さ補正の更新を停止中 (dist={:.1f}mm, 記憶は不変)"
                      .format(_box_shift_dist_mm))
            elif _box_shift_dist_mm is not None:
                try:
                    _hmem = _height_load()
                    _hkey = _height_key(float(box_xz[0]), float(box_xz[1]))
                    _new_entry = _height_update_ratchet(
                        _hmem, _hkey, _box_shift_dist_mm)
                    if _height_save(_hmem):
                        print("[HGT  ] update {} dist={:.1f}mm → dee_y={:.1f}mm "
                              "(n={})".format(
                                  _hkey, _box_shift_dist_mm,
                                  _new_entry.get("dee_y_mm", 0.0),
                                  _new_entry.get("n", 0)))
                except Exception as e:
                    print("[HGT  ] [WARN] ratchet update 例外: {}: {}".format(
                        type(e).__name__, e))
            place_vlm = _judge_image(
                place_img, "place", dry_run, dummy_place_verdict)
            place_vlm_verdict = place_vlm["verdict"]
            place_human_verdict = None
            if human_judge and not dry_run:
                place_human_verdict = _ask_human_verdict(
                    place_img, place_vlm, kind="place")
            place_adopted = place_vlm_verdict   # 分岐は常に VLM。記録のみ人
            _observe_verdict("place", place_adopted, place_vlm["confidence"], place_vlm["reason"])
            # 非移動 stage の通知 emit(stage9b 成功ブランチ→画面に verdict 行)
            _observe_emit_now()

変更は、試行を重ねて20本そろったところでコミットし、GitHub へ送りました。

commit b5cc44ffc212f04ea28e3390dc44d10f26645605
Author: Eiichi Shimazu <studio@adaptdesign.jp>
Date:   Fri Oct 16 10:47:32 2026 +0000

record-demo 中は PLACE 高さ補正記憶を更新しない

箱検出の測り違い (box_shift_dist が成功回でも約 180〜220mm) により,
ラチェット則が成功のたびに「当たった」と判断して dee_y を +5mm 積み上げていた.
セル place_x-200_z200 は 2026-10-15 の 15mm から 2026-10-16 に 29mm まで上がり,
stage8 の候補姿勢が j4=-101° となって送信前ガード (J4_SAFE[-90,60]) で停止した.

--record-demo 指定時は高さ補正を読むだけにし, 更新はログ表示のみ.
置く高さが毎回そろうため, π0.5 の学習データとしても扱いやすい.
記憶ファイルは 15mm に戻し, 元ファイルは KIOXIA archive/place_height_memory_predemo_20261016/ に退避.
修正後: 12 回中 9 回成功, 高さ補正は最後まで 15mm のまま. 成功エピソード合計 20 本.

箱検出の測り違いそのものは未修正 (データ集めの後で対応).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hBKsTcrwnQTywv99gx9US


 dynamic_tracking_limits/place/place_modules/run_pick_place.py | 7 +++++++
 1 file changed, 7 insertions(+)

このコミットは Claude が Mac 連携のシェルから作りました。そのシェルは接続したフォルダの中のファイルを消せない設定のため、git が後片付けで消すはずの .git/HEAD.lock などが残りました。残ったままだと次の git 操作が「File exists」で失敗するので、.git の中で名前を変えて置いています(stale-HEAD-lock-20261016 など)。

cd $HOME/mnt/r2r2r-scripts/.git && mv HEAD.lock stale-HEAD-lock-20261016 && { [ -e objects/maintenance.lock ] && mv objects/maintenance.lock stale-maintenance-lock-20261016 || true; } ; cd .. && git status -sb | head -1 && git log --oneline -1 && find .git -name "*.lock" | head
## main...origin/main [ahead 1]
b5cc44f record-demo 中は PLACE 高さ補正記憶を更新しない

GitHub への送信は Mac のターミナルから行いました。

cd ~/r2r2r-scripts && git push origin main

修正後は12回中9回成功し、成功のお手本は20本になった

直してからの試行では、置いたあとに [HGT ] record-demo: 高さ補正の更新を停止中 が出て、補正は最後まで 15mm のままでした。手首を休ませるため、3回動かすごとに HOME で5分止めています。

お手本の本数は、記録を学習用の形に直すスクリプト(pi05/build_demo_episodes.py、Log#4 で作成)で数えます。人の判定が「つかめた」「箱に入った」の両方 yes の試行だけを採用します。

cd ~/r2r2r-scripts && python3 pi05/build_demo_episodes.py

19本の時点の出力です(Claude が Mac 連携のシェルで、KIOXIA のパスを読み替えて実行)。

[-- ] run_0001_20261015_173058    human_verdict pick=None place=None
[OK ] run_0001_20261015_173315  307  既存 (スキップ・--force で再生成)
[-- ] run_0001_20261015_173928    human_verdict pick=yes place=None
[-- ] run_0001_20261015_174159    human_verdict pick=no place=no
[-- ] run_0001_20261015_174402    human_verdict pick=yes place=no
[-- ] run_0001_20261015_174847    human_verdict pick=no place=no
[-- ] run_0001_20261015_175150    human_verdict pick=None place=None
[OK ] run_0001_20261015_175333  293  既存 (スキップ・--force で再生成)
[-- ] run_0001_20261015_175544    human_verdict pick=no place=no
[-- ] run_0001_20261015_175856    human_verdict pick=yes place=None
[-- ] run_0001_20261015_180056    human_verdict pick=yes place=no
[-- ] run_0001_20261015_180309    human_verdict pick=yes place=no
[OK ] run_0001_20261015_181752  291  既存 (スキップ・--force で再生成)
[OK ] run_0001_20261015_182056  279  既存 (スキップ・--force で再生成)
[OK ] run_0001_20261015_182552  663  既存 (スキップ・--force で再生成)
[OK ] run_0001_20261015_183310  294  既存 (スキップ・--force で再生成)
[OK ] run_0001_20261015_185536  267  既存 (スキップ・--force で再生成)
[OK ] run_0001_20261015_193907  282  既存 (スキップ・--force で再生成)
[OK ] run_0001_20261015_194135  524  既存 (スキップ・--force で再生成)
[OK ] run_0001_20261015_194420  287  既存 (スキップ・--force で再生成)
[-- ] run_0001_20261016_174936    human_verdict pick=yes place=None
[-- ] run_0001_20261016_175123    human_verdict pick=yes place=None
[OK ] run_0001_20261016_175444  328  ok
[OK ] run_0001_20261016_175708  289  ok
[OK ] run_0001_20261016_180013  271  ok
[-- ] run_0001_20261016_180233    human_verdict pick=yes place=no
[-- ] run_0001_20261016_180957    human_verdict pick=yes place=no
[OK ] run_0001_20261016_184725  274  ok
[OK ] run_0001_20261016_185201  266  ok
[-- ] run_0001_20261016_185515    human_verdict pick=yes place=no
[OK ] run_0001_20261016_190925  227  ok
[OK ] run_0001_20261016_191539  290  ok
[OK ] run_0001_20261016_191942  276  ok
[OK ] run_0001_20261016_192229  308  ok

採用エピソード合計: 19  総ステップ: 6016  → /sessions/rcw-013hbkstcrwnqtywv99gx9us/mnt/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_scripted_v1/raw_episodes

もう1回動かして成功し、20本になりました。

[OK ] run_0001_20261016_191539  290  既存 (スキップ・--force で再生成)
[OK ] run_0001_20261016_191942  276  既存 (スキップ・--force で再生成)
[OK ] run_0001_20261016_192229  308  既存 (スキップ・--force で再生成)
[OK ] run_0001_20261016_194238  282  ok

採用エピソード合計: 20  総ステップ: 6298  → /sessions/rcw-013hbkstcrwnqtywv99gx9us/mnt/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_scripted_v1/raw_episodes

箱に入らなかった3回(18:02・18:09・18:55)は、VLM(画像を見て成否を判定する AI)の判定も人の判定も、どちらも n でした。

学習には20本のうち19本を使います。run_0001_20261015_182552 は、2回つかみ損ねて3回目につかめた回で、長さも 663 ステップとほかの倍あります。「つかみ直す」動きは、少ない本数では動きが迷う原因になりうるので、最初の学習からは外しました。

Runpod の A40 で Pod を作り、19本のお手本を送った

手順は前回 Log#4 で作った手順書 pi05/RUNPOD_TRAIN.md のとおりです。変えたのは、使う本数(19本)と、学習の長さ(手順書の 5,000 ステップを 2,000 ステップに)の2点です。

まず、Mac で19本を1つにまとめます。

(base) shimazueiichi@shimadzueiichinoMacBook-Pro ~ % cd /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_scripted_v1 && tar cf ~/mecharm_raw_episodes.tar --exclude 'raw_episodes/run_0001_20261015_182552' raw_episodes
tar tf ~/mecharm_raw_episodes.tar | grep -c 'meta.json'
ls -lh ~/mecharm_raw_episodes.tar
19
-rw-r--r--  1 shimazueiichi  staff   147M 10月 16 19:56 /Users/shimazueiichi/mecharm_raw_episodes.tar
(base) shimazueiichi@shimadzueiichinoMacBook-Pro mecharm_redblock_box_scripted_v1 %

Pod(Runpod で借りる GPU つきの計算機)の設定は、GPU が A40(48GB)、テンプレートが Runpod Pytorch 2.8.0、Container Disk が 200GB、Volume(Pod を消しても残る保存領域)はなしです。料金は1時間 0.62 ドル(GPU 0.59 ドル+ディスク 0.028 ドル)と表示されました。

Runpod の Pod 作成画面。テンプレートは Runpod Pytorch 2.8.0、GPU は A40 1台、1時間 0.62 ドルと表示されている
同じ画面の下側。On-Demand の料金 0.59 ドル/時と、Container disk を 200GB にした設定

作る前に「Set overrides」で、外から SSH で入るための TCP ポート 22 が公開されているかを確かめました。前回は作ったあとに追加したため、Pod が再起動して中身とポート番号が変わりました。今回は最初から 22 が入っていました。

テンプレートの上書き設定。SSH terminal access にチェック、Exposed TCP ports に 22、Container disk 200GB

ここから下のログでは、Pod の IP アドレスとポート番号を <POD_IP> と <SSH_PORT> に置き換えています。Mac から SSH で Pod に入ります。

(base) shimazueiichi@shimadzueiichinoMacBook-Pro mecharm_redblock_box_scripted_v1 % ssh root@<POD_IP> -p <SSH_PORT> -i ~/.ssh/id_ed25519
The authenticity of host '[<POD_IP>]:<SSH_PORT> ([<POD_IP>]:<SSH_PORT>)' can't be established.
ED25519 key fingerprint is: SHA256:/AUQg4o2nZ4+wPcEu9sivKEYnGobxFmsQkhpDZu/do4
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '[<POD_IP>]:<SSH_PORT>' (ED25519) to the list of known hosts.
Welcome to Ubuntu 24.04.3 LTS (GNU/Linux 6.8.0-134-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

This system has been minimized by removing packages and content that are
not required on a system that users do not log into.

To restore this content, you can run the 'unminimize' command.

The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

 _____                             _ 
|  __ \                           | |
| |__) |   _ _ __  _ __   ___   __| |
|  _  / | | | '_ \| '_ \ / _ \ / _` |
| | \ \ |_| | | | | |_) | (_) | (_| |
|_|  \_\__,_|_| |_| .__/ \___/ \__,_|
                  | |                
                  |_|                
For detailed documentation and guides, please visit:
https://docs.runpod.io/ and https://blog.runpod.io/


root@896724c37bbf:/# 

Pod の中で、tmux(SSH が切れても作業を続けられる画面)を入れて、openpi(π0.5 を学習・推論するための、Physical Intelligence 社のプログラム一式)を用意します。

apt-get update && apt-get install -y tmux
mkdir -p /workspace/logs /workspace/r2r2r /workspace/data
tmux new -s lab
cd /workspace
git clone --recurse-submodules https://github.com/Physical-Intelligence/openpi.git
cd openpi && git checkout 215abfb
curl -LsSf https://astral.sh/uv/install.sh | sh
source $HOME/.local/bin/env
GIT_LFS_SKIP_SMUDGE=1 uv sync

uv sync を待つ間に、Mac の別のターミナルからお手本とスクリプトを送りました。

scp -P <SSH_PORT> -i ~/.ssh/id_ed25519 ~/mecharm_raw_episodes.tar root@<POD_IP>:/workspace/data/
scp -P <SSH_PORT> -i ~/.ssh/id_ed25519 -r ~/r2r2r-scripts/pi05 root@<POD_IP>:/workspace/r2r2r/

uv sync の最後と、展開の出力です。

 + werkzeug==3.1.3
 + widgetsnbextension==4.0.14
 + wrapt==1.14.1
 + xxhash==3.5.0
 + yarl==1.20.0
 + zarr==3.0.8
 + zipp==3.22.0
root@896724c37bbf:/workspace/openpi# cd /workspace/data && tar xf mecharm_raw_episodes.tar && ls raw_episodes | wc -l
19
root@896724c37bbf:/workspace/data#

19本・5,635 コマを LeRobot 形式に変換し、正規化の統計を取った

LeRobot 形式は、ロボットの学習データを保存するための共通の形式です。openpi はこの形式を読みます。続けて、関節角などの値の平均と広がりを全データから計算します(正規化の統計)。学習のときに値の大きさをそろえるために使います。

cd /workspace/openpi
python3 /workspace/r2r2r/pi05/openpi_addon/install_addon.py /workspace/openpi
uv run python /workspace/r2r2r/pi05/convert_to_lerobot.py --raw-dir /workspace/data/raw_episodes
uv run scripts/compute_norm_stats.py --config-name pi05_mecharm_lora

出力の最後です。

Creating parquet from Arrow format: 100%|█████████████████████████████| 4/4 [00:00<00:00, 119.65ba/s]
  run_0001_20261016_192229: 308 steps
Map: 100%|████████████████████████████████████████████████| 282/282 [00:00<00:00, 1167.54 examples/s]
Creating parquet from Arrow format: 100%|██████████████████████████████| 3/3 [00:00<00:00, 94.66ba/s]
  run_0001_20261016_194238: 282 steps
done: 19 episodes / 5635 frames → /workspace/.cache/huggingface/lerobot/local/mecharm_redblock_box_v1
100%|██████████████████████████████████████████████████████████| 4.07M/4.07M [00:01<00:00, 4.26MiB/s]
Resolving data files: 100%|██████████████████████████████████████| 19/19 [00:00<00:00, 135070.81it/s]
Downloading data: 100%|████████████████████████████████████████| 19/19 [00:00<00:00, 21211.55files/s]
Generating train split: 5635 examples [00:01, 4416.83 examples/s]
Computing stats: 100%|█████████████████████████████████████████████| 176/176 [00:44<00:00,  3.99it/s]
Writing stats to: /workspace/openpi/assets/pi05_mecharm_lora/local/mecharm_redblock_box_v1
root@896724c37bbf:/workspace/openpi# cd /workspace

5,635 コマは、20本の合計 6,298 ステップから、外した1本(663 ステップ)を引いた数です。1コマは 0.1 秒(10Hz)です。

2,000 ステップの学習は4時間3分、loss は 0.1155 から 0.0054 へ下がった

学習は LoRA(モデル全体ではなく、追加した小さな部品だけを学習する方法)で行います。手順書のコマンドに --num-train-steps=2000 を足しました。

cd /workspace/openpi
XLA_PYTHON_CLIENT_MEM_FRACTION=0.9 uv run scripts/train.py pi05_mecharm_lora --exp-name=mecharm_v1 --overwrite --num-train-steps=2000 2>&1 | tee /workspace/logs/train.log

始まったときの画面の最後です。学習するパラメータの一覧が出たあと、進み具合の表示が始まります。

['PaliGemma']['llm']['layers']['attn']['kv_einsum_1']['lora_b'].value: (18, 2, 1, 32, 256)@float32
['PaliGemma']['llm']['layers']['attn']['kv_einsum_1']['w'].value: (18, 2, 1, 1024, 256)@bfloat16
['PaliGemma']['llm']['layers']['attn']['q_einsum']['lora_a'].value: (18, 8, 2048, 16)@float32
['PaliGemma']['llm']['layers']['attn']['q_einsum']['lora_b'].value: (18, 8, 16, 256)@float32
['PaliGemma']['llm']['layers']['attn']['q_einsum']['w'].value: (18, 8, 2048, 256)@bfloat16
['PaliGemma']['llm']['layers']['attn']['q_einsum_1']['lora_a'].value: (18, 8, 1024, 32)@float32
['PaliGemma']['llm']['layers']['attn']['q_einsum_1']['lora_b'].value: (18, 8, 32, 256)@float32
['PaliGemma']['llm']['layers']['attn']['q_einsum_1']['w'].value: (18, 8, 1024, 256)@bfloat16
['PaliGemma']['llm']['layers']['mlp']['gating_einsum'].value: (18, 2, 2048, 16384)@bfloat16
['PaliGemma']['llm']['layers']['mlp']['gating_einsum_lora_a'].value: (18, 2, 2048, 16)@float32
['PaliGemma']['llm']['layers']['mlp']['gating_einsum_lora_b'].value: (18, 2, 16, 16384)@float32
['PaliGemma']['llm']['layers']['mlp']['linear'].value: (18, 16384, 2048)@bfloat16
['PaliGemma']['llm']['layers']['mlp']['linear_lora_a'].value: (18, 16384, 16)@float32
['PaliGemma']['llm']['layers']['mlp']['linear_lora_b'].value: (18, 16, 2048)@float32
['PaliGemma']['llm']['layers']['mlp_1']['gating_einsum'].value: (18, 2, 1024, 4096)@bfloat16
['PaliGemma']['llm']['layers']['mlp_1']['gating_einsum_lora_a'].value: (18, 2, 1024, 32)@float32
['PaliGemma']['llm']['layers']['mlp_1']['gating_einsum_lora_b'].value: (18, 2, 32, 4096)@float32
['PaliGemma']['llm']['layers']['mlp_1']['linear'].value: (18, 4096, 1024)@bfloat16
['PaliGemma']['llm']['layers']['mlp_1']['linear_lora_a'].value: (18, 4096, 32)@float32
['PaliGemma']['llm']['layers']['mlp_1']['linear_lora_b'].value: (18, 32, 1024)@float32
['PaliGemma']['llm']['layers']['pre_attention_norm']['scale'].value: (18, 2048)@bfloat16
['PaliGemma']['llm']['layers']['pre_attention_norm_1']['Dense_0']['bias'].value: (18, 3072)@bfloat16
['PaliGemma']['llm']['layers']['pre_attention_norm_1']['Dense_0']['kernel'].value: (18, 1024, 3072)@bfloat16
['PaliGemma']['llm']['layers']['pre_ffw_norm']['scale'].value: (18, 2048)@bfloat16
['PaliGemma']['llm']['layers']['pre_ffw_norm_1']['Dense_0']['bias'].value: (18, 3072)@bfloat16
['PaliGemma']['llm']['layers']['pre_ffw_norm_1']['Dense_0']['kernel'].value: (18, 1024, 3072)@bfloat16
['action_in_proj']['bias'].value: (1024,)@float32
['action_in_proj']['kernel'].value: (32, 1024)@float32
['action_out_proj']['bias'].value: (32,)@float32
['action_out_proj']['kernel'].value: (1024, 32)@float32
['time_mlp_in']['bias'].value: (1024,)@float32
['time_mlp_in']['kernel'].value: (1024, 1024)@float32
['time_mlp_out']['bias'].value: (1024,)@float32
['time_mlp_out']['kernel'].value: (1024, 1024)@float32 (3312:train.py:238)
12:04:56.518 [I] Progress on: -/2000 rate:- remaining:? elapsed:00:00 postfix:-                   (3312:tqdm_logging.py:145)

時刻は Pod の時計(UTC)で、12:04 は日本時間の 21:04 です。

loss(モデルの予測と、お手本の動きとの差の大きさ)は、50ステップごとに記録されます。全40行です。

grep -a "loss=" /workspace/logs/train.log
Step 0: grad_norm=1.2376, loss=0.1155, param_norm=1803.7705
Step 50: grad_norm=0.2924, loss=0.0555, param_norm=1803.7704
Step 100: grad_norm=0.1608, loss=0.0390, param_norm=1803.7712
Step 150: grad_norm=0.1604, loss=0.0307, param_norm=1803.7732
Step 200: grad_norm=0.1313, loss=0.0240, param_norm=1803.7778
Step 250: grad_norm=0.1037, loss=0.0197, param_norm=1803.7847
Step 300: grad_norm=0.0882, loss=0.0159, param_norm=1803.7937
Step 350: grad_norm=0.0829, loss=0.0154, param_norm=1803.8044
Step 400: grad_norm=0.0751, loss=0.0136, param_norm=1803.8147
Step 450: grad_norm=0.0692, loss=0.0119, param_norm=1803.8256
Step 500: grad_norm=0.0720, loss=0.0128, param_norm=1803.8364
Step 550: grad_norm=0.0683, loss=0.0134, param_norm=1803.8474
Step 600: grad_norm=0.0635, loss=0.0119, param_norm=1803.8584
Step 650: grad_norm=0.0597, loss=0.0100, param_norm=1803.8677
Step 700: grad_norm=0.0580, loss=0.0096, param_norm=1803.8763
Step 750: grad_norm=0.0562, loss=0.0088, param_norm=1803.8862
Step 800: grad_norm=0.0618, loss=0.0095, param_norm=1803.8965
Step 850: grad_norm=0.0596, loss=0.0085, param_norm=1803.9073
Step 900: grad_norm=0.0603, loss=0.0088, param_norm=1803.9165
Step 950: grad_norm=0.0494, loss=0.0067, param_norm=1803.9255
Step 1000: grad_norm=0.0570, loss=0.0082, param_norm=1803.9341
Step 1050: grad_norm=0.0505, loss=0.0075, param_norm=1803.9431
Step 1100: grad_norm=0.0522, loss=0.0071, param_norm=1803.9517
Step 1150: grad_norm=0.0542, loss=0.0081, param_norm=1803.9604
Step 1200: grad_norm=0.0576, loss=0.0072, param_norm=1803.9678
Step 1250: grad_norm=0.0505, loss=0.0069, param_norm=1803.9767
Step 1300: grad_norm=0.0494, loss=0.0059, param_norm=1803.9846
Step 1350: grad_norm=0.0561, loss=0.0081, param_norm=1803.9933
Step 1400: grad_norm=0.0479, loss=0.0060, param_norm=1804.0012
Step 1450: grad_norm=0.0490, loss=0.0067, param_norm=1804.0078
Step 1500: grad_norm=0.0500, loss=0.0057, param_norm=1804.0151
Step 1550: grad_norm=0.0531, loss=0.0066, param_norm=1804.0232
Step 1600: grad_norm=0.0524, loss=0.0062, param_norm=1804.0297
Step 1650: grad_norm=0.0462, loss=0.0059, param_norm=1804.0349
Step 1700: grad_norm=0.0472, loss=0.0057, param_norm=1804.0409
Step 1750: grad_norm=0.0451, loss=0.0057, param_norm=1804.0475
Step 1800: grad_norm=0.0463, loss=0.0057, param_norm=1804.0531
Step 1850: grad_norm=0.0530, loss=0.0065, param_norm=1804.0593
Step 1900: grad_norm=0.0482, loss=0.0052, param_norm=1804.0647
Step 1950: grad_norm=0.0416, loss=0.0054, param_norm=1804.0697

最初の約300ステップで 0.1155 から 0.0159 まで下がり、そのあとはゆっくり下がって、最後は 0.005 前後で落ち着きました。

学習の途中で、Mac と Pod の SSH が時間切れで切れています。tmux の中で動かしていたので、学習は止まらずに続きました。

13:42:47.939 [I] Progress on: 786it/2.00kit rate:7.1s/it remaining:2:24:29 elapsed:1:37:51 postfix:-
(3312:tqdm_logging.py:145)
13:43:02.228 [I] Progress on: 788it/2.00kit rate:7.1s/it remaining:2:24:16 elapsed:1:38:05 postfix:-
(3312:tqdm_logging.py:145)
13:43:16.510 [I] Progress on: 790it/2.00kit rate:7.1s/it remaining:2:24:00 elapsed:1:38:19 postfix:-
(3312:tqdm_logging.py:145)
13:43:30.793 [I] Progress on: 792it/2.00kit rate:7.1s/it remaining:2:23:45 elapsed:1:38:34 postfix:-
(3312:tqdm_logging.py:145)
Read from remote host <POD_IP>: Operation timed out
Connection to <POD_IP> closed.
client_loop: send disconnect: Broken pipe

途中の保存と、最後の保存の記録です。

14:08:42.183 [I] Finished asynchronous save (blocking + background) in 11.08 seconds to /workspace/openpi/checkpoints/pi05_mecharm_lora/mecharm_v1/1000 (3312:async_checkpointer.py:412)
16:07:44.943 [I] Finished asynchronous save (blocking + background) in 15.36 seconds to /workspace/openpi/checkpoints/pi05_mecharm_lora/mecharm_v1/1999 (3312:async_checkpointer.py:412)
16:07:40.991 [I] Progress on: 2.00kit/2.00kit rate:10.6s/it remaining:00:00 elapsed:4:02:44 postfix:- (3312:tqdm_logging.py:145)

かかった時間は4時間2分44秒です。途中の表示では1ステップ約 7.1 秒(A40・バッチ32)でした。最後の行の rate:10.6s/it は、保存の時間を含んだ最後の区間の値です。前回の試し(2本・200ステップ)では1ステップ約 6.6 秒でした。

朝、SSH で入り直して、終わったことを確かめました。

root@896724c37bbf:/# grep -a "loss=" /workspace/logs/train.log | sed -n '1p;$p'
ls /workspace/openpi/checkpoints/pi05_mecharm_lora/mecharm_v1/
Step 0: grad_norm=1.2376, loss=0.1155, param_norm=1803.7705
Step 1950: grad_norm=0.0416, loss=0.0054, param_norm=1804.0697
1000  1999
root@896724c37bbf:/#

学習したモデル(9.0GB)を、Pod を消す前に KIOXIA へ持ち帰った

Pod は消すと中身も消えます。次に試すときに学習し直さなくて済むよう、2,000 ステップ目のモデル(フォルダ名 1999)を Mac の外付け SSD(KIOXIA)へ持ち帰りました。

(base) shimazueiichi@shimadzueiichinoMacBook-Pro ~ % mkdir -p /Volumes/KIOXIA/r2r2r-data/checkpoints/pi05_mecharm_lora_v1
scp -P <SSH_PORT> -i ~/.ssh/id_ed25519 -r root@<POD_IP>:/workspace/openpi/checkpoints/pi05_mecharm_lora/mecharm_v1/1999 /Volumes/KIOXIA/r2r2r-data/checkpoints/pi05_mecharm_lora_v1/
_CHECKPOINT_METADATA                          100%  426     0.9KB/s   00:00    
norm_stats.json                               100% 1772     4.0KB/s   00:00    
0e0fc224946ae1b3869110a74a467758              100%  171     0.5KB/s   00:00    
792a9a5fdccbdf90e24fc807e82ba307              100% 1156     3.0KB/s   00:00    
b89935267e4b18cc27db5c076f2104b2              100% 1183     3.1KB/s   00:00    
b1ac9ef122a6e95ee7a5f676a69cf0a8              100% 5501    14.2KB/s   00:00    
2ca28251dd2ef0bffc010d664e9ff49b              100% 1320KB 697.9KB/s   00:01    
b405ccc6112f87e080b4d500a536fa44              100%   77MB   8.6MB/s   00:09    
b170dc7440f5890a149777d94bd5dfd1              100%  120MB   8.5MB/s   00:14    
13b36dcb4f2fc509ba8d58c1ddee1168              100%  127MB   8.5MB/s   00:15    
710f04e368fb3a987f47d0f0ee0ebd4c              100%  127MB   8.6MB/s   00:14    
ab92feaddafa6f642825895dd2370b1a              100%  246MB   8.8MB/s   00:27    
5d8bbf72bbf8bc6bf67d968c0863e353              100%  504MB   8.9MB/s   00:56    
17d1fed37d893b2f1b98fa635876506a              100%  127MB   8.6MB/s   00:14    
1a9cfdb434b3b5fbca1c8674be011bf9              100%  417MB   8.4MB/s   00:49    
ee60a913c538beb93dcf7839ec3d4543              100%  423MB   8.7MB/s   00:48    
aa29634d5f34e76704668d31011b301a              100%  430MB   9.0MB/s   00:47    
eb90cce070b5d11609f7ee02e0b0abaa              100%  442MB   8.7MB/s   00:50    
manifest.ocdbt                                100%  796     2.1KB/s   00:00    
process_0                                     100%   19KB  52.3KB/s   00:00    
_sharding                                     100%   27KB  59.7KB/s   00:00    
_METADATA                                     100%   59KB 157.4KB/s   00:00    
e7605ee924019ca0c623f54314ff122e              100% 2897     7.5KB/s   00:00    
manifest.ocdbt                                100%  118     0.3KB/s   00:00    
5d6ed67e7fc853e56aa5c1a6bbc51df9              100%  213     0.6KB/s   00:00    
4c84bf2843a5c4f4f66e088d72c27478              100%  480     1.3KB/s   00:00    
44a8c8e056c1064be41720c247adbc74              100%  496     1.3KB/s   00:00    
7b5c35152283df0e1551b18ce9aa5945              100%  519     1.4KB/s   00:00    
cff6dee763cb540c3725a4b6f07f8aff              100%  757     2.0KB/s   00:00    
4a8a52cbc15bb655383c2076de7f58b6              100%  856     2.3KB/s   00:00    
cb951c6e883921e36908b6d25cb385bf              100%  938     2.5KB/s   00:00    
b6ffa78be338d330e86eec9ff974753a              100%  979     2.6KB/s   00:00    
02a42b2ee1a06c57b0b8d20f96ee12e8              100% 1033     2.7KB/s   00:00    
13686640b6f8adbe756c516bb6953ca6              100% 1083     2.8KB/s   00:00    
f70a89f62633d2bcd42d31c8b5d4bcb3              100% 1144     3.0KB/s   00:00    
0704688aad8ecb7e100c849597eb3b5f              100% 1197     3.1KB/s   00:00    
1dd7747aa329f2b4f833186fad142f66              100% 1244     3.3KB/s   00:00    
550b1b6d334f9817f4bd166dab4198c7              100% 1259     3.3KB/s   00:00    
3fb686396ed3cf9609c7baea4ef248f0              100% 1315     3.5KB/s   00:00    
b3830e7a3e9b8382a3738bf3a450574f              100% 1344     3.5KB/s   00:00    
1cc3269b72d92fa3ede82ccf6031d4ba              100% 2046     5.3KB/s   00:00    
d6aef88b46035ff22a9f8739f24a4b8c              100% 9664    25.0KB/s   00:00    
de66dd5ca49f2a5598b917268f2ff461              100%   90MB   8.5MB/s   00:10    
645918e3a7395fde54fd82bed64c5e90              100%   61MB   7.9MB/s   00:07    
1070f9c6644d4d6db4e149c17303af93              100%   42MB   7.2MB/s   00:05    
5acedd2fef6bf0fb4fc2bd8ab6ac8c4a              100%   17MB   6.4MB/s   00:02    
2bfb9e124fda2cfe14f83297383e929e              100%   56MB   8.0MB/s   00:07    
30951bbf8ff8a45bea4ac4a510b56532              100%   33MB   7.4MB/s   00:04    
4310af4417b67646143754732ff31894              100%  127MB   8.3MB/s   00:15    
30b8103530fc940275fced8c2ab54146              100%  183MB   8.3MB/s   00:22    
e428b2f4fc90a11598c50e35bc3ee2ef              100%  240MB   8.4MB/s   00:28    
1e606656667b4572f98eaeb8fa47dc38              100%  524MB   8.8MB/s   00:59    
907c6a728d9d0c14fc38dbee0f4b9de1              100%  225MB   9.0MB/s   00:24    
db6fafd424457b75d7869d6843c86d9b              100%  474MB   9.0MB/s   00:52    
b67930ba075664c684f60d07fe07de6d              100%  474MB   9.0MB/s   00:52    
9135ce80af2a4da16547f5559d907c6c              100%  791MB   9.1MB/s   01:27    
ab09b5deb014ad86f3e5ca2faaa039b4              100%  902MB   8.5MB/s   01:46    
7eb84d232903ea9e86dbb890f3c326bb              100%  902MB   9.0MB/s   01:40    
c1459bde3a792b3a5da9dfb1d0f0ddcf              100%  902MB   9.0MB/s   01:40    
manifest.ocdbt                                100%  265     0.7KB/s   00:00    
process_0                                     100%   13KB  33.6KB/s   00:00    
_sharding                                     100%   17KB  39.2KB/s   00:00    
_METADATA                                     100%   32KB  86.0KB/s   00:00    
861aa0b47f968b6d3afb828fdbb29654              100% 2819     7.4KB/s   00:00    
manifest.ocdbt                                100%  118     0.3KB/s   00:00    
(base) shimazueiichi@shimadzueiichinoMacBook-Pro ~ % 

転送は1秒あたり約 9MB で、約20分かかりました。持ち帰った中身を確かめます。

cd $HOME/mnt/KIOXIA/r2r2r-data/checkpoints/pi05_mecharm_lora_v1 && du -sh 1999 && find 1999 -type f | wc -l && ls 1999
9.0G	1999
65
_CHECKPOINT_METADATA
assets
params
train_state

学習ログとサーバーのログも、同じ場所に持ち帰りました。

scp -P <SSH_PORT> -i ~/.ssh/id_ed25519 -r root@<POD_IP>:/workspace/logs /Volumes/KIOXIA/r2r2r-data/checkpoints/pi05_mecharm_lora_v1/

推論サーバーを起動し、たたんだ姿勢のまま予測を受け取った

学習したモデルを、Pod の中でサーバーとして起動します。Mac から上カメラの画像と今の関節角を送ると、π0.5 が次の1秒分(10ステップ)の関節角を返します。

cd /workspace/openpi
uv run scripts/serve_policy.py policy:checkpoint --policy.config=pi05_mecharm_lora --policy.dir=checkpoints/pi05_mecharm_lora/mecharm_v1/1999 2>&1 | tee /workspace/logs/serve.log
INFO:absl:orbax-checkpoint version: 0.11.13
INFO:absl:Created BasePyTreeCheckpointHandler: use_ocdbt=True, use_zarr3=False, pytree_metadata_options=PyTreeMetadataOptions(support_rich_types=False), array_metadata_store=<orbax.checkpoint._src.metadata.array_metadata_store.Store object at 0x78f8c6706fd0>
INFO:absl:Restoring checkpoint from /workspace/openpi/checkpoints/pi05_mecharm_lora/mecharm_v1/1999/params.
INFO:absl:[thread=MainThread] Failed to get flag value for EXPERIMENTAL_ORBAX_USE_DISTRIBUTED_PROCESS_ID.
INFO:absl:[process=0] /jax/checkpoint/read/bytes_per_sec: 1.1 GiB/s (total bytes: 6.3 GiB) (time elapsed: 5 seconds) (per-host)
INFO:absl:Finished restoring checkpoint in 5.62 seconds from /workspace/openpi/checkpoints/pi05_mecharm_lora/mecharm_v1/1999/params.
INFO:root:Loaded norm stats from /workspace/openpi/assets/pi05_mecharm_lora/local/mecharm_redblock_box_v1
INFO:root:Loaded norm stats from /workspace/openpi/checkpoints/pi05_mecharm_lora/mecharm_v1/1999/assets/local/mecharm_redblock_box_v1
INFO:root:Creating server (host: 896724c37bbf, ip: 192.168.4.2)
INFO:websockets.server:server listening on 0.0.0.0:8000
INFO:websockets.server:connection open
INFO:openpi.serving.websocket_policy_server:Connection from ('127.0.0.1', 54260) opened

Mac 側では、SSH トンネル(Mac の 8000 番を Pod の 8000 番へつなぐ道)を開き、デーモンを起動してから、ドライラン(予測を表示するだけで、アームには送らない)を実行しました。

ssh -N -L 8000:localhost:8000 -p <SSH_PORT> -i ~/.ssh/id_ed25519 root@<POD_IP>
Last login: Sat Oct 17 07:08:22 on ttys004
(base) shimazueiichi@shimadzueiichinoMacBook-Pro ~ % ssh -i ~/.ssh/id_rsa_pi ubuntu@192.168.2.1 "setsid nohup python3 /home/ubuntu/mecharm_daemon.py </dev/null >>/home/ubuntu/daemon.log 2>&1 &"
sleep 5
ssh -i ~/.ssh/id_rsa_pi ubuntu@192.168.2.1 'tail -3 ~/daemon.log; ss -ltn | grep 5555'
[DAEMON 14:45:05] ✓ MyCobot 初期化完了
[DAEMON 14:45:05] startup_warmup angles=[0.08, 92.9, 3.33, -6.85, -97.91, -30.41]
[DAEMON 14:45:05] ポート 5555 で待ち受け中...
LISTEN   0        1                    0.0.0.0:5555            0.0.0.0:*
(base) shimazueiichi@shimadzueiichinoMacBook-Pro ~ % 

1回目のドライランです。

Last login: Sat Oct 17 07:20:04 on ttys008
(base) shimazueiichi@shimadzueiichinoMacBook-Pro ~ % cd ~/r2r2r-scripts && python3 pi05/run_policy_client.py --max-chunks 5
[SAFE  ] 安全範囲 (学習 20 本 ±10°): j0[-52,+96]  j1[-9,+99]  j2[-52,+30]  j3[-24,+32]  j4[-90,+60]  j5[-51,-19]
[MODE  ] DRY-RUN (アームには送らない)  steps/chunk=5  max_move=20.0°  log=/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261017_072035
[POLICY] 接続 ws://localhost:8000  metadata={}
[WARN  ] HOME から離れています (max|Δ|=97.9°). 学習データは HOME から始まっています
         現在角: j0=+0.1 j1=+92.9 j2=+3.3 j3=-6.8 j4=-97.9 j5=-30.4
[000] 推論 56456ms  5ステップ先  Δ= -0.3  +0.7  -0.6  -0.1  +7.9  -0.0
       [LIMIT] j4 安全範囲でクランプ -98.1→-90.0
[001] 推論 634ms  5ステップ先  Δ= +7.7  -2.2  +5.5  +1.4  +7.9  +0.1
       [LIMIT] j4 安全範囲でクランプ -98.3→-90.0
[002] 推論 584ms  5ステップ先  Δ= +0.2  +0.1  +0.5  -0.2  +7.9  -0.1
       [LIMIT] j4 安全範囲でクランプ -96.0→-90.0
[003] 推論 1439ms  5ステップ先  Δ= +8.5  -3.3  +6.6  +0.3  +7.9  +0.0
       [LIMIT] j4 安全範囲でクランプ -98.6→-90.0
[004] 推論 715ms  5ステップ先  Δ= +5.7  -1.8  +4.2  +0.1  +7.9  -0.0
       [LIMIT] j4 安全範囲でクランプ -98.6→-90.0
[DONE  ] 記録: /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261017_072035
(base) shimazueiichi@shimadzueiichinoMacBook-Pro r2r2r-scripts %

1回目の推論は、サーバー側の準備(初回のコンパイル)で 56 秒かかりました。2回目からは 0.6〜1.4 秒で返っています(秋田の Mac から Runpod までの往復を含む)。

ただ、この予測は判断の材料になりません。アームが HOME(まっすぐ立てた初期姿勢)ではなく、電源を入れたときのたたんだ姿勢(j1=+92.9°、j4=−97.9°)のままでした。お手本の20本はどれも HOME から始まっているので、π0.5 にとっては見たことのない姿勢です。[LIMIT] が毎回出ているのは、今の j4 が安全範囲(−90° まで)の外にあるためで、表示されている j4 の Δ +7.9 は範囲の内側へ戻した分です。

HOME から始めると、予測した最初の1秒がお手本の平均とほぼ重なった

スクリプトの h で HOME に戻してから、ブロックをいつもの位置に置いて、もう一度ドライランをしました。

[SEND] arrival OK: max|Δ|=1.84° (tol 2.5°)
[HOME  ] 手動 HOME 完了 (arrived=True)

[LOOP] 次の操作 [n=次試行 (Enter可) / h=HOME / q=終了]: ^C
[LOOP] プロンプト中 Ctrl-C 受領.
[LOOP] 前試行が未完走または HOME 未到達の場合, 現在姿勢と把持状態を 目視確認のこと. 必要なら h で HOME を試行してから次試行へ.
[LOOP] 常駐モード終了. 実行試行数 = 0
(base) shimazueiichi@shimadzueiichinoMacBook-Pro r2r2r-scripts % python3 pi05/run_policy_client.py --max-chunks 5
[SAFE  ] 安全範囲 (学習 20 本 ±10°): j0[-52,+96]  j1[-9,+99]  j2[-52,+30]  j3[-24,+32]  j4[-90,+60]  j5[-51,-19]
[MODE  ] DRY-RUN (アームには送らない)  steps/chunk=5  max_move=20.0°  log=/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261017_072306
[POLICY] 接続 ws://localhost:8000  metadata={}
[000] 推論 728ms  5ステップ先  Δ= -4.5 +14.2  -6.1  -0.5 -11.7  -0.3
[001] 推論 1056ms  5ステップ先  Δ= -0.4  +0.8  +0.1  -0.1  -0.5  -0.0
[002] 推論 824ms  5ステップ先  Δ= -0.2  +0.7  +0.2  +0.0  -0.0  -0.0
[003] 推論 766ms  5ステップ先  Δ= -3.4  +8.1  -3.1  -0.3  -7.0  +0.1
[004] 推論 731ms  5ステップ先  Δ= +0.1  -0.1  +0.1  -0.1  +0.1  +0.0
[DONE  ] 記録: /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261017_072306
(base) shimazueiichi@shimadzueiichinoMacBook-Pro r2r2r-scripts %

[LIMIT] は出なくなりました。この予測が、お手本の動き出しと合っているかを比べます。お手本19本それぞれについて、HOME から動き出した最初の1秒(10ステップ)の関節角の変化を取り出し、平均しました。π0.5 の予測は、ドライランの記録(log.jsonl)に10ステップ分すべて残っているので、そこから同じ形で取り出します。

cd $HOME/mnt/KIOXIA/r2r2r-data && python3 - <<'PY'
import json, numpy as np, glob
L=[json.loads(l) for l in open("runs/pi05_policy/pi05_20261017_072306/log.jsonl")]
for c in L:
    a=np.array(c["actions"]); s=np.array(c["state"])
    print("chunk",c["chunk"],"lat",c["latency_s"],"n",len(a),"keys",sorted(c.keys()))
c=L[0]; a=np.array(c["actions"]); s=np.array(c["state"])
print("state", np.round(s,2).tolist())
print("pred Δ per step (j0..j4):"); print(np.round(a[:,:5]-s[:5],1).tolist())
D=[]
for d in sorted(glob.glob("datasets/mecharm_redblock_box_scripted_v1/raw_episodes/run_*")):
    if d.endswith("182552"): continue
    st=np.load(f"{d}/episode.npz")["state"]
    mv=np.where(np.abs(st[:,:6]-st[0,:6]).max(1)>1)[0][0]
    D.append(st[mv-1:mv+10,:5]-st[mv-1,:5])
D=np.array(D); print("n eps",len(D))
print("demo mean Δ per step:"); print(np.round(D.mean(0),1).tolist())
print("demo min/max at step5", np.round(D[:,5].min(0),1).tolist(), np.round(D[:,5].max(0),1).tolist())
PY
chunk 0 lat 0.728 n 10 keys ['actions', 'chunk', 'executed', 'grip_cmd', 'latency_s', 'notes', 'state', 't', 'target', 'used_step']
chunk 1 lat 1.056 n 10 keys ['actions', 'chunk', 'executed', 'grip_cmd', 'latency_s', 'notes', 'state', 't', 'target', 'used_step']
chunk 2 lat 0.824 n 10 keys ['actions', 'chunk', 'executed', 'grip_cmd', 'latency_s', 'notes', 'state', 't', 'target', 'used_step']
chunk 3 lat 0.766 n 10 keys ['actions', 'chunk', 'executed', 'grip_cmd', 'latency_s', 'notes', 'state', 't', 'target', 'used_step']
chunk 4 lat 0.731 n 10 keys ['actions', 'chunk', 'executed', 'grip_cmd', 'latency_s', 'notes', 'state', 't', 'target', 'used_step']
state [0.08, 1.23, 1.84, -0.26, 0.79, -30.32, 0.0]
pred Δ per step (j0..j4):
[[-0.4, 1.5, -0.5, -0.2, -0.9], [-1.1, 3.3, -1.3, -0.1, -2.4], [-2.1, 6.5, -2.6, -0.3, -5.0], [-3.2, 9.9, -4.2, -0.4, -8.1], [-4.5, 14.2, -6.1, -0.5, -11.7], [-5.5, 17.8, -7.5, -0.6, -15.1], [-6.3, 21.2, -9.2, -0.6, -18.1], [-7.8, 24.8, -10.8, -0.7, -21.4], [-9.0, 28.7, -12.5, -0.8, -24.9], [-10.2, 32.0, -14.0, -0.9, -27.6]]
n eps 19
demo mean Δ per step:
[[0.0, 0.0, 0.0, 0.0, 0.0], [-0.8999999761581421, 2.299999952316284, -1.0, 0.10000000149011612, -2.0], [-2.200000047683716, 5.400000095367432, -2.4000000953674316, 0.10000000149011612, -4.699999809265137], [-3.5, 8.600000381469727, -3.799999952316284, 0.20000000298023224, -7.5], [-4.699999809265137, 11.699999809265137, -5.199999809265137, 0.20000000298023224, -10.199999809265137], [-6.0, 14.899999618530273, -6.5, 0.30000001192092896, -12.899999618530273], [-7.300000190734863, 18.0, -7.900000095367432, 0.30000001192092896, -15.699999809265137], [-8.5, 21.100000381469727, -9.300000190734863, 0.4000000059604645, -18.399999618530273], [-9.800000190734863, 24.299999237060547, -10.699999809265137, 0.4000000059604645, -21.200000762939453], [-11.100000381469727, 27.399999618530273, -12.100000381469727, 0.5, -23.899999618530273], [-12.399999618530273, 30.5, -13.5, 0.5, -26.600000381469727]]
demo min/max at step5 [-7.699999809265137, 1.5, -8.399999618530273, -2.4000000953674316, -20.5] [-0.699999988079071, 16.799999237060547, -0.800000011920929, 4.099999904632568, -1.100000023841858]

1秒後(10ステップ目)の値を並べます。j0 はベースの回転、j1 は肩、j2 は肘、j3・j4 は手首です。

関節π0.5 の予測お手本19本の平均差
j0(ベース)−10.2°−12.4°2.2°
j1(肩)+32.0°+30.5°1.5°
j2(肘)−14.0°−13.5°0.5°
j3(手首)−0.9°+0.5°1.4°
j4(手首)−27.6°−26.6°1.0°

向きは全関節で同じで、差はどれも 2.2° 以内です。途中の10ステップも、ほぼ同じ速さで増えています。スクリプトがブロックを取りに行くときの最初の動き(肩を前へ倒し、手首を下へ曲げ、ベースをブロックの方へ回す)を、そのまま返しています。

2〜5回目の予測は、1秒後の値が小さく、ばらつきました。

cd $HOME/mnt/KIOXIA/r2r2r-data && python3 - <<'PY'
import json, numpy as np
L=[json.loads(l) for l in open("runs/pi05_policy/pi05_20261017_072306/log.jsonl")]
for c in L:
    a=np.array(c["actions"]); s=np.array(c["state"])
    print(c["chunk"], "step10 Δ", np.round(a[9,:5]-s[:5],1).tolist(), "grip", np.round(a[:,6],2).tolist()[-1], c["notes"], c["executed"])
PY
0 step10 Δ [-10.2, 32.0, -14.0, -0.9, -27.6] grip -0.01 [] False
1 step10 Δ [-5.5, 13.2, -4.3, 0.3, -11.6] grip 0.0 [] False
2 step10 Δ [-4.9, 12.3, -4.6, 0.4, -7.8] grip -0.0 [] False
3 step10 Δ [-10.7, 24.2, -9.8, -1.1, -21.3] grip 0.0 [] False
4 step10 Δ [-0.3, 0.8, -0.3, -0.1, -0.5] grip 0.0 [] False

お手本は、どれも HOME で約1秒止まってから動き出します(動き出しは11ステップ目前後)。ドライランではアームが動かないので、π0.5 は毎回「HOME で止まっている」画像と関節角を受け取ります。そのため「まだ待つ」と「動き出す」の間で、予測が揺れていると考えています。実機に送って姿勢が変われば、先へ進むはずです。それを確かめるのが次の作業です。

A40 を借りた時間と費用

項目値
GPUNVIDIA A40(48GB)× 1、Runpod On-Demand
料金1時間 0.62 ドル(GPU 0.59 ドル+Container disk 200GB 0.028 ドル)
Pod を作った時刻2026-10-16 19:58 ごろ(日本時間)
学習21:04〜01:07(4時間2分44秒)、2,000 ステップ、1ステップ約 7.1 秒、バッチ32
学習の費用(計算)約 4.05 時間 × 0.62 ドル ≒ 2.5 ドル
学習のあと何もしていなかった時間01:07〜07:00 ごろ(約6時間、約 3.7 ドル)
保存したモデル9.0GB(パラメータ部分 6.3GiB)

学習が終わったあとも Pod を朝まで止めていなかったため、使っていない時間の料金がかかっています。Pod はまだ削除していません。実機で π0.5 を動かして確かめたあとに削除し、そのときの費用の合計を次の Log に書きます。

実機で π0.5 を動かす前に残っていること

アームに送るコマンドは、次のとおりです。1回分の予測ごとに止まり、Enter で実行、s で送らずに次へ、h で HOME に戻して終了します。

python3 pi05/run_policy_client.py --execute

送る前に、クライアントが2つの制限をかけます。1つ目は、関節ごとの範囲を、お手本20本で実際に使った範囲の ±10° までに絞ることです(手首 j4 はさらに −90°〜+60°)。2つ目は、1回で動かす量を 20° までにすることです。電源をすぐ切れる位置に手を置いて動かします。

保存先は次のとおりです。

データ場所(KIOXIA の r2r2r-data/ 以下)
お手本(20本)datasets/mecharm_redblock_box_scripted_v1/raw_episodes/
学習したモデルcheckpoints/pi05_mecharm_lora_v1/1999/
学習ログ・サーバーログcheckpoints/pi05_mecharm_lora_v1/logs/
ドライランの記録runs/pi05_policy/pi05_20261017_072035/・pi05_20261017_072306/
高さ補正の元ファイルarchive/place_height_memory_predemo_20261016/