검색
← 모듈 로드맵

직접 해 보기: 보상 모델과 DPO 손실을 넘파이로 계산하기

강의 · 준전문가 · KAITABASE 정리

원제 · Hands-on: Reward Model and DPO Loss in NumPy

브래들리-테리 보상 모델 학습과 DPO 손실 · 기울기를 넘파이로 직접 돌려 보는 실습

먼저 알아야 할 것

브래들리-테리 손실DPO 손실소프트맥스와 로그 확률경사하강법넘파이

다루는 내용

넘파이 코드로 비교 쌍에서 숨은 선호를 복원하는 선형 보상 모델을 학습하고, DPO 손실의 기울기를 유도해 수치 미분으로 검산합니다. 잡음 없는 선호에서는 DPO가 확률을 끝까지 몰아가고, 잡음 있는 선호에서는 결과가 이론상 최적 정책 π_ref·exp(r/β)와 일치함을 실제 실행 결과로 확인합니다.

해설

직접 해 보기: 보상 모델과 DPO 손실을 넘파이로 계산하기

정렬 수업에서 본 두 식, 보상 모델의 브래들리-테리 손실과 DPO 손실을 넘파이(NumPy) 코드 100줄 남짓으로 직접 돌려 봅니다. GPU도, 큰 모델도 필요 없습니다. ① 숨은 '사람 취향'으로 만든 비교 쌍에서 보상 모델이 그 취향을 되찾는지, ② DPO 기울기가 수치 미분과 일치하는지, ③ 잡음 있는 선호로 DPO를 돌리면 이론이 말하는 최적 정책 πrefexp⁡(r/β)\pi_{ref}\exp(r/\beta) 에 정말 도달하는지 확인합니다.

항목내용
참고 출처DPO(arXiv 2305.18290) · InstructGPT(arXiv 2203.02155) — 식만 참고, 코드와 데이터는 이 실습을 위해 새로 작성
트랙 · 수준정렬 · 중급
준비물파이썬 3.10 이상, 넘파이. 실행 시간 수십 초 이내(CPU)
실행 확인이 페이지의 코드는 Windows · Python 3.14 · NumPy 2.5에서 실제로 실행했고, 아래 출력은 그때 결과를 그대로 옮긴 것입니다

들어가기 전에

한눈에 보기

  • 비교 쌍만으로 숨은 선호 가중치를 거의 그대로 복원하는 보상 모델을 학습합니다.
  • DPO 손실의 기울기를 손으로 유도해 코드로 옮기고, 수치 미분으로 검산합니다.
  • 선호가 늘 같은 방향(잡음 없음)이면 DPO가 확률을 끝까지 한쪽으로 몰아간다는 것을 관찰합니다.
  • 잡음 있는 선호에서는 DPO 결과가 πrefexp⁡(r/β)\pi_{ref}\exp(r/\beta) 와 10−1510^{-15} 이하의 차이로 일치하는 것을 확인합니다.

1. 실습 설계

mermaid
flowchart LR
  T[숨은 취향 w*] --> P[비교 쌍 생성<br/>브래들리-테리 확률로 승자 추첨]
  P --> RM[1부: 선형 보상 모델 학습]
  RM --> C[w* 와 비교 · 검증 정확도]
  R[후보 답 4개의 진짜 보상] --> D2[2부: DPO 손실 · 기울기<br/>수치 미분 검산]
  R --> D3[3부: 잡음 있는 선호로 DPO]
  D3 --> TH[이론값 π_ref·exp r/β 와 비교]

왜 이렇게 단순화했나. 실제 보상 모델은 트랜스포머이고 입력은 문장입니다. 하지만 손실 함수의 성질은 입력이 무엇이든 같습니다. 답을 특징 벡터 3개(정확성 · 공손함 · 길이)로, 정책을 후보 답 4개에 대한 소프트맥스로 줄이면 식이 하는 일을 눈으로 확인할 수 있습니다. 숫자와 '취향'은 모두 해설용으로 만든 값입니다.

2. 전체 코드

아래를 align_lab.py 로 저장합니다.

python
# KAITABASE 실습: 보상 모델(브래들리-테리)과 DPO 손실을 넘파이로
import numpy as np
rng = np.random.default_rng(0)

