Introduction aux algorithmes de recherche

CSI 4106 - Automne 2025

Marcel Turcotte

Version: nov. 7, 2025 09h13

Préambule

Message du jour

Objectifs d’apprentissage

  • Décrire le rôle des algorithmes de recherche en IA, essentiel pour la planification, le raisonnement, et des applications telles qu’AlphaGo.
  • Rappeler les concepts clés de la recherche : espace d’états, état initial/final, actions, modèles de transition, fonctions de coût.
  • Identifier les différences entre les algorithmes de recherche non-informés (BFS et DFS).
  • Implémenter BFS et DFS, les comparer en utilisant le problème du puzzle à 8 tuiles.
  • Analyser la performance et l’optimalité de divers algorithmes de recherche.

Justification

Pourquoi étudier ces algorithmes?

Justification

Nous avons perfectionné notre expertise en apprentissage automatique au point de bien comprendre les réseaux neuronaux et le deep learning, ce qui nous permet de développer des modèles simples avec Keras.

En revanche, les récentes avancées sur les recherche arborescente de Monte-Carlo (MCTS) ont joué un rôle central dans la recherche en intelligence artificielle. Nous passons donc de la concentration sur le deep learning vers l’exploration de la recherche.

Justification

L’intégration du deep learning et de MCTS sous-tend des applications modernes comme AlphaGo, AlphaZero, et MuZero.

Les algorithmes de recherche sont donc cruciaux pour la planification et le raisonnement et leur importance devrait croître avec les avancées technologiques.

Recherche (un historique biaisé)

  • 1968 – A* : Recherche heuristique pour la planification en IA.
  • 1970s-1980s – Algorithmes à population (ex : algorithmes génétiques) : Optimisation stochastique pour grands espaces.
  • 1980s – CSP (Problèmes de satisfaction de contraintes) : Recherche dans des espaces structurés avec contraintes explicites.
  • 2013 – DQN : Apprentissage par renforcement (Q-learning) sur données brutes.
  • 2015 – AlphaGo : Recherche d’arbres de jeu avec MCTS combiné au deep learning.
  • 2017 – AlphaZero : MCTS pour auto-apprentissage dans plusieurs domaines.
  • 2019 – MuZero : Recherche dans environnements inconnus sans modèles préexistants.
  • 2020 – Agent57 : Recherche généralisée à travers divers environnements (jeux Atari).
  • 2021 – FunSearch : Généralisation possible des techniques de recherche.

Recherche

AlphaGo - Le Film

AlphaGo2MuZero

Recherche

Applications

  • Cheminement et navigation : Utilisé en robotique et dans les jeux vidéo pour trouver un chemin d’un point de départ à une destination.

  • Résolution de puzzles : Résolution de problèmes comme le puzzle à 8 tuiles, labyrinthes, ou Sudoku.

  • Analyse de réseaux : Analyse de connectivité et des chemins les plus courts dans les réseaux sociaux ou les cartes de transport.

  • Jeux : Évaluation des coups dans les jeux comme les échecs ou le Go.

Applications

  • Planification et ordonnancement : Planification des tâches dans la gestion de projet ou l’ordonnancement de vols.

  • Allocation de ressources : Allocation de ressources dans un réseau ou organisation.

  • Problèmes de configuration : Assemblage de composants pour satisfaire des exigences, comme la configuration d’un système informatique.

Applications

  • Décisions sous incertitude : Décisions en jeux de stratégie en temps réel ou simulations.

  • Narration : Les modèles de langage peuvent générer des récits lorsqu’ils sont guidés par un plan valide provenant d’un planificateur automatisé. (Simon et Muise 2024)

Plan

  1. Recherche déterministe et heuristique : BFS, DFS, A* pour la recherche de chemins et l’optimisation en IA classique.

  2. Algorithmes basés sur la population : Focus sur les problèmes structurés et la recherche stochastique.

  3. Algorithmes de jeux adversariaux : Minimax, élagage alpha-bêta, MCTS pour la prise de décision dans des environnements compétitifs.

Définition

Lorsqu’il n’est pas immédiatement évident de savoir quelle action correcte entreprendre, un agent peut avoir besoin de planifier à l’avance : considérer une séquence d’actions qui forment un chemin vers un état objectif. Un tel agent est appelé un agent de résolution de problèmes, et le processus computationnel qu’il entreprend est appelé recherche.

Terminologie

