代码示例 / 强化学习 / Actor Critic 方法

Actor Critic 方法

作者: Apoorv Nandan
创建日期: 2020/05/13
最后修改: 2024/02/22
描述: 在 CartPole 环境中实现 Actor Critic 方法。

在 Colab 中查看 GitHub 源代码


介绍

该脚本展示了在 CartPole-V0 环境中 Actor Critic 方法的实现。

Actor Critic 方法

当代理执行动作并移动通过环境时,它学习将观察到的环境状态映射到两个可能的输出:

  1. 推荐动作:对动作空间中每个动作的概率值。 负责该输出的代理部分称为 actor
  2. 未来的估计奖励:它期望在未来收到的所有奖励的总和。 负责该输出的代理部分称为 critic

代理和评论员学习执行他们的任务,使得来自 actor 的推荐动作最大化奖励。

CartPole-V0

一根杆子连接到放置在无摩擦轨道上的小车。代理必须施加力量来移动小车。每当杆子保持直立时,它会获得奖励。因此,代理必须学习保持杆子不倒。

参考文献


设置

import os
os.environ["KERAS_BACKEND"] = "tensorflow"
import gym
import numpy as np
import keras
from keras import ops
from keras import layers
import tensorflow as tf

# 整个设置的配置参数
seed = 42
gamma = 0.99  # 过去奖励的折扣因子
max_steps_per_episode = 10000
env = gym.make("CartPole-v0")  # 创建环境
env.seed(seed)
eps = np.finfo(np.float32).eps.item()  # 最小的数,使得 1.0 + eps != 1.0

实现 Actor Critic 网络

该网络学习两个函数:

  1. Actor:该函数输入我们的环境状态并返回其动作空间中每个动作的概率值。
  2. Critic:该函数输入我们的环境状态并返回未来的总奖励估计。

在我们的实现中,它们共享初始层。

num_inputs = 4
num_actions = 2
num_hidden = 128

inputs = layers.Input(shape=(num_inputs,))
common = layers.Dense(num_hidden, activation="relu")(inputs)
action = layers.Dense(num_actions, activation="softmax")(common)
critic = layers.Dense(1)(common)

model = keras.Model(inputs=inputs, outputs=[action, critic])

训练

optimizer = keras.optimizers.Adam(learning_rate=0.01)
huber_loss = keras.losses.Huber()
action_probs_history = []
critic_value_history = []
rewards_history = []
running_reward = 0
episode_count = 0

while True:  # 运行直到解决
    state = env.reset()
    episode_reward = 0
    with tf.GradientTape() as tape:
        for timestep in range(1, max_steps_per_episode):
            # env.render(); 添加这一行将显示代理的尝试
            # 在弹出窗口中。

            state = ops.convert_to_tensor(state)
            state = ops.expand_dims(state, 0)

            # 从环境状态预测动作概率和估计的未来奖励
            action_probs, critic_value = model(state)
            critic_value_history.append(critic_value[0, 0])

            # 从动作概率分布中采样动作
            action = np.random.choice(num_actions, p=np.squeeze(action_probs))
            action_probs_history.append(ops.log(action_probs[0, action]))

            # 在我们的环境中应用采样的动作
            state, reward, done, _ = env.step(action)
            rewards_history.append(reward)
            episode_reward += reward

            if done:
                break

        # 更新运行奖励以检查解决条件
        running_reward = 0.05 * episode_reward + (1 - 0.05) * running_reward

        # 计算奖励的期望值
        # - 在每个时间步,给定时间步后收到的总奖励
        # - 过去的奖励通过与gamma相乘进行折扣
        # - 这些是我们的评论员的标签
        returns = []
        discounted_sum = 0
        for r in rewards_history[::-1]:
            discounted_sum = r + gamma * discounted_sum
            returns.insert(0, discounted_sum)

        # 归一化
        returns = np.array(returns)
        returns = (returns - np.mean(returns)) / (np.std(returns) + eps)
        returns = returns.tolist()

        # 计算损失值以更新我们的网络
        history = zip(action_probs_history, critic_value_history, returns)
        actor_losses = []
        critic_losses = []
        for log_prob, value, ret in history:
            # 在历史的这一点上,评论员估计我们将会得到一个
            # 在未来的总奖励 = `value`。我们采取了一个动作,日志概率为
            # `log_prob`,最终获得的总奖励 = `ret`。
            # 演员必须更新,以便预测一个高概率的动作,从而导致
            # 高奖励(与评论员的估计相比)。
            diff = ret - value
            actor_losses.append(-log_prob * diff)  # 演员损失

            # 评论员必须进行更新,以便预测更好的
            # 未来奖励的估计。
            critic_losses.append(
                huber_loss(ops.expand_dims(value, 0), ops.expand_dims(ret, 0))
            )

        # 反向传播
        loss_value = sum(actor_losses) + sum(critic_losses)
        grads = tape.gradient(loss_value, model.trainable_variables)
        optimizer.apply_gradients(zip(grads, model.trainable_variables))

        # 清除损失和奖励历史
        action_probs_history.clear()
        critic_value_history.clear()
        rewards_history.clear()

    # 记录细节
    episode_count += 1
    if episode_count % 10 == 0:
        template = "运行奖励: {:.2f} 在第 {} 集"
        print(template.format(running_reward, episode_count))

    if running_reward > 195:  # 考虑任务已解决的条件
        print("在第 {} 集解决!".format(episode_count))
        break
当前奖励:8.82 在第 10 轮
当前奖励:23.04 在第 20 轮
当前奖励:28.41 在第 30 轮
当前奖励:53.59 在第 40 轮
当前奖励:53.71 在第 50 轮
当前奖励:77.35 在第 60 轮
当前奖励:74.76 在第 70 轮
当前奖励:57.89 在第 80 轮
当前奖励:46.59 在第 90 轮
当前奖励:43.48 在第 100 轮
当前奖励:63.77 在第 110 轮
当前奖励:111.13 在第 120 轮
当前奖励:142.77 在第 130 轮
当前奖励:127.96 在第 140 轮
当前奖励:113.92 在第 150 轮
当前奖励:128.57 在第 160 轮
当前奖励:139.95 在第 170 轮
当前奖励:154.95 在第 180 轮
当前奖励:171.45 在第 190 轮
当前奖励:171.33 在第 200 轮
当前奖励:177.74 在第 210 轮
当前奖励:184.76 在第 220 轮
当前奖励:190.88 在第 230 轮
当前奖励:154.78 在第 240 轮
当前奖励:114.38 在第 250 轮
当前奖励:107.51 在第 260 轮
当前奖励:128.99 在第 270 轮
当前奖励:157.48 在第 280 轮
当前奖励:174.54 在第 290 轮
当前奖励:184.76 在第 300 轮
当前奖励:190.87 在第 310 轮
当前奖励:194.54 在第 320 轮
在第 322 轮解决!

可视化

在训练的早期阶段: Imgur

在训练的后期阶段: Imgur