7. 从文本提取信息

对于任何给定的问题,很可能已经有人把答案写在某个地方了。以电子形式提供的自然语言文本的数量真的惊人,并且与日俱增。然而,自然语言的复杂性使访问这些文本中的信息非常困难。NLP目前的技术水平仍然有很长的路要走才能够从不受限制的文本对意义建立通用的表示。如果我们不是集中我们的精力在问题或“实体关系”的有限集合,例如:“不同的设施位于何处”或“谁被什么公司雇用”上,我们就能取得重大进展。本章的目的是要回答下列问题:

  1. 我们如何能构建一个系统,从非结构化文本中提取结构化数据如表格?
  2. 有哪些稳健的方法识别一个文本中描述的实体和关系?
  3. 哪些语料库适合这项工作,我们如何使用它们来训练和评估我们的模型?

一路上,我们将应用前面两章中的技术来解决分块和命名实体识别。

1 信息提取

信息有很多种形状和大小。一个重要的形式是结构化数据:实体和关系的可预测的规范的结构。例如,我们可能对公司和地点之间的关系感兴趣。给定一个公司,我们希望能够确定它做业务的位置;反过来,给定位置,我们会想发现哪些公司在该位置做业务。如果我们的数据是表格形式,如1.1中的例子,那么回答这些问题就很简单了。

表 1.1

位置数据

机构名位置名
Omnicom纽约
DDB Needham纽约
Kaplan Thaler Group纽约
BBDO South亚特兰大
Georgia-Pacific亚特兰大

如果这个位置数据被作为一个元组(entity, relation, entity)的列表存储在Python中,那么这个问题:“哪些组织在亚特兰大经营?”可翻译如下:

>>> locs = [('Omnicom', 'IN', 'New York'),
...         ('DDB Needham', 'IN', 'New York'),
...         ('Kaplan Thaler Group', 'IN', 'New York'),
...         ('BBDO South', 'IN', 'Atlanta'),
...         ('Georgia-Pacific', 'IN', 'Atlanta')]
>>> query = [e1 for (e1, rel, e2) in locs if e2=='Atlanta']
>>> print(query)
['BBDO South', 'Georgia-Pacific']

表 1.2

在亚特兰大运营的公司

机构名
BBDO South
Georgia-Pacific

如果我们尝试从文本中获得相似的信息,事情就比较麻烦了。例如,思考下面的片段(来自nltk.corpus.ieer,fileid为NYT19980315.0085)。

(1)The fourth Wells account moving to another agency is the packaged paper-products division of Georgia-Pacific Corp., which arrived at Wells only last fall. Like Hertz and the History Channel, it is also leaving for an Omnicom-owned agency, the BBDO South unit of BBDO Worldwide. BBDO South in Atlanta, which handles corporate advertising for Georgia-Pacific, will assume additional duties for brands like Angel Soft toilet tissue and Sparkle paper towels, said Ken Haldin, a spokesman for Georgia-Pacific in Atlanta.

如果你通读了(1),你将收集到回答例子问题所需的信息。但我们如何能让一台机器理解(1)来返回1.2中的答案呢?这显然是一个困难得多的任务。1.1不同,(1)不包含连结组织名和位置名的结构。

这个问题的解决方法之一是对意义建立一个非常通用的表示(10.)。在这一章中,我们采取不同的方法,事先确定我们将只查找文本中非常具体的各种信息,如组织和地点之间的关系。不是试图用像(1)那样的文字直接回答这个问题,我们首先将自然语言句子的非结构化数据转换成1.1这样的结构化数据。然后,利用强大的查询工具,如SQL。这种从文本获取意义的方法被称为信息提取

信息提取有许多应用,包括商业智能、简历收获、媒体分析、情感检测、专利检索、电子邮件扫描。当前研究的一个特别重要的领域是提取出电子科学文献的结构化数据,特别是在生物学和医学领域。

1.1 信息提取的架构

1.1显示一个简单的信息提取系统的架构。它开始于使用35.中讨论过的几个程序处理文档:首先,使用句子分割器将该文档的原始文本分割成句,使用分词器将每个句子进一步细分为词。接下来,对每个句子进行词性标注,在下一步命名实体识别中将证明这是非常有益的。在这一步,我们寻找每个句子中提到的有潜在价值的实体。最后,我们使用关系识别搜索文本中不同实体间的可能关系。

../images/ie-architecture.png

图 1.1:信息提取系统的简单的流水线结构。该系统以一个文档的原始文本作为其输入,生成(entity, relation, entity)元组的一个列表作为输出。例如,假设一个文档表明Georgia-Pacific公司位于Atlanta,它可能产生元组([ORG: 'Georgia-Pacific'] 'in' [LOC: 'Atlanta'])

要执行前面三项任务,我们可以定义一个简单的函数,简单地连接NLTK中默认的句子分割器[1],分词器[2]和词性标注器[3]

>>> def ie_preprocess(document):
...    sentences = nltk.sent_tokenize(document) [1]
...    sentences = [nltk.word_tokenize(sent) for sent in sentences] [2]
...    sentences = [nltk.pos_tag(sent) for sent in sentences] [3]

注意

请记住我们的例子程序假设你以import nltk, re, pprint开始交互式会话或程序。

接下来,命名实体识别中,我们分割和标注可能组成一个有趣关系的实体。通常情况下,这些将被定义为名词短语,例如the knights who say "ni"或者适当的名称如Monty Python在一些任务中,同时考虑不明确的名词或名词块也是有用的,如every studentcats,这些不必要一定与确定的NPs和适当名称一样的方式指示实体。

最后,在提取关系时,我们搜索对文本中出现在附近的实体对之间的特殊模式,并使用这些模式建立元组记录实体之间的关系。

2 词块划分

我们将用于实体识别的基本技术是词块划分,它分割和标注多词符的序列,如2.1所示。小框显示词级分词和词性标注,大框显示高级别的词块划分。每个这种较大的框叫做一个词块就像分词忽略空白符,词块划分通常选择词符的一个子集。同样像分词一样,词块划分器生成的片段在源文本中不能重叠。

../images/chunk-segmentation.png

图 2.1:词符和词块级别的分割与标注

在本节中,我们将在较深的层面探讨词块划分,以词块的定义和表示开始。我们将看到正则表达式和N-gram的方法来词块划分,使用CoNLL-2000词块划分语料库开发和评估词块划分器。我们将在(5)6回到命名实体识别和关系抽取的任务。

2.1 名词短语词块划分

我们将首先思考名词短语词块划分NP词块划分任务,在那里我们寻找单独名词短语对应的词块。例如,这里是一些《华尔街日报》文本,其中的NP词块用方括号标记:

