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

英语句子的统计解析

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (82投票s)

2005年10月30日

17分钟阅读

viewsIcon

1133314

downloadIcon

23597

演示了如何使用C#版本的OpenNLP(一个统计自然语言解析库)来生成英文句子的解析树。

Parser demo user interface

概述

上一篇文章中,我介绍了名为SharpEntropy的最大熵建模库,它是对一个成熟的Java库MaxEnt toolkit的C#移植。Java MaxEnt库被另一个名为OpenNLP的开源Java库使用,该库提供了许多基于最大熵模型的自然语言处理工具。本文将介绍如何使用我移植的C#版本的OpenNLP库来生成英文句子的解析树,并探讨OpenNLP代码的一些其他功能。请注意,由于原始Java OpenNLP库是在LGPL许可证下发布的,本文提供的C# OpenNLP库的源代码也遵循LGPL许可证。这意味着,它可以自由地用于任何许可证下的软件,但如果您对库本身进行了修改,并且这些修改并非供您个人使用,则必须公开这些修改的源代码。

引言

OpenNLP既是一个与自然语言处理(NLP)相关的开源项目集合的名称,也是由Jason Baldridge、Tom Morton和Gann Bierner用Java编写的NLP工具库的名称。我的C#移植基于最新版本(1.2.0)的Java OpenNLP工具,于2005年4月发布。Java库的开发仍在继续,我希望随着新开发的出现及时更新C#移植。

C#移植中包含的工具包括:句子分割器、分词器、词性标注器、短语提取器(用于“查找非递归的语法标注,如名词短语短语”)、解析器和命名实体识别器。Java库还包含一个共指消解工具,但该功能的代码正在变动中,尚未移植到C#。所有这些工具都由SharpEntropy库处理的最大熵模型驱动。

自从本文初次撰写以来,共指消解工具已移植到C#,并与最新版本的其他工具一起,可在CodePlex上的SharpNLP项目中找到。

设置OpenNLP库

自从本文初次撰写以来,所需的二进制数据文件现已可在CodePlex上的SharpNLP项目中下载。您无需从Sourceforge下载Java兼容文件然后通过ModelConverter工具进行转换,可以直接下载所需的.nbin格式文件。

驱动OpenNLP库的最大熵模型包含一组二进制数据文件,总计123 MB。由于其体积庞大,无法在CodeProject上提供下载。不幸的是,这意味着在您的机器上设置OpenNLP库需要比仅仅下载Zip文件、解压并运行可执行文件更多的步骤。

首先,下载演示项目Zip文件,并将其内容解压到硬盘上的一个文件夹中。然后,在您选择的文件夹中,创建一个名为“Models”的子文件夹。在“Models”文件夹内创建两个子文件夹,一个名为“Parser”,另一个名为“NameFind”。

其次,从SourceForge上的Java OpenNLP库项目区域的CVS存储库下载OpenNLP模型文件。这可以通过CVS客户端完成,或使用Web界面。将分词器(EnglishChunk.bin)、词性标注器(EnglishPOS.bin)、句子分割器(EnglishSD.bin)和词语分词器(EnglishTok.bin)的.bin文件放在您在第一步中创建的Models文件夹中。此截图显示了所需的文件布局

Folder structure for model files (Models)

将命名实体识别器的.bin文件放在NameFind子文件夹中,如下所示

Folder structure for model files (NameFind)

然后,将解析器所需的文件放在Parser子文件夹中。这包括“tagdict”和“head_rules”文件,以及四个.bin文件

Folder structure for model files (Parser)

这些模型由Java OpenNLP团队以原始MaxEnt格式创建。它们必须被转换为.NET格式,以便与C# OpenNLP库一起使用。SharpEntropy的文章解释了SharpEntropy库理解的不同模型格式及其使用原因。

作为演示项目下载的一部分,提供了一个名为ModelConverter.exe的命令行程序,用于转换模型文件。从命令提示符运行它,指定“Models”文件夹的位置,它将处理每个.bin文件并从中创建一个新的.nbin文件。这个过程通常需要一些时间 - 几分钟或更长,具体取决于您的硬件配置。

