# Q 学习和深度 Q 网络 在第 3 章,“马尔可夫决策过程”中,我们讨论了遵循马尔可夫性质的环境转移模型以及延迟的奖励和价值的概念( 或工具)功能。 好,在本章中,我们将研究马尔可夫决策过程,了解 Q 学习,以及一种称为深度 Q 网络的改进方法,用于在不同环境中进行泛化。 我们将在本章介绍以下主题: * 人工智能的有监督和无监督学习 * 基于模型的学习和无模型学习 * Q 学习 * 深度 Q 网络 * 蒙特卡罗树搜索算法 * SARSA 算法 # 为什么是强化学习? 2014 年,Google 以高达 5 亿美元的价格收购了伦敦一家名为 DeepMind 的创业公司。 在新闻中,我们了解到他们创建了一个 AI 代理来击败任何 Atari 游戏,但是 Google 付出如此高的价格收购它的主要原因是因为这一突破向**通用人工智能**靠近了一步。 通用人工智能被称为 AI 代理。 它能够完成各种任务并像人类一样泛化。 当它超过这一点时,该点称为人工超级智能。 目前,AI 社区所做的工作就是我们所说的人工智能,即人工智能,其中 AI 代理能够执行多个任务,但不能概括各种任务。 DeepMind 在研究期刊 **Nature** 上发表了他们的论文[《通过深度强化学习进行人类水平控制》](http://www.davidqiu.com:8888/research/nature14236.pdf),这表明,他们的深度强化学习算法可成功应用于 50 种不同的 Atari 游戏,并在其中 30 种游戏中达到高于人类水平的性能。 他们的 AI 代理称为**深度 Q 学习器**。 在详细深入学习强化学习之前,让我们回顾一下强化学习的基础知识。 有监督和无监督的学习是 AI 应用社区众所周知的。 **监督学习**处理包含输入特征和目标标签(连续或离散)的标记数据集,并创建将这些输入特征映射到目标标签的模型。 另一方面,**无监督学习**处理仅包含输入特征但不包含目标标签的未标记数据集,其目的是发现基础模式,以将数据分类到不同集群中,并分别定义其效用。 不同集群中特定类型的数据。 因此,使用有监督和无监督的学习,我们可以创建数据分类器/回归器或数据生成器,通过一次学习就可以通过一批数据进行学习。 为了随着时间的推移增强学习,该批量需要合并越来越多的数据,从而导致有监督和无监督的学习变得缓慢且难以推广。 让我们考虑一种情况,您希望 AI 代理为您玩特定的视频/虚拟游戏,但要注意的是,随着时间的流逝,该算法应该变得智能。 那么,如何解决这个问题呢? 假设我们拍摄了特定视频游戏中所有最佳玩家的视频,并以图像帧和目标标签的形式输入数据,作为可能采取的不同动作的集合。 由于我们具有输入功能和目标标签,因此形成了监督学习分类问题。 假设数据量巨大,并且我们可以使用具有最新 GPU 的高端机器,那么为该任务创建一个深度神经网络完全有意义。 但是这里有什么收获呢? 为了创建可解决此分类问题的深度神经网络,以使最终的 AI 代理可以击败该游戏中任何级别的任何对手,我们的输入数据需要数千小时的视频数据分配到不同级别的游戏中, 不同的玩家,采用不同的方法赢得比赛,因此我们的神经网络可以以最佳方式概括映射。 获得更多数据的原因是为了避免欠拟合。 此外,过拟合中的大量数据也可能是一个问题,但是正则化是根据给定数据将模型推广到最佳状态的可能解决方案。 因此,我们看到,即使在获得了数千小时的专家播放器的视频数据(即非常高的数据量)之后,这种有监督的学习方法似乎也不是一个很好的解决方案。 这是因为,与其他应用的 AI 问题不同,此处的数据集是动态的而不是静态的。 此处的训练数据是连续的,新的框架在游戏世界中不断出现。 现在,问问自己我们人类如何学习这项任务,答案很简单,即我们通过与环境交互而不是看着其他人与环境交互来学习最好。 因此,AI 代理可以尝试通过与环境交互来更好地学习,并通过一系列的反复试验来逐步发展其学习成果。 在现实世界和游戏世界中,环境通常是*随机*,其中可能发生许多事件。 由于所有事件都与某种发生概率相关联,因此可以对它们进行统计分析,但不能精确确定。 假设在给定的`e`环境中,我们只有三个动作来执行`a`, `b`和`c`,但是每个动作都有一些 与之相关的某种不确定性,即它们的出现机会是随机的,并且它们中的任何一个都可以发生,但是每个结果都不确定。 对于监督分类问题,我们认为环境是确定性的,其中确定了与特定动作相关的结果,并且结果是精确的预测,即特定类别(目标标签)。 在继续讨论该主题之前,让我们看一下两种环境之间的区别: * **确定性环境**:由于没有不确定性,代理的动作可以唯一地确定结果的环境。 例如,在国际象棋中,您将一块从一个正方形移到另一个正方形。 因此,确定结果,即结果平方。 没有随机性。 * **随机环境**:每个动作都与某种程度的随机性相关联的环境,因此,无论我们采取何种行动,都无法确定结果。 例如,将飞镖投掷到旋转的棋盘上或掷骰子。 在这两种情况下,结果均不确定。 因此,对于基于随机环境的问题,似乎最好的学习方法是尝试各种可能性。 因此,与其通过监督分类将其解决为模式识别问题,不如通过试错法更好,在这种方法中,结果标签将替换为量化特定操作对完成给定问题的最终目的的有用性的奖励。 声明。 这产生了环境-代理交互方法,我们在第 1 章,“深度强化–架构和框架”中讨论了该方法,在该系统中,我们设计了一种使代理与环境交互的系统 首先,通过传感器感知状态,通过效应器对环境执行一些操作,然后接收反馈,即对所采取操作的奖励,如下图所示: ![](img/c7f38da6-965e-4b9f-9149-bc088223f8bd.png) 根据传感器在特定时间步长感测环境时接收到的信号,此处的状态基本上是代理对环境的看法。 # 基于模型的学习和无模型学习 在第 3 章,“马尔可夫决策过程”中,我们使用状态,动作,奖励,转移模型和折扣因子来解决我们的马尔可夫决策过程,即 MDP 问题。 因此,如果 MDP 问题的所有这些要素均可用,我们可以轻松地使用规划算法为目标提出解决方案。 这种类型的学习称为**基于模型的学习**,其中 AI 代理将与环境交互,并基于其交互,将尝试近似环境的模型,即状态转换模型。 给定模型,现在代理可以尝试通过值迭代或策略迭代找到最佳策略。 但是,对于我们的 AI 代理来说,学习环境的显式模型不是必需的。 它可以直接从与环境的交互中得出最佳策略,而无需构建模型。 这种学习称为**无模型学习**。 无模型学习涉及在没有具体环境模型的情况下预测某个策略的值函数。 可以使用两种方法完成无模型学习: * 蒙特卡洛学习 * 时差学习 我们将在以下主题中讨论它们两者。 # 蒙特卡洛学习 蒙特卡洛(Monte Carlo)是用于模型免费学习的最简单方法,在该方法中,代理会观察情节中前进的所有步骤(即前瞻)的回报。 因此,在时间`t`时的总估计报酬为![](img/3dc9f398-49e4-4d6c-9ad4-35090d9306ff.png): ![](img/142010fa-0c37-4b54-b47d-e27ca48bd3d6.png) 这里,![](img/c80dd93d-7873-4467-8c24-8eedb1e0075a.png)是折扣因子,`T`是情节结束的时间步长。 我们可以使用以下代码初始化蒙特卡洛学习技术: ```py Initialize: i.e. the policy to be evaluated V i.e. an arbitrary state-value function Returns(s) = empty list #here Returns(s) refer to returns for a particular state i.e. the series of rewards the agent receives from that state onward Repeat forever: Generate an episode using the current For each state 's' appearing in the episode perform the following: R = returns following the first occurrence of 's' Append R to Returns(s) V(s) = Average(Returns(s)) Update policy as per V ``` # 时差学习 与在蒙特卡洛学习中我们要全面展望未来不同,在时间差异学习中,我们只有一个展望,也就是说,我们只观察到情节的下一步: ![](img/d82f0db8-7eb5-4425-b229-a08877c1ba7d.png) 时间差异学习是一种用于学习价值和策略迭代方法中的值函数以及 Q 学习中的 Q 函数的方法。 如果我们希望我们的 AI 代理始终选择最大化折扣未来奖励的行动,那么我们需要某种时间差异学习。 为此,我们需要定义一个函数 Q,该函数表示当我们在状态`s`上执行动作`a`时最大的未来折扣。 因此,Q 函数表示给定状态下的动作质量。 使用它,我们可以通过仅了解当前状态和操作来估计最终得分,而在此之后无需进行任何操作。 因此,目标将是对具有最高 Q 值的状态采取该措施。 因此,我们必须通过称为 Q 学习的过程来学习此 Q 函数。 # 策略内和策略外学习 顾名思义,**脱离策略的**学习是独立于代理行为的最优策略学习。 因此,您不需要一开始就使用特定的策略,并且即使通过随机动作开始,代理也可以学习最佳策略,最终收敛到最佳策略。 Q 学习是非策略学习的一个例子。 另一方面,**基于策略的**学习通过执行当前策略并通过探索方法对其进行更新来学习最佳策略。 因此,基于策略的学习取决于开始时的策略。 SARSA 算法是基于策略学习的示例。 # Q 学习 在强化学习中,我们希望 Q 函数`Q(s, a)`预测状态`s`的最佳动作,以便最大化未来的回报。 使用 Q 学习估计 Q 函数,该过程涉及使用贝尔曼方程通过一系列迭代更新 Q 函数的过程,如下所示: ![](img/8276b9a8-c086-4554-9c03-a77af4be5eb4.png) 这里: `Q(s, a)`为当前状态`s`和动作`a`对的`Q`值 ![](img/ea5b424c-de3c-4fbc-a152-adbc346fd533.png) =学习收敛速度 ![](img/48218f21-9c36-4878-ab3e-bb4cbf9ee090.png) =未来奖励的折扣系数 `Q(s', a')`为在状态`s`下采取动作`a`之后,所得状态`s'`的状态动作对的`Q`值 `R`表示即时奖励 ![](img/84d0d09c-e36e-4493-bf36-f0ec2f63fccb.png) =未来奖励 在状态空间和动作空间是离散的更简单的情况下,使用 Q 表实现 Q 学习,其中表代表状态,列代表动作。 Q 学习涉及的步骤如下: 1. 随机初始化 Q 表 2. 对于每个剧集,请执行以下步骤: 1. 对于给定状态`s`,从 Q 表中选择动作`a` 2. 执行动作`a` 3. 奖励`R`和状态`s'` 4. 通过以下方法更新当前状态操作对的 Q 值,即`Q(s, a)`: ![](img/8276b9a8-c086-4554-9c03-a77af4be5eb4.png) 但是,这里并没有探索新路径,并且在大多数情况下,代理正在利用已知路径。 因此,实现了一定程度的随机性,以使 AI 代理有时通过采取随机动作而不是当前的最佳动作来随机探索新路径。 探索背后的原因是,它增加了获得比当前更好的路径(即新的最佳策略)的可能性: ```py Create Q-table where rows represent different states and columns represent different actions Initialize Q(s,a) arbitrarily For each episode: Start with the starting state i.e. Initialize s to start Repeat for each step in the episode: Choose action a for s using the policy derived from Q [e.g. -greedy, either for the given 's' which 'a' has the max Q-value or choose a random action] Take the chosen action a, observe reward R and new state s' Update until s is the terminal state end ``` # 探索利用困境 下表总结了探索与利用之间的困境: | **探索** | **利用** | | --- | --- | | 除了当前的最佳动作之外,随机选择其他动作,并希望获得更好的回报。 | 选择当前的最佳操作而不尝试其他操作。 | 因此,难题是 AI 是仅根据当前最佳策略基于动作信任已获悉的 Q 值,还是应该随机尝试其他动作以希望获得更好的回报,从而改善 Q 值,因此, 得出更好的最佳策略。 # OpenAI Gym 山地车问题的 Q 学习 **山地车**是强化学习领域的标准测试问题。 它由动力不足的汽车组成,必须将陡峭的山坡驱动到标志点,如下图所示: ![](img/b2dea4b9-c53a-4380-a729-7330fa346818.png) 这里的要点是重力要比汽车的引擎强,因此即使在全油门的情况下,汽车也无法在陡峭的斜坡上加速。 因此,汽车必须以相反的方向反向行驶来利用势能,然后再利用势能到达右上角的标志点。 在这里,状态空间是连续的,由两点定义:位置和速度。 对于给定的状态(即位置和速度),代理可以采取三个离散的动作,即向前移动(向图中右上方),向相反方向(向图中左上方)或不使用引擎 ,即汽车处于空档。 代理会收到负面奖励,直到达到目标状态。 Q 学习可以轻松地应用于具有离散状态空间和动作的环境,但是这个问题成为了强化学习算法的测试平台,因为它具有连续状态空间,并且需要离散化连续状态空间或函数逼近才能将其映射到离散类。 下面列出了山地车问题的技术细节,供您参考: 状态空间是二维且连续的。 它由位置和速度组成,具有以下值: * **位置**:`(-1.2, 0.6)` * **速度**:`(-0.07, 0.07)` 动作空间是离散的和一维的,具有三个选项: * (左,中,右) 每个时间步数都奖励 -1。 起始状态: * **位置**:-0.5 * **速度**:0.0 终端状态条件: * **剧集**在位置![](img/b3a4291b-84d5-499b-a1ca-0a266d37c608.png) 0.6 结束 正如我们现在看到的 Q 学习的参数一样,我们现在将研究解决山地车问题的 Q 学习的实现。 首先,我们将使用以下代码导入依赖项并检查山地车环境: ```py #importing the dependencies import gym import numpy as np #exploring Mountain Car environment env_name = 'MountainCar-v0' env = gym.make(env_name) print("Action Set size :",env.action_space) print("Observation set shape :",env.observation_space) print("Highest state feature value :",env.observation_space.high) print("Lowest state feature value:",env.observation_space.low) print(env.observation_space.shape) ``` 先前的打印语句输出以下内容: ```py Making new env: MountainCar-v0 ('Action Set size :', Discrete(3)) ('Observation set shape :', Box(2,)) ('Highest state feature value :', array([ 0.6 , 0.07])) ('Lowest state feature value:', array([-1.2 , -0.07])) (2,) ``` 因此,我们看到动作空间是一个离散集合,显示了三个可能的动作,状态空间是一个二维连续空间,其中一个维度满足位置,而另一个则满足汽车的速度。 接下来,我们将使用以下代码分配超参数,例如状态数,情节数,学习率(初始和最小值),折扣因子伽玛,情节中的最大步数以及 ε 贪婪的`ε`: ```py n_states = 40 # number of states episodes = 10 # number of episodes initial_lr = 1.0 # initial learning rate min_lr = 0.005 # minimum learning rate gamma = 0.99 # discount factor max_steps = 300 epsilon = 0.05 env = env.unwrapped env.seed(0) #setting environment seed to reproduce same result np.random.seed(0) #setting numpy random number generation seed to reproduce same random numbers ``` 我们的下一个任务是创建一个函数来对连续状态空间进行离散化。 离散化是将连续状态空间观察转换为离散状态空间集: ```py def discretization(env, obs): env_low = env.observation_space.low env_high = env.observation_space.high env_den = (env_high - env_low) / n_states pos_den = env_den[0] vel_den = env_den[1] pos_high = env_high[0] pos_low = env_low[0] vel_high = env_high[1] vel_low = env_low[1] pos_scaled = int((obs[0] - pos_low)/pos_den) #converts to an integer value vel_scaled = int((obs[1] - vel_low)/vel_den) #converts to an integer value return pos_scaled,vel_scaled ``` 现在,我们将通过初始化 Q 表并相应地更新 Q 值来开始实现 Q 学习算法。 在这里,我们将奖励值更新为当前位置与最低点(即起点)之间的绝对差值,以便它通过远离中心即最低点来最大化奖励。 这样做是为了实现更好的收敛: ```py #Q table #rows are states but here state is 2-D pos,vel #columns are actions #therefore, Q- table would be 3-D q_table = np.zeros((n_states,n_states,env.action_space.n)) total_steps = 0 for episode in range(episodes): obs = env.reset() total_reward = 0 # decreasing learning rate alpha over time alpha = max(min_lr,initial_lr*(gamma**(episode//100))) steps = 0 while True: env.render() pos,vel = discretization(env,obs) #action for the current state using epsilon greedy if np.random.uniform(low=0,high=1) < epsilon: a = np.random.choice(env.action_space.n) else: a = np.argmax(q_table[pos][vel]) obs,reward,terminate,_ = env.step(a) total_reward += abs(obs[0]+0.5) #q-table update pos_,vel_ = discretization(env,obs) q_table[pos][vel][a] = (1-alpha)*q_table[pos][vel][a] + alpha*(reward+gamma*np.max(q_table[pos_][vel_])) steps+=1 if terminate: break print("Episode {} completed with total reward {} in {} steps".format(episode+1,total_reward,steps)) while True: #to hold the render at the last step when Car passes the flag env.render() ``` 根据学习情况,前面的 Q 学习器将以以下方式打印输出: ```py Episode 1 completed with total reward 8433.30289388 in 26839 steps Episode 2 completed with total reward 3072.93369963 in 8811 steps Episode 3 completed with total reward 1230.81734028 in 4395 steps Episode 4 completed with total reward 2182.31111239 in 6629 steps Episode 5 completed with total reward 2459.88770998 in 6834 steps Episode 6 completed with total reward 720.943914405 in 2828 steps Episode 7 completed with total reward 389.433014729 in 1591 steps Episode 8 completed with total reward 424.846699654 in 2362 steps Episode 9 completed with total reward 449.500988781 in 1413 steps Episode 10 completed with total reward 222.356805259 in 843 steps ``` 这还将渲染一个环境,显示汽车在行驶并采取最佳路径并达到目标状态,如以下屏幕截图所示: ![](img/69dbc107-d503-4cbe-b66c-c073c7a4a74d.png) 山地赛车游戏的最终状态视图 因此,我们看到无模型学习可以从其与环境的交互中得出最佳策略,而无需创建环境模型。 因此,我们已经知道,Q 学习是一种无模型的时间差异学习,它通过估计 Q 函数来找到最佳状态动作选择策略。 # 深度 Q 网络 如果我们回想起第 2 章和“使用 OpenAI Gym 训练强化学习智能体”,我们曾尝试在其中实现基本的 Q 网络,那么我们就针对一个实际问题研究了 Q 学习 由于连续的状态和动作空间,使用 Q 表不是可行的解决方案。 而且,Q 表是特定于环境的,而不是通用的。 因此,我们需要一个模型,该模型可以将作为输入提供的状态信息映射到可能的一组动作的 Q 值。 在这里,神经网络开始发挥函数逼近器的作用,函数逼近器可以接受向量形式的状态信息输入,并学习将其映射为所有可能动作的 Q 值。 让我们讨论游戏环境中的 Q 学习问题以及深度 Q 网络的发展。 考虑将 Q 学习应用于游戏环境,该状态将由玩家,障碍物,对手等的位置来定义,但这将是特定于游戏的,即使在我们创建时也不能在其他游戏环境中推广 一个以某种方式表示该游戏所有可能状态的 Q 表。 好吧,游戏环境有一个共同点,那就是全部由像素组成。 如果可以将像素输入可以映射到动作的模型中,则可以在所有游戏中将其推广。 DeepMind 的卷积神经网络实现具有游戏图像帧,其中输入和输出是该环境中每个可能动作的 Q 值。 卷积神经网络由三个卷积层和两个全连接层组成。 **卷积神经网络**(**CNN**)的一个元素是池化层,在此已避免。 使用池化层的主要原因是在图像中进行对象检测的情况下,其中图像中对象的位置并不重要,而在此处,在游戏框架中对象的位置非常重要时,则不然。 因此,在游戏环境中,**深度 Q 网络**(**DQN**)由连续的游戏帧组成,作为捕获动作的输入,并为游戏中所有可能的动作输出 Q 值。 游戏。 由于将深度神经网络用作 Q 函数的函数逼近器,因此此过程称为深度 Q 学习。 与 Q 网络相比,深度 Q 网络具有更强的泛化能力。 为了将 Q 网络转换为深度 Q 网络,我们需要进行以下改进: * 使用卷积神经网络代替单层神经网络 * 使用经验回放 * 分离目标网络来计算目标 Q 值 在以下主题中,我们将详细讨论每个参数: # 使用卷积神经网络代替单层神经网络 我们的游戏环境是视频,而卷积神经网络在计算机视觉方面已经显示了最新的成果。 而且,游戏框架中物体检测的水平应该接近人类水平的能力,并且卷积神经网络从图像中学习表示,类似于人类原始视觉皮层的行为。 DeepMind 在其 DQN 网络中使用了三个卷积层和两个全连接层,从而在 Atari 游戏中实现了超人水平的性能,如以下流程图所示: ![](img/373d89a3-057c-444c-91b2-bbe4eeb5e1b4.png) # 使用经验回放 添加到深度 Q 网络的另一个重要功能是**经验回放**。 该功能背后的想法是,代理可以存储其过去的经验,并分批使用它们来训练深度神经网络。 通过存储经验,代理可以随机抽取批量,并帮助网络从各种数据中学习,而不仅仅是对即时经验的决策正式化。 这些经历中的每一个都以包括**状态,动作,奖励**和**下一个状态**的四维向量的形式存储。 为了避免存储问题,体验重放的缓冲区是固定的,并且随着新体验的存储,旧体验将被删除。 为了训练神经网络,从缓冲区中提取均匀批量的随机经验。 # 分离目标网络来计算目标 Q 值 生成目标 Q 值的单独网络是一项重要功能,它使深层 Q 网络具有独特性。 由此单独的目标网络生成的 Q 值用于计算代理在训练过程中采取的每项操作之后的损失。 使用两个网络而不是一个网络的原因在于,由于每个步骤的权重变化,主要的 Q 网络值在每个步骤中都在不断变化,这使得从该网络生成的 Q 值不稳定。 为了获得稳定的 Q 值,使用了另一个神经网络,其权重与主要 Q 网络相比变化缓慢。 这样,训练过程更加稳定。 DeepMind 的文章也发表在[这个页面](http://www.davidqiu.com:8888/research/nature14236.pdf)中。 他们发现这种方法能够稳定训练过程。 [改编自 Minh 等人(2015)](http://www.davidqiu.com:8888/research/nature14236.pdf),以下是 DQN 的伪代码: ```py Input: the image(game frame) pixels Initialize replay memory D for experience replay Initialize action-value function`Q`i.e. primary neural network with random weight Initialize target action-value function QT i.e. target neural network with weights for each episode do Initialize sequence and preprocessed sequence for t = 0 to max_step in an episode do Choose using -greedy such that Perform action Observe reward and image Set and preprocess Store transition in D // experience replay Sample random batch of transitions from D Set Compute the cost function = Perform gradient descent on the cost function w.r.t. the primary network parameter θ // periodic update of target network After every C steps reset , i.e., set until end of episode end ``` # 深度 Q 网络以及其他方面的进步 随着更多的研究和更多的时间,深度 Q 网络已经进行了许多改进,从而获得了更好的架构,从而提供了更高的性能和稳定性。 在本节中,我们将仅讨论两种著名的架构,即**双重 DQN** 和**决斗 DQN**。 # 双 DQN 使用**双重 DQN(DDQN)**的原因是常规 DQN 高估了在给定状态下可能采取的措施的 Q 值。 在常规 DQN 中,所有动作之间的高估都不相等。 因此,问题仍然存在:否则,所有行动之间的均等估计就不会成为问题。 结果,某些次优的行为获得了更高的价值,因此学习最佳策略的时间增加了。 这导致我们对常规 DQN 架构进行了少量修改,并导致了所谓的 DDQN,即双深度 Q 网络。 在 DDQN 中,我们不是在训练过程中计算目标 Q 值时取最大 Q 值,而是使用主要网络选择操作,并使用目标网络为该操作生成目标 Q 值。 这使动作脱钩。 从目标 Q 网络中进行选择,生成目标 Q 值,从而减少高估,并有助于更快地进行训练。 DDQN 中的目标 Q 值通过以下公式更新: ![](img/da95ebb9-7b3c-454e-950c-125072e0b43f.png) 这里: ![](img/9ce748d1-69db-4379-9a41-8f3ee9dc46c2.png)代表主网络的权重,并且 ![](img/e6c5b010-6999-4954-bfb0-64848de92322.png)代表目标网络的权重。 # 决斗 DQN 在**决斗 DQN** 的情况下,Q 值已被修改为状态的值函数和操作的优势函数的总和。 值函数`V(s)`量化处于状态`s`的有用性或优势,优势函数`A(a)`量化行为的优势代替其他可能的操作。 因此, ![](img/f31160d6-e246-41db-84d5-91fc9bd2495c.png) 决斗 DQN 具有独立的网络来计算值和优势函数,然后将它们组合回以获取 Q 函数的值。 将价值和优势的计算脱钩的原因在于,对于给定状态下的每个动作,代理不必照顾不必要的值函数。 因此,将这些计算去耦会导致鲁棒的状态动作 Q 值。 # 用于 OpenAI Gym 山地车问题的深度 Q 网络 在针对山地车问题实现 Q 学习时,我们已经讨论了环境。 让我们直接深入实现一个深度 Q 网络来解决山地车问题。 首先,我们将使用以下代码导入所需的库: ```py #importing the dependencies import numpy as np import tensorflow as tf import gym ``` 让我们讨论一下 DQN 类,其中包含深度 Q 网络的架构: * `__init__`(`self`,`learning_rate`,`gamma`,`n_features`,`n_actions`,`epsilon`,`parameter_changing_pointer`,`memory_size`):默认构造器,用于分配超参数,例如: * `learning_rate` * `gamma`,即折扣因子 * `n_feature`:状态中的要素数,即状态中的尺寸数 * `epsilon`:ε 贪婪条件的阈值,以利用或探索动作 * `build_networks()`:使用 Tensorflow 创建主要和目标网络 * `target_params_replaced(self)`:用主要网络参数替换目标网络参数 * `store_experience(self,obs,a,r,obs_)`:存储经验,即(状态,动作,奖励,新状态)的元组 * `fit(self)`:训练我们的深度 Q 网络 * `epsilon_greedy(self,obs)`:对于给定的观察状态,要采取的操作,即根据现有策略利用操作或随机探索新操作 可以使用以下代码定义具有主要功能的 DQN 类的架构: ```py class DQN: def __init__(self,learning_rate,gamma,n_features, n_actions,epsilon,parameter_changing_pointer,memory_size): .... def build_networks(self): .... def target_params_replaced(self): .... def store_experience(self,obs,a,r,obs_): .... def fit(self): .... def epsilon_greedy(self,obs): .... if __name__ == "__main__": .... ``` `__init__`:在以下代码片段中解释了默认构造器以及注释: ```py def __init__(self,learning_rate,gamma,n_features,n_actions,epsilon,parameter_changing_pointer,memory_size): self.learning_rate = learning_rate self.gamma = gamma self.n_features = n_features self.n_actions = n_actions self.epsilon = epsilon self.batch_size = 100 self.experience_counter = 0 self.experience_limit = memory_size self.replace_target_pointer = parameter_changing_pointer self.learning_counter = 0 self.memory = np.zeros([self.experience_limit,self.n_features*2+2]) #for experience replay self.build_networks() #to fetch parameters under the collection : 'primary_network_parameters' p_params = tf.get_collection('primary_network_parameters') #to fetch parameters under the collection : 'target_network_parameters' t_params = tf.get_collection('target_network_parameters') #replacing tensor replace the target network parameters with primary network parameters self.replacing_target_parameters = [tf.assign(t,p) for t,p in zip(t_params,p_params)] self.sess = tf.Session() self.sess.run(tf.global_variables_initializer()) ``` 现在让我们初始化`build_networks(self)`。 它是构建主要网络和目标网络的功能: * 在`variable_scope`:`primary_network`下创建主要网络参数,并在`primary_network_parameters`集合中创建 * 在`variable_scope`:`target_network`下创建目标网络参数,并在`target_network_parameters`集合中创建目标网络参数 * 这两个参数具有相同的结构,即: * `w1`:与输入层关联的权重矩阵 * `b1`:与输入层关联的偏置向量 * `ReLU`:信号从输入到隐藏层的激活函数 * `w2`:与隐藏层关联的权重矩阵 * `b2`:与隐藏层关联的偏置向量 * 计算主网络输出的 Q 值与目标网络输出的 Q 值之间的损失 * 使用 **adam** 优化器将损失降到最低 我们将使用以下代码定义`build_networks(self)`函数: ```py def build_networks(self): #primary network hidden_units = 10 self.s = tf.placeholder(tf.float32,[None,self.n_features]) self.qtarget = tf.placeholder(tf.float32,[None,self.n_actions]) with tf.variable_scope('primary_network'): c = ['primary_network_parameters', tf.GraphKeys.GLOBAL_VARIABLES] # first layer with tf.variable_scope('layer1'): w1 = tf.get_variable('w1',[self.n_features,hidden_units],initializer=tf.contrib.layers.xavier_initializer(),dtype=tf.float32,collections=c) b1 = tf.get_variable('b1',[1,hidden_units],initializer=tf.contrib.layers.xavier_initializer(),dtype=tf.float32,collections=c) l1 = tf.nn.relu(tf.matmul(self.s, w1) + b1) # second layer with tf.variable_scope('layer2'): w2 = tf.get_variable('w2',[hidden_units,self.n_actions],initializer=tf.contrib.layers.xavier_initializer(),dtype=tf.float32,collections=c) b2 = tf.get_variable('b2',[1,self.n_actions],initializer=tf.contrib.layers.xavier_initializer(),dtype=tf.float32,collections=c) self.qeval = tf.matmul(l1, w2) + b2 with tf.variable_scope('loss'): self.loss = tf.reduce_mean(tf.squared_difference(self.qtarget,self.qeval)) with tf.variable_scope('optimizer'): self.train = tf.train.AdamOptimizer(self.learning_rate).minimize(self.loss) #target network self.st = tf.placeholder(tf.float32,[None,self.n_features]) with tf.variable_scope('target_network'): c = ['target_network_parameters', tf.GraphKeys.GLOBAL_VARIABLES] # first layer with tf.variable_scope('layer1'): w1 = tf.get_variable('w1', [self.n_features,hidden_units],initializer=tf.contrib.layers.xavier_initializer(),dtype=tf.float32,collections=c) b1 = tf.get_variable('b1', [1,hidden_units],initializer=tf.contrib.layers.xavier_initializer(),dtype=tf.float32,collections=c) l1 = tf.nn.relu(tf.matmul(self.st, w1) + b1) # second layer with tf.variable_scope('layer2'): w2 = tf.get_variable('w2',[hidden_units,self.n_actions],initializer=tf.contrib.layers.xavier_initializer(),dtype=tf.float32,collections=c) b2 = tf.get_variable('b2',[1,self.n_actions],initializer=tf.contrib.layers.xavier_initializer(),dtype=tf.float32,collections=c) self.qt = tf.matmul(l1, w2) + b2 ``` 现在,我们将使用以下代码定义`target_params_replaced(self)`。 运行将主网络参数分配给目标网络参数的张量操作的功能: ```py def target_params_replaced(self): self.sess.run(self.replacing_target_parameters) ``` 现在,我们将定义`store_experience(self,obs,a,r,obs_)`,该函数用于将每种体验(即(状态,动作,奖励,新状态)的元组)存储在其体验缓冲区中,通过该数组可以训练主要目标,如 以下代码: ```py def store_experience(self,obs,a,r,obs_): index = self.experience_counter % self.experience_limit self.memory[index,:] = np.hstack((obs,[a,r],obs_)) self.experience_counter+=1 ``` 在这里,我们将定义`fit(self)`,该功能是通过从经验缓冲区中选择一批来训练网络,计算`q_target`的张量值,然后最小化`qeval`之间的损失(即输出)的功能。 来自主网络的数据)和`q_target`(即使用目标网络计算的 Q 值)。 我们将使用以下代码定义函数: ```py def fit(self): # sample batch memory from all memory if self.experience_counter < self.experience_limit: indices = np.random.choice(self.experience_counter, size=self.batch_size) else: indices = np.random.choice(self.experience_limit, size=self.batch_size) batch = self.memory[indices,:] qt,qeval = self.sess.run([self.qt,self.qeval],feed_dict={self.st:batch[:,-self.n_features:],self.s:batch[:,:self.n_features]}) qtarget = qeval.copy() batch_indices = np.arange(self.batch_size, dtype=np.int32) actions = self.memory[indices,self.n_features].astype(int) rewards = self.memory[indices,self.n_features+1] qtarget[batch_indices,actions] = rewards + self.gamma * np.max(qt,axis=1) self.sess.run(self.train,feed_dict = {self.s:batch[:,:self.n_features],self.qtarget:qtarget}) #increasing epsilon if self.epsilon < 0.9: self.epsilon += 0.0002 #replacing target network parameters with primary network parameters if self.learning_counter % self.replace_target_pointer == 0: self.target_params_replaced() print("target parameters changed") self.learning_counter += 1 ``` 我们已经讨论了探索与利用难题。 **ε 贪婪方法**是用于选择阈值`ε`并产生随机数的方法之一。 如果小于`ε`,我们将遵循相同的策略;如果大于`ε`,我们将随机探索行动,反之亦然。 在`epsilon_greedy(self,obs)`中,我们以动态方式实现了 ε 贪婪方法,其中在`fit(self)`函数中,我们在每个学习步骤中都增加了`ε`的值: ```py def epsilon_greedy(self,obs): #epsilon greedy implementation to choose action if np.random.uniform(low=0,high=1) < self.epsilon: return np.argmax(self.sess.run(self.qeval,feed_dict={self.s:obs[np.newaxis,:]})) else: return np.random.choice(self.n_actions) ``` 以下是主要功能,该功能创建上一个 DQN 类的对象,使用 Gym 来获取`MountainCar-v0`环境,并训练代理程序来解决问题。 像在 Q 学习中一样,在这里我们还更新了奖励值,将其作为当前位置与最低点位置(即起点)之间的绝对差值,从而使其偏离中心而最大化了奖励: ```py if __name__ == "__main__": env = gym.make('MountainCar-v0') env = env.unwrapped dqn = DQN(learning_rate=0.001,gamma=0.9,n_features=env.observation_space.shape[0],n_actions=env.action_space.n,epsilon=0.0,parameter_changing_pointer=500,memory_size=5000) episodes = 10 total_steps = 0 for episode in range(episodes): steps = 0 obs = env.reset() episode_reward = 0 while True: env.render() action = dqn.epsilon_greedy(obs) obs_,reward,terminate,_ = env.step(action) reward = abs(obs_[0]+0.5) dqn.store_experience(obs,action,reward,obs_) if total_steps > 1000: dqn.fit() episode_reward+=reward if terminate: break obs = obs_ total_steps+=1 steps+=1 print("Episode {} with Reward : {} at epsilon {} in steps {}".format(episode+1,episode_reward,dqn.epsilon,steps)) while True: #to hold the render at the last step when Car passes the flag env.render() ``` 前面的程序将打印以下内容: ```py target parameters changed target parameters changed target parameters changed target parameters changed target parameters changed target parameters changed Episode 1 with Reward : 1399.25710453 at epsilon 0.5948 in steps 3974 target parameters changed Episode 2 with Reward : 168.166352703 at epsilon 0.6762 in steps 406 target parameters changed Episode 3 with Reward : 67.6246277944 at epsilon 0.7568 in steps 402 Episode 4 with Reward : 53.1292577484 at epsilon 0.7942 in steps 186 target parameters changed Episode 5 with Reward : 38.90009005 at epsilon 0.818 in steps 118 Episode 6 with Reward : 60.9286778233 at epsilon 0.8738 in steps 278 target parameters changed Episode 7 with Reward : 72.433268035 at epsilon 0.9002 in steps 257 Episode 8 with Reward : 80.7812592557 at epsilon 0.9002 in steps 251 target parameters changed Episode 9 with Reward : 92.123978864 at epsilon 0.9002 in steps 234 Episode 10 with Reward : 38.7923903502 at epsilon 0.9002 in steps 126 ``` 在这里,它收敛很快,但是还取决于对动作的探索和利用以及参数和超参数的初始化。 这还将渲染一个环境,显示汽车在行驶并采取最佳路径并达到目标状态,如以下屏幕截图所示: ![](img/c662d7ff-84d0-4610-b63d-daec721d3143.png) 接下来,在以下主题中,我们尝试实现一个深度 Q 网络来解决 OpenAI Gym 中的 Cartpole 问题。 # 用于 OpenAI Gym Cartpole 问题的深度 Q 网络 **Cartpole** 是 MDP 环境中最简单的问题之一,如以下屏幕快照所示。 它由一个在水平轴上移动的推车组成,该推车的中心处固定有一根可旋转的杆。 目的是采取行动,使电杆保持接近垂直且不会向下旋转。 ![](img/59c7c998-1669-4892-a27c-9b375e62a033.png) 车杆环境中的状态是一个 4 维连续空间,其中每个维如下: * `x`:表示购物车位置(最小值为 -2.4,最大值为 2.4) * `x_dot`:表示推车速度(最小值=-![](img/f0d92660-1037-45a4-a905-f22cb45fe740.png),最大值= ![](img/ec6de968-7ab1-4b6f-878f-fb54e27d6c45.png)) * `theta`:显示以弧度为单位的角度(最小值= -0.73,最大值= 0.73) * `theta_dot`:显示角速度(最小值=-![](img/7de21974-59ca-4569-a5f7-e9fb9b9166f1.png),最大值= ![](img/53b0fcd2-43c7-4b95-a66c-c5822cc6b7ef.png)) 在给定状态下的每一步,都有两种可能的动作,即推车可以向左或向右移动,并且每一步收到的奖励为 1。这里,只要杆子靠近 垂直,购物车在边界内。 如果发生以下情况,则情节被视为结束: * 极点下降超过某个角度,即超过![](img/76201eae-2461-41ea-8feb-b04379d77fd2.png) 0.20944 弧度 * 推车超出框架左侧或右侧太远,即超出![](img/21a1066b-d92d-4739-8441-f366541c8b73.png) 2.4 因此,该问题的目的是将杆保持在接近垂直的位置,而推车不会越过边界越长。 为了为 Cartpole 问题实现深层 Q 网络,我们将导入先前创建的 DQN 类。 请按照以下代码在 Cartpole 环境中实现深度 Q 网络。 如果连续 100 次试验的平均奖励大于或等于 195,则认为该问题已解决: ```py #Importing dependencies import gym import numpy as np #Importing the DQN class created preceding from Deep_Q_Network_Mountain_Car import DQN ``` 现在,我们将使用以下代码探索 Cartpole 环境: ```py env = gym.make('CartPole-v0') env = env.unwrapped print(env.action_space) print(env.observation_space) print(env.observation_space.high) print(env.observation_space.low) print("Position extreme threshold value:",env.x_threshold) print("Angle extreme threshold value:",env.theta_threshold_radians) ``` 先前的打印语句输出以下内容: ```py Making new env: CartPole-v0 Discrete(2) Box(4,) [ 4.80000000e+00 3.40282347e+38 4.18879020e-01 3.40282347e+38] [ -4.80000000e+00 -3.40282347e+38 -4.18879020e-01 -3.40282347e+38] Position extreme threshold value: 2.4 Angle extreme threshold value: 0.20943951023931953 ``` 在此,观察空间的高/低值遵循以下顺序(位置,速度,角度,角速度) 下面的代码是我们创建 DQN 上一类的对象,使用 Gym 来获取 Cartpole-v0 环境以及训练代理程序以解决问题的主要部分。 在这里,我们将奖励值更新为位置与极端位置之差和角度与极端极角之差的总和,因为远离极限位置,角度将变得更小,并且更接近 到购物车的中央,因此奖励应该更高。 这样做是为了实现更好的主网络融合。 我们将使用此奖励进行学习,但是为了计算总体成功程度,我们将使用原始奖励: ```py dqn = DQN(learning_rate=0.01,gamma=0.9,n_features=env.observation_space.shape[0],n_actions=env.action_space.n,epsilon=0.0,parameter_changing_pointer=100,memory_size=2000) episodes = 150 total_steps = 0 rew_ep = [] for episode in range(episodes): steps = 0 obs = env.reset() episode_reward = 0 while True: env.render() action = dqn.epsilon_greedy(obs) obs_,reward,terminate,_ = env.step(action) #smaller the theta angle and closer to center then better should be the reward x, vel, angle, ang_vel = obs_ r1 = (env.x_threshold - abs(x))/env.x_threshold - 0.8 r2 = (env.theta_threshold_radians - abs(angle))/env.theta_threshold_radians - 0.5 reward = r1 + r2 dqn.store_experience(obs,action,reward,obs_) if total_steps > 1000: dqn.fit() episode_reward+=reward if terminate: break obs = obs_ total_steps+=1 steps+=1 print("Episode {} with Reward : {} at epsilon {} in steps {}".format(episode+1,episode_reward,dqn.epsilon,steps)) rew_ep.append(episode_reward) print("Mean over last 100 episodes are: ",np.mean(rew_ep[50:])) while True: #to hold the render at the last step when Car passes the flag env.render() ``` 前面的程序将打印输出,如下所示: ```py ................. ................. ................. Episode 145 with Reward : 512.0 at epsilon 0.9002 in steps 511 target parameters changed target parameters changed target parameters changed target parameters changed target parameters changed target parameters changed Episode 146 with Reward : 567.0 at epsilon 0.9002 in steps 566 target parameters changed target parameters changed target parameters changed target parameters changed target parameters changed target parameters changed target parameters changed target parameters changed target parameters changed target parameters changed target parameters changed target parameters changed target parameters changed Episode 147 with Reward : 1310.0 at epsilon 0.9002 in steps 1309 Episode 148 with Reward : 22.0 at epsilon 0.9002 in steps 21 target parameters changed target parameters changed target parameters changed target parameters changed target parameters changed target parameters changed target parameters changed target parameters changed target parameters changed target parameters changed target parameters changed target parameters changed Episode 149 with Reward : 1171.0 at epsilon 0.9002 in steps 1170 target parameters changed target parameters changed target parameters changed target parameters changed target parameters changed target parameters changed target parameters changed target parameters changed target parameters changed target parameters changed Episode 150 with Reward : 1053.0 at epsilon 0.9002 in steps 1052 Mean over last 100 episodes are: 248.72999999999999 ``` 由于输出日志太长,因此在这里,我们的输出适合最近六个情节以及最近 100 个情节的每集平均奖励。 # 用于 OpenAI Gym Atari Breakout 的深度 Q 网络 **Breakout** 环境是 Atari 的 Nolan Bushnell,Steve Bristow 和 Steve Wozniak 团队开发的。与我们在山地车 Cartpole 中看到的状态相比,Atari Breakout 环境的状态要大得多。 或“冰湖”。 状态空间与我们在 Atari Pong 中看到的范围相似。 因此,学习收敛需要很长时间。 以下屏幕快照说明了 Atari Breakout 环境的初始图像帧: ![](img/0ddf6045-7c46-45a8-b644-11756f45a966.png) Breakout-v0 环境的屏幕截图 观察空间是连续的,包含图像帧的像素值,并且动作空间是离散的,包括四个不同的动作。 每个图像帧的大小为`210 * 160 * 3`(高度为 210 像素,宽度为 160 像素,具有 3 个颜色通道,即 RGB)。 因此,我们可以拍摄灰度图像帧,因为不会丢失任何信息,并且尺寸将变为`210 * 160`。 仅拍摄状态的图像帧将不起作用,因为它无法捕获任何运动信息。 因此,我们将为每个状态堆叠四个连续的图像帧。 因此,状态大小将是`4 * 210 * 160 = 134,440`。 对于 Atari Breakout,在一定程度上降低采样率不会造成任何信息丢失。 此外,我们还可以裁剪图像框架以避免图像的不必要部分,并保留可能包含足够信息来玩游戏的重要部分。 首先,使用以下代码检查环境: ```py import gym env = gym.make('Breakout-v0') s = env.reset() print("State space shape : ",env.observation_space) print("Action space shape : ",env.action_space) print("Type of actions : ",env.unwrapped.get_action_meanings()) ``` 这将输出以下语句: ```py State space shape : Box(210, 160, 3) Action space shape : Discrete(4) Type of actions : ['NOOP', 'FIRE', 'RIGHT', 'LEFT'] ``` 因此,我们得到了状态空间和动作空间的形状,以及球拍可以采取的四种不同类型的动作,即无动作(无操作的简称),开火(上方目标砖的球),向右走, 或向左移动以阻止球下降。 我们还检查示例裁剪并查看差异,如下图所示: ![](img/74724bff-3706-4ea9-990d-707020570fbf.png) ![](img/1bbe7bb3-0e3c-48ae-97e8-cb276076ab45.png) 裁剪前(左)和裁剪后(右) 游戏以下列方式进行: * 底部的球拍将球击发,击中砖块以摧毁屏幕的顶层 * 击中砖块后,球反弹回来 * 球拍应向左或向右移动以击中球并阻止其掉落 * 如果球落到下方,也就是说,从球拍下方的屏幕上移开,则游戏结束且玩家输 * 如果球从球拍弹起,它将再次上升,从墙壁上弹起并击中更多砖块 因此,目标是通过摧毁所有积木而不让球进入球拍下来赢得比赛。 让我们开始实现一个深层的 Q 网络,以使我们的代理商学习 Atari Breakout 的游戏。 首先,我们将使用以下代码导入必要的库: ```py #Importing the dependencies import numpy as np import tensorflow as tf import gym from scipy.misc import imresize ``` 具有以下主要功能的`class DQN`的架构可以使用以下代码定义: ```py class DQN: def __init__(self,learning_rate,gamma,n_features,n_actions,epsilon,parameter_changing_pointer,memory_size,epsilon_incrementer): .... def add_layer(self,inputs,w_shape=None,b_shape=None,layer=None,ctivation_fn=None,c=None,isconv=False: .... def weight_variable(self,w_shape,layer,c): .... def bias_variable(self,b_shape,layer,c): .... def conv(self,inputs,w): .... def build_networks(self): .... def target_params_replaced(self): .... def store_experience(self,obs,a,r,obs_): .... def fit(self): .... def epsilon_greedy(self,obs): .... if __name__ == "__main__": .... ``` 让我们讨论 DQN 类及其参数,它包含一个深度 Q 网络的架构: * `__init__(self,learning_rate,gamma,n_features,n_actions,epsilon,parameter_changing_pointer,memory_size)`:分配超参数的默认构造器,例如: * `learning_rate` * `gamma`:即折扣系数 * `n_feature`:状态下的要素数量,即状态下的尺寸数 * `epsilon`:利用或探索行为的ε贪婪条件的阈值 * `parameter_changing_pointer`:一个整数值(例如`n`),指定在每`n`次迭代之后,将主网络的参数复制到目标网络 * `memory_size`:体验回复的最大长度 * `add_layer(self,inputs,w_shape=None,b_shape=None,layer=None,activation_fn=None,c=None,isconv=False)`:在神经网络中创建层的功能 * `weight_variable(self,w_shape,layer,c)`:创建重量参数的功能 * `bias_variable(self,b_shape,layer,c)`:创建偏置参数的功能 * `conv(self,inputs,w)`:对图像帧执行卷积运算的功能 * `build_networks()`:此功能用于使用 TensorFlow 创建主要网络和目标网络 * `target_params_replaced(self)`:用于将目标网络参数替换为主网络参数 * `store_experience(self,obs,a,r,obs_)`:帮助存储经验,即(状态,动作,奖励,新状态)的元组 * `fit(self)`:用于训练我们的深度 Q 网络 * `epsilon_greedy(self,obs)`:它可以帮助我们针对给定的观察状态选择正确的操作,即按照现有策略利用操作或随机探索新操作 现在,使用以下代码定义`__init__ default`构造器: ```py def __init__(self,learning_rate,gamma,n_features,n_actions,epsilon,parameter_changing_pointer, memory_size,epsilon_incrementer): tf.reset_default_graph() self.learning_rate = learning_rate self.gamma = gamma self.n_features = n_features self.n_actions = n_actions self.epsilon = epsilon self.batch_size = 32 self.experience_counter = 0 self.epsilon_incrementer = epsilon_incrementer self.experience_limit = memory_size self.replace_target_pointer = parameter_changing_pointer self.learning_counter = 0 self.memory = [] #np.zeros([self.experience_limit,4]) #for experience replay self.build_networks() p_params = tf.get_collection('primary_network_parameters') t_params = tf.get_collection('target_network_parameters') self.replacing_target_parameters = [tf.assign(t,p) for t,p in zip(t_params,p_params)] self.sess = tf.Session() self.sess.run(tf.global_variables_initializer()) ``` 以下代码定义了`add_layer`函数,该函数通过提供`isconv`的布尔参数来帮助根据卷积的要求创建不同的层或全连接层,如果`isconv`为`true`,则表示是卷积 层: ```py def add_layer(self,inputs,w_shape=None,b_shape=None,layer=None,activation_fn=None,c=None,isconv=False): w = self.weight_variable(w_shape,layer,c) b = self.bias_variable(b_shape,layer,c) eps = tf.constant(value=0.000001, shape=b.shape) if isconv: if activation_fn is None: return self.conv(inputs,w)+b+eps else: h_conv = activation_fn(self.conv(inputs,w)+b+eps) return h_conv if activation_fn is None: return tf.matmul(inputs,w)+b+eps outputs = activation_fn(tf.matmul(inputs,w)+b+eps) return outputs ``` 接下来,我们具有`weight_variable`和`bias_variable`函数。 以下代码用于定义权重参数: ```py def weight_variable(self,w_shape,layer,c): return tf.get_variable('w'+layer,w_shape,initializer=tf.contrib.layers.xavier_initializer(), dtype=tf.float32,collections=c) ``` 定义偏置参数的代码: ```py def bias_variable(self,b_shape,layer,c): return tf.get_variable('b'+layer,b_shape,initializer=tf.contrib.layers.xavier_initializer(), dtype=tf.float32,collections=c) ``` 现在让我们定义`conv(self,inputs,w)`,该函数调用 TensorFlow 的`tf.nn.conv2d`函数并采用: * 输入为二维向量 * **权重**:形状的重量`[patch_size,patch_size,input_vector_depth,output_vector_depth]` * **跨步**:以`[1,x_movement,y_movement,1]`形式出现的列表,其中: * `x_movement`:定义水平移动补丁的步数 * `y_movement`:定义在垂直方向上移动的色块的步数 * **填充**:`SAME`或`VALID`(我们在第 1 章,“深度学习–架构和框架”中讨论了此和有效的填充) 我们将使用以下代码定义函数: ```py def conv(self,inputs,w): #strides [1,x_movement,y_movement,1] #stride[0] = stride[3] = 1 return tf.nn.conv2d(inputs,w,strides=[1,1,1,1],padding='SAME') ``` 现在,让我们定义`build_networks(self)`。 它是在以下情况下建立主要和目标网络的功能: * 在`variable_scope`下创建主要网络参数,即:`primary_network`和集合`primary_network_parameters` * 在`variable_scope`下创建目标网络参数,即:`target_network` 和集合`target_network_parameters` * 两者具有相同的结构,即: * 卷积层 1 * 卷积层 2 * 全连接层 1 * 全连接层 2 * 使用的激活函数:ReLU 该功能还有助于: * 计算主网络输出的 Q 值与目标网络输出的 Q 值之间的损失 我们可以使用亚当优化器将这种损失降到最低。 现在,我们已经了解了该函数,让我们对其进行定义: ```py def build_networks(self): #primary network shape = [None] + self.n_features self.s = tf.placeholder(tf.float32,shape) self.qtarget = tf.placeholder(tf.float32,[None,self.n_actions]) with tf.variable_scope('primary_network'): c = ['primary_network_parameters', tf.GraphKeys.GLOBAL_VARIABLES] #first convolutional layer with tf.variable_scope('convlayer1'): l1 = self.add_layer(self.s,w_shape=[5,5,4,32],b_shape=[32],layer='convL1',activation_fn=tf.nn.relu,c=c,isconv=True) #first convolutional layer with tf.variable_scope('convlayer2'): l2 = self.add_layer(l1,w_shape=[5,5,32,64],b_shape=[64],layer='convL2',activation_fn=tf.nn.relu,c=c,isconv=True) #first fully-connected layer l2 = tf.reshape(l2,[-1,80*80*64]) with tf.variable_scope('FClayer1'): l3 = self.add_layer(l2,w_shape=[80*80*64,128],b_shape=[128],layer='fclayer1',activation_fn=tf.nn.relu,c=c) #second fully-connected layer with tf.variable_scope('FClayer2'): self.qeval = self.add_layer(l3,w_shape=[128,self.n_actions],b_shape=[self.n_actions],layer='fclayer2',c=c) with tf.variable_scope('loss'): self.loss = tf.reduce_mean(tf.squared_difference(self.qtarget,self.qeval)) with tf.variable_scope('optimizer'): self.train = tf.train.AdamOptimizer(self.learning_rate).minimize(self.loss) #target network self.st = tf.placeholder(tf.float32,shape) with tf.variable_scope('target_network'): c = ['target_network_parameters', tf.GraphKeys.GLOBAL_VARIABLES] #first convolutional layer with tf.variable_scope('convlayer1'): l1 = self.add_layer(self.st,w_shape=[5,5,4,32],b_shape=[32],layer='convL1',activation_fn=tf.nn.relu,c=c,isconv=True) #first convolutional layer with tf.variable_scope('convlayer2'): l2 = self.add_layer(l1,w_shape=[5,5,32,64],b_shape=[64],layer='convL2',activation_fn=tf.nn.relu,c=c,isconv=True) #first fully-connected layer l2 = tf.reshape(l2,[-1,80*80*64]) with tf.variable_scope('FClayer1'): l3 = self.add_layer(l2,w_shape=[80*80*64,128],b_shape=[128],layer='fclayer1',activation_fn=tf.nn.relu,c=c) #second fully-connected layer with tf.variable_scope('FClayer2'): self.qt = self.add_layer(l3,w_shape=[128,self.n_actions],b_shape=[self.n_actions],layer='fclayer2',c=c) ``` 现在,让我们定义`target_params_replaced(self)`,它是运行将主网络参数分配给目标网络参数的张量操作的函数: ```py def target_params_replaced(self): self.sess.run(self.replacing_target_parameters) ``` 现在,我们将定义`store_experience(self,obs,a,r,obs_)`函数,以将每种体验(即(状态,动作,奖励,新状态)的元组)存储在其体验缓冲区中,主要目标将在该缓冲区上进行训练: ```py def store_experience(self,obs,a,r,obs_): if len(obs.shape)<3 or len(obs_.shape)<3: print("Wrong shape entered : ",obs.shape,obs_.shape,len(self.memory)) else: index = self.experience_counter % self.experience_limit if self.experience_counter < self.experience_limit: self.memory.append([obs,a,r,obs_]) else: self.memory[index] = [obs,a,r,obs_] self.experience_counter+=1 ``` 现在,我们将定义`fit(self)`函数,以通过从经验缓冲区中选择一个批量来训练网络,计算`q_target`的张量值,然后最小化`qeval`之间的损失(即主数据库的输出) 网络)和`q_target`(即使用目标网络计算的 Q 值): ```py def fit(self): # sample batch memory from all memory indices = np.random.choice(len(self.memory), size=self.batch_size) batch = [self.memory[i] for i in indices] obs_nlist = np.array([i[3] for i in batch]) obs_list = np.array([i[0] for i in batch]) qt,qeval = self.sess.run([self.qt,self.qeval],feed_dict={self.st:obs_nlist,self.s:obs_list}) qtarget = qeval.copy() batch_indices = np.arange(self.batch_size, dtype=np.int32) actions = np.array([int(i[1]) for i in batch]) rewards = np.array([int(i[2]) for i in batch]) qtarget[batch_indices,actions] = rewards + self.gamma * np.max(qt,axis=1) _ = self.sess.run(self.train,feed_dict = {self.s:obs_list,self.qtarget:qtarget}) print(self.learning_counter+1," learning done") #increasing epsilon if self.epsilon < 0.9: self.epsilon += self.epsilon_incrementer #replacing target network parameters with primary network parameters if self.learning_counter % self.replace_target_pointer == 0: self.target_params_replaced() print("target parameters changed") self.learning_counter += 1 ``` 现在,我们将定义`epsilon_greedy(self,obs)`,该功能类似于我们在 DQN 中为山地车和 Cartpole 实现的功能: ```py def epsilon_greedy(self,obs): new_shape = [1]+list(obs.shape) obs = obs.reshape(new_shape) #epsilon greedy implementation to choose action if np.random.uniform(low=0,high=1) < self.epsilon: return np.argmax(self.sess.run(self.qeval,feed_dict={self.s:obs})) #[np.newaxis,:] else: return np.random.choice(self.n_actions) ``` 在类之外,我们有一个函数`preprocessing_image`,该函数用于预处理以下参数: * 裁剪图像 * 将其转换为灰度 * 对图像进行下采样 * 归一化图像 我们将使用以下代码定义函数: ```py def preprocessing_image(s): s = s[31:195] #cropping s = s.mean(axis=2) #converting to greyscale s = imresize(s,size=(80,80),interp='nearest') #downsampling s = s/255.0 #normalizing return s ``` 以下代码定义了`main`函数,该函数创建 DQN 的上一类的对象,使用`gym`来获取`Breakout-v0`环境,并训练代理程序解决问题: ```py if __name__ == "__main__": env = gym.make('Breakout-v0') env = env.unwrapped epsilon_rate_change = 0.9/500000.0 dqn = DQN(learning_rate=0.0001, gamma=0.9, n_features=[80,80,4], n_actions=env.action_space.n, epsilon=0.0, parameter_changing_pointer=100, memory_size=50000, epsilon_incrementer=epsilon_rate_change) episodes = 100000 total_steps = 0 for episode in range(episodes): steps = 0 obs = preprocessing_image(env.reset()) s_rec = np.stack([obs]*4,axis=0) s = np.stack([obs]*4,axis=0) s = s.transpose([1,2,0]) episode_reward = 0 while True: env.render() action = dqn.epsilon_greedy(s) obs_,reward,terminate,_ = env.step(action) obs_ = preprocessing_image(obs_) a = s_rec[1:] a = a.tolist() a.append(obs_) s_rec = np.array(a) s_ = s_rec.transpose([1,2,0]) dqn.store_experience(s,action,reward,s_) if total_steps > 1999 and total_steps%500==0: dqn.fit() episode_reward+=reward if terminate: break s = s_ total_steps+=1 steps+=1 print("Episode {} with Reward : {} at epsilon {} in steps {}".format(episode+1,episode_reward,dqn.epsilon,steps)) while True: #to hold the render at the last step when Car passes the flag env.render() ``` 由于权重参数很多,因此在普通计算机上进行收敛需要花费大量时间,而运行 GPU 的计算机运行成本很高。 但是,要见证在普通计算机上融合的可能性,请运行代码 5 到 6 个小时,以查看代理情况如何好转。 我建议,如果价格合理,请在带有 GPU 的计算机上运行它。 无论如何,前面的`main`函数的示例输出将如下所示: ```py .... .... .... Episode 992 with Reward : 0.0 at epsilon 0.0008766 in steps 174 Episode 993 with Reward : 2.0 at epsilon 0.0008766 in steps 319 (488, ' learning done') Episode 994 with Reward : 0.0 at epsilon 0.0008784 in steps 169 Episode 995 with Reward : 1.0 at epsilon 0.0008784 in steps 228 Episode 996 with Reward : 1.0 at epsilon 0.0008784 in steps 239 (489, ' learning done') Episode 997 with Reward : 4.0 at epsilon 0.0008802 in steps 401 (490, ' learning done') Episode 998 with Reward : 0.0 at epsilon 0.000882 in steps 171 Episode 999 with Reward : 4.0 at epsilon 0.000882 in steps 360 (491, ' learning done') Episode 1000 with Reward : 0.0 at epsilon 0.0008838 in steps 171 Episode 1001 with Reward : 1.0 at epsilon 0.0008838 in steps 238 (492, ' learning done') Episode 1002 with Reward : 1.0 at epsilon 0.0008856 in steps 249 Episode 1003 with Reward : 1.0 at epsilon 0.0008856 in steps 232 .... .... .... ``` 尝试使用不同的参数以更好地收敛。 # 蒙特卡罗树搜索算法 **蒙特卡洛树搜索**(**MCTS**)是一种规划算法,是在出现人工窄智能问题时做出最佳决策的一种方法。 MCTS 致力于解决问题的预先规划方法。 在诸如 **minimax** 和**游戏树**之类的早期算法未能显示出具有复杂问题的结果之后,MCTS 算法变得越来越重要。 那么,是什么使 MCTS 与过去的决策算法(例如 minimax)不同并且更好呢? 让我们首先讨论什么是 minimax。 # Minimax 和游戏树 **Minimax** 是 IBM Deep Blue 在 1996 年 2 月 10 日的国际象棋比赛中击败世界冠军 Gary Kasparov 的算法。 那时的胜利是一个非常重要的里程碑。 minimax 和游戏树都是有向图,其中每个节点代表游戏状态,即游戏位置,如下面的井字游戏图所示: ![](img/1d4b1704-d9ef-4d48-aaff-79d8161cc9e5.png) 井字游戏树。 顶部节点代表游戏的开始位置。 沿着树走下去导致游戏的结果位置 因此,通过搜索游戏树,由于节点和树中存在的节点和路径的组合,AI 代理可以选择最佳的移动方式。 这对于游戏复杂度处于可接受水平的问题很有用,因为随着复杂度的增加,游戏树的大小也会增加。 例如,国际象棋的游戏树具有比宇宙中原子更多的节点。 因此,在这种情况下,仅可能进行搜索。 因此,随着复杂度的增加,极大极小的可用性和游戏树减少。 另一个很好的例子是围棋的开放式中文游戏,其棋类复杂度为`10^360`,而棋类的复杂度为`10^123`。 由于具有如此高的复杂性,minimax 无法绘制评估函数,甚至无法创建如此大的游戏树。 因此,在《深蓝》取得成功的大约 20 年后,没有算法能够掌握围棋游戏。 原因很简单,当时的最新技术(例如 minimax)无法解决诸如围棋游戏之类具有很高复杂性的问题。 而且,解决围棋需要一种更人性化的学习方式,即基于交互。 因此,Google DeepMind 的 **AlphaGo** 被认为是最先进的 AI 代理,它能够在 2016 年使用深度强化学习成功击败 Lee Sedol,该学习用于神经网络,强化学习和 蒙特卡洛树搜索。 这是第一次以人类可以实现的方式完成 AI 任务,即通过不断的交互,并通过不断的反复试验过程来获取知识。 # 蒙特卡罗树搜索 那么,蒙特卡罗树搜索与 minimax 的方法有何不同?它如何在高度复杂的围棋游戏中进行提前计划,围棋有大量潜在的反制动作? MCTS 建立了一个看起来像游戏树的统计树,但是游戏树或 minimax 具有游戏位置,即有向图的节点中的游戏状态,在 MCTS 中,有向图的节点是该游戏的数量 状态,告诉我们成功模拟的次数(即在游戏结束时导致获胜的动作)相对于游戏状态经过的模拟总数的数量。 因此,仿真次数越多,越多的节点有机会成为仿真的一部分,从而导致收敛。 因此,每个节点的值取决于仿真次数。 收敛后,此统计树将指导 AI 代理在每个级别上寻找最佳可能节点,并继续进行直至达到目标。 最初,统计树用于扩展,以便通过多次仿真为可能的游戏状态添加更多节点。 收集足够数量的节点后,同时开始选择更好的节点,如果节点成功实现了问题目标,则每次仿真它们的价值都会增加,从而提高了实用性。 在选择策略中,MCTS 还通过在现有的,有希望的节点与可能更有希望的未探索节点之间保持平衡来纳入探索与利用的权衡。 因此,仿真次数越多,统计树越大,则节点值收敛得越准确,最优决策就越好。 MCTS 是独立于域的,不需要复杂的手写试探法。 因此,它是解决各种开放式 AI 问题的强大算法。 # SARSA 算法 **状态-动作-奖励-状态-动作**(**SARSA**)算法是一种基于策略的学习问题。 就像 Q 学习一样,SARSA 也是一个时差学习问题,也就是说,它会预测情节的下一步以估计未来的回报。 SARSA 和 Q 学习之间的主要区别在于,具有最大 Q 值的动作不用于更新当前状态动作对的 Q 值。 取而代之的是,选择作为当前策略结果的操作的 Q 值,或者由于采用诸如 ε 贪婪之类的探索步骤来更新当前状态操作对的 Q 值。 SARSA 之所以得名,是因为使用五元组`Q(s, a, r, s', a')`完成了 Q 值更新,其中: * `s`,`a`:当前状态和操作 * `r`:采取行动后观察到的奖励`a` * `s'`:在采取操作`a`后到达下一个状态 * `a'`:要在状态`s'`下执行的动作 SARSA 算法涉及的步骤如下: 1. 随机初始化 Q 表 2. 对于每个剧集: 1. 对于给定状态`s`,请从 Q 表中选择操作`a` 2. 执行动作`a` 3. 奖励`R`和新状态`s'` 4. 对于新状态`s'`,请从 Q 表中选择操作`a'` 5. 通过以下操作更新当前状态操作的 Q 值,即`Q(s, a)`对: ![](img/356ee392-4f51-400f-a5af-90f2b50ee30c.png) SARSA 算法的伪代码如下: ```py Create Q-table where rows represent different states and columns represent different actions Initialize Q(s,a) arbitrarily, e.g. 0 for all states set action value for terminal states as 0 For each episode: Start with the starting state that is Initialize s to start Choose action a for s using the policy derived from Q [e.g. -greedy, either for the given 's' which 'a' has the max Q-value or choose a random action] Repeat for each step in the episode: Take the chosen action a, observe reward R and new state s' Choose action a' for s' using the policy derived from Q [e.g. -greedy] Update until s is the terminal state end ``` # 用于 OpenAI Gym 山地车问题的 SARSA 算法 让我们尝试实现先前在山地车问题中解释过的 SARSA 算法。 该程序的初始部分与先前的 Q 学习器具有相似之处。 首先,我们将使用以下代码导入依赖项并检查山地车环境: ```py #importing the dependencies import gym import numpy as np #exploring Mountain Car environment env_name = 'MountainCar-v0' env = gym.make(env_name) print("Action Set size :",env.action_space) print("Observation set shape :",env.observation_space) print("Highest state feature value :",env.observation_space.high) print("Lowest state feature value:",env.observation_space.low) print(env.observation_space.shape) ``` 前面的打印语句输出以下内容: ```py Making new env: MountainCar-v0 ('Action Set size :', Discrete(3)) ('Observation set shape :', Box(2,)) ('Highest state feature value :', array([ 0.6 , 0.07])) ('Lowest state feature value:', array([-1.2 , -0.07])) (2,) ``` 接下来,我们将使用以下代码分配超参数,例如状态数,情节数,学习率(初始和最小值),折扣因子伽玛,情节中的最大步长以及 ε 贪婪的`ε`: ```py n_states = 40 # number of states episodes = 10 # number of episodes initial_lr = 1.0 # initial learning rate min_lr = 0.005 # minimum learning rate gamma = 0.99 # discount factor max_steps = 300 epsilon = 0.05 env = env.unwrapped env.seed(0) #setting environment seed to reproduce same result np.random.seed(0) #setting numpy random number generation seed to reproduce same random numbers ``` 我们的下一个任务是创建一个函数,对连续状态空间进行离散化。 离散化是将连续状态空间观察转换为离散状态空间集。 我们将使用以下代码执行离散化: ```py def discretization(env, obs): env_low = env.observation_space.low env_high = env.observation_space.high env_den = (env_high - env_low) / n_states pos_den = env_den[0] vel_den = env_den[1] pos_high = env_high[0] pos_low = env_low[0] vel_high = env_high[1] vel_low = env_low[1] pos_scaled = int((obs[0] - pos_low)/pos_den) #converts to an integer value vel_scaled = int((obs[1] - vel_low)/vel_den) #converts to an integer value return pos_scaled,vel_scaled ``` 到目前为止,每个任务都与我们在 Q 学习算法中所做的相似。 现在,SARSA 的实现从初始化 Q 表并相应地更新 Q 值开始,如以下代码所示。 同样,在这里,我们将奖励值更新为当前位置与最低点(即起点)之间的绝对差值,以便它通过远离中心即最低点来最大化奖励: ```py #Q table #rows are states but here state is 2-D pos,vel #columns are actions #therefore, Q- table would be 3-D q_table = np.zeros((n_states,n_states,env.action_space.n)) total_steps = 0 for episode in range(episodes): obs = env.reset() total_reward = 0 # decreasing learning rate alpha over time alpha = max(min_lr,initial_lr*(gamma**(episode//100))) steps = 0 #action for the initial state using epsilon greedy if np.random.uniform(low=0,high=1) < epsilon: a = np.random.choice(env.action_space.n) else: pos,vel = discretization(env,obs) a = np.argmax(q_table[pos][vel]) while True: env.render() pos,vel = discretization(env,obs) obs,reward,terminate,_ = env.step(a) total_reward += abs(obs[0]+0.5) pos_,vel_ = discretization(env,obs) #action for the next state using epsilon greedy if np.random.uniform(low=0,high=1) < epsilon: a_ = np.random.choice(env.action_space.n) else: a_ = np.argmax(q_table[pos_][vel_]) #q-table update q_table[pos][vel][a] = (1-alpha)*q_table[pos][vel][a] + alpha*(reward+gamma*q_table[pos_][vel_][a_]) steps+=1 if terminate: break a = a_ print("Episode {} completed with total reward {} in {} steps".format(episode+1,total_reward,steps)) while True: #to hold the render at the last step when Car passes the flag env.render() ``` 前面的程序将以以下方式打印: ```py Episode 1 completed with total reward 11167.6296185 in 36605 steps Episode 2 completed with total reward 830.204697241 in 2213 steps Episode 3 completed with total reward 448.46977318 in 1899 steps Episode 4 completed with total reward 930.154976751 in 3540 steps Episode 5 completed with total reward 6864.96292351 in 20871 steps Episode 6 completed with total reward 677.449030827 in 3995 steps Episode 7 completed with total reward 2994.99152751 in 7401 steps Episode 8 completed with total reward 724.212076546 in 3267 steps Episode 9 completed with total reward 192.502071909 in 928 steps Episode 10 completed with total reward 213.212231118 in 786 steps ``` 因此,我们已经能够成功实现山地车问题的 SARSA 算法。 # 总结 我们知道强化学习可以优化环境中智能体的回报,**马尔可夫决策过程**(**MDP**)是一种环境表示和数学框架,用于使用状态对决策进行建模 ,动作和奖励。 在本章中,我们了解到 Q 学习是一种无需任何转移模型即可为任何 MDP 找到最佳动作选择策略的方法。 另一方面,如果给出了转换模型,则值迭代会为任何 MDP 找到最佳的动作选择策略。 我们还学习了另一个重要的话题,称为深度 Q 网络,这是一种经过改进的 Q 学习方法,它采用深度神经网络作为函数逼近器来在不同环境中进行泛化,这与特定于环境的 Q 表不同。 此外,我们还学会了在 OpenAI Gym 环境中实现 Q 学习,深度 Q 网络和 SARSA 算法。 先前显示的大多数实现可能在具有更好的超参数值和更多训练集的情况下效果更好。 在下一章中,我们将详细介绍著名的异步优势参与者批评算法。