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を一から作成する その207 axis を指定した numpy の関数によるマークのパターンの計算

0
Last updated at Posted at 2025-12-16

目次と前回の記事

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 の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。

今回の記事の内容

前回の記事では NpBoolBoard クラスの count_markpats を定義しましたが、今回の記事ではその 処理を改良して高速化を試みる ことにします。

確認のためのゲーム盤のデータの作成

プログラムが 正しく動作するかどうかを確認 するための ゲーム盤のデータ を下記のプログラムで作成します。このプログラムは前回の記事と同じです。

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_markpats2 つの対角線マークのパターンの数を数える 処理を行います。それぞれについての改良について順番に説明します。

列と行のマークの計算処理の改良

現状では 3 つの列 のマークのパターンの計算を下記のプログラムのように、for 文による繰り返し処理 によって 1 列ずつ計算 を行っています。3 つの行に関しても同様 です。

   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

NpBoolBoard クラスのように np.bool3 次元の ndarray でゲーム盤を表現した場合は、3 つの列や行のマークのパターンの計算を numpy の関数を利用 して まとめて行うことができる のでその方法について説明します。

2 次元の ndarray に対する仮引数 axis を指定した np.sum の計算

np.sumnp.count_nonzero などの numpy の多くの関数 には 仮引数 axis があります。仮引数 axisデフォルト引数 になっており、キーワード引数 axis記述せずに呼び出した場合一般的に nparray のすべての要素を対象 とした計算が行われます。一方、キーワード引数 axis を記述 することで、特定の axis を対象とした計算 が行われます。

ここではわかりやすさを重視して ndarray の要素の合計を計算 する np.sum で説明します。

np.sum仮引数 axis の詳細 については下記のリンク先を参照して下さい。

下記のプログラムのように、np.sum の実引数2 次元の ndarray を記述 すると、実行結果のように すべての要素の合計(1 ~ 9 までの合計)である 45 が計算 されます。なお、変数名は ndarray 2 dimension の略です

import numpy as np

na2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(na2d)
print(np.sum(na2d))

実行結果

[[1 2 3]
 [4 5 6]
 [7 8 9]]
45

一方、np.sumキーワード引数 axis=0 を記述 して実行すると、下記のプログラムのように それぞれの列の合計を要素 とする 1 次元の ndarray が計算されます。

print(np.sum(na, axis=0))

実行結果

[12 15 18]

下図は 上記の計算を図示 したもので、仮引数 axis に代入して指定した 赤い矢印axis 0 の方向に沿って1 axis 1 の それぞれの要素の合計 が計算されます。

仮引数 axis に関する処理は頭の中だけで考えてもわからなくなることが良くあると思います。そのような場合は上記のような図を書くことをお勧めします。

nasum = np.sum(na2d, axis=0) によって行われる計算は以下のようになります。

  • (3, ) の shape(形状)を持つ 1 次元の ndarray が計算 される
  • 任意の i に対して nasum[i] = np.sum(na2d[:, i])2 が計算される

下記は 上記の計算for 文で行う プログラムです。最初に (3, ) の shape の ndarray を作成 する必要があるのでそれをすべての要素が 0 である ndarray を作成する np.zeros を利用して行っていますが、その後ですべての要素の値を計算して代入するので、最初に作成する ndarrayshape が正しければ要素の値は何でもかまいません

実行結果から np.sum(na2d, axis=0) と同じ計算が行われる ことが確認できます。なお、以前の記事 で説明したように np.zeros はデータ型を指定する仮引数 dtype を記述しない場合は np.float64 の ndarray を作成するので、実行結果に表示される ndarray の要素には 12. のように 浮動小数点数型であることを表す小数点が表示 されます。

nasum = np.zeros((3, ))
for i in range(3):
    nasum[i] = np.sum(na2d[:, i])
print(nasum)

実行結果

[12. 15. 18.]

