← Index
2026-08-31 — Analysis

Keh0t0/molmo-augment — place 증강 데이터 검토

MolmoBot | 15,043편 / 121 GB, shard 1개(150편) 전수 + 4편 심층 확인

TL;DR

15,043
에피소드 (101 shard, 121 GB)
32.1%
원본 궤적 중 보존 구간 (95/297 프레임)
2.86
편당 씬 증강 ops (shift/rotate/remove/add)
0.15%
goal image 내 타깃 픽셀 비중 (중앙값)

📌 2026-08-31 16:12 업데이트 — 구판/신판 전수 대조

101개 shard 전부가 재업로드됐다(원격 sha256 fdee0d3c… vs 구판 9b12685a…). 구판 shard_0000을 보관해두고 신판과 1:1 대조한 결과는 아래와 같다.

항목구판신판판정
augment.json instructionPick up the … and put it on the …Place the … on the …150/150 재작성
support 탐지 실패10편 (body가 None/world)0편floor 10 → countertop 104로 재분류
전체 floor 라벨442편 (2.94%)17편 (0.11%)대폭 정리
에피소드 집합150편, 키 완전 동일변화 없음
clip 절단 src_range150/150 동일궤적 미변경
goal_frame index / phase150/150 동일 (go_home 117 · retreat 33)goal은 여전히 배치 이후 프레임
mp4 · h5 · goal png (1,350개)크기 전부 동일바이너리 미변경
clip.h5 → obs_scene.task_description"Pick up the white mug and place it next to the … pot" (동일)미수정 — 주의
지시문 수정은 augment.json/index.json에만 반영됐고, clip.h5의 obs_scene은 그대로다. 우리 로더는 h5 쪽을 읽으므로(dataset.py:506) 지금 상태로 학습을 돌리면 "Pick up" 접두가 다시 붙는다. place 전용 문형으로 학습하려면 prompt_templates를 place 계열만 남겨 넘기거나(권장, 데이터 수정 불필요), 로더가 augment.json의 instruction을 읽도록 경로를 추가해야 한다.

남은 floor 17편은 진짜 바닥이다

구판의 floor는 탐지 실패 폴백이었지만, 신판에 남은 17편은 성격이 다르다. support.z를 대조하면 갈린다:

케이스labelsupport.zbody판정
ep4 구판floor0.0world오라벨 — 실제는 카운터탑
ep4 신판countertop0.9375실제 countertop 바디정정됨
남은 floor (house_10169)floor0.0world진짜 바닥
floor case goal
남은 floor 케이스 goal image — 변기가 보이는 욕실, 물체가 바닥에 놓임
floor case last frame
같은 편 clip 마지막 프레임 (exo)
procthor 씬에서 바닥은 rigid body가 아니라 정적 world 지오메트리라 진짜 바닥 배치도 body="world", z=0.0이 정상값이다. 구판 오라벨과 구분되는 지점은 실제 배치 높이 — 이 편은 물체 시작 z가 0.052 m(바닥 높이)이고, 오라벨이었던 ep4는 0.946 m(카운터 높이)였다. 단 17편 중 시각 확인은 1편(+같은 house의 자매편)만 했다.
사소한 문서 불일치: README에는 Put the {obj} on the {support}.라고 적혀 있으나 실제 데이터는 Place the …다.

1무엇을 만들었나 (검증된 스펙)

