TP Séance 1 : Qu’est-ce que l’Intelligence Artificielle ?

Introduction à l’Intelligence Artificielle — M1 IABD
Télécharger le notebook Ouvrir dans Colab Les sorties sont celles enregistrées dans le notebook : rien n'est réexécuté.

Introduction à l’Intelligence Artificielle — M1 IABD

Dr. El Hadji Bassirou TOURÉ
Université Cheikh Anta Diop de Dakar


Objectifs

  • Explorer concrètement la distinction entre IA symbolique et IA connexionniste
  • Implémenter un agent réflexe simple
  • Expérimenter avec différents types d’environnements
  • Comprendre les limites de chaque approche

Prérequis

  • Python de base (fonctions, classes, boucles)
  • Avoir lu le CM de la séance 1
# Imports nécessaires
import random
import time
from typing import List, Tuple, Dict, Any
from collections import deque

Partie 1 : IA Symbolique vs Machine Learning

Cette partie illustre la différence fondamentale entre l’IA symbolique (règles programmées) et le Machine Learning (apprentissage des données).

1.1 Problème : Classification de nombres

Objectif : Déterminer si un nombre est “petit” (< 50), “moyen” (50-100), ou “grand” (> 100).

# Approche IA symbolique : règles explicites
def classifier_symbolique(n: int) -> str:
    """
    Classification par règles explicites.
    Les seuils sont codés en dur par le programmeur.
    """
    if n < 50:
        return "petit"
    elif n <= 100:
        return "moyen"
    else:
        return "grand"

# Test
test_numbers = [10, 49, 50, 75, 100, 101, 500]
print("Classification symbolique (règles) :")
for n in test_numbers:
    print(f"  {n} -> {classifier_symbolique(n)}")
# Approche ML : apprendre des données
def classifier_ml_simple(n: int, donnees_entrainement: List[Tuple[int, str]]) -> str:
    """
    Classification par k-NN simplifié (k=1).
    Le système apprend des exemples au lieu d'avoir des règles codées.
    """
    # Trouver l'exemple le plus proche
    plus_proche = min(donnees_entrainement, key=lambda x: abs(x[0] - n))
    return plus_proche[1]

# Données d'entraînement (exemples étiquetés)
donnees = [
    (5, "petit"), (20, "petit"), (40, "petit"),
    (55, "moyen"), (70, "moyen"), (90, "moyen"),
    (120, "grand"), (200, "grand"), (500, "grand")
]

print("\nClassification ML (apprentissage des exemples) :")
for n in test_numbers:
    print(f"  {n} -> {classifier_ml_simple(n, donnees)}")

1.2 Questions

  1. Que se passe-t-il si on veut changer les seuils ?
    • Approche symbolique : modifier le code
    • Approche ML : fournir de nouveaux exemples
  2. Exécutez la cellule ci-dessous pour voir ce qui se passe avec des données différentes :
# Nouvelles données avec des seuils différents (30 et 80)
nouvelles_donnees = [
    (5, "petit"), (15, "petit"), (25, "petit"),
    (35, "moyen"), (50, "moyen"), (70, "moyen"),
    (90, "grand"), (150, "grand"), (300, "grand")
]

print("Comparaison avec nouveaux seuils (implicites dans les données) :")
print(f"{'Nombre':>6} | {'Symbolique':>12} | {'ML':>12}")
print("-" * 35)
for n in test_numbers:
    sym = classifier_symbolique(n)
    ml = classifier_ml_simple(n, nouvelles_donnees)
    match = "✓" if sym == ml else "✗"
    print(f"{n:>6} | {sym:>12} | {ml:>12} {match}")

Observation clé : Le système ML s’adapte automatiquement aux nouveaux seuils via les données, alors que le système symbolique nécessite une modification du code.


Partie 2 : Implémentation d’un Agent Réflexe Simple

On implémente un agent aspirateur dans un monde simple (2 cases : A et B).

2.1 L’environnement

class EnvironnementAspirateur:
    """
    Environnement simple avec deux cases (A et B).
    Chaque case peut être propre ou sale.
    """
    
    def __init__(self):
        # État initial aléatoire
        self.cases = {
            'A': random.choice(['propre', 'sale']),
            'B': random.choice(['propre', 'sale'])
        }
        self.position_agent = random.choice(['A', 'B'])
        self.score = 0
        
    def percevoir(self) -> Tuple[str, str]:
        """Renvoie (position, état_case)"""
        return (self.position_agent, self.cases[self.position_agent])
    
    def executer_action(self, action: str):
        """Exécute une action et met à jour le score."""
        if action == 'aspirer':
            if self.cases[self.position_agent] == 'sale':
                self.cases[self.position_agent] = 'propre'
                self.score += 10  # Récompense pour avoir nettoyé
        elif action == 'gauche':
            self.position_agent = 'A'
            self.score -= 1  # Coût du déplacement
        elif action == 'droite':
            self.position_agent = 'B'
            self.score -= 1
            
    def afficher(self):
        """Affiche l'état de l'environnement."""
        agent_A = "🤖" if self.position_agent == 'A' else "  "
        agent_B = "🤖" if self.position_agent == 'B' else "  "
        etat_A = "💩" if self.cases['A'] == 'sale' else "✨"
        etat_B = "💩" if self.cases['B'] == 'sale' else "✨"
        
        print(f"┌────────┬────────┐")
        print(f"│   A    │   B    │")
        print(f"│  {etat_A} {agent_A}│  {etat_B} {agent_B}│")
        print(f"└────────┴────────┘")
        print(f"Score: {self.score}")

