深度强化学习的偏差方差:如何使用OpenAIGym为Atari构建机器人

来自菜鸟教程
跳转至:导航、​搜索

作为 Write for DOnations 计划的一部分,作者选择了 Girls Who Code 来接受捐赠。

介绍

强化学习是控制理论中的一个子领域,它涉及随时间变化的控制系统,广泛包括自动驾驶汽车、机器人和游戏机器人等应用。 在本指南中,您将使用强化学习为 Atari 视频游戏构建机器人。 该机器人无权访问有关游戏的内部信息。 相反,它只能访问游戏的渲染显示和该显示的奖励,这意味着它只能看到人类玩家会看到的内容。

在机器学习中,机器人被正式称为 代理 。 在本教程的情况下,代理是系统中的“参与者”,根据决策功能行事,称为 policy。 主要目标是通过强有力的政策武装他们来发展强大的代理人。 换句话说,我们的目标是通过赋予他们强大的决策能力来开发智能机器人。

您将通过训练一个基本的强化学习代理开始本教程,该代理在玩经典的 Atari 街机游戏 Space Invaders 时会采取随机动作,这将作为您比较的基准。 在此之后,您将探索其他几种技术——包括 Q-learningdeep Q-learning最小二乘法——同时构建扮演 Space Invaders 和Frozen Lake,一个简单的游戏环境,包含在OpenAI发布的强化学习工具包Gym中。 通过学习本教程,您将了解控制机器学习中模型复杂性选择的基本概念。

先决条件

要完成本教程,您需要:

或者,如果您使用的是本地机器,您可以通过我们的 Python 安装和设置系列 阅读适用于您的操作系统的相应教程来安装 Python 3 并设置本地编程环境。

第 1 步 - 创建项目并安装依赖项

为了为您的机器人设置开发环境,您必须下载游戏本身和计算所需的库。

首先为此项目创建一个名为 AtariBot 的工作区:

mkdir ~/AtariBot

导航到新的 AtariBot 目录:

cd ~/AtariBot

然后为项目创建一个新的虚拟环境。 你可以随意命名这个虚拟环境; 在这里,我们将其命名为 ataribot

python3 -m venv ataribot

激活您的环境:

source ataribot/bin/activate

在 Ubuntu 上,从 16.04 版开始,OpenCV 需要安装更多的软件包才能运行。 其中包括 CMake(管理软件构建过程的应用程序)以及会话管理器、杂项扩展和数字图像合成。 运行以下命令来安装这些软件包:

sudo apt-get install -y cmake libsm6 libxext6 libxrender-dev libz-dev

注意: 如果您在运行 MacOS 的本地计算机上遵循本指南,您需要安装的唯一附加软件是 CMake。 使用 Homebrew 安装它(如果您遵循 先决条件 MacOS 教程 ,您将安装它),输入:

brew install cmake

接下来,使用pip安装wheel包,车轮包装标准的参考实现。 一个 Python 库,这个包用作构建轮子的扩展,并包括一个用于处理 .whl 文件的命令行工具:

python -m pip install wheel

除了 wheel,您还需要安装以下软件包:

  • Gym,一个 Python 库,可用于研究各种游戏,以及 Atari 游戏的所有依赖项。 Gym 由 OpenAI 开发,为每个游戏提供公共基准测试,以便统一/评估各种代理和算法的性能。
  • Tensorflow,深度学习库。 这个库使我们能够更有效地运行计算。 具体来说,它通过使用仅在 GPU 上运行的 Tensorflow 抽象构建数学函数来实现这一点。
  • OpenCV,前面提到的计算机视觉库。
  • SciPy,一个提供高效优化算法的科学计算库。
  • NumPy,一个线性代数库。

使用以下命令安装每个软件包。 请注意,此命令指定要安装每个包的哪个版本:

python -m pip install gym==0.9.5 tensorflow==1.5.0 tensorpack==0.8.0 numpy==1.14.0 scipy==1.1.0 opencv-python==3.4.1.15

在此之后,再次使用 pip 安装 Gym 的 Atari 环境,其中包括各种 Atari 视频游戏,包括 Space Invaders:

python -m pip install gym[atari]

如果您的 gym[atari] 软件包安装成功,您的输出将以以下内容结束:

OutputInstalling collected packages: atari-py, Pillow, PyOpenGL
Successfully installed Pillow-5.4.1 PyOpenGL-3.1.0 atari-py-0.1.7

安装了这些依赖项后,您就可以继续构建一个随机播放的代理,作为比较基准。

第 2 步 — 使用 Gym 创建基线随机代理

现在所需的软件已在您的服务器上,您将设置一个代理来玩经典 Atari 游戏 Space Invaders 的简化版本。 对于任何实验,都需要获取基线以帮助您了解模型的性能。 因为这个代理在每一帧都采取随机动作,所以我们将它称为我们的随机基线代理。 在这种情况下,您将与此基准代理进行比较,以了解您的代理在后续步骤中的执行情况。

使用 Gym,您可以维护自己的 游戏循环 。 这意味着您处理游戏执行的每一步:在每个时间步,您给 gym 一个新动作并询问 gym 游戏状态 。 在本教程中,游戏状态是游戏在给定时间步的外观,这正是您在玩游戏时所看到的。

使用您喜欢的文本编辑器,创建一个名为 bot_2_random.py 的 Python 文件。 在这里,我们将使用 nano

nano bot_2_random.py

注意: 在本指南中,机器人的名称与它们出现的步骤编号一致,而不是它们出现的顺序。 因此,这个机器人被命名为 bot_2_random.py 而不是 bot_1_random.py


通过添加以下突出显示的行来启动此脚本。 这些行包括一个解释该脚本将做什么的注释块和两个 import 语句,它们将导入该脚本最终需要的包以运行:

/AtariBot/bot_2_random.py

"""
Bot 2 -- Make a random, baseline agent for the SpaceInvaders game.
"""

import gym
import random

添加main功能。 在这个函数中,创建游戏环境——SpaceInvaders-v0——然后使用env.reset初始化游戏:

/AtariBot/bot_2_random.py

. . .
import gym
import random

def main():
    env = gym.make('SpaceInvaders-v0')
    env.reset()

接下来,添加一个 env.step 函数。 此函数可以返回以下类型的值:

  • state:应用提供的动作后游戏的新状态。
  • reward:状态发生的分数增加。 例如,这可能是当一颗子弹摧毁了一个外星人时,分数增加了 50 分。 然后,reward = 50。 在玩任何基于分数的游戏时,玩家的目标是最大化分数。 这与最大化总奖励是同义的。
  • done:情节是否结束,通常发生在玩家失去所有生命时。
  • info:你暂时搁置的无关信息。

您将使用 reward 来计算您的总奖励。 您还将使用 done 来确定玩家何时死亡,即 done 返回 True 的时间。

添加以下游戏循环,指示游戏循环直到玩家死亡:

/AtariBot/bot_2_random.py

. . .
def main():
    env = gym.make('SpaceInvaders-v0')
    env.reset()

    episode_reward = 0
    while True:
        action = env.action_space.sample()
        _, reward, done, _ = env.step(action)
        episode_reward += reward
        if done:
            print('Reward: %s' % episode_reward)
            break

最后,运行 main 函数。 包括 __name__ 检查以确保 main 仅在您直接使用 python bot_2_random.py 调用它时运行。 如果不加if勾选,Python文件执行时总会触发main,即使导入文件也会触发。 因此,最好将代码放在 main 函数中,仅在 __name__ == '__main__' 时执行。

/AtariBot/bot_2_random.py

. . .
def main():
    . . .
    if done:
        print('Reward %s' % episode_reward)
        break

if __name__ == '__main__':
    main()

保存文件并退出编辑器。 如果您使用 nano,请按 CTRL+XY,然后按 ENTER。 然后,通过键入以下命令运行您的脚本:

python bot_2_random.py

您的程序将输出一个数字,类似于以下内容。 请注意,每次运行文件时都会得到不同的结果:

