Перейти к содержимому
Ступень 66.0 Основы обучения с подкреплением

Actor-critic, GAE и PPO полностью

Градиент политики с выводом на пальцах, REINFORCE, baseline и преимущество, актор и критик, GAE с числовым примером, PPO целиком — clipped objective, value loss, энтропия — и как всё это ложится на языковую модель.

УРОКstage-6/6-0-rl-foundations/03-actor-critic-gae-ppo.mdx2 ч 30 минУрок 3 из 3
После урока вы сможете
  • Вывести градиент политики через «трюк с логарифмом» и объяснить reward-to-go, baseline и преимущество
  • Объяснить схему actor-critic и посчитать преимущества методом GAE(λ) руками и кодом
  • Собрать полный loss PPO — клиппированная цель, ошибка критика, энтропийный бонус — и объяснить роль каждого слагаемого
  • Прочитать формулу PPO в InstructGPT и DeepSeekMath и показать, что именно заменил в ней GRPO

В прошлых уроках агент сначала учил ценности (VV или QQ), а политику получал из них: «в каждом состоянии бери действие с наибольшим QQ». Для языковой модели этот путь неудобен. Действий — около 150 тысяч токенов, состояний — бесконечно много, а главное, после претрейна и SFT у нас уже есть хорошая политика: softmax над словарём. Естественно улучшать её напрямую — двигать параметры θ\theta так, чтобы средняя награда росла. Это методы градиента политикиградиент политикиEN policy gradient∇J = E[r · ∇ log π(y|x)] — основа REINFORCE, PPO и GRPO..

Урок собирает их по кирпичику: градиент политики → REINFORCE → baseline и преимущество → актор и критик → GAE → PPO. PPO — именно тот алгоритм, которым OpenAI обучала InstructGPT, а GRPO из DeepSeek-R1 — его упрощение. После урока вы сможете прочитать их формулы целиком и сказать, что означает каждый символ.

Политика πθ(a∣s)\pi_\theta(a \mid s) — нейросеть с параметрами θ\theta, которая выдаёт вероятности действий. Цель — средняя отдача J(θ)=Eτ∼πθ[R(τ)]J(\theta) = \mathbb{E}_{\tau \sim \pi_\theta}[R(\tau)], где τ=(s0,a0,r0,s1,a1,… )\tau = (s_0, a_0, r_0, s_1, a_1, \dots) — траекториятраекторияEN trajectoryПолная запись эпизода агента — все мысли, вызовы инструментов и наблюдения. Единица данных для оценки и agentic RL. эпизода, а R(τ)R(\tau) — её отдача.

Проблема. Обычный backprop здесь не работает: действия сэмплируются (дискретная операция), а награду выдаёт среда — чёрный ящик, у которого нет производной.

Трюк с логарифмом. Распишем матожидание как сумму по траекториям и продифференцируем (урок 1.2.3). От θ\theta зависят только вероятности:

∇θJ=∑τ∇θPθ(τ) R(τ)=∑τPθ(τ) ∇θlog⁡Pθ(τ) R(τ)=Eτ[∇θlog⁡Pθ(τ) R(τ)]\nabla_\theta J = \sum_\tau \nabla_\theta P_\theta(\tau)\, R(\tau) = \sum_\tau P_\theta(\tau)\, \nabla_\theta \log P_\theta(\tau)\, R(\tau) = \mathbb{E}_{\tau}\big[\nabla_\theta \log P_\theta(\tau)\, R(\tau)\big]

Во втором равенстве использовано ∇P=P ∇log⁡P\nabla P = P\, \nabla \log P — просто производная логарифма: ∇log⁡P=∇P/P\nabla \log P = \nabla P / P. Результат снова матожидание, а его можно оценить сэмплами — сыграть несколько эпизодов и усреднить.

Куда делась среда. Вероятность траектории — произведение вероятностей шагов: Pθ(τ)=p(s0)∏tπθ(at∣st) P(st+1∣st,at)P_\theta(\tau) = p(s_0) \prod_t \pi_\theta(a_t \mid s_t)\, P(s_{t+1} \mid s_t, a_t). Логарифм превращает произведение в сумму, а слагаемые среды log⁡p(s0)\log p(s_0) и log⁡P(st+1∣st,at)\log P(s_{t+1} \mid s_t, a_t) от θ\theta не зависят и при дифференцировании исчезают. Остаётся

∇θJ=Eτ[∑t∇θlog⁡πθ(at∣st)  R(τ)]\nabla_\theta J = \mathbb{E}_{\tau}\Big[\sum_t \nabla_\theta \log \pi_\theta(a_t \mid s_t)\; R(\tau)\Big]

Как читать: ∇θlog⁡πθ(at∣st)\nabla_\theta \log \pi_\theta(a_t \mid s_t) — направление, в котором растёт вероятность выбранного действия. Шагаем в этом направлении с весом R(τ)R(\tau): действия из удачных эпизодов становятся вероятнее, из неудачных — реже. Модель среды не нужна — только умение играть эпизоды и считать log⁡π\log \pi.

