Rasa 开源如何通过 TensorFlow 2.x 获得灵活性
2020 年 12 月 16 日

Vincent D. Warmerdam 和 Vladimir Vlasov 的客座文章,Rasa


Rasa logo with bird

Rasa,我们正在构建用于对话式 AI 的基础设施,开发人员可以使用它来构建基于聊天和语音的助手。 Rasa 开源 是我们提供的核心产品,它提供了一个用于 NLU(自然语言理解)和对话管理的框架。 在 NLU 方面,我们提供处理意图分类和实体检测的模型,这些模型使用 TensorFlow 2.x 构建。

在本文中,我们想讨论迁移到最新版本 TensorFlow 的好处,并深入了解一些 Rasa 内部工作原理。

典型的 Rasa 项目设置

使用 Rasa 开源构建虚拟助手时,通常从定义 故事 开始,这些故事代表用户可能与您的代理进行的对话。 这些故事将用作训练数据,您可以将它们配置为 yaml 文件。 如果我们假装要制作一个允许您在线购买披萨的助手,那么我们的配置中可能会有这样的故事

yaml
version: "2.0"

stories:

- story: happy path
  steps:
  - intent: greet
  - action: utter_greet
  - intent: mood_great
  - action: utter_happy

- story: purchase path
  steps:
  - intent: greet
  - action: utter_greet
  - intent: purchase
    entities: 
product: “pizza”
  - action: confirm_purchase
  - intent: affirm
  - action: confirm_availability

这些故事包含意图和操作。 操作可以是简单的文本回复,也可以触发自定义 Python 代码(例如,检查数据库)。 为了定义每个意图的训练数据,您需要为助手提供示例用户消息,这些消息可能类似于

yaml
version: "2.0"

nlu:
- intent: greet
  examples: |
    - hey
    - hello
    - hi
    - hello there
    - good morning

- intent: purchase
  examples: |
    - i’d like to buy a [veggie pizza](product) for [tomorrow](date_ref)
    - i want to order a [pizza pepperoni](product)
    - i’d want to buy a [pizza](product) and a [cola](product)
- ...

使用 Rasa 训练助手时,您将提供如上所示的配置文件。 您可以在代理可以处理的对话类型中表达得很清楚。 意图和操作就像乐高积木一样,可以灵活地组合在一起,以覆盖许多对话路径。 一旦定义了这些文件,它们就会组合在一起创建代理将从中学习的训练数据集。

Rasa 允许用户构建自定义机器学习管道以适应他们的数据集。 这意味着,如果您愿意,您可以将自己的(预训练)模型纳入自然语言理解。 但是 Rasa 也提供了用 TensorFlow 编写的模型,这些模型专门用于这些任务。

特定模型要求

您可能已经注意到,我们的示例不仅包含意图,还包含实体。 当用户有兴趣购买商品时,他们(通常)也会说出他们有兴趣购买什么。 当用户提供这些信息时,需要检测到它。 如果我们需要向用户提供一个表格来检索这些信息,那将是一个糟糕的体验。

intent greet vs intent purchase

如果您退一步思考,什么样的模型在这里可以很好地工作,您很快就会意识到它不是一项标准任务。 不仅仅是在每次话语中都有许多标签;我们还有多种 *类型* 的标签。 这意味着我们需要具有两个输出的模型。

Types of labels texts to tokens

Rasa 开源提供了一个可以检测意图和实体的模型,称为 DIET。 它使用 Transformer 架构,允许系统从意图和实体之间的交互中学习。 因为它需要同时处理这两项任务,所以典型的机器学习模式将不起作用

model.fit(X, y).predict(X)

您需要不同的抽象。

抽象

这就是 TensorFlow 2.x 对 Rasa 代码库进行改进的地方。 现在更容易自定义 TensorFlow 类。 特别是,我们在 Keras 之上创建了一个 自定义 抽象,以满足我们的需求。 其中一个示例是 Rasa 自己的内部 `RasaModel.` 我们在下面添加了基类的签名。 完整的实现可以 在这里 找到。

class RasaModel(tf.keras.models.Model):

	def __init__(
    	self,
    	random_seed: Optional[int] = None,
    	tensorboard_log_dir: Optional[Text] = None,
    	tensorboard_log_level:Optional[Text] = "epoch",
    	**kwargs,
	) -> None:
    	...

	def fit(
    	self,
    	model_data: RasaModelData,
    	epochs: int,
    	batch_size: Union[List[int], int],
    	evaluate_on_num_examples: int,
    	evaluate_every_num_epochs: int,
    	batch_strategy: Text,
    	silent: bool = False,
    	eager: bool = False,
	) -> None:
		...

这个对象经过定制,允许我们传入我们自己的 `RasaModelData` 对象。 这样做的优点是,我们可以保留 Keras 模型对象提供的所有现有功能,同时可以覆盖一些特定方法以满足我们的需求。 我们可以在保留对“急切模式”的手动控制的同时,使用我们首选的数据格式运行模型,这有助于我们进行调试。

这些 Keras 对象现在是 TensorFlow 2.x 中的中心 API,这使得我们能够非常容易地集成和自定义它们。

训练循环

为了进一步了解代码如何变得更简单,让我们看一下 Rasa 模型中的训练循环。

TensorFlow 1.8 的 Python 伪代码