先程の図からわかるように、2 次元の ndarray に対して axis=0 を実引数に記述して np.sum の計算を行うと、axis 1 の各インデックスの値によって指定 される 1 次元の ndarray である na2d[:, 0]na2d[:, 1]na2d[:, 2] に対して axis 0 の要素の合計が計算 されます。その結果、複数の axis 0 の要素が 1 つの値になる ため以下のような ndarray が計算されます。

  • axis 0 に対応する axis が無くなる
  • 計算される ndarray の次元1 つ減って 2 から 1 になる
  • 計算された 1 次元の ndarray の axis 0元の ndarray の axis 1 に対応 する

下記は 計算前と計算後 の ndarray の axis の対応表 です。

計算前 計算後
axis 0 なし
axis 1 axis 0

上記の表は、以前の記事2 次元の ndarray に対して na[0, :] のように axis 0 に対して 具体的な数値 を、axis 1 に対して すべてのインデックスを表す : を記述 した場合に計算される 1 次元の ndarray の 対応表と同じ で、対応表が同じになる理由は以下の通りです。

  • na[0, :] の場合は axis 0 に具体的な数値を記述することで axis 0 のインデックスの中から 1 つが選択される ため、axis 0 に対応する axis が無くなる
  • np.sum(nd2d, axis=0) の場合は、axis 0 に対応する要素 が合計を計算することで 1 つの値になる ため、axis 0 に対応する axis が無くなる

下記のプログラムのように np.sum のキーワード引数に axis=1 を記述した場合も 同様の処理が行われます

print(np.sum(na2d, axis=1))

実行結果

[ 6 15 24]

下図は 上記の計算を図示 したもので、仮引数 axis に代入して指定した 赤い矢印axis 1 の方向に沿って axis 0 の それぞれの要素の合計 が計算されます。下の ndarrayprint の表示と同じ向き になるように、計算された右上の ndarray の axis 0 の向きを変えた もので、上記の実行結果と同じ内容が計算 されていることが確認できます。

axis=1 を記述した場合も 先ほどと同様の理由 で、下記の表のように axis 1 に対応する axis が無くなり、計算された 1 次元の ndarray の axis 0元の 2 次元の ndarray の axis 0 に対応 することになります。

計算前 計算後
axis 0 axis 0
axis 1 なし

上記をまとめると以下のようになります。

($a_0$, $a_1$) の shape の 2 次元の ndarray に対して axis=x(ただし x = 0, 1)を実引数に記述して np.sum を計算すると、下記のような ndarray が計算される。ただし、$y$ は $y ≠ x$ となる 0 または 1 の整数であるものとする。

  • ($a_0$, $a_1$) から $a_x$ を除いた ($a_y$, ) の shape を持つ 1 次元の ndarray が計算される
    例えば 2 次元の ndarray の shape が (5, 7) で axis=0 を記述した場合は (7, )、axis=1 を記述した場合は (5, ) の shape を持つ 1 次元の ndarray が計算される
  • 計算された 1 次元の ndarray の axis 0 は元の ndarray の axis y に対応する

別の言葉で説明すると、計算される 1 次元の ndarray は 元の 2 次元の ndarray の shape から キーワード引数 axis に記述した axis を取り除いた shape になります。

3 次元の ndarray に対して仮引数 axis を指定した np.sum の計算

3 次元の ndarray に対しても 同様の方法で計算 が行われます。下記のプログラムは 3 次元の ndarray に対して実引数に axis=2 を記述して np.sum の計算を行う プログラムです。

na3d = np.array([[[1, 2, 3], [4, 5, 6], [7, 8, 9]],
                 [[10, 11, 12], [13, 14, 15], [16, 17, 18]]])
print(na3d)
print(np.sum(na3d, axis=2))

実行結果

[[[ 1  2  3]
  [ 4  5  6]
  [ 7  8  9]]

 [[10 11 12]
  [13 14 15]
  [16 17 18]]]
[[ 6 15 24]
 [33 42 51]]

下図は 上記の計算を図示 したもので、赤い矢印axis 2 の方向に沿って 残りの axis 0 と axis 1 のインデックスの それぞれの組み合わせ3 によって表される 1 次元の ndarray の要素の合計 が計算されて右のような 2 次元の ndarray が計算 されます。下の 2 次元の ndarrayprint の表示と同じ向き になるように、右の ndarray の axis 0 と axis 1 の向きを変えた もので、上記の実行結果と同じ内容が計算 されていることが確認できます。

