본문 바로가기

Actor-Critic로 CartPole 학습하기 – REINFORCE에서 한 걸음 더

@eunyoung-study2026. 3. 16. 21:10

1. 이 글에서 다루는 내용

이 글에서는 정책 기반 강화학습의 다음 단계인 Actor-Critic 계열을
CartPole-v1 예제를 중심으로 정리합니다.

  • Actor-Critic의 기본 개념
  • TD Error(Temporal Difference Error)의 의미
  • Q Actor-Critic / Advantage Actor-Critic(A2C) 아이디어
  • CartPole 환경에서의 Actor-Critic 구현 흐름

즉, “REINFORCE 이후, Actor-Critic으로 한 걸음 더 나아가는 내용”을 담고 있습니다.


2. REINFORCE의 한계, 그리고 Actor-Critic이 나오는 이유

REINFORCE는 정책 기반 강화학습의 가장 기본적인 알고리즘입니다.
하지만 실제로 구현해 보면 몇 가지 한계가 보입니다.

  • 에피소드가 끝날 때까지 기다려야 함
    • Return \(G_t\)를 계산하려면 마지막 시점까지 보상을 다 봐야 합니다.
    • 에피소드가 길면 한 번 업데이트하는 데 걸리는 시간이 길어집니다.
  • 분산이 크다
    • \(G_t\)는 “그 이후의 모든 보상”을 다 모으기 때문에,
    • 같은 상태·행동이어도 뒤에 어떤 일이 벌어지느냐에 따라 값이 크게 달라집니다.
    • 업데이트가 많이 흔들리고, 학습이 불안정해질 수 있습니다.

그래서 자연스럽게 이런 질문이 나옵니다.

에피소드 끝까지 기다리지 말고
가다가 바로바로 평가하면서 정책을 고칠 수는 없을까?

이 질문에 대한 현실적인 답이 Actor-Critic 계열 알고리즘입니다.


3. Actor와 Critic – 역할 나누기

노트북에서는 먼저 Actor-Critic을 직관적으로 설명합니다.

3.1 Actor

  • 하는 일: 행동을 선택하는 역할
  • 입력: 현재 상태 \(s\)
  • 출력: 각 행동에 대한 확률 분포 \(\pi(a|s)\)
  • 목표:
    • 좋은 행동의 확률은 점점 높이고,
    • 나쁜 행동의 확률은 점점 낮추는 방향으로 정책을 바꾸는 것

3.2 Critic

  • 하는 일: 지금 상태/행동이 얼마나 괜찮은지 평가
  • 보통 두 가지 중 하나를 학습합니다.
    • 상태 가치 함수 \(V(s)\)
    • 행동 가치 함수 \(Q(s,a)\)
  • Actor 입장에서는:
    • “방금 했던 행동이 생각보다 좋았는지/나빴는지”를 알려주는 심판에 가깝습니다.

정리하면:

  • Actor는 “무엇을 할까?”(행동 선택)
  • Critic은 “방금 한 게 얼마나 괜찮았나?”(평가)

에 집중하는 구조입니다.


4. TD Error – Critic이 주는 피드백 신호

Actor-Critic에서 Critic은 TD Error(Temporal Difference Error) 라는 신호로
“예상 대비 실제 결과”를 Actor에게 알려줍니다.

