IFT 615 — Intelligence artificielle

Hiver 2024

TP3


Remise: Jeudi 14 mars à minuit

Fichiers sources

Mode de soumission: Turnin TP3

Total de points 30. Pondération de 8%.

NB: Tout retard vaudra 0.

Équipe:

La composition des équipes est fournie avec cet énoncé et est obligatoire. [cliquez ici pour voir]

Enseignant : Froduald Kabanza, Courriel: froduald.kabanza@usherbrooke.ca

Auxiliaire : D'Jeff Kanda Nkashama, Courriel: djeff.nkashama.kanda@usherbrooke.ca

Source: The Pac-Man Projects, University of California, Berkeley

Évaluation

Votre implementation sera évaluée par un correcteur automatique autograder.py. Veuillez ne pas changer les noms des fonctions ou des classes fournies dans le code, sinon vous allez perturber le correcteur automatique.

Veuillez noter que votre note finale peut être différente de la note que vous obtenez avec le correcteur automatique.

Soumission

Vous devez soumettre les fichiers bustersAgents.py, NaiveBayesClassifier, et inference.py, ainsi qu’un document solution.pdf.

Plagiat

Nous prendrons soin de vérifier votre code par rapport aux autres soumissions de la classe afin de détecter toute redondance logique.

Tout cas de plagiat sera sanctionné adéquatement. Voir le document informatif du Groupe de travail antiplagiat de l’Université de Sherbrooke à cet effet.


Introduction

logo UdeS Dans ce projet, vous allez concevoir des agents Pacman qui utilisent des capteurs pour localiser et manger des fantômes invisibles. Vous passerez de la localisation de fantômes uniques et stationnaires à la chasse de meutes de fantômes multiples en mouvement.

Le code de ce projet contient plusieurs fichiers, mais vous êtes appelés à ne modifier que les fichiers bustersAgents.py, inference.py et NaiveBayesClassifier aux endroits bien indiqués.

 def observeUpdate(self, observation, gameState):
        """
        Update beliefs based on the distance observation and Pacman's position.
        """
        "*** YOUR CODE HERE ***"

Toutefois, vous pouvez consulter le contenu des autres fichiers sans les modifiers.

Dans ce jeu, Pacman est équipé des capteurs qui fournissent des lectures bruitées de la distance de Manhattan à chaque fantôme. Le jeu se termine lorsque Pacman a mangé tous les fantômes. Pour commencer, essayez de jouer vous-même au jeu en utilisant le clavier.

python busters.py

Questions

Tout au long de ce projet, nous utiliserons la classe DiscreteDistribution définie dans inference.py pour modéliser les distributions de croyances et de poids. Cette classe est une extension de la classe dictionnaire Python, où les clés sont les différents éléments discrets de notre distribution, et les valeurs correspondantes sont proportionnelles à la croyance ou au poids que la distribution attribue à cet élément. Il vous est demandé de compléter les parties manquantes de cette classe, ce qui sera crucial pour toutes les autres questions.






Prérequis



Question 0: 1 point

Tout d’abord, implementez la méthode normalize, qui normalise les valeurs de la distribution pour que leur somme soit égale à un.

Note

Pour une distribution vide ou une distribution dont toutes les valeurs sont égales à zéro, ne faites rien. Notez que cette méthode modifie directement la distribution, au lieu de renvoyer une nouvelle distribution.



Inférence exacte



Question 1 : 2 points

Dans cette question, vous allez implémenter la fonction getObservationProb(noisyDistance, pacmanPosition, ghostPosition, jailPosition) de la classe de base InferenceModule dans inference.py. Cette fonction renvoie la probabilité \(P(noisyDistance\ |\ pacmanPosition,\ ghostPosition)\) de la lecture bruitée de la distance étant donné la position de Pacman et la position du fantôme. Vous n'avez pas à calculer directement cette probabilité. Vous pouvez appeler la fonction busters.getObservationProbability(noisyDistance, trueDistance) qui renvoie une distribution de probabilité sur les lectures de distance étant donné la vraie distance entre Pacman et le fantôme, \(P(noisyDistance\ |\ trueDistance)\). La fonction manhattanDistance est déjà implémentée et permet le calcul de la distance de Manhattan entre Pacman et celui du fantôme.

