跳转至

9 构建基于特征的语法

自然语言具有范围广泛的语法结构,用 8 中所描述的简单的方法很难处理的如此广泛的语法结构。为了获得更大的灵活性,我们改变我们对待语法类别如SNPV的方式。我们将这些原子标签分解为类似字典的结构,其特征可以为一个范围的值。

本章的目的是要回答下列问题:

  1. 我们怎样用特征扩展上下文无关语法框架,以获得更细粒度的对语法类别和产生式的控制?
  2. 特征结构的主要形式化属性是什么,我们如何使用它们来计算?
  3. 我们现在用基于特征的语法能捕捉到什么语言模式和语法结构?

一路上,我们将介绍更多的英语句法主题,包括约定、子类别和无限制依赖成分等现象。

1 语法特征

在第六章中,我们描述了如何建立基于检测文本特征的分类器。那些特征可能非常简单,如提取一个单词的最后一个字母,或者更复杂一点儿,如分类器自己预测的词性标签。在本章中,我们将探讨特征在建立基于规则的语法中的作用。对比特征提取,记录已经自动检测到的特征,我们现在要声明词和短语的特征。我们以一个很简单的例子开始,使用字典存储特征和它们的值。

>>> kim = {'CAT': 'NP', 'ORTH': 'Kim', 'REF': 'k'}
>>> chase = {'CAT': 'V', 'ORTH': 'chased', 'REL': 'chase'}

对象kimchase有几个共同的特征,CAT(语法类别)和ORTH(正字法,即拼写)。此外,每一个还有更面向语义的特征:kim['REF']意在给出kim的指示物,而chase['REL']给出chase表示的关系。在基于规则的语法上下文中,这样的特征和特征值对被称为特征结构,我们将很快看到它们的替代符号。

特征结构包含各种有关语法实体的信息。这些信息不需要详尽无遗,我们可能要进一步增加属性。例如,对于一个动词,根据动词的参数知道它扮演的“语义角色”往往很有用。对于chase,主语扮演“施事”的角色,而宾语扮演“受事”角色。让我们添加这些信息,使用'sbj''obj'作为占位符,它会被填充,当动词和它的语法参数结合时:

>>> chase['AGT'] = 'sbj'
>>> chase['PAT'] = 'obj'

如果我们现在处理句子Kim chased Lee,我们要“绑定”动词的施事角色和主语,受事角色和宾语。我们可以通过链接到相关的NPREF特征做到这个。在下面的例子中,我们做一个简单的假设:在动词直接左侧和右侧的NP分别是主语和宾语。我们还在例子结尾为Lee添加了一个特征结构。

>>> sent = "Kim chased Lee"
>>> tokens = sent.split()
>>> lee = {'CAT': 'NP', 'ORTH': 'Lee', 'REF': 'l'}
>>> def lex2fs(word):
...     for fs in [kim, lee, chase]:
...         if fs['ORTH'] == word:
...             return fs
>>> subj, verb, obj = lex2fs(tokens[0]), lex2fs(tokens[1]), lex2fs(tokens[2])
>>> verb['AGT'] = subj['REF']
>>> verb['PAT'] = obj['REF']
>>> for k in ['ORTH', 'REL', 'AGT', 'PAT']:
...     print("%-5s => %s" % (k, verb[k]))
ORTH  => chased
REL   => chase
AGT   => k
PAT   => l

同样的方法可以适用不同的动词,例如surprise,虽然在这种情况下,主语将扮演“源事”(SRC)的角色,宾语扮演“体验者”(EXP)的角色:

>>> surprise = {'CAT': 'V', 'ORTH': 'surprised', 'REL': 'surprise',
...             'SRC': 'sbj', 'EXP': 'obj'}

特征结构是非常强大的,但我们操纵它们的方式是极其特别的。我们本章接下来的任务是,显示上下文无关语法和分析如何能扩展到合适的特征结构,使我们可以一种更通用的和有原则的方式建立像这样的分析。我们将通过查看句法协议的现象作为开始;我们将展示如何使用特征典雅的表示协议约束,并在一个简单的语法中说明它们的用法。

由于特征结构是表示任何形式的信息的通用的数据结构,我们将从更形式化的视点简要地看着它们,并演示 NLTK 提供的特征结构的支持。在本章的最后一部分,我们将表明,特征的额外表现力开辟了一个用于描述语言结构的复杂性的广泛的可能性。

1.1 句法协议

下面的例子展示词序列对,其中第一个是符合语法的而第二个不是。(我们在词序列的开头用星号表示它是不符合语法的。)

S   ->   NP VP
NP  ->   Det N
VP  ->   V

Det  ->  'this'
N    ->  'dog'
V    ->  'runs'

1.2 使用属性和约束

我们说过非正式的语言类别具有属性;例如,名词具有复数的属性。让我们把这个弄的更明确:

N[NUM=pl]

注意一个句法类别可以有多个特征,例如V[TENSE=pres, NUM=pl]。在一般情况下,我们喜欢多少特征就可以添加多少。

关于 1.1 的最后的细节是语句%start S。这个“指令”告诉分析器以S作为文法的开始符号。

一般情况下,即使我们正在尝试开发很小的语法,把产生式放在一个文件中我们可以编辑、测试和修改是很方便的。我们将 1.1 以 NLTK 的数据格式保存为文件'feat0.fcfg'。你可以使用nltk.data.load()制作你自己的副本进行进一步的实验。

