10. 分析句子的意思

我们已经看到利用计算机的能力来处理大规模文本是多么有用。现在我们已经有了分析器和基于特征的语法,我们能否做一些类似分析句子的意思这样有用的事情?本章的目的是要回答下列问题:

  1. 我们如何能表示自然语言的意思,使计算机能够处理这些表示?
  2. 我们怎样才能将意思表示与无限的句子集合关联?
  3. 我们怎样才能使用程序来连接句子的意思表示到知识的存储?

一路上,我们将学习一些逻辑语义领域的形式化技术,看看如何用它们来查询存储了世间真知的数据库。

1 自然语言理解

1.1 查询数据库

假设有一个程序,让我们输入一个自然语言问题,返回给我们正确的答案:

(1)

a.Which country is Athens in?

b.Greece.

写这样的一个程序有多难?我们可以使用到目前为止在这本书中遇到的技术吗,或者需要新的东西?在本节中,我们将看到解决特定领域的任务是相当简单的。但我们也将看到,要以一个更一般化的方式解决这个问题,就必须开辟一个全新的涉及意思的表示的理念和技术的框架。

因此,让我们开始先假设我们有关于城市和国家的结构化的数据。更具体的,我们将使用一个具有表1.1所示的前几行的数据库表。

注意

1.1中所示的数据来自Chat-80系统(Warren & Pereira, 1982)人口数以千计算,注意在这些例子中使用的数据可以追溯到1980年代,在(Warren & Pereira, 1982)出版时,某些已经有些过时。

表 1.1

city_table:一个城市、国家和人口的表格

城市国家人口
athensgreece1368
bangkokthailand1178
barcelonaspain1280
berlineast_germany3481
birminghamunited_kingdom1112

从这个表格数据检索答案的最明显的方式是在一个如SQL这样的数据库查询语言中编写查询语句。

注意

SQL(结构化查询语言)是为在关系数据库中检索和管理数据而设计的语言。如果你想了解更多有关SQL的信息,http://www.w3schools.com/sql/是一个方便的在线参考。

例如,执行查询(2)将得到值'greece'

(2)SELECT Country FROM city_table WHERE City = 'athens'

这条语句得到由所有City列的值是'athens'的数据行中Country列的所有值组成的结果的集合。

我们怎样才能使用英语得到与我们在查询系统中输入得到的相同的效果呢?9.中描述的基于特征的语法形式可以很容易地从英语翻译到SQL。语法sql0.fcfg说明如何将句子意思表示与句子分析串联组装。每个短语结构规则为特征sem构建值作补充。你可以看到这些补充非常简单;在每一种情况下,我们对分割的子成分用字符串连接操作+来组成父成分的值。

>>> nltk.data.show_cfg('grammars/book_grammars/sql0.fcfg')
% start S
S[SEM=(?np + WHERE + ?vp)] -> NP[SEM=?np] VP[SEM=?vp]
VP[SEM=(?v + ?pp)] -> IV[SEM=?v] PP[SEM=?pp]
VP[SEM=(?v + ?ap)] -> IV[SEM=?v] AP[SEM=?ap]
NP[SEM=(?det + ?n)] -> Det[SEM=?det] N[SEM=?n]
PP[SEM=(?p + ?np)] -> P[SEM=?p] NP[SEM=?np]
AP[SEM=?pp] -> A[SEM=?a] PP[SEM=?pp]
NP[SEM='Country="greece"'] -> 'Greece'
NP[SEM='Country="china"'] -> 'China'
Det[SEM='SELECT'] -> 'Which' | 'What'
N[SEM='City FROM city_table'] -> 'cities'
IV[SEM=''] -> 'are'
A[SEM=''] -> 'located'
P[SEM=''] -> 'in'

这使我们能够分析SQL查询:

>>> from nltk import load_parser
>>> cp = load_parser('grammars/book_grammars/sql0.fcfg')
>>> query = 'What cities are located in China'
>>> trees = list(cp.parse(query.split()))
>>> answer = trees[0].label()['SEM']
>>> answer = [s for s in answer if s]
>>> q = ' '.join(answer)
>>> print(q)
SELECT City FROM city_table WHERE Country="china"

注意

轮到你来:设置跟踪为最大,运行分析器,即cp = load_parser('grammars/book_grammars/sql0.fcfg', trace=3),研究当边被完整的加入到图表中时,如何建立sem的值。

最后,我们在数据库city.db上执行查询,检索出一些结果:

>>> from nltk.sem import chat80
>>> rows = chat80.sql_query('corpora/city_database/city.db', q)
>>> for r in rows: print(r[0], end=" ") [1]
canton chungking dairen harbin kowloon mukden peking shanghai sian tientsin

由于每行r是一个单元素的元组,我们输出元组的成员,而不是元组本身[1]

总结一下,我们已经定义了一个任务:计算机对自然语言查询做出反应,返回有用的数据。我们通过将英语的一个小的子集翻译成SQL来实现这个任务们可以说,我们的NLTK代码已经“理解”SQL,只要Python 能够对数据库执行SQL 查询,通过扩展,它也“理解”如What cities are located in China这样的查询。这相当于自然语言理解的例子能够从荷兰语翻译成英语。假设你是一个英语为母语的人,已经开始学习荷兰语。你的老师问你是否理解(3)的意思:

(3)Margrietje houdt van Brunoke.

如果你知道(3)中单个词的意思,并且知道如何将它们结合在一起组成整个句子的意思,你可能会说(3)的意思与Margrietje loves Brunoke相同。

一个观察者——让我们叫她Olga——可能会将此作为你理解了(3)的意思的证据。但是,这将依赖于Olga自己懂英语。如果她不懂英语,那么你从荷兰语到英语的翻译就不能说明你理解了荷兰语。我们将很快回到这个问题。

语法sql0.fcfg,连同NLTK的Earley分析器是实现从英语翻译到SQL的工具。这个语法够用吗?你已经看到整个句子的SQL翻译是由句子成分的翻译建立起来的。然而,这些成分的意思表示似乎没有很多的合理性。例如,如果我们看一下名词短语Which cities的分析,限定词和名词分别对应SQL片段SELECTCity FROM city_table但这两个都没有单独的符合语法的意思。

我们对这个语法还有另一种批评:我们“硬生生”的把一些数据库的细节加入其中。我们需要知道有关表(如city_table)和字段的名称。但我们的数据库中可能确实存在相同的数据行但是用的是不同的表名和字段名,在这种情况下,SQL查询就不能执行。同样,我们可以不同的格式存储我们的数据,如XML,在这种情况下,检索相同的结果需要我们将我们的英语查询翻译成XML查询语言而不是SQL。这些因素表明我们应该将英语翻译成比SQL更加抽象和通用的东西。

为了突出这一点,让我们考虑另一个英语查询及其翻译:

(4)

a.What cities are in China and have populations above 1,000,000?

b.SELECT City FROM city_table WHERE Country = 'china' AND Population > 1000

注意

轮到你来:扩展语法sql0.fcfg使它能将(4a)转换为(4b),检查查询所返回的值。

你可能会发现,最简单的方法是在一起处理之前扩展语法来处理查询What cities have populations above 1,000,000在你完成了这个任务之后,你可以比较你的方案与NLTK数据格式的grammars/book_grammars/sql1.fcfg

观察(4a)and连接被翻译成(4b)中SQL对应的AND后者告诉我们从两个条件都为真的行中选择结果:Country列的值是'china'Population列的值是大于1000。and的解释包含一个新的想法:它讲的是在某些特定情况下什么是真的,告诉我们在条件sCond1 AND Cond2为真,当且仅当条件Cond1s中为真且Cond2也在s中为真。虽然这并不是and在英语中的全部意思,但它具有很好的属性,就是独立于任何查询语言。事实上,我们已经给了它一个经典逻辑的标准解释。在下面的章节中,我们将探讨将自然语言的句子翻译成逻辑而不是如SQL这样的可执行查询语言的方法。逻辑形式的一个优势是它们更抽象,因此更通用。一旦我们翻译成了逻辑,只要我们想要,就可以再翻译成其他各种特殊用途的语言。事实上,大多数通过自然语言查询数据库的重要的尝试都是使用这种方法。

1.2 自然语言、语义和逻辑

我们一开始尝试捕捉(1a)的意思,通过将它翻译成计算机可以解释和执行的另一种语言,SQL,中的查询。但是,这仍然在回避问题的实质:翻译是否正确。从数据库查询走出来,我们注意到and的意思似乎可以用来指定在特定情况下语句是真或是假。不是将句子S从一种语言翻译到另一种,我们通过将S与现实的情况关联,尝试解释它是关于什么的。让我们更进一步。假设有一种情况s,其中有两个实体Margrietje和她最喜欢的娃娃Brunoke。此外,两个实体之间有一种我们称之为love的关系。如果你理解(3)的意思,那么你知道在情况s中它为真。你知道这个的部分原因是因为你知道Margrietje指的是Margrietje,Brunoke指的是Brunoke,houdt van指的是love关系。

我们引进了语义中的两个基本概念。第一个是在某些情况下,陈述句非真即假第二个是名词短语和专有名词的定义指的是世界上的东西所以(3)在Margrietje喜欢娃娃Brunoke这一情形中为真,如1.1所示。

../images/mimo-and-bruno.png

图 1.1:对Margrietje喜欢Brunoke的情形的描绘

一旦我们采取了在一个情况下是真是假的概念,我们就有了一个强大的进行推理的工具。特别是,我们可以看到句子的集合,询问它们在某些情况下是否为真。例如,(5)中的句子可以都为真,而(6)(7)中的则不能。换句话说,(5)中的句子是一致的,而(6)(7)中的是不一致的

(5)

a.Sylvania is to the north of Freedonia.

b.Freedonia is a republic.

(6)

a.The capital of Freedonia has a population of 9,000.

b.No city in Freedonia has a population of 9,000.

(7)

a.Sylvania is to the north of Freedonia.

b.Freedonia is to the north of Sylvania.

我们选择有关虚构的国家(选自Marx Brothers 1933 年的电影Duck Soup)的句子来强调你对这些例子的推理并不取决于真实世界中的真与假。如果你知道词no的意思,也知道一个国家的首都是一个位于该国的城市,那么你应该能够得出这样的结论:(6)中的两个句子是不一致的,不管Freedonia在哪里和它的首都有多少人口。也就是说,不存在使这两个句子同时都为真的情况。同样的,如果你知道to the north of所表达的关系是不对称的,那么你应该能够得出这样的结论:(7)中的两句话是不一致的。

从广义上讲,自然语言语义表示的基于逻辑的方法关注那些指导我们判断自然语言的一致性和不一致性的方面。设计一种逻辑语言的句法是为了使这些特征形式更明确。结果是如一致性这样的确定性属性往往可以简化成符号操作,也就是说,一种可以被计算机实施的任务。为了实现这种方法,我们首先要开发一种表示某种可能情况的技术。我们做的这些逻辑学家称之为模型。

一个句子集W模型是某种情况的形式化表示,其中W中的所有句子都为真。表示模型通常的方式是集合论。段落的域D(我们当前关心的所有实体)是个体的一个集合,而当集合从D建立,关系也被确立。让我们看一个具体的例子。我们的域D包括3 个孩子,Stefan、Klaus 和Evi,分别用ske表示。我们将它写作D = {s, k, e}表达式boy是包含Stefan和Klaus的集合,表达式girl是包含Evi的集合,表达式is running是包含Stefan和Evi 的集合。1.2是这个模型的图形化描绘。

../images/model_kids.png

图 1.2:一个模型图,包含一个域DD的子集分别对应谓词boygirlis running

