강화학습에는 에이전트가 학습하는 크게 두 가지 접근 방식이 있습니다.
- 가치 기반 (Value-Based): 행동의 가치(Q값)를 계산해서 가장 높은 가치의 행동을 고르는 방식
- 정책 기반 (Policy-Based): 행동의 가치를 먼저 계산하는 것이 아니라, 상황(State)에 따라 어떤 행동을 할지 그 규칙(Policy) 자체를 직접 학습하는 방식
이번 포스팅에서는 정책 기반 강화학습의 이론적 배경이 되는 Policy Gradient Theorem부터, 이를 구현한 REINFORCE 알고리즘, 그리고 가치 기반과 정책 기반의 장점을 합친 Actor-Critic 구조까지 정리해 보겠습니다.
1. Policy Gradient Theorem 이란?
Policy Gradient Theorem은 정책 기반 강화학습에서 "에이전트의 정책을 어떻게 개선해야 하는가?"를 수학적으로 설명하는 정리입니다.
에이전트가 더 많은 보상을 얻으려면, 실제로 높은 보상을 얻은 행동을 선택할 확률은 높이고, 낮은 보상을 얻은 행동을 선택할 확률은 낮추는 방향으로 정책 파라미터($\theta$)를 조정해야 합니다. 이 정리는 환경의 모델(Dynamics)을 알 필요 없이, 경험으로부터 직접 정책을 학습할 수 있게 해주는 핵심 이론입니다.
1.1 Policy Gradient Theorem 수식의 이해
핵심 수식은 다음과 같습니다.
수식의 각 항목이 의미하는 바는 다음과 같습니다.
- $\nabla_\theta J(\theta)$: 정책을 더 좋은 방향으로 바꾸기 위한 기울기(Gradient)입니다. "정책을 어느 방향으로 수정하면 보상이 커질까?"를 의미합니다.
- $\pi_\theta(a|s)$: 현재 상태 $s$에서 행동 $a$를 선택할 확률입니다.
- $\nabla_\theta \log \pi_\theta(a|s)$: 정책 파라미터를 조금 바꿨을 때 해당 행동의 선택 확률이 얼마나 변하는지를 나타내는 '정책의 민감도'입니다.
- $Q^\pi(s,a)$: 상태 $s$에서 행동 $a$를 했을 때, 앞으로 받을 것으로 기대되는 총 보상(가치)입니다.
1.2 수식에 로그(log)가 들어간 이유
수학적으로 계산(미분)을 쉽게 만들기 위해서입니다. 특히 에피소드 동안 일어나는 행동 확률의 곱을 다룰 때, 로그를 취하면 덧셈으로 바뀌어 컴퓨터가 계산하기 훨씬 편해집니다. (이를 Log-derivative trick이라고 부릅니다.)
2. REINFORCE 알고리즘
REINFORCE 알고리즘은 Policy Gradient Theorem을 실제 코드로 구현한 가장 기본적인 몬테카를로(Monte Carlo) 기반 알고리즘입니다.
2.1 동작 과정
- 에이전트가 현재 정책에 따라 에피소드가 끝날 때까지 행동합니다.
- 에피소드가 끝나면, 각 시점(Step)에서 얻은 총 누적 보상인 Return ($G_t$)을 계산합니다.
- 계산된 Return을 이용해 정책을 업데이트합니다.
2.2 누적 보상 (Return, $G_t$)
특정 시점 $t$ 이후에 얻은 모든 보상의 합을 의미하며, 미래 보상에 대한 할인율(Gamma, $\gamma$)을 적용하여 계산합니다.
2.3 업데이트 수식
REINFORCE의 파라미터 업데이트 수식은 다음과 같습니다.
- 보상($G_t$)이 큰 행동은 확률을 높이고, 보상이 작은 행동은 확률을 낮추게 됩니다.
2.4 PyTorch로 구현한 REINFORCE (CartPole-v1)
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()
# 마지막 스텝부터 거꾸로 계산하여 Return(G_t)을 효율적으로 구함
for r, prob in self.data[::-1]:
R = r + gamma * R
# loss = -log(prob) * Return (음수를 붙여 Gradient Ascent 효과)
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() # 확률 분포 기반 행동 샘플링
s_prime, r, done, truncated, info = env.step(a.item())
pi.put_data((r, prob[a])) # 보상과 선택한 행동의 확률 저장
s = s_prime
score += r
pi.train_net() # 에피소드 종료 후 정책 업데이트
if n_epi % print_interval == 0 and n_epi != 0:
print(f"# of episode: {n_epi}, avg score: {score/print_interval:.1f}")
score = 0.0
env.close()
if __name__ == '__main__':
main()
3. Actor-Critic (액터-크리틱)
REINFORCE 알고리즘은 에피소드가 끝날 때까지 기다려야 하고, 전체 Return을 사용하기 때문에 분산(Variance)이 커서 학습이 불안정하다는 단점이 있습니다. 이를 해결하기 위해 등장한 것이 Actor-Critic입니다.
- Actor (배우): "무엇을 할까?"를 결정합니다. 정책 $\pi(a|s)$를 학습합니다.
- Critic (평가자): "방금 한 선택이 얼마나 좋았을까?"를 평가합니다. 가치 함수 $V(s)$ 또는 $Q(s, a)$를 학습합니다.
3.1 Critic이 필요한 이유와 TD Error
Actor 혼자서는 행동의 좋고 나쁨을 정확히 판단하기 어렵습니다. 이때 Critic이 **TD Error (Temporal Difference Error)**를 이용해 피드백을 줍니다.
- $V(s)$: 예상했던 현재 상태의 가치
- $r + \gamma V(s')$: 실제로 경험한 보상과 다음 상태의 가치
- $\delta > 0$: 예상보다 결과가 좋음 $\rightarrow$ Actor는 해당 행동의 확률을 높임
- $\delta < 0$: 예상보다 결과가 나쁨 $\rightarrow$ Actor는 해당 행동의 확률을 낮춤
3.2 REINFORCE vs Actor-Critic
| 특징 | REINFORCE | Actor-Critic |
| 업데이트 시점 | 에피소드가 끝난 후 한 번에 | 매 스텝(Step)마다 가능 |
| 기준 값 | 에피소드 전체 누적 보상 (Return) | Critic의 평가 (TD Error 등) |
| 학습 안정성 | 분산이 커서 불안정함 | Critic 덕분에 상대적으로 안정적임 |
4. 다양한 Actor-Critic의 발전 형태
Actor-Critic 구조에서 Critic이 어떤 값을 평가 기준으로 삼느냐에 따라 여러 알고리즘으로 나뉩니다.
4.1 Q Actor-Critic
Critic이 상태 가치 $V(s)$ 대신 행동 가치 함수 $Q(s,a)$를 학습하는 방식입니다. Actor는 이 $Q$값을 바탕으로 행동 확률을 조정합니다.
4.2 Advantage Actor-Critic (A2C)
단순히 가치를 평가하는 것을 넘어, "평균적인 행동보다 얼마나 더 좋은가?"를 나타내는 Advantage $A(s,a)$를 사용합니다.
이 방식은 학습의 분산을 획기적으로 줄여주어 매우 안정적인 학습을 돕습니다.
4.3 TD Actor-Critic
Critic이 매 스텝마다 발생하는 TD Error 자체를 어드밴티지로 사용하여 정책을 업데이트하는 방식입니다. 에피소드 종료를 기다릴 필요가 없어 학습 속도가 빠릅니다.
5. PyTorch로 구현한 TD Actor-Critic (CartPole-v1)
하나의 신경망 안에 Actor(정책 출력)와 Critic(가치 출력)을 동시에 구성한 코드입니다.
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 # 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) # Actor: 정책(행동 확률) 출력층
self.fc_v = nn.Linear(256, 1) # Critic: 가치(V(s)) 출력층
self.optimizer = optim.Adam(self.parameters(), lr=learning_rate)
def pi(self, x, softmax_dim=0):
x = F.relu(self.fc1(x))
prob = F.softmax(self.fc_pi(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 계산
td_target = r + gamma * self.v(s_prime) * done
# TD Error 계산 (실제 타깃 - 예측 가치)
delta = td_target - self.v(s)
pi = self.pi(s, softmax_dim=1)
pi_a = pi.gather(1, a) # 실제 선택한 행동의 확률
# Actor Loss: -log(pi) * TD Error
# Critic Loss: Smooth L1 Loss (예측 가치와 TD 타깃의 차이)
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:
break
model.train_net() # 지정된 스텝마다 네트워크 업데이트
if n_epi % print_interval == 0 and n_epi != 0:
print(f"# of episode: {n_epi}, avg score: {score/print_interval:.1f}")
score = 0.0
env.close()
if __name__ == '__main__':
main()
'개념 정리 step2 > 강화 학습' 카테고리의 다른 글
| [강화학습] "PPO" 알고리즘 핵심 이론 및 최신 RL 트렌드 정리 (0) | 2026.03.15 |
|---|---|
| [강화학습] A3C, A2C, ACER 알고리즘 핵심 정리 및 구현 (0) | 2026.03.14 |
| [강화학습] Q-learning의 개념부터 Gym을 활용한 DQN 구현까지 (0) | 2026.03.04 |
| [강화학습] Deep Reinforcement Learning 개념 (0) | 2026.03.03 |
| [강화학습] TD Learning (시간차 학습) 개념, 랜덤 벽 GridWorld 실습 (0) | 2026.03.01 |
