LLM·트랜스포머

LLM / 19번째 글

선호 정렬: RLHF와 DPO

인간의 선호로 LLM을 정렬하는 두 방법을 한자리에서 다룬다. RLHF가 보상 모델과 PPO로 두 단계를 밟는 이유, DPO가 그 보상 모델을 대수적으로 지워 내는 과정, 그리고 beta와 학습률이 실제로 무엇을 바꾸는지를 숫자로 따라간다.

PALDYN Team41 MIN READ

지난 글에서 인스트럭션 튜닝의 세 단계—SFT, 보상 모델링, 그리고 RLHF나 DPO—를 큰 그림으로 훑었다. 이 글은 그 마지막 칸에 들어가는 두 방법을 한자리에서 다룬다. RLHF(Reinforcement Learning from Human Feedback)는 사람의 선호를 보상 모델이라는 중간 다리로 옮긴 뒤 강화학습으로 정책을 갱신하고, DPO(Direct Preference Optimization)는 그 다리를 대수로 지워 선호 데이터에서 곧바로 학습한다.

둘을 한 편에 묶는 이유가 있다. 서로 다른 목표를 좇는 경쟁자가 아니라 같은 목적함수를 푸는 두 가지 풀이이기 때문이다. DPO의 유도는 RLHF가 최대화하려던 그 식에서 출발하고, 몇 줄의 변형 끝에 보상 모델이 사라진다. 앞쪽을 모르면 뒤쪽 수식이 어디서 튀어나온 것인지 알 수 없고, 뒤쪽을 모르면 앞쪽이 왜 그렇게 번거로운 절차를 밟는지 알 수 없다. 그러니 순서는 하나다 — 문제를 먼저 세우고, 그 문제를 강화학습으로 푸는 길을 본 다음, 같은 문제를 손실 함수 하나로 접는 길을 본다.

선호 신호와 목적함수

쓰기와 고르기의 비대칭

지도학습은 「이 입력에는 이 출력」이라는 정답 데이터를 요구한다. 사전학습도 SFT도 그 방식으로 돌아간다. 다음 토큰이 무엇이어야 하는지가 데이터에 적혀 있고, 모델은 그 토큰의 확률을 올린다.

그런데 정렬 단계에서 다루는 것은 정답이 하나로 정해지지 않는 질문이다. 「이 코드의 버그를 알려 줘」에 대한 좋은 답은 여러 개다. 어디까지 설명할지, 고친 코드를 통째로 다시 쓸지 바뀐 줄만 짚을지, 왜 그렇게 고쳤는지를 얼마나 길게 적을지 — 맥락마다 다르고 취향도 섞인다. 좋은 답을 하나 골라 「이것이 정답」이라고 못 박으면, 모델은 그 답의 표면적 형태를 외울 뿐 무엇이 좋은 답을 좋게 만드는지는 배우지 못한다.

레이블러 쪽 사정도 같다. 이상적인 답을 직접 쓰는 일은 느리고 비싸며, 쓰는 사람마다 문체가 달라 데이터가 흔들린다. 반면 이미 나와 있는 두 응답을 놓고 「이쪽이 낫다」를 고르는 일은 훨씬 빠르고, 사람들 사이의 답도 훨씬 잘 맞는다. 쓰기는 어렵지만 고르기는 쉽다 — 정렬이 비교 신호 위에 세워진 이유가 이 비대칭이다.

선호 쌍

선호 쌍(preference pairs)은 같은 프롬프트에 대한 두 응답 중 어느 쪽이 나은지를 사람이 표시해 둔 데이터다. 요소는 셋뿐이다 — 프롬프트 xx, 선호된 응답 ywy_w(chosen), 거부된 응답 yly_l(rejected). 이 셋이 RLHF의 보상 모델 학습에도, DPO의 직접 학습에도 그대로 쓰인다. 두 방법이 갈리는 것은 학습 절차이지 데이터 형식이 아니다.

preference_data = [
    {
        "prompt": "기후 변화의 주요 원인을 설명해줘.",
        "chosen": (
            "기후 변화의 주요 원인은 온실가스 배출입니다. "
            "이산화탄소(CO2), 메탄(CH4), 아산화질소(N2O) 등이 대기 중에 "
            "축적되어 지구 온도를 높입니다. 주요 배출원은 화석연료 연소, "
            "산업 활동, 삼림 벌채입니다."
        ),
        "rejected": "기후가 변해요. 왜냐면 지구가 따뜻해져서요.",
    },
    {
        "prompt": "Python에서 리스트 중복 제거 방법은?",
        "chosen": (
            "1. set() 변환: `list(set(lst))` — 순서 보장 안 됨\n"
            "2. dict.fromkeys(): `list(dict.fromkeys(lst))` — 순서 유지"
        ),
        "rejected": "set을 쓰면 됩니다.",
    },
]

