Основы RL для языковых моделей
LLM как агент, который учится на награде: политика, градиент политики и REINFORCE, baseline и advantage, KL-штраф к референсной модели — фундамент RLHF и GRPO.
Зачем это нужно
Заголовок раздела «Зачем это нужно»После SFTобучение с учителем на инструкциях (SFT)EN supervised fine-tuning (SFT) модель умеет подражать образцовым ответам. Но для многих задач написать идеальный ответ трудно, а оценить готовый — легко. Решила ли модель уравнение? Проходят ли тесты? Какой из двух ответов вежливее? Человеку проще сказать «этот лучше», чем написать лучший вариант самому.
Обучение с подкреплениемобучение с подкреплениемEN reinforcement learning (RL) (reinforcement learning, RL) — способ учиться именно так: модель сама генерирует ответ, получает за него число-оценку и подстраивается, чтобы оценки росли. На этом стоят RLHF в ChatGPT (следующий урок), GRPO в DeepSeek-R1 (модуль 6.4) и почти весь современный пост-трейнингпост-трейнинг, дообучение после претрейнаEN post-training.
Классическую теорию вы прошли в модуле 6.0: MDP и функции ценности (6.0.1), TD и Q-learning (6.0.2), градиент политики, actor-critic, GAE и PPO (6.0.3). Теперь применим её к языковой модели. Хорошая новость: для LLM из всей теории чаще всего нужна одна формула — градиент политики — и два приёма к ней: baseline и KL-штраф. Посмотрим, как они выглядят, когда политика — это LLM.
Словарь RL на примере LLM
Заголовок раздела «Словарь RL на примере LLM»АгентагентEN agentВ китайском встречается и «дайли» («посредник»), но в AI-контексте стандарт — «чжинэнти» (буквально «интеллектуальное тело»)., средасреда (в RL и для агентов)EN environmentТо, с чем взаимодействует агент — инструменты, песочница, симулятор пользователя — вместе с правилами выдачи награды., состояние , действие , награданаграда, вознаграждениеEN rewardЧисло, которым среда оценивает действие или весь ответ. У LLM обычно одна награда в конце ответа. , политикаполитика (стратегия агента)EN policy и эпизод (траекториятраекторияEN trajectoryПолная запись эпизода агента — все мысли, вызовы инструментов и наблюдения. Единица данных для оценки и agentic RL.) — из урока 6.0.1. Сопоставим их для игры и для LLM:
| Понятие | Классический RL (игра Atari) | Языковая модель |
|---|---|---|
| Агент | игрок-нейросеть | LLM |
| Состояние | кадр экрана | промпт + уже сгенерированные токены |
| Действие | кнопка джойстика (18 вариантов) | следующий токен (у Qwen ~152 тыс. вариантов) |
| Политика | вероятности кнопок | softmax над словарём — это и есть LLM |
| Переход | физика игры | детерминированный: дописать токен в контекст |
| Награда | очки в игре | оценка ответа: reward model, проверка ответа, тесты |
| Эпизод | одна партия | один ответ до токена <eos> |
Главное наблюдение: LLM уже является политикой. Вероятность всего ответа на промпт — произведение вероятностей токенов, а её логарифм — сумма:
Это та же сумма лог-вероятностей, что и в loss SFT. Разница только в том, чей ответ мы подставляем: в SFT — образцовый от человека, в RL — сгенерированный самой моделью.
Особенность LLM — разреженная награда (sparse reward). Ответ из 500 токенов — это 500 действий, а оценка одна, в самом конце. Поэтому ответ часто рассматривают как одно большое действие: промпт → ответ целиком → награда. Это контекстный бандитконтекстный бандитEN contextual banditЗадача RL из одного шага, но в разных ситуациях (контекстах). Ответ LLM целиком на промпт — пример такой задачи. из урока 6.0.1: один шаг, но в разных ситуациях (промптах).
flowchart LR P["Промпт x"] --> L["LLM = политика π_θ"] L -->|"сэмплируем ответ y"| E["Среда: reward model, тесты, проверка ответа"] E -->|"награда r(x, y)"| U["Обновление θ: чаще генерировать то, что дало высокую r"] U --> L
Цель — максимизировать среднюю награду:
Здесь — набор промптов: берём промпт, модель отвечает, усредняем награду по промптам и ответам.
Как и в уроке 6.0.3, обычный backprop здесь бессилен: ответ получен сэмплированием, а награда — чёрный ящик («тесты прошли — 1, нет — 0»).
Градиент политики для LLM
Заголовок раздела «Градиент политики для LLM»Градиент политики вы вывели в уроке 6.0.3 для целых траекторий. В варианте «ответ — одно действие» вывод занимает три строки: матожидание — сумма по ответам, награда от не зависит, а :
Результат — снова матожидание, его оценивают сэмплами ответов. Это градиент политикиградиент политикиEN policy gradient∇J = E[r · ∇ log π(y|x)] — основа REINFORCE, PPO и GRPO. для LLM.
Как это читать. — направление, в котором растёт вероятность ответа . Ровно этот градиент считает SFT. Значит, policy gradient — это SFT на собственных ответах модели, взвешенный наградой:
- — делаем «шаг SFT» к этому ответу, его вероятность растёт;
- — шаг в обратную сторону, вероятность падает;
- — ничего не происходит.
В коде это одна строка: вместо loss = -logp.sum() в SFT пишем loss = -(reward * logp.sum()).
Пример с числами
Заголовок раздела «Пример с числами»Промпт «Сколько будет 7·8?», три возможных ответа: «56», «54», «не знаю». Политика — softmax от логитов , все вероятности по . Градиент логарифма вероятности по логитам вы уже встречали в уроке 2.2.2: там градиент cross-entropy по логитам равнялся , а — это cross-entropy с правильным ответом , взятая со знаком минус:
Здесь — индикатор: 1, если , и 0 иначе. То есть производная по «своему» логиту равна , а по каждому чужому — .
Сэмплировали «56», награда . Градиент: . Шаг градиентного подъёма с : , новые вероятности . Модель стала чаще отвечать «56», хотя ни разу не видела образцовый ответ — только оценку своего.
REINFORCE
Заголовок раздела «REINFORCE»Алгоритм REINFORCEREINFORCEEN REINFORCEАлгоритм Уильямса (1992) — оценка градиента политики по сэмплам эпизодов (метод Монте-Карло). (Уильямс, 1992) — прямое применение формулы:
- Сгенерировать текущей политикой ответов .
- Посчитать награды .
- Оценить градиент: .
- Шаг: . Вернуться к пункту 1.
Алгоритм on-policyon-policy (обучение на данных текущей политики)EN on-policyПротивоположность — off-policy, обучение на данных другой политики.: формула верна, только если ответы получены текущей политикой (урок 6.0.3). После шага старые ответы «протухают», и для следующего шага нужно генерировать заново. У LLM генерация — самая медленная часть, поэтому RL намного дороже SFT. PPO умеет делать несколько шагов на одном наборе ответов — как это выглядит в RLHF, разберём в следующем уроке.
Проблема дисперсии и baseline
Заголовок раздела «Проблема дисперсии и baseline»Допустим, reward model ставит всем ответам оценки от 5 до 7: у «56» — 7, у «54» — 5, у «не знаю» — 6. Все награды положительны, и любой сэмплированный ответ, даже ошибочный, «подталкивается вверх». Лучший ответ побеждает только в среднем, по многим сэмплам. Оценка градиента прыгает от шага к шагу — у неё огромная дисперсия.
Лекарство — вычесть из награды базовую линию (baselineбазовая линия (baseline)EN baselineЧисло, которое вычитают из награды; не смещает градиент политики, но снижает его дисперсию.) , которая не зависит от ответа:
Матожидание градиента от этого не меняется (, вывод — в уроке 6.0.3), а шум падает — посмотрим, насколько, на нашем примере.
Посчитаем точно для нашего примера (, награды ):
| Baseline | Матожидание градиента | Дисперсия оценки (сумма по компонентам) |
|---|---|---|
| 0 | 24.2 | |
| 6 (средняя награда) | 0.22 |
Направление то же, а дисперсия упала в 109 раз (точные значения — и ). Это как разница между «все ответы хорошие, но некоторые чуть лучше» и «этот лучше среднего, а этот хуже».
Разность называют преимуществом (advantageпреимущество (advantage)EN advantageA = r − b или A(s, a) = Q(s, a) − V(s): насколько действие лучше среднего в этой ситуации.): насколько ответ лучше, чем «обычно» в этой ситуации. Строго — функции ценности из урока 6.0.1. В нашем примере с одним шагом — это просто награда ответа, а — средняя награда по политике. Знак решает, поднимать вероятность ответа или опускать.
Чем брать baseline — главное, чем отличаются RL-алгоритмы для LLM:
| Алгоритм | Baseline | Где встретится |
|---|---|---|
| REINFORCE (наш вариант) | среднее всех прошлых наград | задание этого урока |
| PPO | обученная value-модель (критик) | уроки 6.0.3 и 6.3.2 |
| RLOO | среднее наград других ответов на тот же промпт | статья Cohere «Back to Basics» (2024) |
| GRPO | среднее по группе ответов на промпт, ещё и делённое на std | DeepSeekMath, R1 (модуль 6.4) |
KL-штраф к референсной модели
Заголовок раздела «KL-штраф к референсной модели»RL максимизирует награду — и ничего больше. Если не ограничить модель, она уйдёт далеко от исходной:
- Взлом награды. Reward model — тоже нейросеть с «дырами». Политика найдёт ответы, которые RM оценивает высоко, хотя они плохие: длинная вода, лесть, повторы. Подробно — в следующем уроке.
- Потеря разнообразия. Политика схлопывается в один «самый выгодный» ответ на всё.
- Порча языка и забывание того, что модель умела после претрейна и SFT.
Решение — держать модель рядом с замороженной копией себя до RL. Эту копию называют референсной модельюреференсная модельEN reference modelЗамороженная копия модели до RL (обычно SFT-модель), к которой обучаемую политику «привязывает» KL-штраф. , обычно это SFT-модель. К цели добавляют KL-штрафKL-штрафEN KL penaltyСлагаемое −β·KL(π ‖ π_ref) в цели RL; не даёт политике уйти далеко от референсной модели.:
KL-дивергенциюKL-дивергенция (дивергенция Кульбака — Лейблера)EN Kullback–Leibler divergenceKL(p ‖ q) = Σ p log(p / q) = H(p, q) − H(p) ≥ 0. Несимметрична, поэтому не настоящее расстояние. вы считали в уроке 1.3.3. Для ответов-последовательностей её нельзя посчитать точно (вариантов астрономически много), поэтому оценивают по сэмплу — суммой по токенам сгенерированного ответа:
Пример: модель после RL даёт токену вероятность 0.9, а референсная — 0.3. Вклад в KL: . При (на практике берут значения порядка 0.01–0.1) штраф за этот токен — 0.055. Мелочь, но за сотни «необычных» токенов набегает заметно. Учтите: у отдельного токена или ответа такая оценка бывает и отрицательной (если дала токену меньшую вероятность, чем референс), но в среднем по сэмплам она равна KL и неотрицательна.
К чему приводит KL-штраф
Заголовок раздела «К чему приводит KL-штраф»У задачи «максимум награды минус KL» есть точное решение:
где — нормировка. Это перевзвешенная референсная модель: каждый ответ умножается на .
- : побеждает ответ с максимальной наградой, политика схлопывается в него.
- : , награда игнорируется.
- Между ними — компромисс, который регулирует один параметр.
Запомните эту формулу: в уроке про DPO из неё за три строки получится целый алгоритм.
Практика
Заголовок раздела «Практика»Задание: REINFORCE на многоруком бандите
Заголовок раздела «Задание: REINFORCE на многоруком бандите»Многорукий бандитмногорукий бандитEN multi-armed banditПростейшая задача RL — K «ручек» с неизвестными средними наградами, один шаг на эпизод. — простейшая задача RL: «ручек» с неизвестными средними наградами, один шаг на эпизод. Это ровно «LLM с одним промптом и вариантами ответа». Реализуйте градиент лог-softmax, шаг REINFORCE, точное матожидание градиента и цикл обучения с baseline.
import numpy as np
def softmax(z):
"""Устойчивый softmax (из урока 1.3) — уже готов."""
z = np.asarray(z, dtype=float)
e = np.exp(z - z.max())
return e / e.sum()
def grad_log_prob(logits, action):
"""Градиент log softmax(z)[action] по вектору логитов z.
Ответ — np.ndarray той же длины, что logits: onehot(action) - softmax(z).
"""
# TODO: ваш код
...
def reinforce_update(logits, action, reward, baseline, lr):
"""Один шаг REINFORCE (градиентный ПОДЪЁМ — награду мы максимизируем).
Вернуть НОВЫЙ массив логитов: z + lr * (reward - baseline) * grad_log_prob(z, action).
Входной массив не изменять.
"""
# TODO: ваш код
...
def expected_gradient(logits, rewards, baseline):
"""Точное матожидание оценки градиента REINFORCE для бандита
с детерминированными наградами rewards[a]:
E[g] = sum_a p_a * (rewards[a] - baseline) * grad_log_prob(z, a)
"""
# TODO: ваш код
...
def train_bandit(arm_means, steps=1000, lr=0.1, use_baseline=True, seed=0):
"""Обучить политику softmax(z) на многоруком бандите алгоритмом REINFORCE.
- старт: z = нули;
- rng = np.random.default_rng(seed);
- на каждом шаге: p = softmax(z); действие a ~ p (rng.choice);
награда r ~ Normal(arm_means[a], 1) (rng.normal);
- baseline = среднее всех ПРОШЛЫХ наград (на первом шаге 0);
если use_baseline=False — baseline всегда 0;
- z = reinforce_update(z, a, r, baseline, lr).
Вернуть итоговые вероятности softmax(z).
"""
# TODO: ваш код
...
# Проверьте себя кнопкой «Запустить»:
print(grad_log_prob([0.0, 0.0, 0.0], 1)) # ≈ [-0.333, 0.667, -0.333]
print(train_bandit([10.0, 10.5, 11.0])) # лучшая ручка — последняя
print(train_bandit([10.0, 10.5, 11.0], use_baseline=False)) # а что без baseline?
Нажмите «Запустить» после решения: с baseline политика находит лучшую ручку (), а без него на наградах около 10 «залипает» на случайной ручке. Каждая награда положительна, и шаг — до 0.7 по логиту — толкает вверх любое выпавшее действие, так что выигрывает не лучшая ручка, а та, которой повезло в начале. У эталона получается — уверенно и неправильно.
Локально: REINFORCE + KL на «игрушечной LLM»
Заголовок раздела «Локально: REINFORCE + KL на «игрушечной LLM»»Модель — биграммная таблица логитов над словарём из шести «слов». Она генерирует ответы из 4 токенов, награда — число вежливых слов («спасибо» или «thanks»). Сравним обучение без KL-штрафа и с . Baseline — средняя награда по батчу.
import torch
vocab = ["да", "нет", "ок", "привет", "спасибо", "thanks"]V, L = len(vocab), 4 # словарь и длина «ответа»polite = torch.tensor([0, 0, 0, 0, 1, 1.0]) # награда за каждое «спасибо»/«thanks»
def train(beta, steps=300, batch=64, lr=0.5): torch.manual_seed(0) # «Языковая модель»: биграммная таблица логитов [пред. токен → след. токен] logits = torch.zeros(V + 1, V, requires_grad=True) # +1 строка для <bos> ref = logits.detach().clone() # замороженная копия = π_ref opt = torch.optim.SGD([logits], lr=lr) for step in range(steps + 1): prev = torch.full((batch,), V) # все начинают с <bos> logp, logp_ref, tokens = [], [], [] for t in range(L): # авторегрессионная генерация # Categorical — категориальное распределение (урок 1.3.1) из логитов: строка таблицы для предыдущего токена dist = torch.distributions.Categorical(logits=logits[prev]) tok = dist.sample() # сэмплируем следующий токен для каждого из 64 ответов logp.append(dist.log_prob(tok)) # log π_θ(выбранного токена) — через него пойдёт градиент # log π_ref того же токена: gather достаёт из каждой строки элемент с номером tok logp_ref.append(torch.log_softmax(ref[prev], -1).gather(1, tok[:, None])[:, 0]) tokens.append(tok) prev = tok logp, logp_ref = torch.stack(logp, 1), torch.stack(logp_ref, 1) # [batch, L] tokens = torch.stack(tokens, 1) R = polite[tokens].sum(1) # награда за весь ответ kl = (logp - logp_ref).sum(1).detach() # оценка KL по сэмплу shaped = R - beta * kl # награда с KL-штрафом adv = shaped - shaped.mean() # baseline = среднее по батчу loss = -(adv * logp.sum(1)).mean() # REINFORCE opt.zero_grad() loss.backward() opt.step() if step % 100 == 0: print(f"beta={beta:<4} шаг {step:3d}: награда {R.mean():.2f} из {L}, KL {kl.mean():.2f}") return logits
for beta in [0.0, 0.5]: lg = train(beta) p = torch.softmax(lg[V], -1) # распределение первого токена после <bos> print(" p(первый токен):", {w: round(x, 2) for w, x in zip(vocab, p.tolist())})Вывод (PyTorch 2.x на CPU; в других версиях числа могут немного отличаться):
beta=0.0 шаг 0: награда 1.39 из 4, KL 0.00beta=0.0 шаг 100: награда 3.89 из 4, KL 4.22beta=0.0 шаг 200: награда 3.98 из 4, KL 5.36beta=0.0 шаг 300: награда 3.95 из 4, KL 5.44 p(первый токен): {'да': 0.0, 'нет': 0.0, 'ок': 0.0, 'привет': 0.0, 'спасибо': 0.09, 'thanks': 0.91}beta=0.5 шаг 0: награда 1.39 из 4, KL 0.00beta=0.5 шаг 100: награда 3.14 из 4, KL 1.70beta=0.5 шаг 200: награда 3.23 из 4, KL 1.89beta=0.5 шаг 300: награда 3.22 из 4, KL 1.87 p(первый токен): {'да': 0.05, 'нет': 0.05, 'ок': 0.05, 'привет': 0.05, 'спасибо': 0.39, 'thanks': 0.39}Что видно:
- Без KL награда почти максимальная, но модель схлопнулась: 91% ответов начинаются с «thanks», хотя «спасибо» стоит столько же. Разнообразие потеряно, KL от исходной модели — 5.4.
- С награда ниже (3.2), зато KL втрое меньше, оба вежливых слова равноправны, остальные не исчезли.
- Сверим с формулой . Референс равномерный, а награда — сумма наград отдельных токенов, поэтому распадается на произведение множителей по токенам, и каждый токен оптимальной политики выбирается независимо, с весом у вежливого слова и у остальных: , а для остальных . Обучение пришло ровно туда: 0.39 и 0.05. Теория даёт и награду — близко к 3.22 в логе (это оценка по одному батчу из 64 ответов, она шумит).
Проверь себя
Заголовок раздела «Проверь себя»- LLM — уже готовая политика: состояние — контекст, действие — токен, награда — оценка ответа в конце.
- Градиент политики — это SFT на собственных ответах модели, взвешенный наградой. REINFORCE оценивает его сэмплами и требует свежей генерации на каждом шаге (on-policy).
- Baseline не меняет матожидание градиента, но резко снижает дисперсию; — advantage. Разные способы выбрать baseline дают REINFORCE, PPO, RLOO и GRPO.
- KL-штраф держит модель рядом с ; оптимум — .
Дальше: откуда берётся награда, если ответ нельзя проверить автоматически, — reward model на человеческих предпочтениях, модель Брэдли–Терри и PPO.