실험 설계와 결과 해석 — 잠재가 죽었는지 어떻게 아는가

보지 않고 걷기 — CENet + 비대칭 PPO · 5차시

Author

JungYeon Lee

Published

August 31, 2026

강의 로드맵

회차 주제
0차시 Isaac Sim & Isaac Lab 시작하기 — 시뮬레이터를 손으로 만져보기
1차시 4족 보행 로봇 & Isaac Lab — 관측을 무엇으로 채울 것인가
2차시 PPO 설계 — 비대칭 actor-critic과 탐험의 평형점
3차시 왜 VAE인가 — 히스토리에서 속도와 지형을 뽑아내기
4차시 학습 루프 통합 — CENet과 PPO를 한 반복에 묶기
5차시 (오늘) 실험 설계와 결과 해석 — 잠재가 죽었는지 어떻게 아는가

학습은 돌았습니다. 로그도 쌓였고 곡선도 예쁘게 올라갔습니다. 그런데 이게 정말 CENet 덕분인가요?

이 회차는 숫자를 자랑하는 자리가 아니라 숫자를 의심하는 법을 다룹니다. 이 저장소는 같은 질문에 대해 세 번 다른 답을 냈고, 그중 두 번은 틀렸습니다. 어떻게 틀렸고 어떻게 알아챘는지가 오늘의 내용입니다.

Tip오늘의 목표
  1. 세 팔의 차이를 어떻게 재야 신뢰할 수 있는지 안다 — 무엇을 재고, 어떻게 평균 내고, 오차를 어떻게 붙이는지.
  2. 잠재가 죽었는지 가중치에서 판정할 수 있다. 보상 곡선으로는 안 됩니다.
  3. 비교하면 안 되는 실험을 구별하고, 내 실험의 출처를 기록하는 습관을 갖는다.

난이도 노트 — 이 회차는 앞의 넷과 성격이 다릅니다. 새 알고리즘이 나오지 않고, 대신 “내가 방금 만든 숫자를 얼마나 믿어야 하나” 를 다룹니다. 결과를 직접 만들어 볼 사람에게 가장 쓸모 있고, 아니라면 §3과 §10만 읽어도 됩니다.


1. 무엇을 재는가

먼저 지표를 정합니다. “보상이 높다” 는 비교 기준이 될 수 없습니다 — 레시피가 다르면 보상의 단위 자체가 다르니까요.

1.1 underlying 추종 — 가중치를 되나눈 값

1차시 §5.3에서 공식 레시피가 선속도 추종 가중치를 논문의 1.5배로 쓴다고 했습니다. 그러면 로그에 찍히는 track_lin_vel_xy_exp 는 1.5가 곱해진 값입니다.

\texttt{underlying} = \frac{\texttt{track\_lin\_vel\_xy\_exp}}{1.5}

\exp(-e^2/\sigma) 형태라 0에서 1 사이이고, 1이면 명령 속도를 완벽히 따라간 것입니다. 이렇게 되나눠야 레시피가 다른 실험끼리도 같은 자로 잴 수 있습니다.

Important

함정1.5 는 공식 레시피의 값입니다. 폐기된 논문 레시피는 1.0이라 나누면 안 됩니다. 비교 스크립트에 상수로 박아 두면 어느 날 조용히 틀린 값을 뱉습니다. 실험 로그에 어느 레시피인지 를 같이 남기세요.

1.2 함께 봐야 하는 것들

추종 성능 하나만 보면 속습니다. 로봇이 넘어지지 않으려고 천천히 걷는 것잘 걷는 것을 구별하지 못하거든요. 그래서 최소 이 정도는 같이 봅니다.

지표 왜 필요한가
underlying 추종 명령 속도를 따라가나 주 지표
Curriculum/terrain_levels 지형 난이도가 얼마나 올랐나 험지 성능의 진짜 척도. 쉬운 지형에서만 잘하면 이게 안 오른다
Episode_Termination/base_contact 몸통이 땅에 닿아 끝난 비율 넘어지는가
mean_episode_length 에피소드가 얼마나 오래 갔나 위와 짝
Metrics/success_rate 명령 추종 성공 비율 추종의 이산 버전
error_vel_xy 속도 오차 (m/s) 해석하기 쉬운 물리 단위

1.3 마지막 몇 점으로 “최종값”을 읽지 마세요

가장 흔한 실수입니다. 학습 곡선의 끝부분을 보고 “최종 0.71” 이라고 적는 것.

추종 지표는 iteration 사이에 표준편차 0.03 정도로 출렁입니다. 그런데 우리가 재려는 효과 (CENet 의 기여)는 0.015 수준입니다. 노이즈가 신호의 두 배입니다.

\text{3점 평균의 표준오차} = \frac{0.03}{\sqrt{3}} \approx 0.017 \;>\; \text{재려는 효과 } 0.015

