VKВлад КуликовОткрытые лекции
RL для LLM4 / 13

Модуль 4. Градиенты политики: от REINFORCE к PPO

Сначала попробуйте решить упражнение. Ответы, подробные решения и проектные ориентиры раскрываются отдельно и сохраняют своё состояние при переключении RU/EN.

Скачать файлы курса (.zip) 382,1 КБ

Архив содержит файлы на обоих языках и вспомогательные материалы с сохранением структуры каталогов.

Что именно даёт тождество логарифмической производной

Сначала выберите все верные утверждения о формуле

∇θEX∼pθ[f(X)]=E[f(X)∇θlog⁡pθ(X)].\nabla_\theta \mathbb{E}_{X\sim p_\theta}[f(X)] = \mathbb{E}[f(X)\nabla_\theta\log p_\theta(X)].
  1. Она делает дискретный случайный выбор дифференцируемым.

  2. Она позволяет оценивать градиент по выборкам, не дифференцируя ff.

  3. Она устраняет дисперсию Монте-Карло.

  4. Она допускает недифференцируемую награду, если вероятность выбранного исхода дифференцируема по θ\theta.

  5. Она всегда применима без условий регулярности.

Затем рассмотрите fθ(X)f_\theta(X), которое зависит от θ\theta напрямую. Какой член нужно добавить и что он означает?

Показать ответ и решение

Верны пункты 2 и 4.

  • 2. Тождество переносит производную на log⁡pθ(X)\log p_\theta(X), поэтому f(X)f(X) достаточно вычислить для выбранного исхода; её производная не нужна.
  • 4. Награда может быть результатом программы или человеческой оценки. Требуется дифференцируемость логарифма вероятности выбранного исхода по параметрам политики.

Пункт 1 неверен: дискретный случайный выбор не становится дифференцируемым — строится другой оцениватель градиента. Пункт 3 неверен: Монте-Карло остаётся стохастическим и часто высокодисперсионным. Пункт 5 неверен: перенос производной под сумму или интеграл требует условий регулярности.

При прямой зависимости fθf_\theta правило произведения даёт

∇θE[fθ(X)]=E[fθ(X)∇θlog⁡pθ(X)+∇θfθ(X)].\nabla_\theta\mathbb{E}[f_\theta(X)] = \mathbb{E}\left[ f_\theta(X)\nabla_\theta\log p_\theta(X) + \nabla_\theta f_\theta(X) \right].

Первый член учитывает изменение распределения исходов, второй — изменение самой величины при фиксированном исходе. В классическом REINFORCE награда среды не зависит от параметров политики напрямую. Для дифференцируемого KL-штрафа или другого параметрического регуляризатора прямой член нужно либо сохранить, либо сознательно остановить его градиент и тем самым определить другую вычислительную цель.

Когда динамика среды исчезает из производной

Для каждого сценария укажите, исчезает ли соответствующая часть среды из явной производной ∇θlog⁡pθ(τ)\nabla_\theta\log p_\theta(\tau), и объясните почему.

  1. Фиксированный недифференцируемый симулятор не зависит от θ\theta.

  2. Параметр θ\theta одновременно управляет политикой и вероятностями переходов.

  3. Замороженная модель награды не зависит от θ\theta, но определяет скалярную награду.

  4. Известная дифференцируемая модель мира используется в другом оценивателе, где градиент проводят через переходы.

Отдельно поясните, почему «исчезает из производной» не означает «не влияет на обучение».