(2)[ The/DT market/NN ] for/IN [ system-management/NN software/NN ] for/IN [ Digital/NNP ] [ 's/POS hardware/NN ] is/VBZ fragmented/JJ enough/RB that/IN [ a/DT giant/NN ] such/JJ as/IN [ Computer/NNP Associates/NNPS ] should/MD do/VB well/RB there/RB ./.

正如我们可以看到,NP词块往往是比完整的名词短语稍小的片段。例如,the market for system-management software for Digital's hardware是一个单独的名词短语(包含两个嵌套的名词短语),但是它被更简单的词块the market分割为NP词块。这种差异的动机之一是NP词块被定义为不包含其他的NP词块。因此,修饰一个名词的任何介词短语或从句将不包括在相应的NP词块内,因为它们几乎可以肯定包含更多的名词短语。

NP词块信息最有用的来源之一是词性标记。这是在我们的信息提取系统中进行词性标注的动机之一。我们在2.2中用一个已经标注词性的例句来演示这种方法。为了创建一个NP词块划分器,我们将首先定义一个词块语法,由指示句子应如何进行词块划分的规则组成。在本例中,我们将用一个正则表达式规则定义一个简单的语法[2]这条规则说,每当词块划分器发现一个可选的限定词(DT)后面跟着任何数目的形容词(JJ)然后是一个名词(NN)就应该组成一个NP词块。使用此语法,我们创建了一个词块解析器[3],并在我们的例句上测试[4]结果是一棵树,我们可以打印[5]或图形显示[6]

>>> sentence = [("the", "DT"), ("little", "JJ"), ("yellow", "JJ"), [1]
... ("dog", "NN"), ("barked", "VBD"), ("at", "IN"),  ("the", "DT"), ("cat", "NN")]

>>> grammar = "NP: {<DT>?<JJ>*<NN>}" [2]

>>> cp = nltk.RegexpParser(grammar) [3]
>>> result = cp.parse(sentence) [4]
>>> print(result) [5]
(S
  (NP the/DT little/JJ yellow/JJ dog/NN)
  barked/VBD
  at/IN
  (NP the/DT cat/NN))
>>> result.draw() [6]

例 2.2 (code_chunkex.py)图 2.2:一个简单的基于正则表达式的NP词块划分器的例子。

tree_images/ch07-tree-1.png

2.2 标记模式

组成一个词块语法的规则使用标记模式来描述已标注的词的序列。一个标记模式是一个词性标记序列,用尖括号分隔,如<DT>?<JJ>*<NN>标记模式类似于正则表达式模式(3.4)。现在,思考下面的来自《华尔街日报》的名词短语:

another/DT sharp/JJ dive/NN
trade/NN figures/NNS
any/DT new/JJ policy/NN measures/NNS
earlier/JJR stages/NNS
Panamanian/JJ dictator/NN Manuel/NNP Noriega/NNP

我们可以使用轻微改进的上述第一个标记模式来匹配这些名词短语,如<DT>?<JJ.*>*<NN.*>+这将词块划分任何以一个可选的限定词开头,后面跟零个或多个任何类型的形容词(包括相对形容词,如earlier/JJR),后面跟一个或多个任何类型的名词的标识符序列。然而,很容易找到许多该规则不包括的更复杂的例子:

his/PRP$ Mansion/NNP House/NNP speech/NN
the/DT price/NN cutting/VBG
3/CD %/NN to/TO 4/CD %/NN
more/JJR than/IN 10/CD %/NN
the/DT fastest/JJS developing/VBG trends/NNS
's/POS skill/NN

注意

轮到你来:尝试用标记模式覆盖这些案例。使用图形界面nltk.app.chunkparser()测试它们。使用此工具提供的帮助资料继续完善你的标记模式。

2.3 用正则表达式进行词块划分

要找到一个给定的句子的词块结构,RegexpParser词块划分器以一个没有词符被划分的平面结构开始。词块划分规则轮流应用,依次更新词块结构。一旦所有的规则都被调用,返回生成的词块结构。

2.3显示了一个由2个规则组成的简单的词块语法。第一条规则匹配一个可选的限定词或所有格代名词,零个或多个形容词,然后跟一个名词。第二条规则匹配一个或多个专有名词。我们还定义了一个进行词块划分的例句[1],并在此输入上运行这个词块划分器[2]

grammar = r"""
  NP: {<DT|PP\$>?<JJ>*<NN>}   # chunk determiner/possessive, adjectives and noun
      {<NNP>+}                # chunk sequences of proper nouns
"""
cp = nltk.RegexpParser(grammar)
sentence = [("Rapunzel", "NNP"), ("let", "VBD"), ("down", "RP"), [1]
                 ("her", "PP$"), ("long", "JJ"), ("golden", "JJ"), ("hair", "NN")]
>>> print(cp.parse(sentence)) [2]
(S
  (NP Rapunzel/NNP)
  let/VBD
  down/RP
  (NP her/PP$ long/JJ golden/JJ hair/NN))

例 2.3 (code_chunker1.py)图 2.3:简单的名词短语词块划分器

注意

$符号是正则表达式中的一个特殊字符,必须使用反斜杠转义来匹配PP$标记。

如果标记模式匹配位置重叠,最左边的匹配优先。例如,如果我们应用一个匹配两个连续的名词文本的规则到一个包含三个连续的名词的文本,则只有前两个名词将被划分:

>>> nouns = [("money", "NN"), ("market", "NN"), ("fund", "NN")]
>>> grammar = "NP: {<NN><NN>}  # Chunk two consecutive nouns"
>>> cp = nltk.RegexpParser(grammar)
>>> print(cp.parse(nouns))
(S (NP money/NN market/NN) fund/NN)

一旦我们创建了money market词块,我们就已经消除了允许fund被包含在一个词块中的上下文。这个问题可以避免,使用一种更加宽容的块规则,如NP: {<NN>+}

注意

我们已经为每个块规则添加了一个注释。这些是可选的;当它们的存在时,词块划分器将它作为其跟踪输出的一部分输出这些注释。

2.4 探索文本语料库

2中,我们看到了我们如何在已标注的语料库中提取匹配的特定的词性标记序列的短语。我们可以使用词块划分器更容易的做同样的工作,如下:

>>> cp = nltk.RegexpParser('CHUNK: {<V.*> <TO> <V.*>}')
>>> brown = nltk.corpus.brown
>>> for sent in brown.tagged_sents():
...     tree = cp.parse(sent)
...     for subtree in tree.subtrees():
...         if subtree.label() == 'CHUNK': print(subtree)
...
(CHUNK combined/VBN to/TO achieve/VB)
(CHUNK continue/VB to/TO place/VB)
(CHUNK serve/VB to/TO protect/VB)
(CHUNK wanted/VBD to/TO wait/VB)
(CHUNK allowed/VBN to/TO place/VB)
(CHUNK expected/VBN to/TO become/VB)
...
(CHUNK seems/VBZ to/TO overtake/VB)
(CHUNK want/VB to/TO buy/VB)

注意

轮到你来:将上面的例子封装在函数find_chunks()内,以一个如"CHUNK: {<V.*> <TO> <V.*>}"的词块字符串作为参数。Use it to search the corpus for several other patterns, such as four or more nouns in a row, e.g. "NOUNS: {<N.*>{4,}}"

2.5 词缝加塞

有时定义我们想从一个词块中排除什么比较容易。我们可以定义词缝为一个不包含在词块中的一个词符序列。在下面的例子中,barked/VBD at/IN是一个词缝:

[ the/DT little/JJ yellow/JJ dog/NN ] barked/VBD at/IN [ the/DT cat/NN ]

词缝加塞是从词块中去除一个词符序列的过程。如果匹配的词符序列贯穿一个完整的词块,那么这个完整的词块会被去除;如果词符序列出现在词块中间,这些词符会被去除,在以前只有一个词块的地方留下两个词块。如果序列在词块的两侧,这些词符被去除,一个更小的词块会留下。这三种可能性在2.1中予以说明。

表 2.1

三个词缝加塞规则应用到相同的词块

` `完整的词块词块的中间词块的尾部
输入[a/DT little/JJ dog/NN][a/DT little/JJ dog/NN][a/DT little/JJ dog/NN]
操作Chink "DT JJ NN"Chink "JJ"Chink "NN"
模式}DT JJ NN{}JJ{}NN{
输出a/DT little/JJ dog/NN[a/DT] little/JJ [dog/NN][a/DT little/JJ] dog/NN

2.4中,我们将完整的句子放入到一个单独的词块中,然后切除词缝。

grammar = r"""
  NP:
    {<.*>+}          # Chunk everything
    }<VBD|IN>+{      # Chink sequences of VBD and IN
  """
sentence = [("the", "DT"), ("little", "JJ"), ("yellow", "JJ"),
       ("dog", "NN"), ("barked", "VBD"), ("at", "IN"),  ("the", "DT"), ("cat", "NN")]
cp = nltk.RegexpParser(grammar)
>>> print(cp.parse(sentence))
 (S
   (NP the/DT little/JJ yellow/JJ dog/NN)
   barked/VBD
   at/IN
   (NP the/DT cat/NN))

例 2.4 (code_chinker.py)图 2.4:简单的词缝加塞器

2.6 词块的表示:标记与树

作为标注和分析之间的中间状态(8.,词块结构可以使用标记或树来表示。最广泛的文件表示使用IOB标记在这个方案中,每个词符被三个特殊的词块标记之一标注,I(内部),O(外部)或B(开始)。一个词符被标注为B,如果它标志着一个词块的开始。块内的词符子序列被标注为I所有其他的词符被标注为OBI标记后面跟着词块类型,如B-NP, I-NP当然,没有必要指定出现在词块外的词符类型,所以这些都只标注为O这个方案的例子如2.5所示。

../images/chunk-tagrep.png

图 2.5:词块结构的标记表示形式

IOB标记已成为文件中表示词块结构的标准方式,我们也将使用这种格式。下面是2.5中的信息如何出现在一个文件中的:

We PRP B-NP
saw VBD O
the DT B-NP
yellow JJ I-NP
dog NN I-NP

在此表示中,每个词符一行,和它的词性标记与词块标记一起。这种格式允许我们表示多个词块类型,只要词块不重叠。正如我们前面所看到的,词块的结构也可以使用树表示。这有利于使每个词块作为一个组成部分可以直接操作。一个例子如2.6所示。

../images/chunk-treerep.png

图 2.6:块结构的树表示形式

注意

NLTK使用树作为词块的内部表示,并提供这些树与IOB格式互换的方法。

3 开发和评估词块划分器

现在你对分块的作用有了一些了解,但我们并没有解释如何评估词块划分器。和往常一样,这需要一个合适的已标注语料库。我们一开始寻找将IOB格式转换成NLTK树的机制,然后是使用已化分词块的语料库如何在一个更大的规模上做这个。我们将看到如何为一个词块划分器相对一个语料库的准确性打分,再看看一些数据驱动方式搜索NP词块。我们整个的重点在于扩展一个词块划分器的覆盖范围。

3.1 读取IOB格式与CoNLL2000语料库

使用corpus模块,我们可以加载已经标注并使用IOB符号划分词块的《华尔街日报》文本。这个语料库提供的词块类型有NPVPPP正如我们已经看到的,每个句子使用多行表示,如下所示:

he PRP B-NP
accepted VBD B-VP
the DT B-NP
position NN I-NP
...

转换函数chunk.conllstr2tree()从这些多行字符串建立一个树表示。此外,它允许我们选择使用三个词块类型的任何子集,这里只是NP词块:

>>> text = '''
... he PRP B-NP
... accepted VBD B-VP
... the DT B-NP
... position NN I-NP
... of IN B-PP
... vice NN B-NP
... chairman NN I-NP
... of IN B-PP
... Carlyle NNP B-NP
... Group NNP I-NP
... , , O
... a DT B-NP
... merchant NN I-NP
... banking NN I-NP
... concern NN I-NP
... . . O
... '''
>>> nltk.chunk.conllstr2tree(text, chunk_types=['NP']).draw()
tree_images/ch07-tree-2.png

我们可以使用NLTK的corpus模块访问较大量的已经划分词块的文本。CoNLL2000语料库包含27万词的《华尔街日报文本》,分为“训练”和“测试”两部分,标注有词性标记和IOB格式词块标记。我们可以使用nltk.corpus.conll2000访问这些数据。下面是一个读取语料库的“训练”部分的第100个句子的例子:

>>> from nltk.corpus import conll2000
>>> print(conll2000.chunked_sents('train.txt')[99])
(S
  (PP Over/IN)
  (NP a/DT cup/NN)
  (PP of/IN)
  (NP coffee/NN)
  ,/,
  (NP Mr./NNP Stone/NNP)
  (VP told/VBD)
  (NP his/PRP$ story/NN)
  ./.)

正如你看到的,CoNLL2000语料库包含三种词块类型:NP词块,我们已经看到了;VP词块如has already deliveredPP块如because of因为现在我们唯一感兴趣的是NP词块,我们可以使用chunk_types参数选择它们:

>>> print(conll2000.chunked_sents('train.txt', chunk_types=['NP'])[99])
(S
  Over/IN
  (NP a/DT cup/NN)
  of/IN
  (NP coffee/NN)
  ,/,
  (NP Mr./NNP Stone/NNP)
  told/VBD
  (NP his/PRP$ story/NN)
  ./.)

3.2 简单的评估和基准

现在,我们可以访问一个已划分词块语料,可以评估词块划分器。我们开始为没有什么意义的词块解析器cp建立一个基准,它不划分任何词块:

>>> from nltk.corpus import conll2000
>>> cp = nltk.RegexpParser("")
>>> test_sents = conll2000.chunked_sents('test.txt', chunk_types=['NP'])
>>> print(cp.evaluate(test_sents))
ChunkParse score:
    IOB Accuracy:  43.4%
    Precision:      0.0%
    Recall:         0.0%
    F-Measure:      0.0%

IOB标记准确性表明超过三分之一的词被标注为O,即没有在NP词块中。然而,由于我们的标注器没有找到任何词块,其精度、召回率和F-度量均为零。现在让我们尝试一个初级的正则表达式词块划分器,查找以名词短语标记的特征字母开头的标记(如CD, DTJJ)。

>>> grammar = r"NP: {<[CDJNP].*>+}"
>>> cp = nltk.RegexpParser(grammar)
>>> print(cp.evaluate(test_sents))
ChunkParse score:
    IOB Accuracy:  87.7%
    Precision:     70.6%
    Recall:        67.8%
    F-Measure:     69.2%

正如你看到的,这种方法达到相当好的结果。但是,我们可以采用更多数据驱动的方法改善它,在这里我们使用训练语料找到对每个词性标记最有可能的块标记(I, OB)。换句话说,我们可以使用一元标注器4)建立一个词块划分器。但不是尝试确定每个词的正确的词性标记,而是根据每个词的词性标记,尝试确定正确的词块标记。

3.1中,我们定义了UnigramChunker类,使用一元标注器给句子加词块标记。这个类的大部分代码只是用来在NLTK 的ChunkParserI接口使用的词块树表示和嵌入式标注器使用的IOB表示之间镜像转换。类定义了两个方法:一个构造函数[1],当我们建立一个新的UnigramChunker时调用;以及parse方法[3],用来给新句子划分词块。

class UnigramChunker(nltk.ChunkParserI):
    def __init__(self, train_sents): [1]
        train_data = [[(t,c) for w,t,c in nltk.chunk.tree2conlltags(sent)]
                      for sent in train_sents]
        self.tagger = nltk.UnigramTagger(train_data) [2]

    def parse(self, sentence): [3]
        pos_tags = [pos for (word,pos) in sentence]
        tagged_pos_tags = self.tagger.tag(pos_tags)
        chunktags = [chunktag for (pos, chunktag) in tagged_pos_tags]
        conlltags = [(word, pos, chunktag) for ((word,pos),chunktag)
                     in zip(sentence, chunktags)]
        return nltk.chunk.conlltags2tree(conlltags)

例 3.1 (code_unigram_chunker.py)图 3.1:使用一元标注器划分名词短语词块

构造函数[1]需要训练句子的一个列表,这将是词块树的形式。它首先将训练数据转换成适合训练标注器的形式,使用tree2conlltags映射每个词块树到一个word,tag,chunk三元组的列表。然后使用转换好的训练数据训练一个一元标注器,并存储在self.tagger供以后使用。

parse方法[3]接收一个已标注的句子作为其输入,以从那句话提取词性标记开始。它然后使用在构造函数中训练过的标注器self.tagger,为词性标记标注IOB词块标记。接下来,它提取词块标记,与原句组合,产生conlltags最后,它使用conlltags2tree将结果转换成一个词块树。

现在我们有了UnigramChunker,可以使用CoNLL2000语料库训练它,并测试其表现:

>>> test_sents = conll2000.chunked_sents('test.txt', chunk_types=['NP'])
>>> train_sents = conll2000.chunked_sents('train.txt', chunk_types=['NP'])
>>> unigram_chunker = UnigramChunker(train_sents)
>>> print(unigram_chunker.evaluate(test_sents))
ChunkParse score:
    IOB Accuracy:  92.9%
    Precision:     79.9%
    Recall:        86.8%
    F-Measure:     83.2%

这个分块器相当不错,达到整体F-度量83%的得分。让我们来看一看通过使用一元标注器分配一个标记给每个语料库中出现的词性标记,它学到了什么:

>>> postags = sorted(set(pos for sent in train_sents
...                      for (word,pos) in sent.leaves()))
>>> print(unigram_chunker.tagger.tag(postags))
[('#', 'B-NP'), ('$', 'B-NP'), ("''", 'O'), ('(', 'O'), (')', 'O'),
 (',', 'O'), ('.', 'O'), (':', 'O'), ('CC', 'O'), ('CD', 'I-NP'),
 ('DT', 'B-NP'), ('EX', 'B-NP'), ('FW', 'I-NP'), ('IN', 'O'),
 ('JJ', 'I-NP'), ('JJR', 'B-NP'), ('JJS', 'I-NP'), ('MD', 'O'),
 ('NN', 'I-NP'), ('NNP', 'I-NP'), ('NNPS', 'I-NP'), ('NNS', 'I-NP'),
 ('PDT', 'B-NP'), ('POS', 'B-NP'), ('PRP', 'B-NP'), ('PRP$', 'B-NP'),
 ('RB', 'O'), ('RBR', 'O'), ('RBS', 'B-NP'), ('RP', 'O'), ('SYM', 'O'),
 ('TO', 'O'), ('UH', 'O'), ('VB', 'O'), ('VBD', 'O'), ('VBG', 'O'),
 ('VBN', 'O'), ('VBP', 'O'), ('VBZ', 'O'), ('WDT', 'B-NP'),
 ('WP', 'B-NP'), ('WP$', 'B-NP'), ('WRB', 'O'), ('``', 'O')]

它已经发现大多数标点符号出现在NP词块外,除了两种货币符号#$它也发现限定词(DT)和所有格(PRP$WP$)出现在NP词块的开头,而名词类型(NN, NNP, NNPSNNS)大多出现在NP词块内。

建立了一个一元分块器,很容易建立一个二元分块器:我们只需要改变类的名称为BigramChunker,修改3.1[2]构造一个BigramTagger而不是UnigramTagger由此产生的词块划分器的性能略高于一元词块划分器:

>>> bigram_chunker = BigramChunker(train_sents)
>>> print(bigram_chunker.evaluate(test_sents))
ChunkParse score:
    IOB Accuracy:  93.3%
    Precision:     82.3%
    Recall:        86.8%
    F-Measure:     84.5%

3.3 训练基于分类器的词块划分器

无论是基于正则表达式的词块划分器还是n-gram词块划分器,决定创建什么词块完全基于词性标记。然而,有时词性标记不足以确定一个句子应如何划分词块。例如,考虑下面的两个语句:

(3)

a.Joey/NN sold/VBD the/DT farmer/NN rice/NN ./.

b.Nick/NN broke/VBD my/DT computer/NN monitor/NN ./.

这两句话的词性标记相同,但词块划分方式不同。在第一句中,the farmerrice都是单独的词块,而在第二个句子中相应的部分,the computer monitor,是一个单独的词块。显然,如果我们想最大限度地提升词块划分的准确性,我们需要使用词的内容信息,作为词性标记的补充。

我们包含词的内容信息的方法之一是使用基于分类器的标注器对句子划分词块。如在上一节使用的n-gram词块划分器,这个基于分类器的词块划分器分配IOB标记给句子中的词,然后将这些标记转换为词块。对于基于分类器的标注器本身,我们将使用与我们在1中建立词性标注器相同的方法。

基于分类器的NP词块划分器的基本代码如3.2所示。它包括两个类。第一个类[1]几乎与1.5ConsecutivePosTagger类相同。仅有的两个区别是它调用一个不同的特征提取器[2],和它使用MaxentClassifier而不是NaiveBayesClassifier[3]第二个类[4]基本上是标注器类的一个包装器,将它变成一个词块划分器。训练期间,这第二个类映射训练语料中的词块树到标记序列;在parse() 方法中,它将标注器提供的标记序列转换回一个词块树。

class ConsecutiveNPChunkTagger(nltk.TaggerI): [1]

    def __init__(self, train_sents):
        train_set = []
        for tagged_sent in train_sents:
            untagged_sent = nltk.tag.untag(tagged_sent)
            history = []
            for i, (word, tag) in enumerate(tagged_sent):
                featureset = npchunk_features(untagged_sent, i, history) [2]
                train_set.append( (featureset, tag) )
                history.append(tag)
        self.classifier = nltk.MaxentClassifier.train( [3]
            train_set, algorithm='megam', trace=0)

    def tag(self, sentence):
        history = []
        for i, word in enumerate(sentence):
            featureset = npchunk_features(sentence, i, history)
            tag = self.classifier.classify(featureset)
            history.append(tag)
        return zip(sentence, history)

class ConsecutiveNPChunker(nltk.ChunkParserI): [4]
    def __init__(self, train_sents):
        tagged_sents = [[((w,t),c) for (w,t,c) in
                         nltk.chunk.tree2conlltags(sent)]
                        for sent in train_sents]
        self.tagger = ConsecutiveNPChunkTagger(tagged_sents)

    def parse(self, sentence):
        tagged_sents = self.tagger.tag(sentence)
        conlltags = [(w,t,c) for ((w,t),c) in tagged_sents]
        return nltk.chunk.conlltags2tree(conlltags)

例 3.2 (code_classifier_chunker.py)图 3.2:使用连续分类器划分名词短语词块

留下来唯一需要填写的是特征提取器。首先,我们定义一个简单的特征提取器,它只是提供了当前词符的词性标记。使用此特征提取器,我们的基于分类器的词块划分器的表现与一元词块划分器非常类似:

>>> def npchunk_features(sentence, i, history):
...     word, pos = sentence[i]
...     return {"pos": pos}
>>> chunker = ConsecutiveNPChunker(train_sents)
>>> print(chunker.evaluate(test_sents))
ChunkParse score:
    IOB Accuracy:  92.9%
    Precision:     79.9%
    Recall:        86.7%
    F-Measure:     83.2%

我们还可以添加一个特征表示前面词的词性标记。添加此特征允许词块划分器模拟相邻标记之间的相互作用,由此产生的词块划分器与二元词块划分器非常接近。

>>> def npchunk_features(sentence, i, history):
...     word, pos = sentence[i]
...     if i == 0:
...         prevword, prevpos = "<START>", "<START>"
...     else:
...         prevword, prevpos = sentence[i-1]
...     return {"pos": pos, "prevpos": prevpos}
>>> chunker = ConsecutiveNPChunker(train_sents)
>>> print(chunker.evaluate(test_sents))
ChunkParse score:
    IOB Accuracy:  93.6%
    Precision:     81.9%
    Recall:        87.2%
    F-Measure:     84.5%

下一步,我们将尝试为当前词增加特征,因为我们假设这个词的内容应该对词块划有用。我们发现这个特征确实提高了词块划分器的表现,大约1.5个百分点(相应的错误率减少大约10%)。

>>> def npchunk_features(sentence, i, history):
...     word, pos = sentence[i]
...     if i == 0:
...         prevword, prevpos = "<START>", "<START>"
...     else:
...         prevword, prevpos = sentence[i-1]
...     return {"pos": pos, "word": word, "prevpos": prevpos}
>>> chunker = ConsecutiveNPChunker(train_sents)
>>> print(chunker.evaluate(test_sents))
ChunkParse score:
    IOB Accuracy:  94.5%
    Precision:     84.2%
    Recall:        89.4%
    F-Measure:     86.7%

最后,我们尝试用多种附加特征扩展特征提取器,例如预取特征[1]、配对特征[2]和复杂的语境特征[3]这最后一个特征,称为tags-since-dt,创建一个字符串,描述自最近的限定词以来遇到的所有词性标记,或如果没有限定词则在索引i之前自语句开始以来遇到的所有词性标记。

>>> def npchunk_features(sentence, i, history):
...     word, pos = sentence[i]
...     if i == 0:
...         prevword, prevpos = "<START>", "<START>"
...     else:
...         prevword, prevpos = sentence[i-1]
...     if i == len(sentence)-1:
...         nextword, nextpos = "<END>", "<END>"
...     else:
...         nextword, nextpos = sentence[i+1]
...     return {"pos": pos,
...             "word": word,
...             "prevpos": prevpos,
...             "nextpos": nextpos, [1]
...             "prevpos+pos": "%s+%s" % (prevpos, pos),  [2]
...             "pos+nextpos": "%s+%s" % (pos, nextpos),
...             "tags-since-dt": tags_since_dt(sentence, i)}  [3]
>>> def tags_since_dt(sentence, i):
...     tags = set()
...     for word, pos in sentence[:i]:
...         if pos == 'DT':
...             tags = set()
...         else:
...             tags.add(pos)
...     return '+'.join(sorted(tags))
>>> chunker = ConsecutiveNPChunker(train_sents)
>>> print(chunker.evaluate(test_sents))
ChunkParse score:
    IOB Accuracy:  96.0%
    Precision:     88.6%
    Recall:        91.0%
    F-Measure:     89.8%

注意

轮到你来:尝试为特征提取器函数npchunk_features增加不同的特征,看看是否可以进一步改善NP词块划分器的表现。

4 语言结构中的递归

4.1 用级联词块划分器构建嵌套结构

到目前为止,我们的词块结构一直是相对平的。已标注词符组成的树在如NP这样的词块节点下任意组合。然而,只需创建一个包含递归规则的多级的词块语法,就可以建立任意深度的词块结构。4.1是名词短语、介词短语、动词短语和句子的模式。这是一个四级词块语法器,可以用来创建深度最多为4的结构。

grammar = r"""
  NP: {<DT|JJ|NN.*>+}          # Chunk sequences of DT, JJ, NN
  PP: {<IN><NP>}               # Chunk prepositions followed by NP
  VP: {<VB.*><NP|PP|CLAUSE>+$} # Chunk verbs and their arguments
  CLAUSE: {<NP><VP>}           # Chunk NP, VP
  """
cp = nltk.RegexpParser(grammar)
sentence = [("Mary", "NN"), ("saw", "VBD"), ("the", "DT"), ("cat", "NN"),
    ("sit", "VB"), ("on", "IN"), ("the", "DT"), ("mat", "NN")]
>>> print(cp.parse(sentence))
(S
  (NP Mary/NN)
  saw/VBD
  (CLAUSE
    (NP the/DT cat/NN)
    (VP sit/VB (PP on/IN (NP the/DT mat/NN)))))

例 4.1 (code_cascaded_chunker.py)图 4.1:一个词块划分器,处理NP,PP,VP 和S

不幸的是,这一结果丢掉了saw为首的VP它还有其他缺陷。当我们将此词块划分器应用到一个有更深嵌套的句子时,让我们看看会发生什么。请注意,它无法识别[1]开始的VP词块。

>>> sentence = [("John", "NNP"), ("thinks", "VBZ"), ("Mary", "NN"),
...     ("saw", "VBD"), ("the", "DT"), ("cat", "NN"), ("sit", "VB"),
...     ("on", "IN"), ("the", "DT"), ("mat", "NN")]
>>> print(cp.parse(sentence))
(S
  (NP John/NNP)
  thinks/VBZ
  (NP Mary/NN)
  saw/VBD # [_saw-vbd]
  (CLAUSE
    (NP the/DT cat/NN)
    (VP sit/VB (PP on/IN (NP the/DT mat/NN)))))

这些问题的解决方案是让词块划分器在它的模式中循环:尝试完所有模式之后,重复此过程。我们添加一个可选的第二个参数loop指定这套模式应该循环的次数:

>>> cp = nltk.RegexpParser(grammar, loop=2)
>>> print(cp.parse(sentence))
(S
  (NP John/NNP)
  thinks/VBZ
  (CLAUSE
    (NP Mary/NN)
    (VP
      saw/VBD
      (CLAUSE
        (NP the/DT cat/NN)
        (VP sit/VB (PP on/IN (NP the/DT mat/NN)))))))

注意

这个级联过程使我们能创建深层结构。然而,创建和调试级联过程是困难的,关键点是它能更有效地做全面的分析(见第8.章)。另外,级联过程只能产生固定深度的树(不超过级联级数),完整的句法分析这是不够的。

4.2 Trees

tree是一组连接的加标签节点,从一个特殊的根节点沿一条唯一的路径到达每个节点。下面是一棵树的例子(注意它们标准的画法是颠倒的):

(4)tree_images/ch07-tree-3.png

我们用“家庭”来比喻树中节点的关系:例如,SVP父母;反之VPS 的一个孩子。此外,由于NPVP同为S的两个孩子,它们也是兄弟为方便起见,也有特定树的文本格式:

(S
   (NP Alice)
   (VP
      (V chased)
      (NP
         (Det the)
         (N rabbit))))

虽然我们将只集中关注语法树,树可以用来编码任何同构的超越语言形式序列的层次结构(如形态结构、篇章结构)。一般情况下,叶子和节点值不一定要是字符串。

在NLTK中,我们通过给一个节点添加标签和一系列的孩子创建一棵树:

>>> tree1 = nltk.Tree('NP', ['Alice'])
>>> print(tree1)
(NP Alice)
>>> tree2 = nltk.Tree('NP', ['the', 'rabbit'])
>>> print(tree2)
(NP the rabbit)

我们可以将这些不断合并成更大的树,如下所示:

>>> tree3 = nltk.Tree('VP', ['chased', tree2])
>>> tree4 = nltk.Tree('S', [tree1, tree3])
>>> print(tree4)
(S (NP Alice) (VP chased (NP the rabbit)))

下面是树对象的一些的方法:

>>> print(tree4[1])
(VP chased (NP the rabbit))
>>> tree4[1].label()
'VP'
>>> tree4.leaves()
['Alice', 'chased', 'the', 'rabbit']
>>> tree4[1][1][1]
'rabbit'

复杂的树用括号表示难以阅读。在这些情况下,draw方法是非常有用的。它会打开一个新窗口,包含树的一个图形表示。树显示窗口可以放大和缩小,子树可以折叠和展开,并将图形表示输出为一个postscript文件(包含在一个文档中)。

>>> tree3.draw()                           
../images/parse_draw.png

4.3 树遍历

使用递归函数来遍历树是标准的做法。4.2中的内容进行了演示。

def traverse(t):
    try:
        t.label()
    except AttributeError:
        print(t, end=" ")
    else:
        # Now we know that t.node is defined
        print('(', t.label(), end=" ")
        for child in t:
            traverse(child)
        print(')', end=" ")

 >>> t = nltk.Tree('(S (NP Alice) (VP chased (NP the rabbit)))')
 >>> traverse(t)
 ( S ( NP Alice ) ( VP chased ( NP the rabbit ) ) )

例 4.2 (code_traverse.py)图 4.2:递归函数遍历树

注意

我们已经使用了一种叫做动态类型的技术,检测t是一棵树(如定义了t.label())。

5 命名实体识别

在本章开头,我们简要介绍了命名实体(NE)。命名实体是确切的名词短语,指示特定类型的个体,如组织、人、日期等。5.1列出了一些较常用的NE类型。这些应该是不言自明的,除了“FACILITY”:建筑和土木工程领域的人造产品;以及“GPE”:地缘政治实体,如城市、州/省、国家。

表 5.1

常用命名实体类型

NE类型示例
组织Georgia-Pacific Corp., WHO
Eddy Bonte, President Obama
地点Murray River, Mount Everest
日期June, 2008-06-29
时间two fifty a m, 1:30 p.m.
货币175 million Canadian Dollars, GBP 10.40
百分数twenty pct, 18.75 %
设施Washington Monument, Stonehenge
地缘政治实体South East Asia, Midlothian

命名实体识别(NER)系统的目标是识别文字提及的所有命名实体。可以分解成两个子任务:确定NE的边界和确定其类型。命名实体识别经常是信息提取中关系识别的前奏,它也有助于其他任务。例如,在问答系统(QA)中,我们试图提高信息检索的精确度,不是返回整个页面而只是包含用户问题的答案的那些部分。大多数QA系统利用标准信息检索返回的文件,然后尝试分离文档中包含答案的最小的文本片段。现在假设问题是Who was the first President of the US?,被检索的一个文档中包含下面这段话:

(5)The Washington Monument is the most prominent structure in Washington, D.C. and one of the city's early attractions. It was built in honor of George Washington, who led the country to independence and then became its first President.

分析问题时我们想到答案应该是X was the first President of the US的形式,其中X不仅是一个名词短语,也是一个PERSON类型的命名实体。这应该使我们忽略段落中的第一句话。虽然它包含Washington的两个出现,命名实体识别应该告诉我们它们都不是正确的类型。

我们如何识别命名实体呢?一个办法是查找一个适当的名称列表。例如,识别地点时,我们可以使用地名辞典,或地理词典,如亚历山大地名辞典或盖蒂地名辞典。然而,盲目这样做会出问题,如5.1所示。

../images/locations.png

图 5.1:地点检测,通过在新闻故事中简单的查找:查找地名辞典中的每个词是容易出错的;大小写区分可能有所帮助,但它们不是总会有的。

可以观察到,地名辞典很好的覆盖了很多国家的地点,却错误地认为Sanchez在多米尼加共和国而On在越南。当然,我们可以从地名辞典中忽略这些地名,但这样一来当它们出现在一个文档中时,我们将无法识别它们。

人或组织的名称的情况更加困难。任何这些名称的列表都肯定覆盖不全。每天都有新的组织出现,如果我们正在努力处理当代文本或博客条目,使用名称辞典查找来识别众多实体是不可能的。

困难的另一个原因是许多命名实体措辞有歧义。MayNorth可能分别是日期和地点类型的命名实体的,但也可以都是人名;相反的,Christian Dior看上去像是一个人名,但更可能是组织类型。Yankee在某些上下文中是普通的修饰语,但在短语Yankee infielders中会被标注为组织类型的一个实体。

更大的挑战来自如Stanford University这样的多词名称和包含其他名称的名称,如Cecil H. Green LibraryEscondido Village Conference Service Center因此,在命名实体识别中,我们需要能够识别多词符序列的开头和结尾。

命名实体识别是一个非常适合用基于分类器类型的方法来处理的任务,这些方法我们在名词短语词块划分时看到过。特别是,我们可以建立一个标注器,为使用IOB格式的每个词块都加了适当类型标签的句子中的每个词加标签。这里是CONLL 2002(conll2002)荷兰语训练数据的一部分:

Eddy N B-PER
Bonte N I-PER
is V O
woordvoerder N O
van Prep O
diezelfde Pron O
Hogeschool N B-ORG
. Punc O

在上面的表示中,每个词符一行,与它的词性标记及命名实体标记一起。基于这个训练语料,我们可以构造一个可以用来标注新句子的标注器;使用nltk.chunk.conlltags2tree()函数将标记序列转换成一个词块树。

NLTK提供了一个已经训练好的可以识别命名实体的分类器,使用函数nltk.ne_chunk()访问。如果我们设置参数binary=True[1],那么命名实体只被标注为NE;否则,分类器会添加类型标签,如PERSON, ORGANIZATION和 GPE。

>>> sent = nltk.corpus.treebank.tagged_sents()[22]
>>> print(nltk.ne_chunk(sent, binary=True)) [1]
(S
  The/DT
  (NE U.S./NNP)
  is/VBZ
  one/CD
  ...
  according/VBG
  to/TO
  (NE Brooke/NNP T./NNP Mossman/NNP)
  ...)
>>> print(nltk.ne_chunk(sent)) 
(S
  The/DT
  (GPE U.S./NNP)
  is/VBZ
  one/CD
  ...
  according/VBG
  to/TO
  (PERSON Brooke/NNP T./NNP Mossman/NNP)
  ...)

6 关系抽取

一旦文本中的命名实体已被识别,我们就可以提取它们之间存在的关系。如前所述,我们通常会寻找指定类型的命名实体之间的关系。进行这一任务的方法之一是首先寻找所有X, α, Y)形式的三元组,其中XY是指定类型的命名实体,α表示XY之间关系的字符串。然后我们可以使用正则表达式从α的实体中抽出我们正在查找的关系。下面的例子搜索包含词in的字符串。特殊的正则表达式(?!\b.+ing\b)是一个否定预测先行断言,允许我们忽略如success in supervising the transition of中的字符串,其中in后面跟一个动名词。

