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を一から作成する その199 ndarray の演算とブロードキャストを利用した判定処理

Last updated at Posted at 2025-10-18

目次と前回の記事

Python のバージョンとこれまでに作成したモジュール

本記事のプログラムは Python のバージョン 3.13 で実行しています。また、numpy のバージョンは 2.3.5 です。

以下のリンクから、これまでに作成したモジュールを見ることができます。

リンク 説明
marubatsu.py Marubatsu、Marubatsu_GUI クラスの定義
ai.py AI に関する関数
mbtest.py テストに関する関数
util.py ユーティリティ関数の定義
tree.py ゲーム木に関する Node、Mbtree クラスなどの定義
gui.py GUI に関する処理を行う基底クラスとなる GUI クラスの定義

AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。

NpBoard クラスの改良の続き

今回の記事では前回の記事に引き続き NpBoard クラスで行う処理を、numpy の機能を利用するように修正します。

ndarray に対する演算処理

ndarray と list大きな違い の一つに ndarray どうしの演算処理 があります。

list に対する +* などの 演算子の処理 は、list の結合 という 数値型のデータに対する演算とは異なる処理 を行います。また、list に対しては -/ など の数値型のデータに対して利用できる 演算子をほとんど利用することができません

それに対して ndarray に対する演算子の処理は、ndarray の 各要素 に対して 数値型のデータ に対する場合と 同じ演算子の計算 を行います。また、数値型のデータに対するすべての演算子 による演算を ndarray に対して行うことができます1

今回の記事では ndarray に対する演算処理を利用 した NpBoard クラスの改良を行うので、最初に list と ndarray に対する演算処理の違いについて説明を行います。

+ 演算子の違い

list に対する + 演算子は、下記のプログラムのように 2 つの list の要素を結合 した新しい list を作成するという演算を行います。

l1 = [1, 2, 3]
l2 = [4, 5, 6]
print(l1 + l2)

実行結果

[1, 2, 3, 4, 5, 6]

一方、ndarray に対する + 演算子は、下記のプログラムのように ndarray の 対応する各要素を加算 した新しい 同じ形状の ndarray を作成 するという処理を行います。

import numpy as np

a1 = np.array(l1)
a2 = np.array(l2)
print(a1 + a2)

実行結果

[5 7 9]

上記の計算は下記の手順で行われます。

  • a1 + a2
  • = [1, 2, 3] + [4, 5, 6]
  • = [1 + 4, 2 + 5, 3 + 6]2
  • = [5, 7, 9]

上記のような 各要素の合計を計算 する処理を list に対して行う プログラムは、以前の記事で説明した組み込み関数 zip を利用した場合は下記のプログラムのようになり、上記の a1 + a2 と比べて複雑 なプログラムになります。なお、下記のプログラムの変数 e1e2要素を表す element の頭文字 をとって命名しました。

print([e1 + e2 for e1, e2 in zip(l1, l2)])

実行結果

[5, 7, 9]

処理速度の違い

要素の加算 の処理の 処理速度 は、要素の数が数個のように少ない場合list のほうが高速 ですが、要素の数が多い 場合は ndarray のほうが高速 になります。下記は、要素の数が 1 から 10 までのそれぞれの場合の list と ndarray の要素の加算の処理時間の平均 を計測し、グラフで表示 するプログラムです。%timeit で計算すると 処理時間を変数に代入することができず グラフの表示をプログラムで行えないので、以前の記事で説明した timeit モジュールの timeit を利用しました。なお、標準偏差の計算と表示は省略しました。

  • 6 ~ 8 行目:グラフを表示する際に必要となる、要素の数、list の要素の加算の処理時間の平均、ndarray の要素の加算の処理時間の平均を記録する変数を空の list で初期化する
  • 9、10 行目timeit で計算する回数を表す変数を初期化する。初期化する値は %timeit で処理時間を計算した場合に表示される値を利用した
  • 11 ~ 27 行目:要素の数が 1 ~ 10 までの場合の繰り返し処理を行う
  • 12、13 行目:list と ndarrary の 要素の数を numlist に追加し、print で表示する
  • 14、15 行目i 個の要素を持つ list を 2 つ作成して変数に代入する、要素の値は何でも構わないので 0 とした
  • 16、17 行目:list の要素の加算を行う処理の平均時間を timeit.repeat で計測する。以前の記事で説明したように、グローバル変数 l1l2 に対する処理を行うので globals=globals() を実引数に記述する必要がある点に注意が必要である
  • 18 行目:statistics モジュールの mean を利用して result に記録された処理時間の平均を計算する。timeit.repeat の計算結果は number 回の処理の処理時間を表すので number で除算し、単位をナノ秒($10^{-9}$ 秒)で計算するため $10^9$ を乗算した。1e9以前の記事で説明した $10^9$ を表す指数表記である
  • 19、20 行目:計算した list の要素の加算の平均時間を表示し、listtime に追加する
  • 21 ~ 27 行目:上記と同様の方法で ndarray の要素の加算の処理時間の平均を計算し、arraytime に追加する
  • 29 ~ 33 行目:結果をグラフで描画する
 1  import timeit
 2  import matplotlib.pyplot as plt
 3  import japanize_matplotlib
 4  from statistics import mean
 5  
 6  numlist = []
 7  listtime = []
 8  arraytime = []
 9  number = 1000000
10  repeat = 7
11  for i in range(1, 11):
12      numlist.append(i)
13      print(f"要素の数 = {i}")
14      l1 = [0] * i
15      l2 = [0] * i
16      stmt = "[e1 + e2 for e1, e2 in zip(l1, l2)]"
17      result = timeit.repeat(stmt=stmt, number=number, repeat=repeat, globals=globals())
18      time = mean(result) / number * 1e9
19      print(f"  list の要素の加算:    {time:3.0f} ns")
20      listtime.append(time)
21      a1 = np.array(l1)
22      a2 = np.array(l2)
23      stmt = "a1 + a2"
24      result = timeit.repeat(stmt=stmt, number=number, repeat=repeat, globals=globals())
25      time = mean(result) / number * 1e9
26      print(f"  ndarray の要素の加算: {time:3.0f} ns")
27      arraytime.append(time)
28      
29  plt.plot(numlist, arraytime, label="ndarray の加算")
30  plt.plot(numlist, listtime, label="list の加算")
31  plt.xlabel("要素の数")
32  plt.ylabel("処理時間の平均(ns)")
33  plt.legend()
行番号のないプログラム
import timeit
import matplotlib.pyplot as plt
import japanize_matplotlib
from statistics import mean

numlist = []
listtime = []
arraytime = []
number = 1000000
repeat = 7
for i in range(1, 11):
    numlist.append(i)
    print(f"要素の数 = {i}")
    l1 = [0] * i
    l2 = [0] * i
    stmt = "[e1 + e2 for e1, e2 in zip(l1, l2)]"
    result = timeit.repeat(stmt=stmt, number=number, repeat=repeat, globals=globals())
    time = mean(result) / number * 1e9
    print(f"  list の要素の加算:    {time:3.0f} ns")
    listtime.append(time)
    a1 = np.array(l1)
    a2 = np.array(l2)
    stmt = "a1 + a2"
    result = timeit.repeat(stmt=stmt, number=number, repeat=repeat, globals=globals())
    time = mean(result) / number * 1e9
    print(f"  ndarray の要素の加算: {time:3.0f} ns")
    arraytime.append(time)
    
plt.plot(numlist, arraytime, label="ndarray の加算")
plt.plot(numlist, listtime, label="list の加算")
plt.xlabel("要素の数")
plt.ylabel("処理時間の平均(ns)")
plt.legend()

実行結果

要素の数 = 1
  list の要素の加算:    285 ns
  ndarray の要素の加算: 432 ns
要素の数 = 2
  list の要素の加算:    312 ns
  ndarray の要素の加算: 433 ns
要素の数 = 3
  list の要素の加算:    344 ns
  ndarray の要素の加算: 444 ns
要素の数 = 4
  list の要素の加算:    388 ns
  ndarray の要素の加算: 429 ns
要素の数 = 5
  list の要素の加算:    447 ns
  ndarray の要素の加算: 433 ns
要素の数 = 6
  list の要素の加算:    474 ns
  ndarray の要素の加算: 447 ns
要素の数 = 7
  list の要素の加算:    496 ns
  ndarray の要素の加算: 431 ns
要素の数 = 8
  list の要素の加算:    540 ns
  ndarray の要素の加算: 444 ns
要素の数 = 9
  list の要素の加算:    615 ns
  ndarray の要素の加算: 440 ns
要素の数 = 10
  list の要素の加算:    663 ns
  ndarray の要素の加算: 432 ns

実行結果とグラフからわかるように、list の場合は 要素の数が増える と処理時間の平均も 要素の数に比例して増加 しますが、ndarray の場合は要素の数が増えても 処理時間の平均はほとんど変わりません。そのため、要素の数が 5 以下 の場合は list の方が処理時間の平均が短い ですが、5 を超えるndarray の方が処理時間の平均が短く なります。