nasum = np.sum(na3d, axis=2) によって行われる計算は以下のようになります。

  • (2, 3) の shape を持つ 2 次元の ndarray が計算される
  • 任意の i, j に対して nasum[i][j] = np.sum(na3d[i, j, :]) が計算される

下記は 上記の計算を for 文で行う プログラムです。実行結果から np.sum(na3d, axis=2) と同じ計算が行われる ことが確認できます。

nasum = np.zeros((2, 3))
for i in range(2):
    for j in range(3):
        nasum[i][j] = np.sum(na3d[i, j, :])
print(nasum)

実行結果

[[ 6. 15. 24.]
 [33. 42. 51.]]

下記は 計算前と計算後 の ndarray の axis の対応表 です。3 次元の ndarray の axis 2 に対応する要素の合計が計算 されて 1 つの値になる ので、作成された 2 次元の ndarray には 対応する axis はありません。また、元の 3 次元の ndarray の axis 0 と axis 1 は、作成された 2 次元の ndarrayaxis 0 と axis 1 に対応 します。

計算前 計算後
axis 0 axis 0
axis 1 axis 1
axis 2 なし

先程の場合と同様に、上記の表は以前の記事で説明した 3 次元の ndarray に対して na[:, :, 0] のように axis 2 に対して具体的な数値 を、残りの axis 0 と axis 1 に対して すべてのインデックスを表す : を記述 した場合と 同様 です。

説明は省略しますが、axis=0axis=1 を記述した場合も 同様の処理 が行われます。興味がある方は実際に実行して結果を確認して下さい。

3 次元の ndarray の場合の処理をまとめると以下のようになります。

3 次元の ndarray に対して axis=x を実引数に記述して np.sum を計算すると、下記のような ndarray が計算される。

  • 元の 3 次元の ndarray の shape から axis x を取り除いた shape の 2 次元の ndarray が計算される
    例えば (2, 3, 4) の shape の 3 次元の ndarray に対して axis=1 を指定した場合は (2, 4) の shape の 2 次元の ndarray が計算される
  • 元の 3 次元の ndarray と 計算された 2 次元の ndarray の axis の対応は以下のようになる
    • $i < x$ の場合は元の axis i と計算された axis i がそのまま対応する
    • $x ≦ i$ の場合は元の axis i + 1 と計算された axis i がそのまま対応する

2 次元の ndarray の場合と同様に、計算される 2 次元の ndarray は 元の 3 次元の ndarray の shape から キーワード引数 axis に記述した axis を取り除いた shape になります。

複数の axis を指定した np.sum の計算

np.sum の仮引数 axistuple を代入 することで 複数の axis を指定 することができます。下記は、2 次元の ndarray に対してキーワード引数 axis=(0, 1) を記述して np.sum を計算するプログラムで、axis 0 と axis 1 の両方を指定 したことになります。

print(np.sum(na2d, axis=(0, 1)))

実行結果

45

下図は 上記の計算 を図示したもので、赤い矢印axis 0 と axis 12 つの axis で指定された範囲の要素 の計算されます。2 つの axis で指定される要素が 1 つの値になる ので 次元が 2 つ減ります が、0 次元の ndarray は計算できないのでその代わりに 45 という 数値型のデータ4が計算されます。

下記は、3 次元の ndarray に対してキーワード引数 axis=(0, 1) を記述して np.sum を計算するプログラムです。

print(np.sum(na3d, axis=(0, 1)))

実行結果

[51 57 63]

下図は 上記の計算を図示 したもので、赤い矢印axis 0 と axis 12 つの axis で指定 される黄色、水色、緑色の それぞれの要素の合計が計算 されます。2 つの axis で指定される要素が 1 つの値になる ので 次元が 2 つ減って 下の 1 次元の ndarray が計算 されます。なお、1 次元の ndarray の 要素の色 が、上の 3 次元の ndarray の 合計を計算する要素の色と対応 します。例えば図では表示されていませんが、上の 3 次元の ndarray の 黄色の部分 には後ろに 10, 13, 16 があるので、その 合計は 1 + 4 + 7 + 10 + 13 + 16 = 51 となります。