>>> IN = re.compile(r'.*\bin\b(?!\b.+ing)')
>>> for doc in nltk.corpus.ieer.parsed_docs('NYT_19980315'):
...     for rel in nltk.sem.extract_rels('ORG', 'LOC', doc,
...                                      corpus='ieer', pattern = IN):
...         print(nltk.sem.rtuple(rel))
[ORG: 'WHYY'] 'in' [LOC: 'Philadelphia']
[ORG: 'McGlashan &AMP; Sarrail'] 'firm in' [LOC: 'San Mateo']
[ORG: 'Freedom Forum'] 'in' [LOC: 'Arlington']
[ORG: 'Brookings Institution'] ', the research group in' [LOC: 'Washington']
[ORG: 'Idealab'] ', a self-described business incubator based in' [LOC: 'Los Angeles']
[ORG: 'Open Text'] ', based in' [LOC: 'Waterloo']
[ORG: 'WGBH'] 'in' [LOC: 'Boston']
[ORG: 'Bastille Opera'] 'in' [LOC: 'Paris']
[ORG: 'Omnicom'] 'in' [LOC: 'New York']
[ORG: 'DDB Needham'] 'in' [LOC: 'New York']
[ORG: 'Kaplan Thaler Group'] 'in' [LOC: 'New York']
[ORG: 'BBDO South'] 'in' [LOC: 'Atlanta']
[ORG: 'Georgia-Pacific'] 'in' [LOC: 'Atlanta']