なお、上記で要素の数が増えても ndarray の処理時間の平均が変わらないように見える のは、要素の数に比例して増える処理時間の平均が非常に小さい からです。下記のプログラムのように 要素の数100 から 1000 まで 100 ずつ増やした場合 は実行結果のグラフからわかるように ndarray の処理時間の平均要素の数に比例して増える3 ことが確認できます。ただし、その 増加量list と比較して大幅に少ない ことも確認できます。

なお、要素の数を 100 以上に増やすと list の場合の処理時間が長くなる ので number は先ほどの 1/10 の 10 万回 としました。また、ndarray と list の処理時間の平均を 同じグラフで描画 すると下記の 2 番目のグラフのように ndarray の処理時間が増えているように見えなくなる ので、下記の最初のグラフでは ndarray の処理時間だけをグラフ にしました。それ以外は基本的に先程のプログラムと同じです。

numlist = []
listtime = []
arraytime = []
number = 100000
repeat = 7
for i in range(100, 1100, 100):
    numlist.append(i)
    print(f"要素の数 = {i}")
    l1 = [0] * i
    l2 = [0] * i
    stmt = "[e1 + e2 for e1, e2 in zip(l1, l2)]"
    result = timeit.repeat(stmt=stmt, number=number, repeat=repeat, globals=globals())
    time = mean(result) / number * 1e9
    print(f"  list の要素の加算:    {time:5.0f} ns")
    listtime.append(time)
    a1 = np.array(l1)
    a2 = np.array(l2)
    stmt = "a1 + a2"
    result = timeit.repeat(stmt=stmt, number=number, repeat=repeat, globals=globals())
    time = mean(result) / number * 1e9
    print(f"  ndarray の要素の加算: {time:5.0f} ns")
    arraytime.append(time)
    
plt.plot(numlist, arraytime, label="ndarray の加算")
plt.xlabel("要素の数")
plt.ylabel("処理時間の平均(ns)")
plt.legend()
plt.show()

plt.plot(numlist, arraytime, label="ndarray の加算")
plt.plot(numlist, listtime, label="list の加算")
plt.xlabel("要素の数")
plt.ylabel("処理時間の平均(ns)")
plt.legend()
修正箇所
numlist = []
listtime = []
arraytime = []
-number = 1000000
+number = 100000
repeat = 7
-for i in range(1, 11):
+for i in range(100, 1100, 100):
    numlist.append(i)
    print(f"要素の数 = {i}")
    l1 = [0] * i
    l2 = [0] * i
    stmt = "[e1 + e2 for e1, e2 in zip(l1, l2)]"
    result = timeit.repeat(stmt=stmt, number=number, repeat=repeat, globals=globals())
    time = mean(result) / number * 1e9
    print(f"  list の要素の加算:    {time:5.0f} ns")
    listtime.append(time)
    a1 = np.array(l1)
    a2 = np.array(l2)
    stmt = "a1 + a2"
    result = timeit.repeat(stmt=stmt, number=number, repeat=repeat, globals=globals())
    time = mean(result) / number * 1e9
    print(f"  ndarray の要素の加算: {time:5.0f} ns")
    arraytime.append(time)
    
+plt.plot(numlist, arraytime, label="ndarray の加算")
+plt.xlabel("要素の数")
+plt.ylabel("処理時間の平均(ns)")
+plt.legend()
+plt.show()

plt.plot(numlist, arraytime, label="ndarray の加算")
plt.plot(numlist, listtime, label="list の加算")
plt.xlabel("要素の数")
plt.ylabel("処理時間の平均(ns)")
plt.legend()

実行結果

要素の数 = 100
  list の要素の加算:     4195 ns
  ndarray の要素の加算:   513 ns
要素の数 = 200
  list の要素の加算:     7521 ns
  ndarray の要素の加算:   599 ns
要素の数 = 300
  list の要素の加算:    11146 ns
  ndarray の要素の加算:   666 ns
要素の数 = 400
  list の要素の加算:    14381 ns
  ndarray の要素の加算:   726 ns
要素の数 = 500
  list の要素の加算:    17775 ns
  ndarray の要素の加算:   731 ns
要素の数 = 600
  list の要素の加算:    21025 ns
  ndarray の要素の加算:   869 ns
要素の数 = 700
  list の要素の加算:    24360 ns
  ndarray の要素の加算:   970 ns
要素の数 = 800
  list の要素の加算:    27947 ns
  ndarray の要素の加算:   972 ns
要素の数 = 900
  list の要素の加算:    30787 ns
  ndarray の要素の加算:  1030 ns
要素の数 = 1000
  list の要素の加算:    34084 ns
  ndarray の要素の加算:  1097 ns

このように、各要素の加算処理 を行う場合は、要素の数が多い程 ndarray を利用したほうが 処理速度が大幅に速く なります。数学では 多次元配列のデータの各要素の合計 の計算が良く行わます。そのような計算を python で行う際には ndarray を利用 するとプログラムが 簡潔に記述できる だけでなく、高速に計算を行う ことができます。

* 演算子の違い

* 演算子は下記のプログラムのように list と整数型のデータ に対して計算を行うことができ、同じ list を複数回結合 した新しい list を作成するという処理を行います。

l = [1, 2, 3]
print(l * 3)

実行結果

[1, 2, 3, 1, 2, 3, 1, 2, 3]

一方、ndarray に対する * 演算子は、下記のプログラムのように 2 つの ndarray の各要素を乗算 した新しい ndarray を作成するという計算を行います。

a1 = np.array([1, 2, 3])
a2 = np.array([4, 5, 6])
print(a1 * a2)

実行結果

[ 4 10 18]

上記の計算は下記の手順で行われます。

  • a1 * a2
  • = [1, 2, 3] * [4, 5, 6]
  • = [1 * 4, 2 * 5, 3 * 6]
  • = [4, 10, 18]

list と list に対して * 演算子で計算を行うと TypeError が発生します。

a1 * 3 のように ndarray と整数型 のデータに対して * 演算子で計算を行うこともできますが、その場合は list と整数型のデータに対して * 演算子が行う 結合処理とは異なる処理 が行われます。詳細はこの後の ブロードキャスト で説明します。

実際にプログラムを実行して確認する作業は省略しますが、各要素の乗算処理 も加算処理と同様に 要素の数が多い程 list よりも ndarray のほうが高速 に行うことができます。

他の算術演算子の違い

list に対して -/ などの 他の算術演算子 の計算を行うと TypeError が発生 しますが、ndarray の場合は下記のプログラムのように 各要素に対する算術演算子の計算 を行った新しい ndarray を作成するという処理が行われます。

