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