Lorsqu’un fantôme est capturé, il est envoyé à l’emplacement de la prison, et le capteur de distance renvoie de manière déterministe None.

Une bonne implémentation devrait gérer les cas particulier suivants:

  • Si le fantôme est en prison et si noisyDistance est indéfinie simultanément, alors la probabilité est 1.
  • Au contraire, la probabilité que noisyDistance soit indéfinie lorsque le fantôme n'est pas en prison en même temps est nulle. De la même manière, si le fantôme est en prison alors noisyDistance est nécessairement nul aussi. Sinon, on retourne 0.

Vous pouvez tester votre code avec le correcteur automatique en utilisant la commande suivante:

python autograder.py -q q1

Question 2 : 3 points

Dans cette question, vous devez implémenter la méthode observeUpdate(observation, gameState) de la classe ExactInference de inference.py pour mettre à jour correctement la distribution des croyances de l’agent sur les positions des fantômes en fonction d’une observation des capteurs de Pacman. Les croyances représentent la probabilité que le fantôme se trouve à un endroit particulier, et sont stockées sous la forme d’un objet DiscreteDistribution dans un champ appelé self.beliefs, que vous devez mettre à jour. Vous devez itérer vos mises à jour sur la variable self.allPositions qui inclut toutes les positions légales du fantôme, plus la position spéciale de la prison.

Pour votre implémentation, pensez à utiliser la fonction self.getObservationProb de la question 1, et les fonctions gameState.getPacmanPosition() et self.getJailPosition() pour obtenir respectivement la position de Pacman et la position de la prison .

Dans l’affichage de Pacman, les croyances postérieures élevées sont représentées par des couleurs vives, tandis que les croyances faibles sont représentées par des couleurs faibles. Vous devriez commencer avec un grand nuage de croyances qui se réduit au fur et à mesure que les preuves s’accumulent. En observant les cas de test, assurez-vous de comprendre comment les carrés convergent vers leur coloration finale.

python autograder.py -q q2

Si vous souhaitez exécuter ce test (ou tout autre test) sans graphiques, vous pouvez ajouter l’option suivante :

python autograder.py -q q2 --no-graphic


Question 3 : 4 points

Supposons que Pacman ait également des connaissances sur la façon dont un fantôme peut se déplacer. Par exemple, un fantôme ne peut pas traverser un mur et est limité dans ses déplacements en fonction du temps. Ces informations permettraient au Pacman une mise à jour plus fine de son système de croyance.

Dans la question précédente, vous avez implémenté la mise à jour des croyances de Pacman en fonction de ses observations. Dans cette question, on augmente ce système de croyances avec des informations complémentaires. Vous devez implémenter la fonction elapseTime dans ExactInference. L’étape elapseTime devrait, pour ce problème, mettre à jour la croyance à chaque position sur la carte après l’écoulement d’un pas de temps. Votre agent a accès à la distribution des actions pour le fantôme par le biais de self.getPositionDistribution. Afin d’obtenir la distribution sur les nouvelles positions du fantôme, étant donné sa position précédente, utilisez cette ligne de code :

newPosDist = self.getPositionDistribution(gameState, oldPos)

oldPos se réfère à la position précédente du fantôme. Utilisez encore self.allPositions pour obtenir la liste des positions précédentes. newPosDist est un objet DiscreteDistribution qui donne une liste de positions et leur probabilité au temps t+1 en fonction de la position oldPos au temps t.

python autograder.py -q q3

Question 4 : 2 points

Cette question utilisera vos implémentations observeUpdate et elapseTime ensemble, ainsi qu’une stratégie de chasse simple que vous implémenterez. Dans cette stratégie, Pacman suppose que chaque fantôme se trouve dans la position la plus probable selon ses croyances, puis se dirige vers le fantôme le plus proche. Attention, vous ne devez pas appeler directement les fonctions observeUpdate et elapseTime. Le backend se charge de les appeler à votre place.

