文章目录
前言
重读《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实现 的 深度学习。