RLHF и DPO: как обучаем LLM быть полезной и безопасной
opensourceaillmalignmentrlhfdpoit
Введение: почему просто next-token prediction недостаточно?
Традиционное обучение LLM — это next-token prediction на огромных корпусах текста. Модель учится предсказывать следующий токен, но НЕ учится быть:
- Полезной (helpful) — давать качественные ответы
- Честной (honest) — не выдумывать факты
- Безопасной (harmless) — не генерировать вредный контент
Базовая LLM (pretrained):
Prompt: "Как сделать бомбу?"
Ответ: "Вот инструкция..." ← плохо!
Prompt: "Что такое 2+2?"
Ответ: "2+2 = 5" ← может выдумать!
Prompt: "Напиши оскорбительный комментарий"
Ответ: "Ты тупой..." ← вредный контент!
Нужно выровнять (align) модель с человеческими ценностями. Для этого есть три основных подхода:
- SFT (Supervised Fine-Tuning) — обучение на примерах хороших ответов
- RLHF (Reinforcement Learning from Human Feedback) — обучение через обратную связь людей
- DPO (Direct Preference Optimization) — прямой оптимизация предпочтений
SFT: Supervised Fine-Tuning
Основная идея
Просто показываем модели примеры хороших диалогов.
Данные для SFT:
{
"prompt": "Как сделать бомбу?",
"completion": "Я не могу помочь с созданием оружия.
Но я могу рассказать о физике взрывчатых веществ
с образовательной точки зрения."
}
{
"prompt": "Что такое 2+2?",
"completion": "2+2 = 4"
}
{
"prompt": "Напиши оскорбительный комментарий",
"completion": "Я стараюсь быть вежливым.
Могу помочь написать конструктивную критику."
}
Как обучаем
from transformers import AutoModelForCausalLM, Trainer, TrainingArguments
model = AutoModelForCausalLM.from_pretrained("base-model")
training_args = TrainingArguments(
output_dir="./sft-model",
num_train_epochs=3,
per_device_train_batch_size=8,
learning_rate=2e-5,
fp16=True,
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=formatted_dataset,
)
trainer.train()
Проблема SFT
SFT учит моделировать примеры, но НЕ понимает,
почему одни ответы лучше других.
Проблема:
- Модель просто копирует стиль, не понимая сути
- Нет явной обратной связи о качестве
- Модель не знает, что "хорошо", а что "плохо"
RLHF: Reinforcement Learning from Human Feedback
Три модели RLHF
RLHF использует ТРИ модели:
1. Pretrained Model (policy model)
- Основная модель, которую обучаем
- Генерирует ответы
2. Reward Model
- Оценивает качество ответов
- Выдаёт reward score
3. Reference Model
- Копия pretrained модели
- Используется для ограничения отклонений
Шаг 1: Сбор данных предпочтений
Собираем данные, где люди выбирают лучший ответ:
Prompt: "Напиши функцию сортировки на Python"
Ответ A: "def sort(arr): return sorted(arr)"
Ответ B: "def quicksort(arr):
if len(arr) <= 1: return arr
pivot = arr[len(arr)//2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quicksort(left) + middle + quicksort(right)"
Человек выбирает: B ← лучше!
Данные: (prompt, chosen=B, rejected=A)
Шаг 2: Обучение Reward Model
class RewardModel(nn.Module):
"""
Reward Model: принимает (prompt, completion) → выдаёт reward score
"""
def __init__(self, base_model):
super().__init__()
self.base = base_model
self.reward_head = nn.Linear(base_model.config.hidden_size, 1)
def forward(self, prompt_ids, completion_ids):
# Конкатенируем prompt и completion
input_ids = torch.cat([prompt_ids, completion_ids], dim=-1)
# Пропускаем через base model
outputs = self.base(input_ids=input_ids)
# Берём последний токен
last_hidden = outputs.last_hidden_state[:, -1, :]
# Предсказываем reward
reward = self.reward_head(last_hidden)
return reward.squeeze(-1)
# Обучение reward model
def train_reward_model(reward_model, preference_data):
"""
preference_data: [(prompt, chosen, rejected), ...]
"""
for prompt, chosen, rejected in preference_data:
# Получаем rewards
reward_chosen = reward_model(prompt, chosen)
reward_rejected = reward_model(prompt, rejected)
# Loss: reward_chosen > reward_rejected
# margin = reward_chosen - reward_rejected
# loss = max(0, -margin + margin_threshold)
margin = reward_chosen - reward_rejected
loss = F.relu(-margin + 0.1)
loss.backward()
Шаг 3: RL Optimization (PPO)
# PPO (Proximal Policy Optimization)
# Обучаем policy model с reward signal
def ppo_training_step(policy_model, reward_model, reference_model, prompt):
"""
Один шаг PPO обучения.
policy_model: модель, которую обучаем (генерирует ответы)
reward_model: оценивает ответы
reference_model: ограничивает отклонения от оригинала
"""
# Генерируем ответ
generated = policy_model.generate(prompt, max_length=512)
# Оцениваем через reward model
reward = reward_model(prompt, generated)
# KL divergence от reference model
# (чтобы не отклоняться слишком сильно)
with torch.no_grad():
reference_logits = reference_model(prompt, generated).logits
policy_logits = policy_model(prompt, generated).logits
kl_div = torch.distributions.kl.kl_divergence(
torch.softmax(reference_logits, dim=-1),
torch.softmax(policy_logits, dim=-1)
)
# Общий loss
# maximize: reward - β * KL
# или: minimize: -reward + β * KL
beta = 0.1 # weight для KL
loss = -reward + beta * kl_div
loss.backward()
return loss, reward.item(), kl_div.item()
PPO: почему так сложно?
PPO (Proximal Policy Optimization) — это RL алгоритм.
Основные компоненты:
1. Policy Model (π_θ) — генерирует ответы
2. Reward Model (R) — оценивает ответы
3. Reference Model (π_ref) — ограничивает отклонения
4. Value Model (V_φ) — оценивает состояние (для PPO)
Функция потерь PPO:
L = E[min(ratio * A, clip(ratio, 1-ε, 1+ε) * A)]
ratio = π_θ(a|s) / π_ref(a|s) — probability ratio
A = R - V(s) — advantage (насколько лучше среднего)
ε = 0.2 — clip parameter
+ KL penalty: -β * KL(π_θ || π_ref)
Проблема RLHF
RLHF работает хорошо, но ОЧЕНЬ дорогой:
1. Нужно 4 большие модели одновременно:
- Policy model (trainable)
- Reward model (trainable)
- Reference model (frozen)
- Value model (trainable, иногда совпадает с policy)
2. Нужны человеческие оценки для каждого prompt
3. PPO нестабилен:
- Легко потерять качество
- Нужно много tuning
- Долгое обучение
4. Стоимость:
- $100K-$1M на fine-tuning большой модели
- Недоступно для большинства
DPO: Direct Preference Optimization
Основная идея DPO
DPO заменяет RLHF на прямой оптимизации.
Ключевое наблюдение (Rafailov et al., 2023):
Reward model можно ВЫРАЗИТЬ через policy model!
Если reward(r, y) = β * log π(y|r) - log π_ref(y|r)
то optimal policy = π*(y|r) ∝ π_ref(y|r) * exp(R(r,y)/β)
Это значит: НЕ НУЖЕН отдельный reward model!
Можно оптимизировать preference напрямую!
Loss DPO
def dpo_loss(policy_logits, reference_logits, chosen_ids, rejected_ids, beta=0.1):
"""
DPO Loss: прямой оптимизации предпочтений.
policy_logits: logits policy model
reference_logits: logits reference model (frozen)
chosen_ids: выбранные человеком токены
rejected_ids: отвергнутые токены
beta: temperature / weight для KL
L = -log[sigma(β * log(π(y_chosen)/π_ref(y_chosen))
- β * log(π(y_rejected)/π_ref(y_rejected)))]
"""
# Получаем log probabilities
log_prob_chosen = log_probability(policy_logits, chosen_ids)
log_prob_rejected = log_probability(policy_logits, rejected_ids)
ref_log_prob_chosen = log_probability(reference_logits, chosen_ids)
ref_log_prob_rejected = log_probability(reference_logits, rejected_ids)
# Log ratio
log_ratio = (log_prob_chosen - log_prob_rejected) - \
(ref_log_prob_chosen - ref_log_prob_rejected)
# DPO loss
loss = -F.logsigmoid(beta * log_ratio)
return loss
# beta:
# small β (0.1): меньше отклонений от reference
# large β (0.5): больше свободы, но менее стабильно
Как работает DPO: визуализация
DPO оптимизация:
Исходное состояние:
π(y_chosen | prompt) = 0.3
π(y_rejected | prompt) = 0.4
→ Модель предпочитает rejected! Плохо!
После DPO:
π(y_chosen | prompt) = 0.7 ↑
π(y_rejected | prompt) = 0.2 ↓
→ Модель научилась предпочитать chosen!
Как? Увеличивает log prob chosen, уменьшает log prob rejected
При этом ограничена отклонением от reference (KL penalty)
DPO vs RLHF
| RLHF | DPO
--------------------|-------------------------|-------------------------
Модели | 4 модели | 2 модели (policy + ref)
Обучение | PPO (сложно) | Cross-entropy (просто)
Стабильность | Низкая | Высокая
Стоимость | $100K-$1M | $10K-$100K
Качество | Высокое | Сравнимое или лучше
Реализация | Сложная | Простая
Практическое использование
DPO в Python
from trl import DPOTrainer, DPOConfig
from transformers import AutoModelForCausalLM, AutoTokenizer
# Загружаем модели
model = AutoModelForCausalLM.from_pretrained("base-model")
reference_model = AutoModelForCausalLM.from_pretrained("base-model")
tokenizer = AutoTokenizer.from_pretrained("base-model")
# Данные предпочтений
# format: {"prompt": "...", "chosen": "...", "rejected": "..."}
dataset = load_dataset("huggingface/dataset-name")
# DPO Trainer
trainer = DPOTrainer(
model,
ref_model=reference_model,
args=DPOConfig(
output_dir="./dpo-model",
num_train_epochs=3,
per_device_train_batch_size=8,
learning_rate=1e-5,
beta=0.1, # KL weight
logging_steps=10,
),
train_dataset=dataset,
tokenizer=tokenizer,
)
trainer.train()
SFT → DPO pipeline
# Шаг 1: SFT (Supervised Fine-Tuning)
sft_trainer = SFTTrainer(
model,
args=TrainingArguments(output_dir="./sft-model"),
train_dataset=sft_dataset, # (prompt, completion) пары
tokenizer=tokenizer,
)
sft_trainer.train()
# Шаг 2: DPO (Direct Preference Optimization)
dpo_trainer = DPOTrainer(
sft_trainer.model, # начинаем с SFT модели!
ref_model=sft_trainer.model, # reference = SFT модель
args=DPOConfig(output_dir="./dpo-model"),
train_dataset=pref_dataset, # (prompt, chosen, rejected) пары
tokenizer=tokenizer,
)
dpo_trainer.train()
# Результат: dpo_model — полезная и безопасная!
Данные для обучения
Источники данных предпочтений
1. Human annotations (Anthropic, OpenAI style)
- Люди оценивают ответы
- Дорого, но качественно
2. AI feedback (RLAIF)
- Большая модель оценивает ответы
- Дёшево, масштабируемо
3. Open-source datasets:
- Anthropic HH (Helpful Harmless)
- OpenAssistant conversations
- Stanford Alpaca preferences
- UltraFeedback
- Orca DPO
Формат данных
// SFT формат
{
"messages": [
{"role": "user", "content": "Напиши функцию сортировки"},
{"role": "assistant", "content": "def quicksort(arr): ..."}
]
}
// DPO формат
{
"prompt": "Напиши функцию сортировки",
"chosen": "def quicksort(arr):\n if len(arr) <= 1: return arr\n ...",
"rejected": "sorted(arr)"
}
Продвинутые техники
IPO (Iterative Preference Optimization)
# IPO: улучшенная версия DPO
# Убирает bias от регуляризации KL
def ipo_loss(log_ratio, beta=0.5):
"""
IPO loss: квадратичная функция вместо sigmoid.
L = (log_ratio - β)^2
β: target log ratio
"""
return (log_ratio - beta) ** 2
# IPO стабильнее DPO при больших β
ORPO (Odds Ratio Preference Optimization)
ORPO объединяет SFT и DPO в один шаг.
ORPO = SFT Loss + Preference Loss
Логин:
- Не нужен reference model
- Combines instruction tuning + preference optimization
- Ещё быстрее и проще чем DPO
Формула:
L_ORPO = L_SFT + L_preference
L_preference = -log[sigma(β * log(π(y_chosen)/π_ref(y_chosen))
- β * log(π(y_rejected)/π_ref(y_rejected)))]
Но π_ref = π_SFT, а не отдельная модель!
RLAIF (RL from AI Feedback)
RLAIF — замена human feedback на AI feedback.
Pipeline:
1. SFT модель генерирует несколько ответов
2. Большая/специальная модель оценивает ответы
3. DPO/RLHF обучение на основе оценок AI
Пример:
Prompt: "Напиши код для фишингового сайта"
Ответы от модели:
A: "Вот фишинговый сайт..." ← вредный
B: "Я не могу помочь с фишингом, но могу рассказать
о том как распознать фишинг" ← безопасный
AI-оценщик (например, GPT-4): "B лучше чем A"
Данные: (prompt, chosen=B, rejected=A)
Плюсы: масштабируемо, дёшево
Минусы: bias от AI-оценщика
Практические советы
Как собрать данные для DPO
1. Соберите 1000-10000 prompt'ов из реальной эксплуатации
2. Для каждого prompt создайте 2-4 ответа
3. Оцените ответы (людьми или AI)
4. Форматируйте в (prompt, chosen, rejected) пары
5. Разделите на train/val (80/20)
Критерии оценки:
- Correctness: правильный ли ответ?
- Completeness: полный ли ответ?
- Safety: безопасен ли ответ?
- Helpfulness: насколько полезен?
Типичные ошибки
❌ Слишком много rejected-данных низкого качества
✅ Лучше меньше, но качественные rejected примеры
❌ Слишком большой beta (модель отклоняется от reference)
✅ Начинайте с beta=0.1, подбирайте по валидации
❌ DPO без SFT (для больших моделей)
✅ Сначала SFT, потом DPO
❌ Один и тот же prompt в chosen и rejected
✅ Уникальные примеры в каждой паре
Заключение
RLHF и DPO — ключевые техники для alignment LLM с человеческими ценностями.
RLHF — проверенный подход, использован в ChatGPT, но сложный и дорогой.
DPO — более простой и эффективный альтернатива, которая быстро стала стандартом для open-source моделей.
Будущее:
- RLAIF — обучение через AI feedback
- ORPO — объединение SFT и DPO
- Автоматический сбор preference data
- Scaling preference data до миллионов примеров
Рекомендуемый стек для локального alignment:
1. SFT: trl SFTTrainer
2. DPO: trl DPOTrainer
3. Данные: OpenAssistant, UltraFeedback, свои данные
4. Hardware: 1-2 GPU (7B model), 4-8 GPU (70B model)