이 데이터의 품질이 뒤의 모든 것을 정한다. 선호 쌍은 사람이 만들므로 레이블러 간 일관성(inter-annotator agreement), 곧 같은 쌍을 여러 사람에게 보였을 때 답이 얼마나 겹치는지가 그대로 신호의 선명도가 된다. 일관성이 낮으면 데이터에 담긴 것은 「무엇이 좋은 답인가」가 아니라 「누가 라벨을 붙였는가」다. OpenAI가 InstructGPT를 만들 때 레이블러 교육과 가이드라인 작성에 상당한 노력을 들인 것도 이 때문이다. 어떤 정렬 알고리즘도 이 단계에서 흐려진 신호를 되살리지는 못한다.

두 응답의 품질 차이가 뚜렷해야 한다는 점도 같은 이야기다. 위 예시처럼 한쪽이 명백히 나은 쌍은 배울 것이 많지만, 둘 다 그럴듯해서 레이블러가 동전을 던진 쌍은 노이즈만 더한다.

공통 목적함수

선호 쌍을 손에 넣었다고 하자. 우리가 풀려는 문제는 이렇게 적힌다.

max⁡πθ  Ex∼D,  y∼πθ[r(x,y)]  −  β DKL[πθ(y∣x) ∥ πref(y∣x)]\max_{\pi_\theta} \; \mathbb{E}_{x \sim D,\; y \sim \pi_\theta}\big[r(x,y)\big] \; - \; \beta \, \mathbb{D}_{\mathrm{KL}}\big[\pi_\theta(y \mid x) \,\|\, \pi_{\mathrm{ref}}(y \mid x)\big]

πθ\pi_\theta 는 우리가 학습시키는 모델이고, πref\pi_{\mathrm{ref}} 는 학습 내내 고정해 두는 기준 모델(reference model)로 보통 SFT를 막 마친 체크포인트를 쓴다. r(x,y)r(x,y) 는 응답이 얼마나 선호되는지를 나타내는 보상이다.

앞 항만 있으면 이야기가 단순하다. 보상을 최대화하도록 모델을 밀면 된다. 뒤 항이 필요한 이유는 그 밀기가 제한 없이 진행되면 모델이 언어 모델이기를 그만두기 때문이다. KL 발산(Kullback-Leibler divergence)은 두 확률분포가 얼마나 다른지를 재는 값으로, 완전히 같으면 0이고 멀어질수록 커진다. 이 항에 붙은 β\beta 는 「기준에서 한 걸음 멀어지는 대가」의 가격표다.

숫자로 보면 이 항이 무엇을 하는지 분명해진다. 어떤 방향으로 학습을 밀어 보상이 1.0 오르는데 그 대가로 KL이 10만큼 늘었다고 하자. β=0.02\beta = 0.02 라면 페널티는 0.02×10=0.20.02 \times 10 = 0.2 이고 남는 이득이 0.8이므로 학습은 그 방향으로 간다. 같은 상황에서 β=0.2\beta = 0.2 였다면 페널티가 2.0이라 이득이 음수가 되어 그 방향은 버려진다. β\beta 는 「얼마나 벗어나도 되는가」를 정하는 하나의 손잡이이고, 뒤에서 볼 두 방법 모두 이 손잡이를 그대로 물려받는다.

여기서 길이 갈린다. RLHF는 이 식을 글자 그대로 푼다 — rr 을 근사하는 보상 모델을 학습하고, 강화학습으로 기댓값을 최대화한다. DPO는 이 식의 해를 먼저 적어 놓고 그 해에서 rr 을 역산해, 보상 모델이라는 중간 다리를 통째로 소거한다. 도착점은 같고 경로만 다르다.

RLHF 파이프라인

이 절차가 정렬의 기본형으로 굳은 계기는 2022년 OpenAI의 InstructGPT다. 그 논문은 1.3B 파라미터짜리 RLHF 모델의 응답이 파라미터가 100배 많은 175B GPT-3의 응답보다 레이블러에게 더 선호됐다고 보고했다 — 답의 쓸모를 정하는 것이 모델 크기만은 아니라는 결과다. 아래 세 단계가 그 논문이 세운 절차이고, 이후의 변형들은 대부분 이 골격 위에서 무언가를 덜어 내는 쪽으로 갔다.

SFT

첫 단계는 고품질 데모 데이터로 베이스 모델을 파인튜닝하는 것이다. 숙련된 레이블러가 다양한 프롬프트에 이상적인 응답을 직접 쓴다. InstructGPT는 이런 데모를 약 13,000개 사용했다.

