목차
메타 설명
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에서 무조건 실행되는 코드입니다."
)
}비교하면 다음과 같습니다.
| 구분 | SFT | DPO |
|---|---|---|
| 학습 데이터 | Prompt와 정답 | Prompt와 Chosen·Rejected |
| 핵심 목표 | 모범 답안 모방 | 상대적 선호 학습 |
| 답변 비교 | 없음 | 있음 |
| 일반적인 순서 | 먼저 수행 | SFT 후 수행 |
| 주요 활용 | 지시 수행 학습 | 답변 품질·스타일 정렬 |
| Trainer | SFTTrainer | DPOTrainer |
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 ModelDPO
Preference Dataset
→ Policy Model 직접 최적화DPO 원 논문은 Reward Model과 강화학습 루프를 별도로 운영하는 복잡성을 줄이면서 선호도를 직접 최적화하는 방식을 제안했습니다.
DPO가 강화학습에서 다루던 정렬 문제와 관련되어 있다고 해서 일반적인 환경 탐색형 강화학습처럼 행동하는 것은 아닙니다.
환경에서 행동
→ 보상 획득
→ 다음 상태로 이동대신 이미 수집된 Offline Preference Pair를 이용합니다.
Prompt
Chosen
Rejected5. 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 학습이 시작되기 전 기준이 되는 모델입니다.
수식에서는 다음과 같이 표현합니다.
πrefReference 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.5Beta가 너무 작음:
“새 규칙이 최고입니다!
기존 지식은 잠시 잊겠습니다.”
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 trlPython에서 확인:
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 trlmacOS·Linux
python3 -m venv venv
source venv/bin/activate
python -m pip install --upgrade pip
python -m pip install "transformers[torch]" datasets accelerate peft trlQLoRA까지 사용할 경우
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
rejectedTRL은 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_tokenDPOTrainer의 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.1Preference 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=512DPO 데이터 한 건에는 다음 내용이 포함됩니다.
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-5GPU 메모리가 부족하면 다음과 같이 구성합니다.
per_device_train_batch_size=1
gradient_accumulation_steps=8한 장치 기준 유효 Batch Size:
1 × 8
= 8DPO는 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/accuraciesChosen의 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/marginsChosen Reward와 Rejected Reward의 차이입니다.
Reward Margin
=
Chosen Reward
-
Rejected Reward예:
Chosen Reward:
1.2
Rejected Reward:
0.3
Margin:
0.9Margin이 양수이면 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=FalseReward 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=TrueBatch 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 loginDPOConfig:
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_project2단계: 가상환경
Windows
python -m venv venv
venv\Scripts\activatemacOS·Linux
python3 -m venv venv
source venv/bin/activate3단계: 패키지 설치
python -m pip install "transformers[torch]" datasets accelerate peft trl4단계: 코드 저장
train_dpo.py5단계: 실행
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.json57. 데이터 보안과 편향
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 setChat Template이 있는 Instruct Model을 사용하거나 Tokenizer에 적절한 Template을 설정해야 합니다.
오류 6. Padding Token이 없음
Tokenizer does not have a padding tokenif 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 #코딩공부 #프로그래밍