在本章后面,我们将使用模型来帮助评估英语句子的真假,并用这种方式来说明表示意思的一些方法。然而,在进入更多细节之前,让我们将从更广阔的角度进行讨论,回到我们在5简要提到过的主题。一台计算机可以理解句子的意思吗?我们该如何判断它是否能理解?这类似与问“计算机能思考吗?”阿兰·图灵提出的著名的回答是:通过检查计算机与人类进行理智的对话的能力(Turing, 1950)假设你有一个与人聊天的会话和一个与计算机聊天的会话,但一开始你并不知道哪个是哪个。在与它们两个聊天后,如果你不能识别对方哪一个是计算机,那么计算机就成功地模仿了人类。如果一台计算机成功的被当做人类通过了这个“模仿游戏”(或“图灵测试”,这是它是俗称),那么对于图灵来说,就可以说计算机思考,可以说它具有了智能。所以图灵从侧面回答了这个问题,不是检查计算机的内部状态,而是检查它的行为,作为具有智能的证据。同样的道理,我们认为要说一台计算机懂英语,只需要它的行为表现看上去它懂。这里最重要的是不是图灵模仿游戏的细节,而是以可观察的行为为依据来判断自然语言理解能力的想法。

2 命题逻辑

设计一种逻辑语言的目的是使推理形式更明确。因此,它可以在自然语言中确定一组句子是否是一致的。作为这种方法的一部分,我们需要开发一个句子φ的逻辑表示,它能形式化的捕捉φ为真的条件我们先从一个简单的例子开始:

(8)[Klaus chased Evi] and [Evi ran away].

让我们分别用φ和ψ替代(8)中的两个子句,并用&替代对应的英语词and的逻辑操作: φ & ψ。这种结构是(8)逻辑形式

命题逻辑使我们能只表示语言结构的对应与句子的特定连接词的那些部分。刚才我们看了and其他的连接词还有notorif..., then...命题逻辑形式中,这些连接词的对应形式有时叫做布尔运算符命题逻辑的基本表达式是命题符号,通常写作PQR等。表示布尔运算符的约定很多。由于我们将重点探索NLTK中的逻辑表示方式,所以将使用下列ASCII版本的运算符:

>>> nltk.boolean_ops()
negation            -
conjunction         &
disjunction         |
implication         ->
equivalence         <->

从命题符号和布尔运算符,我们可以建立命题逻辑的规范公式(或简称公式)的无限集合。首先,每个命题字母是一个公式。然后,如果φ是一个公式,那么-φ也是一个公式。如果φ和ψ是公式,那么(φ & ψ) (φ | ψ) (φ -> ψ) (φ <-> ψ)也是公式。

2.1指定了包含这些运算符的公式为真的条件。和以前一样,我们使用φ和ψ作为句子中的变量,iff作为if and only if(当且仅当)的缩写。

表 2.1

命题逻辑的布尔运算符的真值条件。

布尔运算符真值条件
非(it is not the case that ...)s-φ为真iffs中φ为假
与(and)s(φ & ψ)为真iffs中φ为真并且在s中ψ为真
或(or)s(φ | ψ)为真iffs中φ为真或者在s中ψ为真
蕴含 (if ..., then ...)s(φ -> ψ)为真iffs中φ为假或者在s中ψ为真
等价(if and only if)s(φ <-> ψ)为真iffφ和ψ在s中同时为真或者在s中同时为假

这些规则通常是简单的,虽然蕴含的真值条件违反了很多我们通常关于英语中条件句的直觉。形式(P -> Q)的公式是为假只有当P为真并且Q为假时。如果P为假(比如说,P对应The moon is made of green cheese)而Q为真(比如说,Q对应Two plus two equals four)那么P -> Q的结果为真。

NLTK的Expression对象可以将逻辑表达式分析成Expression的各种子类:

>>> read_expr = nltk.sem.Expression.fromstring
>>> read_expr('-(P & Q)')
<NegatedExpression -(P & Q)>
>>> read_expr('P & Q')
<AndExpression (P & Q)>
>>> read_expr('P | (R -> Q)')
<OrExpression (P | (R -> Q))>
>>> read_expr('P <-> -- P')
<IffExpression (P <-> --P)>

从计算的角度来看,逻辑给了我们进行推理的一个重要工具。假设你表达Freedonia is not to the north of Sylvania,而你给出理由Sylvania is to the north of Freedonia。在这种情况下,你已经给出了一个论证句子Sylvania is to the north of Freedonia是论证的假设,而Freedonia is not to the north of Sylvania结论从假设一步一步推到结论,被称为推理通俗地说,就是我们以在结论前面写therefore这样的格式写一个论证。

(9)
Sylvania is to the north of Freedonia.
Therefore, Freedonia is not to the north of Sylvania

一个论证是有效的,如果没有它的所有的前提都为真而结论为假的情况。

现在,(9)的有效性关键取决于短语to the north of的含义,特别的,它实际上是一个非对称的关系:

(10)if x is to the north of y then y is not to the north of x.

不幸的是,在命题逻辑中,我们不能表达这样的规则:我们能用的最小的元素是原子命题,我们不能“向里看”来谈论个体xy之间的关系。在这种情况下,我们可以做的最好的是捕捉不对称的一个特定案例。让我们使用命题符号SnF表示Sylvania is to the north of Freedonia,用FnS 表示Freedonia is to the north of Sylvania要说Freedonia is not to the north of Sylvania,我们写成-FnS。也就是说,我们将not当做短语it is not the case that ...的等价,用一元布尔运算符-来翻译这个。于是现在我们可以将(10)中的蕴含写作:

(11)SnF -> -FnS

给出一个完整的论证版本会怎样?我们将(9)的第一句话为两个命题逻辑公式:SnF(11)中的蕴含,它表示(相当贫乏的)我们的背景知识to the north of的意思。我们将书写[A1, ..., An] / C来表示一个从假设[A1, ..., An]得出结论C的论证。这使得论证(9)可以表示为:

(12)[SnF, SnF -> -FnS] / -FnS

这是一个有效的论证:如果SnFSnF -> -FnS在情况s中都为真,那么-FnS也一定在s中为真。相反,如果FnS为真,这将与我们的理解冲突:在任何可能的情况下两个事物不能同时在对方的北边。同样的,序列[SnF, SnF -> -FnS, FnS]是不一致的——这些句子不能全为真。

论证可以通过使用证明系统来测试“句法有效性”。在稍后的3中我们会再多讲一些这个。NLTK的inference模块通过一个第三方定理证明器Prover9 的接口,可以进行逻辑证明。推理机制的输入首先必须转换成逻辑表达式。

>>> lp = nltk.sem.Expression.fromstring
>>> SnF = read_expr('SnF')
>>> NotFnS = read_expr('-FnS')
>>> R = read_expr('SnF -> -FnS')
>>> prover = nltk.Prover9()
>>> prover.prove(NotFnS, [SnF, R])
True

这里有另一种方式可以看到结论如何得出。SnF -> -FnS在语义上等价于-SnF | -FnS,其中 "|"是对应于or的二元运算符。在一般情况下,φ|ψ在条件s中为真,要么φ在s中为真,要么ψ在s中为真。现在,假设SnF-SnF | -FnS都在s中为真。如果SnF为真,那么-SnF不可能也为真;经典逻辑的一个基本假设是:一个句子在一种情况下不能同时为真和为假。因此,-FnS必须为真。

回想一下,我们解释相对于一个模型的一种逻辑语言的句子,它们是这个世界的一个非常简化的版本。一个命题逻辑的模型需要为每个可能的公式分配值TrueFalse我们一步步的来做这个:首先,为每个命题符号分配一个值,然后确定布尔运算符的含义(即2.1)和运用它们到这些公式的组件的值,来计算复杂的公式的值。估值是从逻辑的基本符号映射到它们的值。下面是一个例子:

>>> val = nltk.Valuation([('P', True), ('Q', True), ('R', False)])

我们使用一个配对的链表初始化一个估值,每个配对由一个语义符号和一个语义值组成。所产生的对象基本上只是一个字典,映射逻辑符号(作为字符串处理)为适当的值。

>>> val['P']
True

正如我们稍后将看到的,我们的模型需要稍微更加复杂些,以便处理将在下一节中讨论的更复杂的逻辑形式;暂时的,在下面的声明中先忽略参数domg

>>> dom = set()
>>> g = nltk.Assignment(dom)

现在,让我们用val初始化模型m

>>> m = nltk.Model(dom, val)

每一个模型都有一个evaluate()方法,可以确定逻辑表达式,如命题逻辑的公式,的语义值;当然,这些值取决于最初我们分配给命题符号如PQR的真值。

>>> print(m.evaluate('(P & Q)', g))
True
>>> print(m.evaluate('-(P & Q)', g))
False
>>> print(m.evaluate('(P & R)', g))
False
>>> print(m.evaluate('(P | R)', g))
True

注意

轮到你来:做实验为不同的命题逻辑公式估值。模型是否给出你所期望的值?

到目前为止,我们已经将我们的英文句子翻译成命题逻辑。因为我们只限于用字母如PQ表示原子句子,不能深入其内部结构。实际上,我们说将原子句子分成主语、宾语和谓词并没有语义上的好处。然而,这似乎是错误的:如果我们想形式化如(9)这样的论证,就必须要能“看到里面”基本的句子。因此,我们将超越命题逻辑到一个更有表现力的东西,也就是一阶逻辑。这正是我们下一节要讲的。

3 一阶逻辑

本章的剩余部分,我们将通过翻译自然语言表达式为一阶逻辑来表示它们的意思。并不是所有的自然语言语义都可以用一阶逻辑表示。但它是计算语义的一个不错的选择,因为它具有足够的表现力来表达语义的很多方面,而且另一方面,有出色的现成系统可用于开展一阶逻辑自动推理。

下一步我们将描述如何构造一阶逻辑公式,然后是这样的公式如何用来评估模型。

3.1 句法

一阶逻辑保留所有命题逻辑的布尔运算符。但它增加了一些重要的新机制。首先,命题被分析成谓词和参数,这将我们与自然语言的结构的距离拉近了一步。一阶逻辑的标准构造规则承认以下术语:独立变量和独立常量、带不同数量的参数的谓词例如,Angus walks可以被形式化为walk(angus)Angus sees Bertie可以被形式化为see(angus, bertie)我们称walk一元谓词see二元谓词作为谓词使用的符号不具有内在的含义,虽然很难记住这一点。回到我们前面的一个例子,(13a)(13b)之间没有逻辑区别。

(13)

a.love(margrietje, brunoke)

b.houden_van(margrietje, brunoke)

一阶逻辑本身没有什么实质性的关于词汇语义的表示——单个词的意思——虽然一些词汇语义理论可以用一阶逻辑编码。原子谓词如see(angus, bertie)在某种情况中是真还是假不是一个逻辑的问题,而是依赖于特定的估值,即我们为常量seeangusbertie选择的值。出于这个原因,这些表达式被称为非逻辑常量相比之下,逻辑常量(如布尔运算符)在一阶逻辑的每个模型中的解释总是相同的。

我们应该在这里提到:有一个二元谓词具有特殊的地位,它就是等号,如在angus = aj这样的公式中的等号。等号被视为一个逻辑常量,因为对于单独的术语t1t2,公式t1 = t2 为真当且仅当t1t2是指同一个实体。