OutputMaking new env: SpaceInvaders-v0
Reward: 210.0

这些随机结果提出了一个问题。 为了产生其他研究人员和从业者可以从中受益的工作,您的结果和试验必须是可重复的。 要更正此问题,请重新打开脚本文件:

nano bot_2_random.py

import random 之后,添加 random.seed(0)。 在 env = gym.make('SpaceInvaders-v0') 之后,添加 env.seed(0)。 总之,这些线以一致的起点“播种”环境,确保结果始终是可重复的。 您的最终文件将完全匹配以下内容:

/AtariBot/bot_2_random.py

"""
Bot 2 -- Make a random, baseline agent for the SpaceInvaders game.
"""

import gym
import random
random.seed(0)


def main():
    env = gym.make('SpaceInvaders-v0')
    env.seed(0)

    env.reset()
    episode_reward = 0
    while True:
        action = env.action_space.sample()
        _, reward, done, _ = env.step(action)
        episode_reward += reward
        if done:
            print('Reward: %s' % episode_reward)
            break


if __name__ == '__main__':
    main()

保存文件并关闭编辑器,然后通过在终端中键入以下内容来运行脚本:

python bot_2_random.py

这将输出以下奖励,确切地说:

OutputMaking new env: SpaceInvaders-v0
Reward: 555.0

这是您的第一个机器人,尽管它相当不智能,因为它在做出决定时不考虑周围环境。 为了更可靠地估计您的机器人的性能,您可以让代理一次运行多个剧集,报告多个剧集的平均奖励。 要配置它,首先重新打开文件:

nano bot_2_random.py

random.seed(0) 之后,添加以下突出显示的行,告诉代理玩游戏 10 集:

/AtariBot/bot_2_random.py

. . .
random.seed(0)

num_episodes = 10
. . .

env.seed(0) 之后,开始一个新的奖励列表:

/AtariBot/bot_2_random.py

. . .
    env.seed(0)
    rewards = []
. . .

env.reset() 中的所有代码嵌套到 for 循环中 main() 的末尾,迭代 num_episodes 次。 确保从 env.reset()break 的每一行缩进四个空格:

/AtariBot/bot_2_random.py

. . .
def main():
    env = gym.make('SpaceInvaders-v0')
    env.seed(0)
    rewards = []

    for _ in range(num_episodes):
        env.reset()
        episode_reward = 0

        while True:
            ...

break 之前,当前主游戏循环的最后一行,将当前情节的奖励添加到所有奖励列表中:

/AtariBot/bot_2_random.py

. . .
        if done:
            print('Reward: %s' % episode_reward)
            rewards.append(episode_reward)
            break
. . .

main 函数结束时,报告平均奖励:

/AtariBot/bot_2_random.py

. . .
def main():
    ...
            print('Reward: %s' % episode_reward)
            break
    print('Average reward: %.2f' % (sum(rewards) / len(rewards)))
    . . .

您的文件现在将与以下内容对齐。 请注意,以下代码块包含一些注释以阐明脚本的关键部分:

/AtariBot/bot_2_random.py

"""
Bot 2 -- Make a random, baseline agent for the SpaceInvaders game.
"""

import gym
import random
random.seed(0)  # make results reproducible

num_episodes = 10


def main():
    env = gym.make('SpaceInvaders-v0')  # create the game
    env.seed(0)  # make results reproducible
    rewards = []

    for _ in range(num_episodes):
        env.reset()
        episode_reward = 0
        while True:
            action = env.action_space.sample()
            _, reward, done, _ = env.step(action)  # random action
            episode_reward += reward
            if done:
                print('Reward: %d' % episode_reward)
                rewards.append(episode_reward)
                break
    print('Average reward: %.2f' % (sum(rewards) / len(rewards)))


if __name__ == '__main__':
    main()

保存文件,退出编辑器,然后运行脚本:

python bot_2_random.py

这将打印以下平均奖励,确切地说:

OutputMaking new env: SpaceInvaders-v0
. . .
Average reward: 163.50

我们现在对要击败的基线分数有了更可靠的估计。 但是,要创建一个优秀的代理,您需要了解强化学习的框架。 怎样才能使“决策”这一抽象概念更加具体?

了解强化学习

在任何游戏中,玩家的目标都是最大化他们的得分。 在本指南中,玩家的分数称为其 奖励 。 为了最大化他们的奖励,玩家必须能够改进其决策能力。 从形式上讲,决策是查看游戏或观察游戏状态并选择动作的过程。 我们的决策函数称为 policy; 策略接受一个状态作为输入并“决定”一个动作:

policy: state -> action

为了构建这样的功能,我们将从强化学习中的一组特定算法开始,称为 Q 学习算法 。 为了说明这些,考虑一个游戏的初始状态,我们称之为 state0:你的飞船和外星人都在他们的起始位置。 然后,假设我们可以访问一个神奇的“Q-table”,它告诉我们每个动作将获得多少奖励:

状态 行动 报酬
状态0 射击 10
状态0 3
状态0 剩下 3

shoot 动作将使您的奖励最大化,因为它会产生最高值的奖励:10。 如您所见,Q 表提供了一种基于观察到的状态做出决策的直接方法:

policy: state -> look at Q-table, pick action with greatest reward

但是,大多数游戏的状态太多,无法在表格中列出。 在这种情况下,Q 学习代理学习 Q 函数 而不是 Q 表。 我们使用这个 Q 函数类似于我们之前使用 Q 表的方式。 将表条目重写为函数为我们提供了以下信息:

Q(state0, shoot) = 10
Q(state0, right) = 3
Q(state0, left) = 3

给定一个特定的状态,我们很容易做出决定:我们只需查看每个可能的动作及其奖励,然后采取与最高预期奖励相对应的动作。 更正式地重新制定早期的政策,我们有:

policy: state -> argmax_{action} Q(state, action)

这满足了决策功能的要求:给定游戏中的一个状态,它决定一个动作。 然而,这个解决方案依赖于知道每个状态和动作的 Q(state, action)。 要估计 Q(state, action),请考虑以下几点:

  1. 鉴于对代理的状态、动作和奖励的许多观察,可以通过取运行平均值来获得每个状态和动作的奖励的估计值。
  2. Space Invaders 是一款奖励延迟的游戏:玩家在外星人被炸毁时获得奖励,而不是在玩家射击时获得奖励。 然而,玩家通过射击采取行动才是奖励的真正动力。 不知何故,Q 函数必须为 (state0, shoot) 分配一个正奖励。

这两个见解被编入以下等式:

Q(state, action) = (1 - learning_rate) * Q(state, action) + learning_rate * Q_target
Q_target = reward + discount_factor * max_{action'} Q(state', action')

这些等式使用以下定义:

  • state:当前时间步的状态
  • action:当前时间步采取的动作
  • reward:当前时间步的奖励
  • state':下一个时间步的新状态,假设我们采取了行动 a
  • action':所有可能的动作
  • learning_rate:学习率
  • discount_factor:折扣因子,当我们传播它时,奖励“降级”了多少

有关这两个方程的完整解释,请参阅这篇关于 Understanding Q-Learning 的文章。

考虑到对强化学习的这种理解,剩下的就是实际运行游戏并为新策略获得这些 Q 值估计。

第三步——为 Frozen Lake 创建一个简单的 Q-learning 代理

现在您有了基准代理,您可以开始创建新代理并将它们与原始代理进行比较。 在这一步中,您将创建一个使用 Q-learning 的代理,这是一种强化学习技术,用于教导代理在给定特定状态下采取哪些行动。 这个代理将玩一个新游戏,FrozenLake。 该游戏的设置在 Gym 网站上描述如下:

冬天来了。 你和你的朋友在公园里玩飞盘时,你猛地一掷,飞盘落在了湖中央。 水大部分是结冰的,但有几个洞冰已经融化了。 如果你走进其中一个洞,你就会掉进冰冷的水中。 此时,国际飞盘短缺,因此您绝对必须在湖中航行并取回飞盘。 然而,冰很滑,所以你不会总是朝着你想要的方向移动。

使用如下网格描述表面:

SFFF       (S: starting point, safe)
FHFH       (F: frozen surface, safe)
FFFH       (H: hole, fall to your doom)
HFFG       (G: goal, where the frisbee is located)

玩家从左上角开始,用 S 表示,然后向右下角的目标前进,用 G 表示。 可用的动作有 rightleftupdown,达到目标得分为 1。 有许多洞,用 H 表示,掉进一个洞里立刻得 0 分。

在本节中,您将实现一个简单的 Q 学习代理。 使用您之前学到的知识,您将创建一个在 explorationexploitation 之间进行权衡的代理。 在这种情况下,探索意味着智能体随机行动,而剥削意味着它使用其 Q 值来选择它认为的最佳行动。 您还将创建一个表来保存 Q 值,并随着代理的行为和学习而逐步更新它。

复制第 2 步中的脚本:

cp bot_2_random.py bot_3_q_table.py

然后打开这个新文件进行编辑:

nano bot_3_q_table.py

首先更新文件顶部描述脚本用途的注释。 因为这只是一个注释,所以脚本正常运行不需要此更改,但它有助于跟踪脚本的作用:

/AtariBot/bot_3_q_table.py

"""
Bot 3 -- Build simple q-learning agent for FrozenLake
"""

. . .

在对脚本进行功能修改之前,您需要为其线性代数实用程序导入 numpy。 在 import gym 正下方,添加突出显示的行:

/AtariBot/bot_3_q_table.py

"""
Bot 3 -- Build simple q-learning agent for FrozenLake
"""

import gym
import numpy as np
import random
random.seed(0)  # make results reproducible
. . .

random.seed(0) 下,为 numpy 添加一个种子:

/AtariBot/bot_3_q_table.py

. . .
import random
random.seed(0)  # make results reproducible
np.random.seed(0)
. . .

接下来,使游戏状态可访问。 将 env.reset() 行更新为以下内容,它将游戏的初始状态存储在变量 state 中:

/AtariBot/bot_3_q_table.py

. . .
    for _ in range(num_episodes):
        state = env.reset()
        . . .

env.step(...) 行更新为以下内容,它存储下一个状态 state2。 您将需要当前的 state 和下一个 - state2 - 来更新 Q 函数。

/AtariBot/bot_3_q_table.py

        . . .
        while True:
            action = env.action_space.sample()
            state2, reward, done, _ = env.step(action)
            . . .

episode_reward += reward 之后,添加一行更新变量 state。 这会保持变量 state 为下一次迭代更新,因为您会期望 state 反映当前状态:

/AtariBot/bot_3_q_table.py

. . .
        while True:
            . . .
            episode_reward += reward
            state = state2
            if done:
                . . .

if done 块中,删除打印每集奖励的 print 语句。 相反,您将输出多集的平均奖励。 if done 块将如下所示:

/AtariBot/bot_3_q_table.py

            . . .
            if done:
                rewards.append(episode_reward)
                break
                . . .

在这些修改之后,您的游戏循环将匹配以下内容:

/AtariBot/bot_3_q_table.py

. . .
    for _ in range(num_episodes):
        state = env.reset()
        episode_reward = 0
        while True:
            action = env.action_space.sample()
            state2, reward, done, _ = env.step(action)
            episode_reward += reward
            state = state2
            if done:
                rewards.append(episode_reward))
                break
                . . .

接下来,添加代理在探索和利用之间进行权衡的能力。 在主游戏循环之前(以 for... 开头),创建 Q 值表:

/AtariBot/bot_3_q_table.py

. . .
    Q = np.zeros((env.observation_space.n, env.action_space.n))
    for _ in range(num_episodes):
      . . .

然后,重写 for 循环以暴露剧集编号:

/AtariBot/bot_3_q_table.py

. . .
    Q = np.zeros((env.observation_space.n, env.action_space.n))
    for episode in range(1, num_episodes + 1):
      . . .

while True: 内部游戏循环中,创建 noiseNoise,或无意义的随机数据,有时会在训练深度神经网络时引入,因为它可以提高模型的性能和准确性。 请注意,噪声越高,Q[state, :] 中的值越不重要。 结果,噪声越高,代理越有可能独立于其对游戏的了解而行动。 换句话说,较高的噪声会促使智能体 探索 随机动作:

/AtariBot/bot_3_q_table.py

        . . .
        while True:
            noise = np.random.random((1, env.action_space.n)) / (episode**2.)
            action = env.action_space.sample()
            . . .

请注意,随着 episodes 的增加,噪声量呈二次方减少:随着时间的推移,代理探索的次数越来越少,因为它可以相信自己对游戏奖励的评估并开始 利用 它的知识。

更新 action 行,让您的代理根据 Q 值表选择动作,并内置一些探索:

/AtariBot/bot_3_q_table.py

            . . .
            noise = np.random.random((1, env.action_space.n)) / (episode**2.)
            action = np.argmax(Q[state, :] + noise)
            state2, reward, done, _ = env.step(action)
            . . .

然后,您的主游戏循环将匹配以下内容:

/AtariBot/bot_3_q_table.py

. . .
    Q = np.zeros((env.observation_space.n, env.action_space.n))
    for episode in range(1, num_episodes + 1):
        state = env.reset()
        episode_reward = 0
        while True:
            noise = np.random.random((1, env.action_space.n)) / (episode**2.)
            action = np.argmax(Q[state, :] + noise)
            state2, reward, done, _ = env.step(action)
            episode_reward += reward
            state = state2
            if done:
                rewards.append(episode_reward)
                break
                . . .

接下来,您将使用 Bellman 更新方程 更新 Q 值表,该方程广泛用于机器学习中,用于在给定环境中找到最优策略。

贝尔曼方程包含两个与该项目高度相关的想法。 首先,多次从特定状态采取特定动作将导致对与该状态和动作相关联的 Q 值的良好估计。 为此,您将增加该机器人必须播放的剧集数,以返回更强的 Q 值估计。 其次,奖励必须通过时间传播,以便为原始动作分配非零奖励。 这个想法在奖励延迟的游戏中最为明显; 例如,在 Space Invaders 中,玩家在外星人被炸毁时获得奖励,而不是在玩家射击时获得奖励。 然而,玩家投篮才是奖励的真正动力。 同样,Q 函数必须分配 (state0, shoot) 正奖励。

首先,将 num_episodes 更新为等于 4000:

/AtariBot/bot_3_q_table.py

. . .
np.random.seed(0)

num_episodes = 4000
. . .

然后,以另外两个变量的形式将必要的超参数添加到文件顶部:

/AtariBot/bot_3_q_table.py

. . .
num_episodes = 4000
discount_factor = 0.8
learning_rate = 0.9
. . .

在包含 env.step(...) 的行之后计算新的目标 Q 值:

/AtariBot/bot_3_q_table.py

            . . .
            state2, reward, done, _ = env.step(action)
            Qtarget = reward + discount_factor * np.max(Q[state2, :])
            episode_reward += reward
            . . .

Qtarget 之后的行中,使用新旧 Q 值的加权平均值更新 Q 值表:

/AtariBot/bot_3_q_table.py

            . . .
            Qtarget = reward + discount_factor * np.max(Q[state2, :])
            Q[state, action] = (1-learning_rate) * Q[state, action] + learning_rate * Qtarget
            episode_reward += reward
            . . .

检查您的主游戏循环现在是否与以下内容匹配:

/AtariBot/bot_3_q_table.py

. . .
    Q = np.zeros((env.observation_space.n, env.action_space.n))
    for episode in range(1, num_episodes + 1):
        state = env.reset()
        episode_reward = 0
        while True:
            noise = np.random.random((1, env.action_space.n)) / (episode**2.)
            action = np.argmax(Q[state, :] + noise)
            state2, reward, done, _ = env.step(action)
            Qtarget = reward + discount_factor * np.max(Q[state2, :])
            Q[state, action] = (1-learning_rate) * Q[state, action] + learning_rate * Qtarget
            episode_reward += reward
            state = state2
            if done:
                rewards.append(episode_reward)
                break
                . . .

