EPITA
Introdution à Prolog, cours n° 3 (suite)
Un programme de résolution propositionnelle
- Représentation de la forme clausale en Prolog
- Principe
- Exemples
- Gestion
des ensembles (listes) de clauses
- Normalisation
- Adjonction
sans duplication
- L'opération
centrale de la résolution
- Réduire
- Recherche d'une
résolvante
- Organisation
du processus global de résolution directe
- L'opérateur
de Robinson = un pas de résolution
- Le
processus de résolution
- Le
prédicat resoudre(Ref, LCl, LRes)
- Mise
en œuvre
- Réfutation
d'une forme clausale
- Traitement
des bases de règles pour moteurs d'inférences
- Réalisation de la
résolution linéaire
- Réalisation de la
SLD-résolution
- La
résolution simplifiée à la Prolog
- Application aux
bases
de règles pour moteurs d'inférences
-
Une proposition est représentée comme un ensemble (liste) de clauses
représentant la conjonction de ses membres.
Chaque clause est représentée par un couple d'ensembles (listes)
d'atomes,
- la première contient les atomes des littéraux négatifs
(donc, les antécédents des séquents de l'algorithme de Wang)
- la seconde les littéraux positifs (les conséquents des
séquents)
- La proposition de l'exemple précédent donne donc,
d'abord dans une
écriture hybride
[[[¬
p]⋁
[q]]⋀
[[¬
q]⋁
[r]]⋀
[[¬
r]⋁
[s]]⋀
[[¬
t⋁¬
s]⋁
[]]⋀
[[]⋁
[p]]⋀
[[]⋁[t]]]
puis en "vrai" Prolog :
[[[p],[q]], [[q],[r]], [[r],[s]], [[t,s],[]],
[[],[p]], [[],[t]]]
- De cette façon, la formule calculée plus haut par
l'algorithme de Wang
se lit :
[[[b], [d, a]], [[a], [d, c]], [[], [b, c]], [[f], []]]
= (¬b⋁d⋁a)⋀(¬a⋁d⋁c)⋀(b⋁c)⋀¬f
, identique à la
donnée, comme annoncé.
- Le complexe de la girafe
(¬
herbivore
⋁
girafe ⋁ buffle⋁ antilope
) ⋀
(¬
herbivore
⋁
¬félin
) ⋀ (¬
panthère
⋁
félin
) ⋀
(¬
tacheté
⋁
panthère ⋁ girafe
) ⋀ herbivore
⋀
tacheté
⋀
¬
girafe
s'écrit en Prolog :
[[[herbivore], [
girafe,
buffle, antilope]],
[[
herbivore,
félin],
[]], [[
panthère], [
félin]],
[[
tacheté], [
panthère,
girafe]], [[], [
herbivore]], [[], [
tacheté
]],
[[
girafe
],
[]] ]
-
-
Les clauses seront considérées comme normalisées, c'est-à-dire que
les
littéraux sont ordonnés en ordre standard fourni
par Prolog, via le prédicat 'sort
'.
/* normalise(DonneeBrute, ResultatNormal) */
normalise([], []).
normalise([[N, P]|S], [[NN, NP]|NS]) :- sort(N, NN), sort(P, NP),
normalise(S,NS).
-
d'une liste d'objets
L1
(clauses ou atomes) à une autre liste d'objets L2
:
le prédicat standard union
de SWI-Prolog est valable puisque nos listes représentent des
ensembles.
Nous aurons aussi à nous assurer qu'un objet ne figure pas dans un
ensemble, ce pourquoi nous ferons appel au prédicat standard member
.
-
Étant donné
X
et Y
deux listes de littéraux de signes opposés,
trouver si elles ont un élément commun U
,
et l'éliminer pour obtenir NX
et NY
.
Si on échoue, c'est qu'il n'y a pas d'atome commun, donc pas de
résolution possible dans ce cas.
Emploi caractéristique de append
(prédicat standard correspondant à concat
amplement exemplifié dans ce cours)
pour
décomposer X
et Y
.
reduire(X, Y, NX, NY) :- append(AX, [U|DX],
X),
append(AY, [U|DY], Y),
append(AX, DX, NX), append(AY, DY, NY).
-
Le prédicat '
reduire
' sert de test.
On essaie successivement les deux cas de figure "littéral négatif dans
la
première clause", "id. dans la seconde".
resolvante([NA, PA], [NB, PB], [NR,
PR]) :-
reduire(NA, PB, NNA, NPB), !,
% négatif
dans A
union(NNA,
NB, NS), add(NPB, PA, PS),
sort(NS,
NR), sort(PS, PR).
resolvante([NA, PA], [NB, PB], [NR, PR]) :-
reduire(PA, NB, NPA, NNB), !,
% négatif
dans B
union(NNB,
NA, NS), add(NPA, PB, PS),
sort(NS,
NR), sort(PS, PR).
-
(ainsi nommé d'après son inventeur, John
Alan Robinson)
- à partir d'une liste de clauses
LCl
,
on calcule
- la liste
LR
de toutes les
résolvantes nouvelles
des clauses de LCl
avec elles-mêmes.
Si cette liste est vide, on en conclut que LCl
est fermée par résolution (il n'y a plus de nouvelle résolvante).
robinson(LCl, LR) :- resoudre(LCl, LCl, LR).
Reste à définir le prédicat resoudre
,
voir plus loin.
-
On itère l'opérateur de Robinson
jusqu'à
- soit trouver la clause vide
[[], []]
,
auquel cas "oui" la liste de clauses donnée est
contradictoire,
- soit ne plus engendrer de nouvelles résolvantes
(fermeture
par résolution),
auquel cas "non" la liste donnée n'est pas
contradictoire.
resolution(LCl) :- member([[],[]], LCl),
!,
write(oui).
resolution(LCl) :- robinson(LCl, LR), sresolution(LCl, LR).
sresolution(LCl, []) :- !, write(non). /* LCl fermee par resolution */
sresolution(LCl, LR) :- append(LR, LCl, L), resolution(L).
-
calcule
LRes
la liste des résolvantes de la liste LCl
avec elle-même qui ne sont pas dans Ref
.
La liste Ref
reste fixe, initialement
c'est la liste LCl
elle-même (appel
à resoudre(LCl, LCl, LR)
),
de manière à garantir qu'on ne retient que les résolvantes qui ne sont
pas dans LCl
.
On procède
par recurrence sur LCl
, en
itérant un prédicat 'resol(Ref, Cl, LCl, U)
',
qui calcule dans LU
la liste des
résolvantes nouvelles de LCl
avec une
clause Cl
.
resoudre(Ref, [], []).
resoudre(Ref, [Cl | SCl], LRes):- resol(Ref, Cl, SCl, LU), % la clause
Cl ne se résout pas contre elle-même !
resoudre(Ref, SCl, LS), union(LU, LS, LRes).
resol(Ref, Cl, [], []). % Résolvantes d'une clause avec une liste de
clauses
resol(Ref, Cl, [P|LS], LT) :- resolvante(Cl, P, R), !, sresol(Ref, R,
Cl, LS, LT).
resol(Ref, Cl, [_|LS], LU) :- resol(Ref, Cl, LS, LU).
sresol(Ref, R, Cl, LS, LR) :- member(R, Ref), !, resol(Ref, Cl, LS,
LR). % resol relancé sur la suite en oubliant R
sresol(Ref, R, Cl, LS, [Res|LR]) :- resol(Ref, Cl, LS, LR).
%
resol relancé sur la suite en gardant R
-
On se dote d'un prédicat de commodité :
rresoudre(LCl) :- normalise(LCl, X), resolution(X).
?- rresoudre([[[p],[q]],
[[q],[r]], [[r],[s]], [[t,s],[]], [[],[p]], [[],[t]]]).
oui
true ;
false.
?- rresoudre([[[b], [d, a]],
[[a], [d, c]], [[], [b, c]], [[f], []]]).
non
true ;
false.
?- rresoudre([[[herbivore],
[girafe, buffle, antilope]],
| [[herbivore, félin], []],
[[panthère], [félin]],
| [[tacheté], [panthère,
girafe]], [[], [herbivore]], [[], [tacheté]],
| [[girafe], []] ]).
oui
true ;
false.
?- rresoudre([[[herbivore],
[girafe, buffle, antilope]],
[[herbivore, félin], []], [[panthère], [félin]],
[[tacheté], [panthère, girafe]], [[], [herbivore]], [[],
[tacheté]],
[[panthère], []] ]).
non
true ;
false.
winst([[[],[p]], [[],[u]],[[p],[q]],
[[q],[r]],
[[r],[s]],[[u,s],[]]]).
myst([[[p],[q]], [[q],[r]], [[r],[s]],
[[t,s],[]],
[[],[p]], [[],[t]]]).
hunt([ [[],[q]], [[],[p,r]], [[p],[s]],
[[r],[s]],
[[p,q,s],[]],[[q,r,s],[]] ]).
lovld([ [[],[a]], [[a],[b]], [[],[b,d]],
[[],[c,d]],
[[d],[c]],[[c],[e]], [[c,e],[]] ]).
nonex([ [[],[a,b,c]], [[],[e]], [[e],[b]],
[[],[r,s]] ]).
nyst([[[p],[q]], [[q],[r]], [[s],[r]],
[[u,s],[]],
[[],[p]], [[],[u]]]).
-
cf. notes de cours n° 2.
Les bases de règles de nos moteurs d'inférences d'ordre 0 sont des
clauses de Horn, donc des clauses !
Mais elles sont représentées en Prolog d'une manière plus économique
(nous allons y revenir).
Pour les traiter par le système de résolution général, il faut les
convertir.
Une base de règles ayant été lue (p. ex. :-[quadcomp].
)...
lregcl([], []).
lregcl([[T|Q]|R], [[Q,[T]]|C]) :- lregcl(R,C).
lfaitcl([], []).
lfaitcl([F|SF], [[[], [F]]|C]) :- lfaitcl(SF, C).
clauses(SCl) :- bagof(R, regle(R), BR), lregcl(BR, RCl),
bagof(F,
fait(F), BF), lfaitcl(BF, FCl),
append(RCl,FCl, SCl).
regp(LButs) :- clauses(SCl), normalise([[LButs,
[]]|SCl],
LC), resolution(LC).
Mais notre procédure est beaucoup trop inefficace pour
traiter toute la base quadcomp.pl
!
Essayons plutôt quadred.pl
regle([carre, rectangle, losange]).
regle([rectangle, parallelogramme, un_angle_droit]).
regle([parallelogramme, diag_milieu]).
regle([losange, parallelogramme, diag_ortho]).
regle([losange, quatre_cotes_egaux]).
fait(diag_milieu).
fait(diag_ortho).
fait(un_angle_droit).
et demandons
?- regp([carre]).
oui
true ;
false.
Le littéral resolin(Ref, Cl, LCl)
réussit ssi il
dérive la
clause vide en résolvant linéairement LCl
,
avec Cl
comme clause initiale, en ne gardant
que les résolvantes qui ne sont pas dans Ref
.
Procède par récurrence sur LCl
,
sans cut
de manière pouvoir explorer toutes
les possibilités par backtrack.
resolin(LRef, Cl, [P|LS]) :-
resolvante(Cl, P,
R), sresolin(LRef, R, Cl, LS).
resolin(LRef, Cl, [_|LS]) :- resolin(Ref, Cl, LS).
% et si LS est vide, échec de resolin, qui va provoquer un backtrack
dans "tresolin"
sresolin(LRef, [[], []], Cl, LS):- !,
write(oui)
.
% Résolvante vide, on a
réussi
sresolin(LRef, R, Cl, LS) :- member(R, LRef), !, resolin(LRef,
Cl, LS).
/* Résolvante déjà connue, on relance sur la suite
*/
sresolin(LRef, R, Cl, LS) :- resolin([R|LRef], R,
[R|LRef]
).
/* Appel
récursif essentiel de la résolution linéaire */
tresolin(LRef, []) :- write(non), !. % échec
tresolin(LRef, [Prem|Suite]) :- resolin(LRef, Prem, Suite), !. %
backtrack sur échec de resolin
tresolin(LRef, [Prem|Suite]) :- tresolin(LRef, Suite).
reslineaire(LCl) :- tresolin(LCl, LCl).
rreslineaire(LCl) :- normalise(LCl, X), reslineaire(X).
/* pour essais à partir des bases de règles */
regl(LButs) :- clauses(SCl), normalise([[LButs,
[]]|SCl],
LC),
reslineaire(LC).
Exécution :
?- rreslineaire([[[herbivore], [girafe, buffle,
antilope]],
| [[herbivore, félin], []], [[panthère],
[félin]],
| [[tacheté], [panthère, girafe]], [[],
[herbivore]], [[], [tacheté]],
| [[girafe], []] ]).
true.
?- rreslineaire([[[herbivore], [girafe, buffle, antilope]],
[[herbivore, félin], []], [[panthère], [félin]],
[[tacheté], [panthère, girafe]], [[], [herbivore]], [[], [tacheté]],
[[panthère], []] ]).
false.
-
Le prédicat-maître est
reshorn(LButs, LClauses, LRef,
AncButs)
,
lancé par reshorn(LButs,
LClauses
,
LRef, LRef)
,
où
reshorn([B|SButs], [[B|Q]|SCl], LRef,
AncButs)
:-
union(Q,SButs,NButs),
sreshorn(NButs, [B|SButs], SCl, LRef, AncButs).
reshorn(LButs, [_|SCl], LRef, AncButs) :-
reshorn(LButs, SCl, LRef, AncButs).
sreshorn([], LButs, SCl, LRef, AncButs) :- !.
/* Liste de buts vide = Resolvante vide,
on a fini */
sreshorn(NButs, LButs, SCl, LRef, AncButs) :-
present(NButs, AncButs), !,
reshorn(LButs, SCl, LRef, AncButs).
sreshorn(NButs, LButs, SCl, LRef, AncButs) :- /* write(NButs), nl, */
reshorn(NButs, LRef, LRef,
[NButs|AncButs]).
La seule résolution possible est entre la clause
négative [B|SButs]
et une clause [B|Q]
(où B
est interprété comme positif), produisant la
résolvante (négative) NButs
.
Comme aucune résolution ne sera possible dans l'avenir entre
elle et une future résolvante courante (qui sera toujours négative),
il est inutile de l'ajouter à l'ensemble de clauses LRef
,
contrairement à ce qui se fait dans le cas général
(clause "sresolin(LRef, Res, Cl, Suite) :- resolin([Res|LRef],
Res, LRef).
").
En revanche, il est essentiel de noter qu'on l'a traitée, pour
éviter de boucler.
D'où l'argument suplémentaire AncButs
.
Le seul degré de liberté est le choix de la construction de la
résolvante,
qui doit réaliser la réunion des listes d'atomes (littéraux
négatifs) SButs
et de la queue de
clause Q
.
La stratégie Prolog de recherche en profondeur (la résolvante étant
gérée comme une pile)
se traduit par union
= append
et elle
entraîne des bouclages intempestifs.
La méthode correcte est de faire une union
sans duplication,
il faut donc recourir au prédicat add
et à la
normalisation,
version simplifiée du predicat "resolvante
" de
la résolution générale.
Exemples
ref1([[p], [u], [q,p,u], [r,q], [s,r]]).
but1([u,s]).
ref2([[q,p], [p,q], [r,q], [s,r], [p], [u]]).
but2([u,s]).
essaih1 :- ref1(R), but1(B), write('Horn'), resh(B,R), write('OK'), nl.
essaih2 :- ref2(R), but2(B), write('Horn'), resh(B,R), write('OK'), nl,
write('Prolog'), respg(B,R), write('FINI').
Exécution :
?- essaih1.
HornOK
true .
?- essaih2.
HornOK
true .
-
L'interprète Prolog gère la résolvante comme une pile, et
ne mémorise pas les résolvantes précédentes,
d'où un programme beaucoup plus simple, mais qui a une forte
tendance a boucler,
resprol(LButs, LRef, LCl)
resprol([], LRef, LCl) :- !.
resprol([B|SButs], LRef, [[B|Q]|SCl]) :-
/* sans "cut" car on peut avoir plusieur clauses de même tête*/
append(Q,
SButs, NButs), resprol(NButs, LRef, LRef).
resprol(LButs, LRef, [_|SCl]) :- resprol(LButs, LRef, SCl).
/* Appel par...*/
respg(LButs, LCl) :- resprol(LButs, LCl, LCl).
essaip1 :- ref1(R), but1(B), write('Prolog'), respg(B,R), write('FINI').
essaip2 :- ref2(R), but2(B), write('Prolog'), respg(B,R),
write('FINI').
Exécution :
?- essaip1.
PrologFINI
true .
?- essaip2.
Prolog
ERROR: Out of local stack
Exception: (285,353) resprol([q], [[q, p], [p, q], [r, q], [s, r], [p],
[u]], [[q, p], [p, q], [r, q], [s, r], [p], [u]]) ? creep
Exception: (285,352) resprol([p], [[q, p], [p, q], [r, q], [s, r], [p],
[u]], [[p, q], [r, q], [s, r], [p], [u]]) ? creep
Exception: (285,350) resprol([q], [[q, p], [p, q], [r, q], [s, r], [p],
[u]], [[q, p], [p, q], [r, q], [s, r], [p], [u]]) ? creep
Exception: (285,349) resprol([p], [[q, p], [p, q], [r, q], [s, r], [p],
[u]], [[p, q], [r, q], [s, r], [p], [u]]) ? creep
Exception: (285,347) resprol([q], [[q, p], [p, q], [r, q], [s, r], [p],
[u]], [[q, p], [p, q], [r, q], [s, r], [p], [u]]) ? abort
% Execution Aborted
?-
-
Cette fois la mise en œuvre est directe !
Il suffit de récupérer les règles qui ont été compilées à la lecture de
la base de règles...
Une base de règles ayant été lue (p. ex. :-[quadcomp].
)...
Pour vérifier qu'une liste d'hypohèses a bien pour conéquences une
liste de buts :
regh(LButs, LHyp) :- setof(C, regle(C), R),
append(R, LHyp, RC), % par la résolution SLD
resh(LButs, RC).
regpg(LButs, LHyp) :- setof(C, regle(C), R), append(R, LHyp, RC), % par
la résolution Prolog
respg(LButs, RC).
Exécution, avec
regle([carre, rectangle, losange]).
regle([rectangle, parallelogramme, un_angle_droit]).
regle([parallelogramme, diag_milieu]).
regle([losange, parallelogramme, diag_ortho]).
regle([losange, quatre_cotes_egaux]).
?- regh([carre], [[diag_milieu], [diag_ortho],
[un_angle_droit]]).
true ;
true ;
false.
?- regpg([carre], [[diag_milieu], [diag_ortho], [un_angle_droit]]).
true ;
true ;
false.
Mais si on ajoute "regle([rectangle, carre]).
"
en tête de la base de règles...
?- regle([rectangle, carre]).
true .
?- regh([carre], [[diag_milieu], [diag_ortho], [un_angle_droit]]).
true .
?- regpg([carre], [[diag_milieu], [diag_ortho], [un_angle_droit]]).
ERROR: Out of local stack