실제로 이것 때문에 결론의 부호가 뒤집힌 적이 있습니다(§5). 그래서 이 저장소의 비교 스크립트는 마지막 10%(400 iteration)를 평균 내고 평균의 표준오차를 함께 찍습니다.

\text{SE} = \frac{s}{\sqrt{n}}, \qquad n = 400

400점을 쓰면 표준오차가 0.0015 수준으로 내려가 0.015짜리 효과를 분간할 수 있습니다.


2. 비교축 설계 — 무엇에서 무엇을 빼는가

1차시 §3.4의 세 팔이 여기서 쓰입니다. 같은 환경, 같은 PPO, 관측만 다릅니다.

flowchart LR
  B(["Base 45<br/>하한선"]) -->|"CENet 이 회수한 양"| W(["CENet 팔 64"])
  W -->|"남은 격차"| O(["Oracle 48<br/>상한선"])
  B -.->|"회수 가능한 총량"| O
  classDef low fill:#eceff1,stroke:#546e7a;
  classDef mid fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px;
  classDef up fill:#fce4ec,stroke:#c2185b;
  class B low; class W mid; class O up;
Figure 1: 세 팔의 뺄셈 — 각 차이가 무엇을 측정하는지

두 개의 뺄셈이 답입니다.

  • Oracle − Base = 선속도를 아는 것의 가치 = 회수 가능한 총량(headroom)
  • CENet 팔 − Base = CENet 이 실제로 회수한 양

그리고 비율 \dfrac{\text{Waq}-\text{Base}}{\text{Oracle}-\text{Base}} 이 “CENet 이 특권 정보의 몇 %를 대신했나” 입니다.

네 번째 셀이 있는 이유OracleDwq 는 “Oracle 관측 + CENet 팔의 PPO 하이퍼파라미터, CENet 은 없음” 입니다. 이게 없으면 (Waq − Oracle) 차이가 CENet 때문인지 신경망 설정 때문인지 구별이 안 됩니다. 교란 변수를 하나씩 분리하려고 셀을 늘린 것입니다.


3. 출처를 먼저 적는다

여기가 이 회차에서 가장 실용적인 절입니다.

이 저장소에는 6런짜리 sweep 이 네 번 돌았고, 서로 비교할 수 없습니다. 매번 설정이 달랐기 때문입니다. 그런데 로그 디렉터리 이름만으로는 그게 안 보입니다.

flowchart TD
  S1["① 3000it · stairheavy<br/>KL 합 + β→4 annealing"] --> R1(["Waq 붕괴<br/>폐기"])
  S2["② 4000it · clip 1.0<br/>보상 4항·랜덤화 이전"] --> R2(["σ 천장 조건<br/>과거 헤드라인"])
  S3["③ 4000it · β=1.0 + 랜덤화<br/>clip 4.0"] --> R3(["Waq 잠재 사망<br/>cenet_kl = 0.0000"])
  S4["④ 4000it · β=0.35<br/>clip 4.0 + 보상 4항 + 랜덤화"] --> R4(["잠재 생존<br/>현재 헤드라인"])
  classDef bad fill:#ffebee,stroke:#c62828;
  classDef old fill:#eceff1,stroke:#546e7a;
  classDef good fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px;
  class S1,R1,S3,R3 bad; class S2,R2 old; class S4,R4 good;
Figure 2: 네 번의 sweep — 설정이 달라 서로 비교할 수 없다
Important실험 위생 규칙 셋
  1. 한 표에는 한 sweep 만. 서로 다른 sweep 의 숫자를 같은 표에 놓지 않습니다.
  2. 모든 결과 표에 출처 열을 답니다. 로그 디렉터리 이름과 날짜.
  3. clip_actions 가 다르면 비교 금지. 2차시 §5에서 봤듯 이 값 하나가 추종 성능을 0.55에서 0.79로 바꿉니다. 다른 어떤 효과보다 큽니다.

특히 sweep ③ 이 교훈적입니다. 이 sweep 의 결과 그림이 한동안 저장소의 대표 그림이었는데, Waq 팔의 CENet 이 죽어 있었습니다 — 로그의 cenet_kl 이 정확히 0.0000 이었습니다. 즉 그 “CENet 팔” 은 실제로는 Base + 아무 의미 없는 19차원이었습니다.

그런데 그 표에서 Waq 는 평지에서 Base 를 +0.0207 로 “이겼습니다”. 죽은 잠재가 이길 리 없으니, 그 +0.0207 은 효과가 아니라 노이즈이거나 다른 교란이었다는 뜻입니다. 실제로 같은 표에서 Oracle − Waq 가 −0.0205 였습니다 — 상한선이 측정 대상보다 낮게 나온 것이고, 이건 표가 이상하다는 신호입니다.

일반 규칙 — 내 방법이 상한선을 이기면, 기뻐하기 전에 상한선을 의심하세요. 상한선은 정의상 못 이깁니다. 이겼다면 상한선이 상한선이 아닌 것입니다. (§4에서 이 규칙을 한 번 더 씁니다.)


