수민 '-'

플오그래밍

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

PPO - Proximal Policy Optimization 정책 업데이트 안정화

PPO(Proximal Policy Optimization)는 강화학습에서 정책(Policy) 을 더 안정적으로 업데이트하기 위해 제안된 정책 기반 알고리즘이다.
기존의 정책 경사(Policy Gradient) 방식은 정책을 한 번에 크게 바꾸면 학습이 흔들릴 수 있는데, PPO는 이전 정책에서 너무 멀리 벗어나지 않도록 제한을 둔다.

이를 위해 ratio(확률 비율)를 계산하고, 허용 범위를 벗어나면 업데이트 크기를 제한하는 clipped objective 를 사용한다.
그리고 Actor-Critic 구조를 기반으로 Actor는 행동 정책을 학습하고, Critic은 상태 가치(Value)를 추정해 Advantage를 계산한다.

PPO는 구현이 비교적 단순하면서도 안정성과 성능이 좋아서 OpenAI Baselines, Stable-Baselines, RLlib 같은 프레임워크에서 널리 쓰이는 대표적인 알고리즘 중 하나다.


1. PPO가 등장한 이유

정책 업데이트를 한 번 할 때 기존 정책과 너무 크게 달라지면, 학습이 불안정해지고 성능이 갑자기 떨어질 수 있다.
그래서 “업데이트를 하되, 정책이 과도하게 바뀌지 않게 하자”는 문제의식이 생겼고, 그 해법 계열이 TRPO와 PPO로 이어진다.


2. TRPO

TRPO는 정책이 이전 정책에서 너무 멀어지지 않도록 KL Divergence 로 변화량을 제한한다.
다만 다음과 같은 이유로 구현이 부담스럽다.

1) 구현이 매우 복잡함
2) 계산량이 많음
3) 헤시안 행렬(Hessian Matrix)을 사용

3. PPO 핵심 아이디어

PPO는 “정책을 바꾸긴 바꾸되, 너무 멀리 가지는 말자”가 목표다.
핵심은 Policy(확률 비율), Advantage, Clip 이 세 덩어리로 정리된다.


1. Policy

  • π(a|s): 상태 s에서 행동 a를 할 확률
  • π_old(a|s): 이전 정책이 같은 행동을 선택할 확률
  • ratio: 현재 정책의 행동 확률 / 이전 정책의 행동 확률

ratio 값은 보통 1이면 “변화 없음”으로 해석한다.


2. Advantage

PPO는 Advantage Actor-Critic 구조를 사용한다.
여기서 Advantage는 “해당 행동이 평균보다 얼마나 좋은가”를 의미한다. 즉, 상태 가치 대비 행동 가치의 상대적인 차이다.


3. PPO Clipped Objective

PPO의 목표 함수는 ratio와 Advantage를 묶고, ratio가 허용 범위를 벗어나면 clip으로 업데이트를 제한한다.

ε는 보통 0.1 ~ 0.2 범위에서 많이 시작한다.


4. Clip

PPO에서 ratio는 “이전 정책 대비 현재 정책이 해당 행동의 확률을 얼마나 바꿨는가”를 의미한다.

PPO의 목표는 “정책이 너무 많이 바뀌지 않게 하자!” 입니다.
그래서 허용 범위를 정하고, 그 밖이면 업데이트를 제한합니다.

ε = 0.2 일 때, 허용 범위는
1 - ε = 0.8
1 + ε = 1.2
ratio는 0.8 ~ 1.2 사이까지만 허용
예를 들어
old policy = 0.4
new policy = 0.42
ratio = 0.42 / 0.4 = 1.05
그러면 허용 범위(0.8 ~ 1.2)이므로 정상 업데이트합니다.
만약 ratio가 1.5일 경우
PPO의 Clip은 정책이 이전 정책에서 너무 크게 변하지 않도록 업데이트 크기를 제한함
(ratio = 1.2로 강제로 제한)

4. PPO 전체 알고리즘

초기 정책 πθ 설정
반복:
    환경에서 데이터를 수집
        state
        action
        reward
    Advantage 계산
    여러 epoch 동안 반복
        ratio 계산
        clip 적용
        loss 계산
        gradient 업데이트

아래 코드는 CartPole-v1 같은 이산 행동 환경에서 PPO를 직접 구현한 예시다.

import gymnasium as gym
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.distributions import Categorical

# Hyperparameters
learning_rate = 0.0005
gamma = 0.98

# GAE: 여러 스텝의 TD 오차를 적당히 섞어서 안정적으로 advantage를 계산하기 위한 아이디어
lmbda = 0.95
eps_clip = 0.1
K_epoch = 3
T_horizon = 20


