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を一から作成する その204 3 次元の np.bool の ndarray によるゲーム盤の表現

Last updated at Posted at 2025-11-24

目次と前回の記事

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.sumnp.count_nonzero

なお、コンピューターの CPU、Python のバージョン、numpy のバージョンなど、コンピュータの環境によって 下記とは 大きく異なる実行結果になる場合 があります。その場合は本記事の検証結果は適用できない 点に注意して下さい。興味がある方は自分のパソコンで上記のプログラムを実行して処理時間を計測してみると良いでしょう。

また、np.sumnp.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.int8np.bool の場合の 処理速度はほぼ同じ である
  • 要素の数が 1000 以上 でデータ型が 整数型 の場合以下の性質がある
    • データ型データサイズが多い程 処理時間が 増える
  • 要素の数が 1000 以上 でデータ型が 浮動小数点数型 の場合以下の性質がある
    • np.float32np.float64 の処理速度は ほぼ同じ である
    • np.float16 の処理速度は他と比べて かなり遅い

要素の数が 10 と 100 の場合で 処理速度が変わらない理由 は、最近のコンピュータは 同じ種類の数値の計算 を、一定の数までまとめて行うことができる からです。その数を超えると上記の表のように処理速度は要素の数に応じて増えていくことになります。

浮動小数点数型np.float16 の処理速度が遅い理由 は、筆者のパソコンの CPU116 ビットの浮動小数点数の計算直接行うことができない ため、プログラムで np.float16 の計算を行う必要があるから ではないかと思います。

浮動小数点数型np.float32np.float64 の処理速度がほぼ同じである理由 は、筆者のパソコンの CPU164 ビットの浮動小数点数の計算を効率的に行う ことができるように設計されている 64 ビットの CPU であり、32 ビットの浮動小数点数の計算と同等の処理速度で計算を行うことができるからではないかと思います。

np.int8np.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.float32np.float64 の処理速度は ほぼ同じ であり、np.float16 の処理速度は他と比べて かなり遅い

また、np.bool では True を整数の 1False を整数の 0 として記録 するので、np.bool の ndarray に対する np.count_nonzeronp.sum の計算結果は等しく なります。np.bool の ndarray に対する np.count_nonzero の処理速度は np.sum よりも 大幅に高速 なので np.count_nonzero を利用したほうが良い ことが確認できました。

整数型np.int16np.int32処理速度が遅い理由 は、筆者のパソコンの CPU164 ビット以外の整数型の計算直接行うことができない ため、プログラムで計算を行う必要があるから ではないかと思います。

浮動小数点数型 の性質に関しては np.count_nonzero で説明した理由と同じ です。

下記は np.sum の処理速度が重要になる場合のデータ型の選択のまとめです。

  • 要素の数が少ない場合 はデータ型を 気にする必要はない
  • 整数型 のデータを扱う場合は np.int64 を選択 する
  • 浮動小数点数型 のデータを扱う場合は np.float32 または np.float64 を選択する
  • 論理値 を扱う場合は np.bool を選択2 し、np.sum の代わりに np.count_nonzero で計算を行う

先程も説明しましたが、筆者のコンピューターと環境が異なっていたり他の numpy の関数 の場合の処理速度は 上記の検証結果と異なる場合がある ので、興味がある方は自分のパソコンで検証を行ってみて下さい。

np.bool の ndarray によるゲーム盤のデータの表現

これまでの ndarray でゲーム盤を表現する NpBoardNpIntBoard クラスでは、ゲーム盤の マスを表す要素〇、×、空の 3 種類のデータを記録 しました。そのため、特定のマークが一直線上にいくつ並んでいるかの判定 を以下の手順で計算を行いました。

  1. 判定する 直線上のマーク を表す 1 次元の ndarray を計算 する
  2. 手順 1 で計算した 1 次元の ndarray に対して == 演算子 を使って 特定のマークが存在するかどうか を表す 1 次元の np.bool の ndarray を計算 する
  3. 手順 2 で計算した 1 次元の ndarray に対して np.count_nonzero を利用して特定の マークの数を数える

下記は NpBoardis_winner メソッド内で記述されている、上記の手順で xplayer のマークが並んでいるかどうかを判定 する 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 での記録

NpBoardNpIntBoard クラスでは 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.int8np.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_movegetmark メソッドの定義

