基於TensorFlow打造強化學習API:TensorForce是怎樣煉成的?

機器之心發表於2017-07-14

TensorForce 是一個構建於 TensorFlow 之上的新型強化學習 API。強化學習元件開發者 reinforce.io 近日發表了一篇部落格文章介紹了 TensorForce 背後的架構和思想。

專案地址:https://github.com/reinforceio/tensorforce

本文將圍繞一個實際的問題進行介紹:應用強化學習的社群可以如何從對指令碼和單個案例的收集更進一步,實現一個強化學習 API——一個用於強化學習的 tf-learn 或 skikit-learn?在討論 TensorForce 框架之前,我們將談一談啟發了這個專案的觀察和思想。如果你只想瞭解這個 API,你可以跳過這一部分。我們要強調一下:這篇文章並不包含對深度強化學習本身的介紹,也沒有提出什麼新模型或談論最新的最佳演算法,因此對於純研究者來說,這篇文章可能並不會那麼有趣。

開發動機

假設你是計算機系統、自然語言處理或其它應用領域的研究者,你一定對強化學習有一些基本的瞭解,並且有興趣將深度強化學習(deep RL)用來控制你的系統的某些方面。

對深度強化學習、DQN、vanilla 策略梯度、A3C 等介紹文章已經有很多了,比如 Karpathy 的文章(http://karpathy.github.io/2016/05/31/rl/)對策略梯度方法背後的直觀思想就進行了很好的描述。另外,你也能找到很多可以幫助上手的程式碼,比如 OpenAI 上手智慧體(https://github.com/openai/baselines)、rllab(https://github.com/openai/rllab)以及 GitHub 上許多特定的演算法。

但是,我們發現在強化學習的研究框架開發和實際應用之間還存在一個巨大的鴻溝。在實際應用時,我們可能會面臨如下的問題:

  • 強化學習邏輯與模擬控制程式碼的緊密耦合:模擬環境 API 是非常方便的,比如,它們讓我們可以建立一個環境物件然後將其用於一個 for 迴圈中,同時還能管理其內部的更新邏輯(比如:透過收集輸出特徵)。如果我們的目標是評估一個強化學習思想,那麼這就是合理的,但將強化學習程式碼和模擬環境分開則要艱難得多。它還涉及到流程控制的問題:當環境就緒後,強化學習程式碼可以呼叫它嗎?或者當環境需要決策時,它會呼叫強化學習智慧體嗎?對於在許多領域中實現的應用強化學習庫,我們往往需要後者。
  • 固定的網路架構:大多數實現案例都包含了硬編碼的神經網路架構。這通常並不是一個大問題,因為我們可以很直接地按照需求加入或移除不同的網路層。儘管如此,如果有一個強化學習庫能夠提供宣告式介面的功能,而無需修改庫程式碼,那麼情況就會好得多。此外,在有的案例中,修改架構(出人意外地)要難得多,比如當需要管理內部狀態的時候(見下文)。
  • 不相容狀態/動作介面:很多早期的開原始碼都使用了流行的 OpenAI Gym 環境,具有平坦的狀態輸入的簡單介面和單個離散或連續動作輸出。但 DeepMind Lab 則使用了一種詞典格式,一般具有多個狀態和動作。而 OpenAI Universe 則使用的是命名關鍵事件(named key events)。理想情況下,我們想讓強化學習智慧體能處理任意數量的狀態和動作,並且具有潛在的不同型別和形狀。比如說,TensorForce 的一位作者正在 NLP 中使用強化學習並且想要處理多模態輸入,其中一個狀態在概念上包含兩個輸入——一張影像和一個對應的描述。
  • 不透明的執行設定和效能問題:寫 TensorFlow 程式碼的時候,我們很自然地會優先關注邏輯。這會帶來大量重複/不必要的運算或實現不必要的中間值。此外,分散式/非同步/並行強化學習的目標也有點不固定,而分散式 TensorFlow 需要對特定的硬體設定進行一定程度的人工調節。同樣,如果最終有一種執行配置只需要宣告可用裝置或機器,然後就能在內部處理好其它一切就好了,比如兩臺有不同 IP 的機器可以執行非同步 VPG。

明確一下,這些問題並不是要批評研究者寫的程式碼,因為這些程式碼本來就沒打算被用作 API 或用於其它應用。在這裡我們介紹的是想要將強化學習應用到不同領域中的研究者的觀點。

TensorForce API

TensorForce 提供了一種宣告式介面,它是可以使用深度強化學習演算法的穩健實現。在想要使用深度強化學習的應用中,它可以作為一個庫使用,讓使用者無需擔心所有底層的設計就能實驗不同的配置和網路架構。我們完全瞭解當前的深度強化學習方法往往比較脆弱,而且需要大量的微調,但這並不意味著我們還不能為強化學習解決方案構建通用的軟體基礎設施。

TensorForce 並不是原始實現結果的集合,因為這不是研究模擬,要將原始實現用在實際環境的應用中還需要大量的工作。任何這樣的框架都將不可避免地包含一些結構決策,這會使得非標準的事情變得更加惱人(抽象洩漏(leaky abstractions))。這就是為什麼核心強化學習研究者可能更傾向於從頭打造他們的模型的原因。使用 TensorForce,我們的目標是獲取當前最佳研究的整體方向,包含其中的新興見解和標準。

接下來,我們將深入到 TensorForce API 的各個基本方面,並討論我們的設計選擇。

建立和配置智慧體

我們首先開始用 TensorForce API 建立一個強化學習智慧體:

from tensorforce import Configuration
from tensorforce.agents import DQNAgent
from tensorforce.core.networks import layered_network_builder

# Define a network builder from an ordered list of layers
layers = [dict(type='dense', size=32),
          dict(type='dense', size=32)]
network = layered_network_builder(layers_config=layers)

# Define a state
states = dict(shape=(10,), type='float')

# Define an action (models internally assert whether
# they support continuous and/or discrete control)
actions = dict(continuous=False, num_actions=5)

# The agent is configured with a single configuration object
agent_config = Configuration(
    batch_size=8,
    learning_rate=0.001,
    memory_capacity=800,
    first_update=80,
    repeat_update=4,
    target_update_frequency=20,
    states=states,
    actions=actions,
    network=network
)
agent = DQNAgent(config=agent_config)

這個示例中的狀態和動作是更一般的狀態/動作的短形式(short-form)。比如由一張影像和一個描述構成多模態輸入按如下方式定義。類似地,也可以定義多輸出動作。注意在整個程式碼中,單個狀態/動作的短形式必須被持續不斷地用於與智慧體的通訊。

states = dict(
    image=dict(shape=(64, 64, 3), type='float'),
    caption=dict(shape=(20,), type='int')
)

配置引數依賴於所用的基本智慧體和模型。每個智慧體的完整引數列表可見於這個示例配置:https://github.com/reinforceio/tensorforce/tree/master/examples/configs

TensorForce 目前提供了以下強化學習演算法:

  • 隨機智慧體基線(RandomAgent)
  • 帶有 generalized advantage estimation 的 vanilla 策略梯度(VPGAgent)
  • 信任區域策略最佳化(TRPOAgent)
  • 深度 Q 學習/雙深度 Q 學習(DQNAgent)
  • 規範化的優勢函式(NAFAgent)
  • 對專家演示的深度 Q 學習(DQFDAgent)
  • Asynchronous Advantage Actor-Critic(A3C)(可以隱含地透過 distributed 使用)

最後一項的意思是說並沒有 A3CAgent 這樣的東西,因為 A3C 實際上描述的是一種非同步更新的機制,而不是一種特定的智慧體。因此,使用分散式 TensorFlow 的非同步更新機制是通用 Model 基類的一部分,所有智慧體都衍生於此。正如論文《Asynchronous Methods for Deep Reinforcement Learning》中描述的那樣,A3C 是透過為 VPGAgent 設定 distributed flag 而隱含地實現的。應該指出,A3C 並不是對每種模型而言都是最優的分散式更新策略(對一些模型甚至完全沒意義),我們將在本文結尾處討論實現其它方法(比如 PAAC)。重要的一點是要在概念上將智慧體和更新語義的問題與執行語義區分開。

我們還想談談模型(model)和智慧體(agent)之間的區別。Agent 類定義了將強化學習作為 API 使用的介面,可以管理傳入觀察資料、預處理、探索等各種工作。其中兩個關鍵方法是 agent.act(state) 和 agent.observe(reward, terminal)。agent.act(state) 返回一個動作,而 agent.observe(reward, terminal) 會根據智慧體的機制更新模型,比如離策略記憶回放(MemoryAgent)或在策略批處理(BatchAgent)。注意,要讓智慧體的內在機制正確工作,必須交替呼叫這些函式。Model 類實現了核心強化學習演算法,並透過 get_action 和 update 方法提供了必要的介面,智慧體可以在相關點處內在地呼叫。比如說,DQNAgent 是一個帶有 DQNModel 和額外一行(用於目標網路更新)的 MemoryAgent 智慧體。

def observe(self, reward, terminal):
    super(DQNAgent, self).observe(reward, terminal)
    if self.timestep >= self.first_update \
            and self.timestep % self.target_update_frequency == 0:
        self.model.update_target()


神經網路配置

強化學習的一個關鍵問題是設計有效的價值函式。在概念上講,我們將模型看作是對更新機制的描述,這有別於實際更新的東西——在深度強化學習的例子中是指一個(或多個)神經網路。因此,模型中並沒有硬編碼的網路,而是根據配置不同的例項化。

在上面的例子中,我們透過程式設計創造了一個網路配置作為描述每一層的詞典列表。這樣的配置也可以透過 JSON 給出,然後使用一個效用函式將其變成一個網路構建器(network constructor)。這裡給出了一個 JSON 網路規範的例子:

[
    {
        "type": "conv2d",
        "size": 32,
        "window": 8,
        "stride": 4
    },
    {
        "type": "conv2d",
        "size": 64,
        "window": 4,
        "stride": 2
    },
    {
        "type": "flatten"
    },
    {
        "type": "dense",
        "size": 512
    }
]

和之前一樣,這個配置必須被新增到該智慧體的配置(configuration)物件中:

from tensorforce.core.networks import from_json

agent_config = Configuration(
    ...
    network=from_json('configs/network_config.json')
    ...
)

預設的啟用層是 relu,但也還有其它啟用函式可用(目前有 elu、selu、softmax、tanh 和 sigmoid)。此外也可以修改層的其它性質,比如,可以將一個稠密層(dense layer)改成這樣:

[
    {
        "type": "dense",
        "size": 64,
        "bias": false,
        "activation": "selu",
        "l2_regularization": 0.001
    }
]

我們選擇不使用已有的層實現(比如來自 tf.layers),從而能對內部運算施加明確的控制,並確保它們能與 TensorForce 的其餘部分正確地整合在一起。我們想要避免對動態 wrapper 庫的依賴,因此僅依賴於更低層的 TensorFlow 運算。

我們的 layer 庫目前僅提供了非常少的基本層型別,但未來還會擴充套件。另外你也可以輕鬆整合你自己的層,下面給出了一個批規範化層的例子:

def batch_normalization(x, variance_epsilon=1e-6):
    mean, variance = tf.nn.moments(x, axes=tuple(range(x.shape.ndims - 1)))
    x = tf.nn.batch_normalization(x, mean=mean, variance=variance,
                                  variance_epsilon=variance_epsilon)
    return x
{
    "type": "[YOUR_MODULE].batch_normalization",
    "variance_epsilon": 1e-9
}

到目前為止,我們已經給出了 TensorForce 建立分層網路的功能,即一個採用單一輸入狀態張量的網路,具有一個層的序列,可以得出一個輸出張量。但是在某些案例中,可能需要或更適合偏離這樣的層堆疊結構。最顯著的情況是當要處理多個輸入狀態時,這是必需的,使用單個處理層序列無法自然地完成這一任務。

我們目前還沒有為自動建立對應的網路構建器提供更高層的配置介面。因此,對於這樣的案例,你必須透過程式設計來定義其網路構建器函式,並像之前一樣將其加入到智慧體配置中。比如之前的多模態輸入(image 和 caption)例子,我們可以按以下方式定義一個網路:

def network_builder(inputs):
    image = inputs['image']  # 64x64x3-dim, float
    caption = inputs['caption']  # 20-dim, int

    with tf.variable_scope('cnn'):
        weights = tf.Variable(tf.random_normal(shape=(3, 3, 3, 16), stddev=0.01))
        image = tf.nn.conv2d(image, filter=weights, strides=(1, 1, 1, 1))
        image = tf.nn.relu(image)
        image = tf.nn.max_pool(image, ksize=(1, 2, 2, 1), strides=(1, 2, 2, 1))

        weights = tf.Variable(tf.random_normal(shape=(3, 3, 16, 32), stddev=0.01))
        image = tf.nn.conv2d(image, filter=weights, strides=(1, 1, 1, 1))
        image = tf.nn.relu(image)
        image = tf.nn.max_pool(image, ksize=(1, 2, 2, 1), strides=(1, 2, 2, 1))

        image = tf.reshape(image, shape=(-1, 16 * 16, 32))
        image = tf.reduce_mean(image, axis=1)

    with tf.variable_scope('lstm'):
        weights = tf.Variable(tf.random_normal(shape=(30, 32), stddev=0.01))
        caption = tf.nn.embedding_lookup(params=weights, ids=caption)
        lstm = tf.contrib.rnn.LSTMCell(num_units=64)
        caption, _ = tf.nn.dynamic_rnn(cell=lstm, inputs=caption, dtype=tf.float32)
        caption = tf.reduce_mean(caption, axis=1)

    return tf.multiply(image, caption)


agent_config = Configuration(
    ...
    network=network_builder
    ...
)

內部狀態和 Episode 管理

和經典的監督學習設定(其中的例項和神經網路呼叫被認為是獨立的)不同,強化學習一個 episode 中的時間步取決於之前的動作,並且還會影響後續的狀態。因此除了其每個時間步的狀態輸入和動作輸出,可以想象神經網路可能有內部狀態在 episode 內的對應於每個時間步的輸入/輸出。下圖展示了這種網路隨時間的工作方式:

基於TensorFlow打造強化學習API:TensorForce是怎樣煉成的?

這些內部狀態的管理(即在時間步之間前向傳播它們和在開始新 episode 時重置它們)可以完全由 TensorForce 的 agent 和 model 類處理。注意這可以處理所有的相關用例(在 batch 之內一個 episode,在 batch 之內多個 episode,在 batch 之內沒有終端的 episode)。到目前為止,LSTM 層型別利用了這個功能:

[
    {
        "type": "dense",
        "size": 32
    },
    {
        "type": "lstm"
    }
]

在這個示例架構中,稠密層的輸出被送入一個 LSTM cell,然後其得出該時間步的最終輸出。當向前推進該 LSTM 一步時,其內部狀態會獲得更新並給出此處的內部狀態輸出。對於下一個時間步,網路會獲得新狀態輸入及這個內部狀態,然後將該 LSTM 又推進一步並輸出實際輸出和新的內部 LSTM 狀態,如此繼續……

對於帶有內部狀態的層的自定義實現,該函式不僅必須要返回該層的輸出,而且還要返回一個內部狀態輸入佔位符的列表、對應的內部狀態輸出張量和一個內部狀態初始化張量列表(這些都長度相同,並且按這個順序)。以下程式碼片段給出了我們的 LSTM 層實現(一個簡化版本),並說明了帶有內部狀態的自定義層的定義方式:

def lstm(x):
    size = x.get_shape()[1].value
    internal_input = tf.placeholder(dtype=tf.float32, shape=(None, 2, size))
    lstm = tf.contrib.rnn.LSTMCell(num_units=size)
    state = tf.contrib.rnn.LSTMStateTuple(internal_input[:, 0, :],
                                          internal_input[:, 1, :])
    x, state = lstm(inputs=x, state=state)
    internal_output = tf.stack(values=(state.c, state.h), axis=1)
    internal_init = np.zeros(shape=(2, size))
    return x, [internal_input], [internal_output], [internal_init]

預處理狀態

我們可以定義被應用於這些狀態(如果指定為列表的詞典,則可能是多個狀態)的預處理步驟,比如,為了對視覺輸入進行下采樣。下面的例子來自 Arcade Learning Environment 前處理器,大多數 DQN 實現都這麼用:

config = Configuration(
    ...
    preprocessing=[
        dict(
            type='image_resize',
            kwargs=dict(width=84, height=84)
        ),
        dict(
            type='grayscale'
        ),
        dict(
            type='center'
        ),
        dict(
            type='sequence',
            kwargs=dict(
                length=4
            )
        )
    ]
    ...
)

這個 stack 中的每一個前處理器都有一個型別,以及可選的 args 列表和/或 kwargs 詞典。比如 sequence 前處理器會取最近的四個狀態(即:幀)然後將它們堆疊起來以模擬馬爾可夫屬性。隨便一提:在使用比如之前提及的 LSTM 層時,這顯然不是必需的,因為 LSTM 層可以透過內部狀態建模和交流時間依賴。

探索

探索可以在 configuration 物件中定義,其可被智慧體應用到其模型決定所在的動作上(以處理多個動作,同樣,會給出一個規範詞典)。比如,為了使用 Ornstein-Uhlenbeck 探索以得到連續的動作輸出,下面的規範會被新增到配置中。

config = Configuration(
    ...
    exploration=dict(
        type='OrnsteinUhlenbeckProcess',
        kwargs=dict(
            sigma=0.1,
            mu=0,
            theta=0.1
        )
    )
    ...
)

以下幾行程式碼新增了一個用於離散動作的 epsilon 探索,它隨時間衰減到最終值:

config = Configuration(
    ...
    exploration=dict(
        type='EpsilonDecay',
        kwargs=dict(
            epsilon=1,
            epsilon_final=0.01,
            epsilon_timesteps=1e6
        )
    )
    ...
)

用 Runner 效用函式使用智慧體

讓我們使用一個智慧體,這個程式碼是在我們測試環境上執行的一個智慧體:https://github.com/reinforceio/tensorforce/blob/master/tensorforce/environments/minimal_test.py,我們將其用於連續積分——一個為給定智慧體/模型的工作方式驗證行動、觀察和更新機制的最小環境。注意我們所有的環境實現(OpenAI Gym、OpenAI Universe、DeepMind Lab)都使用了同一個介面,因此可以很直接地使用另一個環境執行測試。

Runner 效用函式可以促進一個智慧體在一個環境上的執行過程。給定任意一個智慧體和環境例項,它可以管理 episode 的數量,每個 episode 的最大長度、終止條件等。Runner 也可以接受 cluster_spec 引數,如果有這個引數,它可以管理分散式執行(TensorFlow supervisors/sessions/等等)。透過可選的 episode_finished 引數,你還可以週期性地報告結果,還能給出在最大 episode 數之前停止執行的指標。

environment = MinimalTest(continuous=False)

network_config = [
    dict(type='dense', size=32)
]
agent_config = Configuration(
    batch_size=8,
    learning_rate=0.001,
    memory_capacity=800,
    first_update=80,
    repeat_update=4,
    target_update_frequency=20,
    states=environment.states,
    actions=environment.actions,
    network=layered_network_builder(network_config)
)

agent = DQNAgent(config=agent_config)
runner = Runner(agent=agent, environment=environment)

def episode_finished(runner):
    if runner.episode % 100 == 0:
        print(sum(runner.episode_rewards[-100:]) / 100)
    return runner.episode < 100 \
        or not all(reward >= 1.0 for reward in runner.episode_rewards[-100:])

runner.run(episodes=1000, episode_finished=episode_finished)

為了完整,我們明確給出了在一個環境上執行一個智慧體的最小迴圈:

episode = 0
episode_rewards = list()

while True:
    state = environment.reset()
    agent.reset()

    timestep = 0
    episode_reward = 0
    while True:
        action = agent.act(state=state)
        state, reward, terminal = environment.execute(action=action)
        agent.observe(reward=reward, terminal=terminal)

        timestep += 1
        episode_reward += reward

        if terminal or timestep == max_timesteps:
            break

    episode += 1
    episode_rewards.append(episode_reward)

    if all(reward >= 1.0 for reward in episode_rewards[-100:]) \
            or episode == max_episodes:
        break

正如在引言中說的一樣,在一個給定應用場景中使用 runner 類取決於流程控制。如果使用強化學習可以讓我們合理地在 TensorForce 中查詢狀態資訊(比如透過一個佇列或網路服務)並返回動作(到另一個佇列或服務),那麼它可被用於實現環境介面,並因此可以使用(或擴充套件)runner 效用函式。

更常見的情況可能是將 TensorForce 用作驅動控制的外部應用庫,因此無法提供一個環境控制程式碼。對研究者來說,這可能無足輕重,但在計算機系統等領域,這是一個典型的部署問題,這也是大多數研究指令碼只能用於模擬,而無法實際應用的根本原因。

另外值得提及的一點是宣告式的中心配置物件使得我們可以直接用超引數最佳化為強化學習模型的所有元件配置介面,尤其還有網路架構。

進一步思考

我們希望你能發現 TensorForce 很有用。到目前為止,我們的重點還是讓架構先就位,我們認為這能讓我們更持續一致地實現不同的強化學習概念和新的方法,並且避免探索新領域中的深度強化學習用例的不便。

在這樣一個快速發展的領域,要決定在實際的庫中包含哪些功能是很困難的。現在的演算法和概念是非常多的,而且看起來在 Arcade Learning Environment (ALE) 環境的一個子集上,每週都有新想法得到更好的結果。但也有一個問題存在:許多想法都只在易於並行化或有特定 episode 結構的環境中才有效——對於環境屬性以及它們與不同方法的關係,我們還沒有一個準確的概念。但是,我們能看到一些明顯的趨勢:

  • 策略梯度和 Q 學習方法混合以提升樣本效率(PGQ、Q-Prop 等):這是一種合乎邏輯的事情,儘管我們還不清楚哪種混合策略將佔上風,但是我們認為這將成為下一個「標準方法」。我們非常有興趣理解這些方法在不同應用領域(資料豐富/資料稀疏)的實用性。我們一個非常主觀的看法是大多數應用研究者都傾向於使用 vanilla 策略梯度的變體,因為它們易於理解、實現,而且更重要的是比新演算法更穩健,而新演算法可能需要大量的微調才能處理潛在的數值不穩定性(numerical instabilities)。一種不同的看法是非強化學習研究者可能只是不知道相關的新方法,或者不願意費力去實現它們。而這就激勵了 TensorForce 的開發。最後,值得考慮的是,應用領域的更新機制往往沒有建模狀態、動作和回報以及網路架構重要。
  • 更好地利用 GPU 和其他可用於並行/一步/分散式方法的裝置(PAAC、GA3C 等):這一領域的方法的一個問題是關於收集資料與更新所用時間的隱含假設。在非模擬的領域,這些假設可能並不成立,而理解環境屬性會如何影響裝置執行語義還需要更多的研究。我們仍然在使用 feed_dicts,但也在考慮提升輸入處理的效能。
  • 探索模式(比如,基於計數的探索、引數空間噪聲……)
  • 大型離散動作空間、分層模型和子目標(subgoal)的分解。比如 Dulac-Arnold 等人的論文《Deep Reinforcement Learning in Large Discrete Action Spaces》。複雜離散空間(比如許多依賴於狀態的子選項)在應用領域是高度相關的,但目前還難以透過 API 使用。我們預計未來幾年會有大量成果。
  • 用於狀態預測的內部模組和基於全新模型的方法:比如論文《The Predictron: End-To-End Learning and Planning》。
  • 貝葉斯深度強化學習和關於不確定性的推理

總的來說,我們正在跟蹤這些發展,並且將會將此前錯過的已有技術(應該有很多)納入進來;而一旦我們相信一種新想法有變成穩健的標準方法的潛力,我們也會將其納入進來。在這個意義上,我們並沒有與研究框架構成明確的競爭,而是更高程度的覆蓋。

最後說明:我們有一個內部版本來實現這些想法,看我們可以如何將最新的先進方法變成有用的庫函式。一旦我們對一個專案滿意,我們就會考慮將其開源。所以如果 GitHub 上很長時間沒更新了,那很可能是因為我們還在努力地做內部開發(或者是我們的在讀博士太忙了),反正不是因為我們放棄了這個專案。如果你有興趣開發有趣的應用用例,請與我們聯絡。

相關文章