Policy Gradient 완벽 가이드: REINFORCE부터 PPO까지 수학 원리와 PyTorch 구현

Updated Feb 6, 2026

Policy Gradient란 무엇인가?

Policy Gradient는 강화학습에서 정책(policy)을 직접 최적화하는 방법론입니다. Q-learning 같은 Value-based 방법과 달리, 정책 πθ(as)\pi_\theta(a|s)를 파라미터 θ\theta로 표현하고 기댓값 보상을 최대화하는 방향으로 그래디언트를 계산합니다.

핵심 아이디어: 좋은 행동의 확률은 높이고, 나쁜 행동의 확률은 낮춘다.

목적 함수는 다음과 같습니다:

J(θ)=E<em>τπ</em>θ[R(τ)]J(\theta) = \mathbb{E}<em>{\tau \sim \pi</em>\theta}[R(\tau)]

여기서 τ\tau는 궤적(trajectory), R(τ)R(\tau)는 누적 보상입니다. Policy Gradient 정리에 따르면:

θJ(θ)=E<em>τ[</em>t=0Tθlogπθ(atst)Rt]\nabla_\theta J(\theta) = \mathbb{E}<em>{\tau}\left[\sum</em>{t=0}^T \nabla_\theta \log \pi_\theta(a_t|s_t) \cdot R_t\right]

  • θlogπθ(atst)\nabla_\theta \log \pi_\theta(a_t|s_t): 행동 확률의 로그 그래디언트 (“어떤 방향으로 정책을 바꿀지”)
  • RtR_t: 시점 tt부터의 누적 보상 (“얼마나 좋았는지”)

REINFORCE: 가장 기본적인 Policy Gradient

알고리즘 원리

REINFORCE는 Monte Carlo 방식으로 전체 에피소드를 수집한 뒤 그래디언트를 추정합니다.

import torch
import torch.nn as nn
import torch.optim as optim

class PolicyNetwork(nn.Module):
    def __init__(self, state_dim, action_dim):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(state_dim, 128),
            nn.ReLU(),
            nn.Linear(128, action_dim),
            nn.Softmax(dim=-1)
        )

    def forward(self, state):
        return self.net(state)

def reinforce(env, policy, optimizer, gamma=0.99, episodes=1000):
    for episode in range(episodes):
        states, actions, rewards = [], [], []
        state = env.reset()
        done = False

        # 에피소드 수집
        while not done:
            state_tensor = torch.FloatTensor(state)
            probs = policy(state_tensor)
            action = torch.multinomial(probs, 1).item()

            next_state, reward, done, _ = env.step(action)
            states.append(state_tensor)
            actions.append(action)
            rewards.append(reward)
            state = next_state

        # Return 계산 (역방향)
        returns = []
        G = 0
        for r in reversed(rewards):
            G = r + gamma * G
            returns.insert(0, G)

        returns = torch.FloatTensor(returns)
        returns = (returns - returns.mean()) / (returns.std() + 1e-8)  # 정규화

        # Policy Gradient 업데이트
        loss = 0
        for state, action, G in zip(states, actions, returns):
            probs = policy(state)
            log_prob = torch.log(probs[action])
            loss += -log_prob * G  # 음수: gradient ascent를 descent로 변환

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

REINFORCE의 문제점

문제 설명
높은 분산 Monte Carlo 샘플링으로 인해 그래디언트 추정치의 분산이 매우 큼
샘플 비효율 에피소드가 끝나야 학습 가능 (장기 태스크에서 치명적)
불안정한 학습 분산이 크면 학습이 요동치고 수렴 느림

A2C: Advantage를 이용한 분산 감소

Baseline과 Advantage 함수

분산을 줄이기 위해 baseline b(st)b(s_t)를 도입합니다. 보통 Value 함수 V(st)V(s_t)를 사용:

θJ(θ)=E[θlogπθ(atst)(RtV(st))]\nabla_\theta J(\theta) = \mathbb{E}\left[\nabla_\theta \log \pi_\theta(a_t|s_t) \cdot (R_t – V(s_t))\right]

여기서 At=RtV(st)A_t = R_t – V(s_t)Advantage라 하며, “이 행동이 평균보다 얼마나 좋은가”를 나타냅니다.

  • A_t &gt; 0: 평균보다 좋음 → 확률 증가
  • A_t &lt; 0: 평균보다 나쁨 → 확률 감소

Actor-Critic 구조

