Open AI Gym教程(5): 各种环境Wrappers

gym.wrappers自带个多种封装后的Wrapper,对Action,Reward,Obersvation 做了处理(其中Observation Wrapper可以看成类似于图像处理时的各种滤镜)

 

其中的Monitor 可以用来观测环境的参数并保持视频到本地。

outdir = '/tmp/random-agent-results'
env = wrappers.Monitor(env, directory=outdir, force=True)

TimeLimit 可以控制最大的episode的步数。
FrameStack 可以多个Observation 组合成一个多帧的Observation,比如你的AI算法需要连续的几个帧的数据做为输入。
RecordEpisodeStatistics 可以用来记录episodes的一些统计信息
PixelObservationWrapper 可以为原先的Observation增加一些图像数据。

此外gym.vector 目录定义了矢量化环境,一般Gym环境是单个环境,如果我们需要同时使用多个环境来设计我们的AI算法,我们使用使用矢量化环境。这时envs, actions, rewards,和 observations都会是个一维数组。

Open AI Gym教程(4): Wrapper封装后的Env

core.py定义一个Wrapper类,它是环境Env的一个子类,便于模块化变换,比如对环境的reward进行换算,Observation的彩色图像进行灰度处理,提供计算效率。

class Wrapper(Env):
    r"""Wraps the environment to allow a modular transformation.

    This class is the base class for all wrappers. The subclass could override
    some methods to change the behavior of the original environment without touching the
    original code.

    .. note::

        Don't forget to call ``super().__init__(env)`` if the subclass overrides :meth:`__init__`.

    """
    def __init__(self, env):
        self.env = env
        self.action_space = self.env.action_space
        self.observation_space = self.env.observation_space
        self.reward_range = self.env.reward_range
        self.metadata = self.env.metadata

    def __getattr__(self, name):
        if name.startswith('_'):
            raise AttributeError("attempted to get missing private attribute '{}'".format(name))
        return getattr(self.env, name)

    @property
    def spec(self):
        return self.env.spec

    @classmethod
    def class_name(cls):
        return cls.__name__

    def step(self, action):
        return self.env.step(action)

    def reset(self, **kwargs):
        return self.env.reset(**kwargs)

    def render(self, mode='human', **kwargs):
        return self.env.render(mode, **kwargs)

    def close(self):
        return self.env.close()

    def seed(self, seed=None):
        return self.env.seed(seed)

    def compute_reward(self, achieved_goal, desired_goal, info):
        return self.env.compute_reward(achieved_goal, desired_goal, info)

    def __str__(self):
        return '<{}{}>'.format(type(self).__name__, self.env)

    def __repr__(self):
        return str(self)

    @property
    def unwrapped(self):
        return self.env.unwrapped

 

在Core.py定义了三个子类ObservationWrapper,RewardWrapper和ActionWrapper。

class ObservationWrapper(Wrapper):
    def reset(self, **kwargs):
        observation = self.env.reset(**kwargs)
        return self.observation(observation)

    def step(self, action):
        observation, reward, done, info = self.env.step(action)
        return self.observation(observation), reward, done, info

    def observation(self, observation):
        raise NotImplementedError


class RewardWrapper(Wrapper):
    def reset(self, **kwargs):
        return self.env.reset(**kwargs)

    def step(self, action):
        observation, reward, done, info = self.env.step(action)
        return observation, self.reward(reward), done, info

    def reward(self, reward):
        raise NotImplementedError


class ActionWrapper(Wrapper):
    def reset(self, **kwargs):
        return self.env.reset(**kwargs)

    def step(self, action):
        return self.env.step(self.action(action))

    def action(self, action):
        raise NotImplementedError

    def reverse_action(self, action):
        raise NotImplementedError

 

从代码中可以看到,我们可以使用action,reward,observation 方法对之前的action,reward,observation进行转换后再使用到step方法中。
这几个类都是抽象类,具体的实现定义在gym.wrappers。我们后面具体介绍。

Open AI Gym教程(3): 环境 Env

gym/core.py 中定义了可能是Open AI Gym最重要的一个类 Env(环境)。它是对现实世界模型的一个简化的抽象。对应到游戏应用中,就是游戏的一个个场景,我们控制游戏人物(Agent)在这个世界中经历种种历险:

Env定义游戏角色(Agent)活动的环境,比如上图的超级玛丽的世界。ENV定义了几个接口函数:step, reset, render, close, seed。以及几个属性:

  • action_space 角色(Agent)可以实施的动作,比如Mario可以上下左右,跳跃等
  • observation_space 环境可能的状态的值域空间,比如Mario世界所有的场景
  • reward_range 奖励值得取值范围。比如Mario游戏的得分是一个可能的奖励值(实际我们实现AI算法时,光使用得分不一定能得到好的结果,可能需要综合其它因素来计算算法的奖励值),它的缺省范围是[-inf,+inf]