检查一阶逻辑表达式的语法结构往往是有益的,这样做通常的方式是为表达式指定类型下面是Montague语法的约定,我们将使用两个基本类型e是实体类型,而t是公式类型,即有真值的表达式的类型。给定这两种基本类型,我们可以形成函数表达式的复杂类型也就是说,给定任何类型σ和τ,〈σ, τ〉是一个对应与从'σ things’到'τ things’的函数的复杂类型。例如,〈e, t〉是从实体到真值,即一元谓词,的表达式的类型。逻辑表达式可以通过类型检查处理。

>>> read_expr = nltk.sem.Expression.fromstring
>>> expr = read_expr('walk(angus)', type_check=True)
>>> expr.argument
<ConstantExpression angus>
>>> expr.argument.type
e
>>> expr.function
<ConstantExpression walk>
>>> expr.function.type
<e,?>

为什么我们在这个例子的结尾看到<e,?>呢?虽然类型检查器会尝试推断出尽可能多的类型,在这种情况下,它并没有能够推断出walk的类型,所以其结果的类型是未知的。虽然我们期望walk的类型是<e, t>,迄今为止类型检查器知道的,在这个上下文中可能是一些其他类型,如<e, e><e, <e, t>要帮助类型检查器,我们需要指定一个信号,作为一个字典来实施,明确的与非逻辑常量类型关联:

>>> sig = {'walk': '<e, t>'}
>>> expr = read_expr('walk(angus)', signature=sig)
>>> expr.function.type
e

一种二元谓词具有类型〈e, 〈e, t〉〉。虽然这是先组合类型e的一个参数成一个一元谓词的类型,我们可以用二元谓词的两个参数直接组合来表示二元谓词。例如,在Angus sees Cyril的翻译中谓词see会与它的参数结合得到结果see(angus, cyril)

在一阶逻辑中,谓词的参数也可以是独立变量,如xyz在NLTK中,我们采用的惯例:e类型的变量都是小写。独立变量类似于人称代词,如hesheit ,其中我们为了弄清楚它们的含义需要知道它们使用的上下文。

解释(14)中的代名词的方法之一是指向上下文中相关的个体。

(14)He disappeared.

另一种方法是为代词he提供文本中的先行词,例如通过指出(15a)(14)之前。在这里,我们说he与名词短语Cyril指称相同。在这个上下文中,(14)(15b)是语义上是等价的。

(15)

a.Cyril is Angus's dog.

b.Cyril disappeared.

相比之下,思考(16a)中出现的he在这种情况下,它受不确定的NP a dog约束,这是一个与共指关系不同的关系。如果我们替换代词hea dog,结果(16b)就在语义上就等效于(16a)

(16)

a.Angus had a dog but he disappeared.

b.Angus had a dog but a dog disappeared.

对应于(17a),我们可以构建一个开放公式(17b),变量x出现了两次。(我们忽略了时态,以简化论述。)

(17)

a.He is a dog and he disappeared.

b.dog(x)disappear(x)

通过在(17b)前面指定一个存在量词x (“对于某个x”) ,我们可以绑定这些变量,如在(18a)中,它的意思是(18b),或者更习惯写法(18c)

(18)

a.x.(dog(x)disappear(x))

b.At least one entity is a dog and disappeared.

c.A dog disappeared.

下面是(18a)在NLTK中的表示:

(19)exists x.(dog(x) & disappear(x))

除了存在量词,一阶逻辑为我们提供了全称量词x(“对于所有x”),如(20)所示。

(20)

a.x.(dog(x)disappear(x))

b.Everything has the property that if it is a dog, it disappears.

c.Every dog disappeared.

下面是(20a)在NLTK中的表示:

(21)all x.(dog(x) -> disappear(x))

虽然(20a)(20c)的标准一阶逻辑翻译,其真值条件不一定是你所期望的。公式表示如果某个x是狗,那么x消失——但它并没有说有狗存在。在没有狗存在的情况下,(20a)仍然会为真。(请记住,当P为假时,(P -> Q)为真。)现在你可能会说,每条的狗都消失是以狗的存在为前提条件的,逻辑形式化表示的是完全错误的。但也可能找到其他的例子,没有这样一个前提。例如,我们也许可以解释Python表达式astring.replace('ate', '8')是替代astring中出现的所有'ate''8',即使事实上可能没有出现(3.2

我们已经看到了一些量词约束变量的例子。下面的公式中会发生什么?:

((exists x. dog(x)) -> bark(x))

量词exists x的范围是dog(x),所以bark(x)x的出现是不受限制的。因此,它可以被其他一些量词约束,例如下面公式中的all x

all x.((exists x. dog(x)) -> bark(x))

在一般情况下,变量x在公式φ中出现是自由的,如果它在φ中没有出现在all xsome x范围内。相反,如果x在公式φ中是受限的,那么它在all x..φ和exists x.限制范围内。如果公式中所有变量都是受限的,那么我们说这个公式是封闭的

在此之前我们提到过,Expression对象可以处理字符串,并返回类Expression的对象。这个类的每个实例expr都有free()方法,返回一个在expr中自由的变量的集合。

>>> read_expr = nltk.sem.Expression.fromstring
>>> read_expr('dog(cyril)').free()
set()
>>> read_expr('dog(x)').free()
{Variable('x')}
>>> read_expr('own(angus, cyril)').free()
set()
>>> read_expr('exists x.dog(x)').free()
set()
>>> read_expr('((some x. walk(x)) -> sing(x))').free()
{Variable('x')}
>>> read_expr('exists x.own(y, x)').free()
{Variable('y')}

3.2 一阶定理证明

回顾一下我们较早前在(10)中提出的to the north of上的限制:

(22)if x is to the north of y then y is not to the north of x.

我们观察到命题逻辑不足以表示与二元谓词相关的概括,因此,我们不能正确的捕捉论证:Sylvania is to the north of Freedonia. Therefore, Freedonia is not to the north of Sylvania

毫无疑问,用一阶逻辑形式化这些规则是很理想的:

all x. all y.(north_of(x, y) -> -north_of(y, x))

更妙的是,我们可以进行自动推理来证明论证的有效性。

定理证明在一般情况下是为了确定我们要证明的公式(证明目标)是否可以由一个有限序列的推理步骤从一个假设的公式列表派生出来。我们写作Sg,其中S是一个假设的列表(可能为空),g是证明目标。我们将用NLTK中的定理证明接口Prover9来演示这个。首先,我们分析所需的证明目标[1]和两个假设[2] [3]然后我们创建一个Prover9实例[4],并在目标和给定的假设列表上调用它的prove()[5]

>>> NotFnS = read_expr('-north_of(f, s)')  [1]
>>> SnF = read_expr('north_of(s, f)')    [2]
>>> R = read_expr('all x. all y. (north_of(x, y) -> -north_of(y, x))')  [3]
>>> prover = nltk.Prover9()   [4]
>>> prover.prove(NotFnS, [SnF, R])  [5]
True

令人高兴的是,定理证明器证明我们的论证是有效的。相反,它得出结论:不能从我们的假设推到出north_of(f, s)

>>> FnS = read_expr('north_of(f, s)')
>>> prover.prove(FnS, [SnF, R])
False

3.3 一阶逻辑语言总结

我们将借此机会重新表述前面的命题逻辑的语法规则,并添加量词的形式化规则;所有这些一起组成一阶逻辑的句法。此外,我们会明确相关表达式的类型。我们将采取约定:〈en, t〉一种由n个类型为e的参数组成产生一个类型为t的表达式的谓词的类型。在这种情况下,我们说n是谓词的元数

  1. If P is a predicate of type 〈en, t〉, and α1, ... αn are terms of type e, then P1, ... αn) is of type t.
  2. If α and β are both of type e, then (α = β) and (α != β) are of type t.
  3. If φ is of type t, then so is -φ.
  4. If φ and ψ are of type t, then so are (φ & ψ), (φ | ψ), (φ -> ψ) and (φ <-> ψ).
  5. If φ is of type t, and x is a variable of type e, then exists x.φ and all x.φ are of type t.

3.1总结了logic模块的新的逻辑常量,以及Expression模块的两个方法。

表 3.1

一阶逻辑所需的新的逻辑关系和运算符总结,以及Expression类的两个有用的方法。

示例描述
=等于
!=不等于
exists存在量词
all全称量词
e.free()显示e的自由变量
e.simplify()e上进行β-reduction

3.4 Truth in Model

我们已经看到了一阶逻辑句法,在4我们将考察将英语翻译成一阶逻辑的任务。然而,正如我们在1中提到的,只有我们赋予一阶逻辑的句子以含义,才能进一步做下去。换句话说,我们需要给出一阶逻辑的真值条件的语义从计算语义学的角度来看,采用这种方法能走多远是有明显的界限的。虽然我们要谈的是句子在某种情况下为真或为假,我们只能在电脑中以符号的方式表示这些情况。尽管有这些限制,通过在NLTK解码模块仍然可以获得较清晰的真值条件语义。

给定一阶逻辑语言LL的模型M是一个〈D, Val〉对,其中D是一个非空集合,称为模型的Val是一个函数,称为估值函数,它按如下方式从D中分配值给L的表达式:

  1. For every individual constant c in L, Val(c) is an element of D.
  2. For every predicate symbol P of arity n ≥ 0, Val(P) is a function from Dn to {True, False}. (If the arity of P is 0, then Val(P) is simply a truth value, the P is regarded as a propositional symbol.)

根据(ii),如果P的元数是2,然后Val(P)将是一个从D 的元素的配对到{True, False}的函数f我们将在NLTK中建立的模型中采取更方便的替代品,其中Val(P)是一个配对的集合S,定义如下:

(23)S = {s | f(s) = True}

这样的f被称为S特征函数(如在进一步阅读中讨论过的)。

NLTK 的语义关系可以用标准的集合论方法表示:作为元组的集合。例如,假设我们有一个域包括Bertie 、Olive和Cyril,其中Bertie是男孩,Olive是女孩,而Cyril是小狗。为了方便记述,我们用boc作为模型中相应的标签。我们可以声明域如下:

>>> dom = {'b', 'o', 'c'}

我们使用工具函数Valuation.fromstring()symbol => value形式的字符串序列转换成一个Valuation对象。

>>> v = """
... bertie => b
... olive => o
... cyril => c
... boy => {b}
... girl => {o}
... dog => {c}
... walk => {o, c}
... see => {(b, o), (c, b), (o, c)}
... """
>>> val = nltk.Valuation.fromstring(v)
>>> print(val)
{'bertie': 'b',
 'boy': {('b',)},
 'cyril': 'c',
 'dog': {('c',)},
 'girl': {('o',)},
 'olive': 'o',
 'see': {('o', 'c'), ('c', 'b'), ('b', 'o')},
 'walk': {('c',), ('o',)}}

根据这一估值,see的值是一个元组的集合,包含Bertie看到Olive、Cyril 看到Bertie和Olive看到Cyril。

注意

轮到你来:模仿1.2绘制一个图,描述域m和相应的每个一元谓词的集合。

你可能已经注意到,我们的一元谓词(即boygirldog)也是以单个元组的集合而不是个体的集合出现的。这使我们能够方便的统一处理任何元数的关系。一个形式为P1, ... τn)的谓词,其中Pn元的,为真的条件是对应于(τ1, ... τn) 的值的元组属于P的值的元组的集合。

>>> ('o', 'c') in val['see']
True
>>> ('b',) in val['boy']
True

3.5 独立变量和赋值

在我们的模型,上下文的使用对应的是为变量赋值这是一个从独立变量到域中实体的映射。赋值使用构造函数Assignment,它也以论述的模型的域为参数。我们无需实际输入任何绑定,但如果我们要这样做,它们是以(变量,)的形式来绑定,类似于我们前面看到的估值。

