Gymnasium과 Stable-Baselines3
Gymnasium
- Gymnasium: 강화학습 환경을 같은 방식으로 다루기 위한 파이썬 라이브러리
- OpenAI의 gym을 이어받아 Farama 재단이 관리
reset()으로 시작하고step(action)으로 한 단계씩 진행하는 공통 인터페이스 제공
- 기본 제공 환경
- Classic Control: CartPole, MountainCar, Pendulum 등 간단한 물리 문제
- Box2D: LunarLander, BipedalWalker 등 2차원 물리 시뮬레이션
- Toy Text: FrozenLake, Taxi 등 격자 세계
- MuJoCo: 로봇 관절 제어 등 3차원 물리 시뮬레이션
- 환경 인터페이스가 같으므로 같은 학습 코드를 여러 환경에 그대로 적용 가능
Stable-Baselines3
- Stable-Baselines3(SB3): 파이토치(PyTorch)로 구현한 강화학습 알고리즘 라이브러리
- Gymnasium 환경을 그대로 받아 학습
- 검증된 구현을 제공하므로 알고리즘을 직접 구현하지 않고 실험 가능
- 제공 알고리즘
- 가치 기반: DQN
- Actor-Critic 계열: A2C, PPO
- 연속 행동용: DDPG, TD3, SAC
- 모든 알고리즘의 사용법이 같음
model = 알고리즘("정책 종류", 환경): 모델 생성model.learn(total_timesteps=...): 학습model.predict(관찰): 행동 선택model.save(...),알고리즘.load(...): 저장과 불러오기
- 수업 실습에 적합한 이유
pip설치 한 번으로 Colab에서 바로 사용 가능- 일반 파이썬 코드처럼 한 프로세스에서 실행되어 오류를 찾기 쉬움
- 대규모 분산 학습이 필요하면 Ray RLlib 같은 라이브러리를 고려
실습 준비
- Colab에서 SB3와 CartPole 화면 출력에 필요한 패키지를 설치
gymnasium[classic-control]: CartPole 화면을 그리는pygame포함
%pip install -q stable-baselines3 "gymnasium[classic-control]"
import gymnasium as gym # 강화학습 환경 라이브러리
CartPole 환경
- CartPole: 카트를 좌우로 밀어 카트 위의 막대가 쓰러지지 않게 버티는 환경
- 막대가 버틴 매 스텝마다 보상 +1
- 막대가 약 12도 이상 기울거나 카트가 화면 밖으로 나가면 종료
CartPole-v1은 500스텝에 도달하면 중단(truncated) → 최대 수익 500
env = gym.make("CartPole-v1") # 카트 위 막대 세우기 환경 생성
env.observation_space # 관찰 공간
실행 결과
Box([-4.8 -inf -0.41887903 -inf], [4.8 inf 0.41887903 inf], (4,), float32)
- 관찰: 실수 4개로 이뤄진
Box공간- 카트 위치(±4.8), 카트 속도, 막대 각도(±0.42라디안 ≈ ±24도), 막대 각속도
- 속도와 각속도는 범위 제한이 없어
inf로 표시
env.action_space # 행동 공간
실행 결과
Discrete(2)
- 행동:
0은 카트를 왼쪽으로 밀기,1은 오른쪽으로 밀기
무작위 정책
- 학습 전 비교 기준으로, 행동을 무작위로 골라 에피소드 1개를 진행
obs, info = env.reset(seed=0) # 환경 초기화(seed로 시작 상태 고정)
env.action_space.seed(0) # 무작위 행동도 매번 같게 나오도록 고정
steps = 0 # 막대를 버틴 스텝 수
terminated = truncated = False # 종료 여부, 시간 제한에 따른 중단 여부
while not (terminated or truncated): # 에피소드가 끝날 때까지 반복
action = env.action_space.sample() # 무작위로 행동 선택
obs, reward, terminated, truncated, info = env.step(action) # 행동을 환경에 반영
steps += 1 # 버틴 스텝 수 1 증가
steps # 무작위 정책이 버틴 스텝 수
실행 결과
18
- 무작위 정책은 18스텝 만에 막대를 쓰러뜨림
PPO 모델 만들기
from stable_baselines3 import PPO # PPO 알고리즘
model = PPO(
"MlpPolicy", # 다층 퍼셉트론(MLP)으로 정책과 가치 함수를 구성
env, # 학습에 사용할 환경
seed=0, # 결과 재현을 위한 난수 고정
verbose=0, # 학습 중 로그 출력 생략
)
model.policy # 정책 신경망 구조
실행 결과
ActorCriticPolicy(
(features_extractor): FlattenExtractor(
(flatten): Flatten(start_dim=1, end_dim=-1)
)
(pi_features_extractor): FlattenExtractor(
(flatten): Flatten(start_dim=1, end_dim=-1)
)
(vf_features_extractor): FlattenExtractor(
(flatten): Flatten(start_dim=1, end_dim=-1)
)
(mlp_extractor): MlpExtractor(
(policy_net): Sequential(
(0): Linear(in_features=4, out_features=64, bias=True)
(1): Tanh()
(2): Linear(in_features=64, out_features=64, bias=True)
(3): Tanh()
)
(value_net): Sequential(
(0): Linear(in_features=4, out_features=64, bias=True)
(1): Tanh()
(2): Linear(in_features=64, out_features=64, bias=True)
(3): Tanh()
)
)
(action_net): Linear(in_features=64, out_features=2, bias=True)
(value_net): Linear(in_features=64, out_features=1, bias=True)
)
ActorCriticPolicy: 행위자와 비평가를 함께 가진 구조- 행위자:
policy_net→action_net, 관찰 4개를 받아 행동 2개의 점수를 출력 - 비평가:
value_net→value_net, 관찰 4개를 받아 상태의 가치 1개를 출력 - 두 신경망 모두 64개 뉴런의 은닉층 2개를 사용
- 행위자:
단계별 학습과 녹화
- 학습 과정을 눈으로 보기 위해 학습 중간중간 에피소드 1개를 녹화
- Colab에는 모니터가 없으므로
render_mode="rgb_array"로 화면을 이미지 배열로 받아 모음
import cv2 # 프레임에 글자를 쓰기 위한 OpenCV
def record_episode(model, max_steps=500):
"""현재 모델로 에피소드 1개를 실행하고 화면 프레임을 모아 반환"""
env = gym.make("CartPole-v1", render_mode="rgb_array") # 화면을 이미지 배열로 받는 환경
obs, info = env.reset(seed=0) # 매번 같은 시작 상태에서 비교
label = f"steps: {model.num_timesteps:,}" # 지금까지 학습한 스텝 수
frames = [] # 프레임을 모을 리스트
for _ in range(max_steps): # 최대 500스텝까지 실행
action, _ = model.predict(obs, deterministic=True) # 가장 확률이 높은 행동 선택
obs, reward, terminated, truncated, info = env.step(action) # 행동을 환경에 반영
frame = env.render().copy() # 현재 화면을 이미지 배열로 받음
cv2.putText(frame, label, (10, 30), # 왼쪽 위 (10, 30) 위치에
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 0), 2) # 글꼴, 크기, 검은색, 두께
frames.append(frame) # 프레임 저장
if terminated or truncated: # 막대가 쓰러지거나 시간 제한에 도달하면
break # 녹화 종료
env.close() # 녹화용 환경 정리
print(f"{label} -> {len(frames)} steps") # 학습량과 버틴 스텝 수 출력
return frames
deterministic=True: 확률적으로 고르지 않고 확률이 가장 높은 행동만 선택- 학습된 정책의 실력을 일관되게 비교하기 위함
.copy():env.render()가 돌려준 배열을 직접 고치지 않도록 복사본에 글자를 씀
frames = record_episode(model) # 학습 전 모습 녹화
실행 결과
steps: 0 -> 70 steps
for chunk in [5_000, 5_000, 10_000, 20_000]: # 단계별 추가 학습량
model.learn(total_timesteps=chunk, reset_num_timesteps=False) # 이어서 학습
frames += record_episode(model) # 학습 후 모습을 녹화해 이어 붙임
실행 결과
steps: 6,144 -> 163 steps
steps: 12,288 -> 367 steps
steps: 22,528 -> 500 steps
steps: 43,008 -> 500 steps
reset_num_timesteps=False: 학습 스텝 수를 0으로 되돌리지 않고 누적- 학습량이 5,000이 아니라 6,144로 표시되는 이유
- PPO는 기본적으로 2,048스텝(
n_steps)씩 경험을 모은 뒤 학습 - 5,000스텝을 요청하면 2,048 × 3 = 6,144스텝까지 진행
- PPO는 기본적으로 2,048스텝(
- 결과 해석
- 학습 전 70스텝 → 약 6천 스텝 학습 후 163스텝 → 약 1만 2천 스텝 후 367스텝
- 약 2만 2천 스텝부터 최대치인 500스텝을 버팀
동영상 만들기
import imageio # 프레임을 동영상 파일로 저장
imageio.mimsave("training.mp4", frames, fps=30) # 초당 30프레임 mp4로 저장
- 프레임 크기 600×400을 608×400으로 바꾼다는 경고가 나올 수 있음
- 동영상 코덱이 가로세로를 16의 배수로 요구하기 때문으로, 무시해도 됨
- 저장한
training.mp4는 Colab 왼쪽 파일 탐색기에서 내려받기 가능
from IPython.display import Video # 노트북에서 동영상 재생
Video("training.mp4", embed=True) # 동영상을 노트북 안에 넣어 재생
embed=True: 동영상을 노트북 출력에 직접 넣어 Colab에서도 재생 가능- 영상 왼쪽 위의 학습 스텝 수가 늘어날수록 막대를 더 오래 세우는 모습 확인
학습 후 평가
- 영상은 에피소드 1개만 보여 주므로, 여러 에피소드의 평균 보상으로 성능을 확인
from stable_baselines3.common.evaluation import evaluate_policy # 정책 평가 함수
mean_reward, std_reward = evaluate_policy(model, env, n_eval_episodes=10) # 에피소드 10개로 평가
- 환경을
Monitor로 감싸라는 경고가 나올 수 있으나, 이 실습에서는 결과에 영향이 없음
mean_reward # 평균 보상
실행 결과
np.float64(500.0)
std_reward # 보상의 표준편차
실행 결과
np.float64(0.0)
- 에피소드 10개 모두 최대 수익 500을 얻음 → CartPole을 사실상 해결