Caractéristiques de l’environnement

  • Observabilité : Partiellement observable ou totalement observable

  • Composition de l’agent : Agent unique ou multiple

  • Prédictibilité : Déterministe ou non déterministe

  • Dépendance d’état : Sans état ou avec état

  • Dynamique temporelle : Statique ou dynamique

  • Représentation de l’état : Discrète ou continue

Processus de résolution de problèmes

Recherche : Le processus implique de simuler des séquences d’actions jusqu’à ce que l’agent atteigne son objectif. Une séquence réussie est appelée une solution.

Problème de recherche

  • Un ensemble d’états, appelé espace d’états.

  • Un état initial où l’agent commence.

  • Un ou plusieurs états objectifs qui définissent des résultats réussis.

  • Un ensemble d’actions disponibles dans un état donné \(s\).

  • Un modèle de transition qui détermine l’état suivant en fonction de l’état actuel et de l’action sélectionnée.

  • Une fonction de coût d’action qui spécifie le coût de l’exécution de l’action \(a\) dans l’état \(s\) pour atteindre l’état \(s'\).

Définitions

  • Un chemin est défini comme une séquence d’actions.

  • Une solution est un chemin qui relie l’état initial à l’état objectif.

  • Une solution optimale est le chemin avec le coût le plus bas parmi toutes les solutions possibles.

Exemple : 8-Puzzle

Code
import random
import matplotlib.pyplot as plt
import numpy as np

random.seed(58)

def is_solvable(tiles):
    # Compter les inversions dans la liste à plat des tuiles (en excluant l'espace vide)
    inversions = 0
    for i in range(len(tiles)):
        for j in range(i + 1, len(tiles)):
            if tiles[i] != 0 and tiles[j] != 0 and tiles[i] > tiles[j]:
                inversions += 1
    return inversions % 2 == 0

def generate_solvable_board():
    # Générer une configuration de plateau aléatoire qui est garantie d'être résoluble
    tiles = list(range(9))
    random.shuffle(tiles)
    while not is_solvable(tiles):
        random.shuffle(tiles)

    return tiles

def plot_board(board, title, num_pos, position):
    ax = plt.subplot(1, num_pos, position)
    ax.set_title(title)
    ax.set_xticks([])
    ax.set_yticks([])

    board = np.array(board).reshape(3, 3).tolist() # Reconfigurer en une grille 3x3

    # Utiliser une carte de couleurs pour afficher les numéros
    cmap = plt.cm.plasma
    norm = plt.Normalize(vmin=-1, vmax=8)

    for i in range(3):
        for j in range(3):
            tile_value = board[i][j]
            color = cmap(norm(tile_value))
            ax.add_patch(plt.Rectangle((j, 2 - i), 1, 1, facecolor=color, edgecolor='black'))
            if tile_value == 0:
                ax.add_patch(plt.Rectangle((j, 2 - i), 1, 1, facecolor='white', edgecolor='black'))
            else:
                ax.text(j + 0.5, 2 - i + 0.5, str(tile_value),
                        fontsize=16, ha='center', va='center', color='black')

    ax.set_xlim(0, 3)
    ax.set_ylim(0, 3)

Exemple : 8-Puzzle

Code
# Générer un plateau initial résoluble
initial_board = generate_solvable_board()

# Définir l'état objectif
goal_board = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 0]
]

# Tracer les deux plateaux
plt.figure(figsize=(8, 4))
plot_board(initial_board, "Plateau Initial", 2, 1)
plot_board(goal_board, "État Objectif", 2, 2)
plt.tight_layout()
plt.show()

8-Puzzle

  • Comment les états peuvent-ils être représentés ?

  • Qu’est-ce qui constitue l’état initial ?

  • Qu’est-ce qui définit les actions ?

  • Qu’est-ce qui constituerait un chemin ?

  • Qu’est-ce qui caractérise l’état objectif ?

  • Qu’est-ce qui constituerait une solution ?

  • Quel devrait être le coût d’une action ?

Test

Résolvables ?

Résolvables ?

  • Tous les tableaux sont-ils résolvables ?

  • Un tableau est résolvable s’il a la même parité d’inversion que l’état final.

  • Une inversion est une paire de tuiles (à l’exclusion du vide) qui sont dans le mauvais ordre l’une par rapport à l’autre, lorsqu’on lit le tableau comme une liste unidimensionnelle (de gauche à droite, de haut en bas).

  • Lorsque le but est la séquence ordonnée de 1 à 8, il n’y a pas d’inversion, et donc la parité est paire.

  • Voir Archer (1999) ou Slider Puzzle, Princeton

