本章回答下列问题:

(1)如何能构建一个系统,以至从非结构化文本中提取结构化数据?

(2)有哪些稳健的方法识别一个文本描述的实体和关系?

(3)哪些语料库适合这项工作,如何使用它们来训练和评估模型?

使用最后两章技术解决分块和命名实体识别的问题

7.1 信息提取

信息有很多种’形状’和’大小’,一个重要的形式是结构化数据:实体和关系的规范和可预测的组织。例如:我们可能对公司和地点间的关系感兴趣.
给定公司,希望能确定其做业务的位置,反过来,给定位置,想发现那些公司在该位置做业务.
如果数据是表格形式,可以很明确的回答这些问题

表 7-1 位置数据
机构名 位置名
Omnicom New York
DDB Needham New York
Kaplan Thaler Group New York
BBDO South Atlanta
Georgia-Pacific Atlanta

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

1
2
3
4
5
6
7
8
9
	The fourth <b_enamex type="ORGANIZATION">Wells<e_enamex> account moving to another agency is the
packaged paper-products division of <b_enamex type="ORGANIZATION">Georgia-Pacific Corp.<e_enamex>, which
arrived at <b_enamex type="ORGANIZATION">Wells<e_enamex> only last <b_timex type="DATE">fall<e_timex>. Like <b_enamex type="ORGANIZATION">Hertz<e_enamex> and the <b_enamex type="ORGANIZATION">History
Channel<e_enamex>, it is also leaving for an <b_enamex type="ORGANIZATION">Omnicom<e_enamex>-owned agency, the <b_enamex type="ORGANIZATION">BBDO
South<e_enamex> unit of <b_enamex type="ORGANIZATION">BBDO Worldwide<e_enamex>.
<b_enamex type="ORGANIZATION">BBDO South<e_enamex> in <b_enamex type="LOCATION">Atlanta<e_enamex>, which handles corporate advertising for
<b_enamex type="ORGANIZATION">Georgia-Pacific<e_enamex>, will assume additional duties for brands like
Angel Soft toilet tissue and Sparkle paper towels, said <b_enamex type="PERSON">Ken Haldin<e_enamex>,
a spokesman for <b_enamex type="ORGANIZATION">Georgia-Pacific<e_enamex> in <b_enamex type="LOCATION">Atlanta<e_enamex>.

与表7-1不同,片段中不包含连接组织名和位置名的结构,如何让一台机器理解片段然后将链表['BBDO South','Georgia-Pacific']作为答案返回呢?
这是一个困难得多的任务,解决方法之一是建立一个非常一般的含义(ref 10 chapter),
本章的解决方法是提前定为只查找文本中非常具体的各种信息(如组织和地点的关系),
先将自然语言句子这样的非结构数据转换成表7-1的结构化数据,然后利用强大的查询工具(如SQL)
这种从文本获取意义的方法被称为信息提取

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

信息提取结构

图 7-1 显示了一个简单的信息提取系统的结构,

步骤:

  1. 使用句子分割器将文档的原始内容文本分割成句
  2. 使用分词器将每个句子进一步细分为词
  3. 对每个句子进行词性标注
  4. 命名实体识别,寻找每个句子中提到的潜在的实体
  5. 使用关系识别搜索文本中不同实体间的可能关系

7-1

图7-1 用于信息提取系统的简单流水线架构.这个系统需要将文档的原始文本作为输入,将生成`(entity, relation, entity)`的元组列表作为输入. 例如,给定文档假设 Georgia-Pacific 公司位于 Atlanta,它可能会产生元组([ORG: 'Georgia-Pacific'] 'in' [LOC: 'Atlanta']).

执行前面3个任务

1
2
3
4
5
import nltk, re, pprint
def ie_preprocess(document):
sentences = nltk.sent_tokenize(document) #句子分割器
sentences = [nltk.word_tokenize(sent) for sent in sentences] #分词器
sentences = [nltk.pos_tag(sent) for sent in sentence] #词性标注器

7.2 分块

用于实体识别的基本技术是分块(chunking),分割和标注如图7-2所示的多标识符序列.

