前言: 这两天在看
openAI的gym,并尝试用其测试自己写的Sarsa。一塌糊涂,这里来记录下经验教训。官网对于gym的文档不多,也不详细,读了gym的源码,很直观,看注释也可以。强化学习与传统的“监督学习”、“非监督学习”不同,强化学习要时刻与环境/模型交互,以传输数据。这就不能简单地将数据输入,而要整理算法与数据的接口,将二者连接起来。
注:这里的接口是抽象的,用途是实现两个基类 (Class) 或函数在迭代的同时交互数据,并非 java 中所指的 interface 。额外推荐做 java / .net 开发的朋友移步 我没有三颗心脏:谈一谈依赖倒置原则 拓展兴趣。
强化学习中智能体与算法(Agent)的交互

图片来自 https://gym.openai.com/docs/
上面这张图片描述了强化学习算法的训练过程:Agent 做出决策 / 动作 action,Environment 根据这个 action 做出反应,变化状态,Agent 则以 observation 与 reward 的形式接受这些信息,并做出下一个决策。如此往复。
这是一个动态的过程,每一次迭代中,Agent 与 Environment 就要进行交互。这就涉及一个问题,如何设计这个传输并整理数据的接口?
看上去好像没什么可犹豫的,做几个函数就完了:
代码语言:javascript复制class Agent:
...
def update_value_function(self, observation, reward):
...
def action(self, observation):
...
return action
class Environment:
...
def step(self, action):
...
return observation, reward不幸的是实事并非如此,单单 Agent 的训练与决策过程就不止一次涉及到与 Environment 的耦合:
•如贪心动作选择下,Agent 需要通过 Environment 来知晓该状态下所有可用的动作都有哪些;•初始状态是什么?•停止条件?•...
设计接口
不同的开发者有不同的习惯,也许有人会把停止条件与动作选择写进一个函数,有人会把其分开写。就像同样是电源插头,功能都是传输电能,但其形状、适用电压就是不同。

欧洲
这是德法常用的插头,230V。

中国
这是我国大陆的三角插头,220V。
但是我们出国时,不必为不能充电而感到担心,因为我们有“转换插头”这个神器:

同样的功能,不同形状的设备,我们引入“转换插头”这个东西,来使交互成为可能。
在程序设计时,两个类道理相通,但开发时做出的接口不同,就需要用到“转换插头”,对某个类的的输出和输入“包装”一下。
比如我在知道 gym 之前,做了个 Agent 算法接口:
def sarsa(value_function, start_state, end_state, action_available, step):
# @value_function:
# a warpper class for ValueFunion
# @start_state: return state
# start_state() -> tuple
# @end_state: return if_done
# end_state() -> bool
# @action_available: return actions
# action_available(state) -> list
# @step: return next_state, reward
# step(action) -> tuple, float or int然而,gym 对外提供的接口长成这个样子:
class Env(object):
r"""The main OpenAI Gym class. It encapsulates an environment with
arbitrary behind-the-scenes dynamics. An environment can be
partially or fully observed...
"""
def step(self, action):
...
return observation, reward, done, info
def reset(self):
...
return observation
def render(self, mode='human'):
# for plot
...
def close(self):
# for close
...
def seed(self, seed=None):
# for seeding
...关于基类完整的代码可见 https://github.com/openai/gym/blob/master/gym/core.py。
所以你看,我的 Agent 是中国三头的插头,而 gym 提供的测试环境是欧陆的二孔式插口。
三头的插不进二孔的,必须要自己造个“转换插头”了。
于是我写了一个类,相当于给 gym 套了个“转换插头”,把二孔转换为三孔:
class DiscreteState:
def __init__(self, env, block_num=10):
self.discrete_state = []
self.block_num = block_num
self.env = env
self.action = None
self.trace = []
...
def state(self, observation):
rts = []
...
return tuple(rts)
def _return_index(self, ind, sta):
...
def start_state(self):
self.trace = []
observation = self.env.reset()
self.trace.append(observation)
return self.state(observation)
def action_available(self, state):
return [0, 1, 2] # up to env
def step(self, action):
observation, reward, done, info = self.env.step(action)
self.action = action
self.trace.append(observation)
return self.state(observation), reward
def end_state(self):
observation, reward, done, info = self.env.step(self.action)
return done自己写“转换插头”,不如一开始就贴近规范
但是你看,我写的 DiscreteState 并不通用,当 env 变化后,我还需要修改 DiscreteState 其中的代码,及其麻烦。
那么,为什么不一开始就按照 gym 的规范,做一个可以直接把 gym 拿来用的 Agent 呢?
于是我觉得修改之前的代码,并且以后也按照 gym 的接口来标准化我以后的 Agent 接口。
其实对于我这种不太娴熟的开发者,修改原来的代码其实是很不忍心的,但长痛不如短痛,开始干吧。
以后记得:接触一个新领域时,先进行检索、总结,接触并了解该领域的标准化规范,再动手写代码。大大节省时间、提升效率。
后记: 本来决定今天写完代码的。但白天没奈住寂寞,看了两个电影 Frozen 和 Titanic 。Frozen 没有期望中的惊艳,重温 Titanic 注意到不少细节。现在都十一点半了,今天就先结束工作吧!尽量不要熬夜。明天争取早起把代码修改好,然后找找哪里的问题导致不收敛(现在写的 Agent 还很幼稚,甚至没法收敛,实在是进展缓慢、一塌糊涂,希望能尽快解决)。


