1.6 案例:基于Gym库的智能体/环境交互
强化学习算法需要在难易适中的合适环境里才能发挥出其强大的功能。在本节中,我们将安装和应用影响力巨大的强化学习环境库——Gym库。
Gym库(https://gym.openai.com/)是OpenAI推出的强化学习实验环境库。它用Python语言实现了离散时间智能体/环境接口中的环境部分。除了依赖少量商业库外,整个项目是开源免费的。
Gym库内置上百种实验环境,包括以下几类。
·算法环境:包括一些字符串处理等传统计算机算法的实验环境。
·简单文本环境:包括几个用文本表示的简单游戏。
·经典控制环境:包括一些简单几何体的运动,常用于经典强化学习算法的研究。
·Atari游戏环境:包括数十个Atari 2600游戏,具有像素化的图形界面,希望玩家尽可能争夺高分。
·二维方块(Box2D)环境:包括一些连续性控制的任务。
·MuJoCo环境:利用收费的MuJoCo运动引擎进行连续性控制任务。
·机械控制环境:关于机械臂的抓取和控制等。
Gym环境列表可参见网址https://gym.openai.com/envs/。
本节我们将安装并使用Gym库,通过一个完整的实例来演示智能体与环境的交互。
1.6.1 安装Gym库
Gym库在Windows系统、Linu x系统和macOS系统上都可以安装。本节与你一起在Anaconda 3环境里安装Gym库。
请在安装Gym前升级Python和pip。升级pip的命令是:
pip install--upgrade pip
安装Gym可以选择最小安装和完整安装。最小安装的方法是在安装环境(如Anaconda 3的管理员模式)里输入下列命令:
pip install gym
但是,这样安装的Gym库只包括少量的内置环境,如算法环境、简单文字游戏环境和经典控制环境。在此可以先使用这些环境。Gym库完整安装的方法见第10章。
注意:本书后续章节的实战环节将反复使用Gym库,请务必安装。前几章配套的案例只需要用到Gym的最小安装,而最后的综合案例需要依赖最小安装以外的完整安装。本书各章节配套实例需要使用的Gym库范围见表1-1。
表1-1 本书实例的智能体和环境依赖的主要Python扩展库
1.6.2 使用Gym库
本节介绍Gym库的使用。
要使用Gym库,当然首先要导入Gym库。导入Gym库的方法显然是:
import gym
在导入Gym库后,可以通过make()函数来得到环境对象。每一个环境都有一个ID,它是形如“Xxxxx-vd”的Python字符串,如'CartPole-v0'、'Taxi-v2'等。环境名称最后的部分表示版本号,不同版本的环境可能有不同的行为。使用取出环境'CartPole-v0'的代码为:
env = gym.make('CartPole-v0')
想要查看当前Gym库已经注册了哪些环境,可以使用以下代码:
from gym import envs env_specs = envs.registry.all() env_ids = [env_spec.id for env_spec in env_specs] env_ids
每个环境都定义了自己的观测空间和动作空间。环境env的观测空间用env.observation_space表示,动作空间用env.action_space表示。观测空间和动作空间既可以是离散空间(即取值是有限个离散的值),也可以是连续空间(即取值是连续的)。在Gym库中,离散空间一般用gym.spaces.Discrete类表示,连续空间用gym.spaces.Box类表示。例如,环境'MountainCar-v0'的观测空间是Box(2,),表示观测可以用2个float值表示;环境'MountainCar-v0'的动作空间是Dicrete(3),表示动作取值自{0,1,2}。对于离散空间,gym.spaces.Discrete类实例的成员n表示有几个可能的取值;对于连续空间,Box类实例的成员low和high表示每个浮点数的取值范围。
接下来使用环境对象env。首先,初始化环境对象env,代码为:
env.reset()
该调用能返回智能体的初始观测,是np.array对象。
环境初始化后就可以使用了。使用环境的核心是使用环境对象的step()方法。step()方法接收智能体的动作作为参数,并返回以下4个参数。
·观测(observation):np.array对象,表示观测,和env.reset()返回值的意义相同。
·奖励(reward):float类型的值。
·本回合结束指示(done):bool类型的数值。Gym库里的实验环境大多都是回合制的。这个返回值可以指示在当前动作后游戏是否结束。如果游戏结束了,可以通过“env.reset()”开始下一回合。
·其他信息(info):dict类型的值,含有一些调试信息。不一定要使用这个参数。
env.step()的参数需要取自动作空间。可以使用以下语句从动作空间中随机选取一个动作:
action=env.action_space.sample()
每次调用env.step()只会让环境前进一步。所以,env.step()往往放在循环结构里,通过循环调用来完成整个回合。
在env.reset()或env.step()后,可以用以下语句以图形化的方法显示当前环境。
env.render()
使用完环境后,可以使用下列语句关闭环境:
env.close()
注意:如果绘制了实验的图形界面窗口,那么关闭该窗口的最佳方式是调用env.close()。试图直接关闭图形界面窗口可能会导致内存不能释放,甚至会导致死机。
测试智能体在Gym库中某个任务的性能时,学术界一般最关心100个回合的平均回合奖励。至于为什么是100个回合而不是其他回合数(比如128个回合),完全是习惯使然,没有什么特别的原因。对于有些环境,还会指定一个参考的回合奖励值,当连续100个回合的奖励大于指定的值时,就认为这个任务被解决了。但是,并不是所有的任务都指定了这样的值。对于没有指定值的任务,就无所谓任务被解决了或者没有被解决。
1.6.3 小车上山
本节通过一个完整的例子来学习如何与Gym库中的环境交互。本节选用的例子是经典的控制任务:小车上山(MountainCar-v0)。本节主要关心交互的Python代码,而不详细介绍这个控制任务及其求解。任务的具体描述和求解方式会在第6章中介绍。
首先我们来看看这个任务的观测空间和动作空间,可以通过执行代码清单1-1实现。
代码清单1-1 导入环境并查看观测空间和动作空间
import gym env = gym.make('MountainCar-v0') print('观测空间 = {}'.format(env.observation_space)) print('动作空间 = {}'.format(env.action_space)) print('观测范围 = {} ~ {}'.format(env.observation_space.low, env.observation_space.high)) print('动作数 = {}'.format(env.action_space.n))
这段代码的运行结果为:
观测空间 = Box(2,) 动作空间 = Discrete(3) 观测范围 = [-1.2 -0.07] ~ [0.6 0.07] 动作数 = 3
运行结果告诉我们,观测空间是形状为(2,)的浮点型np.array,而动作空间是取{0,1,2}的int型数值。
接下来考虑智能体。智能体往往是我们自己实现的。代码清单1-2给出了一个智能体类——BespokeAgent类。智能体的decide()方法实现了决策功能,而learn()方法实现了学习功能。代码清单1-2给出的BespokeAgent类是一个比较简单的类,它只能根据给定的数学表达式进行决策,并且不能有效学习。所以它并不是一个真正意义上的强化学习智能体类。但是,用于演示智能体和环境的交互已经足够了。
代码清单1-2 根据指定确定性策略决定动作的智能体
class BespokeAgent: def __init__(self, env): pass def decide(self, observation): # 决策 position, velocity = observation lb = min(-0.09 * (position + 0.25) ** 2 + 0.03, 0.3 * (position + 0.9) ** 4 - 0.008) ub = -0.07 * (position + 0.38) ** 2 + 0.06 if lb < velocity < ub: action = 2 else: action = 0 return action # 返回动作 def learn(self, *args): # 学习 pass agent = BespokeAgent(env)
接下来我们试图让智能体与环境交互。代码清单1-3中的play_once()函数可以让智能体和环境交互一个回合。这个函数有4个参数。
·参数env是环境类。
·参数agent是智能体类。
·参数render是bool类型变量,指示在运行过程中是否要图形化显示。如果函数参数render为True,那么在交互过程中会调用env.render()以显示图形化界面,而这个界面可以通过调用env.close()关闭。
·参数train是bool类型的变量,指示在运行过程中是否训练智能体。在训练过程中应当设置为True,以调用agent.learn()函数;在测试过程中应当设置为False,使得智能体不变。
这个函数有一个返回值episode_reward,是float类型的数值,表示智能体与环境交互一个回合的回合总奖励。
代码清单1-3 智能体和环境交互一个回合的代码
def play_montecarlo(env, agent, render=False, train=False): episode_reward = 0. # 记录回合总奖励,初始化为0 observation = env.reset() # 重置游戏环境,开始新回合 while True: # 不断循环,直到回合结束 if render: # 判断是否显示 env.render() # 显示图形界面,图形界面可以用 env.close() 语句关闭 action = agent.decide(observation) next_observation, reward, done, _ = env.step(action) # 执行动作 episode_reward += reward # 收集回合奖励 if train: # 判断是否训练智能体 agent.learn(observation, action, reward, done) # 学习 if done: # 回合结束,跳出循环 break observation = next_observation return episode_reward # 返回回合总奖励
借助于代码清单1-1给出的环境、代码清单1-2给出的智能体和代码清单1-3给出的交互函数,我们可以用下列代码让智能体和环境交互一个回合,并在交互过程中图形化显示。交互完毕后,可用env.close()语句关闭图形化界面。
env.seed(0) # 设置随机数种子,只是为了让结果可以精确复现,一般情况下可删去 episode_reward = play_montecarlo(env, agent, render=True) print('回合奖励 = {}'.format(episode_reward)) env.close() # 此语句可关闭图形界面
为了系统评估智能体的性能,下列代码求出了连续交互100回合的平均回合奖励。小车上山环境有一个参考的回合奖励值-110,如果当连续100个回合的平均回合奖励大于-110,则认为这个任务被解决了。BespokeAgent类对应的策略的平均回合奖励大概就在-110左右。
代码清单1-4 运行100回合求平均以测试性能
episode_rewards = [play_montecarlo(env, agent) for _ in range(100)] print('平均回合奖励 = {}'.format(np.mean(episode_rewards)))