【深度强化学习】OpenAI Gym的使用

前言

重读《Deep Reinforcemnet Learning Hands-on》, 常读常新, 极其深入浅出的一本深度强化学习教程。 本文的唯一贡献是对其进行了翻译和提炼, 加一点自己的理解组织成一篇中文笔记。

原英文书下载地址: 传送门
原代码地址: 传送门

第二章 OpenAI Gym

经过第一章对基本概念的介绍之后, 这一章进行一些代码相关的实战。

深入解析Agent

Agent, 就是实行某些决策 (policy),做出动作与环境交互的角色。 为了对Agent进行深入的解析, 我们首先创建一个简单的环境对象。 这个对象可能没什么实际意义, 但有助于概念的理解:

import torch
import random
class Environment:
    def __init__(self):
        self.steps_left = 10
    def get_observation(self):
        return [0, 0, 0]
    def get_actions(self):
        return [0, 1]
    def is_done(self):
        return self.steps_left == 0
    def action(self, action):
        if self.is_done():
            raise Exception("Game is over")
        self.steps_left -= 1
        return random.random()

上述代码实现了一个极为简单的环境, 但是却包含了所有重要组件:

  • __init__初始化: 环境状态初试化。 这里定义,可执行的动作步骤次数初始为10.
  • get_observation 获取观测: 这个方法用于返回当前的环境观测状态给Agent。 在这个简化例子中, 我们认为状态永远0, 即这个环境并没有什么变化状态。
  • get_actions: 告知Agent,当前可使用的Action集合,即动作空间。 这个集合偶尔会随着状态时间变化——比如机器人运动到角落里,那就不能再向右移动。 这个例子中认为只有0和1两种动作,也没有赋予具体的物理含义。
  • is_done: 判断回合 (episodes)是否结束。 回合可以理解为一轮游戏, 由一系列步骤组成。 比如本例之中, 从一开始初始化的10次剩余步骤,到最后10次全部走完, 就是一个回合结束。
  • action:最重要的一个方法。 核心任务分两样: 处理用户的动作进行响应返回该动作所对应的reward。在这一例子中, 处理用户的动作的响应就是剩余步骤减一, 而返回的奖励值是一个随机数。

接下来,是Agent 部分:

class Agent:
    def __init__(self):
        self.total_reward = 0.0
    def step(self, env):
        current_obs = env.get_observation()
        actions = env.get_actions()
        reward = env.action(random.choice(actions))
        self.total_reward += reward

同样是简单却包括了各种组件的分析:

  • init: 初始化,归零reward以便统计。
  • step 函数:核心函数,负责四项任务:
    • 观测环境: env.get_observation()
    • 进行决策:random.choice(actions)本例中使用随机决策。
    • 提交动作,和环境交互:env.action(random.choice(actions))
    • 获得奖励值,并统计: reward = env.action(random.choice(actions))

最后,使用粘合代码,进行程序运行:

if __name__ == "__main__":
    env = Environment()
    agent = Agent()
    while not env.is_done():
        agent.step(env)
    print("Total reward got: %.4f" % agent.total_reward)

这只是一段简单的示例代码: 强化学习的模型当然可以很复杂——这个环境可以是很复杂的物理环境, Agent也可以是使用了神经网络技术的最新RL算法。 但是,他们在本质上是相同的:

在每一步中, Agent负责从环境中获得观测, 并作出决策选择动作与环境交互。 其结果是到达新的状态(observation), 并获得返回的对应reward值。

有读者可能会问, 如果Agent 和 环境的结构, 如此接近, 是不是早已有人写好了相关的框架呢?

介绍框架前的准备

请确保有以下python库:

  • numpy
  • OpenCV Python bindings
  • Gym
  • Pytorch
  • Ptan

这是原文里提到, 经实践,发现 其实 只需 下载 Pytorch, Ptan 和 OpenCV, Pytorch自带numpy, Ptan自带Gym。

命令, 因书中版本匹配问题, Ptan不能适应于最新的Pytorch 1.4.0, 因此,需要用下方命令下载:
pip install torch==1.3.0+cpu torchvision==0.4.0+cpu -f https://download.pytorch.org/whl/torch_stable.html
这是下载pytorch 1.3.0版本, CPU, 不考虑CUDA的GPU加速。
然后Ptan只需
pip install Ptan即可了。

