Inférence à chaînage avant sans variables

L’inférence à chaînage avant est à la base de la plupart des systèmes experts utilisés à l’heure actuelle. Cette série d’exercices a pour but d’introduire cette technique. La réalisation d’un tel système se fera en plusieurs étapes, la première se limitant à des règles sans variables. Dans les étapes suivantes, vous implémenterez un moteur d’inférence qui utilisera des règles avec variables.

Modules squelettes

Les modules de cette section fournissent le squelette du programme que nous allons développer. Le module exemple_impots_sans_variables.py représente le code d’un fichier test.

inference_sans_variables.zip

Module .../moteur_sans_variables/regle_sans_variables.py :

class RegleSansVariables:
    """ Représentation d'une règle d'inférence pour le chaînage sans\
        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 (sans\
            variables) nécessaires pour déclencher la règle.
            :param conclusion: la proposition (sans variables) résultant du\
            déclenchement de la règle.
        """

        self.conditions = set(conditions)
        self.conclusion = conclusion

    def depend_de(self, fait):
        """ Vérifie si un fait est pertinent pour déclencher la règle.

            :param fait: un fait qui doit faire partie des conditions de\
            déclenchement.
            :return: ``True`` si le fait passé en paramètre fait partie des\
            conditions de déclenchement.
        """

        print('à compléter')

    def satisfaite_par(self, faits):
        """ Vérifie si un ensemble de faits est suffisant pour prouver la\
            conclusion.

            :param list faits: une liste de faits.
            :return: ``True`` si les faits passés en paramètres suffisent à\
            déclencher la règle.
        """

        print('à compléter')

    def __repr__(self):
        """ Représentation d'une règle sous forme de string. """

        print('à compléter')

Module .../moteur_sans_variables/connaissance.py :

class BaseConnaissances:
    """ Une base de connaissances destinée à contenir les faits et les\
        règles d'un système de chaînage avant.
    """

    def __init__(self, constructeur_de_regle):
        """ Construit une base de connaissances.

            Le paramètre ``constructeur_de_regle`` doit être une fonction\
            prenant deux arguments : la liste des conditions d'une règle et sa\
            conclusion. La fonction doit retourner une règle du type désiré.

            :param contructeur_de_regle: une fonction construisant une règle.
        """

        self.faits = []
        self.regles = []
        self.constructeur_de_regle = constructeur_de_regle

    def ajoute_un_fait(self, fait):
        """ Ajoute un fait dans la base de connaissances.

            :param fait: un fait.
        """

        self.faits.append(fait)

    def ajoute_faits(self, faits):
        """ Ajoute une liste de faits dans la base de connaissances.

            :param list faits: une liste de faits.
        """

        self.faits.extend(faits)

    def ajoute_une_regle(self, description):
        """ Ajoute une règle dans la base de connaissances étant donné sa\
            description.

            Une règle est décrite par une liste (ou un tuple) de deux\
            éléments : une liste de conditions et une conclusion.

            Les conditions et la conclusion doivent être des propositions.

            :param description: une description de règle.
        """

        regle = self.constructeur_de_regle(description)
        self.regles.append(regle)

    def ajoute_regles(self, descriptions):
        """ Ajoute des règles dans la base de connaissances.

            L'argument est une liste de descriptions, chacune composée d'une\
            liste de conditions et d'une conséquence.

            :param list descriptions: une liste de descriptions de règles.
        """

        for description in descriptions:
            self.ajoute_une_regle(description)

Module .../moteur_sans_variables/chainage.py :

class Chainage:
    """ Le squelette d'un moteur d'inférence.

        Cette classe n'est pas censée être instanciée directement. Elle doit\
        être sous-classée par des classes filles qui implémentent la méthode\
        ``chaine``.

        :cvar self.trace: représente l'ordre dans lequel les propositions ont\
        été déduites et dans lequel les règles ont été appliquées (à utiliser\
        pour débugger votre code).
        :cvar self.solutions: doit contenir les solutions du chaînage.
    """

    __indentation = 4 * ' '

    def __init__(self, connaissances):
        """ Initialise le moteur d'inférence sans variables.

            :param connaissances: la base de connaissances.
        """

        self.trace = []
        self.solutions = []
        self.connaissances = connaissances

    def reinitialise(self):
        """ Réinitialise le moteur.

            La trace et les solutions sont à nouveau vides après l'appel à\
            cette méthode.
        """

        self.trace = []
        self.solutions = []

    def chaine(self):
        """ Effectue le chaînage.

            Si des solutions sont trouvées, elles sont placées dans\
            ``self.solutions`` et également retournées.

            :return: les solutions.
        """

        # Nous retournons un ensemble vide dans ce cas.
        return self.solutions

    def affiche_trace(self, indent=None):
        """ Affiche la trace d'un chaînage après l'appel à ``chaine``.

            :param str indent: l'identation souhaitée au début de chaque ligne\
            (quatre espaces par défaut).
        """

        if indent is None:
            indent = Chainage.__indentation

        print('Trace:')
        for evenement in self.trace:
            print('{}{}'.format(indent, evenement))

    def affiche_solutions(self, indent=None):
        """ Affiche les solutions d'un chaînage après l'appel à ``chaine``.

            :param str indent: l'identation souhaitée au début de chaque ligne\
            (quatre espaces par défaut).
        """

        if indent is None:
            indent = Chainage.__indentation

        if len(self.solutions) > 0:
            print('Faits déduits:')
            for fait in self.solutions:
                print('{}{}'.format(indent, fait))
        else:
            print('Aucun fait trouvé.')