# ---------- 1부: 브래들리-테리 보상 모델 ----------
# 답 하나를 특징 3개로 나타냄: [정확성, 공손함, 길이]
true_w = np.array([2.0, 1.0, -0.5])          # 숨은 '사람 취향' (해설용)
def sample_answers(n):
    return np.column_stack([rng.normal(0, 1, n), rng.normal(0, 1, n), rng.normal(0, 1, n)])

def make_pairs(n):
    A, B = sample_answers(n), sample_answers(n)
    p_a = 1 / (1 + np.exp(-(A @ true_w - B @ true_w)))   # 사람이 A를 고를 확률
    a_wins = rng.random(n) < p_a
    win = np.where(a_wins[:, None], A, B)
    lose = np.where(a_wins[:, None], B, A)
    return win, lose

def sigmoid(z): return 1 / (1 + np.exp(-z))

def train_rm(win, lose, lr=0.5, steps=300):
    w = np.zeros(3)
    for t in range(steps):
        diff = (win - lose) @ w
        loss = -np.mean(np.log(sigmoid(diff)))
        grad = -((1 - sigmoid(diff))[:, None] * (win - lose)).mean(0)
        w -= lr * grad
        if t in (0, 50, 299):
            print(f"  step {t:3d}  loss {loss:.4f}  w {np.round(w, 3)}")
    return w

print("[1부] 보상 모델 학습 (비교 쌍 2,000개)")
win, lose = make_pairs(2000)
w = train_rm(win, lose)
tw, tl = make_pairs(1000)
acc = np.mean((tw - tl) @ w > 0)
acc_true = np.mean((tw - tl) @ true_w > 0)
print(f"  검증 정확도 {acc:.3f} (숨은 취향 자체로 매겨도 {acc_true:.3f} - 라벨에 잡음이 있어서)")
print(f"  학습된 가중치 / 숨은 가중치 비율 {np.round(w / true_w, 2)}")

# ---------- 2부: DPO 손실과 기울기 ----------
# 프롬프트 하나에 후보 답 4개. 정책 = 소프트맥스(logits)
true_r = np.array([1.5, 0.5, 0.0, -1.0])     # 답별 '진짜' 보상(해설용)
ref_logits = np.zeros(4)                     # 기준 모델: 균등 분포
def logp(logits): return logits - np.log(np.exp(logits).sum())

pairs = [(i, j) for i in range(4) for j in range(4) if true_r[i] > true_r[j]]  # (선호, 비선호)

def dpo_loss_grad(theta, beta):
    lp, lr_ = logp(theta), logp(ref_logits)
    p = np.exp(lp)
    loss, grad = 0.0, np.zeros(4)
    for w_, l_ in pairs:
        z = beta * ((lp[w_] - lr_[w_]) - (lp[l_] - lr_[l_]))
        loss += -np.log(sigmoid(z))
        # d logp[k]/d theta = onehot(k) - p
        dz = beta * ((np.eye(4)[w_] - p) - (np.eye(4)[l_] - p))
        grad += -(1 - sigmoid(z)) * dz
    return loss / len(pairs), grad / len(pairs)

def numeric_grad(theta, beta, eps=1e-5):
    g = np.zeros(4)
    for k in range(4):
        e = np.zeros(4); e[k] = eps
        g[k] = (dpo_loss_grad(theta + e, beta)[0] - dpo_loss_grad(theta - e, beta)[0]) / (2 * eps)
    return g

print("\n[2부] DPO: 후보 답 4개, 선호 쌍", len(pairs), "개")
theta0 = np.array([0.3, -0.2, 0.1, 0.0])
l0, g0 = dpo_loss_grad(theta0, 0.1)
print(f"  기울기 확인(해석 vs 수치) 최대 차이 {np.abs(g0 - numeric_grad(theta0, 0.1)).max():.2e}")
for beta in (0.1, 1.0):
    theta = ref_logits.copy()
    for t in range(2001):
        loss, g = dpo_loss_grad(theta, beta)
        if t in (0, 200, 2000):
            p = np.exp(logp(theta))
            imp = beta * (logp(theta) - logp(ref_logits))
            print(f"  beta {beta}  step {t:4d}  loss {loss:.4f}  확률 {np.round(p, 3)}  암묵적 보상 {np.round(imp, 2)}")
        theta -= 1.0 * g

