PyTorch 1.x 强化学习秘籍(四)
原文:
zh.annas-archive.org/md5/863e6116b9dfbed5ea6521a90f2b5732译者:飞龙
第八章:实现策略梯度和策略优化
在本章中,我们将专注于策略梯度方法,这是近年来最流行的强化学习技术之一。我们将从实现基础的 REINFORCE 算法开始,并继续改进算法基线。我们还将实现更强大的算法,演员-评论家及其变体,并将其应用于解决 CartPole 和 Cliff Walking 问题。我们还将体验一个具有连续动作空间的环境,并采用高斯分布来解决它。最后的有趣部分,我们将基于交叉熵方法训练一个代理来玩 CartPole 游戏。
本章将涵盖以下实例:
-
实现 REINFORCE 算法
-
开发带基线的 REINFORCE 算法
-
实现演员-评论家算法
-
使用演员-评论家算法解决 Cliff Walking
-
设置连续的 Mountain Car 环境
-
使用优势演员-评论家网络解决连续的 Mountain Car 环境
-
通过交叉熵方法玩 CartPole
实现 REINFORCE 算法
最近的一篇文章指出,策略梯度方法变得越来越流行。它们的学习目标是优化动作的概率分布,以便在给定状态下,更有益的动作将具有更高的概率值。在本章的第一个实例中,我们将讨论 REINFORCE 算法,这是高级策略梯度方法的基础。
REINFORCE 算法也被称为 蒙特卡罗策略梯度,因为它基于蒙特卡罗方法优化策略。具体而言,它使用当前策略从一个回合中收集轨迹样本,并用它们来更新策略参数 θ 。策略梯度的学习目标函数如下:
其梯度可以如下推导:
这里,![] 是返回值,即累积折扣奖励直到时间 t,![]并且是随机策略,确定在给定状态下采取某些动作的概率。由于策略更新是在整个回合结束后和所有样本被收集后进行的,REINFORCE 算法是一种离策略算法。
在计算策略梯度后,我们使用反向传播来更新策略参数。通过更新后的策略,我们展开一个回合,收集一组样本,并使用它们来重复更新策略参数。
现在我们将开发 REINFORCE 算法来解决 CartPole (gym.openai.com/envs/CartPole-v0/) 环境。
如何做...
我们将开发带基线的 REINFORCE 算法来解决 CartPole 环境如下:
- 导入所有必要的包并创建一个 CartPole 实例:
>>> import gym
>>> import torch
>>> import torch.nn as nn >>> env = gym.make('CartPole-v0')
- 让我们从
PolicyNetwork类的__init__方法开始,该方法使用神经网络逼近策略:
>>> class PolicyNetwork():
... def __init__(self, n_state, n_action, n_hidden=50, lr=0.001):
... self.model = nn.Sequential(
... nn.Linear(n_state, n_hidden),
... nn.ReLU(),
... nn.Linear(n_hidden, n_action),
... nn.Softmax(),
... )
... self.optimizer = torch.optim.Adam( self.model.parameters(), lr)
- 接下来,添加
predict方法,计算估计的策略:
>>> def predict(self, s):
... """
... Compute the action probabilities of state s using
the learning model
... @param s: input state
... @return: predicted policy
... """
... return self.model(torch.Tensor(s))
- 现在我们开发训练方法,使用一个情节中收集的样本更新神经网络:
>>> def update(self, returns, log_probs):
... """
... Update the weights of the policy network given
the training samples
... @param returns: return (cumulative rewards) for
each step in an episode
... @param log_probs: log probability for each step
... """
... policy_gradient = []
... for log_prob, Gt in zip(log_probs, returns):
... policy_gradient.append(-log_prob * Gt)
...
... loss = torch.stack(policy_gradient).sum()
... self.optimizer.zero_grad()
... loss.backward()
... self.optimizer.step()
PolicyNetwork类的最终方法是get_action,它基于预测的策略对给定状态采样一个动作:
>>> def get_action(self, s):
... """
... Estimate the policy and sample an action,
compute its log probability
... @param s: input state
... @return: the selected action and log probability
... """
... probs = self.predict(s)
... action = torch.multinomial(probs, 1).item()
... log_prob = torch.log(probs[action])
... return action, log_prob
它还返回所选动作的对数概率,这将作为训练样本的一部分使用。
这就是PolicyNetwork类的全部内容!
- 现在,我们可以开始开发REINFORCE算法,使用一个策略网络模型:
>>> def reinforce(env, estimator, n_episode, gamma=1.0):
... """
... REINFORCE algorithm
... @param env: Gym environment
... @param estimator: policy network
... @param n_episode: number of episodes
... @param gamma: the discount factor
... """
... for episode in range(n_episode):
... log_probs = []
... rewards = []
... state = env.reset()
... while True:
... action, log_prob = estimator.get_action(state)
... next_state, reward, is_done, _ = env.step(action)
... total_reward_episode[episode] += reward
... log_probs.append(log_prob)
... rewards.append(reward)
...
... if is_done:
... returns = []
... Gt = 0
... pw = 0
... for reward in rewards[::-1]:
... Gt += gamma ** pw * reward
... pw += 1
... returns.append(Gt)
... returns = returns[::-1]
... returns = torch.tensor(returns)
... returns = (returns - returns.mean()) / (
... returns.std() + 1e-9)
... estimator.update(returns, log_probs)
... print('Episode: {}, total reward: {}'.format( episode, total_reward_episode[episode]))
... break
...
... state = next_state
- 我们指定策略网络的大小(输入、隐藏和输出层)、学习率,然后相应地创建
PolicyNetwork实例:
>>> n_state = env.observation_space.shape[0]
>>> n_action = env.action_space.n
>>> n_hidden = 128
>>> lr = 0.003
>>> policy_net = PolicyNetwork(n_state, n_action, n_hidden, lr)
我们将折扣因子设置为0.9:
>>> gamma = 0.9
- 我们使用刚开发的策略网络执行 REINFORCE 算法的学习,共 500 个情节,并跟踪每个情节的总回报:
>>> n_episode = 500
>>> total_reward_episode = [0] * n_episode
>>> reinforce(env, policy_net, n_episode, gamma)
- 现在让我们显示随时间变化的回报情节图:
>>> import matplotlib.pyplot as plt
>>> plt.plot(total_reward_episode)
>>> plt.title('Episode reward over time')
>>> plt.xlabel('Episode') >>> plt.ylabel('Total reward')
>>> plt.show()
它的工作原理...
在Step 2中,为了简单起见,我们使用一个隐藏层的神经网络。策略网络的输入是一个状态,接着是一个隐藏层,输出是可能采取的个别动作的概率。因此,我们使用 softmax 函数作为输出层的激活函数。
Step 4 用于更新网络参数:给定在一个情节中收集的所有数据,包括所有步骤的回报和对数概率,我们计算策略梯度,然后通过反向传播相应地更新策略参数。
在 Step 6 中,REINFORCE 算法执行以下任务:
-
它运行一个情节:对于情节中的每一步,根据当前估计的策略采样一个动作;它在每一步存储回报和对数策略。
-
一旦一个情节结束,它计算每一步的折扣累积回报;通过减去它们的平均值然后除以它们的标准差来对结果进行归一化。
-
它使用回报和对数概率计算策略梯度,然后更新策略参数。我们还显示每个情节的总回报。
-
它通过重复上述步骤运行
n_episode个情节。
Step 8 将生成以下训练日志:
Episode: 0, total reward: 12.0
Episode: 1, total reward: 18.0
Episode: 2, total reward: 23.0
Episode: 3, total reward: 23.0
Episode: 4, total reward: 11.0
……
……
Episode: 495, total reward: 200.0
Episode: 496, total reward: 200.0
Episode: 497, total reward: 200.0
Episode: 498, total reward: 200.0
Episode: 499, total reward: 200.0
您将在Step 9中观察以下情节:
您可以看到最近 200 个情节中的大部分奖励最高值为+200。
REINFORCE 算法是一系列策略梯度方法的家族,通过以下规则直接更新策略参数:
在这里,α是学习率,![],作为动作的概率映射,而![],作为累积折现奖励,是在一个 episode 中收集的经验。由于训练样本集仅在完成整个 episode 后构建,因此 REINFORCE 中的学习是以离线策略进行的。学习过程可以总结如下:
-
随机初始化策略参数θ。
-
根据当前策略选择动作执行一个 episode。
-
在每个步骤中,存储所选动作的对数概率以及产生的奖励。
-
计算各步骤的回报。
-
使用对数概率和回报计算策略梯度,并通过反向传播更新策略参数θ。
-
重复步骤 2至5。
同样地,由于 REINFORCE 算法依赖于由随机策略生成的完整轨迹,因此它构成了一种蒙特卡洛方法。
参见:
推导策略梯度方程相当棘手。它利用了对数导数技巧。如果你想知道,这里有一个详细的解释:
开发带基线的 REINFORCE 算法
在 REINFORCE 算法中,蒙特卡洛模拟在一个 episode 中播放整个轨迹,然后用于更新策略。然而,随机策略可能在不同的 episode 中在相同状态下采取不同的行动。这可能会导致训练时的混淆,因为一个采样经验希望增加选择某个动作的概率,而另一个采样经验可能希望减少它。为了减少这种高方差问题,在传统 REINFORCE 中,我们将开发一种变体算法,即带基线的 REINFORCE 算法。
在带基线的 REINFORCE 中,我们从回报 G 中减去基线状态值。因此,我们在梯度更新中使用了优势函数 A,描述如下:
这里,V(s)是估计给定状态的状态值函数。通常,我们可以使用线性函数或神经网络来逼近状态值。通过引入基线值,我们可以根据状态给出的平均动作校准奖励。
我们使用两个神经网络开发了带基线的 REINFORCE 算法,一个用于策略,另一个用于值估计,以解决 CartPole 环境。
如何实现...
我们使用 REINFORCE 算法解决 CartPole 环境的方法如下:
- 导入所有必要的包并创建一个 CartPole 实例:
>>> import gym
>>> import torch
>>> import torch.nn as nn >>> from torch.autograd import Variable
>>> env = gym.make('CartPole-v0')
- 关于策略网络部分,基本上与我们在实现 REINFORCE 算法配方中使用的
PolicyNetwork类相同。请记住,在update方法中使用了优势值:
>>> def update(self, advantages, log_probs):
... """
... Update the weights of the policy network given
the training samples
... @param advantages: advantage for each step in an episode
... @param log_probs: log probability for each step
... """
... policy_gradient = []
... for log_prob, Gt in zip(log_probs, advantages):
... policy_gradient.append(-log_prob * Gt)
...
... loss = torch.stack(policy_gradient).sum()
... self.optimizer.zero_grad()
... loss.backward()
... self.optimizer.step()
- 对于价值网络部分,我们使用了一个带有一个隐藏层的回归神经网络:
>>> class ValueNetwork():
... def __init__(self, n_state, n_hidden=50, lr=0.05):
... self.criterion = torch.nn.MSELoss()
... self.model = torch.nn.Sequential(
... torch.nn.Linear(n_state, n_hidden),
... torch.nn.ReLU(),
... torch.nn.Linear(n_hidden, 1)
... )
... self.optimizer = torch.optim.Adam( self.model.parameters(), lr)
它的学习目标是近似状态值;因此,我们使用均方误差作为损失函数。
update方法通过反向传播训练值回归模型,使用一组输入状态和目标输出,当然是:
... def update(self, s, y):
... """
... Update the weights of the DQN given a training sample
... @param s: states
... @param y: target values
... """
... y_pred = self.model(torch.Tensor(s))
... loss = self.criterion(y_pred, Variable(torch.Tensor(y)))
... self.optimizer.zero_grad()
... loss.backward()
... self.optimizer.step()
而predict方法则是用来估计状态值:
... def predict(self, s):
... """
... Compute the Q values of the state for all actions
using the learning model
... @param s: input state
... @return: Q values of the state for all actions
... """
... with torch.no_grad():
... return self.model(torch.Tensor(s))
- 现在,我们可以继续开发基准 REINFORCE 算法,其中包括一个策略和价值网络模型:
>>> def reinforce(env, estimator_policy, estimator_value,
n_episode, gamma=1.0):
... """
... REINFORCE algorithm with baseline
... @param env: Gym environment
... @param estimator_policy: policy network
... @param estimator_value: value network
... @param n_episode: number of episodes
... @param gamma: the discount factor
... """
... for episode in range(n_episode):
... log_probs = []
... states = []
... rewards = []
... state = env.reset()
... while True:
... states.append(state)
... action, log_prob =
estimator_policy.get_action(state)
... next_state, reward, is_done, _ = env.step(action)
... total_reward_episode[episode] += reward
... log_probs.append(log_prob)
... rewards.append(reward)
...
... if is_done:
... Gt = 0
... pw = 0
... returns = []
... for t in range(len(states)-1, -1, -1):
... Gt += gamma ** pw * rewards[t]
... pw += 1
... returns.append(Gt)
... returns = returns[::-1]
... returns = torch.tensor(returns)
... baseline_values =
estimator_value.predict(states)
... advantages = returns - baseline_values
... estimator_value.update(states, returns)
... estimator_policy.update(advantages, log_probs)
... print('Episode: {}, total reward: {}'.format( episode, total_reward_episode[episode]))
... break
... state = next_state
- 我们指定策略网络的大小(输入、隐藏和输出层)、学习率,然后相应地创建一个
PolicyNetwork实例:
>>> n_state = env.observation_space.shape[0]
>>> n_action = env.action_space.n
>>> n_hidden_p = 64
>>> lr_p = 0.003
>>> policy_net = PolicyNetwork(n_state, n_action, n_hidden_p, lr_p)
至于价值网络,我们也设置了其大小并创建了一个实例:
>>> n_hidden_v = 64
>>> lr_v = 0.003
>>> value_net = ValueNetwork(n_state, n_hidden_v, lr_v)
我们将折扣因子设置为0.9:
>>> gamma = 0.9
- 我们使用基准的 REINFORCE 算法进行 2,000 个 episode 的学习,并且我们还会追踪每个 episode 的总奖励:
>>> n_episode = 2000
>>> total_reward_episode = [0] * n_episode
>>> reinforce(env, policy_net, value_net, n_episode, gamma)
- 现在,我们展示随时间变化的 episode 奖励的图表:
>>> import matplotlib.pyplot as plt
>>> plt.plot(total_reward_episode)
>>> plt.title('Episode reward over time')
>>> plt.xlabel('Episode')
>>> plt.ylabel('Total reward')
>>> plt.show()
工作原理...
REINFORCE 高度依赖蒙特卡洛方法生成用于训练策略网络的整个轨迹。然而,在相同的随机策略下,不同的 episode 可能会采取不同的动作。为了减少采样经验的方差,我们从返回中减去状态值。由此产生的优势度量了相对于平均动作的奖励,这将在梯度更新中使用。
在步骤 4中,使用基准的 REINFORCE 算法执行以下任务:
-
它运行一个 episode——处理状态、奖励,并且每一步记录策略的日志。
-
一旦一个 episode 完成,它会计算每一步的折扣累积奖励;它通过价值网络估计基准值;它通过从返回中减去基准值计算优势值。
-
它使用优势值和对数概率计算策略梯度,并更新策略和价值网络。我们还显示每个 episode 的总奖励。
-
它通过重复上述步骤运行
n_episode个 episode。
执行步骤 7中的代码将导致以下图表:
你可以看到,在大约 1,200 个 episode 后,性能非常稳定。
通过额外的价值基准,我们能够重新校准奖励并减少梯度估计的方差。
实施演员-评论家算法
在基准 REINFORCE 算法中,有两个独立的组成部分,策略模型和价值函数。实际上,我们可以结合这两个组件的学习,因为学习价值函数的目标是更新策略网络。这就是演员-评论家算法所做的事情,这也是我们将在本文中开发的内容。
演员-评论家算法的网络包括以下两个部分:
-
演员:它接收输入状态并输出动作概率。本质上,通过使用评论家提供的信息来更新模型,它学习最优策略。
-
评论家:这评估了在输入状态时表现良好的价值函数。价值指导演员如何调整。
这两个组件在网络中共享输入和隐藏层的参数,这样学习效率比分开学习更高。因此,损失函数是两部分的总和,具体是测量演员的动作的负对数似然和估计和计算回报之间的均方误差测量评论家。
演员-评论家算法的一个更受欢迎的版本是优势演员-评论家(A2C)。正如其名称所示,评论部分计算优势值,而不是状态值,这类似于带基线的 REINFORCE。它评估了一个动作在一个状态下相对于其他动作的优越性,并且已知可以减少策略网络中的方差。
如何做...
我们开发演员-评论家算法以解决 CartPole 环境,具体如下:
- 导入所有必要的包并创建一个 CartPole 实例:
>>> import gym
>>> import torch
>>> import torch.nn as nn
>>> import torch.nn.functional as F >>> env = gym.make('CartPole-v0')
- 让我们从演员-评论家神经网络模型开始:
>>> class ActorCriticModel(nn.Module):
... def __init__(self, n_input, n_output, n_hidden):
... super(ActorCriticModel, self).__init__()
... self.fc = nn.Linear(n_input, n_hidden)
... self.action = nn.Linear(n_hidden, n_output)
... self.value = nn.Linear(n_hidden, 1)
...
... def forward(self, x):
... x = torch.Tensor(x)
... x = F.relu(self.fc(x))
... action_probs = F.softmax(self.action(x), dim=-1)
... state_values = self.value(x)
... return action_probs, state_values
- 我们继续使用演员-评论家神经网络开发
PolicyNetwork类的__init__方法:
>>> class PolicyNetwork():
... def __init__(self, n_state, n_action,
n_hidden=50, lr=0.001):
... self.model = ActorCriticModel( n_state, n_action, n_hidden)
... self.optimizer = torch.optim.Adam( self.model.parameters(), lr)
... self.scheduler = torch.optim.lr_scheduler.StepLR( self.optimizer, step_size=10, gamma=0.9)
请注意,我们在此处使用了一个学习率减少器,根据学习进展动态调整学习率。
- 接下来,我们添加
predict方法,它计算估计的动作概率和状态值:
>>> def predict(self, s):
... """
... Compute the output using the Actor Critic model
... @param s: input state
... @return: action probabilities, state_value
... """
... return self.model(torch.Tensor(s))
- 现在,我们开发
training方法,用于使用在一个 episode 中收集的样本更新神经网络:
>>> def update(self, returns, log_probs, state_values):
... """
... Update the weights of the Actor Critic network
given the training samples
... @param returns: return (cumulative rewards) for
each step in an episode
... @param log_probs: log probability for each step
... @param state_values: state-value for each step
... """
... loss = 0
... for log_prob, value, Gt in zip( log_probs, state_values, returns):
... advantage = Gt - value.item()
... policy_loss = -log_prob * advantage
... value_loss = F.smooth_l1_loss(value, Gt)
... loss += policy_loss + value_loss
... self.optimizer.zero_grad()
... loss.backward()
... self.optimizer.step()
PolicyNetwork类的最终方法是get_action,根据预测的策略在给定状态下对动作进行抽样:
>>> def get_action(self, s):
... """
... Estimate the policy and sample an action,
compute its log probability
... @param s: input state
... @return: the selected action and log probability
... """
... action_probs, state_value = self.predict(s)
... action = torch.multinomial(action_probs, 1).item()
... log_prob = torch.log(action_probs[action])
... return action, log_prob, state_value
它还返回所选动作的对数概率,以及估计的状态值。
这就是PolicyNetwork类的全部内容!
- 现在,我们可以继续开发主函数,训练演员-评论家模型:
>>> def actor_critic(env, estimator, n_episode, gamma=1.0):
... """
... Actor Critic algorithm
... @param env: Gym environment
... @param estimator: policy network
... @param n_episode: number of episodes
... @param gamma: the discount factor
... """
... for episode in range(n_episode):
... log_probs = []
... rewards = []
... state_values = []
... state = env.reset()
... while True:
... action, log_prob, state_value =
estimator.get_action(state)
... next_state, reward, is_done, _ = env.step(action)
... total_reward_episode[episode] += reward
... log_probs.append(log_prob)
... state_values.append(state_value)
... rewards.append(reward)
...
... if is_done:
... returns = []
... Gt = 0
... pw = 0
... for reward in rewards[::-1]:
... Gt += gamma ** pw * reward
... pw += 1
... returns.append(Gt)
... returns = returns[::-1]
... returns = torch.tensor(returns)
... returns = (returns - returns.mean()) /
(returns.std() + 1e-9)
... estimator.update( returns, log_probs, state_values)
... print('Episode: {}, total reward: {}'.format( episode, total_reward_episode[episode]))
... if total_reward_episode[episode] >= 195:
... estimator.scheduler.step()
... break
...
... state = next_state
- 我们指定策略网络的大小(输入、隐藏和输出层)、学习率,然后相应地创建一个
PolicyNetwork实例:
>>> n_state = env.observation_space.shape[0]
>>> n_action = env.action_space.n
>>> n_hidden = 128
>>> lr = 0.03
>>> policy_net = PolicyNetwork(n_state, n_action, n_hidden, lr)
我们将折现因子设置为0.9:
>>> gamma = 0.9
- 我们使用刚刚开发的策略网络进行演员-评论家算法的学习,进行了 1,000 个 episode,并跟踪每个 episode 的总奖励:
>>> n_episode = 1000
>>> total_reward_episode = [0] * n_episode
>>> actor_critic(env, policy_net, n_episode, gamma)
- 最后,我们显示随时间变化的 episode 奖励的图表:
>>> import matplotlib.pyplot as plt
>>> plt.plot(total_reward_episode)
>>> plt.title('Episode reward over time')
>>> plt.xlabel('Episode')
>>> plt.ylabel('Total reward')
>>> plt.show()
工作原理是...
正如您在步骤 2中所看到的,演员和评论家共享输入和隐藏层的参数;演员的输出包括采取各个动作的概率,评论家的输出是输入状态的估计值。
在步骤 5中,我们计算优势值及其负对数似然。演员-评论家中的损失函数是优势的负对数似然与回报与估计状态值之间均方误差的组合。请注意,我们使用smooth_l1_loss,当绝对误差低于 1 时,它是一个平方项,否则是一个绝对误差。
在Step 7中,演员-评论者模型的训练函数执行以下任务:
-
它运行一个 episode:对于每个步骤,根据当前估计的策略采样一个动作;它在每个步骤存储奖励、对数策略和估计的状态值。
-
一旦一个 episode 结束,它会计算每一步的折现累积奖励;然后通过减去它们的均值并除以它们的标准差来归一化返回结果。
-
它使用回报、对数概率和状态值更新策略参数。我们还会显示每个 episode 的总奖励。
-
如果一个 episode 的总奖励超过+195,我们会稍微降低学习率。
-
通过重复上述步骤,它运行
n_episode个 episode。
在执行Step 9的训练后,您将看到以下日志:
Episode: 0, total reward: 18.0
Episode: 1, total reward: 9.0
Episode: 2, total reward: 9.0
Episode: 3, total reward: 10.0
Episode: 4, total reward: 10.0
...
...
Episode: 995, total reward: 200.0
Episode: 996, total reward: 200.0
Episode: 997, total reward: 200.0
Episode: 998, total reward: 200.0
Episode: 999, total reward: 200.0
下面的图表展示了Step 10的结果:
您可以看到大约 400 个 episode 后的奖励保持在+200 的最大值。
在优势演员-评论者算法中,我们将学习分解为两个部分 - 演员和评论者。A2C 中的评论者评估在状态下动作的好坏,这指导演员如何反应。再次,优势值被计算为 A(s,a) = Q(s,a) - V(s),这意味着从 Q 值中减去状态值。演员根据评论者的指导估计动作的概率。优势的引入可以减少方差,因此 A2C 被认为比标准演员-评论者模型更稳定。正如我们在 CartPole 环境中看到的,经过数百个 episode 的训练后,A2C 的表现一直很稳定。它优于带基准的 REINFORCE 算法。
使用演员-评论者算法解决 Cliff Walking
在这个示例中,我们将使用 A2C 算法解决一个更复杂的 Cliff Walking 环境问题。
Cliff Walking 是一个典型的 Gym 环境,episode 很长且没有终止的保证。这是一个 4 * 12 的网格问题。代理在每一步可以向上、向右、向下和向左移动。左下角的瓦片是代理的起点,右下角是获胜点,如果到达则结束 episode。最后一行的其余瓦片是悬崖,代理在踩到它们后将被重置到起始位置,但 episode 继续。代理每走一步会产生-1 的奖励,但踩到悬崖时会产生-100 的奖励。
状态是一个从 0 到 47 的整数,表示代理的位置,如图所示:
这样的值并不包含数值意义。例如,处于状态 30 并不意味着它比处于状态 10 多 3 倍。因此,在将状态输入策略网络之前,我们将首先将其转换为一个 one-hot 编码向量。
如何做到……
我们使用 A2C 算法解决 Cliff Walking 如下:
- 导入所有必要的包,并创建一个 CartPole 实例:
>>> import gym
>>> import torch
>>> import torch.nn as nn
>>> import torch.nn.functional as F >>> env = gym.make('CliffWalking-v0')
- 由于状态变为 48 维,我们使用了一个更复杂的具有两个隐藏层的 actor-critic 神经网络:
>>> class ActorCriticModel(nn.Module):
... def __init__(self, n_input, n_output, n_hidden):
... super(ActorCriticModel, self).__init__()
... self.fc1 = nn.Linear(n_input, n_hidden[0])
... self.fc2 = nn.Linear(n_hidden[0], n_hidden[1])
... self.action = nn.Linear(n_hidden[1], n_output)
... self.value = nn.Linear(n_hidden[1], 1)
...
... def forward(self, x):
... x = torch.Tensor(x)
... x = F.relu(self.fc1(x))
... x = F.relu(self.fc2(x))
... action_probs = F.softmax(self.action(x), dim=-1)
... state_values = self.value(x)
... return action_probs, state_values
再次强调,actor 和 critic 共享输入和隐藏层的参数。
-
我们继续使用刚刚在Step 2中开发的 actor-critic 神经网络来使用
PolicyNetwork类。它与Implementing the actor-critic algorithm案例中的PolicyNetwork类相同。 -
接下来,我们开发主函数,训练一个 actor-critic 模型。它几乎与Implementing the actor-critic algorithm案例中的模型相同,只是额外将状态转换为 one-hot 编码向量:
>>> def actor_critic(env, estimator, n_episode, gamma=1.0):
... """
... Actor Critic algorithm
... @param env: Gym environment
... @param estimator: policy network
... @param n_episode: number of episodes
... @param gamma: the discount factor
... """
... for episode in range(n_episode):
... log_probs = []
... rewards = []
... state_values = []
... state = env.reset()
... while True:
... one_hot_state = [0] * 48
... one_hot_state[state] = 1
... action, log_prob, state_value =
estimator.get_action(one_hot_state)
... next_state, reward, is_done, _ = env.step(action)
... total_reward_episode[episode] += reward
... log_probs.append(log_prob)
... state_values.append(state_value)
... rewards.append(reward)
...
... if is_done:
... returns = []
... Gt = 0
... pw = 0
... for reward in rewards[::-1]:
... Gt += gamma ** pw * reward
... pw += 1
... returns.append(Gt)
... returns = returns[::-1]
... returns = torch.tensor(returns)
... returns = (returns - returns.mean()) /
(returns.std() + 1e-9)
... estimator.update( returns, log_probs, state_values)
... print('Episode: {}, total reward: {}'.format( episode, total_reward_episode[episode]))
... if total_reward_episode[episode] >= -14:
... estimator.scheduler.step()
... break
...
... state = next_state
- 我们指定策略网络的大小(输入、隐藏和输出层)、学习率,然后相应地创建一个
PolicyNetwork实例:
>>> n_state = 48
>>> n_action = env.action_space.n
>>> n_hidden = [128, 32]
>>> lr = 0.03
>>> policy_net = PolicyNetwork(n_state, n_action, n_hidden, lr)
我们将折扣因子设为0.9:
>>> gamma = 0.9
- 我们使用刚刚开发的策略网络进行 1000 个 episode 的 actor-critic 算法学习,并跟踪每个 episode 的总奖励:
>>> n_episode = 1000
>>> total_reward_episode = [0] * n_episode
>>> actor_critic(env, policy_net, n_episode, gamma)
- 现在,我们展示自第 100 个 episode 开始训练后的奖励变化曲线图:
>>> import matplotlib.pyplot as plt
>>> plt.plot(range(100, n_episode), total_reward_episode[100:])
>>> plt.title('Episode reward over time')
>>> plt.xlabel('Episode')
>>> plt.ylabel('Total reward')
>>> plt.show()
工作原理...
您可能会注意到在Step 4中,如果一个 episode 的总奖励超过-14,我们会略微降低学习率。-13 的奖励是我们能够通过路径 36-24-25-26-27-28-29-30-31-32-33-34-35-47 获得的最大值。
执行Step 6训练后,您将看到以下日志:
Episode: 0, total reward: -85355
Episode: 1, total reward: -3103
Episode: 2, total reward: -1002
Episode: 3, total reward: -240
Episode: 4, total reward: -118
...
...
Episode: 995, total reward: -13
Episode: 996, total reward: -13
Episode: 997, total reward: -13
Episode: 998, total reward: -13
Episode: 999, total reward: -13
下图显示了Step 7的结果:
正如我们可以观察到的那样,在大约第 180 个 episode 之后,大多数 episode 的奖励达到了最优值-13。
在这个案例中,我们使用 A2C 算法解决了 Cliff Walking 问题。整数状态从 0 到 47 表示 4*12 棋盘中代理的位置,这些位置并没有数值意义,因此我们首先将其转换为 48 维的 one-hot 编码向量。为了处理 48 维输入,我们使用了一个稍微复杂的具有两个隐藏层的神经网络。在我们的实验中,A2C 已被证明是一个稳定的策略方法。
设置连续的 Mountain Car 环境
到目前为止,我们所处理的环境都具有离散的动作值,比如 0 或 1,代表上下左右。在这个案例中,我们将体验一个具有连续动作的 Mountain Car 环境。
连续的 Mountain Car(github.com/openai/gym/wiki/MountainCarContinuous-v0)是一个具有连续动作的 Mountain Car 环境,其值从-1 到 1。如下截图所示,其目标是将汽车驶到右侧山顶上:
在一维赛道上,汽车位于-1.2(最左侧)到 0.6(最右侧)之间,并且目标(黄旗)位于 0.5 处。汽车的引擎不足以在一次通过中将其推上山顶,因此必须来回驾驶以积累动量。因此,动作是一个浮点数,表示如果其值在-1 到 0 之间则将汽车向左推,如果在 0 到 1 之间则将汽车向右推。
环境有两个状态:
-
汽车的位置:这是一个从-1.2 到 0.6 的连续变量
-
汽车的速度:这是一个从-0.07 到 0.07 的连续变量
初始状态包括位置在-0.6 到-0.4 之间,速度为 0。
每一步的奖励与动作 a 相关,为*-a²*。并且达到目标还会有额外的+100 奖励。因此,它惩罚了每一步中所采取的力量,直到汽车到达目标位置。一个 episode 在汽车到达目标位置(显然是),或者经过 1000 步后结束。
如何操作...
让我们通过以下步骤来模拟连续的山车环境:
- 我们导入 Gym 库并创建一个连续山车环境的实例:
>>> import gym
>>> import torch
>>> env = gym.envs.make("MountainCarContinuous-v0")
- 看一下动作空间:
>>> print(env.action_space.low[0])
-1.0
>>> print(env.action_space.high[0])
1.0
- 然后我们重置环境:
>>> env.reset()
array([-0.56756635, 0\. ])
汽车的初始状态为[-0.56756635, 0. ],这意味着初始位置大约为-0.56,速度为 0. 由于初始位置是从-0.6 到-0.4 随机生成的,所以可能看到不同的初始位置。
- 现在让我们采取一个简单的方法:我们只是随机选择一个动作从-1 到 1:
>>> is_done = False
>>> while not is_done:
... random_action = torch.rand(1) * 2 - 1
... next_state, reward, is_done, info = env.step(random_action)
... print(next_state, reward, is_done)
... env.render()
>>> env.render()
[-0.5657432 0.00182313] -0.09924464356736849 False
[-0.5622848 0.00345837] -0.07744002014160288 False
[-0.55754507 0.00473979] -0.04372991690837722 False
......
......
状态(位置和速度)会相应地发生变化,每一步的奖励为*-a²*。
您还会在视频中看到汽车反复向右移动和向左移动。
工作原理是这样的...
正如你所想象的那样,连续的山车问题是一个具有挑战性的环境,甚至比仅有三种不同可能动作的原始离散问题更加困难。我们需要来回驾驶汽车以积累正确的力量和方向。此外,动作空间是连续的,这意味着值查找/更新方法(如 TD 方法、DQN)将不起作用。在下一个示例中,我们将使用 A2C 算法的连续控制版本来解决连续的山车问题。
使用优势演员-评论者网络解决连续山车环境
在这个示例中,我们将使用优势演员-评论者算法来解决连续的山车问题,这当然是一个连续版本。你会看到它与离散版本有何不同。
正如我们在具有离散动作的环境中看到的那样,在连续控制中,由于我们无法对无数个连续动作进行采样,我们如何建模?实际上,我们可以借助高斯分布。我们可以假设动作值服从高斯分布:
在这里,均值,![],以及偏差,,是从策略网络中计算出来的。通过这种调整,我们可以通过当前均值和偏差构建的高斯分布采样动作。连续 A2C 中的损失函数类似于我们在离散控制中使用的损失函数,它是在高斯分布下动作概率的负对数似然和优势值之间的组合,以及实际回报值与预估状态值之间的回归误差。
注意,一个高斯分布用于模拟一个维度的动作,因此,如果动作空间是 k 维的,我们需要使用 k 个高斯分布。在连续 Mountain Car 环境中,动作空间是一维的。就连续控制而言,A2C 的主要困难在于如何构建策略网络,因为它计算了高斯分布的参数。
如何做...
我们使用连续 A2C 来解决连续 Mountain Car 问题,具体如下:
- 导入所有必要的包并创建一个连续 Mountain Car 实例:
>>> import gym
>>> import torch
>>> import torch.nn as nn
>>> import torch.nn.functional as F >>> env = gym.make('MountainCarContinuous-v0')
- 让我们从演员-评论神经网络模型开始:
>>> class ActorCriticModel(nn.Module):
... def __init__(self, n_input, n_output, n_hidden):
... super(ActorCriticModel, self).__init__()
... self.fc = nn.Linear(n_input, n_hidden)
... self.mu = nn.Linear(n_hidden, n_output)
... self.sigma = nn.Linear(n_hidden, n_output)
... self.value = nn.Linear(n_hidden, 1)
... self.distribution = torch.distributions.Normal
...
... def forward(self, x):
... x = F.relu(self.fc(x))
... mu = 2 * torch.tanh(self.mu(x))
... sigma = F.softplus(self.sigma(x)) + 1e-5
... dist = self.distribution( mu.view(1, ).data, sigma.view(1, ).data)
... value = self.value(x)
... return dist, value
- 我们继续使用刚刚开发的演员-评论神经网络的
PolicyNetwork类中的__init__方法:
>>> class PolicyNetwork():
... def __init__(self, n_state, n_action,
n_hidden=50, lr=0.001):
... self.model = ActorCriticModel( n_state, n_action, n_hidden)
... self.optimizer = torch.optim.Adam( self.model.parameters(), lr)
- 接下来,我们添加
predict方法,用于计算预估的动作概率和状态值:
>>> def predict(self, s):
... """
... Compute the output using the continuous Actor Critic model
... @param s: input state
... @return: Gaussian distribution, state_value
... """
... self.model.training = False
... return self.model(torch.Tensor(s))
-
我们现在开发训练方法,该方法使用一个 episode 中收集的样本来更新策略网络。我们将重用实施演员-评论算法食谱中开发的更新方法,这里不再重复。
-
PolicyNetwork类的最终方法是get_action,它从给定状态的预估高斯分布中采样一个动作:
>>> def get_action(self, s):
... """
... Estimate the policy and sample an action,
compute its log probability
... @param s: input state
... @return: the selected action, log probability,
predicted state-value
... """
... dist, state_value = self.predict(s)
... action = dist.sample().numpy()
... log_prob = dist.log_prob(action[0])
... return action, log_prob, state_value
它还返回所选动作的对数概率和预估状态值。
这就是用于连续控制的PolicyNetwork类的全部内容!
现在,我们可以继续开发主函数,训练一个演员-评论模型:
>>> def actor_critic(env, estimator, n_episode, gamma=1.0):
... """
... continuous Actor Critic algorithm
... @param env: Gym environment
... @param estimator: policy network
... @param n_episode: number of episodes
... @param gamma: the discount factor
... """
... for episode in range(n_episode):
... log_probs = []
... rewards = []
... state_values = []
... state = env.reset()
... while True:
... state = scale_state(state)
... action, log_prob, state_value =
estimator.get_action(state)
... action = action.clip(env.action_space.low[0],
... env.action_space.high[0])
... next_state, reward, is_done, _ = env.step(action)
... total_reward_episode[episode] += reward
... log_probs.append(log_prob)
... state_values.append(state_value)
... rewards.append(reward)
... if is_done:
... returns = []
... Gt = 0
... pw = 0
... for reward in rewards[::-1]:
... Gt += gamma ** pw * reward
... pw += 1
... returns.append(Gt)
... returns = returns[::-1]
... returns = torch.tensor(returns)
... returns = (returns - returns.mean()) /
(returns.std() + 1e-9)
... estimator.update( returns, log_probs, state_values)
... print('Episode: {}, total reward: {}'.format( episode, total_reward_episode[episode]))
... break
... state = next_state
scale_state函数用于对输入进行标准化(规范化),以加快模型的收敛速度。我们首先随机生成 10,000 个观测数据,并用它们来训练一个缩放器:
>>> import sklearn.preprocessing
>>> import numpy as np
>>> state_space_samples = np.array(
... [env.observation_space.sample() for x in range(10000)])
>>> scaler = sklearn.preprocessing.StandardScaler()
>>> scaler.fit(state_space_samples)
一旦缩放器被训练好,我们就在scale_state函数中使用它来转换新的输入数据:
>>> def scale_state(state):
... scaled = scaler.transform([state])
... return scaled[0]
- 我们指定策略网络的大小(输入、隐藏和输出层),学习率,然后相应地创建一个
PolicyNetwork实例:
>>> n_state = env.observation_space.shape[0]
>>> n_action = 1
>>> n_hidden = 128
>>> lr = 0.0003
>>> policy_net = PolicyNetwork(n_state, n_action, n_hidden, lr)
我们将折现因子设为0.9:
>>> gamma = 0.9
- 我们使用刚刚开发的策略网络进行 200 个剧集的 actor-critic 算法进行连续控制,并且我们还跟踪每个剧集的总奖励:
>>> n_episode = 200
>>> total_reward_episode = [0] * n_episode
>>> actor_critic(env, policy_net, n_episode, gamma)
- 现在,让我们展示随时间变化的剧集奖励图:
>>> import matplotlib.pyplot as plt
>>> plt.plot(total_reward_episode)
>>> plt.title('Episode reward over time')
>>> plt.xlabel('Episode')
>>> plt.ylabel('Total reward')
>>> plt.show()
如何工作...
在这个配方中,我们使用高斯 A2C 来解决连续的 Mountain Car 环境。
在第 2 步中,我们示例中的网络有一个隐藏层。输出层有三个单独的组件。它们是高斯分布的均值和偏差,以及状态值。分布均值的输出通过 tanh 激活函数缩放到[-1, 1]范围(或此示例中的[-2, 2]),而分布偏差使用 softplus 作为激活函数以确保正偏差。网络返回当前的高斯分布(actor)和估计的状态值(critic)。
在第 7 步的 actor-critic 模型训练函数与我们在实施 actor-critic 算法配方中开发的内容非常相似。您可能会注意到我们在采样动作时添加了一个值剪辑,以使其保持在[-1, 1]范围内。我们将在接下来的步骤中解释scale_state函数的作用。
在执行第 10 步的训练后,您将看到以下日志:
Episode: 0, total reward: 89.46417524456328
Episode: 1, total reward: 89.54226159679301
Episode: 2, total reward: 89.91828341346695
Episode: 3, total reward: 90.04199470314816
Episode: 4, total reward: 86.23157467747066
...
...
Episode: 194, total reward: 92.71676277432059
Episode: 195, total reward: 89.97484988523927
Episode: 196, total reward: 89.26063135086025
Episode: 197, total reward: 87.19460382302674
Episode: 198, total reward: 79.86081433777699
Episode: 199, total reward: 88.98075638481279
以下图表是第 11 步的结果:
根据github.com/openai/gym/wiki/MountainCarContinuous-v0中解决的要求,获得超过+90 的奖励被视为环境已解决。我们有多个剧集解决了环境问题。
在连续的 A2C 中,我们假设动作空间的每个维度都服从高斯分布。高斯分布的均值和偏差是策略网络输出层的一部分。输出层的其余部分用于估计状态值。从当前均值和偏差参数化的高斯分布中采样一个或多个动作。连续 A2C 的损失函数类似于其离散版本,即负对数似然与高斯分布下动作概率以及优势值之间的组合,以及实际回报值与估计状态值之间的回归误差。
还有更多内容...
到目前为止,我们一直是以随机的方式建模策略,从分布或计算的概率中采样动作。作为一个额外部分,我们将简要讨论确定性策略梯度(DPG),在这里我们将策略建模为确定性决策。我们简单地将确定性策略视为随机策略的特例,直接将输入状态映射到动作而不是动作的概率。DPG 算法通常使用以下两组神经网络:
-
Actor-critic 网络:这与我们之前体验过的 A2C 非常相似,但是是以确定性方式进行。它预测状态值和需要执行的动作。
-
目标 actor-critic 网络:这是 actor-critic 网络的定期副本,其目的是稳定学习。显然,你不希望目标一直在变化。该网络为训练提供了延迟的目标。
正如你所看到的,在 DPG 中并没有太多新东西,但它是 A2C 和延迟目标机制的良好结合。请随意自行实现该算法,并用它来解决连续的 Mountain Car 环境。
另请参阅
如果你对 softplus 激活函数不熟悉,或者想要了解更多关于 DPG 的内容,请查看以下材料:
通过交叉熵方法玩 CartPole
在这个最后的示例中,作为一个额外的(也很有趣的)部分,我们将开发一个简单而强大的算法来解决 CartPole 问题。它基于交叉熵,直接将输入状态映射到输出动作。事实上,它比本章中所有其他策略梯度算法更为直接。
我们已经应用了几种策略梯度算法来解决 CartPole 环境。它们使用复杂的神经网络架构和损失函数,这对于如 CartPole 这样的简单环境可能有些过度。为什么不直接预测给定状态下的动作呢?其背后的思想很简单:我们对过去最成功的经验进行建模,仅对正确的动作感兴趣。在这种情况下,目标函数是实际动作和预测动作之间的交叉熵。在 CartPole 中,有两种可能的动作:左和右。为了简单起见,我们可以将其转换为二元分类问题,并使用以下模型图表述:
如何实现它...
我们使用交叉熵来解决 CartPole 问题如下:
- 导入所有必要的包并创建一个 CartPole 实例:
>>> import gym
>>> import torch
>>> import torch.nn as nn
>>> from torch.autograd import Variable >>> env = gym.make('CartPole-v0')
- 让我们从动作估算器开始:
>>> class Estimator():
... def __init__(self, n_state, lr=0.001):
... self.model = nn.Sequential(
... nn.Linear(n_state, 1),
... nn.Sigmoid()
... )
... self.criterion = torch.nn.BCELoss()
... self.optimizer = torch.optim.Adam( self.model.parameters(), lr)
...
... def predict(self, s):
... return self.model(torch.Tensor(s))
...
... def update(self, s, y):
... """
... Update the weights of the estimator given
the training samples
... """
... y_pred = self.predict(s)
... loss = self.criterion( y_pred, Variable(torch.Tensor(y)))
... self.optimizer.zero_grad()
... loss.backward()
... self.optimizer.step()
- 现在我们为交叉熵算法开发主要的训练函数:
>>> def cross_entropy(env, estimator, n_episode, n_samples):
... """
... Cross-entropy algorithm for policy learning
... @param env: Gym environment
... @param estimator: binary estimator
... @param n_episode: number of episodes
... @param n_samples: number of training samples to use
... """
... experience = []
... for episode in range(n_episode):
... rewards = 0
... actions = []
... states = []
... state = env.reset()
... while True:
... action = env.action_space.sample()
... states.append(state)
... actions.append(action)
... next_state, reward, is_done, _ = env.step(action)
... rewards += reward
... if is_done:
... for state, action in zip(states, actions):
... experience.append((rewards, state, action))
... break
... state = next_state
...
... experience = sorted(experience,
key=lambda x: x[0], reverse=True)
... select_experience = experience[:n_samples]
... train_states = [exp[1] for exp in select_experience]
... train_actions = [exp[2] for exp in select_experience]
...
... for _ in range(100):
... estimator.update(train_states, train_actions)
- 然后我们指定动作估算器的输入大小和学习率:
>>> n_state = env.observation_space.shape[0]
>>> lr = 0.01
然后我们相应地创建一个 Estimator 实例:
>>> estimator = Estimator(n_state, lr)
- 我们将生成 5,000 个随机的情节,并精选出最佳的 10,000 个(状态,动作)对用于估算器的训练:
>>> n_episode = 5000
>>> n_samples = 10000
>>> cross_entropy(env, estimator, n_episode, n_samples)
- 模型训练完成后,让我们来测试一下。我们将用它来玩 100 个情节,并记录总奖励:
>>> n_episode = 100
>>> total_reward_episode = [0] * n_episode
>>> for episode in range(n_episode):
... state = env.reset()
... is_done = False
... while not is_done:
... action = 1 if estimator.predict(state).item() >= 0.5 else 0
... next_state, reward, is_done, _ = env.step(action)
... total_reward_episode[episode] += reward
... state = next_state
- 然后我们将性能可视化如下:
>>> import matplotlib.pyplot as plt
>>> plt.plot(total_reward_episode)
>>> plt.title('Episode reward over time')
>>> plt.xlabel('Episode')
>>> plt.ylabel('Total reward')
>>> plt.show()
工作原理...
正如您在Step 2中所看到的,动作估计器有两层 - 输入层和输出层,接着是一个 sigmoid 激活函数,损失函数是二元交叉熵。
Step 3是为了训练交叉熵模型。具体而言,对于每个训练集,我们采取随机行动,累积奖励,并记录状态和行动。在体验了n_episode个集数后,我们提取最成功的集数(具有最高总奖励)并提取n_samples个(状态,行动)对作为训练样本。然后我们在刚构建的训练集上对估计器进行 100 次迭代的训练。
执行Step 7中的代码行将产生以下绘图:
正如您所见,所有测试集都有+200 的奖励!
交叉熵对于简单环境非常简单,但却很有用。它直接建模输入状态和输出行动之间的关系。一个控制问题被构建成一个分类问题,我们试图在所有备选行动中预测正确的行动。关键在于我们只从正确的经验中学习,这指导模型在给定状态时应该选择哪个最有益的行动。
第九章:毕业项目 – 使用 DQN 玩 Flappy Bird
在这最后一章中,我们将致力于一个毕业项目——使用强化学习玩 Flappy Bird。我们将应用我们在本书中学到的知识来构建一个智能机器人。我们还将专注于构建深度 Q 网络(DQNs),微调模型参数并部署模型。让我们看看鸟能在空中停留多久。
最后一个章节将通过以下步骤逐步构建毕业项目:
-
设置游戏环境
-
构建一个深度 Q 网络来玩 Flappy Bird
-
训练和调整网络
-
部署模型并玩游戏
因此,每个食谱中的代码都将基于前面的食谱构建。
设置游戏环境
要使用 DQN 玩 Flappy Bird,我们首先需要设置环境。
我们将使用 Pygame 模拟 Flappy Bird 游戏。Pygame (www.pygame.org) 包含一组为创建视频游戏而开发的 Python 模块。它还包括在游戏中需要的图形和声音库。我们可以按照以下方式安装 Pygame 包:
pip install pygame
Flappy Bird 是由 Dong Nguyen 最初开发的一款著名移动游戏。你可以在 flappybird.io/ 使用键盘自己尝试。游戏的目标是尽可能长时间地保持存活。当鸟触碰到地面或管道时游戏结束。因此,鸟需要在正确的时机振翅通过随机的管道,避免落到地面上。可能的动作包括振翅和不振翅。在游戏环境中,每一步的奖励是 +0.1,并有以下两个例外情况:
-
当发生碰撞时为 -1
-
当鸟通过两个管道之间的间隙时为 +1。原始的 Flappy Bird 游戏根据通过的间隙数量进行评分。
准备工作
从 github.com/yanpanlau/Keras-FlappyBird/tree/master/assets/sprites 下载我们需要的游戏环境资产。为简单起见,我们将只使用 sprites 文件夹中的图像。具体来说,我们需要以下图像:
-
background-black.png: 屏幕的背景图像 -
base.png: 地板的图像 -
pipe-green.png: 鸟需要避开的管道的图像 -
redbird-downflap.png: 鸟向下振翅时的图像 -
redbird-midflap.png: 鸟静止时的图像 -
redbird-upflap.png: 鸟向上振翅时的图像
如果您感兴趣,还可以使用音频文件使游戏更有趣。
如何做…
我们将使用 Pygame 开发 Flappy Bird 游戏环境,步骤如下:
- 我们首先开发一个实用函数,加载图像并将其转换为正确的格式:
>>> from pygame.image import load
>>> from pygame.surfarray import pixels_alpha
>>> from pygame.transform import rotate
>>> def load_images(sprites_path):
... base_image = load(sprites_path +
'base.png').convert_alpha()
... background_image = load(sprites_path +
'background-black.png').convert()
... pipe_images = [rotate(load(sprites_path +
'pipe-green.png').convert_alpha(), 180),
... load(sprites_path +
'pipe-green.png').convert_alpha()]
... bird_images = [load(sprites_path +
'redbird-upflap.png').convert_alpha(),
... load(sprites_path +
'redbird-midflap.png').convert_alpha(),
... load(sprites_path +
'redbird-downflap.png').convert_alpha()]
... bird_hitmask = [pixels_alpha(image).astype(bool)
for image in bird_images]
... pipe_hitmask = [pixels_alpha(image).astype(bool)
for image in pipe_images]
... return base_image, background_image, pipe_images,
bird_images, bird_hitmask, pipe_hitmask
- 导入环境所需的所有包:
>>> from itertools import cycle
>>> from random import randint
>>> import pygame
- 初始化游戏和时钟,并将屏幕刷新频率设置为每秒 30 帧:
>>> pygame.init()
>>> fps_clock = pygame.time.Clock() >>> fps = 30
- 指定屏幕大小并相应地创建屏幕,然后为屏幕添加标题:
>>> screen_width = 288
>>> screen_height = 512
>>> screen = pygame.display.set_mode((screen_width, screen_height)) >>> pygame.display.set_caption('Flappy Bird')
- 然后,使用以下函数加载必要的图像(位于
sprites文件夹中):
>>> base_image, background_image, pipe_images, bird_images, bird_hitmask, pipe_hitmask = load_images('sprites/')
- 获取游戏变量,包括鸟和管道的大小,并设置两个管道之间的垂直间隙为 100:
>>> bird_width = bird_images[0].get_width()
>>> bird_height = bird_images[0].get_height()
>>> pipe_width = pipe_images[0].get_width()
>>> pipe_height = pipe_images[0].get_height() >>> pipe_gap_size = 100
- 鸟的振动运动依次为向上、中间、向下、中间、向上等:
>>> bird_index_gen = cycle([0, 1, 2, 1])
这仅仅是为了使游戏更加有趣。
- 在定义完所有常量后,我们从游戏环境的
FlappyBird类的__init__method开始:
>>> class FlappyBird(object):
... def __init__(self):
... self.pipe_vel_x = -4
... self.min_velocity_y = -8
... self.max_velocity_y = 10
... self.downward_speed = 1
... self.upward_speed = -9
... self.cur_velocity_y = 0
... self.iter = self.bird_index = self.score = 0
... self.bird_x = int(screen_width / 5)
... self.bird_y = int((screen_height - bird_height) / 2)
... self.base_x = 0
... self.base_y = screen_height * 0.79
... self.base_shift = base_image.get_width() -
background_image.get_width()
... self.pipes = [self.gen_random_pipe(screen_width),
self.gen_random_pipe(screen_width * 1.5)]
... self.is_flapped = False
- 我们继续定义
gen_random_pipe方法,该方法在给定的水平位置和随机垂直位置生成一对管道(一个上管道和一个下管道):
>>> def gen_random_pipe(self, x):
... gap_y = randint(2, 10) * 10 + int(self.base_y * 0.2)
... return {"x_upper": x,
... "y_upper": gap_y - pipe_height,
... "x_lower": x,
... "y_lower": gap_y + pipe_gap_size}
上下两个管道的y位置分别为gap_y - pipe_height和gap_y + pipe_gap_size。
- 我们接下来开发的方法是
check_collision,如果鸟与基座或管道碰撞,则返回True:
>>> def check_collision(self):
... if bird_height + self.bird_y >= self.base_y - 1:
... return True
... bird_rect = pygame.Rect(self.bird_x, self.bird_y,
bird_width, bird_height)
... for pipe in self.pipes:
... pipe_boxes = [pygame.Rect(pipe["x_upper"],
pipe["y_upper"], pipe_width, pipe_height),
... pygame.Rect(pipe["x_lower"],
pipe["y_lower"], pipe_width, pipe_height)]
... # Check if the bird's bounding box overlaps to
the bounding box of any pipe
... if bird_rect.collidelist(pipe_boxes) == -1:
... return False
... for i in range(2):
... cropped_bbox = bird_rect.clip(pipe_boxes[i])
... x1 = cropped_bbox.x - bird_rect.x
... y1 = cropped_bbox.y - bird_rect.y
... x2 = cropped_bbox.x - pipe_boxes[i].x
... y2 = cropped_bbox.y - pipe_boxes[i].y
... for x in range(cropped_bbox.width):
... for y in range(cropped_bbox.height):
... if bird_hitmask[self.bird_index][x1+x,
y1+y] and pipe_hitmask[i][
x2+x, y2+y]:
... return True
... return False
- 我们最后需要的最重要的方法是
next_step,它执行一个动作并返回游戏的更新图像帧、收到的奖励以及本轮游戏是否结束:
>>> def next_step(self, action):
... pygame.event.pump()
... reward = 0.1
... if action == 1:
... self.cur_velocity_y = self.upward_speed
... self.is_flapped = True
... # Update score
... bird_center_x = self.bird_x + bird_width / 2
... for pipe in self.pipes:
... pipe_center_x = pipe["x_upper"] +
pipe_width / 2
... if pipe_center_x < bird_center_x
< pipe_center_x + 5:
... self.score += 1
... reward = 1
... break
... # Update index and iteration
... if (self.iter + 1) % 3 == 0:
... self.bird_index = next(bird_index_gen)
... self.iter = (self.iter + 1) % fps
... self.base_x = -((-self.base_x + 100) %
self.base_shift)
... # Update bird's position
... if self.cur_velocity_y < self.max_velocity_y
and not self.is_flapped:
... self.cur_velocity_y += self.downward_speed
... self.is_flapped = False
... self.bird_y += min(self.cur_velocity_y,
self.bird_y - self.cur_velocity_y - bird_height)
... if self.bird_y < 0:
... self.bird_y = 0
... # Update pipe position
... for pipe in self.pipes:
... pipe["x_upper"] += self.pipe_vel_x
... pipe["x_lower"] += self.pipe_vel_x
... # Add new pipe when first pipe is
about to touch left of screen
... if 0 < self.pipes[0]["x_lower"] < 5:
... self.pipes.append(self.gen_random_pipe( screen_width + 10))
... # remove first pipe if its out of the screen
... if self.pipes[0]["x_lower"] < -pipe_width:
... self.pipes.pop(0)
... if self.check_collision():
... is_done = True
... reward = -1
... self.__init__()
... else:
... is_done = False
... # Draw sprites
... screen.blit(background_image, (0, 0))
... screen.blit(base_image, (self.base_x, self.base_y))
... screen.blit(bird_images[self.bird_index],
(self.bird_x, self.bird_y))
... for pipe in self.pipes:
... screen.blit(pipe_images[0], (pipe["x_upper"], pipe["y_upper"]))
... screen.blit(pipe_images[1],
(pipe["x_lower"], pipe["y_lower"]))
... image = pygame.surfarray.array3d( pygame.display.get_surface())
... pygame.display.update()
... fps_clock.tick(fps)
... return image, reward, is_done
至此,关于Flappy Bird环境的介绍就完成了。
它的工作原理...
在第 8 步中,我们定义了管道的速度(每过 4 个单位向左移动一次)、鸟的最小和最大垂直速度(分别为-8和10)、其向上和向下加速度(分别为-9和1)、其默认垂直速度(0)、鸟图像的起始索引(0)、初始得分、鸟的初始水平和垂直位置、基座的位置,以及使用gen_random_pipe方法随机生成的管道的坐标。
在第 11 步中,默认情况下,每个步骤的奖励为+0.1。如果动作是振翅,我们会增加鸟的垂直速度及其向上加速度。然后,我们检查鸟是否成功通过了一对管道。如果是,则游戏得分增加 1,步骤奖励变为+1。我们更新鸟的位置、其图像索引以及管道的位置。如果旧的一对管道即将离开屏幕左侧,将生成新的一对管道,并在旧的一对管道离开屏幕后删除它。如果发生碰撞,本轮游戏将结束,奖励为-1;游戏也将重置。最后,我们会在游戏屏幕上显示更新的帧。
构建一个 Deep Q-Network 来玩 Flappy Bird
现在Flappy Bird环境已经准备就绪,我们可以开始通过构建 DQN 模型来解决它。
正如我们所见,每次采取行动后都会返回一个屏幕图像。CNN 是处理图像输入的最佳神经网络架构之一。在 CNN 中,卷积层能够有效地从图像中提取特征,这些特征将传递到下游的全连接层。在我们的解决方案中,我们将使用具有三个卷积层和一个全连接隐藏层的 CNN。CNN 架构示例如下:
如何做到...
让我们开发一个基于 CNN 的 DQN 模型,步骤如下:
- 导入必要的模块:
>>> import torch
>>> import torch.nn as nn
>>> import torch.nn.functional as F
>>> import numpy as np
>>> import random
- 我们从 CNN 模型开始:
>>> class DQNModel(nn.Module):
... def __init__(self, n_action=2):
... super(DQNModel, self).__init__()
... self.conv1 = nn.Conv2d(4, 32,
kernel_size=8, stride=4)
... self.conv2 = nn.Conv2d(32, 64, 4, stride=2)
... self.conv3 = nn.Conv2d(64, 64, 3, stride=1)
... self.fc = nn.Linear(7 * 7 * 64, 512)
... self.out = nn.Linear(512, n_action)
... self._create_weights()
...
... def _create_weights(self):
... for m in self.modules():
... if isinstance(m, nn.Conv2d) or
isinstance(m, nn.Linear):
... nn.init.uniform(m.weight, -0.01, 0.01)
... nn.init.constant_(m.bias, 0)
...
... def forward(self, x):
... x = F.relu(self.conv1(x))
... x = F.relu(self.conv2(x))
... x = F.relu(self.conv3(x))
... x = x.view(x.size(0), -1)
... x = F.relu(self.fc(x))
... output = self.out(x)
... return output
- 现在使用我们刚刚构建的 CNN 模型开发一个带有经验回放的 DQN:
>>> class DQN():
... def __init__(self, n_action, lr=1e-6):
... self.criterion = torch.nn.MSELoss()
... self.model = DQNModel(n_action)
... self.optimizer = torch.optim.Adam( self.model.parameters(), lr)
predict方法根据输入状态估计输出 Q 值:
>>> def predict(self, s):
... """
... Compute the Q values of the state for all
actions using the learning model
... @param s: input state
... @return: Q values of the state for all actions
... """
... return self.model(torch.Tensor(s))
update方法根据训练样本更新神经网络的权重,并返回当前损失:
>>> def update(self, y_predict, y_target):
... """
... Update the weights of the DQN given a training sample
... @param y_predict:
... @param y_target:
... @return:
... """
... loss = self.criterion(y_predict, y_target)
... self.optimizer.zero_grad()
... loss.backward()
... self.optimizer.step()
... return loss
DQN类的最后部分是replay方法,它在给定一系列过去经验时执行经验重播:
>>> def replay(self, memory, replay_size, gamma):
... """
... Experience replay
... @param memory: a list of experience
... @param replay_size: the number of samples we
use to update the model each time
... @param gamma: the discount factor
... @return: the loss
... """
... if len(memory) >= replay_size:
... replay_data = random.sample(memory, replay_size)
... state_batch, action_batch, next_state_batch,
reward_batch, done_batch = zip(*replay_data)
... state_batch = torch.cat( tuple(state for state in state_batch))
... next_state_batch = torch.cat(
tuple(state for state in next_state_batch))
... q_values_batch = self.predict(state_batch)
... q_values_next_batch =
self.predict(next_state_batch)
... reward_batch = torch.from_numpy(np.array( reward_batch, dtype=np.float32)[:, None])
... action_batch = torch.from_numpy(
... np.array([[1, 0] if action == 0 else [0, 1]
for action in action_batch], dtype=np.float32))
... q_value = torch.sum( q_values_batch * action_batch, dim=1)
... td_targets = torch.cat(
... tuple(reward if terminal else reward +
gamma * torch.max(prediction) for
reward, terminal, prediction
... in zip(reward_batch, done_batch,
q_values_next_batch)))
... loss = self.update(q_value, td_targets)
... return loss
这就是 DQN 类的全部内容。在下一个示例中,我们将对 DQN 模型进行若干次迭代的训练。
工作原理...
在步骤 2中,我们组装了基于 CNN 的 DQN 的骨干部分。它有三个具有不同配置的卷积层。每个卷积层后面跟着一个 ReLU 激活函数。然后将最后一个卷积层的特征图展平,并输入到一个具有 512 个节点的全连接隐藏层,然后是输出层。
注意,我们还设置了权重的初始随机值界限和零偏置,以便模型更容易收敛。
步骤 6是使用经验回放进行逐步训练。如果我们有足够的经验,我们会随机选择一个大小为replay_size的经验集合进行训练。然后,我们将每个经验转换为一个训练样本,该样本由给定输入状态的预测值和输出目标值组成。目标值计算如下:
-
使用奖励和新的 Q 值更新动作的目标 Q 值,如下所示:![]
-
如果是终端状态,则目标 Q 值更新为
r。
最后,我们使用选定的训练样本批次来更新神经网络。
训练和调整网络
在这个示例中,我们将训练 DQN 模型来玩 Flappy Bird。
在训练的每个步骤中,我们根据 epsilon-greedy 策略采取一个动作:在一定概率(epsilon)下,我们会随机采取一个动作,例如拍打或不拍打;否则,我们选择具有最高值的动作。我们还调整 epsilon 的值以便在 DQN 模型刚开始时更多地进行探索,在模型变得更加成熟时更多地进行利用。
正如我们所见,每一步观察的观察是屏幕的二维图像。我们需要将观察图像转换为状态。仅使用一步中的一个图像将无法提供足够的信息来指导代理程序如何反应。因此,我们使用四个相邻步骤的图像来形成一个状态。我们首先将图像重新形状为预期大小,然后将当前帧的图像与前三个帧的图像连接起来。
如何做...
我们按以下方式训练 DQN 模型:
- 导入必要的模块:
>>> import random
>>> import torch
>>> from collections import deque
- 我们从开发ε-greedy 策略开始:
>>> def gen_epsilon_greedy_policy(estimator, epsilon, n_action):
... def policy_function(state):
... if random.random() < epsilon:
... return random.randint(0, n_action - 1)
... else:
... q_values = estimator.predict(state)
... return torch.argmax(q_values).item()
... return policy_function
- 我们指定预处理图像的大小、批处理大小、学习率、γ值、动作数量、初始和最终ε值、迭代次数以及内存的大小:
>>> image_size = 84
>>> batch_size = 32
>>> lr = 1e-6
>>> gamma = 0.99
>>> init_epsilon = 0.1
>>> final_epsilon = 1e-4
>>> n_iter = 2000000
>>> memory_size = 50000
>>> n_action = 2
我们还定期保存训练好的模型,因为这是一个非常漫长的过程:
>>> saved_path = 'trained_models'
不要忘记创建名为trained_models的文件夹。
- 我们为实验的可重现性指定随机种子:
>>> torch.manual_seed(123)
- 我们相应地创建一个 DQN 模型:
>>> estimator = DQN(n_action)
我们还创建一个内存队列:
>>> memory = deque(maxlen=memory_size)
只要队列中的样本超过 50,000 个,就会附加新样本并移除旧样本。
- 接下来,我们初始化一个 Flappy Bird 环境:
>>> env = FlappyBird()
然后我们获取初始图像:
>>> image, reward, is_done = env.next_step(0)
- 正如前面提到的,我们应该将原始图像调整为
image_size * image_size:
>>> import cv2
>>> import numpy as np
>>> def pre_processing(image, width, height):
... image = cv2.cvtColor(cv2.resize(image,
(width, height)), cv2.COLOR_BGR2GRAY)
... _, image = cv2.threshold(image, 1, 255, cv2.THRESH_BINARY)
... return image[None, :, :].astype(np.float32)
如果尚未安装cv2包,您可以使用以下命令安装:
pip install opencv-python
让我们相应地预处理图像:
>>> image = pre_processing(image[:screen_width, :int(env.base_y)], image_size, image_size)
- 现在,我们通过连接四个图像来构造一个状态。因为现在我们只有第一帧图像,所以我们简单地将其复制四次:
>>> image = torch.from_numpy(image) >>> state = torch.cat(tuple(image for _ in range(4)))[None, :, :, :]
- 然后我们对
n_iter步骤的训练循环进行操作:
>>> for iter in range(n_iter):
... epsilon = final_epsilon + (n_iter - iter)
* (init_epsilon - final_epsilon) / n_iter
... policy = gen_epsilon_greedy_policy( estimator, epsilon, n_action)
... action = policy(state)
... next_image, reward, is_done = env.next_step(action)
... next_image = pre_processing(next_image[ :screen_width, :int(env.base_y)], image_size, image_size)
... next_image = torch.from_numpy(next_image)
... next_state = torch.cat(( state[0, 1:, :, :], next_image))[None, :, :, :]
... memory.append([state, action, next_state, reward, is_done])
... loss = estimator.replay(memory, batch_size, gamma)
... state = next_state
... print("Iteration: {}/{}, Action: {},
Loss: {}, Epsilon {}, Reward: {}".format(
... iter + 1, n_iter, action, loss, epsilon, reward))
... if iter+1 % 10000 == 0:
... torch.save(estimator.model, "{}/{}".format( saved_path, iter+1))
在我们运行这部分代码后,我们将看到以下日志:
Iteration: 1/2000000, Action: 0, Loss: None, Epsilon 0.1, Reward: 0.1 Iteration: 2/2000000, Action: 0, Loss: None, Epsilon 0.09999995005000001, Reward: 0.1
Iteration: 3/2000000, Action: 0, Loss: None, Epsilon 0.0999999001, Reward: 0.1
Iteration: 4/2000000, Action: 0, Loss: None, Epsilon 0.09999985015, Reward: 0.1
...
...
Iteration: 201/2000000, Action: 1, Loss: 0.040504034608602524, Epsilon 0.09999001000000002, Reward: 0.1
Iteration: 202/2000000, Action: 1, Loss: 0.010011588223278522, Epsilon 0.09998996005, Reward: 0.1
Iteration: 203/2000000, Action: 1, Loss: 0.07097195833921432, Epsilon 0.09998991010000001, Reward: 0.1
Iteration: 204/2000000, Action: 1, Loss: 0.040418840944767, Epsilon 0.09998986015000001, Reward: 0.1
Iteration: 205/2000000, Action: 1, Loss: 0.00999421812593937, Epsilon 0.09998981020000001, Reward: 0.1
训练会花费一些时间。当然,您可以通过 GPU 加速训练。
- 最后,我们保存最后训练的模型:
>>> torch.save(estimator.model, "{}/final".format(saved_path))
工作原理...
在Step 9中,对于每一个训练步骤,我们执行以下任务:
-
稍微减小ε,并相应地创建ε-greedy 策略。
-
使用ε-greedy 策略计算采取的行动。
-
对生成的图像进行预处理,并通过将其附加到之前三个步骤的图像中来构造新的状态。
-
记录本步骤的经验,包括状态、行动、下一个状态、接收的奖励以及是否结束。
-
使用经验重播更新模型。
-
打印出训练状态并更新状态。
-
定期保存训练好的模型,以避免从头开始重新训练。
部署模型并玩游戏
现在我们已经训练好了 DQN 模型,让我们将其应用于玩 Flappy Bird 游戏。
使用训练模型玩游戏很简单。我们只需在每一步中采取与最高值相关联的动作。我们将播放几个剧集来查看其表现。不要忘记预处理原始屏幕图像并构造状态。
如何做...
我们在新的剧集上测试 DQN 模型的表现如下:
- 我们首先加载最终模型:
>>> model = torch.load("{}/final".format(saved_path))
- 我们运行 100 集,并对每一集执行以下操作:
>>> n_episode = 100 >>> for episode in range(n_episode):
... env = FlappyBird()
... image, reward, is_done = env.next_step(0)
... image = pre_processing(image[:screen_width,
:int(env.base_y)], image_size, image_size)
... image = torch.from_numpy(image)
... state = torch.cat(tuple(image for _ in range(4)))[ None, :, :, :]
... while True:
... prediction = model(state)[0]
... action = torch.argmax(prediction).item()
... next_image, reward, is_done = env.next_step(action)
... if is_done:
... break
... next_image = pre_processing(next_image[:screen_width, :int(env.base_y)], image_size, image_size)
... next_image = torch.from_numpy(next_image)
... next_state = torch.cat((state[0, 1:, :, :],
next_image))[None, :, :, :]
... state = next_state
希望您能看到类似以下图像的内容,鸟类通过一系列管道:
工作原理是这样的...
在第 2 步中,我们对每一集执行以下任务:
-
初始化 Flappy Bird 环境。
-
观察初始图像并生成其状态。
-
使用模型计算给定状态的 Q 值,并选择具有最高 Q 值的动作。
-
观察新图像以及集数是否结束。
-
如果集数继续,计算下一个图像的状态并将其分配给当前状态。
-
重复直到集数结束。