1.2 说明了基于特征的语法图表解析的操作。为输入分词之后,我们导入load_parser函数❶,以语法文件名为输入,返回一个图表分析器cp❷。调用分析器的parse()方法将迭代生成的分析树;如果文法无法分析输入,trees将为空,并将会包含一个或多个分析树,取决于输入是否有句法歧义。

>>> tokens = 'Kim likes children'.split()
>>> from nltk import load_parser 
>>> cp = load_parser('grammars/book_grammars/feat0.fcfg', trace=2)  
>>> for tree in cp.parse(tokens):
...     print(tree)
...
|.Kim .like.chil.|
Leaf Init Rule:
|[----]    .    .| [0:1] 'Kim'
|.    [----]    .| [1:2] 'likes'
|.    .    [----]| [2:3] 'children'
Feature Bottom Up Predict Combine Rule:
|[----]    .    .| [0:1] PropN[NUM='sg'] -> 'Kim' *
Feature Bottom Up Predict Combine Rule:
|[----]    .    .| [0:1] NP[NUM='sg'] -> PropN[NUM='sg'] *
Feature Bottom Up Predict Combine Rule:
|[---->    .    .| [0:1] S[] -> NP[NUM=?n] * VP[NUM=?n] {?n: 'sg'}
Feature Bottom Up Predict Combine Rule:
|.    [----]    .| [1:2] TV[NUM='sg', TENSE='pres'] -> 'likes' *
Feature Bottom Up Predict Combine Rule:
|.    [---->    .| [1:2] VP[NUM=?n, TENSE=?t] -> TV[NUM=?n, TENSE=?t] * NP[] {?n: 'sg', ?t: 'pres'}
Feature Bottom Up Predict Combine Rule:
|.    .    [----]| [2:3] N[NUM='pl'] -> 'children' *
Feature Bottom Up Predict Combine Rule:
|.    .    [----]| [2:3] NP[NUM='pl'] -> N[NUM='pl'] *
Feature Bottom Up Predict Combine Rule:
|.    .    [---->| [2:3] S[] -> NP[NUM=?n] * VP[NUM=?n] {?n: 'pl'}
Feature Single Edge Fundamental Rule:
|.    [---------]| [1:3] VP[NUM='sg', TENSE='pres'] -> TV[NUM='sg', TENSE='pres'] NP[] *
Feature Single Edge Fundamental Rule:
|[==============]| [0:3] S[] -> NP[NUM='sg'] VP[NUM='sg'] *
(S[]
  (NP[NUM='sg'] (PropN[NUM='sg'] Kim))
  (VP[NUM='sg', TENSE='pres']
    (TV[NUM='sg', TENSE='pres'] likes)
    (NP[NUM='pl'] (N[NUM='pl'] children))))

分析过程中的细节对于当前的目标并不重要。然而,有一个实施上的问题与我们前面的讨论语法的大小有关。分析包含特征限制的产生式的一种可行的方法是编译出问题中特征的所有可接受的值,是我们最终得到一个大的完全指定的(6)中那样的 CFG。相比之下,前面例子中显示的分析器过程直接与给定语法的未指定的产生式一起运作。特征值从词汇条目“向上流动”,变量值于是通过如{?n: 'sg', ?t: 'pres'}这样的绑定(即字典)与那些值关联起来。当分析器装配有关它正在建立的树的节点的信息时,这些变量绑定被用来实例化这些节点中的值;从而通过查找绑定中?n?t的值,未指定的VP[NUM=?n, TENSE=?t] -> TV[NUM=?n, TENSE=?t] NP[]实例化为VP[NUM='sg', TENSE='pres'] -> TV[NUM='sg', TENSE='pres'] NP[]

最后,我们可以检查生成的分析树(在这种情况下,只有一个)。

>>> for tree in trees: print(tree)
(S[]
 (NP[NUM='sg'] (PropN[NUM='sg'] Kim))
 (VP[NUM='sg', TENSE='pres']
 (TV[NUM='sg', TENSE='pres'] likes)
 (NP[NUM='pl'] (N[NUM='pl'] children))))

1.3 术语

到目前为止,我们只看到像sgpl这样的特征值。这些简单的值通常被称为原子——也就是,它们不能被分解成更小的部分。原子值的一种特殊情况是布尔值,也就是说,值仅仅指定一个属性是真还是假。例如,我们可能要用布尔特征AUX区分助动词,如canmaywilldo。例如,产生式V[TENSE=pres, AUX=+] -> 'can'意味着can接受TENSE的值为pres,并且AUX的值为+true。有一个广泛采用的约定用缩写表示布尔特征f;不用AUX=+AUX=-,我们分别用+AUX-AUX。这些都是缩写,然而,分析器就像+-是其他原子值一样解释它们。(15)显示了一些有代表性的产生式:

V[TENSE=pres, +AUX] -> 'can'
V[TENSE=pres, +AUX] -> 'may'

V[TENSE=pres, -AUX] -> 'walks'
V[TENSE=pres, -AUX] -> 'likes'

在传递中,我们应该指出有显示 AVM 的替代方法;1.3 显示了一个例子。虽然特征结构呈现的(16)中的风格不太悦目,我们将坚持用这种格式,因为它对应我们将会从 NLTK 得到的输出。

关于表示,我们也注意到特征结构,像字典,对特征的顺序没有指定特别的意义。所以(16)等同于:

[AGR = [NUM = pl  ]]
[      [PER = 3   ]]
[      [GND = fem ]]
[                  ]
[POS = N           ]

2 处理特征结构

在本节中,我们将展示如何在 NLTK 中构建和操作特征结构。我们还将讨论统一的基本操作,这使我们能够结合两个不同的特征结构中的信息。

NLTK 中的特征结构使用构造函数FeatStruct()声明。原子特征值可以是字符串或整数。

>>> fs1 = nltk.FeatStruct(TENSE='past', NUM='sg')
>>> print(fs1)
[ NUM   = 'sg'   ]
[ TENSE = 'past' ]

一个特征结构实际上只是一种字典,所以我们可以平常的方式通过索引访问它的值。我们可以用我们熟悉的方式值给特征:

>>> fs1 = nltk.FeatStruct(PER=3, NUM='pl', GND='fem')
>>> print(fs1['GND'])
fem
>>> fs1['CASE'] = 'acc'

我们还可以为特征结构定义更复杂的值,如前面所讨论的。

>>> fs2 = nltk.FeatStruct(POS='N', AGR=fs1)
>>> print(fs2)
[       [ CASE = 'acc' ] ]
[ AGR = [ GND  = 'fem' ] ]
[       [ NUM  = 'pl'  ] ]
[       [ PER  = 3     ] ]
[                        ]
[ POS = 'N'              ]
>>> print(fs2['AGR'])
[ CASE = 'acc' ]
[ GND  = 'fem' ]
[ NUM  = 'pl'  ]
[ PER  = 3     ]
>>> print(fs2['AGR']['PER'])
3

指定特征结构的另一种方法是使用包含feature=value格式的特征-值对的方括号括起的字符串,其中值本身可能是特征结构:

>>> print(nltk.FeatStruct("[POS='N', AGR=[PER=3, NUM='pl', GND='fem']]"))
[       [ GND = 'fem' ] ]
[ AGR = [ NUM = 'pl'  ] ]
[       [ PER = 3     ] ]
[                       ]
[ POS = 'N'             ]

特征结构本身并不依赖于语言对象;它们是表示知识的通用目的的结构。例如,我们可以将一个人的信息用特征结构编码:

>>> print(nltk.FeatStruct(NAME='Lee', TELNO='01 27 86 42 96', AGE=33))
[ AGE   = 33               ]
[ NAME  = 'Lee'            ]
[ TELNO = '01 27 86 42 96' ]

在接下来的几页中,我们会使用这样的例子来探讨特征结构的标准操作。这将使我们暂时从自然语言处理转移,因为在我们回来谈论语法之前需要打下基础。坚持!

将特征结构作为图来查看往往是有用的;更具体的,作为有向无环图(DAG)。(19)等同于上面的 AVM。

>>> print(nltk.FeatStruct("""[NAME='Lee', ADDRESS=(1)[NUMBER=74, STREET='rue Pascal'],
...                          SPOUSE=[NAME='Kim', ADDRESS->(1)]]"""))
[ ADDRESS = (1) [ NUMBER = 74           ] ]
[               [ STREET = 'rue Pascal' ] ]
[                                         ]
[ NAME    = 'Lee'                         ]
[                                         ]
[ SPOUSE  = [ ADDRESS -> (1)  ]           ]
[           [ NAME    = 'Kim' ]           ]

括号内的整数有时也被称为标记或同指标志。整数的选择并不重要。可以有任意数目的标记在一个单独的特征结构中。

>>> print(nltk.FeatStruct("[A='a', B=(1)[C='c'], D->(1), E->(1)]"))
[ A = 'a'             ]
[                     ]
[ B = (1) [ C = 'c' ] ]
[                     ]
[ D -> (1)            ]
[ E -> (1)            ]

2.1 包含和统一

认为特征结构提供一些对象的部分信息是很正常的,在这个意义上,我们可以根据它们通用的程度给特征结构排序。例如,(23a)(23b)具有更少特征,(23b)(23c)具有更少特征。

[NUMBER = 74]

统一被正式定义为一个(部分)二元操作:FS[0] ⊔ FS[1]。统一是对称的,所以FS[0] ⊔ FS[1] = FS[1] ⊔ FS[0]。在 Python 中也是如此:

>>> print(fs2.unify(fs1))
[ CITY   = 'Paris'      ]
[ NUMBER = 74           ]
[ STREET = 'rue Pascal' ]

如果我们统一两个具有包含关系的特征结构,那么统一的结果是两个中更具体的那个:

>>> fs0 = nltk.FeatStruct(A='a')
>>> fs1 = nltk.FeatStruct(A='b')
>>> fs2 = fs0.unify(fs1)
>>> print(fs2)
None

现在,如果我们看一下统一如何与结构共享相互作用,事情就变得很有趣。首先,让我们在 Python 中定义(21)

>>> fs0 = nltk.FeatStruct("""[NAME=Lee,
...                           ADDRESS=[NUMBER=74,
...                                    STREET='rue Pascal'],
...                           SPOUSE= [NAME=Kim,
...                                    ADDRESS=[NUMBER=74,
...                                             STREET='rue Pascal']]]""")
>>> print(fs0)
[ ADDRESS = [ NUMBER = 74           ]               ]
[           [ STREET = 'rue Pascal' ]               ]
[                                                   ]
[ NAME    = 'Lee'                                   ]
[                                                   ]
[           [ ADDRESS = [ NUMBER = 74           ] ] ]
[ SPOUSE  = [           [ STREET = 'rue Pascal' ] ] ]
[           [                                     ] ]
[           [ NAME    = 'Kim'                     ] ]

我们为Kim的地址指定一个CITY作为参数会发生什么?请注意,fs1需要包括从特征结构的根到CITY的整个路径。

>>> fs1 = nltk.FeatStruct("[SPOUSE = [ADDRESS = [CITY = Paris]]]")
>>> print(fs1.unify(fs0))
[ ADDRESS = [ NUMBER = 74           ]               ]
[           [ STREET = 'rue Pascal' ]               ]
[                                                   ]
[ NAME    = 'Lee'                                   ]
[                                                   ]
[           [           [ CITY   = 'Paris'      ] ] ]
[           [ ADDRESS = [ NUMBER = 74           ] ] ]
[ SPOUSE  = [           [ STREET = 'rue Pascal' ] ] ]
[           [                                     ] ]
[           [ NAME    = 'Kim'                     ] ]

通过对比,如果fs1fs2的结构共享版本统一,结果是非常不同的(如图(22)所示):

>>> fs2 = nltk.FeatStruct("""[NAME=Lee, ADDRESS=(1)[NUMBER=74, STREET='rue Pascal'],
...                           SPOUSE=[NAME=Kim, ADDRESS->(1)]]""")
>>> print(fs1.unify(fs2))
[               [ CITY   = 'Paris'      ] ]
[ ADDRESS = (1) [ NUMBER = 74           ] ]
[               [ STREET = 'rue Pascal' ] ]
[                                         ]
[ NAME    = 'Lee'                         ]
[                                         ]
[ SPOUSE  = [ ADDRESS -> (1)  ]           ]
[           [ NAME    = 'Kim' ]           ]

不是仅仅更新KimLee的地址的“副本”,我们现在同时更新他们两个的地址。更一般的,如果统一包含指定一些路径π的值,那么统一同时更新等价于π的任何路径的值。

正如我们已经看到的,结构共享也可以使用变量表示,如?x

>>> fs1 = nltk.FeatStruct("[ADDRESS1=[NUMBER=74, STREET='rue Pascal']]")
>>> fs2 = nltk.FeatStruct("[ADDRESS1=?x, ADDRESS2=?x]")
>>> print(fs2)
[ ADDRESS1 = ?x ]
[ ADDRESS2 = ?x ]
>>> print(fs2.unify(fs1))
[ ADDRESS1 = (1) [ NUMBER = 74           ] ]
[                [ STREET = 'rue Pascal' ] ]
[                                          ]
[ ADDRESS2 -> (1)                          ]

3 扩展基于特征的语法

在本节中,我们回到基于特征的语法,探索各种语言问题,并展示将特征纳入语法的好处。

3.1 子类别

第 8 中,我们增强了类别标签表示不同类别的动词,分别用标签IVTV表示不及物动词和及物动词。这使我们能编写如下的产生式:

VP -> IV
VP -> TV NP

3.2 核心词回顾

我们注意到,在上一节中,通过从主类别标签分解出子类别信息,我们可以表达有关动词属性的更多概括。类似的另一个属性如下:V类的表达式是VP类的短语的核心。同样,NNP的核心词,A(即形容词)是AP的核心词,P(即介词)是PP的核心词。并非所有的短语都有核心词——例如,一般认为连词短语(如the book and the bell)缺乏核心词——然而,我们希望我们的语法形式能表达它所持有的父母/核心子女关系。现在,VVP只是原子符号,我们需要找到一种方法用特征将它们关联起来(就像我们以前关联IVTV那样)。

X-bar 句法通过抽象出短语级别的概念,解决了这个问题。它通常认为有三个这样的级别。如果N表示词汇级别,那么N'表示更高一层级别,对应较传统的级别NomN''表示短语级别,对应类别NP(34a)演示了这种表示结构,而(34b)是更传统的对应。

S -> N[BAR=2] V[BAR=2]
N[BAR=2] -> Det N[BAR=1]
N[BAR=1] -> N[BAR=1] P[BAR=2]
N[BAR=1] -> N[BAR=0] P[BAR=2]
N[BAR=1] -> N[BAR=0]XS

3.3 助动词与倒装

倒装从句——其中的主语和动词顺序互换——出现在英语疑问句,也出现在“否定”副词之后:

S[+INV] -> V[+AUX] NP VP

3.4 无限制依赖成分

考虑下面的对比:

>>> nltk.data.show_cfg('grammars/book_grammars/feat1.fcfg')
% start S
# ###################
# Grammar Productions
# ###################
S[-INV] -> NP VP
S[-INV]/?x -> NP VP/?x
S[-INV] -> NP S/NP
S[-INV] -> Adv[+NEG] S[+INV]
S[+INV] -> V[+AUX] NP VP
S[+INV]/?x -> V[+AUX] NP VP/?x
SBar -> Comp S[-INV]
SBar/?x -> Comp S[-INV]/?x
VP -> V[SUBCAT=intrans, -AUX]
VP -> V[SUBCAT=trans, -AUX] NP
VP/?x -> V[SUBCAT=trans, -AUX] NP/?x
VP -> V[SUBCAT=clause, -AUX] SBar
VP/?x -> V[SUBCAT=clause, -AUX] SBar/?x
VP -> V[+AUX] VP
VP/?x -> V[+AUX] VP/?x
# ###################
# Lexical Productions
# ###################
V[SUBCAT=intrans, -AUX] -> 'walk' | 'sing'
V[SUBCAT=trans, -AUX] -> 'see' | 'like'
V[SUBCAT=clause, -AUX] -> 'say' | 'claim'
V[+AUX] -> 'do' | 'can'
NP[-WH] -> 'you' | 'cats'
NP[+WH] -> 'who'
Adv[+NEG] -> 'rarely' | 'never'
NP/NP ->
Comp -> 'that'

3.1 中的语法包含一个“缺口引进”产生式,即S[-INV] -> NP S/NP。为了正确的预填充斜线特征,我们需要为扩展SVPNP的产生式中箭头两侧的斜线添加变量值。例如,VP/?x -> V SBar/?xVP -> V SBar的斜线版本,也就是说,可以为一个成分的父母VP指定斜线值,只要也为孩子SBar指定同样的值。最后,NP/NP ->允许NP上的斜线信息为空字符串。使用 3.1 中的语法,我们可以分析序列who do you claim that you like

>>> tokens = 'who do you claim that you like'.split()
>>> from nltk import load_parser
>>> cp = load_parser('grammars/book_grammars/feat1.fcfg')
>>> for tree in cp.parse(tokens):
...     print(tree)
(S[-INV]
 (NP[+WH] who)
 (S[+INV]/NP[]
 (V[+AUX] do)
 (NP[-WH] you)
 (VP[]/NP[]
 (V[-AUX, SUBCAT='clause'] claim)
 (SBar[]/NP[]
 (Comp[] that)
 (S[-INV]/NP[]
 (NP[-WH] you)
 (VP[]/NP[] (V[-AUX, SUBCAT='trans'] like) (NP[]/NP[] )))))))

这棵树的一个更易读的版本如(52)所示。

>>> tokens = 'you claim that you like cats'.split()
>>> for tree in cp.parse(tokens):
...     print(tree)
(S[-INV]
 (NP[-WH] you)
 (VP[]
 (V[-AUX, SUBCAT='clause'] claim)
 (SBar[]
 (Comp[] that)
 (S[-INV]
 (NP[-WH] you)
 (VP[] (V[-AUX, SUBCAT='trans'] like) (NP[-WH] cats))))))

此外,它还允许没有wh结构的倒装句:

>>> tokens = 'rarely do you sing'.split()
>>> for tree in cp.parse(tokens):
...     print(tree)
(S[-INV]
 (Adv[+NEG] rarely)
 (S[+INV]
 (V[+AUX] do)
 (NP[-WH] you)
 (VP[] (V[-AUX, SUBCAT='intrans'] sing))))

3.5 德语中的格和性别

与英语相比,德语的协议具有相对丰富的形态。例如,在德语中定冠词根据格、性别和数量变化,如 3.1 所示。

表 3.1:

德语定冠词的形态范式

>>> nltk.data.show_cfg('grammars/book_grammars/german.fcfg')
% start S
 # Grammar Productions
 S -> NP[CASE=nom, AGR=?a] VP[AGR=?a]
 NP[CASE=?c, AGR=?a] -> PRO[CASE=?c, AGR=?a]
 NP[CASE=?c, AGR=?a] -> Det[CASE=?c, AGR=?a] N[CASE=?c, AGR=?a]
 VP[AGR=?a] -> IV[AGR=?a]
 VP[AGR=?a] -> TV[OBJCASE=?c, AGR=?a] NP[CASE=?c]
 # Lexical Productions
 # Singular determiners
 # masc
 Det[CASE=nom, AGR=[GND=masc,PER=3,NUM=sg]] -> 'der'
 Det[CASE=dat, AGR=[GND=masc,PER=3,NUM=sg]] -> 'dem'
 Det[CASE=acc, AGR=[GND=masc,PER=3,NUM=sg]] -> 'den'
 # fem
 Det[CASE=nom, AGR=[GND=fem,PER=3,NUM=sg]] -> 'die'
 Det[CASE=dat, AGR=[GND=fem,PER=3,NUM=sg]] -> 'der'
 Det[CASE=acc, AGR=[GND=fem,PER=3,NUM=sg]] -> 'die'
 # Plural determiners
 Det[CASE=nom, AGR=[PER=3,NUM=pl]] -> 'die'
 Det[CASE=dat, AGR=[PER=3,NUM=pl]] -> 'den'
 Det[CASE=acc, AGR=[PER=3,NUM=pl]] -> 'die'
 # Nouns
 N[AGR=[GND=masc,PER=3,NUM=sg]] -> 'Hund'
 N[CASE=nom, AGR=[GND=masc,PER=3,NUM=pl]] -> 'Hunde'
 N[CASE=dat, AGR=[GND=masc,PER=3,NUM=pl]] -> 'Hunden'
 N[CASE=acc, AGR=[GND=masc,PER=3,NUM=pl]] -> 'Hunde'
 N[AGR=[GND=fem,PER=3,NUM=sg]] -> 'Katze'
 N[AGR=[GND=fem,PER=3,NUM=pl]] -> 'Katzen'
 # Pronouns
 PRO[CASE=nom, AGR=[PER=1,NUM=sg]] -> 'ich'
 PRO[CASE=acc, AGR=[PER=1,NUM=sg]] -> 'mich'
 PRO[CASE=dat, AGR=[PER=1,NUM=sg]] -> 'mir'
 PRO[CASE=nom, AGR=[PER=2,NUM=sg]] -> 'du'
 PRO[CASE=nom, AGR=[PER=3,NUM=sg]] -> 'er' | 'sie' | 'es'
 PRO[CASE=nom, AGR=[PER=1,NUM=pl]] -> 'wir'
 PRO[CASE=acc, AGR=[PER=1,NUM=pl]] -> 'uns'
 PRO[CASE=dat, AGR=[PER=1,NUM=pl]] -> 'uns'
 PRO[CASE=nom, AGR=[PER=2,NUM=pl]] -> 'ihr'
 PRO[CASE=nom, AGR=[PER=3,NUM=pl]] -> 'sie'
 # Verbs
 IV[AGR=[NUM=sg,PER=1]] -> 'komme'
 IV[AGR=[NUM=sg,PER=2]] -> 'kommst'
 IV[AGR=[NUM=sg,PER=3]] -> 'kommt'
 IV[AGR=[NUM=pl, PER=1]] -> 'kommen'
 IV[AGR=[NUM=pl, PER=2]] -> 'kommt'
 IV[AGR=[NUM=pl, PER=3]] -> 'kommen'
 TV[OBJCASE=acc, AGR=[NUM=sg,PER=1]] -> 'sehe' | 'mag'
 TV[OBJCASE=acc, AGR=[NUM=sg,PER=2]] -> 'siehst' | 'magst'
 TV[OBJCASE=acc, AGR=[NUM=sg,PER=3]] -> 'sieht' | 'mag'
 TV[OBJCASE=dat, AGR=[NUM=sg,PER=1]] -> 'folge' | 'helfe'
 TV[OBJCASE=dat, AGR=[NUM=sg,PER=2]] -> 'folgst' | 'hilfst'
 TV[OBJCASE=dat, AGR=[NUM=sg,PER=3]] -> 'folgt' | 'hilft'
 TV[OBJCASE=acc, AGR=[NUM=pl,PER=1]] -> 'sehen' | 'moegen'
 TV[OBJCASE=acc, AGR=[NUM=pl,PER=2]] -> 'sieht' | 'moegt'
 TV[OBJCASE=acc, AGR=[NUM=pl,PER=3]] -> 'sehen' | 'moegen'
 TV[OBJCASE=dat, AGR=[NUM=pl,PER=1]] -> 'folgen' | 'helfen'
 TV[OBJCASE=dat, AGR=[NUM=pl,PER=2]] -> 'folgt' | 'helft'
 TV[OBJCASE=dat, AGR=[NUM=pl,PER=3]] -> 'folgen' | 'helfen'

正如你可以看到的,特征objcase被用来指定动词支配它的对象的格。下一个例子演示了包含支配与格的动词的句子的分析树。

>>> tokens = 'ich folge den Katzen'.split()
>>> cp = load_parser('grammars/book_grammars/german.fcfg')
>>> for tree in cp.parse(tokens):
...     print(tree)
(S[]
 (NP[AGR=[NUM='sg', PER=1], CASE='nom']
 (PRO[AGR=[NUM='sg', PER=1], CASE='nom'] ich))
 (VP[AGR=[NUM='sg', PER=1]]
 (TV[AGR=[NUM='sg', PER=1], OBJCASE='dat'] folge)
 (NP[AGR=[GND='fem', NUM='pl', PER=3], CASE='dat']
 (Det[AGR=[NUM='pl', PER=3], CASE='dat'] den)
 (N[AGR=[GND='fem', NUM='pl', PER=3]] Katzen))))

在开发语法时,排除不符合语法的词序列往往与分析符合语法的词序列一样具有挑战性。为了能知道在哪里和为什么序列分析失败,设置load_parser()方法的trace参数可能是至关重要的。思考下面的分析故障:

>>> tokens = 'ich folge den Katze'.split()
>>> cp = load_parser('grammars/book_grammars/german.fcfg', trace=2)
>>> for tree in cp.parse(tokens):
...     print(tree)
|.ich.fol.den.Kat.|
Leaf Init Rule:
|[---]   .   .   .| [0:1] 'ich'
|.   [---]   .   .| [1:2] 'folge'
|.   .   [---]   .| [2:3] 'den'
|.   .   .   [---]| [3:4] 'Katze'
Feature Bottom Up Predict Combine Rule:
|[---]   .   .   .| [0:1] PRO[AGR=[NUM='sg', PER=1], CASE='nom']
 -> 'ich' *
Feature Bottom Up Predict Combine Rule:
|[---]   .   .   .| [0:1] NP[AGR=[NUM='sg', PER=1], CASE='nom'] -> PRO[AGR=[NUM='sg', PER=1], CASE='nom'] *
Feature Bottom Up Predict Combine Rule:
|[--->   .   .   .| [0:1] S[] -> NP[AGR=?a, CASE='nom'] * VP[AGR=?a] {?a: [NUM='sg', PER=1]}
Feature Bottom Up Predict Combine Rule:
|.   [---]   .   .| [1:2] TV[AGR=[NUM='sg', PER=1], OBJCASE='dat'] -> 'folge' *
Feature Bottom Up Predict Combine Rule:
|.   [--->   .   .| [1:2] VP[AGR=?a] -> TV[AGR=?a, OBJCASE=?c] * NP[CASE=?c] {?a: [NUM='sg', PER=1], ?c: 'dat'}
Feature Bottom Up Predict Combine Rule:
|.   .   [---]   .| [2:3] Det[AGR=[GND='masc', NUM='sg', PER=3], CASE='acc'] -> 'den' *
|.   .   [---]   .| [2:3] Det[AGR=[NUM='pl', PER=3], CASE='dat'] -> 'den' *
Feature Bottom Up Predict Combine Rule:
|.   .   [--->   .| [2:3] NP[AGR=?a, CASE=?c] -> Det[AGR=?a, CASE=?c] * N[AGR=?a, CASE=?c] {?a: [NUM='pl', PER=3], ?c: 'dat'}
Feature Bottom Up Predict Combine Rule:
|.   .   [--->   .| [2:3] NP[AGR=?a, CASE=?c] -> Det[AGR=?a, CASE=?c] * N[AGR=?a, CASE=?c] {?a: [GND='masc', NUM='sg', PER=3], ?c: 'acc'}
Feature Bottom Up Predict Combine Rule:
|.   .   .   [---]| [3:4] N[AGR=[GND='fem', NUM='sg', PER=3]] -> 'Katze' *

跟踪中的最后两个Scanner行显示den被识别为两个可能的类别:Det[AGR=[GND='masc', NUM='sg', PER=3], CASE='acc']Det[AGR=[NUM='pl', PER=3], CASE='dat']。我们从 3.2 中的语法知道Katze的类别是N[AGR=[GND=fem, NUM=sg, PER=3]]。因而,产生式NP[CASE=?c, AGR=?a] -> Det[CASE=?c, AGR=?a] N[CASE=?c, AGR=?a]中没有变量?a的绑定,这将满足这些限制,因为KatzeAGR值将不与den的任何一个AGR值统一,也就是[GND='masc', NUM='sg', PER=3][NUM='pl', PER=3]

4 小结

  • 上下文无关语法的传统分类是原子符号。特征结构的一个重要的作用是捕捉精细的区分,否则将需要数量翻倍的原子类别。
  • 通过使用特征值上的变量,我们可以表达语法产生式中的限制,允许不同的特征规格的实现可以相互依赖。
  • 通常情况下,我们在词汇层面指定固定的特征值,限制短语中的特征值与它们的孩子中的对应值统一。
  • 特征值可以是原子的或复杂的。原子值的一个特定类别是布尔值,按照惯例用[+/- f]表示。
  • 两个特征可以共享一个值(原子的或复杂的)。具有共享值的结构被称为重入。共享的值被表示为 AVM 中的数字索引(或标记)。
  • 一个特征结构中的路径是一个特征的元组,对应从图的根开始的弧的序列上的标签。
  • 两条路径是等价的,如果它们共享一个值。
  • 包含的特征结构是偏序的。FS[0]包含FS[1],当包含在FS[0]中的所有信息也出现在FS[1]中。
  • 两种结构FS[0]FS[1]的统一,如果成功,就是包含FS[0]FS[1]的合并信息的特征结构FS[2]
  • 如果统一在 FS 中指定一条路径π,那么它也指定等效与π的每个路径π'
  • 我们可以使用特征结构建立对大量广泛语言学现象的简洁的分析,包括动词子类别,倒装结构,无限制依赖结构和格支配。

5 深入阅读

本章进一步的材料请参考http://nltk.org/,包括特征结构、特征语法和语法测试套件。

X-bar 句法:(Jacobs & Rosenbaum, 1970), (Jackendoff, 1977)(我们使用素数替代了 Chomsky 印刷上要求更高的单杠)。

协议现象的一个很好的介绍,请参阅(Corbett, 2006)。

理论语言学中最初使用特征的目的是捕捉语音的音素特性。例如,音/b/可能会被分解成结构[+labial, +voice]。一个重要的动机是捕捉分割的类别之间的一般性;例如/n/在任一+labial辅音前面被读作/m/。在乔姆斯基语法中,对一些现象,如协议,使用原子特征是很标准的,原子特征也用来捕捉跨句法类别的概括,通过类比与音韵。句法理论中使用特征的一个激进的扩展是广义短语结构语法(GPSG; (Gazdar, Klein, & and, 1985)),特别是在使用带有复杂值的特征。

从计算语言学的角度来看,(Dahl & Saint-Dizier, 1985)提出语言的功能方面可以被属性-值结构的统一捕获,一个类似的方法由(Grosz & Stickel, 1983)在 PATR-II 形式体系中精心设计完成。词汇功能语法(LFG; (Bresnan, 1982))的早期工作介绍了 F 结构的概念,它的主要目的是表示语法关系和与成分结构短语关联的谓词参数结构。(Shieber, 1986)提供了研究基于特征语法方面的一个极好的介绍。

当研究人员试图为反面例子建模时,特征结构的代数方法的一个概念上的困难出现了。另一种观点,由(Kasper & Rounds, 1986)和(Johnson, 1988)开创,认为语法涉及结构功能的描述而不是结构本身。这些描述使用逻辑操作如合取相结合,而否定仅仅是特征描述上的普通的逻辑运算。这种面向描述的观点对 LFG 从一开始就是不可或缺的(参见(Huang & Chen, 1989)),也被中心词驱动短语结构语法的较高版本采用(HPSG; (Sag & Wasow, 1999))。http://www.cl.uni-bremen.de/HPSG-Bib/上有 HPSG 文献的全面的参考书目。

本章介绍的特征结构无法捕捉语言信息中重要的限制。例如,有没有办法表达NUM的值只允许是sgpl,而指定[NUM=masc]是反常的。同样地,我们不能说AGR的复合值必须包含特征PERNUMgnd的指定,但不能包含如[SUBCAT=trans]这样的指定。指定类型的特征结构被开发出来弥补这方面的不足。开始,我们规定总是键入特征值。对于原子值,值就是类型。例如,我们可以说NUM的值是类型num。此外,numNUM最一般类型的值。由于类型按层次结构组织,通过指定NUM的值为num的子类型,即要么是sg要么是pl,我们可以更富含信息。

In the case of complex values, we say that feature structures are themselves typed. So for example the value of AGR will be a feature structure of type AGR. We also stipulate that all and only PER, NUM and GND are appropriate features for a structure of type AGR. 一个早期的关于指定类型的特征结构的很好的总结是(Emele & Zajac, 1990)。一个形式化基础的更全面的检查可以在(Carpenter, 1992)中找到,(Copestake, 2002)重点关注为面向 HPSG 的方法实现指定类型的特征结构。

有很多著作是关于德语的基于特征语法框架上的分析的。(Nerbonne, Netter, & Pollard, 1994)是这个主题的 HPSG 著作的一个好的起点,而(Muller, 2002)给出 HPSG 中的德语句法非常广泛和详细的分析。

(Jurafsky & Martin, 2008)的第 15 章讨论了特征结构、统一的算法和将统一整合到分析算法中。

6 练习

  1. ☼ 需要什么样的限制才能正确分析词序列,如I am happyshe is happy而不是you is happythey am happy?实现英语中动词be的现在时态范例的两个解决方案,首先以语法(6)作为起点,然后以语法 (18)为起点。

  2. ☼ 开发 1.1 中语法的变体,使用特征count来区分下面显示的句子:

    fs1 = nltk.FeatStruct("[A = ?x, B= [C = ?x]]")
    fs2 = nltk.FeatStruct("[B = [D = d]]")
    fs3 = nltk.FeatStruct("[B = [C = d]]")
    fs4 = nltk.FeatStruct("[A = (1)[B = b], C->(1)]")
    fs5 = nltk.FeatStruct("[A = (1)[D = ?x], C = [E -> (1), F = ?x] ]")
    fs6 = nltk.FeatStruct("[A = [D = d]]")
    fs7 = nltk.FeatStruct("[A = [D = d], C = [F = [D = d]]]")
    fs8 = nltk.FeatStruct("[A = (1)[D = ?x, G = ?x], C = [B = ?x, E -> (1)] ]")
    fs9 = nltk.FeatStruct("[A = [B = b], C = [E = [G = e]]]")
    fs10 = nltk.FeatStruct("[A = (1)[B = b], C -> (1)]")
    

    在纸上计算下面的统一的结果是什么。(提示:你可能会发现绘制图结构很有用。)

    1. fs1 and fs2
    2. fs1 and fs3
    3. fs4 and fs5
    4. fs5 and fs6
    5. fs5 and fs7
    6. fs8 and fs9
    7. fs8 and fs10

    用 Python 检查你的答案。

  3. ◑ 列出两个包含[A=?x, B=?x]的特征结构。

  4. ◑ 忽略结构共享,给出一个统一两个特征结构的非正式算法。

  5. ◑ 扩展 3.2 中的德语语法,使它能处理所谓的动词第二顺位结构,如下所示:

    Heute sieht der Hund die Katze. (58)

  6. ◑ 同义动词的句法属性看上去略有不同(Levin, 1993)。思考下面的动词loadedfilleddumped的语法模式。你能写语法产生式处理这些数据吗?

    (59)
    
    a.  The farmer *loaded* the cart with sand 
    
    b.  The farmer *loaded* sand into the cart 
    
    c.  The farmer *filled* the cart with sand 
    
    d.  *The farmer *filled* sand into the cart 
    
    e.  *The farmer *dumped* the cart with sand 
    
    f.  The farmer *dumped* sand into the cart 
    
  7. ★ 形态范例很少是完全正规的,矩阵中的每个单元的意义有不同的实现。例如,词位walk的现在时态词性变化只有两种不同形式:第三人称单数的walks和所有其他人称和数量的组合的walk。一个成功的分析不应该额外要求 6 个可能的形态组合中有 5 个有相同的实现。设计和实施一个方法处理这个问题。

  8. ★ 所谓的核心特征在父节点和核心孩子节点之间共享。例如,TENSE是核心特征,在一个VP和它的核心孩子V之间共享。更多细节见(Gazdar, Klein, & and, 1985)。我们看到的结构中大部分是核心结构——除了SUBCATSLASH。由于核心特征的共享是可以预见的,它不需要在语法产生式中明确表示。开发一种方法自动计算核心结构的这种规则行为的比重。

  9. ★ 扩展 NLTK 中特征结构的处理,允许统一值为列表的特征,使用这个来实现一个 HPSG 风格的子类别分析,核心类别的SUBCAT是它的补语的类别和它直接父母的SUBCAT值的连结。

  10. ★ 扩展 NLTK 的特征结构处理,允许带未指定类别的产生式,例如S[-INV] --> ?x S/?x

  11. ★ 扩展 NLTK 的特征结构处理,允许指定类型的特征结构。

  12. ★ 挑选一些(Huddleston & Pullum, 2002)中描述的文法结构,建立一个基于特征的语法计算它们的比例。

关于本文档...

针对 NLTK 3.0 进行更新。本章来自于《Python 自然语言处理》,Steven Bird, Ewan KleinEdward Loper,Copyright © 2014 作者所有。本章依据 Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 United States License 条款,与自然语言工具包 3.0 版一起发行。

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



回到顶部