Пример с числами. Два действия, «влево» и «вправо», логиты z=[0,0]z = [0, 0], вероятности по 0.5. Градиент log⁡softmax\log \mathrm{softmax} по логитам — «one-hot минус вероятности» (вы считали его в уроке 2.2.2). Сыграли эпизод, выбрали «вправо», отдача R=2R = 2. Градиент [0,1]−[0.5,0.5]=[−0.5,0.5][0, 1] - [0.5, 0.5] = [-0.5, 0.5], шаг подъёма z←z+αR⋅[−0.5,0.5]z \leftarrow z + \alpha R \cdot [-0.5, 0.5] с α=0.1\alpha = 0.1 даёт z=[−0.1,0.1]z = [-0.1, 0.1] и π(вправо)=0.550\pi(\text{вправо}) = 0.550. Модель стала чаще выбирать «вправо», хотя никто не говорил ей, что это правильно.

Алгоритм REINFORCEREINFORCEEN REINFORCEАлгоритм Уильямса (1992) — оценка градиента политики по сэмплам эпизодов (метод Монте-Карло). (Уильямс, 1992) — прямое применение формулы: сыграть эпизоды текущей политикой, посчитать ∑t∇log⁡π(at∣st)⋅вес\sum_t \nabla \log \pi(a_t \mid s_t) \cdot \text{вес}, сделать шаг, повторить.

Первое улучшение — причинность. Действие на шаге tt не может повлиять на награды, полученные до него. Значит, вместо отдачи всего эпизода R(τ)R(\tau) каждое действие можно взвешивать только будущими наградами — отдачей GtG_t из урока 6.0.1. Её называют reward-to-go. Матожидание градиента не меняется, а шум из прошлых наград уходит:

∇θJ=E[∑t∇θlog⁡πθ(at∣st)  Gt]\nabla_\theta J = \mathbb{E}\Big[\sum_t \nabla_\theta \log \pi_\theta(a_t \mid s_t)\; G_t\Big]

Важное свойство REINFORCE: он on-policyon-policy (обучение на данных текущей политики)EN on-policyПротивоположность — off-policy, обучение на данных другой политики.. Формула верна только для эпизодов, сыгранных текущей политикой. Сделали шаг — старые эпизоды «протухли», нужно играть заново.

Второе улучшение лечит огромный разброс. Пусть «вправо» в среднем приносит 2, а «влево» — 1, и все отдачи положительны. Если в том же примере выпадет «влево» с отдачей 1, шаг даст z=[0.05,−0.05]z = [0.05, -0.05], и вероятность «влево» вырастет до 0.525, хотя это худшее действие. Лучшее победит только в среднем по многим эпизодам — обучение шумное и медленное.

Лекарство — вычесть из веса базовую линиюбазовая линия (baseline)EN baselineЧисло, которое вычитают из награды; не смещает градиент политики, но снижает его дисперсию. b(st)b(s_t), которая не зависит от действия:

∇θJ=E[∑t∇θlog⁡πθ(at∣st) (Gt−b(st))]\nabla_\theta J = \mathbb{E}\Big[\sum_t \nabla_\theta \log \pi_\theta(a_t \mid s_t)\, \big(G_t - b(s_t)\big)\Big]

Почему можно: Ea∼π[∇log⁡π(a∣s)]=∑aπ⋅∇π/π=∇∑aπ(a∣s)=∇1=0\mathbb{E}_{a \sim \pi}[\nabla \log \pi(a \mid s)] = \sum_a \pi \cdot \nabla \pi / \pi = \nabla \sum_a \pi(a \mid s) = \nabla 1 = 0. Вычитаемое в среднем даёт ноль, матожидание градиента не меняется, а разброс падает. Возьмём b=1.5b = 1.5 (среднюю отдачу). «Вправо» с G=2G = 2 получает вес +0.5+0.5, «влево» с G=1G = 1 — вес −0.5-0.5, и оба эпизода сдвигают логиты одинаково: z=[−0.025,0.025]z = [-0.025, 0.025], π(вправо)=0.512\pi(\text{вправо}) = 0.512. Теперь даже неудачный эпизод учит правильному.

Какой baseline лучший? Почти оптимален b(st)=V(st)b(s_t) = V(s_t) — средняя отдача из этого состояния. Тогда вес Gt−V(st)G_t - V(s_t) — это оценка преимуществапреимущество (advantage)EN advantageA = r − b или A(s, a) = Q(s, a) − V(s): насколько действие лучше среднего в этой ситуации. A(st,at)=Q(st,at)−V(st)A(s_t, a_t) = Q(s_t, a_t) - V(s_t) из урока 6.0.1: «насколько это действие оказалось лучше, чем обычно бывает в этом состоянии». Знак решает, поднимать вероятность или опускать. Но VV нам неизвестна — её нужно учить.

Схема актор-критикактор-критик (actor-critic)EN actor-criticДве сети — актор (политика) и критик (оценка V); критик даёт актору оценку преимущества. учит две модели одновременно:

  • Актор — политика πθ(a∣s)\pi_\theta(a \mid s). Учится градиентом политики, где вес действия — оценка преимущества A^t\hat A_t.
  • Критиккритик, value-модельEN critic / value modelСеть, предсказывающая ожидаемую награду из текущего состояния; в PPO служит baseline для каждого токена. — функция ценности Vψ(s)V_\psi(s). Учится регрессией: предсказывать отдачу из состояния, loss (Vψ(st)−R^t)2\big(V_\psi(s_t) - \hat R_t\big)^2, где R^t\hat R_t — цель для критика (отдача или её оценка).
