【开发随笔】以强化学习环境 gym 库为例:为什么日常中我应该试图标准化接口?

前言: 这两天在看 openAIgym ,并尝试用其测试自己写的 Sarsa 。一塌糊涂,这里来记录下经验教训。官网对于 gym 的文档不多,也不详细,读了 gym 的源码,很直观,也确实用不着什么官方文档。强化学习与传统的“监督学习”、“非监督学习”不同,强化学习要时刻与环境/模型交互,以传输数据。这就不能简单地将数据输入,而要整理算法与数据的接口,将二者连接起来。

注:这里的接口是抽象的,用途是实现两个基类 (Class) 或函数在迭代的同时交互数据,并非 java 中所指的 interface 。额外推荐做 java / .net 开发的朋友移步 我没有三颗心脏:谈一谈依赖倒置原则 拓展兴趣。

强化学习中智能体与算法(Agent)的交互


图片来自 https://gym.openai.com/docs/

上面这张图片描述了强化学习算法的训练过程:Agent 做出决策 / 动作 actionEnvironment 根据这个 action 做出反应,变化状态,Agent 则以 observationreward 的形式接受这些信息,并做出下一个决策。如此往复。

这是一个动态的过程,每一次迭代中,Agent 与 Environment 就要进行交互。这就涉及一个问题,如何设计这个传输并整理数据的接口?

看上去好像没什么可犹豫的,做几个函数就完了:

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 还很幼稚,甚至没法收敛,实在是进展缓慢、一塌糊涂,希望能尽快解决)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章