![]()
Construire un système expert en Python
Comment raisonne-t-on ? Comment suit-on une piste logique pour assembler un puzzle ? On peut apprendre aux ordinateurs à faire quelque chose de très similaire. C'est le principe des systèmes experts.
Ce sont des petits cerveaux IA qu'on construit pour résoudre des problèmes complexes, diagnostic médical, gestion financière, etc. Des briques fondamentales de l'IA qui bossent en silence derrière beaucoup d'applications.
Je vous montre comment j'en ai construit un from scratch en Python. Pas de magie, juste de la logique. À la fin, vous aurez les clés pour créer le vôtre. 🧠💻
C'est quoi un système expert ?
Un Sherlock Holmes numérique. Il prend des faits et des règles pour déduire des réponses. Une machine à raisonnement, parfaite pour prouver des hypothèses ou prendre des décisions complexes.
Expert System Components
Les briques du système
Les règles: Des Lego logiques
Rules Visualization
Le cœur du système : un ensemble de règles. Des briques Lego qu'on assemble pour construire des raisonnements complexes. Chaque règle lie des faits (lettres majuscules) via des connecteurs :
&: ET, Tous les faits doivent êtreVrai.|: OU, Un seul suffit.^: XOR, Un seul, mais pas les deux.=>: IMPLIQUE, Si gauche estVrai, droite l'est aussi.
La table de vérité
Inference Truth Table
Notre antisèche logique. Pour p => q : si p est faux, q peut être n'importe quoi. Mais si p est vrai, q doit l'être aussi. C'est le moteur de la déduction.
Faits et requêtes
Facts and Queries
Les faits sont nos vérités de départ (lettres majuscules). Par défaut, tout est à faux. Un fait devient vrai soit par déclaration initiale (=ABC), soit par déduction.
Les requêtes (?XYZ) sont les questions auxquelles le système doit répondre.
Le résolveur
Chaînage avant vs arrière
Deux approches pour résoudre :
- Chaînage avant : partir des faits, voir où les règles mènent. Entrée du labyrinthe.
- Chaînage arrière : partir de la requête, remonter vers les faits. Sortie du labyrinthe.
J'ai choisi le chaînage arrière. Plus intuitif : on commence par le suspect et on cherche les indices.
La structure de données
La classe Node
Brique de base du système :
class Node:def __init__(self):self.children = [] # A => B : => est enfant de Bself.visited = False # Évite les boucles infiniesself.state = False # Résultat Vrai/Faux
Chaque nœud a un état, trace s'il a été visité, et se connecte à d'autres. Pour A => B : A est enfant de =>, qui est enfant de B.
AtomNode et ConnectorNode
Deux spécialisations :
class AtomNode(Node):def __init__(self, name):super(AtomNode, self).__init__()self.name = name
class ConnectorNode(Node):def __init__(self, connector_type):super(ConnectorNode, self).__init__(tree)self.type = connector_typeself.operands = [] # A + B : A et B sont opérandes de +self.state = None
AtomNode = faits (A, B, C). ConnectorNode = opérateurs (ET, OU, XOR, IMPLIQUE).
Implémentation
Étape 1: Liste unique d'atomes
Parser l'entrée, créer une liste de tous les atomes distincts. Crucial : chaque fois que le système voit 'A', il pointe vers le même objet AtomNode. Source unique de vérité.
Étape 2: Notation Polonaise Inverse (NPI)
RPN Representation
Au lieu de A + B, on écrit A B +. Bizarre en apparence, mais révolutionnaire pour un ordinateur. L'ordre des opérations devient explicite, le parsing est trivial. On lit de gauche à droite, les opérandes sont consommés et remplacés par le résultat.
Étape 3: Connecter les nœuds
Avec les règles en NPI, on construit le réseau :
stack = []for x in npi_rule:if x not in OPERATORS:stack.append(self.atoms[x])else:pop0 = stack.pop()pop1 = stack.pop()# Si un des éléments dépilés est le même connecteur qu'on va créer (AND, OR, XOR)if isinstance(pop0, ConnectorNode) and pop0.type is LST_OP[x]:pop0.add_operand(pop1)new_connector = pop0self.connectors.pop()elif isinstance(pop1, ConnectorNode) and pop1.type is LST_OP[x]:pop1.add_operand(pop0)new_connector = pop1self.connectors.pop()else:connector_x = self.create_connector(LST_OP[x])connector_x.add_operands([pop0, pop1])new_connector = connector_xself.connectors.append(new_connector)stack.append(new_connector)return stack.pop()
Atome → on push sur la pile. Opérateur → on pop les opérandes, on les lie au nouveau connecteur, on push le résultat. Le graphe se construit pièce par pièce.
Étape 4: Résoudre les requêtes
Le moment de vérité. Une fonction récursive qui parcourt le graphe :
# Pseudocodedef resolve(nodeX):if nodeX is True:return Truefor child in nodeX.children:res = resolve(child)if res is True:# Un seul des enfants doit être Vrai pour déduire que le courant est Vraireturn Trueif Node is Connector: # AND OR XOR IMPLYop_results = []for op in nodeX.operands:op_results.append(resolve(op))self.set_state_from_operands(op_results)# Exemple : pour un nœud AND, tous les éléments de op_results doivent être Vrai
On part du nœud requête, on remonte via les enfants. Si un enfant est prouvé Vrai, ça remonte. Pour AND : tous les opérandes doivent être vrais. Pour OR : un seul suffit. La table de vérité guide chaque évaluation.
En résumé
Voilà la logique d'un système expert à chaînage arrière. À partir de règles simples, on crée un système qui raisonne.
C'est une fondation. Pour aller plus loin : chaînage avant, logique floue, etc. Le potentiel est immense.
Le code complet est sur GitHub. Forkez, cassez, améliorez.
Bon code ! 🚀🧠