目次と前回の記事
Python のバージョンとこれまでに作成したモジュール
本記事のプログラムは Python の バージョン 3.13 で実行しています。
以下のリンクから、これまでに作成したモジュールを見ることができます。
リンク | 説明 |
---|---|
marubatsu.py | Marubatsu、Marubatsu_GUI クラスの定義 |
ai.py | AI に関する関数 |
mbtest.py | テストに関する関数 |
util.py | ユーティリティ関数の定義 |
tree.py | ゲーム木に関する Node、Mbtree クラスなどの定義 |
gui.py | GUI に関する処理を行う基底クラスとなる GUI クラスの定義 |
AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。
ベンチマークの設定
前回の記事の最後で今までとは異なるデータ構造でゲーム盤を表現するクラスを定義すると述べましたが、その前にまだいくつかの作業と説明を行う必要があることがわかりましたので、今までと異なるデータ構造の紹介については延期します。
ゲーム盤を表す データ構造の違いによる処理速度を比較 するためには、適切なベンチマークを設定 する必要があります。これまでの記事では様々なベンチマークを利用してきましたが、その種類が多くなってきたので ベンチマークを整理 することにします。
下記は現時点でのゲーム盤のデータを定義する際の 基底クラス となる Board クラスに定義された 抽象メソッドの一覧 です。
抽象メソッド | 処理 |
---|---|
getmark(x, y) |
(x, y) のマスのマークを返す |
setmark(x, y, mark) |
(x, y) のマスに mark を代入する |
board_to_str() |
ゲーム盤を表す文字列を返す |
judge(last_turn, last_move, move_count) |
勝敗判定を計算して返す |
count_markpats(turn, last_turn) |
局面のマークのパターンを返す |
従って、上記のメソッドの処理速度の影響を比較できる ようなベンチマークを設定する必要があります。どのようなベンチマークを設定すればよいかについて少し考えてみて下さい。
メソッドが呼び出される状況の整理
適切なベンチマークを設定 するためには、上記のメソッドが呼び出される状況を整理 する必要があります。下記は上記のメソッドが呼び出される主な状況です。
メソッド | 主な状況 |
---|---|
getmark |
勝敗判定やマークのパターンの計算 |
setmark |
着手を行う際 |
judge |
着手を行った後 |
board_to_str |
ゲーム盤の探索を行う AI で、置換表を利用する場合 |
count_markpats |
ai14s など、マークのパターンを利用して評価値を計算する AI |
getmark
、setmark
、judge
メソッドは 着手を行うたびに呼び出される ので、〇× ゲームの対戦を行う場合は 必ず頻繁に呼び出されます が、board_to_str
と count_markpats
は 特定の AI の関数 でしか呼び出されません。そこで、下記の 3 種類をベンチマークとして設定し、その処理速度を比較することにしました。
-
ai2
VSai2
の対戦
ランダムな着手を行うai2
は 合法手の中からランダムに選択 するという、最も行う処理が少ない AI の関数 なので、AI の関数の処理時間の影響を最も受けない状態 での対戦の 処理時間を計測 することができる。また、ランダムな着手を行うことで 多くの種類の局面に対する処理 が行われる。なお、これまでのベンチマークで利用してきたai2s
は、子ノードの評価値を計算する処理を行う のでai2
よりも多くの処理を行う。そのため、下記のノートで示すようにai2
よりも処理速度が遅い ため 不採用 とした -
ai14s
VSai2
の対戦
count_markpats
を呼び出すai14s
とai2
の対戦を行うことで、count_markpats
による処理時間の影響を比較 することができる。ai14s
VSai14s
の対戦にしなかった理由 は、その場合に 生じる局面の種類が少ない ためであり、ランダムな着手を行うai2
と対戦 を行うことで なるべく多くの種類の局面が生じる ようにした -
ゲーム開始時の局面に対する置換表を利用した
ai_abs_dls
の計算
board_to_str
の処理時間の影響を比較 することができる。対戦を行わない理由はai_abs_dls
の処理時間が長いため、多くの対戦を行うと時間がかかるからである
下記はゲーム開始時の局面に対する ai2
と ai2s
の処理時間を計測するプログラムで、実行結果のように ai2s
のほうが ai2
よりも約 10 倍の処理時間がかかることが確認できます。
from marubatsu import Marubatsu
from ai import ai2, ai2s
mb = Marubatsu()
%timeit ai2(mb)
%timeit ai2s(mb)
実行結果
2.3 μs ± 24.8 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
23.1 μs ± 878 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
Marubatsu クラスの修正
上記で設定したベンチマークを行う前に、現状の Marubatsu クラスに 不便な点がある ことに気づきましたので、その点を解消するための修正 を行うことにします。
以前の記事で AI どうしの対戦 を行う際に不要な 着手の座標のチェックなどを行わない ようにするという工夫を行ないましたが、その 工夫を利用するため には Marubatsu クラスのインスタンスの作成時に実引数に check_coord=False
を記述する必要がある点が面倒 です。
そこで、下記のように Marubatsu クラスを修正 することで、何も記述しなくても人間が着手を行う場合のみ座標のチェックを行う ように改良することにします。
-
__init__
メソッドの 仮引数check_coord
を削除 する -
place_mark
メソッドでは 常に座標のチェックを行う ようにする -
move
メソッドに デフォルト値をFalse
とした 仮引数check_coord
を追加 し、check_coord
がTrue
の場合 はplace_mark
メソッドで 座標のチェックを行う着手 を行い、False
の場合 はself.board.setmark
で 座標のチェックを行わずに着手 を行うようにする。デフォルト値をFalse
とした理由 は、ai_by_move
などの AI の関数の中で呼び出されるmove
メソッドで 座標のチェックを行わない ようにするためである -
unmove
メソッドで 直前の着手を取り消す場合 は座標のチェックを行う必要はないのでself.board.setmark
で直前に着手した マークを削除する ようにする。 - 上記の
unmove
メソッドの修正によりremove_mark
メソッドを 利用する場面はなくなった のでremove_mark
メソッドは廃止 する -
play_loop
メソッドで 人間がキーボードから入力した座標のマスに着手を行う 場合はcheck_coord=True
を実引数に記述してmove
メソッドを呼び出す ことで、座標のチェックを行う ようにする
__init__
メソッドの修正
下記は __init__
メソッドを修正したプログラムです。
-
3 行目:仮引数
check_coord
を削除した - 10 行目の下にあった
check_coord
を同名の属性に代入する処理を削除した
1 from marubatsu import ListBoard
2
3 def __init__(self, boardclass=ListBoard, board_size=3, *args, **kwargs):
4 # ゲーム盤のデータ構造を定義するクラス
5 self.boardclass = boardclass
6 # ゲーム盤の縦横のサイズ
7 self.BOARD_SIZE = board_size
8 # boardclass のパラメータ
9 self.args = args
10 self.kwargs = kwargs
11 # 〇×ゲーム盤を再起動するメソッドを呼び出す
12 self.restart()
13
14 Marubatsu.__init__ = __init__
行番号のないプログラム
from marubatsu import ListBoard
def __init__(self, boardclass=ListBoard, board_size=3, *args, **kwargs):
# ゲーム盤のデータ構造を定義するクラス
self.boardclass = boardclass
# ゲーム盤の縦横のサイズ
self.BOARD_SIZE = board_size
# boardclass のパラメータ
self.args = args
self.kwargs = kwargs
# 〇×ゲーム盤を再起動するメソッドを呼び出す
self.restart()
Marubatsu.__init__ = __init__
修正箇所
from marubatsu import ListBoard
-def __init__(self, boardclass=ListBoard, board_size=3, check_coord:bool=True, *args, **kwargs):
+def __init__(self, boardclass=ListBoard, board_size=3, *args, **kwargs):
# ゲーム盤のデータ構造を定義するクラス
self.boardclass = boardclass
# ゲーム盤の縦横のサイズ
self.BOARD_SIZE = board_size
# boardclass のパラメータ
self.args = args
self.kwargs = kwargs
- # move と unmove メソッドで座標などのチェックを行うかどうか
- self.check_coord = check_coord
# 〇×ゲーム盤を再起動するメソッドを呼び出す
self.restart()
Marubatsu.__init__ = __init__
place_mark
メソッドの修正
下記は place_mark
メソッドを修正したプログラムです。修正箇所は check_coord
属性による条件分岐 と、False
の場合の処理を削除 した点で、check_coord
を導入する前のプログラムに戻すというものです。修正箇所は省略します。
def place_mark(self, x, y, mark):
if 0 <= x < self.BOARD_SIZE and 0 <= y < self.BOARD_SIZE:
if self.board.getmark(x, y) == Marubatsu.EMPTY:
self.board.setmark(x, y, mark)
return True
else:
print("(", x, ",", y, ") のマスにはマークが配置済です")
return False
else:
print("(", x, ",", y, ") はゲーム盤の範囲外の座標です")
return False
Marubatsu.place_mark = place_mark
なお、replace_mark
メソッドは先ほど説明したように 廃止する ことにしたので 修正する必要はありません。
move
メソッドの修正
下記は move
メソッドを修正したプログラムです。
-
1 行目:デフォルト値を
False
とする仮引数check_coord
を追加する -
2、3 行目:
check_coord
がFalse
の場合はself.board.setmark
を呼び出して座標のチェックを行わずに着手を行う -
4、5 行目:
check_coord
がTrue
の場合はplace_mark
メソッドを呼び出して着手を試み、着手を行えなかったことを表すFalse
が返り値となった場合はreturn
文を呼び出して処理を終了する - 6 行目にあった
if self.place_mark
の条件文を削除し、その if 文のインデントを削除して常に着手が行われた場合の処理を行うように修正する
1 def move(self, x, y, check_coord=False):
2 if not check_coord:
3 self.board.setmark(x, y, self.turn)
4 elif not self.place_mark(x, y, self.turn):
5 return
6
インデント以外は元のプログラムと同じなので省略
7
8 Marubatsu.move = move
行番号のないプログラム
def move(self, x, y, check_coord=False):
if not check_coord:
self.board.setmark(x, y, self.turn)
elif not self.place_mark(x, y, self.turn):
return
self.last_turn = self.turn
self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
self.move_count += 1
self.last_move = x, y
self.status = self.board.judge(self.last_turn, self.last_move, self.move_count)
if len(self.records) <= self.move_count:
self.records.append(self.last_move)
else:
self.records[self.move_count] = self.last_move
self.records = self.records[0:self.move_count + 1]
Marubatsu.move = move
修正箇所
-def move(self, x, y):
+def move(self, x, y, check_coord=False):
+ if not check_coord:
+ self.board.setmark(x, y, self.turn)
+ elif not self.place_mark(x, y, self.turn):
+ return
- if self.place_mark(x, y, self.turn):
インデント以外は元のプログラムと同じなので省略
Marubatsu.move = move
unmove
メソッドの修正
下記は unmove
メソッドを修正したプログラムです。
-
3 行目:
self.board.setmark
を呼び出して座標のチェックを行わずにマークを削除するように修正する
1 def unmove(self):
2 if self.move_count > 0:
元と同じなので省略
3 self.board.setmark(x, y, Marubatsu.EMPTY)
4 self.last_move = self.records[-1]
5
6 Marubatsu.unmove = unmove
行番号のないプログラム
def unmove(self):
if self.move_count > 0:
x, y = self.last_move
if self.move_count == 0:
self.last_move = (-1, -1)
self.move_count -= 1
self.turn, self.last_turn = self.last_turn, self.turn
self.status = Marubatsu.PLAYING
x, y = self.records.pop()
self.board.setmark(x, y, Marubatsu.EMPTY)
self.last_move = self.records[-1]
Marubatsu.unmove = unmove
修正箇所
def unmove(self):
if self.move_count > 0:
元と同じなので省略
- self.remove_mark(x, y)
+ self.board.setmark(x, y, Marubatsu.EMPTY)
self.last_move = self.records[-1]
Marubatsu.unmove = unmove
play_loop
メソッドの修正
下記は play_loop
メソッドを修正したプログラムです。
-
7 行目:AI の手番の場合は
move
メソッドにcheck_coord=True
を記述せずに呼び出すことで座標のチェックを行わずに着手を行うように修正する。なお、元のプログラムではself.move(int(x), int(y))
のように座標を組み込み関数int
で整数型に型変換を行っていたが、AI が計算した座標は整数なのでint
を利用する必要はない -
11 ~ 14 行目:人間の手番の場合は、
move
メソッドにcheck_coord=True
を記述して呼び出すように修正する。なお、11 ~ 14 行目の着手を行うプログラムは 5 ~ 14 行目の if 文の後で記述することで手番が AI の場合と人間の両方の場合で実行されていたが、着手を行う処理が AI と 人間で異なるようになったのでこちらに移動した
1 def play_loop(self, mb_gui, params=None):
元と同じなので省略
2 # ゲームの決着がついていない間繰り返す
3 while self.status == Marubatsu.PLAYING:
元と同じなので省略
4 # ai が着手を行うかどうかを判定する
5 if ai[index] is not None:
6 x, y = ai[index](self, **params[index])
7 self.move(x, y)
8 else:
9 # キーボードからの座標の入力
10 coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
元と同じなので省略
11 try:
12 self.move(int(x), int(y), check_coord=True)
13 except:
14 print("整数の座標を入力して下さい")
元と同じなので省略
15
16 Marubatsu.play_loop = play_loop
行番号のないプログラム
def play_loop(self, mb_gui, params=None):
if params is None:
params = [{}, {}]
ai = self.ai
verbose = self.verbose
gui = self.gui
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# 現在の手番を表す ai のインデックスを計算する
index = 0 if self.turn == Marubatsu.CIRCLE else 1
# ゲーム盤の表示
if verbose:
if gui:
# AI どうしの対戦の場合は画面を描画しない
if ai[0] is None or ai[1] is None:
mb_gui.update_gui()
# 手番を人間が担当する場合は、play メソッドを終了する
if ai[index] is None:
return
else:
print(self)
# ai が着手を行うかどうかを判定する
if ai[index] is not None:
x, y = ai[index](self, **params[index])
self.move(x, y)
else:
# キーボードからの座標の入力
coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
# "exit" が入力されていればメッセージを表示して関数を終了する
if coord == "exit":
print("ゲームを終了します")
return
# x 座標と y 座標を要素として持つ list を計算する
xylist = coord.split(",")
# xylist の要素の数が 2 ではない場合
if len(xylist) != 2:
# エラーメッセージを表示する
print("x, y の形式ではありません")
# 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
continue
x, y = xylist
try:
self.move(int(x), int(y), check_coord=True)
except:
print("整数の座標を入力して下さい")
# 決着がついたので、ゲーム盤を表示する
if verbose:
if gui:
mb_gui.update_gui()
else:
print(self)
return self.status
Marubatsu.play_loop = play_loop
修正箇所
def play_loop(self, mb_gui, params=None):
元と同じなので省略
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
元と同じなので省略
# ai が着手を行うかどうかを判定する
if ai[index] is not None:
x, y = ai[index](self, **params[index])
+ self.move(x, y)
else:
# キーボードからの座標の入力
coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
元と同じなので省略
+ try:
+ self.move(int(x), int(y), check_coord=True)
+ except:
+ print("整数の座標を入力して下さい")
- try:
- self.move(int(x), int(y))
- except:
- print("整数の座標を入力して下さい")
元と同じなので省略
Marubatsu.play_loop = play_loop
動作の確認
上記の修正後に下記のプログラムで 人間どうしの対戦 で 下記の順で着手の入力を行う と、実行結果のように 着手できる場合は着手が行われ、着手できない場合は着手せずにメッセージが表示される ことが確認できました。
- 0, 0 を入力
- もう一度 0, 0 を入力
- 盤外の 3, 5 の座標を入力
- 数字以外の a, b の座標を入力
- exit を入力して終了
mb = Marubatsu()
mb.play(ai=[None, None])
実行結果
Turn o
...
...
...
Turn x
O..
...
...
( 0 , 0 ) のマスにはマークが配置済です
Turn x
O..
...
...
( 3 , 5 ) はゲーム盤の範囲外の座標です
Turn x
O..
...
...
整数の座標を入力して下さい
Turn x
O..
...
...
ゲームを終了します
上記の修正の問題点の検証と修正
上記の修正によって実引数に check_coord=True
を記述せず に move
メソッドを呼び出した場合は 座標のチェックなどが行われなくなります。そのため、下記のプログラムのように 自分で move
メソッドを呼び出すプログラムを記述 した際に、既にマークが配置されているマスに着手 を行っても エラーは発生せず に元のマークを 上書きして着手できてしまう ため、間違ったゲーム盤の状況 になってしまう点に注意が必要です。
mb = Marubatsu()
mb.move(0, 0)
print(mb)
mb.move(0, 0)
print(mb)
実行結果
Turn x
O..
...
...
Turn o
X..
...
...
また、下記のプログラムのように ゲーム盤の外に着手 を行うと エラーが発生 します。
mb.move(3, 5)
実行結果
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
Cell In[9], line 1
----> 1 mb.move(3, 5)
Cell In[4], line 3
1 def move(self, x, y, check_coord=False):
2 if not check_coord:
----> 3 self.board.setmark(x, y, self.turn)
4 elif not self.place_mark(x, y, self.turn):
5 return
File c:\Users\ys\ai\marubatsu\194\marubatsu.py:158, in ListBoard.setmark(self, x, y, mark)
156 if x + y == self.BOARD_SIZE - 1:
157 self.diacount[countmark][1] += diff
--> 158 self.board[x][y] = mark
IndexError: list index out of range
従って、これまでと同様に 座標のチェックを行う ようにするためには下記のプログラムのように move
の実引数に check_coord=True
を記述 する必要があります。
mb = Marubatsu()
mb.move(0, 0, check_coord=True)
print(mb)
mb.move(0, 0, check_coord=True)
print(mb)
mb.move(3, 5, check_coord=True)
実行結果
Turn x
O..
...
...
( 0 , 0 ) のマスにはマークが配置済です
Turn x
O..
...
...
( 3 , 5 ) はゲーム盤の範囲外の座標です
cmove
メソッドの追加
着手を行うプログラムを記述する際に check_coord=True
を毎回記述するのは大変 なので、座標などのチェック(check)を行う cmove
という関数を定義することにします。なお、この関数は 今後も頻繁に記述して呼び出す ことになる check_and_move
のような長い名前ではなく、cmove
のように 短く記述できる ようにしました。
cmove
の定義は単純で、下記のプログラムのように 実引数に check_coord=True
を記述して move
を呼び出す だけです。
def cmove(self, x, y):
self.move(x, y, check_coord=True)
Marubatsu.cmove = cmove
上記の定義を実行することで、下記のプログラムのように cmove
で 座標のチェックを伴う着手 を行うことができるようになります。
mb = Marubatsu()
mb.cmove(0, 0)
print(mb)
mb.cmove(0, 0)
print(mb)
mb.cmove(3, 5)
実行結果
Turn x
O..
...
...
( 0 , 0 ) のマスにはマークが配置済です
Turn x
O..
...
...
( 3 , 5 ) はゲーム盤の範囲外の座標です
cmove
の問題点と place_mark
の修正
cmove
には下記のプログラムのように 座標に文字列を記述 すると エラーが発生 するという問題があります。
mb.cmove("a", "b")
実行結果
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[13], line 1
----> 1 mb.cmove("a", "b")
Cell In[11], line 2
1 def cmove(self, x, y):
----> 2 self.move(x, y, check_coord=True)
Cell In[4], line 4
2 if not check_coord:
3 self.board.setmark(x, y, self.turn)
----> 4 elif not self.place_mark(x, y, self.turn):
5 return
7 self.last_turn = self.turn
Cell In[3], line 2
1 def place_mark(self, x, y, mark):
----> 2 if 0 <= x < self.BOARD_SIZE and 0 <= y < self.BOARD_SIZE:
3 if self.board.getmark(x, y) == Marubatsu.EMPTY:
4 self.board.setmark(x, y, mark)
TypeError: '<=' not supported between instances of 'int' and 'str'
座標に文字列を指定した場合を考慮した処理 は、play_loop
メソッドの中で下記のプログラムのように記述されていますが、この処理は cmove
メソッドでは行われない ため上記のようなエラーが発生します。
try:
self.move(int(x), int(y), check_coord=True)
except:
print("整数の座標を入力して下さい")
この処理を cmove
メソッドの中に記述してもかまわないのですが、座標に関する他のチェック は place_mark
メソッドで行っている ので、この処理も 下記のプログラムのように place_mark
メソッドで行う ように修正することにします。
-
3、4 行目:組み込み関数
int
で仮引数x
とy
を整数型のデータに型変換を行う -
2 ~ 7 行目:try ~ except 文で上記の処理の際にエラーが発生した場合は 6 行目でメッセージを表示して
False
を返すようにする
1 def place_mark(self, x, y, mark):
2 try:
3 x = int(x)
4 y = int(y)
5 except:
6 print("整数の座標を入力して下さい")
7 return False
元と同じなので省略
8
9 Marubatsu.place_mark = place_mark
行番号のないプログラム
def place_mark(self, x, y, mark):
try:
x = int(x)
y = int(y)
except:
print("整数の座標を入力して下さい")
return False
if 0 <= x < self.BOARD_SIZE and 0 <= y < self.BOARD_SIZE:
if self.board.getmark(x, y) == Marubatsu.EMPTY:
self.board.setmark(x, y, mark)
return True
else:
print("(", x, ",", y, ") のマスにはマークが配置済です")
return False
else:
print("(", x, ",", y, ") はゲーム盤の範囲外の座標です")
return False
Marubatsu.place_mark = place_mark
修正箇所
def place_mark(self, x, y, mark):
+ try:
+ x = int(x)
+ y = int(y)
+ except:
+ print("整数の座標を入力して下さい")
+ return False
元と同じなので省略
Marubatsu.place_mark = place_mark
上記の修正後に先程と同じプログラムを実行すると、実行結果のように エラーが発生せず に意図したメッセージが表示されるようになったことが確認できます。
mb.cmove("a", "b")
実行結果
整数の座標を入力して下さい
play_loop
の修正
cmove
の定義によって、play_loop
の中で 人間が着手を行う場合の処理 を下記のプログラムの 3 行目のように cmove
で置き換える ことができます。なお、整数型への型変換 を place_mark
で行う ようになったので cmove
の実引数に int(x)
や int(y)
を記述して 型変換を行う必要はありません。
1 def play_loop(self, mb_gui, params=None):
元と同じなので省略
2 x, y = xylist
3 self.cmove(x, y)
元と同じなので省略
4
5 Marubatsu.play_loop = play_loop
行番号のないプログラム
def play_loop(self, mb_gui, params=None):
if params is None:
params = [{}, {}]
ai = self.ai
verbose = self.verbose
gui = self.gui
# ゲームの決着がついていない間繰り返す
while self.status == Marubatsu.PLAYING:
# 現在の手番を表す ai のインデックスを計算する
index = 0 if self.turn == Marubatsu.CIRCLE else 1
# ゲーム盤の表示
if verbose:
if gui:
# AI どうしの対戦の場合は画面を描画しない
if ai[0] is None or ai[1] is None:
mb_gui.update_gui()
# 手番を人間が担当する場合は、play メソッドを終了する
if ai[index] is None:
return
else:
print(self)
# ai が着手を行うかどうかを判定する
if ai[index] is not None:
x, y = ai[index](self, **params[index])
self.move(x, y)
else:
# キーボードからの座標の入力
coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
# "exit" が入力されていればメッセージを表示して関数を終了する
if coord == "exit":
print("ゲームを終了します")
return
# x 座標と y 座標を要素として持つ list を計算する
xylist = coord.split(",")
# xylist の要素の数が 2 ではない場合
if len(xylist) != 2:
# エラーメッセージを表示する
print("x, y の形式ではありません")
# 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
continue
x, y = xylist
self.cmove(x, y)
# 決着がついたので、ゲーム盤を表示する
if verbose:
if gui:
mb_gui.update_gui()
else:
print(self)
return self.status
Marubatsu.play_loop = play_loop
修正箇所
def play_loop(self, mb_gui, params=None):
元と同じなので省略
- try:
- self.move(int(x), int(y), check_coord=True)
- except:
- print("整数の座標を入力して下さい")
+ self.cmove(x, y)
元と同じなので省略
Marubatsu.play_loop = play_loop
上記の修正後に下記のプログラムを実行して 座標に a, b を入力 すると、実行結果のように正しいメッセージが表示されることが確認できます。
mb.play(ai=[None, None])
実行結果
Turn o
...
...
...
整数の座標を入力して下さい
Turn o
...
...
...
ゲームを終了します
Marubatsu_GUI クラスの修正
初心者には気づきづらいかもしれませんが、先程の修正を行った結果 GUI で 〇× ゲームを遊ぶ場合 に 既に着手が行われたマスをクリック して着手を行うと ゲーム盤の表示が消えてしまう という バグが発生 します。具体的には下記のプログラムを実行して (0, 0) のマスをクリックして 〇 を着手した後でもう一度 (0, 0) のマスをクリックすると、実行結果のようにゲーム盤の表示が消えてしまします。
from util import gui_play
gui_play()
実行結果

