【深度强化学习】Actor-Critic 算法
本书之前的章节讲解了基于值函数的方法(DQN)和基于策略的方法(REINFORCE),其中基于值函数的方法只学习一个价值函数,而基于策略的方法只学习一个策略函数。那么,一个很自然的问题是,有没有什么方法既学习价值函数,又学习策略函数呢? 答案就是 Actor-Critic。Actor-Critic 是囊括一系列算法的整体架构,目前很多高效的前沿算法都属于 Actor-Critic 算法,本章接下来将会介绍一种最简单的 Actor-Critic 算法。需要明确的是,Actor-Critic 算法本质上是基于策略的算法,因为这一系列算法的目标都是优化一个带参数的策略,只是会额外学习价值函数,从而帮助策略函数更好地学习。
回顾策略梯度
回顾一下,在 REINFORCE 算法中,目标函数的梯度中有一项轨迹回报,用于指导策略的更新。REINFOCE 算法用蒙特卡洛方法来估计Q(s,a)Q(s,a),能不能考虑拟合一个值函数来指导策略进行学习呢?这正是 Actor-Critic 算法所做的。在策略梯度中,可以把梯度写成下面这个更加一般的形式:g=E[∑t=0Tψt∇θlogπθ(at∣st)]g=\mathbb{E}\left[\sum_{t=0}^T\psi_t\nabla_\theta\log\pi_\theta(a_t|s_t)\right]其中,ψt\psi_t可以有很多种形式:& 6.r_t+\gamma V^{\pi_\theta}(s_{t+1})-V^{\pi_\theta}(s_t):\text{时序差分残差。}
REINFORCE 通过蒙特卡洛采样的方法对策略梯度的估计是无偏的,但是方差非常大。我们可以用形式(3)引入基线函数(baseline function) b(st)b(s_t)来减小方差。此外,我们也可以采用 Actor-Critic 算法估计一个动作价值函数QQ,代替蒙特卡洛采样得到的回报,这便是形式(4)。这个时候,我们可以把状态价值函数VV作为基线,从QQ函数减去这个VV函数则得到了AA函数,我们称之为优势函数(advantage function) ,这便是形式(5)。更进一步,我们可以利用等式Q=r+γVQ=r+\gamma V得到形式(6)。
本文将着重介绍形式(6),即通过时序差分残差ψt=rt+γVπ(st+1)−Vπ(st)\psi_t=r_t+\gamma V{\pi}(s_{t+1})-V{\pi}(s_t)来指导策略梯度进行学习。事实上,用值或者值本质上也是用奖励来进行指导,但是用神经网络进行估计的方法可以减小方差、提高鲁棒性。除此之外,REINFORCE 算法基于蒙特卡洛采样,只能在序列结束后进行更新,这同时也要求任务具有有限的步数,而 Actor-Critic 算法则可以在每一步之后都进行更新,并且不对任务的步数做限制。
学到这里兴许你会产生一些疑问,至少下面是我当时产生的疑问:
Q1: 为什么减去基线函数为什么能够减小方差?
在强化学习等相关领域中,∑t′=tTγt′−trt′−b(st)\sum_{t' = t}^{T} \gamma^{t' - t} r_{t'} - b(s_t) 这种形式常见于策略梯度方法中引入基线(baseline)的情形,以下从几个方面解释减去基线函数能够减小方差的原因:
1. 策略梯度的基本形式
策略梯度算法用于优化策略网络的参数,目标是最大化累计奖励的期望。策略梯度的一般形式为 ∇θJ(θ)≈Eτ∼πθ[(∑t=0Tr(st,at))∇θlogπθ(at∣st)]\nabla_{\theta} J(\theta) \approx \mathbb{E}{\tau \sim \pi{\theta}} \left[ \left(\sum_{t = 0}^{T} r(s_t, a_t) \right) \nabla_{\theta} \log \pi_{\theta}(a_t | s_t) \right] ,其中 τ\tau 是一个轨迹,r(st,at)r(s_t, a_t) 是在状态 sts_t 采取动作 ata_t 获得的奖励,πθ(at∣st)\pi_{\theta}(a_t | s_t) 是策略网络在状态 sts_t 采取动作 ata_t 的概率。
2. 引入基线函数的策略梯度
为了减小策略梯度估计的方差,引入基线函数 b(st)b(s_t) ,改进后的策略梯度变为 ∇θJ(θ)≈Eτ∼πθ[(∑t=0Tr(st,at)−b(st))∇θlogπθ(at∣st)]\nabla_{\theta} J(\theta) \approx \mathbb{E}{\tau \sim \pi{\theta}} \left[ \left(\sum_{t = 0}^{T} r(s_t, a_t) - b(s_t) \right) \nabla_{\theta} \log \pi_{\theta}(a_t | s_t) \right] 。
3. 减小方差的原理
- 期望不变性 :只要基线函数 b(st)b(s_t) 仅依赖于状态 sts_t (而不依赖于动作 ata_t ),那么 Eat∼πθ(at∣st)[b(st)]=b(st)\mathbb{E}{a_t \sim \pi{\theta}(a_t | s_t)} [b(s_t)] = b(s_t) 。这意味着 Eτ∼πθ[(∑t=0Tr(st,at)−b(st))∇θlogπθ(at∣st)]=Eτ∼πθ[(∑t=0Tr(st,at))∇θlogπθ(at∣st)]\mathbb{E}{\tau \sim \pi{\theta}} \left[ \left(\sum_{t = 0}^{T} r(s_t, a_t) - b(s_t) \right) \nabla_{\theta} \log \pi_{\theta}(a_t | s_t) \right] = \mathbb{E}{\tau \sim \pi{\theta}} \left[ \left(\sum_{t = 0}^{T} r(s_t, a_t) \right) \nabla_{\theta} \log \pi_{\theta}(a_t | s_t) \right] ,即引入基线函数不会改变策略梯度的期望,保证了算法的正确性。
- 方差减小 :通过选择合适的基线函数(例如状态价值函数 Vπ(st)V^{\pi}(s_t) 的估计),可以使得 ∑t=0Tr(st,at)−b(st)\sum_{t = 0}^{T} r(s_t, a_t) - b(s_t) 的方差小于 ∑t=0Tr(st,at)\sum_{t = 0}^{T} r(s_t, a_t) 的方差。直观地说,基线函数起到了一个参考点的作用,它去除了一些与动作选择无关的背景噪声。例如,如果某个状态本身就倾向于产生高奖励,减去一个基于状态的基线函数可以抵消这种状态本身的奖励倾向,使得不同动作的优劣更加凸显,从而减小了策略梯度估计的方差。
总之,减去基线函数在不改变策略梯度期望的前提下,通过去除状态相关的奖励偏差,有效地减小了策略梯度估计的方差,提高了强化学习算法的稳定性和收敛速度。
Q2: 为什么引入基线函数不会改变策略梯度的期望
在强化学习策略梯度相关内容中,引入基线函数不改变策略梯度期望,原因如下:
前提条件
策略梯度的目标是最大化累计奖励的期望,策略梯度原始形式为 ∇θJ(θ)≈Eτ∼πθ[(∑t=0Tr(st,at))∇θlogπθ(at∣st)]\nabla_{\theta}J(\theta) \approx \mathbb{E}{\tau \sim \pi{\theta}}\left[\left(\sum_{t = 0}^{T}r(s_t, a_t)\right)\nabla_{\theta}\log\pi_{\theta}(a_t|s_t)\right] ,其中 τ\tau 是轨迹,r(st,at)r(s_t, a_t) 是在状态 sts_t 采取动作 ata_t 获得的奖励,πθ(at∣st)\pi_{\theta}(a_t|s_t) 是策略网络在状态 sts_t 采取动作 ata_t 的概率。
引入基线函数 b(st)b(s_t) 后,策略梯度变为 ∇θJ(θ)≈Eτ∼πθ[(∑t=0Tr(st,at)−b(st))∇θlogπθ(at∣st)]\nabla_{\theta}J(\theta) \approx \mathbb{E}{\tau \sim \pi{\theta}}\left[\left(\sum_{t = 0}^{T}r(s_t, a_t) - b(s_t)\right)\nabla_{\theta}\log\pi_{\theta}(a_t|s_t)\right] 。
期望推导
根据期望的性质,对于随机变量 XX 和 YY ,有 E[X−Y]=E[X]−E[Y]\mathbb{E}[X - Y] = \mathbb{E}[X] - \mathbb{E}[Y] 。这里令 X=(∑t=0Tr(st,at))∇θlogπθ(at∣st)X = \left(\sum_{t = 0}^{T}r(s_t, a_t)\right)\nabla_{\theta}\log\pi_{\theta}(a_t|s_t) ,Y=b(st)∇θlogπθ(at∣st)Y = b(s_t)\nabla_{\theta}\log\pi_{\theta}(a_t|s_t) 。
由于基线函数 b(st)b(s_t) 只依赖于状态 sts_t ,不依赖于动作 ata_t ,那么对于给定状态 sts_t ,在动作 ata_t 上的期望 Eat∼πθ(at∣st)[b(st)]=b(st)\mathbb{E}{a_t \sim \pi{\theta}(a_t|s_t)}[b(s_t)] = b(s_t) 。
所以 Eτ∼πθ[b(st)∇θlogπθ(at∣st)]=Eτ∼πθ[Eat∼πθ(at∣st)[b(st)]∇θlogπθ(at∣st)]=Eτ∼πθ[b(st)Eat∼πθ(at∣st)∇θlogπθ(at∣st)]\mathbb{E}{\tau \sim \pi{\theta}}\left[b(s_t)\nabla_{\theta}\log\pi_{\theta}(a_t|s_t)\right] = \mathbb{E}{\tau \sim \pi{\theta}}\left[\mathbb{E}{a_t \sim \pi{\theta}(a_t|s_t)}[b(s_t)]\nabla_{\theta}\log\pi_{\theta}(a_t|s_t)\right] = \mathbb{E}{\tau \sim \pi{\theta}}\left[b(s_t)\mathbb{E}{a_t \sim \pi{\theta}(a_t|s_t)}\nabla_{\theta}\log\pi_{\theta}(a_t|s_t)\right] 。
又因为 Eat∼πθ(at∣st)∇θlogπθ(at∣st)=0\mathbb{E}{a_t \sim \pi{\theta}(a_t|s_t)}\nabla_{\theta}\log\pi_{\theta}(a_t|s_t) = 0 (这是概率分布的一个性质,对概率分布的对数求期望关于参数的梯度为 0 ),所以 Eτ∼πθ[b(st)∇θlogπθ(at∣st)]=0\mathbb{E}{\tau \sim \pi{\theta}}\left[b(s_t)\nabla_{\theta}\log\pi_{\theta}(a_t|s_t)\right] = 0 。
则 Eτ∼πθ[(∑t=0Tr(st,at)−b(st))∇θlogπθ(at∣st)]=Eτ∼πθ[(∑t=0Tr(st,at))∇θlogπθ(at∣st)]−Eτ∼πθ[b(st)∇θlogπθ(at∣st)]=Eτ∼πθ[(∑t=0Tr(st,at))∇θlogπθ(at∣st)]\mathbb{E}{\tau \sim \pi{\theta}}\left[\left(\sum_{t = 0}^{T}r(s_t, a_t) - b(s_t)\right)\nabla_{\theta}\log\pi_{\theta}(a_t|s_t)\right] = \mathbb{E}{\tau \sim \pi{\theta}}\left[\left(\sum_{t = 0}^{T}r(s_t, a_t)\right)\nabla_{\theta}\log\pi_{\theta}(a_t|s_t)\right] - \mathbb{E}{\tau \sim \pi{\theta}}\left[b(s_t)\nabla_{\theta}\log\pi_{\theta}(a_t|s_t)\right] = \mathbb{E}{\tau \sim \pi{\theta}}\left[\left(\sum_{t = 0}^{T}r(s_t, a_t)\right)\nabla_{\theta}\log\pi_{\theta}(a_t|s_t)\right] 。
综上,引入仅依赖于状态的基线函数不会改变策略梯度的期望。
Q3: 为什么对概率分布的对数求期望关于参数的梯度为 0?
设策略 πθ(a∣s)\pi_{\theta}(a|s) 是关于参数 θ\theta 的概率分布,表示在状态 ss 下采取动作 aa 的概率。我们要证明 Ea∼πθ(a∣s)[∇θlogπθ(a∣s)]=0\mathbb{E}{a\sim\pi{\theta}(a|s)}\left[\nabla_{\theta}\log\pi_{\theta}(a|s)\right]=0 ,推导过程如下:
1. 根据期望的定义展开
期望的定义为 Ea∼πθ(a∣s)[f(a)]=∑aπθ(a∣s)f(a)\mathbb{E}{a\sim\pi{\theta}(a|s)}[f(a)]=\sum_{a}\pi_{\theta}(a|s)f(a) (对于离散动作空间,若为连续动作空间则是积分形式 ∫πθ(a∣s)f(a)da\int\pi_{\theta}(a|s)f(a)da ,这里以离散为例说明,连续情况类似),那么 Ea∼πθ(a∣s)[∇θlogπθ(a∣s)]=∑aπθ(a∣s)∇θlogπθ(a∣s)\mathbb{E}{a\sim\pi{\theta}(a|s)}\left[\nabla_{\theta}\log\pi_{\theta}(a|s)\right]=\sum_{a}\pi_{\theta}(a|s)\nabla_{\theta}\log\pi_{\theta}(a|s) 。
2. 利用对数函数的求导性质
根据对数函数求导公式 ∇θlogx=1x∇θx\nabla_{\theta}\log x = \frac{1}{x}\nabla_{\theta}x ,对于 πθ(a∣s)\pi_{\theta}(a|s) ,有 ∇θlogπθ(a∣s)=1πθ(a∣s)∇θπθ(a∣s)\nabla_{\theta}\log\pi_{\theta}(a|s)=\frac{1}{\pi_{\theta}(a|s)}\nabla_{\theta}\pi_{\theta}(a|s) 。
3. 代入期望表达式并化简
将 ∇θlogπθ(a∣s)=1πθ(a∣s)∇θπθ(a∣s)\nabla_{\theta}\log\pi_{\theta}(a|s)=\frac{1}{\pi_{\theta}(a|s)}\nabla_{\theta}\pi_{\theta}(a|s) 代入 ∑aπθ(a∣s)∇θlogπθ(a∣s)\sum_{a}\pi_{\theta}(a|s)\nabla_{\theta}\log\pi_{\theta}(a|s) 中,可得:
∑aπθ(a∣s)∇θlogπθ(a∣s)=∑aπθ(a∣s)1πθ(a∣s)∇θπθ(a∣s)=∑a∇θπθ(a∣s)
4. 利用概率分布的性质
因为概率分布满足 ∑aπθ(a∣s)=1\sum_{a}\pi_{\theta}(a|s)=1 ,对其两边关于参数 θ\theta 求梯度,根据求导的线性性质(和的导数等于导数的和),有 ∇θ∑aπθ(a∣s)=∑a∇θπθ(a∣s)\nabla_{\theta}\sum_{a}\pi_{\theta}(a|s)=\sum_{a}\nabla_{\theta}\pi_{\theta}(a|s) ,而常数 1 关于参数 θ\theta 的梯度为 0 ,即 ∇θ∑aπθ(a∣s)=0\nabla_{\theta}\sum_{a}\pi_{\theta}(a|s) = 0 ,所以 ∑a∇θπθ(a∣s)=0\sum_{a}\nabla_{\theta}\pi_{\theta}(a|s)=0 。
综上,Ea∼πθ(a∣s)[∇θlogπθ(a∣s)]=0\mathbb{E}{a\sim\pi{\theta}(a|s)}\left[\nabla_{\theta}\log\pi_{\theta}(a|s)\right]=0 ,即对概率分布的对数求期望关于参数的梯度为 0 。
Actor-Critic 算法
我们将 Actor-Critic 分为两个部分:Actor(策略网络)和 Critic(价值网络),如图 10-1 所示。

- Actor 要做的是与环境交互,并在 Critic 价值函数的指导下用策略梯度学习一个更好的策略。
- Critic 要做的是通过 Actor 与环境交互收集的数据学习一个价值函数,这个价值函数会用于判断在当前状态什么动作是好的,什么动作不是好的,进而帮助 Actor 进行策略更新。
Actor 的更新采用策略梯度的原则,那 Critic 如何更新呢?我们将 Critic 价值网络表示为VωV_{\omega},参数为ω{\omega}。于是,我们可以采取时序差分残差的学习方式,对于单个数据定义如下价值函数的损失函数:L(ω)=12(r+γVω(st+1)−Vω(st))2\mathcal{L}(\omega)=\frac{1}{2}(r+\gamma V_\omega(s_{t+1})-V_\omega(s_t))^2与 DQN 中一样,我们采取类似于目标网络的方法,将上式中r+γVω(st+1)r+\gamma V_{\omega}(s_{t+1})作为时序差分目标,不会产生梯度来更新价值函数。因此,价值函数的梯度为:∇ωL(ω)=−(r+γVω(st+1)−Vω(st))∇ωVω(st)\nabla_\omega\mathcal{L}(\omega)=-(r+\gamma V_\omega(s_{t+1})-V_\omega(s_t))\nabla_\omega V_\omega(s_t)
然后使用梯度下降方法来更新 Critic 价值网络参数即可。
Actor-Critic 算法的具体流程如下:
-
初始化策略网络参数θ\theta,价值网络参数ω\omega
-
for 序列 e=1→Ee=1\to E do :
- 用当前策略采样轨迹{s1,a1,r1,s2,a2,r2,...}{s_1,a_1,r_1,s_2,a_2,r_2,...}
- 为每一步数据计算δt=rt+γVω(st+1)−Vω(st)\delta_t=r_t+\gamma V_{\omega}(s_{t+1})-V_{\omega}(s_t):
- 更新价值参数ω=ω+αω∑tδt∇ωVω(st)\omega=\omega+\alpha_{\omega}\sum_{t}\delta_t\nabla_{\omega}V_{\omega}(s_t)
- 更新策略参数θ=θ+αθ∑tδt∇θlogπθ(at∣st)\theta=\theta+\alpha_{\theta}\sum_{t}\delta_t\nabla_{\theta}\log \pi_{\theta}(a_t|s_t)
-
end for
虽然这里写的是梯度,但是我们实际写代码的时候只需要把loss给写出来,梯度是loss对参数求导,所以实际上:
lossactor=mean(∑tδtlogπθ(at∣st))losscritic=mean(∑tδtVω(st))\text{loss}{\text{actor}}=\text{mean}\left( \sum_t\delta_t \log\pi{\theta}(a_t|s_t) \right)\ \text{loss}{\text{critic}}=\text{mean}\left( \sum_t\delta_t V{\omega}(s_t) \right) Actor-Critic 代码实践:
# 我们仍然在车杆环境上进行 Actor-Critic 算法的实验。
import gym
import torch
import torch.nn.functional as F
import numpy as np
import matplotlib.pyplot as plt
# rl_utils是作者自己写的文件,在github上去下载:
# https://github.com/boyu-ai/Hands-on-RL/blob/main/rl_utils.py
# kaggle上导入库操作:
from shutil import copyfile
copyfile(src='/kaggle/input/rlutils/rl_utils.py', dst = "../working/rl_utils.py")
import rl_utils
# 首先定义策略网络PolicyNet(与 REINFORCE 算法一样)。
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)
# Actor-Critic 算法中额外引入一个价值网络,接下来的代码定义价值网络ValueNet,其输入是某个状态,输出则是状态的价值。
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)
# 现在定义ActorCritic算法,主要包含采取动作(take_action())和更新网络参数(update())两个函数。
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 和 Critic,我们就可以开始实验了,看看 Actor-Critic 在车杆环境上表现如何吧!
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-v0'
env = gym.make(env_name)
env.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)
python


在 CartPole-v0 环境中,满分就是 200 分。和 REINFORCE 相似,接下来我们绘制训练过程中每一条轨迹的回报变化图以及其经过平滑处理的版本。
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()
python

根据实验结果我们可以发现,Actor-Critic 算法很快便能收敛到最优策略,并且训练过程非常稳定,抖动情况相比 REINFORCE 算法有了明显的改进,这说明价值函数的引入减小了方差。
本章讲解了 Actor-Critic 算法,它是基于值函数的方法和基于策略的方法的叠加。价值模块 Critic 在策略模块 Actor 采样的数据中学习分辨什么是好的动作,什么不是好的动作,进而指导 Actor 进行策略更新(都体现在loss上了) 。随着 Actor 的训练的进行,其与环境交互所产生的数据分布也发生改变,这需要 Critic 尽快适应新的数据分布并给出好的判别 (给它一个更大的学习率)。
Actor-Critic 算法非常实用, TRPO、PPO、DDPG、SAC 等深度强化学习算法都是在 Actor-Critic 框架下进行发展的。深入了解 Actor-Critic 算法对读懂目前深度强化学习的研究热点大有裨益。
