前言: 這兩天在看
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 就要進行交互。這就涉及一個問題,如何設計這個傳輸並整理數據的接口?
看上去好像沒什麼可猶豫的,做幾個函數就完了:
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 還很幼稚,甚至沒法收斂,實在是進展緩慢、一塌糊塗,希望能儘快解決)。