본문 바로가기

ACER – 경험 재사용으로 Actor-Critic 강화학습 가속하기 (CartPole 구현 포함)

@eunyoung-study2026. 3. 17. 17:44

1. 이번 글에서 다루는 내용

이번 글에서는 ACER(Actor-Critic with Experience Replay) 알고리즘을
CartPole-v1 환경에 직접 적용해보면서

  • 왜 ACER가 등장했는지
  • 기존 Actor-Critic / A2C / A3C와 무엇이 다른지
  • Replay BufferImportance Sampling이 왜 필요한지

를 코드와 함께 정리해보려고 합니다.


2. 왜 ACER가 필요했을까?

2.1 Actor-Critic / A2C / A3C의 한계

앞에서 봤던 Actor-Critic 계열 알고리즘(A2C, A3C 등)은 공통적으로:

  • on-policy 방식입니다.
    • “지금의 정책”으로 모은 데이터만 가지고
    • 그 정책을 곧바로 업데이트합니다.
  • 그래서:
    • 한 번 사용한 경험은 다시 쓰지 못하고
    • 샘플 효율(sample efficiency)이 낮습니다.

A2C / A3C는 여러 환경을 동시에 돌려서 이 문제를 어느 정도 완화했지만,

  • 여전히 “금방 잊혀지는 경험”
  • Q-learning / DQN처럼 Replay Buffer에 오래 쌓아 두고 재사용하는 구조는 아닙니다.

2.2 Replay Buffer를 쓰면 안 되나?

Q-learning 계열(DQN)은:

  • Replay Buffer에서 과거의 경험을 섞어서 학습하기 때문에
    • 데이터 상관성이 줄어들고,
    • 샘플 효율이 좋습니다.

그렇다면,

“Actor-Critic에도 Replay Buffer를 붙이면 좋지 않을까?”

문제는 Actor-Critic이 정책 gradient 기반(on-policy) 이라는 점입니다.

  • Replay Buffer에 쌓여 있는 데이터는
    • 예전 정책 (\mu(a|s))로 수집된 경험입니다.
  • 현재 정책 (\pi(a|s))와는 다르기 때문에
    • 그대로 쓰면 off-policy bias가 생깁니다.

ACER는 여기서 한 발 더 나아가,

Actor-Critic + Experience Replay + Off-policy 보정(Importance Sampling)
을 결합해서, “안정적인 off-policy Actor-Critic”을 만들려고 한 시도입니다.


3. ACER의 핵심 아이디어

ACER 논문의 전체 수식을 다 구현하기보다는, 과제에서 꼭 짚고 넘어가야 하는 포인트만 정리하면 다음과 같습니다.

  1. Actor-Critic 기반
    • \(\pi(a|s)\)를 학습하는 Actor,
    • \(V(s)\) 또는 \(Q(s,a)\)를 학습하는 Critic 구조
  2. Experience Replay
    • Replay Buffer에 과거 경험을 저장해 두고
    • 나중에 꺼내서도 계속 학습 (샘플 효율 ↑)
  3. Importance Sampling으로 Off-policy 보정
    • Buffer 안의 경험은 옛날 정책 \(\mu(a|s)\)로 모인 것
    • 현재 정책 \(\pi(a|s)\)와 다르기 때문에
      • 중요도 비율 \(w = \frac{\pi(a|s)}{\mu(a|s)}\)를 곱해서 보정
    • 너무 큰 w는 클리핑해서 분산을 줄임
  4. (논문 버전) 신뢰구간/트러스트 리전, Q-리트레이스 등
    • 이 글에서는 학습용 구현을 위해 생략하고,
    • “Replay + Importance Sampling + Actor-Critic” 구조에 집중합니다.

4. CartPole용 간단 ACER 구현 구조

이번에 구현한 코드는 아래 네 부분으로 나뉩니다.

  1. ActorCritic 네트워크
  2. Replay Buffer
  3. On-policy 업데이트 (A2C 스타일)
  4. Off-policy 업데이트 (Replay + Importance Sampling)
  5. 학습 루프 (on/off 섞어서 업데이트)

4.1 ActorCritic 네트워크

class ActorCritic(nn.Module):
    def __init__(self, state_dim=4, action_dim=2, hidden_dim=128):
        super().__init__()
        self.fc1 = nn.Linear(state_dim, hidden_dim)
        self.pi_head = nn.Linear(hidden_dim, action_dim)
        self.v_head = nn.Linear(hidden_dim, 1)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        logits = self.pi_head(x)
        v = self.v_head(x)
        pi = F.softmax(logits, dim=-1)
        return pi, v.squeeze(-1)

    def act(self, s):
        s_t = torch.as_tensor(s, dtype=torch.float32, device=DEVICE)
        pi, v = self.forward(s_t)
        dist = Categorical(pi)
        a = dist.sample()
        return a.item(), pi.detach().cpu().numpy(), v.item()
  • 출력:
    • 정책 \(\pi(a|s)\) – 행동 2개(왼/오른쪽)에 대한 확률
    • 상태 가치 \(V(s)\)
  • act():
    • CartPole 환경에서 실제로 행동을 샘플링할 때 사용
    • 나중에 Replay Buffer에 저장하기 위해 behavior 정책의 확률 μ(a|s) 도 함께 리턴합니다.

