web-dev-qa-db-ja.com

Java Minimax Alpha-Beta Pruning Recursion Return

Javaのチェッカーゲームのアルファベータプルーニングでミニマックスを実装しようとしています。私のミニマックスアルゴリズムは完全に機能します。私のコードは、アルファベータコードを配置して実行されます。残念ながら、標準のミニマックスアルゴリズムに対して1000ゲームをプレイすると、アルファベータアルゴリズムは常に50ゲーム程度遅れます。

アルファベータ法は動きの質を低下させるべきではないので、それらを達成するのにかかる時間だけで、何かが間違っている必要があります。ただし、ペンと紙を取り出し、仮想のリーフノード値を描画し、アルゴリズムを使用して、正しい最良の動きを計算するかどうかを予測しました。論理エラーはないようです。このビデオのツリーを使用しました: アルファベータプルーニング アルゴリズムをトレースします。論理的にはすべて同じ選択を行う必要があるため、機能する実装である必要があります。

また、コードにprintステートメントを入れました(混乱を減らすために削除されました)。値は正しく返され、表示され、プルーニングが行われます。最善の努力にもかかわらず、論理エラーがどこにあるのかを見つけることができませんでした。これは、これを実装するための3回目の試みであり、すべて同じ問題が発生しています。

ここに完全なコードを投稿することはできません。長すぎるため、エラーに関連するメソッドを含めました。確かではありませんが、問題は非再帰的なmove()メソッドにある可能性が高いと思われますが、論理エラーを見つけることができないので、もっとスラッシングして、おそらく物事を作っているでしょう韻や理由がなくても、良くなるよりも悪くなる。

forループの再帰呼び出しから複数の整数値を回復するトリックはありますか?ミニマックスとネガマックスの両方の実装で正常に動作しますが、アルファベータ剪定はいくつかの奇妙な結果を生み出すようです。

@Override
public GameState move(GameState state) 
{
    int alpha = -INFINITY;
    int beta = INFINITY;
    int bestScore = -Integer.MAX_VALUE;
    GameTreeNode gameTreeRoot = new GameTreeNode(state);
    GameState bestMove = null;
    for(GameTreeNode child: gameTreeRoot.getChildren())
    {
        if(bestMove == null)
        {
            bestMove = child.getState();
        }
        alpha = Math.max(alpha, miniMax(child, plyDepth - 1, alpha, beta));
        if(alpha > bestScore)
        {
            bestMove = child.getState();
            bestScore = alpha;
        }
    }
    return bestMove;
}

private int miniMax(GameTreeNode currentNode, int depth, int alpha, int beta) 
{
    if(depth <= 0 || terminalNode(currentNode.getState())) 
    {
        return getHeuristic(currentNode.getState());
    }
    if(currentNode.getState().getCurrentPlayer().equals(selfColor))
    {
        for(GameTreeNode child: currentNode.getChildren())
        {
            alpha = Math.max(alpha, miniMax(child, depth - 1, alpha, beta));

            if(alpha >= beta)
            {
                return beta;
            }
        }
        return alpha;
    }
    else
    {
        for(GameTreeNode child: currentNode.getChildren())
        {
            beta = Math.min(beta, miniMax(child, depth - 1, alpha, beta));

            if(alpha >= beta)
            {
                return alpha;
            }
        }
        return beta;
    }
}
//Checks to see if the node is terminal
private boolean terminalNode(GameState state)
{
if(state.getStatus().equals(win) || state.getStatus().equals(lose) || state.getStatus().equals(draw))
    {
        return true;
    }
    else
    {
        return false;
    }
}
18
sage88

あなたはすでにあなたの問題を修正しました、しかしあなたが遭遇した問題はかなり一般的です。したがって、AIエージェントのアルゴリズムの一部を構築するときはいつでも、それを適切にテストする必要があります。したがって、ミニマックスアルゴリズムが正しければ、ランダムなツリーを多数生成して、結果が同じかどうかを確認できます。たとえば、pythonでは、次の方法でこれを行うことができます。

class Node():
    def __init__(self, data, children):
        self.data = data
        self.children = children

def generateTree(depth, branching):
    total = branching**depth
    values = [randint(-100, 100) for _ in xrange(total)]
    level = [Node(values[i], []) for i in xrange(total)]

    for _ in xrange(depth):
        total /= branching
        level = [Node(None, level[i * branching: (i+1) * branching]) for i in xrange(total)]

    return level[0], values

これで、ランダムなツリーが多数あるツリーを生成し、結果を比較できます。

tree, values = generateTree(depth, branching)
print negamax(tree, depth, 1) == alpha_beta_negamax(tree, depth, float('-inf'), float('inf'), 1)

ミニマックスとアルファベータは最高の値を返すことを忘れないでください。実際のゲームに興味があるのは動きです。ムーブを返すことができるようにそれらを変更するのは簡単ですが、これはムーブがどのように返されるかを決定するのは開発者次第です。これは、最良の解決策につながる多くの動きが存在する可能性があるためです(最初の動き、最後の動き、または最も一般的な動きは、すべての動きを見つけてランダムな動きを返すことです)。

