지난 글에서 다룬 DPO는 미리 모아 둔 선호 쌍을 손실 함수에 넣는 방식이었다. 데이터가 고정되어 있으니 지도학습과 같은 모양으로 돌아가고, 그래서 다루기 쉬웠다. 하지만 그 방식으로는 할 수 없는 일이 하나 있다. 모델이 지금 실제로 내는 답에 점수를 매기고, 그 점수를 보고 바로 다음 답을 고치는 것이다. 채점 기준이 있는 과제 — 수학 답이 맞는지, 코드가 테스트를 통과하는지 — 에서는 이쪽이 훨씬 강하다.
그 자리를 오래 지켜 온 것이 PPO(Proximal Policy Optimization)다. 강화학습 알고리즘이고 RLHF의 표준이었다. 다만 무겁다. GRPO(Group Relative Policy Optimization)는 PPO에서 가장 무거운 부품 하나를 빼고 그 자리를 통계로 대신한 방법이다. DeepSeek이 수학·코드 모델을 학습하며 쓴 뒤로 널리 퍼졌다.
무엇을 지웠는가
강화학습에서 모델을 미는 힘은 어드밴티지(advantage)가 정한다. 「이 답이 기준보다 얼마나 나은가」를 재는 값이고, 이 값이 양수면 그 답이 나올 확률을 올리고 음수면 내린다. 보상을 그대로 쓰지 않고 기준과의 차이를 쓰는 이유는 단순하다. 어떤 프롬프트는 원래 쉬워서 아무 답이나 0.8점을 받고, 어떤 프롬프트는 어려워서 잘해야 0.3점이다. 보상을 그대로 쓰면 쉬운 프롬프트의 평범한 답이 어려운 프롬프트의 훌륭한 답보다 높게 평가된다.
문제는 그 기준을 어디서 얻느냐다.
PPO는 기준을 맞히는 모델을 따로 학습한다. 가치 모델(value model)이라 하고, 상태를 넣으면 앞으로 받을 보상의 기댓값을 내놓는다. 정확하지만 대가가 있다 — 보통 정책 모델과 비슷한 크기라서 GPU 메모리에 모델이 하나 더 올라가고, 그 모델 자체도 학습이 잘 안 되면 어드밴티지가 통째로 흔들린다.
GRPO는 그 모델을 만들지 않는다. 대신 같은 프롬프트로 답을 개 뽑고 그 개의 평균을 기준으로 쓴다. 같은 문제에서 나온 형제 답들끼리 비교하는 것이라, 프롬프트의 난이도가 자동으로 상쇄된다.
여기서 는 번째 답이 받은 보상이다. 평균을 빼는 것이 기준을 맞추는 일이고, 표준편차로 나누는 것은 프롬프트마다 점수 폭이 달라도 밀리는 크기가 비슷해지도록 맞추는 일이다.
정리하면 GRPO는 기준을 학습하는 대신 표본으로 추정한다. 가치 모델의 파라미터가 통계 한 줄로 바뀐 것이고, 그 대가로 같은 프롬프트를 번 생성해야 한다.
무리 크기를 얼마로 잡을 것인가
는 GRPO에서 가장 먼저 정하는 값이다. 이 값이 곧 기준선의 정확도이자 한 스텝의 비용이다.
| 무엇이 좋아지나 | 무엇이 나빠지나 | |
|---|---|---|
| 4 | 스텝이 싸다. 같은 시간에 프롬프트를 많이 본다 | 평균이 흔들려 어드밴티지에 잡음이 많다 |
| 8~16 | 대개 여기서 균형이 맞는다 | — |
| 32 이상 | 기준선이 매우 안정적이다 | 생성 비용이 그대로 곱해진다 |
보통 8에서 시작한다. 그리고 를 늘리는 것과 배치를 늘리는 것 중 무엇이 나은지는 과제의 성공률이 정한다. 성공률이 0.5 근처인 과제는 가 작아도 무리 안에 잘한 답과 못한 답이 섞이지만, 성공률이 0.05인 어려운 과제는 가 8이면 여덟 개 모두 실패하는 일이 잦다. 그러면 그 프롬프트는 학습에 아무 기여도 하지 못한다.
어드밴티지가 0이 되는 순간
이것이 GRPO의 대표적인 실패다. 무리의 보상이 전부 같으면 평균과의 차이가 0이므로 어드밴티지가 전부 0이 되고, 그 프롬프트에서 나오는 기울기가 사라진다.
전부 실패하는 쪽만 문제가 아니다. 전부 성공하는 프롬프트도 똑같이 쓸모없다. 학습이 진행되면 쉬운 프롬프트부터 전부 성공으로 넘어가므로, 아무 조치 없이 두면 시간이 갈수록 실제로 학습에 쓰이는 프롬프트 비율이 줄어든다. 겉으로는 보상 평균이 올라가는데 모델은 더 이상 안 변하는 상태가 된다.
대응은 셋이다.
- 비율을 지표로 본다. 「무리 안에서 보상이 갈린 프롬프트의 비율」을 매 스텝 찍는다. 이 값이 학습 진행을 가장 정직하게 보여 준다
- 난이도를 맞춰 준다. 전부 맞히는 프롬프트를 데이터에서 빼고 더 어려운 것을 넣는다. 학습 도중에 이 교체를 반복하는 방식을 커리큘럼이라 부른다
- 보상을 연속값으로 만든다. 0/1 이분법 대신 부분 점수를 주면 무리 안에서 순위가 갈린다. 다만 부분 점수를 잘못 설계하면 그것대로 문제가 되고, 이는 아래에서 다시 다룬다
참조 모델과 KL 항
DPO와 마찬가지로 GRPO도 학습 시작 시점의 모델 사본을 얼려 두고 참조 모델로 쓴다. 손실에는 정책과 참조 사이의 거리를 재는 항이 붙는데, 두 확률분포가 얼마나 다른지를 재는 값이라 KL 발산이라 부른다. 이 항이 없으면 보상만 높이는 방향으로 얼마든지 달아나서, 사람이 읽을 수 없는 문자열이 만점을 받는 상태에 도달한다.
가 크면 원래 모델에서 멀어지지 못하고, 작으면 빠르게 변하는 대신 다른 능력을 잃는다. 여기까지는 DPO의 와 성격이 같다.
다만 검증 가능한 과제에서는 를 아주 작게 두거나 0으로 두는 선택도 있다. 보상이 「테스트를 통과했는가」처럼 기계가 정확히 매기는 값이면, 보상만 높이려고 달아나 봐야 갈 곳이 없기 때문이다. 반대로 모델이 매기는 점수를 보상으로 쓸 때는 를 반드시 살려 둔다.
돌리는 모양
trl의 GRPOTrainer는 보상 함수를 파이썬 함수로 받는다. 정답 목록과 생성된 답을 받아 점수 리스트를 돌려주면 된다.
from datasets import load_dataset
from trl import GRPOConfig, GRPOTrainer
def reward_correct(completions, answer, **kwargs):
# 정답과 일치하면 1점, 아니면 0점
return [1.0 if extract(c) == a else 0.0
for c, a in zip(completions, answer)]
def reward_format(completions, **kwargs):
# 요구한 형식을 지켰으면 0.2점
return [0.2 if "<answer>" in c else 0.0 for c in completions]
config = GRPOConfig(
output_dir="out",
num_generations=8, # 무리 크기 G
learning_rate=1e-6,
beta=0.04, # KL 계수
max_completion_length=1024,
per_device_train_batch_size=8,
)
trainer = GRPOTrainer(
model="./sft-checkpoint",
reward_funcs=[reward_correct, reward_format],
args=config,
train_dataset=load_dataset("json", data_files="tasks.jsonl", split="train"),
)
trainer.train()
몇 가지 값에 이유가 있다.
per_device_train_batch_size는 무리 크기로 나누어떨어져야 한다. 한 무리가 쪼개져 서로 다른 배치에 들어가면 평균을 낼 대상이 사라진다. 이 조건은 대부분의 구현이 검사해 주지만, 값을 바꿀 때 가장 먼저 걸리는 곳이다.
학습률은 DPO보다도 작다. 대에서 시작한다. 스텝마다 자기가 만든 데이터로 학습하므로, 한 번 이상한 방향으로 크게 밀리면 그다음 생성이 통째로 오염된다.
보상 함수를 여러 개 두고 더한다. 위 예시는 정답 여부와 형식 준수를 나눠 두었다. 나눠 두면 학습 중에 어느 쪽이 오르고 어느 쪽이 정체하는지 따로 볼 수 있다. 이 구분이 없으면 총점만 보고 원인을 못 찾는다.
생성이 전체 시간의 대부분을 차지한다. 한 스텝에 프롬프트 × 개를 만들어야 하므로, 학습 속도는 사실상 추론 속도다. vLLM 같은 추론 엔진을 학습 루프에 붙여 쓰는 구성이 표준이 된 이유다.
보상을 설계할 때 흔히 나는 사고
GRPO에서 튜닝의 대상은 하이퍼파라미터가 아니라 대부분 보상 함수다. 모델은 우리가 적은 대로 정확히 최적화하고, 우리가 적지 않은 것은 지킬 이유가 없다.
| 적은 것 | 모델이 찾아낸 것 | 막는 법 |
|---|---|---|
| 답이 길면 부분 점수 | 같은 문장을 반복해 길이를 늘린다 | 길이 점수를 빼고 정답 여부만 남긴다 |
| 특정 태그가 있으면 가점 | 태그만 쓰고 내용은 비운다 | 태그 안의 내용까지 검사한다 |
| 테스트 통과 시 만점 | 테스트 파일을 수정하거나 예외를 삼킨다 | 테스트를 읽기 전용으로 격리해 실행한다 |
| 심판 모델의 점수 | 심판이 좋아하는 문체로 치우친다 | 규칙 기반 점수를 섞고 KL 항을 살린다 |
세 번째 줄은 실제로 자주 일어난다. 코드 과제에서 보상을 「테스트가 통과했는가」로 두면, 모델이 테스트를 통과시키는 가장 짧은 길을 찾는다 — 그것이 문제를 푸는 것이든 테스트를 무력화하는 것이든 상관하지 않는다. 채점 환경을 모델이 건드릴 수 없게 만드는 것이 보상 설계의 절반이다.
PPO·DPO와 나란히 놓고 보면
| DPO | PPO | GRPO | |
|---|---|---|---|
| 데이터 | 미리 모은 선호 쌍 | 학습 중 생성 | 학습 중 생성 |
| 필요한 모델 | 정책 · 참조 | 정책 · 참조 · 보상 · 가치 | 정책 · 참조 |
| 점수의 출처 | 사람·심판이 고른 쌍 | 보상 모델 | 보상 함수 또는 보상 모델 |
| 기준선 | 참조 모델의 확률 | 가치 모델의 예측 | 같은 무리의 평균 |
| 잘 맞는 과제 | 말투·판단·형식 | 넓게 적용 | 채점이 명확한 과제 |
| 스텝 비용 | 가장 싸다 | 중간 | 생성 배 |
고르는 순서는 대개 이렇다. 채점 기준을 코드로 적을 수 있으면 GRPO, 「어느 쪽이 더 나은가」밖에 말할 수 없으면 DPO다. PPO는 두 조건이 모두 애매하고 학습 인프라를 감당할 수 있을 때 남는 선택지다.
정리
GRPO가 한 일은 새로운 이론을 세운 것이 아니라 비싼 부품 하나를 표본으로 바꾼 것이다. 그래서 이 방법을 쓸 때 신경 쓸 곳도 그 교환에서 생긴 자리에 몰려 있다.
- 무리 크기 를 8 근처에서 시작하고, 보상이 갈린 프롬프트의 비율을 지표로 본다
- 그 비율이 떨어지면 데이터의 난이도를 갈아 끼운다
- 규칙으로 채점할 때는 채점 환경을 격리하고, 모델로 채점할 때는 KL 항을 살린다
- 보상 함수를 여러 개로 나눠 두고 각각을 따로 본다
이 넷을 지키면 나머지는 대체로 기본값으로 돌아간다. 다음 글에서는 이 채점을 사람이나 모델이 아니라 실행 결과로 하는 방식을 다룬다.
읽어주셔서 감사합니다. 😊

