评估 Dialogflow 意图中训练短语的质量

本教程介绍如何分析和评估为 Dialogflow 代理的意图提供的训练短语的质量。此分析的目的是避免代理混淆与目标意图无关的短语或与其他意图相关性更高的短语。

此处使用 TensorFlow Hub (tf.Hub) Universal Sentence Encoder 模块生成训练短语的语义嵌入。然后,您根据相同意图和不同意图中的嵌入之间的相似度来计算凝聚度 (Cohesion) 和分离度 (Separation) 衡量值。本教程还标识了易混淆的训练短语,即相较于为其提供的意图,更接近(就嵌入空间而言)其他意图的短语。

您可以在此 Colab 笔记本中找到本教程使用的代码。本文假设您对 Dialogflow 的背景知识有基本的了解。如需详细了解 Dialogflow,请参阅这个由多篇文章组成的教程,其中介绍了如何在 Google Cloud 上使用 Dialogflow Enterprise Edition 来构建、保护和扩缩聊天机器人。

简介

利用 Dialogflow,您可以提供强大的自然语言理解 (NLU) 引擎来处理和理解自然语言输入,从而在产品和服务上构建对话界面。Dialogflow 的用例包括:

  • 构建适用于航空公司、电影院等机构的预订和预约机器人。
  • 简化快餐外卖系统。
  • 通过半自动呼叫中心实现高效的客户服务。

虽然您可以实现复杂的对话流来处理用户语言,但 Dialogflow 主要执行以下步骤:

  1. 回答用户提问,例如“我上个月的帐单总额是多少?”
  2. 代理解析输入,并将其与意图(例如 bill_value_inquiry 等)匹配。
  3. 代理还提取实体信息,如“上个月”。
  4. 给定所提取实体的意图后,代理调用履行 (fulfillment) 来响应用户的请求。

下表介绍了 Dialogflow 平台中的关键概念。

术语 说明
代理 关于代理的最贴切的描述是可集成到系统中的 NLU 模块。当用户输入的文本或口头请求与代理中的意图匹配时,代理可将这些输入转换为可对其执行操作的数据。
意图 在对话中,意图将用户输入映射到响应。在每个意图中,您可以定义可触发意图的用户话语、从每句话中提取的内容以及响应方式的示例(训练短语)。
实体 意图使代理能够理解特定用户输入背后的动机,实体则用于选出用户提及的信息特定中的片段。例如,可用于履行用户的请求的街道地址、商品名称或金额及单位。
履行 履行允许您使用代理提取的实体信息,按意图在后端生成动态响应或触发操作。

如需详细了解 Dialogflow 概念,请参阅 Dialogflow 文档

意图对 Dialogflow 系统至关重要,因为它们将用户请求与履行该请求所需的正确业务逻辑相关联。例如,某电信服务提供商的 Dialogflow 系统可能具有 bill_value_inquirypay_billupgrade_contractcancel_contractadd_service 等意图。但是,为了将用户话语(文本或口头)与正确意图匹配,需要使用一组相关性训练短语对意图进行训练。例如,对于天气查询意图,训练短语可能有:

  • “现在的天气如何?”
  • ”明天开罗的气温是多少?”
  • “下周我去苏黎世时需要带伞吗?”

如果您在系统中创建了多个意图,提供给某个意图的一些短语可能易混淆或具有误导性,例如,与其他意图更相关的短语可能会被用来训练错误的意图。比方说,假设您有一个 Dialogflow 代理,该代理用作某销售组织的真实数据来源。您可能有两个用于抓取联系人的意图:一个用于内部客户支持团队,一个用于客户,分别称为 get_internal_contactsget_external_contacts。每个意图的典型训练短语是:

  • get_internal_contacts:“谁是客户 X 的联系人?”
  • get_external_contacts:“如何联系客户 X?”

假设您的用户在查找外部联系人时提供“客户 X 的联系人”这一请求。此请求可能会令 Dialogflow 代理感到困惑,因为该短语与两个意图均匹配。如果匹配了错误的意图,用户将获得糟糕的体验,因为他们不得不更改请求的表达方式,麻烦又耗时。

