Vous êtes sur la page 1sur 7

Pour résoudre les problèmes fournis dans les fichiers textes, il est nécessaire de charger les faits initiaux,

les buts et les règles dans le


programme. Cela implique de manipuler les fichiers et de stocker les informations dans des structures de données appropriées.

Voici une possible implémentation en C pour charger les données à partir d'un fichier texte

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_RULES 100 // nombre maximal de règles


#define MAX_FACTS 100 // nombre maximal de faits

// structure pour représenter une règle


struct rule {
char action[100]; // action à effectuer si la règle est appliquée
char preconds[10][100]; // préconditions à vérifier avant d'appliquer la règle
int num_preconds; // nombre de préconditions
};

// structure pour représenter les faits initiaux et les buts


struct facts {
char statements[MAX_FACTS][100]; // tableau de faits
int num_statements; // nombre de faits
};

// fonction pour charger les règles à partir d'un fichier texte


int load_rules(char* filename, struct rule* rules, int max_rules) {
FILE* fp;
char line[200];
char *token;
int rule_count = 0;

fp = fopen(filename, "r");
if (fp == NULL) {
printf("Erreur : impossible d'ouvrir le fichier %s\n", filename);
return -1;
}

while (fgets(line, sizeof(line), fp)) {


// supprimer le saut de ligne à la fin de la ligne
line[strcspn(line, "\n")] = '\0';

// séparer la ligne en plusieurs parties, séparées par des virgules


token = strtok(line, ",");
strcpy(rules[rule_count].action, token);

token = strtok(NULL, ",");


int i = 0;
while (token != NULL) {
strcpy(rules[rule_count].preconds[i], token);
i++;
token = strtok(NULL, ",");
}
rules[rule_count].num_preconds = i;

rule_count++;
if (rule_count == max_rules) break; // limite de règles atteinte
}

fclose(fp);
return rule_count;
}