Running ModelConverter.exe from the command line

(此截图与上面的文件夹截图一样,都来自我用于测试的Windows 98虚拟机。当然,代码在新操作系统上也能正常运行——我的主要开发机器是Windows XP。)

模型转换器成功完成后,演示可执行文件应该可以正常运行。

演示项目包含什么?

除了ModelConverter之外,演示项目还提供了两个Windows Forms可执行文件:ToolsExample.exeParseTree.exe。这两个都使用了OpenNLP.dll,而OpenNLP.dll又依赖于SharpEntropy.dll,即我在上一篇文章中介绍的SharpEntropy库。Parse Tree演示还使用了NetronProject的树形视图控件(经过修改的版本),名为“Lithium”,可在CodeProject 此处获取

Tools Example提供了一个简单的界面来展示OpenNLP库提供的各种自然语言处理工具。Parse Tree演示使用修改后的Lithium控件,以更图形化的方式演示OpenNLP在英文句子解析方面所能实现的功能。

运行源代码

提供了两个Windows Forms可执行文件、ModelConverter程序和OpenNLP库(LGPL许可)的源代码。修改后的Lithium控件的源代码也已包含在内,尽管对原始CodeProject版本的修改很小。SharpEntropy库的源代码可从我的SharpEntropy文章中获取。

源代码的编写方式是,EXEs会在其运行的文件夹内查找“Models”文件夹。这意味着,如果您从开发环境中运行项目,则需要将“Models”子文件夹放置在编译代码时创建的相应“bin”目录中,或者修改源代码以查找其他位置。这是来自MainForm构造函数的相关代码

mModelPath = System.IO.Path.GetDirectoryName(
   System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase);
mModelPath = new System.Uri(mModelPath).LocalPath + @"\Models\";

这可以替换为您自己的计算Models文件夹位置的方案。

关于性能的说明

OpenNLP代码设置为使用一个SharpEntropy.IO.IGisModelReader实现,该实现将所有模型数据保存在内存中。在使用一些简单工具(如句子分割器或分词器)时,这不太可能造成问题。更复杂的工具,如解析器和命名实体识别器,使用多个大型模型。英文解析器的最大熵模型数据大约占用250 MB内存,因此我建议您在使用此代码时使用功能强大的硬件。如果您的PC内存不足并开始使用硬盘,性能将急剧下降。

检测句子结束

如果我们有一个字符串变量input包含一段文本,那么将其分割成句子的一个简单且有限的方法是使用input.Split('.')来获得字符串数组。将其扩展为input.Split('.', '!', '?')将能更正确地处理更多情况。但是,虽然这组标点符号可以作为句子的结束符,但这种技术无法识别它们也可能出现在句子的中间。请看下面这个简单的段落

Mr. Jones went shopping. His grocery bill came to $23.45.

使用此输入的Split方法将产生一个包含五个元素的数组,而我们实际上只需要一个包含两个元素的数组。我们可以通过将字符'.''!''?'中的每一个都视为潜在的句子结束标记而非确定的标记来实现此目的。我们扫描输入文本,每当遇到这些字符之一时,就需要一种方法来判断它是否标记着句子的结束。这时最大熵模型就派上用场了。生成一组与可能的句子结束位置相关的谓词。使用各种特征,涉及可能的句子结束标记之前和之后的字符,来生成这组谓词。然后,这组谓词会根据MaxEnt模型进行评估。如果最佳结果指示句子中断,那么直到句子结束标记位置(包括该标记)的所有字符将被分开形成一个新句子。

所有这些功能都包含在OpenNLP.Tools.SentenceDetect命名空间中的类里,因此执行智能句子分割所需要做的就是实例化一个EnglishMaximumEntropySentenceDetector对象并调用其SentenceDetect方法。

using OpenNLP.Tools.SentenceDetect;
        
EnglishMaximumEntropySentenceDetector sentenceDetector = 
  new EnglishMaximumEntropySentenceDetector(mModelPath + "EnglishSD.nbin");