Step方法

step(self, action)

step步进游戏一步,比如按下右键(或者什么也不做),环境根据这个动作前进一步,变换游戏的场景,判断游戏是否结束等,这个方法返回四个值:

  • observation (观测量),输入一个动作后下个场景的状态,它的类型是一个对象,不同环境类型不同。
  • reward (奖励),浮点类型,前个动作所带来的奖励。也和环境有关,我们的目前是获取更多的奖励。
  • done ,游戏结束标志,布尔类型。 比如Flappy Bird游戏小鸟撞死了,我们一般需要检测这个标志以便重新开始下一轮尝试。
  • info 游戏中的一些辅助信息,也和场景有关。一般来说我们不能使用使用这些信息来构造我们的算法。

Reset方法

游戏结束后重置游戏,以便于下次尝试。它返回游戏的初始observation (观测量)。

Render方法

render(self, mode='human')

render 用来”显示”游戏的场景,这个方法不是每个环境都必须提供的。 它可以分三种模式

  • human   一般表示屏幕显示,用于人机交互。
  • rgb_array  返回一个numpy数组(x,y,3)代表图像的RGB数值,比较适合生成视频。
  • ansi 文字描述。

Close 方法

用来释放环境可能使用到的资源或其它工作。

Seed方法

seed(self, seed=None)

 

seed 提供环境中用到的随机数值生成器的种子。环境一般无需另外提供该方法,如果环境使用多个随机数值生成器,我们可以提供多个种子以免多个随机数值生成器产生的数值有相关性。

Open AI Gym教程(2): 值域空间Spaces

Gym除去各种环境定义外核心的代码其实并不大,下面是Gym源码的目录结构:

其中envs 占据了较大篇幅。单其核心部分是spaces, 和core.py 。这些定义了Env几个主要的类定,Env(环境),Spaces(值域),由space定义了Action和Observation的空间。这些构成了Action-Environment Loop的基本部件。

Spaces 中定义几种不同类型的值域空间类型,Space为基类,定义Space类型的接口:

class Space(object):
    """Defines the observation and action spaces, so you can write generic
    code that applies to any Env. For example, you can choose a random
    action.
    """
    def __init__(self, shape=None, dtype=None):
        import numpy as np  # takes about 300-400ms to import, so we load lazily
        self.shape = None if shape is None else tuple(shape)
        self.dtype = None if dtype is None else np.dtype(dtype)
        self.np_random = None
        self.seed()

    def sample(self):
        """Randomly sample an element of this space. Can be 
        uniform or non-uniform sampling based on boundedness of space."""
        raise NotImplementedError

    def seed(self, seed=None):
        """Seed the PRNG of this space. """
        self.np_random, seed = seeding.np_random(seed)
        return [seed]

    def contains(self, x):
        """
        Return boolean specifying if x is a valid
        member of this space
        """
        raise NotImplementedError

    def __contains__(self, x):
        return self.contains(x)

    def to_jsonable(self, sample_n):
        """Convert a batch of samples from this space to a JSONable data type."""
        # By default, assume identity is JSONable
        return sample_n

    def from_jsonable(self, sample_n):
        """Convert a JSONable data type to a batch of samples from this space."""
        # By default, assume identity is JSONable
        return sample_n

其中最主要的两种为 Discrete 和 Box

Discrete

Discrete为离散的整数 [0,1,2…n]

In [1]: from gym.spaces import *                                                                                               

In [2]: space = Discrete(5)                                                                                                    

In [3]: space.sample()                                                                                                         
Out[3]: 2

In [4]: space.sample()                                                                                                         
Out[4]: 1

In [5]: space.sample()                                                                                                         
Out[5]: 4

Box

Box 为连续的多维有理数空间R^n。 它表示n个闭区间的卡氏积。每个区间的取值可以是

[a, b], (-oo, b], [a, oo), or (-oo, oo)

Box 常见两种用法,一是每个维度的取值范围相同:

In [1]: from gym.spaces import *                                                                                               

In [2]: import numpy as np                                                                                                     

In [3]: space = Box(low=-1.0, high=2.0, shape=(3, 4), dtype=np.float32)                                                        

In [4]: space.sample()                                                                                                         
Out[4]: 
array([[-0.49652585,  0.9263435 ,  0.38507813,  0.783846  ],
       [ 0.85791075,  1.8828201 , -0.9763712 , -0.7506176 ],
       [ 0.5605676 ,  0.58183783, -0.43566808,  0.50398904]],
      dtype=float32)