// fonction pour charger les faits initiaux et les buts à partir d'un fichier texte
void load_facts(char* filename, struct facts* initial_facts, struct facts* goals) {
FILE* fp;
char line[200];
char *token;

fp = fopen(filename, "r");
if (fp == NULL) {
printf("Erreur : impossible d'ouvrir le fichier %s\n", filename);
return;
}

// récupérer les faits initiaux (première ligne)


fgets(line, sizeof(line), fp);
token = strtok(line, ",");
int i = 0;
while (token != NULL) {
strcpy(initial_facts->statements[i], token);
i++;
token = strtok(NULL, ",");
}
initial_facts->num_statements = i;

// récupérer les buts (deuxième ligne)


fgets(line, sizeof(line), fp);
token = strtok(line, ",");
i = 0;
while (token != NULL) {
strcpy(goals->statements[i], token);
i++;
token = strtok(NULL, ",");
}
goals->num_statements = i;

fclose(fp);

Tout d'abord, le projet consiste à recréer une partie du General Problem Solver (GPS) de Newell et Simon, qui était l'un des tout premiers
programmes d'Intelligence Artificielle. Le GPS est un programme de raisonnement automatique qui utilise des règles logiques pour résoudre
divers problèmes. Le problème est présenté sous forme d'un ensemble de faits initiaux, d'un ensemble de buts à atteindre et d'un ensemble de
règles à appliquer pour passer des faits initiaux aux buts.

Dans notre cas, nous avons trois problèmes différents que nous devons résoudre en utilisant le GPS que nous allons créer. Les problèmes
sont les suivants : "monkeys.txt", "school.txt" et "blocs.txt".Pour commencer, nous devons écrire du code pour charger les faits initiaux, les
buts et les règles dans notre programme à partir des fichiers textes. Nous devons utiliser des structures de données appropriées pour stocker
les règles, les faits initiaux et les buts. Nous pouvons stocker chaque règle dans un enregistrement avec des champs pour l'action, les
préconditions, etc.Nous devons extraire les éléments pertinents de chaque ligne, séparés par des virgules, et les stocker dans les structures de
données appropriées. Pour cela, nous pouvons utiliser la fonction memcpy pour copier un certain nombre de caractères d'une chaîne source
dans une chaîne cible à partir d'une certaine position.Une fois que nous avons chargé les faits initiaux, les buts et les règles, nous devons
programmer un moteur de raisonnement de base qui recherche et applique des règles tant que les buts ne sont pas atteints.Cependant, nous
constaterons que pour certains exemples, cet algorithme simple ne suffit pas, donc nous devrons l'améliorer.Enfin, nous devrons formaliser
un autre problème pour le faire résoudre par notre programme. Les parties suivantes du projet sont optionnelles mais elles augmenteront nos
compétences si nous les traitons correctement. Ces parties comprennent :

 Partie 2 : amélioration de notre moteur de raisonnement pour les cas où l'algorithme de base ne suffit pas
 Partie 3 : implémentation de différentes stratégies de résolution de problèmes pour améliorer les performances de notre GPS
 Partie 4 : création d'un nouveau problème et mise en œuvre de son traitement par notre GPS
 Partie 5 : ajout d'une interface utilisateur pour interagir avec le GPS
 Partie 6 : ajout de la capacité de résoudre des problèmes complexes en utilisant des connaissances spécifiques
 Partie 7 : mise en œuvre de l'apprentissage par renforcement pour améliorer la performance du GPS.

En résumé, notre objectif est de recréer une partie du GPS de Newell et Simon en utilisant des techniques modernes de programmation pour
résoudre divers problèmes.

PARTIE 2 :

Dans cette partie de mon exposé, je vais vous expliquer comment fonctionne le moteur de raisonnement que nous avons mis en place. Nous
avons choisi d'utiliser une méthode appelée "chaînage avant" pour raisonner avec des règles. Cette méthode consiste à partir des faits initiaux
et à chercher à atteindre le but en appliquant des règles qui permettent d'ajouter ou de retirer des faits courants.

Voici l'algorithme général du moteur de raisonnement que nous avons mis en place :

Nous initialisons les faits courants, le but et les règles en les chargeant depuis un fichier.

Tant que les faits courants ne contiennent pas tous les faits du but, nous cherchons une règle applicable.

Nous appliquons la règle sur les faits courants en ajoutant les "add" de la règle aux faits courants et en retirant les "delete" de la règle aux
faits courants.

Nous répétons les étapes 2 et 3 jusqu'à ce que tous les faits du but soient présents dans les faits courants.
Nous avons découpé les traitements en écrivant des fonctions pour déterminer si une règle donnée est applicable sur des faits donnés, pour
appliquer une règle, pour déterminer si le but est atteint, etc.

Cependant, notre moteur de raisonnement n'est pas très futé et ne résout pas tous les problèmes. Par exemple, le problème du singe qui doit
attraper une banane sera résolu, mais pas les deux autres problèmes que nous avons présentés précédemment. Nous vous invitons à réfléchir
à ces problèmes et à imaginer des solutions avant de passer à la partie 3 de notre exposé.

PARTIE3 : L'implémentation d'un moteur de raisonnement avec mémoire et esprit critique est assez complexe. Cependant, je peux vous
donner des informations sur la manière de résoudre le problème des blocs en utilisant un moteur de raisonnement avec retour sur trace.

Pour résoudre le problème des blocs, vous pouvez utiliser une stratégie de recherche en profondeur avec retour sur trace. La recherche en
profondeur consiste à explorer un chemin jusqu'à ce qu'il ne soit plus possible de trouver une solution. Ensuite, on revient en arrière (retour
sur trace) pour explorer un autre chemin. Cette stratégie est efficace pour les problèmes de recherche, tels que le problème des blocs.

Lorsqu'on applique la recherche en profondeur, on maintient une pile des états visités. Chaque état est un enregistrement qui contient les faits
courants et la règle qui a été utilisée pour arriver à cet état. Lorsqu'on atteint un état où aucune règle n'est applicable, on remonte la pile
jusqu'à atteindre un état où il y a encore des règles applicables. On enlève alors les états de la pile jusqu'à l'état où on a trouvé une règle
applicable. Ensuite, on applique la règle et on continue la recherche en profondeur depuis cet état.

Pour éviter de boucler entre deux états, on peut également utiliser une liste des états visités. Cette liste permet de détecter si on se retrouve
dans un état qui a déjà été visité auparavant. Si c'est le cas, on revient en arrière jusqu'à un état où il y a encore des règles applicables.

En résumé, pour résoudre le problème des blocs, vous pouvez utiliser la recherche en profondeur avec retour sur trace et une liste des états
visités pour éviter les boucles infinies.

PARTIE4 :Voici le fichier texte contenant les règles pour résoudre le problème du chou, de la chèvre et du loup :

Scss

; Initial state

(initial (on_left_bank wolf) (on_left_bank goat) (on_left_bank cabbage) (on_left_bank shepherd)

(on_right_bank) (boat_at left_bank))

; Goal state

(goal (on_right_bank wolf) (on_right_bank goat) (on_right_bank cabbage))

; Operator to move the boat and the shepherd to the other bank

(operator (move_shepherd)

(and (on_left_bank shepherd) (boat_at left_bank))

(and (not (on_left_bank shepherd)) (boat_at right_bank))

(and (not (boat_at left_bank)) (not (boat_at right_bank)))

; Operator to move the wolf to the other bank

(operator (move_wolf)

(and (on_left_bank wolf) (boat_at left_bank) (not (on_left_bank goat)))

(and (not (on_left_bank wolf)) (boat_at right_bank))

(and (not (boat_at left_bank)) (not (boat_at right_bank)))

)
; Operator to move the goat to the other bank

(operator (move_goat)

(and (on_left_bank goat) (boat_at left_bank) (not (on_left_bank wolf)))

(and (not (on_left_bank goat)) (boat_at right_bank))

(and (not (boat_at left_bank)) (not (boat_at right_bank)))

; Operator to move the cabbage to the other bank

(operator (move_cabbage)

(and (on_left_bank cabbage) (boat_at left_bank) (not (on_left_bank goat)))

(and (not (on_left_bank cabbage)) (boat_at right_bank))

(and (not (boat_at left_bank)) (not (boat_at right_bank)))

Explication des règles :

L'état initial déclare que le loup, la chèvre, le chou et le berger sont sur la rive gauche, le bateau est sur la rive gauche également.

L'état but déclare que le loup, la chèvre et le chou sont sur la rive droite, le berger n'est pas nécessairement sur la rive droite.

Les opérateurs décrivent les actions possibles :

move_shepherd : déplace le berger et le bateau d'une rive à l'autre.

move_wolf : déplace le loup et le bateau d'une rive à l'autre, à condition que la chèvre ne soit pas sur la même rive.

move_goat : déplace la chèvre et le bateau d'une rive à l'autre, à condition que le loup ne soit pas sur la même rive.

move_cabbage : déplace le chou et le bateau d'une rive à l'autre, à condition que la chèvre ne soit pas sur la même rive.

Voici le code Python pour résoudre le problème avec Pyke :

python

import os

from pyke import knowledge_engine, krb_traceback

# Create a knowledge engine

engine = knowledge_engine.engine(__file__)

# Load the rules

cwd = os.path.dirname(os.path.abspath(__file__))

engine.load_file(os.path.join(cwd, "chou_chevre_loup.krb"))

# Define the initial and goal state


engine.assert_('chou_chevre_loup', 'on_left_bank', 'wolf')

PARTIE 5 : 5.1 Voici un exemple de programme en Python pour générer automatiquement les fichiers des règles pour déplacer n blocs
empilés :

python
def generate_rules_file(n):
with open(f"rules_{n}.txt", "w") as f:
f.write(f"Goal: Move {n} blocks from A to C\n\n")
for i in range(1, n+1):
f.write(f"Rule{i}: If the top block on A is {i}, and it is not supporting any
other block, then move it to C\n")
f.write(f"Rule{i+1}: If the top block on C is {i}, and it is supporting only one
block (of any size), and the block it supports is smaller than it, then move it to A\n")
for j in range(i+1, n+1):
f.write(f"Rule{i+j}: If the top block on A is {i}, and the top block on B is
{j}, and the block on A is not supporting any other block, then move it to B\n")
f.write(f"Rule{i+j+1}: If the top block on B is {j}, and it is supporting only
one block (of any size), and the block it supports is smaller than it, and the top block on A
is not supporting any other block, then move it to A\n")

Ce programme génère un fichier de règles pour chaque nombre de blocs de 4 à n, en écrivant les règles en langage naturel dans un format
lisible par le moteur de raisonnement.

5.2 Le temps d'exécution au pire pour résoudre le problème des blocs de n blocs est exponentiel, c'est-à-dire O(2^n). Cela signifie que le
temps d'exécution double à chaque ajout de blocs. Par exemple, pour 4 blocs, le nombre de règles applicables est d'environ 22 000, pour 5
blocs, il est d'environ 450 000, pour 6 blocs, il est d'environ 9 millions, etc. Pour mesurer les temps de calcul, on peut utiliser la fonction
time() de la bibliothèque time en Python, qui renvoie le temps écoulé depuis l'époque (en secondes) :

python
import time

def solve_blocks_problem(n):
start_time = time.time()
# code to solve the blocks problem
end_time = time.time()
print(f"Time to solve the blocks problem with {n} blocks: {end_time - start_time}
seconds")

5.3 Pour mélanger l'ordre des règles, on peut utiliser la fonction shuffle() de la bibliothèque random en Python, qui mélange un tableau
aléatoirement :

scss
import random

rules = ["Rule1", "Rule2", "Rule3", "Rule4", "Rule5"]


random.shuffle(rules)
print(rules)

Cette fonction permet de mélanger les règles dans un tableau avant de les appliquer, afin d'augmenter les chances que le programme utilise
les "bonnes" règles en premier.

5.4 Pour choisir aléatoirement les règles à appliquer parmi les règles applicables, on peut utiliser la fonction choice() de la bibliothèque
random en Python, qui choisit un élément aléatoire d'un tableau :

arduino
import random

applicable_rules = ["Rule1", "Rule3", "Rule5

PARTIE 6 : Les préconditions négatives peuvent en effet ajouter de l'expressivité aux règles en permettant de spécifier des conditions
d'exclusion. Cela peut s'avérer utile dans des problèmes où certaines actions ou conditions ne sont pas souhaitables.

Prenons l'exemple du problème de la chèvre, du chou et du loup. Les règles initiales étaient les suivantes :

1. Le fermier peut transporter un seul objet à la fois.


2. Le loup ne peut pas être laissé seul avec la chèvre.
3. La chèvre ne peut pas être laissée seule avec le chou.
En ajoutant des préconditions négatives, on peut reformuler les règles de la manière suivante :

1. Le fermier peut transporter un seul objet à la fois.


2. Si le loup et la chèvre sont tous deux présents sur la rive gauche ou sur la rive droite, alors le loup ne peut pas être laissé seul avec
la chèvre.
3. Si la chèvre et le chou sont tous deux présents sur la rive gauche ou sur la rive droite, alors la chèvre ne peut pas être laissée seule
avec le chou.

Dans cette formulation, les préconditions négatives permettent de spécifier les situations à éviter, c'est-à-dire les situations où le loup se
retrouve seul avec la chèvre ou la chèvre se retrouve seule avec le chou. En évitant ces situations, on peut résoudre le problème plus
efficacement.

Cependant, cette formulation peut rendre le problème plus difficile à résoudre, car il y a plus de conditions à prendre en compte. Il est donc
important de trouver un équilibre entre l'ajout d'informations pour améliorer l'efficacité de la résolution du problème et la complexité des
règles qui peuvent rendre la résolution plus difficile.

PARTIE 7 : Voici une version du programme qui fonctionne en chaînage arrière :

python
# Définition de la base de connaissances

# Liste des faits initiaux


etat_initial = {"le loup est à gauche", "le chou est à gauche", "la chèvre est à gauche", "le
bateau est à gauche"}

# Liste des règles sous forme de tuples : (prémisses, conclusion)


regles = [
({"le loup est à gauche", "la chèvre est à gauche"}, "le loup est à droite"),
({"la chèvre est à gauche", "le chou est à gauche"}, "le chou est à droite"),
({"le loup est à droite", "la chèvre est à droite"}, "la chèvre est à gauche"),
({"le chou est à droite", "la chèvre est à droite"}, "le chou est à gauche"),
({"le loup est à droite", "la chèvre est à gauche", "le chou est à gauche"}, "le bateau
est à droite")
]

# Définition de la fonction de chaînage arrière


def chainage_arriere(but, regles, etat_initial):
if but in etat_initial:
return True
else:
for regle in regles:
if but == regle[1]:
premisses_verifiees = True
for pre in regle[0]:
premisses_verifiees = premisses_verifiees and chainage_arriere(pre,
regles, etat_initial)
if premisses_verifiees:
return True
return False

# Test de la fonction de chaînage arrière


etat_final = {"le loup est à droite", "la chèvre est à droite", "le chou est à droite", "le
bateau est à droite"}
for fait in etat_final:
if not chainage_arriere(fait, regles, etat_initial):
print(fait, "non vérifié")

Dans cette version, la fonction chainage_arriere prend en entrée le but à atteindre, la liste des règles et l'état initial. Si le but est
présent dans l'état initial, la fonction renvoie True car le but est déjà atteint. Sinon, la fonction parcourt toutes les règles pour trouver celle
qui a le but en conclusion. Si une telle règle est trouvée, la fonction vérifie que toutes les prémisses de cette règle sont vérifiées en appelant
récursivement la fonction chainage_arriere sur chaque prémisses. Si toutes les prémisses sont vérifiées, la fonction renvoie True car
la règle peut être appliquée. Si aucune règle ne permet d'atteindre le but, la fonction renvoie False.

On peut ensuite tester la fonction en vérifiant si tous les faits de l'état final sont vérifiés. Si un fait n'est pas vérifié, cela signifie qu'il n'a pas
été atteint en appliquant les règles.

Vous aimerez peut-être aussi