>>> g = nltk.Assignment(dom, [('x', 'o'), ('y', 'c')])
>>> g
{'y': 'c', 'x': 'o'}

此外,还可以使用print()查看赋值,使用与逻辑教科书中经常出现的符号类似的符号:

>>> print(g)
g[c/y][o/x]

现在让我们看看如何为一阶逻辑的原子公式估值。首先,我们创建了一个模型,然后调用evaluate()方法来计算真值。

>>> m = nltk.Model(dom, val)
>>> m.evaluate('see(olive, y)', g)
True

这里发生了什么?我们正在为一个公式估值,类似于我们前面的例子see(olive, cyril)然而,当解释函数遇到变量y时,不是检查val中的值,它在变量赋值g中查询这个变量的值:

>>> g['y']
'c'

由于我们已经知道ocsee关系中表示的含义,所以True值是我们所期望的。在这种情况下,我们可以说赋值g满足公式see(olive, y)相比之下,下面的公式相对g的评估结果为False(检查为什么会是你看到的这样)。

>>> m.evaluate('see(y, x)', g)
False

在我们的方法中(虽然不是标准的一阶逻辑),变量赋值是部分的。例如,g中除了xy没有其它变量。方法purge()清除一个赋值中所有的绑定。

>>> g.purge()
>>> g
{}

如果我们现在尝试为公式,如see(olive, y),相对于g估值,就像试图解释一个包含一个him的句子,我们不知道him指什么。在这种情况下,估值函数未能提供一个真值。

>>> m.evaluate('see(olive, y)', g)
'Undefined'

由于我们的模型已经包含了解释布尔运算的规则,任意复杂的公式都可以组合和评估。

>>> m.evaluate('see(bertie, olive) & boy(bertie) & -walk(bertie)', g)
True

确定模型中公式的真假的一般过程称为模型检查

3.6 量化

现代逻辑的关键特征之一就是变量满足的概念可以用来解释量化的公式。让我们用(24)作为一个例子。

(24)exists x.(girl(x) & walk(x))

它什么时候为真?让我们想想我们的域,即在dom中的所有个体。我们要检查这些个体中是否有属性是女孩并且这种走路的。换句话说,我们想知道dom中是否有某个u使g[u/x]满足开放的公式(25)

(25)girl(x) & walk(x)

思考下面的:

>>> m.evaluate('exists x.(girl(x) & walk(x))', g)
True

在这里evaluate()True,因为dom中有某些u通过绑定xu的赋值满足((25) )。事实上,o是这样一个u

>>> m.evaluate('girl(x) & walk(x)', g.add('x', 'o'))
True

NLTK中提供了一个有用的工具是satisfiers()方法。它返回满足开放公式的所有个体的集合。该方法的参数是一个已分析的公式、一个变量和一个赋值。下面是几个例子:

>>> fmla1 = read_expr('girl(x) | boy(x)')
>>> m.satisfiers(fmla1, 'x', g)
{'b', 'o'}
>>> fmla2 = read_expr('girl(x) -> walk(x)')
>>> m.satisfiers(fmla2, 'x', g)
{'c', 'b', 'o'}
>>> fmla3 = read_expr('walk(x) -> girl(x)')
>>> m.satisfiers(fmla3, 'x', g)
{'b', 'o'}

想一想为什么fmla2fmla3是那样的值,这是非常有用。->的真值条件的意思是fmla2等价于-girl(x) | walk(x),要么不是女孩要么没有步行的个体满足条件。因为b(Bertie)和c(Cyril)都不是女孩,根据模型m,它们都满足整个公式。当然o也满足公式,因为o两项都满足。现在,因为话题的域的每一个成员都满足fmla2,相应的全称量化公式也为真。

>>> m.evaluate('all x.(girl(x) -> walk(x))', g)
True

换句话说,一个全称量化公式∀x.φ关于g为真,只有对每一个u,φ关于g[u/x]为真。

注意

轮到你来:先用笔和纸,然后用m.evaluate(),尝试弄清楚all x.(girl(x) & walk(x))exists x.(boy(x) -> walk(x))的真值。确保你能理解为什么它们得到这些值。

3.7 量词范围歧义

当我们给一个句子的形式化表示个量词时,会发生什么?

(26)Everybody admires someone.

用一阶逻辑有(至少)两种方法表示((26)):

(27)

a.all x.(person(x) -> exists y.(person(y) & admire(x,y)))

b.exists y.(person(y) & all x.(person(x) -> admire(x,y)))

这两个我们都能用吗?答案是肯定的,但它们的含义不同。(27b)在逻辑上强于(27a):它声称只有一个人,也就是Bruce,被所有人钦佩。(27a)只要其对于每一个u我们可以找到u钦佩的一些人u';但每次找到的人u'可能不同。我们使用术语量化范围来区分(27a)(27b)首先,∀的范围比∃广,而在(27b)中范围顺序颠倒了。所以现在我们有两种方式表示(26)的意思,它们都相当合理。换句话说,我们称(26)关于量词范围有歧义(27)中的公式给我们一种使这两个读法明确的方法。然而,我们不只是对与(26)相关联的两个不同的表示感兴趣。我们也想要显示模型中的两种表述是如何导致不同的真值条件的细节。

为了更仔细的检查歧义,让我们对估值做如下修正:

>>> v2 = """
... bruce => b
... elspeth => e
... julia => j
... matthew => m
... person => {b, e, j, m}
... admire => {(j, b), (b, b), (m, e), (e, m)}
... """
>>> val2 = nltk.Valuation.fromstring(v2)

admire关系可以使用(28)所示的映射图进行可视化。

(28)../images/models_admire.png

(28)中,两个个体xy之间的箭头表示x钦佩y因此,jb都钦佩b(布鲁斯很虚荣),而e钦佩mm钦佩e。在这个模型中,公式(27a)为真而(27b)为假。探索这些结果的方法之一是使用Model对象的satisfiers()方法。

>>> dom2 = val2.domain
>>> m2 = nltk.Model(dom2, val2)
>>> g2 = nltk.Assignment(dom2)
>>> fmla4 = read_expr('(person(x) -> exists y.(person(y) & admire(x, y)))')
>>> m2.satisfiers(fmla4, 'x', g2)
{'e', 'b', 'm', 'j'}

这表明fmla4包含域中每一个个体。相反,思考下面的公式fmla5;没有满足y的值。

>>> fmla5 = read_expr('(person(y) & all x.(person(x) -> admire(x, y)))')
>>> m2.satisfiers(fmla5, 'y', g2)
set()

也就是说,没有大家都钦佩的人。看看另一个开放的公式fmla6,我们可以验证有一个人,即Bruce,它被Julia和Bruce都钦佩。

>>> fmla6 = read_expr('(person(y) & all x.((x = bruce | x = julia) -> admire(x, y)))')
>>> m2.satisfiers(fmla6, 'y', g2)
{'b'}

注意

轮到你来:基于m2设计一个新的模型,使(27a)在你的模型中为假;同样的,设计一个新的模型使(27b)为真。

3.8 模型的建立

我们一直假设我们已经有了一个模型,并要检查模型中的一个句子的真值。相比之下,模型的建立是给定一些句子的集合,尝试创造一种新的模型。如果成功,那么我们知道集合是一致的,因为我们有模型的存在作为证据。

我们通过创建Mace()的一个实例并调用它的build_model()方法来调用Mace4模式产生器,与调用Prover9定理证明器类似的方法。一种选择是将我们的候选的句子集合作为假设,保留目标为未指定。下面的交互显示了[a, c1][a, c2]都是一致的列表,因为Mace成功的为它们都建立了一个模型,而[c1, c2]不一致。

>>> a3 = read_expr('exists x.(man(x) & walks(x))')
>>> c1 = read_expr('mortal(socrates)')
>>> c2 = read_expr('-mortal(socrates)')
>>> mb = nltk.Mace(5)
>>> print(mb.build_model(None, [a3, c1]))
True
>>> print(mb.build_model(None, [a3, c2]))
True
>>> print(mb.build_model(None, [c1, c2]))
False

我们也可以使用模型建立器作为定理证明器的辅助。假设我们正试图证明Sg,即g是假设S = [s1, s2, ..., sn]的逻辑派生。我们可以同样的输入提供给Mace4,模型建立器将尝试找出一个反例,就是要表明g遵循从S。因此,给定此输入,Mace4将尝试为假设S连同g的否定找到一个模型,即列表S' = [s1, s2, ..., sn, -g]如果gS不能证明出来,那么Mace4会返回一个反例,比Prover9更快的得出结论:无法找到所需的证明。相反,如果gS可以证明出来,Mace4 可能要花很长时间不能成功地找到一个反例模型,最终会放弃。

让我们思考一个具体的方案。我们的假设是列表[There is a woman that every man loves, Adam is a man, Eve is a woman]。我们的结论是Adam loves EveMace4能找到使假设为真而结论为假的模型吗?在下面的代码中,我们使用MaceCommand()检查已建立的模型。

>>> a4 = read_expr('exists y. (woman(y) & all x. (man(x) -> love(x,y)))')
>>> a5 = read_expr('man(adam)')
>>> a6 = read_expr('woman(eve)')
>>> g = read_expr('love(adam,eve)')
>>> mc = nltk.MaceCommand(g, assumptions=[a4, a5, a6])
>>> mc.build_model()
True

因此答案是肯定的:Mace4发现了一个反例模型,其中Adam爱某个女人而不是Eve。但是,让我们细看Mace4的模型,转换成我们用来估值的格式。

>>> print(mc.valuation)
{'C1': 'b',
 'adam': 'a',
 'eve': 'a',
 'love': {('a', 'b')},
 'man': {('a',)},
 'woman': {('a',), ('b',)}}

这个估值的一般形式应是你熟悉的:它包含了一些单独的常量和谓词,每一个都有适当类型的值。可能令人费解的是C1它是一个“Skolem 常量”,模型生成器作为存在量词的表示引入的。也就是说,模型生成器遇到a4里面的exists y,它知道,域中有某个个体b满足a4中的开放公式。然而,它不知道b是否也是它的输入中的某个地方的一个独立常量的标志,所以它为b凭空创造了一个新名字,即C1现在,由于我们的假设中没有关于独立常量adameve的信息,模型生成器认为没有任何理由将它们当做表示不同的实体,于是它们都得到映射到a此外,我们并没有指定manwoman表示不相交的集合,因此,模型生成器让它们相互重叠。这个演示非常明显的隐含了我们用来解释我们的情境的知识,而模型生成器对此一无所知。因此,让我们添加一个新的假设,使man和woman不相交。模型生成器仍然产生一个反例模型,但这次更符合我们直觉的有关情况:

>>> a7 = read_expr('all x. (man(x) -> -woman(x))')
>>> g = read_expr('love(adam,eve)')
>>> mc = nltk.MaceCommand(g, assumptions=[a4, a5, a6, a7])
>>> mc.build_model()
True
>>> print(mc.valuation)
{'C1': 'c',
 'adam': 'a',
 'eve': 'b',
 'love': {('a', 'c')},
 'man': {('a',)},
 'woman': {('c',), ('b',)}}

经再三考虑,我们可以看到我们的假设中没有说Eve是论域中唯一的女性,所以反例模型其实是可以接受的。如果想排除这种可能性,我们将不得不添加进一步的假设,如exists y. all x. (woman(x) -> (x = y))以确保模型中只有一个女性。

4 英语句子的语义

4.1 基于特征的语法中的合成语义学

