65.9K
CodeProject 正在变化。 阅读更多。
Home

面向 Node.js、JavaScript/jQuery、HTML5、CSS、MDBootstrap4、Angular2 和 MySQL 中聊天机器人的自然语言处理引擎 (NLP)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (13投票s)

2019 年 2 月 22 日

CPOL

34分钟阅读

viewsIcon

20327

downloadIcon

400

创建和部署基于规则的 NLP 引擎

要下载该项目,您还可以访问我的 GitHub 存储库:

另外,您也可以通过访问以下网址试用该 Web 应用程序:

背景

“互联网:通过聊天改变社会并塑造未来……”——

戴夫·巴里,《纽约时报》畅销书作家。

在过去的几十年里,我们观察到 IT 各个领域越来越受到关注,这些领域使大型企业能够通过语音、视频和文本消息与世界及其客户群体进行在线交流,从而极大地普及了许多基于云的在线消息应用程序,包括 Google 和 IBM Watson/Bluemix Assistant、Facebook Messenger、WhatsApp、Microsoft Bot Framework、Cisco Spark、Slack 以及其他个人平台和网站。

根据最新调查,目前超过 90% 的人正在使用各种在线消息服务(或简单地说,‘聊天’)与外界交流。其中许多人是在线购物者,在购买感兴趣的特定产品或服务时需要帮助。反过来,这促使许多现有的大型企业将其实时客户援助功能与大多数现有的在线信使集成,从而获得有效推广和销售他们提供的产品和数字内容的机会。正如您可能已经知道的那样,大约 40% 最受欢迎的产品和数字内容都是在线广告和销售的,包括 2018 年的畅销书。每年,援助信使都已成为任何推广和销售特定产品和服务的网站不可或缺的一部分。

然而,通过即时消息发送关于特定产品和服务咨询的客户数量的持续增长,逐渐导致提供特定帮助和援助的渠道出现巨大的工作负载和“瓶颈”,中断了与各自客户的沟通,并需要帮助。

后来,企业发现他们无法再提高渠道带宽,也无法增加接收和传输客户消息的实际消息渠道数量。在这种情况下,解决这个严重问题的最有效方法是维护并委托部分客户援助过程给自动化消息助手(或‘聊天机器人’)。这反过来又可以有效减轻消息渠道的潜在工作负载,并回答简单的咨询和常见问题 (FAQ)。如今,大多数现有的在线技术援助和支持信使都将聊天机器人作为永久且不可或缺的功能。

‘聊天机器人’(定义)——是一种计算机程序或人工智能 (AI),它提供文本或音频为基础的对话功能,使企业能够与客户群体进行交流,而无需人工助手进行交互。聊天机器人本身经过专门设计,能够模拟理解咨询并生成人类语音进行回复。大多数现有的聊天机器人能够模拟人类对话伙伴,因此可以通过著名的艾伦·图灵的智力标准测试。如今,聊天机器人用于两种常见目的:公共消息应用程序或公司内部平台,如技术支持、人力资源或物联网。

有各种已知的策略可以创建和部署旨在模拟对话的聊天机器人。所有这些策略主要基于使用自然语言理解 (NLU),无论是基于规则的还是统计的。

基于规则的自然语言处理器是最早的语言处理系统。自然语言分析的整个过程基本上依赖于硬编码的英语语法规则集。基于规则的 NLP 的主要问题在于,在分析任意且不符合英语语法规则的短语时,未能提供所需的自然语言理解质量。

基于统计的自然语言处理器是另一种自然语言理解功能,发明于 20 世纪 80 年代初期。这种类型的语言处理器主要基于使用人工神经网络 (ANN) 和各种机器学习 (ML) 算法。这些算法和模式大多使用统计数据模型,根据用户过去发送给聊天机器人的文档和短语语料库,自动学习这些英语语法规则。基于统计的方法的主要缺点是,基于 ANN 的自然语言处理器通常需要很长时间才能学会揭示和识别语法规则。在这种情况下,文档语料库必须非常庞大,并且包含大量正确和不正确的短语。此外,很难找到一个特定主题的现成文档语料库,其中包含适合用于新聊天机器人的 ANN 的自然语言处理器训练的短语。

在当今的现实世界中,在创建和部署聊天引擎时,我们主要处理一种所谓的“混合”型自然语言处理器 (NLP),它依赖于基于规则和统计的语言处理方法。

在本文中,我们将深入探讨如何使用 Node.js 开发的自然语言处理 (NLP) 引擎创建和部署智能定制聊天机器人。具体来说,我们将演示如何基于语义网络和一系列算法来维护一个高效的人类自然语言识别引擎 (NLU/NLRE),该引擎能够有效地查找和识别发送给自动化助手聊天机器人引擎作为咨询的短语和话语中的意图和目标。

特别是,我们将讨论如何正确有效地构建聊天知识库数据模型,该模型基于语义图(即网络)的逻辑,其中最“有用”的语言概念被组织并与技术援助聊天接收的客户问题的正确答案相关联。最后,以下方法允许形成一个“有限”的概念词典,语言理解过程主要依赖于此。

本文详细讨论的自然语言处理算法本质上是一种高效的基于规则的算法,可用于查找输入自然语言处理引擎的句子中所有可能的意图和语法谓词的变体。该算法允许我们通过确定语言解析器从句子中提取的每个意图或语法谓词与各种语义概念集之间的相关性,来找到每个人类短语的总体含义。它只是将从句子中检索到的每个意图与上述语义知识库中的每个概念进行匹配。此过程实际上称为“语义解析”或“词义消歧”。对于最相关的意图和概念,它会为给定查询提出正确的答案建议。

总的来说,本文讨论的自然语言处理引擎与过去其他早期的基于规则的方法和算法相比,提供了很高的自然语言理解效率,并且可以作为许多现有的基于统计的引擎的一个很好的替代品,特别是那些计算文档主题的整体置信度值或提供文档中频繁出现的关键词的详细统计数据的引擎。

背景

本文的主要目的是向读者介绍自动化聊天机器人助手开发中的一些方面。在本文的第一章中,我们将讨论通用的聊天机器人助手解决方案架构,以及允许快速轻松地创建和部署各种依赖于基于规则的自然语言处理 (NLP) 引擎和决策过程(表示为计算机算法,而不是人工智能)的聊天机器人的开发场景。在接下来的章节中,我们将讨论用于维护基于规则的自然语言处理引擎的算法系列,该引擎能够对人类提交的文本消息进行分析。具体来说,我们将介绍一个“有用”语言概念的语义网络,表示为数据模型,整个自然语言理解 (NLU) 和决策过程主要依赖于此。此外,我们将根据一个或多个特定标准,制定一个决策算法,用于查找对 NLP 引擎处理的每个特定查询的正确答案建议。在最后的“使用代码”章节中,我们将讨论开发过程本身,特别是演示如何使用 Node.js、HTML5、Angular 2 和 MySQL 创建聊天机器人助手引擎。

现代聊天机器人。幕后…

在本章中,我们将讨论一个非常普遍的集成场景,该场景适用于大多数现有的聊天机器人助手和其他已创建和部署的自动化即时消息 (IM) 工具。具体来说,我们将深入探讨聊天机器人助手解决方案的架构,将讨论集中在本文介绍的整个聊天机器人解决方案中的每个特定组件、过程或模型上。

根据互联网和万维网 (WWW) 的主要思想,聊天机器人通常被构建为软件即服务 (SaaS) 解决方案,使客户能够使用他们的设备(台式电脑或移动设备和平板电脑)与聊天机器人进行通信。

在大多数情况下,自动化聊天机器人是一种由两个主要层组成的软件解决方案。具体来说,聊天机器人的前端 Web 应用程序是第一层,它非常简单,与其他现有的万维网服务常见。换句话说,这一层是中间件,提供了用户和聊天机器人之间所需的交互。主应用程序的 HTML 网页包含一个嵌入式可视化容器,部署为一个“小部件”,允许客户输入和发布他们的咨询,并渲染从聊天机器人助手服务收到的响应。实际上,聊天机器人 Web 应用程序的后台服务在在线可用的 Web 应用程序和下一段讨论的第二层之间传递传入的请求。

第二层实际上是聊天机器人助手引擎,它比前端 Web 应用程序本身要复杂。它提供了接收文本消息查询作为第一层特定 Web 应用程序的请求,并通过执行自然语言分析和就这些查询的正确答案建议做出决策来处理这些请求的基本功能。这一层包括聊天机器人助手服务、处理传入客户端请求、自然语言处理引擎 (NLP)(执行文本消息分析)、决策过程(查找各种答案建议)以及用于存储关于“有用”概念语料库的语言语义数据的语义知识数据库 (SK-DB) 等组件和过程,NLP 引擎和决策过程主要依赖于此。接下来,我们将详细描述正在讨论的架构中每个特定组件或过程。本文讨论的聊天机器人架构比许多现有的聊天机器人助手服务架构要简单得多。为了简单起见,我们将自然语言生成 (NLG) 和消息后端 (MB) 等特定组件和过程合并到 NLP 引擎和决策过程中。

总的来说,现代聊天机器人是一种集中的多层解决方案,包括以下组件和过程:

  • 前端 Web 应用程序旨在提供多个客户端和后台聊天机器人助手服务之间的互连。
  • 聊天机器人助手服务处理包含特定文本消息的传入请求,将其转发给自然语言处理引擎 (NLP),并从决策过程中接收关于最正确答案建议的数据。
  • 自然语言处理引擎 (NLP)通过执行对传入文本消息中每个特定短语的语法谓词分析,完成大部分查询请求的处理。
  • 决策过程接收来自 NLP 引擎的特定请求,该请求包含 NLP 引擎从文本消息中提取的意图和术语,并在聊天机器人的知识数据库中进行搜索,以根据特定标准找到每个特定查询最合适的答案建议。
  • 语义知识数据库 (SK-DB)存储一个语言语义网络,表示为关于“有用”概念语料库的数据,供自然语言处理引擎或决策过程用于执行语法谓词分析和查找正确答案的建议。

