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を一から作成する その208 ndarray の回転、転置、結合、axis の追加による NpBoolBoard クラスの実装

0
Last updated at Posted at 2025-12-23

目次と前回の記事

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

calc_same_hashables の定義

今回の記事では残っていた NpBoolBoard クラスの calc_same_hashables の定義を行います。また、その後で calc_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.

3 次元の ndarray の回転と反転

同一局面ハッシュ可能な値を計算 する calc_same_hashables では、ゲーム盤の 回転と左右の反転 を下記のプログラムのように np.rot90np.fliplr を利用して行いました。

for i in range(7):
    if i != 3:
        self.board = np.rot90(self.board)
    else:
        self.board = np.fliplr(self.board)
    hashable = self.board_to_hashable()

上記は 2 次元の ndarray でゲーム盤を表現する NpBoard クラスの場合の処理なので、3 次元の ndarray でゲーム盤を表現する NpBoolBoard クラスの場合は上記とは 異なるプログラムを記述する必要 があります。

2 次元の ndarray の回転

np.rot90 には 回転する方向2 つの axis によって指定 する axes1 という仮引数があります。回転する方向は 2 つの要素を持つ tuple で指定し、最初の要素が表す axis が、次の要素が表す axis と重なる2 ように ndarray を 90 度回転 させます。

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

本記事では利用しませんが、np.rot90 には回転の回数を指定する k という仮引数があります。例えば k=2 を実引数に記述すると 90 × 2 = 180 度の回転が行われます。

仮引数 axes(0, 1) をデフォルト値 とする デフォルト引数 となっているので、キーワード引数 axes を記述せずnp.rot90 を呼び出した場合は axis 0 が axis 1 に重なるように ndarray を回転する処理が行われます。

言葉の説明だけではわらりづらいと思いますので具体例を挙げながら図で説明します。下記のプログラムは 2 次元の ndarraynp.rot90 で回転 させる処理を行うプログラムです。

import numpy as np

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

実行結果

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

下記は 上記の処理を図示 したものです。図の下部 に示したように axis 0 が axis 1 に重なるように回転 が行われた結果、ndarray の要素が 反時計回りに 90 度回転 します。

下記のプログラムのように axis=(1, 0) を記述した場合は axis 1 が axis 0 に重なるように回転 が行われるので、実行結果のように ndarray の要素が 時計回りに 90 度回転 します。図は回転方向が逆になるだけなので省略します。

print(na2d)
print()
print(np.rot90(na2d, axes=(1, 0)))

実行結果

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

3 次元の ndarray の回転

3 次元の ndarray の場合も 同様の処理 が行われます。下記のプログラムは 3 次元の ndarraynp.rot90 で回転 させる処理を行うプログラムです。

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.rot90(na3d))

実行結果

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

 [[10 11 12]
  [13 14 15]
  [16 17 18]]]

[[[ 7  8  9]
  [16 17 18]]

 [[ 4  5  6]
  [13 14 15]]

 [[ 1  2  3]
  [10 11 12]]]

下記は 上記の処理を図示 したものです。図の下部 に示したように axis 0 が axis 1 に重なるように回転 が行われた結果、図の 3 次元の ndarray を 奥に倒すように 90 度回転 します。

下記のプログラムのように axes=(1, 2) を記述して np.rot90 で回転 させることによって、na3d[0]na3d[1] が表す 2 次元の ndarrayまとめて axis 1 から axis 2 の方向反時計回りに 90 度回転 させることができます。

print(na3d)
print()
print(np.rot90(na3d, axes=(1, 2)))

実行結果

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

 [[10 11 12]
  [13 14 15]
  [16 17 18]]]

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

 [[12 15 18]
  [11 14 17]
  [10 13 16]]]

下記は 上記の処理を図示 したものです。図の下部 に示したように axis 1 が axis 2 に重なるように回転 が行われた結果、図の 3 次元の ndarray が 反時計回りに 90 度回転 します。

以前の記事でも述べましたが、多次元の ndarray の計算がわかりづらいと思った人は、図を描くことをお勧めします。

calc_same_hashables の定義

3 次元の ndarray に対する 特定の軸の反転 の処理については 以前の記事np.flip を利用して行う方法を説明しました。従って、calc_same_hashablesnp.rot90np.flip を利用した下記のプログラムのように定義することができます。3 次元の ndarray では 〇 と × を表すデータを axis 0 のインデックスで表現 される 2 次元の ndarray で記録 していますが、下記のプログラムでは その両方のデータをまとめて 回転または反転しています。

なお、calc_same_hashables が計算する ハッシュ可能なデータ以前の記事で説明した ndarray の tobytes メソッドで計算される b'\x00\x00\x00\(略)' のような bytes 型の データなので、それを見ても 正しい計算が行われているかどうかを確認できません。そこで、下記のプログラムでは 9 行目で print(mb) でゲーム盤を表示 することで 正しい局面が計算できているかを確認 できるようにしています。正しく動作することが確認できた後で 9 行目のプログラムは削除 します。

  • 6 行目:先程説明した方法で 3 次元の ndarray で表現されるゲーム盤を 90 度回転させる
  • 8 行目以前の記事で説明した方法で np.flip でゲーム盤を反転させる
  • 9 行目:回転または反転したゲーム盤を表示することで、正しい処理が行われているかどうかを確認できるようにした。この行は後で削除する
 1  from marubatsu import NpBoolBoard
 2  
 3  def calc_same_hashables(self, move=None):
元と同じなので省略
 4      for i in range(7):
 5          if i != 3:
 6              self.board = np.rot90(self.board, axes=(1, 2))
 7          else:
 8              self.board = np.flip(self.board, axis=1)
 9          print(mb)
10          hashable = self.board_to_hashable()
元と同じなので省略
11  
12  NpBoolBoard.calc_same_hashables = calc_same_hashables
行番号のないプログラム
from marubatsu import NpBoolBoard

def calc_same_hashables(self, move=None):
    if move is None:
        hashables = set([self.board_to_hashable()])
    else:
        hashables = { self.board_to_hashable(): move }
    boardorig = self.board
    if move is not None:
        x, y = self.move_to_xy(move)
    for i in range(7):
        if i != 3:
            self.board = np.rot90(self.board, axes=(1, 2))
        else:
            self.board = np.flip(self.board, axis=1)
        print(mb)
        hashable = self.board_to_hashable()
        if move is None:
            hashables.add(hashable)
        else:
            if i == 3:
                y = self.BOARD_SIZE - y - 1
            else:
                x, y = self.BOARD_SIZE - y - 1, x
            hashables[hashable] = self.xy_to_move(x, y)
    self.board = boardorig
    return hashables