4. 헤드라인 — 현재 설정의 6런

출처: logs/rsl_rl/, 2026-08-30 ~ 08-31 실행. seed 42 · 4096 env · 4000 iteration · clip_actions=4.0 · 보상 4항 복원 · 도메인 randomization ON · \beta=0.35. 수렴 구간 = 마지막 400 iteration 평균 ± 평균의 표준오차.

4.1 험지 (rough) — 유효한 비교

underlying 추종
Base (45) 0.6870 ± 0.0014
CENet 팔 (64) 0.7017 ± 0.0014
Oracle (48) 0.7407 ± 0.0014
차이 판정
CENet 팔 − Base +0.0147 ± 0.0020 유의
Oracle − CENet 팔 +0.0390 ± 0.0020 유의
Oracle − Base (총 여유폭) +0.0537

\text{회수율} = \frac{0.0147}{0.0537} \approx 27\%

Base < CENet 팔 < Oracle 순서가 나왔습니다. 이게 원래 기대했던 순서입니다 — 외부 센서 없이 히스토리만으로, 특권 정보가 주는 이득의 약 4분의 1을 회수했습니다.

보조 지표도 같은 방향입니다.

지표 Base CENet 팔 Oracle
평균 보상 22.71 22.94 24.10
지형 레벨 5.41 5.39 5.53
error_vel_xy 0.3117 0.2986 0.2798
base_contact 종료 0.0907 0.0968 0.0668
에피소드 길이 945.7 929.2 984.6

속도 오차는 개선됐는데(error_vel_xy 0.312 → 0.299) 넘어지는 비율은 오히려 조금 늘었습니다 (0.0907 → 0.0968). CENet 이 준 것은 “속도를 더 잘 아는 것” 이지 “더 안 넘어지는 것” 이 아니라는 해석과 맞습니다. 차이가 표준오차 근처라 강하게 주장할 것은 아닙니다.

4.2 평지 (flat) — 이 열은 읽으면 안 됩니다

underlying 추종
Base (45) 0.8917 ± 0.0004
CENet 팔 (64) 0.9125 ± 0.0004
Oracle (48) 0.8920 ± 0.0006

Oracle 이 Base 와 똑같습니다 (+0.0003). 그리고 CENet 팔이 Oracle 을 이겼습니다(+0.0205). §3의 규칙대로, 기뻐하지 말고 상한선을 의심할 차례입니다.

원인을 찾았습니다. 평지 Oracle 팔만 critic 이 노이즈 낀 관측을 봅니다.

# logs/rsl_rl/OracleDwq-Official-Flat-PPO-v0/.../params/agent.yaml
obs_groups:
  actor:  [policy]
  critic: [policy]     # ← Base·CENet 팔은 [critic]

환경 설정 쪽에서는 이걸 고쳐 뒀습니다 — 노이즈 없는 critic 그룹을 따로 만들어 둡니다. 그런데 러너 설정이 critic 을 다시 policy 로 되돌려 놓습니다. 환경의 수정이 무효화된 것입니다. 그 결과 여섯 팔 중 평지 Oracle 하나만 가치 함수가 관측 노이즈를 보며 학습합니다.

Important

상한선이 되어야 할 팔에 핸디캡이 걸려 있습니다. 그러니 이 sweep 의 평지 열에서는 “회수 가능한 총량” 을 잴 수 없고, CENet 팔 − Base = +0.0208 이 무엇의 효과인지도 말할 수 없습니다. 험지 열만 해석 가능합니다.

이건 과거 sweep 의 이야기가 아니라 지금 설정에 남아 있는 문제입니다. 저장소 문서가 이미 증상(평지 Oracle 0.8920 이 Base 0.8917 과 동률)을 기록해 두었는데, 고친 곳과 되돌리는 곳이 달라서 계속 재현되고 있었습니다.

배울 것 — 설정이 여러 층으로 겹치면(환경 cfg → 러너 cfg → CLI) 나중 층이 앞 층의 수정을 조용히 덮습니다. 그래서 결과를 믿기 전에 실제로 저장된 설정 파일(params/agent.yaml)을 읽어야 합니다. 코드에 그렇게 쓰여 있다는 것과 그 런이 그렇게 돌았다는 것은 다릅니다.

4.3 이 표에 붙는 단서

Important
  1. 단일 seed(42). ± 는 한 런 안에서의 출렁임이지 seed 간 변동이 아닙니다. seed 를 바꾸면 이 정도 차이는 흔들릴 수 있습니다.
  2. 평지 열은 교란되어 있습니다(§4.2).
  3. AdaBoot 은 여전히 시간 기반입니다(4차시 §7.2) — 논문의 되먹임이 없습니다.
  4. 회수율 27%는 험지, 이 지형 조합, 이 randomization 설정에서의 값입니다.

5. 부호가 뒤집힌 적이 있다

이 표가 왜 중요한지 보여 주는 사건입니다.

