LLM 엔지니어링5장 · LoRA 파인튜닝과 DPO 정렬19 / 23← 이전목차다음 →MetaContents

5장. LoRA 파인튜닝과 DPO 정렬

실습 5 — 선호 데이터와 로그 확률

DPO 데이터 dpo_shop.jsonl 을 살펴보고, 아직 DPO 를 하지 않은 Qwen 지시 모델이 고른 답과 반대 답 가운데 어느 쪽을 더 낼 것 같은지 로그 확률로 재 봅니다. DPO 가 무엇을 바꾸려는지 학습 전에 숫자로 확인하는 실습입니다. CPU 에서도 됩니다.

1단계. 데이터 살펴보기

Python
import json
from collections import Counter

with open("dpo_shop.jsonl", encoding="utf-8") as f:
    pairs = [json.loads(line) for line in f]
print("쌍의 수:", len(pairs))
print(json.dumps(pairs[0], ensure_ascii=False, indent=1))
print(Counter(p["rejected"][:12] for p in pairs))
출력
쌍의 수: 72
{
 "prompt": "배송은 얼마나 걸리나요?",
 "chosen": "안녕하세요, 고객님. 결제 후 1~3일 안에 도착합니다. 제주와 섬 지역은 2일 더 걸립니다. 감사합니다.",
 "rejected": "그건 누리집에 다 나와 있어요. 직접 찾아보세요.",
 "의도": "배송 기간"
}
Counter({'상황에 따라 다르기 때': 27, '그건 누리집에 다 나와': 24, '죄송합니다, 저는 인공': 21})

2단계. 답 하나의 로그 확률 재기

Python
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM

name = "Qwen/Qwen2.5-0.5B-Instruct"
tok = AutoTokenizer.from_pretrained(name)
model = AutoModelForCausalLM.from_pretrained(name, dtype=torch.float32).eval()
SYSTEM = "당신은 온라인 쇼핑몰 고객센터 상담원입니다."

def answer_logprob(q, a):
    """질문 q 다음에 답 a 가 나올 로그 확률의 평균(토큰 하나당)을 돌려준다."""
    msgs = [{"role": "system", "content": SYSTEM}, {"role": "user", "content": q}]
    p_ids = tok(tok.apply_chat_template(msgs, add_generation_prompt=True, tokenize=False), add_special_tokens=False)["input_ids"]
    f_ids = tok(tok.apply_chat_template(msgs + [{"role": "assistant", "content": a}], tokenize=False), add_special_tokens=False)["input_ids"]
    ids = torch.tensor([f_ids])
    with torch.no_grad():
        logits = model(ids).logits[0, :-1]                    # 자리마다 "다음 토큰"의 점수
    logp = torch.log_softmax(logits, dim=-1)                  # 점수를 로그 확률로
    target = ids[0, 1:]                                       # 실제 다음 토큰
    token_logp = logp[torch.arange(len(target)), target]      # 실제 다음 토큰의 로그 확률
    return token_logp[len(p_ids) - 1:].mean().item()          # 답 부분만 평균

p = pairs[0]
print("고른 답:", round(answer_logprob(p["prompt"], p["chosen"]), 3))
print("반대 답:", round(answer_logprob(p["prompt"], p["rejected"]), 3))
출력
고른 답: -2.197
반대 답: -3.197

3단계. 여러 쌍에서 견주기

Python
prefer_chosen = 0
for p in pairs[::6]:                      # 72쌍 가운데 6개 건너 하나씩, 12쌍
    c = answer_logprob(p["prompt"], p["chosen"])
    r = answer_logprob(p["prompt"], p["rejected"])
    prefer_chosen += c > r
    print(f"{'고른 답' if c > r else '반대 답'} 쪽  고른 {c:6.3f}  반대 {r:6.3f}  {p['prompt']}")
print(f"고른 답을 더 낼 것 같은 쌍: {prefer_chosen} / 12")
출력
고른 답 쪽  고른 -2.197  반대 -3.197  배송은 얼마나 걸리나요?
반대 답 쪽  고른 -2.311  반대 -2.134  배송비는 얼마인가요?
반대 답 쪽  고른 -2.040  반대 -1.940  교환은 언제까지 되나요?
반대 답 쪽  고른 -2.505  반대 -2.108  교환 배송비는 누가 내요?
반대 답 쪽  고른 -2.255  반대 -2.082  환불은 언제 되나요?
고른 답 쪽  고른 -1.847  반대 -1.912  포인트로 산 것도 환불돼요?
반대 답 쪽  고른 -2.239  반대 -1.601  주문을 취소하고 싶어요.
고른 답 쪽  고른 -2.193  반대 -3.005  주소를 바꾸고 싶어요.
반대 답 쪽  고른 -1.899  반대 -1.574  어떤 결제 수단이 있나요?
반대 답 쪽  고른 -1.895  반대 -1.737  쿠폰을 여러 장 쓸 수 있나요?
고른 답 쪽  고른 -1.829  반대 -2.074  적립금은 얼마나 쌓여요?
고른 답 쪽  고른 -1.429  반대 -1.974  고객센터 운영 시간이 어떻게 돼요?
고른 답을 더 낼 것 같은 쌍: 5 / 12

