您好,欢迎来到九壹网。
搜索
您的当前位置:首页用Prolog开发专家系统(yjb)

用Prolog开发专家系统(yjb)

来源:九壹网
 用Prolog开发专家系统

素材来源于:中国人工智能网

闫净斌收集整理 2011-10-27

目 录

第一章 解释 ................................... 1

解释 ................................................................................................................................... 1

解释的种类 ....................................................................................................................... 2 在Clam中使用解释 ........................................................................................................ 2 跟踪 ................................................................................................................................... 7 回答how问题 .................................................................................................................. 9 回答why提问 ................................................................................................................ 14 在原始的推理引擎中加入解释 ..................................................................................... 16

第二章 前向链 ................................. 23

前向链 ............................................................................................................................. 23

Production系统(我不知道应该如何翻译这里的production)...................................... 23 Oops的使用 ................................................................................................................... 25

第三章 OOPS改进 .............................. 39

解释 ................................................................................................................................. 39 增强功能 ......................................................................................................................... 39 规则的选择 ..................................................................................................................... 39 LEX ................................................................................................................................. 43 MEA ................................................................................................................................ 51

第四章 frame(框架) ............................ 54

程序 ................................................................................................................................. 55 框架的表达 ..................................................................................................................... 57 谓词的实现 ..................................................................................................................... 58

i

第一章 解释

专家系统的一个重要的功能就是要能够解释它自己的行为。用Prolog开放具体解释能力的专家系统。

解释

专家系统的一个重要的功能就是要能够解释它自己的行为。这意味着用户可以在任何时候询问系统为什么得出某个结论,或者为什么提出某个问题。

这对于用户来说是一项重要的功能,有时候用户只要求知道答案,可是有时候用户需要知道解释,而通常的专家系统无法对它的行为做出有说服力的解释,而只能够告诉用户它使用了哪些规则得出的结论,至于为什么这些规则能够得出这样的结论,系统是无法解释的。例如下面这个例子:

汽车能够启动么? 不行 引擎发动了么? 是的 你问到汽油味道了么?是的 建议:等待5秒钟,然后再试。 为什么?

因为我使用了这样的规则:

如果不能够启动而且引擎发动了而且问到汽油味,那么就推荐的等待5秒再试。

很显然这个专家系统无法解释其选择某个规则的原因,而只能告诉用户它使用了某种规则。如果用户硬要刨根问底的话这个系统就为力了。

为了让系统具有真正的解释功能,我们需要比规则更多的知识。对每个规则进行注释是一个比较好的方法,这种方法将在以后的章节介绍。还有一种方法就是把更多的知识进行编码,推理引擎和解释引擎都同时使用这个知识库。

1

还有些专家系统的知识库是属于经验知识,在这种情况下系统的解释可以直接使用规则。像识别鸟类的分类系统就属于这种情况。鸟类识别系统就能够使用它的规则直接进行解释,例如为什么某种鸟是野鸭,就是因为它具有野鸭的一些特性,而这些特性就是规则所定义的。识别鸟类并不存在什么高深的理论,而只是根据某些特点进行分类的。

也许对于用户来说某些解释是多余的,不过对于开发人员来说这是十分重要的。这和通常的语言中的跟踪调试有些类似。当系统没有按照预期的效果执行的时候,开发人员可以根据解释研究错误的产生原因。知识工程师也可以根据解释从而设计出更加贴近用户的知识库。

解释的种类

在一般的专家系统中常用的有4种解释。

1. 报告当前的会话进程。

2. 解释系统是如何得出某个结论的。 3. 解释为怎么系统向用户询问某个问题。 4. 解释为什么某个结论不成立。

在我们上一章介绍的Clam外壳程序中,推理引擎是自己编写的,所以这些解释特性并不难加入系统当中。在第一章的原始外壳中没有推理引擎,而是使用prolog的内部引擎,这样就无法加入新的解释特性,为了达到这个目的,我们需要编写自己的推理引擎,而这个引擎的运作方式和prolog相同,也就是说需要使用prolog编写一个prolog,好在这项工作并不难完成。

在Clam中使用解释

首先让我们看看在Clam中加入了解释的一个例子,这里沿用了上一章汽车诊断系统。 首先用户打开对话跟踪功能,跟踪的信息使用粗体字表示,跟踪信息显示了系统是如何调用规则的。注意系统正确的表示出了规则的嵌套调用。

报告当前的会话进程的解释:

consult, restart, load, list, trace, how, exit

2

:trace on

consult, restart, load, list, trace, how, exit :consult

call rule 1

Does the engine turn over? : no

call rule 2

Are the lights weak? : yes

exit rule 2

call rule 3

Is the radio weak? : yes

exit rule 3 exit rule 1

3

call rule 4 fail rule 4 call rule 5 fail rule 5 call rule 6 fail rule 6

problem-battery-cf-75 done with problem

下面来看看如何解释为什么要向系统提问。用户可以在任何时候向推理引擎询问why,请看这个例子: ...

Is the radio weak? : why rule 3 If

radio_weak Then

battery_bad 50 rule 1 If

not turn_over battery_bad Then

problem is battery 100

4

goal problem ...

这里可以看出来当用户向系统询问为什么问is the radio weak这个问题的时候,系统把有关这个问题的几个规则列出来了。

再来看看how提问,当系统给出了某个结论的时候,用户可能想知道是如何得到这个结论的,这个时候向系统询问how。 ...

problem-battery-cf-75

done with problem

consult, restart, load, list, trace, how, exit :how

Goal? problem is battery

problem is battery was derived from rules: 1 rule 1 If

not turn_over battery_bad Then

5

problem is battery 100

在这里列出了能够直接得到结论的规则。如果用户需要继续知道为什么battery_bad的话,就进行下面的询问:

consult, restart, load, list, trace, how, exit :how

Goal? battery_bad

battery_bad was derived from rules: 3 2 rule 3 If

radio_weak Then

battery_bad 50 rule 2 If

lights_weak Then

battery_bad 50

在这里有两个规则可以得到battery_bad的结论,系统把它们都列举出来了。

6

看完了示范,该是研究程序的时候了。

跟踪

首先我们来看看如何制作跟踪功能。这个跟踪功能可以向用户报告某个规则的调用、退出以及失败几个事件。 这里使用谓词bugdisp来向用户显示跟踪信息,它的参数是一个要显示出来的列表。

为了让用户可以选择是否打开跟踪功能,bugdisp首先检查ruletrace是否为真。因此在我们的外壳程序中就又多了一个打开或者关闭跟踪功能的命令。然后我们就可以把bugdisp放在任何想要显示跟踪信息的地方了。

bugdisp(L) :- ruletrace, write_line(L), !.

bugdisp(_).

write_line([]) :- nl. write_line([H|T]) :- write(H), tab(1), write_line(T).

然后我们在外壳程序中加入trace(on)和trace(off)两个命令。

do( trace(X) ) :- set_trace(X), !.

set_trace(off) :- ruletrace,

retract( ruletrace ).

7

set_trace(on) :- not ruletrace, asserta( ruletrace ).

set_trace(_).

现在我们已经编写好了可以显示跟踪信息的谓词了。下面我们就需要把bugdisp放入到适当地方,让它显示出跟踪信息。在上一章介绍的谓词fg中很容易找到规则被调用和规则成功的地方。下面就是增加了跟踪功能的fg谓词。

fg(Goal, CurCF) :-

rule(N, lhs(IfList), rhs(Goal, CF)), bugdisp(['call rule', N]), prove(N, IfList, Tally), bugdisp(['exit rule', N]), adjust(CF, Tally, NewCF), update(Goal, NewCF, CurCF, N), CurCF == 100, !.

fg(Goal, CF) :- fact(Goal, CF).

当某个规则的目标满足fg的目标的时候,这个规则就被调用,所以在rule后面加入call rule。当这个规则的前提都得到证实的时候这个规则就成功了,因此在prove后面加入exit rule。

那么规则在什么时候失败呢?在prove失败的时候,规则就失败了,因此我们加入一个处理prove失败的子句:

prove(N, IfList, Tally) :-

8

prov(IfList, 100, Tally), !.

prove(N, _, _) :-

bugdisp(['fail rule', N]), fail.

注意上面的第二个子句就是新加入的,当第一个子句失败的时候,就调用这个子句,它首先显示失败信息,然后再失败。