このバグの原因は GUI で 〇× ゲームの対戦の処理 を行う Marubatsu_GUI の中の マスをクリックして着手を行う処理 で move
メソッドが実引数 check_coord=True
を記述せずに呼び出されている からです。従って、クリックで着手を行う処理が定義されている create_event_handler
メソッド内のローカル関数 on_mouse_down
の処理を下記のプログラムのように修正することでこのバグを修正することができます。
-
15 行目:座標のチェックを行う
cmove
を呼び出すように修正する
1 from marubatsu import Marubatsu_GUI
2 from tkinter import Tk, filedialog
3 import pickle
4 from datetime import datetime
5 import math
6
7 def create_event_handler(self):
元と同じなので省略
8 # ゲーム盤の上でマウスを押した場合のイベントハンドラ
9 def on_mouse_down(event):
10 # Axes の上でマウスを押していた場合のみ処理を行う
11 if event.inaxes and self.mb.status == Marubatsu.PLAYING:
12 x = math.floor(event.xdata)
13 y = math.floor(event.ydata)
14 with self.output:
15 self.mb.cmove(x, y)
16 # 次の手番の処理を行うメソッドを呼び出す
17 self.mb.play_loop(self, self.params)
元と同じなので省略
18
19 Marubatsu_GUI.create_event_handler = create_event_handler
行番号のないプログラム
from marubatsu import Marubatsu_GUI
from tkinter import Tk, filedialog
import pickle
from datetime import datetime
import math
def create_event_handler(self):
# 乱数の種のチェックボックスのイベントハンドラを定義する
def on_checkbox_changed(changed):
self.update_widgets_status()
self.checkbox.observe(on_checkbox_changed, names="value")
# 開く、保存ボタンのイベントハンドラを定義する
def on_load_button_clicked(b=None):
path = filedialog.askopenfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
initialdir="save")
if path != "":
with open(path, "rb") as f:
data = pickle.load(f)
self.mb.records = data["records"]
self.mb.ai = data["ai"]
self.params = data["params"] if "params" in data else [ {}, {} ]
if "names" in data:
names = data["names"]
else:
names = [ "人間" if self.mb.ai[i] is None else self.mb.ai[i].__name__ for i in range(2)]
options = self.dropdown_list[0].options.copy()
for i in range(2):
value = (self.mb.ai[i], self.params[i])
if not value in options.values():
options[names[i]] = value
for i in range(2):
self.dropdown_list[i].options = options
self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])
status_options = options.copy()
status_options["手番の AI"] = ("Auto", None)
self.status_dropdown.options = status_options
change_step(data["move_count"])
if data["seed"] is not None:
self.checkbox.value = True
self.inttext.value = data["seed"]
else:
self.checkbox.value = False
def on_save_button_clicked(b=None):
names = [ self.dropdown_list[i].label for i in range(2) ]
timestr = datetime.now().strftime("%Y年%m月%d日 %H時%M分%S秒")
fname = f"{names[0]} VS {names[1]} {timestr}"
path = filedialog.asksaveasfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
initialdir="save", initialfile=fname,
defaultextension="mbsav")
if path != "":
with open(path, "wb") as f:
data = {
"records": self.mb.records,
"move_count": self.mb.move_count,
"ai": self.mb.ai,
"params": self.params,
"names": names,
"seed": self.inttext.value if self.checkbox.value else None
}
pickle.dump(data, f)
def on_show_tree_button_clicked(b=None):
self.show_subtree = not self.show_subtree
self.mbtree_gui.vbox.layout.display = None if self.show_subtree else "none"
self.update_gui()
def on_reset_tree_button_clicked(b=None):
self.update_gui()
def on_help_button_clicked(b=None):
self.help.layout.display = "none" if self.help.layout.display is None else None
self.load_button.on_click(on_load_button_clicked)
self.save_button.on_click(on_save_button_clicked)
self.show_tree_button.on_click(on_show_tree_button_clicked)
self.reset_tree_button.on_click(on_reset_tree_button_clicked)
self.help_button.on_click(on_help_button_clicked)
def on_show_status_button_clicked(b=None):
self.show_status = not self.show_status
self.update_gui()
def on_status_dropdown_changed(changed):
self.update_gui()
def on_size_slider_changed(changed):
self.size = changed["new"]
self.fig.set_figwidth(self.size)
self.fig.set_figheight(self.size)
self.update_gui()
self.show_status_button.on_click(on_show_status_button_clicked)
self.status_dropdown.observe(on_status_dropdown_changed, names="value")
self.size_slider.observe(on_size_slider_changed, names="value")
# 変更ボタンのイベントハンドラを定義する
def on_change_button_clicked(b):
for i in range(2):
self.mb.ai[i], self.params[i] = self.dropdown_list[i].value
self.mb.play_loop(self, self.params)
# リセットボタンのイベントハンドラを定義する
def on_reset_button_clicked(b=None):
# 乱数の種のチェックボックスが ON の場合に、乱数の種の処理を行う
if self.checkbox.value:
random.seed(self.inttext.value)
self.mb.restart()
self.output.clear_output()
on_change_button_clicked(b)
# 待ったボタンのイベントハンドラを定義する
def on_undo_button_clicked(b=None):
if self.mb.move_count >= 2 and self.mb.move_count == len(self.mb.records) - 1:
self.mb.move_count -= 2
self.mb.records = self.mb.records[0:self.mb.move_count+1]
self.mb.change_step(self.mb.move_count)
self.update_gui()
# イベントハンドラをボタンに結びつける
self.change_button.on_click(on_change_button_clicked)
self.reset_button.on_click(on_reset_button_clicked)
self.undo_button.on_click(on_undo_button_clicked)
# step 手目の局面に移動する
def change_step(step):
self.mb.change_step(step)
# 描画を更新する
self.update_gui()
def on_first_button_clicked(b=None):
change_step(0)
def on_prev_button_clicked(b=None):
change_step(self.mb.move_count - 1)
def on_next_button_clicked(b=None):
change_step(self.mb.move_count + 1)
def on_last_button_clicked(b=None):
change_step(len(self.mb.records) - 1)
def on_slider_changed(changed):
if self.mb.move_count != changed["new"]:
change_step(changed["new"])
self.first_button.on_click(on_first_button_clicked)
self.prev_button.on_click(on_prev_button_clicked)
self.next_button.on_click(on_next_button_clicked)
self.last_button.on_click(on_last_button_clicked)
self.slider.observe(on_slider_changed, names="value")
# ゲーム盤の上でマウスを押した場合のイベントハンドラ
def on_mouse_down(event):
# Axes の上でマウスを押していた場合のみ処理を行う
if event.inaxes and self.mb.status == Marubatsu.PLAYING:
x = math.floor(event.xdata)
y = math.floor(event.ydata)
with self.output:
self.mb.cmove(x, y)
# 次の手番の処理を行うメソッドを呼び出す
self.mb.play_loop(self, self.params)
# ゲーム盤を選択した状態でキーを押した場合のイベントハンドラ
def on_key_press(event):
keymap = {
"up": on_first_button_clicked,
"left": on_prev_button_clicked,
"right": on_next_button_clicked,
"down": on_last_button_clicked,
"0": on_undo_button_clicked,
"enter": on_reset_button_clicked,
"-": on_load_button_clicked,
"l": on_load_button_clicked,
"+": on_save_button_clicked,
"s": on_save_button_clicked,
"*": on_help_button_clicked,
"h": on_help_button_clicked,
}
if event.key in keymap:
keymap[event.key]()
else:
try:
num = int(event.key) - 1
event.inaxes = True
event.xdata = num % 3
event.ydata = 2 - (num // 3)
on_mouse_down(event)
except:
pass
# fig の画像イベントハンドラを結び付ける
self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)
self.fig.canvas.mpl_connect("key_press_event", on_key_press)
Marubatsu_GUI.create_event_handler = create_event_handler
修正箇所
from marubatsu import Marubatsu_GUI
from tkinter import Tk, filedialog
import pickle
from datetime import datetime
import math
def create_event_handler(self):
元と同じなので省略
# ゲーム盤の上でマウスを押した場合のイベントハンドラ
def on_mouse_down(event):
# Axes の上でマウスを押していた場合のみ処理を行う
if event.inaxes and self.mb.status == Marubatsu.PLAYING:
x = math.floor(event.xdata)
y = math.floor(event.ydata)
with self.output:
- self.mb.move(x, y)
+ self.mb.cmove(x, y)
# 次の手番の処理を行うメソッドを呼び出す
self.mb.play_loop(self, self.params)
元と同じなので省略
Marubatsu_GUI.create_event_handler = create_event_handler
上記の修正後に下記のプログラムを実行し、着手済みのマスをクリックしてもバグが発生しなくなった ことを確認してみて下さい。
gui_play()
ベンチマークを行う関数の定義とベンチマークの実行
3 種類の ベンチマーク を行うプログラムを 毎回記述するのは大変 なので、その処理を行う 下記の仮引数 を持つ benchmark
という名前の 関数を定義 することにします。
仮引数 | 意味 | デフォルト値 |
---|---|---|
mbparams |
Marubatsu クラスのインスタンスの作成時の実引数 | 空の dict |
match_num |
ai_match で対戦を行う回数 |
50000 |
seed |
乱数の種。None の場合は乱数の種を初期化しない |
0 |
乱数の種 を設定できるようにした理由は、これまでの ランダムな着手 を行う AI どうしの ai_match
での対戦 では 毎回対戦成績が変わる ため、同じ実引数で ai_match
を何度も実行した場合 の 1 秒あたりの対戦回数1の ばらつきが大きかったため です。ベンチマーク では、毎回同じ処理が行われないと処理時間の比較の意味が薄れる ので、乱数の種を利用 して ベンチマークで同じ対戦が行われる ようにしました。
また、これまでは match_num=5000
を指定することで先手と後手を入れ替えて 10000 回の対戦を行いましたが、数秒でその対戦を行うことができます。そこで match_num
のデフォルト値 を 10 倍の 50000 にして 10 万回の対戦 を行うことで、以前の記事 で説明した 大数の法則 によって 1 秒あたりの対戦回数の精度を高める ことにしました。
下記はそのように benchmark
を定義したプログラムです。この、benchmark
は util.py に記述 することにします。
-
4 行目:上記の仮引数を持つ
benchmark
を定義する -
5、6 行目:
seed
がNone
でない場合に乱数の種の初期化を行う -
8 行目:
ai_match
でai2
vSai2
の対戦を行う -
9 行目:
ai_match
でai14s
vSai2
の対戦を行う -
11 ~ 14 行目:ゲーム開始時の局面に対して置換表を利用した
ai_abs_dls
の処理時間を %timeit で計測する
1 from ai import ai_match, ai14s, ai_abs_dls
2 import random
3
4 def benchmark(mbparams={}, match_num=50000, seed=0):
5 if seed is not None:
6 random.seed(seed)
7
8 ai_match(ai=[ai2, ai2], match_num=match_num, mbparams=mbparams)
9 ai_match(ai=[ai14s, ai2], match_num=match_num, mbparams=mbparams)
10
11 mb = Marubatsu(**mbparams)
12 eval_params = {"minimax": True}
13 print("ai_abs_dls")
14 %timeit ai_abs_dls(mb, eval_func=ai14s, eval_params=eval_params, use_tt=True, maxdepth=8)
行番号のないプログラム
from ai import ai_match, ai14s, ai_abs_dls
import random
def benchmark(mbparams={}, match_num=50000, seed=0):
if seed is not None:
random.seed(seed)
ai_match(ai=[ai2, ai2], match_num=match_num, mbparams=mbparams)
ai_match(ai=[ai14s, ai2], match_num=match_num, mbparams=mbparams)
mb = Marubatsu(**mbparams)
eval_params = {"minimax": True}
print("ai_abs_dls")
%timeit ai_abs_dls(mb, eval_func=ai14s, eval_params=eval_params, use_tt=True, maxdepth=8)
ベンチマークの実行
下記のそれぞれの組み合わせに対してベンチマークを実行します。
- ゲーム盤のデータ構造を表すクラスとして ListBoard と List1dBoard を利用する場合
- 直線上のマークの数を 数える 場合と 数えない 場合
下記は上記の組み合わせでベンチマークを行うプログラムです。
from marubatsu import List1dBoard
for boardclass in [ListBoard, List1dBoard]:
for count_linemark in [False, True]:
print(f"boardclass: {boardclass.__name__}, count_linemark {count_linemark}")
benchmark(mbparams={"boardclass": boardclass, "count_linemark": count_linemark})
print()
実行結果
boardclass: ListBoard, count_linemark False
ai2 VS ai2
100%|██████████| 50000/50000 [00:04<00:00, 12391.16it/s]
count win lose draw
o 29454 14352 6194
x 14208 29592 6200
total 43662 43944 12394
ratio win lose draw
o 58.9% 28.7% 12.4%
x 28.4% 59.2% 12.4%
total 43.7% 43.9% 12.4%
ai14s VS ai2
100%|██████████| 50000/50000 [00:51<00:00, 969.45it/s]
count win lose draw
o 49446 0 554
x 44043 0 5957
total 93489 0 6511
ratio win lose draw
o 98.9% 0.0% 1.1%
x 88.1% 0.0% 11.9%
total 93.5% 0.0% 6.5%
ai_abs_dls
17.4 ms ± 143 μs per loop (mean ± std. dev. of 7 runs, 10 loops each)
boardclass: ListBoard, count_linemark True
ai2 VS ai2
100%|██████████| 50000/50000 [00:04<00:00, 12340.74it/s]
count win lose draw
o 29454 14352 6194
x 14208 29592 6200
total 43662 43944 12394
ratio win lose draw
o 58.9% 28.7% 12.4%
x 28.4% 59.2% 12.4%
total 43.7% 43.9% 12.4%
ai14s VS ai2
100%|██████████| 50000/50000 [00:26<00:00, 1882.32it/s]
count win lose draw
o 49446 0 554
x 44043 0 5957
total 93489 0 6511
ratio win lose draw
o 98.9% 0.0% 1.1%
x 88.1% 0.0% 11.9%
total 93.5% 0.0% 6.5%
ai_abs_dls
17.6 ms ± 397 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
boardclass: List1dBoard, count_linemark False
ai2 VS ai2
100%|██████████| 50000/50000 [00:04<00:00, 11801.20it/s]
count win lose draw
o 29454 14352 6194
x 14208 29592 6200
total 43662 43944 12394
ratio win lose draw
o 58.9% 28.7% 12.4%
x 28.4% 59.2% 12.4%
total 43.7% 43.9% 12.4%
ai14s VS ai2
100%|██████████| 50000/50000 [00:52<00:00, 956.38it/s]
count win lose draw
o 49446 0 554
x 44043 0 5957
total 93489 0 6511
ratio win lose draw
o 98.9% 0.0% 1.1%
x 88.1% 0.0% 11.9%
total 93.5% 0.0% 6.5%
ai_abs_dls
17.6 ms ± 477 μs per loop (mean ± std. dev. of 7 runs, 10 loops each)
boardclass: List1dBoard, count_linemark True
ai2 VS ai2
100%|██████████| 50000/50000 [00:04<00:00, 12300.84it/s]
count win lose draw
o 29454 14352 6194
x 14208 29592 6200
total 43662 43944 12394
ratio win lose draw
o 58.9% 28.7% 12.4%
x 28.4% 59.2% 12.4%
total 43.7% 43.9% 12.4%
ai14s VS ai2
100%|██████████| 50000/50000 [00:26<00:00, 1875.35it/s]
count win lose draw
o 49446 0 554
x 44043 0 5957
total 93489 0 6511
ratio win lose draw
o 98.9% 0.0% 1.1%
x 88.1% 0.0% 11.9%
total 93.5% 0.0% 6.5%
ai_abs_dls
18.6 ms ± 1.48 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
下記は実行結果をまとめた表です。なお、ai2
は ai2s
よりも処理速度が 10 倍ほど速い ので以前の記事の ai2s
VS ai2s
の対戦よりも 1 秒あたりの対戦回数が 大きく増えます。
boardclass | count_linemark |
ai2 VS ai2
|
ai14s VS ai2
|
ai_abs_dls |
---|---|---|---|---|
ListBoard | False |
12391.16 回/秒 | 969.45 回/秒 | 17.4 ms |
ListBoard | True |
12340.74 回/秒 | 1882.32 回/秒 | 17.6 ms |
List1dBoard | False |
11801.20 回/秒 | 956.38 回/秒 | 17.6 ms |
List1dBoard | True |
12300.84 回/秒 | 1875.35 回/秒 | 18.6 ms |
上記から以下の事がわかります。
-
すべての条件 で
ai2
VSai2
の処理速度はほぼかわらない2 ので、対戦におけるgetmark
、setmark
、judge
の処理時間の合計 は ListBoard と List1dBoard によるゲーム盤のデータ構造の違いや、直線上のマークを数えるかどうかによって ほぼ変化しない -
count_markpats
を呼び出すai14s
VSais
の対戦では 直線上のマークを数える ことで 1 秒あたりの 処理回数が 約 2 倍 になるので、count_markpats
を利用する場合 は 直線上のマークを数えたほうが処理速度が速くなる -
すべての条件 で
board_to_str
を利用するai_abs_dls
の処理速度はほぼかわらない ので、board_to_str
の処理時間 は ListBoard と List1dBoard によるゲーム盤のデータ構造の違いや、直線上のマークを数えるかどうかによって ほぼ変化しない
乱数の種の影響の確認
ai2
VS ai2
と ai14s
VS ai2
の対戦結果は いずれの場合も下記の表のようになる ので、乱数の種を設定 したことで 条件が異なっても同じ AI どうしの対戦成績が同じになる ことが確認できます。なお、対戦成績は全体の結果のみを表にまとめましたが、〇 が先手と × が先手の場合の結果も同じになります。
対戦カード | 全体の勝ち | 全体の負け | 全体の引き分け |
---|---|---|---|
ai2 VS ai2 |
43662 | 43944 | 12394 |
ai14s VS ai2 |
93489 | 0 | 6511 |
また、下記のプログラムで 乱数の種を 1 とした ai2
VS ai2
の対戦を ListBoard と直線上のマークの数を数えない設定で行うと、実行結果から 上記とは異なる対戦成績になる ことが確認できます。興味がある方は seed=None
を記述して実行した場合は乱数の種が初期化されないので毎回異なる対戦成績になることを確認してみて下さい。
benchmark(mbparams={"boardclass": ListBoard, "count_linemark": False}, seed=1)
実行結果
ai2 VS ai2
100%|██████████| 50000/50000 [00:04<00:00, 11799.85it/s]
count win lose draw
o 29318 14501 6181
x 14278 29306 6416
total 43596 43807 12597
ratio win lose draw
o 58.6% 29.0% 12.4%
x 28.6% 58.6% 12.8%
total 43.6% 43.8% 12.6%
ai14s VS ai2
100%|██████████| 50000/50000 [00:51<00:00, 971.35it/s]
count win lose draw
o 49524 0 476
x 44126 0 5874
total 93650 0 6350
ratio win lose draw
o 99.0% 0.0% 1.0%
x 88.3% 0.0% 11.7%
total 93.7% 0.0% 6.3%
ai_abs_dls
17.8 ms ± 341 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
timeit モジュールによる benchmark
の修正
%timeit
による処理時間の計測は JupyterLab 上でしか利用できない ので、先程定義した benchmark
を util.py に記述 し、benchmark
を インポートして呼び出す と エラーが発生する ことが判明したので、エラーが発生しないように修正します。具体的には、%timeit
と %%timeit
による処理は組み込みモジュールである timeit モジュールの関数を利用 しているので、timeit モジュールを利用して処理時間を計測するように修正します。
timeit モジュールの詳細については下記のリンク先を参照して下さい。
timeit モジュール
%timeit
や %%timeit
による処理時間の計測を行った際は、適切な時間内 で計測が行われるように、自動的に処理を行う回数が調整 されます。例えば下記のプログラムで 1 + 2
の処理時間の平均と標準偏差を計測すると、実行結果の () の中のメッセージ から 100,000,000 回の繰り返し処理 を 7 回行い、その平均と標準偏差が計算されたことがわかります。
%timeit 1 + 2
実行結果
10.1 ns ± 0.224 ns per loop (mean ± std. dev. of 7 runs, 100,000,000 loops each)
timeit モジュールでは repeat
という下記の処理を行う関数を利用することで %timeit と同様の処理 を行うことができます。
- 指定した処理 を、指定した回数(X) だけ繰り返した 処理時間を計測 する
- 上記の処理 を 指定した回数(Y) だけ行い、それぞれの 処理時間の一覧 を表す list を返り値として返す
上記の (X) と (Y) は下記の %timeit の表示の X と Y に対応 します。
10.1 ns ± 0.224 ns per loop (mean ± std. dev. of Y runs, X loops each)
返り値の処理時間の一覧 から、平均 と 標準偏差を計算 することで、%timeit と同様の処理 を行うことができます。%timeit
との大きな違い は、繰り返しの回数 を 自分で指定する必要がある 点です。
下記は repeat
の主な仮引数の意味 を表す表で、globals
については後で説明します。
仮引数 | 意味 |
---|---|
stmt |
処理時間を計測するプログラムを表す文字列 プログラムの文を意味する statement の略 |
number |
1 回の繰り返し処理で行う繰り返しの回数 |
repeat |
繰り返し処理を行う回数 |
globals |
処理を行う際に了する名前空間 |
下記は timeit モジュールの repeat
関数で、先程 %timeit で行われた場合と同様に 1 + 2
を 100,000,000 回繰り返す処理 を 7 回行った際 の それぞれの処理時間を表す list を表示 するプログラムです。なお、repeat
の返り値 の list の要素の 単位は秒 です。
import timeit
number = 10000000
repeat = 7
result = timeit.repeat(stmt="1 + 2", number=number, repeat=repeat)
print(result)
実行結果
[1.14166110008955, 1.1156697981059551, 1.1045803017914295, 1.1856296993792057, 1.1052100993692875, 1.1550979986786842, 1.1028691977262497]
1+2
の 1 回あたりの処理時間 はそれぞれの処理時間を number
で割った秒数 なので、下記のプログラムで計算することができます。
result = [time / number for time in result]
print(result)
実行結果
[1.14166110008955e-08, 1.115669798105955e-08, 1.1045803017914295e-08, 1.1856296993792057e-08, 1.1052100993692875e-08, 1.1550979986786842e-08, 1.1028691977262497e-08]
7 つ の 1 回あたりの処理時間 の 平均 と 標準偏差 は statistic モジュール の mean
と stdev
を利用して下記のプログラムで計算することができます。ns(ナノ秒)は 1000000000 分の 1 秒 なので、結果に 1000000000 を乗算して ns 単位で結果を表示 しました。
from statistics import mean, stdev
print(f"mean {mean(result) * 1000000000} ns")
print(f"stdev {stdev(result) * 1000000000} ns")
実行結果
mean 11.30102599305766 ns
stdev 0.31787355736488865 ns
下記は先ほどの %timeit と 上記 の 平均と標準偏差をまとめた表 です。下記の表の 両方の結果が大きく変わらない ことから、timeit.repeat
を利用して %timeit
とほぼ計算を行うことができる ことが確認できました。
平均 | 標準偏差 | |
---|---|---|
%timeit | 10.1 ns | 0.224 ns |
timeit.repeat |
11.3 ns | 0.318 ns |
timeit モジュールの repeat
の詳細は下記のリンク先を参照して下さい。
また、timeit には他にも様々な関数が定義されているので、興味がある方は上記のリンク先を参照して下さい。
名前空間に関する注意点
timeit.repeat
で実行する stmt
の処理 は timeit.repeat
の名前空間で実行される ため、グローバル名前空間 に登録された 変数名や関数名を利用 すると エラーが発 生します。
例えば下記のプログラムのように グローバル変数 a
の値を参照する a + 1
を timeit.repeat
で計算 しようとすると、実行結果のように a
が定義されていない(not defined) という エラーが発生 します。
a = 1
timeit.repeat("stmt=a+1", number=number, repeat=repeat)
実行結果
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[28], line 2
1 a = 1
----> 2 timeit.repeat("stmt=a+1", number=number, repeat=repeat)
略
NameError: name 'a' is not defined
グローバル変数 や グローバル関数 を 扱う処理 を timeit.repeat
で計算する場合は、キーワード引数 globals
に グローバル名前空間を代入する処理を記述 する必要があります。具体的には下記のプログラムの globals=globals()
のように グローバル名前空間を返り値として返す 組み込み関数 globals
の返り値をキーワード引数 globals
に代入 して呼び出すと、実行結果のようにエラーが発生しなくなります。
a = 1
print(timeit.repeat("stmt=a+1", number=number, repeat=repeat, globals=globals()))
実行結果
[2.256257101893425, 2.2410292997956276, 2.1918498016893864, 2.1492957025766373, 2.1939339004456997, 2.156707100570202, 2.1734426990151405]
組み込み関数 globals
の詳細については下記のリンク先を参照して下さい。
関数内で利用する場合の注意点
下記のプログラムのように timeit.repeat
を 関数の中で実行 する場合で、関数の ローカル変数の計算 を行う場合は globals=globals()
では実行結果のように エラーが発生 します。
def b():
c = 1
print(timeit.repeat("stmt=c+1", number=number, repeat=repeat, globals=globals()))
b()
実行結果
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[30], line 5
2 c = 1
3 print(timeit.repeat("stmt=c+1", number=number, repeat=repeat, globals=globals()))
----> 5 b()
Cell In[30], line 3
1 def b():
2 c = 1
----> 3 print(timeit.repeat("stmt=c+1", number=number, repeat=repeat, globals=globals()))
略
NameError: name 'c' is not defined
エラーが発生する理由は、timeit.repeat
で計算 する c
がグローバル変数ではなく、関数 b
の ローカル変数 だからです。このような場合は ローカル名前空間を返り値として返す 組み込み変数 locals
を利用して下記のプログラムのように キーワード引数に globals=locals()
を記述します。
def b():
c = 1
print(timeit.repeat("stmt=c+1", number=number, repeat=repeat, globals=locals()))
b()
実行結果
[2.2663614004850388, 2.2158589027822018, 2.2128161005675793, 2.1701188012957573, 2.165589399635792, 2.216084398329258, 2.170499600470066]
組み込み関数 locals
の詳細については下記のリンク先を参照して下さい。
グローバル変数とローカル変数の両方を利用する場合
benchmark
内 で %timeit
で処理時間を計測する下記の処理では、グローバル関数 ai_abs_dls
、ai14s
と ローカル変数 mb
、eval_params
のように グローバル名前空間とローカル名前空間の両方 が利用されています。
%timeit ai_abs_dls(mb, eval_func=ai14s, eval_params=eval_params, use_tt=True, maxdepth=8)
ai_abs_dls
や ai14s
のように timeit.repeat
で 処理を行う関数が他のモジュールで定義 されている場合は、timeit.repeat
の処理を行う前 で ローカルなインポート を行うことで、それらの関数がローカル名前空間に登録される ようになります。従って、benchmark
は下記のプログラムのように修正することができます。
-
4 行目:
timeit.repeat
の実引数に記述するnumber
とrepeat
の値を代入する仮引数を追加した。それぞれのデフォルト値は修正前のbenchmark
の %timeit で表示される(mean ± std. dev. of 7 runs, 10 loops each)
の表示からそれぞれ 10 と 7 とした3 -
8 行目:
benchmark
内で利用する AI の関数をローカルなインポートを行うようにした -
15 行目:
timeit.repeat
で計測する処理を表す文字列をstmt
に代入する -
17 ~ 19 行目:先程説明した方法で
timeit.repeat
の処理を行い、その平均と標準偏差を計算して表示する。結果は %timeit と同じフォーマットで表示されるようにした
1 import timeit
2 from statistics import mean, stdev
3
4 def benchmark(mbparams={}, match_num=50000, seed=0, number=10, repeat=7):
5 if seed is not None:
6 random.seed(seed)
7
8 from ai import ai2, ai14s, ai_match, ai_abs_dls
9
10 ai_match(ai=[ai2, ai2], match_num=match_num, mbparams=mbparams)
11 ai_match(ai=[ai14s, ai2], match_num=match_num, mbparams=mbparams)
12
13 mb = Marubatsu(**mbparams)
14 eval_params = {"minimax": True}
15 stmt = "ai_abs_dls(mb, eval_func=ai14s, eval_params=eval_params, use_tt=True, maxdepth=8)"
16 print("ai_abs_dls")
17 result = timeit.repeat(stmt=stmt, number=number, repeat=repeat, globals=locals())
18 result = [time / number for time in result]
19 print(f"{mean(result) * 1000:5.1f} ms ± {stdev(result) * 1000:5.1f} ms per loop (mean ± std. dev. of {repeat} runs, {number} loops each)")
行番号のないプログラム
import timeit
from statistics import mean, stdev
def benchmark(mbparams={}, match_num=50000, seed=0, number=10, repeat=7):
if seed is not None:
random.seed(seed)
from ai import ai2, ai14s, ai_match, ai_abs_dls
ai_match(ai=[ai2, ai2], match_num=match_num, mbparams=mbparams)
ai_match(ai=[ai14s, ai2], match_num=match_num, mbparams=mbparams)
mb = Marubatsu(**mbparams)
eval_params = {"minimax": True}
stmt = "ai_abs_dls(mb, eval_func=ai14s, eval_params=eval_params, use_tt=True, maxdepth=8)"
print("ai_abs_dls")
result = timeit.repeat(stmt=stmt, number=number, repeat=repeat, globals=locals())
result = [time / number for time in result]
print(f"{mean(result) * 1000:5.1f} ms ± {stdev(result) * 1000:5.1f} ms per loop (mean ± std. dev. of {repeat} runs, {number} loops each)")
修正箇所
import timeit
from statistics import mean, stdev
def benchmark(mbparams={}, match_num=50000, seed=0, number=10, repeat=7):
if seed is not None:
random.seed(seed)
+ from ai import ai2, ai14s, ai_match, ai_abs_dls
ai_match(ai=[ai2, ai2], match_num=match_num, mbparams=mbparams)
ai_match(ai=[ai14s, ai2], match_num=match_num, mbparams=mbparams)
mb = Marubatsu(**mbparams)
eval_params = {"minimax": True}
+ stmt = "ai_abs_dls(mb, eval_func=ai14s, eval_params=eval_params, use_tt=True, maxdepth=8)"
print("ai_abs_dls")
- %timeit ai_abs_dls(mb, eval_func=ai14s, eval_params=eval_params, use_tt=True, maxdepth=8)
+ result = timeit.repeat(stmt=stmt, number=number, repeat=repeat, globals=locals())
+ result = [time / number for time in result]
+ print(f"{mean(result) * 1000:5.1f} ms ± {stdev(result) * 1000:5.1f} ms per loop (mean ± std. dev. of {repeat} runs, {number} loops each)")
上記の修正後に下記のプログラムを実行すると、実行結果から timeit.result
の処理に対して修正前の %timeit と同様の表示 が行われることが確認できます。
benchmark()
実行結果
略
ai_abs_dls
19.1 ms ± 2.3 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
今回の記事のまとめ
今回の記事では ゲーム盤を表すデータ構造の違いによる処理速度の違いを比較 するための ベンチマークの設定 と、座標のチェックに関するプログラムの改良 を行いました。
また、ベンチマークを行うための関数を定義 していくつかの条件でベンチマークによる 処理速度を計測 しました。
本記事で入力したプログラム
リンク | 説明 |
---|---|
marubatsu.ipynb | 本記事で入力して実行した JupyterLab のファイル |
marubatsu.py | 本記事で更新した marubatsu_new.py |
util.py | 本記事で更新した util_new.py |
次回の記事