정책을 직접 학습하는 강화학습과 Policy Gradient Theorem
강화학습에는 가치 기반(Value-Based) 과 정책 기반(Policy-Based) 이 있다. 가치 기반은 Q값을 구한 뒤 가장 좋은 행동을 고르고, 정책 기반은 행동을 선택하는 규칙(정책) 자체를 직접 학습한다. “정책을 더 좋은 방향으로 어떻게 업데이트할지”를 수학적으로 설명하는 것이 Policy Gradient Theorem 이고, 이를 구현한 대표 알고리즘이 REINFORCE 와 Actor-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. 동작 과정
- 에이전트가 정책 πθ에 따라 행동한다.
- 에피소드가 끝날 때까지 환경과 상호작용한다.
- 각 시점 t에서 Return G_t(그 시점 이후 보상의 할인 합)를 계산한다.
- 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의 장점
- 안정성: Critic이 즉각적인 평가를 해주어 REINFORCE보다 안정적인 경우가 많다.
- 연속 행동: 조향, 로봇 제어처럼 연속 행동 공간에 적합한 변형이 많다.
- 실제 사용: 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 코드를 비교해 보면 정책 기반·가치 기반 결합의 차이가 잘 드러난다.
'현재 > 강화학습' 카테고리의 다른 글
| A2C - Advantage Actor-Critic 동기식 병렬 학습 (0) | 2026.03.17 |
|---|---|
| A3C - Asynchronous Advantage Actor-Critic (0) | 2026.03.16 |
| Q-learning과 DQN - Q값 업데이트, Gym·CartPole 실습 (0) | 2026.03.12 |
| Deep RL - 함수 근사, 신경망, 가치 기반·정책 기반 강화학습 (0) | 2026.03.11 |
| Monte Carlo와 TD Learning - GridWorld로 비교하는 모델 프리 가치 학습 (0) | 2026.03.10 |