NpBoolBoard.calc_same_hashables = calc_same_hashables
修正箇所
from marubatsu import NpBoolBoard

def calc_same_hashables(self, move=None):
元と同じなので省略
    for i in range(7):
        if i != 3:
-           self.board = np.rot90(self.board)
+           self.board = np.rot90(self.board, axes=(1, 2))
        else:
-           self.board = np.fliplr(self.board)
+           self.board = np.flip(self.board, axis=1)
+       print(mb)
        hashable = self.board_to_hashable()
元と同じなので省略

NpBoolBoard.calc_same_hashables = calc_same_hashables

上記を実行した後で下記のプログラムで 先程作成した mb に対して calc_same_hashables を実行すると、実行結果から 最初の局面 に対して 90 度の回転を 3 回左右の反転を 1 回90 度の回転が 4 回 を順に行った局面が表示されるので、正しい処理が行われていることが確認 できます。

print(mb)
mb.board.calc_same_hashables()

実行結果

winner o
oxx
ox.
Oo.

winner o
ooo
oxx
..x

winner o
.oo
.xo
Xxo

winner o
x..
xxo
Ooo

winner o
..x
oxx
Ooo

winner o
oo.
ox.
Oxx

winner o
ooo
xxo
X..

winner o
xxo
.xo
.oo

確認ができたので、下記のプログラムで先程確認のために表示した print(mb) を削除 します。先程との違いは print(mb)を削除しただけなので折りたたみます。

修正したプログラム
def calc_same_hashables(self, move=None):
    if move is None:
        hashables = set([self.board_to_hashable()])
    else:
        hashables = { self.board_to_hashable(): move }
    boardorig = self.board
    if move is not None:
        x, y = self.move_to_xy(move)
    for i in range(7):
        if i != 3:
            self.board = np.rot90(self.board, axes=(1, 2))
        else:
            self.board = np.flip(self.board, axis=1)
        hashable = self.board_to_hashable()
        if move is None:
            hashables.add(hashable)
        else:
            if i == 3:
                y = self.BOARD_SIZE - y - 1
            else:
                x, y = self.BOARD_SIZE - y - 1, x
            hashables[hashable] = self.xy_to_move(x, y)
    self.board = boardorig
    return hashables

NpBoolBoard.calc_same_hashables = calc_same_hashables

benchmark による処理速度の計測

これで NpBoolBoard クラスのメソッドが すべて定義できた ので、下記のプログラムで benchmark でその処理速度を計測 することにします。

from util import benchmark

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

実行結果

