[4.60517019 6.90775528 9.21034037]
CSI 4106 - Automne 2024
Version: nov. 27, 2024 10h48
Expliquer le concept et les étapes clés de la Recherche Arborescente de Monte-Carlo (MCTS).
Comparer MCTS avec d’autres algorithmes de recherche tels que BFS, DFS, A*, le Recuit Simulé et les Algorithmes Génétiques.
Analyser comment MCTS équilibre exploration et exploitation en utilisant la formule UCB1.
Implémenter MCTS dans des applications pratiques comme le Tic-Tac-Toe.
Dans le cours introductif sur la recherche dans l’espace d’états, j’ai utilisé la Recherche Arborescente de Monte-Carlo (MCTS), un élément clé d’AlphaGo, pour illustrer le rôle des algorithmes de recherche dans le raisonnement.
Aujourd’hui, nous concluons cette série en examinant les détails de l’implémentation de cet algorithme.
Un algorithme de Monte-Carlo est une méthode computationnelle qui utilise l’échantillonnage aléatoire pour obtenir des résultats numériques, souvent utilisée pour l’optimisation, l’intégration numérique et l’estimation de distributions de probabilité.
Il se caractérise par sa capacité à traiter des problèmes complexes avec des solutions probabilistes, en échangeant l’exactitude contre l’efficacité et la scalabilité.
Comme d’autres algorithmes discutés précédemment, tels que BFS, DFS, et \(A^\star\), la Recherche Arborescente de Monte-Carlo (MCTS) maintient une frontière de nœuds non expansés.
À l’instar de \(A^\star\), la Recherche Arborescente de Monte-Carlo (MCTS) utilise une heuristique, appelée politique, pour déterminer le nœud optimal à étendre.
Cependant, dans \(A^\star\), l’heuristique est généralement une fonction statique qui estime le coût pour atteindre un objectif, tandis que dans MCTS, la “politique” implique une évaluation dynamique.
À l’instar du Recuit Simulé et des Algorithmes Génétiques, la Recherche Arborescente de Monte-Carlo (MCTS) intègre un mécanisme pour équilibrer exploration et exploitation.
MCTS exploite tous les nœuds visités dans son processus de prise de décision, contrairement à \(A^\star\), qui se concentre principalement sur la frontière actuelle.
De plus, MCTS met à jour itérativement la valeur de ses nœuds en fonction des simulations, tandis que \(A^\star\) utilise généralement une heuristique statique.
Contrairement aux algorithmes précédents avec des arbres de recherche implicites, MCTS construit une structure d’arbre explicite pendant l’exécution.
Initialement, l’arbre possède un seul nœud, qui est \(S_0\).
Nous ajoutons ses descendants et nous sommes prêts à commencer.
La recherche d’arbre de Monte Carlo construit lentement son arbre de recherche.
À chaque itération, les étapes suivantes se produisent :
Sélection : Identifier le nœud optimal en parcourant un seul chemin dans l’arbre, guidé par UCB1.
Expansion : Élargir le nœud s’il est une feuille dans l’arbre MCTS, si \(n \gt 0\).
Déroulement : Simuler une partie depuis l’état actuel jusqu’à un état terminal en sélectionnant des actions aléatoirement.
Rétropropagation : Utiliser l’information obtenue pour mettre à jour le nœud actuel et tous les nœuds parents jusqu’à la racine.
Chaque nœud enregistre son score total et son nombre de visites.
Cette information est utilisée pour calculer une valeur qui guide le parcours de l’arbre, équilibrant exploration et exploitation.
\[ \mathrm{UCB1}(S_i) = \overline{V_i} + C \sqrt{\frac{\ln(N)}{n_i}} \]
La valeur habituelle pour \(C\) est \(\sqrt{2}\).
L’exploration se produit essentiellement lorsque deux nœuds ont approximativement le même score moyen, alors MCTS favorise les nœuds ayant moins de visites (en divisant par \(n\)).
Pour \(n \lt \ln(N)\), la valeur du rapport est supérieure à 1, tandis que pour \(n \gt \ln(N)\), le rapport devient inférieur à 1.
Il y a donc une petite fraction du temps où l’exploration entre en jeu. Mais même alors, la contribution du rapport est assez modérée, nous prenons la racine carrée de ce rapport, multipliée par \(\sqrt{2} \sim 1.414213562\).
[4.60517019 6.90775528 9.21034037]
import numpy as np
import matplotlib.pyplot as plt
num_iterations = 10
# Définir la plage pour n et N
n_values = np.arange(1, num_iterations + 1)
N_values = np.arange(1, num_iterations + 1)
# Préparer une grille pour n et N
N, n = np.meshgrid(N_values, n_values)
# Calculer l'expression pour chaque paire (n, N)
Z = np.sqrt(2) * np.sqrt(np.log(N) / n)
# Tracé
plt.figure(figsize=(8, 6))
plt.contourf(N, n, Z, cmap='viridis')
plt.colorbar(label=r'$\sqrt{2} \times \sqrt{\frac{\log{N}}{n}}$')
plt.xlabel('N')
plt.ylabel('n')
plt.title(r'Visualisation de $\sqrt{2} \times \sqrt{\frac{\log{N}}{n}}$ pour $n, N = 1..\mathrm{num\_iterations}$')
plt.show()
import numpy as np
import matplotlib.pyplot as plt
num_iterations = 100
# Définir la plage pour n et N
n_values = np.arange(1, num_iterations + 1)
N_values = np.arange(1, num_iterations + 1)
# Préparer une grille pour n et N
N, n = np.meshgrid(N_values, n_values)
# Calculer l'expression pour chaque paire (n, N)
Z = np.sqrt(2) * np.sqrt(np.log(N) / n)
# Tracé
plt.figure(figsize=(8, 6))
plt.contourf(N, n, Z, cmap='viridis')
plt.colorbar(label=r'$\sqrt{2} \times \sqrt{\frac{\log{N}}{n}}$')
plt.xlabel('N')
plt.ylabel('n')
plt.title(r'Visualisation de $\sqrt{2} \times \sqrt{\frac{\log{N}}{n}}$ pour $n, N = 1..\mathrm{num\_iterations}$')
plt.show()
# Classe pour le jeu de Tic-Tac-Toe
class TicTacToe:
def __init__(self):
# Initialiser le plateau 3x3 avec des espaces vides
self.size = 3
self.board = np.full((self.size, self.size), ' ')
def get_valid_moves(self, state):
"""
Retourne une liste de positions disponibles sur le plateau.
"""
moves = [(i, j) for i in range(self.size) for j in range(self.size) if state[i][j] == ' ']
return moves
def make_move(self, state, move, player):
"""
Place le symbole du joueur sur le plateau à la position spécifiée.
Retourne le nouvel état.
"""
new_state = state.copy()
new_state[move[0], move[1]] = player
return new_state
def get_opponent(self, player):
"""
Retourne l'adversaire du joueur donné.
"""
return 'O' if player == 'X' else 'X'
def is_terminal(self, state):
"""
Vérifie si le jeu est terminé, soit par victoire soit par match nul.
"""
return self.evaluate(state) != 0 or ' ' not in state
def evaluate(self, state):
"""
Évalue l'état du plateau.
Retourne :
1 si 'X' gagne,
-1 si 'O' gagne,
0 sinon (match nul ou jeu en cours).
"""
lines = []
# Lignes et colonnes
for i in range(self.size):
lines.append(state[i, :]) # Ligne i
lines.append(state[:, i]) # Colonne i
# Diagonales
lines.append(np.diag(state))
lines.append(np.diag(np.fliplr(state)))
# Vérification d'un gagnant
for line in lines:
if np.all(line == 'X'):
return 1
elif np.all(line == 'O'):
return -1
# Pas de gagnant
return 0
def display(self, state):
"""
Affiche le plateau dans la console.
"""
print("\nPlateau actuel :")
for i in range(self.size):
row = '|'.join(state[i])
print(row)
if i < self.size - 1:
print('-' * (self.size * 2 - 1))
Node
# Classe Node pour MCTS
class Node:
def __init__(self, state, parent=None, move=None, player='X'):
self.state = state # État du jeu à ce nœud
self.parent = parent # Nœud parent
self.children = [] # Liste des nœuds enfants
self.visits = 0 # Nombre de fois que le nœud a été visité
self.wins = 0 # Nombre de victoires à partir de ce nœud
self.move = move # Coup qui a mené à ce nœud
self.player = player # Joueur qui a effectué le coup
def is_fully_expanded(self, game):
"""
Vérifie si tous les coups possibles à partir de ce nœud ont été explorés.
"""
return len(self.children) == len(game.get_valid_moves(self.state))
def best_child(self, c_param=math.sqrt(2)):
"""
Sélectionne le nœud enfant avec la valeur UCB1 la plus élevée.
"""
choices_weights = []
for child in self.children:
if child.visits == 0:
ucb1 = float('inf')
else:
win_rate = child.wins / child.visits
exploration = c_param * math.sqrt((2 * math.log(self.visits)) / child.visits)
ucb1 = win_rate + exploration
choices_weights.append(ucb1)
return self.children[np.argmax(choices_weights)]
def most_visited_child(self):
"""
Sélectionne le nœud enfant avec le plus grand nombre de visites.
"""
visits = [child.visits for child in self.children]
return self.children[np.argmax(visits)]
mcts
# Algorithme MCTS
def mcts(game, root, iterations):
for _ in range(iterations):
node = tree_policy(game, root)
reward = default_policy(game, node.state, node.player)
backup(node, reward)
return root.most_visited_child()
def tree_policy(game, node):
"""
Phases de sélection et d'expansion.
"""
while not game.is_terminal(node.state):
if not node.is_fully_expanded(game):
return expand(game, node)
else:
node = node.best_child()
return node
def expand(game, node):
"""
Élargir un nœud enfant à partir du nœud actuel.
"""
tried_moves = [child.move for child in node.children]
possible_moves = game.get_valid_moves(node.state)
for move in possible_moves:
if move not in tried_moves:
next_state = game.make_move(node.state, move, node.player)
child_node = Node(state=next_state, parent=node, move=move, player=game.get_opponent(node.player))
node.children.append(child_node)
return child_node
def default_policy(game, state, player):
"""
Phase de simulation : jouer le jeu aléatoirement à partir de l'état donné.
"""
current_state = state.copy()
current_player = player
while not game.is_terminal(current_state):
possible_moves = game.get_valid_moves(current_state)
move = random.choice(possible_moves)
current_state = game.make_move(current_state, move, current_player)
current_player = game.get_opponent(current_player)
result = game.evaluate(current_state)
return result
def backup(node, reward):
"""
Phase de rétropropagation : mettre à jour les statistiques du nœud.
"""
while node is not None:
node.visits += 1
# Mettre à jour les victoires. Si le résultat est une victoire pour le joueur qui vient de jouer, ajouter 1.
if (node.player == 'X' and reward == 1) or (node.player == 'O' and reward == -1):
node.wins += 1
elif reward == 0:
node.wins += 0.5 # Considérer un match nul comme une demi-victoire
node = node.parent
test_tic_tac_toe_mcts
# Fonction principale pour démontrer l'application
def test_tic_tac_toe_mcts():
game = TicTacToe()
current_state = game.board.copy()
current_player = 'X'
root_node = Node(state=current_state, player=current_player)
while not game.is_terminal(current_state):
game.display(current_state)
print(f"C'est le tour du joueur {current_player}.")
if current_player == 'X':
# Tour de l'IA utilisant MCTS
iterations = 1000 # Ajuster si nécessaire
best_child = mcts(game, root_node, iterations)
current_state = best_child.state
root_node = best_child
else:
# Tour du joueur humain
possible_moves = game.get_valid_moves(current_state)
print("Coups possibles :", possible_moves)
move = None
while move not in possible_moves:
try:
move_input = input("Entrez votre coup sous la forme 'ligne,colonne' : ")
move = tuple(int(x.strip()) for x in move_input.split(','))
except:
print("Entrée invalide. Veuillez entrer les numéros de ligne et de colonne séparés par une virgule.")
current_state = game.make_move(current_state, move, current_player)
# Mettre à jour l'arbre : trouver ou créer le nœud enfant correspondant au coup
matching_child = None
for child in root_node.children:
if child.move == move:
matching_child = child
break
if matching_child:
root_node = matching_child
else:
root_node = Node(state=current_state, parent=None, move=move, player=game.get_opponent(current_player))
# Changer de joueur
current_player = game.get_opponent(current_player)
# Fin de la partie
game.display(current_state)
result = game.evaluate(current_state)
if result == 1:
print("X gagne !")
elif result == -1:
print("O gagne !")
else:
print("C'est un match nul !")
if __name__ == "__main__":
test_tic_tac_toe_mcts()
Implémentez un code pour visualiser l’arbre de recherche, soit sous forme de texte, soit en utilisant Graphviz.
Incorporez des heuristiques pour détecter lorsqu’un coup gagnant est réalisable en un seul mouvement.
Expérimentez en faisant varier le nombre d’itérations et la constante \(C\).
La recherche d’arbre de Monte Carlo (MCTS) est un algorithme de recherche utilisé pour la prise de décision dans des jeux complexes.
MCTS fonctionne en quatre étapes principales : Sélection, Expansion, Déroulement (Simulation) et Rétropropagation.
Il équilibre exploration et exploitation en utilisant la formule UCB1, qui guide la sélection des nœuds en fonction du nombre de visites et des scores.
MCTS maintient un arbre de recherche explicite, mettant à jour les valeurs des nœuds de manière itérative en fonction des simulations.
L’algorithme a des applications variées, y compris dans les jeux d’IA, la conception de médicaments, le routage de circuits et la conduite autonome.
Introduit en 2008, MCTS a gagné en importance grâce à son utilisation dans AlphaGo en 2016.
Contrairement aux algorithmes traditionnels comme \(A^\star\), MCTS utilise des politiques dynamiques et exploite tous les nœuds visités pour la prise de décision.
Mettre en œuvre MCTS implique de suivre les statistiques des nœuds et d’appliquer la formule UCB1 pour guider la recherche.
Un exemple pratique de MCTS est démontré à travers l’implémentation du Tic-Tac-Toe.
Une exploration plus approfondie inclut l’intégration de MCTS avec des modèles d’apprentissage profond comme AlphaZero et MuZero.
Marcel Turcotte
École de science informatique et de génie électrique (SIGE)
Université d’Ottawa