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

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

DPO 의 원리

운전 연수를 떠올려 보세요. 강사가 "이렇게 하세요, 저렇게는 하지 마세요"를 짝지어 보여 줍니다. 연수생은 좋은 운전은 더 자주, 나쁜 운전은 덜 하게 되지만, 원래의 운전 습관에서 너무 멀어지지는 않습니다. DPO 가 모델을 바꾸는 방법이 이것입니다.

두 답 넣기, 확률 재기, 차이 견주기, 고치기의 네 단계를 이은 DPO 흐름도

1번에서 같은 질문에 고른 답과 반대 답을 함께 넣습니다. 2번에서 학습 중인 모델과 기준 모델(학습 전의 모델)이 두 답을 낼 확률을 각각 잽니다. 3번에서 "기준 모델보다 고른 답의 확률은 얼마나 올렸고, 반대 답의 확률은 얼마나 내렸나"를 견주고, 4번에서 그 차이가 커지도록 LoRA 만 조금 고칩니다. 여기서 강사의 "이렇게, 저렇게는 말고"가 선호 데이터, 원래 운전 습관이 기준 모델입니다.

DPO 의 손실

기호뜻
고른 답의 보상beta × (학습 모델의 로그 확률 - 기준 모델의 로그 확률), 고른 답에서
반대 답의 보상같은 계산, 반대 답에서
차이(margin)고른 답의 보상 - 반대 답의 보상
손실-log(sigmoid(차이)). 차이가 클수록 0 에 가까워진다

기준 모델과 견주는 까닭은, 모델이 반대 답을 피하려고 엉뚱한 말을 지어내지 않게 붙잡아 두기 위해서입니다. beta 가 작을수록 기준 모델에서 멀리 갈 수 있습니다.

Python
import math

def dpo_loss(policy_c, ref_c, policy_r, ref_r, beta=0.1):
    """로그 확률 넷으로 DPO 손실을 계산한다."""
    reward_c = beta * (policy_c - ref_c)          # 고른 답을 기준보다 얼마나 더 좋아하나
    reward_r = beta * (policy_r - ref_r)          # 반대 답을 기준보다 얼마나 더 좋아하나
    margin = reward_c - reward_r
    return -math.log(1 / (1 + math.exp(-margin))), margin

print("학습 전(기준과 같음):", [round(v, 3) for v in dpo_loss(-40, -40, -30, -30)])
print("학습 뒤:", [round(v, 3) for v in dpo_loss(-30, -40, -60, -30)])

학습 전에는 학습 모델과 기준 모델이 같아 차이가 0 이고 손실은 0.693(log 2)입니다. 학습 뒤 고른 답의 로그 확률이 10 오르고 반대 답이 30 내려가면 차이는 4.0, 손실은 0.018 로 줄어듭니다. 실습 6의 학습 기록에서 이 숫자들이 실제로 어떻게 움직이는지 봅니다.

코드에 나오는 함수

코드쉬운 뜻
dpo_loss(policy_c, ref_c, policy_r, ref_r, beta=0.1)로그 확률 넷을 받아 손실과 차이를 돌려준다. policy 는 학습 중인 모델, ref 는 reference(기준 모델), c 는 chosen(고른 답), r 은 rejected(반대 답)다
beta * (policy_c - ref_c)고른 답의 보상이다. 학습 모델이 기준 모델보다 고른 답의 로그 확률을 얼마나 올렸는지에 beta 를 곱한다
1 / (1 + math.exp(-margin))sigmoid(시그모이드, S 자 곡선) 함수다. 어떤 수든 0 과 1 사이로 바꾼다. 차이가 클수록 1 에 가깝다
-math.log(...)확률에 로그를 씌우고 부호를 바꾼다. 1 에 가까운 값은 손실 0 쪽, 0 에 가까운 값은 큰 손실이 된다
round(v, 3)손실과 차이를 소수 셋째 자리까지 줄여 찍는다

이 코드가 뜻하는 것

운전 연수의 점수판으로 읽으면, 연수생이 처음보다 좋은 운전(고른 답)을 얼마나 더 하게 됐고 나쁜 운전(반대 답)을 얼마나 덜 하게 됐는지 재서, 그 간격이 클수록 벌점(손실)을 적게 주는 계산입니다. 여기서 처음 습관이 기준 모델, 벌점이 DPO 손실입니다.

