G1 v391 학습에서는 TensorBoard의 숫자만 보면 정책이 빠르게 좋아지는 것처럼 보였습니다. Train/mean_reward는 11.704에서 414.803으로 올랐고, Train/mean_episode_length도 13.29에서 194.22로 늘었습니다. adaptive PPO가 기록한 learning rate는 1.0e-5에서 1.139e-4까지 올라갔습니다.
그런데 첫 번째로 저장한 model_6490부터 보행은 무너져 있었습니다. 기준 trace의 route-valid row는 1,334개였는데 98개로 줄었고, heading failure는 56개에서 1,540개, bad foot separation은 0개에서 508개, crossing은 0개에서 151개가 됐습니다. slow와 nominal 평가의 낙상률도 28.125%와 25%였습니다. reward와 episode length는 올라갔지만 제가 보존하려던 정책은 사라졌습니다.
처음에는 TensorBoard를 한 개의 건강검진표처럼 읽었습니다. reward가 오르고 loss가 finite라면 학습은 정상이며, 남은 문제는 조금 더 돌리면 된다고 생각하기 쉽습니다. 실제 RSL-RL 로그에는 서로 다른 모집단과 시간축이 한 화면에 섞여 있습니다. 완료된 episode의 통계, PPO mini-batch의 optimizer 통계, 별도 evaluator의 행동 판정은 같은 질문에 답하지 않습니다. 이 글은 PPO의 rollout·GAE·clipping 원리를 다시 강의하는 대신, 현재 G1 구현에서 각 scalar가 어디서 만들어지고 어떤 결론까지 허용하는지 정리합니다.

