【開發隨筆】以強化學習環境 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 還很幼稚,甚至沒法收斂,實在是進展緩慢、一塌糊塗,希望能儘快解決)。

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