我们训练代理的逻辑现在已经完成。 剩下的就是添加报告机制。

即使 Python 不强制执行严格的类型检查,也可以在函数声明中添加类型以保持简洁。 在文件顶部,在读取 import gym 的第一行之前,导入 List 类型:

/AtariBot/bot_3_q_table.py

. . .
from typing import List
import gym
. . .

learning_rate = 0.9 之后,在 main 函数之外,声明报告的间隔和格式:

/AtariBot/bot_3_q_table.py

. . .
learning_rate = 0.9
report_interval = 500
report = '100-ep Average: %.2f . Best 100-ep Average: %.2f . Average: %.2f ' \
         '(Episode %d)'

def main():
  . . .

main 函数之前,添加一个新函数,该函数将使用所有奖励列表填充此 report 字符串:

/AtariBot/bot_3_q_table.py

. . .
report = '100-ep Average: %.2f . Best 100-ep Average: %.2f . Average: %.2f ' \
         '(Episode %d)'

def print_report(rewards: List, episode: int):
    """Print rewards report for current episode
    - Average for last 100 episodes
    - Best 100-episode average across all time
    - Average for all episodes across time
    """
    print(report % (
        np.mean(rewards[-100:]),
        max([np.mean(rewards[i:i+100]) for i in range(len(rewards) - 100)]),
        np.mean(rewards),
        episode))


def main():
  . . .

将游戏更改为 FrozenLake 而不是 SpaceInvaders

/AtariBot/bot_3_q_table.py

. . .
def main():
    env = gym.make('FrozenLake-v0')  # create the game
    . . .

rewards.append(...) 之后,打印过去 100 集的平均奖励,并打印所有集的平均奖励:

/AtariBot/bot_3_q_table.py

            . . .
            if done:
                rewards.append(episode_reward)
                if episode % report_interval == 0:
                    print_report(rewards, episode)
                . . .

main() 函数结束时,再次报告两个平均值。 通过将读取 print('Average reward: %.2f' % (sum(rewards) / len(rewards))) 的行替换为以下突出显示的行来执行此操作:

/AtariBot/bot_3_q_table.py

. . .
def main():
    ...
                break
    print_report(rewards, -1)
. . .

最后,您已经完成了 Q-learning 代理。 检查您的脚本是否与以下内容一致:

/AtariBot/bot_3_q_table.py

"""
Bot 3 -- Build simple q-learning agent for FrozenLake
"""

from typing import List
import gym
import numpy as np
import random
random.seed(0)  # make results reproducible
np.random.seed(0)  # make results reproducible

num_episodes = 4000
discount_factor = 0.8
learning_rate = 0.9
report_interval = 500
report = '100-ep Average: %.2f . Best 100-ep Average: %.2f . Average: %.2f ' \
         '(Episode %d)'


def print_report(rewards: List, episode: int):
    """Print rewards report for current episode
    - Average for last 100 episodes
    - Best 100-episode average across all time
    - Average for all episodes across time
    """
    print(report % (
        np.mean(rewards[-100:]),
        max([np.mean(rewards[i:i+100]) for i in range(len(rewards) - 100)]),
        np.mean(rewards),
        episode))


def main():
    env = gym.make('FrozenLake-v0')  # create the game
    env.seed(0)  # make results reproducible
    rewards = []

    Q = np.zeros((env.observation_space.n, env.action_space.n))
    for episode in range(1, num_episodes + 1):
        state = env.reset()
        episode_reward = 0
        while True:
            noise = np.random.random((1, env.action_space.n)) / (episode**2.)
            action = np.argmax(Q[state, :] + noise)
            state2, reward, done, _ = env.step(action)
            Qtarget = reward + discount_factor * np.max(Q[state2, :])
            Q[state, action] = (1-learning_rate) * Q[state, action] + learning_rate * Qtarget
            episode_reward += reward
            state = state2
            if done:
                rewards.append(episode_reward)
                if episode % report_interval == 0:
                    print_report(rewards, episode)
                break
    print_report(rewards, -1)

if __name__ == '__main__':
    main()

保存文件,退出编辑器,然后运行脚本:

python bot_3_q_table.py

您的输出将匹配以下内容:

Output100-ep Average: 0.11 . Best 100-ep Average: 0.12 . Average: 0.03 (Episode 500)
100-ep Average: 0.25 . Best 100-ep Average: 0.24 . Average: 0.09 (Episode 1000)
100-ep Average: 0.39 . Best 100-ep Average: 0.48 . Average: 0.19 (Episode 1500)
100-ep Average: 0.43 . Best 100-ep Average: 0.55 . Average: 0.25 (Episode 2000)
100-ep Average: 0.44 . Best 100-ep Average: 0.55 . Average: 0.29 (Episode 2500)
100-ep Average: 0.64 . Best 100-ep Average: 0.68 . Average: 0.32 (Episode 3000)
100-ep Average: 0.63 . Best 100-ep Average: 0.71 . Average: 0.36 (Episode 3500)
100-ep Average: 0.56 . Best 100-ep Average: 0.78 . Average: 0.40 (Episode 4000)
100-ep Average: 0.56 . Best 100-ep Average: 0.78 . Average: 0.40 (Episode -1)

你现在有了你的第一个重要的游戏机器人,但让我们来看看这个平均奖励 0.78。 根据 Gym FrozenLake 页面,“解决”游戏意味着达到 0.78 的 100 集平均值。 通俗地说,“解决”的意思是“玩得很好”。 虽然不是在创纪录的时间内,但 Q-table 代理能够在 4000 集中解决 FrozenLake。

但是,游戏可能更复杂。 在这里,您使用了一个表来存储所有 144 种可能的状态,但考虑井字游戏,其中有 19,683 种可能的状态。 同样,考虑有太多可能的状态无法计算的 Space Invaders。 随着游戏变得越来越复杂,Q-table 是不可持续的。 出于这个原因,您需要某种方法来近似 Q 表。 随着您在下一步中继续试验,您将设计一个可以接受状态和动作作为输入并输出 Q 值的函数。

第 4 步 — 为 Frozen Lake 构建深度 Q 学习代理

在强化学习中,神经网络根据 stateaction 输入有效地预测 Q 的值,使用表格存储所有可能的值,但这在复杂游戏中变得不稳定。 相反,深度强化学习使用神经网络来逼近 Q 函数。 有关详细信息,请参阅 了解深度 Q-Learning

要习惯 Tensorflow,这是您在第 1 步中安装的深度学习库,您将使用 Tensorflow 的抽象重新实现迄今为止使用的所有逻辑,并且您将使用神经网络来逼近您的 Q 函数。 但是,您的神经网络将非常简单:您的输出 Q(s) 是一个矩阵 W 乘以您的输入 s。 这被称为具有一个 全连接层 的神经网络:

Q(s) = Ws

重申一下,目标是重新实现我们已经使用 Tensorflow 抽象构建的机器人的所有逻辑。 这将使您的操作更加高效,因为 Tensorflow 可以在 GPU 上执行所有计算。

首先从第 3 步复制您的 Q-table 脚本:

cp bot_3_q_table.py bot_4_q_network.py

然后使用 nano 或您喜欢的文本编辑器打开新文件:

nano bot_4_q_network.py

首先,更新文件顶部的注释:

/AtariBot/bot_4_q_network.py

"""
Bot 4 -- Use Q-learning network to train bot
"""

. . .

接下来,通过在 import random 正下方添加 import 指令来导入 Tensorflow 包。 此外,在 np.random.seed(0) 正下方添加 tf.set_radon_seed(0)。 这将确保此脚本的结果在所有会话中都是可重复的:

/AtariBot/bot_4_q_network.py

. . .
import random
import tensorflow as tf
random.seed(0)
np.random.seed(0)
tf.set_random_seed(0)
. . .

重新定义文件顶部的超参数以匹配以下内容,并添加一个名为 exploration_probability 的函数,该函数将返回每一步的探索概率。 请记住,在这种情况下,“探索”意味着采取随机行动,而不是采取 Q 值估计建议的行动:

/AtariBot/bot_4_q_network.py

. . .
num_episodes = 4000
discount_factor = 0.99
learning_rate = 0.15
report_interval = 500
exploration_probability = lambda episode: 50. / (episode + 10)
report = '100-ep Average: %.2f . Best 100-ep Average: %.2f . Average: %.2f ' \
         '(Episode %d)'
. . .

接下来,您将添加一个 单热编码 函数。 简而言之,单热编码是将变量转换为有助于机器学习算法做出更好预测的形式的过程。 如果您想了解有关 one-hot 编码的更多信息,可以查看 计算机视觉中的对抗性示例:如何构建然后愚弄基于情感的狗过滤器

report = ... 正下方,添加一个 one_hot 函数:

/AtariBot/bot_4_q_network.py

. . .
report = '100-ep Average: %.2f . Best 100-ep Average: %.2f . Average: %.2f ' \
         '(Episode %d)'

def one_hot(i: int, n: int) -> np.array:
    """Implements one-hot encoding by selecting the ith standard basis vector"""
    return np.identity(n)[i].reshape((1, -1))

def print_report(rewards: List, episode: int):
. . .

接下来,您将使用 Tensorflow 的抽象重写您的算法逻辑。 不过,在此之前,您需要先为您的数据创建 placeholders

main 函数中,在 rewards=[] 正下方,插入以下突出显示的内容。 在这里,您在时间 t(如 obs_t_ph)和时间 t+1(如 obs_tp1_ph)定义占位符,以及您的操作、奖励和 Q 目标的占位符:

/AtariBot/bot_4_q_network.py

. . .
def main():
    env = gym.make('FrozenLake-v0')  # create the game
    env.seed(0)  # make results reproducible
    rewards = []

    # 1. Setup placeholders
    n_obs, n_actions = env.observation_space.n, env.action_space.n
    obs_t_ph = tf.placeholder(shape=[1, n_obs], dtype=tf.float32)
    obs_tp1_ph = tf.placeholder(shape=[1, n_obs], dtype=tf.float32)
    act_ph = tf.placeholder(tf.int32, shape=())
    rew_ph = tf.placeholder(shape=(), dtype=tf.float32)
    q_target_ph = tf.placeholder(shape=[1, n_actions], dtype=tf.float32)

    Q = np.zeros((env.observation_space.n, env.action_space.n))
    for episode in range(1, num_episodes + 1):
        . . .

在以 q_target_ph = 开头的行的正下方,插入以下突出显示的行。 此代码通过计算所有 aQ(s, a) 来开始计算,以生成 q_current 和 Q(s', a')[X118X ] 为所有 a' 制作 q_target

/AtariBot/bot_4_q_network.py

    . . .
    rew_ph = tf.placeholder(shape=(), dtype=tf.float32)
    q_target_ph = tf.placeholder(shape=[1, n_actions], dtype=tf.float32)

    # 2. Setup computation graph
    W = tf.Variable(tf.random_uniform([n_obs, n_actions], 0, 0.01))
    q_current = tf.matmul(obs_t_ph, W)
    q_target = tf.matmul(obs_tp1_ph, W)

    Q = np.zeros((env.observation_space.n, env.action_space.n))
    for episode in range(1, num_episodes + 1):
        . . .

再次直接在您添加的最后一行下方,插入以下突出显示的代码。 前两行等效于在步骤 3 中添加的计算 Qtarget 的行,其中 Qtarget = reward + discount_factor * np.max(Q[state2, :])。 接下来的两行设置你的损失,而最后一行计算最大化你的 Q 值的动作:

/AtariBot/bot_4_q_network.py

    . . .
    q_current = tf.matmul(obs_t_ph, W)
    q_target = tf.matmul(obs_tp1_ph, W)

    q_target_max = tf.reduce_max(q_target_ph, axis=1)
    q_target_sa = rew_ph + discount_factor * q_target_max
    q_current_sa = q_current[0, act_ph]
    error = tf.reduce_sum(tf.square(q_target_sa - q_current_sa))
    pred_act_ph = tf.argmax(q_current, 1)

    Q = np.zeros((env.observation_space.n, env.action_space.n))
    for episode in range(1, num_episodes + 1):
        . . .

设置算法和损失函数后,定义优化器:

/AtariBot/bot_4_q_network.py

    . . .
    error = tf.reduce_sum(tf.square(q_target_sa - q_current_sa))
    pred_act_ph = tf.argmax(q_current, 1)

    # 3. Setup optimization
    trainer = tf.train.GradientDescentOptimizer(learning_rate=learning_rate)
    update_model = trainer.minimize(error)

    Q = np.zeros((env.observation_space.n, env.action_space.n))
    for episode in range(1, num_episodes + 1):
        . . .

接下来,设置游戏循环的主体。 为此,将数据传递给 Tensorflow 占位符,然后 Tensorflow 的抽象将处理 GPU 上的计算,并返回算法的结果。

首先删除旧的 Q 表和逻辑。 具体来说,删除定义 Q(在 for 循环之前)、noise(在 while 循环中)、action 的行、QtargetQ[state, action]。 将 state 重命名为 obs_t 并将 state2 重命名为 obs_tp1 以与您之前设置的 Tensorflow 占位符对齐。 完成后,您的 for 循环将匹配以下内容:

/AtariBot/bot_4_q_network.py

    . . .
    # 3. Setup optimization
    trainer = tf.train.GradientDescentOptimizer(learning_rate=learning_rate)
    update_model = trainer.minimize(error)

    for episode in range(1, num_episodes + 1):
        obs_t = env.reset()
        episode_reward = 0
        while True:

            obs_tp1, reward, done, _ = env.step(action)

            episode_reward += reward
            obs_t = obs_tp1
            if done:
                ...

for 循环的正上方,添加以下两条突出显示的行。 这些行初始化一个 Tensorflow 会话,该会话反过来管理在 GPU 上运行操作所需的资源。 第二行初始化计算图中的所有变量; 例如,在更新权重之前将权重初始化为 0。 此外,您将在 with 语句中嵌套 for 循环,因此将整个 for 循环缩进四个空格:

/AtariBot/bot_4_q_network.py

    . . .
    trainer = tf.train.GradientDescentOptimizer(learning_rate=learning_rate)
        update_model = trainer.minimize(error)

    with tf.Session() as session:
        session.run(tf.global_variables_initializer())

        for episode in range(1, num_episodes + 1):
            obs_t = env.reset()
            ...

在读取 obs_tp1, reward, done, _ = env.step(action) 的行之前,插入以下行来计算 action。 此代码评估相应的占位符并以一定概率将操作替换为随机操作:

/AtariBot/bot_4_q_network.py

            . . .
            while True:
                # 4. Take step using best action or random action
                obs_t_oh = one_hot(obs_t, n_obs)
                action = session.run(pred_act_ph, feed_dict={obs_t_ph: obs_t_oh})[0]
                if np.random.rand(1) < exploration_probability(episode):
                    action = env.action_space.sample()
                . . .

在包含 env.step(action) 的行之后,插入以下内容以训练神经网络估计 Q 值函数:

/AtariBot/bot_4_q_network.py

                . . .
                obs_tp1, reward, done, _ = env.step(action)

                # 5. Train model
                obs_tp1_oh = one_hot(obs_tp1, n_obs)
                q_target_val = session.run(q_target, feed_dict={obs_tp1_ph: obs_tp1_oh})
                session.run(update_model, feed_dict={
                    obs_t_ph: obs_t_oh,
                    rew_ph: reward,
                    q_target_ph: q_target_val,
                    act_ph: action
                })
                episode_reward += reward
                . . .

