G1 보행 정책을 학습시키면서 한동안 이상한 현상을 보고 있었습니다. 로봇의 발 교차를 실패로 판정하도록 평가기를 만들었는데, 학습된 로봇은 특정 구간에서 계속 두 발을 겹쳤습니다. 처음에는 보상 함수가 약하거나 PPO가 편법을 찾았다고 생각했습니다. 그런데 프레임을 거꾸로 추적해 보니 문제는 정책보다 앞에 있었습니다. 정책이 따라야 할 정답 동작 자체가 발을 교차하고 있었습니다.
전체 참조 모션 13,065프레임 가운데 3.9418%에서 signed foot separation이 음수였고, 최솟값은 -0.2831m였습니다. 더 결정적인 수치는 따로 있었습니다. 학습된 모델에서 발이 교차한 행의 99.31%가 참조 모션의 교차 구간과 겹쳤습니다. 로봇이 정답을 못 따른 것이 아니라, 정답을 꽤 충실하게 따라간 결과가 우리가 금지한 동작이었던 셈입니다.
이 사건 이후 제 프로젝트에서 reference trajectory는 더 이상 “사람처럼 걷는 애니메이션 파일”이 아닙니다. 출처가 확인된 원본 모션을 G1의 29개 관절에 맞게 옮기고, 관절 순서와 좌표계 계약을 검사하고, 속도와 형태가 적절한 구간만 골라 50Hz로 변환하고, Isaac Sim에서 forward kinematics를 계산한 뒤, 마지막으로 학습 코드가 그 값을 정말 참조값으로 읽는지까지 확인한 학습 자산입니다.
이 글은 G1 학습 방법 전체 정리 가운데 모션 리타겟팅과 reference trajectory 공급망만 떼어 자세히 기록합니다. PPO의 수식이나 보상 설계보다 한 단계 앞의 질문, 즉 “정책이 따라야 할 정답은 어떻게 만들어지는가?”에 집중합니다.