count_even_odd_parity

Code
from itertools import permutations

def count_even_odd_parity():
    even_count = 0
    odd_count = 0

    for perm in permutations(range(9)):  # 0 représente la tuile vide
        inversions = 0
        for i in range(9):
            for j in range(i + 1, 9):
                if perm[i] != 0 and perm[j] != 0 and perm[i] > perm[j]:
                    inversions += 1
        if inversions % 2 == 0:
            even_count += 1
        else:
            odd_count += 1

    return even_count, odd_count
even, odd = count_even_odd_parity()

print(f"Even parity: {even}")
print(f"Odd parity: {odd}")
Even parity: 181440
Odd parity: 181440

Arbre de recherche

Arbre de recherche

Arbre de recherche

  • La racine de l’arbre de recherche représente l’état initial du problème.

  • Développer un nœud implique d’évaluer toutes les actions possibles disponibles à partir de cet état.

  • Le résultat d’une action est le nouvel état atteint après avoir appliqué cette action à l’état actuel.

  • À l’instar d’autres structures d’arbres, chaque nœud a un parent et peut avoir des enfants.

Frontière

Frontière

Frontière

Recherche non informée

Définition

Une recherche non informée (ou recherche aveugle) est une stratégie de recherche qui explore l’espace de recherche en utilisant uniquement les informations disponibles dans la définition du problème, sans aucune connaissance spécifique au domaine, en évaluant les nœuds uniquement sur la base de leurs propriétés intrinsèques plutôt que sur des coûts estimés ou des heuristiques.

Représentation de l’état

Code
# Tracer les deux plateaux
initial_state_8 = [6, 4, 5,
                   8, 2, 7,
                   1, 0, 3]

goal_state_8 = [1, 2, 3,
                4, 5, 6,
                7, 8, 0]

plt.figure(figsize=(4, 2))
plot_board(initial_state_8, "Plateau Initial", 2, 1)
plot_board(goal_state_8, "État Objectif", 2, 2)
plt.tight_layout()
plt.show()

initial_state_8 = [6, 4, 5,
                   8, 2, 7,
                   1, 0, 3]

goal_state_8 = [1, 2, 3,
                4, 5, 6,
                7, 8, 0]

is_goal

def is_goal(state, goal_state):
    """Détermine si un état donné correspond à l'état objectif."""
    return state == goal_state

expand

def expand(state):
    """Génère les états successeurs en déplaçant la tuile vide dans toutes les directions possibles."""
    size = int(len(state) ** 0.5) # Déterminer la taille du puzzle (3 pour le 8-Puzzle, 4 pour le 15-Puzzle)
    idx = state.index(0) # Trouver l'index de la tuile vide représentée par 0
    x, y = idx % size, idx // size # Convertir l'index en coordonnées (x, y)
    neighbors = []

    # Définir les mouvements possibles : Gauche, Droite, Haut, Bas
    moves = [(-1, 0), (1, 0), (0, -1), (0, 1)]
    for dx, dy in moves:
        nx, ny = x + dx, y + dy
        # Vérifier si la nouvelle position est dans les limites du puzzle
        if 0 <= nx < size and 0 <= ny < size:
            n_idx = ny * size + nx
            new_state = state.copy()
            # Échanger la tuile vide avec la tuile adjacente
            new_state[idx], new_state[n_idx] = new_state[n_idx], new_state[idx]
            neighbors.append(new_state)
    return neighbors

expand

Code
plt.figure(figsize=(8, 2))

solutions = expand(initial_state_8)

plot_board(initial_state_8, "État Initial", 4, 1)

for i, solution in enumerate(solutions):
    plot_board(solution, f"État : {i}", 4, i+2)

plt.tight_layout()
plt.show()

expand(initial_state_8)
[[6, 4, 5, 8, 2, 7, 0, 1, 3],
 [6, 4, 5, 8, 2, 7, 1, 3, 0],
 [6, 4, 5, 8, 0, 7, 1, 2, 3]]

is_empty

def is_empty(frontier):
    """Vérifie si la frontière est vide."""
    return len(frontier) == 0

Cycles

Un chemin qui revisite les mêmes états forme un cycle.

Permettre des cycles rendrait l’arbre de recherche résultant infini.

Pour éviter cela, nous surveillons les états qui ont été atteints, bien que cela entraîne un coût en mémoire.

Recherche en largeur

Recherche en largeur

from collections import deque