あなたの場合、問題は戻り値のランダム性にあったので、テスト中の良いアプローチはランダム性を修正することです。

0
Salvador Dali

問題を見つけたとおっしゃっていましたが、ミニマックスアルファベータ法はすべきではありません。

if it is MAX's turn to move
  for child in children
     result = alphaBetaMinimax(child, alpha, beta)
     if result > alpha
        alpha = result
        if node is root
           bestMove = operator of child
     if alpha >= beta
        return alpha
  return alpha

if it is MIN's turn to move
  for child in children
     result = alphaBetaMinimax(child, alpha, beta)
     if result < beta
        beta = result
        if node is root
           bestMove = operator of child
     if beta <= alpha
        return beta
  return beta

あなたが書いた:

  if alpha >= beta
    return beta
return alpha
2
Adrian

2013年3月16日、sage88は次のように質問しました。

forループの再帰呼び出しから複数の整数値を回復するトリックはありますか?ミニマックスとネガマックスの両方の実装で正常に機能しますが、アルファベータ法は奇妙な結果をもたらすようです。

アルファベータプルーニングでは、対象となる出力値はノードのスコアのみです。最小ノードのベータの最終値は、その親の最大ノードのアルファ値として考慮されます。同様に、最大ノードのアルファの最終値は、その親最小ノードのベータ値と見なされます。したがって:

最も関連性の高いトリックであるため、あなたの質問に対する答えはアルゴリズム自体です。

とはいえ、実装には2つのエラーがあります。1)Adrian Blackburnが最初に指摘したように、最小ノードから誤ってアルファを返し、その逆も同様であるため、精度が歪められます。 2)現在のノードの値で親のアルファまたはベータを時期尚早に考慮することにより、剪定の機会をあきらめています。このバージョンでは、戻り値が修正され、プルーニングが最大化されます。

_private int miniMax(GameTreeNode currentNode, int depth, int alpha, int beta) {
    if (depth <= 0 || terminalNode(currentNode.getState())) {
        return getHeuristic(currentNode.getState());
    }
    if (currentNode.getState().getCurrentPlayer().equals(selfColor)) {
        int currentAlpha = -INFINITY;
        for (GameTreeNode child : currentNode.getChildren()) {
            currentAlpha = Math.max(currentAlpha, miniMax(child, depth - 1, alpha, beta));
            alpha = Math.max(alpha, currentAlpha);
            if (alpha >= beta) {
                return alpha;
            }
        }
        return currentAlpha;
    }
    int currentBeta = INFINITY;
    for (GameTreeNode child : currentNode.getChildren()) {
        currentBeta = Math.min(currentBeta, miniMax(child, depth - 1, alpha, beta));
        beta = Math.min(beta, currentBeta);
        if (beta <= alpha) {
            return beta;
        }
    }
    return currentBeta;
}
_

楽しくて面白い質問を投稿してくれてありがとう:)

さらに楽しくするために、move()メソッドを明確にして、Math.max()への冗長な呼び出しを削除します。

_@Override
public GameState move(GameState state) {
    GameState bestMove = null;
    int bestScore = -INFINITY;
    GameTreeNode gameTreeRoot = new GameTreeNode(state);
    for (GameTreeNode child : gameTreeRoot.getChildren()) {
        int alpha = miniMax(child, plyDepth - 1, bestScore, INFINITY);
        if (alpha > bestScore || bestMove == null) {
            bestMove = child.getState();
            bestScore = alpha;
        }
    }
    return bestMove;
}
_

最後に(さらに楽しい)、単なる提案であり、terminalNode()の意図を明確にするためにメソッド名を変更しますが、これをGameStateに移動して、パラメーターなしで呼び出すことができるようにします。

_private boolean isTerminal(GameState state) {
    //return Is.any(state.getStatus(), win, lose, draw);
    return state.getStatus().equals(win)
        || state.getStatus().equals(lose)
        || state.getStatus().equals(draw);
}
_
1
gknicker

あなたの質問に答えるだけ

Forループの再帰呼び出しから複数の整数値を回復するためのトリックはありますか?

はい、Javaでは、オブジェクトを再帰関数呼び出しに渡し、そのオブジェクトの内容を変更する必要があります。関数が戻った後、変更された値にアクセスできるようになります。

例えば。

class ToBeReturned {
    int returnValue1;
    int returnValue2;
    int returnValue3;
}
1
DanLatimer

賭けの剪定結果を達成するには、ある種の移動順序を実装する必要があります。チェスでは、通常、キャプチャまたはチェックします。このような動きは評価を最も変える傾向があるため、剪定に大きな影響を与えます。チェッカーでは、対戦相手の石を奪ったり、8位のセルフストーンを宣伝したりする可能性があります(使用されている用語がわかりません)。

0
Ales Dolecek