RLHF cổ điển đi ba chặng: SFT → huấn luyện một reward model → dùng PPO tinh chỉnh policy theo reward đó kèm phạt KL. Lúc chạy phải giữ bốn model cùng lúc (policy, reference, reward, critic), và vì có RL bên trong nên rất khó train ổn định.
DPO bỏ được cả reward model lẫn RL. Ý tưởng gốc: với ràng buộc KL, policy tối ưu của bài toán RLHF có dạng đóng theo reward — π*(y|x) ∝ π_ref(y|x)·exp(r(x,y)/β). Đảo lại thì reward viết được theo chính policy: r(x,y) = β·log(π*(y|x)/π_ref(y|x)). Thay biểu thức này vào mô hình so sánh cặp Bradley-Terry, reward triệt tiêu — còn lại một hàm loss chỉ chứa policy và reference:
L_DPO = −E[ log σ( β·log(π_θ(y_w|x)/π_ref(y_w|x))
− β·log(π_θ(y_l|x)/π_ref(y_l|x)) ) ]trong đó y_w là câu được chọn, y_l là câu bị loại, π_ref là model SFT đóng băng, β cỡ 0,1–0,5.
Nói gọn: reward model vẫn còn đó, nhưng nằm ẩn ngay trong policy thay vì là một mạng riêng phải huấn luyện.
Luồng chạy: lấy model SFT làm reference → thu dữ liệu cặp (prompt, câu tốt, câu tệ) → train bằng loss trên. Không reward model, không rollout, không PPO.
Vì sao thay được RLHF:
- Chỉ là một hàm loss có giám sát, viết như huấn luyện thường.
- Ổn định — không có vòng lặp RL nên không bị policy trôi hay reward hacking.
- Bộ nhớ còn hai model thay vì bốn.
- Chất lượng ngang hoặc hơn PPO trên các bộ so sánh công khai, với công sức ít hơn hẳn.
Hạn chế:
- Offline — học trên tập cặp cố định, không sinh và chấm ngay trong lúc train như PPO. Dữ liệu lệch thì policy lệch theo.
- Nhạy với β, phải dò.
- Chỉ học theo từng cặp so sánh, không rút ra được một hàm reward dùng lại cho việc khác.
Biến thể đáng biết: KTO chỉ cần nhãn tốt/xấu cho từng câu thay vì cặp nên dễ thu dữ liệu hơn nhiều; ORPO gộp luôn SFT và preference vào một loss, bỏ được một chặng; SimPO bỏ cả reference model.
Chọn khi nào: DPO là mặc định hợp lý cho phần lớn việc align model mở. PPO vẫn dùng ở pipeline online quy mô lớn vì chịu dữ liệu preference nhiễu tốt hơn. Không đủ người gán nhãn thì thay bằng preference do AI chấm.
from trl import DPOTrainer, DPOConfig
# dữ liệu: {"prompt": ..., "chosen": ..., "rejected": ...}
trainer = DPOTrainer(
model=sft_model,
ref_model=None, # để None thì tự sao chép từ model
args=DPOConfig(beta=0.1, learning_rate=5e-7, num_train_epochs=1),
train_dataset=pref_dataset,
)
trainer.train()