在本章开头,我们简要说明了一种在句法分析的基础上建立语义表示的方法,使用在9.开发的语法框架。这一次,不是构建一个SQL查询,我们将建立一个逻辑形式。我们设计这样的语法的指导思想之一是组合原则(也称为Frege原则;下面给出的公式参见(Gleitman & Liberman, 1995) 。)

组合原则:整体的含义是部分的含义与它们的句法结合方式的函数。

我们将假设一个复杂的表达式的语义相关部分由句法分析理论给出。在本章中,我们将认为表达式已经用上下文无关语法分析过。然而,这不是组合原则的内容。

我们现在的目标是以一种可以与分析过程平滑对接的方式整合语义表达的构建。(29)说明了我们想建立的这种分析的第一个近似。

(29)tree_images/ch10-tree-1.png

(29),根节点的sem值显示了整个句子的语义表示,而较低节点处的sem值显示句子成分的语义表示。由于sem值必须以特殊的方式来对待,它们被括在尖括号里面用来与其他特征值区别。

到目前为止还好,但我们如何编写能给我们这样的结果的语法规则呢?我们的方法将与本章开始采用的语法sql0.fcfg类似,其中我们将为词汇节点指定语义表示,然后组合它们的子节点的每个部分的语义表示。然而,在目前情况下,我们将使用函数应用而不是字符串连接作为组成的模式。为了更具体,假设我们有sem节点带有适当值的NPVP成分。那么,一个Ssem值由(30)这样的规则处理。请看在sem的值是一个变量的情况下,我们省略了尖括号)。

(30)S[SEM=<?vp(?np)>] -> NP[SEM=?np] VP[SEM=?vp]

(30)告诉我们,给定某个sem?np表示主语NP,某个sem?vp表示VP,父母Ssem值通过将?vp当做?np的函数表达式来构建。由此,我们可以得出结论?vp必须表示一个在它的域中有?np的表示的函数。(30)是一个很好的使用组合原则建立语义的例子。

要完成文法是非常简单的;我们需要的规则全部显示如下。

VP[SEM=?v] -> IV[SEM=?v]
NP[SEM=<cyril>] -> 'Cyril'
IV[SEM=<\x.bark(x)>] -> 'barks'

VP规则说的是父母的语义与核心孩子的语义相同。两个词法规则提供非逻辑常数分别作为Cyrilbarks 的语义值。barks入口处有一块额外的符号,我们将简短的解释。

在讲述组合语义规则的细节之前,我们需要为我们的工具箱添加新的工具,称为λ演算。这是一个宝贵的工具,用于在我们组装一个英文句子的意思表示时组合一阶逻辑表达式。

4.2 λ演算

3中,我们指出数学集合符号对于制定我们想从文档中选择的词的属性P很有用。我们用(31)说明这个,它是“所有w的集合,其中wV(词汇表)的元素且w有属性P”的表示。

(31){w | wV & P(w)}

事实证明添加一些能达到同样的效果的东西到一阶逻辑中是非常有用的。我们用λ运算符(发音为“lambda”)做这个。(31)的λ对应是(32)(由于我们不是要在这里讲述理论,我们只将V当作一元谓词。)

(32)λw. (V(w)P(w))

注意

λ表达式最初由Alonzo Church设计用来表示可计算函数,并提供数学和逻辑的基础。研究λ表达式的理论被称为λ演算。需要注意的是λ演算不是一阶逻辑的一部分——它们都可以单独使用。

λ是一个约束运算符,就像一阶逻辑量词。如果我们有一个开放公式,如(33a),那么我们可以将变量x与λ运算符绑定,如(33b)所示。(33c)中给出相应的NLTK表示。

(33)

a.(walk(x)chew_gum(x))

b.λx.(walk(x)chew_gum(x))

c.\x.(walk(x) & chew_gum(x))

请记住\是Python中的特殊字符。我们要么使用转义字符(另一个\)要么使用“原始字符串”(3.4):

>>> read_expr = nltk.sem.Expression.fromstring
>>> expr = read_expr(r'\x.(walk(x) & chew_gum(x))')
>>> expr
<LambdaExpression \x.(walk(x) & chew_gum(x))>
>>> expr.free()
set()
>>> print(read_expr(r'\x.(walk(x) & chew_gum(y))'))
\x.(walk(x) & chew_gum(y))

我们对绑定表达式中的变量的结果有一个特殊的名字:λ-抽象当你第一次遇到λ-抽象时,很难对它们的意思得到一个直观的感觉。(33b)的一对英语表示是“是一个x,其中x步行且x嚼口香糖”或“具有步行和嚼口香糖的属性。”通常认为λ-抽象可以很好的表示动词短语(或无主语从句),尤其是当它作为参数出现在它自己的右侧时。(34a)和它的翻译(34b)中的演示。

(34)

a.To walk and chew-gum is hard

b.hard(\x.(walk(x) & chew_gum(x)))

所以一般的描绘是这样的:一个开放公式φ有自由变量xx抽象为一个属性表达式λx.φ——满足φ的x的属性。这里有一个如何建立抽象的官方版本:

(35)If α is of type τ, and x is a variable of type e, then \x.α is of type 〈e, τ〉.

(34b)说明了一个情况:我们说某物的属性,即它是很难的。但我们通常所说的属性是为它们指定个体。而事实上,如果φ是一个开放公式,那么抽象λx.φ可以被用来作为一元谓词。(36)中,术语gerald满足(33b)

(36)\x.(walk(x) & chew_gum(x)) (gerald)

(36)说Gerald具有步行和嚼口香糖的属性,与(37)的意思相同。

(37)(walk(gerald) & chew_gum(gerald))

这里我们所做的是从\x.(walk(x) & chew_gum(x))的开头移除\x,并替换(walk(x) & chew_gum(x))中出现的所有xgerald我们将使用α[β/x]作为符合,表示替换所有在α中自由出现的x为表达式β。所以:

(walk(x) & chew_gum(x))[gerald/x]

(37)是相同的表达式。(36)(37)的约简是简化语义表示的非常有用的操作,我们将在本章的其余部分大量用到它。该操作通常被称为β-reduction为了使它在语义上合理,我们希望λx.α(β)能保持与α[β/x]有相同的语义值。这的确是真实的,稍微有些复杂,我们马上会遇到。为了在NLTK中实施表达式的β-约简,我们可以调用simplify()方法[1]

>>> expr = read_expr(r'\x.(walk(x) & chew_gum(x))(gerald)')
>>> print(expr)
\x.(walk(x) & chew_gum(x))(gerald)
>>> print(expr.simplify()) [1]
(walk(gerald) & chew_gum(gerald))

虽然我们迄今只考虑了λ-抽象的主体是一个某种类型t的开放公式,这不是必要的限制;主体可以是任何符合语法的表达式。下面是一个有两个λ的例子。

(38)\x.\y.(dog(x) & own(y, x))

正如(33b)中起到一元谓词的作用,(38)就像一个二元谓词:它可以直接应用到两个参数[1]逻辑表达式可以包含嵌套的λ,如\x.\y.写成缩写形式\x y. [1]

>>> print(read_expr(r'\x.\y.(dog(x) & own(y, x))(cyril)').simplify())
\y.(dog(cyril) & own(y,cyril))
>>> print(read_expr(r'\x y.(dog(x) & own(y, x))(cyril, angus)').simplify()) [1]
(dog(cyril) & own(angus,cyril))

我们所有的λ-抽象到目前为止只涉及熟悉的一阶变量:xy等——类型e的变量。但假设我们要处理一个抽象,例如\x.walk(x)作为另一个λ-抽象的参数我们不妨试试这个:

\y.y(angus)(\x.walk(x))

但由于变量y规定是e类型,\y.y(angus)只适用于e类型的参数,而\x.walk(x)是〈e, t〉类型的!相反,我们需要允许在更高级的类型的变量上抽象。让我们用PQ作为〈e, t〉类型的变量,那么我们可以有一个抽象,如\P.P(angus)由于P是〈e, t〉类型,整个抽象是〈〈e, t〉, t〉类型。那么\P.P(angus)(\x.walk(x))是合法的,可以通过β-约简化简为\x.walk(x)(angus),然后再次化简为walk(angus)

在进行β-约简时,对变量有些注意事项。例如,思考(39a)(39b)的λ-术语,它只是一个自由变量的不同身份。

(39)

a.\y.see(y, x)

b.\y.see(y, z)

现在假设我们应用λ-术语\P.exists x.P(x)这些术语中的每一个:

(40)

a.\P.exists x.P(x)(\y.see(y, x))

b.\P.exists x.P(x)(\y.see(y, z))

我们较早前指出过应用程序的结果应该是语义等价的。但是,如果我们让(39a)中的自由变量x落入(40a)中存在量词的范围内,那么约简之后的结果会不同:

(41)

a.exists x.see(x, x)

b.exists x.see(x, z)

(41a)是指有某个x能看到他/她自己,而(41b)的意思是有某个x能看到一个未指定的个体z出了什么错?显然,我们要禁止(41a)所示的那种变量“捕获”。

为了处理这个问题,让我们退后一会儿。我们使用的变量的特别的名字是否受到在(40a)的函数表达式的存在量词的约束呢?答案是否定的。事实上,给定任何绑定变量的表达式(包含∀、∃或λ),为绑定的变量选择名字完全是任意的。例如,exists x.P(x)exists y.P(y)是等价的;它们被称为α-等价,或字母变体重新标记绑定的变量的过程被称为α-转换当我们在logic模块中测试VariableBinderExpression是否相等(即使用==)时,我们其实是测试α-等价:

>>> expr1 = read_expr('exists x.P(x)')
>>> print(expr1)
exists x.P(x)
>>> expr2 = expr1.alpha_convert(nltk.sem.Variable('z'))
>>> print(expr2)
exists z.P(z)
>>> expr1 == expr2
True

当β-约减在一个应用f(a)中实施时,我们检查是否有自由变量在a同时也作为f的子术语中绑定的变量出现。假设在上面讨论的例子中,xa中的自由变量,f包括子术语exists x.P(x)在这种情况下,我们产生一个exists x.P(x)的字母变体,也就是说,exists z1.P(z1),然后再进行约减。这种重新标记由logic中的β-约减代码自动进行,可以在下面的例子中看到的结果。

>>> expr3 = read_expr('\P.(exists x.P(x))(\y.see(y, x))')
>>> print(expr3)
(\P.exists x.P(x))(\y.see(y,x))
>>> print(expr3.simplify())
exists z1.see(z1,x)

注意

当你在下面的章节运行这些例子时,你可能会发现返回的逻辑表达式的变量名不同;例如你可能在前面的公式的z1的位置看到z14这种标签的变化是无害的——事实上,它仅仅是一个字母变体的例子。

在此附注之后,让我们回到英语句子的逻辑形式建立的任务。

4.3 量化的NP

在本节开始,我们简要介绍了如何为Cyril barks构建语义表示。你会以为这太容易了——肯定还有更多关于建立组合语义的。例如,量词?没错,这是一个至关重要的问题。例如,我们要给出(42a)的逻辑形式(42b)如何才能实现呢?

(42)

a.A dog barks.

b.exists x.(dog(x) & bark(x))

让我们作一个假设,我们建立复杂的语义表示的唯一操作是函数应用。那么我们的问题是这样的:我们如何给量化的NP a dog一个语义表示,使它可以在(42b)中的结果中与bark结合?作为第一步,让我们将主语的sem值作为函数表达式,而不是参数。(这有时被称为类型提升。)现在,我们寻找实例化?np的方式,使[SEM=<?np(\x.bark(x))>]等价于[SEM=<exists x.(dog(x) & bark(x))>]这是否看上去有点让人联想到λ-演算中的β-约减?换句话说,我们想要一个λ-术语M来取代?np,从而应用M'bark'产生(42b)要做到这一点,我们替代(42b)中出现的'bark'为一个谓词变量'P',并用λ绑定变量,如(43)中所示。

