SAC 강화 학습 알고리즘에 대한 개념적인 부분과 실제 토이 프로젝트 강화 학습 실습 정리
SAC 개념 정리 및 수학적 원리
1. 최대 엔트로피 강화학습 (Maximum Entropy Reinforcement Learning)
기존의 강화학습은 오직 '기대 보상의 총합'을 최대화하는 것만을 목표로 삼았습니다.
하지만 SAC는 여기에 엔트로피(Entropy, $\mathcal{H}$)라는 개념을 더합니다. 엔트로피는 무작위성, 즉 '행동의 다양성'을 의미합니다. SAC의 궁극적인 목표 함수는 다음과 같이 정의됩니다.
- 의미: "보상을 최대로 받으면서도, 가능한 한 무작위로(다양하게) 행동해라!"
- 장점: 에이전트가 초반에 우연히 찾은 꼼수(Local Optima)에 빠지지 않고 끊임없이 새로운 전략을 탐험하게 만듭니다. 또한 예상치 못한 상황(예: 타이쿤 게임에서 손님이 갑자기 몰리는 경우)에서도 유연하게 대처할 수 있는 '강건함(Robustness)'을 부여합니다.
- $\alpha$ (Temperature Parameter): 이전 설정 파일에 있던 ent_coef입니다. 보상과 엔트로피 중 어느 것을 더 중요하게 여길지 조절하는 비율입니다.
2. 액터-크리틱 구조 (Actor-Critic Architecture)
SAC는 두 개의 인공신경망이 서로 상호작용하며 학습하는 구조를 가집니다.
- Actor (액터, $\pi_\phi$): 현재 상태($s_t$)를 보고 어떤 행동($a_t$)을 할지 결정하는 '정책(Policy)' 네트워크입니다. 서빙을 할지, 요리를 할지 확률 분포를 출력합니다.
- Critic (크리틱, $Q_\theta$): 액터가 한 행동이 얼마나 좋았는지 '가치(Q-value)'를 평가하는 네트워크입니다. 액터에게 "방금 그 서빙은 10점 만점에 8점짜리였어"라고 피드백을 줍니다.
SAC에서 크리틱은 일반적인 Q-value가 아니라, 앞서 말한 엔트로피 보너스가 포함된 Soft Q-value를 학습하여 액터를 지도합니다.
3. Clipped Double Q-Learning (과대평가 방지)
강화학습 에이전트들은 종종 "내가 한 행동이 실제보다 훨씬 뛰어나다"고 착각하는 Q-value 과대평가(Overestimation Bias) 문제에 빠집니다.
SAC는 이를 막기 위해 크리틱(Q 네트워크)을 2개 만듭니다 ($Q_{\theta_1}, Q_{\theta_2}$). 그리고 액터의 행동을 평가할 때, 두 네트워크가 예측한 점수 중 더 낮은 점수(Min)를 채택합니다.
이렇게 비관적으로 평가함으로써 에이전트가 섣불리 자만하여 학습이 망가지는 것을 방지하고 매우 안정적인 훈련을 가능하게 합니다.
4. 재매개변수화 기법 (Reparameterization Trick)
SAC의 액터는 확정적인 하나의 행동을 내뱉는 것이 아니라, "평균이 $\mu$이고 표준편차가 $\sigma$인 정규분포"처럼 확률 분포를 출력합니다. 문제는 이 분포에서 행동을 무작위로 '샘플링(Sampling)'하는 과정은 미분이 불가능해서 인공신경망의 역전파(Backpropagation) 학습이 끊긴다는 점입니다.
이를 해결하기 위해 꼼수를 씁니다.
- 기존의 미분 불가능한 방식: $a \sim \mathcal{N}(\mu, \sigma^2)$ (분포에서 직접 뽑기)
- 재매개변수화 방식: $a = \mu + \sigma \odot \epsilon \quad (\text{단, } \epsilon \sim \mathcal{N}(0, 1))$
네트워크는 $\mu$와 $\sigma$만 출력하고, 외부에서 단순한 노이즈 $\epsilon$을 곱해서 더해주는 식($y = ax + b$ 형태)으로 식을 바꿉니다. 이렇게 하면 샘플링의 무작위성은 유지하면서도 수학적으로 미분이 가능해져서, 크리틱의 피드백이 액터 네트워크 끝까지 온전히 전달될 수 있습니다. (이산 행동을 쓰는 Discrete SAC에서는 Gumbel-Softmax 트릭 등 유사한 원리의 다른 기법이 사용됩니다.)
결론적으로 SAC는 "최대한 다양하게 시도하면서(최대 엔트로피), 과거의 모든 경험을 재활용하고(오프폴리시), 두 명의 깐깐한 평가자(Double Q)를 두어 안정적으로 학습하는 똑똑한 알고리즘"입니다.
진행중인 프로젝트(RL-Tycoon-Agent) 코드로 보는 "SAC"
📁config.json
{
"_comment": "SAC (Soft Actor-Critic) Discrete 전용 설정. sb3-contrib의 DiscreteSAC 사용 또는 SB3 SAC+wrapper.",
"algorithm": "SAC",
"policy": "MlpPolicy",
"training": {
"total_timesteps": 1000000,
"n_envs": 1,
"seed": 42,
"eval_freq": 5000
},
"hyperparameters": {
"learning_rate": 0.0003,
"buffer_size": 500000,
"learning_starts": 20000,
"batch_size": 256,
"gamma": 0.99,
"tau": 0.005,
"train_freq": 1,
"gradient_steps": 4,
"max_grad_norm": 1.0,
"ent_coef": "auto",
"target_entropy": "auto"
},
"network": {
"net_arch": [256, 256],
"activation_fn": "relu"
},
"reward_shaping": {
"take_order": 6.0,
"submit_kitchen": 5.0,
"pickup_food": 5.0,
"serve_food": 5.0,
"pickup_drink": 5.0,
"serve_drink": 5.0,
"customer_payment": 0.0,
"lost_customer": -15.0,
"wrong_table": -1.5,
"trash": -4.0,
"trash_orphan": 1.0,
"orphan_cleared": 0.0,
"blocked_move": -0.2,
"idle_penalty": -1.3,
"time_penalty": -0.02,
"buy_upgrade": 5.0,
"food_unlock": 0.2,
"no_upgrade": -0.02,
"customer_waiting": -0.3,
"waiting_customer_seated": 3.0,
"waiting_customer_left": -8.0,
"win": 200.0,
"game_end": 0.01,
"net_profit_delta": 0.2,
"rating_delta": 10.0,
"final_score_delta": 0.05
},
"game_overrides": {
"target_money": null,
"day_limit": null
}
}
📁trainer.py
"""
SAC Trainer – Soft Actor-Critic for Discrete actions.
SB3의 기본 SAC는 연속 행동 공간만 지원하므로,
이 구현에서는 PyTorch로 Discrete SAC를 직접 구현합니다.
핵심 특징:
- Maximum entropy RL: 탐색과 활용의 자동 균형
- Automatic temperature (alpha) tuning
- Twin Q-networks (Double Q-learning)
- Replay Buffer 사용
"""
import os
from typing import Any
from collections import deque
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.optim import Adam
from algorithms.base import BaseTrainer
from algorithms.common import load_algo_config, make_env, save_run_config, EarlyStopTracker
# ────────────────────────────────────────────────
# Networks
# ────────────────────────────────────────────────
class SoftQNetwork(nn.Module):
"""Twin Q-Network for discrete actions."""
def __init__(self, obs_dim: int, act_dim: int,
hidden: list[int] | None = None):
super().__init__()
hidden = hidden or [256, 256]
layers1, layers2 = [], []
prev = obs_dim
for h in hidden:
layers1 += [nn.Linear(prev, h), nn.ReLU()]
layers2 += [nn.Linear(prev, h), nn.ReLU()]
prev = h
layers1.append(nn.Linear(prev, act_dim))
layers2.append(nn.Linear(prev, act_dim))
self.q1 = nn.Sequential(*layers1)
self.q2 = nn.Sequential(*layers2)
def forward(self, obs):
return self.q1(obs), self.q2(obs)
class PolicyNetwork(nn.Module):
"""Categorical policy for discrete actions."""
def __init__(self, obs_dim: int, act_dim: int,
hidden: list[int] | None = None):
super().__init__()
hidden = hidden or [256, 256]
layers = []
prev = obs_dim
for h in hidden:
layers += [nn.Linear(prev, h), nn.ReLU()]
prev = h
layers.append(nn.Linear(prev, act_dim))
self.net = nn.Sequential(*layers)
def forward(self, obs):
logits = self.net(obs)
probs = F.softmax(logits, dim=-1)
return probs
def get_action(self, obs, deterministic=False):
probs = self.forward(obs)
if deterministic:
return probs.argmax(dim=-1)
dist = torch.distributions.Categorical(probs)
return dist.sample()
# ────────────────────────────────────────────────
# Replay Buffer
# ────────────────────────────────────────────────
class ReplayBuffer:
def __init__(self, capacity: int = 100_000):
self.buffer = deque(maxlen=capacity)
def push(self, obs, action, reward, next_obs, done):
self.buffer.append((obs, action, reward, next_obs, done))
def sample(self, batch_size: int):
idxs = np.random.choice(len(self.buffer), batch_size, replace=False)
batch = [self.buffer[i] for i in idxs]
obs, act, rew, nobs, done = zip(*batch)
return (np.array(obs, dtype=np.float32),
np.array(act, dtype=np.int64),
np.array(rew, dtype=np.float32),
np.array(nobs, dtype=np.float32),
np.array(done, dtype=np.float32))
def __len__(self):
return len(self.buffer)
# ────────────────────────────────────────────────
# SAC Discrete Trainer
# ────────────────────────────────────────────────
class SACTrainer(BaseTrainer):
name = "SAC"
def __init__(self):
self.policy: PolicyNetwork | None = None
self.q_net: SoftQNetwork | None = None
self.q_target: SoftQNetwork | None = None
self.cfg: dict = {}
self.save_path: str = "models/sac"
self._timesteps: int = 200_000
self._obs_dim: int = 0
self._act_dim: int = 0
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
def build(self, cfg: dict | None = None, config_path: str | None = None,
save_path: str | None = None, **overrides) -> None:
days = overrides.pop("days", None)
self.cfg = cfg or load_algo_config("sac", config_path, days=days)
t = self.cfg.get("training", {})
hp = self.cfg.get("hyperparameters", {})
net = self.cfg.get("network", {})
game_ov = self.cfg.get("game_overrides", {})
reward_cfg = self.cfg.get("reward_shaping", {})
self._timesteps = overrides.get("timesteps",
t.get("total_timesteps", 200_000))
seed = overrides.get("seed", t.get("seed", 42))
self.save_path = save_path or self.save_path
os.makedirs(self.save_path, exist_ok=True)
save_run_config(self.save_path, self.cfg)
# 환경 차원
tmp = make_env(0, seed, game_ov, reward_cfg)()
self._obs_dim = tmp.observation_space.shape[0]
self._act_dim = tmp.action_space.n
tmp.close()
hidden = net.get("net_arch", [256, 256])
self.policy = PolicyNetwork(self._obs_dim, self._act_dim, hidden).to(self.device)
self.q_net = SoftQNetwork(self._obs_dim, self._act_dim, hidden).to(self.device)
self.q_target = SoftQNetwork(self._obs_dim, self._act_dim, hidden).to(self.device)
self.q_target.load_state_dict(self.q_net.state_dict())
lr = hp.get("learning_rate", 3e-4)
self._policy_opt = Adam(self.policy.parameters(), lr=lr)
self._q_opt = Adam(self.q_net.parameters(), lr=lr)
# Auto temperature
target_ent = hp.get("target_entropy", "auto")
if target_ent == "auto":
self._target_entropy = -np.log(1.0 / self._act_dim) * 0.45
else:
self._target_entropy = float(target_ent)
self._log_alpha = torch.zeros(1, requires_grad=True, device=self.device)
self._alpha_opt = Adam([self._log_alpha], lr=lr)
self._max_grad_norm = hp.get("max_grad_norm", 1.0)
self._hp = hp
self._game_ov = game_ov
self._reward_cfg = reward_cfg
self._seed = seed
def train(self) -> dict[str, Any]:
assert self.policy is not None, "call build() first"
hp = self._hp
gamma = hp.get("gamma", 0.99)
tau = hp.get("tau", 0.005)
batch_size = hp.get("batch_size", 256)
buffer_size = hp.get("buffer_size", 100_000)
learning_starts = hp.get("learning_starts", 1000)
train_freq = hp.get("train_freq", 1)
gradient_steps = hp.get("gradient_steps", 1)
eval_freq = self.cfg.get("training", {}).get("eval_freq", 5000)
replay = ReplayBuffer(buffer_size)
env = make_env(0, self._seed, self._game_ov, self._reward_cfg)()
eval_env = make_env(0, self._seed + 1000, self._game_ov, self._reward_cfg)()
early_stop = EarlyStopTracker(patience=50, min_delta=1.0, verbose=1)
obs, _ = env.reset()
episode_rewards = []
ep_reward = 0.0
best_eval = float("-inf")
for step in range(1, self._timesteps + 1):
if step < learning_starts:
action = env.action_space.sample()
else:
obs_t = torch.FloatTensor(obs).unsqueeze(0).to(self.device)
action = self.policy.get_action(obs_t).item()
next_obs, reward, terminated, truncated, info = env.step(action)
done = terminated or truncated
replay.push(obs, action, reward, next_obs, float(done))
obs = next_obs
ep_reward += reward
if done:
episode_rewards.append(ep_reward)
if len(episode_rewards) % 10 == 0:
print(f" [SAC (소프트 액터-크리틱)] 스텝(Step) {step}, 에피소드(Episodes) {len(episode_rewards)}, "
f"평균보상(AvgReward, 최근10): {np.mean(episode_rewards[-10:]):.1f}")
obs, _ = env.reset()
ep_reward = 0.0
# Train
if step >= learning_starts and step % train_freq == 0:
for _ in range(gradient_steps):
self._update(replay, batch_size, gamma, tau)
# Eval
if step % eval_freq == 0:
eval_r = self._evaluate(eval_env, n_episodes=5)
print(f" [SAC] 평가(Eval) 스텝 {step}: 평균보상(mean_reward)={eval_r:.1f}")
if eval_r > best_eval:
best_eval = eval_r
self.save(os.path.join(self.save_path, "best_model"))
if not early_stop.check(eval_r):
print(f" [SAC] 조기 종료 (Early stopped), 스텝: {step}")
break
self.save(os.path.join(self.save_path, "final_model"))
env.close()
eval_env.close()
print(f"[✓] SAC (소프트 액터-크리틱) 학습 완료. 모델 → '{self.save_path}/'")
return {"algorithm": "SAC", "timesteps": self._timesteps,
"episodes": len(episode_rewards), "save_path": self.save_path}
def _update(self, replay: ReplayBuffer, batch_size: int,
gamma: float, tau: float):
obs_b, act_b, rew_b, nobs_b, done_b = replay.sample(batch_size)
obs_t = torch.FloatTensor(obs_b).to(self.device)
act_t = torch.LongTensor(act_b).to(self.device)
rew_t = torch.FloatTensor(rew_b).to(self.device)
nobs_t = torch.FloatTensor(nobs_b).to(self.device)
done_t = torch.FloatTensor(done_b).to(self.device)
alpha = self._log_alpha.exp().detach()
# Q target
with torch.no_grad():
next_probs = self.policy(nobs_t)
next_log_probs = torch.log(next_probs + 1e-8)
q1_next, q2_next = self.q_target(nobs_t)
q_next = torch.min(q1_next, q2_next)
v_next = (next_probs * (q_next - alpha * next_log_probs)).sum(dim=-1)
q_target = rew_t + gamma * (1.0 - done_t) * v_next
# Q loss
q1, q2 = self.q_net(obs_t)
q1_a = q1.gather(1, act_t.unsqueeze(-1)).squeeze(-1)
q2_a = q2.gather(1, act_t.unsqueeze(-1)).squeeze(-1)
q_loss = F.mse_loss(q1_a, q_target) + F.mse_loss(q2_a, q_target)
self._q_opt.zero_grad()
q_loss.backward()
nn.utils.clip_grad_norm_(self.q_net.parameters(), self._max_grad_norm)
self._q_opt.step()
# Policy loss
probs = self.policy(obs_t)
log_probs = torch.log(probs + 1e-8)
q1_pi, q2_pi = self.q_net(obs_t)
q_pi = torch.min(q1_pi.detach(), q2_pi.detach())
policy_loss = (probs * (alpha * log_probs - q_pi)).sum(dim=-1).mean()
self._policy_opt.zero_grad()
policy_loss.backward()
nn.utils.clip_grad_norm_(self.policy.parameters(), self._max_grad_norm)
self._policy_opt.step()
# Alpha loss
alpha_loss = -(self._log_alpha *
(probs.detach() *
(log_probs.detach() + self._target_entropy)
).sum(dim=-1)).mean()
self._alpha_opt.zero_grad()
alpha_loss.backward()
self._alpha_opt.step()
# Soft update target
for tp, sp in zip(self.q_target.parameters(), self.q_net.parameters()):
tp.data.copy_(tau * sp.data + (1.0 - tau) * tp.data)
def _evaluate(self, env, n_episodes: int = 5) -> float:
total = 0.0
for _ in range(n_episodes):
obs, _ = env.reset()
done = False
while not done:
obs_t = torch.FloatTensor(obs).unsqueeze(0).to(self.device)
action = self.policy.get_action(obs_t, deterministic=True).item()
obs, r, terminated, truncated, _ = env.step(action)
total += r
done = terminated or truncated
return total / n_episodes
def save(self, path: str) -> None:
torch.save({
"policy": self.policy.state_dict(),
"q_net": self.q_net.state_dict(),
"q_target": self.q_target.state_dict(),
"log_alpha": self._log_alpha.data,
}, path + ".pt")
def load(self, path: str) -> None:
ckpt = torch.load(path + ".pt", map_location=self.device)
if self.policy:
self.policy.load_state_dict(ckpt["policy"])
if self.q_net:
self.q_net.load_state_dict(ckpt["q_net"])
if self.q_target:
self.q_target.load_state_dict(ckpt["q_target"])
self._log_alpha.data = ckpt["log_alpha"]
def predict(self, obs, deterministic: bool = True) -> int:
assert self.policy is not None
obs_t = torch.FloatTensor(obs).unsqueeze(0).to(self.device)
action = self.policy.get_action(obs_t, deterministic=deterministic)
return int(action.item())
1. 하이퍼파라미터 (SAC의 핵심)
- ent_coef: "auto", target_entropy: "auto": SAC 알고리즘의 가장 큰 특징입니다. 에이전트가 한 가지 행동만 반복하는 것을 막고 '탐험(Exploration)'과 '활용(Exploitation)'의 비율을 알아서 조절하도록 합니다. 타이쿤 게임처럼 변수가 많은 환경에서는 고정된 값보다 "auto"로 두는 것이 훨씬 유연하게 대처합니다.
- gamma: 0.99: 미래의 보상을 얼마나 중요하게 여길지 결정하는 할인율입니다. 0.99는 약 100스텝 앞의 미래까지 내다보겠다는 뜻입니다. 손님을 받고 음식을 내어주는 데까지 시간이 걸리므로, 장기적인 시야를 갖도록 높게 설정되었습니다.
- train_freq: 1, gradient_steps: 1: 에이전트가 환경에서 1스텝 행동할 때마다 신경망을 1번씩 업데이트합니다. SAC의 장점인 뛰어난 '샘플 효율성(Sample Efficiency)'을 극대화하여 빠르게 학습하도록 유도한 세팅입니다.
- learning_starts: 500: 학습을 시작하기 전 무작위로 500번 행동해 보면서 buffer_size에 다양한 경험을 초기 적재합니다. 초반에 너무 편향된 데이터로 학습이 시작되는 것을 방지합니다.
2. 신경망 구조 (network)
- net_arch: [256, 256], activation_fn: "relu": 은닉층 2개에 각각 256개의 노드를 가진 표준적인 다층 퍼셉트론(MLP) 구조입니다. 화면의 픽셀(이미지)을 직접 입력받는 것이 아니라, 게임 내의 정보(손님 위치, 음식 상태, 잔고 등)를 숫자 배열(Vector)로 받는 환경에 가장 적합하고 가벼운 구조입니다.
3. 보상 설계 (reward_shaping)
강화학습에서 에이전트가 복잡한 일련의 과정을 수행하게 만드는 Breadcrumbs 전략이 아주 잘 적용되어 있습니다.
- 단계별 긍정 보상 (+5.0 ~ +6.0): take_order -> submit_kitchen -> pickup_food -> serve_food로 이어지는 정상적인 영업 사이클마다 5~6점의 높은 보상을 주어, 에이전트가 "아, 이 순서대로 행동해야 점수를 얻는구나"라고 쉽게 깨닫게 만들었습니다.
- 치명적인 실수에 대한 강한 페널티 (-8.0, -15.0): lost_customer(-15.0)나 waiting_customer_left(-8.0)에 큰 감점을 주어, 무슨 일이 있어도 손님을 놓치는 행동은 피하도록 강력하게 제어하고 있습니다.
- 지연 및 무의미한 행동 방지 (-0.02, -1.3): idle_penalty(-1.3)와 time_penalty(-0.02)를 통해 에이전트가 아무것도 안 하고 가만히 서 있거나 맵을 빙빙 도는 행동을 억제합니다. 빨리빨리 움직이게 만드는 '채찍' 역할입니다.
- 최종 목표 (win: 200.0): 스테이지 클리어나 목표 달성 시 압도적인 보상을 주어, 궁극적인 목적이 게임 승리임을 각인시킵니다.
4. 게임 오버라이드 (game_overrides)
- target_money: null, day_limit: null: 제한 시간이나 목표 금액을 없애서 게임을 '무한 모드'처럼 만들었습니다. 에이전트가 초반에 게임 오버 당해서 학습 흐름이 끊기는 것을 막고, 최대한 오랫동안 생존하며 다양한 서빙 경험을 쌓게 하려는 의도입니다.
요약하자면, 이 설정은 타이쿤 게임의 복잡한 서빙 메커니즘을 단계별 보상으로 친절하게 알려주면서도, SAC 특유의 자동 탐험 조절 능력을 활용해 빠르고 안정적으로 똑똑한 직원을 훈련시키기 위한 교과서적인 세팅입니다.
다른 알고리즘과 다르게 n_envs를 1로 둔 이유
PPO 같은 알고리즘을 사용할 때는 실제로 그렇게 세팅하는 것이 정석이기도 합니다. 하지만 현재 설정이 SAC(Soft Actor-Critic) 알고리즘이기 때문에 n_envs를 1로 둔 합당한 이유들이 있습니다. 핵심적인 이유 3가지를 정리해 봅니다.
1. 리플레이 버퍼(Replay Buffer)의 존재
PPO 같은 알고리즘(On-policy)은 실시간으로 수집한 데이터로만 학습하기 때문에, n_envs=8로 여러 환경에서 다양한 데이터를 한 번에 긁어오는 것이 필수적입니다. 하지만 SAC는 오프폴리시(Off-policy) 알고리즘입니다. 에이전트가 겪은 모든 경험을 10만 개(buffer_size: 100000)짜리 거대한 메모리 통에 차곡차곡 쌓아두고, 거기서 무작위로 256개(batch_size: 256)씩 뽑아서 학습합니다. 1개의 환경에서 순차적으로 데이터를 모아도 버퍼 안에서 섞이기 때문에, 굳이 여러 환경을 동시에 띄워 데이터를 섞을 필요성이 상대적으로 낮습니다.
2. 병목 현상: 데이터 수집 vs. 신경망 학습
SAC는 데이터 수집(게임 플레이)보다 신경망을 업데이트(학습)하는 데 압도적으로 많은 연산량을 차지합니다. n_envs=8로 설정하면 게임 데이터는 8배 빨리 모이겠지만, 그만큼 쏟아지는 데이터를 소화하기 위해 그래픽카드가 신경망 업데이트(gradient_steps)를 더 많이, 더 자주 해야 합니다. 결국 병목 현상이 환경 시뮬레이션 쪽이 아니라 GPU의 역전파(Backpropagation) 연산 쪽에서 발생하므로, 환경 개수를 늘린다고 해서 전체 훈련 시간이 8배로 극적이게 짧아지지는 않습니다.
3. 커스텀 환경의 병렬 처리 안정성
타이쿤 게임처럼 복잡한 룰, 객체(손님, 음식 등), 상태 관리가 들어간 커스텀 환경은 파이썬의 멀티프로세싱(SubprocVecEnv)으로 8개를 동시에 띄울 때 예기치 못한 메모리 누수나 충돌이 발생하기 쉽습니다. 따라서 에이전트의 논리적 오류를 잡고 학습이 정상적으로 이루어지는지 확인하는 단계에서는 가장 안정적인 n_envs=1로 두고 검증하는 것이 일반적입니다.
물론, 지금 구성하신 타이쿤 환경이 가볍고 멀티프로세싱에 안정적이라면 GPU의 성능을 믿고 n_envs를 4나 8로 올려서 실험해 보는 것도 아주 좋은 시도입니다.
'2. 자료구조와 알고리즘 > 2-2 강화학습 알고리즘' 카테고리의 다른 글
| [강화 학습] ACER(Actor-Critic with Experience Replay) 개념 정리 (0) | 2026.03.17 |
|---|---|
| [개인 공부] 강화 학습 "알고리즘"에 대한 공부(DQN, DDPG, PPO, GA, A3C / A2C, SAC, MARL, Model-Based RL) (0) | 2026.03.05 |
