Java-自然语言处理-全-

Java 自然语言处理(全)

零、前言

自然语言处理 ( NLP )允许你提取任何句子,识别模式、特殊名称、公司名称等等。第二版Java 自然语言处理教你如何在 Java 库的帮助下执行语言分析,同时不断从结果中获得洞察力。

你将从理解 NLP 及其各种概念如何工作开始。掌握了基础知识之后,您将探索用于 NLP 的 Java 中的重要工具和库,比如 CoreNLP、OpenNLP、Neuroph、Mallet 等等。然后,您将开始对不同的输入和任务执行 NLP,比如标记化、模型训练、词性、解析树等等。你将学习统计机器翻译,摘要,对话系统,复杂搜索,监督和非监督的自然语言处理,以及其他东西。到本书结束时,你将会学到更多关于 NLP、神经网络和各种其他 Java 中用于增强 NLP 应用程序性能的训练模型。

这本书是给谁的

如果您是数据分析师、数据科学家或机器学习工程师,想要使用 Java 从语言中提取信息,那么使用 Java 的自然语言处理适合您。Java 编程知识是必需的,虽然对统计学的基本理解是有用的,但不是强制性的。

这本书涵盖的内容

第一章、介绍自然语言处理,解释自然语言处理的重要性和用途。本章中使用的 NLP 技术用简单的例子来说明它们的用法。

第二章,寻找文本的部分,主要关注于标记化。这是更高级的 NLP 任务的第一步。图示了核心 Java 和 Java NLP 标记化 API。

第三章、找句子,证明了句子边界消歧是一项重要的自然语言处理任务。这一步是许多其他下游 NLP 任务的先驱,在这些任务中,文本元素不应该跨句子边界分割。这包括确保所有短语都在一个句子中,并支持词性分析。

第四章、找人和物,涵盖了通常所说的命名实体识别 ( NER )。这项任务涉及识别文本中的人物、地点和类似实体。这项技术是处理查询和搜索的初步步骤。

第五章,检测词性,向你展示如何检测词性,词性是文本的语法元素,比如名词和动词。识别这些元素是确定文本含义和检测文本内部关系的重要一步。

第六章,用特征表示文本,解释如何使用 N 元语法表示文本,并概述它们在揭示上下文中的作用。

第七章,信息检索,处理信息检索中发现的大量数据,并使用各种方法找到相关信息,如布尔检索、字典和容错检索。

第八章,对文本和文档进行分类,证明了对文本进行分类对于垃圾邮件检测和情感分析等任务非常有用。支持这一过程的自然语言处理技术被研究和说明。

第九章,主题建模,讨论了使用包含一些文本的文档进行主题建模的基础。

第十章,使用解析器提取关系,演示解析树。解析树有许多用途,包括信息提取。它保存了关于这些元素之间关系的信息。我们给出了一个实现简单查询的例子来说明这个过程。

第十一章、组合流水线,阐述了围绕解决 NLP 问题的技术组合使用的几个问题。

第十二章,创建聊天机器人,介绍不同类型的聊天机器人,我们也将开发一个简单的预约聊天机器人。

从这本书中获得最大收益

Java SDK 8 用于说明自然语言处理技术。需要各种 NLP APIs,并且可以很容易地下载。IDE 不是必需的,但却是理想的。

下载示例代码文件

你可以从你在www.packtpub.com的账户下载本书的示例代码文件。如果你在其他地方购买了这本书,你可以访问 www.packtpub.com/support 的并注册,让文件直接通过电子邮件发送给你。

您可以按照以下步骤下载代码文件:

  1. www.packtpub.com登录或注册。
  2. 选择支持选项卡。
  3. 点击代码下载和勘误表。
  4. 在搜索框中输入图书名称,然后按照屏幕指示进行操作。

下载文件后,请确保使用最新版本的解压缩或解压文件夹:

  • WinRAR/7-Zip for Windows
  • 适用于 Mac 的 Zipeg/iZip/UnRarX
  • 用于 Linux 的 7-Zip/PeaZip

这本书的代码包也托管在 GitHub 的 https://GitHub . com/packt publishing/Natural-Language-Processing-with-Java-Second-Edition 上。如果代码有更新,它将在现有的 GitHub 库中更新。

我们在也有丰富的书籍和视频目录中的其他代码包。看看他们!

下载彩色图像

我们还提供了一个 PDF 文件,其中有本书中使用的截图/图表的彩色图像。可以在这里下载:www . packtpub . com/sites/default/files/downloads/naturalglanguageprocessingwithjavasecondedition _ color images . pdf

使用的惯例

本书通篇使用了许多文本约定。

CodeInText:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、伪 URL、用户输入和 Twitter 句柄。下面是一个例子:“为了处理文本,我们将使用theSentence变量作为Annotator的输入。”

代码块设置如下:

System.out.println(tagger.tagString("AFAIK she H8 cth!")); 
System.out.println(tagger.tagString( 
    "BTW had a GR8 tym at the party BBIAM."));

任何命令行输入或输出都按如下方式编写:

mallet-2.0.6$ bin/mallet import-dir --input sample-data/web/en --output tutorial.mallet --keep-sequence --remove-stopwords

Bold :表示一个新术语、一个重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词出现在文本中,如下所示。下面是一个例子:“从管理面板中选择系统信息。”

警告或重要提示如下所示。

提示和技巧是这样出现的。

取得联系

我们随时欢迎读者的反馈。

总体反馈:发送电子邮件feedback@packtpub.com,在邮件主题中提及书名。如果您对本书的任何方面有疑问,请发邮件至questions@packtpub.com联系我们。

勘误表:虽然我们已经尽力确保内容的准确性,但错误还是会发生。如果你在这本书里发现了一个错误,请告诉我们,我们将不胜感激。请访问 www.packtpub.com/submit-errata,选择您的图书,点击勘误表提交表格链接,并输入详细信息。

盗版:如果您在互联网上遇到我们作品的任何形式的非法拷贝,如果您能提供我们的地址或网站名称,我们将不胜感激。请通过copyright@packtpub.com联系我们,并提供材料链接。

如果你有兴趣成为一名作家:如果有你擅长的主题,并且你有兴趣写书或投稿,请访问 authors.packtpub.com。

复习

请留下评论。一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢?潜在的读者可以看到并使用您不带偏见的意见来做出购买决定,我们 Packt 可以了解您对我们产品的看法,我们的作者可以看到您对他们的书的反馈。谢谢大家!

更多关于 Packt 的信息,请访问packtpub.com

一、自然语言处理简介

自然语言处理 ( NLP )是一个广泛的话题,专注于使用计算机来分析自然语言。它涉及诸如语音处理、关系提取、文档分类和文本汇总等领域。然而,这些类型的分析是基于一套基本的技术,如标记化、句子检测、分类和提取关系。这些基本技术是本书的重点。我们将从 NLP 的详细讨论开始,研究它为什么重要,并确定应用领域。

有许多工具支持 NLP 任务。我们将重点关注 Java 语言以及各种 Java 应用程序员接口(API)如何支持 NLP。在这一章中,我们将简要介绍主要的 API,包括 Apache 的 OpenNLP、Stanford NLP 库、LingPipe 和 GATE。

接下来是对本书中描述的基本 NLP 技术的讨论。这些技术的本质和使用通过一个 NLP APIs 来展示和说明。这些技术中的许多将使用模型。模型类似于一组用于执行任务(如标记文本)的规则。它们通常由从文件实例化的类来表示。我们将用一个关于如何准备数据来支持 NLP 任务的简短讨论来结束这一章。

NLP 不容易。虽然有些问题可以相对容易地解决,但还有许多其他问题需要使用复杂的技术。我们将努力为 NLP 处理提供一个基础,这样你将能够更好地理解哪种技术适用于给定的问题。

NLP 是一个庞大而复杂的领域。在本书中,我们只能解决其中的一小部分。我们将关注可以使用 Java 实现的核心 NLP 任务。在本书中,我们将使用 Java SE SDK 和其他库(如 OpenNLP 和 Stanford NLP)演示许多 NLP 技术。要使用这些库,需要将特定的 API JAR 文件与使用它们的项目相关联。关于这些库的讨论可以在 NLP 工具的部分的调查中找到,并且包含到这些库的下载链接。本书中的示例是使用 NetBeans 8.0.2 开发的。这些项目要求将 API JAR 文件添加到“项目属性”对话框的“库”类别。

在本章中,我们将了解以下主题:

  • 什么是 NLP?
  • 为什么要用 NLP?
  • 为什么 NLP 这么难?
  • 自然语言处理工具综述
  • 面向 Java 的深度学习
  • 文本处理任务概述
  • 了解 NLP 模型
  • 准备数据

什么是 NLP?

NLP 的正式定义经常包括大意如下的措辞:它是使用计算机科学、人工智能 ( AI )和形式语言学概念来分析自然语言的研究领域。一个不太正式的定义表明,它是一组用于从自然语言源(如网页和文本文档)中获取有意义和有用信息的工具。

有意义和有用意味着它有一些商业价值,尽管它经常用于学术问题。这一点从它对搜索引擎的支持中可以很容易地看出来。使用 NLP 技术处理用户查询,以便生成用户可以使用的结果页面。现代搜索引擎在这方面非常成功。NLP 技术也在自动帮助系统和支持复杂查询系统中得到应用,如 IBM 的 Watson 项目。

当我们使用一种语言时,经常会遇到语法和语义这两个术语。语言的句法指的是控制有效句子结构的规则。例如,英语中常见的句子结构以主语开头,后跟动词,然后是宾语,如“蒂姆击球了”我们不习惯不寻常的句子顺序,如“击球添”虽然英语的句法规则不像计算机语言那样严格,但我们仍然希望句子遵循基本的句法规则。

句子的语义就是它的意思。作为说英语的人,我们理解“蒂姆击球”这句话的意思然而,英语和其他自然语言有时会有歧义,一个句子的意思可能只能根据它的上下文来确定。正如我们将看到的,各种机器学习技术可以用来尝试推导文本的含义。

随着讨论的深入,我们将介绍许多语言学术语,这些术语将帮助我们更好地理解自然语言,并为我们提供一个通用词汇来解释各种 NLP 技术。我们将看到如何将文本分割成单个元素,以及如何对这些元素进行分类。

一般来说,这些方法用于增强应用程序,从而使它们对用户更有价值。NLP 的使用范围可以从相对简单的使用到那些推动今天可能的事情。在本书中,我们将展示一些例子来说明简单的方法,这些方法可能是解决某些问题所需要的全部,这些方法可以使用更高级的库和类来解决复杂的需求。

为什么要用 NLP?

NLP 在各种各样的学科中被用来解决许多不同类型的问题。文本分析是在文本上执行的,范围从用户输入的用于因特网查询的几个单词到需要摘要的多个文档。近年来,我们看到非结构化数据的数量和可用性大幅增长。这采取了博客、推特和各种其他社交媒体的形式。NLP 是分析这类信息的理想工具。

机器学习和文本分析经常被用来增强应用程序的效用。应用领域的简要列表如下:

  • 搜索:识别文本的特定元素。它可以简单到在文档中查找某个名称的出现,或者可能涉及使用同义词和替换拼写/拼写错误来查找与原始搜索字符串接近的条目。
  • 机器翻译:这通常包括将一种自然语言翻译成另一种语言。
  • 总结:段落、文章、文档或文档集合可能需要进行总结。NLP 已经成功地用于这个目的。
  • 命名实体识别 ( NER ):这包括从文本中提取地名、人名和事物名。通常,这与其他 NLP 任务结合使用,如处理查询。
  • 信息分组:这是一个重要的活动,它获取文本数据并创建一组反映文档内容的类别。您可能遇到过许多网站,这些网站根据您的需求组织数据,并在网站的左侧列出了类别。
  • 词性标注 ( 词性):在这个任务中,文本被分成不同的语法元素,比如名词和动词。这对进一步分析文本很有用。
  • 情感分析:人们对电影、书籍和其他产品的感觉和态度可以用这种技术来确定。这有助于提供关于产品感觉如何的自动反馈。
  • 回答问题:当 IBM 的沃森成功赢得一场危险竞赛时,这种类型的处理得到了说明。然而,它的用途并不仅限于赢得比赛,还被用于许多其他领域,包括医学。
  • 语音识别:人类的语音很难分析。这个领域的许多进展都是 NLP 努力的结果。
  • 自然语言生成 ( NLG ):这是从一个数据或知识源,比如数据库,生成文本的过程。它可以自动报告信息,如天气报告,或总结医疗报告。

NLP 任务经常使用不同的机器学习技术。一种常见的方法是从训练模型执行任务开始,验证模型是否正确,然后将模型应用于问题。我们将在了解 NLP 模型部分进一步研究这一过程。

为什么 NLP 这么难?

NLP 不容易。有几个因素使这个过程变得困难。例如,有数百种自然语言,每种语言都有不同的语法规则。当单词的意思依赖于上下文时,它们可能是模糊的。在这里,我们将检查几个更重要的问题领域。

在性格层面,有几个因素需要考虑。例如,需要考虑用于文档的编码方案。可以使用 ASCII、UTF-8、UTF-16 或 Latin-1 等方案对文本进行编码。可能需要考虑其他因素,如文本是否应区分大小写。标点符号和数字可能需要特殊处理。我们有时需要考虑使用表情符号(字符组合和特殊字符图像)、超链接、重复标点(...或-)、文件扩展名以及嵌入句点的用户名。正如我们将在准备数据一节中讨论的,其中许多都是通过预处理文本来处理的。

当我们文本进行标记时,通常意味着我们将文本分解成一系列单词。这些文字被称为令牌。这个过程被称为标记化。当一种语言使用空白字符来描述单词时,这个过程并不太困难。对于像汉语这样的语言来说,这可能相当困难,因为它使用独特的单词符号。

单词和词素可能需要被分配一个词性 ( 词性)标签,以识别它是什么类型的单位。一个语素是有意义的文本的最小划分。前缀和后缀是语素的例子。通常,当我们处理单词时,我们需要考虑同义词、缩写词、首字母缩略词和拼写。

词干是可能需要应用的另一项任务。词干化就是找到一个单词的词干的过程。例如,像这样的单词有词干。搜索引擎经常使用词干来帮助查询。

与词干紧密相关的是词条化的过程。这个过程决定了一个词的基本形式,称为它的引理。比如操作这个词,它的词干是 oper 但它的词条是oper。引理化是一个比词干化更精细的过程,它使用词汇和形态学技术来找到一个引理。在某些情况下,这可以导致更精确的分析。

单词被组合成短语和句子。句子检测可能会有问题,并且不像查找句子末尾的句号那么简单。句点出现在许多地方,包括缩写如 Ms .,以及数字如 12.834。

我们经常需要了解句子中哪些词是名词,哪些是动词。我们经常关心词与词之间的关系。例如,共指解析确定一个或多个句子中某些词之间的关系。考虑下面的句子:

“这座城市很大,但很美。它充满了整个山谷。”

单词 it 是 city 的同指。当一个单词有多个意思时,我们可能需要执行词义消歧 ( WSD )来确定想要的意思。这有时很难做到。例如,“约翰回家了。”家是指房子、城市还是其他单位?它的意思有时可以从使用它的上下文中推断出来。例如,“约翰回家了。它位于一条死胡同的尽头。”

尽管有这些困难,NLP 在大多数情况下都能够很好地完成这些任务,并为许多问题域提供附加值。例如,可以对客户推文进行情感分析,从而为不满意的客户提供可能的免费产品。可以很容易地对医学文档进行总结,以突出相关主题并提高生产率。

总结是产生不同单位的简短描述的过程。这些单元可以包括多个句子、段落、一个文档或多个文档。目的可能是识别那些传达单元意思的句子,确定理解单元的先决条件,或者在这些单元中寻找项目。通常,文本的上下文对完成这项任务很重要。

自然语言处理工具综述

有许多工具支持 NLP。Java SE SDK 提供了其中的一些功能,但是除了最简单的问题之外,这些功能的实用性有限。其他库,如 Apache 的 OpenNLP 和 LingPipe,为 NLP 问题提供了广泛而复杂的支持。

低级 Java 支持包括字符串库,比如StringStringBuilderStringBuffer。这些类拥有执行搜索、匹配和文本替换的方法。正则表达式使用特殊编码来匹配子字符串。Java 提供了一套丰富的使用正则表达式的技术。

如前所述,记号赋予器用于将文本分割成单独的元素。Java 通过以下方式为标记器提供支持:

  • String类的split方法
  • StreamTokenizer
  • StringTokenizer

也存在许多用于 Java 的 NLP 库/API。下表列出了部分基于 Java 的 NLP APIs。其中大部分都是开源的。此外,还有许多商业 API 可供使用。我们将关注开源 API:

| API | 网址 |
| Apertium | www.apertium.org/ |
| 文本工程的通用体系结构 | gate.ac.uk/ |
| 基于学习的 Java | github.com/CogComp/lbjava |
| 灵管 | alias-i.com/lingpipe/ |
| 木槌 | mallet.cs.umass.edu/ |
| 蒙特林瓜 | web.media.mit.edu/~hugo/montylingua/ |
| Apache OpenNLP | opennlp.apache.org/ |
| UIMA | uima.apache.org/ |
| 斯坦福解析器 | nlp.stanford.edu/software |
| Apache Lucene 核心 | lucene.apache.org/core/ |
| 雪球 | snowballstem.org/ |

这些 NLP 任务中的许多被组合起来形成一个管道。流水线由各种 NLP 任务组成,这些任务被集成到一系列步骤中以实现处理目标。支持管道的框架的例子有文本工程通用架构 ( )和阿帕奇 UIMA。

在下一节中,我们将更深入地讨论几个 NLP APIs。本文将简要概述它们的功能,并列出每个 API 的有用链接。

Apache OpenNLP

Apache OpenNLP 项目是一个基于机器学习的工具包,用于处理自然语言文本;它解决了常见的 NLP 任务,并将贯穿本书。它由几个组件组成,这些组件执行特定的任务,允许对模型进行训练,并支持对模型进行测试。OpenNLP 使用的一般方法是从文件中实例化一个支持任务的模型,然后针对该模型执行方法来执行任务。

例如,在下面的序列中,我们将标记一个简单的字符串。为了正确执行该代码,它必须处理FileNotFoundExceptionIOException异常。我们使用 try-with-resource 块通过en-token.bin文件打开一个FileInputStream实例。该文件包含一个使用英文文本训练的模型:

try (InputStream is = new FileInputStream( 
        new File(getModelDir(), "en-token.bin"))){ 
    // Insert code to tokenize the text 
} catch (FileNotFoundException ex) { 
    ... 
} catch (IOException ex) { 
    ... 
} 

然后使用这个文件在try块中创建一个TokenizerModel类的实例。接下来,我们创建一个Tokenizer类的实例,如下所示:

TokenizerModel model = new TokenizerModel(is); 
Tokenizer tokenizer = new TokenizerME(model); 

然后应用tokenize方法,它的参数是要标记的文本。该方法返回一组String对象:

String tokens[] = tokenizer.tokenize("He lives at 1511 W." 
  + "Randolph."); 

for-each 语句显示标记,如下所示。左括号和右括号用于清楚地识别标记:

for (String a : tokens) { 
  System.out.print("[" + a + "] "); 
} 
System.out.println(); 

当我们执行这个命令时,我们将得到以下输出:

[He] [lives] [at] [1511] [W.] [Randolph] [.]  

在这种情况下,标记器识别出W.是一个缩写,最后一个句点是一个单独的标记,用来区分句子的结尾。

我们将在本书的许多例子中使用 OpenNLP API。OpenNLP 链接列于下表 :

| OpenNLP | 网站 |
| 主页 | 【https://opennlp.apache.org/】 |
| 证明文件 | |
| Javadoc | 【http://nlp.stanford.edu/nlp/javadoc/javanlp/index.html】 |
| [计] 下载 | 【https://opennlp.apache.org/cgi-bin/download.cgi】 |
| 维基网 | CWI ki . Apache . org/confluence/display/open NLP/Index % 3b 408 c 73729 acccdd 071d 9 EC 354 fc 54 |

斯坦福 NLP

斯坦福大学自然语言处理小组进行自然语言处理研究,并为自然语言处理任务提供工具。斯坦福 CoreNLP 就是这些工具集之一。此外,还有其他工具集,如斯坦福解析器、斯坦福 POS 标记器和斯坦福分类器。斯坦福工具支持英语和汉语以及基本的 NLP 任务,包括标记化和名称实体识别。

这些工具是在完整的 GPL 下发布的,但是它不允许在商业应用程序中使用它们,尽管商业许可证是可用的。该 API 组织良好,支持核心的 NLP 功能。

斯坦福小组支持几种标记化方法。我们将使用PTBTokenizer类来说明这个 NLP 库的使用。这里演示的构造函数使用一个Reader对象、一个LexedTokenFactory<T>参数和一个字符串来指定要使用几个选项中的哪一个。

LexedTokenFactory是由CoreLabelTokenFactoryWordTokenFactory类实现的接口。前一个类支持保留标记的开始和结束字符位置,而后一个类只是将标记作为不带任何位置信息的字符串返回。默认情况下使用WordTokenFactory类。

在下面的例子中使用了CoreLabelTokenFactory类。使用字符串创建一个StringReader。最后一个参数用于选项参数,在本例中是nullIterator接口由PTBTokenizer类实现,允许我们使用hasNextnext方法来显示令牌:

PTBTokenizer ptb = new PTBTokenizer( 
new StringReader("He lives at 1511 W. Randolph."), 
new CoreLabelTokenFactory(), null); 
while (ptb.hasNext()) { 
  System.out.println(ptb.next()); 
} 

输出如下所示:

He
lives
at
1511
W.
Randolph
.  

我们将在本书中广泛使用斯坦福大学的 NLP 图书馆。下表列出了斯坦福大学的链接。每个发行版中都有文档和下载链接:

| 斯坦福 NLP | 网站 |
| 主页 | 【http://nlp.stanford.edu/index.shtml】 |
| -好的 | 【http://nlp.stanford.edu/software/corenlp.shtml#Download】 |
| 句法分析程序 | 【http://nlp.stanford.edu/software/lex-parser.shtml】 |
| POS 标签 | 【http://nlp.stanford.edu/software/tagger.shtml】 |
| Java-NLP-用户邮件列表 | 【https://mailman.stanford.edu/mailman/listinfo/java-nlp-user】 |

灵管

LingPipe 由一组执行常见 NLP 任务的工具组成。它支持模型训练和测试。该工具有免版税版本和授权版本。免费版本的生产使用是有限的。

为了演示 LingPipe 的用法,我们将使用Tokenizer类来说明如何使用它来标记文本。首先声明两个列表,一个保存标记,另一个保存空白:

List<String> tokenList = new ArrayList<>(); 
List<String> whiteList = new ArrayList<>(); 

You can download the example code files for all Packt books you have purchased from your account at www.packtpub.com. If you purchased this book elsewhere, you can visit www.packtpub.com/support and register to have the files emailed directly to you.

接下来,声明一个字符串来保存要标记的文本:

String text = "A sample sentence processed \nby \tthe " + 
    "LingPipe tokenizer."; 

现在,创建一个Tokenizer类的实例。如下面的代码块所示,一个静态的tokenizer方法被用来创建一个基于Indo-European factory类的Tokenizer类的实例:

Tokenizer tokenizer = IndoEuropeanTokenizerFactory.INSTANCE. 
tokenizer(text.toCharArray(), 0, text.length()); 

然后使用该类的tokenize方法来填充这两个列表:

tokenizer.tokenize(tokenList, whiteList); 

使用 for-each 语句显示标记:

for(String element : tokenList) { 
  System.out.print(element + " "); 
} 
System.out.println(); 

此示例的输出如下所示:

A sample sentence processed by the LingPipe tokenizer

下表列出了 LingPipe 链接:

| 灵管 | 网站 |
| 主页 | 【http://alias-i.com/lingpipe/index.html】 |
| 教程 | 【http://alias-i.com/lingpipe/demos/tutorial/read-me.html】 |
| JavaDocs | 【http://alias-i.com/lingpipe/docs/api/index.html】 |
| [计] 下载 | 【http://alias-i.com/lingpipe/web/install.html】 |
| 核心 | 【http://alias-i.com/lingpipe/web/download.html】 |
| 模型 | 【http://alias-i.com/lingpipe/web/models.html】 |

大门

GATE 是一套用 Java 编写的工具,由英国谢菲尔德大学开发。它支持许多 NLP 任务和语言。它也可以用作 NLP 处理的管道。它支持一个 API 和 GATE Developer,GATE Developer 是一个显示文本和注释的文档查看器。这对于使用突出显示的注释检查文档非常有用。盖茨米伊美,一个索引和搜索各种来源产生的文本的工具,也是可用的。使用 GATE 完成许多 NLP 任务需要一些代码。GATE Embedded 用于将 GATE 功能直接嵌入到代码中。下表列出了有用的门链接:

| Gate | 网站 |
| 主页 | 【https://gate.ac.uk/】 |
| 证明文件 | 【https://gate.ac.uk/documentation.html】 |
| JavaDocs | 【http://jenkins.gate.ac.uk/job/GATE-Nightly/javadoc/】 |
| [计] 下载 | 【https://gate.ac.uk/download/】 |
| 维基网 | 【http://gatewiki.sf.net/】 |

TwitIE 是一个开源的信息提取管道。它包含以下内容:

  • 社交媒体数据-语言识别
  • Twitter 标记器,用于处理表情符号、用户名、URL 等等
  • POS 标签
  • 文本规范化

它是 GATE Twitter 插件的一部分。下表列出了所需的链接:

| 定理 | 网站 |
| 主页 | gate.ac.uk/wiki/twitie.html |
| 证明文件 | gate . AC . uk/sale/ranlp 2013/tw itie/tw itie-ranlp 2013 . pdf?m=1 |

UIMA

结构化信息标准促进组织是一个专注于面向信息的商业技术的联盟。它开发了非结构化信息管理架构 ( UIMA )标准作为 NLP 管道的框架。它得到了阿帕奇 UIMA 公司的支持。

尽管它支持管道创建,但它也描述了一系列用于文本分析的设计模式、数据表示和用户角色。下表列出了 UIMA 链接:

| 阿帕奇 UIMA | 网站 |
| 主页 | 【https://uima.apache.org/】 |
| 证明文件 | 【https://uima.apache.org/documentation.html】 |
| JavaDocs | 【https://uima.apache.org/d/uimaj-2.6.0/apidocs/index.html】 |
| [计] 下载 | 【https://uima.apache.org/downloads.cgi】 |
| 维基网 | 【https://cwiki.apache.org/confluence/display/UIMA/Index】 |

Apache Lucene 核心

Apache Lucene Core 是用 Java 编写的全功能文本搜索引擎的开源库。它使用标记化将文本分成小块来索引元素。它还为分析目的提供了标记化前和标记化后的选项。它支持词干化、过滤、文本规范化和标记化后的同义词扩展。使用时,它创建一个目录和索引文件,并可用于搜索内容。它不能作为 NLP 工具包,但它提供了处理文本和高级字符串操作的强大工具。它提供了一个免费的搜索引擎。下表列出了 Apache Lucene 的重要链接:

| 阿帕奇 Lucene | 网站 |
| 主页 | lucene.apache.org/ |
| 证明文件 | lucene.apache.org/core/documentation.html |
| JavaDocs | lucene.apache.org/core/7_3_0/core/index.html |
| [计] 下载 | Lucene . Apache . org/core/mirrors-core-latest-redir . html? |

面向 Java 的深度学习

深度学习是机器学习的一部分,是人工智能的一个子集。深度学习的灵感来自人类大脑在其生物形式中的功能。它使用神经元等术语来创建神经网络,这可以是监督或无监督学习的一部分。深度学习概念广泛应用于计算机视觉、语音识别、NLP、社交网络分析和过滤、欺诈检测、预测等领域。深度学习在 2010 年的图像处理领域证明了自己,当时它在一次图像网络竞赛中胜过了所有其他人,现在它已经开始在 NLP 中显示出有希望的结果。深度学习表现非常好的一些领域包括命名实体识别 ( NER )、情感分析、词性标注、机器翻译、文本分类、字幕生成和问题回答。

这段精彩阅读可以在戈德堡在 https://arxiv.org/abs/1510.00726 的作品中找到。有各种工具和库可用于深度学习。下面是一个库列表,可以帮助您入门:

  • deep learning 4j(【https://deeplearning4j.org/】??):这是一个面向 JVM 的开源、分布式深度学习库。
  • Weka(www.cs.waikato.ac.nz/ml/weka/index.html):它是 Java 中的一款数据挖掘软件,拥有一系列支持预处理、预测、回归、聚类、关联规则和可视化的机器学习算法。
  • 海量在线分析(MOA)(【https://moa.cms.waikato.ac.nz/】??):用于实时流。支持机器学习和数据挖掘。
  • 由索引结构支持的 KDD 应用程序开发环境(??)(埃尔基)(elki-project.github.io/):这是一个数据挖掘软件,专注于研究算法,强调聚类分析和离群点检测中的非监督方法。
  • Neuroph(【http://neuroph.sourceforge.net/index.html】??):它是一个轻量级的 Java 神经网络框架,用于开发 Apache Licensee 2.0 许可的神经网络架构。它还支持用于创建和训练数据集的 GUI 工具。
  • aero solve(【http://airbnb.io/aerosolve/】??):这是一个为人类设计的机器学习包,就像在网上看到的那样。它由 Airbnb 开发,更倾向于机器学习。

你可以在 GitHub(github.com/search?l=Java&amp;q =深度+学习&amp;储存库& amp。utf8=%E2%9C%93 )用于深度学习和 Java。

文本处理任务概述

尽管有许多 NLP 任务可以执行,我们将只关注这些任务的一个子集。此处对这些任务进行了简要概述,这也反映在以下章节中:

  • 第二章,寻找正文部分
  • 第三章,找句子
  • 第四章、寻找人和事
  • 第五章、检测词性
  • 第八章、对文本和文件进行分类
  • 第十章,使用解析器提取关系
  • 第十一章、组合方式

这些任务中有许多是与其他任务一起使用来实现目标的。我们将在阅读本书的过程中看到这一点。例如,在许多其他任务中,标记化经常被用作初始步骤。这是一个基本的步骤。

查找部分文本

文本可以分解成许多不同类型的元素,如单词、句子和段落。有几种方法对这些元素进行分类。当我们在本书中提到部分文本时,我们指的是单词,有时称为标记。形态学是研究词的结构。在我们对自然语言处理的探索中,我们将使用一些形态学术语。但是,对单词进行分类的方法有很多种,包括以下几种:

  • 简单词:这些是一个词的意思的共同内涵,包括这句话里的 17 个词。
  • 语素:这是一个词有意义的最小单位。例如,在有界一词中,有界被认为是一个语素。语素还包括后缀、 ed 等部分。
  • 前缀/后缀:在词根之前或之后。例如,在单词 graduate 中, ation 是基于单词 graduate 的后缀。
  • 同义词:这是一个和另一个单词意思相同的单词。像 small 和 tiny 这样的词可以被认为是同义词。解决这个问题需要词义消歧。
  • 缩写:这些缩写词缩短了一个单词的用法。我们不用史密斯先生,而是用史密斯先生。
  • 首字母缩写词:它们被广泛应用于许多领域,包括计算机科学。他们用字母组合来表示短语,如 FORTRAN 的公式翻译。它们可以是递归的,比如 GNU。当然,我们将继续使用的是 NLP。
  • 缩写:我们会发现这些对于常用的单词组合很有用,比如这个句子的第一个单词。
  • 数字:通常只用数字的专用词。然而,更复杂的版本可以包含一个句点和一个特殊字符,以反映科学记数法或特定基数的数字。

识别这些部分对其他 NLP 任务很有用。例如,要确定一个句子的边界,就必须将它拆分开来,并确定哪些元素终止了一个句子。

将文本分开的过程称为标记化。结果是令牌流。决定元素拆分位置的文本元素称为分隔符。对于大多数英文文本,空格被用作分隔符。这种类型的分隔符通常包括空格、制表符和换行符。

标记化可以简单也可以复杂。这里,我们将使用String class' split方法演示一个简单的标记化。首先,声明一个字符串来保存要标记的文本:

String text = "Mr. Smith went to 123 Washington avenue."; 

split方法使用一个正则表达式参数来指定如何拆分文本。在下面的代码序列中,它的参数是\\s+字符串。这指定将使用一个或多个空格作为分隔符:

String tokens[] = text.split("\\s+"); 

for-each 语句用于显示结果标记:

for(String token : tokens) { 
  System.out.println(token); 
} 

执行时,输出将如下所示:

Mr.
Smith
went
to
123
Washington
avenue.  

在第二章、寻找部分文本中,我们将深入探讨标记化过程。

寻找句子

我们倾向于认为识别句子的过程很简单。在英语中,我们寻找终止字符,如句号、问号或感叹号。然而,正如我们将在第三章、寻找句子中看到的,这并不总是那么简单。使寻找句末变得更加困难的因素包括在诸如史密斯博士204 SW 这样的短语中使用嵌入式句号。公园街

这个过程也叫句界消歧()。这在英语中是一个比汉语或日语更严重的问题,因为汉语或日语有明确的句子分隔符。

**识别句子是有用的,原因有很多。一些自然语言处理任务,比如词性标注和实体提取,是针对单个句子的。问答应用程序也需要识别单个句子。为了使这些过程正确工作,必须正确确定句子边界。

下面的例子演示了如何使用 Stanford DocumentPreprocessor类找到句子。这个类将生成一个基于简单文本或 XML 文档的句子列表。该类实现了Iterable接口,允许它在 for-each 语句中轻松使用。

首先声明一个包含以下句子的字符串:

String paragraph = "The first sentence. The second sentence."; 

基于字符串创建一个StringReader对象。这个类支持简单的read类型方法,并被用作DocumentPreprocessor构造函数的参数:

Reader reader = new StringReader(paragraph); 
DocumentPreprocessor documentPreprocessor =  
new DocumentPreprocessor(reader); 

DocumentPreprocessor对象现在将保存段落中的句子。在下面的语句中,创建了一个字符串列表,用于保存找到的句子:

List<String> sentenceList = new LinkedList<String>(); 

然后,documentPreprocessor对象的每个元素被处理,并由一系列的HasWord对象组成,如下面的代码块所示。HasWord元素是代表一个单词的对象。StringBuilder的一个实例被用来构造句子,其中hasWordList元素的每个元素都被添加到列表中。句子完成后,将被添加到sentenceList列表中:

for (List<HasWord> element : documentPreprocessor) { 
  StringBuilder sentence = new StringBuilder(); 
  List<HasWord> hasWordList = element; 
  for (HasWord token : hasWordList) { 
      sentence.append(token).append(" "); 
  } 
  sentenceList.add(sentence.toString()); 
} 

然后使用 for-each 语句来显示句子:

for (String sentence : sentenceList) { 
  System.out.println(sentence); 
} 

输出如下所示:

The first sentence . 
The second sentence .   

SBD 过程在第三章、中有详细介绍。

特征工程

特征工程在开发自然语言处理应用程序中起着重要的作用;这对于机器学习非常重要,尤其是在基于预测的模型中。它是使用领域知识将原始数据转换为特征的过程,以便机器学习算法能够工作。特征为我们提供了原始数据的更集中的视图。一旦识别出特征,就进行特征选择以降低数据的维度。当处理原始数据时,检测到模式或特征,但是这可能不足以增强训练数据集。工程特征通过提供有助于区分数据模式的相关信息来增强训练。在原始数据集或提取的要素中,新要素可能不会被捕获或不明显。因此,特征工程是一门艺术,需要领域专业知识。它仍然是人类的手艺,是机器还不擅长的东西。

第六章、用特征表示文本,将展示如何将文本文档表示为对文本文档不起作用的传统特征。

寻找人和事物

搜索引擎很好地满足了大多数用户的需求。人们经常使用搜索引擎来查找商业地址或电影放映时间。文字处理器可以执行简单的搜索来定位文本中的特定单词或短语。然而,当我们需要考虑其他因素时,这项任务会变得更加复杂,例如是否应该使用同义词,或者我们是否有兴趣找到与某个主题密切相关的东西。

例如,假设我们访问一个网站,因为我们有兴趣购买一台新的笔记本电脑。毕竟,谁不需要新的笔记本电脑呢?当您访问该网站时,搜索引擎将用于查找具备您正在寻找的功能的笔记本电脑。这种搜索通常是根据以前对供应商信息的分析进行的。这种分析通常需要对文本进行处理,以便获得最终可以呈现给客户的有用信息。

呈现可以是小平面的形式。这些通常显示在网页的左侧。例如,笔记本电脑的方面可能包括超极本、Chromebook 或硬盘大小等类别。下面的截图说明了这一点,它是亚马逊网页的一部分:

有些搜索可能非常简单。例如,String类和相关类都有方法,比如indexOflastIndexOf方法,可以找到String类的出现。在下面的简单例子中,目标字符串出现的索引由indexOf方法返回:

String text = "Mr. Smith went to 123 Washington avenue."; 
String target = "Washington"; 
int index = text.indexOf(target); 
System.out.println(index); 

此序列的输出如下所示:

22

这种方法只对最简单的问题有用。

当搜索文本时,一种常见的技术是使用一种称为倒排索引的数据结构。这个过程包括对文本进行标记,并识别文本中感兴趣的术语及其位置。然后,术语及其位置存储在倒排索引中。当对术语进行搜索时,在倒排索引中查找术语,并检索位置信息。这比每次需要时在文档中搜索术语要快。这种数据结构经常用于数据库、信息检索系统和搜索引擎。

更复杂的搜索可能涉及对诸如“波士顿有哪些好餐馆?”为了回答这个查询,我们可能需要执行实体识别/解析来识别查询中的重要术语,执行语义分析来确定查询的含义,进行搜索,然后对候选响应进行排序。

为了说明查找姓名的过程,我们结合使用了一个标记器和 OpenNLP TokenNameFinderModel类来查找文本中的姓名。由于这个技术可能会抛出IOException,我们将使用一个try...catch块来处理它。声明这个块和包含句子的字符串数组,如下所示:

try { 
    String[] sentences = { 
         "Tim was a good neighbor. Perhaps not as good a Bob " +  
        "Haywood, but still pretty good. Of course Mr. Adam " +  
        "took the cake!"}; 
    // Insert code to find the names here 
  } catch (IOException ex) { 
    ex.printStackTrace(); 
}

在对句子进行处理之前,我们需要对文本进行分词。使用Tokenizer类设置记号赋予器,如下所示:

Tokenizer tokenizer = SimpleTokenizer.INSTANCE; 

我们将需要使用一个模型来检测句子。这是为了避免对可能跨越句子边界的术语进行分组。我们将基于在en-ner-person.bin文件中找到的模型使用TokenNameFinderModel类。从这个文件中创建一个TokenNameFinderModel的实例,如下所示:

TokenNameFinderModel model = new TokenNameFinderModel( 
new File("C:\\OpenNLP Models", "en-ner-person.bin")); 

NameFinderME类将执行查找名称的实际任务。这个类的一个实例是使用TokenNameFinderModel实例创建的,如下所示:

NameFinderME finder = new NameFinderME(model); 

使用 for-each 语句处理每个句子,如下面的代码序列所示。tokenize方法将把句子分割成记号,而find方法返回一组Span对象。这些对象存储由find方法标识的名称的起始和结束索引:

for (String sentence : sentences) { 
    String[] tokens = tokenizer.tokenize(sentence); 
    Span[] nameSpans = finder.find(tokens); 
    System.out.println(Arrays.toString( 
    Span.spansToStrings(nameSpans, tokens))); 
} 

执行时,它将生成以下输出:

[Tim, Bob Haywood, Adam]  

第四章、寻找人和物的主要焦点是名字识别。

检测词类

另一种对文本各部分进行分类的方法是在句子层面。一个句子可以根据类别(如名词、动词、副词和介词)分解成单个单词或单词组合。我们大多数人在学校里学会了如何做这件事。我们还学习了不要用介词结束一个句子,这与我们在本段第二句中所做的相反。

检测词性在其他任务中很有用,例如提取关系和确定文本的含义。确定这些关系被称为解析。POS 处理对于提高发送到管道其他元素的数据质量非常有用。

POS 流程的内部可能很复杂。幸运的是,大多数复杂性对我们来说是隐藏的,封装在类和方法中。我们将使用几个 OpenNLP 类来说明这个过程。我们需要一个模型来检测 POS。将使用在en-pos-maxent.bin文件中找到的模型来使用和实例化POSModel类,如下所示:

POSModel model = new POSModelLoader().load( 
    new File("../OpenNLP Models/" "en-pos-maxent.bin")); 

POSTaggerME类用于执行实际的标记。基于以前的模型创建该类的实例,如下所示:

POSTaggerME tagger = new POSTaggerME(model); 

接下来,声明包含要处理的文本的字符串:

String sentence = "POS processing is useful for enhancing the "  
   + "quality of data sent to other elements of a pipeline."; 

这里,我们将使用WhitespaceTokenizer来标记文本:

String tokens[] = WhitespaceTokenizer.INSTANCE.tokenize(sentence); 

然后使用tag方法来查找那些将结果
存储在字符串数组中的词性:

String[] tags = tagger.tag(tokens); 

然后将显示令牌及其相应的标签:

for(int i=0; i<tokens.length; i++) { 
    System.out.print(tokens[i] + "[" + tags[i] + "] "); 
} 

执行时,将产生以下输出:

    POS[NNP] processing[NN] is[VBZ] useful[JJ] for[IN] enhancing[VBG] the[DT] quality[NN] of[IN] data[NNS] sent[VBN] to[TO] other[JJ] elements[NNS] of[IN] a[DT] pipeline.[NN]  

每个标记后面都有一个缩写,包含在括号内,表示它的位置。例如,NNP 表示它是专有名词。这些缩写将在第五章、检测词类中涉及,专门深入探讨这个话题。

文本和文档分类

分类涉及给文本或文档中的信息分配标签。当过程发生时,这些标签可能是已知的,也可能是未知的。当标签已知时,该过程称为分类。当标签未知时,该过程被称为聚类

在自然语言处理中感兴趣的还有分类的过程。这是将一些文本元素分配到几个可能的组之一的过程。例如,军用飞机可以分为战斗机、轰炸机、侦察机、运输机或救援飞机。

分类器可以按照它们产生的输出类型来组织。这可以是二进制的,产生是/否输出。这种类型通常用于支持垃圾邮件过滤器。其他类型将导致多个可能的类别。

与许多其他 NLP 任务相比,分类更像是一个过程。它包括我们将在了解 NLP 模型部分讨论的步骤。由于这个过程的长度,我们将不在这里举例说明。在第八章、对文本和文档进行分类中,我们将研究分类过程并提供一个详细的例子。

提取关系

关系抽取识别文本中存在的关系。例如,用“生活的意义和目的显而易见”这个句子,我们知道这个句子的主题是“生活的意义和目的”它与暗示“显而易见”的最后一个短语有关。

人类可以很好地确定事物之间的关系,至少在高层次上。确定深层关系可能更加困难。使用计算机提取关系也很有挑战性。然而,计算机可以处理大型数据集,以找到对人类来说不明显或在合理的时间内无法完成的关系。

许多关系都是可能的。这些包括一些关系,比如某物的位置,两个人之间的关系,系统的组成部分,以及谁是负责人。关系提取对许多任务都很有用,包括建立知识库、进行趋势分析、收集情报和进行产品搜索。寻找关系有时被称为文本分析

我们可以使用几种技术来执行关系提取。这些在第十章、使用解析器提取关系中有更详细的介绍。这里,我们将使用斯坦福 NLP StanfordCoreNLP类说明一种识别句子中关系的技术。这个类支持一个管道,在这个管道中标注器被指定并应用于文本。注释器可以被认为是要执行的操作。当创建类的实例时,使用在java.util包中找到的Properties对象添加注释器。

首先,创建一个Properties类的实例。然后,按如下方式分配注释者:

Properties properties = new Properties();         
properties.put("annotators", "tokenize, ssplit, parse"); 

我们使用了三个注释器,它们指定了要执行的操作。在这种情况下,这些是解析文本所需的最低要求。第一个是tokenize,将对文本进行标记。ssplit注释器将标记分割成句子。最后一个注释者parse,执行语法分析,对文本进行解析。

接下来,使用属性的引用变量创建一个StanfordCoreNLP类的实例:

StanfordCoreNLP pipeline = new StanfordCoreNLP(properties); 

然后,创建一个Annotation实例,它使用文本作为它的参数:

Annotation annotation = new Annotation( 
    "The meaning and purpose of life is plain to see."); 

pipeline对象应用annotate方法来处理annotation对象。最后,使用prettyPrint方法显示处理结果:

pipeline.annotate(annotation); 
pipeline.prettyPrint(annotation, System.out); 

这段代码的输出如下所示:

    Sentence #1 (11 tokens):
    The meaning and purpose of life is plain to see.
    [Text=The CharacterOffsetBegin=0 CharacterOffsetEnd=3 PartOfSpeech=DT] [Text=meaning CharacterOffsetBegin=4 CharacterOffsetEnd=11 PartOfSpeech=NN] [Text=and CharacterOffsetBegin=12 CharacterOffsetEnd=15 PartOfSpeech=CC] [Text=purpose CharacterOffsetBegin=16 CharacterOffsetEnd=23 PartOfSpeech=NN] [Text=of CharacterOffsetBegin=24 CharacterOffsetEnd=26 PartOfSpeech=IN] [Text=life CharacterOffsetBegin=27 CharacterOffsetEnd=31 PartOfSpeech=NN] [Text=is CharacterOffsetBegin=32 CharacterOffsetEnd=34 PartOfSpeech=VBZ] [Text=plain CharacterOffsetBegin=35 CharacterOffsetEnd=40 PartOfSpeech=JJ] [Text=to CharacterOffsetBegin=41 CharacterOffsetEnd=43 PartOfSpeech=TO] [Text=see CharacterOffsetBegin=44 CharacterOffsetEnd=47 PartOfSpeech=VB] [Text=. CharacterOffsetBegin=47 CharacterOffsetEnd=48 PartOfSpeech=.] 
    (ROOT
      (S
        (NP
          (NP (DT The) (NN meaning)
            (CC and)
            (NN purpose))
          (PP (IN of)
            (NP (NN life))))
        (VP (VBZ is)
          (ADJP (JJ plain)
            (S
              (VP (TO to)
                (VP (VB see))))))
        (. .)))

    root(ROOT-0, plain-8)
    det(meaning-2, The-1)
    nsubj(plain-8, meaning-2)
    conj_and(meaning-2, purpose-4)
    prep_of(meaning-2, life-6)
    cop(plain-8, is-7)
    aux(see-10, to-9)
    xcomp(plain-8, see-10)

输出的第一部分显示文本以及令牌和位置。接下来是树状结构,显示句子的组织。最后一部分显示了语法层面上的元素之间的关系。考虑下面的例子:

prep_of(meaning-2, life-6)  

这显示了介词的是如何被用来将表示和表示联系起来的。这些信息对于许多文本简化任务非常有用。

使用综合方法

如前所述,NLP 问题通常涉及使用一个以上的基本 NLP 任务。这些经常在一个管道中组合以获得期望的结果。在前一节中,我们看到了管道的一个用途,提取关系

大多数 NLP 解决方案将使用管道。我们将在第十一章、组合管道中提供几个管道的例子。

了解 NLP 模型

不管执行的是 NLP 任务还是使用的 NLP 工具集,它们都有几个共同的步骤。在本节中,我们将介绍这些步骤。当你阅读本书中介绍的章节和技术时,你会看到这些步骤会有细微的变化。现在对它们有一个很好的理解将会减轻学习技术的任务。

基本步骤包括以下内容:

  1. 确定任务
  2. 选择模型
  3. 构建和训练模型
  4. 验证模型
  5. 使用模型

我们将在下面的小节中讨论这些步骤。

确定任务

理解需要解决的问题很重要。基于这种理解,可以设计出由一系列步骤组成的解决方案。这些步骤中的每一步都将使用 NLP 任务。

例如,假设我们想要回答一个查询,比如“谁是巴黎的市长?”我们需要将查询解析到 POS 中,确定问题的性质、问题的限定元素,并最终使用通过其他 NLP 任务创建的知识库来回答问题。

其他问题可能不太复杂。我们可能只需要将文本分解成组件,这样文本就可以与类别相关联。例如,可以分析供应商的产品描述来确定潜在的产品类别。对汽车描述的分析将允许它被分类为轿车、跑车、SUV 或小型车。

一旦你对 NLP 任务有了概念,你就能更好地将它们与你试图解决的问题相匹配。

选择模型

我们将要研究的许多任务都是基于模型的。例如,如果我们需要将一个文档拆分成句子,我们需要一个算法来完成这项工作。然而,即使是最好的句子边界检测技术也很难每次都做到正确。这导致了模型的发展,该模型检查文本的元素,然后使用该信息来确定断句发生的位置。

正确的模型可能取决于正在处理的文本的性质。在确定历史文献的句子结尾方面做得很好的模型在应用于医学文本时可能不太好。

已经创建了许多模型,我们可以用它们来完成手头的 NLP 任务。根据需要解决的问题,我们可以做出明智的决策,确定哪种模型是最好的。在某些情况下,我们可能需要训练一个新的模型。这些决策经常涉及准确性和速度之间的权衡。理解问题域和所需的结果质量使我们能够选择合适的模型。

构建和训练模型

训练模型是针对一组数据执行算法、制定模型,然后验证模型的过程。我们可能会遇到这样的情况,需要处理的文本与我们以前看到和使用的文本有很大不同。例如,使用新闻文本训练的模型在处理推文时可能效果不佳。这可能意味着现有的模型将无法很好地处理这些新数据。当这种情况出现时,我们将需要训练一个新的模型。

为了训练一个模型,我们通常会使用以我们知道正确答案的方式标记的数据。例如,如果我们处理的是词性标注,那么数据中会标记出词性元素(比如名词和动词)。当模型被训练时,它将使用这些信息来创建模型。这个数据集被称为语料库

验证模型

一旦创建了模型,我们需要用一个样本集来验证它。典型的验证方法是使用已知正确响应的样本集。当模型与这些数据一起使用时,我们能够将其结果与已知的好结果进行比较,并评估模型的质量。通常,只有一部分语料库用于训练,而另一部分用于验证。

使用模型

使用模型只是将模型应用于手头的问题。细节取决于所使用的模型。这在之前的几个演示中有所说明,比如在检测词性部分,我们使用了包含在en-pos-maxent.bin文件中的 POS 模型。

准备数据

自然语言处理中的一个重要步骤是寻找和准备要处理的数据。这包括用于培训目的的数据和需要处理的数据。有几个因素需要考虑。在这里,我们将关注 Java 为处理字符提供的支持。

我们需要考虑角色是如何表现的。虽然我们将主要处理英语文本,但其他语言也存在独特的问题。不仅一个字符的编码方式不同,阅读文本的顺序也会不同。例如,日语从右向左按列排列文本。

也有许多可能的编码。这些语言包括 ASCII、拉丁语和 Unicode 等等。下表列出了更完整的列表。尤其是 Unicode,它是一种复杂而广泛的编码方案:

| 编码 | 描述 |
| 美国信息交换标准代码 | 使用 128 (0-127)个值的字符编码。 |
| 拉丁语 | 有几个拉丁变体使用 256 个值。它们包括变音符号和其他字符的各种组合。不同版本的拉丁语被用来称呼不同的印欧语言,如土耳其语和世界语。 |
| Big5 | 寻址中文字符集的双字节编码。 |
| 统一码 | Unicode 有三种编码:UTF-8、UTF-16 和 UTF-32。它们分别使用 1、2 和 4 个字节。这种编码能够代表当今存在的所有已知语言,包括较新的语言,如克林贡语和精灵语。 |

Java 能够处理这些编码方案。javac可执行文件的-encoding命令行选项用于指定要使用的编码方案。在下面的命令行中,指定了Big5编码方案:

javac -encoding Big5

使用原始的char数据类型、Character类以及其他几个类和接口来支持字符处理,如下表所示:

| 字符类型 | 描述 |
| char | 原始数据类型。 |
| Character | char的包装类。 |
| CharBuffer | 这个类支持一个缓冲区char,为获取/放置字符或一系列字符操作提供方法。 |
| CharSequence | 由CharBufferSegmentStringStringBufferStringBuilder实现的接口。它支持对字符序列的只读访问。 |

Java 还提供了许多支持字符串的类和接口。下表对这些进行了总结。我们将在许多例子中使用这些。StringStringBufferStringBuilder类提供了相似的字符串处理能力,但是在它们是否可以被修改以及它们是否是线程安全的方面有所不同。CharacterIterator接口和StringCharacterIterator类提供了遍历字符序列的技术。

Segment类表示一段文本:

| 类/接口 | 描述 |
| String | 不可变的字符串。 |
| StringBuffer | 表示可修改的字符串。它是线程安全的。 |
| StringBuilder | 与StringBuffer类兼容,但
不是线程安全的。 |
| Segment | 表示字符数组中的一段文本。它提供了对数组中字符数据的快速访问。 |
| CharacterIterator | 为文本定义迭代器。它支持文本的双向遍历。 |
| StringCharacterIterator | 为String实现CharacterIterator接口的类。 |

如果我们从文件中读取,我们还需要考虑文件格式。通常,数据是从注释单词的来源获得的。例如,如果我们使用一个网页作为文本的来源,我们会发现它是用 HTML 标签标记的。这些不一定与分析过程相关,可能需要删除。

多用途互联网邮件扩展 ( MIME )类型用于表征文件使用的格式。下表列出了常见的文件类型。要么我们需要明确地删除或改变文件中的标记,要么使用专门的软件来处理它。一些 NLP APIs 提供了处理特殊文件格式的工具:

| 文件格式 | 哑剧类型 | 描述 |
| 文本 | 纯文本/文本 | 简单文本文件 |
| 办公室类型文件 | 应用程序/MS Word 应用/ vnd.oasis.opendocument.text | 微软办公开放式办公室 |
| 便携文档格式 | 应用程序/PDF | Adobe 可移植文档格式 |
| 超文本标记语言 | 文本/HTML | 网页 |
| 可扩展标记语言 | 文本/XML | 可扩展标记语言 |
| 数据库ˌ资料库 | 不适用 | 数据可以有多种不同的格式 |

许多 NLP APIs 都假设数据是干净的。当它不是的时候,就需要清理,以免我们得到不可靠和误导的结果。

摘要

在本章中,我们介绍了 NLP 及其用途。我们发现它在许多地方被用来解决许多不同类型的问题,从简单的搜索到复杂的分类问题。介绍了 Java 在核心字符串支持和高级 NLP 库方面对 NLP 的支持。使用代码解释和说明了基本的 NLP 任务。还包括了 NLP 和特征工程中深度学习的基础,以显示深度学习如何影响 NLP。我们还研究了训练、验证和使用模型的过程。

在本书中,我们将使用简单和更复杂的方法为使用基本的 NLP 任务打下基础。您可能会发现有些问题只需要简单的方法,在这种情况下,知道如何使用简单的技术可能就足够了。在其他情况下,可能需要更复杂的技术。无论是哪种情况,您都要准备好确定需要哪种工具,并能够为任务选择合适的技术。

在下一章中,第二章,寻找文本部分,我们将考察标记化的过程,看看它如何被用来寻找文本部分。**

二、查找部分文本

查找文本的各个部分涉及到将文本分解成称为记号的单个单元,并可选地对这些记号执行额外的处理。这种额外的处理可以包括词干化、词汇化、停用词移除、同义词扩展以及将文本转换为小写。

我们将展示在标准 Java 发行版中发现的几种标记化技术。这些都包括在内,因为有时这是你做这项工作所需要的。在这种情况下,可能不需要导入 NLP 库。然而,这些技术是有限的。接下来讨论 NLP APIs 支持的特定标记化器或标记化方法。这些例子将为如何使用标记器以及它们产生的输出类型提供参考。接下来简单比较了这两种方法之间的差异。

有许多专门的记号赋予者。例如,Apache Lucene 项目支持各种语言和专门文档的标记器。WikipediaTokenizer类是一个标记器,处理特定于维基百科的文档,而ArabicAnalyzer类处理阿拉伯文本。不可能在这里说明所有这些不同的方法。

我们还将研究如何训练某些记号赋予者来处理专门的文本。当遇到不同形式的文本时,这很有用。它通常可以消除编写新的专用记号赋予器的需要。

接下来,我们将说明如何使用这些记号赋予器来支持特定的操作,比如词干化、词汇化和停用词移除。词性也可以被认为是部分文本的特殊实例。不过这个话题在第五章、检测词性中有所考察。

因此,我们将在本章中讨论以下主题:

  • 什么是标记化?
  • 标记化器的使用
  • NLP 标记器 API
  • 理解标准化

理解文本的各个部分

对文本的各个部分进行分类有多种方法。例如,我们可能关注字符级的问题,如标点符号,可能需要忽略或扩展缩写。在单词级别,我们可能需要执行不同的操作,例如:

  • 使用词干化和/或词汇化识别词素
  • 扩展缩写和首字母缩略词
  • 隔离数字单位

我们不能总是用标点符号来拆分单词,因为标点符号有时会被认为是单词的一部分,比如单词不能。我们也可能关心将多个单词组合成有意义的短语。句子检测也是一个因素。我们不一定要将跨越句子边界的单词组合在一起。

在这一章中,我们主要关注记号化过程和一些专门的技术,比如词干。我们不会试图展示它们在其他 NLP 任务中是如何使用的。这些工作留待后面的章节讨论。

什么是标记化?

标记化是将文本分解成更简单单元的过程。对于大多数文本,我们关心的是孤立词。令牌根据一组分隔符进行拆分。这些分隔符通常是空白字符。Java 中的空白由Character类的isWhitespace方法定义。下表列出了这些字符。但是,有时可能需要使用一组不同的分隔符。例如,当空白分隔符模糊了文本分隔符(如段落边界)时,不同的分隔符会很有用,检测这些文本分隔符很重要:

字符 意为
Unicode 空格字符 (空格 _ 分隔符、行 _ 分隔符或段落 _ 分隔符)
\t U+0009 水平制表
\n U+000A 馈线
\u000B U+000B 垂直制表
\f U+000C 换页
\r U+000D 回车
\u001C U+001C 文件分隔符
\u001D U+001D 组分隔符
\u001E U+001E 记录分隔符
\u001F U+001F 单元分离器

令牌化过程因大量因素而变得复杂,例如:

  • 语言:不同的语言带来不同的挑战。空白是一种常用的分隔符,但是如果我们需要使用中文,它是不够的,因为中文不使用空白。
  • 文本格式:文本通常以不同的格式存储或呈现。相对于 HTML 或其他标记技术,简单文本的处理方式将使标记化过程变得复杂。
  • 停用词:常用词对于一些自然语言处理任务来说可能不重要,比如一般的搜索。这些常用词被称为停用词。当停用词对手头的 NLP 任务没有帮助时,它们有时会被移除。这些可以包括诸如 a以及之类的词。
  • 文本扩展:对于首字母缩写词和缩写词,有时需要
    来扩展它们,以便后处理可以产生更高质量的结果。
    例如,如果搜索者对单词机器感兴趣,知道 IBM 代表国际商业机器可能是有用的。
  • 大小写:单词的大小写(大写或小写)在某些情况下可能很重要。例如,单词的大小写可以帮助识别专有名词。识别文本的各个部分时,转换为相同的大小写有助于简化搜索。
  • 词干化和词汇化:这些过程将改变单词以获得它们的词根

删除停用词可以节省索引空间,加快索引过程。但是,有些引擎不删除停用字词,因为它们对某些查询可能很有用。例如,在执行精确匹配时,删除停用词会导致未命中。此外,NER 任务通常依赖于停用词的包含。认识到罗密欧与朱丽叶是一部戏剧取决于这个词的包含。

There are many lists that define stopwords. Sometimes, what constitutes a stopword is dependent on the problem domain. A list of stopwords can be found at www.ranks.nl/stopwords. It lists a few categories of English stopwords and stopwords for languages other than English. At www.textfixer.com/resources/common-english-words.txt, you will find a comma-separated formatted list of English stopwords.

改编自斯坦福的十大停用词(library . Stanford . edu/blogs/digital-library-blog/2011/12/stop words-search works-be-or-not-be)可以在下表中找到:

| 停止字 | 事件 |
| 这 | Seven thousand five hundred and seventy-eight |
| 关于 | Six thousand five hundred and eighty-two |
| 和 | Four thousand one hundred and six |
| 在 | Two thousand two hundred and ninety-eight |
| a | One thousand one hundred and thirty-seven |
| 到 | One thousand and thirty-three |
| 为 | Six hundred and ninety-five |
| 在 | Six hundred and eighty-five |
| 一;一个 | Two hundred and eighty-nine |
| 随着 | Two hundred and thirty-one |

我们将重点关注用于标记英语文本的技术。这通常涉及到使用空白或其他分隔符来返回一个令牌列表。

Parsing is closely related to tokenization. They are both concerned with identifying parts of text, but parsing is also concerned with identifying the parts of speech and their relationship to each other.

标记化器的使用

标记化的输出可以用于简单的任务,比如拼写检查和处理简单的搜索。它对于各种下游 NLP 任务也很有用,比如识别词性、句子检测和分类。接下来的大部分章节都会涉及到需要标记化的任务。

通常,令牌化过程只是更大任务序列中的一步。这些步骤涉及到管道的使用,我们将在使用管道一节中说明。这突出了为下游任务产生高质量结果的记号赋予器的需要。如果分词器做得不好,下游的任务将受到不利影响。

Java 中有许多不同的记号赋予器和记号化技术。有几个核心 Java 类被设计成支持标记化。其中一些现在已经过时了。还有许多 NLP APIs 被设计用来解决简单和复杂的令牌化问题。接下来的两节将研究这些方法。首先,我们将看到 Java 核心类必须提供什么,然后我们将演示一些 NLP API 标记化库。

简单的 Java 标记化器

有几个 Java 类支持简单的标记化;其中一些如下:

  • 扫描仪
  • 线
  • break 迭代器
  • StreamTokenizer
  • 字符串标记器

尽管这些类提供了有限的支持,但了解它们的使用方法还是很有用的。对于某些任务,这些类就足够了。当核心 Java 类可以完成这项工作时,为什么要使用更难理解、效率更低的方法呢?我们将讨论这些类中的每一个,因为它们支持令牌化过程。

StreamTokenizerStringTokenizer类不应用于新的开发。相反,?? 方法通常是更好的选择。它们被包含在这里,以防你碰到它们,想知道它们是否应该被使用。

使用 Scanner 类

Scanner类用于从文本源读取数据。这可能是标准输入,也可能来自文件。它提供了一种简单易用的技术来支持令牌化。

Scanner类使用空白作为默认分隔符。可以使用许多不同的构造函数来创建Scanner类的实例。
以下序列中的构造函数使用了一个简单的字符串。next方法从输入流中检索下一个令牌。令牌从字符串中分离出来,存储到字符串列表中,然后显示出来:

Scanner scanner = new Scanner("Let's pause, and then "
    + " reflect."); 
List<String> list = new ArrayList<>(); 
while(scanner.hasNext()) { 
    String token = scanner.next(); 
    list.add(token); 
} 
for(String token : list) { 
    System.out.println(token); 
} 

执行时,我们得到以下输出:

Let's
pause,
and
then
reflect.

这个简单的实现有几个缺点。如果我们需要我们的收缩被识别并可能被分割,正如第一个令牌所演示的,这个实现没有做到。此外,句子的最后一个单词被返回并附加了一个句点。

指定分隔符

如果我们对默认分隔符不满意,有几种方法可以用来改变它的行为。下表中总结了其中几种方法docs . Oracle . com/javase/7/docs/API/Java/util/scanner . html。提供这个列表是为了让您了解什么是可能的:

| 方法 | 效果 |
| useLocale | 使用区域设置来设置默认分隔符匹配 |
| useDelimiter | 基于字符串或模式设置分隔符 |
| useRadix | 指定处理数字时要使用的基数 |
| skip | 跳过输入匹配模式并忽略分隔符 |
| findInLine | 忽略分隔符,查找模式的下一个匹配项 |

这里,我们将演示useDelimiter方法的使用。如果我们在前面部分的示例中的while语句之前使用下面的语句,将使用的唯一分隔符将是空格、撇号和句点:

scanner.useDelimiter("[ ,.]"); 

执行时,将显示以下内容。空行反映了逗号分隔符的使用。在本例中,它具有返回空字符串作为令牌的不良效果:

Let's
pause

and
then
reflect  

此方法使用字符串中定义的模式。左括号和右括号用于创建一类字符。这是一个匹配这三个字符的正则表达式。关于 Java 模式的解释可以在 http://docs.oracle.com/javase/8/docs/api/找到。可以使用reset方法将分隔符列表重置为空白。

使用拆分方法

我们在第一章、NLP 简介中演示了Stringclass’split方法。
为方便起见,此处重复:

String text = "Mr. Smith went to 123 Washington avenue."; 
String tokens[] = text.split("\\s+"); 
for (String token : tokens) { 
    System.out.println(token); 
} 

输出如下所示:

Mr.
Smith
went
to
123
Washington
avenue.

split方法也使用正则表达式。如果我们用上一节中使用的相同字符串("Let's pause, and then reflect.")替换文本,我们将得到相同的输出。

split方法有一个重载版本,它使用一个整数来指定正则表达式模式应用于目标文本的次数。使用此参数可以在达到指定的匹配次数后停止操作。

Pattern类也有一个split方法。它将根据用于创建Pattern对象的模式来分割它的参数。

使用 BreakIterator 类

标记化的另一种方法是使用BreakIterator类。这个类支持不同文本单元的整数边界的位置。在这一节中,我们将说明如何使用它来查找单词。

该类有一个受保护的默认构造函数。我们将使用静态的getWordInstance方法来获取该类的一个实例。这个方法使用一个Locale对象重载了一个版本。该类拥有几种访问边界的方法,如下表所示。它有一个字段DONE,用于指示已经找到最后一个边界:

| 方法 | 用途 |
| first | 返回文本的第一个边界 |
| next | 返回当前边界之后的下一个边界 |
| previous | 返回当前边界之前的边界 |
| setText | 将字符串与BreakIterator实例相关联 |

为了演示这个类,我们声明了一个BreakIterator类的实例和一个与之一起使用的字符串:

BreakIterator wordIterator = BreakIterator.getWordInstance(); 
String text = "Let's pause, and then reflect."; 

然后将文本指定给实例,并确定第一条边界:

wordIterator.setText(text); 
int boundary = wordIterator.first();

接下来的循环将使用beginend变量存储断词的开始和结束边界索引。边界值是整数。将显示每个边界对及其相关文本。

当找到最后一个边界时,循环终止:

while (boundary != BreakIterator.DONE) { 
    int begin = boundary; 
    System.out.print(boundary + "-"); 
    boundary = wordIterator.next(); 
    int end = boundary; 
    if(end == BreakIterator.DONE) break; 
    System.out.println(boundary + " [" 
    + text.substring(begin, end) + "]"); 
} 

输出如下,括号用于清楚地描述文本:

0-5 [Let's]
5-6 [ ]
6-11 [pause]
11-12 [,]
12-13 [ ]
13-16 [and]
16-17 [ ]
17-21 [then]
21-22 [ ]
22-29 [reflect]
29-30 [.]  

这种技术在识别基本令牌方面做得相当好。

使用 StreamTokenizer 类

java.io包中找到的StreamTokenizer类被设计用来标记输入流。它是一个较老的类,不如在使用 StringTokenizer 类一节中讨论的StringTokenizer类灵活。类的实例通常基于文件创建,并将对文件中的文本进行标记。它可以使用字符串来构造。

该类使用一个nextToken方法来返回流中的下一个令牌。返回的令牌是一个整数。整数值反映了返回的令牌类型。根据令牌类型,可以用不同的方式处理令牌。

StreamTokenizer类字段如下表所示:

| 字段 | 数据类型 | 意为 |
| nval | double | 如果当前令牌是数字,则包含一个数字 |
| sval | String | 如果当前标记是单词标记,则包含该标记 |
| TT_EOF | static int | 流结尾的常数 |
| TT_EOL | static int | 行尾的常数 |
| TT_NUMBER | static int | 读取的令牌数 |
| TT_WORD | static int | 指示单词标记的常数 |
| ttype | int | 令牌读取的类型 |

在这个例子中,创建了一个标记器,然后声明了用于终止循环的变量isEOFnextToken方法返回令牌类型。根据令牌类型,将显示数字令牌和字符串令牌:

try { 
    StreamTokenizer tokenizer = new StreamTokenizer( 
          newStringReader("Let's pause, and then reflect.")); 
    boolean isEOF = false; 
    while (!isEOF) { 
        int token = tokenizer.nextToken(); 
        switch (token) { 
            case StreamTokenizer.TT_EOF: 
                isEOF = true; 
                break; 
            case StreamTokenizer.TT_EOL: 
                break; 
            case StreamTokenizer.TT_WORD: 
                System.out.println(tokenizer.sval); 
                break; 
            case StreamTokenizer.TT_NUMBER: 
                System.out.println(tokenizer.nval); 
                break; 
            default: 
                System.out.println((char) token); 
        } 
    } 
} catch (IOException ex) { 
    // Handle the exception 
} 

执行时,我们得到以下输出:

Let
'  

这不是我们通常所期望的。问题是标记器使用撇号(单引号字符)和双引号来表示引用的文本。因为没有对应的匹配,所以它消耗字符串的剩余部分。

我们可以使用ordinaryChar方法来指定哪些字符应该被视为通用字符。单引号和逗号字符在这里被指定为普通字符:

tokenizer.ordinaryChar('\''); 
tokenizer.ordinaryChar(','); 

当这些语句被添加到前面的代码中并被执行时,我们得到以下输出:

Let
'
s
pause
,
and
then
reflect.  

撇号现在不是问题了。这两个字符被视为分隔符,并作为标记返回。还有一个whitespaceChars方法可以指定哪些字符将被视为空白。

使用 StringTokenizer 类

java.util包中可以找到StringTokenizer类。它比StreamTokenizer类提供了更多的灵活性,并且被设计用来处理来自任何来源的字符串。类的构造函数接受要标记的字符串作为它的参数,并使用nextToken方法返回标记。如果输入流中存在更多的标记,hasMoreTokens方法将返回true。这按以下顺序进行了说明:

StringTokenizerst = new StringTokenizer("Let's pause, and "
     + "then reflect."); 
while (st.hasMoreTokens()) { 
    System.out.println(st.nextToken()); 
}

执行时,我们得到以下输出:

Let's
pause,
and
then
reflect.

构造函数被重载,允许指定分隔符以及分隔符是否应作为标记返回。

Java 核心令牌化的性能考虑

当使用这些核心 Java 标记化方法时,有必要简要讨论一下它们的性能。由于影响代码执行的各种因素,衡量性能有时会很棘手。也就是说,在这里可以找到几种 Java 核心令牌化技术的性能的有趣比较:stack overflow . com/questions/5965767/performance-of-string tokenizer-class-vs-split-method-in-Java。对于他们正在解决的问题来说,indexOf方法是最快的。

NLP 标记器 API

在本节中,我们将使用 OpenNLP、Stanford 和 LingPipe APIs 演示几种不同的标记化技术。尽管还有许多其他可用的 API,但我们只对这些 API 进行了演示。这些例子会让你知道哪些技术是可用的。

我们将使用一个名为paragraph的字符串来说明这些技术。该字符串包含一个新的换行符,该换行符可能出现在真实文本中的意外位置。其定义如下:

private String paragraph = "Let's pause, \nand then +
     + "reflect.";

使用 OpenNLPTokenizer 类

OpenNLP 拥有一个由三个类实现的Tokenizer接口:SimpleTokenizerTokenizerMEWhitespaceTokenizer。该接口支持两种方法:

  • tokenize:向其传递一个字符串以进行标记化,并以字符串形式返回一个由
    个标记组成的数组。
  • tokenizePos:传递一个字符串,返回一个Span
    对象的数组。Span类用于指定标记的开始和结束
    偏移量。

这些类中的每一个都将在下面的部分中进行演示。

使用 SimpleTokenizer 类

顾名思义,SimpleTokenizer类执行简单的文本标记化。INSTANCE字段用于实例化该类,如下面的代码序列所示。对paragraph变量执行tokenize方法,然后显示令牌:

SimpleTokenizer simpleTokenizer = SimpleTokenizer.INSTANCE; 
String tokens[] = simpleTokenizer.tokenize(paragraph); 
for(String token : tokens) { 
    System.out.println(token); 
} 

执行时,我们得到以下输出:

    Let
    '
    s
    pause
    ,
    and
    then
    reflect
    .  

使用这个标记器,标点符号作为单独的标记返回。

使用 WhitespaceTokenizer 类

顾名思义,这个类使用空格作为分隔符。在下面的代码序列中,创建了一个记号赋予器的实例,并使用paragraph作为输入对其执行tokenize方法。然后,for 语句显示标记:

String tokens[] = 
 WhitespaceTokenizer.INSTANCE.tokenize(paragraph); 
for (String token : tokens) { 
    System.out.println(token); 
} 

输出如下所示:

    Let's
    pause,
    and
    then
    reflect.  

虽然这并不能区分缩写和相似的文本单元,但对于某些应用程序来说,这是很有用的。该类还拥有一个返回令牌边界的tokizePos方法。

使用 TokenizerME 类

TokenizerME类使用用最大熵 ( MaxEnt )和一个统计模型创建的模型来执行标记化。MaxEnt 模型用于确定数据之间的关系——在我们的例子中是文本。一些文本来源,如各种社交媒体,格式不规范,使用大量俚语和特殊符号,如表情符号。统计记号赋予器,例如 MaxEnt 模型,提高了记号化过程的质量。

A detailed discussion of this model is not possible here due to its complexity. A good starting point for an interested reader can be found at en.wikipedia.org/w/index.php?title=Multinomial_logistic_regression&redirect=no.

一个TokenizerModel类隐藏了模型,并用于实例化记号赋予器。该模型必须先前已经被训练过。在下面的例子中,使用在en-token.bin文件中找到的模型实例化了记号赋予器。这个模型已经被训练为处理普通的英语文本。

模型文件的位置由getModelDir方法返回,您需要实现这个方法。返回值取决于模型在系统中的存储位置。许多这些模型可以在 http://opennlp.sourceforge.net/models-1.5/的找到。

在创建了一个FileInputStream类的实例之后,输入流被用作TokenizerModel构造函数的参数。tokenize方法将生成一个字符串数组。接下来是显示令牌的代码:

try { 
    InputStream modelInputStream = new FileInputStream( 
        new File(getModelDir(), "en-token.bin")); 
    TokenizerModel model = new 
         TokenizerModel(modelInputStream); 
    Tokenizer tokenizer = new TokenizerME(model); 
    String tokens[] = tokenizer.tokenize(paragraph); 
    for (String token : tokens) { 
        System.out.println(token); 
    } 
} catch (IOException ex) { 
    // Handle the exception 
} 

输出如下所示:

Let
's
pause
,
and
then
reflect
.  

使用斯坦福记号赋予器

几个斯坦福 NLP API 类支持记号化;其中几个是
如下:

  • PTBTokenizer
  • DocumentPreprocessor
  • 作为管道的StanfordCoreNLP

每个例子都将使用前面定义的paragraph字符串。

使用 PTBTokenizer 类

这个分词器模仿了佩恩树库 3 ( PTB )分词器(www.cis.upenn.edu/~treebank/)。它在选项和对 Unicode 的支持方面不同于 PTB。PTBTokenizer类支持几个旧的构造函数;但是,建议使用三参数构造函数。这个构造函数使用一个Reader对象、一个LexedTokenFactory<T>参数和一个字符串来指定使用几个选项中的哪一个。

LexedTokenFactory接口是由CoreLabelTokenFactoryWordTokenFactory类实现的。前一个类支持保留标记的开始和结束字符位置,而后一个类只是将标记作为不带任何位置信息的字符串返回。默认情况下使用WordTokenFactory类。我们将演示这两个类的用法。

在下面的例子中使用了CoreLabelTokenFactory类。使用paragraph创建一个StringReader实例。最后一个参数用于选项,在本例中是nullIterator接口由PTBTokenizer类实现,允许我们使用hasNextnext方法来显示令牌:

PTBTokenizer ptb = new PTBTokenizer( 
    new StringReader(paragraph), new 
 CoreLabelTokenFactory(),null); 
while (ptb.hasNext()) { 
    System.out.println(ptb.next()); 
} 

输出如下所示:

Let
's
pause
,
and
then
reflect
.  

使用WordTokenFactory类可以获得相同的输出,如下所示:

PTBTokenizerptb = new PTBTokenizer( 
    new StringReader(paragraph), new WordTokenFactory(), null);

CoreLabelTokenFactory类的功能是通过PTBTokenizer构造函数的 options 参数实现的。这些选项提供了控制记号赋予器行为的方法。选项包括如何处理引号、如何映射省略号,以及它应该处理英式英语拼写还是美式英语拼写。选项列表可以在NLP . Stanford . edu/NLP/javadoc/javanlp/edu/Stanford/NLP/process/ptbtokenizer . html找到。

在下面的代码序列中,PTBTokenizer对象是使用CoreLabelTokenFactory变量ctf和选项"invertible=true"创建的。这个选项允许我们获得并使用一个CoreLabel对象,它将给出每个令牌的开始和结束位置:

CoreLabelTokenFactory ctf = new CoreLabelTokenFactory(); 
PTBTokenizer ptb = new PTBTokenizer( 
    new StringReader(paragraph),ctf,"invertible=true"); 
while (ptb.hasNext()) { 
    CoreLabel cl = (CoreLabel)ptb.next(); 
    System.out.println(cl.originalText() + " (" +  
        cl.beginPosition() + "-" + cl.endPosition() + ")"); 
} 

这个序列的输出如下。括号中的数字表示标记的开始和结束位置:

Let (0-3)
's (3-5)
pause (6-11)
, (11-12)
and (14-17)
then (18-22)
reflect (23-30)
. (30-31)  

使用 document 预处理程序类

DocumentPreprocessor类标记来自输入流的输入。此外,它实现了Iterable接口,使得遍历标记化序列变得容易。记号赋予器支持简单文本和 XML 数据的记号化。

为了说明这个过程,我们将使用一个StringReader类的实例,它使用paragraph字符串,定义如下:

Reader reader = new StringReader(paragraph);

然后实例化DocumentPreprocessor类的一个实例:

DocumentPreprocessor documentPreprocessor = 
      new DocumentPreprocessor(reader); 

DocumentPreprocessor类实现了Iterable<java.util.List<HasWord>>接口。HasWord接口包含两个处理文字的方法:setWordword。后一种方法将单词作为字符串返回。在下面的代码序列中,DocumentPreprocessor类将输入文本分割成句子,并存储为List<HasWord>。一个Iterator对象用于提取一个句子,然后一个 for-each 语句将显示标记:

Iterator<List<HasWord>> it = documentPreprocessor.iterator(); 
while (it.hasNext()) { 
    List<HasWord> sentence = it.next(); 
    for (HasWord token : sentence) { 
        System.out.println(token); 
    } 
} 

执行时,我们得到以下输出:

Let
's
pause
,
and
then
reflect
.  

使用管道

这里,我们将使用StanfordCoreNLP类,如第一章、NLP 介绍中所示。然而,我们使用一个更简单的注释器字符串来标记段落。如下面的代码所示,创建了一个Properties对象,并为其分配了tokenizessplit标注器。

tokenize注释器指定将发生标记化,而ssplit注释导致句子被拆分:

Properties properties = new Properties(); 
properties.put("annotators", "tokenize, ssplit");

接下来创建StanfordCoreNLP类和Annotation类:

StanfordCoreNLP pipeline = new StanfordCoreNLP(properties); 
Annotation annotation = new Annotation(paragraph); 

执行annotate方法来标记文本,然后prettyPrint方法将显示标记:

pipeline.annotate(annotation); 
pipeline.prettyPrint(annotation, System.out); 

显示各种统计信息,后面是输出中用位置信息标记的标记,如下所示:

    Sentence #1 (8 tokens):
    Let's pause, 
    and then reflect.
    [Text=Let CharacterOffsetBegin=0 CharacterOffsetEnd=3] [Text='s CharacterOffsetBegin=3 CharacterOffsetEnd=5] [Text=pause CharacterOffsetBegin=6 CharacterOffsetEnd=11] [Text=, CharacterOffsetBegin=11 CharacterOffsetEnd=12] [Text=and CharacterOffsetBegin=14 CharacterOffsetEnd=17] [Text=then CharacterOffsetBegin=18 CharacterOffsetEnd=22] [Text=reflect CharacterOffsetBegin=23 CharacterOffsetEnd=30] [Text=. CharacterOffsetBegin=30 CharacterOffsetEnd=31]

使用 LingPipe 记号赋予器

LingPipe 支持许多记号赋予器。在这一节中,我们将说明IndoEuropeanTokenizerFactory类的用法。在后面的章节中,我们将展示 LingPipe 支持标记化的其他方式。它的INSTANCE字段提供了一个印欧语标记器的实例。tokenizer方法根据要处理的文本返回一个Tokenizer类的实例,如下所示:

char text[] = paragraph.toCharArray(); 
TokenizerFactory tokenizerFactory = 
 IndoEuropeanTokenizerFactory.INSTANCE; 
Tokenizer tokenizer = tokenizerFactory.tokenizer(text, 0, 
 text.length); 
for (String token : tokenizer) { 
    System.out.println(token); 
}

输出如下所示:

Let
'
s
pause
,
and
then
reflect
.  

这些标记化器支持普通文本的标记化。在下一节中,我们将演示如何训练分词器来处理独特的文本。

训练分词器查找部分文本

当我们遇到标准分词器不能很好处理的文本时,训练分词器是很有用的。我们可以创建一个用于执行标记化的标记化器模型,而不是编写一个定制的标记化器。

为了演示如何创建这样的模型,我们将从文件中读取训练数据,然后使用该数据训练模型。数据存储为由空格和<SPLIT>字段分隔的一系列单词。这个<SPLIT>字段用于提供关于如何识别令牌的进一步信息。它们可以帮助识别数字(如23.6)和标点符号(如逗号)之间的分隔符。我们将使用的训练数据存储在training-data.train文件中,如下所示:

These fields are used to provide further information about how tokens should be identified<SPLIT>.  
They can help identify breaks between numbers<SPLIT>, such as 23.6<SPLIT>, punctuation characters such as commas<SPLIT>. 

我们使用的数据并不代表唯一的文本,但它确实说明了如何注释文本以及用于训练模型的过程。

我们将使用 OpenNLP TokenizerME类的重载train方法来创建一个模型。最后两个参数需要额外的解释。MaxEnt 用于确定文本元素之间的关系。

我们可以指定模型在包含到模型中之前必须处理的功能的数量。这些特征可以被认为是模型的方面。迭代指的是在确定模型参数时训练过程将迭代的次数。一些TokenME类参数如下:

| 参数 | 用途 |
| String | 所用语言的代码 |
| ObjectStream<TokenSample> | 包含训练数据的ObjectStream参数 |
| boolean | 如果true,则字母数字数据被忽略 |
| int | 指定处理特征的次数 |
| int | 用于训练
MaxEnt 模型的迭代次数 |

在下面的例子中,我们首先定义一个用于存储新模型的BufferedOutputStream对象。本例中使用的几个方法将生成异常,这些异常在catch块中处理:

BufferedOutputStream modelOutputStream = null; 
try { 
    ... 
} catch (UnsupportedEncodingException ex) { 
    // Handle the exception 
} catch (IOException ex) { 
    // Handle the exception 
} 

使用PlainTextByLineStream类创建一个ObjectStream类的实例。这使用训练文件和字符编码方案作为其构造函数参数。这用于创建TokenSample对象的第二个ObjectStream实例。这些对象是包含令牌范围信息的文本:

ObjectStream<String> lineStream = new PlainTextByLineStream( 
    new FileInputStream("training-data.train"), "UTF-8"); 
ObjectStream<TokenSample> sampleStream =  
    new TokenSampleStream(lineStream); 

现在可以使用train方法了,如下面的代码所示。英语被指定为语言。字母数字信息被忽略。特征值和迭代值分别设置为5100:

TokenizerModel model = TokenizerME.train( 
    "en", sampleStream, true, 5, 100);

下表详细给出了train方法的参数:

| 参数 | 意为 |
| 语言代码 | 指定所用自然语言的字符串 |
| 样品 | 示例文本 |
| 字母数字优化 | 如果true,则跳过字母数字 |
| 近路 | 处理特征的次数 |
| 迭代次数 | 为定型模型而执行的迭代次数 |

下面的代码序列将创建一个输出流,然后将模型写出到mymodel.bin文件。然后模型就可以使用了:

BufferedOutputStream modelOutputStream = new 
 BufferedOutputStream( 
    new FileOutputStream(new File("mymodel.bin"))); 
model.serialize(modelOutputStream); 

这里将不讨论输出的细节。然而,它实际上记录了训练过程。序列的输出如下所示,但是最后一部分被缩短了,为了节省空间,大部分迭代步骤都被删除了:

    Indexing events using cutoff of 5

    Dropped event F:[p=2, s=3.6,, p1=2, p1_num, p2=bok, p1f1=23, f1=3, f1_num, f2=., f2_eos, f12=3.]
    Dropped event F:[p=23, s=.6,, p1=3, p1_num, p2=2, p2_num, p21=23, p1f1=3., f1=., f1_eos, f2=6, f2_num, f12=.6]
    Dropped event F:[p=23., s=6,, p1=., p1_eos, p2=3, p2_num, p21=3., p1f1=.6, f1=6, f1_num, f2=,, f12=6,]
      Computing event counts...  done. 27 events
      Indexing...  done.
    Sorting and merging events... done. Reduced 23 events to 4.
    Done indexing.
    Incorporating indexed data for training...  
    done.
      Number of Event Tokens: 4
          Number of Outcomes: 2
        Number of Predicates: 4
    ...done.
    Computing model parameters ...
    Performing 100 iterations.
      1:  ...loglikelihood=-15.942385152878742  0.8695652173913043
      2:  ...loglikelihood=-9.223608340603953  0.8695652173913043
      3:  ...loglikelihood=-8.222154969329086  0.8695652173913043
      4:  ...loglikelihood=-7.885816898591612  0.8695652173913043
      5:  ...loglikelihood=-7.674336804488621  0.8695652173913043
      6:  ...loglikelihood=-7.494512270303332  0.8695652173913043
    Dropped event T:[p=23.6, s=,, p1=6, p1_num, p2=., p2_eos, p21=.6, p1f1=6,, f1=,, f2=bok]
      7:  ...loglikelihood=-7.327098298508153  0.8695652173913043
      8:  ...loglikelihood=-7.1676028756216965  0.8695652173913043
      9:  ...loglikelihood=-7.014728408489079  0.8695652173913043
    ...
    100:  ...loglikelihood=-2.3177060257465376  1.0

我们可以使用该模型,如下面的序列所示。这与我们在使用 TokenizerME 类一节中使用的技术相同。唯一的区别是这里使用的模型:

try { 
    paragraph = "A demonstration of how to train a 
 tokenizer."; 
    InputStream modelIn = new FileInputStream(new File( 
        ".", "mymodel.bin")); 
    TokenizerModel model = new TokenizerModel(modelIn); 
    Tokenizer tokenizer = new TokenizerME(model); 
    String tokens[] = tokenizer.tokenize(paragraph); 
    for (String token : tokens) { 
        System.out.println(token); 
} catch (IOException ex) { 
    ex.printStackTrace(); 
} 

输出如下所示:

A
demonstration
of
how
to
train
a
tokenizer
.

比较标记化器

下表显示了 NLP API 标记化器的简要比较。生成的令牌列在令牌化器的名称下。它们都基于同一个文本:“让我们暂停,然后反思。”请记住,输出是基于类的简单使用。可能存在示例中未包含的选项,这些选项会影响令牌的生成方式。目的是简单地展示基于示例代码和数据的预期输出类型:

| SimpleTokenizer | WhitespaceTokenizer | TokenizerME | PTBTokenizer | DocumentPreprocessor | IndoEuropeanTokenizerFactory |
| 让 | 让我们 | 让 | 让 | 让 | 让 |
| ' | 暂停, | s | s | s | ' |
| s | 和 | 中止 | 中止 | 中止 | s |
| 中止 | 然后 | , | , | , | 中止 |
| , | 反思。 | 和 | 和 | 和 | , |
| 和 | | 然后 | 然后 | 然后 | 和 |
| 然后 | | 显示 | 显示 | 显示 | 然后 |
| 显示 | | 。 | 。 | 。 | 显示 |
| 。 | | | | | 。 |

理解标准化

规范化是将单词列表转换为更统一的序列的过程。这有助于为以后的处理准备文本。通过将单词转换成标准格式,其他操作就能够处理数据,而不必处理可能会影响该过程的问题。例如,将所有单词转换为小写将简化搜索过程。

规范化过程可以改进文本匹配。例如,术语调制解调器路由器有几种表达方式,如调制解调器和路由器、调制解调器&路由器、调制解调器/路由器和调制解调器-路由器。通过将这些词规范化为通用形式,可以更容易地向购物者提供正确的信息。

请理解,规范化过程也可能会影响 NLP 任务。当大小写很重要时,转换成小写字母会降低搜索的可靠性。

规范化操作可以包括以下内容:

  • 将字符改为小写
  • 扩展缩写
  • 删除停用词
  • 词干化和词汇化

我们将在这里研究这些技术,除了扩展缩写。这种技术类似于用于删除停用词的技术,只是缩写被替换为它们的扩展版本。

转换成小写

将文本转换为小写是一个简单的过程,可以改善搜索结果。我们既可以使用 Java 方法,比如String类的toLowerCase方法,也可以使用一些 NLP APIs 中的功能,比如 LingPipe 的LowerCaseTokenizerFactory类。这里演示了toLowerCase方法:

String text = "A Sample string with acronyms, IBM, and UPPER " 
   + "and lowercase letters."; 
String result = text.toLowerCase(); 
System.out.println(result); 

输出如下所示:

    a sample string with acronyms, ibm, and upper and lowercase letters.

LingPipe 的LowerCaseTokenizerFactory方法在使用管道规范化一节中进行了说明。

删除停用词

有几种方法可以删除停用词。一个简单的方法是创建一个类来保存和删除停用词。此外,几个 NLP APIs 提供了对停用词移除的支持。我们将创建一个名为StopWords的简单类来演示第一种方法。然后我们将使用 LingPipe 的EnglishStopTokenizerFactory类来演示第二种方法。

创建停用字词类

移除停用字词的过程包括检查令牌流,将它们与停用字词列表进行比较,然后从流中移除停用字词。为了说明这种方法,我们将创建一个支持基本操作的简单类,如下表所示:

| 构造器/方法 | 用途 |
| 默认构造函数 | 使用一组默认的停用词 |
| 单参数构造函数 | 使用存储在文件中的停用词 |
| addStopWord | 向内部列表添加新的停用字词 |
| removeStopWords | 接受一个单词数组,并返回一个删除了停用词的新数组 |

创建一个名为StopWords的类,声明两个实例变量,如下面的代码块所示。defaultStopWords变量是一个保存默认停用词表的数组。HashSet变量的stopWords列表用于保存用于处理的停用词:

public class StopWords { 

    private String[] defaultStopWords = {"i", "a", "about", "an", 
       "are", "as", "at", "be", "by", "com", "for", "from", "how", 
       "in", "is", "it", "of", "on", "or", "that", "the", "this", 
       "to", "was", "what", "when", where", "who", "will", "with"}; 

    private static HashSet stopWords  = new HashSet(); 
    ... 
} 

接下来是该类的两个构造函数,它们填充了HashSet:

public StopWords() { 
    stopWords.addAll(Arrays.asList(defaultStopWords)); 
} 

public StopWords(String fileName) { 
    try { 
        BufferedReader bufferedreader =  
                new BufferedReader(new FileReader(fileName)); 
        while (bufferedreader.ready()) { 
            stopWords.add(bufferedreader.readLine()); 
        } 
    } catch (IOException ex) { 
        ex.printStackTrace(); 
    } 
}

addStopWord便利方法允许添加额外的单词:

public void addStopWord(String word) { 
    stopWords.add(word); 
}

removeStopWords方法用于删除停用词。它创建ArrayList来保存传递给该方法的原始单词。for循环用于从列表中移除停用词。contains方法将确定提交的单词是否是停用词,如果是,则删除它。ArrayList被转换成一个字符串数组,然后返回。这显示如下:

public String[] removeStopWords(String[] words) { 
    ArrayList<String> tokens =  
        new ArrayList<String>(Arrays.asList(words)); 
    for (int i = 0; i < tokens.size(); i++) { 
        if (stopWords.contains(tokens.get(i))) { 
            tokens.remove(i); 
        } 
    } 
    return (String[]) tokens.toArray(
         new String[tokens.size()]); 
} 

下面的序列说明了如何使用停用词。首先,我们使用默认构造函数声明了一个StopWords类的实例。声明了 OpenNLP SimpleTokenizer类并定义了示例文本,如下所示:

StopWords stopWords = new StopWords(); 
SimpleTokenizer simpleTokenizer = SimpleTokenizer.INSTANCE; 
paragraph = "A simple approach is to create a class " 
    + "to hold and remove stopwords."; 

示例文本被标记化,然后传递给removeStopWords方法。
然后显示新列表:

String tokens[] = simpleTokenizer.tokenize(paragraph); 
String list[] = stopWords.removeStopWords(tokens); 
for (String word : list) { 
    System.out.println(word); 
}

执行时,我们得到以下输出。A未被移除,因为它是大写的,并且该类不执行大小写转换:

A
simple
approach
create
class
hold
remove
stopwords
.  

使用 LingPipe 删除停用词

LingPipe 拥有EnglishStopTokenizerFactory类,我们将使用它来识别和删除停用词。这个列表中的单词可以在alias-I . com/ling pipe/docs/API/com/aliasi/token izer/englishstoptokenizerfactory . html找到。它们包括诸如 a、was、but、he 和 for 之类的词。

factory类的构造函数需要一个TokenizerFactory实例作为它的参数。我们将使用工厂的tokenizer方法来处理单词列表并删除停用词。我们首先声明要标记化的字符串:

String paragraph = "A simple approach is to create a class "  
    + "to hold and remove stopwords."; 

接下来,我们基于IndoEuropeanTokenizerFactory类创建一个TokenizerFactory的实例。然后,我们使用该工厂作为参数来创建我们的EnglishStopTokenizerFactory实例:

TokenizerFactory factory = 
 IndoEuropeanTokenizerFactory.INSTANCE; 
factory = new EnglishStopTokenizerFactory(factory); 

使用 LingPipe Tokenizer类和工厂的tokenizer方法,处理在paragraph变量中声明的文本。tokenizer方法使用了一个char数组,一个起始索引,其长度为:

Tokenizer tokenizer = factory.tokenizer(paragraph.toCharArray(), 
   0, paragraph.length());

下面的 for-each 语句将迭代修改后的列表:

for (String token : tokenizer) { 
    System.out.println(token); 
} 

输出如下所示:

A
simple
approach
create
class
hold
remove
stopwords
.  

注意,尽管字母A是一个停用词,但它并没有从列表中删除。这是因为停用词表使用小写的 a ,而不是大写的 A 。结果,它漏掉了这个词。我们将在使用管道规格化部分纠正这个问题。

使用词干

寻找单词的词干需要去掉任何前缀或后缀,剩下的就是词干。识别词干对于寻找相似单词很重要的任务很有用。例如,搜索可能会寻找出现的单词,如 book 。有很多单词包含这个单词,包括 books、booked、bookings 和 bookmark。识别词干,然后在文档中查找它们的出现是很有用的。在许多情况下,这可以提高搜索的质量。

词干分析器可能产生不是真实单词的词干。例如,它可以决定 bounty、bounty 和 bountiful 都有相同的词干, bounti 。这对于搜索仍然很有用。

与词干相似的是词汇化。这是寻找它的引理的过程,它的形式就像在字典中找到的一样。这对于一些搜索也很有用。词干提取通常被视为一种更原始的技术,试图找到一个单词的词根涉及到切掉一个标记的开头和/或结尾部分。
词汇化可以被认为是一种更复杂的方法,它致力于寻找一个单词的词法或词汇意义。例如,单词have的词干为 hav ,而其词条为 have 。此外,单词有不同的词干,但相同的引理,
词汇化通常比词干化使用更多的计算资源。它们都有自己的位置,它们的效用部分取决于需要解决的问题。

使用波特斯特梅尔

波特斯特梅尔是英语中常用的词干分析器。它的主页可以在 http://tartarus.org/martin/PorterStemmer/找到。它用五个步骤来做一个单词。这些步骤是:

  1. 改变复数,简单现在,过去和过去分词,将 y 转换为 I,例如 agreed 将被改为 agree,sleep 将被改为 sleepi
  2. 将双后缀改为单后缀,例如专门化将改为专门化
  3. 按照步骤 2 中的方法,将“特殊”更改为“特殊”
  4. 通过将 speci 改为 speci 来更改剩余的单个后缀
  5. 它删除 e 或删除末尾的双字母,例如属性将被更改为 attrib 或将被更改为 wil

虽然 Apache OpenNLP 1.5.3 不包含PorterStemmer类,但其源代码可以从SVN . Apache . org/repos/ASF/open NLP/trunk/open NLP-tools/src/main/Java/open NLP/tools/stemmer/porter stemmer . Java下载。然后可以将它添加到您的项目中。

在下面的例子中,我们演示了针对单词数组的PorterStemmer类。输入可能很容易来自其他文本源。创建了一个PorterStemmer类的实例,然后将它的stem方法应用于数组中的每个单词:

String words[] = {"bank", "banking", "banks", "banker", "banked", 
     "bankart"}; 
PorterStemmer ps = new PorterStemmer(); 
for(String word : words) { 
    String stem = ps.stem(word); 
    System.out.println("Word: " + word + "  Stem: " + stem); 
} 

执行时,您将获得以下输出:

Word: bank  Stem: bank
Word: banking  Stem: bank
Word: banks  Stem: bank
Word: banker  Stem: banker
Word: banked  Stem: bank
Word: bankart  Stem: bankart  

最后一个词与单词损害结合使用,如 Bankart 损害。这是肩膀的伤,和前面的话没有太大关系。它确实表明在寻找词干时只使用普通词缀。

其他可能有用的PorterStemmer类方法可以在下表中找到:

| 方法 | 意为 |
| add | 这将在当前词干的末尾添加一个char |
| stem | 如果出现不同的词干,不带参数的方法将返回true |
| reset | 重置词干分析器,以便使用不同的单词 |

用 LingPipe 堵塞

PorterStemmerTokenizerFactory类用于使用 LingPipe 查找词干。在这个例子中,我们将使用与使用波特斯特梅尔一节中的相同的单词数组。IndoEuropeanTokenizerFactory类用于执行初始标记化,随后使用波特斯特梅尔。这些类别的定义如下:

TokenizerFactory tokenizerFactory = 
 IndoEuropeanTokenizerFactory.INSTANCE; 
TokenizerFactory porterFactory =  
    new PorterStemmerTokenizerFactory(tokenizerFactory); 

接下来声明一个保存词干的数组。我们重用了上一节中声明的words数组。每个单词都是单独处理的。单词被标记化,其词干存储在stems中,如下面的代码块所示。然后显示单词及其词干:

String[] stems = new String[words.length]; 
for (int i = 0; i < words.length; i++) { 
    Tokenization tokenizer = new Tokenization(words[i],porterFactory); 
    stems = tokenizer.tokens(); 
    System.out.print("Word: " + words[i]); 
    for (String stem : stems) { 
        System.out.println("  Stem: " + stem); 
    } 
} 

执行时,我们得到以下输出:

Word: bank  Stem: bank
Word: banking  Stem: bank
Word: banks  Stem: bank
Word: banker  Stem: banker
Word: banked  Stem: bank
Word: bankart  Stem: bankart  

我们已经使用 OpenNLP 和 LingPipe 示例演示了波特斯特梅尔。值得注意的是,还有其他类型的词干分析器可用,包括 Ngrams 和各种混合概率/算法方法。

使用词汇化

许多 NLP APIs 都支持词汇化。在这一节中,我们将说明如何使用StanfordCoreNLPOpenNLPLemmatizer类来执行词汇化。词汇化过程决定了一个词的词汇。一个引理可以被认为是一个单词的字典形式。例如,的引理是

使用 StanfordLemmatizer 类

我们将使用带有管道的StanfordCoreNLP类来演示术语化。我们首先用四个标注器设置管道,包括lemma,如下所示:

StanfordCoreNLP pipeline; 
Properties props = new Properties(); 
props.put("annotators", "tokenize, ssplit, pos, lemma"); 
pipeline = new StanfordCoreNLP(props);

这些注释器是必需的,解释如下:

注释者 要执行的操作
tokenize 标记化
ssplit 分句
pos 词性标注
lemma 词汇化
ner NER
parse 句法分析
dcoref 共指消解

一个paragraph变量与Annotation构造函数一起使用,然后执行annotate方法,如下所示:

String paragraph = "Similar to stemming is Lemmatization. "  
    +"This is the process of finding its lemma, its form " +  
    +"as found in a dictionary."; 
Annotation document = new Annotation(paragraph); 
pipeline.annotate(document); 

我们现在需要迭代语句和语句的标记。AnnotationCoreMap class' get方法将返回指定类型的值。如果没有指定类型的值,它将返回null。我们将使用这些类来获得一个引理列表。

首先,返回一个句子列表,然后处理每个句子的每个单词以找到词条。这里声明了sentenceslemmas的列表:

List<CoreMap> sentences = 
     document.get(SentencesAnnotation.class); 
List<String> lemmas = new LinkedList<>(); 

两个 for-each 语句迭代语句来填充lemmas列表。
完成后,将显示列表:

for (CoreMap sentence : sentences) { 
    for (CoreLabelword : sentence.get(TokensAnnotation.class)) { 
        lemmas.add(word.get(LemmaAnnotation.class)); 
    } 
} 

System.out.print("[");
for (String element : lemmas) { 
    System.out.print(element + " "); 
} 
System.out.println("]"); 

该序列的输出如下:

    [similar to stem be lemmatization . this be the process of find its lemma , its form as find in a dictionary . ]

将它与原始测试进行比较,我们可以看到它做得非常好:

    Similar to stemming is Lemmatization. This is the process of finding its lemma, its form as found in a dictionary. 

在 OpenNLP 中使用词汇化

OpenNLP 还支持使用JWNLDictionary类的词汇化。此类的构造函数使用一个字符串,该字符串包含用于标识根的字典文件的路径。我们将使用普林斯顿大学(wordnet.princeton.edu)开发的 WordNet 字典。实际的字典是存储在目录中的一系列文件。这些文件包含单词列表和它们的词根。对于本节中使用的示例,我们将使用在code.google.com/p/xssm/downloads/detail?找到的词典 name = similarityutils . zip&can = 2&q =

JWNLDictionary class' getLemmas方法传递我们想要处理的单词和第二个参数,该参数指定单词的位置。如果我们想要准确的结果,那么词性匹配实际的单词类型是很重要的。

在下面的代码序列中,我们使用以\dict\结尾的路径创建了一个JWNLDictionary类的实例。这是字典的位置。我们还定义了样本文本。构造函数可以抛出IOExceptionJWNLException,我们在try...catch块序列中处理它们:

try { 
    dictionary = new JWNLDictionary("...\dict\"); 
    paragraph = "Eat, drink, and be merry, for life is but a dream"; 
    ... 
} catch (IOException | JWNLException ex) 
    // 
}

在文本初始化之后,添加以下语句。首先,我们使用WhitespaceTokenizer类对字符串进行标记,正如在使用 WhitespaceTokenizer 类一节中所解释的。然后,将每个令牌传递给getLemmas方法,用一个空字符串作为 POS 类型。然后显示原始令牌及其lemmas:

String tokens[] = 
     WhitespaceTokenizer.INSTANCE.tokenize(paragraph); 
for (String token : tokens) { 
    String[] lemmas = dictionary.getLemmas(token, ""); 
    for (String lemma : lemmas) { 
        System.out.println("Token: " + token + "  Lemma: " 
             + lemma); 
    } 
} 

输出如下所示:

Token: Eat,  Lemma: at
Token: drink,  Lemma: drink
Token: be  Lemma: be
Token: life  Lemma: life
Token: is  Lemma: is
Token: is  Lemma: i
Token: a  Lemma: a
Token: dream  Lemma: dream  

除了返回两个引理的is标记之外,引理化过程工作得很好。第二个无效。这说明了为令牌使用正确 POS 的重要性。我们可以使用一个或多个 POS 标签作为getLemmas方法的参数。然而,这回避了一个问题:我们如何确定正确的位置?这个话题在第五章、检测词性中详细讨论。

下表列出了 POS 标签的简短列表。本榜单改编自www . ling . upenn . edu/courses/Fall _ 2003/ling 001/Penn _ tree bank _ pos . html。宾夕法尼亚大学树库标签集的完整列表可以在 http://www.comp.leeds.ac.uk/ccalas/tagsets/upenn.html找到:

标签 描述
姐姐(网络用语)ˌ法官ˌ裁判员(judges) 形容词
神经网络 名词,单数,还是复数
NNS Noun, plural
NNP 专有名词,单数
NNPS 专有名词,复数
刷卡机 所有格结尾
富含血小板血浆 人称代词
副词
菲律宾共和国 颗粒
动词 动词,基本形式
VBD 动词,过去式
VBG 动词、动名词或现在分词

使用管道进行规范化

在这一节中,我们将使用管道结合许多规范化技术。为了演示这个过程,我们将扩展在中使用 LingPipe 一节中使用的例子来删除停用词。我们将添加两个额外的工厂来规范化文本:LowerCaseTokenizerFactoryPorterStemmerTokenizerFactory

EnglishStopTokenizerFactory创建之前添加LowerCaseTokenizerFactory工厂,在EnglishStopTokenizerFactory创建之后添加PorterStemmerTokenizerFactory,如下图:

paragraph = "A simple approach is to create a class " 
     + "to hold and remove stopwords."; 
TokenizerFactory factory = 
     IndoEuropeanTokenizerFactory.INSTANCE; 
factory = new LowerCaseTokenizerFactory(factory); 
factory = new EnglishStopTokenizerFactory(factory); 
factory = new PorterStemmerTokenizerFactory(factory); 
Tokenizer tokenizer = 
     factory.tokenizer(paragraph.toCharArray(), 0, 
     paragraph.length()); 
for (String token : tokenizer) { 
    System.out.println(token); 
} 

输出如下所示:

simpl
approach
creat
class
hold
remov
stopword
.  

我们剩下的是去掉了停用词的小写单词的词干。

摘要

在这一章中,我们举例说明了标记文本和对文本执行规范化的各种方法。我们从基于核心 Java 类的简单标记化技术开始,比如String类的split方法和StringTokenizer类。当我们决定放弃使用 NLP API 类时,这些方法会很有用。

我们演示了如何使用 OpenNLP、Stanford 和 LingPipe APIs 执行标记化。我们发现,在如何执行标记化以及可以在这些 API 中应用的选项方面存在差异。提供了它们产出的简要比较。

讨论了规范化,这可能涉及到将字符转换为小写、扩展缩写、删除停用词、词干和词汇化。我们展示了如何使用核心 Java 类和 NLP APIs 来应用这些技术。

在下一章第三章、寻找句子、中,我们将研究使用各种 NLP APIs 确定句子结尾所涉及的问题。

三、搜索语句

将文本分割成句子也叫句子边界消歧()。这个过程对于许多下游的需要在句子中分析的 NLP 任务是有用的;例如,词性和短语分析通常在一个句子中进行。

**在这一章中,我们将解释为什么 SBD 是困难的。然后,我们将研究一些在某些情况下可能有效的核心 Java 方法,并继续讨论各种 NLP APIs 对模型的使用。我们还将研究句子检测模型的训练和验证方法。我们可以添加额外的规则来进一步优化这个过程,但是这只能在一定程度上起作用。之后,模型必须被训练来处理普通和特殊的情况。本章的后半部分着重于这些模型及其使用。

我们将在本章中讨论以下主题:

  • SBD 进程
  • 是什么让 SBD 变得困难?
  • 使用 NLP APIs
  • 训练句子检测器模型

SBD 进程

SBD 过程依赖于语言,并且通常不简单。检测句子的常见方法包括使用一组规则或训练一个模型来检测它们。下面是一组检测句子的简单规则。如果下列条件为真,则检测到句子结束:

  • 文本以句点、问号或感叹号结束
  • 句点前面没有缩写,后面也没有数字

虽然这对于大多数句子来说很有效,但并不是对所有的句子都有效。例如,确定什么是缩写并不总是容易的,像省略号这样的序列可能会与句号混淆。

大多数搜索引擎并不关心 SBD。他们只对查询的标记及其位置感兴趣。执行数据提取的词性标注和其他 NLP 任务将经常处理单个句子。句子边界的检测将有助于分离看起来可能跨越句子的短语。例如,考虑下面的句子:

“建设过程结束了。盖房子的那座小山很矮。”

如果我们正在搜索短语 over the hill ,我们会不经意地在这里找到它。

本章中的许多例子将使用下面的文字来演示 SBD。这篇课文由三个简单的句子组成,后面跟着一个更复杂的句子:

private static String paragraph = "When determining the end of sentences " 
    + "we need to consider several factors. Sentences may end with " 
    + "exclamation marks! Or possibly questions marks? Within " 
    + "sentences we may find numbers like 3.14159, abbreviations " 
    + "such as found in Mr. Smith, and possibly ellipses either " 
    + "within a sentence ..., or at the end of a sentence..."; 

是什么让 SBD 变得困难?

将文本分解成句子很困难,原因有很多:

  • 标点符号经常含糊不清
  • 缩写通常包含句点
  • 通过使用引号,句子可以相互嵌入
  • 对于更专业的文本,比如 tweets 和聊天会话,我们可能需要考虑使用新行或完成从句

标点歧义最好用句号来说明。它经常被用来区分一个句子的结尾。然而,它也可以用在许多其他上下文中,包括缩写、数字、电子邮件地址和省略号。其他标点符号,如问号和感叹号,也用在嵌入的引号和特殊文本中,如可能在文档中的代码。

句点用于多种情况:

  • 终止一项判决
  • 以缩写结尾
  • 结束一个缩写并结束一个句子
  • 对于省略号
  • 对于句末的省略号
  • 嵌入在引号或括号中

我们遇到的大多数句子都以句号结尾。这使得它们易于识别。然而,当它们以缩写结尾时,识别它们就有点困难了。以下句子包含带句点的缩写:

"史密斯夫妇去参加舞会了."

在下面的两个句子中,我们有一个出现在句末的缩写:

"他是中央情报局的特工。"

"他是中央情报局的特工。"

在最后一句中,缩写的每个字母后面都有一个句点。虽然不常见,但这可能会发生,我们不能简单地忽视它。

另一个让 SBD 感到困难的问题是试图确定一个单词是否是一个缩写。我们不能简单地把所有的大写序列都当成缩写。也许用户不小心输入了一个全部大写的单词,或者文本被预处理以将所有字符转换成小写。此外,一些缩写由一系列大写和小写字母组成。为了处理缩写,有时会使用有效缩写的列表。然而,缩写通常是特定领域的。

省略号会使问题更加复杂。它们可能是单个字符(扩展 ASCII 0 x 85 或 Unicode (U+2026))或三个句点的序列。此外,还有 Unicode 水平省略号(U+2026)、垂直省略号(U+22EE)以及垂直和水平省略号的表示形式(U+FE19)。除了这些,还有 HTML 编码。对于 Java,使用\uFE19。编码上的这些变化说明了在分析文本之前对其进行良好预处理的必要性。

下面两个句子说明了省略号的可能用法:

“然后就有了...一个。”

"这份名单还在继续,而且……"

第二句以省略号结尾。在某些情况下,正如《司法协助手册》(www.mlahandbook.org/fragment/public_index)所建议的,我们可以使用括号来区分添加的省略号和原文本中的省略号,如下所示:

“人民[...使用各种交通工具...]" ( 少年 73 )。

我们还会发现嵌入在另一个句子中的句子,比如:

那人说:“那不对。”

感叹号和问号代表其他问题,即使这些字符的出现比句点更有限。感叹号可以出现在句尾以外的地方。在某些词的情况下,比如 Yahoo!感叹号是单词的一部分。此外,多个感叹号用于强调,如“最美好的祝愿!!"这可以导致识别实际上不存在的多个句子。

理解 LingPipe 的 HeuristicSentenceModel 类的 SBD 规则

还有其他规则可以用来执行 SBD。LingPipe 的HeuristicSentenceModel类使用一系列令牌规则来执行 SBD。我们将在这里展示它们,因为它们提供了对哪些规则有用的洞察。

这个类使用三组标记和两个标志来帮助这个过程:

  • 可能的停顿:这是一组标记,可以是一个句子的最后一个标记
  • 不可能的倒数第二个单词:这些单词不能是句子中倒数第二个单词
  • 不可能开始:这是一组不能用来开始一个句子的标记
  • 平衡括号:该标志表示在一个句子中所有匹配的括号都匹配之前,该句子不应被终止
  • Force final boundary :这指定输入流中的最后一个标记应该被视为语句结束符,即使它不是一个可能的终止符

平衡括号包括()和[]。但是,如果文本格式不正确,此规则将失败。下表列出了默认令牌集:

| 可能的停靠点 | 不可能的倒数第二名 | 不可能的开始 |
| 。 | 任何一个字母 | 闭括号 |
| .. | 个人和专业头衔、军衔等等 | , |
| ! | 逗号、冒号和引号 | ; |
| ? | 常见缩写 | : |
| " | 方向 | - |
| '' | 公司标志 | - |
| ). | 时间、月份等等 | - |
| | 美国政党 | % |
| | 美国各州(不是我或我所在的州) | " |
| | 运货条款 | |
| | 地址缩写 | |

尽管 LingPipe 的HeuristicSentenceModel类使用了这些规则,但是没有理由说它们不能在 SBD 工具的其他实现中使用。

SBD 的启发式方法可能不总是像其他技术一样准确。然而,它们可能在特定的领域中工作,并且通常具有更快和使用更少内存的优势。

简单 Java SBDs

有时,文本可能足够简单,Java 核心支持就足够了。有两种方法可以执行 SBD:使用正则表达式和使用BreakIterator类。我们将在这里研究这两种方法。

使用正则表达式

正则表达式可能很难理解。虽然简单的表达式通常不是问题,但是随着它们变得越来越复杂,它们的可读性也会变差。当试图将正则表达式用于 SBD 时,这是正则表达式的局限性之一。

我们将给出两种不同的正则表达式。第一个表达式很简单,但是做得不太好。它展示了一个对于某些问题领域来说可能过于简单的解决方案。第二个更复杂,做得更好。

在本例中,我们创建了一个匹配句点、问号和感叹号的正则表达式类。String class' split方法用于将文本拆分成句子:

String simple = "[.?!]"; 
String[] splitString = (paragraph.split(simple)); 
for (String string : splitString) { 
    System.out.println(string); 
}

输出如下所示:

    When determining the end of sentences we need to consider several factors
     Sentences may end with exclamation marks
     Or possibly questions marks
     Within sentences we may find numbers like 3
    14159, abbreviations such as found in Mr
     Smith, and possibly ellipses either within a sentence ..., or at the end of a sentence...

正如所料,该方法将段落分割成字符,而不管它们是数字还是缩写的一部分。

第二种方法会产生更好的结果。这个例子改编自stack overflow . com/questions/5553410/regular-expression-match-a-sentence上的一个例子。使用了编译以下正则表达式的Pattern类:

    [^.!?\s][^.!?]*(?:.!?[^.!?]*)*[.!?]?['"]?(?=\s|$)

以下代码序列中的注释解释了每个部分的含义:

Pattern sentencePattern = Pattern.compile( 
    "# Match a sentence ending in punctuation or EOS.\n" 
    + "[^.!?\\s]    # First char is non-punct, non-ws\n" 
    + "[^.!?]*      # Greedily consume up to punctuation.\n" 
    + "(?:          # Group for unrolling the loop.\n" 
    + "  [.!?]      # (special) inner punctuation ok if\n" 
    + "  (?!['\"]?\\s|$)  # not followed by ws or EOS.\n" 
    + "  [^.!?]*    # Greedily consume up to punctuation.\n" 
    + ")*           # Zero or more (special normal*)\n" 
    + "[.!?]?       # Optional ending punctuation.\n" 
    + "['\"]?       # Optional closing quote.\n" 
    + "(?=\\s|$)", 
    Pattern.MULTILINE | Pattern.COMMENTS); 

使用在regexper.com/找到的显示工具可以生成该表达式的另一种表示。如下图所示,它以图形方式描述了表达式,并阐明了其工作原理:

对示例段落执行matcher方法,然后显示结果:

Matcher matcher = sentencePattern.matcher(paragraph); 
while (matcher.find()) { 
    System.out.println(matcher.group()); 
} 

输出如下。保留了句子终止符,但缩写仍然存在问题:

    When determining the end of sentences we need to consider several factors.
    Sentences may end with exclamation marks!
    Or possibly questions marks?
    Within sentences we may find numbers like 3.14159, abbreviations such as found in Mr.
    Smith, and possibly ellipses either within a sentence ..., or at the end of a sentence...

使用 BreakIterator 类

BreakIterator类可以用来检测各种文本边界,比如字符、单词、句子和行之间的边界。不同的方法用于创建不同的BreakIterator类实例,如下所示:

  • 对于字符,使用getCharacterInstance方法
  • 对于单词,使用getWordInstance方法
  • 对于句子,使用getSentenceInstance方法
  • 对于线,使用getLineInstance方法

检测字符之间的分隔符有时很重要,例如,当我们需要处理由多个 Unicode 字符组成的字符时,比如ü。该字符有时由\u0075 (u)和\u00a8 ( ) Unicode 字符组合而成。该类将识别这些类型的字符。这种能力在 https://docs.oracle.com/javase/tutorial/i18n/text/char.html有更详细的说明。

BreakIterator类可以用来检测一个句子的结尾。它使用引用当前边界的光标。它支持一个next和一个previous方法,分别在文本中向前和向后移动光标。BreakIterator有一个受保护的默认构造函数。要获得一个BreakIterator类的实例来检测句子的结尾,使用静态的getSentenceInstance方法,如下所示:

BreakIterator sentenceIterator = 
 BreakIterator.getSentenceInstance(); 

还有一个方法的重载版本。它将一个Locale实例作为参数:

Locale currentLocale = new Locale("en", "US"); 
BreakIterator sentenceIterator =  
    BreakIterator.getSentenceInstance(currentLocale); 

一旦创建了一个实例,setText方法将把文本关联到
,用迭代器进行处理:

sentenceIterator.setText(paragraph); 

BreakIterator使用一系列方法和字段识别文本中的边界。所有这些函数都返回整数值,下表对它们进行了详细说明:

| 方法 | 用途 |
| first | 返回文本的第一个边界 |
| next | 返回当前边界之后的边界 |
| previous | 返回当前边界之前的边界 |
| DONE | 最后一个整数,赋值为-1(表示没有边界可寻) |

为了以连续的方式使用迭代器,使用first方法识别第一个边界,然后重复调用next方法来寻找后续的边界。当DONE返回时,过程终止。下面的代码序列说明了这种技术,它使用了之前声明的sentenceIterator实例:

int boundary = sentenceIterator.first(); 
while (boundary != BreakIterator.DONE) { 
    int begin = boundary; 
    System.out.print(boundary + "-"); 
    boundary = sentenceIterator.next(); 
    int end = boundary; 
    if (end == BreakIterator.DONE) { 
        break; 
    } 
    System.out.println(boundary + " [" 
        + paragraph.substring(begin, end) + "]"); 
} 

在执行时,我们得到以下输出:

    0-75 [When determining the end of sentences we need to consider several factors. ]
    75-117 [Sentences may end with exclamation marks! ]
    117-146 [Or possibly questions marks? ]
    146-233 [Within sentences we may find numbers like 3.14159 , abbreviations such as found in Mr. ]
    233-319 [Smith, and possibly ellipses either within a sentence ... , or at the end of a sentence...]
    319-

该输出适用于简单的句子,但不适用于更复杂的句子。

正则表达式和BreakIterator类的使用都有局限性。它们对于由相对简单的句子组成的文本很有用。然而,当文本变得更加复杂时,最好使用 NLP APIs,这将在下一节中讨论。

使用 NLP APIs

有许多支持 SBD 的 NLP API 类。一些是基于规则的,而另一些则使用使用常见和不常见文本训练的模型。我们将使用 OpenNLP、Stanford 和 LingPipe APIs 说明句子检测类的用法。

模型也可以被训练。关于这种方法的讨论在训练句子检测器模型一节中进行了说明。在处理专业文本(如医学或法律文本)时,需要专业模型。

使用 OpenNLP

OpenNLP 使用模型来执行 SBD。基于一个模型文件,创建了一个SentenceDetectorME类的实例。通过sentDetect方法返回句子,通过sentPosDetect方法返回位置信息。

使用 SentenceDetectorME 类

使用SentenceModel类从文件中加载模型。然后使用该模型创建一个SentenceDetectorME类的实例,并调用sentDetect方法来执行 SBD。该方法返回一个字符串数组,每个元素包含一个句子。

下面的示例演示了这一过程。try-with-resources 块用于打开包含模型的en-sent.bin文件。然后,处理paragraph字符串。接下来,捕捉各种 IO 类型异常(如有必要)。最后,使用 for-each 语句来显示句子:

try (InputStream is = new FileInputStream( 
        new File(getModelDir(), "en-sent.bin"))) { 
    SentenceModel model = new SentenceModel(is); 
    SentenceDetectorME detector = new SentenceDetectorME(model); 
    String sentences[] = detector.sentDetect(paragraph); 
    for (String sentence : sentences) { 
        System.out.println(sentence); 
    } 
} catch (FileNotFoundException ex) { 
    // Handle exception 
} catch (IOException ex) { 
    // Handle exception 
}

在执行时,我们得到以下输出:

    When determining the end of sentences we need to consider several factors.
    Sentences may end with exclamation marks!
    Or possibly questions marks?
    Within sentences we may find numbers like 3.14159, abbreviations such as found in Mr. Smith, and possibly ellipses either within a sentence ..., or at the end of a sentence...

这一段的输出效果很好。它既能捕捉简单的句子,也能捕捉更复杂的句子。当然,经过处理的文本并不总是完美的。下面的段落在某些地方有多余的空格,但在需要的地方缺少空格。此问题可能出现在聊天会话分析中:

paragraph = " This sentence starts with spaces and ends with "  
    + "spaces . This sentence has no spaces between the next " 
    + "one.This is the next one."; 

当我们在前面的例子中使用这一段时,我们得到下面的输出:

    This sentence starts with spaces and ends with spaces  .
    This sentence has no spaces between the next one.This is the next one.

第一句的前导空格被删除,但结尾空格没有删除。第三句没有检测出来,和第二句合并了。

getSentenceProbabilities方法返回一个 doubles 数组,表示从最后一次使用sentDetect方法中检测到的句子的置信度。在显示句子的 for-each 语句后添加以下代码:

double probablities[] = detector.getSentenceProbabilities(); 
for (double probablity : probablities) { 
    System.out.println(probablity); 
} 

通过执行原始段落,我们得到以下输出:

    0.9841708738988814
    0.908052385070974
    0.9130082376342675
    1.0

显示的数字是表示置信度的概率。

使用 sentPosDetect 方法

SentenceDetectorME类拥有一个为每个句子返回Span对象的sentPosDetect方法。使用与上一节相同的代码,除了两处更改:用sentPosDetect方法替换sentDetect方法,用这里使用的方法替换 for-each 语句:

Span spans[] = detector.sentPosDetect(paragraph); 
for (Span span : spans) { 
    System.out.println(span); 
} 

接下来的输出使用原始段落。Span对象包含默认执行toString方法返回的位置信息:

    [0..74)
    [75..116)
    [117..145)
    [146..317)  

Span类拥有许多方法。下面的代码序列演示了如何使用getStartgetEnd方法来清楚地显示这些跨度所代表的文本:

for (Span span : spans) { 
    System.out.println(span + "[" + paragraph.substring( 
        span.getStart(), span.getEnd()) +"]"); 
} 

输出显示识别的句子:

     [0..74)[When determining the end of sentences we need to consider several factors.]
    [75..116)[Sentences may end with exclamation marks!]
    [117..145)[Or possibly questions marks?]
    [146..317)[Within sentences we may find numbers like 3.14159, abbreviations such as found in Mr. Smith, and possibly ellipses either within a sentence ..., or at the end of a sentence...]

还有许多其他有价值的方法。下表列出了这些功能:

| 方法 | 意为 |
| contains | 确定另一个Span对象或索引是否包含在目标中的重载方法 |
| crosses | 确定两个跨度是否重叠 |
| length | 跨度的长度 |
| startsWith | 确定跨度是否从目标跨度开始 |

使用斯坦福 API

斯坦福 NLP 库支持几种用于执行句子检测的技术。在本节中,我们将使用以下类来演示这一过程:

  • PTBTokenizer
  • DocumentPreprocessor
  • StanfordCoreNLP

尽管它们都执行 SBD,但每个都使用不同的方法来执行流程。

使用 PTBTokenizer 类

PTBTokenizer类使用规则来执行 SBD,并有多种标记化选项。这个类的构造函数拥有三个参数:

  • 封装要处理的文本的Reader
  • 实现LexedTokenFactory接口的对象
  • 保存标记化选项的字符串

这些选项允许我们指定文本、要使用的标记器以及我们可能需要用于特定文本流的任何选项。

在下面的代码序列中,创建了一个StringReader类的实例来封装文本。在本例中,CoreLabelTokenFactory类与选项一起使用,剩下的选项为null:

PTBTokenizer ptb = new PTBTokenizer(new StringReader(paragraph), 
     new CoreLabelTokenFactory(), null); 

我们将使用WordToSentenceProcessor类创建一个List类的List实例来保存句子及其标记。它的process方法使用由PTBTokenizer实例产生的令牌来创建List类的列表,如下所示:

WordToSentenceProcessor wtsp = new WordToSentenceProcessor(); 
List<List<CoreLabel>> sents = wtsp.process(ptb.tokenize());

List类的该List实例可以通过多种方式显示。在下面的序列中,List类的toString方法显示括在括号中的列表,其元素用逗号分隔:

for (List<CoreLabel> sent : sents) { 
    System.out.println(sent); 
} 

该序列的输出产生以下内容:

    [When, determining, the, end, of, sentences, we, need, to, consider, several, factors, .]
    [Sentences, may, end, with, exclamation, marks, !]
    [Or, possibly, questions, marks, ?]
    [Within, sentences, we, may, find, numbers, like, 3.14159, ,, abbreviations, such, as, found, in, Mr., Smith, ,, and, possibly, ellipses, either, within, a, sentence, ..., ,, or, at, the, end, of, a, sentence, ...]  

此处显示的另一种方法是在单独的行上显示每个句子:

for (List<CoreLabel> sent : sents) { 
    for (CoreLabel element : sent) { 
        System.out.print(element + " "); 
     } 
    System.out.println(); 
} 

输出如下所示:

    When determining the end of sentences we need to consider several factors . 
    Sentences may end with exclamation marks ! 
    Or possibly questions marks ? 
    Within sentences we may find numbers like 3.14159 , abbreviations such as found in Mr. Smith , and possibly ellipses either within a sentence ... , or at the end of a sentence ... 

如果我们只对单词和句子的位置感兴趣,我们可以使用endPosition方法,如下所示:

for (List<CoreLabel> sent : sents) { 
    for (CoreLabel element : sent) { 
        System.out.print(element.endPosition() + " "); 
     } 
    System.out.println(); 
} 

当执行这个命令时,我们得到以下输出。每行的最后一个数字是句子边界的索引:

    4 16 20 24 27 37 40 45 48 57 65 73 74 
    84 88 92 97 109 115 116 
    119 128 138 144 145 
    152 162 165 169 174 182 187 195 196 210 215 218 224 227 231 237 238 242 251 260 267 274 276 285 287 288 291 294 298 302 305 307 316 317

每个句子的第一个元素及其索引按以下顺序显示:

for (List<CoreLabel> sent : sents) { 
    System.out.println(sent.get(0) + " "  
        + sent.get(0).beginPosition()); 
} 

输出如下所示:

    When 0
    Sentences 75
    Or 117
    Within 146

如果我们对一个句子的最后成分感兴趣,我们可以用下面的顺序。列表元素的数量用于显示终止字符及其结束位置:

for (List<CoreLabel> sent : sents) { 
    int size = sent.size(); 
    System.out.println(sent.get(size-1) + " "  
        + sent.get(size-1).endPosition()); 
} 

这将产生以下输出:

    . 74
    ! 116
    ? 145
    ... 317  

当调用PTBTokenizer类的构造函数时,有许多选项可用。这些选项包含在构造函数的第三个参数中。选项字符串由逗号分隔的选项组成,如下所示:

"americanize=true,normalizeFractions=true,asciiQuotes=true".

下表列出了这些选项中的几个:

| 选项 | 意为 |
| invertible | 用于指示必须保留标记和空白,以便可以重建原始字符串 |
| tokenizeNLs | 指示行尾必须被视为标记 |
| americanize | 如果是真的,这将把英式拼写改写成美式拼写 |
| normalizeAmpersandEntity | 将 XML & amp 字符转换为& amp 符号 |
| normalizeFractions | 将常见的分数字符(如)转换为长格式(1/2) |
| asciiQuotes | 会将引号字符转换为更简单的“和”字符 |
| unicodeQuotes | 会将引号字符转换为范围从 U+2018 到 U+201D 的字符 |

以下序列说明了此选项字符串的用法:

paragraph = "The colour of money is green. Common fraction " 
    + "characters such as ½  are converted to the long form 1/2\. " 
    + "Quotes such as "cat" are converted to their simpler form."; 
ptb = new PTBTokenizer( 
    new StringReader(paragraph), new CoreLabelTokenFactory(), 
    "americanize=true,normalizeFractions=true,asciiQuotes=true"); 
wtsp = new WordToSentenceProcessor(); 
sents = wtsp.process(ptb.tokenize()); 
for (List<CoreLabel> sent : sents) { 
    for (CoreLabel element : sent) { 
        System.out.print(element + " "); 
    } 
    System.out.println(); 
} 

输出如下所示:

    The color of money is green . 
    Common fraction characters such as 1/2 are converted to the long form 1/2 . 
    Quotes such as " cat " are converted to their simpler form . 

“colour”一词的英国拼法被转换成了美国的对应拼法。分数½展开为三个字符:1/2。在最后一句中,智能引号被转换成了更简单的形式。

使用 document 预处理程序类

DocumentPreprocessor类的一个实例被创建时,它使用它的Reader参数产生一个句子列表。它还实现了Iterable接口,这使得遍历列表变得很容易。

在下面的示例中,该段落用于创建一个StringReader对象,该对象用于实例化DocumentPreprocessor实例:

Reader reader = new StringReader(paragraph); 
DocumentPreprocessor dp = new DocumentPreprocessor(reader); 
for (List sentence : dp) { 
    System.out.println(sentence); 
} 

在执行时,我们得到以下输出:

    [When, determining, the, end, of, sentences, we, need, to, consider, several, factors, .]
    [Sentences, may, end, with, exclamation, marks, !]
    [Or, possibly, questions, marks, ?]
    [Within, sentences, we, may, find, numbers, like, 3.14159, ,, abbreviations, such, as, found, in, Mr., Smith, ,, and, possibly, ellipses, either, within, a, sentence, ..., ,, or, at, the, end, of, a, sentence, ...]  

默认情况下,PTBTokenizer用于标记输入。setTokenizerFactory方法可以用来指定一个不同的记号赋予器。还有其他几种有用的方法,如下表所示:

| 方法 | 目的 |
| setElementDelimiter | 它的参数指定了一个 XML 元素。只会处理这些元素中的文本。 |
| setSentenceDelimiter | 处理器将假设字符串参数是一个句子分隔符。 |
| setSentenceFinalPuncWords | 它的字符串数组参数指定了句子的结束分隔符。 |
| setKeepEmptySentences | 当与空白模型一起使用时,如果它的参数是true,空句将被保留。 |

该类可以处理纯文本或 XML 文档。

为了演示如何处理 XML 文件,我们将创建一个名为XMLText.xml的简单 XML 文件,其中包含以下数据:

<?xml version="1.0" encoding="UTF-8"?> 
<?xml-stylesheet type="text/xsl"?> 
<document> 
    <sentences> 
        <sentence id="1"> 
            <word>When</word> 
            <word>the</word> 
            <word>day</word> 
            <word>is</word> 
            <word>done</word> 
            <word>we</word> 
            <word>can</word> 
            <word>sleep</word> 
            <word>.</word> 
        </sentence> 
        <sentence id="2"> 
            <word>When</word> 
            <word>the</word> 
            <word>morning</word> 
            <word>comes</word> 
            <word>we</word> 
            <word>can</word> 
            <word>wake</word> 
            <word>.</word> 
        </sentence> 
        <sentence id="3"> 
            <word>After</word> 
            <word>that</word> 
            <word>who</word> 
            <word>knows</word> 
            <word>.</word> 
        </sentence> 
    </sentences> 
</document> 

我们将重用前面例子中的代码。但是,我们将打开XMLText.xml文件,并使用DocumentPreprocessor.DocType.XML作为DocumentPreprocessor类的构造函数的第二个参数,如下面的代码所示。这将指定处理器应该将文本视为 XML 文本。此外,我们将指定只处理那些在<sentence>标记内的 XML 元素:

try { 
    Reader reader = new FileReader("XMLText.xml"); 
    DocumentPreprocessor dp = new DocumentPreprocessor( 
        reader, DocumentPreprocessor.DocType.XML); 
    dp.setElementDelimiter("sentence"); 
    for (List sentence : dp) { 
        System.out.println(sentence); 
    } 
} catch (FileNotFoundException ex) { 
    // Handle exception 
} 

该示例的输出如下:

    [When, the, day, is, done, we, can, sleep, .] 
    [When, the, morning, comes, we, can, wake, .]
    [After, that, who, knows, .]  

使用ListIterator可以得到更清晰的输出,如下所示:

for (List sentence : dp) { 
    ListIterator list = sentence.listIterator(); 
     while (list.hasNext()) { 
        System.out.print(list.next() + " "); 
    } 
    System.out.println(); 
} 

它的输出如下:

    When the day is done we can sleep . 
    When the morning comes we can wake . 
    After that who knows . 

如果我们没有指定元素分隔符,每个单词将显示如下:

    [When]
    [the]
    [day]
    [is]
    [done]
    ...
    [who]
    [knows]
    [.]

使用 StanfordCoreNLP 类

StanfordCoreNLP类支持使用ssplit注释器进行句子检测。在下面的例子中,使用了tokenizessplit标注器。创建一个管道对象,并对管道应用annotate方法,使用段落作为其参数:

Properties properties = new Properties(); 
properties.put("annotators", "tokenize, ssplit"); 
StanfordCoreNLP pipeline = new StanfordCoreNLP(properties); 
Annotation annotation = new Annotation(paragraph); 
pipeline.annotate(annotation); 

输出包含大量信息。这里只显示了第一行的输出:

    Sentence #1 (13 tokens):
    When determining the end of sentences we need to consider several factors.
    [Text=When CharacterOffsetBegin=0 CharacterOffsetEnd=4] [Text=determining CharacterOffsetBegin=5 CharacterOffsetEnd=16] [Text=the CharacterOffsetBegin=17 CharacterOffsetEnd=20] [Text=end CharacterOffsetBegin=21 CharacterOffsetEnd=24] [Text=of CharacterOffsetBegin=25 CharacterOffsetEnd=27] [Text=sentences CharacterOffsetBegin=28 CharacterOffsetEnd=37] [Text=we CharacterOffsetBegin=38 CharacterOffsetEnd=40] [Text=need CharacterOffsetBegin=41 CharacterOffsetEnd=45] [Text=to CharacterOffsetBegin=46 CharacterOffsetEnd=48] [Text=consider CharacterOffsetBegin=49 CharacterOffsetEnd=57] [Text=several CharacterOffsetBegin=58 CharacterOffsetEnd=65] [Text=factors CharacterOffsetBegin=66 CharacterOffsetEnd=73] [Text=. CharacterOffsetBegin=73 CharacterOffsetEnd=74] 

或者,我们可以使用xmlPrint方法。这将产生 XML 格式的输出,这通常更容易提取感兴趣的信息。
这里展示了这个方法,它需要处理IOException:

try { 
    pipeline.xmlPrint(annotation, System.out); 
} catch (IOException ex) { 
    // Handle exception 
}

部分输出如下所示:

<?xml version="1.0" encoding="UTF-8"?> 
<?xml-stylesheet href="CoreNLP-to-HTML.xsl" type="text/xsl"?> 
<root> 
  <document> 
    <sentences> 
      <sentence id="1"> 
        <tokens> 
          <token id="1"> 
            <word>When</word> 
            <CharacterOffsetBegin>0</CharacterOffsetBegin> 
            <CharacterOffsetEnd>4</CharacterOffsetEnd> 
          </token> 
... 
         <token id="34"> 
            <word>...</word> 
            <CharacterOffsetBegin>316</CharacterOffsetBegin> 
            <CharacterOffsetEnd>317</CharacterOffsetEnd> 
          </token> 
        </tokens> 
      </sentence> 
    </sentences> 
  </document> 
</root> 

使用 LingPipe

LingPipe 使用类的层次结构来支持 SBD,如下图所示:

这个层次结构的基础是 AbstractSentenceModel 类,它的主要方法是一个重载的boundaryIndices方法。这个方法返回一个边界索引的整数数组,其中数组的每个元素代表一个句子边界。

从这个类派生的是 HeuristicSentenceModel 类。这个类使用一系列可能的停止、不可能的倒数第二和不可能的开始标记集。这些在前面的理解 LingPipe 的 HeuristicSentenceModel 类部分已经讨论过了。

indeuropeansentcemodelMedlineSentenceModel 类是从 HeuristicSentenceModel 类派生而来的。他们分别接受过英语培训和医学专业培训。我们将在下面的小节中演示这两个类。

使用 IndoEuropeanSentenceModel 类

IndoEuropeanSentenceModel模型用于英文文本。它的双参数构造函数将指定:

  • 最后一个令牌是否必须是一个停止符
  • 括号是否应该平衡

默认构造函数不强制最后一个标记是一个停止符,也不期望括号应该是平衡的。句子模型需要和分词器一起使用。为此,我们将使用IndoEuropeanTokenizerFactory类的默认构造函数,如下所示:

TokenizerFactory TOKENIZER_FACTORY= 
 IndoEuropeanTokenizerFactory.INSTANCE; 
com.aliasi.sentences.SentenceModel sentenceModel = new IndoEuropeanSentenceModel(); 

创建一个标记化器,并调用它的tokenize方法来填充两个列表:

List<String> tokenList = new ArrayList<>(); 
List<String> whiteList = new ArrayList<>(); 
Tokenizer tokenizer= TOKENIZER_FACTORY.tokenizer( 
    paragraph.toCharArray(),0, paragraph.length()); 
tokenizer.tokenize(tokenList, whiteList);

boundaryIndices方法返回一个整数边界索引数组。该方法需要两个包含标记和空格的String数组参数。tokenize方法为这些元素使用了两个列表。这意味着我们需要将列表转换成等价的数组,如下所示:

String[] tokens = new String[tokenList.size()]; 
String[] whites = new String[whiteList.size()]; 
tokenList.toArray(tokens); 
whiteList.toArray(whites); 

然后我们可以使用boundaryIndices方法并显示索引:

int[] sentenceBoundaries= 
 sentenceModel.boundaryIndices(tokens, whites); 
for(int boundary : sentenceBoundaries) { 
    System.out.println(boundary); 
} 

输出如下所示:

    12
    19
    24  

为了显示实际的句子,我们将使用下面的顺序。空白索引与标记相差一个:

int start = 0; 
for(int boundary : sentenceBoundaries) { 
    while(start<=boundary) { 
        System.out.print(tokenList.get(start) 
     + whiteList.get(start+1)); 
        start++; 
    } 
    System.out.println(); 
} 

以下输出是结果:

    When determining the end of sentences we need to consider several factors. 
    Sentences may end with exclamation marks! 
    Or possibly questions marks?

可惜,它漏掉了最后一句。这是因为最后一句以省略号结尾。如果我们在句尾添加一个句点,我们会得到以下输出:

    When determining the end of sentences we need to consider several factors. 
    Sentences may end with exclamation marks! 
    Or possibly questions marks? 
    Within sentences we may find numbers like 3.14159, abbreviations such as found in Mr. Smith, and possibly ellipses either within a sentence ..., or at the end of a sentence....

使用 SentenceChunker 类

另一种方法是使用SentenceChunker类来执行 SBD。这个类的构造函数需要一个TokenizerFactory对象和一个SentenceModel对象,如下所示:

TokenizerFactory tokenizerfactory = 
 IndoEuropeanTokenizerFactory.INSTANCE; 
SentenceModel sentenceModel = new IndoEuropeanSentenceModel(); 

使用tokenizerfactory
句子实例创建SentenceChunker实例:

SentenceChunker sentenceChunker =  
    new SentenceChunker(tokenizerfactory, sentenceModel); 

SentenceChunker类实现了Chunker接口,该接口使用了一个chunk方法。这个方法返回一个实现Chunking接口的对象。这个对象用一个字符序列(CharSequence)指定文本的“块”。

chunk方法使用一个字符数组和数组中的索引来指定需要处理的文本部分。一个Chunking对象是这样返回的:

Chunking chunking = sentenceChunker.chunk( 
    paragraph.toCharArray(),0, paragraph.length()); 

我们将使用Chunking对象有两个目的。首先,我们将使用它的chunkSet方法返回一组Chunk对象。然后,我们将获得一个包含所有句子的字符串:

Set<Chunk> sentences = chunking.chunkSet(); 
String slice = chunking.charSequence().toString();

一个Chunk对象存储句子边界的字符偏移量。我们将结合使用它的startend方法来显示句子,如下面的代码所示。每个元素和句子都包含句子的边界。我们使用这些信息来显示切片中的每个句子:

for (Chunk sentence : sentences) { 
    System.out.println("[" + slice.substring(sentence.start(), 
       sentence.end()) + "]"); 
} 

以下是输出。但是,对于以省略号结尾的句子,它仍然存在问题,因此在处理文本之前,在最后一句的末尾添加了一个句点。

    [When determining the end of sentences we need to consider several factors.]
    [Sentences may end with exclamation marks!]
    [Or possibly questions marks?]
    [Within sentences we may find numbers like 3.14159, abbreviations such as found in Mr. Smith, and possibly ellipses either within a sentence ..., or at the end of a sentence....]

尽管IndoEuropeanSentenceModel类对于英语文本相当适用,但对于专业文本可能并不总是适用。在下一节中,我们将检查MedlineSentenceModel类的使用,它已经被训练来处理医学文本。

使用 MedlineSentenceModel 类

LingPipe 语句模型使用的是 MEDLINE ,这是一个生物医学文献的大集合。这个集合以 XML 格式存储,由美国国家医学图书馆(【http://www.nlm.nih.gov/】??)维护。

LingPipe 使用它的MedlineSentenceModel类来执行 SBD。这个模型已经针对 MEDLINE 数据进行了训练。它使用简单的文本,并将其标记为标记和空白。然后使用 MEDLINE 模型来查找文本的句子。

在下面的例子中,我们将使用来自www.ncbi.nlm.nih.gov/pmc/articles/PMC3139422/的一段话来演示模型的使用,如这里所声明的:

paragraph = "HepG2 cells were obtained from the American Type 
 Culture "  
    + "Collection (Rockville, MD, USA) and were used only until "  
    + "passage 30\. They were routinely grown at 37°C in Dulbecco's " 
    + "modified Eagle's medium (DMEM) containing 10 % fetal bovine " 
    + "serum (FBS), 2 mM glutamine, 1 mM sodium pyruvate, and 25 " 
    + "mM glucose (Invitrogen, Carlsbad, CA, USA) in a humidified " 
    + "atmosphere containing 5% CO2\. For precursor and 13C-sugar "  
    + "experiments, tissue culture treated polystyrene 35 mm " 
    + "dishes (Corning Inc, Lowell, MA, USA) were seeded with 2 " 
    + "× 106 cells and grown to confluency in DMEM."; 

下面的代码基于SentenceChunker类,如前一节所示。不同之处在于MedlineSentenceModel类的使用:

TokenizerFactory tokenizerfactory = 
     IndoEuropeanTokenizerFactory.INSTANCE; 
MedlineSentenceModel sentenceModel = new 
     MedlineSentenceModel(); 
SentenceChunker sentenceChunker =  
    new SentenceChunker(tokenizerfactory, 
 sentenceModel); 
     = sentenceChunker.chunk( 
    paragraph.toCharArray(), 0, paragraph.length()); 
Set<Chunk> sentences = chunking.chunkSet(); 
String slice = chunking.charSequence().toString(); 
for (Chunk sentence : sentences) { 
    System.out.println("[" 
        + slice.substring(sentence.start(), 
 sentence.end())  
        + "]"); 
} 

输出如下所示:

    [HepG2 cells were obtained from the American Type Culture Collection (Rockville, MD, USA) and were used only until passage 30.]
    [They were routinely grown at 37°C in Dulbecco's modified Eagle's medium (DMEM) containing 10 % fetal bovine serum (FBS), 2 mM glutamine, 1 mM sodium pyruvate, and 25 mM glucose (Invitrogen, Carlsbad, CA, USA) in a humidified atmosphere containing 5% CO2.]
    [For precursor and 13C-sugar experiments, tissue culture treated polystyrene 35 mm dishes (Corning Inc, Lowell, MA, USA) were seeded with 2 × 106 cells and grown to confluency in DMEM.] 

当针对医学文本执行时,该模型将比其他模型执行得更好。

训练句子检测器模型

我们将用 OpenNLP 的SentenceDetectorME类来说明训练过程。这个类有一个静态的train方法,使用在文件中找到的例句。该方法返回一个模型,该模型通常被序列化为一个文件以供以后使用。

模型使用特殊的带注释的数据来清楚地指定句子的结束位置。通常,一个大文件被用来为训练目的提供一个好的样本。该文件的一部分用于训练目的,其余部分用于在模型被训练后对其进行验证。

OpenNLP 使用的训练文件每行包含一句话。通常至少需要 10 到 20 个例句来避免处理错误。为了演示这个过程,我们将使用一个名为sentence.train的文件。它由儒勒·凡尔纳的第五章《海底两万里》组成。这本书的正文可以在 http://www.gutenberg.org/files/164/164-h/164-h.htm#chap05找到。该文件可以从 https://github . com/packt publishing/Natural-Language-Processing-with Java-Second-Edition下载,也可以从本书的 GitHub 资源库下载。

一个FileReader对象用于打开文件。这个对象被用作PlainTextByLineStream构造函数的参数。产生的流由文件中每行的一个字符串组成。这被用作SentenceSampleStream构造函数的参数,它将句子字符串转换成SentenceSample对象。这些对象保存每个句子的开始索引。这个过程如下所示,其中语句被包含在一个try块中,以处理这些语句可能抛出的异常:

try { 
    ObjectStream<String> lineStream = new PlainTextByLineStream( 
        new FileReader("sentence.train")); 
    ObjectStream<SentenceSample> sampleStream 
        = new SentenceSampleStream(lineStream); 
    ... 
    } catch (FileNotFoundException ex) { 
        ex.printStackTrace();
        // Handle exception 
    } catch (IOException ex) { 
        ex.printStackTrace(); 
        // Handle exception 
} 

现在,train方法可以这样使用:

SentenceModel model = SentenceDetectorME.train("en", 
     sampleStream, true, 
    null, TrainingParameters.defaultParams());

该方法的输出是经过训练的模型。下表详细列出了该方法的参数:

| 参数 | 意为 |
| "en" | 指定
文本的语言是英语 |
| sampleStream | 训练文本流 |
| true | 指定是否应该使用显示的结束标记 |
| null | 缩略语词典 |
| TrainingParameters.defaultParams() | 指定应使用默认训练参数 |

在下面的序列中,OutputStream被创建并用于将模型保存在modelFile文件中。这使得模型可以在其他应用程序中重复使用:

OutputStream modelStream = new BufferedOutputStream( 
    new FileOutputStream("modelFile")); 
model.serialize(modelStream); 

这个过程的输出如下。为了节省空间,这里没有显示所有的迭代。默认情况下,将索引事件截止到5并将迭代次数截止到 100:

    Indexing events using cutoff of 5

        Computing event counts...  done. 93 events
        Indexing...  done.
    Sorting and merging events... done. Reduced 93 events to 63.
    Done indexing.
    Incorporating indexed data for training...  
    done.
        Number of Event Tokens: 63
            Number of Outcomes: 2
          Number of Predicates: 21
    ...done.
    Computing model parameters ...
    Performing 100 iterations.
      1:  ... loglikelihood=-64.4626877920749    0.9032258064516129
      2:  ... loglikelihood=-31.11084296202819    0.9032258064516129
      3:  ... loglikelihood=-26.418795734248626    0.9032258064516129
      4:  ... loglikelihood=-24.327956749903198    0.9032258064516129
      5:  ... loglikelihood=-22.766489585258565    0.9032258064516129
      6:  ... loglikelihood=-21.46379347841989    0.9139784946236559
      7:  ... loglikelihood=-20.356036369911394    0.9139784946236559
      8:  ... loglikelihood=-19.406935608514992    0.9139784946236559
      9:  ... loglikelihood=-18.58725539754483    0.9139784946236559
     10:  ... loglikelihood=-17.873030559849326    0.9139784946236559
     ...
     99:  ... loglikelihood=-7.214933901940582    0.978494623655914
    100:  ... loglikelihood=-7.183774954664058    0.978494623655914

使用训练好的模型

然后,我们可以使用该模型,如下面的代码序列所示。这是基于使用 SentenceDetectorME 类一节中的所阐述的技术:

try (InputStream is = new FileInputStream( 
        new File(getModelDir(), "modelFile"))) { 
    SentenceModel model = new SentenceModel(is); 
    SentenceDetectorME detector = new 
     SentenceDetectorME(model); 
    String sentences[] = detector.sentDetect(paragraph); 
    for (String sentence : sentences) { 
        System.out.println(sentence); 
    } 
} catch (FileNotFoundException ex) { 
    // Handle exception 
} catch (IOException ex) { 
    // Handle exception 
} 

输出如下所示:

    When determining the end of sentences we need to consider several factors.
    Sentences may end with exclamation marks! Or possibly questions marks?
    Within sentences we may find numbers like 3.14159,
    abbreviations such as found in Mr.
    Smith, and possibly ellipses either within a sentence ..., or at the end of a sentence...

这个模型没有很好地处理最后一句话,这反映了样本文本和模型所针对的文本之间的不匹配。使用相关的培训数据很重要。否则,基于该输出的下游任务将受到影响。

使用 SentenceDetectorEvaluator 类评估模型

出于评估的目的,我们保留了样本文件的一部分,以便我们可以使用SentenceDetectorEvaluator类来评估模型。我们修改了sentence.train文件,提取了最后 10 个句子,并将它们放在一个名为evalSample的文件中。然后,我们使用这个文件来评估模型。在下面的例子中,我们重用了lineStreamsampleStream变量来创建一个基于文件内容的SentenceSample对象流:

lineStream = new PlainTextByLineStream(
     new FileReader("evalSample")); 
sampleStream = new SentenceSampleStream(lineStream); 

使用之前创建的SentenceDetectorME类变量detector创建了一个SentenceDetectorEvaluator类的实例。构造函数的第二个参数是一个SentenceDetectorEvaluationMonitor对象,我们在这里不使用它。于是,evaluate的方法就叫做:

SentenceDetectorEvaluator sentenceDetectorEvaluator 
    = new SentenceDetectorEvaluator(detector, null); 
sentenceDetectorEvaluator.evaluate(sampleStream); 

getFMeasure方法将返回FMeasure类的一个实例,它提供了模型质量的度量:

System.out.println(sentenceDetectorEvaluator.getFMeasure()); 

输出如下。精度是包含的正确实例的分数,而召回反映了模型的敏感性。F-measure 是一个结合了召回率和准确率的分数。从本质上来说,它反映了模型运行的好坏。对于标记化和 SBD 任务,最好将精度保持在 90%以上:

    Precision: 0.8181818181818182
    Recall: 0.9
    F-Measure: 0.8571428571428572

摘要

在这一章中,我们讨论了使句子检测成为一项困难任务的许多问题,例如由用于数字和缩写的句点引起的问题。省略号和内嵌引号的使用也会有问题。

Java 提供了一些技术来检测句子的结尾。我们看到了如何使用正则表达式和BreakIterator类。这些技巧对于简单的句子很有用,但是对于更复杂的句子就没那么好用了。

还演示了各种 NLP APIs 的使用。其中一些基于规则处理文本,而另一些使用模型。我们还演示了如何训练和评估模型。

在下一章,第四章、查找人和事,你将学习如何使用文本查找人和事。**

四、搜索人和事物

寻找人和事物的过程称为命名实体识别()。诸如人和地点之类的实体与具有名称的类别相关联,这些名称标识它们是什么。一个命名的类别可以简单到。常见的实体类型包括以下几种:

*** 人

  • 位置
  • 组织
  • 金钱
  • 时间
  • 资源定位符

在文档中查找名称、位置和各种东西是重要且有用的 NLP 任务。它们被用在许多地方,例如进行简单的搜索、处理查询、解析引用、消除文本的歧义以及寻找文本的含义。例如,NER 有时只对那些属于单一类别的实体感兴趣。使用类别,搜索可以被隔离到那些项目类型。其他 NLP 任务使用 NER,例如在词性 ( 词性)标签中以及在执行交叉引用任务中。

NER 进程涉及两项任务:

  • 实体检测
  • 实体分类

检测涉及找到文本中实体的位置。一旦找到它,确定发现了什么类型的实体是很重要的。在完成这两项任务后,结果可用于解决其他任务,如搜索和确定文本的含义。例如,任务可能包括从电影或书评中识别姓名,以及帮助查找可能感兴趣的其他电影或书籍。提取位置信息可以帮助提供对附近服务的参考。

我们将在本章中讨论以下主题:

  • 为什么 NER 很难?
  • 姓名识别技术
  • 对 NER 使用正则表达式
  • 使用 NLP APIs
  • 使用 NER 注记工具构建新数据集
  • 训练模型

为什么 NER 很难?

像许多 NLP 任务一样,NER 并不总是简单的。虽然文本的标记化将揭示其组成部分,但理解它们是什么可能是困难的。由于语言的模糊性,使用专有名词并不总是有效的。例如,Penny 和 Faith 虽然是有效的名称,但也可以分别用于度量货币和信仰。我们还可以找到像乔治亚这样的词,它们被用作一个国家、一个州和一个人的名字。我们也不能列出所有的人或地方或实体,因为它们不是预先定义的。考虑下面两个简单的句子:

  • 现在工作更难找了
  • 乔布斯说点总是会连接在一起的

在这两句话中,乔布斯似乎是一个实体,但他们并不相关,在第二句话中,它甚至不是一个实体。我们需要使用一些复杂的技术来检查实体在上下文中的出现。句子可能以不同的方式使用同一个实体的名称。比方说,IBM 和国际商业机器公司;这两个术语在文本中用来指同一个实体,但对 NER 来说,这是一个挑战。再举一个例子:铃木和日产可能被 NER 解释为人名,而不是公司名。

有些短语很有挑战性。考虑短语“大都会会展大厅”可能包含本身是有效实体的单词。因此,当领域众所周知时,可以很容易地识别实体列表,并且也很容易实现。

NER 通常应用于句子级别,否则短语很容易连接句子,导致实体的错误识别。举下面两句话为例:

鲍勃去了南方。达科塔去了西部。”

如果我们忽略了句子的边界,那么我们可能会无意中找到南达科他州的位置实体。

URL、电子邮件地址和专用号码等专用文本可能很难隔离。如果我们必须考虑实体形式的变化,这种识别就变得更加困难。例如,电话号码是否使用括号?是用破折号,句号,还是其他字符来分隔它的各个部分?我们需要考虑国际电话号码吗?

这些因素促成了对良好 NER 技术的需求。

姓名识别技术

有许多可用的 NER 技术。有些使用正则表达式,有些基于预定义的字典。正则表达式有很强的表达能力,可以隔离实体。实体名称的字典可以与文本的标记进行比较以找到匹配。

另一种常见的 NER 方法是使用经过训练的模型来检测它们的存在。这些模型依赖于我们正在寻找的实体类型和目标语言。适用于一个领域(如网页)的模型可能不适用于另一个领域(如医学期刊)。

当模型被训练时,它使用一个带注释的文本块,该文本块标识感兴趣的实体。要衡量模型的训练效果,可以使用以下几种方法:

  • 精度:与评估数据中发现的跨度完全匹配的实体的百分比
  • Recall :这是语料库中定义的在相同位置找到的实体的百分比
  • 性能指标:由 F1 = 2 精度召回/(召回+精度)给出的精度和召回的调和平均值

当我们讨论模型的评估时,我们将使用这些方法。

NER 也被称为实体识别和实体分块。组块是对文本的分析,以识别其部分,如名词、动词或其他成分。作为人类,我们倾向于把一个句子分成不同的部分。这些部分形成了一个结构,我们用它来确定它的意义。NER 进程将创建文本跨度,如英国女王。然而,在这些跨度内可能有其他实体,例如英国

NER 系统使用不同的技术构建,可分为以下几类:

  • 基于规则的方法使用领域专家制定的规则来识别实体。基于规则的系统解析文本并生成解析树或其他抽象格式。它可以是使用一组单词的基于列表的查找,也可以是需要深入了解实体识别的语言学方法。

  • 机器学习方法使用带有统计模型的基于模式的学习,其中名词被识别和分类。机器学习又可以分为三种不同的类型:

    • 监督学习使用带标签的数据来建立模型
    • 半监督学习使用标记数据以及其他信息来建立模型
    • 无监督学习使用未标记的数据,并从输入中学习
  • NE 提取通常用于从网页中提取数据。它不仅学习,而且为 NER 形成或建立一个列表。

列表和正则表达式

一种技术是使用标准实体列表和正则表达式来标识命名实体。命名实体有时被称为专有名词。标准实体列表可以是州、常用名称、月份或经常引用的位置的列表。地名词典是包含与地图一起使用的地理信息的列表,提供了位置相关实体的来源。然而,维护这样的列表可能很耗时。它们也可以是特定于语言和地区的。对列表进行更改可能会很繁琐。我们将在本章后面的部分使用 ExactDictionaryChunker 类演示这种方法。

正则表达式在识别实体时很有用。它们强大的语法在许多情况下提供了足够的灵活性,可以准确地分离出感兴趣的实体。然而,这种灵活性也会使它们难以理解和维护。我们将在本章中演示几种正则表达式方法。

统计分类器

统计分类器确定一个单词是实体的开始,还是实体的继续,或者根本不是实体。样本文本被标记以隔离实体。一旦开发了分类器,就可以针对不同问题领域的不同数据集对其进行训练。这种方法的缺点是需要有人对样本文本进行注释,这是一个耗时的过程。此外,它还依赖于域。

我们将考察几种表演 NER 的方法。首先,我们将从解释如何使用正则表达式来标识实体开始。

对 NER 使用正则表达式

正则表达式可用于标识文档中的实体。我们将研究两种通用方法:

  • 第一种使用 Java 支持的正则表达式。在实体相对简单且形式一致的情况下,这可能很有用。
  • 第二种方法使用专门设计用于正则表达式的类。为了演示这一点,我们将使用 LingPipe 的RegExChunker类。

当使用正则表达式时,避免重新发明轮子是有利的。预定义和经过测试的表达式有很多来源。一个这样的图书馆可以在 http://regexlib.com/Default.aspx 找到。在我们的例子中,我们将使用这个库中的几个正则表达式。

为了测试这些方法的效果,我们将在大多数示例中使用以下文本:

private static String regularExpressionText 
    = "He left his email address (rgb@colorworks.com) and his " 
    + "phone number,800-555-1234\. We believe his current address " 
    + "is 100 Washington Place, Seattle, CO 12345-1234\. I " 
    + "understand you can also call at 123-555-1234 between " 
    + "8:00 AM and 4:30 most days. His URL is http://example.com " 
    + "and he was born on February 25, 1954 or 2/25/1954.";

使用 Java 的正则表达式查找实体

为了演示如何使用这些表达式,我们将从几个简单的例子开始。最初的例子从下面的声明开始。这是一个简单的表达式,用于识别特定类型的电话号码:

String phoneNumberRE = "\\d{3}-\\d{3}-\\d{4}"; 

我们将使用下面的代码来测试我们的简单表达式。Pattern类的compile方法获取一个正则表达式,并将其编译成一个Pattern对象。然后可以对目标文本执行它的matcher方法,返回一个Matcher对象。这个对象允许我们重复识别正则表达式匹配:

Pattern pattern = Pattern.compile(phoneNumberRE); 
Matcher matcher = pattern.matcher(regularExpressionText); 
while (matcher.find()) { 
    System.out.println(matcher.group() + " [" + matcher.start() 
        + ":" + matcher.end() + "]"); 
} 

当匹配发生时,find方法将返回true。它的group方法返回匹配表达式的文本。它的startend方法给我们匹配文本在目标文本中的位置。

执行时,我们将得到以下输出:

    800-555-1234 [68:80]
    123-555-1234 [196:208]

许多其他正则表达式也可以以类似的方式使用。下表列出了这些选项。第三列是在前面的代码序列中使用相应的正则表达式时产生的输出:

实体类型 正则表达式 输出
统一资源定位器 `\b(https?|ftp|file|ldap)😕/[-A-Za-z0-9+&@#/%?
=_&#124;!:,.;]*[-A-Za-z0-9+&@#/%=_|]` http://example.com [256:274]
邮政区码 [0-9]{5}(\\-?[0-9]{4})? 12345-1234 [150:160]
电子邮件 [a-zA-Z0-9'._%+-]+@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,4} rgb@colorworks.com [27:45]
时间 (([0-1]?[0-9])&#124;([2][0-3])):([0-5]?[0-9])(:([0-5]?[0-9]))? 8:00 [217:221]``4:30 [229:233]
日期 `((0?[13578]|10|12)(-|\/)
(([1-9])|(0[1-9])|([12])([0-9]?)|(3[01]?))(-|\/)
((19)([2-9])(\d{1})|(20)([01])(\d{1})|([8901])
(\d{1}))|(0?[2469]|11)(-|\/)(([1-9])
|(0[1-9])|([12])([0-9]?)|(3[0]?))
(-|\/)((19)([2-9])(\d{1})|(20)([01])
(\d{1})|([8901])(\d{1})))` 2/25/1954 [315:324]

我们还可以使用许多其他的正则表达式。然而,这些例子说明了基本的技术。正如日期正则表达式所展示的,其中一些可能相当复杂。

正则表达式遗漏一些实体并将其他非实体误报为实体是很常见的。例如,我们可以用以下表达式替换文本:

regularExpressionText =  
    "(888)555-1111 888-SEL-HIGH 888-555-2222-J88-W3S"; 

执行代码将返回以下内容:

    888-555-2222 [27:39]

它漏掉了前两个电话号码,并将零件号误报为电话号码。

我们还可以使用|操作符一次搜索多个正则表达式。在下面的语句中,使用该运算符组合了三个正则表达式。它们是使用上表中的相应条目声明的:

Pattern pattern = Pattern.compile(phoneNumberRE + "|"  
    + timeRE + "|" + emailRegEx); 

当使用前一节开始时定义的原始regularExpressionText文本执行时,我们得到以下输出:

    rgb@colorworks.com [27:45]
    800-555-1234 [68:80]
    123-555-1234 [196:208]
    8:00 [217:221]
    4:30 [229:233]

使用 LingPipe 的 RegExChunker 类

RegExChunker类使用块来查找文本中的实体。该类使用正则表达式来表示实体。它的chunk方法返回一个Chunking对象,可以像我们在前面的例子中那样使用它。

RegExChunker类的构造函数有三个参数:

  • 这是一个正则表达式
  • String:这是一种实体或类别
  • double:分数的值

在下面的例子中,我们将使用一个表示时间的正则表达式来演示这个类。正则表达式与本章前面的使用 Java 的正则表达式查找实体一节中使用的相同。然后创建了Chunker实例:

String timeRE =  
   "(([0-1]?[0-9])|([2][0-3])):([0-5]?[0-9])(:([0-5]?[0-9]))?"; 
       Chunker chunker = new RegExChunker(timeRE,"time",1.0); 

使用了Chunk方法和displayChunkSet方法,如下所示:

Chunking chunking = chunker.chunk(regularExpressionText); 
Set<Chunk> chunkSet = chunking.chunkSet(); 
displayChunkSet(chunker, regularExpressionText); 

下面的代码段显示了displayChunkSet方法。chunkSet方法返回一组Chunk实例的集合。我们可以使用各种方法来显示块的特定部分:

public void displayChunkSet(Chunker chunker, String text) { 
    Chunking chunking = chunker.chunk(text); 
    Set<Chunk> set = chunking.chunkSet(); 
    for (Chunk chunk : set) { 
        System.out.println("Type: " + chunk.type() + " Entity: [" 
             + text.substring(chunk.start(), chunk.end()) 
             + "] Score: " + chunk.score()); 
    } 
} 

输出如下所示:

    Type: time Entity: [8:00] Score: 1.0
    Type: time Entity: [4:30] Score: 1.0+95

或者,我们可以声明一个简单的类来封装正则表达式,这有助于在其他情况下重用。接下来,声明了TimeRegexChunker类,它支持时间实体的标识:

public class TimeRegexChunker extends RegExChunker { 
    private final static String TIME_RE =  
      "(([0-1]?[0-9])|([2][0-3])):([0-5]?[0-9])(:([0-5]?[0-9]))?"; 
    private final static String CHUNK_TYPE = "time"; 
    private final static double CHUNK_SCORE = 1.0; 

    public TimeRegexChunker() { 
        super(TIME_RE,CHUNK_TYPE,CHUNK_SCORE); 
    } 
} 

要使用这个类,用下面的声明替换这个部分的初始声明chunker:

Chunker chunker = new TimeRegexChunker(); 

输出将和以前一样。

使用 NLP APIs

我们将使用 OpenNLP、Stanford API 和 LingPipe 演示 NER 过程。每种方法都提供了替代技术,通常可以很好地识别文本中的实体。以下声明将作为演示 API 的示例文本:

String sentences[] = {"Joe was the last person to see Fred. ", 
  "He saw him in Boston at McKenzie's pub at 3:00 where he " 
  + " paid $2.45 for an ale. ", 
  "Joe wanted to go to Vermont for the day to visit a cousin who " 
  + "works at IBM, but Sally and he had to look for Fred"}; 

为 NER 使用 OpenNLP

我们将使用 OpenNLP API 演示使用TokenNameFinderModel类来执行 NLP。此外,我们将演示如何确定被识别的实体是正确的概率。

一般的方法是将文本转换成一系列标记化的句子,使用适当的模型创建一个TokenNameFinderModel类的实例,然后使用find方法识别文本中的实体。

下面的例子演示了TokenNameFinderModel类的用法。我们一开始会用一个简单句,然后用多个句子。这句话是这样定义的:

String sentence = "He was the last person to see Fred."; 

我们将使用在en-token.binen-ner-person.bin文件中找到的模型,分别用于标记器和名称查找器模型。这些文件的InputStream对象是使用 try-with-resources 块打开的,如下所示:

try (InputStream tokenStream = new FileInputStream( 
        new File(getModelDir(), "en-token.bin")); 
        InputStream modelStream = new FileInputStream( 
            new File(getModelDir(), "en-ner-person.bin"));) { 
    ... 

} catch (Exception ex) { 
    // Handle exceptions 
} 

try块中,创建了TokenizerModelTokenizer对象:

    TokenizerModel tokenModel = new TokenizerModel(tokenStream); 
    Tokenizer tokenizer = new TokenizerME(tokenModel); 

接下来,使用person模型创建一个NameFinderME类的实例:

TokenNameFinderModel entityModel =  
    new TokenNameFinderModel(modelStream); 
NameFinderME nameFinder = new NameFinderME(entityModel); 

我们现在可以使用tokenize方法来标记文本,使用find方法来识别文本中的人。find方法将使用标记化的String数组作为输入,并返回一个Span对象的数组,如下所示:

String tokens[] = tokenizer.tokenize(sentence); 
Span nameSpans[] = nameFinder.find(tokens);

我们讨论了第三章、找句子中的Span类。您可能还记得,这个类保存了找到的实体的位置信息。实际的字符串实体仍然在tokens数组中:

下面的for语句显示在句子中找到的人。它的位置信息和人显示在不同的行上:

for (int i = 0; i < nameSpans.length; i++) { 
    System.out.println("Span: " + nameSpans[i].toString()); 
    System.out.println("Entity: " 
        + tokens[nameSpans[i].getStart()]); 
} 

输出如下所示:

    Span: [7..9) person
    Entity: Fred

我们经常会用到多个句子。为了演示这一点,我们将使用之前定义的sentences字符串数组。先前的for语句被替换为以下序列。对每个句子调用tokenize方法,然后显示实体信息,就像前面一样:

for (String sentence : sentences) { 
    String tokens[] = tokenizer.tokenize(sentence); 
    Span nameSpans[] = nameFinder.find(tokens); 
    for (int i = 0; i < nameSpans.length; i++) { 
        System.out.println("Span: " + nameSpans[i].toString()); 
        System.out.println("Entity: "  
            + tokens[nameSpans[i].getStart()]); 
    } 
    System.out.println(); 
} 

输出如下。在检测到的两个人之间有一个额外的空白行,因为第二个句子不包含person:

    Span: [0..1) person
    Entity: Joe
    Span: [7..9) person
    Entity: Fred

    Span: [0..1) person
    Entity: Joe
    Span: [19..20) person
    Entity: Sally
    Span: [26..27) person
    Entity: Fred

确定实体的准确性

TokenNameFinderModel识别文本中的实体时,它计算该实体的概率。我们可以使用probs方法访问这些信息,如下面的代码行所示。这个方法返回一个 doubles 数组,它对应于nameSpans数组的元素:

double[] spanProbs = nameFinder.probs(nameSpans); 

在使用find方法后,立即将该语句添加到前面的示例中。然后,在嵌套的for语句的末尾添加以下语句:

System.out.println("Probability: " + spanProbs[i]); 

当执行这个示例时,您将获得以下输出。概率字段反映了实体分配的置信度。对于第一个实体,模型有 80.529%的把握认为Joe是一个person:

    Span: [0..1) person
    Entity: Joe
    Probability: 0.8052914774025202
    Span: [7..9) person
    Entity: Fred
    Probability: 0.9042160889302772

    Span: [0..1) person
    Entity: Joe
    Probability: 0.9620970782763985
    Span: [19..20) person
    Entity: Sally
    Probability: 0.964568603518126
    Span: [26..27) person
    Entity: Fred
    Probability: 0.990383039618594

使用其他实体类型

OpenNLP 支持不同的库,如下表所列。这些模型可以从 http://opennlp.sourceforge.net/models-1.5/下载。 en前缀指定英语为语言,ner表示该型号适用于 NER:

| 英国发现者型号 | 文件名 |
| 位置名称查找器模型 | en-ner-location.bin |
| 货币名称查找器模型 | en-ner-money.bin |
| 组织名称查找器模型 | en-ner-organization.bin |
| 百分比名称查找器模型 | en-ner-percentage.bin |
| 人名搜索模型 | en-ner-person.bin |
| 时间名称查找器模型 | en-ner-time.bin |

如果我们将语句修改为使用不同的模型文件,我们可以看到它们是如何对照例句工作的:

InputStream modelStream = new FileInputStream( 
    new File(getModelDir(), "en-ner-time.bin"));) { 

下表显示了各种输出:

型号 输出
en-ner-location.bin Span: [4..5) location``Entity: Boston``Probability: 0.8656908776583051``Span: [5..6) location``Entity: Vermont``Probability: 0.9732488014011262
en-ner-money.bin Span: [14..16) money``Entity: 2.45``Probability: 0.7200919701507937
en-ner-organization.bin Span: [16..17) organization``Entity: IBM``Probability: 0.9256970736336729
en-ner-time.bin 模型无法检测此文本序列中的时间

当使用en-ner-money.bin模型时,早期代码序列中的令牌数组中的索引必须增加 1。否则,返回的都是美元符号。

模型在示例文本中找不到时间实体。这说明模型没有足够的信心在文本中找到任何时间实体。

处理多个实体类型

我们还可以同时处理多个实体类型。这包括基于循环中的每个模型创建NameFinderME类的实例,并将模型应用于每个句子,在发现实体时跟踪它们。

我们将用下面的例子来说明这个过程。它需要重写前面的try块,以在块中创建InputStream实例,如下所示:

try { 
    InputStream tokenStream = new FileInputStream( 
        new File(getModelDir(), "en-token.bin")); 
    TokenizerModel tokenModel = new TokenizerModel(tokenStream); 
    Tokenizer tokenizer = new TokenizerME(tokenModel); 
    ... 
} catch (Exception ex) { 
    // Handle exceptions 
} 

try块中,我们将定义一个String数组来保存模型文件的名称。如此处所示,我们将对人员、位置和组织使用模型:

String modelNames[] = {"en-ner-person.bin",  
    "en-ner-location.bin", "en-ner-organization.bin"}; 

创建一个ArrayList实例来保存被发现的实体:

ArrayList<String> list = new ArrayList(); 

一个foreach语句用于一次加载一个模型,然后创建一个NameFinderME类的实例:

for(String name : modelNames) { 
    TokenNameFinderModel entityModel = new TokenNameFinderModel( 
        new FileInputStream(new File(getModelDir(), name))); 
    NameFinderME nameFinder = new NameFinderME(entityModel); 
    ... 
} 

以前,我们并不试图识别实体出现在哪个句子中。这并不难做到,但是我们需要使用一个简单的for语句而不是foreach语句来跟踪句子索引。下面的例子显示了这一点,前面的例子被修改为使用整数变量index来保存句子。否则,代码的工作方式与前面相同:

for (int index = 0; index < sentences.length; index++) { 
    String tokens[] = tokenizer.tokenize(sentences[index]); 
    Span nameSpans[] = nameFinder.find(tokens); 
    for(Span span : nameSpans) { 
        list.add("Sentence: " + index 
            + " Span: " + span.toString() + " Entity: " 
            + tokens[span.getStart()]); 
    } 
} 

然后显示发现的实体:

for(String element : list) { 
    System.out.println(element); 
} 

输出如下所示:

Sentence: 0 Span: [0..1) person Entity: Joe
Sentence: 0 Span: [7..9) person Entity: Fred
Sentence: 2 Span: [0..1) person Entity: Joe
Sentence: 2 Span: [19..20) person Entity: Sally
Sentence: 2 Span: [26..27) person Entity: Fred
Sentence: 1 Span: [4..5) location Entity: Boston
Sentence: 2 Span: [5..6) location Entity: Vermont
Sentence: 2 Span: [16..17) organization Entity: IBM  

为 NER 使用 Stanford API

我们将演示CRFClassifier类,因为它将用于执行 NER。这个类实现了所谓的线性链条件随机场 ( CRF )序列模型。

为了演示CRFClassifier类的使用,我们将从分类器文件字符串的声明开始,如下所示:

String model = getModelDir() +  
    "\\english.conll.4class.distsim.crf.ser.gz"; 

然后使用模型创建分类器:

CRFClassifier<CoreLabel> classifier = 
    CRFClassifier.getClassifierNoExceptions(model);

classify方法接受一个表示要处理的文本的字符串。要使用sentences文本,我们需要将其转换成一个简单的字符串:

String sentence = ""; 
for (String element : sentences) { 
    sentence += element; 
} 

然后将classify方法应用于文本:

List<List<CoreLabel>> entityList = classifier.classify(sentence); 

返回CoreLabel对象的List实例的List实例。返回的对象是一个包含另一个列表的列表。包含的列表是CoreLabel对象的一个List实例。CoreLabel类表示附加了附加信息的单词。列表包含这些单词的列表。在下面代码序列的外部 for-each 语句中,引用变量internalList代表文本中的一个句子。在内部 for-each 语句中,显示了内部列表中的每个单词。word方法返回单词,get方法返回单词的类型。

然后显示单词及其类型:

for (List<CoreLabel> internalList: entityList) { 
    for (CoreLabel coreLabel : internalList) { 
        String word = coreLabel.word(); 
        String category = coreLabel.get( 
            CoreAnnotations.AnswerAnnotation.class); 
        System.out.println(word + ":" + category); 
    } 
} 

部分输出如下。它已被截断,因为显示了每个单词。O代表另一类:

    Joe:PERSON
    was:O
    the:O
    last:O
    person:O
    to:O
    see:O
    Fred:PERSON
    .:O
 He:O ... look:O for:O Fred:PERSON

要过滤掉不相关的单词,请用以下语句替换println语句。这将消除其他类别:

if (!"O".equals(category)) { 
    System.out.println(word + ":" + category); 
} 

现在输出更简单了:

Joe:PERSON
Fred:PERSON
Boston:LOCATION
McKenzie:PERSON
Joe:PERSON
Vermont:LOCATION
IBM:ORGANIZATION
Sally:PERSON
Fred:PERSON  

为 NER 使用 LingPipe

我们之前在本章前面的为 NER 使用正则表达式一节中演示了使用正则表达式来使用 LingPipe。在这里,我们将演示如何使用命名实体模型和ExactDictionaryChunker类来执行 NER 分析。

使用 LingPipe 的命名实体模型

LingPipe 有几个命名实体模型,我们可以使用它们进行分块。这些文件由一个序列化对象组成,可以从文件中读取该对象,然后将其应用于文本。这些对象实现了Chunker接口。分块过程产生一系列的Chunking对象,这些对象识别感兴趣的实体。

下表列出了 NER 的型号。这些模型可以从alias-i.com/lingpipe/web/models.html下载:

| 型 | 文集 | 文件 |
| 英语新闻 | MUC-6 | 新新闻。查里斯克林春克 |
| 英语基因 | 基因标签 | 新生物基因标签。HmmChunker |
| 英语基因组学 | 妖怪 | 生物基因学。TokenShapeChunker |

我们将使用在ne-en-news-muc6.AbstractCharLmRescoringChunker文件中找到的模型来演示如何使用这个类。
我们将从一个try...catch块开始处理异常,如下例所示。该文件被打开并与AbstractExternalizable类的静态readObject方法一起使用,以创建一个Chunker类的实例。该方法
将读入序列化模型:

try { 
    File modelFile = new File(getModelDir(),  
        "ne-en-news-muc6.AbstractCharLmRescoringChunker"); 
     Chunker chunker = (Chunker)  
        AbstractExternalizable.readObject(modelFile); 
    ... 
} catch (IOException | ClassNotFoundException ex) { 
    // Handle exception 
} 

ChunkerChunking接口提供了处理一组文本块的方法。它的chunk方法返回一个实现Chunking实例的对象。以下序列显示了在文本的每个句子中找到的块,如下所示:

for (int i = 0; i < sentences.length; ++i) { 
    Chunking chunking = chunker.chunk(sentences[i]); 
    System.out.println("Chunking=" + chunking); 
} 

该序列的输出如下:

    Chunking=Joe was the last person to see Fred.  : [0-3:PERSON@-Infinity, 31-35:ORGANIZATION@-Infinity]
    Chunking=He saw him in Boston at McKenzie's pub at 3:00 where he paid $2.45 for an ale.  : [14-20:LOCATION@-Infinity, 24-32:PERSON@-Infinity]
    Chunking=Joe wanted to go to Vermont for the day to visit a cousin who works at IBM, but Sally and he had to look for Fred : [0-3:PERSON@-Infinity, 20-27:ORGANIZATION@-Infinity, 71-74:ORGANIZATION@-Infinity, 109-113:ORGANIZATION@-Infinity]

相反,我们可以使用Chunk类的方法来提取特定的信息,如下面的代码所示。我们将用下面的foreach语句替换前面的for语句。这调用了使用本章前面的 LingPipe 部分的 RegExChunker 类在中开发的displayChunkSet方法:

for (String sentence : sentences) { 
    displayChunkSet(chunker, sentence); 
} 

下面的输出显示了结果。但是,它并不总是与实体类型正确匹配:

Type: PERSON Entity: [Joe] Score: -Infinity
Type: ORGANIZATION Entity: [Fred] Score: -Infinity
Type: LOCATION Entity: [Boston] Score: -Infinity
Type: PERSON Entity: [McKenzie] Score: -Infinity
Type: PERSON Entity: [Joe] Score: -Infinity
Type: ORGANIZATION Entity: [Vermont] Score: -Infinity
Type: ORGANIZATION Entity: [IBM] Score: -Infinity
Type: ORGANIZATION Entity: [Fred] Score: -Infinity  

使用 ExactDictionaryChunker 类

ExactDictionaryChunker类提供了一种简单的方法来创建实体及其类型的字典,稍后可以用它在文本中找到它们。它使用一个MapDictionary对象存储条目,然后使用ExactDictionaryChunker类根据字典提取组块。

AbstractDictionary接口支持对实体、类别和分数的基本操作。该分数用于匹配过程。MapDictionaryTrieDictionary类实现了AbstractDictionary接口。TrieDictionary类使用字符 trie 结构存储信息。这种方法使用较少的内存,所以当内存有限时,这种方法工作得很好。在我们的例子中,我们将使用MapDictionary类。

为了说明这种方法,我们将从对MapDictionary类的声明开始:

private MapDictionary<String> dictionary;

字典将包含我们有兴趣寻找的实体。我们需要初始化模型,如下面的initializeDictionary方法所示。这里使用的DictionaryEntry构造函数接受三个参数:

  • String:实体的名称
  • String:实体的类别
  • Double:表示该实体的分数

在确定匹配时使用分数。一些实体被声明并添加到字典中:

private static void initializeDictionary() { 
    dictionary = new MapDictionary<String>(); 
    dictionary.addEntry( 
        new DictionaryEntry<String>("Joe","PERSON",1.0)); 
    dictionary.addEntry( 
        new DictionaryEntry<String>("Fred","PERSON",1.0)); 
    dictionary.addEntry( 
        new DictionaryEntry<String>("Boston","PLACE",1.0)); 
    dictionary.addEntry( 
        new DictionaryEntry<String>("pub","PLACE",1.0)); 
    dictionary.addEntry( 
        new DictionaryEntry<String>("Vermont","PLACE",1.0)); 
    dictionary.addEntry( 
        new DictionaryEntry<String>("IBM","ORGANIZATION",1.0)); 
    dictionary.addEntry( 
        new DictionaryEntry<String>("Sally","PERSON",1.0)); 
} 

一个ExactDictionaryChunker实例将使用这个字典。这里详细说明了ExactDictionaryChunker类的参数:

  • Dictionary<String>:包含实体的字典
  • 这是分块器使用的标记器
  • boolean:如果是true,分块器应该返回所有匹配
  • boolean:如果是true,匹配区分大小写

匹配可以重叠。例如,在短语第一国家银行中,实体银行可以单独使用,也可以与短语的其余部分结合使用。第三个参数是,boolean决定是否返回所有的匹配。

在下面的序列中,字典被初始化。然后,我们使用印欧标记器创建了一个ExactDictionaryChunker类的实例,在这里我们返回所有匹配项,并忽略标记的大小写:

initializeDictionary(); 
ExactDictionaryChunker dictionaryChunker 
    = new ExactDictionaryChunker(dictionary, 
        IndoEuropeanTokenizerFactory.INSTANCE, true, false); 

dictionaryChunker对象用于每个句子,如下面的代码序列所示。我们将使用displayChunkSet方法,正如在本章前面的中使用 的 RegExChunker 类所开发的:

for (String sentence : sentences) { 
    System.out.println("\nTEXT=" + sentence); 
    displayChunkSet(dictionaryChunker, sentence); 
} 

在执行时,我们得到以下输出:

TEXT=Joe was the last person to see Fred. 
Type: PERSON Entity: [Joe] Score: 1.0
Type: PERSON Entity: [Fred] Score: 1.0

TEXT=He saw him in Boston at McKenzie's pub at 3:00 where he paid $2.45 for an ale. 
Type: PLACE Entity: [Boston] Score: 1.0
Type: PLACE Entity: [pub] Score: 1.0

TEXT=Joe wanted to go to Vermont for the day to visit a cousin who works at IBM, but Sally and he had to look for Fred
Type: PERSON Entity: [Joe] Score: 1.0
Type: PLACE Entity: [Vermont] Score: 1.0
Type: ORGANIZATION Entity: [IBM] Score: 1.0
Type: PERSON Entity: [Sally] Score: 1.0
Type: PERSON Entity: [Fred] Score: 1.0  

这做得很好,但是为大量词汇创建字典需要很大的努力。

使用 NER 注记工具构建新数据集

有许多不同形式的注释工具。有些是独立的,可以在本地机器上配置或安装,有些是基于云的,有些是免费的,有些是付费的。在这一节中,我们将关注免费的注释工具,了解如何使用它们,并看看我们可以通过注释实现什么。

为了了解如何使用注释来创建数据集,我们将看看这些工具:

  • 顽童
  • 斯坦福注释者

brat 代表 brat 快速注释工具,可以在【http://brat.nlplab.org/index.html】的找到。可以在线使用,也可以离线使用。在你的本地机器上安装它很简单:按照brat.nlplab.org/installation.html中列出的步骤。安装并运行后,打开浏览器。您需要在data/test目录下创建一个text1.txt文件,内容如下:

Joe was the last person to see Fred. He saw him in Boston at McKenzie's pub at 3:00 where he paid $2.45 for an ale. Joe wanted to go to Vermont for the day to visit a cousin who works at IBM, but Sally and he had to look for Fred.

由于显示没有选择文件,使用标签键可以选择文件。我们将创建一个名为text1.txt的文本文件,其内容与我们在前面的例子中处理的内容相同:

它将显示text1.txt文件的内容:

要注释文档,首先我们必须登录:

登录后,选择您希望注释的任何单词,这将打开新的注释窗口,其中列出/配置了实体类型和事件类型。所有这些信息都存储并预配置在data/test目录下的annotation.conf文件中。您可以根据需要修改文件:

当我们继续选择文本时,注释将显示在文本上:

一旦保存,注释文件可以被发现为text1.ann [ Filename.ann ]。

另一个工具是斯坦福标注工具,可以从NLP . Stanford . edu/software/Stanford-manual-Annotation-tool-2004-05-16 . tar . gz下载。下载完成后,提取并双击annotator.jar,或者执行以下命令:

> java -jar annotator.jar

它将显示以下内容:

您可以打开任何文本文件,也可以编写内容并保存文件。我们在前面的注释示例中使用的文本将再次使用,只是为了展示如何使用斯坦福注释工具。

一旦内容可用,下一步就是创建标签。从“标记”菜单中,选择“添加标记”选项,这将打开“标记创建”窗口,如以下屏幕截图所示:

输入标签名称,然后单击确定。然后会要求您选择标签的颜色。它将在主窗口的右侧窗格中显示标记,如下面的屏幕截图所示:

同样,我们可以创建任意多的标签。一旦创建了标签,下一步就是注释文本。要注释文本,比如说,Joe,使用鼠标选择文本并点击右边的名称标签。它将向文本添加标记,如下所示:

同样,正如我们对 Joe 所做的那样,我们可以根据需要标记任何其他文本,并保存文件。还可以保存标签,以便在其他文本上重复使用。保存的文件是普通的文本文件,可以在任何文本编辑器中查看。

训练模型

我们将使用 OpenNLP 来演示如何训练一个模型。使用的培训文件必须:

  • 包含标记来区分实体
  • 每行一句话

我们将使用下面的模型文件,命名为en-ner-person.train:

<START:person> Joe <END> was the last person to see <START:person> Fred <END>.  
He saw him in Boston at McKenzie's pub at 3:00 where he paid $2.45 for an ale.  
<START:person> Joe <END> wanted to go to Vermont for the day to visit a cousin who works at IBM, but <START:person> Sally <END> and he had to look for <START:person> Fred <END>. 

这个例子中的几个方法能够抛出异常。这些语句将放在 try-with-resource 块中,如下所示,在这里创建了模型的输出流:

try (OutputStream modelOutputStream = new BufferedOutputStream( 
        new FileOutputStream(new File("modelFile")));) { 
    ... 
} catch (IOException ex) { 
    // Handle exception 
} 

在这个块中,我们使用PlainTextByLineStream类创建了一个OutputStream<String>对象。这个类的构造函数接受一个FileInputStream实例,并将每一行作为一个String对象返回。en-ner-person.train文件被用作输入文件,如下所示。UTF-8字符串是指所使用的编码序列:

ObjectStream<String> lineStream = new PlainTextByLineStream( 
    new FileInputStream("en-ner-person.train"), "UTF-8"); 

lineStream对象包含用描述文本中实体的标签注释的流。这些需要被转换成NameSample对象,以便模型可以被训练。这个转换是由NameSampleDataStream类执行的,如下所示。一个NameSample对象保存文本中实体的名称:

ObjectStream<NameSample> sampleStream =  
    new NameSampleDataStream(lineStream); 

train方法现在可以如下执行:

TokenNameFinderModel model = NameFinderME.train( 
    "en", "person",  sampleStream,  
    Collections.<String, Object>emptyMap(), 100, 5);

下表详细列出了该方法的参数:

| 参数 | 意为 |
| "en" | 语言代码 |
| "person" | 实体类型 |
| sampleStream | 抽样资料 |
| null | 资源 |
| 100 | 迭代次数 |
| 5 | 近路 |

然后,模型被序列化为输出文件:

model.serialize(modelOutputStream); 

这个序列的输出如下。为了节省空间,它被缩短了。提供了有关创建模型的基本信息:

    Indexing events using cutoff of 5

      Computing event counts...  done. 53 events
      Indexing...  done.
    Sorting and merging events... done. Reduced 53 events to 46.
    Done indexing.
    Incorporating indexed data for training...  
    done.
      Number of Event Tokens: 46
          Number of Outcomes: 2
        Number of Predicates: 34
    ...done.
    Computing model parameters ...
    Performing 100 iterations.
      1:  ... loglikelihood=-36.73680056967707  0.05660377358490566
      2:  ... loglikelihood=-17.499660626361216  0.9433962264150944
      3:  ... loglikelihood=-13.216835449617108  0.9433962264150944
      4:  ... loglikelihood=-11.461783667999262  0.9433962264150944
      5:  ... loglikelihood=-10.380239416084963  0.9433962264150944
      6:  ... loglikelihood=-9.570622475692486  0.9433962264150944
      7:  ... loglikelihood=-8.919945779143012  0.9433962264150944
    ...
     99:  ... loglikelihood=-3.513810438211968  0.9622641509433962
    100:  ... loglikelihood=-3.507213816708068  0.9622641509433962

评估模型

可以使用TokenNameFinderEvaluator类来评估模型。评估过程使用标记的样本文本来执行评估。
在这个简单的例子中,创建了一个名为en-ner-person.eval的文件,其中包含以下文本:

<START:person> Bill <END> went to the farm to see <START:person> Sally <END>.  
Unable to find <START:person> Sally <END> he went to town. 
There he saw <START:person> Fred <END> who had seen <START:person> Sally <END> at the book store with <START:person> Mary <END>. 

下面的代码用于执行评估。之前的模型被用作TokenNameFinderEvaluator构造函数的参数。基于评估文件创建一个NameSampleDataStream实例。TokenNameFinderEvaluator类的evaluate方法执行评估:

TokenNameFinderEvaluator evaluator =  
    new TokenNameFinderEvaluator(new NameFinderME(model));     
lineStream = new PlainTextByLineStream( 
    new FileInputStream("en-ner-person.eval"), "UTF-8"); 
sampleStream = new NameSampleDataStream(lineStream); 
evaluator.evaluate(sampleStream); 

为了确定模型与评估数据的配合程度,需要执行getFMeasure方法。然后显示结果:

FMeasure result = evaluator.getFMeasure(); 
System.out.println(result.toString()); 

以下输出显示了PrecisionRecallF-Measure。它表明找到的 50%的实体与评估数据完全匹配。Recall是在相同位置找到的语料库中定义的实体的百分比。性能度量是调和平均值,定义为 F1 = 2 精度召回/(召回+精度):

Precision: 0.5 Recall: 0.25 F-Measure: 0.3333333333333333  

为了创建更好的模型,数据集和评估集应该更大。这里的目的是展示用于训练和评估 POS 模型的基本方法。

摘要

NER 包括检测实体,然后对它们进行分类。常见的类别包括名称、位置和事物。这是许多应用程序用来支持搜索、解析引用和查找文本含义的一项重要任务。该流程经常用于下游任务。

我们研究了几种表演 NER 的技巧。正则表达式是核心 Java 类和 NLP APIs 都支持的一种方法。这种技术对许多应用程序都很有用,并且有大量的正则表达式库可用。

基于字典的方法也是可能的,并且对于某些应用程序来说效果很好。然而,它们有时需要相当大的努力来填充。我们使用 LingPipe 的MapDictionary类来说明这种方法。

经过训练的模型也可以用来执行 NER。我们检查了其中的几个,并演示了如何使用 OpenNLP NameFinderME类训练一个模型。这个过程与早期的培训过程非常相似。

在下一章,第五章,检测词类我们将学习如何检测名词、形容词、介词等词类。**

五、检测词性

以前,我们识别文本的各个部分,如人物、地点和事物。在这一章中,我们将考察寻找词性 ( 词性)的过程。这些是我们在英语中认为是语法元素的部分,如名词和动词。我们会发现,单词的上下文是决定它是什么类型单词的一个重要方面。

我们将研究标记过程,它本质上是将一个 POS 分配给一个标记。这个过程是检测 POS 的核心。我们将简要讨论为什么标记是重要的,然后检查使检测词性变得困难的各种因素。各种自然语言处理(NLP)API 随后被用来说明标记过程。我们还将演示如何训练一个模型来处理专门的文本。

我们将在本章中讨论以下主题:

  • 标记过程
  • 使用 NLP APIs

标记过程

标记是将描述分配给令牌或部分文本的过程。这种描述称为标签。位置标记是将位置标记分配给令牌的过程。这些标签通常是语法标签,如名词、动词和形容词。例如,考虑下面的句子:

"奶牛跳过了月亮。"

对于这些初始示例中的许多示例,我们将说明 POS tagger 使用 OpenNLP tagger 的结果,这将在本章后面的使用 OpenNLP POS tagger一节中讨论。如果我们在前面的例子中使用这个标记符,我们将得到下面的结果。注意,单词后面是一个正斜杠,然后是它们的 POS 标签。这些标签将很快得到解释:

 The/DT cow/NN jumped/VBD over/IN the/DT moon./NN

根据上下文,单词可能有多个相关联的标签。例如,单词 saw 可以是名词,也可以是动词。当一个单词可以被分类成不同的类别时,诸如其位置、其附近的单词或类似信息的信息被用于概率性地确定适当的类别。例如,如果一个单词前面是限定词,后面是名词,那么将这个单词标记为形容词。

一般的标记过程包括标记文本、确定可能的标记和解决不明确的标记。算法用于执行位置识别(标记)。有两种通用方法:

  • 基于规则的:基于规则的标签使用一组规则,以及一个单词和可能标签的字典。当一个单词有多个标签时,使用这些规则。规则通常使用前面和/或后面的单词来选择标签。
  • 随机标签:随机标签要么基于马尔可夫模型,要么基于线索,要么使用决策树,要么使用最大熵。马尔可夫模型是有限状态机,其中每个状态有两种概率分布。它的目标是为一个句子找到最佳的标签序列。隐马尔可夫模型 ( HMM )也有使用。在这些模型中,状态转换是不可见的。

最大熵标记器使用统计学来确定单词的词性,并且经常使用语料库来训练模型。语料库是用词性标签标注的单词的集合。许多语言都有语料库。这些都需要很大的努力去开发。常用的语料库有宾夕法尼亚树库(https://www.seas.upenn.edu/~pdtb//)或布朗语料库(www . Essex . AC . uk/linguistics/external/clmt/W3C/Corpus _ ling/content/corpora/list/private/Brown/Brown . html)。

来自 Penn Treebank 语料库的示例说明了 POS 标记,如下所示:

    Well/UH what/WP do/VBP you/PRP think/VB about/IN
    the/DT idea/NN of/IN ,/, uh/UH ,/, kids/NNS having/VBG
    to/TO do/VB public/JJ service/NN work/NN for/IN a/DT
    year/NN ?/.

英语传统上有九种词类:名词、动词、冠词、形容词、介词、代词、副词、连词和感叹词。然而,更完整的分析通常需要额外的类别和子类别。已经发现了多达 150 种不同的词类。在某些情况下,可能需要创建新的标签。下表列出了一个简短的列表。这些是我们将在本章中经常使用的标签:

| 部分 | 意为 |
| 神经网络 | 名词,单数,还是复数 |
| 暗行扫描(Dark Trace) | 限定词 |
| 动词 | 动词,基本形式 |
| VBD | 动词,过去式 |
| VBZ | 动词,第三人称单数现在时 |
| 在…里 | 介词或从属连词 |
| NNP | 专有名词,单数 |
| 到 | 到 |
| 姐姐(网络用语)ˌ法官ˌ裁判员(judges) | 形容词 |

下表显示了更全面的列表。本榜单改编自www . ling . upenn . edu/courses/Fall _ 2003/ling 001/Penn _ tree bank _ pos . html。宾夕法尼亚大学(Penn)树库标签集的完整列表可以在 http://www.comp.leeds.ac.uk/ccalas/tagsets/upenn.html找到。一组标签被称为标签集:

标签 描述 标签 描述
抄送 并列连词 PRP 元 所有格代名词
激光唱片 基数 副词
暗行扫描(Dark Trace) 限定词 RBR 副词,比较
前妻;前夫 存在主义 随机阻塞系统(Random Barrage System 的缩写) 副词,最高级
转发 外来词 菲律宾共和国 颗粒
在…里 介词或从属连词 符号 标志
姐姐(网络用语)ˌ法官ˌ裁判员(judges) 形容词
JJR 形容词,比较级 感叹词
JJS 形容词,最高级 动词 动词,基本形式
莱索托 列表项目标记 VBD 动词,过去式
医学博士 情态的 VBG 动词、动名词或现在分词
神经网络 名词,单数,还是复数 VBN 动词,过去分词
NNS Noun, plural VBP 动词,非第三人称单数现在时
NNP 专有名词,单数 VBZ 动词,第三人称单数现在时
NNPS 专有名词,复数 禁水试验 疑问限定词
太平洋夏季时间 前限定词 文字处理 疑问代词
刷卡机 所有格结尾 WP$ 所有格 wh 代词
富含血小板血浆 人称代词 战时难民事务委员会(War Refugee Board) 疑问副词

人工语料库的开发是劳动密集型的。然而,已经开发了一些统计技术来创建语料库。许多语料库是可用的。第一个是布朗文集(clu.uni.no/icame/manuals/BROWN/INDEX.HTM)。较新的包括超过 1 亿字的英国国家语料库(【http://www.natcorp.ox.ac.uk/corpus/index.xml】)和美国国家语料库(【http://www.anc.org/】)。

张贴标签的重要性

对句子进行正确的标注可以提高后续处理任务的质量。如果我们知道 sue 是动词而不是名词,那么这可以帮助建立标记之间的正确关系。确定词性、短语、从句以及它们之间的任何关系被称为解析。这与标记化相反,在标记化中,我们只对识别单词元素感兴趣,而不关心它们的意思。

词性标注用于许多下游过程,例如问题分析和分析文本的情感。一些社交媒体网站经常对评估客户交流的情绪感兴趣。文本索引将频繁使用 POS 数据。语音处理可以使用标签来帮助决定如何发音。

POS 难的原因是什么?

一门语言的许多方面都会使词性标注变得困难。大多数英语单词都有两个或更多的相关标签。字典并不总是足以确定一个单词的位置。例如,像比尔力量这样的词的意思取决于它们的上下文。下面的句子演示了它们如何在同一个句子中作为名词和动词使用。

“比尔用暴力迫使经理将账单撕成两半。”

将 OpenNLP 标记符与此语句一起使用会产生以下输出:

    Bill/NNP used/VBD the/DT force/NN to/TO force/VB the/DT manger/NN to/TO tear/VB the/DT bill/NN in/IN two./PRP$

textese 是不同文本形式的组合,包括缩写、标签、表情符号和俚语,在 tweets 和 text 等通信媒体中的使用使得标记句子变得更加困难。例如,下面的消息很难标记:

“阿发克她已经死了!顺便说一句,我们在 BBIAM 聚会上玩得很开心。”

它的对等词是:

“据我所知,她讨厌打扫房子!顺便说一句,我在聚会上玩得很开心。一会儿就回来。”

使用 OpenNLP 标记器,我们将获得以下输出:

    AFAIK/NNS she/PRP H8/CD cth!/.
    BTW/NNP had/VBD a/DT GR8/CD tym/NN at/IN the/DT party/NN BBIAM./.

在本章后面的使用 MaxentTagger 类标记 textese 一节中,我们将演示 LingPipe 如何处理 textese。下表给出了常见文本术语的简短列表:

| 短语 | 文字 | 短语 | 文字 |
| 据我所知 | 就我所知 | 顺便问一下 | 顺便说一下 |
| 远离键盘 | 离开键盘的 | 你只能靠自己了 | 游乐儿 |
| 谢谢 | THNX 或 THX | 尽快 | 尽快(As Soon As Possible) |
| 今天 | 2 天 | 你这话是什么意思 | WDYMBT |
| 以前 | B4 | 马上回来 | BBIAM |
| 再见 | C U | 不可以 | (cannot)不能 |
| 哈哈的笑 | 倍硬 | 后来 | l8R |
| 大声笑出来 | 英雄联盟 | 另一方面 | 另一方面 |
| 笑得在地上打滚 | 罗夫尔还是 ROTFL | 我不知道 | 我不知道 |
| 伟大的 | GR8 | 打扫房子 | 三己糖酰基鞘氨醇 |
| 此刻 | 异步传输模式 | 管见所及 | 恕我直言 |

There are several lists of textese; a large list can be found at
www.ukrainecalling.com/textspeak.aspx.

标记化是词性标注过程中的一个重要步骤。如果标记没有正确分割,我们可能会得到错误的结果。还有其他几个潜在的问题,包括:

  • 如果我们使用小写字母,那么像山姆这样的词可能会与奖励管理的人或系统(【www.sam.gov】??)混淆
  • 我们必须考虑到缩写,如不能,并认识到撇号可以使用不同的字符
  • 虽然像反之亦然这样的短语可以被视为一个单位,但它已经被用于英格兰的一个乐队、一部小说的标题和一本杂志的标题
  • 我们不能忽视用连字符连接的单词,如首切首切,它们的意思不同于它们各自的用法
  • 有些单词嵌入了数字,比如 iPhone 5S
  • 还需要处理 URL 或电子邮件地址等特殊字符序列

一些单词被发现嵌入在引号或括号中,这可能会使它们的意思混淆。考虑下面的例子:

"“蓝色”是否正确(事实并非如此)是有争议的。"

“蓝色”可以指蓝色,也可以是一个人的昵称。
这个句子的标签输出如下:

Whether/IN "Blue"/NNP was/VBD correct/JJ or/CC not/RB (it's/JJ not)/NN is/VBZ debatable/VBG

使用 NLP APIs

我们将使用 OpenNLP、Stanford API 和 LingPipe 演示词性标注。每个例子都会用到下面的句子。这是儒勒·凡尔纳的《海底两万里》中第五章的第一句话,出自《冒险的 ??》和《??》。

private String[] sentence = {"The", "voyage", "of", "the",  
    "Abraham", "Lincoln", "was", "for", "a", "long", "time", "marked",  
    "by", "no", "special", "incident."};

要处理的文本可能不总是以这种方式定义。有时,句子会以单个字符串的形式出现:

String theSentence = "The voyage of the Abraham Lincoln was for a "  
    + "long time marked by no special incident.";

我们可能需要将一个字符串转换成一个字符串数组。有许多技术可以将这个字符串转换成单词数组。以下tokenizeSentence方法执行该操作:

public String[] tokenizeSentence(String sentence) { 
    String words[] = sentence.split("S+"); 
    return words; 
}

下面的代码演示了此方法的用法:

String words[] = tokenizeSentence(theSentence); 
for(String word : words) { 
    System.out.print(word + " ");  
} 
System.out.println(); 

输出如下所示:

The voyage of the Abraham Lincoln was for a long time marked by no special incident.

或者,我们可以使用一个标记器,比如 OpenNLP 的WhitespaceTokenizer类,如下所示:

String words[] = 
     WhitespaceTokenizer.INSTANCE.tokenize(sentence);

使用 OpenNLP 位置标签

OpenNLP 提供了几个支持词性标注的类。我们将演示如何使用POSTaggerME类来执行基本的标记,以及如何使用ChunkerME类来执行分块。组块包括根据相关单词的类型对它们进行分组。这可以提供对句子结构的额外洞察。我们还将研究一个POSDictionary实例的创建和使用。

为 POS 标记器使用 OpenNLP POSTaggerME 类

OpenNLP POSTaggerME类使用最大熵来处理标签。
tagger 根据单词本身和单词的上下文来确定标签的类型。任何给定的单词都可能有多个相关联的标签。标记器使用概率模型来确定要分配的特定标记。

POS 模型是从文件中加载的。en-pos-maxent.bin模型经常被使用,它基于 Penn TreeBank 标签集。在opennlp.sourceforge.net/models-1.5/可以找到 OpenNLP 的各种预训练 POS 模型。

我们从一个 try-catch 块开始,处理加载模型时可能产生的任何IOException,如下所示。

我们将en-pos-maxent.bin文件用于模型:

try (InputStream modelIn = new FileInputStream( 
    new File(getModelDir(), "en-pos-maxent.bin"));) { 
    ... 
} 
catch (IOException e) { 
    // Handle exceptions 
} 

接下来,创建POSModelPOSTaggerME实例,如下所示:

POSModel model = new POSModel(modelIn); 
POSTaggerME tagger = new POSTaggerME(model); 

tag方法现在可以应用于 tagger,使用要处理的文本作为它的参数:

String tags[] = tagger.tag(sentence); 

然后显示单词及其标签,如下所示:

for (int i = 0; i<sentence.length; i++) { 
    System.out.print(sentence[i] + "/" + tags[i] + " "); 
} 

输出如下。每个单词后面都有它的类型:

The/DT voyage/NN of/IN the/DT Abraham/NNP Lincoln/NNP was/VBD for/IN a/DT long/JJ time/NN marked/VBN by/IN no/DT special/JJ incident./NN

对于任何一个句子,都可能有不止一个可能的标签分配给单词。topKSequences方法将根据正确的概率返回一组序列。在下面的代码序列中,使用sentence变量执行topKSequences方法,然后显示:

Sequence topSequences[] = tagger.topKSequences(sentence); 
for (inti = 0; i<topSequences.length; i++) { 
    System.out.println(topSequences[i]); 
}

其输出如下,其中第一个数字代表加权分数,括号内的标签是评分的标签序列:

    -0.5563571615737618 [DT, NN, IN, DT, NNP, NNP, VBD, IN, DT, JJ, NN, VBN, IN, DT, JJ, NN]
    -2.9886144610050907 [DT, NN, IN, DT, NNP, NNP, VBD, IN, DT, JJ, NN, VBN, IN, DT, JJ, .]
    -3.771930515521527 [DT, NN, IN, DT, NNP, NNP, VBD, IN, DT, JJ, NN, VBN, IN, DT, NN, NN]

Ensure that you include the correct Sequence class. For this example, use import opennlp.tools.util.Sequence;.

Sequence类有几个方法,详见下表:

| 方法 | 意为 |
| getOutcomes | 返回表示句子标签的字符串列表 |
| getProbs | 返回代表序列中每个标签概率的一组double变量 |
| getScore | 返回序列的加权值 |

在下面的序列中,我们使用其中的几个方法来演示它们的作用。对于每个序列,将显示标记及其概率,用正斜杠分隔:

for (int i = 0; i<topSequences.length; i++) { 
    List<String> outcomes = topSequences[i].getOutcomes(); 
    double probabilities[] = topSequences[i].getProbs(); 
    for (int j = 0; j <outcomes.size(); j++) {  
        System.out.printf("%s/%5.3f ",outcomes.get(j), 
        probabilities[j]); 
    } 
    System.out.println(); 
} 
System.out.println();

输出如下。每一对行代表一个序列,其中输出已被包装:

    DT/0.992 NN/0.990 IN/0.989 DT/0.990 NNP/0.996 NNP/0.991 VBD/0.994 IN/0.996 DT/0.996 JJ/0.991 NN/0.994 VBN/0.860 IN/0.985 DT/0.960 JJ/0.919 NN/0.832 
    DT/0.992 NN/0.990 IN/0.989 DT/0.990 NNP/0.996 NNP/0.991 VBD/0.994 IN/0.996 DT/0.996 JJ/0.991 NN/0.994 VBN/0.860 IN/0.985 DT/0.960 JJ/0.919 ./0.073 
    DT/0.992 NN/0.990 IN/0.989 DT/0.990 NNP/0.996 NNP/0.991 VBD/0.994 IN/0.996 DT/0.996 JJ/0.991 NN/0.994 VBN/0.860 IN/0.985 DT/0.960 NN/0.073 NN/0.419

使用 opennlp 重庆

组块的过程包括将一个句子分成几个部分或几个组块。然后可以用标签对这些块进行注释。我们将使用ChunkerME类来说明这是如何完成的。这个类使用一个加载到ChunkerModel实例中的模型。ChunkerME类的chunk方法执行实际的分块过程。我们还将研究使用chunkAsSpans方法来返回关于这些块的跨度的信息。这让我们可以看到一个块有多长,以及什么元素组成了这个块。

我们将使用en-pos-maxent.bin文件为POSTaggerME实例创建一个模型。我们需要使用这个实例来标记文本,就像我们在本章前面的为 POS taggers 使用 OpenNLP POSTaggerME 类一节中所做的那样。我们还将使用en-chunker.bin文件创建一个ChunkerModel实例,与ChunkerME实例一起使用。

这些模型是使用输入流创建的,如下例所示。我们使用 try-with-resources 块来打开和关闭文件,并处理可能引发的任何异常:

try ( 
        InputStream posModelStream = new FileInputStream( 
            getModelDir() + "\\en-pos-maxent.bin"); 
        InputStream chunkerStream = new FileInputStream( 
            getModelDir() + "\\en-chunker.bin");) { 
    ... 
} catch (IOException ex) { 
    // Handle exceptions 
}

下面的代码序列创建并使用一个标记来查找句子的位置。然后显示句子及其标签:

POSModel model = new POSModel(posModelStream); 
POSTaggerME tagger = new POSTaggerME(model); 

String tags[] = tagger.tag(sentence); 
for(int i=0; i<tags.length; i++) { 
    System.out.print(sentence[i] + "/" + tags[i] + " "); 
} 
System.out.println();

输出如下。我们已经展示了该输出,以便清楚了解分块器的工作原理:

The/DT voyage/NN of/IN the/DT Abraham/NNP Lincoln/NNP was/VBD for/IN a/DT long/JJ time/NN marked/VBN by/IN no/DT special/JJ incident./NN

使用输入流创建一个ChunkerModel实例。由此创建了ChunkerME实例,然后使用chunk方法,如下所示。chunk方法将使用句子的标记和它的标签来创建一个字符串数组。每个字符串将保存关于令牌及其块的信息:

ChunkerModel chunkerModel = new 
     ChunkerModel(chunkerStream); 
ChunkerME chunkerME = new ChunkerME(chunkerModel); 
String result[] = chunkerME.chunk(sentence, tags);

显示了results数组中的每个标记及其块标签,如下所示:

for (int i = 0; i < result.length; i++) { 
    System.out.println("[" + sentence[i] + "] " + result[i]); 
}

输出如下。该标记用括号括起来,后面是 chunk 标记。下表解释了这些标签:

| 第一部分 |
| B | 标签开头 |
| 我 | 标签的延续 |
| E | 标签结束(如果标签只有一个单词,则不会出现) |
| 第二部分 |
| 公证人 | 名词块 |
| 动词 | 动词组块 |

多个单词组合在一起,如“The voyage”、the Abraham Lincoln:

    [The] B-NP
    [voyage] I-NP
    [of] B-PP
    [the] B-NP
    [Abraham] I-NP
    [Lincoln] I-NP
    [was] B-VP
    [for] B-PP
    [a] B-NP
    [long] I-NP
    [time] I-NP
    [marked] B-VP
    [by] B-PP
    [no] B-NP
    [special] I-NP
    [incident.] I-NP

如果我们想获得更多关于块的详细信息,我们可以使用ChunkerME类的chunkAsSpans方法。这个方法返回一个Span对象的数组。每个对象代表文本中的一个跨度。

还有几个其他的ChunkerME类方法可用。这里,我们将举例说明getTypegetStartgetEnd方法的使用。getType方法返回块标签的第二部分,而getStartgetEnd方法分别返回原始sentence数组中标记的开始和结束索引。length方法返回多个标记的跨度长度。

在下面的序列中,使用sentencetags数组执行chunkAsSpans方法。然后显示spans数组。外部的for循环一次处理一个Span对象,显示基本的跨度信息。
内部for循环显示括号内的跨区文本:

Span[] spans = chunkerME.chunkAsSpans(sentence, tags); 
for (Span span : spans) { 
    System.out.print("Type: " + span.getType() + " - "  
        + " Begin: " + span.getStart()  
        + " End:" + span.getEnd() 
        + " Length: " + span.length() + "  ["); 
    for (int j = span.getStart(); j < span.getEnd(); j++) { 
        System.out.print(sentence[j] + " "); 
    } 
    System.out.println("]"); 
}

下面的输出清楚地显示了跨度类型、它在sentence数组中的位置、它的Length,以及实际的跨度文本:

    Type: NP -  Begin: 0 End:2 Length: 2  [The voyage ]
    Type: PP -  Begin: 2 End:3 Length: 1  [of ]
    Type: NP -  Begin: 3 End:6 Length: 3  [the Abraham Lincoln ]
    Type: VP -  Begin: 6 End:7 Length: 1  [was ]
    Type: PP -  Begin: 7 End:8 Length: 1  [for ]
    Type: NP -  Begin: 8 End:11 Length: 3  [a long time ]
    Type: VP -  Begin: 11 End:12 Length: 1  [marked ]
    Type: PP -  Begin: 12 End:13 Length: 1  [by ]
    Type: NP -  Begin: 13 End:16 Length: 3  [no special incident. ]

使用 POSDictionary 类

标签字典指定了单词的有效标签。这可以防止标签不适当地应用于单词。此外,一些搜索算法执行得更快,因为它们不必考虑其他不太可能的标签。

在本节中,我们将演示如何:

  • 获取标记者的标记字典
  • 确定一个单词有哪些标签
  • 展示如何改变一个单词的标签
  • 向新的标记器工厂添加新的标记字典

与前面的示例一样,我们将使用 try-with-resources 块打开 POS 模型的输入流,然后创建我们的模型和标记器工厂,如下所示:

try (InputStream modelIn = new FileInputStream( 
        new File(getModelDir(), "en-pos-maxent.bin"));) { 
    POSModel model = new POSModel(modelIn); 
    POSTaggerFactory posTaggerFactory = model.getFactory(); 
    ... 
} catch (IOException e) { 
    //Handle exceptions 
}

获取标记者的标记字典

我们使用了POSModel类的getFactory方法来获得一个POSTaggerFactory实例。我们将使用它的getTagDictionary方法来获得它的TagDictionary实例。这里举例说明了这一点:

MutableTagDictionary tagDictionary =  
  (MutableTagDictionary)posTaggerFactory.getTagDictionary(); 

MutableTagDictionary接口扩展了TagDictionary接口。TagDictionary接口拥有一个getTags方法,MutableTagDictionary接口增加了一个put方法,允许将标签添加到字典中。这些接口是由POSDictionary类实现的。

确定单词的标签

要获得给定单词的标签,使用getTags方法。这将返回由字符串表示的标记数组。然后会显示标签,如下所示:

String tags[] = tagDictionary.getTags("force"); 
for (String tag : tags) { 
    System.out.print("/" + tag); 
} 
System.out.println(); 

输出如下所示:

/NN/VBP/VB

这意味着“力”这个词可以有三种不同的解释。

更改单词的标签

接口的方法允许我们给一个单词添加标签。该方法有两个参数:单词及其新标签。方法返回包含前面标记的数组。

在下面的例子中,我们用新标签替换旧标签。然后显示旧标签:

String oldTags[] = tagDictionary.put("force", "newTag"); 
for (String tag : oldTags) { 
    System.out.print("/" + tag); 
} 
System.out.println();

以下输出列出了单词的旧标签:

/NN/VBP/VB

这些标签已被新标签替换,如下所示,其中显示了当前标签:

tags = tagDictionary.getTags("force"); 
for (String tag : tags) { 
    System.out.print("/" + tag); 
} 
System.out.println();

我们得到的是以下内容:

 /newTag

为了保留旧标签,我们需要创建一个字符串数组来保存旧标签和新标签,然后使用该数组作为put方法的第二个参数,如下所示:

String newTags[] = new String[tags.length+1]; 
for (int i=0; i<tags.length; i++) { 
    newTags[i] = tags[i]; 
} 
newTags[tags.length] = "newTag"; 
oldTags = tagDictionary.put("force", newTags);

如果我们重新显示当前标签,如此处所示,我们可以看到旧标签被保留,新标签被添加:

 /NN/VBP/VB/newTag  

When adding tags, be careful to assign the tags in the proper order, as it will influence which tag is assigned.

添加新的标记字典

一个新的标签字典可以被添加到一个POSTaggerFactory实例中。我们将通过创建一个新的POSTaggerFactory并添加我们之前开发的tagDictionary来说明这个过程。首先,我们使用默认构造函数创建一个新工厂,如下面的代码所示。

接下来对新工厂调用setTagDictionary方法:

POSTaggerFactory newFactory = new POSTaggerFactory(); 
newFactory.setTagDictionary(tagDictionary); 

为了确认已经添加了tag字典,我们显示了单词"force"的标签,如下所示:

tags = newFactory.getTagDictionary().getTags("force"); 
for (String tag : tags) { 
    System.out.print("/" + tag); 
} 
System.out.println(); 

标签是相同的,如下所示:

 /NN/VBP/VB/newTag

从文件创建字典

如果我们需要创建一个新的字典,那么一种方法是创建一个包含所有单词及其标签的 XML 文件,然后从该文件创建字典。OpenNLP 通过POSDictionary类的create方法支持这种方法。

XML 文件由根元素dictionary和一系列的元素entry组成。entry元素使用tags属性来指定单词的标签。这个单词作为一个token元素包含在entry元素中。使用存储在dictionary.txt文件中的两个单词的简单示例如下:

<dictionary case_sensitive="false"> 
    <entry tags="JJ VB"> 
        <token>strong</token> 
    </entry> 
    <entry tags="NN VBP VB"> 
        <token>force</token> 
    </entry> 
</dictionary>

为了创建字典,我们使用基于输入流的create方法,如下所示:

try (InputStream dictionaryIn =  
      new FileInputStream(new File("dictionary.txt"));) { 
    POSDictionary dictionary = 
     POSDictionary.create(dictionaryIn); 
    ... 
} catch (IOException e) { 
    // Handle exceptions 
}

POSDictionary类有一个返回迭代器对象的iterator方法。它的next方法为字典中的每个单词返回一个字符串。我们可以使用这些方法来显示字典的内容,如下所示:

Iterator<String> iterator = dictionary.iterator(); 
while (iterator.hasNext()) { 
    String entry = iterator.next(); 
    String tags[] = dictionary.getTags(entry); 
    System.out.print(entry + " "); 
    for (String tag : tags) { 
        System.out.print("/" + tag); 
    } 
    System.out.println(); 
}

下面的输出显示了我们可以预期的结果:

  strong /JJ/VB
  force /NN/VBP/VB

使用斯坦福 POS 标签

在这一节中,我们将研究斯坦福 API 支持的两种不同的方法来执行标记。第一种技术使用了MaxentTagger类。顾名思义,它使用最大熵来寻找位置。我们还将使用这个类来演示一个用于处理 textese 类型文本的模型。第二种方法将对注释器使用管道方法。英语标签使用宾州树库英语 POS 标签集。

使用斯坦福 MaxentTagger

MaxentTagger类使用一个模型来执行标记任务。API 中捆绑了许多模型,所有模型都带有文件扩展名.tagger。它们包括英语、中文、阿拉伯语、法语和德语模型。
这里列出了英国型号。前缀wsj,指的是基于华尔街日报的模型。其他术语指的是用于训练模型的技术。这里不涉及这些概念:

  • wsj-0-18-bidirectional-distsim.tagger
  • wsj-0-18-bidirectional-nodistsim.tagger
  • wsj-0-18-caseless-left3words-distsim.tagger
  • wsj-0-18-left3words-distsim.tagger
  • wsj-0-18-left3words-nodistsim.tagger
  • english-bidirectional-distsim.tagger
  • english-caseless-left3words-distsim.tagger
  • english-left3words-distsim.tagger

该示例从文件中读入一系列句子。然后处理每个句子,并显示访问和显示单词和标签的各种方式。

我们从 try-with-resources 块开始处理 IO 异常,如下所示。wsj-0-18-bidirectional-distsim.tagger文件用于创建MaxentTagger类的一个实例。

使用MaxentTagger类的tokenizeText方法创建HasWord对象的List实例的List实例。这些句子是从sentences.txt文件中读入的。HasWord接口表示单词并包含两个方法:一个setWord和一个word方法。后一种方法将单词作为字符串返回。每个句子由一个HasWord对象的List实例表示:

try { 
    MaxentTagger tagger = new MaxentTagger(getModelDir() +  
        "//wsj-0-18-bidirectional-distsim.tagger"); 
    List<List<HasWord>> sentences = MaxentTagger.tokenizeText( 
        new BufferedReader(new FileReader("sentences.txt"))); 
    ... 
} catch (FileNotFoundException ex) { 
    // Handle exceptions 
}

sentences.txt文件包含本书第五章的前四句话在一次冒险中**海底两万里:

The voyage of the Abraham Lincoln was for a long time marked by no special incident. 
But one circumstance happened which showed the wonderful dexterity of Ned Land, and proved what confidence we might place in him. 
The 30th of June, the frigate spoke some American whalers, from whom we learned that they knew nothing about the narwhal. 
But one of them, the captain of the Monroe, knowing that Ned Land had shipped on board the Abraham Lincoln, begged for his help in chasing a whale they had in sight.

添加一个循环来处理sentences列表中的每个句子。tagSentence方法返回TaggedWord对象的List实例,如下面的代码所示。TaggedWord类实现了HasWord接口,并添加了一个tag方法,该方法返回与单词相关联的标签。如此处所示,toString方法用于显示每个句子:

List<TaggedWord> taggedSentence = 
     tagger.tagSentence(sentence); 
for (List<HasWord> sentence : sentences) { 
    List<TaggedWord> taggedSentence= 
         tagger.tagSentence(sentence); 
    System.out.println(taggedSentence); 
}

输出如下所示:

    [The/DT, voyage/NN, of/IN, the/DT, Abraham/NNP, Lincoln/NNP, was/VBD, for/IN, a/DT, long/JJ, --- time/NN, marked/VBN, by/IN, no/DT, special/JJ, incident/NN, ./.]
     [But/CC, one/CD, circumstance/NN, happened/VBD, which/WDT, showed/VBD, the/DT, wonderful/JJ, dexterity/NN, of/IN, Ned/NNP, Land/NNP, ,/,, and/CC, proved/VBD, what/WP, confidence/NN, we/PRP, might/MD, place/VB, in/IN, him/PRP, ./.]
    [The/DT, 30th/JJ, of/IN, June/NNP, ,/,, the/DT, frigate/NN, spoke/VBD, some/DT, American/JJ, whalers/NNS, ,/,, from/IN, whom/WP, we/PRP, learned/VBD, that/IN, they/PRP, knew/VBD, nothing/NN, about/IN, the/DT, narwhal/NN, ./.]
    [But/CC, one/CD, of/IN, them/PRP, ,/,, the/DT, captain/NN, of/IN, the/DT, Monroe/NNP, ,/,, knowing/VBG, that/IN, Ned/NNP, Land/NNP, had/VBD, shipped/VBN, on/IN, board/NN, the/DT, Abraham/NNP, Lincoln/NNP, ,/,, begged/VBN, for/IN, his/PRP$, help/NN, in/IN, chasing/VBG, a/DT, whale/NN, they/PRP, had/VBD, in/IN, sight/NN, ./.]

或者,我们可以使用Sentence类的listToString方法将标记的句子转换成一个简单的String对象。

HasWordtoString方法使用第二个参数的值false来创建结果字符串,如下所示:

List<TaggedWord> taggedSentence = 
     tagger.tagSentence(sentence); 
for (List<HasWord> sentence : sentences) { 
    List<TaggedWord> taggedSentence= 
         tagger.tagSentence(sentence); 
    System.out.println(Sentence.listToString(taggedSentence, false)); 
}

这产生了更美观的输出:

    The/DT voyage/NN of/IN the/DT Abraham/NNP Lincoln/NNP was/VBD for/IN a/DT long/JJ time/NN marked/VBN by/IN no/DT special/JJ incident/NN ./.
    But/CC one/CD circumstance/NN happened/VBD which/WDT showed/VBD the/DT wonderful/JJ dexterity/NN of/IN Ned/NNP Land/NNP ,/, and/CC proved/VBD what/WP confidence/NN we/PRP might/MD place/VB in/IN him/PRP ./.
    The/DT 30th/JJ of/IN June/NNP ,/, the/DT frigate/NN spoke/VBD some/DT American/JJ whalers/NNS ,/, from/IN whom/WP we/PRP learned/VBD that/IN they/PRP knew/VBD nothing/NN about/IN the/DT narwhal/NN ./.
    But/CC one/CD of/IN them/PRP ,/, the/DT captain/NN of/IN the/DT Monroe/NNP ,/, knowing/VBG that/IN Ned/NNP Land/NNP had/VBD shipped/VBN on/IN board/NN the/DT Abraham/NNP Lincoln/NNP ,/, begged/VBN for/IN his/PRP$ help/NN in/IN chasing/VBG a/DT whale/NN they/PRP had/VBD in/IN sight/NN ./.

我们可以使用下面的代码序列来产生相同的结果。wordtag方法提取单词及其标签:

List<TaggedWord> taggedSentence = 
     tagger.tagSentence(sentence); 
for (TaggedWord taggedWord : taggedSentence) { 
    System.out.print(taggedWord.word() + "/" + 
         taggedWord.tag() + " "); 
} 
System.out.println();

如果我们只对查找给定标签的特定出现感兴趣,我们可以使用如下序列,它将只列出单数名词(NN):

List<TaggedWord> taggedSentence = 
     tagger.tagSentence(sentence); 
for (TaggedWord taggedWord : taggedSentence) { 
    if (taggedWord.tag().startsWith("NN")) { 
        System.out.print(taggedWord.word() + " "); 
    } 
} 
System.out.println();

每个句子都会显示单数名词,如下所示:

    NN Tagged: voyage Abraham Lincoln time incident 
    NN Tagged: circumstance dexterity Ned Land confidence 
    NN Tagged: June frigate whalers nothing narwhal 
    NN Tagged: captain Monroe Ned Land board Abraham Lincoln help whale sight

使用 MaxentTagger 类标记 textese

我们可以使用不同的模型来处理可能包含 textese 的 Twitter 文本。文本工程通用架构 ( )(【https://gate.ac.uk/wiki/twitter-postagger.html】??)已经为 Twitter 文本开发了一个模型。此处使用模型来处理 textese:

MaxentTagger tagger = new MaxentTagger(getModelDir()  
    + "//gate-EN-twitter.model"); 

在这里,我们使用来自MaxentTagger类的tagString方法。本章前面的小节处理 textese:

System.out.println(tagger.tagString("AFAIK she H8 cth!")); System.out.println(tagger.tagString( "BTW had a GR8 tym at the party BBIAM.")); 

输出如下所示:

    AFAIK_NNP she_PRP H8_VBP cth!_NN 
    BTW_UH had_VBD a_DT GR8_NNP tym_NNP at_IN the_DT party_NN BBIAM._NNP

使用斯坦福管道执行标记

我们在之前的几个例子中使用了斯坦福管道。在这个例子中,我们将使用斯坦福管道来提取 POS 标签。和我们之前的 Stanford 例子一样,我们基于一组注释器创建了一个管道:tokenizessplit
pos

这些将标记化,将文本分割成句子,然后找到位置标记:

Properties props = new Properties(); 
props.put("annotators", "tokenize, ssplit, pos"); 
StanfordCoreNLP pipeline = new StanfordCoreNLP(props); 

为了处理文本,我们将使用theSentence变量作为Annotator的输入。然后调用管道的annotate方法,如下所示:

Annotation document = new Annotation(theSentence); 
pipeline.annotate(document);

因为管道可以执行不同类型的处理,所以使用一列CoreMap对象来访问单词和标签。Annotation类的get方法返回句子列表,如下所示:

List<CoreMap> sentences = 
     document.get(SentencesAnnotation.class);

可以使用get方法访问CoreMap对象的内容。该方法的参数是所需信息的类。如下面的代码示例所示,使用TextAnnotation类访问令牌,使用PartOfSpeechAnnotation类检索 POS 标记。将显示每个句子的每个单词及其标签:

for (CoreMap sentence : sentences) { 
    for (CoreLabel token : sentence.get(TokensAnnotation.class)) { 
        String word = token.get(TextAnnotation.class); 
        String pos = token.get(PartOfSpeechAnnotation.class); 
        System.out.print(word + "/" + pos + " "); 
    } 
    System.out.println(); 
}

输出如下所示:

The/DT voyage/NN of/IN the/DT Abraham/NNP Lincoln/NNP was/VBD for/IN a/DT long/JJ time/NN marked/VBN by/IN no/DT special/JJ incident/NN ./.

管道可以使用附加选项来控制标记器的工作方式。例如,默认情况下,使用english-left3words-distsim.tagger tagger 模型。我们可以使用pos.model属性指定一个不同的模型,如下所示。还有一个pos.maxlen属性来控制最大句子长度:

props.put("pos.model", 
"C:/.../Models/english-caseless-left3words-distsim.tagger"); 

有时候,有一个 XML 格式的标记文档是很有用的。StanfordCoreNLP类的xmlPrint方法会写出这样一个文档。该方法的第一个参数是要显示的注释器。它的第二个参数是要写入的OutputStream对象。在以下代码序列中,先前的标记结果被写入标准输出。它包含在一个try...catch块中,用于处理 IO 异常:

try { 
    pipeline.xmlPrint(document, System.out); 
} catch (IOException ex) { 
    // Handle exceptions 
}

部分结果列表如下。仅显示前两个单词和最后一个单词。每个 token 标签都包含单词、其位置及其 POS 标签:

    <?xml version="1.0" encoding="UTF-8"?>
    <?xml-stylesheet href="CoreNLP-to-HTML.xsl" type="text/xsl"?>
    <root>
    <document>
    <sentences>
    <sentence id="1">
    <tokens>
    <token id="1">
    <word>The</word>
    <CharacterOffsetBegin>0</CharacterOffsetBegin>
    <CharacterOffsetEnd>3</CharacterOffsetEnd>
    <POS>DT</POS>
    </token>
    <token id="2">
    <word>voyage</word>
    <CharacterOffsetBegin>4</CharacterOffsetBegin>
    <CharacterOffsetEnd>10</CharacterOffsetEnd>
    <POS>NN</POS>
    </token>
             ...
    <token id="17">
    <word>.</word>
    <CharacterOffsetBegin>83</CharacterOffsetBegin>
    <CharacterOffsetEnd>84</CharacterOffsetEnd>
    <POS>.</POS>
    </token>
    </tokens>
    </sentence>
    </sentences>
    </document>
    </root>

prettyPrint方法以类似的方式工作:

pipeline.prettyPrint(document, System.out); 

然而,输出并不真的那么漂亮,如此处所示。显示原始句子,后面是每个单词、其位置和标签。输出已被格式化,以便于阅读:

    The voyage of the Abraham Lincoln was for a long time marked by no special incident.
    [Text=The CharacterOffsetBegin=0 CharacterOffsetEnd=3 PartOfSpeech=DT] 
    [Text=voyage CharacterOffsetBegin=4 CharacterOffsetEnd=10 PartOfSpeech=NN] 
    [Text=of CharacterOffsetBegin=11 CharacterOffsetEnd=13 PartOfSpeech=IN] 
    [Text=the CharacterOffsetBegin=14 CharacterOffsetEnd=17 PartOfSpeech=DT] 
    [Text=Abraham CharacterOffsetBegin=18 CharacterOffsetEnd=25 PartOfSpeech=NNP]
     [Text=Lincoln CharacterOffsetBegin=26 CharacterOffsetEnd=33 PartOfSpeech=NNP]
     [Text=was CharacterOffsetBegin=34 CharacterOffsetEnd=37 PartOfSpeech=VBD]
     [Text=for CharacterOffsetBegin=38 CharacterOffsetEnd=41 PartOfSpeech=IN]
     [Text=a CharacterOffsetBegin=42 CharacterOffsetEnd=43 PartOfSpeech=DT]
     [Text=long CharacterOffsetBegin=44 CharacterOffsetEnd=48 PartOfSpeech=JJ]
     [Text=time CharacterOffsetBegin=49 CharacterOffsetEnd=53 PartOfSpeech=NN]
     [Text=marked CharacterOffsetBegin=54 CharacterOffsetEnd=60 PartOfSpeech=VBN]
     [Text=by CharacterOffsetBegin=61 CharacterOffsetEnd=63 PartOfSpeech=IN] 
    [Text=no CharacterOffsetBegin=64 CharacterOffsetEnd=66 PartOfSpeech=DT]
     [Text=special CharacterOffsetBegin=67 CharacterOffsetEnd=74 PartOfSpeech=JJ]
     [Text=incident CharacterOffsetBegin=75 CharacterOffsetEnd=83 PartOfSpeech=NN]
     [Text=. CharacterOffsetBegin=83 CharacterOffsetEnd=84 PartOfSpeech=.]

使用 LingPipe POS 标签

LingPipe 使用Tagger接口来支持词性标注。这个接口只有一个方法:tag。它返回一个Tagging对象的List实例。这些对象是单词和它们的标签。该接口由ChainCrfHmmDecoder类实现。

ChainCrf类使用线性链条件随机场解码和估计来确定标签。HmmDecoder类使用 HMM 来执行标记。接下来我们将举例说明这个类。

HmmDecoder类使用tag方法来确定最可能(最好)的标签。它还有一个tagNBest方法,对可能的标签进行评分,并返回这个评分标签的迭代器。灵管有三种 POS 型号,可以从alias-i.com/lingpipe/web/models.html下载。下表列出了这些选项。在我们的演示中,我们将使用 Brown 语料库模型:

| 型号 | 文件 |
| 英语通用文本:布朗语料库 | pos-en-general-brown.HiddenMarkovModel |
| 英语生物医学文本:MedPost 语料库 | pos-en-bio-medpost.HiddenMarkovModel |
| 英语生物医学文本:GENIA 语料库 | pos-en-bio-genia.HiddenMarkovModel |

使用带有 Best_First 标记的 HmmDecoder 类

我们从处理异常的 try-with-resources 块和创建HmmDecoder实例的代码开始,如下面的代码所示。

从文件中读取模型,然后用作HmmDecoder构造函数的参数:

try ( 
        FileInputStream inputStream =  
            new FileInputStream(getModelDir() 
            + "//pos-en-general-brown.HiddenMarkovModel"); 
        ObjectInputStream objectStream = 
            new ObjectInputStream(inputStream);) { 
    HiddenMarkovModel hmm = (HiddenMarkovModel) 
        objectStream.readObject(); 
    HmmDecoder decoder = new HmmDecoder(hmm); 
    ... 
} catch (IOException ex) { 
 // Handle exceptions 
} catch (ClassNotFoundException ex) { 
 // Handle exceptions 
};

我们将对theSentence变量进行标记。首先,它需要被标记化。我们将使用一个IndoEuropean标记器,如下所示。tokenizer方法要求将文本字符串转换成字符数组。然后,tokenize方法以字符串的形式返回一个令牌数组:

TokenizerFactory TOKENIZER_FACTORY =  
    IndoEuropeanTokenizerFactory.INSTANCE; 
char[] charArray = theSentence.toCharArray(); 
Tokenizer tokenizer =  
    TOKENIZER_FACTORY.tokenizer( 
      charArray, 0, charArray.length); 
String[] tokens = tokenizer.tokenize();

实际的标记是由HmmDecoder类的tag方法执行的。然而,这个方法需要一个String令牌的List实例。这个列表是使用Arrays类的asList方法创建的。Tagging类保存一系列标记和标签:

List<String> tokenList = Arrays.asList(tokens); 
Tagging<String> tagString = decoder.tag(tokenList);

我们现在准备好显示令牌及其标签。下面的循环使用tokentag方法分别访问Tagging对象中的标记和标签。然后显示它们:

for (int i = 0; i < tagString.size(); ++i) { 
    System.out.print(tagString.token(i) + "/"  
    + tagString.tag(i) + " "); 
}

输出如下所示:

The/at voyage/nn of/in the/at Abraham/np Lincoln/np was/bedz for/in a/at long/jj time/nn marked/vbn by/in no/at special/jj incident/nn ./.

使用带有 NBest 标记的 HmmDecoder 类

标记过程考虑标记的多种组合。HmmDecoder类的tagNBest方法返回反映不同订单可信度的ScoredTagging对象的迭代器。该方法采用一个令牌列表和一个指定所需最大结果数的数字。

前面的句子不够模糊,不足以说明标签的组合。相反,我们将使用下面的句子:

String[] sentence = {"Bill", "used", "the", "force", 
     "to", "force", "the", "manager", "to",  
    "tear", "the", "bill","in", "to."}; 
List<String> tokenList = Arrays.asList(sentence); 

这里显示了使用此方法的一个示例,从结果数的声明开始:

int maxResults = 5;

使用上一节中创建的decoder对象,我们将tagNBest方法应用于它,如下所示:

Iterator<ScoredTagging<String>> iterator =  
    decoder.tagNBest(tokenList, maxResults); 

迭代器将允许我们访问五个不同的分数。ScoredTagging类拥有一个score方法,该方法返回一个反映它认为自己执行得有多好的值。在下面的代码序列中,一个printf语句显示了这个分数。接下来是一个循环,其中显示了令牌及其标签。

结果是一个分数,后跟带有标签的单词序列:

while (iterator.hasNext()) { 
    ScoredTagging<String> scoredTagging = iterator.next(); 
    System.out.printf("Score: %7.3f   Sequence: ", 
        scoredTagging.score()); 
    for (int i = 0; i < tokenList.size(); ++i) { 
        System.out.print(scoredTagging.token(i) + "/"  
            + scoredTagging.tag(i) + " "); 
    } 
    System.out.println(); 
}

输出如下。注意,单词"force"可以有一个标签nnjjvb:

    Score: -148.796   Sequence: Bill/np used/vbd the/at force/nn to/to force/vb the/at manager/nn to/to tear/vb the/at bill/nn in/in two./nn 
    Score: -154.434   Sequence: Bill/np used/vbn the/at force/nn to/to force/vb the/at manager/nn to/to tear/vb the/at bill/nn in/in two./nn 
    Score: -154.781   Sequence: Bill/np used/vbd the/at force/nn to/in force/nn the/at manager/nn to/to tear/vb the/at bill/nn in/in two./nn 
    Score: -157.126   Sequence: Bill/np used/vbd the/at force/nn to/to force/vb the/at manager/jj to/to tear/vb the/at bill/nn in/in two./nn 
    Score: -157.340   Sequence: Bill/np used/vbd the/at force/jj to/to force/vb the/at manager/nn to/to tear/vb the/at bill/nn in/in two./nn  

使用 HmmDecoder 类确定标签可信度

可以使用网格结构来执行统计分析,这对于分析可选的单词排序是有用的。这个结构表示向前/向后得分。HmmDecoder类的tagMarginal方法返回TagLattice类的一个实例,它代表一个网格。

我们可以使用ConditionalClassification类的一个实例来检查网格的每个令牌。在下面的例子中,tagMarginal方法返回一个TagLattice实例。使用一个循环来获取网格中每个标记的ConditionalClassification实例。

我们使用的是在上一节中开发的同一个tokenList实例:

TagLattice<String> lattice = decoder.tagMarginal(tokenList); 
for (int index = 0; index < tokenList.size(); index++) { 
    ConditionalClassification classification =  
        lattice.tokenClassification(index); 
    ... 
}

ConditionalClassification类有一个score和一个category方法。score方法返回给定类别的相对分数。category方法返回这个类别,也就是标签。令牌、其分数及其类别如下所示:

System.out.printf("%-8s",tokenList.get(index)); 
for (int i = 0; i < 4; ++i) { 
    double score = classification.score(i); 
    String tag = classification.category(i); 
    System.out.printf("%7.3f/%-3s ",score,tag); 
} 
System.out.println(); 

输出如下所示:

    Bill      0.974/np    0.018/nn    0.006/rb    0.001/nps 
    used      0.935/vbd   0.065/vbn   0.000/jj    0.000/rb  
    the       1.000/at    0.000/jj    0.000/pps   0.000/pp$$ 
    force     0.977/nn    0.016/jj    0.006/vb    0.001/rb  
    to        0.944/to    0.055/in    0.000/rb    0.000/nn  
    force     0.945/vb    0.053/nn    0.002/rb    0.001/jj  
    the       1.000/at    0.000/jj    0.000/vb    0.000/nn  
    manager   0.982/nn    0.018/jj    0.000/nn$   0.000/vb  
    to        0.988/to    0.012/in    0.000/rb    0.000/nn  
    tear      0.991/vb    0.007/nn    0.001/rb    0.001/jj  
    the       1.000/at    0.000/jj    0.000/vb    0.000/nn  
    bill      0.994/nn    0.003/jj    0.002/rb    0.001/nns 
    in        0.990/in    0.004/rp    0.002/nn    0.001/jj  
    two.      0.960/nn    0.013/np    0.011/nns   0.008/rb

为 OpenNLP POSModel 定型

训练一个 OpenNLP POSModel类似于前面的训练例子。需要一个训练文件,它应该足够大以提供一个好的样本集。培训文件的每句话必须单独在一行上。每一行都由一个标记、下划线字符和标签组成。

下面的训练数据是使用第五章的前五句话创建的,第五章是《海底两万里中的在一家企业。虽然这不是一个大的样本集,但它很容易创建,并且足以用于演示目的。它保存在一个名为sample.train的文件中:

    The_DT voyage_NN of_IN the_DT Abraham_NNP Lincoln_NNP was_VBD for_IN a_DT long_JJ time_NN marked_VBN by_IN no_DT special_JJ incident._NN
    But_CC one_CD circumstance_NN happened_VBD which_WDT showed_VBD the_DT wonderful_JJ dexterity_NN of_IN Ned_NNP Land,_NNP and_CC proved_VBD what_WP confidence_NN we_PRP might_MD place_VB in_IN him._PRP$ 
    The_DT 30th_JJ of_IN June,_NNP the_DT frigate_NN spoke_VBD some_DT American_NNP whalers,_, from_IN whom_WP we_PRP learned_VBD that_IN they_PRP knew_VBD nothing_NN about_IN the_DT narwhal._NN 
    But_CC one_CD of_IN them,_PRP$ the_DT captain_NN of_IN the_DT Monroe,_NNP knowing_VBG that_IN Ned_NNP Land_NNP had_VBD shipped_VBN on_IN board_NN the_DT Abraham_NNP Lincoln,_NNP begged_VBD for_IN his_PRP$ help_NN in_IN chasing_VBG a_DT whale_NN they_PRP had_VBD in_IN sight._NN

我们将演示使用POSModel类的train方法创建模型,以及如何将模型保存到文件中。我们从声明POSModel实例变量开始:

POSModel model = null;

try-with-resources 块打开示例文件:

try (InputStream dataIn = new FileInputStream("sample.train");) { 
    ... 
} catch (IOException e) { 
    // Handle exceptions 
}

创建一个PlainTextByLineStream类的实例,并与WordTagSampleStream类一起创建一个ObjectStream<POSSample>实例。这将样本数据转换成train方法要求的格式:

ObjectStream<String> lineStream =  
    new PlainTextByLineStream(dataIn, "UTF-8"); 
ObjectStream<POSSample> sampleStream =  
    new WordTagSampleStream(lineStream); 

train方法使用它的参数来指定语言、样本流、训练参数和任何需要的字典(本例中没有),如下所示:

model = POSTaggerME.train("en", sampleStream, 
    TrainingParameters.defaultParams(), null, null); 

这个过程的输出是冗长的。以下输出已被缩短以节省空间:

    Indexing events using cutoff of 5

      Computing event counts...  done. 90 events
      Indexing...  done.
    Sorting and merging events... done. Reduced 90 events to 82.
    Done indexing.
    Incorporating indexed data for training...  
    done.
      Number of Event Tokens: 82
          Number of Outcomes: 17
        Number of Predicates: 45
    ...done.
    Computing model parameters ...
    Performing 100 iterations.
      1:  ... loglikelihood=-254.98920096505964  0.14444444444444443
      2:  ... loglikelihood=-201.19283975630537  0.6
      3:  ... loglikelihood=-174.8849213436524  0.6111111111111112
      4:  ... loglikelihood=-157.58164262220754  0.6333333333333333
      5:  ... loglikelihood=-144.69272379986646  0.6555555555555556
    ...
     99:  ... loglikelihood=-33.461128002846024  0.9333333333333333
    100:  ... loglikelihood=-33.29073273669207  0.9333333333333333

为了将模型保存到文件中,我们使用了下面的代码。输出流被创建,POSModel类的serialize方法将模型保存到en_pos_verne.bin文件中:

try (OutputStream modelOut = new BufferedOutputStream( 
        new FileOutputStream(new File("en_pos_verne.bin")));) { 
    model.serialize(modelOut); 
} catch (IOException e) { 
    // Handle exceptions 
}

摘要

词性标注是一种识别句子语法部分的强大技术。它为下游任务提供了有用的处理,如问题分析和分析文本的情感。当我们在第七章、信息检索中讨论解析时,我们将回到这个主题。

由于大多数语言中存在歧义,标记不是一个简单的过程。越来越多的使用 textese 只会让这个过程变得更加困难。幸运的是,有一些模型可以很好地识别这种类型的文本。然而,随着新术语和俚语的引入,这些模型需要不断更新。

我们研究了 OpenNLP、斯坦福 API 和 LingPipe 在支持标记方面的使用。这些库使用几种不同的方法来标记单词,包括基于规则和基于模型的方法。我们看到了如何使用字典来增强标记过程。

我们简要介绍了模型训练过程。预先标记的样本文本被用作该过程的输入,模型作为输出出现。虽然我们没有解决模型的验证问题,但是这可以通过与我们在前面章节中所完成的相似的方式来完成。

各种 POS 标记方法可以根据许多因素进行比较,例如它们的准确性和运行速度。虽然我们没有在这里讨论这些问题,但是有许多可用的网络资源。一个检验他们跑得多快的对比可以在 http://mattwilkens . com/2008/11/08/evaluating-pos-taggers-speed/找到。

在下一章中,第六章,表示具有特征的文本,我们将研究基于内容对文档进行分类的技术。

六、用特征表示文本

考虑到文本的上下文,文本包含需要提取的特征,但是对机器来说,处理一整节文本以包含上下文是非常困难的。

在这一章中,我们将看到如何使用 N-gram 来呈现文本,以及它们在关联上下文中扮演什么角色。我们将看到单词嵌入,其中单词的表示被转换或映射为数字(实数),以便机器能够以更好的方式理解和处理它们。由于文本的数量,这可能导致高维数的问题。因此,接下来,我们将看到如何以保持上下文的方式降低向量的维数。

在本章中,我们将讨论以下主题:

  • N-grams
  • 单词嵌入
  • 手套
  • word2vec
  • 降维
  • 主成分分析
  • 分布式随机邻居嵌入

N-grams

N-grams 是一种概率模型,用于预测下一个单词、文本或字母。它以统计结构捕捉语言,因为机器更擅长处理数字而不是文本。许多公司在拼写纠正和建议、断词或总结文本时使用这种方法。让我们试着去理解它。n-gram 只是一系列单词或字母,主要是单词。考虑句子"This is n-gram model",它有四个单词或记号,所以它是一个 4-gram;来自同一文本的 n-gram 将是“这是 n-gram”和“是 n-gram 模型”。两个单词是一个双字母,一个单词是一个单字母。让我们使用 Java 和 OpenNLP 来尝试一下:

        String sampletext = "This is n-gram model";
        System.out.println(sampletext);

        StringList tokens = new             StringList(WhitespaceTokenizer.INSTANCE.tokenize(sampletext));
        System.out.println("Tokens " + tokens);

        NGramModel nGramModel = new NGramModel();
        nGramModel.add(tokens,3,4); 

        System.out.println("Total ngrams: " + nGramModel.numberOfGrams());
        for (StringList ngram : nGramModel) {
            System.out.println(nGramModel.getCount(ngram) + " - " + ngram);
        }

我们从一个字符串开始,使用记号赋予器,我们得到所有的记号。利用nGramModel,我们计算出 N-grams 中的N;在前面的例子中,它是 3-gram,输出如下:

This is n-gram model
Tokens [This,is,n-gram,model]
Total ngrams: 3
1 - [is,n-gram,model]
1 - [This,is,n-gram]
1 - [This,is,n-gram,model]

如果我们将n-gram行改为 2,输出如下:

This is n-gram model
Tokens [This,is,n-gram,model]
Total ngrams: 6
1 - [is,n-gram,model]
1 - [n-gram,model]
1 - [This,is,n-gram]
1 - [This,is,n-gram,model]
1 - [is,n-gram]
1 - [This,is]

使用n-gram,我们可以找到一个单词序列的概率:哪个单词出现在给定单词 x 之前或之后的概率。从前面的二元模型中,我们可以得出结论,model出现在单词n-gram之后的概率比其他任何单词都高。

下一步是准备一个频率表,找出接下来会出现的单词;例如,对于二元模型,该表如下所示:

| 字 1 | 字 2 | 计数/频率 |
| 是 | 这 | Fifty-five thousand |
| 是 | 这 | Twenty-five thousand |
| 是 | 这 | Forty-five thousand |

从这个表中,我们可以说在给定的上下文中,单词最有可能出现在单词之前。这看起来很简单,但是想想有 20,000 或更多单词的文本。在这种情况下,频率表可能需要数十亿个条目。

另一种方式是用概率进行估算,用带单词 w1,w2 的句子 W ,....wn ,我们要从 Wwi 的概率将是:

这里, N =总字数c() 表示字数。使用概率链规则,它将是这样的:

让我们试着理解我们的句子,“这是 n 元模型”:

P("这是 n-gram 模型")= P("这")P("是" | "这")P("n-gram"| "这是")P("模型" | "这是 n-gram")

这看起来简单,但对于长句子和计算估计,这并不简单。但是,使用马尔可夫假设,该等式可以被简化,因为马尔可夫假设说一个单词出现的概率取决于前一个单词:

P("这是 n-gram 模型")= P("这")P("是" | "这")P("n-gram"| "是")P("模型" | "n-gram")

所以,现在,我们可以这样说:

单词嵌入

需要教会计算机处理上下文。比如说,“我喜欢吃苹果。”电脑需要明白,在这里,苹果是一种水果,而不是一家公司。我们希望单词具有相同含义的文本具有相同的表示,或者至少是相似的表示,以便机器可以理解单词具有相同的含义。单词嵌入的主要目的是捕获尽可能多的关于单词的上下文、层次和形态信息。

单词嵌入可以以两种方式分类:

  • 基于频率的嵌入
  • 基于预测的嵌入

顾名思义,基于频率的嵌入使用计数机制,而基于预测的嵌入使用概率机制。

基于频率的嵌入可以以不同的方式完成,使用计数向量、TD-IDF 向量或同现向量/矩阵。计数向量试图从所有文档中学习。它将学习一个词汇项,并计算它在目标文档中出现的次数。让我们考虑一个非常简单的例子,有两个文档, d1d2 :

  • d1 =计数向量,给定总字数
  • d2 = Count 函数,返回集合中值的总数

下一步是寻找记号,它们是 ["计数"、"向量"、"给予"、"总计"、" of "、"字"、"返回"、"数字"、"值"、" in "、" set"]

给定两个文档和十一个令牌,计数向量或矩阵将如下所示:

| | 计数 | 矢量 | | 总计 | | | 返回 | | | 中的 | 设置 |
| d1 | Two | one | one | one | one | one | Zero | Zero | Zero | Zero | Zero |
| d2 | one | Zero | Zero | one | one | Zero | one | one | one | one | one |

但是,当有很多文档、文本量很大并且有大量文本时,矩阵将很难构造并且包含许多行和列。有时,常用词被删除,如 a、an、the 和 this。

第二种方法是 TF-IDF 载体。 TF 代表词频,IDF 代表逆文档频率。这种方法背后的想法是删除所有文档中常见的、出现频率很高的不必要的单词,但不添加任何意义。这包括诸如 a、an、the、This、that 和 are 等单词。“The”是英语中最常见的单词,因此它会在任何文档中频繁出现。

让我们将 TF 定义为术语在文档中出现的次数/术语在文档中的数量, IDF = log(N/n) ,其中 N 是文档的数量, n 是术语在文档中出现的数量。考虑前面的例子,术语或字数在 d1 中出现两次,在 d2 中出现一次,因此其 TF 计算如下:

  • TF(计数/ d1) = 2/7
  • TF(计数/d2) = 1/8
  • TF(总数/d1) = 1/2
  • TF(总数/d2) = 1/2

让我们为单词或 term total 计算 IDF。**总计在两个文档中出现一次,因此 IDF 将为:

IDF(总计)= log(2/2) = 0

所以,如果这个词出现在每个文档中,那么这个词就有可能不太相关,可以忽略。如果该术语出现在一些文档中,而不是所有文档中,则它可能与字数有一些关联:

IDF(计数)= log(3/2) = 0.17609

为了计算 TF-IDF,我们只需将上一步计算的值相乘:

TF-IDF(合计,d1) = 1/2 * 0 = 0

TF-IDF(count,d1) = 2/7 * 0.17609 = 0.0503

另一种方法是使用共现向量或矩阵。它对一起出现的单词起作用,因此将具有相似的上下文,并因此捕获单词之间的关系。它通过决定上下文窗口的长度来工作,上下文窗口定义了要查找的单词的数量。考虑句子“这是单词嵌入的例子。”

当我们说上下文窗口的大小为 2 时,这意味着我们只对给定单词之前和之后的两个单词感兴趣。假设单词是“word”,那么当我们计算它的共现时,将只考虑“word”之前的两个单词和“word”之后的两个单词。这样的表格或矩阵被转换成概率。它有许多优点,因为它保留了单词之间的关系,但是这种矩阵的大小是巨大的。

另一种方法是使用基于预测的嵌入,这可以使用连续单词包 ( CBOW )或跳格模型来完成。CBOW 预测一个词在给定情境、上下文或场景中出现的概率,可以是单个词,也可以是多个词。考虑句子“使用连续单词包的示例单词”这样,上下文就成了 ["样"、"词"、"用"、"连续"、"包"、"的"、"词】] 。这将被输入一个神经网络。现在,它将帮助我们预测给定上下文中的单词。

另一种方法是使用 skip-gram 模型,该模型使用与 CBOW 相同的方法,但其目的是根据上下文预测给定单词的所有其他单词,也就是说,它应该预测给定单词的上下文。

这两种方法都需要理解神经网络,其中输入通过使用权重的隐藏层传递。下一层是使用 softmax 函数计算的输出层,其值与原始值进行比较,原始值可能不同于第一次运行的值,然后计算损失。损失是原始值和预测值之间的差异;然后,这个损失被反向传播,权重被调整,并且该过程被重复,直到损失最小或接近 0。

在接下来的几节中,我们将看到如何使用 word2vec,它是 CBOW 和 skip-gram 模型的组合。

手套

单词表示的全局向量 ( 手套)是单词表示的模型。它属于无监督学习的范畴。它通过开发单词出现的计数矩阵来学习。最初,它从存储几乎所有单词及其共现信息的大矩阵开始,该矩阵存储一些单词在给定文本的序列中出现的频率。Stanford NLP 中提供了对 GloVe 的支持,但 Java 中没有实现。要了解更多关于 GloVe 的信息,请访问 https://nlp.stanford.edu/pubs/glove.pdf。斯坦福手套的简介和一些资源可以在 https://nlp.stanford.edu/projects/glove/找到。为了了解 GloVe 的功能,我们将使用在github.com/erwtokritos/JGloVe找到的 GloVe 的 Java 实现。

代码还包括测试文件和文本文件。文本文件的内容如下:

human interface computer
survey user computer system response time
eps user interface system
system human system eps
user response time
trees
graph trees
graph minors trees
graph minors survey
I like graph and stuff
I like trees and stuff
Sometimes I build a graph
Sometimes I build trees

GloVe 展示了与上一篇文章相似的单词。从前面的文本中查找类似于graph的单词的结果如下:

INFO: Building vocabulary complete.. There are 19 terms
Iteration #1 , cost = 0.4109707480627031
Iteration #2 , cost = 0.37748817335537205
Iteration #3 , cost = 0.3563396433036622
Iteration #4 , cost = 0.3483667149265019
Iteration #5 , cost = 0.3434632969758875
Iteration #6 , cost = 0.33917154339742045
Iteration #7 , cost = 0.3304641363014488
Iteration #8 , cost = 0.32717383183159243
Iteration #9 , cost = 0.3240225514512226
Iteration #10 , cost = 0.32196412138868596
@trees
@minors
@computer
@a
@like
@survey
@eps
@interface
@and
@human
@user
@time
@response
@system
@Sometimes

所以,第一个匹配的单词是“树”,然后是“未成年人”,依此类推。它用于测试的代码如下:

        String file = "test.txt";

        Options options = new Options(); 
        options.debug = true;

        Vocabulary vocab = GloVe.build_vocabulary(file, options);

        options.window_size = 3;
        List<Cooccurrence> c =  GloVe.build_cooccurrence(vocab, file, options);

        options.iterations = 10;
        options.vector_size = 10;
        options.debug = true;
        DoubleMatrix W = GloVe.train(vocab, c, options);  

        List<String> similars = Methods.most_similar(W, vocab, "graph", 15);
        for(String similar : similars) {
            System.out.println("@" + similar);
        }

Word2vec

GloVe 是一个基于计数的模型,其中创建了一个矩阵来对单词进行计数,而 word2vec 是一个预测模型,它使用预测和损失调整来查找相似性。它像一个前馈神经网络一样工作,并使用各种技术进行优化,包括随机梯度下降 ( SGD ),这些都是机器学习的核心概念。它在从向量表示中的给定上下文单词预测单词时更有用。我们将使用来自 https://github.com/IsaacChanghau/Word2VecfJava 的 word2vec 的实现。我们还需要来自drive . Google . com/file/d/0 b 7 xkcwpi 5 kdynlnuttlss 21 pqmm/edit 的GoogleNews-vectors-negative300.bin文件?usp=sharing ,因为它包含针对GoogleNews数据集的预训练向量,包含 300 个维度的向量,包含 300 万个单词和短语。示例程序将查找相似的单词来删除。以下是输出示例:

loading embeddings and creating word2vec...
[main] INFO org.nd4j.linalg.factory.Nd4jBackend - Loaded [CpuBackend] backend
[main] INFO org.nd4j.nativeblas.NativeOpsHolder - Number of threads used for NativeOps: 2
[main] INFO org.reflections.Reflections - Reflections took 410 ms to scan 1 urls, producing 29 keys and 189 values 
[main] INFO org.nd4j.nativeblas.Nd4jBlas - Number of threads used for BLAS: 2
[main] INFO org.nd4j.linalg.api.ops.executioner.DefaultOpExecutioner - Backend used: [CPU]; OS: [Linux]
[main] INFO org.nd4j.linalg.api.ops.executioner.DefaultOpExecutioner - Cores: [4]; Memory: [5.3GB];
[main] INFO org.nd4j.linalg.api.ops.executioner.DefaultOpExecutioner - Blas vendor: [OPENBLAS]
[main] INFO org.reflections.Reflections - Reflections took 373 ms to scan 1 urls, producing 373 keys and 1449 values 
done...
kill    1.0000001192092896
kills    0.6048964262008667
killing    0.6003166437149048
destroy    0.5964594483375549
exterminate    0.5908634066581726
decapitate    0.5677944421768188
assassinate    0.5450955629348755
behead    0.532557487487793
terrorize    0.5281200408935547
commit_suicide    0.5269641280174255
0.10049013048410416
0.1868356168270111

降维

单词嵌入现在是自然语言处理的基本构件。GloVe,或者 word2vec,或者任何其他形式的单词嵌入都会生成一个二维矩阵,但是它存储在一维向量中。 Dimensonality 这里指的是这些向量的大小,和词汇量的大小不一样。下图取自 https://nlp.stanford.edu/projects/glove/,显示了词汇与向量维度的关系:

大维度的另一个问题是在现实世界中使用单词嵌入所需的内存;具有超过一百万个标记的简单的 300 维向量将需要 6 GB 或更多的内存来处理。在真实的 NLP 用例中,使用这么多内存是不实际的。最好的方法是减少维数来减小尺寸。 t 分布随机邻居嵌入 ( t-SNE )和主成分分析 ( PCA )是两种常用的实现降维的方法。在下一节中,我们将看到如何使用这两种算法来实现降维。

主成分分析

主成分分析 ( PCA )是一种线性的确定性算法,试图捕捉数据内的相似性。一旦发现相似性,就可以用它从高维数据中去除不必要的维度。它使用特征向量和特征值的概念。假设你对矩阵有基本的了解,一个简单的例子将帮助你理解特征向量和特征值:

这相当于以下内容:

这是特征向量的情况, 4 是特征值。

PCA 方法很简单。它从数据中减去平均值开始;然后,它找到协方差矩阵,并计算其特征向量和特征值。一旦你有了特征向量和特征值,把它们从高到低排序,这样我们就可以忽略不太重要的部分。如果特征值很小,损耗可以忽略不计。如果您有具有 n 维的数据,并且您计算了 n 个特征向量和特征值,您可以从 n 个特征向量中选择一些,比如说, m 个特征向量,其中 m 将总是小于 n ,因此最终数据集将只有 m 维。

分布式随机邻居嵌入

T-分布式随机邻居嵌入 ( t-SNE ),广泛应用于机器学习中,是一种非线性、非确定性的算法,创建了数千维数据的二维映射。

换句话说,它将高维空间中的数据转换成 2D 平面。SNE 霸王龙试图保留数据中的本地邻居。这是一种非常流行的降维方法,因为它非常灵活,能够在其他算法失败的地方找到数据中的结构或关系。它通过计算对象 i 选择潜在邻居 j 的概率来做到这一点。它将从高维空间中选取相似的对象,因为它比不太相似的对象具有更高的概率。它使用对象之间的欧几里德距离作为相似性度量的基础。t-SNE 使用困惑功能进行微调,并决定如何平衡本地和全球数据。

t-SNE 实现有多种语言版本;我们将使用在 https://github.com/lejon/T-SNE-Java可用的那个。使用gitmvn,您可以构建和使用这里提供的例子。执行以下命令:

> git clone https://github.com/lejon/T-SNE-Java.git
> cd T-SNE-Java
> mvn install
> cd tsne-demo
> java -jar target/tsne-demos-2.4.0.jar -nohdr -nolbls src/main/resources/datasets/iris_X.txt 

输出如下所示:

TSneCsv: Running 2000 iterations of t-SNE on src/main/resources/datasets/iris_X.txt
NA string is: null
Loaded CSV with: 150 rows and 4 columns.
Dataset types:[class java.lang.Double, class java.lang.Double, class java.lang.Double, class java.lang.Double]
 V0             V1             V2             V3
 0     5.10000000     3.50000000     1.40000000     0.20000000
 1     4.90000000     3.00000000     1.40000000     0.20000000
 2     4.70000000     3.20000000     1.30000000     0.20000000
 3     4.60000000     3.10000000     1.50000000     0.20000000
 4     5.00000000     3.60000000     1.40000000     0.20000000
 5     5.40000000     3.90000000     1.70000000     0.40000000
 6     4.60000000     3.40000000     1.40000000     0.30000000
 7     5.00000000     3.40000000     1.50000000     0.20000000
 8     4.40000000     2.90000000     1.40000000     0.20000000
 9     4.90000000     3.10000000     1.50000000     0.10000000

Dim:150 x 4
000: [5.1000, 3.5000, 1.4000, 0.2000...]
001: [4.9000, 3.0000, 1.4000, 0.2000...]
002: [4.7000, 3.2000, 1.3000, 0.2000...]
003: [4.6000, 3.1000, 1.5000, 0.2000...]
004: [5.0000, 3.6000, 1.4000, 0.2000...]
 .
 .
 .
145: [6.7000, 3.0000, 5.2000, 2.3000]
146: [6.3000, 2.5000, 5.0000, 1.9000]
147: [6.5000, 3.0000, 5.2000, 2.0000]
148: [6.2000, 3.4000, 5.4000, 2.3000]
149: [5.9000, 3.0000, 5.1000, 1.8000]
X:Shape is = 150 x 4
Using no_dims = 2, perplexity = 20.000000, and theta = 0.500000
Computing input similarities...
Done in 0.06 seconds (sparsity = 0.472756)!
Learning embedding...
Iteration 50: error is 64.67259135061494 (50 iterations in 0.19 seconds)
Iteration 100: error is 61.50118570075227 (50 iterations in 0.20 seconds)
Iteration 150: error is 61.373758889762875 (50 iterations in 0.20 seconds)
Iteration 200: error is 55.78219488135168 (50 iterations in 0.09 seconds)
Iteration 250: error is 2.3581173593529687 (50 iterations in 0.09 seconds)
Iteration 300: error is 2.2349608757095827 (50 iterations in 0.07 seconds)
Iteration 350: error is 1.9906437450336596 (50 iterations in 0.07 seconds)
Iteration 400: error is 1.8958764344779482 (50 iterations in 0.08 seconds)
Iteration 450: error is 1.7360726540960958 (50 iterations in 0.08 seconds)
Iteration 500: error is 1.553250634564741 (50 iterations in 0.09 seconds)
Iteration 550: error is 1.294981722012944 (50 iterations in 0.06 seconds)
Iteration 600: error is 1.0985607573299603 (50 iterations in 0.03 seconds)
Iteration 650: error is 1.0810715645272573 (50 iterations in 0.04 seconds)
Iteration 700: error is 0.8168399675722107 (50 iterations in 0.05 seconds)
Iteration 750: error is 0.7158739920771124 (50 iterations in 0.03 seconds)
Iteration 800: error is 0.6911748222330966 (50 iterations in 0.04 seconds)
Iteration 850: error is 0.6123536061655738 (50 iterations in 0.04 seconds)
Iteration 900: error is 0.5631133416913786 (50 iterations in 0.04 seconds)
Iteration 950: error is 0.5905547118496892 (50 iterations in 0.03 seconds)
Iteration 1000: error is 0.5053631170520657 (50 iterations in 0.04 seconds)
Iteration 1050: error is 0.44752244538411406 (50 iterations in 0.04 seconds)
Iteration 1100: error is 0.40661841893114614 (50 iterations in 0.03 seconds)
Iteration 1150: error is 0.3267394426152807 (50 iterations in 0.05 seconds)
Iteration 1200: error is 0.3393774577158965 (50 iterations in 0.03 seconds)
Iteration 1250: error is 0.37023103950965025 (50 iterations in 0.04 seconds)
Iteration 1300: error is 0.3192975790641602 (50 iterations in 0.04 seconds)
Iteration 1350: error is 0.28140161036965816 (50 iterations in 0.03 seconds)
Iteration 1400: error is 0.30413739839879855 (50 iterations in 0.04 seconds)
Iteration 1450: error is 0.31755361125826165 (50 iterations in 0.04 seconds)
Iteration 1500: error is 0.36301524742916624 (50 iterations in 0.04 seconds)
Iteration 1550: error is 0.3063801941900375 (50 iterations in 0.03 seconds)
Iteration 1600: error is 0.2928584822753138 (50 iterations in 0.03 seconds)
Iteration 1650: error is 0.2867502934852756 (50 iterations in 0.03 seconds)
Iteration 1700: error is 0.470469997545481 (50 iterations in 0.04 seconds)
Iteration 1750: error is 0.4792376115843584 (50 iterations in 0.04 seconds)
Iteration 1800: error is 0.5100126924750723 (50 iterations in 0.06 seconds)
Iteration 1850: error is 0.37855035406353427 (50 iterations in 0.04 seconds)
Iteration 1900: error is 0.32776847081948496 (50 iterations in 0.04 seconds)
Iteration 1950: error is 0.3875134029990107 (50 iterations in 0.04 seconds)
Iteration 1999: error is 0.32560416632168365 (50 iterations in 0.04 seconds)
Fitting performed in 2.29 seconds.
TSne took: 2.43 seconds

这个例子使用了iris_X.txt,它有 150 行 4 列,所以尺寸是 150 x 4。它试图通过将困惑度设置为 20 并将θ设置为 0.5 来将这些维度减少到 2。它对iris_X.txt中提供的数据进行迭代,并使用梯度下降,在 2000 次迭代后得出 2D 平面上的图形。该图显示了 2D 平面中数据的聚类,从而有效地降低了维度。对于如何实现这一点的数学方法,有许多关于该主题的论文,维基百科的文章(en . Wikipedia . org/wiki/T-distributed _ random _ neighbor _ embedding)也对此进行了解释。

摘要

在这一章中,我们讨论了单词嵌入以及它在自然语言处理中的重要性。n-gram 用于显示如何将单词视为向量,以及如何存储单词数以找到相关性。GloVe 和 word2vec 是两种常见的单词嵌入方法,其中单词计数或概率存储在向量中。这两种方法都导致高维数,这在现实世界中是不可行的,尤其是在移动设备或内存较少的设备上。我们已经看到了两种不同的降维方法。在下一章中,第七章,信息检索我们将看到如何从文本等非结构化格式中进行信息检索。

七、信息检索

信息检索 ( IR )处理在非结构化数据中寻找信息。任何没有特定或一般化结构的数据都是非结构化数据,处理这样的数据对机器来说是一个巨大的挑战。非结构化数据的一些例子是本地 PC 或 web 上可用的文本文件、doc 文件、XML 文件等。因此,处理如此大量的非结构化数据并找到相关信息是一项具有挑战性的任务。

我们将在本章中讨论以下主题:

  • 布尔检索
  • 字典和容错检索
  • 向量空间模型
  • 评分和术语权重
  • 逆文档频率
  • TF-IDF 加权
  • 信息检索系统的评价

布尔检索

布尔检索处理一种检索系统或算法,其中 IR 查询可以被视为使用操作ANDORNOT的术语的布尔表达式。布尔检索模型是一种将文档视为单词并可以使用布尔表达式应用查询词的模型。一个标准的例子是考虑莎士比亚的文集。该查询用于确定包含单词“Brutus”和“Caesar”但不包含“Calpurnia”的戏剧。使用在基于 Unix 的系统上可用的grep命令,这样的查询是可行的。

当文档大小有限时,这是一个有效的过程,但是要快速处理大的文档或 web 上可用的数据量,并根据出现次数对其进行排序是不可能的。

另一种方法是提前为文档编制索引。方法是创建一个关联矩阵,以二进制形式记录,并标记该术语是否出现在给定的播放中:

| | 安东尼和克娄巴特拉 | 尤利乌斯·凯撒 | 暴风雨 | 哈姆雷特 | 奥赛罗 | 麦克白 |
| 布鲁图斯 | one | one | Zero | Zero | Zero | one |
| 凯撒 | one | one | Zero | one | Zero | Zero |
| 卡尔珀尼亚 | Zero | one | Zero | Zero | Zero | Zero |
| 怜悯 | one | Zero | one | one | one | one |
| 错误 | one | Zero | one | one | one | Zero |

现在,为了回答前面对“布鲁图斯”和“凯撒”而不是“卡尔珀尼亚”的请求,这个查询可以变成 110100 和 110111 和 101111 = 100100,所以答案是安东尼和克利奥帕特拉哈姆雷特是满足我们查询的戏剧。

前面的矩阵很好,但考虑到语料库很大,它可以增长为任何带有 1 和 0 条目的东西。设想创建一个包含 100 万个文档的 500,000 个术语的矩阵,这将产生一个 500,000 x 100 万个维度的矩阵。如上表所示,矩阵条目将为 0 和 1,因此使用倒排索引。它以字典的形式存储术语和文档列表,如下图所示:

取自 https://nlp.stanford.edu/IR-book/pdf/01bool.pdf

术语中的文档来自一个列表,称为过帐列表,单个文档称为过帐。为了创建这样的结构,文档被标记化,并且所创建的标记通过语言预处理被规范化。一旦形成了规范化的标记,就创建了字典和发布。为了提供排名,还存储了术语的频率,如下图所示:

存储的额外信息对于排名检索模型中的搜索引擎是有用的。为了高效的查询处理,还对发布列表进行排序。使用这种方法,减少了存储需求;回想一下有 1 和 0 的 m x n 矩阵。这也有助于处理布尔查询或检索。

字典和容错检索

字典数据结构存储列表术语词汇表,以及包含给定术语的文档列表,也作为发布。

字典数据结构可以以两种不同的方式存储:使用哈希表或树。当语料库增长时,存储这种数据结构的幼稚方法将导致性能问题。一些信息检索系统使用散列方法,而另一些使用树方法来制作字典。这两种方法各有利弊。

哈希表以整数的形式存储词汇术语,这是通过哈希得到的。哈希表中的查找或搜索更快,因为它是时间常数 O(1) 。如果搜索是基于前缀的搜索,如查找以“abc”开头的文本,那么如果使用散列表来存储术语,将不起作用,因为术语将被散列。不容易找到微小的变异。随着条款的增多,重复使用的成本也越来越高。

基于树方法使用树结构,通常是二叉树,这对于搜索非常有效。它有效地处理前缀库搜索。它比较慢,因为搜索需要花费 O(log M) 的时间。采油树的每次重新平衡都很昂贵。

通配符查询

通配符查询使用*来指定要搜索的内容。它可以出现在不同的地方,如单词的开头或结尾。搜索词可能以*its开头,这意味着查找以its结尾的单词。这种查询称为后缀查询。搜索词可能会在末尾使用*,比如its*,表示查找以its开头的单词。这种查询称为前缀查询。就树而言,前缀查询很容易,因为它们需要我们在its <= t <= itt之间查找术语。后缀查询需要额外的树来维护向后移动的术语。下一种需要更多操作的查询是中间有*的查询,比如"fil*er""se*te""pro*cent"。要解决这样的查询,需要找到"fil*""*er",并将两个集合的结果求交集。这是一个昂贵的操作,因为需要在树的两个方向上遍历;这需要一个变通方法来简化它。一种方法是修改查询,使其仅在末尾包含"*"。permuterm 索引方法为单词添加了一个特殊字符"$";例如,术语“你好”可以表示为hello$ello$hllo$helo$helo$hell。让我们假设这个查询是针对hel*o的,那么它将寻找helo,以o$hel结束。它只是旋转通配符,使其只出现在末尾。它将 B 树中的所有旋转相加。也很占地方。另一种方法是使用 bigram (k-gram)索引,它比 permuterm 索引更有效。在二元模型索引中,所有的 bigram 都被枚举。例如,“四月是最残酷的一个月”,拆分成 2-grams (bigrams)将如下所示:

$a, ap, pr, ri, il, l$, $i, is, s$, $t, th, he, e$, $c, cr, ru, ue, el, le, es, st, t$, $m, mo, on, nt, h$

$用来表示学期的开始或结束。它为所有二元模型和包含该二元模型的字典术语维护第二个索引的倒排形式。它检索所有匹配二元模型的帖子,并与整个列表相交。现在,像hel*这样的查询作为$hheel运行。它应用后置过滤器来过滤不相关的结果。它既快又节省空间。

拼写纠正

拼写纠正最好的例子是谷歌。当我们搜索拼写不正确的内容时,它会给出正确的拼写建议,如下图所示:

谷歌上的拼写纠正简单例子

大多数拼写校正算法使用的两个基本原则如下:

  • 找到与拼写错误的单词最接近的匹配项。这就要求我们对术语有接近度。
  • 如果两个或两个以上的单词是正确的,并且连在一起,请使用最常用的一个。最常见的单词是基于文档中每个术语的计数计算的;选择最高的。

拼写校正的两种具体形式是孤立术语校正和上下文敏感校正。孤立术语校正处理拼写错误。基本上,它检查每个单词的拼写错误;它不考虑句子的上下文。例如,如果遇到单词“form ”,而不是单词“from ”,它会将其视为正确,因为拼写是正确的。上下文敏感校正将查看周围的单词,并可以建议所需的校正,因此它可以建议“形式”而不是“形式”如果给定的句子是“我们从 A 点飞到 B 点”,在这个句子中,单词“form”是错误的,但是拼写是正确的,所以孤立的术语校正将把它视为正确的,而上下文敏感的校正将建议“from”而不是“form”

桑迪克斯

当拼写错误是由听起来像目标术语的查询引起时,需要语音纠正。这主要发生在人名中。这个想法是为每个词生成一个散列,使其与发音相同的单词相同。算法执行语音散列,使得对于相似发音单词的散列是相同的,这被称为 Soundex 算法。它是 1981 年为美国人口普查而发明的。方法如下:

  1. 将每个要索引的术语转换为四个字符的简化形式。从这些简化形式到原始术语建立倒排索引;称之为 Soundex 指数。
  2. 对查询词进行同样的操作。
  3. 当查询要求 Soundex 匹配时,搜索这个 Soundex 索引。

它是许多流行数据库提供的标准算法。Soundex 对信息检索没有太大帮助,但它有自己的应用,其中按人名搜索很重要。

向量空间模型

布尔检索工作良好,但它只给出二进制输出;它表示术语匹配或不在文档中,如果只有有限数量的文档,这很好。如果文档数量增加,生成的结果人类很难遵循。考虑一个搜索词,在一百万个文档中搜索 X,其中一半返回肯定结果。下一阶段是根据某种基础(比如等级或其他机制)对文档进行排序,以显示结果。

如果需要排名,那么文档需要附加某种分数,这是由搜索引擎给出的。对于普通用户来说,编写布尔查询本身是一项困难的任务,他们必须使用 and、or 和 not 进行查询。实时查询可以简单到单个单词查询,也可以复杂到包含多个单词的句子。

向量空间模型可以分为三个阶段:

  • 文档索引,从文档中提取术语
  • 对索引术语进行加权,从而可以增强检索系统
  • 基于查询和相似性度量对文档进行排序

总是有元数据与包含各种类型信息的文档相关联,例如:

  • 作者详细信息
  • 编成日期
  • 文件的格式
  • 标题
  • 出版日期
  • 抽象(尽管不总是)

这些元数据有助于形成查询,例如“搜索作者为 xyz 并发表于 2017 的所有文档”或“搜索标题包含单词 AI 且作者为 ABC 的文档”对于这样的查询,维护参数索引,并且这样的查询被称为参数搜索。区域包含自由文本,如标题,这在参数索引中是不可能的。通常,为每个参数准备一个单独的参数指数。搜索标题或摘要需要区域方法。为每个区域准备了一个单独的索引,如下图所示:

这确保了数据的有效检索和存储。对于字段和区域的布尔查询和检索,它仍然工作得很好。

将一组文档表示为公共向量空间中的向量被称为向量空间模型。

评分和术语权重

术语加权处理评估术语相对于文档的重要性。一个简单的方法是,除了停用词之外,在文档中出现较多的术语是一个重要的术语。可以给每个文档分配 0-1 的分数。分数是显示术语或查询在文档中匹配程度的度量。分数为 0 表示文档中不存在该术语。随着术语在文档中的出现频率增加,分数从 0 向 1 移动。因此,对于给定的术语 X ,三个文档、 d1d2d3 的得分分别为 0.2、0.3 和 0.5,这意味着 d3 中的匹配比 d2 更重要,而 d1 对于总得分最不重要。这同样适用于这些区域。如何给术语分配这样的分数或权重需要从一些训练集中学习,或者连续运行并更新术语的分数。

实时查询将是自由文本的形式,而不是布尔表达式的形式;例如,布尔查询将能够回答某个东西是否看起来像 AB ,但是不能回答 C ,而自由文本查询将检查 A 是否与 B 在一起并且 C 是否不存在。因此,在自由文本中,需要一种评分机制,将每个单独术语的分数相加,并根据文档将权重分配给该术语。最简单的方法是分配一个权重,该权重等于该术语在文档中出现的次数。这种加权方案称为词频,通常记为,其中 tf 为词频, t 为词频, d 为文档。

逆文档频率

如果我们认为所有的术语对于所有的查询都具有相同的重要性,那么它并不适用于所有的查询。如果文档与冰有关,很明显“冰”几乎会出现在所有文档中,很可能出现频率很高。收集频率和文档频率是两个不同的术语,需要加以解释。一个集合包含许多文档。集合频率 ( 比照)显示术语 ( t )在集合中所有文档中出现的频率,而文档频率 ( df )显示 t 在单个文档中出现的频率。所以单词“ice”将具有高的收集频率,因为它被假定出现在集合中的所有文档中。一个简单的想法是,如果这些词的收集频率很高,就降低它们的权重。逆频率定义如下:

这里, N 是集合中的文档总数。经常性术语的 idf 可能较低,而非经常性术语的 idf 可能较高。

TF-IDF 加权

TF-IDF 结合了术语频率 ( TF )和逆文档频率 ( IDF )的方法,为文档中的每个术语生成一个权重,使用以下公式完成:

换句话说,它给文档 d 中的术语 t 分配一个权重,如下所示:

  • 如果 term t 在几个文档中出现多次,它将是最高的
  • 如果术语 t 在一个文档中出现的次数很少,它将会更低
  • 如果术语 t 出现在所有文档中,它将是最低的
  • 如果术语 t 没有出现在任何文档中,则为 0

信息检索系统的评价

为了以标准的方式评估一个信息检索系统,需要一个测试集,它应该具有以下内容:

  • 一批文件
  • 测试所需信息的查询集
  • 相关或不相关的二元评估

集合中的文件分为相关和不相关两类。测试文档集合应该有一个合理的大小,这样测试可以有一个合理的范围来找到平均性能。输出的相关性总是相对于所需的信息进行评估,而不是基于查询。换句话说,在结果中有一个查询词并不意味着它是相关的。例如,如果搜索词或查询是“Python”,结果可能显示 Python 编程语言或宠物 Python;两个结果都包含查询术语,但是它是否与用户相关是重要的因素。如果系统包含一个参数化的索引,那么它可以被调优以获得更好的性能,在这种情况下,需要一个单独的测试集合来测试参数。可能发生的情况是,根据也由参数改变的参数,分配的权重是不同的。

有一些标准的测试集可用于信息检索的评估。其中一些如下所列:

  • 克兰菲尔德收集了 1398 份空气动力学杂志的摘要和 225 个问题,以及对所有问题的详尽的相关性判断。
  • 从 1992 年开始,文本检索会议 ( TREC )维持了一个大的 IR 测试系列用于评估。它由 189 万个文档和 450 个信息需求的相关性判断组成。
  • GOV2 拥有 2500 万个网页。
  • NTCIR 侧重于东亚语言和跨语言信息检索的测试集。http://ntcir.nii.ac.jp/about/
  • 路透社由 806,791 份文件组成。
  • 20 新闻组是另一个广泛用于分类的集合。

用于发现检索系统有效性的两个度量是精确度和召回率。Precision 是检索到的相关文档的比例,recall 是找到的相关文档的比例。

摘要

在本章中,我们讲述了如何使用各种技术从非结构化数据中找到信息。我们讨论了布尔检索、字典和容错检索。我们还讲述了通配符查询及其使用方法。简要介绍拼写校正,然后介绍向量空间模型和 TF-IDF 加权,最后介绍信息检索评估。在下一章第八章、文本和文档分类中,我们将讲述如何对文本和文档进行分类。

八、文本和文档分类

在本章中,我们将演示如何使用各种自然语言处理(NLP)API 来执行文本分类。这不要与文本聚类混淆。聚类涉及不使用预定义类别的文本识别。相反,分类使用预定义的类别。在这一章中,我们将着重于文本分类,其中标签被分配给文本以指定其类型。

用于执行文本分类的一般方法从模型的训练开始。该模型被验证,然后用于分类文档。我们将重点关注该流程的培训和使用阶段。

文档可以根据任意数量的属性进行分类,例如主题、文档类型、出版时间、作者、使用的语言和阅读水平。一些分类方法需要人工标记样本数据。

情感分析是一种分类。它关注的是确定文本试图向读者传达什么,通常是以积极或消极的态度。我们将研究几种可用于执行这种类型分析的技术。

我们将在本章中讨论以下主题:

  • 如何使用分类
  • 理解情感分析
  • 文本分类技术
  • 使用 API 对文本进行分类

如何使用分类

对文本进行分类有多种用途:

  • 垃圾邮件检测
  • 作者归属
  • 情感分析
  • 年龄和性别识别
  • 确定文档的主题
  • 语言识别

垃圾邮件是大多数电子邮件用户的不幸现实。如果一封电子邮件可以被归类为垃圾邮件,那么它可以被移动到垃圾邮件文件夹。可以分析文本消息,并且可以使用某些属性来将该电子邮件指定为垃圾邮件。这些属性可能包括拼写错误、缺少合适的收件人电子邮件地址以及不标准的 URL。

分类已被用来确定文件的作者。这已经在历史文献上执行过,例如《联邦党人文集》和《?? 原色》这本书,作者是用分类技术识别的。

情感分析是一种确定一段文本的态度的技术。电影评论一直是这种分析的热门领域,但它几乎可以用于任何产品评论。这有助于公司更好地评估他们的产品是如何被感知的。通常,一个消极或积极的属性被分配给文本。情感分析也称为意见提取/挖掘和主观性分析。消费者信心和股票市场的表现可以从推特和其他来源预测。

分类可用于确定文本作者的年龄和性别,并提供对其作者的更多了解。通常,代词、限定词和名词短语的数量被用来识别作者的性别。女性倾向于使用更多的代词,男性倾向于使用更多的限定词。

当我们需要组织大量文档时,确定文本的主题是很有用的。搜索引擎非常关注这种活动,但它也被简单地用于将文档放在不同的类别中——例如,在标签云中。标签云是反映每个单词出现的相对频率的一组单词。

下图是 IBM Word Cloud Generator 生成的标签云的例子(www . softpedia . com/get/Office-Tools/Other-Office-Tools/IBM-Word-Cloud-Generator . shtml),可以在upload . wikimedia . org/Wikipedia/commons/9/9e/Foundation-l _ Word _ Cloud _ without _ headers _ and _ quotes . png找到:

使用分类技术来支持对文档所使用的语言的识别。这种分析对于许多 NLP 问题非常有用,在这些问题中我们需要应用特定的语言模型。

理解情感分析

对于情绪分析,我们关心的是谁对某个特定的产品或话题有什么样的感觉。例如,这可以告诉我们,一个特定城市的公民对一个运动队的表现有积极或消极的感觉。他们对团队表现的看法可能和对管理的看法不同。

情感分析可以用于自动确定关于产品的某些方面或属性的情感,然后以某种有意义的方式显示结果。

凯利蓝皮书(www.kbb.com/toyota/camry/2014-toyota-camry/?)对 2014 款凯美瑞的回顾说明了这一点 r=471659652516861060 ),如下截图所示:

如果您向下滚动,可以找到关于该型号的专家评论,如下所示:

属性(如总体评分和值)以条形图和数值的形式显示。这些值的计算可以使用情感分析来自动执行。

情感分析可以应用于一个句子、一个子句或者整个文档。情感分析可以是正面的,也可以是负面的,或者它可以是使用数字值的评级,比如 1 到 10。更复杂的态度类型是可能的。

使过程更加复杂的是,在单个句子或文档中,可以针对不同的主题表达不同的情感。

我们如何知道哪些词有哪些类型的情感?这个问题可以用情感词典来回答。在这种情况下,词典是包含不同单词情感的字典。《普通问询者》
(www.wjh.harvard.edu/~inquirer/)就是这样一部词典。它包含 1915 个被认为是肯定的单词。它还包含一个表示其他属性的单词列表,如痛苦、快乐、力量和动机。还有其他可供使用的词典,如 MPQA 主观性线索词典(mpqa.cs.pitt.edu/)。

有时,可能需要建立一个词典。这通常是使用半监督学习来完成的,其中使用一些标记的例子或规则来引导词汇构建过程。当正在使用的词典的领域与我们正在处理的问题领域的领域不匹配时,这是很有用的。

我们不仅对获得积极或消极的情绪感兴趣,我们还对确定情绪的属性感兴趣——有时称为目标。考虑下面的例子:

“旅途很艰难,但乘务员做得很好,让我们很舒服。”

这句话包含两种情绪:粗糙和舒适。第一个是负的,第二个是正的。积极情绪的目标或属性是工作,消极情绪的目标是旅行。

文本分类技术

分类与获取一个特定的文档并确定它是否属于其他几个文档组之一有关。有两种对文本进行分类的基本技术:

  • 基于规则的分类
  • 监督机器学习

基于规则的分类使用单词和其他属性的组合,这些属性是围绕专家制定的规则组织的。这些可能非常有效,但创建它们是一个耗时的过程。

监督机器学习 ( SML )采用一组带注释的训练文档来创建模型。该模型通常被称为分类器。有很多不同的机器学习技术,包括朴素贝叶斯、支持向量机 ( SVM )、k-最近邻。

我们并不关心这些方法是如何工作的,但是感兴趣的读者将会找到无数的扩展这些和其他技术的资料。

使用 API 对文本进行分类

我们将使用 OpenNLP、Stanford API 和 LingPipe 来演示各种分类方法。我们将花更多的时间在 LingPipe 上,因为它提供了几种不同的分类方法。

使用 OpenNLP

DocumentCategorizer接口指定了可用于支持分类过程的方法。该接口由DocumentCategorizerME类实现。该课程将使用最大熵框架将文本分类到预定义的类别中。在本节中,我们将执行以下操作:

  • 演示如何训练模型
  • 说明如何使用该模型

训练 OpenNLP 分类模型

首先,我们必须训练我们的模型,因为 OpenNLP 没有预构建的模型。这个过程包括创建一个训练数据文件,然后使用DocumentCategorizerME模型来执行实际的训练。创建的模型
通常保存在一个文件中以备后用。

培训文件格式由一系列行组成,每行代表一个文档。这一行的第一个词是类别。类别后面是由空格分隔的文本。下面是一个dog类别的例子:

dog The most interesting feature of a dog is its ...

为了演示训练过程,我们创建了en-animals.train文件,其中我们创建了两个类别:猫和狗。对于训练文本,我们使用了维基百科的部分内容。对于狗(en.wikipedia.org/wiki/Dog,我们用作为宠物部分。对于猫(en.wikipedia.org/wiki/Cats_and_humans),我们用了宠物段加上驯养品种段的第一段。我们还删除了部分中的数字引用。

下面的代码显示了每一行的第一部分:

dog The most widespread form of interspecies bonding occurs ... 
dog There have been two major trends in the changing status of  ... 
dog There are a vast range of commodity forms available to  ... 
dog An Australian Cattle Dog in reindeer antlers sits on Santa's lap ... 
dog A pet dog taking part in Christmas traditions ... 
dog The majority of contemporary people with dogs describe their  ... 
dog Another study of dogs' roles in families showed many dogs have  ... 
dog According to statistics published by the American Pet Products  ... 
dog The latest study using Magnetic resonance imaging (MRI) ... 
cat Cats are common pets in Europe and North America, and their  ... 
cat Although cat ownership has commonly been associated  ... 
cat The concept of a cat breed appeared in Britain during ... 
cat Cats come in a variety of colors and patterns. These are physical  ... 
cat A natural behavior in cats is to hook their front claws periodically  ... 
cat Although scratching can serve cats to keep their claws from growing  ... 

创建训练数据时,使用足够大的样本量非常重要。我们使用的数据不足以进行某些分析。然而,正如我们将看到的,它在正确识别类别方面做得很好。

DoccatModel类支持文本的分类和归类。使用基于注释文本的train方法来训练模型。train方法使用一个表示语言的字符串和一个保存训练数据的ObjectStream<DocumentSample>实例。DocumentSample实例保存带注释的文本及其类别。

在下面的示例中,en-animal.train文件用于训练模型。它的输入流被用来创建一个PlainTextByLineStream实例,然后被转换成一个ObjectStream<DocumentSample>实例。然后应用train方法。代码包含在一个try-with-resources块中以处理异常。我们还创建了一个输出流,我们将使用它来持久化模型:

DoccatModel model = null; 
try (InputStream dataIn =  
            new FileInputStream("en-animal.train"); 
        OutputStream dataOut =  
            new FileOutputStream("en-animal.model");) { 
    ObjectStream<String> lineStream 
        = new PlainTextByLineStream(dataIn, "UTF-8"); 
    ObjectStream<DocumentSample> sampleStream =  
        new DocumentSampleStream(lineStream);             
    model = DocumentCategorizerME.train("en", sampleStream); 
    ... 
} catch (IOException e) { 
// Handle exceptions   
} 

输出如下,为了简洁起见,已经缩短了:

    Indexing events using cutoff of 5

      Computing event counts...  done. 12 events
      Indexing...  done.
    Sorting and merging events... done. Reduced 12 events to 12.
    Done indexing.
    Incorporating indexed data for training...  
    done.
      Number of Event Tokens: 12
          Number of Outcomes: 2
        Number of Predicates: 30
    ...done.
    Computing model parameters ...
    Performing 100 iterations.
      1:  ... loglikelihood=-8.317766166719343  0.75
      2:  ... loglikelihood=-7.1439957443937265  0.75
      3:  ... loglikelihood=-6.560690872956419  0.75
      4:  ... loglikelihood=-6.106743124066829  0.75
      5:  ... loglikelihood=-5.721805583104927  0.8333333333333334
      6:  ... loglikelihood=-5.3891508904777785  0.8333333333333334
      7:  ... loglikelihood=-5.098768040466029  0.8333333333333334
    ...
     98:  ... loglikelihood=-1.4117372921765519  1.0
     99:  ... loglikelihood=-1.4052738190352423  1.0
    100:  ... loglikelihood=-1.398916120150312  1.0

使用serialize方法保存模型,如下面的代码所示。模型被保存到en-animal.model文件中,就像在前面的try-with-resources block中打开的一样:

OutputStream modelOut = null; 
modelOut = new BufferedOutputStream(dataOut); 
model.serialize(modelOut);

使用文档分类器对文本进行分类

一旦创建了一个模型,我们就可以使用DocumentCategorizerME类对文本进行分类。我们需要读取模型,创建一个DocumentCategorizerME类的实例,然后调用categorize方法返回一个概率数组,告诉我们文本最适合哪个类别。

因为我们是从文件中读取,所以需要处理异常,如下所示:

try (InputStream modelIn =  
        new FileInputStream(new File("en-animal.model"));) { 
    ... 
} catch (IOException ex) { 
    // Handle exceptions 
} 

通过InputStream,我们创建了DoccatModelDocumentCategorizerME类的实例,如下所示:

DoccatModel model = new DoccatModel(modelIn); 
DocumentCategorizerME categorizer =  
    new DocumentCategorizerME(model); 

使用字符串作为参数调用categorize方法。这将返回一个 double 值数组,其中每个元素都具有文本属于某个类别的可能性。DocumentCategorizerME类的getNumberOfCategories方法返回模型处理的类别数。DocumentCategorizerME类的getCategory方法返回给定类别的索引。

我们在下面的代码中使用了这些方法来显示每个类别及其相应的可能性:

double[] outcomes = categorizer.categorize(inputText); 
for (int i = 0; i<categorizer.getNumberOfCategories(); i++) { 
    String category = categorizer.getCategory(i); 
    System.out.println(category + " - " + outcomes[i]); 
} 

为了测试,我们使用了维基百科中《绿野仙踪》(【http://en.wikipedia.org/wiki/Toto_(Oz)】)中多萝西的狗托托的部分文章。我们用了经典著作的第一句话一节,在这里声明:

String toto = "Toto belongs to Dorothy Gale, the heroine of "  
        + "the first and many subsequent books. In the first " 
        + "book, he never spoke, although other animals, native " 
        + "to Oz, did. In subsequent books, other animals " 
        + "gained the ability to speak upon reaching Oz or " 
        + "similar lands, but Toto remained speechless."; 

为了测试一只猫,我们使用了位于 https://en.wikipedia.org/wiki/Tortoiseshell_cat的维基百科文章中玳瑁和印花布部分的第一句话,如下所示:

String calico = "This cat is also known as a calimanco cat or " 
        + "clouded tiger cat, and by the abbreviation 'tortie'. " 
        + "In the cat fancy, a tortoiseshell cat is patched " 
        + "over with red (or its dilute form, cream) and black " 
        + "(or its dilute blue) mottled throughout the coat.";  

使用toto的文本,我们得到以下输出。这表明该文本应放在dog类别中:

    dog - 0.5870711529777994
    cat - 0.41292884702220056  

使用calico会产生以下结果:

    dog - 0.28960436044424276
    cat - 0.7103956395557574

我们可以使用getBestCategory方法只返回最佳类别。此方法使用结果数组并返回一个字符串。getAllResults方法将以字符串的形式返回所有结果。这两种方法说明如下:

System.out.println(categorizer.getBestCategory(outcomes)); 
System.out.println(categorizer.getAllResults(outcomes)); 

输出如下所示:

cat
dog[0.2896]  cat[0.7104]

使用斯坦福 API

斯坦福 API 支持几个分类器。我们将研究使用ColumnDataClassifier类进行一般分类,使用StanfordCoreNLP管道进行情感分析。斯坦福 API 支持的分类器有时很难使用。通过ColumnDataClassifier类,我们将演示如何对盒子的大小进行分类。通过管道,我们将说明如何确定短文本短语的正面或负面情绪。分类器可以从www-nlp.stanford.edu/wiki/Software/Classifier下载。

使用 ColumnDataClassifier 类进行分类

该分类器使用具有多个值的数据来描述数据。在本演示中,我们将使用一个训练文件来创建一个分类器。然后我们将使用一个测试文件来评估分类器的性能。该类使用属性文件来配置创建过程。

我们将创建一个分类器,试图根据它的尺寸来分类一个盒子。有三种可能的类别:小型、中型和大型。盒子的高度、宽度和长度将被表示为浮点数。它们用于描述箱子的特征。

属性文件指定参数信息,并提供有关训练和测试文件的数据。可以指定许多可能的属性。对于这个例子,我们将只使用几个更相关的属性。

我们将使用下面的属性文件,另存为box.prop。第一组属性处理包含在训练和测试文件中的特性的数量。因为我们使用了三个值,所以指定了三个realValued列。trainFiletestFile属性指定了各自文件的位置和名称:

useClassFeature=true 
1.realValued=true 
2.realValued=true 
3.realValued=true 
trainFile=.box.train 
testFile=.box.test 

培训和测试文件使用相同的格式。每一行由一个类别和其后的定义值组成,每一行由一个制表符分隔。box.train训练文件包含 60 个条目,而box.test文件包含 30 个条目。这些文件可以从github . com/packt publishing/Natural-Language-Processing-with-Java-Second-Edition/或者从 GitHub 资源库下载。下面的代码显示了box.train文件的第一行。品类小;其高度、宽度和长度分别为2.341.601.50:

small  2.34  1.60  1.50

创建分类器的代码如下面的代码所示。使用属性文件作为构造函数的参数创建了一个ColumnDataClassifier类的实例。Classifier接口的一个实例由makeClassifier方法返回。这个接口支持三种方法,我们将演示其中的两种。readTrainingExamples方法从训练文件中读取训练数据:

ColumnDataClassifier cdc =  
    new ColumnDataClassifier("box.prop"); 
Classifier<String, String> classifier =  
    cdc.makeClassifier(cdc.readTrainingExamples("box.train")); 

当执行时,我们得到大量的输出。我们将在这一部分讨论更相关的部分。输出的第一部分重复了属性文件的各个部分:

    3.realValued = true
    testFile = .box.test
    ...
    trainFile = .box.train

下一部分显示数据集的数量,以及关于各种要素的信息,如下所示:

    Reading dataset from box.train ... done [0.1s, 60 items].
    numDatums: 60
    numLabels: 3 [small, medium, large]
    ...
    AVEIMPROVE     The average improvement / current value
    EVALSCORE      The last available eval score
    Iter ## evals ## <SCALING> [LINESEARCH] VALUE TIME |GNORM| {RELNORM} AVEIMPROVE EVALSCORE

然后,分类器对数据进行迭代以创建分类器:

    Iter 1 evals 1 <D> [113M 3.107E-4] 5.985E1 0.00s |3.829E1| {1.959E-1} 0.000E0 - 
    Iter 2 evals 5 <D> [M 1.000E0] 5.949E1 0.01s |1.862E1| {9.525E-2} 3.058E-3 - 
    Iter 3 evals 6 <D> [M 1.000E0] 5.923E1 0.01s |1.741E1| {8.904E-2} 3.485E-3 - 
    ...
    Iter 21 evals 24 <D> [1M 2.850E-1] 3.306E1 0.02s |4.149E-1| {2.122E-3} 1.775E-4 - 
    Iter 22 evals 26 <D> [M 1.000E0] 3.306E1 0.02s
    QNMinimizer terminated due to average improvement: | newest_val - previous_val | / |newestVal| < TOL 
    Total time spent in optimization: 0.07s

此时,分类器就可以使用了。接下来,我们使用测试文件来验证分类器。我们从使用ObjectBank类的getLineIterator方法从文本文件中获取一行开始。这个类支持将读入的数据转换成更标准化的形式。getLineIterator方法以分类器可以使用的格式一次返回一行。此流程的循环如下所示:

for (String line :  
        ObjectBank.getLineIterator("box.test", "utf-8")) { 
    ... 
} 

在 for-each 语句中,从该行创建一个Datum实例,然后使用它的classOf方法返回预测的类别,如下面的代码所示。Datum接口支持包含特性的对象。当用作classOf方法的参数时,由分类器确定的类别被返回:

Datum<String, String> datum = cdc.makeDatumFromLine(line); 
System.out.println("Datum: {"  
    + line + "]\tPredicted Category: "  
    + classifier.classOf(datum)); 

当执行这个序列时,测试文件的每一行都被处理,并且显示预测的类别,如下面的代码所示。这里只显示了前两行和后两行。分类器能够正确分类所有测试数据:

    Datum: {small  1.33  3.50  5.43]  Predicted Category: medium
    Datum: {small  1.18  1.73  3.14]  Predicted Category: small
    ...
    Datum: {large  6.01  9.35  16.64]  Predicted Category: large
    Datum: {large  6.76  9.66  15.44]  Predicted Category: large

为了测试一个单独的条目,我们可以使用makeDatumFromStrings方法来创建一个Datum实例。在下面的代码序列中,创建了一个一维字符串数组,其中每个元素表示一个框的数据值。第一个条目“类别”为空。然后,Datum实例被用作classOf方法的参数来预测它的类别:

String sample[] = {"", "6.90", "9.8", "15.69"}; 
Datum<String, String> datum =  
    cdc.makeDatumFromStrings(sample); 
System.out.println("Category: " + classifier.classOf(datum)); 

此处显示了该序列的输出。它正确地对盒子进行分类:

Category: large

使用斯坦福管道进行情感分析

在本节中,我们将说明如何使用斯坦福 API 来执行情感分析。我们将使用StanfordCoreNLP管道对不同的文本进行分析。

我们将使用三种不同的文本,如下面的代码中所定义的。review字符串是来自烂番茄(www.rottentomatoes.com/m/forrest_gump/)的关于电影阿甘正传的影评:

String review = "An overly sentimental film with a somewhat " 
    + "problematic message, but its sweetness and charm " 
    + "are occasionally enough to approximate true depth " 
    + "and grace. "; 

String sam = "Sam was an odd sort of fellow. Not prone " 
    + "to angry and not prone to merriment. Overall, " 
    + "an odd fellow."; 

String mary = "Mary thought that custard pie was the " 
    + "best pie in the world. However, she loathed " 
    + "chocolate pie."; 

为了执行这个分析,我们需要使用一个情感annotator,如下面的代码所示。这也需要使用tokenizessplitparse标注器。parse注释器提供了更多关于文本的结构信息,这将在第十章、使用解析器提取关系中详细讨论:

Properties props = new Properties(); 
props.put("annotators", "tokenize, ssplit, parse, sentiment"); 
StanfordCoreNLP pipeline = new StanfordCoreNLP(props); 

该文本用于创建一个Annotation实例,然后用作执行实际工作的annotate方法的参数,如下所示:

Annotation annotation = new Annotation(review); 
pipeline.annotate(annotation); 

下面的数组包含不同可能情绪的字符串:

String[] sentimentText = {"Very Negative", "Negative",  
    "Neutral", "Positive", "Very Positive"};

Annotation类的get方法返回一个实现CoreMap接口的对象。在这种情况下,这些对象表示将输入文本拆分成句子的结果,如下面的代码所示。对于每个句子,获得一个代表树形结构的Tree对象的实例,该树形结构包含对情感文本的解析。getPredictedClass方法向sentimentText数组返回一个索引,反映测试的情绪:

for (CoreMap sentence : annotation.get( 
        CoreAnnotations.SentencesAnnotation.class)) { 
    Tree tree = sentence.get( 
        SentimentCoreAnnotations.AnnotatedTree.class); 
    int score = RNNCoreAnnotations.getPredictedClass(tree); 
    System.out.println(sentimentText[score]); 
} 

当使用review字符串执行代码时,我们得到以下输出:

Positive  

这篇课文由三个句子组成。每个的输出如下,显示了每个句子的情感:

Neutral
Negative
Neutral  

正文mary由两个句子组成。每个的输出如下:

Positive
Neutral  

使用 LingPipe 对文本进行分类

在本节中,我们将使用 LingPipe 来演示一些分类任务,包括使用训练好的模型进行一般的文本分类、情感分析和语言识别。我们将涵盖以下分类主题:

  • 使用Classified类训练文本
  • 使用其他培训类别的培训模型
  • 使用 LingPipe 分类文本
  • 使用 LingPipe 执行情感分析
  • 识别使用的语言

本节中描述的几个任务将使用以下声明。LingPipe 附带了几个类别的训练数据。categories数组包含用 LingPipe 打包的类别的名称:

String[] categories = {"soc.religion.christian", 
    "talk.religion.misc","alt.atheism","misc.forsale"}; 

DynamicLMClassifier类用于执行实际的分类。它是使用categories数组创建的,为它提供要使用的类别的名称。nGramSize值指定序列中连续项目的数量,这些项目在模型中用于分类目的:

int nGramSize = 6; 
DynamicLMClassifier<NGramProcessLM> classifier =  
    DynamicLMClassifier.createNGramProcess( 
        categories, nGramSize); 

使用分类类训练文本

使用 LingPipe 的一般文本分类包括使用训练文件训练DynamicLMClassifier类,然后使用该类执行实际的分类。LingPipe 附带了几个训练数据集,可以在名为demos/data/fourNewsGroups/4news-train的 LingPipe 目录中找到。我们将使用这些数据集来说明培训过程。这个例子是在alias-I . com/lingpipe/demos/tutorial/classify/read-me . html找到的过程的简化版本。

我们首先声明trainingDirectory:

String directory = ".../demos"; 
File trainingDirectory = new File(directory  
    + "/data/fourNewsGroups/4news-train"); 

trainingDirectory中,有四个子目录,它们的名称列在categories数组中。在每个子目录中,都有一系列带有数字名称的文件。这些文件包含处理子目录名称的新闻组(【http://qwone.com/~jason/20Newsgroups/】??)数据。

训练模型的过程包括通过DynamicLMClassifier类的handle方法使用每个文件和类别。该方法将使用该文件为该类别创建一个训练实例,然后用该实例扩充模型。这个过程使用嵌套的for循环。

外部的for循环使用目录名创建一个File对象,然后对其应用list方法。list方法返回目录中的文件列表。这些文件的名称存储在trainingFiles数组中,将在内部for循环中使用:

for (int i = 0; i < categories.length; ++i) { 
    File classDir =  
        new File(trainingDirectory, categories[i]); 
    String[] trainingFiles = classDir.list(); 
    // Inner for-loop 
} 

内部的for循环,如下面的代码所示,将打开每个文件并从文件中读取文本。Classification类表示具有指定类别的分类。它与文本一起用来创建一个Classified实例。DynamicLMClassifier类的handle方法用新信息更新模型:

for (int j = 0; j < trainingFiles.length; ++j) { 
    try { 
        File file = new File(classDir, trainingFiles[j]); 
        String text = Files.readFromFile(file, "ISO-8859-1"); 
        Classification classification =  
            new Classification(categories[i]); 
        Classified<CharSequence> classified =  
            new Classified<>(text, classification); 
        classifier.handle(classified); 
    } catch (IOException ex) { 
        // Handle exceptions 
    } 
} 

You can alternatively use the com.aliasi.util.Files class instead in java.io.File; otherwise, the readFromFile method will not be available.

该分类器可以序列化以备后用,如下面的代码所示。AbstractExternalizable类是一个支持对象序列化的实用类。它有一个静态的compileTo方法,接受一个Compilable实例和一个File对象。它将对象写入文件,如下所示:

try { 
    AbstractExternalizable.compileTo( (Compilable) classifier, 
        new File("classifier.model"));
} catch (IOException ex) { 
    // Handle exceptions 
} 

分类器的加载将在本章后面的使用 LingPipe 对文本进行分类部分进行说明。

使用其他培训类别

其他新闻组数据可以在 http://qwone.com/~jason/20Newsgroups/找到。这些数据集合可用于为其他模型定型,如下表所列。虽然只有 20 个类别,但它们可以成为有用的培训模型。有三种不同的下载方式。有些已经过排序,有些则删除了重复数据:

| 新闻组 |
| comp.graphics | sci.crypt |
| comp.os.ms-windows.misc | sci.electronics |
| comp.sys.ibm.pc.hardware | sci.med |
| comp.sys.mac.hardware | sci.space |
| comp.windows.x | misc.forsale |
| rec.autos | talk.politics.misc |
| rec.motoXrcycles | talk.politics.guns |
| rec.sport.baseball | talk.politics.mideast |
| rec.sport.hockey | talk.religion.misc |
| alt.atheism | |

使用 LingPipe 分类文本

为了对文本进行分类,我们将使用DynamicLMClassifier类的classify方法。我们将用两个不同的文本序列来演示它的用法:

这些字符串在此处声明:

String forSale =  
    "Finding a home for sale has never been " 
    + "easier. With Homes.com, you can search new " 
    + "homes, foreclosures, multi-family homes, " 
    + "as well as condos and townhouses for sale. " 
    + "You can even search our real estate agent " 
    + "directory to work with a professional " 
    + "Realtor and find your perfect home."; 
String martinLuther =  
    "Luther taught that salvation and subsequently " 
    + "eternity in heaven is not earned by good deeds " 
    + "but is received only as a free gift of God's " 
    + "grace through faith in Jesus Christ as redeemer " 
    + "from sin and subsequently eternity in Hell."; 

要重用前一节中序列化的分类器,请使用AbstractExternalizable类的readObject方法,如以下代码所示。我们将使用LMClassifier类而不是DynamicLMClassifier类。它们都支持classify方法,但是DynamicLMClassifier类不容易序列化:

LMClassifier classifier = null; 
try { 
    classifier = (LMClassifier)  
        AbstractExternalizable.readObject( 
            new File("classifier.model")); 
} catch (IOException | ClassNotFoundException ex) { 
    // Handle exceptions 
} 

在下面的代码序列中,我们将应用LMClassifier类的classify方法。这将返回一个JointClassification实例,我们用它来确定最佳匹配:

JointClassification classification =  
    classifier.classify(text); 
System.out.println("Text: " + text); 
String bestCategory = classification.bestCategory(); 
System.out.println("Best Category: " + bestCategory);

对于forSale文本,我们得到以下输出:

    Text: Finding a home for sale has never been easier. With Homes.com, you can search new homes, foreclosures, multi-family homes, as well as condos and townhouses for sale. You can even search our real estate agent directory to work with a professional Realtor and find your perfect home.
    Best Category: misc.forsale

对于martinLuther文本,我们得到以下输出:

    Text: Luther taught that salvation and subsequently eternity in heaven is not earned by good deeds but is received only as a free gift of God's grace through faith in Jesus Christ as redeemer from sin and subsequently eternity in Hell.
    Best Category: soc.religion.christian

他们都对文本进行了正确的分类。

使用 LingPipe 进行情感分析

情感分析的执行方式与普通文本分类非常相似。一个区别是,它只使用两个类别:积极和消极。

我们需要使用数据文件来训练我们的模型。我们将通过使用为电影开发的情感数据(www . cs . Cornell . edu/people/pabo/movie-review-data/review _ polarity . tar . gz)来使用在alias-I . com/ling pipe/demos/tutorial/sensation/read-me . html执行的情感分析的简化版本。这些数据是从 IMDb 电影档案馆中的 1000 条正面和 1000 条负面电影评论中得出的。

这些评论需要下载和提取。将提取一个txt_sentoken目录及其两个子目录:negpos。这两个子目录都包含电影评论。尽管这些文件中的一部分可以保留以评估所创建的模型,但是我们将使用所有这些文件来简化解释。

我们将从使用 LingPipe 对文本部分进行分类的中声明的变量的重新初始化开始。categories数组被设置为两个元素的数组来保存这两个类别。使用新的类别数组和大小为8nGramSizeclassifier变量分配一个新的DynamicLMClassifier实例:

categories = new String[2]; 
categories[0] = "neg"; 
categories[1] = "pos"; 
nGramSize = 8; 
classifier = DynamicLMClassifier.createNGramProcess( 
    categories, nGramSize); 

正如我们前面所做的,我们将基于培训文件中的内容创建一系列实例。我们不会详细检查下面的代码,因为它与使用分类类部分的培训文本中的代码非常相似。主要区别在于只需要处理两个类别:

String directory = "..."; 
File trainingDirectory = new File(directory, "txt_sentoken"); 
for (int i = 0; i < categories.length; ++i) { 
    Classification classification =  
        new Classification(categories[i]); 
    File file = new File(trainingDirectory, categories[i]); 
    File[] trainingFiles = file.listFiles(); 
    for (int j = 0; j < trainingFiles.length; ++j) { 
        try { 
            String review = Files.readFromFile( 
                trainingFiles[j], "ISO-8859-1"); 
            Classified<CharSequence> classified =  
                new Classified<>(review, classification); 
            classifier.handle(classified); 
        } catch (IOException ex) { 
            ex.printStackTrace(); 
        } 
    } 
} 

该模型现在可以使用了。我们将使用电影阿甘正传的评论:

String review = "An overly sentimental film with a somewhat " 
    + "problematic message, but its sweetness and charm " 
    + "are occasionally enough to approximate true depth " 
    + "and grace. "; 

我们使用classify方法来执行实际工作。它返回一个Classification实例,其bestCategory方法返回最佳类别,如下所示:

Classification classification = classifier.classify(review); 
String bestCategory = classification.bestCategory(); 
System.out.println("Best Category: " + bestCategory); 

执行时,我们得到以下输出:

Best Category: pos  

这种方法也适用于其他类别的文本。

使用 LingPipe 的语言识别

LingPipe 附带了一个名为langid-leipzig.classifier的模型,它针对几种语言进行了训练,可以在demos/models目录中找到。下表包含支持的语言列表。这个模型是使用从莱比锡语料库(【http://corpora.uni-leipzig.de/】)中获得的训练数据开发的。另一个好工具可以在 http://code.google.com/p/language-detection/找到:

| 语言 | 缩写 | 语言 | 缩写 |
| 加泰罗尼亚语 | 猫 | 意大利的 | 它 |
| 丹麦的 | 男高中生 | 日本人 | 治安官 |
| 英语 | 在中 | 韩国的 | 韩国 |
| 爱沙尼亚语 | 电子工程师 | 挪威的 | 不 |
| 芬兰人的 | 船方不负担装货费用 | 索布人的 | 吸收 |
| 法语 | 神父 | 瑞典的 | 如果 |
| 德国人 | (加在动词之前)表示“否定”,“相反”;(加在名词之前构成动词)表示“除去”,“除掉” | 土耳其的 | tr |

为了使用这个模型,我们基本上使用了本章前面的使用 LingPipe 对文本进行分类一节中使用的相同代码。我们从《阿甘正传》(??)的同一个电影评论开始:

String text = "An overly sentimental film with a somewhat " 
    + "problematic message, but its sweetness and charm " 
    + "are occasionally enough to approximate true depth " 
    + "and grace. "; 
System.out.println("Text: " + text); 

使用langid-leipzig.classifier文件创建LMClassifier实例:

LMClassifier classifier = null; 
try { 
    classifier = (LMClassifier)  
        AbstractExternalizable.readObject( 
            new File(".../langid-leipzig.classifier")); 
} catch (IOException | ClassNotFoundException ex) { 
    // Handle exceptions 
}

使用classify方法,然后应用bestCategory方法,以获得最佳的语言匹配,如下所示:

Classification classification = classifier.classify(text); 
String bestCategory = classification.bestCategory(); 
System.out.println("Best Language: " + bestCategory); 

输出如下,选择英语作为语言:

    Text: An overly sentimental film with a somewhat problematic message, but its sweetness and charm are occasionally enough to approximate true depth and grace. 
    Best Language: en

以下代码示例使用瑞典语的瑞典语维基百科条目的第一句(sv.wikipedia.org/wiki/Svenska)作为文本:

text = "Svenska är ett östnordiskt språk som talas av cirka " 
    + "tio miljoner personer[1], främst i Finland " 
    + "och Sverige."; 

如此处所示,输出正确选择了瑞典语:

    Text: Svenska är ett östnordiskt språk som talas av cirka tio miljoner personer[1], främst i Finland och Sverige.
    Best Language: se

训练可以使用我们在以前的 LingPipe 模型中使用的相同方法进行。执行语言识别时的另一个考虑是文本可以用多种语言书写。这可能会使语言检测过程变得复杂。

摘要

在这一章中,我们讨论了围绕文本分类的问题,并研究了执行这一过程的几种方法。文本的分类对于许多活动都是有用的,例如检测垃圾电子邮件、确定文档的作者、执行性别识别以及执行语言识别。

我们还演示了如何执行情感分析。这种分析关注的是确定一篇文章在本质上是积极的还是消极的。还可以使用该过程来评估其他情感属性。

我们使用的大多数方法都要求我们首先基于训练数据创建一个模型。通常,这个模型需要用一组测试数据来验证。一旦模型被创建,它通常很容易使用。

在下一章中,第九章,主题建模我们将研究解析过程以及它如何有助于从文本中提取关系。

九、主题建模

在这一章中,我们将使用包含一些文本的文档来学习主题建模的基础知识。这里的想法是使用某些可用的方法从文本中获取主题。这个过程属于文本挖掘的范畴,在搜索、聚类和组织文本中起着重要的作用。今天,它被许多网站用于推荐目的,例如当新闻网站根据读者当前正在阅读的文章的主题来推荐文章时。本章涵盖主题建模的基础知识,包括潜在狄利克雷分配 ( LDA )的基本概念。它还将向您展示如何使用 MALLET 包进行主题建模。

我们将在本章中讨论以下主题:

  • 什么是主题建模?
  • LDA 的基础知识
  • 用木槌进行主题建模

什么是主题建模?

简单地说,主题建模是一种技术,通过这种技术,计算机程序试图从文本中提取主题。文本通常是非结构化数据,如博客、电子邮件、文章、书中的一章或类似内容。这是一种文本挖掘方法,但不应与基于规则的文本挖掘相混淆。在机器学习场景中,主题建模属于无监督学习的范畴,其中机器或计算机程序试图通过观察最后一组文本中的一串单词来找到主题。当给定“IT 行业”的主题时,一个好的模型应该产生单词“程序”、“程序员”、“IT”、“计算机”、“软件”和“硬件”。它有助于理解大量文本,并在搜索引擎的运行中发挥着至关重要的作用。

主题建模可以与组织、分类、理解和总结大量文本信息的方法一起使用。它使我们能够使用主题发现集合和注释中隐藏的模式。它从文档集合中找到最能代表集合的单词组。

有许多不同的方法来做主题建模,但最流行的是 LDA。下一节将介绍 LDA 的基础知识。

LDA 的基础知识

在不同的主题建模方法中,LDA 是最常用的方法。这是文本数据挖掘和机器学习的一种形式,其中执行回溯来找出文档的主题。它还涉及概率的使用,因为它是一个生成概率模型。

LDA 将文档表示为基于概率给出主题的主题混合物。

任何给定的文档都有或多或少的机会将某个单词作为其潜在主题;例如,给定一个关于体育的文档,单词“cricket”出现的概率高于单词“Android One Phone”出现的概率。如果文档是关于移动技术的,那么“Android One Phone”这个词出现的概率会高于“cricket”这个词。使用抽样方法,以半随机方式使用狄利克雷分布从文档中选择一些词作为主题。这些随机选择的主题可能不是最适合作为文档的潜在主题,因此对于每个文档,需要检查单词并计算单词来自文档的概率。设 p(主题|文档)是来自文档 d 的一个单词分配给主题 t—p(主题)是来自单词 w 的所有文档的主题 t 的概率。这有助于找到构成主题的每个单词的比例。它查找每个单词在主题中的相关性以及主题在文档中的相关性。现在,给单词 w 重新分配一个新的主题——我们称之为topic’——使用p(topic ' | document) p(word | topic ')*。重复这个过程,直到你完成指定的题目。

为此,LDA 使用文档-术语矩阵,并将其转换为文档-主题矩阵和主题-术语矩阵。LDA 使用采样技术来改进矩阵。假设有标记为 d1、d2、d3 的 N 个文档....dn 。有 M 个项标为 t1、t2、t3....tm ,因此文档-术语矩阵将表示文档中术语的数量,并表示如下:

| | t1 | t2 | t3 | tm |
| d1 | Zero | three | one | Two |
| d2 | Zero | five | four | one |
| d3 | one | Zero | three | Two |
| dn | Zero | one | one | Two |

k 成为我们希望 LDA 建议的主题数量。它将文档-术语矩阵分为维度-主题矩阵和主题-术语矩阵:

| | 话题-1 | 话题-2 | 话题-k |
| d1 | one | Zero | one |
| d2 | one | one | Zero |
| d3 | one | Zero | one |
| dn | one | Zero | one |

文档-主题矩阵[ N x k

| | t1 | t2 | t3 | tm |
| 话题-1 | Zero | one | one | Zero |
| 话题-2 | one | one | Zero | Zero |
| 话题-k | one | Zero | one | Zero |

主题–术语矩阵[ k x m

要了解 LDA 是如何工作的,请访问 https://lettier.com/projects/lda-topic-modeling/。这是一个很好的网页,您可以在其中添加文档,决定主题的数量,并调整 alpha 和 beta 参数来获得主题。

用木槌进行主题建模

MALLET 是主题建模方面的知名库。它还支持文档分类和序列标记。更多关于木槌的信息可以在 http://mallet.cs.umass.edu/index.php找到。要下载 MALLET,请访问 http://mallet.cs.umass.edu/download.php(最新版本是 2.0.6)。下载完成后,解压目录中的 MALLET。它包含 MALLET 目录的sample-data/web/en路径中的.txt格式的样本数据。

第一步是将文件导入 MALLET 的内部格式。为此,打开命令提示符或终端,移动到mallet目录,并执行以下命令:

mallet-2.0.6$ bin/mallet import-dir --input sample-data/web/en --output tutorial.mallet --keep-sequence --remove-stopwords

该命令将生成tutorial.mallet文件。

培养

下一步是使用train-topics构建主题模型,并使用train-topics命令保存output-statetopic-keystopics:

mallet-2.0.6$ bin/mallet train-topics --input tutorial.mallet --num-topics 20 --output-state topic-state.gz --output-topic-keys tutorial_keys.txt --output-doc-topics tutorial_compostion.txt

这将针对20主题进行训练,并将为你的材料语料库中的每个单词以及它们所属的主题创建一个 ZIP 文件。所有的topic-keys将被存储在tutorial_key.txt中。文件的主题建议将存储在tutorial_composition.txt中。

估价

A tutorial_key.txt是一个简单的文本文件,内容看起来会类似于下面的截图:

它包含所有的主题,因为我们要求 20 个主题。文件中的行可以从三个方面来看。第一种是使用从0开始的数字,表示主题编号。第二个数字是 Dirichlet 参数,默认值为2.5,第三种方法是查看显示可能主题的段落。tutorial_compostion.txt文件包含每个主题和每个原始文本文件的百分比分解。tutorial_compostion.txt文件可以在 Excel 或 LibreOffice 中打开,以便您更容易理解。它显示主题中所有单词的文件名,后跟topicproportion:

第一档为hawes.txt,话题19占比 0.438 %。

让我们使用自定义数据来尝试一下。在mallet目录下创建一个mydata文件夹,包含四个文本文件,文件名分别为1.txt2.txt3.txt4.txt。以下是该文件的内容:

| 文件名 | 内容 |
| 1.txt | 我喜欢吃香蕉。 |
| 2.txt | 我有一只狗。他也喜欢吃香蕉。 |
| 3.txt | 香蕉是一种水果,营养丰富。 |
| 4.txt | 早上吃香蕉是一个健康的习惯。 |

让我们对模型进行训练和评估。执行以下两个命令:

mallet-2.0.6$ bin/mallet import-dir --input mydata/ --output mytutorial.mallet --keep-sequence --remove-stopwords

mallet-2.0.6$ bin/mallet train-topics  --input mytutorial.mallet --num-topics 2 --output-state mytopic-state.gz --output-topic-keys mytutorial_keys.txt --output-doc-topics mytutorial_compostion.txt

如前所述,它将创建三个文件,我们现在将详细了解这三个文件。

第一档是mytopic-state.gz。提取并打开文件。这将显示使用的所有单词,以及它们设置在哪个主题中:

下一个文件是mytutorial_key.txt,当打开时,将显示主题术语。由于我们要求两个主题,它将有两行:

最后一个文件是mytutorial_composition.txt,我们会在 Excel 或者 LibreOffice 中打开。它将显示doctopicproportion:

可以看出,对于包含Banana is a fruit, rich in nutrients.3.txt文件,主题0与主题1的比例更大。从第一个文件中,我们可以看到主题0包含了主题banananutrientslovehealthy

摘要

在这一章中,我们学习了为什么我们应该进行主题建模,以及它在一个数据不断增长的世界中的重要性。我们还研究了 LDA 的概念及其在决定如何从给定的语料库中选择主题中的应用。我们还研究了 MALLET 工具在样本数据主题建模和创建我们自己的定制数据中的应用。我们还了解了生成的不同文件以及如何解释它们。

在下一章第十章、使用解析器提取关系中,我们将看到如何使用解析器提取关系。

十、使用解析器提取关系

解析是为文本单元创建解析树的过程。这个单元可能是一行代码或一个句子。对于计算机语言来说,这很容易做到,因为它们的设计就是为了让这项任务变得简单。然而,这增加了编写代码的难度。自然语言解析要困难得多,这是因为自然语言中存在歧义。这种模糊性使得语言难以学习,但却提供了极大的灵活性和表现力。在这里,我们对解析计算机语言不感兴趣,而是自然语言。

解析树是一种分层的数据结构,表示句子的句法结构。通常,这表现为一个有根的树形图,我们很快就会举例说明。我们将使用解析树来帮助识别树中实体之间的关系。

解析用于许多任务,包括:

  • 语言的机器翻译
  • 从文本合成语音
  • 语音识别
  • 语法检查
  • 信息提取

共指消解是指文本中两个或两个以上的表达式指代同一个人或事物的情况。以这句话为例:

"特德去参加聚会,在那里他出尽了洋相。"

泰德本人这几个词指的是同一个实体,泰德。这对于确定文本的正确解释和文本各部分的相对重要性是很重要的。我们将演示斯坦福 API 如何解决这个问题。

从文本中提取关系和信息是一项重要的自然语言处理任务。关系可能存在于实体之间,例如句子的主语和它的宾语、其他实体或者它的行为。我们可能还想确定关系,并以结构化的形式呈现它们。我们可以使用这些信息来呈现结果,供人们立即使用,或者格式化关系,以便它们可以更好地用于下游任务。

在这一章中,我们将研究解析过程,看看解析树是如何使用的。我们将检查关系提取过程,研究关系类型,使用提取的关系,并学习使用 NLP APIs。

我们将在本章中讨论以下主题:

  • 关系类型
  • 理解解析树
  • 使用提取的关系
  • 提取关系
  • 使用 NLP APIs
  • 为问答系统提取关系

关系类型

有许多可能的关系类型。下表列出了一些关系类别和示例。一个包含大量关系的有趣网站是 Freebase(www.freebase.com/)。它是一个按类别组织的人、地点和事物的数据库。WordNet 词库(【http://wordnet.princeton.edu/】??)包含许多关系:

| 关系 | 例子 |
| 个人的 | 的父亲,姐妹,女朋友 |
| 组织的 | 附属于,小组委员会 |
| 空间的 | 在…的东北方向,在…之下 |
| 身体的 | 的一部分,由...组成 |
| 相互作用 | 与...结合、交往、反应 |

命名实体识别 ( NER )是自然语言处理分类的一个低级类型,在第四章、寻找人和事物中有所涉及。然而,许多应用程序需要超越这一点,并识别不同类型的关系。例如,当 NER 被应用于识别个人时,那么知道我们正在与一个人打交道可以进一步精炼存在的关系。

一旦识别出这些实体,就可以创建到它们包含的文档的链接,或者用作索引。对于问答应用,命名实体通常用于回答。当文本的情感被确定时,它需要被归因于某个实体。

例如,考虑以下输入:

He was the last person to see Fred. 

使用 OpenNLP NER 作为前一句话的输入,正如我们在第四章、寻找人和事中所做的那样,我们得到以下输出:

Span: [7..9) person
Entity: Fred 

使用 OpenNLP 解析器,我们得到了更多关于这个句子的信息:

    (TOP (S (NP (PRP He)) (VP (VBD was) (NP (NP (DT the) (JJ last) (NN person)) (SBAR (S (VP (TO to) (VP (VB see))))))) (. Fred.)))  

考虑以下输入:

The cow jumped over the moon. 

对于前面的句子,解析器返回:

    (TOP (S (NP (DT The) (NN cow)) (VP (VBD jumped) (PP (IN over) (NP (DT the) (NN moon))))))

有两种类型的解析:

  • 依存关系:关注词与词之间的关系
  • 短语结构:这处理短语和它们的递归结构

依存关系可以使用主语、限定词和介词等标签来查找关系。解析技术包括移位归约、生成树和级联分块。我们在这里不关心这些差异,而是将重点放在各种解析器的使用和结果上。

理解解析树

解析树表示文本元素之间的层次关系。例如,依存关系树显示了一个句子的语法元素之间的关系。让我们重新考虑下面这句话:

The cow jumped over the moon. 

这里显示了前面句子的解析树。它是使用本章后面的使用词典化解析器类一节中的技术生成的:

    (ROOT
      (S
        (NP (DT The) (NN cow))
        (VP (VBD jumped)
          (PP (IN over)
            (NP (DT the) (NN moon))))
        (. .)))

这句话可以图形化描绘,如下图所示。它是使用在nlpviz.bpodgursky.com/发现的应用程序生成的。另一个允许你以图形化方式检查文本的编辑器是 grammar scope(【http://grammarscope.sourceforge.net/】??)。这是一个斯坦福支持的工具,它使用基于 Swing 的 GUI 来生成解析树、语法结构、类型依赖和文本语义图:

然而,可能有不止一种方法来解析一个句子。解析是困难的,因为它需要处理大量可能存在歧义的文本。下面的输出说明了前一个例句的其他可能的依赖树。该树是使用 OpenNLP 生成的,这将在本章后面的使用 OpenNLP 一节中演示:

    (TOP (S (NP (DT The) (NN cow)) (VP (VBD jumped) (PP (IN over) (NP (DT the) (NN moon))))))
    (TOP (S (NP (DT The) (NN cow)) (VP (VP (VBD jumped) (PRT (RP over))) (NP (DT the) (NN moon)))))
    (TOP (S (NP (DT The) (NNS cow)) (VP (VBD jumped) (PP (IN over) (NP (DT the) (NN moon)))))) 

每一个都代表了同一个句子的稍微不同的解析。首先显示最有可能的解析。

使用提取的关系

提取的关系可用于多种目的,包括:

  • 构建知识库
  • 创建目录
  • 产品搜索
  • 专利分析
  • 股票分析
  • 情报分析

维基百科的信息框展示了一个展示关系的例子,如下图所示。该信息框用于输入 Oklahoma,并包含关系类型,如官方语言、首都及其所在地区的详细信息:

有许多使用维基百科建立的数据库提取关系和信息,例如:

另一个简单但有趣的例子是当谷歌搜索planet mercury时出现的信息框。如下面的屏幕截图所示,我们不仅获得了查询的链接列表,还在页面的右侧看到了 Mercury 的关系和图像列表:

信息抽取也用于创建 web 索引。这些索引是为网站开发的,允许用户在网站中导航。美国人口普查局(www.census.gov/main/www/a2z)的网页索引示例如下图所示:

提取关系

有许多技术可以用来提取关系。这些可以分为以下几类:

  • 手工制作的图案
  • 监督方法
  • 半监督或无监督方法
  • 自举方法
  • 远程监控方法
  • 无监督方法

当我们没有训练数据时,就使用手工构建的模型。这可能发生在新的业务领域或者全新类型的项目中。这些通常需要使用规则。规则可能是:

如果使用了“男演员”或“女演员”一词,而没有使用“电影”或“商业”一词,则该文本应归类为戏剧

然而,这种方法需要花费很多精力,并且需要根据手头的实际文本进行调整。

如果只有很少的训练数据是可亲的,那么朴素贝叶斯分类器是一个很好的选择。当有更多数据可用时,可以使用诸如支持向量机(【SVM】)、正则化逻辑回归和随机森林等技术。

虽然更详细地理解这些技术是有用的,但是我们在这里不会涉及它们,因为我们的重点是这些技术的使用。

使用 NLP APIs

我们将使用 OpenNLP 和 Stanford APIs 来演示关系信息的解析和提取。也可以使用 LingPipe,但这里不讨论。如何使用 LingPipe 解析生物医学文献的示例可以在alias-I . com/LingPipe-3 . 9 . 3/demos/tutorial/MEDLINE/read-me . html找到。

使用 OpenNLP

使用ParserTool类解析文本很简单。它的静态parseLine方法接受三个参数并返回一个Parser实例。这些论点如下:

  • 包含要分析的文本的字符串
  • 一个实例
  • 一个整数,指定要返回多少个分析

Parser实例保存解析的元素。语法分析按概率顺序返回。为了创建一个Parser实例,我们将使用ParserFactory类的create方法。这个方法使用了一个我们将使用en-parser-chunking.bin文件创建的ParserModel实例。

这里显示了这个过程,其中使用 try-with-resources 块创建了模型文件的输入流。创建了一个ParserModel实例,然后是一个Parser实例:

String fileLocation = getModelDir() +  
    "/en-parser-chunking.bin"; 
try (InputStream modelInputStream =  
            new FileInputStream(fileLocation);) { 
     ParserModel model = new ParserModel(modelInputStream); 
    Parser parser = ParserFactory.create(model); 
    ... 
} catch (IOException ex) { 
    // Handle exceptions 
} 

我们将用一个简单的句子来演示解析过程。在下面的代码序列中,使用第三个参数的值3来调用parseLine方法。这将返回前三个解析:

String sentence = "The cow jumped over the moon"; 
Parse parses[] = ParserTool.parseLine(sentence, parser, 3); 

接下来,将显示这些分析及其概率,如下所示:

for(Parse parse : parses) { 
    parse.show(); 
    System.out.println("Probability: " + parse.getProb()); 
} 

输出如下所示:

    (TOP (S (NP (DT The) (NN cow)) (VP (VBD jumped) (PP (IN over) (NP (DT the) (NN moon))))))
    Probability: -1.043506016751117
    (TOP (S (NP (DT The) (NN cow)) (VP (VP (VBD jumped) (PRT (RP over))) (NP (DT the) (NN moon)))))
    Probability: -4.248553665013661
    (TOP (S (NP (DT The) (NNS cow)) (VP (VBD jumped) (PP (IN over) (NP (DT the) (NN moon))))))
    Probability: -4.761071294573854

注意,每次解析产生的标签顺序和分配略有不同。下面的输出显示了第一个解析的格式,以便于阅读:

    (TOP 
          (S 
              (NP 
                   (DT The) 
                   (NN cow)
              )
              (VP 
                   (VBD jumped) 
                   (PP 
                        (IN over)
                        (NP 
                             (DT the)
                             (NN moon)
                         )
                   )
               )
         )
    )

可以使用showCodeTree方法来显示父子关系:

parse.showCodeTree(); 

第一次解析的输出如下所示。每行的第一部分显示了用括号括起来的元素级别。接下来显示标签,后面是由->分隔的两个哈希值。第一个数字代表元素,第二个数字代表其父元素。例如,在第三行中,它显示专有名词The,拥有名词短语The cow的父级:

[0] S -929208263 -> -929208263 TOP The cow jumped over the moon
[0.0] NP -929237012 -> -929208263 S The cow
[0.0.0] DT -929242488 -> -929237012 NP The
[0.0.0.0] TK -929242488 -> -929242488 DT The
[0.0.1] NN -929034400 -> -929237012 NP cow
[0.0.1.0] TK -929034400 -> -929034400 NN cow
[0.1] VP -928803039 -> -929208263 S jumped over the moon
[0.1.0] VBD -928822205 -> -928803039 VP jumped
[0.1.0.0] TK -928822205 -> -928822205 VBD jumped
[0.1.1] PP -928448468 -> -928803039 VP over the moon
[0.1.1.0] IN -928460789 -> -928448468 PP over
[0.1.1.0.0] TK -928460789 -> -928460789 IN over
[0.1.1.1] NP -928195203 -> -928448468 PP the moon
[0.1.1.1.0] DT -928202048 -> -928195203 NP the
[0.1.1.1.0.0] TK -928202048 -> -928202048 DT the
[0.1.1.1.1] NN -927992591 -> -928195203 NP moon
[0.1.1.1.1.0] TK -927992591 -> -927992591 NN moon  

访问解析元素的另一种方式是通过getChildren方法。这个方法返回一个Parse对象的数组,每个对象代表解析的一个元素。使用各种Parse方法,我们可以获得每个元素的文本、标记和标签。这里举例说明了这一点:

Parse children[] = parse.getChildren(); 
for (Parse parseElement : children) { 
    System.out.println(parseElement.getText()); 
    System.out.println(parseElement.getType()); 
    Parse tags[] = parseElement.getTagNodes(); 
    System.out.println("Tags"); 
    for (Parse tag : tags) { 
        System.out.println("[" + tag + "]"  
            + " type: " + tag.getType()  
            + "  Probability: " + tag.getProb()  
            + "  Label: " + tag.getLabel()); 
    } 
} 

该序列的输出如下:

The cow jumped over the moon
S
Tags
[The] type: DT  Probability: 0.9380626549164167  Label: null
[cow] type: NN  Probability: 0.9574993337971017  Label: null
[jumped] type: VBD  Probability: 0.9652983971550483  Label: S-VP
[over] type: IN  Probability: 0.7990638213315913  Label: S-PP
[the] type: DT  Probability: 0.9848023215770413  Label: null
[moon] type: NN  Probability: 0.9942338356992393  Label: null  

使用斯坦福 API

Stanford NLP API 中有几种解析方法。首先,我们将演示一个通用解析器,即LexicalizedParser类。然后,我们将说明如何使用TreePrint类显示解析器的结果。接下来将演示如何使用GrammaticalStructure类来确定单词依赖关系。

使用 LexicalizedParser 类

LexicalizedParser类是一个词汇化的 PCFG 解析器。它可以使用各种模型来执行解析过程。使用apply方法和CoreLabel对象的List实例来创建解析树。

在下面的代码序列中,使用englishPCFG.ser.gz模型实例化解析器:

String parserModel = ".../models/lexparser/englishPCFG.ser.gz"; 
LexicalizedParser lexicalizedParser =  
   LexicalizedParser.loadModel(parserModel);

使用Sentence类的toCoreLabelList方法创建CoreLabel对象的list实例。CoreLabel对象包含一个单词和其他信息。这些单词没有标记或标签。数组中的单词已被有效地标记化:

String[] senetenceArray = {"The", "cow", "jumped", "over",  
    "the", "moon", "."}; 
List<CoreLabel> words =  
    Sentence.toCoreLabelList(senetenceArray); 

现在可以调用apply方法了:

Tree parseTree = lexicalizedParser.apply(words); 

显示解析结果的一个简单方法是使用pennPrint方法,该方法以与 Penn TreeBank 相同的方式显示parseTree(www . SFS . uni-tuebingen . de/~ DM/07/autumn/795.10/pt b-annotation-guide/root . html):

parseTree.pennPrint(); 

输出如下所示:

    (ROOT
      (S
        (NP (DT The) (NN cow))
        (VP (VBD jumped)
          (PP (IN over)
            (NP (DT the) (NN moon))))
        (. .)))

Tree类提供了许多使用解析树的方法。

使用 TreePrint 类

TreePrint类提供了一种显示树的简单方法。使用描述要使用的显示格式的字符串创建类的实例。使用静态outputTreeFormats变量可以获得一组有效的输出格式,如下表所示:

| | 树形格式字符串 | |
| penn | dependencies | collocations |
| oneline | typedDependencies | semanticGraph |
| rootSymbolOnly | typedDependenciesCollapsed | conllStyleDependencies |
| words | latexTree | conll2007 |
| wordsAndTags | xmlTree | |

斯坦福使用类型依赖来描述句子中存在的语法关系。这些在斯坦福类型依赖手册(【http://nlp.stanford.edu/software/dependencies_manual.pdf】??)中有详细说明。

下面的代码示例说明了如何使用TreePrint类。printTree方法执行实际的显示操作。

在这种情况下,TreePrint对象被创建,显示"typedDependenciesCollapsed":

TreePrint treePrint =  
    new TreePrint("typedDependenciesCollapsed"); 
treePrint.printTree(parseTree); 

这个序列的输出如下,其中数字反映了它在句子中的位置:

det(cow-2, The-1)
nsubj(jumped-3, cow-2)
root(ROOT-0, jumped-3)
det(moon-6, the-5)
prep_over(jumped-3, moon-6)  

使用penn字符串创建对象会产生以下输出:

    (ROOT (S (NP (DT The) (NN cow)) (VP (VBD jumped) (PP (IN over) (NP (DT the) (NN moon)))) (. .)))

dependencies字符串产生一个简单的依赖列表:

    dep(cow-2,The-1)
    dep(jumped-3,cow-2)
    dep(null-0,jumped-3,root)
    dep(jumped-3,over-4)
    dep(moon-6,the-5)
    dep(over-4,moon-6)

这些格式可以用逗号组合。以下示例将导致显示同时使用penn样式和typedDependenciesCollapsed格式:

    "penn,typedDependenciesCollapsed"  

使用 GrammaticalStructure 类查找单词依赖关系

另一种解析文本的方法是结合使用我们在上一节中创建的LexicalizedParser对象和TreebankLanguagePack接口。树库是已经用句法或语义信息注释的文本语料库,提供关于句子结构的信息。第一个主要的树银行是宾夕法尼亚树银行(【http://www.cis.upenn.edu/~treebank/】)))。可以手动或半自动创建树库。

下面的例子说明了如何使用解析器格式化一个简单的字符串。一个TokenizerFactory创建一个记号赋予器。

我们在中使用词汇化解析器类一节中讨论的CoreLabel类在这里使用:

String sentence = "The cow jumped over the moon."; 
TokenizerFactory<CoreLabel> tokenizerFactory =  
    PTBTokenizer.factory(new CoreLabelTokenFactory(), ""); 
Tokenizer<CoreLabel> tokenizer =  
    tokenizerFactory.getTokenizer(new StringReader(sentence)); 
List<CoreLabel> wordList = tokenizer.tokenize(); 
parseTree = lexicalizedParser.apply(wordList); 

TreebankLanguagePack接口指定了使用树库的方法。在下面的代码中,创建了一系列对象,最终创建了一个TypedDependency实例,用于获取句子元素的依赖信息。一个GrammaticalStructureFactory对象的实例被创建并用于创建一个GrammaticalStructure类的实例。

正如这个类的名字所暗示的,它存储了树中元素之间的语法信息:

TreebankLanguagePack tlp =  
    lexicalizedParser.treebankLanguagePack; 
GrammaticalStructureFactory gsf =  
    tlp.grammaticalStructureFactory(); 
GrammaticalStructure gs =  
    gsf.newGrammaticalStructure(parseTree); 
List<TypedDependency> tdl = gs.typedDependenciesCCprocessed(); 

我们可以简单地显示列表,如下所示:

System.out.println(tdl);

输出如下所示:

    [det(cow-2, The-1), nsubj(jumped-3, cow-2), root(ROOT-0, jumped-3), det(moon-6, the-5), prep_over(jumped-3, moon-6)]  

也可以使用govrelndep方法
提取这些信息,这些方法分别返回调控字、关系和依赖元素,如下所示:

for(TypedDependency dependency : tdl) { 
    System.out.println("Governor Word: [" + dependency.gov()  
        + "] Relation: [" + dependency.reln().getLongName() 
        + "] Dependent Word: [" + dependency.dep() + "]"); 
} 

输出如下所示:

    Governor Word: [cow/NN] Relation: [determiner] Dependent Word: [The/DT]
    Governor Word: [jumped/VBD] Relation: [nominal subject] Dependent Word: [cow/NN]
    Governor Word: [ROOT] Relation: [root] Dependent Word: [jumped/VBD]
    Governor Word: [moon/NN] Relation: [determiner] Dependent Word: [the/DT]
    Governor Word: [jumped/VBD] Relation: [prep_collapsed] Dependent Word: [moon/NN]  

由此,我们可以看出一个句子中的关系以及这种关系的要素。

查找共指消解实体

共指消解指的是在文本中出现两个或多个指代同一个人或实体的表达式。考虑下面的句子:

"他拿了他的现金,她拿了她的零钱,他们一起买了午餐。"

这个句子中有几个指代。字他的指的是指的是。另外,他们既指又指

一个内指是一个在它之前或之后的表达式的共指。内照应可以分为回指和照应。在下面的句子中,单词 It 是指代其先行词地震的回指词:

”玛丽感觉到了地震。它震动了整栋大楼。”

在下一个句子中, she 是一个后转代词,因为它指向后置的玛丽:

"当玛丽坐在那里时,她感觉到了地震。"

斯坦福 API 支持使用dcoref注释的StanfordCoreNLP类的共指解析。我们将用前面的句子演示这个类的用法。

我们将从创建管道和使用annotate方法开始,如下所示:

String sentence = "He took his cash and she took her change "  
    + "and together they bought their lunch."; 
Properties props = new Properties(); 
props.put("annotators",  
    "tokenize, ssplit, pos, lemma, ner, parse, dcoref"); 
StanfordCoreNLP pipeline = new StanfordCoreNLP(props); 
Annotation annotation = new Annotation(sentence); 
pipeline.annotate(annotation); 

Annotation class' get方法,当与CorefChainAnnotation.class参数一起使用时,将返回CorefChain对象的Map实例,如下所示。这些对象包含关于在句子中找到的共指的信息:

Map<Integer, CorefChain> corefChainMap =  
    annotation.get(CorefChainAnnotation.class); 

使用整数对一组CorefChain对象进行索引。我们可以迭代这些对象,如下面的代码所示。获取密钥集,然后显示每个CorefChain对象:

Set<Integer> set = corefChainMap.keySet(); 
Iterator<Integer> setIterator = set.iterator(); 
while(setIterator.hasNext()) { 
    CorefChain corefChain =  
        corefChainMap.get(setIterator.next()); 
    System.out.println("CorefChain: " + corefChain); 
} 

将生成以下输出:

CorefChain: CHAIN1-["He" in sentence 1, "his" in sentence 1]
CorefChain: CHAIN2-["his cash" in sentence 1]
CorefChain: CHAIN4-["she" in sentence 1, "her" in sentence 1]
CorefChain: CHAIN5-["her change" in sentence 1]
CorefChain: CHAIN7-["they" in sentence 1, "their" in sentence 1]
CorefChain: CHAIN8-["their lunch" in sentence 1]

我们使用CorefChainCorefMention类的方法获得更详细的信息。后一类包含关于在句子中找到的特定共指的信息。

将以下代码序列添加到前面的while循环体中,以获取并显示该信息。该类的startIndexendIndex字段是指单词在句子中的位置:

System.out.print("ClusterId: " + corefChain.getChainID()); 
CorefMention mention = corefChain.getRepresentativeMention(); 
System.out.println(" CorefMention: " + mention  
    + " Span: [" + mention.mentionSpan + "]"); 

List<CorefMention> mentionList =  
    corefChain.getMentionsInTextualOrder(); 
Iterator<CorefMention> mentionIterator =  
    mentionList.iterator(); 
while(mentionIterator.hasNext()) { 
    CorefMention cfm = mentionIterator.next(); 
    System.out.println("\tMention: " + cfm  
        + " Span: [" + mention.mentionSpan + "]"); 
    System.out.print("\tMention Mention Type: "  
        + cfm.mentionType + " Gender: " + cfm.gender); 
    System.out.println(" Start: " + cfm.startIndex  
        + " End: " + cfm.endIndex); 
} 
System.out.println(); 

输出如下。为了节省空间,只显示第一次和最后一次提及:

    CorefChain: CHAIN1-["He" in sentence 1, "his" in sentence 1]
    ClusterId: 1 CorefMention: "He" in sentence 1 Span: [He]
      Mention: "He" in sentence 1 Span: [He]
      Mention Type: PRONOMINAL Gender: MALE Start: 1 End: 2
      Mention: "his" in sentence 1 Span: [He]
      Mention Type: PRONOMINAL Gender: MALE Start: 3 End: 4
    ...
    CorefChain: CHAIN8-["their lunch" in sentence 1]
    ClusterId: 8 CorefMention: "their lunch" in sentence 1 Span: [their lunch]
      Mention: "their lunch" in sentence 1 Span: [their lunch]
      Mention Type: NOMINAL Gender: UNKNOWN Start: 14 End: 16

为问答系统提取关系

在这一节中,我们将研究一种提取关系的方法,这种方法对于回答查询很有用。可能/候选查询包括以下内容:

  • 谁是/曾经是美国第 14 任总统?
  • 第一任总统的家乡是哪里?
  • 赫伯特·胡佛是什么时候的总统?

回答这类问题的过程并不容易。我们将演示一种方法来回答某些类型的问题,但是我们将简化这个过程的许多方面。即使有这些限制,我们也会发现系统对查询的响应很好。

这个过程包括几个步骤:

  1. 查找单词依赖关系
  2. 确定问题的类型
  3. 提取其相关成分
  4. 寻找答案
  5. 给出答案

我们将展示识别一个问题是否属于何人、何事、何时或何地类型的一般框架。接下来,我们将调查回答类型问题所需的一些问题。

为了使这个例子简单,我们将把问题限制在与美国总统有关的问题上。将使用一个简单的总统事实数据库来查找问题的答案。

查找单词依赖关系

问题存储为简单的字符串:

String question =  
    "Who is the 32nd president of the United States?";

我们将使用LexicalizedParser类,正如在使用语法结构类查找单词依赖部分中开发的。为方便起见,此处复制了相关代码:

String parserModel = ".../englishPCFG.ser.gz"; 
LexicalizedParser lexicalizedParser =  
    LexicalizedParser.loadModel(parserModel); 

TokenizerFactory<CoreLabel> tokenizerFactory =  
    PTBTokenizer.factory(new CoreLabelTokenFactory(), ""); 
Tokenizer<CoreLabel> tokenizer =  
    tokenizerFactory.getTokenizer(new StringReader(question)); 
List<CoreLabel> wordList = tokenizer.tokenize(); 
Tree parseTree = lexicalizedParser.apply(wordList); 

TreebankLanguagePack tlp =  
    lexicalizedParser.treebankLanguagePack(); 
GrammaticalStructureFactory gsf =  
    tlp.grammaticalStructureFactory(); 
GrammaticalStructure gs =  
    gsf.newGrammaticalStructure(parseTree); 
List<TypedDependency> tdl = gs.typedDependenciesCCprocessed(); 
System.out.println(tdl); 
for (TypedDependency dependency : tdl) { 
    System.out.println("Governor Word: [" + dependency.gov()  
        + "] Relation: [" + dependency.reln().getLongName() 
        + "] Dependent Word: [" + dependency.dep() + "]"); 
} 

当执行该问题时,我们得到以下输出:

    [root(ROOT-0, Who-1), cop(Who-1, is-2), det(president-5, the-3), amod(president-5, 32nd-4), nsubj(Who-1, president-5), det(States-9, the-7), nn(States-9, United-8), prep_of(president-5, States-9)]
    Governor Word: [ROOT] Relation: [root] Dependent Word: [Who/WP]
    Governor Word: [Who/WP] Relation: [copula] Dependent Word: [is/VBZ]
    Governor Word: [president/NN] Relation: [determiner] Dependent Word: [the/DT]
    Governor Word: [president/NN] Relation: [adjectival modifier] Dependent Word: [32nd/JJ]
    Governor Word: [Who/WP] Relation: [nominal subject] Dependent Word: [president/NN]
    Governor Word: [States/NNPS] Relation: [determiner] Dependent Word: [the/DT]
    Governor Word: [States/NNPS] Relation: [nn modifier] Dependent Word: [United/NNP]
    Governor Word: [president/NN] Relation: [prep_collapsed] Dependent Word: [States/NNPS]

这些信息为确定问题的类型提供了基础。

确定问题类型

检测到的关系提出了检测不同类型问题的方法。例如,要确定它是否是一个 who 类型的问题,我们可以检查关系是否是一个nominal subject和总督是否是who

在下面的代码中,我们迭代问题类型依赖项,以确定它是否匹配该组合,如果匹配,则调用processWhoQuestion方法来处理问题:

for (TypedDependency dependency : tdl) { 
    if ("nominal subject".equals( dependency.reln().getLongName()) 
        && "who".equalsIgnoreCase( dependency.gov().originalText())) { 
        processWhoQuestion(tdl); 
    } 
} 

这种简单的区分相当有效。它将正确识别同一问题的所有下列变体:

    Who is the 32nd president of the United States?
    Who was the 32nd president of the United States?
    The 32nd president of the United States was who?
    The 32nd president is who of the United States?

我们还可以使用不同的选择标准来确定其他问题类型。以下问题代表了其他问题类型:

    What was the 3rd President's party?
    When was the 12th president inaugurated?
    Where is the 30th president's home town?

我们可以使用下表中建议的关系来确定问题类型:

| 题型 | 关系 | 总督 | 依赖 |
| 什么 | 名词性主语 | 什么 | 钠 |
| 当...的时候 | 状语 | 钠 | 当...的时候 |
| 在哪里 | 状语 | 钠 | 在哪里 |

这种方法确实需要硬编码relationships.createPresidentList

寻找答案

一旦我们知道了问题的类型,我们就可以利用课文中的关系来回答问题。为了说明这个过程,我们将开发processWhoQuestion方法。这种方法使用TypedDependency列表来收集回答类型的关于总统的问题所需的信息。具体来说,我们需要知道他们对哪个总统感兴趣,基于总统的序数排名。

我们还需要一份主席名单来搜索相关信息。开发了createPresidentList方法来执行这项任务。它读取一个文件,PresidentList,包含总统的名字,就职年份,以及任职的最后一年。该文件使用以下格式,可以从github . com/packt publishing/Natural-Language-Processing-with-Java-Second-Edition下载:

    George Washington   (1789-1797) 

下面的createPresidentList方法演示了如何使用 OpenNLP 的SimpleTokenizer类来标记每一行。总统的名字由不同数量的符号组成。一旦确定了这一点,就很容易提取日期:

public List<President> createPresidentList() { 
    ArrayList<President> list = new ArrayList<>(); 
    String line = null; 
    try (FileReader reader = new FileReader("PresidentList"); 
            BufferedReader br = new BufferedReader(reader)) { 
        while ((line = br.readLine()) != null) { 
            SimpleTokenizer simpleTokenizer =  
                SimpleTokenizer.INSTANCE; 
            String tokens[] = simpleTokenizer.tokenize(line); 
            String name = ""; 
            String start = ""; 
            String end = ""; 
            int i = 0; 
            while (!"(".equals(tokens[i])) { 
                name += tokens[i] + " "; 
                i++; 
            } 
            start = tokens[i + 1]; 
            end = tokens[i + 3]; 
            if (end.equalsIgnoreCase("present")) { 
                end = start; 
            } 
            list.add(new President(name,  
                Integer.parseInt(start), 
                Integer.parseInt(end))); 
        } 
     } catch (IOException ex) { 
        // Handle exceptions 
    } 
    return list; 
} 

President类保存总统信息,如下所示。getter 方法已经被省略了:

public class President { 
    private String name; 
    private int start; 
    private int end; 

    public President(String name, int start, int end) { 
        this.name = name; 
        this.start = start; 
        this.end = end; 
    } 
    ... 
} 

下面是processWhoQuestion方法。我们再次使用类型依赖来提取问题的序数值。如果管理者是presidentadjectival modifier是关系,那么从属词就是序数。
这个字符串被传递给getOrder方法,该方法返回一个整数形式的序数。我们在上面加 1,因为总统的名单也是从 1 开始的:

public void processWhoQuestion(List<TypedDependency> tdl) { 
    List<President> list = createPresidentList(); 
    for (TypedDependency dependency : tdl) { 
        if ("president".equalsIgnoreCase( 
                dependency.gov().originalText()) 
                && "adjectival modifier".equals( 
                  dependency.reln().getLongName())) { 
            String positionText =  
                dependency.dep().originalText(); 
            int position = getOrder(positionText)-1; 
            System.out.println("The president is "  
                + list.get(position).getName()); 
        } 
    } 
}

getOrder方法如下,简单地获取第一个数字字符并将它们转换成整数。一个更复杂的版本会考虑其他变体,包括“第一”和“第十六”这样的词:

private static int getOrder(String position) { 
    String tmp = ""; 
    int i = 0; 
    while (Character.isDigit(position.charAt(i))) { 
        tmp += position.charAt(i++); 
    } 
    return Integer.parseInt(tmp); 
} 

执行时,我们得到以下输出:

The president is Franklin D . Roosevelt

这个实现是一个简单的例子,说明如何从句子中提取信息并用来回答问题。其他类型的问题可以以类似的方式实现,留给读者作为练习。

摘要

我们已经讨论了解析过程以及如何使用它从文本中提取关系。它可以用于许多目的,包括语法检查和文本的机器翻译。有许多可能的文本关系。这些关系包括“父亲”、“亲近”和“下级”。他们关心的是文本元素之间的关系。

解析文本将返回文本中存在的关系。这些关系可用于提取感兴趣的信息。我们演示了许多使用 OpenNLP 和 Stanford APIs 解析文本的技术。

我们还解释了如何使用 Stanford API 来查找文本中的共指解析。当两个或两个以上的表达,如他们指同一个人时,就会出现这种情况。

最后,我们用一个例子来说明如何使用解析器从句子中提取关系。这些关系被用来提取信息,以回答关于美国总统的简单查询。

在下一章中,第十一章,联合管道,我们将探讨在这一章和前几章中开发的技术如何用于解决更复杂的问题。

十一、组合管道

在这一章中,我们将讨论几个关于使用技术组合来解决 NLP 问题的问题。我们将从简单介绍准备数据的过程开始。接下来是关于管道及其构造的讨论。管道只不过是为解决某些问题而集成的一系列任务。管道的主要优点是能够插入和移除管道的各种元素,以稍微不同的方式解决问题。

斯坦福 API 支持一个很好的管道架构,这一点我们在本书中已经反复使用过。我们将详述这种方法的细节,然后展示如何使用 OpenNLP 来构建管道。为处理准备数据是解决许多 NLP 问题的重要的第一步。我们在第一章、自然语言处理简介中介绍了数据准备过程,然后在第二章、查找部分文本中讨论了归一化过程。在这一章中,我们将着重于从不同的数据源中提取文本,例如 HTML、Word 和 PDF 文档。Stanford StanfordCoreNLP类是易于使用的管道的一个很好的例子。从某种意义上说,它是预构的。实际执行的任务取决于添加的注释。这适用于许多类型的问题。但是,其他 NLP APIs 不像斯坦福 API 那样直接支持管道架构;虽然构建起来更加困难,但是这些方法对于许多应用来说更加灵活。我们将使用 OpenNLP 演示这个构建过程。

我们将在本章中讨论以下主题:

  • 准备数据
  • 使用样板文件从 HTML 中提取文本
  • 使用兴趣点从 Word 文档中提取文本
  • 使用 PDFBox 从 PDF 文档中提取文本
  • 使用 Apache Tika 进行内容分析和提取
  • 管道
  • 利用斯坦福管道
  • 在斯坦福管道中使用多核
  • 创建搜索文本的管道

准备数据

文本提取是你想要进行的任何 NLP 任务的主要阶段。如果给定一篇博客文章,我们希望提取博客的内容,并希望找到文章的标题、文章的作者、文章发布的日期、文章的文本或内容、类似媒体的图像、文章中的视频以及其他文章的链接(如果有的话)。文本提取包括以下内容:

  • 结构化,以便识别不同的字段、内容块等
  • 确定文档的语言
  • 寻找句子、段落、短语和引语
  • 将文本分解成标记,以便进一步处理
  • 标准化和标记
  • 词汇化和词干化,以减少变化并接近词根

这也有助于主题建模,我们已经在第九章、主题建模中讨论过。在这里,我们将快速介绍如何对 HTML、Word 和 PDF 文档执行文本提取。虽然有几个 API 支持这些任务,但我们将使用以下 API:

一些 API 支持使用 XML 进行输入和输出。例如,Stanford XMLUtils类提供了对读取 XML 文件和操作 XML 数据的支持。LingPipe 的XMLParser类将解析 XML 文本。组织以多种形式存储数据,通常不是简单的文本文件。演示文稿存储在 PowerPoint 幻灯片中,规范使用 Word 文档创建,公司提供 PDF 文档形式的营销和其他材料。大多数组织都有互联网,这意味着许多有用的信息都可以在 HTML 文档中找到。由于这些数据源的广泛性,我们需要使用工具来提取它们的文本进行处理。

使用样板文件从 HTML 中提取文本

有几个库可用于从 HTML 文档中提取文本。我们将演示如何使用样板管(code.google.com/p/boilerpipe/)来执行这个操作。这是一个灵活的 API,不仅可以提取 HTML 文档的整个文本,还可以提取 HTML 文档的选定部分,如标题和单个文本块。我们将使用 http://en.wikipedia.org/wiki/Berlin的 HTML 页面来说明样板文件的使用。该页面的一部分如下面的截图所示:

为了使用 boilerpipe,您需要下载 Xerces 解析器的二进制文件,可以在xerces.apache.org/index.html找到。

我们首先创建一个表示这个页面的 URL 对象。我们将使用两个类来提取文本。第一个是代表 HTML 文档的HTMLDocument类。第二个是代表 HTML 文档中文本的TextDocument类。它由一个或多个TextBlock对象组成,如果需要,可以单独访问这些对象。我们将为柏林页面创建一个HTMLDocument实例。BoilerpipeSAXInput类使用这个输入源创建一个TextDocument实例。然后,它使用TextDocument class' getText方法来检索文本。此方法使用两个参数。第一个参数指定是否包含标记为内容的TextBlock实例。第二个参数指定是否应该包含非内容的TextBlock实例。在这个例子中,两种类型的TextBlock实例都包括在内。以下是工作代码:

try{
            URL url = new URL("https://en.wikipedia.org/wiki/Berlin");
            HTMLDocument htmldoc = HTMLFetcher.fetch(url);
            InputSource is = htmldoc.toInputSource();
            TextDocument document = new BoilerpipeSAXInput(is).getTextDocument();
            System.out.println(document.getText(true, true));
        } catch (MalformedURLException ex) {
            System.out.println(ex);
        } catch (IOException ex) {
            System.out.println(ex);
        } catch (SAXException | BoilerpipeProcessingException ex) {
            System.out.println(ex);
        }

输出很长,但这里显示了几行:

Berlin
From Wikipedia, the free encyclopedia
Jump to navigation Jump to search
This article is about the capital of Germany. For other uses, see Berlin (disambiguation) .
State of Germany in Germany
Berlin
State of Germany
From top: Skyline including the TV Tower ,
City West skyline with Kaiser Wilhelm Memorial Church , Brandenburg Gate ,
East Side Gallery ( Berlin Wall ),
Oberbaum Bridge over the Spree ,
Reichstag building ( Bundestag )
.......
This page was last edited on 18 June 2018, at 11:18 (UTC).
Text is available under the Creative Commons Attribution-ShareAlike License ; additional terms may apply.  By using this site, you agree to the Terms of Use and Privacy Policy . Wikipedia® is a registered trademark of the Wikimedia Foundation, Inc. , a non-profit organization.
Privacy policy
About Wikipedia
Disclaimers
Contact Wikipedia
Developers
Cookie statement
Mobile view

使用兴趣点从 Word 文档中提取文本

Apache POI 项目(poi.apache.org/index.html)是一个用于从微软 Office 产品中提取信息的 API。这是一个庞大的库,允许从 Word 文档和其他办公产品(如 Excel 和 Outlook)中提取信息。下载 POI 的 API 时,还需要使用 XMLBeans(【http://xmlbeans.apache.org/】)支持 POI。XMLBeans 的二进制文件可以从 http://www.java2s.com/Code/Jar/x/Downloadxmlbeans524jar.htm下载。我们的兴趣是演示如何使用 POI 从 word 文档中提取文本。

为了演示这一点,我们将使用一个名为TestDocument.docx的文件,其中包含一些文本、表格和其他内容,如下面的截图所示(我们已经获取了维基百科的英文主页):

不同版本的 Word 使用几种不同的文件格式。为了简化选择使用哪个文本提取类,我们将使用ExtractorFactory工厂类。尽管 POI 的功能相当强大,但提取文本的过程却很简单。如此处所示,代表文件TestDocument.docxFileInputStream对象被ExtractorFactory类的createExtractor方法用来选择适当的POITextExtractor实例。这是几个不同提取器的基类。将getText方法应用于提取器以获取文本:

private static String getResourcePath(){
        File currDir = new File(".");
        String path = currDir .getAbsolutePath();
        path = path.substring(0, path.length()-2);
        String resourcePath = path + File.separator  + "src/chapter11/TestDocument.docx";
        return resourcePath;
    }
    public static void main(String args[]){
        try {
            FileInputStream fis = new FileInputStream(getResourcePath());
            POITextExtractor textExtractor = ExtractorFactory.createExtractor(fis);
            System.out.println(textExtractor.getText());
        } catch (FileNotFoundException ex) {
            Logger.getLogger(WordDocExtractor.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IOException ex) {
            System.out.println(ex);
        } catch (OpenXML4JException ex) {
            System.out.println(ex);
        } catch (XmlException ex) {
            System.out.println(ex);
        }   
    }

输出如下所示:

Jump to navigation Jump to search
Welcome to Wikipedia,
the free encyclopedia that anyone can edit.
5,673,388 articles in English
Arts
Biography
Geography
History
Mathematics
Science
Society
Technology
All portals
From today's featured article George Steiner The Portage to San Cristobal of A.H. is a 1981 literary and philosophical novella by George Steiner (pictured). The story is about Jewish Nazi hunters who find a fictional Adolf Hitler (A.H.) alive in the Amazon jungle thirty years after the end of World War II. The book was controversial, particularly among reviewers and Jewish scholars, because the author allows Hitler to defend himself when he is put on trial in the jungle by his captors. There Hitler maintains that Israel owes its existence to the Holocaust and that he is the "benefactor of the Jews". A central theme of The Portage is the nature of language, and revolves around Steiner's lifelong work on the subject and his fascination in the power and terror of human speech. Other themes include the philosophical and moral analysis of history, justice, guilt and revenge. Despite the controversy, it was a 1983 finalist in the PEN/Faulkner Award for Fiction. It was adapted for the theatre by British playwright Christopher Hampton. (Full article...) Recently featured: Monroe Edwards C. R. M. F. Cruttwell Russulaceae Archive By email More featured articles Did you know... Maria Bengtsson ... that a reviewer found Maria Bengtsson (pictured) believable and expressive when she first performed the title role of Arabella by Strauss? ... that the 2018 Osaka earthquake disrupted train services during the morning rush hour, forcing passengers to walk between the tracks? ... that funding for Celia Brackenridge's research into child protection in football was ended because the sport "was not ready for a gay former lacrosse international rummaging through its dirty linen"? ... that the multi-armed Heliaster helianthus sheds several of its arms when attacked by the six-armed predatory starfish Meyenaster gelatinosus? ... that if elected, Democratic candidate Deb Haaland would be the first Native American woman to become a member of the United States House of Representatives? ... that 145 Vietnamese civilians were killed during the 1967 Thuy Bo massacre? ... that Velvl Greene, a University of Minnesota professor of public health, taught more than 30,000 students? ... that a group of Fijians placed a newspaper ad to recruit skiers for Fiji at the 2002 Olympic Games after discussing it at a New Year's Eve party? Archive Start a new article Nominate an article In the news Lake Toba Saudi Arabia lifts its ban on women driving. Canada legalizes the cultivation of cannabis for recreational use with effect from October 2018, making it the second country to do so. An overloaded tourist ferry capsizes in Lake Toba (pictured), Indonesia, killing at least 3 people and leaving 193 others missing. In golf, Brooks Koepka wins the U.S. Open at the Shinnecock Hills Golf Club. Ongoing: FIFA World Cup Recent deaths: Joe Jackson Richard Harrison Yan Jizhou John Mack Nominate an article On this day June 28: Vidovdan in Serbia Anna Pavlova as Giselle 1776 – American Revolutionary War: South Carolina militia repelled a British attack on Charleston. 1841 – Giselle (Anna Pavlova pictured in the title role), a ballet by French composer Adolphe Adam, was first performed at the Théâtre de l'Académie Royale de Musique in Paris. 1911 – The first meteorite to suggest signs of aqueous processes on Mars fell to Earth in Abu Hummus, Egypt. 1978 – In Regents of the Univ. of Cal. v. Bakke, the U.S. Supreme Court barred quota systems in college admissions but declared that affirmative action programs giving advantage to minorities are constitutional. 2016 – Gunmen attacked Istanbul's Atatürk Airport, killing 45 people and injuring more than 230 others. Primož Trubar (d. 1586) · Paul Broca (b. 1824) · Yvonne Sylvain (b. 1907) More anniversaries: June 27 June 28 June 29 Archive By email List of historical anniversaries

Today's featured picture
    Henry VIII of England (1491–1547) was King of England from 1509 until his death. Henry was the second Tudor monarch, succeeding his father, Henry VII. Perhaps best known for his six marriages, his disagreement with the Pope on the question of annulment led Henry to initiate the English Reformation, separating the Church of England from papal authority and making the English monarch the Supreme Head of the Church of England. He also instituted radical changes to the English Constitution, expanded royal power, dissolved monasteries, and united England and Wales. In this, he spent lavishly and frequently quelled unrest using charges of treason and heresy. Painting: Workshop of Hans Holbein the Younger Recently featured: Lion of Al-lāt Sagittarius Japanese destroyer Yamakaze (1936) Archive More featured pictures

Other areas of Wikipedia
Community portal – Bulletin board, projects, resources and activities covering a wide range of Wikipedia areas.
Help desk – Ask questions about using Wikipedia.

此外,还可以使用metaExtractor提取关于文档的元数据,如下面的代码所示:

POITextExtractor metaExtractor = textExtractor.getMetadataTextExtractor();
            System.out.println(metaExtractor.getText());

它将生成以下输出:

Created = Thu Jun 28 06:36:00 UTC 2018
CreatedString = 2018-06-28T06:36:00Z
Creator = Ashish
LastModifiedBy = Ashish
LastPrintedString = 
Modified = Thu Jun 28 06:37:00 UTC 2018
ModifiedString = 2018-06-28T06:37:00Z
Revision = 1
Application = Microsoft Office Word
AppVersion = 12.0000
Characters = 26588
CharactersWithSpaces = 31190
Company = 
HyperlinksChanged = false
Lines = 221
LinksUpToDate = false
Pages = 8
Paragraphs = 62
Template = Normal.dotm
TotalTime = 1

另一种方法是使用XWPFDocument创建一个POIXMLPropertiesTextExtractor类的实例,它可用于CorePropertiesExtendedProperties,如下面的代码所示:

fis = new FileInputStream(getResourcePath());
            POIXMLPropertiesTextExtractor properties = new POIXMLPropertiesTextExtractor(new XWPFDocument(fis));
            CoreProperties coreProperties = properties.getCoreProperties();
            System.out.println(properties.getCorePropertiesText());

            ExtendedProperties extendedProperties = properties.getExtendedProperties();
            System.out.println(properties.getExtendedPropertiesText());

输出如下所示:

Created = Thu Jun 28 06:36:00 UTC 2018
CreatedString = 2018-06-28T06:36:00Z
Creator = Ashish
LastModifiedBy = Ashish
LastPrintedString = 
Modified = Thu Jun 28 06:37:00 UTC 2018
ModifiedString = 2018-06-28T06:37:00Z
Revision = 1

Application = Microsoft Office Word
AppVersion = 12.0000
Characters = 26588
CharactersWithSpaces = 31190
Company = 
HyperlinksChanged = false
Lines = 221
LinksUpToDate = false
Pages = 8
Paragraphs = 62
Template = Normal.dotm
TotalTime = 1

使用 PDFBox 从 PDF 文档中提取文本

Apache PDF box(pdfbox.apache.org/)项目是一个处理 PDF 文档的 API。它支持文本提取和其他任务,如文档合并、表单填充和 PDF 创建。我们将只说明文本提取过程。为了演示 POI 的使用,我们将使用一个名为TestDocument.pdf的文件。该文件使用TestDocument.docx文件保存为 PDF 文档,如使用 POI 从 Word 文档中提取文本部分所示。这个过程很简单。为 PDF 文档创建一个File对象。PDDocument类表示文档,PDFTextStripper类使用getText方法执行实际的文本提取,如下所示:

File file = new File(getResourcePath());
PDDocument pd = PDDocument.load(file);
PDFTextStripper stripper = new PDFTextStripper();
String text= stripper.getText(pd);
System.out.println(text);

输出如下所示:

Jump to navigation Jump to search  
Welcome to Wikipedia, 
the free encyclopedia that anyone can edit. 
5,673,388 articles in English 
 Arts 
 Biography 
 Geography 
 History 
 Mathematics 
 Science 
 Society 
 Technology 
 All portals 
From today's featured article 

George Steiner 
The Portage to San Cristobal of A.H. is a 1981 
literary and philosophical novella by George Steiner 
(pictured). The story is about Jewish Nazi hunters 
who find a fictional Adolf Hitler (A.H.) alive in the 
Amazon jungle thirty years after the end of World 
War II. The book was controversial, particularly 
among reviewers and Jewish scholars, because the 
author allows Hitler to defend himself when he is 
put on trial in the jungle by his captors. There Hitler 
maintains that Israel owes its existence to the 
Holocaust and that he is the "benefactor of the 
Jews". A central theme of The Portage is the nature 
of language, and revolves around Steiner's lifelong 
work on the subject and his fascination in the power 
and terror of human speech. Other themes include 
the philosophical and moral analysis of history, 
justice, guilt and revenge. Despite the controversy, it 
was a 1983 finalist in the PEN/Faulkner Award for 
Fiction. It was adapted for the theatre by British 

In the news 

Lake Toba 
 Saudi Arabia lifts its ban on 
women driving. 
 Canada legalizes the cultivation of 
cannabis for recreational use 
with effect from October 2018, 
making it the second country to do 
so. 
 An overloaded tourist ferry 
capsizes in Lake Toba (pictured), 
Indonesia, killing at least 3 people 
and leaving 193 others missing. 
 In golf, Brooks Koepka wins the 
U.S. Open at the Shinnecock Hills 
Golf Club. 
Ongoing:  
 FIFA World Cup
.....

使用 Apache Tika 进行内容分析和提取

Apache Tika 能够从数千个不同类型的文件中检测和提取元数据和文本,如.doc.docx.ppt.pdf.xls等。它可以用于各种文件格式,这使得它对于搜索引擎、索引、内容分析、翻译等等非常有用。可以从tika.apache.org/download.html下载。本节将探讨 Tika 如何用于各种格式的文本提取。我们将只使用Testdocument.docxTestDocument.pdf

使用 Tika 非常简单,如以下代码所示:

File file = new File("TestDocument.pdf");            
Tika tika = new Tika();
String filetype = tika.detect(file);

System.out.println(filetype);
System.out.println(tika.parseToString(file));            

只需创建一个Tika的实例,并使用detectparseToString方法获得以下输出:

application/pdf
Jump to navigation Jump to search  

Welcome to Wikipedia, 
the free encyclopedia that anyone can edit. 

5,673,388 articles in English 

 Arts 

 Biography 

 Geography 

 History 

 Mathematics 

 Science 

 Society 

 Technology 

 All portals 

From today's featured article 

George Steiner 

The Portage to San Cristobal of A.H. is a 1981 

literary and philosophical novella by George Steiner 

(pictured). The story is about Jewish Nazi hunters 

who find a fictional Adolf Hitler (A.H.) alive in the 

Amazon jungle thirty years after the end of World 

War II. The book was controversial, particularly 
....

在内部,Tika 将首先检测文档的类型,选择合适的解析器,然后从文档中提取文本。Tika 还提供了解析器接口和类来解析文档。我们也可以使用 Tika 的AutoDetectParserCompositeParser来实现同样的事情。使用解析器,可以获得文档的元数据。更多关于 Tika 的信息可以在 https://tika.apache.org/的找到。

管道

管道只不过是一系列操作,其中一个操作的输出被用作另一个操作的输入。我们在前几章的几个例子中看到了它的使用,但是它们都相对较短。特别是,我们看到了 Stanford StanfordCoreNLP类如何通过使用注释器对象很好地支持管道的概念。我们将在下一节讨论这种方法。如果结构合理,管道的一个优点是可以轻松添加和删除处理元素。例如,如果管道的一个步骤将一个令牌转换为小写,那么很容易删除这个步骤,而管道的其余元素保持不变。然而,有些管道并不总是如此灵活。一个步骤可能需要前一个步骤才能正常工作。在一个管道中,比如由StanfordCoreNLP类支持的管道,需要下面一组注释器来支持 POS 处理:

 props.put("annotators", "tokenize, ssplit, pos");

如果我们忽略了ssplit注释器,就会产生下面的异常:

*java.lang.IllegalArgumentException: annotator "pos" requires  annotator "ssplit"*

虽然斯坦福管道不需要花费很多精力来建立,但其他管道可能需要。我们将在本章后面的创建搜索文本的管道一节中演示后一种方法。

利用斯坦福管道

在本节中,我们将更详细地讨论斯坦福管道。虽然我们在本书的几个例子中使用了它,但是我们还没有完全探索它的能力。您以前使用过这个管道,现在能够更好地理解如何使用它。阅读本节后,您将能够更好地评估其功能和对您需求的适用性。edu.stanford.nlp.pipeline包保存了 StanfordCoreNLP 和注释器类。一般方法使用下面的代码序列来处理文本字符串。Properties类保存注释名称,而注释类表示要处理的文本。 StanfordCoreNLP 类的 Annotate 方法将应用属性列表中指定的注释。 CoreMap 接口是所有可注释对象的基本接口。它使用键和值对。下图显示了类和接口的层次结构:

它是类和接口之间关系的简化版本。 CoreLabel 类实现了 CoreMap 接口。它代表一个附有注释信息的单词。附加的信息取决于创建管线时设置的属性。然而,总是有位置信息可用,比如它的开始和结束位置或者实体前后的空白。用于 CoreMapCoreLabelget方法返回特定于其参数的信息。get方法被重载并返回一个依赖于其参数类型的值。CoreLabel 类已经被用来访问句子中的单个单词。

我们将使用keyset方法,该方法返回一组当前由Annotation对象持有的所有注释键。在应用annotate方法之前和之后都会显示按键。完整的工作代码如下所示:

String text = "The robber took the cash and ran";
        Properties props = new Properties();
        props.put("annotators", "tokenize, ssplit, pos, lemma, ner, parse, dcoref");
        StanfordCoreNLP pipeline = new StanfordCoreNLP(props);
        Annotation annotation = new Annotation(text);

        System.out.println("Before annotate method executed ");
        Set<Class<?>> annotationSet = annotation.keySet();
        for(Class c : annotationSet) {
            System.out.println("\tClass: " + c.getName());
        }

        pipeline.annotate(annotation);

        System.out.println("After annotate method executed ");
        annotationSet = annotation.keySet();
        for(Class c : annotationSet) {
            System.out.println("\tClass: " + c.getName());
        }
        List<CoreMap> sentences = annotation.get(SentencesAnnotation.class);
        for (CoreMap sentence : sentences) {
            for (CoreLabel token: sentence.get(TokensAnnotation.class)) {
                String word = token.get(TextAnnotation.class); 
                String pos = token.get(PartOfSpeechAnnotation.class); 
                System.out.println(word);
                System.out.println(pos);
            }
        }

以下输出显示了调用前后以及单词和位置:

Before annotate method executed 
    Class: edu.stanford.nlp.ling.CoreAnnotations$TextAnnotation
After annotate method executed 
    Class: edu.stanford.nlp.ling.CoreAnnotations$TextAnnotation
    Class: edu.stanford.nlp.ling.CoreAnnotations$TokensAnnotation
    Class: edu.stanford.nlp.ling.CoreAnnotations$SentencesAnnotation
    Class: edu.stanford.nlp.ling.CoreAnnotations$MentionsAnnotation
    Class: edu.stanford.nlp.coref.CorefCoreAnnotations$CorefMentionsAnnotation
    Class: edu.stanford.nlp.ling.CoreAnnotations$CorefMentionToEntityMentionMappingAnnotation
    Class: edu.stanford.nlp.ling.CoreAnnotations$EntityMentionToCorefMentionMappingAnnotation
    Class: edu.stanford.nlp.coref.CorefCoreAnnotations$CorefChainAnnotation
The
DT
robber
NN
took
VBD
the
DT
cash
NN
and
CC
ran
VBD

在斯坦福管道中使用多核

annotate方法也可以利用多个内核。它是一个重载方法,其中一个版本使用一个Iterable<Annotation>的实例作为它的参数。它将使用可用的处理器处理每个Annotation实例。
我们将使用之前定义的pipeline对象来演示这个版本的annotate方法。
首先,我们基于四个短句创建四个Annotation对象,如下所示。为了充分利用这项技术,最好使用更大的数据集。以下是工作代码片段:

Annotation annotation1 = new Annotation("The robber took the cash and ran.");
Annotation annotation2 = new Annotation("The policeman chased him down the street.");
Annotation annotation3 = new Annotation("A passerby, watching the action, tripped the thief "
            + "as he passed by.");
Annotation annotation4 = new Annotation("They all lived happily ever after, except for the thief "
            + "of course.");

ArrayList<Annotation> list = new ArrayList();
list.add(annotation1);
list.add(annotation2);
list.add(annotation3);
list.add(annotation4);
Iterable<Annotation> iterable = list;
pipeline.annotate(iterable);
List<CoreMap> sentences1 = annotation2.get(SentencesAnnotation.class);

for (CoreMap sentence : sentences1) {
    for (CoreLabel token : sentence.get(TokensAnnotation.class)) {
                String word = token.get(TextAnnotation.class);
                String pos = token.get(PartOfSpeechAnnotation.class);
                System.out.println("Word: " + word + " POS Tag: " + pos);
            }
        }

输出如下所示:

Word: The POS Tag: DT
Word: policeman POS Tag: NN
Word: chased POS Tag: VBD
Word: him POS Tag: PRP
Word: down POS Tag: RP
Word: the POS Tag: DT
Word: street POS Tag: NN
Word: . POS Tag: 

创建搜索文本的管道

搜索是一个丰富而复杂的主题。有许多不同类型的搜索和执行搜索的方法。这里的目的是演示如何应用各种 NLP 技术来支持这项工作。在大多数机器上,可以在合理的时间内一次处理一个文本文档。然而,当需要搜索多个大型文档时,创建索引是支持搜索的常用方法。这导致搜索过程在合理的时间内完成。我们将演示一种创建索引的方法,然后使用该索引进行搜索。虽然我们将使用的文本不是很大,但它足以演示这个过程。
我们需要做到以下几点:

  • 从文件中读取文本
  • 标记和查找句子边界
  • 删除停用词
  • 累积索引统计数据
  • 写出索引文件

有几个因素会影响索引文件的内容,包括:

  • 停用词的删除
  • 区分大小写的搜索
  • 查找同义词
  • 使用词干化和词汇化
  • 允许跨句子边界搜索

我们将使用 OpenNLP 来演示这个过程。这个例子的目的是演示如何在流水线过程中结合 NLP 技术来解决搜索类型的问题。这不是一个全面的解决方案,我们将忽略一些技术,如词干。此外,不会介绍索引文件的实际创建,而是留给读者作为练习。在这里,我们将集中讨论如何使用 NLP 技术。具体来说,我们将执行以下操作:

  • 把这本书分成句子
  • 将句子转换成小写
  • 删除停用词
  • 创建内部索引数据结构

我们将开发两个类来支持索引数据结构:WordPositions。我们还将扩充在第二章、中开发的StopWords类,以支持removeStopWords方法的重载版本。新版本将提供一种更方便的删除停用词的方法。我们从一个 try-with-resources 块开始,打开句子模型en-sent.bin的流,以及一个包含儒勒·凡尔纳的海底两万里内容的文件。这本书是从 http://www.gutenberg.org/ebooks/164 下载的。以下代码显示了搜索的一个工作示例:

try {
            InputStream is = new FileInputStream(new File(getResourcePath() + "en-sent.bin"));
            FileReader fr = new FileReader(getResourcePath() + "pg164.txt");
            BufferedReader br = new BufferedReader(fr);
            System.out.println(getResourcePath() + "en-sent.bin");
            SentenceModel model = new SentenceModel(is);
            SentenceDetectorME detector = new SentenceDetectorME(model);

            String line;
            StringBuilder sb = new StringBuilder();
            while((line = br.readLine())!=null){
                sb.append(line + " ");
            }
            String sentences[] = detector.sentDetect(sb.toString());
            for (int i = 0; i < sentences.length; i++) {
                sentences[i] = sentences[i].toLowerCase();
            }

//            StopWords stopWords = new StopWords("stop-words_english_2_en.txt");
//            for (int i = 0; i < sentences.length; i++) {
//                sentences[i] = stopWords.removeStopWords(sentences[i]);
//            }

            HashMap<String, Word> wordMap = new HashMap();
            for (int sentenceIndex = 0; sentenceIndex < sentences.length; sentenceIndex++) {
            String words[] = WhitespaceTokenizer.INSTANCE.tokenize(sentences[sentenceIndex]);
            Word word;
            for (int wordIndex = 0; 
                    wordIndex < words.length; wordIndex++) {
                String newWord = words[wordIndex];
                if (wordMap.containsKey(newWord)) {
                     word = wordMap.remove(newWord);
                } else {
                    word = new Word();
                }
                word.addWord(newWord, sentenceIndex, wordIndex);
                wordMap.put(newWord, word);
            }

            Word sword = wordMap.get("sea");
            ArrayList<Positions> positions = sword.getPositions();
            for (Positions position : positions) {
                System.out.println(sword.getWord() + " is found at line " 
                    + position.sentence + ", word " 
                    + position.position);
            }
        }

        } catch (FileNotFoundException ex) {
            Logger.getLogger(SearchText.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IOException ex) {
            Logger.getLogger(SearchText.class.getName()).log(Level.SEVERE, null, ex);
        }
class Positions {
    int sentence;
    int position;

    Positions(int sentence, int position) {
        this.sentence = sentence;
        this.position = position;
    }
}

public class Word {
    private String word;
    private final ArrayList<Positions> positions;

    public Word() {
        this.positions = new ArrayList();
    }

    public void addWord(String word, int sentence, 
            int position) {
        this.word = word;
        Positions counts = new Positions(sentence, position);
        positions.add(counts);
    }

    public ArrayList<Positions> getPositions() {
        return positions;
    }

    public String getWord() {
        return word;
    }
}

让我们分解代码来理解它。SentenceModel用于创建SentenceDetectorME类的实例,如下所示:

SentenceModel model = new SentenceModel(is);
SentenceDetectorME detector = new SentenceDetectorME(model);

接下来,我们将使用一个StringBuilder实例创建一个字符串来支持句子边界的检测。该书的文件被读取并添加到StringBuilder实例中。然后应用sentDetect方法创建一个句子数组,我们使用toLowerCase方法将文本转换成小写。这样做是为了确保当停用词被删除时,该方法将捕获所有停用词,如下所示:

String line;
StringBuilder sb = new StringBuilder();
while((line = br.readLine())!=null){
    sb.append(line + " ");
}
String sentences[] = detector.sentDetect(sb.toString());
for (int i = 0; i < sentences.length; i++) {
    sentences[i] = sentences[i].toLowerCase();
}

下一步是基于处理后的文本创建一个类似索引的数据结构。这个结构将使用WordPositions类。Word类由单词字段和Positions对象的ArrayList组成。因为一个单词可能在文档中出现不止一次,所以列表用于维护它在文档中的位置。Positions类包含一个字段,用于表示句子编号sentence,以及单词在句子中的位置position。这两个类的定义如下:

class Positions {
    int sentence;
    int position;

    Positions(int sentence, int position) {
        this.sentence = sentence;
        this.position = position;
    }
}

public class Word {
    private String word;
    private final ArrayList<Positions> positions;

    public Word() {
        this.positions = new ArrayList();
    }

    public void addWord(String word, int sentence, 
            int position) {
        this.word = word;
        Positions counts = new Positions(sentence, position);
        positions.add(counts);
    }

    public ArrayList<Positions> getPositions() {
        return positions;
    }

    public String getWord() {
        return word;
    }
}

为了使用这些类,我们创建一个HashMap实例来保存文件中每个单词的位置信息。在 map 中创建单词条目的过程如下面的代码所示。每个句子都被标记化,然后检查每个标记,看它是否存在于映射中。该单词被用作哈希映射的关键字。containsKey方法确定单词是否已经被添加。如果有,那么删除Word实例。如果之前没有添加过这个单词,那么就会创建一个新的Word实例。不管怎样,新的位置信息被添加到Word实例中,然后被添加到地图中,如下所示:

HashMap<String, Word> wordMap = new HashMap();
            for (int sentenceIndex = 0; sentenceIndex < sentences.length; sentenceIndex++) {
            String words[] = WhitespaceTokenizer.INSTANCE.tokenize(sentences[sentenceIndex]);
            Word word;
            for (int wordIndex = 0; 
                    wordIndex < words.length; wordIndex++) {
                String newWord = words[wordIndex];
                if (wordMap.containsKey(newWord)) {
                     word = wordMap.remove(newWord);
                } else {
                    word = new Word();
                }
                word.addWord(newWord, sentenceIndex, wordIndex);
                wordMap.put(newWord, word);
            }

为了演示实际的查找过程,我们使用get方法返回单词“reef”的Word对象的一个实例。用getPositions方法返回位置列表,然后显示每个位置,如下所示:

Word sword = wordMap.get("sea");
            ArrayList<Positions> positions = sword.getPositions();
            for (Positions position : positions) {
                System.out.println(sword.getWord() + " is found at line " 
                    + position.sentence + ", word " 
                    + position.position);
            }

输出如下所示:

sea is found at line 0, word 7
sea is found at line 2, word 6
sea is found at line 2, word 37
sea is found at line 3, word 5
sea is found at line 20, word 11
sea is found at line 39, word 3
sea is found at line 46, word 6
sea is found at line 57, word 4
sea is found at line 133, word 2
sea is found at line 229, word 3
sea is found at line 281, word 14
sea is found at line 292, word 12
sea is found at line 320, word 22
sea is found at line 328, word 21
sea is found at line 355, word 22
sea is found at line 363, word 1
sea is found at line 391, word 13
sea is found at line 395, word 6
sea is found at line 450, word 12
sea is found at line 460, word 6
.....

这个实现相对简单,但是演示了如何组合各种 NLP 技术来创建和使用可以保存为索引文件的索引数据结构。其他增强功能也是可能的,包括以下内容:

  • 其他过滤操作
  • 将文档信息存储在Positions类中
  • 将章节信息存储在Positions类中
  • 提供搜索选项,例如:
    • 区分大小写的搜索
    • 精确文本搜索
    • 更好的异常处理

这些是留给读者的练习。

摘要

在本章中,我们讨论了准备数据的过程,并讨论了管道。我们展示了几种从 HTML、Word 和 PDF 文档中提取文本的技术。我们还看到了 Apache Tika 如何轻松地用于任何类型的文档提取。我们证明了流水线只不过是为解决某个问题而集成的一系列任务。我们可以根据需要插入和移除管道中的各种元素。详细讨论了斯坦福管道体系结构。我们研究了可以使用的各种注释器。探索了该流水线的细节,以及它如何与多个处理器一起使用。在下一章,第十二章,创建聊天机器人我们将致力于创建一个简单的聊天机器人来演示我们到目前为止看到的 NLP 的使用。

十二、创建聊天机器人

聊天机器人在过去几年中变得很流行,许多企业使用它来帮助客户通过网络执行日常任务。社交媒体和信使平台是聊天机器人发展的最大推动力。最近,脸书信使在其信使平台上攻击了 10 万个机器人。除了聊天机器人,语音机器人如今也获得了很大的吸引力,亚马逊的 Alexa 就是语音机器人的一个主要例子。聊天机器人现在已经深入到客户市场,这样客户可以得到及时的回复,而不必等待信息。随着时间的推移,机器学习的发展已经使聊天机器人从简单的对话发展到以行动为导向,现在它们可以帮助客户预约、获取产品细节,甚至接受用户的输入、预订和预定以及在线订单。医疗保健行业正在看到聊天机器人的使用可以帮助越来越多的患者。

你也可以理解聊天机器人的重要性和预期增长,因为许多大公司的负责人已经大量投资聊天机器人或购买基于聊天机器人的公司。你可以说出任何一个大型组织——比如谷歌、微软、脸书或 IBM——都在积极提供聊天机器人平台和 API。我们都用过 Siri,或者 Google Assistant,或者 Alexa,都不过是机器人。

下图是 2017 年聊天机器人的版图:

来源–https://blog . key reply . com/the-chatbot-landscape-2017-edition-ff 2 e 3d 2 a 0 BDB

同心圆从内圈开始,显示平台、品牌、提供商和工具。

在这一章中,我们将会看到不同类型的聊天机器人,我们也将会开发一个简单的预约聊天机器人。

本章将涵盖以下主题:

  • 聊天机器人架构
  • 人工语言互联网计算机实体

聊天机器人架构

聊天机器人只不过是一个可以与用户聊天并代表用户执行特定级别任务的计算机程序。聊天机器人似乎在用户的问题和解决方案之间有着直接的联系。聊天机器人的主要方面如下:

  • 简单聊天机器人 : 关于这种类型的聊天机器人,用户会键入一些文本,大多是以问题的形式,机器人会以文本的形式做出适当的回复。
  • 对话聊天机器人:这种聊天机器人知道对话的上下文并保持状态。根据用户,对用户文本的响应是以对话的形式。
  • 人工智能聊天机器人(AI chatbot):这种聊天机器人从提供给它的训练数据中学习,这些数据是从许多不同的场景或过去的一长串对话中准备的。

聊天机器人的主要方面是使用一些预定义的库或数据库,或使用机器学习模型来生成对用户文本的正确或适当的响应。机器学习算法允许用大量数据或对话的例子训练机器人选择模式。它使用意图分类和实体来生成响应。为了找到意图和实体,它使用了自然语言理解的概念()😗***

****

为聊天机器人使用机器学习需要对机器学习算法有很好的理解,这超出了本书的范围。

我们将研究一个不涉及机器学习的选项,这样的模型被称为基于检索的模型,其中响应是从一些预定义的逻辑和上下文中生成的。它易于构建且可靠,但在响应生成方面并不是 100%准确。它被广泛使用,有几个 API 和算法可用于这样的模型。它基于if...else条件生成响应,这被称为基于模式的响应生成:

它依靠人工智能标记语言 ( AIML )来记录模式和反应。这将在下一节讨论。

人工语言互联网计算机实体

人工语言互联网计算机实体 ( 爱丽丝)是一个由 AIML 创建的免费聊天机器人软件。这是一个 NLP 聊天机器人,它可以使用一些启发式模式匹配规则与人类进行对话。它已经三次获得罗布纳奖,该奖授予有成就的会说话的机器人。它没有通过图灵测试,但仍然可以用于正常聊天,并且可以定制。

了解 AIML

在本节中,我们将使用 AIML。AIML 是一种基于 XML 的标记语言,用于开发人工智能应用程序,尤其是软件代理。它包含用户请求的规则或响应,供 NLU 单位内部使用。简单来说,我们在 AIML 中添加的规则越多,我们的聊天机器人就会越智能,越准确。

因为 AIML 是基于 XML 的标记语言;它以根标签<aiml>开始,所以一个典型的 AIML 文件看起来像这样:

<?xml version="1.0" encoding="UTF-8"?>
<aiml>
</aiml>

为了添加问题和答案或对可能的查询的响应,使用了<category>标签。它是聊天机器人知识库的基本单元。简单地说,<category>接受输入并返回输出。所有 AIML 元素必须包含在<category>元素中。<pattern>标签用于匹配用户的输入,<template>标签是对用户输入的响应。将它添加到前面的代码中,代码现在应该如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<aiml>
    <category>
        <pattern>Hello</pattern>
        <template> Hello, How are you ? </template>
    </category>
</aiml>

所以,每当用户输入单词Hello,机器人就会用Hello, How are you ?来回应。

<pattern>标签中使用一个*作为通配符来指定任何内容都可以代替星号,在<template>标签中使用一个<star>标签来形成响应,如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<aiml>
    <category>
        <pattern>I like *.</pattern>
        <template>Ok, so you like <star/></template>
    </category>
</aiml>

现在,当用户说“I like Mangoes”时,机器人的响应将是“Ok so you like mangoes”。我们也可以使用多个*,如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<aiml>
    <category>
    <pattern>I like * and *</pattern>
        <template> Ok, so you like <star index="1"/> and <star index="2"/></template>
    </category>
</aiml>

现在,当用户说“I like Mangoes and Bananas”时,机器人的响应将是“Ok so you like mangoes and bananas”。

接下来是<srai>标签,用于不同的模式以生成相同的模板,如下:

<?xml version="1.0" encoding="UTF-8"?>
<aiml>
    <category>
        <pattern>I WANT TO BOOK AN APPOINTMENT</pattern>
        <template>Are you sure</template>
    </category>
    <category>
        <pattern>Can I *</pattern>
        <template><srai>I want to <star/></srai></template>
    </category>    
    <category>
        <pattern>May I * </pattern>
        <template>
            <srai>I want to <star/></srai>
        </template>
    </category>
</aiml>

第一个类别具有“I WANT TO BOOK AN APPOINTMENT”的模式,其响应为“Are you sure”。在下一个类别中,如果用户询问“Can I book an appointment或“”),得到的响应将是相同的:“Are you sure”。

正如我们在这里看到的,<srai>标签有许多用途,它也可以用于同义词和关键字解析。

更多标签请参考call mom . Pandora bots . com/static/reference/# aiml-2-0-reference

使用 ALICE 和 AIML 开发聊天机器人

要开发聊天机器人,我们需要一个 AIML 解释器或 AIML 的参考实现。一个这样的工具是 AB 程序,它可以在 https://code.google.com/archive/p/program-ab/找到。在下载部分,程序 AB 有这个 ZIP 文件。提取文件,该文件将包含以下目录:

  • bots:包含super文件夹以显示机器人的名称
  • data:包含样本文本
  • lib:包含Ab.jar
  • out:包含一个类文件

bots目录的super子目录中,我们可以看到目录名aimlaimlfconfigdatamapssets。这些是使用 AIML 和 ALICE 创建聊天机器人所需的标准目录。让我们测试一下聊天机器人。打开一个新的终端,移动到我们提取的program-ab文件夹,并执行以下命令:

program-ab-0.0.4.3$ java -cp lib/Ab.jar Main bot = test action=chat trace=false

它将加载所有文件,并向您显示一个提示,如下所示:

Human :

试着用一些文本聊天,你很快就会意识到它是有效的,但并不总是有效,也不是对所有的查询都有效。以下是聊天演示:

现在,让我们创建自己的聊天机器人来安排约会。第一步是创建一个 AIML 文件。

在新的 NetBeans 项目中创建以下文件夹结构,并将Ab.jar添加到项目库中:

aiml目录中,我们创建一个 AIML 文件,内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<aiml>
<!--  -->
<category><pattern>I WANT TO BOOK AN APPOINTMENT</pattern>
<template>Are you sure you want to book an appointment</template>
</category>
<category><pattern>YES</pattern><that>ARE YOU SURE YOU WANT TO BOOK AN APPOINTMENT</that>
<template>Can you tell me date and time</template>
</category>
<category><pattern>NO</pattern><that>ARE YOU SURE YOU WANT TO BOOK AN APPOINTMENT</that>
<template>No Worries.</template>
</category>
<category><pattern>DATE * TIME *</pattern><that>CAN YOU TELL ME DATE AND TIME</that>
<template>You want appointment on <set name="udate"><star index="1"/> </set> and time <set name="utime"><star index="2"/></set>. Should i confirm.</template>
</category>
<category><pattern>YES</pattern><that>SHOULD I CONFIRM</that>
<template><get name="username"/>, your appointment is confirmed for <get name="udate"/> : <get name="utime"/></template>
</category>
<category><pattern>I AM *</pattern>
<template>Hello <set name="username"> <star/>! </set></template>
</category>
<category><pattern>BYE</pattern>
<template>Bye <get name="username"/> Thanks for the conversation!</template>
</category>
</aiml>

让我们研究一下 AIML 文件。使用setget标签,可以将上下文保存在变量中,并在需要时进行检索:

<category><pattern>I AM *</pattern>
<template>Hello <set name="username"> <star/>! </set></template>
</category>

这展示了set属性的使用,所以当用户输入“I am ashish”时,它被保存在变量name中,响应为“Hello Ashish !”。现在,通过使用get打印用户名,这可以在 AIML 的任何地方使用。因此,这意味着可以保持使用setget标签上下文。

下一步是创建一个约会。当用户要求预约时,响应将要求确认,如下所示:

<category><pattern>I WANT TO BOOK AN APPOINTMENT</pattern>
<template>Are you sure you want to book an appointment</template>
</category>

现在,来自用户的预期请求将是 yes 或 no,根据它们生成下一个响应。为了在最后一个问题的上下文中继续对话,使用标签,如下所示:

<category><pattern>YES</pattern><that>ARE YOU SURE YOU WANT TO BOOK AN APPOINTMENT</that>
<template>Can you tell me date and time</template>
</category>
<category><pattern>NO</pattern><that>ARE YOU SURE YOU WANT TO BOOK AN APPOINTMENT</that>
<template>No Worries.</template>
</category>

如果用户说“YES”,聊天机器人将询问日期和时间,该日期和时间再次被保存,并且询问确认用户是否想要在规定的日期和时间预约,如下所示:

<category><pattern>DATE * TIME *</pattern><that>CAN YOU TELL ME DATE AND TIME</that>
<template>You want appointment on <set name="udate"><star index="1"/> </set> and time <set name="utime"><star index="2"/></set>. Should i confirm.</template>
</category>
<category><pattern>YES</pattern><that>SHOULD I CONFIRM</that>
<template><get name="username"/>, your appointment is confirmed for <get name="udate"/> : <get name="utime"/></template>
</category>

示例聊天输出如下:

Robot : Hello, I am your appointment scheduler May i know your name
Human : 
I am ashish
Robot : Hello ashish!
Human : 
I want to book an appointment
Robot : Are you sure you want to book an appointment
Human : 
yes
Robot : Can you tell me date and time
Human : 
Date 24/06/2018 time 4 pm
Robot : You want appointment on 24/06/2018 and time 4 pm. Should i confirm.
Human : 
yes
Robot : ashish!, your appointment is confirmed for 24/06/2018 : 4 pm

将此 AIML 文件作为myaiml.aiml保存在aiml目录中。下一步是创建 AIML 中间格式 CSV 文件。创建一个名为GenerateAIML.java的 Java 文件,并添加以下代码:

public class GenerateAIML {

        private static final boolean TRACE_MODE = false;
        static String botName = "appointment";

    public static void main(String[] args) {
        try {

            String resourcesPath = getResourcesPath();
            System.out.println(resourcesPath);
            MagicBooleans.trace_mode = TRACE_MODE;
            Bot bot = new Bot("appointment", resourcesPath);

            bot.writeAIMLFiles();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static String getResourcesPath(){
        File currDir = new File(".");
        String path = currDir .getAbsolutePath();
        path = path.substring(0, path.length()-2);
        System.out.println(path);
        String resourcePath = path + File.separator  + "src/chapter12/mybot";
        return resourcePath;
    }
}

执行这个文件。它会在aimlif目录下生成myaiml.aiml.csv

根据您在 NetBeans 中的软件包更改ResourcePath变量。在本例中,chapter12是包名,mybot是包内的目录。

创建另一个 Java 文件来测试 bot,如下所示:

public class Mychatbotdemo {
    private static final boolean TRACE_MODE = false;
    static String botName = "appointment";
    private static String getResourcePath(){
        File currDir = new File(".");
        String path = currDir .getAbsolutePath();
        path = path.substring(0, path.length()-2);
        System.out.println(path);
            String resourcePath = path + File.separator  + "src/chapter12/mybot";
        return resourcePath;
    }
    public static void main(String args[]){
        try
        {
            String resourcePath = getResourcePath();
            System.out.println(resourcePath);
            MagicBooleans.trace_mode = TRACE_MODE;
            Bot bot = new Bot(botName, resourcePath);
            Chat chatSession = new Chat(bot);
            bot.brain.nodeStats();
            String textLine = "";
            System.out.println("Robot : Hello, I am your appointment scheduler May i know your name");
            while(true){

                System.out.println("Human : ");
                textLine = IOUtils.readInputTextLine();
                if ((textLine==null) || (textLine.length()<1)){
                    textLine = MagicStrings.null_input;
                }
                if(textLine.equals("q")){
                    System.exit(0);
                } else if (textLine.equals("wq")){
                    bot.writeQuit();
                } else {
                    String request = textLine;
                    if(MagicBooleans.trace_mode)
                        System.out.println("STATE=" + request + ":THAT" + ((History)chatSession.thatHistory.get(0)).get(0) + ": Topic" + chatSession.predicates.get("topic"));
                    String response = chatSession.multisentenceRespond(request);
                    while(response.contains("&lt;"))
                        response = response.replace("&lt;", "<");
                    while(response.contains("&gt;"))
                        response = response.replace("&gt;", ">");
                    System.out.println("Robot : " + response);
                }
            }
        }
        catch(Exception e){
            e.printStackTrace();
        }

    }
}

执行 Java 代码,您将看到提示说Human:,它将等待输入。按下 Q 将结束程序。根据我们的 AIML 文件,我们的对话是有限的,因为我们只要求基本信息。我们可以将它与super文件夹集成在一起,并将我们的 AIML 文件添加到super目录中,这样我们就可以使用默认的所有可用对话和我们的定制约会对话。

摘要

在这一章中,我们看到了聊天机器人的重要性以及它们的发展方向。我们还向您展示了不同的聊天机器人架构。我们从了解 ALICE 和 AIML 开始,使用 AIML,我们创建了一个用于约会安排的演示聊天机器人,以展示使用 ALICE 和 AIML 的聊天机器人的概念。****

posted @ 2024-08-19 17:28  绝不原创的飞龙  阅读(3)  评论(0编辑  收藏  举报