1. 파인튜닝 방법
Table with columns: 항목, 내용| 항목 | 내용 |
|---|
| 베이스 | Qwen/Qwen3.5-4B (Apache-2.0) |
| 방식 | LoRA r=16, 언어 모델의 q/k/v/o/gate/up/down에만 적용, 비전 타워 동결 |
| 데이터 | 실제 프레임 7장 → 반전·물체 붙여넣기·화질저하로 63장면 → 지시문 5,503개 자동 생성 → 1,600개로 1 epoch |
| 학습 시간 | A100 1장, 약 60분 |
| 출력 강제 | vLLM 구조화 출력(json_schema). 스킬 이름과 팔은 스키마 enum이라 잘못된 값이 나올 수 없음 |
지시문은 handover / pick-up / put-on / 손 지정 / 거절(없는 물체, 미지원 동작, 모호, 위험) / 양팔 복합의 6가지 유형, 영어·한국어 반반입니다.
2. 파인튜닝 전후 비교
학습에 쓰지 않은 프레임 3장, 170개 지시문(영 85 / 한 85). 같은 방·카메라·물체 세트이므로 "이 시연 환경" 기준 점수입니다.
Table with columns: 모델, 완전 일치, arm 정확도, 지연 (p50)| 모델 | 완전 일치 | arm 정확도 | 지연 (p50) |
|---|
| Qwen3.5-4B, 프롬프트만 | 140 / 170 (82%) | 0.87 | 1.3 s |
| Qwen3.5-9B, 프롬프트만 | 153 / 170 (90%) | 0.95 | 2.0 s |
| Gemma 4 12B, 프롬프트만 | 152 / 170 (89%) | 0.89 | 2.8 s |
| Qwen3.5-4B + LoRA SFT (이 모델) | 169 / 170 (99%) | 0.99 | 1.3 s |
| + GRPO | 170 / 170 | 1.00 | 1.2 s (SFT와 차이는 노이즈 수준) |
프롬프트만으로도 handover·pick-up·손 지정은 거의 만점입니다. 파인튜닝이 고친 것은 거절 형식, 옆 책상 물체 무시, "던져" 같은 미지원 동작, 모호성 판단, 양팔 복합 지시입니다.
주의: 시연에 나올 물체는 반드시 작업대 위에 놓인 사진으로 학습 데이터에 넣어야 합니다. 옆 책상 방해물로만 등장한 물체는 "항상 없다"고 학습됩니다(사과에서 실제로 발생, 합성 데이터로 수정).
3. 출력 형식
{
"visible_objects": [{"name": "banana", "nearest_arm": "right"}],
"status": "ok",
"reason": null,
"skills": [
{"skill": "approach", "target": "banana", "arm": "right"},
{"skill": "pick", "target": "banana", "arm": "right"},
{"skill": "lift", "target": null, "arm": "right"},
{"skill": "handover", "target": null, "arm": "right"}
]
}
skill: approach | pick | lift | handover | put_on | ready_pose
target: approach/pick은 물체 이름(SAM3 프롬프트로 사용), put_on은 목적지, 나머지는 null
arm: left | right. 같은 팔은 배열 순서대로, 두 팔은 병렬 실행
status가 ok가 아니면(object_not_visible | unsupported_action | ambiguous | unsafe) skills는 빈 배열이며 실행하지 않습니다
4. 실행 매뉴얼
서버 (GPU 머신)
pip install "vllm>=0.27"
vllm serve napalna/rby1-skill-planner-qwen3.5-4b --served-model-name planner-sft --port 8002 \
--dtype bfloat16 --max-model-len 8192 --limit-mm-per-prompt '{"image": 1}' \
--structured-outputs-config '{"backend": "xgrammar"}'
curl http://localhost:8002/health # 200이면 준비 완료 (기동 약 3분, VRAM 약 20 GB)
상위 모듈 → 플래너 호출 (로봇 리포 plan_actions.py)
from plan_actions import plan_actions
plan = plan_actions("바나나 주세요",
endpoint="http://<server>:8002/v1/chat/completions",
image=head_frame_bgr,
strict=True)
plan.meta
plan.to_json()
플래너 → 하위 모듈: plan.to_json()의 skills 배열을 TaskQueueManager.load_plan()에 넘기면 됩니다. 하위 모듈이 읽는 필드는 skill, target, arm 셋뿐입니다. plan.meta["status"] != "ok"이거나 problems가 있으면 실행하지 마세요.
파이썬 없이 HTTP로 직접 호출
import base64, json, requests
from plan_actions import SYSTEM_PROMPT, PLAN_JSON_SCHEMA
img = base64.b64encode(open("frame.jpg", "rb").read()).decode()
r = requests.post("http://<server>:8002/v1/chat/completions", json={
"model": "planner-sft", "temperature": 0, "max_tokens": 512,
"chat_template_kwargs": {"enable_thinking": False},
"response_format": {"type": "json_schema", "json_schema": {"name": "plan", "schema": PLAN_JSON_SCHEMA, "strict": True}},
"messages": [{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": [{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64," + img}},
{"type": "text", "text": "Instruction: 바나나 주세요"}]}]})
print(json.loads(r.json()["choices"][0]["message"]["content"]))
시스템 프롬프트와 스키마는 로봇 리포의 plan_actions.py에 있으며, 학습 때와 같은 프롬프트를 써야 합니다.
5. 리포 구성
napalna/rby1-skill-planner-qwen3.5-4b — 병합 가중치 (vLLM으로 바로 서빙)
napalna/rby1-skill-planner-qwen3.5-4b-lora — LoRA 어댑터만 (100 MB, PEFT로 베이스 위에 로드)
- 벤치마크·학습 코드: 로봇 리포
arpa_h_demo_robot_side (plan_actions.py, PLANNER.md)