Implémentez la méthode chooseAction de GreedyBustersAgent dans bustersAgents.py (ligne 134). Votre agent doit d’abord trouver la position la plus probable de chaque fantôme non capturé restant, puis choisir une action qui minimise la distance entre Pacman et le fantôme le plus proche.

Pour trouver la distance du labyrinthe entre deux positions quelconques (pos1 et pos2), utilisez self.distancer.getDistance(pos1, pos2). Pour trouver la position du successeur d’une position après une action :

successorPosition = Actions.getSuccessor(position, action)

On vous fournit les livingGhostPositionDistributions, une liste d’objets DiscreteDistribution représentant les distributions de croyance de position pour chacun des fantômes qui ne sont pas encore capturés.

Pour tester votre code :

python autograder.py -q q4





Inférence approximative



Question 5 : 2 points

Pour les prochaines questions, vous allez implémenter un algorithme de filtrage de particules pour suivre un seul fantôme.

Tout d’abord, implémentez les fonctions initializeUniformly et getBeliefDistribution dans la classe ParticleFilter du fichier inference.py. Une particule est une position de fantôme dans ce problème d’inférence. Notez que, pour l’initialisation, les particules doivent être distribuées de manière égale (et non aléatoire) sur les positions légales afin d’assurer une priorité uniforme. Astuce: l’opérateur mod devrait être utilisé pour initializeUniformly afin de permettre une sélection uniforme des positions légales.

Notez que la variable dans laquelle vous stockez vos particules doit être une liste. La méthode getBeliefDistribution prendra ensuite la liste de particules et pour la convertir en un objet DiscreteDistribution. N'oubliez pas de normaliser votre distribution après l'avoir initialisée. De plus, self.particules hérite de la méthode count(key) permettant de compter le nombre de répétitions de l'élément key.

Testez votre code:

python autograder.py -q q5


Question 6 : 3 points

Ensuite, nous allons implémenter la méthode observeUpdate dans la classe ParticleFilter dans inference.py. Cette méthode construit une distribution de poids sur self.particles où le poids d’une particule est la probabilité de l’observation étant donné la position de Pacman et l’emplacement de cette particule. Ensuite, nous rééchantillonnons à partir de cette distribution pondérée pour construire notre nouvelle liste de particules.

Vous devez à nouveau utiliser la fonction self.getObservationProb pour trouver la probabilité d’une observation compte tenu de la position de Pacman, de la position d’un fantôme potentiel et de la position de la prison. La méthode sample de la classe DiscreteDistribution vous sera également utile. Pour rappel, vous pouvez obtenir la position de Pacman en utilisant gameState.getPacmanPosition(), et la position de la prison en utilisant self.getJailPosition().

Il y a un cas spécial qu’une implémentation adéquate doit gérer. Lorsque toutes les particules reçoivent un poids nul, la liste des particules doit être réinitialisée en appelant initializeUniformly. À cet effet, la méthode total de la DiscreteDistribution peut être utile.

python autograder.py -q q6


Question 7 : 3 points

Implémentez la fonction elapseTime dans la classe ParticleFilter du fichier inference.py. Cette fonction doit construire une nouvelle liste de particules qui correspond à chaque particule existante dans self.particles en avançant d’un pas de temps, puis réaffecter cette nouvelle liste à self.particles. Une fois terminé, vous devriez être en mesure de suivre les fantômes presque aussi efficacement qu’avec l’inférence exacte.

Notez que dans cette question, nous testerons à la fois la fonction elapseTime en isolation, ainsi que l’implémentation complète du filtre à particules combinant elapseTime et observe.

Comme dans la méthode elapseTime de la classe ExactInference, vous devez utiliser :

newPosDist = self.getPositionDistribution(gameState, oldPos)

Cette ligne de code permet d’obtenir la distribution sur les nouvelles positions du fantôme, compte tenu de sa position précédente (oldPos). La méthode d’échantillonnage d'exemples (sample) de la classe DiscreteDistribution sera également utile.

python autograder.py -q q7





Filtre de particules joints



Question 8 : 1 point