Показать ответ и решение
  1. Да. Фиксированный симулятор входит в pθ(τ)p_\theta(\tau), но его логарифм не зависит от θ\theta, поэтому производная равна нулю.

  2. Нет. Если переходы параметризованы тем же θ\theta, появляется дополнительный член ∇θlog⁡Pθ(s′∣s,a)\nabla_\theta\log P_\theta(s'\mid s,a).

  3. Да, из явной производной. Замороженная модель награды не требует дифференцирования, но её выход умножает градиент логарифма вероятности и определяет направление обучения.

  4. Нет в выбранном оценивателе. Если мы сознательно проводим градиент через дифференцируемую модель мира, используются производные переходов. Это уже не чистый оцениватель с логарифмической производной по действиям.

Среда остаётся в распределении траекторий. Она определяет, какие состояния и награды увидит политика. Поэтому «производная перехода равна нулю» не означает «переход не влияет на градиентную оценку через данные».

Каузальность и две договорённости о дисконтировании

Объясните, почему награды до действия ata_t можно удалить из множителя при градиенте логарифма вероятности. Затем сравните:

Gt=∑k=tT−1γk−trk+1G_t=\sum_{k=t}^{T-1}\gamma^{k-t}r_{k+1}

и вес γtGt\gamma^tG_t.

В каком выводе появляется внешний γt\gamma^t? Почему запись без него всё же часто корректна при другой договорённости?

Показать ответ и решение

Условно на истории до ata_t прошлые награды уже фиксированы. Их вклад имеет вид

C Eat∼π[∇log⁡π(at∣st)]=0.C\,\mathbb{E}_{a_t\sim\pi}[\nabla\log\pi(a_t\mid s_t)]=0.

Поэтому остаются только награды, на которые действие могло повлиять, — накопленная награда от текущего шага.

Если цель буквально равна

E[∑tγtrt+1],\mathbb{E}\left[\sum_t\gamma^t r_{t+1}\right],

то после перегруппировки наград множитель при градиенте логарифма вероятности равен γtGt\gamma^tG_t. Запись только с GtG_t часто использует дисконтированное распределение посещений состояний или иную нормировку цели. Она не является алгебраически той же строкой без изменения договорённости. При конечном горизонте удаление внешнего множителя меняет относительный вес ранних и поздних решений и потому может изменить направление градиента, а не только его длину.

Допустимые и недопустимые опорные уровни

Какие из следующих величин можно вычесть из Q(s,a)Q(s,a) без дополнительной поправки, сохранив ожидаемый градиент? Предположите, что состояние ss уже зафиксировано.

  1. Константа 77.

  2. Vϕ(s)V_\phi(s).

  3. Средняя награда независимых ответов на тот же запрос, не включающая текущий ответ.

  4. Qψ(s,a)Q_\psi(s,a), зависящая от текущего действия.

  5. Среднее той же группы, включающее текущую награду.

Разделите ответы на три категории: «точно допустимо», «не допустимо без поправки», «даёт известный масштаб при конечном размере группы, но не буквальную несмещённость».

Показать ответ и решение

Точно допустимы: 1, 2, 3.

  • Константа и функция состояния не зависят от текущего действия.
  • Среднее независимых других ответов условно не зависит от текущего ответа.

Не допустимо без поправки: 4. Qψ(s,a)Q_\psi(s,a) зависит от действия, поэтому доказательство через нулевое ожидание градиента логарифма вероятности не работает.

Известный масштаб при конечном размере группы: 5. Общее среднее включает текущую награду. При условно независимой группе оно масштабирует ожидаемый градиент на (G−1)/G(G-1)/G, а не сохраняет его буквально.

Общее среднее и исключение текущего ответа

Пусть GG условно независимых ответов на один запрос имеют награды RiR_i.

  1. Почему Rˉ\bar R не является независимым от текущего ответа опорным уровнем?

  2. Каков ожидаемый масштаб градиента при преимуществах Ri−RˉR_i-\bar R?

  3. Почему среднее с исключением текущего ответа восстанавливает правильное ожидание?

  4. Как связаны Ri−RˉR_i-\bar R и Ri−Rˉ−iR_i-\bar R_{-i}?

  5. Что происходит, если все награды равны?

Показать ответ и решение
  1. Rˉ\bar R содержит Ri/GR_i/G, а RiR_i зависит от текущего ответа YiY_i.

  2. Ожидаемый масштаб равен (G−1)/G(G-1)/G от исходного градиента.

  3. Среднее остальных ответов условно независимо от YiY_i, поэтому его произведение с градиентом логарифма вероятности имеет нулевое ожидание.

  4. Алгебраически

Ri−Rˉ−i=GG−1(Ri−Rˉ).R_i-\bar R_{-i} = \frac{G}{G-1}(R_i-\bar R).
  1. Оба типа преимуществ равны нулю. Внутригруппового сигнала для ранжирования ответов нет.

Бутстрэппинг и граница обратной рекурсии GAE

Разберите два случая.

Случай I. Агент достиг ограничения времени, но не терминального состояния. Среда возвращает финальное наблюдение и затем выполняет reset().

Случай II. Сборщик заполнил фиксированный буфер роллаутов. Эпизод не завершился, среда не сбрасывается, но следующего преимущества в текущем массиве уже нет.

Для каждого случая ответьте:

  1. Нужно ли использовать V(st+1)V(s_{t+1}) в TD-ошибке?

  2. Нужно ли переносить обратную рекурсию на следующий элемент текущего массива?

  3. Какие значения имеют terminated и trace_cut?

  4. Почему один флаг done недостаточен?

Показать ответ и решение

Случай I — техническое усечение с последующим reset().

  1. Да. Продолжение в математической задаче существует, поэтому используется ценность последнего наблюдения до сброса.

  2. Нет. Следующий элемент после reset() принадлежит другому эпизоду.

  3. terminated=False, trace_cut=True.

Случай II — граница локального буфера.

  1. Да. Среда продолжает тот же эпизод, поэтому следующий прогноз нужен для бутстрэппинга.

  2. Нет внутри данного массива, если следующий фрагмент не присоединён к вычислению GAE. Потоковая реализация могла бы перенести накопитель между фрагментами.

  3. terminated=False, trace_cut=True для последней записи локального массива.

Один done смешивает два вопроса: существует ли ценность продолжения и имеется ли следующее преимущество в текущей обратной рекурсии. Если занулять бутстрэппинг при каждом done, цели около временных ограничений и границ буфера смещаются: при положительной ценности продолжения цели занижаются, при отрицательной — завышаются.

Четыре области клиппированной цели PPO

Для ε=0.2\varepsilon=0.2 заполните таблицу. Укажите, активна ли клиппированная ветвь, и вычислите пессимистический член min⁡(rA,clip⁡(r)A)\min(rA,\operatorname{clip}(r)A).

rrAAКлиппирование меняет цель?Пессимистический член
1.42??
0.62??
1.4-2??
0.6-2??

После таблицы объясните, почему ratio_outside_fraction может равняться 1, а active_clip_fraction — 0.5.

Показать ответ и решение

При ε=0.2\varepsilon=0.2 интервал равен [0.8,1.2][0.8,1.2].

rrAAКлиппирование меняет цель?Пессимистический член
1.42да1.2⋅2=2.41.2\cdot2=2.4
0.62нет0.6⋅2=1.20.6\cdot2=1.2
1.4-2нет1.4⋅(−2)=−2.81.4\cdot(-2)=-2.8
0.6-2да0.8⋅(−2)=−1.60.8\cdot(-2)=-1.6

Все четыре отношения находятся вне интервала, поэтому ratio_outside_fraction=1. Клиппирование создаёт пессимистический минимум только в первой и четвёртой строках, поэтому active_clip_fraction=2/4=0.5.

RLOO, GRPO и распределение вклада между токенами

Сформулируйте:

  1. одну общую идею RLOO и GRPO;

  2. два существенных различия между их базовыми формулировками;

  3. почему один коэффициент уровня полного ответа, применённый ко всем токенам, даёт корректный градиент награды за полный ответ;

  4. почему тот же приём не решает задачу распределения вклада между токенами.

Показать ответ и решение
  1. Общая идея — несколько ответов на один запрос и относительная оценка внутри группы без обязательного отдельного критика.

  2. Базовый RLOO использует среднее с исключением текущего ответа и цель REINFORCE; канонический GRPO использует PPO-подобные отношения и клиппирование, обычно нормирует групповой сигнал и добавляет регуляризацию относительно базовой политики.

  3. Логарифм вероятности ответа равен сумме логарифмов вероятностей токенов. Поэтому один коэффициент ответа, умноженный на эту сумму, задаёт градиент логарифма вероятности полной последовательности.

  4. Коэффициент одинаков для всех токенов и не сообщает, какой локальный выбор вызвал награду. Он оптимизирует цель, заданную на уровне полного ответа, но не локализует причинный вклад.

Градиент логарифма softmax

Для логитов

z=(1,2,3)z=(1,2,3)

и выбранного действия a=2a=2 вычислите

∇zlog⁡π(a)=ea−softmax⁡(z).\nabla_z\log\pi(a)=e_a-\operatorname{softmax}(z).

Дайте ответ с точностью не хуже 10−610^{-6} и проверьте, что сумма компонент равна нулю.

Показать ответ и решение

Сначала

softmax⁡(1,2,3)≈(0.09003057,0.24472847,0.66524096).\operatorname{softmax}(1,2,3) \approx (0.09003057,0.24472847,0.66524096).

Для действия 2:

e2−π≈(−0.09003057,−0.24472847,0.33475904).e_2-\pi \approx (-0.09003057,-0.24472847,0.33475904).

Сумма компонент с учётом округления равна нулю.

Награда от текущего шага и внешний дисконт

Для наград

(r1,r2,r3)=(1,2,3)(r_1,r_2,r_3)=(1,2,3)

и γ=0.5\gamma=0.5 вычислите:

  1. вектор GtG_t;

  2. вектор γtGt\gamma^tG_t для t=0,1,2t=0,1,2.

Поясните, какую цель представляет второй вектор.

Показать ответ и решение

Обратная рекурсия:

G2=3,G_2=3, G1=2+0.5⋅3=3.5,G_1=2+0.5\cdot3=3.5, G0=1+0.5⋅3.5=2.75.G_0=1+0.5\cdot3.5=2.75.

Итак,

G=(2.75,3.5,3).G=(2.75,3.5,3).

Внешний дисконт:

γtGt=(2.75,1.75,0.75).\gamma^tG_t=(2.75,1.75,0.75).

Второй вектор соответствует прямому градиенту буквальной цели с весами γt\gamma^t для наград от начала эпизода.

Ценность и оптимальный скалярный опорный уровень

Политика имеет логиты (2,0)(2,0), а значения действий равны (1,4)(1,4).

  1. Вычислите вероятности действий.

  2. Вычислите Vπ=∑aπ(a)Q(a)V^\pi=\sum_a\pi(a)Q(a).

  3. Для каждого действия найдите ∥∇zlog⁡π(a)∥2\|\nabla_z\log\pi(a)\|^2.

  4. Вычислите

b∗=∑aπ(a)Q(a)∥∇log⁡π(a)∥2∑aπ(a)∥∇log⁡π(a)∥2.b^* = \frac{\sum_a\pi(a)Q(a)\|\nabla\log\pi(a)\|^2} {\sum_a\pi(a)\|\nabla\log\pi(a)\|^2}.

Почему VπV^\pi и b∗b^* различаются?

Показать ответ и решение

Вероятности:

π=softmax⁡(2,0)≈(0.88079708,0.11920292).\pi = \operatorname{softmax}(2,0) \approx (0.88079708,0.11920292).

Ценность:

Vπ=0.88079708⋅1+0.11920292⋅4≈1.35760877.V^\pi =0.88079708\cdot1+0.11920292\cdot4 \approx1.35760877.

Градиенты логарифмических вероятностей:

g0=(1−p0,−p1)=(0.11920292,−0.11920292),g_0=(1-p_0,-p_1)=(0.11920292,-0.11920292), g1=(−p0,1−p1)=(−0.88079708,0.88079708).g_1=(-p_0,1-p_1)=(-0.88079708,0.88079708).

Их квадраты норм:

∥g0∥2≈0.02841867,∥g1∥2≈1.55160699.\|g_0\|^2\approx0.02841867, \qquad \|g_1\|^2\approx1.55160699.

После подстановки:

b∗≈3.64239123.b^*\approx3.64239123.

Различие возникает потому, что b∗b^* взвешивает значения действий квадратом нормы градиента логарифма вероятности. Редкое действие 1 имеет значительно больший градиентный вектор и поэтому получает больший вес, чем в обычном VπV^\pi.

Групповые преимущества

Для наград

R=(1,3,8,2)R=(1,3,8,2)

вычислите:

  1. общее среднее;

  2. центрированные преимущества Ri−RˉR_i-\bar R;

  3. опорные уровни с исключением текущего элемента;

  4. RLOO-преимущества;

  5. коэффициент пропорциональности между двумя векторами преимуществ;

  6. ожидаемый масштаб оценивателя с общим средним для группы размера 4.

Показать ответ и решение

Общее среднее:

Rˉ=1+3+8+24=3.5.\bar R=\frac{1+3+8+2}{4}=3.5.

Центрированные преимущества:

R−Rˉ=(−2.5,−0.5,4.5,−1.5).R-\bar R=(-2.5,-0.5,4.5,-1.5).

Опорные уровни с исключением текущего элемента:

(133,113,2,4)≈(4.333333,3.666667,2,4).\left(\frac{13}{3},\frac{11}{3},2,4\right) \approx(4.333333,3.666667,2,4).

RLOO-преимущества:

(−103,−23,6,−2)≈(−3.333333,−0.666667,6,−2).\left(-\frac{10}{3},-\frac{2}{3},6,-2\right) \approx(-3.333333,-0.666667,6,-2).

Они равны 4/34/3 от центрированных преимуществ. Ожидаемый масштаб оценки с общим средним равен

G−1G=34.\frac{G-1}{G}=\frac{3}{4}.

GAE при техническом усечении

Даны два независимых перехода в массиве. Первый завершает эпизод из-за ограничения времени; второй относится к другому эпизоду:

r=(1,100),V=(2,0),Vnext=(10,0).r=(1,100),\quad V=(2,0),\quad V_{\text{next}}=(10,0).

Маски:

terminated=(0,1),episode_end=(1,1).\mathrm{terminated}=(0,1),\qquad \mathrm{episode\_end}=(1,1).

При γ=0.9\gamma=0.9 и λ=1\lambda=1 вычислите TD-ошибки и GAE-преимущества. Объясните, почему награда 100 не входит в преимущество первого перехода.

Показать ответ и решение

Первый переход усечён, но не терминален:

δ0=1+0.9⋅10−2=8.\delta_0 =1+0.9\cdot10-2=8.

Второй переход терминален:

δ1=100+0−0=100.\delta_1 =100+0-0=100.

Поскольку episode_end[0]=1, рекурсия не переносит δ1\delta_1 назад:

A0=8,A1=100.A_0=8, \qquad A_1=100.

Награда 100 относится к другому эпизоду после reset(). Она соседняя только в массиве, но не в траектории.

Члены PPO и выборочная диагностика KL

Пусть

r=(1.5,0.5,1.5,0.5),A=(2,2,−2,−2),ε=0.2.r=(1.5,0.5,1.5,0.5), \qquad A=(2,2,-2,-2), \qquad \varepsilon=0.2.

Вычислите:

  1. неклиппированные члены rArA;

  2. клиппированные члены;

  3. пессимистические члены;

  4. среднюю цель PPO;

  5. долю отношений вне интервала;

  6. долю примеров, где клиппирование изменяет цель;

  7. выборочную диагностику

D^=1N∑i[(ri−1)−log⁡ri].\widehat D = \frac{1}{N}\sum_i[(r_i-1)-\log r_i].
Показать ответ и решение

Неклиппированные члены:

rA=(3,1,−3,−1).rA=(3,1,-3,-1).

Клиппированные отношения равны (1.2,0.8,1.2,0.8)(1.2,0.8,1.2,0.8), поэтому соответствующие члены:

(2.4,1.6,−2.4,−1.6).(2.4,1.6,-2.4,-1.6).

Покомпонентный минимум:

(2.4,1,−3,−1.6).(2.4,1,-3,-1.6).

Средняя цель:

2.4+1−3−1.64=−0.3.\frac{2.4+1-3-1.6}{4}=-0.3.

Все отношения вне интервала, поэтому ratio_outside_fraction=1. Клиппирование меняет минимум в двух из четырёх случаев, поэтому active_clip_fraction=0.5.

Для двух значений r=1.5r=1.5 вклад равен 0.5−log⁡1.5≈0.0945350.5-\log 1.5\approx0.094535, а для двух значений r=0.5r=0.5 — −0.5−log⁡0.5≈0.193147-0.5-\log0.5\approx0.193147. Поэтому

D^≈2⋅0.094535+2⋅0.1931474=0.143841.\widehat D \approx \frac{2\cdot0.094535+2\cdot0.193147}{4} = 0.143841.

Это неотрицательная выборочная диагностика. Она совпадает с обратной KL в математическом ожидании по старой политике при требуемом совпадении носителей, но один конечный пакет не превращает её в точное KL-ограничение.

Устойчивый softmax и градиент логарифма вероятности

Реализуйте:

Python
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];
  • сумма градиента равна нулю с абсолютной погрешностью не более 10−1210^{-12};
  • некорректный индекс действия вызывает ValueError.

