在Rasa,我们正在建立对话式人工智能的基础设施,供开发者用来建立基于聊天和语音的助手。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编写的模型,专门用于这些任务。
具体的模型要求
你可能已经注意到,我们的例子不仅包括意图,还包括实体。当一个用户有兴趣进行购买时,他们(通常)也会说他们有兴趣购买什么。这些信息需要在用户提供时被检测出来。如果我们需要向用户提供一个表单来检索这些信息,那将是一个糟糕的体验。
如果你退一步想一想,什么样的模型可以在这里很好地工作,你很快就会认识到,这不是一个标准的任务。不仅仅是我们在每个语料中有许多标签;我们还有多种*类型的标签。这意味着我们需要有两个输出的模型。
Rasa开放源码提供了一个可以检测意图和实体的模型,叫做DIET。它使用一个转化器架构,允许系统从意图和实体的互动中学习。因为它需要同时处理这两项任务,所以典型的机器学习模式是行不通的。
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](https://github.com/RasaHQ/rasa/blob/1.10.x/rasa/utils/tensorflow/model_data.py#L31)` 对象。这样做的好处是,我们可以保留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.layer.Dense)来创建它。
我们越来越喜欢自定义,甚至把损失函数作为一个层来实现。这对我们来说很有意义,因为考虑到损失在NLP中会变得很复杂。许多NLP任务会要求你在训练过程中进行采样,这样你就会有负面例子的标签。在这个过程中,你可能还需要屏蔽标记。我们还对记录相似性损失以及标签准确性感兴趣。通过制作我们自己的层,我们正在建立可重复使用的组件,而且它也很容易维护。
学到的经验
发现这个定制的机会对Rasa来说是一个巨大的变化。我们喜欢把我们的算法设计得灵活并适用于许多情况,我们很高兴知道底层技术栈允许我们这样做。对于那些正在进行TensorFlow迁移的人,我们确实有一些建议。
- 从思考你的应用程序中需要哪些 "乐高砖 "开始。这个心理设计步骤将使你更容易认识到如何利用现有的Keras/TensorFlow对象来满足你的用例。
- 试图通过立即进行深入研究来沉浸其中是很诱人的。相反,从一个工作实例开始,并从那里开始深入研究可能会有帮助。TensorFlow不是一个普通的Python包,其内部结构可能会变得复杂。你与之交互的Python代码需要与C++交互以保持张量操作的性能。一旦代码运行,你就可以更好地开始调整/优化所有新的TensorFlow版本的性能特征。