이 단계를 건너뛸 수 없는 이유는 뒤의 두 단계가 모두 「그럴듯한 응답이 이미 나온다」를 전제하기 때문이다. 베이스 모델은 질문을 받으면 답하는 대신 질문을 이어 쓰려 든다. 그런 모델로 응답 쌍을 만들면 레이블러가 고를 것이 없고, 강화학습을 걸어도 탐색할 좋은 행동이 애초에 분포 안에 없다. SFT는 성능을 올리는 단계라기보다 이후 단계가 딛고 설 바닥을 까는 단계다. 그리고 앞 절에서 본 πref\pi_{\mathrm{ref}} 가 바로 이 체크포인트다.

RLHF 3단계 파이프라인

보상 모델과 Bradley-Terry

두 번째 단계가 RLHF의 핵심이다. 같은 프롬프트에 SFT 모델이 여러 응답을 내놓고, 레이블러가 순위를 매긴다. 이 선호 쌍으로 보상 모델(Reward Model)을 학습시킨다. 보상 모델은 프롬프트와 응답을 받아 스칼라 하나를 뱉는 모델이고, 보통 SFT 모델에서 출발해 마지막 출력층만 값 하나를 내는 헤드로 갈아 끼운다.

문제는 학습 신호다. 데이터에는 「이 응답의 점수는 7.2점」 같은 절대값이 없고 「A가 B보다 낫다」는 순서만 있다. Bradley-Terry 모델은 이 순서를 점수로 옮기는 고전적인 방법으로, 두 대상의 점수 차이가 클수록 이긴 쪽이 선택될 확률이 높다고 놓는다.

P(yw≻yl∣x)=σ(r(x,yw)−r(x,yl))P(y_w \succ y_l \mid x) = \sigma\big(r(x, y_w) - r(x, y_l)\big)

σ\sigma 는 시그모이드다. 보상 모델은 데이터에 적힌 선택이 나올 확률을 최대화하도록 학습된다. 점수 차이가 0이면 확률은 0.5, 곧 「모르겠다」이고, 차이가 2면 σ(2)≈0.88\sigma(2) \approx 0.88 이다. 여기서 중요한 성질 하나가 나온다 — 차이만 의미가 있고 절대적인 눈금은 없다. 모든 응답의 점수에 5를 더해도 확률은 그대로다. 그래서 보상 모델의 출력을 「이 답은 3.1점짜리」처럼 읽으면 안 된다. 같은 프롬프트 안에서 두 응답을 비교할 때만 뜻이 있는 값이다.

보상 모델 학습

PPO의 상태와 행동

보상 모델이 준비되면 PPO(Proximal Policy Optimization)로 언어 모델을 추가 학습한다. 강화학습 용어로 옮기면 이렇게 대응한다.

  • 정책(Policy): 현재 LLM, 곧 다음 토큰의 확률 분포를 내놓는 함수
  • 상태(State): 지금까지 만들어진 토큰 시퀀스
  • 행동(Action): 다음 토큰 하나를 고르는 것
  • 보상(Reward): 응답을 끝까지 만든 뒤 보상 모델이 매기는 점수

마지막 줄이 이 문제를 까다롭게 만든다. 보상은 토큰마다 오지 않고 응답이 끝나야 한 번 온다. 200토큰짜리 답을 만들었는데 돌아오는 신호가 숫자 하나라면, 그 200번의 선택 중 무엇이 잘한 것이고 무엇이 나빴는지 나누는 일이 남는다. 강화학습이 이 몫을 다루려고 가치 모델(value model)을 하나 더 둔다 — 지금 상태에서 앞으로 받을 보상을 예측하는 모델이고, 실제 보상에서 이 예측을 뺀 값이 어드밴티지(advantage), 곧 「기대보다 얼마나 잘했는가」다. 뒤에서 볼 메모리 부담의 네 번째 모델이 이것이다.

클리핑과 KL 페널티

PPO 이전에는 REINFORCE 같은 단순한 정책 그래디언트를 썼다. 문제는 업데이트 한 번이 너무 크면 정책이 갑자기 나쁜 쪽으로 굴러떨어지고, 그다음부터는 나쁜 정책이 만든 데이터로 학습하므로 회복이 어렵다는 것이다. 지도학습이라면 다음 배치가 원래 데이터를 다시 보여 주지만, 강화학습에서는 데이터를 정책 자신이 만든다.

PPO는 클리핑(clipping)으로 이 자리를 막는다.

LCLIP=E[min⁡(rt(θ)At,  clip(rt(θ), 1−ϵ, 1+ϵ)At)]L_{\mathrm{CLIP}} = \mathbb{E}\Big[\min\big(r_t(\theta) A_t,\; \mathrm{clip}(r_t(\theta),\, 1-\epsilon,\, 1+\epsilon) A_t\big)\Big]

rt(θ)r_t(\theta) 는 새 정책과 이전 정책이 그 토큰에 준 확률의 비율이고 AtA_t 는 어드밴티지, ϵ\epsilon 은 클리핑 폭으로 보통 0.2를 쓴다. 뜻을 풀면 이렇다 — 새 정책이 어떤 토큰의 확률을 세 배로 올리려 해도 rt=3r_t = 3 이 1.2로 잘려, 그 방향으로 얻을 수 있는 이득이 거기서 멈춘다. 한 번의 갱신이 정책을 옮길 수 있는 거리에 천장을 씌우는 장치다.