CENet 손실의 감축 축을 고치기 전(3차시 §5), 같은 비교를 하면 험지에서

\text{Waq} - \text{Base} = \mathbf{-0.0166}

였습니다. CENet 을 붙이면 오히려 나빠진다는 결과입니다. 고친 뒤에는 +0.0073, 그리고 지금 설정에서는 +0.0147 입니다. 부호가 뒤집혔습니다.

Important그런데 두 런의 보상 곡선은 똑같이 생겼었습니다

이게 핵심입니다. 잠재가 죽은 런과 살아 있는 런의 학습 곡선을 나란히 놓으면 구별할 수 없습니다. 둘 다 잘 올라가고, 둘 다 수렴하고, 둘 다 로봇이 걷습니다.

당연합니다 — 잠재가 죽어도 actor 는 여전히 45차원 고유수용성 관측을 온전히 받고 있으니 Base 만큼은 걷습니다. 죽은 19차원은 그냥 상수일 뿐입니다.

그래서 진단은 곡선이 아니라 가중치에서 나와야 합니다.


6. 붕괴 포렌식 — 가중치를 심문하는 법

6.1 프로브 정의

체크포인트만 있으면 시뮬레이터 없이 판정할 수 있습니다.

# 개념 — stage5/task04_diagnose
def probe_encoder(cenet_state_dict, num_samples=2048, seed=0):
    x = torch.randn(num_samples, 225, generator=g)
    h = encoder(x)
    est_vel, mu, logvar = h.split([3, 16, 16], dim=-1)
    return {
        "mu_abs":      mu.abs().mean(),
        "est_vel_std": est_vel.std(dim=0).mean(),
        "kl":          kl_per_dim(mu, logvar).sum(),
    }
1
진짜 관측이 아니라 \mathcal{N}(0,1) 를 넣습니다.
2
인코더만 돌립니다. 디코더도 환경도 필요 없습니다.
3
출력이 0 근처에 뭉쳐 있으면 인코더가 아무 반응도 안 하는 것.
4
입력이 바뀔 때 출력이 얼마나 움직이나 — 민감도.

왜 진짜 관측이 아니라 난수를 넣나 — 재려는 것이 정확도가 아니라 입력 민감도이기 때문입니다. 죽은 인코더는 무엇을 넣어도 같은 값을 뱉습니다. 그리고 난수를 쓰면 시뮬레이터 없이, 어떤 체크포인트에도 똑같은 잣대를 댈 수 있습니다.

6.2 네 개의 체크포인트

3차시 §6에서 잠재가 두 가지 방식으로 죽는다고 했습니다. 실측값으로 확인합니다.

체크포인트 \lVert\mu\rVert \lVert W_z\rVert 판정
sweep ① stairheavy 4.17e−3 1.91 ⓐ KL 과압
sweep ② clip1 2.87e−2 0.259 생존 (약함)
sweep ③ collapsedwaq 6.28e−4 0.0044 ⓑ 디코더가 버림
sweep ④ 현재 0.175 0.928 건강
Important

\lVert\mu\rVert 만 보면 1행과 3행이 같은 무리입니다 — 둘 다 1e−3 대이고 둘 다 “죽었다” 입니다. 그런데 처방이 정반대입니다.

가르는 것은 \lVert W_z\rVert 하나입니다 — 1.91 대 0.0044, 430배 차이.

  • 1행: 디코더는 z읽고 싶어합니다(가중치가 큼). 인코더가 KL 에 눌려 못 보내는 것. → \beta 를 내리면 살아납니다.
  • 3행: 디코더가 z아예 안 봅니다(가중치가 0). 요금을 깎아 줘도 안 삽니다. → z 에게 할 일을 줘야 합니다.

현재 sweep 은 \lVert\mu\rVert 0.175, \lVert W_z\rVert 0.928 로 양쪽 다 건강한 영역에 있습니다. 학습 로그의 다른 지표도 일관됩니다 — KL 4.17 nats, \sigma 0.843(1.0에서 충분히 떨어짐), 활성 차원 16개 중 4개.

“16개 중 4개만 쓴다” 는 나쁜 신호가 아닙니다. 3차시 §2의 ②에서 봤듯 KL 은 정보 요금제이고, 신경망이 필요 없는 차원을 스스로 끄는 것은 설계대로 작동하는 것입니다. 0개면 붕괴, 16개 전부면 요금이 너무 싸서 노이즈까지 싣고 있다는 뜻일 수 있습니다.

가로축 \lVert\mu\rVert, 세로축 \lVert W_z\rVert (둘 다 로그 스케일). 실측 4점이 찍혀 있습니다. 보라색 점을 끌어서 내 체크포인트 값을 넣어 보세요.

가로축만 봐서는 ⓐ와 ⓑ가 구별되지 않습니다 — 세로축이 처방을 가릅니다

