GRPO 학습 루프에서 점수는 그대로 쓰이지 않습니다. 한 프롬프트에 답을 여러 개 뽑고, 그 묶음 안에서 점수를 표준화한 뒤에야 손실로 갑니다.
rewards = reward_fn(prompts, completions).view(-1, G) # (배치, 그룹 크기)
adv = (rewards - rewards.mean(-1, keepdim=True)) / (rewards.std(-1, keepdim=True) + 1e-4)
loss = -(logp * adv.detach().view(-1)).mean()
지난 글에서 우리는 를 얻었고, 저 자리에는 이 들어가야 했습니다. 그런데 코드는 이 아니라 에서 무언가를 뺀 값을 넣고, 그것을 다시 무언가로 나누고 있습니다. 점수를 두 번이나 바꿔 놓았는데 왜 여전히 같은 것을 배울까요.
지난 글 끝에서 본 관찰이 이 질문의 절반입니다. 세 점수 에 상수 50을 더해도 참 그래디언트는 한 자리도 바뀌지 않는데 같은 정확도에 필요한 표본은 7,400배가 되었습니다. 신호는 그대로인데 잡음만 커지는 자유도가 있다는 뜻이고, 그렇다면 반대로 잡음만 줄이는 방향으로도 그 자유도를 쓸 수 있어야 합니다.
이 글은 그것을 정리로 못 박습니다 — 무엇을 빼도 되는가, 무엇을 빼면 안 되는가, 어떤 것을 빼는 것이 가장 좋은가입니다. 마지막 절에서는 뺄셈이 아니라 나눗셈, 곧 코드의 두 번째 조작이 이 정리 밖에 있다는 것까지 봅니다. 이 결과를 알고리즘으로 조립하는 일은 액터-크리틱과 PPO과 추론 모델의 강화학습이 맡습니다.
점수 함수의 기댓값 0
세 걸음 증명
모든 것이 다음 한 줄에서 나옵니다.
를 점수 함수 라 부릅니다. 어떤 답 가 나왔을 때 파라미터를 어느 방향으로 밀면 그 답이 더 잘 나오는지를 가리키는 벡터입니다. 그 벡터의 평균이 0이라는 것이 이 절의 주장이고, 증명은 세 걸음이며 두 번째 걸음이 지난 글의 로그 미분 트릭입니다.
마지막 줄이 핵심입니다. 확률은 무엇을 하든 합이 1이므로, 그 합을 로 미분하면 상수를 미분한 것이라 0입니다. 를 어느 방향으로 밀든 어떤 답의 확률이 오르면 다른 답의 확률은 그만큼 내려간다는 사실을 식으로 적은 것입니다.
세 번째 걸음에 단서가 하나 붙습니다. 미분 기호를 합 기호 앞으로 빼냈는데, 이 교환이 늘 되는 것은 아닙니다. 답이 유한 개면 합이 유한 개 항의 덧셈이라 아무 문제가 없지만, 답이 연속이면 합이 적분이 되고 미분과 적분의 순서를 바꿀 수 있어야 합니다. 언어 모델은 어휘가 유한하고 길이도 유한하니 답의 가짓수가 유한해 이 조건이 저절로 만족됩니다. 연속 행동을 다루는 제어 문제에서는 확률밀도가 에 대해 매끄럽고 적분이 유계라는 정칙 조건을 따로 확인해야 합니다.
세 답으로 직접 더해 보기
식이 아니라 수로 보면 더 분명합니다. 답이 셋이고 로짓이 인 소프트맥스 정책을 놓습니다. 확률은 이고, 이 정책에서 점수 함수는 원핫 벡터에서 확률 벡터를 뺀 것입니다.
답마다 적어 보면 이렇습니다.
| 나온 답 | 점수 함수 | 확률 |
|---|---|---|
| 1번 | 0.4123 | |
| 2번 | 0.3376 | |
| 3번 | 0.2501 |
첫 성분만 확률로 가중해 더해 봅니다.
둘째 성분은 , 셋째 성분은 로 셋 다 0입니다. 표를 세로로 읽으면 이유가 보입니다. 어느 열에서든 양수는 한 칸뿐이고 나머지 두 칸은 음수인데, 그 한 칸이 클수록 확률 가중치는 작습니다. 정확히 상쇄되도록 짜여 있는 것이 아니라, 확률의 합이 1이라는 제약이 그렇게 만듭니다.
피셔 정보라는 이름
평균이 0이면 2차 모멘트가 곧 공분산입니다. 점수 함수의 공분산 행렬에는 이미 이름이 있습니다.
이것이 피셔 정보 행렬 입니다. 정책을 조금 흔들었을 때 분포가 얼마나 달라지는지를 재는 값이고, 위 예에서 대각합은 0.6535입니다.
이 이름을 여기서 꺼내 두는 것은 다음 절 때문입니다. 분산을 계산하면 이라는 가중치가 나오는데, 그 기댓값이 바로 피셔 정보 행렬의 대각합입니다. 정책 그래디언트의 분산이 크다는 말과 정책이 파라미터 변화에 민감하다는 말이 같은 양을 가리키고 있다는 뜻입니다.
빼도 되는 것의 조건
프롬프트에만 의존하는 기준
가 와 무관한 값이면
이므로, 대신 를 넣어도 기댓값이 변하지 않습니다.
이 를 베이스라인이라 부르고, 빼고 남은 값 를 어드밴티지라 부릅니다. 「이 답이 기준보다 얼마나 나았는가」라는 뜻입니다.
베이스라인이 진짜 상수일 필요는 없습니다. 위 계산에서 에 요구한 것은 하나뿐입니다 — 기댓값을 취하는 변수 에 의존하지 않을 것입니다. 프롬프트 는 기댓값 바깥에 있으므로 가 의 함수여도 됩니다.
안쪽 기댓값에서 는 상수라 그대로 밖으로 나오고, 남은 것이 0입니다. 그래서 프롬프트마다 다른 기준을 써도 됩니다. 쉬운 문제에는 높은 기준을, 어려운 문제에는 낮은 기준을 두는 것이 자연스럽고, 실제로 유용한 것도 이쪽입니다. 상태의 기대 보상 를 따로 학습해 베이스라인으로 쓰는 방식이 여기서 나옵니다.
반대로 가 에 의존하면 정리가 무너집니다. 극단적인 예가 분명합니다. 로 두면 어드밴티지가 항상 0이라 추정량도 항상 0이고, 참 그래디언트는 0이 아니었으므로 완전히 틀린 값입니다. 분산은 완벽하게 0이 되었지만 얻은 것이 아무것도 없습니다. 분산을 줄이는 일과 답을 지우는 일은 종이 한 장 차이이고, 그 종이 한 장이 「 에 의존하지 않을 것」입니다.
부정확한 기준도 편향을 만들지 않는다
를 신경망으로 학습해 쓴다면 그 값은 당연히 부정확합니다. 학습 초기에는 엉뚱한 수를 내놓기도 합니다. 그래도 추정량은 불편입니다.
위 유도 어디에도 「 가 참값에 가깝다」는 가정이 없기 때문입니다. 조건은 정확함이 아니라 와 무관함 입니다. 가 얼마나 틀렸든 그 값은 를 뽑기 전에 정해져 있고, 그러면 기댓값 안에서 상수로 빠져나옵니다.
틀린 만큼 치르는 값은 편향이 아니라 분산입니다. 다음 절에서 볼 총분산은 의 이차식이라 최적점에서 멀어질수록 커지고, 정확히 그만큼 표본이 더 듭니다. 크리틱의 품질이 학습 속도를 좌우하되 학습의 목표는 안 흔드는 것이 이 구조입니다.
같은 이유로 를 스텝마다 바꿔도 됩니다. 각 스텝의 기댓값을 계산할 때 그 스텝의 는 이미 정해진 수이므로, 스텝마다 다른 값을 써도 매번 불편인 추정량을 얻습니다.
θ에 딸린 값과 detach
한 가지가 더 남습니다. 학습한 가 아니라 정책 자신의 파라미터에 딸린 값을 베이스라인으로 쓰면, 불편성은 그대로인데 코드가 달라집니다.
가 의 함수여도 에 의존하지 않으면 이라 위 유도가 그대로 성립합니다. 문제는 우리가 그래디언트를 손으로 적지 않고 손실을 자동미분에 맡긴다는 데 있습니다. 손실 를 그대로 미분하면 곱의 미분 규칙이 적용되어 를 지나는 항이 하나 더 생깁니다. 그 항은 우리가 원한 적 없는 방향이고, 원한 적 없으니 이라는 보장도 없습니다.
맨 앞 코드의 adv.detach()가 그 자리를 끊습니다. 어드밴티지를 미분 그래프에서 떼어 내 숫자로만 쓰겠다는 선언이고, 그래야 손실을 미분한 결과가 우리가 유도한 식과 같아집니다. 어드밴티지를 계산하면서 no_grad 블록을 쓰거나 점수를 미리 텐서로 만들어 두는 구현도 같은 일을 합니다.
최적 베이스라인
총분산의 포물선
빼도 되는 것이 많으니 이제 가장 좋은 것을 고를 수 있습니다. 총분산 — 성분별 분산의 합 — 을 의 함수로 적어 봅니다. 평균은 와 무관하게 이므로 움직이는 것은 2차 항뿐입니다.
에 대해 미분해 0으로 놓습니다.
가 에 대해 아래로 볼록한 이차식이므로 이 자리가 최소입니다. 읽으면 이렇습니다 — 최적 베이스라인은 그냥 점수의 평균이 아니라, 로 가중한 점수의 평균입니다. 그래디언트를 크게 흔드는 답의 점수에 더 큰 몫을 줍니다.
앞 절의 세 답 예에서 그 가중치를 실제로 적어 보면 입니다. 확률이 낮은 답일수록 크다는 것이 눈에 띕니다. 드물게 나오는 답일수록 그것이 나왔을 때 파라미터를 크게 흔들기 때문이고, 그래서 최적 베이스라인은 드문 답 쪽으로 조금 끌려갑니다.
성분마다 다른 최적값
위에서 최소화한 것은 성분별 분산을 다 더한 하나의 수입니다. 그런데 정책 그래디언트는 벡터이고, 성분마다 따로 최적화하면 답이 달라집니다.
파라미터마다 다른 베이스라인을 쓰는 것이고, 각 성분의 분산을 따로 최소로 만드니 총분산도 스칼라 하나를 쓸 때보다 작거나 같습니다. 그런데도 실무에서 이렇게 하지 않습니다. 파라미터가 수십억 개면 베이스라인도 수십억 개를 들고 다녀야 하고, 그 각각을 추정하는 데 드는 표본이 얻는 것보다 큽니다.
가중치 이 답마다 고르면 스칼라 최적값 이 그냥 점수의 평균 과 같아집니다. 가중평균에서 가중치가 같으면 산술평균이 되기 때문입니다. 성분별 최적값도 마찬가지로 전부 그 값으로 모입니다. 실제 정책에서 가중치가 완전히 고르지는 않지만 크게 벌어지지도 않는다는 것이 다음 소절의 수치입니다.
그냥 평균을 쓰는 이유
숫자로 보겠습니다. 지난 글의 장난감 문제에서 점수만 로 옮겨 놓은 것입니다.
import numpy as np
th = np.array([0.3, 0.1, -0.2])
e = np.exp(th - th.max()); p = e / e.sum()
S = np.eye(3) - p # 각 답에서의 ∇log π
R = np.array([6.0, 5.0, 4.0])
def var(b):
G = (R - b)[:, None] * S # 어드밴티지를 곱한 추정량 값
m = p @ G
return float((p @ (G ** 2) - m ** 2).sum())
s2 = (S ** 2).sum(1)
bstar = (p * R * s2).sum() / (p * s2).sum()
print(f"E[r] = {p @ R:.4f} b* = {bstar:.4f}")
for b in (0, 2, 4, 5, 6, 8, 10):
print(f" b={b:>2} 총분산 {var(float(b)):8.4f}")
print(f" b=b* 총분산 {var(float(bstar)):8.4f}")
print(f" b=E[r] 총분산 {var(float(p @ R)):8.4f}")
# E[r] = 5.1622 b* = 5.0054
# b= 0 총분산 16.5922
# b= 2 총분산 6.1225
# b= 4 총분산 0.8806
# b= 5 총분산 0.2200
# b= 6 총분산 0.8665
# b= 8 총분산 6.0802
# b=10 총분산 16.5218
# b=b* 총분산 0.2200
# b=E[r] 총분산 0.2361
베이스라인을 안 쓰면 16.59, 최적으로 쓰면 0.22입니다. 75배 차이이고, 필요한 표본 수로 치면 그대로 75배입니다. 그리고 마지막 두 줄이 실무에서 중요한 사실을 말해 줍니다. 최적값은 이지만 그냥 점수 평균 를 써도 분산이 0.2361로 거의 같습니다. 최적값을 쓰려면 을 답마다 계산해야 하는데, 그 수고에 비해 남는 것이 7% 정도라는 뜻입니다.
이유는 포물선의 모양에 있습니다. 는 의 이차식이라 최소점 근처에서 평평하고, 최소점에서 만큼 벗어났을 때 늘어나는 양은 가 아니라 에 비례합니다. 위 표에서 와 은 최적값에서 각각 0.005와 1.0만큼 떨어져 있는데 분산은 0.2200과 0.8665입니다. 반대로 최적점에서 멀어지면 대가가 급격히 커집니다 — 은 5만큼 벗어난 것이고 분산은 75배입니다.
표본으로 추정한 평균을 쓰면 그 평평함이 한 번 더 중요해집니다. 우리가 실제로 빼는 것은 참 평균이 아니라 개의 표본 평균이라 그 자체가 흔들리고, 흔들리는 폭은 가 작을수록 큽니다. 포물선이 뾰족했다면 그 흔들림만큼 이득이 깎였겠지만 바닥이 넓어서 거의 깎이지 않습니다. 대신 표본 평균을 쓰면 가 에 딸리게 되어 다음 절의 어긋남이 생깁니다.
분산의 바닥
베이스라인으로 내릴 수 있는 데는 한계가 있습니다. 를 최적으로 골라도 남는 분산이 있고, 위 예에서 그 값이 0.2200입니다.
남는 이유는 하나입니다. 베이스라인은 모든 답에서 같은 값을 빼는 평행이동 이라, 답마다 다른 것은 손대지 못합니다. 점수 는 어느 수를 빼도 서로 1씩 떨어져 있고, 그 차이가 추정량을 흔드는 몫은 그대로 남습니다. 분산이 0이 되려면 어드밴티지가 답마다 같아야 하는데 그러면 배울 것이 없습니다.
바닥을 더 내리려면 다른 손잡이가 필요합니다. 같은 답을 여러 번 뽑아 평균 내는 것(표본을 늘리는 것), 답을 토큰 단위로 쪼개 각 토큰에 더 정확한 기준을 주는 것, 점수 자체를 덜 튀게 설계하는 것이 그런 손잡이입니다. 베이스라인은 그중 가장 싼 것이고 — 공짜에 가깝고 편향도 없고 — 그래서 먼저 씁니다.
GRPO의 그룹 평균
(1−1/G)배가 되는 계산
이제 맨 앞 코드로 돌아갈 수 있습니다. GRPO는 프롬프트 하나에 답을 개 뽑고 그 개의 점수 평균을 빼는데, 그 평균이 하려는 일은 정확히 의 추정입니다. 상태에만 의존하는 베이스라인이니 위 정리가 그대로 적용됩니다. 크리틱 신경망을 따로 학습하는 대신 같은 프롬프트에서 답을 여러 개 뽑아 그 자리에서 평균 내는 것으로 대신하는 것이고, 모델을 하나 덜 들고 다녀도 되는 것이 이 방식의 이점입니다.
다만 미묘한 자리가 하나 있습니다. 그룹 평균 안에는 지금 그래디언트를 계산하고 있는 그 답의 점수 도 들어 있습니다. 그러면 베이스라인이 에 의존하게 되어 위에서 「무너진다」고 한 조건에 걸립니다. 얼마나 어긋나는지 계산해 보면 깔끔한 답이 나옵니다.
인 항들은 와 독립이라 기댓값이 갈라지고 뒤쪽이 0이 되어 사라집니다. 남는 것은 자기 자신의 몫뿐이고, 따라서 추정량의 기댓값은
가 됩니다. 방향은 정확히 참 그래디언트이고 크기만 배입니다. 표본으로 확인해 보면 그대로입니다.
rng = np.random.default_rng(1)
exact = p * (R - p @ R)
for G in (2, 4, 8):
y = rng.choice(3, size=(400_000, G), p=p)
r, gl = R[y], S[y]
g_in = ((r - r.mean(1, keepdims=True))[..., None] * gl).mean(1).mean(0)
loo = (r.sum(1, keepdims=True) - r) / (G - 1) # 자기 자신만 뺀 평균
g_loo = ((r - loo)[..., None] * gl).mean(1).mean(0)
print(f"G={G} 그룹평균 {np.round(g_in, 4)} 이론값 {np.round((1 - 1/G) * exact, 4)}")
print(f" 제외평균 {np.round(g_loo, 4)} 참값 {np.round(exact, 4)}")
# G=2 그룹평균 [ 0.1734 -0.0275 -0.1459] 이론값 [ 0.1727 -0.0274 -0.1453]
# 제외평균 [ 0.3467 -0.055 -0.2917] 참값 [ 0.3454 -0.0548 -0.2907]
# G=4 그룹평균 [ 0.259 -0.0412 -0.2178] 이론값 [ 0.2591 -0.0411 -0.218 ]
# 제외평균 [ 0.3454 -0.0549 -0.2905] 참값 [ 0.3454 -0.0548 -0.2907]
# G=8 그룹평균 [ 0.3023 -0.048 -0.2543] 이론값 [ 0.3023 -0.0479 -0.2543]
# 제외평균 [ 0.3454 -0.0548 -0.2906] 참값 [ 0.3454 -0.0548 -0.2907]
그룹 평균 쪽은 세 모두 이론값과 소수 셋째 자리까지 맞습니다. 방향이 같고 크기만 줄어드는 어긋남이라 학습률을 흡수해 버리는 종류이고, 이면 12.5%입니다. 그래도 완전히 없애고 싶으면 자기 자신을 뺀 나머지 개의 평균을 기준으로 쓰면 됩니다 — 위 코드의 loo가 그것이고, 세 경우 모두 참값과 맞습니다.
그룹의 점수가 전부 같을 때
이 방식에는 크기 축소보다 훨씬 아픈 자리가 따로 있습니다. 한 그룹의 점수가 전부 같으면 그 그룹의 어드밴티지가 통째로 0입니다. 평균이 곧 그 값이라 이 모두 0이 되고, 그 프롬프트는 이번 스텝에 아무것도 보태지 않습니다. 답을 개나 생성한 비용은 이미 치렀는데 얻은 신호가 없습니다.
맞고 틀림 둘뿐인 채점에서 이 일이 자주 납니다. 한 답이 맞을 확률을 라 하고 답들이 서로 독립이라 보면, 그룹이 전부 같은 값일 확률은 입니다.
| 정답률 | |||
|---|---|---|---|
| 0.5 | 12.5% | 0.8% | 0.0% |
| 0.7 | 24.8% | 5.8% | 0.3% |
| 0.9 | 65.6% | 43.1% | 18.5% |
| 0.95 | 81.5% | 66.3% | 44.0% |
읽는 법이 중요합니다. 모델이 잘 푸는 문제일수록 버려지는 비율이 높습니다. 에서 정답률이 0.9인 문제는 43%가 헛돌고, 0.95면 66%입니다. 아직 못 푸는 문제도 마찬가지라 정답률 0.05면 똑같이 66%입니다. 가장 많은 신호를 주는 것은 반쯤 푸는 문제 이고, 그래서 학습 데이터에서 너무 쉬운 문제와 너무 어려운 문제를 걸러 내거나 통과율을 보고 난이도를 맞추는 손질이 뒤따릅니다.
G를 키우는 맞바꿈
를 키우면 이 절에서 본 두 가지가 동시에 나아집니다. 크기 축소는 이니 에서 25%, 에서 12.5%, 에서 6.25%로 줄고, 통째로 버려지는 그룹의 비율도 위 표대로 줄어듭니다. 표본 평균이 참 평균 주위에서 흔들리는 폭도 가 클수록 작아집니다.
대신 비용이 그대로 배입니다. 한 프롬프트마다 답을 개 생성해야 하고, 생성이 학습 루프에서 가장 비싼 단계입니다. 같은 예산이면 를 두 배로 하는 것과 프롬프트를 두 배로 보는 것 중 하나를 골라야 합니다.
어느 쪽이 나은지는 어디에서 오는 잡음이 더 큰가에 달려 있습니다. 같은 프롬프트 안에서 답들의 점수가 크게 갈리면 를 키우는 쪽이 낫고, 프롬프트마다 난이도가 크게 다르면 프롬프트를 늘리는 쪽이 낫습니다. 실제 구현에서 를 4에서 16 사이로 잡는 것은 그 사이 어딘가에서 타협한 결과입니다.
점수의 축척이 하는 일
100배 하면 학습률 100배
맨 앞 코드에는 나눗셈이 하나 더 있었습니다. 그룹 표준편차로 나누는 것인데, 이것은 이 글의 정리가 보장하는 범위 밖입니다. 왜 밖인지 보려면 먼저 곱셈이 무엇을 하는지부터 봐야 합니다.
점수를 통째로 배 하면 어드밴티지도 배이고, 추정량도 배입니다.
기울기가 통째로 배가 된 것이니 그 방향으로 한 걸음 가는 일은 학습률을 배 한 것과 같습니다. 점수를 0에서 1로 주다가 0에서 100으로 바꾸면 하이퍼파라미터를 하나도 안 건드려도 걸음 크기가 100배가 됩니다. 채점 기준을 손보다가 학습이 발산했다면 이것이 먼저 의심할 자리입니다.
더하기와 곱하기의 갈림
같은 자리에서 더하기와 곱하기가 갈립니다.
| 점수에 한 일 | 참 그래디언트 | 무엇이 지켜 주는가 |
|---|---|---|
| 상수를 뺀다 | 그대로 | 이 글의 보조정리 |
| 상수를 곱한다 | 배 | 아무것도 |
빼기는 정리가 보장하지만 곱하기는 보장해 주는 것이 없습니다. 둘을 「점수를 정규화한다」는 한마디로 묶어 버리면 이 차이가 안 보입니다. 표준화는 빼기와 나누기를 한 줄에 적은 것이고, 앞의 반만 이 글의 정리 아래 있습니다.
나눗셈 쪽이 더 나쁜 점이 하나 더 있습니다. 그룹 표준편차는 에 의존하는 값이라, 곱해지는 상수가 프롬프트마다 다를 뿐 아니라 뽑힌 답에 따라서도 달라집니다. 그래서 이 조작은 상수배가 아니라 프롬프트마다 학습률을 다르게 주는 것 에 가깝습니다. 쉬운 문제와 어려운 문제의 걸음 크기를 맞추는 효과가 있어 널리 쓰이지만, 편향이 없다는 근거는 위 유도에서 나오지 않습니다.
분모의 1e−4
코드의 + 1e-4도 이 축척 이야기의 일부입니다. 앞 절에서 본 「점수가 전부 같은 그룹」에서는 표준편차가 정확히 0이라 그대로 나누면 값이 터집니다. 분자도 0이므로 수학적으로는 부정형이고, 부동소수점에서는 nan이 되어 그 스텝의 모든 파라미터를 오염시킵니다.
작은 수를 더해 두면 분자가 0인 채로 분모가 이 되어 어드밴티지가 0으로 떨어집니다. 신호가 없는 그룹이 신호가 없는 채로 지나가는 것이고, 이것이 의도한 동작입니다.
주의할 것은 표준편차가 0은 아닌데 아주 작은 경우입니다. 점수가 처럼 거의 같으면 표준편차가 언저리라 분모에서 더한 값과 같은 자릿수가 되고, 어드밴티지가 절반쯤으로 줄어듭니다. 의미 없는 차이를 크게 증폭하지 않으려는 것이라 이쪽도 대체로 의도한 동작이지만, 점수의 스케일을 바꾸면 이 경계도 함께 움직인다는 것은 알고 있어야 합니다. 점수를 100배로 키우면 은 사실상 없는 것과 같아집니다.
어드밴티지를 잘라 두는 변형
축척을 잡는 손잡이가 나눗셈만 있는 것은 아닙니다. 어드밴티지를 로 자르는 변형도 자주 쓰입니다. 표준편차로 나누는 것이 그룹 전체의 크기를 맞추는 것이라면, 자르기는 유독 튀는 답 하나가 스텝을 통째로 끌고 가는 것을 막습니다.
둘 다 분산을 줄이는 대신 편향을 들여옵니다. 자르기는 인 답에서만 값을 바꾸므로 그런 답이 드물면 편향도 작고, 대신 그 드문 답이 큰 신호를 지고 있었다면 그것을 깎는 것이기도 합니다. 뺄셈만이 이 글의 정리가 공짜로 허락하는 조작이고, 나머지는 전부 얼마쯤의 편향을 값으로 치릅니다.
다시 그 한 줄로
정리하면 맨 앞 코드의 첫 줄은 이렇게 읽힙니다. rewards.mean(-1)은 그 프롬프트의 를 그 자리에서 추정한 값이고, 그것을 빼는 것은 이라는 보조정리가 허락한 조작입니다. 남은 adv는 「같은 문제에 대한 다른 답들보다 이 답이 얼마나 나았는가」이고, 이 값이 양수인 답의 로그 확률을 올립니다. 이어지는 나눗셈은 정리가 보장하지 않는 추가 조작이고, .detach()는 그 값이 숫자로만 쓰이도록 미분 그래프를 끊습니다. 한 줄에 성격이 다른 셋이 들어 있었던 셈입니다.
베이스라인이 없으면 무슨 일이 벌어지는지도 이제 분명합니다. 채점자가 점수를 0에서 10 사이로 준다면 어드밴티지가 늘 양수라 모든 답의 확률을 올리려 듭니다. 확률의 합이 1이라 실제로 오르는 것은 상대적으로 더 많이 밀린 쪽뿐이니 학습이 되기는 하지만, 그 상대적인 차이가 큰 공통 성분에 묻혀 표본이 훨씬 많이 듭니다. 앞에서 본 16.59 대 0.22가 그 차이입니다.
다음 글부터는 점수 자체가 어디서 오는지로 넘어갑니다. 사람이 두 답을 비교해 하나를 고른 데이터만 있을 때 그것을 점수 함수로 바꾸는 방법입니다.
정리
- 점수 함수의 기댓값은 0이다 — . 확률의 합이 1이고 그 합을 미분하면 0이기 때문이다. 답이 연속이면 미분과 적분의 순서를 바꿀 수 있다는 조건이 붙지만, 어휘가 유한한 언어 모델에서는 저절로 만족된다.
- 평균이 0이라 2차 모멘트가 곧 공분산이고, 그 행렬이 피셔 정보다. 다음 절의 분산 가중치 의 기댓값이 그 대각합이다.
- 따라서 에 의존하지 않는 어떤 값 를 빼도 추정량은 여전히 불편이다. 이 가 베이스라인, 가 어드밴티지다.
- 프롬프트에만 의존하는 도 되고, 그 값이 부정확해도 된다 — 조건은 정확함이 아니라 와 무관함이다. 틀린 만큼은 편향이 아니라 분산으로 치른다. 반면 가 에 의존하면 무너진다 — 는 분산을 0으로 만들지만 추정량도 0으로 만든다.
- 가 에 딸린 값이면 불편성은 그대로지만 자동미분이 를 지나는 항을 하나 더 만든다.
adv.detach()가 그 자리를 끊는다. - 총분산은 의 이차식이고, 최소가 되는 곳은 다. 성분마다 따로 최적화하면 파라미터별로 다른 값이 나오지만, 수십억 개를 추정하는 비용이 이득보다 크다.
- 실무에서는 그냥 점수 평균을 쓴다. 장난감 문제에서 최적값이 5.0054, 점수 평균이 5.1622인데 분산은 0.2200 대 0.2361로 거의 같았다. 베이스라인을 아예 안 쓴 16.5922와 비교하면 75배 차이다. 포물선 바닥이 평평해서 근처면 충분하다.
- 베이스라인은 평행이동이라 답마다 다른 몫은 못 없앤다. 위 예에서 0.2200이 그 바닥이다.
- GRPO의 그룹 평균은 이 정리의 직접 적용이다. 크리틱 없이 같은 프롬프트의 답 개로 를 추정한다. 자기 점수가 평균에 섞여 있어 추정량이 정확히 가 되는데, 방향은 그대로이고 크기만 줄어든다. 자기를 뺀 개 평균을 쓰면 그 어긋남도 사라진다.
- 그룹의 점수가 전부 같으면 어드밴티지가 통째로 0이라 그 프롬프트가 헛돈다. 이진 채점에서 , 정답률 0.9면 43%가 그렇다. 반쯤 푸는 문제가 가장 많은 신호를 준다.
- 점수를 배 하면 그래디언트도 배라 학습률을 배 한 것과 같다. 빼기는 정리가 지켜 주지만 곱하기는 지켜 주지 않는다.
- 그룹 표준편차로 나누는 것은 이 정리 밖이다. 뺄셈이 아니라 나눗셈이고 에 의존하므로, 편향이 없다는 보장은 여기서 나오지 않는다. 분모의 은 점수가 전부 같은 그룹에서
nan을 막는다.
읽어주셔서 감사합니다. 😊