下記は 上記の計算for 文で行う プログラムです。axis 2 の各インデックスの値によって指定される 2 次元の ndarray である na3d[:, :, 0]na3d[:, :, 1]na3d[:, :, 2] に対して axis 0 と axis 1 で指定される範囲の要素の合計 が計算 されます。

nasum = np.zeros((3, ))
for i in range(3):
    nasum[i] = np.sum(na3d[:, :, i])
print(nasum)

実行結果

[51. 57. 63.]

axis を指定した n 次元の ndarray に対する np.sum の計算

ここまでは 2 次元や 3 次元の ndarray に対する計算を紹介しましたが、n 次元の ndarray に対しても 同様の手順 で下記のような計算が行われます。

n 次元 の ndarray に対してキーワード引数 axism 個の axis を記述 して np.sum を計算すると、下記のような ndarray が計算される。

  • 元の n 次元の ndarray の shape からキーワード引数 axis で指定した すべての axis を取り除いた shapen - m 次元 の ndarray が計算される
  • 元の n 次元の ndarray と 計算された n - m 次元の ndarray の axis の対応 は、「n 次元の ndarray の 削除しない axis を番号が 小さい順に並べた もの」が、「計算された n - m 次元の ndarray の axis を番号が小さい順に並べた もの」に対応する
  • 計算後の ndarray の各要素の値は、元の ndarray の 対応する axis に同じインデックスの値を記述 し、それ以外の削除される axis のインデックスに : を記述した場合の m 次元の ndarray の要素の合計 になる

例えば (2, 3, 4, 5, 6) の shape の 5 次元の ndarray に対して axis=(2, 3) を指定した場合は axis 2 と axis 3 に対応する 4 と 5 の要素を取り除いた (2, 3, 6) の shape の 5 - 2 = 3 次元の ndarray が計算されます。

また、その場合の axis の対応 は以下の表のようになります。

計算前 計算後
axis 0 axis 0
axis 1 axis 1
axis 2 なし
axis 3 なし
axis 4 axis 2

元の 5 次元の ndarray が na5d、計算された 3 次元の ndarray が na3d という変数に代入される場合は、下記の式で nd3d の各要素の値が計算 されます。ただし、x, y, z はその axis のインデックスの範囲内の任意の整数 であるものとします。

nd3d[x, y, z] = np.sum(na5d[z, y, :, :, z])

上記から、キーワード引数 axisすべての axis を指定 することで任意の ndarray の すべての要素の合計が計算 されることがわかります。従って、キーワード引数 axis を記述せずnp.sum を呼び出した場合は、キーワード引数 axis にすべての axis が指定した場合と同じ計算 が行われることがわかります。

axis を指定した行と列のマークのパターンの計算

上記ではわかりやすさを重視して np.sum で説明を行いましたが、0 でない要素の数を数える np.count_nonzero に対してもキーワード引数 axis を指定することで axis を考慮した np.sum と同様の計算 を行うことができます。

例えば 〇 と × の 3 つの それぞれの行のマークの数 は下記のプログラムのように 行を表す axis=1 を実引数に記述した np.count_nonzero で計算することができます。

print(mb)
print(np.count_nonzero(mb.board.board, axis=1))

実行結果

winner o
oxx
ox.
Oo.

[[1 1 2]
 [2 1 0]]

下図は 上記の計算を図示 したものす。これまでの図とは異なり、〇× ゲームのゲーム盤 に合わせて 左右方向を axis 1上下方向を axis 2 としている点に注意して下さい。また図の T は True を、F は False を表します。

赤い矢印axis 1 の方向に沿って 0 でない True の数が計算された 結果、右の 2 次元の ndarray が計算されます。下は print で表示されるのと同じ向きになるように axis の向きを変えたもの で、上記の 実行結果と同じ内容が計算 されていることが確認できます。

下記は 計算前と計算後 の ndarray の axis の対応表 です。計算された 2 次元の ndarray各行のそれぞれのマークの数 が計算されていることがわかります。

計算前 計算後 意味
axis 0 axis 0 マークの種類
axis 1 axis 1 行の番号
axis 2 なし