클리핑이 한 걸음의 크기를 제한한다면, 앞서 본 KL 페널티는 출발점에서의 총 거리를 제한한다. 둘은 역할이 다르고 함께 쓰인다. InstructGPT는 β=0.02\beta = 0.02 를 썼다. β\beta 가 너무 작으면 모델이 기준에서 멀리 떠내려가 다음 절의 보상 해킹에 빠지고, 너무 크면 페널티가 개선을 통째로 상쇄해 아무 일도 일어나지 않는다.

RLHF의 비용

보상 해킹

보상 해킹(reward hacking)은 모델이 보상 모델의 허점을 찾아, 실제로는 좋지 않은데 높은 점수를 받는 응답을 만들어 내는 현상이다. 강화학습이 최대화하는 것은 「좋은 응답」이 아니라 「보상 모델의 출력」이고, 둘은 같은 것이 아니다.

전형적인 예가 길이다. 레이블러가 자세한 답을 선호하는 경향이 있으면 보상 모델은 길이와 상관관계가 있는 무언가를 점수로 삼게 되고, 정책은 그 상관을 발견해 내용과 무관하게 길고 자신감 넘치는 문장을 늘린다. 서론과 요약을 덧붙이고, 목록을 만들고, 「이 점은 매우 중요합니다」 같은 문장을 끼운다. 보상 곡선은 매끄럽게 올라가는데 사람이 읽어 보면 답이 나빠져 있다.

핵심은 이것이 버그가 아니라 최적화가 제대로 작동한 결과라는 점이다. 보상 모델은 유한한 데이터로 학습한 근사이고, 정책은 그 근사가 실제 선호와 어긋나는 지점을 찾아내는 데 아주 능하다.

해킹 완화 장치

완전히 막는 방법은 없고, 늦추는 장치가 넷 있다.

  1. KL 페널티: 기준 모델에서 멀어질수록 비용이 붙는다. 보상 모델이 신뢰할 만한 영역, 곧 학습 데이터가 있던 분포 근처에 정책을 붙들어 둔다.
  2. 보상 모델 앙상블: 여러 보상 모델을 학습해 그중 최솟값을 쓴다. 한 모델의 허점을 다른 모델이 함께 갖고 있을 확률이 낮으므로, 전부를 동시에 속여야 점수가 오른다.
  3. 주기적 재학습: 정책이 새로 만든 응답을 레이블러에게 다시 보내 보상 모델을 갱신한다. 해킹당한 응답에 낮은 점수가 붙으면 그 구멍이 메워진다.
  4. 원칙 기반 접근: 사람의 선호 대신 명시된 규칙에 비추어 응답을 평가한다. 다음 글의 주제다.

실무에서 가장 먼저 보는 계기판은 KL 값 자체다. 학습 중 KL이 완만히 오르는 것은 정상이지만 갑자기 치솟으면 정책이 기준에서 떨어져 나가는 중이고, 보상 곡선이 함께 급등한다면 해킹을 의심할 자리다.

def check_kl(kl_value, threshold=10.0):
    """KL이 급등하면 β를 높이거나 학습률을 낮춘다."""
    if kl_value > threshold:
        print(f"경고: KL {kl_value:.2f} > {threshold} — 보상 해킹 가능성")
        return True
    return False

메모리 부담

RLHF를 실제 규모에서 돌릴 때 가장 먼저 부딪히는 벽은 알고리즘이 아니라 메모리다. PPO 학습은 네 모델을 동시에 들고 있어야 한다 — 정책, 기준 모델, 보상 모델, 가치 모델.

넷을 모두 70B로 잡고 bf16으로 올리면 가중치만 모델당 70×109×2=14070 \times 10^9 \times 2 = 140 GB, 합쳐 560GB다. 여기에 정책 쪽에는 옵티마이저 상태와 그래디언트가 얹히고, 응답을 생성하는 동안의 KV 캐시도 따로 든다. 실무에서는 보상 모델과 가치 모델을 더 작게 두거나 정책과 가중치를 공유해 이 숫자를 줄이지만, 어느 쪽이든 지도학습 파인튜닝과는 다른 급의 인프라를 요구한다.

샘플 효율도 문제다. 강화학습은 지도학습에 비해 같은 컴퓨트로 얻는 개선이 작다. 학습 루프 안에서 응답을 새로 생성해야 하므로 스텝마다 추론 비용이 들고, 그렇게 얻은 신호는 응답 하나에 스칼라 하나뿐이다. 게다가 학습률·β\beta·ϵ\epsilon·배치 크기가 서로 얽혀 있어 한 값을 바꾸면 다른 값도 다시 찾아야 한다. RLHF의 진짜 비용은 GPU가 아니라 이 튜닝 루프에 드는 사람의 시간이라는 말이 나오는 이유다.