因此,您要确保同一意图中的短语更相似,而不同意图之间的短语不那么相似。本教程的其余部分介绍如何评估为每个意图提供的训练短语的质量,以及如何标识可能混淆的训练短语。

方法

本教程中使用的方法是计算两个短语间的相似度,进而计算所有训练短语的相似度矩阵。获得该矩阵后,您可以计算以下项:

  • 凝聚度:同一意图中,各对短语之间的平均相似度值。该值是针对每个意图的。意图的凝聚度值越高,表示意图训练短语越好。
  • 分离度:对于给定的两个意图,一个意图中的每个训练短语与另一意图中的每个训练短语之间的平均差距。
  • 易混淆短语:与其他意图中的训练短语高度相似的训练短语。

要计算两个短语之间的相似度值,您必须将每个短语转换为代表短语的语义(嵌入)的实值特征向量。为帮助完成此任务,教程使用 TensorFlow Hub (tf.Hub),该库用于发布、发现和使用机器学习模型的可重用模块。这些模块可以是预训练模型,也可以是从文本、图片等内容中提取的嵌入。您可以浏览可用的文本嵌入。本教程使用 Universal Sentence Encoder (v2) 模块,该模块用于将文本编码成可用于文本分类、语义相似度、聚类及其他自然语言任务的 512 维向量。

本教程使用余弦相似度作为两个嵌入向量之间的接近度指标。给定两个实值向量(在我们的示例中,是从两个训练短语中提取的两个嵌入向量),余弦相似度表示使用以下公式计算的这两个向量之间角度的余弦:

$$ \cos(A,B) = \frac{\sum_{i=1}^{n}A_iB_i}{\sqrt{\sum_{i=1}^{n}{A_i^2}}\sqrt{\sum_{i=1}^{n}{B_i^2}}} $$

在此公式中,“n”是向量中的元素数。向量之间的角度越小,该角度的余弦值越大,表示相似度越高。任意两个向量之间的余弦相似度值始终介于 0 和 1 之间。

图 1 展示此方法的概览:

意图凝聚度和分离度评估概览

图 1:意图凝聚度和分离度评估概览

此图展示的顺序如下:

  1. 导入意图及其训练短语。
  2. 使用 tf.Hub Universal Sentence Encoder 预训练模块生成训练短语的嵌入。
  3. 在二维空间中直观显示生成的嵌入。
  4. 计算嵌入余弦相似度矩阵,其中包含不同意图中的所有训练短语之间的两两相似度值。
  5. 计算凝聚度和分离度指标。
  6. 标识易混淆短语。

目标

  • (可选)创建一个 Dialogflow 代理。
  • 导入意图与训练短语。
  • 运行 Colab 笔记本以评估意图质量。

费用

本教程使用 Google Cloud 的以下收费组件:

  • Dialogflow:Standard Edition 免费,而 Enterprise Edition 提供收费的企业支持服务。您可以在创建 Dialogflow 代理时选择使用哪个版本。您的帐号可以同时包含来自两个版本的代理。如需了解详情,请参阅 Dialogflow 价格页面

准备工作

  1. 登录您的 Google 帐号。

    如果您还没有 Google 帐号,请注册新帐号

  2. 在 Google Cloud Console 的项目选择器页面上,选择或创建一个 Google Cloud 项目。

    转到项目选择器页面

  3. 确保您的 Cloud 项目已启用结算功能。 了解如何确认您的项目是否已启用结算功能

  4. 启用 Dialogflow API。

    启用 API

  5. 创建一个服务帐号以调用 Dialogflow API。

    创建服务帐号
  6. 服务帐号详情对话框中,如下方屏幕截图所示输入帐号名称和说明,然后点击创建

    服务帐号详情对话框的屏幕截图
  7. 将角色设置为 Dialogflow API Client,然后点击继续

    服务帐号权限对话框的屏幕截图

在 Colab 笔记本中完成教程

以下部分介绍方法部分中讨论的用于计算凝聚度和分离度指标以及标识易混淆短语的步骤。