(43)\P.exists x.(dog(x) & P(x))

我们已经在(43)中使用了不同风格的变量——也就是,'P'而不是'x''y'——表明我们在不同类型的对象上抽象——不是单个个体,而是类型为e, t的函数表达式。因此,作为一个整体(43)的类型是〈〈e, t〉, t〉。我们将普遍把这个作为NP的类型。为了进一步说明,一个普遍的量化的NP看起来像(44)

(44)\P.all x.(dog(x) -> P(x))

现在我们几乎完成了,除了我们还需要进行进一步的抽象,加上将限定词a,即(43),和dog的语义组合的过程的应用。

(45)\Q P.exists x.(Q(x) & P(x))

(46)作为一个函数表达式应用到dog产生(43),应用到bark给我们\P.exists x.(dog(x) & P(x))(\x.bark(x))最后,进行β-约简产生我们正想要的,即(42b)

4.4 及物动词

我们的下一个挑战是处理含及物动词的句子,如(46)

(46)Angus chases a dog.

我们所要建设的输出语义是exists x.(dog(x) & chase(angus, x))让我们看看我们如何能够利用λ-抽象得到这样的结果。可能的解决方案中一个重要制约因素是需要a dog的语义表示与NP是否充当句子的主语或宾语独立。换句话说,我们希望得到上述公式作为输出,同时坚持(43)作为NP的语义。第二个制约因素是VP应该有一个统一的解释的类型,不管它们是否只是一个不及物动词或及物动词加对象组成。更具体地说,我们规定VP的类型一直是〈e, t〉。鉴于这些制约因素,下面是chases a dog的成功的语义表示。

(47)\y.exists x.(dog(x) & chase(y, x))

(47)作为一个y的属性,使得对于某只狗xy追逐x;或者更通俗的,作为一个追逐一只狗的y我们现在的任务是为chases设计语义表示,它可以与(43)结合从而派生出(47)

让我们在(47)上进行β-约简的逆操作,得到(48)

(48)\P.exists x.(dog(x) & P(x))(\z.chase(y, z))

(48)在一开始可能会稍微难读;你需要看到,它涉及从(43)\z.chase(y,z)应用量化的NP 表示。(48)通过β-约简与exists x.(dog(x) & chase(y, x))等效。

现在,让我们用与NP的类型相同的变量X,也就是〈〈e, t〉, t〉类型,替换(48)中的函数表达式。

(49)X(\z.chase(y, z))

及物动词的表示将必须使用X类型的参数来产生VP类型,也就是〈e, t〉类型的函数表达式。我们可以确保这个,通过在(49)中的X变量和主语变量y上的抽象。因此,完整的解决方案是给出(50)中所示的chases的语义表示。

(50)\X y.X(\x.chase(y, x))

如果(50)应用到(43),β-约简后的结果与(47)等效,这是我们一直都想要的东西:

>>> read_expr = nltk.sem.Expression.fromstring
>>> tvp = read_expr(r'\X x.X(\y.chase(x,y))')
>>> np = read_expr(r'(\P.exists x.(dog(x) & P(x)))')
>>> vp = nltk.sem.ApplicationExpression(tvp, np)
>>> print(vp)
(\X x.X(\y.chase(x,y)))(\P.exists x.(dog(x) & P(x)))
>>> print(vp.simplify())
\x.exists z2.(dog(z2) & chase(x,z2))

为了建立一个句子的语义表示,我们也需要组合主语NP的语义。如果后者是一个量化的表达式,例如every girl,一切都与我们前面讲过的a dog barks一样的处理方式;主语转换为函数表达式,这被用于VP的语义表示。然而,我们现在似乎已经用适当的名称为自己创造了另一个问题。到目前为止,这些已经作为单独的常量进行了语义的处理,这些不能作为像(47)那样的表达式的函数应用。因此,我们需要为它们提出不同的语义表示。我们在这种情况下所做的是重新解释适当的名称,使它们也成为如量化的NP那样的函数表达式。这里是Angus的λ表达式。

(51)\P.P(angus)

(51)表示相应与Angus为真的所有属性的集合的特征函数。从独立常量angus转换为\P.P(angus)是类型提升的另一个例子,前面简单的提到过,允许我们用等效的函数应用\x.walk(x)(angus)替换一个布尔值的应用,如\P.P(angus)(\x.walk(x))通过β-约简,两个表达式都约简为walk(angus)

语法simple-sem.fcfg包含一个用于分析和翻译简单的我们刚才一直找寻的这类例子的一个小的规则集合。下面是一个稍微复杂的例子。

>>> from nltk import load_parser
>>> parser = load_parser('grammars/book_grammars/simple-sem.fcfg', trace=0)
>>> sentence = 'Angus gives a bone to every dog'
>>> tokens = sentence.split()
>>> for tree in parser.parse(tokens):
...     print(tree.label()['SEM'])
all z2.(dog(z2) -> exists z1.(bone(z1) & give(angus,z1,z2)))

NLTK提供一些实用工具使获得和检查的语义解释更容易。函数interpret_sents()用于批量解释输入句子的列表。它建立一个字典d,其中对每个输入的句子sentd[sent]是包含sent的分析树和语义表示的(synrep, semrep)对的列表。该值是一个列表,因为sent可能有句法歧义;在下面的例子中,列表中的每个句子只有一个分析树。

>>> sents = ['Irene walks', 'Cyril bites an ankle']
>>> grammar_file = 'grammars/book_grammars/simple-sem.fcfg'
>>> for results in nltk.interpret_sents(sents, grammar_file):
...     for (synrep, semrep) in results:
...         print(synrep)
(S[SEM=<walk(irene)>]
  (NP[-LOC, NUM='sg', SEM=<\P.P(irene)>]
    (PropN[-LOC, NUM='sg', SEM=<\P.P(irene)>] Irene))
  (VP[NUM='sg', SEM=<\x.walk(x)>]
    (IV[NUM='sg', SEM=<\x.walk(x)>, TNS='pres'] walks)))
(S[SEM=<exists z3.(ankle(z3) & bite(cyril,z3))>]
  (NP[-LOC, NUM='sg', SEM=<\P.P(cyril)>]
    (PropN[-LOC, NUM='sg', SEM=<\P.P(cyril)>] Cyril))
  (VP[NUM='sg', SEM=<\x.exists z3.(ankle(z3) & bite(x,z3))>]
    (TV[NUM='sg', SEM=<\X x.X(\y.bite(x,y))>, TNS='pres'] bites)
    (NP[NUM='sg', SEM=<\Q.exists x.(ankle(x) & Q(x))>]
      (Det[NUM='sg', SEM=<\P Q.exists x.(P(x) & Q(x))>] an)
      (Nom[NUM='sg', SEM=<\x.ankle(x)>]
        (N[NUM='sg', SEM=<\x.ankle(x)>] ankle)))))

现在我们已经看到了英文句子如何转换成逻辑形式,前面我们看到了在模型中如何检查逻辑形式的真假。把这两个映射放在一起,我们可以检查一个给定的模型中的英语句子的真值。让我们看看前面定义的模型m工具evaluate_sents()类似于interpret_sents(),除了我们需要传递一个模型和一个变量赋值作为参数。输出是三元组(synrep, semrep, value),其中synrepsemrep和以前一样,value是真值。为简单起见,下面的例子只处理一个简单的句子。

>>> v = """
... bertie => b
... olive => o
... cyril => c
... boy => {b}
... girl => {o}
... dog => {c}
... walk => {o, c}
... see => {(b, o), (c, b), (o, c)}
... """
>>> val = nltk.Valuation.fromstring(v)
>>> g = nltk.Assignment(val.domain)
>>> m = nltk.Model(val.domain, val)
>>> sent = 'Cyril sees every boy'
>>> grammar_file = 'grammars/book_grammars/simple-sem.fcfg'
>>> results = nltk.evaluate_sents([sent], grammar_file, m, g)[0]
>>> for (syntree, semrep, value) in results:
...     print(semrep)
...     print(value)
all z4.(boy(z4) -> see(cyril,z4))
True

4.5 再述量词歧义

上述方法的一个重要的限制是它们没有处理范围歧义。我们的翻译方法是句法驱动的,认为语义表示与句法分析紧密耦合,语义中量词的范围也因此反映句法分析树中相应的NP的相对范围。因此,像(26)这样的句子,在这里重复,总是会被翻译为(53a)而不是(53b)

(52)Every girl chases a dog.

(53)

a.all x.(girl(x) -> exists y.(dog(y) & chase(x,y)))

b.exists y.(dog(y) & all x.(girl(x) -> chase(x,y)))

有许多方法来处理范围歧义,我们将简要的看看最简单的一个。首先,让我们简要地考虑具有范围的公式的结构。4.1描绘了(52)的这两种不同的读法。

../images/quant-ambig.png

图 4.1:量词范围

让我们先考虑左侧的结构。在顶部,有对应于every girl的量词。φ可以被看作是量词范围内的所有东西的一个占位符。向下移动,我们看到可以插入相应与a dog的量词作为φ的实例。这提供了一种新的占位符ψ,表示a dog的范围,这一点,我们可以堵塞语义的“核心”,即对应于x chases y开放的句子。右侧的结构是相同的,除了两个量词的顺序交换了。

在被称为Cooper存储的方法中,语义表示不再是一阶逻辑表达式,而是一个由一个“核心”语义表示加一个绑定操作符列表组成的配对。就目前而言,可以认为一个绑定操作符是如(44)(45)那样的量化NP的语义表示。沿4.1所示的线向下,我们假设我们已经构建了一个Cooper存储风格的句子(52)的语义表示,让我们将开放公式chase(x,y)作为核心。给定有关(52)中两个NP的绑定操作符的列表,我们将一个绑定操作符从列表挑出来,与核心结合。

\P.exists y.(dog(y) & P(y))(\z2.chase(z1,z2))

然后,我们将列表中的另一个绑定操作符应用到结果中。

\P.all x.(girl(x) -> P(x))(\z1.exists x.(dog(x) & chase(z1,x)))

当列表为空时,我们就有了句子的传统的逻辑形式。将绑定操作符与核心以这种方式组合被称为S-检索如果我们仔细的允许绑定操作符的每个可能的顺序(例如,通过将列表进行全排列;见4.5),那么我们将能够产生量词的每一个可能的范围排序。

接下来要解决的问题是我们如何建立一个核心+存储表示的组合。如前所述,语法中每个短语和词法规则将有一个sem特征,但现在将有嵌入特征corestore要说明这里的机制,让我们考虑一个简单的例子Cyril smiles下面是动词smiles的词法规则(取自语法storage.fcfg),看起来还可以。

IV[SEM=[core=<\x.smile(x)>, store=(/)]] -> 'smiles'

适当的名称Cyril的规则更为复杂。

NP[SEM=[core=<@x>, store=(<bo(\P.P(cyril),@x)>)]] -> 'Cyril'

谓词bo有两个子部分:一个适当的名称的标准(类型提升)表示,以及表达式@x,被称为绑定操作符的地址(我们将简要解释为什么需要地址变量。)@x是原变量,也就是,范围在逻辑的独立变量之上的变量,你会看到,它也提供了核心的值。VP的规则只是向上渗透IV的语义,有趣的工作由S规则来做。

VP[SEM=?s] -> IV[SEM=?s]