各行のマークのパターン は下記のプログラムで計算することができます。実行結果から 0 行 の 「〇××」、1 行 の「〇×空」、2 行 の「〇〇空」のマークのパターンが 正しく計算されている ことが確認できます。

  • 1、2 行目:直前のターンを 〇、現在のターンを × として計算することで、(〇 のマークの数, × のマークの数, 空のマスの数) というマークのパターンが計算されるようにした
  • 3 行目:上記で説明した方法で各行の各マークの数を数える処理を行う
  • 4 行目:各行についての繰り返し処理を行う
  • 5 ~ 8 行目y 行の turn のマークの数は rowdata[turn, y] に記録されているのでそれを利用して y 行のマークのパターンを計算する
1  last_turn = mb.board.CIRCLE
2  turn = mb.board.CROSS
3  rowdata = np.count_nonzero(mb.board.board, axis=1)
4  for y in range(mb.board.BOARD_SIZE):
5      turn_count = rowdata[turn, y]
6      lastturn_count = rowdata[last_turn, y]
7      emptycount = mb.board.BOARD_SIZE - turn_count - lastturn_count
8      print((lastturn_count, turn_count, emptycount))
行番号のないプログラム
last_turn = mb.board.CIRCLE
turn = mb.board.CROSS
rowdata = np.count_nonzero(mb.board.board, axis=1)
for y in range(mb.board.BOARD_SIZE):
    turn_count = rowdata[turn, y]
    lastturn_count = rowdata[last_turn, y]
    emptycount = mb.board.BOARD_SIZE - turn_count - lastturn_count
    print((lastturn_count, turn_count, emptycount))

実行結果

(np.int64(1), np.int64(2), np.int64(0))
(np.int64(1), np.int64(1), np.int64(1))
(np.int64(2), np.int64(0), np.int64(1))

列のマークのパターン の計算は、3 行目で np.count_nonzero(mb.board.board, axis=2) のように列を表す axis=2 を記述 することで計算することができます。

対角線のマークの計算処理

前回の記事では下記のプログラムによって 対角線のマークの一覧の計算 を行いました。

# 対角線 1(左上から右下方向)のマークの計算
print(np.diagonal(mb.board.board, axis1=1, axis2=2))
# 対角線 2(左下から右上方向)のマークの計算
print(np.diagonal(np.flip(mb.board.board, axis=2), axis1=1, axis2=2))

実行結果

[[ True False False]   # 対角線 1 の「〇×空」の 〇 のデータ
 [False  True False]]  # 対角線 1 の「〇×空」の × のデータ
[[ True False False]   # 対角線 2 の「〇×空」の 〇 のデータ
 [False  True  True]]  # 対角線 2 の「〇××」の × のデータ

上記で計算される 2 次元の ndarrayaxis は下記のような意味を持ちます。

axis 意味
axis 0 マークの種類
axis 1 対角線

従って、上記で計算したデータに対して 下記のプログラムのように 対角線を表す axis=1 を記述して np.count_nonzero を呼び出すことで それぞれの対角線の 〇 と × の数の計算まとめて行う ことができます。実行結果から 正しい計算が行われる ことが確認できました。

# 対角線 1 のマークの数の計算
print(np.count_nonzero(np.diagonal(mb.board.board, axis1=1, axis2=2), axis=1))
# 対角線 2 のマークの数の計算
print(np.count_nonzero(np.diagonal(np.flip(mb.board.board, axis=2), 
                                   axis1=1, axis2=2), axis=1))

実行結果

[1 1]   # 対角線 1 の「〇×空」の 〇 と × の個数
[1 2]   # 対角線 2 の「〇××」の 〇 と × の個数

count_markpats の修正(count_markpats2 の定義)