开始使用 Colab 笔记本

  1. 转到 Colab 笔记本:https://colab.research.google.com/drive/...

  2. 在 Google 云端硬盘中制作本地副本。

    将笔记本复制到您的 Google 云端硬盘

  3. 在 Cloud Shell 中,安装本教程其余部分所需的 Python 库,然后导入所需的库和模块。

    !pip install --quiet --upgrade tensorflow dialogflow scipy tensorflow-hub seaborn
    
  4. 设置您在准备工作部分创建的 Google Cloud PROJECT_IDSERVICE_ACCOUNT_EMAIL

    设置您的 Google Cloud PROJECT_ID 和 SERVICE_ACCOUNT_EMAIL

  5. 为您的会话进行身份验证,从而为您的服务帐号创建密钥:

    auth.authenticate_user()
    !gcloud config set project {PROJECT_ID}
    !gcloud iam service-accounts keys create sa-key.json \
        --iam-account={SERVICE_ACCOUNT_EMAIL} --project={PROJECT_ID}
    

    运行这些命令后,会显示一个链接。

  6. 点击该链接,为您的用户帐号进行身份验证。

  7. 复制网页上的身份验证代码,并将其粘贴到笔记本中的 Enter verification code 字段:

    笔记本中的 **Enter verification code** 字段

设置 Dialogflow 代理

如果您已拥有要在本教程中使用的 Dialogflow 代理,可以跳过此步骤。但如果您没有代理,或者要设置新代理,则可以下载名为 intents-healthcheck 的 ZIP 文件,其中包含已导出 Dialogflow 代理的内容。如下所示,您可以将此代理导入您的 Dialogflow 帐号:

  1. 下载已导入代理的 ZIP 文件:

    gsutil cp gs://dialogflow-intent-health-check/intent-quality-demo.zip .
    
  2. 转到 https://dialogflow.com/

  3. 点击右上角的 Go to Console 按钮。

  4. 在左侧菜单中,点击 Create new agent

    创建新代理

  5. 输入代理名称intents-healthcheck

  6. Google Project 列表中选择您的 Google Cloud 项目。

    • 一个 Google Cloud 项目只能有一个 Dialogflow 代理。因此,如果您在列表中没有找到您的 Google Cloud 项目,则表示已存在与您项目关联的代理。
    • 如果您选择 Create a new project,Dialogflow 会创建一个与您的代理同名的 Google Cloud 项目。
  7. 点击 Create

    输入有关代理的信息

  8. 在左侧菜单中,选择新代理,然后点击 Settings 图标。然后,在页面中间的菜单中,选择 Export and Import

    “Export and Import”对话框

  9. 点击 Restore from zip

    1. 选择您在第 1 步中下载的 agent-backup.zip 文件。
    2. 在表单底部的文本框中键入 RESTORE 以进行确认。
    3. 点击 Restore

    从 ZIP 文件恢复代理

    恢复代理后,Dialogflow 会创建 5 个意图。

  10. 从左侧菜单中选择 Intents 来验证已导入的意图。您会看到以下意图:

    验证已导入的意图

在本教程的其余部分中,您将使用此已恢复的代理。

在 Colab 笔记本中浏览代码

以下部分介绍了当您运行笔记本时,笔记本中的代码执行哪些操作。

抓取您的意图

以下代码使用 fetch_intents_training_phrases 方法从 Dialogflow 代理中抓取意图及其训练短语该方法会返回字典,在该字典中,键是您的 Dialogflow 代理中命名的意图,每个值是每个实体中的训练短语列表。在代码中,project 引用代理所属的项目,service_account_file 引用您之前创建的文件。