您的最终文件将匹配 这个托管在 GitHub 上的文件 。 保存文件,退出编辑器,然后运行脚本:

python bot_4_q_network.py

您的输出将以以下结束,确切地说:

Output100-ep Average: 0.11 . Best 100-ep Average: 0.11 . Average: 0.05 (Episode 500)
100-ep Average: 0.41 . Best 100-ep Average: 0.54 . Average: 0.19 (Episode 1000)
100-ep Average: 0.56 . Best 100-ep Average: 0.73 . Average: 0.31 (Episode 1500)
100-ep Average: 0.57 . Best 100-ep Average: 0.73 . Average: 0.36 (Episode 2000)
100-ep Average: 0.65 . Best 100-ep Average: 0.73 . Average: 0.41 (Episode 2500)
100-ep Average: 0.65 . Best 100-ep Average: 0.73 . Average: 0.43 (Episode 3000)
100-ep Average: 0.69 . Best 100-ep Average: 0.73 . Average: 0.46 (Episode 3500)
100-ep Average: 0.77 . Best 100-ep Average: 0.79 . Average: 0.48 (Episode 4000)
100-ep Average: 0.77 . Best 100-ep Average: 0.79 . Average: 0.48 (Episode -1)

您现在已经训练了您的第一个深度 Q 学习代理。 对于像 FrozenLake 这样简单的游戏,你的深度 Q 学习代理需要 4000 集来训练。 想象一下,如果游戏要复杂得多。 这需要多少训练样本来训练? 事实证明,代理可能需要 百万 个样本。 所需的样本数量称为样本复杂度,下一节将进一步探讨这个概念。

了解偏差-方差权衡

一般来说,样本复杂度与机器学习中的模型复杂度不一致:

  1. 模型复杂度:人们想要一个足够复杂的模型来解决他们的问题。 例如,像一条线这样简单的模型不足以复杂到预测汽车的轨迹。
  2. 样本复杂度:人们想要一个不需要很多样本的模型。 这可能是因为他们对标记数据的访问有限、计算能力不足、内存有限等。

假设我们有两种模型,一种简单,一种极其复杂。 为了使两个模型达到相同的性能,偏差方差告诉我们,极其复杂的模型将需要成倍增加的样本来训练。 恰当的例子:您的基于神经网络的 Q 学习代理需要 4000 集才能解决 FrozenLake。 向神经网络代理添加第二层会使必要的训练次数增加四倍。 随着神经网络越来越复杂,这种鸿沟只会越来越大。 为了保持相同的错误率,增加模型复杂度会成倍增加样本复杂度。 同样,降低样本复杂度会降低模型复杂度。 因此,我们无法最大化模型复杂性和最小化样本复杂性以满足我们的心愿。

然而,我们可以利用我们对这种权衡的了解。 有关 偏差方差分解 背后数学的直观解释,请参阅 了解偏差方差权衡 。 在高层次上,偏差-方差分解是将“真实误差”分解为两个部分:偏差和方差。 我们将“真实误差”称为 均方误差 (MSE),这是我们预测的标签和真实标签之间的预期差异。 下图显示了随着模型复杂度的增加“真实误差”的变化:

第 5 步 — 为 Frozen Lake 构建最小二乘代理

最小二乘方法,也称为线性回归,是一种广泛应用于数学和数据科学领域的回归分析方法。 在机器学习中,它通常用于寻找两个参数或数据集的最优线性模型。

在第 4 步中,您构建了一个神经网络来计算 Q 值。 在此步骤中,您将使用 岭回归 (最小二乘的一种变体)来计算此 Q 值向量,而不是神经网络。 希望使用像最小二乘这样简单的模型,解决游戏将需要更少的训练集。

首先从第 3 步复制脚本:

cp bot_3_q_table.py bot_5_ls.py

打开新文件:

nano bot_5_ls.py

同样,更新文件顶部的注释,描述该脚本将执行的操作:

/AtariBot/bot_4_q_network.py

"""
Bot 5 -- Build least squares q-learning agent for FrozenLake
"""

. . .

在文件顶部附近的导入块之前,再添加两个导入以进行类型检查:

/AtariBot/bot_5_ls.py

. . .
from typing import Tuple
from typing import Callable
from typing import List
import gym
. . .

在您的超参数列表中,添加另一个超参数 w_lr,以控制第二个 Q 函数的学习率。 此外,将集数更新为 5000,将折扣系数更新为 0.85。 通过将 num_episodesdiscount_factor 超参数更改为更大的值,代理将能够发出更强的性能:

/AtariBot/bot_5_ls.py

. . .
num_episodes = 5000
discount_factor = 0.85
learning_rate = 0.9
w_lr = 0.5
report_interval = 500
. . .

print_report 函数之前,添加以下高阶函数。 它返回一个 lambda——一个匿名函数——抽象出模型:

/AtariBot/bot_5_ls.py

. . .
report_interval = 500
report = '100-ep Average: %.2f . Best 100-ep Average: %.2f . Average: %.2f ' \
         '(Episode %d)'

def makeQ(model: np.array) -> Callable[[np.array], np.array]:
    """Returns a Q-function, which takes state -> distribution over actions"""
    return lambda X: X.dot(model)

def print_report(rewards: List, episode: int):
    . . .

makeQ 之后,添加另一个函数 initialize,它使用正态分布值初始化模型:

/AtariBot/bot_5_ls.py

. . .
def makeQ(model: np.array) -> Callable[[np.array], np.array]:
    """Returns a Q-function, which takes state -> distribution over actions"""
    return lambda X: X.dot(model)

def initialize(shape: Tuple):
    """Initialize model"""
    W = np.random.normal(0.0, 0.1, shape)
    Q = makeQ(W)
    return W, Q

def print_report(rewards: List, episode: int):
    . . .

initialize 块之后,添加一个 train 方法,该方法计算岭回归封闭式解,然后用新模型对旧模型进行加权。 它返回模型和抽象的 Q 函数:

/AtariBot/bot_5_ls.py

. . .
def initialize(shape: Tuple):
    ...
    return W, Q

def train(X: np.array, y: np.array, W: np.array) -> Tuple[np.array, Callable]:
    """Train the model, using solution to ridge regression"""
    I = np.eye(X.shape[1])
    newW = np.linalg.inv(X.T.dot(X) + 10e-4 * I).dot(X.T.dot(y))
    W = w_lr * newW + (1 - w_lr) * W
    Q = makeQ(W)
    return W, Q

def print_report(rewards: List, episode: int):
    . . .

train 之后,添加最后一个函数 one_hot,为您的状态和操作执行 one-hot 编码:

/AtariBot/bot_5_ls.py

. . .
def train(X: np.array, y: np.array, W: np.array) -> Tuple[np.array, Callable]:
    ...
    return W, Q

def one_hot(i: int, n: int) -> np.array:
    """Implements one-hot encoding by selecting the ith standard basis vector"""
    return np.identity(n)[i]

def print_report(rewards: List, episode: int):
    . . .

在此之后,您将需要修改训练逻辑。 在您编写的上一个脚本中,每次迭代都会更新 Q 表。 但是,此脚本将在每个时间步收集样本和标签,并每 10 步训练一个新模型。 此外,它不会使用 Q 表或神经网络,而是使用最小二乘模型来预测 Q 值。

转到 main 函数并将 Q 表 (Q = np.zeros(...)) 的定义替换为以下内容:

/AtariBot/bot_5_ls.py

. . .
def main():
    ...
    rewards = []

    n_obs, n_actions = env.observation_space.n, env.action_space.n
    W, Q = initialize((n_obs, n_actions))
    states, labels = [], []
    for episode in range(1, num_episodes + 1):
        . . .

for 循环之前向下滚动。 如果存储的信息过多,则直接在此下方添加以下行以重置 stateslabels 列表:

/AtariBot/bot_5_ls.py