string[] sentences = sentenceDetector.SentenceDetect(input);

最简单的EnglishMaximumEntropySentenceDetector构造函数接受一个参数,即一个string,其中包含句子检测MaxEnt模型文件的文件路径。如果将上面简单示例中显示的文本传递给SentenceDetect方法,结果将是一个包含两个元素的数组:“Mr. Jones went shopping.”和“His grocery bill came to $23.45.”

Tools Example可执行文件展示了OpenNLP库的句子分割能力。在顶部文本框中输入一段文本,然后单击“Split”按钮。分割后的句子将显示在下部文本框中,每行一个。

Sentence splitting with the Tools Example

分词

在隔离出一个句子后,我们可能希望对其应用某种NLP技术——例如,词性标注或完全解析。此过程的第一步是将句子分割成“词元”(token)——即单词和标点符号。同样,仅使用Split方法不足以准确实现这一点。取而代之的是,我们可以使用EnglishMaximumEntropyTokenizer对象的Tokenize方法。该类以及OpenNLP.Tools.Tokenize命名空间中的相关类,使用与我在Sharpentropy文章后半部分描述的相同的分词方法,此处不再赘述。与句子检测类一样,使用此功能就像实例化一个类并调用一个方法一样简单

using OpenNLP.Tools.Tokenize;
        
EnglishMaximumEntropyTokenizer tokenizer = 
  new EnglishMaximumEntropyTokenizer(mModelPath + "EnglishTok.nbin");
string[] tokens = tokenizer.Tokenize(sentence);

此分词器会分割包含缩写的词语:例如,它会将“don't”分割成“do”和“n't”,因为它被设计成将这些词元传递给其他NLP工具,“do”被识别为动词,“n't”被识别为“not”的缩写,作为修饰前一个动词“do”的副词。

Tools Example中的“Tokenize”按钮将顶部文本框中的文本分割成句子,然后对每个句子进行分词。输出结果在下部文本框中,词元之间用管道符分隔。

Tokenizing with the Tools Example

词性标注

词性标注是指为句子中的每个单词分配一个词性(有时缩写为POS)。在获得分词过程产生的词元数组后,我们可以将该数组馈送给词性标注器

using OpenNLP.Tools.PosTagger;

EnglishMaximumEntropyPosTagger posTagger = 
  new EnglishMaximumEntropyPosTagger(mModelPath + "EnglishPOS.nbin");
string[] tags = mPosTagger.Tag(tokens);

POS标签以与词元数组长度相同的数组形式返回,其中数组中每个索引处的标签与词元数组同一索引处的词元匹配。POS标签由符合宾夕法尼亚大学开发的语言语料库Penn Treebank方案的代码缩写组成。可以通过调用AllTags()方法获得可能的标签列表;以下是它们,以及Penn Treebank的描述

CC    Coordinating conjunction  RP    Particle
CD    Cardinal number           SYM   Symbol
DT    Determiner                TO    to
EX    Existential there         UH    Interjection
FW    Foreign word              VB    Verb, base form
IN    Preposition/subordinate   VBD   Verb, past tense
      conjunction
JJ    Adjective                 VBG   Verb, gerund/present participle
JJR   Adjective, comparative    VBN   Verb, past participle
JJS   Adjective, superlative    VBP   Verb, non-3rd ps. sing. present
LS    List item marker          VBZ   Verb, 3rd ps. sing. present
MD    Modal                     WDT   wh-determiner
NN    Noun, singular or mass    WP    wh-pronoun
NNP   Proper noun, singular     WP$   Possessive wh-pronoun
NNPS  Proper noun, plural       WRB   wh-adverb
NNS   Noun, plural            	``    Left open double quote
PDT   Predeterminer             ,     Comma
POS   Possessive ending         ''    Right close double quote
PRP   Personal pronoun          .     Sentence-final punctuation
PRP$  Possessive pronoun        :     Colon, semi-colon
RB    Adverb                    $     Dollar sign
RBR   Adverb, comparative       #     Pound sign
RBS   Adverb, superlative       -LRB- Left parenthesis *
                                -RRB- Right parenthesis *
                        