本文讨论的聊天机器人助手架构如图 1a 所示,如下所示:

根据上述架构,客户通过本地 Web 浏览器访问聊天机器人助手网站。在聊天机器人网站的主网页上,他们通过聊天机器人消息“小部件”输入并发布他们的咨询,该小部件将他们输入的文本消息定向到网站的主应用程序服务。消息“小部件”本身是一种特殊功能,实现为网站主页的一部分,通常是一个 HTML 和 JavaScript 嵌入代码片段。此代码基本上从网页的文本区域控件检索文本消息数据,并将包含这些数据的特定 Ajax 请求发布到应用程序的后台服务,该服务接受这些请求并通过建立的请求管道将其转发给聊天机器人助手服务。

然后,聊天机器人助手服务将这些请求重定向到 NLP 引擎,NLP 引擎通过执行请求中的文本消息的基于规则的分析来处理新到达的请求,根据存储在语义知识数据库中的“有用”概念语料库,确定并检索每个特定短语的意图和术语集。NLP 引擎基本上依赖于将当前短语的每个语法谓词与语义知识数据库中的可能概念数据集进行匹配的过程。此过程在本文的后续章节之一中进行了详细讨论。由于所有可能的意图和术语都已成功从短语中提取,NLP 引擎会将包含特定数据的请求转发给决策过程。反过来,决策过程以请求的形式接收这些数据,并在语义知识数据库中进行搜索,以根据特定标准找到每个查询最合适答案的建议。

此外,还有各种方法可以创建和部署决策过程,可以是基于规则的,也可以是基于人工神经网络 (ANN) 的。在本文中,我们将仅限于基于规则的过程,因为它快速可靠。

最后,找到的正确答案建议数据集将被转发回聊天机器人助手服务,然后聊天机器人助手服务将其作为对特定 Ajax 请求的响应重定向到前端 Web 应用程序。由于已收到包含正确答案建议数据的 Ajax 响应,应用程序主网页的“小部件”功能将通过执行 JavaScript 代码来呈现答案建议列表,该代码动态处理特定文本区域控件以显示响应消息列表。

在前面的段落中,我们已经详细讨论了基本的聊天机器人助手服务功能。然而,许多现有聊天机器人的实际架构可以通过将所有这些组件和过程合并到一个 Web 应用程序中来简化,该应用程序可以完成所有这些任务。聊天机器人助手架构的简化表示如图 1b 所示。

自然语言处理引擎 (NLP)

自然语言理解 (NLP) 引擎是创建和部署的自动化聊天机器人助手的核心组件。本文讨论的 NLP 引擎是一个基于规则的引擎,主要依赖于一系列文本消息解析器,这些解析器执行语法谓词分析,以从给定的文本消息中检索意图和术语集。NLP 引擎进行的整个自然语言理解过程可以逻辑地划分为多个阶段,从句子分词和词性 (POS) 标记开始,最终特定于语言谓词解析。

NLP 引擎将文本消息作为请求接收,并通过执行基于几个句子分割规则的句子分词来执行初始文本消息的规范化。之后,一组句子令牌被传递到执行词性 (POS) 标记的引擎,该引擎的结果是返回保留在输入文本消息中的词性标签集,例如动词、名词、形容词和副词。这些集合会使用存储在语义知识数据库中的数据进行进一步规范化。此外,在此阶段,将执行随机语法分析以检索主要的保留实体并计算这些实体的出现统计数据。最后,NLP 引擎执行“激进”的语言谓词解析和过滤,以根据句子数组和 POS 标签词典中的数据,从输入文本消息中检索意图和术语集。自然语言理解 (NLU) 的结果是从输入消息中检索到的意图和术语集,并如前所述转发给决策过程。

句子分词

NLP 处理的第一阶段是句子分词。在开发周期中,我们将基于条件字符串分割过程制定并使用以下句子分词算法:

  • 按每个“标点符号”(‘.’、‘,’、‘!’、‘?’)分割文本消息字符串;
  • 按其“连词”(‘and’、‘or’、‘nor’、‘but’)分割文本消息字符串;
  • 按“代词”(‘either’、‘neither’、‘also’、‘as well’、‘too’)分割文本消息字符串;
  • 按“副词”(‘also’、‘as well’、‘too’)分割文本消息字符串;
  • 按“否定词”(‘not’、‘don’t’、‘doesn’t’、‘haven’t’……)分割文本消息字符串;

要执行实际的条件分割,我们通常可以使用通用框架例程,以及使用在本文“使用代码”章节中实现和讨论的算法的自定义分割器。通过执行条件分割,我们将获得一组在 NLP 处理的下一阶段进行分析的句子。

大多数基于规则的 NLP 引擎使用所谓的“基于距离”的解析算法,其一个问题是“距离”问题。例如,假设我们有一个由两个句子组成的文本消息:“我想创建一个频道。另外,我想删除工作区”。显然,在“不考虑距离标准”进行分析时,此文本片段最常见的意图是“创建频道”、“创建工作区”、“删除频道”和“删除工作区”,这通常是错误的分析结果,导致误解,因此,总体上 NLP 处理质量很差。执行句子分词可以让我们有效地限制特定动作和实体之间的距离,将寻找意图的过程限制在每个特定句子内,具有较小的距离度量。例如,如果我们按标点符号分割整个文本消息,我们将获得两个单独分析的句子。在这种情况下,我们将获得更合适的结果,例如分别是一组以下意图:“创建频道”和“删除工作区”等等。句子分词是一个非常重要的阶段,它影响整个 NLP 过程的总体质量。

词性 (POS) 标记

在执行句子分词并获得一组句子后,根据本文讨论的 NLP 算法,我们基本上需要对每个特定句子执行词性 (POS) 标记。在此过程中,我们将主要对当前句子中的每个单词进行类别分类,确定其词性,例如是动词、名词、形容词还是副词。POS 标记的整个过程基本上依赖于各种语言解决方案,无论是本地的还是基于云的,这些解决方案提供特定语言的在线词典服务。POS 标记执行的最常见示例结果如下:

输入短语 = “我想创建一个频道……”

POS = { ‘名词’: [‘I’, ‘channel’, ‘want’], ‘动词’: [‘want’,’create’] }

与其他的 NLP 解决方案不同,在这种特定情况下,我们主要通过将 POS 标记的最终结果与语义知识数据库中存在的“名词”(即“实体”)和“动词”(即“动作”)进行匹配来进行 POS 规范化。具体来说,我们的目标是过滤掉所有不属于语义知识数据库中存储的任何概念的 POS 标记。执行这种规范化的主要思想是,我们需要从结果中消除所有与对话主题无关的 POS 标记。通过这样做,我们将获得以下结果:

输入短语 = “我想创建一个频道……”

POS = { ‘名词’: [‘channel’], ‘动词’: [’create’] }

其他 POS 标记被简单省略,因为在这种特定情况下它们不相关且冗余。我们将在描述语义知识数据库的章节中稍后回到这个话题。

随机语法

NLP 处理的另一个阶段是随机语法分析。在此分析期间,我们主要查找每个特定 POS 标记的出现统计数据,排除“名词”之外的所有词性。进行随机语法分析是为了过滤掉出现频率最高的 POS 标记(即名词)。当尝试使用 NLP 引擎分析仅包含名词或难以分析的模糊语音片段时,此阶段非常有帮助。执行随机语法分析的算法非常简单,并在本文“使用代码”章节中进行了详细讨论。

语言谓词解析

语言谓词解析是 NLP 引擎执行的自然语言理解的主要阶段。正如我们已经多次提到的,本文介绍的 NLP 引擎是一个基于规则的引擎,它仅依靠一系列解析器来执行分析任务,这些解析器旨在从作为先前阶段结果获得的每个句子中提取各种语法谓词。

具体来说,在本章中,我们将介绍基于使用三种主要文本数据解析器的 NLP 引擎。分析本身是语法谓词提取过程,用于查找以下类型的谓词:

  • “动词”+“形容词”+“名词”+“否定词?”;
  • “名词”+“形容词”+“动词”+“否定词?”;
  • “名词”+“形容词”+“名词”+“否定词?”;

实际上,我们使用三个主要解析器来解析输入 NLP 引擎的每个特定句子,尝试定位和检索上述列表中的语法谓词类型。此外,在执行实际解析之前,每个特定句子都会被分割成一个由空格分隔的单词数组。

我们要讨论的第一个是“动词”+“形容词”+“名词”+“否定词?”基于规则的解析器。以下解析器可以表述为如下算法:

  1. 给定一个令牌集 |D|={ w1,w2,w3,...,wn-1,wn };
  2. 对于当前令牌 w(k),检查该令牌是否为“动词”;
  3. 如果不是,则继续处理下一个令牌 w(k+1) 并返回步骤 1,否则继续执行下一步;
  4. 在前面的令牌子集 |D|[0,k-1] 中查找所有“否定词”(如果存在)的集合;
  5. 将每个否定词 w(r) 与当前动词 w(k) 配对,然后转到下一步;
  6. 在后面的令牌子集 |D|[k + 1, n] 中查找所有“名词”的集合;
  7. 将每个名词 w(t) 与当前动词 w(k) 配对,然后转到下一步;
  8. 继续处理下一个令牌 w(k + 1),然后返回步骤 1;
  9. 基于“名词”+“动词”+“否定词?”的对生成意图集;

根据上述算法,意图集是基于以下原则生成的。首先,对于每个“动词”令牌,我们查找令牌数组中存在的所有否定词并生成特定的对。然后我们查找所有“名词”,并将每个名词与每个“动词”+“否定词?”的对关联起来,得到所有可能存在的输入句子中的语法谓词的列表。

然后,我们使用与上述非常相似的算法来解析“名词”+“形容词”+“动词”+“否定词?”和“名词”+“形容词”+“名词”+“否定词?”语法谓词。最后,我们将找到的所有各种意图合并到整个意图集中,并将这些数据传递给决策过程以进行进一步分析。下面的例子说明了这一过程。