. . .
def main():
    ...
    for episode in range(1, num_episodes + 1):
        if len(states) >= 10000:
            states, labels = [], []
            . . .

修改这一行之后直接定义state = env.reset()的行,使其变为如下。 这将立即对状态进行 one-hot 编码,因为它的所有用法都需要 one-hot 向量:

/AtariBot/bot_5_ls.py

. . .
    for episode in range(1, num_episodes + 1):
        if len(states) >= 10000:
            states, labels = [], []
        state = one_hot(env.reset(), n_obs)
. . .

while 主游戏循环的第一行之前,修改 states 的列表:

/AtariBot/bot_5_ls.py

. . .
    for episode in range(1, num_episodes + 1):
        ...
        episode_reward = 0
        while True:
            states.append(state)
            noise = np.random.random((1, env.action_space.n)) / (episode**2.)
            . . .

更新 action 的计算,降低噪声概率,并修改 Q 函数评估:

/AtariBot/bot_5_ls.py

. . .
        while True:
            states.append(state)
            noise = np.random.random((1, n_actions)) / episode
            action = np.argmax(Q(state) + noise)
            state2, reward, done, _ = env.step(action)
            . . .

添加 state2 的 one-hot 版本并修改 Qtarget 定义中的 Q 函数调用,如下所示:

/AtariBot/bot_5_ls.py

. . .
        while True:
            ...
            state2, reward, done, _ = env.step(action)

            state2 = one_hot(state2, n_obs)
            Qtarget = reward + discount_factor * np.max(Q(state2))
            . . .

删除更新 Q[state,action] = ... 的行并将其替换为以下行。 此代码获取当前模型的输出,并仅更新此输出中与当前采取的操作相对应的值。 因此,其他动作的 Q 值不会产生损失:

/AtariBot/bot_5_ls.py

. . .
            state2 = one_hot(state2, n_obs)
            Qtarget = reward + discount_factor * np.max(Q(state2))
            label = Q(state)
            label[action] = (1 - learning_rate) * label[action] + learning_rate * Qtarget
            labels.append(label)

            episode_reward += reward
            . . .

state = state2 之后,向模型添加定期更新。 这将每 10 个时间步训练您的模型:

/AtariBot/bot_5_ls.py

. . .
            state = state2
            if len(states) % 10 == 0:
                W, Q = train(np.array(states), np.array(labels), W)
            if done:
            . . .

仔细检查您的文件是否与 源代码 匹配。 然后,保存文件,退出编辑器,然后运行脚本:

python bot_5_ls.py

这将输出以下内容:

Output100-ep Average: 0.17 . Best 100-ep Average: 0.17 . Average: 0.09 (Episode 500)
100-ep Average: 0.11 . Best 100-ep Average: 0.24 . Average: 0.10 (Episode 1000)
100-ep Average: 0.08 . Best 100-ep Average: 0.24 . Average: 0.10 (Episode 1500)
100-ep Average: 0.24 . Best 100-ep Average: 0.25 . Average: 0.11 (Episode 2000)
100-ep Average: 0.32 . Best 100-ep Average: 0.31 . Average: 0.14 (Episode 2500)
100-ep Average: 0.35 . Best 100-ep Average: 0.38 . Average: 0.16 (Episode 3000)
100-ep Average: 0.59 . Best 100-ep Average: 0.62 . Average: 0.22 (Episode 3500)
100-ep Average: 0.66 . Best 100-ep Average: 0.66 . Average: 0.26 (Episode 4000)
100-ep Average: 0.60 . Best 100-ep Average: 0.72 . Average: 0.30 (Episode 4500)
100-ep Average: 0.75 . Best 100-ep Average: 0.82 . Average: 0.34 (Episode 5000)
100-ep Average: 0.75 . Best 100-ep Average: 0.82 . Average: 0.34 (Episode -1)

回想一下,根据 Gym FrozenLake 页面,“解决”游戏意味着达到 0.78 的 100 集平均值。 在这里,智能体平均达到 0.82,这意味着它能够在 5000 集中解决游戏。 虽然这并不能在更少的情节中解决游戏,但这种基本的最小二乘法仍然能够解决具有大致相同数量的训练情节的简单游戏。 尽管您的神经网络可能会变得越来越复杂,但您已经证明对于 FrozenLake,简单的模型就足够了。

至此,您已经探索了三个 Q 学习代理:一个使用 Q 表,另一个使用神经网络,第三个使用最小二乘法。 接下来,您将为更复杂的游戏构建深度强化学习代理:Space Invaders。

第 6 步 — 为太空入侵者创建深度 Q 学习代理

假设您完美地调整了之前的 Q-learning 算法的模型复杂度和样本复杂度,无论您选择的是神经网络还是最小二乘法。 事实证明,即使在训练集数特别多的情况下,这种不智能的 Q 学习代理在更复杂的游戏中仍然表现不佳。 本节将介绍两种可以提高性能的技术,然后您将测试使用这些技术训练的代理。

DeepMind 的研究人员开发了第一个能够在没有任何人为干预的情况下不断调整其行为的通用代理,他们还训练他们的代理玩各种 Atari 游戏。 DeepMind 的原始深度 Q 学习 (DQN) 论文 认识到两个重要问题:

  1. Correlated states:取我们游戏在时间 0 的状态,我们称之为 s0。 假设我们根据之前推导出的规则更新 Q(s0)。 现在,取时间 1 的状态,我们称之为 s1,并根据相同的规则更新 Q(s1)。 请注意,游戏在时间 0 的状态与其在时间 1 的状态非常相似。 例如,在 Space Invaders 中,外星人可能每个移动了一个像素。 更简洁地说,s0s1 非常相似。 同样,我们也期望 Q(s0)Q(s1) 非常相似,因此更新一个会影响另一个。 这会导致 Q 值波动,因为对 Q(s0) 的更新实际上可能会抵消对 Q(s1) 的更新。 更正式地说,s0s1相关的。 由于 Q 函数是确定性的,因此 Q(s1)Q(s0) 相关。
  2. Q 函数不稳定性:回想一下,Q 函数既是我们训练的模型,也是我们标签的来源。 假设我们的标签是真正代表 分布L 的随机选择的值。 每次更新 Q 时,我们都会更改 L,这意味着我们的模型正在尝试学习移动目标。 这是一个问题,因为我们使用的模型假设一个固定分布。

为了对抗相关状态和不稳定的 Q 函数:

  1. 可以保留一个称为 重放缓冲区 的状态列表。 每个时间步,您将观察到的游戏状态添加到此重放缓冲区。 您还可以从该列表中随机抽取一部分状态,并在这些状态上进行训练。
  2. DeepMind 的团队复制了 Q(s, a)。 一个称为 Q_current(s, a),它是您更新的 Q 函数。 您需要另一个用于后续状态的 Q 函数,Q_target(s', a'),您不会更新它。 回想一下 Q_target(s', a') 用于生成标签。 通过将 Q_currentQ_target 分离并修复后者,您可以修复标签采样的分布。 然后,你的深度学习模型可以花很短的时间学习这个分布。 一段时间后,您重新复制 Q_current 以获得新的 Q_target

您不会自己实现这些,但您将加载使用这些解决方案训练的预训练模型。 为此,请创建一个新目录,您将在其中存储这些模型的参数:

mkdir models

然后使用 wget 下载预训练的 Space Invaders 模型的参数:

wget http://models.tensorpack.com/OpenAIGym/SpaceInvaders-v0.tfmodel -P models

接下来,下载一个 Python 脚本,该脚本指定与您刚刚下载的参数关联的模型。 请注意,此预训练模型对输入有两个必须牢记的约束:

  • 必须将状态下采样或缩小到 84 x 84。
  • 输入由堆叠的四个状态组成。

稍后我们将更详细地解决这些限制。 现在,通过键入以下内容下载脚本:

wget https://github.com/alvinwan/bots-for-atari-games/raw/master/src/bot_6_a3c.py

