모델 운영

MLOPS / 76번째 글

RLVR — 채점 가능한 과제로만 학습시키기

RLVR은 보상 모델 대신 검증기를 쓰는 학습입니다. 무엇이 검증 가능한 과제인지, 답 추출과 정규화가 왜 검증기의 절반인지, 그리고 격리·타임아웃·거짓 음성을 어떻게 다루는지 정리합니다.

PALDYN Team14 MIN READ

지난 글에서 GRPO는 보상을 어디서 얻는지 정하지 않는다고 했다. 무리의 평균을 기준으로 삼는 계산 규칙일 뿐이고, 점수 자체는 밖에서 들어온다. 그 점수를 프로그램이 매기는 구성을 따로 RLVR(Reinforcement Learning with Verifiable Rewards)이라 부른다. 검증 가능한 보상으로 하는 강화학습이라는 뜻이다.

이름에 방법이 아니라 보상의 출처가 들어간 것이 핵심이다. 알고리즘은 GRPO여도 되고 PPO여도 된다. 달라지는 것은 「이 답이 좋은가」를 사람도 모델도 아닌 코드가 판정한다는 점 하나다.

채점을 사람 대신 프로그램이 한다

왜 이것이 통했는가

RLHF의 보상 모델은 학습된 모델이다. 학습된 모델은 틀린다. 그리고 강화학습은 보상의 빈틈을 찾아내는 데 매우 능하다 — 보상 모델이 실수로 높은 점수를 주는 답의 모양이 있으면, 정책은 몇천 스텝 안에 정확히 그 모양을 만들어 낸다. 이것을 보상 해킹이라 하고, RLHF에서 학습을 오래 못 돌리는 가장 큰 이유였다.

검증기에는 그 빈틈이 훨씬 적다. 「이 수식의 답이 12인가」는 취향이 아니라 사실이고, 잘 짠 검증기는 100만 번을 물어도 같은 답을 준다. 그래서 RLVR은 보상이 무너지기 전까지 훨씬 오래 돌릴 수 있다. 추론 모델의 성능이 크게 오른 배경이 대체로 이것이다.

대신 조건이 붙는다. 채점을 코드로 적을 수 있어야 한다.

무엇이 검증 가능한가

과제 검증 방법 조심할 곳
수학 정답 최종 답을 정답과 비교 표기가 여러 가지다
코드 단위 테스트 실행 실행 격리·타임아웃
형식 준수 스키마·정규식 검사 형식만 맞고 내용이 빈다
구조화 추출 필드별 정확도 계산 부분 점수 설계가 필요하다
도구 호출 인자 비교 또는 실제 실행 부작용이 있는 도구는 못 쓴다
번역·요약 — 여기서부터는 검증기가 없다

마지막 줄이 이 방법의 경계다. 정답이 하나로 정해지지 않는 과제에는 RLVR을 쓸 수 없다. 그런 과제는 앞 글의 DPO나, 다음 글에서 다룰 보상 모델의 자리다.

경계가 뚜렷해 보이지만 실제로는 밀어 볼 여지가 있다. 「이메일을 써 달라」는 검증할 수 없어도 「이메일에 반드시 회신 기한이 들어가야 한다」는 검증할 수 있다. 과제 전체가 아니라 과제가 만족해야 할 제약을 검증기로 옮기는 방식이 실무에서 자주 쓰인다. 다만 그렇게 만든 보상은 품질이 아니라 제약 준수만 재므로, 제약을 다 만족하면서 형편없는 답이 만점을 받을 수 있다는 점을 알고 써야 한다.

검증기의 절반은 답을 꺼내는 일

수학 과제에서 보상 함수를 처음 짜면 대개 이렇게 시작한다.

def reward(completion, answer):
    return 1.0 if answer in completion else 0.0

이 함수는 두 방향으로 다 틀린다. 정답이 2일 때 답에 12가 들어 있으면 통과하고, 모델이 분수로 답하면 떨어뜨린다.

같은 답을 네 가지로 쓴다