搜索关键字in执行的相当不错,虽然它的检索结果也会误报,例如[ORG: House Transportation Committee] , secured the most money in the [LOC: New York];一种简单的基于字符串的方法排除这样的填充字符串似乎不太可能。

如前文所示,conll2002命名实体语料库的荷兰语部分不只包含命名实体标注,也包含词性标注。这允许我们设计对这些标记敏感的模式,如下面的例子所示。clause()方法以分条形式输出关系,其中二元关系符号作为参数relsym的值被指定[1]

>>> from nltk.corpus import conll2002
>>> vnv = """
... (
... is/V|    # 3rd sing present and
... was/V|   # past forms of the verb zijn ('be')
... werd/V|  # and also present
... wordt/V  # past of worden ('become)
... )
... .*       # followed by anything
... van/Prep # followed by van ('of')
... """
>>> VAN = re.compile(vnv, re.VERBOSE)
>>> for doc in conll2002.chunked_sents('ned.train'):
...     for r in nltk.sem.extract_rels('PER', 'ORG', doc,
...                                    corpus='conll2002', pattern=VAN):
...         print(nltk.sem.clause(r, relsym="VAN")) [1]
VAN("cornet_d'elzius", 'buitenlandse_handel')
VAN('johan_rottiers', 'kardinaal_van_roey_instituut')
VAN('annie_lennox', 'eurythmics')