A2C는 두 개의 네트워크를 동시에 학습합니다:

  1. Actor πθ(as)\pi_\theta(a|s): 정책 (행동 선택)
  2. Critic Vϕ(s)V_\phi(s): 가치 함수 (상태 평가)
class ActorCritic(nn.Module):
    def __init__(self, state_dim, action_dim):
        super().__init__()
        self.shared = nn.Sequential(
            nn.Linear(state_dim, 128),
            nn.ReLU()
        )
        self.actor = nn.Linear(128, action_dim)
        self.critic = nn.Linear(128, 1)

    def forward(self, state):
        features = self.shared(state)
        action_probs = torch.softmax(self.actor(features), dim=-1)
        value = self.critic(features)
        return action_probs, value

def a2c_update(model, optimizer, states, actions, rewards, next_states, dones, gamma=0.99):
    states = torch.FloatTensor(states)
    actions = torch.LongTensor(actions)
    rewards = torch.FloatTensor(rewards)
    next_states = torch.FloatTensor(next_states)
    dones = torch.FloatTensor(dones)

    # Current policy and value
    probs, values = model(states)
    _, next_values = model(next_states)

    # TD Target과 Advantage 계산
    td_target = rewards + gamma * next_values.squeeze() * (1 - dones)
    advantage = td_target - values.squeeze()

    # Actor loss (Policy Gradient with Advantage)
    log_probs = torch.log(probs.gather(1, actions.unsqueeze(1)).squeeze())
    actor_loss = -(log_probs * advantage.detach()).mean()

    # Critic loss (MSE)
    critic_loss = advantage.pow(2).mean()

    # Entropy bonus (exploration)
    entropy = -(probs * torch.log(probs + 1e-8)).sum(dim=1).mean()

    loss = actor_loss + 0.5 * critic_loss - 0.01 * entropy

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

A2C의 개선점

  • 분산 감소: Baseline 사용으로 그래디언트 추정치가 안정적
  • 온라인 학습: 매 스텝 업데이트 가능 (에피소드 종료 불필요)
  • 병렬화: 여러 환경을 동시에 실행하여 샘플 효율 향상

PPO: 안전한 정책 업데이트

Clipped Surrogate Objective

PPO는 정책이 급격히 변하는 것을 방지합니다. importance sampling ratio를 클리핑:

rt(θ)=πθ(atst)πθold(atst)r_t(\theta) = \frac{\pi_\theta(a_t|s_t)}{\pi_{\theta_{old}}(a_t|s_t)}

PPO 목적 함수:

LCLIP(θ)=Et[min(rt(θ)At,clip(rt(θ),1ϵ,1+ϵ)At)]L^{CLIP}(\theta) = \mathbb{E}_t\left[\min\left(r_t(\theta) A_t, \text{clip}(r_t(\theta), 1-\epsilon, 1+\epsilon) A_t\right)\right]

  • rtr_t: 새 정책과 옛 정책의 확률 비율
  • ϵ\epsilon: 클리핑 범위 (보통 0.1~0.2)
  • clip(rt,1ϵ,1+ϵ)\text{clip}(r_t, 1-\epsilon, 1+\epsilon): rtr_t[0.8,1.2][0.8, 1.2] 범위로 제한

왜 클리핑?: rtr_t가 너무 크면 정책이 급변하여 학습이 불안정해집니다. 클리핑으로 보수적인 업데이트를 강제합니다.

PyTorch 구현

def ppo_update(model, optimizer, memory, epochs=4, clip_epsilon=0.2, gamma=0.99, gae_lambda=0.95):
    states, actions, old_log_probs, rewards, next_states, dones = memory

    states = torch.FloatTensor(states)
    actions = torch.LongTensor(actions)
    old_log_probs = torch.FloatTensor(old_log_probs)

    # GAE (Generalized Advantage Estimation) 계산
    with torch.no_grad():
        _, values = model(states)
        _, next_values = model(next_states)

        advantages = []
        gae = 0
        for t in reversed(range(len(rewards))):
            delta = rewards[t] + gamma * next_values[t] * (1 - dones[t]) - values[t]
            gae = delta + gamma * gae_lambda * (1 - dones[t]) * gae
            advantages.insert(0, gae)

        advantages = torch.FloatTensor(advantages)
        returns = advantages + values.squeeze()
        advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)

    # Multiple epochs of SGD
    for _ in range(epochs):
        probs, values = model(states)
        log_probs = torch.log(probs.gather(1, actions.unsqueeze(1)).squeeze())

        # Ratio와 Clipped Objective
        ratio = torch.exp(log_probs - old_log_probs)
        surr1 = ratio * advantages
        surr2 = torch.clamp(ratio, 1 - clip_epsilon, 1 + clip_epsilon) * advantages
        actor_loss = -torch.min(surr1, surr2).mean()

        # Value loss
        critic_loss = (returns - values.squeeze()).pow(2).mean()

        # Entropy bonus
        entropy = -(probs * torch.log(probs + 1e-8)).sum(dim=1).mean()

        loss = actor_loss + 0.5 * critic_loss - 0.01 * entropy

        optimizer.zero_grad()
        loss.backward()
        torch.nn.utils.clip_grad_norm_(model.parameters(), 0.5)  # Gradient clipping
        optimizer.step()