맞은 답을 틀렸다고 하는 실수가 특히 나쁘다. 강화학습에서 이런 거짓 음성은 잡음이 아니라 잘못된 방향의 신호다. 모델은 그 답을 덜 내도록 밀리고, 결국 검증기가 알아보는 표기법 쪽으로만 답을 쓰게 된다. 학습이 끝난 모델이 이상하게 경직된 형식으로만 답한다면 대개 여기가 원인이다.

그래서 검증기는 최소한 이 세 단계를 갖는다.

  1. 추출 — 정해 둔 자리에서만 답을 꺼낸다. 프롬프트에서 <answer> 태그나 \boxed{} 를 요구하고, 없으면 마지막 줄을 본다
  2. 정규화 — 공백·쉼표·단위를 떼고, 분수와 소수를 같은 형태로 옮긴다. 수식이면 기호 계산 라이브러리로 동치를 판정한다
  3. 비교 — 정수는 그대로, 실수는 허용 오차를 두고 비교한다
from sympy import simplify
from sympy.parsing.latex import parse_latex

def extract(text: str) -> str | None:
    # 프롬프트에서 요구한 자리에서만 꺼낸다
    m = re.search(r"<answer>(.*?)</answer>", text, re.S)
    return m.group(1).strip() if m else None

def reward_correct(completion: str, answer: str) -> float:
    got = extract(completion)
    if got is None:
        return 0.0                      # 형식을 안 지킨 것도 오답이다
    if got.replace(",", "") == answer:
        return 1.0                      # 문자열이 같으면 빠르게 통과
    try:
        return 1.0 if simplify(parse_latex(got) - parse_latex(answer)) == 0 else 0.0
    except Exception:
        return 0.0

기호 계산은 느리다. 그래서 싼 비교를 먼저 하고 실패한 것만 넘긴다. 무리 크기가 8이고 배치가 64면 스텝마다 512번 채점하므로, 검증기 한 번이 100ms만 되어도 학습 속도가 절반이 된다.

그리고 검증기가 무엇을 떨어뜨렸는지 표본으로 남긴다. 0점을 받은 답 몇 개를 주기적으로 파일에 적어 두고 사람이 읽으면, 거짓 음성이 쌓이고 있는지 며칠 안에 알 수 있다. 이 습관이 없으면 보상 곡선만 보고 몇만 스텝을 낭비한다.

코드 과제의 격리

코드를 실행해 채점할 때는 검증기가 곧 실행 환경이다. 여기에는 학습과 무관한 종류의 위험이 붙는다.

  • 타임아웃. 무한 루프를 도는 답이 반드시 나온다. 프로세스 단위로 시간 제한을 걸고 초과하면 0점을 준다
  • 자원 제한. 메모리와 프로세스 수에 상한을 둔다. 상한이 없으면 답 하나가 학습 노드를 통째로 멈춘다
  • 네트워크 차단. 밖으로 나가는 연결을 막는다. 느려지기도 하고, 답이 정답을 검색해 오는 길이 되기도 한다
  • 테스트 파일 보호. 테스트를 답과 같은 디렉터리에 두지 않는다. 정책은 테스트를 통과시키는 가장 짧은 길을 찾고, 테스트를 지우는 것이 그 길일 때가 있다
  • 병렬 실행. 채점이 학습을 막지 않도록 프로세스 풀에 맡긴다

이 다섯 중 앞의 넷은 보안 조치처럼 보이지만 실제로는 보상의 정확도를 지키는 조치다. 격리가 새면 검증기가 틀린 점수를 주고, 틀린 점수는 정책이 그대로 배운다.

0점과 1점만 있을 때 생기는 일

RLVR의 보상은 대개 이분법이다. 그래서 앞 글에서 본 문제가 그대로 커진다 — 무리 안의 답이 전부 0점이거나 전부 1점이면 학습 신호가 없다.

대응으로 부분 점수를 넣고 싶어지는데, 여기서 넣는 부분 점수는 성격을 나눠서 봐야 한다.

부분 점수 성격 판단
테스트 10개 중 통과한 비율 과제의 실제 진척을 잰다 넣는다
형식을 지켰으면 0.1점 학습 초반에 형식을 잡아 준다 작게 넣고 나중에 뺀다
답이 길면 가점 과제와 무관하다 넣지 않는다
풀이 단계가 많으면 가점 과제와 무관하다 넣지 않는다

