목록으로

프로그래밍 · Python

Python 완전정복 시리즈 40: DPOTrainer 완벽 이해하기

BeanCon
Python에서 Hugging Face TRL DPOTrainer로 AI 답변 선호도를 학습하는 방법을 설명하는 대표 이미지

Python 완전정복 시리즈 40편입니다. Hugging Face TRL의 DPOTrainer로 좋은 답변과 나쁜 답변을 비교해 AI 모델의 선호도를 학습하는 방법을 정리했습니다. DPO와 Preference Optimization, SFT·RLHF와의 차이, Policy Model과 Reference Model, Chosen·Rejected 응답, DPO Loss, Beta, Preference Dataset, Conversational Preference Format, 길이 편향, 데이터 품질 검사, DPOConfig, Loss Type, Reward Accuracy, Reward Margin, Chosen·Rejected Log Probability, LoRA·QLoRA, Reference Log Probability 사전 계산, Adapter 저장·병합, Hub 업로드, Gradio 비교 화면과 오류 해결까지 다룹니다.

목차

메타 설명
AI 모델이 단순히 정답을 따라 하는 단계를 넘어 더 정확하고 유용한 답변을 선택하도록 만들 수 있을까요? Hugging Face TRL의 DPOTrainer를 이용해 Preference Dataset, Chosen·Rejected 응답, Reference Model, Beta, DPO Loss, Reward Margin, LoRA·QLoRA, Checkpoint, Adapter 저장, 대화형 추론과 Gradio 비교 화면까지 실습합니다.

지난 시간에는 TRL의 `SFTTrainer`를 이용해 질문과 모범 답안으로 대화형 AI 모델을 학습했습니다.

사용자 질문

→ 모범 답안 제공

→ Assistant 답변 Token 학습

→ 질문에 답하는 방법 습득

SFT를 마친 모델은 이전보다 사용자의 지시를 잘 따르고 원하는 형식으로 답할 가능성이 커집니다.

하지만 현실의 답변 품질은 단순히 정답 하나로 정의하기 어렵습니다.

다음 두 답변을 살펴보겠습니다.

질문:

Python의 리스트와 튜플 차이를
초보자에게 설명해 주세요.

첫 번째 답변:

리스트는 mutable이고 튜플은 immutable입니다.

두 번째 답변:

리스트와 튜플은 여러 값을 순서대로 저장합니다.

리스트는 생성 후 값을 추가하거나 수정할 수 있지만
튜플은 일반적으로 생성 후 값을 변경할 수 없습니다.

값이 자주 바뀐다면 리스트,
고정된 데이터를 보관한다면 튜플을 고려할 수 있습니다.

두 답변 모두 완전히 틀렸다고 보기는 어렵습니다.

하지만 초보자에게 더 도움이 되는 답변은 두 번째입니다.

정확성

+ 충분한 설명

+ 사용 상황

+ 이해하기 쉬운 표현

이처럼 AI에게 다음 내용을 가르치고 싶을 수 있습니다.

너무 짧은 답변보다
충분한 설명을 선호한다.

틀린 코드보다
실행 가능한 코드를 선호한다.

질문을 피하는 답변보다
직접적인 답변을 선호한다.

장황한 서론보다
핵심부터 설명하는 답변을 선호한다.

SFT에서는 모범 답안 하나를 제공합니다.

DPO에서는 같은 질문에 대한 좋은 답변과 나쁜 답변을 함께 제공합니다.

질문

├─ Chosen
│  더 선호하는 답변
│
└─ Rejected
   덜 선호하는 답변

오늘의 주인공은 Hugging Face TRL의 `DPOTrainer`입니다.

SFTTrainer:

“이 답변을 따라 하세요.”


DPOTrainer:

“두 답변 중 이쪽이 더 좋습니다.”

AI 훈련소에 모범 답안만 있던 시대가 끝났습니다.

이제 심사위원이 입장합니다. 🧑‍⚖️🤖

1. DPO란?

DPO는 다음 표현의 약자입니다.

Direct Preference Optimization

한국어로는 다음처럼 표현할 수 있습니다.

직접 선호도 최적화

DPO는 같은 Prompt에 대한 두 개의 응답 중 어떤 응답이 더 선호되는지를 학습하는 Post-training 방법입니다.

Prompt

+ Chosen Response

+ Rejected Response

→ 모델 선호도 학습

DPO는 별도의 Reward Model을 먼저 학습하고 강화학습을 수행하는 전통적인 RLHF 파이프라인을 단순화합니다. 원 논문은 인간 선호도 문제를 직접 최적화할 수 있는 형태로 변환해 별도의 Reward Model 학습이나 학습 중 모델 응답 Sampling 없이 단일 분류 형태의 목적함수로 학습하는 방법을 제안했습니다.

2. Preference Optimization이 필요한 이유

SFT는 하나의 모범 답안을 따라 하도록 학습합니다.

질문:

Python 예외 처리를 설명해 주세요.


모범 답안:

try-except는 프로그램 실행 중 발생하는
예외를 처리하는 문법입니다.

하지만 실제 모델의 답변에는 여러 품질 기준이 존재합니다.

사실이 정확한가?

질문에 직접 답했는가?

설명이 충분한가?

코드가 실행 가능한가?

불필요하게 장황하지 않은가?

사용자가 요구한 형식을 따르는가?

위험한 내용을 포함하지 않는가?

모범 답안 하나만으로는 다음과 같은 미묘한 차이를 명확히 가르치기 어렵습니다.

답변 A:

정확하지만 너무 짧음


답변 B:

정확하고 예제도 포함


답변 C:

길지만 핵심과 무관함


답변 D:

자신감 있지만 기술적으로 틀림

Preference Dataset은 답변을 서로 비교해 어떤 특성을 더 선호하는지 보여줍니다.

3. SFT와 DPO의 차이

SFT

질문

→ 정답 하나

→ 정답 Token의 확률을 높임

예:

{
    "prompt": "Python 함수란 무엇인가요?",
    "completion": (
        "함수는 특정 작업을 묶어 "
        "재사용할 수 있게 만든 코드 블록입니다."
    )
}

DPO

질문

→ 좋은 답변

→ 나쁜 답변

→ 좋은 답변을 상대적으로 더 선호하도록 학습

예:

{
    "prompt": "Python 함수란 무엇인가요?",

    "chosen": (
        "함수는 특정 작업을 묶어 "
        "재사용할 수 있게 만든 코드 블록입니다."
    ),

    "rejected": (
        "함수는 Python에서 무조건 실행되는 코드입니다."
    )
}

비교하면 다음과 같습니다.

구분SFTDPO
학습 데이터Prompt와 정답Prompt와 Chosen·Rejected
핵심 목표모범 답안 모방상대적 선호 학습
답변 비교없음있음
일반적인 순서먼저 수행SFT 후 수행
주요 활용지시 수행 학습답변 품질·스타일 정렬
TrainerSFTTrainerDPOTrainer
SFT:

답변하는 방법을 가르침


DPO:

어떤 답변이 더 좋은지 가르침

4. RLHF와 DPO의 차이

전통적인 RLHF 파이프라인을 단순화하면 다음과 같습니다.

1. SFT Model 준비

2. Preference Dataset 수집

3. Reward Model 학습

4. Policy가 답변 생성

5. Reward Model이 점수 계산

6. PPO 등의 강화학습으로 Policy 업데이트

DPO는 다음과 같이 진행됩니다.

1. SFT Model 준비

2. Preference Dataset 수집

3. Chosen·Rejected 확률 비교

4. DPO Loss로 Policy 직접 업데이트

RLHF

Preference Dataset

→ Reward Model

→ 강화학습

→ Policy Model

DPO

Preference Dataset

→ Policy Model 직접 최적화

DPO 원 논문은 Reward Model과 강화학습 루프를 별도로 운영하는 복잡성을 줄이면서 선호도를 직접 최적화하는 방식을 제안했습니다.

DPO가 강화학습에서 다루던 정렬 문제와 관련되어 있다고 해서 일반적인 환경 탐색형 강화학습처럼 행동하는 것은 아닙니다.

환경에서 행동

→ 보상 획득

→ 다음 상태로 이동

대신 이미 수집된 Offline Preference Pair를 이용합니다.

Prompt

Chosen

Rejected

5. DPO의 전체 학습 흐름

SFT 또는 Instruct Model 준비

→ Preference Dataset 준비

→ Prompt에 Chat Template 적용

→ Chosen 답변 Tokenization

→ Rejected 답변 Tokenization

→ Policy Model 확률 계산

→ Reference Model 확률 계산

→ Chosen·Rejected 상대 점수 계산

→ DPO Loss 계산

→ Policy Model 업데이트

TRL의 `DPOTrainer`는 Preference Dataset 전처리, Chat Template 적용, Tokenization, Reference 확률 계산과 DPO Loss 학습을 관리합니다. Conversational Format이 입력되면 Chat Template도 자동으로 적용합니다.

6. Policy Model이란?

Policy Model은 DPO를 통해 실제로 학습되는 모델입니다.

수식에서는 일반적으로 다음 기호로 표현합니다.

πθ
π

→ Policy


θ

→ 현재 학습 중인 파라미터

Policy Model의 목표는 다음과 같습니다.

Chosen 답변:

더 높은 상대적 선호 점수


Rejected 답변:

더 낮은 상대적 선호 점수

LoRA를 사용한다면 전체 기본 모델이 아니라 Adapter 파라미터가 업데이트됩니다.

기본 모델:

동결


DPO LoRA Adapter:

학습

7. Reference Model이란?

Reference Model은 DPO 학습이 시작되기 전 기준이 되는 모델입니다.

수식에서는 다음과 같이 표현합니다.

πref

Reference Model의 역할은 Policy Model이 선호도 데이터에 지나치게 끌려가 기존 능력을 크게 잃지 않도록 기준점을 제공하는 것입니다.

Policy Model:

새로운 선호도를 학습


Reference Model:

학습 전 행동을 기억하는 기준

DPOTrainer에 `ref_model`을 직접 전달할 수 있습니다.

trainer = DPOTrainer(
    model=policy_model,
    ref_model=reference_model,
    train_dataset=train_dataset
)

`ref_model=None`이면 현재 DPOTrainer는 학습 시작 전 Policy에 해당하는 초기 상태를 Reference Policy로 자동 사용합니다.

Reference Model:

“학습 전에는 이렇게 답했습니다.”


Policy Model:

“Preference Dataset을 보고
이 방향으로 바꾸겠습니다.”

8. Chosen과 Rejected

Chosen

같은 Prompt에서 더 선호되는 응답입니다.

정확함

질문에 직접 답함

필요한 예제를 포함함

요청한 형식을 따름

불필요한 내용이 적음

Rejected

같은 Prompt에서 상대적으로 덜 선호되는 응답입니다.

사실 오류

질문 회피

지나치게 짧음

불필요하게 장황함

형식 위반

실행되지 않는 코드

Rejected는 반드시 끔찍한 답변일 필요는 없습니다.

Chosen:

매우 좋은 답변


Rejected:

그럭저럭 괜찮지만
Chosen보다 부족한 답변

이런 미세한 비교도 유용합니다.

9. DPO Loss 직관적으로 이해하기

DPO는 다음 네 가지 확률을 비교합니다.

Policy가 Chosen에 부여한 확률

Policy가 Rejected에 부여한 확률

Reference가 Chosen에 부여한 확률

Reference가 Rejected에 부여한 확률

DPO의 핵심은 단순히 Policy가 Chosen을 좋아하게 만드는 데 그치지 않습니다.

Reference Model과 비교했을 때

Policy Model이 Chosen을
Rejected보다 상대적으로 더 선호하도록 만든다.

DPO Loss는 다음과 같은 방향으로 모델을 학습합니다.

Policy의 Chosen 선호도

-

Policy의 Rejected 선호도

>

Reference의 Chosen·Rejected 선호도 차이

현재 TRL 문서의 기본 DPO Loss는 Policy와 Reference의 Chosen·Rejected Log Probability 비율 차이에 Beta를 적용한 뒤 Sigmoid 기반 손실을 계산합니다.

복잡한 수식을 한 문장으로 번역하면 다음과 같습니다.

학습 전보다

좋은 답변은 더 밀어주고

나쁜 답변은 더 멀리한다.

10. Beta의 의미

DPO의 중요한 설정 중 하나는 `beta`입니다.

beta=0.1

현재 TRL의 기본값도 `0.1`입니다. 공식 문서는 Beta가 Reference Model에서 얼마나 벗어날지를 조절하며, 값이 높을수록 Reference Model에서의 이탈을 더 강하게 제한한다고 설명합니다.

작은 Beta

Preference Signal을 강하게 반영할 수 있음

Reference에서 더 멀리 이동할 수 있음

과도한 선호 최적화 위험 증가

큰 Beta

Reference Model 근처에 머무름

기존 능력을 더 보존

Preference 변화가 약할 수 있음

일반적인 실험 후보:

0.05

0.1

0.2

0.5
Beta가 너무 작음:

“새 규칙이 최고입니다!
기존 지식은 잠시 잊겠습니다.”


Beta가 너무 큼:

“새 규칙은 읽어봤지만
원래 하던 대로 답하겠습니다.”

11. DPO가 실제로 학습하는 것

DPO 데이터가 다음과 같다고 가정하겠습니다.

Chosen:

핵심 설명
+ 짧은 코드
+ 주의사항


Rejected:

정의 한 줄만 제공

이 Pair가 반복되면 모델은 다음 패턴을 선호할 가능성이 커집니다.

한 줄짜리 답변보다
적절한 설명과 예제가 있는 답변

다음 Pair도 학습할 수 있습니다.

Chosen:

모르는 내용은 확인이 필요하다고 답변


Rejected:

확인되지 않은 내용을 사실처럼 생성
Chosen:

사용자가 요구한 JSON만 출력


Rejected:

JSON 앞뒤에 설명 문장 추가

DPO는 추상적인 `좋은 답변`을 자동으로 발견하지 않습니다.

Preference Dataset에 표현된 비교 기준을 배웁니다.

12. DPO의 장점

별도 Reward Model 불필요

Reward Model을 따로 학습하고 배포하지 않아도 됩니다.

강화학습 루프 단순화

PPO 같은 온라인 강화학습 절차보다 구현 구조가 단순합니다.

Offline Dataset 활용

미리 수집한 Preference Pair로 학습할 수 있습니다.

Trainer 통합

TRL의 DPOTrainer가 전처리, Reference 계산과 학습 루프를 제공합니다.

PEFT 연결

LoRA·QLoRA Adapter를 학습할 수 있습니다.

평가 지표 제공

Chosen·Rejected Reward와 Margin을 확인할 수 있습니다.

DPOTrainer는 기본 DPO뿐 아니라 여러 Preference Loss와 PEFT·Quantization 통합도 지원합니다.

13. DPO의 한계

Preference 데이터가 필요함

같은 Prompt에 대한 비교 가능한 두 응답을 수집해야 합니다.

Preference Label 오류에 민감함

Chosen과 Rejected가 뒤바뀌면 모델은 잘못된 방향을 학습합니다.

절대적인 정확성을 보장하지 않음

Chosen이 Rejected보다 낫더라도 Chosen 자체가 틀릴 수 있습니다.

Chosen:

조금 덜 틀린 답변


Rejected:

매우 틀린 답변

모델은 `조금 덜 틀린 답변`을 선호하도록 학습할 수 있습니다.

길이 편향

긴 답변이나 짧은 답변이 데이터에서 일관되게 선호되면 내용보다 길이를 학습할 수 있습니다.

Reference Model 메모리

구성에 따라 Policy와 Reference 계산 때문에 메모리와 연산량이 늘어납니다.

과도한 최적화

특정 Preference Dataset에 지나치게 맞추면 일반적인 능력이 떨어질 수 있습니다.

14. 현재 TRL 버전

2026년 8월 5일 기준 PyPI에 공개된 최신 TRL 안정 버전은 1.9.2이며, 2026년 7월 28일 등록되었습니다. 현재 패키지는 Python 3.10 이상을 요구합니다.

설치 버전 확인:

python -m pip show trl

Python에서 확인:

import trl

print(trl.__version__)

TRL은 빠르게 변경되는 라이브러리입니다.

2024년 코드:

DPOConfig 인수 A


2026년 설치 버전:

DPOConfig 인수 B

실습 코드는 설치한 버전의 공식 문서와 함께 확인해야 합니다.

15. 개발 환경 설치

Windows

python -m venv venv
venv\Scripts\activate

python -m pip install --upgrade pip
python -m pip install "transformers[torch]" datasets accelerate peft trl

macOS·Linux

python3 -m venv venv
source venv/bin/activate

python -m pip install --upgrade pip
python -m pip install "transformers[torch]" datasets accelerate peft trl

QLoRA까지 사용할 경우

python -m pip install bitsandbytes

설치 확인

import datasets
import peft
import torch
import transformers
import trl

print(f"Transformers: {transformers.__version__}")
print(f"TRL: {trl.__version__}")
print(f"PEFT: {peft.__version__}")
print(f"Datasets: {datasets.__version__}")
print(f"PyTorch: {torch.__version__}")
print(f"CUDA: {torch.cuda.is_available()}")

16. DPOTrainer란?

`DPOTrainer`는 Preference Dataset으로 Causal Language Model을 학습하는 TRL Trainer입니다.

가장 단순한 형태는 다음과 같습니다.

from datasets import load_dataset
from trl import DPOTrainer

dataset = load_dataset(
    "trl-lib/ultrafeedback_binarized",
    split="train"
)

trainer = DPOTrainer(
    model="Qwen/Qwen3-0.6B",
    train_dataset=dataset
)

trainer.train()

현재 DPOTrainer는 모델 ID, 모델 객체 또는 PEFT 모델 객체를 받을 수 있으며, `ref_model=None`일 때 학습 전 Policy를 Reference로 자동 사용합니다. 또한 `peft_config`와 `quantization_config`를 직접 받아 LoRA·QLoRA 학습을 구성할 수 있습니다.

17. DPO 데이터 형식

DPOTrainer는 Preference Dataset을 사용합니다.

필수 개념은 다음과 같습니다.

prompt

chosen

rejected

TRL은 Standard Format과 Conversational Format을 모두 지원합니다. 명시적 Prompt가 없는 Implicit Format도 처리할 수 있지만 공식 문서는 가능한 경우 Explicit Prompt 사용을 권장합니다.

18. Standard Preference Format

일반 문자열로 구성합니다.

example = {
    "prompt": (
        "Python에서 리스트와 튜플의 차이는?"
    ),

    "chosen": (
        "리스트는 변경할 수 있고 "
        "튜플은 일반적으로 변경할 수 없습니다."
    ),

    "rejected": (
        "둘은 완전히 같은 자료형입니다."
    )
}

간단한 Completion Model이나 직접 Chat Template을 적용한 문자열 데이터에서 사용할 수 있습니다.

19. Conversational Preference Format

역할 기반 메시지로 구성합니다.

example = {
    "prompt": [
        {
            "role": "system",
            "content": (
                "당신은 친절한 Python 교관입니다."
            )
        },
        {
            "role": "user",
            "content": (
                "리스트와 튜플의 차이를 설명해 주세요."
            )
        }
    ],

    "chosen": [
        {
            "role": "assistant",
            "content": (
                "리스트는 값을 변경할 수 있지만 "
                "튜플은 일반적으로 변경할 수 없습니다."
            )
        }
    ],

    "rejected": [
        {
            "role": "assistant",
            "content": (
                "둘은 같은 것입니다."
            )
        }
    ]
}

Conversational Dataset을 전달하면 DPOTrainer가 모델의 Chat Template을 자동으로 적용합니다.

이번 프로젝트에서는 이 형식을 사용합니다.

20. Explicit Prompt와 Implicit Prompt

Explicit Prompt

{
    "prompt": "질문",
    "chosen": "좋은 답변",
    "rejected": "나쁜 답변"
}

질문과 두 Completion이 분리되어 있습니다.

Implicit Prompt

{
    "chosen": "질문 + 좋은 답변",
    "rejected": "질문 + 나쁜 답변"
}

DPOTrainer가 Chosen과 Rejected의 공통 앞부분에서 Prompt를 추출할 수 있습니다.

하지만 데이터 생성과 검증이 쉬운 Explicit Prompt를 권장합니다.

Explicit Prompt:

질문 위치가 명확함

답변 비교가 쉬움

중복 제거가 쉬움

데이터 오류 탐지가 쉬움

21. Preference Dataset 만들기

Python 교관용 데이터를 만들어 보겠습니다.

SYSTEM_MESSAGE = (
    "당신은 Python 입문자를 위한 친절하고 정확한 교관입니다. "
    "핵심부터 설명하고 필요한 경우 짧은 코드 예제를 제공합니다."
)