count_linemark False
ai2 VS ai2
100%|██████████| 50000/50000 [00:06<00:00, 7407.77it/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:05<00:00, 760.68it/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
 43.2 ms ±   3.1 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

count_linemark True
ai2 VS ai2
100%|██████████| 50000/50000 [00:08<00:00, 6156.21it/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:28<00:00, 1765.56it/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
 43.2 ms ±   3.0 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

下記は以前の記事で行った NpBoardNpIntBoard クラスのベンチマークの結果に 上記をの結果を加えた 表です。

boardclass count_linemark ai2 VS ai2 ai14s VS ai2 ai_abs_dls
NpBoard False 5972.04 回/秒 692.95 回/秒 44.1 ms
NpIntBoard False 7185.05 回/秒 723.75 回/秒 43.5 ms
NpBoolBoard False 7407.77 回/秒 760.68 回/秒 43.2 ms
NpBoard True 7478.88 回/秒 1894.42 回/秒 39.8 ms
NpIntBoard True 9820.80 回/秒 2030.51 回/秒 38.5 ms
NpBoolBoard True 6156.21 回/秒 1765.56 回/秒 43.2 ms

ai2 VS ai2ai14s VS ai2 のベンチマークについては以前の記事以前の記事で説明したように、count_linemarkFalse の場合の処理は NpBoolBoard クラスが最も速い ことが確認できました。一方 count_linemarkTrue の場合の処理は 3 次元の ndarray による 処理の効率の向上が活かせない ため NpBoolBoard クラスの処理速度が最も遅く なります。

今回の記事で実装した calc_same_hashables の処理では、NpBoard クラスや NpIntBoard クラスと同様に 回転と反転処理1 回np.rot90np.flip で行いますが、その際に計算する 3 次 の ndarray要素の数 は NpBoard や NpIntBoard の 2 次元の ndarray 要素の数2 倍 になります。そのため、残念ながら NpBoolBoard クラスの calc_same_hashables の処理は NpBoard や NpIntBoard クラスよりも 遅くなります。その結果、calc_same_hashables を利用する ai_abl_dls の処理速度は count_linemarkFalse の場合は NpBoard や NpIntBoard と ほぼ同じ になり、True の場合は 最も遅く なります。

上記から count_linemarkFalse の場合は 3 次元の ndaray によるゲーム盤の表現によって 2 次元の ndarray でゲーム盤を表現する場合と比べて ベンチマークの処理速度 が全体として 若干高速化 することが確認できました。

ndarray の転置、結合による count_markpats3 の実装

前回の記事で定義した count_markpats2 では、行と列2 つの対角線 に対する処理を 別々に行いました が、それらを まとめて行う ことができます。残念ながらまとめて行うことで処理速度が向上することはありませんが、参考までにその実装方法について説明します。

なお、前回の記事と同様に、これまでに実装した count_markpatscount_markpats2処理速度と比較できる ように、count_markpats3 というメソッドを定義することにします。

ndarray の転置による行のマークのパターンの計算

count_markpats2 では、下記のような処理を行うことで 行と列マークのパターンを計算 していますが、この処理は以下の手順で処理を行っています。

  1. 繰り返し処理で np.count_nonzeroaxis=1axis=2 を順番に記述して呼び出すことで、行と列 のそれぞれのマークの数を数えた 2 次元の ndarray を計算 する
  2. 2 次元の ndarray から 各行と各列 のそれぞれの マークの数を参照 して 変数に代入 する
  3. 手順 2 の変数を利用してマークのパターンを表す tuple を計算し、対応する markpats のキーの値を 1 増やすことでマークのパターンを数える
# 行と列のマークのパターンの計算
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

下記は先ほどの mb に対して 各行のマークの数 を数えた 2 次元の ndarray を計算するプログラムです。プログラムは前回の記事と同じなので忘れた方は復習して下さい。

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

実行結果(# のコメントは筆者が追記したものです)

winner o
oxx
ox.
Oo.

[[1 1 2]    # 各行の 〇 のマークの数
 [2 1 0]]   # 各行の × のマークの数

計算された 2 次元の ndarray の axis は以下の表ような意味を持ちます。

axis 意味
axis 0 マークの種類
axis 1 行の番号

マークのパターンのデータ構造の変更とその計算方法

マークのパターンデータ構造 は各行の 〇 の数× の数空のマスの数 を要素とする tuple ですが、上記の表から 各行〇 の数× の数 を表す 1 次元の ndarray は、下記のプログラムのように マークの種類 を表す axis 0 に対して : を、 を表す axis 1行の番号 を記述した narow[:, 0]narow[:, 1]narow[:, 2] で計算することができます。

print(mb)
for i in range(mb.BOARD_SIZE):
    print(narow[:, i])

実行結果(# のコメントは筆者が追記したものです)

winner o
oxx
ox.
Oo.

[1 2]  # 0 行の 〇 と × の数
[1 1]  # 1 行の 〇 と × の数
[2 0]  # 2 行の 〇 と × の数

これまでのマークのパターン空のマスの数を記録 していましたが、〇 の数と × の数と空のマスの数の 合計ゲーム盤のサイズに等しい ので、〇 の数と × の数が決まれば 空のマスの数は 自動的に「ゲーム盤のサイズ - 〇 の数 - × の数」という 式で計算できます。従って、マークのパターン空のマスの数を記録する必要はありません

そこで、count_markpats3 では マークのパターン を表す tuple の要素に 〇 の数と × の数だけを記録 することにします。

そのようにすることで マークのパターンのデータ構造が変わってしまう ので、マークのパターンを利用する ai14s などのプログラムを修正する必要 が生じますが、今回の記事では その修正は行わない ことにします。その理由は count_markpats3実装後に行った検証処理速度count_markpats と比較して 速くならない 事が判明したので count_markpats3 を採用しないことにした からです。

ndarrayハッシュ可能なデータではない ため、markpats に代入された dict のキーとすることはできません1 次元の ndarray を組み込み関数 tuple の実引数に記述 して呼び出すことで ハッシュ可能な tuple のデータに 変換 できるので、下記のプログラムの 6 行目のように記述することで (〇 の数, × の数) を表す tuple を計算し、その個数を markpats で計算 することができます。実行結果から 3 つの行のマークのパターンの数が 正しく計算されることが確認 できます。

1  from collections import defaultdict
2  from pprint import pprint
3
4  markpats = defaultdict(int)
5  for i in range(mb.BOARD_SIZE):
6      markpats[tuple(narow[:, i])] += 1
7  pprint(markpats)
行番号のないプログラム
from collections import defaultdict
from pprint import pprint

markpats = defaultdict(int)
for i in range(mb.BOARD_SIZE):
    markpats[tuple(narow[:, i])] += 1
pprint(markpats)

実行結果

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

tolist メソッドによる処理速度の改善方法

ndarray を tuple に変換 する際は、ndarray の tolist メソッドで list に変換3 してから行ったほうが 処理が高速 になります。下記は tolist の利用の有無 による 処理速度を比較 するプログラムで、tolist を利用 したほうが 処理速度が約 2 倍になる ことがわかります。

%%timeit

markpats = defaultdict(int)
for i in range(mb.BOARD_SIZE):
    markpats[tuple(narow[:, i])] += 1

実行結果

3.58 μs ± 94.1 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
%%timeit

markpats = defaultdict(int)
for i in range(mb.BOARD_SIZE):
    markpats[tuple(narow[:, i].tolist())] += 1   # ここで tolist を利用する

実行結果

1.79 μs ± 120 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)

ndarray の転置によるマークのパターンの計算

上記のプログラムでは、narow[:, i] のように 具体的なインデックスを記述 して 各行の 〇 と × の数を計算 しましたが、下記のプログラムのように 2 次元の ndarray が代入された narow に対して 直接 for 文で繰り返し処理を行う ほうが、プログラムが簡潔 になります。ただし、ndarray に対して for 文で繰り返し処理を行う と、axis 0 の各要素 に対する繰り返し処理が行われた結果、実行結果のように narow[0, :]narow[1, :] の内容が表示されてしまいます。これは 2 次元の list に対する繰り返し処理と同様 です。

for data in narow:
    print(data)

実行結果

[1 1 2]
[2 1 0]

この問題は ndarrayaxis 0 と axis 1 を入れ替えaxis 0 が行の番号 を、axis 1 がマークの種類 を表すようにすることで解決することができます。axis を入れ替えた ndarray は ndarray の T 属性 で計算することができます。例えば 2 次元の ndarray の場合は、下記のプログラムのように T 属性 によって axis 0 と axis 1 を入れ替えた 2 次元の ndarray を計算することができます。なお、ndarray の 軸を入れ替える ことを 転置(transpose) と呼び、T転置を表す transpose の略 です。

print(narow)
print()
print(narow.T)

実行結果

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

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

ndarray の T 属性 の詳細については下記のリンク先を参照して下さい。

3 次元以上の ndarray の T 属性は、axis を逆順に並べるように入れ替えます。例えば (2, 3, 4, 5, 6) の shape の 5 次元の ndarray の T 属性は、下記のプログラムのように (6, 5, 4, 3, 2) の shape の 5 次元の ndarray になります。

print(np.zeros((2, 3, 4, 5, 6)).T.shape)

実行結果

(6, 5, 4, 3, 2)

本記事では利用しないので説明は省略しますが、3 次元以上の ndarray の axis を任意の順番に入れ替える場合は np.transpose という関数を利用します。なお、ndarray には np.transpose と同じ処理を行う transpose というメソッドがあるので、どちらを使っても構いません。詳細は下記のリンク先を参照して下さい。

下記は T 属性 を利用して 行のマークのパターンを計算 するプログラムです。実行結果から 正しい計算が行われることが確認 できます。

markpats = defaultdict(int)
for markpat in narow.T:
    markpats[tuple(markpat)] += 1
pprint(markpats)

実行結果

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

先程と同様に tolist メソッドを利用して 処理速度を改善 することができます。tolist メソッドは 先ほどと同様1 次元の ndarray であるマークのパターン に対して行う方法と、2 次元の ndarray である narow.T を 2 次元の list に変換 する方法があります。それぞれの処理速度 を下記のプログラムで計測します。

%%timeit

markpats = defaultdict(int)
for markpat in narow.T:
    markpats[tuple(markpat)] += 1

実行結果

3.91 μs ± 226 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
%%timeit

markpats = defaultdict(int)
for markpat in narow.T:
    markpats[tuple(markpat.tolist())] += 1   # ここで tolist を利用する

実行結果

2.25 μs ± 210 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
%%timeit

markpats = defaultdict(int)
for markpat in narow.T.tolist():    # narow.T に対して tolist を呼び出す
    markpats[tuple(markpat)] += 1

実行結果

1.18 μs ± 16.7 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

実行結果から narow.T に対して tolist を呼び出して変換する処理が 最も処理速度が速い ことが確認できました。また、先程の 転置を行わない場合 の 1.79 μs よりも 処理速度が約 1.5 倍ほど速い ことも確認できました。処理が速くなる理由 は ndarray の要素に対する処理が list の要素に対する処理よりも遅い ためです。

手番を考慮した処理

マークのパターン は元のプログラムで下記のように記述されているように、直前の手番現在の手番の順番 でマークの数を tuple 要素に代入しますが、上記のプログラム では 常に 〇 の数、× の数の順 になっているため、〇 の手番の場合要素の順番が逆になってしまう という問題があります。

markpats[(lastturn_count, turn_count, emptycount)] += 1

この問題を解決するためには、〇 の手番の場合tuple の要素の順番を入れ替える 必要があります。要素の順番を入れ替える 方法としては np.flip を利用して マークの種類を表す axis の軸を反転 するという方法があります。マークの種類は 2 種類 なので、対応する軸を反転 することで 〇 と × のマークの順番が入れ替わります

下記はそのような処理を行うプログラムです。なお、これまで検証に利用してきた mb の局面は 〇 の手番ではない ので、下記のプログラムでは 〇 の手番の局面になる mb2 という局面を新たに作成しました。実行結果から正しい計算が行われることが確認できます。

  • 1 ~ 7 行目:先ほどの mb の最後の 1 手を無くした 〇 の手番の局面の mb2 を作成 する
  • 10 行目:各行の 〇 と × のマークの数を数えた 2 次元の ndarray を計算し、.T を記述することで axis 0 と axis 1 を転置した 2 次元の ndarray を計算する
  • 12、13 行目〇 の手番 の場合は 手番を表す axis 1 の軸を反転 して 順番を入れ替える
  • 14 行目:axis の入れ替えは 10 行目で行っているので、先程と異なり narow に対して tolist を呼び出すようにした
 1  mb2 = Marubatsu(boardclass=NpBoolBoard)
 2  mb2.cmove(0, 0)
 3  mb2.cmove(1, 0)
 4  mb2.cmove(0, 1)
 5  mb2.cmove(2, 0)
 6  mb2.cmove(1, 2)
 7  mb2.cmove(1, 1)
 8  print(mb2)
 9  
10  narow2 = np.count_nonzero(mb2.board.board, axis=1).T
11  markpats = defaultdict(int)
12  if mb2.turn == mb.CIRCLE:
13      narow = np.flip(narow2, axis=1)
14  for markpat in narow.tolist():
15      markpats[tuple(markpat)] += 1
16  print(markpats)
行番号のないプログラム
mb2 = Marubatsu(boardclass=NpBoolBoard)
mb2.cmove(0, 0)
mb2.cmove(1, 0)
mb2.cmove(0, 1)
mb2.cmove(2, 0)
mb2.cmove(1, 2)
mb2.cmove(1, 1)
print(mb2)

narow2 = np.count_nonzero(mb2.board.board, axis=1).T
markpats = defaultdict(int)
if mb2.turn == mb.CIRCLE:
    narow2 = np.flip(narow2, axis=1)
for markpat in narow2.tolist():
    markpats[tuple(markpat)] += 1
print(markpats)

実行結果

Turn o
oxx
oX.
.o.

defaultdict(<class 'int'>, {(2, 1): 1, (1, 1): 1, (0, 1): 1})

Counter を利用したマークのパターンの数を数える処理

これまでは defaultdict を利用 してマークのパターンの数を数えていましたが、マークのパターンを要素とする list を計算することで、以前の記事で説明した Counter を利用 して マークのパターンを数える ことができます。

Counter を利用するためには、2 次元の ndarray をマークのパターンを表す tuple を要素とする list に変換 する必要があります。その変換は下記のプログラムのように list 内包表記で行う ことができます。実行結果から 2 行目の l2dlist の要素[] で囲まれた list であるのに対し、3 行目の list 内包表記で作成 された list の要素() で囲まれた tuple に変換されている ことが確認できます。

l2d = narow2.tolist()
print(l2d)
print([tuple(data) for data in l2d])

実行結果

[[2, 1], [1, 1], [0, 1]]
[(2, 1), (1, 1), (0, 1)]

下記は上記で計算した マークのパターンを要素として持つ list に対して Counter でマークのパターンの数を数える プログラムです。実行結果から正しい処理が行われていることが確認できます。

  • 6 行目:list 内包表記で 2 次元の list の各要素を tuple に変換した list を計算し、それに対して Counter を利用してマークのパターンの数を数える
1  from collections import Counter
2  
3  narow = np.count_nonzero(mb.board.board, axis=1)
4  if mb.turn == mb.CIRCLE:
5      narow = np.flip(narow, axis=0)
6  markpats = Counter([tuple(data) for data in narow.T.tolist()])
7  print(markpats)
行番号のないプログラム
from collections import Counter

narow2 = np.count_nonzero(mb2.board.board, axis=1).T
if mb2.turn == mb.CIRCLE:
    narow2 = np.flip(narow2, axis=1)
markpats = Counter([tuple(data) for data in narow2.tolist()])
print(markpats)

実行結果

Counter({(2, 1): 1, (1, 1): 1, (0, 1): 1})

Counterdict や defaultdict と同様[キー] を記述することで キーの値を参照 することができるので、defaultdict で表現されていた マークのパターンを Counter に置き換えても プログラムは 正しく動作 します。これは 以前の記事で説明した ポリモーフィズム です。

下記は それぞれの処理速度を計測 するプログラムです。実行結果から Counter のほうが若干処理速度は遅いですが、大きくは変わらない ことが確認できました。Counter のほうがプログラムが簡潔に記述できる ので本記事では Counter のほうを採用 することにしますが、速度が気になる人は defaultdict の方を採用して下さい。

%%timeit

narow = np.count_nonzero(mb.board.board, axis=1)
markpats = defaultdict(int)
if mb.turn == mb.CIRCLE:
    narow = np.flip(narow, axis=0)
for markpat in narow.T.tolist():
    markpats[tuple(markpat)] += 1

実行結果

5.92 μs ± 143 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
%%timeit

narow = np.count_nonzero(mb.board.board, axis=1)
if mb.turn == mb.CIRCLE:
    narow = np.flip(narow, axis=0)
markpats = Counter([tuple(data) for data in narow.T.tolist()])

実行結果

6.8 μs ± 208 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)

ndarray の結合による行と列のマークのパターンの計算

mb2各列 のマークのパターンは下記のプログラムのように np.count_nonzero の実引数に axis=2 を記述 して計算します。実行結果から 正しい計算が行われることが確認 できます。〇 手番 なので マークのパターン(× の数, 〇 の数) となる点に注意して下さい。

print(mb2)
nacol = np.count_nonzero(mb2.board.board, axis=2).T
if mb2.turn == mb2.CIRCLE:
    nacol = np.flip(nacol, axis=1)
markpats = Counter([tuple(data) for data in nacol.tolist()])
print(markpats)

実行結果

Turn o
oxx
oX.
.o.

Counter({(0, 2): 1, (2, 1): 1, (1, 0): 1})

np.concatenate による 2 次元の ndarray の結合

2 つ以上 の ndarray を 1 つの ndarray にまとめる処理 の事を 結合 または 連結 と呼び、どちらも concatenate の日本語訳 です。本記事では結合と表記 することにします。

上記では 行と列 のマークのパターンの数を 別々に Counter で計算 していますが、2 つの 行と列の ndarray を結合 することで まとめて計算を行う ことができます。

ndarray の結合良く行われる処理 なので、numpy には ndarray を結合する 複数の関数が用意 されています。np.concatenate はその中の一つで、最初の実引数に結合する ndarray の一覧 を記述し、仮引数 axis で指定した axis に沿って ndarray を結合 します。

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

下記は 2 つの 2 次元の ndarrayaxis 0 で結合 するプログラムです。なお、2 つの 2 次元の ndarray の shape を同じ (2, 2) とした のは、axis 0 と axis 1 のどちらでも結合できる からです。後で説明するように 特定の条件 を満たしていれば 異なる shape の ndarray を結合 することができます。また、実際のプログラムは示しませんが 3 つ以上の 2 次元の ndarray を結合 することもできます。興味がある方は実際に試してみて下さい。

na2d1 = np.array([[1, 2], [3, 4]])
na2d2 = np.array([[5, 6], [7, 8]])
print(na2d1)
print()
print(na2d2)
print()
print(np.concatenate([na2d1, na2d2], axis=0))

実行結果

[[1 2]
 [3 4]]

[[5 6]
 [7 8]]

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

下記は 上記の処理を図示 したものです。図のように 2 つ(2, 2)shape2 次元の ndarrayaxis 0 の方向に結合 した結果、(4, 2)shape の新しい 2 次元の ndarray が計算 されます。また、下図から以下の事がわかります。

  • 結合した ndarray の axis 0 のインデックスの数元の ndarrayaxis 0 のインデックスの数の合計 である 2 + 2 = 4 になる
  • 結合した ndarray の axis 1 のインデックスの数元の ndarray の axis 1 の数と同じ になる。逆に言えば、axis 1 のインデックスの数が異なる 2 次元の ndarray を axis 0 の方向に沿って結合することはできない

下記は先ほどと同じ 2 つの 2 次元の ndarrayaxis 1 で結合 するプログラムです。

print(na2d1)
print()
print(na2d2)
print()
print(np.concatenate([na2d1, na2d2], axis=1))

実行結果

[[1 2]
 [3 4]]

[[5 6]
 [7 8]]

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

下記は 上記の処理を図示 したものです。図のように 2 つ(2, 2)shape2 次元の ndarrayaxis 1 の方向に結合 した結果、(2, 4)shape の新しい 2 次元の ndarray が計算 されます。また、下図から以下の事がわかります。

  • 結合した ndarray の axis 1 のインデックスの数元の ndarrayaxis 1 のインデックスの数の合計 である 2 + 2 = 4 になる
  • 結合した ndarray の axis 0 のインデックスの数元の ndarray の axis 0 の数と同じ になる。逆に言えば、axis 0 のインデックスの数が異なる 2 次元の ndarray を axis 1 の方向に沿って結合することはできない

2 次元の ndarraynp.concatenate による結合 をまとめると以下のようになります。

  • axis=0 を記述した場合
    • n 個の ($x_0$, $y$)、($x_1$, $y$)、・・・、($x_{n-1}$, $y$) の shape の 2 次元の ndarray を結合すると ($x_0 + x_1 + ・・・ + x_{n-1}$, $y$) の shape の 2 次元の ndarray が計算される
    • 結合する ndarray の axis 1 のインデックスの数はすべて同じでなければならない
  • axis=1 を記述した場合
    • n 個の ($x$, $y_0$)、($x$, $y_1$)、・・・、($x$, $y_{n-1}$) の shape の 2 次元の ndarray を結合すると ($x$, $y_0 + y_1 + ・・・ + y_{n-1}$) の shape の 2 次元の ndarray が計算される
    • 結合する ndarray の axis 0 のインデックスの数はすべて同じでなければならない

上記の例では同じ shape の 2 次元の ndarray を結合しましたが、(2, 2) と (5, 2) のように 異なる shape の 2 次元の ndarray を axis 0 で結合することができ、その場合は (2 + 5, 2) = (7, 2) の shape の 2 次元の ndarray が計算されます。

np.concatenate による 3 次元以上の ndarray の結合

本記事では行いませんが、3 次元の ndarray の場合も同様の処理が行われます。

下記は 3 次元の ndarrayaxis 2 で結合 するプログラムです。axis 2 としたのは図示しやすいためで、他の axis でも同様の処理が行われます。

na3d1 = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
na3d2 = np.array([[[9, 10], [11, 12]], [[13, 14], [15, 16]]])
print(na3d1)
print()
print(na3d2)
print()
print(np.concatenate([na3d1, na3d2], axis=2))

実行結果

[[[1 2]
  [3 4]]

 [[5 6]
  [7 8]]]

[[[ 9 10]
  [11 12]]

 [[13 14]
  [15 16]]]

[[[ 1  2  9 10]
  [ 3  4 11 12]]

 [[ 5  6 13 14]
  [ 7  8 15 16]]]

下記は 上記の処理を図示 したものです。図のように 2 つ(2, 2, 2)shape3 次元の ndarrayaxis 2 の方向に結合 した結果、(2, 2, 4)shape の新しい 3 次元の ndarray が計算 されます。また、下図から以下の事がわかります。

  • 結合した ndarray の axis 2 のインデックスの数元の ndarrayaxis 2 のインデックスの数の合計 である 2 + 2 = 4 になる
  • 結合した ndarray の axis 0 と axis 1 のインデックスの数元の ndarray の axis 0 と axis 1 の数と同じ になる。逆に言えば、axis 0 と axis 1 のインデックスの数が異なる 3 次元の ndarray を axis 2 の方向に沿って結合することはできない

2 次元と 3 次元の ndarray の結合の図から、複数の n 次元の ndarraynp.concatenate を利用して axis x で結合 した場合の処理が下記のようになることがわかります。

  • 作成される ndarray の 次元は変わらない
  • 結合する すべての ndarrayaxis x 以外インデックスの数は等しい必要がある
  • 作成される ndarrayaxis のインデックスの数 は以下のようになる
    • axis x 以外 の axis は 変化しない
    • axis x のインデックスの数は 結合する ndarrayaxis xインデックスの数の合計

例えば 5 次元 の (2, 3, 4, 5, 6) と (2, 3, 7, 5, 6) と (2, 3, 8, 5, 6) の shape の ndarray を axis 2 で結合 すると (2, 3, 4 + 7 + 8, 5, 6) = (2, 3, 19, 5, 6) の shape の 5 次元の ndarray が計算されます。下記はそのことを実証するプログラムです。

na5d1 = np.zeros((2, 3, 4, 5, 6))
na5d2 = np.zeros((2, 3, 7, 5, 6))
na5d3 = np.zeros((2, 3, 8, 5, 6))
print(np.concatenate([na5d1, na5d2, na5d3], axis=2).shape)

実行結果

(2, 3, 19, 5, 6)

結合による行と列のマークのパターンの数の計算

下記は np.concatenate によって 行と列のマークの数 を表す 2 次元の ndarray を結合 し、一回の Countermb行と列のマークのパターンの数を計算 するプログラムです。ゲーム盤の内容と実行結果を見比べて 正しい値が計算されていることを確認 して下さい。なお、mb は × の手番 なのでマークのパターンは (〇 の数, × の数) になります。

  • 2、3 行目:行と列のマークの数を数えた 2 次元の ndarray を計算する
  • 4、5 行目:行と列のマークの数を数えた 2 次元の ndarray を axis 0 で結合することで、axis 0 がマークのパターンの一覧の通し番号を、axis 1 がマークの種類を表す 2 次元の ndarray を作成する。5 行目は確認のための表示である
  • 6 ~ 8 行目:先程と同じ方法でマークのパターンの数を計算する
1  print(mb)
2  narow = np.count_nonzero(mb.board.board, axis=1).T
3  nacol = np.count_nonzero(mb.board.board, axis=2).T
4  napat = np.concatenate([narow, nacol], axis=0)
5  print(napat)
6  if mb.turn == mb.CIRCLE:
7      napat = np.flip(napat, axis=1)
8  markpats = Counter([tuple(data) for data in napat.tolist()])
9  print(markpats)
行番号のないプログラム
print(mb)
narow = np.count_nonzero(mb.board.board, axis=1).T
nacol = np.count_nonzero(mb.board.board, axis=2).T
napat = np.concatenate([narow, nacol], axis=0)
print(napat)
if mb.turn == mb.CIRCLE:
    napat = np.flip(napat, axis=1)
markpats = Counter([tuple(data) for data in napat.tolist()])
print(markpats)

実行結果

Turn o
oxx
oX.
.o.

[[1 2]
 [1 1]
 [1 0]
 [2 0]
 [1 2]
 [0 1]]
Counter({(2, 1): 2, (1, 1): 1, (0, 1): 1, (0, 2): 1, (1, 0): 1})

np.vstacknp.hstack による結合

本記事では利用しませんが、numpy には print で表示した際の axis の方向を基準 にして ndarray の 結合処理 を行う np.vstacknp.hstack があります。この 2 つは比較的よく使われますが、ndarray の次元によって行う処理が異なる 点がわかりづらいので紹介します。

なお、print では 3 次元以上の ndarrayそれぞれの axis を異なる方向にうまく表示できない ので方向が合わなくなります。3 次元 の場合は axis 0 を縦方向axis 1 を横方向axis 2 を奥方向 と考えて下さい。4 次元以上 の ndarray は 図示することは困難 なので np.vstacknp.hstack ではなく、np.concatenate を利用 したほうが良いでしょう。

np.vstack垂直方向(vertical)の axis に沿って ndarray を結合 する処理を行い、ndarray の次元によって 下記のように 行う処理が異なります

  • 1 次元の ndarrayprint では横方向に表示するので、1 次元の ndarray を縦に並べて結合 した 2 次元の ndarray を計算 する。結合によって ndarray の次元が異なる点に注意
  • 2 次元の ndarrayprint では axis 0 が縦方向 になるので、np.concatenateaxis=0 を記述した場合と同じ計算によって 2 次元の ndarray を計算 する
  • 3 次元以上の ndarray の場合は print での表示方向とは関係なく np.concatenateaxis=0 を記述した場合と同じ計算を行う

下記は 1 次元 の 2 つの (3, ) の shape の ndarray を np.vstack で結合 するプログラムで、実行結果のように na1d1na1d2縦に並べて表示される ような 2 次元の ndarray が計算 されます。この場合のみ元の ndarray と作成された ndarray の次元が異なります

na1d1 = np.array([1, 2, 3])
na1d2 = np.array([4, 5, 6])
print(np.vstack([na1d1, na1d2]))

実行結果

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

2 次元の ndarray に対しては、下記のプログラムのように print での縦方向 に相当する axis=0 を記述した np.concatenate と同じ計算 を行います。

print(np.vstack([na2d1, na2d2]))
print()
print(np.concatenate([na2d1, na2d2], axis=0))

実行結果

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

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

3 次元の ndarray に対しては、下記のプログラムのように axis=0 を記述した np.concatenate と同じ計算 を行います。プログラムは省略しますが、4 次元以上の ndarray の場合も同様 です。

print(np.vstack([na3d1, na3d2]))
print()
print(np.concatenate([na3d1, na3d2], axis=0))

実行結果

[[[ 1  2]
  [ 3  4]]

 [[ 5  6]
  [ 7  8]]

 [[ 9 10]
  [11 12]]

 [[13 14]
  [15 16]]]

[[[ 1  2]
  [ 3  4]]

 [[ 5  6]
  [ 7  8]]

 [[ 9 10]
  [11 12]]

 [[13 14]
  [15 16]]]

np.hstack水平方向(horizontal)の axis に沿って ndarray を結合 する処理を行い、ndarray の次元によって 下記のように 行う処理が異なります

  • 1 次元の ndarrayprint では横方向に表示するので、1 次元の ndarray を 横に並べて結合した 1 次元の ndarray を計算 する。np.vstack と異なり 同じ 1 次元の ndarray が計算される 点に注意すること
  • 2 次元の ndarrayprint では axis 1 が縦方向になる ので、np.concatenateaxis=1 を記述した場合と同じ計算 によって 2 次元の ndarray を計算する
  • 3 次元以上の ndarray の場合は print での表示方向とは関係なく np.concatenateaxis=1 を記述した場合と同じ計算 を行う

下記は 1 次元(3, ) の shape の ndarray を np.vstack で結合 するプログラムで、実行結果のように na1d1na1d2 が横に並べて表示 される 1 次元の ndarray が計算 されます。

print(na1d1)
print(na1d2)
print(np.hstack([na1d1, na1d2]))

実行結果

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

2 次元の ndarray に対しては、下記のプログラムのように print での横方向に相当 する axis=1 を記述した np.concatenate と同じ計算 を行います。

print(np.hstack([na2d1, na2d2]))
print()
print(np.concatenate([na2d1, na2d2], axis=1))

実行結果

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

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

3 次元の ndarray に対しては、下記のプログラムのように axis=1 を記述した np.concatenate と同じ計算 を行います。プログラムは省略しますが、4 次元以上の ndarray の場合も同様 です。

print(np.hstack([na3d1, na3d2]))
print()
print(np.concatenate([na3d1, na3d2], axis=1))

実行結果

[[[ 1  2]
  [ 3  4]
  [ 9 10]
  [11 12]]

 [[ 5  6]
  [ 7  8]
  [13 14]
  [15 16]]]

[[[ 1  2]
  [ 3  4]
  [ 9 10]
  [11 12]]

 [[ 5  6]
  [ 7  8]
  [13 14]
  [15 16]]]

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

numpy には他にも np.dstacknp.stacknp.block などの 結合を行う他の関数 が用意されています。下記の中で np.stack は連結前と後で ndarray の次元が 1 つ増えるという特徴があります。興味がある方は下記のリンク先を参照して下さい。

axis の追加による対角線のマークのパターンの計算

左上から右下方向の 対角線 1マークの数 は、下記のプログラムで計算されますが、実行結果からわかるように (2, ) の shape の 1 次元の ndarray が計算 されます。

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

実行結果

[1 1]

この ndarray の次元行や列のマークの数 を表す 2 次元の ndarray と異なる ため、np.concatenate を利用してデータを 結合することはできません

下記は 行や列のマークの数 を表す 2 次元の ndarray と上記で計算される 対角線のマークの数 を表す 1 次元の ndarrayaxis の表 です。

行や列のマーク 対角線のマーク 意味
axis 0 なし マークの数の一覧の通し番号
axis 1 axis 0 マークの種類

上記の表から、対角線のマークの数 を表す 1 次元の ndarrayマークの数の一覧の通し番号 を表す 新しい axis を加えて 2 次元の ndarray に変換する ことで、np.concatenate で結合できる ようになることがわかります。変換前と変換後で ndarray の要素の数は変わらない ので、追加する axis のインデックスの数は 1 になります。対角線のマークの数 を表す 1 次元の ndarray の shape(2, ) なので、具体的には 先頭に 1 を加えた (1, 2) の shape の 2 次元の ndarray に変換します。

下図は (2, ) の shape の 1 次元の ndarray から (1, 2) の shape の 2 次元の ndarray への変換 を表す図です。左右の ndarray同一の要素 を持ちますが、左が横方向の axis 0 のみ を持つのに対し、右は縦方向の axis 0横方向の axis 1 を持つ点が 異なります。このように 要素は変わらない が、インデックスの数が 1 の axis が増える という処理が行われます。

numpy には ndarray に インデックスの数が 1 の axis を増やす 処理を いくつかの方法で行う ことができます。その中の一つが [] の中 に記述する tuple の中で 追加したい axis に対応する要素np.newaxis を、それ以外の axis に対応する要素: を記述 します。下記は (3, ) の shape の 1 次元の ndarray先頭に axis を追加 するプログラムで、実行結果から (1, 3) の shape の 2 次元の ndarray が計算 されていることが確認できます。

print(na1d1)
print(na1d1.shape)
print(na1d1[np.newaxis, :])
print(na1d1[np.newaxis, :].shape)

実行結果

[1 2 3]
(3,)
[[1 2 3]]
(1, 3)

ndarray の次元を最低でも 2 次元や 3 次元に変換する np.atleast_2dnp.atleast_3d なども良く使われます。詳細は下記のリンク先を参照して下さい。

下記は np.newaxis を利用して mb の局面の 行と列と対角線マークのパターン の数を Counterまとめて数える プログラムです。

  • 4 ~ 7 行目[np.newaxis, :] を記述して 2 つの対角線のマークの数を表す (2, ) の shape の 1 次元の ndarray をそれぞれ (1, 2) の shape の 2 次元の ndarray に変換する
  • 8、9 行目np.concatenate で対角線のデータも結合するように修正する。9 行目でその内容を確認できるように print で表示した
 1  print(mb)
 2  narow = np.count_nonzero(mb.board.board, axis=1).T
 3  nacol = np.count_nonzero(mb.board.board, axis=2).T
 4  nadiag1 = np.count_nonzero(np.diagonal(mb.board.board,
 5                                         axis1=1, axis2=2), axis=1)[np.newaxis, :]
 6  nadiag2 = np.count_nonzero(np.diagonal(np.flip(mb.board.board, axis=2),
 7                                         axis1=1, axis2=2), axis=1)[np.newaxis, :]
 8  napat = np.concatenate([narow, nacol, nadiag1, nadiag2], axis=0)
 9  print(napat)
10  if mb.turn == mb.CIRCLE:
11      napat = np.flip(napat, axis=1)
12  markpats = Counter([tuple(data) for data in napat.tolist()])
13  print(markpats)
行番号のないプログラム
print(mb)
narow = np.count_nonzero(mb.board.board, axis=1).T
nacol = np.count_nonzero(mb.board.board, axis=2).T
nadiag1 = np.count_nonzero(np.diagonal(mb.board.board,
                                       axis1=1, axis2=2), axis=1)[np.newaxis, :]
nadiag2 = np.count_nonzero(np.diagonal(np.flip(mb.board.board, axis=2),
                                       axis1=1, axis2=2), axis=1)[np.newaxis, :]
napat = np.concatenate([narow, nacol, nadiag1, nadiag2], axis=0)
print(napat)
if mb.turn == mb.CIRCLE:
    napat = np.flip(napat, axis=1)
markpats = Counter([tuple(data) for data in napat.tolist()])
print(markpats)

実行結果

winner o
oxx
ox.
Oo.

[[1 2]
 [1 1]
 [2 0]
 [3 0]
 [1 2]
 [0 1]
 [1 1]
 [1 2]]
Counter({(1, 2): 3, (1, 1): 2, (2, 0): 1, (3, 0): 1, (0, 1): 1})

実行結果が前回の記事mb のマークのパターンcount_markpats2 で計算した下記の実行結果と順番は異なりますが 同じ内容になる ことから、上記のプログラムが 正しい処理を行うことが確認 できました。

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.newaxis の詳細については下記のリンク先を参照して下さい。

また、ndarray の shape を変更する方法 として np.reshape や ndarray の shape メソッドがあります。詳細は下記のリンク先を参照して下さい。

count_markpats3 の定義

下記は count_markpats3 の定義です。

  • 3 行目:defaultdict でのマークのパターンの計算は self.coun_linemarkTrue の場合のみで行うようになったので、2 行目の前にあった処理を 3 行目に移動した
  • 5 ~ 13 行目:先程説明した方法でマークのパターンを計算するように修正した
 1  def count_markpats3(self, turn, last_turn):
 2      if self.count_linemark:
 3          markpats = defaultdict(int)
元と同じなので省略
 4      else:
 5          narow = np.count_nonzero(self.board, axis=1).T
 6          nacol = np.count_nonzero(self.board, axis=2).T
 7          nadiag1 = np.count_nonzero(np.diagonal(self.board, axis1=1, axis2=2), axis=1)[np.newaxis, :]
 8          nadiag2 = np.count_nonzero(np.diagonal(np.flip(self.board, axis=2),
 9                                              axis1=1, axis2=2), axis=1)[np.newaxis, :]
10          napat = np.concatenate([narow, nacol, nadiag1, nadiag2], axis=0)
11          if turn == self.CIRCLE:
12              napat = np.flip(napat, axis=1)
13          markpats = Counter([tuple(data) for data in napat.tolist()])
14  
15      return markpats  
16  
17  NpBoolBoard.count_markpats3 = count_markpats3
行番号のないプログラム
def count_markpats3(self, turn, last_turn):
    if self.count_linemark:
        markpats = defaultdict(int)
        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:
        narow = np.count_nonzero(self.board, axis=1).T
        nacol = np.count_nonzero(self.board, axis=2).T
        nadiag1 = np.count_nonzero(np.diagonal(self.board, axis1=1, axis2=2), axis=1)[np.newaxis, :]
        nadiag2 = np.count_nonzero(np.diagonal(np.flip(self.board, axis=2),
                                               axis1=1, axis2=2), axis=1)[np.newaxis, :]
        napat = np.concatenate([narow, nacol, nadiag1, nadiag2], axis=0)
        if turn == self.CIRCLE:
            napat = np.flip(napat, axis=1)
        markpats = Counter([tuple(data) for data in napat.tolist()])

    return markpats  

NpBoolBoard.count_markpats3 = count_markpats3
修正箇所
def count_markpats3(self, turn, last_turn):
-   markpats = defaultdict(int)
    if self.count_linemark:
+       markpats = defaultdict(int)
元と同じなので省略
    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       
+       narow = np.count_nonzero(self.board, axis=1).T
+       nacol = np.count_nonzero(self.board, axis=2).T
+       nadiag1 = np.count_nonzero(np.diagonal(self.board, axis1=1, axis2=2), axis=1)[np.newaxis, :]
+       nadiag2 = np.count_nonzero(np.diagonal(np.flip(self.board, axis=2),
+                                              axis1=1, axis2=2), axis=1)[np.newaxis, :]
+       napat = np.concatenate([narow, nacol, nadiag1, nadiag2], axis=0)
+       if turn == self.CIRCLE:
+           napat = np.flip(napat, axis=1)
+       markpats = Counter([tuple(data) for data in napat.tolist()])

    return markpats  

NpBoolBoard.count_markpats3 = count_markpats3

処理速度の比較

下記のプログラムで count_makrpatscount_markpats2count_markpats3処理速度を比較 します。

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

実行結果

18.4 μs ± 1.05 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
29 μs ± 1.23 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
28.6 μs ± 923 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

実行結果から、残念ながら count_markpats3 の処理速度count_markpat2 とほとんど変わらない ことが確認できました。プログラムをより 簡潔に記述 することはできたと思いますが、残念ながら処理速度は count_markpat よりも遅い ので count_markpats3 は marubatsu.py のほうに参考までに記述は残しますが、採用しない ことにします。

今回の記事のまとめ

今回の記事では 3 次元の ndarray に対する np.rot90 による回転 の方法について説明し、calc_same_hashabels を定義して NpBoolBoard クラスを完成 させました。また、NpBoolBoard クラスの 処理速度について検証 しました。

次に、ndarray の転置と結合 を利用した calc_markpats別の定義方法 について説明しました。残念ながら処理速度は向上しませんでしたが、ndarray の転置や結合は良く使われる のでこの機会にその概要をぜひ覚えておくことをお勧めします。

今回の記事で numpy を利用したゲーム盤の表現は終了します。ndarray要素の数が少ない処理の効率が悪くなる ため、残念ながら 〇× ゲームのような 小さなゲーム盤を ndarray で表現 した場合の 処理速度は速くありません が、将棋などのもっと 大きなゲーム盤 の場合は ndarray を利用することで 処理速度が速くなることが期待 できます。また、ndarray を利用することでプログラムを より簡潔に記述できる という利点が得られます。

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

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

次回の記事

  1. 複数の axis を必ず指定するので axis の複数形である axes という名前になっています

  2. 本家のドキュメントには Rotation direction is from the first towards the second axis のように説明されています

  3. 残念ながら ndarray を直接 tuple に変換するメソッドはありません

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?