이렇게 해보세요 — 점을 가로로만 왼쪽 끝까지 끌어 보세요. 판정이 안 바뀝니다. 이제 세로로 끌어 보세요. 같은 \lVert\mu\rVert 인데 처방이 뒤집힙니다. 이게 2차원으로 봐야 하는 이유입니다.

6.3 프로브 값은 흔들립니다

한 가지 주의. 붕괴한 인코더는 대부분의 입력에 상수를 뱉다가 드문 입력에만 크게 반응합니다. 그래서 꼬리가 두꺼워지고, KL 이나 est_vel_std 같은 값이 표본 수와 seed 에 따라 출렁입니다. 같은 체크포인트가 2048/seed 2 에서 7.05e−2, 8192/seed 0 에서 4.07e−2 로 나온 적이 있습니다.

판정은 흔들리지 않는 \lVert\mu\rVert\sigma, 그리고 가중치 노름으로 하세요.


7. 절제 실험 읽는 법

\beta 를 0.35로 정하기까지의 과정입니다. 잘 설계된 절제 실험이 어떻게 생겼는지 보는 예시로 읽으세요.

7.1 용의자 좁히기

sweep ③ 에서 잠재가 죽었을 때, 그 sweep 은 이전 대비 세 가지가 바뀌어 있었습니다 — clip_actions 1.0→4.0, 보상 4항 복원, 도메인 randomization 추가. 셋 중 무엇이 범인일까요?

하나씩 되돌립니다. (험지 CENet 팔, 600 iteration)

되돌린 것 \lVert\mu\rVert KL (nats) 판정
이번 sweep 6.28e−4 5.7e−6 붕괴
clip 만 1.0로 clip 1.02e−3 6.3e−5 붕괴
보상만 제거 보상 4항 1.23e−3 1.9e−5 붕괴
randomization 만 제거 randomization 2.42e−2 0.406 생존

범인은 도메인 randomization 입니다. clip 과 보상은 무죄입니다.

왜 randomization 이 잠재를 죽이나 — 1차시 §6.3의 randomization 항목들은 전부 시작할 때 정해지는 환경 상수입니다(마찰, 하중, 질량중심, 게인, 모터 출력). 0.1초짜리 히스토리로 이걸 역추정하기는 매우 어렵습니다. 반면 그것들 때문에 o_{t+1}예측 불가능한 성분은 즉시 커집니다.

재구성 과제가 갑자기 훨씬 어려워졌는데 z 로는 풀 수 없는 종류의 어려움입니다. z 로 흐르는 재구성 그래디언트가 약해지고, 그러면 약한 KL 압력조차 못 이깁니다. → 3차시 §6의 ⓑ 경로입니다.

7.2 처방 고르기

범인을 알았으니 처방을 고릅니다. randomization 은 sim2real 때문에 뺄 수 없으므로, 요금(KL)을 깎는 쪽으로 갑니다.

\lVert\mu\rVert KL (nats) 활성 차원 판정
\beta = 0.35 0.215 4.18 5 / 16 최선
free bits 0.05 0.194 0.83 16 / 16 (바닥) 생존하나 얇음
IsaacGym 프리셋 0.008 0.0005 0 / 16 붕괴

free_bits 는 각 차원에 최소 정보량을 강제하는 방법인데, 16개 전부가 바닥값에 붙어 있습니다. “억지로 켜 놓았을 뿐 실제로 쓰지는 않는” 상태라 \beta 조정보다 못했습니다.

그리고 지평 검증을 했습니다. 600 iteration 짜리 결론이 4000에서도 유지되는지 — \lVert\mu\rVert 가 0.134 → 0.152 → 0.176 → 0.171 → 0.170 → 0.172 로 감쇠 없이 갔습니다. 그래서 채택했고, §4의 6런이 그 설정입니다.

7.3 철회된 결론

절제 실험 표에는 원래 한 줄이 더 있었습니다 — “밀치기(push)를 켜면 잠재가 산다”(\lVert\mu\rVert 3.87e−2). 600 iteration 에서는 분명히 그랬습니다.

1500 iteration 에서 다시 하니 iteration 100 에 붕괴했습니다.

오염이 아닙니다. 4차시 §7.3런 길이 함정입니다 — AdaBoot 램프가 max_iterations 에 비례하니, 600짜리 셀과 1500짜리 셀은 초반 커리큘럼이 다른 실험이었습니다.

Important

짧은 셀로 빠르게 훑는 것은 좋습니다. 다만 그 결론은 그 지평에서만 유효하고, 채택하기 전에 반드시 목표 지평에서 재확인해야 합니다. 이 저장소는 그걸 안 해서 결론 하나를 철회했습니다.


8. 랜덤화는 성능을 깎는 게 정상이다

1차시 §6.3에서 미뤄 둔 수치입니다. (험지 Base, 1500 iteration)

설정 underlying 지형 레벨 커리큘럼 기울기
randomization 없음 0.7500 5.56 +0.059
+ randomization 0.6373 5.05 +0.057
+ 논문 push 까지 0.4915 4.14 +0.103

