모델 운영

MLOPS / 74번째 글

DPO 실전 — 선호 데이터로 모델을 미세조정하기

DPO는 보상 모델 없이 선호 쌍만으로 모델을 정렬합니다. 손실 함수가 실제로 무엇을 하는지, 데이터를 어떻게 만들고 β를 어떻게 잡는지, 그리고 손실이 내려가는데 답이 나빠지는 상황을 어떻게 알아채는지 정리합니다.

PALDYN Team13 MIN READ

지난 글에서 비용을 크게 줄이는 마지막 레버로 미세조정을 꼽았다. 그 미세조정 중에서 「우리가 원하는 말투와 판단으로 답하게 만드는」 쪽을 맡는 것이 DPO(Direct Preference Optimization)다. 어떤 답이 정답인지가 아니라 둘 중 어느 쪽이 더 나은지만 알려 주면 되기 때문에, 정답을 적어 줄 수 없는 종류의 개선에 쓴다.

이름의 「직접」은 무엇을 건너뛰었다는 뜻이다. 기존 방식과 나란히 놓고 보면 무엇을 건너뛰었는지가 바로 보인다.

DPO는 중간 단계 하나를 지운다

RLHF는 선호 데이터로 보상 모델을 먼저 학습한다. 답을 넣으면 점수를 내는 별도의 모델이다. 그다음 강화학습으로 그 점수를 높이도록 정책 모델을 갱신한다. 학습 중에 네 모델이 메모리에 함께 올라가고, 강화학습이라 안정적으로 돌리기 까다롭다.

DPO는 보상 모델을 만들지 않는다. 선호 쌍을 손실 함수에 직접 넣어 한 번에 갱신한다. 지도학습과 같은 모양으로 돌아가므로 다루기가 훨씬 쉽고, 이것이 DPO가 널리 쓰이게 된 이유다.

손실 함수가 하는 일

프롬프트 xx 에 대해 사람이 고른 답을 ywy_w, 버린 답을 yly_l 이라 하자. 학습 중인 모델을 πθ\pi_\theta, SFT를 끝낸 시점에 얼려 둔 사본을 πref\pi_{\text{ref}} 라 하면 손실은 이렇게 생겼다.

LDPO=−E[log⁡σ(βlog⁡πθ(yw∣x)πref(yw∣x)−βlog⁡πθ(yl∣x)πref(yl∣x))]\mathcal{L}_{\text{DPO}} = -\mathbb{E}\left[\log \sigma\left(\beta \log \frac{\pi_\theta(y_w \mid x)}{\pi_{\text{ref}}(y_w \mid x)} - \beta \log \frac{\pi_\theta(y_l \mid x)}{\pi_{\text{ref}}(y_l \mid x)}\right)\right]

복잡해 보이지만 읽는 법은 단순하다. 안쪽의 각 항은 참조 모델 대비 이 답을 얼마나 더 좋아하게 됐는가를 잰다. 좋은 답에서 그 값이 커지고 나쁜 답에서 작아지면 괄호 안이 양수가 되고, σ\sigma 를 지나 손실이 작아진다. 즉 좋은 답의 확률을 올리고 나쁜 답의 확률을 내리되, 둘 다 참조 모델을 기준으로 잰다.

참조 모델이 기준으로 들어가 있다는 것이 핵심이다. 이것이 없으면 모델이 원래 답하던 방식에서 얼마든지 멀어질 수 있고, 그러면 선호 데이터에 없던 능력이 통째로 무너진다. 참조와의 거리를 얼마나 허용할지가 β\beta 다.

  • β\beta 가 크면 참조에서 멀어지지 못한다. 안전하지만 잘 안 바뀐다
  • β\beta 가 작으면 선호 데이터를 강하게 따라간다. 빨리 바뀌지만 다른 능력을 잃는다

