수민 '-'

플오그래밍

제가 작성하는 모든 글은 절대 상업적인 이용이 아니며, 그저 개인적인 공부 용도로만 사용하는 것임을 밝힙니다.

Policy 기반 에이전트 - REINFORCE, Actor-Critic, TD Actor-Critic

정책을 직접 학습하는 강화학습과 Policy Gradient Theorem

강화학습에는 가치 기반(Value-Based)정책 기반(Policy-Based) 이 있다. 가치 기반은 Q값을 구한 뒤 가장 좋은 행동을 고르고, 정책 기반은 행동을 선택하는 규칙(정책) 자체를 직접 학습한다. “정책을 더 좋은 방향으로 어떻게 업데이트할지”를 수학적으로 설명하는 것이 Policy Gradient Theorem 이고, 이를 구현한 대표 알고리즘이 REINFORCEActor-Critic 계열이다. 이 글에서는 정리 내용과 CartPole-v1 예제 코드까지 정리한다.

(Policy 기반 에이전트 - 류지 프로젝트를 바탕으로 재구성했다.)


1. Policy Gradient Theorem

1-1. 왜 필요한가

강화학습 접근은 크게 두 가지다.

  • 가치 기반: 행동의 가치(Q값)를 계산해서 가장 큰 행동을 고르는 방식 (예: Q-learning, DQN)
  • 정책 기반: 행동 가치를 먼저 구하지 않고, 행동을 선택하는 규칙(정책) 을 직접 학습하는 방식 (예: REINFORCE, Policy Gradient, PPO)

정책 기반에서는 다음 질문이 생긴다.

정책을 더 좋은 방향으로 바꾸려면
정확히 어떻게 파라미터를 업데이트해야 할까?

이에 대한 답을 주는 것이 Policy Gradient Theorem 이다.

1-2. 핵심 수식

정책 파라미터 θ에 대한 기대 보상 J(θ)의 그래디언트는 다음 형태로 쓸 수 있다.

∇θ J(θ) ∝ E[ ∇θ log πθ(a|s) · Q^π(s,a) ]
  • ∇θ J(θ): 정책을 “어느 방향으로” 수정하면 보상이 커지는지를 나타내는 기울기.
  • log πθ(a|s): 현재 상태에서 실제로 선택한 행동의 로그 확률. 그 행동의 확률을 키우거나 줄이기 위한 도구.
  • ∇θ log πθ(a|s): θ를 조금 바꿨을 때 그 행동의 선택 확률이 얼마나 변하는지(정책의 민감도).
  • Q^π(s,a): 상태 s에서 행동 a를 했을 때 앞으로 받을 것으로 기대되는 보상.

즉, “선택한 행동의 로그 확률”에 “그 행동의 가치(Q 또는 Return)”를 곱한 방향으로 θ를 조정하면, 높은 보상을 낸 행동의 확률은 올라가고 낮은 보상을 낸 행동의 확률은 내려간다. 환경 모델 없이 경험만으로 정책을 학습할 수 있으며, REINFORCE·Actor-Critic 등 정책 기반 알고리즘의 이론적 바탕이 된다.

1-3. 수식에 로그가 들어가는 이유

수학적으로 미분을 쉽게 하기 위해서다. 확률의 곱을 다룰 때 로그를 취하면 곱이 덧셈으로 바뀌어 계산이 단순해진다.


2. REINFORCE 알고리즘

REINFORCE는 에피소드가 끝난 뒤, 그 에피소드에서 수집한 (상태, 행동, 보상)을 이용해 정책을 한 번에 업데이트하는 Monte Carlo 형식의 정책 기반 알고리즘이다.

2-1. 동작 과정

  1. 에이전트가 정책 πθ에 따라 행동한다.
  2. 에피소드가 끝날 때까지 환경과 상호작용한다.
  3. 각 시점 t에서 Return G_t(그 시점 이후 보상의 할인 합)를 계산한다.
  4. G_t를 이용해 정책 파라미터를 업데이트한다.

2-2. Return

Return G_t는 “그 시점 이후에 얻은 모든 보상의 할인 합”이다.

G_t = r_t + γ·r_{t+1} + γ²·r_{t+2} + …

에피소드가 끝난 뒤 역순으로 누적하면 계산하기 편하다: R ← r + γ·R.

2-3. 업데이트 수식

θ ← θ + α · ∇θ log πθ(a_t|s_t) · G_t
  • 실제로 선택한 행동 a_t의 로그 확률에 그 이후의 총 보상(Return) G_t를 곱해 θ를 갱신한다.
  • 보상이 큰 행동은 확률을 높이고, 보상이 작은 행동은 확률을 낮추는 방향이다.

2-4. 의사코드

정책 파라미터 θ 초기화
반복:
    에피소드 실행 → (s0,a0,r0), (s1,a1,r1), ... 수집
    각 시간 t에 대해:
        Return Gt 계산
        θ ← θ + α * ∇θ log πθ(at|st) * Gt