사람의 BVH 파일을 G1에 바로 넣을 수 없는 이유
BVH는 사람 골격의 계층 구조와 시간에 따른 관절 회전을 표현합니다. LAFAN1의 걷기 동작을 재생하면 사람이 자연스럽게 앞으로 걷는 모습이 보입니다. 하지만 이 파일을 G1 정책에 그대로 입력할 수는 없습니다. 사람과 G1은 골격의 길이도, 관절 수와 회전축도, 관절 가동 범위도 다릅니다. 사람의 어깨와 척추를 표현하는 방식이 G1의 waist와 7자유도 팔에 일대일로 대응하지도 않습니다.
제 파이프라인의 원본 후보는 두 종류입니다. 첫 번째는 LAFAN1 공식 데이터셋이나 100STYLE의 사람 BVH입니다. 두 번째는 이미 Unitree G1에 맞게 리타겟된 LAFAN1 CSV입니다. 전자는 원본부터 변환 과정을 통제할 수 있지만 리타겟팅 품질을 직접 책임져야 합니다. 후자는 빠르게 재생 가능한 36열 결과를 얻을 수 있지만, 그 결과가 어떤 solver와 가정으로 만들어졌는지 확인하기 어렵습니다. 둘 중 하나가 항상 우월한 것이 아니라, 서로 다른 실패 가능성을 가진 두 공급자입니다.
사람 BVH 경로에서는 GMR(General Motion Retargeting)을 사용합니다. 현재 구현은 source human을 LAFAN1 BVH로, target robot을 Unitree G1으로 지정하고 DAQP solver로 각 프레임의 G1 자세를 구합니다. 실제 사람 키는 1.75m로 두었고, 필요하면 관절 속도 제한도 적용합니다. 100STYLE은 뼈 이름과 전방축이 달라 별도의 bone alias와 local forward +Z 규칙을 둡니다. 라이브러리가 두 형식을 모두 읽는다고 해서 같은 옵션으로 처리하면 안 되는 이유입니다.
리타겟팅 결과 한 프레임은 36개의 generalized coordinate로 정규화됩니다.
qpos[0:3] = root position (x, y, z)
qpos[3:7] = root quaternion (x, y, z, w) # CSV 계약
qpos[7:36] = 29 G1 joint positions
29 joints = 12 leg + 3 waist + 14 arm
여기서 36은 임의의 feature dimension이 아닙니다. free root의 위치 3개와 quaternion 4개, G1 관절 29개를 더한 물리 상태의 계약입니다. MuJoCo 내부 qpos에서는 quaternion을 WXYZ로 다루므로 CSV의 XYZW와 변환할 때 순서를 바꿔야 합니다. 관절값도 이름 없이 29개 숫자만 맞으면 되는 것이 아닙니다. left_hip_pitch_joint부터 양쪽 다리, waist, 양팔까지 actuator와 정확히 같은 순서여야 합니다. 열 하나가 밀려도 배열 shape은 정상이고 시뮬레이터는 실행될 수 있습니다. 다만 무릎에 줄 명령이 허리로 가는 식의 조용한 오류가 됩니다.
리타겟팅 스크립트는 변환기보다 계약 검사기에 가깝습니다
초기에는 BVH를 CSV로 만들기만 하면 리타겟팅이 끝났다고 생각하기 쉽습니다. 실제로 중요한 코드는 solver 호출보다 그 앞뒤의 fail-closed 검사입니다. 현재 retarget_g1_bvh_gmr.py는 출력이 반드시 [frames, 36]인지, NaN이나 Inf가 없는지, root quaternion의 norm이 0이 아니며 정규화됐는지 확인합니다. 이미 리타겟된 CSV를 받을 때는 입력 fps가 정확히 30인지도 검사합니다.
G1 모델 쪽에서는 MuJoCo의 nq=36, nv=35, nu=29를 확인합니다. 29개 관절의 qpos address가 7부터 35까지 연속인지, actuator 순서와 선언한 joint order가 같은지, 각 프레임의 값이 관절 한계를 벗어나지 않는지도 봅니다. 이 가운데 하나라도 다르면 “대충 비슷한 G1″로 계속 진행하지 않고 변환을 중단합니다.
이렇게 엄격하게 만든 이유는 모션 파일의 오류가 학습 단계에서 쉽게 다른 문제로 위장하기 때문입니다. quaternion 순서가 틀리면 root가 비정상적으로 회전하지만 관측 정규화나 reset 문제로 보일 수 있습니다. 관절 순서가 틀리면 자세가 무너지지만 reward weight가 약한 것으로 해석할 수 있습니다. 입력 fps가 다르면 속도와 접촉 주기가 달라지는데, PPO의 action scale이나 control decimation을 의심하게 됩니다. 학습을 시작한 뒤에는 원인을 분리하기가 훨씬 비쌉니다.
| 계약 | 현재 기준 | 어겼을 때 보이는 증상 | 처리 |
|---|---|---|---|
| 프레임 shape | [F, 36] |
열 누락·추가, joint offset | 즉시 중단 |
| root quaternion | CSV XYZW, unit norm | root 회전·각속도 폭주 | 순서 변환 후 정규화 검사 |
| G1 joint order | 29개 이름·주소·actuator 일치 | 다른 관절이 움직이는 조용한 오류 | exact match |
| 입력 시간축 | pre-retargeted CSV 30fps | 보행 속도·접촉 주기 왜곡 | 명시되지 않으면 거부 |
| 관절 한계 | G1 model limit 이내 | 초기화 clipping, 비현실적 자세 | 프레임별 검사 |
| 계보 | source·revision·license·SHA256 | 같은 이름의 다른 자산 혼입 | metadata에 고정 |
좌표계와 quaternion 자체는 world·body·local frame과 quaternion 정리에서 따로 설명했습니다. 여기서 강조할 부분은 수학 공식을 아는 것과 데이터 계약을 자동 검사하는 것이 다르다는 점입니다. 사람이 파일을 한 번 열어 보고 “자세가 대충 맞는다”고 판단하는 대신, 학습 전에 틀린 입력이 통과하지 못하도록 코드를 두어야 합니다.
전체 모션보다 먼저 ‘학습할 구간’을 고릅니다
리타겟팅이 성공했다고 전체 프레임을 곧바로 변환하지 않습니다. 긴 모션에는 출발, 정지, 방향 전환, 몸을 돌리는 동작, 촬영용 과장 동작이 함께 들어 있습니다. 저는 현재 보행 속도를 slow 0.20~0.45m/s, nominal 0.55~0.90m/s, fast 0.95~1.40m/s 세 구간으로 나누고, 일정 길이의 window를 검색합니다. straightness가 0.80 이상인지, 속도 변동계수가 0.60 이하인지, 정지 프레임 비율이 0.10 이하인지가 첫 번째 조건입니다.
그다음에는 로봇 형태에 맞는지 봅니다. waist 평균 각도, hip roll/yaw, pelvis-to-torso pitch, 보행 중 양발이 모두 뜨는 flight fraction, 팔과 반대쪽 hip의 결합, signed foot separation을 계산합니다. 현재 pre-screen의 대표 상한은 waist와 hip roll/yaw 0.15rad, pelvis-to-torso pitch 0.20rad, walking flight fraction 0.05입니다. 발 교차는 허용하지 않습니다.
M40c에서 17개 window를 검사했을 때 통과한 후보는 0개였습니다. local GMR로 만든 slow·nominal·fast 샘플은 모두 waist나 torso 형태 조건에서 탈락했습니다. 그래서 리타겟팅 코드를 바로 고치지 않고, 완전한 G1 artifact를 제공하는 두 번째 공급자인 Unitree-retargeted 데이터로 같은 검사를 반복했습니다. 이 경로는 36열, quaternion, joint order, limit, provenance를 모두 통과했고 waist와 torso 지표도 전반적으로 나아졌습니다.
M40e에서는 241개의 안정적인 window를 찾았습니다. slow 169개, nominal 72개였고 fast는 하나도 없었습니다. 형태 조건까지 통과한 것은 slow 1개, nominal 16개, fast 0개였습니다. 최종 정량 후보로 slow walk2_subject4[630:990)와 nominal walk3_subject4[240:600)를 골랐습니다. 평균 속도는 각각 0.401m/s와 0.731m/s, 최소 발 간격은 +0.093m와 +0.105m였습니다.
하지만 이 둘도 바로 “승인된 reference”라고 부르지 않았습니다. 이 단계에서 증명된 것은 몇 개의 수치 gate를 통과했다는 사실뿐입니다. Isaac Sim 재생 화면에서 관절이 부자연스럽지 않은지, 팔과 몸통의 형태가 사람 걸음처럼 보이는지, contact-aware metric과 세 속도 coverage가 충족되는지는 별도 문제입니다. 후보를 찾은 것과 학습 정답을 승인한 것은 다른 사건입니다.
30Hz CSV를 50Hz FK-complete NPZ로 바꾸는 과정
선정된 G1 CSV는 convert_g1_motion_csv.py를 거쳐 50Hz NPZ가 됩니다. 현재 정책 주기가 0.02초이므로 한 policy step마다 참조 프레임 하나를 소비하려면 50Hz 시간축이 자연스럽습니다. 단순히 30fps 프레임을 복제하지 않고, 새 timestamp에서 root 위치와 관절 위치는 선형 보간하고 root quaternion은 SLERP로 보간합니다. 관절 속도와 root 선속도는 50Hz 간격의 gradient로 구하고, root 각속도는 인접 quaternion의 상대 회전을 이용해 계산합니다.
input: G1 qpos CSV, 30 Hz, shape [F, 36]
root position -> linear interpolation
root quaternion -> SLERP
joint position -> linear interpolation
linear velocity -> finite difference at 0.02 s
angular velocity -> relative quaternion / 0.02 s
output: FK-complete NPZ, 50 Hz
변환기의 핵심은 보간보다 forward kinematics입니다. 각 프레임의 root와 29개 joint state를 Isaac articulation에 쓰고, 시뮬레이션의 kinematic state를 갱신해 모든 추적 body의 위치·자세·선속도·각속도를 얻습니다. 이 때문에 최종 NPZ에는 관절 상태만 들어 있지 않습니다.
| NPZ key | 역할 | 학습에서 쓰이는 곳 |
|---|---|---|
fps |
참조 시간축 | duration·frame progression 검사 |
joint_names, body_names |
열과 body 의미 고정 | G1 asset과 exact mapping |
joint_pos, joint_vel |
29개 관절 목표 | policy command·joint tracking error |
body_pos_w, body_quat_w |
전신 위치·자세 목표 | anchor·body tracking error |
body_lin_vel_w, body_ang_vel_w |
전신 속도 목표 | velocity tracking error |
공식 whole_body_tracking 구현도 입력 reference가 이미 robot generalized coordinate로 리타겟돼 있다고 가정하고, CSV를 forward-kinematics 정보가 포함된 NPZ로 바꿉니다. 제 로컬 파이프라인은 registry에서 모션을 받는 대신 명시적인 로컬 파일 경로와 SHA256을 사용하고, shape·이름·frame count를 추가로 검사하는 방향으로 경계를 강화했습니다.
check_g1_motion_tracking_compat.py는 필수 key가 모두 있는지, joint_pos가 [F, 29]인지, 모든 시계열의 frame count가 같은지 확인합니다. upstream 환경 import와 G1 action scale까지 확인한 뒤에야 compatible을 반환합니다. CSV 변환 명령이 성공했다는 이유만으로 학습 명령으로 넘어가지 않도록 preflight를 분리해 둔 것입니다.
reference의 phase는 별도 숫자가 아니라 현재 프레임입니다
모션 트래킹 설명에서 “phase를 관측한다”는 말을 자주 쓰지만, 현재 NPZ에는 phase라는 별도 scalar가 없습니다. 실제 구현에서 phase의 역할을 하는 것은 환경마다 보유한 time_steps index입니다. MotionCommand는 이 index로 joint_pos, joint_vel, body pose와 velocity의 현재 참조 프레임을 꺼냅니다. 매 policy update마다 index를 1 증가시키고, 모션 끝에 도달한 환경은 새 시작 프레임을 뽑습니다.
시작 프레임은 항상 0이 아닙니다. adaptive sampler가 모션을 여러 bin으로 나누고, 이전 episode가 자주 실패한 구간에 더 높은 확률을 줍니다. reset된 환경은 선택된 bin 안에서 임의의 frame으로 이동합니다. 따라서 현재 phase는 “frame index / 전체 frame 수”에 가까운 진행 상태이며, fps와 policy time, reset sampling 규칙을 함께 알아야 실제 시간을 복원할 수 있습니다. physics·policy·rollout 시간축을 reference 시간축과 분리해 적어 둔 이유도 여기에 있습니다.
모션의 일부만 자를 때는 slice_g1_motion_npz.py를 사용합니다. 구간은 [frame_start, frame_end)로 고정하고, 모든 time-series key를 같은 범위로 자릅니다. 새 첫 프레임의 pelvis XY를 원점으로 만들기 위해 모든 body XY를 함께 rebase합니다. duration은 단순히 frame_count / fps가 아니라 샘플 사이 간격을 반영한 (frame_count - 1) / fps로 기록합니다. 원본과 결과 SHA256, 정확한 frame interval도 metadata에 남깁니다.
M1: 오래 살아남는 것과 경로를 따라 걷는 것은 달랐습니다
첫 전체 학습인 M1에서는 Unitree-retargeted LAFAN1의 walk1_subject1 전체 13,065프레임을 50Hz FK-complete NPZ로 변환했습니다. actor observation 160차원, critic observation 286차원, action 29차원, 14개 tracked link와 9개 reward term을 확인한 뒤 4,096개 병렬 환경에서 PPO를 model 298까지 학습했습니다.
결정론적 64환경, 10초 평가에서 survival은 95.31%였고 body position error는 0.0666m였습니다. 이 숫자만 보면 첫 결과로 꽤 괜찮아 보입니다. 하지만 로봇은 1.670m 이동한 반면 같은 시간의 reference는 3.720m 전진했습니다. planar tracking error는 2.256m, 진행률은 43.35%였습니다. 쓰러지지 않고 관절 자세를 어느 정도 흉내 냈지만, 참조가 이동한 경로의 절반도 따라가지 못했습니다.
이 경험은 reference 검사의 범위를 바꿨습니다. 관절 자세가 자연스러워 보이는지와 별개로 root displacement, heading, route-valid row를 측정해야 했습니다. 이후 평가에서는 survival을 안전 gate로 두되, 경로 진행률과 planar error를 acceptance에 따로 넣었습니다. seed·scenario·metric·acceptance gate 계약에서 학습 곡선과 held-out 평가를 분리한 배경이기도 합니다.
M13과 M14: 평가 기준과 참조 모션이 서로 모순됐습니다
M13에서 발 교차를 phase별로 추적하자 문제가 선명해졌습니다. 전체 reference의 교차는 주로 phase bin 16~19에 몰려 있었습니다. 그런데 평가기는 signed foot separation이 0보다 작으면 정책 실패로 판정했습니다. 학습 목적에는 “이 자세를 따라가라”고 넣고, 평가 목적에는 “이 자세를 만들면 탈락”이라고 적은 셈이었습니다. reward tuning으로 해결할 수 있는 문제가 아니었습니다.
먼저 reference에서 교차가 없고 연속적으로 걸을 수 있는 구간 [6956, 10125)를 찾았습니다. 3,169프레임, 50Hz 기준 63.36초이며 최소 발 간격은 +0.0818m였습니다. M14에서는 PPO를 한 번도 업데이트하지 않고 model 1591을 이 safe slice에서 다시 평가했습니다. 64개 환경이 모두 살아남았고 발 교차는 0건으로 사라졌습니다. 진행률도 68.26%로 올랐습니다.
그 뒤 100 update를 학습한 model 1690은 survival 64/64, crossing 0을 유지하면서 진행률 88.85%, body error 3.75cm까지 개선됐습니다. 하지만 미리 정한 route threshold 95%에는 못 미쳤으므로 승인하지 않았습니다. 여기서 중요한 것은 M14의 수치 향상보다 인과관계입니다. 정책 코드를 바꾸지 않고 reference 구간만 바꿨는데 교차가 사라졌습니다. 이 대조 실험으로 문제의 주된 원인이 정책이 아니라 참조에 있었음을 좁힐 수 있었습니다.
M40과 M50: 더 좋은 reference가 더 좋은 숫자를 만들었지만, 보기 좋은 걸음은 아니었습니다
M40에서는 임시 safe slice를 넘어 재사용 가능한 reference 공급망을 만들었습니다. local GMR 경로와 이미 리타겟된 Unitree 경로를 같은 36열 계약과 형태 gate로 비교하고, source URL·archive hash·dataset revision·라이선스·GMR revision을 metadata에 고정했습니다. LAFAN1 공식 저장소의 라이선스는 CC BY-NC-ND 4.0이므로 원본 frame이나 파생 trajectory를 블로그에 배포하지 않고 내부 비상업 실험 자산으로만 유지합니다. 이 글의 그림도 실제 모션 프레임을 싣지 않고 파이프라인과 집계 수치만 보여 줍니다.
M50은 이 과정을 통과한 nominal 후보 walk3_subject4[240:600)를 사용한 reference swap 실험입니다. M15와 비교해 아홉 개 reward와 학습 계약은 그대로 두고 모션 파일만 바꿨습니다. 4,096환경, seed 42, 100 PPO update 뒤 여러 checkpoint를 같은 조건으로 평가했습니다. model 1875는 전 환경 생존을 유지했고 planar error가 baseline 0.13834m에서 0.05561m로 줄었습니다. 발 교차는 0건, 최소 발 간격은 0.28385m였습니다.
흥미로운 점은 마지막 model 1888의 reward가 더 높았지만 planar error는 0.06526m로 다시 나빠졌다는 것입니다. 그래서 reward가 가장 높은 마지막 checkpoint가 아니라, 미리 정한 metric에서 더 나은 model 1875를 선택했습니다. 그런데 Isaac Sim 재생에서는 무릎이 중앙으로 겹쳐 보였고 hip·waist·torso가 어색했으며 팔은 비대칭적이고 약하게 움직였습니다. 정량 tracking은 좋아졌지만 사람다운 걸음의 reference로는 거부했습니다.
이 결과는 “좋은 reference”라는 말을 둘로 나누게 했습니다. 첫째는 로봇이 안정적으로 따라갈 수 있고 route·separation 수치가 좋은 reference입니다. 둘째는 원하는 morphology와 contact pattern을 실제로 담은 reference입니다. M50은 첫 번째 기준에서는 진전이었지만 두 번째 기준을 통과하지 못했습니다. 숫자가 좋아졌다고 시각 검수를 생략할 수 없는 이유입니다.
M74와 M75: 모션 파일이 맞아도 소비자가 잘못 읽으면 오차는 0이 됩니다
참조 자산을 엄격하게 검사한 뒤에도 더 위험한 오류가 남았습니다. M72와 M73에서 torso morphology error를 계산하는 코드가 reference body pose가 아니라 현재 로봇의 robot_body_pos_w와 robot_body_quat_w를 읽고 있었습니다. 비교식의 양쪽이 같은 현재 상태였기 때문에 update 25와 125에서 수집한 6,400개 표본의 torso roll·pitch·yaw reference-minus-robot error가 정확히 0이었습니다.
# 잘못된 배선: current robot state를 reference 자리에 사용
reference_pose = command.robot_body_pos_w
# 수정된 배선: motion frame을 robot anchor에 맞춘 reference
reference_pose = command.body_pos_relative_w
reference_quat = command.body_quat_relative_w
실행은 재현 가능했고 loss도 finite였기 때문에 표면적으로는 정상적인 실험처럼 보였습니다. 하지만 objective의 의미는 무효였습니다. 현재 상태와 현재 상태의 차이는 언제나 0이므로, torso를 reference에 맞추라는 학습 신호가 생길 수 없었습니다. M75에서 source를 body_pos_relative_w와 body_quat_relative_w로 바꾸고 canary를 돌리자 reference error가 거의 모든 표본에서 0이 아닌 값으로 나타났습니다.
그 상태로 4,096환경, 125 update, 약 1,228.8만 transition을 새로 학습했습니다. lower-body·torso composite error는 1.54133에서 1.04133으로 32.44% 줄었고 survival 100%, crossing 0을 유지했습니다. 그래도 foot·pitch·yaw·right-arm gate가 남아 있어 시각 검수 후보로만 올렸습니다. 배선을 바로잡은 것은 목표를 달성한 것이 아니라, 목표를 실제로 학습할 수 있는 상태로 만든 것입니다.
이 오류 때문에 reference 검사는 파일에서 끝나지 않게 됐습니다. 이제는 별도의 canary에서 “로봇이 reference와 다른 자세일 때 error가 실제로 0이 아닌가”, “current-state tensor와 reference tensor가 같은 storage나 식으로 alias되지 않았는가”, “time step을 바꾸면 reference가 변하는가”를 확인해야 합니다. PPO 학습 로그 읽기에서 custom loss가 존재하는 것과 올바른 행동 신호라는 것을 분리한 이유도 같습니다.
루프가 가능한 모션인지도 별도 품질 계약으로 검사합니다
긴 reference를 끝까지 재생한 뒤 처음으로 되돌리거나, 짧은 clip을 반복 학습하면 마지막 프레임과 첫 프레임의 seam이 새로운 충격을 만듭니다. 그래서 g1_motion_reference_quality.py는 root 높이 차이, pelvis 기울기, 관절 위치·속도, root 선속도·각속도의 양 끝 차이를 계산합니다.
현재 대표 gate는 root Z gap 0.08m 이하, pelvis tilt geodesic gap 0.20rad 이하, joint position RMS 0.35rad 이하, joint velocity RMS 3rad/s 이하입니다. joint position 최대 차이는 1.0rad, joint velocity 최대 차이는 8rad/s, root 선속도 gap은 0.50m/s, 각속도 gap은 1.50rad/s를 넘지 않아야 합니다. 이 값들은 보행의 보편적인 진리가 아니라 현재 프로젝트가 실패를 분리하기 위해 세운 운영 기준입니다. reference 종류와 clip 길이가 바뀌면 다시 검증해야 합니다.
루프 seam을 통과해도 waist·hip·arm 형태, pelvis-to-torso 자세, 접촉 proxy, signed foot separation, bilateral coupling, 명시적 속도 조건, 최소 reference 개수, 시각 승인 여부를 함께 기록합니다. 하나의 점수로 합치지 않는 이유는 실패 원인을 보존하기 위해서입니다. seam은 좋은데 팔이 죽어 있을 수 있고, 발 간격은 안전하지만 double support가 지나치게 길 수 있습니다.
reference promotion은 파일 확장자가 아니라 상태 전이입니다
현재 파이프라인에서는 NPZ가 존재한다는 사실만으로 학습에 사용할 수 없습니다. artifact metadata에 diagnostic_only, source_static_gate_passed, training_eligible, reference_promoted 상태를 둘 수 있습니다. diagnostic-only이거나 상태끼리 모순되면 학습 스크립트가 거부합니다. 승인 flag가 없는 옛 자산은 호환을 위해 읽을 수 있지만, 파생 자산을 만들 때는 source SHA와 promotion 근거를 함께 남깁니다.
| 상태 | 의미 | 허용하는 작업 |
|---|---|---|
| raw source | 출처·라이선스만 확인한 BVH/CSV | inventory, retarget |
| retargeted candidate | 36열·joint order·limit 통과 | window search, replay |
| diagnostic reference | FK NPZ지만 품질 gate 미완료 | 검사·디버깅만 |
| training eligible | 정적·시간축·형태 조건 통과 | bounded PPO canary |
| promoted reference | held-out·시각·계보 승인 완료 | 학습 block의 기준 자산 |
train_g1_motion_tracking.py도 모션 파일을 필수 인자로 받고, eligibility를 확인한 뒤 env_cfg.commands.motion.motion_file에 주입합니다. 한 번의 update로 배선만 확인하는 canary와 실제 학습 block을 역할로 구분해, smoke test checkpoint를 “학습된 보행”으로 보고하지 못하게 했습니다. checkpoint 자체의 계보는 모델·optimizer·normalizer·resume 계약에서 이어집니다.
지금 남은 문제는 ‘세 속도에서 승인된 사람다운 reference’입니다
현재 파이프라인은 사람 BVH와 Unitree-retargeted CSV를 G1 36차원 계약으로 받아 50Hz FK-complete NPZ로 만들 수 있습니다. 발 교차가 없는 구간을 찾고, 속도·straightness·waist·hip·torso·arm·flight 조건으로 후보를 줄이고, 루프 seam과 소비자 alias까지 검사합니다. 과거처럼 아무 reference나 넣고 reward를 조정하는 단계에서는 벗어났습니다.
그렇다고 최종 목표를 달성한 것은 아닙니다. M40e에서는 fast 후보가 0개였고, slow와 nominal 정량 후보도 시각·contact-aware 검수를 끝내지 못했습니다. M50은 route와 planar error를 개선했지만 무릎·hip·waist·팔 형태 때문에 거부됐습니다. M75 이후에는 올바른 reference error를 학습할 수 있게 됐지만 foot·contact 계열 gate가 남았습니다. 지금의 병목은 PPO를 더 오래 돌리는 것이 아니라, 세 속도에서 형태와 접촉을 함께 통과하는 reference set을 승인하는 일입니다.
다음 실험은 새 reward를 먼저 추가하지 않습니다. slow·nominal·fast 각각에서 source hash가 고정된 후보를 확보하고, 동일한 loop-seam·morphology·contact 표를 생성한 뒤 Isaac Sim replay와 CAM 검수 결과를 artifact metadata에 묶습니다. 그다음 canary에서 reference와 robot tensor의 비동일성, frame progression, non-zero error를 자동 검증하고, 같은 seed와 checkpoint에서 reference만 교체하는 대조 실험을 수행합니다.
이번 시행착오에서 가장 크게 바뀐 생각은 간단합니다. 모션 트래킹에서 reference는 정답처럼 주어지는 외부 입력이 아닙니다. 정답도 검증해야 하는 모델 구성요소입니다. 사람이 자연스럽게 걸은 기록이라도 G1에 옮기는 순간 관절 구조와 시간축, 접촉, solver, 데이터 계보의 영향을 받습니다. 그리고 완벽한 파일을 만들어도 학습 코드가 현재 로봇 상태를 참조값으로 읽으면 모든 검사는 무의미해집니다.
그래서 지금 제가 관리하는 파이프라인은 BVH에서 끝나지 않습니다. source → retarget → curate → Isaac FK → MotionCommand → PPO → held-out evaluation 전체를 하나의 reference 공급망으로 봅니다. 발 교차를 없애기 위해 시작한 추적이 결국 “정답이 어디에서 만들어지고, 누가 그 정답을 읽는가”를 명시하는 작업으로 확장된 셈입니다.
기술 기준 시점: 2026-07-22. 구현 설명은 origin/main@ee4d96e의 training/rl/retarget_g1_bvh_gmr.py, convert_g1_motion_csv.py, slice_g1_motion_npz.py, check_g1_motion_tracking_compat.py, g1_motion_artifact_eligibility.py, training/eval/g1_motion_reference_quality.py 및 G1 M1·M13~M14·M40·M50·M74~M75 실험 기록을 기준으로 작성했습니다. LAFAN1 모션 자산은 공식 라이선스 조건에 따라 내부 비상업 실험용으로만 유지하며, 이 글은 원본 또는 파생 모션 데이터를 배포하지 않습니다.