Module .../moteur_sans_variables/chainage_avant_sans_variables.py :

from .chainage import Chainage

class ChainageAvantSansVariables(Chainage):
    """ Un moteur d'inférence à chaînage avant sans variables. """

    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 .../exemple_impots_sans_variables.py :

from sys import argv, exit
from moteur_sans_variables.regle_sans_variables import RegleSansVariables
from moteur_sans_variables.connaissance import BaseConnaissances
from moteur_sans_variables.chainage_avant_sans_variables import ChainageAvantSansVariables

# La description d'une règle est une liste de deux éléments:
# une liste de conditions, et une conclusion.
regles = [
          [['pas-d-enfants'], 'réduc-enfant-0'],
          [['enfants'], 'réduc-enfant-100'],
          [['bas-salaire'], 'réduc-loyer-200'],
          [['moyen-salaire'], 'réduc-loyer-100'],
          [['haut-salaire'], 'réduc-loyer-0'],
          [['pas-de-loyer'], 'réduc-loyer-0'],
          [['petit-trajet'], 'réduc-trajet-0'],
          [['réduc-enfant-0', 'long-trajet'], 'réduc-trajet-100'],
          [['réduc-loyer-0', 'long-trajet'], 'réduc-trajet-100'],
          [['réduc-enfant-100', 'réduc-loyer-100', 'long-trajet'],
           'réduc-trajet-50'],
          [['réduc-enfant-100', 'réduc-loyer-200', 'long-trajet'],
           'réduc-trajet-0'],
          [['réduc-enfant-0', 'réduc-loyer-0', 'réduc-trajet-0'],
           'réduc-0'],
          [['réduc-enfant-100', 'réduc-loyer-0', 'réduc-trajet-0'],
           'réduc-100'],
          [['réduc-enfant-0', 'réduc-loyer-100', 'réduc-trajet-0'],
           'réduc-100'],
          [['réduc-enfant-100', 'réduc-loyer-100', 'réduc-trajet-0'],
           'réduc-200'],
          [['réduc-enfant-0', 'réduc-loyer-200', 'réduc-trajet-0'],
           'réduc-200'],
          [['réduc-enfant-100', 'réduc-loyer-200', 'réduc-trajet-0'],
           'réduc-300'],
          [['réduc-enfant-0', 'réduc-loyer-0', 'réduc-trajet-50'],
           'réduc-50'],
          [['réduc-enfant-100', 'réduc-loyer-0', 'réduc-trajet-50'],
           'réduc-150'],
          [['réduc-enfant-0', 'réduc-loyer-100', 'réduc-trajet-50'],
           'réduc-150'],
          [['réduc-enfant-100', 'réduc-loyer-100', 'réduc-trajet-50'],
           'réduc-250'],
          [['réduc-enfant-0', 'réduc-loyer-200', 'réduc-trajet-50'],
           'réduc-250'],
          [['réduc-enfant-0', 'réduc-loyer-200', 'réduc-trajet-50'],
           'réduc-250'],
          [['réduc-enfant-100', 'réduc-loyer-200', 'réduc-trajet-50'],
           'réduc-350'],
          [['réduc-enfant-0', 'réduc-loyer-0', 'réduc-trajet-100'],
           'réduc-100'],
          [['réduc-enfant-100', 'réduc-loyer-0', 'réduc-trajet-100'],
           'réduc-200'],
          [['réduc-enfant-0', 'réduc-loyer-100', 'réduc-trajet-100'],
           'réduc-200'],
          [['réduc-enfant-100', 'réduc-loyer-100', 'réduc-trajet-100'],
           'réduc-300'],
          [['réduc-enfant-0', 'réduc-loyer-200', 'réduc-trajet-100'],
           'réduc-300'],
          [['réduc-enfant-100', 'réduc-loyer-200', 'réduc-trajet-100'],
           'réduc-400'],
        ]

if len(argv) < 2 or argv[1].lower() not in ('a', 'b'):
    print('On attend au moins un arguments: A ou B')
    exit(1)

if argv[1].lower() == 'a':
    faits_initiaux = ['bas-salaire', 'loyer', 'enfants', 'long-trajet']
