探索木がuniformであるとは、すべての中間ノードの分岐数が一定の数wであり、すべての端末ノードが同じ高さdにあることという。探索木がuniformであるとすると、MinMax探索はすべてのパスを探索することから調べる端末ノードの数はwd個だけあるのに対して、αβ探索では最善の場合、wfloor(d/2)+wceil(d/2)-1個となる[1]。
つまり、αβ探索を効率良く実行するためには、どの手から探索をするかが重要となり、これをmove orderingとよぶ。
move orderingを改善するための一つの手法としてkiller heuristicという手法がある。これは、探索木のある場所で枝刈りが行われたときに、枝刈りをおこした"killing" moveを、次に同じ深さのノードに到達した際に、その手が可能であれば先に調べるというものである。
NegaAlphaを元にした、プログラムは次のとおり、
trait KillerHeuristic[N <: Node[N]] { val numKillerMoves: Int var killerMoves: Array[List[Move]] = _ def initKillerMoves(maxDepth: Int) { killerMoves = Array.fill(maxDepth) { List.empty } } def reorderByKillerMoves(i: Int, moves: List[Move]): List[Move] = { val km = killerMoves(i) var is = km intersect moves if (!is.isEmpty) is ++ (moves diff is) else moves } def recordKillerMove(i: Int, move: Move) { val km = killerMoves(i) if (km contains move) { killerMoves(i) = move :: (km filterNot (_ == move)) } else { killerMoves(i) = (move :: km) take numKillerMoves } } } abstract class KillerHeuristicPlayer[N <: Node[N]](val maxDepth: Int, override val numKillerMoves: Int) extends Player[N] with KillerHeuristic[N] { override def play(ply: Int, node: N, last: Move): Move = { initKillerMoves(maxDepth) val (m, s) = play(node, Int.MinValue + 1, Int.MaxValue, maxDepth) Log.i("killer_moves", numKillerMoves.toString + "," + (killerMoves map { _.length }).mkString(",")) m } def play(node: N, alpha: Int, beta: Int, depth: Int): (Move, Int) = { if (depth == 0 || node.isTerminal) { return (Move.empty, score(node)) } var moves = node.possibleMoves().toList // reorder by killer moves moves = reorderByKillerMoves(depth - 1, moves) // nega-alpha search var bestMove = Move.empty var alpha_ = alpha for (m <- moves) { val n = node.play(m).get val (_, s) = play(n, -beta, -alpha_, depth - 1) if (-s >= beta) { // beta cut // record the killer move recordKillerMove(depth - 1, m) return (m, beta) } if (-s > alpha_) { bestMove = m alpha_ = -s } } (bestMove, alpha_) } def score(node: N): Int }
深さ6の探索で、保持するkiller move数を変化させて実験した結果は次の通り。αβ探索の結果も比較のためにプロットしている。
killer move数は1つだけのときは、少し悪いが他はほぼ同程度である。
また、1試合あたりの総ノード数の平均を表にすると次のようになる。最も小さなkiller move数が32のときで、αβ探索に対して48.4%の改善となっている。
1 128 16 2 32 4 112789.62 98864.71 97492.46 108800.69 93997.23 94617.41 64 8 alpha beta 96379.85 98554.55 182336.39
ここで、32のときに最小となっているが、depthが深くなるにつれてkiller move数は増える傾向にあり、depth==6のときに最大30~40個のkiller moveが保持されていた。
killer moveの処理の分、探索が遅くなるが、時間でみてもほぼ同じく33.4%の改善となっている。interior nodeでの重い処理以上に、枝刈りの効果のほうが大きいことが分かる。
1 128 16 2 32 4 16365.91 15681.85 15086.16 16000.39 15284.74 14969.75 64 8 alpha beta 15488.62 15192.91 22964.40
[1] Knuth, D. E., and Moore, R. W., "An Analysis of Alpha-Beta Pruning". Artificial Intelligence Vol. 6, No. 4, pp. 293–326 (1975).
[2] Akl, S.G., Newborn, M.M., "The principle continuation and the killer heuristic". In Proceedings of the ACM Annual Conference, pp. 82–118 (1977).
0 件のコメント:
コメントを投稿