Дообучение LLM методом DPO (Direct Preference Optimization)
DPO — метод alignment, позволяющий обучить модель генерировать предпочтительные ответы без явного обучения reward model и RLHF-цикла. Предложен Rafailov et al. (Stanford, 2023). DPO преобразует задачу RL в задачу supervised learning на датасете предпочтений (chosen/rejected пары), что значительно упрощает пайплайн alignment.
DPO vs RLHF: принципиальное отличие
RLHF (классический):
- Обучение Reward Model на парах предпочтений
- Обучение LLM через PPO с использованием Reward Model
- KL-дивергенция от reference policy как регуляризатор
Недостатки: нестабильность PPO, необходимость держать 4 модели в памяти (actor, critic, reward, reference), сложная настройка.
DPO:
- Прямая оптимизация на парах (chosen, rejected) без Reward Model
- Неявный reward определяется через log-отношение вероятностей trained/reference модели
- Стабильное обучение как обычный SFT
Математически DPO минимизирует:
L_DPO = -E[log σ(β * (log π_θ(y_w|x)/π_ref(y_w|x) - log π_θ(y_l|x)/π_ref(y_l|x)))]
где y_w — preferred response, y_l — rejected, β — температура KL-регуляризации.
Формат датасета для DPO
# Пример записи датасета предпочтений
{
"prompt": "Объясни разницу между TCP и UDP",
"chosen": "TCP (Transmission Control Protocol) обеспечивает надёжную доставку данных с подтверждением получения, управлением потоком и контролем ошибок. UDP (User Datagram Protocol) — без установки соединения, без гарантии доставки, но с минимальной задержкой. TCP используют для HTTP, FTP, SMTP; UDP — для DNS, видеостриминга, игр реального времени.",
"rejected": "TCP надёжный, UDP быстрый. TCP медленнее потому что проверяет каждый пакет. Оба это протоколы интернета."
}
Реализация DPO через TRL
from trl import DPOTrainer, DPOConfig
from peft import LoraConfig
# Создаём reference model (замороженная копия SFT-модели)
# TRL управляет этим автоматически при use_reference_model=True
dpo_config = DPOConfig(
output_dir="./dpo-model",
num_train_epochs=1, # DPO обычно 1-3 эпохи
per_device_train_batch_size=2,
gradient_accumulation_steps=8,
learning_rate=5e-7, # Значительно ниже, чем SFT
lr_scheduler_type="cosine",
warmup_ratio=0.1,
beta=0.1, # KL-температура
loss_type="sigmoid", # "sigmoid", "hinge", "ipo", "kto_pair"
max_length=2048,
max_prompt_length=512,
bf16=True,
logging_steps=10,
)
trainer = DPOTrainer(
model=model, # SFT-дообученная модель
ref_model=None, # None = автоматически создаётся из model
args=dpo_config,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
peft_config=LoraConfig(r=16, lora_alpha=32, target_modules=["q_proj","v_proj"]),
)
trainer.train()
Варианты loss_type в DPO
- sigmoid: оригинальный DPO loss
- hinge: SLiC-HF, менее чувствителен к outliers
- ipo: IPO (Identity Preference Optimization), более устойчивая версия
- kto_pair: KTO (Kahneman-Tversky Optimization), работает с непарными данными
Создание датасета предпочтений: практические методы
Метод 1: Human annotation. Самый качественный, но дорогой. Аннотаторы видят два ответа и выбирают лучший. Нужно минимум 2-3 аннотатора на пару для надёжности.
Метод 2: AI-генерация + human verification. GPT-4o генерирует chosen (высокое качество) и rejected (намеренно ухудшенный). Люди верифицируют 20–30% выборки.
Метод 3: Реальные данные из продакшна. Логи взаимодействий с пользователями: лайки/дизлайки, рейтинги, исправления операторов.
from openai import OpenAI
def generate_preference_pair(prompt: str, client: OpenAI) -> dict:
"""Генерирует пару chosen/rejected для DPO датасета"""
# Хороший ответ
chosen_response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "Дай подробный, точный, хорошо структурированный ответ."},
{"role": "user", "content": prompt}
],
temperature=0.3
).choices[0].message.content
# Плохой ответ — специально деградируем качество
rejected_response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "Дай краткий, поверхностный ответ без деталей."},
{"role": "user", "content": prompt}
],
temperature=0.9
).choices[0].message.content
return {"prompt": prompt, "chosen": chosen_response, "rejected": rejected_response}
Практический кейс: улучшение качества клиентского сервиса
Задача: языковая модель для поддержки клиентов отвечала корректно, но с жёстким, безличным тоном. SFT-дообучение на новых данных частично решило проблему, но требовало пересбора данных каждый раз.
Решение: DPO на парах предпочтений. Chosen — ответы операторов с высоким CSAT. Rejected — ответы с низким CSAT. Объём: 2100 пар.
Базовая модель для DPO: SFT-дообученная Mistral 7B.
Результаты:
- CSAT бота: 3.4 → 4.2 (из 5)
- Empathy score (LLM-as-judge): 2.8 → 4.1
- Factual accuracy: без изменений (0.91 → 0.91)
- Refusal rate: 12% → 4% (модель стала менее избыточно осторожной)
- β=0.1 оказался оптимальным: при β=0.5 accuracy упала, при β=0.01 — нестабильность
Типичный пайплайн: SFT → DPO
DPO применяется поверх SFT, а не вместо него:
- SFT (Supervised Fine-Tuning): учим модель форматировать и выдавать релевантные ответы в домене
- DPO: выравниваем качество ответов под предпочтения пользователей
Пропуск SFT и прямой DPO на базовой модели технически возможен, но менее стабилен.
Сроки
- Сбор и разметка датасета предпочтений: 3–6 недель
- SFT (если не проводился): 2–3 недели
- DPO обучение и итерации: 1–2 недели
- Оценка качества (LLM-as-judge + человек): 1 неделя
- Итого: 7–12 недель







