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 Configurationfrom tensorforce.agents import DQNAgentfrom tensorforce.core.networks import layered_network_builder# Define a network builder from an ordered list of layerslayers = [dict(type='dense', size=32), dict(type='dense', size=32)]network = layered_network_builder(layers_config=layers)# Define a statestates = 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 objectagent_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_jsonagent_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 内的对应于每个时间步的输入/输出。下图展示了这种网络随时间的工作方式:
这些内部状态的管理(即在时间步之间前向传播它们和在开始新 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 = 0episode_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 上很长时间没更新了,那很可能是因为我们还在努力地做内部开发(或者是我们的在读博士太忙了),反正不是因为我们放弃了这个项目。如果你有兴趣开发有趣的应用用例,请与我们联系。