In [5]: space.sample()                                                                                                         
Out[5]: 
array([[ 1.0351197 , -0.26707068,  1.2349498 , -0.03579823],
       [ 0.35440695,  1.6972734 , -0.94597757,  0.43317792],
       [ 1.7264552 ,  0.7422606 , -0.641941  ,  1.9083056 ]],
      dtype=float32)

另外一种是每个维度的取值范围不同:

In [7]: space = Box(low=np.array([-1.0, -2.0]), high=np.array([2.0, 4.0]), dtype=np.float32)                                   


In [8]: space.sample()                                                                                                         
Out[8]: array([-0.01725261, -1.8928218 ], dtype=float32)

In [9]: space.sample()                                                                                                         
Out[9]: array([-0.37487462, -0.4833201 ], dtype=float32)

In [10]: space.sample()                                                                                                        
Out[10]: array([-0.2957047,  1.2446854], dtype=float32)

MultiDiscrete

MultiDiscrete 是多维离散整数,这对于游戏类型的操作尤为有用,比如我们可以定义任天堂游戏操纵杆为一个多维离散整数集:

大多数环境我们用0代表不做任何操作NOOP.

1) Arrow Keys: Discrete 5  - NOOP[0], UP[1], RIGHT[2], DOWN[3], LEFT[4]  - params: min: 0, max: 4
2) Button A:   Discrete 2  - NOOP[0], Pressed[1] - params: min: 0, max: 1
3) Button B:   Discrete 2  - NOOP[0], Pressed[1] - params: min: 0, max: 1

可以使用MultiDiscrete([ 5, 2, 2 ])来表示:

In [11]: space = MultiDiscrete([ 5, 2, 2 ])                                                                                    

In [12]: space.sample()                                                                                                        
Out[12]: array([2, 1, 0])

In [13]: space.sample()                                                                                                        
Out[13]: array([2, 1, 1])

In [14]: space.sample()                                                                                                        
Out[14]: array([3, 0, 0])

Spaces其它的几种类型基本上也是有Discrete和Box组合而成,比如MultiBinary和MultiDiscrete非常类似,只是每个取值只能是二员0或者1.dict和tuple分别对应于字典和元组类型:

In [16]: space = Dict({"position": Discrete(2), "velocity": Discrete(3)})                                                      

In [17]: space.sample()                                                                                                        
Out[17]: OrderedDict([('position', 1), ('velocity', 2)])

In [18]: space.sample()                                                                                                        
Out[18]: OrderedDict([('position', 0), ('velocity', 0)])

In [19]: space = Tuple((Discrete(2), Discrete(3)))                                                                             

In [20]: space.sample()                                                                                                        
Out[20]: (0, 1)

In [21]: space.sample()                                                                                                        
Out[21]: (1, 0)

In [22]: space.sample()                                                                                                        
Out[22]: (1, 2)

In [23]: space = MultiBinary(5)                                                                                                

In [24]: space.sample()                                                                                                        
Out[24]: array([0, 1, 1, 1, 1], dtype=int8)

In [25]: space.sample()                                                                                                        
Out[25]: array([1, 0, 1, 0, 1], dtype=int8)

In [26]: space.sample()                                                                                                        
Out[26]: array([1, 0, 1, 0, 1], dtype=int8)

 

Open AI Gym教程(1): 概述

前面提到计划开发一个通用的Android游戏OpenAI Gym环境,完成了iRobot Android Agent开发后,后面的主要工作是完成相应的Gym 环境的开发。Open AI Gym现有的文档比较简洁。因此只有通过对源码的分析才能对Gym环境有比较好的理解。

由Open AI 开发的Gym 环境提供了一些标准的测试环境(Environment)用来测试增强型机器学习(Reinforcement Learning)的算法。这些环境提供了一些标准接口,可以用来开发一些通用的算法。

本篇介绍Gym 环境的一些基本概念,都后续开发自定义的环境非常有用。

环境(Environment)

环境是对于我们现实世界的一个简单的抽象,比如是一个游戏的场景,比如超级玛丽的每个场景,而超级玛丽对于我们的一个代理(Agent)。我们玩游戏时是通过我们使用键盘或是操纵杆来控制超级玛丽通过各个关口,而使用增强型机器学习,我们是通过相应的算法来控制超级玛丽(Agent),试图完成通关。

Gym 自带了上百个实验环境,可以分成下面几个大类:

  • Algorithms   一些基本算法环境
  • Atari    Atari游戏
  • Box2D   二维Box2D 物理模拟系统
  • Classic Control  几种经典的物理场景
  • MuJoCo  MuJoCo 3D,需要MuJoCo的使用许可
  • Robotics  一些机器人模拟场景
  • Toy Text   简单的文字游戏
  • 3rd Party Enviroments  第三方提供的Gym场景