\[
\delta = r + \gamma V(s') - V(s)
\]

  • \(V(s)\): 현재 상태에 대해 원래 생각하고 있던 가치
  • \(r + \gamma V(s')\): 한 스텝 실제로 해 보니 얻은 보상과 다음 상태 가치를 고려한 새로운 평가

결국,

TD Error = 실제로 관찰한 값 - 원래 예상한 값

4.1 TD Error를 어떻게 해석할까?

  • \(\delta > 0\) (양수)
    • 실제 결과가 예상보다 좋았음
    • “방금 행동이 생각보다 잘했다” → 그 행동의 확률을 키워야 합니다.
  • \(\delta < 0\) (음수)
    • 실제 결과가 예상보다 나빴음
    • “방금 행동은 생각보다 별로였다” → 그 행동의 확률을 줄여야 합니다.

즉, TD Error는 REINFORCE에서의 Return처럼
“이 행동이 얼마나 좋았는지/나빴는지”를 알려주는 시그널이지만,

  • 에피소드 끝까지 기다리지 않고
  • 매 스텝마다 바로바로 계산할 수 있다는 점이 결정적인 차이입니다.

5. Actor-Critic 기본 구조 정리

노트북에서 설명하는 Actor-Critic의 한 사이클은 다음과 같습니다.

현재 상태 s
1. Actor가 정책 π로 행동 a를 선택
2. 환경이 보상 r, 다음 상태 s'를 반환
3. Critic이 TD Error δ = r + γV(s') − V(s)를 계산
4. Actor는 δ를 이용해 방금 행동의 확률을 조정 (좋았으면 ↑, 나빴으면 ↓)
5. Critic도 V(s)를 td_target 쪽으로 업데이트

이 과정을 반복하면서 Actor와 Critic 둘 다 점점 좋아집니다.

  • Actor: 정책 \(\pi(a|s)\) 가 좋아짐
  • Critic: 가치 추정 \(V(s)\) 가 정교해짐

6. Q Actor-Critic / Advantage Actor-Critic 아이디어

노트북 후반부에서는 Actor-Critic을 조금 더 다양한 형태로 확장합니다.

6.1 Q Actor-Critic

  • Critic이 \(V(s)\) 대신 \(Q(s,a)\) 를 학습하는 구조
  • Actor는 Critic이 계산한 \(Q(s,a)\)를 보고:
    • Q가 큰 행동의 선택 확률은 높이고
    • Q가 작은 행동의 확률은 낮추는 방향으로 정책을 업데이트
  • 가치 기반(Q-learning)의 “행동 가치를 직접 보는 시각”
    정책 기반의 “정책 자체를 업데이트하는 방식”을 섞은 형태라고 볼 수 있습니다.

6.2 Advantage Actor-Critic (A2C)

노트북에서는 Advantage Actor-Critic도 간단히 언급합니다.

  • Critic은 상태 가치 \(V(s)\)만 학습
  • Advantage \(A(s,a)\)를 사용해 정책을 업데이트

\[
A(s,a) = Q(s,a) - V(s)
\]

  • 직관:
    • “이 행동이 이 상태에서 평균적인 행동보다 얼마나 더 좋은가?
  • 장점:
    • 단순히 Q만 쓰는 것보다 분산이 줄어들어 업데이트가 더 안정적
    • 실제로 많이 쓰이는 A2C, A3C, PPO 등에서 기본 아이디어로 활용됩니다.

7. CartPole Actor-Critic 구현 – 네트워크 구조

이제 노트북 코드에서 사용하는 CartPole Actor-Critic 구현을 글로 풀어보겠습니다.

7.1 모델 구조

하나의 신경망 안에 Actor와 Critic을 동시에 넣습니다.

class ActorCritic(nn.Module):
    def __init__(self):
        super(ActorCritic, self).__init__()
        self.data = []

        self.fc1 = nn.Linear(4, 256)
        self.fc_pi = nn.Linear(256, 2)   # 정책 π(a|s)
        self.fc_v  = nn.Linear(256, 1)   # 가치 V(s)
        self.optimizer = optim.Adam(self.parameters(), lr=learning_rate)
  • 입력: CartPole 상태 4차원 (위치, 속도, 막대 각도, 각속도)
  • fc1: 공통 은닉층 – 상태에서 유용한 특징을 추출
  • fc_pi: Actor 쪽 출력층 – 행동 2개(왼/오른쪽)에 대한 확률
  • fc_v: Critic 쪽 출력층 – 현재 상태의 가치 \(V(s)\)

두 가지 메서드로 역할을 나눕니다.

    def pi(self, x, softmax_dim=0):
        x = F.relu(self.fc1(x))
        x = self.fc_pi(x)
        prob = F.softmax(x, dim=softmax_dim)
        return prob

    def v(self, x):
        x = F.relu(self.fc1(x))
        v = self.fc_v(x)
        return v
  • pi: Actor – 정책 확률 \(\pi(a|s)\) 계산
  • v: Critic – 상태 가치 \(V(s)\) 계산

8. Rollout과 배치 만들기

노트북에서는 한 번 학습하기 전에 여러 스텝의 데이터를 모아 한 번에 업데이트합니다.

# Hyperparameters
learning_rate = 0.0002
gamma         = 0.98
n_rollout     = 10
  • n_rollout = 10:
    • 한 에피소드 전체가 아니라, 10스텝씩 끊어서 데이터를 모으고 업데이트
    • 이렇게 하면 완전한 에피소드가 아니어도 TD Error로 학습이 가능합니다.

모은 데이터는 (s, a, r, s_prime, done) 튜플 리스트로 저장합니다.

    def put_data(self, transition):
        self.data.append(transition)

학습할 때는 이 리스트를 한 번에 텐서 배치로 바꿉니다.

    def make_batch(self):
        s_lst, a_lst, r_lst, s_prime_lst, done_lst = [], [], [], [], []
        for transition in self.data:
            s, a, r, s_prime, done = transition
            s_lst.append(s)
            a_lst.append([a])
            r_lst.append([r / 100.0])  # 보상 스케일링
            s_prime_lst.append(s_prime)
            done_mask = 0.0 if done else 1.0
            done_lst.append([done_mask])

        # 리스트를 numpy → torch.tensor 로 변환 (배치)
        ...

        self.data = []
        return s_batch, a_batch, r_batch, s_prime_batch, done_batch
  • 보상을 r / 100.0으로 스케일링해서 학습을 조금 더 안정화합니다.
  • done_mask는 종료 상태에서 다음 상태 가치 \(V(s')\) 를 더하지 않기 위한 마스크입니다.

9. Actor-Critic 학습 – TD 타깃과 손실

이제 핵심인 train_net 부분입니다.

9.1 TD 타깃과 TD Error

    def train_net(self):
        s, a, r, s_prime, done = self.make_batch()

        td_target = r + gamma * self.v(s_prime) * done
        delta = td_target - self.v(s)   # TD Error
  • td_target = r + γ V(s') * done_mask
    • 종료 상태면 done_mask = 0 이라서 다음 상태 가치는 더해지지 않습니다.
  • delta = td_target - V(s)
    • Critic 관점: “내가 생각한 V(s)가 실제 경험과 얼마나 달랐나?”

9.2 Actor 손실 – 정책을 어떻게 바꿀까?

        pi = self.pi(s, softmax_dim=1)   # 각 상태에서의 행동 확률
        pi_a = pi.gather(1, a)           # 실제로 선택한 행동의 확률 π(a|s)

        loss = -torch.log(pi_a) * delta.detach() \
               + F.smooth_l1_loss(self.v(s), td_target.detach())
  • pi: 각 상태에서 [왼쪽, 오른쪽] 확률
  • a: 실제로 선택한 행동 인덱스
  • pi_a = pi.gather(1, a):
    • “그때 실제로 선택한 행동의 확률”만 뽑아온 것
  • -log(pi_a) * delta:
    • \(\delta > 0\) (생각보다 결과가 좋았음)
      → 이 행동의 확률이 더 커지도록 업데이트
    • \(\delta < 0\) (생각보다 나빴음)
      → 이 행동의 확률이 줄어들도록 업데이트

여기서 delta.detach()를 쓰는 이유는,
Actor를 업데이트할 때 Critic 쪽 gradient는 끊어 주기 위해서입니다.

9.3 Critic 손실 – V(s)를 더 정확하게

        F.smooth_l1_loss(self.v(s), td_target.detach())
  • SmoothL1Loss는 L1과 L2의 장점을 섞은 손실 함수로,
    • 작은 오차에는 L2처럼 부드럽게,
    • 큰 오차에는 L1처럼 이상치에 덜 민감하게 작동합니다.
  • Critic 입장에서는 V(s)가 td_target 쪽으로 조금씩 이동하도록 학습됩니다.

마지막으로 배치 전체에 대해 평균을 내고 역전파합니다.

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

이 한 번의 train_net() 호출로 Actor와 Critic이 함께 업데이트됩니다.


10. CartPole Actor-Critic 학습 루프

전체 학습 루프는 다음과 같습니다.

def main():
    env = gym.make('CartPole-v1')
    model = ActorCritic()
    print_interval = 20
    score = 0.0

    for n_epi in range(10000):
        done = False
        s, _ = env.reset()

        while not done:
            for t in range(n_rollout):
                prob = model.pi(torch.from_numpy(s).float())
                m = Categorical(prob)
                a = m.sample().item()

                s_prime, r, done, truncated, info = env.step(a)
                model.put_data((s, a, r, s_prime, done))

                s = s_prime
                score += r

                if done:
                    break

            model.train_net()

        if n_epi % print_interval == 0 and n_epi != 0:
            print("# of episode :{}, avg score : {:.1f}".format(
                n_epi, score / print_interval))
            score = 0.0

    env.close()

10.1 흐름 요약

  • 환경 초기화 → 상태 s 받기
  • n_rollout 스텝 동안:
    • 현재 정책 π로부터 행동 a 샘플링
    • 환경에서 s', r, done 받기
    • (s, a, r, s', done)을 버퍼에 저장
    • 상태 업데이트, 점수 누적
    • 종료되면 루프 탈출
  • train_net()으로 방금 모은 rollout 배치에 대해 Actor-Critic 업데이트
  • 일정 에피소드마다 평균 점수 출력

에피소드가 진행될수록 평균 점수가 점점 올라가면서,
에이전트가 CartPole을 더 오래 유지하는 방향으로 정책을 배우게 됩니다.

더보기

[ 오늘의 정리 ] - REINFORCE에서 Actor-Critic으로

  • REINFORCE는 Policy Gradient의 출발점이지만,
    • 에피소드 전체 Return을 써서 업데이트 → 분산이 크고, 느릴 수 있습니다.
  • Actor-Critic은 여기에 평가자(Critic)를 붙여서,
    • TD Error를 이용해 매 스텝마다 정책을 조금씩 고쳐 나가는 구조입니다.
  • Q Actor-Critic, Advantage Actor-Critic(A2C) 같은 변형을 통해
    • Q(s,a) 정보나 Advantage (A(s,a))를 활용해
    • 분산을 줄이고, 업데이트를 더 안정적으로 만들 수 있습니다.
  • 노트북의 CartPole 예제에서는
    • 하나의 네트워크 안에 Actor와 Critic을 같이 두고,
    • rollout → 배치 → TD 타깃/TD Error → 손실 계산 → 역전파
    • 흐름으로 Actor-Critic을 구현하고 있습니다.

한마디로 요약하자면,

REINFORCE가 “정책 기반 강화학습의 기본기”라면, Actor-Critic은 그 위에 Critic을 얹어
더 빠르고 안정적으로 학습하게 만든 실전형 정책 기반 알고리즘이다.
eunyoung-study
@eunyoung-study :: 은영의 이해 노트

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

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

목차