Модуль 4. Градиенты политики: от REINFORCE к PPO
Сначала попробуйте решить упражнение. Ответы, подробные решения и проектные ориентиры раскрываются отдельно и сохраняют своё состояние при переключении RU/EN.
Скачать файлы курса (.zip) 382,1 КБАрхив содержит файлы на обоих языках и вспомогательные материалы с сохранением структуры каталогов.
Что именно даёт тождество логарифмической производной
Сначала выберите все верные утверждения о формуле
-
Она делает дискретный случайный выбор дифференцируемым.
-
Она позволяет оценивать градиент по выборкам, не дифференцируя .
-
Она устраняет дисперсию Монте-Карло.
-
Она допускает недифференцируемую награду, если вероятность выбранного исхода дифференцируема по .
-
Она всегда применима без условий регулярности.
Затем рассмотрите , которое зависит от напрямую. Какой член нужно добавить и что он означает?
Показать ответ и решение
Верны пункты 2 и 4.
- 2. Тождество переносит производную на , поэтому достаточно вычислить для выбранного исхода; её производная не нужна.
- 4. Награда может быть результатом программы или человеческой оценки. Требуется дифференцируемость логарифма вероятности выбранного исхода по параметрам политики.
Пункт 1 неверен: дискретный случайный выбор не становится дифференцируемым — строится другой оцениватель градиента. Пункт 3 неверен: Монте-Карло остаётся стохастическим и часто высокодисперсионным. Пункт 5 неверен: перенос производной под сумму или интеграл требует условий регулярности.
При прямой зависимости правило произведения даёт
Первый член учитывает изменение распределения исходов, второй — изменение самой величины при фиксированном исходе. В классическом REINFORCE награда среды не зависит от параметров политики напрямую. Для дифференцируемого KL-штрафа или другого параметрического регуляризатора прямой член нужно либо сохранить, либо сознательно остановить его градиент и тем самым определить другую вычислительную цель.
Когда динамика среды исчезает из производной
Для каждого сценария укажите, исчезает ли соответствующая часть среды из явной производной , и объясните почему.
-
Фиксированный недифференцируемый симулятор не зависит от .
-
Параметр одновременно управляет политикой и вероятностями переходов.
-
Замороженная модель награды не зависит от , но определяет скалярную награду.
-
Известная дифференцируемая модель мира используется в другом оценивателе, где градиент проводят через переходы.
Отдельно поясните, почему «исчезает из производной» не означает «не влияет на обучение».
Показать ответ и решение
-
Да. Фиксированный симулятор входит в , но его логарифм не зависит от , поэтому производная равна нулю.
-
Нет. Если переходы параметризованы тем же , появляется дополнительный член .
-
Да, из явной производной. Замороженная модель награды не требует дифференцирования, но её выход умножает градиент логарифма вероятности и определяет направление обучения.
-
Нет в выбранном оценивателе. Если мы сознательно проводим градиент через дифференцируемую модель мира, используются производные переходов. Это уже не чистый оцениватель с логарифмической производной по действиям.
Среда остаётся в распределении траекторий. Она определяет, какие состояния и награды увидит политика. Поэтому «производная перехода равна нулю» не означает «переход не влияет на градиентную оценку через данные».
Каузальность и две договорённости о дисконтировании
Объясните, почему награды до действия можно удалить из множителя при градиенте логарифма вероятности. Затем сравните:
и вес .
В каком выводе появляется внешний ? Почему запись без него всё же часто корректна при другой договорённости?
Показать ответ и решение
Условно на истории до прошлые награды уже фиксированы. Их вклад имеет вид
Поэтому остаются только награды, на которые действие могло повлиять, — накопленная награда от текущего шага.
Если цель буквально равна
то после перегруппировки наград множитель при градиенте логарифма вероятности равен . Запись только с часто использует дисконтированное распределение посещений состояний или иную нормировку цели. Она не является алгебраически той же строкой без изменения договорённости. При конечном горизонте удаление внешнего множителя меняет относительный вес ранних и поздних решений и потому может изменить направление градиента, а не только его длину.
Допустимые и недопустимые опорные уровни
Какие из следующих величин можно вычесть из без дополнительной поправки, сохранив ожидаемый градиент? Предположите, что состояние уже зафиксировано.
-
Константа .
-
.
-
Средняя награда независимых ответов на тот же запрос, не включающая текущий ответ.
-
, зависящая от текущего действия.
-
Среднее той же группы, включающее текущую награду.
Разделите ответы на три категории: «точно допустимо», «не допустимо без поправки», «даёт известный масштаб при конечном размере группы, но не буквальную несмещённость».
Показать ответ и решение
Точно допустимы: 1, 2, 3.
- Константа и функция состояния не зависят от текущего действия.
- Среднее независимых других ответов условно не зависит от текущего ответа.
Не допустимо без поправки: 4. зависит от действия, поэтому доказательство через нулевое ожидание градиента логарифма вероятности не работает.
Известный масштаб при конечном размере группы: 5. Общее среднее включает текущую награду. При условно независимой группе оно масштабирует ожидаемый градиент на , а не сохраняет его буквально.
Общее среднее и исключение текущего ответа
Пусть условно независимых ответов на один запрос имеют награды .
-
Почему не является независимым от текущего ответа опорным уровнем?
-
Каков ожидаемый масштаб градиента при преимуществах ?
-
Почему среднее с исключением текущего ответа восстанавливает правильное ожидание?
-
Как связаны и ?
-
Что происходит, если все награды равны?
Показать ответ и решение
-
содержит , а зависит от текущего ответа .
-
Ожидаемый масштаб равен от исходного градиента.
-
Среднее остальных ответов условно независимо от , поэтому его произведение с градиентом логарифма вероятности имеет нулевое ожидание.
-
Алгебраически
- Оба типа преимуществ равны нулю. Внутригруппового сигнала для ранжирования ответов нет.
Бутстрэппинг и граница обратной рекурсии GAE
Разберите два случая.
Случай I. Агент достиг ограничения времени, но не терминального состояния. Среда возвращает финальное наблюдение и затем выполняет reset().
Случай II. Сборщик заполнил фиксированный буфер роллаутов. Эпизод не завершился, среда не сбрасывается, но следующего преимущества в текущем массиве уже нет.
Для каждого случая ответьте:
-
Нужно ли использовать в TD-ошибке?
-
Нужно ли переносить обратную рекурсию на следующий элемент текущего массива?
-
Какие значения имеют
terminatedиtrace_cut? -
Почему один флаг
doneнедостаточен?
Показать ответ и решение
Случай I — техническое усечение с последующим reset().
-
Да. Продолжение в математической задаче существует, поэтому используется ценность последнего наблюдения до сброса.
-
Нет. Следующий элемент после
reset()принадлежит другому эпизоду. -
terminated=False,trace_cut=True.
Случай II — граница локального буфера.
-
Да. Среда продолжает тот же эпизод, поэтому следующий прогноз нужен для бутстрэппинга.
-
Нет внутри данного массива, если следующий фрагмент не присоединён к вычислению GAE. Потоковая реализация могла бы перенести накопитель между фрагментами.
-
terminated=False,trace_cut=Trueдля последней записи локального массива.
Один done смешивает два вопроса: существует ли ценность продолжения и имеется ли следующее преимущество в текущей обратной рекурсии. Если занулять бутстрэппинг при каждом done, цели около временных ограничений и границ буфера смещаются: при положительной ценности продолжения цели занижаются, при отрицательной — завышаются.
Четыре области клиппированной цели PPO
Для заполните таблицу. Укажите, активна ли клиппированная ветвь, и вычислите пессимистический член .
| Клиппирование меняет цель? | Пессимистический член | ||
|---|---|---|---|
| 1.4 | 2 | ? | ? |
| 0.6 | 2 | ? | ? |
| 1.4 | -2 | ? | ? |
| 0.6 | -2 | ? | ? |
После таблицы объясните, почему ratio_outside_fraction может равняться 1, а active_clip_fraction — 0.5.
Показать ответ и решение
При интервал равен .
| Клиппирование меняет цель? | Пессимистический член | ||
|---|---|---|---|
| 1.4 | 2 | да | |
| 0.6 | 2 | нет | |
| 1.4 | -2 | нет | |
| 0.6 | -2 | да |
Все четыре отношения находятся вне интервала, поэтому ratio_outside_fraction=1. Клиппирование создаёт пессимистический минимум только в первой и четвёртой строках, поэтому active_clip_fraction=2/4=0.5.
RLOO, GRPO и распределение вклада между токенами
Сформулируйте:
-
одну общую идею RLOO и GRPO;
-
два существенных различия между их базовыми формулировками;
-
почему один коэффициент уровня полного ответа, применённый ко всем токенам, даёт корректный градиент награды за полный ответ;
-
почему тот же приём не решает задачу распределения вклада между токенами.
Показать ответ и решение
-
Общая идея — несколько ответов на один запрос и относительная оценка внутри группы без обязательного отдельного критика.
-
Базовый RLOO использует среднее с исключением текущего ответа и цель REINFORCE; канонический GRPO использует PPO-подобные отношения и клиппирование, обычно нормирует групповой сигнал и добавляет регуляризацию относительно базовой политики.
-
Логарифм вероятности ответа равен сумме логарифмов вероятностей токенов. Поэтому один коэффициент ответа, умноженный на эту сумму, задаёт градиент логарифма вероятности полной последовательности.
-
Коэффициент одинаков для всех токенов и не сообщает, какой локальный выбор вызвал награду. Он оптимизирует цель, заданную на уровне полного ответа, но не локализует причинный вклад.
Градиент логарифма softmax
Для логитов
и выбранного действия вычислите
Дайте ответ с точностью не хуже и проверьте, что сумма компонент равна нулю.
Показать ответ и решение
Сначала
Для действия 2:
Сумма компонент с учётом округления равна нулю.
Награда от текущего шага и внешний дисконт
Для наград
и вычислите:
-
вектор ;
-
вектор для .
Поясните, какую цель представляет второй вектор.
Показать ответ и решение
Обратная рекурсия:
Итак,
Внешний дисконт:
Второй вектор соответствует прямому градиенту буквальной цели с весами для наград от начала эпизода.
Ценность и оптимальный скалярный опорный уровень
Политика имеет логиты , а значения действий равны .
-
Вычислите вероятности действий.
-
Вычислите .
-
Для каждого действия найдите .
-
Вычислите
Почему и различаются?
Показать ответ и решение
Вероятности:
Ценность:
Градиенты логарифмических вероятностей:
Их квадраты норм:
После подстановки:
Различие возникает потому, что взвешивает значения действий квадратом нормы градиента логарифма вероятности. Редкое действие 1 имеет значительно больший градиентный вектор и поэтому получает больший вес, чем в обычном .
Групповые преимущества
Для наград
вычислите:
-
общее среднее;
-
центрированные преимущества ;
-
опорные уровни с исключением текущего элемента;
-
RLOO-преимущества;
-
коэффициент пропорциональности между двумя векторами преимуществ;
-
ожидаемый масштаб оценивателя с общим средним для группы размера 4.
Показать ответ и решение
Общее среднее:
Центрированные преимущества:
Опорные уровни с исключением текущего элемента:
RLOO-преимущества:
Они равны от центрированных преимуществ. Ожидаемый масштаб оценки с общим средним равен
GAE при техническом усечении
Даны два независимых перехода в массиве. Первый завершает эпизод из-за ограничения времени; второй относится к другому эпизоду:
Маски:
При и вычислите TD-ошибки и GAE-преимущества. Объясните, почему награда 100 не входит в преимущество первого перехода.
Показать ответ и решение
Первый переход усечён, но не терминален:
Второй переход терминален:
Поскольку episode_end[0]=1, рекурсия не переносит назад:
Награда 100 относится к другому эпизоду после reset(). Она соседняя только в массиве, но не в траектории.
Члены PPO и выборочная диагностика KL
Пусть
Вычислите:
-
неклиппированные члены ;
-
клиппированные члены;
-
пессимистические члены;
-
среднюю цель PPO;
-
долю отношений вне интервала;
-
долю примеров, где клиппирование изменяет цель;
-
выборочную диагностику
Показать ответ и решение
Неклиппированные члены:
Клиппированные отношения равны , поэтому соответствующие члены:
Покомпонентный минимум:
Средняя цель:
Все отношения вне интервала, поэтому ratio_outside_fraction=1. Клиппирование меняет минимум в двух из четырёх случаев, поэтому active_clip_fraction=0.5.
Для двух значений вклад равен , а для двух значений — . Поэтому
Это неотрицательная выборочная диагностика. Она совпадает с обратной KL в математическом ожидании по старой политике при требуемом совпадении носителей, но один конечный пакет не превращает её в точное KL-ограничение.
Устойчивый softmax и градиент логарифма вероятности
Реализуйте:
import numpy as np
def stable_softmax(logits: np.ndarray) -> np.ndarray:
...
def grad_log_softmax(logits: np.ndarray, action: int) -> np.ndarray:
...Требования:
- вход — одномерный массив;
- вычисление устойчиво для
[1000, 999]; - сумма градиента равна нулю с абсолютной погрешностью не более ;
- некорректный индекс действия вызывает
ValueError.
Открытые проверки:
np.testing.assert_allclose(stable_softmax([1000.0, 999.0]), [0.7310585786, 0.2689414214], rtol=2e-10)Показать ответ и решение
import numpy as np
def stable_softmax(logits: np.ndarray) -> np.ndarray:
z = np.asarray(logits, dtype=float)
if z.ndim != 1:
raise ValueError("logits must be one-dimensional")
shifted = z - np.max(z)
exp_z = np.exp(shifted)
return exp_z / np.sum(exp_z)
def grad_log_softmax(logits: np.ndarray, action: int) -> np.ndarray:
p = stable_softmax(logits)
if not 0 <= action < len(p):
raise ValueError("action is out of range")
result = -p
result[action] += 1.0
return resultВычитание максимума не меняет softmax, но предотвращает переполнение экспоненты.
Накопленная награда от текущего шага и две договорённости о дисконтировании
Реализуйте:
def reward_to_go(rewards: np.ndarray, gamma: float) -> np.ndarray:
...
def discounted_score_weights(rewards: np.ndarray, gamma: float) -> np.ndarray:
...Первый результат содержит , второй — . Алгоритм должен работать за времени и не использовать вложенные циклы.
Открытые проверки:
np.testing.assert_allclose(reward_to_go([1.0, 2.0, 3.0], 0.5), [2.75, 3.5, 3.0])Показать ответ и решение
import numpy as np
def reward_to_go(rewards: np.ndarray, gamma: float) -> np.ndarray:
r = np.asarray(rewards, dtype=float)
if r.ndim != 1:
raise ValueError("rewards must be one-dimensional")
if not 0.0 <= gamma <= 1.0:
raise ValueError("gamma must lie in [0, 1]")
result = np.empty_like(r)
running = 0.0
for t in range(len(r) - 1, -1, -1):
running = r[t] + gamma * running
result[t] = running
return result
def discounted_score_weights(rewards: np.ndarray, gamma: float) -> np.ndarray:
g = reward_to_go(rewards, gamma)
return (gamma ** np.arange(len(g), dtype=float)) * gОдин обратный проход даёт сложность и постоянную дополнительную память помимо результата.
Среднее, исключение текущего элемента и внутригрупповая нормировка
Реализуйте:
def leave_one_out_baselines(rewards: np.ndarray) -> np.ndarray:
...
def group_mean_advantages(rewards: np.ndarray) -> np.ndarray:
...
def group_zscore_advantages(rewards: np.ndarray, eps: float = 1e-8) -> np.ndarray:
...Для группы из одного элемента leave_one_out_baselines должна вызывать ValueError. При одинаковых наградах стандартизованные преимущества должны быть нулевыми, а не NaN.
Открытые проверки:
np.testing.assert_allclose(leave_one_out_baselines([1.0, 3.0, 8.0, 2.0]), [13/3, 11/3, 2.0, 4.0])Показать ответ и решение
import numpy as np
def leave_one_out_baselines(rewards: np.ndarray) -> np.ndarray:
r = np.asarray(rewards, dtype=float)
if r.ndim != 1 or len(r) < 2:
raise ValueError("at least two rewards are required")
return (np.sum(r) - r) / (len(r) - 1)
def group_mean_advantages(rewards: np.ndarray) -> np.ndarray:
r = np.asarray(rewards, dtype=float)
if r.ndim != 1 or len(r) == 0:
raise ValueError("rewards must be a non-empty vector")
return r - np.mean(r)
def group_zscore_advantages(rewards: np.ndarray, eps: float = 1e-8) -> np.ndarray:
centered = group_mean_advantages(rewards)
scale = float(np.sqrt(np.mean(centered ** 2)))
if scale < eps:
return np.zeros_like(centered)
return centered / scaleИспользована «популяционная» внутригрупповая дисперсия с делением на , что соответствует распространённой нормировке преимуществ.
GAE с раздельными масками бутстрэппинга и рекурсии
Реализуйте обратную рекурсию:
def gae_advantages(
rewards: np.ndarray,
values: np.ndarray,
next_values: np.ndarray,
terminated: np.ndarray,
trace_cuts: np.ndarray,
gamma: float,
gae_lambda: float,
) -> np.ndarray:
...terminated отключает бутстрэппинг в TD-ошибке. trace_cuts отключает перенос рекурсии на следующий элемент текущего массива. Последний элемент фиксированного роллаут-буфера может иметь terminated=False, trace_cuts=True.
Открытые проверки:
rewards = np.array([1.0, 2.0])
values = np.array([0.5, 0.7])
next_values = np.array([0.7, 0.0])
term = np.array([False, True])
cuts = np.array([False, True])
result = gae_advantages(rewards, values, next_values, term, cuts, gamma=0.9, gae_lambda=0.8)
delta1 = 2.0 - 0.7
delta0 = 1.0 + 0.9 * 0.7 - 0.5
np.testing.assert_allclose(result, [delta0 + 0.9 * 0.8 * delta1, delta1])
# Нетерминальная граница буфера: бутстрэппинг есть, утечки из следующей строки нет.
result = gae_advantages(
np.array([1.0, 100.0]), np.array([2.0, 0.0]), np.array([10.0, 0.0]),
np.array([False, True]), np.array([True, True]), 0.9, 1.0,
)
np.testing.assert_allclose(result, [8.0, 100.0])Показать ответ и решение
import numpy as np
def gae_advantages(
rewards: np.ndarray,
values: np.ndarray,
next_values: np.ndarray,
terminated: np.ndarray,
trace_cuts: np.ndarray,
gamma: float,
gae_lambda: float,
) -> np.ndarray:
r = np.asarray(rewards, dtype=float)
v = np.asarray(values, dtype=float)
nv = np.asarray(next_values, dtype=float)
term = np.asarray(terminated, dtype=bool)
cuts = np.asarray(trace_cuts, dtype=bool)
if not (r.shape == v.shape == nv.shape == term.shape == cuts.shape) or r.ndim != 1:
raise ValueError("all inputs must be aligned one-dimensional arrays")
if not (0.0 <= gamma <= 1.0 and 0.0 <= gae_lambda <= 1.0):
raise ValueError("gamma and gae_lambda must lie in [0, 1]")
deltas = r + gamma * (~term).astype(float) * nv - v
result = np.empty_like(deltas)
running = 0.0
for t in range(len(r) - 1, -1, -1):
running = deltas[t] + gamma * gae_lambda * (not cuts[t]) * running
result[t] = running
return resultКлючевой момент — две маски отвечают на разные вопросы. При усечении или локальной границе буфера terminated=False, поэтому следующий прогноз используется; trace_cuts=True, поэтому текущая пакетная рекурсия останавливается.
Клиппированные члены и диагностика PPO
Реализуйте:
def ppo_clipped_terms(ratio, advantage, clip_epsilon=0.2):
...
def ppo_diagnostics(new_log_prob, old_log_prob, advantage, clip_epsilon=0.2):
...Диагностика должна возвращать:
ratio_outside_fraction;active_clip_fraction;approx_kl_nonnegative = mean((ratio - 1) - log_ratio);mean_log_ratio.
Отношения должны вычисляться из разности логарифмических вероятностей.
Открытые проверки:
ratio = np.array([0.75, 1.00, 1.30, 1.10])
advantage = np.array([1.0, 1.0, 1.0, -1.0])
unclipped, clipped, pessimistic = ppo_clipped_terms(ratio, advantage, 0.2)
assert np.allclose(unclipped, [0.75, 1.0, 1.3, -1.1])
assert np.allclose(clipped, [0.8, 1.0, 1.2, -1.1])
assert np.allclose(pessimistic, [0.75, 1.0, 1.2, -1.1])Показать ответ и решение
from dataclasses import dataclass
import numpy as np
@dataclass(frozen=True)
class PPODiagnostics:
ratio_outside_fraction: float
active_clip_fraction: float
approx_kl_nonnegative: float
mean_log_ratio: float
def ppo_clipped_terms(ratio, advantage, clip_epsilon=0.2):
r = np.asarray(ratio, dtype=float)
a = np.asarray(advantage, dtype=float)
if r.shape != a.shape or np.any(r <= 0):
raise ValueError("positive aligned ratios are required")
unclipped = r * a
clipped = np.clip(r, 1 - clip_epsilon, 1 + clip_epsilon) * a
pessimistic = np.minimum(unclipped, clipped)
return unclipped, clipped, pessimistic
def ppo_diagnostics(new_log_prob, old_log_prob, advantage, clip_epsilon=0.2):
log_ratio = np.asarray(new_log_prob, dtype=float) - np.asarray(old_log_prob, dtype=float)
ratio = np.exp(log_ratio)
a = np.asarray(advantage, dtype=float)
unclipped, _, pessimistic = ppo_clipped_terms(ratio, a, clip_epsilon)
outside = np.mean((ratio < 1 - clip_epsilon) | (ratio > 1 + clip_epsilon))
active = np.mean(pessimistic < unclipped - 1e-15)
approx_kl = np.mean((ratio - 1) - log_ratio)
return PPODiagnostics(float(outside), float(active), float(approx_kl), float(np.mean(log_ratio)))active_clip_fraction проверяет не положение отношения само по себе, а изменение пессимистического минимума.
Маска ответа, вероятность последовательности и токеновые преимущества
Реализуйте три функции:
def response_token_mask(input_ids, prompt_lengths, pad_token_id, eos_token_id):
...
def sequence_log_probability(token_log_probs, mask=None):
...
def broadcast_sequence_advantages(sequence_advantages, token_mask):
...Первая функция возвращает маску формы [batch, T-1], согласованную с метками input_ids[:, 1:]: токены запроса и PAD исключаются, первый сгенерированный EOS включается, позиции после него исключаются. Вторая суммирует логарифмические вероятности токенов ответа. Третья распространяет одно преимущество полного ответа на ненулевые позиции маски.
Открытая проверка:
ids = np.array([
[1, 10, 11, 20, 21, 2, 0],
[1, 12, 30, 2, 99, 0, 0],
])
mask = response_token_mask(ids, np.array([3, 2]), pad_token_id=0, eos_token_id=2)
np.testing.assert_array_equal(mask, [
[0, 0, 1, 1, 1, 0],
[0, 1, 1, 0, 0, 0],
])Показать ответ и решение
import numpy as np
def response_token_mask(input_ids, prompt_lengths, pad_token_id, eos_token_id):
ids = np.asarray(input_ids)
lengths = np.asarray(prompt_lengths, dtype=int)
if ids.ndim != 2 or lengths.shape != (ids.shape[0],):
raise ValueError("input_ids must be [B, T] and prompt_lengths [B]")
mask = np.zeros((ids.shape[0], ids.shape[1] - 1), dtype=float)
for b in range(ids.shape[0]):
active = True
for pos in range(1, ids.shape[1]):
token = int(ids[b, pos])
if pos < lengths[b] or token == pad_token_id or not active:
continue
mask[b, pos - 1] = 1.0
if token == eos_token_id:
active = False
return mask
def sequence_log_probability(token_log_probs, mask=None):
lp = np.asarray(token_log_probs, dtype=float)
if mask is None:
return np.sum(lp, axis=-1)
m = np.asarray(mask, dtype=float)
if m.shape != lp.shape:
raise ValueError("mask must match token_log_probs")
return np.sum(lp * m, axis=-1)
def broadcast_sequence_advantages(sequence_advantages, token_mask):
a = np.asarray(sequence_advantages, dtype=float)
m = np.asarray(token_mask, dtype=float)
if a.ndim != 1 or m.ndim != 2 or m.shape[0] != a.shape[0]:
raise ValueError("advantages must be [B] and token_mask [B, T]")
return a[:, None] * mВозвращаемая маска сдвинута относительно исходных токенов, потому что логиты позиции оценивают метку позиции . Сгенерированный EOS является действием политики и включается; запрос, PAD и хвост после EOS не входят в функцию потерь. Одинаковое преимущество на ненулевых позициях реализует градиент полного ответа, но не превращается в локальное объяснение вклада токенов.