def get_intents(service_account_file, project):

    dialogflow_entity_client =  dialogflow.EntityTypesClient.from_service_account_file(service_account_file)
    parent = dialogflow_entity_client.project_agent_path(project)
    entities = list(dialogflow_entity_client.list_entity_types(parent))

    dialogflow_intents_client = dialogflow.IntentsClient.from_service_account_file(service_account_file)
    parent = dialogflow_intents_client.project_agent_path(project)
    intents = list(dialogflow_intents_client.list_intents(
        parent=parent,
        intent_view=dialogflow.enums.IntentView.INTENT_VIEW_FULL))

    entities_name_to_value = {}
    for intent in intents:
        entities_used = {entity.display_name
            for entity in intent.parameters}

        for entity in entities:
            if entity.display_name in entities_used \
                    and entity.display_name not in entities_name_to_value:
                entities_name_to_value[entity.display_name] = np.random.choice(
                    np.random.choice(entity.entities).synonyms, replace=False)

    intent_to_training_phrases = defaultdict(list)
    for intent in intents:
        for training_phrase in intent.training_phrases:
            parts = [entities_name_to_value[part.alias] if part.entity_type else part.text
                for part in training_phrase.parts]
            intent_to_training_phrases[intent.display_name].append("".join(parts))
        # Remove intents with no training phrases
        if not intent_to_training_phrases[intent.display_name]:
            del intent_to_training_phrases[intent.display_name]
    return intent_to_training_phrases
 

以下代码会验证检索到的意图:

intent_training_phrases = fetch_intents_training_phrases("sa-key.json", project_id)
for intent in intent_training_phrases:
    print("{}:{}".format(intent, len(intent_training_phrases[intent])))

fetch_intents_training_phrases 方法返回以下列表。此代码段显示了演示 intents-healthcheck 代理中的意图,后跟每个意图中可用的训练短语数。

start_conversation:4
close_conversation:5
get_internal_contacts:17
request_help:7
get_external_contacts:6

为训练短语生成嵌入

以下代码下载 tf.Hub Universal Sentence Encoder 预训练模块:

embed_module = hub.Module("https://tfhub.dev/google/universal-sentence-encoder/2")

首次使用后,该模块将在本地缓存。

以下代码会实现一个方法,该方法接受句子列表并返回基于 tf.Hub 模块的嵌入列表:

def make_embeddings_fn():
    placeholder = tf.placeholder(dtype=tf.string)
    embed = embed_module(placeholder)
    session = tf.Session()
    session.run([tf.global_variables_initializer(), tf.tables_initializer()])
    def _embeddings_fn(sentences):
        computed_embeddings = session.run(
            embed, feed_dict={placeholder: sentences})
        return computed_embeddings
    return _embeddings_fn

generate_embeddings = make_embeddings_fn()

此方法确保创建 tf.Session,并确保嵌入模块仅加载一次,而不是在每次调用方法时都加载。

以下代码为意图中的训练短语生成嵌入:

{
    intent: {
        training_phrase': [embedding_array]
    }
}

training_phrases_with_embeddings = defaultdict(list)
for intent_name, training_phrases_list in intent_training_phrases.items():
    computed_embeddings = generate_embeddings(training_phrases_list)
    training_phrases_with_embeddings[intent_name] = dict(zip(training_phrases_list, computed_embeddings))

此代码段创建 training_phrases_with_embeddings 嵌套的字典。

以下代码验证生成的嵌入:

training_phrases_with_embeddings = defaultdict(list)
for intent_name, training_phrases_list in intent_training_phrases.items():
    computed_embeddings = generate_embeddings(training_phrases_list)
    training_phrases_with_embeddings[intent_name] = dict(zip(training_phrases_list, computed_embeddings))
 

以下代码段显示了 start_conversation 意图中的每个训练短语,以及每个短语的嵌入向量的前五个元素。Universal Sentence Encoder 为每个训练短语生成一个 512 维嵌入向量。

Ciao!:[-0.03649221  0.02498418 -0.03456857  0.02827227  0.00471277]
Howdy!:[-0.02732556 -0.00821852 -0.00794602  0.06356855 -0.03726532]
Hello!:[-0.0255452   0.00690543 -0.00611844  0.05633081 -0.0142823 ]
Hi!:[-0.03227544 -0.00985429 -0.01329378  0.06012927 -0.03646606]

在二维空间中直观呈现嵌入

以下代码使用主成分分析计算主成分,从而将嵌入的维度从 512 减少至 2:

from sklearn.decomposition import PCA
embedding_vectors = None

for intent in training_phrases_with_embeddings:
    embeddings = list(training_phrases_with_embeddings[intent].values())
    if embedding_vectors is None:
        embedding_vectors = embeddings
    else:
        embedding_vectors = np.concatenate((embedding_vectors, embeddings))