print("- 演算子(減算)")
print(a1 - a2)
print("/ 演算子(除算)")
print(a1 / a2)
print("// 演算子(除算の商を計算)")
print(a1 // a2)
print("% 演算子(除算の余りを計算)")
print(a1 % a2)
print("** 演算子(べき乗を計算)")
print(a1 ** a2)

実行結果

- 演算子(減算)
[-3 -3 -3]
/ 演算子(除算)
[0.25 0.4  0.5 ]
// 演算子(除算の商を計算)
[0 0 0]
% 演算子(除算の余りを計算)
[1 2 3]
** 演算子(べき乗を計算)
[  1  32 729]

+ 演算子は ndarray の要素のデータ型が文字列型の場合は文字列の結合の計算を行いますが、それ以外の算術演算子で数値型以外の ndarray に対して計算を行うと基本的には TypeError のエラーが発生します。

他にも & などのビット演算子という分類の演算子などを ndarray に対して利用できます。ビット演算子の詳細については下記のリンク先を参照して下さい。ビット演算については今後の記事で紹介する予定です。

累積代入演算子の違い

+= のような 演算と代入を同時に行う演算子 のことを 累積代入演算子 と呼びます。

list に対して +=*= の累積代入演算子で計算を行うと、下記のプログラムのように新しい list を作成するのではなく、元の list に対する結合処理 が行われます。なお、list に対して 他の累積代入演算子で演算を行うことはできません(TypeError が発生します)。

l = [1, 2, 3]
l += [4, 5, 6]
print(l)
l *= 2
print(l)

実行結果

[1, 2, 3, 4, 5, 6]
[1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6]

ndarray に対しては +=*= だけでなく 他の累積代入演算子の演算も行うことができ、下記のプログラムのように 元の ndarray に対する演算 が行われます。

a1 = np.array([1, 2, 3])
a2 = np.array([4, 5, 6])
a1 += a2
print(a1)
a1 -= a2
print(a1)

実行結果

[5 7 9]
[1 2 3]

比較演算子の違い

list に対して ==比較演算子の演算 を行うと、下記のプログラムのように list の 各要素に対して == の演算 を行い、すべてが True の場合に True を、そうでない場合に False を計算するという処理を行います。!= 演算子の場合は == 演算子の逆 の計算を行います。

l1 = [1, 2, 3]
l2 = [4, 5, 6]
l3 = [1, 2, 3]
print(l1 == l2)
print(l1 == l3)
print(l1 != l2)
print(l1 != l3)

実行結果

False
True
True
False

list に対する 比較演算子の計算 は、下記のような手順で計算が行われます。

  1. 先頭の要素から順番に == 演算子で比較し、等しくない最初の要素を見つける
  2. すべての要素が等しい場合は最後の要素に対して次の手順 3 の処理を行う
  3. 手順 1 または 2 の要素に対して比較演算子の計算を行い、その計算結果を返す

例えば [1, 2, 3] < [1, 2, 5] は 2 番目の要素まで等しいので 3 番目の要素に対して < 演算子で比較を行い、3 < 5True なので True が計算されます。

上記の手順は 辞書の単語の並び順の比較 を行えるため 辞書順の比較 と呼びます。

ndarray に対する 比較演算子 の演算を行うと、下記のプログラムのように それぞれの要素に対して比較演算子による計算 を行った新しい ndarray を作成するという処理を行います。

a1 = np.array([1, 2, 3])
a2 = np.array([4, 5, 6])
a3 = np.array([3, 2, 1])
print(a1 == a2)
print(a1 == a3)
print(a1 < a2)
print(a1 < a3)

実行結果

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

上記の a1 < a3 は下記の手順で計算が行われます。

  • a1 < a3
  • = [1, 2, 3] < [3, 2, 1]
  • = [1 < 3, 2 < 2, 3 < 1]
  • = [True, False, False]

上記のように ndarray の場合は比較演算子であっても、算術演算子の場合と同様に各要素に対する演算を行った新しい ndarray を作成するという処理を行います。

2 次元以上の ndarray に対する演算子の処理

ここまでの例では 1 次元の ndarray に対する演算子の処理を紹介しましたが、下記のプログラムのように 2 次元の ndarray の場合も同様に それぞれの要素に対して演算子の処理を行う という点では変わりません。具体例は示しませんが 3 次元以上の ndarray も同様 です。

a1 = np.array([[1, 2], [3, 4]])
a2 = np.array([[5, 6], [7, 8]])
print(a1 + a2)
print(a1 * a2)

実行結果

[[ 6  8]
 [10 12]]
[[ 5 12]
 [21 32]]

上記の a1 + a2 は下記の手順で計算が行われます。

  • a1 + a2
  • = [[1, 2], [3, 4]] + [[5, 6], [7, 8]]
  • = [[1 + 5, 2 + 6], [3 + 7, 4 + 8]]
  • = [[6, 8], [10, 12]]

ブロードキャスト

これまでの例では 同じ形状の ndarray どうし の演算を行いましたが、下記のプログラムのように 異なる形状の ndarray どうし の演算を行うこともできます。

a3 = np.array([9, 10])
print(a1 + a3)

実行結果

[[10 12]
 [12 14]]

上記のように 異なる形状の ndarray の演算 を行う場合は、形状が小さいほうの ndarray大きいほうの ndarray に拡張 して計算を行います。numpy ではこのことを ブロードキャスト(broadcasting)4と呼びます。

上記の場合は a12 × 22 次元の ndarray で、a3要素を 2 つ持つ 1 次元の ndarray なので a3 のほうが形状が小さい です。従って、a3a1 の形状にブロードキャスト を行ってから計算が行われます。

ブロードキャスト は、元の ndarray の要素を複製 することで行います。例えば a3a1 の形状にブロードキャスト する際には、[a3, a3]、すなわち [[9, 10], [9, 10] という形で a1 の形状と同じ ndarray を作成 します。従って上記の a1 + a3 は下記の手順で計算が行われます。

  • a1 + a3
  • = [[1, 2], [3, 4]] + [9, 10]
  • = [[1, 2], [3, 4]] + [[9, 10], [9, 10]]
  • = [[1 + 9, 2 + 10], [3 + 9, 4 + 10]]
  • = [[10, 12], [12, 14]]

ブロードキャストで良く行われる計算 として、ndarray数値型のデータの演算 が挙げられます。この場合は下記のプログラムのようにブロードキャストによって ndarray と同じ形状 で、すべての要素が数値型と同じデータ の ndarray が作成されて計算が行われます。

print(a1 + 1)
print(a1 == 1)

実行結果

[[2 3]
 [4 5]]
[[ True False]
 [False False]]

上記の a1 == 1 は下記の手順で計算が行われます。

  • a1 == 1
  • = [[1, 2], [3, 4]] == 1
  • = [[1, 2], [3, 4]] == [[1, 1], [1, 1]]
  • = [[1 == 1, 2 == 1], [3 == 1, 4 == 1]]
  • = [[True, False], [False, False]]

ブロードキャストは非常に便利なのですが、詳細な手順は若干複雑です。詳細は下記のリンク先を参照して下さい。今回の記事では最も単純でわかりやすい 数値型のデータに対するブロードキャスト のみを行います。

ブロードキャストを利用した判定処理

これまでは 直線上に同じマークが並んでいるか どうかを "".join でその直線上の マークの文字列を結合した値を計算 して判定を行っていましたが、この方法はあまり 直観的な方法ではない ため、プログラムがわかりづらい という欠点があります。

直観的には 直線上のマークの数を数えたほうがわかりやすい ので、その方法で判定を行うプログラムを紹介します。具体的には先ほど説明した ブロードキャスト を利用して 直線上のマークを表す ndarray指定したマークが等しいか どうかを表す ndarray を計算し、その中の True の数を数える という方法です。

下記は NpBoard を利用した ゲーム盤を表す ndarray に対して以下の処理を行うプログラムです。実行結果から 正しい計算が行われていることが確認 できます。

  • (0, 0) に着手を行いゲーム盤を表示する
  • 0 列を表す ndarray を計算して表示する
  • == 演算子とブロードキャストを利用して 〇 が配置されているかどうかを表す ndarray を計算して表示する
from marubatsu import Marubatsu, NpBoard

mb = Marubatsu(boardclass=NpBoard)
mb.cmove(0, 0)
print(mb)
col0 = mb.board.board[0, :]
print(col0)
col0circle = col0 == Marubatsu.CIRCLE
print(col0circle)

実行結果

Turn x
O..
...
...

['o' '.' '.']
[ True False False]

True の要素の数を数える方法

python では TrueFalse に対する 四則演算(+-×÷)などの計算処理 を行うと、True1False0 みなして計算を行います。従って下記のプログラムのように sum または np.sum を利用して 0 列の 〇 のマークの数を計算 することができます。

print(sum(col0circle))
print(np.sum(col0circle))

実行結果

1
1

ただし、TrueFalse を要素とする ndarray の True の要素の数を数える 場合は、下記のプログラムのように ndarray0 でない(non zero)要素の数を数える np.count_nonzero を利用することができ、そのほうが 処理速度が高速 になります。

print(np.count_nonzero(col0circle))

実行結果

1

下記は上記の 3 種類の処理速度を計測するプログラムで、実行結果から np.count_nonzero の処理速度が最も速く5、下記では他よりも約 10 倍速いことが確認できます。

%timeit sum(col0circle)
%timeit np.sum(col0circle)
%timeit np.count_nonzero(col0circle)

実行結果

2.41 μs ± 21.9 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
3.18 μs ± 125 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
329 ns ± 9.89 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

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

is_winner の修正

上記の方法で判定を行うように is_winner を下記のプログラムのように修正します。

  • 4 行目の下にあった winstr を計算する処理を削除した
  • 5、6、9、13 行目:上記の方法で判定を行うように修正した。np.count_nonzero で計算を行う際は list ではなく ndarray に対して行ったほうが処理速度が速い ので tolist で list に変換する 処理を行わないほうが良い 点に注意すること
 1  def is_winner(self, player, last_move):
 2      x, y = last_move
 3      if self.count_linemark:
元と同じなので省略
 4      else:
 5          if np.count_nonzero(self.board[x, :] == player) == self.BOARD_SIZE  or \
 6          np.count_nonzero(self.board[:, y] == player) == self.BOARD_SIZE:
 7              return True
 8          # 左上から右下方向の判定
 9          if x == y and np.count_nonzero(np.diag(self.board) == player) == self.BOARD_SIZE:
10              return True
11          # 右上から左下方向の判定
12          if x + y == self.BOARD_SIZE - 1 and \
13              np.count_nonzero(np.diag(np.fliplr(self.board)) == player) == self.BOARD_SIZE:
14              return True
15      
16      # どの一直線上にも配置されていない場合は、player は勝利していないので False を返す
17      return False 
18  
19  NpBoard.is_winner = is_winner
行番号のないプログラム
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[x, :] == player) == self.BOARD_SIZE  or \
        np.count_nonzero(self.board[:, y] == player) == 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 

NpBoard.is_winner = is_winner
修正箇所
def is_winner(self, player, last_move):
    x, y = last_move
    if self.count_linemark:
元と同じなので省略
    else:
-       winstr = player * self.BOARD_SIZE
-       if "".join(self.board[x, :].tolist()) == winstr  or \
-       "".join(self.board[:, y].tolist()) == winstr:
+       if np.count_nonzero(self.board[x, :] == player) == self.BOARD_SIZE  or \
+       np.count_nonzero(self.board[:, y] == player) == self.BOARD_SIZE:
            return True
        # 左上から右下方向の判定
-       if x == y and "".join(np.diag(self.board).tolist()) == winstr:        
+       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 \
-           "".join(np.diag(np.fliplr(self.board)).tolist()) == winstr:
+           np.count_nonzero(np.diag(np.fliplr(self.board)) == player) == self.BOARD_SIZE:
            return True
    
    # どの一直線上にも配置されていない場合は、player は勝利していないので False を返す
    return False 

NpBoard.is_winner = is_winner

上記の修正後に下記のプログラムで ベンチマークを実行 して 処理速度を測定 します。count_linemarkTrue の場合は 上記で修正した処理は実行されないので省略 しました。

from util import benchmark

boardclass = NpBoard
count_linemark = False
print(f"boardclass: {boardclass.__name__}, count_linemark {count_linemark}")
benchmark(mbparams={"boardclass": boardclass, "count_linemark": count_linemark})
print()

実行結果

boardclass: NpBoard, count_linemark False
ai2 VS ai2
100%|██████████| 50000/50000 [00:08<00:00, 5985.44it/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:14<00:00, 672.35it/s]
count     win    lose    draw
o       49446       0     554
x       44043       0    5957
total   93489       0    6511

ratio     win    lose    draw
o       98.9%    0.0%    1.1%
x       88.1%    0.0%   11.9%
total   93.5%    0.0%    6.5%

ai_abs_dls
 42.2 ms ±   2.9 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

下記は修正前の count_linemarkFalse の場合の前回の記事のベンチマークに上記の実行結果を加えた表です。

修正箇所 ai2 VS ai2 ai14s VS ai2 ai_abs_dls
修正前 7778.06 回/秒 732.36 回/秒 39.2 ms
ブロードキャストによる判定処理 5985.44 回/秒 672.35 回/秒 42.2 ms

上記から残念ながら count_linemarkFalse の場合の 処理速度がいずれも遅くなった ことが確認できました。これはおそらく 上記の処理 が「各マスがマークが等しいかどうかを判定する」、「True の数を数える」、「True の数とゲーム盤のサイズが等しいことを判定する」という 3 つの処理 であるのに対し、修正前の処理が「各マスの文字列を連結する」、「連結した文字列とゲーム盤のサイズだけマークを連結した文字列が等しいことを判定する」という 2 つの処理 であることが理由ではないかと考えられます。

なお、上記の == 演算子と count_nonzero を利用した処理は、"".join を利用したプログラム よりもわかりやすく、実際にこの方法で ndarray の中で 特定の値の要素の数を数える処理は良く行われます。また、"".join による判定すべての要素が同じ文字列であることを判定 することはできますが、次で修正を行う count_markpats のように、特定の要素の数を数える 必要がある場合に 利用することはできません

NpBoard はどのように修正しても ListBoard よりも処理速度が遅くなるようなので、本記事では わかりやすさを重視して上記の修正を採用 することにします。

count_markpats の処理の修正その 1

マークのパターンの数を数える count_markpats では、count_linemarkFalse の場合は下記の count_marks を利用して 各直線上のマークのパターンを計算 しています。

def count_marks(self, turn, last_turn, x, y, dx, dy, datatype="dict"):
    count = defaultdict(int)
    for _ in range(self.BOARD_SIZE):
        count[self.board[x][y]] += 1
        x += dx
        y += dy
    if datatype == "dict":
        return count
    else:
        return Markpat(count[last_turn], count[turn], count[Marubatsu.EMPTY])      

上記の count_marks では直線上の 最初のマスの座標 を仮引数 xy、直線上の 次のマス x, y 座標の差分 を仮引数 dxdy に代入することで直線上の各マスの座標を計算するという処理を行っていますが、直線上のマスの一覧 のデータは前回の記事で説明したように、スライス表記や np.diag などを利用して計算でき、そのほうが プログラムがわかりやすくなる ので、そのように修正することにします。

そのために、count_marks の処理を下記のように修正することにします。

  • マークのパターンを計算する 直線上のマスの一覧count_marks を呼び出す count_markpats 内で計算 し、そのデータを代入 する仮引数 linedata を追加 する
  • 仮引数 xydxdy を削除 する
  • 確認した所、datatype"dict" の場合の処理を行うプログラムは 現状ではもう存在せず、今後もおそらく利用することはないと思われるので仮引数 datatype を削除 する
  • 返り値を Markpat のインスタンスとしているが、返り値は tuple でもかまわない点 と、tuple のほうが処理速度が速い ので tuple で返すようにする

count_marks の修正

下記はそのように count_marks を修正したプログラムです。

  • 3 行目:仮引数 linedata を追加し、xydxdydatatype を削除する
  • 5、6 行目linedata からマークを取り出してマークのパターンを計算するようにした
  • 7 行目datatype に関する処理を削除し、tuple で返り値を返すようにした
1  from collections import defaultdict
2  
3  def count_marks(self, linedata, turn, last_turn):
4      count = defaultdict(int)
5      for mark in linedata:
6          count[mark] += 1
7      return count[last_turn], count[turn], count[Marubatsu.EMPTY]
8  
9  NpBoard.count_marks = count_marks 
行番号のないプログラム
from collections import defaultdict

def count_marks(self, linedata, turn, last_turn):
    count = defaultdict(int)
    for mark in linedata:
        count[mark] += 1
    return count[last_turn], count[turn], count[Marubatsu.EMPTY]

NpBoard.count_marks = count_marks 
修正箇所
from collections import defaultdict

def count_marks(self, linedata, turn, last_turn):
    count = defaultdict(int)
-   for _ in range(self.BOARD_SIZE):
+   for mark in linedata:
-       count[self.board[x][y]] += 1
+       count[mark] += 1
-       x += dx
-       y += dy

-   if datatype == "dict":
-       return count
-   else:
-       return Markpat(count[last_turn], count[turn], count[Marubatsu.EMPTY])  
+   return count[last_turn], count[turn], count[Marubatsu.EMPTY]

NpBoard.count_marks = count_marks 

count_markpats の修正

次に、下記のプログラムのように count_markpats 内で 直線上のマークの一覧を計算 するように修正します。

  • 8、10、13、16 行目前回の記事と同じ方法で直線上のマスの一覧を表す ndarray を計算し、それを count_marks の実引数に記述するように修正した
 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.diag(self.board), turn, last_turn)
