強化学習 AlphaZero 7 (三目並べ 原始モンテカルロ探索)

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

原始モンテカルロ探索はランダムシミュレーションによって状態価値を計算する手法です。
何回もランダムプレイを行い、勝率の高い手を価値が高いと評価します。

まず、三目並べを実装します。(前回のアルファベータ法で定義したものと同じです。)

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)))

原始モンテカルロ探索で行動選択する関数を定義します。
合法手ごとに10回プレイアウトした時の状態価値の合計の計算し、その合計が最も大きな行動を選択します。

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
# 原始モンテカルロ探索とランダム・アルファベータ法の対戦

# パラメータ
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))

# VSランダム
next_actions = (mcs_action, random_action)
evaluate_algorithm_of('VS_Random {:.3f}', next_actions)

# VSアルファベータ法
next_actions = (mcs_action, alpha_beta_action)
evaluate_algorithm_of('VS_AlphaBeta {:.3f}', next_actions)

結果は次の通りです。
結果
ランダムとの対戦では勝率93%と圧勝ですが、アルファベータ法との対戦では勝率23.5%となかなか厳しい結果となりました。

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

参考

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