flowchart LR AG["에이전트 (정책 πθ)"] -->|"행동 a"| EN["환경 (IsaacLab)"] EN -->|"보상 r, 다음 상태 s'"| AG classDef ag fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px; classDef en fill:#e3f2fd,stroke:#1565c0,stroke-width:2px; class AG ag; class EN en;
PPO 설계 — 비대칭 actor-critic과 탐험의 평형점
보지 않고 걷기 — CENet + 비대칭 PPO · 2차시
강의 로드맵
| 회차 | 주제 |
|---|---|
| 0차시 | Isaac Sim & Isaac Lab 시작하기 — 시뮬레이터를 손으로 만져보기 |
| 1차시 | 4족 보행 로봇 & Isaac Lab — 관측을 무엇으로 채울 것인가 |
| 2차시 (오늘) | PPO 설계 — 비대칭 actor-critic과 탐험의 평형점 |
| 3차시 | 왜 VAE인가 — 히스토리에서 속도와 지형을 뽑아내기 |
| 4차시 | 학습 루프 통합 — CENet과 PPO를 한 반복에 묶기 |
| 5차시 | 실험 설계와 결과 해석 — 잠재가 죽었는지 어떻게 아는가 |
1차시 환경 위에서, 로봇이 명령 속도로 걷도록 정책을 학습시킵니다. 알고리즘은 PPO, 라이브러리는 rsl_rl입니다.
이 회차의 절반은 PPO 자체이고, 나머지 절반은 왜 이 값들인가 입니다. 하이퍼파라미터 하나가 어떻게 정책의 탐험 폭을 천장에 붙여 버렸는지, 그리고 그걸 어떻게 알아냈는지를 §5에서 끝까지 따라갑니다 — 이 시리즈에서 “설계” 라는 말이 무슨 뜻인지 보여 주는 절입니다.
- 강화학습의 기본 개념부터 PPO-clip과 GAE를 직관으로 이해한다.
- PPO 설정(
rsl_rl_ppo_cfg.py)을 코드로 읽고, 롤아웃→갱신 학습 루프를 따라간다. - 왜 이 값인가 — 하이퍼파라미터 하나가 탐험을 천장에 못박은 사건을 끝까지 추적한다 (§5).
이 강의는 강화학습이 처음이어도 따라올 수 있게 직관 → 비유 → 수식 순서로 풉니다. 수식의 모든 기호는 바로 옆에서 풀어 설명합니다.
1. 강화학습과 PPO — 직관부터
강화학습이 뭔가요?
강화학습(RL)은 시행착오로 배우는 방식입니다. 강아지에게 간식으로 “앉아”를 가르치듯, 로봇 (에이전트)이 환경에서 여러 행동을 해 보고, 잘했을 때 보상을 받으며 점점 더 나은 행동을 익힙니다. 지도학습처럼 정답 라벨이 주어지는 게 아니라, 오직 보상이라는 점수만 있습니다.
매 순간 아래 고리가 돕니다 — 로봇이 상태(관측)를 보고 → 행동하면 → 환경이 보상과 다음 상태를 돌려줍니다. 이 고리를 수없이 반복하며 “어떤 상황에서 어떤 행동을 하면 점수가 큰지”를 배웁니다.
정책 · 가치 · 어드밴티지
학습의 주인공인 정책(policy) \pi_\theta는 “상황을 보고 행동을 고르는 전략”입니다 (\theta는 신경망 파라미터). 걷기처럼 행동이 연속값일 때는 하나의 값이 아니라 확률분포를 출력합니다 — 가우시안(정규분포)으로 평균 \mu_\theta(s)와 표준편차 \sigma를 냅니다.
\pi_\theta(a\mid s)=\mathcal{N}\big(\mu_\theta(s),\,\sigma^2\big)
쉽게 말하면 — \mu는 “지금 상황에서 내 최선의 행동”, \sigma는 “얼마나 모험을 해 볼지”입니다. \sigma가 크면 다양한 행동을 시도(탐험)하고, 학습이 끝나갈수록 작아지며 확신에 찬 행동을 합니다.
행동을 평가하려면 두 개념이 더 필요합니다.
- 가치 V(s) — “이 상황은 앞으로 받을 보상까지 따지면 얼마나 좋은가”. 즉 상황의 점수.
- 어드밴티지 A(s,a)=Q(s,a)-V(s) — “그 상황에서 이 행동이 평균보다 얼마나 더 좋았나”. A>0이면 평소보다 잘한 행동, A<0이면 못한 행동. 이 값이 학습의 방향키입니다.
정책을 어떻게 개선하나 — 그리고 PPO가 푸는 문제
기본 아이디어는 한 문장입니다: 어드밴티지가 양수였던 행동은 더 자주, 음수였던 행동은 덜 하도록 정책을 살짝 민다. 문제는 얼마나 미느냐입니다. 한 번의 경험으로 정책을 너무 크게 바꾸면 학습이 무너집니다.
비유 — 한 판 이겼다고 전략을 통째로 갈아엎으면, 그게 운이었을 때 다음 판을 망칩니다. 조금씩 신중하게 바꿔야 합니다.
PPO는 이 “조금씩”을 강제합니다. 새 정책이 옛 정책보다 그 행동을 몇 배 더(혹은 덜) 하려는지를 확률비 r_t로 재고, 이 비율이 [1-\epsilon,\,1+\epsilon]를 벗어나면 더 이상 이득을 주지 않도록 잘라(clip) 버립니다. 즉 “한 번에 ε(=20%)까지만 바꿔”라는 안전벨트입니다.
r_t(\theta)=\frac{\pi_\theta(a_t\mid s_t)}{\pi_{\theta_{old}}(a_t\mid s_t)}, \qquad L^{CLIP} = \mathbb{E}\Big[\min\big(r_t A_t,\ \text{clip}(r_t, 1-\epsilon, 1+\epsilon)\,A_t\big)\Big]
기호 풀이 — r_t: 새/옛 정책의 확률비(1이면 그대로, 1.2면 20% 더 하려 함), \mathbb{E}[\cdot]: 모은 경험들의 평균, \text{clip}(\cdot): 값을 범위 안으로 가두는 함수, \epsilon=0.2: 허용 폭.
아래 그래프로 직접 확인해 보세요. 확률비 r_t가 clip 구간을 벗어나면 목적함수가 평평해져 (기울기 0 → 더 못 바뀜) 정책이 한 번에 크게 변하지 못합니다. 어드밴티지 부호와 \epsilon을 바꿔가며 회색 구간 밖에서 곡선이 꺾이는 지점을 보세요.
실선 = clip 목적함수 LCLIP, 점선 = clip 없는 r·A, 회색 띠 = clip 구간
어드밴티지는 어떻게 구하나 — GAE
앞서 어드밴티지가 방향키라고 했는데, “이 행동 덕분에 미래 보상이 평균보다 나았나”는 미래가 불확실해 추정하기 까다롭습니다. GAE는 한 스텝만 내다보는 추정(편향 적음·노이즈 큼)과 끝까지 보는 추정(노이즈 적음·편향 큼) 사이를 \lambda로 절충합니다.
먼저 한 스텝의 “예상보다 좋았던 정도”인 TD 잔차 \delta_t를 정의하고, 이를 미래로 갈수록 점점 작게 가중해 더합니다.
\delta_t = r_t + \gamma V(s_{t+1}) - V(s_t), \qquad \hat A_t = \sum_{l\ge 0}(\gamma\lambda)^l\,\delta_{t+l}
기호 풀이 — \delta_t: 받은 보상 + 다음 상황 가치가 현재 가치보다 얼마나 컸나(=예상 대비 깜짝 정도), \gamma=0.99: 할인율(먼 미래 보상일수록 덜 중요하게), \lambda=0.95: 편향-노이즈 절충 손잡이.
최종 손실 — 세 가지를 함께
PPO가 실제로 최소화하는 손실은 세 조각의 합입니다.
L = \underbrace{L^{CLIP}}_{\text{① 정책 개선}}\;-\;c_1\underbrace{L^{VF}}_{\text{② 가치 정확도}}\;+\;c_2\underbrace{S[\pi_\theta]}_{\text{③ 탐험 유지}}, \qquad c_1=1.0,\ c_2=0.005
- ① L^{CLIP} — 위에서 본 안전한 정책 개선.
- ② L^{VF} — critic이 가치 V(s)를 정확히 맞히게(가치가 정확해야 ①의 어드밴티지도 정확).
- ③ S[\pi_\theta] — 정책의 엔트로피(무작위성) 보너스. 너무 일찍 한 행동만 고집하지 않도록 약간의 탐험을 남겨둠.
이 상수 \epsilon, c_1, c_2, \gamma, \lambda가 코드 설정에 그대로 들어갑니다 — 아래에서 직접 확인합니다.
2. 네트워크 설정 — 코드로 읽기
actor 와 critic 이 다른 입력을 받는 비대칭 구조이고(1차시 §3.4), 둘 다 [512,256,128] ELU MLP입니다.
# dreamwaq_manager/.../config/go2/agents/rsl_rl_ppo_cfg.py (Go2BasePPORunnerCfg)
actor = RslRlMLPModelCfg(
hidden_dims=[512, 256, 128],
activation="elu",
obs_normalization=True,
distribution_cfg=RslRlMLPModelCfg.GaussianDistributionCfg(
init_std=1.0,
std_type="log",
),
)
critic = RslRlMLPModelCfg(
hidden_dims=[512, 256, 128],
activation="elu",
obs_normalization=False,
)- 1
-
은닉층 3개
512 → 256 → 128. actor 출력은 12(행동 평균), critic 출력은 1(상태가치). - 2
-
actor 입력 정규화 ON — 관측 분포를 표준화해 학습 안정화 (단, CENet 팔은 4차시에서 이 정규화기를
Identity로 갈아 끼웁니다. 이유가 꽤 재미있습니다). - 3
- 가우시안 정책의 초기 표준편차 \sigma=1.0 — 초기 탐험 폭.
- 4
- \sigma를 log 스케일로 파라미터화 → 항상 양수 보장, 수치 안정.
- 5
- critic은 정규화 OFF (Base). 입력이 특권 관측이라 분포가 안정적.
| Actor | Critic | |
|---|---|---|
| 입력 (Base / Oracle / CENet 팔) | 45 / 48 / 64 | rough 235 · flat 48 |
| 출력 | 12 (행동 평균) | 1 (가치) |
critic 차원의 구성은 1차시 §3.4에 정리돼 있습니다 — rough 는 45 + lin_vel(3) + height_scan(187), flat 은 45 + lin_vel(3). 평지에는 알려 줄 지형이 없어 critic 의 특권이 선속도 하나로 줄어듭니다. 이 사실이 나중에 “왜 평지에서는 CENet 이 별 이득이 없어 보이는가”를 설명합니다(5차시).
flowchart LR O1["Actor 관측<br/>45 / 48 / 64"] --> AM["MLP 512·256·128 ELU"] --> MU["평균 μ (12)"] --> DI["𝒩(μ, σ²)"] --> ACT(["행동 (12)"]) O2["Critic 관측<br/>235 rough / 48 flat (특권)"] --> CM["MLP 512·256·128 ELU"] --> V(["가치 V (1)"]) V -. "GAE 어드밴티지" .-> AM classDef a fill:#e8f5e9,stroke:#2e7d32; classDef c fill:#fce4ec,stroke:#c2185b; class O1,AM,MU,DI,ACT a; class O2,CM,V c;
왜 비대칭인가? critic은 학습 중에만 쓰이므로 지형·외란 같은 특권 정보로 정확한 가치를 추정 → 더 좋은 어드밴티지 → actor 학습 가속. actor는 배포 가능한 관측만 받습니다.
3. PPO 알고리즘 설정 — 코드로 읽기
§1의 수식 상수들이 그대로 설정에 들어갑니다.
# rsl_rl_ppo_cfg.py (Go2BasePPORunnerCfg.algorithm)
algorithm = RslRlPpoAlgorithmCfg(
clip_param=0.2,
value_loss_coef=1.0,
use_clipped_value_loss=True,
entropy_coef=0.005,
gamma=0.99,
lam=0.95,
num_learning_epochs=5,
num_mini_batches=4,
learning_rate=1.0e-3,
schedule="adaptive",
desired_kl=0.01,
max_grad_norm=1.0,
)- 1
- \epsilon=0.2 — L^{CLIP}의 clip 범위.
- 2
- c_1=1.0 — 가치손실 가중치. clipped value loss도 사용 — 가치 갱신도 한 번에 크게 튀지 않도록 같은 방식으로 자릅니다.
- 3
- c_2=0.005 — 엔트로피 보너스(탐험 유지). Isaac Lab 기본값 0.01의 절반. §5 전체가 이 한 줄에 관한 이야기입니다.
- 4
- \gamma=0.99, \lambda=0.95 — GAE 상수.
- 5
- 한 롤아웃을 5 epoch × 4 mini-batch로 재사용해 갱신.
- 6
- KL 기반 adaptive 학습률 — §3.4에서 규칙을 정확히 봅니다.
- 7
- 그래디언트 노름을 1.0으로 클리핑 → 폭주 방지.
max_iterations — cfg 기본값은 5000이지만, 보고된 모든 실험은 CLI 로 --max_iterations 4000 을 걸어 돌렸습니다. “몇 번 도느냐”는 단순한 예산 문제로 보이지만, 4차시에서 이 값이 학습 초반의 커리큘럼 자체를 조용히 바꿔 버린다는 것을 보게 됩니다. 그래서 이 시리즈는 iteration 수를 항상 명시합니다.
clip_actions = 4.0 — 정책이 낸 행동을 \pm 4 로 자릅니다. 예전에는 이 값이 1.0 이었고, “큰 행동이 관절 한계를 넘어 학습이 붕괴하는 것을 막는다”는 설명이 붙어 있었습니다. 그 설명은 틀렸습니다. 1.0 은 붕괴를 막은 게 아니라 다른 문제의 원인이었고, 그걸 알아내는 과정이 §5입니다.
3.4 학습률을 스스로 조절하기 — 그리고 그 사이의 죽은 구간
schedule="adaptive" 한 줄이 하는 일을 정확히 봅시다. 학습률을 고정하지 않고, 정책이 한 갱신에서 얼마나 많이 변했는지를 재서 되먹임합니다. 그 척도가 KL 발산입니다.
- 1
-
너무 많이 변했다 (KL > 0.02) → 학습률을 1.5로 나눔. 바닥은
1e-5. - 2
-
너무 적게 변했다 (KL < 0.005) → 학습률에 1.5를 곱함. 천장은
1e-2.
비유 — 운전 중 속도계를 보며 액셀을 조절하는 것과 같습니다. 너무 빠르면(정책이 급변하면) 발을 떼고, 너무 느리면 밟습니다. 다만 속도계가 60~80 사이면 아무것도 하지 않습니다 — 매번 반응하면 오히려 출렁이기 때문입니다.
이 “아무것도 하지 않는” 구간이 데드존입니다. desired_kl = 0.01 기준으로:
| 측정 KL | 동작 |
|---|---|
| > 0.02 | lr ÷ 1.5 |
| 0.005 \le \text{KL} \le 0.02 | 아무것도 안 함 ← 데드존 |
| 0 < \text{KL} < 0.005 | lr × 1.5 |
| = 0 | 아무것도 안 함 (아래 참조) |
이 메커니즘은 §5에서 용의자를 지우는 데 쓰입니다 — rough 와 flat 의 학습률 궤적이 사실상 같았기 때문에(1.3e−4 ~ 3.9e−4), “adaptive lr 때문에 이상해진 것”이라는 가설을 실험 없이 배제할 수 있었습니다.
4. 학습 루프 — 코드로 읽기
OnPolicyRunner.learn()은 롤아웃 수집 → 어드밴티지 계산 → PPO 갱신을 반복합니다. 한 반복의 골격은 다음과 같습니다.
- 1
- 롤아웃 수집: 24스텝 동안 4096개 환경에서 전이를 모음 (스텝당 4096개 샘플).
- 2
- actor가 현재 관측에서 행동을 샘플(가우시안에서 추출).
- 3
-
환경 한 스텝 진행 — 1차시의
_pre_physics_step→물리→_get_rewards→_get_observations흐름. - 4
- 전이를 storage에 기록하고 관측 정규화 통계를 갱신.
- 5
- 롤아웃 종료 후 GAE로 어드밴티지·리턴 계산(§1 수식).
- 6
- PPO 갱신: 5 epoch × 4 mini-batch로 L = L^{CLIP} - c_1 L^{VF} + c_2 S 최소화.
flowchart LR S(["관측"]) --> ACT["actor.act<br/>행동 샘플"] --> ENV["env.step<br/>(4096 병렬)"] --> ST["전이 저장"] ST -->|"24스텝 미만: 반복"| ACT ST -->|"24스텝 완료"| GAE["compute_returns<br/>(GAE)"] --> UPD["update<br/>5ep × 4mb"] --> S classDef hot fill:#fff3e0,stroke:#f57c00; class GAE,UPD hot;
Base 와 Oracle 은 이 표준 runner 를 그대로 씁니다. CENet 팔만 커스텀 runner 로 갈아 끼우는데, 교체는 설정 한 줄로 끝납니다.
문자열 하나가 rsl_rl 의 기본 runner 대신 CENet 을 품은 runner 를 고릅니다. 이 한 줄이 정확히 무엇을 갈아 끼우는지 — 45차원 환경에서 어떻게 64차원 actor 가 나오는지 — 는 4차시에서 통째로 다룹니다. PPO 설정 자체는 세 팔이 완전히 동일합니다.
5. 탐험은 어디서 멈추나 — σ 천장
여기서부터가 이 회차의 본론입니다. 지금까지는 “설정이 이렇습니다” 였고, 지금부터는 “이 설정이 어떻게 틀렸고 어떻게 알아냈나” 입니다.
5.1 증상 — 똑같은 숫자 17.0273
정책은 가우시안입니다. 평균 \mu 를 내고 거기에 표준편차 \sigma 만큼 노이즈를 얹어 행동을 뽑습니다. \sigma 가 탐험의 폭입니다. 학습이 진행되면 정책이 확신을 갖게 되면서 \sigma 는 자연스럽게 줄어들어야 합니다.
그런데 로그를 보니 Loss/entropy 가 6개 런 전부에서 정확히 17.0273 이었습니다.
H = 12 \times \tfrac{1}{2}\ln(2\pi e) = 12 \times 1.41894 = 17.0273
기호 풀이 — 12는 행동 차원(관절 12개), \tfrac12\ln(2\pi e) 는 표준편차가 정확히 1인 1차원 가우시안의 엔트로피. 즉 이 값은 \sigma = 1 일 때의 엔트로피입니다.
\sigma 의 상한은 log_std 클램프 [-5, 0] 이 걸어 둔 \sigma \le e^0 = 1 입니다. 정책이 학습 내내 탐험 폭의 천장에 눌려 있었다는 뜻입니다.
| 천장에 붙어 있던 비율 | |
|---|---|
| rough, 마지막 400 iteration | 100% |
| rough, 전체 | 62 ~ 76% |
| flat | 0% (한 번도 안 닿음) |
로봇은 4000 iteration 내내 최대 진폭의 무작위 노이즈를 관절에 넣고 있었습니다. 그 노이즈에 비하면 CENet 이 추정해 준 3차원 속도 같은 건 묻혀 버립니다.
5.2 왜 멈추지 않았나 — 부력과 항력
비유 — \sigma 를 물속 물체의 깊이라고 합시다. 두 힘이 겹칩니다.
- 엔트로피 보너스는 위로 미는 일정한 부력입니다. \sigma 가 크든 작든 세기가 같습니다.
- 서로게이트 손실은 아래로 당기는 항력입니다. \sigma 가 커지면 행동이 엉망이 되어 보상이 떨어지니, 손실이 “\sigma 를 줄여라” 라고 밀어냅니다.
평형은 둘이 같아지는 깊이에서 잡힙니다. 그런데 항력이 어느 깊이부터 더 이상 세지지 않고 포화해 버린다면? 부력이 그 포화값보다 크면 물체는 수면까지 끝없이 떠오릅니다. 평형점이 아예 존재하지 않는 것입니다.
5.3 수식 — 부력의 세기는 정확히 c_2 다
가우시안 정책의 엔트로피는 H = \sum_i \log \sigma_i + \text{const} 입니다. 손실에는 +c_2 S 가 아니라 부호를 뒤집어 -c_2 H 로 들어가므로, \log\sigma_i 에 대한 기울기는
\frac{\partial(-c_2 H)}{\partial \log\sigma_i} = -c_2
상수입니다. \sigma 에 전혀 의존하지 않습니다 — 비유의 “일정한 부력” 이 이것입니다.
따라서 log_std 가 받는 전체 기울기 g_{total} 과 서로게이트가 주는 기울기 g_{surr} 사이에는
g_{surr} = g_{total} + c_2
가 정확히 성립합니다. 기호 풀이 — g_{total}: 실제로 옵티마이저에 전달되는 기울기(계측 가능), c_2: entropy_coef =0.005, g_{surr}: 엔트로피 보너스를 뺀 순수한 “성능 압력”. \sigma 가 평형에 있으려면 g_{total} = 0, 즉 g_{surr} = c_2 여야 합니다.
5.4 계측 — 균형점이 존재하지 않았다
DWQ_PROBE_STDGRAD=1 로 g_{surr} 를 직접 재 봤습니다.
| \sigma | 0.195 | 0.266 | 0.426 | 1.000 | 1.574 |
|---|---|---|---|---|---|
| g_{surr} | 8.8e−4 | 1.42e−3 | 2.05e−3 | 2.19e−3 | 2.59e−3 |
\sigma 를 아무리 키워도 g_{surr} 가 ≈2.2e−3 근처에서 포화합니다. 그런데 c_2 = 0.005 입니다.
\max_\sigma g_{surr} \approx 2.2\times10^{-3} \;<\; c_2 = 5.0\times10^{-3}
항력이 부력보다 항상 작습니다. 그러니 평형점이 아예 없습니다. \sigma 는 클램프 천장까지 올라갈 수밖에 없었습니다 — 버그가 아니라, 설정이 만든 필연이었습니다.
c_2(수평선)와 g_{surr}(\sigma)(곡선)를 겹쳐 그립니다. 두 선이 만나는 곳이 평형 \sigma 입니다. entropy_coef 를 낮춰 수평선을 끌어내리면 어느 순간 교점이 생깁니다.
가로축 σ · 세로축 log_std가 받는 상승력 · 점 = 실제 계측값 5개 · 가로선 = 엔트로피 보너스 c₂ · 세로 점선 = log_std 클램프 천장(σ=1)
이렇게 해보세요 — ① 기본 상태(clip 1.0, c_2=0.005)에서 교점이 없는 것을 확인합니다. ② entropy_coef 를 0.002 아래로 내리면 교점이 생기고 평형 \sigma 가 나타납니다. ③ 다시 0.005 로 올린 뒤 clip_actions 를 4.0 으로 바꿔 보세요 — 곡선 전체가 올라오면서 \sigma^* \approx 0.37 에서 교점이 생깁니다. 처방이 두 개인 이유가 여기 있습니다.
곡선은 계측 5점을 이은 개형이지 시뮬레이션이 아닙니다. clip 4.0 곡선은 실측 균형점 \sigma^*=0.374 에 맞춰 배율을 역산한 것입니다.
5.5 진짜 원인 — 클리핑은 기울기에 보이지 않는다
왜 g_{surr} 가 포화했을까요? clip_actions = 1.0 때문입니다.
행동은 wrapper 에서 \pm 1 로 잘립니다. 그런데 log-probability 와 엔트로피는 자르기 전의 가우시안에서 계산됩니다. 그래서 \sigma 를 1 너머로 키우면:
- 실제 로봇에 가는 행동: 거의 안 변합니다(어차피 잘리니까) → 보상도 안 변함 → 항력 포화
- 엔트로피 보너스: \log\sigma 에 비례해 계속 지급됩니다 → 부력 그대로
노이즈가 공짜가 된 것입니다. 정책 입장에서는 \sigma 를 키우는 게 순이득이니 천장까지 올립니다.
5.6 처방 — 무엇을 고쳐야 하나
가설이 여러 개였으므로 하나씩 껐다 켜 봤습니다. rough Base, seed 42, 4096 env, 1500 iteration.
| 셀 | 바꾼 것 | \sigma | 천장 비율 | 추종 성능 | 지형 레벨 |
|---|---|---|---|---|---|
| p0 | (기준) | 0.9998 | 99.3% | 0.5177 | 2.69 |
| p1 | entropy_coef 0.001 |
0.266 | 0% | 0.5490 | 2.85 |
| p5 | entropy_coef 0.002 |
0.426 | 0% | 0.5506 | 2.91 |
| p3 | entropy_coef 0 |
0.195 | 0% | 0.5105 ↓ | 2.79 |
| p2 | 클램프 천장 0 → 0.5 | 1.574 | 0%* | 0.4960 | 2.55 |
| p4 | clip_actions 1.0 → 4.0 |
0.374 | 0% | 0.7254 | 5.35 |
두 셀이 특히 중요합니다.
- p3 — 엔트로피 보너스를 아예 없앴더니 오히려 더 나빠졌습니다(0.5105). 탐험은 필요합니다. 즉 “c_2 를 0으로” 는 답이 아니고, 내부 최적값이 존재합니다.
- p2 — 천장을 \sigma \le 1.65 로 올렸더니 \sigma 가 1.574 까지 따라 올라갔습니다. 천장이 원인이 아니라는 것을 실험으로 반증한 셈입니다. 천장을 올리면 거기까지 올라갑니다.
그리고 p4 가 압도적입니다. 원인을 직접 제거하니 추종 성능이 0.52 → 0.73, 지형 커리큘럼이 2.69 → 5.35 로 뛰었습니다.
5.7 완주 확인
1500 iteration 짜리 진단을 4000 iteration 으로 완주시켜 확인했습니다(rough Base).
| 지표 | 이전 | 이후 |
|---|---|---|
| 천장에 붙은 비율 | 76.4% | 0.0% |
| \sigma | 1.0000 | 0.3665 |
Loss/entropy |
17.027 | 4.752 |
| 추종 성능 | 0.5492 | 0.7868 |
속도 오차 error_vel_xy |
0.4234 | 0.2293 (−46%) |
| 성공률 | 0.6348 | 0.9288 |
| 몸통접촉 종료 | 0.0978 | 0.0481 |
| 지형 레벨 | 3.01 | 5.81 |
공짜는 아닙니다 — dof_torques_l2 가 4.7%, action_rate_l2 가 13% 늘었습니다. 더 크게 움직이니 더 힘을 씁니다. 참고로 1차시에서 본 action_rate · smoothness 페널티가 바로 이 비용에 값을 매기는 항입니다.
- 스모크 테스트로 판정하지 마세요. 200 iteration 에서는 \sigma 가 아직 올라가는 중이라 아무 이상이 없어 보입니다. 이 지표는 완주 궤적으로만 보입니다.
log_std클램프[-5, 0]은 이제 NaN 가드일 뿐입니다. 원인을 고친 뒤로 평형이 \sigma^*=0.374 에 잡히므로 클램프에 닿지 않습니다. 그리고 이 클램프는 두 곳에 독립적으로 걸려 있습니다 — runner 의 옵티마이저 post-step 훅, 그리고train.py가GaussianDistribution.update를 통째로 갈아 끼우는 프로세스 전역 몽키패치.- “증상을 누르는 처방”을 조심하세요. 옛 문서의 “±1 클리핑이 붕괴를 막는다”는 설명은 클램프 상한이 2.0(\sigma \le 7.4)이던 시절 이야기였습니다. 그때는 실제로 \sigma 폭주를 눌러 줬지만, 누른 것은 증상이었고 그 대가로 평형 자체를 없애 버렸습니다.
6. 실행 — 학습과 평가
태스크 ID만 바꾸면 세 팔 모두 같은 스크립트로 학습합니다.
# 학습 (헤드리스, 4096 envs) — 보고된 실험은 전부 4000 iteration
python train.py --task=DreamWaQ-BaseDwq-Rough-PPO-v0 --headless \
--num_envs=4096 --seed=42 --max_iterations=4000
# 세 팔 — 태스크 ID만 다르다
# Base DreamWaQ-BaseDwq-Rough-PPO-v0
# Oracle DreamWaQ-OracleDwq-Rough-PPO-v0
# CENet 팔 DreamWaQ-Waq-Official-Rough-PPO-v0
# 빠른 스모크 테스트 (§5.7 참고 — 이걸로 성능을 판정하지 말 것)
python train.py --task=DreamWaQ-BaseDwq-Rough-PPO-v0 --headless \
--num_envs=64 --max_iterations=30
# wandb 로깅 / 이어서 학습
python train.py --task=DreamWaQ-BaseDwq-Rough-PPO-v0 --headless --logger=wandb
python train.py --task=DreamWaQ-BaseDwq-Rough-PPO-v0 --headless \
--resume --load_run=FOLDER_NAME --checkpoint=NUMBER
# 평가 (GUI)
python play.py --task=DreamWaQ-BaseDwq-Rough-PPO-Play-v0 \
--load_run=FOLDER_NAME --checkpoint=model_3999.pt- 환경 수: 4096 (RTX 4080 16GB 안정), 8192는 OOM.
- log_std 클램프
[-5, 0]→ \sigma \in [0.007,\ 1.0]. §5.7 이후로는 NaN 가드 역할만 합니다. - 학습 시간: flat 약 30분, rough 약 2시간 45분 (4096 env / 4000 iteration 기준).
- GPU 하나에 하나씩. 학습 중에 play/평가를 같이 띄우면 OOM 납니다.
7. 손으로 해보기
이 회차는 학습지 4장입니다. exercises/stage3_ppo/ 에 있고, PPO 를 직접 한 번 구현해 본 다음 그것이 rsl_rl 의 어디인지 짚는 순서입니다.
학습지 1 — GAE 어드밴티지 L2
목표: “이 행동이 평균보다 얼마나 좋았나” 를 계산합니다. §1의 그 수식입니다.
\delta_t = r_t + \gamma V(s_{t+1})(1-d_t) - V(s_t), \qquad A_t = \delta_t + \gamma\lambda(1-d_t)A_{t+1}, \qquad \text{returns} = A + V
채울 곳: TODO(ppo-gae) — compute_gae 본문
- 뒤에서 앞으로 훑어야 A_{t+1} 을 이미 알고 있습니다 (
reversed). - t = T-1 에서 V(s_{t+1}) 은 인자로 받은
last_value입니다 — 롤아웃이 에피소드 중간에 잘렸으므로 거기서 부트스트랩합니다. done인 스텝은 다음 상태로 이어지지 않으므로 (1-d_t) 로 두 군데 끊습니다. 한 군데만 끊으면 채점기가 잡습니다.
통과 기준 8개 중 개념을 가르는 둘:
- \lambda = 0 이면 1-스텝 TD 와 정확히 같아야 한다
- \gamma = \lambda = 1 이면 몬테카를로와 정확히 같아야 한다
\lambda 가 무엇을 조절하는지 이해했다면 이 둘이 자동으로 맞습니다. 안 맞으면 \lambda 를 아직 “그냥 0.95인 상수” 로 알고 있는 것입니다.
학습지 2 — PPO 손실 함수 L2
목표: PPO 를 PPO 답게 만드는 clipped surrogate objective 를 만듭니다.
채울 곳: TODO(ppo-losses)
clip 이 아니라 min 이다
clip 만 쓰면 안 됩니다. min(unclipped, clipped) 이어야 합니다.
| 상황 | 어떻게 되나 | 왜 |
|---|---|---|
| A>0, 확률비가 위로 벗어남 | 클립 | 좋은 행동이라도 한 번에 너무 밀지 않는다 |
| A>0, 확률비가 아래로 벗어남 | 클립 안 함 | 되돌릴 여지를 남긴다 |
| A<0, 확률비가 아래로 벗어남 | 클립 | |
| A<0, 확률비가 위로 벗어남 | 클립 안 함 |
min 은 “항상 비관적인 쪽을 택한다” 는 뜻입니다. 위 §1의 인터랙티브 그래프에서 곡선이 한쪽만 평평해지는 것이 이 성질입니다.
통과 기준 10개 중 결정적인 것은 8번 — “A>0 이고 확률비가 아래쪽이면 클리핑되면 안 된다”. clip 으로 잘못 짜면 다른 9개는 통과하고 여기서만 걸립니다.
학습지 3 — 내가 짠 게 여기 이 클래스다 L0 · 읽기만
목표: 쓸 코드는 없습니다. 앞의 둘에서 직접 쓴 것이 rsl_rl 의 어디인지 짚습니다.
| 내가 쓴 것 | rsl_rl |
|---|---|
compute_gae |
PPO.compute_returns |
ppo_losses |
PPO.update 안쪽 |
| (없음) | desired_kl 적응형 학습률 → 학습지 4 |
train() 루프 |
OnPolicyRunner.learn |
이 스크립트는 설명을 하드코딩하지 않습니다. 설치된 rsl_rl 소스를 inspect 로 직접 꺼내 내 코드와 나란히 출력합니다. 그래서 라이브러리 버전이 올라가면 출력도 따라 바뀝니다.
반드시 학습지 1·2를 끝내고 오세요. 직접 써 보지 않고 읽으면 남는 게 없습니다.
여기서 보게 되는 것 하나 — OnPolicyRunner.learn 은 79줄인데 그중 알고리즘은 3줄입니다. 나머지는 로깅과 체크포인트입니다. 그리고 4차시에서 갈아 끼우는 것이 정확히 그 3줄 주변입니다.
학습지 4 — 적응형 학습률 L1
목표: 학습지 3에서 “우리에겐 없다” 로 넘긴 것을 직접 구현합니다. §3.4의 그 규칙입니다.
채울 곳: TODO(ppo-cfg) — adapt_learning_rate() 안의 빈칸 하나. clamp_log_std() 는 주어집니다(읽고 실행만).
통과 기준 14개 중 대부분이 데드존 경계값입니다 — 0.005 / 0.008 / 0.01 / 0.015 / 0.02 에서 각각 무슨 일이 일어나야 하는지. 부등호를 < 로 쓸지 <= 로 쓸지가 여기서 갈립니다.
그리고 kl_mean > 0 가드 — §3.4에서 본 그것입니다. 없으면 정책이 죽어 KL 이 0이 됐을 때 학습률을 천장까지 밀어 올립니다.
이 학습지의 clamp_log_std() 를 실행해 보면 §5의 log_std 폭주를 축소판으로 볼 수 있습니다. §5를 읽고 나서 다시 돌려 보면 클램프가 왜 “해결책” 이 아니라 “증상 억제” 였는지 감이 옵니다.
토이 환경 기준선 — 외워 두세요
학습지 1·2는 1차원 속도 추종 토이 환경에서 채점됩니다. 실제 태스크와 구조가 같습니다 (v \leftrightarrow 몸통 속도, v_{cmd} \leftrightarrow 명령, \exp(-4e^2) \leftrightarrow 추종 보상).
| 정책 | 점수 |
|---|---|
| 완벽한 추종 | 1.0000 |
| 가만히 서 있기 | 0.4426 |
| 무작위 | 0.5333 |
제대로 학습되면 0.44 → 약 0.95 로 40초 안에 올라갑니다.
8. 핵심 정리
- PPO-Clip(\epsilon=0.2)이 확률비를 잘라 안정적으로 갱신하고, GAE(\gamma=0.99,\lambda=0.95)가 어드밴티지를 추정한다.
- 비대칭 actor-critic
[512,256,128]ELU. critic 은 rough 235 / flat 48. - 손실 상수 c_1=1.0, c_2=0.005,
clip_actions4.0 — 이 두 값이 이 회차의 주인공. - 엔트로피 보너스의 기울기는 \sigma 와 무관한 상수 c_2 다. 서로게이트 기울기가 그보다 낮은 값에서 포화하면 평형점이 아예 없고, \sigma 는 클램프까지 올라간다.
- 그 포화의 원인은 클리핑이 log-prob 에 보이지 않는 것이었다.
clip_actions를 4.0 으로 되돌리자 추종 0.55 → 0.79, 지형 레벨 3.0 → 5.8. - 학습 루프는 롤아웃(24스텝) → GAE → 5ep×4mb 갱신의 반복. 세 팔의 PPO 설정은 동일하고, CENet 팔만
class_name한 줄로 runner 를 바꾼다.
actor는 아직 선속도도 지형도 모릅니다. 고유수용성 히스토리만으로 이를 추정하는 CENet(VAE) 의 forward/update를 코드로 읽습니다. 그리고 정면으로 묻습니다 — 속도만 필요하면 회귀 하나면 되는데, 왜 하필 VAE 인가?