줄하는 일쉽게 말하면
reward_c = beta * (policy_c - ref_c)고른 답을 기준보다 얼마나 더 좋아하는지 잰다좋은 운전이 처음보다 얼마나 늘었나
reward_r = beta * (policy_r - ref_r)반대 답을 기준보다 얼마나 더 좋아하는지 잰다나쁜 운전이 처음보다 얼마나 늘었나
margin = reward_c - reward_r두 보상의 차이다좋은 운전과 나쁜 운전의 간격
return -math.log(sigmoid(margin)), margin간격을 벌점으로 바꾼다간격이 넓을수록 벌점이 작다
dpo_loss(-40, -40, -30, -30)학습 모델과 기준 모델이 똑같은 경우연수 첫날
dpo_loss(-30, -40, -60, -30)고른 답은 -40 에서 -30 으로 오르고, 반대 답은 -30 에서 -60 으로 내려간 경우연수를 마친 뒤

학습 뒤의 차이 4.0 은 0.1 × (10) - 0.1 × (-30) = 1.0 + 3.0 으로 나옵니다. 손실은 이 차이만 보고 정해지므로, 고른 답을 올리든 반대 답을 내리든 간격만 벌어지면 줄어듭니다.

작게 줄여서 눈으로 보기

차이(margin)에 따라 손실이 어떻게 바뀌는지, 그리고 beta 가 차이를 어떻게 키우는지 숫자로 늘어놓아 봅니다.

Python
import math

def loss_from_margin(m):
    return -math.log(1 / (1 + math.exp(-m)))         # -log(sigmoid(차이))

for m in [-2, -1, 0, 1, 2, 4]:
    print(f"차이 {m:>2} → 손실 {loss_from_margin(m):.3f}")

# 같은 로그 확률 변화(고른 답 +10, 반대 답 -30)라도 beta 에 따라 차이가 달라진다
for beta in [0.05, 0.1, 0.5]:
    margin = beta * 10 - beta * (-30)
    print(f"beta {beta}: 차이 {margin:.1f}, 손실 {loss_from_margin(margin):.3f}")
출력
차이 -2 → 손실 2.127
차이 -1 → 손실 1.313
차이  0 → 손실 0.693
차이  1 → 손실 0.313
차이  2 → 손실 0.127
차이  4 → 손실 0.018
beta 0.05: 차이 2.0, 손실 0.127
beta 0.1: 차이 4.0, 손실 0.018
beta 0.5: 차이 20.0, 손실 0.000
결과읽는 법
차이 -2, -1반대 답 쪽으로 기울어 있으면 손실이 0.693 보다 크다. 학습이 이 쌍을 강하게 고치려 한다
차이 0두 답을 똑같이 보는 상태로 손실은 0.693 이다. 학습을 시작할 때의 값이다
차이 1, 2, 4고른 답 쪽으로 기울수록 손실이 0.313, 0.127, 0.018 로 0 에 가까워진다. 이미 잘 고른 쌍은 더 고칠 것이 적다
beta 줄같은 변화라도 beta 가 0.05 면 차이 2.0, 0.5 면 20.0 이다. beta 가 크면 조금만 움직여도 손실이 바로 0 에 가까워져서 더 멀리 갈 까닭이 없다. 그래서 beta 가 작을수록 기준 모델에서 멀리 간다

beta 0.5 의 손실은 0 이 아니라 0 에 아주 가까운 수인데, 소수 셋째 자리까지 줄여 찍어 0.000 으로 보입니다.

주의할 점

  • DPO 는 기준 모델을 따로 GPU 에 올려야 하지만, LoRA 와 함께 쓰면 "LoRA 를 끈 같은 모델"을 기준으로 써서 메모리를 아낍니다. trl 이 알아서 해 줍니다.
  • 학습 전 모델이 고른 답을 거의 내지 못하는 질문에서는 DPO 만으로는 한계가 있습니다. 그럴 때는 SFT 를 먼저 합니다(프로젝트의 순서).
← 이전 절5장 · 선호 데이터다음 절 →5장 · DPO 지표 읽기