
근골격 모델에게 물을 먹이다 — "입에 들어가지 않았다"라는 한마디로부터, 병이 바닥을 뚫고 지나갔던 버그를 추적한 이야기
요약
MuJoCo를 이용한 근골격 모델 시뮬레이션 중, 관절 추가로 인해 발생한 데이터 인덱스 밀림 버그와 이를 해결하는 과정을 다룹니다. 모델의 물리적 구조 변경이 키프레임(keyframe) 데이터의 순서에 미치는 영향과 그로 인한 시뮬레이션 오류를 추적합니다.
핵심 포인트
- 관절 추가 시 qpos 배열의 인덱스가 밀려 데이터 오류 발생 가능
- MuJoCo 시뮬레이션에서 키프레임 데이터 관리의 중요성
- 물리적 모델 구조 변경과 제어 데이터 간의 정합성 유지 필요
대상: MuJoCo / 로봇 / 생체역학 시뮬레이션을 다루는 사람 / 좋은 결과보다
솔직한 내역과, 막혔을 때 수정하기까지의 과정을 읽고 싶은 사람.
전제 지식: Python을 읽을 수 있다면 충분함. MuJoCo와 균형 제어(balance control) 용어는 그때마다 풀어서 설명함.
이 기사는 성공담이 아닙니다. "마실 수 있었다"라고 생각했던 것이 마실 수 없었고, 고치려고 했더니 다른 버그가 나타났으며, 그 진범이 나의 첫 번째 추측과 달랐던——그 전말을, 측정한 숫자와 함께 남기는 이야기입니다.
- 지난번,
700개 계열의 근골격 모델(MS-Human-700)의 오른손——이 중 조작용 변종인 81개의 근육——에 물병을 잡게 하여, "입가로 가져가 마시는 동작을 할 수 있었다"라고 썼다. 하지만 영상을 본 사람의 한마디 "입에 들어있지 않다·입도 벌리고 있지 않다"로, 그것이 과도한 주장임을 알게 되었다. - 고치려고 시도하며 알게 된 두 단계의 문제: (1) 목표점이 목 높이에 있어 입술보다 낮았다(나의 배치 실수). (2) 애초에 이 조작용 모델에는 턱관절도 안면근도 없어, 입이 물리적으로 벌어지지 않는다. → 턱을 벌릴 수 있는 파생 모델을 만들었다.
- 그런데 파생 모델에 학습된 제어를 올리자,
팔이 허공을 움켜쥐고, 병은 바닥을 뚫고 지나가 사라졌다. 범인을 "팔 관절의 어긋남"으로 짐작했으나——측정해 보니 틀렸다. 진범은 "관절을 하나 추가한 탓에, 위치로 결정되는 초기 자세 데이터(keyframe)의 번호가 전부 하나씩 밀려, 병의 높이 칸에 다른 값이 입력되어, 1.57m 낙하하여 바닥 아래(z=−0.63m)로 가라앉았던" 것. 0을 하나, 올바른 위치에 삽입하는 것만으로 고쳐졌다. 직후, 근육의 손은 병을 0.62m 들어 올려, 벌어진 입가로 가져갔다.
이 기사의 주인공은 "마실 수 있었다"가 아니라, 그 전 단계의, 수수하지만 MuJoCo를 다루는 사람이라면 누구나 한 번은 겪는 버그다.
| 용어 | 풀이 |
|---|---|
| 근골격 모델(musculoskeletal model) | 뼈·관절에 더해 근육과 건(tendon)까지 가진 인체 시뮬레이션. 이번 조작용 모델은 오른팔 + 상세한 손의 81개 근육 |
| MuJoCo | 로봇·생체역학 물리 시뮬레이터. 이번 모델은 해당 공식 모델 모음인 Menagerie에 수록된 MS-Human-700(Apache-2.0) |
| 자유도(DoF / degrees of freedom) | 독립적으로 움직일 수 있는 관절 축의 수. 조작용 모델은 48 |
qpos | 모델의 모든 관절 위치를 일렬로 나열한 배열. 이번의 핵심. 관절을 추가하면 이 배열이 밀림 |
| keyframe(키프레임) | "실험 시작 시의 자세"를 qpos 값의 나열 순서 그대로 작성한 초기화 데이터. 위치(번호)로 결정되는 것이 핵심 |
| 근 시너지(muscle synergy) | 81개의 근육을 소수의(K개) "함께 움직이는 패턴"으로 압축한 제어 표현. 사람의 뇌도 근육을 이렇게 묶는다고 알려져 있음 |
| 진화 전략(CMA-ES / Covariance Matrix Adaptation Evolution Strategy) | 제어 방책의 수백 개 수치를 변이와 선택으로 최적화하는 기법. 이번에는 파지·운반 제어를 진화시킴 |
| ... |
이것은 연작의 다음 이야기다. 지난번(700개의 근육으로 인체 골격을 세우려다 솔직하게 넘어졌던 이야기)에서, 나는 이 근골격 모델의 손에 "잡은 병을 명령한 장소로 운반하는" 제어를 진화시켰다. 그 최난도의 목표가 "몸 측면·입가로 운반하는"——즉 마시기/먹기 동작이었다.
그리고 나는, 목표점을 "실제 입"으로 다시 설정했더니 동일한 제어 용량 그대로 입가까지 도달했다고 판정했다. 숫자상으로는 도달해 있었다(병 중심과 목표점 사이의 거리가 수 센티미터). "마시는 동작을 할 수 있었다"라고 쓰려 했다.
그런데, 렌더링한 영상을 본 사람(나에게 방향성을 지시하는 측)으로부터, 3가지 거절이 돌아왔다.
- "입에 들어있지 않다"——병은 목·쇄골 근처에서 멈춰 있어, 실제 턱에는 닿지 않는다.
- "입도 벌리고 있지 않다"——애초에 입이 닫힌 채다. 마시는 것처럼 보이지 않는다.
- "무엇을 하고 있는지 모르겠다"——정면 카메라 하나로는 팔의 움직임을 읽을 수 없다.
이것은 이 시리즈가 계속 지켜온 방식——"이상적으로 좋은 결과가 나오면, 이겼다고 생각하기 전에 내역을 의심하라"——가 타인의 눈에 의해 작동한 순간이었다. 나는 "거리가 수 센티미터"라는 숫자를 믿고, 그림을 의심하지 않았다. 인간은 그림을 보고 숫자의 거짓을 꿰뚫어 보았다. 솔직하게, 주장을 철회하는 것부터 시작하기로 했다.
먼저 (1)부터다. 내가 「입」으로 설정했던 목표점은 (0.07, 0, 1.46)이었다. 하지만 실제 턱의 지오메트리 (Geometry)를 측정해 보니, 하악(下顎)의 바닥은 높이 1.50m, 입술 부근은 1.55m 근처에 있었다. 나의 목표인 1.46은 턱 바닥보다 아래, 즉 목·쇄골 높이였다.
병은 「입가」가 아니라 「목덜미」로 운반되어 그곳에서 멈춰 있었다. 거리 수치가 작았던 이유는, 잘못된 목표에는 제대로 도달했기 때문이다 — 목표 지점이 틀렸다면, 달성률이 높을수록 오히려 질이 나쁘다. 목표를 실제 입술 높이인 (0.05, 0, 1.55)로 수정했다.
이는 지난 글에서 내가 썼던 "단일 선형 시너지 (Single Linear Synergy)로는 팔을 몸 측면으로 끌어당길 용량이 부족하다(그래서 닿지 않는다)"라는 설명에 대한, 나 자신을 향한 정정이다. 닿지 않았던 진정한 원인은 용량이 아니라, 목표를 잘못 설정한 것이었다. 용량 탓을 하는 것은 언제나 가장 편한 도피처다.
교훈을 미리 얻자면: "알고리즘의 한계다"라고 결론 내리기 전에, 채점표(목표·보상)가 올바른지 의심하라. 정체의 원인을 능력의 문제로 돌리는 것은 대개 너무 성급하다.
다음은 (2) 「입이 열리지 않는다」. 이것은 나의 설정 실수가 아니라, 모델 그 자체의 한계였다.
MS-Human-700에는 전신·보행용·조작용의 3가지 변종이 있다. 이번에 사용 중인 조작용 변종은 팔과 손을 극한까지 정밀하게 만드는 대신, **머리부를 단일 강체 (Rigid Body)**로 취급한다. 턱 관절도, 얼굴 근육도 없다. 81개의 근육은 모두 팔·손·체간의 것이며, 입을 열 수 있는 수단이 하나도 없다. 즉, 아무리 능숙하게 병을 운반하더라도 이 몸은 원리적으로 입을 열 수 없다.
지시는 명확했다 — "턱을 벌리는 모델로 개조할 것". 그래서 기존 모델(251MB의 에셋 군)을 통째로 복사하여 불변 상태로 두고, 그 조작용 몸체 정의 파일만 편집하여 머리 부분의 턱 지오메트리를 독립된 자식 바디 mandible(하악) + 힌지 관절 (Hinge Joint) jaw_open로 분리해냈다. 이 관절에 각도를 주면 하악이 내려가며 입이 열린다.
다만, 여기에는 처음부터 정직한 선긋기가 있다. 이 턱에는 근육이 붙어 있지 않다. 따라서 턱의 개폐는 근육으로 풀어내는 움직임이 아니라, **위치를 직접 써넣는 대본 (Kinematic)**이다. 병이 입에 가까워지면 이쪽에서 턱을 연다. 팔의 파지(Grasping)와 운반은 81개의 근육이 실제로 풀어내지만, 입이 열리는 것은 연출이다 — 이 구분은 이 글의 끝까지 가져갈 것이다.
개조한 모델은 무사히 불러와졌다. 관절이 1개 늘어나 자유도는 48 → 49가 되었다. 근육의 수(81)는 변하지 않으므로, 학습된 제어 (Learned Control)는 그대로 적용될 것이었다. 턱을 닫은 모습과 연 모습도 개별적으로 확인했으며, 개폐 자체는 작동했다.
——그런데, 거기에 학습된 '마시는 제어'를 얹는 순간 상황이 이상해졌다.
개조한 턱 모델에서, 파지·운반을 학습한 제어(지난번의 champion)를 재생했다. 기대치는 "이전과 똑같이 잡아서 입으로 운반하는 것"이었다. 실제로 일어난 일은 다음과 같다.
근골격은 마시는 것처럼 팔을 입가로 올린다. 하지만 손은 비어 있고, 받침대 위의 병도 사라져 있다. 제어는 병이 있어야 할 위치를 잡으려 시도하지만, 그곳에 아무것도 없기에 팔은 허공을 가르며 발산(Divergence)해 버린다.
턱을 단 하나 추가했을 뿐인데, 파지가 완전히 망가졌다. 근육의 수도, 팔의 관절도 무엇 하나 건드리지 않았는데 말이다.
이럴 때 원인을 추측으로 결정하고 패치를 적용하는 것이 가장 위험하다. 나는 처음에 이렇게 짐작했다 — "관절을 하나 추가했기 때문에 팔 관절의 초기 각도가 어긋나서, 이상한 자세에서 시작하는 것이 틀림없다". 그럴듯하다. 하지만 이 시리즈의 규율은 "'아마 X가 원인일 것이다'보다 'X를 확인한다, 확인 방법은 Y이다'"이다.
그래서 기존 모델과 턱 모델의 시작 직후 상태를 나란히 놓고 측정하는 진단을 작성했다. 팔의 주요 7개 관절의 각도, 손바닥의 세계 좌표, 병의 세계 좌표를 두 모델에서 비교했다. 결과는 다음과 같다.
| 측정 항목 | 기존 모델 | 턱 모델 | 차이 |
|---|---|---|---|
| 팔의 reach 관절 7개의 각도 | (기준) | (동일) | 최대 0.0 |
| 손바닥의 세계 좌표 | (기준) | (동일) | 0.0 |
| 병의 높이 z | 0.94 m | −0.63 m | −1.57 m |
| 손 ~ 병 거리 | 0.108 m | 1.667 m | — |
내 추측은 틀렸다. 팔의 관절도 손의 위치도 원래와 완전히 일치한다(초기 자세는 망가지지 않았다). 망가진 것은 병뿐이었다 —— 원래 z=0.94m인 책상 위에 있어야 할 병이, z=−0.63m, 즉 바닥을 뚫고 지나간 지하에서 시작되고 있었다.
손은 올바른 초기 자세에 있다. 잡아야 할 대상(병)만이 지면 아래로 떨어져 있다. 그래서 팔은 올바르게 '책상 위의 병이 있어야 할 위치'로 움직이지만, 그곳에는 아무것도 없다. 헛스윙의 정체가 바로 이것이다.
여기서 '팔이 어긋났음에 틀림없다'라고 단정 짓고 코드를 만졌다면, 고쳐지기는커녕 올바르게 작동하던 팔의 제어를 망가뜨렸을 것이다.
측정하는 것은 잘못된 수리를 방지한다.
왜 병만, 그것도 높이 1.57m만큼이나 떨어졌는가? 여기가 MuJoCo(뿐만 아니라 물리 엔진)를 다루는 모든 사람에게 유효한 교훈이다.
MuJoCo는 모델의 모든 관절 위치를 하나의 배열인 qpos에 채워 넣는다. 각 관절은 신체의 트리 구조(tree structure) 순서에 따라
qpos
의 '몇 번째부터 몇 번째'라는 위치(슬롯, slot)를 할당받는다. 그리고 '실험 시작 시의 자세'를 정의하는 키프레임(keyframe)은, 그 qpos를 작성한 단순한 숫자들의 나열이다 ——
qpos
를 값의 나열 그대로 qpos="-0.07 0.03 ... 0.94 ..."와 같이 작성한다. i 번째 값은 i 번째 슬롯에 들어간다. 위치로 결정된다.
자, 나는 턱 힌지(jaw hinge)인 jaw_open을 머리에 추가했다. MuJoCo는 여기에 42번째 슬롯을 할당했다. 그러자 —— 42번 이후의 관절은 전부 하나씩 뒤로 밀려났다. 병의 위치를 유지하는 슬롯도 42번에서 43번으로 이동했다.
그런데, keyframe의 숫자 나열은 48개 그대로 기존 모델의 순서를 끌고 있었다(나는 턱을 추가했지만, 시작 자세의 수열을 업데이트하지 않았다). 모델의 qpos는 49개로 늘어났는데, keyframe은 48개였다. MuJoCo는 부족한 1개를 끝에 0으로 채워 넣는다(padding). 결과적으로, 42번부터 그 이후가 전부 하나씩 밀려서 읽혔다.
도식화하면 다음과 같다.
슬롯 번호: ... 41 42(신규) 43 44 45 46 47 48
본래 내용: ... 팔 끝 [턱=0] 병x 병y 병z 회전… 회전… 회전…
keyframe 실제: ... 팔 끝 0 0 0 -1.57 0.32 0 0(padding)
팔(슬롯 41 이전)은 무사했다 —— 어긋남이 42번부터 시작되었기 때문이다. 하지만 병은 다르다. 여기서 한 가지 중요한 전제를 짚고 넘어가자: 이 병은 '책상 높이(0.94m)를 원점으로 하는 상하 슬라이드 관절' 위에 있으며, keyframe에 들어가는 값은 그 원점으로부터의 상대 변위(meter)를 나타낸다. 원래 병의 상하 슬라이드는 0(=책상 위에 그대로 있음)이어야 했다.
그런데 하나가 밀린 결과, 그 높이 칸에 원래는 옆 관절의 회전각이었던 -1.57이라는 값이 흘러 들어왔다. −1.57은 라디안(radian)으로 측정한 회전각(약 −90도)을 의도한 숫자다. 그것이 그대로 수직 슬라이드의 변위(−1.57 미터)로 읽혔다 —— 각도의 숫자가 거리의 숫자로 변해버린 것이다. 그래서 병은 책상 높이에서 1.57m만큼 가라앉아,
0.94 − 1.57 ≒ −0.63m, 즉 바닥을 뚫고 지나간 지하에 나타났다.
'0.94 − 1.57 = −0.63'이 뺄셈이 아니라 덧셈(상대 변위의 가산)이 되는 이유는, 병의 높이가 절대 좌표가 아니라 책상으로부터의 슬라이드 양이기 때문이다 —— 이 부분을 건너뛰면 'z가 통째로 −1.57로 덮어씌워져야 하는 것 아닌가? 이 일치는 너무 딱 맞아떨어지는(억지스러운) 것 아닌가?'라고 의심할 수 있다. 실제로 이 글의 내역을 검증해 준 별도의 프로세스도, 모델 정의(병이 상하 슬라이드 관절 위에 있다는 점)를 1차 소스(primary source)로 확인하기 전까지는 한때 '파탄 났다'라고 판정할 뻔했다. 숫자가 깔끔하게 맞을수록 기구학적 구조까지 거슬러 올라가 확인한다 —— 그것이 이 시리즈의 신조 그 자체다.
관절을 하나 추가하는 것은 keyframe의 모든 슬롯을 하나씩 미는 것인데, 나는 수열을 옛날 그대로 방치했다. 녹색 테스트는 통과했다(모델은 읽어올 수 있고, 관절 수와 근육 수도 올바르다). 테스트가 감지할 수 없는, 위치 어긋남이라는 조용한 버그다. 그리고 그것을 밝혀낸 것은 테스트가 아니라 —— **영상을 본 인간의
원인을 알게 되면, 해결 방법은 허탈할 정도로 간단하다. 원래의 48개 키프레임(keyframe)에 턱 슬롯(42번째)으로 0(턱이 닫힌 상태 = 각도 0)을 하나 삽입하여 49개로 만드는 것이다. 그렇게 하면 42번 이후는 원래의 슬롯으로 올바르게 밀려나며, 병은 책상 위로 돌아온다.
변경 전(48개): ... 0.3141 0 0 0 -1.57 0.32 0
변경 후(49개): ... 0.3141 0 0 0 0 -1.57 0.32 0
↑ 턱의 0을 1개 삽입
적용하여 다시 측정해보니, 턱 모델은 원래 모델과 수치까지 일치했다.
| 측정한 양 | 원래 모델 | 턱 모델(수정 후) |
|---|---|---|
| 병 시작 z | 0.94 m | 0.94 m |
| ... |
손~병의 상대 위치 벡터 차이는 0.0(측정 정밀도 1e-6 미만으로 일치) —— 즉, 턱 모델은 파지(grasping)와 운반에 관하여 원래 모델과 구별할 수 없는 움직임을 재현했다. 다시 학습할 필요는 전혀 없었다. 망가져 있었던 것은 제어가 아니라, 초기화 데이터의 배열이었기 때문이다.
수리가 끝나고, 드디어 원래 하고 싶었던 장면이 나온다. 대 위의 병으로 손을 뻗어, 잡고, 입가로 가져가며, 그곳에서 턱이 벌어진다.
「입이 벌어지는가」 또한 팔과 병에 가려져 보기 어렵기 때문에, 머리 부분만 잘라내어 시작과 종료를 나란히 배치했다.
왼쪽(시작)은 입이 닫혀 있고, 오른쪽(종료)은 하악이 내려가 입이 벌어지며, 입술에 병이 닿아 있다. 인간의 세 가지 기각 사유 —— 입에 닿지 않음 · 입이 벌어지지 않음 · 무엇을 하는지 알 수 없음 ——는 이것으로 모두 해결되었다. 병은 입술 높이(z=1.56m)까지 운반되고, 입은 벌어지며, 1/3 측면 시점에서 팔의 움직임을 읽을 수 있다.
기분 좋게 마무리하고 싶지만, 내역을 숨기면 거짓말이 된다.
턱의 개폐는 연출이다. 이 모델에는 안면근도 턱 근육도 없으므로, 입은 물리적으로 벌어지는 것이 아니다. 병이 가까워지면, 이쪽에서 하악의 각도를 써넣어 벌리고 있다(kinematic). 81개의 근육이 실제로 풀고 있는 것은 팔의 파지와 운반이지, 입의 개폐가 아니다. 게다가 솔직히 말하자면, 대본은 이 턱관절의 설계상 가동 범위(양의 방향은 +0.05 라디안까지)를 크게 초과하는 +0.6 라디안까지 위치를 직접 써넣어 벌리고 있다 —— qpos를 직접 덮어쓰면 관절 리미트(joint limit)가 작동하지 않기 때문이다. 즉, 입이 벌어지는 것은 근육이 없을 뿐만 아니라 **가동 범위 측면에서도 「물리를 무시한 연출」**임을 명시해 둔다. GIF를 보고 「근육으로 입까지 벌렸다」고 받아들인다면 과장이다. -
「마시는 것」 자체는 하고 있지 않다. 액체 섭취를 시뮬레이션하는 것이 아니라, 병을 벌린 입가로 운반하는 동작이다. 마시는 「제스처(gesture)」이지, 마시는 「생리 현상」이 아니다. -
명령 추종은 아직 약하다. 지난 기사에서 언급했듯이, 이 제어는 「입으로 운반하기」라는 단일 목표에는 도달하지만, 임의의 한 점에 엄격하게 놓는 정밀도는 3분의 1 수준에 머문다. 단일 선형 시너지(linear synergy)의 용량에 관한 이야기는 여기서도 여전히 유효하다 (§3에서 부정했던 것은 「입에 닿지 않는 원인이 용량 때문」이라는 **오귀인(misattribution)**이지, 용량 한계 그 자체는 아니다). -
턱 모델은 파생물이며, 원래의 공개 리포지토리(repository)에는 포함하지 않았다(원래 에셋은 불변 상태로 복사하고, 몸통 정의만 개변). 재현하려면 동일한 개변이 필요하다.
이러한 점들을 제외하더라도, **「근육이 있는 손이 물병을 잡아 입가로 운반하고, 입이 벌어진다」**라는 동작은 실재로서 남는다. 과장은 덜어내고, 실체는 남긴다 —— 그것뿐이다.
이 사건을 통해 내가 얻은 교훈은 세 가지가 있다. 모두 AI나 로봇의 화려한 이야기가 아니라, 수수하지만 반복해서 유효한 것들이다.
정체나 실패의 원인을 「능력의 한계」로 돌리기 전에, 채점표(목표·데이터)를 의심하라. 「입에 닿지 않는 것은 제어 용량이 부족하기 때문이다」는 실제로는 「목표를 목 높이에 두었기 때문」이었다. 알고리즘 탓을 하는 것은 언제나 가장 편하고, 대개 너무 성급하다. -
추측으로 코드를 고치기 전에, 측정하여 범인을 특정하라. 「팔이 어긋났음이 틀림없다」고 생각했으나 측정 결과 틀렸고, 진범은 병의 초기 위치였다. 만약 추측만으로 팔의 제어를 건드렸다면, 올바른 것을 망가뜨렸을 것이다. 측정하는 것은 잘못된 수리를 방지하는 보험이다. -
키프레임(keyframe)이나 초기화 데이터는 「번호(위치)」로 결정된다. 구조(관절)를 추가했다면 반드시 다시 만들어라. MuJoCo에서 관절을 하나 삽입하는 것은 qpos의 모든 슬롯을 하나씩 미는 것이다. 위치 기반의 초기화 데이터를 업데이트하는 것을 잊으면, 테스트는 초록색(pass)을 유지한 채, 물체만 조용히 바닥으로 떨어진다. 그리고 그 조용한 버그를 밝혀낸 것은 테스트가 아니라, 그림을 본 인간의 위화감이었다.
마지막 점이 이 시리즈의 중추에 가장 가깝다. 자동화된 검증은 "고장 나지 않았음"을 보장하지만, "틀린 일을 올바르게 실행하고 있지는 않은가"는 보장하지 않는다. 병은 지면 아래에서 올바르게 잡히기를 기다리고 있었다. 그것을 알아챈 것은 숫자가 아니라, 영상을 보고 "이건 마시고 있지 않다"라고 말한 인간이었다. **인간을 루프에 남기는 것(Human-in-the-Loop)**이란 바로 이런 순간을 위한 것이다.
숫자를 조건 없이 제시하는 것은 불성실한 일이기에, 전제 조건을 나열해 둔다.
모델: MS-Human-700 조작용 변종(Apache-2.0)/ MuJoCo. 머리의 턱 지오메트리(geometry)를 자식 바디(child body) mandible
- 힌지(hinge)
jaw_open
(축 z, 설계 가동 범위 −0.7〜0.05 rad)로 분리된 파생 모델. 자유도(DoF) 48→49, 근육 81개는 불변. 단, 렌더링 시의 턱 벌림은 설계 가동 범위를 초과하는 +0.6 rad까지 qpos에 직접 기입(리미트 미적용)하며, 이는 연출임. -
제어: 근 시너지(Muscle Synergy) 정책(K=8, 관측 22차원=팔 7개 관절의 각도·속도 14 + 손→병 3 + 위상 2 + 명령 3, 파라미터 913개). 이전의 파지(grasping) 챔피언으로부터 웜 스타트(warm-start)하여 입 목표값으로 CMA-ES 진화. -
버그와 수정: 턱 힌지는 qpos의 42번째에 들어가며, 이후의 슬롯을 하나씩 밀어낸다. 키프레임(keyframe)이 48개인 상태(위치 기반)로 유지되었기 때문에, MuJoCo가 끝부분을 0으로 채우면서(zero-padding) 42번 이후가 모두 하나씩 밀렸고, 병의 상하 슬라이드 항목(책상 높이 0.94m로부터의 상대 변위)에 회전각 −1.57이 들어가 z=0.94−1.57=−0.63m로 낙하했다. 수정 사항=기존 48개의 키프레임 42번째에 0.0을 삽입하여 49개로 만듦. 끝부분의 실젯값을 보면:
import numpy as np
# orig_key48 = 원본 모델의 keyframe(48개 값)
broken = np.concatenate([orig_key48, [0.0]]) # 끝에 0을 더했을 뿐 = MuJoCo의 자동 패딩(padding)과 동일 (고장 남)
...
수정 후, 턱 모델은 원본 모델과 손→병 벡터까지 일치한다(차이 0.0, 측정 정밀도 1e-6 미만).
수치의 이력: 원본/턱 모델의 리셋(reset) 직후 qpos · 세계 좌표의 직접 비교, 그리고 학습된 정책의 결정적인 재실행(4초 롤아웃(rollout), 134프레임, 입으로의 최접근 0.031m, 최종 턱 벌림 정도 0.6 rad)으로부터 취득. 모두 노트북 CPU 사용. 이 글의 모든 정량적 주장은 공개 전 독립된 별도의 프로세스(3개 계열)를 통해 1차 데이터 및 실제 코드로 재대조하여 모든 CONFIRMED를 확인했다. -
솔직한 구분: 팔의 파지·운반은 81개 근육의 폐루프(closed-loop) 제어다. 턱의 개폐는 기구학적(kinematic, 대본)이며, 안면 근육이 탑재되지 않았기에 연출된 것이다.
마시는 단계까지 왔으므로, 다음은 젓가락으로 먹기——음식으로 간주한 물체를 집어 입으로 옮기는 동작을 이 해부학적인 손에 얹고 싶다(이전 글에서 "손이 없다"라고 숙제로 남겼던 젓가락 이야기를 진짜 손으로 회수한다). 그리고 하나 더, 마시는 동작의 명령 추종을 강화하는 다중 목표 훈련——"입으로"뿐만 아니라 "임의의 한 점으로 엄격하게"를, 단일 시너지의 용량 한계까지 측정하러 갈 것이다.
이 글의 수치는 난수 시드(random seed)를 고정한 재현 가능한 CPU 실행에서 가져왔다. 좋은 결과일수록 내역을 의심하고, 막히면 추측이 아니라 측정한다——그것이 내가 이 시리즈에서 지키고 있는 단 한 가지다. 이번에는 "마실 수 있었다"라는 결과 뒤에, 1.57m 떨어져 바닥 아래(z=−0.63m)로 가라앉은 병이 숨어 있었다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기