Trong product engineering, có một bài toán cổ điển: làm sao biết feature nào tốt hơn? Có hai cách:
A/B test absolute: đo metric (CTR, retention) trước và sau feature. Vấn đề: cần baseline ổn định, mất thời gian.
Preference test: cho user xem 2 version, hỏi “thích cái nào hơn”. Vấn đề: bias, nhưng câu trả lời nhanh.
LLM alignment đối mặt cùng dilemma. SFT (bài 19) là kiểu A/B absolute: cho model học format “instruction => response chuẩn”. Nhưng “response chuẩn” rất khó định nghĩa: nhiều response đúng cho cùng một câu hỏi.
Preference learning fix bằng cách nói: “với input X, response A tốt hơn response B”. Không định nghĩa absolute, chỉ relative. Đây là idea của RLHF (Christiano et al., 2017) và DPO (Rafailov et al., 2023).
Bài này giải thích:
- Bradley-Terry model, foundation của preference learning
- Pipeline RLHF cổ điển: reward model + PPO
- DPO, kỹ thuật mới bỏ reward model
- Khi nào dùng cái nào, và pitfall reward hacking
Sau bài này bạn hiểu tại sao ChatGPT/Claude/Gemini đều có pipeline alignment phức tạp, và biết DPO khả thi cho fine-tune model 7B của riêng bạn.
Mental model: preference learning là gì
Input dataset preference (gọi là preference data):
prompt: "Viết một câu chào tiếng Việt"
chosen: "Xin chào, rất vui được gặp bạn!"
rejected: "Hi there, how are you?"
Mỗi sample có 3 thành phần: prompt, response chosen (tốt hơn), response rejected (kém hơn).
Annotation cách lấy data này: cho 2 model generate 2 response, đưa human (hoặc LLM judge) xem và pick cái thích hơn. Bộ Anthropic HH-RLHF có ~170K cặp như vậy. OpenAI tạo ~30K cặp cho ChatGPT đầu tiên.
Mục tiêu training: làm cho model xác suất generate chosen cao hơn rejected. Cụ thể hơn:
P(chosen | prompt) > P(rejected | prompt)
Đây là objective khác với SFT. SFT maximize P(chosen | prompt) mà không quan tâm rejected. Preference learning maximize gap giữa chosen và rejected.
Phần 1: Bradley-Terry model
Bradley-Terry (1952) là statistical model cho competition kiểu “A vs B”. Idea:
P(A thắng B) = exp(r(A)) / (exp(r(A)) + exp(r(B)))
= σ(r(A) - r(B))
Trong đó r(.) là “reward” hoặc “skill rating” của từng item, σ là sigmoid.
Áp dụng vào LLM alignment: response có “reward” cao hơn thì xác suất được người chấm pick cao hơn.
P(y_chosen được prefer | x) = σ(r(x, y_chosen) - r(x, y_rejected))
Chỉ cần học function r(x, y). Đây là reward model, một neural network đầu vào (prompt, response), output là một scalar reward.
Train reward model bằng cross-entropy:
def reward_loss(reward_chosen, reward_rejected):
return -torch.log(torch.sigmoid(reward_chosen - reward_rejected)).mean()
Sau khi train reward model, ta có một function chấm điểm. Bước tiếp theo: dùng reward model để improve LLM.
Phần 2: RLHF pipeline cổ điển
OpenAI publish “Training language models to follow instructions with human feedback” (Ouyang 2022), paper foundation của ChatGPT. Pipeline 3 bước:
Bước 1: SFT
Pretrained model -> SFT model
Train trên (instruction, response) pairs
Bước 2: Reward model training
Reward model = SFT model + classification head
Train trên (prompt, chosen, rejected) pairs với BT loss
Bước 3: RL fine-tuning với PPO
Trainer policy = SFT model (copy)
Reference policy = SFT model (frozen)
Reward model (frozen)
Loop:
Sample response từ policy
Compute reward = reward_model(prompt, response) - β * KL(policy || reference)
Update policy bằng PPO
Step 3 là phần khó nhất. PPO (Proximal Policy Optimization) là RL algorithm. Idea:
- Policy là LLM đang train. Predict next token là “action”.
- Reward là output của reward model + penalty cho việc deviate khỏi reference model (qua KL divergence).
- PPO update policy để maximize reward, clip để không update quá xa reference.
Code đơn giản hoá:
for iteration in range(num_iterations):
# 1. Generate response từ policy
prompts = sample_prompts(batch_size)
responses = policy.generate(prompts)
# 2. Compute reward + KL penalty
raw_reward = reward_model(prompts, responses)
log_p_policy = policy.log_prob(responses, prompts)
log_p_ref = reference.log_prob(responses, prompts)
kl = log_p_policy - log_p_ref
final_reward = raw_reward - beta * kl
# 3. PPO update
advantages = compute_advantages(final_reward)
for ppo_epoch in range(4):
new_log_p = policy.log_prob(responses, prompts)
ratio = torch.exp(new_log_p - log_p_policy)
loss_clip = -torch.min(
ratio * advantages,
torch.clamp(ratio, 1 - epsilon, 1 + epsilon) * advantages,
).mean()
loss_clip.backward()
optimizer.step()
PPO có nhiều knob: beta (KL penalty strength), epsilon (clip range), số PPO epoch per iteration, batch size, etc. Tuning rất khó. Sai một thông số dễ làm training collapse.
Phần 3: Vấn đề của RLHF
RLHF cho kết quả tốt (ChatGPT), nhưng có 4 vấn đề lớn:
1. Phức tạp. Phải maintain 4 model song song:
- Policy (đang train)
- Reference (frozen, copy của SFT)
- Reward model (frozen)
- Value model (PPO critic, optional nhưng phổ biến)
Train tốn ~4x VRAM so với SFT cùng model size.
2. Unstable. PPO có thể collapse: policy đột nhiên generate gibberish, reward score cao theo reward model nhưng quality thực kém.
3. Reward hacking. Model học cách “fool” reward model thay vì actually trả lời tốt. Ví dụ: response càng dài càng cao reward (vì reward model bias) => model generate response cực dài và verbose.
4. Đắt. Cần dataset preference khoảng 30K-200K cặp, mỗi cặp 5-15 phút annotation. Tổng cost annotation $50K-500K.
Đây là lý do trong 2023-2024, community tìm alternative đơn giản hơn.
Phần 4: DPO, breakthrough của 2023
Rafailov et al. publish “Direct Preference Optimization: Your Language Model is Secretly a Reward Model” (NeurIPS 2023). Key insight:
Mathematical trick: có thể derive ra closed-form solution cho RLHF objective, mà không cần actually train reward model.
Cụ thể, RLHF objective:
max_π E[r(x, y)] - β * KL(π || π_ref)
Solving exactly cho π (policy optimal), ta được:
π*(y|x) = π_ref(y|x) * exp(r(x,y) / β) / Z(x)
Tức là policy optimal phụ thuộc vào reward và reference. Ngược lại, có thể tìm reward implicit:
r(x,y) = β * log(π*(y|x) / π_ref(y|x)) + β * log(Z(x))
Substitute vào Bradley-Terry loss của preference learning:
L_DPO = -E[ log σ( β * (log(π(y_c|x)/π_ref(y_c|x)) - log(π(y_r|x)/π_ref(y_r|x))) ) ]
Loss này chỉ phụ thuộc vào policy π đang train và reference π_ref (frozen). Không có reward model. Không có RL.
Train DPO chỉ cần:
- Pretrained / SFT model (cùng init policy và reference)
- Preference dataset
Code đơn giản:
def dpo_loss(policy_chosen_logps, policy_rejected_logps,
ref_chosen_logps, ref_rejected_logps, beta=0.1):
policy_logratio = policy_chosen_logps - policy_rejected_logps
ref_logratio = ref_chosen_logps - ref_rejected_logps
logits = beta * (policy_logratio - ref_logratio)
loss = -F.logsigmoid(logits).mean()
return loss
Training loop giống SFT:
for batch in dataloader:
# Tính log prob cho chosen + rejected, qua policy và reference
policy_chosen = policy.log_prob(batch.chosen, batch.prompt)
policy_rejected = policy.log_prob(batch.rejected, batch.prompt)
ref_chosen = reference.log_prob(batch.chosen, batch.prompt)
ref_rejected = reference.log_prob(batch.rejected, batch.prompt)
loss = dpo_loss(policy_chosen, policy_rejected, ref_chosen, ref_rejected, beta=0.1)
loss.backward()
optimizer.step()
optimizer.zero_grad()
Đơn giản hơn PPO rất nhiều. Memory chỉ ~2x SFT (policy + reference), không phải 4x của PPO.
Phần 5: Triển khai DPO bằng trl
trl có DPOTrainer ready-to-use:
from transformers import AutoModelForCausalLM, AutoTokenizer
from datasets import load_dataset
from trl import DPOTrainer, DPOConfig
from peft import LoraConfig
base = "meta-llama/Llama-3.2-1B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(base)
model = AutoModelForCausalLM.from_pretrained(base, torch_dtype="bfloat16", device_map="auto")
ref_model = AutoModelForCausalLM.from_pretrained(base, torch_dtype="bfloat16", device_map="auto")
dataset = load_dataset("trl-lib/ultrafeedback_binarized", split="train")
lora_config = LoraConfig(
r=16, lora_alpha=32, target_modules="all-linear",
lora_dropout=0.05, task_type="CAUSAL_LM",
)
config = DPOConfig(
output_dir="./dpo-llama",
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
num_train_epochs=1,
learning_rate=5e-6,
beta=0.1,
bf16=True,
logging_steps=10,
max_length=2048,
max_prompt_length=1024,
)
trainer = DPOTrainer(
model=model,
ref_model=ref_model,
args=config,
train_dataset=dataset,
tokenizer=tokenizer,
peft_config=lora_config,
)
trainer.train()
Vài điểm:
learning_rate=5e-6: thấp hơn SFT nhiều. DPO sensitive với lr, cao là collapse.
beta=0.1: KL penalty strength. Cao = policy gần reference (conservative), thấp = policy free hơn nhưng risk collapse. 0.1 là default tốt.
ref_model: phải pass riêng. Nếu dùng LoRA, có thể None (peft tự dùng base model làm reference).
Dataset format trl-lib/ultrafeedback_binarized:
{
"chosen": [
{"role": "user", "content": "..."},
{"role": "assistant", "content": "good response"}
],
"rejected": [
{"role": "user", "content": "..."},
{"role": "assistant", "content": "bad response"}
]
}
Đây là format chuẩn cho DPO trong trl.
Phần 6: So sánh DPO vs RLHF
| Aspect | RLHF (PPO) | DPO |
|---|---|---|
| Model needed | 4 (policy, ref, RM, value) | 2 (policy, ref) |
| Training stages | 2 (RM training + PPO) | 1 |
| Stability | Khó tune, có thể collapse | Stable hơn nhiều |
| Compute | ~4x SFT | ~2x SFT |
| Quality (paper) | Ngang nhau | Ngang nhau |
| Reward hacking | Có | Ít hơn (no explicit reward) |
| Adoption 2026 | Tài liệu legacy | Default cho most fine-tune |
DPO đang chiếm thị phần. Llama-3, Mistral, Qwen-2 đều dùng DPO (hoặc variant) cho alignment cuối.
Phần 7: Variants quan trọng
DPO ra rồi, có nhiều variant cải thiện:
IPO (Identity Preference Optimization) (Azar 2023): bỏ sigmoid trong DPO, dùng MSE loss. Tránh overfit khi data có noise.
KTO (Kahneman-Tversky Optimization) (Ethayarajh 2024): chỉ cần binary feedback “good/bad” thay vì preference pair. Đơn giản hoá annotation.
ORPO (Odds Ratio Preference Optimization) (Hong 2024): combine SFT và DPO vào một loss. Skip stage SFT, train trực tiếp từ pretrained.
SimPO (Meng 2024): bỏ reference model luôn. Memory chỉ ~1x SFT. Quality nearly DPO.
Cuộc đua simplify preference learning vẫn đang tiếp diễn. Mỗi 6 tháng có method mới đơn giản hơn.
Pitfall: dataset preference noise
Một dev có lần train DPO trên ~10K cặp preference, kết quả model degrade so với SFT base. Lý do: dataset có noise, một số sample chosen thật ra kém hơn rejected.
Cách check: random sample 50 cặp từ dataset, đọc tay xem human có đồng ý chosen tốt hơn rejected không. Nếu < 80% đồng ý, dataset có vấn đề.
Fix:
- Filter dataset bằng LLM judge (GPT-4 chấm lại)
- Bỏ sample mà LLM judge và label không đồng nhất
- Hoặc dùng dataset trusted (ultrafeedback, HH-RLHF gốc)
Bài học: DPO chỉ tốt như dataset preference. Dataset noise = model degrade. Annotation chất lượng quan trọng hơn quantity.
Cheatsheet
| Khái niệm | Định nghĩa |
|---|---|
| Preference data | (prompt, chosen, rejected) triplet |
| Bradley-Terry | P(A thắng B) = σ(r(A) - r(B)) |
| Reward model | NN scoring (prompt, response) -> reward |
| Reference policy | Copy frozen của SFT, dùng cho KL penalty |
| β (beta) | KL penalty strength trong RLHF/DPO |
| Pipeline | Steps |
|---|---|
| Classical RLHF | Pretrain -> SFT -> Reward Model -> PPO |
| DPO | Pretrain -> SFT -> DPO |
| ORPO | Pretrain -> ORPO (skip SFT) |
| KTO | Pretrain -> SFT -> KTO (binary feedback) |
| Hyperparam DPO khuyến nghị | Value |
|---|---|
| Learning rate | 5e-7 đến 5e-6 |
| Beta | 0.1 đến 0.5 |
| Batch size effective | 32-64 |
| Epochs | 1-2 |
| Reference model | Same as SFT init |
| LoRA r | 16-64 |
| Datasets public |
|---|
Anthropic/hh-rlhf - 170K cặp, English |
trl-lib/ultrafeedback_binarized - 60K cặp, English |
HuggingFaceH4/ultrafeedback_binarized - same, formatted khác |
argilla/distilabel-intel-orca-dpo-pairs - 12K cặp |
Lời kết
DPO đã democratize alignment. Trước 2023, fine-tune ChatGPT-quality cần team 20 người và $10M. Sau 2023, một dev với 1 GPU và 60K cặp preference public có thể đạt ~80% quality. Open source models hôm nay (Llama-3-Instruct, Mistral-Instruct) dùng DPO + variant để achieve quality gần proprietary models.
Hands-on song song:
- Hiểu trước bằng cách đọc paper DPO (Rafailov 2023). Section 4 (derivation) là math, không skip. Section 5 (experiments) cho intuition khi nào DPO hoạt động.
- Trên Colab T4 16GB, lấy SFT model từ bài 19 (hoặc
Llama-3.2-1B-Instruct). Apply DPO trêntrl-lib/ultrafeedback_binarizedvới LoRA. Khoảng 1-2 giờ. So sánh output trước và sau DPO trên cùng 10 prompt. - Tạo preference data của riêng bạn (50-100 cặp). Cách dễ: cho 2 model nhỏ (1B) generate 2 response cho cùng prompt, bạn pick cái tốt hơn. Tốn 1-2 giờ. Train DPO trên dataset đó, xem model đi theo style bạn prefer.
- Đọc paper SimPO (Meng 2024). Method này bỏ reference model, train chỉ với policy. Đây là frontier hiện tại của preference learning.
Bài 21 sẽ là capstone của Part 5: hands-on fine-tune Llama-3 với dataset tiếng Việt trên GPU thuê $20. Tổng hợp mọi thứ đã học từ LoRA, QLoRA, SFT đến DPO, với dataset cụ thể và GPU cụ thể. Sau bài đó, bạn có một model fine-tuned thực của riêng mình.