Preference Pair:

````pythonPREFERENCE_EXAMPLES = [{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("Python 변수란 무엇인가요?")}],

"chosen": [{"role": "assistant","content": ("변수는 값을 저장하기 위해 사용하는 이름입니다.\n\n""```python\n""age = 20\n""print(age)\n""```\n\n""이 코드에서 age는 숫자 20을 가리키는 변수입니다.")}],

"rejected": [{"role": "assistant","content": ("변수는 그냥 Python에 있는 기능입니다.")}]}]````

22. 좋은 Chosen 데이터 만들기

Chosen은 단순히 Rejected보다 길다고 좋은 답변이 아닙니다.

다음 기준을 검토합니다.

사실 정확성

Python 문법과 동작이 정확한가?

질문 관련성

질문에 직접 답하는가?

완전성

초보자가 이해할 핵심 정보가 포함되었는가?

실행 가능성

코드 예제가 실제로 실행되는가?

형식 준수

사용자가 요구한 표·JSON·코드 형식을 따르는가?

적절한 길이

필요한 내용은 포함하지만
불필요한 반복은 없는가?

23. 좋은 Rejected 데이터 만들기

Rejected는 실제 모델에서 발생할 수 있는 실패 유형을 반영하는 것이 좋습니다.

사실 오류

튜플은 언제든 수정할 수 있습니다.

질문 회피

Python은 매우 인기 있는 언어입니다.

설명 부족

리스트는 리스트입니다.

실행되지 않는 코드

for item of items:
    print(item)

잘못된 형식

사용자가 JSON만 요청했지만 다음처럼 응답합니다.

설명해 드리겠습니다.

{
    "result": true
}

과도한 장황함

간단한 질문에 관련 없는 역사와 철학을 여러 문단으로 설명합니다.

Rejected는 모델이 실제로 고쳐야 할 실패 유형을 대표해야 합니다.

24. 애매한 Preference Pair

다음 Pair는 품질 차이가 너무 작을 수 있습니다.

Chosen:

리스트는 값을 저장합니다.


Rejected:

리스트에는 값을 저장합니다.

둘의 의미와 품질이 거의 같습니다.

Labeler마다 선택이 달라질 수 있습니다.

반대로 차이가 너무 쉬운 Pair만 있어도 문제가 됩니다.

Chosen:

정확하고 친절한 답변


Rejected:

무작위 기호 문자열

모델은 섬세한 답변 품질보다 `정상적인 문장인지 여부`만 배울 수 있습니다.

좋은 Dataset에는 난이도가 다양한 Pair가 필요합니다.

명백한 사실 오류

설명 부족

형식 위반

미묘한 정확성 차이

장황함 차이

사용자 의도 이해 차이

25. 답변 길이 편향

Preference Dataset에서 항상 긴 답변이 Chosen이라면 모델은 다음 규칙을 잘못 학습할 수 있습니다.

좋은 답변

=

긴 답변

항상 짧은 답변이 Chosen이라면 다음과 같은 모델이 될 수 있습니다.

사용자:

자세히 설명해 주세요.


모델:

네.

길이 편향을 줄이려면 다음 Pair를 섞습니다.

짧지만 정확한 Chosen

긴데 관련 없는 Rejected
충분히 자세한 Chosen

지나치게 짧은 Rejected

현재 TRL은 기본 Sigmoid DPO 외에도 Completion Token 수로 정규화해 길이 편향을 다루는 `sigmoid_norm` Loss를 제공합니다.

26. 데이터 품질 검사

def validate_preference_example(
    example: dict,
    index: int
) -> None:
    required_columns = {
        "prompt",
        "chosen",
        "rejected"
    }

    missing_columns = (
        required_columns
        - set(example.keys())
    )

    if missing_columns:
        raise ValueError(
            f"{index}번 데이터에 열이 없습니다: "
            f"{sorted(missing_columns)}"
        )

    for column_name in (
        "prompt",
        "chosen",
        "rejected"
    ):
        messages = example[column_name]

        if not isinstance(messages, list):
            raise TypeError(
                f"{index}번 {column_name}은 "
                "메시지 리스트여야 합니다."
            )

        if not messages:
            raise ValueError(
                f"{index}번 {column_name}이 비어 있습니다."
            )

        for message in messages:
            role = message.get("role")
            content = str(
                message.get("content", "")
            ).strip()

            if role not in {
                "system",
                "user",
                "assistant"
            }:
                raise ValueError(
                    f"{index}번 데이터의 역할이 잘못되었습니다: "
                    f"{role}"
                )

            if not content:
                raise ValueError(
                    f"{index}번 데이터에 빈 메시지가 있습니다."
                )

    chosen_text = example["chosen"][-1]["content"].strip()
    rejected_text = example["rejected"][-1]["content"].strip()

    if chosen_text == rejected_text:
        raise ValueError(
            f"{index}번 Chosen과 Rejected가 같습니다."
        )

    if example["chosen"][-1]["role"] != "assistant":
        raise ValueError(
            f"{index}번 Chosen은 assistant여야 합니다."
        )

    if example["rejected"][-1]["role"] != "assistant":
        raise ValueError(
            f"{index}번 Rejected는 assistant여야 합니다."
        )

27. Train·Validation 분리

from datasets import Dataset

dataset = Dataset.from_list(
    PREFERENCE_EXAMPLES
)

split_dataset = dataset.train_test_split(
    test_size=0.2,
    seed=2026,
    shuffle=True
)

train_dataset = split_dataset["train"]
validation_dataset = split_dataset["test"]

실제 프로젝트에서는 세 종류로 분리하는 편이 좋습니다.

Train:

DPO 학습


Validation:

Beta·Learning Rate·Checkpoint 선택


Test:

최종 Preference 평가

동일하거나 매우 유사한 Prompt가 Train과 Test에 동시에 포함되지 않도록 주의합니다.

28. Tokenizer와 Chat Template

from transformers import AutoTokenizer

MODEL_ID = (
    "Qwen/"
    "Qwen2.5-0.5B-Instruct"
)

tokenizer = AutoTokenizer.from_pretrained(
    MODEL_ID
)

Padding Token 확인:

if tokenizer.pad_token is None:
    tokenizer.pad_token = tokenizer.eos_token

DPOTrainer의 Processing Class는 Padding Token이 필요하며 현재 문서는 Padding 방향을 왼쪽으로 설정하도록 안내합니다. Padding Token이 없다면 EOS Token을 기본값으로 사용할 수 있습니다.

tokenizer.padding_side = "left"

Chat Template 확인:

sample_prompt = (
    PREFERENCE_EXAMPLES[0]["prompt"]
)

formatted_prompt = (
    tokenizer.apply_chat_template(
        sample_prompt,
        tokenize=False,
        add_generation_prompt=True
    )
)

print(formatted_prompt)

29. LoRA 설정

from peft import (
    LoraConfig,
    TaskType
)

lora_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    inference_mode=False,

    r=16,
    lora_alpha=32,
    lora_dropout=0.05,

    target_modules=[
        "q_proj",
        "k_proj",
        "v_proj",
        "o_proj"
    ],

    bias="none"
)

DPOTrainer에 `peft_config`를 전달하면 모델을 PEFT 모델로 감싸 LoRA Adapter만 학습할 수 있습니다. 이미 학습 가능한 PEFT 모델을 직접 전달하는 경우에는 `peft_config`를 다시 전달하지 않습니다.

30. DPOConfig 이해하기

from trl import DPOConfig

training_args = DPOConfig(
    output_dir=(
        "outputs/python-tutor-dpo"
    ),

    learning_rate=1e-5,

    per_device_train_batch_size=1,
    per_device_eval_batch_size=1,

    gradient_accumulation_steps=8,

    num_train_epochs=3,

    beta=0.1,
    loss_type=["sigmoid"],

    max_length=512,

    eval_strategy="epoch",
    save_strategy="epoch",

    load_best_model_at_end=True,
    metric_for_best_model="eval_loss",
    greater_is_better=False,

    logging_steps=5,

    save_total_limit=2,

    report_to="none"
)

현재 DPOConfig는 일반 TrainingArguments 설정과 함께 다음 DPO 전용 옵션을 제공합니다.

beta

loss_type

loss_weights

max_length

truncation_mode

precompute_ref_log_probs

label_smoothing

sync_ref_model

activation_offloading

현재 기본값은 `beta=0.1`, `max_length=1024`, `loss_type=["sigmoid"]`이며, DPOConfig의 기본 Learning Rate는 일반 TrainingArguments보다 작은 `1e-6`입니다.

31. Beta 설정하기

beta=0.1

실험 예:

실험 A:

beta=0.05


실험 B:

beta=0.1


실험 C:

beta=0.2

비교할 항목:

Reward Accuracy

Reward Margin

Validation Loss

Chosen 답변 생성 비율

답변 다양성

기존 지식 유지

형식 준수

Beta 하나만 보고 결과를 판단하지 않습니다.

Reward Margin 상승

하지만

일반 질문 성능 하락

이런 현상이 발생할 수 있습니다.

32. Loss Type 설정하기

기본 DPO:

loss_type=["sigmoid"]

현재 TRL은 여러 Preference Loss를 지원합니다.

Sigmoid

loss_type=["sigmoid"]

기본 DPO Loss입니다.

Hinge

loss_type=["hinge"]

Margin 기반 목적함수를 사용합니다.

IPO

loss_type=["ipo"]

Preference Logit의 과적합 문제를 줄이려는 변형입니다.

Robust DPO

loss_type=["robust"],
label_smoothing=0.1

Preference Label에 오류가 있을 수 있다고 가정합니다.

Sigmoid Norm

loss_type=["sigmoid_norm"]

Completion 길이를 기준으로 정규화합니다.

SFT Loss 결합

loss_type=[
    "sigmoid",
    "sft"
],

loss_weights=[
    1.0,
    0.2
]

현재 DPOTrainer는 여러 Loss를 동시에 결합하는 기능도 제공합니다.

첫 실습에서는 기본 Sigmoid Loss부터 사용합니다.

33. 최대 입력 길이

max_length=512

DPO 데이터 한 건에는 다음 내용이 포함됩니다.

Prompt

+

Chosen Completion

또는:

Prompt

+

Rejected Completion

길이가 `max_length`를 초과하면 잘립니다.

현재 DPOConfig의 기본 Truncation Mode는 `keep_start`이며, `keep_end`는 폐기 예정입니다.

