在强化学习的广阔天地中,我们常常面临一个两难的选择:是应该基于价值去判断每一步的最优性,还是直接基于策略去尝试每一个动作?如果我们只关注价值,可能会陷入在高维空间中计算的泥潭;如果我们只关注策略,可能会因为样本效率低而难以收敛。今天,我们将深入探讨一种完美的平衡方案——Actor-Critic 算法。在这篇文章中,你将学会如何结合这两个世界的精华,构建一个既高效又稳定的智能体,我们还会通过完整的代码示例带你一步步实现它。
Actor-Critic 核心概念
想象一下,如果你在练习投篮,你需要两个角色的帮助:一个是动作的执行者,负责根据当前的情况决定用多大的力量和角度投篮;另一个是动作的评论者,站在旁边告诉你刚才这一投投得准不准,下次该怎么调整。这正是 Actor-Critic 算法的核心思想。
这种算法结合了两个神经网络,通过分工合作来提升学习效率:
- Actor (行动者/策略网络):负责与环境交互,决定在特定状态下采取什么动作。它的目标是找到最优策略。
- Critic (评论者/价值网络):负责评估 Actor 选择的动作好坏,即计算状态价值或动作价值。它的目标是准确估计价值函数。
通过这种“双核”驱动,Critic 的反馈可以有效减少 Actor 更新时的方差,从而让学习过程更加稳定和高效。
关键术语详解
为了更好地理解算法,我们需要先厘清两个基石概念:策略和价值函数。
#### 1. 策略
策略通常记为 $\pi(a|s)$,表示在状态 $s$ 下采取动作 $a$ 的概率。在深度强化学习中,我们使用一个参数为 $\theta$ 的神经网络来建模这个策略。Actor 的任务就是通过梯度上升来优化 $\theta$,使得期望回报最大化。
#### 2. 价值函数
价值函数,记为 $V(s)$ 或 $Q(s,a)$,用来估计从当前状态(或状态-动作对)开始,遵循特定策略能获得的预期累积奖励。Critic 网络的参数 $w$ 就是用来拟合这个价值的。
Actor-Critic 算法的工作原理
那么,这两个网络是如何协同工作的呢?Actor-Critic 算法的核心在于利用 Critic 计算出的“优势”来指导 Actor 的更新方向。
算法目标函数与数学推导
我们的总体目标包含两部分:Actor 试图最大化期望回报,而 Critic 试图最小化价值估计的误差。
#### 1. 策略梯度
对于 Actor,我们使用策略梯度定理。为了加快收敛并减少方差,我们不直接使用累积回报 $G_t$,而是使用 Critic 计算出的优势函数 $A(s,a)$。Actor 的目标函数梯度公式如下:
$$
abla\theta J(\theta)\approx \frac{1}{N} \sum{i=0}^{N}
abla\theta \log\pi\theta (ai|si)\cdot A(si,ai)
$$
这里的含义如下:
- $J(\theta)$:期望回报。
- $\pi_\theta (a\mid s)$:策略函数。
- $A(s,a)$:优势函数,衡量动作 $a$ 比平均水平好多少。
如果 Critic 告诉我们这个动作非常好($A(s,a)$ 为正),我们就会增加该动作的概率;反之则降低。
#### 2. Critic 的损失函数
Critic 的任务是准确估计价值。最常用的方法是计算均方误差 (MSE)。如果我们使用动作价值 $Q(s,a)$ 和状态价值 $V(s)$ 的差值作为优势,那么 Critic 需要最小化估计值与目标值之间的差异:
$$
ablaw J(w) \approx \frac{1}{N}\sum{i=1}^{N}
ablaw (V{w}(si)- yi)^2
$$
其中 $y_i$ 是真实的目标回报,或者是带自举的回报估计(Target)。
#### 3. 优势函数
这是连接 Actor 和 Critic 的桥梁:
$$A(s,a) = Q(s,a) – V(s)$$
在实际实现中,我们经常使用 TD Error (时间差分误差) 来近似优势函数:
$$A(s, a) \approx r + \gamma V(s‘) – V(s)$$
更新规则
在训练循环中,我们交替更新这两个网络:
Actor 更新 (梯度上升):
> $\theta{t+1}= \thetat + \alpha
abla\theta J(\thetat)$
- 这里的 $\alpha$ 是 Actor 的学习率。我们根据 Critic 的评分调整参数。
Critic 更新 (梯度下降):
> $w{t+1} = wt -\beta
ablaw J(wt)$
- $\beta$ 是 Critic 的学习率。我们通过反向传播减小 Critic 的预测误差。
实战演练:使用 PyTorch 训练 CartPole
纸上得来终觉浅,让我们来看一个实际的例子。我们将使用 PyTorch(比 TensorFlow 在研究中更为灵活)和 OpenAI Gym 的 CartPole 环境来从零实现一个 Actor-Critic 智能体。
第1步:环境准备与导入库
首先,我们需要导入必要的库并创建环境。我们将使用 INLINECODE674e21b3 来模拟环境,使用 INLINECODE60191089 来构建神经网络。
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import gym
from torch.distributions import Categorical
# 创建 CartPole 环境
# 如果你的环境安装较新,建议使用 gym.make("CartPole-v1")
env = gym.make(‘CartPole-v1‘)
state_dim = env.observation_space.shape[0] # 状态维度:4
action_dim = env.action_space.n # 动作维度:2
print(f"状态维度: {state_dim}, 动作维度: {action_dim}")
第2步:定义 Actor-Critic 网络
为了简化代码和优化训练速度,我们通常将 Actor 和 Critic 合并在一个类中,共享前几层的特征提取层,或者简单地作为两个独立的输出头。下面是一个共享特征提取层的实现,这种结构在实践中非常高效。
class ActorCritic(nn.Module):
def __init__(self, state_dim, action_dim, hidden_dim=64):
super(ActorCritic, self).__init__()
# 共享的特征提取层
self.shared_layers = nn.Sequential(
nn.Linear(state_dim, hidden_dim),
nn.ReLU()
)
# Actor Head: 输出动作概率分布
self.actor = nn.Sequential(
nn.Linear(hidden_dim, action_dim),
nn.Softmax(dim=-1)
)
# Critic Head: 输出状态价值 V(s)
self.critic = nn.Sequential(
nn.Linear(hidden_dim, 1)
)
def forward(self, state):
# 共享特征提取
x = self.shared_layers(state)
# 计算 Actor 和 Critic 输出
action_probs = self.actor(x)
state_value = self.critic(x)
return action_probs, state_value
def act(self, state):
# 用于交互的动作采样函数
state = torch.from_numpy(state).float().unsqueeze(0) # 增加 batch 维度
action_probs, _ = self.forward(state)
# 创建分布并采样
dist = Categorical(action_probs)
action = dist.sample()
return action.item(), dist.log_prob(action)
第3步:定义优化器和超参数
在强化学习中,超参数的选择至关重要。我们将使用一个适中的学习率,并设置 Gamma(折扣因子)来平衡即时奖励和长期奖励。
# 初始化网络
model = ActorCritic(state_dim, action_dim)
# Adam 优化器通常比 SGD 在 RL 中表现更好
optimizer = optim.Adam(model.parameters(), lr=3e-3)
# 超参数配置
GAMMA = 0.99 # 折扣因子
EPISODES = 1000 # 训练轮数
第4步:核心训练循环实现
这是最关键的部分。我们将实现一个完整的训练循环,包含经验收集和联合更新。我们会在每个 episode 结束后使用蒙特卡洛更新来计算回报,这是 A2C (Advantage Actor-Critic) 的简化变体。
def train_one_episode():
state = env.reset()
episode_rewards = []
log_probs = []
state_values = []
# --- 1. 经验收集阶段 ---
done = False
while not done:
action, log_prob = model.act(state)
next_state, reward, done, _ = env.step(action)
# 存储经验
log_probs.append(log_prob)
episode_rewards.append(reward)
# 获取状态价值 V(s)
state_tensor = torch.from_numpy(state).float().unsqueeze(0)
_, value = model(state_tensor)
state_values.append(value)
state = next_state
# --- 2. 计算回报和优势 ---
returns = []
R = 0
# 从后往前计算折线回报
for r in episode_rewards[::-1]:
R = r + GAMMA * R
returns.insert(0, R)
returns = torch.tensor(returns)
# 归一化回报可以稳定训练(可选技巧)
returns = (returns - returns.mean()) / (returns.std() + 1e-9)
state_values = torch.stack(state_values).squeeze()
# 优势函数 A(s, a) = R - V(s)
# 这里我们使用实际的回报 R 作为 TD 目标
advantages = returns - state_values.detach()
# --- 3. 计算损失并更新 ---
log_probs = torch.stack(log_probs)
# Actor Loss: -log_prob * advantage
# 我们希望最大化优势,所以最小化负数
actor_loss = -(log_probs * advantages).mean()
# Critic Loss: (V(s) - R)^2
critic_loss = nn.MSELoss()(state_values, returns)
# 总损失
total_loss = actor_loss + critic_loss
# 反向传播
optimizer.zero_grad()
total_loss.backward()
optimizer.step()
return sum(episode_rewards)
# 开始训练
print("开始训练...")
running_reward = 0
for episode in range(1, EPISODES + 1):
episode_reward = train_one_episode()
# 计算最近100局的平均奖励以平滑曲线
running_reward = 0.05 * episode_reward + 0.95 * running_reward
if episode % 10 == 0:
print(f"Episode {episode}, Last Reward: {episode_reward}, Average Reward: {running_reward:.2f}")
# CartPole-v1 的平均 195 分视为解决
if running_reward > env.spec.reward_threshold:
print(f" solved! Episode {episode}")
break
第5步:实战中的优化技巧与代码解析
在上面的代码中,我们实现了基础的 Actor-Critic 循环。但是,你可能会遇到训练不稳定的情况。让我们深入探讨一些优化技巧,并展示如何应用它们。
#### 1. 梯度裁剪
在高维空间中,梯度爆炸会迅速毁掉训练过程。我们可以在 optimizer.step() 之前添加梯度裁剪:
# 在 total_loss.backward() 之后添加
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=0.5)
#### 2. 熵正则化
如果你的智能体过快地收敛到某个动作(过早收敛),可能会导致表现次优。我们可以通过添加熵奖励来鼓励探索。
# 计算 Actor Loss 时加入熵项
# 1. 获取当前策略的熵
entropy = []
# ...在循环中...
action_probs, _ = model(state_tensor)
dist = Categorical(action_probs)
entropy.append(dist.entropy())
# ...更新 Loss...
entropy = torch.stack(entropy).mean()
actor_loss = -(log_probs * advantages).mean() - 0.01 * entropy # 0.01 是熵系数
进阶代码示例:向量环境与 GAE
为了进一步提高效率,我们可以使用向量化环境(并行运行多个环境)来加速数据收集,并使用广义优势估计来平滑优势估计。以下是核心逻辑的伪代码/结构展示:
# 需要安装: pip install gym[vector_envs]
import gym.vector
# 创建多个并行环境
vec_env = gym.vector.make(‘CartPole-v1‘, num_envs=4)
model = ActorCritic(state_dim, action_dim)
# GAE (Generalized Advantage Estimation) 参数
tau = 0.95
def compute_gae(rewards, values, masks):
# 实现 GAE 算法以计算更稳定的优势
# ... (详细的 GAE 计算逻辑) ...
return advantages, targets
# 训练循环不再按 episode,而是按 n_steps 步数
for update in range(10000):
states, actions, log_probs, rewards, values, dones = [], [], [], [], [], []
for _ in range(n_steps):
# 交互逻辑略...
pass
# 计算 GAE
advantages, targets = compute_gae(rewards, values, dones)
# 更新网络...
常见问题与解决方案
在实践 Actor-Critic 时,你可能会遇到以下问题:
- Critic 估计过高:有时 Critic 会给出一味乐观的估计。这通常可以通过使用目标网络 来缓解,即保留一个 Critic 的副本用于计算 TD 目标,并定期同步。
- 训练震荡:如果损失曲线剧烈波动,尝试降低学习率(例如降到 1e-4),或者减小网络层数/隐藏单元数。CartPole 这种简单环境不需要太大的网络。
- 局部最优:智能体可能学会了一个次优策略且跳不出来。保持一定的探索率(如熵系数)非常重要。
结语与后续步骤
通过这篇文章,我们不仅从理论上拆解了 Actor-Critic 算法的数学原理,还使用 PyTorch 编写了一个完整的、可运行的智能体。你现在已经掌握了强化学习中从纯策略梯度到 Actor-Critic 这一重要的进阶步骤。
接下来的学习建议:
- 试试将代码中的 CartPole 替换为 LunarLander,这是一个更适合展示 Actor-Critic 能力的环境。
- 深入研究 A2C (Advantage Actor-Critic) 和 A3C (Asynchronous Actor-Critic),理解并行化如何进一步提升性能。
- 探索 DDPG 或 SAC,看看 Actor-Critic 思想是如何扩展到连续动作空间的。
希望这篇文章能为你打开强化学习进阶的大门。快去运行你的代码,看看智能体是如何一步步学会平衡的!