9. 학습을 돌리는 법 — 환경·도구·디버깅
강화학습 디버깅에는 손실 곡선이라는 나침반이 없다. 그래서 순서를 정해 두고 밟는다
이 장에서 처음 나오는 말6개
Gymnasium짐네이지엄- 환경의 표준 인터페이스.
reset()으로 시작하고step()으로 한 걸음 나가는 규약. OpenAI Gym의 후계다. 래퍼Wrapper- 환경을 감싸서 관측·보상·종료를 가공하는 얇은 층. 정규화·프레임 스택이 전부 이것으로 붙는다.
벡터 환경Vectorized Env- 환경 여러 개를 한 객체처럼 다뤄 배치로 step하는 것. 학습 속도를 좌우한다.
롤아웃Rollout- 정책으로 환경을 굴려 경험을 모으는 구간. on-policy 학습의 한 사이클 단위다.
시드Seed- 난수 초기값. 강화학습은 시드만 바꿔도 결과가 갈리므로 반드시 여러 개로 돌린다.
SPSSteps Per Second- 초당 환경 스텝 수. 강화학습 실험의 처리량 지표이자 병목 진단의 출발점이다.
환경 — Gymnasium 규약
섹션 제목: “환경 — Gymnasium 규약”강화학습의 모든 도구가 이 인터페이스를 전제한다. 직접 문제를 정의할 때는 이 규약에 맞춰 클래스를 하나 쓰는 것이 시작이다.
import gymnasium as gymfrom gymnasium import spacesimport numpy as np
class TradingEnv(gym.Env): def __init__(self, data): self.data = data # 행동: 목표 비중 −1(최대 공매도) ~ +1(최대 매수) self.action_space = spaces.Box(low=-1.0, high=1.0, shape=(1,), dtype=np.float32) # 관측: 특징 20개 + 내 포지션 + 남은 현금 self.observation_space = spaces.Box(-np.inf, np.inf, shape=(22,), dtype=np.float32)
def reset(self, seed=None, options=None): super().reset(seed=seed) # 난수 시드 처리 — 빠뜨리면 재현이 안 된다 self.t = 0 self.position = 0.0 return self._obs(), {} # (관측, info)
def step(self, action): ... terminated = self.equity <= self.ruin_level # 문제 정의상 끝 truncated = self.t >= len(self.data) - 1 # 외부 데이터 경계로 잘렸을 뿐 return self._obs(), reward, terminated, truncated, info| 반환값 | 뜻 | 틀리면 |
|---|---|---|
terminated | 문제 정의상 종료 (파산 · 넘어짐) | — |
truncated | 과제 밖의 사유로 잘림 (외부 데이터 경계 · 평가 예산) | 외부 중단을 실패로 배운다 |
info | 학습에 안 쓰는 부가 정보 (진단용) | 보상 항별 값을 여기 담아 로깅한다 |
래퍼 — 정규화는 여기서 붙인다
섹션 제목: “래퍼 — 정규화는 여기서 붙인다”환경 본체를 건드리지 않고 관측·보상을 가공하는 층이다. 6장에서 본 정규화도 이 자리에 붙는다.
env = gym.make("MyEnv-v0")env = gym.wrappers.RecordEpisodeStatistics(env) # 에피소드 리턴/길이 자동 기록env = gym.wrappers.ClipAction(env) # 행동 범위 밖을 잘라 낸다env = gym.wrappers.NormalizeObservation(env) # 러닝 평균·분산으로 관측 정규화env = gym.wrappers.NormalizeReward(env) # 리턴 표준편차로 보상 스케일 조정env = gym.wrappers.TransformObservation(env, lambda o: np.clip(o, -10, 10), env.observation_space)| 래퍼 | 무엇을 막나 |
|---|---|
NormalizeObservation | 스케일 큰 관측 하나가 학습을 지배하는 것 |
NormalizeReward | 가치 손실 폭주 |
ClipAction | 분포에서 뽑힌 극단 행동 |
FrameStackObservation | 부분 관측 (속도 정보 부재) |
TimeLimit | 무한 에피소드 |
벡터 환경 — 처리량이 곧 실험 속도
섹션 제목: “벡터 환경 — 처리량이 곧 실험 속도”on-policy 알고리즘은 배치가 커야 안정적이라, 환경을 여러 개 동시에 굴린다.
envs = gym.make_vec("MyEnv-v0", num_envs=32, vectorization_mode="async")obs, _ = envs.reset(seed=0)obs, rewards, terminations, truncations, infos = envs.step(actions) # 전부 배열로| 모드 | 언제 |
|---|---|
sync | 환경이 가벼울 때 (파이썬 계산). 프로세스 오버헤드가 더 크다 |
async | 환경이 무거울 때 (물리 시뮬 · 데이터 로딩) |
| GPU 네이티브 | Isaac Lab · MJX 등. 환경 자체가 GPU에서 수천 개 병렬 (15장) |
마지막 것이 로봇 학습의 판을 바꿨다. CPU 환경 32개와 GPU 환경 4096개는 같은 알고리즘으로도 결과가 다른 세계다.
도구 — 무엇으로 시작하나
섹션 제목: “도구 — 무엇으로 시작하나”| 도구 | 성격 | 고를 때 |
|---|---|---|
| Stable-Baselines3 2.9 | 검증된 구현, scikit-learn풍 API | 기준선을 빨리 잡을 때. 첫 실험은 여기서 |
| CleanRL | 알고리즘 하나 = 파일 하나 | 알고리즘을 뜯어고칠 때, 공부할 때 |
| RSL-RL / rl_games / skrl | 대규모 GPU 병렬 학습에 최적화 | Isaac Lab 기반 로봇 학습 |
| Ray RLlib | 분산 학습·프로덕션 지향 | 여러 노드로 확장할 때 |
프로젝트 구조
섹션 제목: “프로젝트 구조”디렉터리envs/
- my_env.py Gymnasium 환경 — 상태·행동·보상의 단일 원본
- wrappers.py 정규화·마스킹 등
디렉터리configs/
- ppo_base.yaml 하이퍼파라미터는 코드가 아니라 설정 파일로
- train.py 학습 진입점 (시드·설정을 인자로)
- eval.py 학습과 분리된 평가 — 10장
디렉터리runs/ 로그·체크포인트·정규화 통계
- …
하이퍼파라미터를 코드에 박지 않는 것이 중요하다. 시드 5개 × 설정 3개를 돌리는 것이 기본이라, 설정을 인자로 받지 않으면 실험 관리가 즉시 무너진다.
무엇을 로깅하나
섹션 제목: “무엇을 로깅하나”손실 곡선이 지표가 아니므로, 볼 것을 미리 정해 둔다.
| 지표 | 정상 | 이상하면 |
|---|---|---|
| 에피소드 누적 보상 | 과제에 맞는 개선 추세 | 정체하면 기준선·환경·보상을 함께 점검 |
| 에피소드 길이 | 과제에 맞게 | 너무 짧으면 조기 종료가 학습을 막는다 |
| 보상 항별 기여도 | 의도한 비율 | 한 항이 다 먹고 있으면 가중치 문제 |
| 정책 엔트로피 | 서서히 감소 | 급락하면 탐색 붕괴 (5장) |
| 근사 KL (PPO) | 절대 정상값 없음 (0.01은 흔한 출발 기준) | 평소보다 급증하면 학습률·에폭·클립 점검 |
| 클립 비율 (PPO) | 절대 정상값 없음 (0.1~0.3은 참고 범위) | 지속적으로 높으면 갱신 폭을 점검 |
| 설명 분산 (크리틱) | 1에 가까울수록 예측력이 높음 | 0 이하면 크리틱이 상수 예측보다 낫지 않음 |
| Q값 평균 (off-policy) | 실제 리턴과 비슷 | 훨씬 크면 과대추정 (4장) |
| SPS | 안정 | 떨어지면 버퍼·로깅·환경 병목 |
학습이 안 될 때 — 진단 순서
섹션 제목: “학습이 안 될 때 — 진단 순서”-
랜덤 정책의 성적을 먼저 잰다.
기준선이 없으면 “안 오른다”를 판정할 수 없다. 랜덤 정책과 아무것도 안 하는 정책의 리턴을 둘 다 기록해 둔다. 학습된 정책이 후자를 못 이기면 보상 설계 문제다.
-
환경을 사람 손으로 굴려 본다.
좋은 행동을 손으로 넣었을 때 보상이 실제로 커지는가. 여기서 안 커지면 알고리즘이 배울 수 있는 문제가 아니다.
-
아주 쉬운 버전으로 줄인다.
상태를 최소로, 에피소드를 짧게, 목표를 가깝게. 줄인 문제도 못 풀면 환경·보상 쪽에 버그가 있다.
-
관측을 찍어 본다.
범위가
−10~10을 크게 벗어나는가.NaN이 있는가. 정규화 래퍼가 붙어 있는가. 여기서 잡히는 문제가 제일 많다. -
보상 항별 기여도를 본다.
의도한 비율인가. 한 항이 지배하고 있지 않은가 (8장).
-
검증된 구현체로 바꿔 돌린다.
SB3 기본값으로 되면 내 구현 문제, 안 되면 환경 문제.
-
그다음에야 하이퍼파라미터를 만진다.
순서는 학습률 → 배치·롤아웃 길이 → 엔트로피 계수 → 네트워크 크기.
γ는 문제 정의라 함부로 안 만진다 (2장).
재현성 — 시드와 결정성
섹션 제목: “재현성 — 시드와 결정성”| 해야 할 것 | 이유 |
|---|---|
reset(seed=...) 로 환경 시드 고정 | 환경 난수를 통제 |
| 파이썬·NumPy·PyTorch 시드 고정 | 초기화·샘플링 통제 |
| 시드를 최소 5개 돌린다 | 하나로는 아무 말도 못 한다 (10장) |
| 라이브러리 버전을 고정 | 구현 디테일이 성능을 바꾼다 |
| 정규화 통계를 체크포인트와 함께 저장 | 위 함정 2 |
GPU에서 완전한 결정성은 대개 포기한다 — 비결정적 커널을 끄면 크게 느려진다. 대신 시드를 여러 개 돌려 분포로 보고하는 쪽이 표준이다.
계산 예산 감각
섹션 제목: “계산 예산 감각”| 과제 | 대략적인 스텝 수 | 비고 |
|---|---|---|
| 카트폴류 | ~10만 | 노트북 몇 분 |
| MuJoCo 연속 제어 | 100만~1000만 | GPU 한 장 몇 시간 |
| 사족 보행 (GPU 병렬 시뮬) | 1억~10억 | 환경 4096개면 수십 분 |
| 조작 (픽셀 입력) | 수억~ | 며칠 |
| 트레이딩 (일봉) | 데이터가 부족한 쪽 | 20년 일봉 ≈ 5천 스텝. 스텝 수가 아니라 데이터 양이 한계 |
참고 자료
섹션 제목: “참고 자료”- Gymnasium Env API —
reset·step과 종료 신호의 공식 규약 - Gymnasium: Handling Time Limits — 고정 horizon과 외부 시간 제한의 구분
- Gymnasium Observation Wrappers —
FrameStackObservation등 최신 래퍼 이름 - Stable-Baselines3 RL Tips and Tricks — 환경 점검·평가·정규화 실무 지침
- CleanRL Documentation — 단일 파일 구현과 벤치마크 자료
9장 요약
섹션 제목: “9장 요약”- 환경은 Gymnasium 규약으로 쓴다 — 과제 종료는
terminated, 외부 중단은truncated - 정규화는 환경 본체가 아니라 래퍼로 붙이고, 평가 시 학습 통계를 그대로 사용한다
- 정규화 통계를 체크포인트와 함께 저장하지 않으면 배포 시 성능이 무너진다
- 벡터 환경의 처리량이 곧 실험 속도다. GPU 네이티브 병렬은 다른 세계를 연다
- 첫 실험은 검증된 구현(SB3)의 기본값으로 — 환경 문제와 구현 문제를 가르기 위해서다
- 손실 곡선은 지표가 아니다. 누적 보상 · 엔트로피 · 설명 분산 · 항별 보상을 본다
- 진단은 순서대로 — 기준선 → 손으로 굴려 보기 → 쉬운 버전 → 관측 확인 → 보상 확인 → 검증 구현 → 그다음에야 하이퍼파라미터
- 시드는 최소 5개. GPU 완전 결정성은 포기하고 분포로 보고한다
- 로봇은 계산이 병목이고, 트레이딩은 데이터가 병목이다