Skip to content

Latest commit

 

History

History
1511 lines (1128 loc) · 74.4 KB

05.md

File metadata and controls

1511 lines (1128 loc) · 74.4 KB

五、Q 学习和深度 Q 网络

在第 3 章,“马尔可夫决策过程”中,我们讨论了遵循马尔可夫性质的环境转移模型以及延迟的奖励和值函数的概念(或工具)。 好,在本章中,我们将研究马尔可夫决策过程,了解 Q 学习,以及一种称为深度 Q 网络的改进方法,用于在不同环境中进行泛化。

我们将在本章介绍以下主题:

  • 人工智能的有监督和无监督学习
  • 基于模型的学习和无模型学习
  • Q 学习
  • 深度 Q 网络
  • 蒙特卡罗树搜索算法
  • SARSA 算法

为什么是强化学习?

2014 年,Google 以高达 5 亿美元的价格收购了伦敦一家名为 DeepMind 的创业公司。 在新闻中,我们了解到他们创建了一个 AI 智能体来击败任何 Atari 游戏,但是 Google 付出如此高的价格收购它的主要原因是因为这一突破向通用人工智能靠近了一步。 通用人工智能被称为 AI 智能体。 它能够完成各种任务并像人类一样泛化。 当它超过这一点时,该点称为人工超级智能。 目前,AI 社区所做的工作就是我们所说的人工智能,即人工智能,其中 AI 智能体能够执行多个任务,但不能概括各种任务。

DeepMind 在研究期刊 Nature 上发表了他们的论文《通过深度强化学习进行人类水平控制》,这表明,他们的深度强化学习算法可成功应用于 50 种不同的 Atari 游戏,并在其中 30 种游戏中达到高于人类水平的表现。 他们的 AI 智能体称为深度 Q 学习器。 在详细深入学习强化学习之前,让我们回顾一下强化学习的基础知识。

有监督和无监督的学习是 AI 应用社区众所周知的。 监督学习处理包含输入特征和目标标签(连续或离散)的标记数据集,并创建将这些输入特征映射到目标标签的模型。 另一方面,无监督学习处理仅包含输入特征但不包含目标标签的未标记数据集,其目的是发现基础模式,以将数据分类到不同集群中,并分别定义其效用。 不同集群中特定类型的数据。

因此,使用有监督和无监督的学习,我们可以创建数据分类器/回归器或数据生成器,通过一次学习就可以通过一批数据进行学习。 为了随着时间的推移增强学习,该批量需要合并越来越多的数据,从而导致有监督和无监督的学习变得缓慢且难以推广。 让我们考虑一种情况,您希望 AI 智能体为您玩特定的视频/虚拟游戏,但要注意的是,随着时间的流逝,该算法应该变得智能。

那么,如何解决这个问题呢?

假设我们拍摄了特定视频游戏中所有最佳玩家的视频,并以图像帧和目标标签的形式输入数据,作为可能采取的不同动作的集合。 由于我们具有输入特征和目标标签,因此形成了监督学习分类问题。 假设数据量巨大,并且我们可以使用具有最新 GPU 的高端机器,那么为该任务创建一个深度神经网络完全有意义。

但是这里有什么收获呢?

为了创建可解决此分类问题的深度神经网络,以使最终的 AI 智能体可以击败该游戏中任何级别的任何对手,我们的输入数据需要数千小时的视频数据分配到不同级别的游戏中, 不同的玩家,采用不同的方法赢得比赛,因此我们的神经网络可以以最佳方式概括映射。 获得更多数据的原因是为了避免欠拟合。 此外,过拟合中的大量数据也可能是一个问题,但是正则化是根据给定数据将模型推广到最佳状态的可能解决方案。 因此,我们看到,即使在获得了数千小时的专家播放器的视频数据(即非常高的数据量)之后,这种有监督的学习方法似乎也不是一个很好的解决方案。 这是因为,与其他应用的 AI 问题不同,此处的数据集是动态的而不是静态的。

此处的训练数据是连续的,新的框架在游戏世界中不断出现。 现在,问问自己我们人类如何学习这项任务,答案很简单,即我们通过与环境交互而不是看着其他人与环境交互来学习最好。 因此,AI 智能体可以尝试通过与环境交互来更好地学习,并通过一系列的反复试验来逐步发展其学习成果。