길이가 너무 작으면 답변 뒷부분이 잘릴 수 있습니다.

Chosen:

정확한 설명

+ 코드

+ 마지막 주의사항


Truncation:

마지막 주의사항 삭제

데이터의 Token 길이 분포를 확인한 뒤 결정합니다.

34. 학습률과 Batch Size

현재 DPOConfig의 기본 Learning Rate는 `1e-6`입니다.

LoRA Adapter를 학습할 때는 새 파라미터만 업데이트하기 때문에 공식 예제는 약 `1e-5` 수준의 더 높은 Learning Rate를 일반적인 시작점으로 안내합니다.

learning_rate=1e-5

GPU 메모리가 부족하면 다음과 같이 구성합니다.

per_device_train_batch_size=1
gradient_accumulation_steps=8

한 장치 기준 유효 Batch Size:

1 × 8

= 8

DPO는 Chosen과 Rejected를 모두 처리하고 Reference 확률도 계산하므로 일반 SFT보다 메모리 사용량이 커질 수 있습니다.

35. DPOTrainer 생성

from trl import DPOTrainer

trainer = DPOTrainer(
    model=MODEL_ID,

    ref_model=None,

    args=training_args,

    train_dataset=train_dataset,
    eval_dataset=validation_dataset,

    processing_class=tokenizer,

    peft_config=lora_config
)

구성 요소:

model

→ 학습할 Policy Model


ref_model

→ Reference Model
→ None이면 초기 Policy를 기준으로 사용


args

→ DPO 학습 설정


train_dataset

→ Preference 학습 데이터


eval_dataset

→ Validation Preference 데이터


processing_class

→ Tokenizer와 Chat Template


peft_config

→ LoRA Adapter 설정

36. Reference Model 자동 처리

다음처럼 `ref_model=None`으로 설정했습니다.

ref_model=None

현재 DPOTrainer는 별도 Reference Model이 전달되지 않으면 DPO 학습 시작 전 초기 Policy를 Reference Policy로 사용합니다.

직접 Reference Model을 전달할 수도 있습니다.

from transformers import (
    AutoModelForCausalLM
)

reference_model = (
    AutoModelForCausalLM
    .from_pretrained(
        MODEL_ID
    )
)

trainer = DPOTrainer(
    model=policy_model,
    ref_model=reference_model,
    args=training_args,
    train_dataset=train_dataset
)

하지만 Policy와 Reference를 동시에 GPU에 올리면 메모리 사용량이 커집니다.

37. DPO 학습 시작

train_result = trainer.train()

학습 중 내부 흐름:

Prompt + Chosen Tokenization

Prompt + Rejected Tokenization

→ Policy Chosen Log Probability

→ Policy Rejected Log Probability

→ Reference Chosen Log Probability

→ Reference Rejected Log Probability

→ DPO Reward 계산

→ DPO Loss 계산

→ LoRA Adapter 업데이트

결과 저장:

trainer.log_metrics(
    "train",
    train_result.metrics
)

trainer.save_metrics(
    "train",
    train_result.metrics
)

trainer.save_state()

38. DPO 로그 읽기

DPOTrainer는 다음과 같은 지표를 기록합니다.

loss

rewards/chosen

rewards/rejected

rewards/margins

rewards/accuracies

logps/chosen

logps/rejected

logits/chosen

logits/rejected

entropy

mean_token_accuracy

grad_norm

learning_rate

이 지표들은 단순 Training Loss보다 DPO 학습 방향을 더 자세히 보여줍니다.

39. Reward Accuracy

rewards/accuracies

Chosen의 Implicit Reward가 Rejected보다 높은 Pair의 비율입니다.

예:

Preference Pair:
100개

Chosen Reward가 더 높은 Pair:
78개

Reward Accuracy:
0.78

값이 증가하면 모델이 Preference Pair의 방향을 더 잘 구분하고 있다는 신호가 될 수 있습니다.

하지만 다음도 확인해야 합니다.

Train Reward Accuracy:
0.99

Validation Reward Accuracy:
0.61

이 경우 Train Preference Pair를 외운 과적합일 수 있습니다.

40. Reward Margin

rewards/margins

Chosen Reward와 Rejected Reward의 차이입니다.

Reward Margin

=

Chosen Reward

-

Rejected Reward

예:

Chosen Reward:
1.2

Rejected Reward:
0.3

Margin:
0.9

Margin이 양수이면 Chosen을 더 선호합니다.

Margin < 0

→ Rejected를 더 선호


Margin ≈ 0

→ 두 답변을 비슷하게 평가


Margin > 0

→ Chosen을 더 선호

현재 DPOTrainer는 Chosen·Rejected Reward와 그 차이인 Reward Margin을 로그로 제공합니다.

41. Chosen·Rejected Log Probability

logps/chosen

logps/rejected

각 Completion Token Sequence에 모델이 부여한 평균 Log Probability입니다.

직관적으로는 다음처럼 볼 수 있습니다.

높은 Log Probability

→ 모델이 해당 답변을 더 자연스럽게 생성할 가능성


낮은 Log Probability

→ 모델이 해당 답변을 덜 생성할 가능성

다만 DPO는 Policy의 값만 보는 것이 아니라 Reference와의 상대적 변화도 함께 사용합니다.

Policy Chosen 확률 증가

만으로 판단하지 않음

Reference 대비
Chosen·Rejected 변화 차이를 비교

공식 문서는 실제 학습에서 Chosen 확률을 올리기보다 Rejected 확률을 억제하는 형태로 Margin이 벌어지는 경우가 흔하다고 설명합니다.

42. Validation 평가

validation_metrics = (
    trainer.evaluate()
)

print(validation_metrics)

저장:

trainer.log_metrics(
    "validation",
    validation_metrics
)

trainer.save_metrics(
    "validation",
    validation_metrics
)

확인할 항목:

eval_loss

eval_rewards/chosen

eval_rewards/rejected

eval_rewards/margins

eval_rewards/accuracies

최적 Checkpoint 선택:

load_best_model_at_end=True
metric_for_best_model="eval_loss"
greater_is_better=False

Reward Accuracy를 사용할 수도 있지만 Metric 이름이 실제 로그에 어떻게 저장되는지 먼저 확인하는 편이 안전합니다.

43. Adapter 저장

from pathlib import Path

ADAPTER_DIR = Path(
    "models/python-tutor-dpo-lora"
)

trainer.save_model(
    str(ADAPTER_DIR)
)

tokenizer.save_pretrained(
    ADAPTER_DIR
)

LoRA를 사용하면 일반적으로 다음 파일이 저장됩니다.

adapter_config.json

adapter_model.safetensors

tokenizer.json

tokenizer_config.json

기본 모델 전체가 아니라 DPO로 학습된 Adapter가 저장됩니다.

44. 저장된 DPO Adapter 불러오기

from peft import (
    AutoPeftModelForCausalLM
)

model = (
    AutoPeftModelForCausalLM
    .from_pretrained(
        ADAPTER_DIR
    )
)

model.eval()

장치 선택:

import torch

if torch.cuda.is_available():
    device = torch.device("cuda")

elif torch.backends.mps.is_available():
    device = torch.device("mps")

else:
    device = torch.device("cpu")

model = model.to(device)

45. 대화형 추론

messages = [
    {
        "role": "system",
        "content": SYSTEM_MESSAGE
    },
    {
        "role": "user",
        "content": (
            "Python에서 try-except는 "
            "언제 사용하나요?"
        )
    }
]

Chat Template 적용:

input_ids = (
    tokenizer.apply_chat_template(
        messages,
        tokenize=True,
        add_generation_prompt=True,
        return_tensors="pt"
    )
    .to(device)
)

생성:

with torch.inference_mode():
    output_ids = model.generate(
        input_ids=input_ids,

        max_new_tokens=256,

        do_sample=False,

        eos_token_id=(
            tokenizer.eos_token_id
        ),

        pad_token_id=(
            tokenizer.pad_token_id
        )
    )

새 답변만 추출:

generated_ids = output_ids[
    0,
    input_ids.shape[-1]:
]

response = tokenizer.decode(
    generated_ids,
    skip_special_tokens=True
)

print(response)

46. DPO 전후 결과 비교

같은 Prompt를 초기 모델과 DPO 모델에 입력합니다.

질문:

Python 리스트 컴프리헨션을
초보자에게 설명해 주세요.

비교 항목:

항목초기 모델DPO 모델
사실 정확성평가평가
핵심부터 설명평가평가
코드 예제평가평가
질문 관련성평가평가
불필요한 반복평가평가
초보자 친화성평가평가

DPO Dataset에서 선호한 특성이 실제 생성 결과에 반영되는지 확인합니다.

학습 데이터:

코드 예제가 있는 답변 선호


평가 결과:

DPO 모델이 코드 예제를 더 자주 생성하는가?

47. Preference 평가표 만들기

사람 평가자는 두 모델의 이름을 숨긴 상태에서 답변을 비교하는 것이 좋습니다.

답변 A

답변 B

어느 답변이 더 좋은가?

평가 CSV 예:

question,answer_a,answer_b,preferred,reason

평가 기준:

A가 더 좋음

B가 더 좋음

비슷함

둘 다 나쁨

승률 계산:

import pandas as pd

results = pd.read_csv(
    "preference_evaluation.csv"
)

win_rate = (
    results["preferred"]
    .eq("dpo")
    .mean()
)

print(
    f"DPO 승률: {win_rate:.2%}"
)

Reward Accuracy가 높아도 실제 생성 답변의 사람 선호도가 높지 않을 수 있습니다.

학습 지표와 생성 평가를 함께 봐야 합니다.

48. QLoRA로 DPO 학습하기

큰 모델에서는 기본 모델을 4비트로 불러오고 LoRA Adapter를 학습할 수 있습니다.

import torch

from transformers import (
    BitsAndBytesConfig
)

compute_dtype = (
    torch.bfloat16
    if (
        torch.cuda.is_available()
        and torch.cuda.is_bf16_supported()
    )
    else torch.float16
)

quantization_config = (
    BitsAndBytesConfig(
        load_in_4bit=True,

        bnb_4bit_quant_type="nf4",

        bnb_4bit_use_double_quant=True,

        bnb_4bit_compute_dtype=(
            compute_dtype
        )
    )
)

DPOTrainer 연결:

