1. 이 글에서 다루는 내용
이 글에서는 정책 기반 강화학습의 다음 단계인 Actor-Critic 계열을CartPole-v1 예제를 중심으로 정리합니다.
- Actor-Critic의 기본 개념
- TD Error(Temporal Difference Error)의 의미
- Q Actor-Critic / Advantage Actor-Critic(A2C) 아이디어
- CartPole 환경에서의 Actor-Critic 구현 흐름
즉, “REINFORCE 이후, Actor-Critic으로 한 걸음 더 나아가는 내용”을 담고 있습니다.
2. REINFORCE의 한계, 그리고 Actor-Critic이 나오는 이유
REINFORCE는 정책 기반 강화학습의 가장 기본적인 알고리즘입니다.
하지만 실제로 구현해 보면 몇 가지 한계가 보입니다.
- 에피소드가 끝날 때까지 기다려야 함
- Return \(G_t\)를 계산하려면 마지막 시점까지 보상을 다 봐야 합니다.
- 에피소드가 길면 한 번 업데이트하는 데 걸리는 시간이 길어집니다.
- 분산이 크다
- \(G_t\)는 “그 이후의 모든 보상”을 다 모으기 때문에,
- 같은 상태·행동이어도 뒤에 어떤 일이 벌어지느냐에 따라 값이 크게 달라집니다.
- 업데이트가 많이 흔들리고, 학습이 불안정해질 수 있습니다.
그래서 자연스럽게 이런 질문이 나옵니다.
에피소드 끝까지 기다리지 말고
가다가 바로바로 평가하면서 정책을 고칠 수는 없을까?
이 질문에 대한 현실적인 답이 Actor-Critic 계열 알고리즘입니다.
3. Actor와 Critic – 역할 나누기
노트북에서는 먼저 Actor-Critic을 직관적으로 설명합니다.
3.1 Actor
- 하는 일: 행동을 선택하는 역할
- 입력: 현재 상태 \(s\)
- 출력: 각 행동에 대한 확률 분포 \(\pi(a|s)\)
- 목표:
- 좋은 행동의 확률은 점점 높이고,
- 나쁜 행동의 확률은 점점 낮추는 방향으로 정책을 바꾸는 것
3.2 Critic
- 하는 일: 지금 상태/행동이 얼마나 괜찮은지 평가
- 보통 두 가지 중 하나를 학습합니다.
- 상태 가치 함수 \(V(s)\)
- 행동 가치 함수 \(Q(s,a)\)
- Actor 입장에서는:
- “방금 했던 행동이 생각보다 좋았는지/나빴는지”를 알려주는 심판에 가깝습니다.
정리하면:
- Actor는 “무엇을 할까?”(행동 선택)
- Critic은 “방금 한 게 얼마나 괜찮았나?”(평가)
에 집중하는 구조입니다.
4. TD Error – Critic이 주는 피드백 신호
Actor-Critic에서 Critic은 TD Error(Temporal Difference Error) 라는 신호로
“예상 대비 실제 결과”를 Actor에게 알려줍니다.
\[
\delta = r + \gamma V(s') - V(s)
\]
- \(V(s)\): 현재 상태에 대해 원래 생각하고 있던 가치
- \(r + \gamma V(s')\): 한 스텝 실제로 해 보니 얻은 보상과 다음 상태 가치를 고려한 새로운 평가
결국,
TD Error = 실제로 관찰한 값 - 원래 예상한 값
4.1 TD Error를 어떻게 해석할까?
- \(\delta > 0\) (양수)
- 실제 결과가 예상보다 좋았음
- “방금 행동이 생각보다 잘했다” → 그 행동의 확률을 키워야 합니다.
- \(\delta < 0\) (음수)
- 실제 결과가 예상보다 나빴음
- “방금 행동은 생각보다 별로였다” → 그 행동의 확률을 줄여야 합니다.
즉, TD Error는 REINFORCE에서의 Return처럼
“이 행동이 얼마나 좋았는지/나빴는지”를 알려주는 시그널이지만,
- 에피소드 끝까지 기다리지 않고
- 매 스텝마다 바로바로 계산할 수 있다는 점이 결정적인 차이입니다.
5. Actor-Critic 기본 구조 정리
노트북에서 설명하는 Actor-Critic의 한 사이클은 다음과 같습니다.
현재 상태 s
1. Actor가 정책 π로 행동 a를 선택
2. 환경이 보상 r, 다음 상태 s'를 반환
3. Critic이 TD Error δ = r + γV(s') − V(s)를 계산
4. Actor는 δ를 이용해 방금 행동의 확률을 조정 (좋았으면 ↑, 나빴으면 ↓)
5. Critic도 V(s)를 td_target 쪽으로 업데이트
이 과정을 반복하면서 Actor와 Critic 둘 다 점점 좋아집니다.
- Actor: 정책 \(\pi(a|s)\) 가 좋아짐
- Critic: 가치 추정 \(V(s)\) 가 정교해짐
6. Q Actor-Critic / Advantage Actor-Critic 아이디어
노트북 후반부에서는 Actor-Critic을 조금 더 다양한 형태로 확장합니다.
6.1 Q Actor-Critic
- Critic이 \(V(s)\) 대신 \(Q(s,a)\) 를 학습하는 구조
- Actor는 Critic이 계산한 \(Q(s,a)\)를 보고:
- Q가 큰 행동의 선택 확률은 높이고
- Q가 작은 행동의 확률은 낮추는 방향으로 정책을 업데이트
- 가치 기반(Q-learning)의 “행동 가치를 직접 보는 시각”과
정책 기반의 “정책 자체를 업데이트하는 방식”을 섞은 형태라고 볼 수 있습니다.
6.2 Advantage Actor-Critic (A2C)
노트북에서는 Advantage Actor-Critic도 간단히 언급합니다.
- Critic은 상태 가치 \(V(s)\)만 학습
- Advantage \(A(s,a)\)를 사용해 정책을 업데이트
\[
A(s,a) = Q(s,a) - V(s)
\]
- 직관:
- “이 행동이 이 상태에서 평균적인 행동보다 얼마나 더 좋은가?”
- 장점:
- 단순히 Q만 쓰는 것보다 분산이 줄어들어 업데이트가 더 안정적
- 실제로 많이 쓰이는 A2C, A3C, PPO 등에서 기본 아이디어로 활용됩니다.
7. CartPole Actor-Critic 구현 – 네트워크 구조
이제 노트북 코드에서 사용하는 CartPole Actor-Critic 구현을 글로 풀어보겠습니다.
7.1 모델 구조
하나의 신경망 안에 Actor와 Critic을 동시에 넣습니다.
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) # 정책 π(a|s)
self.fc_v = nn.Linear(256, 1) # 가치 V(s)
self.optimizer = optim.Adam(self.parameters(), lr=learning_rate)
- 입력: CartPole 상태 4차원 (위치, 속도, 막대 각도, 각속도)
fc1: 공통 은닉층 – 상태에서 유용한 특징을 추출fc_pi: Actor 쪽 출력층 – 행동 2개(왼/오른쪽)에 대한 확률fc_v: Critic 쪽 출력층 – 현재 상태의 가치 \(V(s)\)
두 가지 메서드로 역할을 나눕니다.
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
pi: Actor – 정책 확률 \(\pi(a|s)\) 계산v: Critic – 상태 가치 \(V(s)\) 계산
8. Rollout과 배치 만들기
노트북에서는 한 번 학습하기 전에 여러 스텝의 데이터를 모아 한 번에 업데이트합니다.
# Hyperparameters
learning_rate = 0.0002
gamma = 0.98
n_rollout = 10
n_rollout = 10:- 한 에피소드 전체가 아니라, 10스텝씩 끊어서 데이터를 모으고 업데이트
- 이렇게 하면 완전한 에피소드가 아니어도 TD Error로 학습이 가능합니다.
모은 데이터는 (s, a, r, s_prime, done) 튜플 리스트로 저장합니다.
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])
# 리스트를 numpy → torch.tensor 로 변환 (배치)
...
self.data = []
return s_batch, a_batch, r_batch, s_prime_batch, done_batch
- 보상을
r / 100.0으로 스케일링해서 학습을 조금 더 안정화합니다. done_mask는 종료 상태에서 다음 상태 가치 \(V(s')\) 를 더하지 않기 위한 마스크입니다.
9. Actor-Critic 학습 – TD 타깃과 손실
이제 핵심인 train_net 부분입니다.
9.1 TD 타깃과 TD Error
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) # TD Error
td_target = r + γ V(s') * done_mask- 종료 상태면
done_mask = 0이라서 다음 상태 가치는 더해지지 않습니다.
- 종료 상태면
delta = td_target - V(s)- Critic 관점: “내가 생각한 V(s)가 실제 경험과 얼마나 달랐나?”
9.2 Actor 손실 – 정책을 어떻게 바꿀까?
pi = self.pi(s, softmax_dim=1) # 각 상태에서의 행동 확률
pi_a = pi.gather(1, a) # 실제로 선택한 행동의 확률 π(a|s)
loss = -torch.log(pi_a) * delta.detach() \
+ F.smooth_l1_loss(self.v(s), td_target.detach())
pi: 각 상태에서 [왼쪽, 오른쪽] 확률a: 실제로 선택한 행동 인덱스pi_a = pi.gather(1, a):- “그때 실제로 선택한 행동의 확률”만 뽑아온 것
-log(pi_a) * delta:- \(\delta > 0\) (생각보다 결과가 좋았음)
→ 이 행동의 확률이 더 커지도록 업데이트 - \(\delta < 0\) (생각보다 나빴음)
→ 이 행동의 확률이 줄어들도록 업데이트
- \(\delta > 0\) (생각보다 결과가 좋았음)
여기서 delta.detach()를 쓰는 이유는,
Actor를 업데이트할 때 Critic 쪽 gradient는 끊어 주기 위해서입니다.
9.3 Critic 손실 – V(s)를 더 정확하게
F.smooth_l1_loss(self.v(s), td_target.detach())
SmoothL1Loss는 L1과 L2의 장점을 섞은 손실 함수로,- 작은 오차에는 L2처럼 부드럽게,
- 큰 오차에는 L1처럼 이상치에 덜 민감하게 작동합니다.
- Critic 입장에서는 V(s)가
td_target쪽으로 조금씩 이동하도록 학습됩니다.
마지막으로 배치 전체에 대해 평균을 내고 역전파합니다.
self.optimizer.zero_grad()
loss.mean().backward()
self.optimizer.step()
이 한 번의 train_net() 호출로 Actor와 Critic이 함께 업데이트됩니다.
10. CartPole Actor-Critic 학습 루프
전체 학습 루프는 다음과 같습니다.
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("# of episode :{}, avg score : {:.1f}".format(
n_epi, score / print_interval))
score = 0.0
env.close()
10.1 흐름 요약
- 환경 초기화 → 상태
s받기 n_rollout스텝 동안:- 현재 정책
π로부터 행동a샘플링 - 환경에서
s',r,done받기 (s, a, r, s', done)을 버퍼에 저장- 상태 업데이트, 점수 누적
- 종료되면 루프 탈출
- 현재 정책
train_net()으로 방금 모은 rollout 배치에 대해 Actor-Critic 업데이트- 일정 에피소드마다 평균 점수 출력
에피소드가 진행될수록 평균 점수가 점점 올라가면서,
에이전트가 CartPole을 더 오래 유지하는 방향으로 정책을 배우게 됩니다.
[ 오늘의 정리 ] - REINFORCE에서 Actor-Critic으로
- REINFORCE는 Policy Gradient의 출발점이지만,
- 에피소드 전체 Return을 써서 업데이트 → 분산이 크고, 느릴 수 있습니다.
- Actor-Critic은 여기에 평가자(Critic)를 붙여서,
- TD Error를 이용해 매 스텝마다 정책을 조금씩 고쳐 나가는 구조입니다.
- Q Actor-Critic, Advantage Actor-Critic(A2C) 같은 변형을 통해
- Q(s,a) 정보나 Advantage (A(s,a))를 활용해
- 분산을 줄이고, 업데이트를 더 안정적으로 만들 수 있습니다.
- 노트북의 CartPole 예제에서는
- 하나의 네트워크 안에 Actor와 Critic을 같이 두고,
- rollout → 배치 → TD 타깃/TD Error → 손실 계산 → 역전파
- 흐름으로 Actor-Critic을 구현하고 있습니다.
한마디로 요약하자면,
REINFORCE가 “정책 기반 강화학습의 기본기”라면, Actor-Critic은 그 위에 Critic을 얹어
더 빠르고 안정적으로 학습하게 만든 실전형 정책 기반 알고리즘이다.
'개념 정리실 > 강화학습' 카테고리의 다른 글
| ACER – 경험 재사용으로 Actor-Critic 강화학습 가속하기 (CartPole 구현 포함) (1) | 2026.03.17 |
|---|---|
| A3C – 여러 에이전트가 함께 학습하는 Actor-Critic (0) | 2026.03.16 |
| Policy Gradient & REINFORCE – 정책 기반 에이전트 (1) | 2026.03.12 |
| DQN (Deep Q-Network) – Gym으로 CartPole 풀어보기 (0) | 2026.03.12 |
| Q-learning – 가장 유명한 가치 기반 강화학습 (0) | 2026.03.11 |