在现实世界和游戏世界中,环境通常是随机,其中可能发生许多事件。 由于所有事件都与某种发生概率相关联,因此可以对它们进行统计分析,但不能精确确定。 假设在给定的e环境中,我们只有三个动作来执行abc,但是每个动作都有一些与之相关的某种不确定性,即它们的出现机会是随机的,并且它们中的任何一个都可以发生,但是每个结果都不确定。 对于监督分类问题,我们认为环境是确定性的,其中确定了与特定动作相关的结果,并且结果是精确的预测,即特定类别(目标标签)。 在继续讨论该主题之前,让我们看一下两种环境之间的区别:

  • 确定性环境:由于没有不确定性,智能体的动作可以唯一地确定结果的环境。 例如,在国际象棋中,您将一块从一个正方形移到另一个正方形。 因此,确定结果,即结果平方。 没有随机性。
  • 随机环境:每个动作都与某种程度的随机性相关联的环境,因此,无论我们采取何种行动,都无法确定结果。 例如,将飞镖投掷到旋转的棋盘上或掷骰子。 在这两种情况下,结果均不确定。

因此,对于基于随机环境的问题,似乎最好的学习方法是尝试各种可能性。 因此,与其通过监督分类将其解决为模式识别问题,不如通过试错法更好,在这种方法中,结果标签将替换为量化特定操作对完成给定问题的最终目的的有用性的奖励。 声明。

这产生了环境-智能体交互方法,我们在第 1 章,“深度强化–架构和框架”中讨论了该方法,在该系统中,我们设计了一种使智能体与环境交互的系统。 首先,通过传感器感知状态,通过效应器对环境执行一些操作,然后接收反馈,即对所采取操作的奖励,如下图所示:

根据传感器在特定时间步长感测环境时接收到的信号,此处的状态基本上是智能体对环境的看法。

基于模型的学习和无模型学习

在第 3 章,“马尔可夫决策过程”中,我们使用状态,动作,奖励,转移模型和折扣因子来解决我们的马尔可夫决策过程,即 MDP 问题。 因此,如果 MDP 问题的所有这些元素均可用,我们可以轻松地使用规划算法为目标提出解决方案。 这种类型的学习称为基于模型的学习,其中 AI 智能体将与环境交互,并基于其交互,将尝试近似环境的模型,即状态转换模型。 给定模型,现在智能体可以尝试通过值迭代或策略迭代找到最佳策略。

但是,对于我们的 AI 智能体来说,学习环境的显式模型不是必需的。 它可以直接从与环境的交互中得出最佳策略,而无需构建模型。 这种学习称为无模型学习。 无模型学习涉及在没有具体环境模型的情况下预测某个策略的值函数。

可以使用两种方法完成无模型学习:

  • 蒙特卡洛学习
  • 时差学习

我们将在以下主题中讨论它们两者。

蒙特卡洛学习

蒙特卡洛(Monte Carlo)是用于模型免费学习的最简单方法,在该方法中,智能体会观察剧集中前进的所有步骤(即前瞻)的回报。 因此,在时间t时的总估计报酬为R[t]

这里,γ是折扣因子,T是剧集结束的时间步长。 我们可以使用以下代码初始化蒙特卡洛学习技术:

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

时差学习

与在蒙特卡洛学习中我们要全面展望未来不同,在时间差异学习中,我们只有一个展望,也就是说,我们只观察到剧集的下一步:

时间差异学习是一种用于学习值和策略迭代方法中的值函数以及 Q 学习中的 Q 函数的方法。

如果我们希望我们的 AI 智能体始终选择最大化折扣未来奖励的行动,那么我们需要某种时间差异学习。 为此,我们需要定义一个函数 Q,该函数表示当我们在状态s上执行动作a时最大的未来折扣。 因此,Q 函数表示给定状态下的动作质量。 使用它,我们可以通过仅了解当前状态和操作来估计最终得分,而在此之后无需进行任何操作。 因此,目标将是对具有最高 Q 值的状态采取该措施。 因此,我们必须通过称为 Q 学习的过程来学习此 Q 函数。