14          markpats[count] += 1
15          # 右上から左下方向の判定
16          count = self.count_marks(np.diag(np.fliplr(self.board)), turn, last_turn)
17          markpats[count] += 1
18  
19      return markpats  
20  
21  NpBoard.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[Marubatsu.CIRCLE], countdict[Marubatsu.CROSS]):
                emptycount = self.BOARD_SIZE - circlecount - crosscount
                if last_turn == Marubatsu.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.diag(self.board), turn, last_turn)
        markpats[count] += 1
        # 右上から左下方向の判定
        count = self.count_marks(np.diag(np.fliplr(self.board)), turn, last_turn)
        markpats[count] += 1

    return markpats  

NpBoard.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(turn, last_turn, x=0, y=i, dx=1, dy=0, datatype="tuple")
+           count = self.count_marks(self.board[i, :], turn, last_turn)
            markpats[count] += 1
-           count = self.count_marks(turn, last_turn, x=i, y=0, dx=0, dy=1, datatype="tuple")
+           count = self.count_marks(self.board[:, i], turn, last_turn)
            markpats[count] += 1
        # 左上から右下方向の判定
-       count = self.count_marks(turn, last_turn, x=0, y=0, dx=1, dy=1, datatype="tuple")
+       count = self.count_marks(np.diag(self.board), turn, last_turn)
        markpats[count] += 1
        # 右上から左下方向の判定
-       count = self.count_marks(turn, last_turn, x=self.BOARD_SIZE - 1, y=0, dx=-1, dy=1, datatype="tuple")
+       count = self.count_marks(np.diag(np.fliplr(self.board)), turn, last_turn)
        markpats[count] += 1

    return markpats  

NpBoard.count_markpats = count_markpats

上記の修正後に下記のプログラムでベンチマークを実行して処理速度を測定します。count_linemarkTrue の場合の処理は変わらないので省略 しました。

boardclass = NpBoard
count_linemark = False
print(f"boardclass: {boardclass.__name__}, count_linemark {count_linemark}")
benchmark(mbparams={"boardclass": boardclass, "count_linemark": count_linemark})
print()

実行結果

boardclass: NpBoard, count_linemark False
ai2 VS ai2
100%|██████████| 50000/50000 [00:08<00:00, 5814.46it/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:29<00:00, 557.79it/s]
count     win    lose    draw
o       49446       0     554
x       44043       0    5957
total   93489       0    6511

ratio     win    lose    draw
o       98.9%    0.0%    1.1%
x       88.1%    0.0%   11.9%
total   93.5%    0.0%    6.5%

ai_abs_dls
 41.7 ms ±   1.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

