EPITA

Introdution à Prolog, cours n° 3 (suite)

Jean-François Perrot

Un programme de résolution propositionnelle


  1. Représentation de la forme clausale en Prolog
    1. Principe
      1. Exemples
    2. Gestion des ensembles (listes) de clauses
      1. Normalisation
      2. Adjonction sans duplication

  2. L'opération centrale de la résolution
    1. Réduire
    2. Recherche d'une résolvante

  3. Organisation du processus global de résolution directe
    1. L'opérateur de Robinson = un pas de résolution
    2. Le processus de résolution
    3. Le prédicat resoudre(Ref, LCl, LRes)

  4. Mise en œuvre
    1. Réfutation d'une forme clausale
    2. Traitement des bases de règles pour moteurs d'inférences

  5. Réalisation de la résolution linéaire
    1. Réalisation de la SLD-résolution
    2. La résolution simplifiée à la Prolog
    3. Application aux bases de règles pour moteurs d'inférences

Représentation de la forme clausale en Prolog

  1. Principe

    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,
    1. la première contient les atomes des littéraux négatifs
      (donc, les antécédents des séquents de l'algorithme de Wang)

    2. la seconde les littéraux positifs (les conséquents des séquents)
    Exemples
  2. Gestion des ensembles (listes) de clauses

L'opération centrale de la résolution

  1. Réduire

    É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).



  2. Recherche d'une résolvante

    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).



Organisation du processus global de résolution directe

  1. L'opérateur de Robinson = un pas de résolution

    (ainsi nommé d'après son inventeur, John Alan Robinson)


    robinson(LCl, LR) :- resoudre(LCl, LCl, LR).

    Reste à définir le prédicat resoudre, voir plus loin.
  2. Le processus de résolution

    On itère l'opérateur de Robinson jusqu'à

    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).



  3. Le prédicat resoudre(Ref, LCl, LRes)

    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

Mise en œuvre



Réalisation de la résolution linéaire

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.


  1. Réalisation de la SLD-résolution

    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 .


  2. La résolution simplifiée à la Prolog

    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
    ?-


  3. Application aux bases de règles pour moteurs d'inférences

    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