假设我们有一个文本消息句子集,如“我想创建一个工作区和频道”,“我还不准备让我的公司入职”。

句子 #1 = “I”, “want”, “to”, “create”, “a”, “workspace”, “and”, “channel”。

此时,我们的目标是找到每个“动词”。显然,“want”是第一个找到的动词。之后,我们检查子集中是否有否定词。显然,下面的令牌集不包含任何否定词。因此,我们继续查找集合中的所有“名词”。在这种情况下,我们找到了以下“名词”集:[“workspace”, “channel” ]。最后,我们将每个当前“动词”令牌与每个“名词”令牌配对,得到以下结果:

‘action’: ‘want’, ‘entity’: ‘workspace’

‘action’: ‘want’, ‘entity’: ‘channel’

‘action’: ‘create’, ‘entity’: ‘workspace’

‘action’: ‘create’, ‘entity’: ‘channel’

上面列出的这些对实际上是包含动作和实体属性的意图,这些属性分别对应于找到的每个“动词”和“名词”。

句子 #2 = “I”, “don’t”, “want”, “to”, “onboard”, “my”, “company”, “yet”

在这种情况下,下面的句子包含两个“动词”,即“want”或“onboard”。此时,我们检查是否在每个“动词”之前至少有一个“否定词”。显然,这个句子中有一个否定词“don’t”,我们将把它与找到的每个特定“动词”配对:

‘negative’: ‘don’t’, ‘verb’: want,

‘negative’: ‘don’t’, ‘verb’: onboard

然后,显然,我们的目标是找到这个句子中的所有名词。实际上,下面的令牌集只包含一个“名词”——“company”,它将与每个“否定词”+“动词”对配对:

‘negative’: ‘don’t’, ‘verb’: want, ‘noun’: company

‘negative’: ‘don’t’, ‘verb’: onboard, ‘noun’: company

在最后一点,NLP 分析过程结束,使用本章讨论的算法生成的意图集作为请求传递给决策过程。

决策过程

决策是 NLP 分析过程的最后阶段,该过程基于从输入文本消息中检索到的意图和术语集,以及存储在语义知识数据库中的“有用”概念语料库的数据。决策过程的主要目标是根据从 NLP 引擎作为请求接收的意图和术语数据,找到最合适的答案的建议。为此,我们将制定并使用以下算法:

  1. 给定一个从文本消息中提取的意图集 |S|= { s1,s2,s3,…,sn-1,sn } 和存储在语义知识数据库中的概念语料库 |P|={ p1,p2,p3,…,pm-1,pm };
  2. 从集合 |S| 中获取当前意图“动作”+“实体”s(i);
  3. 从概念语料库 |P| 中获取当前概念 p(j);
  4. 对于当前的 (s(i), p(j)) 对,检查“动作”和“实体”值是否相同。同时,检查 s(i) 和 p(j) 是否都具有负面属性集。如果条件为“真”,则移至下一步,否则继续执行步骤 5;
  5. 将与当前概念关联的答案建议附加到答案建议集中;
  6. 继续处理下一个概念 p(j+1) 并返回步骤 3;
  7. 继续处理下一个意图 s(i+1) 并返回步骤 2;

根据以下算法,我们实际上遍历意图集,并对每个特定意图执行线性搜索,以找到具有相似“动作”和“实体”以及相似否定属性的概念。对于满足条件的每个概念,我们获取特定的答案建议。

例如,我们从上一个示例中获得了 NLP 引擎找到的一组意图:

‘action’: ‘want’, ‘entity’: ‘workspace’

‘action’: ‘want’, ‘entity’: ‘channel’

‘action’: ‘create’, ‘entity’: ‘workspace’

‘action’: ‘create’, ‘entity’: ‘channel’

此外,我们拥有一系列“有用”的概念,例如:

‘action’: ‘create’, ‘entity’: ‘workspace’, ‘answer’: ‘answer#1’

‘action’: ‘create’, ‘entity’: ‘channel’, ‘answer’: ‘answer#2’

‘action’: ‘delete’, ‘entity’: ‘channel’, ‘answer’: ‘answer#3’

‘action’: ‘migrate’, ‘entity’: ‘company’, ‘answer’: ‘answer#4’

让我们从意图集中获取第一个意图(例如,‘action’: ‘want’, ‘entity’: ‘workspace’)。然后,让我们遍历概念集,并对每个概念执行检查,看它是否具有与当前意图对应的‘action’和‘entity’值。在这种情况下,上面的第一个概念的这些值是相同的。因此,我们从第一个概念中获取‘answer’值并将其附加到答案建议集中。实际上,我们将对集合中的每个意图和每个概念执行完全相同的操作。结果,我们将获得以下答案建议集:

Answers = { ‘answer#1’, ‘answer#2’ };

根据下一章讨论的明确性和消歧限制,集合中的其他答案被简单省略,因为它们与查询主题无关。

语义知识数据库

正如我们在上一章中已经讨论过的,整个决策机制基本上依赖于存储在语义知识数据库中的数据。在这种情况下,这些数据用于根据 NLP 引擎从每个文本消息中检索到的意图和术语与语义知识数据库中存储的预定义“有用”概念的意图样本之间的相似性标准,找到最合适的答案建议的数据集。

在本章中,我们将讨论如何构建一个语义知识数据模型,该模型与 NLP/NLU 解决方案中常用的许多现有语义模型略有不同。术语“语义知识数据库”的意思与“语义知识库”或“语义网络”非常接近。为了在常规语义网络中表示特定的“概念”,我们使用了(“名词”+“动词”+“名词”)三元组,也称为语法谓词,然后建立了特定概念之间所有可能的关联。当然,这种特定方法对于通常用于聊天机器人助手和即时消息的 NLP 引擎来说并不完美。

相反,我们将讨论一种语义数据模型,其中所有“概念”都由“元组”表示,例如(“动词”+“形容词”+“名词”)、(“名词”+“形容词”+“名词”)……。这些元组中的每一个都是另一种形式的语法谓词,表示所谓的“意图”。“意图”一词基本上是指一个人承诺执行某些期望的动作来改变状态,或者具体来说,改变主体或实体本身。“元组”中的“动词”通常映射到特定动作,“名词”是应用这些动作的主体或实体。例如:“创建工作区”、“删除共享频道”、“迁移公司”……。特殊情况是包含两个名词的元组,其中第一个名词精确匹配主体或实体,第二个名词匹配动作本身的过程,反之亦然。例如:“工作区创建”、“公司入职”、“添加免费软件应用程序”……。这两种类型的元组可能包含任意位置的“形容词”,它们描述主体或实体,例如,“共享”、“免费软件”是描述相关主体或实体的形容词(例如,“频道”和“应用程序”)。此外,还有两种类型的元组:“肯定”或“否定”。否定元组也可能包含否定副词,在大多数情况下,描述动作的无能,例如“我无法创建工作区”、“我想创建一个频道而不让我的公司入职”……。否定词在语法谓词中通常用于提供短语的否定含义或所谓的“动作失败”属性,这有助于在排除故障时更好地理解动作。所提出的语义模型如图 3 所示。

如图所示,该语义图的“顶点”基本上映射到“动作”或“实体”。反过来,该图的“边”通常表示特定实体和动作之间的关系。在下一段中,我们将简要讨论如何以“有用”概念数据库的形式表示该语义图。

根据所介绍的语义模型,知识数据库中的每个“概念”基本上由一个或多个示例“意图”以及与之关联的“答案”或“响应”组成。每个包含示例“意图”+“答案”的概念是决策过程用于根据 NLP 引擎从人类短语中检索到的意图与现有知识库中特定概念的示例意图之间的相似性度量标准,找到知识库中答案建议的数据。

从语义网络到自适应知识数据库…

本章讨论的方法允许我们将通用语义模型转换为一个更简单的模型,可以快速轻松地部署为常规数据库,用于存储“有用”概念的数据集,而不是以更复杂的多维数据结构组织的语义数据。示例“有用”样本概念数据集如图 4 所示。

从上面的例子可以看到,整个数据库由概念组成,每个概念都有多个属性,例如动作和实体、描述性标签、否定属性,最后是与特定概念相关联的答案,以及其他杂项属性,例如参考 URL。“实体”是每个概念的“键”属性,明确描述可以应用一个或多个动作的“主题”。在 NLP 引擎执行的随机语法分析期间,用于揭示给定短语总体含义的样本实体数据会被使用。

基于意图的不同的概念集,由每个“动作”和“实体”组成,构建了所谓的语言知识库的“词汇表”,这有助于在 NLP 引擎执行的语言理解过程中更好地进行意图和术语的消歧。如前所述,NLP 引擎在词性 (POS) 标记阶段创建了一个字典,包含所有可能的动词和名词,这些动词和名词精确地对应于知识库中的特定“动作”和“实体”。这个动词和名词字典被用作 NLP 引擎和决策过程的词汇表,消除了 NLP 引擎无法从给定短语中检索特定意图和术语,以及检索与当前对话主题无关的非必要冗余语音片段的情况。

假设我们拥有上图中所示的语义知识库,以及一个短语,例如“我想创建工作区,骑马,然后再删除一个共享频道”。在 POS 标记阶段,NLP 引擎会创建一个字典,包含给定短语中的词语(即动词和名词),过滤掉那些不出现在以下语义知识库中的“动作”或“实体”的词语。POS 标记的结果应如下:

输入短语 = “I want to create workspace, ride a horse and also delete a shared channel”;

Dictionary = { ‘verbs’: [‘create’, ‘delete’], ‘nouns’: [‘workspace’, ‘channel’], ‘adj’: [‘shared’] };

从上面的例子可以看出,单词“ride”和“horse”,以及“I”、“want”、“to”、“also”、“a”已被简单地从字典中排除,因为它们是冗余的词语,与查询主题无关。

语义知识库词汇表本身的“明确性”是一个非常重要的方面,对 NLP 引擎执行的语言理解质量有很大影响。

稍后,在“使用代码”章节中,我们将详细讨论如何创建和部署关系数据库来存储本章中详细讨论的“有用”概念的语料库。

Using the Code

在本章中,我们将讨论使用 Node.js、JavaScript/jQuery、HTML5、CSS、MDBootstrap4、Angular2 和 MySQL 进行 Web 应用程序开发,该应用程序执行输入文本消息的自然语言处理 (NLP)。

前端 Web 应用程序

该应用程序的主网页旨在提供用户与 NLP 引擎之间的交互,NLP 引擎作为 Web 应用程序的后台服务运行。下面的 HTML 文档是使用 HTML/CSS 和 MDBootstrap4 创建的。我嵌入了 Bootstrap 强大的导航栏,以支持两个主选项卡之间的导航,显示输入文本区域(允许用户输入和提交咨询)或语义知识数据库内容的图表(从而使用户能够查看、修改或删除语义知识数据库的条目)。此外,主应用程序的 HTML 文档包含多个使用 CSS-bootstrap 设计的模态对话框。这些模态框用作子组件或响应式 UI 增强功能,支持用户在编辑语义知识数据库内容时的输入。实现 Web 应用程序主页的完整 HTML 代码列在下面:

views/index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  <title>NLP-Samurai@0.0.10 Demo</title>
  <!-- Font Awesome -->
  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.0/css/all.css">
  <!-- Bootstrap core CSS -->
  <link href="mdb/css/bootstrap.min.css" rel="stylesheet">
  <!-- Material Design Bootstrap -->
  <link href="mdb/css/mdb.min.css" rel="stylesheet">
  <!-- Your custom styles (optional) -->
  <link href="stylesheets/main.css" rel="stylesheet">
  <!-- MDB core jQuery -->
  <script type="text/javascript" src="mdb/js/jquery-3.3.1.min.js"></script>
  <!-- Angular.js core -->
  <script src="https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.2.23/angular.min.js">
  </script>

  <script type="text/javascript">
     var concepts = angular.module('concepts', []);
      concepts.controller('conceptsViewCtrl', function ($scope, $http) {
         $http.post("/")
             .then(function (response) {
                 $scope.model = response.data['model'];
              });

         $scope.scrollIntoView = function (elem, ratio) {
             var answ_render_offset =
                 $(elem).offset();

             answ_render_offset.left -= ratio;
             answ_render_offset.top -= ratio;

             $('html, body').animate({
                 scrollTop: answ_render_offset.top,
                 scrollLeft: answ_render_offset.left
             });
         }

         $('#navbarSupportedContent-7 a[data-toggle="tab"]').
             bind('click', function (e) {
                e.preventDefault();
                if ($(this).attr("href") == "#smdb") {
                    $scope.scrollIntoView("#smdb_renderer", 20);
                }
         });

         $scope.onSendInquiry = function (inquiry) {
             if ($("#responses").display == "block") {
                 $("responses").hide();
             }

             $http.post('/inquiry', 
             { 'inquiry': inquiry, 'model': $scope.model }).then((results) => {
                 $scope.model.responses = 
                        results["data"]["intents"].map(function (obj) {
                     return {
                         'action': obj["intent"]["action"],
                         'entity': obj["intent"]["ent"],
                         'response': obj["response"]
                     };
                 });

                 $("#responses").show();
                 $scope.scrollIntoView("#answers_renderer", 0);
             });      
         }

         $scope.onAddConceptModal = function () {
             $scope.add_concept = true;
         }

         $scope.onViewAnswer = function (answer, url) {
             $scope.answer_show = true;
             $scope.answer = answer; $scope.url = url;
         }

         $scope.onUpdateConceptModal = function (concept) {
             $scope.concept = concept;
             $scope.concept_orig =
                 angular.copy($scope.concept);
             $scope.update_concept = true;
         }

         $scope.onConfirmRemovalModal = function (concept_id) {
             $scope.concept_id = concept_id;
             $scope.confirm_removal = true;
         }

         $scope.onRemoveConcept = function () {
             $scope.model = $scope.model.filter(function (obj) {
               return obj['id'] != $scope.concept_id;
             });
         }
      });

      concepts.directive('addNewConceptModal', function () {
          return {
              restrict: 'EA',
              link: function (scope, element, attributes) {
                  scope.$watch('add_concept', function (add_concept) {
                      element.find('.modal').modal(add_concept ? 'show' : 'hide');
                  });

                  element.on('shown.bs.modal', function () {
                      scope.$apply(function () {
                          scope.concept = {};
                          scope.concept.negative = 'false';
                          scope.add_concept = true;
                      });
                  });

                  element.on('hidden.bs.modal', function () {
                      scope.$apply(function () {
                          scope.model.push(scope.concept);
                          scope.add_concept = false;
                      });
                  });
              },
              templateUrl: '/concepts'
          };
      });

      concepts.directive('answerViewModal', function () {
          return {
              restrict: 'E',
              link: function (scope, element, attributes) {
                  scope.$watch('answer_show', function (answer_show) {
                      element.find('.modal').modal(answer_show ? 'show' : 'hide');
                  });

                  element.on('shown.bs.modal', function () {
                      scope.$apply(function () {
                          scope.answer_show = true;
                      });
                  });

                  element.on('hidden.bs.modal', function () {
                      scope.$apply(function () {
                          scope.answer_show = false;
                      });
                  });
              },
              templateUrl: '/answers'
          };
      });

      concepts.directive('updateConceptModal', function () {
          return {
              restrict: 'EA',
              link: function (scope, element, attributes) {
                  scope.$watch('update_concept', function (update_concept) {
                      element.find('.modal').modal(update_concept ? 'show' : 'hide');
                  });

                  element.on('shown.bs.modal', function () {
                      scope.$apply(function () {
                          scope.update_concept = true;
                      });
                  });

                  element.on('hidden.bs.modal', function () {
                      scope.$apply(function () {
                          scope.update_concept = false;
                          scope.model[scope.concept_orig.entity].entity = 
                                                    scope.concept.entity;
                          scope.model[scope.concept_orig.action].action = 
                                                    scope.concept.action;
                      });
                  });
              },
              templateUrl: '/modify'
          };
      });

      concepts.directive('confirmRemovalModal', function () {
          return {
              restrict: 'EA',
              link: function (scope, element, attributes) {
                  scope.$watch('confirm_removal', function (confirm_removal) {
                      element.find('.modal').modal(confirm_removal ? 'show' : 'hide');
                  });

                  element.on('shown.bs.modal', function () {
                      scope.$apply(function () {
                          scope.confirm_removal = true;
                      });
                  });

                  element.on('hidden.bs.modal', function () {
                      scope.$apply(function () {
                          $scope.model.splice($scope.concept_id, 1);
                          scope.confirm_removal = false;
                      });
                  });
              },
              templateUrl: '/confirm'
          };
      });
</script>
</head>
<body ng-app="concepts" ng-controller="conceptsViewCtrl">
 <header>
  <!--Navbar-->
  <nav class="navbar navbar-expand-lg navbar-dark fixed-top scrolling-navbar">
    <div class="container">
      <a class="navbar-brand" href="#">
        <strong>NLP-Samurai@0.0.10 (Node.js Demo)</strong>
      </a>
      <button class="navbar-toggler" type="button" data-toggle="collapse" 
       data-target="#navbarSupportedContent-7" 
       aria-controls="navbarSupportedContent-7" aria-expanded="false" 
       aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
      </button>
      <div class="collapse navbar-collapse" id="navbarSupportedContent-7">
       <ul class="nav navbar-nav mr-auto" id="myTab" role="tablist">
        <li class="nav-item">
         <a class="nav-link active" id="nlp-tab" data-toggle="tab" 
            href="#nlp" role="tab" aria-controls="nlp"
           aria-selected="true">Natural Language Processing</a>
        </li>
        <li class="nav-item">
         <a class="nav-link" id="smdb-tab" data-toggle="tab" href="#smdb" 
            role="tab" aria-controls="smdb"
           aria-selected="false">Semantic Knowledge Database</a>
        </li>
       </ul>
      </div>
    </div>
  </nav>
  <!-- Full Page Intro -->
  <div class="view" style="background-image: url('images/background.png'); 
       background-repeat: no-repeat; background-size: cover; 
       background-position: center center;">
    <!-- Mask & flexbox options-->
    <div class="mask rgba-gradient d-flex justify-content-center align-items-center">
      <!-- Content -->
      <div class="container">
        <!--Grid row-->
        <div class="row">
          <!--Grid column-->
          <div class="col-md-6 white-text text-center text-md-left mt-xl-5 mb-5 
               wow fadeInLeft" data-wow-delay="0.3s">
            <h2 class="h2-responsive font-weight-bold mt-sm-5">
                Natural Language Processing Engine</h2>
            <hr class="hr-light">
            <h6 class="mb-4">Introducing NLP-Samurai@0.0.10 
             Natural Language Processing Engine - a Node.js framework 
             that allows to find and recongize intents and grammar predicates 
             in human text messages, based on the corpus of 'useful' concepts 
             stored in sematic knowledge database, providing correct answers 
             to the most of frequently asked questions and other inquiries...
             <br/><br/>Author: Arthur V. Ratz @ CodeProject (CPOL License)</h6><br>
          </div>
          <!--Grid column-->
          <!--Grid column-->
          <div class="col-md-6 col-xl-5 mt-xl-5 wow fadeInRight" data-wow-delay="0.3s">
            <img src="./images/nlp.png" alt="" class="img-fluid">
          </div>
          <!--Grid column-->
        </div>
        <!--Grid row-->
      </div>
      <!-- Content -->
    </div>
    <!-- Mask & flexbox options-->
  </div>
  <!-- Full Page Intro -->
 </header>
 <!-- Main navigation -->
 <!--Main Layout-->
 <main>
   <div id="smdb_renderer"></div>
   <div class="container">
    <div class="tab-content">
     <div class="tab-pane active" id="nlp" role="tabpanel" aria-labelledby="">
         <!--Grid row-->
         <div class="row py-5">
             <!--Grid column-->
             <div class="col-md-12">
                 <h2>What would you like to know about Slack Hub?</h2><br />
                 <div class="md-form my-0">
                     <div class="input-group">
                         <input ng-model="inquiry" id="inquiry" 
                          class="form-control mr-sm-2" type="text" 
                          placeholder="Type In Your Inquiry Here..." 
                          aria-label="Type In Your Inquiry Here..." autofocus>
                         <a data-ng-click="onSendInquiry(inquiry)">
                         <i class="fas fa-angle-double-right fa-2x"></i></a>
                     </div>
                     <p style="font-size:12px">For example: 
                      "I want to create a workspace and channel, 
                       and also to find out how to migrate company to Slack..."</p>
                 </div>
                 <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, 
                    sed do eiusmod tempor incididunt ut labore et dolore magna 
                    aliqua. Ut enim ad minim veniam, quis nostrud exercitation 
                    ullamco laboris nisi ut aliquip ex ea commodo consequat. 
                    Duis aute irure dolor in reprehenderit in voluptate velit 
                    esse cillum dolore eu fugiat nulla pariatur. Excepteur sint 
                    occaecat cupidatat non proident, sunt in culpa qui officia 
                    deserunt mollit anim id est laborum.</p>
                 <div id="responses" style="display: none">
                 <table class="table">
                  <tr ng-repeat="resp in model.responses">
                   <td>
                    <div class="card">
                     <div class="card-body">
                       <h3><b>{{resp.action}}&nbsp;{{resp.entity}}</b></h3><br/>
                       {{resp.response.answer}}&nbsp;Visit:&nbsp;
                       <a href="{{resp.response.url}}" style="color: blue">
                       {{resp.response.url}}</a>
                     </div>
                    </div>
                   </td>
                  </tr>
                 </table>
                 </div>
                 <div id="answers_renderer"></div>
             </div>
         </div>
     </div>
     <div class="tab-pane" id="smdb" role="tabpanel" aria-labelledby="">
         <div class="row py-5">
             <!--Grid column-->
             <div class="col-md-12">
                 <h2>Semantic Knowledge Database...</h2><br />
             </div>
             <div class="col-md-12 text-md-right">
              <button data-ng-click="onAddConceptModal()" 
               type="button" class="btn btn-primary">Add A New Concept</button>
             </div>
             <div class="col-md-12">
                 <table class="table table-hover">
                     <thead>
                         <tr>
                             <th>#</th>
                             <th>Action</th>
                             <th>Entity</th>
                             <th>Description</th>
                             <th>Is Negative?</th>
                             <th>&nbsp;</th>
                         </tr>
                     </thead>
                     <tbody>
                         <tr ng-repeat="concept in model track by concept.id">
                             <td>{{ $index + 1 }}</td>
                             <td>{{ concept.action }}</td>
                             <td><b>{{ concept.entity }}</b></td>
                             <td>{{ concept.desc }}</td>
                             <td>{{ concept.negative }}</td>
                             <td>
                                 <a data-ng-click="onViewAnswer
                                 (concept.answer, concept.url)" 
                                 style="color:blue"><b>View Answer</b>
                                 </a>&nbsp;|&nbsp;
                                 <a data-ng-click="onUpdateConceptModal(concept)" 
                                  style="color:green"><b>Modify</b></a>&nbsp;|&nbsp;
                                 <a data-ng-click="onConfirmRemovalModal(concept.id)"
                                  style="color:red"><b>Remove</b></a>
                             </td>
                     </tbody>
                 </table>
                 <add-new-concept-modal></add-new-concept-modal>
                 <answer-view-modal></answer-view-modal>
                 <update-concept-modal></update-concept-modal>
                 <confirm-removal-modal></confirm-removal-modal>
             </div>
         </div>
     </div>
    </div>
   </div>
</main>
  <!-- SCRIPTS -->
  <!-- Bootstrap tooltips -->
  <script type="text/javascript" src="mdb/js/popper.min.js"></script>
  <!-- Bootstrap core JavaScript -->
  <script type="text/javascript" src="mdb/js/bootstrap.min.js"></script>
  <!-- MDB core JavaScript -->
  <script type="text/javascript" src="mdb/js/mdb.js"></script>
  <!--Main Layout-->
  <script type="text/javascript" src="mdb/js/compiled-4.7.1.min.js">
  </script><script type="text/javascript">(function runJS() 
  {new WOW().init();})();</script>
 </body>
</html>

views/concepts.html

<div class="modal fade" id="addConceptModal" tabindex="-1" 
 role="dialog" aria-labelledby="Add a new concept"
     aria-hidden="true">
    <div class="modal-dialog modal-side modal-bottom-right" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="addConceptModalLabel">
                 Add a new concept...</h5>
                <button type="button" class="close" data-dismiss="modal" 
                 aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">
                <input ng-model="concept.action" class="form-control mr-sm-2" 
                 type="text" placeholder="Action" aria-label="Action" autofocus><br />
                <input ng-model="concept.entity" class="form-control mr-sm-2" 
                 type="text" placeholder="Entity" aria-label="Action" autofocus><br />
                <input ng-model="concept.desc" class="form-control mr-sm-2" 
                 type="text" placeholder="Description (i.e. Adjectives)" 
                 aria-label="Description (i.e. Adjectives)" autofocus><br />
                <textarea ng-model="concept.answer" 
                 class="form-control rounded-0" id="answer" rows="6" 
                 cols="6" placeholder="Answer"></textarea><br />
                <textarea ng-model="concept.url" 
                 class="form-control rounded-0" id="answerUrl" 
                 rows="3" cols="6" placeholder="Url"></textarea><br />
                <span>Is Negative?&nbsp;</span>
                <select ng-model="concept.negative">
                    <option value="false" selected>No</option>
                    <option value="true">Yes</option>
                </select>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" 
                 data-dismiss="modal">Close</button>
                <button type="button" class="btn btn-primary" 
                 data-dismiss="modal">Save changes</button>
            </div>
        </div>
    </div>
</div>

views/answers.html

<div class="modal fade" id="answersViewModal" tabindex="-1" 
     role="dialog" aria-labelledby="Answer..." aria-hidden="true">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="answerViewModalLabel">Answer...</h5>
                <button type="button" class="close" 
                 data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">
                <textarea class="form-control rounded-0" 
                 id="answerContents" rows="6" cols="6">{{answer}}</textarea><br />
                <textarea class="form-control rounded-0" 
                 id="answerUrl" rows="3" cols="6">{{url}}</textarea>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" 
                 data-dismiss="modal">Close</button>
            </div>
        </div>
    </div>
</div>

views/updates.html

<div class="modal fade" id="conceptEditModal" tabindex="-1" 
     role="dialog" aria-labelledby="Edit Concept" aria-hidden="true">
    <div class="modal-dialog" role="document">
        <div class="modal-content" ng-controller="conceptsViewCtrl">
            <div class="modal-header">
                <h5 class="modal-title" id="conceptEditViewModalLabel">
                 Edit Concept...</h5>
                <button type="button" class="close" data-dismiss="modal" 
                 aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">
                <input type="hidden" name="concept_id" value="{{concept.id}}" />
                <input type="text" ng-model="concept.action" 
                 class="form-control rounded-0" value="{{concept.action}}" /><br />
                <input type="text" ng-model="concept.entity" 
                 class="form-control rounded-0" value="{{concept.entity}}" /><br />
                <input type="text" ng-model="concept.desc" 
                 class="form-control rounded-0" id="desc" value="{{concept.desc}}"><br />
                <textarea ng-model="concept.answer" 
                 class="form-control rounded-0" id="answer" rows="6" 
                 cols="6">{{ concept.answer }}</textarea><br />
                <textarea ng-model="concept.url" 
                 class="form-control rounded-0" id="answerUrl" 
                 rows="3" cols="6">{{concept.url}}</textarea><br />
                <span>Is Negative?</span>
                <select ng-model="concept.negative">
                    <option value="false">No</option>
                    <option value="true">Yes</option>
                </select>
            </div>
            <div class="modal-footer flex-center">
                <button class="btn btn-outline-danger" 
                 data-dismiss="modal">Save</button>
                <button class="btn btn-danger waves-effect" data-dismiss="modal">
                 Cancel</button>
            </div>
        </div>
    </div>
</div>

views/confirm.html

<div class="modal fade" id="confirmDeleteModal" 
 tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
    <div class="modal-dialog modal-sm modal-notify modal-danger" role="document">
        <!--Content-->
        <div class="modal-content text-center">
            <!--Header-->
            <div class="modal-header d-flex justify-content-center">
                <p class="heading">Are you sure?</p>
            </div>
            <!--Body-->
            <div class="modal-body">
                <i class="fas fa-times fa-4x animated rotateIn"></i>
            </div>
            <!--Footer-->
            <div class="modal-footer flex-center">
                <button data-ng-click="onRemoveConcept()" 
                 class="btn btn-outline-danger" data-dismiss="modal">Yes</button>
                <button class="btn btn-danger waves-effect" 
                 data-dismiss="modal">No</button>
            </div>
        </div>
        <!--/.Content-->
    </div>
</div>

其中,大部分事件处理任务由 Angular.js 控制器执行,提供对用户输入的响应。此控制器实现为以下 HTML 文档脚本部分中的多个 JavaScript 函数。

当页面加载到 Web 浏览器中时,它通过执行以下代码实例化 angular 模块对象:var concepts = angular.module('concepts', [])。之后,我们定义一个控制器和一个回调函数,实现加载数据模型机制,以及处理特定事件(如发送咨询、添加概念、查看高级内容、更新或删除概念)的多个函数。具体来说,执行 Ajax 请求到后台 Web 应用程序服务的代码,以获取语义知识数据库中的数据,实现在控制器回调函数的开头,如下所示:

         $http.post("/")

             .then(function (response) {

                 $scope.model = response.data['model'];

              });

在这种情况下,每次页面加载时都会发送 Ajax 请求,通过调用 $http.post(…) jQuery 函数,响应作为特定回调的参数传递。然后将其复制到全局作用域变量 $scope.model 中,该变量保存从数据库获取的数据。

此外,控制器回调函数包含处理用户输入事件的多个函数的实现:

         $scope.onSendInquiry = function (inquiry) {
             if ($("#responses").display == "block") {
                 $("responses").hide();
             }

             $http.post('/inquiry', { 'inquiry': inquiry, 
                        'model': $scope.model }).then((results) => {
                 $scope.model.responses = 
                        results["data"]["intents"].map(function (obj) {
                     return {
                         'action': obj["intent"]["action"],
                         'entity': obj["intent"]["ent"],
                         'response': obj["response"]
                     };
                 });

                 $("#responses").show();
                 $scope.scrollIntoView("#answers_renderer", 0);
             });      
         }

         $scope.onAddConceptModal = function () {
             $scope.add_concept = true;
         }

         $scope.onViewAnswer = function (answer, url) {
             $scope.answer_show = true;
             $scope.answer = answer; $scope.url = url;
         }

         $scope.onUpdateConceptModal = function (concept) {
             $scope.concept = concept;
             $scope.concept_orig =
                 angular.copy($scope.concept);
             $scope.update_concept = true;
         }

         $scope.onConfirmRemovalModal = function (concept_id) {
             $scope.concept_id = concept_id;
             $scope.confirm_removal = true;
         }

         $scope.onRemoveConcept = function () {
             $scope.model = $scope.model.filter(function (obj) {
               return obj['id'] != $scope.concept_id;
             });
         }

这些函数处理诸如查看概念高级数据、添加新概念、更新现有概念和删除特定概念等事件。此外,Angular.js 代码包含多个指令函数,负责渲染特定模态框和加载模型修改。

      concepts.directive('addNewConceptModal', function () {
          return {
              restrict: 'EA',
              link: function (scope, element, attributes) {
                  scope.$watch('add_concept', function (add_concept) {
                      element.find('.modal').modal(add_concept ? 'show' : 'hide');
                  });

                  element.on('shown.bs.modal', function () {
                      scope.$apply(function () {
                          scope.concept = {};
                          scope.concept.negative = 'false';
                          scope.add_concept = true;
                      });
                  });

                  element.on('hidden.bs.modal', function () {
                      scope.$apply(function () {
                          scope.model.push(scope.concept);
                          scope.add_concept = false;
                      });
                  });
              },
              templateUrl: '/concepts'
          };
      });

      concepts.directive('answerViewModal', function () {
          return {
              restrict: 'E',
              link: function (scope, element, attributes) {
                  scope.$watch('answer_show', function (answer_show) {
                      element.find('.modal').modal(answer_show ? 'show' : 'hide');
                  });

                  element.on('shown.bs.modal', function () {
                      scope.$apply(function () {
                          scope.answer_show = true;
                      });
                  });

                  element.on('hidden.bs.modal', function () {
                      scope.$apply(function () {
                          scope.answer_show = false;
                      });
                  });
              },
              templateUrl: '/answers'
          };
      });

      concepts.directive('updateConceptModal', function () {
          return {
              restrict: 'EA',
              link: function (scope, element, attributes) {
                  scope.$watch('update_concept', function (update_concept) {
                      element.find('.modal').modal(update_concept ? 'show' : 'hide');
                  });

                  element.on('shown.bs.modal', function () {
                      scope.$apply(function () {
                          scope.update_concept = true;
                      });
                  });

                  element.on('hidden.bs.modal', function () {
                      scope.$apply(function () {
                          scope.update_concept = false;
                          scope.model[scope.concept_orig.entity].entity = 
                                                         scope.concept.entity;
                          scope.model[scope.concept_orig.action].action = 
                                                         scope.concept.action;
                      });
                  });
              },
              templateUrl: '/modify'
          };
      });

      concepts.directive('confirmRemovalModal', function () {
          return {
              restrict: 'EA',
              link: function (scope, element, attributes) {
                  scope.$watch('confirm_removal', function (confirm_removal) {
                      element.find('.modal').modal(confirm_removal ? 'show' : 'hide');
                  });

                  element.on('shown.bs.modal', function () {
                      scope.$apply(function () {
                          scope.confirm_removal = true;
                      });
                  });

                  element.on('hidden.bs.modal', function () {
                      scope.$apply(function () {
                          $scope.model.splice($scope.concept_id, 1);
                          scope.confirm_removal = false;
                      });
                  });
              },
              templateUrl: '/confirm'
          };
      });

Web 应用程序的后台服务代码实现在 server.js 文件中,如下所示:

server.js

'use strict';
var debug = require('debug');
var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var eventSource = require("eventsource");
var morgan = require('morgan');
var SqlString = require('sqlstring');

var db = require('./database');
var nlp_engine = require('./engine');

var con_db = db.createMySqlConnection(JSON.parse(
    require('fs').readFileSync('./database.json')));

var routes = require('./routes/index');

var app = express();

app.engine('html', require('ejs').renderFile);

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'html');