class PPO(nn.Module):
    def __init__(self):
        super(PPO, 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, prob_a_lst, done_lst = [], [], [], [], [], []

        for transition in self.data:
            s, a, r, s_prime, prob_a, done = transition

            s_lst.append(s)
            a_lst.append([a])
            r_lst.append([r])
            s_prime_lst.append(s_prime)
            prob_a_lst.append([prob_a])

            done_mask = 0 if done else 1
            done_lst.append([done_mask])

        s = torch.tensor(np.array(s_lst), dtype=torch.float)
        a = torch.tensor(np.array(a_lst), dtype=torch.long)
        r = torch.tensor(np.array(r_lst), dtype=torch.float)
        s_prime = torch.tensor(np.array(s_prime_lst), dtype=torch.float)
        done_mask = torch.tensor(np.array(done_lst), dtype=torch.float)
        prob_a = torch.tensor(np.array(prob_a_lst), dtype=torch.float)

        self.data = []
        return s, a, r, s_prime, done_mask, prob_a

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

        for _ in range(K_epoch):
            td_target = r + gamma * self.v(s_prime) * done_mask
            delta = td_target - self.v(s)
            delta = delta.detach().cpu().numpy()

            # advantage 계산 (delta를 뒤에서부터 누적)
            advantage_lst = []
            advantage = 0.0
            for delta_t in delta[::-1]:
                advantage = gamma * lmbda * advantage + delta_t[0]
                advantage_lst.append([advantage])

            advantage_lst.reverse()
            advantage = torch.tensor(advantage_lst, dtype=torch.float)

            # 현재 정책에서 행동 a의 확률
            pi = self.pi(s, softmax_dim=1)
            pi_a = pi.gather(1, a)

            # ratio = pi_new(a|s) / pi_old(a|s)
            ratio = torch.exp(torch.log(pi_a + 1e-8) - torch.log(prob_a + 1e-8))

            surr1 = ratio * advantage
            surr2 = torch.clamp(ratio, 1 - eps_clip, 1 + eps_clip) * advantage

            loss = -torch.min(surr1, surr2) + 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 = PPO()
    score = 0.0
    print_interval = 20

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

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

                s_prime, r, terminated, truncated, info = env.step(a)
                done = terminated or truncated

                model.put_data((s, a, r / 100.0, s_prime, prob[a].item(), 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()


if __name__ == '__main__':
    main()

5. 그 외 강화학습

World Model RL

머릿 속 시뮬레이션처럼 “가상의 미래”를 상상하면서 학습한다.

Transformer RL

좋은 행동 패턴을 시퀀스로 보고, Transformer 기반 학습으로 정책을 만든다.

Offline RL

환경에서 새 데이터를 모으지 않고, 이미 수집된 데이터셋만으로 학습한다.


SAC

SAC(Soft Actor-Critic)은 최대 엔트로피(Maximum Entropy) 강화학습 기반의 Off-policy Actor-Critic 알고리즘이다.
보상(reward)을 최대화하는 것뿐 아니라 정책의 엔트로피(무작위성)도 함께 최대화해, 특정 행동에 과도하게 수렴하는 것을 막고 더 다양한 탐색을 유도한다.

연속 행동 공간(continuous action space)에서 특히 강력한 성능을 보이며, 로봇 제어 같은 실제 환경에서도 널리 쓰인다.


MuZero

MuZero는 DeepMind의 모델 기반 강화학습 알고리즘이다.
환경의 정확한 규칙이나 모델을 미리 알지 못해도, 학습 과정에서 world model을 함께 학습해 의사결정을 수행할 수 있다.

MuZero는 representation network, dynamics network, prediction network를 함께 학습하고, 의사결정을 위해 MCTS를 사용한다.


MCTS

MCTS(Monte Carlo Tree Search)는 가능한 행동들을 트리로 확장하면서, 여러 번의 시뮬레이션 통계를 기반으로 가장 좋은 행동을 찾는 탐색 알고리즘이다.

Selection → Expansion → Simulation → Backpropagation의 네 단계를 반복하며, 점점 더 유망한 가지에 탐색 자원을 집중한다.


Dreamer

Dreamer는 실제 환경에서 상호작용하며 학습하기보다, 학습된 world model 내부에서 미래를 “상상”하며 정책을 학습하는 모델 기반 강화학습 계열이다.

잠재 상태(latent state) 기반 동적 모델을 학습하고, 그 모델로 가상의 trajectory를 만들어 정책을 업데이트한다.


Decision Transformer

Decision Transformer는 강화학습 문제를 시퀀스 모델링으로 바꾸는 접근이다.
Transformer에 과거의 상태, 행동, 목표 보상(return)을 하나의 시퀀스로 넣고, 다음 행동을 예측한다.

기존처럼 반복적으로 정책/가치를 갱신하는 대신, 오프라인 데이터셋을 활용해 지도학습 방식으로 학습한다. Return-to-Go를 조건으로 넣을 수 있어 “원하는 성능 수준에 맞춰 행동 생성”이 가능하다.


마치며

  • PPO는 정책 업데이트를 너무 크게 만들지 않도록 ratio 기반으로 Clip을 적용하는 방식이다.
  • Advantage Actor-Critic으로 학습 신호를 안정적으로 만들고, clipped objective로 정책 변화를 제한한다.
  • 구현이 어렵지 않은 편인데도 안정성과 성능이 좋아서 실무/프레임워크에서 기본 알고리즘으로 자주 사용된다.