下記は今回の記事で説明した方法で計算を行うように count_markpats を修正したプログラムです。ただし、これまでの count_markpats の処理速度と比較できる ように下記では count_markpats2 という名前で定義しました。なお、self.count_linemarkTrue の場合の処理は変わらないので 説明は省略 し、内容が元のプログラムから大きく変わったので修正箇所は省略します。また、これまでは 直線上のマークのパターンを計算する処理count_markpat で行っていましたが、下記のプログラムではその計算を count_markpats2 の中で直接行うように修正 したので、count_markpat は利用していません

  • 10、11 行目:列と行の処理の違いは、11 行目の count_nonzero のキーワード引数 axis の値が 1 と 2 であるかだけなので、繰り返し処理で行うようにした
  • 11 ~ 16 行目:先程説明した方法で列と行のマークのパターンを計算するように修正した。また、行と列の両方のデータを計算するので先ほどのプログラムから変数の名前を rowdata から data に、y から i に変更した
  • 19 ~ 23 行目:対角線 1 と対角線 2 のマークの数を要素とする list を計算する
  • 24 ~ 28 行目:先程説明した方法でそれぞれの対角線のマークのパターンを計算する
 1  from marubatsu import NpBoolBoard
 2  from collections import defaultdict
 3  
 4  def count_markpats(self, turn, last_turn):
 5      markpats = defaultdict(int)
 6  
 7      if self.count_linemark:
元と同じなので省略
 8      else:
 9          # 行と列のマークのパターンの計算
10          for axis in [1, 2]:
11              data = np.count_nonzero(self.board, axis=axis)
12              for i in range(self.BOARD_SIZE):
13                  turn_count = data[turn, i]
14                  lastturn_count = data[last_turn, i]
15                  emptycount = self.BOARD_SIZE - turn_count - lastturn_count
16                  markpats[(lastturn_count, turn_count, emptycount)] += 1
17  
18          # 対角線のマークのパターンの計算       
19          diagdata = [
20              np.count_nonzero(np.diagonal(self.board, axis1=1, axis2=2), axis=1),
21              np.count_nonzero(np.diagonal(np.flip(self.board, axis=2), 
22                                           axis1=1, axis2=2), axis=1),
23          ]
24          for data in diagdata:
25              turn_count = data[turn]
26              lastturn_count = data[last_turn]
27              emptycount = self.BOARD_SIZE - turn_count - lastturn_count
28              markpats[(lastturn_count, turn_count, emptycount)] += 1
29  
30      return markpats
31  
32  NpBoolBoard.count_markpats2 = count_markpats2
行番号のないプログラム
from marubatsu import NpBoolBoard
from collections import defaultdict

def count_markpats2(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 axis in [1, 2]:
            data = np.count_nonzero(self.board, axis=axis)
            for i in range(self.BOARD_SIZE):
                turn_count = data[turn, i]
                lastturn_count = data[last_turn, i]
                emptycount = self.BOARD_SIZE - turn_count - lastturn_count
                markpats[(lastturn_count, turn_count, emptycount)] += 1

        # 対角線のマークのパターンの計算          
        diagdata = [
            np.count_nonzero(np.diagonal(self.board, axis1=1, axis2=2), axis=1),
            np.count_nonzero(np.diagonal(np.flip(self.board, axis=2),
                                         axis1=1, axis2=2), axis=1),
        ]
        for data in diagdata:
            turn_count = data[turn]
            lastturn_count = data[last_turn]
            emptycount = self.BOARD_SIZE - turn_count - lastturn_count
            markpats[(lastturn_count, turn_count, emptycount)] += 1

    return markpats

NpBoolBoard.count_markpats2 = count_markpats2

行と列のマークの数は shape が (2, 3) の 2 次元の ndarray であるのに対し、対角線のマークの数は shape が (2, ) の 1 次元の ndarray で次元が異なっています。そのため、上記では行と列と、対角線のマークのパターンの計算を別々に行っています。

2025/12/21:修正

keepdims ではまとめられない事に気が付きましたので下記の内容は取り消します。また、まとめる方法については次回の記事で説明します。

説明が長くなるのでプログラムは示しませんが、np.count_nonzero の実引数に keepdims=True を実引数に記述することで、対角線のマークの数を shape が (2, 1) の 2 次元の ndarray として計算することができ、行、列、対角線のすべてを一つの繰り返し処理でまとめることができます。

np.count_nonzero の仮引数 keepdims に関する詳細は下記のリンク先を参照して下さい。np.sum などの多くの numpy の関数は同様の仮引数 keepdims を持ちます。

処理の確認と処理速度の比較

上記の修正後に下記のプログラムで mb の局面のマークのパターン を上記で定義した count_markpats2 で計算5して表示すると、実行結果に前回の記事同じ結果が表示される ので 正しい処理が行われることが確認 できました。

from pprint import pprint
pprint(mb.board.count_markpats2(mb.turn, mb.last_turn))

実行結果

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})