출처 FrankaPickAndPlaceNextToOmniCamConfig 구성 place/shard_0000..0100.tar (150편/shard, 무압축) + index.json 편 구조 clip.h5 · episode_*.mp4 ×5 · clip_meta.json · augment.json goal_im/{wrist_camera, exo_camera_1, exo_camera_2}.png 절단 phase_cut = "preplace..retreat" (policy_phases: preplace=6, place=7, retreat=8) 예) src_range [105,188] / src_length 292 → clip 84 프레임 영상 624×352 @15.2fps, 프레임 수가 clip 길이와 정확히 일치 (84/69/134/120 확인) goal 640×480, 배치 이후 프레임에서 렌더 (go_home 117 / retreat 33) 게이트 양쪽 exo 가시성 ≥ 0.0002 (폐기율 19%)
항목측정값 (shard_0000, 150편)
phase_cutpreplace..retreat 150/150 (일관)
clip 길이평균 95.4 프레임 (59~145) / 원본 평균 297.2 → 보존율 32.1%
support 라벨countertop 94, dining table 29, sofa 13, floor 10, dresser 2, bed 2 — 6종뿐
reference object제거됨 34/150 (23%), 유지 116 — is_support는 전부 False
증강 opsrotate 138 · remove 134 · shift 111 · add 46 (편당 평균 2.86, 최대 10)
goal 카메라150/150 전부 exo2에서 선택
전체 지시문(15,043)100% Pick up X and put it on Y 형태, wrist 가시성 0인 편 40.9%
고유 house4,971개 (편당 3.03편)

2데이터 예시 — 영상으로 보기

각 편은 물체를 이미 쥔 상태에서 시작해 배치 후 후퇴까지다. 원본 지시문(h5 내부)과 증강이 새로 붙인 지시문(augment.json)을 나란히 둔다.

① 머그 → 다이닝 테이블

84 / 292 프레임src [105,188]support: dining tableops: shift×2, rotate×3ref 유지
원본Pick up the white mug and place it next to the dark gray rounded cast iron pot 증강Pick up the … and put it on the dining table. → Place the plain white ceramic mug with handle on the dining table. (08-31 개정)
exo (randomized_zed2_analogue_1)
wrist (wrist_camera_zed_mini)
clip 첫 프레임 exo
clip 첫 프레임 (exo) — 넘겨받는 시점의 장면
clip 첫 프레임 wrist
clip 첫 프레임 — 머그가 이미 그리퍼에 물려 있다
goal image exo2
goal image (exo_camera_2, 640×480)

② 스프레이 → 카운터탑

134 / 349 프레임src [112,245]support: countertopops: rotate×2ref 유지
원본Pick up the green plastic spray bottle with nozzle and place it next to the handwear 증강Place the green plastic spray bottle with nozzle on the countertop. (08-31 개정)
exo
wrist
clip 첫 프레임 exo
clip 첫 프레임 (exo) — 넘겨받는 시점의 장면
첫 프레임
clip 첫 프레임 (파지 상태)
goal
goal image (exo2)

③ 리모컨 → 소파

69 / 258 프레임src [86,154]support: sofaops: shift×1ref 유지
원본Pick up the slim black remote control and place it next to the hardcover book with wood-textured spine 증강Place the slim black remote with buttons on the sofa. (08-31 개정)
exo
wrist
clip 첫 프레임 exo
clip 첫 프레임 (exo) — 넘겨받는 시점의 장면
첫 프레임
clip 첫 프레임 (파지 상태)
goal
goal image (exo2)

④ 뒤집개 → 카운터탑 (구판 floor 오라벨이 정정된 편)

120 / 318 프레임src [95,214]support: floor → countertopops: 없음ref 유지
원본Pick up the food turner and place it next to the cylindrical glass pepper shaker metal 증강Pick up the … and put it on the floor. → Place the dark-handled kitchen spatula on the countertop. (08-31 라벨 정정)
exo
wrist
clip 첫 프레임 exo
clip 첫 프레임 (exo) — 넘겨받는 시점의 장면
첫 프레임
clip 첫 프레임 (파지 상태)
goal
goal image (exo2)
이 편의 "floor"는 라벨 버그다. goal image를 보면 실제 배치면은 돌 카운터탑이고 바닥이 아니다. augment.json을 열어보면 support = {"body": "world", "z": 0.0, "label": "floor"} — 지지면 탐지가 실패했을 때 전역 원점으로 폴백하면서 z=0이라 "floor"가 붙었다. shard 2개(300편)로 검증하니 floor 14편이 100% 탐지 실패였고, 나머지 라벨(countertop 181·dining table 68·sofa 21·bed 7·kitchen cabinet 4·dresser 3·chest of drawers 2)은 실패 0건이다. 즉 label=="floor" ⟺ body ∈ {None, "world"}가 정확히 일치한다. 전체 15,043편 중 442편(2.94%).