2-5. CartPole-v1에서의 REINFORCE 구현

  • 신경망이 행동별 확률을 출력하고, 그 확률로 행동을 샘플링한다.
  • 에피소드 동안 (보상, 선택한 행동의 확률)을 저장해 두었다가, 끝난 뒤 Return을 역순으로 계산하고 한 번에 정책을 업데이트한다.
import gymnasium as gym
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.distributions import Categorical

learning_rate = 0.0002
gamma = 0.98

class Policy(nn.Module):
    def __init__(self):
        super(Policy, self).__init__()
        self.data = []
        self.fc1 = nn.Linear(4, 128)
        self.fc2 = nn.Linear(128, 2)
        self.optimizer = optim.Adam(self.parameters(), lr=learning_rate)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = F.softmax(self.fc2(x), dim=0)
        return x

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

    def train_net(self):
        R = 0
        self.optimizer.zero_grad()
        for r, prob in self.data[::-1]:
            R = r + gamma * R
            loss = -torch.log(prob) * R
            loss.backward()
        self.optimizer.step()
        self.data = []

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

    for n_epi in range(10000):
        s, _ = env.reset()
        done = False
        while not done:
            prob = pi(torch.from_numpy(s).float())
            m = Categorical(prob)
            a = m.sample().item()
            s_prime, r, done, truncated, info = env.step(a)
            pi.put_data((r, prob[a]))
            s = s_prime
            score += r
            if done or truncated:
                break
        pi.train_net()
        if n_epi % print_interval == 0 and n_epi != 0:
            print("n_episode: {}, avg score: {:.1f}".format(n_epi, score / print_interval))
            score = 0.0
    env.close()

if __name__ == '__main__':
    main()
  • prob[a]: 실제로 선택한 행동의 확률. Return R이 크면 그 행동의 로그 확률을 키우는 쪽으로 학습한다.

3. Actor-Critic 개요

Actor-Critic정책(Actor)가치 평가(Critic) 를 함께 쓰는 구조다.

  • Actor: “무엇을 할까?” — 상태를 보고 행동을 선택하는 정책 π(a|s) 학습.
  • Critic: “방금 한 선택이 얼마나 좋았는가?” — 가치 함수 V(s) 또는 Q(s,a) 학습.

즉, 정책 기반의 유연함과 가치 기반의 평가를 같이 써서, Actor는 Critic의 평가를 받아 정책을 고치고, Critic은 자신의 가치 추정을 더 정확하게 맞춘다.

3-1. 전체 흐름

1. 상태 s 관찰
2. Actor가 행동 a 선택
3. 환경이 보상 r, 다음 상태 s' 반환
4. Critic이 평가(TD Error 등) 계산
5. Actor는 그 평가로 정책 수정, Critic은 가치 추정 수정

3-2. Critic이 필요한 이유

정책 기반에서는 “좋은 행동의 확률을 높이고 나쁜 행동의 확률을 낮추라”고 하는데, 그 행동이 좋은지 나쁜지를 어떻게 알까?

  • REINFORCE는 에피소드가 끝난 뒤 Return으로 판단한다. 에피소드가 길면 느리고, Return 분산이 커서 학습이 불안정할 수 있다.
  • Critic은 현재 상태·행동의 가치를 추정해 Actor에게 “방금 행동은 예상보다 좋았다/나빴다” 같은 즉각적인 신호를 준다. 그래서 매 스텝 또는 몇 스텝마다 업데이트할 수 있고, 더 안정적인 학습이 가능해진다.

3-3. TD Error

Critic이 상태 가치 V(s)를 학습할 때 많이 쓰는 것이 TD Error다.