Открытые проверки:

Python
np.testing.assert_allclose(stable_softmax([1000.0, 999.0]), [0.7310585786, 0.2689414214], rtol=2e-10)
Показать ответ и решение
Python
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, но предотвращает переполнение экспоненты.

Накопленная награда от текущего шага и две договорённости о дисконтировании

Реализуйте:

Python
def reward_to_go(rewards: np.ndarray, gamma: float) -> np.ndarray:
    ...

def discounted_score_weights(rewards: np.ndarray, gamma: float) -> np.ndarray:
    ...

Первый результат содержит GtG_t, второй — γtGt\gamma^tG_t. Алгоритм должен работать за O(T)O(T) времени и не использовать вложенные циклы.

Открытые проверки:

Python
np.testing.assert_allclose(reward_to_go([1.0, 2.0, 3.0], 0.5), [2.75, 3.5, 3.0])
Показать ответ и решение
Python
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

Один обратный проход даёт сложность O(T)O(T) и постоянную дополнительную память помимо результата.

Среднее, исключение текущего элемента и внутригрупповая нормировка

Реализуйте:

Python
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.

Открытые проверки:

Python
np.testing.assert_allclose(leave_one_out_baselines([1.0, 3.0, 8.0, 2.0]), [13/3, 11/3, 2.0, 4.0])
Показать ответ и решение
Python
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

