Menu
Avatar
The menu of my blog
Quick Stats
Quests
30 Quests
Messages
2 Messages
Playback
5 Playback
Items
6 Items
Skills
2 Skills
Trace
1 Trace
Message

The Sword Art Online Utilities Project

Welcome, traveler. This is a personal blog built in the style of the legendary SAO game interface. Navigate through the menu to explore the journal, skills, and item logs.

© 2020-2026 Nagi-ovo | RSS | Breezing
← Back to Quest Log
Actor Critic 方法初探
Actor Critic 方法初探

学习 Actor-Critic 方法,结合策略梯度(Actor)和价值函数(Critic)的优势,实现更高效的强化学习。

2024年10月10日 2024年10月10日 25 min read
actor-critic强化学习RL

Human-Crafted

Written directly by the author with no AI-generated sections.

Actor Critic 方法初探

方差问题

策略梯度(Policy Gradient)方法因其直观和有效性而备受关注。我们之前探讨过Reinforce算法,它在许多任务中表现良好。然而,Reinforce 方法依赖于蒙特卡洛(Monte Carlo)采样来估计回报,这意味着我们需要使用整个回合的数据来计算回报。这种方法带来了一个关键问题——策略梯度估计中的高方差。

Advantage estimation

PG 估计的核心在于找到回报增加最快的方向。换句话说,我们需要更新策略的权重,使得那些带来高回报的动作在未来被选中的概率更高。理想情况下,这样的更新会使得策略逐步优化,获得更高的总回报。

然而,使用蒙特卡洛方法估计回报时,由于依赖整个回合的数据计算实际回报(不估计回报),策略梯度的估计会有显著的方差(无偏但高方差)。高方差意味着我们的梯度估计不稳定,可能导致训练过程缓慢甚至不收敛。为了获得可靠的梯度估计,我们可能需要大量的样本,这在实际应用中往往代价高昂。

Reinforce variance

环境和策略的随机性导致相同初始状态可能产生截然不同的回报,造成高方差。因此,从同一状态开始的回报在不同情节中可能显著变化。使用大量轨迹可减少方差,提供更准确的回报估计。但大 batch size 会降低样本效率,因此需要寻找其他减少方差的机制。

Advantage Actor-Critic (A2C)

通过 Actor-Critic 方法减少方差

从前面所学知识和章节导语带来的一个直观感受是“如果结合 Value-Based 和 Policy-Based,方差和训练的问题都会得到优化”。演员 - 评论家(Actor-Critic)方法 正是这样的混合架构,具体来说:

  • 演员(Actor):负责选择动作,基于当前策略生成动作概率分布。
  • 评论家(Critic):估计当前策略下的价值函数,提供对动作选择的反馈。

想象你和你的朋友都是菜鸟玩家。你负责操作(Actor),而你的朋友负责观察和评价(Critic)。一开始,你们都不太懂游戏。你瞎猫撞死耗子般地操作,而你的朋友也在摸索如何评价你的表现。随着时间推移,你通过实践不断改进操作技巧(Policy),同时你的朋友也在学习如何更准确地评估每个动作的好坏(Value)。

Cover

你们互相帮助,共同进步:你的操作为朋友提供了评估的基础,而朋友的反馈帮助你调整策略。

也就是说,我们会学习两个函数逼近(神经网络):

  • 控制 agent(Actor)操作的的策略函数:πθ(s)\pi_\theta(s)πθ​(s)
  • 衡量行动好坏辅助策略优化(Critic)的价值函数:q^w(s,a)\hat{q}_w(s,a)q^​w​(s,a)

算法流程

  • 在每个时间步 ttt,我们从环境中获取当前状态 StS_tSt​ ,作为输入传递给我们的 Actor 和 Critic。
  • Actor 根据状态输出一个动作 AtA_tAt​ 。

A2C Algorithm

  • Critic 也将该动作作为输入,并使用 StS_tSt​ 和 AtA_tAt​ ,计算在该状态下采取该动作的价值:即 Q 值。
  • 在环境中执行动作 AtA_tAt​ ,输出新状态 St+1S_{t+1}St+1​ 和奖励 Rt+1R_{t+1}Rt+1​ 。
  • Actor 利用 QQQ 值更新其策略参数。

