目次と前回の記事
Python のバージョンとこれまでに作成したモジュール
本記事のプログラムは Python のバージョン 3.13 で実行しています。また、numpy のバージョンは 2.3.5 です。
以下のリンクから、これまでに作成したモジュールを見ることができます。本文で説明しますが、下記の ai.py は前回のファイルから修正を行っています。
| リンク | 説明 |
|---|---|
| marubatsu.py | Marubatsu、Marubatsu_GUI クラスの定義 |
| ai.py | AI に関する関数 |
| mbtest.py | テストに関する関数 |
| util.py | ユーティリティ関数の定義 |
| tree.py | ゲーム木に関する Node、Mbtree クラスなどの定義 |
| gui.py | GUI に関する処理を行う基底クラスとなる GUI クラスの定義 |
AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。
NpBoolBoard の定義の続き
今回の記事は以前の記事で定義を開始した NpBoolBoard の定義の続き を行います。具体的には ai14s の実行に必要な count_parkpats の定義を行います。なお、ai_abs_dls の実行に必要な calc_same_boardtexts メソッドに関しては次回以降の記事で定義する予定です。
確認のためのゲーム盤のデータの作成
プログラムが 正しく動作するかどうかを確認 するための ゲーム盤のデータ を下記のプログラムで作成します。0 行、0 列、斜め方向に それぞれ異なるマークが並ぶ ようにしました。
from marubatsu import NpBoolBoard, Marubatsu
mb = Marubatsu(boardclass=NpBoolBoard)
mb.cmove(0, 0)
mb.cmove(1, 0)
mb.cmove(0, 1)
mb.cmove(2, 0)
mb.cmove(1, 2)
mb.cmove(1, 1)
mb.cmove(0, 2)
print(mb)
実行結果
winner o
oxx
ox.
Oo.
count_markpats メソッドの定義
マークのパターンの一覧 を計算する count_markpats メソッドを定義します。なお、count_linemark が True の場合の 処理は変わらない ので説明は省略します。
count_markpats メソッドは、ゲーム盤の それぞれの直線上のマークの一覧 を表すデータを計算し、count_markpat メソッドで そのデータからマークのパターンを計算 するという処理を行います。そこで、それぞれの直線上のマークの一覧を計算 する処理を、3 次元の ndarray で表現されたゲーム盤に対して 行う方法 について少し考えてみて下さい。
i 行と i 列のデータの計算方法
NpBoard クラスの count_markpats メソッドは 2 次元の ndarray から i 列 のマークのパターンと i 行 のマークのパターンを 計算するために必要な i 列と i 行のデータ を下記のプログラムで計算しています。
self.board[i, :] # i 列のデータ
self.board[:, i] # i 行のデータ
NpBoolBoard クラスの場合は、3 次元の ndarray のゲーム盤の i 列と i 行のデータ を下記のプログラムで計算することができます。下記のプログラムの意味について忘れた方は前回の記事を復習して下さい。なお、前回の記事では axis 1 を縦方向の行の番号、axis 2 を横方向の列の番号 とした 図で表現 しましたが、〇× ゲーム を表す 3 次元の ndarray の場合は axis 1 を横方向の列の番号、axis 2 を縦方向の行の番号 としている点に注意して下さい。
self.board[:, i, :] # i 列のデータ
self.board[:, :, i] # i 行のデータ
下記は先ほど作成した mb の局面 の 0 列 のデータを表す 2 次元の ndarray を表示 するプログラムで、前回の記事で説明したように、計算された 2 次元の ndarray の axis 0 がマークの種類 を、axis 1 が列の番号 を表します。実行結果から 〇 が 3 つ並ぶ 0 列 のデータが 正しく計算できている ことが確認できます。
print(mb.board.board[:, 0, :])
実行結果(# 以降のコメントは筆者が追記しました。以後も同様です)
[[ True True True] # 0 列の 〇 のデータ。〇 が 3 つ並ぶことを表す
[False False False]] # 0 列の × のデータ。× が配置されないことを表す
下記は 計算前と計算後 の ndarray の axis の対応表 です。
| 計算前 | 計算後 | 意味 |
|---|---|---|
| axis 0 | axis 0 | マークの種類 |
| axis 1 | なし | 列の番号 |
| axis 2 | axis 1 | 行の番号 |
下記は、先ほど作成した mb の局面 の 0 行 のデータを表示するプログラムで、実行結果から「〇、×、×」が並ぶ 0 行のデータが正しく計算できている ことが確認できます。
print(mb.board.board[:, :, 0])
実行結果
[[ True False False] # 0 行の 〇 のデータ。最初のマスのみに 〇 が配置されることを表す
[False True True]] # × の 0 行のデータ。2、3 番目のマスに × が配置されることを表す
下記は 計算前と計算後 の ndarray の axis の対応表 です。
| 計算前 | 計算後 | 意味 |
|---|---|---|
| axis 0 | axis 0 | マークの種類 |
| axis 1 | axis 1 | 列の番号 |
| axis 2 | なし | 行の番号 |
対角線 1(左上から右下の斜め方向)のデータの計算方法
NpBoard クラスでは 左上から右下 の斜め方向の直線上のデータを以前の記事で説明した 対角線の要素を計算 する np.diag を利用した下記のプログラムで計算しました。
np.diag(self.board)
左上から右下の斜め方向 という表現は 冗長 なので、以後は 以下の表のように表記 することにします。別の言葉で説明すると、〇× ゲームの 対角線 1 の要素は x 座標と y 座標が等しい要素 の事を、対角線 2 はもう片方の対角線の要素を表します。なお、この後で説明しますが、対角線 2 は右上から左下ではなく 左下から右上方向 に 要素を列挙 します。
| 方向 | 表記 |
|---|---|
| 左上から右下方向 | 対角線 1 |
| 左下から右上方向 | 対角線 2 |
3 次元の ndarray の 対角線 1 のデータは、下記のプログラムのように 〇 と × のマーク を表す それぞれの 2 次元の ndarray を計算し、それぞれのデータに対して np.diag を利用 することで計算することができます。実行結果から「〇、×、なし」が並ぶ対角線 1 のデータが 正しく計算できている ことが確認できます。
import numpy as np
print(np.diag(mb.board.board[0, :, :])) # 〇 のマークを表す 2 次元の ndarray
print(np.diag(mb.board.board[1, :, :])) # × のマークを表す 2 次元の ndarray
実行結果
[ True False False] # 対角線 1 の 〇 のデータ
[False True False] # 対角線 1 の × のデータ
上記では 〇 と × のマークを表す 2 次元の ndarray を それぞれ計算 し、それぞれに対して np.diag を実行 して計算を行いましたが、実は np.diagonal という関数を利用することで 対角線 1 のマークを表すデータを まとめて一度に計算 することができます。
np.diagonal の利用方法
np.diagonal は 2 次元以上 の ndarray から、指定した 2 つの axis を基準 とした 対角線 1 の要素 を取り出した ndarray を計算 する関数です。
2 つの axis は仮引数 axis1 と axis2 で 番号を指定 し、下記のような計算が行われます。
- 仮引数
axis1とaxis2で指定した 2 つの axis のインデックスが同じ である すべての要素 を計算する - 計算される ndarray の axis は、
axis1とaxis2で指定された axis を取り除き、最後に対角線 1 の要素 を表す axis を追加する - 2 つの axis を取り除き、1 つの axis を追加するので、結果として計算される ndarray の 次元 は元の ndarray の次元より 1 小さくなる
仮引数の名前 である axis1 と axis2 は np.diagonal に対して指定する 2 つの axis のうちの 1 番目と 2 番目のデータ であることを表しています。表記が似ているので紛らわしいですが、axis1 の 1 は 1 番目に指定する axis が axis 1 であるという意味ではない 点に注意して下さい。例えば axis1=2, axis2=0 と記述した場合は 1 番目の axis として axis 2 を、2 番目の axis として axis 0 を指定したことになります。
本記事では 仮引数 axis1 を 網掛け で、1 番の axis を axis 1 のように 網掛けをせずに表記 することで区別することにします。
下記は np.diagonal で NpBoolBoard の 〇× ゲームのゲーム盤と同じ (2, 3, 3) の形状 を持つ 3 次元の ndarray に対して 〇× ゲームの対角線 1 の要素を表すデータを計算して表示するプログラムです。〇× ゲームのデータの要素は True と False の 2 種類しかないためわかりづらいと思いましたので、このようなデータで計算を行いました。
na3d = np.array([[[1, 2, 3], [4, 5, 6], [7, 8, 9]],
[[10, 11, 12], [13, 14, 15], [16, 17, 18]]])
print(na3d)
print()
print("np.diagonal")
print(np.diagonal(na3d, axis1=1, axis2=2))
実行結果
[[[ 1 2 3]
[ 4 5 6]
[ 7 8 9]]
[[10 11 12]
[13 14 15]
[16 17 18]]]
np.diagonal
[[ 1 5 9]
[10 14 18]]
下図は上記の np.diagonal が計算する ndarray の要素を図示 したものです。赤線 の axis 1 と axis 2 を基準 とした、axis 1 と axis 2 の インデックスが同じ である 水色 の 対角線 1 に対応する要素 を持つ 2 次元の ndarray が計算されます。
下記は 計算された 2 次元の ndarray の図です。
下記は 計算前と計算後 の ndarray の axis の対応表 です。元の axis 1 と axis 2 が削除 され、〇× ゲームの対角線 1 の要素の 番号 を表す axis 1 が追加 されます。np.diagonal などの 計算前と計算後の ndarray の次元が異なる 処理は、計算前と計算後の axis の対応 が わかりづらい と思う人が多いのではないかと思います。そのような場合は上記のような 図を書いたり、下記のような 対応表を作成 すると良いでしょう。
| 計算前 | 計算後 | 〇×ゲームのゲーム盤としての意味 |
|---|---|---|
| axis 0 | axis 0 | マークの種類 |
| axis 1 | なし | 列の番号 |
| axis 2 | なし | 行の番号 |
| なし | axis 1 | 対角線 1 の要素の番号 |
下記は np.diagonal で mb の局面の 対角線 1 のマークを表すデータを計算して表示するプログラムです。実行結果から、先程の np.diag による計算と 同様の内容 が 2 次元の ndarray として計算 されていることが確認できます。
print(np.diagonal(mb.board.board, axis1=1, axis2=2))
実行結果
[[ True False False]
[False True False]]
np.diagonal の詳細については下記のリンク先を参照して下さい。
対角線 1 の要素を計算する際に、下記のプログラムのように axis1 と axis2 の順番を入れ替えても結果は変わりません。
print(np.diagonal(mb.board.board, axis1=2, axis2=1))
実行結果
[[ True False False]
[False True False]]
本記事では利用しませんが、np.diagonal には offset という、対角線 1 と平行な斜めの直線上の要素を計算する仮引数があり、offset が正の整数の場合は axis2 の正の方向に、負の整数の場合は axis1 の正の方向に offset の絶対値の値だけ対角線 1 からずらした要素が計算されます。
na = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(na)
print("offset = 0, axis1 = 0, axis2 = 1")
print(np.diagonal(na, offset=0, axis1=0, axis2=1))
print("offset = 1, axis1 = 0, axis2 = 1")
print(np.diagonal(na, offset=1, axis1=0, axis2=1))
print("offset = -1, axis1 = 0, axis2 = 1")
print(np.diagonal(na, offset=-1, axis1=0, axis2=1))
実行結果
[[1 2 3]
[4 5 6]
[7 8 9]]
offset = 0, axis1 = 0, axis2 = 1
[1 5 9]
offset = 1, axis1 = 0, axis2 = 1
[2 6]
offset = -1, axis1 = 0, axis2 = 1
[4 8]
下図は上記を図示したものです。
offset に 0 以外を指定した場合は、下記のプログラムのように axis1 と axis2 の値を逆にすると offset の正負を入れ替えた場合と同じ計算が行われます。
na = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(na)
print("offset = 0, axis1 = 1, axis2 = 0")
print(np.diagonal(na, offset=0, axis1=1, axis2=0))
print("offset = 1, axis1 = 1, axis2 = 0")
print(np.diagonal(na, offset=1, axis1=1, axis2=0))
print("offset = -1, axis1 = 1, axis2 = 0")
print(np.diagonal(na, offset=-1, axis1=1, axis2=0))
実行結果
[[1 2 3]
[4 5 6]
[7 8 9]]
offset = 0, axis1 = 1, axis2 = 0
[1 5 9]
offset = 1, axis1 = 1, axis2 = 0
[4 8]
offset = -1, axis1 = 1, axis2 = 0
[2 6]
処理時間の比較
上記の 2 回の np.diag と、1 回の np.diagonal の対角線 1 の計算の 処理時間を比較 することにします。下記は np.diag での処理時間を計測するプログラムです。
%%timeit
np.diag(mb.board.board[0])
np.diag(mb.board.board[1])
実行結果
2.96 μs ± 78.5 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
下記は np.diagonal での処理時間を計測するプログラムです。
%timeit np.diagonal(mb.board.board, axis1=1, axis2=2)
実行結果
940 ns ± 36.2 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
実行結果から、np.diagonal を利用したほうが 処理時間が約 1/3 になることが確認できました。以前の記事で 2 次元の np.bool の ndarray を要素とする dict ではなく、3 次元の np.bool の ndarray でのゲーム盤の表現を 採用した理由 は、上記のように 〇 と × を表すデータ を まとめて高速に計算 することができるからです。また、この後や次回の記事で説明するように np.flip など 多くの numpy の関数 も 同様の性質 を持ちます。
対角線 2(左下から右上の斜め方向)のデータの計算方法
NpBoard クラスでは 対角線 2 のデータを np.diag と以前の記事で説明した 左右を反転 する np.fliplr を利用した下記のプログラムで計算しました。
np.diag(np.fliplr(self.board))
なお、np.fliplr は名前の中の lr が left と right の頭文字 であることから、2 次元の ndarray の 左右を反転 する処理を行う関数ですが、それは 2 次元の ndarray の axis 0 を上下方向、axis 1 を左右方向に図示 した場合に 左右に反転 するという意味です。NpBoard クラスの 〇× ゲームのゲーム盤を表す 2 次元の ndarray は axis 0 が 左右方向 の x 軸を、axis 1 が上下方向 の y 軸を表すので、np.fliplr によってゲーム盤の 上下が反転される 点に注意が必要です。上記のプログラムでは np.fliplr によってゲーム盤を 上下に反転した後 で np.diag によって 対角線 1 のマスが計算 されるので、計算された 1 次元の ndarray の要素には 左下から右上方向の順 にマスの値が代入されます。
np.fliplr が行う処理
以前の記事では説明していませんでしたが、np.fliplr は axis 1 の軸を反転 した ndarray を計算 するという処理を行う関数です。下記は 2 次元の ndarray に対する np.fliplr の計算を行うプログラムです。
print(np.fliplr(na))
実行結果
[[3 2 1]
[6 5 4]
[9 8 7]]
上記の np.fliplr では下図のように 赤色の axis 1 の軸の反転 の処理が行われます。下図では ndarray の中身はそのまま に、axis 1 だけを反転 している点に注意して下さい。
前回の記事で説明したように、ndarray を図示 する際に、軸の方向や向きを変えても同じデータを表す ので、上図の 右の 2 次元の ndarray を 軸と中身を左右に反転 すると 下図 のように print で 2 次元の ndarray を表示 場合と同様に axis 0 が上下方向、axis 1 が左右方向 として図示されるようになります。先程の実行結果 と 下図の要素の並びが一致 することを確認して下さい。
3 次元の ndarray に対して np.fliplr が行う処理
np.fliplr は 3 次元以上 の ndarray に対しても axis 1 を反転 した ndarray を計算 する処理を行います。下記のプログラムは先ほどの mb のゲーム盤のデータ に対して np.fliplr の計算を行うプログラムです。
print("元のデータ")
print(mb.board.board)
print()
print("np.fliplr の結果")
print(np.fliplr(mb.board.board))
実行結果
元のデータ
[[[ True True True] # 〇 のマークを表すデータ
[False False True]
[False False False]]
[[False False False] # × のマークを表すデータ
[ True True False]
[ True False False]]]
np.fliplr の結果
[[[False False False] # 〇 のマークを表すデータ
[False False True]
[ True True True]]
[[ True False False] # × のマークを表すデータ
[ True True False]
[False False False]]]
先程の 2 次元の ndarray に対する np.fliplr によって 要素が左右に反転 したのに対して、上記の 3 次元の ndarray に対する np.fliplr では 〇 と × のマークを表す それぞれの 2 次元の ndarray の要素が 上下に反転 します。そのような計算が行われる理由は、上記の np.fliplr では下図のように 赤色の axis 1 の軸の反転 の処理が行われるからです。
上図の右の 3 次元の ndarray を axis 1 と中身を上下に反転 すると下図のようになり、元の 3 次元の ndarray と比較 して 上下が反転 します。
上記の実行結果からわかるように、np.flip を利用することで、〇 と × を表すデータ を まとめて axis 1 で反転 することができます。
また、上図の 3 次元の ndarray に対して 左右を反転 する処理を行うためには、axis 1 ではなく axis 2 を反転する必要がある ことがわかります。
np.fliplr と類似する処理を行う np.flipud は axis 0 で反転を行う関数です。
np.flip による軸の反転処理
3 次元の ndarray の axis 2 を反転 した ndarray の計算は、ndarray の 任意の軸を反転 する np.flip を利用することで計算できます。np.flip は axis という仮引数 で 反転する axis を下記のように 指定 します。
- 整数 を記述した場合は、記述した番号の axis の反転 を行う
- tuple を記述 した場合は、tuple の すべての要素の番号の axis の反転 を行う
- キーワード引数
axisを記述しない 場合は すべての axis の反転 を行う
従って、下記のプログラムのように axis=2 をキーワード引数に記述して np.flip を呼び出すことで、ゲーム盤を表す 3 次元の ndarray に対して、NpBoard クラスでゲーム盤を表現する 2 次元の ndarray に対して np.fliplr が行うのと同様の処理 を行うことができます。実行結果から 左右が反転 していることを確認して下さい。
print("元のデータ")
print(mb.board.board)
print()
print("np.flip の結果")
print(np.flip(mb.board.board, axis=2))
実行結果
元のデータ
[[[ True True True]
[False False True]
[False False False]]
[[False False False]
[ True True False]
[ True False False]]]
np.flip の結果
[[[ True True True]
[ True False False]
[False False False]]
[[False False False]
[False True True]
[False False True]]]
np.flip の詳細については下記のリンク先を参照して下さい。
np.fliplr は np.flip のキーワード引数に axis=1 を記述した場合と同じ処理を行います。np.flipud は axis=0 を記述した場合と同様です。
対角線 2 のデータの計算方法
対角線 2 のデータの計算は、下記のプログラムのように上記の np.flip と np.diagonal を組みあわせる ことで計算することができます。実行結果から 左下から右上 に「〇、×、×」が並ぶ 対角線 2 のデータが正しく計算できている ことが確認できます。
print(np.diagonal(np.flip(mb.board.board, axis=2), axis1=1, axis2=2))
実行結果
[[ True False False] # 対角線 2 の 〇 のデータ
[False True True]] # 対角線 2 の × のデータ
count_markpats メソッドの定義
下記は count_markpats メソッドの定義で、それぞれの直線上のマスを 上記で説明した方法で計算 するように NpBoard クラスの count_markpats から修正しました。なお、self.count_linemark が True の場合の処理は NpBoard クラスと同じです。
-
8、10 行目:
count_marksでi列のマークのパターンを計算するために必要な情報はself.board[:, i, :]なので、それをcount_marksの実引数に記述するように修正した。10 行目のi行のマークのパターンを計算する処理も同様 - 13、16、17 行目:先程説明した方法で対角線 1 と 対角線 2 を計算するように修正した
1 def count_markpats(self, turn, last_turn):
2 markpats = defaultdict(int)
3
4 if self.count_linemark:
元と同じなので省略
5 else:
6 # 横方向と縦方向の判定
7 for i in range(self.BOARD_SIZE):
8 count = self.count_marks(self.board[:, i, :], turn, last_turn)
9 markpats[count] += 1
10 count = self.count_marks(self.board[:, :, i], turn, last_turn)
11 markpats[count] += 1
12 # 左上から右下方向の判定
13 count = self.count_marks(np.diagonal(self.board, axis1=1, axis2=2), turn, last_turn)
14 markpats[count] += 1
15 # 右上から左下方向の判定
16 count = self.count_marks(np.diagonal(np.flip(self.board, axis=2),
17 axis1=1, axis2=2), turn, last_turn)
18 markpats[count] += 1
19
20 return markpats
21
22 NpBoolBoard.count_markpats = count_markpats
行番号のないプログラム
def count_markpats(self, turn, last_turn):
markpats = defaultdict(int)
if self.count_linemark:
for countdict in [self.rowcount, self.colcount, self.diacount]:
for circlecount, crosscount in zip(countdict[self.CIRCLE], countdict[self.CROSS]):
emptycount = self.BOARD_SIZE - circlecount - crosscount
if last_turn == self.CIRCLE:
markpats[(circlecount, crosscount, emptycount)] += 1
else:
markpats[(crosscount, circlecount, emptycount)] += 1
else:
# 横方向と縦方向の判定
for i in range(self.BOARD_SIZE):
count = self.count_marks(self.board[:, i, :], turn, last_turn)
markpats[count] += 1
count = self.count_marks(self.board[:, :, i], turn, last_turn)
markpats[count] += 1
# 左上から右下方向の判定
count = self.count_marks(np.diagonal(self.board, axis1=1, axis2=2), turn, last_turn)
markpats[count] += 1
# 右上から左下方向の判定
count = self.count_marks(np.diagonal(np.flip(self.board, axis=2),
axis1=1, axis2=2), turn, last_turn)
markpats[count] += 1
return markpats
NpBoolBoard.count_markpats = count_markpats
修正箇所
def count_markpats(self, turn, last_turn):
markpats = defaultdict(int)
if self.count_linemark:
元と同じなので省略
else:
# 横方向と縦方向の判定
for i in range(self.BOARD_SIZE):
- count = self.count_marks(self.board[i, :], turn, last_turn)
+ count = self.count_marks(self.board[:, i, :], turn, last_turn)
markpats[count] += 1
- count = self.count_marks(self.board[:, i], turn, last_turn)
+ count = self.count_marks(self.board[:, :, i], turn, last_turn)
markpats[count] += 1
# 左上から右下方向の判定
- count = self.count_marks(np.diag(self.board), turn, last_turn)
+ count = self.count_marks(np.diagonal(self.board, axis1=1, axis2=2), turn, last_turn)
markpats[count] += 1
# 右上から左下方向の判定
- count = self.count_marks(np.diag(np.fliplr(self.board)), turn, last_turn)
+ count = self.count_marks(np.diagonal(np.flip(self.board, axis=2),
+ axis1=1, axis2=2), turn, last_turn)
markpats[count] += 1
return markpats
NpBoolBoard.count_markpats = count_markpats
count_markpat メソッドの定義
NpBoard クラスの calc_markpats では 直線上のマーク を表すデータを 1 次元の ndarray で表現 していましたが、NpBoolBoard クラスの calc_markpats では直線上のマークを表すデータを 2 次元の ndarray で表現 するので、指定した直線上のマークのパターンを計算 する count_markpat を下記のプログラムのように 修正する必要が あります。
NpBoard クラスの calc_markpat では以前の記事 で説明した Counter クラス を利用して 仮引数 linedata に代入された 1 次元の ndarray の要素に代入されたそれぞれの マークの数を数えるという処理を行いました。
NpBoolBoard クラスの場合は calc_markpat の 仮引数 linedata には 〇 と × のそれぞれのマークに対して マークが配置されている場合は True、配置されていない場合は False が代入された np.bool の 2 次元の ndarray が代入されるので、Counter クラス を利用してそれぞれの マークの数を数えることはできません。
以前の記事で説明したように np.bool の ndarray では True を整数の 1、False を整数の 0 として記録しているので、下記のプログラムのように マークに対応する要素 に対して np.count_nonzero を利用して マークの数を数える ことができます。
-
2、3 行目:
turnの手番のマークのデータはlinedata[turn]が表すnp.boolの 1 次元の ndarray 記録されているので、np.count_nonzeroでturnの手番のマークの数を計算することができる。3 行目の直前の手番のマークの数の計算も同様の方法で計算できる -
4、5 行目:この部分の処理は NpBoard クラスの
calc_markpatと同じ
1 def count_marks(self, linedata, turn, last_turn):
2 turn_count = np.count_nonzero(linedata[turn])
3 lastturn_count = np.count_nonzero(linedata[last_turn])
4 emptycount = self.BOARD_SIZE - turn_count - lastturn_count
5 return lastturn_count, turn_count, emptycount
6
7 NpBoolBoard.count_marks = count_marks
行番号のないプログラム
def count_marks(self, linedata, turn, last_turn):
turn_count = np.count_nonzero(linedata[turn])
lastturn_count = np.count_nonzero(linedata[last_turn])
emptycount = self.BOARD_SIZE - turn_count - lastturn_count
return lastturn_count, turn_count, emptycount
NpBoolBoard.count_marks = count_marks
修正箇所
def count_marks(self, linedata, turn, last_turn):
- counter = Counter(linedata.tolist())
- turn_count = counter[turn]
+ turn_count = np.count_nonzero(linedata[turn])
- lastturn_count = counter[last_turn]
+ lastturn_count = np.count_nonzero(linedata[last_turn])
emptycount = self.BOARD_SIZE - turn_count - lastturn_count
return lastturn_count, turn_count, emptycount
NpBoolBoard.count_marks = count_marks
上記の定義後に、下記のプログラムで mb の局面のマークのパターンを計算 して表示することで、プログラムが 正しく実装されていることを確認 します。
from pprint import pprint
print(mb)
pprint(mb.count_markpats())
実行結果
winner o
oxx
ox.
Oo.
defaultdict(<class 'int'>,
{(np.int64(0), np.int64(1), np.int64(2)): 1,
(np.int64(1), np.int64(1), np.int64(1)): 2,
(np.int64(1), np.int64(2), np.int64(0)): 3,
(np.int64(2), np.int64(0), np.int64(1)): 1,
(np.int64(3), np.int64(0), np.int64(0)): 1})
下記は上記の実行結果をまとめた表です。上記で表示される ゲーム盤と見比べて、正しい計算が行われていることを確認 して下さい。なお、np.int64(1) は numpy の np.int64 型の整数の 1 を表します。
| 〇 | × | 空 | 数 |
|---|---|---|---|
| 0 | 1 | 2 | 1 |
| 1 | 1 | 1 | 2 |
| 1 | 2 | 0 | 3 |
| 2 | 0 | 1 | 1 |
| 3 | 0 | 0 | 1 |
処理速度の計測
count_markpats を定義 したことで、ai14s の処理を行うことができる ようになりましたので、下記のプログラムで ai14s VS ai2 の対戦 を行い、その 処理速度を計測 することにします。なお、benchmark と同じ対戦結果 になるように random.seed(0) を実行して 乱数の種を初期化 し、ai2 VS ai2 の対戦の後に ai14s VS ai2 の対戦を行いました。実行結果の 対戦成績が benchmark と同じ結果になる ことから今回の記事で定義した count_markpats が正しい処理を行う ことが確認できました。
from ai import ai_match, ai2, ai14s
import random
random.seed(0)
ai_match(ai=[ai2, ai2], match_num=50000, mbparams={"boardclass": NpBoolBoard})
ai_match(ai=[ai14s, ai2], match_num=50000, mbparams={"boardclass": NpBoolBoard})
random.seed(0)
ai_match(ai=[ai2, ai2], match_num=50000, mbparams={"boardclass": NpBoolBoard, "count_linemark": True})
ai_match(ai=[ai14s, ai2], match_num=50000, mbparams={"boardclass": NpBoolBoard, "count_linemark": True})
実行結果
ai2 VS ai2
100%|██████████| 50000/50000 [00:09<00:00, 5555.23it/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 [01:04<00:00, 779.70it/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%
ai2 VS ai2
100%|██████████| 50000/50000 [00:07<00:00, 6596.47it/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:27<00:00, 1807.04it/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%
下記は以前の記事の NpBoard、NpIntBoard クラスを利用した場合の ai14 VS ai2 のベンチマークの結果に 上記の結果を加えた表 です。ai2 VS ai2 の対戦の処理速度に関しては以前の記事で検証済なので省略しました。
boardclass |
count_linemark |
ai14 VS ai2
|
|---|---|---|
| NpBoard | False |
692.95 回/秒 |
| NpIntBoard | False |
723.75 回/秒 |
| NpBoolBoard | False |
779.70 回/秒 |
| NpBoard | True |
1994.42 回/秒 |
| NpIntBoard | True |
2030.51 回/秒 |
| NpBoolBoard | True |
1807.04 回/秒 |
上記から、今回の記事で実装した処理が行われる count_linemark が False の場合の 処理速度 は、NpBoard クラスよりはほんの少し高速 になりますが、NpIntBoard クラスとほぼ変わらない ことが確認できました。
また、count_linemark が True の場合は以前の記事で検証した ai2 VS ai2 の処理速度の検証 と 同様の理由 から NpBoard や NpBoolBoard クラスよりも 処理速度が遅くなります。
なお、count_markpats を改良することで 高速化 することができます。次回の記事でその高速化を行い、改めてその処理速度を検証することにします。
今回の記事のまとめ
今回の記事では np.diagonal と np.flip を利用することで NpBoolBoard クラスの 3 次元の ndarray で表現されるゲーム盤 に対する 各直線のマークのパターンを計算する方法 について説明しました。また、NpBoolBoard クラスの count_markpats の定義を行い、count_linemark が False の場合の ai14s VS ai2 の対戦の処理速度が NpBoard の場合よりも少しだけ高速 になることを確認しました。
次回の記事では count_markpats の処理速度の高速化と、残りの calc_same_boardtexts の実装を行う予定です。
本記事で入力したプログラム
| リンク | 説明 |
|---|---|
| marubatsu.ipynb | 本記事で入力して実行した JupyterLab のファイル |
| marubatsu.py | 本記事で更新した marubatsu_new.py |
次回の記事
近日公開予定です