randomization 을 켜면 추종 성능이 0.75 → 0.64로 떨어집니다. 15% 하락입니다. 그런데도 켠 채로 갑니다.

Important판정 기준이 “점수” 가 아니기 때문입니다

randomization 을 켠 실험에서 봐야 할 것은 “얼마나 떨어졌나” 가 아니라 “여전히 학습이 진행 중인가” 입니다. 두 지표로 봅니다.

  • 여전히 걷는가 — 걷습니다(지형 레벨 5.05는 충분히 높습니다).
  • 커리큘럼이 계속 오르는가 — 기울기 +0.057 대 기준선 +0.059. 사실상 같습니다. 즉 학습 속도가 느려진 게 아니라 난이도가 올라간 것입니다.

push 를 뺀 이유도 여기 있습니다. push 는 기울기가 오히려 +0.103 으로 더 가파릅니다 — 학습이 망가진 게 아니라 아직 한창 올라가는 중이라는 뜻입니다. 지형 4.14에서 +0.103/100it 이면 4000 iteration 으로는 포화에 못 닿습니다. 기각이 아니라 예산 문제입니다.


9. 실패는 어떻게 생겼나

1차시 §5.5의 논문 레시피 기준선을 다시 떠올려 봅시다 — 평균 보상 −7.03, base_contact 종료 78%, 추종 0.073.

그리고 2차시 §7의 토이 환경 기준선 — 가만히 서 있기 0.4426, 무작위 0.5333.

이 둘은 같은 이야기입니다. 강화학습의 기본 실패 모드는 “폭발” 이 아니라 “포기” 입니다. 움직이면 벌을 받고 안 움직이면 안 받으니, 로봇은 안 움직이는 법을 배웁니다. 곡선은 평평하게 수렴하고, 손실은 안정적이고, 아무 에러도 안 납니다.

체크리스트 — 학습이 “잘 수렴한 것 같은데 이상하다” 싶으면 순서대로 확인하세요.

  1. 에피소드 길이가 최대치인가, 아니면 3분의 1쯤에서 끝나는가?
  2. 종료 사유가 타임아웃인가 넘어짐인가?
  3. 행동의 표준편차가 천장에 붙어 있지 않은가? (2차시 §5)
  4. 커리큘럼이 오르고 있는가, 아니면 초기값 근처에 머물러 있는가?
  5. (CENet 을 쓴다면) cenet_mu_abs 가 1e−3 대로 떨어지지 않았는가?

보상 곡선은 이 다섯 개 중 아무것도 알려주지 않습니다.


10. 아직 모르는 것

이 회차의 결론은 자랑이 아니라 목록입니다. 정직한 결과 보고에는 모르는 것의 목록이 반드시 들어갑니다.

Important이 시리즈가 답하지 못한 것
  1. 단일 seed. §4의 모든 ± 는 한 런 안의 출렁임이지 seed 간 변동이 아닙니다. 최소 3 seed 를 돌려야 “+0.0147” 을 효과라고 부를 수 있습니다.
  2. 평지 열은 교란되어 있습니다(§4.2). 평지 Oracle 의 critic 설정을 고치고 다시 돌려야 평지에서의 회수 가능 총량을 알 수 있습니다.
  3. EstNet 대조 런이 없습니다. “VAE 가 단순 회귀보다 낫다” 는 3차시 §7에서 설계 논증으로만 말했고, 숫자로는 확인하지 않았습니다.
  4. 속도 추정 궤적이 없습니다. “CENet 이 추정한 속도 vs 진짜 속도” 를 시간축으로 그린 그림이 CENet 을 가장 직접적으로 보여 주는 자료인데, 수집 스크립트는 있지만 한 번도 돌린 적이 없습니다.
  5. AdaBoot 이 여전히 시간 기반입니다. 논문의 되먹임을 구현하면 §7.3의 함정 자체가 사라질 수 있습니다.
  6. system_delay 가 Manager 스택에 미구현입니다. 실로봇에는 반드시 있는 지연입니다.
  7. push × CENet 조합을 검증하지 않았습니다(§8).

이 목록이 길다는 것이 이 프로젝트의 흠은 아닙니다. 목록을 갖고 있다는 것이 중요합니다. “우리 방법이 좋습니다” 만 있고 이런 목록이 없는 보고서는, 확인을 안 한 것이지 확인할 게 없는 것이 아닙니다.


11. 손으로 해보기

이 회차의 학습지 4장은 성격이 다릅니다. 구현이 아니라 판정입니다 — 영상으로 보고, 숫자로 확인하고, 해석문을 쓰고, 원인을 가릅니다.

cd exercises/stage5_compare/task02_curves
~/IsaacLab/_isaac_sim/python.sh check.py
~/IsaacLab/_isaac_sim/python.sh check.py --solution

학습지 1·2는 학습 산출물이 필요합니다. 각 런 폴더의 videos/play/rl-video-step-0.mp4 와 tfevents 를 씁니다.