* The Penn Treebank uses the ( and ) symbols, 
  but these are used elsewhere by the OpenNLP parser.

用于POS标注的最大熵模型是使用《华尔街日报》和布朗语料库的文本训练的。可以通过提供POS查找列表来进一步控制POS标注器。有两个备用EnglishMaximumEntropyPosTagger构造函数,它们通过文件路径或PosLookupList对象指定POS查找列表。标准的POS标注器不使用查找列表,但完整的解析器使用。查找列表包含一个文本文件,每行一个单词及其可能的POS标签。这意味着,如果您要标注的句子中的单词在查找列表中找到,POS标注器可以将可能的POS标签列表限制在查找列表中指定的那些标签,从而更有可能选择正确的标签。

Tag方法有两个版本,一个接受字符串数组,另一个接受ArrayList。除了这些方法外,EnglishMaximumEntropyPosTagger还提供了一个TagSentence方法。此方法绕过了分词步骤,接受整个句子,并依赖于简单的Split来查找词元。它还以一种常用于显示POS标注算法结果的格式,将每个词元后面跟着一个“/”然后是其标签,来产生POS标注的结果。

Tools Example应用程序将输入段落分割成句子,对每个句子进行分词,然后使用Tag方法对该句子进行POS标注。这里,我们看到G. K. Chesterton的小说《The Man Who Was Thursday》前几句的结果。每个词元后面跟着一个“/”字符,然后是最大熵模型分配的、最有可能的词性标签。

part-of-speech tagging with the Tools Example

查找短语(“分块”)

OpenNLP分块工具会将句子的词元分组为更大的短语,每个短语对应一个语法单元,如名词短语或动词短语。这是通往完全解析的下一步,但它本身在查找比单个单词更大的句子意义单元时也可能很有用。要执行分块任务,需要一个已进行POS标注的词元集合。

EnglishTreebankChunker类有一个Chunk方法,它接受我们通过调用POS标注器生成的词元字符串数组和POS标签字符串数组,并返回第三个字符串数组,同样是每个词元一个条目。此数组需要一些解释才能有用。它包含的字符串以“B-”开头,表示该词元是一个短语的开始,或者以“I-”开头,表示该词元位于短语内部但不是其开头。在此前缀之后是表示该词元所属短语类型的Penn Treebank标签

ADJP    Adjective Phrase    PP     Prepositional Phrase
ADVP    Adverb Phrase       PRT    Particle
CONJP   Conjunction Phrase  SBAR   Clause introduced by a subordinating conjunction
INTJ    Interjection        UCP    Unlike Coordinated Phrase
LST     List marker         VP     Verb Phrase
NP      Noun Phrase

EnglishTreebankChunker类还有一个GetChunks方法,它将返回整个句子作为格式化的字符串,其中短语用方括号表示。可以这样调用

using OpenNLP.Tools.Chunker;
        
EnglishTreebankChunker chunker = 
  new EnglishTreebankChunker(mModelPath + "EnglishChunk.nbin");
string formattedSentence = chunker.GetChunks(tokens, tags);

Tools Example应用程序使用POS标注代码生成词元和标签的字符串数组,然后将其传递给分块器。结果显示了之前指示的POS标签,但输出句子中的短语用方括号括起来。

chunking with the Tools Example

完全解析

生成完全解析树的任务建立在我们到目前为止所涵盖的NLP算法之上,但它进一步将分块的短语组合成一个树状图,说明句子的结构。OpenNLP库实现的完全解析算法使用句子分割和分词步骤,但将POS标注和分块作为独立但相关的过程的一部分执行,该过程由“Models”文件夹的“Parser”子文件夹中的模型驱动。完全解析POS标注步骤使用在tagdict文件中找到的标签查找列表。

通过创建EnglishTreebankParser类的对象,然后调用DoParse方法来调用完全解析器

using OpenNLP.Tools.Parser;

EnglishTreebankParser parser = 
   new EnglishTreebankParser(mModelPath, true, false);