下記は getmark_by_movegetmark メソッドの定義です。getmark は Board クラスの抽象メソッドではないので定義しなくてもかまいませんが、この後で定義する calc_legal_moves から呼び出す ので、少しでも 処理速度を向上させるため定義 しました。
また、NpBoard クラスのメソッドの定義と大幅に異なるので修正箇所は省略しました。

  • 2 行目move のマスに 〇 が配置されているかを判定 するためには、3 次元の ndarray である self.board のインデックス(self.CIRCLE, x 座標, y 座標) を表す tuple を記述する必要がある。NpBoolBoard クラスの 着手を表すデータ(x 座標, y 座標) という tuple なので、(self.CIRCLE, ) という 1 つの要素を持つ tuple2 つの要素を持つ tuple である move+ 演算子で連結 することで (self.CIRCLE, x 座標, y 座標) を表す tuple を 計算する
  • 4 行目move のマスに × が配置されているかを上記と同様の方法で判定する
  • 6、7 行目move のマスに 〇 も × も配置されていない場合は空のマスなので self.EMPTY を返り値として返す
  • 10 ~ 15 行目getmark の場合は仮引数 xy に 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_linemarkTrue の場合のみ必要であったが、NpBoolBoard クラスの場合は 4 行目以降で必要となるので 3 行目の後にあった x, y = move の処理を 2 行目に移動した
  • 4 ~ 8 行目markself.EMPTY の場合は (x, y) のマス 〇 が配置 されている場合は (self.CIRCLE, x, y) の要素に False を、そうでなければ (self.CROSS, x, y) の要素に False を代入して (x, y) のマスのマークを削除 する
  • 7、8 行目markself.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_movemove_to_xy メソッドの定義

xy_to_movemove_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_strboard_to_hashable メソッドの定義

下記は board_to_strboard_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()

judgeis_winner メソッドの定義

下記は judge メソッドとその中から呼び出される is_winner メソッドの定義です。judge メソッドの処理は ListBoard と同じなので説明は省略します。また、is_winner の中の self.count_linemarkTrue の場合の処理は 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_markpatscount_markscalc_same_hashables メソッド

ここまでの定義で ai2 が利用するメソッドはすべて定義 できています。そのため、現時点で ai2 VS ai2 の対戦を行うことは可能 です。

そこで、今回の記事では NpBoolBoard クラスのメソッドの定義はここまで にし、ai14sai_abs_dls で必要となる count_markpatscount_markscalc_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 の処理速度を計測 することにします。benchmarkai14sai_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_movesetmark_by_move の処理は NpBoard と NpIntBoard クラスでは 1 行で行える ので 処理時間が短い
  • count_linemarkFalse の場合の 直線上にマークが並んでいるか の判定処理は NpBoolBoard では == による判定処理を行わなくても済む 分だけ 処理時間が短い

count_linemarkFalse の場合は、NpBoolBoard の欠点よりも 利点のほうが上回る ため、上記の表のように ai2 VS ai2 の処理速度は NpBoard や NpIntBoard よりも 高速になります。また、NpBoard と NpIntBoard クラスと異なり、count_linemarkFalse のほうが True の場合よりも 処理速度が高速 になります。

一方、count_linemarkTrue の場合は NpBoolBoard の利点が得られない ので、NpBoard や NpBoolBoard クラスよりも 処理速度が遅く なります。

なお、ai14sai_abs_dls の処理速度については次回の記事で検証することにします。

今回の記事のまとめ

今回の記事では最初に ndarray のデータ型による処理速度の違いについて説明しました。
また、3 次元の np.bool の ndarray によるゲーム盤の表現方法について説明し、NpBoolBoard クラスの定義を行いました。次回の記事では今回の記事で定義しなかったメソッドの定義を行います。

本記事で入力したプログラム

リンク 説明
marubatsu.ipynb 本記事で入力して実行した JupyterLab のファイル
marubatsu.py 本記事で更新した marubatsu_new.py

次回の記事

  1. おそらく最近のほとんどの CPU は筆者のパソコンの CPU と同じような性質を持つと思います 2 3

  2. np.int8 を選択してもかまいませんが、論理型のデータを扱う際にわざわざ np.int8 を選択する積極的な理由はないと思います。 2

  3. 間違ってそのようなデータを記録した場合は (x, y) のマスに何のマークが配置されているかがわからなくなってしまい、意図しない処理が行われてしまう可能性が高いので、そのようなデータを記録しないように注意する必要があります

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?