次に下記のプログラムで count_markpats2改良前の count_markpats処理速度を比較 します。実行結果から、残念ながら今回の記事で改良した count_markpats2 のほうが処理時間が長くなってしまう ことが確認できました。

%timeit mb.count_markpats()
%timeit mb.board.count_markpats2(mb.turn, mb.last_turn)

実行結果

19.3 μs ± 1.52 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
30.1 μs ± 1.06 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

これは筆者にとっても意外な結果だったのですが、色々調べてみると ndarray の要素の数が少ない場合まとめて処理を行うことによる効率の上昇の効果が得られない ことが原因であることがわかりました。そのことをこれから示しますが、その検証を行う際に バグがあることを発見 したので修正することにします。

バグの修正

下記のプログラムのように mb.restart()ゲーム開始時の局面 に戻した後で count_markpats2 を実行すると実行結果のような エラーが発生 します。

mb.restart()
print(mb.board.count_markpats2(mb.turn, mb.last_turn))

実行結果

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[18], line 2
      1 mb.restart()
----> 2 print(mb.board.count_markpats2(mb.turn, mb.last_turn))

Cell In[15], line 23
     21         lastturn_count = data[last_turn, i]
     22         emptycount = mb.board.BOARD_SIZE - turn_count - lastturn_count
---> 23         markpats[(lastturn_count, turn_count, emptycount)] += 1
     25 # 対角線のマークのパターンの計算          
     26 diagdata = [
     27     np.count_nonzero(np.diagonal(mb.board.board, axis1=1, axis2=2), axis=1),
     28     np.count_nonzero(np.diagonal(np.flip(mb.board.board, axis=2),
     29                                  axis1=1, axis2=2), axis=1),
     30 ]

TypeError: unhashable type: 'numpy.ndarray'

エラーメッセージから ---> が表示されている行の (lastturn_count, turn_count, emptycount)ハッシュ可能な値でない ことが原因であることがわかります。

そこで、count_markpats2 内で上記の lastturn_countturn_count を計算する 下記のプログラムを実行して その内容を表示 してみることにしました。

turn = mb.turn
last_turn = mb.last_turn
data = np.count_nonzero(mb.board.board, axis=0)
for i in range(mb.board.BOARD_SIZE):
    turn_count = data[turn, i]
    lastturn_count = data[last_turn, i]
    print(turn_count)
    print(lastturn_count)

実行結果

0
[[0 0 0]]
0
[[0 0 0]]
0
[[0 0 0]]

実行結果から lastturn_count の内容が 2 次元の ndarray になっていることがわかります。ndarray はハッシュ可能な値ではない ので上記のようなエラーが発生します。

lastturn_countdata[last_turn, i] によって計算されるので下記のプログラムで last_turn を表示 すると実行結果から None が代入されていることが確認できます。

print(last_turn)

実行結果

None

なお、使い道は今回の記事では説明しませんが、下記のプログラムのように ndarray に対して インデックスに None を記述 すると元の ndarray の shape の先頭の要素に 1 を追加 した 1 つ次元が増えた ndarray が計算 されます。

na = np.array([[1, 2, 3], [4, 5, 6]])
print(na)
print(na.shape)
na2 = na[None]
print(na2)
print(na2.shape)

実行結果

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

ゲーム開始時の局面last_turnNone が代入 される理由は Marubatsu クラスの 初期化を行う restart メソッド の中で self.last_moveNone を代入しているからです。calc_markpats2 の処理が正しく行われるようにする ためには、下記のプログラムの 7 行目のように restart メソッド内で last_move に × の手番を代入 する必要があります。

 1  def restart(self):
 2      self.initialize_board()
 3      self.turn = self.CIRCLE     
 4      self.move_count = 0
 5      self.status = self.PLAYING
 6      self.last_move = -1, -1          
 7      self.last_turn = self.CROSS
 8      self.records = [self.last_move]
 9  