elif argv[1].lower() == 'b':
    faits_initiaux = ['pas-d-enfants', 'pas-de-loyer',
                      'haut-salaire', 'long-trajet']

bc = BaseConnaissances(lambda descr: RegleSansVariables(descr[0], descr[1]))
bc.ajoute_faits(faits_initiaux)
bc.ajoute_regles(regles)

moteur = ChainageAvantSansVariables(bc)
moteur.chaine()

moteur.affiche_solutions()

if len(argv) > 2 and argv[2].lower() == 'trace':
    # Utile durant le déboggage.
    moteur.affiche_trace()

Idée de base

L’idée de base d’un moteur d’inférence à chaînage avant est de déduire toutes les faits possibles à partir d’un ensemble de règles et de faits initiaux, c’est-à-dire de propositions qui sont tenues pour vraies dès le départ. Chaque fois qu’un nouveau fait est déduit, l’ensemble des règles doit être appliqué à nouveau à la base des faits : il est en effet possible que le fait nouvellement déduit permette le déclenchement d’une règle qui a déjà été essayée auparavant sans succès. Le processus d’inférence se termine lorsque plus aucun fait nouveau ne peut être déduit.

Exercice 1 : Les faits et les règles

Dans cette série, les propositions ne contiendront pas de variables et seront représentées par des chaînes de caractères (string) contenant leur description en langage naturel. Les faits seront donc des propositions. En outre, les règles seront des clauses de Horn, et donc composées de deux parties :

  • Un ensemble de conditions (des propositions qui doivent être toutes satisfaites pour que la règle se déclenche) ;
  • Une seule conclusion (une proposition qui pourra le cas échéant être inséré dans la base des faits).

Les règles seront ainsi représentées par la classe RegleSansVariables du module regle_sans_variables.py. Cette classe possède deux attributs : une liste de propositions, qui représentent les conditions, et une proposition, qui représente la conclusion. Pour utiliser RegleSansVariables, vous devez donc compléter les méthodes :

  • depend_de(self, fait), qui doit retourner True si le fait passé en argument fait partie des conditions ;
  • satisfaite_par(self, faits), qui doit retourner True si toutes les conditions de déclenchement de la règle sont présentes dans la liste de faits passée en argument ;
  • __repr__, qui retourne une représentation d’une règle sous forme de string. Cela nous permettra d’afficher les règles de manière plus pratique en utilisant la syntaxe print(règle), au lieu de print(règle.conditions) et print(règle.conclusion).

Pensez à utilisez l’opérateur in pour écrire depend_de et la méthode issubset de la classe set pour implémenter satisfaite_par.

Les faits et les règles pertinents pour un problème seront collectés dans la classe BaseConnaissances, qui est décrite dans le module connaissance.py.

Exercice 2 : Le moteur d’inférence à chaînage avant sans variables

Vous disposez maintenant du code nécessaire pour implémenter le moteur à chaînage avant sans variables. Ce code est à implémenter dans chainage_avant_sans_variables.py, en complétant la classe ChainageAvantSansVariables. Notez que cette dernière est une sous-classe de la classe Chainage du module chainage.py et qu’elle hérite par conséquent des deux méthodes affiche_solutions et affiche_trace, qui servent à afficher les résultats et le parcours de l’algorithme. La classe ChainageAvantSansVariables doit recevoir une instance de BaseConnaissances en tant que paramètre de son constructeur. C’est à partir du contenu de cette base de connaissance qu’elle recherchera des faits nouveaux.

Votre tâche consiste à implémenter la méthode chaine dans la classe ChainageAvantSansVariables. N’oubliez pas de placer les faits déduits dans la variable self.solutions au moment où ils sont découverts. Vous pouvez également ajouter les règles et les faits à self.trace à mesure qu’ils interviennent dans l’inférence.

Pour rappel, l’algorithme à implémenter est le suivant :

ChainageAvantSansVariables(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 r.depend_de(q) et r.satisfaite_par(solutions) THEN
10.                 ajouter la conclusion de r en queue de Q
11.             END IF
12.         END FOR
13.     END IF
14. END WHILE
15. RETURN solutions
END ChainageAvantSansVariables

Rappelez-vous qu’en Python, les listes peuvent s’employer comme des queues grâces aux méthodes append et pop.

Test du programme

exemple_impots_sans_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 sur un premier exemple en ajoutant l’option A et vérifiez que le fait 'réduc-300' est correctement déduit. Vous pouvez afficher la trace en utilisant l’option trace.

python3 exemple_impots_sans_variables.py A
python3 exemple_impots_sans_variables.py A trace

Le module contient un deuxième exemple. Quelle devrait être alors la réduction d’impôts ?

python3 exemple_impots_sans_variables.py B
python3 exemple_impots_sans_variables.py B trace