PyTorch 1.x 强化学习秘籍(三)
原文:
zh.annas-archive.org/md5/863e6116b9dfbed5ea6521a90f2b5732译者:飞龙
第五章:解决多臂老虎机问题
多臂老虎机算法可能是强化学习中最流行的算法之一。本章将从创建多臂老虎机开始,并尝试使用随机策略。我们将专注于如何使用ε-贪心、softmax 探索、上置信区间和汤普森抽样等四种策略解决多臂老虎机问题。我们将看到它们如何以独特的方式处理探索与利用的困境。我们还将解决一个价值十亿美元的问题,即在线广告,演示如何使用多臂老虎机算法解决它。最后,我们将使用上下文老虎机解决上下文广告问题,以在广告优化中做出更明智的决策。
本章将涵盖以下配方:
-
创建多臂老虎机环境
-
使用ε-贪心策略解决多臂老虎机问题
-
使用 softmax 探索解决多臂老虎机问题
-
使用上置信区间算法解决多臂老虎机问题
-
使用多臂老虎机解决互联网广告问题
-
使用汤普森抽样算法解决多臂老虎机问题
-
使用上下文老虎机解决互联网广告问题
创建多臂老虎机环境
让我们开始一个简单的项目,使用蒙特卡洛方法来估计π的值,这是无模型强化学习算法的核心。
多臂老虎机问题是最简单的强化学习问题之一。它最好被描述为一个有多个杠杆(臂)的老虎机,每个杠杆有不同的支付和支付概率。我们的目标是发现具有最大回报的最佳杠杆,以便在之后继续选择它。让我们从一个简单的多臂老虎机问题开始,其中每个臂的支付和支付概率是固定的。在创建环境后,我们将使用随机策略算法来解决它。
如何做…
让我们按照以下步骤开发多臂老虎机环境:
>>> import torch
>>> class BanditEnv():
... """
... Multi-armed bandit environment
... payout_list:
... A list of probabilities of the likelihood that a
particular bandit will pay out
... reward_list:
... A list of rewards of the payout that bandit has
... """
... def __init__(self, payout_list, reward_list):
... self.payout_list = payout_list
... self.reward_list = reward_list
...
... def step(self, action):
... if torch.rand(1).item() < self.payout_list[action]:
... return self.reward_list[action]
... return 0
步骤方法执行一个动作,并在支付时返回奖励,否则返回 0。
现在,我们将以多臂老虎机为例,并使用随机策略解决它:
- 定义三臂老虎机的支付概率和奖励,并创建老虎机环境的实例:
>>> bandit_payout = [0.1, 0.15, 0.3]
>>> bandit_reward = [4, 3, 1]>>> bandit_env = BanditEnv(bandit_payout, bandit_reward)
例如,选择臂 0 获得奖励 4 的概率为 10%。
- 我们指定要运行的集数,并定义保存通过选择各个臂累积的总奖励、选择各个臂的次数以及各个臂随时间的平均奖励的列表:
>>> n_episode = 100000
>>> n_action = len(bandit_payout)
>>> action_count = [0 for _ in range(n_action)]
>>> action_total_reward = [0 for _ in range(n_action)]
>>> action_avg_reward = [[] for action in range(n_action)]
- 定义随机策略,随机选择一个臂:
>>> def random_policy():
... action = torch.multinomial(torch.ones(n_action), 1).item()
... return action
- 现在,我们运行 100,000 个集数。对于每个集数,我们还更新每个臂的统计数据:
>>> for episode in range(n_episode):
... action = random_policy()
... reward = bandit_env.step(action)
... action_count[action] += 1
... action_total_reward[action] += reward
... for a in range(n_action):
... if action_count[a]:
... action_avg_reward[a].append(
action_total_reward[a] / action_count[a])
... else:
... action_avg_reward[a].append(0)
- 运行了 100,000 个集数后,我们绘制了随时间变化的平均奖励的结果:
>>> import matplotlib.pyplot as plt
>>> for action in range(n_action):
... plt.plot(action_avg_reward[action])
>>> plt.legend([‘Arm {}’.format(action) for action in range(n_action)])
>>> plt.title(‘Average reward over time’)
>>> plt.xscale(‘log’)
>>> plt.xlabel(‘Episode’)
>>> plt.ylabel(‘Average reward’)
>>> plt.show()
它是如何工作的…
在我们刚刚处理的示例中,有三台老虎机。每台机器都有不同的支付(奖励)和支付概率。在每个 episode 中,我们随机选择一台机器的一个臂来拉(执行一个动作),并以一定的概率获得支付。
执行Step 5中的代码行,你将看到以下图表:
臂 1 是平均奖励最高的最佳臂。此外,平均奖励在大约 10,000 个 episode 后开始饱和。
此解决方案看起来非常幼稚,因为我们仅对所有臂进行了探索。在接下来的配方中,我们将提出更智能的策略。
使用ε-贪婪策略解决多臂老虎机问题
不再仅仅通过随机策略进行探索,我们可以通过探索与利用的结合做得更好。这就是著名的ε-贪婪策略。
对于多臂老虎机的ε-贪婪策略,大部分时间利用最佳动作,同时不时探索不同的动作。给定参数ε,其取值范围为 0 到 1,执行探索和利用的概率分别为ε和 1 - ε:
- ε:每个动作的概率如下计算:
这里,|A|是可能动作的数量。
- 贪婪:优选具有最高状态-动作值的动作,并且其被选择的概率增加 1 - ε:
如何操作...
我们使用ε-贪婪策略解决多臂老虎机问题如下:
- 导入 PyTorch 和我们在之前的配方中开发的老虎机环境,创建多臂老虎机环境(假设
BanditEnv类在名为multi_armed_bandit.py的文件中):
>>> import torch
>>> from multi_armed_bandit import BanditEnv
- 定义三臂老虎机的支付概率和奖励,并创建一个老虎机环境的实例:
>>> bandit_payout = [0.1, 0.15, 0.3]
>>> bandit_reward = [4, 3, 1] >>> bandit_env = BanditEnv(bandit_payout, bandit_reward)
- 指定要运行的 episode 数,并定义持有通过选择各个臂累积的总奖励、选择各个臂的次数以及每个臂随时间变化的平均奖励的列表:
>>> n_episode = 100000
>>> n_action = len(bandit_payout)
>>> action_count = [0 for _ in range(n_action)]
>>> action_total_reward = [0 for _ in range(n_action)]
>>> action_avg_reward = [[] for action in range(n_action)]
- 定义ε-贪婪策略函数,指定ε的值,并创建一个ε-贪婪策略实例:
>>> def gen_epsilon_greedy_policy(n_action, epsilon):
... def policy_function(Q):
... probs = torch.ones(n_action) * epsilon / n_action
... best_action = torch.argmax(Q).item()
... probs[best_action] += 1.0 - epsilon
... action = torch.multinomial(probs, 1).item()
... return action
... return policy_function >>> epsilon = 0.2
>>> epsilon_greedy_policy = gen_epsilon_greedy_policy(n_action, epsilon)
- 初始化
Q函数,即各个臂获得的平均奖励:
>>> Q = torch.zeros(n_action)
我们将随时间更新Q函数。
- 现在,我们运行 100,000 个 episode。每个 episode,我们还会随时间更新每个臂的统计信息:
>>> for episode in range(n_episode):
... action = epsilon_greedy_policy(Q)
... reward = bandit_env.step(action)
... action_count[action] += 1
... action_total_reward[action] += reward
... Q[action] = action_total_reward[action] / action_count[action]
... for a in range(n_action):
... if action_count[a]:
... action_avg_reward[a].append(
action_total_reward[a] / action_count[a])
... else:
... action_avg_reward[a].append(0)
- 运行 100,000 个 episode 后,我们绘制了随时间变化的平均奖励结果:
>>> import matplotlib.pyplot as plt
>>> for action in range(n_action):
... plt.plot(action_avg_reward[action])
>>> plt.legend([‘Arm {}’.format(action) for action in range(n_action)])
>>> plt.title(‘Average reward over time’)
>>> plt.xscale(‘log’)
>>> plt.xlabel(‘Episode’)
>>> plt.ylabel(‘Average reward’)
>>> plt.show()
工作原理...
类似于其他 MDP 问题,ε-贪婪策略以 1 - ε的概率选择最佳臂,并以ε的概率进行随机探索。ε管理着探索与利用之间的权衡。
在Step 7中,你将看到以下图表:
臂 1 是最佳臂,在最后具有最大的平均奖励。此外,它的平均奖励在大约 1,000 个剧集后开始饱和。
更多内容...
你可能想知道ε-贪婪策略是否确实优于随机策略。除了在ε-贪婪策略中最优臂的值较早收敛外,我们还可以证明,在训练过程中,通过ε-贪婪策略获得的平均奖励比随机策略更高。
我们可以简单地计算所有剧集的平均奖励:
>>> print(sum(action_total_reward) / n_episode)
0.43718
在 100,000 个剧集中,使用ε-贪婪策略的平均支付率为 0.43718。对随机策略解决方案进行相同计算后,得到平均支付率为 0.37902。
使用 softmax 探索解决多臂赌博问题
在本示例中,我们将使用 softmax 探索算法解决多臂赌博问题。我们将看到它与ε-贪婪策略的不同之处。
正如我们在ε-贪婪中看到的,当进行探索时,我们以 ε/|A| 的概率随机选择非最佳臂之一。每个非最佳臂在 Q 函数中的价值不管其值都是等效的。此外,无论其值如何,最佳臂都以固定概率被选择。在 softmax 探索 中,根据 Q 函数值的 softmax 分布选择臂。概率计算如下:
这里,τ 参数是温度因子,用于指定探索的随机性。τ 值越高,探索就越接近平等;τ 值越低,选择最佳臂的可能性就越大。
如何做到...
我们如下解决了使用 softmax 探索算法的多臂赌博问题:
- 导入 PyTorch 和我们在第一个示例中开发的赌博环境,创建多臂赌博环境(假设
BanditEnv类在名为multi_armed_bandit.py的文件中):
>>> import torch
>>> from multi_armed_bandit import BanditEnv
- 定义三臂赌博的支付概率和奖励,并创建赌博环境的实例:
>>> bandit_payout = [0.1, 0.15, 0.3]
>>> bandit_reward = [4, 3, 1] >>> bandit_env = BanditEnv(bandit_payout, bandit_reward)
- 我们指定要运行的剧集数量,并定义保存通过选择各个臂累积的总奖励、选择各个臂的次数以及每个臂随时间的平均奖励的列表:
>>> n_episode = 100000
>>> n_action = len(bandit_payout)
>>> action_count = [0 for _ in range(n_action)]
>>> action_total_reward = [0 for _ in range(n_action)]
>>> action_avg_reward = [[] for action in range(n_action)]
- 定义 softmax 探索策略函数,指定 τ 的值,并创建 softmax 探索策略实例:
>>> def gen_softmax_exploration_policy(tau):
... def policy_function(Q):
... probs = torch.exp(Q / tau)
... probs = probs / torch.sum(probs)
... action = torch.multinomial(probs, 1).item()
... return action
... return policy_function >>> tau = 0.1
>>> softmax_exploration_policy = gen_softmax_exploration_policy(tau)
- 初始化 Q 函数,即通过各个臂获得的平均奖励:
>>> Q = torch.zeros(n_action)
我们将随时间更新 Q 函数。
- 现在,我们运行 100,000 个剧集。对于每个剧集,我们还更新每个臂的统计信息:
>>> for episode in range(n_episode):
... action = softmax_exploration_policy(Q)
... reward = bandit_env.step(action)
... action_count[action] += 1
... action_total_reward[action] += reward
... Q[action] = action_total_reward[action] / action_count[action]
... for a in range(n_action):
... if action_count[a]:
... action_avg_reward[a].append( action_total_reward[a] / action_count[a])
... else:
... action_avg_reward[a].append(0)
- 运行了 100,000 个剧集后,我们绘制了随时间变化的平均奖励结果:
>>> import matplotlib.pyplot as plt
>>> for action in range(n_action):
... plt.plot(action_avg_reward[action])
>>> plt.legend([‘Arm {}’.format(action) for action in range(n_action)])
>>> plt.title(‘Average reward over time’)
>>> plt.xscale(‘log’)
>>> plt.xlabel(‘Episode’)
>>> plt.ylabel(‘Average reward’)
>>> plt.show()
工作原理...
使用 softmax 探索策略,利用基于 Q 值的 softmax 函数解决了开发与探索的困境。它不是使用最佳臂和非最佳臂的固定概率对,而是根据τ参数作为温度因子的 softmax 分布调整概率。τ值越高,焦点就会更多地转向探索。
在步骤 7中,您将看到以下绘图:
臂 1 是最佳臂,在最后具有最大的平均奖励。此外,在这个例子中,它的平均奖励在大约 800 个 episode 后开始饱和。
使用上置信度边界算法解决多臂赌博问题
在前两个配方中,我们通过在 epsilon-贪婪策略中将概率分配为固定值或者根据 Q 函数值计算 softmax 探索算法中的概率,探索了多臂赌博问题中的随机动作。在任一算法中,随机执行动作的概率并不随时间调整。理想情况下,我们希望随着学习的进行减少探索。在本配方中,我们将使用称为上置信度边界的新算法来实现这一目标。
上置信度边界(UCB)算法源于置信区间的概念。一般来说,置信区间是真值所在的一系列值。在 UCB 算法中,臂的置信区间是该臂获取的平均奖励所处的范围。该区间的形式为[下置信度边界,上置信度边界],我们只使用上置信度边界,即 UCB,来估计该臂的潜力。UCB 的计算公式如下:
这里,t 是 episode 的数量,N(a)是在 t 个 episode 中臂 a 被选择的次数。随着学习的进行,置信区间收缩并变得越来越精确。应该拉动的臂是具有最高 UCB 的臂。
如何做...
我们使用 UCB 算法解决多臂赌博问题的步骤如下:
- 导入 PyTorch 和第一个配方中开发的赌博环境,创建多臂赌博环境(假设
BanditEnv类位于名为multi_armed_bandit.py的文件中):
>>> import torch
>>> from multi_armed_bandit import BanditEnv
- 定义三臂赌博的赔率概率和奖励,并创建赌博环境的一个实例:
>>> bandit_payout = [0.1, 0.15, 0.3]
>>> bandit_reward = [4, 3, 1] >>> bandit_env = BanditEnv(bandit_payout, bandit_reward)
- 我们指定要运行的 episode 数量,并定义保存通过选择不同臂积累的总奖励、选择各个臂的次数以及各个臂随时间的平均奖励的列表:
>>> n_episode = 100000
>>> n_action = len(bandit_payout)
>>> action_count = torch.tensor([0\. for _ in range(n_action)])
>>> action_total_reward = [0 for _ in range(n_action)]
>>> action_avg_reward = [[] for action in range(n_action)]
- 定义 UCB 策略函数,根据 UCB 公式计算最佳臂:
>>> def upper_confidence_bound(Q, action_count, t):
... ucb = torch.sqrt((2 * torch.log(torch.tensor(float(t)))) / action_count) + Q
... return torch.argmax(ucb)
- 初始化 Q 函数,它是使用各个臂获取的平均奖励:
>>> Q = torch.empty(n_action)
随着时间的推移,我们将更新 Q 函数。
- 现在,我们使用我们的 UCB 策略运行 100,000 个 episode。对于每个 episode,我们还更新每个臂的统计信息:
>>> for episode in range(n_episode):
... action = upper_confidence_bound(Q, action_count, episode)
... reward = bandit_env.step(action)
... action_count[action] += 1
... action_total_reward[action] += reward
... Q[action] = action_total_reward[action] / action_count[action]
... for a in range(n_action):
... if action_count[a]:
... action_avg_reward[a].append( action_total_reward[a] / action_count[a])
... else:
... action_avg_reward[a].append(0)
- 在运行了 10 万个剧集后,我们绘制了随时间变化的平均奖励结果:
>>> import matplotlib.pyplot as plt
>>> for action in range(n_action):
... plt.plot(action_avg_reward[action])
>>> plt.legend([‘Arm {}’.format(action) for action in range(n_action)])
>>> plt.title(‘Average reward over time’)
>>> plt.xscale(‘log’)
>>> plt.xlabel(‘Episode’)
>>> plt.ylabel(‘Average reward’)
>>> plt.show()
工作原理...
在这个示例中,我们使用了 UCB 算法解决了多臂赌博机问题。它根据剧集数调整开发-探索困境。对于数据点较少的动作,其置信区间相对较宽,因此选择此动作具有相对较高的不确定性。随着更多的动作剧集被选中,置信区间变窄并收缩到其实际值。在这种情况下,选择(或不选择)此动作是非常确定的。最后,在每个剧集中,UCB 算法拉动具有最高 UCB 的臂,并随着时间的推移获得越来越多的信心。
在第 7 步中运行代码后,您将看到以下绘图:
第一个臂是最佳的臂,最终平均奖励最高。
还有更多内容...
您可能想知道 UCB 是否真的优于ε-greedy 策略。我们可以计算整个训练过程中的平均奖励,平均奖励最高的策略学习速度更快。
我们可以简单地平均所有剧集的奖励:
>>> print(sum(action_total_reward) / n_episode)
0.44605
在 10 万个剧集中,使用 UCB 的平均支付率为 0.44605,高于ε-greedy 策略的 0.43718。
另请参阅
对于那些想要了解置信区间的人,请随时查看以下内容:www.stat.yale.edu/Courses/1997-98/101/confint.htm
解决互联网广告问题的多臂赌博机
想象一下,您是一位在网站上进行广告优化的广告商:
-
广告背景有三种不同的颜色 – 红色,绿色和蓝色。哪种将实现最佳点击率(CTR)?
-
广告有三种不同的文案 – 学习…,免费… 和 尝试…。哪一个将实现最佳 CTR?
对于每位访客,我们需要选择一个广告,以最大化随时间的点击率(CTR)。我们如何解决这个问题?
或许您在考虑 A/B 测试,其中您随机将流量分成几组,并将每个广告分配到不同的组中,然后在观察一段时间后选择具有最高 CTR 的组中的广告。然而,这基本上是完全的探索,我们通常不确定观察期应该多长,最终会失去大量潜在的点击。此外,在 A/B 测试中,假设广告的未知 CTR 不会随时间而变化。否则,这种 A/B 测试应定期重新运行。
多臂赌博机确实可以比 A/B 测试做得更好。每个臂是一个广告,臂的奖励要么是 1(点击),要么是 0(未点击)。
让我们尝试用 UCB 算法解决这个问题。
如何做到...
我们可以使用 UCB 算法解决多臂赌博机广告问题,具体如下:
- 导入 PyTorch 和我们在第一个示例中开发的老虎机环境,《创建多臂老虎机环境》(假设
BanditEnv类位于名为multi_armed_bandit.py的文件中):
>>> import torch
>>> from multi_armed_bandit import BanditEnv
- 定义三臂老虎机(例如三个广告候选项)的支付概率和奖励,并创建老虎机环境的实例:
>>> bandit_payout = [0.01, 0.015, 0.03]
>>> bandit_reward = [1, 1, 1]>>> bandit_env = BanditEnv(bandit_payout, bandit_reward)
在这里,广告 0 的真实点击率为 1%,广告 1 为 1.5%,广告 2 为 3%。
- 我们指定要运行的周期数,并定义包含通过选择各个臂累积的总奖励、选择各个臂的次数以及每个臂随时间的平均奖励的列表:
>>> n_episode = 100000
>>> n_action = len(bandit_payout)
>>> action_count = torch.tensor([0\. for _ in range(n_action)])
>>> action_total_reward = [0 for _ in range(n_action)]
>>> action_avg_reward = [[] for action in range(n_action)]
- 定义 UCB 策略函数,根据 UCB 公式计算最佳臂:
>>> def upper_confidence_bound(Q, action_count, t):
... ucb = torch.sqrt((2 * torch.log(
torch.tensor(float(t)))) / action_count) + Q
... return torch.argmax(ucb)
- 初始化 Q 函数,即各个臂获得的平均奖励:
>>> Q = torch.empty(n_action)
我们将随时间更新 Q 函数。
- 现在,我们使用 UCB 策略运行 100,000 个周期。对于每个周期,我们还更新每个臂的统计信息:
>>> for episode in range(n_episode):
... action = upper_confidence_bound(Q, action_count, episode)
... reward = bandit_env.step(action)
... action_count[action] += 1
... action_total_reward[action] += reward
... Q[action] = action_total_reward[action] / action_count[action]
... for a in range(n_action):
... if action_count[a]:
... action_avg_reward[a].append(
action_total_reward[a] / action_count[a])
... else:
... action_avg_reward[a].append(0)
- 运行 100,000 个周期后,我们绘制随时间变化的平均奖励结果:
>>> import matplotlib.pyplot as plt
>>> for action in range(n_action):
... plt.plot(action_avg_reward[action])
>>> plt.legend([‘Arm {}’.format(action) for action in range(n_action)])
>>> plt.title(‘Average reward over time’)
>>> plt.xscale(‘log’)
>>> plt.xlabel(‘Episode’)
>>> plt.ylabel(‘Average reward’)
>>> plt.show()
它的工作原理…
在这个示例中,我们以多臂老虎机的方式解决了广告优化问题。它克服了 A/B 测试方法所面临的挑战。我们使用 UCB 算法解决多臂(多广告)老虎机问题;每个臂的奖励要么是 1,要么是 0。UCB(或其他算法如 epsilon-greedy 和 softmax 探索)动态地在开发和探索之间切换。对于数据点较少的广告,置信区间相对较宽,因此选择此动作具有相对高的不确定性。随着广告被选择的次数增多,置信区间变窄,并收敛到其实际值。
您可以在 第 7 步 中看到生成的图表如下:
模型收敛后,广告 2 是预测的点击率(平均奖励)最高的广告。
最终,我们发现广告 2 是最优选择,这是真实的。而且我们越早发现这一点越好,因为我们会损失更少的潜在点击。在这个例子中,大约在 100 个周期后,广告 2 表现优于其他广告。
使用汤普森抽样算法解决多臂老虎机问题
在这个示例中,我们将使用另一种算法——汤普森抽样,解决广告老虎机问题中的开发和探索困境。我们将看到它与前三种算法的显著区别。
汤普森抽样(TS)也称为贝叶斯老虎机,因为它从以下角度应用贝叶斯思维:
-
这是一个概率算法。
-
它计算每个臂的先验分布并从每个分布中抽样一个值。
-
然后选择值最高的臂并观察奖励。
-
最后,根据观察到的奖励更新先验分布。这个过程称为贝叶斯更新。
正如我们在广告优化案例中看到的,每个臂的奖励要么是 1 要么是 0。我们可以使用贝塔分布作为我们的先验分布,因为贝塔分布的值在 0 到 1 之间。贝塔分布由两个参数α和β参数化。α表示我们获得奖励为 1 的次数,β表示我们获得奖励为 0 的次数。
为了帮助你更好地理解贝塔分布,我们将首先看几个贝塔分布,然后再实施 TS 算法。
怎么做……
让我们通过以下步骤来探索贝塔分布:
- 导入 PyTorch 和 matplotlib 因为我们将可视化分布的形状:
>>> import torch
>>> import matplotlib.pyplot as plt
- 我们首先通过起始位置α=1 和β=1 来可视化贝塔分布的形状:
>>> beta1 = torch.distributions.beta.Beta(1, 1)
>>> samples1 = [beta1.sample() for _ in range(100000)]
>>> plt.hist(samples1, range=[0, 1], bins=10)
>>> plt.title(‘beta(1, 1)’)
>>> plt.show()
你将看到以下的绘图:
显然,当α=1 且β=1 时,它不提供有关真实值在 0 到 1 范围内位置的任何信息。因此,它成为均匀分布。
- 我们随后用α=5 和β=1 来可视化贝塔分布的形状:
>>> beta2 = torch.distributions.beta.Beta(5, 1)
>>> samples2 = [beta2.sample() for _ in range(100000)]
>>> plt.hist(samples2, range=[0, 1], bins=10)
>>> plt.title(‘beta(5, 1)’)
>>> plt.show()
你将看到以下的绘图:
当α=5 且β=1 时,这意味着在 4 次实验中有 4 次连续的奖励为 1。分布向 1 偏移。
- 现在,让我们实验α=1 和β=5:
>>> beta3 = torch.distributions.beta.Beta(1, 5)
>>> samples3= [beta3.sample() for _ in range(100000)]
>>> plt.hist(samples3, range=[0, 1], bins=10)
>>> plt.title(‘beta(1, 5)’)
>>> plt.show()
你将看到以下的绘图:
当α=1 且β=5 时,这意味着在 4 次实验中有 4 次连续的奖励为 0。分布向 0 偏移。
- 最后,让我们看看当α=5 且β=5 时的情况:
>>> beta4 = torch.distributions.beta.Beta(5, 5)
>>> samples4= [beta4.sample() for _ in range(100000)]
>>> plt.hist(samples4, range=[0, 1], bins=10)
>>> plt.title(‘beta(5, 5)’)
>>> plt.show()
你将看到以下的绘图:
当α=5 且β=5 时,在 8 轮中观察到相同数量的点击和未点击。分布向中间点0.5偏移。
现在是时候使用汤普森采样算法来解决多臂老虎机广告问题了:
- 导入我们在第一个示例中开发的老虎机环境,创建多臂老虎机环境(假设
BanditEnv类在名为multi_armed_bandit.py的文件中):
>>> from multi_armed_bandit import BanditEnv
- 定义三臂老虎机(三个广告候选项)的支付概率和奖励,并创建一个老虎机环境的实例:
>>> bandit_payout = [0.01, 0.015, 0.03]
>>> bandit_reward = [1, 1, 1]>>> bandit_env = BanditEnv(bandit_payout, bandit_reward)
- 我们指定要运行的剧集数,并定义包含通过选择各个臂累积的总奖励、选择各个臂的次数以及每个臂的平均奖励随时间变化的列表:
>>> n_episode = 100000
>>> n_action = len(bandit_payout)
>>> action_count = torch.tensor([0\. for _ in range(n_action)])
>>> action_total_reward = [0 for _ in range(n_action)]
>>> action_avg_reward = [[] for action in range(n_action)]
- 定义 TS 函数,从每个臂的贝塔分布中抽样一个值,并选择具有最高值的臂:
>>> def thompson_sampling(alpha, beta):
... prior_values = torch.distributions.beta.Beta(alpha, beta).sample()
... return torch.argmax(prior_values)
- 为每个臂初始化α和β:
>>> alpha = torch.ones(n_action)
>>> beta = torch.ones(n_action)
注意,每个贝塔分布的起始值应为α=β=1。
- 现在,我们使用 TS 算法运行了 100,000 个剧集。对于每个剧集,我们还根据观察到的奖励更新每个臂的 α 和 β:
>>> for episode in range(n_episode):
... action = thompson_sampling(alpha, beta)
... reward = bandit_env.step(action)
... action_count[action] += 1
... action_total_reward[action] += reward
... if reward > 0:
... alpha[action] += 1
... else:
... beta[action] += 1
... for a in range(n_action):
... if action_count[a]:
... action_avg_reward[a].append( action_total_reward[a] / action_count[a])
... else:
... action_avg_reward[a].append(0)
- 运行 100,000 个剧集后,我们绘制了随时间变化的平均奖励结果:
>>> import matplotlib.pyplot as plt
>>> for action in range(n_action):
... plt.plot(action_avg_reward[action])
>>> plt.legend([‘Arm {}’.format(action) for action in range(n_action)])
>>> plt.title(‘Average reward over time’)
>>> plt.xscale(‘log’)
>>> plt.xlabel(‘Episode’)
>>> plt.ylabel(‘Average reward’)
>>> plt.show()
工作原理...
在本文中,我们使用 TS 算法解决了广告赌博机问题。TS 与另外三种方法的最大区别在于采用贝叶斯优化。它首先计算每个可能臂的先验分布,然后从每个分布中随机抽取一个值。然后选择具有最高值的臂,并使用观察到的结果更新先验分布。TS 策略既是随机的又是贪婪的。如果某个广告更有可能获得点击,则其贝塔分布向 1 移动,因此随机样本的值趋向于更接近 1。
运行步骤 7 中的代码行后,您将看到以下图表:
广告 2 是最佳广告,预测的点击率(平均奖励)最高。
另请参阅
对于希望了解贝塔分布的人,可以随时查看以下链接:
解决互联网广告问题的上下文赌博机
您可能会注意到,在广告优化问题中,我们只关心广告本身,而忽略可能影响广告是否被点击的其他信息,例如用户信息和网页信息。在本文中,我们将讨论如何考虑超出广告本身的更多信息,并使用上下文赌博机解决这个问题。
到目前为止,我们处理过的多臂赌博机问题不涉及状态的概念,这与 MDPs 非常不同。我们只有几个动作,并且会生成与所选动作相关联的奖励。上下文赌博机通过引入状态的概念扩展了多臂赌博机。状态提供了环境的描述,帮助代理人采取更加明智的行动。在广告示例中,状态可以是用户的性别(两个状态,男性和女性)、用户的年龄组(例如四个状态)或页面类别(例如体育、财务或新闻)。直观地说,特定人口统计学的用户更有可能在某些页面上点击广告。
理解上下文赌博机并不难。一个多臂赌博机是一台具有多个臂的单机,而上下文赌博机是一组这样的机器(赌博机)。上下文赌博机中的每台机器是一个具有多个臂的状态。学习的目标是找到每台机器(状态)的最佳臂(动作)。
我们将以两个状态的广告示例为例。
如何做...
我们使用 UCB 算法解决上下文老虎机广告问题如下:
- 导入 PyTorch 和我们在第一个示例中开发的老虎机环境,创建一个多臂老虎机环境(假设
BanditEnv类在名为multi_armed_bandit.py的文件中):
>>> import torch
>>> from multi_armed_bandit import BanditEnv
- 定义两个三臂老虎机的支付概率和奖励:
>>> bandit_payout_machines = [
... [0.01, 0.015, 0.03],
... [0.025, 0.01, 0.015]
... ]
>>> bandit_reward_machines = [
... [1, 1, 1],
... [1, 1, 1]
... ]
在这里,广告 0 的真实 CTR 为 1%,广告 1 为 1.5%,广告 2 为 3%适用于第一个状态,以及第二个状态的[2.5%,1%,1.5%]。
我们的情况下有两台老虎机:
>>> n_machine = len(bandit_payout_machines)
根据相应的支付信息创建一个老虎机列表:
>>> bandit_env_machines = [BanditEnv(bandit_payout, bandit_reward)
... for bandit_payout, bandit_reward in
... zip(bandit_payout_machines, bandit_reward_machines)]
- 我们指定要运行的剧集数,并定义包含在每个状态下选择各个臂时累计的总奖励、每个状态下选择各个臂的次数以及每个状态下各个臂随时间的平均奖励的列表:
>>> n_episode = 100000
>>> n_action = len(bandit_payout_machines[0])
>>> action_count = torch.zeros(n_machine, n_action)
>>> action_total_reward = torch.zeros(n_machine, n_action)
>>> action_avg_reward = [[[] for action in range(n_action)] for _ in range(n_machine)]
- 定义 UCB 策略函数,根据 UCB 公式计算最佳臂:
>>> def upper_confidence_bound(Q, action_count, t):
... ucb = torch.sqrt((2 * torch.log(
torch.tensor(float(t)))) / action_count) + Q
... return torch.argmax(ucb)
- 初始化 Q 函数,这是在各个状态下使用各个臂获得的平均奖励:
>>> Q_machines = torch.empty(n_machine, n_action)
我们将随时间更新 Q 函数。
- 现在,我们使用 UCB 策略运行 100,000 个剧集。对于每个剧集,我们还更新每个状态下每个臂的统计数据:
>>> for episode in range(n_episode):
... state = torch.randint(0, n_machine, (1,)).item()
... action = upper_confidence_bound( Q_machines[state], action_count[state], episode)
... reward = bandit_env_machines[state].step(action)
... action_count[state][action] += 1
... action_total_reward[state][action] += reward
... Q_machines[state][action] = action_total_reward[state][action] / action_count[state][action]
... for a in range(n_action):
... if action_count[state][a]:
... action_avg_reward[state][a].append( action_total_reward[state][a] / action_count[state][a])
... else:
... action_avg_reward[state][a].append(0)
- 运行 100,000 个剧集后,我们绘制每个状态随时间变化的平均奖励结果:
>>> import matplotlib.pyplot as plt
>>> for state in range(n_machine):
... for action in range(n_action):
... plt.plot(action_avg_reward[state][action])
... plt.legend([‘Arm {}’.format(action) for action in range(n_action)])
... plt.xscale(‘log’)
... plt.title( ‘Average reward over time for state {}’.format(state))
... plt.xlabel(‘Episode’)
... plt.ylabel(‘Average reward’)
... plt.show()
工作原理如下...
在这个示例中,我们使用 UCB 算法解决了上下文广告问题的上下文老虎机问题。
运行步骤 7中的代码行,您将看到以下绘图。
我们得到了第一个状态的结果:
我们得到了第二个状态的结果:
给定第一个状态,广告 2 是最佳广告,具有最高的预测点击率。给定第二个状态,广告 0 是最佳广告,具有最高的平均奖励。这两者都是真实的。
上下文老虎机是一组多臂老虎机。每个老虎机代表环境的唯一状态。状态提供了环境的描述,帮助代理者采取更明智的行动。在我们的广告示例中,男性用户可能比女性用户更有可能点击广告。我们简单地使用了两台老虎机来包含两种状态,并在每种状态下寻找最佳的拉杆臂。
请注意,尽管上下文老虎机涉及状态的概念,但它们仍然与 MDP 有所不同。首先,上下文老虎机中的状态不是由先前的动作或状态决定的,而只是环境的观察。其次,上下文老虎机中没有延迟或折现奖励,因为老虎机剧集是一步。然而,与多臂老虎机相比,上下文老虎机更接近 MDP,因为动作是环境状态的条件。可以说上下文老虎机介于多臂老虎机和完整 MDP 强化学习之间是安全的。
第六章:通过函数逼近扩展学习
到目前为止,在 MC 和 TD 方法中,我们已经以查找表的形式表示了值函数。TD 方法能够在一个 episode 中实时更新 Q 函数,这被认为是 MC 方法的进步。然而,TD 方法对于具有许多状态和/或动作的问题仍然不够可扩展。使用 TD 方法学习太多个状态和动作对的值将会非常缓慢。
本章将重点讲述函数逼近,这可以克服 TD 方法中的扩展问题。我们将从设置 Mountain Car 环境开始。在开发线性函数估计器之后,我们将其融入 Q-learning 和 SARSA 算法中。然后,我们将利用经验重放改进 Q-learning 算法,并尝试使用神经网络作为函数估计器。最后,我们将讨论如何利用本章学到的内容解决 CartPole 问题。
本章将涵盖以下示例:
-
设置 Mountain Car 环境的游乐场
-
使用梯度下降逼近估算 Q 函数
-
使用线性函数逼近开发 Q-learning
-
使用线性函数逼近开发 SARSA
-
使用经验重放进行批处理
-
使用神经网络函数逼近开发 Q-learning
-
使用函数逼近解决 CartPole 问题
设置 Mountain Car 环境的游乐场
TD 方法可以在一个 episode 中学习 Q 函数,但不具备可扩展性。例如,国际象棋游戏的状态数约为 1,040 个,围棋游戏为 1,070 个。此外,使用 TD 方法学习连续状态的值似乎是不可行的。因此,我们需要使用**函数逼近(FA)**来解决这类问题,它使用一组特征来逼近状态空间。
在第一个示例中,我们将开始熟悉 Mountain Car 环境,我们将在接下来的示例中使用 FA 方法来解决它。
Mountain Car (gym.openai.com/envs/MountainCar-v0/) 是一个具有连续状态的典型 Gym 环境。如下图所示,其目标是将车辆驶上山顶:
在一维轨道上,车辆位于-1.2(最左侧)到 0.6(最右侧)之间,目标(黄旗)位于 0.5 处。车辆的引擎不足以使其在单次通过中驱动到顶部,因此它必须来回驾驶以积累动量。因此,每一步有三个离散动作:
-
向左推(0)
-
无推力(1)
-
向右推(2)
环境有两个状态:
-
车的位置:这是一个从-1.2 到 0.6 的连续变量。
-
车的速度:这是一个从-0.07 到 0.07 的连续变量。
每一步的奖励为-1,直到汽车达到目标位置(位置为 0.5)。
一集结束时,汽车到达目标位置(显然),或者经过 200 步之后。
准备工作
要运行山车环境,让我们首先在环境表中搜索其名称 – github.com/openai/gym/wiki/Table-of-environments。我们得到了MountainCar-v0,还知道观察空间由两个浮点数表示,有三种可能的动作(左=0,无推力=1,右=2)。
如何操作...
让我们按照以下步骤模拟山车环境:
- 我们导入 Gym 库并创建山车环境的一个实例:
>>> import gym
>>> env = gym.envs.make("MountainCar-v0")
>>> n_action = env.action_space.n
>>> print(n_action)
3
- 重置环境:
>>> env.reset()
array([-0.52354759, 0\. ])
汽车从状态[-0.52354759, 0.]开始,这意味着初始位置大约在-0.5,速度为 0。由于初始位置是从-0.6 到-0.4 随机生成的,你可能会看到不同的初始位置。
- 现在让我们采取一种简单的方法:我们只需不断向右推车,希望它能够到达山顶:
>>> is_done = False
>>> while not is_done:
... next_state, reward, is_done, info = env.step(2)
... print(next_state, reward, is_done)
... env.render()
>>> env.render()
[-0.49286453 0.00077561] -1.0 False
[-0.4913191 0.00154543] -1.0 False
[-0.48901538 0.00230371] -1.0 False
[-0.48597058 0.0030448 ] -1.0 False
......
......
[-0.29239555 -0.0046231 ] -1.0 False
[-0.29761694 -0.00522139] -1.0 False
[-0.30340632 -0.00578938] -1.0 True
- 关闭环境:
env.close()
工作原理...
在Step 3中,状态(位置和速度)会相应地改变,每一步的奖励是-1。
你也会在视频中看到,汽车反复向右移动,然后回到左边,但最终未能到达山顶:
正如你所想象的那样,山车问题并不像你想象的那么简单。我们需要来回驾驶汽车以积累动量。而状态变量是连续的,这意味着表格查找/更新方法(如 TD 方法)不起作用。在下一个配方中,我们将使用 FA 方法解决山车问题。
使用梯度下降逼近估计 Q 函数
从这个配方开始,我们将开发 FA 算法来解决具有连续状态变量的环境。我们将从使用线性函数和梯度下降逼近 Q 函数开始。
FA的主要思想是使用一组特征来估计 Q 值。这对于具有大状态空间的过程非常有用,其中 Q 表格变得非常庞大。有几种方法可以将特征映射到 Q 值上;例如,线性逼近是特征的线性组合和神经网络。通过线性逼近,动作的状态值函数可以用特征的加权和表示:
在这里,F1(s),F2(s),……,Fn(s)是给定输入状态 s 的一组特征;θ1,θ2,……,θn 是应用于相应特征的权重。或者我们可以将其表示为 V(s)=θF(s)。
正如我们在 TD 方法中所见,我们有以下公式来计算未来的状态:
在这里,r 是从状态 st 转换到 st+1 获得的相关奖励,α是学习率,γ是折扣因子。让我们将δ表示为 TD 误差项,现在我们有以下内容:
这与梯度下降的确切形式相同。因此,学习的目标是找到最优权重θ,以最佳方式逼近每个可能动作的状态值函数 V(s)。在这种情况下,我们尝试最小化的损失函数类似于回归问题中的损失函数,即实际值和估计值之间的均方误差。在每个 episode 的每一步之后,我们都有一个真实状态值的新估计,并且我们将权重θ朝向它们的最优值前进一步。
还要注意的一件事是特征集 F(s),给定输入状态 s。一个好的特征集能够捕捉不同输入的动态。通常,我们可以在各种参数下使用一组高斯函数生成一组特征,包括均值和标准差。
如何做到…
我们基于线性函数开发 Q 函数的逼近器如下:
- 导入所有必要的包:
>>> import torch
>>> from torch.autograd import Variable
>>> import math
变量包装了张量并支持反向传播。
- 然后,启动线性函数的
Estimator类的__init__method:
>>> class Estimator():
... def __init__(self, n_feat, n_state, n_action, lr=0.05):
... self.w, self.b = self.get_gaussian_wb(n_feat, n_state)
... self.n_feat = n_feat
... self.models = []
... self.optimizers = []
... self.criterion = torch.nn.MSELoss()
... for _ in range(n_action):
... model = torch.nn.Linear(n_feat, 1)
... self.models.append(model)
... optimizer = torch.optim.SGD(model.parameters(), lr)
... self.optimizers.append(optimizer)
它接受三个参数:特征数量n_feat,状态数量和动作数量。它首先从高斯分布生成特征函数 F(s)的一组系数w和b,稍后我们将定义。然后初始化n_action个线性模型,其中每个模型对应一个动作,并相应地初始化n_action个优化器。对于线性模型,我们在此处使用 PyTorch 的 Linear 模块。它接受n_feat个单元并生成一个输出,即一个动作的预测状态值。随机梯度下降优化器也与每个线性模型一起初始化。每个优化器的学习率为 0.05。损失函数是均方误差。
- 我们现在继续定义
get_gaussian_wb方法,它生成特征函数 F(s)的一组系数w和b:
>>> def get_gaussian_wb(self, n_feat, n_state, sigma=.2):
... """
... Generate the coefficients of the feature set from
Gaussian distribution
... @param n_feat: number of features
... @param n_state: number of states
... @param sigma: kernel parameter
... @return: coefficients of the features
... """
... torch.manual_seed(0)
... w = torch.randn((n_state, n_feat)) * 1.0 / sigma
... b = torch.rand(n_feat) * 2.0 * math.pi
... return w, b
系数w是一个n_feat乘以n_state的矩阵,其值从由参数 sigma 定义的方差高斯分布生成;偏置b是从[0, 2π]均匀分布生成的n_feat值的列表。
注意,设置特定的随机种子(torch.manual_seed(0))非常重要,这样在不同运行中,状态始终可以映射到相同的特征。
- 接下来,我们开发将状态空间映射到特征空间的函数,基于
w和b:
>>> def get_feature(self, s):
... """
... Generate features based on the input state
... @param s: input state
... @return: features
... """
... features = (2.0 / self.n_feat) ** .5 * torch.cos(
torch.matmul(torch.tensor(s).float(), self.w)
+ self.b)
... return features
状态 s 的特征生成如下:
使用余弦变换确保特征在[-1, 1]范围内,尽管输入状态的值可能不同。
- 由于我们已经定义了模型和特征生成,现在我们开发训练方法,用数据点更新线性模型:
>>> def update(self, s, a, y):
... """
... Update the weights for the linear estimator with
the given training sample
... @param s: state
... @param a: action
... @param y: target value
... """
... features = Variable(self.get_feature(s))
... y_pred = self.modelsa
... loss = self.criterion(y_pred,
Variable(torch.Tensor([y])))
... self.optimizers[a].zero_grad()
... loss.backward()
... self.optimizers[a].step()
给定一个训练数据点,它首先使用get_feature方法将状态转换为特征空间。然后将生成的特征馈送到给定动作a的当前线性模型中。预测结果连同目标值用于计算损失和梯度。然后通过反向传播更新权重θ。
- 下一个操作涉及使用当前模型预测每个动作在给定状态下的状态值:
>>> def predict(self, s):
... """
... Compute the Q values of the state using
the learning model
... @param s: input state
... @return: Q values of the state
... """
... features = self.get_feature(s)
... with torch.no_grad():
... return torch.tensor([model(features)
for model in self.models])
这就是关于Estimator类的全部内容。
- 现在,让我们玩弄一些虚拟数据。首先,创建一个
Estimator对象,将一个二维状态映射到一个十维特征,并与一个可能的动作配合使用:
>>> estimator = Estimator(10, 2, 1)
- 现在,生成状态[0.5, 0.1]的特征。
>>> s1 = [0.5, 0.1]
>>> print(estimator.get_feature(s1))
tensor([ 0.3163, -0.4467, -0.0450, -0.1490, 0.2393, -0.4181, -0.4426, 0.3074,
-0.4451, 0.1808])
正如您所看到的,生成的特征是一个 10 维向量。
- 对一系列状态和目标状态值(在本例中我们只有一个动作)进行估算器训练:
>>> s_list = [[1, 2], [2, 2], [3, 4], [2, 3], [2, 1]]
>>> target_list = [1, 1.5, 2, 2, 1.5]
>>> for s, target in zip(s_list, target_list):
... feature = estimator.get_feature(s)
... estimator.update(s, 0, target)
- 最后,我们使用训练好的线性模型来预测新状态的值:
>>> print(estimator.predict([0.5, 0.1]))
tensor([0.6172])
>>> print(estimator.predict([2, 3]))
tensor([0.8733])
对于状态[0.5, 0.1],预测的值与动作为 0.5847,而对于[2, 3],预测的值为 0.7969。
工作原理如下……
FA 方法通过比 TD 方法中的 Q 表计算更紧凑的模型来近似状态值。FA 首先将状态空间映射到特征空间,然后使用回归模型估算 Q 值。通过这种方式,学习过程变成了监督学习。类型回归模型包括线性模型和神经网络。在本文中,我们开发了一个基于线性回归的估算器。它根据从高斯分布中采样的系数生成特征。它通过梯度下降更新线性模型的权重,并根据状态预测 Q 值。
FA 显著减少了需要学习的状态数量,在 TD 方法中学习数百万个状态是不可行的。更重要的是,它能够推广到未见的状态,因为状态值是由给定输入状态的估计函数参数化的。
另请参阅
如果您对线性回归或梯度下降不熟悉,请查看以下资料:
开发具有线性函数逼近的 Q-learning
在前一篇文章中,我们基于线性回归开发了一个值估算器。我们将在 Q-learning 中使用这个估算器,作为我们 FA 旅程的一部分。
正如我们所看到的,Q-learning 是一种离线学习算法,它基于以下方程更新 Q 函数:
这里,s'是在状态s中采取动作a后得到的结果状态;r是相关的奖励;α是学习率;γ是折扣因子。此外,![] 表示行为策略是贪婪的,即在状态s'中选择最高的 Q 值以生成学习数据。在 Q-learning 中,根据ε-greedy 策略采取行动。同样地,Q-learning 与 FA 具有以下误差项:
我们的学习目标是将误差项最小化为零,这意味着估算的 V(st)应满足以下方程:
现在,目标是找到最优权重θ,例如 V(s)=θF(s),以最佳方式逼近每个可能动作的状态值函数 V(s)。在这种情况下,我们试图最小化的损失函数类似于回归问题中的损失函数,即实际值和估算值之间的均方误差。
如何做到…
让我们使用前一篇文章中开发的线性估算器linear_estimator.py中的Estimator,开发 Q-learning 与 FA:
- 导入必要的模块并创建一个 Mountain Car 环境:
>>> import gym
>>> import torch
>>> from linear_estimator import Estimator >>> env = gym.envs.make("MountainCar-v0")
- 然后,开始定义ε-greedy 策略:
>>> def gen_epsilon_greedy_policy(estimator, epsilon, n_action):
... def policy_function(state):
... probs = torch.ones(n_action) * epsilon / n_action
... q_values = estimator.predict(state)
... best_action = torch.argmax(q_values).item()
... probs[best_action] += 1.0 - epsilon
... action = torch.multinomial(probs, 1).item()
... return action
... return policy_function
这里的参数ε取值从 0 到 1,|A|表示可能的动作数,估算器用于预测状态-动作值。每个动作以ε/ |A|的概率被选中,而具有最高预测状态-动作值的动作则以 1- ε + ε/ |A|的概率被选中。
- 现在,定义执行使用线性估算器
Estimator的 Q-learning 的函数:
>>> def q_learning(env, estimator, n_episode, gamma=1.0,
epsilon=0.1, epsilon_decay=.99):
... """
... Q-Learning algorithm using Function Approximation
... @param env: Gym environment
... @param estimator: Estimator object
... @param n_episode: number of episodes
... @param gamma: the discount factor
... @param epsilon: parameter for epsilon_greedy
... @param epsilon_decay: epsilon decreasing factor
... """
... for episode in range(n_episode):
... policy = gen_epsilon_greedy_policy(estimator,
epsilon * epsilon_decay ** episode, n_action)
... state = env.reset()
... is_done = False
... while not is_done:
... action = policy(state)
... next_state, reward, is_done, _ = env.step(action)
... q_values_next = estimator.predict(next_state)
... td_target = reward +
gamma * torch.max(q_values_next)
... estimator.update(state, action, td_target)
... total_reward_episode[episode] += reward
...
... if is_done:
... break
... state = next_state
q_learning()函数执行以下任务:
-
在每个 episode 中,创建一个ε-greedy 策略,其中ε因子衰减到 99%(例如,如果第一个 episode 中的ε为 0.1,则第二个 episode 中的ε将为 0.099)。
-
运行一个 episode:在每一步中,根据ε-greedy 策略采取一个动作a;使用当前的估算器计算新状态的Q值;然后计算目标值,![],并用它来训练估算器。
-
运行
n_episode个 episode 并记录每个 episode 的总奖励。
- 我们指定特征数量为
200,学习率为0.03,并相应地创建一个估算器:
>>> n_state = env.observation_space.shape[0]
>>> n_action = env.action_space.n
>>> n_feature = 200
>>> lr = 0.03 >>> estimator = Estimator(n_feature, n_state, n_action, lr)
- 我们使用 FA 进行 300 个 episode 的 Q-learning,并且记录每个 episode 的总奖励:
>>> n_episode = 300
>>> total_reward_episode = [0] * n_episode
>>> q_learning(env, estimator, n_episode, epsilon=0.1)
- 然后,我们显示随时间变化的 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()
工作原理是…
如您所见,在 Q 学习中使用函数逼近时,它尝试学习最佳权重以便最佳估计 Q 值。它与 TD Q 学习类似,因为它们都从另一个策略生成学习数据。对于具有大状态空间的环境,Q 学习使用一组回归模型和潜在特征来近似 Q 值,而 TD Q 学习则需要精确的表查找来更新 Q 值。Q 学习使用函数逼近在每一步之后更新回归模型,这也使它类似于 TD Q 学习方法。
在训练 Q 学习模型后,我们只需使用回归模型预测所有可能动作的状态-动作值,并在给定状态时选择值最大的动作。在步骤 6中,我们导入pyplot以绘制所有奖励,结果如下图所示:
您可以看到,在大多数情况下,经过前 25 次迭代后,汽车在约 130 到 160 步内到达山顶。
开发使用线性函数逼近的 SARSA
我们在前面的步骤中使用了离策略 Q 学习算法成功解决了 Mountain Car 问题。现在,我们将使用状态-动作-奖励-状态-动作(SARSA)算法(当然是 FA 版本)来完成此任务。
一般来说,SARSA 算法根据以下方程更新 Q 函数:
这里,s'是在状态s中采取动作a后的结果状态;r是相关奖励;α是学习率;γ是折扣因子。我们通过遵循ε-greedy 策略来选择下一个动作a'来更新Q值。然后在下一步中执行动作a'。因此,带有函数逼近的 SARSA 具有以下误差项:
我们的学习目标是将误差项最小化为零,这意味着估计的 V(st)应满足以下方程:
现在,目标是找到最优权重θ,如 V(s)=θF(s),以最佳方式逼近每个可能动作的状态值函数 V(s)。在这种情况下,我们试图最小化的损失函数类似于回归问题中的损失函数,即实际值与估计值之间的均方误差。
如何做…
让我们使用在用梯度下降逼近估算 Q 函数食谱中开发的线性估计器linear_estimator.py中的Estimator,来开发使用线性估计的 SARSA。
- 导入必要的模块并创建一个 Mountain Car 环境:
>>> import gym
>>> import torch
>>> from linear_estimator import Estimator >>> env = gym.envs.make("MountainCar-v0")
-
我们将重用上一步骤中开发的ε-greedy 策略函数,使用线性函数逼近开发 Q 学习。
-
现在,定义执行带有函数逼近的 SARSA 算法的函数:
>>> def sarsa(env, estimator, n_episode, gamma=1.0,
epsilon=0.1, epsilon_decay=.99):
... """
... SARSA algorithm using Function Approximation
... @param env: Gym environment
... @param estimator: Estimator object
... @param n_episode: number of episodes
... @param gamma: the discount factor
... @param epsilon: parameter for epsilon_greedy
... @param epsilon_decay: epsilon decreasing factor
... """
... for episode in range(n_episode):
... policy = gen_epsilon_greedy_policy(estimator,
epsilon * epsilon_decay ** episode,
env.action_space.n)
... state = env.reset()
... action = policy(state)
... is_done = False
...
... while not is_done:
... next_state, reward, done, _ = env.step(action)
... q_values_next = estimator.predict(next_state)
... next_action = policy(next_state)
... td_target = reward +
gamma * q_values_next[next_action]
... estimator.update(state, action, td_target)
... total_reward_episode[episode] += reward
...
... if done:
... break
... state = next_state
... action = next_action
sarsa()函数执行以下任务:
-
在每一集中,创建一个带有衰减至 99%的ε-greedy 策略。
-
运行一个 episode:在每一步中,根据ε-greedy 策略选择一个动作a;在新状态中,根据ε-greedy 策略选择一个新动作;然后,使用当前估算器计算新状态的 Q 值;计算目标值![],并用它来更新估算器。
-
运行
n_episode个 episode 并记录每个 episode 的总奖励。
- 我们将特征数指定为 200,学习率为 0.03,并相应地创建一个估算器:
>>> n_state = env.observation_space.shape[0]
>>> n_action = env.action_space.n
>>> n_feature = 200
>>> lr = 0.03
>>> estimator = Estimator(n_feature, n_state, n_action, lr)
- 然后我们对 FA 执行 300 个 episode 的 SARSA,并且还跟踪每个 episode 的总奖励:
>>> n_episode = 300
>>> total_reward_episode = [0] * n_episode
>>> sarsa(env, estimator, n_episode, epsilon=0.1)
- 然后,我们显示随时间变化的 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()
工作原理是…
使用 FA 的 SARSA 尝试学习最佳权重以估算最佳的 Q 值。它通过选择在同一策略下选择的动作来优化估算,而不是像 Q-learning 中那样从另一种策略中学习经验。
类似地,训练完 SARSA 模型后,我们只需使用回归模型预测所有可能动作的状态-动作值,并在给定状态时选择具有最大值的动作。
在第 6 步中,我们使用pyplot绘制奖励,将得到以下图表:
您可以看到,在大多数 episode 中,经过前 100 个 episode 后,汽车在大约 130 到 160 步内到达山顶。
使用经验重放来进行批处理整合
在前两个配方中,我们分别开发了两种 FA 学习算法:离线策略和在线策略。在本配方中,我们将通过引入经验重放来提高离线 Q-learning 的性能。
经验重放意味着我们在一个 episode 期间存储 agent 的经验,而不是运行 Q-learning。带有经验重放的学习阶段变成了两个阶段:获得经验和在 episode 完成后根据获得的经验更新模型。具体来说,经验(也称为缓冲区或内存)包括个别步骤中的过去状态、执行的动作、接收的奖励和下一个状态。
在学习阶段中,从经验中随机采样一定数量的数据点,并用于训练学习模型。经验重放可以通过提供一组低相关性样本来稳定训练,从而提高学习效率。
如何做…
让我们将经验重放应用于使用线性估算器Estimator的 FA Q-learning,该估算器来自我们在上一个配方中开发的使用梯度下降逼近估算 Q 函数:
- 导入必要的模块并创建一个 Mountain Car 环境:
>>> import gym
>>> import torch
>>> from linear_estimator import Estimator
>>> from collections import deque
>>> import random >>> env = gym.envs.make("MountainCar-v0")
-
我们将在前一节开发带有线性函数近似的 Q-learning中开发的ε-greedy 策略函数进行重用。
-
然后,将特征数量指定为
200,学习率指定为0.03,并相应地创建估算器:
>>> n_state = env.observation_space.shape[0]
>>> n_action = env.action_space.n
>>> n_feature = 200
>>> lr = 0.03
>>> estimator = Estimator(n_feature, n_state, n_action, lr)
- 接下来,定义保存经验的缓冲区:
>>> memory = deque(maxlen=400)
将新样本追加到队列中,并在队列中有超过 400 个样本时移除旧样本。
- 现在,定义执行带有经验重播 FA Q-learning 的函数:
>>> def q_learning(env, estimator, n_episode, replay_size,
gamma=1.0, epsilon=0.1, epsilon_decay=.99):
... """
... Q-Learning algorithm using Function Approximation,
with experience replay
... @param env: Gym environment
... @param estimator: Estimator object
... @param replay_size: number of samples we use to
update the model each time
... @param n_episode: number of episode
... @param gamma: the discount factor
... @param epsilon: parameter for epsilon_greedy
... @param epsilon_decay: epsilon decreasing factor
... """
... for episode in range(n_episode):
... policy = gen_epsilon_greedy_policy(estimator,
epsilon * epsilon_decay ** episode,
n_action)
... state = env.reset()
... is_done = False
... while not is_done:
... action = policy(state)
... next_state, reward, is_done, _ = env.step(action)
... total_reward_episode[episode] += reward
... if is_done:
... break
...
... q_values_next = estimator.predict(next_state)
... td_target = reward +
gamma * torch.max(q_values_next)
... memory.append((state, action, td_target))
... state = next_state
...
... replay_data = random.sample(memory,
min(replay_size, len(memory)))
... for state, action, td_target in replay_data:
... estimator.update(state, action, td_target)
该函数执行以下任务:
-
在每个周期中,创建一个 epsilon-greedy 策略,其中 epsilon 因子衰减到 99%(例如,如果第一个周期的 epsilon 为 0.1,则第二个周期为 0.099)。
-
运行一个周期:在每个步骤中,根据 epsilon-greedy 策略选择一个动作a;使用当前估算器计算新状态的Q值;然后计算目标值,![],并将状态、动作和目标值元组存储在缓冲内存中。
-
每个周期结束后,从缓冲内存中随机选择
replay_size个样本,并使用它们来训练估算器。 -
运行
n_episode个周期,并记录每个周期的总奖励。
- 我们执行了 1,000 个周期的经验重播 Q-learning:
>>> n_episode = 1000
我们需要更多的周期,仅仅因为模型尚未充分训练,所以代理在早期周期中采取随机步骤。
我们将 190 设置为重播样本大小:
>>> replay_size = 190
我们还会跟踪每个周期的总奖励:
>>> total_reward_episode = [0] * n_episode
>>> q_learning(env, estimator, n_episode, replay_size, epsilon=0.1)
- 现在,我们展示随时间变化的情节长度的图表:
>>> 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()
这将导致以下图表:
您可以看到使用经验重播的 Q-learning 性能变得更加稳定。在第一个 500 个周期后,大多数周期的奖励保持在-160 至-120 的范围内。
运作方式...
在这个示例中,我们使用 FA Q-learning 解决了 Mountain Car 问题,同时使用了经验重播。它比纯 FA Q-learning 表现更好,因为我们使用经验重播收集了更少的校正训练数据。我们不会急于训练估算器,而是首先将在周期期间观察到的数据点存储在缓冲区中,然后我们从缓冲区中随机选择一个样本批次并训练估算器。这形成了一个输入数据集,其中样本之间更独立,从而使训练更加稳定和高效。
开发带有神经网络函数逼近的 Q-learning
正如前文所述,我们还可以使用神经网络作为逼近函数。在这个示例中,我们将使用神经网络对 Q-learning 进行逼近来解决 Mountain Car 环境问题。
FA 的目标是使用一组特征通过回归模型估计 Q 值。使用神经网络作为估算模型,通过在隐藏层中引入非线性激活增加回归模型的灵活性(多层神经网络)和非线性。Q-learning 模型的其余部分与线性逼近非常相似。我们还使用梯度下降来训练网络。学习的最终目标是找到网络的最优权重,以最佳逼近每个可能动作的状态值函数 V(s)。我们试图最小化的损失函数也是实际值与估计值之间的均方误差。
如何做到这一点…
让我们从实现基于神经网络的估计器开始。我们将重用我们在使用梯度下降逼近估算 Q 函数一节中开发的线性估计器的大部分部分。不同之处在于,我们将输入层和输出层连接到一个隐藏层,然后是一个激活函数,在这种情况下是一个 ReLU(修正线性单元)函数。因此,我们只需要按照以下方式修改__init__方法:
>>> class Estimator():
... def __init__(self, n_feat, n_state, n_action, lr=0.05):
... self.w, self.b = self.get_gaussian_wb(n_feat, n_state)
... self.n_feat = n_feat
... self.models = []
... self.optimizers = []
... self.criterion = torch.nn.MSELoss()
... for _ in range(n_action):
... model = torch.nn.Sequential(
... torch.nn.Linear(n_feat, n_hidden),
... torch.nn.ReLU(),
... torch.nn.Linear(n_hidden, 1)
... )
... self.models.append(model)
... optimizer = torch.optim.Adam(model.parameters(), lr)
... self.optimizers.append(optimizer)
正如你所见,隐藏层有n_hidden个节点,以及一个 ReLU 激活函数torch.nn.ReLU(),在隐藏层之后,接着是生成估算值的输出层。
神经网络Estimator的其他部分与线性Estimator相同。你可以将它们复制到nn_estimator.py文件中。
现在,我们继续使用经验回放的神经网络进行 Q-learning 如下:
- 导入必要的模块,包括我们刚刚开发的神经网络估计器
Estimator,从nn_estimator.py中,并创建一个 Mountain Car 环境:
>>> import gym
>>> import torch
>>> from nn_estimator import Estimator
>>> from collections import deque
>>> import random >>> env = gym.envs.make("MountainCar-v0")
-
我们将重用在开发带有线性函数逼近的 Q-learning一节中开发的 epsilon-贪婪策略函数。
-
接着,我们将特征数设定为 200,学习率设定为 0.001,隐藏层大小设定为 50,并相应地创建一个估计器:
>>> n_state = env.observation_space.shape[0]
>>> n_action = env.action_space.n
>>> n_feature = 200
>>> n_hidden = 50
>>> lr = 0.001
>>> estimator = Estimator(n_feature, n_state, n_action, n_hidden, lr)
- 接下来,定义保存经验的缓冲区:
>>> memory = deque(maxlen=300)
新样本将被附加到队列中,只要队列中有超过 300 个样本,旧样本就会被移除。
-
我们将重用我们在前一节使用经验回放进行批处理中开发的
q_learning函数。它执行带有经验回放的 FA Q-learning。 -
我们进行经验回放的 Q-learning,共 1,000 个 episodes,并将 200 设置为回放样本大小。
>>> n_episode = 1000
>>> replay_size = 200
我们还会跟踪每个 episode 的总奖励:
>>> total_reward_episode = [0] * n_episode
>>> q_learning(env, estimator, n_episode, replay_size, epsilon=0.1)
- 然后,我们显示随时间变化的 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()
工作原理…
使用神经网络进行 FA 非常类似于线性函数逼近。它不再使用简单的线性函数,而是使用神经网络将特征映射到目标值。算法的其余部分基本相同,但由于神经网络的更复杂结构和非线性激活,具有更高的灵活性和更强的预测能力。
在第 7 步中,我们绘制随时间变化的情节长度图表,结果如下:
您可以看到,与使用线性函数相比,使用神经网络的 Q-learning 性能更好。在第一个 500 个情节之后,大多数情节的奖励保持在-140 到-85 的范围内。
参见
如果您想了解有关神经网络的知识,请查看以下材料:
-
pytorch.org/tutorials/beginner/blitz/neural_networks_tutorial.html -
www.cs.toronto.edu/~jlucas/teaching/csc411/lectures/tut5_handout.pdf
使用函数逼近解决 CartPole 问题
这是本章的一个额外配方,在这里我们将使用 FA 解决 CartPole 问题。
正如我们在第一章中看到的,开始使用强化学习和 PyTorch,我们在模拟 CartPole 环境配方中模拟了 CartPole 环境,并分别使用随机搜索、爬山和策略梯度算法解决了环境,包括实施和评估随机搜索策略、开发爬山算法和开发策略梯度算法。现在,让我们尝试使用本章讨论的内容解决 CartPole 问题。
如何做到...
我们演示了基于神经网络的 FA 解决方案,没有经验重演如下:
- 导入必要的模块,包括神经网络
Estimator,从我们在上一个配方中开发的nn_estimator.py中创建 CartPole 环境:
>>> import gym
>>> import torch
>>> from nn_estimator import Estimator >>> env = gym.envs.make("CartPole-v0")
-
我们将重用在上一个配方中开发的 epsilon-greedy 策略函数,使用线性函数逼近开发 Q-learning。
-
我们然后指定特征数量为 400(请注意 CartPole 环境的状态空间是 4 维),学习率为 0.01,隐藏层大小为 100,并相应地创建神经网络估计器:
>>> n_state = env.observation_space.shape[0]
>>> n_action = env.action_space.n
>>> n_feature = 400
>>> n_hidden = 100
>>> lr = 0.01
>>> estimator = Estimator(n_feature, n_state, n_action, n_hidden, lr)
-
我们将重用在上一个配方中开发的
q_learning函数,使用线性函数逼近开发 Q-learning。这执行 FA Q-learning。 -
我们进行了 1,000 个情节的 FA Q-learning,并跟踪每个情节的总奖励:
>>> n_episode = 1000
>>> total_reward_episode = [0] * n_episode
>>> q_learning(env, estimator, n_episode, epsilon=0.1)
- 最后,我们展示随时间变化的情节长度的图表:
>>> 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()
它的工作原理...
我们使用神经网络中的 FA 算法解决了 CartPole 问题。请注意,环境具有四维观测空间,是 Mountain Car 的两倍,因此我们直观地增加了我们使用的特征数和隐藏层的大小。可以自由地尝试使用神经网络的 SARSA 或经验回放的 Q-learning,并查看它们是否表现更好。
在第 6 步中,我们绘制了随时间变化的集数长度,结果如下图所示:
大多数情况下,从第 300 集后的总奖励值为最大值+200。
第七章:深度 Q 网络的实际应用
深度 Q 学习,或使用深度 Q 网络,被认为是最现代的强化学习技术。在本章中,我们将逐步开发各种深度 Q 网络模型,并将其应用于解决几个强化学习问题。我们将从基本的 Q 网络开始,并通过经验重播来增强它们。我们将通过使用额外的目标网络来提高鲁棒性,并演示如何微调深度 Q 网络。我们还将尝试决斗深度 Q 网络,并看看它们的价值函数如何与其他类型的深度 Q 网络不同。在最后两个实例中,我们将通过将卷积神经网络整合到深度 Q 网络中来解决复杂的 Atari 游戏问题。
本章将介绍以下内容:
-
开发深度 Q 网络
-
通过经验重播改进 DQN
-
开发双重深度 Q 网络
-
调整 CartPole 的双重 DQN 超参数
-
开发决斗深度 Q 网络
-
将深度 Q 网络应用于 Atari 游戏
-
使用卷积神经网络玩 Atari 游戏
开发深度 Q 网络
您将回忆起函数逼近(FA)是使用从原始状态生成的一组特征来逼近状态空间。深度 Q 网络(DQN)与使用神经网络进行特征逼近非常相似,但它们直接使用神经网络将状态映射到动作值,而不是使用生成的特征作为媒介。
在深度 Q 学习中,神经网络被训练以输出每个动作给定输入状态 s 下的适当 Q(s,a) 值。根据 epsilon-greedy 策略选择代理的动作 a,基于输出 Q(s,a) 值。具有两个隐藏层的 DQN 结构如下图所示:
您将回忆起 Q 学习是一种离线学习算法,并且它根据以下方程更新 Q 函数:
在这里,s' 是在状态 s 中采取动作 a 后得到的结果状态;r 是相关的奖励;α 是学习率;γ 是折扣因子。同时,![] 表示行为策略是贪婪的,其中在状态 s' 中选择最高的 Q 值以生成学习数据。类似地,DQN 学习以最小化以下误差项:
现在,目标变成寻找最佳网络模型以最好地逼近每个可能动作的状态值函数 Q(s, a)。在这种情况下,我们试图最小化的损失函数类似于回归问题中的均方误差,即实际值和估计值之间的均方误差。
现在,我们将开发一个 DQN 模型来解决 Mountain Car(gym.openai.com/envs/MountainCar-v0/)问题。
怎么做……
我们使用 DQN 开发深度 Q 学习如下:
- 导入所有必要的包:
>>> import gym
>>> import torch
>>> from torch.autograd import Variable
>>> import random
变量包装了一个张量并支持反向传播。
- 让我们从
DQN类的__init__方法开始:
>>> class DQN():
... def __init__(self, n_state, n_action, 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, n_action)
... )
... self.optimizer = torch.optim.Adam( self.model.parameters(), lr)
- 现在我们开发训练方法,用于更新神经网络与数据点:
>>> def update(self, s, y):
... """
... Update the weights of the DQN given a training sample
... @param s: state
... @param y: target value
... """
... 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()
- 接下来是给定状态预测每个动作的状态值:
>>> 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))
这就是 DQN 类的全部内容!现在我们可以继续开发学习算法了。
- 我们开始创建一个 Mountain Car 环境:
>>> env = gym.envs.make("MountainCar-v0")
- 然后,我们定义 epsilon-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
- 现在,使用 DQN 定义深度 Q 学习算法:
>>> def q_learning(env, estimator, n_episode, gamma=1.0,
epsilon=0.1, epsilon_decay=.99):
... """
... Deep Q-Learning using DQN
... @param env: Gym environment
... @param estimator: Estimator object
... @param n_episode: number of episodes
... @param gamma: the discount factor
... @param epsilon: parameter for epsilon_greedy
... @param epsilon_decay: epsilon decreasing factor
... """
... for episode in range(n_episode):
... policy = gen_epsilon_greedy_policy( estimator, epsilon, n_action)
... state = env.reset()
... is_done = False
... while not is_done:
... action = policy(state)
... next_state, reward, is_done, _ = env.step(action)
... total_reward_episode[episode] += reward
... modified_reward = next_state[0] + 0.5
... if next_state[0] >= 0.5:
... modified_reward += 100
... elif next_state[0] >= 0.25:
... modified_reward += 20
... elif next_state[0] >= 0.1:
... modified_reward += 10
... elif next_state[0] >= 0:
... modified_reward += 5
...
... q_values = estimator.predict(state).tolist()
...
... if is_done:
... q_values[action] = modified_reward
... estimator.update(state, q_values)
... break
... q_values_next = estimator.predict(next_state)
... q_values[action] = modified_reward + gamma *
torch.max(q_values_next).item()
... estimator.update(state, q_values)
... state = next_state
... print('Episode: {}, total reward: {}, epsilon:
{}'.format(episode,
total_reward_episode[episode], epsilon))
... epsilon = max(epsilon * epsilon_decay, 0.01)
- 然后,我们指定隐藏层的大小和学习率,并相应地创建一个
DQN实例:
>>> n_state = env.observation_space.shape[0]
>>> n_action = env.action_space.n
>>> n_hidden = 50
>>> lr = 0.001
>>> dqn = DQN(n_state, n_action, n_hidden, lr)
- 接下来,我们使用刚开发的 DQN 进行 1,000 个回合的深度 Q 学习,并且还跟踪每个回合的总(原始)奖励:
>>> n_episode = 1000
>>> total_reward_episode = [0] * n_episode
>>> q_learning(env, dqn, n_episode, gamma=.99, epsilon=.3)
Episode: 0, total reward: -200.0, epsilon: 0.3
Episode: 1, total reward: -200.0, epsilon: 0.297
Episode: 2, total reward: -200.0, epsilon: 0.29402999999999996
……
……
Episode: 993, total reward: -177.0, epsilon: 0.01
Episode: 994, total reward: -200.0, epsilon: 0.01
Episode: 995, total reward: -172.0, epsilon: 0.01
Episode: 996, total reward: -200.0, epsilon: 0.01
Episode: 997, total reward: -200.0, epsilon: 0.01
Episode: 998, total reward: -173.0, epsilon: 0.01
Episode: 999, total reward: -200.0, epsilon: 0.01
- 现在,让我们展示随时间变化的回合奖励图:
>>> 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 中,DQN 类接受四个参数:输入状态数和输出动作数,隐藏节点数(我们这里只使用一个隐藏层作为示例),以及学习率。它初始化一个具有一个隐藏层的神经网络,并使用 ReLU 激活函数。它接收 n_state 个单位并生成一个 n_action 的输出,这些是各个动作的预测状态值。一个优化器 Adam 与每个线性模型一起初始化。损失函数是均方误差。
Step 3 用于更新网络:给定一个训练数据点,使用预测结果和目标值计算损失和梯度。然后通过反向传播更新神经网络模型。
在 Step 7 中,深度 Q 学习函数执行以下任务:
-
在每个回合中,创建一个 epsilon-greedy 策略,其中 epsilon 的因子衰减到 99%(例如,如果第一个回合中的 epsilon 是 0.1,则第二个回合中将为 0.099)。我们还将 0.01 设置为较低的 epsilon 限制。
-
运行一个回合:在状态 s 的每一步中,根据 epsilon-greedy 策略选择动作 a;然后,使用 DQN 的
predict方法计算上一个状态的 Q 值q_value。 -
计算新状态 s' 的 Q 值
q_values_next;然后,通过更新旧的 Q 值q_values来计算目标值,用于动作的。
-
使用数据点 (s, Q(s)) 来训练神经网络。注意 Q(s) 包含所有动作的值。
-
运行
n_episode个回合并记录每个回合的总奖励。
您可能注意到,我们在训练模型时使用了修改版奖励。它基于汽车的位置,因为我们希望它达到+0.5 的位置。而且,我们还为大于或等于+0.5、+0.25、+0.1 和 0 的位置提供分层激励。这种修改后的奖励设置区分了不同的汽车位置,并偏爱接近目标的位置;因此,与每步-1 的原始单调奖励相比,它大大加速了学习过程。
最后,在第 10 步,您将看到如下的结果图:
您可以看到,在最后的 200 个回合中,汽车在大约 170 到 180 步后达到山顶。
深度 Q 学习用神经网络而不是一组中间人工特征更直接地逼近状态值。给定一个步骤,其中旧状态通过采取行动转换为新状态,并接收奖励,训练 DQN 涉及以下阶段:
-
使用神经网络模型估计旧状态的Q值。
-
使用神经网络模型估计新状态的Q值。
-
使用奖励和新的Q值更新动作的目标 Q 值,如![]
-
注意,如果是终端状态,目标 Q 值将更新为 r。
-
使用旧状态作为输入,并将目标Q值作为输出训练神经网络模型。
它通过梯度下降更新网络的权重,并能预测给定状态的 Q 值。
DQN 显著减少了需要学习的状态数量,在 TD 方法中学习数百万个状态是不可行的。此外,它直接将输入状态映射到 Q 值,并不需要生成人工特征的额外函数。
另见
如果您对 Adam 优化器作为高级梯度下降方法不熟悉,请查看以下资料:
使用经验重播改进 DQNs
使用神经网络逐个样本逼近 Q 值的近似并不非常稳定。您将回忆起,在 FA 中,我们通过经验重播来提高稳定性。同样,在这个配方中,我们将应用经验重播到 DQNs 中。
使用经验回放,我们在训练会话的每个周期内存储代理的经验(一个经验由旧状态、新状态、动作和奖励组成)到内存队列中。每当我们积累到足够的经验时,从内存中随机抽取一批经验,并用于训练神经网络。学习经验回放分为两个阶段:积累经验和基于随机选择的过去经验更新模型。否则,模型将继续从最近的经验中学习,神经网络模型可能会陷入局部最小值。
我们将开发具有经验回放功能的 DQN 来解决山车问题。
如何实现...
我们将如下开发具有经验回放的 DQN:
- 导入必要的模块并创建一个山车环境:
>>> import gym
>>> import torch
>>> from collections import deque
>>> import random
>>> from torch.autograd import Variable >>> env = gym.envs.make("MountainCar-v0")
- 要添加经验回放功能,我们将在
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
... """
... if len(memory) >= replay_size:
... replay_data = random.sample(memory, replay_size)
... states = []
... td_targets = []
... for state, action, next_state, reward,
is_done in replay_data:
... states.append(state)
... q_values = self.predict(state).tolist()
... if is_done:
... q_values[action] = reward
... else:
... q_values_next = self.predict(next_state)
... q_values[action] = reward + gamma *
torch.max(q_values_next).item()
... td_targets.append(q_values)
...
... self.update(states, td_targets)
DQN类的其余部分保持不变。
-
我们将重用我们在开发深度 Q 网络配方中开发的
gen_epsilon_greedy_policy函数,这里不再重复。 -
然后,我们指定神经网络的形状,包括输入的大小、输出和隐藏层的大小,将学习率设置为 0.001,并相应地创建一个 DQN:
>>> n_state = env.observation_space.shape[0]
>>> n_action = env.action_space.n
>>> n_hidden = 50
>>> lr = 0.001
>>> dqn = DQN(n_state, n_action, n_hidden, lr)
- 接下来,我们定义存储经验的缓冲区:
>>> memory = deque(maxlen=10000)
如果队列中的样本超过10000个,则将新样本附加到队列中,并删除旧样本。
- 现在,我们定义执行经验回放的深度 Q 学习函数:
>>> def q_learning(env, estimator, n_episode, replay_size,
gamma=1.0, epsilon=0.1, epsilon_decay=.99):
... """
... Deep Q-Learning using DQN, with experience replay
... @param env: Gym environment
... @param estimator: Estimator object
... @param replay_size: the number of samples we use to
update the model each time
... @param n_episode: number of episodes
... @param gamma: the discount factor
... @param epsilon: parameter for epsilon_greedy
... @param epsilon_decay: epsilon decreasing factor
... """
... for episode in range(n_episode):
... policy = gen_epsilon_greedy_policy( estimator, epsilon, n_action)
... state = env.reset()
... is_done = False
... while not is_done:
... action = policy(state)
... next_state, reward, is_done, _ = env.step(action)
... total_reward_episode[episode] += reward
... modified_reward = next_state[0] + 0.5
... if next_state[0] >= 0.5:
... modified_reward += 100
... elif next_state[0] >= 0.25:
... modified_reward += 20
... elif next_state[0] >= 0.1:
... modified_reward += 10
... elif next_state[0] >= 0:
... modified_reward += 5
... memory.append((state, action, next_state,
modified_reward, is_done))
... if is_done:
... break
... estimator.replay(memory, replay_size, gamma)
... state = next_state
... print('Episode: {}, total reward: {}, epsilon:
{}'.format(episode, total_reward_episode[episode],
epsilon))
... epsilon = max(epsilon * epsilon_decay, 0.01)
- 然后,我们为
600个周期执行具有经验回放的深度 Q 学习:
>>> n_episode = 600
我们将20设置为每步的重放样本大小:
>>> replay_size = 20
我们还跟踪每个周期的总奖励:
>>> total_reward_episode = [0] * n_episode
>>> q_learning(env, dqn, n_episode, replay_size, gamma=.9, epsilon=.3)
- 现在,是时候显示随时间变化的周期奖励图了:
>>> 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 步中,经验回放函数首先随机选择replay_size个经验样本。然后,将每个经验转换为由输入状态和输出目标值组成的训练样本。最后,使用选定的批量更新神经网络。
在第 6 步中,执行具有经验回放的深度 Q 学习,包括以下任务:
-
在每个周期中,创建一个带有衰减到 99%的ε贪婪策略。
-
运行一个周期:在每一步中,根据ε-贪婪策略选择一个动作a;将这一经验(旧状态、动作、新状态、奖励)存储在内存中。
-
在每一步中,进行经验回放来训练神经网络,前提是我们有足够的训练样本可以随机选择。
-
运行
n_episode个周期,并记录每个周期的总奖励。
执行第 8 步中的代码行将产生以下图表:
您可以看到,在最后 200 个周期的大多数周期中,车在大约 120 到 160 步内达到了山顶。
在深度 Q 学习中,经验重放意味着我们为每一步存储代理的经验,并随机抽取过去经验的一些样本来训练 DQN。 在这种情况下,学习分为两个阶段:积累经验和基于过去经验批次更新模型。 具体而言,经验(也称为缓冲区或内存)包括过去的状态、采取的动作、获得的奖励和下一个状态。 经验重放可以通过提供一组低相关性的样本来稳定训练,从而增加学习效率。
开发双深度 Q 网络
在我们迄今开发的深度 Q 学习算法中,同一个神经网络用于计算预测值和目标值。 这可能导致很多发散,因为目标值不断变化,而预测必须追赶它。 在这个配方中,我们将开发一种新的算法,使用两个神经网络代替一个。
在双重 DQN中,我们使用一个单独的网络来估计目标,而不是预测网络。 这个单独的网络与预测网络具有相同的结构。 其权重在每个T集后固定(T是我们可以调整的超参数),这意味着它们仅在每个T集后更新。 更新是通过简单地复制预测网络的权重来完成的。 这样,目标函数在一段时间内保持不变,从而导致更稳定的训练过程。
数学上,双重 DQN 被训练来最小化以下误差项:
在这里,s'是采取行动a后的结果状态,r是相关的奖励;α是学习率;γ是折扣因子。 另外,![]是目标网络的函数,而 Q 是预测网络的函数。
现在让我们使用双重 DQN 来解决 Mountain Car 问题。
如何做…
我们按以下方式开发使用双重 DQN 的深度 Q 学习:
- 导入必要的模块并创建一个 Mountain Car 环境:
>>> import gym
>>> import torch
>>> from collections import deque
>>> import random
>>> import copy
>>> from torch.autograd import Variable >>> env = gym.envs.make("MountainCar-v0")
- 在经验重放阶段,为了整合目标网络,我们首先在
DQN类的__init__方法中对其进行初始化:
>>> class DQN():
... def __init__(self, n_state, n_action,
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, n_action)
... )
... self.optimizer = torch.optim.Adam( self.model.parameters(), lr)
... self.model_target = copy.deepcopy(self.model)
目标网络与预测网络具有相同的结构。
- 因此,我们添加了使用目标网络计算值的计算:
>>> def target_predict(self, s):
... """
... Compute the Q values of the state for all actions
using the target network
... @param s: input state
... @return: targeted Q values of the state for all actions
... """
... with torch.no_grad():
... return self.model_target(torch.Tensor(s))
- 我们还添加了同步目标网络权重的方法:
>>> def copy_target(self):
... self.model_target.load_state_dict(self.model.state_dict())
- 在经验重放中,我们使用目标网络来计算目标值,而不是预测网络:
>>> def replay(self, memory, replay_size, gamma):
... """
... Experience replay with target network
... @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
... """
... if len(memory) >= replay_size:
... replay_data = random.sample(memory, replay_size)
... states = []
... td_targets = []
... for state, action, next_state, reward, is_done
in replay_data:
... states.append(state)
... q_values = self.predict(state).tolist()
... if is_done:
... q_values[action] = reward
... else:
... q_values_next = self.target_predict( next_state).detach()
... q_values[action] = reward + gamma *
torch.max(q_values_next).item()
...
... td_targets.append(q_values)
...
... self.update(states, td_targets)
DQN 类的其余部分保持不变。
-
我们将重用我们在《深度 Q 网络开发》配方中开发的
gen_epsilon_greedy_policy函数,并且这里不会重复它。 -
然后我们指定神经网络的形状,包括输入的大小、输出和隐藏层的大小,将
0.01作为学习率,并相应地创建一个 DQN:
>>> n_state = env.observation_space.shape[0]
>>> n_action = env.action_space.n
>>> n_hidden = 50
>>> lr = 0.01
>>> dqn = DQN(n_state, n_action, n_hidden, lr)
- 接下来,我们定义保存经验的缓冲区:
>>> memory = deque(maxlen=10000)
只要队列中有超过10000个样本,新样本将被追加到队列中,并移除旧样本。
- 现在,让我们开发双 DQN 的深度 Q 学习:
>>> def q_learning(env, estimator, n_episode, replay_size,
target_update=10, gamma=1.0, epsilon=0.1,
epsilon_decay=.99):
... """
... Deep Q-Learning using double DQN, with experience replay
... @param env: Gym environment
... @param estimator: DQN object
... @param replay_size: number of samples we use
to update the model each time
... @param target_update: number of episodes before
updating the target network
... @param n_episode: number of episodes
... @param gamma: the discount factor
... @param epsilon: parameter for epsilon_greedy
... @param epsilon_decay: epsilon decreasing factor
... """
... for episode in range(n_episode):
... if episode % target_update == 0:
... estimator.copy_target()
... policy = gen_epsilon_greedy_policy( estimator, epsilon, n_action)
... state = env.reset()
... is_done = False
... while not is_done:
... action = policy(state)
... next_state, reward, is_done, _ = env.step(action)
... total_reward_episode[episode] += reward
... modified_reward = next_state[0] + 0.5
... if next_state[0] >= 0.5:
... modified_reward += 100
... elif next_state[0] >= 0.25:
... modified_reward += 20
... elif next_state[0] >= 0.1:
... modified_reward += 10
... elif next_state[0] >= 0:
... modified_reward += 5
... memory.append((state, action, next_state,
modified_reward, is_done))
... if is_done:
... break
... estimator.replay(memory, replay_size, gamma)
... state = next_state
... print('Episode: {}, total reward: {}, epsilon: {}'.format(episode, total_reward_episode[episode], epsilon))
... epsilon = max(epsilon * epsilon_decay, 0.01)
- 我们执行双 DQN 的深度 Q 学习,共进行
1000个回合:
>>> n_episode = 1000
我们将每一步的回放样本大小设置为20:
>>> replay_size = 20
我们每 10 个回合更新一次目标网络:
>>> target_update = 10
我们还会跟踪每个回合的总奖励:
>>> total_reward_episode = [0] * n_episode
>>> q_learning(env, dqn, n_episode, replay_size, target_update, gamma=.9, epsilon=1)
Episode: 0, total reward: -200.0, epsilon: 1
Episode: 1, total reward: -200.0, epsilon: 0.99
Episode: 2, total reward: -200.0, epsilon: 0.9801
……
……
Episode: 991, total reward: -151.0, epsilon: 0.01
Episode: 992, total reward: -200.0, epsilon: 0.01
Episode: 993, total reward: -158.0, epsilon: 0.01
Episode: 994, total reward: -160.0, epsilon: 0.01
Episode: 995, total reward: -200.0, epsilon: 0.01
Episode: 996, total reward: -200.0, epsilon: 0.01
Episode: 997, total reward: -200.0, epsilon: 0.01
Episode: 998, total reward: -151.0, epsilon: 0.01
Episode: 999, total reward: -200.0, epsilon: 0.01
- 然后,我们显示随时间变化的回合奖励图:
>>> 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 5中,经验回放函数首先随机选择replay_size个样本经验。然后将每个经验转换为由输入状态和输出目标值组成的训练样本。最后,使用选定的批次更新预测网络。
Step 9是双 DQN 中最重要的步骤:它使用不同的网络计算目标值,然后定期更新这个网络。函数的其余部分类似于带经验回放的深度 Q 学习。
Step 11中的可视化函数将生成以下图表:
你可以看到,在大多数情况下,经过第一个400个回合后,小车在大约80到160步内到达山顶。
在双 DQN 的深度 Q 学习中,我们分别创建两个用于预测和目标计算的网络。第一个网络用于预测和检索Q值,而第二个网络用于提供稳定的目标Q值。并且,经过一段时间(比如每 10 个回合或 1500 个训练步骤),我们同步预测网络和目标网络。在这种双网络设置中,目标值是暂时固定的,而不是被不断修改的,因此预测网络有更稳定的目标来学习。我们获得的结果表明,双 DQN 优于单 DQN。
为 CartPole 调优双 DQN 超参数
在这个示例中,让我们使用双 DQN 解决 CartPole 环境。我们将展示如何调优双 DQN 的超参数以达到最佳性能。
为了调优超参数,我们可以应用网格搜索技术来探索一组不同的值组合,并选择表现最佳的那一组。我们可以从粗范围的值开始,并逐渐缩小范围。并且不要忘记为所有后续的随机数生成器固定种子,以确保可重现性:
-
Gym 环境随机数生成器
-
ε-贪心随机数生成器
-
PyTorch 神经网络的初始权重
如何做到...
我们使用双 DQN 解决 CartPole 环境如下:
- 导入必要的模块并创建一个 CartPole 环境:
>>> import gym
>>> import torch
>>> from collections import deque
>>> import random
>>> import copy
>>> from torch.autograd import Variable >>> env = gym.envs.make("CartPole-v0")
-
我们将重用上一个开发双深度 Q 网络示例中开发的
DQN类。 -
我们将重复使用在《深度 Q 网络开发》食谱中开发的
gen_epsilon_greedy_policy函数,并且不在这里重复。 -
现在,我们将使用双重 DQN 开发深度 Q 学习:
>>> def q_learning(env, estimator, n_episode, replay_size,
target_update=10, gamma=1.0, epsilon=0.1,
epsilon_decay=.99):
... """
... Deep Q-Learning using double DQN, with experience replay
... @param env: Gym environment
... @param estimator: DQN object
... @param replay_size: number of samples we use to
update the model each time
... @param target_update: number of episodes before
updating the target network
... @param n_episode: number of episodes
... @param gamma: the discount factor
... @param epsilon: parameter for epsilon_greedy
... @param epsilon_decay: epsilon decreasing factor
... """
... for episode in range(n_episode):
... if episode % target_update == 0:
... estimator.copy_target()
... policy = gen_epsilon_greedy_policy( estimator, epsilon, n_action)
... state = env.reset()
... is_done = False
... while not is_done:
... action = policy(state)
... next_state, reward, is_done, _ = env.step(action)
... total_reward_episode[episode] += reward
... memory.append((state, action,
next_state, reward, is_done))
... if is_done:
... break
... estimator.replay(memory, replay_size, gamma)
... state = next_state
... epsilon = max(epsilon * epsilon_decay, 0.01)
- 然后,我们指定神经网络的形状,包括输入的大小、输出的大小、隐藏层的大小和周期数,以及用于评估性能的周期数:
>>> n_state = env.observation_space.shape[0]
>>> n_action = env.action_space.n
>>> n_episode = 600
>>> last_episode = 200
- 然后,我们为以下超参数定义了几个值,以便在网格搜索中探索:
>>> n_hidden_options = [30, 40]
>>> lr_options = [0.001, 0.003]
>>> replay_size_options = [20, 25]
>>> target_update_options = [30, 35]
- 最后,我们执行一个网格搜索,在每次迭代中,我们根据一组超参数创建一个 DQN,并允许其学习 600 个周期。然后,通过对最后 200 个周期的总奖励进行平均来评估其性能:
>>> for n_hidden in n_hidden_options:
... for lr in lr_options:
... for replay_size in replay_size_options:
... for target_update in target_update_options:
... env.seed(1)
... random.seed(1)
... torch.manual_seed(1)
... dqn = DQN(n_state, n_action, n_hidden, lr)
... memory = deque(maxlen=10000)
... total_reward_episode = [0] * n_episode
... q_learning(env, dqn, n_episode, replay_size,
target_update, gamma=.9, epsilon=1)
... print(n_hidden, lr, replay_size, target_update,
sum(total_reward_episode[-last_episode:])/last_episode)
它是如何工作的...
执行了第 7 步后,我们得到了以下网格搜索结果:
30 0.001 20 30 143.15
30 0.001 20 35 156.165
30 0.001 25 30 180.575
30 0.001 25 35 192.765
30 0.003 20 30 187.435
30 0.003 20 35 122.42
30 0.003 25 30 169.32
30 0.003 25 35 172.65
40 0.001 20 30 136.64
40 0.001 20 35 160.08
40 0.001 25 30 141.955
40 0.001 25 35 122.915
40 0.003 20 30 143.855
40 0.003 20 35 178.52
40 0.003 25 30 125.52
40 0.003 25 35 178.85
我们可以看到,通过n_hidden=30、lr=0.001、replay_size=25和target_update=35的组合,我们获得了最佳的平均奖励,192.77。
随意进一步微调超参数,以获得更好的 DQN 模型。
在这个食谱中,我们使用双重 DQNs 解决了 CartPole 问题。我们使用网格搜索对超参数的值进行了微调。在我们的示例中,我们优化了隐藏层的大小、学习率、回放批量大小和目标网络更新频率。还有其他超参数我们也可以探索,如周期数、初始 epsilon 值和 epsilon 衰减值。为了确保实验的可重复性和可比性,我们保持了随机种子固定,使得 Gym 环境的随机性、epsilon-greedy 动作以及神经网络的权重初始化保持不变。每个 DQN 模型的性能是通过最后几个周期的平均总奖励来衡量的。
开发对抗深度 Q 网络
在这个食谱中,我们将开发另一种高级 DQN 类型,对抗 DQNs(DDQNs)。特别是,我们将看到在 DDQNs 中如何将 Q 值的计算分为两部分。
在 DDQNs 中,Q 值由以下两个函数计算:
在这里,V(s)是状态值函数,计算处于状态s时的值;A(s, a)是状态相关的动作优势函数,估计采取动作a相比于在状态s下采取其他动作更好多少。通过解耦value和advantage函数,我们能够适应我们的代理在学习过程中可能不一定同时查看值和优势的事实。换句话说,使用 DDQNs 的代理可以根据其偏好有效地优化任一或两个函数。
如何做...
我们使用 DDQNs 解决 Mountain Car 问题如下:
- 导入必要的模块并创建一个 Mountain Car 环境:
>>> import gym
>>> import torch
>>> from collections import deque
>>> import random
>>> from torch.autograd import Variable
>>> import torch.nn as nn >>> env = gym.envs.make("MountainCar-v0")
- 接下来,我们按以下方式定义 DDQN 模型:
>>> class DuelingModel(nn.Module):
... def __init__(self, n_input, n_output, n_hidden):
... super(DuelingModel, self).__init__()
... self.adv1 = nn.Linear(n_input, n_hidden)
... self.adv2 = nn.Linear(n_hidden, n_output)
... self.val1 = nn.Linear(n_input, n_hidden)
... self.val2 = nn.Linear(n_hidden, 1)
...
... def forward(self, x):
... adv = nn.functional.relu(self.adv1(x))
... adv = self.adv2(adv)
... val = nn.functional.relu(self.val1(x))
... val = self.val2(val)
... return val + adv - adv.mean()
- 因此,我们在
DQN类中使用 DDQN 模型:
>>> class DQN():
... def __init__(self, n_state, n_action, n_hidden=50, lr=0.05):
... self.criterion = torch.nn.MSELoss()
... self.model = DuelingModel(n_state, n_action, n_hidden)
... self.optimizer = torch.optim.Adam(self.model.parameters(), lr)
DQN类的其余部分保持不变。
-
我们将重复使用我们在开发深度 Q-Networks配方中开发的
gen_epsilon_greedy_policy函数,并且这里不会重复。 -
我们将重复使用我们在通过经验重播改进 DQNs配方中开发的
q_learning函数,并且这里不会重复。 -
我们然后指定神经网络的形状,包括输入的大小,输出和隐藏层,将
0.001设置为学习率,并相应创建一个 DQN 模型:
>>> n_state = env.observation_space.shape[0]
>>> n_action = env.action_space.n
>>> n_hidden = 50
>>> lr = 0.001
>>> dqn = DQN(n_state, n_action, n_hidden, lr)
- 接下来,我们定义保存经验的缓冲区:
>>> memory = deque(maxlen=10000)
新样本将被添加到队列中,并且只要队列中有超过10000个样本,旧样本就会被删除。
- 然后,我们执行包含 DDQN 的 Deep Q-learning,进行了
600个剧集:
>>> n_episode = 600
我们将每步设置为20作为回放样本大小:
>>> replay_size = 20
我们还会跟踪每一集的总奖励:
>>> total_reward_episode = [0] * n_episode
>>> q_learning(env, dqn, n_episode, replay_size, gamma=.9,
epsilon=.3)
- 现在,我们可以显示随时间变化的剧集奖励的绘图:
>>> 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是 Dueling DQN 的核心部分。它由两部分组成,动作优势(adv)和状态值(val)。同样,我们使用一个隐藏层作为示例。
执行Step 9将导致以下绘图:
在 DDQNs 中,预测的 Q 值由两个元素组成:状态值和动作优势。第一个估计在某个状态下的表现有多好。第二个指示相比其他选择,采取特定动作有多好。这两个元素分别计算并结合到 DQN 的最后一层。您将记得,传统的 DQNs 只更新给定状态下某个动作的 Q 值。DDQNs 更新所有动作(而不仅仅是给定动作)可以利用的状态值,以及动作的优势。因此,认为 DDQNs 更加稳健。
将 Deep Q-Networks 应用于 Atari 游戏
到目前为止,我们处理的问题相当简单,有时应用 DQNs 可能有点过头了。在这个和下一个配方中,我们将使用 DQNs 来解决 Atari 游戏,这些游戏问题要复杂得多。
我们将在这个配方中以 Pong (gym.openai.com/envs/Pong-v0/)为例。它模拟了 Atari 2600 游戏 Pong,代理与另一玩家打乒乓球。这个环境的观察是屏幕的 RGB 图像(参考下面的截图):
这是一个形状为(210,160,3)的矩阵,意味着图像的大小为210 * 160,有三个 RGB 通道。
代理(右侧)在比赛中上下移动以击打球。如果错过了,另一名玩家(左侧)将获得 1 分;同样,如果另一名玩家错过了球,代理将获得 1 分。比赛的胜者是首先得到 21 分的人。代理可以在 Pong 环境中采取以下 6 种可能的动作:
-
0: NOOP: 代理保持静止
-
1: FIRE: 不是一个有意义的动作
-
2: RIGHT: 代理向上移动
-
3: LEFT: 代理向下移动
-
4: RIGHTFIRE: 与 2 相同
-
5: LEFTFIRE: 与 5 相同
每个动作都会在k帧的持续时间内重复执行(k可以是 2、3、4 或 16,取决于 Pong 环境的具体变体)。奖励可以是以下任意一种:
-
-1: 代理错过球。
-
1: 对手错过球。
-
0: 否则。
Pong 中的观察空间210 * 160 * 3比我们通常处理的要大得多。因此,我们将把图像缩小到84 * 84并转换为灰度,然后使用 DQNs 来解决它。
怎么做…
我们将从以下内容开始探索 Pong 环境:
- 导入必要的模块并创建一个 Pong 环境:
>>> import gym
>>> import torch
>>> import random >>> env = gym.envs.make("PongDeterministic-v4")
在这个 Pong 环境的变体中,一个动作是确定性的,并且在 16 帧的持续时间内重复执行。
- 查看观察空间和动作空间:
>>> state_shape = env.observation_space.shape
>>> n_action = env.action_space.n
>>> print(state_shape)
(210, 160, 3)
>>> print(n_action)
6
>>> print(env.unwrapped.get_action_meanings())
['NOOP', 'FIRE', 'RIGHT', 'LEFT', 'RIGHTFIRE', 'LEFTFIRE']
- 指定三个动作:
>>> ACTIONS = [0, 2, 3]
>>> n_action = 3
这些动作分别是不移动、向上移动和向下移动。
- 让我们采取随机动作并渲染屏幕:
>>> env.reset()
>>> is_done = False
>>> while not is_done:
... action = ACTIONS[random.randint(0, n_action - 1)]
... obs, reward, is_done, _ = env.step(action)
... print(reward, is_done)
... env.render()
0.0 False
0.0 False
0.0 False
……
……
0.0 False
0.0 False
0.0 False
-1.0 True
您将在屏幕上看到两名玩家在打乒乓球,即使代理正在输。
- 现在,我们开发一个屏幕处理函数来缩小图像并将其转换为灰度:
>>> import torchvision.transforms as T
>>> from PIL import Image
>>> image_size = 84
>>> transform = T.Compose([T.ToPILImage(),
... T.Grayscale(num_output_channels=1),
... T.Resize((image_size, image_size),
interpolation=Image.CUBIC),
... T.ToTensor(),
... ])
现在,我们只需定义一个调整图像大小至84 * 84的调整器:
>>> def get_state(obs):
... state = obs.transpose((2, 0, 1))
... state = torch.from_numpy(state)
... state = transform(state)
... return state
此函数将调整大小后的图像重塑为大小为(1, 84, 84):
>>> state = get_state(obs)
>>> print(state.shape)
torch.Size([1, 84, 84])
现在,我们可以使用双 DQNs 开始解决环境,如下所示:
- 这次我们将使用一个较大的神经网络,有两个隐藏层,因为输入大小约为 21,000:
>>> from collections import deque
>>> import copy
>>> from torch.autograd import Variable
>>> class DQN():
... def __init__(self, n_state, n_action, n_hidden, lr=0.05):
... self.criterion = torch.nn.MSELoss()
... self.model = torch.nn.Sequential(
... torch.nn.Linear(n_state, n_hidden[0]),
... torch.nn.ReLU(),
... torch.nn.Linear(n_hidden[0], n_hidden[1]),
... torch.nn.ReLU(),
... torch.nn.Linear(n_hidden[1], n_action)
... )
... self.model_target = copy.deepcopy(self.model)
... self.optimizer = torch.optim.Adam( self.model.parameters(), lr)
DQN类的其余部分与Developing double deep Q-networks食谱中的一样,只是对replay方法进行了小的更改:
>>> def replay(self, memory, replay_size, gamma):
... """
... Experience replay with target network
... @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
... """
... if len(memory) >= replay_size:
... replay_data = random.sample(memory, replay_size)
... states = []
... td_targets = []
... for state, action, next_state, reward,
is_done in replay_data:
... states.append(state.tolist())
... q_values = self.predict(state).tolist()
... if is_done:
... q_values[action] = reward
... else:
... q_values_next = self.target_predict( next_state).detach()
... q_values[action] = reward + gamma *
torch.max(q_values_next).item()
... td_targets.append(q_values)
... self.update(states, td_targets)
-
我们将重复使用我们在Developing Deep Q-Networks食谱中开发的
gen_epsilon_greedy_policy函数,并且这里不再重复。 -
现在,我们将开发带有双 DQN 的深度 Q 学习:
>>> def q_learning(env, estimator, n_episode, replay_size,
target_update=10, gamma=1.0, epsilon=0.1,
epsilon_decay=.99):
... """
... Deep Q-Learning using double DQN, with experience replay
... @param env: Gym environment
... @param estimator: DQN object
... @param replay_size: number of samples we use to
update the model each time
... @param target_update: number of episodes before
updating the target network
... @param n_episode: number of episodes
... @param gamma: the discount factor
... @param epsilon: parameter for epsilon_greedy
... @param epsilon_decay: epsilon decreasing factor
... """
... for episode in range(n_episode):
... if episode % target_update == 0:
... estimator.copy_target()
... policy = gen_epsilon_greedy_policy( estimator, epsilon, n_action)
... obs = env.reset()
... state = get_state(obs).view(image_size * image_size)[0]
... is_done = False
... while not is_done:
... action = policy(state)
... next_obs, reward, is_done, _ =
env.step(ACTIONS[action])
... total_reward_episode[episode] += reward
... next_state = get_state(obs).view( image_size * image_size)
... memory.append((state, action, next_state,
reward, is_done))
... if is_done:
... break
... estimator.replay(memory, replay_size, gamma)
... state = next_state
... print('Episode: {}, total reward: {}, epsilon:
{}'.format(episode, total_reward_episode[episode],
epsilon))
... epsilon = max(epsilon * epsilon_decay, 0.01)
给定大小为*[210, 160, 3]的观察结果,将其转换为更小尺寸的灰度矩阵[84, 84]*并将其扁平化,以便我们可以将其馈送到我们的网络中。
- 现在,我们指定神经网络的形状,包括输入和隐藏层的大小:
>>> n_state = image_size * image_size
>>> n_hidden = [200, 50]
剩余的超参数如下:
>>> n_episode = 1000
>>> lr = 0.003
>>> replay_size = 32
>>> target_update = 10
现在,我们相应地创建一个 DQN:
>>> dqn = DQN(n_state, n_action, n_hidden, lr)
- 接下来,我们定义保存经验的缓冲区:
>>> memory = deque(maxlen=10000)
- 最后,我们执行深度 Q 学习,并跟踪每个 episode 的总奖励:
>>> total_reward_episode = [0] * n_episode
>>> q_learning(env, dqn, n_episode, replay_size, target_update, gamma=.9, epsilon=1)
它是如何工作的…
Pong 中的观察情况比本章中迄今为止我们处理过的环境复杂得多。它是一个 210 * 160 屏幕尺寸的三通道图像。因此,我们首先将其转换为灰度图像,将其缩小为 84 * 84,然后展平,以便馈送到全连接神经网络中。由于输入维度约为 6000,我们使用两个隐藏层来适应复杂性。
在 Atari 游戏中使用卷积神经网络
在上一篇文章中,我们将 Pong 环境中的每个观察图像视为灰度数组,并将其馈送到全连接神经网络中。将图像展平可能会导致信息丢失。为什么不直接使用图像作为输入呢?在这篇文章中,我们将卷积神经网络(CNNs)集成到 DQN 模型中。
CNN 是处理图像输入的最佳神经网络架构之一。在 CNN 中,卷积层能够有效地从图像中提取特征,这些特征将传递给下游的全连接层。下图展示了一个具有两个卷积层的 CNN 示例:
正如你可以想象的,如果我们简单地将图像展平成一个向量,我们将丢失一些关于球和两名玩家位置的信息。这些信息对于模型学习至关重要。在 CNN 中的卷积操作中,多个滤波器生成的一组特征映射可以捕捉到这些信息。
再次,我们将图像从 210 * 160 缩小到 84 * 84,但这次保留三个 RGB 通道,而不是将它们展平成数组。
怎么做...
让我们使用基于 CNN 的 DQN 来解决 Pong 环境,如下所示:
- 导入必要的模块并创建 Pong 环境:
>>> import gym
>>> import torch
>>> import random >>> from collections import deque
>>> import copy
>>> from torch.autograd import Variable
>>> import torch.nn as nn
>>> import torch.nn.functional as F
>>> env = gym.envs.make("PongDeterministic-v4")
- 然后,我们指定三个动作:
>>> ACTIONS = [0, 2, 3]
>>> n_action = 3
这些动作是不动、向上移动和向下移动。
- 现在,我们开发一个图像处理函数来缩小图像:
>>> import torchvision.transforms as T
>>> from PIL import Image
>>> image_size = 84
>>> transform = T.Compose([T.ToPILImage(),
... T.Resize((image_size, image_size),
interpolation=Image.CUBIC),
... T.ToTensor()])
现在,我们定义一个调整器,将图像缩小为 84 * 84,然后将图像重塑为 (3, 84, 84):
>>> def get_state(obs):
... state = obs.transpose((2, 0, 1))
... state = torch.from_numpy(state)
... state = transform(state).unsqueeze(0)
... return state
- 现在,我们开始通过开发 CNN 模型来解决 Pong 环境:
>>> class CNNModel(nn.Module):
... def __init__(self, n_channel, n_action):
... super(CNNModel, self).__init__()
... self.conv1 = nn.Conv2d(in_channels=n_channel,
out_channels=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 = torch.nn.Linear(7 * 7 * 64, 512)
... self.out = torch.nn.Linear(512, n_action)
...
... 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
- 现在我们将使用刚刚在我们的
DQN模型中定义的 CNN 模型:
>>> class DQN():
... def __init__(self, n_channel, n_action, lr=0.05):
... self.criterion = torch.nn.MSELoss()
... self.model = CNNModel(n_channel, n_action)
... self.model_target = copy.deepcopy(self.model)
... self.optimizer = torch.optim.Adam( self.model.parameters(), lr)
DQN类的其余部分与“开发双重深度 Q 网络”一章中的相同,只是replay方法略有改变:
>>> def replay(self, memory, replay_size, gamma):
... """
... Experience replay with target network
... @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
... """
... if len(memory) >= replay_size:
... replay_data = random.sample(memory, replay_size)
... states = []
... td_targets = []
... for state, action, next_state, reward,
is_done in replay_data:
... states.append(state.tolist()[0])
... q_values = self.predict(state).tolist()[0]
... if is_done:
... q_values[action] = reward
... else:
... q_values_next = self.target_predict( next_state).detach()
... q_values[action] = reward + gamma *
torch.max(q_values_next).item()
... td_targets.append(q_values)
... self.update(states, td_targets)
-
我们将重复使用我们在“开发深度 Q 网络”一章中开发的
gen_epsilon_greedy_policy函数,这里不再重复。 -
现在,我们使用双重 DQN 开发深度 Q-learning:
>>> def q_learning(env, estimator, n_episode, replay_size,
target_update=10, gamma=1.0, epsilon=0.1,
epsilon_decay=.99):
... """
... Deep Q-Learning using double DQN, with experience replay
... @param env: Gym environment
... @param estimator: DQN object
... @param replay_size: number of samples we use to
update the model each time
... @param target_update: number of episodes before
updating the target network
... @param n_episode: number of episodes
... @param gamma: the discount factor
... @param epsilon: parameter for epsilon_greedy
... @param epsilon_decay: epsilon decreasing factor
... """
... for episode in range(n_episode):
... if episode % target_update == 0:
... estimator.copy_target()
... policy = gen_epsilon_greedy_policy( estimator, epsilon, n_action)
... obs = env.reset()
... state = get_state(obs)
... is_done = False
... while not is_done:
... action = policy(state)
... next_obs, reward, is_done, _ =
env.step(ACTIONS[action])
... total_reward_episode[episode] += reward
... next_state = get_state(obs)
... memory.append((state, action, next_state,
reward, is_done))
... if is_done:
... break
... estimator.replay(memory, replay_size, gamma)
... state = next_state
... print('Episode: {}, total reward: {}, epsilon: {}' .format(episode, total_reward_episode[episode], epsilon))
... epsilon = max(epsilon * epsilon_decay, 0.01)
- 然后,我们将剩余的超参数指定如下:
>>> n_episode = 1000
>>> lr = 0.00025
>>> replay_size = 32
>>> target_update = 10
根据需要创建一个 DQN:
>>> dqn = DQN(3, n_action, lr)
- 接下来,我们定义保存经验的缓冲区:
>>> memory = deque(maxlen=100000)
- 最后,我们执行深度 Q-learning,并追踪每个周期的总奖励:
>>> total_reward_episode = [0] * n_episode >>> q_learning(env, dqn, n_episode, replay_size, target_update, gamma=.9, epsilon=1)
它是如何工作的...
步骤 3 中的图像预处理函数首先将每个通道的图像缩小到 84 * 84,然后将其尺寸更改为 (3, 84, 84)。这是为了确保将具有正确尺寸的图像输入到网络中。
在 步骤 4 中,CNN 模型有三个卷积层和一个 ReLU 激活函数,每个卷积层后都跟随着。最后一个卷积层产生的特征映射然后被展平并输入到具有 512 个节点的全连接隐藏层,然后是输出层。
将 CNNs 结合到 DQNs 中首先由 DeepMind 提出,并发表在 Playing Atari with Deep Reinforcement Learning(www.cs.toronto.edu/~vmnih/docs/dqn.pdf)。该模型以图像像素作为输入,并输出估计的未来奖励值。它还适用于其他 Atari 游戏环境,其中观察是游戏屏幕的图像。卷积组件是一组有效的分层特征提取器,它们可以从复杂环境中的原始图像数据中学习特征表示,并通过全连接层学习成功的控制策略。
请记住,即使在 GPU 上,前面示例中的训练通常也需要几天时间,在 2.9 GHz 英特尔 i7 四核 CPU 上大约需要 90 小时。
另见
如果您对 CNN 不熟悉,请查看以下资料:
-
Hands-On Deep Learning Architectures with Python(Packt Publishing,作者:刘宇熙(Hayden)和萨兰什·梅塔),第四章,CNN 架构。
-
R Deep Learning Projects(Packt Publishing,作者:刘宇熙(Hayden)和帕布罗·马尔多纳多),第一章,使用卷积神经网络识别手写数字,和第二章,智能车辆的交通标志识别。