Actor-Critic и непрерывные действия
- О чём эта тема
- Две ветки траектории соединяются: политика-актёр учится по градиенту из конспекта 9, а веса её шагам выставляет критик — функция ценности из конспектов 3–7. TD-ошибка оказывается несмещённой оценкой преимущества, обучение перестаёт ждать конца эпизода, а гауссова политика открывает дорогу к непрерывным действиям — вплоть до DDPG. По конспекту преподавателя и лекции 6 курса «RL: от бандитов до RLHF» (мехмат МГУ).
- Аннотация
- Конспект начинается с двух хронических болезней REINFORCE — обновление только по концу эпизода и шумный вес \(G_t\) — и лечит обе одним персонажем: критиком. Сначала формализуется преимущество \(A = Q - V\) и доказывается, что его среднее по политике равно нулю; затем — центральная теорема: TD-ошибка одного перехода является несмещённой оценкой преимущества, так что учить нужно одну лишь \(V_\varphi\). Отсюда собирается on-policy алгоритм актёр–критик с обновлением на каждом шаге, обсуждаются его смещение против дисперсии и условия сходимости. Практика — CartPole: критик как обучаемый baseline заметно ускоряет и разглаживает наш собственный REINFORCE из конспекта 9, кривые обоих запусков — на одном графике (включая честную историю о том, как первый запуск коллапсировал). Тренажёр показывает актёра и критика в четыре клетки коридора: видно, как растёт V, как TD-ошибка гонит вероятности к цели. Финал — непрерывные действия: гауссова политика с тремя головами (среднее, дисперсия, ценность), векторные среды для ускорения A2C и DDPG — детерминированный актёр с критикой Q, перенявший у DQN память переходов и целевые сети.
- Пререквизиты
- Конспект 9 (градиент политики, baseline, REINFORCE), конспект 4 (TD(0), TD-ошибка, смещение против дисперсии), конспект 7 (semi-gradient, проекционное уравнение Беллмана, replay buffer и целевая сеть), конспект 8 (преимущество A = Q − V из Dueling).
- Мотивация
- REINFORCE узнаёт о качестве действий только с финальным свистком: сыграй эпизод до конца — потом учись. Для CartPole это терпимо, для часовой игры или робота — нет. Вторая беда глубже: вес \(G_t\) шумит всей случайностью будущего, и мы уже знаем лучший успокоитель — вычесть \(V(S_t)\), — но взять её неоткуда. Выход напрашивается сам: у нас полконспекта траектории посвящено тому, как учить функции ценности. Пусть вторая сеть учит \(V\) на лету и подсказывает первой, насколько действие оказалось лучше ожидаемого. Сеть-политика действует — «актёр»; сеть-ценность оценивает — «критик».
1. Преимущество действия
Начнём с величины, которая уже дважды мелькала в траектории — в Dueling-сети (конспект 8, формула (8.2)) и в baseline конспекта 9. Преимущество (англ. advantage) действия \(a\) в состоянии \(s\) при политике \(\pi\):
Это «качество действия за вычетом качества места»: положительное преимущество — действие лучше среднего для этого состояния, отрицательное — хуже. Среднее преимущество по самой политике равно нулю:
где посередине — связь \(V = \sum_a \pi Q\) из конспекта 3 (формула (3.7)). Теперь возьмём градиент политики с baseline (конспект 9, формула (9.9)) и подставим лучший baseline \(b(s) = V^{\pi_\theta}(s)\) — в скобках возникает ровно преимущество:
Формула красива и бесполезна в лоб: чтобы знать \(A\), нужно знать и \(Q\), и \(V\) — две неизвестные функции вместо одной. Следующий раздел сводит их к одной, причём к самой дешёвой.
2. TD-ошибка — это и есть преимущество
Вспомним TD-ошибку из конспекта 4 (формула (4.10)), записанную для параметрического критика \(V_\varphi\):
Теорема 10.1 (TD-ошибка как оценка преимущества). Пусть критик идеален: \(V_\varphi = V^{\pi_\theta}\). Тогда для любого перехода
Доказательство. Условное ожидание первого и второго слагаемых (10.4) — это \(\mathbb{E}[R_{t+1} + \gamma V^{\pi}(S_{t+1}) \,|\, s, a]\). По определению \(Q^\pi(s,a) = \mathbb{E}[R_{t+1} + \gamma G_{t+1} | s, a]\), а по марковости \(\mathbb{E}[G_{t+1} | s, a] = \mathbb{E}[V^\pi(S_{t+1}) | s, a]\) (усреднение сначала по будущему при данном \(S_{t+1}\), затем по \(S_{t+1}\) — формула повторного ожидания). Значит, первое слагаемое равно \(Q^\pi(s, a)\), и \(\mathbb{E}[\delta_t | s, a] = Q^\pi(s,a) - V^\pi(s) = A^\pi(s,a)\). \(\blacksquare\)
Смысл теоремы трудно переоценить: однопереходная величина — награда плюс перепад оценок критика — в среднем равна преимуществу, для которого формально нужны две полные функции. Достаточно учить одну сеть \(V_\varphi\), а вместо \(A\) в (10.3) подставлять \(\delta_t\):
Честная оговорка: критик никогда не идеален, и при \(V_\varphi \ne V^{\pi}\) оценка (10.6) смещена. Это знакомая по конспекту 4 сделка «смещение против дисперсии» — та же, что между TD и Монте-Карло: REINFORCE несмещён и шумен, актёр-критик чуть смещён и спокоен. На практике даже грубый критик снижает дисперсию настолько, что небольшое смещение окупается с лихвой.
3. Алгоритм: актёр и критик учатся вместе
Собираем конструкцию. Актёр — политика \(\pi_\theta(a|s)\), обновляется в сторону роста \(J(\theta)\); критик — функция ценности \(V_\varphi(s)\), приближает \(V^{\pi_\theta}\). Один шаг среды — и оба получают по обновлению:
- разыграть действие \(A_t \sim \pi_\theta(\cdot | S_t)\), получить \(R_{t+1}, S_{t+1}\);
- вычислить TD-ошибку \(\delta_t = R_{t+1} + \gamma V_\varphi(S_{t+1}) - V_\varphi(S_t)\);
- обновить критика (TD(0), конспект 4): \(\varphi \leftarrow \varphi + \alpha_\varphi\, \delta_t \nabla_\varphi V_\varphi(S_t)\);
- обновить актёра (градиент политики): \(\theta \leftarrow \theta + \alpha_\theta\, \gamma^t \delta_t \nabla_\theta \log \pi_\theta(A_t | S_t)\).
Сравните с REINFORCE: там цикл начинался словами «сыграть полный эпизод», здесь обучение идёт на каждом шаге — как в TD-методах, и это не совпадение: шаг 3 — дословно полуградиентный TD(0) из конспекта 7, со всеми его свойствами. При фиксированном актёре критик — стохастическая аппроксимация оператора Беллмана и при линейной аппроксимации сходится к решению проекционного уравнения (конспект 7, формула (7.5)).
Слова «при фиксированном актёре» — не мелкий шрифт. На практике оба обновляются одновременно, и строгие гарантии требуют разделения масштабов времени: критик должен подстраиваться существенно быстрее, чем меняется актёр, — иначе актёр учится по оценкам вчерашней политики. Отсюда практическое правило: скорость обучения критика берут больше, чем актёра. Для нейросетей точных теорем нет, но конструкция устойчиво работает.
advantage = returns - values без values.detach() и суммируют
потери актёра и критика. Тогда «вес» при \(\log \pi\) сам получает градиент, и актёрская
часть ошибки начинает подгонять критика под действия вместо оценки политики.
Вес — константа для градиента актёра: только detach() (или
no_grad).
4. Практика: критик ускоряет наш REINFORCE
Проверим обещание «дисперсия упадёт — обучение ускорится» на собственном опыте. Возьмём REINFORCE с CartPole из конспекта 9 и добавим критика — вторую сеть, обучаемую приближать возврат; вес шага актёра сменится с нормированного \(G_t\) на преимущество \(G_t - V_\varphi(S_t)\) (вариант конспекта преподавателя: в роли \(Q\) — реальный возврат эпизода). Ядро кода:
actor = nn.Sequential(nn.Linear(nS, 128), nn.ReLU(), nn.Linear(128, nA)) critic = nn.Sequential(nn.Linear(nS, 128), nn.ReLU(), nn.Linear(128, 1)) # ... эпизод сыгран: log_probs, values, returns посчитаны ... advantage = returns - values.detach() # A = G - V; detach обязателен! actor_loss = -(torch.stack(log_probs) * advantage).sum() critic_loss = ((returns - values) ** 2).sum() # критик приближает возврат
Оба запуска — из этой траектории, одинаковые сети 128 нейронов, одинаковый порог «решено» (средняя длина 475):
Актёр-критик решил задачу на 543-м эпизоде против 625-го у REINFORCE, и по сглаженным кривым видно главное: он раньше выходит из «плато случайности» и растёт ровнее (разброс длин эпизодов на среднем участке обучения — 156 против 174 шагов). Ложка дёгтя из собственного опыта: первый запуск актёра-критика с равными скоростями обучения обеих сетей (0,01) к 850-му эпизоду коллапсировал — политика выродилась, длина упала до девяти шагов. Причина — нарушение правила из раздела 3: актёр менялся быстрее, чем критик успевал его оценивать. Лечение — снизить скорость актёра впятеро (0,002 против 0,01 у критика) и нормировать преимущество по эпизоду; с этими поправками кривая выше. Правило масштабов времени — не теоретическая вежливость, а условие выживания.
Полный код для проектов: актёр-критик на CartPole (PyTorch + Gymnasium, один файл)
# a2c_cartpole.py — актёр-критик с преимуществом (G - V) на CartPole-v1. # Актёр и критик — отдельные сети; advantage по конспекту: вес log pi = G_t - V(s_t). import numpy as np import torch import torch.nn as nn from torch.distributions import Categorical import gymnasium as gym gamma = 0.99 env = gym.make("CartPole-v1") torch.manual_seed(1); np.random.seed(1) nS, nA = env.observation_space.shape[0], env.action_space.n actor = nn.Sequential( # политика: логиты действий nn.Linear(nS, 128), nn.ReLU(), nn.Linear(128, nA)) critic = nn.Sequential( # критик: скалярная ценность V(s) nn.Linear(nS, 128), nn.ReLU(), nn.Linear(128, 1)) opt_actor = torch.optim.Adam(actor.parameters(), lr=0.002) # актёр медленнее критика! opt_critic = torch.optim.Adam(critic.parameters(), lr=0.01) running, history = 10.0, [] for episode in range(1, 1001): state, _ = env.reset(seed=episode) log_probs, values, rewards = [], [], [] for t in range(1, 1001): s = torch.from_numpy(state).float() c = Categorical(logits=actor(s)) a = c.sample() log_probs.append(c.log_prob(a)) values.append(critic(s).squeeze()) state, r, term, trunc, _ = env.step(int(a)) rewards.append(r) if term or trunc: break # возвраты G_t R, returns = 0.0, [] for r in rewards[::-1]: R = r + gamma * R returns.insert(0, R) returns = torch.tensor(returns) values = torch.stack(values) advantage = returns - values.detach() # A = G - V (критик как baseline) advantage = (advantage - advantage.mean()) / (advantage.std() + 1e-8) actor_loss = -(torch.stack(log_probs) * advantage).sum() critic_loss = ((returns - values) ** 2).sum() # критик учится приближать G opt_actor.zero_grad(); actor_loss.backward(); opt_actor.step() opt_critic.zero_grad(); critic_loss.backward(); opt_critic.step() history.append(t) running = running * 0.99 + t * 0.01 if episode % 50 == 0: print(f"эпизод {episode:4d} длина {t:4d} средняя {running:6.1f}", flush=True) if running > 475: print(f"Решено на эпизоде {episode}: средняя длина {running:.1f}") break np.save(r"C:\Users\Evgenie\AppData\Local\Temp\a2c_history.npy", np.array(history))
5. Тренажёр: актёр и критик в четыре клетки
Вся механика видна на задаче размером с ладонь: коридор из пяти клеток, старт слева, выход справа, каждый шаг стоит −1 — чем быстрее к выходу, тем лучше. Актёр — по логиту на клетку (вероятность шага вправо через сигмоиду), критик — по числу \(V\) на клетку. Обе таблички обновляются по алгоритму раздела 3, и всё происходящее печатается в журнал: \(\delta_t\) каждого шага, рост \(V\), сдвиг вероятностей.
За чем смотреть. Запустите анимированный эпизод: синий агент бродит по коридору, и каждая посещённая клетка вспыхивает цветом TD-ошибки — красным, когда переход хуже ожиданий критика, зелёным, когда лучше. Первые эпизоды почти сплошь красные: критик наивно ждёт нуля, а каждый шаг стоит −1, — столбики V растут вниз, к чёрным рискам-эталонам \(-(1-\gamma^{d})/(1-\gamma)\) точного решения. Нажмите «+10 мгновенно» пару раз: столбики доросли до рисок, вспышки позеленели у выхода, и стрелки политики вытянулись вправо — сначала в клетке 3, потом всё глубже. Это и есть механика раздела 3 в замедленной съёмке: критик проявляет структуру задачи, которой REINFORCE с его «одним возвратом на весь эпизод» не видит, — и передаёт её актёру через δ каждого шага.
6. Непрерывные действия: гауссова политика
До сих пор действий было конечное число, и сеть актёра заканчивалась softmax. Пусть теперь действие — вектор из \(m\) вещественных чисел: моменты на суставах робота, угол руля, крутящий момент маятника. Softmax по континууму не построишь, но политике и не нужно перечислять действия — достаточно уметь разыгрывать их и вычислять \(\log \pi\). Классический выбор — нормальное распределение по каждой компоненте:
где средние \(\mu_k(s)\) и дисперсии \(D_k(s)\) выдаёт сеть. Логарифм политики — в градиент он входит напрямую — считается в одну строку:
Сеть из конспекта преподавателя — общий ствол признаков и три головы: средние, дисперсии и ценность (актёр и критик в одном теле, как экстрактор Dueling в конспекте 8):
class ModelA2C(nn.Module): def __init__(self, n, m, h): # n - состояние, m - действие, h - скрытых super().__init__() self.base = nn.Sequential( # общие признаки состояния nn.Linear(n, h), nn.ReLU() ) self.mu = nn.Sequential( # средние действий: симметричный диапазон nn.Linear(h, m), nn.Tanh() ) self.var = nn.Sequential( # дисперсии: положительные -> Softplus nn.Linear(h, m), nn.Softplus() ) self.value = nn.Linear(h, 1) # ценность: любая -> без активации def forward(self, x): y = self.base(x) return self.mu(y), self.var(y), self.value(y)
Выбор активаций — маленький урок проектирования голов: \(\tanh\) на средних, потому что компоненты действий живут в симметричных пределах; Softplus («гладкий ReLU») на дисперсиях, потому что они обязаны быть положительными; ценность — без активации, ей можно всё. Заметьте, что дисперсия — обучаемая: политика сама решает, где действовать уверенно, а где продолжать разведку — стохастичность политики опять работает вместо ε-жадности.
Полный код для проектов: актёр-критик с гауссовой политикой на Pendulum-v1
# a2c_pendulum.py — актёр-критик с гауссовой политикой на Pendulum-v1. # Сеть ModelA2C по конспекту: общий ствол, головы mu (Tanh), var (Softplus), value. import numpy as np import torch import torch.nn as nn from torch.distributions import Normal import gymnasium as gym gamma = 0.9 # короткий горизонт Pendulum env = gym.make("Pendulum-v1") torch.manual_seed(2); np.random.seed(2) nS = env.observation_space.shape[0] # 3: cos, sin, скорость nA = env.action_space.shape[0] # 1: крутящий момент A_MAX = float(env.action_space.high[0]) # 2.0 class ModelA2C(nn.Module): def __init__(self, n, m, h=128): super().__init__() self.base = nn.Sequential(nn.Linear(n, h), nn.ReLU()) self.mu = nn.Sequential(nn.Linear(h, m), nn.Tanh()) # средние: [-1,1] self.var = nn.Sequential(nn.Linear(h, m), nn.Softplus()) # дисперсии > 0 self.value = nn.Linear(h, 1) def forward(self, x): y = self.base(x) return self.mu(y), self.var(y), self.value(y) model = ModelA2C(nS, nA) optimizer = torch.optim.Adam(model.parameters(), lr=0.0005) history = [] for episode in range(1, 3001): state, _ = env.reset(seed=episode) log_probs, values, rewards, entropies = [], [], [], [] total = 0.0 for t in range(200): s = torch.from_numpy(state).float() mu, var, v = model(s) dist = Normal(mu * A_MAX, torch.sqrt(var) + 1e-5) # политика (10.7) a = dist.sample() log_probs.append(dist.log_prob(a).sum()) # log pi (10.8) entropies.append(dist.entropy().sum()) values.append(v.squeeze()) 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 R, returns = 0.0, [] for r in rewards[::-1]: R = r + gamma * R returns.insert(0, R) returns = torch.tensor(returns, dtype=torch.float32) values = torch.stack(values) adv = returns - values.detach() adv = (adv - adv.mean()) / (adv.std() + 1e-8) loss = (-(torch.stack(log_probs) * adv).sum() + 0.5 * ((returns - values) ** 2).sum() - 0.001 * torch.stack(entropies).sum()) # бонус разведки optimizer.zero_grad(); loss.backward() nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() history.append(total) if episode % 200 == 0: print(f"эпизод {episode:5d} средний возврат за 100: {np.mean(history[-100:]):8.1f}", flush=True) np.save(r"C:\Users\Evgenie\AppData\Local\Temp\pend_history.npy", np.array(history)) torch.save(model.state_dict(), r"C:\Users\Evgenie\AppData\Local\Temp\pend_model.pt")
Код честный, и результат его — тоже честный: за 3000 эпизодов средний возврат остаётся около −1450 при случайных −1400…−1600, то есть ванильный актёр-критик Pendulum не решает (мы гоняли его при подготовке конспекта — и это известное свойство, а не опечатка в коде). Причины уже разобраны в этой траектории: награда густая, но информативный сигнал — редкие удачные раскачки; on-policy метод видит каждый опыт один раз; дисперсия гауссовой политики схлопывается раньше, чем найдена вертикаль. Именно на таких задачах и окупаются DDPG (раздел 7) с его памятью переходов — и PPO, который в следующем конспекте доведёт этот самый маятник до вертикали.
Практическая деталь из туториала Gymnasium «Speeding up A2C»: on-policy методы любят
векторные среды — несколько копий среды шагают параллельно
(gym.make_vec("LunarLander-v3", num_envs=3)), актёр получает батч состояний
и выдаёт батч действий. Больше независимых переходов на обновление — ниже дисперсия
градиента, та же идея, что случайный батч из памяти DQN, но без нарушения on-policy:
все переходы свежие. Там же показана доменная рандомизация — параллельные копии среды
с разной гравитацией и ветром дают более живучую политику.
7. DDPG: детерминированный актёр и критик Q
Гауссов актёр — не единственный путь в непрерывность. Конспект преподавателя предлагает взглянуть на проблему с другой стороны: дискретизировать непрерывное действие нельзя (10 степеней свободы по 4 уровня — уже \(4^{10} = 1\,048\,576\) действий, сходимости не дождаться), но можно сделать политику детерминированной функцией \(a = \pi_\theta(s)\) — сеть просто выдаёт вектор действия. Так устроен DDPG (Deep Deterministic Policy Gradient):
- актёр \(\pi_\theta(s)\) выдаёт действие напрямую — никакого розыгрыша;
- критик здесь оценивает не \(V(s)\), а \(Q_\varphi(s, a)\) — ему на вход подаются состояние и действие; обучается он по TD-ошибке, как в Q-методах;
- актёр обновляется так, чтобы выдавать действия с большим Q: градиент \(\nabla_\theta Q_\varphi(s, \pi_\theta(s))\) течёт сквозь критика в актёра — вот где окупается детерминированность: через розыгрыш случайного действия градиент не протолкнёшь, а через функцию — легко.
Детерминированность разом лишает метод встроенной разведки — политика всегда выдаёт одно и то же. Лекарство из конспекта преподавателя: добавлять шум — в действия или прямо в параметры сети. А поскольку детерминированная политика с шумом фактически off-policy, DDPG свободно заимствует у DQN оба стабилизатора из конспекта 7: память переходов и целевые сети (по паре на актёра и критика). Получается гибрид двух веток траектории: скелет обучения — от DQN, градиент политики — от актёра-критика. Развитие этой линии — TD3 (твин-критики против переоценки — привет от Double DQN) — можно попробовать в туториале «Lunar Lander with TD3» из материалов курса.
| A2C (разделы 1–6) | DDPG | |
|---|---|---|
| политика | стохастическая \(\pi_\theta(a|s)\) | детерминированная \(a = \pi_\theta(s)\) |
| критик | \(V_\varphi(s)\) | \(Q_\varphi(s, a)\) |
| режим данных | on-policy (свежие эпизоды) | off-policy (память переходов) |
| разведка | встроена в розыгрыш | внешний шум |
| наследство DQN | — | replay buffer, целевые сети |
Контрольные вопросы
-
A^π(s,a) = Q^π(s,a) − V^π(s) — насколько действие лучше среднего для этого состояния. Среднее по π: Σ_a π(a|s)Q(s,a) = V(s) (конспект 3, (3.7)), поэтому E[A] = V − V = 0. Положительное A — действие лучше обычного, отрицательное — хуже.
-
E[δ_t|s,a] = E[R + γV^π(S′)|s,a] − V^π(s). Первое слагаемое равно Q^π(s,a): по определению Q = E[R + γG′|s,a], а по марковости E[G′|s,a] = E[V^π(S′)|s,a]. Итого E[δ|s,a] = Q − V = A (теорема 10.1).
-
Вес при ∇log π — не монте-карловский возврат G_t, а TD-ошибка δ_t (или G−V); обновление происходит на каждом шаге среды, а не по концу эпизода. Дисперсия веса резко падает (δ — один переход, G — сумма всего будущего), обучение ускоряется; цена — небольшое смещение из-за неидеального критика (bias-variance, как TD против MC в конспекте 4).
-
Вес (G − V) для градиента актёра — константа; без detach градиент актёрской потери потечёт в параметры критика и начнёт подгонять V под сделанные действия, а не под политику. Критик обучается только своей потерей (V − G)² или TD-ошибкой.
-
Гарантии сходимости требуют разделения масштабов времени: критик должен успевать оценивать текущую политику быстрее, чем актёр её меняет, — иначе актёр учится по оценкам устаревшей политики. Практическое следствие: α критика выше α актёра (в туториале A2C так и есть).
-
Гауссова политика (10.7): по каждой компоненте действия — нормальное распределение; сеть выдаёт средние μ_k (Tanh — симметричный диапазон) и дисперсии D_k (Softplus — положительность). Действие разыгрывается, log π считается по (10.8) и идёт в градиент политики. Дисперсия обучаемая — политика сама управляет разведкой.
-
Актёр детерминирован: a = π_θ(s), усреднять по действиям нечего. Критику Q_φ(s,a) подают состояние и действие; актёр обновляется по ∇_θ Q_φ(s, π_θ(s)) — градиент течёт сквозь критика в параметры актёра, что возможно именно благодаря детерминированности (через случайный розыгрыш градиент не проходит).
-
Память переходов и целевые сети (для актёра и критика). Детерминированная политика с внешним шумом разведки — фактически off-policy: цель критика однопереходная и не зависит от породившей данные политики, поэтому старый опыт из буфера пригоден — в отличие от on-policy A2C/REINFORCE.
Источники
- Mnih, V. Asynchronous Methods for Deep Reinforcement Learning / V. Mnih, A. P. Badia, M. Mirza [et al.] // arXiv. — 2016. — URL: https://arxiv.org/abs/1602.01783 (дата обращения: 08.07.2026).
- Lillicrap, T. P. Continuous control with deep reinforcement learning / T. P. Lillicrap, J. J. Hunt, A. Pritzel [et al.] // arXiv. — 2015. — URL: https://arxiv.org/abs/1509.02971 (дата обращения: 08.07.2026).
- Speeding Up A2C Training with Vector Envs — Gymnasium Documentation : [сайт]. — URL: https://gymnasium.farama.org/tutorials/training_agents/vector_a2c.html (дата обращения: 08.07.2026).