19편의 정책은 순서만 외우고 눈을 뜨지 않았습니다. 그래서 “뭘 더 주면 될까”를 세 번 시도했습니다. 데이터를 3배로, 눈의 학습을 열어 주고, 카메라를 3대로. 세 번 다 기각됐습니다. 이 편은 그 세 번의 실패와, 그 뒤에야 던진 다른 질문에 대한 기록입니다.
가설 1과 2 — 데이터를 늘리고 눈을 열다
첫 정책은 300편으로 배웠고, 이미지 인코더는 얼린 채였습니다(기본 레시피가 그렇습니다). 그래서 두 가지를 한꺼번에 바꿨습니다. 1,000편으로 늘리고, 인코더도 함께 학습되게 열었습니다.
| 지표 | v1 (300편, 눈 동결) | v2 (1000편, 눈 개방) |
|---|---|---|
| 최종 loss | 0.023 | 0.020 |
| 학습 조합 성공률 | 0/25 | 1/25 |
| 홀드아웃 | 0/5 | 0/5 |
| 접지율(10회 반복) | 31% ±27%p | 32% ±19%p |
아무것도 안 움직였습니다. 성공 1회는 표준편차 안이라 우연과 구분되지 않습니다.
여기서 재미있는 관찰이 하나 나왔습니다. 5,000스텝 시점의 loss가 v1과 v2 모두 0.067로 같았는데, 그때 v1은 데이터를 0.96번 돌았고 v2는 0.27번 돌았습니다. 같은 loss가 전혀 다른 상태를 가리킵니다. loss는 두 실험을 구분조차 못 해 줍니다.
가설 3 — 눈의 위치를 바꾸다
남은 용의자는 관측이었습니다. 점검하다 뚜렷한 허점을 찾았습니다. 우리가 쓴 기반 모델은 카메라 3대를 기대하는데 우리는 1대만 쓰고 나머지 두 칸을 빈 슬롯으로 채우고 있었습니다. 게다가 그 1대가 비스듬한 측면이라, 4cm짜리 큐브의 좌우 위치와 앞뒤 깊이가 화면에서 뒤엉켜 있었습니다.
그래서 위에서 내려다보는 카메라와 손목에 붙인 카메라를 추가했습니다. 손목 카메라를 다는 데만 세 번 헤맸습니다. 그리퍼 뒤통수만 보이고, 새까만 화면이 나오고, 방향이 반대였습니다. 원인은 카메라가 자기 좌표계의 −z축 방향을 본다는 규약이었습니다. 카메라를 손에 붙일 때 그 축을 손끝 쪽으로 돌려 줘야 했습니다.