10  Marubatsu.restart = restart
行番号のないプログラム
def restart(self):
    self.initialize_board()
    self.turn = self.CIRCLE     
    self.move_count = 0
    self.status = self.PLAYING
    self.last_move = -1, -1          
    self.last_turn = self.CROSS
    self.records = [self.last_move]
    
Marubatsu.restart = restart
修正箇所
def restart(self):
    self.initialize_board()
    self.turn = self.CIRCLE     
    self.move_count = 0
    self.status = self.PLAYING
    self.last_move = -1, -1          
-   self.last_turn = None
+   self.last_turn = self.CROSS
    self.records = [self.last_move]
    
Marubatsu.restart = restart

上記の修正後に下記のプログラムを実行すると 正しく動作することが確認 できます。

mb.restart()
print(mb.board.count_markpats2(mb.turn, mb.last_turn))

実行結果

defaultdict(<class 'int'>, {(np.int64(0), np.int64(0), np.int64(3)): 8})

要素の多い ndarray の場合の処理速度の比較

次に、下記のプログラムで ゲーム盤のサイズが 10、50, 100 の場合の処理速度を表示することにします。それぞれ board 属性に代入された ndarray の shape が (2, 10, 10)、(2, 50, 50)、(2, 100, 100) となるので、要素の数 はそれぞれ 200, 5000, 20000 となります。

for board_size in [10, 50, 100]:
    mb = Marubatsu(boardclass=NpBoolBoard, board_size=board_size)
    print(f"board_size = {board_size}")
    print("count_markpats")
    %timeit mb.count_markpats()
    print("count_markpats2")
    %timeit mb.board.count_markpats2(mb.turn, mb.last_turn)
    print()

実行結果

board_size = 10
count_markpats
42.4 μs ± 1.65 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
count_markpats2
39.2 μs ± 1.47 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

board_size = 50
count_markpats
171 μs ± 2.27 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
count_markpats2
95.4 μs ± 1.48 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

board_size = 100
count_markpats
349 μs ± 17.8 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
count_markpats2
180 μs ± 1.48 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

実行結果からゲーム盤のサイズが多くなって ndarray の要素の数が多くなるcount_markpats2 の方が処理速度が速くなる ことが確認できました。

今回の記事のまとめ

今回の記事では axis を指定 することで マークのパターンをまとめて計算 する方法を紹介しました。残念ながら 〇× ゲームの ゲーム盤が小さすぎるため この改良によって 処理速度が低下 してしまいましたが、より大きなゲーム盤 の場合は 処理速度が向上 します。

今回の記事で紹介した axis を指定した numpy の関数の計算 はプログラムを より簡潔に記述できる という利点があるのと、axis を指定する計算 は ndarray に対して実際に 頻繁に行われる ので 覚えておくと良い のではないかと思います。

なお、count_markpats2 は処理速度が低下してしまうので 採用は見送ります が、せっかくなので メソッドの定義はそのまま残しておく ことにします。

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

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

次回の記事

修正履歴

更新日時 更新内容
2025/12/21 keepdims に関する内容が間違っていたので修正しました
  1. 公式のドキュメントには仮引数 axis の説明として「Axis or axes along which a sum is performed」のように「沿って」を意味する along という単語で説明されています

  2. np.sum(na2d[:][i]) ではうまく計算されない点に注意して下さい。実際にこの記事を書く際に筆者は間違えてしまいました。以前の記事で説明したように na2d[:]na2d を計算するので na2d[:][i]na2d[i] と同じ意味になり、意図した計算が行われません

  3. 具体的には na3d[0, 0, :]na3d[0, 1, :]na3d[0, 2, :]na3d[1, 0, :]na3d[1, 1, :]na3d[1, 2, :] です

  4. 表示からは区別できませんが、np.int64 の 45 が計算されます。興味がある方は print(type(np.sum(na2d, axis=(0, 1))) を実行してみて下さい。<class 'numpy.int64'> と表示されるはずです

  5. 前回の記事のように mb.count_markpats を呼び出すと、修正前の mb.board.bount_markpats が呼び出されるのでこのように記述する必要があります

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?