Trust Region и PPO
- О чём эта тема
- Финал траектории — алгоритм, на котором сегодня держится значительная часть практического RL, включая дообучение языковых моделей: Proximal Policy Optimization. Путь к нему: переиспользование чужих данных через importance ratio, опасность больших шагов, доверительный регион — и знаменитый клип, заменяющий всю эту машинерию тремя строками кода. По лекции 6 курса «RL: от бандитов до RLHF» (мехмат МГУ).
- Аннотация
- Конспект начинается с главной неэффективности policy-методов: после каждого шага оптимизатора собранные траектории устаревают. Суррогатная функция с importance ratio позволяет несколько раз учиться на одном батче — но у неё, как доказывается, короткий поводок: она приближает истинную цель лишь пока новая политика близка к старой, и граница Шульмана превращает «близка» в строгий KL-термин. TRPO решает задачу с KL-ограничением честно и дорого; PPO заменяет ограничение клипированием importance ratio, и теорема о трёх случаях показывает, почему это работает: у выгодных действий срезается потолок, у невыгодных — пол, а в окрестности старой политики градиент не искажается. Тренажёр рисует клип-цель как функцию ratio при разных знаках преимущества. Дальше собирается полный функционал PPO — клип-цель, потеря критика, энтропийный бонус — и обобщённая оценка преимущества GAE, интерполирующая между TD и Монте-Карло. Практика — наш PPO на CartPole: три поколения policy-методов этой траектории на одном графике. Финал — мостик во вторую ступень: как PPO дообучает языковые модели.
- Пререквизиты
- Конспект 10 (актёр-критик, преимущество, TD-ошибка как его оценка), конспект 9 (градиент политики, on-policy), конспект 4 (важностная выборка — она возвращается!, TD против Монте-Карло), конспект 2 (разведка).
- Мотивация
- Посчитаем, куда уходит время актёра-критика: сыграть эпизод — тысячи прогонов сети, один градиентный шаг — и всё выбросить, потому что политика изменилась и данные «протухли». Обидно до невозможности: в DQN каждый переход работал сотни раз. Нельзя ли и здесь учиться на батче несколько эпох? Можно — если честно пересчитывать вклад чужих траекторий (важностная выборка из конспекта 4) и не уходить далеко от политики, которая их собирала. Из этих двух «если» выросли TRPO и PPO — второй стал алгоритмом по умолчанию везде, от игр до обучения ассистентов вроде того, с которым вы, возможно, сейчас разговариваете.
1. Чужие данные: importance ratio
Пусть батч траекторий собран политикой \(\pi_{\theta_{\text{old}}}\), а обновлять мы хотим уже новую \(\pi_\theta\). Знакомая по конспекту 4 ситуация off-policy оценивания, и лекарство то же — важностная выборка. Определим importance ratio (отношение правдоподобий) для каждого шага:
Число прозрачное: \(r_t > 1\) — новая политика делает это действие охотнее старой, \(r_t < 1\) — реже; при \(\theta = \theta_{\text{old}}\) все \(r_t = 1\). Из него строится суррогатная функция — замена настоящей цели \(J(\theta)\), вычислимая по старым данным:
где \(\hat A_t\) — оценка преимущества из конспекта 10. Почему это честная замена? В точке \(\theta = \theta_{\text{old}}\) градиенты совпадают: продифференцировав (11.1), получаем \(\nabla_\theta r_t \big|_{\theta_{\text{old}}} = \nabla_\theta \log \pi_\theta\), и \(\nabla L\) превращается в знакомый градиент политики с весом-преимуществом (конспект 10, формула (10.3)). Значит, первый шаг по суррогату — это в точности шаг актёра-критика; вопрос в том, что происходит на втором, третьем, десятом шаге по тем же данным.
1.1. Проблема больших шагов
А происходит вот что: суррогат приближает цель, лишь пока распределение посещаемых состояний новой политики похоже на старое. Теория монотонного улучшения политик (Шульман и др., 2015) даёт точную нижнюю границу:
где \(C > 0\) зависит от \(\gamma\) и максимума награды, а KL — дивергенция Кульбака–Лейблера, мера «непохожести» распределений действий (не метрика: несимметрична и без неравенства треугольника, но нулевая тогда и только тогда, когда политики совпадают). Читается граница так: пока новая политика близка к старой (KL мал), рост суррогата гарантирует рост настоящей цели; стоит уйти далеко — граница становится тривиальной, и суррогат может расти, пока \(J\) падает. Окрестность старой политики, внутри которой суррогату можно верить, называется доверительным регионом (англ. trust region).
Алгоритм TRPO так и поступает: максимизирует \(L(\theta)\) при явном ограничении \(\mathrm{KL} \le \delta\). Честно — и дорого: нужен гессиан и условная оптимизация, что для сетей с миллионами параметров мучительно. PPO предлагает аппроксимацию, умещающуюся в три строки.
2. Клип: доверительный регион за три строки
Идея Proximal Policy Optimization: не ограничивать KL явно, а обрезать сам ratio — лишить оптимизатор стимула уводить политику далеко. Обозначив \(\operatorname{clip}(x, a, b)\) — прижатие числа \(x\) к отрезку \([a, b]\), — запишем клипированную цель:
где \(\varepsilon\) — гиперпараметр, обычно от 0,1 до 0,3.
Теорема 11.1 (клип по случаям). Для каждого слагаемого (11.4):
- при \(\hat A_t > 0\) цель перестаёт расти, как только \(r_t > 1 + \varepsilon\);
- при \(\hat A_t < 0\) цель перестаёт убывать, как только \(r_t < 1 - \varepsilon\);
- при \(\theta = \theta_{\text{old}}\) (то есть \(r_t = 1\)) градиент \(L^{\text{CLIP}}\) совпадает с градиентом суррогата (11.2).
Доказательство. Обозначим \(r = r_t\) и сравним две функции под минимумом: \(f_1(r) = r \hat A_t\) и \(f_2(r) = \operatorname{clip}(r, 1{-}\varepsilon, 1{+}\varepsilon)\, \hat A_t\). Случай \(\hat A_t > 0\): \(f_1\) растёт, а \(f_2\) при \(r > 1{+}\varepsilon\) — константа \((1{+}\varepsilon)\hat A_t\); минимум при \(r \le 1{+}\varepsilon\) равен обеим, дальше — константе, и градиент по \(r\) зануляется: наращивать вероятность выгодного действия сверх порога незачем. Случай \(\hat A_t < 0\): \(f_1\) убывает, \(f_2\) при \(r < 1{-}\varepsilon\) — константа \((1{-}\varepsilon)\hat A_t\); минимум совпадает с \(f_1\) при \(r \ge 1{-}\varepsilon\) и с константой ниже порога — градиент зануляется: душить невыгодное действие сильнее порога тоже незачем. Случай \(r = 1\): точка лежит строго внутри \([1{-}\varepsilon, 1{+}\varepsilon]\), клип не активен, цели и градиенты совпадают. \(\blacksquare\)
Геометрически \(L^{\text{CLIP}}\) — кусочно-линейная функция от \(r\): у выгодных действий срезан потолок, у невыгодных — пол; за счёт минимума это всегда пессимистическая нижняя оценка суррогата, отсюда консервативность обновления. Убедитесь в каждом случае теоремы сами:
Подвигайте ползунок при \(\hat A = +1\): внутри коридора \([1{-}\varepsilon, 1{+}\varepsilon]\) клип-цель повторяет прямую, за правым порогом — полка: расти дальше оптимизатору незачем, вероятность действия за один цикл не вырастет более чем в \(1{+}\varepsilon\) раз. Переключите на \(\hat A = -1\): полка возникает слева — то же ограничение на подавление. Заметьте асимметрию, которую часто упускают: при \(\hat A > 0\) движение влево (ухудшение) не клипируется — минимум оставляет честную прямую, пессимизм работает только против чрезмерного оптимизма.
3. Полный функционал и GAE
Клип-цель отвечает только за актёра. Рабочий PPO оптимизирует сумму трёх слагаемых — по общим параметрам, если актёр и критик делят ствол сети (как ModelA2C из конспекта 10):
Второе слагаемое — критик, приближающий цель \(\hat V_t\). Третье — энтропийный бонус: энтропия \(H(\pi(\cdot|s)) = -\sum_a \pi(a|s) \log \pi(a|s)\) измеряет «размазанность» распределения действий, и её поощрение мешает политике преждевременно схлопнуться в детерминированную — та же забота о разведке, что ε-жадность в конспекте 2 и обучаемая дисперсия в конспекте 10, только третьим способом.
Осталось сказать, откуда берутся \(\hat A_t\) и \(\hat V_t\). Стандарт — обобщённая оценка преимущества (Generalized Advantage Estimation, GAE): экспоненциально взвешенная сумма TD-ошибок критика
а цель критика — \(\hat V_t = \hat A_t + V_\varphi(S_t)\). Параметр \(\lambda\) — старый знакомый: при \(\lambda = 0\) остаётся одна TD-ошибка (минимум дисперсии, максимум смещения — актёр-критик конспекта 10), при \(\lambda = 1\) сумма телескопируется в монте-карловский возврат минус \(V\) (несмещённо и шумно — REINFORCE с baseline). Это в третий раз спектр «TD против Монте-Карло» из конспекта 4 — теперь в виде ручки, которую крутят: на практике \(\lambda = 0{,}9\)–\(0{,}97\).
- собрать батч траекторий политикой \(\pi_\theta\); зафиксировать \(\theta_{\text{old}} \leftarrow \theta\);
- по каждому переходу вычислить \(\hat A_t\) (GAE) и \(\hat V_t\);
- нормализовать \(\hat A_t\) по батчу (вычесть среднее, поделить на разброс);
- K эпох градиентного спуска по (11.5), пересчитывая \(r_t(\theta)\) на каждом шаге;
- вернуться к шагу 1 со свежими данными.
Шаг 4 — то, ради чего всё затевалось: одни и те же данные прокручиваются K раз, и клип страхует от ухода из доверительного региона. Из каждого собранного эпизода PPO извлекает в K раз больше обучения, чем актёр-критик с его одним проходом, — а по простоте несравним с TRPO.
log_prob старой политики на каждой эпохе заново — от текущей
сети. Тогда \(r_t \equiv 1\), клип никогда не активен, и «PPO» тихо вырождается в
обычный актёр-критик с K-кратным переобучением на батче. Логарифмы вероятностей старой
политики фиксируются в момент сбора данных (detach/no_grad)
и в эпохах не пересчитываются.
4. Практика: три поколения на одной задаче
Замкнём линию, начатую в конспекте 9. Тот же CartPole, те же 128 нейронов; PPO собран по псевдокоду выше — общий ствол, две головы, GAE, батч из четырёх эпизодов, четыре эпохи на батч. Все три запуска — наши, при подготовке конспектов:
Результат стоит прочесть внимательно, потому что он честнее рекламного. Кривая PPO — самая гладкая из трёх, и, выйдя на максимум, он держит его железно: с трёхсотой итерации каждый батч — ровно 500 шагов, без единого срыва (у REINFORCE и актёра-критика провалы случаются до самого конца). Но по числу эпизодов на игрушечном CartPole PPO не выигрывает — он тратит их батчами. Его экономия — в другом: каждый собранный батч прокручивается четыре эпохи, так что на один сбор данных приходится вчетверо больше обучения; на задачах, где эпизод дорог — робот, симулятор посложнее, генерация текста, — эта арифметика решает. И главное его достоинство — не скорость, а предсказуемость: клип не даёт политике разрушить саму себя одним неудачным шагом (вспомните коллапс актёра-критика из конспекта 10 — с PPO такой сценарий устроить трудно).
А теперь обещанный должок конспекта 10 — маятник, который ванильный актёр-критик так и не поднял. Тот же PPO, только политика гауссова (конспект 10, формула (10.7)) с обучаемым разбросом:
Итог нашего запуска (400 итераций и ещё 400 со сниженным до 10⁻⁴ шагом — тот же приём «дожать аккуратнее», что у машинки в конспекте 7): средний возврат стохастической политики вырос с −1450 до −500 с лишним; детерминированная (по средним μ) версия набирает −724 в среднем по 20 эпизодам, а из верхней полусферы держит вертикаль почти идеально — лучший эпизод −1,5. Это не «решённая» задача (эталон около −150: подъём снизу удаётся не из каждого положения), но контраст с актёром-критиком из конспекта 10, который за 3000 эпизодов не сдвинулся с −1450, — наглядный. Желающим дожать маятник до конца — увеличить батч, добавить нормализацию наблюдений или взять готовую реализацию из библиотек (Stable-Baselines3, AgileRL): архитектурно там тот же код, что перед вами.
Полный код для проектов: PPO с гауссовой политикой на Pendulum-v1
# ppo_pendulum.py — PPO с гауссовой политикой на Pendulum-v1. # Та же схема, что ppo_cartpole.py, но политика непрерывная (10.7)-(10.8). import numpy as np import torch import torch.nn as nn from torch.distributions import Normal import gymnasium as gym gamma, lam, clip_eps = 0.9, 0.95, 0.2 c_v, c_h, K_epochs, batch_episodes = 0.5, 0.01, 10, 8 env = gym.make("Pendulum-v1") torch.manual_seed(3); np.random.seed(3) nS, nA = env.observation_space.shape[0], env.action_space.shape[0] A_MAX = float(env.action_space.high[0]) class ActorCritic(nn.Module): def __init__(self, h=128): super().__init__() self.base = nn.Sequential(nn.Linear(nS, h), nn.ReLU()) self.mu = nn.Sequential(nn.Linear(h, nA), nn.Tanh()) self.logstd = nn.Parameter(torch.zeros(nA)) # обучаемый лог-разброс self.v = nn.Linear(h, 1) def dist_v(self, x): y = self.base(x) return Normal(self.mu(y) * A_MAX, torch.exp(self.logstd)), self.v(y).squeeze(-1) model = ActorCritic() optimizer = torch.optim.Adam(model.parameters(), lr=0.0003) def collect_batch(): S, A, LOGP, ADV, VTARG, totals = [], [], [], [], [], [] for _ in range(batch_episodes): state, _ = env.reset() states, actions, logps, rewards, values = [], [], [], [], [] total = 0.0 for t in range(200): s = torch.from_numpy(state).float() with torch.no_grad(): dist, v = model.dist_v(s) a = dist.sample() states.append(s); actions.append(a) logps.append(dist.log_prob(a).sum()); values.append(v) state, r, term, trunc, _ = env.step(np.clip(a.numpy(), -A_MAX, A_MAX)) rewards.append(r / 8.0) total += r if term or trunc: break totals.append(total) values = torch.stack(values) adv, gae = torch.zeros(len(rewards)), 0.0 for t in reversed(range(len(rewards))): v_next = values[t + 1] if t + 1 < len(rewards) else 0.0 delta = rewards[t] + gamma * v_next - values[t] gae = delta + gamma * lam * gae adv[t] = gae S += states; A += actions; LOGP += logps ADV.append(adv); VTARG.append(adv + values) ADV = torch.cat(ADV); VTARG = torch.cat(VTARG) ADV = (ADV - ADV.mean()) / (ADV.std() + 1e-8) return (torch.stack(S), torch.stack(A), torch.stack(LOGP), ADV, VTARG, float(np.mean(totals))) history = [] for it in range(1, 401): S, A, LOGP_OLD, ADV, VTARG, mean_ret = collect_batch() for _ in range(K_epochs): dist, v = model.dist_v(S) logp = dist.log_prob(A).sum(-1) ratio = torch.exp(logp - LOGP_OLD) surr1 = ratio * ADV surr2 = torch.clamp(ratio, 1 - clip_eps, 1 + clip_eps) * ADV loss = (-torch.min(surr1, surr2).mean() + c_v * ((v - VTARG) ** 2).mean() - c_h * dist.entropy().sum(-1).mean()) optimizer.zero_grad(); loss.backward(); optimizer.step() history.append(mean_ret) if it % 20 == 0: print(f"итерация {it:4d} средний возврат батча {mean_ret:8.1f} " f"за 10 итераций {np.mean(history[-10:]):8.1f}", flush=True) np.save(r"C:\Users\Evgenie\AppData\Local\Temp\ppo_pend_history.npy", np.array(history)) torch.save(model.state_dict(), r"C:\Users\Evgenie\AppData\Local\Temp\ppo_pend_model.pt")
Полный код для проектов: PPO на CartPole (PyTorch + Gymnasium, один файл)
# ppo_cartpole.py — PPO с клипированной целью на CartPole-v1, один файл. # Батч из нескольких эпизодов -> GAE -> K эпох оптимизации функционала (11.6). import numpy as np import torch import torch.nn as nn from torch.distributions import Categorical import gymnasium as gym gamma, lam = 0.99, 0.95 # дисконт и параметр GAE clip_eps = 0.2 # epsilon клипа c_v, c_h = 0.5, 0.01 # веса потерь критика и энтропийного бонуса K_epochs = 4 # эпох оптимизации на одном батче batch_episodes = 4 # эпизодов в батче env = gym.make("CartPole-v1") torch.manual_seed(1); np.random.seed(1) nS, nA = env.observation_space.shape[0], env.action_space.n class ActorCritic(nn.Module): """Общий ствол, две головы: логиты политики и ценность.""" def __init__(self): super().__init__() self.base = nn.Sequential(nn.Linear(nS, 128), nn.ReLU()) self.pi = nn.Linear(128, nA) self.v = nn.Linear(128, 1) def forward(self, x): y = self.base(x) return self.pi(y), self.v(y).squeeze(-1) model = ActorCritic() optimizer = torch.optim.Adam(model.parameters(), lr=0.003) def collect_batch(): """Играем batch_episodes эпизодов текущей политикой, считаем GAE.""" S, A, LOGP, ADV, VTARG, lengths = [], [], [], [], [], [] for _ in range(batch_episodes): state, _ = env.reset() states, actions, logps, rewards, values = [], [], [], [], [] for t in range(1000): s = torch.from_numpy(state).float() with torch.no_grad(): logits, v = model(s) c = Categorical(logits=logits) a = c.sample() states.append(s); actions.append(a) logps.append(c.log_prob(a)); values.append(v) state, r, term, trunc, _ = env.step(int(a)) rewards.append(r) if term or trunc: break lengths.append(len(rewards)) # GAE (11.4): A_t = sum (gamma*lam)^l * delta_{t+l}; V-цель = A + V values = torch.stack(values) adv, gae = torch.zeros(len(rewards)), 0.0 for t in reversed(range(len(rewards))): v_next = values[t + 1] if t + 1 < len(rewards) else 0.0 # term: V=0 delta = rewards[t] + gamma * v_next - values[t] gae = delta + gamma * lam * gae adv[t] = gae S += states; A += actions; LOGP += logps ADV.append(adv); VTARG.append(adv + values) ADV = torch.cat(ADV); VTARG = torch.cat(VTARG) ADV = (ADV - ADV.mean()) / (ADV.std() + 1e-8) # нормировка advantage return (torch.stack(S), torch.stack(A), torch.stack(LOGP), ADV, VTARG, float(np.mean(lengths))) running, history = 10.0, [] for it in range(1, 501): S, A, LOGP_OLD, ADV, VTARG, mean_len = collect_batch() for _ in range(K_epochs): # K эпох на одном батче logits, v = model(S) c = Categorical(logits=logits) ratio = torch.exp(c.log_prob(A) - LOGP_OLD) # r_t(theta) surr1 = ratio * ADV surr2 = torch.clamp(ratio, 1 - clip_eps, 1 + clip_eps) * ADV loss = (-torch.min(surr1, surr2).mean() # -L^CLIP + c_v * ((v - VTARG) ** 2).mean() # критик - c_h * c.entropy().mean()) # энтропийный бонус optimizer.zero_grad(); loss.backward(); optimizer.step() history += [mean_len] * batch_episodes running = running * 0.9 + mean_len * 0.1 if it % 10 == 0: print(f"итерация {it:4d} средняя длина батча {mean_len:6.1f} скользящая {running:6.1f}", flush=True) if running > 475: print(f"Решено на итерации {it} (~{it * batch_episodes} эпизодов): скользящая {running:.1f}") break np.save(r"C:\Users\Evgenie\AppData\Local\Temp\ppo_history.npy", np.array(history))
Для промышленных задач писать PPO руками не обязательно: готовые реализации есть в библиотеках экосистемы Gymnasium (Stable-Baselines3, AgileRL — туториал «On-Policy Training» из материалов курса показывает обучение PPO в несколько строк). Но однажды собранный своими руками клип запомнится лучше любой документации.
5. Куда это ведёт: PPO и языковые модели
Закончим тем, с чего траектория начиналась, — мотивацией из конспекта 1 про системы,
которые «учатся у результата». Самое громкое применение PPO сегодня — дообучение больших
языковых моделей на человеческих предпочтениях (RLHF). Приглядитесь: генерация текста —
это MDP, в котором состояние — запрос плюс уже написанные токены, действие — следующий
токен из словаря в десятки тысяч слов, эпизод — один ответ, а награду в конце выставляет
отдельная модель-оценщик, обученная на сравнениях ответов людьми. Всё, чем мы занимались
одиннадцать конспектов, переносится сюда дословно: value-методы отпадают из-за
чудовищного пространства действий (конспект 9), политика — трансформер с softmax по
словарю (тот же розыгрыш из Categorical), критик — value-голова поверх той
же сети (общий ствол, конспект 10), преимущество считается GAE, шаг ограничивается
клипом — плюс один новый ингредиент, KL-штраф к исходной модели, чтобы в погоне за
наградой не разучиться говорить по-человечески. Разбор этой кухни — RLHF, reward-модели,
DPO и GRPO — тема второй ступени курса; фундамент для неё у вас теперь есть
полностью.
Контрольные вопросы
-
r_t(θ) = π_θ(A_t|S_t)/π_θold(A_t|S_t) — во сколько раз новая политика охотнее делает то же действие. Дифференцируя, ∇r_t|θold = ∇π/π = ∇log π_θ, поэтому ∇L(θold) = E[∇log π · Â] — ровно градиент политики с весом-преимуществом (10.3). Первый шаг по суррогату — обычный шаг актёра-критика.
-
Суррогат считает по состояниям, посещённым старой политикой, а новая посещает другие. Граница Шульмана: J(θ) ≥ L(θ) − C·max_s KL(π_θ||π_θold). При малом KL рост L гарантирует рост J; при большом граница тривиальна — суррогат может расти, пока настоящая цель падает. Зона доверия суррогату — trust region.
-
При Â>0 цель перестаёт расти при r>1+ε — вероятность выгодного действия за цикл не вырастет более чем в 1+ε раз; при Â<0 перестаёт убывать при r<1−ε — подавление тоже ограничено; при r=1 клип не активен и градиент совпадает с суррогатным. Min делает клип-цель пессимистической нижней оценкой — ухудшения не клипируются.
-
−L^CLIP — обновление актёра с ограничением шага; c_v·(V−V̂)² — обучение критика (без него нет преимуществ); −c_H·H(π) — энтропийный бонус против преждевременного схлопывания политики в детерминированную (третье воплощение заботы о разведке после ε-жадности и обучаемой дисперсии).
-
Â_t = Σ (γλ)^l δ_{t+l}. При λ=0 остаётся одна TD-ошибка — минимальная дисперсия, смещение неидеального критика (актёр-критик TD(0)); при λ=1 сумма телескопируется в G_t − V(S_t) — несмещённый, но шумный Монте-Карло с baseline. λ≈0,9–0,97 — рабочий компромисс; это регулируемая версия спектра из конспекта 4.
-
Актёр-критик on-policy: после первого же шага данные принадлежат другой политике, и веса при ∇log π смещены. PPO взвешивает чужие переходы importance ratio (честная поправка важностной выборки) и клипом удерживает политику в доверительном регионе, где поправка достоверна. Отсюда кратный выигрыш в расходе эпизодов.
-
r_t станет тождественной единицей: клип никогда не активируется, ограничение шага исчезает, и метод вырождается в актёр-критик, К раз переобучающийся на устаревшем батче — с риском разрушить политику. Старые log_prob фиксируются при сборе данных и не трогаются.
-
Состояние — запрос и уже сгенерированные токены, действие — следующий токен (словарь 32–128 тыс.), эпизод — ответ целиком, награда — скаляр reward-модели в конце плюс пошаговый KL-штраф к исходной модели (чтобы не разучиться языку). Политика — трансформер, критик — value-голова, преимущество — GAE, шаг ограничен клипом: тот же PPO, что на CartPole.
Источники
- Schulman, J. Proximal Policy Optimization Algorithms / J. Schulman, F. Wolski, P. Dhariwal [et al.] // arXiv. — 2017. — URL: https://arxiv.org/abs/1707.06347 (дата обращения: 08.07.2026).
- Schulman, J. Trust Region Policy Optimization / J. Schulman, S. Levine, P. Moritz [et al.] // arXiv. — 2015. — URL: https://arxiv.org/abs/1502.05477 (дата обращения: 08.07.2026).
- Schulman, J. High-Dimensional Continuous Control Using Generalized Advantage Estimation / J. Schulman, P. Moritz, S. Levine [et al.] // arXiv. — 2015. — URL: https://arxiv.org/abs/1506.02438 (дата обращения: 08.07.2026).