4.2 Replay Buffer와 behavior 확률 저장

class ReplayBuffer:
    def __init__(self, capacity):
        self.buffer = deque(maxlen=capacity)

    def push(self, s, a, r, done, s_next, behavior_prob):
        self.buffer.append((s, a, r, done, s_next, behavior_prob))
  • 저장되는 항목:
    • 상태 s, 행동 a, 보상 r, 종료 여부 done, 다음 상태 s_next
    • 그리고 그때 사용했던 정책 확률 \(\mu(a|s)\) (behavior_prob)

behavior_prob이 나중에 Off-policy 업데이트에서
Importance Sampling 비율을 계산할 때 사용됩니다.


5. On-policy 업데이트 (A2C 스타일)

먼저, 에피소드 단위로 한 번씩 on-policy 업데이트를 수행합니다.
즉, A2C/Actor-Critic처럼 “방금 수집한 궤적”만 가지고 한 번 업데이트합니다.

def acer_on_policy_update(model, optimizer, s_traj, a_traj, r_traj, done_traj):
    s_traj = torch.as_tensor(np.array(s_traj), dtype=torch.float32, device=DEVICE)
    a_traj = torch.as_tensor(a_traj, dtype=torch.int64, device=DEVICE)
    r_traj = torch.as_tensor(r_traj, dtype=torch.float32, device=DEVICE)
    done_traj = torch.as_tensor(done_traj, dtype=torch.float32, device=DEVICE)

    with torch.no_grad():
        pi_next, v_next = model(s_traj[-1:])
        bootstrap = 0.0  # 단순화를 위해 마지막 상태는 부트스트랩하지 않음

    # n-step Return 계산
    returns = []
    R = bootstrap
    for r, d in zip(reversed(r_traj), reversed(done_traj)):
        R = r + GAMMA * R * (1.0 - d)
        returns.append(R)
    returns.reverse()
    returns = torch.stack(returns)

    pi, v = model(s_traj)
    dist = Categorical(pi)
    log_pi_a = dist.log_prob(a_traj)

    advantage = returns - v

    value_loss = F.mse_loss(v, returns.detach())
    policy_loss = -(log_pi_a * advantage.detach()).mean()
    entropy_loss = dist.entropy().mean()

    loss = policy_loss + value_loss * 0.5 - ENTROPY_COEF * entropy_loss

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
  • 핵심은 A2C와 거의 동일합니다.
    • n-step Return을 이용해 advantage를 만들고
    • 정책/가치 손실을 모두 최소화합니다.

ACER에서도 on-policy 업데이트는 A2C와 비슷한 형태를 가질 수 있고,
우리는 여기에 추가로 off-policy 업데이트를 더하는 구조로 구현했습니다.


6. Off-policy 업데이트 – Replay + Importance Sampling

이제 Replay Buffer에서 샘플링한 옛날 경험을 이용해 off-policy 업데이트를 수행합니다.

def acer_off_policy_update(model, optimizer, replay_buffer, batch_size=BATCH_SIZE):
    if len(replay_buffer) < batch_size:
        return

    s, a, r, done, s_next, mu = replay_buffer.sample(batch_size)

    s = torch.as_tensor(s, dtype=torch.float32, device=DEVICE)
    a = torch.as_tensor(a, dtype=torch.int64, device=DEVICE)
    r = torch.as_tensor(r, dtype=torch.float32, device=DEVICE)
    done = torch.as_tensor(done, dtype=torch.float32, device=DEVICE)
    s_next = torch.as_tensor(s_next, dtype=torch.float32, device=DEVICE)
    mu = torch.as_tensor(mu, dtype=torch.float32, device=DEVICE)  # behavior prob

    pi, v = model(s)
    pi_next, v_next = model(s_next)

    dist = Categorical(pi)
    log_pi_a = dist.log_prob(a)
    pi_a = torch.exp(log_pi_a)    # π(a|s)

    # 중요도 비율 w = π(a|s) / μ(a|s)
    w = (pi_a / (mu + 1e-8)).detach()
    w_clipped = torch.clamp(w, max=IS_CLIP)

    # 1-step TD target
    td_target = r + GAMMA * v_next * (1.0 - done)
    advantage = td_target.detach() - v

    policy_loss = -(w_clipped * log_pi_a * advantage.detach()).mean()
    value_loss = F.mse_loss(v, td_target.detach())
    entropy = dist.entropy().mean()

    loss = policy_loss + 0.5 * value_loss - ENTROPY_COEF * entropy

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

