Java 自然语言处理(一)
自然语言处理NLP)是一个广泛的话题,专注于使用计算机来分析自然语言。它涉及诸如语音处理、关系提取、文档分类和文本汇总等领域。然而,这些类型的分析是基于一套基本的技术,如标记化、句子检测、分类和提取关系。这些基本技术是本书的重点。我们将从 NLP 的详细讨论开始,研究它为什么重要,并确定应用领域。有许多工具支持 NLP 任务。我们将重点关注 Java 语言以及各种 Java应用程序员接口API)如
零、前言
自然语言处理 ( 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 的并注册,让文件直接通过电子邮件发送给你。
您可以按照以下步骤下载代码文件:
- 在www.packtpub.com登录或注册。
- 选择支持选项卡。
- 点击代码下载和勘误表。
- 在搜索框中输入图书名称,然后按照屏幕指示进行操作。
下载文件后,请确保使用最新版本的解压缩或解压文件夹:
- 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 支持包括字符串库,比如String
、StringBuilder
和StringBuffer
。这些类拥有执行搜索、匹配和文本替换的方法。正则表达式使用特殊编码来匹配子字符串。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 使用的一般方法是从文件中实例化一个支持任务的模型,然后针对该模型执行方法来执行任务。
例如,在下面的序列中,我们将标记一个简单的字符串。为了正确执行该代码,它必须处理FileNotFoundException
和IOException
异常。我们使用 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
是由CoreLabelTokenFactory
和WordTokenFactory
类实现的接口。前一个类支持保留标记的开始和结束字符位置,而后一个类只是将标记作为不带任何位置信息的字符串返回。默认情况下使用WordTokenFactory
类。
在下面的例子中使用了CoreLabelTokenFactory
类。使用字符串创建一个StringReader
。最后一个参数用于选项参数,在本例中是null
。Iterator
接口由PTBTokenizer
类实现,允许我们使用hasNext
和next
方法来显示令牌:
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&;q =深度+学习&;储存库& 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
类和相关类都有方法,比如indexOf
和lastIndexOf
方法,可以找到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 工具集,它们都有几个共同的步骤。在本节中,我们将介绍这些步骤。当你阅读本书中介绍的章节和技术时,你会看到这些步骤会有细微的变化。现在对它们有一个很好的理解将会减轻学习技术的任务。
基本步骤包括以下内容:
- 确定任务
- 选择模型
- 构建和训练模型
- 验证模型
- 使用模型
我们将在下面的小节中讨论这些步骤。
确定任务
理解需要解决的问题很重要。基于这种理解,可以设计出由一系列步骤组成的解决方案。这些步骤中的每一步都将使用 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
| 由CharBuffer
、Segment
、String
、StringBuffer
、StringBuilder
实现的接口。它支持对字符序列的只读访问。 |
Java 还提供了许多支持字符串的类和接口。下表对这些进行了总结。我们将在许多例子中使用这些。String
、StringBuffer
和StringBuilder
类提供了相似的字符串处理能力,但是在它们是否可以被修改以及它们是否是线程安全的方面有所不同。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 类可以完成这项工作时,为什么要使用更难理解、效率更低的方法呢?我们将讨论这些类中的每一个,因为它们支持令牌化过程。
StreamTokenizer
和StringTokenizer
类不应用于新的开发。相反,?? 方法通常是更好的选择。它们被包含在这里,以防你碰到它们,想知道它们是否应该被使用。
使用 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 简介中演示了String
class’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();
接下来的循环将使用begin
和end
变量存储断词的开始和结束边界索引。边界值是整数。将显示每个边界对及其相关文本。
当找到最后一个边界时,循环终止:
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
| 令牌读取的类型 |
在这个例子中,创建了一个标记器,然后声明了用于终止循环的变量isEOF
。nextToken
方法返回令牌类型。根据令牌类型,将显示数字令牌和字符串令牌:
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
接口:SimpleTokenizer
、TokenizerME
和WhitespaceTokenizer
。该接口支持两种方法:
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
接口是由CoreLabelTokenFactory
和WordTokenFactory
类实现的。前一个类支持保留标记的开始和结束字符位置,而后一个类只是将标记作为不带任何位置信息的字符串返回。默认情况下使用WordTokenFactory
类。我们将演示这两个类的用法。
在下面的例子中使用了CoreLabelTokenFactory
类。使用paragraph
创建一个StringReader
实例。最后一个参数用于选项,在本例中是null
。Iterator
接口由PTBTokenizer
类实现,允许我们使用hasNext
和next
方法来显示令牌:
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
接口包含两个处理文字的方法:setWord
和word
。后一种方法将单词作为字符串返回。在下面的代码序列中,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
对象,并为其分配了tokenize
和ssplit
标注器。
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
方法了,如下面的代码所示。英语被指定为语言。字母数字信息被忽略。特征值和迭代值分别设置为5
和100
:
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/找到。它用五个步骤来做一个单词。这些步骤是:
- 改变复数,简单现在,过去和过去分词,将 y 转换为 I,例如 agreed 将被改为 agree,sleep 将被改为 sleepi
- 将双后缀改为单后缀,例如专门化将改为专门化
- 按照步骤 2 中的方法,将“特殊”更改为“特殊”
- 通过将 speci 改为 speci 来更改剩余的单个后缀
- 它删除 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 都支持词汇化。在这一节中,我们将说明如何使用StanfordCoreNLP
和OpenNLPLemmatizer
类来执行词汇化。词汇化过程决定了一个词的词汇。一个引理可以被认为是一个单词的字典形式。例如,的引理是是是。
使用 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);
我们现在需要迭代语句和语句的标记。Annotation
和CoreMap
class’ get
方法将返回指定类型的值。如果没有指定类型的值,它将返回null
。我们将使用这些类来获得一个引理列表。
首先,返回一个句子列表,然后处理每个句子的每个单词以找到词条。这里声明了sentences
和lemmas
的列表:
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
类的实例。这是字典的位置。我们还定义了样本文本。构造函数可以抛出IOException
和JWNLException
,我们在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 一节中使用的例子来删除停用词。我们将添加两个额外的工厂来规范化文本:LowerCaseTokenizerFactory
和PorterStemmerTokenizerFactory
。
在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 确定句子结尾所涉及的问题。
更多推荐
所有评论(0)