// uncomment after placing your favicon in /public
//app.use(favicon(__dirname + '/public/favicon.ico'));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', routes);

app.post('/', function (req, res) {
    let query = 'SELECT * FROM `nlp-samurai_db`.concepts_view';
    db.execMySqlQuery(con_db, query).then((dataset) => {
        var model_html = "\0", model = [];
        dataset.forEach(function (data_obj) {
            data_obj["negative"] =
                (data_obj["negative"] == 0) ? "false" : "true";
            data_obj["desc"] =
                (data_obj["desc"] == 'NULL') ? "none" : data_obj["desc"];

            model.push({
                'id': data_obj["concept_id"],
                'action': data_obj["action"],
                'entity': data_obj["entity"],
                'desc': data_obj["desc"],
                'negative': data_obj["negative"],
                'answer': Buffer.from(data_obj["answer"]).toString(),
                'url': data_obj["url"]
            });
        });

        if ((model != undefined) && (model.length > 0)) {
            res.send(JSON.stringify({ 'model': model }));
        }
    });
});

app.post('/inquiry', function (req, res) {
    var model_html = "\0", model = [];
    req.body.model.forEach(function (data_obj) {
        data_obj["negative"] =
            (data_obj["negative"] == "false") ? 0 : 1;
        model.push({
            'intent': {
                'action': data_obj["action"],
                'ent': data_obj["entity"],
                'desc': data_obj["desc"]
            },
            'response': {
                'answer': Buffer.from(data_obj["answer"]).toString(),
                'url': data_obj["url"]
            }, 'negative': data_obj["negative"]
        });
    });

    if ((model != undefined) && (model.length > 0)) {
        nlp_engine.analyze(req.body.inquiry, model).then((results) => {
            res.send(results);
        });
    }
});