이 실습이 뜻하는 것

이 실습은 손님에게서 "더 좋은 응대"와 "아쉬운 응대"를 짝지어 받아 두고, 지금의 직원이 어느 쪽 말을 더 하기 쉬운지 재 보는 것과 같습니다. 짝 목록을 훑어 아쉬운 응대가 어떤 종류인지 세고(1단계), 첫 짝에서 두 응대가 직원 입에서 나올 가능성을 재고(2단계), 12짝으로 넓혀 견줍니다(3단계). 여기서 더 좋은 응대가 고른 답(chosen), 아쉬운 응대가 반대 답(rejected), 나올 가능성이 로그 확률입니다. 비유와 다른 점은, 사람에게 "이 말을 할 가능성"을 숫자로 물을 수는 없지만 모델은 답의 토큰마다 확률을 바로 알려 준다는 것입니다.

단계출력뜻
1. 쌍의 수72질문 하나에 좋은 답과 아쉬운 답이 짝지어진 쌍이 72개입니다
1. 첫 쌍반대 답 "그건 누리집에 다 나와 있어요. 직접 찾아보세요."반대 답은 사실이 틀렸다기보다 불친절하거나 답을 피하는 말입니다
1. 반대 답 종류27, 24, 21반대 답은 "상황에 따라 다르기 때문에", "그건 누리집에 다 나와", "죄송합니다, 저는 인공지능" 세 가지 흠 문장으로 나뉩니다. 합하면 72입니다
2. 첫 쌍고른 -2.197, 반대 -3.1970 에 가까울수록 내기 쉬운 답입니다. 첫 쌍에서는 고른 답 쪽이 더 내기 쉽습니다
3. 12쌍고른 답을 더 낼 것 같은 쌍 5 / 12나머지 7쌍은 반대 답을 더 내기 쉽다고 나왔습니다. 지금의 모델은 두루뭉술하게 피하는 말을 더 자연스럽게 여깁니다

한 줄로 줄이면, 아직 DPO 를 하지 않은 모델은 12쌍 가운데 7쌍에서 아쉬운 답을 더 내기 쉽다는 것을 숫자로 확인한 실습입니다. 이 순서를 뒤집는 학습이 실습 6의 DPO(Direct Preference Optimization, 직접 선호 최적화)입니다. 로그 확률은 "얼마나 내기 쉬운가"를 보일 뿐 "얼마나 좋은 답인가"를 재지는 않고, 72쌍 가운데 12쌍만 본 결과입니다.

코드 설명

  • 로그 확률은 0 에 가까울수록(덜 음수일수록) 모델이 그 답을 낼 것 같다는 뜻입니다. 답의 길이가 다르면 합이 불공평해지므로 토큰 하나당 평균을 냈습니다.
  • logits[0, :-1] 과 ids[0, 1:] 로 한 칸 어긋나게 짝짓는 것은 4장 "사전학습" 절의 한 칸 밀기와 같습니다. 자리마다 "다음에 실제로 온 토큰"의 확률을 꺼냅니다.
  • 첫 쌍에서는 고른 답의 평균 로그 확률이 -2.197, 반대 답이 -3.197 로 고른 답을 더 낼 것 같습니다.
  • 그러나 12쌍 가운데 고른 답 쪽이 더 높은 것은 5쌍뿐이고, 7쌍은 "상황에 따라 다르기 때문에 정확히 말씀드리기 어렵습니다" 같은 반대 답을 더 낼 것 같다고 나옵니다. 지금의 모델이 두루뭉술하게 피하는 답을 더 자연스럽게 여긴다는 뜻입니다. DPO 는 이 순서를 뒤집어, 고른 답의 로그 확률이 반대 답보다 높아지도록 학습합니다.
← 이전 절5장 · 실습 4 — 어댑터 불러오기와 합치기다음 절 →5장 · 실습 6 — DPO 로 답변 성향 맞추기