PPO, Proximal Policy Optimization Algorithms
1. Introduction
기존의 강화학습 방법론들은 저마다 치명적인 한계가 있었습니다.
- Q-learning: 연속적인 제어나 단순한 문제에서 잘 작동하지 않을 때가 많습니다.
- Vanilla Policy Gradient: 데이터 효율성이 너무 떨어지고, 하이퍼파라미터에 따라 성능이 널뛰기합니다(강건성이 낮음).
[note]
강건성(Robustness) : 시스템, 모델, 제품이 노이즈, 오류, 예기치 않은 입력 변화나 환경적 동요 속에서도 안정적으로 제 기능을 유지하며 튼튼하게 작동하는 능력. 인공지능 분야에서는 '데이터가 변동되어도 성능이 급격히 떨어지지 않는 신뢰성'을 의미함! - TRPO (Trust Region Policy Optimization): 성능은 안정적이지만 수학적으로 너무 복잡하고, 드롭아웃이나 파라미터 공유 같은 딥러닝 아키텍처와 호환이 잘 안 됩니다.
TRPO처럼 '데이터 효율성이 좋고 성능이 안정적' 이면서도, 계산이 복잡하지 않고 구현이 훨씬 단순한 1차 미분 기반의 알고리즘(PPO) 을 제안하는 것입니다.
2. Background
PPO가 나오기 전 사용되던 알고리즘들의 수학적 배경을 짚고 넘어갑니다.
- Policy Optimization: 기대 누적 보상을 최대화하기 위해 정책 의 파라미터 를 직접 업데이트하는 방식입니다.
- TRPO의 한계: TRPO는 정책이 한 번에 너무 크게 변하는 것을 막기 위해, 업데이트 전후 정책 간의 KL 발산(KL Divergence) 이 특정 크기를 넘지 못하도록 수학적인 제약을 걸었습니다. 하지만 이 방식은 목적 함수를 푸는 과정이 지나치게 복잡하다는 단점이 있었습니다.
3. Clipped Surrogate Objective
본 논문의 가장 핵심적인 기여입니다. TRPO의 복잡한 KL 발산 제약 대신, 아주 단순하고 직관적인 'Clipped Probability Ratio (잘린 확률 비율)' 수식을 통해 안정적인 학습을 유도합니다.
3-1. 확률 비율 (Probability Ratio, )
PPO의 핵심!!!
PPO 수식의 모든 것은 이 작은 변수 하나에서 시작합니다.
- 의미: "업데이트하기 전의 옛날 정책()과 비교했을 때, 지금의 새로운 정책()이 이 행동()을 얼마나 더 자주 하려고 하는가?"를 나타내는 비율입니다.
- 만약 라면, 옛날보다 이 행동을 1.5배 더 많이 하겠다는 뜻입니다. 라면 절반으로 줄이겠다는 뜻이죠.
3-2. 기본 대리 목적 함수 ()
위에서 구한 비율을 바탕으로, 우리가 모델을 학습시킬 때 쓰는 아주 원초적인 목표(Objective)를 만듭니다.
-
(Advantage): "이 행동이 평균적인 행동보다 얼마나 더 좋았나?"를 나타내는 점수입니다.
-
좋은 행동이면 가 플러스(+), 나쁜 행동이면 마이너스(-)가 됩니다.
-
의미: 좋은 행동(+점수)은 비율()을 높여서 상을 주고, 나쁜 행동(-점수)은 비율을 낮춰서 벌을 주자는 아주 상식적인 수식입니다.
-
문제점: 모델에게 이 수식을 던져주면, 점수가 좋은 행동을 발견했을 때 비율()을 1.5배, 2배, 10배씩 무한정 올리려고 폭주해 버립니다. 이러면 기존에 쌓아둔 학습 밸런스가 한 번에 박살 납니다 (이게 기존 방법론의 문제였습니다).
3-3. 클리핑된 목적 함수 ()
PPO 논문이 제안한 가장 중요한 하이라이트 수식!!
이 복잡해 보이는 수식을 두 덩어리로 쪼개서 직관적으로 번역해 보겠습니다.
은 보통 를 씁니다.
- 부분:
- 새로운 정책이 옛날 정책보다 딱 0.8배(1-0.2)에서 1.2배(1+0.2) 사이에서만 변하도록 잘라버립니다(클리핑).
- 아무리 좋은 행동을 발견했어도 한 번에 확률을 1.2배 이상 못 올리게 막고, 아무리 나쁜 행동이어도 한 번에 0.8배 밑으로 못 떨어지게 안전장치를 건 것입니다.
- 부분:
- 클리핑을 안 한 오리지널 값과, 클리핑을 한 값 중에서 '더 작은 값(비관적인 값)'을 최종 목표로 선택합니다.
- 즉, 모델이 허황되게 "나 이 행동 확률 5배로 올릴래!"라고 해도, 수식은 "안 돼, 1.2배까지만 올린 걸로 계산해"라고 무시해버리는 역할을 합니다. (Gemini의 예시 good)
4. Adaptive KL Penalty Coefficient
논문에서 제안한 또 다른 대안입니다. 클리핑 연산 대신, 기존 TRPO처럼 KL 발산을 페널티로 사용하되 그 페널티의 강도(계수)를 학습 과정에서 동적으로 조절(Adaptive)하는 방식입니다.
- 결론: 논문의 실험 결과, 이 방식보다 3번의 Clipping 방식이 구현하기도 훨씬 쉽고 성능도 더 우수한 것으로 나타났습니다. 따라서 실제 PPO 구현체에서는 주로 Clipping 방식을 사용합니다.
그러니 가볍게 넘어가겠습니다..ㅎㅎ
5. Algorithm
실제 PPO 알고리즘을 딥러닝 모델로 구현할 때 사용하는 통합된 손실 함수(Total Loss Function) 구조입니다.
- (정책 최적화): 안전한 범위 내에서 행동 확률을 업데이트합니다.
- (Value Function): 현재 상태를 평가하는 가치 네트워크(Value Function)의 예측 오차를 줄입니다.
쉽게 말해, 현재 상태가 얼마나 유리한지 정확하게 점수를 매기는 법을 배우는 것입니다.
- (Entropy): 에이전트가 한 가지 행동에만 고착되지 않도록, 탐험(Exploration)을 장려합니다.
PPO는 한 번 수집한 궤적 데이터를 미니배치로 쪼개어 여러 번의 에포크(Epoch) 동안 재학습할 수 있어 샘플 효율성이 극대화됩니다.
6. Experiments
- Continuous Control (연속 제어): MuJoCo 물리 엔진 기반의 7개 로봇 시뮬레이션 환경에서, PPO는 기존의 모든 정책 경사 알고리즘을 압도하는 성능을 보여주었습니다.
- 3D Humanoid (고차원 제어): 3D 휴머노이드가 달리거나 조향하는 등 자유도가 매우 높은 복잡한 제어 문제도 성공적으로 학습해 냈습니다.
- Atari Domain (아타리 게임): 49개의 픽셀 기반 아타리 게임에서도 구조가 훨씬 복잡한 다른 알고리즘들과 대등하거나 더 나은 성능을 달성했습니다.
7. Conclusion
- PPO는 TRPO가 가진 안정성과 신뢰성을 유지하면서도 구현 난이도를 혁신적으로 낮췄습니다.
이를 가능케 한 KEY가 바로 확률 비율
- 약간의 목적 함수 수정만으로 바로 적용할 수 있을 만큼 매우 직관적입니다.
- 정책 네트워크와 가치 네트워크가 파라미터를 공유하는 범용적인 딥러닝 세팅에서도 훌륭하게 작동하여, 샘플 효율성과 단순함 사이에서 최적의 균형을 이뤄낸 강화학습계의 표준 알고리즘입니다.
Code
이후 실험에서 쓰게 될 라이브러리인
Stable Baselines3의 코드를 분석하면서 이론으로 배운 PPO의 3단계 핵심 수식이 실제로 어떻게 구현되어 있는지 알아봅시다!
Reference: Stable Baselines3 PPO 소스코드 (GitHub)
1. Probability Ratio
수식:
코드에서는 나눗셈 대신 로그(log)의 뺄셈을 이용해 계산의 안정성을 높입니다.
import torch
from torch.nn import functional as F
# 현재 정책의 가치(values), 로그 확률(log_prob), 엔트로피(entropy) 계산
values, log_prob, entropy = self.policy.evaluate_actions(rollout_data.observations, rollout_data.actions)
# 해당 연산에 쓰이는 observations나 actions는 이미 저장된 data에서 그냥 가져온다..!
# 1. 확률 비율(ratio) 계산
# rollout_data.old_log_prob: 데이터 수집 단계에서 저장해둔 이전 정책의 로그 확률
# exp(log A - log B) = A / B
ratio = torch.exp(log_prob - rollout_data.old_log_prob)
2. Clipped Surrogate Objective
수식:
pytorch의 torch.clamp 함수를 사용하면 논문의 핵심인 클리핑 한 줄로 해결됩니다.
# Advantage 정규화 (코드 아래에 추가 설명 참고!!)
advantages = rollout_data.advantages
if self.normalize_advantage:
advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
# 클리핑 범위(epsilon) 설정. 보통 0.2를 사용!
clip_range = self.clip_range(self._current_progress_remaining)
# 2-1. 클리핑하지 않은 원래 목표값 (r_t * A_t)
policy_loss_1 = advantages * ratio
# 2-2. 클리핑을 적용한 목표값 (0.8 ~ 1.2 사이로 자름)
policy_loss_2 = advantages * torch.clamp(ratio, 1 - clip_range, 1 + clip_range)
# 2-3. 둘 중 더 작은 값(비관적인 값)을 선택
# gradient ascent를 수행하기 위해 부호를 마이너스(-)로 바꿈!!
policy_loss = -torch.min(policy_loss_1, policy_loss_2).mean()
어드밴티지 정규화(Advantage Normalization) 는 강화학습 코드를 짤 때 성능을 비약적으로 끌어올려 주는 기법으로, 논문의 메인 수식에는 크게 강조되지 않지만, 실제 구현체(Stable Baselines3 등)에서는 무조건 사용되는 핵심 디테일입니다.
- 수식 자체는 아주 간단한 표준화(Standardization) 공식입니다.
# 평균을 빼고, 표준편차로 나누기! (1e-8은 0으로 나누는 것을 방지하기 위한 아주 작은 값)
advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
그렇다면 이 Advantage Normalization은 대체 어떻게 성능을 올릴 수 있는 걸까요?
1. "절대 평가"를 "상대 평가"로 변환 (평균을 0으로 맞추기)
어드밴티지()의 본래 뜻은 "이 행동이 예상보다 얼마나 좋았는가?"입니다. 하지만 환경에 따라 보상의 기준이 제각각입니다. 어떤 게임은 살아남기만 해도 매 턴마다 +100점을 주고, 어떤 게임은 목표를 달성해야만 겨우 +1점을 줍니다.
- 정규화 전: 모든 행동의 어드밴티지가
+90, +100, +110처럼 전부 플러스(+)일 수 있습니다. 이러면 신경망은 "아, 내가 한 행동이 다 좋은 거구나!" 하고 모든 행동의 확률을 다 높이려고 듭니다. - 정규화 후 (
- advantages.mean()): 평균이 0이 됩니다. 아까의 값들이-10, 0, +10으로 바뀝니다. - 효과: 방금 수집한 데이터(배치) 안에서 "딱 절반의 잘한 행동(+)은 확률을 높이고, 나머지 절반의 못한 행동(-)은 확률을 낮춰라!"라는 아주 명확한 상대적 피드백을 줄 수 있게 됩니다.
2. PPO 클리핑(Clipping)과의 시너지 (표준편차를 1로 만들기)
앞서 PPO의 핵심이 바로, 정책이 너무 크게 변하지 않도록 클리핑(clip_range=0.2)을 하는 것이라고 배웠습니다. 그런데 여기서 간과하기 쉬운 맹점이 있습니다.
PPO의 목적 함수는 비율(ratio) * 어드밴티지(A) 입니다.
클리핑은 '비율(ratio)'이 0.8~1.2를 넘지 못하게 막아줄 뿐, 어드밴티지 값 자체의 크기를 막아주지는 못합니다.
- 만약 어드밴티지가 10,000점이라면? 클리핑을 해도 곱해지는 값이 너무 커서 신경망 가중치가 한 번에 폭주(Gradient Exploding)해버립니다.
- 정규화 후 (
/ advantages.std()): 어드밴티지의 값들이 대부분 -2.0 ~ +2.0 사이의 예쁘고 일정한 범위로 압축됩니다. - 효과: PPO의 클리핑 메커니즘과 곱해졌을 때 신경망이 소화하기 딱 좋은 크기의 gradient(기울기)가 만들어지며, 학습이 붕괴하지 않고 아주 안정적으로 우상향하게 됩니다.
3. 하이퍼파라미터 튜닝의 최소화 (범용성)
AI 모델을 학습시킬 때 가장 골치 아픈 것이 학습률(Learning Rate)을 설정하는 것입니다. 보상 스케일이 1000단위인 로봇 환경과 1단위인 로봇 환경은 원래라면 학습률을 다르게 세팅해야 합니다.
하지만 어드밴티지 정규화를 거치면, 어떤 환경이든 신경망에 들어가는 피드백(Loss)의 스케일이 비슷해집니다. 덕분에 Stable Baselines3 같은 라이브러리가 기본 학습률(예: 3e-4) 하나만으로도 거의 모든 환경에서 준수한 성능을 뽑아낼 수 있는 엄청난 범용성을 가지게 됩니다.
3. Value Loss and Entropy Bonus
가치 함수(Critic)의 오차를 구하고, 탐험(Exploration)을 유도하는 엔트로피를 계산합니다.
[note]
Advantage = Return - Value
이때, Value를 계산해주는 것이 바로 Critic!!!
# 3-1. 가치 함수 오차 (Value Loss / Critic Loss): MSE(Mean Squared Error) 사용
value_loss = F.mse_loss(rollout_data.returns, values.flatten())
# 3-2. 엔트로피 보너스 (Entropy Bonus)
# Loss 수식에 더하기 위해 마이너스 부호를 붙여 최소화(엔트로피는 최대화) 유도
entropy_loss = -torch.mean(entropy)
4. Total Loss
수식:
- 위에서 구한 3가지 Loss를 하나로 합친 뒤 역전파를 수행합니다.
- Policy Loss: 안전하게 행동 방침 개선
- Value Loss: 얼마나 좋은지/나쁜지 평가
- Entropy: 가끔 새로운 시도 장려
# ent_coef(엔트로피 가중치)는 보통 0.01, vf_coef(가치 함수 가중치)는 0.5 사용!
loss = policy_loss + self.ent_coef * entropy_loss + self.vf_coef * value_loss
# 역전파(Backpropagation) 수행
self.policy.optimizer.zero_grad()
loss.backward()
# 그래디언트 폭주를 막기 위한 그래디언트 클리핑 추가 적용 (실전 디테일!)
torch.nn.utils.clip_grad_norm_(self.policy.parameters(), self.max_grad_norm)
self.policy.optimizer.step()