# Test de l'environnement
env = EnvironnementAspirateur()
print("État initial de l'environnement :")
env.afficher()
print(f"Perception de l'agent : {env.percevoir()}")

2.2 Agent réflexe simple

class AgentReflexeSimple:
    """
    Agent réflexe simple basé sur des règles condition-action.
    Ne mémorise rien, décide uniquement sur la perception actuelle.
    """
    
    def choisir_action(self, perception: Tuple[str, str]) -> str:
        """
        Règles :
        - Si la case est sale -> aspirer
        - Si en A et propre -> aller à droite
        - Si en B et propre -> aller à gauche
        """
        position, etat = perception
        
        if etat == 'sale':
            return 'aspirer'
        elif position == 'A':
            return 'droite'
        else:  # position == 'B'
            return 'gauche'


def simuler_agent(agent, environnement, n_etapes: int = 10):
    """Simule un agent dans un environnement pendant n étapes."""
    print("\n" + "="*50)
    print("SIMULATION")
    print("="*50)
    
    for etape in range(n_etapes):
        print(f"\n--- Étape {etape + 1} ---")
        
        # L'agent perçoit
        perception = environnement.percevoir()
        print(f"Perception: {perception}")
        
        # L'agent décide
        action = agent.choisir_action(perception)
        print(f"Action choisie: {action}")
        
        # L'environnement exécute
        environnement.executer_action(action)
        environnement.afficher()
        
        # Vérifier si tout est propre
        if all(v == 'propre' for v in environnement.cases.values()):
            print("\n✅ Tout est propre !")
            break
    
    print(f"\n{'='*50}")
    print(f"Score final: {environnement.score}")
    print(f"{'='*50}")

# Simulation
env = EnvironnementAspirateur()
agent = AgentReflexeSimple()
simuler_agent(agent, env)

2.3 Exercice : Améliorer l’agent

L’agent réflexe simple a un problème : il continue à se déplacer même quand tout est propre.

Exercice : Créez un AgentReflexeAvecMemoire qui se souvient des cases déjà visitées et propres.

class AgentReflexeAvecMemoire:
    """
    Agent basé sur modèle : maintient un état interne
    pour se souvenir de l'état des cases visitées.
    """
    
    def __init__(self):
        # État interne : mémoire des cases connues comme propres
        self.cases_propres = set()
        
    def choisir_action(self, perception: Tuple[str, str]) -> str:
        """
        À COMPLÉTER
        
        Règles suggérées :
        1. Si la case actuelle est sale -> aspirer
        2. Sinon, mémoriser que cette case est propre
        3. Si toutes les cases connues sont propres -> ne rien faire
        4. Sinon, se déplacer vers une case non visitée/sale
        """
        position, etat = perception
        
        # VOTRE CODE ICI
        pass


# Test de votre agent
# env = EnvironnementAspirateur()
# agent_ameliore = AgentReflexeAvecMemoire()
# simuler_agent(agent_ameliore, env, n_etapes=15)

2.4 Solution

class AgentReflexeAvecMemoireSolution:
    """
    Solution : Agent basé sur modèle avec mémoire.
    """
    
    def __init__(self):
        self.cases_propres = set()
        self.toutes_cases = {'A', 'B'}
        
    def choisir_action(self, perception: Tuple[str, str]) -> str:
        position, etat = perception
        
        # Si sale, aspirer
        if etat == 'sale':
            self.cases_propres.discard(position)  # Au cas où on pensait qu'elle était propre
            return 'aspirer'
        
        # Mémoriser que cette case est propre
        self.cases_propres.add(position)
        
        # Si on connaît toutes les cases comme propres, ne rien faire
        if self.cases_propres == self.toutes_cases:
            return 'rien'  # Nouvelle action : ne rien faire
        
        # Sinon, aller vers l'autre case
        if position == 'A':
            return 'droite'
        else:
            return 'gauche'


# Modification de l'environnement pour accepter 'rien'
class EnvironnementAmelioré(EnvironnementAspirateur):
    def executer_action(self, action: str):
        if action == 'rien':
            pass  # Ne rien faire ne coûte rien
        else:
            super().executer_action(action)

# Simulation
print("Agent AVEC mémoire :")
env = EnvironnementAmelioré()
agent = AgentReflexeAvecMemoireSolution()
simuler_agent(agent, env, n_etapes=10)

Partie 3 : Exploration des Types d’Environnements

On va modifier l’environnement pour explorer les différentes propriétés.

