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を一から作成する その206 np.diagonal と np.flip を利用した NpBoolBoard クラスのメソッドの定義

Posted at

目次と前回の記事

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_linemarkTrue の場合の 処理は変わらない ので説明は省略します。

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.diagonal2 次元以上 の ndarray から、指定した 2 つの axis を基準 とした 対角線 1 の要素 を取り出した ndarray を計算 する関数です。

2 つの axis は仮引数 axis1axis2番号を指定 し、下記のような計算が行われます。

  • 仮引数 axis1axis2 で指定した 2 つの axis のインデックスが同じ である すべての要素 を計算する
  • 計算される ndarray の axis は、axis1axis2 で指定された axis を取り除き最後に対角線 1 の要素 を表す axis を追加する
  • 2 つの axis を取り除き、1 つの axis を追加するので、結果として計算される ndarray の 次元 は元の ndarray の次元より 1 小さくなる

仮引数の名前 である axis1axis2np.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 番の axisaxis 1 のように 網掛けをせずに表記 することで区別することにします。

下記は np.diagonalNpBoolBoard の 〇× ゲームのゲーム盤と同じ (2, 3, 3) の形状 を持つ 3 次元の ndarray に対して 〇× ゲームの対角線 1 の要素を表すデータを計算して表示するプログラムです。〇× ゲームのデータの要素は TrueFalse の 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.diagonalmb の局面の 対角線 1 のマークを表すデータを計算して表示するプログラムです。実行結果から、先程の np.diag による計算と 同様の内容2 次元の ndarray として計算 されていることが確認できます。

print(np.diagonal(mb.board.board, axis1=1, axis2=2))

実行結果

[[ True False False]
 [False  True False]]

np.diagonal の詳細については下記のリンク先を参照して下さい。

対角線 1 の要素を計算する際に、下記のプログラムのように axis1axis2 の順番を入れ替えても結果は変わりません。

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 以外を指定した場合は、下記のプログラムのように axis1axis2 の値を逆にすると 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 は名前の中の lrleft と right の頭文字 であることから、2 次元の ndarray左右を反転 する処理を行う関数ですが、それは 2 次元の ndarray の axis 0 を上下方向axis 1 を左右方向に図示 した場合に 左右に反転 するという意味です。NpBoard クラスの 〇× ゲームのゲーム盤を表す 2 次元の ndarrayaxis 0 が 左右方向 の x 軸を、axis 1 が上下方向 の y 軸を表すので、np.fliplr によってゲーム盤の 上下が反転される 点に注意が必要です。上記のプログラムでは np.fliplr によってゲーム盤を 上下に反転した後np.diag によって 対角線 1 のマスが計算 されるので、計算された 1 次元の ndarray の要素には 左下から右上方向の順 にマスの値が代入されます。

np.fliplr が行う処理

以前の記事では説明していませんでしたが、np.fliplraxis 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.fliplr3 次元以上 の 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 次元の ndarrayaxis 2 を反転 した ndarray の計算は、ndarray任意の軸を反転 する np.flip を利用することで計算できます。np.flipaxis という仮引数反転する 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.fliplrnp.flip のキーワード引数に axis=1 を記述した場合と同じ処理を行います。np.flipudaxis=0 を記述した場合と同様です。

対角線 2 のデータの計算方法

対角線 2 のデータの計算は、下記のプログラムのように上記の np.flipnp.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_linemarkTrue の場合の処理は NpBoard クラスと同じです。

  • 8、10 行目count_marksi 列のマークのパターンを計算するために必要な情報は 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.bool2 次元の ndarray が代入されるので、Counter クラス を利用してそれぞれの マークの数を数えることはできません

以前の記事で説明したように np.bool の ndarray では True を整数の 1False を整数の 0 として記録しているので、下記のプログラムのように マークに対応する要素 に対して np.count_nonzero を利用して マークの数を数える ことができます。

  • 2、3 行目turn の手番のマークのデータは linedata[turn] が表す np.bool の 1 次元の ndarray 記録されているので、np.count_nonzeroturn の手番のマークの数を計算することができる。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%

下記は以前の記事NpBoardNpIntBoard クラスを利用した場合の 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_linemarkFalse の場合の 処理速度 は、NpBoard クラスよりはほんの少し高速 になりますが、NpIntBoard クラスとほぼ変わらない ことが確認できました。

また、count_linemarkTrue の場合は以前の記事で検証した ai2 VS ai2 の処理速度の検証同様の理由 から NpBoard や NpBoolBoard クラスよりも 処理速度が遅くなります

なお、count_markpats を改良することで 高速化 することができます。次回の記事でその高速化を行い、改めてその処理速度を検証することにします。

今回の記事のまとめ

今回の記事では np.diagonalnp.flip を利用することで NpBoolBoard クラスの 3 次元の ndarray で表現されるゲーム盤 に対する 各直線のマークのパターンを計算する方法 について説明しました。また、NpBoolBoard クラスの count_markpats の定義を行い、count_linemarkFalse の場合の ai14s VS ai2 の対戦の処理速度が NpBoard の場合よりも少しだけ高速 になることを確認しました。

次回の記事では count_markpats の処理速度の高速化と、残りの calc_same_boardtexts の実装を行う予定です。

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

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

次回の記事

近日公開予定です

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?