SigLIP + PaliGemma + Flow Matching 기반 연속 제어 모바일 VLA — 초기 설계부터 오늘 ablation까지
2026-03-17, "MoNa-pi"(Mobile Navigation Pi-zero)로 명명. Physical Intelligence의 π0 아키텍처를 3-DOF 옴니휠 홀로노믹 모바일 로봇에 이식하는 것이 핵심 목표. MoNaVLA(이산 분류 기반)의 한계를 출발점으로 명시했다.
근거: docs/MoNa-AI_Project_Proposal.md, docs/MoNa-pi_Design_Proposal_v1.md, docs/domain_analysis_pi0_vs_mona.md
2026-04-15, 첫 전체 파이프라인 구현(shosae 커밋). 데이터 전처리부터 학습 config까지 한 번에 갖춰졌고, 같은 날 GitHub Pages 첫 배포도 진행됐다.
같은 날(2026-04-15) 저녁, 백본을 PaliGemma + Action Expert로 전면 교체. 임시 구조에서 π0 원논문에 가까운 구조로 갈아탄 분기점.
load_pretrained_paligemma=False: 로컬 SigLIP+Gemma 캐시 사용 / True: google/paligemma-3b-pt-224 다운로드.monapi-train 분기 운영 체계 확립).
2026-05-27, Flow Head를 π0 원논문의 AdaLN-Zero 방식으로 전면 재작성. 13개 파일, +501/-113줄 변경.
h = action_proj(x_t) + t_emb — 시간 임베딩을 그냥 더함, self-attn은 query만 norm(α1,β1,γ1,α2,β2,γ2) = AdaLNModulation(cond_emb)h = h + γ1 * self_attn(norm1(h)*(1+α1)+β1)h = h + cross_attn(norm2(h), vlm_cond)h = h + γ2 * mlp(norm3(h)*(1+α2)+β2)신규 모듈: models/heads/mona_action_expert.py (정규화 래퍼), scripts/verify_mona_expert.py (수치 정합성 검증).
2026-05-28 하루 동안, AdaLN-Zero 전환 이후 드러난 dtype/API 불일치를 연속으로 수정. transformers 버전업과 BF16 전환이 맞물린 전형적인 인프라 정리 구간.
pg.model.vision_tower — transformers 5.x에서 PaliGemma attribute 경로가 바뀐 것 대응Serbot2(로봇 온보드) 메모리 제약 때문에 PaliGemma 3B를 BF16(~6GB)에서 INT8(~3GB)로 양자화. MoNaVLA의 monavla-driving 접근법을 그대로 참고.
BitsAndBytesConfig 적용, 로드 후 explicit gc.collect() 추가.DEPLOY.md 1절).
2026-06-01, V5(150ep) + add_free(221ep)를 병합해 244개 고유 에피소드로 from-scratch 재학습. 이 커밋이 특히 중요한 이유는 MoNaVLA에서 보고된 "center_straight 0% 병목"을 직접 타겟으로 해소하려 한 흔적이 커밋 메시지에 명시돼 있다는 점이다.
mobile_vla_dataset_merged = v5(150) + add_free(221) = 244 unique eppretrain.ckpt_path: "" (처음부터 재학습 — 266 샘플에 70-epoch 학습으로 과적합되는 것을 방지)
2026-06-01~02, 4가지 변형(base PaliGemma / lerobot 사전학습 백본 / Gemma Action Expert / LoRA)을 동일 조건(val 38 regular / 53 full)으로 비교. 이 시점까지의 아키텍처 탐색을 마무리하는 종합 실험.
| 구성 | regular(38) | full(53) | FPE |
|---|---|---|---|
| base PaliGemma | 100% | 73.6% | 0.386m |
| lerobot 사전학습 백본 | 100% | 73.6% | 0.391m |
| Gemma Action Expert | 100% | 75.5% | 0.394m ← 전체 최고 |
| LoRA | 97.4% | 75.5% | 0.373m |
2026-06-23, 6/2에 작성됐지만 한 번도 실행되지 않았던 scripts/run_ablation.sh(M6→M5→M7)를 처음 실행. 중간에 브랜치 전환 실수로 M5 eval이 한 번 끊겼다가(eval_closedloop.py가 monapi-driving엔 없는 파일이라 working tree에서 사라짐) monapi-train에 고정한 채 재실행해 완료했다.
| 구성 | full(53) | FPE |
|---|---|---|
| baseline(증강 있음) | 73.6% | 0.3906m |
| M5(증강 없음) | 73.6% | 0.3868m |
episode_idx를 실제 h5 파일명으로 역매핑한 결과, 처음 추정(center/straight 카테고리)과 전혀 다른 그림이었다.
| 카테고리 | 구성 | 실패 |
|---|---|---|
| 정형 9종(center/left/right×straight/left/right) | 38개 | 0개(0%) |
| free_center_*(극단오프셋/대각선/원거리/조명) | 5개 | 4개(robot_far만 성공) |
| free_left_* | 5개 | 5개(100%) |
| free_right_* | 5개 | 5개(100%) |
data/ood_augment.py 신규 구현(robot_close/far zoom, basket_extreme 좌우 shift, 윈도우 8프레임 동일 적용)으로 재학습 후 동일 full(53) eval.
| 구성 | SR | FPE |
|---|---|---|
| baseline | 73.6% | 0.3906m |
| M5(no-aug) | 73.6% | 0.3868m |
| M8(ood-aug) | 73.6% | 0.3944m |
checkpoints/ep116/best)를 M5/M8과 동일한 eval 모집단(244ep val split, full n=53)에 직접 통과시켜 진짜 비교를 완성했다.
| 구성 | SR(동일 n=53) | FPE |
|---|---|---|
| baseline(244ep) | 73.6% | 0.3906m |
| M5(244ep, no-aug) | 73.6% | 0.3868m |
| M8(244ep, ood-aug) | 73.6% | 0.3944m |
| M7(116ep) | 71.7% | 0.3790m |
free_* OOD 일반화는 데이터 양을 늘리는 것만으로는 개선되지 않는다는 그림으로 수렴한다. 다음 단계는 양이 아니라 free_*류를 직접 타겟한 데이터 수집.
data/dataset.py의 build_train_val_split()을 점검하다 발견. 기존 구현은 윈도우(샘플) 생성 후 random_split()으로 train/val을 나눴는데, window_size=8 슬라이딩 윈도우는 인접 윈도우끼리 7/8프레임이 겹친다 — 같은 에피소드의 윈도우가 train/val 양쪽으로 갈리는 누출이 발생.
nav_h5_dataset_impl.py는 처음부터 에피소드(파일) 단위로 나눈 뒤 윈도우를 생성하는 좋은 패턴을 갖고 있었는데 MoNa-pi엔 포팅이 안 돼 있었다.
free_* 비율이 전체의 ~8%뿐이라 단순 shuffle로는 val에 0개 뽑히는 경우가 실제 발생(stratify 끄고 확인) — stratify_free=True 옵션으로 free_*/정형을 따로 비율 분할 후 합치도록 보강.
| 카테고리(수정 후, n=17) | SR |
|---|---|
| 정형 9종(15개) | 100% |
| free_*(2개) | 0% |
free_* 표본이 n=2뿐이라 통계적으로 약함)를 바로 처리. build_train_val_split()의 stratify_free를 exclude_free_holdout(기본 True)으로 교체해 free_* 21개를 train/val 분할에서 완전히 제외하고, 신규 build_free_holdout()으로 매 실험마다 동일한 21개 전체를 평가하도록 고정했다. eval_closedloop.py에 --free-only 플래그 추가.
free_* SR을 못 바꿨다 — free_*를 학습에 일부 끼워 넣는 게 이미 효과 없다고 확인된 상태에서, stratify로 val에 어떤 1~2개가 뽑히는지가 흔들리면 앞으로의 어떤 개선(실데이터 수집, grounding 신호 주입)도 "효과가 있었는지" 판단할 기준이 안정적이지 않다. 학습 신호로서는 버려도 되는 손실 대비, 고정 모집단으로 얻는 측정 안정성이 이후 모든 비교의 전제조건.
train.py DataLoader sanity 정상. 장기 로드맵(Phase 2: 실데이터 수집, Phase 3: grounding 신호 파일럿)은 plan.md 참고.
generate() detect로 "gray basket" 오프라인 추출(244 에피소드, 4609프레임 중 valid 43.9%) 후 3조건(image_only/bbox_only/bbox+image concat) 동일 조건(15 epoch, 정형 9종 train 202/val 22)으로 학습, --free-only(n=20, 새 고정 holdout)·--regular-only(n=15)로 평가.
| 조건 | 학습시간 | regular SR | free SR |
|---|---|---|---|
| image_only(baseline) | 28분 | 100% | 15.0%(3/20) |
| bbox_only | 4.5분(백본 스킵) | 100% | 20.0%(4/20) |
| bbox+image(concat) | 27분 | 0% | 0% |
free_* SR을 n=20(9-6 이전 M9는 n=2)으로 처음 측정한 결과다 — image_only 기준으로도 0%가 아니라 15%. M9의 "free_* 0%"는 표본 2개의 통계적 한계였을 뿐, 보편적 사실이 아니었다는 게 드러남. 9-6(Phase 1 고정 holdout)이 의도한 대로 더 신뢰 가능한 측정을 만들어냄.
전체 결과: docs/ABLATION_RESULTS_20260623.md
같은 날, MoNaVLA 세션에서 발견한 파이프라인 교훈들을 MoNa-pi 코드에 직접 대조 검증. 가정으로 옮기지 않고 실제 코드를 읽어 검증된 것만 적용했다.
data/dataset.py): resize(..., PILImage.BILINEAR) 명시inference/engine.py): resize(...) 보간법 미지정 → Pillow 기본값(BICUBIC)monapi-driving·monapi-train 양 브랜치 모두 BILINEAR로 통일, 커밋·푸시 완료.
camera_node.py→collector→controller 전체 경로가 일관되게 RGB(rgb8)로 통일돼 있음.logs/D8_attention_v2.log)에서 PaliGemma 백본 text attention이 forward/left/right 3방향 모두 72.6~74.4%로 정상 — MoNaVLA의 Google-robot post-train Kosmos-2 0% 붕괴와 대조적으로 MoNa-pi는 건강한 상태.
전체 분석: docs/MONAVLA_CROSSCHECK_20260623.md