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;
실험 설계와 결과 해석 — 잠재가 죽었는지 어떻게 아는가
보지 않고 걷기 — CENet + 비대칭 PPO · 5차시
강의 로드맵
| 회차 | 주제 |
|---|---|
| 0차시 | Isaac Sim & Isaac Lab 시작하기 — 시뮬레이터를 손으로 만져보기 |
| 1차시 | 4족 보행 로봇 & Isaac Lab — 관측을 무엇으로 채울 것인가 |
| 2차시 | PPO 설계 — 비대칭 actor-critic과 탐험의 평형점 |
| 3차시 | 왜 VAE인가 — 히스토리에서 속도와 지형을 뽑아내기 |
| 4차시 | 학습 루프 통합 — CENet과 PPO를 한 반복에 묶기 |
| 5차시 (오늘) | 실험 설계와 결과 해석 — 잠재가 죽었는지 어떻게 아는가 |
학습은 돌았습니다. 로그도 쌓였고 곡선도 예쁘게 올라갔습니다. 그런데 이게 정말 CENet 덕분인가요?
이 회차는 숫자를 자랑하는 자리가 아니라 숫자를 의심하는 법을 다룹니다. 이 저장소는 같은 질문에 대해 세 번 다른 답을 냈고, 그중 두 번은 틀렸습니다. 어떻게 틀렸고 어떻게 알아챘는지가 오늘의 내용입니다.
- 세 팔의 차이를 어떻게 재야 신뢰할 수 있는지 안다 — 무엇을 재고, 어떻게 평균 내고, 오차를 어떻게 붙이는지.
- 잠재가 죽었는지 가중치에서 판정할 수 있다. 보상 곡선으로는 안 됩니다.
- 비교하면 안 되는 실험을 구별하고, 내 실험의 출처를 기록하는 습관을 갖는다.
난이도 노트 — 이 회차는 앞의 넷과 성격이 다릅니다. 새 알고리즘이 나오지 않고, 대신 “내가 방금 만든 숫자를 얼마나 믿어야 하나” 를 다룹니다. 결과를 직접 만들어 볼 사람에게 가장 쓸모 있고, 아니라면 §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이면 명령 속도를 완벽히 따라간 것입니다. 이렇게 되나눠야 레시피가 다른 실험끼리도 같은 자로 잴 수 있습니다.
함정 — 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, 관측만 다릅니다.
두 개의 뺄셈이 답입니다.
- Oracle − Base = 선속도를 아는 것의 가치 = 회수 가능한 총량(headroom)
- CENet 팔 − Base = CENet 이 실제로 회수한 양
그리고 비율 \dfrac{\text{Waq}-\text{Base}}{\text{Oracle}-\text{Base}} 이 “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;
- 한 표에는 한 sweep 만. 서로 다른 sweep 의 숫자를 같은 표에 놓지 않습니다.
- 모든 결과 표에 출처 열을 답니다. 로그 디렉터리 이름과 날짜.
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런
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 |
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 이 노이즈 낀 관측을 봅니다.
환경 설정 쪽에서는 이걸 고쳐 뒀습니다 — 노이즈 없는 critic 그룹을 따로 만들어 둡니다. 그런데 러너 설정이 critic 을 다시 policy 로 되돌려 놓습니다. 환경의 수정이 무효화된 것입니다. 그 결과 여섯 팔 중 평지 Oracle 하나만 가치 함수가 관측 노이즈를 보며 학습합니다.
상한선이 되어야 할 팔에 핸디캡이 걸려 있습니다. 그러니 이 sweep 의 평지 열에서는 “회수 가능한 총량” 을 잴 수 없고, CENet 팔 − Base = +0.0208 이 무엇의 효과인지도 말할 수 없습니다. 험지 열만 해석 가능합니다.
이건 과거 sweep 의 이야기가 아니라 지금 설정에 남아 있는 문제입니다. 저장소 문서가 이미 증상(평지 Oracle 0.8920 이 Base 0.8917 과 동률)을 기록해 두었는데, 고친 곳과 되돌리는 곳이 달라서 계속 재현되고 있었습니다.
배울 것 — 설정이 여러 층으로 겹치면(환경 cfg → 러너 cfg → CLI) 나중 층이 앞 층의 수정을 조용히 덮습니다. 그래서 결과를 믿기 전에 실제로 저장된 설정 파일(params/agent.yaml)을 읽어야 합니다. 코드에 그렇게 쓰여 있다는 것과 그 런이 그렇게 돌았다는 것은 다릅니다.
4.3 이 표에 붙는 단서
- 단일 seed(42). ± 는 한 런 안에서의 출렁임이지 seed 간 변동이 아닙니다. seed 를 바꾸면 이 정도 차이는 흔들릴 수 있습니다.
- 평지 열은 교란되어 있습니다(§4.2).
- AdaBoot 은 여전히 시간 기반입니다(4차시 §7.2) — 논문의 되먹임이 없습니다.
- 회수율 27%는 험지, 이 지형 조합, 이 randomization 설정에서의 값입니다.
5. 부호가 뒤집힌 적이 있다
이 표가 왜 중요한지 보여 주는 사건입니다.
CENet 손실의 감축 축을 고치기 전(3차시 §5), 같은 비교를 하면 험지에서
\text{Waq} - \text{Base} = \mathbf{-0.0166}
였습니다. CENet 을 붙이면 오히려 나빠진다는 결과입니다. 고친 뒤에는 +0.0073, 그리고 지금 설정에서는 +0.0147 입니다. 부호가 뒤집혔습니다.
이게 핵심입니다. 잠재가 죽은 런과 살아 있는 런의 학습 곡선을 나란히 놓으면 구별할 수 없습니다. 둘 다 잘 올라가고, 둘 다 수렴하고, 둘 다 로봇이 걷습니다.
당연합니다 — 잠재가 죽어도 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 | 건강 |
\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개.
가로축 \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짜리 셀은 초반 커리큘럼이 다른 실험이었습니다.
짧은 셀로 빠르게 훑는 것은 좋습니다. 다만 그 결론은 그 지평에서만 유효하고, 채택하기 전에 반드시 목표 지평에서 재확인해야 합니다. 이 저장소는 그걸 안 해서 결론 하나를 철회했습니다.
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% 하락입니다. 그런데도 켠 채로 갑니다.
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.
이 둘은 같은 이야기입니다. 강화학습의 기본 실패 모드는 “폭발” 이 아니라 “포기” 입니다. 움직이면 벌을 받고 안 움직이면 안 받으니, 로봇은 안 움직이는 법을 배웁니다. 곡선은 평평하게 수렴하고, 손실은 안정적이고, 아무 에러도 안 납니다.
체크리스트 — 학습이 “잘 수렴한 것 같은데 이상하다” 싶으면 순서대로 확인하세요.
- 에피소드 길이가 최대치인가, 아니면 3분의 1쯤에서 끝나는가?
- 종료 사유가 타임아웃인가 넘어짐인가?
- 행동의 표준편차가 천장에 붙어 있지 않은가? (2차시 §5)
- 커리큘럼이 오르고 있는가, 아니면 초기값 근처에 머물러 있는가?
- (CENet 을 쓴다면)
cenet_mu_abs가 1e−3 대로 떨어지지 않았는가?
보상 곡선은 이 다섯 개 중 아무것도 알려주지 않습니다.
10. 아직 모르는 것
이 회차의 결론은 자랑이 아니라 목록입니다. 정직한 결과 보고에는 모르는 것의 목록이 반드시 들어갑니다.
- 단일 seed. §4의 모든 ± 는 한 런 안의 출렁임이지 seed 간 변동이 아닙니다. 최소 3 seed 를 돌려야 “+0.0147” 을 효과라고 부를 수 있습니다.
- 평지 열은 교란되어 있습니다(§4.2). 평지 Oracle 의 critic 설정을 고치고 다시 돌려야 평지에서의 회수 가능 총량을 알 수 있습니다.
- EstNet 대조 런이 없습니다. “VAE 가 단순 회귀보다 낫다” 는 3차시 §7에서 설계 논증으로만 말했고, 숫자로는 확인하지 않았습니다.
- 속도 추정 궤적이 없습니다. “CENet 이 추정한 속도 vs 진짜 속도” 를 시간축으로 그린 그림이 CENet 을 가장 직접적으로 보여 주는 자료인데, 수집 스크립트는 있지만 한 번도 돌린 적이 없습니다.
- AdaBoot 이 여전히 시간 기반입니다. 논문의 되먹임을 구현하면 §7.3의 함정 자체가 사라질 수 있습니다.
system_delay가 Manager 스택에 미구현입니다. 실로봇에는 반드시 있는 지연입니다.- push × CENet 조합을 검증하지 않았습니다(§8).
이 목록이 길다는 것이 이 프로젝트의 흠은 아닙니다. 목록을 갖고 있다는 것이 중요합니다. “우리 방법이 좋습니다” 만 있고 이런 목록이 없는 보고서는, 확인을 안 한 것이지 확인할 게 없는 것이 아닙니다.
11. 손으로 해보기
이 회차의 학습지 4장은 성격이 다릅니다. 구현이 아니라 판정입니다 — 영상으로 보고, 숫자로 확인하고, 해석문을 쓰고, 원인을 가릅니다.
학습지 1 — 체크포인트 3종을 눈으로 본다 L0 · 보기만
목표: 쓸 코드는 없습니다. 봅니다.
rough 3종(Base / CENet 팔 / Oracle)을 나란히 봅니다.
지표만 보지 말고 영상으로 보행을 확인하세요. 보상 곡선이 올라가는데 로봇은 무릎으로 기어가고 있는 경우가 실제로 있습니다. 숫자로는 알 수 없습니다.
§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 이 무슨 일을 했는지 씁니다. 코드는 없습니다.
미리 말해 둡니다 — 결과는 깔끔하지 않습니다. 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.
처방은 정반대입니다. 한쪽은 \beta 를 낮추면 살고, 다른 쪽은 아무리 낮춰도 안 삽니다. 겉모습만 보고 처방을 고르면 반은 틀립니다.
이 학습지는 겉모습이 같은 두 상태를 가르는 두 번째 지표를 재는 것입니다 — §6.2의 \lVert W_z\rVert, 즉 디코더 1층에서 z 쪽으로 들어오는 가중치의 열 노름입니다.
왜 그렇게 갈리는지는 구조에 있습니다. 디코더는 [\hat v(3), z(16)] 을 받아 다음 관측을 복원하는데, z 를 살려 두는 힘은 재구성 손실 하나뿐입니다 — 정책 그래디언트는 인코더에 닿지 않고, \hat v 는 vel_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이 이 시리즈의 진짜 결론입니다.
- 직접 돌려 보기 — 각 회차 말미의 실습(
exercises/)이 GAE·PPO 손실·ToyVAE·CENetforward·손실·관측 증강·붕괴 진단까지 이어집니다. 읽는 것과 채우는 것은 다릅니다. - §10의 목록 중 하나 풀기 — seed 3개 돌려 보기, 평지 Oracle 의 critic 설정 고치기, EstNet 대조 런 돌려 보기. 전부 이 시리즈가 하다 만 것들입니다.
- 위로 얹기 — 이 정책은 명령 속도 (v_x, v_y, \omega_{yaw}) 세 개만 받습니다. 그 위에 비전이나 언어로 명령을 만들어 주는 층을 얹는 확장은
lec/ideas.md에 정리해 뒀습니다. 아래 정책은 그대로 두고 위에 층을 더하는 구조입니다.