小框显示词级标识符和词性标注,大框显示较高级的程序分块.较大的框叫做组块(chunk).

就像分词忽略空白符,程序分块通常选择标识符的一个子集.

同分词一样,分块构成的源文本中的片段不能重叠

7-2

图7-2 词标识符和块级别的分割与标注

本节将在较深的层面上探讨程序分块,以组块的定义和表示开始.我们将看到正则表达式和n-gram方法分块,使用CoNLL-2000分块语料库开发和评估分块器。

名词短语分块

NP-chunking(名词短语分块),寻找单独名词短语对应的块

下面是一些《华尔街日报》的文本,其中的NP-分块用方括号标记.

[ 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 是一个单独的名词短语(含2个嵌套的名词短语),
它里面有一个简单的NP-分块the market.
这种差异产生的动机是:NP-分块被定义为不包含其它的NP-分块.
因此,修饰一个名词的任何一个介词短语或从句将不包括在相应的NP-分块内
(因为它们几乎可以肯定包含更多的名词短语).

NP-分块信息最有用的来源之一是词性标记。这是在信息提取系统中进行词性标注的动机之一。

为了创建NP-分块,首先定义分块语法,规定句子应如何分块。在本例中,使用一个正则表达式规则定义一个简单的语法。

这条规则是NP-分块由可选的且后面跟着任意数目形容词(JJ)的限定词和名词(NN)组成。使用此语法创建组块分析器,测试例句。结果得到树状图,可以输出或显示图形。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import nltk

# 已标注词性的例句
sentence = [("the", "DT"), ("little", "JJ"), ("yellow", "JJ"), ("dog", "NN"), ("barked", "VBD"), ("at", "IN"),
("the", "DT"), ("cat", "NN")]
# 正则表达式规则定义NP-分块语法:由可选的且后面跟着任意数目形容词(JJ)的限定词和名词(NN)组成
grammar = "NP: {<DT>?<JJ>*<NN>}"
cp = nltk.RegexpParser(grammar)
result = cp.parse(sentence)
print(result)

result.draw()
"""
(S
(NP the/DT little/JJ yellow/JJ dog/NN)
barked/VBD
at/IN
(NP the/DT cat/NN))
"""

nlp_eg7-1

标记模式

组成块语法的规则利用标记模式描述已标注的词的序列.
标记模式是用尖括号分隔的词性标记序列,如NP: {<DT>?<JJ>*<NN>}.
标记模式类似正则表达式模式(ref 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

略微改进之前的标记模式,以匹配这些名词短语.如NP: {<DT>?<JJ.*>*<NN.*>+}

然而,还存在许多该规则不包括的、更复杂的例子.

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()测试覆盖这些案例的标记模式.
使用此工具提供的帮助资料完善标记模式

用正则表达式分块

为了找到给定句子的分块结构, RegexpParser 分块器
以一个平面结构开始,其中的标识符都未被分块.

轮流应用分块规则,依次更新块结构.所有的规则都被调用后,返回快结构.

1
2
3
4
5
6
7
8
grammar = r"""
NP: {<DT|PP\$>?<JJ>*<NN>} #匹配一个可选的限定词或所有格代名词
{<NNP>+} #匹配一个或多个专有名词
"""
cp = nltk.RegexpParser(grammar)
sentence = [("Rapunzel", "NNP"), ("let", "VBD"), ("down", "RP"), ("her", "PP$"), ("long", "JJ"), ("golden", "JJ"),
("hair", "NN")]
print(cp.parse(sentence))
(S
    (NP Rapunzel / NNP)
    let / VBD
    down / RP
    (NP her / PP$ long / JJ golden / JJ hair / NN))

如果将匹配两个连续名词的文本的规则应用到包含3个连续名词的文本中,则只有前两个名词被分块

1
2
3
4
5
6
nouns = [("money", "NN"), ("market", "NN"), ("fund", "NN")]
grammar = "NP: {<NN><NN>}" # 匹配两个名词

cp = nltk.RegexpParser(grammar)
print(cp.parse(nouns))
# (S (NP money/NN market/NN) fund/NN)

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

探索文本语料库

使用分块器可以更轻松的在已标注的语料库中提取匹配特定词性标记序列的短语(ref 5.2)

1
2
3
4
5
6
7
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)
......

find_chunks 函数见代码库

缝隙

定义想从块中排除什么有时是很容易的,为不包括在大块中的标识符序列定义一个缝隙,

[ the/DT little/JJ yellow/JJ dog/NN ] barked/VBD at/IN [ the/DT cat/NN ]barked/VBD at/IN是一个缝隙

加缝隙是从大块中去除标识符序列的过程.

  • 如果匹配的标识符序列贯穿整块,那么这个整块将被去除;
  • 如果标识符序列出现在块中间,这些标识符会被去除,留下一个较小的块,在以前只有一个块的地方留下两个块
  • 如果序列在块的周边,这些标记会被去除,留下一个较小的块

表 7-2 演示了此3中可能.

表 7-2. 3个加缝规则应用于同一个块
整个块 块中间 块结尾
输入 [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
1
2
3
4
5
6
7
8
9
grammar = r"""
NP:
{<.*>+} # 匹配所有
}<VBD|IN>+{ #匹配 VBD或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))

分块的表示:标记与树状图

作为标注和分析之间的中间状态(ref 8 chapter),块结构可以使用标记或树状图来表示。
使用最广泛的表示是IOB标记

在这个方案中,每个标识符被用3个特殊的块标签之一标注,I(inside,内部),O(outside,外部)或B(begin,开始).

B标志着它是分块的开始。块内的标识符子序列被标注为I,其他标注为O

B和I标记是块类型的后缀,如B-NP, I-NP。

IOB标记已成为文件中表示块结构的标准方式,图 7-3 展示了此方案的例子:

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

nlp_pic_7-3

图 7-3 分块结构的标记标识符

在这种表示方式中,每个标识符一行,和它的词性标记与块标记一起.这种格式允许表示多个块类型,只要块不重叠.块的结构也可以使用树来表示.这有利于使每块作为一个组成部分可以直接操作,如下图:

nlp_pic_7-4

图 7-4 块结构的树状图表示

NLTK用树状图作为分块的内部表示,却提供这些树状图与IOB之间格式转换的方法

7.3 开发和评估分块器

如何评估分块器?

评估分块器需要一个合适的已标注语料库.

  • 首先寻找将IOB格式转换成NLTK树状图的机制
  • 然后是如何在一个更大规模上使用已分块的语料库完成上述过程

本节学习

  1. 如何为一个分块器相对于一个语料库的准确性打分,
  2. 利用一些数据驱动方式搜索NP分块,

读取IOB格式与CoNLL2000分块语料库

重点:扩展分块器的覆盖范围

使用corpora模块,可以加载已标注的《华尔街日报文本》文本,然后使用IOB符号分块
(这个语料库提供的分块类型有NPVPPP,每个句子使用多行表示)

转换函数用多行字符串建立一个树状图表示,参数可以选择使用3个分块类型的任何子集(例子中只使用了NP分块)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import nltk

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()

nlp_pic_7-3

CoNLL2000分块语料库包含27万词的《华尔街日报》文本,分为’训练’和’测试’两部分,标注有词性标记和IOB格式分块标记。

读取conll2000语料库训练部分的100个句子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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)
# ./.)

# 包含3中分块类型:NP分块,VP分块,PP分块;只选择NP分块
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)
# ./.)

简单评估和基准

为琐碎的不创建任何块的块分析器cp建立一个基准

1
2
3
4
5
6
7
8
9
10
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%%
"""

这里有个相当神奇的地方,程序输出了两个%,书中只有1个%,尚未定位到问题

IOB标记准确性表明超过三分之一的词被标注为O,即没有在NP分块中.然而由于标注器没有找到任何分块,精度、召回率、F-度量均为零

查找以名词短语标记的特征字母(如CD,DT和JJ)开头的标记

1
2
3
4
5
6
7
8
9
10
11
12
grammar = r"NP: {<[CDJNP].*>+}"
cp = nltk.RegexpParser(grammar) # 初级的正则表达式分块器
test_sents = conll2000.chunked_sents('test.txt')
print(cp.evaluate(test_sents)) # 评估结果
"""
ChunkParse score:
IOB Accuracy: 87.7%%
Precision: 70.6%%
Recall: 38.5%%
F-Measure: 49.8%%
"""
# 使用 chunk_types=['NP'] 参数,ChunkParse score结果为和书中一致

这种方法结果较好,但是仍可以采用更多数据驱动的方式改善它,
这里可以使用训练语料找到对每个词性标记最有可能的块标记(I、O或B),
即使用unigram标注器(ref 5.4)建立一个分块器(不是确定每个词的正确词性标记,
而是给定每个词的词性标记,尝试确定正确的块标记)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import nltk


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

def parse(self, sentence):
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]
#为词性标注IOB块标记
conlltags = [(word, pos, chunktag) for ((word,pos),chunktag) in zip(sentence, chunktags)]
return nltk.chunk.conlltags2tree(conlltags) #转换成分块树状图

使用训练语料找到对每个词性标记最有可能的块标记(I、O或B)
可以用bigram标注器建立一个分块器,但不是要确定每个词的正确词性标记,而是给定每个词的词性标记,尝试确定正确的块标记

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from nltk.corpus import conll2000
import nltk


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

def parse(self, sentence):
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]
#为词性标注IOB块标记
conlltags = [(word, pos, chunktag) for ((word,pos),chunktag) in zip(sentence, chunktags)]
return nltk.chunk.conlltags2tree(conlltags) #转换成分块树状图