注意

轮到你来:替换最后一行[1]print(rtuple(rel, lcon=True, rcon=True))这将显示实际的词表示两个NE之间关系以及它们左右的默认10个词的窗口的上下文。在一本荷兰语词典的帮助下,你也许能够找出为什么结果VAN('annie_lennox', 'eurythmics')是个误报。

7 小结

8 深入阅读

本章的附加材料发布在http://nltk.org/,包括网络上免费提供的资源的链接。关于使用NLTK词块划分的更多的例子,请看在http://nltk.org/howto上的词块划分HOWTO。

分块的普及很大一部分是由于Abney的开创性的工作,如(Church, Young, & Bloothooft, 1996)http://www.vinartus.net/spa/97a.pdf中描述了Abney的Cass词块划分器器。

根据Ross和Tukey在1975年的论文(Church, Young, & Bloothooft, 1996),单词词缝最初的意思是一个停用词序列。

IOB格式(有时也称为BIO格式)由(Ramshaw & Marcus, 1995)开发用来NP划分词块,并被由Conference on Natural Language Learning在1999年用于NP加括号共享任务。CoNLL 2000采用相同的格式标注了华尔街日报的文本作为一个NP词块划分共享任务的一部分。

(Jurafsky & Martin, 2008)的13.5节包含有关词块划分的一个讨论。第22 章讲述信息提取,包括命名实体识别。有关生物学和医学中的文本挖掘的信息,请参阅(Ananiadou & McNaught, 2006)