回答how问题

当用户想知道系统是如何得出某个结论的时候,可以向系统提问how。实现这种方法有两个途径,一种当用户询问how的时候重新跟踪系统的调用过程,另外一种则是把推理过程直接保存在工作空间中。我们使用后面这种方法。在我们的工作空间中原来的储存信息格式如下:

fact(AV,CF).

它只保存了属性信息和确信度信息,由于现在我们要回答用户是如何得到某个结论的,因此我们要加入第三个参数RuleList,改进后的fact格式如下:

fact(AV,CF,RuleList).

RuleList用来保存推理出这个fact所使用的规则列表。这里的RuleList不是整个求解树,而是是直接推导出这个fact的规则。

fact是使用update谓词更新的,因此需要改写update。我们给update新加入一个参数用来告诉update是哪个规则引起的update,也就是说是哪个规则支持的需要更新的这个fact。

update(Goal, NewCF, CF, RuleN) :-

9

fact(Goal, OldCF, _), %如果已经存在这个fact, combine(NewCF, OldCF, CF),

retract( fact(Goal, OldCF, OldRules) ), %就取得原来的rulelist, asserta( fact(Goal, CF, [RuleN | OldRules]) ), !. %并且添加新的Rule进去。

update(Goal, CF, CF, RuleN) :- %否则就是系统中不存在这个fact, asserta( fact(Goal, CF, [RuleN]) ). %就直接添加入工作空间。

调用update的谓词fg也要相应的有所改动:

fg(Goal, CurCF) :-

rule(N, lhs(IfList), rhs(Goal, CF)), ...

update(Goal, NewCF, CurCF, N), %把规则名传递给update。 ...

下面再来编写处理用户的how命令,最简单的办法就是把用户询问的Goal对应的rulelist给列出来,不过如果我们能够把规则的内容也显示出来的话,这样就更加方便了。

how(Goal) :-

fact(Goal, CF, Rules), CF > 20,

pretty(Goal, PG),

write_line([PG, was, derived, from, 'rules: '|Rules]), nl,

list_rules(Rules), fail.

10

how(_).

how(not Goal) :- fact(Goal, CF, Rules), CF -20,

pretty(not Goal, PG),

write_line([PG, was, derived, from, 'rules: '|Rules]), nl,

list_rules(Rules), fail.

pretty谓词用来把属性值结构转化为更容易的阅读的列表。

pretty(av(A, yes), [A]) :- !.

pretty(not av(A, yes), [not, A]) :- !.

pretty(av(A, no), [not, A]) :- !.

pretty(not av(A, V), [not, A, is, V]).

pretty(av(A, V), [A, is, V]).

list_rules用来显示出规则的内容。

list_rules([]).

list_rules([R|X]) :-

11

list_rule(R), list_rules(X).

list_rule(N) :-

rule(N, lhs(Iflist), rhs(Goal, CF)), write_line(['rule ', N]), write_line(['If']), write_ifs(Iflist), write_line(['Then']), pretty(Goal, PG),

write_line([' ', PG, CF]), nl.

write_ifs([]).

write_ifs([H|T]) :- pretty(H, HP), tab(5), write_line(HP), write_ifs(T).

我们还可以反过来使用pretty,也就是说把用户输入的列表,转换为属性值的结构,这样的话,用户就不需要知道系统内部是如何表达知识的了。 how :-

write('Goal? '), read_line(X), nl, pretty(Goal, X), how(Goal).

最后把how命令加入外壳程序的命令列表:

12

do(how) :- how, !.

上面就是完整的how的编写过程了。不过它只显示出直接推导出某个规则,而这些规则又是基于其他的规则或者事实的,如何进一步的推理信息呢?有两种方法:

让用户使用how继续询问,

让how命令自动的显示完整的证明树。

第一项功能已经实现,如何实现第二个功能呢?我们使用how_lhs把rulelist中的每个规则作为目标,递归的调用how。当某个目标的rulelist为空的时候,表示这个目标不是由规则得出的,而是用户输入的已知事实,这就是说完成了整个证明树的搜索过程。

list_rules([]).

list_rules([R|X]) :- list_rule(R), how_lhs(R), list_rules(X).

how_lhs(N) :-

rule(N, lhs(Iflist), _), !,

how_ifs(Iflist).

how_ifs([]).

13

how_ifs([Goal|X]) :- how(Goal), how_ifs(X).

在这里我们回答how提问有3种选择:只显示规则名,显示规则的内容,显示完整的搜索树。

回答why提问

当系统得出某个结论之后,用户可以使用how向它询问是如何得出这个结论的。而在系统的用户的对话过程中,系统为了收集资料会向用户询问,这个时候如果用户感到困惑的时候,可以询问系统为什么问这个问题。

为了回答why问题,我们需要跟踪推理过程,也就是说要记录下以前推理一些信息。在推理谓词中增加一个保存这种信息的参数,就可以很好的解决这个问题。在findgoal和prove中我们增加了一个Hist参数。

findgoal(Goal, CurCF, Hist) :- fg(Goal, CurCF, Hist).

fg(Goal, CurCF, Hist) :- ...

prove(N, IfList, Tally, Hist), ...

在prove谓词中,在递归调用findgoal去寻找进一步的解的前面,先把现在所使用的规则记录下来。

prove(N, IfList, Tally, Hist) :- prov(IfList, 100, Tally, [N|Hist]), !.

14

prove(N, _, _) :-

bugdisp(['fail rule', N]), fail.

prov([], Tally, Tally, Hist).

prov([H|T], CurTal, Tally, Hist) :- findgoal(H, CF, Hist), min(CurTal, CF, Tal), Tal >= 20,

prov(T, Tal, Tally, Hist).

我们举个例子说明一下,当系统需要证明a的时候,它发现要使用b,c。当它开始着手证明b之前,先把a记录下来,这样当用户询问系统你为什么要证明b的时候,它就可以告诉用户:我是要证明a才来证明b的。假如证明b又需要d和e,在在证明d和e之前,先把b记录下来,而d和e是需要向用户收集的信息,用户可能会询问为什么要问我d和e这样的问题,这时系统中的历史列表应该是[b,a],系统查询这个列表就会告诉用户,我需要证明b,因此询问d和e,而用户问为什么要证明b呢,系统就会回答是要证明a。显然有了这个历史列表,系统就知道自己为什么要获得某个属性的值了。

由于why命令是用户在和系统的对话的中间询问的,和原来how命令有些不同。原来的外壳命令help、exit、how等等,都不能在对话中间使用,下面我们就修改原来的程序,让用户可以打断和系统的对话,来做其他的事情。用下面的get_user谓词替代以前的向用户询问的谓词,这个get_user谓词允许在对话中间运行why,trace,help命令,当然加入其他的命令也是不困难的。

get_user(X, Hist) :- repeat,

15

write(': '), read_line(X), process_ans(X, Hist).

process_ans([why], Hist) :- nl, write_hist(Hist), !, fail.

process_ans([trace, X], _) :- set_trace(X), !, fail.

process_ans([help], _) :- help, !, fail.

process_ans(X, _). % just return user's answer

write_hist([]) :- nl.

write_hist([goal(X)|T]) :- write_line([goal, X]), !, write_hist(T).

write_hist([N|T]) :- list_rule(N), !, write_hist(T).

在回答why的时候,不仅仅显示规则名,还可以显示规则的内容。

在原始的推理引擎中加入解释

还记得第一章介绍的那个识别鸟类的专家系统么,由于这个专家系统使用的是prolog的推理引擎,所以无法加入解释功能,为了加入解释,我们必须使用prolog编写一个prolog解释器,这项工作很容易完成。当编写完成了自己的推理引擎之后,就可以很方便的处理解释了。

16

推理引擎首先要能够读取规则,在prolog中,子句本身就是prolog的项。内部谓词clause可以让我们存取规则。它的两个参数分别与子句的头和内容匹配。事实的内容就只有目标ture一个。

在prolog的语法中,使用“,”隔开规则的每个子目标,其实在prolog中,规则的储存方法和我们看上去的有很大的不同。下面我们举个例子。

对于规则a:-b,c,d.在prolog中的实际结构是:-(a,&(b,&(c,d))).可以看出,这和我们自己定义的数据结构信息是相同的。例如我们可能会定义:father(a,b).在这里father就是谓词,a和b就是参数。而在规则中,:-是谓词,&也是谓词。这一点在前面的prolog语言介绍中过。