Использована «популяционная» внутригрупповая дисперсия с делением на GG, что соответствует распространённой нормировке преимуществ.

GAE с раздельными масками бутстрэппинга и рекурсии

Реализуйте обратную рекурсию:

Python
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.

Открытые проверки:

Python
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])
Показать ответ и решение
Python
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

Реализуйте:

Python
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.

Отношения должны вычисляться из разности логарифмических вероятностей.

Открытые проверки:

Python
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])
Показать ответ и решение
Python
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 проверяет не положение отношения само по себе, а изменение пессимистического минимума.

Маска ответа, вероятность последовательности и токеновые преимущества

Реализуйте три функции:

Python
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 включается, позиции после него исключаются. Вторая суммирует логарифмические вероятности токенов ответа. Третья распространяет одно преимущество полного ответа на ненулевые позиции маски.

Открытая проверка:

Python
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],
])
Показать ответ и решение
Python
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

Возвращаемая маска сдвинута относительно исходных токенов, потому что логиты позиции kk оценивают метку позиции k+1k+1. Сгенерированный EOS является действием политики и включается; запрос, PAD и хвост после EOS не входят в функцию потерь. Одинаковое преимущество на ненулевых позициях реализует градиент полного ответа, но не превращается в локальное объяснение вклада токенов.