2020 年 12 月 16 日 — Vincent D. Warmerdam 和 Vladimir Vlasov 的客座文章,Rasa
在 Rasa,我们正在构建用于对话式 AI 的基础设施,开发人员可以使用它来构建基于聊天和语音的助手。 Rasa 开源 是我们提供的核心产品,它提供了一个用于 NLU(自然语言理解)和对话管理的框架。 在 NLU 方面,我们提供处理意图分类和实体检测的模型,这些模型使用 TensorFlow 2.x 构建。…
Vincent D. Warmerdam 和 Vladimir Vlasov 的客座文章,Rasa
在 Rasa,我们正在构建用于对话式 AI 的基础设施,开发人员可以使用它来构建基于聊天和语音的助手。 Rasa 开源 是我们提供的核心产品,它提供了一个用于 NLU(自然语言理解)和对话管理的框架。 在 NLU 方面,我们提供处理意图分类和实体检测的模型,这些模型使用 TensorFlow 2.x 构建。
在本文中,我们想讨论迁移到最新版本 TensorFlow 的好处,并深入了解一些 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。 它使用 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)继承即可创建它。
我们非常喜欢自定义,甚至将损失函数实现为一个层。 考虑到损失在 NLP 中可能变得很复杂,这很有道理。 许多 NLP 任务都需要您进行采样,以便您在训练期间也拥有负面样本的标签。 您可能还需要在此过程中掩盖标记。 我们也对记录相似性损失和标签准确性感兴趣。 通过仅仅创建我们自己的层,我们正在构建可重复使用的组件,并且它也很容易维护。
经验教训
发现这种自定义的机会对 Rasa 产生了巨大影响。 我们喜欢设计灵活且适用于多种情况的算法,我们很高兴地了解到基础技术堆栈使我们能够做到这一点。 我们对正在进行 TensorFlow 迁移的人有一些建议