6.1 왜 Importance Sampling이 필요한가?

Replay Buffer에 있는 데이터는 예전 정책 μ(a|s) 로 모인 데이터입니다.
현재 정책은 \(\pi(a|s)\)이므로, 정책이 바뀌었을 가능성이 큽니다.

  • 그대로 써버리면 “현재 정책”이 아니라 “예전 정책”의 그래디언트를 따라가게 됩니다.
  • 이걸 바로잡기 위해 중요도 비율을 곱해 줍니다.

\[
w = \frac{\pi(a|s)}{\mu(a|s)}
\]

이렇게 하면,

  • μ(a|s)가 작았지만 π(a|s)는 큰 행동 → w가 커져서 더 크게 반영
  • μ(a|s)가 컸지만 π(a|s)는 작은 행동 → w가 작아져서 덜 반영

즉, “지금 정책이 높은 확률을 주는 행동일수록 더 신뢰하고 업데이트”하게 됩니다.

6.2 왜 w를 클리핑하는가?

  • 이론적으로는 w를 그대로 쓰는 것이 unbiased하지만,
  • 실제 학습에서는 w가 너무 커져서 분산이 폭발할 수 있습니다.
  • 그래서 코드에서는:
w_clipped = torch.clamp(w, max=IS_CLIP)

처럼 상한을 둬서, “너무 과한 중요도 비율은 적당히 잘라내고 쓰자” 라는 타협을 합니다.


7. 전체 학습 루프 요약

마지막으로 전체 학습 루프는 다음과 같습니다.

for episode in range(1, MAX_EPISODES + 1):
    s, _ = env.reset()
    done = False
    ep_reward = 0.0

    s_traj, a_traj, r_traj, done_traj = [], [], [], []

    while not done:
        a, pi_probs, _ = model.act(s)
        s_next, r, terminated, truncated, _ = env.step(a)
        done = terminated or truncated

        ep_reward += r

        s_traj.append(s)
        a_traj.append(a)
        r_traj.append(r / 100.0)
        done_traj.append(float(done))

        behavior_prob = pi_probs[a]  # μ(a|s)
        replay_buffer.push(s, a, r / 100.0, float(done), s_next, behavior_prob)

        s = s_next

    # 1) on-policy 업데이트 (에피소드마다 1회)
    acer_on_policy_update(model, optimizer, s_traj, a_traj, r_traj, done_traj)

    # 2) off-policy 업데이트 (Replay에서 여러 번)
    if len(replay_buffer) >= REPLAY_START_SIZE:
        for _ in range(4):
            acer_off_policy_update(model, optimizer, replay_buffer, BATCH_SIZE)

요약하면:
“방금 수집한 최신 궤적”으로 on-policy 업데이트를 하고,
Replay Buffer에서 꺼낸 “과거 궤적”으로 off-policy 업데이트를 여러 번 더 한다.

 

이로 인해:

  • A2C/A3C보다 경험 재사용을 훨씬 많이 하게 되고,
  • 샘플 효율이 좋아지는 방향으로 Actor-Critic을 확장할 수 있습니다.

8. ACER vs A2C / A3C – 정리

  • A2C / A3C
    • Actor-Critic + Advantage
    • on-policy (Replay Buffer를 쓰지 않거나, 제한적으로만 사용)
    • A2C: 동기식, A3C: 비동기식
  • ACER (Actor-Critic with Experience Replay)
    • Actor-Critic + Advantage
    • Experience Replay를 적극적으로 사용 (off-policy)
    • Off-policy로 인한 bias/분산 문제를
      Importance Sampling + 클리핑 + 추가 안정화 기법으로 보정
    • 즉, “Replay Buffer를 써도 망가지지 않는 Actor-Critic”을 목표로 한 알고리즘
더보기

[ 오늘의 마무리 ]

이번 CartPole 예제에서는

  • 논문 ACER 전체를 그대로 구현하기보다는,
  • Actor-Critic + Replay Buffer + Importance Sampling 이라는 핵심 아이디어를 살려
  • 학습용으로 단순화된 ACER 스타일 구현을 만들어 보았습니다.
eunyoung-study
@eunyoung-study :: 은영의 이해 노트

개념을 이해하고, 논문을 풀어보고, 코드로 확인하는 기록 ! 오늘도 파이팅 😉

공감하셨다면 ❤️ 구독도 환영합니다! 🤗

목차