보통 0.1 근처에서 시작해 0.05~0.5 범위를 훑는다. β\beta 는 학습률보다 결과를 크게 바꾸므로 먼저 정하고 나머지를 맞추는 편이 낫다.

데이터가 전부다

DPO에 넣는 것은 (x,yw,yl)(x, y_w, y_l) 삼중항이다. 만드는 방법이 몇 가지 있고, 방법이 데이터의 성격을 정한다.

만드는 법 어떻게 주의할 점
사람 비교 두 답을 사람에게 보여 주고 고르게 한다 가장 정확하고 가장 느리다
모델 심판 강한 모델에게 고르게 한다 심판 모델의 취향이 그대로 들어온다
규칙 테스트 통과·형식 준수 여부로 가른다 검증 가능한 과제에서만 된다
기존 대비 새 답과 기존 운영 답을 비교한다 운영 로그가 있어야 한다

어느 방법을 쓰든 지켜야 할 것이 하나 있다. 두 답은 같은 모델이 같은 프롬프트에서 낸 것이어야 한다. 좋은 답을 큰 모델에서 가져오고 나쁜 답을 우리 모델에서 가져오면, 모델은 「좋은 답의 성질」이 아니라 「큰 모델의 문체」를 배운다. 실제로 이렇게 만든 데이터로 학습하면 말투만 바뀌고 판단은 그대로인 결과가 나온다.

그리고 길이를 맞춘다. 사람이든 모델이든 긴 답을 고르는 경향이 있어서, 아무 조치 없이 모으면 ywy_w 가 yly_l 보다 평균적으로 길어진다. 그러면 DPO는 「길게 쓰면 좋다」를 배우고, 학습 후 답이 눈에 띄게 길어진다. 대응은 셋이다 — 쌍의 길이 차이에 상한을 두고 모으거나, 길이 차이가 큰 쌍을 버리거나, 길이를 벌점으로 넣은 변형을 쓴다.

애매한 쌍은 넣지 않는다. 사람 둘이 갈리는 쌍은 정보가 아니라 잡음이다. 선호가 뚜렷한 쌍만 남기면 데이터 수가 줄어드는데, DPO는 수천 쌍 규모에서도 잘 동작하므로 대개 그편이 낫다.

돌리는 모양

trl의 DPOTrainer를 쓰면 코드는 짧다. 중요한 것은 코드가 아니라 그 앞에 SFT가 있어야 한다는 점이다.

from datasets import load_dataset
from trl import DPOConfig, DPOTrainer

# 각 행: prompt / chosen / rejected
dataset = load_dataset("json", data_files="prefs.jsonl", split="train")

config = DPOConfig(
    output_dir="out",
    beta=0.1,
    learning_rate=5e-7,
    num_train_epochs=1,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=8,
    max_length=1024,
    max_prompt_length=512,
)

trainer = DPOTrainer(
    model="./sft-checkpoint",   # SFT를 끝낸 모델
    args=config,
    train_dataset=dataset,
)
trainer.train()

몇 가지 값에 이유가 있다.

학습률이 SFT보다 훨씬 작다. 10−710^{-7} 대를 쓴다. DPO는 이미 쓸 만한 모델을 조금 미는 작업이라 크게 잡으면 그대로 무너진다. SFT에서 쓰던 10−510^{-5} 를 그대로 가져오는 것이 가장 흔한 실수다.

에포크는 1에서 시작한다. 선호 데이터는 같은 쌍을 여러 번 보면 빠르게 과적합한다. 2를 넘기는 경우는 드물다.

참조 모델을 따로 지정하지 않으면 시작 시점의 모델 사본이 쓰인다. 그래서 SFT 없이 베이스 모델에서 바로 DPO를 돌리면 참조가 베이스 모델이 되고, 선호 데이터가 가리키는 방향과 참조가 멀어 학습이 불안정해진다. SFT를 먼저 하는 것은 선택이 아니라 순서다.