# 使用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%%

此分块器结果更好,使用unigram标注器标记每个在语料库中出现的词性标记

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

结果表明除了两种货币符号#$,大多数标点符号出现在NP分块以外.
限定词(DT)和所有格(PRP$和 WP$)出现在NP分块的开头,
而名词类型(NN,NNP,NNPS,NNS)大多出现在NP的分块之内.

修改unigram分块器,可以很容易的建立bigram分块器,修改代码如下

diff:true
1
2
- self.tagger = nltk.UnigramTagger(train_data)
+ self.tagger = nltk.BigramTagger(train_data)

训练基于分类器的分块器

无论是基于正则表达式的分块器还是n-gram分块器,创建什么样的分块完全取决于词性标记.
然而,有时词性标记不足以确定一个句子应如何分块

1
2
(3) a. Joey/NN sold/VBD the/DT farmer/NN rice/NN ./.
b. Nick/NN broke/VBD my/DT computer/NN monitor/NN ./.

考虑例句,两句话词性标记相同,但分块方式不同

第一句中,the farmer 和 rice 都是单独分块
第二句中,my computer monitor 是单独的分块
如果想最大限度的提升分块的性能,需要使用词的内容作为词性标记的补充.

包含词的内容信息的一种方法是使用基于分类器的标注器对句子分块.
比如使用n-gram分块器,这个基于分类器器分块器分配IOB标记给句子中的词,
然后将这些标记转换为块.