flowchart LR
E["Среда"] -->|"состояния и награды"| C["Критик V_ψ(s)"]
E --> B["Батч траекторий"]
A["Актор: политика π_θ"] -->|"действия"| E
C -->|"V(s_t)"| G["Оценка преимущества (GAE)"]
B --> G
G -->|"Â_t: что было лучше обычного"| A
G -->|"цели R̂_t"| C
Actor-critic: критик оценивает ситуации, актор учится на преимуществах

Как получить A^t\hat A_t из критика? Вспомним TD-ошибку из урока 6.0.2:

δt=rt+γ V(st+1)−V(st)\delta_t = r_t + \gamma\, V(s_{t+1}) - V(s_t)

В среднем по случайности среды rt+γV(st+1)r_t + \gamma V(s_{t+1}) — это Q(st,at)Q(s_t, a_t), поэтому δt\delta_t — оценка преимущества по одному шагу: шума мало (одна награда), но она целиком доверяет критику. Можно взять больше настоящих наград — nn-шаговая оценка. Удобный факт: она равна сумме TD-ошибок, потому что промежуточные VV взаимно сокращаются:

A^t(2)=rt+γrt+1+γ2V(st+2)−V(st)=δt+γ δt+1\hat A_t^{(2)} = r_t + \gamma r_{t+1} + \gamma^2 V(s_{t+2}) - V(s_t) = \delta_t + \gamma\, \delta_{t+1}

При nn до конца эпизода получаем Gt−V(st)G_t - V(s_t) — Монте-Карло с baseline: без смещения, но с полным шумом.

Какое nn выбрать? GAEобобщённая оценка преимущества (GAE)EN generalized advantage estimation (GAE)Schulman и др., 2015. Смешивает оценки преимущества разной длины с коэффициентом λ. (generalized advantage estimation, Schulman et al., 2015) не выбирает, а смешивает все: nn-шаговая оценка берётся с весом (1−λ)λn−1(1-\lambda)\lambda^{n-1}. После перегруппировки слагаемых получается простая формула — экспоненциально затухающая сумма TD-ошибок:

A^tGAE=∑l=0∞(γλ)l δt+l⟺A^t=δt+γλ A^t+1\hat A_t^{\text{GAE}} = \sum_{l=0}^{\infty} (\gamma\lambda)^l\, \delta_{t+l} \qquad\Longleftrightarrow\qquad \hat A_t = \delta_t + \gamma\lambda\, \hat A_{t+1}

Как читать: преимущество действия — его собственный «сюрприз» δt\delta_t плюс сюрпризы следующих шагов, которые приписываются ему тем слабее, чем дальше они случились. λ∈[0,1]\lambda \in [0, 1] — ручка между двумя крайностями:

  • λ=0\lambda = 0: A^t=δt\hat A_t = \delta_t — TD, мало шума, но ошибки критика переходят в преимущество (смещение);
  • λ=1\lambda = 1: A^t=Gt−V(st)\hat A_t = G_t - V(s_t) — Монте-Карло, без смещения, но шумно.

Считают GAE одним проходом с конца траектории, как отдачу в уроке 6.0.1. Цели для критика — R^t=A^t+V(st)\hat R_t = \hat A_t + V(s_t).

Откуда берётся формула (вывод в две строки)

nn-шаговая оценка — сумма первых nn TD-ошибок: A^t(n)=∑l=0n−1γlδt+l\hat A_t^{(n)} = \sum_{l=0}^{n-1} \gamma^l \delta_{t+l}. Смешаем их с весами (1−λ)λn−1(1-\lambda)\lambda^{n-1} (в сумме они дают 1) и поменяем порядок суммирования. Ошибка δt+l\delta_{t+l} входит во все оценки с n>ln > l, а сумма их весов — геометрическая прогрессия:

∑n=1∞(1−λ)λn−1∑l=0n−1γlδt+l=∑l=0∞γlδt+l∑n=l+1∞(1−λ)λn−1=∑l=0∞γlδt+l λl\sum_{n=1}^{\infty} (1-\lambda)\lambda^{n-1} \sum_{l=0}^{n-1} \gamma^l \delta_{t+l} = \sum_{l=0}^{\infty} \gamma^l \delta_{t+l} \sum_{n=l+1}^{\infty} (1-\lambda)\lambda^{n-1} = \sum_{l=0}^{\infty} \gamma^l \delta_{t+l}\, \lambda^l

Это и есть ∑l(γλ)lδt+l\sum_l (\gamma\lambda)^l \delta_{t+l}. В примере ниже проверим численно: при λ=0.5\lambda = 0.5 смесь оценок 0.5 [A^(1)+0.5 A^(2)]+0.25 A^(3)0.5\,[\hat A^{(1)} + 0.5\,\hat A^{(2)}] + 0.25\,\hat A^{(3)} даёт то же 0.28250.2825, что и рекурсия.

Эпизод из трёх шагов: робот забрал посылку (r0=1r_0 = 1), проехал пустой участок (r1=0r_1 = 0), доставил (r2=2r_2 = 2) — конец. Критик ожидал V=[2.0, 1.5, 1.0]V = [2.0,\ 1.5,\ 1.0], после конца V=0V = 0, γ=0.9\gamma = 0.9. TD-ошибки:

δ0=1+0.9⋅1.5−2.0=0.35,δ1=0+0.9⋅1.0−1.5=−0.6,δ2=2+0−1.0=1.0\delta_0 = 1 + 0.9 \cdot 1.5 - 2.0 = 0.35, \quad \delta_1 = 0 + 0.9 \cdot 1.0 - 1.5 = -0.6, \quad \delta_2 = 2 + 0 - 1.0 = 1.0

На шаге 1 критик «разочаровался» (δ1<0\delta_1 < 0), но на шаге 2 всё кончилось лучше ожидаемого. Преимущества при разных λ\lambda (γλ\gamma\lambda — множитель в рекурсии):

λ\lambdaA^2\hat A_2A^1=δ1+γλA^2\hat A_1 = \delta_1 + \gamma\lambda \hat A_2A^0=δ0+γλA^1\hat A_0 = \delta_0 + \gamma\lambda \hat A_1Смысл
01.0−0.60.35только свой шаг: «пустой участок» — плохое действие
0.51.0−0.150.2825будущему верим наполовину
0.951.00.2550.568типичное значение в PPO
11.00.30.62Gt−V(st)G_t - V(s_t): G=[2.62, 1.8, 2]G = [2.62,\ 1.8,\ 2]

Проверка формулы про nn шагов: A^0(2)=1+0.9⋅0+0.81⋅1.0−2.0=−0.19\hat A_0^{(2)} = 1 + 0.9 \cdot 0 + 0.81 \cdot 1.0 - 2.0 = -0.19 и δ0+γδ1=0.35−0.54=−0.19\delta_0 + \gamma\delta_1 = 0.35 - 0.54 = -0.19 — совпало.

PPOпроксимальная оптимизация политики (PPO)EN proximal policy optimization (PPO)Schulman и др., 2017. Ограничивает шаг обновления «клиппингом» отношения вероятностей. (proximal policy optimization, Schulman et al., 2017) — actor-critic с GAE и тремя инженерными решениями.

Сыграть эпизоды — дорого (у LLM генерация — самая медленная часть). Хочется сделать по одному батчу несколько шагов оптимизации. Но после первого шага политика уже не та, что собирала данные: данные стали off-policy (урок 6.0.2). Поправка — выборка по значимостивыборка по значимости (importance sampling)EN importance samplingПеревзвешивание сэмплов старой политики отношением π_new / π_old — основа PPO.: каждый сэмпл взвешивают отношением вероятностей

ρt(θ)=πθ(at∣st)πold(at∣st),L(θ)=Et[ρt A^t]\rho_t(\theta) = \frac{\pi_\theta(a_t \mid s_t)}{\pi_{\text{old}}(a_t \mid s_t)}, \qquad L(\theta) = \mathbb{E}_t\big[\rho_t\, \hat A_t\big]

Если новая политика выбирает действие вдвое чаще старой (ρ=2\rho = 2), его вклад учитывают с весом 2 — и среднее по старым сэмплам совпадает со средним по новой политике. В момент, когда πθ=πold\pi_\theta = \pi_{\text{old}}, градиент ρtA^t\rho_t \hat A_t равен A^t∇log⁡πθ(at∣st)\hat A_t \nabla \log \pi_\theta(a_t \mid s_t) — ровно градиент политики с преимуществом.

Поправка честна, только пока политики близки: если они разошлись, новая политика попадала бы в другие состояния, а этого отношение ρ\rho не учитывает. Предшественник PPO, TRPOTRPO (оптимизация политики в доверительной области)EN trust region policy optimization (TRPO)Schulman и др., 2015 — шаг политики с ограничением на KL к старой политике; предшественник PPO., ограничивал шаг условием на KL-дивергенцию и решал сложную задачу оптимизации с ограничением. PPO делает проще — обрезает ρ\rho до отрезка [1−ε,1+ε][1 - \varepsilon, 1 + \varepsilon] (обычно ε=0.2\varepsilon = 0.2) и берёт пессимистичный вариант:

LCLIP(θ)=Et[min⁡(ρtA^t, clip(ρt,1−ε,1+ε) A^t)]L^{\text{CLIP}}(\theta) = \mathbb{E}_t\Big[\min\big(\rho_t \hat A_t,\ \mathrm{clip}(\rho_t, 1-\varepsilon, 1+\varepsilon)\, \hat A_t\big)\Big]

Пять случаев при ε=0.2\varepsilon = 0.2:

A^\hat Aρ\rhoρA^\rho \hat Aclip(ρ) A^\mathrm{clip}(\rho)\, \hat AminГрадиентСмысл
+11.11.11.11.1естьхорошее действие, поднимаем дальше
+11.51.51.21.20уже подняли на 50% — хватит
+10.50.50.80.5естьхорошее действие стало реже — исправляем
−10.7−0.7−0.8−0.80плохое уже опустили на 30% — хватит
−11.5−1.5−1.2−1.5естьплохое стало чаще — исправляем