trainer = DPOTrainer(
    model=MODEL_ID,

    args=training_args,

    train_dataset=train_dataset,
    eval_dataset=validation_dataset,

    processing_class=tokenizer,

    peft_config=lora_config,

    quantization_config=(
        quantization_config
    )
)

현재 DPOTrainer는 모델을 ID 문자열로 전달한 경우 `quantization_config`와 `peft_config`를 결합해 QLoRA 학습을 구성할 수 있습니다. 이미 모델 객체를 생성해 전달하면 Trainer의 `quantization_config`는 무시됩니다.

49. Reference Log Probability 사전 계산

DPO 학습 중에는 Reference Model의 Chosen·Rejected Log Probability가 필요합니다.

이를 학습 전에 미리 계산할 수 있습니다.

precompute_ref_log_probs=True

Batch Size:

precompute_ref_batch_size=4

장점:

Reference Log Probability를 먼저 계산

→ 학습 중 Reference Forward Pass 감소

→ Reference Model을 계속 메모리에 유지할 필요 감소 가능

현재 DPOConfig는 이 기능이 Reference Model 메모리를 절약할 수 있다고 설명합니다. 다만 IterableDataset, Liger Kernel의 일부 설정, Reference 동기화 기능과는 함께 사용할 수 없습니다.

training_args = DPOConfig(
    precompute_ref_log_probs=True,
    precompute_ref_batch_size=4
)

데이터셋이 크면 사전 계산 단계에도 시간이 필요합니다.

50. Checkpoint와 학습 재개

최근 Checkpoint에서 재개:

trainer.train(
    resume_from_checkpoint=True
)

특정 Checkpoint:

trainer.train(
    resume_from_checkpoint=(
        "outputs/python-tutor-dpo/"
        "checkpoint-100"
    )
)

DPOTrainer는 Transformers Trainer를 기반으로 하며 모델, Optimizer와 Scheduler 상태가 포함된 Checkpoint에서 학습을 재개할 수 있습니다.

다음 폴더를 혼동하지 않습니다.

checkpoint-100/

→ 학습 재개용


models/python-tutor-dpo-lora/

→ 최종 추론·배포용

51. Adapter 병합

merged_model = (
    model.merge_and_unload(
        safe_merge=True
    )
)

저장:

MERGED_MODEL_DIR = Path(
    "models/python-tutor-dpo-merged"
)

merged_model.save_pretrained(
    MERGED_MODEL_DIR
)

tokenizer.save_pretrained(
    MERGED_MODEL_DIR
)

병합 후에는 일반 Transformers 모델처럼 사용할 수 있습니다.

병합 전:

Base Model
+ DPO Adapter


병합 후:

전체 단일 모델

단, Adapter 교체 기능과 작은 저장 용량의 장점은 사라집니다.

52. Hugging Face Hub 업로드

로그인:

hf auth login

DPOConfig:

training_args = DPOConfig(
    push_to_hub=True,

    hub_model_id=(
        "사용자명/"
        "python-tutor-dpo-lora"
    ),

    hub_private_repo=True,

    hub_strategy="end"
)

업로드:

trainer.push_to_hub(
    commit_message=(
        "Align Python tutor "
        "with DPO preferences"
    )
)

DPOTrainer의 `push_to_hub()`은 학습 모델과 Processing Class를 설정된 Hub Repository에 업로드합니다.

53. Gradio 비교 화면

초기 모델과 DPO 모델의 답변을 나란히 비교할 수 있습니다.

import gradio as gr


def compare_answers(
    question: str
) -> tuple[str, str]:
    cleaned_question = question.strip()

    if not cleaned_question:
        raise gr.Error(
            "비교할 질문을 입력해 주세요."
        )

    base_answer = generate_answer(
        base_model,
        base_tokenizer,
        base_device,
        cleaned_question
    )

    dpo_answer = generate_answer(
        dpo_model,
        dpo_tokenizer,
        dpo_device,
        cleaned_question
    )

    return (
        base_answer,
        dpo_answer
    )


with gr.Blocks() as demo:
    gr.Markdown(
        """
        # ⚖️ DPO 답변 비교 연구소

        초기 모델과 DPO 모델의 답변을
        같은 질문으로 비교합니다.
        """
    )

    question_input = gr.Textbox(
        label="Python 질문",
        lines=4,
        value=(
            "Python의 리스트와 튜플 차이를 "
            "초보자에게 설명해 주세요."
        )
    )

    compare_button = gr.Button(
        "답변 비교",
        variant="primary"
    )

    with gr.Row():
        base_output = gr.Textbox(
            label="초기 모델",
            lines=14
        )

        dpo_output = gr.Textbox(
            label="DPO 모델",
            lines=14
        )

    compare_button.click(
        fn=compare_answers,
        inputs=question_input,
        outputs=[
            base_output,
            dpo_output
        ]
    )


demo.queue()
demo.launch()

54. 실전 프로젝트 전체 코드

다음 내용을 `train_dpo.py`로 저장합니다.

````pythonfrom pathlib import Pathimport jsonimport time

import torch

from datasets import Dataset

from peft import (AutoPeftModelForCausalLM,LoraConfig,TaskType)

from transformers import (AutoTokenizer,set_seed)

from trl import (DPOConfig,DPOTrainer)

MODEL_ID = ("Qwen/""Qwen2.5-0.5B-Instruct")

OUTPUT_DIR = Path("outputs/""python-tutor-dpo")

ADAPTER_DIR = Path("models/""python-tutor-dpo-lora")

SEED = 2026

SYSTEM_MESSAGE = ("당신은 Python 입문자를 위한 친절하고 정확한 교관입니다. ""먼저 핵심 개념을 설명하고 필요한 경우 짧은 코드 예제를 제공합니다. ""확실하지 않은 내용은 추측하지 않습니다.")

