ミニマックスアルゴリズムの紹介

Minimaxアルゴリズムの紹介

1. 概要

この記事では、ミニマックスアルゴリズムとそのAIへの応用について説明します。 これはゲーム理論のアルゴリズムなので、それを使用して簡単なゲームを実装します。

また、アルゴリズムを使用する利点について説明し、アルゴリズムを改善する方法を確認します。

2. 前書き

ミニマックスは意思決定アルゴリズム、typically used in a turn-based, two player gamesです。 アルゴリズムの目標は、最適な次の動きを見つけることです。

このアルゴリズムでは、1人のプレーヤーを最大化装置と呼び、他のプレーヤーを最小化装置と呼びます。 評価スコアをゲームボードに割り当てると、1人のプレーヤーが最大スコアのゲーム状態を選択しようとし、もう1人が最小スコアの状態を選択しようとします。

つまり、themaximizer works to get the highest score, while the minimizer tries get the lowest score by trying to counter movesです。

これは、zero-sum gameの概念に基づいています。 In a zero-sum game, the total utility score is divided among the players. An increase in one player’s score results into the decrease in another player’s score.したがって、合計スコアは常にゼロです。 1人のプレイヤーが勝つには、もう1人のプレイヤーが負けなければなりません。 そのようなゲームの例は、チェス、ポーカー、チェッカー、三目並べです。

興味深い事実 - 1997年、IBMのチェスをするコンピューターDeep Blue(ミニaxで構築)は、Garry Kasparov(チェスの世界チャンピオン)を破りました。

3. ミニマックスアルゴリズム

私たちの目標は、プレーヤーに最適な動きを見つけることです。 そのためには、最高の評価スコアを持つノードを選択するだけです。 プロセスをよりスマートにするために、潜在的な対戦相手の動きを先読みして評価することもできます。

各動きについて、計算能力が許す限り多くの動きを先読みできます。 アルゴリズムは、対戦相手が最適にプレーしていると仮定します。

技術的には、ルートノードから始めて、最適なノードを選択します。 評価スコアに基づいてノードを評価します。 この場合、評価関数はスコアを結果ノード(リーフ)にのみ割り当てることができます。 したがって、スコアを持つ葉に再帰的に到達し、スコアを逆伝播します。

以下のゲームツリーを検討してください。

image

Maximizerstarts with the root nodeで、最大スコアの移動を選択します。 残念ながら、リーフにのみ評価スコアがあるため、アルゴリズムは再帰的にリーフノードに到達する必要があります。 特定のゲームツリーでは、現在、最小化がchoose a move from the leaf nodesになっているため、スコアが最小のノード(ここではノード3と4)が選択されます。 ルートノードに到達するまで、同様に最適なノードを選択し続けます。

それでは、アルゴリズムのステップを正式に定義しましょう。

  1. 完全なゲームツリーを構築する

  2. 評価関数を使用して葉のスコアを評価する

  3. 選手の種類を考慮して、葉から根までのバックアップスコア:

    • 最大プレーヤーの場合、最大スコアの子を選択します

    • 最小プレーヤーの場合、最小スコアの子を選択します

  4. ルートノードで、最大値を持つノードを選択し、対応する移動を実行します

4. 実装

それでは、ゲームを実装しましょう。

ゲームには、heap with n number of bonesがあります。 両方のプレイヤーは順番にpick up 1,2 or 3 bonesする必要があります。 骨を取ることができないプレイヤーはゲームに負けます。 各プレイヤーは最適にプレイします。 nの値を前提として、AIを作成しましょう。

ゲームのルールを定義するために、GameOfBonesクラスを実装します。

class GameOfBones {
    static List getPossibleStates(int noOfBonesInHeap) {
        return IntStream.rangeClosed(1, 3).boxed()
          .map(i -> noOfBonesInHeap - i)
          .filter(newHeapCount -> newHeapCount >= 0)
          .collect(Collectors.toList());
    }
}

さらに、NodeクラスとTreeクラスの実装も必要です。