Jusqu’à présent, nous avons suivi chaque fantôme indépendamment, ce qui fonctionne bien pour le RandomGhost par défaut ou le DirectionalGhost . Cependant, DispersingGhost choisit des actions qui évitent les autres fantômes. Puisque les modèles de transition des fantômes ne sont plus indépendants, tous les fantômes doivent être suivis conjointement dans un réseau de Bayes dynamique !

Le réseau de Bayes a la structure suivante, où les variables cachées \(G\) représentent les positions des fantômes et les variables d’émission \(E\) sont les distances bruitées à chaque fantôme. Cette structure peut être étendue à d’autres fantômes, mais seuls deux (\(a\) et \(b\)) sont présentés ci-dessous.

logo UdeS

Vous allez maintenant implémenter un filtre de particules qui suit plusieurs fantômes simultanément. Chaque particule représentera un tuple de positions de fantômes qui est un échantillon de l’endroit où se trouvent tous les fantômes à l’heure actuelle. Le code est déjà configuré pour extraire les distributions marginales de chaque fantôme à partir de l’algorithme d’inférence conjointe que vous allez créer, afin de pouvoir afficher des nuages de croyances sur les fantômes individuels.

Complétez la méthode initializeUniformly dans JointParticleFilter du fichier inference.py. Le package de la librairie standard Python itertools peut vous êtres utile. Plus précisément, consultez itertools.product pour obtenir une implémentation du produit cartésien. Cependant, notez que les permutations ne sont pas retournées dans un ordre aléatoire. Par conséquent, vous devez ensuite mélanger la liste des permutations afin d’assurer un placement uniforme des particules sur la carte de jeu. Après le mélange, vous pouvez utiliser l’opérateur mod pour indexer la liste des permutations et ajouter des particules à self.particles.

Pour clarifier, nous n’avons pas eu besoin de mélanger dans initializeUniformly de q5 parce que q8, contrairement à q5, a quelques tests où self.numParticles est inférieur au nombre total de permutations, donc nous devons mélanger la liste pour s’assurer que les premiers éléments self.numParticles dans votre liste de permutations sont répartis uniformément sur l’ensemble du tableau.

Comme précédemment, utilisez self.legalPositions pour obtenir une liste des positions qu’un fantôme peut occuper et la variable dans laquelle vous stockez vos particules doit être une liste.

python autograder.py -q q8


Question 9 : 3 points

Dans cette question, vous allez compléter la méthode observeUpdate dans la classe JointParticleFilter de inference.py. Votre implémentation devrait être similaire à celle de ParticleFilter, mais appliquée à plusieurs fantômes. Une implémentation correcte pondérera et rééchantillonnera la liste entière des particules en fonction de l’observation de toutes les distances vers les fantômes.

Pour boucler sur tous les fantômes, utilisez :

for i in range(self.numGhosts):
    ...

Vous pouvez toujours obtenir la position de Pacman en utilisant gameState.getPacmanPosition(), mais pour obtenir la position de la prison d’un fantôme, utilisez self.getJailPosition(i), puisqu’il y a maintenant plusieurs fantômes ayant chacun leur propre position de prison.

Votre implémentation doit également gérer le cas particulier où toutes les particules reçoivent un poids nul. Dans ce cas, self.particles doit être recréé à partir de la distribution antérieure en appelant initializeUniformly.

Comme dans la méthode de mise à jour de la classe ParticleFilter, vous devez à nouveau utiliser la fonction self.getObservationProb pour trouver la probabilité d’une observation étant donné la position de Pacman, une position potentielle de fantôme et la position de la prison. La méthode sample de la classe DiscreteDistribution vous sera de nouveau utile.

python autograder.py -q q9


Question 10 : 2 points

Complétez la méthode elapseTime dans JointParticleFilter dans inference.py pour rééchantillonner correctement chaque particule pour le réseau Bayesien. En particulier, chaque fantôme doit trouver une nouvelle position conditionnée par les positions de tous les fantômes au pas de temps précédent.

Comme dans la dernière question, vous pouvez boucler sur les fantômes en utilisant :

for i in range(self.numGhosts):
    ...

Ensuite, en supposant que i se réfère à l’indice du fantôme, pour obtenir les distributions sur les nouvelles positions pour ce seul fantôme, étant donné la liste (prevGhostPositions) des positions précédentes de tous les fantômes, utilisez :