학습지 1 — 체크포인트 3종을 눈으로 본다 L0 · 보기만

목표: 쓸 코드는 없습니다. 봅니다.

cd dreamwaq_manager/logs/rsl_rl
ls */*/videos/play/*.mp4

rough 3종(Base / CENet 팔 / Oracle)을 나란히 봅니다.

Important이 시리즈의 제1 교훈

지표만 보지 말고 영상으로 보행을 확인하세요. 보상 곡선이 올라가는데 로봇은 무릎으로 기어가고 있는 경우가 실제로 있습니다. 숫자로는 알 수 없습니다.

§1.2에서 “추종 성능 하나만 보면 속는다” 고 한 것의 가장 직접적인 형태입니다. 그리고 §5의 “두 런의 보상 곡선은 똑같이 생겼었다” 와 같은 이야기이기도 합니다.

학습지 2 — 곡선과 요약표 L2

목표: 학습지 1에서 눈으로 본 차이를 숫자로 확인합니다. summarize() 를 씁니다.

왜 총 보상을 그대로 쓰면 안 되나Train/mean_reward 는 여러 보상 항의 입니다 (공식 레시피는 10항을 로깅합니다). 값이 올랐어도 속도 추종이 좋아진 것인지 에너지 페널티가 줄어든 것인지 알 수 없습니다. 그래서 §1.1처럼 추종 항만 떼어 가중치로 되돌립니다.

\texttt{underlying} = \frac{\texttt{Episode\_Reward/track\_lin\_vel\_xy\_exp}}{1.5} \in [0,1]

그다음 §2의 두 델타를 계산합니다 — CENet 팔 − Base, Oracle − CENet 팔.

일부러 마지막 3점만 평균 내 보세요. §1.3의 계산대로 표준오차가 효과보다 커지고, 실행할 때마다 결론이 바뀌는 것을 직접 보게 됩니다. 그다음 마지막 10%(400 iteration)로 바꾸면 값이 안정됩니다. 이 차이를 손으로 겪는 것이 이 학습지의 목적입니다.

학습지 3 — 두 델타를 해석한다 서술

목표: 학습지 2가 낸 숫자로 CENet 이 무슨 일을 했는지 씁니다. 코드는 없습니다.

Important

미리 말해 둡니다 — 결과는 깔끔하지 않습니다. rough 에서는 기대한 순서가 나왔고 flat 에서는 안 나왔습니다(§4.2). 곁가지 지표 중 하나는 아예 반대를 가리킵니다 (§4.1의 base_contact). 그것을 어떻게 읽을지가 이 과제입니다.

정답이 하나가 아닙니다. 다만 틀린 답은 분명히 있습니다 — 유리한 열만 골라 쓰거나, 교란된 열(§4.2)을 그대로 결론에 넣는 것.

먼저 비교가 성립하는지부터 확인하는 연습을 합니다. 세 런은 env·보상·지형 커리큘럼· randomization·PPO 하이퍼파라미터·예산(4000 iter × 4096 env)·seed(42)·critic 관측이 전부 같고, 다른 것은 actor 관측뿐입니다. 이걸 확인하지 않고 뺄셈부터 하면 §3의 sweep 혼동을 그대로 반복하게 됩니다.

학습지 4 — 무엇이 먼저 죽었는지 가른다 L2

목표: §6.1의 probe_encoder 를 직접 구현합니다. 체크포인트만으로 붕괴를 판정합니다.

학습지 3에서 “순서가 안 나오면 그것도 결과다” 라고 했습니다. 그럼 어떻게 원인을 찾나요?

이 프로젝트의 잠재 z(16차원)는 두 번 죽었고 기전이 서로 달랐습니다. 그리고 둘은 겉으로 완전히 똑같이 생겼습니다\lVert\mu\rVert \to 0, \sigma \to 1, KL \to 0.

Important

처방은 정반대입니다. 한쪽은 \beta 를 낮추면 살고, 다른 쪽은 아무리 낮춰도 안 삽니다. 겉모습만 보고 처방을 고르면 반은 틀립니다.

이 학습지는 겉모습이 같은 두 상태를 가르는 두 번째 지표를 재는 것입니다 — §6.2의 \lVert W_z\rVert, 즉 디코더 1층에서 z 쪽으로 들어오는 가중치의 열 노름입니다.

왜 그렇게 갈리는지는 구조에 있습니다. 디코더는 [\hat v(3), z(16)] 을 받아 다음 관측을 복원하는데, z 를 살려 두는 힘은 재구성 손실 하나뿐입니다 — 정책 그래디언트는 인코더에 닿지 않고, \hat vvel_loss 로 따로 지도학습됩니다. 그래서 두 가지로 죽습니다 (3차시 §6의 ⓐ/ⓑ).

풀고 나면 §6.2의 4행 표를 직접 재현할 수 있고, 위 사분면 위젯에 자기 체크포인트 값을 넣어 판정을 받을 수 있습니다.


12. 핵심 정리

  • underlying 추종으로 재고, 지형 레벨·종료 사유·에피소드 길이를 함께 본다.
  • 마지막 몇 점으로 최종값을 읽지 않는다. 마지막 10% 평균 ± 표준오차 — 노이즈(sd 0.03)가 재려는 효과(0.015)보다 크기 때문이다.
  • 세 팔의 뺄셈이 답이다: Oracle − Base = 회수 가능한 총량, CENet 팔 − Base = 회수한 양.
  • 출처를 먼저 적는다. 이 저장소의 네 sweep 은 서로 비교할 수 없고, 로그 이름으로는 그게 안 보인다. clip_actions 가 다르면 비교 금지.
  • 현재 설정(β=0.35) 험지 결과: Base 0.6870 / CENet 팔 0.7017 / Oracle 0.7407. CENet 팔 − Base = +0.0147 ± 0.0020, 회수율 약 27%. 단일 seed.
  • 평지 열은 읽으면 안 된다 — Oracle 팔의 critic 이 혼자 노이즈를 본다. 상한선을 이겼다면 상한선을 의심하라.
  • 보상 곡선으로는 잠재의 생사를 알 수 없다. 진단은 가중치에서 나온다 — \lVert\mu\rVert\lVert W_z\rVert 두 축으로 봐야 처방이 갈린다.
  • 절제는 한 번에 하나씩 되돌린다. 그리고 짧은 지평의 결론은 목표 지평에서 재확인한다.
  • randomization 은 점수를 깎는 게 정상이다. 판정 기준은 점수가 아니라 “여전히 걷는가 + 커리큘럼이 오르는가” 다.
  • 모르는 것의 목록을 갖고 다닌다.

13. 시리즈 마무리

여섯 회차를 하나로 이으면 이렇습니다.

차시 한 줄 요약
0차시 시뮬레이터를 손으로 만져본다. 세 층 · 3단 구조 · 학습지 5장
1차시 관측(45)·행동(PD)·보상·randomization 으로 환경을 만든다. 세 팔의 비교축을 세운다
2차시 PPO + 비대칭 actor-critic 으로 학습한다. 하이퍼파라미터 하나가 탐험을 천장에 못박은 사건
3차시 왜 VAE 인가 — 라벨 없는 지형을 짜내려고. 그리고 \beta 하나가 잠재를 죽이는 방식
4차시 CENet 과 PPO 를 한 반복에 묶는다. 정규화 수술 · AdaBoot · 조용한 실패
5차시 (오늘) 결과를 의심하는 법. 잠재의 생사는 곡선이 아니라 가중치에서

\underbrace{45}_{\text{1차시}} \;\longrightarrow\; \underbrace{\text{CENet}}_{\text{3차시}} \;\longrightarrow\; \underbrace{64}_{\text{4차시}} \;\longrightarrow\; \underbrace{\text{PPO}}_{\text{2차시}} \;\longrightarrow\; \underbrace{\text{검증}}_{\text{5차시}}

이 시리즈가 정말 말하려던 것

알고리즘 하나를 재현하는 이야기가 아니었습니다. 반복해서 나온 것은 세 가지 습관입니다.

① 값에는 이유가 있어야 한다. entropy_coef=0.005, clip_actions=4.0, \beta=0.35 — 셋 다 누가 정해 준 값이 아니라 틀린 값을 쓰다가 증상을 보고 고친 값입니다. 그 과정이 2차시 §5와 3차시 §5에 그대로 남아 있습니다.

② 조용한 실패를 의심한다. 이 시리즈에서 가장 많이 반복된 문장입니다. σ가 천장에 붙어도, 잠재가 죽어도, CENet 이 NaN 을 뱉어도 학습 곡선은 멀쩡했습니다. 에러도 안 났습니다. 곡선만 보고 있으면 아무것도 못 잡습니다.

③ 모르는 것을 적어 둔다. §10이 이 시리즈의 진짜 결론입니다.

Tip더 나아가기
  • 직접 돌려 보기 — 각 회차 말미의 실습(exercises/)이 GAE·PPO 손실·ToyVAE·CENet forward·손실·관측 증강·붕괴 진단까지 이어집니다. 읽는 것과 채우는 것은 다릅니다.
  • §10의 목록 중 하나 풀기 — seed 3개 돌려 보기, 평지 Oracle 의 critic 설정 고치기, EstNet 대조 런 돌려 보기. 전부 이 시리즈가 하다 만 것들입니다.
  • 위로 얹기 — 이 정책은 명령 속도 (v_x, v_y, \omega_{yaw}) 세 개만 받습니다. 그 위에 비전이나 언어로 명령을 만들어 주는 층을 얹는 확장은 lec/ideas.md 에 정리해 뒀습니다. 아래 정책은 그대로 두고 위에 층을 더하는 구조입니다.