本小节示例需要安装 oCaml

使用连续分类器对名词短语分块

1
2


7.4 语言结构中的递归

用级联分块器构建嵌套结构

只需创建一个包含递归规则的多级的分块语法,就可以建立任意深度的分块结构

例子展示名词短语、介词短语、动词短语和句子的模式
这是一个4级分块语法器,可以用来创建深度最深为4的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import nltk

grammar = r"""
NP: {<DT|JJ|NN.*>+}
PP: {<IN><NP>}
VP: {<VB.*><NP|PP|CLAUSE>+$}
CLAUSE: {<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)))))
"""

结果丢掉了以saw为首的VP
将此分块器应用到有更深嵌套的句子中,无法识别开始的VP块,如下例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
(CLAUSE
(NP the/DT cat/NN)
(VP sit/VB (PP on/IN (NP the/DT mat/NN)))))
"""

解决方案: 添加loop参数,指定模式应该循环次数,让分块器在他的模式中循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)))))))
"""

树状图

在NLTK中,创建树状图,方法是给节点添加标签和一个子链表

tree = nltk.Tree('NP',['the','rabbit'])

tree的一些方法:

1
2
3
4
print(tree[1])
tree.node
tree.leaves()
tree.draw()

树遍历

使用递归函数来遍历树

1
2


