0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonで〇×ゲームのAIを一から作成する その194 ベンチマークの設定と関数の定義と timeit モジュールの使い方

Last updated at Posted at 2025-09-17

目次と前回の記事

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

getmarksetmarkjudge メソッドは 着手を行うたびに呼び出される ので、〇× ゲームの対戦を行う場合は 必ず頻繁に呼び出されます が、board_to_strcount_markpats特定の AI の関数 でしか呼び出されません。そこで、下記の 3 種類をベンチマークとして設定し、その処理速度を比較することにしました。

  • ai2 VS ai2 の対戦
    ランダムな着手を行う ai2合法手の中からランダムに選択 するという、最も行う処理が少ない AI の関数 なので、AI の関数の処理時間の影響を最も受けない状態 での対戦の 処理時間を計測 することができる。また、ランダムな着手を行うことで 多くの種類の局面に対する処理 が行われる。なお、これまでのベンチマークで利用してきた ai2s は、子ノードの評価値を計算する処理を行う ので ai2 よりも多くの処理を行う。そのため、下記のノートで示すように ai2 よりも処理速度が遅い ため 不採用 とした
  • ai14s VS ai2 の対戦
    count_markpats を呼び出す ai14sai2 の対戦を行うことで、count_markpats による処理時間の影響を比較 することができる。ai14s VS ai14s の対戦にしなかった理由 は、その場合に 生じる局面の種類が少ない ためであり、ランダムな着手を行う ai2 と対戦 を行うことで なるべく多くの種類の局面が生じる ようにした
  • ゲーム開始時の局面に対する置換表を利用した ai_abs_dls の計算
    board_to_str の処理時間の影響を比較 することができる。対戦を行わない理由は ai_abs_dls の処理時間が長いため、多くの対戦を行うと時間がかかるからである

下記はゲーム開始時の局面に対する ai2ai2s の処理時間を計測するプログラムで、実行結果のように 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_coordTrue の場合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_coordFalse の場合は self.board.setmark を呼び出して座標のチェックを行わずに着手を行う
  • 4、5 行目check_coordTrue の場合は 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 で仮引数 xy を整数型のデータに型変換を行う
  • 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 を定義したプログラムです。この、benchmarkutil.py に記述 することにします。

  • 4 行目:上記の仮引数を持つ benchmark を定義する
  • 5、6 行目seedNone でない場合に乱数の種の初期化を行う
  • 8 行目ai_matchai2 vS ai2 の対戦を行う
  • 9 行目ai_matchai14s vS ai2 の対戦を行う
  • 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)

ベンチマークの実行

下記のそれぞれの組み合わせに対してベンチマークを実行します。

  • ゲーム盤のデータ構造を表すクラスとして ListBoardList1dBoard を利用する場合
  • 直線上のマークの数を 数える 場合と 数えない 場合

下記は上記の組み合わせでベンチマークを行うプログラムです。

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)

下記は実行結果をまとめた表です。なお、ai2ai2s よりも処理速度が 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 VS ai2 の処理速度はほぼかわらない2 ので、対戦における getmarksetmarkjudge の処理時間の合計 は ListBoard と List1dBoard によるゲーム盤のデータ構造の違いや、直線上のマークを数えるかどうかによって ほぼ変化しない
  • count_markpats を呼び出す ai14s VS ais の対戦では 直線上のマークを数える ことで 1 秒あたりの 処理回数が 約 2 倍 になるので、count_markpats を利用する場合直線上のマークを数えたほうが処理速度が速くなる
  • すべての条件board_to_str を利用する ai_abs_dls の処理速度はほぼかわらない ので、board_to_str の処理時間 は ListBoard と List1dBoard によるゲーム盤のデータ構造の違いや、直線上のマークを数えるかどうかによって ほぼ変化しない

乱数の種の影響の確認

ai2 VS ai2ai14s 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 + 2100,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+21 回あたりの処理時間 はそれぞれの処理時間を 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 モジュールmeanstdev を利用して下記のプログラムで計算することができます。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 + 1timeit.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_dlsai14sローカル変数 mbeval_params のように グローバル名前空間とローカル名前空間の両方 が利用されています。

%timeit ai_abs_dls(mb, eval_func=ai14s, eval_params=eval_params, use_tt=True, maxdepth=8)    

ai_abs_dlsai14s のように timeit.repeat処理を行う関数が他のモジュールで定義 されている場合は、timeit.repeat の処理を行う前ローカルなインポート を行うことで、それらの関数がローカル名前空間に登録される ようになります。従って、benchmark は下記のプログラムのように修正することができます。

  • 4 行目timeit.repeat の実引数に記述する numberrepeat の値を代入する仮引数を追加した。それぞれのデフォルト値は修正前の 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

次回の記事

  1. これまでの記事では「1 秒間の対戦回数の平均」と表記してきましたが、「1 秒あたりの対戦回数」のほうが正確だと思いましたので今後はそのように表記することにします

  2. 若干の違いはありますが、この程度の差は誤差の範囲だと思います

  3. ai_abs_dls の処理時間は 1 + 2 と比べて長いので、number100000000 のような値にするといつまでたっても処理が終わらなくなる点に注意して下さい

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?