3.1 Environnement stochastique

class EnvironnementStochastique(EnvironnementAspirateur):
    """
    Environnement où :
    - Les cases peuvent se salir spontanément
    - L'aspirateur peut échouer avec une certaine probabilité
    """
    
    def __init__(self, proba_salir=0.1, proba_echec=0.2):
        super().__init__()
        self.proba_salir = proba_salir
        self.proba_echec = proba_echec
        
    def executer_action(self, action: str):
        # Les cases peuvent se salir spontanément
        for case in self.cases:
            if self.cases[case] == 'propre' and random.random() < self.proba_salir:
                self.cases[case] = 'sale'
                print(f"  [!] La case {case} s'est salie !")
        
        # L'aspiration peut échouer
        if action == 'aspirer' and random.random() < self.proba_echec:
            print("  [!] L'aspiration a échoué !")
            self.score -= 1
            return
            
        super().executer_action(action)

# Simulation avec environnement stochastique
print("Environnement STOCHASTIQUE :")
env = EnvironnementStochastique(proba_salir=0.2, proba_echec=0.1)
agent = AgentReflexeSimple()
simuler_agent(agent, env, n_etapes=15)

3.2 Questions d’analyse

Exécutez plusieurs fois les simulations et répondez :

  1. L’agent réflexe simple fonctionne-t-il bien dans l’environnement stochastique ?
  2. Pourquoi l’agent avec mémoire peut-il avoir des problèmes dans l’environnement stochastique ?
  3. Comment pourrait-on améliorer l’agent pour gérer l’incertitude ?

Partie 4 : Comparaison Finale — Quel Agent pour Quel Environnement ?

4.1 Benchmark

def benchmark_agent(AgentClass, EnvClass, n_simulations=100, n_etapes=20, **env_kwargs):
    """
    Évalue un agent sur plusieurs simulations.
    Retourne le score moyen et la proportion de succès.
    """
    scores = []
    succes = 0
    
    for _ in range(n_simulations):
        env = EnvClass(**env_kwargs)
        agent = AgentClass()
        
        for _ in range(n_etapes):
            perception = env.percevoir()
            action = agent.choisir_action(perception)
            if action:
                env.executer_action(action)
            
            # Vérifier si tout est propre
            if all(v == 'propre' for v in env.cases.values()):
                succes += 1
                break
        
        scores.append(env.score)
    
    return {
        'score_moyen': sum(scores) / len(scores),
        'taux_succes': succes / n_simulations * 100
    }

# Benchmark dans différents environnements
print("BENCHMARK DES AGENTS")
print("=" * 60)

# Environnement déterministe
print("\n1. Environnement DÉTERMINISTE")
print("-" * 40)
res = benchmark_agent(AgentReflexeSimple, EnvironnementAspirateur)
print(f"Agent réflexe simple : Score={res['score_moyen']:.1f}, Succès={res['taux_succes']:.0f}%")

res = benchmark_agent(AgentReflexeAvecMemoireSolution, EnvironnementAmelioré)
print(f"Agent avec mémoire   : Score={res['score_moyen']:.1f}, Succès={res['taux_succes']:.0f}%")

# Environnement stochastique
print("\n2. Environnement STOCHASTIQUE (proba_salir=0.1)")
print("-" * 40)
res = benchmark_agent(AgentReflexeSimple, EnvironnementStochastique, 
                      proba_salir=0.1, proba_echec=0.1)
print(f"Agent réflexe simple : Score={res['score_moyen']:.1f}, Succès={res['taux_succes']:.0f}%")

Pour aller plus loin (optionnel)

Défi 1 : Environnement à 4 cases

Modifiez l’environnement pour avoir 4 cases (grille 2x2). L’agent doit pouvoir se déplacer dans 4 directions.

# VOTRE CODE ICI - Environnement 2x2
# Indice : utilisez des coordonnées (x, y) au lieu de 'A' et 'B'

Défi 2 : Agent basé sur buts

Créez un agent qui a un but explicite : “toutes les cases doivent être propres”. Au lieu de règles réflexes, il doit planifier une séquence d’actions.

# VOTRE CODE ICI - Agent basé sur buts
# Indice : utilisez BFS pour trouver la séquence d'actions optimale

Résumé du TP

Dans ce TP, nous avons :

  1. Comparé IA symbolique et ML : Les règles explicites vs l’apprentissage des données
  2. Implémenté un agent réflexe simple : Décisions basées uniquement sur la perception actuelle
  3. Amélioré vers un agent avec mémoire : Maintien d’un état interne pour de meilleures décisions
  4. Exploré différents types d’environnements : Déterministe vs stochastique
  5. Mesuré les performances : Benchmark pour comparer les approches

Points clés : - Un agent réflexe simple suffit pour des environnements simples et déterministes - La mémoire (agent basé sur modèle) devient nécessaire quand l’observabilité est partielle - L’incertitude (environnement stochastique) pose des défis que nous aborderons dans les séances suivantes (probabilités, apprentissage)

Retour au sommet