强化学习:演员评论家 Actor-Critic 算法
Actor-Critic(演员 - 评论家)算法是强化学习中一种结合了策略梯度方法与值函数估计的混合架构。它通过两个核心组件协同工作,既保留了策略方法的灵活性,又利用值函数降低了方差,从而提升学习效率。
算法核心概念
角色分工
想象一个学习爬山的机器人:
- Actor(演员):负责决策。就像冒险家,根据当前状态选择动作(向左或向右)。
- Critic(评论家):负责评估。就像导师,观察动作结果并给出评价(这一步走得好还是坏)。
两者协作机制如下:
- Actor 根据策略 $\pi_\theta(a|s)$ 采样动作。
- Critic 估算当前状态的价值 $V(s)$。
- Actor 依据 Critic 反馈的误差调整策略参数。
这种分工使得 Actor 专注于优化动作选择,而 Critic 专注于准确预测回报,互相弥补不足。
背景与动机
策略梯度的局限性
纯策略梯度方法(Policy Gradient)直接优化策略分布,其核心更新公式涉及优势函数 $A^\pi(s, a)$。虽然理论优雅,但存在明显缺陷:
- 高方差:直接使用环境奖励信号计算梯度会导致方差过大,训练不稳定。
- 低效率:需要大量采样才能收敛,因为奖励信号稀疏且滞后。
Actor-Critic 的提出
为了解决上述问题,Sutton 等人提出了 Actor-Critic 框架。核心思想是利用 Critic 估算的值函数来替代原始奖励作为基线,从而降低梯度方差。Critic 通过学习状态价值函数 $V^\pi(s)$,提供 TD 误差(时序差分误差)作为更稳定的指导信号。
数学推导
优化目标
强化学习的目标是最大化累积折扣奖励的期望: $$ J(\theta) = \mathbb{E}{\pi\theta} \left[ \sum_{t=0}^{\infty} \gamma^t r_t \right] $$ 其中 $\gamma$ 是折扣因子,$r_t$ 是即时奖励。
策略梯度定理
为了优化策略参数 $\theta$,我们需要计算目标函数的梯度: $$ \nabla_\theta J(\theta) = \mathbb{E}{\pi\theta} \left[ \nabla_\theta \log \pi_\theta(a|s) \cdot A^\pi(s, a) \right] $$ 在 Actor-Critic 中,优势函数 $A^\pi(s, a)$ 通常由 Critic 提供的 TD 误差 $\delta$ 近似: $$ \delta = r + \gamma V(s') - V(s) $$
Critic 的更新
Critic 的目标是最小化均方误差,学习状态值函数: $$ L(w) = \frac{1}{2} \mathbb{E} \left[ (r + \gamma V(s') - V(s))^2 \right] $$ 其梯度更新方向为: $$ \nabla_w L(w) = \delta \cdot \nabla_w V(s) $$
Actor 的更新
Actor 利用 Critic 计算的 TD 误差来指导策略更新: $$ \theta \leftarrow \theta + \alpha \cdot \nabla_\theta \log \pi_\theta(a|s) \cdot \delta $$ 这里 $\delta$ 充当了优势函数的代理,告诉 Actor 当前动作比平均水平好多少。
PyTorch 实现
下面是一个基于 PyTorch 的完整 Actor-Critic 实现示例。我们定义了两个网络:PolicyNet(Actor)和 ValueNet(Critic)。
import torch
from torch import nn
from torch.nn import functional F
numpy np
(nn.Module):
():
(PolicyNet, ).__init__()
.fc1 = nn.Linear(n_states, n_hiddens)
.fc2 = nn.Linear(n_hiddens, n_actions)
():
x = F.relu(.fc1(x))
F.softmax(.fc2(x), dim=)
(nn.Module):
():
(ValueNet, ).__init__()
.fc1 = nn.Linear(n_states, n_hiddens)
.fc2 = nn.Linear(n_hiddens, )
():
x = F.relu(.fc1(x))
.fc2(x)
:
():
.gamma = gamma
.actor = PolicyNet(n_states, n_hiddens, n_actions)
.critic = ValueNet(n_states, n_hiddens)
.actor_optimizer = torch.optim.Adam(.actor.parameters(), lr=actor_lr)
.critic_optimizer = torch.optim.Adam(.critic.parameters(), lr=critic_lr)
():
state = torch.tensor(state[np.newaxis, :], dtype=torch.)
probs = .actor(state)
action_dist = torch.distributions.Categorical(probs)
action = action_dist.sample().item()
action, probs
():
states = torch.tensor(transition_dict[], dtype=torch.)
actions = torch.tensor(transition_dict[]).view(-, )
rewards = torch.tensor(transition_dict[], dtype=torch.).view(-, )
next_states = torch.tensor(transition_dict[], dtype=torch.)
dones = torch.tensor(transition_dict[], dtype=torch.).view(-, )
td_value = .critic(states)
td_target = rewards + .gamma * .critic(next_states) * ( - dones)
td_delta = td_target - td_value
critic_loss = F.mse_loss(td_value, td_target.detach())
.critic_optimizer.zero_grad()
critic_loss.backward()
.critic_optimizer.step()
log_probs = torch.log(.actor(states).gather(, actions))
actor_loss = -torch.mean(log_probs * td_delta.detach())
.actor_optimizer.zero_grad()
actor_loss.backward()
.actor_optimizer.step()


