赞
踩
之前的章节介绍了基于策略梯度的算法REINFORCE、Actor-Critic以及两个改进算法–TRPO和PPO。这类算法有一个共同的特点:它们都是在线策略算法,这意味着他们的样本效率比较低。我们回忆以下DQN算法,DQN算法直接估计最优函数Q,可以做到离线策略学习,但是他只能处理动作空间有限的环境,这是因为他需要从所有动作中挑选一个Q值最大的动作。如果动作个数是无限的,虽然我们可以先将动作空间离散化,但是这样做比较粗糙,无法精准控制。本节要讲解的深度确定性策略梯度算法就是用类似的思想来处理动作空间无限的环境并且使用的是离线策略算法,它构造一个确定性策略,用梯度上升的方法来最大化Q值。DDPG也属于一种Actor-Critic算法。之前学习的REINFORCE、TRPO和PPO算法是学习随机性策略,而本章的DDPG则是学习一个确定性策略。
之前我们学习的策略是随机性的,可以表示为 a π θ ( ⋅ ∣ s ) a~\pi_{\theta}(\cdot|s) a πθ(⋅∣s);而如果策略是确定性的,则可以记为 a = μ θ ( s ) a=\mu_\theta(s) a=μθ(s)。于策略梯度定理相似,我们可以推导出确定性策略梯度定理:
∇ θ J ( π θ ) = E s ν π β [ ∇ θ μ θ ( s ) ∇ a Q ω μ ( s , a ) ∣ a = μ θ ( s ) ] \nabla _{\theta}J\left( \pi _{\theta} \right) =\mathbb E_{s~\nu ^{\pi _{\beta}}}\left[ \nabla _{\theta}\mu _{\theta}\left( s \right) \nabla _aQ_{\omega}^{\mu}\left( s,a \right) |_{a=\mu _{\theta}\left( s \right)} \right] ∇θJ(πθ)=Es νπβ[∇θμθ(s)∇aQωμ(s,a)∣a=μθ(s)]
其中, π β \pi_\beta πβ是用来收集数据的行为策略。我们可以这样理解这个定理:假设现在已经有函数Q,给定一个状态s,但由于现在动作空间是无限的,无法通过遍历所有动作来得到Q值最大的动作,因此我们想用策略 μ \mu μ找到使 Q ( s , a ) Q(s,a) Q(s,a)值最大的动作 a a a,即 μ ( s ) = a r g m a x a Q ( s , a ) \mu(s)=arg max_aQ(s,a) μ(s)=argmaxaQ(s,a)。此时Q就是Critic, μ \mu μ就是Actor,这是一个Actor-Critic的框架,如图13-1所示。
那怎么得到这个 μ \mu μ呢?首先用Q对 μ θ \mu_\theta μθ求导 ∇ θ Q ( s , μ θ ( s ) ) \nabla _\theta Q(s,\mu_\theta(s)) ∇θQ(s,μθ(s)),其中会用到梯度的链式法则,先对 a a a求导,在对 θ \theta θ求导。然后通过梯度上升的方法来最大化函数Q,得到Q值最大的动作。
下面来看一下DDPG算法的细节。DDPG要用到4个神经网络,其中Actor和Critic各用一个网络,此外它们都各自有一个目标网络。为什么需要目标网络呢?DDPG也需要目标网络是因为目标网络也会被用在计算目标Q值。DDPG中的目标网络的更新于DQN中略有不同:在DQN中,每隔一段时间将Q网络直接复制给目标Q网络;而在DDPG网络中,目标Q网络的更新采取的是一种软更新的方式,即让目标Q网络缓慢更新,逐渐接近Q网络,其公式为:
ω − < − − τ ω + ( 1 − τ ) ω − \omega^-<--\tau\omega+(1-\tau)\omega^- ω−<−−τω+(1−τ)ω−
通常 τ \tau τ是一个比较小的数,当 τ = 1 \tau=1 τ=1时,就和DQN的更新方式一致了。而目标 μ \mu μ网络也使用这种软更新的方式。
另外,由于函数 Q Q Q存在 Q Q Q过高估计的问题,DDPG采用了Double DQN中的技术来更新 Q Q Q网络。但是,由于DDPG采用的时确定性策略,它本身的探索十分有仙女。回忆一下DQN算法,它的探索主要由 ϵ − \epsilon- ϵ−贪婪策略的行为策略产生。同样作为一种离线策略的算法,DDPG在行为策略上引入一个随机噪声 N \mathcal{N} N来进行探索。
DDPG的具体算法流程:
∇ θ J ≈ 1 N Σ N i = 1 ∇ θ μ θ ( s i ) ∇ a Q ω ( s i , a ) ∣ a = μ θ ( s i ) \nabla _{\theta}J\approx \frac{1}{N}\underset{i=1}{\overset{N}{\varSigma}}\nabla _{\theta}\mu _{\theta}\left( s_i \right) \nabla _aQ_{\omega}\left( s_i,a \right) |_{a=\mu _{\theta}\left( s_i \right)} ∇θJ≈N1i=1ΣN∇θμθ(si)∇aQω(si,a)∣a=μθ(si)
ω − ← τ ω + ( 1 − τ ) ω − θ − ← τ θ + ( 1 − τ ) θ − \omega ^-\gets \tau \omega +\left( 1-\tau \right) \omega ^-\theta ^-\gets \tau \theta +\left( 1-\tau \right) \theta ^- ω−←τω+(1−τ)ω−θ−←τθ+(1−τ)θ−
end for
end for
对于策略网络和价值网络,我们都采用只有一层隐藏层的神经网络。策略网络的输出层用正切函数( y = t a n h x y=tanh x y=tanhx)作为激活函数,这是因为正切函数的值域是[-1,1],方便按比例调整成环境可以接受的动作范围。在DDPG中处理的是与连续动作交互的环境,Q网络的输入是状态和动作拼接后的向量,Q网络的输出是一个值,表示该状态动作对的价值。
import random import gym import numpy as np from tqdm import tqdm import torch from torch import nn 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, action_bound): super(PolicyNet, self).__init__() self.fc1 = torch.nn.Linear(state_dim, hidden_dim) self.fc2 = torch.nn.Linear(hidden_dim, action_dim) self.action_bound = action_bound # action_bound是环境可以接受的动作最大值 def forward(self, x): x = F.relu(self.fc1(x)) return torch.tanh(self.fc2(x)) * self.action_bound # 将连续动作转回来 class QValueNet(torch.nn.Module): def __init__(self, state_dim, hidden_dim, action_dim): super(QValueNet, self).__init__() self.fc1 = torch.nn.Linear(state_dim + action_dim, hidden_dim) self.fc2 = torch.nn.Linear(hidden_dim, hidden_dim) self.fc_out = torch.nn.Linear(hidden_dim, 1) def forward(self, x, a): cat = torch.cat([x, a], dim=1) # 拼接状态和动作 x = F.relu(self.fc1(cat)) x = F.relu(self.fc2(x)) return self.fc_out(x)
接下来是DDPG算法的主体部分。在用策略网络采取动作的时候,为了更好地探索,我们向动作中加入高斯噪声。在DDPG的原始论文中,添加的噪声符合奥恩斯坦-乌伦贝克(OU)随机过程:
Δ x t = θ ( μ − x t − 1 ) + σ W \varDelta x_t=\theta \left( \mu -x_{t-1} \right) +\sigma W Δxt=θ(μ−xt−1)+σW
其中, μ \mu μ是均值,W是符合布朗运动的随机噪声, θ \theta θ和 σ \sigma σ是比例参数。可以看出,当 x t − 1 x_{t-1} xt−1偏离均值时, x t x_t xt的值会向均值靠拢。OU随机过程的特点是在均值附近做出线性负反馈,并有额外的干扰项。OU随机过程是与时间相关的,适用于有惯性的系统,在DDPG的实践中,不少地方仅使用正态分布的噪声。这里为了简单起见,同样使用正态分布的噪声。
class DDPG: ''' DDPG算法 ''' def __init__(self, state_dim, hidden_dim, action_dim, action_bound, sigma, actor_lr, critic_lr, tau, gamma, device): self.actor = PolicyNet(state_dim, hidden_dim, action_dim, action_bound).to(device) self.critic = QValueNet(state_dim, hidden_dim, action_dim).to(device) self.target_actor = PolicyNet(state_dim, hidden_dim, action_dim, action_bound).to(device) self.target_critic = QValueNet(state_dim, hidden_dim, action_dim).to(device) # 初始化目标价值网络并设置和价值网络相同的参数 self.target_critic.load_state_dict(self.critic.state_dict()) # 初始化目标策略网络并设置和策略相同的参数 self.target_actor.load_state_dict(self.actor.state_dict()) 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.sigma = sigma # 高斯噪声的标准差,均值直接设为0 self.tau = tau # 目标网络软更新参数 self.action_dim = action_dim self.device = device def take_action(self, state): state = torch.tensor([state], dtype=torch.float).to(self.device) action = self.actor(state).item() # 给动作添加噪声,增加探索 action = action + self.sigma * np.random.randn(self.action_dim) # 噪声是正态分布的噪声 return action def soft_update(self, net, target_net): for param_target, param in zip(target_net.parameters(), net.parameters()): param_target.data.copy_(param_target.data * (1.0 - self.tau) + param.data * self.tau) def update(self, transition_dict): states = torch.tensor(transition_dict['states'], dtype=torch.float).to(self.device) actions = torch.tensor(transition_dict['actions'], dtype=torch.float).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) next_q_values = self.target_critic(next_states, self.target_actor(next_states)) q_targets = rewards + self.gamma * next_q_values * (1 - dones) critic_loss = torch.mean(F.mse_loss(self.critic(states, actions), q_targets)) self.critic_optimizer.zero_grad() critic_loss.backward() self.critic_optimizer.step() actor_loss = -torch.mean(self.critic(states, self.actor(states))) self.actor_optimizer.zero_grad() actor_loss.backward() self.actor_optimizer.step() self.soft_update(self.actor, self.target_actor) # 软更新策略网络 self.soft_update(self.critic, self.target_critic) # 软更新价值网络 actor_lr = 3e-4 critic_lr = 3e-3 num_episodes = 200 hidden_dim = 64 gamma = 0.98 tau = 0.005 # 软更新参数 buffer_size = 10000 minimal_size = 1000 batch_size = 64 sigma = 0.01 # 高斯噪声标准差 device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") env_name = 'Pendulum-v0' env = gym.make(env_name) random.seed(0) np.random.seed(0) env.seed(0) torch.manual_seed(0) replay_buffer = rl_utils.ReplayBuffer(buffer_size) state_dim = env.observation_space.shape[0] action_dim = env.action_space.shape[0] action_bound = env.action_space.high[0] # 动作最大值 agent = DDPG(state_dim, hidden_dim, action_dim, action_bound, sigma, actor_lr, critic_lr, tau, gamma, device) return_list = rl_utils.train_off_policy_agent(env, agent, num_episodes, replay_buffer, minimal_size, batch_size) episodes_list = list(range(len(return_list))) plt.plot(episodes_list, return_list) plt.xlabel('Episodes') plt.ylabel('Returns') plt.title('DDPG 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('DDPG on {}'.format(env_name)) plt.show()
Iteration 0: 100%|██████████| 20/20 [00:11<00:00, 1.78it/s, episode=20, return=-1266.015]
Iteration 1: 100%|██████████| 20/20 [00:14<00:00, 1.39it/s, episode=40, return=-610.296]
Iteration 2: 100%|██████████| 20/20 [00:14<00:00, 1.37it/s, episode=60, return=-185.336]
Iteration 3: 100%|██████████| 20/20 [00:14<00:00, 1.36it/s, episode=80, return=-201.593]
Iteration 4: 100%|██████████| 20/20 [00:14<00:00, 1.37it/s, episode=100, return=-157.392]
Iteration 5: 100%|██████████| 20/20 [00:14<00:00, 1.39it/s, episode=120, return=-156.995]
Iteration 6: 100%|██████████| 20/20 [00:14<00:00, 1.39it/s, episode=140, return=-175.051]
Iteration 7: 100%|██████████| 20/20 [00:14<00:00, 1.36it/s, episode=160, return=-191.872]
Iteration 8: 100%|██████████| 20/20 [00:14<00:00, 1.38it/s, episode=180, return=-192.037]
Iteration 9: 100%|██████████| 20/20 [00:14<00:00, 1.36it/s, episode=200, return=-204.490]
**该算法和DQN算法的主要区别:**DQN算法是离散的动作,DDPG算法是连续的动作 两个算法最大的区别是在目标网络的更新方面,DQN算法是在训练网络更新多少步后才会再更新目标网络,而DDPG算法是使用软更新方式,让目标网络缓慢更新。
本章讲解了深度确定性策略梯度算法(DDPG),它是面向连续动作空间的深度确定性策略的典型算法。相比于它的先期工作,即确定性梯度算法(DPG),DDPG加入了目标网络和软更新的方法,这对深度模型构建的价值网络和策略网络的稳定学习起到了关键作用。DDPG算法也被引入了多智能体强化学习领域,催生了MADDPG算法,我们会在后续的章节中对此展开讨论。
当括号里有一个参数时,输出的是一维矩阵,有参数大小列数据;
当括号里是两个或者三个参数是,就是相应大小的矩阵
当括号里是一个矩阵时会输出一个和矩阵大小一样的矩阵
作用:函数输出的是标准正态分布的随机数
标准正态分布是以0为均值,以1为标准差的正态分布,记为N(0,1)
使用方法和np.random.randn()方法相同
作用:基本作用也和np.random.randn()相同,最大的不同是,这个函数产生的随机数在0-1之间,但是都符合正态分布。
为95%),在-2.58~+2.58范围内曲线下面积为0.9900(即取值在这个范围的概率为99%)。
使用方法和np.random.randn()方法相同
作用:基本作用也和np.random.randn()相同,最大的不同是,这个函数产生的随机数在0-1之间,但是都符合正态分布。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。