import nltk
nltk.download('maxent_ne_chunker')
信息有很多种形状和大小。一个重要的形式是结构化数据:实体和关系的可预测的规范的结构。
例如,我们可能对公司和地点之间的关系感兴趣。给定一个公司,我们希望能够确定它做业务的位置;反过来,给定位置,我们会想发现哪些公司在该位置做业务。如果我们的数据是表格形式(结构化数据),如1.1中的例子,那么回答这些问题就很简单了。
"""
如果这个位置数据被作为一个元组(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)
"""
执行信息提取结构的前三项任务:可以定义一个函数,简单地连接 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] 词性标注器
分块:chunk,通常是选择标识符的一个子集,如NP: DT(限定词)+JJ(形容词)+NN(名词),eg: a little dog
首先思考名词短语词块划分或NP词块划分任务,在那里我们寻找单独名词短语对应的词块。 例如,这里是一些《华尔街日报》文本,其中的NP词块用方括号标记:
sentence = [("the", "DT"), ("little", "JJ"), ("yellow", "JJ"),
("dog", "NN"), ("barked", "VBD"), ("at", "IN"), ("the", "DT"), ("cat", "NN")]
grammar = "NP: {?*}" # NP-chunking的模式
cp = nltk.RegexpParser(grammar) # 正则表达式分块器
result = cp.parse(sentence)
print(result)
result.draw()
# 一个可选的限定词或所有格代名词,零个或多个形容词,然后跟一个名词 --> NP: {?*}
# 一个或多个专有名词 --> +
grammar = r"""
NP: {?*} # 符号$是正则表达式中的一个特殊字符,必须使用反斜杠转义来匹配
{+}
"""
cp = nltk.RegexpParser(grammar) # 正则表达式进行词块划分
sentence = [("Rapunzel", "NNP"), ("let", "VBD"), ("down", "RP"), ("her", "PP$"), ("long", "JJ"), ("golden", "JJ"), ("hair", "NN")]
print (cp.parse(sentence))
# 如果标记模式匹配位置重叠,最左边的匹配优先。
# 例如,如果我们应用一个匹配两个连续的名词文本的规则到一个包含三个连续的名词的文本,则只有前两个名词将被划分
nouns = [("money", "NN"), ("market", "NN"), ("fund", "NN")]
grammar = "NP: {} # Chunk two consecutive nouns"
cp = nltk.RegexpParser(grammar)
print(cp.parse(nouns)) # (S (NP money/NN market/NN) fund/NN)
在本书5.2中,我们看到了我们如何在已标注的语料库中提取匹配的特定的词性标记序列的短语。我们可以使用词块划分器更容易的做同样的工作,如下:
cp = nltk.RegexpParser('CHUNK: { }' )
brown = nltk.corpus.brown
count = 0
for sent in brown.tagged_sents():
tree = cp.parse(sent)
for subtree in tree.subtrees():
if subtree.label() == 'CHUNK': print(subtree)
count += 1
if count >= 30: break
有时定义我们想从一个词块中排除什么比较容易。我们可以定义词缝为一个不包含在词块中的一个词符序列。在下面的例子中,barked/VBD at/IN是一个词缝
# 在例7-3中,我们将整个句子作为一个块,然后练习加缝隙。例7-3.简单的加缝器。
grammar = r"""
NP: {<.*>+} # Chunk: everything
}+{ #Chink sequences of VBD and IN
"""
cp = nltk.RegexpParser(grammar) # 正则表达式进行词块划分
sentence = [("the", "DT"), ("little", "JJ"), ("yellow", "JJ"),
("dog", "NN"), ("barked", "VBD"), ("at", "IN"), ("the", "DT"), ("cat", "NN")]
print (cp.parse(sentence))
# IOB标记已成为文件中表示词块结构的标准方式,我们也将使用这种格式。下面是2.5中的信息如何出现在一个文件中
We PRP B-NP
saw VBD O
the DT B-NP
yellow JJ I-NP
dog NN I-NP
# 如下所示
he PRP B-NP
accepted VBD B-VP
the DT B-NP
position NN I-NP
可以使用NLTK的corpus模块访问较大量的已经划分词块的文本。
CoNLL2000语料库包含27万词的《华尔街日报文本》,分为“训练”和“测试”两部分,标注有词性标记和IOB格式词块标记。
可以使用nltk.corpus.conll2000访问这些数据
# 下面是一个读取语料库的“训练”部分的第100个句子的例子
from nltk.corpus import conll2000
print(conll2000.chunked_sents('train.txt')[99]) # 标注有词性标记和IOB格式词块标记
(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语料库包含三种词块类型:
1. NP词块
2. VP词块如has already delivered
3. PP块如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)
./.)
"""
可以访问一个已划分词块语料,可以评估词块划分器。开始为没有什么意义的词块解析器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, DT和JJ)。"
grammar = r"NP: {<[CDJNP].*>+}"
cp = nltk.RegexpParser(grammar)
print(cp.evaluate(test_sents))
"""
result:
ChunkParse score:
IOB Accuracy: 87.7%%
Precision: 70.6%%
Recall: 67.8%%
F-Measure: 69.2%%
"""
这种方法达到相当好的结果。但是,我们可以采用更多数据驱动的方法改善它,在这里我们使用训练语料找到对每个词性标记最有可能的块标记(I, O或B)。换句话说,我们可以使用一元标注器(4)建立一个词块划分器。但不是尝试确定每个词的正确的词性标记,而是根据每个词的词性标记,尝试确定正确的词块标记。
我们曾定义了UnigramChunker类,使用一元标注器给句子加词块标记。这个类的大部分代码只是用来在NLTK 的ChunkParserI接口使用的词块树表示和嵌入式标注器使用的IOB表示之间镜像转换。类定义了两个方法:一个构造函数[1],当我们建立一个新的UnigramChunker时调用;以及parse方法[3],用来给新句子划分词块。
class UnigramChunker(nltk.ChunkParserI):
def __init__(self, train_sents):
# 测试数据 :将训练数据转换成适合训练标注器的形式
# 使用tree2conlltags映射每个词块树到一个word,tag,chunk三元组的列表
train_data = [[(t,c) for w,t,c in nltk.chunk.tree2conlltags(sent)]
for sent in train_sents]
# 使用转换好的训练数据训练一个一元标注器,并存储在self.tagger供以后使用
self.tagger = nltk.UnigramTagger(train_data)
def parse(self, sentence):
# 接收一个已标注的句子作为其输入
pos_tags = [pos for (word,pos) in sentence]
# 然后使用在构造函数中训练过的标注器self.tagger,为词性标记标注IOB词块标记
tagged_pos_tags = self.tagger.tag(pos_tags)
chunktags = [chunktag for (pos, chunktag) in tagged_pos_tags]
# 它提取词块标记,与原句组合,产生conlltags
conlltags = [(word, pos, chunktag) for ((word,pos),chunktag)
in zip(sentence, chunktags)]
# 它使用conlltags2tree将结果转换成一个词块树
return nltk.chunk.conlltags2tree(conlltags)
构造函数[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))
它已经发现大多数标点符号出现在NP词块外,除了两种货币符号#和* 。 它 也 发 现 限 定 词 ( D T ) 和 所 有 格 ( P R P 。它也发现限定词(DT)和所有格(PRP 。它也发现限定词(DT)和所有格(PRP*和WP$)出现在NP词块的开头,而名词类型(NN, NNP, NNPS,NNS)大多出现在NP词块内。
建立了一个一元分块器,很容易建立一个二元分块器:我们只需要改变类的名称为BigramChunker,修改3.1行[2]构造一个BigramTagger而不是UnigramTagger。由此产生的词块划分器的性能略高于一元词块划分器:
"""
建立一个二元分块器:我们只需要改变类的名称为BigramChunker
"""
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]
conlltags = [(word, pos, chunktag) for ((word,pos),chunktag)
in zip(sentence, chunktags)]
return nltk.chunk.conlltags2tree(conlltags)
bigram_chunker = BigramChunker(train_sents)
print(bigram_chunker.evaluate(test_sents)) # F-Measure: 84.5%% 上升了
无论是基于正则表达式的词块划分器还是n-gram词块划分器,决定创建什么词块完全基于词性标记。然而,有时词性标记不足以确定一个句子应如何划分词块。例如,考虑下面的两个语句:
运行本节的代码出现报错,还未能解决 lookup error NLTK was unable to find the megam file! Use software specific configuration paramaters or set the MEGAM environment variable.
# 使用连续分类器对名词短语分块。
class ConsecutiveNPChunkTagger(nltk.TaggerI):
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) # npchunk_features是特征提取器
train_set.append( (featureset, tag) )
history.append(tag)
self.classifier = nltk.MaxentClassifier.train(
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):
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)
"""
唯一需要填写的是特征提取器。首先,我们定义一个简单的特征提取器,它只是提供了当前词符的词性标记。
使用此特征提取器,我们的基于分类器的词块划分器的表现与一元词块划分器非常类似:
"""
def npchunk_features(sentence, i, history):
word, pos = sentence[i]
return {
"pos": pos}
chunker = ConsecutiveNPChunker(train_sents)
print(chunker.evaluate(test_sents))
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))
到目前为止,我们的词块结构一直是相对平的。已标注词符组成的树在如NP这样的词块节点下任意组合。然而,只需创建一个包含递归规则的多级的词块语法,就可以建立任意深度的词块结构。
"""
4.1是名词短语、介词短语、动词短语和句子的模式。这是一个四级词块语法器,可以用来创建深度最多为4的结构
"""
grammar = r"""
NP: {+} # 名词短语模式
PP: {} # 介词短语模式
VP: {+$} # 动词短语模式
CLAUSE: {} # 句子模型
"""
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。它还有其他缺陷。当我们将此词块划分器应用到一个有更深嵌套的句子时,让我们看看会发生什么。请注意,它无法识别[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
(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))
"""
这个级联过程使我们能创建深层结构。
然而,创建和调试级联过程是困难的,关键点是它能更有效地做全面的分析(见第8章)。
另外,级联过程只能产生固定深度的树(不超过级联级数),完整的句法分析这是不够的。
"""
# 在NLTK中,我们通过给一个节点添加标签和一系列的孩子创建一棵树
tree1 = nltk.Tree('NP', ['Alice'])
print(tree1)
tree2 = nltk.Tree('NP', ['the', 'rabbit'])
print(tree2)
# 可以将这些不断合并成更大的树,如下所示
tree3 = nltk.Tree('VP', ['chased', tree2])
tree4 = nltk.Tree('S', [tree1, tree3])
print(tree4)
# 下面是树对象的一些的方法
print(tree4[1])
print(tree4[1].label())
print(tree4.leaves())
print(tree4[1][1][1])
复杂的树用括号表示难以阅读。在这些情况下,draw方法是非常有用的。它会打开一个新窗口,包含树的一个图形表示。树显示窗口可以放大和缩小,子树可以折叠和展开,并将图形表示输出为一个postscript文件(包含在一个文档中)。
tree4.draw()
使用递归函数来遍历树是标准的做法
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 = tree4
traverse(t)
"""
( S ( NP Alice ) ( VP chased ( NP the rabbit ) ) )
"""
NLTK提供了一个已经训练好的可以识别命名实体的分类器,使用函数 nltk.ne_chunk()访问。如果我们设置参数binary=True①,那么命名实体只被标注为NE;否则,分类器会添加类型标签,如PERSON,ORGANIZATION,and GPE。
sent =nltk.corpus.treebank.tagged_sents()[22]
print(nltk.ne_chunk(sent,binary=True))
print(nltk.ne_chunk(sent))
一旦文本中的命名实体已被识别,我们就可以提取它们之间存在的关系。如前所述,我们通常会寻找指定类型的命名实体之间的关系。进行这一任务的方法之一是首先寻找所有X, α, Y)形式的三元组,其中X和Y是指定类型的命名实体,α表示X和Y之间关系的字符串。然后我们可以使用正则表达式从α的实体中抽出我们正在查找的关系。
"""
下面的例子搜索包含词in的字符串。
特殊的正则表达式(?!\b.+ing\b)是一个否定预测先行断言,允许我们忽略如success in supervising the transition of中的字符串,
其中in后面跟一个动名词。
"""
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.rtuple(rel))
搜索关键字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)
print(VAN)
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]