有了上面的知识,我们就很容易编写出递归处理每个子目标的程序了。

recurse(FirstGoal & RemainingGoals) :- process(FirstGoal), recurse(RemainingGoals).

recurse(SingleGoal) :- process(SingleGoal).

这里使用&是为了读者不把prolog中的两种逗号搞混淆,一种逗号是用来把谓词的参数分开的例如:father(a,b). 另外一种则是用来连接规则中的两个子目标的, 表示并且的意思,这种逗号实际上是谓词。

有了可以存取prolog的事实和规则的方法以后,我们就可以很容易的编写出处理这些事实和规则的谓词了。

prove(true) :- !.

17

prove((Goal, Rest)) :-

clause(Goal, Body), %找到和目标匹配的子句 prove(Body), %证明Body部分。

prove(Rest). %证明上一层目标的剩余部分。

prove(Goal) :-

clause(Goal, Body), %找到和目标匹配的子句。 prove(Body). %证明这个子句的Body部分。

注意,prove谓词正好模拟了prolog的解题过程。首先他找到头部和第一个目标匹配某个子句。然后试图证明这个子句的目标列表。

上面的这个解释器只能够处理纯prolog子句。对于prolog的内部谓词为力。因此最后我们加上一条:

prove(X):-call(X).

用来调用内部谓词。

在我们的这个外壳程序中并不打算使用prolog的内部谓词,不过因为需要调用ask和menuask这样的谓词来和用户对话,这些谓词对于上面的解释器就可以被认为是内部谓词了。

和前面的Clam一样,我们加入参数Hist来回答用户的why提问。

prove(true, _) :- !.

prove((Goal, Rest), Hist) :-

18

prov(Goal, (Goal, Rest)), prove(Rest, Hist).

prov(true, _) :- !.

prov(menuask(X, Y, Z), Hist) :- menuask(X, Y, Z, Hist), !.

prov(ask(X, Y), Hist) :- ask(X, Y, Hist), !.

prov(Goal, Hist) :- clause(Goal, List), prove(List, [Goal|Hist]).

注意这里的历史记录保存的是目标列表,而不是规则名。

下面来修改顶层的谓词。 solve :-

abolish(known, 3), define(known, 3), prove(top_goal(X), []),

write('The answer is '), write(X), nl.

solve :-

write('No answer found'), nl.

处理why提问的程序和clam类似。

get_user(X, Hist) :-

19

repeat, read(X),

process_ans(X, Hist), !.

process_ans(why, Hist) :- write(Hist), !, fail.

process_ans(X, _).

最后的对话形式如下:

?- identify.

nostrils : external_tubular? why.

[nostrils(external_tubular), order(tubenose), family(albatross), bird(laysan_albatross)] nostrils : external_tubular?

在Clam外壳程序中,为了回答用户的how提问,我们使用了在工作空间中保存相应的信息的方法,在这里就简单多了, 只需要重新求解某个目标,并且把中间的证明过程都显示出来:

how(Goal) :- clause(Goal, List), prove(List, []), write(List).

我们还可以让系统来回答whynot的提问,也就是说系统可以告诉用户为什么某个结论不成立。这段程序和前面的解释器类似,不过它能够显示出目标是在什么地方失败的。

20

whynot(Goal) :- clause(Goal, List),

write_line([Goal, 'fails because: ']), explain(List).

whynot(_).

explain( (H, T) ) :- check(H), explain(T).

explain(H) :- check(H).

check(H) :- prove(H, _), write_line([H, succeeds]), !.

check(H) :- write_line([H, fails]), fail.

whynot和how一样存在一个问题。是自动的给出整棵树,从而找到失败的根源,还是让用户自己不停的询问呢?上面的这段程序只能够给出最近的失败的原因,如果在第二个check子句中再调用whynot就可以显示出整个树了。我们举一个例子说明一下。 假设: a:-b,c. b:-d. c:-e.

21

而e不成立,所以最终的结果是a也不成立。用户询问whynot a,系统只能够告诉用户是因为c不成立。当用户问whynot c的时候,系统才会显示出是因为e不成立。这就是让用户不停的询问的方法。

如果当用户询问whynot a,系统能够自动的递归寻找失败的原因,它就会直接告诉用户是因为e不成立导致c不成立,从而导致a不成立的。

加入解释的部分就介绍完了,下一章介绍数据驱动的专家系统的设计方法。 22

第二章 前向链

讨论有关使用前向链技术建造专家系统的方法。详细讨论了前向链系统是如何工作的,以及如何使用prolog实现它。

前向链

(forward chaining)

(一种用于处理知识库的搜索策略,按照此策略,从已知的事实出发推出解答或者建议。常用于配置、设计、规划、预测和解释型专家系统中。)

本章将讨论有关使用前向链技术建造专家系统的方法。我们将详细讨论前向链系统是如何工作的,以及如何使用prolog实现它。

许多类型的专家系统都需要使用前向链技术,或者说是数据驱动的推理引擎。Digital Equipment公司的XCON系统就是一个典型的例子。这个系统用来配置计算机。它从用户的订购信息出发,根据这些用户的需求信息,给出相应的配置。这个系统是使用一种叫做OPS5的语言编写的。

数据驱动的专家系统和前面介绍的目标驱动的专家系统有很大的不同。

目标驱动适用于最终的可能解答较少的情况,例如诊断或识别系统。这样的系统通过从用户那里获得有用的信息,从而有效的证明或者推翻一系列的可能的解答。最终给出正确的答案。

有些系统由于组合爆炸的原因,最终的可能解答数量大得惊人,以至于无法对其一一进行测试,这个时候就需要使用数据驱动了(前向链)。例如配置一台机器,假设某台机器由10个部件组成,而每个部件又有10种可选的方案,那么一共就有10的10次方种的最终配置方法,显然对每种方法进行测试是不可能的。

Production系统(我不知道应该如何翻译这里的production)

我们通常把前向链系统称之为\"production\"系统。系统中的每个规则事实上都是一个微型小程序,我们就把这些小程序称为production。每个production都由两部分组成:左边的条件模板和右边的操作,当左边的模板和工作空间中的元素

23

匹配的时候,就运行它右边的操作。本章将集中精力介绍一种叫做Oops的production系统。

通常的Production系统由三部分组成:

  

规则的集合

包含系统当前状态的工作空间 知道如何使用规则的推理引擎

规则的形式如下:

左边部分(LHS) ==> 右边部分(RHS)

左边部分是此规则运行的条件,也就是说当工作空间中的数据满足左边部分的条件的时候,此规则将被运行。而右边部分则是实际的运行程序。 整个系统的工作运循环如下:

1. 选择一个左边部分符合工作空间的数据的规则

2. 运行此规则的右边部分,通常会因此而改变工作空间的状态 3. 重复上面的步骤,直到没有规则能被使用为止。

当有多个规则同时符合工作空间的数据的时候,不同的Production系统使用不同的选择规则的算法(第一步中的算法)。在我们第一个版本的Oops中,使用最简单的算法:即选择第一个符合工作空间的数据的规则。

知识工程师使用Oops的特殊的语法为某种应用编写规则,规则的语法如下: rule :

[: , .......] ==> [, ....]. 其中

rule id:唯一标识规则的标识符

24

N:条件编号(可选)

Condition:需要个工作空间匹配的条件 Action:规则所运行的操作

这里的条件(condition)是合法的prolog数据结构,可以包含变量。和prolog一样,变量也是大写字母或者下划线开头的。一般来说条件模板和工作空间的数据进行匹配,还可以使用比较操作符(= > =等)来比较绑定之后的变量的值。

Oops的数据表达方法要比前面介绍的Clam的方法要丰富得多,在Clam中,我们只使用如下的两种数据表达方法:

属性-属性值 物体-属性-属性值

在Oops中可以使用任何形式的合法的prolog项,并且可以包含变量。

下面列出了RHS中可以使用的操作: assert(X) 向工作空间中添加项X

retract(all) 删除工作空间中与LHS中的条件匹配的所有项 retract(N) 删除工作空间中与LHS的第N个条件匹配的项 X=<数学表达式> 给X赋值 X#Y 把结构X和Y进行联合 Write(X) 输出X的值 Nl 输出行终止符 Read(X) 读入某个项 Prompt(X,Y) 输出X 读入Y