下記は先ほどの count_linemarkFalse の場合の結果に上記の実行結果を加えた表です。

修正箇所 ai2 VS ai2 ai14s VS ai2 ai_abs_dls
修正前(ブロードキャストの利用) 5985.44 回/秒 672.35 回/秒 42.2 ms
count_markpats の修正その 1 5814.46 回/秒 557.79 回/秒 41.7 ms

上記で修正した count_markpatsai14s から呼び出されるので、ai14s VS ai2 と、静的評価関数として ai14s が呼び出される ai_abs_dls の処理で呼び出されます。

上記から残念ながら count_markpats が呼び出される ai14s VS ai2処理速度が遅くなった ことが確認できました。ai_abs_dls では全体の処理の中で count_markpats の処理時間の割合が小さい ので大きな変化は見られません。また、ai2 VS ai2 の処理では count_markpats が呼び出されない ので処理速度は ほとんど変化しません

count_markpats の修正その 2

count_marks では直線上の各マスのマークを 順番に調べて数える ことでマークのパターンを計算していましたが、下記のプログラムのように 〇 と × のマークの数を count_nonzero で計算 することもできます。なお、空のマスの数ゲーム盤のサイズから 〇 と × のマークの数を引く ことで計算できます。

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  NpBoard.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

NpBoard.count_marks = count_marks 
修正箇所
def count_marks(self, linedata, turn, last_turn):
-   count = defaultdict(int)
-   for mark in linedata:
-       count[mark] += 1
+   turn_count = np.count_nonzero(linedata == turn)
+   lastturn_count = np.count_nonzero(linedata == last_turn)
+   emptycount = self.BOARD_SIZE - turn_count - lastturn_count
-   return count[last_turn], count[turn], count[Marubatsu.EMPTY]
+   return lastturn_count, turn_count, emptycount

NpBoard.count_marks = count_marks 

上記の修正後に下記のプログラムでベンチマークを実行して処理速度を測定します。count_linemarkTrue の場合の処理は変わらないので省略 しました。

boardclass = NpBoard
count_linemark = False
print(f"boardclass: {boardclass.__name__}, count_linemark {count_linemark}")
benchmark(mbparams={"boardclass": boardclass, "count_linemark": count_linemark})
print()

実行結果

boardclass: NpBoard, count_linemark False
ai2 VS ai2
100%|██████████| 50000/50000 [00:08<00:00, 6060.90it/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:49<00:00, 456.02it/s]
count     win    lose    draw
o       49446       0     554
x       44043       0    5957
total   93489       0    6511

ratio     win    lose    draw
o       98.9%    0.0%    1.1%
x       88.1%    0.0%   11.9%
total   93.5%    0.0%    6.5%

ai_abs_dls
 41.7 ms ±   2.1 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

下記は先ほどの count_linemarkFalse の場合の結果に上記の実行結果を加えた表です。

修正箇所 ai2 VS ai2 ai14s VS ai2 ai_abs_dls
修正前(ブロードキャストの利用) 5985.44 回/秒 672.35 回/秒 42.2 ms
count_markpats の修正その 1 5814.46 回/秒 557.79 回/秒 41.7 ms
count_markpats の修正その 2 6060.90 回/秒 456.02 回/秒 41.7 ms

上記から残念ながら ai14s VS ai2 の処理速度が さらに遅くなった ことが確認できました。遅くなる原因は、修正その 1 では直線上の各マスに対して 1 回の繰り返し処理 で 〇 と × のマーク数を数えていたのに対し、修正その 2 では 〇 と × のそれぞれに対して 別々にマークの数を数える処理を行っている ためです。ただし、後で説明しますが、ゲーム盤のサイズが大きい場合修正その 2 の方法のほうが 処理速度が速くなります

なお、先程と同様の理由で ai2 VS ai2ai_abs_dls の処理速度はほぼ変化しません。

count_markpats の修正その 3

マークのパターンの計算は、list などの 反復可能オブジェクトのそれぞれの要素の個数を数える count モジュールの Counter クラスを利用して計算することができます。

Counter の実引数反復可能オブジェクトを記述 して呼び出すと、反復可能オブジェクト内の 各要素の個数を計算 した dict のようなインスタンス が作成されます。下記のプログラムは (0, 0) に 〇 を着手 した局面の 0 列 のデータを Counter の実引数に記述 して、0 列〇 と × と空のマスの個数を計算 するプログラムです。実行結果から 〇 を表す o の要素が 1 つ× を表す o の要素が 0空のマスを表す . の要素が 2 つ あることが確認できます。

なお、print で表示される np.str_('o') は、Counter が数えた値が 文字列(str)型の "o" が記録された ndarray の要素 であることを表しますが、このデータは 通常の文字列の "o" と同じように扱われる ので、counter[Marubatsu.CIRCLE]"o" のキーの値を参照 できます。また、"x" のキーは存在しません が、その場合に "x" のキーの値を参照すると 0 が計算される点が dict と異なるので上記では dict のようなインスタンスと説明しました。

from collections import Counter

print(mbnp.board.board[0, :])
counter = Counter(mbnp.board.board[0, :])
print(counter)
print(counter[Marubatsu.CIRCLE])
print(counter[Marubatsu.CROSS])
print(counter[Marubatsu.EMPTY])

実行結果

['o' '.' '.']
Counter({np.str_('.'): 2, np.str_('o'): 1})
1
0
2

下記は上記の Counter を利用するように count_marks を修正するプログラムです。

  • 2 行目:直線上のマスのデータを表す linedata を実引数に記述して Counter を呼び出すことで、linedata の 〇 と × と 空のマスの数を数えたデータを計算する。Counter を利用 する際は ndarray よりも list の方が処理速度が速い ので 前回の記事で説明した tolist で ndarray を list に変換した
  • 3 ~ 5 行目turnlast_turn のマークの数を変数に代入し、5 行目で空のマスの数を計算する
1  def count_marks(self, linedata, turn, last_turn):
2      counter = Counter(linedata.tolist())
3      turn_count = counter[turn]
4      lastturn_count = counter[last_turn]
5      emptycount = self.BOARD_SIZE - turn_count - lastturn_count
6      return lastturn_count, turn_count, emptycount
7  
8  NpBoard.count_marks = count_marks 
行番号のないプログラム
def count_marks(self, linedata, turn, last_turn):
    counter = Counter(linedata.tolist())
    turn_count = counter[turn]
    lastturn_count = counter[last_turn]
    emptycount = self.BOARD_SIZE - turn_count - lastturn_count
    return lastturn_count, turn_count, emptycount

NpBoard.count_marks = count_marks 
修正箇所
def count_marks(self, linedata, turn, last_turn):
+   counter = Counter(linedata.tolist())
-   turn_count = np.count_nonzero(linedata == turn)
+   turn_count = counter[turn]
-   lastturn_count = np.count_nonzero(linedata == last_turn)
+   lastturn_count = counter[last_turn]
    emptycount = self.BOARD_SIZE - turn_count - lastturn_count
    return lastturn_count, turn_count, emptycount

NpBoard.count_marks = count_marks 

上記の修正後に下記のプログラムでベンチマークを実行して処理速度を測定します。count_linemarkTrue の場合の処理は変わらないので省略しました。

boardclass = NpBoard
count_linemark = False
print(f"boardclass: {boardclass.__name__}, count_linemark {count_linemark}")
benchmark(mbparams={"boardclass": boardclass, "count_linemark": count_linemark})
print()

実行結果

boardclass: NpBoard, count_linemark False
ai2 VS ai2
100%|██████████| 50000/50000 [00:08<00:00, 6032.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 [01:16<00:00, 650.49it/s]
count     win    lose    draw
o       49446       0     554
x       44043       0    5957
total   93489       0    6511

ratio     win    lose    draw
o       98.9%    0.0%    1.1%
x       88.1%    0.0%   11.9%
total   93.5%    0.0%    6.5%

ai_abs_dls
 42.4 ms ±   3.8 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

下記は先ほどの count_linemarkFalse の場合の結果に上記の実行結果を加えた表です。

修正箇所 ai2 VS ai2 ai14s VS ai2 ai_abs_dls
修正前(ブロードキャストの利用) 5985.44 回/秒 672.35 回/秒 42.2 ms
count_markpats の修正その 1 5814.46 回/秒 557.79 回/秒 41.7 ms
count_markpats の修正その 2 6060.90 回/秒 456.02 回/秒 41.7 ms
count_markpats の修正その 3 6060.90 回/秒 650.49 回/秒 41.7 ms

上記から Counter を利用することで、count_markpats の修正を行う前と同じくらい の処理速度で計算が行われることが確認できました。処理速度はほとんど変わりませんが プログラムがわかりやすくなる という利点があるので、本記事では Counter を利用した 修正その 3 を採用 することにします。

Countercount_nonzero による処理速度の違い

上記では Countercount_nonzero による処理速度の違いはほとんどありませんが、要素の数が増えた場合count_nonzero の方が処理速度が速く なります。下記のプログラムはそのことを検証するプログラムで、".""o""x"100 個ずつ要素として持つ list に対して Count でそれぞれの 要素の数を数える処理 と、同じ要素を持つ ndarray に対して "o""x" の要素の数を count_nonzero で計算 する 処理時間を計測 しています。

l = [".", "o", "x"] * 100
print(Counter(l))
%timeit Counter(l)
a = np.array(l)
print(np.count_nonzero(a == Marubatsu.CIRCLE))
print(np.count_nonzero(a == Marubatsu.CROSS))
%timeit np.count_nonzero(a == Marubatsu.CIRCLE)
%timeit np.count_nonzero(a == Marubatsu.CROSS)

実行結果

Counter({'.': 100, 'o': 100, 'x': 100})
10.4 μs ± 40.2 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
100
100
4.61 μs ± 34.6 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
4.76 μs ± 148 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)