您现在将运行这个预训练的 Space Invaders 代理来查看它的执行情况。 与我们过去使用的几个机器人不同,您将从头开始编写此脚本。

创建一个新的脚本文件:

nano bot_6_dqn.py

通过添加标题注释、导入必要的实用程序并开始主游戏循环来开始此脚本:

/AtariBot/bot_6_dqn.py

"""
Bot 6 - Fully featured deep q-learning network.
"""

import cv2
import gym
import numpy as np
import random
import tensorflow as tf
from bot_6_a3c import a3c_model


def main():

if __name__ == '__main__':
    main()

导入后立即设置随机种子以使您的结果可重复。 此外,定义一个超参数 num_episodes,它将告诉脚本运行代理的情节数量:

/AtariBot/bot_6_dqn.py

. . .
import tensorflow as tf
from bot_6_a3c import a3c_model
random.seed(0)  # make results reproducible
tf.set_random_seed(0)

num_episodes = 10

def main():
  . . .

在声明 num_episodes 之后的两行,定义了一个 downsample 函数,该函数将所有图像下采样到 84 x 84 的大小。 在将所有图像传递到预训练神经网络之前,您将对所有图像进行下采样,因为预训练模型是在 84 x 84 图像上训练的:

/AtariBot/bot_6_dqn.py

. . .
num_episodes = 10

def downsample(state):
    return cv2.resize(state, (84, 84), interpolation=cv2.INTER_LINEAR)[None]

def main():
  . . .

main 函数开始时创建游戏环境并为环境播种,以便结果可重现:

/AtariBot/bot_6_dqn.py

. . .
def main():
    env = gym.make('SpaceInvaders-v0')  # create the game
    env.seed(0)  # make results reproducible
    . . .

直接在环境种子之后,初始化一个空列表来保存奖励:

/AtariBot/bot_6_dqn.py

. . .
def main():
    env = gym.make('SpaceInvaders-v0')  # create the game
    env.seed(0)  # make results reproducible
    rewards = []
    . . .

使用您在此步骤开始时下载的预训练模型参数初始化预训练模型:

/AtariBot/bot_6_dqn.py

. . .
def main():
    env = gym.make('SpaceInvaders-v0')  # create the game
    env.seed(0)  # make results reproducible
    rewards = []
    model = a3c_model(load='models/SpaceInvaders-v0.tfmodel')
    . . .

接下来,添加一些行告诉脚本迭代 num_episodes 次以计算平均性能并将每个情节的奖励初始化为 0。 另外,添加一行重置环境(env.reset()),在过程中收集新的初始状态,用downsample()对该初始状态进行下采样,并使用[开始游戏循环X194X] 循环:

/AtariBot/bot_6_dqn.py

. . .
def main():
    env = gym.make('SpaceInvaders-v0')  # create the game
    env.seed(0)  # make results reproducible
    rewards = []
    model = a3c_model(load='models/SpaceInvaders-v0.tfmodel')
    for _ in range(num_episodes):
        episode_reward = 0
        states = [downsample(env.reset())]
        while True:
        . . .

新的神经网络不是一次接受一种状态,而是一次接受四种状态。 因此,您必须等到 states 列表至少包含四个状态后,才能应用预训练模型。 在读取 while True: 的行下方添加以下行。 这些告诉代理在少于四个状态时采取随机动作,或者如果至少有四个状态,则连接这些状态并将其传递给预训练模型:

/AtariBot/bot_6_dqn.py

        . . .
        while True:
            if len(states) < 4:
                action = env.action_space.sample()
            else:
                frames = np.concatenate(states[-4:], axis=3)
                action = np.argmax(model([frames]))
                . . .

然后采取行动并更新相关数据。 添加观察状态的下采样版本,并更新这一集的奖励:

/AtariBot/bot_6_dqn.py

        . . .
        while True:
            ...
                action = np.argmax(model([frames]))
            state, reward, done, _ = env.step(action)
            states.append(downsample(state))
            episode_reward += reward
            . . .

接下来,添加以下行来检查情节是否为 done,如果是,则打印情节的总奖励并修改所有结果的列表并提前中断 while 循环:

/AtariBot/bot_6_dqn.py

        . . .
        while True:
            ...
            episode_reward += reward
            if done:
                print('Reward: %d' % episode_reward)
                rewards.append(episode_reward)
                break
                . . .

whilefor 循环之外,打印平均奖励。 将其放在 main 函数的末尾:

/AtariBot/bot_6_dqn.py

def main():
    ...
                break
    print('Average reward: %.2f' % (sum(rewards) / len(rewards)))

检查您的文件是否与以下内容匹配:

/AtariBot/bot_6_dqn.py

"""
Bot 6 - Fully featured deep q-learning network.
"""

import cv2
import gym
import numpy as np
import random
import tensorflow as tf
from bot_6_a3c import a3c_model
random.seed(0)  # make results reproducible
tf.set_random_seed(0)

num_episodes = 10


def downsample(state):
    return cv2.resize(state, (84, 84), interpolation=cv2.INTER_LINEAR)[None]

def main():
    env = gym.make('SpaceInvaders-v0')  # create the game
    env.seed(0)  # make results reproducible
    rewards = []

    model = a3c_model(load='models/SpaceInvaders-v0.tfmodel')
    for _ in range(num_episodes):
        episode_reward = 0
        states = [downsample(env.reset())]
        while True:
            if len(states) < 4:
                action = env.action_space.sample()
            else:
                frames = np.concatenate(states[-4:], axis=3)
                action = np.argmax(model([frames]))
            state, reward, done, _ = env.step(action)
            states.append(downsample(state))
            episode_reward += reward
            if done:
                print('Reward: %d' % episode_reward)
                rewards.append(episode_reward)
                break
    print('Average reward: %.2f' % (sum(rewards) / len(rewards)))


if __name__ == '__main__':
    main()

保存文件并退出编辑器。 然后,运行脚本:

python bot_6_dqn.py

您的输出将以以下内容结束:

Output. . .
Reward: 1230
Reward: 4510
Reward: 1860
Reward: 2555
Reward: 515
Reward: 1830
Reward: 4100
Reward: 4350
Reward: 1705
Reward: 4905
Average reward: 2756.00

将此与第一个脚本的结果进行比较,您在其中为 Space Invaders 运行了一个随机代理。 在那种情况下,平均奖励只有 150 左右,这意味着这个结果要好二十倍以上。 但是,您只运行了三集的代码,因为它相当慢,而且三集的平均值不是一个可靠的指标。 运行超过 10 集,平均为 2756; 100多集,平均2500左右。 只有有了这些平均值,您才能轻松得出结论,您的代理确实表现得更好了一个数量级,并且您现在拥有一个可以很好地玩 Space Invaders 的代理。

但是,回想一下在上一节中提出的关于样本复杂性的问题。 事实证明,这个 Space Invaders 代理需要数百万个样本来训练。 事实上,这个代理需要 24 小时在四个 Titan X GPU 上训练到目前的水平; 换句话说,它需要大量的计算来充分训练它。 你能用更少的样本训练一个类似的高性能智能体吗? 前面的步骤应该为您提供足够的知识来开始探索这个问题。 使用更简单的模型和每个偏差 - 方差权衡,这可能是可能的。

结论

在本教程中,您为游戏构建了几个机器人,并探索了机器学习中的一个基本概念,称为偏差方差。 下一个很自然的问题是:您能否为更复杂的游戏(例如星际争霸 2)构建机器人? 事实证明,这是一个悬而未决的研究问题,并辅以来自 Google、DeepMind 和 Blizzard 合作者的开源工具。 如果这些是您感兴趣的问题,请参阅 OpenAI 公开呼吁研究,了解当前问题。

本教程的主要内容是偏差-方差权衡。 由机器学习从业者来考虑模型复杂性的影响。 尽管可以利用高度复杂的模型并在过多的计算、样本和时间上分层,但降低模型复杂性可以显着减少所需的资源。