Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

몬테카를로 트리 탐색

몬테카를로 트리 탐색은 게임 플레이와 강화 학습에서 의사 결정에 사용되는 강력한 알고리즘입니다. 이는 트리 탐색과 몬테카를로 시뮬레이션의 장점을 결합한 휴리스틱 탐색 알고리즘입니다. MCTS는 신경기호(neurosymbolic) 시스템이나 강화 학습처럼 크고 복잡한 탐색 공간을 다룰 때 특히 유용합니다.

오늘은 Python으로 MCTS를 구현해 봅시다. 먼저 상태를 표현하는 데 사용할 Node 클래스를 정의해야 합니다.

rom dataclasses import dataclass, field
from collections import defaultdict
import math
from typing import Dict, Set, List, Optional
from abc import ABC, abstractmethod
import random


class Node(ABC):
    """Abstract base class for game state nodes."""

    @abstractmethod
    def find_children(self) -> Set["Node"]:
        """Return all possible child nodes."""

    @abstractmethod
    def get_random_child(self) -> Optional["Node"]:
        """Return a random child node."""

    @abstractmethod
    def is_terminal(self) -> bool:
        """Check if the node is a terminal state."""

    @abstractmethod
    def reward(self) -> float:
        """Return the reward value for a terminal node."""

    @abstractmethod
    def __hash__(self) -> int:
        """Define a hash function for the node."""

    @abstractmethod
    def __eq__(self, other: object) -> bool:
        """Define equality comparison for nodes."""

이제 탐색 상태를 보관하고 트리 탐색을 수행할 MCTS 클래스를 정의하겠습니다.

@dataclass
class MCTS:
    """Monte Carlo Tree Search implementation."""

    exploration_weight: float = 1.0
    total_rewards: Dict[Node, float] = field(default_factory=lambda: defaultdict(float))
    visit_counts: Dict[Node, int] = field(default_factory=lambda: defaultdict(int))
    children: Dict[Node, Set[Node]] = field(default_factory=lambda: defaultdict(set))

이제 현재 노드에서 최선의 수를 선택할 select_best_move 메서드를 구현하겠습니다.

    def select_best_move(self, node: Node) -> Node:
        """Select the best move from the current node."""
        if node.is_terminal():
            raise ValueError(f"Cannot select move from terminal node: {node}")

        if node not in self.children:
            return node.get_random_child()

이 알고리즘에서는 최선의 수를 선택하기 위해 UCT(Upper Confidence Trees) 공식을 사용하겠습니다.

여기서:

  • 는 노드의 총 보상입니다.
  • 는 노드가 방문된 횟수입니다.
  • 은 시뮬레이션의 총 횟수입니다.
  • 는 탐험 상수입니다.
    def simulate(self, node: Node) -> None:
        """Perform one iteration of the MCTS algorithm."""
        path = self._traverse_tree(node)
        leaf = path[-1]
        self._expand_node(leaf)
        reward = self._simulate_random_playout(leaf)
        self._backpropagate(path, reward)

    def _traverse_tree(self, node: Node) -> List[Node]:
        """Traverse the tree to find an unexplored node."""
        path = []
        while True:
            path.append(node)
            if node not in self.children or not self.children[node]:
                return path
            unexplored = self.children[node] - set(self.children.keys())
            if unexplored:
                path.append(unexplored.pop())
                return path
            node = self._select_uct(node)

    def _expand_node(self, node: Node) -> None:
        """Expand the node by adding its children to the tree."""
        if node in self.children:
            return
        self.children[node] = node.find_children()

이제 주어진 노드에서 무작위 플레이아웃을 시뮬레이션할 _simulate_random_playout 메서드를 구현하겠습니다.

def _simulate_random_playout(self, node: Node) -> float:
    """Simulate a random playout from the given node."""
    invert_reward = True
    while True:
        if node.is_terminal():
            reward = node.reward()
            return 1 - reward if invert_reward else reward
        node = node.get_random_child()
        invert_reward = not invert_reward

이제 경로상의 노드들에 대한 통계를 갱신할 _backpropagate 메서드를 구현하겠습니다.

def _backpropagate(self, path: List[Node], reward: float) -> None:
    """Update the statistics for the nodes in the path."""
    for node in reversed(path):
        self.visit_counts[node] += 1
        self.total_rewards[node] += reward
        reward = 1 - reward

