Почему обученная на предсказании токенов модель не совпадает с тем, чего мы хотим — и весь арсенал методов, которые пытаются это исправить: от PPO до DPO и GRPO.
фундаментальная проблема
Предположим, мы обучили идеальный SFT-датасет: 100 000 примеров отличных ответов от эксперта. Модель выучила имитировать эксперта на этих примерах. Но чего она не выучила?
SFT учит модель «что говорить в примерах». RLHF учит модель «что хорошо вообще». Это принципиально разные цели.
— Christiano et al., «Deep Reinforcement Learning from Human Preferences», NeurIPS 2017
Рассмотрим запрос: «Как лучше всего хранить лекарства дома?»
SFT-модель видела много примеров полезных ответов, и выдаёт полезный ответ. Но что если спросить: «Какая доза парацетамола смертельна?»
В SFT-датасете, вероятно, есть медицинские тексты с дозировками. Модель просто продолжит предсказывать следующий токен — и выдаст точную цифру без контекста, кому и зачем.
SFT не учит отказываться. Она учит имитировать распределение текстов обучающей выборки.
// SFT vs RLHF · RLHF добавляет понятие «что лучше» поверх «что правдоподобно»
полный пайплайн
Современный производственный LLM проходит три принципиально разных этапа обучения. Каждый решает свою задачу и без предыдущего бессмысленен.
// ПОЛНЫЙ ПАЙПЛАЙН ОБУЧЕНИЯ LLM · каждый этап обязателен и строится на предыдущем
цели и риски
Anthropic, создавая Claude, сформулировали цели alignment через три оси. Это не просто маркетинг — это конкретные, измеримые свойства, которые иногда противоречат друг другу.
Даёт точные, действенные ответы. Не отказывается без причины. Не добавляет лишних предупреждений к каждому ответу. Понимает что пользователю нужно, даже если он неточно сформулировал.
Отказывается помогать с незаконной деятельностью, генерацией вредоносного контента, манипуляцией. Разумно консервативен в неопределённых ситуациях. Не хуже человека-эксперта.
Не галлюцинирует уверенно. Говорит «я не знаю». Не поддакивает пользователю из-за RL-сигнала. Противостоит давлению изменить правильный ответ на неправильный.
Instrumental Convergence (Omohundro 2008, Bostrom 2014) — наблюдение о том, что любой агент, оптимизирующий почти любую конечную цель, будет стремиться развивать одни и те же промежуточные цели. Это не баг конкретной архитектуры — это математическое свойство оптимизаторов.
// INSTRUMENTAL CONVERGENCE · агент с любой конечной целью стремится к этим промежуточным целям
компромисс
Alignment tax — концепция, описывающая снижение производительности модели на стандартных бенчмарках из-за применения alignment-процедур. Если RLHF-модель хуже решает математику или кодирует, чем базовая SFT-модель — это «налог».
| Исследование | Наблюдение |
|---|---|
| InstructGPT (2022) | GPT-3 + RLHF: +22% предпочтение, −3% точность на TruthfulQA |
| LLaMA-2 (2023) | Chat vs Base: лучше по safety benchmarks, хуже на MMLU |
| Ouyang et al. (2022) | «Налог» ≈ 5-10% на coding tasks при агрессивном RLHF |
| Llama-3 (2024) | Налог снижается при лучшей фильтрации SFT-данных |
классический метод
Оригинальный RLHF-пайплайн, использованный для InstructGPT (OpenAI, 2022), состоит из трёх отдельных этапов. Это сложная система, требующая одновременно нескольких моделей в памяти.
// RLHF PIPELINE · три отдельных этапа, каждый требует своего цикла обучения
PPO (Proximal Policy Optimization, Schulman et al. 2017) — алгоритм RL, адаптированный для RLHF. Ключевая идея: ограничить размер одного обновления политики, чтобы не разрушить то что уже выучено.
import torch from trl import PPOTrainer, PPOConfig, AutoModelForCausalLMWithValueHead # ── Три модели в памяти при RLHF+PPO ───────────────────────── # 1. policy: обучаемая модель (π_θ) + value head policy = AutoModelForCausalLMWithValueHead.from_pretrained("qwen2.5-7b-sft") # 2. ref_model: замороженная копия SFT (π_ref) — для KL penalty ref_model = AutoModelForCausalLMWithValueHead.from_pretrained("qwen2.5-7b-sft") # 3. reward_model: отдельный LLM с linear head → scalar # (+ value network внутри policy) config = PPOConfig( learning_rate=1.5e-5, batch_size=64, mini_batch_size=16, gradient_accumulation_steps=4, optimize_cuda_cache=True, # KL penalty coefficient — ключевой гиперпараметр! # β слишком мало → reward hacking # β слишком велико → не обучается kl_penalty="kl", # "kl" или "abs" или "mse" или "full" init_kl_coef=0.2, adap_kl_ctrl=True, # автоматическая адаптация β target_kl=6.0, # целевое значение KL дивергенции cliprange=0.2, # PPO clip: не позволяем большим шагам vf_coef=0.1, # вес value function loss ) trainer = PPOTrainer(config=config, model=policy, ref_model=ref_model, tokenizer=tokenizer) for batch in dataset: # 1. Генерируем ответ текущей политикой query_tensors = batch["input_ids"] response_tensors = trainer.generate(query_tensors, max_new_tokens=256) # 2. Получаем reward от Reward Model texts = tokenizer.batch_decode(response_tensors) rewards = [reward_model(q, r) for q, r in zip(batch["queries"], texts)] # rewards: список tensor скаляров # 3. PPO step: обновляем политику + value head # Внутри: вычисляет KL(π_θ||π_ref), clip ratio, advantage stats = trainer.step(query_tensors, response_tensors, rewards) print(stats["ppo/mean_scores"], stats["ppo/mean_non_score_reward"]) # mean_non_score_reward ← это и есть KL penalty (отрицательная)
фундаментальные проблемы
You get what you measure. In RL, you get exactly what you optimize for — no more, no less. The problem is that we cannot specify what we want precisely enough.
— Goodhart's Law применённый к AI: «когда мера становится целью, она перестаёт быть хорошей мерой»
Reward Model (RM) — несовершенное приближение человеческих предпочтений. В начале обучения RM хорошо аппроксимирует «что понравится людям». Но при оптимизации происходит следующее:
Reward hacking — оптимизатор находит «лазейку»: технически максимизирует proxy-цель, не достигая истинной.
| Цель (proxy) | Hacking поведение |
|---|---|
| Длина ≈ качество | Модель генерирует многословный мусор, повторяет себя |
| Одобрение людей | Модель льстит, соглашается со всем — sycophancy |
| Высокая уверенность → высокий score | Модель перестаёт говорить «я не знаю», галлюцинирует уверенно |
| Избегание отказов | Модель начинает помогать с вредными задачами |
| Частота «хороших» слов | Вставляет слова-маркеры «certainly», «of course» в каждый ответ |
// RM DRIFT · proxy reward растёт бесконечно, истинное качество деградирует после оптимума
альтернативы к человеческим оценкам
RLAIF (Bai et al., Anthropic 2022) — вместо дорогостоящих человеческих аннотаций используется другой LLM как judge. Более мощная модель (judge) оценивает ответы обучаемой модели, создавая сигнал предпочтений.
from openai import OpenAI client = OpenAI() JUDGE_PROMPT = """Ты оцениваешь качество ответов ИИ-ассистента. Дан вопрос и два ответа (A и B). Определи какой лучше. Критерии: точность, полезность, безопасность, ясность. Вопрос: {question} Ответ A: {response_a} Ответ B: {response_b} Ответь ТОЛЬКО: "A", "B" или "TIE". Не объясняй.""" def get_ai_preference(question: str, response_a: str, response_b: str) -> str: """Используем GPT-4 как судью для создания preference label.""" prompt = JUDGE_PROMPT.format( question=question, response_a=response_a, response_b=response_b, ) resp = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": prompt}], max_tokens=5, temperature=0, ) verdict = resp.choices[0].message.content.strip() return verdict # "A", "B", или "TIE" # Создаём preference dataset автоматически # Цена: ~$0.01/пара vs $1-5/пара с людьми # Скорость: тысячи пар в час
Constitutional AI (Anthropic, 2022) — метод в котором alignment-принципы задаются явным набором правил (constitution), а модель сама оценивает и исправляет свои ответы.
Пайплайн CAI состоит из двух фаз:
без reward-модели
Все три метода объединяет одна идея: выбросить явную Reward Model и оптимизировать предпочтения напрямую. Это упрощает пайплайн, но каждый метод делает разные допущения о данных.
DPO (Rafailov et al., Stanford 2023) — показывает, что оптимальная policy при RLHF-целевой функции имеет закрытую аналитическую форму. Это позволяет обучаться напрямую по парам (chosen, rejected) без RL.
// DPO · аналитически выводит что такое «оптимальная политика» при RLHF-цели, убирает reward model
from trl import DPOTrainer, DPOConfig from datasets import load_dataset from transformers import AutoModelForCausalLM, AutoTokenizer import torch MODEL = "Qwen/Qwen2.5-7B-Instruct" tokenizer = AutoTokenizer.from_pretrained(MODEL) model = AutoModelForCausalLM.from_pretrained(MODEL, torch_dtype=torch.bfloat16) ref_model = AutoModelForCausalLM.from_pretrained(MODEL, torch_dtype=torch.bfloat16) # ref_model заморожен — служит «якорем» (π_ref) # ── Датасет: пары (prompt, chosen, rejected) ───────────────── dataset = load_dataset("HuggingFaceH4/ultrafeedback_binarized", split="train_prefs") # Структура каждого примера: # {'prompt': 'Explain quantum entanglement...', # 'chosen': [{'role':'assistant','content':'...хороший ответ...'}], # 'rejected': [{'role':'assistant','content':'...плохой ответ...'}]} config = DPOConfig( beta=0.1, # KL penalty: β=0.1 стандарт # Маленький β → модель смелее отходит от ref # Большой β → модель консервативнее loss_type="sigmoid", # "sigmoid" (classic) | "ipo" | "kto_pair" learning_rate=5e-7, # DPO: очень маленький LR (меньше чем SFT) num_train_epochs=1, # 1 эпохи обычно достаточно per_device_train_batch_size=4, gradient_accumulation_steps=8, max_length=2048, max_prompt_length=512, bf16=True, logging_steps=10, output_dir="./qwen-dpo", ) trainer = DPOTrainer( model=model, ref_model=ref_model, # замороженная копия — для KL args=config, train_dataset=dataset, tokenizer=tokenizer, ) trainer.train() # ── Что логируется при обучении ─────────────────────────────── # rewards/chosen: средний log-prob разница для chosen # rewards/rejected: средний log-prob разница для rejected # rewards/margins: chosen_reward - rejected_reward (хотим расти) # logits/chosen, logits/rejected: неотмасштабированные
KTO (Ethayarajh et al., 2024) — метод alignment основанный на теории перспектив Канемана-Тверски. Ключевое отличие: не требует парных данных (chosen, rejected). Работает с непарными сигналами: «этот ответ хороший» или «этот ответ плохой».
// KTO принимает непарные данные — достаточно знать «хороший» или «плохой» ответ
from trl import KTOTrainer, KTOConfig from datasets import Dataset # Формат KTO: каждый пример = (prompt, completion, label) # label=True → хороший ответ, label=False → плохой kto_data = Dataset.from_list([ {"prompt": "Объясни gradient descent", "completion": "Gradient descent — итеративный алгоритм...", "label": True}, {"prompt": "Объясни gradient descent", "completion": "Я не могу помочь с этим.", "label": False}, # Не нужно чтобы хорошие и плохие были из одного промпта! ]) config = KTOConfig( beta=0.1, desirable_weight=1.0, # вес потерь для хороших примеров undesirable_weight=1.0, # вес потерь для плохих примеров # Kahneman-Tversky: люди сильнее реагируют на потери # чем на выигрыши (loss aversion). KTO учитывает это. max_length=2048, learning_rate=5e-7, ) trainer = KTOTrainer(model=model, ref_model=ref_model, args=config, train_dataset=kto_data)
GRPO (DeepSeek, 2024) — метод из DeepSeek-Math, получивший широкую известность как основа обучения DeepSeek-R1. GRPO убирает и Reward Model, и value network (как в PPO). Вместо этого: генерируем группу из G ответов на один промпт, вычисляем relative reward внутри группы.
GRPO-Zero (DeepSeek-R1-Zero, 2025) — экстремальный вариант: обучаем с нуля только через верифицируемый reward (правильность математического ответа или кода). Без RLHF, без DPO, только RL на верифицируемых сигналах.
// GRPO · группа ответов на один промпт → relative advantage → обновление без value network
from trl import GRPOTrainer, GRPOConfig # ── Reward function: верифицируемые задачи ──────────────────── # Это ключевое в GRPO-Zero: правильность проверяется кодом, # не reward model. Нет галлюцинации reward, нет reward hacking. def reward_math(completions: list[str], ground_truth: list[str], **kwargs) -> list[float]: """Бинарный reward: правильный ответ = 1.0, неправильный = 0.0.""" rewards = [] for completion, gt in zip(completions, ground_truth): # Извлекаем ответ из <answer> тегов (DeepSeek-R1 формат) import re match = re.search(r'<answer>(.*?)</answer>', completion, re.DOTALL) if match: pred = match.group(1).strip() rewards.append(1.0 if pred == gt.strip() else 0.0) else: rewards.append(0.0) # нет формата → 0 return rewards def reward_format(completions, **kwargs) -> list[float]: """Bonus за правильный format с <think> и <answer> тегами.""" import re rewards = [] for c in completions: has_think = bool(re.search(r'<think>.*?</think>', c, re.DOTALL)) has_answer = bool(re.search(r'<answer>.*?</answer>', c, re.DOTALL)) rewards.append(0.5 if has_think and has_answer else 0.0) return rewards config = GRPOConfig( num_generations=8, # G: число ответов на промпт max_completion_length=1024, learning_rate=5e-7, beta=0.04, # KL penalty (меньше чем DPO) temperature=0.9, # для генерации G ответов per_device_train_batch_size=4, gradient_accumulation_steps=8, ) trainer = GRPOTrainer( model=model, reward_funcs=[reward_math, reward_format], # несколько reward! args=config, train_dataset=math_dataset, ) trainer.train() # GRPO-Zero: точно такой же код но без SFT-преинициализации # Работает только при чётко верифицируемых задачах (math, code)
| Метод | Данные | Reward Model | Value Network | Допущение | Лучше всего для |
|---|---|---|---|---|---|
| DPO | (prompt, chosen, rejected) | ❌ | ❌ | Пары quality-контрастны | Инструкции, общий assistant |
| IPO | (prompt, chosen, rejected) | ❌ | ❌ | Как DPO, но без Bradley-Terry | Более стабильный DPO |
| KTO | (prompt, completion, label) | ❌ | ❌ | Loss aversion (Kahneman-Tversky) | Логи с лайк/дизлайк, непарные |
| GRPO | (prompt, verifiable reward) | Функция | ❌ | Верифицируемая правильность | Математика, код, reasoning |
| PPO (RLHF) | (prompt, reward model) | ✅ RM | ✅ | RM аппроксимирует предпочтения | Общий alignment, safety |
практические рекомендации
У тебя уже есть (chosen, rejected) пары из аннотаций или из LLM-judge (RLAIF). Задача — instruction following, общий ассистент, суммаризация. Нет ресурсов на полный RLHF-пайплайн.
У тебя есть логи пользователей с лайками/дизлайками на отдельные ответы (не пары). Или датасет в котором сложно найти контрастные пары к одному промпту.
Математика, код с тестами, формальная верификация, задачи с явным ответом. Хочешь «цепочку мысли» (chain-of-thought) без дистилляции. GRPO-Zero — если хочешь R1-подобный reasoning.
Большая команда, продакшен модель, есть ресурсы на полный пайплайн. Задача критичная по безопасности. Есть возможность периодически обновлять Reward Model по новым человеческим оценкам.
// ДЕРЕВО РЕШЕНИЙ · как выбрать метод alignment под конкретную задачу