public class Node {
    int noOfBones;
    boolean isMaxPlayer;
    int score;
    List children;
    // setters and getters
}
public class Tree {
    Node root;
    // setters and getters
}

次に、アルゴリズムを実装します。 先を見越して最高の動きを見つけるにはゲームツリーが必要です。 それを実装しましょう:

public class MiniMax {
    Tree tree;

    public void constructTree(int noOfBones) {
        tree = new Tree();
        Node root = new Node(noOfBones, true);
        tree.setRoot(root);
        constructTree(root);
    }

    private void constructTree(Node parentNode) {
        List listofPossibleHeaps
          = GameOfBones.getPossibleStates(parentNode.getNoOfBones());
        boolean isChildMaxPlayer = !parentNode.isMaxPlayer();
        listofPossibleHeaps.forEach(n -> {
            Node newNode = new Node(n, isChildMaxPlayer);
            parentNode.addChild(newNode);
            if (newNode.getNoOfBones() > 0) {
                constructTree(newNode);
            }
        });
    }
}

次に、両方のプレーヤーに最適な動きを選択することにより、プレイアウトをシミュレートするcheckWinメソッドを実装します。 スコアを次のように設定します。

  • +1、マキシマイザーが勝った場合

  • -1、ミニマイザーが勝った場合

checkWinは、最初のプレーヤー(この場合はマキシマイザー)が勝った場合にtrueを返します。

public boolean checkWin() {
    Node root = tree.getRoot();
    checkWin(root);
    return root.getScore() == 1;
}

private void checkWin(Node node) {
    List children = node.getChildren();
    boolean isMaxPlayer = node.isMaxPlayer();
    children.forEach(child -> {
        if (child.getNoOfBones() == 0) {
            child.setScore(isMaxPlayer ? 1 : -1);
        } else {
            checkWin(child);
        }
    });
    Node bestChild = findBestChild(isMaxPlayer, children);
    node.setScore(bestChild.getScore());
}

ここで、findBestChildメソッドは、プレーヤーがマキシマイザーである場合に最大スコアを持つノードを見つけます。 それ以外の場合、最小スコアの子を返します。

private Node findBestChild(boolean isMaxPlayer, List children) {
    Comparator byScoreComparator = Comparator.comparing(Node::getScore);
    return children.stream()
      .max(isMaxPlayer ? byScoreComparator : byScoreComparator.reversed())
      .orElseThrow(NoSuchElementException::new);
}

最後に、n(ヒープ内のボーンの数)の値を使用してテストケースを実装しましょう。

@Test
public void givenMiniMax_whenCheckWin_thenComputeOptimal() {
    miniMax.constructTree(6);
    boolean result = miniMax.checkWin();

    assertTrue(result);

    miniMax.constructTree(8);
    result = miniMax.checkWin();

    assertFalse(result);
}

5. 改善

ほとんどの問題では、ゲームツリー全体を構築することはできません。 In practice, we can develop a partial tree (construct the tree till a predefined number of levels only)

次に、evaluation function,を実装する必要があります。これにより、プレーヤーにとって現在の状態がどの程度良好かを判断できるはずです。

完全なゲームツリーを構築しなくても、分岐係数の高いゲームの動きを計算するには時間がかかる場合があります。

ゲームツリーのFortunately, there is an option to find the optimal move, without exploring every node。 いくつかのルールに従うことでいくつかのブランチをスキップできますが、最終結果には影響しません。 This process is calledpruningAlpha–beta pruningは、ミニマックスアルゴリズムの一般的な変形です。

6. 結論

Minimaxアルゴリズムは、コンピューターボードゲームで最も人気のあるアルゴリズムの1つです。 ターンベースのゲームに広く適用されています。 プレイヤーがゲームに関する完全な情報を持っている場合、これは良い選択です。

分岐係数が非常に高いゲーム(たとえば、 GOのゲーム)。 それにもかかわらず、適切な実装を考えると、かなりスマートなAIになります。

いつものように、アルゴリズムの完全なコードはover on GitHubにあります。