10.1 简介 #
本书之前的章节讲解了基于值函数的方法 (DQN
) 和基于策略的方法 (REINFORCE
),其中基于值函数的方法只学习一个价值函数,而基于策略的方法只学习一个策略函数。那么,一个很自然的问题是,有没有什么方法既学习价值函数,又学习策略函数呢?答案就是 Actor-Critic
。Actor-Critic
是囊括一系列算法的整体架构,目前很多高效的前沿算法都属于 Actor-Critic
算法,本章接下来将会介绍一种最简单的 Actor-Critic
算法。需要明确的是,Actor-Critic
算法本质上是基于策略的算法,因为这一系列算法的目标都是优化一个带参数的策略,只是会额外学习价值函数,从而帮助策略函数更好地学习。
10.2 Actor-Critic #
回顾一下,在 REINFORCE
算法中,目标函数的梯度中有一项轨迹回报,用于指导策略的更新。REINFOCE
算法用蒙特卡洛方法来估计 $Q(s,a)$,能不能考虑拟合一个值函数来指导策略进行学习呢?这正是 Actor-Critic
算法所做的。在策略梯度中,可以把梯度写成下面这个更加一般的形式:
$$ g=\mathbb{E}\left[ \sum_{t=0}^T \psi_t \nabla_\theta \log \pi_\theta(a_t|s_t) \right] $$
其中,$\psi_t$ 可以有很多种形式:
$$ \begin{align*} 1. & \sum_{t^\prime=0}^T \gamma^{t^\prime} r_{t^\prime}:& \text{轨迹总回报} \\ 2. & \sum_{t^\prime=t}^T \gamma^{t^\prime - t} r_{t^\prime}:& \text{动作 } a_t \text{ 之后的回报} \\ 3. & \sum_{t^\prime=t}^T \gamma^{t^\prime - t} r_{t^\prime} - b(s_t):& \text{基准线版本的改进} \\ 4. & Q^{\pi_\theta}(s_t, a_t):& \text{动作价值函数} \\ 5. & A^{\pi_\theta}(s_t, a_t):& \text{优势函数} \\ 6. & r_t + \gamma V^{\pi_\theta}(s_{t+1}) - V^{\pi_\theta}(s_{t}):& \text{时序差分} \\ \end{align*} $$
9.5 节提到 REINFORCE
通过蒙特卡洛采样的方法对策略梯度的估计是无偏的,但是方差非常大。我们可以用形式 (3) 引入基线函数(baseline function
) $b(s_t)$ 来减小方差。此外,我们也可以采用 Actor-Critic
算法估计一个动作价值函数 $Q$,代替蒙特卡洛采样得到的回报,这便是形式 (4)。这个时候,我们可以把状态价值函数 $V$ 作为基线,从 $Q$ 函数减去这个 $V$ 函数则得到了 $A$ 函数,我们称之为优势函数 (advantage function
),这便是形式 (5)。更进一步,我们可以利用等式 $Q=r+\gamma V$ 得到形式(6)。
本章将着重介绍形式 (6),即通过时序差分 $\psi_t=r_t + \gamma V^{\pi_\theta}(s_{t+1}) - V^{\pi_\theta}(s_{t})$ 来指导策略梯度进行学习。事实上,用 $Q$ 值或者 $V$ 值本质上也是用奖励来进行指导,但是用神经网络进行估计的方法可以减小方差、提高鲁棒性。除此之外,REINFORCE
算法基于蒙特卡洛采样,只能在序列结束后进行更新,这同时也要求任务具有有限的步数,而 Actor-Critic
算法则可以在每一步之后都进行更新,并且不对任务的步数做限制。
我们将 Actor-Critic
分为两个部分:Actor
(策略网络)和 Critic
(价值网络),如图 10-1 所示。
Actor
要做的是与环境交互,并在Critic
价值函数的指导下用策略梯度学习一个更好的策略。Critic
要做的是通过Actor
与环境交互收集的数据学习一个价值函数,这个价值函数会用于判断在当前状态什么动作是好的,什么动作不是好的,进而帮助Actor
进行策略更新。
Actor
的更新采用策略梯度的原则,那 Critic
如何更新呢?我们将 Critic
价值网络表示为 $V_w$,参数为 $w$。于是,我们可以采取时序差分残差的学习方式,对于单个数据定义如下价值函数的损失函数:
$$ \mathcal{L}(w) = \cfrac{1}{2} (r + \gamma V_{w}(s_{t+1}) - V_{w}(s_{t}))^2 $$
与 DQN
中一样,我们采取类似于目标网络的方法,将上式中 $r + \gamma V_{w}(s_{t+1})$ 作为时序差分目标,不会产生梯度来更新价值函数。因此,价值函数的梯度为:
$$ \nabla_w \mathcal{L}(w) = -(r + \gamma V_{w}(s_{t+1}) - V_{w}(s_{t})) \nabla_w V_{w}(s_{t}) $$
然后使用梯度下降方法来更新 Critic
价值网络参数即可。
Actor-Critic
算法的具体流程如下:
- 初始化策略网络参数 $\theta$,价值网络参数 $w$
- for 序列 $e=1 \rightarrow E$ do:
- 用当前策略 $\theta_\pi$ 采样轨迹 $\{ s_1, a_1, r_1, s_2, a_2, r_2, ,\cdots \}$
- 为每一步数据计算: $\delta_t = r + \gamma V_{w}(s_{t+1}) - V_{w}(s_{t})$
- 更新价值参数 $w = w + \alpha_w\sum_t \delta_t \nabla_w V_{w}(s_{t})$
- 更新策略参数 $\theta = \theta + \alpha_\theta \sum_t \delta_t \nabla_\theta \log \pi_\theta(a_t|s_t)$
- end for
10.3 Actor-Critic 代码实践 #
import torch
import numpy as np
import gymnasium as gym
import torch.nn.functional as F
import matplotlib.pyplot as plt
import rl_utils
class PolicyNet(torch.nn.Module):
def __init__(self, state_dim, hidden_dim, action_dim):
super(PolicyNet, self).__init__()
self.fc1 = torch.nn.Linear(state_dim, hidden_dim)
self.fc2 = torch.nn.Linear(hidden_dim, action_dim)
def forward(self, x):
x = F.relu(self.fc1(x))
return F.softmax(self.fc2(x), dim=1)
class ValueNet(torch.nn.Module):
def __init__(self, state_dim, hidden_dim):
super(ValueNet, self).__init__()
self.fc1 = torch.nn.Linear(state_dim, hidden_dim)
self.fc2 = torch.nn.Linear(hidden_dim, 1)
def forward(self, x):
x = F.relu(self.fc1(x))
return self.fc2(x)
class ActorCritic:
def __init__(self, state_dim, hidden_dim, action_dim, actor_lr, critic_lr,
gamma, device):
# 策略网络
self.actor = PolicyNet(state_dim, hidden_dim, action_dim).to(device)
self.critic = ValueNet(state_dim, hidden_dim).to(device) # 价值网络
# 策略网络优化器
self.actor_optimizer = torch.optim.Adam(self.actor.parameters(),
lr=actor_lr)
self.critic_optimizer = torch.optim.Adam(self.critic.parameters(),
lr=critic_lr) # 价值网络优化器
self.gamma = gamma
self.device = device
def take_action(self, state):
state = torch.tensor([state], dtype=torch.float).to(self.device)
probs = self.actor(state)
action_dist = torch.distributions.Categorical(probs)
action = action_dist.sample()
return action.item()
def update(self, transition_dict):
states = torch.tensor(transition_dict['states'],
dtype=torch.float).to(self.device)
actions = torch.tensor(transition_dict['actions']).view(-1, 1).to(
self.device)
rewards = torch.tensor(transition_dict['rewards'],
dtype=torch.float).view(-1, 1).to(self.device)
next_states = torch.tensor(transition_dict['next_states'],
dtype=torch.float).to(self.device)
dones = torch.tensor(transition_dict['dones'],
dtype=torch.float).view(-1, 1).to(self.device)
# 时序差分目标
td_target = rewards + self.gamma * self.critic(next_states) * (1 -
dones)
td_delta = td_target - self.critic(states) # 时序差分误差
log_probs = torch.log(self.actor(states).gather(1, actions))
actor_loss = torch.mean(-log_probs * td_delta.detach())
# 均方误差损失函数
critic_loss = torch.mean(
F.mse_loss(self.critic(states), td_target.detach()))
self.actor_optimizer.zero_grad()
self.critic_optimizer.zero_grad()
actor_loss.backward() # 计算策略网络的梯度
critic_loss.backward() # 计算价值网络的梯度
self.actor_optimizer.step() # 更新策略网络的参数
self.critic_optimizer.step() # 更新价值网络的参数
actor_lr = 1e-3
critic_lr = 1e-2
num_episodes = 1000
hidden_dim = 128
gamma = 0.98
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
env_name = "CartPole-v1"
env = gym.make(env_name)
np.random.seed(0)
torch.manual_seed(0)
state_dim = env.observation_space.shape[0]
action_dim = env.action_space.n
agent = ActorCritic(state_dim, hidden_dim, action_dim, actor_lr, critic_lr, gamma, device)
return_list = rl_utils.train_on_policy_agent(env, agent, num_episodes)
episodes_list = list(range(len(return_list)))
plt.plot(episodes_list, return_list)
plt.xlabel('Episodes')
plt.ylabel('Returns')
plt.title('Actor-Critic on {}'.format(env_name))
plt.show()
mv_return = rl_utils.moving_average(return_list, 9)
plt.plot(episodes_list, mv_return)
plt.xlabel('Episodes')
plt.ylabel('Returns')
plt.title('Actor-Critic on {}'.format(env_name))
plt.show()
Iteration 0: 100%|█████████████████████████████| 100/100 [00:00<00:00, 527.64it/s, episode=100, return=22.600]
Iteration 1: 100%|█████████████████████████████| 100/100 [00:00<00:00, 375.04it/s, episode=200, return=58.800]
Iteration 2: 100%|████████████████████████████| 100/100 [00:00<00:00, 245.43it/s, episode=300, return=104.200]
Iteration 3: 100%|████████████████████████████| 100/100 [00:00<00:00, 114.04it/s, episode=400, return=204.400]
Iteration 4: 100%|█████████████████████████████| 100/100 [00:01<00:00, 80.29it/s, episode=500, return=240.900]
Iteration 5: 100%|█████████████████████████████| 100/100 [00:01<00:00, 56.73it/s, episode=600, return=389.200]
Iteration 6: 100%|█████████████████████████████| 100/100 [00:02<00:00, 43.15it/s, episode=700, return=500.000]
Iteration 7: 100%|█████████████████████████████| 100/100 [00:02<00:00, 40.64it/s, episode=800, return=500.000]
Iteration 8: 100%|█████████████████████████████| 100/100 [00:02<00:00, 40.61it/s, episode=900, return=500.000]
Iteration 9: 100%|████████████████████████████| 100/100 [00:02<00:00, 40.20it/s, episode=1000, return=500.000]
根据实验结果我们可以发现,Actor-Critic
算法很快便能收敛到最优策略,并且训练过程非常稳定,抖动情况相比 REINFORCE
算法有了明显的改进,这说明价值函数的引入减小了方差。