newPosDist = self.getPositionDistribution(gameState, prevGhostPositions, i, self.ghostAgents[i])

Testez avec cette commande:

python autograder.py -q q10



Classifieur bayésien naïf



Question 11 : 4 points

Dans cette question vous devez implémenter un classifieur bayésien naïf multinomial pour identifier la langue d’un texte quelconque. Étant donné une matrice \(T\) avec \(n\) lignes et \(d\) colonnes où \(n\) et \(d\) représentent respectivement le nombre de documents du corpus et le nombre de mots (tokens) distincts, votre classifieur doit identifier la langue \(lang \in \{\mathrm{anglais}, \mathrm{espagnol}, \mathrm{français}, \mathrm{portugais}\}\) d'un document \(t_i \in T\), \(i = 1 \ldots n\). Chaque \(t_i\) est un vecteur \( (\mathrm{freq}(w_{i1}), \mathrm{freq}(w_{i2}), …, \mathrm{freq}(w_{id}) ) \) où \(w_{ij}\) et \(\mathrm{freq}(w_{ij})\) dénotent respectivement le mot \(j, j = 1 \ldots d\) et sa fréquence dans le document \(t_i\). Voici un exemple de la représentation des données:

Exemple :

  bonjour hi hello comment man classe surprise langue
T1 1 0 0 0 0 1 0 french
T2 0 3 0 0 1 0 0 english
T3 1 0 0 0 0 1 4 french

Dans cet exemple, le document T3 contient une fois le mot "bonjour", et quatre fois le terme "surprise".

Vous implémentez d'abord la fonction get_posterior(X) qui calcule les probabilités a posteriori des langues étant donné un texte. En d’autre termes, vous devez calculer

\( \begin{align*} P(lang \mid w_1, \dots, w_d) &= P(lang) \prod_{i=1}^{d} P(w_i \mid lang)^{\mathrm{freq}(w_i)} \\ &= log\Big(P(lang)\Big) + \sum_{i=1}^d log\Big(P(w_i \mid lang)\Big) \cdot \mathrm{freq}(w_i) \end{align*} \)

où \(log(\cdot)\) représente le logarithme naturel. Pour cette implémentation, on vous fournit les probabilités a priori \(P(lang)\) via le tableau self.prior de taille n_langues et les probabilités des mots étant donné une langue \(P(wi\mid lang)\) sont dans la matrice self.likelihood de taille n_langues x d.

Finalement, implémentez la fonction predict(X) qui prédit la langue de chaque document présent dans l'entrée X. Le code fourni convertit déjà les langues en nombre naturel (par exemple, "français" peut être identifié par "1"). Vous devez donc simplement retourner un vecteur de dimension équivalente au nombre de lignes de la matrice X où chaque élément représente le code de la langue prédite.

Astuce: rappelez-vous de la méthode argmax de Numpy dans le TP1 et de son argument axis.

Utilisez la commande suivante pour tester votre code:

python NaiveBayesClassifier.py

Prenez une capture d'écran de la matrice de confusion et interprétez les performances de votre classifieur.(1 point)

Une bonne implementation devrait passer le test ci-après:

python NaiveBayesClassifierTest.py
Membre 1 Membre 2
Duchesneau, Paul Girard Hivon, Maxime
Desfossés, Alexandre Ménard Tétreault, Yuhan
Krid, Ahmed Bahaedine Breton Corona, Eduardo Yvan
Tientcheu Tchako, David Jeeson Gendreau, Tommy
Pion, Raphaël Yahya, Mohamed
Carignan, Benjamin Bourgeois, Thomas
Grenier, Philippe-Olivier Proulx, Hugo
Philion, Guillaume Lavallée, Louis
Dia, Adam Lamothe-Morin, Zoé
Bellavance, Nicolas Crozet, Thomas
Pépin, Pierre-Luc Tétreault, Etienne
Boutin, Karl Turcotte, Raphaël
Mailhot, Christophe Allard, Cloé
Bergeron, Marc-Olivier Boulanger, Bastien
Lessard, Nathan Gauthier, Carl
Giasson, Frédéric Charbonneau, Victor