La recherche en largeur (BFS) utilise une file d’attente pour gérer les nœuds de la frontière, également connus sous le nom de liste ouverte.

Recherche en largeur

def bfs(initial_state, goal_state):
    frontier = deque() # Initialiser la file d'attente pour BFS
    frontier.append((initial_state, [])) # Chaque élément est un tuple : (état, chemin)

    explored = set()
    explored.add(tuple(initial_state))

    iterations = 0 # utilisé simplement pour comparer les algorithmes

    while not is_empty(frontier):
        current_state, path = frontier.popleft()

        if is_goal(current_state, goal_state):
            print(f"Nombre d'itérations : {iterations}")
            return path + [current_state] # Retourner le chemin réussi

        iterations = iterations + 1

        for neighbor in expand(current_state):
            neighbor_tuple = tuple(neighbor)
            if neighbor_tuple not in explored:
                explored.add(neighbor_tuple)
                frontier.append((neighbor, path + [current_state]))

    return None # Aucune solution trouvée

Cas simple

Code
plt.figure(figsize=(8, 2))

initial_state_8 = [1, 2, 3,
                   4, 0, 6,
                   7, 5, 8]

goal_state_8 = [1, 2, 3,
                4, 5, 6,
                7, 8, 0]

solutions = bfs(initial_state_8, goal_state_8)

for i, solution in enumerate(solutions):
    plot_board(solution, f"Étape : {i}", 3, i+1)

plt.tight_layout()
plt.show()
Nombre d'itérations : 12

initial_state_8 = [1, 2, 3,
                   4, 0, 6,
                   7, 5, 8]

goal_state_8 = [1, 2, 3,
                4, 5, 6,
                7, 8, 0]

bfs(initial_state_8, goal_state_8)
Nombre d'itérations : 12
[[1, 2, 3, 4, 0, 6, 7, 5, 8],
 [1, 2, 3, 4, 5, 6, 7, 0, 8],
 [1, 2, 3, 4, 5, 6, 7, 8, 0]]

Cas difficile

initial_state_8 = [6, 4, 5,
                   8, 2, 7,
                   1, 0, 3]

goal_state_8 = [1, 2, 3,
                4, 5, 6,
                7, 8, 0]

print("Résolution du 8-Puzzle avec la recherche en largeur (BFS)...")

solution_8_bfs = bfs(initial_state_8, goal_state_8)

if solution_8_bfs:
    print(f"Solution BFS trouvée en {len(solution_8_bfs) - 1} mouvements :")
    print_solution(solution_8_bfs)
else:
    print("Aucune solution trouvée pour le 8-Puzzle en utilisant BFS.")
Résolution du 8-Puzzle avec la recherche en largeur (BFS)...
Nombre d'itérations : 145605
Solution BFS trouvée en 25 mouvements :
Étape 0 :
6 4 5
8 2 7
1   3

Étape 1 :
6 4 5
8 2 7
  1 3

Étape 2 :
6 4 5
  2 7
8 1 3

Étape 3 :
6 4 5
2   7
8 1 3

Étape 4 :
6   5
2 4 7
8 1 3

Étape 5 :
  6 5
2 4 7
8 1 3

Étape 6 :
2 6 5
  4 7
8 1 3

Étape 7 :
2 6 5
4   7
8 1 3

Étape 8 :
2 6 5
4 1 7
8   3

Étape 9 :
2 6 5
4 1 7
  8 3

Étape 10 :
2 6 5
  1 7
4 8 3

Étape 11 :
2 6 5
1   7
4 8 3

Étape 12 :
2 6 5
1 7  
4 8 3

Étape 13 :
2 6 5
1 7 3
4 8  

Étape 14 :
2 6 5
1 7 3
4   8

Étape 15 :
2 6 5
1   3
4 7 8

Étape 16 :
2   5
1 6 3
4 7 8

Étape 17 :
2 5  
1 6 3
4 7 8

Étape 18 :
2 5 3
1 6  
4 7 8

Étape 19 :
2 5 3
1   6
4 7 8

Étape 20 :
2   3
1 5 6
4 7 8

Étape 21 :
  2 3
1 5 6
4 7 8

Étape 22 :
1 2 3
  5 6
4 7 8

Étape 23 :
1 2 3
4 5 6
  7 8

Étape 24 :
1 2 3
4 5 6
7   8

Étape 25 :
1 2 3
4 5 6
7 8  

Arbre de recherche BFS

Recherche en profondeur

Recherche en profondeur