実行結果から Counter の処理時間 である 10.4 μs は、count_nonzeroox の要素の数を計算する 4.61 + 4.76 = 9.37 μs よりも長い ことが確認できます。下記のプログラムのように 要素の数を増やすとさらに増やして 1000 にすると、実行結果の 121 μs と 35.9 + 34.9 = 70.8 μs のように、 count_nonzero の処理時間のほうがさらに短く なります。

l = [".", "o", "x"] * 1000
print(Counter(l))
%timeit Counter(l)
a = np.array(l)
print(np.count_nonzero(a == Marubatsu.CIRCLE))
print(np.count_nonzero(a == Marubatsu.CROSS))
%timeit np.count_nonzero(a == Marubatsu.CIRCLE)
%timeit np.count_nonzero(a == Marubatsu.CROSS)

実行結果

Counter({'.': 1000, 'o': 1000, 'x': 1000})
121 μs ± 500 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
1000
1000
35.9 μs ± 1.2 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
34.9 μs ± 277 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

〇× ゲームはゲーム盤のサイズが小さいので Counter の方が処理速度が速いですが、もっと大きなゲーム盤 の場合は count_nonzero を利用したほうが処理速度が速くなる でしょう6

board_to_str の修正

ndarray には flatten という任意の次元の ndarray を 1 次元の ndarray に変換 するメソッドがあります。下記は (0, 0)、(1, 0)、(2, 0) の順で着手 を行ったゲーム盤を表す 2 次元の ndarrayflatten で 1 次元の ndarray に変換 するプログラムです。

mb = Marubatsu(boardclass=NpBoard)
mb.cmove(0, 0)
mb.cmove(1, 0)
mb.cmove(2, 0)
print(mb.board.board)
print(mb.board.board.flatten())

実行結果

[['o' '.' '.']
 ['x' '.' '.']
 ['o' '.' '.']]
['o' '.' '.' 'x' '.' '.' 'o' '.' '.']

下記のプログラムのように、flatten で変換された 1 次元の ndarray に対して "".join を利用して 要素の文字列を結合 することで、実行結果のように board_to_str と同じ処理 を行うことができます。なお、前回の記事で説明したように list のほうが "".join の処理速度が速い ので tolist で ndarray を list に変換 しました。

なお、ndarray はその次元に関わらず 内部では 1 次元の配列でデータを記録 しているので、flatten の処理を 高速に行うことができます

print("".join(mbnp.board.board.flatten().tolist()))
print(mbnp.board_to_str())

実行結果

o..x..o..
o..x..o..

下記は上記の方法で計算するように board_to_str を修正したプログラムです。

def board_to_str(self):
    return "".join(self.board.flatten().tolist())

NpBoard.board_to_str = board_to_str
修正箇所
def board_to_str(self):
-   txt = ""
-   for col in self.board:
-       txt += "".join(col)
-   return txt
+   return "".join(self.board.flatten().tolist())

NpBoard.board_to_str = board_to_str

ndarray の flatten メソッドの詳細については下記のリンク先を参照して下さい。

上記の修正後に下記のプログラムでベンチマークを実行して処理速度を測定します。board_to_strcount_linemark の値に関わらず ai_abs_dls から呼び出される ので、count_linemarkFalseTrue の両方の場合 のベンチマークを行います。

boardclass = NpBoard
for count_linemark in [False, True]:
    print(f"boardclass: {boardclass.__name__}, count_linemark {count_linemark}")
    benchmark(mbparams={"boardclass": boardclass, "count_linemark": count_linemark})
    print()

実行結果

boardclass: NpBoard, count_linemark False
ai2 VS ai2
100%|██████████| 50000/50000 [00:08<00:00, 5935.01it/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:16<00:00, 655.54it/s]
count     win    lose    draw
o       49446       0     554
x       44043       0    5957
total   93489       0    6511

ratio     win    lose    draw
o       98.9%    0.0%    1.1%
x       88.1%    0.0%   11.9%
total   93.5%    0.0%    6.5%

ai_abs_dls
 37.4 ms ±   4.4 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

boardclass: NpBoard, count_linemark True
ai2 VS ai2
100%|██████████| 50000/50000 [00:06<00:00, 8277.09it/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:31<00:00, 1612.00it/s]
count     win    lose    draw
o       49446       0     554
x       44043       0    5957
total   93489       0    6511

ratio     win    lose    draw
o       98.9%    0.0%    1.1%
x       88.1%    0.0%   11.9%
total   93.5%    0.0%    6.5%

ai_abs_dls
 30.1 ms ±   1.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

下記は先ほどの count_linemarkFalse の場合の結果に上記の実行結果を加えた表です。

修正箇所 ai2 VS ai2 ai14s VS ai2 ai_abs_dls
修正前(ブロードキャストの利用) 5985.44 回/秒 672.35 回/秒 42.2 ms
count_markpats の修正その 3 6060.90 回/秒 650.49 回/秒 41.7 ms
board_to_str の修正 5935.01 回/秒 655.54 回/秒 37.4 ms

上記の実行結果から board_to_str を利用しない ai2 VS ai2ai14 VS ai2 の処理速度は ほとんど変わりません が、board_to_str を利用する ai_abs_dls の処理速度が速くなった ことが確認できます。

board_to_str を修正する前前回の記事での count_linemarkTrue の場合の ai_abs_dls のベンチマークの結果は 38.8 ms でした。上記の結果は 30.1 ms なので、count_linemarkTrue の場合はかなり処理速度が速くなったことが確認できます。

Markpat の廃止

今回の記事で NpBoardcalc_markpats の返り値 を Markpat のインスタンスから tuple に変更 しましたので、ListBoard と List1dBoard でも同様の修正 を行うことにします。下記はそのように修正したプログラムです。

  • 3、36 行目:ListBoard と List1dBoard クラスの count_marks の仮引数 datatype を削除する
  • 10、43 行目:istBoard と List1dBoard クラスの count_marksdatatype に関する処理を削除し、tuple を返すように修正する
  • 21、23、26、29 行目:ListBoard クラスの count_markpats 内で count_marks を呼び出す際に実引数 datatype を削除する
 1  from marubatsu import ListBoard, List1dBoard
 2  
 3  def count_marks(self, turn, last_turn, x, y, dx, dy):
 4      count = defaultdict(int)
 5      for _ in range(self.BOARD_SIZE):
 6          count[self.board[x][y]] += 1
 7          x += dx
 8          y += dy
 9  
10      return (count[last_turn], count[turn], count[Marubatsu.EMPTY])    
11  
12  ListBoard.count_marks = count_marks 
13  
14  def count_markpats(self, turn, last_turn):
15      markpats = defaultdict(int)
16      
17      if self.count_linemark:
元と同じなので省略
18      else:
19          # 横方向と縦方向の判定
20          for i in range(self.BOARD_SIZE):
21              count = self.count_marks(turn, last_turn, x=0, y=i, dx=1, dy=0)
22              markpats[count] += 1
23              count = self.count_marks(turn, last_turn, x=i, y=0, dx=0, dy=1)
24              markpats[count] += 1
25          # 左上から右下方向の判定
26          count = self.count_marks(turn, last_turn, x=0, y=0, dx=1, dy=1)
27          markpats[count] += 1
28          # 右上から左下方向の判定
29          count = self.count_marks(turn, last_turn, x=self.BOARD_SIZE - 1, y=0, dx=-1, dy=1)
30          markpats[count] += 1
31  
32      return markpats     
33  
34  ListBoard.count_markpats = count_markpats
35  
36  def count_marks(self, turn, last_turn, x, y, dx, dy):
37      count = defaultdict(int)
38      for _ in range(self.BOARD_SIZE):
39          count[self.board[y + x * self.BOARD_SIZE]] += 1
40          x += dx
41          y += dy
42  
43      return (count[last_turn], count[turn], count[Marubatsu.EMPTY])    
44  
45  List1dBoard.count_marks = count_marks 
行番号のないプログラム
from marubatsu import ListBoard, List1dBoard

def count_marks(self, turn, last_turn, x, y, dx, dy):
    count = defaultdict(int)
    for _ in range(self.BOARD_SIZE):
        count[self.board[x][y]] += 1
        x += dx
        y += dy

    return (count[last_turn], count[turn], count[Marubatsu.EMPTY])    