DPO의 유도

닫힌 형태의 해

여기서 관점을 뒤집는다. 2023년 스탠퍼드 연구팀이 내놓은 DPO 논문의 출발점이 이 뒤집기다. 앞의 목적함수를 풀어 나가는 대신, 그 문제의 해가 어떻게 생겼는지를 먼저 적는다. KL로 정규화된 보상 최대화 문제는 닫힌 형태의 해를 갖는다.

π∗(y∣x)  ∝  πref(y∣x)⋅exp⁡ ⁣(r(x,y)/β)\pi^*(y \mid x) \;\propto\; \pi_{\mathrm{ref}}(y \mid x) \cdot \exp\!\big(r(x,y) / \beta\big)

읽으면 이렇다 — 최적 정책은 기준 모델의 확률에 보상의 지수를 곱한 것이다. 보상이 높은 응답일수록 기준 모델보다 확률이 올라가고, β\beta 가 클수록 지수의 효과가 눌려 기준 모델에 가까워진다. 앞서 「β\beta 가 벗어남의 가격표」라고 한 말이 이 식에 그대로 들어 있다.

이제 이 식을 rr 에 대해 푼다. 양변에 로그를 취하고 옮기면 된다.

r(x,y)=βlog⁡π∗(y∣x)πref(y∣x)+βlog⁡Z(x)r(x,y) = \beta \log \frac{\pi^*(y \mid x)}{\pi_{\mathrm{ref}}(y \mid x)} + \beta \log Z(x)

방향이 뒤집혔다는 데 주목한다. 원래는 보상을 알아야 최적 정책을 구할 수 있었는데, 이제는 정책 하나만 있으면 그 정책이 암묵적으로 어떤 보상을 최적화한 결과인지 읽어 낼 수 있다. 보상 함수를 별도의 신경망으로 둘 이유가 사라지는 첫 번째 지점이다.

분할 함수의 소거

걸리는 것은 Z(x)Z(x) 하나다. 분할 함수(partition function)라 부르는 이 값은 확률의 합을 1로 맞추기 위한 정규화 상수로, 가능한 모든 응답 yy 에 대한 합이다. 언어 모델에서 「가능한 모든 응답」의 수는 어휘 크기를 응답 길이만큼 거듭제곱한 값이라 계산이 불가능하다.

여기서 Bradley-Terry가 다시 등장한다. 그 모델이 쓰는 것은 두 응답의 보상 차이뿐이었다. 같은 프롬프트 xx 에 대한 ywy_w 와 yly_l 의 보상을 위 식으로 각각 쓰고 빼면, βlog⁡Z(x)\beta \log Z(x) 는 둘 다 xx 에만 의존하므로 그대로 사라진다. 계산할 수 없던 항이 뺄셈 한 번에 없어지는 것이다.

남은 것을 Bradley-Terry의 시그모이드 안에 넣고 음의 로그 가능도를 취하면 DPO 손실이 된다.

LDPO=− E[log⁡σ ⁣(βlog⁡πθ(yw∣x)πref(yw∣x)−βlog⁡πθ(yl∣x)πref(yl∣x))]L_{\mathrm{DPO}} = -\,\mathbb{E}\left[\log \sigma\!\left(\beta \log \frac{\pi_\theta(y_w \mid x)}{\pi_{\mathrm{ref}}(y_w \mid x)} - \beta \log \frac{\pi_\theta(y_l \mid x)}{\pi_{\mathrm{ref}}(y_l \mid x)}\right)\right]

이 식에 보상 모델이 없다. 있는 것은 학습 중인 정책과 고정된 기준 모델, 그리고 선호 쌍뿐이다. 강화학습 루프도, 가치 모델도, 학습 중 생성도 사라졌다.

DPO 손실 구현

구현은 놀랍도록 짧다. 필요한 값은 네 개의 로그 확률 — 정책과 기준 모델이 각각 chosen과 rejected에 매긴 시퀀스 전체의 로그 확률이다.

import torch.nn.functional as F

def dpo_loss(pi_chosen, pi_rejected, ref_chosen, ref_rejected, beta=0.1):
    """네 인자 모두 시퀀스 전체의 로그 확률 합이다."""
    chosen_logratio = pi_chosen - ref_chosen
    rejected_logratio = pi_rejected - ref_rejected
    margin = beta * (chosen_logratio - rejected_logratio)
    return -F.logsigmoid(margin).mean()

한 배치의 계산은 순전파 네 번(정책 두 번, 기준 모델 두 번)과 이 손실 한 줄이다. 기준 모델은 학습하지 않으므로 그래디언트가 필요 없고, 옵티마이저 상태를 들 필요도 없다. 동시에 올리는 모델이 넷에서 둘로 줄고 그중 하나는 추론만 한다.