策略内和策略外学习

顾名思义,脱离策略的学习是独立于智能体行为的最优策略学习。 因此,您不需要一开始就使用特定的策略,并且即使通过随机动作开始,智能体也可以学习最佳策略,最终收敛到最佳策略。 Q 学习是非策略学习的一个例子。

另一方面,基于策略的学习通过执行当前策略并通过探索方法对其进行更新来学习最佳策略。 因此,基于策略的学习取决于开始时的策略。 SARSA 算法是基于策略学习的示例。

Q 学习

在强化学习中,我们希望 Q 函数Q(s, a)预测状态s的最佳动作,以便最大化未来的回报。 使用 Q 学习估计 Q 函数,该过程涉及使用贝尔曼方程通过一系列迭代更新 Q 函数的过程,如下所示:

这里:

Q(s, a)为当前状态s和动作a对的Q

=学习收敛速度

=未来奖励的折扣系数

Q(s', a')为在状态s下采取动作a之后,所得状态s'的状态动作对的Q

R表示即时奖励

=未来奖励

在状态空间和动作空间是离散的更简单的情况下,使用 Q 表实现 Q 学习,其中表代表状态,列代表动作。

Q 学习涉及的步骤如下:

  1. 随机初始化 Q 表
  2. 对于每个剧集,请执行以下步骤:
    1. 对于给定状态s,从 Q 表中选择动作a
    2. 执行动作a
    3. 奖励R和状态s'
    4. 通过以下方法更新当前状态操作对的 Q 值,即Q(s, a)

但是,这里并没有探索新路径,并且在大多数情况下,智能体正在利用已知路径。 因此,实现了一定程度的随机性,以使 AI 智能体有时通过采取随机动作而不是当前的最佳动作来随机探索新路径。 探索背后的原因是,它增加了获得比当前更好的路径(即新的最佳策略)的可能性:

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 学习

山地车是强化学习领域的标准测试问题。 它由动力不足的汽车组成,必须将陡峭的山坡驱动到标志点,如下图所示:

这里的要点是重力要比汽车的引擎强,因此即使在全油门的情况下,汽车也无法在陡峭的斜坡上加速。 因此,汽车必须以相反的方向反向行驶来利用势能,然后再利用势能到达右上角的标志点。

在这里,状态空间是连续的,由两点定义:位置和速度。 对于给定的状态(即位置和速度),智能体可以采取三个离散的动作,即向前移动(向图中右上方),向相反方向(向图中左上方)或不使用引擎 ,即汽车处于空档。 智能体会收到负面奖励,直到达到目标状态。

Q 学习可以轻松地应用于具有离散状态空间和动作的环境,但是这个问题成为了强化学习算法的测试平台,因为它具有连续状态空间,并且需要离散化连续状态空间或函数逼近才能将其映射到离散类。

下面列出了山地车问题的技术细节,供您参考:

状态空间是二维且连续的。 它由位置和速度组成,具有以下值:

  • 位置(-1.2, 0.6)
  • 速度(-0.07, 0.07)

动作空间是离散的和一维的,具有三个选项:

  • (左,中,右)

每个时间步数都奖励 -1。

起始状态:

  • 位置:-0.5
  • 速度:0.0

终端状态条件:

  • 剧集在大于等于 0.6 的位置结束

正如我们现在看到的 Q 学习的参数一样,我们现在将研究解决山地车问题的 Q 学习的实现。

首先,我们将使用以下代码导入依赖项并检查山地车环境:

#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) 

先前的打印语句输出以下内容:

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,)

因此,我们看到动作空间是一个离散集合,显示了三个可能的动作,状态空间是一个二维连续空间,其中一个维度满足位置,而另一个则满足汽车的速度。 接下来,我们将使用以下代码分配超参数,例如状态数,剧集数,学习率(初始和最小值),折扣因子伽玛,剧集中的最大步数以及 ε 贪婪的ε

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

我们的下一个任务是创建一个函数来对连续状态空间进行离散化。 离散化是将连续状态空间观察转换为离散状态空间集:

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 学习算法。 在这里,我们将奖励值更新为当前位置与最低点(即起点)之间的绝对差值,以便它通过远离中心即最低点来最大化奖励。 这样做是为了实现更好的收敛:

#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 学习器将以以下方式打印输出:

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

这还将渲染一个环境,显示汽车在行驶并采取最佳路径并达到目标状态,如以下屏幕截图所示:

山地赛车游戏的最终状态视图

因此,我们看到无模型学习可以从其与环境的交互中得出最佳策略,而无需创建环境模型。 因此,我们已经知道,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 游戏中实现了超人水平的表现,如以下流程图所示:

使用经验回放

添加到深度 Q 网络的另一个重要功能是经验回放。 该功能背后的想法是,智能体可以存储其过去的经验,并分批使用它们来训练深度神经网络。 通过存储经验,智能体可以随机抽取批量,并帮助网络从各种数据中学习,而不仅仅是对即时经验的决策正式化。 这些经历中的每一个都以包括状态,动作,奖励下一个状态的四维向量的形式存储。

为了避免存储问题,经验回放的缓冲区是固定的,并且随着新体验的存储,旧体验将被删除。 为了训练神经网络,从缓冲区中提取均匀批量的随机经验。

分离目标网络来计算目标 Q 值

生成目标 Q 值的单独网络是一项重要功能,它使深层 Q 网络具有独特性。 由此单独的目标网络生成的 Q 值用于计算智能体在训练过程中采取的每项操作之后的损失。 使用两个网络而不是一个网络的原因在于,由于每个步骤的权重变化,主要的 Q 网络值在每个步骤中都在不断变化,这使得从该网络生成的 Q 值不稳定。

为了获得稳定的 Q 值,使用了另一个神经网络,其权重与主要 Q 网络相比变化缓慢。 这样,训练过程更加稳定。 DeepMind 的文章也发表在这个页面中。 他们发现这种方法能够稳定训练过程。

改编自 Minh 等人(2015),以下是 DQN 的伪代码:

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 值通过以下公式更新:

这里:

代表主网络的权重,并且

代表目标网络的权重。

决斗 DQN

决斗 DQN 的情况下,Q 值已被修改为状态的值函数和操作的优势函数的总和。 值函数V(s)量化处于状态s的有用性或优势,优势函数A(a)量化行为的优势代替其他可能的操作。 因此,

决斗 DQN 具有独立的网络来计算值和优势函数,然后将它们组合回以获取 Q 函数的值。 将值和优势的计算脱钩的原因在于,对于给定状态下的每个动作,智能体不必照顾不必要的值函数。 因此,将这些计算去耦会导致鲁棒的状态动作 Q 值。

用于 OpenAI Gym 山地车问题的深度 Q 网络

在针对山地车问题实现 Q 学习时,我们已经讨论了环境。 让我们直接深入实现一个深度 Q 网络来解决山地车问题。 首先,我们将使用以下代码导入所需的库:

#importing the dependencies

import numpy as np
import tensorflow as tf
import gym

让我们讨论一下 DQN 类,其中包含深度 Q 网络的架构:

  • __init__selflearning_rategamman_featuresn_actionsepsilonparameter_changing_pointermemory_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):对于给定的观察状态,要采取的操作,即根据现有策略利用操作或随机探索新操作

可以使用以下代码定义具有main函数的 DQN 类的架构:

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__:在以下代码片段中解释了默认构造器以及注释:

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_scopeprimary_network下创建主要网络参数,并在primary_network_parameters集合中创建
  • variable_scopetarget_network下创建目标网络参数,并在target_network_parameters集合中创建目标网络参数
  • 这两个参数具有相同的结构,即:
    • w1:与输入层关联的权重矩阵
    • b1:与输入层关联的偏置向量
    • ReLU:信号从输入到隐藏层的激活函数
    • w2:与隐藏层关联的权重矩阵
    • b2:与隐藏层关联的偏置向量
  • 计算主网络输出的 Q 值与目标网络输出的 Q 值之间的损失
  • 使用 adam 优化器将损失降到最低

我们将使用以下代码定义build_networks(self)函数:

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)。 运行将主网络参数分配给目标网络参数的张量操作的函数:

def target_params_replaced(self):
        self.sess.run(self.replacing_target_parameters)

现在,我们将定义store_experience(self,obs,a,r,obs_),该函数用于将每种体验(即(状态,动作,奖励,新状态)的元组)存储在其体验缓冲区中,通过该数组可以训练主要目标,例如以下代码:

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 值)。 我们将使用以下代码定义函数:

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)函数中,我们在每个学习步骤中都增加了ε的值:

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)

以下是main函数,该函数创建上一个 DQN 类的对象,使用 Gym 来获取MountainCar-v0环境,并训练智能体程序来解决问题。 像在 Q 学习中一样,在这里我们还更新了奖励值,将其作为当前位置与最低点位置(即起点)之间的绝对差值,从而使其偏离中心而最大化了奖励:

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() 

前面的程序将打印以下内容:

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

在这里,它收敛很快,但是还取决于对动作的探索和利用以及参数和超参数的初始化。 这还将渲染一个环境,显示汽车在行驶并采取最佳路径并达到目标状态,如以下屏幕截图所示:

接下来,在以下主题中,我们尝试实现一个深度 Q 网络来解决 OpenAI Gym 中的 Cartpole 问题。

用于 OpenAI Gym Cartpole 问题的深度 Q 网络

Cartpole 是 MDP 环境中最简单的问题之一,如以下屏幕快照所示。 它由一个在水平轴上移动的推车组成,该推车的中心处固定有一根可旋转的杆。 目的是采取行动,使电杆保持接近垂直且不会向下旋转。

车杆环境中的状态是一个 4 维连续空间,其中每个维如下:

  • x:表示推车位置(最小值为 -2.4,最大值为 2.4)
  • x_dot:表示推车速度(最小值为-∞
  • theta:显示以弧度为单位的角度(最小值为 -0.73,最大值为 0.73)
  • theta_dot:显示角速度(最小值为-∞,最大值为

在给定状态下的每一步,都有两种可能的动作,即推车可以向左或向右移动,并且每一步收到的奖励为 1。这里,只要杆子靠近垂直,推车在边界内。 如果发生以下情况,则剧集被视为结束:

  • 极点下降超过某个角度,即超过 ±0.20944 弧度
  • 推车超出框架左侧或右侧太远,即超出 ±2.4

因此,该问题的目的是将杆保持在接近垂直的位置,而推车不会越过边界越长。

为了为 Cartpole 问题实现深层 Q 网络,我们将导入先前创建的 DQN 类。 请按照以下代码在 Cartpole 环境中实现深度 Q 网络。 如果连续 100 次试验的平均奖励大于或等于 195,则认为该问题已解决:

#Importing dependencies
import gym
import numpy as np

#Importing the DQN class created preceding
from Deep_Q_Network_Mountain_Car import DQN

现在,我们将使用以下代码探索 Cartpole 环境:

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)

先前的打印语句输出以下内容:

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 环境以及训练智能体程序以解决问题的主要部分。 在这里,我们将奖励值更新为位置与极端位置之差和角度与极端极角之差的总和,因为远离极限位置,角度将变得更小,并且更接近推车的中央,因此奖励应该更高。 这样做是为了实现更好的主网络融合。 我们将使用此奖励进行学习,但是为了计算总体成功程度,我们将使用原始奖励:

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()

前面的程序将打印输出,如下所示:

.................
.................
.................
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 环境的初始图像帧:

Breakout-v0 环境的屏幕截图

观察空间是连续的,包含图像帧的像素值,并且动作空间是离散的,包括四个不同的动作。 每个图像帧的大小为210 * 160 * 3(高度为 210 像素,宽度为 160 像素,具有 3 个颜色通道,即 RGB)。 因此,我们可以拍摄灰度图像帧,因为不会丢失任何信息,并且尺寸将变为210 * 160。 仅拍摄状态的图像帧将不起作用,因为它无法捕获任何运动信息。 因此,我们将为每个状态堆叠四个连续的图像帧。 因此,状态大小将是4 * 210 * 160 = 134,440。 对于 Atari Breakout,在一定程度上降低采样率不会造成任何信息丢失。 此外,我们还可以裁剪图像框架以避免图像的不必要部分,并保留可能包含足够信息来玩游戏的重要部分。

首先,使用以下代码检查环境:

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())

