情報論理工学 研究室 第8回: ミニマックス法
「強い手」の選択 「強い手」の選択 強い手とは? ゲームによって異なる 大きな得点が得られる手 相手の得点を下げる手 価値の高い駒を取る手 価値の高い駒を守る手 有利な地点を取る手 相手に不利な地点を取らせる手 有利な選択ができるようになる手 相手に不利な選択を強要する手 ゲームによって異なる
「強い手」を得る手法 「強い手」を得る手法 局面の評価値計算 定跡・定石データベースの利用 先読み 完全読み切り・必勝読み切り モンテカルロ法
局面の評価値計算 局面の評価値計算 現在の局面からどのくらい優勢かを計算する 得点を多く取っている 盤上に強い駒がある 強いカードを持っている 有利な地点を抑えている 相手を攻撃できる 相手の攻撃を防げる 可能な手の数が多い :
評価値計算による手の選択 各合法手に対する1手先の局面を生成 各局面の評価値を計算 最も評価値の高い手を選択 +5 -2 +7 -5 +1
ArrayList<Move> moveList = generateMoveList (); // 合法手リスト生成 /* 最も評価値の高い手を選ぶ */ Move SelectMove () { ArrayList<Move> moveList = generateMoveList (); // 合法手リスト生成 if (moveList.isEmpty()) return null; // 合法手無し int selectedValue = -∞; // 局面の評価値 Move selectedMove = null; // 選択した手 for (Move move : moveList) { Phase phase = nextPhase (move); // 1手先の局面を生成 int value = phase.getValue(); // 局面の評価値を求める if (value > selectedValue) { // 評価値最大の手を記憶 selectedMove = move; selectedValue = value; } return move;
駒割による評価値:将棋 9 8 7 6 5 4 3 2 1 八 七 六 五 四 三 二 一 九 先手:101点 後手:85点 ▲8六銀まで 歩 ▲8六銀 歩 香 桂 金 玉 桂 香 角 飛 金 先手:101点 後手:85点 (先手の銀得) 歩 歩 歩 歩 銀 歩 歩 歩 飛車で 只取り できる? 歩 8六の銀が 8二の飛車で 取れそう 銀 歩 銀 歩 歩 歩 歩 歩 歩 角 金 飛 銀 香 桂 玉 金 桂 香 歩 ▲8六銀まで
駒割による評価値:将棋 9 8 7 6 5 4 3 2 1 八 七 六 五 四 三 二 一 九 先手:93点 後手:93点 △8六同飛まで 歩 ▲8六銀 △同飛 歩 香 桂 金 玉 桂 香 銀 金 先手:93点 後手:93点 (互いに駒損無し) 角 歩 歩 歩 歩 銀 歩 歩 歩 歩 飛 歩 銀 歩 歩 しかし… 歩 歩 歩 歩 角 金 飛 銀 香 桂 玉 金 桂 香 歩 △8六同飛まで
駒割による評価値:将棋 ▲9五角が 王手飛車! 9 8 7 6 5 4 3 2 1 八 七 六 五 四 三 二 一 九 先手:93点 歩 ▲8六銀 △同飛 ▲9五角 歩 香 桂 金 玉 桂 香 銀 金 先手:93点 後手:93点 (互いに駒損無し) 角 歩 歩 歩 歩 銀 歩 歩 歩 角 歩 ▲9五角が 王手飛車! 飛 歩 銀 歩 歩 歩 歩 歩 歩 金 飛 銀 香 桂 玉 金 桂 香 歩 ▲9五角まで
先読み 現在の局面は優勢でも 数手先に逆転される場合がある 局面の先読みが必要 ○ 優勢のようだ しかし… 数手先 × 劣勢!
先読み 先読み 数手先の局面を生成し、評価値を計算 ○番 ×番 ○番 +2 +2 -6 +1 -4 +5 +2 +3 -4 +5 -2 -6 この局面の 評価値を求める -4 +5 -2 -6 +2 -2 -1 +3
ミニマックス(mini-max)法 ミニマックス法 自分にとっての最善手=相手にとっての最悪手 ⇒ 相手が常に最善手を指してくると仮定 ⇒ 相手が常に最善手を指してくると仮定 自分の手番:最も評価値の高い手を採用 相手の手番:最も評価値の低い手を採用
各頂点の得点計算 ○番:最大値を選択 ×番:最少値を選択 ○番 ×番 +3 -5 +1 +3 -4 -2 -2 -5 +5 +1
int maxScore (int depth) { if (depth == 0) // 深さ制限に達した場合 /* 局面の評価値を計算する(自分の手番) */ /* depth 先まで読んで最も高い評価値を返す */ int maxScore (int depth) { if (depth == 0) // 深さ制限に達した場合 return value; // 先読み無しの評価値を返す int scoreMax = -∞; ArrayList<Move> moveList = generateMoves(); // 合法手リスト生成 for (Move move : moveList ) { // 全ての合法手に対して判定 Phase phase = nextPhase (move); // 次の局面を生成 int score = phase.minScore (depth -1); // 次局面(相手番)の評価値計算 if (score > scoreMax) // 自分にとって良い手が見つかった場合 scoreMax = score; // 評価値更新 } return scoreMax; // 最も高い評価値を返す
int minScore (int depth) { if (depth == 0) // 深さ制限に達した場合 /* 局面の評価値を計算する(相手の手番) */ /* depth 先まで読んで最も低い評価値を返す */ int minScore (int depth) { if (depth == 0) // 深さ制限に達した場合 return value; // 先読み無しの評価値を返す int scoreMin = +∞; ArrayList<Move> moveList = generateMoves(); // 合法手リスト生成 for (Move move : moveList ) { // 全ての合法手に対して判定 Phase phase = nextPhase (move); // 次の局面を生成 int score = phase.maxScore (depth -1); // 次局面(自番)の評価値計算 if (score < scoreMin) // 相手にとって良い手が見つかった場合 scoreMin = score; // 評価値更新 } return scoreMin; // 最も低い評価値を返す
先読み手数が増えると可能な局面数は指数的に増える 各局面での合法手数 2 4 8 先 読 み 手 数 10 1,000 1,000,000 109 20 1012 1018 30 1027 40 1024 1036 50 1015 1030 1045 先読み手数が増えると可能な局面数は指数的に増える ⇒適当な枝刈りが必要
ミニマックス法 無駄な探索はどれ? 最も評価値の 高い手を採用 -3 -3 -8 -5 -6 +9 -3 +6 +2 -6 +4 -8 -2 +3 +7 +5 -2 -6 最も評価値の 低い手を採用 無駄な探索はどれ?
-3未満の評価値が出た枝は探索しなくていい ミニマックス法 -3以上確定 ⇒-3未満の手は 絶対に採用されない >-3 -6以下確定 -3 <-6 <-5 <-6 +9 -3 +6 +2 -6 +4 -5 +5 -2 -6 この部分の探索は不要 -3未満の評価値が出た枝は探索しなくていい
アルファベータ(alpha-beta)法 アルファベータ法 ミニマックス法の改良アルゴリズム 必要の無い探索は行わない 絶対に採用されない手は読まない α:それまでに発見した自番で最も大きな評価値 β:それまでに発見した相手番で最も小さい評価値 相手の手番:αよりも小さい評価値になれば探索打ち切り 自分の手番:βよりも大きい評価値になれば探索打ち切り
/* αβ法により局面の評価値を計算する(自分の手番) */ /* depth 先まで読んで最も高い評価値を返す */ int maxScore (int depth, int alpha, int beta) { if (depth == 0) // 深さ制限に達した場合 return value; // 先読み無しの評価値を返す int scoreMax = -∞; ArrayList<Move> moveList = generateMoves(); // 合法手リスト生成 for (Move move : moveList ) { // 全ての合法手に対して判定 Phase phase = nextPhase (move); // 次の局面を生成 int score = phase.minScore (depth -1, alpha, beta); // 次局面の評価値計算 if (score≧beta) // β値を上回ったら探索中止 return score; // この値は採用されることは無い if (score > scoreMax) // 自分にとって良い手が見つかった場合 scoreMax = score; // 評価値更新 if (scoreMax > alpha) alpha = scoreMax; // α値更新 } return scoreMax; // 最も高い評価値を返す
/* αβ法により局面の評価値を計算する(相手の手番) */ /* depth 先まで読んで最も低い評価値を返す */ int minScore (int depth, int alpha, int beta) { if (depth == 0) // 深さ制限に達した場合 return value; // 先読み無しの評価値を返す int scoreMin = +∞; ArrayList<Move> moveList = generateMoves(); // 合法手リスト生成 for (Move move : moveList ) { // 全ての合法手に対して判定 Phase phase = nextPhase (move); // 次の局面を生成 int score = phase.maxScore (depth -1, alpha, beta); // 次局面の評価値計算 if (score≦alpha) // α値を下回ったら探索中止 return score; // この値は採用されることは無い if (score > scoreMin) // 相手にとって良い手が見つかった場合 scoreMin = score; // 評価値更新 if (scoreMin < beta) beta = scoreMin; // β値更新 } return scoreMin; // 最も低い評価値を返す
アルファベータ法の効率 枝刈りできるのはどこ? +5 +5 +3 -1 -4 +5 +6 +8 +9 +3 +4 +6 +7 -1 +3 +3 +7 -4 -2 -1 +5 枝刈りできるのはどこ?
アルファベータ法 枝刈り可能 +5 α値 +5 +5 +6 +8 +9 +3 -1 -4 左から探索 <+3 <-1 <-4 +5 +6 +8 +9 +3 -1 -4 左から探索 枝刈り可能
アルファベータ法 全く枝刈りできない! +5 α値 α値 α値 +5 +3 -1 -4 +5 +6 +8 +9 +3 +4 +6 +7 -1 +3 +7 -4 -2 -1 +5 右から探索 全く枝刈りできない!
アルファベータ法の効率 アルファベータ法の効率 良い手から先に探索すると効率がいい +5 +6 +8 +9 +3 +4 +7 -1 -4 -4 -2 良い手 悪い手 良い手 悪い手 効率高 効率低
そもそも「良い手」がわかるなら探索の必要無し アルファベータ法の効率 アルファベータ法の効率 良い手から先に探索すると効率がいい でもどうやって「良い手」を探す? そもそも「良い手」がわかるなら探索の必要無し 「良さそうな手」から先に探索
効率のいい探索 まず予備探索を行う ミニマックス法(先読み数:小)で探索 評価値順に手をソートする アルファベータ法(先読み数:大)で探索 探索が2回必要だが、 枝刈りできるメリットの方が大きい
ネガマックス(negamax)法 ネガマックス法 関数maxScore()とminScore()を一つにまとめる この2つはほぼ同じ関数(最大値か最小値かの違い) ⇒評価値の符号を反転させれば同一 自分の手番:評価値そのまま使用 相手の手番:評価値*-1を使用 ⇒手番に関係なく評価値が最大の手を探せばいい
/* ネガマックス法により局面の評価値を計算する */ /* depth 先まで読んで最も高い評価値を返す */ int negaMaxScore (int depth, int alpha, int beta) { if (depth == 0) // 深さ制限に達した場合 return value; // 先読み無しの評価値を返す int scoreMax = -∞; ArrayList<Move> moveList = generateMoves(); // 合法手リスト生成 for (Move move : moveList ) { // 全ての合法手に対して判定 Phase phase = nextPhase (move); // 次の局面を生成 int score = -phase.negaMaxScore (depth -1, -beta, -alpha); // 次局面 if (score≧beta) // β値を上回ったら探索中止 return score; // この値は採用されることは無い if (score > scoreMax) // 良い手が見つかった場合 scoreMax = score; // 評価値更新 if (scoreMax > alpha) alpha = scoreMax; // α値更新 } return scoreMax; // 最も高い評価値を返す
ネガマックス法 int negaMaxScore (int depth, int alpha, int beta) int score = -phase.negaMaxScore (depth-1, -beta, -alpha) 相手番の評価値計算 符号を入れ替え、α:=-β、β:=-αとして計算する 評価値 v が α < v < β ⇔評価値 –v が –β < -v < -α
同一局面の処理 探索中同一局面が現れるケース 手順前後の同一局面 千日手 手順が違っても同一局面となる ⇒局面の評価値を再利用できる 手順中で同一局面が現れる(千日手) ⇒探索の無限ループを回避する必要がある
手順前後の同一局面 手順1 1:f5, 2:d6, 3:c3, 4:d3, 5:c4 手順2 8 7 6 5 4 3 2 1 a b c d e f g h ● 手順1 1:f5, 2:d6, 3:c3, 4:d3, 5:c4 手順2 1:f5, 2:d6, 3:c4, 4:d3, 5:c3 ● ● どちらも同じ局面になる ● ●
手順前後の同一局面 3:c3 3:c4 4:d3 4:d3 5:c4 5:c3 評価値をコピー +5 +5 同一局面
千日手による同一局面 a b c d 5 6 7 8 a b c d 5 6 7 8 a b c d 5 6 7 8 a b c d 5 6 Qd8+ ... Kb8 ... Ka7 a b c d 5 6 7 8 a b c d 5 6 7 8 Qa4+
千日手による同一局面 同一局面 Qd8+ ... Ka7 Qa4+ ... Kb8 千日手なので判定打ち切り
局面の同一判定 equals()メソッド 同一の局面か判定する boolean equals (Phase phase) { for (int i=0; i<SIZE; ++i) for (int j=0; j<SIZE; ++j) if (this.board[i][j] ≠ phase.board[i][j]) return false; // 1箇所でも異なればfalse if (this.turn ≠phase.turn) return false; : return true; // 全て同じならtrue } だがこの判定は時間がかかる
局面の同一判定 探索中には多くの局面が現れる 局面の同一判定は時間がかかる 同一の可能性のある局面を絞り込む ハッシュ関数による同一判定 探索中には多くの局面が現れる 局面の同一判定は時間がかかる 同一の可能性のある局面を絞り込む ハッシュ関数による同一判定 ハッシュ関数で局面を数値化、 同一のハッシュ値を持つ局面のみ同一判定
※実際はもっと複雑なハッシュ関数を用いる ハッシュ関数の例:チェス a b c d e f g h 1 2 3 4 5 6 7 8 駒がある:1 駒が無い:0 として64ビットの 数値で表現 11011111 11100101 00010010 00001000 00101000 0010000 11111111 10111001 DFE51208 2820F7B9 ※実際はもっと複雑なハッシュ関数を用いる
同一局面の判定 No Yes No Yes No Yes 同一のハッシュ値の 局面があるか? 同一局面か? 同一局面無し 探索木上で祖先か? 優れたハッシュ関数なら この判定は不要 Yes 探索木上で祖先か? No 千日手 Yes 評価値をコピー
宿題:3目並べの着手選択 3目並べ着手選択 先手後手それぞれを人間かCPUのどちらが受け持つかを選べるようにせよ 探索の深さは適当な値に設定する 先読み無しの評価値も適当と思われる方法を採る