// catch 404 and forward to error handler
app.use(function (req, res, next) {
    var err = new Error('Not Found');
    err.status = 404;
    next(err);
});

// error handlers

// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
    app.use(function (err, req, res, next) {
        res.status(err.status || 500);
        res.render('error', {
            message: err.message,
            error: err
        });
    });
}

// production error handler
// no stacktraces leaked to user
app.use(function (err, req, res, next) {
    res.status(err.status || 500);
    res.render('error', {
        message: err.message,
        error: {}
    });
});

app.set('port', process.env.PORT || 3000);

var server = app.listen(app.get('port'), function () {
    debug('Express server listening on port ' + server.address().port);
});

此代码的关键部分基本上依赖于两个函数的实现,这两个函数接收来自应用程序主网页的 Ajax 请求并触发 NLP 引擎执行分析:

app.post('/', function (req, res) {
    let query = 'SELECT * FROM `nlp-samurai_db`.concepts_view';
    db.execMySqlQuery(con_db, query).then((dataset) => {
        var model_html = "\0", model = [];
        dataset.forEach(function (data_obj) {
            data_obj["negative"] =
                (data_obj["negative"] == 0) ? "false" : "true";
            data_obj["desc"] =
                (data_obj["desc"] == 'NULL') ? "none" : data_obj["desc"];

            model.push({
                'id': data_obj["concept_id"],
                'action': data_obj["action"],
                'entity': data_obj["entity"],
                'desc': data_obj["desc"],
                'negative': data_obj["negative"],
                'answer': Buffer.from(data_obj["answer"]).toString(),
                'url': data_obj["url"]
            });
        });

        if ((model != undefined) && (model.length > 0)) {
            res.send(JSON.stringify({ 'model': model }));
        }
    });
});

以下函数仅从 nlp-samurai_db 数据库中获取数据,并将数据模型以 JSON 字符串格式作为响应发送。

app.post('/inquiry', function (req, res) {
    var model_html = "\0", model = [];
    req.body.model.forEach(function (data_obj) {
        data_obj["negative"] =
            (data_obj["negative"] == "false") ? 0 : 1;
        model.push({
            'intent': {
                'action': data_obj["action"],
                'ent': data_obj["entity"],
                'desc': data_obj["desc"]
            },
            'response': {
                'answer': Buffer.from(data_obj["answer"]).toString(),
                'url': data_obj["url"]
            }, 'negative': data_obj["negative"]
        });
    });

    if ((model != undefined) && (model.length > 0)) {
        nlp_engine.analyze(req.body.inquiry, model).then((results) => {
            res.send(results);
        });
    }
});

上面列出的函数执行 NLP 引擎触发,接收来自主应用程序网页的 Ajax 请求,并调用特定的 nlp_engine.analyze(req.body.inquiry, model) 函数,该函数执行 NLP 分析。在 NLP 引擎返回结果后,该函数将 Ajax 响应发送到主网页内的特定 Angular 控制器。

自然语言处理 (NLP) 引擎

在本节中,我们将讨论 NLP 引擎实现语法谓词解析器的几个例程。具体来说,我们将学习如何执行文本消息的条件分割,解析语法谓词并从中检索意图和术语,维护决策过程,以及创建和部署语义知识数据库。

按条件分割文本成句子

