LLM Course, глава 12 (часть 2): GRPO из DeepSeekMath по формулам
Перевод раздела 12.3b курса Hugging Face: групповая выборка, групповое преимущество, отношение вероятностей, клиппинг и KL-штраф в GRPO — с разобранным примером и кодом на PyTorch.
Это перевод раздела 12.3b курса LLM Course. Он продолжает часть 1 и разбирает GRPO уже с формулами. Дальше — часть 3: GRPO в TRL и практика.
Углубим понимание GRPO, чтобы улучшить обучение нашей модели.
GRPO оценивает ответы модели напрямую — сравнивая их внутри групп сгенерированных ответов — и так оптимизирует модель-политику, вместо того чтобы обучать отдельную модель ценности (value model, «критика»). Это заметно снижает вычислительные затраты!
GRPO применим к любой проверяемой задаче, где можно определить правильность ответа. Например, в математических рассуждениях правильность легко проверить, сравнив ответ с эталоном.
Прежде чем углубляться в технические детали, посмотрим на работу GRPO в общих чертах. (В оригинале — схема: вопрос подаётся политике, она выдаёт ответов, каждый оценивается наградой, награды нормируются в преимущества, а преимущества вместе с KL-штрафом относительно эталонной модели дают обновление политики.)
flowchart LR Q["Вопрос q"] --> P["Политика π_old<br/>генерирует G ответов"] P --> O["o1, o2, …, oG"] O --> R["Награды r1…rG<br/>(функция или модель)"] R --> A["Преимущества<br/>A_i = (r_i − mean) / std"] A --> U["Обновление π_θ:<br/>клиппинг + KL к π_ref"]
Теперь разберём GRPO по шагам.
Алгоритм GRPO
Заголовок раздела «Алгоритм GRPO»Главное новшество GRPO — подход к оценке и обучению на нескольких сгенерированных ответах сразу. Вместо отдельной модели награды он сравнивает ответы внутри одной группы и решает, какие из них стоит закрепить.
Пройдём каждый шаг алгоритма подробно.
Шаг 1. Групповая выборка
Заголовок раздела «Шаг 1. Групповая выборка»Первый шаг — сгенерировать на каждый вопрос несколько возможных ответов. Получается разнообразный набор ответов, которые можно сравнивать друг с другом.
На каждый вопрос модель генерирует ответов (размер группы) обучаемой политикой: , например , где каждый — один вариант ответа модели.
Для наглядности возьмём простую арифметическую задачу.
Вопрос : вычислить .
Ответы (): .
Обратите внимание: одни ответы верные (14), другие — нет (16 или 10). Это разнообразие критически важно для следующего шага.
Шаг 2. Расчёт преимущества
Заголовок раздела «Шаг 2. Расчёт преимущества»Получив несколько ответов, нужно понять, какие из них лучше. Для этого и считается преимущество (advantage).
Распределение наград
Заголовок раздела «Распределение наград»Сначала каждому ответу присваивается награда. В этом примере используем модель награды, но, как мы узнали в прошлом разделе, подойдёт любая функция, возвращающая награду.
Каждому сгенерированному ответу ставится оценка по правильности (например, 1 за верный ответ и 0 за неверный), а затем для каждого считается преимущество.
Формула преимущества
Заголовок раздела «Формула преимущества»Ключевая идея GRPO: абсолютная мера качества не нужна — достаточно сравнивать ответы внутри одной группы. Для этого награды стандартизируют:
Словами: из награды ответа вычитаем среднюю награду группы и делим на разброс наград в группе.
Продолжим арифметический пример. Пусть у нас 8 ответов, 4 из них верные, остальные неверные. Тогда:
| Величина | Значение |
|---|---|
| Среднее по группе | |
| Стандартное отклонение | |
| Преимущество верного ответа | |
| Преимущество неверного ответа |
Интерпретация
Заголовок раздела «Интерпретация»Посчитав преимущества, разберёмся, что они значат.
Стандартизация (то есть взвешивание через ) позволяет модели оценить относительное качество каждого ответа: оптимизация поощряет ответы лучше среднего (с высокой наградой) и подавляет худшие. Если , ответ лучше среднего уровня в своей группе; если , качество ответа ниже среднего (то есть ответ плохой).
В примере выше при (верный ответ) вероятность его генерации на шагах оптимизации будет повышаться.
Преимущества посчитаны — можно обновлять политику.
Шаг 3. Обновление политики
Заголовок раздела «Шаг 3. Обновление политики»Последний шаг — с помощью преимуществ обновить модель так, чтобы в будущем она чаще выдавала хорошие ответы.
Целевая функция обновления политики:
Поначалу формула выглядит пугающе, но она собрана из нескольких частей, у каждой из которых своя важная роль. Разберём их по очереди.
Ключевые части целевой функции
Заголовок раздела «Ключевые части целевой функции»Функция обновления GRPO сочетает несколько приёмов, чтобы обучение было устойчивым и эффективным. Разберём каждую часть.
1. Отношение вероятностей
Заголовок раздела «1. Отношение вероятностей»Отношение вероятностей определяется так:
Интуитивно формула сравнивает, насколько вероятность ответа у новой модели отличается от вероятности у старой, и при этом учитывает предпочтение ответов, улучшающих ожидаемый результат.
Интерпретация
Заголовок раздела «Интерпретация»- Если , новая модель даёт ответу большую вероятность, чем старая.
- Если , новая модель даёт меньшую вероятность.
Это отношение позволяет контролировать, насколько сильно модель меняется за шаг, и подводит нас к следующей части.
2. Функция обрезки (clip)
Заголовок раздела «2. Функция обрезки (clip)»Функция обрезки определяется так:
Она ограничивает отношение отрезком , чтобы избежать резких изменений, «безумных» обновлений и слишком далёкого ухода от старой политики. Иначе говоря, она ограничивает рост отношения вероятностей и тем самым поддерживает устойчивость: обновления не уводят новую модель слишком далеко от старой.
Пример (ε = 0.2)
Заголовок раздела «Пример (ε = 0.2)»Разберём два сценария, чтобы лучше понять функцию обрезки.
- Случай 1. Новая политика даёт конкретному ответу вероятность 0.9, а старая давала 0.5. Значит, новая политика
закрепляет этот ответ, повышая его вероятность, — но в контролируемых пределах: обрезка «связывает ей руки», чтобы
изменение не было резким.
- Отношение: (верхняя граница 1.2).
- Случай 2. Новая политика не в пользу ответа (меньшая вероятность, например 0.2): если ответ бесполезен, рост
был бы ошибкой, и модель штрафуется.
- Отношение: (нижняя граница 0.8).
Интерпретация
Заголовок раздела «Интерпретация»- Формула побуждает новую модель отдавать предпочтение ответам, которые старая модель недооценивала, если они улучшают результат.
- Если старая модель уже давала ответу высокую вероятность, новая может ещё закрепить его, но только в контролируемых пределах (например, при — от 0.8 до 1.2).
- Если старая модель переоценивала плохой ответ, новую модель отучают сохранять для него высокую вероятность.
- Таким образом, благодаря отношению вероятностей обновления политики пропорциональны преимуществу , но сдерживаются, чтобы не было резких изменений.
Обрезка защищает от резких изменений, но нужна ещё одна страховка, чтобы модель не уходила слишком далеко от исходного поведения.
3. KL-дивергенция
Заголовок раздела «3. KL-дивергенция»Слагаемое с KL-дивергенцией:
В слагаемом KL — это по сути выход модели до обновления, per_token_logps, а — выход
новой модели, new_per_token_logps. Теоретически KL-дивергенцию минимизируют, чтобы при оптимизации модель не уходила
слишком далеко от исходного поведения. Это помогает найти баланс между улучшением по сигналу награды и сохранением
связности. В этом контексте минимизация KL снижает риск, что модель начнёт генерировать бессмысленный текст или, в
случае математических рассуждений, совсем неверные ответы.
Интерпретация
Заголовок раздела «Интерпретация»- Штраф KL удерживает ответы модели близко к исходному распределению и не даёт им резко сместиться.
- Вместо того чтобы сползать к совершенно бессмысленным ответам, модель уточняет своё понимание, сохраняя при этом возможность исследовать.
Математическое определение
Заголовок раздела «Математическое определение»Для интересующихся математическими деталями — формальное определение.
Напомним, что KL-расстояние определяется так:
В RLHF два интересующих распределения — это обычно распределение новой версии модели и распределение эталонной (reference) политики .
Роль параметра β
Заголовок раздела «Роль параметра β»Коэффициент задаёт, насколько строго соблюдается ограничение KL:
- Большой β (сильный штраф KL)
- Сильнее ограничены обновления политики: модель остаётся близко к эталонному распределению.
- Адаптация может замедлиться: модели труднее найти лучшие ответы.
- Маленький β (слабый штраф KL)
- Больше свободы в обновлении политики: модель может дальше уходить от эталона.
- Быстрее адаптация, но выше риск неустойчивости: модель может научиться «взламывать» награду (reward hacking).
- Риск переоптимизации: если модель награды несовершенна, политика может начать генерировать бессмыслицу.
- В оригинальной статье DeepSeekMath .
Поняв части GRPO, посмотрим, как они работают вместе на полном примере.
Разобранный пример GRPO
Заголовок раздела «Разобранный пример GRPO»Чтобы закрепить понимание, пройдём полный пример от начала до конца.
Шаг 1. Групповая выборка
Заголовок раздела «Шаг 1. Групповая выборка»Сначала генерируем несколько ответов модели.
Генерируем ответов: 4 из них верные (, награда 1) и 4 неверные (награда 0). Получаем:
Шаг 2. Расчёт преимущества
Заголовок раздела «Шаг 2. Расчёт преимущества»Затем считаем преимущества, чтобы понять, какие ответы лучше среднего:
| Статистика | Значение |
|---|---|
| Среднее по группе | |
| Стандартное отклонение | |
| Преимущество верного ответа | |
| Преимущество неверного ответа |
Шаг 3. Обновление политики
Заголовок раздела «Шаг 3. Обновление политики»Наконец, обновляем модель, закрепляя верные ответы:
- Пусть старая политика () давала верному ответу вероятность , а новая повысила её до . Тогда:
- Когда целевая функция перевзвешена, модель стремится чаще генерировать верный ответ, а KL-дивергенция ограничивает уход от эталонной политики.
Теория понятна — посмотрим, как реализовать GRPO в коде.
Пример реализации
Заголовок раздела «Пример реализации»Соберём всё в практический пример. Код ниже показывает, как реализовать GRPO на PyTorch.
1. Загружаем модель и генерируем ответы
Заголовок раздела «1. Загружаем модель и генерируем ответы»Сначала загрузим модель и сгенерируем несколько ответов на вопрос:
import torchimport torch.nn.functional as Ffrom transformers import AutoModelForCausalLM, AutoTokenizer
# Загружаем модель и токенизаторmodel_name = "Qwen/Qwen2-Math-1.5B"model = AutoModelForCausalLM.from_pretrained(model_name)tokenizer = AutoTokenizer.from_pretrained(model_name)model.eval()
# Переносим модель на GPU, если он естьdevice = torch.device("cuda" if torch.cuda.is_available() else "cpu")model.to(device)
# Входной промптprompt = "Solve y = 2x + 1 for x = 2, y = " # Верный ответ: 5inputs = tokenizer(prompt, return_tensors="pt", padding=True)input_ids = inputs["input_ids"].to(device) # Форма: (1, prompt_len)attention_mask = inputs["attention_mask"].to(device)
# Шаг 1: генерируем 8 ответов (B = 2 группы, G = 4 ответа в группе)batch_size, num_generations = 2, 4outputs = model.generate( input_ids=input_ids, # Форма: (1, prompt_len) attention_mask=attention_mask, max_new_tokens=1, # seq_len = 1 (по одному токену на ответ) num_return_sequences=batch_size * num_generations, # всего 8 ответов do_sample=True, top_k=10, temperature=0.7, pad_token_id=tokenizer.eos_token_id, return_dict_in_generate=True, output_scores=True,)Начальная генерация (до всяких шагов обучения) выдаст что-то вроде:
Output 1: 5.0Output 2: 6.0Output 3: 7.0Output 4: 5.0Output 5: 10.0Output 6: 2.0Output 7: 5.0Output 8: 5.02. Считаем награды
Заголовок раздела «2. Считаем награды»Теперь определим, какие ответы верны, и назначим награды.
В GRPO на один и тот же промпт генерируется несколько ответов. Например, для промптов "Solve y = 2x + 1 for x = 2, y = "
и "Solve y = 2x + 1 for x = 4, y = " у нас две группы ответов:
[5, 6, 7, 5]и[10, 2, 9, 9], а верные ответы — 5 и 9.
На практике награды выдаёт функция награды на правилах, которая проверяет правильность ответа, или более сложная нейросетевая модель, обученная оценивать правильность, или их сочетание. Для простоты пусть награда за ответ равна 1, если он верен, и 0, если нет. Тогда:
reward_1 = [1, 0, 0, 1]reward_2 = [0, 0, 1, 1]Дальше считаем среднее и стандартное отклонение наград по группам:
# Форма: (B * G,) = (8,), потому что 2 группы по 4 ответа мы вытягиваем в один векторrewards = torch.tensor([1, 0, 0, 1, 0, 0, 1, 1], dtype=torch.float32)num_generations = 4
# Группируем награды: форма (B, G) = (2, 4)rewards_grouped = rewards.view(-1, num_generations)
# Среднее по группе: форма (B,) = (2,)mean_grouped_rewards = rewards_grouped.mean(dim=1)
# Стандартное отклонение по группе: форма (B,) = (2,)std_grouped_rewards = rewards_grouped.std(dim=1)
# Растягиваем до формы наград и нормируем: форма (B * G,) = (8,)# зачем растягивать? чтобы посчитать преимущество каждого ответа внутри его группыmean_grouped_rewards = mean_grouped_rewards.repeat_interleave(num_generations, dim=0)std_grouped_rewards = std_grouped_rewards.repeat_interleave(num_generations, dim=0)Вывод:
Grouped Rewards: tensor([[1., 0., 0., 1.], [0., 0., 1., 1.]])Mean per group: tensor([0.5000, 0.5000])Std per group: tensor([0.5774, 0.5774])Broadcasted Mean: tensor([0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000])Broadcasted Std: tensor([0.5774, 0.5774, 0.5774, 0.5774, 0.5774, 0.5774, 0.5774, 0.5774])Теперь считаем преимущество каждого ответа:
# Преимущества: форма (B * G,) = (8,)advantages = (rewards - mean_grouped_rewards) / (std_grouped_rewards + 1e-8)Вывод:
Advantages: tensor([ 0.8659, -0.8660, -0.8660, 0.8659, -0.8660, -0.8660, 0.8659, 0.8659])Это следует из формулы преимущества выше:
Для reward_1 = [1, 0, 0, 1]:(1 - 0.5) / 0.5774 ≈ 0.8659(0 - 0.5) / 0.5774 ≈ -0.8660Для reward_2 = [0, 0, 1, 1] — та же картина.Но форма здесь (B*G,) = (8,), а на практике она должна совпадать с формой логитов, верно? Поэтому добавим тензору
преимуществ измерение и получим форму (B*G, 1) = (8, 1):
# Форма (B * G, 1) = (8, 1), чтобы совпасть с формой логитовadvantages = advantages.unsqueeze(1)Вывод:
Advantages: tensor([[ 0.8659], [-0.8660], [-0.8660], [ 0.8659], [-0.8660], [-0.8660], [ 0.8659], [ 0.8659]])Теперь всё готово — переходим к обновлению модели-политики по преимуществам.
3. Обновляем политику
Заголовок раздела «3. Обновляем политику»Наконец, используем преимущества для обновления модели:
# Отношение вероятностей новой и старой политикиratio = torch.exp( new_per_token_logps - per_token_logps) # Форма: (B*G, seq_len); seq_len — длина ответа, то есть число сгенерированных токенов; для простоты здесь 1 → (8, 1)Заметим, что per_token_logps можно получить, прогнав сгенерированные ответы через модель, взяв логиты и применив к
ним softmax, чтобы получить вероятности: F.softmax(logits, dim=-1).
# Функция обрезкиeps = self.cliprange # например, 0.2pg_losses1 = -advantages * ratio # Форма: (B*G, seq_len) → (8, 1)pg_losses2 = -advantages * torch.clamp( ratio, 1.0 - eps, 1.0 + eps) # Форма: (B*G, seq_len) → (8, 1)pg_loss_max = torch.max(pg_losses1, pg_losses2) # Форма: (B*G, seq_len) → (8, 1)
# Теперь добавляем штраф KL. Форма: (B*G, seq_len) → (8, 1)per_token_loss = pg_loss_max + self.beta * per_token_klper_token_kl можно посчитать так:
# Форма: (B*G, seq_len) → (8, 1)per_token_kl = F.kl_div( F.log_softmax(new_per_token_logps, dim=-1), F.softmax(per_token_logps, dim=-1), reduction="none",).sum(dim=-1, keepdim=True)Полный пример есть здесь (примечание переводчика: на момент перевода файла по этой ссылке в репозитории нет). GRPO реализован и замечательной командой TRL — подробности в коде TRL/GRPO_trainer.
Исправленный пример от переводчика
Заголовок раздела «Исправленный пример от переводчика»Этот скрипт целиком считает loss GRPO на игрушечных числах и запускается на CPU за доли секунды. Логика — как в
GRPOTrainer из TRL.
import torch
torch.manual_seed(0)G = 4 # размер группыrewards = torch.tensor([1., 0., 0., 1., 0., 0., 1., 1.]) # 2 группы по 4 ответа
# 1) Групповые преимуществаgrouped = rewards.view(-1, G) # (2, 4)mean = grouped.mean(dim=1).repeat_interleave(G) # (8,)std = grouped.std(dim=1).repeat_interleave(G) # (8,) выборочное std, как в torch по умолчаниюadv = ((rewards - mean) / (std + 1e-4)).unsqueeze(1) # (8, 1)print("advantages:", adv.squeeze().round(decimals=4))
# 2) Лог-вероятности сгенерированных токенов (B*G, seq_len) — здесь игрушечные числаseq_len = 3old_logps = torch.log(torch.full((8, seq_len), 0.5)) # π_old: модель при генерацииref_logps = torch.log(torch.full((8, seq_len), 0.5)) # π_ref: замороженная исходная модельnew_logps = old_logps + 0.3 * adv # π_θ: после шага верные ответы стали вероятнее
# 3) Отношение вероятностей и клиппинг (PPO-стиль)eps, beta = 0.2, 0.04ratio = torch.exp(new_logps - old_logps)loss1 = -adv * ratioloss2 = -adv * torch.clamp(ratio, 1 - eps, 1 + eps)pg_loss = torch.max(loss1, loss2)
# 4) KL-оценка k3 (как в TRL и DeepSeekMath): exp(ref - new) - (ref - new) - 1d = ref_logps - new_logpsper_token_kl = torch.exp(d) - d - 1
# 5) Итоговый loss: среднее по токенам ответа, затем по ответамper_token_loss = pg_loss + beta * per_token_klloss = per_token_loss.mean(dim=1).mean()print("ratio (верный, неверный):", round(ratio[0, 0].item(), 4), round(ratio[1, 0].item(), 4))print("k3 KL (верный):", round(per_token_kl[0, 0].item(), 4))print("loss:", round(loss.item(), 4))Вывод:
advantages: tensor([ 0.8659, -0.8659, -0.8659, 0.8659, -0.8659, -0.8659, 0.8659, 0.8659])ratio (верный, неверный): 1.2966 0.7712k3 KL (верный): 0.031loss: -0.1718Как читать результат: у верного ответа отношение 1.30 обрезается до 1.2, вклад ; у неверного отношение 0.77 «поднимается» до 0.8, вклад . Среднее , плюс крошечный KL-штраф — итого .
Итоги и следующие шаги
Заголовок раздела «Итоги и следующие шаги»Поздравляем! Вы разобрались в групповой относительной оптимизации политики (GRPO). Вспомним, что мы прошли:
- GRPO сравнивает несколько ответов внутри группы, чтобы понять, какие лучше, — без отдельной модели ценности.
- Расчёт преимущества стандартизирует награды и показывает, какие ответы выше или ниже среднего.
- Обновление политики использует обрезанную целевую функцию со штрафом KL, чтобы обучение было устойчивым.
Этот подход особенно силён в задачах математических рассуждений, где правильность можно проверить объективно. GRPO позволяет обучать эффективнее традиционных подходов RLHF, которым нужна отдельная модель-критик.
Продолжая изучать GRPO, поэкспериментируйте с размером группы, функциями награды и коэффициентом штрафа KL и посмотрите, как они влияют на качество модели.
Удачного обучения! 🚀
Что сделать руками (от переводчика)
Заголовок раздела «Что сделать руками (от переводчика)»- Посчитайте на бумаге преимущества для группы наград (выборочное std). Проверьте себя кодом из примера. (Ответ: у верных и у неверного.)
- В исправленном примере поставьте
beta = 0иbeta = 1. Как меняется loss? Почему приbeta = 0эталонная модель вообще не нужна? - Замените в примере сдвиг
0.3 * advна0.05 * adv. Срабатывает ли теперь обрезка? (Подсказка: .) - Перейдите к CodeExercise в уроке 6.4.1 — там то же самое на NumPy с автотестами.
Словарик
Заголовок раздела «Словарик»| English | Русский |
|---|---|
| group sampling | групповая выборка |
| group relative advantage | групповое относительное преимущество |
| value model, critic | модель ценности, критик |
| probability ratio | отношение вероятностей |
| clipping, clip range ε | обрезка, диапазон обрезки ε |
| reference policy | эталонная (референсная) политика |
| old policy | старая политика (снимок на момент генерации) |
| KL estimator k3 | KL-оценка k3 |
| reward hacking | «взлом» награды |
| per-token log-probabilities | лог-вероятности токенов |