OpenCV的下载可以参考:传送门

OpenAI Gym API

Gym 是 OpenAI开发的一个API库, 提供了大量使用了统一接口的环境, 即 Env。以下将介绍, Gym提供的Env环境中包含哪些组件。

  • Action Space 动作空间: 在环境中允许操作的动作集合, 包括离散动作, 连续动作, 及他们的混合。
  • Observation Space 观测空间:观测即每个时间戳上环境提供给Agent的相关信息, 包括reward。Observation可以简单的只有几个数字, 也可以复杂到许多高维的图像。 观测空间也可以是离散的, 如 灯泡的开关状态。
  • 一个 step方法, 用于执行动作, 获取新状态 和 对应的 reward, 并显示回合是否结束。
  • 一个 reset方法, 让环境回到初始状态,重新开始一个新的回合。

Space 类

Gym 中 有一个 Space 抽象类,有以下继承类:

  • Discrete(n), 代表一个离散的空间 0 ~ n- 1。比如 Discrete(n=4)就可以用来代表动作空间 [上, 下, 左, 右]。
  • Box(low, high, shape), 代表一个连续的空间, 范围为low~high。比如, 一张210 * 160 的RGB图像, 其可以表示为 Box(low = 0, high =255, shape = (210, 160, 3)) 类的一个实例。
  • Tuple, 前两个类的组合, 如 Tuple(Discrete(4), Box(1,1, (1,)))。
    这些类用于代码实现 动作空间和观测空间。 他们都必须实现Space类的两个方法:
  • sample(), 从空间中随机采样一份, 如Discrete(4).sample()就可以表示从4个动作随机选择一个动作。
  • contains(), 检查输入是否存在于空间中, 一般用于检查执行的动作是否合理(在动作空间内)等。

Env 类

将之前的文字描述用实际代码来讲述:Env类包括以下四个类成员:

  • action_space: 环境内允许的动作集合。
  • observation_space:提供观测
  • reset(): 重置回合
  • step():最重要的方法:执行动作, 获取reward, 检测是否回合结束。

还有一些 类似 render()这样的方法, 可以让环境用更人性化的方式体现。 但我们现在的重点显然是reset() 和 step()两个方法。

reset()方法不需要任何参数, 就是重启游戏到初始状态,返回值即是初次观测。

step()方法

step方法一共做了以下四个任务:

  • 对环境执行本次决定的动作
  • 获取执行动作后的新观测状态
  • 获取本步中得到的reward
  • 获取游戏是否结束的指示

step方法的输入值只有一个, 就是执行的动作 action,其余都是返回值。

因此, 你大概已经get到了环境env的使用方法:
在一个循环中, 使用step()方法,直到回合结束。 然后用reset()方法重启, 循环往复。剩下还有一个问题:如何创造环境实例?

创建环境

Gym中有各种各样已被定义好的环境, 其命名格式都是:
环境名-vn, n代表版本号。 如 Breakout-v0。 Gym拥有超过700个已定义的环境, 除去重复的(版本不同的相同环境),也有100多种环境可供使用。 比如:

  • 传统控制问题: 一些经典RL论文的benchmark方法
  • Atari 2600: 经典的小游戏, 共63种。

更详细的Gym预定义环境介绍可以在官网找到, 这里不再赘述。

第一个Gym 环境实践: CartPole

在这里插入图片描述
CartPole环境是对CartPole这个小游戏的还原: 上图的木棍会往 两侧倾斜, 玩家的任务是移动 下方的木块——只能往左或往右,使得木棍不倒下。

  • 环境的观测值为4个浮点数:
    • 木棍质心的x坐标
    • 木棍的速度
    • 木棍与木块的夹角
    • 木棍的角速度
  • Agent的动作: 向左 或 向右
  • Reward奖励: 每一时间戳,木棍不倒, 奖励+1
  • is_done判断: 木棍倒下时,回合(episode)结束

实现一个随机的Agent