기준은 하나다. 그 점수가 오르는 것이 과제를 더 잘 푸는 것과 같은 뜻인가. 형식 점수는 이 기준에서 애매한 자리에 있어서, 초반에 0.1 정도로 두었다가 형식이 안정되면 빼는 식으로 다룬다. 계속 두면 모델이 형식만 정확한 빈 답을 내기 시작한다.

난이도 쪽 대응이 더 근본적이다. 학습 도중 각 프롬프트의 통과율을 기록해 두고, 통과율이 0이나 1로 굳은 프롬프트를 빼고 중간에 있는 것을 채워 넣는다. 데이터셋을 한 번 만들어 놓고 끝내는 것이 아니라 학습과 함께 굴리는 대상으로 보는 것이 RLVR의 실제 작업량이다.

검증할 수 있는 것에서 배운 것이 밖으로 새는가

RLVR로 수학과 코드만 학습했는데 다른 과제도 좋아지는 현상이 관찰된다. 정답을 맞히는 법이 아니라 길게 생각하고 스스로 검토하는 습관이 학습되기 때문으로 본다. 그 습관은 과제를 가리지 않는다.

다만 이 이전이 공짜는 아니다. 두 가지를 함께 봐야 한다.

  • 답이 길어진다. 생각을 늘리는 방향으로 보상이 걸리므로 추론 길이가 자란다. 짧게 답해야 할 질문에도 길어지면 그것대로 비용이고 품질 저하다
  • 학습에 없던 능력이 줄어들 수 있다. 앞 글에서 말한 것과 같은 점검이 여기서도 필요하다 — 학습 전후로 무관한 과제의 점수를 재 둔다

그래서 실무 구성은 대개 섞는다. 검증 가능한 과제로 RLVR을 돌리고, 검증할 수 없는 쪽은 선호 데이터로 따로 맞춘다. 둘 중 하나만 쓰는 구성은 드물다.

정리

RLVR은 새 알고리즘이 아니라 보상을 학습된 모델에서 코드로 옮긴 구성이다. 그 교환으로 보상 해킹이 줄고 학습을 오래 돌릴 수 있게 됐지만, 대신 검증기의 품질이 곧 모델의 품질이 됐다.

작업의 무게가 어디에 실리는지로 요약하면 이렇다.

  • 채점 규칙을 코드로 적을 수 있는 과제만 고른다
  • 추출과 정규화에 시간을 쓴다. 거짓 음성이 가장 비싼 실수다
  • 실행 채점은 격리·타임아웃·자원 제한을 먼저 세운다
  • 통과율이 0이나 1로 굳은 프롬프트를 학습 도중 갈아 끼운다
  • 0점 받은 답을 주기적으로 사람이 읽는다

다음 글에서는 검증기를 짤 수 없는 과제로 넘어가, 점수를 매기는 모델 자체를 만드는 일을 다룬다.


읽어주셔서 감사합니다. 😊

LATEST

모델 운영의 최신 글

모델 운영2026.09.04

KV 캐시 양자화 — 가중치보다 이쪽이 먼저 넘친다

긴 문맥에서 GPU 메모리를 실제로 잡아먹는 것은 가중치가 아니라 KV 캐시입니다. 캐시 크기를 계산하는 법, K와 V를 다르게 다뤄야 하는 이유, 어디까지 줄여도 되는지를 정리합니다.

16 MIN
모델 운영2026.09.04

양자화 보정 — 데이터 128개가 모델 품질을 정한다

양자화에서 스케일을 정하는 절차가 보정입니다. 무엇을 재는지, 데이터를 어디서 몇 개 뽑아야 하는지, 자르는 지점을 어떻게 고르는지, 그리고 잘못된 보정이 어떤 모양으로 드러나는지를 정리합니다.

21 MIN
모델 운영2026.09.03

과제 특화 증류 — 큰 모델의 답을 작은 모델에 옮긴다

범용 성능이 아니라 우리 과제 하나만 잘하는 작은 모델을 만드는 방법입니다. 교사에게 무엇을 받아야 하는지, 데이터를 어떻게 모으고 거르는지, 손익 분기가 어디인지를 정리합니다.

15 MIN