Prolog开发专家系统-解释

Prolog开发专家系统-解释专家系统的一个重要的功能就是要能够解释它自己的行为用Prolog开放具体解释能力的专家系统解释专家系统的一个重要的功能就是要能够解释它自己的行为这意味着用户可以在任何时候询问系统为什么得出某个结论,或者为什么提出某个问题这对于用户来说是一项重要的功能,有时候用户只要求知道答案,可是有时候用户需要知道解释,而通常的专家系统无法对它的行为做出有说服力的解释,而只能够告诉用户它使用了哪些规则得出的结论,至于为什么这些规则能够得出这样的结论,系统是无法解释的例如下面这个例子:汽车能够启动么? 不行引擎发动了么? 是的你问到汽油味道了么?是的建议:等待5秒钟,然后再试为什么?因为我使用了这样的规则:如果不能够启动而且引擎发动了而且问到汽油味,那么就推荐的等待5秒再试 很显然这个专家系统无法解释其选择某个规则的原因,而只能告诉用户它使用了某种规则如果用户硬要刨根问底的话这个系统就无能为力了为了让系统具有真正的解释功能,我们需要比规则更多的知识对每个规则进行注释是一个比较好的方法,这种方法将在以后的章节介绍还有一种方法就是把更多的知识进行编码,推理引擎和解释引擎都同时使用这个知识库。
还有些专家系统的知识库是属于经验知识,在这种情况下系统的解释可以直接使用规则像识别鸟类的分类系统就属于这种情况鸟类识别系统就能够使用它的规则直接进行解释,例如为什么某种鸟是野鸭,就是因为它具有野鸭的一些特性,而这些特性就是规则所定义的识别鸟类并不存在什么高深的理论,而只是根据某些特点进行分类的也许对于用户来说某些解释是多余的,不过对于开发人员来说这是十分重要的这和通常的语言中的跟踪调试有些类似当系统没有按照预期的效果执行的时候,开发人员可以根据解释研究错误的产生原因知识工程师也可以根据解释从而设计出更加贴近用户的知识库 解释的种类在一般的专家系统中常用的有4种解释1. 报告当前的会话进程2. 解释系统是如何得出某个结论的3. 解释为怎么系统向用户询问某个问题4. 解释为什么某个结论不成立在我们上一章介绍的Clam外壳程序中,推理引擎是自己编写的,所以这些解释特性并不难加入系统当中在第一章的原始外壳中没有推理引擎,而是使用prolog的内部引擎,这样就无法加入新的解释特性,为了达到这个目的,我们需要编写自己的推理引擎,而这个引擎的运作方式和prolog相同,也就是说需要使用prolog编写一个prolog,好在这项工作并不难完成。
在Clam中使用解释首先让我们看看在Clam中加入了解释的一个例子,这里沿用了上一章汽车诊断系统 首先用户打开对话跟踪功能,跟踪的信息使用粗体字表示,跟踪信息显示了系统是如何调用规则的注意系统正确的表示出了规则的嵌套调用报告当前的会话进程的解释:consult, restart, load, list, trace, how, exit:trace onconsult, restart, load, list, trace, how, exit:consultcall rule 1 Does the engine turn over?: nocall rule 2 Are the lights weak?: yesexit rule 2 call rule 3 Is the radio weak?: yesexit rule 3 exit rule 1 call rule 4 fail rule 4call rule 5 fail rule 5call rule 6 fail rule 6problem-battery-cf-75done with problem下面来看看如何解释为什么要向系统提问。
用户可以在任何时候向推理引擎询问why,请看这个例子:...Is the radio weak?: whyrule 3 If radio_weak Then battery_bad 50 rule 1 If not turn_over battery_bad Then problem is battery 100 goal problem ...这里可以看出来当用户向系统询问为什么问is the radio weak这个问题的时候,系统把有关这个问题的几个规则列出来了再来看看how提问,当系统给出了某个结论的时候,用户可能想知道是如何得到这个结论的,这个时候向系统询问howproblem-battery-cf-75done with problemconsult, restart, load, list, trace, how, exit:howGoal? problem is batteryproblem is battery was derived from rules: 1 rule 1 If not turn_over battery_bad Then problem is battery 100 在这里列出了能够直接得到结论的规则。
如果用户需要继续知道为什么battery_bad的话,就进行下面的询问:consult, restart, load, list, trace, how, exit:howGoal? battery_badbattery_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的结论,系统把它们都列举出来了看完了示范,该是研究程序的时候了 跟踪首先我们来看看如何制作跟踪功能这个跟踪功能可以向用户报告某个规则的调用、退出以及失败几个事件 这里使用谓词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 ).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) :- 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,也就是说是哪个规则支持的需要更新的这个factupdate(Goal, NewCF, CF, RuleN) :- 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.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]) :-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命令加入外壳程序的命令列表: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([]).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]), !.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, 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 answerwrite_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解释器,这项工作很容易完成当编写完成了自己的推理引擎之后,就可以很方便的处理解释了推理引擎首先要能够读取规则,在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) :- !. 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) :-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) :-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的提问,也就是说系统可以告诉用户为什么某个结论不成立。
这段程序和前面的解释器类似,不过它能够显示出目标是在什么地方失败的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.而e不成立,所以最终的结果是a也不成立用户询问whynot a,系统只能够告诉用户是因为c不成立当用户问whynot c的时候,系统才会显示出是因为e不成立。
这就是让用户不停的询问的方法如果当用户询问whynot a,系统能够自动的递归寻找失败的原因,它就会直接告诉用户是因为e不成立导致c不成立,从而导致a不成立的。