深入解析 Actor-Critic 算法:从理论到 PyTorch 实战指南

在强化学习的广阔天地中,我们常常面临一个两难的选择:是应该基于价值去判断每一步的最优性,还是直接基于策略去尝试每一个动作?如果我们只关注价值,可能会陷入在高维空间中计算的泥潭;如果我们只关注策略,可能会因为样本效率低而难以收敛。今天,我们将深入探讨一种完美的平衡方案——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 的预测误差。

!Actor-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),理解并行化如何进一步提升性能。
  • 探索 DDPGSAC,看看 Actor-Critic 思想是如何扩展到连续动作空间的。

希望这篇文章能为你打开强化学习进阶的大门。快去运行你的代码,看看智能体是如何一步步学会平衡的!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。如需转载,请注明文章出处豆丁博客和来源网址。https://shluqu.cn/45423.html
点赞
0.00 平均评分 (0% 分数) - 0