GAE (Generalized Advantage Estimation)

PPO는 보통 GAE를 사용하여 Advantage를 추정합니다:

AtGAE=l=0(γλ)lδt+lA_t^{GAE} = \sum_{l=0}^\infty (\gamma \lambda)^l \delta_{t+l}

여기서 δt=rt+γV(st+1)V(st)\delta_t = r_t + \gamma V(s_{t+1}) – V(s_t) (TD error).

  • λ=0\lambda=0: 1-step TD (분산 낮음, 편향 높음)
  • λ=1\lambda=1: Monte Carlo (분산 높음, 편향 없음)
  • λ=0.95\lambda=0.95: 적절한 균형 (실무 표준)

세 알고리즘 비교

특징 REINFORCE A2C PPO
그래디언트 추정 Monte Carlo TD (Temporal Difference) Clipped Importance Sampling
Baseline 없음 (또는 단순 평균) Value 함수 V(s)V(s) Value 함수 + GAE
업데이트 빈도 에피소드 종료 후 매 스텝 배치 수집 후 여러 epoch
안정성 낮음 (분산 큼) 중간 높음 (클리핑으로 보수적 업데이트)
샘플 효율 낮음 중간 높음 (데이터 재사용)
구현 복잡도 쉬움 중간 어려움
실무 활용 간단한 실험 실시간 학습 대부분의 SOTA 벤치마크

실전 팁

1. 하이퍼파라미터 튜닝

  • Learning rate: Actor는 작게 (1e-4~3e-4), Critic은 크게 (1e-3)
  • Discount factor γ\gamma: 0.99 (장기 보상), 0.9 (단기 보상)
  • GAE λ\lambda: 0.95가 안정적
  • PPO clip ϵ\epsilon: 0.1~0.2 (클수록 공격적 업데이트)

2. 트릭들

기법 효과
Reward normalization 보상 스케일 안정화
Advantage normalization 그래디언트 안정화
Gradient clipping 폭발 방지
Entropy bonus 탐험 유도 (초반에 크게, 후반에 작게)
Learning rate decay 후반 미세 조정

3. 디버깅 체크리스트

  • [ ] 정책 엔트로피가 점점 감소하는가? (탐험→활용)
  • [ ] Critic loss가 수렴하는가?
  • [ ] Clipping fraction이 5~30%인가? (너무 높으면 업데이트 제한 과도)
  • [ ] Explained variance가 0.7 이상인가? (Critic이 Return을 잘 예측)

마무리

Policy Gradient는 강화학습의 핵심 방법론입니다. REINFORCE는 개념적으로 단순하지만 분산이 크고, A2C는 Baseline으로 분산을 줄이고 온라인 학습을 가능케 하며, PPO는 클리핑과 GAE로 안정적이고 샘플 효율적인 학습을 달성합니다.

실무에서는 PPO가 사실상의 표준입니다. 로봇 제어, 게임 AI, 대규모 언어 모델의 RLHF(Reinforcement Learning from Human Feedback)까지 폭넓게 사용되며, 안정성과 성능이 검증되었습니다.

코드를 직접 실행해보며 각 알고리즘의 차이를 체감하고, 하이퍼파라미터를 조정하면서 그래디언트 추정과 분산 감소의 중요성을 경험해보세요. Policy Gradient를 마스터하면 최신 강화학습 논문을 이해하는 데 필수적인 기반을 갖추게 됩니다.

Did you find this helpful?

☕ Buy me a coffee

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

TODAY 365 | TOTAL 2,588