后面的文章我们将对Algorithms,Box2D,Classic Control 和Toy Text 下的环境逐个分析以帮助理解。

使用这些环境也比较简单,比如我们使用经典的物理场景中的CartPole环境

import gym
env = gym.make('CartPole-v0')
env.reset()
for _ in range(1000):
    env.render()
    env.step(env.action_space.sample()) # take a random action
env.close()

Gym的环境一般带有版本号,比如v0,v1,以便于环境变化时可以使用同一个版本来比较不同算法的性能。

环境是Gym中最重要的一个概念,我们在后面会继续详细介绍。

观测量Observations

为了能够定量的分析环境,我们需要知道环境当前的状态,这在Gym中称为观测量(Observations),不同的环境观测量类型是不同的,比如可以是游戏的场景的截图,也就是我们玩游戏时看到的游戏画面,但是为了计算的效率,可以使用灰度和缩小后人图像。也有可能是一个数值数组,比如Flappy Bird游戏中,每个水管的位置和小鸟的位置。

环境对象一个步进(step)方法,输入一个动作(Action),比如左移,右移,跳跃,可以返回4个值:

  • observation (观测量),输入一个动作后下个场景的状态,它的类型是一个对象,不同环境类型不同。
  • reward (奖励),浮点类型,前个动作所带来的奖励。也和环境有关,我们的目前是获取更多的奖励。
  • done ,游戏结束标志,布尔类型。 比如Flappy Bird游戏小鸟撞死了,我们一般需要检测这个标志以便重新开始下一轮尝试。
  • info 游戏中的一些辅助信息,也和场景有关。一般来说我们不能使用使用这些信息来构造我们的算法。

下图为经典的“代理环境循环”(Agent-Environment loop)

基本步骤是首先使用reset()方法,得到初始的observation。 因此修改前面的代码,检测done标志:

import gym
env = gym.make('CartPole-v0')
for i_episode in range(20):
    observation = env.reset()
    for t in range(100):
        env.render()
        print(observation)
        action = env.action_space.sample()
        observation, reward, done, info = env.step(action)
        if done:
            print("Episode finished after {} timesteps".format(t+1))
            break
env.close()

一个可能的显示结果为:

[-0.061586   -0.75893141  0.05793238  1.15547541]
[-0.07676463 -0.95475889  0.08104189  1.46574644]
[-0.0958598  -1.15077434  0.11035682  1.78260485]
[-0.11887529 -0.95705275  0.14600892  1.5261692 ]
[-0.13801635 -0.7639636   0.1765323   1.28239155]
[-0.15329562 -0.57147373  0.20218013  1.04977545]
Episode finished after 14 timesteps
[-0.02786724  0.00361763 -0.03938967 -0.01611184]
[-0.02779488 -0.19091794 -0.03971191  0.26388759]
[-0.03161324  0.00474768 -0.03443415 -0.04105167]

可以看到这个环境的测量值是一个四元数组。

值域空间Spaces

代码env.action_space.sample() ,我们使用了动作空间的随机取样。一般来说一个游戏中代理可能的操作是有限的,比如超级玛丽可以上下左右,跳动。可以取值的范围是一个离散的区域,比如0-5.每个数字对于一个操作,0代表不做任何操作。

有些游戏可以使用连续的数值,比较飞机模拟器,速度,仰角,方向,翻滚可以是一个连续的区间。这些可能的取值就构成了动作的值域空间(Spaces)。

此外所有可能的观测量(observation)也组成了另外一个值域空间。比如所有的游戏场景(连续的)。象棋中棋子所有可能的分布(离散的)。

离散的和连续的的空间是Gym中最常用的两种值域空间类型。分别是Discrete和Box类型。后面我们也将详细说明值域空间(Spaces)的用法。

开发通用Android 游戏 Open AI Gym 环境

前不久看到scrcpy 它是一个Android设备的桌面工具,可以在桌面系统镜像操作任何Android设备,而且无需root操作,性能也非常好。

scrcpy是C开发,它的基本软件构架如下:

 

想想是不是可以改写下scrcpy,添加一些比较保存按键,鼠标事件,然后回放,就可以实现远程操作,比如玩游戏时,可以保存一些重复的操作,然后回放。再进一步,把游戏改造成OpenAI GYM环境,就可以方便使用各种AI算法来玩游戏。

然后就快速的修改scrcpy来验证这个想法,https://github.com/guidebee/scragt ,可以保存比较保存按键,鼠标事件,然后回放以及截屏。

然后正式启动着个项目,原先scrcpy是C,使用meson编译。程序本身不算复杂。因此重新使用C++,CMake 重写了scrcpy,改成iRobot (Anroid agent)目前基本完成了对scrcpy的改造。

下一步就是实现通用Open AI Gym环境了。