上一章我们详细的叙述了如何时候目标驱动法来编制专家系统。这种目标驱动法可以很好的解决像识别鸟类那样具有明显结构层次的问题。 在前面这个系统中用户对问题的的答案都是百分之百的肯定或者否定的。也就是说都具有确切的答案。不过解决许多实际问题的时候,专家通常都考虑许多不确定的因素,而用户在回答问题的时候,也不会都是百分之百的肯定。例如在鸟类识别系统中,有些鸟的颜色就很难确定。
在本章中,我们要来讨论具有非确定因素的专家系统,这里我们将建造一个能够处理非确定因素的目标驱动的外壳程序Clam。这里的推理机构已经不同于纯prolog的模式匹配和回溯,。所以在这个外壳程序中我们也包含了相应的推理机构。
可信度
为了能够处理非确信因素,我们为每一条规则或者信息引入可信度的概念。在推理过程中,推理机构自动的处理不同的可信度的组合。
我们还是从一个例子开始吧。在这个例子中可信度(cf)是从-100到100的整数。
这是一个用来诊断汽车为什么不能够发动的专家系统,请注意我们引入的cf。
goal problem.
rule 1
if not turn_over and
battery_bad
then problem is battery.
rule 2
if lights_weak
then battery_bad cf 50. %注意cf为50,就表示是电池损坏的可信度是50。
rule 3
if radio_weak
then battery_bad cf 50.
rule 4
if turn_over and
smell_gas
then problem is flooded cf 80.
rule 5
if turn_over and
gas_gauge is empty
then problem is out_of_gas cf 90.
rule 6
if turn_over and
gas_gauge is low
then problem is out_of_gas cf 30.
ask turn_over
menu (yes no)
prompt 'Does the engine turn over?'.
ask lights_weak
menu (yes no)
prompt 'Are the lights weak?'.
ask radio_weak
menu (yes no)
prompt 'Is the radio weak?'.
ask smell_gas
menu (yes no)
prompt 'Do you smell gas?'.
ask gas_gauge
menu (empty low full)
prompt 'What does the gas gauge say?'.
以上这些就是Clam外壳程序所定义的知识库的格式。这个知识库同样也是目标驱动的。在这个知识库中允许可信度相加。例如规则5和6的结论都是“汽车没有气了”,不过是从不同的角度得出的结论,规则2和3则是说“电池没有电了”,不过也都不是绝对确信的。
使用Clam外壳程序和上面的知识库联接起来以后,就是一个完整的可以处理肥缺性因素的专家系统了,下面是某次对话的实例:
consult, restart, load, list, trace, how, exit
:consult
Does the engine turn over?
: yes
Do you smell gas?
: yes
What does the gas gauge say?
empty
low
full
: empty
problem-out_of_gas-cf-90
problem-flooded-cf-80
done with problem
注意这个推理机构和prolog有所不同,但系统找到某一个答案以后,并没有停止搜索,它会找出所有的答案,并且给每个答案给出确信度,我们可以看出,这个确信度并不是概率值,它只是简单的为每个答案评分。
在用户回答系统的问题的时候同样可以使用确信度。下面是这种对话的例子:
:consult
Does the engine turn over?
: yes
Do you smell gas?
: yes cf 50 %用户的回答使用了确信度
What does the gas gauge say?
empty
low
full
: empty
problem-out_of_gas-cf-90
problem-flooded-cf-40
done with problem
注意在这个例子中用户只有50点的确信度是闻到了汽油的气味。系统考虑用户对答案的确信度, 从而改变它对结果的确信度。
系统还会对确信度进行联合,在上面的例子之中有两个规则都推导出是电池坏了,如果这两个规则同时成立,那么是电池坏了的确定度就会提高。下面是这个例子:
:consult
Does the engine turn over?
: no
Are the lights weak?
: yes
Is the radio weak?
: yes
problem-battery-cf-75
done with problem
在这个例子中,系统联合了两种出现电池损坏情况的确信度50,从而得出最终的确信度为75。
确信度的性质
确信度有许多使用的方法,在系统的推理过程中确信度的继承传播的途径也有许多钟,我们可以把它们大致分为以下几类:
有些规则的结论是非确信的
有些规则的前提是非确信的
用户输入的答案是非确信的
联接非确信的前提和非确信的结论
更新工作空间中的非确信的数据成为新的非确信的数据
建立确信度的极限,一旦低于这个极限,某种非确信的前提就不成立了。
在早期的人工智能系统中,MYCIN是比较成功的应用确信度的实用系统之一,这个系统用来诊断传染病。直到目前为止许多商用专家系统还在使用这种技术。
MYCIN的确信度
设计MYCIN确信度的目的是让我们的专家系统很够得出和真正的专家相符的答案。也就是说要按照专家那样思考,考虑众多的不确定因素,最后给出的答案也可能是不确定的。较深确信度理论牵涉到概率论以及对复杂的真是的系统进行统计研究。
前面我们介绍了两种把确信度加入系统的方法,一种是在规则中加入确信度信息,一种是用户在输入回答问题的时候加入确信度信息。
在专家系统的工作空间中,除了要保留以前的属性以及其值还要保存确信度CF。
在我们的规则中,规则的前提的确信度是100,就是说如果工作空间中保存这下面两条信息的话:
turn_over cf 100
smell_gas cf 100
那么根据规则4:
rule 4
if turn_over and
smell_gas
then problem is flooded cf 80
就会把结论problem flooded cf 80加入到系统的工作空间中。
计算前提的确信度
虽然在规则中定义的前提的确信度都是100,但是在真正的问题当中,前提不可能是100%成立的。这样我们的专家系统就必须能够自动的处理前提的确信度。这里我们使用最简单的算法。前提的确信度等于前提中所有子目标中最小的确信度的值。例如,如果工作空间中保存着下面的两个信息:
turn_over cf 80
smell_gas cf 50
那么规则4的前提的确信度就是这两个中间较小的一个:50。
联合前提确信度和规则确信度
前提确信度指的就是已知的前提的可信程度,而规则确信度指的是通过肯定的前提推出某种结论的可信度。例如前面的规则4推出problem is flooded的确信度是80。
那么当前提的确信度不是100%的时候,得出的结论的确信度应该是多少呢?我们使用下面的算法来计算:
CF=RuleCFPremiseCF/100
如果工作空间中存储如下信息,那么根据上面说的确定前提确信度的方法,RremiseCF就等于50。
turn_over cf 80
smell_gas cf 50
而规则4的确信度RuleCF是80,
那么最终的结论problem is flooded的确信度就是CF=8050/100=40。
前提确信度的极限
当某个前提的确信度小于一定的数值时,我们就认为该前提不成立,从而也不是用该前提进行推理。在这里我们所使用的最小的确信度是20。也就是说如果工作空间中储存如下的信息:
turn_over cf 80
smell_gas cf 15
那么系统是不会使用规则4进行推理的,因为前提确信度小于20。
联合目标确信度
假设系统中有多个规则都得出了同样的结论,每个规则都会得出一个确信度。如何把这些确信度联合起来得出最终的结论的确信度呢?
在某个规则得到结论以后,系统会搜寻工作空间来察看是否已经存在了这样的结论,如果存在,就要把这两个确信度进行运算,从而得出新的确信度。运算公式如下:
CF(X, Y) = X + Y(100 - X)/100. X, Y both > 0
CF(X, Y) = X + Y/1 - min(|X|, |Y|). one of X, Y 0
CF(X, Y) = -CF(-X, -Y). X, Y both 0
X,Y表示从不同的规则得出的某个结论的确信度,CF(X,Y)就把这两个确信度进行联合。
我们来看一个例子,在前面的诊断汽车启动故障的专家系统中,规则2和3的结论都是battery_bad。
rule 2
if lights_weak
then battery_bad cf 50.
rule 3
if radio_weak
then battery_bad cf 50.
那么如果工作空间中储存如下的信息:
lights_weak cf 100
radio_weak cf 100
两个规则都会得出battery_bad cf 50的结论,利用前面的公式运算,就会得出最终的结论battery_bad cf 75。
从这个例子中我们不难看出,为什么要自己编写Clam的推理引擎。我们必须找出所有支持或者反对某个结论的证据,然后把它们的确信度进行运算,从而得出最终答案的确信度。而prolog的推理机构只能够一次找到一个答案,并且只有成功或者失败两种结果。
规则的格式
既然是使用我们自己编写的推理引擎,那么我们就可以自己定义规则的格式,而不需要使用prolog提供的标准格式,怎样就更加易于用户理解阅读规则。它的基本形式如下:
rule(Name,LHS,RHS).
LHS是前提,而RHS是结论。Name是规则名称。
在prolog提供的标准的规则结构中是这样的形式:
RHS:-LHS.
对于RHS,它包括结论以及这个结论的可信度。
rhs(Goal,CF).
LHS则包括一系列的子目标用来证明或者推翻RHS。
lhs(GoalList).
其中GoalList是子目标的列表。
下面来定义目标的格式,在这里我们使用最简单的目标格式:属性以及其值。例如gas_gauge(汽油表)是一个属性,而low则是它的值。有些属性具有布尔值,就是说只有是或者不是两种值。我们定义的一种结构贮存这种值:
av(Attribute,Value).其中Attribute,Value都是简单的原子。
完整的规则结构如下:
rule(Name,
lhs( [av(A1, V1), av(A2, V2), ....] ),
rhs( av(Attr, Val), CF) ).
前面的规则5按照这种形式表达的话就是:
rule(5,
lhs( [av(turns_over, yes), av(gas_gauge, empty)] ),
rhs( av(problem, flooded), 80) ).
很明显这种规则的定义并不是很容易阅读。不过它对于我们的推理引擎是再合适不过的了。在今后的我们会介绍两种增进规则的阅读性的方法,一种是使用定义操作符的方法,另外一种是使用prolog内建的DCG功能。(关于DCG在prolog的语言教学的最后一章:自然语言中有介绍)
推理引擎
到目前为止我们已经定义了自己的知识表达的规则,下面我们就来开发自己的推理引擎。首先我们来总结一下推理引擎需要完成的工作。
如前面说叙述的那样处理可信度。
维护工作空间的数据。
找到关于某个属性的所有的信息,并且保存到工作空间中。
推理引擎所使用的主要的谓词如下:
findgoal
fact
askable
query_user
fg
rule
prove
findgoal
adjust
update
fact
combine
这里使用树状结构表达谓词之间的层次,下面我们都会分别详细介绍。
存储空间的格式
让我们首先来定义存储空间中储存的格式。我们使用下面的格式储存:
fact(av(A,V),CF).
使用fact储存已经知道的事实。
寻找属性的值
我们从寻找某个目标的值开始编写推理引擎。在这个汽车专家系统中我们是希望找到problem这个属性的值。主谓词为findgoal/2,在这个汽车专家系统中我们可以在解释器中输入下面的询问以开始和专家系统的对话:
?-findgoal(av(problem,X),CF).
findgoal需要处理三种不同的情况:
属性值已经存在与工作空间;
有能够推理出属性值的规则;
必须对用户进行询问。
我们使用askable谓词定义那些属性是需要从用户那里获得的,同时也定义询问的提示,例如:
askable(live,'Where does it live?').
使用前面的fact谓词和askable谓词,我们可以来编写findgoal了。
findgoal的第一个子句用来处理工作空间中已经存在的信息。如果在工作空间中找到了所需要的信息,就不需要执行其他的子句了,因此最后面有个截断符号。
findgoal(av(Attr,Val),CF):-
fact(av(Attr,Val),CF),
!.
如果在工作空间没有找到相关的信息,而所要询问的属性被定义为从用户那里获得的时候,就向用户提问。
findgoal(av(Attr, Val), CF) :-
not fact(av(Attr, _), _),
askable(Attr, Prompt),
query_user(Attr, Prompt),
!,
findgoal(av(Attr, Val), CF).
query_user谓词向用户询问属性值以及它的确信度,然后把用户的回答加入到工作空间中,而最后的递归调用findgoal则会找到query_user加入工作空间的信息。
query_user(Attr, Prompt) :-
write(Prompt),
read(Val),
read(CF),
asserta( fact(av(Attr, Val), CF)).
最后我们要来编写从规则推算出属性值的子句。注意,我们的推理引擎需要找出和目标有关的信息,然后合并确信度。这个工作有fg谓词完成。fg使用尾递归的方法找出所有从RHS可以推导出目标属性值的规则。当属性值的确信度为100,或者所有的规则搜索完毕的时候,就是fg递归的边界条件。
findgoal(Goal, CurCF) :-
fg(Goal, CurCF).
fg(Goal, CurCF) :-
rule(N, lhs(IfList), rhs(Goal, CF)), %寻找某个包含目标Goal的规则
prove(IfList, Tally), %计算前提可信度
adjust(CF, Tally, NewCF), %根据规则确信度和前提确信度计算新的确信度。
update(Goal, NewCF, CurCF), %更新数据。
CurCF == 100, !. %如果确信度为100就停止搜索。
fg(Goal, CF) :- fact(Goal, CF).
在fg中调用了3个新的谓词:
prove-检查LHS前提,并且计算它的确信度。
adjust-联合LHS确信度和RHS确信度。
update-更新工作空间中的值。
这个新的推理引擎的工作原理如下:首先找到RHS和目标匹配的规则,然后把这个规则的LHS(前提)传递给prove,prove计算前提的确信度,为了计算前提的确信度,就需要递归调用findgoal。如果prove失败,那么就回溯到上一个目标,也就是寻找下一个符合目标的规则。
prove(IfList, Tally) :-
prov(IfList, 100, Tally).
prov([], Tally, Tally).
prov([H|T], CurTal, Tally) :-
findgoal(H, CF), %找到前提H的确信度CF,
min(CurTal, CF, Tal),%比较CF和当前的最小的确信度CurTal,返回其中较小的一个。
Tal >= 20, %判断前提确信度大于极限确信度。
prov(T, Tal, Tally). %递归处理剩下的列表。
min(X, Y, X) :- X = Y, !.
min(X, Y, Y) :- Y = X.
prove的输入参数是规则的前提列表,而输出则是这个前提列表的确信度。prove调用prov,prov多一个保存临时数据的参数。prov递归调用它自己来逐个考虑前提列表中的每个前提,而它递归调用findgoal找到每个前提的确信度。由于前提确信度就是所有前提中最小的确信度,所以使用min谓词寻找最小的值。每一次保存的临时数据都是到目前为止最小的前提确信度。
当prove成功的找到了前提确定度以后,就使用adjust谓词联合前提确信度和规则确信度,从而得到最终的目标确信度。
adjust(CF1, CF2, CF) :-
X is CF1 CF2 / 100,
int_round(X, CF).
int_round(X, I) :-
X >= 0,
I is integer(X + 0.5).
int_round(X, I) :-
X 0, I is integer(X - 0.5).
由于系统的工作空间中可能已经存在关于某个目标的确信度,所以要使用update来联合通过不同的途径计算的某个目标的几个确信度。即完成前面所说的联合目标确信度的工作。
update(Goal, NewCF, CF) :-
fact(Goal, OldCF),
combine(NewCF, OldCF, CF),
retract( fact(Goal, OldCF) ),
asserta( fact(Goal, CF) ),
!.
update(Goal, CF, CF) :-
asserta( fact(Goal, CF) ).
combine(CF1, CF2, CF) :-
CF1 >= 0,
CF2 >= 0,
X is CF1 + CF2(100 - CF1)/100,
int_round(X, CF).
combine(CF1, CF2, CF) :-
CF1 0,
CF2 0,
X is - ( -CF1 -CF2 (100 + CF1)/100),
int_round(X, CF).
combine(CF1, CF2, CF) :-
(CF1 0; CF2 0),
(CF1 > 0; CF2 > 0),
abs_minimum(CF1, CF2, MCF),
X is 100 (CF1 + CF2) / (100 - MCF),
int_round(X, CF).
这几个谓词比较简单,这里就不详细的解释了,其实就是按照前面的公式直接编写出来的。
最后还要加上能够处理否定的子句,因为某个规则也可能是“如果不....就....”的形式。在这里我们加入一个新的findgoal子句来处理这种情况:
findgoal(not Goal, NCF) :-
findgoal(Goal, CF),
NCF is - CF, !.
它可以解释为如果Goal的确信度是CF,那么not Goal确信度就是-CF。
这个子句应该放在所有的findgoal子句的前面。
到此为止,我们的推理引擎就全部制作完毕了,怎么样,理解了么?
外壳程序
这里所使用的外壳程序和上一章所介绍的类似,就不再多解释了。这里的top_goal和上一章的有所不同,请自己体会。
super :-
repeat,
write('consult, load, exit'), nl,
write(':'),
read_line(X),
doit(X),
X == exit.
doit(consult) :- top_goals, !.
doit(load) :- load_rules, !.
doit(exit).
top_goals :-
top_goal(Attr),
top(Attr),
print_goal(Attr),
fail.
top_goals.
top(Attr) :-
findgoal(av(Attr, Val), CF), !.
top(_) :- true.
print_goal(Attr) :-
nl,
fact(av(Attr, X), CF),
CF >= 20,
outp(av(Attr, X), CF), nl,
fail.
print_goal(Attr) :-
write('done with '), write(Attr), nl, nl.
outp(av(A, V), CF) :-
output(A, V, PrintList),
write(A-'cf'-CF),
printlist(PrintList),
!.
outp(av(A, V), CF) :-
write(A-V-'cf'-CF).
printlist([]).
printlist([H|T]) :- write(H), printlist(T).
改进规则的表达
到目前为止我们已经介绍完了包含非确定因素的专家系统的编写方法,最后我们讨论一下如何使用prolog的DCG功能美化规则的定义方法。(关于DCG在prolog的语言教学的最后一章:自然语言中有介绍)
load_rules(F) :-
clear_db,
see(F),
lod_ruls,
write('rules loaded'), nl,
seen, !.
lod_ruls :-
repeat,
read_sentence(L),
process(L),
L == eof.
process(eof) :- !.
process(L) :-
trans(R, L, []),
assertz(R), !.
process(L) :-
write('translate error on:'), nl,
write(L), nl.
clear_db :-
abolish(top_goal, 1),
abolish(askable, 4),
abolish(rule, 3).
这段代码调用read_sentence把句子转化为列表,然后再使用trans谓词把列表转化为prolog的子句。也就是说这段程序把前面我们容易看懂得规则翻译成推理引擎能够是别的规则格式。
顶层的翻译谓词trans用以识别三种允许的规则表达模式。
trans(top_goal(X))-->[goal, is, X].
trans(top_goal(X))-->[goal, X].
trans(askable(A, M, P))-->
[ask, A], menux(M), prompt(A, P).
trans(rule(N, lhs(IF), rhs(THEN, CF)))--> id(N), if(IF), then(THEN, CF).
下面的谓词用以识句子中的菜单和提示语。
menux(M)--> [menu, '('], menuxlist(M).
menuxlist([Item])--> [Item, ')'].
menuxlist([Item|T])--> [Item], menuxlist(T).
prompt(_, P)--> [prompt, P].
prompt(P, P)--> [].
最后是分析规则结构的谓词。根据我们的需要可以写出非常接近自然语言的规则翻译功能。
id(N)-->[rule, N].
if(IF)-->[if], iflist(IF).
iflist([IF])-->phrase(IF), [then].
iflist([Hif|Tif])-->phrase(Hif), [and], iflist(Tif).
iflist([Hif|Tif])-->phrase(Hif), [', '], iflist(Tif).
then(THEN, CF)-->phrase(THEN), [cf], [CF].
then(THEN, 100)-->phrase(THEN).
phrase(not av(Attr, yes))-->[not, Attr].
phrase(not av(Attr, yes))-->[not, a, Attr].
phrase(not av(Attr, yes))-->[not, an, Attr].
phrase(not av(Attr, Val))-->[not, Attr, is, Val].
phrase(not av(Attr, Val))-->[not, Attr, are, Val].
phrase(av(Attr, Val))-->[Attr, is, Val].
phrase(av(Attr, Val))-->[Attr, are, Val].
phrase(av(Attr, yes))-->[Attr].
使用DCG可以方便的定义简便的知识库的语法。
下一章将介绍如何给专家系统加入解释。 |