这将输出以下语句:

State space shape : Box(210, 160, 3)
Action space shape : Discrete(4)
Type of actions : ['NOOP', 'FIRE', 'RIGHT', 'LEFT']

因此,我们得到了状态空间和动作空间的形状,以及球拍可以采取的四种不同类型的动作,即无动作(无操作的简称),开火(上方目标砖的球),向右走, 或向左移动以阻止球下降。

我们还检查示例裁剪并查看差异,如下图所示:

裁剪前(左)和裁剪后(右)

游戏以下列方式进行:

  • 底部的球拍将球击发,击中砖块以摧毁屏幕的顶层
  • 击中砖块后,球反弹回来
  • 球拍应向左或向右移动以击中球并阻止其掉落
  • 如果球落到下方,也就是说,从球拍下方的屏幕上移开,则游戏结束且玩家输
  • 如果球从球拍弹起,它将再次上升,从墙壁上弹起并击中更多砖块

因此,目标是通过摧毁所有积木而不让球进入球拍下来赢得比赛。

让我们开始实现一个深层的 Q 网络,以使我们的智能体学习 Atari Breakout 的游戏。 首先,我们将使用以下代码导入必要的库:

#Importing the dependencies

import numpy as np
import tensorflow as tf
import gym
from scipy.misc import imresize

具有以下main函数的class DQN的架构可以使用以下代码定义:

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构造器:

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的布尔参数来帮助根据卷积的要求创建不同的层或全连接层,如果isconvtrue,则表示是卷积层:

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_variablebias_variable函数。 以下代码用于定义权重参数:

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)

定义偏置参数的代码:

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:定义在垂直方向上移动的色块的步数
  • 填充SAMEVALID(我们在第 1 章,“深度学习–架构和框架”中讨论了此和有效的填充)

我们将使用以下代码定义函数:

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 值之间的损失

我们可以使用亚当优化器将这种损失降到最低。

现在,我们已经了解了该函数,让我们对其进行定义:

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),它是运行将主网络参数分配给目标网络参数的张量操作的函数:

def target_params_replaced(self):
     self.sess.run(self.replacing_target_parameters)

现在,我们将定义store_experience(self,obs,a,r,obs_)函数,以将每种体验(即(状态,动作,奖励,新状态)的元组)存储在其体验缓冲区中,主要目标将在该缓冲区上进行训练:

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 值):

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 实现的函数:

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,该函数用于预处理以下参数:

  • 裁剪图像
  • 将其转换为灰度
  • 对图像进行下采样
  • 归一化图像

我们将使用以下代码定义函数:

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环境,并训练智能体程序解决问题:

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函数的示例输出将如下所示:

....
....
....
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 和游戏树都是有向图,其中每个节点代表游戏状态,即游戏位置,如下面的井字游戏图所示:

井字游戏树。 顶部节点代表游戏的开始位置。 沿着树走下去导致游戏的结果位置

因此,通过搜索游戏树,由于节点和树中存在的节点和路径的组合,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 值更新,其中:

  • sa:当前状态和操作
  • 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)对:

SARSA 算法的伪代码如下:

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 学习器具有相似之处。

首先,我们将使用以下代码导入依赖项并检查山地车环境:

#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) 

前面的打印语句输出以下内容:

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,)

接下来,我们将使用以下代码分配超参数,例如状态数,剧集数,学习率(初始和最小值),折扣因子伽玛,剧集中的最大步长以及 ε 贪婪的ε

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

我们的下一个任务是创建一个函数,对连续状态空间进行离散化。 离散化是将连续状态空间观察转换为离散状态空间集。 我们将使用以下代码执行离散化:

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 值开始,如以下代码所示。 同样,在这里,我们将奖励值更新为当前位置与最低点(即起点)之间的绝对差值,以便它通过远离中心即最低点来最大化奖励:

#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()  

前面的程序将以以下方式打印:

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 算法。 先前显示的大多数实现可能在具有更好的超参数值和更多训练集的情况下效果更好。

在下一章中,我们将详细介绍著名的异步优势参与者批评算法。