ListBoard.count_marks = count_marks 

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[Marubatsu.CIRCLE], countdict[Marubatsu.CROSS]):
                emptycount = self.BOARD_SIZE - circlecount - crosscount
                if last_turn == Marubatsu.CIRCLE:
                    markpats[(circlecount, crosscount, emptycount)] += 1
                else:
                    markpats[(crosscount, circlecount, emptycount)] += 1
    else:
        # 横方向と縦方向の判定
        for i in range(self.BOARD_SIZE):
            count = self.count_marks(turn, last_turn, x=0, y=i, dx=1, dy=0)
            markpats[count] += 1
            count = self.count_marks(turn, last_turn, x=i, y=0, dx=0, dy=1)
            markpats[count] += 1
        # 左上から右下方向の判定
        count = self.count_marks(turn, last_turn, x=0, y=0, dx=1, dy=1)
        markpats[count] += 1
        # 右上から左下方向の判定
        count = self.count_marks(turn, last_turn, x=self.BOARD_SIZE - 1, y=0, dx=-1, dy=1)
        markpats[count] += 1

    return markpats     

ListBoard.count_markpats = count_markpats

def count_marks(self, turn, last_turn, x, y, dx, dy):
    count = defaultdict(int)
    for _ in range(self.BOARD_SIZE):
        count[self.board[y + x * self.BOARD_SIZE]] += 1
        x += dx
        y += dy

    return (count[last_turn], count[turn], count[Marubatsu.EMPTY])    

List1dBoard.count_marks = count_marks 
修正箇所
from marubatsu import ListBoard, List1dBoard

-def count_marks(self, turn, last_turn, x, y, dx, dy, datatype="dict"):
+def count_marks(self, turn, last_turn, x, y, dx, dy):
    count = defaultdict(int)
    for _ in range(self.BOARD_SIZE):
        count[self.board[x][y]] += 1
        x += dx
        y += dy

-   if datatype == "dict":
-       return count
-   else:
-       return Markpat(count[last_turn], count[turn], count[Marubatsu.EMPTY])  
+   return (count[last_turn], count[turn], count[Marubatsu.EMPTY])    

ListBoard.count_marks = count_marks 

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(turn, last_turn, x=0, y=i, dx=1, dy=0, datatype="tuple")
+           count = self.count_marks(turn, last_turn, x=0, y=i, dx=1, dy=0)
            markpats[count] += 1
-           count = self.count_marks(turn, last_turn, x=i, y=0, dx=0, dy=1, datatype="tuple")
+           count = self.count_marks(turn, last_turn, x=i, y=0, dx=0, dy=1)
            markpats[count] += 1
        # 左上から右下方向の判定
-       count = self.count_marks(turn, last_turn, x=0, y=0, dx=1, dy=1, datatype="tuple")
+       count = self.count_marks(turn, last_turn, x=0, y=0, dx=1, dy=1)
        markpats[count] += 1
        # 右上から左下方向の判定
-       count = self.count_marks(turn, last_turn, x=self.BOARD_SIZE - 1, y=0, dx=-1, dy=1, datatype="tuple")
+       count = self.count_marks(turn, last_turn, x=self.BOARD_SIZE - 1, y=0, dx=-1, dy=1)
        markpats[count] += 1

    return markpats     

ListBoard.count_markpats = count_markpats

-def count_marks(self, turn, last_turn, x, y, dx, dy, datatype="dict"):
+def count_marks(self, turn, last_turn, x, y, dx, dy):
    count = defaultdict(int)
    for _ in range(self.BOARD_SIZE):
        count[self.board[y + x * self.BOARD_SIZE]] += 1
        x += dx
        y += dy

-   if datatype == "dict":
-       return count
-   else:
-       return Markpat(count[last_turn], count[turn], count[Marubatsu.EMPTY])  
+   return (count[last_turn], count[turn], count[Marubatsu.EMPTY])    

List1dBoard.count_marks = count_marks 

また、ai14s 内で Markpat を利用 している部分があるので、下記のプログラムのように その部分を tuple に修正 します。なお、ai14s 以外 の AI の関数でも同様の記述がありますが、それらの関数は今後利用する予定はあまりないので 修正は省略 します。

  • 6、7、9、12、15、17 行目markpats の添字を tuple に修正する
 1  from ai import ai_by_score
 2  
 3  @ai_by_score
 4  def ai14s(mb, debug=False, score_victory=300, score_sure_victory=200, score_defeat=-100, score_special=100, score_201=2, score_102=0.5, score_012=-1):
元と同じなので省略
 5      # 相手が勝利できる場合は評価値を加算する
 6      if markpats[(0, 2, 1)] > 0:
 7          score = score_defeat * markpats[(0, 2, 1)]
 8      # 次の自分の手番で自分が必ず勝利できる場合
 9      elif markpats[(2, 0, 1)] >= 2:
10          return score_sure_victory
元と同じなので省略
11      # 次の自分の手番で自分が勝利できる場合は評価値に score_201 を加算する
12      if markpats[(2, 0, 1)] == 1:
13          score += score_201
14      # 「自 1 敵 0 空 2」1 つあたり score_102 だけ、評価値を加算する
15      score += markpats[(1, 0, 2)] * score_102
16      # 「自 0 敵 1 空 2」1 つあたり score_201 だけ、評価値を減算する
17      score += markpats[(0, 1, 2)] * score_012
18      
19      # 計算した評価値を返す
20      return score
行番号のないプログラム
from ai import ai_by_score

@ai_by_score
def ai14s(mb, debug=False, score_victory=300, score_sure_victory=200, \
          score_defeat=-100, score_special=100, score_201=2, \
          score_102=0.5, score_012=-1):
    # 評価値の合計を計算する変数を 0 で初期化する
    score = 0        

    # 自分が勝利している場合
    if mb.status == mb.last_turn:
        return score_victory

    markpats = mb.count_markpats()
    if debug:
        pprint(markpats)
    # 相手が勝利できる場合は評価値を加算する
    if markpats[(0, 2, 1)] > 0:
        score = score_defeat * markpats[(0, 2, 1)]
    # 次の自分の手番で自分が必ず勝利できる場合
    elif markpats[(2, 0, 1)] >= 2:
        return score_sure_victory
    
    # 斜め方向に 〇×〇 が並び、いずれかの辺の 1 つのマスのみに × が配置されている場合
    if  mb.move_count == 4 and \
        mb.board.getmark(1, 1) == Marubatsu.CROSS and \
        (mb.board.getmark(0, 0) == mb.board.getmark(2, 2) == Marubatsu.CIRCLE or \
        mb.board.getmark(2, 0) == mb.board.getmark(0, 2) == Marubatsu.CIRCLE) and \
        (mb.board.getmark(1, 0) == Marubatsu.CROSS or \
        mb.board.getmark(0, 1) == Marubatsu.CROSS or \
        mb.board.getmark(2, 1) == Marubatsu.CROSS or \
        mb.board.getmark(1, 2) == Marubatsu.CROSS):
        return score_special    

    # 次の自分の手番で自分が勝利できる場合は評価値に score_201 を加算する
    if markpats[(2, 0, 1)] == 1:
        score += score_201
    # 「自 1 敵 0 空 2」1 つあたり score_102 だけ、評価値を加算する
    score += markpats[(1, 0, 2)] * score_102
    # 「自 0 敵 1 空 2」1 つあたり score_201 だけ、評価値を減算する
    score += markpats[(0, 1, 2)] * score_012
    
    # 計算した評価値を返す
    return score
修正箇所
from ai import ai_by_score

@ai_by_score
def ai14s(mb, debug=False, score_victory=300, score_sure_victory=200, \
          score_defeat=-100, score_special=100, score_201=2, \
          score_102=0.5, score_012=-1):
元と同じなので省略
    # 相手が勝利できる場合は評価値を加算する
-   if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
+   if markpats[(0, 2, 1)] > 0:
-       score = score_defeat * markpats[Markpat(last_turn=0, turn=2, empty=1)]
+       score = score_defeat * markpats[(0, 2, 1)]
    # 次の自分の手番で自分が必ず勝利できる場合
-   elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
+   elif markpats[(2, 0, 1)] >= 2:
        return score_sure_victory
元と同じなので省略
    # 次の自分の手番で自分が勝利できる場合は評価値に score_201 を加算する
-   if markpats[Markpat(last_turn=2, turn=0, empty=1)] == 1:
+   if markpats[(2, 0, 1)] == 1:
        score += score_201
    # 「自 1 敵 0 空 2」1 つあたり score_102 だけ、評価値を加算する
-   score += markpats[Markpat(last_turn=1, turn=0, empty=2)] * score_102
+   score += markpats[(1, 0, 2)] * score_102
    # 「自 0 敵 1 空 2」1 つあたり score_201 だけ、評価値を減算する
-   score += markpats[Markpat(last_turn=0, turn=1, empty=2)] * score_012
+   score += markpats[(0, 1, 2)] * score_012
    
    # 計算した評価値を返す
    return score