손실이 무엇을 시키는지는 margin을 보면 된다. 이 값이 클수록 손실이 작아지므로, 학습은 chosen의 로그 비율을 올리고 rejected의 로그 비율을 내리는 방향으로 간다. 숫자를 넣어 보자. 아직 아무것도 안 배운 시점에는 정책이 기준 모델과 같아 두 로그 비율이 모두 0이고, margin도 0이라 손실은 −log⁡σ(0)=0.693-\log \sigma(0) = 0.693 이다. 학습이 진행돼 정책이 chosen의 로그 확률을 기준보다 1.0 올리고 rejected를 1.0 내렸다면, β=0.1\beta = 0.1 에서 margin은 0.1×(1.0−(−1.0))=0.20.1 \times (1.0 - (-1.0)) = 0.2 이고 손실은 −log⁡σ(0.2)≈0.598-\log \sigma(0.2) \approx 0.598 로 내려간다.

TRL 같은 라이브러리는 이 손실을 DPOTrainer로 감싸 두었고, 데이터셋에 prompt·chosen·rejected 열만 있으면 나머지를 알아서 처리한다. 다만 인자 이름과 설정 클래스는 버전에 따라 달라져 온 편이므로, 쓰는 버전의 문서를 그때 확인하는 편이 낫다. 위 네 줄이 무엇을 계산하는지 알고 있으면 어느 버전이든 대응은 어렵지 않다.

DPO 손실 함수 구현

DPO의 하이퍼파라미터

beta

DPO의 β\beta 는 RLHF의 KL 페널티 계수와 같은 역할을 한다 — 정책이 기준 모델에서 얼마나 벗어날 수 있는지를 정한다. 다만 페널티 항으로 따로 붙는 것이 아니라 손실 안쪽에 곱해져 있어서, 그 효과가 조금 덜 직관적이다.

앞의 계산을 β\beta 만 바꿔 되풀이하면 보인다. 정책이 chosen을 1.0 올리고 rejected를 1.0 내린 같은 상황에서 β=0.5\beta = 0.5 라면 margin은 0.5×2.0=1.00.5 \times 2.0 = 1.0 이고 손실은 −log⁡σ(1.0)≈0.313-\log \sigma(1.0) \approx 0.313 이다. β=0.1\beta = 0.1 일 때의 0.598보다 훨씬 낮다. 같은 만큼 움직였는데 손실이 더 많이 내려갔다는 것은 β\beta 가 클수록 손실이 일찍 만족한다는 뜻이고, 만족한 손실은 그래디언트를 거의 내놓지 않으므로 정책은 거기서 멈춘다. 반대로 β\beta 가 작으면 같은 이동으로는 손실이 조금밖에 안 내려가 학습이 계속 더 밀어붙인다.

β\beta 정책의 움직임 위험
0.01 근처 선호를 강하게 반영해 크게 벗어난다 다양성 감소, 문장이 이상해짐
0.05~0.2 실무에서 쓰는 대역 —
0.5 근처 기준 모델에 가깝게 머문다 선호 신호가 거의 반영되지 않음

실용적 권장값은 0.05~0.2이고, Llama 계열에는 0.1이 많이 쓰인다. 한 값으로 정하기 어렵다면 몇 개를 나란히 돌려 검증 지표로 고르는 것이 정석이다 — DPO는 한 판이 싸므로 이 비교가 부담스럽지 않다는 것 자체가 장점이다.

학습률과 확률 붕괴

DPO는 일반적인 파인튜닝(1e-4~2e-4)보다 훨씬 낮은 학습률을 쓴다. 보통 5e-7에서 1e-6 사이로, 100배에서 400배 낮은 값이다.

이렇게 낮춰야 하는 이유가 손실의 생김새에 들어 있다. DPO 손실이 보는 것은 두 로그 비율의 차이뿐이다. 차이를 벌리는 방법은 「chosen을 올리고 rejected를 내리기」만 있는 것이 아니라 「chosen을 조금 내리고 rejected를 많이 내리기」도 있다. 후자가 손실을 더 빨리 낮출 수 있으면 학습은 그쪽으로 간다. 그 결과가 확률 붕괴로, 선호된 응답을 포함해 모든 응답의 확률이 함께 떨어지는 현상이다. 지표상 손실은 잘 내려가는데 실제로 모델이 만들어 내는 문장은 무너져 있다.

학습 중에 chosen의 로그 확률 자체를 따로 찍어 보는 것이 이 사고를 잡는 가장 싼 방법이다. 손실과 margin만 보면 붕괴가 성공처럼 보이기 때문이다. chosen의 절대 로그 확률이 계속 내려가고 있으면 학습률을 낮추거나 β\beta 를 올린다.

기준 모델과 데이터 품질