TensorBoard 한 화면에는 세 개의 시간이 섞여 있습니다
현재 G1 기본 설정은 4,096개 환경을 24 step씩 진행해 PPO iteration 하나에 98,304개의 transition을 모읍니다. rollout이 끝나면 return과 advantage를 계산하고, 저장한 batch를 4개 mini-batch로 나누어 5 epoch 학습합니다. 기본 설정이라면 iteration마다 20번의 optimizer step이 일어납니다. physics step·policy step·rollout의 시간축에 optimizer의 시간축이 하나 더 붙는 셈입니다.
RSL-RL의 logger 구현은 completed episode의 reward와 length를 최대 100개짜리 deque에 넣습니다. 반면 Loss/value, Loss/surrogate, Loss/entropy는 해당 iteration의 optimizer step에서 계산한 평균입니다. Perf/total_fps는 transition 처리 속도입니다. 세 값은 x축을 iteration으로 공유하지만 분모가 다릅니다.
| TensorBoard 계열 | 표본 | 주요 질문 | 단독으로 말할 수 없는 것 |
|---|---|---|---|
Train/* |
최근 완료된 episode 최대 100개 | 어떤 return과 episode 길이가 끝났는가 | 현재 살아 있는 episode와 held-out 행동 품질 |
Loss/* |
한 iteration의 mini-batch update 평균 | optimizer가 어떤 학습 신호를 받았는가 | 새 checkpoint가 더 잘 걷는가 |
Policy/* |
현재 action distribution parameter | 탐색 분포가 얼마나 넓은가 | 그 탐색이 유용한 관절 조합인가 |
Perf/* |
수집·학습 wall time | 파이프라인이 얼마나 빨리 도는가 | sample efficiency와 정책 품질 |
별도 eval |
고정 checkpoint·seed·scenario | 사전 지정한 행동 gate를 통과하는가 | 평가하지 않은 외란과 실물 안전 |
따라서 저는 곡선을 보기 전에 scalar의 생성 위치부터 확인합니다. 같은 iteration에 찍혔다는 이유로 reward 상승과 surrogate loss 하락을 인과관계로 묶지 않습니다. 먼저 rollout 표본이 무엇인지, 다음으로 optimizer가 무엇을 바꿨는지, 마지막으로 고정 평가에서 행동이 어떻게 달라졌는지를 순서대로 연결합니다.
mean reward는 최근 완료된 episode만 보고 있습니다
Train/mean_reward는 이번 rollout의 모든 transition reward 평균이 아닙니다. logger는 환경별 reward를 episode가 끝날 때까지 누적한 뒤 done이 나온 환경만 최근 100개 buffer에 넣습니다. 아직 오래 살아 있는 episode는 그 순간의 평균에 포함되지 않습니다. 반대로 초기에 빠르게 넘어지는 episode가 많으면 짧은 실패가 buffer를 빠르게 채웁니다.
이 때문에 mean_reward는 reward scale, episode 길이, 종료 분포가 함께 바뀌는 지표입니다. 살아 있는 시간마다 양의 reward를 받는 환경에서는 episode가 길어지는 것만으로 return이 커질 수 있습니다. reward profile을 바꾸면 서로 다른 iteration의 절대값도 바로 비교할 수 없습니다. reward shaping과 우회 행동에서 외부 평가를 분리한 이유가 여기에도 적용됩니다.
Train/mean_episode_length의 단위도 조심해야 합니다. logger는 매 environment step마다 cur_episode_length += 1을 수행하며 초를 곱하지 않습니다. raw scalar의 단위는 환경 step입니다. G1의 policy step이 0.02초라면 194.22 step은 약 3.88초입니다. 과거 보고서에서 이 값을 바로 초로 표기한 부분이 있었지만, 이 글에서는 raw step과 환산 시간을 분리합니다.
termination·timeout·reset 계약을 같이 보지 않으면 episode length의 의미도 흐려집니다. 낙상 threshold가 바뀌어 episode가 늦게 잘린 것인지, 정책이 실제로 회복한 것인지, timeout episode가 늘어난 것인지가 서로 다르기 때문입니다. 평균 두 개만으로는 분모를 복원할 수 없으므로 completed episode count, termination reason과 timeout count를 함께 남겨야 합니다.
surrogate loss의 부호는 보행 점수가 아닙니다
RSL-RL PPO 구현은 old policy와 current policy의 action log probability 차이로 ratio를 만들고, advantage를 곱한 unclipped·clipped 항 중 더 보수적인 값을 평균합니다. 이 구현은 gradient descent를 사용하므로 surrogate 항 앞에 음수가 붙습니다. TensorBoard의 Loss/surrogate는 그 mini-batch objective의 평균입니다.
ratio = exp(log_prob_new - log_prob_old)
surrogate = -advantage * ratio
surrogate_clipped = -advantage * clip(ratio, 1-epsilon, 1+epsilon)
surrogate_loss = mean(max(surrogate, surrogate_clipped))
advantage는 rollout 단위 또는 mini-batch 단위로 정규화될 수 있고, 같은 batch를 여러 epoch 다시 사용하면서 current policy도 계속 바뀝니다. 그래서 surrogate loss가 양수인지 음수인지, 0에 가까운지만 보고 정책이 좋아졌다고 판단할 기준은 없습니다. v488에서는 surrogate가 -0.00718로 작았지만 morphology와 raw-action gate가 실패했습니다. v485에서는 0.2549였고 slow·nominal 모두 100% 낙상했습니다. 두 값은 서로 다른 rollout과 objective에서 나온 optimizer 진단값이지 보행 점수가 아닙니다.
clipping도 여기서 오해하기 쉽습니다. PPO 원 논문은 sampled data를 여러 epoch mini-batch로 최적화할 수 있도록 clipped surrogate objective를 제안합니다. 그러나 clip은 모든 sample의 실제 policy ratio를 hard bound 안에 가두지 않습니다. 현재 workspace는 clip_param=0.2지만 clip된 sample의 비율을 기본 TensorBoard에 기록하지 않습니다. surrogate loss 하나만으로 업데이트가 얼마나 clip boundary에 몰렸는지 알 수 없습니다.
value loss는 critic의 오차이며 reward scale에 묶여 있습니다
Loss/value는 critic이 예측한 value와 rollout에서 만든 return target의 제곱 오차입니다. clipped value loss를 켜면 old value에서 너무 멀리 움직인 예측도 함께 비교해 더 큰 오차를 사용합니다. 이 값이 NaN이나 폭발적인 크기로 변하면 critic·return·reward scale을 의심할 이유가 생깁니다. 그렇다고 작은 value loss가 좋은 정책을 뜻하지는 않습니다.
첫째, reward의 단위와 분산이 바뀌면 return target의 scale도 바뀝니다. reward profile이 다른 run의 value loss 260과 380을 그대로 순위로 놓을 수 없습니다. 둘째, critic이 잘못 정의된 reward의 return을 정확히 예측해도 행동 목표는 틀릴 수 있습니다. 셋째, privileged critic은 actor보다 많은 정보를 볼 수 있어 value loss를 줄이면서도 actor가 필요한 행동을 구분하지 못할 수 있습니다. 이 경계는 asymmetric actor–critic 글에서 별도로 다뤘습니다.
v488의 value loss는 260.3270으로 finite였고 v485는 379.5155였습니다. 두 run 모두 external behavior gate는 실패했습니다. 여기서 확인한 것은 학습 코드가 finite return과 value prediction을 사용해 update를 수행했다는 사실까지입니다. critic의 RMSE를 행동 승인 지표로 승격시키지는 않았습니다.
Loss/entropy는 손실값이 아니라 분포의 엔트로피입니다
RSL-RL logger는 algorithm이 돌려준 모든 값을 Loss/ 아래에 넣습니다. 그래서 Loss/entropy라는 이름이 붙지만 실제로 저장되는 값은 양의 action-distribution entropy입니다. optimizer의 total loss에는 - entropy_coef × entropy로 들어갑니다. 현재 G1 기본 설정의 entropy_coef는 0.002입니다.
연속 action의 Gaussian policy에서 entropy와 standard deviation은 연결돼 있습니다. Policy/mean_std가 줄면 sampling 분포가 좁아지고, 커지면 같은 mean action 주위의 탐색이 넓어집니다. 하지만 29개 관절 std의 평균은 어느 관절에서 탐색이 사라졌는지 숨깁니다. 상체 std가 커지고 다리 std가 줄어도 mean은 비슷할 수 있습니다.
v485의 entropy 11.5041이나 v488의 mean std 0.38796은 분포가 finite하고 완전히 수축하지 않았다는 정도만 말합니다. 넓은 탐색이 좋은 보행 방향을 포함하는지, raw action saturation을 키우는지, reference tracking을 흐리는지는 trace와 관절별 std가 필요합니다. entropy 감소를 무조건 수렴으로, 증가를 무조건 탐색 성공으로 부르면 안 됩니다.
KL이 learning rate를 바꾸지만 현재 KL 곡선은 남지 않습니다
현재 G1 기본 설정은 adaptive schedule과 desired_kl=0.01을 사용합니다. 각 mini-batch에서 old distribution과 current distribution의 KL을 계산하고, 평균 KL이 목표의 2배보다 크면 learning rate를 1.5로 나눕니다. KL이 목표의 절반보다 작고 0보다 크면 1.5를 곱합니다. learning rate는 1e-5와 1e-2 사이로 제한됩니다.
if kl_mean > 2 * desired_kl:
learning_rate = max(1e-5, learning_rate / 1.5)
elif 0 < kl_mean < desired_kl / 2:
learning_rate = min(1e-2, learning_rate * 1.5)
# 한 iteration에 5 epoch x 4 mini-batch라면 여러 번 바뀔 수 있다.
# TensorBoard Loss/learning_rate는 iteration 끝의 값만 저장한다.
문제는 KL이 이 제어문 안에서만 사용되고 loss_dict에 포함되지 않는다는 점입니다. 공식 RSL-RL 구현과 현재 G1 custom PPO 모두 기본 TensorBoard에 KL scalar를 남기지 않습니다. v392 audit도 v390·v391 event file에서 KL tag가 없음을 확인했습니다. 따라서 learning rate가 올라간 사실은 KL이 낮은 구간을 만났다는 간접 증거지만, 언제 몇 번 threshold를 넘었는지 재구성할 수 없습니다.
v488에서는 command에 learning_rate=1e-6, desired_kl=5e-4를 넣었지만 TensorBoard가 기록한 값은 1e-5였습니다. adaptive code의 floor가 요청값보다 컸기 때문입니다. 명령행에 적힌 의도보다 runtime scalar와 실제 optimizer state가 우선합니다. checkpoint·optimizer·resume 계약에서 learning rate를 checkpoint state로 본 이유도 같습니다.
custom loss가 찍혔다는 사실은 배선과 행동을 분리해 줍니다
v504는 time-windowed morphology objective를 추가한 one-iteration smoke였습니다. 실행과 checkpoint 저장은 끝났지만 TensorBoard에 g1_time_windowed_morphology* tag가 하나도 없었습니다. 원인은 해당 coefficient만 켰을 때 train.py가 custom G1PhaseReferencePPO를 선택하지 않는 배선이었습니다. finite한 기본 value·surrogate loss가 있어도 의도한 objective test로 인정할 수 없었습니다.
v505에서 trainer 선택 조건을 고친 뒤에는 custom loss 0.244, late active fraction 0.173, route gate active 0.294와 blocked 0.687이 기록됐습니다. 이제 "신호가 계산됐고 특정 row에서 활성화됐다"는 것은 증명됐습니다. 하지만 slow와 nominal 낙상률은 모두 100%였고 morphology·route gate도 실패했습니다. 배선 검증이 행동 검증으로 자동 승격되지 않았습니다.
반대 방향의 사례도 있습니다. v464는 g1_route_residual_authority loss 2.226, active fraction 0.304를 기록했습니다. 신호는 살아 있었습니다. 그러나 saved actor-head row의 L2 변화는 waist 0.000147, hip roll/yaw 0.000167, arms 0.000153에 그쳤고 눈에 보이는 morphology blocker도 남았습니다. loss가 0이 아닌가, gradient가 존재하는가, parameter가 움직였는가, 행동이 달라졌는가는 네 개의 다른 질문입니다.
| 증거 단계 | 필요한 로그·검사 | 통과가 증명하는 것 | 아직 증명하지 않는 것 |
|---|---|---|---|
| objective wiring | custom loss·active fraction | 의도한 code path와 조건이 실행됨 | gradient가 actor에 도달함 |
| update authority | gradient norm·parameter delta | 선택한 parameter가 실제 움직임 | 좋은 방향으로 움직임 |
| behavior response | paired trace·joint/action metric | 관측 가능한 행동이 달라짐 | 전체 scenario gate 통과 |
| promotion | held-out multi-seed evaluation | 선언한 범위의 acceptance 충족 | 실물 배포 안전 |
v391의 reward 상승은 첫 checkpoint 붕괴를 숨겼습니다
v391은 Stage-A route-retention reward profile을 기존 checkpoint에 이어 학습한 10-iteration probe였습니다. 학습 reward는 11.704에서 414.803, completed episode length는 13.29에서 194.22 step으로 늘었습니다. adaptive schedule 아래 learning rate도 1.0e-5 → 1.139e-4로 커졌습니다. 이 세 곡선만 보면 안정적으로 더 긴 episode를 학습하며 update를 확대하는 모습입니다.
하지만 v392에서 이미 저장된 intermediate checkpoint를 다시 평가하자 첫 model_6490부터 route·heading·foot separation·crossing이 동시에 무너져 있었습니다. 뒤의 model 6495와 6497에서 route row가 98→231→252로 조금 회복됐지만 baseline 1,334에는 훨씬 못 미쳤습니다. training scalar의 단조 상승과 행동 gate의 방향이 반대였습니다.
이 결과만으로 원인을 reward geometry 하나로 확정하지 않았습니다. model-only resume, normalizer, optimizer moment와 update budget도 경쟁 설명이었습니다. 다만 "더 돌리면 회복될 것"이라는 설명은 약해졌습니다. 첫 checkpoint부터 파괴적 drift가 있었고 10 iteration 동안 acceptance surface로 돌아오지 못했기 때문입니다. 다음 실험은 reward 항 추가가 아니라 update budget을 줄이는 대조가 됐습니다.
v393은 optimizer를 줄이면 경로가 보존되지만 정책 승인은 남는다고 보여 줬습니다
v393은 v391과 같은 reward profile과 base checkpoint를 사용하면서 PPO update control만 바꿨습니다. schedule을 fixed로 두고 learning rate를 1e-5로 고정했으며, 5 epoch×4 mini-batch를 1 epoch×4 mini-batch로 줄였습니다. 10-iteration adaptive probe 대신 2-iteration bounded probe를 실행했습니다.
결과는 baseline route row 1,334개 대비 1,371개, heading bad 56개 대비 45개, bad separation 4개, crossing 0개였습니다. slow·nominal 평가에서도 낙상은 0이었습니다. v391의 즉시 route collapse가 update budget에 민감하다는 해석이 강해졌습니다. reward profile만으로는 설명하기 어려운 차이였습니다.
그렇다고 v393을 좋은 보행으로 승인하지 않았습니다. slow displacement 0.718m와 nominal 6.054m는 gate에 못 미쳤고, waist overuse·hip roll/yaw twist·arm action도 남았습니다. update control은 기존 경로를 지키는 조건을 찾는 데 기여했지만 목표 morphology를 만들지는 않았습니다. 학습 안정성 개선과 행동 목표 달성을 분리한 판정입니다.
학습 중에는 곡선 하나가 아니라 실패 가설 순서로 읽습니다
현재 G1에서 TensorBoard를 볼 때는 먼저 실행 무결성을 봅니다. NaN/Inf, FPS 급락, custom tag 부재, unexpected learning rate처럼 코드 경로 자체를 의심할 신호가 있는지 확인합니다. 그다음 completed episode의 수와 termination reason을 보고 reward·length의 표본을 해석합니다. 이후 optimizer 지표와 parameter movement를 확인하고, 마지막으로 고정 checkpoint 평가를 연결합니다.
| 관찰 조합 | 먼저 의심할 것 | 다음 검사 | 바로 내리면 안 되는 결론 |
|---|---|---|---|
| reward↑, episode length↑, eval↓ | reward shortcut·update drift·종료 분포 변화 | early checkpoint paired eval, termination histogram | 학습이 거의 끝남 |
| custom loss 없음 | trainer 선택·조건문·tag wiring | one-iteration canary와 expected tag assertion | objective가 약함 |
| custom loss 있음, parameter delta≈0 | gradient ownership·scale·clip·optimizer group | pre/post grad norm과 selected-row delta | 더 오래 돌리면 행동이 바뀜 |
| LR 급변, KL tag 없음 | adaptive schedule의 숨은 mini-batch KL | per-mini-batch KL·LR trajectory 기록 | 최종 LR만으로 안정성 설명 |
| entropy·std 감소 | 관절별 탐색 수축·saturation 회피 | per-joint std와 deterministic eval | 정책 수렴 |
| value loss 감소, eval 불변 | critic만 return을 더 잘 맞춤 | return scale·explained variance·actor delta | actor 개선 |
이 순서는 원인을 확정하는 공식이 아닙니다. 같은 로그 패턴에 reward 문제, observation drift, resume state 불일치, action saturation이 모두 들어갈 수 있습니다. 목적은 한 곡선에 이름을 붙이는 것이 아니라 다음 실험에서 어느 경쟁 설명을 고정하고 어느 변수 하나를 바꿀지 정하는 데 있습니다.
현재 로그에 추가해야 할 것은 더 많은 숫자가 아니라 빠진 연결입니다
현재 RSL-RL 기본 로그와 G1 custom scalar는 reward, value, surrogate, entropy, learning rate, mean std와 여러 objective-specific loss를 저장합니다. 반면 adaptive LR을 실제로 움직인 KL, clip boundary에 걸린 sample 비율, advantage·return 분포, completed episode 수, gradient norm과 checkpoint parameter delta는 일관된 공통 schema로 묶여 있지 않습니다.
required_per_iteration = {
"rollout": [
"completed_episode_count", "termination_reason_count",
"reward_per_step", "return_mean_std", "advantage_mean_std"
],
"update": [
"kl_mean_min_max", "clip_fraction", "lr_min_max_final",
"grad_norm_pre_clip", "grad_norm_post_clip", "parameter_delta_l2"
],
"artifact": [
"checkpoint_sha", "optimizer_sha", "normalizer_sha", "config_sha"
],
"evaluation": [
"paired_baseline_id", "scenario_gate_report"
]
}
특히 KL과 LR은 iteration 최종값만 남기지 않고 mini-batch 내부의 min·mean·max와 조정 횟수를 함께 기록해야 합니다. clip fraction은 전체와 관절 group을 나눌 수 없지만, 적어도 policy ratio가 clip 경계에 얼마나 몰렸는지 알려 줍니다. completed episode count와 termination histogram을 붙이면 reward 평균의 표본이 언제 바뀌었는지도 추적할 수 있습니다.
여기까지 구현되더라도 TensorBoard가 정책을 승인하지는 않습니다. seed·scenario·metric·acceptance gate 글에서 정리한 held-out evaluation이 마지막 판정을 맡습니다. 학습 로그의 역할은 "왜 변했을 가능성이 있는가"를 좁히는 것이고, evaluator의 역할은 "그 변화를 받아들일 수 있는가"를 판단하는 것입니다.
v391에서 제가 놓친 것은 reward 곡선이 거짓말을 했다는 사실이 아니었습니다. 그 곡선에 답할 수 없는 질문을 물었습니다. reward 상승은 최근 완료된 episode의 return이 커졌다는 뜻이었고, learning rate 상승은 adaptive schedule이 update를 넓혔다는 뜻이었습니다. 둘 다 첫 checkpoint가 기준 보행을 보존했다는 증거는 아니었습니다. 다음 로그 개선의 목표는 멋진 dashboard가 아니라, rollout의 표본·optimizer의 이동·checkpoint의 행동을 한 iteration ID로 다시 연결하는 것입니다.
기술 기준 시점: 2026-07-22. 구현 설명은 origin/main@ee4d96e의 G1 PPO 설정, training/rl/g1_phase_reference_ppo.py, training/rl/train.py와 RSL-RL logger/PPO source를 기준으로 했습니다. 실험 수치는 v391–v393, v464, v485, v488, v504–v505 기록에서 가져왔습니다. 모든 checkpoint는 프로젝트 내부 비교 대상이며 승인된 실물 보행 정책을 뜻하지 않습니다.