Oops的使用

在Winston & Home 的Lisp的书中有一个使用前向链技术编写的动物识别的例子。这个例子中的规则如果使用Oops的语法来表达,就有如下的形式:

25

rule id6:

[1: has(X, pointed_teeth), 2: has(X, claws), 3: has(X, forward_eyes)] ==>

[retract(all),

assert(isa(X, carnivore))].

从上面的规则可以看出,如果工作空间中有如下的prolog项,这个规则的条件将被满足,从而运行此规则的RHS部分。

has(robie, pointed_teeth) has(robie, claws) has(robie, forward_eyes)

当运行RHS中的retract(all)时,这三个prolog项将被删除,而添加项isa(robie,carnivore).

由于工作空间中的与条件匹配的三个项已经被删除,所以这个规则将不会再被调用了,而其它的规则会继续满足工作空间中的数据,例如:

rule id10:

[1: isa(X, mammal), 2: isa(X, carnivore), 3: has(X, black_stripes)] ==>

[retract(all), assert(isa(X, tiger))].

26

表示关系的规则也很容易编写。我们再来看一个Winston&Horn的动物识别专家系统中的例子。这个规则表示的意思是:子女和父母属于同一种动物,其代码如下:

rule id16:

[1: isa(Animal, Type), 2: parent(Animal, Child)] ==>

[retract(2),

assert(isa(Child, Type))].

这个规则会把工作空间中形如 isa(robie, tiger) parent(robie, suzie) 的项转化成下面的项。 isa(robie, tiger) isa(suzie, tiger)