上記の修正後に下記のプログラムで ListBoard、List1dBoard、ArrayBoard、NpBoard に対するベンチマーク を実行します。なお、util.py の benchmark は上記で修正した ai14s を ai.py からインポートしているので利用できないため、面倒ですが benchmark と同じ処理を行う下記のプログラムでベンチマークを行います。ただし、timeit で行っていた ai_abs_dls の処理時間の計測は %timeit で行ったほうが簡単に記述できるので、%timeit で計測するようにしました。

from ai import ai2, ai_abs_dls, ai_match
from marubatsu import ArrayBoard
import random

for boardclass in [ListBoard, List1dBoard, ArrayBoard, NpBoard]:
    for count_linemark in [False, True]:
        print(f"boardclass: {boardclass.__name__}, count_linemark {count_linemark}")

        random.seed(0)    
        mbparams={"boardclass": boardclass, "count_linemark": count_linemark}
        match_num=50000
        ai_match(ai=[ai2, ai2], match_num=match_num, mbparams=mbparams)   
        ai_match(ai=[ai14s, ai2], match_num=match_num, mbparams=mbparams)

        mb = Marubatsu(**mbparams)
        eval_params = {"minimax": True}
        print("ai_abs_dls")
        %timeit ai_abs_dls(mb, eval_func=ai14s, eval_params=eval_params, use_tt=True, maxdepth=8)
        print()

実行結果

boardclass: ListBoard, count_linemark False
ai2 VS ai2
100%|██████████| 50000/50000 [00:03<00:00, 14417.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%

ai14s VS ai2
100%|██████████| 50000/50000 [00:35<00:00, 1414.87it/s]
count     win    lose    draw
o       49446       0     554
x       44043       0    5957
total   93489       0    6511

ratio     win    lose    draw
o       98.9%    0.0%    1.1%
x       88.1%    0.0%   11.9%
total   93.5%    0.0%    6.5%

ai_abs_dls
17.9 ms ± 1.71 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

boardclass: ListBoard, count_linemark True
ai2 VS ai2
100%|██████████| 50000/50000 [00:03<00:00, 14190.42it/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:19<00:00, 2515.18it/s]
count     win    lose    draw
o       49446       0     554
x       44043       0    5957
total   93489       0    6511

ratio     win    lose    draw
o       98.9%    0.0%    1.1%
x       88.1%    0.0%   11.9%
total   93.5%    0.0%    6.5%

ai_abs_dls
18 ms ± 1.28 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

boardclass: List1dBoard, count_linemark False
ai2 VS ai2
100%|██████████| 50000/50000 [00:03<00:00, 16462.19it/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:35<00:00, 1410.48it/s]
count     win    lose    draw
o       49446       0     554
x       44043       0    5957
total   93489       0    6511

ratio     win    lose    draw
o       98.9%    0.0%    1.1%
x       88.1%    0.0%   11.9%
total   93.5%    0.0%    6.5%

ai_abs_dls
17 ms ± 1.09 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

boardclass: List1dBoard, count_linemark True
ai2 VS ai2
100%|██████████| 50000/50000 [00:02<00:00, 17134.46it/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:19<00:00, 2575.17it/s]
count     win    lose    draw
o       49446       0     554
x       44043       0    5957
total   93489       0    6511

ratio     win    lose    draw
o       98.9%    0.0%    1.1%
x       88.1%    0.0%   11.9%
total   93.5%    0.0%    6.5%

ai_abs_dls
17.7 ms ± 214 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)

boardclass: ArrayBoard, count_linemark False
ai2 VS ai2
100%|██████████| 50000/50000 [00:03<00:00, 15494.01it/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:36<00:00, 1378.08it/s]
count     win    lose    draw
o       49446       0     554
x       44043       0    5957
total   93489       0    6511

ratio     win    lose    draw
o       98.9%    0.0%    1.1%
x       88.1%    0.0%   11.9%
total   93.5%    0.0%    6.5%

ai_abs_dls
18.3 ms ± 1.29 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

boardclass: ArrayBoard, count_linemark True
ai2 VS ai2
100%|██████████| 50000/50000 [00:03<00:00, 13786.83it/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:20<00:00, 2436.19it/s]
count     win    lose    draw
o       49446       0     554
x       44043       0    5957
total   93489       0    6511

ratio     win    lose    draw
o       98.9%    0.0%    1.1%
x       88.1%    0.0%   11.9%
total   93.5%    0.0%    6.5%

ai_abs_dls
18.5 ms ± 215 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)

boardclass: NpBoard, count_linemark False
ai2 VS ai2
100%|██████████| 50000/50000 [00:08<00:00, 5707.22it/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:10<00:00, 706.02it/s]
count     win    lose    draw
o       49446       0     554
x       44043       0    5957
total   93489       0    6511

ratio     win    lose    draw
o       98.9%    0.0%    1.1%
x       88.1%    0.0%   11.9%
total   93.5%    0.0%    6.5%

ai_abs_dls
34.3 ms ± 313 μs per loop (mean ± std. dev. of 7 runs, 10 loops each)

boardclass: NpBoard, count_linemark True
ai2 VS ai2
100%|██████████| 50000/50000 [00:06<00:00, 7997.50it/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:25<00:00, 1999.46it/s]
count     win    lose    draw
o       49446       0     554
x       44043       0    5957
total   93489       0    6511

ratio     win    lose    draw
o       98.9%    0.0%    1.1%
x       88.1%    0.0%   11.9%
total   93.5%    0.0%    6.5%

ai_abs_dls
30.4 ms ± 1.49 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

下記は 以前の記事の ListBoard、List1dBoard、ArrayBoard のベンチマークの結果と、先程行った NpBoard のベンチマークの結果に上記の実行結果を加えた表です。下段が上記の実行結果を表します。

boardclass count_linemark ai2 VS ai2 ai14s VS ai2 ai_abs_dls
ListBoard False 14916.51 回/秒
14417.97 回/秒
1116.53 回/秒
1414.87 回/秒
17.4 ms
17.9 ms
ListBoard True 15463.39 回/秒
14910.42 回/秒
2030.27 回/秒
2515.18 回/秒
17.4 ms
18.0 ms
List1dBoard False 17404.27 回/秒
16462.19 回/秒
1145.23 回/秒
1410.48 回/秒
16.7 ms
17.0 ms
List1dBoard True 17176.56 回/秒
17134.46 回/秒
2152.38 回/秒
2575.17 回/秒
17.3 ms
17.7 ms
ArrayBoard False 14702.96 回/秒
15494.01 回/秒
1023.88 回/秒
1378.08 回/秒
18.8 ms
18.3 ms
ArrayBoard True 14645.31 回/秒
13786.83 回/秒
1903.46 回/秒
2436.19 回/秒
19.8 ms
18.5 ms
NpBoard False 5935.01 回/秒
5707.22 回/秒
655.54 回/秒
706.02 回/秒
37.4 ms
34.3 ms
NpBoard True 8227.09 回/秒
7997.50 回/秒
1612.00 回/秒
1999.46 回/秒
30.1 ms
30.4 ms

上記から ai14s VS ai2 の処理速度がかなり向上 したことが確認でき、思いのほか Markpat の処理が tuple と比べて処理時間がかかっていたことが確認できました。なお、count_markpats を利用しない ai2 VS ai2ai_abs_dls の処理速度は先程と同様の理由でほとんど変わりません。

今回の記事のまとめ

今回の記事では ndarray に対する演算ブロードキャスト について説明し、それらを利用して 直線上にマーク並んでいるかどうかを判定する プログラムの記述方法について紹介しました。残念ながら 〇× ゲームの場合はこの記述方法によって 処理速度が遅くなって しまいましたが、プログラムが わかりやすく簡潔に記述できる という利点が得られます。また、〇× ゲームにはあてはまりませんが、ndarray を利用したほうが list よりも 処理速度が速くなる場合が多い ので紹介しました。

また、flatten を利用して 2 次元の ndarray を 1 次元の ndarray に変換 することで board_to_str の処理速度を改善 する方法について紹介し、処理速度の低下の原因となっていた Markpat を廃止して tuple に置き換え ました。

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

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

次回の記事

  1. もしかすると例外があるかもしれませんが、ほぼすべての演算子を利用できると考えて良いと思います

  2. ndarray を print で表示した場合は各要素の間を空白で区切って表示されますが、それに倣って [1 + 4 2 + 5 3 + 6] のように表記するとわかりづらいので , で区切って表記しました。以後の記事でも同様の表記を行います

  3. グラフがまっすぐな直線にならないのは誤差によるものです

  4. broadcast は拡散する、ばらまくという意味の英単語で、放送のことを broadcast と呼びます

  5. 要素の数が 3 つと少ないので np.sum のほうが sum よりも処理速度が遅くなっていますが、要素の数が多い場合は np.sum のほうが sum よりも処理速度が速くなります

  6. マークの種類がもっと多い場合は 1 度ですべてのマークの数を計算できる Counter の処理速度の方が速くなると思います

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?