PyTorch 1.x 强化学习秘籍(二)
原文:
zh.annas-archive.org/md5/863e6116b9dfbed5ea6521a90f2b5732译者:飞龙
第三章:用于进行数值估计的蒙特卡洛方法
在前一章中,我们使用动态规划评估和解决了马尔可夫决策过程 (MDP)。像 DP 这样的模型基方法有一些缺点。它们需要完全了解环境,包括转移矩阵和奖励矩阵。它们的可扩展性也有限,特别是对于具有大量状态的环境。
在本章中,我们将继续我们的学习之旅,采用无模型方法,即蒙特卡洛 (MC) 方法,它不需要环境的先前知识,并且比 DP 具有更高的可扩展性。我们将从使用蒙特卡洛方法估计π的值开始。接下来,我们将讨论如何使用 MC 方法以首次访问和每次访问的方式来预测状态值和状态-动作值。我们将演示如何使用蒙特卡洛训练代理玩二十一点游戏。此外,我们还将实现基于策略和离策略的 MC 控制,以找到二十一点的最优策略。还将介绍具有ε-贪心策略和加权重要性采样的高级 MC 控制。
本章将涵盖以下配方:
-
使用蒙特卡洛方法计算π
-
执行蒙特卡洛策略评估
-
使用蒙特卡洛预测玩二十一点
-
执行基于策略的蒙特卡洛控制
-
开发具有ε-贪心策略的蒙特卡洛控制
-
执行离策略蒙特卡洛控制
-
开发具有加权重要性采样的 MC 控制
使用蒙特卡洛方法计算π
让我们开始一个简单的项目:使用蒙特卡洛方法估算π的值,这是无模型强化学习算法的核心。
蒙特卡洛方法是使用随机性解决问题的任何方法。该算法重复适当的随机抽样,并观察满足特定属性的样本分数,以便进行数值估计。
让我们做一个有趣的练习,使用 MC 方法近似计算π的值。我们在一个边长为 2 的正方形内随机放置大量点(-1<x<1, -1<y<1),并计算落入单位半径圆内的点数。我们都知道正方形的面积为:
圆的面积为:
如果我们将圆的面积除以正方形的面积,我们有以下结果:
S/C可以用落入圆内的点的比例来衡量。因此,π的值可以估计为S/C的四倍。
如何做到...
我们使用 MC 方法来估算π的值如下:
- 导入必要的模块,包括 PyTorch,π的真实值的
math,以及用于在正方形内绘制随机点的matplotlib:
>>> import torch
>>> import math
>>> import matplotlib.pyplot as plt
- 我们随机生成 1,000 个点在正方形内,范围为-1<x<1 和-1<y<1:
>>> n_point = 1000
>>> points = torch.rand((n_point, 2)) * 2 - 1
- 初始化单位圆内的点数,并存储这些点的列表:
>>> n_point_circle = 0
>>> points_circle = []
- 对于每个随机点,计算到原点的距离。如果距离小于 1,则点落在圆内:
>>> for point in points:
... r = torch.sqrt(point[0] ** 2 + point[1] ** 2)
... if r <= 1:
... points_circle.append(point)
... n_point_circle += 1
- 统计圆内点的数量,并跟踪这些点:
>>> points_circle = torch.stack(points_circle)
- 绘制所有随机点,并对圆内的点使用不同的颜色:
>>> plt.plot(points[:, 0].numpy(), points[:, 1].numpy(), 'y.')
>>> plt.plot(points_circle[:, 0].numpy(), points_circle[:, 1].numpy(), 'c.')
- 绘制圆以获得更好的可视化效果:
>>> i = torch.linspace(0, 2 * math.pi)
>>> plt.plot(torch.cos(i).numpy(), torch.sin(i).numpy())
>>> plt.axes().set_aspect('equal')
>>> plt.show()
- 最后,计算π的值:
>>> pi_estimated = 4 * (n_point_circle / n_point)
>>> print('Estimated value of pi is:', pi_estimated)
工作原理是这样的...
在第 5 步,您将看到以下图,其中的点是随机放置在圆内:
蒙特卡洛方法之所以如此强大,要归功于大数定律(LLN)。根据大数定律,大量重复事件或动作的平均表现最终会收敛于期望值。在我们的情况下,大量随机点,4 * (n_point_circle / n_point) 最终会收敛于π的真实值。
最后,在第 8 步,我们打印π的估计值,得到以下结果:
Estimated value of pi is: 3.156
使用蒙特卡洛方法近似计算π的值非常接近其真实值(3.14159...)。
还有更多内容...
我们可以通过比 1,000 次更多的迭代进一步改进我们的估计。在这里,我们将尝试 10,000 次迭代。在每次迭代中,我们在正方形内随机生成一个点,并检查它是否在圆内;根据落入圆内的点的比例,我们即时估算π的值。
然后我们将估计历史与π的真实值一起绘制。将它们放入以下函数中:
>>> def estimate_pi_mc(n_iteration):
... n_point_circle = 0
... pi_iteration = []
... for i in range(1, n_iteration+1):
... point = torch.rand(2) * 2 - 1
... r = torch.sqrt(point[0] ** 2 + point[1] ** 2)
... if r <= 1:
... n_point_circle += 1
... pi_iteration.append(4 * (n_point_circle / i))
... plt.plot(pi_iteration)
... plt.plot([math.pi] * n_iteration, '--')
... plt.xlabel('Iteration')
... plt.ylabel('Estimated pi')
... plt.title('Estimation history')
... plt.show()
... print('Estimated value of pi is:', pi_iteration[-1]) The estimated value of pi is: 3.1364
然后我们使用 10,000 次迭代调用这个函数:
>>> estimate_pi_mc(10000)
参考以下图表查看估计历史的结果:
我们可以看到,随着更多的迭代次数,π的估计值越来越接近真实值。事件或行动总是存在一些变化。增加重复次数可以帮助平滑这种变化。
另请参阅
如果你对蒙特卡洛方法的更多应用感兴趣,这里有一些有趣的应用:
-
通过 MC 树搜索玩游戏,如围棋,哈瓦纳,战舰,寻找最佳移动:
en.wikipedia.org/wiki/Monte_Carlo_tree_search -
评估投资和投资组合:
en.wikipedia.org/wiki/Monte_Carlo_methods_in_finance -
使用 MC 模拟研究生物系统:
en.wikipedia.org/wiki/Bayesian_inference_in_phylogeny
执行蒙特卡洛策略评估
在第二章,马尔可夫决策过程与动态规划中,我们应用 DP 进行策略评估,即策略的值(或状态值)函数。这确实效果很好,但也有一些限制。基本上,它需要完全了解环境,包括转移矩阵和奖励矩阵。然而,在大多数实际情况下,转移矩阵事先是未知的。需要已知 MDP 的强化学习算法被归类为基于模型的算法。另一方面,不需要先验知识的转移和奖励的算法被称为无模型算法。基于蒙特卡洛的强化学习是一种无模型方法。
在这个示例中,我们将使用蒙特卡洛方法评估值函数。我们再次使用 FrozenLake 环境作为示例,假设我们无法访问其转移和奖励矩阵。你会记得过程的返回,即长期内的总奖励,如下所示:
MC 策略评估使用经验均值返回而不是期望返回(如 DP 中)来估计值函数。有两种方法可以进行 MC 策略评估。一种是首次访问 MC 预测,它仅对状态 s 在一个 episode 中的第一次出现进行返回平均。另一种是每次访问 MC 预测,它对状态 s 在一个 episode 中的每次出现进行返回平均。显然,首次访问 MC 预测比每次访问版本计算要少得多,因此更频繁地使用。
如何做...
我们对 FrozenLake 的最优策略执行首次访问 MC 预测如下:
- 导入 PyTorch 和 Gym 库,并创建 FrozenLake 环境的实例:
>>> import torch
>>> import gym >>> env = gym.make("FrozenLake-v0")
- 要使用蒙特卡洛方法评估策略,我们首先需要定义一个函数,该函数模拟给定策略的 FrozenLake episode,并返回每个步骤的奖励和状态:
>>> def run_episode(env, policy):
... state = env.reset()
... rewards = []
... states = [state]
... is_done = False
... while not is_done:
... action = policy[state].item()
... state, reward, is_done, info = env.step(action)
... states.append(state)
... rewards.append(reward)
... if is_done:
... break
... states = torch.tensor(states)
... rewards = torch.tensor(rewards)
... return states, rewards
同样,在蒙特卡洛设置中,我们需要跟踪所有步骤的状态和奖励,因为我们无法访问完整的环境,包括转移概率和奖励矩阵。
- 现在,定义一个使用首次访问 MC 评估给定策略的函数:
>>> def mc_prediction_first_visit(env, policy, gamma, n_episode):
... n_state = policy.shape[0]
... V = torch.zeros(n_state)
... N = torch.zeros(n_state)
... for episode in range(n_episode):
... states_t, rewards_t = run_episode(env, policy)
... return_t = 0
... first_visit = torch.zeros(n_state)
... G = torch.zeros(n_state)
... for state_t, reward_t in zip(reversed(states_t)[1:],
reversed(rewards_t)):
... return_t = gamma * return_t + reward_t
... G[state_t] = return_t
... first_visit[state_t] = 1
... for state in range(n_state):
... if first_visit[state] > 0:
... V[state] += G[state]
... N[state] += 1
... for state in range(n_state):
... if N[state] > 0:
... V[state] = V[state] / N[state]
... return V
- 我们将折现率设定为 1 以便计算更加简便,并模拟了 10,000 个 episode:
>>> gamma = 1
>>> n_episode = 10000
- 我们使用前一章节中计算的最优策略,马尔可夫决策过程与动态规划,将其输入到首次访问 MC 函数中,同时还包括其他参数:
>>> optimal_policy = torch.tensor([0., 3., 3., 3., 0., 3., 2., 3., 3., 1., 0., 3., 3., 2., 1., 3.])
>>> value = mc_prediction_first_visit(env, optimal_policy, gamma, n_episode)
>>> print('The value function calculated by first-visit MC prediction:\n', value)
The value function calculated by first-visit MC prediction:
tensor([0.7463, 0.5004, 0.4938, 0.4602, 0.7463, 0.0000, 0.3914, 0.0000, 0.7463, 0.7469, 0.6797, 0.0000, 0.0000, 0.8038, 0.8911, 0.0000])
我们刚刚使用首次访问 MC 预测解决了最优策略的值函数。
它的工作原理...
在第 3 步中,我们在 MC 预测中执行以下任务:
-
我们运行
n_episode个 episode -
对于每个 episode,我们计算每个状态的首次访问的返回
-
对于每个状态,我们通过平均所有集的首次回报来获取值
正如您所看到的,在基于 MC 的预测中,并不需要了解环境的完整模型。事实上,在大多数真实情况下,过渡矩阵和奖励矩阵事先是未知的,或者极其难以获得。想象一下下棋或围棋中可能的状态数量以及可能的动作数量;几乎不可能计算出过渡矩阵和奖励矩阵。无模型强化学习是通过与环境交互从经验中学习的过程。
在我们的情况下,我们只考虑了可以观察到的内容,这包括每一步中的新状态和奖励,并使用 Monte Carlo 方法进行预测。请注意,我们模拟的集数越多,我们可以获得的预测越精确。如果您绘制每个集后更新的值,您将看到它如何随时间收敛,这与我们估计π值时看到的情况类似。
还有更多...
我们决定为冰湖的最优策略也执行每次访问的 MC 预测:
- 我们定义了使用每次访问 MC 评估给定策略的函数:
>>> def mc_prediction_every_visit(env, policy, gamma, n_episode):
... n_state = policy.shape[0]
... V = torch.zeros(n_state)
... N = torch.zeros(n_state)
... G = torch.zeros(n_state)
... for episode in range(n_episode):
... states_t, rewards_t = run_episode(env, policy)
... return_t = 0
... for state_t, reward_t in zip(reversed(states_t)[1:],
reversed(rewards_t)):
... return_t = gamma * return_t + reward_t
... G[state_t] += return_t
... N[state_t] += 1
... for state in range(n_state):
... if N[state] > 0:
... V[state] = G[state] / N[state]
... return V
与首次访问 MC 类似,每次访问函数执行以下任务:
-
运行
n_episode集 -
对于每一集,它计算每次访问状态的回报
-
对于每个状态,通过平均所有集的所有回报来获取值
- 通过在函数中输入策略和其他参数来计算值:
>>> value = mc_prediction_every_visit(env, optimal_policy, gamma, n_episode)
- 显示结果值:
>>> print('The value function calculated by every-visit MC prediction:\n', value)
The value function calculated by every-visit MC prediction:
tensor([0.6221, 0.4322, 0.3903, 0.3578, 0.6246, 0.0000, 0.3520, 0.0000, 0.6428, 0.6759, 0.6323, 0.0000, 0.0000, 0.7624, 0.8801, 0.0000])
使用 Monte Carlo 预测玩 21 点
在这个示例中,我们将玩 21 点(也称为 21),并评估我们认为可能有效的策略。您将通过 21 点的示例更加熟悉使用 Monte Carlo 预测,并准备在即将到来的示例中使用 Monte Carlo 控制搜索最优策略。
21 点是一种流行的纸牌游戏,目标是使牌的总和尽可能接近 21 点而不超过它。J、K 和 Q 牌的点数为 10,2 到 10 的牌的点数为 2 到 10。A 牌可以是 1 点或 11 点;选择后者称为可用A。玩家与庄家竞争。一开始,双方都会得到两张随机牌,但只有一张庄家的牌对玩家可见。玩家可以要求额外的牌(称为要牌)或停止接收更多的牌(称为停牌)。在玩家停牌之前,如果他们的牌的总和超过 21(称为爆牌),则玩家输。否则,如果庄家的牌总和超过 21,玩家赢。
如果两方都没有爆牌,得分最高的一方将获胜,或者可能是平局。Gym 中的 21 点环境如下所述:
-
Blackjack 有限 MDP 的一轮情节以每方两张牌开始,只有一张庄家的牌是可见的。
-
一个情节以任一方获胜或双方平局结束。情节的最终奖励如下:如果玩家获胜,则为
+1;如果玩家输,则为-1;如果平局,则为0。 -
每轮中,玩家可以执行两个动作,要牌(1)和停牌(0),表示请求另一张牌和请求不再接收任何更多的牌。
我们首先尝试一个简单的策略,即只要总点数小于 18(或者您更喜欢的 19 或 20),就继续添加新的牌。
如何做到...
让我们从模拟 Blackjack 环境开始,并探索其状态和行动:
- 导入 PyTorch 和 Gym,并创建一个
Blackjack实例:
>>> import torch
>>> import gym
>>> env = gym.make('Blackjack-v0')
- 重置环境:
>>> env.reset()
>>> env.reset()
(20, 5, False)
它返回三个状态变量:
-
玩家的点数(本例中为
20) -
庄家的点数(本例中为
5) -
玩家是否拥有可重复使用的 Ace(本例中为
False)
可重复使用的 Ace 意味着玩家拥有一个 Ace,可以将其计为 11 而不会爆牌。如果玩家没有 Ace,或者有 Ace 但使他们爆牌,状态参数将变为 False。
查看以下情节:
>>> env.reset()
(18, 6, True)
18 点和 True 表示玩家有一个 Ace 和一个 7,Ace 被计为 11。
- 让我们执行一些动作,看看 Blackjack 环境的工作原理。首先,我们要牌(请求额外的一张牌),因为我们有可重复使用的 Ace,这提供了一些灵活性:
>>> env.step(1)
((20, 6, True), 0, False, {})
这返回三个状态变量 (20, 6, True),一个奖励(目前为0),以及该情节是否结束(目前为False)。
然后我们停止抽牌:
>>> env.step(0)
((20, 6, True), 1, True, {})
在这一情节中我们刚刚赢得了比赛,因此奖励为 1,现在情节结束了。再次提醒,一旦玩家选择 停牌,庄家将采取他们的行动。
- 有时我们会输掉;例如:
>>> env.reset()
(15, 10, False)
>>> env.step(1)
((25, 10, False), -1, True, {})
接下来,我们将预测一个简单策略的值,在该策略中,当分数达到 18 时停止添加新牌:
- 和往常一样,我们首先需要定义一个函数,模拟一个简单策略下的 Blackjack 情节:
>>> def run_episode(env, hold_score):
... state = env.reset()
... rewards = []
... states = [state]
... is_done = False
... while not is_done:
... action = 1 if state[0] < hold_score else 0
... state, reward, is_done, info = env.step(action)
... states.append(state)
... rewards.append(reward)
... if is_done:
... break
... return states, rewards
- 现在,我们定义一个评估简单的 Blackjack 策略的函数,使用首次访问 MC 方法:
>>> from collections import defaultdict
>>> def mc_prediction_first_visit(env, hold_score, gamma,
n_episode):
... V = defaultdict(float)
... N = defaultdict(int)
... for episode in range(n_episode):
... states_t, rewards_t = run_episode(env, hold_score)
... return_t = 0
... G = {}
... for state_t, reward_t in zip(states_t[1::-1],
rewards_t[::-1]):
... return_t = gamma * return_t + reward_t
... G[state_t] = return_t
... for state, return_t in G.items():
... if state[0] <= 21:
... V[state] += return_t
... N[state] += 1
... for state in V:
... V[state] = V[state] / N[state]
... return V
- 我们将
hold_score设置为 18,折扣率设置为 1,并模拟 500,000 个情节:
>>> hold_score = 18
>>> gamma = 1
>>> n_episode = 500000
- 现在,让我们通过插入所有变量来执行 MC 预测:
>>> value = mc_prediction_first_visit(env, hold_score, gamma, n_episode)
我们尝试打印结果值函数:
>>> print('The value function calculated by first-visit MC prediction:\n', value)
我们刚刚计算了所有可能状态的值:
>>> print('Number of states:', len(value))
Number of states: 280
总共有 280 个状态。
它的工作原理...
正如您所见,在 第 4 步,我们的点数超过了 21,所以我们输了。再次提醒,Blackjack 的状态实际上是一个三元组。第一个元素是玩家的分数;第二个元素是庄家牌堆中的明牌,其值可以是 1 到 10;第三个元素是是否拥有可重复使用的 Ace。
值得注意的是,在步骤 5中,在每一轮的一个回合中,代理根据当前分数是否停止,如果分数小于hold_score则停止,否则继续。同样,在蒙特卡罗设置中,我们跟踪所有步骤的状态和奖励。
执行步骤 8中的代码行,您将看到以下结果:
The value function calculated by first-visit MC prediction:
defaultdict(<class 'float'>, {(20, 6, False): 0.6923485653560042, (17, 5, False): -0.24390243902439024, (16, 5, False): -0.19118165784832453, (20, 10, False): 0.4326379146490474, (20, 7, False): 0.7686220540168588, (16, 6, False): -0.19249478804725503,
……
……
(5, 9, False): -0.20612244897959184, (12, 7, True): 0.058823529411764705, (6, 4, False): -0.26582278481012656, (4, 8, False): -0.14937759336099585, (4, 3, False): -0.1680327868852459, (4, 9, False): -0.20276497695852536, (4, 4, False): -0.3201754385964912, (12, 8, True): 0.11057692307692307})
我们刚刚体验了使用 MC 预测计算 21 点环境中 280 个状态的值函数的效果。在步骤 2中的 MC 预测函数中,我们执行了以下任务:
-
我们在简单的 21 点策略下运行了
n_episode轮次。 -
对于每一轮次,我们计算了每个状态的首次访问的回报。
-
对于每个状态,我们通过所有轮次的首次回报的平均值来获取价值。
请注意,我们忽略了玩家总分大于 21 分的状态,因为我们知道它们都会是-1。
21 点环境的模型,包括转移矩阵和奖励矩阵,在先验上是不知道的。此外,获取两个状态之间的转移概率是非常昂贵的。事实上,转移矩阵的大小将是 280 * 280 * 2,这将需要大量的计算。在基于 MC 的解决方案中,我们只需模拟足够的轮次,并且对于每一轮次,计算回报并相应地更新值函数即可。
下次你使用简单策略玩 21 点时(如果总分达到某一水平则停止),使用预测的值来决定每局游戏下注金额将会很有趣。
还有更多...
因为在这种情况下有很多状态,逐一读取它们的值是困难的。我们实际上可以通过制作三维表面图来可视化值函数。状态是三维的,第三个维度具有两个可能的选项(有可用有效王牌或无)。我们可以将我们的图分为两部分:一部分用于具有可用有效王牌的状态,另一部分用于没有可用有效王牌的状态。在每个图中,x轴是玩家的总和,y轴是庄家的明牌,z轴是值。
让我们按照这些步骤来创建可视化:
- 导入用于可视化的 matplotlib 中的所有必要模块:
>>> import matplotlib
>>> import matplotlib.pyplot as plt
>>> from mpl_toolkits.mplot3d import Axes3D
- 定义一个创建三维表面图的实用函数:
>>> def plot_surface(X, Y, Z, title):
... fig = plt.figure(figsize=(20, 10))
... ax = fig.add_subplot(111, projection='3d')
... surf = ax.plot_surface(X, Y, Z, rstride=1, cstride=1,
... cmap=matplotlib.cm.coolwarm, vmin=-1.0, vmax=1.0)
... ax.set_xlabel('Player Sum')
... ax.set_ylabel('Dealer Showing')
... ax.set_zlabel('Value')
... ax.set_title(title)
... ax.view_init(ax.elev, -120)
... fig.colorbar(surf)
... plt.show()
- 接下来,我们定义一个函数,构建要在三个维度上绘制的数组,并调用
plot_surface来可视化具有和不具有可用有效王牌的值:
>>> def plot_blackjack_value(V):
... player_sum_range = range(12, 22)
... dealer_show_range = range(1, 11)
... X, Y = torch.meshgrid([torch.tensor(player_sum_range),
torch.tensor(dealer_show_range)])
... values_to_plot = torch.zeros((len(player_sum_range),
len(dealer_show_range), 2))
... for i, player in enumerate(player_sum_range):
... for j, dealer in enumerate(dealer_show_range):
... for k, ace in enumerate([False, True]):
... values_to_plot[i, j, k] =
V[(player, dealer, ace)]
... plot_surface(X, Y, values_to_plot[:,:,0].numpy(),
"Blackjack Value Function Without Usable Ace")
... plot_surface(X, Y, values_to_plot[:,:,1].numpy(),
"Blackjack Value Function With Usable Ace")
我们只对玩家得分超过 11 分的状态感兴趣,并创建一个values_to_plot张量来存储这些数值。
- 最后,我们调用
plot_blackjack_value函数:
>>> plot_blackjack_value(value)
无法使用有效王牌的状态的结果值图如下所示:
而对于有可用有效王牌的状态的值函数如下图所示:
随时调整hold_score的值并查看它如何影响值函数。
另请参阅
如果您对Blackjack环境还不熟悉,可以从源代码了解更多信息,位于github.com/openai/gym/blob/master/gym/envs/toy_text/blackjack.py。
有时,阅读代码比阅读简单的英文描述更容易。
执行基于策略的 Monte Carlo 控制
在上一个示例中,我们预测了一种策略的值,其中代理如果得分达到 18 则持有。这是每个人都可以轻松提出的一种简单策略,尽管显然不是最优策略。在本示例中,我们将使用基于策略的 Monte Carlo 控制来寻找最优的 Blackjack 玩法策略。
Monte Carlo 预测用于评估给定策略的值,而Monte Carlo 控制(MC 控制)用于在没有给定策略时寻找最优策略。基本上有两种类型的 MC 控制:基于策略和脱离策略。基于策略方法通过执行策略并评估和改进来学习最优策略,而脱离策略方法使用由另一个策略生成的数据来学习最优策略。基于策略 MC 控制的工作方式与动态规划中的策略迭代非常相似,有两个阶段,评估和改进:
-
在评估阶段,它评估动作值函数(也称为行动值或效用),而不是评估值函数(也称为状态值或效用)。行动值更常被称为Q 函数,它是在给定策略下,通过在状态s中采取动作a来获得的状态-动作对*(s, a)*的效用。再次强调,评估可以以首次访问方式或每次访问方式进行。
-
在改进阶段,通过为每个状态分配最优动作来更新策略:
最优策略将通过在大量迭代中交替两个阶段来获取。
如何做...
让我们通过以下步骤使用基于策略的 MC 控制来寻找最优的 Blackjack 策略:
- 导入必要的模块并创建一个 Blackjack 实例:
>>> import torch
>>> import gym
>>> env = gym.make('Blackjack-v0')
- 接下来,让我们开发一个函数,运行一个剧集并根据 Q 函数采取行动。这是改进阶段:
>>> def run_episode(env, Q, n_action):
... """
... Run a episode given a Q-function
... @param env: OpenAI Gym environment
... @param Q: Q-function
... @param n_action: action space
... @return: resulting states, actions and rewards for the entire episode
... """
... state = env.reset()
... rewards = []
... actions = []
... states = []
... is_done = False
... action = torch.randint(0, n_action, [1]).item()
... while not is_done:
... actions.append(action)
... states.append(state)
... state, reward, is_done, info = env.step(action)
... rewards.append(reward)
... if is_done:
... break
... action = torch.argmax(Q[state]).item()
... return states, actions, rewards
- 现在,我们开发了基于策略的 MC 控制算法:
>>> from collections import defaultdict
>>> def mc_control_on_policy(env, gamma, n_episode):
... """
... Obtain the optimal policy with on-policy MC control method
... @param env: OpenAI Gym environment
... @param gamma: discount factor
... @param n_episode: number of episodes
... @return: the optimal Q-function, and the optimal policy
... """ ... n_action = env.action_space.n
... G_sum = defaultdict(float)
... N = defaultdict(int)
... Q = defaultdict(lambda: torch.empty(env.action_space.n))
... for episode in range(n_episode):
... states_t, actions_t, rewards_t = run_episode(env, Q, n_action)
... return_t = 0
... G = {}
... for state_t, action_t, reward_t in zip(states_t[::-1],
actions_t[::-1], rewards_t[::-1]):
... return_t = gamma * return_t + reward_t
... G[(state_t, action_t)] = return_t
... for state_action, return_t in G.items():
... state, action = state_action
... if state[0] <= 21:
... G_sum[state_action] += return_t
... N[state_action] += 1
... Q[state][action] = G_sum[state_action]
/ N[state_action]
... policy = {}
... for state, actions in Q.items():
... policy[state] = torch.argmax(actions).item()
... return Q, policy
- 我们将折现率设为 1,并将使用 500,000 个剧集:
>>> gamma = 1
>>> n_episode = 500000
- 执行基于策略的 MC 控制,以获取最优的 Q 函数和策略:
>>> optimal_Q, optimal_policy = mc_control_on_policy(env, gamma, n_episode) >>> print(optimal_policy)
- 我们还可以计算最优策略的值函数,并打印出最优值如下:
>>> optimal_value = defaultdict(float)
>>> for state, action_values in optimal_Q.items():
... optimal_value[state] = torch.max(action_values).item() >>> print(optimal_value)
- 使用
plot_blackjack_value和我们在上一个示例使用 Monte Carlo 预测玩 Blackjack中开发的plot_surface函数来可视化值:
>>> plot_blackjack_value(optimal_value)
它是如何工作的...
在这个方案中,我们通过探索启动的 on-policy MC 控制解决了二十一点游戏。通过模拟每个剧集交替进行评估和改进,达到了我们的策略优化目标。
在 Step 2,我们运行一个剧集,并根据 Q 函数执行动作,具体任务如下:
-
我们初始化一个剧集。
-
我们以探索启动的方式随机选择一个动作。
-
第一个动作后,我们根据当前 Q 函数采取动作,即 ![]。
-
我们记录剧集中所有步骤的状态、动作和奖励,这将在评估阶段中使用。
需要注意的是,第一个动作是随机选择的,因为只有在这种情况下,MC 控制算法才会收敛到最优解。在 MC 算法中以随机动作开始一个剧集称为 探索启动。
在探索启动设置中,为了确保策略收敛到最优解,一个剧集中的第一个动作是随机选择的。否则,一些状态将永远不会被访问,因此它们的状态-动作值永远不会被优化,最终策略将变得次优。
Step 2 是改进阶段,Step 3 是用于 MC 控制的阶段,在这个阶段我们执行以下任务:
-
用任意小的值初始化 Q 函数。
-
运行
n_episode个剧集。 -
对于每个剧集,执行策略改进,并获得状态、动作和奖励;并使用基于结果状态、动作和奖励的首次访问 MC 预测执行策略评估,更新 Q 函数。
-
最后,最优 Q 函数完成并且通过在最优 Q 函数中为每个状态选择最佳动作获得最优策略。
在每次迭代中,我们通过采取相对于当前动作值函数 Q 的最优动作来使策略贪婪化(即 ![])。因此,我们将能够获得一个最优策略,即使我们从任意策略开始。
在 Step 5,您可以看到最优策略的结果如下:
{(16, 8, True): 1, (11, 2, False): 1, (15, 5, True): 1, (14, 9, False): 1, (11, 6, False): 1, (20, 3, False): 0, (9, 6, False): 0, (12, 9, False): 0, (21, 2, True): 0, (16, 10, False): 1, (17, 5, False): 0, (13, 10, False): 1, (12, 10, False): 1, (14, 10, False): 0, (10, 2, False): 1, (20, 4, False): 0, (11, 4, False): 1, (16, 9, False): 0, (10, 8,
……
……
1, (18, 6, True): 0, (12, 2, True): 1, (8, 3, False): 1, (13, 3, True): 0, (4, 7, False): 1, (18, 8, True): 0, (6, 5, False): 1, (17, 6, True): 0, (19, 9, True): 0, (4, 4, False): 0, (14, 5, True): 1, (12, 6, True): 0, (4, 9, False): 1, (13, 4, True): 1, (4, 8, False): 1, (14, 3, True): 1, (12, 4, True): 1, (4, 6, False): 0, (12, 5, True): 0, (4, 2, False): 1, (4, 3, False): 1, (5, 4, False): 1, (4, 1, False): 0}
在 Step 6,您可以看到最优策略的结果值如下:
{(21, 8, False): 0.9262458682060242, (11, 8, False): 0.16684606671333313, (16, 10, False): -0.4662476181983948, (16, 10, True): -0.3643564283847809, (14, 8, False): -0.2743947207927704, (13, 10, False): -0.3887477219104767, (12, 9, False): -0.22795115411281586
……
……
(4, 3, False): -0.18421052396297455, (4, 8, False): -0.16806723177433014, (13, 2, True): 0.05485232174396515, (5, 5, False): -0.09459459781646729, (5, 8, False): -0.3690987229347229, (20, 2, True): 0.6965699195861816, (17, 2, True): -0.09696969389915466, (12, 2, True): 0.0517241396009922}
在 Step 7,您将看到没有可用 Ace 的状态的结果值图如下:
并且对于有可用 Ace 的状态,其值函数如下所示:
还有更多...
您可能想知道最优策略是否比简单策略表现更好。现在让我们在最优策略和简单策略下模拟 100,000 个二十一点剧集。我们将比较两种策略的获胜和失败的机会:
- 首先,我们定义一个简单的策略,当分数达到 18 时采取 stick 动作:
>>> hold_score = 18
>>> hold_policy = {}
>>> player_sum_range = range(2, 22)
>>> for player in range(2, 22):
... for dealer in range(1, 11):
... action = 1 if player < hold_score else 0
... hold_policy[(player, dealer, False)] = action
... hold_policy[(player, dealer, True)] = action
- 接下来,我们定义一个包装函数,根据给定的策略运行一个剧集,并返回最终的奖励:
>>> def simulate_episode(env, policy):
... state = env.reset()
... is_done = False
... while not is_done:
... action = policy[state]
... state, reward, is_done, info = env.step(action)
... if is_done:
... return reward
- 然后,我们指定回合数(100,000),并开始计算胜利和失败的次数:
>>> n_episode = 100000
>>> n_win_optimal = 0
>>> n_win_simple = 0
>>> n_lose_optimal = 0
>>> n_lose_simple = 0
- 然后,我们运行了 100,000 个回合并跟踪了胜利和失败的情况:
>>> for _ in range(n_episode):
... reward = simulate_episode(env, optimal_policy)
... if reward == 1:
... n_win_optimal += 1
... elif reward == -1:
... n_lose_optimal += 1
... reward = simulate_episode(env, hold_policy)
... if reward == 1:
... n_win_simple += 1
... elif reward == -1:
... n_lose_simple += 1
- 最后,我们打印出我们得到的结果:
>>> print('Winning probability under the simple policy: {}'.format(n_win_simple/n_episode))
Winning probability under the simple policy: 0.39923
>>> print('Winning probability under the optimal policy: {}'.format(n_win_optimal/n_episode))
Winning probability under the optimal policy: 0.41281
在最优策略下玩牌有 41.28%的赢的可能性,而在简单策略下玩牌有 39.92%的可能性。然后,我们有输的概率:
>>> print('Losing probability under the simple policy: {}'.format(n_lose_simple/n_episode))
Losing probability under the simple policy: 0.51024
>>> print('Losing probability under the optimal policy: {}'.format(n_lose_optimal/n_episode))
Losing probability under the optimal policy: 0.493
另一方面,在最优策略下玩牌有 49.3%的输的可能性,而在简单策略下玩牌有 51.02%的可能性。
我们的最优策略显然是赢家!
开发 MC 控制和ε-贪心策略:
在前一步骤中,我们使用 MC 控制和贪心搜索搜索最优策略,选择具有最高状态-动作值的动作。然而,在早期回合中的最佳选择并不保证最优解。如果我们只关注临时的最佳选项并忽略整体问题,我们将陷入局部最优解而无法达到全局最优解。解决方法是ε-贪心策略。
在ε-贪心策略的 MC 控制中,我们不再始终利用最佳动作,而是在一定概率下随机选择动作。顾名思义,该算法有两个方面:
- ε:给定参数ε,其值从0到1,每个动作的选择概率如下计算:
这里,|A| 是可能的动作数。
- 贪心:偏爱具有最高状态-动作值的动作,并且它被选择的概率增加了1-ε:
ε-贪心策略大部分时间都会利用最佳动作,同时也会时不时地探索不同的动作。
如何操作...
让我们使用ε-贪心策略解决 Blackjack 环境:
- 导入必要的模块并创建一个 Blackjack 实例:
>>> import torch
>>> import gym
>>> env = gym.make('Blackjack-v0')
- 接下来,让我们开发一个运行回合并执行ε-贪心的函数:
>>> def run_episode(env, Q, epsilon, n_action):
... """
... Run a episode and performs epsilon-greedy policy
... @param env: OpenAI Gym environment
... @param Q: Q-function
... @param epsilon: the trade-off between exploration and exploitation
... @param n_action: action space
... @return: resulting states, actions and rewards for the entire episode
... """
... state = env.reset()
... rewards = []
... actions = []
... states = []
... is_done = False
... while not is_done:
... probs = torch.ones(n_action) * epsilon / n_action
... best_action = torch.argmax(Q[state]).item()
... probs[best_action] += 1.0 - epsilon
... action = torch.multinomial(probs, 1).item()
... actions.append(action)
... states.append(state)
... state, reward, is_done, info = env.step(action)
... rewards.append(reward)
... if is_done:
... break
... return states, actions, rewards
- 现在,开发 on-policy MC 控制和ε-贪心策略:
>>> from collections import defaultdict
>>> def mc_control_epsilon_greedy(env, gamma, n_episode, epsilon):
... """
... Obtain the optimal policy with on-policy MC control with epsilon_greedy
... @param env: OpenAI Gym environment
... @param gamma: discount factor
... @param n_episode: number of episodes
... @param epsilon: the trade-off between exploration and exploitation
... @return: the optimal Q-function, and the optimal policy
... """
... n_action = env.action_space.n
... G_sum = defaultdict(float)
... N = defaultdict(int)
... Q = defaultdict(lambda: torch.empty(n_action))
... for episode in range(n_episode):
... states_t, actions_t, rewards_t =
run_episode(env, Q, epsilon, n_action)
... return_t = 0
... G = {}
... for state_t, action_t, reward_t in zip(states_t[::-1],
actions_t[::-1], rewards_t[::-1]):
... return_t = gamma * return_t + reward_t
... G[(state_t, action_t)] = return_t
... for state_action, return_t in G.items():
... state, action = state_action
... if state[0] <= 21:
... G_sum[state_action] += return_t
... N[state_action] += 1
... Q[state][action] =
G_sum[state_action] / N[state_action]
... policy = {}
... for state, actions in Q.items():
... policy[state] = torch.argmax(actions).item()
... return Q, policy
- 我们将折扣率指定为 1,ε指定为 0.1,并将使用 500,000 个回合:
>>> gamma = 1
>>> n_episode = 500000
>>> epsilon = 0.1
- 执行 MC 控制和ε-贪心策略以获取最优的 Q 函数和策略:
>>> optimal_Q, optimal_policy = mc_control_epsilon_greedy(env, gamma, n_episode, epsilon)
随意打印出最优值,并使用我们开发的plot_blackjack_value和plot_surface函数进行可视化。我们此处不再重复该过程。
- 最后,我们想知道ε-贪心方法是否真的效果更好。再次,我们模拟了 100,000 个 Blackjack 的回合,在ε-贪心生成的最优策略下计算赢和输的概率:
>>> n_episode = 100000
>>> n_win_optimal = 0
>>> n_lose_optimal = 0
>>> for _ in range(n_episode):
... reward = simulate_episode(env, optimal_policy)
... if reward == 1:
... n_win_optimal += 1
... elif reward == -1:
... n_lose_optimal += 1
这里,我们重新使用了上一个示例中的simulate_episode函数。
工作原理...
在这个示例中,我们使用ε-贪心的 on-policy MC 控制解决了 Blackjack 游戏。
在第 2 步中,我们运行一个回合,并执行ε-贪心,完成以下任务:
-
我们初始化一个回合。
-
我们计算选择各个动作的概率:基于当前 Q 函数的最佳动作的概率为![],否则的概率为![]。
-
我们记录了每一集中所有步骤的状态、动作和奖励,这将在评估阶段中使用。
ε贪心方法通过以![]的概率利用最佳动作,并同时允许以![]的概率随机探索其他动作。超参数ε是利用与探索之间的权衡。如果其值为 0,则算法完全贪婪;如果值为 1,则每个动作均匀选择,因此算法只进行随机探索。
ε的值需要根据实验进行调整,没有一个适用于所有实验的通用值。话虽如此,一般来说,我们可以选择 0.1、0.2 或 0.3 作为起点。另一种方法是从稍大的值(如 0.5 或 0.7)开始,并逐渐减少(例如每集减少 0.999)。通过这种方式,策略将在开始时专注于探索不同的动作,并随着时间的推移,趋向于利用好的动作。
最后,在执行步骤 6、对来自 10 万集的结果进行平均并打印获胜概率后,我们现在有以下结果:
>>> print('Winning probability under the optimal policy: {}'.format(n_win_optimal/n_episode))
Winning probability under the optimal policy: 0.42436
通过ε贪心方法得到的最优策略具有 42.44%的获胜机率,比没有ε贪心的获胜机率(41.28%)要高。
然后,我们还打印出了失利概率:
>>> print('Losing probability under the optimal policy: {}'.format(n_lose_optimal/n_episode))
Losing probability under the optimal policy: 0.48048
如您所见,ε贪心方法具有较低的失利机率(48.05%与没有ε贪心的 49.3%相比)。
执行离策略蒙特卡洛控制
另一种基于 MC 的方法来解决 MDP 是离策略控制,我们将在这个章节中讨论。
离策略方法通过由另一个称为行为策略b 生成的数据来优化目标策略π。目标策略始终进行利用,而行为策略则用于探索目的。这意味着目标策略在当前 Q 函数的贪婪方面是贪婪的,而行为策略生成行为以便目标策略有数据可学习。行为策略可以是任何东西,只要所有状态的所有动作都能以非零概率选择,这保证了行为策略可以探索所有可能性。
由于我们在离策略方法中处理两种不同的策略,我们只能在两种策略中发生的剧集中使用共同步骤。这意味着我们从行为策略下执行的最新步骤开始,其行动与贪婪策略下执行的行动不同。为了了解另一个策略的目标策略,并使用一种称为重要性抽样的技术,这种技术通常用于估计在给定从不同分布生成的样本下的预期值。状态-动作对的加权重要性计算如下:
这里,π(ak | sk)是在目标策略下在状态sk中采取动作ak的概率;b(ak | sk)是在行为策略下的概率;权重wt是从步骤t到剧集结束时那两个概率的比率的乘积。权重wt应用于步骤t的回报。
如何做...
让我们使用以下步骤来搜索使用离策略蒙特卡洛控制的最优 21 点策略:
- 导入必要的模块并创建一个 21 点实例:
>>> import torch
>>> import gym
>>> env = gym.make('Blackjack-v0')
- 我们首先定义行为策略,它在我们的情况下随机选择一个动作:
>>> def gen_random_policy(n_action):
... probs = torch.ones(n_action) / n_action
... def policy_function(state):
... return probs
... return policy_function
>>> random_policy = gen_random_policy(env.action_space.n)
行为策略可以是任何东西,只要它以非零概率选择所有状态中的所有动作。
- 接下来,让我们开发一个函数,运行一个剧集,并在行为策略下执行动作:
>>> def run_episode(env, behavior_policy):
... """
... Run a episode given a behavior policy
... @param env: OpenAI Gym environment
... @param behavior_policy: behavior policy
... @return: resulting states, actions and rewards for the entire episode
... """
... state = env.reset()
... rewards = [] ... actions = []
... states = []
... is_done = False
... while not is_done:
... probs = behavior_policy(state)
... action = torch.multinomial(probs, 1).item()
... actions.append(action)
... states.append(state)
... state, reward, is_done, info = env.step(action)
... rewards.append(reward)
... if is_done:
... break
... return states, actions, rewards
这记录了剧集中所有步骤的状态、动作和奖励,这将作为目标策略的学习数据使用。
- 现在,我们将开发离策略蒙特卡洛控制算法:
>>> from collections import defaultdict
>>> def mc_control_off_policy(env, gamma, n_episode, behavior_policy):
... """
... Obtain the optimal policy with off-policy MC control method
... @param env: OpenAI Gym environment
... @param gamma: discount factor
... @param n_episode: number of episodes
... @param behavior_policy: behavior policy
... @return: the optimal Q-function, and the optimal policy
... """
... n_action = env.action_space.n
... G_sum = defaultdict(float)
... N = defaultdict(int)
... Q = defaultdict(lambda: torch.empty(n_action))
... for episode in range(n_episode):
... W = {}
... w = 1
... states_t, actions_t, rewards_t =
run_episode(env, behavior_policy)
... return_t = 0 ... G = {}
... for state_t, action_t, reward_t in zip(states_t[::-1],
actions_t[::-1], rewards_t[::-1]):
... return_t = gamma * return_t + reward_t
... G[(state_t, action_t)] = return_t
... if action_t != torch.argmax(Q[state_t]).item():
... break
... w *= 1./ behavior_policy(state_t)[action_t]
... for state_action, return_t in G.items():
... state, action = state_action
... if state[0] <= 21:
... G_sum[state_action] +=
return_t * W[state_action]
... N[state_action] += 1
... Q[state][action] =
G_sum[state_action] / N[state_action]
... policy = {}
... for state, actions in Q.items():
... policy[state] = torch.argmax(actions).item()
... return Q, policy
- 我们将折现率设为 1,并将使用 500,000 个剧集:
>>> gamma = 1
>>> n_episode = 500000
- 使用
random_policy行为策略执行离策略蒙特卡洛控制以获取最优 Q 函数和策略:
>>> optimal_Q, optimal_policy = mc_control_off_policy(env, gamma, n_episode, random_policy)
它是如何工作的...
在这个示例中,我们使用离策略蒙特卡洛解决 21 点游戏。
在步骤 4中,离策略蒙特卡洛控制算法执行以下任务:
-
它用任意小的值初始化 Q 函数。
-
它运行
n_episode个剧集。 -
对于每个剧集,它执行行为策略以生成状态、动作和奖励;它使用基于共同步骤的首次访问蒙特卡洛预测对目标策略进行策略评估;并根据加权回报更新 Q 函数。
-
最后,最优 Q 函数完成,并且通过在最优 Q 函数中为每个状态选择最佳动作来获取最优策略。
它通过观察另一个代理并重复使用从另一个策略生成的经验来学习目标策略。目标策略以贪婪方式优化,而行为策略则继续探索不同的选项。它将行为策略的回报与目标策略中其概率的重要性比率平均起来。你可能会想知道为什么在重要性比率 wt 的计算中,π (ak | sk) 总是等于 1。回想一下,我们只考虑在行为策略和目标策略下采取的共同步骤,并且目标策略总是贪婪的。因此,π (a | s) = 1 总是成立。
还有更多内容…
我们可以实际上以增量方式实现蒙特卡洛方法。在一个 episode 中,我们可以即时计算 Q 函数,而不是为每个首次出现的状态-动作对存储回报和重要性比率。在非增量方式中,Q 函数在 n 个 episode 中的所有存储回报最终计算出来:
而在增量方法中,Q 函数在每个 episode 的每个步骤中更新如下:
增量等价版本更高效,因为它减少了内存消耗并且更具可扩展性。让我们继续实施它:
>>> def mc_control_off_policy_incremental(env, gamma, n_episode, behavior_policy):
... n_action = env.action_space.n
... N = defaultdict(int)
... Q = defaultdict(lambda: torch.empty(n_action))
... for episode in range(n_episode):
... W = 1.
... states_t, actions_t, rewards_t =
run_episode(env, behavior_policy)
... return_t = 0.
... for state_t, action_t, reward_t in
zip(states_t[::-1], actions_t[::-1], rewards_t[::-1]):
... return_t = gamma * return_t + reward_t
... N[(state_t, action_t)] += 1
... Q[state_t][action_t] += (W / N[(state_t, action_t)]) * (return_t - Q[state_t][action_t])
... if action_t != torch.argmax(Q[state_t]).item():
... break
... W *= 1./ behavior_policy(state_t)[action_t]
... policy = {}
... for state, actions in Q.items():
... policy[state] = torch.argmax(actions).item()
... return Q, policy
我们可以调用此增量版本来获得最优策略:
>>> optimal_Q, optimal_policy = mc_control_off_policy_incremental(env, gamma, n_episode, random_policy)
另请参阅
欲了解重要性抽样的详细解释,请参考以下完美资源:
statweb.stanford.edu/~owen/mc/Ch-var-is.pdf
使用加权重要性抽样开发蒙特卡洛控制
在上一个示例中,我们简单地使用了行为策略的回报与目标策略中其概率的重要性比率的平均值。这种技术在形式上称为普通重要性抽样。众所周知,它具有很高的方差,因此我们通常更喜欢重要性抽样的加权版本,在本示例中我们将讨论这一点。
加权重要性抽样与普通重要性抽样的不同之处在于它在平均回报方面采用了加权平均值:
它通常与普通版本相比具有更低的方差。如果您对二十一点游戏尝试过普通重要性抽样,您会发现每次实验结果都不同。
如何做…
让我们通过以下步骤使用加权重要性抽样来解决二十一点游戏的离策略蒙特卡洛控制问题:
- 导入必要的模块并创建一个二十一点实例:
>>> import torch
>>> import gym
>>> env = gym.make('Blackjack-v0')
- 我们首先定义行为策略,该策略在我们的情况下随机选择一个动作:
>>> random_policy = gen_random_policy(env.action_space.n)
-
接下来,我们重复使用
run_episode函数,该函数在行为策略下运行一个 episode 并采取行动。 -
现在,我们使用加权重要性抽样来开发离策略蒙特卡洛控制算法:
>>> from collections import defaultdict
>>> def mc_control_off_policy_weighted(env, gamma, n_episode, behavior_policy):
... """
... Obtain the optimal policy with off-policy MC control method with weighted importance sampling
... @param env: OpenAI Gym environment
... @param gamma: discount factor
... @param n_episode: number of episodes
... @param behavior_policy: behavior policy
... @return: the optimal Q-function, and the optimal policy
... """
... n_action = env.action_space.n
... N = defaultdict(float)
... Q = defaultdict(lambda: torch.empty(n_action))
... for episode in range(n_episode):
... W = 1.
... states_t, actions_t, rewards_t =
run_episode(env, behavior_policy)
... return_t = 0.
... for state_t, action_t, reward_t in zip(states_t[::-1],
actions_t[::-1], rewards_t[::-1]):
... return_t = gamma * return_t + reward_t
... N[(state_t, action_t)] += W
... Q[state_t][action_t] += (W / N[(state_t, action_t)])
* (return_t - Q[state_t][action_t])
... if action_t != torch.argmax(Q[state_t]).item():
... break
... W *= 1./ behavior_policy(state_t)[action_t]
... policy = {}
... for state, actions in Q.items():
... policy[state] = torch.argmax(actions).item()
... return Q, policy
注意这是离策略蒙特卡洛控制的增量版本。
- 我们将折扣率设定为 1,并将使用 500,000 个情节:
>>> gamma = 1
>>> n_episode = 500000
- 使用
random_policy行为策略执行离策略蒙特卡洛控制,以获取最优的 Q 函数和策略:
>>> optimal_Q, optimal_policy = mc_control_off_policy_weighted(env, gamma, n_episode, random_policy)
工作原理如下...
我们在这个示例中使用了带有加权重要性抽样的离策略蒙特卡洛控制来解决二十一点问题。这与普通重要性抽样非常相似,但不是通过比率来缩放回报和平均结果,而是使用加权平均值来缩放回报。实际上,加权重要性抽样的方差比普通重要性抽样低得多,因此被强烈推荐使用。
还有更多...
最后,为什么不模拟一些情节,看看在生成的最优策略下获胜和失败的机会如何?
我们重复使用了我们在执行策略蒙特卡洛控制食谱中开发的simulate_episode函数,并模拟了 100,000 个情节:
>>> n_episode = 100000
>>> n_win_optimal = 0
>>> n_lose_optimal = 0
>>> for _ in range(n_episode):
... reward = simulate_episode(env, optimal_policy)
... if reward == 1:
... n_win_optimal += 1
... elif reward == -1:
... n_lose_optimal += 1
然后,我们打印出我们得到的结果:
>>> print('Winning probability under the optimal policy: {}'.format(n_win_optimal/n_episode))
Winning probability under the optimal policy: 0.43072
>>> print('Losing probability under the optimal policy: {}'.format(n_lose_optimal/n_episode))
Losing probability under the optimal policy: 0.47756
另请参阅
有关加权重要性抽样优于普通重要性抽样的证明,请随时查看以下内容:
-
Hesterberg, T. C., 进展中的重要性抽样, 统计系, 斯坦福大学, 1988
-
Casella, G., Robert, C. P., Post-processing accept-reject samples: recycling and rescaling. 计算与图形统计杂志, 7(2):139–157, 1988
-
Precup, D., Sutton, R. S., Singh, S., 离策略策略评估的资格痕迹。在第 17 届国际机器学习大会上的论文集, pp. 759–766, 2000
第四章:时间差分和 Q-learning
在上一章中,我们通过蒙特卡洛方法解决了马尔可夫决策过程(MDP),这是一种无模型方法,不需要环境的先验知识。然而,在 MC 学习中,价值函数和 Q 函数通常在情节结束之前更新。这可能存在问题,因为有些过程非常长,甚至无法正常结束。在本章中,我们将采用时间差分(TD)方法来解决这个问题。在 TD 方法中,我们在每个时间步更新动作值,显著提高了学习效率。
本章将从设置 Cliff Walking 和 Windy Gridworld 环境的游乐场开始,这些将作为本章 TD 控制方法的主要讨论点。通过我们的逐步指南,读者将获得 Q-learning 用于离策略控制和 SARSA 用于在策略控制的实际经验。我们还将处理一个有趣的项目——出租车问题,并展示如何分别使用 Q-learning 和 SARSA 算法来解决它。最后,我们将额外介绍双 Q-learning 算法。
我们将包括以下的步骤:
-
设置 Cliff Walking 环境的游乐场
-
开发 Q-learning 算法
-
设置 Windy Gridworld 环境的游乐场
-
开发 SARSA 算法
-
用 Q-learning 解决出租车问题
-
用 SARSA 解决出租车问题
-
开发 Double Q-learning 算法
设置 Cliff Walking 环境的游乐场
在第一个步骤中,我们将开始熟悉 Cliff Walking 环境,我们将在后续步骤中使用 TD 方法来解决它。
Cliff Walking 是一个典型的 gym 环境,具有长时间的不确定结束的情节。这是一个 4 * 12 的网格问题。一个代理在每一步可以向上、向右、向下和向左移动。左下角的方块是代理的起点,右下角是获胜的点,如果到达则会结束一个情节。最后一行剩余的方块是悬崖,代理踩上其中任何一个后会被重置到起始位置,但情节仍继续。每一步代理走的时候会产生一个 -1 的奖励,除非踩到悬崖,那么会产生 -100 的奖励。
准备工作
要运行 Cliff Walking 环境,首先在github.com/openai/gym/wiki/Table-of-environments表格中搜索它的名称。我们得到 CliffWalking-v0,并且知道观察空间由整数表示,范围从 0(左上角方块)到 47(右下角目标方块),有四个可能的动作(上 = 0,右 = 1,下 = 2,左 = 3)。
怎么做...
让我们通过以下步骤模拟 Cliff Walking 环境:
- 我们导入 Gym 库,并创建一个 Cliff Walking 环境的实例:
>>> import gym
>>> env = gym.make("CliffWalking-v0")
>>> n_state = env.observation_space.n
>>> print(n_state)
48
>>> n_action = env.action_space.n
>>> print(n_action)
4
- 然后,我们重置环境:
>>> env.reset()
0
代理从状态 36 开始,作为左下角的瓷砖。
- 然后,我们渲染环境:
>>> env.render()
- 现在,无论是否可行走,让我们进行一个向下的移动:
>>> new_state, reward, is_done, info = env.step(2)
>>> env.render()
o o o o o o o o o o o o
o o o o o o o o o o o o
o o o o o o o o o o o o
x C C C C C C C C C C T
代理保持不动。现在,打印出我们刚刚获得的内容:
>>> print(new_state)
36
>>> print(reward)
-1
再次,每个移动都会导致 -1 的奖励:
>>> print(is_done)
False
该 episode 还没有完成,因为代理人还没有达到目标:
>>> print(info)
{'prob': 1.0}
这意味着移动是确定性的。
现在,让我们执行一个向上的移动,因为它是可行走的:
>>> new_state, reward, is_done, info = env.step(0)
>>> env.render()
o o o o o o o o o o o o
o o o o o o o o o o o o
x o o o o o o o o o o o
o C C C C C C C C C C T
打印出我们刚刚获得的内容:
>>> print(new_state)
24
代理人向上移动:
>>> print(reward)
-1
这导致 -1 的奖励。
- 现在让我们尝试向右和向下移动:
>>> new_state, reward, is_done, info = env.step(1)
>>> new_state, reward, is_done, info = env.step(2)
>>> env.render()
o o o o o o o o o o o o
o o o o o o o o o o o o
o o o o o o o o o o o o
x C C C C C C C C C C T
代理人踩到了悬崖,因此被重置到起点并获得了 -100 的奖励:
>>> print(new_state)
36
>>> print(reward)
-100
>>> print(is_done)
False
- 最后,让我们尝试以最短路径达到目标:
>>> new_state, reward, is_done, info = env.step(0)
>>> for _ in range(11):
... env.step(1)
>>> new_state, reward, is_done, info = env.step(2)
>>> env.render()
o o o o o o o o o o o o
o o o o o o o o o o o o
o o o o o o o o o o o o
o C C C C C C C C C C x
>>> print(new_state)
47
>>> print(reward)
-1
>>> print(is_done)
True
工作原理...
在步骤 1中,我们导入 Gym 库并创建 Cliff Walking 环境的实例。然后,在步骤 2中重置环境。
在步骤 3中,我们渲染环境,你会看到一个 4 * 12 的矩阵如下,表示一个网格,其中包括起始瓷砖(x)代表代理人所站的位置,目标瓷砖(T),10 个悬崖瓷砖(C),以及常规瓷砖(o):
在步骤 4、5和6中,我们进行了各种移动,并看到了这些移动的各种结果和收到的奖励。
如你所想象的,Cliff Walking 的一个场景可能会非常长,甚至是无限的,因为一旦踩到悬崖就会重置游戏。尽早达到目标是更好的,因为每走一步都会导致奖励为 -1 或者 -100。在下一个实例中,我们将通过时间差分方法解决 Cliff Walking 问题。
开发 Q-learning 算法
时间差分(TD)学习也是一种无模型学习算法,就像 MC 学习一样。你会记得,在 MC 学习中,Q 函数在整个 episode 结束时更新(无论是首次访问还是每次访问模式)。TD 学习的主要优势在于它在 episode 中的每一步都更新 Q 函数。
在这个示例中,我们将介绍一种名为Q-learning的流行时间差分方法。Q-learning 是一种离策略学习算法。它根据以下方程更新 Q 函数:
这里,s' 是采取动作 a 后的结果状态 s;r 是相关的奖励;α 是学习率;γ 是折扣因子。此外,![] 意味着行为策略是贪婪的,选择状态 s' 中最高的 Q 值来生成学习数据。在 Q-learning 中,动作是根据 epsilon-greedy 策略执行的。
如何实现...
我们通过 Q-learning 解决 Cliff Walking 环境如下:
- 导入 PyTorch 和 Gym 库,并创建 Cliff Walking 环境的实例:
>>> import torch
>>> import gym >>> env = gym.make("CliffWalking-v0")
>>> from collections import defaultdict
- 让我们从定义 epsilon-greedy 策略开始:
>>> def gen_epsilon_greedy_policy(n_action, epsilon):
... def policy_function(state, Q):
... probs = torch.ones(n_action) * epsilon / n_action
... best_action = torch.argmax(Q[state]).item()
... probs[best_action] += 1.0 - epsilon
... action = torch.multinomial(probs, 1).item()
... return action
... return policy_function
- 现在定义执行 Q-learning 的函数:
>>> def q_learning(env, gamma, n_episode, alpha):
... """
... Obtain the optimal policy with off-policy Q-learning method
... @param env: OpenAI Gym environment
... @param gamma: discount factor
... @param n_episode: number of episodes
... @return: the optimal Q-function, and the optimal policy
... """
... n_action = env.action_space.n
... Q = defaultdict(lambda: torch.zeros(n_action))
... for episode in range(n_episode):
... state = env.reset()
... is_done = False
... while not is_done:
... action = epsilon_greedy_policy(state, Q)
... next_state, reward, is_done, info =
env.step(action)
... td_delta = reward +
gamma * torch.max(Q[next_state])
- Q[state][action]
... Q[state][action] += alpha * td_delta
... if is_done:
... break
... state = next_state
... policy = {}
... for state, actions in Q.items():
... policy[state] = torch.argmax(actions).item()
... return Q, policy
- 我们将折扣率设为
1,学习率设为0.4,ε设为0.1;然后模拟 500 个回合:
>>> gamma = 1
>>> n_episode = 500
>>> alpha = 0.4
>>> epsilon = 0.1
- 接下来,我们创建ε-贪心策略的一个实例:
>>> epsilon_greedy_policy = gen_epsilon_greedy_policy(env.action_space.n, epsilon)
- 最后,我们使用之前定义的输入参数执行 Q 学习,并打印出最优策略:
>>> optimal_Q, optimal_policy = q_learning(env, gamma, n_episode, alpha) >>> print('The optimal policy:\n', optimal_policy)
The optimal policy:
{36: 0, 24: 1, 25: 1, 13: 1, 12: 2, 0: 3, 1: 1, 14: 2, 2: 1, 26: 1, 15: 1, 27: 1, 28: 1, 16: 2, 4: 2, 3: 1, 29: 1, 17: 1, 5: 0, 30: 1, 18: 1, 6: 1, 19: 1, 7: 1, 31: 1, 32: 1, 20: 2, 8: 1, 33: 1, 21: 1, 9: 1, 34: 1, 22: 2, 10: 2, 23: 2, 11: 2, 35: 2, 47: 3}
工作原理...
在步骤 2中,ε-贪心策略接受一个参数ε,其值从 0 到 1,|A|是可能动作的数量。每个动作的概率为ε/|A|,并且以 1-ε+ε/|A|的概率选择具有最高状态-动作值的动作。
在步骤 3中,我们在以下任务中执行 Q 学习:
-
我们用全零初始化 Q 表。
-
在每个回合中,我们让代理根据ε-贪心策略选择动作。然后,我们针对每个步骤更新 Q 函数。
-
我们运行
n_episode个回合。 -
我们基于最优的 Q 函数获得了最优策略。
在步骤 6中,再次,up = 0,right = 1,down = 2,left = 3;因此,根据最优策略,代理从状态 36 开始,然后向上移动到状态 24,然后向右一直移动到状态 35,最后向下到达目标:
在 Q 学习中可以看到,它通过学习由另一个策略生成的经验来优化 Q 函数。这与离策略 MC 控制方法非常相似。不同之处在于,它实时更新 Q 函数,而不是在整个回合结束后。这被认为是有利的,特别是对于回合时间较长的环境,延迟学习直到回合结束是低效的。在 Q 学习(或任何其他 TD 方法)的每一个步骤中,我们都会获取更多关于环境的信息,并立即使用此信息来更新值。在我们的案例中,通过仅运行 500 个学习回合,我们获得了最优策略。
还有更多...
实际上,在大约 50 个回合后获得了最优策略。我们可以绘制每个回合的长度随时间变化的图表来验证这一点。还可以选择随时间获得的每个回合的总奖励。
- 我们定义两个列表分别存储每个回合的长度和总奖励:
>>> length_episode = [0] * n_episode
>>> total_reward_episode = [0] * n_episode
- 我们在学习过程中跟踪每个回合的长度和总奖励。以下是更新版本的
q_learning:
>>> def q_learning(env, gamma, n_episode, alpha):
... n_action = env.action_space.n
... Q = defaultdict(lambda: torch.zeros(n_action))
... for episode in range(n_episode):
... state = env.reset()
... is_done = False
... while not is_done:
... action = epsilon_greedy_policy(state, Q)
... next_state, reward, is_done, info =
env.step(action)
... td_delta = reward +
gamma * torch.max(Q[next_state])
- Q[state][action]
... Q[state][action] += alpha * td_delta
... length_episode[episode] += 1
... total_reward_episode[episode] += reward
... if is_done:
... break
... state = next_state
... policy = {}
... for state, actions in Q.items():
... policy[state] = torch.argmax(actions).item()
... return Q, policy
- 现在,展示随时间变化的回合长度的图表:
>>> import matplotlib.pyplot as plt
>>> plt.plot(length_episode)
>>> plt.title('Episode length over time')
>>> plt.xlabel('Episode')
>>> plt.ylabel('Length')
>>> plt.show()
这将导致以下绘图:
- 展示随时间变化的回合奖励的图表:
>>> plt.plot(total_reward_episode)
>>> plt.title('Episode reward over time')
>>> plt.xlabel('Episode')
>>> plt.ylabel('Total reward')
>>> plt.show()
这将导致以下绘图:
再次,如果减小ε的值,您将看到较小的波动,这是ε-贪心策略中随机探索的效果。
设置多风格格子世界环境的游乐场
在上一个示例中,我们解决了一个相对简单的环境,在那里我们可以很容易地获取最优策略。在这个示例中,让我们模拟一个更复杂的网格环境,风格网格世界,在这个环境中,外部力会将代理从某些瓦片移开。这将为我们在下一个示例中使用 TD 方法搜索最优策略做准备。
风格网格世界是一个 7 * 10 的棋盘问题,显示如下:
代理在每一步可以向上、向右、向下和向左移动。第 30 块瓦片是代理的起始点,第 37 块瓦片是获胜点,如果达到则一个 episode 结束。每一步代理走动会产生-1 的奖励。
在这个环境中的复杂性在于,第 4 至 9 列有额外的风力。从这些列的瓦片移动时,代理会额外受到向上的推力。第 7 和第 8 列的风力为 1,第 4、5、6 和 9 列的风力为 2。例如,如果代理试图从状态 43 向右移动,它们将会落在状态 34;如果代理试图从状态 48 向左移动,它们将会落在状态 37;如果代理试图从状态 67 向上移动,它们将会落在状态 37,因为代理会受到额外的 2 单位向上的力;如果代理试图从状态 27 向下移动,它们将会落在状态 17,因为额外的 2 单位向上力抵消了 1 单位向下力。
目前,风格网格世界还没有包含在 Gym 环境中。我们将通过参考 Cliff Walking 环境来实现它:github.com/openai/gym/blob/master/gym/envs/toy_text/cliffwalking.py。
如何做…
让我们开发风格网格世界环境:
- 从 Gym 中导入必要的模块,NumPy 和
discrete类:
>>> import numpy as np
>>> import sys
>>> from gym.envs.toy_text import discrete
- 定义四个动作:
>>> UP = 0
>>> RIGHT = 1
>>> DOWN = 2
>>> LEFT = 3
- 让我们从在
WindyGridworldEnv类中定义__init__方法开始:
>>> class WindyGridworldEnv(discrete.DiscreteEnv):
... def __init__(self):
... self.shape = (7, 10)
... nS = self.shape[0] * self.shape[1]
... nA = 4
... # Wind locations
... winds = np.zeros(self.shape)
... winds[:,[3,4,5,8]] = 1
... winds[:,[6,7]] = 2
... self.goal = (3, 7)
... # Calculate transition probabilities and rewards
... P = {}
... for s in range(nS):
... position = np.unravel_index(s, self.shape)
... P[s] = {a: [] for a in range(nA)}
... P[s][UP] = self._calculate_transition_prob(
position, [-1, 0], winds)
... P[s][RIGHT] = self._calculate_transition_prob(
position, [0, 1], winds)
... P[s][DOWN] = self._calculate_transition_prob(
position, [1, 0], winds)
... P[s][LEFT] = self._calculate_transition_prob(
position, [0, -1], winds)
... # Calculate initial state distribution
... # We always start in state (3, 0)
... isd = np.zeros(nS)
... isd[np.ravel_multi_index((3,0), self.shape)] = 1.0
... super(WindyGridworldEnv, self).__init__(nS, nA, P, isd)
这定义了观察空间、风区域和风力、转移和奖励矩阵,以及初始状态。
- 接下来,我们定义
_calculate_transition_prob方法来确定动作的结果,包括概率(为 1),新状态,奖励(始终为-1),以及是否完成:
... def _calculate_transition_prob(self, current,
delta, winds):
... """
... Determine the outcome for an action. Transition
Prob is always 1.0.
... @param current: (row, col), current position
on the grid
... @param delta: Change in position for transition
... @param winds: Wind effect
... @return: (1.0, new_state, reward, is_done)
... """
... new_position = np.array(current) + np.array(delta)
+ np.array([-1, 0]) * winds[tuple(current)]
... new_position = self._limit_coordinates( new_position).astype(int)
... new_state = np.ravel_multi_index( tuple(new_position), self.shape)
... is_done = tuple(new_position) == self.goal
... return [(1.0, new_state, -1.0, is_done)]
这计算基于当前状态、移动和风效应的状态,并确保新位置在网格内。最后,它检查代理是否达到目标状态。
- 接下来,我们定义
_limit_coordinates方法,用于防止代理掉出网格世界:
... def _limit_coordinates(self, coord):
... coord[0] = min(coord[0], self.shape[0] - 1)
... coord[0] = max(coord[0], 0)
... coord[1] = min(coord[1], self.shape[1] - 1)
... coord[1] = max(coord[1], 0)
... return coord
- 最后,我们添加
render方法以显示代理和网格环境:
... def render(self):
... outfile = sys.stdout
... for s in range(self.nS):
... position = np.unravel_index(s, self.shape)
... if self.s == s:
... output = " x "
... elif position == self.goal:
... output = " T "
... else:
... output = " o "
... if position[1] == 0:
... output = output.lstrip()
... if position[1] == self.shape[1] - 1:
... output = output.rstrip()
... output += "\n"
... outfile.write(output)
... outfile.write("\n")
X 表示代理当前的位置,T 是目标瓦片,其余瓦片表示为 o。
现在,让我们按以下步骤模拟风格网格世界环境:
- 创建一个风格网格世界环境的实例:
>>> env = WindyGridworldEnv()
- 重置并渲染环境:
>>> env.reset()
>>> env.render()
o o o o o o o o o o
o o o o o o o o o o
o o o o o o o o o o
x o o o o o o T o o
o o o o o o o o o o
o o o o o o o o o o
o o o o o o o o o o
代理器从状态 30 开始。
- 向右移动一步:
>>> print(env.step(1))
>>> env.render()
(31, -1.0, False, {'prob': 1.0})
o o o o o o o o o o
o o o o o o o o o o
o o o o o o o o o o
o x o o o o o T o o
o o o o o o o o o o
o o o o o o o o o o
o o o o o o o o o o
代理器降落在状态 31,奖励为 -1。
- 右移两步:
>>> print(env.step(1))
>>> print(env.step(1))
>>> env.render()
(32, -1.0, False, {'prob': 1.0})
(33, -1.0, False, {'prob': 1.0})
o o o o o o o o o o
o o o o o o o o o o
o o o o o o o o o o
o o o x o o o T o o
o o o o o o o o o o
o o o o o o o o o o
o o o o o o o o o o
- 现在,再向右移动一步:
>>> print(env.step(1))
>>> env.render()
(24, -1.0, False, {'prob': 1.0})
o o o o o o o o o o
o o o o o o o o o o
o o o o x o o o o o
o o o o o o o T o o
o o o o o o o o o o
o o o o o o o o o o
o o o o o o o o o o
风向上 1 单位,代理器降落在状态 24。
随意尝试环境,直到达到目标。
工作原理……
我们刚刚开发了一个类似于 Cliff Walking 的网格环境。Windy Gridworld 和 Cliff Walking 的区别在于额外的向上推力。每个动作在 Windy Gridworld 剧集中将导致奖励 -1。因此,尽快达到目标更为有效。在下一个步骤中,我们将使用另一种 TD 控制方法解决 Windy Gridworld 问题。
开发 SARSA 算法
你会记得 Q-learning 是一种离策略 TD 学习算法。在本配方中,我们将使用一种在线策略 TD 学习算法解决 MDP,称为 状态-行动-奖励-状态-行动(SARSA)。
类似于 Q-learning,SARSA 关注状态-动作值。它根据以下方程更新 Q 函数:
在这里,s' 是在状态 s 中采取动作 a 后的结果状态;r 是相关的奖励;α 是学习率;γ 是折扣因子。你会记得,在 Q-learning 中,一种行为贪婪策略 ![] 用于更新 Q 值。在 SARSA 中,我们简单地通过遵循 epsilon-greedy 策略来选择下一个动作 a' 来更新 Q 值。然后动作 a' 在下一步中被执行。因此,SARSA 是一个在线策略算法。
如何实现……
我们执行 SARSA 解决 Windy Gridworld 环境,步骤如下:
- 导入 PyTorch 和
WindyGridworldEnvmodule(假设它在名为windy_gridworld.py的文件中),并创建 Windy Gridworld 环境的实例:
>>> import torch
>>> from windy_gridworld import WindyGridworldEnv >>> env = WindyGridworldEnv()
- 让我们从定义 epsilon-greedy 行为策略开始:
>>> def gen_epsilon_greedy_policy(n_action, epsilon):
... def policy_function(state, Q):
... probs = torch.ones(n_action) * epsilon / n_action
... best_action = torch.argmax(Q[state]).item()
... probs[best_action] += 1.0 - epsilon
... action = torch.multinomial(probs, 1).item()
... return action
... return policy_function
- 我们指定了要运行的剧集数,并初始化了用于跟踪每一剧集的长度和总奖励的两个变量:
>>> n_episode = 500
>>> length_episode = [0] * n_episode
>>> total_reward_episode = [0] * n_episode
- 现在,我们定义执行 SARSA 的函数:
>>> from collections import defaultdict
>>> def sarsa(env, gamma, n_episode, alpha):
... """
... Obtain the optimal policy with on-policy SARSA algorithm
... @param env: OpenAI Gym environment
... @param gamma: discount factor
... @param n_episode: number of episodes
... @return: the optimal Q-function, and the optimal policy
... """
... n_action = env.action_space.n
... Q = defaultdict(lambda: torch.zeros(n_action))
... for episode in range(n_episode):
... state = env.reset()
... is_done = False
... action = epsilon_greedy_policy(state, Q)
... while not is_done:
... next_state, reward, is_done, info
= env.step(action)
... next_action = epsilon_greedy_policy(next_state, Q)
... td_delta = reward +
gamma * Q[next_state][next_action]
- Q[state][action]
... Q[state][action] += alpha * td_delta
... length_episode[episode] += 1
... total_reward_episode[episode] += reward
... if is_done:
... break
... state = next_state
... action = next_action
... policy = {}
... for state, actions in Q.items():
... policy[state] = torch.argmax(actions).item()
... return Q, policy
- 我们将折扣率指定为 1,学习率为 0.4,epsilon 为 0.1:
>>> gamma = 1
>>> alpha = 0.4
>>> epsilon = 0.1
- 接下来,我们创建 epsilon-greedy 策略的实例:
>>> epsilon_greedy_policy = gen_epsilon_greedy_policy(env.action_space.n, epsilon)
- 最后,我们使用之前步骤中定义的输入参数执行 SARSA,并打印出最优策略:
>>> optimal_Q, optimal_policy = sarsa(env, gamma, n_episode, alpha) >>> print('The optimal policy:\n', optimal_policy)
The optimal policy:
{30: 2, 31: 1, 32: 1, 40: 1, 50: 2, 60: 1, 61: 1, 51: 1, 41: 1, 42: 1, 20: 1, 21: 1, 62: 1, 63: 2, 52: 1, 53: 1, 43: 1, 22: 1, 11: 1, 10: 1, 0: 1, 33: 1, 23: 1, 12: 1, 13: 1, 2: 1, 1: 1, 3: 1, 24: 1, 4: 1, 5: 1, 6: 1, 14: 1, 7: 1, 8: 1, 9: 2, 19: 2, 18: 2, 29: 2, 28: 1, 17: 2, 39: 2, 38: 1, 27: 0, 49: 3, 48: 3, 37: 3, 34: 1, 59: 2, 58: 3, 47: 2, 26: 1, 44: 1, 15: 1, 69: 3, 68: 1, 57: 2, 36: 1, 25: 1, 54: 2, 16: 1, 35: 1, 45: 1}
工作原理……
在 步骤 4 中,SARSA 函数执行以下任务:
-
它使用全零初始化 Q 表。
-
在每一剧集中,它让代理器遵循 epsilon-greedy 策略来选择采取的行动。对于每一步,它根据方程 ![] 更新 Q 函数,其中
a'是根据 epsilon-greedy 策略选择的。然后,在新状态s'中采取新的动作a'。 -
我们运行
n_episode个剧集。 -
我们基于最优 Q 函数获取最优策略。
正如在 SARSA 方法中所见,它通过采取在相同策略下选择的动作来优化 Q 函数,即 epsilon 贪婪策略。这与 on-policy MC 控制方法非常相似。不同之处在于,它通过个别步骤中的小导数来更新 Q 函数,而不是在整个集结束后。在集合长度较长的环境中,此方法被认为是优势,因为将学习延迟到集的结束是低效的。在 SARSA 的每一个单步中,我们获得更多关于环境的信息,并利用这些信息立即更新值。在我们的案例中,仅通过运行 500 个学习集,我们获得了最优策略。
还有更多...
实际上,在大约 200 集后获得了最优策略。我们可以绘制每一集的长度和总奖励随时间变化的图表来验证这一点:
- 显示随时间变化的剧集长度图:
>>> import matplotlib.pyplot as plt
>>> plt.plot(length_episode)
>>> plt.title('Episode length over time')
>>> plt.xlabel('Episode')
>>> plt.ylabel('Length')
>>> plt.show()
这将导致以下图表:
您可以看到,剧集长度在 200 集后开始饱和。请注意,这些小波动是由 epsilon 贪婪策略中的随机探索造成的。
- 显示随时间变化的剧集奖励图:
>>> plt.plot(total_reward_episode)
>>> plt.title('Episode reward over time')
>>> plt.xlabel('Episode')
>>> plt.ylabel('Total reward')
>>> plt.show()
这将导致以下图表:
如果你减小 epsilon 的值,你将看到更小的波动,这是在 epsilon 贪婪策略中随机探索的影响。
在接下来的两个示例中,我们将使用我们刚学到的两种 TD 方法来解决一个更复杂的环境,该环境具有更多的可能状态和动作。让我们从 Q 学习开始。
使用 Q 学习解决出租车问题
出租车问题 (gym.openai.com/envs/Taxi-v2/) 是另一个流行的网格世界问题。在一个 5 * 5 的网格中,代理作为出租车司机,在一个位置接载乘客,然后将乘客送达目的地。看下面的例子:
彩色方块有以下含义:
-
黄色:出租车的起始位置。起始位置在每一集中是随机的。
-
蓝色:乘客的位置。在每一集中也是随机选择的。
-
紫色:乘客的目的地。同样,在每一集中随机选择。
-
绿色:带有乘客的出租车位置。
R、Y、B 和 G 这四个字母指示唯一允许接载和送达乘客的方块。其中一个是目的地,一个是乘客的位置。
出租车可以采取以下六个确定性动作:
-
0:向南移动
-
1:向北移动
-
2:向东移动
-
3:向西移动
-
4:接载乘客
-
5:送达乘客
两个方块之间有一根柱子 |,防止出租车从一个方块移动到另一个方块。
每一步的奖励通常是 -1,以下是例外情况:
-
+20:乘客被送达目的地。一个回合将结束。
-
-10:尝试非法接乘或下车(不在 R、Y、B 或 G 中)。
还有一件需要注意的事情是,观察空间远远大于 25(5*5),因为我们还应考虑乘客和目的地的位置,以及出租车是否为空或已满。因此,观察空间应为 25 * 5(乘客或已在出租车的 4 个可能位置) * 4(目的地)= 500 维度。
准备就绪
要运行出租车环境,让我们首先在环境表中搜索其名称,github.com/openai/gym/wiki/Table-of-environments。我们得到 Taxi-v2,并且知道观察空间由一个从 0 到 499 的整数表示,并且有四种可能的动作(向上 = 0,向右 = 1,向下 = 2,向左 = 3)。
如何做到…
让我们从以下步骤开始模拟出租车环境:
- 我们导入 Gym 库并创建出租车环境的实例:
>>> import gym
>>> env = gym.make('Taxi-v2')
>>> n_state = env.observation_space.n
>>> print(n_state)
500
>>> n_action = env.action_space.n
>>> print(n_action)
6
- 然后,我们重置环境:
>>> env.reset()
262
- 然后,我们渲染环境:
>>> env.render()
您将看到一个类似的 5 * 5 矩阵如下:
乘客位于 R 位置,目的地位于 Y。由于初始状态是随机生成的,您将看到不同的结果。
- 现在让我们向西移动三个瓷砖,向北移动两个瓷砖去接乘客(您可以根据初始状态进行调整),然后执行接乘客。接着,我们再次渲染环境:
>>> print(env.step(3))
(242, -1, False, {'prob': 1.0})
>>> print(env.step(3))
(222, -1, False, {'prob': 1.0})
>>> print(env.step(3))
(202, -1, False, {'prob': 1.0})
>>> print(env.step(1))
(102, -1, False, {'prob': 1.0})
>>> print(env.step(1))
(2, -1, False, {'prob': 1.0})
>>> print(env.step(4))
(18, -1, False, {'prob': 1.0})
Render the environment:
>>> env.render()
- 您将看到更新的最新矩阵(根据您的初始状态可能会得到不同的输出):
出租车变成了绿色。
- 现在,我们向南移动四个瓷砖去到达目的地(您可以根据初始状态进行调整),然后执行下车:
>>> print(env.step(0))
(118, -1, False, {'prob': 1.0})
>>> print(env.step(0))
(218, -1, False, {'prob': 1.0})
>>> print(env.step(0))
(318, -1, False, {'prob': 1.0})
>>> print(env.step(0))
(418, -1, False, {'prob': 1.0})
>>> print(env.step(5))
(410, 20, True, {'prob': 1.0})
最后它获得 +20 的奖励,并且回合结束。
现在,我们渲染环境:
>>> env.render()
您将看到以下更新的矩阵:
现在我们将执行 Q 学习来解决出租车环境,如下所示:
- 导入 PyTorch 库:
>>> import torch
-
然后,开始定义 epsilon-greedy 策略。我们将重用“开发 Q 学习算法”食谱中定义的
gen_epsilon_greedy_policy函数。 -
现在,我们指定回合的数量,并初始化用于跟踪每个回合长度和总奖励的两个变量:
>>> n_episode = 1000
>>> length_episode = [0] * n_episode
>>> total_reward_episode = [0] * n_episode
-
接下来,我们定义执行 Q 学习的函数。我们将重用“开发 Q 学习算法”食谱中定义的
q_learning函数。 -
现在,我们指定其余的参数,包括折扣率、学习率和 epsilon,并创建一个 epsilon-greedy 策略的实例:
>>> gamma = 1
>>> alpha = 0.4
>>> epsilon = 0.1 >>> epsilon_greedy_policy = gen_epsilon_greedy_policy(env.action_space.n, epsilon)
- 最后,我们进行 Q 学习来获得出租车问题的最优策略:
>>> optimal_Q, optimal_policy = q_learning(env, gamma, n_episode, alpha)
工作原理…
在这个食谱中,我们通过离线 Q 学习解决了出租车问题。
在步骤 6之后,您可以绘制每个周期的长度和总奖励,以验证模型是否收敛。时间序列的奖励图如下所示:
时间序列的奖励图如下所示:
您可以看到,优化在 400 个周期后开始饱和。
出租车环境是一个相对复杂的网格问题,有 500 个离散状态和 6 种可能的动作。Q-learning 通过学习贪婪策略生成的经验来优化每个步骤中的 Q 函数。我们在学习过程中获取环境信息,并使用这些信息按照ε-贪婪策略立即更新值。
使用 SARSA 解决出租车问题
在这个示例中,我们将使用 SARSA 算法解决出租车环境,并使用网格搜索算法微调超参数。
我们将从 SARSA 模型的默认超参数值开始。这些值是基于直觉和一些试验选择的。接下来,我们将提出最佳值的一组值。
如何做...
我们按照以下方式执行 SARSA 来解决出租车环境:
- 导入 PyTorch 和
gym模块,并创建出租车环境的一个实例:
>>> import torch
>>> import gym >>> env = gym.make('Taxi-v2')
-
然后,开始定义 ε-贪婪行为策略。我们将重用开发 SARSA 算法配方中定义的
gen_epsilon_greedy_policy函数。 -
然后,我们指定要追踪每个周期的长度和总奖励的两个变量的数量:
>>> n_episode = 1000 >>> length_episode = [0] * n_episode
>>> total_reward_episode = [0] * n_episode
-
现在,我们定义执行 SARSA 的函数。我们将重用开发 SARSA 算法配方中定义的
sarsa函数。 -
我们将折现率设定为
1,默认学习率设定为0.4,默认 ε 设定为0.1:
>>> gamma = 1
>>> alpha = 0.4
>>> epsilon = 0.01
- 接下来,我们创建 ε-贪婪策略的一个实例:
>>> epsilon_greedy_policy = gen_epsilon_greedy_policy(env.action_space.n, epsilon)
- 最后,我们使用前面步骤中定义的输入参数执行 SARSA:
>>> optimal_Q, optimal_policy = sarsa(env, gamma, n_episode, alpha)
工作原理...
在步骤 7之后,您可以绘制每个周期的长度和总奖励,以验证模型是否收敛。时间序列的奖励图如下所示:
时间序列的奖励图如下所示:
这个 SARSA 模型工作得很好,但不一定是最好的。稍后,我们将使用网格搜索来寻找 SARSA 模型下最佳的一组超参数。
出租车环境是一个相对复杂的网格问题,有 500 个离散状态和 6 种可能的动作。SARSA 算法通过学习和优化目标策略来优化每个步骤中的 Q 函数。我们在学习过程中获取环境信息,并使用这些信息按照ε-贪婪策略立即更新值。
还有更多...
网格搜索是一种程序化的方法,用于在强化学习中找到超参数的最佳值集合。每组超参数的性能由以下三个指标来衡量:
-
前几个 episode 的平均总奖励:我们希望尽早获得最大的奖励。
-
前几个 episode 的平均 episode 长度:我们希望出租车尽快到达目的地。
-
每个时间步的前几个 episode 的平均奖励:我们希望尽快获得最大的奖励。
让我们继续实施它:
- 我们在这里使用了三个 alpha 候选值[0.4, 0.5 和 0.6]和三个 epsilon 候选值[0.1, 0.03 和 0.01],并且仅考虑了前 500 个 episode:
>>> alpha_options = [0.4, 0.5, 0.6]
>>> epsilon_options = [0.1, 0.03, 0.01]
>>> n_episode = 500
- 我们通过训练每组超参数的 SARSA 模型并评估相应的性能来进行网格搜索:
>>> for alpha in alpha_options:
... for epsilon in epsilon_options:
... length_episode = [0] * n_episode
... total_reward_episode = [0] * n_episode
... sarsa(env, gamma, n_episode, alpha)
... reward_per_step = [reward/float(step) for
reward, step in zip(
total_reward_episode, length_episode)]
... print('alpha: {}, epsilon: {}'.format(alpha, epsilon))
... print('Average reward over {} episodes: {}'.format( n_episode, sum(total_reward_episode) / n_episode))
... print('Average length over {} episodes: {}'.format( n_episode, sum(length_episode) / n_episode))
... print('Average reward per step over {} episodes:
{}\n'.format(n_episode, sum(reward_per_step) / n_episode))
运行上述代码会生成以下结果:
alpha: 0.4, epsilon: 0.1
Average reward over 500 episodes: -75.442
Average length over 500 episodes: 57.682
Average reward per step over 500 episodes: -0.32510755063660324
alpha: 0.4, epsilon: 0.03
Average reward over 500 episodes: -73.378
Average length over 500 episodes: 56.53
Average reward per step over 500 episodes: -0.2761201410280632
alpha: 0.4, epsilon: 0.01
Average reward over 500 episodes: -78.722
Average length over 500 episodes: 59.366
Average reward per step over 500 episodes: -0.3561815084186654
alpha: 0.5, epsilon: 0.1
Average reward over 500 episodes: -72.026
Average length over 500 episodes: 55.592
Average reward per step over 500 episodes: -0.25355404831497264
alpha: 0.5, epsilon: 0.03
Average reward over 500 episodes: -67.562
Average length over 500 episodes: 52.706
Average reward per step over 500 episodes: -0.20602525679639022
alpha: 0.5, epsilon: 0.01
Average reward over 500 episodes: -75.252
Average length over 500 episodes: 56.73
Average reward per step over 500 episodes: -0.2588407558703358
alpha: 0.6, epsilon: 0.1
Average reward over 500 episodes: -62.568
Average length over 500 episodes: 49.488
Average reward per step over 500 episodes: -0.1700284221229244
alpha: 0.6, epsilon: 0.03
Average reward over 500 episodes: -68.56
Average length over 500 episodes: 52.804
Average reward per step over 500 episodes: -0.24794191768600077
alpha: 0.6, epsilon: 0.01
Average reward over 500 episodes: -63.468
Average length over 500 episodes: 49.752
Average reward per step over 500 episodes: -0.14350124172091722
我们可以看到,在这种情况下,最佳的超参数集合是 alpha: 0.6,epsilon: 0.01,它实现了每步最大的奖励和较大的平均奖励以及较短的平均 episode 长度。
开发双 Q-learning 算法
在这是一个额外的步骤,在本章中我们将开发双 Q-learning 算法。
Q-learning 是一种强大且流行的 TD 控制强化学习算法。然而,在某些情况下可能表现不佳,主要是因为贪婪组件maxa'Q(s', a')。它可能会高估动作值并导致性能不佳。双 Q-learning 通过利用两个 Q 函数来克服这一问题。我们将两个 Q 函数表示为Q1和Q2。在每一步中,随机选择一个 Q 函数进行更新。如果选择Q1,则更新如下:
如果选择 Q2,则更新如下:
这意味着每个 Q 函数都从另一个 Q 函数更新,遵循贪婪搜索,这通过使用单个 Q 函数减少了动作值的高估。
如何做...
现在我们开发双 Q-learning 来解决出租车环境,如下所示:
- 导入所需的库并创建 Taxi 环境的实例:
>>> import torch >>> import gym
>>> env = gym.make('Taxi-v2')
-
然后,开始定义 epsilon-greedy 策略。我们将重用在开发 Q-learning 算法步骤中定义的
gen_epsilon_greedy_policy函数。 -
然后,我们指定了 episode 的数量,并初始化了两个变量来跟踪每个 episode 的长度和总奖励:
>>> n_episode = 3000
>>> length_episode = [0] * n_episode
>>> total_reward_episode = [0] * n_episode
在这里,我们模拟了 3,000 个 episode,因为双 Q-learning 需要更多的 episode 才能收敛。
- 接下来,我们定义执行双 Q-learning 的函数:
>>> def double_q_learning(env, gamma, n_episode, alpha):
... """
... Obtain the optimal policy with off-policy double
Q-learning method
... @param env: OpenAI Gym environment
... @param gamma: discount factor
... @param n_episode: number of episodes
... @return: the optimal Q-function, and the optimal policy
... """
... n_action = env.action_space.n
... n_state = env.observation_space.n
... Q1 = torch.zeros(n_state, n_action)
... Q2 = torch.zeros(n_state, n_action)
... for episode in range(n_episode):
... state = env.reset()
... is_done = False
... while not is_done:
... action = epsilon_greedy_policy(state, Q1 + Q2)
... next_state, reward, is_done, info
= env.step(action)
... if (torch.rand(1).item() < 0.5):
... best_next_action = torch.argmax(Q1[next_state])
... td_delta = reward +
gamma * Q2[next_state][best_next_action]
- Q1[state][action]
... Q1[state][action] += alpha * td_delta
... else:
... best_next_action = torch.argmax(Q2[next_state])
... td_delta = reward +
gamma * Q1[next_state][best_next_action]
- Q2[state][action]
... Q2[state][action] += alpha * td_delta
... length_episode[episode] += 1
... total_reward_episode[episode] += reward
... if is_done:
... break
... state = next_state
... policy = {}
... Q = Q1 + Q2
... for state in range(n_state):
... policy[state] = torch.argmax(Q[state]).item()
... return Q, policy
- 然后,我们指定了剩余的参数,包括折扣率、学习率和 epsilon,并创建了 epsilon-greedy-policy 的实例:
>>> gamma = 1
>>> alpha = 0.4
>>> epsilon = 0.1 >>> epsilon_greedy_policy = gen_epsilon_greedy_policy(env.action_space.n, epsilon)
- 最后,我们执行双 Q-learning 以获得出租车问题的最优策略:
>>> optimal_Q, optimal_policy = double_q_learning(env, gamma, n_episode, alpha)
工作原理...
我们在本示例中使用双 Q 学习算法解决了出租车问题。
在第 4 步中,我们执行双 Q 学习,完成以下任务:
-
将两个 Q 表初始化为全零。
-
在每个周期的每个步骤中,我们随机选择一个 Q 函数来更新。让代理根据 epsilon-greedy 策略选择动作并使用另一个 Q 函数更新所选的 Q 函数。
-
运行
n_episode个周期。 -
基于最优 Q 函数获得最优策略,通过求和(或平均)两个 Q 函数来实现。
在第 6 步之后,您可以绘制每个周期的长度和总奖励,以验证模型是否收敛。周期长度随时间的变化图如下所示:
奖励随时间变化的图表如下所示:
双 Q 学习克服了单 Q 学习在复杂环境中的潜在缺点。它随机地在两个 Q 函数之间切换并更新它们,这可以防止一个 Q 函数的动作值被高估。同时,它可能会低估 Q 函数,因为它不会在时间步长内更新相同的 Q 函数。因此,我们可以看到最优动作值需要更多的周期来收敛。
另请参阅
了解双 Q 学习背后的理论,请参阅 Hado van Hasselt 的原始论文,papers.nips.cc/paper/3964-double-q-learning,发表于神经信息处理系统进展 23(NIPS 2010),2613-2621,2010 年。