书中例子会报TypeError: Tree: Expected a node value and child list 错误

7.5 命名实体识别

表 7-3 常用命名实体类型
NE类型 例子
组织(ORGANIZATION) Georgia-Pacific Corp., WHO
人(PERSON) Eddy Bonte, President Obama
地点(LOCATION) Murray River, Mount Everest
日期(DATE) June, 2008-06-29
时间(TIME) two fifty a m, 1:30 p.m.
货币(MONEY) 175 million Canadian Dollars, GBP 10.40
百分数(PERCENT) twenty pct, 18.75 %
设施(FACILITY) Washington Monument, Stonehenge
地缘政治实体(GPE 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不仅是一个名词短语也是一个PER类型的命名实体。

识别命名实体可以通过查找适当的名称列表(如识别地点时,可以使用地名词典),但盲目这样做会出问题,比如人或组织名词的列表无法完全覆盖,另外许多实体措辞有歧义,如May和North可能是日期和地点,也有可能都是人名.
更大的挑战来自如’Stanford University’这样的多词名词和包含其他名词的名称,因此我们需要能够识别多标识符序列的开头和结尾

NER是一个非常适合用于分类器类型的方法。

NLTK提供了一个已经训练好的可以识别命名实体的分类器,使用函数nltk.ne_chunk()访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import nltk

sent = nltk.corpus.treebank.tagged_sents()[22]
# 如果设置参数binary=True,那么命名实体只被标注为NE,否则,分类器会添加类型标签,如 PERSON, ORGANIZATION and GPE 等
print(nltk.ne_chunk(sent, binary=True))
# (S
# The/DT
# (NE U.S./NNP)
# is/VBZ
# one/CD
# ....
# according/VBG
# to/TO
# (NE Brooke/NNP)
# ...)
print(nltk.ne_chunk(sent)) # PERSON, ORGANIZATION and GPE
# (S
# The/DT
# (GPE U.S./NNP)
# is/VBZ
# one/CD
# ......
# according/VBG
# to/TO
# (PERSON Brooke/NNP T./NNP Mossman/NNP)
# ....)

7.6 关系抽取

只要文本中的命名实体被识别,就可以提取它们之间存在的关系。

关系抽取的方法是首先寻找所有(X, $\alpha$, Y)形式的三元组,其中X和Y是指定类型的命名实体,$\alpha$表示X和Y之间关系的字符串

搜索包含词 in 的字符串,正则表达式会忽略动名词前为in的字符串(否定预测先行断言)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import re
import nltk

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.relextract.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']

荷兰语的命名实体语料库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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.relextract.clause(r, relsym="VAN"))
# VAN("cornet_d'elzius", 'buitenlandse_handel')
# VAN('johan_rottiers', 'kardinaal_van_roey_instituut')
# VAN('annie_lennox', 'eurythmics')

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.relextract.rtuple(r, lcon=True, rcon=True))
# ...'')[PER: "Cornet/V d'Elzius/N"] 'is/V op/Prep dit/Pron ogenblik/N kabinetsadviseur/N van/Prep staatssecretaris/N voor/Prep' [ORG: 'Buitenlandse/N Handel/N'](''...
# ...'')[PER: 'Johan/N Rottiers/N'] 'is/V informaticacoördinator/N van/Prep het/Art' [ORG: 'Kardinaal/N Van/N Roey/N Instituut/N']('in/Prep'...
# ...'Door/Prep rugproblemen/N van/Prep zangeres/N')[PER: 'Annie/N Lennox/N'] 'wordt/V het/Art concert/N van/Prep' [ORG: 'Eurythmics/N']('vandaag/Adv in/Prep'...

7.7小结(略)

7.8深入阅读(略)

最后更新: 2019年05月09日 17:33

原始链接: https://ice-melt.github.io/2019/05/08/Python_NLP_07/

× 您的支持是我原创的动力
打赏二维码