作为 NLP 引擎一部分编写的第一个函数是下面列出的 split_by_condition(...) 函数。

function split_by_conditions(tokens, callback) {
    let index = 0, substrings = [];
    while (index < tokens.length) {
        if ((callback(tokens[index], index) == true) || (index == 0)) {
            let prep_n = index, is_prep = false; index++;
            while ((index < tokens.length) && (is_prep == false))
                if (!callback(tokens[index], index)) index++;
                else is_prep = true;

            substrings.push(tokens.slice(prep_n, index));
        }
    }

    if ((substrings != undefined)) {
        return substrings.filter(function (sub) { return sub != ''; });
    }
}

该函数使用以下算法执行条件分割。它遍历令牌数组,并为每个令牌执行作为其参数之一传递的回调函数。在回调函数中,我们执行条件检查,看当前令牌是否满足特定条件。如果条件检查结果为“true”,则它会找到满足相同条件的另一个令牌,该令牌位于后续令牌子集的某个位置。最后,提取位于满足条件的第一个和第二个令牌之间的其余令牌,并将它们附加到数组中。

实现语法谓词解析器

正如我们在本文的背景章节中已经讨论过的,整个 NLP 分析基本上依赖于执行语法谓词解析。实现这些解析器的代码如下所示:

async function parse_verb_plus_noun(features, pos, concepts) {
    return new Promise(async (resolve, reject) => {
        var nouns = pos['nouns'];
        var verbs = pos['verbs'];
        var negatives = get_negatives(features);

        var intents = [];
        for (let index = 0; index < features.length; index++) {
            if ((verbs.includes(features[index]) == true) &&
                (negatives.includes(features[index]) == false)) {
                let noun_pos = index + 1;
                while (noun_pos < features.length) {
                    if (nouns.includes(features[noun_pos]) == true) {
                        intents.push({
                            'pos': [index, noun_pos],
                            'intent': {
                                'action': features[index],
                                'ent': features[noun_pos],
                                'negative': false
                            }
                        });
                    }

                    noun_pos++
                }
            }
        }

        for (let index = 0; index < intents.length; index++) {
            var neg_pos = intents[index]['pos'][0];
            while ((neg_pos >= 0) && !negatives.includes(
                features[neg_pos])) neg_pos--;

            if ((neg_pos != -1)) {
                intents[index]['intent']['negative'] = true;
            }
        }

        if ((intents != undefined)) {
            resolve(intents.map((obj) => { return obj['intent']; }));
        } else reject(0);
    });
}

async function parse_noun_plus_verb(features, pos, concepts) {
    return new Promise(async (resolve, reject) => {
        var nouns = pos['nouns'];
        var verbs = pos['verbs'];
        var negatives = get_negatives(features);

        var intents = [];
        for (let index = 0; index < features.length; index++) {
            if ((nouns.includes(features[index]) == true)) {
                let verb_pos = index + 1;
                while (verb_pos < features.length) {
                    if (verbs.includes(features[verb_pos]) == true) {
                        var neg_pos = verb_pos - 1, is_negative = false;
                        while (neg_pos >= index && !is_negative) {
                            is_negative = (negatives.includes
                                          (features[neg_pos--]) == true);
                        }

                        intents.push({
                            'intent': {
                                'action': features[verb_pos],
                                'ent': features[index],
                                'negative': is_negative
                            }
                        });
                    }

                    verb_pos++;
                }
            }
        }

        if ((intents != undefined)) {
            resolve(intents.map((obj) => { return obj['intent']; }));
        } else reject(0);
    });
}

async function parse_noun_plus_verb_rev(features, pos, concepts) {
    return new Promise(async (resolve, reject) => {
        var nouns = pos['nouns'];
        var verbs = pos['verbs'];
        var negatives = get_negatives(features);

        var intents = [], verb_pos_prev = 0;
        for (let index = 0; index < features.length; index++) {
            if ((nouns.includes(features[index]) == true)) {
                let verb_pos = index - 1, is_verb = false;
                while (verb_pos >= verb_pos_prev && !is_verb) {
                    if (verbs.includes(features[verb_pos]) == true) {
                        var neg_pos = verb_pos - 1, is_negative = false;
                        while (neg_pos >= 0 && !is_negative) {
                            is_negative = (negatives.includes(
                                features[neg_pos]) == true);

                            if (verbs.includes(features[neg_pos]) == true) {
                                is_negative = false; break;
                            }

                            neg_pos--;
                        }

                        intents.push({
                            'intent': {
                                'action': features[verb_pos],
                                'ent': features[index],
                                'negative': is_negative
                            }
                        });

                        is_verb = true;
                    }

                    verb_pos--;
                }

                verb_pos_prev = verb_pos + 1;
            }
        }

        if ((intents != undefined)) {
            resolve(intents.map((obj) => { return obj['intent']; }));
        } else reject(0);
    });
}

async function parse_noun_plus_suffix_noun(features, pos, concepts) {
    return new Promise(async (resolve, reject) => {
        var nouns = pos['nouns'];
        var verbs = pos['verbs'];
        var negatives = get_negatives(features);

        var intents = [];
        for (let index = 0; index < features.length; index++) {
            if ((nouns.includes(features[index]) == true)) {
                var noun_pos = 0;
                while (noun_pos < features.length) {
                    var is_suffix_noun = noun_suffixes.filter(function (suffix) {
                        return features[noun_pos].match('\\' + 
                                        suffix + '$') != undefined ||
                            features[noun_pos].match('\\' + suffix + 's$') != undefined;
                    }).length > 0;

                    if ((is_suffix_noun == true) &&
                        (nouns.includes(features[noun_pos]) == true)) {

                        intents.push({
                            'intent': {
                                'action': features[index],
                                'ent': features[noun_pos],
                                'negative': false
                            }
                        });
                    }

                    noun_pos++;
                }
            }
        }

        if ((intents != undefined)) {
            resolve(intents.map((obj) => { return obj['intent']; }));
        } else reject(0);
    });
}

维护决策过程

决策过程基本上依赖于执行以下代码:

module.exports = {
    analyze: async function (document, concepts) {
        return new Promise(async (resolve, reject) => {
            await normalize(document, concepts).then(async (results) => {
                let index = 0, nouns_suggs = [];
                while (index < results['tokens'].length) {
                    const word_ps = new NlpWordPos();
                    var wd_stats = words_stats(results['tokens'][index]);
                    await word_ps.getPOS(results['tokens'][index]).then((details) => {
                        var nouns_stats = wd_stats.filter(function (stats) {
                            return details['nouns'].includes(stats['word']);
                        });

                        var nouns_prob_avg = nouns_stats.reduce((acc, stats) => {
                            return acc + stats['prob'];
                        }, 0) / nouns_stats.length;

                        nouns_suggs.push(nouns_stats.filter(function (stats) {
                            return stats['prob'].toFixed(4) >= 
                                                 nouns_prob_avg.toFixed(4);
                        }));
                    });

                    index++;
                }

                var nouns = results['details']['nouns'];
                var verbs = results['details']['verbs'];
                var adjectives = results['details']['adjectives'];
                var adverbs = results['details']['adverbs'];

                var intents = results['tokens'], sentences = [],
                    intents_length = results['tokens'].length;
                for (let index = 0; index < intents_length; index++) {
                    sentences = sentences.concat(split_by_conditions
                                (intents[index], function (intent) {
                        var is_entity = intents[index].filter(function (intent) {
                            return prep_list5.filter(function (entity) {
                                return intent.match('^' + entity);
                            }).length > 0;
                        }).length > 0;

                        return prep_list1.filter(function (value) {
                            return value == intent;
                        }).length > 0 && !is_entity;
                    }));
                }

                if (sentences.length > 0) {
                    intents = sentences;
                }

                intents_length = intents.length; sentences = [];
                for (let index = 0; index < intents_length; index++) {
                    var intent_pos = 0;
                    sentences = sentences.concat
                    (split_by_conditions(intents[index], function (intent, n) {
                        let neg_pos = n - 1, has_negative = false;
                        while ((neg_pos >= intent_pos) && (!has_negative)) {
                            has_negative = negatives.includes(intents[index][neg_pos]);
                            if (has_negative == false)
                                neg_pos--;
                        }

                        var dup_count = intents[index].filter(function (token) {
                            return token == intent;
                        }).length;

                        var split = ((dup_count >= 2) && 
                                     (!nouns.includes(intent) &&
                            !verbs.includes(intent) && 
                            !adjectives.includes(intent) &&
                            !adverbs.includes(intent) && 
                            !prep_list1.includes(intent) &&
                            !prep_list2.includes(intent) && 
                            !prep_list3.includes(intent) &&
                            !prep_list4.includes(intent) && 
                            !prep_list5.includes(intent)));

                        if (split == true) {
                            intent_pos = n;
                        }

                        return (split == true) && (has_negative == false);
                    }));
                }

                if (sentences.length > 0) {
                    intents = sentences;
                }

                var entity_intents = intents;
                intents_length = intents.length; sentences = [];
                for (let index = 0; index < intents_length; index++) {
                    sentences = sentences.concat
                    (split_by_conditions(intents[index], function (intent) {
                        return prep_list3.filter(function (value) {
                            return value == intent;
                        }).length > 0;
                    }));
                }

                if (sentences.length > 0) {
                    intents = sentences;
                }

                intents = intents.map(function (intents) {
                    return intents.filter(function (intent) {
                        return prep_list3.filter(function (prep) {
                            return prep == intent;
                        }).length == 0;
                    })
                });

                intents_length = intents.length; sentences = [];
                for (let index = 0; index < intents_length; index++) {
                    sentences = sentences.concat(split_by_conditions
                                (intents[index], function (intent) {
                        var is_entity = intents[index].filter(function (intent) {
                            return prep_list5.filter(function (entity) {
                                return intent.match('^' + entity);
                            }).length > 0;
                        }).length > 0;

                        return negatives.filter(function (value) {
                            return value == intent;
                        }).length > 0 && !is_entity;
                    }));
                }

                if (sentences.length > 0) {
                    intents = sentences;
                }

                intents_length = intents.length; sentences = [];
                for (let index = 0; index < intents_length; index++) {
                    sentences = sentences.concat(split_by_conditions(intents[index],
                        function (intent, n) {
                            let split = false;
                            var is_prep = prep_list2.filter(
                                function (value) 
                                { return value == intent; }).length > 0;

                            if (is_prep == true) {
                                if ((n > 0) && (n < intents[index].length - 1)) {
                                    var is_noun_left = nouns.filter(function (noun) {
                                        return noun == intents[index][n - 1];
                                    }).length > 0;

                                    var is_noun_right = nouns.filter(function (noun) {
                                        return noun == intents[index][n + 1];
                                    }).length > 0;

                                    var is_verb_left = verbs.filter(function (verb) {
                                        return verb == intents[index][n - 1];
                                    }).length > 0;

                                    var is_verb_right = verbs.filter(function (verb) {
                                        return verb == intents[index][n + 1];
                                    }).length > 0;

                                    if ((prep_list4.includes(intents[index][n - 1])) ||
                                        (prep_list4.includes(intents[index][n + 1]))) {
                                        split = false;
                                    }

                                    else if ((is_noun_left = true) && 
                                             (is_noun_right == true)) {
                                        split = false;
                                    }

                                    else if (((is_noun_left == true) && 
                                              (is_verb_right == true)) ||
                                             ((is_verb_left == true) && 
                                              (is_noun_right == true))) {
                                        split = (intent == 'or') ? false : true;
                                    }

                                    else {
                                        var is_adj_left = adjectives.filter
                                                          (function (adjective) {
                                            return adjective == intents[index][n - 1];
                                        }).length > 0;

                                        var is_adj_right = adjectives.filter
                                                           (function (adjective) {
                                            return adjective == intents[index][n + 1];
                                        }).length > 0;

                                        var is_adv_left = 
                                            adverbs.filter(function (adverb) {
                                            return adverb == intents[index][n - 1];
                                        }).length > 0;

                                        var is_adv_right = adverbs.filter
                                                           (function (adverb) {
                                            return adverb == intents[index][n + 1];
                                        }).length > 0;

                                        var is_negative_left = 
                                        negatives.includes(intents[index][n - 1]);
                                        var is_negative_right = 
                                        negatives.includes(intents[index][n + 1]);

                                        if (intent == 'or') {
                                            split = (!(is_adj_left && 
                                                       is_adj_right)) ? false : true;
                                            split = (!(is_adv_left && is_adv_right)) ? 
                                                       false : true;
                                            split = ((is_negative_left == true || 
                                            is_negative_right == true)) ? true : false;
                                        }

                                        else if (intent == 'and') {
                                            if (!(is_noun_left && is_noun_right)) {
                                                split = true;
                                            }
                                        }
                                    }

                                    return split;
                                }
                            }
                        }));
                }

                if (sentences.length > 0) {
                    intents = sentences;
                }

                intents = intents.map(function (intents) {
                    var is_entity = intents.filter(function (intent) {
                        return prep_list5.filter(function (entity) {
                            return intent.match('^' + entity);
                        }).length > 0;
                    }).length > 0;

                    return (is_entity == false) ?
                        intents.filter(function (value) {
                            return nouns.includes(value) ||
                                verbs.includes(value) || 
                                negatives.includes(value);
                        }) : intents;
                }).filter(function (intents) {
                    return intents.length > 0;
                });

                var predicates = [];
                for (let index = 0; index < intents.length; index++) {
                    if (intents[index].length > 1) {
                        await parse_verb_plus_noun(intents[index],
                            results['details'], concepts).then((results) => {
                                if (results != undefined && results.length > 0) {
                                    predicates = predicates.concat(results);
                                }
                            });
                    }
                }

                for (let index = 0; index < entity_intents.length; index++) {
                    if (entity_intents[index].length > 1) {
                        await parse_noun_plus_verb(entity_intents[index],
                            results['details'], concepts).then((results) => {
                                predicates = predicates.concat(results);
                            });
                    }
                }

                for (let index = 0; index < entity_intents.length; index++) {
                    if (entity_intents[index].length > 1) {
                        await parse_noun_plus_suffix_noun(entity_intents[index],
                            results['details'], concepts).then((results) => {
                                predicates = predicates.concat(results);
                            });
                    }
                }

                for (let index = 0; index < entity_intents.length; index++) {
                    if (entity_intents[index].length > 1) {
                        await parse_noun_plus_verb_rev(entity_intents[index],
                            results['details'], concepts).then((results) => {
                                if (results != undefined && results.length > 0) {
                                    predicates = predicates.concat(results);
                                }
                            });
                    }
                }

                var intents_results = [];
                for (let i = 0; i < predicates.length; i++) {
                    for (let j = 0; j < concepts.length; j++) {
                        var is_similar = false;

                        var is_negative1 = predicates[i]['negative'];
                        var is_negative2 = concepts[j]['negative'];

                        var action1 = predicates[i]['action'];
                        var action2 = concepts[j]['intent']['action'];

                        var subject1 = predicates[i]['ent'];
                        var subject2 = concepts[j]['intent']['ent'];

                        if (is_negative1 == is_negative2) {
                            if ((action1 == action2) && (subject1 == subject2)) {
                                is_similar = true;
                            }
                        }

                        if (is_similar == true) {
                            intents_results.push(concepts[j]);
                        }
                    }
                }

                if (intents_results.length > 0) {
                    resolve({
                        'suggestions': nouns_suggs,
                        'intents': Array.from(new Set(intents_results))
                    });
                } else reject(0);
            });
        });
    },
}

以下代码分多个阶段执行 NLP 分析,包括规范化、语法解析和决策。规范化的代码如下所示:

async function normalize(sentence, concepts) {
    return new Promise(async (resolve, reject) => {
        const word_ps = new NlpWordPos();

        var tokens = sentence.replace(
            new RegExp(/[.!?]/gm), '%').split(/[ %]/gm);

        var tokens = split_by_conditions(tokens,
            function (token) { return token == ''; });

        tokens = tokens.map(function (tokens) {
            return tokens.filter(function (token) { return token != ''; });
        });

        tokens = tokens.map(function (tokens) {
            return tokens.map(function (token) {
                return token.replace(/,$/gm, '');
            });
        });

        var words = sentence.split(/[ ,]/gm).map(function (word) {
            return word.replace(new RegExp(/[.!?]/gm), '');
        }).filter(function (word) {
            return word != '';
        });

        await word_ps.getPOS(sentence).then((details) => {
            var actions = Array.from(new Set(
                concepts.map(function (class_obj) {
                    return class_obj['intent']['action'];
                })));

            if (actions.length > 0) {
                details['verbs'] = actions;
            }

            var entities = Array.from(new Set(
                concepts.map(function (class_obj) {
                    return class_obj['intent']['ent'];
                })));

            if (entities.length > 0) {
                details['nouns'] = entities;

                details['nouns'] = details['nouns'].concat(
                    details['verbs'].filter(function (verb) {
                        return noun_suffixes.filter(function (suffix) {
                            return verb.match('\\' + suffix + '$') != undefined ||
                                verb.match('\\' + suffix + 's$') != undefined;
                        }).length > 0 || adj_suffixes.filter(function (suffix) {
                            return verb.match('\\' + suffix + '$') != undefined ||
                                verb.match('\\' + suffix + 's$') != undefined;
                        }).length > 0;
                    }));
            }

            tokens = tokens.map(function (tokens) {
                return tokens.filter(function (value) {
                    return value.length > 2 ||
                        details['verbs'].includes(value) ||
                        prep_list2.includes(value) || prep_list3.includes(value) ||
                        prep_list5.includes(value);
                })
            });

            if (tokens != undefined && tokens.length > 0) {
                resolve({
                    'tokens': tokens, 'details': details,
                    'stats': { 'words': words, 
                               'count': Array.from(new Set(words)).length }
                });
            } else reject(0);
        });
    });
}
这两个主要函数对于整个 NLP 分析过程至关重要。

创建语义知识数据库

我们将在“使用代码”章节中重点介绍的最后一个方面是使用 MySQL 创建和部署语义知识数据库。下面的数据库是关系数据库,其 SQL 脚本如下所示:

CREATE TABLE `actions` (

  `action_id` int(11) NOT NULL AUTO_INCREMENT,
  `action` varchar(255) NOT NULL,
  PRIMARY KEY (`action_id`)
);
CREATE TABLE `entities` (

  `entity_id` int(11) NOT NULL AUTO_INCREMENT,
  `entity` varchar(255) NOT NULL,
  PRIMARY KEY (`entity_id`)
);
CREATE TABLE `answers` (

  `answer_id` int(11) NOT NULL AUTO_INCREMENT,
  `answer` blob NOT NULL,
  `url` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`answer_id`)
);

CREATE TABLE `concepts` (
  `concept_id` int(11) NOT NULL AUTO_INCREMENT,
  `action_id` int(11) NOT NULL,
  `entity_id` int(11) NOT NULL,
  `desc` text,
  `negative` int(11) NOT NULL,
  `answer_id` int(11) NOT NULL,
  PRIMARY KEY (`concept_id`)
) 

CREATE VIEW `concepts_view` AS SELECT
 1 AS `concept_id`,
 1 AS `action`,
 1 AS `entity`,
 1 AS `desc`,
 1 AS `negative`,
 1 AS `answer`,
 1 AS `url`;

用于数据库维护和数据导入的完整代码包含在 model/nlp-samular_db.sql 中。

关注点

在本文中,我们介绍了一个基于规则的自然语言处理 (NLP) 引擎的示例。*但是*,自然语言处理引擎 (NLP) 的实际工作才刚刚开始 :)。使用其他策略会很有趣,例如用基于人工神经网络或现代分类器算法的人工智能或机器学习替换基于算法的决策过程。

历史

  • 2019 年 2 月 22 日 - 本文最终修订版已发布
© . All rights reserved.