메모리가 빠듯하면 LoRA로 정책을 학습하고 어댑터를 끈 상태를 참조로 쓰는 방식이 있다. 참조 모델 사본을 따로 안 들고 있어도 되므로 크게 절약된다.

손실이 내려가는데 답이 나빠질 때

DPO에는 손실 곡선만 봐서는 안 보이는 실패가 있다. 손실은 두 로그확률의 차이로 계산되므로, 좋은 답의 확률이 내려가도 나쁜 답의 확률이 더 빨리 내려가면 손실은 잘 내려간다.

학습 중에 봐야 할 곡선 두 개

오른쪽 모양이 되면 모델이 선호 데이터에 나온 답 전체를 덜 좋아하게 된 것이고, 그 자리를 데이터에 없던 이상한 답이 채운다. 학습은 잘 끝났는데 실제로 써 보면 답이 짧아지거나 회피가 늘어나는 결과로 나타난다.

그래서 학습 중에 최소한 이 셋을 함께 본다.

  • chosen 로그확률. 크게 내려가면 멈춘다. 이것이 가장 중요한 신호다
  • rejected 로그확률. 내려가는 것이 정상이다
  • 정확도 — 괄호 안이 양수인 쌍의 비율. 0.9를 넘어가면 데이터를 다 외운 것이므로 더 돌릴 이유가 없다

대응은 β\beta 를 키우거나, 학습률을 낮추거나, SFT 손실을 조금 섞는 것이다. 마지막 방식은 좋은 답의 확률을 직접 올리는 항을 더하는 것이라 이 실패를 정면으로 막는다.

그리고 선호 데이터에 없는 능력을 따로 재 둔다. 코드 생성이나 수식 계산처럼 선호 쌍에 들어가지 않은 과제의 점수를 학습 전후로 비교한다. 이것이 떨어졌다면 참조에서 너무 멀어진 것이다.

변형들

DPO가 나온 뒤 같은 계열의 방법이 여럿 나왔다. 고르는 기준은 대개 데이터의 모양이다.

방법 무엇이 다른가 어울리는 자리
IPO 과적합을 막는 항으로 손실을 바꿨다 선호가 거의 결정적인 데이터
KTO 쌍이 아니라 답 하나에 좋다·나쁘다만 붙인다 👍👎 로그처럼 쌍이 없는 데이터
ORPO SFT와 정렬을 한 단계로 합친다 SFT를 따로 돌릴 여유가 없을 때
SimPO 참조 모델 없이 길이로 정규화한다 참조 사본을 못 들 만큼 메모리가 빠듯할 때

KTO가 실무에서 특히 쓸모 있다. 운영 서비스에서 모으기 쉬운 것은 「이 답 좋았나요」에 대한 단답이지 두 답의 비교가 아니기 때문이다. 쌍을 만들려고 억지로 짝지으면 앞에서 말한 「같은 모델·같은 프롬프트」 조건이 깨진다.

어느 것을 쓰든 앞의 데이터 규칙과 진단 방법은 그대로 적용된다. 방법을 바꿔서 해결되는 문제보다 데이터를 고쳐서 해결되는 문제가 훨씬 많다.

정리

DPO는 강화학습을 지도학습 모양으로 바꿔 놓아 정렬 작업의 문턱을 크게 낮췄다. 그래서 지금은 「돌릴 수 있는가」가 아니라 「무엇을 넣을 것인가」가 전부인 작업이 됐다.

체크리스트로 줄이면 이렇다. SFT를 먼저 하고, 두 답을 같은 모델에서 뽑고, 길이를 맞추고, 학습률을 10−710^{-7} 대로 낮추고, chosen 로그확률을 지켜본다. 이 다섯 줄에서 어긋나는 것이 DPO 실패의 대부분이다.


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

LATEST

모델 운영의 최신 글

모델 운영2026.09.04

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

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

16 MIN
모델 운영2026.09.04

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

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

21 MIN
모델 운영2026.09.03

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

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

15 MIN