9 练习

  1. ☼ IOB 格式分类标注标识符为IOB三个标签为什么是必要的?如果我们只使用IO标记会造成什么问题?
  2. ☼ 写一个标记模式匹配包含复数中心名词在内的名词短语,如"many/JJ researchers/NNS", "two/CD weeks/NNS", "both/DT new/JJ positions/NNS"。通过泛化处理单数名词短语的标记模式,尝试做这个。
  3. ☼ 选择CoNLL语料库中三种词块类型之一。研究CoNLL语料库,并尝试观察组成这种类型词块的词性标记序列的任何模式。使用正则表达式词块划分器nltk.RegexpParser开发一个简单的词块划分器。讨论任何难以可靠划分词块的标记序列。
  4. 词块的早期定义是出现在词缝之间的内容。开发一个词块划分器以将完整的句子作为一个单独的词块开始,然后其余的工作完全加塞词缝完成。在你自己的应用程序的帮助下,确定哪些标记(或标记序列)最有可能组成词缝。相对于完全基于词块规则的词块划分器,比较这种方法的表现和易用性。
  5. ◑ 写一个标记模式,涵盖包含动名词在内的名词短语,如"the/DT receiving/VBG end/NN", "assistant/NN managing/VBG editor/NN"。将这些模式加入到语法,每行一个。用自己设计的一些已标注的句子,测试你的工作。
  6. ◑ 写一个或多个标记模式处理有连接词的名词短语,如"July/NNP and/CC August/NNP", "all/DT your/PRP$ managers/NNS and/CC supervisors/NNS", "company/NN courts/NNS and/CC adjudicators/NNS"。
  7. ◑ 用任何你之前已经开发的词块划分器执行下列评估任务。(请注意,大多数词块划分语料库包含一些内部的不一致,以至于任何合理的基于规则的方法都将产生错误。)
    1. 在来自词块划分语料库的100个句子上评估你的词块划分器,报告精度、召回率和F-量度。
    2. 使用chunkscore.missed()chunkscore.incorrect()方法识别你的词块划分器的错误。讨论。
    3. 与本章的评估部分讨论的基准词块划分器比较你的词块划分器的表现。
  8. ◑ 使用基于正则表达式的词块语法RegexpChunk,为CoNLL语料库中词块类型中的一个开发一个词块划分器。使用词块、词缝、合并或拆分规则的任意组合。
  9. ◑ 有时一个词的标注不正确,例如"12/CD or/CC so/RB cases/VBZ"中的中心名词。不用要求手工校正标注器的输出,好的词块划分器使用标注器的错误输出也能运作。查找使用不正确的标记正确为名词短语划分词块的其他例子。
  10. ◑ 二元词块划分器的准确性得分约为90%。研究它的错误,并试图找出它为什么不能获得100%的准确率。实验三元词块划分。你能够再提高准确性吗?
  11. ★ 在IOB词块标注上应用n-gram和Brill标注方法。不是给词分配词性标记,在这里我们给词性标记分配IOB标记。例如如果标记DT(限定符)经常出现在一个词块的开头,它会被标注为B(开始)。相对于本章中讲到的正则表达式词块划分方法,评估这些词块划分方法的表现。
  12. ★ 在5.中我们看到,通过查找有歧义的n-grams可以得到标注准确性的上限,即在训练数据中有多种可能的方式标注的n-grams。应用同样的方法来确定一个n-gram词块划分器的上限。
  13. ★ 挑选CoNLL语料库中三种词块类型之一。编写函数为你选择的类型做以下任务:
    1. 列出与此词块类型的每个实例一起出现的所有标记序列。
    2. 计数每个标记序列的频率,并产生一个按频率减少的顺序排列的列表;每行要包含一个整数(频率)和一个标记序列。
    3. 检查高频标记序列。使用这些作为开发一个更好的词块划分器的基础。
  14. ★ 在评估一节中提到的基准词块划分器往往会产生比它应该产生的块更大的词块。例如,短语[every/DT time/NN] [she/PRP] sees/VBZ [a/DT newspaper/NN]包含两个连续的词块,我们的基准词块划分器不正确地将前两个结合: [every/DT time/NN she/PRP]写一个程序,找出这些通常出现在一个词块的开头的词块内部的标记有哪些,然后设计一个或多个规则分裂这些词块。将这些与现有的基准词块划分器组合,重新评估它,看看你是否已经发现了一个改进的基准。
  15. ★ 开发一个NP词块划分器,转换POS标注文本为元组的一个列表,其中每个元组由一个后面跟一个名词短语和介词的动词组成,如the little cat sat on the mat变成('sat', 'on', 'NP')...
  16. ★ 宾州树库样例包含一部分已标注的《华尔街日报》文本,已经按名词短语划分词块。其格式使用方括号,我们已经在本章遇到它了几次。该语料可以使用for sent in nltk.corpus.treebank_chunk.chunked_sents(fileid)来访问。这些都是平坦的树,正如我们使用nltk.corpus.conll2000.chunked_sents()得到的一样。
    1. 函数nltk.tree.pprint()nltk.chunk.tree2conllstr()可以用来从一棵树创建树库和IOB字符串。编写函数chunk2brackets()chunk2iob(),以一个单独的词块树为它们唯一的参数,返回所需的多行字符串表示。
    2. 写命令行转换工具bracket2iob.pyiob2bracket.py,(分别)读取树库或CoNLL格式的一个文件,将它转换为其他格式。(从NLTK语料库获得一些原始的树库或CoNLL 数据,保存到一个文件,然后使用for line in open(filename)从Python访问它。)
  17. ★ 一个n-gram词块划分器可以使用除当前词性标记和n-1个前面的词块的标记以外其他信息。调查其他的上下文模型,如n-1个前面的词性标记,或一个写前面词块标记连同前面和后面的词性标记的组合。
  18. ★ 思考一个n-gram标注器使用临近的标记的方式。现在观察一个词块划分器可能如何重新使用这个序列信息。例如:这两个任务将使用名词往往跟在形容词后面(英文中)的信息。这会出现相同的信息被保存在两个地方的情况。随着规则集规模增长,这会成为一个问题吗?如果是,推测可能会解决这个问题的任何方式。

关于本文档...

针对NLTK 3.0 作出更新。本章来自于Natural Language Processing with PythonSteven Bird, Ewan KleinEdward Loper,Copyright © 2014 作者所有。本章依据Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 United States License [http://creativecommons.org/licenses/by-nc-nd/3.0/us/] 条款,与自然语言工具包 [http://nltk.org/] 3.0 版一起发行。

本文档构建于星期三 2015 年 7 月 1 日 12:30:05 AEST