Parse sentenceParse = parser.DoParse(sentence);

EnglishTreebankParser类有许多构造函数,但其中一个最简单的接受三个参数:Models文件夹的路径,以及两个布尔标志:第一个用于指示我们是否使用标签查找列表,第二个用于指示标签查找列表是否区分大小写。DoParse方法也有一些重载,接受单个句子或句子字符串数组,并且还允许您选择请求多个排名靠前的解析树(排名最靠前的解析树在前)。DoParse方法的简单版本接受单个句子,并返回一个OpenNLP.Tools.Parser.Parse类型的对象。该对象是Parse对象树的根,代表对句子的最佳猜测解析。可以使用Parse对象的GetChildren()方法和Parent属性来遍历该树。每个解析节点的Penn Treebank标签存储在Type属性中,除非该节点代表句子中的一个词元——在这种情况下,Type属性将等于MaximumEntropyParser.TokenNodeSpan属性指示解析节点对应的句子部分。此属性为OpenNLP.Tools.Util.Span类型,并具有StartEnd属性,指示解析节点表示的句子部分的字符。

Parse Tree演示应用程序展示了如何遍历此Parse结构并将其映射到Lithium图形控件,生成解析树的图形表示。该工作由MainForm类的ShowParse()方法启动。该方法调用递归的AddChildNodes()方法来构建图形。

parse diagram with the Parse Graph demo

同时,Tools Example使用根Parse对象的内置Show()方法生成解析图的文本表示

parsing with the Tools Example

命名实体识别

“命名实体识别”是OpenNLP库用来指代识别句子中实体类别的术语——例如,人名、地点、日期等等。命名实体识别器最多可以找到七种不同类型的实体,由NameFind子文件夹中的七个最大熵模型文件表示——日期、地点、金钱、组织、百分比、人物和时间。当然,可以使用SharpEntropy库训练新的模型来查找其他类别的实体。由于此算法依赖于训练数据的使用,而且像“person”或“location”这样的类别可能包含非常多的词元,因此它远非万无一失。

首先通过创建OpenNLP.Tools.NameFind.EnglishNameFinder类型的对象来调用命名实体识别函数,然后将包含命名实体识别最大熵模型的NameFind子文件夹的路径传递给它。然后,调用GetNames()方法,传入要查找的实体类型的字符串数组和输入句子。

using OpenNLP.Tools.NameFind;

EnglishNameFinder nameFinder = 
   new EnglishNameFinder(mModelPath + "namefind\\");
string[] models = new string[] {"date", "location", "money", 
    "organization", "percentage", "person", "time"};
string formattedSentence = mameFinder.GetNames(models, sentence);

结果是一个格式化的句子,其中包含类似XML的标签,指示已找到实体的位置。

Name finding with the Tools Example

也可以传递一个Parse对象(由EnglishTreebankParser生成的解析树结构的根)而不是字符串句子。这将把命名实体识别器找到的实体插入到解析结构中。

结论

我移植到C#的OpenNLP库提供了一套工具,可以轻松地执行一些重要的自然语言处理任务。演示应用程序说明了调用库的类并快速获得良好结果的简单性。该库确实依赖于在内存中保存大型最大熵模型数据文件,因此更复杂的NLP任务(完全解析和命名实体识别)是内存密集型的。在内存充足的机器上,性能非常出色:一台3.4 GHz的Pentium IV机器,配备2 GB内存,在12秒内加载了解析数据到内存中。一旦加载,通过将句子数据传递给模型进行查询,几乎可以瞬时获得解析结果。

Java OpenNLP库的开发仍在继续。C#版本现在有一个共指消解工具,并且其开发也在积极进行中,位于CodePlex上的SharpNLP项目。对从磁盘快速检索MaxEnt模型数据的研究(而不是将数据保存在内存中)也在继续。

参考文献

历史

  • 第三版(2006年12月13日。添加了对CodePlex上SharpNLP项目的引用。
  • 第二版(2006年5月4日)。为下载添加了.NET 2.0版本的代码。
  • 初始版本(2005年10月30日)。
© . All rights reserved.