Правило: PPO не даёт уйти слишком далеко в «выгодную» сторону, но всегда даёт исправить ошибку. Проверим кодом — посчитаем цель и её производную по log⁡π\log \pi (в неклиппированной ветке ∂(ρA^)/∂log⁡π=ρA^\partial(\rho \hat A)/\partial \log \pi = \rho \hat A):

import numpy as np
adv = np.array([1.0, 1.0, 1.0, -1.0, -1.0])
ratio = np.array([1.1, 1.5, 0.5, 0.7, 1.5]) # ρ = π_θ / π_old
eps = 0.2
unclipped = ratio * adv
clipped = np.clip(ratio, 1 - eps, 1 + eps) * adv
objective = np.minimum(unclipped, clipped)
grad = np.where(unclipped <= clipped, ratio * adv, 0.0) # градиент есть, только если выбрана ветка ρ·A
for a, r, o, g in zip(adv, ratio, objective, grad):
print(f"A={a:+.0f} ρ={r:.1f} цель={o:+.2f} d(цель)/d(log π)={g:+.2f}")
A=+1 ρ=1.1 цель=+1.10 d(цель)/d(log π)=+1.10
A=+1 ρ=1.5 цель=+1.20 d(цель)/d(log π)=+0.00
A=+1 ρ=0.5 цель=+0.50 d(цель)/d(log π)=+0.50
A=-1 ρ=0.7 цель=-0.80 d(цель)/d(log π)=+0.00
A=-1 ρ=1.5 цель=-1.50 d(цель)/d(log π)=-1.50

Актор и критик часто учат одним оптимизатором, поэтому loss для минимизации складывают из трёх частей:

L(θ,ψ)=−LCLIP(θ)+cv Et[(Vψ(st)−R^t)2]−ce Et[H(πθ(⋅∣st))]\mathcal{L}(\theta, \psi) = -L^{\text{CLIP}}(\theta) + c_v\, \mathbb{E}_t\big[(V_\psi(s_t) - \hat R_t)^2\big] - c_e\, \mathbb{E}_t\big[H(\pi_\theta(\cdot \mid s_t))\big]
  • Минус клиппированная цель — её мы максимизируем.
  • Ошибка критика с весом cvc_v (в статье для Atari cv=1c_v = 1, в популярных реализациях 0.5).
  • Энтропийный бонусэнтропийный бонусEN entropy bonusДобавка c·H[π(·|s)] к цели — награда за «неуверенность» политики, чтобы она не схлопнулась раньше времени и продолжала исследовать. с весом cec_e (обычно 0–0.01). Энтропияэнтропия (информационная)EN (Shannon) entropyH(p) = −Σ p log p — среднее «удивление», мера неопределённости распределения. Максимальна у равномерного распределения. из урока 1.3.3 велика, когда политика не уверена. Награда за неё не даёт политике слишком рано схлопнуться в одно действие и перестать исследовать — роль ε-жадности из урока 6.0.2.

Ещё один стандартный приём — нормировать преимущества по батчу: (A^−mean)/std(\hat A - \mathrm{mean}) / \mathrm{std}. Масштаб наград перестаёт влиять на размер шага. Запомните: в GRPO почти та же нормировка, только по группе ответов на один промпт.

Пример. Батч из четырёх действий: ρ=[1.2,0.8,0.75,2.0]\rho = [1.2, 0.8, 0.75, 2.0], A^=[1,−0.5,2,−1]\hat A = [1, -0.5, 2, -1]. Варианты ρA^=[1.2,−0.4,1.5,−2.0]\rho \hat A = [1.2, -0.4, 1.5, -2.0], клиппированные [1.2,−0.4,1.6,−1.2][1.2, -0.4, 1.6, -1.2], минимум [1.2,−0.4,1.5,−2.0][1.2, -0.4, 1.5, -2.0], цель =0.075= 0.075. Критик ошибся на [−0.5,0,−1,1][-0.5, 0, -1, 1]: MSE =0.5625= 0.5625. Средняя энтропия 1.0. Loss =−0.075+0.5⋅0.5625−0.01⋅1.0≈0.196= -0.075 + 0.5 \cdot 0.5625 - 0.01 \cdot 1.0 \approx 0.196.

  1. Сыграть TT шагов текущей политикой πold\pi_{\text{old}}, запомнить st,at,rts_t, a_t, r_t, log⁡πold(at∣st)\log \pi_{\text{old}}(a_t \mid s_t) и V(st)V(s_t).
  2. Посчитать δt\delta_t, преимущества GAE и цели R^t=A^t+V(st)\hat R_t = \hat A_t + V(s_t).
  3. KK эпох: перемешать батч, для каждого мини-батча нормировать A^\hat A и сделать шаг по L(θ,ψ)\mathcal{L}(\theta, \psi).
  4. Повторить с новым батчем.

В статье PPO для задач MuJoCo: T=2048T = 2048, K=10K = 10, мини-батч 64, Adam с шагом 3⋅10−43 \cdot 10^{-4}, γ=0.99\gamma = 0.99, λ=0.95\lambda = 0.95. Во время обучения смотрят на три числа: среднюю награду, энтропию (не должна падать до нуля слишком рано), KL между старой и новой политикой и clipfrac — долю сэмплов, где сработал клиппинг. Если KL и clipfrac растут, шаг слишком большой.

