目次と前回の記事
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 の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。
ndarray のデータ型による処理速度の違い
今回の記事では最初に前回の記事で説明できなかった ndarray のデータ型 による 処理速度の違い について説明します。
numpy には ndarray の要素の合計を計算する np.sum や、0 でない要素の数を数える np.count_nonzero などの ndarray に対する様々な計算を行う関数 が用意されていますが、それらの関数の 処理速度 は ndarray のデータ型によって変化 します。そのため、処理速度が重要 な処理を行う場合は 適切なデータ型を選択 する必要があります。
なお、処理速度が重要でない場合 は、整数 を扱う場合は np.int64、浮動小数点数 を扱う場合は np.float64 を選択すればよいでしょう。
データ型の違いによる処理速度の違い を実際に示すことにします。下記のプログラムは下記の条件の組み合わせで処理速度を計測するプログラムです。文字列を扱う場合は基本的には np.str を利用するしかないので np.str は除外しました。
- ndarray のデータ型:前回の記事で紹介した整数型、浮動小数点数型、
np.bool - ndarray の要素の数:10、100、10000、10000、100000
- 関数:
np.sum、np.count_nonzero
なお、コンピューターの CPU、Python のバージョン、numpy のバージョンなど、コンピュータの環境によって 下記とは 大きく異なる実行結果になる場合 があります。その場合は本記事の検証結果は適用できない 点に注意して下さい。興味がある方は自分のパソコンで上記のプログラムを実行して処理時間を計測してみると良いでしょう。
また、np.sum と np.count_nonzero 以外の関数では異なる結果になる場合がある ので、興味がある方は調べてみて下さい。
import numpy as np
funclist = { np.sum, np.count_nonzero }
sizelist = [ 10, 100, 1000, 10000, 100000 ]
dtypelist = [ np.int8, np.int16, np.int32, np.int64,
np.float16, np.float32, np.float64, np.bool ]
for func in funclist:
print(func.__name__)
for dtype in dtypelist:
print(f"dtype: {dtype.__name__}")
for size in sizelist:
print(f"size: {size}")
ndarray = np.zeros((size,), dtype=dtype)
%timeit func(ndarray)
print()
print()
実行結果
count_nonzero
dtype: int8
size: 10
298 ns ± 14.2 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
size: 100
292 ns ± 9.41 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
size: 1000
396 ns ± 12.1 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
size: 10000
682 ns ± 15.1 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
size: 100000
3.63 μs ± 234 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
dtype: int16
size: 10
291 ns ± 3.14 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
size: 100
314 ns ± 13.8 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
size: 1000
429 ns ± 11.8 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
size: 10000
966 ns ± 15.9 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
size: 100000
6.34 μs ± 152 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
dtype: int32
size: 10
288 ns ± 3.15 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
size: 100
308 ns ± 11 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
size: 1000
485 ns ± 2.4 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
size: 10000
1.6 μs ± 13.8 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
size: 100000
12.1 μs ± 225 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
dtype: int64
size: 10
288 ns ± 2.45 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
size: 100
320 ns ± 7.61 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
size: 1000
678 ns ± 18.2 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
size: 10000
3.4 μs ± 58.1 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 100000
29.8 μs ± 181 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
dtype: float16
size: 10
306 ns ± 12.3 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
size: 100
543 ns ± 15.9 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
size: 1000
2.97 μs ± 109 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 10000
27.1 μs ± 1.59 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
size: 100000
257 μs ± 11.8 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
dtype: float32
size: 10
283 ns ± 1.74 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
size: 100
384 ns ± 16.1 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
size: 1000
1.21 μs ± 20.3 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
size: 10000
9.7 μs ± 153 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 100000
95 μs ± 3.56 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
dtype: float64
size: 10
285 ns ± 3.72 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
size: 100
380 ns ± 10.2 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
size: 1000
1.23 μs ± 27.2 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
size: 10000
9.74 μs ± 192 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 100000
94.1 μs ± 1.76 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
dtype: bool
size: 10
295 ns ± 2.6 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
size: 100
297 ns ± 9.99 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
size: 1000
407 ns ± 8.24 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
size: 10000
683 ns ± 8.46 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
size: 100000
3.55 μs ± 24.1 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
sum
dtype: int8
size: 10
2.94 μs ± 22.1 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 100
3.09 μs ± 192 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 1000
3.69 μs ± 10.7 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 10000
8.92 μs ± 175 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 100000
56.6 μs ± 567 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
dtype: int16
size: 10
2.95 μs ± 28.2 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 100
3.06 μs ± 118 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 1000
3.79 μs ± 212 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 10000
8.89 μs ± 172 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 100000
57.8 μs ± 1.47 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
dtype: int32
size: 10
3.06 μs ± 132 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 100
3.07 μs ± 60.9 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 1000
4.06 μs ± 261 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 10000
10.6 μs ± 184 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 100000
74.1 μs ± 662 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
dtype: int64
size: 10
2.83 μs ± 147 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 100
2.75 μs ± 44.7 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 1000
3.1 μs ± 28.7 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 10000
5.44 μs ± 302 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 100000
27.8 μs ± 1.16 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
dtype: float16
size: 10
2.8 μs ± 118 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 100
2.96 μs ± 53.6 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 1000
5.25 μs ± 118 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 10000
26.7 μs ± 1.08 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
size: 100000
240 μs ± 14.2 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
dtype: float32
size: 10
2.75 μs ± 167 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 100
2.67 μs ± 17.2 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 1000
3.09 μs ± 127 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 10000
5.42 μs ± 87.7 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 100000
26.3 μs ± 1.36 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
dtype: float64
size: 10
2.7 μs ± 18.6 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 100
2.69 μs ± 22.5 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 1000
3.07 μs ± 25.2 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 10000
5.8 μs ± 253 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 100000
30.2 μs ± 1.14 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
dtype: bool
size: 10
3.1 μs ± 89.1 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 100
3.22 μs ± 163 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 1000
4 μs ± 109 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 10000
10.9 μs ± 313 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
size: 100000
77.2 μs ± 1.76 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
np.count_nonzero の処理速度の検証
下記は np.count_nonzero の結果をまとめた表です。表の列 は ndarray の要素の数 を、セルの数値 は 処理時間の平均 で μs を単位 として 小数点以下第二桁で四捨五入 した値です。
| データ型 | 10 | 100 | 1000 | 10000 | 100000 |
|---|---|---|---|---|---|
np.int8 |
0.3 | 0.3 | 0.4 | 0.7 | 3.6 |
np.int16 |
0.3 | 0.3 | 0.4 | 1.0 | 6.3 |
np.int32 |
0.3 | 0.3 | 0.5 | 1.6 | 12.1 |
np.int64 |
0.3 | 0.3 | 0.7 | 3.4 | 29.8 |
np.float16 |
0.3 | 0.5 | 3.0 | 27.1 | 257.0 |
np.float32 |
0.3 | 0.4 | 1.2 | 9.7 | 95.0 |
np.float64 |
0.3 | 0.4 | 1.2 | 9.7 | 94.1 |
np.bool |
0.3 | 0.3 | 0.4 | 0.7 | 3.6 |
上記の表から下記の事がわかります。
- 要素の数が 10 と 100 ではすべての場合で 処理速度はほぼ変わらない
- 要素の数が 1000 以上 の場合は 要素の数が多い程処理時間が増える
-
np.int8とnp.boolの場合の 処理速度はほぼ同じ である -
要素の数が 1000 以上 でデータ型が 整数型 の場合以下の性質がある
- データ型 の データサイズが多い程 処理時間が 増える
-
要素の数が 1000 以上 でデータ型が 浮動小数点数型 の場合以下の性質がある
-
np.float32とnp.float64の処理速度は ほぼ同じ である -
np.float16の処理速度は他と比べて かなり遅い
-
要素の数が 10 と 100 の場合で 処理速度が変わらない理由 は、最近のコンピュータは 同じ種類の数値の計算 を、一定の数までまとめて行うことができる からです。その数を超えると上記の表のように処理速度は要素の数に応じて増えていくことになります。
浮動小数点数型 で np.float16 の処理速度が遅い理由 は、筆者のパソコンの CPU1 が 16 ビットの浮動小数点数の計算 を 直接行うことができない ため、プログラムで np.float16 の計算を行う必要があるから ではないかと思います。
浮動小数点数型 で np.float32 と np.float64 の処理速度がほぼ同じである理由 は、筆者のパソコンの CPU1 が 64 ビットの浮動小数点数の計算を効率的に行う ことができるように設計されている 64 ビットの CPU であり、32 ビットの浮動小数点数の計算と同等の処理速度で計算を行うことができるからではないかと思います。
np.int8 と np.bool の処理速度がほぼ同じである理由 は、どちらも 0 を 1 バイトの 2 進数の 0 で表現するため、行う処理が同じ だからです。
下記は np.count_nonzero の処理速度が重要になる場合 の データ型の選択 のまとめです。
- 要素の数が少ない場合 はデータ型を 気にする必要はない
- 整数型 のデータを扱う場合はできるだけ サイズが小さいデータ型を選択 する
-
浮動小数点数型 のデータを扱う場合は
np.float32またはnp.float64を選択 する -
論理値 を扱う場合は
np.boolを選択する2
np.sum の処理速度の検証
下記は np.sum の結果をまとめた表です。表の列 は ndarray の要素の数 を、セルの数値 は 処理時間の平均 で μs を単位 として 小数点以下第二桁で四捨五入 した値です。
| データ型 | 10 | 100 | 1000 | 10000 | 100000 |
|---|---|---|---|---|---|
np.int8 |
2.9 | 3.1 | 3.7 | 8.9 | 56.6 |
np.int16 |
3.0 | 3.1 | 3.8 | 8.9 | 57.8 |
np.int32 |
3.1 | 3.1 | 4.1 | 10.6 | 74.1 |
np.int64 |
2.8 | 2.8 | 3.1 | 5.4 | 27.8 |
np.float16 |
2.8 | 3.0 | 5.3 | 26.7 | 240.0 |
np.float32 |
2.8 | 2.7 | 3.1 | 5.4 | 26.3 |
np.float64 |
2.7 | 2.7 | 3.1 | 5.8 | 30.2 |
np.bool |
3.1 | 3.2 | 4.0 | 10.9 | 77.2 |
上記の表から下記の事がわかります。
- 要素の数が 10 と 100 ではすべての場合で 処理速度はほぼ変わらない
- 要素の数が 1000 以上 の場合は 要素の数が多い程処理時間が増える
-
整数型 の場合は
np.int64の処理速度が 最も早い -
浮動小数点数型 の場合は
np.float32とnp.float64の処理速度は ほぼ同じ であり、np.float16の処理速度は他と比べて かなり遅い
また、np.bool では True を整数の 1、False を整数の 0 として記録 するので、np.bool の ndarray に対する np.count_nonzero と np.sum の計算結果は等しく なります。np.bool の ndarray に対する np.count_nonzero の処理速度は np.sum よりも 大幅に高速 なので np.count_nonzero を利用したほうが良い ことが確認できました。
整数型 で np.int16 と np.int32 の 処理速度が遅い理由 は、筆者のパソコンの CPU1 が 64 ビット以外の整数型の計算 を 直接行うことができない ため、プログラムで計算を行う必要があるから ではないかと思います。
浮動小数点数型 の性質に関しては np.count_nonzero で説明した理由と同じ です。
下記は np.sum の処理速度が重要になる場合のデータ型の選択のまとめです。
- 要素の数が少ない場合 はデータ型を 気にする必要はない
-
整数型 のデータを扱う場合は
np.int64を選択 する -
浮動小数点数型 のデータを扱う場合は
np.float32またはnp.float64を選択する -
論理値 を扱う場合は
np.boolを選択2 し、np.sumの代わりにnp.count_nonzeroで計算を行う
先程も説明しましたが、筆者のコンピューターと環境が異なっていたり、他の numpy の関数 の場合の処理速度は 上記の検証結果と異なる場合がある ので、興味がある方は自分のパソコンで検証を行ってみて下さい。
np.bool の ndarray によるゲーム盤のデータの表現
これまでの ndarray でゲーム盤を表現する NpBoard と NpIntBoard クラスでは、ゲーム盤の マスを表す要素 に 〇、×、空の 3 種類のデータを記録 しました。そのため、特定のマークが一直線上にいくつ並んでいるかの判定 を以下の手順で計算を行いました。
- 判定する 直線上のマーク を表す 1 次元の ndarray を計算 する
- 手順 1 で計算した 1 次元の ndarray に対して
==演算子 を使って 特定のマークが存在するかどうか を表す 1 次元のnp.boolの ndarray を計算 する - 手順 2 で計算した 1 次元の ndarray に対して
np.count_nonzeroを利用して特定の マークの数を数える
下記は NpBoard の is_winner メソッド内で記述されている、上記の手順で x 列 に player のマークが並んでいるかどうかを判定 する if 文で、上記の手順が下記のように対応します。
- 手順 1 が
self.board[x, :] - 手順 2 が
self.board[x, :] == player - 手順 3 が
np.count_nonzero(self.board[x, :] == player) == self.BOARD_SIZE
if np.count_nonzero(self.board[x, :] == player) == self.BOARD_SIZE:
上記の手順では、手順 2 で ndarray に対して == 演算子 を使って特定のマークが存在するかどうかを 判定する処理が必要 でしたが、この 手順 2 の処理を行わなくて済む ように ゲーム盤を表現する 方法があります。その方法について少し考えてみて下さい。
2 次元の np.bool の ndarray を要素とする dict での記録
NpBoard と NpIntBoard クラスでは 1 つ の 2 次元の ndarray でゲーム盤を表現 していましたが、下記のように 2 つ の 2 次元の ndarray でゲーム盤を表現する こともできます。
- 〇 のマークが存在するか どうかを表す 2 次元の ndarray
- × のマークが存在するか どうかを表す 2 次元の ndarray
2 次元の ndarray の要素 には、マークが存在する 場合と 存在しない 場合で 別の値を記録 します。数値型 のデータで記録する場合は 存在しない場合に 0 を、存在する場合に 1 を記録するのが一般的です。np.bool で記録する場合は 存在しない場合に False を、存在する場合に True を記録します。
上記のいずれかの方法 でデータを記録することで、直線上の特定のマークの数 を先程の手順 2 の == 演算子による計算の処理を行わずに np.count_nonzero だけで計算 することができるようになります。
〇× ゲームのゲーム盤はマスの数が 9 しかないため、先程検証したように ndarray のデータ型を np.int8 や np.int64 などの整数型にした場合と np.bool にした場合で処理速度はほとんど変わりませんが、マークが存在するかどうかを表すデータ としては np.bool のほうがふさわしい と思いましたので本記事では np.bool の ndarray でゲーム盤を表現 する NpBoolBoard クラスを定義 することにします。
〇× ゲームのゲーム開始時のゲーム盤 を 2 次元の np.bool の ndarray で記録 する場合は、下記のプログラムのように マークを表すデータをキー とする dict によって表現 するという方法が考えられます。なお、下記は NpBoolBoard クラスの __init__ メソッドに記述する内容を想定しています。
self.board = {
self.CIRCLE: np.full((3, 3), True),
self.CROSS: np.full((3, 3), True),
}
(x, y) のマスのマークが 〇、×、空 の場合は、下記の表のようになります。
| マスのマーク | self.board[self.CIRCLE][x][y] |
self.board[self.CROSS][x][y] |
|---|---|---|
| 〇 | True |
False |
| × | False |
True |
| 空 | False |
False |
上記の表からわかるように、空のマスに対応 する 2 次元の ndarray を 用意する必要ありません。また、self.board[self.CIRCLE][x][y] と self.board[self.CROSS][x][y] の 両方が True になることはありません3。
上記の方法では、これまでと同様に 手番とマークを表すデータ として 文字列型 や 数値型 のデータを 利用することができます。これまでと異なる のは、2 次元の ndarray の 要素 にマスに配置されたマークを表すデータを代入するのではなく、特定のマークが配置されているか どうかを表すデータを代入するという点です。
上記の方法では 手番とマークを表すデータ を dict のキー とするので、手番とマークを表すデータはどのようなデータでもかまわないというわけではなく、ハッシュ可能なデータ型にする必要がある 点に注意して下さい。
本記事では実装はしませんが、ndarray の代わりにゲーム盤を下記のように 2 つの 2 次元の list で表現することもできます。興味がある方は実装してみて下さい。
- 〇 のマークが存在するかどうかを表す 2 次元の list
- × のマークが存在するかどうかを表す 2 次元の list
3 次元の np.bool の ndarray での記録
別の方法として、マークを表す数値、x 座標、y 座標 の順で 3 つのインデックス を持つ 3 次元 の np.bool の ndarray で記録 するという方法があります。ndarray の インデックス は 0 から始まる整数 なので、整数の 0 を 〇 のマーク、1 を × のマーク に 割り当てます。
下記のプログラムは 〇× ゲーム の ゲーム開始時のゲーム盤 を 3 次元の np.bool の ndarray で記録 するプログラムです。マークを表す数値 は 〇 と × の 2 種類 なので、3 次元の ndarray の形状(shape)は (2, 3, 3) という tuple で表現します。
self.board = np.full((2, 3, 3), False)
(x, y) のマスのマークが 〇、×、空 の場合は、下記の表のようになります。
| マスのマーク | self.board[0][x][y] |
self.board[1][x][y] |
|---|---|---|
| 〇 | True |
False |
| × | False |
True |
| 空 | False |
False |
後で説明しますが、こちらのほうがプログラムが記述しやすく、処理速度も高速になる ので本記事はこちらの方法で NpBoolBoard クラスを定義することにします。
NpBoolBoard クラスの定義
NpBoolBoard クラスの定義を行います。実装してみたところ、それぞれのメソッドの内容はこれまでに定義したクラスと異なっていましたので、ゲーム盤を表現するクラスの基底クラスとなる Board クラスを継承 し、__init__ メソッドと下記の抽象メソッド をそれぞれ定義することにします。
| 抽象メソッド | 処理 |
|---|---|
getmark_by_move(move) |
move のマスのマークを返す |
setmark_by_move(move, mark) |
move のマスに mark を代入する |
xy_to_move(x, y) |
(x, y) のマスのゲーム盤のクラスの座標を返す |
move_to_xy(move) |
move のマスの x, y の座標を返す |
judge(last_turn, last_move, move_count) |
勝敗判定を計算して返す |
count_markpats(turn, last_turn) |
局面のマークのパターンを返す |
calc_legal_moves() |
合法手の一覧を表す、ゲーム盤のクラスの座標を要素とする list を返す |
calc_same_boardtexts(move) |
同一局面の一覧と move の同一局面に対応する座標を計算する |
board_to_str() |
ゲーム盤を表す文字列を返す |
board_to_hashable() |
ゲーム盤を表すハッシュ可能な値を返す |
下記は今回の記事で定義する NpBoolBoard クラスの定義です。それぞれのメソッドについてはこの後で説明します。
from marubatsu import Marubatsu, Board
import numpy as np
class NpBoolBoard(Board):
EMPTY = -1
CIRCLE = 0
CROSS = 1
MARK_TABLE = {
EMPTY: Marubatsu.EMPTY_STR,
CIRCLE: Marubatsu.CIRCLE_STR,
CROSS: Marubatsu.CROSS_STR,
Marubatsu.DRAW: Marubatsu.DRAW,
}
def __init__(self, board_size=3, count_linemark=False):
self.BOARD_SIZE = board_size
self.count_linemark = count_linemark
self.board = np.full((2, self.BOARD_SIZE, self.BOARD_SIZE), False)
if self.count_linemark:
self.rowcount = {
self.CIRCLE: [0] * self.BOARD_SIZE,
self.CROSS: [0] * self.BOARD_SIZE,
}
self.colcount = {
self.CIRCLE: [0] * self.BOARD_SIZE,
self.CROSS: [0] * self.BOARD_SIZE,
}
self.diacount = {
self.CIRCLE: [0] * 2,
self.CROSS: [0] * 2,
}
def getmark_by_move(self, move):
if self.board[(self.CIRCLE, ) + move]:
return self.CIRCLE
elif self.board[(self.CROSS, ) + move]:
return self.CROSS
else:
return self.EMPTY
def getmark(self, x, y):
if self.board[(self.CIRCLE, x, y)]:
return self.CIRCLE
elif self.board[(self.CROSS, x, y)]:
return self.CROSS
else:
return self.EMPTY
def setmark_by_move(self, move, mark):
x, y = move
if self.count_linemark:
if mark != self.EMPTY:
diff = 1
changedmark = mark
else:
diff = -1
changedmark = self.getmark(x, y)
self.colcount[changedmark][x] += diff
self.rowcount[changedmark][y] += diff
if x == y:
self.diacount[changedmark][0] += diff
if x + y == self.BOARD_SIZE - 1:
self.diacount[changedmark][1] += diff
if mark == self.EMPTY:
if self.board[self.CIRCLE, x, y]:
self.board[self.CIRCLE, x, y] = False
else:
self.board[self.CROSS, x, y] = False
else:
self.board[mark, x, y] = True
def xy_to_move(self, x, y):
return (x, y)
def move_to_xy(self, move):
return move
def calc_legal_moves(self):
legal_moves = [(x, y) for y in range(self.BOARD_SIZE)
for x in range(self.BOARD_SIZE)
if self.getmark(x, y) == self.EMPTY]
return legal_moves
def board_to_str(self):
txt = ""
for x in range(self.BOARD_SIZE):
for y in range(self.BOARD_SIZE):
txt += self.MARK_TABLE[self.getmark(x, y)]
return txt
def board_to_hashable(self):
return self.board.tobytes()
def judge(self, last_turn, last_move, move_count):
if move_count < self.BOARD_SIZE * 2 - 1:
return Marubatsu.PLAYING
# 直前に着手を行ったプレイヤーの勝利の判定
if self.is_winner(last_turn, last_move):
return last_turn
# 引き分けの判定
elif move_count == self.BOARD_SIZE ** 2:
return Marubatsu.DRAW
# 上記のどれでもなければ決着がついていない
else:
return Marubatsu.PLAYING
def is_winner(self, player, last_move):
x, y = last_move
if self.count_linemark:
if self.rowcount[player][y] == self.BOARD_SIZE or \
self.colcount[player][x] == self.BOARD_SIZE:
return True
# 左上から右下方向の判定
if x == y and self.diacount[player][0] == self.BOARD_SIZE:
return True
# 右上から左下方向の判定
if x + y == self.BOARD_SIZE - 1 and \
self.diacount[player][1] == self.BOARD_SIZE:
return True
else:
if np.count_nonzero(self.board[player, x, :]) == self.BOARD_SIZE or \
np.count_nonzero(self.board[player, :, y]) == self.BOARD_SIZE:
return True
# 左上から右下方向の判定
if x == y and np.count_nonzero(np.diag(self.board[player])) == self.BOARD_SIZE:
return True
# 右上から左下方向の判定
if x + y == self.BOARD_SIZE - 1 and \
np.count_nonzero(np.diag(np.fliplr(self.board[player]))) == self.BOARD_SIZE:
return True
# どの一直線上にも配置されていない場合は、player は勝利していないので False を返す
return False
def count_markpats(self, turn, last_turn):
pass
def count_marks(self, linedata, turn, last_turn):
pass
def calc_same_hashables(self):
pass
クラス属性と __init__ メソッドの定義
下記は クラス属性 と __init__ メソッド の定義です。修正箇所は NpBoard クラスからのものです。VSCode 上では上記の NpBoolBoard クラスの定義を実行するので、以下の説明では行番号の無いプログラムは省略します。
-
2 ~ 10 行目:先程説明したように 〇 を表す
CIRCLEと × を表すCROSSはゲーム盤を表す ndarray の最初のインデックスの値として利用するので、それぞれ 0 と 1 を割り当てた。空のマスを表すEMPTYに対応する ndarray のインデックスは存在しない ので、0 と 1 以外の値 を割り当てればよい。本記事では -1 を割り当てた。MARK_TABLEの値に関してはこれまでと同様である -
15 行目:先程説明した方法でゲーム開始時の局面を表す 3 次元の
np.boolの ndarray をself.boardに代入する
1 class NpBoolBoard(Board):
2 EMPTY = -1
3 CIRCLE = 0
4 CROSS = 1
5 MARK_TABLE = {
6 EMPTY: Marubatsu.EMPTY_STR,
7 CIRCLE: Marubatsu.CIRCLE_STR,
8 CROSS: Marubatsu.CROSS_STR,
9 Marubatsu.DRAW: Marubatsu.DRAW,
10 }
11
12 def __init__(self, board_size=3, count_linemark=False):
13 self.BOARD_SIZE = board_size
14 self.count_linemark = count_linemark
15 self.board = np.full((2, self.BOARD_SIZE, self.BOARD_SIZE), False)
元と同じなので省略
修正箇所
class NpBoolBoard(Board):
- EMPTY = "."
+ EMPTY = -1
- CIRCLE = "o"
+ CIRCLE = 0
- CROSS = "x"
+ CROSS = 1
MARK_TABLE = {
EMPTY: Marubatsu.EMPTY_STR,
CIRCLE: Marubatsu.CIRCLE_STR,
CROSS: Marubatsu.CROSS_STR,
Marubatsu.DRAW: Marubatsu.DRAW,
}
def __init__(self, board_size=3, count_linemark=False):
self.BOARD_SIZE = board_size
self.count_linemark = count_linemark
- self.board = np.full((self.BOARD_SIZE, self.BOARD_SIZE), self.EMPTY)
+ self.board = np.full((2, self.BOARD_SIZE, self.BOARD_SIZE), False)
元と同じなので省略
getmark_by_move と getmark メソッドの定義
下記は getmark_by_move と getmark メソッドの定義です。getmark は Board クラスの抽象メソッドではないので定義しなくてもかまいませんが、この後で定義する calc_legal_moves から呼び出す ので、少しでも 処理速度を向上させるため定義 しました。
また、NpBoard クラスのメソッドの定義と大幅に異なるので修正箇所は省略しました。
-
2 行目:
moveのマスに 〇 が配置されているかを判定 するためには、3 次元の ndarray であるself.boardのインデックス に(self.CIRCLE, x 座標, y 座標)を表す tuple を記述する必要がある。NpBoolBoard クラスの 着手を表すデータ は(x 座標, y 座標)という tuple なので、(self.CIRCLE, )という 1 つの要素を持つ tuple と 2 つの要素を持つ tuple であるmoveを+演算子で連結 することで(self.CIRCLE, x 座標, y 座標)を表す tuple を 計算する -
4 行目:
moveのマスに × が配置されているかを上記と同様の方法で判定する -
6、7 行目:
moveのマスに 〇 も × も配置されていない場合は空のマスなのでself.EMPTYを返り値として返す -
10 ~ 15 行目:
getmarkの場合は仮引数xとyに x 座標と y 座標が代入されているので 〇 が配置されているか を表す要素のインデックスは(self.CIRCLE, x, y)である
1 def getmark_by_move(self, move):
2 if self.board[(self.CIRCLE, ) + move]:
3 return self.CIRCLE
4 elif self.board[(self.CROSS, ) + move]:
5 return self.CROSS
6 else:
7 return self.EMPTY
8
9 def getmark(self, x, y):
10 if self.board[(self.CIRCLE, x, y)]:
11 return self.CIRCLE
12 elif self.board[(self.CROSS, x, y)]:
13 return self.CROSS
14 else:
15 return self.EMPTY
setmark_by_move メソッドの定義
下記は setmark_by_move メソッドの定義です。なお、setmark は AI で対戦する場合は呼び出されないため AI どうしの対戦の処理速度に影響は及ぼしません。そのため NpBoolBoard クラスでは定義せず、Board クラスで定義されている setmark を利用 することにします。
-
2 行目:NpBoard クラスでは x 座標と y 座標の値は
self.count_linemarkがTrueの場合のみ必要であったが、NpBoolBoard クラスの場合は 4 行目以降で必要となるので 3 行目の後にあったx, y = moveの処理を 2 行目に移動した -
4 ~ 8 行目:
markがself.EMPTYの場合は (x, y) のマス 〇 が配置 されている場合は(self.CIRCLE, x, y)の要素にFalseを、そうでなければ(self.CROSS, x, y)の要素にFalseを代入して (x, y) のマスのマークを削除 する -
7、8 行目:
markがself.CIRCLEまたはself.CROSSの場合は、(mark, x, y)の要素にTrueを代入 して マークを配置 する
1 def setmark_by_move(self, move, mark):
2 x, y = move
3 if self.count_linemark:
元と同じなので省略
4 if mark == self.EMPTY:
5 if self.board[self.CIRCLE, x, y]:
6 self.board[self.CIRCLE, x, y] = False
7 else:
8 self.board[self.CROSS, x, y] = False
9 else:
10 self.board[mark, x, y] = True
修正箇所
def setmark_by_move(self, move, mark):
+ x, y = move
if self.count_linemark:
- x, y = move
元と同じなので省略
+ if mark == self.EMPTY:
+ if self.board[self.CIRCLE, x, y]:
+ self.board[self.CIRCLE, x, y] = False
+ else:
+ self.board[self.CROSS, x, y] = False
+ else:
+ self.board[mark, x, y] = True
- self.board[move] = mark
上記の 4 ~ 8 行目のプログラムを下記のように (x, y) のマスに配置されているマークを判定せずに、(x, y) の 〇 と × の両方の要素に False を代入するようにしてもかまいません。if 文の判定処理がなくなっているので処理時間はほぼ変わらず、プログラムの行数が短くなるという利点が得られますが、プログラムの意味が若干わかりづらくなるという欠点が生じます。
if mark == self.EMPTY:
self.board[self.CIRCLE, x, y] = False
self.board[self.CROSS, x, y] = False
xy_to_move と move_to_xy メソッドの定義
xy_to_move と move_to_xy メソッドの処理は ListBoard クラスの処理と同じ ですが、NpBoolBoard クラスは ListBoard クラスを継承していない のでそれぞれを下記のように 定義する必要があります。ListBoard クラスと同じなので説明は省略します。
def xy_to_move(self, x, y):
return (x, y)
def move_to_xy(self, move):
return move
calc_legal_moves メソッドの定義
下記は calc_legal_moves メソッドの定義です。
-
4 行目:NpBoard クラスの場合は (x, y) のマスが空であるか どうかを
if self.board[x, y] == self.EMPTYという式で判定できたが、NpBoolBoard クラスの場合は そのような簡単な式では判定できない のでself.getmark(x, y)を利用して判定 するようにした
1 def calc_legal_moves(self):
2 legal_moves = [(x, y) for y in range(self.BOARD_SIZE)
3 for x in range(self.BOARD_SIZE)
4 if self.getmark(x, y) == self.EMPTY]
5 return legal_moves
修正箇所
def calc_legal_moves(self):
legal_moves = [(x, y) for y in range(self.BOARD_SIZE)
for x in range(self.BOARD_SIZE)
- if self.board[x, y] == self.EMPTY]
+ if self.getmark(x, y) == self.EMPTY]
return legal_moves
board_to_str と board_to_hashable メソッドの定義
下記は board_to_str と board_to_hashable メソッドの定義です。ndarray の要素 の内容がマークを表す文字列ではなく、True または False になった ので board_to_str では "".join() を利用 してゲーム盤を表す文字列を 計算することはできません。board_to_str は以前の記事で board_to_hashable を定義することで AI の関数から呼び出さないようにした ので、下記のプログラムでは 処理速度を気にせず に ゲーム盤の マスを一つずつ調べて MARK_TABLE で文字列に変換 するという処理を行うようにしました。
ndarray はその データ型や形状に関わらず tobytes メソッドで ハッシュ可能な値を計算することができる ので、board_to_hashable の処理は NpBoard クラスと同じ です。
なお、board_to_str は NpBoard クラスと完全に異なる処理を行っている点と、board_to_hashable は同じ処理を行うことから修正箇所は省略しました。
-
2 行目:ゲーム盤を表す文字列を計算する
txtを空文字で初期化する -
3 ~ 5 行目:2 重の for 文によってすべてのゲーム盤のマスを繰り返し処理で計算する。5 行目では (x, y) のマスのマークを文字列に変換した値を
txtに結合している -
6 行目:計算した
txtを返り値として返す
1 def board_to_str(self):
2 txt = ""
3 for x in range(self.BOARD_SIZE):
4 for y in range(self.BOARD_SIZE):
5 txt += self.MARK_TABLE[self.getmark(x, y)]
6 return txt
7
8 def board_to_hashable(self):
9 return self.board.tobytes()
judge と is_winner メソッドの定義
下記は judge メソッドとその中から呼び出される is_winner メソッドの定義です。judge メソッドの処理は ListBoard と同じなので説明は省略します。また、is_winner の中の self.count_linemark が True の場合の処理は NpBoard と同じです。
-
7、8 行目:NpBoard では x 列に
playerのマークが並んでいるかどうかをself.board[x, :] == playerの計算結果をnp.count_nonzeroで数えるという処理を行っていたが、NpBoolBoard ではself.board[player, x, :]に対して直接np.count_nonzeroで数えれば良い 点が異なる。y 列に関しても同様である -
11、15 行目:斜め方向の判定も同様に
np.diag(self.board) == playerのようなplayerと等しいかどうかを判定する処理が不要になる
```python
1 def judge(self, last_turn, last_move, move_count):
元と同じなので省略
2
3 def is_winner(self, player, last_move):
4 x, y = last_move
5 if self.count_linemark:
元と同じなので省略
6 else:
7 if np.count_nonzero(self.board[player, x, :]) == self.BOARD_SIZE or \
8 np.count_nonzero(self.board[player, :, y]) == self.BOARD_SIZE:
9 return True
10 # 左上から右下方向の判定
11 if x == y and np.count_nonzero(np.diag(self.board[player])) == self.BOARD_SIZE:
12 return True
13 # 右上から左下方向の判定
14 if x + y == self.BOARD_SIZE - 1 and \
15 np.count_nonzero(np.diag(np.fliplr(self.board[player]))) == self.BOARD_SIZE:
16 return True
17
18 # どの一直線上にも配置されていない場合は、player は勝利していないので False を返す
19 return False
修正箇所
def judge(self, last_turn, last_move, move_count):
元と同じなので省略
def is_winner(self, player, last_move):
x, y = last_move
if self.count_linemark:
元と同じなので省略
else:
- if np.count_nonzero(self.board[x, :] == player) == self.BOARD_SIZE or \
+ if np.count_nonzero(self.board[player, x, :]) == self.BOARD_SIZE or \
- np.count_nonzero(self.board[:, y] == player) == self.BOARD_SIZE:
+ np.count_nonzero(self.board[player, :, y]) == self.BOARD_SIZE:
return True
# 左上から右下方向の判定
- if x == y and np.count_nonzero(np.diag(self.board) == player) == self.BOARD_SIZE:
+ if x == y and np.count_nonzero(np.diag(self.board[player])) == self.BOARD_SIZE:
return True
# 右上から左下方向の判定
if x + y == self.BOARD_SIZE - 1 and \
- np.count_nonzero(np.diag(np.fliplr(self.board)) == player) == self.BOARD_SIZE:
+ np.count_nonzero(np.diag(np.fliplr(self.board[player]))) == self.BOARD_SIZE:
return True
# どの一直線上にも配置されていない場合は、player は勝利していないので False を返す
return False
count_markpats、count_marks、calc_same_hashables メソッド
ここまでの定義で ai2 が利用するメソッドはすべて定義 できています。そのため、現時点で ai2 VS ai2 の対戦を行うことは可能 です。
そこで、今回の記事では NpBoolBoard クラスのメソッドの定義はここまで にし、ai14s や ai_abs_dls で必要となる count_markpats、count_marks、calc_same_hashables メソッドは次回の記事で定義 することにします。ただし、これらのメソッドは Board クラスの抽象クラスとして定義されているので、定義しないとエラーが発生 します。そのため、下記のプログラムのように 何もしない関数として定義 しておくことにします。
def count_markpats(self, turn, last_turn):
pass
def count_marks(self, linedata, turn, last_turn):
pass
def calc_same_hashables(self):
pass
ai2 VS ai2 の処理速度の計算
上記で定義した NpBoolBoard クラスを利用した場合の ai2 VS ai2 の処理速度を計測 することにします。benchmark は ai14s や ai_abs_dls を呼び出すため エラーが発生する ので、下記のプログラムで ai_match を利用して対戦を行う ことにします。なお、benchmark と同じ対戦結果 になるように random.seed(0) を実行して乱数の種を初期化 しました。
from ai import ai_match, ai2
import random
random.seed(0)
ai_match(ai=[ai2, ai2], match_num=50000, mbparams={"boardclass": NpBoolBoard})
random.seed(0)
ai_match(ai=[ai2, ai2], match_num=50000, mbparams={"boardclass": NpBoolBoard, "count_linemark": True})
実行結果
ai2 VS ai2
100%|██████████| 50000/50000 [00:06<00:00, 7587.97it/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%
ai2 VS ai2
100%|██████████| 50000/50000 [00:07<00:00, 6367.55it/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%
下記は以前の記事の NpBoard、NpIntBoard クラスを利用した場合の ai2 VS ai2 のベンチマークの結果に 上記の結果を加えた表 です。
| boardclass | count_linemark |
ai2 VS ai2
|
|---|---|---|
| NpBoard | False |
5972.04 回/秒 |
| NpIntBoard | False |
7185.05 回/秒 |
| NpBoolBoard | False |
7587.97 回/秒 |
| NpBoard | True |
7478.88 回/秒 |
| NpIntBoard | True |
9820.80 回/秒 |
| NpBoolBoard | True |
6367.55 回/秒 |
NpBoolBoard は、NpBoard や NpIntBoard と比較して下記の点で処理時間が異なります。
- マークの参照と配置を行う
getmark_by_moveとsetmark_by_moveの処理は NpBoard と NpIntBoard クラスでは 1 行で行える ので 処理時間が短い -
count_linemarkがFalseの場合の 直線上にマークが並んでいるか の判定処理は NpBoolBoard では==による判定処理を行わなくても済む 分だけ 処理時間が短い
count_linemark が False の場合は、NpBoolBoard の欠点よりも 利点のほうが上回る ため、上記の表のように ai2 VS ai2 の処理速度は NpBoard や NpIntBoard よりも 高速になります。また、NpBoard と NpIntBoard クラスと異なり、count_linemark が False のほうが True の場合よりも 処理速度が高速 になります。
一方、count_linemark が True の場合は NpBoolBoard の利点が得られない ので、NpBoard や NpBoolBoard クラスよりも 処理速度が遅く なります。
なお、ai14s と ai_abs_dls の処理速度については次回の記事で検証することにします。
今回の記事のまとめ
今回の記事では最初に ndarray のデータ型による処理速度の違いについて説明しました。
また、3 次元の np.bool の ndarray によるゲーム盤の表現方法について説明し、NpBoolBoard クラスの定義を行いました。次回の記事では今回の記事で定義しなかったメソッドの定義を行います。
本記事で入力したプログラム
| リンク | 説明 |
|---|---|
| marubatsu.ipynb | 本記事で入力して実行した JupyterLab のファイル |
| marubatsu.py | 本記事で更新した marubatsu_new.py |
次回の記事