지난 글에서 상태마다 행동의 가치를 적어 두는 Q-테이블에서 출발해, 그 표가 무너지는 자리를 신경망으로 넘어선 DQN까지 따라갔다. 둘은 저장하는 자리만 다를 뿐 하는 일이 같았다 — 가치를 배우고 정책은 로 뽑아 쓴다. 그리고 그 가 벽을 하나 남겼다. 이번 글의 정책 경사법은 가치를 거치지 않고 정책 자체를 신경망으로 두어 그 벽을 넘는다. 유도는 세 줄로 끝나지만, 그렇게 얻은 추정량이 실제로 학습을 굴러가게 만들려면 분산·탐색·시간 지평을 각각 손봐야 한다. 이 글은 그 순서를 따라간다.
가치 기반과 정책 기반
argmax가 남긴 벽
가치 기반 방법은 를 배우고 행동은 그중 최댓값을 주는 것으로 고른다. 이 한 줄에 조건이 숨어 있다. 최댓값을 고르려면 행동을 하나씩 셀 수 있어야 한다. 상태는 아무리 커도 신경망이 받아 주지만, 행동이 「관절을 −180도에서 180도 사이 어디로 돌릴 것인가」라면 셀 수 있는 목록이 없다.
구간을 잘게 쪼개 이산화하는 우회로가 있긴 하다. 그런데 관절이 여섯 개이고 각각을 열 칸으로 쪼개면 행동이 백만 개가 되고, 관절 수에 따라 지수로 늘어난다. 정책 기반은 이 문제를 아예 다르게 푼다. 라는 분포를 신경망으로 직접 내놓고 거기서 표본을 뽑으므로, 행동이 연속이어도 출력층이 평균과 표준편차 두 개면 끝난다.
확률적 정책이 필요한 자리
연속 행동만이 이유는 아니다. 최적 정책이 아예 확률적인 문제가 있다. 가위바위보에서 언제나 같은 것을 내는 전략은 상대가 읽는 순간 최악이 되고, 최적 전략은 셋을 3분의 1씩 내는 확률 분포다. 포커의 블러핑도 같은 구조다.
가치 기반에서도 -greedy로 무작위성을 섞지만 그것은 탐색을 위한 잡음이지 정책의 일부가 아니다. 학습이 끝나면 걷어 내는 것이 전제이고, 섞는 비율도 상태와 무관하게 같다. 정책 기반에서는 분포 자체가 학습 대상이라, 확률적인 것이 최적이면 그 확률이 그대로 학습된다.
상대가 있는 문제만 그런 것도 아니다. 상태를 일부만 관측하는 환경에서도 확률적 정책이 필요하다. 겉보기에 같은 두 상태가 실제로는 다른 상태인데 관측으로 구분되지 않으면, 그 관측에 대해 하나의 행동을 못 박는 순간 둘 중 한쪽에서는 언제나 틀린 행동을 하게 된다. 둘을 섞어 내는 확률적 정책이 그 자리에서는 결정론적 정책보다 낫다.
수렴의 성격
셋째 차이는 안정성이다. 가치 기반은 함수 근사·부트스트래핑·오프폴리시 학습 셋이 겹치면 발산할 수 있다는 것이 알려져 있고, DQN의 리플레이 버퍼와 타깃 네트워크가 그 발산을 막으려는 장치다. 정책 경사는 목적 함수를 직접 경사 상승으로 올라가는 형태라 적어도 국소 최적으로 수렴한다는 보장이 있다.
다만 이 보장은 생각보다 약하다. 「국소 최적으로 간다」는 말은 좋은 정책으로 간다는 뜻이 아니고, 보장이 성립하는 조건도 학습률이 충분히 작고 경사 추정이 편향되지 않았을 때다. 이어지는 절들이 대부분 그 두 조건을 실제로 맞추는 이야기다.
정책 경사 정리
로그 미분 항등식
목표는 기대 누적 보상 를 최대화하는 것이다. 곤란한 점은 기대값의 분포 자체가 에 달려 있다는 것이다. 보상 함수는 와 무관한데 어떤 궤적이 얼마나 자주 나오는지가 에 달렸으니, 안쪽을 미분하는 것으로는 답이 안 나온다.
여기서 쓰는 도구가 하나뿐이다. 미분 결과를 원래 확률로 나누어 다시 곱하는 항등식이다.
좌변은 확률의 미분이고 우변은 확률 곱하기 로그 확률의 미분이다. 이 모양이 중요한 이유는 우변이 「 로 뽑은 표본에 대한 기대값」 꼴이기 때문이다. 미분을 기대값 안으로 집어넣을 길이 열린다.
세 줄 유도
남은 것은 를 푸는 일이다. 궤적 하나의 확률은 시작 상태 확률과 매 스텝의 전이 확률, 그리고 정책의 곱이다. 로그를 씌우면 곱이 합이 되는데, 이 중 시작 상태 확률과 전이 확률은 환경이 정하는 값이라 로 미분하면 0이다.
환경 모델이 통째로 사라졌다는 것이 이 유도의 핵심이다. 전이 확률을 몰라도 정책의 로그 확률만 미분할 수 있으면 경사를 추정할 수 있다. 모델 없이 배우는 방법이라는 말이 여기서 나온다.
손실 함수의 정체
구현에서는 경사 상승 대신 부호를 뒤집어 손실로 만든다.
for episode in range(1000):
log_probs, rewards = run_episode(env, policy) # 현재 정책으로 궤적 하나
returns = compute_returns(rewards, gamma=0.99)
# 경사 상승을 경사 하강으로 뒤집은 대리 손실
loss = -(torch.stack(log_probs) * returns).mean()
optimizer.zero_grad()
loss.backward()
nn.utils.clip_grad_norm_(policy.parameters(), max_norm=0.5)
optimizer.step()
루프의 모양이 지도학습과 똑같아 보이는 것이 함정이다. 지도학습에서는 데이터가 밖에 고정되어 있고 모델만 바뀌지만, 여기서는 모델이 바뀌면 다음 루프의 데이터가 바뀐다. 첫 줄에서 궤적을 모으는 정책이 바로 지금 학습 중인 그 정책이기 때문이다. 이 되먹임이 강화학습의 거의 모든 어려움을 만든다.
그리고 이 손실을 지도학습의 손실과 같은 것으로 읽으면 안 된다. 지도학습의 손실은 값 자체가 의미를 갖지만, 여기서는 이 값이 무엇을 재는 것도 아니다. 미분했을 때 원하는 경사가 나오도록 만든 대리 목적일 뿐이라, 이 숫자가 내려가는지 올라가는지를 학습이 잘 되는 신호로 읽으면 곤란하다. 봐야 하는 것은 언제나 에피소드당 보상이다.
베이스라인
분산이 큰 이유
유도는 깔끔한데 실제로 돌려 보면 학습이 심하게 흔들린다. 추정량의 분산이 크기 때문이고, 원인은 두 가지가 겹친 것이다.
하나는 리턴이 궤적 전체에서 오는 값이라는 점이다. 200스텝짜리 에피소드에서 어느 한 행동이 결과에 기여한 몫은 작은데, 곱해지는 는 그 뒤에 일어난 모든 일의 합이라 대부분이 그 행동과 무관한 잡음이다. 다른 하나는 보상의 부호다. CartPole처럼 매 스텝 을 주는 환경에서는 리턴이 언제나 양수라, 어떤 행동을 했든 그 확률이 전부 올라간다. 나쁜 행동도 덜 올라갈 뿐 내려가지 않는다. 방향이 아니라 크기 차이로만 학습하는 셈이라 신호 대비 잡음이 나쁘다.
이 둘이 겹치면 어떤 일이 벌어지는지는 숫자를 하나 넣어 보면 분명하다. 같은 상태에서 서로 다른 세 행동을 했는데 리턴이 각각 102, 100, 98로 나왔다고 하자. 행동의 좋고 나쁨이 실제로 담긴 몫은 ±2뿐인데 곱해지는 값은 100 근처이므로, 경사의 98%는 세 행동을 똑같이 밀어 올리는 데 쓰이고 정작 셋을 가르는 정보는 2%에 묻힌다. 표본이 늘면 평균이 이 공통 성분을 지워 주기는 하지만, 지워질 때까지 필요한 표본 수가 그만큼 많아진다는 뜻이다.
빼도 편향이 안 생기는 이유
해법은 상태에만 의존하는 값 를 리턴에서 빼는 것이다.
빼도 되는 근거는 한 줄로 나온다. 어떤 상태에서든 행동에 대한 로그 확률 미분의 기대값이 0이기 때문이다.
확률의 합이 언제나 1이라 그 미분이 0이라는 것이 전부다. 가 행동과 무관하므로 기대값 밖으로 빠져나오고, 남는 것에 0이 곱해져 사라진다. 기대값은 그대로인데 분산만 바뀐다 — 공짜로 얻는 것이 드문 분야에서 드문 공짜다.
표준화와 어드밴티지
가장 간단한 베이스라인은 배치 안 리턴의 평균이다. 여기에 표준편차로 나누기까지 하면 크기까지 고르게 잡힌다.
returns = (returns - returns.mean()) / (returns.std() + 1e-8)
두 줄이 아니라 한 줄에 서로 다른 두 가지가 들어 있다는 점은 알아 둘 만하다. 평균을 빼는 것은 위에서 본 베이스라인이고, 표준편차로 나누는 것은 경사의 크기를 조절하는 일이라 사실상 학습률을 보상 규모에 맞춰 자동으로 바꾸는 효과다. 그리고 엄밀히 말하면 같은 배치의 표본으로 평균을 냈으므로 완전히 편향이 없지는 않다. 배치가 아주 작을 때 이 편향이 보이지만, 실무에서 그것 때문에 문제가 되는 경우는 드물다.
더 나은 베이스라인은 상태 가치 다. 이때 가 어드밴티지, 곧 「이 행동이 그 상태의 평균보다 얼마나 나았는가」가 된다. 를 별도 신경망으로 함께 학습시키는 구조가 곧 액터-크리틱이다.
연속 행동 정책
가우시안 출력
연속 행동에서는 신경망이 행동을 직접 내놓는 대신 분포의 파라미터를 내놓는다. 가장 흔한 것이 가우시안이고, 출력은 평균 와 표준편차 다. 행동은 거기서 뽑고, 경사에 필요한 로그 확률은 가우시안 밀도의 로그로 계산한다.
from torch.distributions import Normal
mean = self.mean_head(features)
std = self.log_std.exp()
dist = Normal(mean, std)
action = dist.rsample() # 미분 가능한 샘플링
log_prob = dist.log_prob(action).sum(-1) # 차원별 로그 확률의 합
표준편차를 직접 출력하지 않고 로그를 출력해 지수를 취하는 것이 관행이다. 표준편차는 양수여야 하는데 신경망 출력은 음수가 될 수 있고, 지수를 씌우면 그 제약이 저절로 지켜진다.
σ를 어디에 둘 것인가
설계에서 갈리는 지점은 를 상태에 따라 바꿀 것인가다. 두 선택지가 각각 다른 것을 가정한다.
- 상태와 무관한 학습 파라미터 — 탐색의 폭이 학습 진행에 따라서만 줄어든다. 구현이 단순하고 초기 학습이 안정적이다.
- 상태 의존 출력 — 상황마다 확신의 정도가 다르다고 보는 것이다. 표현력이 크지만 학습 초기에 가 급히 작아지면 탐색이 죽는다.
로봇 제어 계열의 표준 구현이 대체로 앞쪽을 기본값으로 두는 이유가 그 실패 모드 때문이다. 가 작아지는 것은 되돌리기 어렵다 — 좁은 분포에서 뽑은 행동만 보게 되므로 밖이 더 좋다는 증거를 얻을 길이 없다. 그래서 상태 의존으로 두더라도 하한을 걸어 둔다.
tanh 스쿼싱과 로그 확률 보정
실제 환경의 행동에는 범위가 있다. 관절 토크가 이라면 가우시안에서 뽑은 값을 그대로 쓸 수 없다. 흔한 처리는 로 눌러 범위 안에 넣는 것이다.
여기에 자주 빠뜨리는 보정이 하나 따라온다. 변수를 변환하면 확률 밀도가 그대로 옮겨 가지 않는다. 변환의 야코비안만큼 밀도가 늘어나거나 줄어들므로, 로그 확률에서 그 몫을 빼 줘야 한다.
이 항을 빼먹으면 경사가 조용히 틀린 채로 학습이 진행된다. 에러도 없고 학습이 아예 안 되지도 않아서 찾기가 어렵다. 증상은 행동이 범위 끝에 달라붙는 것이다 — 의 기울기가 0에 가까운 자리에서 보정이 커야 하는데 그것이 빠졌으니, 모델이 끝으로 가는 것을 벌하지 않기 때문이다. 구현상 는 수치적으로 0에 가까워지므로 작은 값을 더해 로그를 안전하게 만든다.
탐색과 시간 지평
엔트로피 보너스
정책 경사는 스스로를 좁히는 경향이 있다. 어떤 행동이 우연히 좋은 리턴을 받으면 그 확률이 올라가고, 확률이 올라가면 더 자주 뽑혀 더 자주 보상을 받는다. 다른 행동은 뽑히지 않으니 그것이 더 좋았을지 알 방법이 없다. 이 되먹임이 빠르면 정책이 몇백 에피소드 만에 결정론적으로 굳어 버린다.
그래서 손실에 정책 분포의 엔트로피를 보너스로 더한다. 분포가 평평할수록 이득을 주는 항이므로 성급하게 뾰족해지는 것을 벌한다. 이산 행동에서는 계수 0.01 근처가 흔한 출발점이고, 학습이 진행되면서 0으로 감쇠시킨다. 계수가 크면 정책이 끝까지 무작위에 가깝게 남고, 없으면 조기에 굳는다. 어느 쪽인지 판별하는 방법은 엔트로피 값 자체를 매 에피소드 기록하는 것이다 — 보상보다 먼저 무너지는 것이 보통 이 값이다.
할인율이 정하는 지평선
할인율 는 미래 보상을 얼마나 깎을지 정하는 값으로 소개되지만, 실제로 정하는 것은 모델이 몇 스텝 앞까지 신경 쓰는가다. 가중치가 로 줄어드는 기하급수의 합이 이므로 그 값이 대략적인 지평선이 된다.
| 실질 지평선 | |
|---|---|
| 0.9 | 10스텝 |
| 0.99 | 100스텝 |
| 0.999 | 1,000스텝 |
에피소드가 500스텝인데 를 두면 10스텝 뒤의 결과만 보고 판단하는 근시안이 되고, 반대로 100스텝짜리 과제에 를 두면 잡음까지 멀리서 끌어와 분산만 키운다. 과제의 실제 길이를 먼저 세어 보고 고르는 값이다.
값을 고르기 어려운 자리가 하나 있다. 보상이 아주 드물게, 그것도 에피소드 끝에서만 주어지는 과제다. 지평선을 짧게 잡으면 그 보상이 앞쪽 행동까지 닿지 않아 학습 신호가 아예 없고, 길게 잡으면 닿기는 하는데 중간의 무관한 행동들에도 같은 몫이 배분되어 무엇이 기여했는지가 흐려진다. 이 긴장은 할인율만으로는 풀리지 않고, 중간 보상을 설계해 넣거나 되돌아보며 목표를 다시 매기는 별도의 장치를 쓴다.
보상 스케일
같은 알고리즘이 보상 규모만 달라져도 전혀 다르게 학습한다. 스텝마다 을 주는 환경과 을 주는 환경은 경사의 크기가 100배 차이 나고, 이는 학습률을 100배 올린 것과 같다. 그래서 보상을 설계하는 쪽에서 규모를 맞추거나, 앞에서 본 리턴 표준화로 받는 쪽에서 맞춘다. 둘 다 안 하면 환경을 바꿀 때마다 학습률을 새로 찾게 된다.
REINFORCE가 흔들리는 자리
시드마다 갈리는 곡선
REINFORCE를 직접 돌려 보면 가장 먼저 놀라는 것이 재현성이다. 코드도 하이퍼파라미터도 그대로 두고 난수 시드만 바꿔 다섯 번 돌리면, 어떤 시드는 목표 점수에 도달하고 어떤 시드는 같은 에피소드 수 안에 거의 오르지 않는 일이 흔하다. 알고리즘이 고장 난 것이 아니라 초기 정책에서 뽑힌 초반 궤적이 운에 좌우되고, 그 운이 앞에서 본 자기 강화 되먹임을 타고 증폭되기 때문이다.
그래서 이 계열의 결과는 한 번 돌린 곡선으로 말할 수 없다. 시드를 최소 다섯에서 열까지 바꿔 돌리고, 평균 하나가 아니라 중앙값과 퍼짐을 함께 적는 것이 관행이 됐다. 「좋아졌다」를 주장하려면 그 퍼짐보다 큰 차이여야 한다.
샘플 효율
두 번째 약점은 표본을 한 번 쓰고 버린다는 것이다. REINFORCE는 현재 정책으로 모은 궤적으로만 경사를 계산할 수 있어서, 업데이트를 한 번 하고 나면 그 데이터는 다른 정책의 것이 되어 쓸 수 없다. DQN이 리플레이 버퍼로 같은 경험을 수십 번 재사용하는 것과 대조된다.
에피소드가 끝나야 리턴을 계산할 수 있다는 제약도 함께 붙는다. 한 에피소드가 수천 스텝이면 그 시간 동안 업데이트가 한 번도 없다. 이 둘이 겹쳐 정책 경사 계열의 실험은 가치 기반보다 훨씬 많은 환경 상호작용을 요구한다.
그래도 이 계열이 널리 쓰이는 것은 환경 상호작용이 싼 경우가 많기 때문이다. 시뮬레이터는 병렬로 수백 개를 동시에 돌릴 수 있어서, 표본을 버리는 비효율을 개수로 메울 수 있다. 실제 로봇처럼 상호작용 하나가 비싼 자리에서는 이야기가 달라지고, 그래서 그쪽에서는 오프폴리시 방법이나 모델 기반 방법을 먼저 본다. 알고리즘을 고르는 기준은 성능 표가 아니라 내 환경에서 한 스텝이 얼마짜리인가다.
무엇을 기록할 것인가
디버깅에 필요한 값은 정해져 있다. 에피소드 보상, 정책 엔트로피, 경사의 노름, 그리고 연속 행동이면 의 평균이다. 보상만 보고 있으면 학습이 멈췄을 때 원인을 고를 수 없는데, 이 넷을 함께 그려 두면 대개 한 줄로 갈린다 — 엔트로피가 먼저 0으로 내려갔으면 탐색이 죽은 것이고, 경사 노름이 튀었으면 한 번의 큰 스텝이 정책을 망가뜨린 것이다.
경사 노름은 기록만 할 것이 아니라 잘라 두는 편이 낫다. 리턴이 우연히 큰 궤적 하나가 배치에 들어오면 그 스텝의 경사가 평소의 몇십 배가 되는데, 정책 경사에서는 앞서 말한 이유로 그 한 번이 되돌릴 수 없는 손상이 된다. 노름의 상한을 정해 두고 넘으면 비례해 줄이는 처리가 표준이 된 것은 성능을 올리려는 것이 아니라 최악을 막으려는 것이다.
신뢰 영역으로 가는 다리
한 번의 큰 스텝
마지막 실패 모드가 가장 아프다. 지도학습에서 학습률을 크게 잡으면 손실이 튀지만 데이터는 그대로 남아 있어 회복할 수 있다. 정책 경사에서는 그렇지 않다. 파라미터가 크게 움직여 정책이 나빠지면 그 나빠진 정책으로 다음 데이터를 모은다. 좋은 궤적을 더는 만나지 못하니 그것을 되돌릴 신호도 못 얻는다. 한 번의 큰 스텝이 되돌릴 수 없는 이유다.
스텝 크기를 어디서 잴 것인가
문제의 뿌리는 파라미터 공간에서의 거리와 정책 공간에서의 거리가 비례하지 않는다는 데 있다. 를 같은 크기만큼 움직여도 어떤 자리에서는 행동 분포가 거의 그대로이고 어떤 자리에서는 완전히 달라진다. 소프트맥스 출력이 이미 뾰족한 상태에서는 작은 변화가 확률을 통째로 뒤집는다.
그래서 다음 세대의 방법들은 스텝 크기를 파라미터가 아니라 정책 분포의 변화량으로 잰다. 업데이트 전후 정책의 KL 발산을 일정 범위 안으로 묶어 두는 것이 신뢰 영역의 발상이고, 그 제약을 정직하게 푸는 대신 확률비를 잘라 내는 값싼 근사가 PPO다.
이 관점이 부수적으로 푸는 문제가 하나 더 있다. 변화량을 재고 묶을 수 있으면 같은 데이터를 여러 번 써도 안전한 범위가 생긴다. 앞 절에서 본 「표본을 한 번 쓰고 버린다」는 제약이 여기서 느슨해지는 것이다 — 정책이 데이터를 모은 그 정책에서 너무 멀어지지 않는 동안에는 같은 배치로 업데이트를 몇 번 더 해도 된다. 분산 문제와 샘플 효율 문제가 서로 다른 문제처럼 보였는데 해법이 한자리에서 나오는 셈이다.
RLHF와의 연결
이 절의 이야기가 언어 모델과 무관해 보이지 않는 이유가 있다. 사람의 선호로 언어 모델을 학습시키는 RLHF가 정확히 이 틀 위에 서 있기 때문이다. 대응은 한 줄로 읽힌다 — 상태는 지금까지의 프롬프트와 생성한 토큰이고, 행동은 다음 토큰이고, 정책은 언어 모델 자신이며, 보상은 사람의 선호를 학습한 별도 모델이 매기는 점수다.
그러면 앞에서 본 문제들이 그대로 따라온다. 보상이 답 전체에 한 번만 주어지므로 리턴이 궤적 끝에서 오고, 그래서 베이스라인이 필요하다. 정책이 스스로를 좁히는 되먹임도 같아서, 보상 모델이 높게 치는 몇 가지 표현으로 답이 수렴해 버리는 현상이 실제로 관찰된다. 무엇보다 한 번의 큰 스텝이 되돌릴 수 없다는 성질이 같다. RLHF가 PPO를 쓰고 원래 모델과의 KL 발산에 벌점을 거는 것은 이 글에서 본 이유들의 직접적인 귀결이다.
다음으로
정리하면 이 글에서 얻은 것은 둘이다. 정책을 직접 미분할 수 있게 해 주는 정리 하나, 그리고 그 추정량을 쓸 만하게 만드는 장치들 — 베이스라인·엔트로피·할인율·행동 범위 처리다. 남은 두 문제는 표본을 한 번밖에 못 쓴다는 것과 한 스텝이 너무 클 수 있다는 것이며, 둘 다 같은 자리에서 풀린다. 다음 글에서는 베이스라인을 신경망으로 함께 학습시키는 액터-크리틱과, 확률비를 잘라 스텝을 묶는 PPO를 다룬다.
읽어주셔서 감사합니다. 😊