Actor Critic Architecture

Training Code

  • Actor 使用更新后的参数,在给定新状态 St+1S_{t+1}St+1​ 时生成下一步要采取的动作 At+1A_{t+1}At+1​ 。
  • Critic 随后更新其价值参数。

TD Error

Critic 起到了提供用于调整回报估计的 baseline 作用,从而使得梯度估计更加稳定。训练过程更为平滑,收敛速度更快,所需样本数量也大大减少。

添加 Advantage(A2C)

可以通过使用Advantage Function 作为 Critic 而不是动作值函数来进一步稳定学习。

优势:让好动作脱颖而出

核心想法是,将你的动作通过两个部分来评估:

  1. 你获得的即时奖励和下一个状态的价值。
  2. 你在当前状态中对价值的预期。

数学上,我们称之为优势:

A(st,at)=rt+1+γV(st+1)−V(st)A(s_t, a_t) = r_{t+1} + \gamma V(s_{t+1}) - V(s_t)A(st​,at​)=rt+1​+γV(st+1​)−V(st​)

这表达的是:在状态 sts_tst​ 中,你做出的动作 ata_tat​,比你在状态 sts_tst​ 中原本的期望(由 V(st)V(s_t)V(st​) 表示的 baseline 期望)好多少?

如果实际的奖励和未来状态的价值 rt+1+γV(st+1)r_{t+1} + \gamma V(s_{t+1})rt+1​+γV(st+1​) 高于你对当前状态 V(st)V(s_t)V(st​) 的预期,那么这个动作是好的;如果低了,那……你可以做得更好。

这个优势不仅告诉你动作好不好,还告诉你有多好或者有多差(相对于 baseline 来说)。

策略更新

当我们执行某个动作时,奖励本身并不足够指导策略的改进。奖励告诉我们某个动作好或不好,但它并没有告诉我们这个动作究竟有多好,或者比预期好多少。 所以改进策略时,与其盲目追逐奖励,不如关注调整动作,基于它们超越(或低于)期望的程度。现在就可以微调策略,向那些持续表现优于 baseline 的动作靠拢。

这就是我们得到的更新公式:

∇θJ(θ)∼∑t=0T−1∇θlog⁡πθ(at∣st)A(st,at)\nabla_\theta J(\theta) \sim \sum_{t=0}^{T-1} \nabla_\theta \log \pi_\theta(a_t | s_t) A(s_t, a_t)∇θ​J(θ)∼t=0∑T−1​∇θ​logπθ​(at​∣st​)A(st​,at​)
  • ∇θlog⁡πθ(at∣st)\nabla_\theta \log \pi_\theta(a_t | s_t)∇θ​logπθ​(at​∣st​):表示在每个时间步 ttt,我们计算策略 πθ\pi_\thetaπθ​ 选择动作 ata_tat​ 的概率的对数梯度。这一步帮助我们找到在当前策略下,如何通过改变参数 θ\thetaθ 来提高选择 ata_tat​ 的可能性。
  • A(st,at)A(s_t, a_t)A(st​,at​):动作的优势函数,告诉我们这个动作 ata_tat​ 在状态 sts_tst​ 下相对于 baseline 有多好或多差。

简单来说:你的策略 πθ\pi_\thetaπθ​ 的梯度由优势 A(st,at)A(s_t, a_t)A(st​,at​) 调整。你在更新你的策略,不仅基于这个动作是否带来了一些奖励,而是基于这个动作相比期望超越了多少。

更妙的是:你只需要一个神经网络来预测价值函数 V(s)V(s)V(s)。

现在来聊聊 TD 误差

当然,计算这个优势函数很棒,但在线学习有一个妙处:你不需要等到最后再更新策略。于是,时序差分误差(TD Error) 就登场了:

δ=r+γV(s′)−V(s)\delta = r + \gamma V(s') - V(s)δ=r+γV(s′)−V(s)

这里的关键是:TD 误差实际上是优势函数的在线估计。它告诉你,就在此刻,你的动作是否让未来状态比你预期的要好。这个误差 δ\deltaδ 直接反映了优势的概念:

  • 如果 δ>0\delta > 0δ>0:“嘿,这个动作比我想象中好!”(优势为正)。
  • 如果 δ<0\delta < 0δ<0:“嗯,我本来以为会更好……”(优势为负)。

这让你可以逐步调整你的策略,不用等到一整 episode 结束才做改变。这对提高效率来说,简直是绝佳策略。

代码实现

Actor-Critic 网络架构

首先,我们需要构建一个神经网络。这是一个双头网络:一个用于 Actor(学习策略来选择动作),另一个用于 Critic(估计状态的价值)。

class ActorCritic(nn.Module):
    def __init__(self, num_inputs, num_actions, hidden_size, learning_rate=3e-4):
        super(ActorCritic, self).__init__()
 
        # Critic 网络(价值函数近似)
        # 这个网络用于预测 V(s),也就是状态 s 的价值
        self.critic_linear1 = nn.Linear(num_inputs, hidden_size)
        self.critic_linear2 = nn.Linear(hidden_size, 1)  # 价值函数是标量输出
 
        # Actor 网络(策略函数近似)
        # 这个网络用于预测 π(a|s),即在状态 s 下选择动作 a 的概率
        self.actor_linear1 = nn.Linear(num_inputs, hidden_size)
        self.actor_linear2 = nn.Linear(hidden_size, num_actions)  # 输出对所有动作的概率分布
    
    def forward(self, state):
        # 将状态转换为 torch tensor,并增加一个维度以支持 batch 处理
        state = Variable(torch.from_numpy(state).float().unsqueeze(0))
        
        # Critic 网络的前向传播
        value = F.relu(self.critic_linear1(state))
        value = self.critic_linear2(value)  # 输出状态的价值 V(s)
        
        # Actor 网络的前向传播
        policy_dist = F.relu(self.actor_linear1(state))
        policy_dist = F.softmax(self.actor_linear2(policy_dist), dim=1)  # 用 softmax 将原始的数值转换为动作的概率分布(策略)
        
        return value, policy_dist
A2C 算法的核心实现

接下来进入 A2C 的核心:主循环和更新机制。每一 episode 中,agent 在环境中运行一定步数,收集状态、动作和奖励的轨迹。在每集结束时,更新 Actor(策略)和 Critic(价值函数)。

def a2c(env):
    # 从环境中获取输入和输出的维度
    num_inputs = env.observation_space.shape[0]
    num_outputs = env.action_space.n
    
    # 初始化 Actor-Critic 网络
    actor_critic = ActorCritic(num_inputs, num_outputs, hidden_size)
    ac_optimizer = optim.Adam(actor_critic.parameters(), lr=learning_rate)
 
    # 用于追踪性能的数据容器
    all_lengths = []  # 追踪每 episode 的长度
    average_lengths = []  # 追踪最近 10 episode 的平均长度
    all_rewards = []  # 追踪每 episode 的累计奖励
    entropy_term = 0  # 激励探索
 
    # 进入每一 episode 的循环
    for episode in range(max_episodes):
        log_probs = []  # 存储动作的对数概率
        values = []  # 存储 Critic 的价值估计(V(s))
        rewards = []  # 存储获得的奖励
 
        state = env.reset()  # 重置环境,开始新的一集
        for steps in range(num_steps):
            # 前向传播通过网络
            value, policy_dist = actor_critic.forward(state)
            value = value.detach().numpy()[0,0]  # Critic 估计当前状态的价值
            dist = policy_dist.detach().numpy()
            
            # 从动作概率分布中采样一个动作
            action = np.random.choice(num_outputs, p=np.squeeze(dist))
            log_prob = torch.log(policy_dist.squeeze(0)[action])  # 记录选中动作的对数概率
            entropy = -np.sum(np.mean(dist) * np.log(dist))  # 使用熵衡量探索的多样性
            new_state, reward, done, _ = env.step(action)  # 执行动作,获取奖励和新状态
 
            # 记录这一段轨迹的数据
            rewards.append(reward)
            values.append(value)
            log_probs.append(log_prob)
            entropy_term += entropy
            state = new_state  # 更新为新的状态
            
            # 如果本 episode 结束,记录并退出本轮循环
            if done or steps == num_steps-1:
                Qval, _ = actor_critic.forward(new_state)  # 最后状态的价值估计
                Qval = Qval.detach().numpy()[0,0]
                all_rewards.append(np.sum(rewards))  # 记录本 episode 的总奖励
                all_lengths.append(steps)
                average_lengths.append(np.mean(all_lengths[-10:]))
                if episode % 10 == 0:
                    sys.stdout.write("episode: {}, reward: {}, total length: {}, average length: {} \n".format(
                        episode, np.sum(rewards), steps, average_lengths[-1]))
                break
 
        # 计算 Q 值(Critic 的目标值)
        Qvals = np.zeros_like(values)  # 初始化 Q 值数组
        for t in reversed(range(len(rewards))):
            Qval = rewards[t] + GAMMA * Qval  # Bellman 公式计算 Q 值
            Qvals[t] = Qval
 
        Qvals = torch.FloatTensor(Qvals)
        log_probs = torch.stack(log_probs)
        
        # 计算优势函数
        advantage = Qvals - values  # 动作的表现超出了 Critic 的期望多少?
 
        # 损失函数
        actor_loss = (-log_probs * advantage).mean()  # 策略损失(鼓励表现更好的动作)
        critic_loss = 0.5 * advantage.pow(2).mean()  # 价值函数损失(最小化预测误差)
        ac_loss = actor_loss + critic_loss + 0.001 * entropy_term  # 总损失,包括熵激励
 
        # 反向传播和优化
        ac_optimizer.zero_grad()
        ac_loss.backward()
        ac_optimizer.step()
  • Actor 更新基于策略梯度:我们将动作的对数概率与动作的优势相乘,若动作比预期表现好,优势为正,则鼓励该动作。
  • Critic 更新基于均方误差:Critic 比较其预测的 V(st)V(s_t)V(st​) 与实际的回报 QtQ_tQt​,并最小化这个差异。
  • entropy_term:通过引入熵来鼓励探索行为,防止智能体过早收敛到某些动作而缺乏足够的探索。

总结

A2C 的核心就是通过优势函数引导策略更新,同时借助TD 误差进行在线学习,让 agent 的每一步行动更加聪明和高效。

  • 优势函数:不仅告诉你动作好坏,还告诉你相对 baseline 有多好。
  • TD 误差:一个实时计算优势的利器,帮助你快速高效地更新策略。

Asynchronous A2C(A3C)

A3C 是在 Deepmind 的论文《Asynchronous Methods for Deep Reinforcement Learning》中提出的。本质上,A3C 相当于 并行化 的 A2C,多个并行的 agent(工作线程)在并行环境中独立更新全局的价值函数,因此被称为“异步”,在现代的多核 CPU 上效率较高。

可以从前面看出来,A2C 是 A3C 的同步版本,在执行更新之前等待每个参与者完成其经验段,平均所有参与者的结果,优点是可以更高效地利用 GPU。并且在OpenAI Baselines: ACKTR & A2C中提到: 我们的同步 A2C 实现的性能优于我们的异步实现——我们没有看到任何证据表明异步引入的噪声提供了任何性能优势。在使用单 GPU 机器时,这种 A2C 实现比 A3C 更具成本效益,并且在使用更大策略时比仅使用 CPU 的 A3C 实现更快。

参考资料

  • Advantage Actor-Critic (A2C) - Hugging Face Deep RL Course
  • OpenAI Baselines: ACKTR & A2C | OpenAI
  • 代码实现:Understanding Actor Critic Methods and A2C | by Chris Yoon | Towards Data Science
Article Info Human-Crafted
Title Actor Critic 方法初探
Author Nagi-ovo
URL
Last Updated 2024年10月10日
Citation

商业转载请联系站长获得授权;非商业转载请注明出处并附上本文链接。

你可以复制、分发并改编本文,但衍生作品需采用相同许可协议。本文采用 CC BY-NC-SA 4.0 授权。

Session 00:00:00