工作空间的初始状态由谓词initial_data定义,例如: initial_data([gives(robie, milk), eats_meat(robie), has(robie, tawny_color), has(robie, dark_spots), parent(robie, suzie)].

我们的系统还应该具有向用户询问初始数据的能力。在一般的程序设计中我们可以使用循环的读如数据来完成这个任务。

27

在我们的这个Oops系统中,为了和其它的规则统一,因此要使用和前面一样的规则来完成这样的功能。理论上讲,系统中的规则是没有先后次序的,所有的规则都处于同等的地位,不过在实际运行中,写在前面的规则总是先运行,而写在后面的规则总是后运行,因此我们可以方便地写出下面的规则来完成从用户取得数据的功能:

initial_data([read_facts]). %初始状态,在系统运行的时候,初始状态会写入工作空间。

rule 1: %这个规则定义了读入数据循环的结束条件。

[1: end, % 即如果工作空间中同时存在end和read_facts两项时, 2: read_facts] ==>

[retract(all)]. % 就把这两项删除。

rule 2: %这个规则是循环体

[1: read_facts] % 如果工作空间中存在项read_facts, ==>

[prompt('Attribute? ', X), %提示用户输入数据,

assert(X)]. % 并把此数据写入到工作空间,注意如果用户输入end,规则1将先满足,从而删除read_facts项,使得规则2将不会再被满足。

动物识别的专家系统可以使用数据驱动或者是目标驱动的方法完成。事实上对于此类问题,目标驱动的方法更加合适一些。

数据驱动(前向链)最适合应用于配置某项东西。在附录中给出了一个在客厅中放置家具的专家系统。这个程序中包含一系列的放置家具的规则,例如:

床要放在较长的墙一边,并且不能够和门在一起。 电话应该在床的对面,一伸手就可以拿到。

28

如果灯或者电视机所靠的墙没有插座,就需要购买额外的电线。

下面是这几个规则的程序,注意在实际的程序中要更加复杂一些,因为还要考虑空间的利用问题。

% f1 - 床和门对着 rule f1:

[1: furniture(couch, LenC), % 找到尚未放置的床,LenC是此床所需要的空间,

position(door, DoorWall), % 找到有门的墙DoorWall,

opposite(DoorWall, OW), % 找到与有门的墙DoorWall相对的墙OW, right(DoorWall, RW), % 找到DoorWall右边的墙RW, 2: wall(OW, LenOW), % 墙OW所能够使用的空间LenOW wall(RW, LenRW), % 墙RW所能够使用的空间LenRW

LenOW >= LenRW, % 如果对面的墙的空间比右面的墙的空间大,

LenC = LenOW] % 而床需要的空间比对面的墙的空间小,也就是说能够放下床 ==>

[retract(1), % 删除1号条件,也就是说床已经就位了

assert(position(couch, OW)), % 写入床的就位信息,床放在了墙OW旁 retract(2), % 删除原来的纪录OW剩余空间的项。 NewSpace = LenOW - LenC, % 计算出新的空间大小

assert(wall(OW, NewSpace))]. % 并把墙剩余的空间加入到内存中。 请读者自己理解下面的规则的意思:

% f3 电视机应该和床对着放。 rule f3:

[1: furniture(tv, LenTV), 2: position(couch, CW), 3: opposite(CW, W),

29

4: wall(W, LenW), LenW >= LenTV] ==>

[retract(1),

assert(position(tv, W)), retract(4),

NewSpace = LenW - LenTV, assert(wall(W, NewSpace))]. % 是否需要额外的电线

rule f12:

[1: position(tv, W), 2: not(position(plug, W))] ==>

[assert(buy(extension_cord, W)), assert(position(plug, W))].

rule f13:

[1: position(table_lamp, W), 2: not(position(plug, W))] ==>

[assert(buy(extension_cord, W)), assert(position(plug, W))].

同样,这个系统也许要从用户那里获得一些初始信息。它需要知道房间的大小,所需要放置的家具,插头的位置等等。我们设置几个从用户那里收集信息的目标。例如这个系统的初始目标是收有关家具的信息,当此目标完成之后再设置第二个目标:获得关于墙的信息。下面是这段程序:

30

rule 1: %此规则提示用户输出家具方面的信息

[1: goal(place_furniture), % 当前的目标是收集家具信息 2: legal_furniture(LF)] %系统所能够配置的家具列表LF ==>

[retract(1), %删除当前的目标

cls, nl, %显示一些输入数据的时候需要注意的事项

write('Enter a single item of furniture at each prompt.'), nl, write('Include the width (in feet) of each item.'), nl, write('The format is Item:Length.'), nl, nl, write('The legal values are:'), nl, write(LF), nl, nl,

write('When there is no more furniture, enter \"end:end\".'), nl, assert(goal(read_furniture))]. %重新设置目标,开始读入家具信息。

rule 2: %家具信息读入完毕

[1: furniture(end, end), % 如果家具信息读入完毕 2: goal(read_furniture)] ==>

[retract(all), %删除当前目标

assert(goal(read_walls))]. %加入新的目标,开始读入墙的信息

rule 3: %循环读入家具信息 [1: goal(read_furniture), 2: legal_furniture(LF)] ==>

[prompt('furniture> ', F:L), member(F, LF),

assert(furniture(F, L))].

31

rule 4: % 如果上面的规则的运行失败,即输入的家具不在系统所能够处理的范围之内,此规则将运行 [1: goal(read_furniture), 2: legal_furniture(LF)] ==>

[write('Unknown piece of furniture, must be one of:'), nl, write(LF), nl].

上面的两段配置房间的程序显示了production系统的优点和缺点。放置家具的规则简单、清晰、易懂,并且可以任意的添加或者删除规则而不至于影响整个系统的运行。

另一方面,从用户那里收集数据的规则却是相当繁琐、晦涩难懂的。并且规则的次序也非常重要,不能够任意的颠倒次序。其实这一部分功能如果使用一般的过程性语言来编写是很容易的,这里不过是为了说明如何使用Oops这样的描述性语言来编写过程性的程序而已。

最好的办法莫过于取长补短,让Oops做它擅长的事情,不擅长的事情直接调用Prolog谓词来实现,这种方法将在后面介绍。(注意:这个Oops系统实际上是使用Prolog实现的,所以在Oops中直接调用Prolog谓词是轻而易举的事情)。

下面我们就来详细介绍如何使用Prolog来编写Oops的规则解释器。

使用Prolog来实现Oops即简洁又易懂,真是何乐而不为阿!那么为什么Prolog很容易实现Oops呢?下面是几个原因:

Oops中的每个规则可以使用单一的Prolog项来表示(不过这个项具有极其复杂的结构)。

32

可以把规则的结构符号==>定义成为Prolog的操作符,从而使的Oops的规则容易阅读。

Prolog的内建的回溯搜索功能,使得系统能够自动选择规则。

Prolog内建的模式匹配功能,使得很容易把工作空间的数据和规则的条件进行比对。

由于每个规则在Prolog表达为单一的项,因此在规则的LHS和RHS中的变量能够自动的绑定。

Prolog的动态数据库可以方面的表达工作空间的数据。

表达规则的Prolog项主要由两个列表组成,即RHS和LHS,(当然还包含其他一些信息)。这个项就是通常的Prolog的数据结构:rule为谓词名,然后是一系列的参数。

下面是鸟类识别专家系统的规则之一,

rule id4: [1: flies(X), 2: lays_eggs(X)] ==>

[assert(isa(X,bird)), retract(all)].

在prolog中我们使用如下的结构储存此规则:

rule(==>(:(id4, [:(1, flies(X)), :(2, lays_eggs(X))], [retract(all), assert(isa(X, bird))])).

33

这种表达有点难于阅读,如果我们把其中的几个符号定义为prolog的操作符,就可以很清晰的书写规则了:(有关操作符定义的具体内容,请参阅Prolog教程中的操作符一章。)

op(230, xfx, ==>). op(32, xfy, :). op(250, fx, rule).

工作空间的数据使用谓词fact储存,下面的子句将向工作空间中添加一条数据: asserta( fact(isa(robie, carnivore)) ),

这样工作空间中就多了一条子句:fact(isa(robie, carnivore))。

下图显示了在Oops推理引擎中使用的主要的谓词。

谓词go构成了整个推理引擎的循环。它首先找到与工作空间的数据匹配的第一个规则,并且运行这个规则的action部分。然后通过递归调用go,来重复上面的操作。如果找不到和工作空间数据匹配的规则,那么go的第二个子句将被调用,整个推理过程结束。go的第二个子句负责输出最后得到的工作空间的状态,也就是最终结果。 go:-

call(rule ID: LHS ==> RHS), %获得某个规则的LHS和RHS try(LHS, RHS), %测试并运行匹配的规则

write('Rule fired '), write(ID), nl, %输出所使用的规则 !, go. %循环

go:- %当第一个子句失败,也就是没有规则可以运用的时候,循环结束 nl, write(done), nl,

print_state. %输出工作空间的状态

34

代码十分简洁,这主要得益于prolog内建的模式匹配和回溯搜寻功能。特别要注意的是,由于我们采用单一的prolog项来表达规则,因此在规则的条件和动作(即LHS和RHS)部分的变量能够自动的进行匹配。

try/2谓词也很简单。如果match/2失败,则try/2失败,引起go谓词的回溯,从而考察下一条规则。

try(LHS, RHS):- %考察LHS,并且运行RHS match(LHS), %判断LHS是否与工作空间的数据匹配

process(RHS, LHS). %如果匹配,则运行RHS,注意在RHS中的某些动作需要LHS中的数据,因此把LHS也传递给谓词process/2。

谓词match/1递归的考察条件列表中的每个条件,并且和工作空间的数据比对。如果条件使用的是数值比较测试,则调用test/1,而不在工作空间中搜索。

match([]) :- !. %条件列表为空则结束

match([N:Prem|Rest]) :-%处理有编号的条件 !,

(fact(Prem); %此条件要么是工作空间中的事实 test(Prem)), %要么是比较数值大小 match(Rest). %递归调用处理剩下的条件列表

match([Prem|Rest]) :- %处理没有编号的条件 (fact(Prem); test(Prem)), match(Rest).

35

test/1谓词的程序如下:

test(X >= Y):- X >= Y, !.

test(X = Y):- X is Y, !. % use = for arithmetic test(X # Y):- X = Y, !. % use # for unification test(member(X, Y)):- member(X, Y), !.

test(not(X)):- fact(X), !, fail.

我们还可以增加test/1的子句,来实现更多的测试工作。

如果match/1成功,将调用process/2。process/2依次运行RHS中的动作。

process([], _) :- !.

process([Action|Rest], LHS) :-

take(Action, LHS), %调用take/2运行Action process(Rest, LHS).%处理剩下的动作列表

只有recract动作需要使用到LHS中的信息,因此我们使用take/2完成这种动作,而使用take/1完成其他的动作。

take(retract(N), LHS) :- %如果动作是retract

(N == all; integer(N)), %其中N要么是一个整数,要么是all。 retr(N, LHS), !. %调用retr/2来完成retract动作

take(A, _) :-take(A), !. %如果是别的种类的动作,则调用take/1实现。 take(retract(X)) :- retract(fact(X)), !.

take(assert(X)) :- asserta(fact(X)), write(adding-X), nl, !.

36

take(X # Y) :- X=Y, !. take(X = Y) :- X is Y, !. take(write(X)) :- write(X), !. take(nl) :- nl, !.

take(read(X)) :- read(X), !.

retr/2的程序如下:

retr(all, LHS) :-retrall(LHS), !.

retr(N, []) :-write('retract error, no '-N), nl, !. retr(N, [N:Prem|_]) :- retract(fact(Prem)), !. retr(N, [_|Rest]) :- !, retr(N, Rest). retrall([]).

retrall([N:Prem|Rest]) :- retract(fact(Prem)), !, retrall(Rest).

retrall([Prem|Rest]) :- retract(fact(Prem)), !, retrall(Rest).

retrall([_|Rest]) :- % must have been a test retrall(Rest).

retr/2找到LHS中的编号为N的条件,并且从工作空间中删除它。如果N为all,则删除LHS中的所有条件。这里要注意LHS中也许有测试条件,即工作空间中没有这样的数据可以删除,上面最后的retrall子句就是考虑这种情况的。

37

到此为止,我们就基本实现了数据驱动的推理引擎了。

38

第三章 OOPS改进

现在的Oops系统只是简单的前向链系统,使用Prolog实现前向链系统的解释功能。

解释

前向链系统的解释功能比较难以实现。这是因为在推理过程中,每个规则都有可能改变工作空间的状态,而这些状态的改变并没有被纪录下来,因此我们就无法知道当前的状态是如何由初始状态变化过来的。

为了实现解释的功能,我们可以把所使用的规则和工作空间的事实联系起来,这样我们就可以马上知道某个事实是由哪些规则的得到的。但是,某些规则会删除工作空间的事实,这样被删除的事实就无法得到解释了。为了得到全面的解释功能,系统必须标识这些要删掉的事实,而不是真正的删除它们。

增强功能

现在的Oops系统只是简单的前向链系统,我们还可以在下面两个方面增强这个系统的功能:

当有几个规则都与工作空间的数据匹配的时候,使用更有效的规则选择方法。 提高系统的性能。

当前的规则选择方法只是简单的选择第一条与工作空间数据匹配的规则。当有多个规则同时满足的时候,我们还可以有更多的规则选择方法。例如,我们可以选择与最近加入工作空间的事实匹配的规则。或者选择与工作空间的事实匹配最多的规则。这些规则选择方法都能够在一定程度上使整个系统更加完善。

规则的选择

OPS5系统(也许可以算得上最著名的前向链系统了)提供了两种选择规则的方法,它们分别叫做LEX和MEA。它们都通过纪录历史信息来选择最佳的规则。它们的区别在于使用这些历史信息的方法有所不同。这两种方法都可以加入到我们的Oops系统中。

39

这两种方法的第一步都是找到与工作空间数据匹配的所有的规则。我们把这样一组规则叫做冲突集合。事实上冲突集合中的元素并不是规则本身,而是规则的实例。所谓规则的实例是指把规则和工作空间的数据进行匹配之后的产物。也就是说规则中的变量都已经绑定为具体的值了。某个规则可以同时有几个实例,即工作空间中有多组事实都能够和这个规则匹配。

例如,下面是动物识别系统中的某个规则: rule 12: [...

eats(X, meat), ...] ==> ...

在工作空间中

可能存在如下的两条事实: ...

eats(robie, meat). eats(suzie, meat). ...

如果在LHS中的其他的条件都满足的话,这样的两条事实就会产生规则12的两个实例,一个是由robie产生的的,另一个是suzie产生的。

得到这种冲突集合的最简单的方法就是使用内部谓词findall,它的三个参数如下:

40

所要收集的实例的模板 要收集的实例所需要满足的条件 最终的收集的实例的列表 在这里我们所使用的模板如下:

r(Inst, ID, LHS, RHS),

ID,LHS,RHS分别是满足工作空间的条件的ID号、LHS和RHS,注意这三个参数表示的是满足条件的规则本身,而不是规则匹配之后的实例,规则匹配之后的实例存放在第一个参数Inst中。

得到冲突集合的程序如下:

conflict_set(CS) :-

findall(r(Inst, ID, LHS, RHS), %findall的第一个参数,即模板

[rule ID: LHS ==> RHS,%条件,这里有两个目标,第一个目标找到某条规则, match(LHS, Inst)], %第二个目标判断此规则是否与工作空间的数据匹配,如果匹配,则LHS匹配后的实例储存在Inst中。 CS).%最后的冲突集合。

我们还需要修改前面的match谓词。在前面介绍的match中,只能够判断规则的LHS是否与工作空间的数据匹配,而不能够返回匹配之后的实例,现在我们来完成这部分工作。

(注意:前面我们说过,需要使用纪录历史信息来选择最佳的规则,这里的历史信息就是工作空间中某条事实产生的时间。因此我们需要修改储存事实的结构fact。新的fact结构如下:

fact(somefact,time),第一个参数就是所储存的事实本身,第二个参数是这个事实加入工作空间的时间。为此我们需要编写新的加入事实的子句,而不是直接使用内部谓词asserta:

41

assert_ws(fact(X, T)) :-

getchron(T), %获得当前的时间,此时间每加入一条事实增加1 asserta(fact(X, T)). %加入带时间记号的事实

getchron(N) :-

retract( chron(N) ), %删除原来的时间,初始化工作空间的时候,工作空间中有chorn(0)。

NN is N + 1, %时间增加1

asserta( chron(NN) ), !.%加入新的时间

有了这样的时间记号,我们就可以很容易的知道事实加入到工作空间的次序,在选择应用的规则的时候,我们将用到这个信息。)

好了,最后我们看一下修改之后的match谓词:

match([], []) :- !. %递归终点

match([N:Prem|Rest], [Prem/Time|IRest]) :- %条件有编号N的情况 !,

(fact(Prem, Time); %如果条件是工作空间的事实,注意这几句话的末尾是';'表示或者的意思

test(Prem), Time = 0), %如果条件是测试语句,测试语句的时间信息为0。 match(Rest, IRest).%递归调用

match([Prem|Rest], [Prem/Time|IRest]) :- %条件无编号的情况 (fact(Prem, Time); test(Prem), Time = 0), match(Rest, IRest).

42

match/2的第二个参数返回的就是返匹配之后的实例。

LEX

也许你还不太明白前面做的这些工作的意义,不要紧,我们下面就进入正题了。

前面所过,OPS5系统提供了两种选择规则的方法,它们分别叫做LEX和MEA。我们先来看看LEX是如何从冲突集合中选择出要应用的规则的吧。

LEX选择规则有3个标准:

第一个标准是refraction(辞典中refraction的意思是折射,我想在这里的意思应该是re-fraction,即重组)

这个标准删除冲突集合中已经被使用过的实例。两个实例相同的标准是变量绑定的值和时间信息都相同。这个新版本的Oops中,每应用一条规则,都会把这条规则应用的实例记录下来,如果其后产生的冲突集合中有和以前纪录的实例相同的实例,则冲突集合中的这条实例所对应的规则将不会被运用。这样就避免了同样的规则重复的运用于同样的数据上面。如果程序员希望某个规则重复的运用于同样的数据上面,他必须在每次运用之后适当的改变工作空间中的数据。

第二个标准是recency(新近)

这个标准选择那些使用工作空间中最新的数据的规则。我们对冲突集合中的实例按照时间信息排序,排在前面的实例是与最新的事实绑定的实例。这样有利于系统专注于完成某个任务,最新加入的事实将能够马上得到运用,而不是打一换一个地方。

第三个标准是specificity(特殊性)

43

如果使用了上面的两个标准之后仍然有几个规则可以选择的话,就选择最特殊的规则。所谓规则的特殊性,是指此规则的LHS中的条件的多少,条件越多规则越特殊。这样可以使得条件多的规则首先被运用。

如果上面三个标准都满足之后仍然有多个规则的话,就随机的选择一个吧。

为了实现LEX的规则选择策略,我们必须对前面介绍的Oops的规则做适当的修改。在前面的Oops中,开发者必须确信当某个规则被运用之后,此规则将修改触发此规则的事实。这样才能够保证同样的规则不会总是和同样的数据绑定,而导致死循环。

例如前面曾经介绍过的鸟类识别的某个规则:

rule id6:

[1: has(X, pointed_teeth), 2: has(X, claws), 3: has(X, forward_eyes)] ==>

[retract(all), %注意这里删除了满足此规则的所有事实,这样这个规则就不会重复的作用于相同的数据上了。 assert(isa(X, carnivore))].

反过来,如果开发者希望某个规则被重复调用,他必须依靠规则的先后持续来完成这种功能。下面是我们前面介绍过的通过循环从用户获得数据的程序。

initial_data([read_facts]). %初始状态,在系统运行的时候,初始状态会写入工作空间。

rule 1: %这个规则定义了读入数据循环的结束条件。

[1: end, % 即如果工作空间中同时存在end和read_facts两项时,

44

2: read_facts] ==>

[retract(all)]. % 就把这两项删除。

rule 2: %这个规则是循环体

[1: read_facts] % 如果工作空间中存在项read_facts, ==>

[prompt('Attribute? ', X), %提示用户输入数据,

assert(X)]. % 并把此数据写入到工作空间,注意如果用户输入end,规则1将先满足,从而删除read_facts项,使得规则2将不会再被满足。

但是一旦系统具有LEX的规则选择功能之后,上面使用的方法就不适合了。这是因为新的Oops中,规则没有先后次序了,系统会一次性的找到所有可以运用的规则,然后按照前面的三个标准选择合适的规则。这样为了控制推理引擎,我们必须做更多的工作。

例如在放置家具的专家系统中,为了首先让系统放置床,如果使用以前的Oops,只需要把放置床的规则放到前面就行了,而现在则必须作如下的修改:

rule gather_data %从用户收集信息的规则 ... ==> [...

assert( goal(couch_first) ) ]. %此规则必须最后往工作空间中加入这样的标志,才能够保证先放置床。

rule couch %放置床的规则

[ goal(couch_first), %判断有无标示 ...

45

注意放置床的标志是最后加入到工作空间中的,也就是这个标志是最新的,这样第二个标准“新近”将起作用,从而导致放置床的规则先被使用。而放置床的规则可以放在程序中的任意位置,即和规则的次序无关。

如果我们希望系统最后使用某一条规则的话,也需要使用一些小技巧。最简单的方法就是让这个规则的LHS为空,这样第三个标准“特殊性”将使得这个规则最后被使用。

下面来看具体实现LEX还需要什么改变。

首先我们需要修改以前的go谓词。新版本的go谓词将首先取得冲突集合,然后把这个冲突集合传递给select_rule/2。select_rule/2使用前面介绍的三个标准找到将被执行的规则,然后再运行这个规则,最后还需要把这个规则的实例存入工作空间中,以共第一个标准检查之用。程序如下: go :-

conflict_set(CS),

select_rule(CS, r(Inst, ID, LHS, RHS)), process(RHS, LHS),

asserta( instantiation(Inst) ), write('Rule fired '), write(ID), nl, !, go. go.

select_rule/2的程序如下:

select_rule(CS, R) :-

46

refract(CS, CS1), lex_sort(CSR, [R|_]).

首先调用refract/2把重复的实例删除,也就是完成标准1。然后调用lex_sort/2对剩下的实例排序,找出最适合的规则。lex_sort/2同时实现了标准2和3。

我们再来看看refract/2是如何实现的:

refract([], []).

refract([r(Inst, _, _, _)|T], TR) :- instantiation(Inst), !, refract(T, TR).

refract([H|T], [H|TR]) :- refract(T, TR).

refract/2依次比对工作空间已经储存的实例和列表中的实例,如果相同就删除它。这段程序应该不难理解,再来看lex_sort/2的实现方法吧。

lex_sort/2运行之后并不会删除剩下的规则,而只是将冲突集合中的规则排序,以使得列表的第一个规则是所希望运行的规则。我们为每个规则进行评分,然后按照分值来排序。这个分值同时考虑了新近和特殊性两个因素。按照分值大小排序的工作由内部谓词keysort/2完成。

(有关keysort/2的说明:keysort/2有两个参数,第一个参数是待排序的列表,第二个参数是排序之后的列表,列表中的元素的形式如下为 key-term,key为排序的根据,term为实际需要排序的数据,例如:

47

?- keysort([1-a,5-b,3-c],X). X = [1 - a,3 - c,5 - b] ; no

keysort/2的排序关键字也可以是列表,例如:

?- keysort([[2,3,1]-a,[3,1,2]-b,[2,3,4]-c,[2,3]-d],X). X = [[2,3] - d,[2,3,1] - a,[2,3,4] - a,[3,1,2] - b] ; no

在amzi prolog中keysort/2是内部谓词,若你的prolog解释器没有此内部谓词,就需要自己编写。 )

lex_sort(L, R) :- build_keys(L, LK), keysort(LK, X), reverse(X, Y), strip_keys(Y, R).

其中,谓词build_keys为冲突集合中的每一项添加一个分值,即排序用的关键字,然后使用keysort/2对带关键字的冲突集合排序,由于keysort/2是按照从小到大的顺序排序的,而我们需要的是key最大的规则,因此调用列表谓词reverse/2颠倒列表中的元素的次序,最后调用strip_keys/2删除用来排序的关键字。

在这里我们的排序关键字是一个列表,此列表的元素就是与某规则的LHS中的某个条件匹配的事实的时间信息。这个列表关键字是排序过的,也就是说最近的时

48

间信息将在列表的前面。再对这个复杂的列表关键字进行排序就可以找出我们所需要的规则了。例如,考虑下面的两条规则,和工作空间的事实: rule t1: [flies(X), lays_eggs(X)] ==>

[assert(bird(X))]. rule t2: [mammal(X), long_ears(X), eats_carrots(X)] ==>

[assert(animal(X, rabbit))].

fact( flies(lara), 9).

fact( flies(zach), 6).

fact( lays_eggs(lara), 7).

fact( lays_eggs(zach), 8).

fact( mammal(bonbon), 3).

fact( long_ears(bonbon), 4).

fact( eats_carrots(bonbon), 5).

49

对于第一个规则将生成2个实例,一个是lara的,一个是zach的。对于第二个规则将生成一个bonbon的实例。这三个实例的已排序的列表关键字如下: [9, 7] [8, 6] [5, 4, 3]

使用这样的列表关键字排序的时候,是逐个比较其中的元素的,这样就可以满足“新近”标准的要求,而对于[4,5,6]和[4,5]这样两个关键字进行比较的时候,长的列表关键字将排在后面(注意最后我们还要颠倒排序后的列表,因此排在最后的元素,将成为列表的头),这样就满足了“特殊性”标准的要求。

下面是build_keys/2的程序:

build_keys([], []).

build_keys([r(Inst, A, B, C)|T], [Key-r(Inst, A, B, C)|TR]) :- build_chlist(Inst, ChL), sort(ChL, X), reverse(X, Key), build_keys(T, TR). build_chlist([], []).

build_chlist([_/Chron|T], [Chron|TC]) :- build_chlist(T, TC).

build_keys/2调用build_chlist/2来产生未排序的列表关键字,然后再使用内部谓词sort/2对此列表关键之中的元素排序,最后使用reverse/2颠倒列表关键字。

50

最后是strip_keys/2的程序,这个程序把最后的的列表中的关键字去掉:

strip_keys([], []).

strip_keys([Key-X|Y], [X|Z]) :- strip_keys(Y, Z).

MEA

最后让我们来简单的介绍一下另外一种选择规则的方法:MEA。

OPS5中的另外一种选择规则的方法是 MEA。这个方法和LEX类似,不过它增加了一个标准:在LEX的第一个标准完成之后,也就是删除了与工作空间的数据重复的实例之后,MEA考察与冲突集合中的所有实例的第一个条件匹配的事实的时间信息,然后选择出这个时间信息最大的那个规则运行,如果有几个相同的时候,再使用LEX中的第2、3个标准来选择。 也就是说规则的第一个条件有决定优先权的功能。

乍看起来这个算法似乎不合清理,但是加入了这样的标准之后,我们就很容易控制系统的流程了。前向链的系统流程一般是通过在工作空间中设置目标事实来实现的,规则的条件中有测试目标的子句,这样就可以确定某个规则只有在某个条件满足的时候才能够被运用。而如果让规则第一个条件有决定性的优先权, 就可以很容易的时间这种流程控制了,即只有在第一个条件的时间信息最大的时候,也就是满足最新的目标的时候,这个规则才会被运用。事实上,如果采用了MEA这种规则选择方法,我们甚至可以使用前向链系统实现后向链(目标驱动)的功能。

在LEX的基础上实现MEA很容易,首先修改select_rule谓词:

select_rule(R, CS) :- refract(CS, CS1),

51

mea_filter(0, CS1, [], CSR), lex_sort(CSR, [R|_]).

mea_filter/4检查工作空间中是否有strategy(mea).子句,如果没有则直接返回refract/2产生的冲突集合,如果有这个子句则运用mea的选择方法。这样我们就可以在系统运行的时候,通过往工作空间加入或者删除strategy(mea).来决定是否使用mea选择方法了。

下面是mea_filter/4的程序:

mea_filter(_, X, _, X) :- not strategy(mea), !. %不使用mea方法 mea_filter(_, [], X, X). %递归终点

mea_filter(Max, [r([A/T|Z], B, C, D)|X], Temp, ML) :- %小于的情况,不加入中间信息 T Max,

!, mea_filter(Max, X, Temp, ML).

mea_filter(Max, [r([A/T|Z], B, C, D)|X], Temp, ML) :- %等于的情况,加入到中间信息 T = Max,

!, mea_filter(Max, X, [r([A/T|Z], B, C, D)|Temp], ML).

mea_filter(Max, [r([A/T|Z], B, C, D)|X], Temp, ML) :- %大于的情况,置换原来的中间信息 T > Max,

!, mea_filter(T, X, [r([A/T|Z], B, C, D)], ML).

我们使用第三个参数保存中间结果,如果某个规则的第一个时间信息小于最大的时间信息,这个规则将不会被加入中间结果中;如果等于,则加入中间结果;如果大于则把中间结果以前的规则替换掉。

52

到此为止,前向链系统就基本介绍完了。使用这个专家系统框架的知识工程师在把原始知识转换为系统的规则的时候,他必须对系统的运行情况非常了解。他必须知道什么时候使用什么规则选择策略,这样才能够有效的使用此专家系统的中的功能。

53

第四章 frame(框架)

介绍更深的有关专家系统方面的知识,如何用Prolog开放专家系统并使用复杂的数据结构。

frame(框架)

(我把frame翻译为框架也许不太准确)

直到现在为止,我们介绍的专家系统中所使用的数据结构都是十分简单的。而在实际的应用当中我们总是希望在数据结构中加入一些智能化的东西,例如:自动赋缺省值,自动计算某些值,以及表达数据之间的关系等等。

在众多的有关智能数据结构的研究中,以框架为基础的实现方案应该算是最流行的了。系统中某个对象的信息被保存在框架中。框架使用slot储存对象的多个属性。而每个slot又有多个facet用来储存属性的值、缺省值或者计算属性值的程序。

不同的框架可以有层次的连接在一起,某些框架可以继承其他框架的信息。例如:兔子和老鼠的框架可以继承哺乳动物框架的信息。在哺乳动物框架中储存的是所有哺乳动物都具有的属性和属性值。这些属性和属性值直接被兔子和老鼠的框架继承,而不需要特别说明。哺乳动物框架中还可以储存一些属性以及其缺省值,这样的缺省值可以被继承它的框架覆盖,例如可以在哺乳动物框架中储存“腿-4条”这个缺省值,而在猴子的框架中使用“腿-2条”覆盖它。对于一般的哺乳动物(例如兔子、老鼠)的框架就不需要覆盖这个值。

框架系统的另外一个特点就是守护程序(demon)。所谓守护程序就是在某种条件下自动激活的程序。例如在某个商业运用中我们可以使用守护程序监视收支平衡,一旦收支平衡超过某个限度,这个守护程序就自动运行,以完成某些任务。这种守护程序还用来可以对数据类型进行检测,防止不合理的数据出现。

54

下图是框架的一个例子:

兔子(rabbit)和猴子(monkey)的框架继承了哺乳动物(mammal)框架的信息,因此需要在这两个框架中添加一条属性,表示它们继承了哪个表格:第一行的ako属性就是表示这种继承关系的。ako的意思是:a kind of。在mammal框架中,使用守护程序检测腿的数目是否小于等于4。在猴子的框架中覆盖了mammal框架中的legs的缺省值。

程序

为了使用prolog实现框架系统,我们需要考虑两件事情:用户界面和储存框架信息的数据结构。

我们使用下面三个谓词来存取框架系统的数据。

get_frame - 取得框架的某个属性值

add_frame - 添加或者修改框架的属性值

del_frame - 删除属性值

从用户的角度来看,这些操作和操作数据库的纪录是完全一样的。框架相当于一条纪录,框架中的某一个属性(slot)相当于纪录的域。框架系统中的智能功能,例如继承、缺省值、守护程序对于用户来说都是自动运行的。

(注:一个框架储存的是关于某个对象的信息,例如为了储存关于某个班的学生的信息,我们需要为每个学生生成一个框架,用来储存这个学生的相关信息,从这一点来看,框架和数据库中的纪录是一样的,所不同的是框架附带许多智能功能,在后面的讨论中我们会逐渐了解这一点)

55

上面3个谓词的第一个参数是框架的名字,第二个参数是框架的slot的列表,每个slot使用“属性-属性值”的形式表达。例如为了取得Dennis的身高体重信息,我们使用如下的子句:

?- get_frame(dennis, [weight-W, height-H]). W = 155 H = 5-10

为mary喜爱的体育运动添加一项橄榄球则是:

?- add_frame(mary, [sport-rugby]).

而如果mary原来喜欢跑步,而现在不喜欢跑步了,就使用del_frame:

?- del_frame(mary, [sport-run]).

这三个基本的存取框架数据的谓词还可以有更加复杂的运用,例如我们可能希望找到框架数据库中所有的女性橄榄球手:

?- get_frame(X, [ako-woman, sport-rugby]). X = mary ; X = kelly;

假设我们需要找到有共同爱好的男人和女人,则可以使用下面的程序:

in_common(M, W, H) :-

get_frame(M, [ako-man, hobby-H]), get_frame(W, [ako-woman, hobby-H]).

56

框架的表达

下面我们讨论使用什么样的具体的数据结构来表达框架(当然我们讨论的是使用prolog的数据类型)。框架是相当复杂的结构,它具有框架名和多个属性,每个属性可以拥有属性值,这和普通的数据库中的纪录是一样的。所不同的是框架系统中的属性具有多个面(facet)。即属性可能有缺省值、计算属性值的谓词、以及监视属性值的守护程序。

除此之外,框架还可以有层次关系,每个框架都可以有一个ako属性用来储存这个框架所继承的框架,在某且情况下所继承的框架还有可能不止一个。

我们使用谓词frame/2来储存框架信息。第一个参数是框架名,第二个参数是属性列表,正如前面所说的那样,每个属性有可能有多个面,因此关于某个属性的信息需要使用相当的结构来储存。下面是我们使用的储存框架信息的结构:

frame(name, [ %框架名 slotname1 - [ %属性名 facet1 val11, %属性的某个面 facet2 val12, ...],

slotname2 - [ %属性名 facet1 val21, facet2 val 22, ...], ...]).

其中facet1 facet2为下面的值:

val - 若facet的值为val表示,后面的值就是这个属性的值 def - def表示后面的值是属性的缺省值

57

calc - calc后面定义的是计算属性值的谓词

add - 当增加属性的某个值的时候调用add后面的谓词 del - 当删除属性的某个值的时候调用del后面的谓词 也许你还没有完全搞明白,不要紧,让我们来看几个例子:

frame(man, [

ako-[val [person]], hair-[def short, del bald], weight-[calc male_weight] ]).

框架名为man,继承框架person,hair属性的缺省值是short,当删除hair属性的时候调用bald谓词。当体重没有属性值时,调用谓词male_weight计算。

frame(woman, [ ako-[val [person]], hair-[def long],

weight-[calc female_weight] ]).

框架名为woman,继承person,hair的缺省值为long,当体重没有属性值时,调用谓词female_weight计算。

当我们具体定义某个人的框架的时候,可以继承上面的框架的信息。

另外,框架中某个属性的属性值有可能只有一个,例如hair,而有些属性可以是多值的,例如爱好。单属性值使用项储存,多属性值使用列表储存,all_frame,del_frame可以自动的处理这些情况。

谓词的实现

我们首先来编写谓词get_frame,下图表示了get_frame所调用的谓词:

58

我们采用自顶向下的编写方法,先来看最上层的程序:

get_frame(Thing, ReqList) :-

frame(Thing, SlotList), %找到Thing框架的属性列表SlotList slot_vals(Thing, ReqList, SlotList).

slot_vals把属性列表SlotList和用户的询问模板列表ReqList进行匹配。它是个标准的递归处理列表的谓词,每一次从询问模板列表RegList中取出一项来处理。处理的第一步是把取出来的项转化为更加正规的的形式。在这里我们用req/4结构表达转化后的形式,它的四个参数分别为:

框架的名字 被请求的slot 被请求的facet 被请求的值。

例如,假设我们调用如下谓词:

get_frame(dennis, [weight-W, height-H]).

那么[weight-W, height-H]就是所谓的询问模板列表,solt_vals会每次从这个列表中取出一项来处理。即首先取出weight-W,然后是height-H。当取出weight-W项以后,还需要把这个项转化为更加正规的形式即

req(dennis,weight,val,W)。(表示dennis这个框架中的weight这个slot中的val这个facet中的值为W,由于W为变量,在后面的处理中会绑定为我们所需要的值。)

solt_vals可以同时识别如下两种形式:

?- get_frame( dennis, hair-X ).

59

?- get_frame( dennis, [hair-X, height-Y] ).

即当用户询问的模板列表只包括一项的时候,可以直接写出这项,而不需要使用表示列表的[]扩起来。

slot_vals是标准的递归处理列表的谓词,它依次处理列表中的每个元素。实际的匹配工作是靠find_solt完成的。

% solt_vals/3, 参数1:框架名。参数2:用户询问列表。参数3:系统中保存的框架的属性列表。

% 目的:把用户的询问列表和框架的属性列表进行匹配。

slot_vals(_, [], _). % 递归终结条件

slot_vals(T, [Req|Rest], SlotList) :- % 处理用户的询问模板是列表的情况 prep_req(Req, req(T, S, F, V)), % 调用prep_req把询问模板列表中的Req项转化为req/4结构,以便后续的谓词进行操作。

find_slot(req(T, S, F, V), SlotList), % find_slot进行实际的匹配工作。 !, slot_vals(T, Rest, SlotList). % 递归处理尾列表

slot_vals(T, Req, SlotList) :- %处理不是列表的情况 prep_req(Req, req(T, S, F, V)), find_slot(req(T, S, F, V), SlotList).

询问模板列表中的元素都有如下的形式: Slot-X

把上述形式转化为正规的req/4结构的谓词prep_req,必须考虑如下3种情况:

60

X是变量:在这种情况下,我们的目的是询问Slot的值,也就是名为val的facet X是Facet(Val)形式:这种情况是询问特定的facet的值 X不是变量:这种情况是用来和slot的值进行比较 下面是prep_req/2的程序代码:

prep_req(Slot-X, req(T, Slot, val, X)) :- var(X), !.% 当X为变量的时候,即第一种情况(询问val这个facet值)

prep_req(Slot-X, req(T, Slot, Facet, Val)) :- nonvar(X), % X 不是变量

X =.. [Facet, Val], % 而是Facet(Val)的形式 facet_list(FL), % 获得所有的facet列表,

member(Facet, FL), !. % 如果用户所询问的facet在此列表中,则允许查询。 prep_req(Slot-X, req(T, Slot, val, X)). % 最后一种情况,既不是变量,又不是Facet(Val)的形式。

facet_list([val, def, calc, add, del, edit]).% 定义facet的所有可能的名字

例如,如果询问是:

?- get_frame(dennis, [hair-X]).

则会自动产生一个更加正规的询问: req(dennis, hair, val, X)

61

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- 91gzw.com 版权所有 湘ICP备2023023988号-2

违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务