我们已经列出了用于旧训练循环的部分代码(请参阅 这里 获取完整的实现)。 请注意,它正在使用 `session.run` 来计算损失以及准确性。

def train_tf_dataset(
	train_init_op: "tf.Operation",
	eval_init_op: "tf.Operation",
	batch_size_in: "tf.Tensor",
	loss: "tf.Tensor",
	acc: "tf.Tensor",
	train_op: "tf.Tensor",
	session: "tf.Session",
	epochs: int,
	batch_size: Union[List[int], int],
	evaluate_on_num_examples: int,
	evaluate_every_num_epochs: int,
)
	session.run(tf.global_variables_initializer())
	pbar = tqdm(range(epochs),desc="Epochs", disable=is_logging_disabled())

for ep in pbar:
  ep_batch_size=linearly_increasing_batch_size(ep, batch_size, epochs)
   session.run(train_init_op, feed_dict={batch_size_in: ep_batch_size})

    ep_train_loss = 0
    ep_train_acc = 0
    batches_per_epoch = 0
    while True:
    	  try:
        	_, batch_train_loss, batch_train_acc = session.run(
            	[train_op, loss, acc])
        	batches_per_epoch += 1
        	ep_train_loss += batch_train_loss
        	ep_train_acc += batch_train_acc

    	  except tf.errors.OutOfRangeError:
        	break

train_tf_dataset 函数需要许多张量作为输入。 在 TensorFlow 1.8 中,您需要跟踪这些张量,因为它们包含您想要运行的所有操作。 在实践中,这会导致代码笨拙,因为很难将关注点分离。

TensorFlow 2.x 的 Python 伪代码

在 TensorFlow 2 中,由于 Keras 抽象,所有这些都变得容易多了。 我们可以从 Keras 类继承,该类允许我们将代码隔离开来。 以下是 Rasa 的 DIET 分类器 中的 `train` 方法(请参阅 这里 获取完整的实现)。

def train(
    	self,
    	training_data: TrainingData,
    	config: Optional[RasaNLUModelConfig] = None,
    	**kwargs: Any,
	) -> None:
    	"""Train the embedding intent classifier on a data set."""

    	model_data = self.preprocess_train_data(training_data)

    	self.model = self.model_class()(
        	config=self.component_config,
    	)

    	self.model.fit(
        	model_data,
        	self.component_config[EPOCHS],
        	self.component_config[BATCH_SIZES],
        	self.component_config[EVAL_NUM_EXAMPLES],
        	self.component_config[EVAL_NUM_EPOCHS],
        	self.component_config[BATCH_STRATEGY],
    	)

来自 Keras 的面向对象编程风格使我们能够进行更多自定义。 我们能够以这样一种方式实现我们自己的 `self.model.fit`,这样我们就不需要再担心 `session` 了。 我们甚至不需要跟踪张量,因为 Keras API 为您抽象了所有内容。

如果您有兴趣了解完整代码,您可以在 这里 找到旧循环,并在 这里 找到新循环。

额外的功能层

我们不仅在 Keras 模型中应用了这种抽象;我们还使用类似的技术开发了一些神经网络层。

我们自己实现了一些自定义层。 例如,我们有一个名为 `DenseWithSparseWeights.` 的层。 它就像一个密集层一样工作,但是我们在预先删除了许多权重,使其更稀疏。 我们只需要从正确的类(tf.keras.layers.Dense)继承即可创建它。

normal dense vs sparse dense model

我们非常喜欢自定义,甚至将损失函数实现为一个层。 考虑到损失在 NLP 中可能变得很复杂,这很有道理。 许多 NLP 任务都需要您进行采样,以便您在训练期间也拥有负面样本的标签。 您可能还需要在此过程中掩盖标记。 我们也对记录相似性损失和标签准确性感兴趣。 通过仅仅创建我们自己的层,我们正在构建可重复使用的组件,并且它也很容易维护。

custom layer

经验教训

发现这种自定义的机会对 Rasa 产生了巨大影响。 我们喜欢设计灵活且适用于多种情况的算法,我们很高兴地了解到基础技术堆栈使我们能够做到这一点。 我们对正在进行 TensorFlow 迁移的人有一些建议

  1. 首先,思考一下您的应用程序需要哪些“乐高积木”。 这种思维设计步骤将使您更容易识别如何在您的用例中利用现有的 Keras/TensorFlow 对象。
  2. 立即深入研究可能会很诱人。 相反,从一个有效的示例开始,然后从那里深入挖掘可能会有所帮助。 TensorFlow 不是一个普通的 Python 包,它的内部结构可能很复杂。 您交互的 Python 代码需要与 C++ 交互,以保持张量运算的性能。 一旦代码运行起来,您就可以更好地开始调整/优化所有新 TensorFlow 版本的性能特征。
下一篇文章
How Rasa Open Source Gained Layers of Flexibility with TensorFlow 2.x

Vincent D. Warmerdam 和 Vladimir Vlasov 的客座文章,Rasa
Rasa,我们正在构建用于对话式 AI 的基础设施,开发人员可以使用它来构建基于聊天和语音的助手。 Rasa 开源 是我们提供的核心产品,它提供了一个用于 NLU(自然语言理解)和对话管理的框架。 在 NLU 方面,我们提供处理意图分类和实体检测的模型,这些模型使用 TensorFlow 2.x 构建。…