# ---------- 3부: 잡음 있는 선호(브래들리-테리 확률 라벨)로 DPO ----------
# 쌍 (i, j)마다 사람이 i를 고를 확률 q = sigmoid(r_i - r_j). 손실은 q, 1-q로 섞은 교차 엔트로피
def dpo_soft(theta, beta):
    lp, lr_ = logp(theta), logp(ref_logits); p = np.exp(lp)
    loss, grad, n = 0.0, np.zeros(4), 0
    for i in range(4):
        for j in range(i + 1, 4):
            q = sigmoid(true_r[i] - true_r[j])
            z = beta * ((lp[i] - lr_[i]) - (lp[j] - lr_[j]))
            loss += -(q * np.log(sigmoid(z)) + (1 - q) * np.log(sigmoid(-z)))
            dz = beta * ((np.eye(4)[i] - p) - (np.eye(4)[j] - p))
            grad += -(q - sigmoid(z)) * dz
            n += 1
    return loss / n, grad / n

print("\n[3부] 잡음 있는 선호로 DPO -> 이론상 최적 정책 pi_ref * exp(r / beta) 와 비교")
for beta in (1.0, 0.5):
    theta = ref_logits.copy()
    for t in range(20000):
        loss, g = dpo_soft(theta, beta)
        theta -= 2.0 * g
    p = np.exp(logp(theta))
    target = np.exp(logp(ref_logits) + true_r / beta)
    target /= target.sum()
    print(f"  beta {beta}  학습 결과 {np.round(p, 3)}  이론값 {np.round(target, 3)}  최대 차이 {np.abs(p - target).max():.1e}")

실행:

bash
python -X utf8 align_lab.py

-X utf8 은 Windows 콘솔에서 한글 출력이 깨지거나 오류가 나는 것을 막습니다(macOS · 리눅스는 없어도 됩니다).

3. 실행 결과

text
[1부] 보상 모델 학습 (비교 쌍 2,000개)
  step   0  loss 0.6931  w [ 0.222  0.105 -0.058]
  step  50  loss 0.3331  w [ 1.772  0.904 -0.491]
  step 299  loss 0.3303  w [ 2.06   1.057 -0.57 ]
  검증 정확도 0.839 (숨은 취향 자체로 매겨도 0.840 - 라벨에 잡음이 있어서)
  학습된 가중치 / 숨은 가중치 비율 [1.03 1.06 1.14]

[2부] DPO: 후보 답 4개, 선호 쌍 6 개
  기울기 확인(해석 vs 수치) 최대 차이 9.47e-12
  beta 0.1  step    0  loss 0.6931  확률 [0.25 0.25 0.25 0.25]  암묵적 보상 [0. 0. 0. 0.]
  beta 0.1  step  200  loss 0.4891  확률 [0.942 0.055 0.003 0.   ]  암묵적 보상 [ 0.13 -0.15 -0.44 -0.72]
  beta 0.1  step 2000  loss 0.1445  확률 [1. 0. 0. 0.]  암묵적 보상 [ 0.14 -1.22 -2.48 -3.84]
  beta 1.0  step    0  loss 0.6931  확률 [0.25 0.25 0.25 0.25]  암묵적 보상 [0. 0. 0. 0.]
  beta 1.0  step  200  loss 0.0220  확률 [0.96  0.038 0.002 0.   ]  암묵적 보상 [ 1.35 -1.89 -4.85 -8.08]
  beta 1.0  step 2000  loss 0.0025  확률 [0.996 0.004 0.    0.   ]  암묵적 보상 [  1.38  -4.04  -9.18 -14.6 ]

[3부] 잡음 있는 선호로 DPO -> 이론상 최적 정책 pi_ref * exp(r / beta) 와 비교
  beta 1.0  학습 결과 [0.598 0.22  0.133 0.049]  이론값 [0.598 0.22  0.133 0.049]  최대 차이 1.1e-16
  beta 0.5  학습 결과 [0.839 0.114 0.042 0.006]  이론값 [0.839 0.114 0.042 0.006]  최대 차이 8.9e-16

4. 결과 읽기