pca = PCA(n_components=3)
pca.fit(embedding_vectors)

此代码段使用 sklearn 中的 PCA 类生成训练短语嵌入的 2D 表示。

以下代码生成已降维的短语嵌入的直观呈现:

import matplotlib.pyplot as plt

fig = plt.figure(figsize=(15,10))
ax = fig.add_subplot(111)

legend = []

for color, intent in enumerate(training_phrases_with_embeddings):
    phrases = list(training_phrases_with_embeddings[intent].keys())
    embeddings = list(training_phrases_with_embeddings[intent].values())
    points = pca.transform(embeddings)
    xs = points[:,0]
    ys = points[:,1]
    ax.scatter(xs, ys, marker='o', s=100, c="C"+str(color))
    for i, phrase in enumerate(phrases):
        ax.annotate(phrase, (xs[i], ys[i]))
    legend.append(intent)

ax.legend(legend)
plt.show()

下图显示产生的显示效果: 直观呈现已降维的短语嵌入

计算短语两两之间的相似度

以下代码使用 sklearn.metrics.pairwise.cosine_similarity 计算训练短语嵌入两两之间的余弦相似度。此代码会使用两两相似度值创建一个 Dataframe similarity_df

from sklearn.metrics.pairwise import cosine_similarity

flatten = []
for intent in training_phrases_with_embeddings:
        for phrase in training_phrases_with_embeddings[intent]:
            flatten.append((intent, phrase, training_phrases_with_embeddings[intent][phrase]))

data = []
for i in range(len(flatten)):
    for j in range(i+1, len(flatten)):
        intent_1 = flatten[i][0]
        phrase_1 = flatten[i][1]
        embedd_1 = flatten[i][2]
        intent_2 = flatten[j][0]
        phrase_2 = flatten[j][1]
        embedd_2 = flatten[j][2]
        similarity = cosine_similarity([embedd_1], [embedd_2])[0][0]
        record = [intent_1, phrase_1, intent_2, phrase_2, similarity]
        data.append(record)

similarity_df = pd.DataFrame(data,
    columns=["Intent A", "Phrase A", "Intent B", "Phrase B", "Similarity"])

以下代码显示示例相似度记录:

different_intent = similarity_df['Intent A'] != similarity_df['Intent B']
display(similarity_df[different_intent].sort_values('Similarity',
ascending=False).head(5))

以下代码段显示了不属于同一意图的最相似的训练短语:

不属于同一意图的最相似的训练短语

属于不同意图但相似度值高的短语可能会令 Dialogflow 代理困惑,并导致将用户输入引导至错误的意图。

衡量意图的凝聚度和分离度

以下代码会计算每个意图的凝聚度值,如“方法”部分所述。

same_intent = similarity_df['Intent A'] == similarity_df['Intent B']
cohesion_df = pd.DataFrame(similarity_df[same_intent].groupby('Intent A', as_index=False)['Similarity'].mean())
cohesion_df.columns = ['Intent', 'Cohesion']
display(cohesion_df)

结果是每个意图的凝聚度值:

为每个意图计算凝聚度值

以下代码计算意图之间的两两分离度,如“方法”部分所述。

different_intent = similarity_df['Intent A'] != similarity_df['Intent B']
separation_df = pd.DataFrame(similarity_df[different_intent].groupby(['Intent A', 'Intent B'], as_index=False)['Similarity'].mean())
separation_df['Separation'] = 1 - separation_df['Similarity']
del separation_df['Similarity']
display(separation_df.sort_values('Separation'))

结果是意图之间的两两分离度:

计算意图间的两两分离度

进一步改进

要改进意图的训练短语的质量,请考虑以下方法:

  • 找出属于不同意图的相似度高的短语,更改或移除这些短语。
  • 查找属于不同意图的最相似的短语。
  • 在凝聚度低的意图中添加更多训练短语,并调查分离度低的意图中的训练短语。

清理

  1. 在 Cloud Console 中,转到管理资源页面。

    转到“管理资源”

  2. 在项目列表中,选择要删除的项目,然后点击删除
  3. 在对话框中输入项目 ID,然后点击关闭以删除项目。

后续步骤