強化学習 AlphaZero 8 (三目並べ モンテカルロ木探索)

三目並べをモンテカルロ木探索で解いていきます。

モンテカルロ木探索は、原始モンテカルロ探索をベースに「有望な手」をより深く調べることで改善を試みます。具体的には、選択・評価・展開・更新の4操作を行います。

まず、三目並べを実装します。(前回の原始モンテカルロ探索で定義したものと同じです。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# 三目並べの作成
import random

# ゲームの状態
class State:
# 初期化
def __init__(self, pieces=None, enemy_pieces=None):
# 石の配置
self.pieces = pieces if pieces != None else [0] * 9
self.enemy_pieces = enemy_pieces if enemy_pieces != None else [0] * 9

# 石の数の取得
def piece_count(self, pieces):
count = 0
for i in pieces:
if i == 1:
count += 1
return count

# 負けかどうか
def is_lose(self):
# 3並びかどうか
def is_comp(x, y, dx, dy):
for k in range(3):
if y < 0 or 2 < y or x < 0 or 2 < x or \
self.enemy_pieces[x+y*3] == 0:
return False
x, y = x+dx, y+dy
return True

# 負けかどうか
if is_comp(0, 0, 1, 1) or is_comp(0, 2, 1, -1):
return True
for i in range(3):
if is_comp(0, i, 1, 0) or is_comp(i, 0, 0, 1):
return True
return False

# 引き分けかどうか
def is_draw(self):
return self.piece_count(self.pieces) + self.piece_count(self.enemy_pieces) == 9

# ゲーム終了かどうか
def is_done(self):
return self.is_lose() or self.is_draw()

# 次の状態の取得
def next(self, action):
pieces = self.pieces.copy()
pieces[action] = 1
return State(self.enemy_pieces, pieces)

# 合法手のリストの取得
def legal_actions(self):
actions = []
for i in range(9):
if self.pieces[i] == 0 and self.enemy_pieces[i] == 0:
actions.append(i)
return actions

# 先手かどうか
def is_first_player(self):
return self.piece_count(self.pieces) == self.piece_count(self.enemy_pieces)

# 文字列表示
def __str__(self):
ox = ('o', 'x') if self.is_first_player() else ('x', 'o')
str = ''
for i in range(9):
if self.pieces[i] == 1:
str += ox[0]
elif self.enemy_pieces[i] == 1:
str += ox[1]
else:
str += '-'
if i % 3 == 2:
str += '\n'
return str

ランダムで行動を選択する関数を定義します。対戦相手用です。

1
2
3
4
# ランダムで行動選択
def random_action(state):
legal_actions = state.legal_actions()
return legal_actions[random.randint(0, len(legal_actions)-1)]

アルファベータ法で行動を選択する関数を定義します。対戦相手用です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# アルファベータ法で状態価値計算
def alpha_beta(state, alpha, beta):
# 負けは状態価値-1
if state.is_lose():
return -1

# 引き分けは状態価値0
if state.is_draw():
return 0

# 合法手の状態価値の計算
for action in state.legal_actions():
score = -alpha_beta(state.next(action), -beta, -alpha)
if score > alpha:
alpha = score

# 現ノードのベストスコアが親ノードを超えたら探索終了
if alpha >= beta:
return alpha

# 合法手の状態価値の最大値を返す
return alpha

# アルファベータ法で行動選択
def alpha_beta_action(state):
# 合法手の状態価値の計算
best_action = 0
alpha = -float('inf')
for action in state.legal_actions():
score = -alpha_beta(state.next(action), -float('inf'), -alpha)
if score > alpha:
best_action = action
alpha = score

# 合法手の状態価値の最大値を持つ行動を返す
return best_action

プレイアウト関数を定義します。

1
2
3
4
5
6
7
8
9
10
11
12
# プレイアウト
def playout(state):
# 負けは状態価値-1
if state.is_lose():
return -1

# 引き分けは状態価値0
if state.is_draw():
return 0

# 次の状態の状態価値
return -playout(state.next(random_action(state)))

原始モンテカルロ探索で行動を選択する関数を定義します。対戦相手用です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 原始モンテカルロ探索で行動選択
def mcs_action(state):
# 合法手ごとに10回プレイアウトした時の状態価値の合計の計算
legal_actions = state.legal_actions()
values = [0] * len(legal_actions)
for i, action in enumerate(legal_actions):
for _ in range(10):
values[i] += -playout(state.next(action))

# 合法手の状態価値の合計の最大値を持つ行動を返す
return legal_actions[argmax(values)]

# 最大値のインデックスを返す
def argmax(collection, key=None):
return collection.index(max(collection))

モンテカルロ木探索で行動を選択する関数を定義します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import math

# モンテカルロ木探索の行動選択
def mcts_action(state):
# モンテカルロ木探索のノードの定義
class Node:
# ノードの初期化
def __init__(self, state):
self.state = state # 状態
self.w = 0 # 累計価値
self.n = 0 # 試行回数
self.child_nodes = None # 子ノード群

# 局面の価値の計算
def evaluate(self):
# ゲーム終了時
if self.state.is_done():
# 勝敗結果で価値を取得
value = -1 if self.state.is_lose() else 0 # 負けは-1、引き分けは0

# 累計価値と試行回数の更新
self.w += value
self.n += 1
return value

# 子ノードが存在しない時
if not self.child_nodes:
# プレイアウトで価値を取得
value = playout(self.state)

# 累計価値と試行回数の更新
self.w += value
self.n += 1

# 子ノードの展開
if self.n == 10:
self.expand()
return value

# 子ノードが存在する時
else:
# UCB1が最大の子ノードの評価で価値を取得
value = -self.next_child_node().evaluate()

# 累計価値と試行回数の更新
self.w += value
self.n += 1
return value

# 子ノードの展開
def expand(self):
legal_actions = self.state.legal_actions()
self.child_nodes = []
for action in legal_actions:
self.child_nodes.append(Node(self.state.next(action)))

# UCB1が最大の子ノードの取得
def next_child_node(self):
# 試行回数が0の子ノードを返す
for child_node in self.child_nodes:
if child_node.n == 0:
return child_node

# UCB1の計算
t = 0
for c in self.child_nodes:
t += c.n
ucb1_values = []
for child_node in self.child_nodes:
ucb1_values.append(-child_node.w/child_node.n+(2*math.log(t)/child_node.n)**0.5)

# UCB1が最大の子ノードを返す
return self.child_nodes[argmax(ucb1_values)]

# 現在の局面のノードの作成
root_node = Node(state)
root_node.expand()

# 100回のシミュレーションを実行
for _ in range(100):
root_node.evaluate()

# 試行回数の最大値を持つ行動を返す
legal_actions = state.legal_actions()
n_list = []
for c in root_node.child_nodes:
n_list.append(c.n)
return legal_actions[argmax(n_list)]

モンテカルロ木探索とランダムおよびアルファベータ法、原始モンテカルロ探索の対戦を行います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# モンテカルロ木探索とランダムおよびアルファベータ法、原始モンテカルロ探索の対戦

# パラメータ
EP_GAME_COUNT = 100 # 1評価あたりのゲーム数

# 先手プレイヤーのポイント
def first_player_point(ended_state):
# 1:先手勝利, 0:先手敗北, 0.5:引き分け
if ended_state.is_lose():
return 0 if ended_state.is_first_player() else 1
return 0.5

# 1ゲームの実行
def play(next_actions):
# 状態の生成
state = State()

# ゲーム終了までループ
while True:
# ゲーム終了時
if state.is_done():
break

# 行動の取得
next_action = next_actions[0] if state.is_first_player() else next_actions[1]
action = next_action(state)

# 次の状態の取得
state = state.next(action)

# 先手プレイヤーのポイントを返す
return first_player_point(state)

# 任意のアルゴリズムの評価
def evaluate_algorithm_of(label, next_actions):
# 複数回の対戦を繰り返す
total_point = 0
for i in range(EP_GAME_COUNT):
# 1ゲームの実行
if i % 2 == 0:
total_point += play(next_actions)
else:
total_point += 1 - play(list(reversed(next_actions)))

# 出力
print('\rEvaluate {}/{}'.format(i + 1, EP_GAME_COUNT), end='')
print('')

# 平均ポイントの計算
average_point = total_point / EP_GAME_COUNT
print(label.format(average_point * 100))

# VSランダム
next_actions = (mcts_action, random_action)
evaluate_algorithm_of('モンテカルロ木探索 VS ランダム {:.2f}%\n', next_actions)

# VSアルファベータ法
next_actions = (mcts_action, alpha_beta_action)
evaluate_algorithm_of('モンテカルロ木探索 VS アルファベータ法 {:.2f}%\n', next_actions)

# VSモンテカルロ法
next_actions = (mcts_action, mcs_action)
evaluate_algorithm_of('モンテカルロ木探索 VS 原始モンテカルロ探索 {:.2f}%\n', next_actions)

結果は次の通りです。
結果
ランダムとの対戦では勝率93%と圧勝ですが、アルファベータ法との対戦では勝率36.5%となり原始モンテカルロ探索よりは勝率が10%以上あがりました。

モンテカルロ木探索と原始モンテカルロ探索の対戦では67.00%と原始モンテカルロ探索よりモンテカルロ木探索がやや強い結果となりました。

(Google Colaboratoryで動作確認しています。)

参考

AlphaZero 深層学習・強化学習・探索 人工知能プログラミング実践入門 サポートページ