1부: 보상 모델은 취향을 되찾습니다. 손실이 log⁡2≈0.693\log 2\approx0.693(아무것도 모를 때)에서 0.330까지 내려가고, 학습된 가중치가 숨은 가중치 (2.0, 1.0, −0.5)(2.0,\ 1.0,\ -0.5) 와 비율 1.03~1.14로 가깝습니다. '길이'에 음수 가중치가 붙은 것도 맞게 찾았습니다.

  • 검증 정확도가 0.839인데, 숨은 취향 그대로 채점해도 0.840입니다. 라벨 자체가 확률적으로 뽑혔기 때문에(사람도 늘 같은 선택을 하지 않음) 정확도 100%는 불가능 하고, 이 보상 모델은 그 한계에 거의 닿았습니다.
  • 실제 RLHF에서 보상 모델 정확도가 생각보다 낮게 보이는 이유 중 하나가 이것입니다. 사람끼리도 의견이 갈리는 쌍이 많습니다.

2부: 기울기 유도가 맞습니다. 손으로 쓴 기울기와 수치 미분의 최대 차이가 10−1110^{-11} 수준입니다. 코드의 핵심 한 줄은 소프트맥스 로그 확률의 미분 ∂log⁡pk/∂θ=ek−p\partial\log p_k/\partial\theta=\mathbf e_k-p 입니다.

  • 출발점에서 손실은 정확히 0.6931=log⁡20.6931=\log2 입니다. 정책이 기준 모델과 같으면 암묵적 보상 차이가 0이기 때문입니다.
  • 선호가 늘 같은 방향(답 0 > 1 > 2 > 3)이면, β=0.1\beta=0.1 에서도 2,000걸음 뒤 확률이 (1,0,0,0)(1,0,0,0) 으로 몰립니다. 손실은 계속 줄어들 여지가 있어 확률을 끝까지 밀어붙입니다. β\beta 가 KL 페널티 역할을 한다고 해도, 데이터가 '절대 이긴다'고 말하면 멈추지 않습니다. 실제 선호 데이터가 한쪽으로 치우쳐 있으면 과하게 확신하는 모델이 될 수 있다는 점을 기억해 둡니다.
  • 암묵적 보상의 순서 (0.14>−1.22>−2.48>−3.84)(0.14 > -1.22 > -2.48 > -3.84) 는 진짜 보상 순서와 같습니다.

3부: 이론과 정확히 일치합니다. 사람이 쌍 (i,j)(i,j) 에서 ii 를 고를 확률이 σ(ri−rj)\sigma(r_i-r_j) 인 '잡음 있는' 선호를 쓰면, DPO 결과가 이론상 최적 정책 π∗∝πrefexp⁡(r/β)\pi^*\propto\pi_{ref}\exp(r/\beta) 와 10−1510^{-15} 이하의 차이로 같습니다(부동소수점 오차 수준). DPO 논문의 출발점인 식이 실제로 성립한다는 것을 숫자로 본 것입니다.

  • β=1\beta=1 이면 (0.598,0.220,0.133,0.049)(0.598, 0.220, 0.133, 0.049), β=0.5\beta=0.5 면 (0.839,0.114,0.042,0.006)(0.839, 0.114, 0.042, 0.006) 입니다. β\beta 가 작을수록 좋은 답에 더 쏠립니다(기준 모델에서 더 멀리 감).

5. 바꿔 보며 배우기

  1. 1부에서 비교 쌍을 2,000개 → 50개로 줄여 보세요. 가중치 비율과 검증 정확도는 어떻게 되나요?
    • 실제로 돌려 본 결과: 가중치 비율이 (1.33, 0.98, 1.12)(1.33,\ 0.98,\ 1.12) 로 숨은 값에서 더 벗어나고, 학습 손실은 0.278로 오히려 더 낮아집니다. 적은 데이터의 잡음에 맞춰진 것입니다(과적합). 이 쉬운 문제에서는 검증 정확도가 0.837로 거의 그대로였지만, 특징이 많고 복잡한 실제 보상 모델에서는 데이터 부족이 훨씬 크게 드러납니다.
  2. true_w 의 길이 가중치를 +0.5+0.5 로 바꾸고, 보상 모델로 best-of-nn 을 하면 어떤 답이 뽑힐까요?
    • 예상: 긴 답이 뽑힙니다. 채점기가 '길이'를 좋아하면 정책도 길이를 늘리는 쪽으로 가는 것이 보상 해킹의 씨앗입니다.
  3. 3부에서 ref_logits 를 (0,0,0,2)(0, 0, 0, 2) 처럼 가장 나쁜 답을 좋아하는 기준 모델로 바꿔 보세요.
    • 실제로 돌려 본 결과(β=1\beta=1): (0.455, 0.167, 0.102, 0.276)(0.455,\ 0.167,\ 0.102,\ 0.276) 으로, 가장 나쁜 답 3이 27.6%나 남습니다. 결과가 πrefexp⁡(r/β)\pi_{ref}\exp(r/\beta) 이므로 기준 모델의 선호가 그대로 곱해지기 때문입니다(코드의 이론값도 같은 값으로 나옵니다). DPO는 기준 모델(보통 SFT 모델)의 품질에 크게 기댄다 는 뜻입니다.
  4. 2부의 학습률을 1에서 10으로 올리면?
    • 실제로 돌려 본 결과: 이 작은 문제는 발산하지 않고, β=0.1\beta=0.1 에서 200걸음 만에 확률이 (1,0,0,0)(1,0,0,0) 으로 몰립니다. 같은 걸음 수에서 기준 모델로부터 훨씬 멀리 갑니다. 실제 대형 모델에서는 큰 학습률이 학습을 불안정하게 만들 수 있어, DPO 논문은 학습률 10−610^{-6} 을 썼습니다.