S[SEM=[core=<?vp(?np)>, store=(?b1+?b2)]] ->
   NP[SEM=[core=?np, store=?b1]] VP[SEM=[core=?vp, store=?b2]]

S节点的核心值是应用VP核心值,即\x.smile(x),到主语NP的值的结果。后者不会是@x,而是一个@x的实例,也就是z3β-约简后,<?vp(?np)><smile(z3)>统一。现在,当@x实例化为分析过程的一部分时,它将会同样的被实例化。特别是主语NPstore中出现的@x也将被映射到z3,产生元素bo(\P.P(cyril),z3)这些步骤可以在下面的分析树中看到。

(S[SEM=[core=<smile(z3)>, store=(bo(\P.P(cyril),z3))]]
  (NP[SEM=[core=<z3>, store=(bo(\P.P(cyril),z3))]] Cyril)
  (VP[SEM=[core=<\x.smile(x)>, store=()]]
    (IV[SEM=[core=<\x.smile(x)>, store=()]] smiles)))

让我们回到更复杂的例子,(52),看看在用语法storage.fcfg分析后,存储风格的sem值是什么。

core  = <chase(z1,z2)>
store = (bo(\P.all x.(girl(x) -> P(x)),z1), bo(\P.exists x.(dog(x) & P(x)),z2))

现在,应该很清楚为什么地址变量是绑定操作符的一个重要组成部分。记得在S-检索过程中,我们将绑定操作符从store列表移出,先后将它们应用到核心。假设我们从我们想要与chase(z1,z2)结合的bo(\P.all x.(girl(x) -> P(x)),z1)开始。绑定操作符的量词部分是\P.all x.(girl(x) -> P(x)),将这与chase(z1,z2)结合,后者需要先被转换成λ-抽象。我们怎么知道在哪些变量上进行抽象?这是z1的地址告诉我们的,即every girl都有追求者而不是主动追求别人的人。

模块nltk.sem.cooper_storage处理将存储形式的语义表示转换成标准逻辑形式的任务。首先,我们构造一个CooperStore实例,并检查它的storecore

>>> from nltk.sem import cooper_storage as cs
>>> sentence = 'every girl chases a dog'
>>> trees = cs.parse_with_bindops(sentence, grammar='grammars/book_grammars/storage.fcfg')
>>> semrep = trees[0].label()['SEM']
>>> cs_semrep = cs.CooperStore(semrep)
>>> print(cs_semrep.core)
chase(z2,z4)
>>> for bo in cs_semrep.store:
...     print(bo)
bo(\P.all x.(girl(x) -> P(x)),z2)
bo(\P.exists x.(dog(x) & P(x)),z4)

最后,我们调用s_retrieve()检查读法。

>>> cs_semrep.s_retrieve(trace=True)
Permutation 1
   (\P.all x.(girl(x) -> P(x)))(\z2.chase(z2,z4))
   (\P.exists x.(dog(x) & P(x)))(\z4.all x.(girl(x) -> chase(x,z4)))
Permutation 2
   (\P.exists x.(dog(x) & P(x)))(\z4.chase(z2,z4))
   (\P.all x.(girl(x) -> P(x)))(\z2.exists x.(dog(x) & chase(z2,x)))
>>> for reading in cs_semrep.readings:
...     print(reading)
exists x.(dog(x) & all z3.(girl(z3) -> chase(z3,x)))
all x.(girl(x) -> exists z4.(dog(z4) & chase(x,z4)))

5 段落语义层

段落是句子的序列。很多时候,段落中的一个句子的解释依赖它前面的句子。一个明显的例子来自照应代词,如hesheit给定一个段落如Angus used to have a dog. But he recently disappeared.,你可能会解释he指的是Angus的狗。然而,在Angus used to have a dog. He took him for walks in New Town.中,你更可能解释he指的是Angus自己。

5.1 段落表示理论

一阶逻辑中的量化的标准方法仅限于单个句子。然而,似乎是有量词的范围可以扩大到两个或两个以上的句子的例子。。我们之前看到过一个,下面是第二个例子,与它的翻译一起。

(54)

a.Angus owns a dog. It bit Irene.

b.x.(dog(x)own(Angus, x)bite(x, Irene))

也就是说,NP a dog的作用像一个绑定第二句话中的it的量词。段落表示理论(DRT)的目标是提供一种方法处理这个和看上去是段落的特征的其它语义现象。一个段落表示结构(DRS)根据一个段落指称的列表和一个条件列表表示段落的意思。段落指称是段落中正在讨论的事情,它对应一阶逻辑的单个变量。DRS条件应用于那些段落指称,对应于一阶逻辑的原子开放公式。5.1演示了(54a)中第一句话的DRS如何增强为两个句子的DRS。

../images/drs1.png

图 5.1:建立一个DRS:左侧的DRS表示段落中第一句话的处理结果,而右侧的DRS显示处理并整合第二句上下文的效果。

在处理(54a)的第二句时,以5.1左侧的已经呈现的上下文背景进行解释。代词it触发另外一个新的段落指称,也就是u,我们需要为它找一个先行词——也就是,我们想算出it指的是什么。在DRT中,为一个参照代词寻找先行词的任务包括将它连接到已经在当前DRS中的话题指称,y是显而易见的选择。(我们会在不久讲述更多关于指代消解的内容。)处理步骤产生一个新的条件u = y第二句贡献的其余的内容也与第一个的内容合并,如5.1右侧所示。

5.1说明DRS如何表示多个句子。这种情况是一个两句话的段落,但原则上一个DRS可以对应整个文本的解释。我们可以查询5.1中DRS右侧的真值。非正式地,在某个条件s中为真,如果在情况s中有实体a, ci,对应DRS中的段落指称使s中所有条件都为真;也就是说,aAngusc是a dog,a拥有ciIrenec咬了i

为了处理DRS计算,我们需要将其转换成线性格式。下面是一个例子,其中DRS是由一个段落指称列表和一个DRS条件列表组成的配对:

([x, y], [angus(x), dog(y), own(x,y)])

在NLTK建立DRS对象最简单的方法是通过解析一个字符串表示[1]

>>> read_dexpr = nltk.sem.DrtExpression.fromstring
>>> drs1 = read_dexpr('([x, y], [angus(x), dog(y), own(x, y)])') [1]
>>> print(drs1)
([x,y],[angus(x), dog(y), own(x,y)])

我们可以使用draw()方法[1]可视化结果,如5.2所示。

>>> drs1.draw() [1]
../images/drs_screenshot0.png

图 5.2:DRS截图

我们讨论5.1中DRS的真值条件时,假设最上面的段落指称被解释为存在量词,而条件也进行了解释,虽然它们是联合的。事实上,每一个DRS都可以转化为一阶逻辑公式,fol()方法实现这种转换。

>>> print(drs1.fol())
exists x y.(angus(x) & dog(y) & own(x,y))

作为一阶逻辑表达式功能补充,DRT表达式有DRS-连接运算符,用+符号表示。两个DRS的连接是一个单独的DRS包含合并的段落指称和来自两个论证的条件。DRS-连接自动进行α-转换绑定变量避免名称冲突。

>>> drs2 = read_dexpr('([x], [walk(x)]) + ([y], [run(y)])')
>>> print(drs2)
(([x],[walk(x)]) + ([y],[run(y)]))
>>> print(drs2.simplify())
([x,y],[walk(x), run(y)])

虽然迄今为止见到的所有条件都是原子的,一个DRS可以内嵌入另一个DRS,这是全称量词被处理的方式。drs3中,没有顶层的段落指称,唯一的条件是由两个子DRS组成,通过蕴含连接。再次,我们可以使用fol()来获得真值条件的句柄。

>>> drs3 = read_dexpr('([], [(([x], [dog(x)]) -> ([y],[ankle(y), bite(x, y)]))])')
>>> print(drs3.fol())
all x.(dog(x) -> exists y.(ankle(y) & bite(x,y)))

我们较早前指出DRT旨在通过链接照应代词和现有的段落指称来解释照应代词。DRT设置约束条件使段落指称可以像先行词那样“可访问”,但并不打算解释一个特殊的先行词如何被从候选集合中选出的。模块nltk.sem.drt_resolve_anaphora采用了类此的保守策略:如果DRS包含PRO(x)形式的条件,方法resolve_anaphora()将其替换为x = [...]形式的条件,其中[...]是一个可能的先行词列表。

>>> drs4 = read_dexpr('([x, y], [angus(x), dog(y), own(x, y)])')
>>> drs5 = read_dexpr('([u, z], [PRO(u), irene(z), bite(u, z)])')
>>> drs6 = drs4 + drs5
>>> print(drs6.simplify())
([u,x,y,z],[angus(x), dog(y), own(x,y), PRO(u), irene(z), bite(u,z)])
>>> print(drs6.simplify().resolve_anaphora())
([u,x,y,z],[angus(x), dog(y), own(x,y), (u = [x,y,z]), irene(z), bite(u,z)])

由于指代消解算法已分离到它自己的模块,这有利于在替代程序中交换,使对正确的先行词的猜测更加智能。

我们对DRS的处理与处理λ-抽象的现有机制是完全兼容的,因此可以直接基于DRT而不是一阶逻辑建立组合语义表示。这种技术在下面的不确定性规则(是语法drt.fcfg的一部分)中说明。为便于比较,我们已经从simple-sem.fcfg增加了不确定性的平行规则。

Det[num=sg,SEM=<\P Q.(([x],[]) + P(x) + Q(x))>] -> 'a'
Det[num=sg,SEM=<\P Q. exists x.(P(x) & Q(x))>] -> 'a'

为了对DRT规则如何运作有更好的了解,请看下面的NP a dog子树。

(NP[num='sg', SEM=<\Q.(([x],[dog(x)]) + Q(x))>]
  (Det[num='sg', SEM=<\P Q.((([x],[]) + P(x)) + Q(x))>] a)
  (Nom[num='sg', SEM=<\x.([],[dog(x)])>]
    (N[num='sg', SEM=<\x.([],[dog(x)])>] dog)))))

作为一个函数表达式不确定性的λ-抽象被应用到\x.([],[dog(x)]),得到\Q.(([x],[]) + ([],[dog(x)]) + Q(x));简化后,我们得到\Q.(([x],[dog(x)]) + Q(x))NP 为一个整体表示。

为了解析语法drt.fcfg,我们在load_parser()调用中指定特征结构中的SEM值用DrtParser解析。

>>> from nltk import load_parser
>>> parser = load_parser('grammars/book_grammars/drt.fcfg', logic_parser=nltk.sem.drt.DrtParser())
>>> trees = list(parser.parse('Angus owns a dog'.split()))
>>> print(trees[0].label()['SEM'].simplify())
([x,z2],[Angus(x), dog(z2), own(x,z2)])

5.2 段落处理

我们解释一句话时会使用丰富的上下文知识,一部分取决于前面的内容,一部分取决于我们的背景假设。DRT提供了一个句子的含义如何集成到前面段落表示中的理论,但是在前面的讨论中明显缺少这两个部分。首先,一直没有尝试纳入任何一种推理;第二,我们只处理了个别句子。这些遗漏由模块nltk.inference.discourse纠正。

段落是一个句子的序列s1, ... sn段落线是读法的序列s1-ri, ... sn-rj ,每个序列对应段落中的一个句子。该模块按增量处理句子,当有歧义时保持追踪所有可能的线。为简单起见,下面的例子中忽略了范围歧义。

>>> dt = nltk.DiscourseTester(['A student dances', 'Every student is a person'])
>>> dt.readings()

s0 readings:

s0-r0: exists x.(student(x) & dance(x))

s1 readings:

s1-r0: all x.(student(x) -> person(x))

一个新句子添加到当前的段落时,设置参数consistchk=True会通过每条线,即每个可接受的读法的序列的检查模块来检查一致性。在这种情况下,用户可以选择收回有问题的句子。

>>> dt.add_sentence('No person dances', consistchk=True)
Inconsistent discourse: d0 ['s0-r0', 's1-r0', 's2-r0']:
    s0-r0: exists x.(student(x) & dance(x))
    s1-r0: all x.(student(x) -> person(x))
    s2-r0: -exists x.(person(x) & dance(x))
>>> dt.retract_sentence('No person dances', verbose=True)
Current sentences are
s0: A student dances
s1: Every student is a person

以类似的方式,我们使用informchk=True检查新的句子φ是否对当前的段落有信息量。定理证明器将段落线中现有的句子当做假设,尝试证明φ;如果没有发现这样的证据,那么它是有信息量的。

>>> dt.add_sentence('A person dances', informchk=True)
Sentence 'A person dances' under reading 'exists x.(person(x) & dance(x))':
Not informative relative to thread 'd0'

也可以传递另一套假设作为背景知识,并使用这些筛选出不一致的读法;详情请参阅http://nltk.org/howto上的段落HOWTO。

discourse模块可适应语义歧义,筛选出不可接受的读法。下面的例子调用Glue语义和DRT。由于Glue语义模块被配置为使用的覆盖面广的Malt 依存关系分析器,输入(Every dog chases a boy. He runs.)需要分词和标注。

>>> from nltk.tag import RegexpTagger
>>> tagger = RegexpTagger(
...     [('^(chases|runs)$', 'VB'),
...      ('^(a)$', 'ex_quant'),
...      ('^(every)$', 'univ_quant'),
...      ('^(dog|boy)$', 'NN'),
...      ('^(He)$', 'PRP')
... ])
>>> rc = nltk.DrtGlueReadingCommand(depparser=nltk.MaltParser(tagger=tagger))
>>> dt = nltk.DiscourseTester(['Every dog chases a boy', 'He runs'], rc)
>>> dt.readings()

s0 readings:

s0-r0: ([],[(([x],[dog(x)]) -> ([z3],[boy(z3), chases(x,z3)]))])
s0-r1: ([z4],[boy(z4), (([x],[dog(x)]) -> ([],[chases(x,z4)]))])

s1 readings:

s1-r0: ([x],[PRO(x), runs(x)])

段落的第一句有两种可能的读法,取决于量词的作用域。第二句的唯一的读法通过条件PRO(x)`表示代词He现在让我们看看段落线的结果:

>>> dt.readings(show_thread_readings=True)
d0: ['s0-r0', 's1-r0'] : INVALID: AnaphoraResolutionException
d1: ['s0-r1', 's1-r0'] : ([z6,z10],[boy(z6), (([x],[dog(x)]) ->
([],[chases(x,z6)])), (z10 = z6), runs(z10)])

当我们检查段落线d0d1时,我们看到读法s0-r0,其中every dog超出了a boy的范围,被认为是不可接受的,因为第二句的代词不能得到解释。相比之下,段落线d1中的代词(重写为z24通过等式(z24 = z20)绑定。

不可接受的读法可以通过传递参数filter=True过滤掉。

>>> dt.readings(show_thread_readings=True, filter=True)
d1: ['s0-r1', 's1-r0'] : ([z12,z15],[boy(z12), (([x],[dog(x)]) ->
([],[chases(x,z12)])), (z17 = z12), runs(z15)])

虽然这一小段是极其有限的,它应该能让你对于我们在超越单个句子后产生的语义处理问题,以及部署用来解决它们的技术有所了解。

6 小结

7 深入阅读

关于本章的进一步材料以及如何安装Prover9定理证明器和Mace4模型生成器的内容请查阅http://nltk.org/这两个推论工具一般信息见(McCune, 2008)

用NLTK进行语义分析的更多例子,请参阅http://nltk.org/howto上的语义和逻辑HOWTO。请注意,范围歧义还有其他两种解决方法,即(Blackburn & Bos, 2005)描述的Hole语义(Dalrymple, 1999)描述的Glue语义

自然语言语义中还有很多现象没有在本章中涉及到,主要有:

  1. 事件、时态和体;
  2. 语义角色;
  3. 广义量词,如most
  4. 内涵结构,例如像maybelieve这样的动词。

(1)和(2)可以使用一阶逻辑处理,(3)和(4)需要不同的逻辑。下面的读物中很多都讲述了这些问题。

建立自然语言前端数据库方面的结果和技术的综合概述可以在(Androutsopoulos, Ritchie, & Thanisch, 1995)中找到。

任何一本现代逻辑的入门书都将提出命题和一阶逻辑。强烈推荐(Hodges, 1977),书中有很多有关自然语言的有趣且有洞察力的文字和插图。

要说范围广泛,参阅两卷本的关于逻辑教科书(Gamut, 1991)(Gamut, 1991),也包含了有关自然语言的形式语义的当代材料,例如Montague文法和内涵逻辑。(Kamp & Reyle, 1993)提供段落表示理论的权威报告,包括涵盖大量且有趣的自然语言片段,包括时态、体和形态。另一个对许多自然语言结构的语义的全面研究是(Carpenter, 1997)

有许多作品介绍语言学理论框架内的逻辑语义。(Chierchia & McConnell-Ginet, 1990)与句法相对无关,而(Heim & Kratzer, 1998) and (Larson & Segal, 1995)都更明确的倾向于将语义真值条件整合到乔姆斯基框架中。

(Blackburn & Bos, 2005)是致力于计算语义的第一本教科书,为该领域提供了极好的介绍。它扩展了许多本章涵盖的主题,包括量词范围歧义的未指定、一阶逻辑推理以及段落处理。

要获得更先进的当代语义方法的概述,包括处理时态和广义量词,尝试查阅(Lappin, 1996)(Benthem & Meulen, 1997)

8 练习

  1. ☼ 将下列句子翻译成命题逻辑,并用Expression.fromstring()验证结果。提供显示你的翻译中命题变量如何对应英语表达的一个要点。

    1. If Angus sings, it is not the case that Bertie sulks.
    2. Cyril runs and barks.
    3. It will snow if it doesn't rain.
    4. It's not the case that Irene will be happy if Olive or Tofu comes.
    5. Pat didn't cough or sneeze.
    6. If you don't come if I call, I won't come if you call.
  2. ☼ 翻译下面的句子为一阶逻辑的谓词参数公式。

    1. Angus likes Cyril and Irene hates Cyril.
    2. Tofu is taller than Bertie.
    3. Bruce loves himself and Pat does too.
    4. Cyril saw Bertie, but Angus didn't.
    5. Cyril is a fourlegged friend.
    6. Tofu and Olive are near each other.
  3. ☼ 翻译下列句子为成一阶逻辑的量化公式。

    1. Angus likes someone and someone likes Julia.
    2. Angus loves a dog who loves him.
    3. Nobody smiles at Pat.
    4. Somebody coughs and sneezes.
    5. Nobody coughed or sneezed.
    6. Bruce loves somebody other than Bruce.
    7. Nobody other than Matthew loves somebody Pat.
    8. Cyril likes everyone except for Irene.
    9. Exactly one person is asleep.
  4. ☼ 翻译下列动词短语,使用λ-抽象和一阶逻辑的量化公式。

    1. feed Cyril and give a capuccino to Angus
    2. be given 'War and Peace' by Pat
    3. be loved by everyone
    4. be loved or detested by everyone
    5. be loved by everyone and detested by no-one
  5. ☼ 思考下面的语句:

    >>> read_expr = nltk.sem.Expression.fromstring
    >>> e2 = read_expr('pat')
    >>> e3 = nltk.sem.ApplicationExpression(e1, e2)
    >>> print(e3.simplify())
    exists y.love(pat, y)

    显然这里缺少了什么东西,即e1值的声明。为了ApplicationExpression(e1, e2)被β-转换为exists y.love(pat, y)e1必须是一个以pat为参数的λ-抽象。你的任务是构建这样的一个抽象,将它绑定到e1,使上面的语句都是满足(上到字母方差)。此外,提供一个e3.simplify()的非正式的英文翻译。

    现在根据e3.simplify()的进一步情况(如下所示)继续做同样的任务。

    >>> print(e3.simplify())
    exists y.(love(pat,y) | love(y,pat))
    >>> print(e3.simplify())
    exists y.(love(pat,y) | love(y,pat))
    >>> print(e3.simplify())
    walk(fido)
  6. ☼ 如前面的练习中那样,找到一个λ-抽象e1,产生与下面显示的等效的结果。

    >>> e2 = read_expr('chase')
    >>> e3 = nltk.sem.ApplicationExpression(e1, e2)
    >>> print(e3.simplify())
    \x.all y.(dog(y) -> chase(x,pat))
    >>> e2 = read_expr('chase')
    >>> e3 = nltk.sem.ApplicationExpression(e1, e2)
    >>> print(e3.simplify())
    \x.exists y.(dog(y) & chase(pat,x))
    >>> e2 = read_expr('give')
    >>> e3 = nltk.sem.ApplicationExpression(e1, e2)
    >>> print(e3.simplify())
    \x0 x1.exists y.(present(y) & give(x1,y,x0))
  7. ☼ 如前面的练习中那样,找到一个λ-抽象e1,产生与下面显示的等效的结果。

    >>> e2 = read_expr('bark')
    >>> e3 = nltk.sem.ApplicationExpression(e1, e2)
    >>> print(e3.simplify())
    exists y.(dog(x) & bark(x))
    >>> e2 = read_expr('bark')
    >>> e3 = nltk.sem.ApplicationExpression(e1, e2)
    >>> print(e3.simplify())
    bark(fido)
    >>> e2 = read_expr('\\P. all x. (dog(x) -> P(x))')
    >>> e3 = nltk.sem.ApplicationExpression(e1, e2)
    >>> print(e3.simplify())
    all x.(dog(x) -> bark(x))
  8. ◑ 开发一种方法,翻译英语句子为带有二元广义量词的公式。在此方法中,给定广义量词Q,量化公式的形式为Q(A, B),其中AB是〈e, t〉类型的表达式。那么,例如all(A, B)为真当且仅当A表示的是B所表示的一个子集。

  9. ◑ 扩展前面练习中的方法,使量词如mostexactly three的真值条件可以在模型中计算。

  10. ◑ 修改sem.evaluate代码,使它能提供一个有用的错误消息,如果一个表达式不在模型的估值函数的域中。

  11. ★ 从儿童读物中选择三个或四个连续的句子。一个例子是nltk.corpus.gutenberg中的故事集:bryant-stories.txtburgess-busterbrown.txtedgeworth-parents.txt开发一个语法,能将你的句子翻译成一阶逻辑,建立一个模型,使它能检查这些翻译为真或为假。

  12. ★ 实施前面的练习,但使用DRT作为意思表示。

  13. ★ 以(Warren & Pereira, 1982)为出发点,开发一种技术,转换一个自然语言查询为一种可以更加有效的在模型中评估的形式。例如,给定一个(P(x) & Q(x))形式的查询,将它转换为(Q(x) & P(x)),如果Q的范围比P小。

关于本文档...

针对NLTK 3.0 作出更新。本章来自于Natural Language Processing with PythonSteven Bird, Ewan KleinEdward Loper,Copyright © 2014 作者所有。本章依据Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 United States License [http://creativecommons.org/licenses/by-nc-nd/3.0/us/] 条款,与自然语言工具包 [http://nltk.org/] 3.0 版一起发行。

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