6.2 有害的歧义
不幸的是,随着文法覆盖范围的增加和输入句子长度的增长,分析树的数量也迅速增长。事实上,它以天文数字的速度增长。
让我们在一个简单的例子帮助下来探讨这个问题。词 fish 既是名词又是动词。我们可以造这样的句子 fish fish fish,意思是 fish like to fish for other fish。(用 police 尝试一下,如果你喜欢更有意思的东西。)下面是“fish”句子的玩具文法。
>>> grammar = nltk.CFG.fromstring("""
... S -> NP V NP
... NP -> NP Sbar
... Sbar -> NP V
... NP -> 'fish'
... V -> 'fish'
... """)
现在,我们可以尝试分析一个较长的句子,fish fish fish fish fish,其中一个意思是:“fish that other fish fish are in the habit of fishing fish themselves”。我们使用 NLTK 的图表分析器,它在本章前面介绍过。这句话有两种读法。
>>> tokens = ["fish"] * 5
>>> cp = nltk.ChartParser(grammar)
>>> for tree in cp.parse(tokens):
... print(tree)
(S (NP fish) (V fish) (NP (NP fish) (Sbar (NP fish) (V fish))))
(S (NP (NP fish) (Sbar (NP fish) (V fish))) (V fish) (NP fish))
随着句子长度增加到(3,5,7,…),我们得到的分析树的数量是:1; 2; 5; 14; 42; 132; 429; 1,430; 4,862; 16,796; 58,786; 208,012; ….(这是 Catalan 数,我们在4的练习中见过)。最后一个是句子长度为 23 的分析树的数目,这是宾州树库 WSJ 部分的句子的平均长度。对于一个长度为 50 的句子有超过 10<sup>12</sup>的解析,这只是 Piglet 句子长度的一半(1),这些句子小孩可以毫不费力的处理。没有实际的自然语言处理系统可以为一个句子构建数以百万计的树,并根据上下文选择一个合适的。很显然,人也不会这样做!
请注意,这个问题不是只在我们选择的例子中存在。(Church & Patil, 1982)指出PP
附着句法歧义在像(18)这样的句子中也是按 Catalan 数的比例增长。
def give(t):
return t.label() == 'VP' and len(t) > 2 and t[1].label() == 'NP'\
and (t[2].label() == 'PP-DTV' or t[2].label() == 'NP')\
and ('give' in t[0].leaves() or 'gave' in t[0].leaves())
def sent(t):
return ' '.join(token for token in t.leaves() if token[0] not in '*-0')
def print_node(t, width):
output = "%s %s: %s / %s: %s" %\
(sent(t[0]), t[1].label(), sent(t[1]), t[2].label(), sent(t[2]))
if len(output) > width:
output = output[:width] + "..."
print(output)
我们可以观察到一种强烈的倾向就是最短的补语最先出现。然而,这并没有解释类似give NP: federal judges / NP: a raise
的形式,其中有生性起了重要作用。事实上,根据(Bresnan & Hay, 2006)的调查,存在大量的影响因素。这些偏好可以用加权语法来表示。
概率上下文无关语法(或 PCFG)是一种上下文无关语法,它的每一个产生式关联一个概率。它会产生与相应的上下文无关语法相同的文本解析,并给每个解析分配一个概率。PCFG 产生的一个解析的概率仅仅是它用到的产生式的概率的乘积。
最简单的方法定义一个 PCFG 是从一个加权产生式序列组成的特殊格式的字符串加载它,其中权值出现在括号里,如6.4所示。
grammar = nltk.PCFG.fromstring("""
S -> NP VP [1.0]
VP -> TV NP [0.4]
VP -> IV [0.3]
VP -> DatV NP NP [0.3]
TV -> 'saw' [1.0]
IV -> 'ate' [1.0]
DatV -> 'gave' [1.0]
NP -> 'telescopes' [0.8]
NP -> 'Jack' [0.2]
""")
有时可以很方便的将多个产生式组合成一行,如VP -> TV NP [0.4] | IV [0.3] | DatV NP NP [0.3]
。为了确保由文法生成的树能形成概率分布,PCFG 语法强加了约束,产生式所有给定的左侧的概率之和必须为 1。6.4中的语法符合这个约束:对S
只有一个产生式,它的概率是 1.0;对于VP
,0.4+0.3+0.3=1.0;对于NP
,0.8+0.2=1.0。parse()
返回的分析树包含概率:
>>> viterbi_parser = nltk.ViterbiParser(grammar)
>>> for tree in viterbi_parser.parse(['Jack', 'saw', 'telescopes']):
... print(tree)
(S (NP Jack) (VP (TV saw) (NP telescopes))) (p=0.064)
现在,分析树被分配了概率,一个给定的句子可能有数量庞大的可能的解析就不再是问题。分析器将负责寻找最有可能的解析。