πref\pi_{\mathrm{ref}} 는 학습 내내 고정된다. 표준은 SFT 체크포인트를 그대로 쓰는 것이다. 여기를 잘못 잡으면 손실의 의미가 통째로 흔들린다 — 기준 모델이 없으면 로그 비율이 그냥 로그 확률이 되어 β\beta 의 KL 제어 효과가 사라지고, 학습 중인 정책의 복사본을 계속 갱신하며 쓰면 「벗어나지 말아야 할 출발점」이 매번 옮겨 다니게 된다.

데이터 품질에서는 DPO가 RLHF보다 더 예민하다. RLHF에서는 보상 모델이 중간에 끼어 있어 개별 선호 쌍의 노이즈가 한 번 평균되지만, DPO는 선호 쌍 하나하나가 그대로 그래디언트가 된다. 완충재가 없다는 것은 좋은 데이터에서는 빨리 배운다는 뜻이고, 나쁜 데이터에서는 빨리 망가진다는 뜻이기도 하다. 어느 쪽이든 정렬의 병목은 알고리즘이 아니라 선호 데이터라는 결론은 같다.

DPO의 변형

IPO

DPO가 나온 뒤 손실 함수를 손보는 변형이 여럿 나왔다. IPO(Identity Preference Optimization)는 DPO가 이론적으로 과적합할 수 있다는 문제를 지적한다. 시그모이드 손실은 margin을 계속 키우면 계속 작아지므로, 학습이 「충분히 벌어졌다」에서 멈출 이유가 없다. 데이터에 우연히 섞인 선호까지 끝까지 밀어붙이게 된다.

LIPO=E[(log⁡πθ(yw∣x)πref(yw∣x)−log⁡πθ(yl∣x)πref(yl∣x)−12β)2]L_{\mathrm{IPO}} = \mathbb{E}\left[\left(\log \frac{\pi_\theta(y_w \mid x)}{\pi_{\mathrm{ref}}(y_w \mid x)} - \log \frac{\pi_\theta(y_l \mid x)}{\pi_{\mathrm{ref}}(y_l \mid x)} - \frac{1}{2\beta}\right)^2\right]

차이가 1/(2β)1/(2\beta) 라는 목표값에 맞춰지도록 제곱 오차로 벌한다. 더 벌리면 손실이 오히려 늘어나므로 과도한 최적화가 구조적으로 막힌다. TRL에서는 손실 종류를 ipo로 지정해 쓴다.

SimPO

SimPO(Simple Preference Optimization)는 다른 자리를 건드린다. 기준 모델을 아예 없애고, 응답 길이로 정규화한 평균 로그 확률을 직접 비교한다. 기준 모델을 메모리에 올릴 필요가 없으니 사용량이 DPO의 절반으로 줄고, 순전파도 두 번이면 된다.

길이 정규화가 곁다리가 아니다. 시퀀스 전체의 로그 확률 합은 응답이 길수록 작아지므로, 정규화 없이 비교하면 길이 자체가 신호에 섞인다. 평균으로 나누면 그 편향이 빠진다. 기준 모델이 사라진 만큼 벗어남을 제어할 다른 장치가 필요해서, SimPO는 마진 파라미터를 하나 더 두고 β\beta 도 DPO보다 훨씬 큰 값을 쓴다.

ORPO

ORPO(Odds Ratio Preference Optimization)는 아예 앞 단계를 없앤다. SFT와 선호 정렬을 하나의 손실로 묶어, 베이스 모델에서 정렬된 모델까지 학습 루프 한 번으로 간다.

LORPO=LNLL+λ⋅LORL_{\mathrm{ORPO}} = L_{\mathrm{NLL}} + \lambda \cdot L_{\mathrm{OR}}

LNLLL_{\mathrm{NLL}} 은 chosen 응답에 대한 평범한 언어 모델 손실이고, LORL_{\mathrm{OR}} 은 chosen과 rejected의 승산비(odds ratio)에 기반한 선호 손실이다. 앞 항이 SFT가 하던 일을 하고 뒤 항이 정렬을 한다. 기준 모델이 필요 없는 것도 여기서 따라온다 — 벗어나지 말아야 할 출발점 대신 SFT 손실 자체가 정책을 데이터 분포에 묶어 두기 때문이다. TRL에는 ORPOTrainer로 들어 있다.

세 변형의 공통점을 보면 DPO 이후의 흐름이 보인다. 손실 함수를 바꿔서 단계를 하나씩 덜어 내는 것이다. RLHF에서 보상 모델을 덜어 낸 것이 DPO였고, 거기서 기준 모델을 덜어 낸 것이 SimPO, SFT 단계까지 덜어 낸 것이 ORPO다.

DPO 변형의 계보

RLHF와 DPO의 선택

온라인과 오프라인의 경계

RLHF와 DPO의 차이 중 성능에 가장 크게 남는 것은 수식이 아니라 데이터를 어디서 얻는가다.

