# π0.5 持ち上げ不足:学習・実機の食い違い点検 2026-10-22。コード・19本のraw_episodes・指定のオフラインCSV・実機3回のlog.jsonl・serve.log・FK比較表を読み取り専用で確認した。アーム、グリッパー、カメラ、推論サーバーへの操作・接続は行っていない。対象コード、HOME、SPEED、J4_SAFEは変更していない。以下の数値集計は保存済みデータだけを使った。 ## 結論 **最有力は、チャンク末尾の姿勢だけを送る再計画方式と、3°の到達許容が、小さい持ち上げ指令を十分に実現できないまま次の予測へ進めていること。** それに、つかむ時点で学習軌道より深い姿勢へ外れる問題が重なっている。 「モデルが肩をお手本の4分の1しか上げる予測をしていない」とする根拠は、今回の数値からは支持できない。位置Cのrawでも5ステップ先のj1変化は約−2〜−3.8°で、実機ログの−2〜−3.6°と同程度の区間がある。**予測変位と実現変位を分ける必要がある。** またj1だけでは高さは決まらず、j2/j3/j4との組合せも関係する。 ただし、3°判定だけを唯一の原因と断定はしない。到達フラグは速度ゼロ・最終収束を保証しないし、次の実測までの間に追加移動する可能性もある。以下のログは、結果として小さい肩指令が十分実現していないことを示す。機械の追従特性・画像による分布ずれとの因果分離は追加の非駆動試験が必要。 ## A1〜A6 ### A1 ⚠️ 気になる:RGBと縮小は同じ、画像生成経路は同一ではない - 学習raw生成:`build_demo_episodes.py:381–392`。recorderのtop.mp4をデコードし、長辺384へINTER_AREAで縮小、JPEG品質95で保存。 - LeRobot変換:`convert_to_lerobot.py:58–64`。JPEGをPillowでRGBにしてadd_frame。サイズをメタデータと照合。 - オフライン評価:`eval_policy_offline.py:68–72,117–126`。raw JPEGをOpenCVで読み、BGR→RGB。再縮小しない。 - 実機:`run_policy_client.py:134–143,315–330`。カメラから取得したフレームを長辺384へ同じINTER_AREAで縮小、BGR→RGBし、JPEGを通さず送る。 **色順の逆転、縮小サイズの不一致はコード上見つからない。** 位置Cのmetaは384×216。共通addonの `_parse_image` は配列のdtype/軸を扱うだけで、別の色変換はない(`mecharm_policy.py:28–34,41–61`)。ローカルopenpiのModelTransformFactoryはResizeImages(224,224)を持ち、実機も評価も同じサーバーなら同じパディング処理を受ける。 一方、学習は「カメラ→avc1動画→デコード→縮小→JPEG→デコード」、実機は「カメラ→縮小→直接送信」。recorderは `recorder.py:80,478–480` でavc1保存。圧縮・デコード差は実在するが、それが高さ不足の主因とする証拠はまだない。Pillow/OpenCVのデコード差やLeRobot側の再保存もピクセル同一を保証しない。 実機クライアントはカメラ解像度1920×1080を明示設定していない(245–256行)。実際の取得サイズに合わせて縮小する。1920×1080であることは保存画像だけでは逆算できない。フレーミングOKは画素全体・照明・箱・爪・ブロック位置の学習分布一致の証明ではない。 重要:実機のframes/*.jpgは、**推論へ送った後**にJPEG化して保存される(336行)。このJPEGを再送しても当時のRGB配列を完全再現したことにはならない。 ### A2 ⚠️ 気になる:7次元・度数法は同じ、状態の時間的意味が違う 関節の元の取得関数は共通のdaemon_get_angles。お手本側はrun_pick_placeのtapが実測6軸をrecorderへ送る(`run_pick_place.py:593–608`)、rawではactualを線形補間する(`build_demo_episodes.py:102–125,303–324`)。実機はdaemonの単発実測をそのまま使う(`run_policy_client.py:122–131,315–328`)。角度の順番やrad/degの入替は見つからないが、**動作中の補間状態と、到達後に読み直す状態**の差がある。画像と関節の同時刻取得も保証されない。 グリッパーはどちらも0=open/1=closeの指令状態で、把持センサー値ではない。 - お手本:指令直前にgripper_cmdを通知(`run_pick_place.py:2273–2275`)、その時刻でrawのstateを切替(`build_demo_episodes.py:318–324`)。 - 実機:指令を送り2秒待ってからソフト上のgripを更新(`run_policy_client.py:401–407`)。次の問い合わせはその後なので、通常は閉じた後にgrip=1となり整合する。途中2秒間を問い合わせていないため、この差だけで主因とは言えない。 - plan_chunkは最初に開閉が変わる予測位置fを見つけ、**その直前の関節姿勢**を選ぶ。f=0なら現在姿勢で開閉する(146–154行)。開閉予定ステップまでの関節変位を1ステップ分省く設計で、把持姿勢のずれに寄与し得る。 初回はgrip=0と仮定して撮像・推論し、実際に開くのは最初の送信直前(242–243,386–392行)。開始時に爪が閉じていれば、最初の画像とstate[6]は食い違う。これは初回の潜在的不整合で、今回の持ち上げ不足の直接原因とまでは未確認。 ### A3 ✅ 同じ:関節6軸はdelta学習→絶対角復元、爪はdelta対象外 `openpi_addon/install_addon.py:51–57`:mask=make_bool_mask(6,-1)。先頭6次元True、7番目False。 ローカルopenpiの `transforms.py:204–242` ではDeltaActionsが**チャンク先頭stateからの差分**を引き、AbsoluteActionsで同じstateを加算する。隣接actions間の差分を累積する方式ではない。クライアントが追加でdeltaを積算しないのは正しい。 `policy_config.py:77–87` の出力はmodel transforms→Unnormalize→data transformsで、最後に絶対角へ戻す。MecharmOutputsは先頭7次元を返す(`mecharm_policy.py:65–68`)。実機ログのactionsも絶対角の値域で、deltaをそのまま角度として送るような明らかな不一致はない。 19本すべてで `actions[:-1,:6] == state[1:,:6]` の最大差は **0**。爪も `actions[:-1,6] != state[1:,6]` は **0件**。 注意:trim後は残った次のstateをactionに再設定し、tを等間隔に振り直す(build:352–361,401–407)。したがって「0.1秒後」は**圧縮後の学習時間**であり、元動画の実時間0.1秒後とは限らない。19本のt_orig隣接間隔の最大は約11.70秒。削除境界の動作は別途確認が必要だが、これだけで持ち上げ不足と断定しない。 この判定はローカルaddon/openpiと保存ログに基づく。稼働中コンテナのソースのハッシュ照合は未実施。 ### A4 ⚠️ 気になる:trim2のcheckpoint assetsを読む証拠はあるが、バイト一致は未確認 `add_trim_config.py`は元configをコピーし、name/repo_id/num_train_stepsのみ変更する。action_horizon=10、6軸deltaの設定は維持される。 `serve.log:8,15–16`:3999/paramsを復元し、config側のtrim2 assetsに続いて **checkpoint/3999/assets/local/mecharm_redblock_box_teach_v1_trim2** を読んでいる。ローカル `policy_config.py:59–64` もnorm_stats未指定ならcheckpoint assetsを優先する。旧teach_v1やv1のnormを使っている証拠はない。 SSDには `checkpoints/pi05_mecharm_trim_lora_v1/3999/assets/local/mecharm_redblock_box_teach_v1_trim2/norm_stats.json` がある。ただしサーバー側ファイル、学習時assetsのSHA256、10/21オフライン評価時のcheckpoint識別子は記録から照合できていない。**「同じrepo_id」から「同じnormの中身」とは断定しない。** ログからは低優先の原因候補。 ### A5 ❌ 食い違っている:10Hz軌道の評価と末尾1姿勢の実行+3°到達判定 `run_policy_client.py:333–334,393–395`はチャンクの選択姿勢1つだけを送り、 `_send_and_wait_arrival(...,3.0)` を呼ぶ。呼出先 `j0_sweep_scan.py:259–270` は全軸の最大角度誤差≤3°で成功し、速度ゼロや一定時間の収束を要求しない。 小さな持ち上げ指令が2°程度なら、開始姿勢自体が到達判定内にある。send自体は行われるため「必ず動かない」わけではないが、十分な移動を確認する前に成功して次の計画へ進める設計になっている。別軸の大きな変位も末尾姿勢への1回の補間となり、学習の軌跡と同じ経路を通る保証がない。途中で十分に上がってから旋回する軌跡を、浅く上げつつ旋回する直行補間に変え得る。 保存ログで確認できる例(102053): |chunk|現在j1|送信target j1|次の実測j1|指令上昇量|実現上昇量| |---|---:|---:|---:|---:|---:| |11|69.34|67.03|68.73|2.31|0.61| |12|68.73|66.70|68.20|2.03|0.53| |16|65.47|62.03|63.98|3.44|1.49| |21|59.32|57.40|59.32|1.92|0.00| |24|58.97|57.92|58.97|1.05|0.00| |26|58.35|56.10|58.35|2.25|0.00| いずれもnotes=[]。apply_limitsによるj1切詰めが原因の例ではない。 閉状態・開閉指令なし・target j1が現在より小さい行を集計し、その次のstateを実現値とした: |記録|該当指令数|指令上昇量の合計°|実現上昇量の合計°|残差中央値°| |---|---:|---:|---:|---:| |095251|3|18.81|11.86|2.39| |100713|4|20.30|12.04|1.99| |102053|17|41.22|11.07|1.93| 合計は各行の現在→targetと現在→次stateを足した**指令追従の診断値**で、正味高さや同一出発点からの総角度ではない。未実現分を次回再び指令するので指令合計には重複がある。それでも「予測した上昇が実測に残らない」ことを示す。 102053の該当区間の問い合わせ記録間隔の中央値は約4.43秒。**5ステップ=学習上0.5秒先であり、実時間0.5秒周期ではない。** 095251/100713も対応区間は中央値約7.06/5.33秒。推論・通信・送信・読角・待機が加わっている。 「動き出し部分を繰り返す」はあり得るが、モデルに停止状態を示す速度入力があるわけではない。静止を直接識別していると断定せず、実測が軌道に追いつかず、チャンク後半を捨てて同じような局所予測を繰り返す閉ループ問題として捉える。 ### A6 ⚠️ 気になる:オフライン評価は実行器を通らず、学習データ上の点評価 `eval_policy_offline.py:123–145`はお手本の画像/stateを直接渡して、予測チャンクと教師actionsを比較する。plan_chunk・apply_limits・到達許容・グリッパー待機・次の実測を通さない。よって「オフラインで合う」は、実機でその姿勢遷移を実現できることを検証していない。 位置C t=36〜108の25問い合わせはerr_meanの平均0.3564°だが、その範囲でerr_maxの最大は9.91°。全軸×全時点の平均だけでは局所誤差を隠す。stuckも最大変位軸で判定する(130–137行)ため、j0が回ればj1の持ち上げ不足は検出しにくい。 お手本Cの5ステップ先j1変位:t60=−2.00°、t66=−2.02°、t72=−3.36°、t78=−3.46°、t84=−3.78°。実機の−2〜−3.6°が常に教師の4分の1とは言えない。実時間あたりの速度低下、到達残差、違う状態に対する予測を混同しないこと。 実機102053では閉じた次のstate j1=69.34°に対し、教師Cは約63.7°。j0や他軸も異なる。ブロック位置を合わせても、画像内の腕姿勢・state・進行段階まで同じにはならない。最初のずれが把持前から出る理由は、画像とstateの入替試験で分離する必要がある。 ## FK補足との対応 指定の `out_ee_height_compare.txt` を読んだ。教師4本は把持時41.3〜41.7mm、最高124.9〜135.2mm。実機は把持直後28.6/25.7/9.8mm、最高71.6/50.8/36.5mmで、把持前の下がり過ぎと把持後の上昇不足の両方があるという説明と整合する。 ただしFK表の生成コード・キャリブレーション精度を独立検証した結果ではない。今回の確定事実は関節ログの追従残差であり、mm換算値は提供されたFK表として扱う。 ## アームを動かさない切り分け(優先順) ### 1. 保存actionsを実行器へ再生する、純粋な数値検査 ハードウェアをimportせず、plan_chunk/apply_limitsの純粋部分だけで、各行の「生actions→開閉で選んだ姿勢→制限後target→次state」を比較する。教師actionsにも同じ選択処理をかける。3°判定内の小移動割合、j1/j2/j4残差、開閉直前の省略量を出す。 今回のログ集計でこの第一段階は実施済み。さらに既存FKの純粋計算だけを使えば、予測5/10ステップ先・選択target・次の実測それぞれの高さを比較できる。予測時点で低いのか、targetで低くなるのか、実現時に低くなるのかを直接分離できる。実機用クライアントの--executeなし起動でもカメラ・daemonアクセスがあるため、この検査には使わない。 ### 2. 保存画像×保存stateの2×2再問い合わせ 把持前・把持直後・上昇中の代表点をそれぞれ選び、同じcheckpoint・prompt・normを固定して、次をサーバー**だけ**に問い合わせる。 |画像|state|狙い| |---|---|---| |教師|教師|現在のサーバーで教師再現の基準を作る| |実機|教師|画像差への感度| |教師|実機|state差への感度| |実機|実機|当時の挙動に近い再現| 各条件を複数回、可能なら同一ノイズ条件でも比較し、j1だけでなく6軸の5/10ステップ先変位・爪・FK高さを保存する。混合入力は物理的に不整合なので、実行用の良い指令を探す試験ではなく、入力感度の診断として扱う。 教師の画像・stateでも現在は再現しなければ、入力差を論じる前にcheckpoint/norm/サーバー版の違いを調べる。実機JPEGは当時の入力と完全同一でないため、再現誤差をすべてサーバーの変化と断定しない。 ### 3. 圧縮・デコードだけを変える試験 同じ保存画像からPillow/OpenCV読込、JPEG再圧縮の有無を変え、stateと推論ノイズを固定して比較する。保存画像だけでは元の非圧縮実機RGBや動画圧縮前画像を復元できないので、これは圧縮への感度試験に限る。推論へ渡す直前とモデル変換後のshape/dtype/RGB統計・パディングを保存して、色順や形状の違いを直接確かめる。 ### 4. 正規化と実行版の同一性を証明する サーバーのcheckpoint assetsのnorm_stats.json、学習時のnorm、SSD上3999のnormのSHA256と7次元の内容を比較する。checkpoint・config・addon・transforms/policy_configの版も記録する。モデル出力のUnnormalize前後、AbsoluteActions前後を同一入力で検査し、関節6軸だけstateが加算され爪は加算されないことを確かめる。 現時点ではこれらのサーバー再問い合わせ・サーバー側ハッシュ照合は未実施。アーム操作なしで可能な次段階の提案である。 ## 現段階の優先順位 1. **確認済み:小さい持ち上げ指令に比べて大きい到達許容と追従残差。** 実測が約2°届かない状態で再計画する。最優先の原因候補。 2. **確認済み:把持時点で教師より深い姿勢、末尾姿勢への直行、学習時間と実時間の違い。** その状態での分布ずれ・軌道の同期崩れが連鎖する可能性。 3. **未分離:画像の撮像・圧縮・シーン差、教師上でのみ良いモデルの汎化。** 2×2試験で調べる。 4. **低優先:deltaの誤解釈や旧normの使用。** 現コードとログにはそれを支持する証拠がないが、稼働サーバーの同一性までは未証明。 この点検から直ちに制約を緩めたり、速度を変えたり、送信を増やす結論にはしない。まず保存記録とサーバーだけの試験で、予測・実行・観測のどこが高さを失わせているかを確定する。