PREFERENCE_EXAMPLES = [{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("Python 변수란 무엇인가요?")}],"chosen": [{"role": "assistant","content": ("변수는 프로그램에서 값을 저장하기 위해 ""붙이는 이름입니다.\n\n""```python\n""age = 20\n""print(age)\n""```\n\n""이 코드에서 age는 숫자 20을 가리키는 변수입니다.")}],"rejected": [{"role": "assistant","content": ("변수는 Python에 있는 기능입니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("리스트와 튜플의 차이를 설명해 주세요.")}],"chosen": [{"role": "assistant","content": ("리스트와 튜플은 여러 값을 순서대로 저장합니다.\n\n""리스트는 생성 후 값을 추가하거나 수정할 수 있지만 ""튜플은 일반적으로 생성 후 값을 변경할 수 없습니다.\n\n""```python\n""numbers = [1, 2, 3]\n""point = (10, 20)\n""```\n\n""변경이 필요한 데이터에는 리스트, ""고정된 데이터에는 튜플을 고려할 수 있습니다.")}],"rejected": [{"role": "assistant","content": ("리스트는 대괄호이고 튜플은 소괄호입니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("for 반복문을 예제로 설명해 주세요.")}],"chosen": [{"role": "assistant","content": ("for 반복문은 여러 데이터를 하나씩 꺼내 ""같은 작업을 반복할 때 사용합니다.\n\n""```python\n""fruits = ['사과', '바나나', '포도']\n\n""for fruit in fruits:\n"" print(fruit)\n""```\n\n""이 코드는 리스트의 값을 차례대로 출력합니다.")}],"rejected": [{"role": "assistant","content": ("for는 반복할 때 씁니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("while 반복문을 사용할 때 ""주의할 점은 무엇인가요?")}],"chosen": [{"role": "assistant","content": ("while 반복문에서는 조건이 언젠가 거짓이 되도록 ""반복 중에 관련 값을 변경해야 합니다.\n\n""```python\n""count = 0\n\n""while count < 3:\n"" print(count)\n"" count += 1\n""```\n\n""count 값을 변경하지 않으면 조건이 계속 참이어서 ""무한 반복이 발생할 수 있습니다.")}],"rejected": [{"role": "assistant","content": ("while문은 계속 실행되므로 ""종료할 방법이 없습니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("함수의 return은 어떤 역할을 하나요?")}],"chosen": [{"role": "assistant","content": ("return은 함수가 만든 결과를 호출한 곳으로 전달하고 ""함수 실행을 종료합니다.\n\n""```python\n""def add(a, b):\n"" return a + b\n\n""result = add(3, 5)\n""print(result)\n""```\n\n""이 예제에서 add 함수는 숫자 8을 반환합니다.")}],"rejected": [{"role": "assistant","content": ("return은 값을 화면에 출력하는 명령입니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("딕셔너리는 어떤 자료형인가요?")}],"chosen": [{"role": "assistant","content": ("딕셔너리는 Key와 Value를 한 쌍으로 저장하는 자료형입니다.\n\n""```python\n""user = {\n"" 'name': '홍길동',\n"" 'age': 20\n""}\n\n""print(user['name'])\n""```\n\n""의미 있는 Key를 이용해 값에 접근할 수 있습니다.")}],"rejected": [{"role": "assistant","content": ("딕셔너리는 단어를 번역하는 기능입니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("try-except는 언제 사용하나요?")}],"chosen": [{"role": "assistant","content": ("try-except는 프로그램 실행 중 발생할 수 있는 ""예외를 처리할 때 사용합니다.\n\n""```python\n""try:\n"" number = int(input('숫자: '))\n""except ValueError:\n"" print('숫자를 입력해 주세요.')\n""```\n\n""예상 가능한 예외를 구체적으로 처리하면 ""프로그램이 갑자기 종료되는 상황을 줄일 수 있습니다.")}],"rejected": [{"role": "assistant","content": ("모든 코드를 try-except로 감싸면 ""오류가 완전히 사라집니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("None을 비교할 때 어떻게 작성해야 하나요?")}],"chosen": [{"role": "assistant","content": ("None을 비교할 때는 일반적으로 is 또는 is not을 사용합니다.\n\n""```python\n""result = None\n\n""if result is None:\n"" print('결과가 없습니다.')\n""```\n\n""None은 하나의 특별한 객체이므로 ""동일성 비교인 is가 적합합니다.")}],"rejected": [{"role": "assistant","content": ("None은 숫자 0이므로 result == 0으로 비교합니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("클래스와 객체의 관계를 쉽게 설명해 주세요.")}],"chosen": [{"role": "assistant","content": ("클래스는 객체를 만들기 위한 설계도이고 ""객체는 그 설계도로 만든 실제 결과물입니다.\n\n""```python\n""class Dog:\n"" def __init__(self, name):\n"" self.name = name\n\n""dog = Dog('초코')\n""```\n\n""Dog는 클래스이고 dog는 Dog 클래스로 만든 객체입니다.")}],"rejected": [{"role": "assistant","content": ("클래스와 객체는 이름만 다르고 완전히 같습니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("가상환경은 왜 필요한가요?")}],"chosen": [{"role": "assistant","content": ("가상환경은 프로젝트마다 Python 패키지와 버전을 ""독립적으로 관리하기 위해 사용합니다.\n\n""```bash\n""python -m venv venv\n""```\n\n""프로젝트마다 서로 다른 라이브러리 버전을 사용해도 ""충돌하지 않도록 도와줍니다.")}],"rejected": [{"role": "assistant","content": ("가상환경은 Python 실행 속도를 ""무조건 빠르게 만드는 기능입니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("문자열을 숫자로 바꾸는 방법을 알려 주세요.")}],"chosen": [{"role": "assistant","content": ("정수로 변환하려면 int(), ""실수로 변환하려면 float()를 사용합니다.\n\n""```python\n""age = int('20')\n""height = float('175.5')\n""```\n\n""숫자로 변환할 수 없는 문자열을 전달하면 ""ValueError가 발생할 수 있습니다.")}],"rejected": [{"role": "assistant","content": ("문자열은 숫자로 바꿀 수 없습니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("리스트에서 중복 값을 제거하고 싶습니다.")}],"chosen": [{"role": "assistant","content": ("순서를 유지할 필요가 없다면 set으로 변환할 수 있습니다.\n\n""```python\n""numbers = [1, 2, 2, 3]\n""unique_numbers = list(set(numbers))\n""```\n\n""원래 순서를 유지하려면 다음처럼 작성할 수 있습니다.\n\n""```python\n""unique_numbers = list(dict.fromkeys(numbers))\n""```")}],"rejected": [{"role": "assistant","content": ("리스트의 중복 값은 자동으로 제거됩니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("파일을 안전하게 열고 닫는 방법을 알려 주세요.")}],"chosen": [{"role": "assistant","content": ("with 문을 사용하면 작업이 끝난 뒤 파일을 자동으로 닫을 수 있습니다.\n\n""```python\n""with open('data.txt', 'r', encoding='utf-8') as file:\n"" content = file.read()\n""```\n\n""중간에 예외가 발생해도 파일 정리가 수행된다는 장점이 있습니다.")}],"rejected": [{"role": "assistant","content": ("파일은 프로그램이 끝날 때까지 ""닫지 않아도 항상 안전합니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("Boolean 자료형을 설명해 주세요.")}],"chosen": [{"role": "assistant","content": ("Boolean은 참과 거짓을 나타내는 자료형으로 ""값은 True와 False입니다.\n\n""```python\n""is_logged_in = True\n\n""if is_logged_in:\n"" print('로그인 상태입니다.')\n""```\n\n""조건문에서 실행 방향을 결정할 때 자주 사용합니다.")}],"rejected": [{"role": "assistant","content": ("Boolean은 문자열만 저장하는 자료형입니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("함수 매개변수와 인자의 차이는 무엇인가요?")}],"chosen": [{"role": "assistant","content": ("매개변수는 함수를 정의할 때 사용하는 이름이고, ""인자는 함수를 호출할 때 전달하는 실제 값입니다.\n\n""```python\n""def greet(name):\n"" print(f'{name}님, 안녕하세요.')\n\n""greet('홍길동')\n""```\n\n""여기서 name은 매개변수이고 ""'홍길동'은 인자입니다.")}],"rejected": [{"role": "assistant","content": ("매개변수와 인자는 항상 완전히 같은 뜻입니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("Python 코딩 스타일에서 ""들여쓰기는 왜 중요한가요?")}],"chosen": [{"role": "assistant","content": ("Python은 들여쓰기로 코드 블록의 범위를 구분합니다.\n\n""```python\n""if True:\n"" print('조건이 참입니다.')\n""```\n\n""들여쓰기가 잘못되면 IndentationError가 발생하거나 ""의도와 다른 블록으로 실행될 수 있습니다.")}],"rejected": [{"role": "assistant","content": ("Python의 들여쓰기는 보기 좋게 만드는 장식이라 ""없어도 실행에는 문제가 없습니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("모듈을 사용하는 이유는 무엇인가요?")}],"chosen": [{"role": "assistant","content": ("모듈은 관련 함수와 클래스, 변수를 ""하나의 Python 파일에 정리한 것입니다.\n\n""```python\n""import math\n""print(math.sqrt(16))\n""```\n\n""코드를 기능별로 분리하고 다른 프로그램에서 ""재사용하기 쉬워진다는 장점이 있습니다.")}],"rejected": [{"role": "assistant","content": ("모듈은 컴퓨터의 메모리를 의미합니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("set 자료형의 특징을 알려 주세요.")}],"chosen": [{"role": "assistant","content": ("set은 중복되지 않는 값들을 저장하는 집합 자료형입니다.\n\n""```python\n""numbers = {1, 2, 2, 3}\n""print(numbers)\n""```\n\n""중복 제거와 합집합, 교집합 같은 집합 연산에 유용합니다. ""일반적인 set은 인덱스로 값에 접근하지 않습니다.")}],"rejected": [{"role": "assistant","content": ("set은 입력한 순서를 보장하며 ""인덱스로 값을 꺼내는 리스트입니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("Python에서 주석은 어떻게 작성하나요?")}],"chosen": [{"role": "assistant","content": ("한 줄 주석은 # 기호로 작성합니다.\n\n""```python\n""# 사용자 나이를 저장합니다.\n""age = 20\n""```\n\n""주석은 코드의 의도나 이유를 설명할 때 사용하며, ""코드 내용을 그대로 반복하는 주석은 줄이는 편이 좋습니다.")}],"rejected": [{"role": "assistant","content": ("Python에는 주석 기능이 없습니다.")}]},{"prompt": [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": ("enumerate 함수는 언제 사용하나요?")}],"chosen": [{"role": "assistant","content": ("enumerate는 반복하면서 값과 인덱스를 ""함께 사용하고 싶을 때 유용합니다.\n\n""```python\n""fruits = ['사과', '바나나']\n\n""for index, fruit in enumerate(fruits):\n"" print(index, fruit)\n""```\n\n""인덱스를 직접 증가시키는 별도 변수를 줄일 수 있습니다.")}],"rejected": [{"role": "assistant","content": ("enumerate는 리스트를 삭제하는 함수입니다.")}]}]

def validate_preference_example(example: dict,index: int) -> None:required_columns = {"prompt","chosen","rejected"}

missing_columns = (required_columns- set(example.keys()))

if missing_columns:raise ValueError(f"{index}번 데이터에 열이 없습니다: "f"{sorted(missing_columns)}")

for column_name in ("prompt","chosen","rejected"):messages = example[column_name]

if not isinstance(messages, list):raise TypeError(f"{index}번 {column_name}은 ""메시지 리스트여야 합니다.")

if not messages:raise ValueError(f"{index}번 {column_name}이 비어 있습니다.")

for message in messages:role = message.get("role")content = str(message.get("content","")).strip()

if role not in {"system","user","assistant"}:raise ValueError(f"{index}번 데이터에 "f"잘못된 역할이 있습니다: {role}")

if not content:raise ValueError(f"{index}번 데이터에 ""빈 메시지가 있습니다.")

if example["chosen"][-1]["role"] != "assistant":raise ValueError(f"{index}번 Chosen의 마지막 역할은 ""assistant여야 합니다.")

if example["rejected"][-1]["role"] != "assistant":raise ValueError(f"{index}번 Rejected의 마지막 역할은 ""assistant여야 합니다.")

chosen_text = (example["chosen"][-1]["content"].strip())

rejected_text = (example["rejected"][-1]["content"].strip())

if chosen_text == rejected_text:raise ValueError(f"{index}번 Chosen과 Rejected가 같습니다.")

def select_precision() -> tuple[bool,bool,torch.dtype]:if not torch.cuda.is_available():return (False,False,torch.float32)

if torch.cuda.is_bf16_supported():return (True,False,torch.bfloat16)

return (False,True,torch.float16)

def select_device() -> torch.device:if torch.cuda.is_available():return torch.device("cuda")

if torch.backends.mps.is_available():return torch.device("mps")

return torch.device("cpu")

def generate_answer(model,tokenizer,device: torch.device,question: str) -> str:messages = [{"role": "system","content": SYSTEM_MESSAGE},{"role": "user","content": question}]

input_ids = (tokenizer.apply_chat_template(messages,tokenize=True,add_generation_prompt=True,return_tensors="pt").to(device))

with torch.inference_mode():output_ids = model.generate(input_ids=input_ids,

max_new_tokens=256,

do_sample=False,

eos_token_id=(tokenizer.eos_token_id),

pad_token_id=(tokenizer.pad_token_id))

generated_ids = output_ids[0,input_ids.shape[-1]:]

return tokenizer.decode(generated_ids,skip_special_tokens=True).strip()

def main() -> None:set_seed(SEED)

OUTPUT_DIR.mkdir(parents=True,exist_ok=True)

ADAPTER_DIR.mkdir(parents=True,exist_ok=True)

print("[1/10] Preference 데이터 검증")

for index, example in enumerate(PREFERENCE_EXAMPLES):validate_preference_example(example,index)

print(f"검증된 Preference Pair: "f"{len(PREFERENCE_EXAMPLES)}개")

print("[2/10] Dataset 생성")

dataset = Dataset.from_list(PREFERENCE_EXAMPLES)

split_dataset = (dataset.train_test_split(test_size=0.2,seed=SEED,shuffle=True))

train_dataset = (split_dataset["train"])

validation_dataset = (split_dataset["test"])

print(f"Train: {len(train_dataset)}개")

print(f"Validation: "f"{len(validation_dataset)}개")

print("[3/10] Tokenizer 로딩")

tokenizer = (AutoTokenizer.from_pretrained(MODEL_ID))

if tokenizer.pad_token is None:tokenizer.pad_token = (tokenizer.eos_token)

tokenizer.padding_side = "left"

print("[4/10] Chat Template 확인")

formatted_prompt = (tokenizer.apply_chat_template(PREFERENCE_EXAMPLES[0]["prompt"],tokenize=False,add_generation_prompt=True))

print(formatted_prompt)

print("[5/10] LoRA 설정")

lora_config = LoraConfig(task_type=TaskType.CAUSAL_LM,inference_mode=False,

r=16,lora_alpha=32,lora_dropout=0.05,

target_modules=["q_proj","k_proj","v_proj","o_proj"],

bias="none")

(use_bf16,use_fp16,model_dtype) = select_precision()

print(f"CUDA: "f"{torch.cuda.is_available()}")

print(f"BF16: {use_bf16}")

print(f"FP16: {use_fp16}")

print("[6/10] DPOConfig 생성")

training_args = DPOConfig(output_dir=str(OUTPUT_DIR),

model_init_kwargs={"dtype": model_dtype},

learning_rate=1e-5,lr_scheduler_type="linear",warmup_ratio=0.1,

per_device_train_batch_size=1,per_device_eval_batch_size=1,

gradient_accumulation_steps=8,

num_train_epochs=3,

beta=0.1,

loss_type=["sigmoid"],

max_length=512,truncation_mode="keep_start",

eval_strategy="epoch",save_strategy="epoch",

logging_strategy="steps",logging_steps=5,logging_first_step=True,

load_best_model_at_end=True,metric_for_best_model=("eval_loss"),greater_is_better=False,

save_total_limit=2,

gradient_checkpointing=True,

bf16=use_bf16,fp16=use_fp16,

report_to="none",

seed=SEED,data_seed=SEED,

run_name=("python-tutor-dpo-lora"))

print("[7/10] DPOTrainer 생성")

trainer = DPOTrainer(model=MODEL_ID,

ref_model=None,

args=training_args,

train_dataset=train_dataset,eval_dataset=(validation_dataset),

processing_class=tokenizer,

peft_config=lora_config)

print("\n[학습 가능한 파라미터]")

trainer.model.print_trainable_parameters()

print("[8/10] DPO 학습 시작")

start_time = (time.perf_counter())

train_result = trainer.train()

elapsed_seconds = (time.perf_counter()- start_time)

train_result.metrics["elapsed_seconds"] = elapsed_seconds

trainer.log_metrics("train",train_result.metrics)

trainer.save_metrics("train",train_result.metrics)

trainer.save_state()

print("[9/10] Validation 평가")

validation_metrics = (trainer.evaluate())

trainer.log_metrics("validation",validation_metrics)

trainer.save_metrics("validation",validation_metrics)

print("[10/10] Adapter 저장과 추론")

trainer.save_model(str(ADAPTER_DIR))

tokenizer.save_pretrained(ADAPTER_DIR)

training_summary = {"model_id": MODEL_ID,"preference_pairs": len(PREFERENCE_EXAMPLES),"train_pairs": len(train_dataset),"validation_pairs": len(validation_dataset),"beta": 0.1,"loss_type": "sigmoid","elapsed_seconds": (elapsed_seconds),"best_checkpoint": (trainer.state.best_model_checkpoint),"best_metric": (trainer.state.best_metric)}

(OUTPUT_DIR/ "training_summary.json").write_text(json.dumps(training_summary,ensure_ascii=False,indent=2),encoding="utf-8")

print(f"Adapter 저장 위치: "f"{ADAPTER_DIR.resolve()}")

del trainer

if torch.cuda.is_available():torch.cuda.empty_cache()

dpo_model = (AutoPeftModelForCausalLM.from_pretrained(ADAPTER_DIR))

device = select_device()

dpo_model = dpo_model.to(device)

dpo_model.eval()

evaluation_questions = [("Python 함수의 return은 ""무슨 역할을 하나요?"),("리스트에서 중복 값을 ""제거하는 방법을 알려 주세요."),("파일을 안전하게 열고 닫으려면 ""어떤 문법을 사용해야 하나요?")]

print("\n[DPO 모델 생성 결과]")

for question in (evaluation_questions):answer = generate_answer(dpo_model,tokenizer,device,question)

print(f"\n질문:\n{question}")

print(f"\n답변:\n{answer}")

print("\n"+ "=" * 70)

if __name__ == "__main__":main()````

55. 프로젝트 실행 방법

1단계: 프로젝트 폴더

mkdir trl_dpo_project
cd trl_dpo_project

2단계: 가상환경

Windows

python -m venv venv
venv\Scripts\activate

macOS·Linux

python3 -m venv venv
source venv/bin/activate

3단계: 패키지 설치

python -m pip install "transformers[torch]" datasets accelerate peft trl

4단계: 코드 저장

train_dpo.py

5단계: 실행

python train_dpo.py

이 예제 데이터는 DPO 파이프라인을 확인하기 위한 소규모 학습용 데이터입니다.

Preference Pair 20개

→ 코드 동작 검증


실제 업무 모델

→ 충분한 Pair 수
→ 전문가 검수
→ 독립 Test Set
→ 사람 선호도 평가

56. 결과 파일 구조

trl_dpo_project/
├─ train_dpo.py
│
├─ outputs/
│  └─ python-tutor-dpo/
│     ├─ checkpoint-...
│     ├─ train_results.json
│     ├─ validation_results.json
│     ├─ trainer_state.json
│     └─ training_summary.json
│
└─ models/
   └─ python-tutor-dpo-lora/
      ├─ adapter_config.json
      ├─ adapter_model.safetensors
      ├─ tokenizer.json
      ├─ tokenizer_config.json
      └─ special_tokens_map.json

57. 데이터 보안과 편향

Preference Dataset은 모델의 행동 기준을 결정합니다.

누가 Chosen을 선택했는가?

어떤 기준으로 선택했는가?

평가자는 같은 기준을 사용했는가?

특정 집단의 표현 방식만 선호하지 않았는가?

긴 답변을 무조건 선호하지 않았는가?

특정 정치적·문화적 관점이 과도하게 반영되지 않았는가?

평가자마다 좋은 답변의 기준이 다를 수 있습니다.

평가자 A:

간결한 답변 선호


평가자 B:

자세한 답변 선호


평가자 C:

코드 중심 답변 선호

평가 지침을 문서화하지 않으면 Preference Label이 일관되지 않을 수 있습니다.

평가 지침 예

1. 사실 정확성을 가장 우선한다.

2. 질문에 직접 답해야 한다.

3. 요청하지 않은 내용은 최소화한다.

4. 코드는 실행 가능해야 한다.

5. 초보자가 모르는 용어는 설명한다.

6. Chosen과 Rejected의 길이만으로 판단하지 않는다.

민감한 상담 기록이나 내부 문서를 Preference Dataset으로 사용할 때는 개인정보와 기밀정보를 제거하고 접근 권한을 제한해야 합니다.

58. 자주 발생하는 오류

오류 1. trl을 찾을 수 없음

ModuleNotFoundError:
No module named 'trl'

설치:

python -m pip install --upgrade trl

오류 2. Python 버전이 낮음

현재 TRL 1.9.2는 Python 3.10 이상을 요구합니다.

확인:

python --version

오류 3. Preference 열이 없음

prompt

chosen

rejected

세 열을 확인합니다.

print(
    train_dataset.column_names
)

오류 4. Chosen과 Rejected가 문자열이 아님

Standard Format에서는 문자열을 사용합니다.

{
    "prompt": "질문",
    "chosen": "좋은 답변",
    "rejected": "나쁜 답변"
}

Conversational Format에서는 메시지 리스트를 사용합니다.

{
    "prompt": [
        {
            "role": "user",
            "content": "질문"
        }
    ],
    "chosen": [
        {
            "role": "assistant",
            "content": "좋은 답변"
        }
    ],
    "rejected": [
        {
            "role": "assistant",
            "content": "나쁜 답변"
        }
    ]
}

두 형식을 섞지 않습니다.

오류 5. Chat Template이 없음

tokenizer.chat_template is not set

Chat Template이 있는 Instruct Model을 사용하거나 Tokenizer에 적절한 Template을 설정해야 합니다.

오류 6. Padding Token이 없음

Tokenizer does not have a padding token
if tokenizer.pad_token is None:
    tokenizer.pad_token = (
        tokenizer.eos_token
    )

tokenizer.padding_side = "left"

오류 7. Target Module을 찾지 못함

Target modules not found

모델 구조 확인:

import torch

from transformers import (
    AutoModelForCausalLM
)

model = (
    AutoModelForCausalLM
    .from_pretrained(
        MODEL_ID
    )
)

for name, module in (
    model.named_modules()
):
    if isinstance(
        module,
        torch.nn.Linear
    ):
        print(name)

Qwen 계열 예:

target_modules=[
    "q_proj",
    "k_proj",
    "v_proj",
    "o_proj"
]

오류 8. 학습 가능한 파라미터가 0개

trainer.model.print_trainable_parameters()

확인:

peft_config 전달 여부

Target Module 이름

inference_mode=False

Adapter 활성화 여부

오류 9. CUDA 메모리 부족

CUDA out of memory

대응:

Batch Size 감소

Gradient Accumulation 증가

max_length 감소

LoRA Rank 감소

Target Module 감소

QLoRA 사용

Reference Log Probability 사전 계산

오류 10. Policy와 Reference 때문에 메모리가 부족함

다음을 검토합니다.

precompute_ref_log_probs=True

또는:

LoRA·QLoRA 사용

작은 모델 사용

입력 길이 감소

CPU Offloading 검토

오류 11. Loss가 NaN

확인:

Learning Rate가 너무 높은가?

FP16이 불안정한가?

Chosen이나 Rejected가 비어 있는가?

모든 Completion Token이 잘렸는가?

Gradient Norm이 너무 큰가?

Learning Rate 감소:

learning_rate=5e-6

오류 12. Reward Accuracy가 0.5 근처에서 변하지 않음

가능한 원인:

Chosen과 Rejected 차이가 불명확

Preference Label 오류

Learning Rate 부족

Epoch 부족

LoRA Target 부족

데이터가 너무 적음

오류 13. Reward Accuracy는 높지만 생성 품질이 나쁨

가능한 원인:

Train Pair 과적합

Rejected가 지나치게 쉬움

실제 평가 Prompt와 학습 분포가 다름

Chosen 자체의 품질이 낮음

답변 길이 편향

사람 평가와 독립 Test Set이 필요합니다.

오류 14. Beta 변경 효과가 이상함

Beta 외에도 다음 설정이 영향을 줍니다.

Learning Rate

Loss Type

Batch Size

데이터 난이도

Reference Model

Epoch

LoRA Rank

한 번에 여러 설정을 바꾸지 않고 실험을 분리합니다.

오류 15. Rejected 답변을 더 자주 생성함

확인:

Chosen과 Rejected가 뒤바뀌지 않았는가?

데이터 열 Mapping이 맞는가?

Adapter가 정상 로드되었는가?

학습 Checkpoint가 맞는가?

Chat Template이 동일한가?

오류 16. 학습 후 답변이 지나치게 길어짐

Preference Dataset에서 긴 답변이 항상 Chosen이었을 수 있습니다.

대응:

짧고 정확한 Chosen 추가

길고 관련 없는 Rejected 추가

답변 길이 분포 균형

sigmoid_norm 실험

생성 max_new_tokens 제한

오류 17. 학습 후 답변이 지나치게 짧아짐

짧은 답변만 Chosen

긴 답변은 모두 Rejected

이런 데이터 패턴이 없는지 확인합니다.

오류 18. Checkpoint 재개 실패

Checkpoint 폴더에 다음 상태가 있는지 확인합니다.

optimizer.pt

scheduler.pt

trainer_state.json

Adapter 가중치

최종 Adapter 폴더는 학습 재개용 Checkpoint와 다릅니다.

오류 19. QLoRA가 CPU나 MPS에서 작동하지 않음

`bitsandbytes`의 4비트 학습 지원은 운영체제와 Hardware Backend에 따라 달라집니다.

CUDA GPU가 아닌 환경에서는 일반 LoRA부터 검토합니다.

오류 20. eval_loss가 감소하지만 Preference 승률이 오르지 않음

Loss는 학습 목적함수를 나타냅니다.

실제 생성 결과의 품질은 다음 방법으로 별도 평가합니다.

Blind Pairwise Evaluation

사람 평가

LLM Judge

형식 검증

코드 실행 테스트

사실 정확성 테스트

59. 연습 문제

문제 1

SFT와 DPO의 차이를 세 문장으로 설명하세요.

문제 2

다음 응답을 Preference Dataset으로 구성하세요.

질문:

Python 리스트란 무엇인가요?


Chosen:

여러 값을 순서대로 저장하는
변경 가능한 자료형입니다.


Rejected:

숫자만 저장하는 고정 자료형입니다.

문제 3

같은 데이터를 Conversational Preference Format으로 변환하세요.

문제 4

Preference Dataset에서 다음 오류를 검사하는 함수를 작성하세요.

Prompt 누락

Chosen 누락

Rejected 누락

빈 답변

Chosen과 Rejected 동일

문제 5

다음 Beta를 각각 실험하세요.

0.05

0.1

0.2

문제 6

각 실험에서 다음 지표를 비교하세요.

Validation Loss

Reward Accuracy

Reward Margin

사람 평가 승률

문제 7

다음 Loss Type을 비교하세요.

sigmoid

hinge

ipo

sigmoid_norm

문제 8

LoRA Rank를 다음과 같이 변경하세요.

8

16

32

문제 9

Chosen과 Rejected의 평균 Token 길이를 출력하세요.

문제 10

Chosen이 항상 긴 데이터만 따로 찾아 출력하세요.

문제 11

Reference Log Probability를 학습 전에 계산하도록 설정하세요.

precompute_ref_log_probs=True

문제 12

QLoRA를 적용해 4비트 DPO 학습을 구성하세요.

문제 13

최근 Checkpoint에서 DPO 학습을 재개하세요.

문제 14

학습된 Adapter를 저장하고 다시 불러오세요.

문제 15

DPO Adapter를 기본 모델에 병합하세요.

문제 16

초기 모델과 DPO 모델의 답변을 동일 Prompt로 비교하세요.

문제 17

사람 평가용 CSV 파일을 만드세요.

question

base_answer

dpo_answer

preferred

reason

문제 18

DPO 모델의 사람 평가 승률을 계산하세요.

문제 19

Gradio로 두 답변을 나란히 표시하는 비교 화면을 만드세요.

문제 20

실전 프로젝트에 다음 기능을 추가하세요.

JSONL Preference Dataset 로딩

중복 Prompt 제거

Chosen·Rejected 길이 분석

Beta 자동 비교 실험

Reward Margin 그래프

학습 전후 답변 저장

Blind Evaluation 화면

Hub 비공개 업로드

60. 핵심 요약

Preference Dataset

example = {
    "prompt": "질문",
    "chosen": "더 좋은 답변",
    "rejected": "덜 좋은 답변"
}

Conversational Preference Dataset

example = {
    "prompt": [
        {
            "role": "user",
            "content": "질문"
        }
    ],

    "chosen": [
        {
            "role": "assistant",
            "content": "더 좋은 답변"
        }
    ],

    "rejected": [
        {
            "role": "assistant",
            "content": "덜 좋은 답변"
        }
    ]
}

LoRA 설정

lora_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,

    r=16,
    lora_alpha=32,
    lora_dropout=0.05,

    target_modules=[
        "q_proj",
        "k_proj",
        "v_proj",
        "o_proj"
    ]
)

DPOConfig

training_args = DPOConfig(
    output_dir="outputs/dpo",

    learning_rate=1e-5,

    per_device_train_batch_size=1,
    gradient_accumulation_steps=8,

    num_train_epochs=3,

    beta=0.1,
    loss_type=["sigmoid"],

    max_length=512,

    eval_strategy="epoch",
    save_strategy="epoch"
)

DPOTrainer

trainer = DPOTrainer(
    model=MODEL_ID,

    ref_model=None,

    args=training_args,

    train_dataset=train_dataset,
    eval_dataset=validation_dataset,

    processing_class=tokenizer,

    peft_config=lora_config
)

학습

trainer.train()

평가

metrics = trainer.evaluate()

Adapter 저장

trainer.save_model(
    "saved_dpo_adapter"
)

Adapter 불러오기

model = (
    AutoPeftModelForCausalLM
    .from_pretrained(
        "saved_dpo_adapter"
    )
)

QLoRA

trainer = DPOTrainer(
    model=MODEL_ID,

    train_dataset=train_dataset,

    peft_config=lora_config,

    quantization_config=(
        quantization_config
    )
)

61. 마무리

이번 시간에는 Hugging Face TRL의 DPOTrainer를 이용해 좋은 답변과 나쁜 답변을 비교하며 모델의 선호도를 학습하는 방법을 알아보았습니다.

전체 과정을 다시 정리해 보겠습니다.

SFT 또는 Instruct Model 준비

→ Preference Dataset 생성

→ Prompt·Chosen·Rejected 구성

→ Chat Template 적용

→ Policy와 Reference 확률 계산

→ Chosen·Rejected Reward 비교

→ DPO Loss 계산

→ LoRA Adapter 업데이트

→ Reward Accuracy·Margin 확인

→ Adapter 저장

→ 생성 결과 사람 평가

SFT가 모델에게 모범 답안을 보여주는 수업이었다면 DPO는 두 개의 답안을 놓고 더 나은 쪽을 선택하도록 훈련하는 수업입니다.

SFT:

“이것이 정답입니다.”


DPO:

“A와 B 중 A가 더 좋습니다.”

DPO의 핵심 요소는 세 가지입니다.

Prompt

Chosen

Rejected

그리고 학습 중에는 두 모델의 관점이 함께 사용됩니다.

Policy Model:

새로운 선호도를 학습


Reference Model:

학습 전 행동을 기준으로 제공

Beta는 Policy가 Reference에서 얼마나 멀리 이동할지를 조절합니다.

작은 Beta:

더 적극적인 변화


큰 Beta:

더 보수적인 변화

하지만 DPOTrainer가 좋은 답변의 기준을 스스로 발명하지는 않습니다.

Chosen에 무엇을 넣었는가?

Rejected에 무엇을 넣었는가?

누가 어떤 기준으로 비교했는가?

이 세 질문이 모델의 행동 방향을 결정합니다.

평가자:

“긴 답변이 항상 더 친절합니다.”


Dataset:

긴 답변을 모두 Chosen으로 지정


모델:

질문이 무엇이든 장편 답변 생성

모델은 Preference Label 뒤에 숨은 의도를 읽지 못합니다.

반복되는 패턴을 배웁니다.

따라서 좋은 Preference Dataset에는 단순한 정답과 오답만 필요한 것이 아닙니다.

정확성과 오류

충분함과 부족함

간결함과 장황함

직접 답변과 질문 회피

형식 준수와 형식 위반

안전한 답변과 위험한 답변

다양한 품질 차이가 포함되어야 합니다.

학습 로그에서 Reward Accuracy와 Reward Margin이 좋아졌더라도 마지막 검증은 실제 생성 결과로 해야 합니다.

Training Loss

→ 학습 목적함수


Reward Accuracy

→ Preference Pair 구분 능력


사람 평가

→ 실제 답변 품질

세 가지는 서로 연결되어 있지만 같은 숫자는 아닙니다.

AI 정렬에서 가장 중요한 장비는 GPU만이 아닙니다.

일관된 평가 기준

검증된 Preference Pair

독립적인 Test Prompt

사람의 꼼꼼한 비교

결국 DPO는 모델에게 좋은 답변의 정의를 주입하는 기술이 아니라, 사람이 데이터로 표현한 선호도를 모델이 따라가게 만드는 기술입니다.

AI:

“어떤 답변을 좋아해야 하나요?”


Preference Dataset:

“이 답변을 더 좋아하세요.”


DPOTrainer:

“두 답변의 차이를 학습하겠습니다.”

AI 심사위원의 채점표가 완성되었습니다. ⚖️🤖

다음 편 예고

[Python 완전정복 시리즈 #41] RewardTrainer 완벽 이해하기 | AI 답변의 품질을 점수로 평가하는 Reward Model 만드는 방법

다음 시간에는 Preference Dataset을 이용해 Prompt와 답변의 품질을 숫자로 평가하는 Reward Model을 학습합니다.

RewardTrainer, Sequence Classification Model, Chosen·Rejected 점수, Reward Margin, Pairwise Accuracy, LoRA, Checkpoint, 모델 저장, 여러 답변 순위 평가와 Gradio 채점 화면까지 실습합니다.

#Python #파이썬 #Python강좌 #HuggingFace #TRL #DPO #DPOTrainer #DirectPreferenceOptimization #PreferenceLearning #선호도학습 #Chosen #Rejected #ReferenceModel #PolicyModel #RewardMargin #RewardAccuracy #LoRA #QLoRA #PEFT #Transformers #생성형AI #LLM #파인튜닝 #AI정렬 #RLHF #ChatTemplate #PreferenceDataset #Gradio #PyTorch #코딩공부 #프로그래밍

댓글

0

댓글을 불러오는 중입니다.