10. 제대로 된 걸음 — “그런데 저거, 진짜 걷는 거 맞아?”

이번 단계 목표: 06~09번의 로봇이 정말 ‘걷고’ 있는지 의심해 봅니다. 걸음을 수치로 측정해 실제 휴머노이드 보행과 비교하고, 발견한 문제(스포일러: 심각합니다)를 걸음 교정 보상으로 고쳐 재학습합니다.


발단: 영상을 다시 보다가 생긴 의심

09번까지 끝내고 영상들을 다시 보는데, 뭔가 이상했습니다. 로봇이 앞으로 잘 가고, 돌라면 돌고, 밀어도 안 넘어지는데… 다리가 사람처럼 성큼성큼 움직이지 않습니다. 발을 종종거리는 것 같기도 하고. 카메라가 따라가는 영상에선 몸통이 매끄럽게 전진하니 그럴듯해 보이는데, 정말 ‘걷는’ 걸까요?

의심이 들면 재야 합니다. 학습된 정책을 10초 걷게 하면서 발의 접촉·위치·속도를 매 스텝 기록해 걸음 지표를 뽑았습니다.

진단: 그것은 걸음이 아니었다

지표 우리 정책 (06번, 실측) 실제 휴머노이드/사람 보행 판정
걸음 빈도 발당 8.25 Hz (스윙 40ms) 사람 느린 걷기 발당 ~0.7–0.9Hz, 실물 G1 ~1.5–2Hz 10배 빠른 잔발
발 들림 높이 최대 1.1cm 5–10cm 사실상 발을 끎
한 걸음 보폭 6cm 사람 0.5m/s 기준 30–40cm 종종걸음
좌우 발 간격 55cm 실물 G1 ~20cm 안팎 다리를 쫙 벌림
무릎 가동 범위 10–24° 사람 보행 ~5–60° 뻗정다리
몸통 높이 출렁임 ±1mm 실제 보행은 수 cm씩 주기적으로 오르내림 미끄러지듯 감

6cm × 8.25Hz ≈ 0.5m/s. 속도는 정확히 명령대로인데, 그 속도를 내는 방법이 ‘걷기’가 아니라 다리를 벌리고 발을 1cm만 들어 초당 16번 잔발을 치는 셔플(shuffle)이었습니다. 스케이트 타듯 미끄러져 가는 거죠.

왜 이렇게 배웠나 — 보상표를 다시 읽어 보면

우리 보상표(06번)를 ‘최적화기의 눈’으로 다시 읽어 보면, 셔플은 버그가 아니라 고득점 답안입니다:

  • tracking_lin_vel은 속도만 채점합니다. 어떻게 내는지는 안 봅니다.
  • lin_vel_z(출렁임 벌점)·base_height(키 유지)는… 실제 걸음이 원래 출렁이는 운동이라, 미끄러지기가 진짜 걸음보다 고득점입니다.
  • 발 들림·박자·보폭·발 간격은 채점표에 없습니다. 채점 안 하는 자유도는 ‘속도 내기 가장 편한 값’으로 갑니다: 전부 최소화.
  • 다리 kp 350(아주 뻣뻣한 제어)이 8Hz 잔발을 물리적으로 가능하게 했습니다.

이 현상은 RL에서 유명한 보상 해킹(specification gaming)입니다. 워낙 중요한 주제라 별도 연구 노트로 정리했습니다 → R01. 보상은 빈틈을 찾는다

처방: 걸음의 ‘형태’를 채점표에 넣는다

Unitree가 실제 G1 학습에 쓰는 공식 설정(unitree_rl_gym)을 참고해, 우리 채점표에 없던 걸음 형태 항목을 추가했습니다 (g1_train.py --task gait):

새 보상 항목 채점 내용 잡는 문제
contact (+0.18) 박자(0.8초 주기 위상 클록)에 맞춰 왼발·오른발이 번갈아 닿는가 잔발·비정형 스텝
feet_swing_height (−20) 스윙 중인 발이 8cm 올라갔는가 발 끌기
contact_no_vel (−0.2) 땅에 닿은 발이 미끄러지지 않는가 스케이팅
hip_pos (−1.0) 엉덩이 roll/yaw가 벌어지지 않았는가 쩍벌 자세
dof_vel/dof_acc 관절 속도·가속 벌점 (에너지) 8Hz 진동
alive (+0.15) 서 있기만 해도 약간 + 초반 학습 안정