import gym
if __name__ == "__main__":
    env = gym.make("CartPole-v0")
    total_reward = 0.0
    total_steps = 0
    obs = env.reset()

    while True:
        action = env.action_space.sample()
        obs, reward, done, _ = env.step(action)
        total_reward += reward
        total_steps += 1
        if done:
            break
    print("Episode done in %d steps, total reward %.2f" % (total_steps, total_reward))

代码非常简单: 首先对奖励值, 步骤数初始化为0. 通过env.reset()函数, 获取初始观测obs。然后进入回合的循环, 当回合结束时(done=1)时退出循环。 通过 sample()方法, 随机在动作空间中选取一个动作, 然后使用env.step()方法, agent与env进行交互, 获取新的obs, 此步所得的reward, 以及是否结束的flag(done)。step()方法的最后一个返回参数为默认的extra_info, 即额外信息,而本例中并没有用到, 因此用 _ 来接受赋值。

由于是随机选取动作的无脑Agent, 结果具有随机性, 一般如下:
Episode done in 15 steps, total reward 15.00
一般在12-16次之间, 木棍就自然倒下了。 大部分环境都有其 “reward boundary”, 可以理解为对Agent的评价指标的benchmark——比如CartPole的reward boundary是195,即坚持195次。 那么显然, 这个随机的Agent的性能是非常低下的。 但这仅仅只是一个开始的示例, 后面我们会加以改进。

Gym 的 额外功能——装饰器和监视器

迄今为止,我们已经了解了Gym 三分之二的API功能。 这一额外功能并非必须, 但可以使得你的代码更加轻松自如。

装饰器 Wrappers

我们经常会遇到以下一些情况:

  • 希望把过往的观测储存下来, 并将最近的N个观测均返回给 Agent, 这在游戏任务中很常见。
  • 有时候希望对图像观测进行一些预处理,使得Agent更容易接受。
  • 有时候希望对reward进行归一化。

这时候, 装饰器可以允许你在原有的Gym自带的observation上进行自定义的修改。
在这里插入图片描述
针对不同的自定义需求, Wrapper也分为上图三个具体子类:观测装饰器,动作装饰器和奖励装饰器。 分别暴露了 observation(obs)函数接口, reward(rew)函数接口, 和 action(act)函数接口用于自定义的改写。

以下, 用一个动作装饰器作为例子展示下装饰器的使用。 我们的目的是自定义动作函数: 即在本来的策略下, 10%的概率采用随机的动作。 这一点在强化学习中很常用, 即不满足于现有策略, 也会使用随机策略来进行探索改进。

import gym

import random


class RandomActionWrapper(gym.ActionWrapper):
    def __init__(self, env, epsilon=0.1):
        super(RandomActionWrapper, self).__init__(env)
        self.epsilon = epsilon

    def action(self, action):
        if random.random() < self.epsilon:
            print("Random!")
            return self.env.action_space.sample()
        return action


if __name__ == "__main__":
    env = RandomActionWrapper(gym.make("CartPole-v0"))

    obs = env.reset()
    total_reward = 0.0

    while True:
        obs, reward, done, _ = env.step(0)
        total_reward += reward
        if done:
            break

    print("Reward got: %.2f" % total_reward)

首先, 创建一个RandomActionWrapper类, 继承自 ActionWrapper类。 初始化一个epsilon值0.1, 即代表10%的概率。 然后需要改写action()方法: 接受action为参数, 然后返回被我们修改的自定义的action,这里选用的就是random的随机动作, 实现了我们的目标。

后续的环境使用则和之前的例子没有区别, 在运行中没有用到action()类, 这个我们自定义的方法会在env.step()自动调用, 即我们简洁的完成了对action的自定义, 而在外部看来没有什么区别。

这里的装饰器类, 和python的装饰器的效果是类似的。 只是用法不同, python装饰器是用@来实现, 而这里则是用类的继承。

监视器 Monitor

用于记录训练过程,笔者这里暂时配置失败, 没能成功运行, 考虑到对强化学习的理解,影响不大,这里先不再深究。

总结

本章介绍了 Gym库的基本使用——如何用他人的框架快速搭建环境和Agent。 下一章中, 我们会快速回顾学习下 用pytorch实现 的 深度学习。

猜你喜欢

转载自blog.csdn.net/weixin_39274659/article/details/106892512