def dfs(initial_state, goal_state):
    frontier = [(initial_state, [])] # Chaque élément est un tuple : (état, chemin)

    explored = set()
    explored.add(tuple(initial_state))

    iterations = 0

    while not is_empty(frontier):
        current_state, path = frontier.pop()

        if is_goal(current_state, goal_state):
            print(f"Nombre d'itérations : {iterations}")
            return path + [current_state] # Retourner le chemin réussi

        iterations = iterations + 1

        for neighbor in expand(current_state):
            neighbor_tuple = tuple(neighbor)
            if neighbor_tuple not in explored:
                explored.add(neighbor_tuple)
                frontier.append((neighbor, path + [current_state]))

    return None # Aucune solution trouvée

Arbre de recherche DFS

Arbre de recherche DFS

Cas Simple

Code
plt.figure(figsize=(8, 2))

initial_state_8 = [1, 2, 3,
                   4, 0, 6,
                   7, 5, 8]

goal_state_8 = [1, 2, 3,
                4, 5, 6,
                7, 8, 0]

solutions = dfs(initial_state_8, goal_state_8)

for i, solution in enumerate(solutions):
    plot_board(solution, f"Étape : {i}", 3, i+1)

plt.tight_layout()
plt.show()
Nombre d'itérations : 2

initial_state_8 = [1, 2, 3,
                   4, 0, 6,
                   7, 5, 8]

goal_state_8 = [1, 2, 3,
                4, 5, 6,
                7, 8, 0]

bfs(initial_state_8, goal_state_8)
Nombre d'itérations : 12
[[1, 2, 3, 4, 0, 6, 7, 5, 8],
 [1, 2, 3, 4, 5, 6, 7, 0, 8],
 [1, 2, 3, 4, 5, 6, 7, 8, 0]]

Cas Difficile

initial_state_8 = [6, 4, 5,
                   8, 2, 7,
                   1, 0, 3]

goal_state_8 = [1, 2, 3,
                4, 5, 6,
                7, 8, 0]

print("Résolution du 8-Puzzle avec DFS...")

solution_8_bfs = dfs(initial_state_8, goal_state_8)

if solution_8_bfs:
    print(f"Solution DFS trouvée: {len(solution_8_bfs) - 1} mouvements")
    # print_solution(solution_8_bfs)
else:
    print("Aucune solution trouvée pour le 8-Puzzle en utilisant DFS.")
Résolution du 8-Puzzle avec DFS...
Nombre d'itérations : 1187
Solution DFS trouvée: 1157 mouvements

Remarques

  • La recherche en largeur (BFS) identifie la solution optimale, 25 mouvements, en 145 605 itérations.

  • La recherche en profondeur (DFS) découvre une solution impliquant 1 157 mouvements en 1 187 itérations.

Prologue

Résumé

  • Justification pour l’étude de la recherche
  • Terminologie et concepts clés
  • Algorithmes de recherche non informée
  • Recherche en largeur (BFS)
  • Recherche en profondeur (DFS)
  • Implémentations

Prochaine conférence

  • Nous explorerons plus en détail les fonctions heuristiques et examinerons d’autres algorithmes de recherche.

Références

Archer, Aaron F. 1999. « A Modern Treatment of the 15 Puzzle ». The American Mathematical Monthly 106 (9): 793‑99. https://doi.org/10.1080/00029890.1999.12005124.
Russell, Stuart, et Peter Norvig. 2020. Artificial Intelligence: A Modern Approach. 4ᵉ éd. Pearson. http://aima.cs.berkeley.edu/.
Schrittwieser, Julian, Ioannis Antonoglou, Thomas Hubert, Karen Simonyan, Laurent Sifre, Simon Schmitt, Arthur Guez, et al. 2020. « Mastering Atari, Go, chess and shogi by planning with a learned model ». Nature 588 (7839): 604‑9. https://doi.org/10.1038/s41586-020-03051-4.
Silver, David, Aja Huang, Chris J. Maddison, Arthur Guez, Laurent Sifre, George van den Driessche, Julian Schrittwieser, et al. 2016. « Mastering the game of Go with deep neural networks and tree search ». Nature 529 (7587): 484‑89. https://doi.org/10.1038/nature16961.
Simon, Nisha, et Christian Muise. 2024. « Want To Choose Your Own Adventure? Then First Make a Plan. » Proceedings of the Canadian Conference on Artificial Intelligence.

Marcel Turcotte

Marcel.Turcotte@uOttawa.ca

École de science informatique et de génie électrique (SIGE)

Université d’Ottawa