列举事实
让我们通过一个反例来解释这个概念。我们用简单的事实来说明名词、动词等:
det(the).
det(a).
n(woman).
n(man).
v(shoots).
现在我们可以实现一个名词短语 np,如下所示:
np([X,Y]) :-
det(X),
n(Y).
换句话说,“名词短语是一个含有两个单词的句子,第一个是 det
,第二个是 n
”。如果我们查询np([a,woman])
,它将成功等等。
但是现在我们需要做更高级的事情,定义 动词短语。有两种可能的动词短语:一个带有动词和名词短语的,最初定义为:
vp(X,Z):- v(X,Y),np(Y,Z).
我们可以将其定义为:
vp([X|Y]) :-
v(X),
np(Y).
还有只有一个动词的句子:
vp(X,Z):- v(X,Z).
这可以转换为:
vp([X]) :-
v(X).
猜测问题
问题在于两个变体有不同数量的单词:有一个单词和三个单词的动词短语。这并不是真正的问题,但现在假设——我知道这不是正确的英语——存在一句话定义为vp
后跟np
,因此这将是:
s(X,Z):- vp(X,Y), np(Y,Z).
在原始语法中,存在一个问题。如果我们想将其转换为新的表示方式,我们需要知道vp
会消耗多少内容(vp
将吃掉多少单词)。我们无法提前知道这一点:因为此时我们对句子了解不多,无法猜测vp
是否会吃掉一个或三个单词。
当然,我们可以猜测单词数量:
s([X|Y]) :-
vp([X]),
np(Y).
s([X,Y,Z|T]) :-
vp([X,Y,Z]),
np(Z).
但我希望你能想象一下,如果你用1、3、5和7个单词来定义动词短语,事情会变得麻烦。解决这个问题的另一种方法是将其留给Prolog处理:
s(S) :-
append(VP,NP,S),
vp(VP),
np(NP).
现在Prolog将首先猜测如何将句子分成两部分,然后尝试匹配每一部分。但问题在于对于一个有n个单词的句子,有n个断点。
因此,例如Prolog会首先将其拆分为:
VP=[],NP=[shoots,the,man,the,woman]
(记住我们交换了动词短语和名词短语的顺序)。显然,如果
vp
得到一个空字符串,它将会非常不开心。所以这很容易被拒绝。但接下来它说:
VP=[shoots],NP=[the,man,the,woman]
现在,
vp
只需要
shoots
就感到高兴了,但要实现这一点需要一些计算工作。然而,
np
对这个冗长的部分并不满意。因此,Prolog 再次回溯:
VP=[shoots,the],NP=[man,the,woman]
现在 vp
会再次抱怨它被给予了太多的单词。最终Prolog将使用以下方法正确分割:
VP=[shoots,the,woman],NP=[the,woman]
重点是需要大量的猜测。对于每一个这样的猜测,
vp
和
np
也需要工作。对于一个真正复杂的语法,
vp
和
np
可能会进一步分割句子,导致大量的试错。
真正的原因是
append/3
没有"语义"线索来分割句子,所以它尝试了所有可能性。然而,更感兴趣的是一种方法,其中
vp
可以提供关于它真正想要的句子的信息。
此外,如果你必须将句子分成三个部分,这样做的方式数量甚至增加到
O(n^2)等级。因此,猜测不起作用。
你也可以尝试生成一个随机动词短语,然后希望它与原句匹配:
s(S) :-
vp(VP),
append(VP,NP,S),
np(NP).
但在这种情况下,猜测动词短语的数量将呈指数级增长。当然,您可以给出“提示”等以加快该过程,但仍需要一些时间。
解决方案
您想要做的是为每个谓词提供句子的一部分,使得谓词看起来像:
predicate(Subsentence,Remaining)
子句
是以谓语为开头的单词列表。例如,对于名词短语,它可能看起来像
[the,woman,shoots,the,man]
。每个谓语
消耗它感兴趣的单词:直到某个点的单词。在这种情况下,名词短语只对
['the','woman']
感兴趣,因为那是一个名词短语。为了进行其余的解析,它返回剩余部分
[shoots,the,woman]
,希望其他谓语可以消耗该句子的剩余部分。
对于我们的事实表来说,这非常容易:
det([the|W],W).
det([a|W],W).
n([woman|W],W).
n([man|W],W).
v([shoots|W],W).
这意味着如果你查询一个句子:
[the,woman,shoots,...]
,并问
det/2
它是否是一个限定词,它会回答:“是的,
the
是一个限定词,但剩下的部分
[woman,shoots,...]
”不是限定词的一部分,请与其他内容匹配。这种匹配是因为列表被表示为链接列表。
[the,woman,shoots,...]
实际上被表示为
[the | [woman | [shoots | ...]]]
(所以它指向下一个“子列表”)。如果你匹配:
[the|[woman|[shoots|...]]]
det([the|W] ,W)
它将把[woman|[shoots|...]]
与W
统一起来,因此会得到:
det([the|[woman|[shoots|...]],[woman|[shoots|...]]).
因此,返回“剩余”列表后,它已经“消耗”了“the”部分。
更高级别的谓词
现在,假设我们定义名词短语:
np(X,Z):- det(X,Y), n(Y,Z).
我们再次使用
[the,woman,shoots,...]
调用,它将查询统一的
X
列表。它将首先调用
det
来消耗
the
,无需回溯。接下来,
Y
等于
[woman,shoots,...]
,现在
n/2
将消耗女人并返回
[shoots,...]
。这也是
np
将返回的结果,另一个谓词将不得不消耗它。
实用性
假设您将自己的姓名作为附加名词介绍:
n([doug,smith|W],W).
抱歉使用小写字母,但否则Prolog会将它们视为变量。
它将仅尝试将前两个单词与 doug
和 smith
匹配,如果成功,则尝试匹配剩余的句子。因此,可以构造像这样的句子:[the,doug,smith,shoots,the,woman]
(对此表示抱歉,此外在英语中,一些名词短语直接映射到名词np(X,Y) :- n(X,Y)
,因此可以删除 the
以获得更复杂的英语语法)。
完全消除猜测?
是否完全消除了猜测?不是。仍然可能存在重叠消耗的情况。例如,您可以添加:
n([doug,smith|W],W).
n([doug|W],W).
在这种情况下,如果您查询
[the,doug,smith,shoots,the,woman]
,它将首先消耗掉
det
中的“the”,然后从
[doug,smith,...]
中查找一个名词来消耗。有两个候选人。Prolog将首先尝试仅吃掉
doug
,并将
[smith,shoots,...]
作为整个动词短语匹配,但由于
smith
不是动词,它将回溯,重新考虑仅吃掉一个单词,因此决定将
doug
和
smith
都作为名词而非动词来消耗。
关键在于使用append时,Prolog也必须回溯。
结论
通过使用差分列表,您可以消耗任意数量的单词。剩余部分被返回,使得其他句子部分(如动词短语)旨在消耗剩余部分。该列表还始终完全搞定,因此绝对不会使用蛮力方法先生成各种动词短语。
s([a,woman,shoots,a,man], X)?
。 - lurker