6. 실제 규모로 넘어가려면

  • 특징 벡터 대신 언어 모델의 마지막 은닉 상태에 선형 층을 붙이면 실제 보상 모델 구조가 됩니다(InstructGPT는 SFT 모델의 출력층을 떼고 점수 하나를 내게 했습니다).
  • 후보 4개 대신 답 문장의 토큰 로그 확률 합 을 log⁡π(y∣x)\log\pi(y|x) 로 쓰면 실제 DPO가 됩니다. DPO 논문 부록의 파이토치 의사 코드도 -logsigmoid(beta * (pi_logratios - ref_logratios)) 한 줄이 핵심입니다.
  • 라이브러리로는 Hugging Face TRL의 보상 모델 · DPO 트레이너를 쓸 수 있습니다. 이 실습은 그 안에서 무엇이 계산되는지 보는 용도입니다.

스스로 점검

  1. 1부에서 검증 정확도가 1.0에 못 미치는 것이 학습 실패가 아닌 이유는?
    • 답: 라벨이 브래들리-테리 확률로 뽑혀 원래 잡음이 있고, 숨은 취향으로 채점해도 0.840이 한계이기 때문입니다.
  2. 2부에서 확률이 (1,0,0,0)(1,0,0,0) 으로 몰린 이유는?
    • 답: 모든 쌍에서 같은 쪽이 항상 이긴다는 데이터라, 손실을 더 줄이려면 확률 차이를 끝없이 키우는 것이 유리하기 때문입니다.
  3. 3부 결과가 이론값과 일치한다는 것은 무엇을 확인한 것인가요?
    • 답: KL 페널티가 붙은 보상 최대화의 최적 정책이 πrefexp⁡(r/β)\pi_{ref}\exp(r/\beta) 이고, DPO가 보상 모델 없이 그 정책에 도달한다는 DPO의 핵심 주장을 확인한 것입니다.

용어 정리

용어쉬운 뜻
특징 벡터답을 몇 개의 숫자로 요약한 것(여기서는 정확성 · 공손함 · 길이)
선형 보상 모델특징에 가중치를 곱해 더한 값을 점수로 쓰는 모델
수치 미분입력을 아주 조금 바꿔 손실 변화로 기울기를 근사하는 검산법
확률 라벨(soft label)'항상 A'가 아니라 'A일 확률 0.7'처럼 주는 라벨
최적 정책주어진 목적식을 가장 크게 만드는 정책
best-of-nn답을 nn 개 만들어 점수가 가장 높은 것을 고르는 방법

다음에 볼 것

라이선스

본문과 코드는 KAITABASE가 이 실습을 위해 새로 작성했습니다. 데이터와 '취향' 가중치는 모두 해설용으로 만든 값입니다. 식은 DPO · InstructGPT 원 논문을 따랐습니다.

출처

AI 원리부터 이해하기 · 정렬 모듈

정렬 모듈 학습

이 글은 정렬 모듈의 학습 자료입니다. 전체 10개 자료 중 7번째입니다.

AI 기초 과정은 현재 진도 저장을 지원하지 않습니다.

출처·원문

원문 보기

요약만

본문 · 코드 자체 작성