⑤ 두루마리 → 욕실 바닥 (신판에 남은 floor 17편 — 오라벨이 아니라 진짜 바닥)

122 / 324 프레임src [99,220]support: floor (z=0.0)ops: 없음ref 유지
원본Pick up the white paper roll and place it next to the light brown cardboard hollow roll 증강Place the white paper roll on the floor.
exo (randomized_zed2_analogue_1)
wrist (wrist_camera_zed_mini)
clip 첫 프레임 exo
clip 첫 프레임 (exo)
clip 첫 프레임 wrist
clip 첫 프레임 (wrist) — 두루마리 구멍에 그리퍼가 들어가 있다
goal image
goal image — 변기가 보이는 욕실, 바닥에 놓임
clip 마지막 프레임
clip 마지막 프레임 (exo)
여기서는 support.z = 0.0, body = "world"가 정상값이다 — procthor 씬의 바닥은 rigid body가 아니라 정적 world 지오메트리이기 때문. 물체 시작 높이도 0.052 m로 바닥 수준이다. 구판 ④번의 오라벨(z=0.0인데 실제는 0.946 m 카운터탑)과 구분되는 지점이 바로 이 높이다.

3검증 — 무엇을 직접 확인했나

clip은 물체를 쥔 상태에서 시작한다 (확인). ①편 기준 t=0~65에서 commanded_action.gripper = 255(닫힘), grasp_state_pickup_obj.held = True, qpos.gripper ≈ 0.756. t=66에 그리퍼가 0으로 열리며 릴리즈(클립의 79% 지점), 이후 phase 8(retreat)로 후퇴. 첫 프레임 wrist 영상에도 머그가 물려 있는 게 그대로 보인다.
action/state 규약이 원본과 동일 (확인). actions/joint_pos_rel는 {arm:[7]} 델타, commanded_action.gripper는 {0,255} 이진, obs/agent/qpos는 {arm:[7], gripper:[2]}. 5개 카메라 이름도 원본과 같다 (randomized_zed2_analogue_1/2, wrist_camera_zed_mini, …). → 우리 --data.exo-camera / --data.wrist-camera 설정 그대로 물린다.
원본 지시문이 살아 있다 (확인, 그리고 이게 중요하다). clip.h5의 obs_scene에 task_type="pick_and_place_next_to", 원본 task_description("place it next to the dark gray rounded cast iron pot"), 가중치 붙은 referral_expressions가 모두 보존돼 있다. 우리 로더(dataset.py:506-531)는 바로 이 필드를 읽어 referral expression을 샘플링하므로, augment.json의 거친 재작성 지시문은 학습에 들어가지 않는다.
exo view의 이상한 화각은 증강 탓이 아니다 (확인). randomized_zed2_analogue_1은 이름 그대로 에피소드마다 카메라 포즈가 랜덤화되는 뷰다. 원본 FrankaPickAndPlaceNextToOmniCamConfig/part2에서 뽑은 아래 프레임들도 똑같이 테이블이 화면을 가득 채우거나 로봇 베이스가 잘려 나간다. 해상도도 624×352로 증강본과 동일하다. (증강본은 전량 part0에서 파생됐고 우리가 받아둔 건 part2라 동일 편 대조는 불가 — 대신 같은 config·같은 카메라로 비교했다. 덧붙여 기존 aux로 쓰던 part2와 소스 part가 달라 편 중복은 없다.)
원본 exo 1
원본 NextTo part2 — exo frame 174/318
원본 exo 2
원본 NextTo part2 — exo frame 160/291
증강 exo
증강본 clip 첫 프레임 — 동일 특성
goal image는 관측 스트림과 정렬돼 있지 않다. goal은 640×480 exo_camera_1/2·wrist_camera인데, 관측 영상은 624×352 randomized_zed2_analogue_*·wrist_camera_zed_mini다. 해상도도 카메라 이름도 다르고, 마지막 관측 프레임과 goal 이미지를 나란히 놓으면 시점이 서로 다르다. 나중에 goal 조건화를 붙일 때 cross-view 대응 부담이 추가된다.