박자를 지키려면 정책이 박자를 알아야 하므로, 관측에 위상 클록(sin/cos 2개)을 추가했습니다 (관측 45→47차원). PD 게인도 공식 값(엉덩이 100/무릎 150/발목 40)으로 낮춰 잔발의 물리적 토대를 없앴습니다. 명령 추종(steer)은 그대로 유지 — 조종 가능하면서 제대로 걷는 정책이 목표입니다.

cd scripts/train
python g1_train.py --task gait --max_iterations 3000    # 학습 (GPU)
python g1_eval.py  --task gait --ckpt 2999 --record     # 조종 시나리오 + 녹화

재학습 결과

4096개 병렬 환경 × 3000 iteration, 36분 걸렸습니다. 채점 항목이 14개로 늘었는데도 학습은 안정적이었고(iteration 300에 이미 박자 보상이 수렴 근처), 보상 37.6에서 수렴했습니다.

같은 잣대(g1_gait_metrics.py)로 before/after를 측정한 결과가 이 단계의 성적표입니다:

지표 06번 (셔플) 10번 (교정 후) 실제 보행 기준
걸음 빈도 발당 8.25 Hz 1.25 Hz 0.7–2 Hz
스윙 시간 40ms 340ms 300–450ms
발 들림 최대 1.1cm 3.6–4.5cm 5–10cm 근접
보폭 6cm 32cm 30–40cm
좌우 발 간격 55cm 15cm ~20cm
무릎 가동 범위 10–24° 18–46° 5–60°
양발 동시 접지 33% 15% 20–30% 근처

걸음 빈도가 정확히 1.25Hz — 우리가 준 위상 클록(0.8초 주기)에 딱 맞습니다. 정책이 박자를 듣고 걷는다는 뜻입니다. 발 들림만 목표(8cm)에 조금 못 미치는데, 벌점(−20)과 다른 항목들 사이의 절충으로 4cm쯤에서 타협한 것으로 보입니다(사람도 발을 들어 옮기는 구간의 실제 들림이 수 cm 수준이라 실용적으론 충분합니다).

조종 능력도 유지됐습니다: 전진→좌회전→전진→우회전 시나리오 16초 완주, 회전각 오차 3~5°(08번과 동급). 걸음 형태를 고치면서 기능은 잃지 않았습니다.

성큼 내딛는 순간

제대로 걷는 G1

전체 영상:


회고 — 막혔던 점 & 배운 것

  • “되었다”와 “맞다”는 다르다. 06~09번은 전부 ‘되었’습니다 — 속도 추종도, 조종도, 외란 회복도 수치로 확인했으니까요. 그런데 그 모든 기능이 올라탄 걸음 자체가 가짜였습니다. 원격으로 결과만 보고받을 때 가장 놓치기 쉬운 부분이, 바로 이 “그럴듯한 영상 뒤의 실체”입니다.
  • 검증 도구부터 만들었어야 했다. 걸음 지표 측정(g1_gait_metrics.py)은 30분짜리 작업이었는데, 이게 있었다면 06번 시점에 셔플을 바로 잡아냈을 겁니다. “눈으로 본 영상”은 검증이 아니고, 수치 잣대가 검증입니다.
  • 보상 14개가 겁먹을 일이 아니었다. 채점 항목을 8개에서 14개로 늘리면 학습이 불안해질까 걱정했는데, 오히려 300 iteration 만에 박자를 잡았습니다. 항목 하나하나가 ‘탐색 공간의 이상한 구석’을 막아 줘서, 학습이 사람이 의도한 좁은 길로 빠르게 수렴한 것으로 보입니다.
  • 뻣뻣한 PD가 셔플의 공범이었다. kp 350은 03번(자세 버티기)에선 필수였지만, 보행에선 8Hz 잔발을 가능하게 한 물리적 토대였습니다. 공식 값(100/150/40)으로 낮추니 잔발 자체가 물리적으로 어려워졌습니다. 단계마다 맞는 하이퍼파라미터가 다릅니다.
  • 위상 클록은 ‘지식의 주입’이다. 보상만으로 박자를 유도할 수도 있지만, 관측에 sin/cos 박자를 넣어 주니 정책이 그걸 메트로놈처럼 씁니다. “걸음이란 주기 운동이다”라는 인간의 지식을 구조로 넣어 준 것 — 보상 설계와 관측 설계는 한 몸입니다.
  • 이 단계의 이론적 배경(왜 RL은 빈틈을 반드시 찾아내는가, 다른 분야의 사례들)은 연구 노트 R01에 따로 정리했습니다.

다음 단계

같은 눈으로 이전 결과물들도 하나씩 재검증할 차례입니다. 첫 번째 재검증에서 바로 반전이 나옵니다 → 11. 걸음과 강건함

이 시리즈의 전체 글 목록 → 시리즈 목차