PPO는 학습 중에 현재 정책으로 새 응답을 생성하고, 그 응답을 보상 모델이 채점한다. 정책이 이상한 쪽으로 흘러가면 그 이상한 응답에 낮은 점수가 붙어 곧바로 교정된다. 반면 DPO가 보는 것은 학습 시작 전에 고정된 데이터셋이다. 정책이 그 데이터에 없던 영역으로 이동하면, 그 영역이 좋은지 나쁜지 알려 줄 신호가 없다.

RLHF (PPO) DPO
보상 모델 따로 학습한다 없다
동시에 올리는 모델 넷 (정책·기준·보상·가치) 둘 (정책·기준)
데이터 학습 중 생성 (온라인) 고정 데이터셋 (오프라인)
벗어남 제어 목적함수의 KL 페널티 손실 안의 β\beta
하이퍼파라미터 민감도 높다 낮다
구현 규모 강화학습 루프 한 벌 지도학습과 비슷

이것이 DPO의 구조적 한계이고, 뒤이은 연구들이 학습 도중 새 응답을 만들어 라벨을 붙이는 온라인 변형을 내놓은 이유이기도 하다. 다만 실무에서 이 차이가 결정적이었던 적은 생각보다 적다. 선호 데이터가 충분히 넓게 깔려 있으면 정책이 데이터 밖으로 크게 벗어날 일도 드물기 때문이다.

DPO와 RLHF 비교

선택 기준

2024년 이후 공개된 오픈소스 LLM들—Llama 3, Mistral, Qwen 계열—은 대체로 DPO나 그 변형으로 정렬한다. 이유는 위 표에 거의 다 들어 있다. 코드가 짧아 디버깅이 쉽고, 지도학습에 가까운 안정성 덕에 하이퍼파라미터를 오래 만지지 않아도 되며, 메모리와 학습 시간이 적게 든다. 그리고 많은 벤치마크에서 RLHF와 대등하거나 나은 결과가 나온다.

상황 고를 것
선호 데이터는 있고 GPU 예산이 빠듯하다 DPO
실험 회전을 빨리 돌려야 한다 DPO
보상 모델을 이미 갖고 있고 여러 곳에 재사용한다 RLHF
학습 중 탐색으로 데이터 밖 영역까지 다뤄야 한다 RLHF
무엇부터 할지 모르겠다 DPO로 시작한다

마지막 줄이 실무의 기본값이다. DPO를 한 판 돌리는 비용이 RLHF 파이프라인을 세우는 비용보다 훨씬 싸므로, 먼저 돌려 보고 부족한 지점이 확인된 다음에 옮겨도 늦지 않다. 반대 순서는 비싸다.

다음 걸음

두 방법이 공유하는 전제를 마지막으로 짚어 둔다. 선호 쌍은 사람이 만든다. 보상 모델을 지워도, 기준 모델을 지워도, SFT 단계를 합쳐도 그 자리는 남는다. 그리고 이 자리가 가장 느리고 비싸며, 앞에서 본 것처럼 신호의 품질이 여기서 정해진다. 모델이 커지고 다뤄야 할 상황이 넓어질수록 사람이 라벨을 붙여야 할 쌍의 수도 함께 늘어난다.

다음 글에서는 그 자리를 규칙으로 대신하는 접근을 본다. 무엇이 좋은 응답인지를 사람이 매번 고르는 대신 원칙을 문장으로 적어 두고, 모델이 그 원칙에 비추어 자기 응답을 비평하고 고치게 한 뒤, 그렇게 얻은 쌍으로 정렬한다. 정렬 알고리즘 자체는 이 글에서 본 것을 그대로 쓰고, 바뀌는 것은 선호 쌍을 누가 만드는가 하나다.


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

LATEST

LLM·트랜스포머의 최신 글

LLM·트랜스포머2026.08.14

작은 모델을 우리 일에 맞추는 법

파인튜닝이 실제로 고치는 것은 지식이 아니라 행동입니다. 데이터 몇 건이 필요한지, LoRA가 무엇을 바꾸는지, 학습 전에 무엇을 먼저 만들어야 하는지를 정리합니다.

15 MIN
LLM·트랜스포머2026.08.13

작은 모델로 내려도 되는지 판단하는 법

비용·지연·품질은 같은 방향으로 움직이지 않습니다. 폴백을 붙였을 때의 손익분기, 격차가 벌어지는 작업 유형, 그리고 내리기 전에 통과해야 할 네 관문을 정리합니다.

10 MIN
LLM·트랜스포머2026.08.13

작은 모델이 다시 쓸 만해진 이유

10억에서 100억 파라미터 사이의 모델이 다시 실무에 들어오고 있습니다. 무엇이 달라졌는지, 메모리는 어떻게 계산하는지, 무엇을 잘하고 무엇을 못하는지 정리합니다.

10 MIN