6.2 有害的歧义

不幸的是,随着文法覆盖范围的增加和输入句子长度的增长,分析树的数量也迅速增长。事实上,它以天文数字的速度增长。

让我们在一个简单的例子帮助下来探讨这个问题。词 fish 既是名词又是动词。我们可以造这样的句子 fish fish fish,意思是 fish like to fish for other fish。(用 police 尝试一下,如果你喜欢更有意思的东西。)下面是“fish”句子的玩具文法。

  1. >>> grammar = nltk.CFG.fromstring("""
  2. ... S -> NP V NP
  3. ... NP -> NP Sbar
  4. ... Sbar -> NP V
  5. ... NP -> 'fish'
  6. ... V -> 'fish'
  7. ... """)

现在,我们可以尝试分析一个较长的句子,fish fish fish fish fish,其中一个意思是:“fish that other fish fish are in the habit of fishing fish themselves”。我们使用 NLTK 的图表分析器,它在本章前面介绍过。这句话有两种读法。

  1. >>> tokens = ["fish"] * 5
  2. >>> cp = nltk.ChartParser(grammar)
  3. >>> for tree in cp.parse(tokens):
  4. ... print(tree)
  5. (S (NP fish) (V fish) (NP (NP fish) (Sbar (NP fish) (V fish))))
  6. (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 数的比例增长。

  1. def give(t):
  2. return t.label() == 'VP' and len(t) > 2 and t[1].label() == 'NP'\
  3. and (t[2].label() == 'PP-DTV' or t[2].label() == 'NP')\
  4. and ('give' in t[0].leaves() or 'gave' in t[0].leaves())
  5. def sent(t):
  6. return ' '.join(token for token in t.leaves() if token[0] not in '*-0')
  7. def print_node(t, width):
  8. output = "%s %s: %s / %s: %s" %\
  9. (sent(t[0]), t[1].label(), sent(t[1]), t[2].label(), sent(t[2]))
  10. if len(output) > width:
  11. output = output[:width] + "..."
  12. print(output)

我们可以观察到一种强烈的倾向就是最短的补语最先出现。然而,这并没有解释类似give NP: federal judges / NP: a raise的形式,其中有生性起了重要作用。事实上,根据(Bresnan & Hay, 2006)的调查,存在大量的影响因素。这些偏好可以用加权语法来表示。

概率上下文无关语法(或 PCFG)是一种上下文无关语法,它的每一个产生式关联一个概率。它会产生与相应的上下文无关语法相同的文本解析,并给每个解析分配一个概率。PCFG 产生的一个解析的概率仅仅是它用到的产生式的概率的乘积。

最简单的方法定义一个 PCFG 是从一个加权产生式序列组成的特殊格式的字符串加载它,其中权值出现在括号里,如6.4所示。

  1. grammar = nltk.PCFG.fromstring("""
  2. S -> NP VP [1.0]
  3. VP -> TV NP [0.4]
  4. VP -> IV [0.3]
  5. VP -> DatV NP NP [0.3]
  6. TV -> 'saw' [1.0]
  7. IV -> 'ate' [1.0]
  8. DatV -> 'gave' [1.0]
  9. NP -> 'telescopes' [0.8]
  10. NP -> 'Jack' [0.2]
  11. """)

有时可以很方便的将多个产生式组合成一行,如VP -&gt; 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()返回的分析树包含概率:

  1. >>> viterbi_parser = nltk.ViterbiParser(grammar)
  2. >>> for tree in viterbi_parser.parse(['Jack', 'saw', 'telescopes']):
  3. ... print(tree)
  4. (S (NP Jack) (VP (TV saw) (NP telescopes))) (p=0.064)

现在,分析树被分配了概率,一个给定的句子可能有数量庞大的可能的解析就不再是问题。分析器将负责寻找最有可能的解析。