δ = r + γ·V(s') − V(s)
  • V(s): 현재까지 예상하던 “현재 상태의 가치”.
  • r + γ·V(s'): 방금 한 스텝을 경험해 본 “실제로 얻은 값”에 대한 추정.

즉, 실제로 관찰한 값 − 원래 예상한 값이다.

  • δ > 0: 예상보다 좋았다 → 그 행동의 확률을 높이는 방향으로 Actor 업데이트.
  • δ < 0: 예상보다 나빴다 → 그 행동의 확률을 낮추는 방향으로 Actor 업데이트.

3-4. Actor·Critic 학습 요약

  • Actor: state → neural network → action probabilities. Critic이 “방금 행동이 좋았다”고 하면 해당 행동 확률을 키운다.
  • Critic: state → neural network → V(s). 실제 경험 r, s'로 TD 타깃 r + γ·V(s')를 만들고, V(s)를 그 타깃에 가깝게 학습한다.

3-5. REINFORCE vs Actor-Critic

구분 REINFORCE Actor-Critic
시점 에피소드 끝까지 기다린 뒤 업데이트 매 스텝(또는 몇 스텝)마다 업데이트 가능
신호 Return G_t TD Error 등 Critic의 평가
분산 Return 사용으로 분산이 클 수 있음 Critic으로 분산 감소 가능
학습 속도 상대적으로 느림 상대적으로 빠르고 안정적일 수 있음

3-6. Actor-Critic의 장점

  1. 안정성: Critic이 즉각적인 평가를 해주어 REINFORCE보다 안정적인 경우가 많다.
  2. 연속 행동: 조향, 로봇 제어처럼 연속 행동 공간에 적합한 변형이 많다.
  3. 실제 사용: A2C, A3C, PPO, DDPG, SAC 등 현대 강화학습 알고리즘 상당수가 Actor-Critic 계열이다.

4. Q Actor-Critic / Advantage Actor-Critic (A2C) / TD Actor-Critic

  • Q Actor-Critic: Critic이 Q(s,a)를 학습하고, Actor는 그 Q값을 이용해 “Q가 큰 행동”의 확률을 높인다.
  • Advantage Actor-Critic (A2C): Advantage A(s,a) = Q(s,a) − V(s)를 사용해 정책을 업데이트한다. “평균보다 얼마나 나은지”만 반영해 분산을 줄인다. Critic은 V(s)를 학습해 Advantage 계산에 쓴다.
  • TD Actor-Critic: Critic이 V(s)TD 학습으로 업데이트하고, TD Error δ = r + γ·V(s') − V(s)를 Actor의 정책 그래디언트 신호로 쓴다. 에피소드 끝을 기다리지 않고 매 스텝(또는 n-step)마다 업데이트할 수 있다.

5. TD Actor-Critic 구현 (CartPole-v1)

아래는 TD Actor-Critic 형태로, CartPole-v1에서 Actor(정책)와 Critic(V(s))을 같이 학습하는 예제다.

  • Actor: 상태 4차원 → FC 256 → 정책 출력 2 (왼쪽/오른쪽 확률).
  • Critic: 같은 공유 레이어 후 V(s) 1개 출력.
  • n_rollout 스텝만큼 (s, a, r, s', done)을 모은 뒤, TD 타깃 r + γ·V(s')·done_mask와 TD Error δ = td_target − V(s)를 계산하고, Actor는 −log π(a|s)·δ, Critic은 V(s)를 td_target에 맞추는 손실로 학습한다.
import gymnasium as gym
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import numpy as np
from torch.distributions import Categorical

learning_rate = 0.0002
gamma = 0.98
n_rollout = 10

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)
        self.fc_v = nn.Linear(256, 1)
        self.optimizer = optim.Adam(self.parameters(), lr=learning_rate)

    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

    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])
        s_batch = torch.tensor(np.array(s_lst), dtype=torch.float)
        a_batch = torch.tensor(np.array(a_lst), dtype=torch.long)
        r_batch = torch.tensor(np.array(r_lst), dtype=torch.float)
        s_prime_batch = torch.tensor(np.array(s_prime_lst), dtype=torch.float)
        done_batch = torch.tensor(np.array(done_lst), dtype=torch.float)
        self.data = []
        return s_batch, a_batch, r_batch, s_prime_batch, done_batch

    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)
        pi = self.pi(s, softmax_dim=1)
        pi_a = pi.gather(1, a)
        loss = -torch.log(pi_a) * delta.detach() + F.smooth_l1_loss(self.v(s), td_target.detach())
        self.optimizer.zero_grad()
        loss.mean().backward()
        self.optimizer.step()

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 or truncated:
                    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()

if __name__ == '__main__':
    main()
  • delta.detach(): Actor 쪽에서는 Critic이 준 “좋다/나쁘다” 신호만 쓰고, Critic 그래디언트는 Actor 손실을 타고 역전파되지 않게 한다.
  • done: 종료 시 다음 상태 가치를 0으로 두기 위한 마스크다.

6. 정리

주제 핵심
Policy Gradient Theorem ∇θ J ∝ E[ ∇θ log πθ(a
REINFORCE 에피소드 끝까지 수집 후 Return으로 θ ← θ + α·∇θ log πθ(a_t
Actor-Critic Actor는 정책, Critic은 V(s) 또는 Q(s,a). Critic의 평가(TD Error 등)로 매 스텝에 가깝게 업데이트 가능.
TD Error δ = r + γ·V(s') − V(s). 양수면 그 행동 확률을 높이고, 음수면 낮춘다.
Q / A2C / TD Actor-Critic Critic이 Q 학습, Advantage A = Q−V 사용, 또는 V(s)+TD 학습 등 변형.

마치며

정책 기반 강화학습에서는 Policy Gradient Theorem이 “정책을 어떻게 업데이트할지”의 기준을 준다. REINFORCE는 이 정리를 Return을 써서 구현한 기본 형태이고, Actor-Critic은 Critic으로 즉각적인 평가(TD Error 등)를 넣어 더 빠르고 안정적으로 학습한다. A2C, PPO 등 실전 알고리즘은 대부분 이 Actor-Critic 구조를 바탕으로 하므로, REINFORCE와 TD Actor-Critic 코드를 비교해 보면 정책 기반·가치 기반 결합의 차이가 잘 드러난다.