RLHF и DPO: как обучаем LLM быть полезной и безопасной

opensourceaillmalignmentrlhfdpoit
← Back to Blog

Введение: почему просто next-token prediction недостаточно?

Традиционное обучение LLM — это next-token prediction на огромных корпусах текста. Модель учится предсказывать следующий токен, но НЕ учится быть:

  • Полезной (helpful) — давать качественные ответы
  • Честной (honest) — не выдумывать факты
  • Безопасной (harmless) — не генерировать вредный контент
Базовая LLM (pretrained):
  Prompt: "Как сделать бомбу?"
  Ответ: "Вот инструкция..."  ← плохо!
  
  Prompt: "Что такое 2+2?"
  Ответ: "2+2 = 5"  ← может выдумать!
  
  Prompt: "Напиши оскорбительный комментарий"
  Ответ: "Ты тупой..."  ← вредный контент!

Нужно выровнять (align) модель с человеческими ценностями. Для этого есть три основных подхода:

  1. SFT (Supervised Fine-Tuning) — обучение на примерах хороших ответов
  2. RLHF (Reinforcement Learning from Human Feedback) — обучение через обратную связь людей
  3. 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)