了解 Rasa Open Source 如何通过 TensorFlow 2.x 获得层的灵活性

特邀博文 / Vincent D. Warmerdam 和 Vladimir Vlasov,Rasa

Rasa 致力于构建对话式 AI 的基础架构,供开发者用来构建基于聊天和语音的助理。我们的基石产品 Rasa Open Source 提供了 NLU(自然语言理解)和对话管理的框架。在 NLU 方面,我们提供了使用基于 TensorFlow 2.x 构建的模型来处理意图分类和实体检测的模型。

在本文中,我们将介绍迁移到最新版 TensorFlow 的优势,并深入了解一些 Rasa 内部构件的工作原理。

典型的 Rasa 项目设置

使用 Rasa Open Source 构建虚拟助理时,通常需要先定义 故事 (Stories),这些故事代表用户可能与您的代理进行的对话。这些故事将用作训练数据,您可以将它们配置为 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 编写的模型,专门用于处理这些任务。

特定模型要求

您可能已经注意到,我们的示例不仅包括意图,还包括实体。当用户有购买意图时,他们(通常)也会例举他们的目标购买物品。当用户提供这些信息时,需要对其进行检测。如果我们需要通过向用户提供表单来获取这些信息,用户体验会大打折扣。

如果我们回想下,看看什么样的模型可以胜任这种场景,很快就会认识到这不是一项标准任务。这不仅仅是因为我们在每次说话时都有许多标签,我们的标签也有多种类型。这意味着我们需要具有两个输出的模型。

Rasa Open Source 提供了一个可以检测意图和实体的模型,称为 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 模型对象提供的所有现有特性,同时我们可以覆写一些特定的方法来满足我们的需求。我们可以使用首选数据格式运行模型,同时保持对“eager 模式”的手动控制,这有助于我们进行调试。

这些 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) 即可创建。

我们非常热衷于自定义,甚至将损失函数作为一层来实现。考虑到 NLP 中的损失可能会变得复杂,这对我们来说很有意义。许多 NLP 任务将要求您进行抽样,以便在训练过程中也拥有负面样本的标签。在此过程中,您可能还需要屏蔽 token。我们也有兴趣记录相似度损失以及标签准确率。通过创建自己的层,我们就可以构建可重用的组件,并且组件易于维护。

经验总结

探索这个自定义功能对 Rasa 来说意义重大。我们希望将算法设计得灵活并适用于多种情况,并且我们很高兴得知底层技术栈允许我们这样做。对于正在进行 TensorFlow 迁移工作的用户,我们有如下建议:

  1. 首先考虑您的应用程序需要哪些“积木”。理论层面的设计环节可使您更轻松地认识到如何将现有的 Keras/TensorFlow 对象用于您的用例。

  2. 大家可能急切地想深入研究一番,以全面了解。那么,从一个可行的示例入手并深入研究可能会有所帮助。TensorFlow 不是普通的 Python 软件包,内部结构可能会变得复杂。您与之交互的 Python 代码需要与 C++ 交互,以保持张量运算的性能。一旦代码生效,您就可以更轻松地开始调节/优化所有新的 TensorFlow 版本的性能特性。

原文:How Rasa Open Source Gained Layers of Flexibility with TensorFlow 2.x
中文:TensorFlow 公众号