强化学习组件开发者 reinforce.io 近日发表了一篇博客文章介绍了 TensorForce 背后的架构和思想。本文会介绍TensorForce背后的架构和理念,如何用TensorFlow打造新型强化学习API。
这篇文章重点解决一个实际问题:应用增强学习社区怎样能从脚本和个例的收集来迭代增强学习的API —一个用于增强学习的 tf-learn 或 skikit-learn?
在讨论TensorForce框架之前,我们先探讨项目的观察和想法。如果你只想了解API,请随意跳过这部分。这篇文章不包含增强学习本身的介绍,也不展现一个新的模型或者讨论最新的算法。
▍动机
如果你是计算机系统、自然语言处理或者其他应用领域的科研人员。你对增强学习应该有一个基本的了解,并且对探索增强学习控制系统的一些方面有兴趣。
关于深度学习有很多博文,DQN(深度增强学习)、vanilla策略梯度、A3C算法等(比如Karpathy’s,(http://karpathy.github.io/2016/05/31/rl/)对策略梯度方法进行了直观的描述)。有很多开源代码可以帮助你起步,例如:OpenAI starter agents(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)
这个例子中的状态和动作是一个更通用状态/动作接口的简短形式。多通道输入包括一个图片和一个标题,定义如下。相同地,多个输出动作可以被定义。注意,单一的状态/动作简短形式可以被连续地用于与代理代码通信。
states = dict(
image=dict(shape=(64, 64, 3), type='float'),
caption=dict(shape=(20,), type='int')
)
配置参数取决于潜在的代理和使用的模型。一个对于每个代理的完整参数列表可以在样例配置被发现。
接下来,增强学习算法在TensorForce中是可以获得的:
随机代理基线(RandomAgent)
带有 generalized advantage estimation 的 vanilla 策略梯度(VPGAgent)
信任区域策略优化(TRPOAgent)
深度 Q 学习/双深度 Q 学习(DQNAgent)
规范化的优势函数(NAFAgent)
对专家演示的深度 Q 学习(DQFDAgent)
Asynchronous Advantage Actor-Critic(A3C)(可以隐含地通过 distributed 使用)
最后一点并不是说没有像A3CAgent,因为A3C描述一个异步更新的机制,不是一个特定的代理。因此,使用分布式TensorFlow异步更新机制是部分通用模型基本分类,所有的代理都来源于此。
因此如论文中描述的A3C代理是暗中地被实施,通过设置VPGAgent分布式标记。应当被注意的是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输出,并且利用函数被转化它为一个网络构造器。这里有一个JSON网络说明:
[
{
"type": "conv2d",
"size": 32,
"window": 8,
"stride": 4
},
{
"type": "conv2d",
"size": 64,
"window": 4,
"stride": 2
},
{
"type": "flatten"
},
{
"type": "dense",
"size": 512
}
]
和之前一样,这个配置必须被加到代理配置对象:
from tensorforce.core.networks import from_json
agent_config = Configuration(
...
network=from_json('configs/network_config.json')
...
)
默认层激活是relu, 但是这有其他激活函数可以获得(当下,elu, selu, softmax, tanh和sigmoid).更进一步,一个层的其他性能可以被更改。例如,一个被更改的密集层看上下像这样:
[
{
"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
...
)
▍内部状态和事件管理
不同于经典的监督式学习instances的设置,强化学习一个 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学习环境预处理器被应用于多个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
)
)
]
...
)
堆栈的每个预处理器有一个类型,并且可选args的list和一个kwargs的dict。比如 sequence 预处理器会取最近的四个状态(即:帧)然后将它们堆叠起来以模拟马尔可夫属性。随便一提:在使用比如之前提及的 LSTM 层时,这显然不是必需的,因为 LSTM 层可以通过内部状态建模和交流时间依赖。
▍探索
探索可以被配置对象定义,被代理应用到它的动作模型取决于(处理多个操作,再次,包含具体要求的词典可以被赋予)。比如,为了使用 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效用函数使用代理
让我们使用一个智能体,这个代码是在我们测试环境上运行的一个智能体:http://t.cn/RKgIs4L,我们将其用于连续积分——一个为给定智能体/模型的工作方式验证行动、观察和更新机制的最小环境。注意我们所有的环境实现(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》。
贝叶斯深度强化学习和关于不确定性的推理
我们遵循这些发展,并尝试把我们丢失已建立的技术找回来,新思路一旦我们被证实,有可能成为标准方法。在这个意义上,我们很明白不与有更高覆盖面的研究框架竞争。
最后请注意: 我们有一个内在的beta版,使用最领先的方法作为库函数。一旦我们觉得时机成熟,我们将考虑将它开源。
作者:Michael Schaarschmidt, Alexander Kuhnle, Kai Fricke
编译:西西
★推荐阅读★
招聘 志愿者
希望你有稳定输出的时间,英文能力佳,从业者优先。
加入「AI从业者社群」请备注个人信息
添加小鸡微信 liulailiuwang