Inférence à chaînage avant avec variables¶
Le but de cette série est de développer un moteur d’inférence à chaînage avant capable de manipuler des règles comportant des variables. Dans un premier temps, vous devrez étendre l’implémentation des règles en complétant quelques fonctions utilitaires. Puis vous construirez un filtre, qui permettra de comparer deux propositions dont l’une pourra contenir des variables. Ensuite, en utilisant votre filtre, vous implémenterez un moteur d’inférence avec variables.
Vous aurez également la possibilité d’implémenter un unificateur et de le tester sur votre moteur à chaînage avant avec variables. Un unificateur permet aussi de comparer deux propositions. La différence fondamentale avec le filtre est que l’unificateur accepte la présence de variables dans les deux expressions, ce qui rend possible de l’utiliser dans le chaînage arrière.
Modules squelettes¶
Les modules qui suivent constituent le squelette du programme que nous allons développer. Le dernier, exemple_impots_avec_variables.py, est un module de test.
Module .../moteur_avec_variables/proposition_avec_variables.py :
""" Fonctions utilitaires pour gérer des propositions sans ou avec variables\
dans un moteur d'inférence.
"""
def est_atomique(proposition):
""" Vérifie si la proposition courante est un atome (c'est le cas s'il\
s'agit d'un string).
:param proposition: une proposition.
:return: ``True`` si la proposition est de type string.
"""
print('à compléter')
def est_une_variable(proposition, marqueur='?'):
""" Vérifie si la proposition courante est une variable (c'est le cas s'il\
s'agit d'un atome dont la description commence par le marqueur de\
variables).
:param proposition: une proposition.
:param marqueur: marqueur de variable avec valeur par défaut : ``'?'``.
:return: ``True`` si l'argument est un atome et commence par le marqueur\
de variables.
"""
print('à compléter')
def tete(proposition):
""" Coupe la proposition courante et retourne son premier élément.
A noter que dans le cas d'une proposition atomique, la méthode soulève\
une exception.
:param proposition: une proposition.
:return: la tête de la proposition composée.
"""
if est_atomique(proposition):
raise Exception("Proposition atomique: Impossible de la segmenter.")
elif len(proposition) > 0:
return proposition[0]
else:
raise Exception("Proposition vide: Impossible de la segmenter.")
def corps(proposition):
""" Coupe la proposition courante et retourne la portion située après le\
premier élément.
A noter que dans le cas d'une proposition atomique, la méthode soulève\
une exception.
:param proposition: une proposition.
:return: le corps de la proposition composée.
"""
if est_atomique(proposition):
raise Exception("Proposition atomique: Impossible de la segmenter.")
elif len(proposition) > 0:
return proposition[1:]
else:
raise Exception("Proposition vide: Impossible de la segmenter.")
def lister_variables(proposition):
""" Retourne un ensemble (de type ``set``) contenant les variables\
mentionnées dans la proposition courante.
:param proposition: une proposition.
:return: la liste des variables apparaissant dans la proposition.
"""
variables = set()
if est_atomique(proposition):
if est_une_variable(proposition):
variables.add(proposition)
else:
for sous_prop in proposition:
variables.update(lister_variables(sous_prop))
return variables
Module .../moteur_avec_variables/regle_avec_variables.py :
class RegleAvecVariables:
""" Représentation d'une règle d'inférence pour le chaînage avec\
variables.
"""
def __init__(self, conditions, conclusion):
""" Construit une règle étant donné une liste de conditions et une\
conclusion.
:param list conditions: une collection de propositions (pouvant\
contenir des variables) nécessaires à déclencher la règle.
:param conclusion: la proposition (pouvant contenir des variables)\
résultant du déclenchement de la règle.
"""
self.conditions = conditions
self.conclusion = conclusion
def depend_de(self, fait, methode):
""" Vérifie qu'un fait fait partie, sous réserve de substitution,\
des conditions de la règle.
:param fait: un fait qui doit faire partie des conditions de\
déclenchement.
:param methode: ``Filtre`` ou ``Unificateur``, détermine le type\
de pattern match à appliquer.
:return: un dictionnaire qui attribue un environnement à chaque\
condition qui peut être satisfaite par le fait pasée en paramètre.\
``False`` si aucune condition n'est satisfaite par le fait.
"""
print('à compléter')
def satisfaite_par(self, faits, cond, env, methode):
""" Vérifie que des faits suffisent, sous réserve de substitution,\
à déclencher la règle.
:param list faits: une liste de faits.
:param cond: la condition qui a donné lieu à ``env`` par le\
pattern match.
:param dict env: un environnement de départ déjà établi par\
``depend_de``.
:param methode: ``Filtre`` ou ``Unificateur``, détermine le type\
de pattern match à appliquer.
:return: une liste d'environnements qui correspondent à toutes les\
substitutions possibles entre les conditions de la règle et les\
propositions. On retourne une liste vide si au moins une condition\
ne peut être satisfaite.
"""
print('à compléter')
def __repr__(self):
""" Représentation d'une règle sous forme de string. """
return '{} => {}'.format(str(self.conditions), str(self.conclusion))
Module .../moteur_avec_variables/chainage_avant_avec_variables.py :
from moteur_sans_variables.chainage import Chainage
from .filtre import Filtre
class ChainageAvantAvecVariables(Chainage):
""" Un moteur d'inférence à chaînage avant avec variables. """
def __init__(self, connaissances, methode=None):
"""
:param methode: ``Filtre`` ou ``Unificateur``, détermine le type de\
pattern match à appliquer. ``Filtre`` par défaut.
"""
Chainage.__init__(self, connaissances)
if methode is None:
self.methode = Filtre()
else:
self.methode = methode
def instancie_conclusion(self, regle, envs):
""" Instancie la conclusion d'une règle pour tous les environnements.
:param regle: la règle dont la conclusion doit être instanciée.
:param list envs: les environnements servant à instancier la\
conclusion.
:return: une liste de propositions correspondant aux différentes\
instanciations de la conclusion.
"""
print('à compléter')
def chaine(self):
""" Effectue le chaînage avant sur les faits et les règles contenus\
dans la base de connaissances.
"""
print('à compléter')
Module .../moteur_avec_variables/filtre.py :
from .proposition_avec_variables import est_atomique, est_une_variable, tete, corps
class Filtre:
""" Classe implémentant les méthodes de filtrage de propositions avec\
variables.
"""
echec = 'échec'
def substitue(self, pattern, env):
""" Effectue des substitutions de variables par leurs valeurs dans un\
pattern.
:param pattern: une proposition dont les variables doivent être\
remplacées par des valeurs.\
Une proposition est soit un atome, soit une liste contenant des\
atomes et / ou d'autre listes.
:param dict env: un environnment, c'est-à-dire un dictionnaire de\
substitutions ``{variable : valeur}``.
:return: le pattern dont les variables ont été remplacées par leurs\
valeurs dans l'environnment.
"""
print('à compléter')
def filtre(self, datum, pattern):
""" Effectue le filtrage entre un datum et un pattern.
:param datum: une proposition sans variables.
:param pattern: une proposition pouvant contenir des variables.
:return: un environnment c'est-à-dire un dictionnaire de\
substitutions ``{variable : valeur}``, ou ``'échec'`` si le filtrage\
échoue.
"""
print('à compléter')
def pattern_match(self, datum, pattern, env=None):
""" Effectue le filtrage en tenant compte d'un environnement initial.
:param datum: une proposition sans variables.
:param pattern: une proposition pouvant contenir des variables.
:param dict env: l'environnement initial à prendre en compte.
:return: un nouvel environnment ou ``'échec'``.
"""
print('à compléter')
Module .../moteur_avec_variables/unificateur.py :
from .proposition_avec_variables import est_atomique, est_une_variable, tete, corps
class Unificateur:
""" Classe implémentant les méthodes de l'unification de propositions avec\
variables. """
echec = 'échec'
def substitue(self, pattern, env):
""" Effectue des substitutions de variables dans un pattern.
:param pattern: une proposition dont les variables doivent être\
remplacées par d'autres propositions.
:param dict env: un environnment, c'est-à-dire un dictionnaire de\
substitutions ``{variable : proposition}``.
:return: le pattern dont les variables ont été remplacées les\
propositions qui leur sont associées dans l'environnement.
"""
print('à compléter')
def unifie(self, prop1, prop2):
""" Effectue l'unification entre deux propositions.
:param prop1: une proposition pouvant contenir des variables.
:param prop2: une proposition pouvant contenir des variables.
:return: un environnment, c'est-à-dire un dictionnaire de\
substitutions ``{variable : proposition}``, ou ``'échec'`` si\
l'unification a échoué.
"""
print('à compléter')
def pattern_match(self, prop1, prop2, env=None):
""" Effectue l'unification en tenant compte d'un environnement initial.
:param prop1: une proposition pouvant contenir des variables.
:param prop2: une proposition pouvant contenir des variables.
:param dict env: l'environnement initial à prendre en compte.
:return: un nouvel environnment ou ``'échec'``.
"""
print('à compléter')
Module .../exemple_impots_avec_variables.py :
from sys import argv, exit
from moteur_avec_variables.regle_avec_variables import RegleAvecVariables
from moteur_sans_variables.connaissance import BaseConnaissances
from moteur_avec_variables.filtre import Filtre
from moteur_avec_variables.unificateur import Unificateur
from moteur_avec_variables.chainage_avant_avec_variables import ChainageAvantAvecVariables
faits_initiaux = [
('add', '0', '0', '0', '0'),
('add', '100', '100', '0', '0'),
('add', '100', '0', '100', '0'),
('add', '200', '100', '100', '0'),
('add', '200', '0', '200', '0'),
('add', '300', '100', '200', '0'),
('add', '50', '0', '0', '50'),
('add', '150', '100', '0', '50'),
('add', '150', '0', '100', '50'),
('add', '250', '100', '100', '50'),
('add', '250', '0', '200', '50'),
('add', '350', '100', '200', '50'),
('add', '100', '0', '0', '100'),
('add', '200', '100', '0', '100'),
('add', '200', '0', '100', '100'),
('add', '300', '100', '100', '100'),
('add', '300', '0', '200', '100'),
('add', '400', '100', '200', '100'),
# Paul
('bas-salaire', 'Paul'),
('loyer', 'Paul'),
('enfants', 'Paul'),
('long-trajet', 'Paul'),
# Marc
('moyen-salaire', 'Marc'),
('loyer', 'Marc'),
('enfants', 'Marc'),
('long-trajet', 'Marc'),
# Jean
('haut-salaire', 'Jean'),
('pas-de-loyer', 'Jean'),
('pas-d-enfants', 'Jean'),
('long-trajet', 'Jean'),
]
regles = [
# Reduction enfants
[[('pas-d-enfants', '?x')], ('réduc-enfant', '0', '?x')],
[[('enfants', '?x')], ('réduc-enfant', '100', '?x')],
# Reduction loyer
[[('bas-salaire', '?x'), ('loyer', '?x')], ('réduc-loyer', '200', '?x')],
[[('moyen-salaire', '?x'), ('loyer', '?x')], ('réduc-loyer', '100', '?x')],
[[('haut-salaire', '?x'), ('loyer', '?x')], ('réduc-loyer', '0', '?x')],
[[('pas-de-loyer', '?x')], ('réduc-loyer', '0', '?x')],
# Reduction transport
[[('petit-trajet', '?x')], ('réduc-trajet', '0', '?x')],
[[('réduc-enfant', '0', '?x'), ('long-trajet', '?x')],
('réduc-trajet', '100', '?x')],
[[('réduc-loyer', '0', '?x'), ('long-trajet', '?x')],
('réduc-trajet', '100', '?x')],
[[('réduc-enfant', '100', '?x'), ('réduc-loyer', '100', '?x'),
('long-trajet', '?x')], ('réduc-trajet', '50', '?x')],
[[('réduc-enfant', '100', '?x'), ('réduc-loyer', '200', '?x'),
('long-trajet', '?x')], ('réduc-trajet', '0', '?x')],
# Reduction totale
[[('réduc-enfant', '?a', '?x'), ('réduc-loyer', '?b', '?x'),
('réduc-trajet', '?c', '?x'), ('add', '?res', '?a', '?b', '?c')],
('réduc', '?res', '?x')],
]
if len(argv) < 2 or argv[1].lower() not in ('filtre', 'unificateur'):
print('On attend un argument: Filtre ou Unificateur')
exit(1)
if argv[1].lower() == 'filtre':
methode = Filtre()
elif argv[1].lower() == 'unificateur':
methode = Unificateur()
bc = BaseConnaissances(lambda descr: RegleAvecVariables(descr[0], descr[1]))
bc.ajoute_faits(faits_initiaux)
bc.ajoute_regles(regles)
moteur = ChainageAvantAvecVariables(connaissances=bc, methode=methode)
moteur.chaine()
moteur.affiche_solutions()
if len(argv) > 2 and argv[2].lower() == 'trace':
moteur.affiche_trace()
Le code de cette série d’exercices s’appuie sur le code développé pour l’inférence sans variables. Il est donc important de respecter strictement la structure des dossiers que nous vous fournissons. Sinon, Python ne pourra pas importer correctement les modules.
Les faits et les règles¶
Au cours des exercices précédents, vous avez manipulé des faits simples et des règles sans variables. Dans cette série, les faits pourront être composées et les règles pourront contenir des variables. Nous parlerons plus généralement de propositions qui sont définies récursivement comme étant :
- Un atome, présenté sous la forme d’une
stringet représentant soit une variable, soit une valeur ; - Ou un
tuplecontenant des propositions.
Vous trouverez ici des informations détaillées sur la syntaxe des tuples en Python. L’essentiel à retenir pour cet exercice est qu’un tuple est une séquence de valeurs construite en alignant plusieurs éléments séparés par des virgules : t3 = 'str', 0, []. Pour plus de clarté, on entoure généralement ces valeurs de parenthèses : t3 = ('str', 0, []). Notez enfin qu’un tuple composé d’un seul élément doit contenir une virgule finale. Ici aussi, il est préférable d’utiliser des parenthèses : t1 = ('unique',).
Par convention, une variable sera un atome qui commence par un point d’interrogation. Exemple :
'?x'
'?qui'
Les faits seront des propositions sans variables. Voici par exemple deux descriptions de faits :
'Paul'
('réduc-loyer', '200', 'Michel')
En général cependant, une proposition pourra contenir des variables, comme dans ces exemples :
('réduc-loyer', '200', '?x')
('bas-salaire', '?z')
('réduc-trajet', '?x', '?y')
Comme dans la série précédente, les règles seront constituées d’une liste de conditions et d’une conclusion. Voici les descriptions de deux règles possibles :
[('pas-d-enfants', '?x')], ('réduc-enfant', '0','?x')
[[('bas-salaire', '?x'), ('loyer', '?x')], ('réduc-loyer', '200', '?x')
La classe RegleSansVariables de la série précédente doit ainsi être adaptée à l’utilisation de variables. Nous créerons donc une nouvelle classe RegleAvecVariables qui devra redéfinir les méthodes depend_de et satisfaite_par en faisant appel aux méthodes de pattern matching (filtrage ou unification). Vous devrez compléter ces méthodes une fois le pattern matching implémenté.
Exercice 1 : Le filtre¶
La technique du filtrage permet d’établir des correspondances entre deux propositions. Plus précisément, un filtre détermine les substitutions variables-valeurs qui permettent de retrouver une proposition sans variables (le datum) à partir d’une autre (le pattern) qui peut contenir des variables.
Pour commencer, vous devrez coder deux fonctions utilitaires dans le module proposition_avec_variables.py, afin de faciliter l’implémentation du filtrage : La fonction est_atomique(prop), qui doit retourner True si la proposition prop est une string et la fonction est_une_variable(prop), qui doit retourner True si prop est un atome et si le premier caractère de sa description indique une variable ('?').
La méthode Filtre.substitue¶
Vous pouvez maintenant vous attaquer à la classe Filtre du module filtre.py et implémenter sa méthode substitue. Cette méthode doit retourner un pattern dont les variables auront été remplacées par les valeurs disponibles dans l’environnement env qui est passé en paramètre. env est un dictionnaire qui contient des substitutions variable-valeur. N’oubliez pas que le pattern est une proposition, donc soit un atome, soit un tuple pouvant contenir d’autres propositions. Pensez donc à utiliser une méthode récursive pour traiter ces cas.
Pour vous guider, voici quelques exemples du fonctionnement de la méthode :
substitue('doctorant', {'?z': 'Paolo', '?y': 'Michel', '?x': 'Vincent'})
-> 'doctorant'
substitue('?x', {'?z': 'Paolo', '?y': 'Michel', '?x': 'Vincent'})
-> 'Vincent'
substitue(('?x', 'est un', 'doctorant'), {})
-> ('?x', 'est un', 'doctorant')
substitue(('?x', 'est un', 'doctorant'), {'?x': 'Vincent'})
-> ('Vincent', 'est un', 'doctorant')
substitue(('?x', 'est un', 'doctorant'), {'?y':'Michel'})
-> ('?x', 'est un', 'doctorant')
substitue(('?x', 'est un', ('?a')), {'?y': 'Michel', '?a': 'Vincent'})
-> ('?x', 'est un', ('Vincent'))
La méthode Filtre.filtre¶
Nous allons maintenant écrire la méthode filtre de la classe Filtre, qui implémente l’algorithme de filtrage. La méthode retournera :
- Un environnement,
{'?x':'toto', ..., '?y':'titi'}, dans le cas où le processus aboutit à des substitutions ; - Un environnement vide,
{}, si le processus réussit sans aucune substitution, c’est-à-dire lorsque les deux propositions sont identiques ; - La constante
Filtre.echec, en cas d’erreur de filtrage (sidatumetpatternsont incompatibles).
La méthode possède la signature filtre(datum, pattern), où datum est une proposition sans variables, et pattern une proposition pouvant contenir des variables.
Voici son pseudo-code :
Filtre(datum, pattern)
1. IF pattern == () AND datum == () THEN RETURN {}
2. ELSE IF pattern == () OR datum == () THEN RETURN échec
3. ELSE IF pattern est un atome THEN
4. IF pattern et datum sont identiques THEN RETURN {}
5. ELSE IF pattern est une variable THEN RETURN {pattern: datum}
6. ELSE RETURN échec
7. END IF
8. ELSE IF datum est un atome THEN RETURN échec
9. ELSE
10. F1 <- premier (datum)
11. T1 <- reste (datum)
12. F2 <- premier(pattern)
13. T2 <- reste(pattern)
14. Z1 <- Filtre(F1, F2)
15. IF Z1 == échec THEN RETURN échec END IF
16. G1 <- T1
17. G2 <- remplacer les variables de T2 selon les substitutions de Z1
18. Z2 <- Filtre(G1, G2)
19. IF Z2 == échec THEN RETURN échec END IF
20. RETURN {Z1 UNION Z2}
21. END IF
END Filtre
Et voici quelques exemples de son usage :
filtre('Vincent', '?x')
-> {'?x': 'Vincent'}
filtre(('Vincent', 'est un', 'doctorant'), ('?x', 'est un', 'doctorant'))
-> {'?x': 'Vincent'}
filtre(('Vincent', 'est un', 'doctorant') , ('Vincent', 'est un', 'doctorant'))
-> {}
filtre(('Vincent', 'est un', 'doctorant'), ('?x', 'est un', '?x'))
-> échec
filtre(('Vincent', 'est un', ('doctorant')) , ('Vincent', 'est un', ('?y')))
-> {'?y': 'doctorant'}
Vous trouverez ici quelques fonctions utiles pour manipuler des dictionnaires. La méthode update est particulièrement commode pour obtenir l’union de deux dictionnaires. Une façon brève et élégante pour retourner un dictionnaire à un élément est : return {key: value}.
La vraie fonction d’interface : Filtre.pattern_match¶
La fonction filtre n’est pas très pratique pour un programme hôte car il n’est pas possible de lui fournir en entrée un environnement. Dans le processus de chaînage avant que vous allez écrire, chaque condition d’une règle doit être vérifiée avant de pouvoir être utilisée. Cela implique que chaque condition soit filtrée avec succès par un fait existant. Comme plusieurs conditions peuvent être présentes, il est nécessaire de tester chacune en respectant l’environnement obtenu lors des filtrages précédents. Il faut donc pouvoir fournir à la fonction de filtrage un environnement déjà établi.
Vous devez ainsi compléter la méthode pattern_match de la classe Filtre, qui permettra de prendre en compte un environnement de substitutions déjà existantes. Cette méthode prend en paramètres deux propositions, un datum et un pattern, accompagnés d’un environnement sous forme d’argument optionnel, et retourne un nouvel environnement. Elle s’appuiera bien évidemment sur les méthodes filtre et substitue.
Voici une liste d’exemples qui prennent en compte des environnements préexistants :
pattern_match(('Vincent', 'est un', 'doctorant'), ('?x', 'est un', 'doctorant') , {'?y':'doctorant'})
-> {'?y': 'doctorant', '?x': 'Vincent'}
pattern_match(('Vincent', 'est un', 'doctorant'), ('?x', 'est un', '?y'), {'?y':'doctorant'})
-> {'?y': 'doctorant', '?x': 'Vincent'}`
pattern_match('Vincent', '?x', {})
-> {'?x': 'Vincent'}
Les arguments optionnels d’une fonction Python obéissent à la syntaxe argument=valeur. À cause de problèmes de mutabilité, il est déconseillé d’utiliser {} comme valeur par défaut. Il est préférable d’utiliser la valeur None et d’assigner un dictionnaire vide à la variable à l’intérieur de la méthode. Vous pouvez consulter cet article pour plus d’informations.
Exercice 2 : De retour aux règles¶
La méthode RegleAvecVariables.depend_de¶
La classe RegleAvecVariables, qui est à implémenter dans le module regle_avec_variables.py, reprend les noms des méthodes de la classe RegleSansVariables que nous avons utilisée dans l’exercice précédent, mais avec une implémentation passablement différente. La méthode dépend_de doit ainsi vérifier qu’un fait passée en paramètre est un déclencheur des conditions de la règle, et doit retourner un dictionnaire associant à chaque condition de la règle l’environnement résultant du pattern match entre cette condition et le fait. Si la recherche de substitutions aboutit à un échec pour une condition, il n’est pas nécessaire de la mentionner dans le dictionnaire. Il faudra donc comparer chaque condition de la règle avec le fait à l’aide de la méthode pattern_match du filtre, et recueillir les environnements résultants.
La méthode dépend_de prend en entrée deux paramètres : fait, un fait à tester, et methode, l’objet de pattern matching utilisé, soit un filtre soit un unificateur (filtre par défaut). N’oubliez pas que cette fonction doit vérifier toutes les conditions de la règle. Pour vous aider, voici quelques exemples :
règle = RegleAvecVariables([('père', '?x', '?y'), ('père', '?y', '?z')],
('grand-père, '?x', '?z'))
# Le fait ('père', 'Jean', 'Paul') peut satisfaire la première ou la seconde condition :
methode.pattern_match(('père', 'Jean', 'Paul'), ('père', '?x', '?y'))
-> {'?x': 'Jean', '?y': 'Paul'}
methode.pattern_match(('père', 'Jean', 'Paul'), ('père', '?y', '?z'))
-> {'?y': 'Jean', '?z': 'Paul'}
# La méthode depend_de renvoie donc un dictionnaire avec ces deux conditions associées
# à leurs environnements respectives :
depend_de(('père', 'Jean', 'Paul'), Filtre())
-> {('père', '?x', '?y'): {'?x': 'Jean', '?y': 'Paul'},
('père', '?y', '?z'): {'?y': 'Jean', '?z': 'Paul'}}
La méthode RegleAvecVariables.satisfaite_par¶
La méthode satisfaite_par de la classe RegleAvecVariables vérifie que les faits passés en paramètres satisfont toutes les conditions de la règle tout en respectant l’environnement de départ inféré par dépend_de. La méthode prend en entrée quatre paramètres :
faits: une liste de faits ;cond: la condition qui a servi à découvrir l’environnementenv;env: l’environnement déjà établi pardepend_de;methode: l’objet de pattern matching utilisé, soit un filtre soit un unificateur (filtre par défaut).
La méthode satisfaite_par retourne une liste d’environnements qui correspondent à toutes les substitutions possibles entre les conditions de la règle et les faits. Il s’agit donc de trouver les environnements qui satisfont chacun toutes les conditions. Si ce n’est pas possible, la méthode doit retourner une liste vide. En outre, chaque nouvel environnement construit devra être testé lors de la vérification de la prochaine condition. Voici un petit exemple pour clarifier les choses :
faits = [('père', 'Jean', 'Paul'),
('père', 'Florent', 'Paul'),
('père', 'Paul', 'Michel')]
regle = RegleAvecVariables([('père', '?x', '?y'), ('père', '?y', '?z')],
('grand-père, '?x', '?z'))
satisfaite_par(faits, ('père', '?x', '?y'), {'?x': 'Jean', '?y': 'Paul'})
-> [{'?x': 'Jean', '?y': 'Paul', '?z': 'Michel'}]
satisfaite_par(faits, ('père', '?y', '?z'), {'?y': 'Jean', '?z': 'Paul'})
-> []
Et voici le pseudo-code de la méthode :
SatisfaitePar(regle, faits, cond, env)
1. conditions_a_tester <- conditions de regle sauf cond
2. environnements_satisfaisants <- [env]
3. FOR EACH condition cond1 de conditions_a_tester DO
4. environnements_nouveaux <- liste vide
5. FOR EACH fait de faits DO
6. FOR EACH environnement env1 de environnements_satisfaisants DO
7. env1 <- nouvel environnement déterminé par pattern_match(fait, cond1, env1)
8. IF NOT env1 == échec THEN
9. ajouter env1 à environnements_nouveaux
10. END IF
11. END FOR
12. END FOR
13. IF environnements_nouveaux est vide THEN
14. RETURN liste vide
15. END IF
16. environnements_satisfaisants <- environnements_nouveaux
17. END FOR
18. RETURN environnements_satisfaisants
END SatisfaitePar
Exercice 3 : Le moteur d’inférence à chaînage avant avec variables¶
La méthode ChainageAvantAvecVariables.instancie_conclusion¶
Une fois que les conditions d’une règle ont été validées, il faut instancier la conclusion en accord avec la liste des environnements ainsi obtenus afin de déduire de nouvelles propositions. La fonction instancie_conclusion de la classe ChainageAvantAvecVariables prend comme paramètres une règle et une liste d’environnements, et retourne une liste de nouveaux faits (un par environnement). Exemples :
règle = RegleAvecVariables(liste_de_conditions, ('?x', '?y'))
instancie_conclusion(règle, [{'?x': 'X','?y': 'Y'}])
-> [('X', 'Y')]
Vous pouvez implémenter cette méthode de façon simple avec une boucle for itérant sur les environnements, mais il est aussi possible de l’écrire en une ligne sous forme de list comprehension (un peu plus complexe mais plus élégant). Pensez en outre à utiliser la fonction substitue de la classe de pattern match.
La méthode ChainageAvantAvecVariables.chaine¶
Grâce aux changements apportés aux règles, grâce à la méthode instancie_conclusion et au module de filtrage que nous avons développé, nous pouvons maintenant réaliser une nouvelle version de notre moteur d’inférence à chaînage avant, avec la capacité de manipuler des règles comportant des variables.
Implémentez l’algorithme de chaînage avant dans la méthode chaine de la classe ChainageAvantAvecVariables. Pour rappel, l’algorithme à implémenter est le suivant :
ChainageAvantAvecVariables(faits_depart, regles)
1. solutions <- liste vide
2. Q <- faits_depart
3. WHILE Q n'est pas vide DO
4. q <- premier(Q)
5. Q <- reste(Q)
6. IF q n'est pas dans solutions THEN
7. ajouter q à solutions
8. FOR EACH règle r de regles DO
9. IF une condition de r dépend de q THEN
10. FOR EACH condition cond et environnement env déduits de r.depend_de(q) DO
11. envs <- r.satisfaite_par(solutions, cond, env)
12. instances <- instancier la conclusion de r selon envs
13. ajouter instances en queue de Q
14. END FOR
15. END IF
16. END FOR
17. END IF
18. END WHILE
19. RETURN solutions
END ChainageAvantAvecVariables
Test du programme : Chaînage avant avec filtre¶
Le module exemple_impots_avec_variables.py contient les règles et les faits nécessaires pour le calcul du montant d’une réduction d’impôts. Après avoir écrit votre programme, testez-le en exécutant dans le terminal avec l’option filtre. Vous pouvez afficher la trace en ajoutant l’option trace. Quelle devrait être la réduction d’impôts ?
python3 exemple_impots_avec_variables.py filtre
python3 exemple_impots_avec_variables.py filtre trace
Exercice 4 : L’unificateur [OPTIONNEL]¶
Nous allons maintenant construire un unificateur. C’est un outil analogue à un filtre mais bien plus puissant. À la différence du filtre, qui compare deux expressions dont l’une seulement peut comporter des variables, l’objectif d’un unificateur est de comparer deux expressions pouvant toutes deux contenir des variables. L’unificateur est donc une version généralisée du filtre. Il fournit comme résultat les correspondances entre les deux propositions sous la forme de substitutions variable-proposition (lorsqu’il en existe).
Pour mieux comprendre l’utilité de l’unificateur, voyons quelques exemples :
unifie(('Vincent', 'est un', 'doctorant'), ('Vincent', 'est un', 'doctorant'))
-> {}
unifie(('Vincent', 'est un', 'doctorant'), ('Michel', 'est un', 'doctorant'))
-> échec
unifie(('foo', '?x', ('?y', 'bar', 'Jean')), ('foo', 'Jean', ('Marc', 'bar', '?x')))
-> {'?y': 'Marc', '?x': 'Jean'}
unifie(('p', '?x', ('f', '?y')), ('p', ('f','a'), '?x'))
-> {'?y': 'a', '?x': ('f', 'a')}`
Nous utiliserons des conventions analogues à celles que nous avons appliquées dans le cas du filtrage. Désormais cependant un environnement pourra associer des propositions, pas nécessairement des valeurs, à des variables. Le résultat final de l’unification contiendra plutôt des substitutions variable-proposition sans variables, mais les étapes intermédiaires pourront aussi renvoyer des environnements qui associent des propositions contenant des variables à d’autres variables.
La méthode Unificateur.substitue¶
La fonction substitue de la classe Unificateur doit permettre d’instancier une proposition étant donné un ensemble de substitutions variable-proposition. Exemples :
substitue(('?x', 'est un', 'doctorant') , {})
-> ('?x', 'est un', 'doctorant')
substitue(('p', '?x') , {'?y': ('g','?z'), '?x': ('f', '?y'), '?z': ('a')})
-> ('p', ('f', ('g', ('a'))))
substitue(('p', '?x'), {'?y': ('g','?z'), '?x': ('f','?y'), '?z': ('?q')})
-> ('p', ('f', ('g', ('?q'))))
Lorsque vous rédigerez le code de cette méthodes, rappelez-vous qu’une variable doit parfois être remplacée par une définition de substitution qui contient elle-même des variables. Il faut donc veiller à aussi remplacer toutes les variables qui figurent dans la proposition associée à une autre variable. Pensez à implémenter une solution récursive.
La méthode Unificateur.unifie¶
Vous avez maintenant tout ce qu’il faut pour implémenter la fonction Unificateur.unifie. L’algorithme d’unification est très proche de l’algorithme de filtrage :
Unifie(prop1, prop2)
1. IF prop1 ou prop2 est un atome THEN
2. si nécessaire, échanger prop1 et prop2 pour que prop1 soit un atome
3. IF prop1 et prop2 sont identiques THEN RETURN {}
4. ELSE IF prop1 est une variable THEN
5. IF prop1 apparaît dans prop2 THEN RETURN échec
6. ELSE RETURN {prop1: prop2}
7. END IF
8. ELSE IF prop2 est une variable THEN RETURN {prop2: prop1}
9. ELSE RETURN échec
10. END IF
11. ELSE
12. F1 <- premier(prop1)
13. T1 <- reste(prop1)
14. F2 <- premier(prop2)
15. T2 <- reste(prop2)
16. Z1 <- Unifie(F1, F2)
17. IF Z1 == échec THEN RETURN échec END IF
18. G1 <- remplacer les variables de T1 selon les substitutions de Z1
19. G2 <- remplacer les variables de T2 selon les substitutions de Z1
20. Z2 <- Unifie(G1, G2)
21. IF Z2 == échec THEN RETURN échec END IF
22. RETURN {Z1 UNION Z2}
23. END IF
END Unifie
La vraie fonction d’interface : Unificateur.pattern_match¶
Comme dans le cas de la méthode filtre, unifie n’est pas très pratique pour un programme hôte. Il faut donc aussi coder une fonction Unificateur.pattern_match qui permette de prendre en compte un environnement de substitutions déjà existantes. Cette fonction prendra en paramètres deux expressions pouvant contenir des variables et un environnement à titre d’argument optionnel. Elle doit retourner un nouvel environnement ou la constante Unificateur.echec, selon que le pattern matching a réussi ou échoué. La méthode s’appuiera bien évidemment sur unifie et substitue.
Voici une liste d’exemples qui prennent en compte des environnements préexistants :
pattern_match(('foo', '?x', ('?y', 'bar', 'Jean')), ('foo', 'Jean', ('Marc', 'bar', '?x')), {'?y': 'Marc'})
-> {'?y': 'Marc', '?x': 'Jean'}
pattern_match(('foo', '?x', ('?y', 'bar', 'Paul')), ('foo', 'Jean', ('Marc', 'bar', '?x')), {})
-> échec
Pour coder cette méthode, il convient donc de s’assurer que l’environnement est valide, puis de remplacer les variables des deux propositions par les définitions de l’environnement, et enfin de procéder à l’unification.
Test du programme : Chaînage avant avec unificateur¶
Essayez d’utiliser l’unificateur à la place du filtre, en lançant exemple_impots_avec_variables.py avec l’option `unificateur’. Que constatez-vous et pourquoi ?
python3 exemple_impots_avec_variables.py unificateur
python3 exemple_impots_avec_variables.py unificateur trace
Est-il vraiment nécessaire d’utiliser un unificateur dans le chaînage avant ?