Всё, что выше, переносится на LLM почти дословно:

RL-понятиеКлассика (CartPole)Языковая модель
Действие ata_tтолкнуть тележку влево или вправоследующий токен
Состояние sts_t4 числа: положение, скорость, уголпромпт и уже написанные токены
Эпизодпока шест не упал (до 500 шагов)один ответ до токена конца
Награда+1 за каждый шагоценка ответа в конце + KL-штраф на каждом токене
γ\gamma, λ\lambda0.99, 0.95обычно γ=1\gamma = 1, λ\lambda около 0.95–1
Актор и критикдве маленькие сетиLLM и её копия со скалярной головой

Формула InstructGPT (Ouyang et al., 2022, §3.5) записана как цель, которую максимизируют:

objective(ϕ)=E(x,y)∼πϕRL[rθ(x,y)−βlog⁡πϕRL(y∣x)πSFT(y∣x)]+γ Ex∼Dpretrain[log⁡πϕRL(x)]\text{objective}(\phi) = \mathbb{E}_{(x, y) \sim \pi_\phi^{\text{RL}}}\Big[r_\theta(x, y) - \beta \log \frac{\pi_\phi^{\text{RL}}(y \mid x)}{\pi^{\text{SFT}}(y \mid x)}\Big] + \gamma\, \mathbb{E}_{x \sim D_{\text{pretrain}}}\big[\log \pi_\phi^{\text{RL}}(x)\big]

Теперь вы читаете её целиком: πϕRL\pi_\phi^{\text{RL}} — актор; rθ(x,y)r_\theta(x, y) — награда от reward model за ответ yy на промпт xx; второе слагаемое — штраф за уход от SFT-модели (подробно — в уроках 6.3.1–6.3.2); последнее слагаемое подмешивает обычный претрейн. Осторожно: здесь γ\gamma — вес претрейна, а не дисконт. Оптимизируют эту цель алгоритмом PPO из этого урока: клиппинг, критик, GAE по токенам.

Формула PPO в DeepSeekMath (Shao et al., 2024, формулы 1–2) — тот же LCLIPL^{\text{CLIP}}, усреднённый по токенам ответа oo на вопрос qq: 1∣o∣∑tmin⁡(ρtAt,clip(ρt,1−ε,1+ε)At)\frac{1}{|o|}\sum_t \min\big(\rho_t A_t, \mathrm{clip}(\rho_t, 1-\varepsilon, 1+\varepsilon) A_t\big). Авторы прямо пишут, что AtA_t считается GAE по наградам и обученной функции ценности VψV_\psi, а награда токена — оценка reward model минус βlog⁡(πθ/πref)\beta \log(\pi_\theta / \pi_{\text{ref}}). И сразу объясняют, что им не нравится: критик — ещё одна модель размером с политику, а учить его точным на каждом токене трудно, когда награда приходит только на последнем. Так появился GRPO:

PPO (DeepSeekMath, ф. 1–2)GRPO (ф. 3)
ПреимуществоGAE по критику VψV_\psi, своё у каждого токена(ri−mean)/std(r_i - \mathrm{mean}) / \mathrm{std} по группе ответов на промпт, одно на все токены
Критикобучается, размером с политикунет
KL к референсув награде на каждом токенеотдельным слагаемым в loss
Клиппинг ρ\rhoестьесть
Ответов на промптобычно одингруппа из GG (в статье 64)

Иначе говоря, GRPO оставил от PPO отношение вероятностей с клиппингом, а преимущество оценивает без критика — средней наградой группы, то есть выборочной оценкой V(промпт)V(\text{промпт}) из урока 6.0.1. Подробно — в уроках 6.3.2 (PPO в RLHF) и 6.4.1 (GRPO).

Реализуйте TD-ошибки, GAE, клиппированную цель, нормировку преимуществ и полный loss PPO. Тесты сверят ответы с примерами урока (робот-курьер, таблица пяти случаев) и проверят, что в клиппированной области градиент равен нулю.

GAE и loss PPO на NumPynumpy · нужен интернет7 тестов
import numpy as np


def td_errors(rewards, values, last_value, gamma):
    """TD-ошибки δ_t = r_t + γ·V(s_{t+1}) − V(s_t) для одной траектории.

    rewards — [T], values — [T] (оценки критика V(s_0) … V(s_{T-1})),
    last_value — V(s_T) после последнего шага (0, если эпизод закончился).
    Ответ — np.ndarray длины T. Подсказка: «следующие» ценности — это values[1:] и last_value.
    """
    # TODO: ваш код
    ...


def gae(rewards, values, last_value, gamma, lam):
    """GAE(λ): A_t = δ_t + γλ·A_{t+1}, считаем с конца траектории (A_T = 0).

    Вернуть кортеж (advantages, returns), где returns = advantages + values — цели для критика.
    """
    # TODO: ваш код
    ...


def ppo_clip_objective(logp_new, logp_old, adv, eps=0.2):
    """Клиппированная цель PPO (её МАКСИМИЗИРУЮТ): mean(min(ρ·A, clip(ρ, 1−ε, 1+ε)·A)).

    ρ = exp(logp_new − logp_old) — отношение вероятностей новой и старой политики.
    Вернуть одно число (float).
    """
    # TODO: ваш код
    ...