select 메서드는 UCT 지표를 사용하여 자식 노드를 선택합니다.

    def _select_uct(self, node: Node) -> Node:
        """Select a child node using the UCT formula."""
        assert all(n in self.children for n in self.children[node])
        log_n_parent = math.log(self.visit_counts[node])

        def uct(n: Node) -> float:
            return self.total_rewards[n] / self.visit_counts[
                n
            ] + self.exploration_weight * math.sqrt(log_n_parent / self.visit_counts[n])

        return max(self.children[node], key=uct)

그리고 마지막으로 노드의 평균 보상을 바탕으로 노드의 점수를 계산할 _calculate_node_score 메서드를 구현하겠습니다.

def _calculate_node_score(self, nodeNode) -> float:
    """Calculate the score for a node based on its average reward."""
    if self.visit_counts[node] == 0:
        return float("-inf")
    return self.total_rewards[node] / self.visit_counts[node]

이제 이를 사용하려면 게임의 상태를 표현하는 데 사용할 TicTacToeNode 클래스를 정의해야 합니다.

class TicTacToeNode(Node):
    def __init__(self, state: str, player: str):
        self.state = state
        self.player = player

    def find_children(self) -> Set["TicTacToeNode"]:
        if self.is_terminal():
            return set()
        return {
            TicTacToeNode(
                self.state[:i] + self.player + self.state[i + 1 :],
                "O" if self.player == "X" else "X",
            )
            for i, value in enumerate(self.state)
            if value == " "
        }

    def get_random_child(self) -> Optional["TicTacToeNode"]:
        if self.is_terminal():
            return None
        empty_spots = [i for i, value in enumerate(self.state) if value == " "]
        index = random.choice(empty_spots)
        return TicTacToeNode(
            self.state[:index] + self.player + self.state[index + 1 :],
            "O" if self.player == "X" else "X",
        )

    def is_terminal(self) -> bool:
        return self.winner() is not None or " " not in self.state

    def reward(self) -> float:
        winner = self.winner()
        if winner is None:
            return 0.5  # Draw
        return 1.0 if winner == self.player else 0.0

    def __hash__(self) -> int:
        return hash(self.state)

    def __eq__(self, other: object) -> bool:
        return isinstance(other, TicTacToeNode) and self.state == other.state

    def winner(self) -> Optional[str]:
        lines = [
            (0, 1, 2),
            (3, 4, 5),
            (6, 7, 8),  # Rows
            (0, 3, 6),
            (1, 4, 7),
            (2, 5, 8),  # Columns
            (0, 4, 8),
            (2, 4, 6),  # Diagonals
        ]
        for line in lines:
            if self.state[line[0]] == self.state[line[1]] == self.state[line[2]] != " ":
                return self.state[line[0]]
        return None

이제 MCTS 클래스를 사용하여 틱택토 게임을 할 수 있습니다.

def play_game():
    state = " " * 9
    mcts = MCTS()
    board = TicTacToeNode(state, "X")

    print("Initial board:")
    print_board(board.state)

    while True:
        # Human player's turn (O)
        human_move = int(input("Enter your move (0-8): "))
        board = TicTacToeNode(
            board.state[:human_move] + "O" + board.state[human_move + 1 :], "X"
        )
        print("\nBoard after your move:")
        print_board(board.state)

        if board.is_terminal():
            break

        # AI player's turn (X)
        for _ in range(1000):  # Number of MCTS iterations
            mcts.simulate(board)
        board = mcts.select_best_move(board)
        print("\nBoard after AI move:")
        print_board(board.state)

        if board.is_terminal():
            break

    winner = board.winner()
    if winner:
        print(f"\nPlayer {winner} wins!")
    else:
        print("\nIt's a draw!")


def print_board(state: str):
    for i in range(0, 9, 3):
        print(" ".join(state[i : i + 3]))


if __name__ == "__main__":
    play_game()

원문 © Stephen Diehl, stephendiehl.com · 글 CC BY-SA 4.0, 코드 MIT · Stephen Diehl은 런던의 소프트웨어 엔지니어이자 작가입니다(저자가 정한 소개 문구). 이 글은 저자가 개인 자격으로 쓴 것이며 어떤 조직과의 연관도 밝히거나 암시해서는 안 됩니다 (라이선스와 고지).
이 페이지는 원문을 한국어로 옮기며 변경한 비공식 번역이며 같은 CC BY-SA 4.0으로 제공합니다 · yeokja 한국어 번역 · Anthropic 사의 claude-opus-5-5 모델을 활용하여 번역되었으며 학습을 모두 비허용한 상태로 작업하였습니다.