4지시문 뜯어보기 — 왜 전부 "put"이고 "Pick up"이 붙어 있나

이 데이터에는 지시문이 두 벌 들어 있다. 출처가 다르고, 어느 쪽을 쓰느냐가 학습 성격을 바꾼다.

출처형태전체 15,043편 기준우리 로더가 읽나
clip.h5 → obs_scene.task_description
원본 NextTo 그대로
Pick up the white mug and place it next to the dark gray rounded cast iron pot referral_expressions까지 보존읽는다 (dataset.py:506)
augment.json / index.json → instruction
증강이 새로 생성
Pick up the … and put it on the dining table.
Place the plain white ceramic mug with handle on the dining table. (08-31 개정)
15,043/15,043이 Place로 시작
Pick up 0편 · it 0편
읽지 않는다
수정은 augment.json에만 들어갔다 — h5는 그대로다. 구판/신판 shard_0000을 직접 대조한 결과: augment.json의 instruction은 150/150 전부 재작성됐지만 (Pick up the … and put it on the dining table. → Place the … on the dining table.), clip.h5의 obs_scene.task_description은 양쪽 모두 "Pick up the white mug and place it next to the … pot"으로 동일하다. 우리 로더(dataset.py:506)가 읽는 건 h5 쪽이므로, 지금 그대로 학습하면 "Pick up" 접두가 다시 붙는다. 해결은 둘 중 하나 — ⓐ 아래 prompt_templates로 place 계열 문형만 쓰거나, ⓑ 로더가 augment.json의 instruction을 읽도록 경로를 추가하거나.
"put"인 이유는 증강 스크립트의 템플릿 선택일 뿐이다. 원본은 place it next to인데 증강본은 전부 put it on으로 바뀌었다(place it 0편). 평가 벤치 문형은 PnP가 place it in or on the <그릇>, NextTo가 place it next to the <참조물>이라 증강본 문형은 어느 쪽과도 다르다. 다만 2-stage 배포에서는 벤치 문형을 맞출 이유가 없으니 실제 배포 프롬프트와 일치시키는 것이 기준이 되어야 한다.
floor 오라벨은 08-31 업데이트로 정리됐다. 구판 shard_0000의 floor 10편(전부 body ∈ {None,"world"} 탐지 실패)이 신판에서 전량 countertop으로 재분류됐고(94 → 104), 탐지 실패는 10 → 0이 됐다. 위 4번 예시의 실제 배치면이 돌 카운터탑이었다는 관찰과 일치한다. 전체로는 floor가 442편(2.94%) → 17편(0.11%). 남은 17편이 진짜 바닥인지 잔여 실패인지는 미확인.

"Pick up" 접두 제거 — 데이터를 고칠 필요가 없다

molmobot_pi0/prompt_templates.py에 이미 pick_and_place_next_to용 템플릿 10개 그룹이 있고, 그중 집기 접두가 없고 물체 이름을 쓰는 형태가 그대로 들어 있다:

[1] "Put the {pickup_name} next to the {place_name}." ← 원하는 형태 [2] "Move the {pickup_name} next to the {place_name}." [6] "Place the {pickup_name} next to the {place_name}." [9] "Drop the {pickup_name} next to the {place_name}." [5] "Transfer/Relocate the {pickup_name} to be next to the {place_name}." # 반면 아래 그룹은 place 전용 학습에서 빼야 한다 [0] "Pick up the {pickup_name} and place it next to …" [3] "Grab the {pickup_name} and put it next to …" [4] "Take the {pickup_name} and set it next to …" [7] "… Please pick up the {pickup_name} and place it …" ← 그룹 안에 섞여 있음 [8] "Get/Fetch the {pickup_name} and put it …" ← fetch도 집기 함의

dataset.py:_get_task()가 referral_expressions에서 표현을 샘플링해 prompt_template.format(**sampled_referral_exps)로 문장을 만든다. 그리고 randomize_prompts = (prompt_templates is not None)(dataset_openpi.py:215)이므로, place 계열 그룹만 남긴 prompt_templates를 넘기면 그걸로 끝난다 — h5도 tar도 건드릴 필요가 없다.

주의: prompt_templates는 dict[str, list[list[str]]]라 tyro CLI로 넘기면 이 프로젝트에서 이미 한 번 크게 데인 dict 파싱 함정(플래그 반복 시 마지막 항목만 남음)에 걸린다. CLI 대신 config 정의부에서 직접 지정할 것.

goal image를 3번째 슬롯에 넣는 방법

# pi0_pytorch.py embed_prefix — 3장 전부 SigLIP 인코딩, mask는 pad mask로만 작용 for img, img_mask in zip(images, img_masks, strict=True): img_emb = self._apply_checkpoint(image_embed_func, img) # 3번째도 이미 계산되고 있음 pad_masks.append(img_mask[:, None].expand(bsize, num_img_embs)) # droid_policy.py DroidInputs — 바꿀 곳은 여기 두 줄 images = (base_image, wrist_image, np.zeros_like(base_image)) → goal_image image_masks = (np.True_, np.True_, np.False_) → np.True_

토큰 수가 변하지 않으므로 시퀀스 길이·메모리 영향이 없다. 3번째 슬롯은 사전학습 내내 pad 처리돼 학습된 prior가 없을 뿐 충돌하는 prior도 없다. SigLIP 인코더는 슬롯 공유라 goal image의 특징 자체는 이미 의미 있게 뽑힌다. 권장은 goal dropout(배치의 일부는 다시 마스크 아웃)으로, goal이 없어도 동작하는 모델을 유지하는 것.

5그래서 뭘 하면 되나

  1. 가장 싼 결정적 실험 (권장) — 이 데이터를 aux 가중치 0.1로 얹어 10k step만 학습하고 PnP·NextTo를 평가한다. 비교 기준은 이미 있다: mix v2 @10k = Pick 21.0 / PnP 37.0 / NextTo 14.0. 비용은 학습 약 6시간 + eval 2잡. 이게 추측을 끝낸다.
  2. 지시문은 건드리지 말 것 — h5의 원본 NextTo 지시문이 이미 살아 있고 로더가 그걸 읽는다. augment.json의 support 기반 지시문("put it on the floor")을 굳이 끌어다 쓰면 타깃이 6종으로 뭉개지고 벤치 문형("place it in or on the <그릇>" / "place it next to the <참조물>")과도 어긋난다.
  3. goal image를 살리려면 별도 트랙으로 — 3번째 이미지 슬롯의 마스크를 켜고 goal을 넣는 변형. 사전학습 가중치가 적응해야 하므로 A/B는 반드시 같은 step 수로. 이건 데이터 문제가 아니라 모델 입력 설계 변경이다.
  4. 규모를 늘릴 여지 — Pick이 772k편으로 효과를 봤던 전례를 보면, place 증강도 15k로는 부족할 수 있다. 같은 파이프라인으로 FrankaPickAndPlaceOmniCamConfig(main, 221k편) 쪽 place 구간까지 뽑으면 규모가 한 자릿수 올라간다.