def normalize_advantages(adv, eps=1e-8):
    """Нормировка по батчу: (A − mean) / (std + eps)."""
    # TODO: ваш код
    ...


def ppo_loss(logp_new, logp_old, adv, values_pred, returns, entropy, eps=0.2, c_v=0.5, c_e=0.01):
    """Полный loss PPO для МИНИМИЗАЦИИ:
        −clip_objective + c_v · mean((V_pred − returns)²) − c_e · mean(entropy)
    Вернуть одно число (float).
    """
    # TODO: ваш код
    ...


# Проверьте себя кнопкой «Запустить»: пример из урока (робот-курьер)
r, V = [1.0, 0.0, 2.0], [2.0, 1.5, 1.0]
print("δ =", td_errors(r, V, 0.0, 0.9))                    # [0.35, -0.6, 1.0]
for lam in [0.0, 0.5, 0.95, 1.0]:
    out = gae(r, V, 0.0, 0.9, lam)
    if out is not None:
        print(f"λ = {lam}: A = {np.round(out[0], 4)}")
print(ppo_clip_objective(np.log([1.1, 1.5, 0.5, 0.7, 1.5]), np.zeros(5), [1, 1, 1, -1, -1]))   # ≈ 0.1

Полный PPO на PyTorch в 70 строк: GAE с честной обработкой terminated и truncated, клиппинг, критик, энтропия, нормировка преимуществ и мониторинг KL и clipfrac. Нужны pip install gymnasium и PyTorch. Гиперпараметры — из таблицы статьи PPO.

import gymnasium as gym
import numpy as np
import torch
import torch.nn as nn
torch.manual_seed(0)
env = gym.make("CartPole-v1")
n_obs, n_act = env.observation_space.shape[0], env.action_space.n
actor = nn.Sequential(nn.Linear(n_obs, 64), nn.Tanh(), nn.Linear(64, 64), nn.Tanh(), nn.Linear(64, n_act))
critic = nn.Sequential(nn.Linear(n_obs, 64), nn.Tanh(), nn.Linear(64, 64), nn.Tanh(), nn.Linear(64, 1))
opt = torch.optim.Adam(list(actor.parameters()) + list(critic.parameters()), lr=3e-4)
T, epochs, mb = 2048, 10, 64 # шагов в батче, эпох на батч, размер мини-батча
gamma, lam, eps, c_v, c_e = 0.99, 0.95, 0.2, 0.5, 0.01
s, _ = env.reset(seed=0)
ep_ret, finished = 0.0, []
for update in range(1, 26): # 25 обновлений × 2048 шагов ≈ 51 тыс. шагов
# ── 1. Сбор батча текущей политикой (она же π_old) ──
S, A, R, D, TR, LOGP, V, V_NEXT = [], [], [], [], [], [], [], []
for _ in range(T):
st = torch.as_tensor(s, dtype=torch.float32)
with torch.no_grad():
dist = torch.distributions.Categorical(logits=actor(st))
a = dist.sample()
v = critic(st).item()
s2, r, terminated, truncated, _ = env.step(a.item())
with torch.no_grad(): # V(s_{t+1}) нужен, если эпизод оборван по лимиту
v_next = critic(torch.as_tensor(s2, dtype=torch.float32)).item()
S.append(s); A.append(a.item()); R.append(r); LOGP.append(dist.log_prob(a).item())
D.append(terminated or truncated); TR.append(truncated); V.append(v); V_NEXT.append(v_next)
ep_ret += r
s = s2
if terminated or truncated:
finished.append(ep_ret)
ep_ret = 0.0
s, _ = env.reset()
# ── 2. GAE: δ_t = r_t + γV(s_{t+1})·[не конец] − V(s_t), A_t = δ_t + γλA_{t+1}·[эпизод не кончился] ──
R, V, V_NEXT = np.array(R), np.array(V), np.array(V_NEXT)
D, TR = np.array(D), np.array(TR)
terminal = D & ~TR
delta = R + gamma * V_NEXT * (~terminal) - V
adv, running = np.zeros(T), 0.0
for t in reversed(range(T)):
running = delta[t] + gamma * lam * running * (not D[t])
adv[t] = running
ret = adv + V # цели для критика
S = torch.as_tensor(np.array(S), dtype=torch.float32)
A, LOGP = torch.as_tensor(A), torch.as_tensor(LOGP, dtype=torch.float32)
ADV, RET = torch.as_tensor(adv, dtype=torch.float32), torch.as_tensor(ret, dtype=torch.float32)
# ── 3. Несколько эпох по мини-батчам ──
kls, clips, ents = [], [], []
for _ in range(epochs):
for idx in torch.randperm(T).split(mb):
dist = torch.distributions.Categorical(logits=actor(S[idx]))
logp = dist.log_prob(A[idx])
ratio = torch.exp(logp - LOGP[idx]) # ρ = π_θ / π_old
a_mb = (ADV[idx] - ADV[idx].mean()) / (ADV[idx].std() + 1e-8) # нормировка преимуществ
policy_obj = torch.min(ratio * a_mb, torch.clamp(ratio, 1 - eps, 1 + eps) * a_mb).mean()
value_loss = ((critic(S[idx]).squeeze(-1) - RET[idx]) ** 2).mean()
entropy = dist.entropy().mean()
loss = -policy_obj + c_v * value_loss - c_e * entropy
opt.zero_grad()
loss.backward()
nn.utils.clip_grad_norm_(list(actor.parameters()) + list(critic.parameters()), 0.5)
opt.step()
with torch.no_grad():
kls.append((LOGP[idx] - logp).mean().item()) # оценка KL(π_old ‖ π_θ)
clips.append(((ratio - 1).abs() > eps).float().mean().item())
ents.append(entropy.item())
if update % 3 == 1 or update == 25:
last = np.mean(finished[-10:]) if finished else 0.0
print(f"обновление {update:2d} шагов {update * T:6d} отдача {last:6.1f} энтропия {np.mean(ents):.3f} "
f"KL {np.mean(kls):.4f} clipfrac {np.mean(clips):.3f}")