| 실험 | 조건 | 최종 loss | 학습 조합 | 홀드아웃 |
|---|---|---|---|---|
| v1 | 300편, 측면 1대 | 0.023 | 0/25 | 0/5 |
| v2 | 1000편, 눈 개방 | 0.020 | 1/25 | 0/5 |
| v3 | 1000편, 카메라 3대 | 0.020 | 1/25 | 0/5 |
세 번째 기각입니다. loss는 셋 다 0.02이고 성공률은 잡음 안에서 구분되지 않습니다.
덤으로 알게 된 사실 하나. 빈 슬롯도 계산은 그대로 먹고 있었습니다. 우리는 처음부터 카메라 3대분을 계산하면서 그중 두 칸에 검은 화면을 넣고 있었던 겁니다. 아낀 것도 없이 정보만 버린 셈입니다.
곁가지: 변환이 11시간 걸릴 뻔한 이야기
카메라가 3대가 되자 데이터 변환이 분당 1.5편으로 떨어졌습니다. 1,000편이면 11시간입니다. 추측으로 두 번 헛다리를 짚었습니다. “영상 인코딩이 3배라 그렇겠지” 하고 배치 설정을 올렸다가 라이브러리 버그로 죽었고, “그럼 이미지로 저장하자” 했는데 여전히 느렸습니다.
추측을 멈추고 단계별로 시간을 쟀습니다. 프로파일에서는 편당 4초인데 실제 스크립트는 40초. 두 코드의 차이가 곧 답이었습니다. 압축 파일은 키를 인덱싱할 때마다 배열 전체의 압축을 풉니다. 반복문 안에서 data["image_side"][i]를 쓰면 프레임마다 45MB를 통째로 다시 푸는 셈이라, 한 편에 900번의 압축 해제가 일어납니다. 배열을 반복문 밖으로 한 번만 꺼내니 분당 14.2편이 됐습니다. 70분짜리 작업이었습니다.
느리면 추측하지 말고 재십시오. 10편에서 “영상은 검증이 아니다”라고 배운 것과 같은 계열의 교훈이, 이번엔 성능 영역에서 반복됐습니다.
질문을 바꿉니다 — 왜 안 보는가
세 번을 틀리고 나서야 “무엇을 더 줄까” 대신 “왜 안 보는가”를 물었습니다. 방법은 간단합니다. 입력을 하나씩 지워 보고 출력이 얼마나 바뀌는지 재는 겁니다. 아무 의미 없는 잡음도 함께 넣어 바닥을 확인했습니다.
| 시점 | 잡음 바닥 | 이미지를 지웠을 때 | 관절 상태를 지웠을 때 |
|---|---|---|---|
| 시작 순간 | 0.063 | 0.072 | 0.015 |
| 진행 중(240프레임) | 0.007 | 0.004 (잡음 수준) | 0.260 |
읽는 법은 이렇습니다. 정책은 시작 순간에만 이미지를 봅니다. 팔이 움직이기 시작하면 이미지를 무시하고 관절 상태만 따라갑니다.
왜 그럴까요. 학습 중에는 그게 최적이기 때문입니다. 우리 데이터에서 팔은 늘 정답 궤적 위에 있습니다. 그러니 “지금 팔이 어디 있나”만 보면 다음 동작이 거의 결정됩니다. 이미지를 읽는 어려운 일을 할 이유가 없습니다. 그래서 loss는 0.02까지 떨어집니다.
실전에서는 재앙입니다. 한 번 흘깃 본 부정확한 목표를 그대로 밀어붙이고, 스스로 움직이지 않으니 관절 상태도 영영 정답 근처에 가지 않습니다.
개념 박스: 따라 하기 병리 (causal confusion) 모방학습의 고전적 실패입니다. 정책이 “원인”이 아니라 “정답과 함께 움직이는 손쉬운 신호”를 붙잡는 현상입니다. 자동차 운전 데이터로 유명한 예가 있습니다. 브레이크등이 켜진 프레임을 보여 주면 정책은 앞차가 아니라 자기 브레이크등을 보고 브레이크를 밟습니다. 우리 전문가가 완벽한 개루프 스크립트라 지름길이 유난히 깨끗했습니다. 사람이 조종한 시연에는 흔들림과 교정이 섞여 있어 상태가 이만큼 정답을 흘리지 않습니다.
이 하나로 앞의 세 기각이 전부 설명됩니다. 데이터를 3배로? 지름길이 3배 더 확실해질 뿐입니다. 눈을 열어? 안 쓰는 눈을 더 좋게 만드는 일입니다. 손목 카메라를 달아? 파지 순간에 안 보는데 가까이 보여줘야 소용없습니다.
지름길을 끊었는데, 결과는 그대로였습니다
처방은 분명합니다. 관절 상태를 0으로 채워 버리면 됩니다. 매 순간 이미지를 봐야만 행동을 낼 수 있게 강제하는 겁니다.
- 기계적으로는 성공했습니다. 진행 중 시점의 이미지 효과가 잡음 수준(0.004)에서 0.185로 올라갔습니다. 정책이 처음부터 끝까지 이미지를 봅니다.
- 그런데 성공률은 0/30입니다.
- loss는 0.033으로 오히려 높아졌습니다. 지름길이 막혔으니 당연하고, 오히려 정상 신호입니다.
원인을 고쳤는데 결과가 안 바뀌는 경험은 묘합니다. 뜻은 하나입니다. 원인이 하나뿐이라는 보장은 없습니다. 병목이 “안 본다”에서 “봐도 정확히 못 읽는다”로 옮겨갔습니다.
눈이냐 손이냐 — 공짜로 답을 줘 봤습니다
여기서 가르는 실험을 했습니다. 관절 상태 자리에 대상 물체와 받침의 실제 좌표 4개를 넣고 학습시킵니다. 지각을 공짜로 주는 겁니다. 못 집으면 문제는 손(제어)이고, 집으면 문제는 눈(지각)입니다.
| v1~v4 (2만 스텝) | 오라클 (1만 스텝) | |
|---|---|---|
| 학습 조합 | 0~1 / 25 | 23 / 25 |
| 홀드아웃 | 0 / 5 | 5 / 5 |
절반만 학습한 체크포인트가 92%를 냅니다. 같은 모델, 같은 행동 표현, 같은 데이터인데 좌표만 주면 됩니다. 병목은 손이 아니라 눈이었습니다.
정직하게 단서를 하나 답니다. 오라클은 대상 좌표를 숫자로 건네므로 지시문을 해석할 필요가 없습니다. 그러니 홀드아웃 5/5를 “언어 일반화 성공”으로 읽으면 안 됩니다. 언어 시험은 지각을 실제로 붙인 뒤에야 의미가 있습니다.
이번 편에서 남은 것
- 데이터 양, 모델 개방, 관측 설계 — 세 가설이 모두 기각됐습니다.
- 진짜 원인은 정책이 이미지를 안 보기로 한 것이었고, 그건 학습 중에는 합리적인 선택이었습니다.
- 지름길을 끊자 병목이 한 칸 옮겨갔습니다. 좌표를 공짜로 주면 92%가 나옵니다. 문제는 눈입니다.
그러면 눈이 왜 못 읽을까요. 이미지에 정보가 없는 걸까요, 정보는 있는데 못 꺼내는 걸까요. 처방이 완전히 다릅니다. 다음 편에서 그걸 가르려다가, 이번에는 진단 도구가 세 번째로 고장 납니다. 어쩌면 이 시리즈의 진짜 주인공은 로봇이 아니라 자꾸 망가지는 자입니다.
이 시리즈의 전체 글 목록 → 시리즈 목차