Вывод (PyTorch 2.x, gymnasium 1.x, один поток CPU; на другой версии числа будут немного другими):

обновление 1 шагов 2048 отдача 23.6 энтропия 0.688 KL 0.0084 clipfrac 0.138
обновление 4 шагов 8192 отдача 46.8 энтропия 0.616 KL 0.0060 clipfrac 0.059
обновление 7 шагов 14336 отдача 183.2 энтропия 0.592 KL 0.0083 clipfrac 0.069
обновление 10 шагов 20480 отдача 338.4 энтропия 0.564 KL 0.0068 clipfrac 0.053
обновление 13 шагов 26624 отдача 472.6 энтропия 0.540 KL 0.0046 clipfrac 0.072
обновление 16 шагов 32768 отдача 500.0 энтропия 0.558 KL 0.0063 clipfrac 0.047
обновление 19 шагов 38912 отдача 453.2 энтропия 0.549 KL 0.0023 clipfrac 0.069
обновление 22 шагов 45056 отдача 494.7 энтропия 0.529 KL 0.0038 clipfrac 0.083
обновление 25 шагов 51200 отдача 500.0 энтропия 0.484 KL 0.0048 clipfrac 0.068

Что видно. Отдача выросла с 24 до максимума 500 примерно за 30 тысяч шагов. Энтропия падает плавно — с ln⁡2≈0.693\ln 2 \approx 0.693 (монетка) до 0.48: политика становится увереннее, но не схлопывается. KL держится около 0.005, а clipfrac — в районе 5–8%: шаги маленькие, клиппинг срабатывает изредка, как и задумано. На нагруженном 4-ядерном CPU прогон занял около трёх минут. Сохраните средние значения по обновлениям в списки и постройте три графика — награда, энтропия и KL: так выглядят панели мониторинга любого RL-фреймворка, в том числе verl в модуле 6.4.

Проверь себя6 вопросов
1 Почему в градиенте политики ∇θJ=E[∑t∇θlog⁡πθ(at∣st) A^t]\nabla_\theta J = \mathbb{E}[\sum_t \nabla_\theta \log \pi_\theta(a_t \mid s_t)\, \hat A_t] не нужна модель среды?
2 TD-ошибки трёхшаговой траектории δ=[0.1,0.3,0.1]\delta = [0.1, 0.3, 0.1], γ=1\gamma = 1, λ=0.5\lambda = 0.5. Чему равно A^0\hat A_0 по GAE? Округлите до трёх знаков.
3 Какие утверждения о λ в GAE верны?Можно выбрать несколько ответов
4 В PPO с ε=0.2\varepsilon = 0.2 у действия A^=−1\hat A = -1 и ρ=0.7\rho = 0.7. Что сделает градиент клиппированной цели?
5 Зачем в loss PPO энтропийный бонус −ceH(π)-c_e H(\pi)?
6 Что GRPO изменил по сравнению с PPO из DeepSeekMath?
  • Градиент политики E[∑t∇log⁡πθ(at∣st) A^t]\mathbb{E}[\sum_t \nabla \log \pi_\theta(a_t \mid s_t)\, \hat A_t] выводится трюком с логарифмом и не требует модели среды. REINFORCE оценивает его по эпизодам; reward-to-go и baseline снижают шум, а лучший baseline V(s)V(s) превращает вес действия в преимущество.
  • Actor-critic: критик VψV_\psi учится регрессией к отдачам, актор — на преимуществах. GAE(λ) — затухающая сумма TD-ошибок A^t=δt+γλA^t+1\hat A_t = \delta_t + \gamma\lambda \hat A_{t+1}; λ\lambda двигает оценку между TD и Монте-Карло.
  • PPO переиспользует батч через отношение вероятностей ρ\rho, клиппингом не даёт уйти далеко и учит всё одним loss: −LCLIP+cv⋅MSE критика−ce⋅энтропия-L^{\text{CLIP}} + c_v \cdot \text{MSE критика} - c_e \cdot \text{энтропия}.
  • Для LLM: токен — действие, ответ — эпизод, награда в конце плюс KL на токенах. InstructGPT — это PPO с такой наградой; GRPO — PPO без критика, с групповым преимуществом.

Дальше: начинаем путь от базовой модели к ассистенту — что умеет модель после претрейна и из каких этапов состоит пост-трейнинг. Словарь RL из этого модуля вернётся в модулях 6.3 и 6.4.