目次と前回の記事
Python のバージョンとこれまでに作成したモジュール
前回の記事までは Python のバージョン 3.11 で実行していましたが、本記事から Python の バージョン 3.13 で実行しています。
以下のリンクから、これまでに作成したモジュールを見ることができます。なお、下記の marubatsu.py は 前回の記事の修正を行う前 のプログラムです。また、test.py の ファイル名を mbtest.py に変更 しました。その理由については今回の記事で説明します。
リンク | 説明 |
---|---|
marubatsu.py | Marubatsu、Marubatsu_GUI クラスの定義 |
ai.py | AI に関する関数 |
mbtest.py | テストに関する関数 |
util.py | ユーティリティ関数の定義 |
tree.py | ゲーム木に関する Node、Mbtree クラスなどの定義 |
gui.py | GUI に関する処理を行う基底クラスとなる GUI クラスの定義 |
AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。
コンピュータの環境による処理速度の違い
前回の記事の marubatsu.ipynb のプログラムを 別のパソコンで実行 してみたところ、前回の記事で行った処理の改善による 処理速度の上昇率 が、前回の記事と大きく異なる ことに気がつきました。下記はその実行結果の JuptyerLab のファイルのリンクです。前回の記事と同じ処理 を行うため、今回の記事の marubatsu.py の内容は 前回の記事の修正を行う前のものに戻しました。
下記は、前回の記事 の ベンチマークの結果の表 に 上記の結果を加えた表 です。最初の 4 つの列の意味 は以下の通りで、〇 が記載されている行 は その修正が行われた ことを表します。また、処理時間は精度が低いので 1 秒間の対戦回数のみ を記しました。また、太字の数値 は 最も対戦回数が多い 数値を表します。
- 4 以下:4 手目以下の判定の修正
- 手番:判定を行う必要がない手番の判定の修正
- 直線:必要のない直線の判定の修正
- 記録:各直線上のマークの数を記録の修正
4 以下 | 手番 | 直線 | 記録 | 前回の対戦回数1 | 別 PC での対戦回数 |
---|---|---|---|---|---|
323.25 | 180.02 | ||||
〇 | 876.84 | 238.63 | |||
〇 | 〇 | 1242.03 | 260.22 | ||
〇 | 〇 | 〇 | 1891.24 | 277.57 | |
〇 | 〇 | 〇 | 〇 | 1992.20 | 188.59 |
〇 | 〇 | 1165.33 | 186.15 | ||
〇 | 〇 | 〇 | 1661.86 | 248.92 |
別のパソコン は本記事のプログラムを実行してきたパソコンよりも 性能が低い ので、表のように すべての場合 で 対戦回数は少なくなります。しかし、下記の点が 前回の結果と大きく異なります。
- 本記事の執筆で利用しているパソコンの場合の 最大の対戦回数 は 改善前の約 6 倍 になっているのに対し、別のパソコンでは 277.57 ÷ 180.02 = 約 1.5 倍 にしかならない
- 別のパソコン では「4 以下」、「手番」、「直線」の 3 種類の改善 を行った場合が 最も対戦回数が多くなる
直観的 には A、B、C という 3 つの処理 を行うプログラムを 別のパソコンで実行 した際に 処理速度が 2 倍 になった場合は、A、B、C の処理速度がすべて 2 倍になると思う人が多いのではないかもしれませんが、実際にはパソコンの CPU、メモリ、外部記憶装置 などの 環境が変わる と、3 つの処理の 処理速度の倍率がそれぞれ異なる 場合があります。例えば A が 3 倍、B が 2 倍、C が 1 倍 となった結果 全体で処理速度が 2 倍 になる 可能性 があります。
上記のようなことが起きた理由はおそらく 前回の記事で行った改善の効率 が、別のパソコンの環境ではあまり良くない からではないかと思います。このように、あるパソコンで行った 改善の効率 が 常にどのパソコンでも高いとは限らない 点に注意が必要です。
他にも、パソコンで同時に複数のアプリケーションを実行すると、他のアプリケーションが CPU やメモリなどを消費した結果、アプリケーションの処理速度が変化する場合があります。そのため、これまでは本記事のプログラムを実行する際にできるだけ他のアプリケーションを実行しないようにしていました。一方、別のパソコンで marubatsu.ipynb を実行した際には多数のアプリケーションを同時に実行していたので、そのせいで処理が遅くなっていた可能性はあります。
Python のバージョンによる処理速度の違い
本記事では これまでは Python の バージョン 3.11 でプログラムを実行していますが、上記の 別のパソコン では Python の バージョン 3.10 でプログラムを実行していたことがわかりました。Python の バージョンが上がると処理速度が速くなる ことを聞いたことがあり、別のパソコン には Python の バージョン 3.11 の仮想環境 がありましたので、その環境で 同じ処理を行って 処理速度を計測 してみました。下記は実行結果の JuptyerLab のリンクです。
下記は先ほどの表に上記の実行結果を加えた表です。
4 以下 | 手番 | 直線 | 記録 | 前回 | 別 PC 3.10 | 別 PC 3.11 |
---|---|---|---|---|---|---|
323.25 | 180.02 | 203.96 | ||||
〇 | 876.84 | 238.63 | 298.92 | |||
〇 | 〇 | 1242.03 | 260.22 | 322.94 | ||
〇 | 〇 | 〇 | 1891.24 | 277.57 | 350.03 | |
〇 | 〇 | 〇 | 〇 | 1992.20 | 188.59 | 242.11 |
〇 | 〇 | 1165.33 | 186.15 | 238.05 | ||
〇 | 〇 | 〇 | 1661.86 | 248.92 | 311.04 |
表から Python の バージョン 3.11 のほうが 3.10 よりも 10 ~ 30 % ほど対戦回数 が多く、処理速度が速くなっている ことが確認できました。ただし、最大の対戦回数 が修正前の対戦回数の 約 1.5 倍 である点は 変わらない ので、最大の 対戦回数が約 6 倍にならない のは Python のバージョンが 3.10 であったからではなく、別のパソコンの環境による可能性が高い ことがわかります。
Python のバージョンに関する補足
Python のバージョン は 3.11.4 のように . で区切られて 3 つに分かれて おり、前から順に「メジャーバージョン」、「マイナーバージョン」、「マイクロバージョン」と呼びます。それぞれの意味は、本家のドキュメントには下記のように説明されています。
Python のバージョン番号は "A.B.C" または "A.B" が付けられます :
A はメジャーバージョン番号です -- 言語に本当に大きな変更があった場合に増やされます。
B はマイナーバージョン番号です -- 驚きの少ない変更があった場合に増やされます。
C はマイクロバージョン番号です -- バグフィックスリリースごとに増やされます。
マイクロバージョン は バグの修正ごとに増やされる ものなので、重要 なのは メジャーバージョンとマイナーバージョン です。そこで、本記事では マイクロバージョンの表記を省略 することにします。
本記事のパソコンでの Python のバージョンによる違い
Python は現在も 日々改善が行われている プログラム言語で、この記事の執筆を開始した 2023 年 9 月 の時点ではバージョンが 3.11 でしたが、今回の記事を執筆した 2025 年 8 月 の時点での最新バージョンは 3.13 になっています。さきほど別のパソコンで Python のバージョンが上がると処理速度が上がることが確認できたので、本記事で利用しているパソコンに Python の バージョン が 3.10、3.12、3.13 の 仮想環境を作成 し、前回の記事のプログラムを実行して 処理速度を比較 してみることにします。
下記は、それぞれの仮想環境 で前回の記事の marubatsu.ipynb を実行した 結果のファイル です。ただし、この後で説明しますが、最初のセルの内容と mbtest.py をインポートするという変更を行いました。
最初のセルの説明とと test モジュールのファイル名の変更
実行結果をまとめる前に、前回の記事の marubatsu.ipynb から修正した部分を説明します。
最初のセルに記述した下記のプログラムは、Python のバージョンを表示する処理 です。
#python のバージョンの表示
import sys
print(sys.version)
実行結果
3.13.5 | packaged by Anaconda, Inc. | (main, Jun 12 2025, 16:37:03) [MSC v.1929 64 bit (AMD64)]
Python の バージョン 3.12 から test という組み込みモジュールが存在 するため from test import test_judge
を実行すると、本記事で定義した test.py ではなく組み込みモジュールの test から test_judge
をインポートしようとして エラーが発生する ことが判明しました。そのため、本記事で作成した test.py のファイル名を mbtest.py という名前に変更 し、from mbtest import test_judge
で test_judge
をインポート するように修正しました。
バージョンによる処理速度の検証
下記は先ほどの実行結果をまとめた表です。3.11 の列 は 前回の記事の結果 です。太字の数字 が 最も対戦回数が多い(処理速度が速い)値を表します。
4 以下 | 手番 | 直線 | 記録 | 3.10 | 3.11 | 3.12 | 3.13 |
---|---|---|---|---|---|---|---|
202.40 | 323.25 | 431.38 | 689.49 | ||||
〇 | 512.89 | 876.84 | 1111.64 | 1593.37 | |||
〇 | 〇 | 674.19 | 1242.03 | 1463.72 | 2094.84 | ||
〇 | 〇 | 〇 | 959.18 | 1891.24 | 2176.32 | 2762.23 | |
〇 | 〇 | 〇 | 〇 | 954.89 | 1992.20 | 1600.80 | 2523.63 |
〇 | 〇 | 658.53 | 1165.33 | 1309.87 | 2058.37 | ||
〇 | 〇 | 〇 | 953.70 | 1661.86 | 1642.92 | 2318.97 |
表から、Python の バージョンが 3.10 ~ 3.13 の範囲では、バージョンが上がると処理速度が速くなる ことが確認できました。バージョン 3.10 と 最新の 3.13 では 約 2 ~ 3 倍、これまでの記事で利用してきた バージョン 3.11 と 3.13 では処理速度が 約 1.5 ~ 2 倍 程向上するようです。このことから、処理速度を速くするため には 最新 Python のバージョンを利用 することが 重要である ことがわかりました。
また、バージョン 3.12 と 3.13 では、「4 以下」、「手番」、「直線」の 3 種類の改善 を行った場合が 最も対戦回数が多くなる ことがわかります。このようなことが起きる理由は、おそらく バージョン 3.12 から list の処理速度が他の処理と比較して高速化 したため、「記録」の修正 によって 短縮された処理時間より も、追加された処理時間のほうが長く なってしまった可能性が考えられます。このように Python のバージョンが変わる と、パソコンの環境が変わった場合と同様に 特定の処理が他の処理と比較してより高速化 することで、それまでに行った 高速化の改善の効率が変化してしまう ことがあるようです。
バージョンがあがるとこれほど処理速度が改善するとは思っていなかったのでこれまでは Python のバージョンを 3.11 のまま記事を執筆していましたが、上記の結果を考慮して 今回の記事から Python の バージョンを 3.13 とした仮想環境でプログラムを実行 することにします。また、今回以降の記事では記事の冒頭に Python のバージョンを記載することにします。なお、マイクロバージョンはバグの修正の際に増える ため 処理速度は改善されない のではないかと思います。そのため、本記事では以後は マイナーバージョンが更新 された際に Python のバージョンをアップデート することにします。
Python のバージョンのアップグレードの方法
本記事では Python のバージョンが 3.13 の新しい仮想環境を作成しましたが、既存の仮想環境 の Python のバージョンだけをアップグレード することもできるのでその方法を説明します。なお、モジュールも同様の方法で最新のバージョンにアップグレード できます。
Anaconda Navigator を利用する方法
下記の手順を行って下さい。
- Anaconda Navigator を立ち上げる
- 左の Environments をクリックして表示される仮想環境の一覧から marubatsu をクリックすると、右に marubatsu の仮想環境にインストールされているモジュールの一覧が表示される
- 上のメニューから「installed」を選択し、その右の「Search packages」と表示されたテキストボックスに「python」を入力する
- 下に python が表示され、右に現在インストールされている Python のバージョンが表示される。バージョンが青字で表示され、左に矢印が表示されている場合は新しいバージョンがあることを表しているので、バージョンの部分をクリックして下の「Apply」ボタンをクリックする。
- 「Update packages」というパネルが表示され、しばらく待つと「Apply」ボタンがクリックできるようになるのでクリックする。
conda を利用する方法
下記の手順を行って下さい。
- スタートメニューから、「Anaconda Powershell Prompt (anaconda3)」を探して実行する
-
conda activate marubatsu
を入力してエンターキー、marubatsu の仮想環境に入る -
conda install python
を入力してエンターキーを押す - インストールが開始され、メッセージが大量に表示される。[y/n]? のようなメッセージが表示された場合は y を入力してエンターキーを押す
Python のバージョンを指定してインストールしたい場合は、上記の手順 3 で下記のように = の後に Python のバージョンを入力してエンターキーを押してください。
conda install python=3.12
別の仮想環境を作成する方法
仮想環境の Python のバージョンを変える とこれまで動作していた プログラムが正しく動作しなくなる可能性 がまれにあります。その点が心配な方は、新しい仮想環境を作成 すると良いでしょう。仮想環境の作成の際 に Python の バージョンを指定できる のでそこで 3.13 のバージョンを指定します。なお、新しい仮想環境 には本記事でこれまでにインストールした ipykernel、matplotlib、japanize_matplotlib、tqdm、ipywidgets モジュールはインストールされていないので、それらの モジュールをインストールする必要 があります。
また、Anaconda Navigator や conda では うまくアップグレードできない場合もある ようです。どうしてもうまくアップグレードできない場合 は 仮想環境を新しく作って下さい。
仮想環境の作成やモジュールのインストール方法については 以前の記事を参照して下さい。
各直線上のマークの数を記録するかどうかの切り替え
前回の記事では、最も処理速度が速い 4 つの修正をすべて採用する と説明しましたが、先程の検証結果から バージョン 3.13 では 各直線上のマークの数を記録する という 修正を行わないほうが処理が高速になる ことが判明しました。
ただし、この後で説明するように 各直線上のマークの数を記録する方法 には 大きな利点が存在する ので、各直線上のマークの数を記録するかどうか を下記の方法で 選択できるようにする ことにします。
- Marubatsu クラスに
counut_linemark
という、各直線(line)のマークの数を数える(count)かどうかを表す 属性を追加 する - Marubatsu クラスの
__init__
メソッドにデフォルト値をFalse
とする 仮引数count_linemark
を追加 し、その値をcount_linemark
属性に代入する
各メソッドの修正
__init__
メソッドで count_linemark
属性に値を設定 し、count_linemark
属性が True
の場合 に 各直線上のマークの数を記録する 処理を行うように各メソッドを修正します。
下記は __init__
メソッドを修正したプログラムです。
-
3 行目:デフォルト値を
False
とした仮引数count_linemark
を追加する -
7 行目:
count_linemark
属性に仮引数count_linemark
を代入する
1 from marubatsu import Marubatsu
2
3 def __init__(self, board_size=3, count_linemark=False):
4 # ゲーム盤の縦横のサイズ
5 self.BOARD_SIZE = board_size
6 # 各直線上のマークの数を数えるかどうか
7 self.count_linemark = count_linemark
8 # 〇×ゲーム盤を再起動するメソッドを呼び出す
9 self.restart()
10
11 Marubatsu.__init__ = __init__
行番号のないプログラム
from marubatsu import Marubatsu
def __init__(self, board_size=3, count_linemark=False):
# ゲーム盤の縦横のサイズ
self.BOARD_SIZE = board_size
# 各直線上のマークの数を数えるかどうか
self.count_linemark = count_linemark
# 〇×ゲーム盤を再起動するメソッドを呼び出す
self.restart()
Marubatsu.__init__ = __init__
修正箇所
from marubatsu import Marubatsu
-def __init__(self, board_size=3):
+def __init__(self, board_size=3, count_linemark=False):
# ゲーム盤の縦横のサイズ
self.BOARD_SIZE = board_size
+ # 各直線上のマークの数を数えるかどうか
+ self.count_linemark = count_linemark
# 〇×ゲーム盤を再起動するメソッドを呼び出す
self.restart()
Marubatsu.__init__ = __init__
下記は restart
メソッドを修正したプログラムです。
-
3 ~ 15 行目:
count_linemark
属性がTrue
の場合のみ、各直線上のマークの数を数えるための dict を初期化する
1 def restart(self):
元と同じなので省略
2 self.records = [self.last_move]
3 if self.count_linemark:
4 self.rowcount = {
5 Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
6 Marubatsu.CROSS: [0] * self.BOARD_SIZE,
7 }
8 self.colcount = {
9 Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
10 Marubatsu.CROSS: [0] * self.BOARD_SIZE,
11 }
12 self.diacount = {
13 Marubatsu.CIRCLE: [0] * 2,
14 Marubatsu.CROSS: [0] * 2,
15 }
16
17 Marubatsu.restart = restart
行番号のないプログラム
def restart(self):
self.initialize_board()
self.turn = Marubatsu.CIRCLE
self.move_count = 0
self.status = Marubatsu.PLAYING
self.last_move = -1, -1
self.last_turn = None
self.records = [self.last_move]
if self.count_linemark:
self.rowcount = {
Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
Marubatsu.CROSS: [0] * self.BOARD_SIZE,
}
self.colcount = {
Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
Marubatsu.CROSS: [0] * self.BOARD_SIZE,
}
self.diacount = {
Marubatsu.CIRCLE: [0] * 2,
Marubatsu.CROSS: [0] * 2,
}
Marubatsu.restart = restart
修正箇所
def restart(self):
元と同じなので省略
self.records = [self.last_move]
+ if self.count_linemark:
+ self.rowcount = {
+ Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
+ Marubatsu.CROSS: [0] * self.BOARD_SIZE,
+ }
+ self.colcount = {
+ Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
+ Marubatsu.CROSS: [0] * self.BOARD_SIZE,
+ }
+ self.diacount = {
+ Marubatsu.CIRCLE: [0] * 2,
+ Marubatsu.CROSS: [0] * 2,
+ }
Marubatsu.restart = restart
下記は move
メソッドを修正したプログラムです。
-
4 ~ 10 行目:
count_linemark
属性がTrue
の場合のみ、着手したマスの各直線上のマークの数を増やす処理を行う
1 def move(self, x, y):
2 if self.place_mark(x, y, self.turn):
元と同じなので省略
3 self.last_move = x, y
4 if self.count_linemark:
5 self.colcount[self.last_turn][x] += 1
6 self.rowcount[self.last_turn][y] += 1
7 if x == y:
8 self.diacount[self.last_turn][0] += 1
9 if x + y == self.BOARD_SIZE - 1:
10 self.diacount[self.last_turn][1] += 1
11 self.status = self.judge()
元と同じなので省略
12
13 Marubatsu.move = move
行番号のないプログラム
def move(self, x, y):
if self.place_mark(x, y, self.turn):
self.last_turn = self.turn
self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
self.move_count += 1
self.last_move = x, y
if self.count_linemark:
self.colcount[self.last_turn][x] += 1
self.rowcount[self.last_turn][y] += 1
if x == y:
self.diacount[self.last_turn][0] += 1
if x + y == self.BOARD_SIZE - 1:
self.diacount[self.last_turn][1] += 1
self.status = self.judge()
if len(self.records) <= self.move_count:
self.records.append(self.last_move)
else:
self.records[self.move_count] = self.last_move
self.records = self.records[0:self.move_count + 1]
Marubatsu.move = move
修正箇所
def move(self, x, y):
if self.place_mark(x, y, self.turn):
元と同じなので省略
self.last_move = x, y
+ if self.count_linemark:
+ self.colcount[self.last_turn][x] += 1
+ self.rowcount[self.last_turn][y] += 1
+ if x == y:
+ self.diacount[self.last_turn][0] += 1
+ if x + y == self.BOARD_SIZE - 1:
+ self.diacount[self.last_turn][1] += 1
self.status = self.judge()
元と同じなので省略
Marubatsu.move = move
下記は unmove
メソッドを修正したプログラムです。
-
4 ~ 10 行目:
count_linemark
属性がTrue
の場合のみ、直前に着手したマスの各直線上のマークの数を減らす処理を行う
1 def unmove(self):
2 if self.move_count > 0:
3 x, y = self.last_move
4 if self.count_linemark:
5 self.colcount[self.last_turn][x] -= 1
6 self.rowcount[self.last_turn][y] -= 1
7 if x == y:
8 self.diacount[self.last_turn][0] -= 1
9 if x + y == self.BOARD_SIZE - 1:
10 self.diacount[self.last_turn][1] -= 1
元と同じなので省略
11
12 Marubatsu.unmove = unmove
行番号のないプログラム
def unmove(self):
if self.move_count > 0:
x, y = self.last_move
if self.count_linemark:
self.colcount[self.last_turn][x] -= 1
self.rowcount[self.last_turn][y] -= 1
if x == y:
self.diacount[self.last_turn][0] -= 1
if x + y == self.BOARD_SIZE - 1:
self.diacount[self.last_turn][1] -= 1
if self.move_count == 0:
self.last_move = (-1, -1)
self.move_count -= 1
self.turn, self.last_turn = self.last_turn, self.turn
self.status = Marubatsu.PLAYING
x, y = self.records.pop()
self.remove_mark(x, y)
self.last_move = self.records[-1]
Marubatsu.unmove = unmove
修正箇所
def unmove(self):
if self.move_count > 0:
x, y = self.last_move
+ if self.count_linemark:
+ self.colcount[self.last_turn][x] -= 1
+ self.rowcount[self.last_turn][y] -= 1
+ if x == y:
+ self.diacount[self.last_turn][0] -= 1
+ if x + y == self.BOARD_SIZE - 1:
+ self.diacount[self.last_turn][1] -= 1
元と同じなので省略
Marubatsu.unmove = unmove
下記は is_winner
メソッドを修正したプログラムです。修正は if 文で処理を分けるだけなので修正箇所は省略しました。
-
3 ~ 13 行目:
count_linemark
属性がTrue
の場合は、着手したマスの各直線上のマークの数で勝利しているかの判定を行う -
14 ~ 24 行目:
count_linemark
属性がFalse
の場合は、is_same
で勝利しているかの判定を行う
1 def is_winner(self, player):
2 x, y = self.last_move
3 if self.count_linemark:
4 if self.rowcount[player][y] == self.BOARD_SIZE or \
5 self.colcount[player][x] == self.BOARD_SIZE:
6 return True
7 # 左上から右下方向の判定
8 if x == y and self.diacount[player][0] == self.BOARD_SIZE:
9 return True
10 # 右上から左下方向の判定
11 if x + y == self.BOARD_SIZE - 1 and \
12 self.diacount[player][1] == self.BOARD_SIZE:
13 return True
14 else:
15 if self.is_same(player, coord=[0, y], dx=1, dy=0) or \
16 self.is_same(player, coord=[x, 0], dx=0, dy=1):
17 return True
18 # 左上から右下方向の判定
19 if x == y and self.is_same(player, coord=[0, 0], dx=1, dy=1):
20 return True
21 # 右上から左下方向の判定
22 if x + y == self.BOARD_SIZE - 1 and \
23 self.is_same(player, coord=[self.BOARD_SIZE - 1, 0], dx=-1, dy=1):
24 return True
25
26 # どの一直線上にも配置されていない場合は、player は勝利していないので False を返す
27 return False
28
29 Marubatsu.is_winner = is_winner
先程説明したように marubatsu.py を 前回の記事の修正を行う前のプログラムに戻した ので、judge
メソッドを 前回の記事 のように修正 する必要があります。プログラムは前回の記事と同じなのでおりたたみました。
judge
メソッドの修正
def judge(self):
if self.move_count < self.BOARD_SIZE * 2 - 1:
return Marubatsu.PLAYING
# 直前に着手を行ったプレイヤーの勝利の判定
if self.is_winner(self.last_turn):
return self.last_turn
# 引き分けの判定
elif self.is_full():
return Marubatsu.DRAW
# 上記のどれでもなければ決着がついていない
else:
return Marubatsu.PLAYING
Marubatsu.judge = judge
なお、前回の記事 の最後で is_same
メソッドを削除 することにしましたが、前回の記事の修正を行う前の marubatsu.py には残っているので 再定義する必要はありません。
ai_match
の修正
現状の ai_match
は mb = Marubatsu()
で Marubatsu クラスのインスタンスを作成 するので、下記のプログラムのように Marubatsu クラスの インスタンスを作成する際に記述する実引数 を表す dict を代入する仮引数 mbparams
を追加 する必要があります。mbparams
に代入した dict は 5 行目で以前の記事で説明した マッピング型の展開 を行うので、デフォルト値を空の dict とするデフォルト引数にしました。
-
4 行目:デフォルト値を空の dict とする仮引数
mbparams
を追加する -
5 行目:Marubatsu クラスのインスタンスを作成する際に、実引数に
**mbparams
を記述してマッピング型の展開が行われるように修正する
1 from collections import defaultdict
2 from tqdm import tqdm
3
4 def ai_match(ai, params=[{}, {}], match_num=10000, mbparams={}):
5 mb = Marubatsu(**mbparams)
元と同じなので省略
行番号のないプログラム
from collections import defaultdict
from tqdm import tqdm
def ai_match(ai, params=[{}, {}], match_num=10000, mbparams={}):
mb = Marubatsu(**mbparams)
# ai[0] VS ai[1] と ai[1] VS a[0] の対戦を match_num 回行い、通算成績を数える
count_list = [ defaultdict(int), defaultdict(int)]
for _ in tqdm(range(match_num)):
count_list[0][mb.play(ai, params=params, verbose=False)] += 1
count_list[1][mb.play(ai=ai[::-1], params=params[::-1], verbose=False)] += 1
# ai[0] から見た通算成績を計算する
count_list_ai0 = [
# ai[0] VS ai[1] の場合の、ai[0] から見た通算成績
{
"win": count_list[0][Marubatsu.CIRCLE],
"lose": count_list[0][Marubatsu.CROSS],
"draw": count_list[0][Marubatsu.DRAW],
},
# ai[1] VS ai[0] の場合の、ai[0] から見た通算成績
{
"win": count_list[1][Marubatsu.CROSS],
"lose": count_list[1][Marubatsu.CIRCLE],
"draw": count_list[1][Marubatsu.DRAW],
},
]
# 両方の対戦の通算成績の合計を計算する
count_list_ai0.append({})
for key in count_list_ai0[0]:
count_list_ai0[2][key] = count_list_ai0[0][key] + count_list_ai0[1][key]
# それぞれの比率を計算し、ratio_list に代入する
ratio_list = [ {}, {}, {} ]
for i in range(3):
for key in count_list_ai0[i]:
ratio_list[i][key] = count_list_ai0[i][key] / sum(count_list_ai0[i].values())
# 各行の先頭に表示する文字列のリスト
item_text_list = [ Marubatsu.CIRCLE, Marubatsu.CROSS, "total" ]
# 通算成績の回数と比率の表示
width = max(len(str(match_num * 2)), 7)
diff_list = [ ("count", count_list_ai0, f"{width}d"),
("ratio", ratio_list, f"{width}.1%") ]
for title, data, format in diff_list:
print(title, end="")
for key in data[0]:
print(f" {key:>{width}}", end="")
print()
for i in range(3):
print(f"{item_text_list[i]:5}", end="")
for value in data[i].values():
print(f" {value:{format}}", end="")
print()
print()
return diff_list
修正箇所
from collections import defaultdict
from tqdm import tqdm
-def ai_match(ai, params=[{}, {}], match_num=10000):
+def ai_match(ai, params=[{}, {}], match_num=10000, mbparams={}):
- mb = Marubatsu()
+ mb = Marubatsu(**mbparams)
元と同じなので省略
処理の確認
下記は、各直線上のマークの数を記録しない 場合で 前回の記事 のベンチマークの ai2s
VS ai2s
の対戦を 10000 回行う プログラムです。
from ai import ai2s
ai_match(ai=[ai2s, ai2s], match_num=5000)
実行結果
100%|██████████| 5000/5000 [00:01<00:00, 2889.57it/s]
count win lose draw
o 2859 1508 633
x 1381 2949 670
total 4240 4457 1303
ratio win lose draw
o 57.2% 30.2% 12.7%
x 27.6% 59.0% 13.4%
total 42.4% 44.6% 13.0%
下記は ai_match
の実引数に mbparams={"count_linemark": True}
を記述することで 各直線上のマークの数を記録する 場合で ai2s
VS ai2s
の対戦を 10000 回行う プログラムです。
ai_match(ai=[ai2s, ai2s], match_num=5000, mbparams={"count_linemark": True})
実行結果
100%|██████████| 5000/5000 [00:01<00:00, 2756.29it/s]
count win lose draw
o 2893 1462 645
x 1489 2926 585
total 4382 4388 1230
ratio win lose draw
o 57.9% 29.2% 12.9%
x 29.8% 58.5% 11.7%
total 43.8% 43.9% 12.3%
下記は修正前と修正後の 対戦回数をまとめた表 です。関係のない行は削除し、最初の 4 列は 1 列にまとめました。若干修正後の方が対戦回数が増えていますが、およその傾向は変わらない ことがわかります。ai2s
VS ai2s
はランダムな対戦が行われるので決着がつくまでの手数が毎回異なります。そのため、下記の修正前後程度の違いは誤差の範囲だと思います。
修正前 | 修正後 | |
---|---|---|
各直線上のマークの数を記録しない | 2762.23 | 2889.57 |
各直線上のマークの数を記録する | 2523.63 | 2756.29 |
if 文の追加による処理速度の違い
上記では、各直線上のマークの数を記録するか どうかを 区別するため に いくつかの if 文 による条件分岐の 処理を追加 したので、それによって処理速度が遅くなることを心配した人がいるかもしれません。実際には if 文の処理 は条件式が単純であれば 処理時間はほとんどかからない ため、目に見えて処理時間増える ようなことは ありません。
例えば、下記のプログラムのように a
に False
を代入した後で if 文で a
が True
かどうかを判定 するプログラムの処理時間を %%timeit で計測 すると、実行結果のように処理時間の平均が 16.6 ns = 0.00166 μs という 非常に短いことが確認 できます。
a = False
%%timeit
if a:
pass
実行結果
16.6 ns ± 1.68 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
以前の記事で計測したように、ai2s
と ai14s
の 処理時間の平均 は 約 58 ~ 362 μs と 78 ~ 530 μs で、if 文の処理と比較 して 1000 倍以上 の時間がかかります。そのため、if 文を数か所追加したくらい では、全体の処理時間が 目に見えて増えるようなことはありません。
もちろん、if 文の処理を数十個以上追加したり、if 文の条件式に時間のかかる複雑な式を設定した場合は処理時間に大きな影響を与えるので、そのような場合は処理時間が目に見えて増える場合があります。
各直線上のマークの数を記録する処理の利点
Python の バージョン 3.13 では残念ながら 各直線上のマークの数を記録する という方法は ai2s
VS ai2s
の対戦では 処理速度が悪化 してしまいますが、ai14s
VS ai14s
の場合は ある工夫を行なうことで 各直線上のマークの数を記録したほうが 処理速度を速く することができます。これが、上記で各直線上のマークの数を記録するかどうかを 選択できるようにした理由 です。どのような工夫を行なえば良いかについて少し考えてみて下さい。
修正前の処理時間の計測
修正による 処理速度を比較 するために、修正前の ai14s
VS ai14s
の 10000 回の対戦 を各直線上のマークの数を 記録しない場合 と する場合で計測 することにします。
下記は 各直線上のマークの数を記録しない 場合の対戦を行うプログラムです。ai14s
は ai2s
と比べて複雑な処理を行っているため 処理時間が長い ので、1 秒間の 対戦回数 が 415.86 回 のように ai2s
VS ai2s
の対戦の場合の 2889.57 回 と比べて 大幅に少なく なっています。また、ai14s
は弱解決の AI なので対戦成績が 100 % 引き分けになります。
from ai import ai14s
ai_match(ai=[ai14s, ai14s], match_num=5000)
実行結果
100%|██████████| 5000/5000 [00:12<00:00, 415.86it/s]
count win lose draw
o 0 0 5000
x 0 0 5000
total 0 0 10000
ratio win lose draw
o 0.0% 0.0% 100.0%
x 0.0% 0.0% 100.0%
total 0.0% 0.0% 100.0%
下記は 各直線上のマークの数を記録する場合 の対戦で、対戦回数が 452.00 回 で上記と あまり変わらない ことが確認できます。これは全体に占める ai14s
の処理時間が長い ため 各直線上のマークの数を記録するかどうかの 処理速度の差 が全体からみると 小さな割合 になってしまい、誤差のほうが大きく なってしまったためです。
ai_match(ai=[ai14s, ai14s], match_num=5000, mbparams={"count_linemark": True})
実行結果
100%|██████████| 5000/5000 [00:11<00:00, 452.00it/s]
count win lose draw
o 0 0 5000
x 0 0 5000
total 0 0 10000
ratio win lose draw
o 0.0% 0.0% 100.0%
x 0.0% 0.0% 100.0%
total 0.0% 0.0% 100.0%
下記はベンチマークの結果をまとめた表です。
ai2s VS ai2s
|
ai14s VS ai14s
|
|
---|---|---|
各直線上のマークの数を記録しない | 2762.23 | 415.86 |
各直線上のマークの数を記録する | 2523.63 | 452.00 |
count_markpats
の改良
ai14s
では、以前の記事で定義した下記の マークのパターンの数を数える count_markpats
を利用して局面の 評価値を計算 しています。この関数が行う処理の詳細について忘れた方は以前の記事を復習して下さい。
def count_markpats(self):
markpats = defaultdict(int)
# 横方向と縦方向の判定
for i in range(self.BOARD_SIZE):
count = self.count_marks(coord=[0, i], dx=1, dy=0, datatype="tuple")
markpats[count] += 1
count = self.count_marks(coord=[i, 0], dx=0, dy=1, datatype="tuple")
markpats[count] += 1
# 左上から右下方向の判定
count = self.count_marks(coord=[0, 0], dx=1, dy=1, datatype="tuple")
markpats[count] += 1
# 右上から左下方向の判定
count = self.count_marks(coord=[2, 0], dx=-1, dy=1, datatype="tuple")
markpats[count] += 1
return markpats
この関数では、8 種類 のそれぞれの 直線のマークのパターン を count_marks
で計算 し、それをそれぞれの マークのパターンの数を記録 する markpats
に反映する という処理を行っています。また、count_marks
では、下記のプログラムで直線上の それぞれのマークの数を数える ことでマークのパターンを計算していますが、各直線上のマークの数を記録する場合 は 〇 と × のマークの数は rowcount
などの属性に記録済 です。従って、その値を利用 してマークのパターン計算することで 処理速度を大幅に向上 させることができます。どのようなプログラムを記述すれば良いかについて少し考えてみて下さい。
def count_marks(self, coord, dx, dy, datatype="dict"):
x, y = coord
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[self.last_turn], count[self.turn], count[Marubatsu.EMPTY])
Markpats
の復習
count_markpats
では実引数に datatype="tuple"
を記述して count_marks
を呼び出している ので、count_marks
が計算する マークのパターン は下記の Markpat という NamedTuple を継承した クラス で定義されます。下記のプログラムの意味について忘れた方は以前の記事を復習して下さい。なお、復習が面倒な方は (直前のターンのマークの数, 現在のターンのマークの数, 空のマスの数)
という tuple とほぼ同じデータ構造 だと考えて大丈夫です。
class Markpat(NamedTuple):
"""マークのパターン.
Attributes:
last_turn (int):
直前のターンのマークの数
turn (int):
現在のターンのマークの数
empty (int):
空のマスの数
"""
last_turn: int
turn: int
empty: int
例えば ゲーム開始後に (0, 0) を着手した局面 の場合を考えてみることにします。
その局面は 直前の手番が 〇 で、1 行目の 〇 の数 は self.rowcount[Marubatsu.CIRCLE][0]
、× の数 は self.rowcount[Marubatsu.CROSS][0]
に記録されています。空のマスの数 は「ゲーム盤のサイズ - 〇 のマークの数 - × のマークの数」で計算できる ので、1 行目のマークのパターン は 下記のプログラムで計算 することができます。実行結果から 1 行目のマークのパターンが 正しく計算されている ことが確認できます。
from marubatsu import Markpat
mb = Marubatsu(count_linemark=True)
mb.move(0, 0)
print(mb)
circlenum = mb.rowcount[Marubatsu.CIRCLE][0]
crossnum = mb.rowcount[Marubatsu.CROSS][0]
emptynum = mb.BOARD_SIZE - circlenum - crossnum
print(Markpat(circlenum, crossnum, emptynum))
実行結果
Turn x
O..
...
...
Markpat(last_turn=1, turn=0, empty=2)
また、2 行目 と 3 行目 のマークのパターンは下記のプログラムで計算できます。
circlenum = mb.rowcount[Marubatsu.CIRCLE][1]
crossnum = mb.rowcount[Marubatsu.CROSS][1]
emptynum = mb.BOARD_SIZE - circlenum - crossnum
print(Markpat(circlenum, crossnum, emptynum))
circlenum = mb.rowcount[Marubatsu.CIRCLE][2]
crossnum = mb.rowcount[Marubatsu.CROSS][2]
emptynum = mb.BOARD_SIZE - circlenum - crossnum
print(Markpat(circlenum, crossnum, emptynum))
実行結果
Markpat(last_turn=0, turn=0, empty=3)
Markpat(last_turn=0, turn=0, empty=3)
上記の処理を for 文の繰り返し処理で行う 場合は、mb.rowcount[Marubatsu.CIRCLE]
と mb.rowcount[Marubatsu.CROSS]
という 2 つの list の要素 を 先頭から順番に取り出す という処理を行う必要があり、これまでに本記事で説明した方法では下記のプログラムのように list の インデックス を for 文で 順番に取り出し、その インデックスを記述 して複数の list から要素を取り出すという処理を記述する必要があります。
for y in range(mb.BOARD_SIZE):
circlenum = mb.rowcount[Marubatsu.CIRCLE][y]
crossnum = mb.rowcount[Marubatsu.CROSS][y]
emptynum = mb.BOARD_SIZE - circlenum - crossnum
print(Markpat(circlenum, crossnum, emptynum))
実行結果
Markpat(last_turn=1, turn=0, empty=2)
Markpat(last_turn=0, turn=0, empty=3)
Markpat(last_turn=0, turn=0, empty=3)
上記のプログラムでも正しい処理が行われるのですが、python では list の要素を繰り返し処理で順番に取り出す際 に list の インデックスを利用することはあまり行われません。
実は昔のプログラムでは、上記のように for 文の変数に list のインデックスを代入して処理を行うのが一般的でした。なお、現在でも繰り返しの中でインデックスが必要になる場合がありますが、Python ではそのような場合は以前の記事で説明した組み込み関数 enumerate
を利用して記述するのが一般的です。
組み込み関数 zip
の利用方法
上記のような、複数の list の要素 を 先頭から順番に取り出す 処理は、zip
という組み込み関数を利用することで 簡潔に記述 することができます。
例えば、[1, 2, 3]
と [4, 5, 6]
という list に対して、先頭の要素から順番 にそれぞれの 合計を計算 する処理を行うプログラムは下記のように記述することができます。
lista = [1, 2, 3]
listb = [4, 5, 6]
for a, b in zip(lista, listb):
print(a + b)
行番号のないプログラム
lista = [1, 2, 3]
listb = [4, 5, 6]
for a, b in zip(lista, listb):
print(a + b)
実行結果
5
7
9
組み込み関数 zip
の実引数に 反復可能オブジェクトを複数記述 することで、for 文で以下のようなデータを順番に取り出すことができる 新しい反復可能オブジェクトが作成 されます。
-
zip
の実引数のそれぞれの反復オブジェクトから、先頭から順番に要素を取り出す - 取り出したデータを要素とする tuple が for 文の直後に記述した変数に代入される
上記の例では、繰り返しのたび に lista
と listb
の先頭の要素から順番に、(1, 4)
、(2, 5)
、(3, 6)
という tuple が取り出され、その tuple の要素が a
と b
に代入 されます。
組み込み関数 zip
の詳細については下記のプログラムを参照して下さい。
zip
を利用したマークのパターンの計算
下記は、3 つの行 のマークのパターンを zip
を利用して表示 するプログラムです。zip
を利用することで先ほどのプログラムと比較して 簡潔にプログラムを記述 できます。
for circlenum, crossnum in zip(mb.rowcount[Marubatsu.CIRCLE], mb.rowcount[Marubatsu.CROSS]):
emptynum = mb.BOARD_SIZE - circlenum - crossnum
print(Markpat(circlenum, crossnum, emptynum))
実行結果
Markpat(last_turn=1, turn=0, empty=2)
Markpat(last_turn=0, turn=0, empty=3)
Markpat(last_turn=0, turn=0, empty=3)
3 つの列 と 2 つの斜めの直線 のマークのパターンも 同様の方法で計算 できますが、3 回の for 文を記述するのは面倒なので、下記のプログラムのように 行、列、斜めのデータを記録する変数 を 要素として持つ list を for 文で繰り返す ことで 処理をまとめる ことができます。実行結果から 1 行目、1 列目、左上から右下の斜めのマークのパターンが (1, 0, 2)
で、それ以外が (0, 0, 3)
という正しい表示がされていることが確認できます。
for countdict in [mb.rowcount, mb.colcount, mb.diacount]:
for circlenum, crossnum in zip(countdict[Marubatsu.CIRCLE], countdict[Marubatsu.CROSS]):
emptynum = mb.BOARD_SIZE - circlenum - crossnum
print(Markpat(circlenum, crossnum, emptynum))
実行結果
Markpat(last_turn=1, turn=0, empty=2)
Markpat(last_turn=0, turn=0, empty=3)
Markpat(last_turn=0, turn=0, empty=3)
Markpat(last_turn=1, turn=0, empty=2)
Markpat(last_turn=0, turn=0, empty=3)
Markpat(last_turn=0, turn=0, empty=3)
Markpat(last_turn=1, turn=0, empty=2)
Markpat(last_turn=0, turn=0, empty=3)
8 つのマークのパターンを計算する方法がわかったので、下記のプログラムのように count_markpats
を修正することができます。修正は if 文で処理を分けるだけなので修正箇所は省略しました。
-
6 ~ 13 行目:
count_linemark
がTrue
の場合は上記の方法で各直線上のマークの数を計算し、10 ~ 13 行目で直前の手番を考慮して計算したマークのパターンの数を数える -
14 ~ 26 行目:
count_linemark
がFalse
の場合はこれまでと同じ処理を行う
1 from collections import defaultdict
2
3 def count_markpats(self):
4 markpats = defaultdict(int)
5
6 if self.count_linemark:
7 for countdict in [self.rowcount, self.colcount, self.diacount]:
8 for circlecount, crosscount in zip(countdict[Marubatsu.CIRCLE], countdict[Marubatsu.CROSS]):
9 emptycount = self.BOARD_SIZE - circlecount - crosscount
10 if self.last_turn == Marubatsu.CIRCLE:
11 markpats[Markpat(circlecount, crosscount, emptycount)] += 1
12 else:
13 markpats[Markpat(crosscount, circlecount, emptycount)] += 1
14 else:
15 # 横方向と縦方向の判定
16 for i in range(self.BOARD_SIZE):
17 count = self.count_marks(coord=[0, i], dx=1, dy=0, datatype="tuple")
18 markpats[count] += 1
19 count = self.count_marks(coord=[i, 0], dx=0, dy=1, datatype="tuple")
20 markpats[count] += 1
21 # 左上から右下方向の判定
22 count = self.count_marks(coord=[0, 0], dx=1, dy=1, datatype="tuple")
23 markpats[count] += 1
24 # 右上から左下方向の判定
25 count = self.count_marks(coord=[2, 0], dx=-1, dy=1, datatype="tuple")
26 markpats[count] += 1
27
28 return markpats
29
30 Marubatsu.count_markpats = count_markpats
行番号のないプログラム
from collections import defaultdict
def count_markpats(self):
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 self.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(coord=[0, i], dx=1, dy=0, datatype="tuple")
markpats[count] += 1
count = self.count_marks(coord=[i, 0], dx=0, dy=1, datatype="tuple")
markpats[count] += 1
# 左上から右下方向の判定
count = self.count_marks(coord=[0, 0], dx=1, dy=1, datatype="tuple")
markpats[count] += 1
# 右上から左下方向の判定
count = self.count_marks(coord=[2, 0], dx=-1, dy=1, datatype="tuple")
markpats[count] += 1
return markpats
Marubatsu.count_markpats = count_markpats
上記の修正後に ai14s
VS ai14s
の 10000 回の対戦 を行い、修正前の処理時間と比較することにします。
下記は 各直線上のマークの数を記録しない場合 の対戦を行うプログラムです。
ai_match(ai=[ai14s, ai14s], match_num=5000)
実行結果
100%|██████████| 5000/5000 [00:11<00:00, 438.59it/s]
count win lose draw
o 0 0 5000
x 0 0 5000
total 0 0 10000
ratio win lose draw
o 0.0% 0.0% 100.0%
x 0.0% 0.0% 100.0%
total 0.0% 0.0% 100.0%
下記は 各直線上のマークの数を記録する場合 の対戦を行うプログラムです。
ai_match(ai=[ai14s, ai14s], match_num=5000, mbparams={"count_linemark": True})
実行結果
100%|██████████| 5000/5000 [00:06<00:00, 740.22it/s]
count win lose draw
o 0 0 5000
x 0 0 5000
total 0 0 10000
ratio win lose draw
o 0.0% 0.0% 100.0%
x 0.0% 0.0% 100.0%
total 0.0% 0.0% 100.0%
下記は ai14s
VS ai14s
の 修正前と修正後の処理速度 をまとめた表です。
修正前 | 修正後 | |
---|---|---|
各直線上のマークの数を記録しない | 415.86 | 438.59 |
各直線上のマークの数を記録する | 452.07 | 740.22 |
各直線上のマークの数を記録しない場合 は count_markpats
が 行う処理は変わらない ので 対戦回数はほぼ同じ であることが確認できます。
各直線上のマークの数を記録する場合 は対戦回数が 約 1.6 倍に増えた ことから、処理速度が大幅に改善された ことがわかります。
Markpat から tuple への変更
Markpat が利用する Namedtuple は、tuple の要素 に dict と同じように 名前を付ける ことができる点から プログラムがわかりやすくなるという利点 がありますが、その分だけ tuple と比べて処理速度が遅くなる という欠点があります。
マークのパターン は、Markpat ではなく tuple として作成しても構わない ようにプログラムを記述したので、そのように count_markpats
のプログラムを修正して 処理速度を比較 してみることにします。下記はそのように count_markpats
を修正したプログラムです。
-
3、5 行目:
Markpat
を削除して tuple でマークのパターンを記述するように修正した
1 def count_markpats(self):
元と同じなので省略
2 if self.last_turn == Marubatsu.CIRCLE:
3 markpats[(circlecount, crosscount, emptycount)] += 1
4 else:
5 markpats[(crosscount, circlecount, emptycount)] += 1
元と同じなので省略
6
7 Marubatsu.count_markpats = count_markpats
行番号のないプログラム
def count_markpats(self):
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 self.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(coord=[0, i], dx=1, dy=0, datatype="tuple")
markpats[count] += 1
count = self.count_marks(coord=[i, 0], dx=0, dy=1, datatype="tuple")
markpats[count] += 1
# 左上から右下方向の判定
count = self.count_marks(coord=[0, 0], dx=1, dy=1, datatype="tuple")
markpats[count] += 1
# 右上から左下方向の判定
count = self.count_marks(coord=[2, 0], dx=-1, dy=1, datatype="tuple")
markpats[count] += 1
return markpats
Marubatsu.count_markpats = count_markpats
修正箇所
def count_markpats(self):
元と同じなので省略
if self.last_turn == Marubatsu.CIRCLE:
- markpats[Markpats(circlecount, crosscount, emptycount)] += 1
+ markpats[(circlecount, crosscount, emptycount)] += 1
else:
- markpats[Markpats(crosscount, circlecount, emptycount)] += 1
+ markpats[(crosscount, circlecount, emptycount)] += 1
元と同じなので省略
Marubatsu.count_markpats = count_markpats
上記の修正後に下記のプログラムで 各直線上のマークの数を記録する場合 で対戦を行います。各直線上のマークの数を記録しない場合の count_markpats
の処理は変わらないので対戦は省略しました。
ai_match(ai=[ai14s, ai14s], match_num=5000, mbparams={"count_linemark": True})
実行結果
100%|██████████| 5000/5000 [00:05<00:00, 896.40it/s]
count win lose draw
o 0 0 5000
x 0 0 5000
total 0 0 10000
ratio win lose draw
o 0.0% 0.0% 100.0%
x 0.0% 0.0% 100.0%
total 0.0% 0.0% 100.0%
下記は上記の実行結果を加えた表です。実行結果から Namedtuple を tuple に変更 することで 処理速度がさらに向上 することが確認できました。このように、似たようなデータ型 でも 便利な機能を増やすことで その分だけ 処理速度が低下する場合があります。
修正前 | 修正後 | tuple に変更 | |
---|---|---|---|
各直線上のマークの数を記録しない | 415.86 | 438.59 | |
各直線上のマークの数を記録する | 452.07 | 740.22 | 896.40 |
当然ですが count_markpats
を利用しない場合 は 上記の処理速度の改善は得られない ので、count_markpats
を利用する AI で対戦を行わない場合は各直線上のマークの数を 記録しないほうが処理速度が速く なります。そのため、Marubatsu クラスの __init__
メソッドの仮引数 count_linemark
のデフォルト値を False
に設定 しました。
なお、以前の記事でも述べましたが、処理速度を重視する必要がない アプリケーションでは、処理速度よりも プログラムのわかりやすさを重視 してプログラムを記述することをお勧めします。一般的に処理速度を向上させようとするとプログラムがわかりづらくなり、バグが発生しやすくなるから です。
今回の記事のまとめ
今回の記事では最初に コンピューターの環境 と Python のバージョンの違い による 処理速度の影響 について説明しました。
次に、ai14s
のような マークのパターンを利用 して評価値を計算する AI の場合は、各直線上のマークの数を記録する処理 で記録されたデータを うまく活用 することで 処理時間を改善できる ことを示しました。また、その際に Python で 良く使われる組み込み関数 zip
の使い方について説明しました。
なお、ai2s
の場合は各直線上のマークの数を記録しないほうが処理速度が速いので、各直線上のマークの数を記録するか どうかを 設定できる ような工夫を行ないました。
本記事で入力したプログラム
リンク | 説明 |
---|---|
marubatsu.ipynb | 本記事で入力して実行した JupyterLab のファイル |
marubatsu別PC310.ipynb | 別のパソコンの Python のバージョン 3.10 で実行した前回の記事の JupyterLab のファイル |
marubatsu別PC311.ipynb | 別のパソコンの Python のバージョン 3.11 で実行した前回の記事の JupyterLab のファイル |
marubatsu310.ipynb | Python のバージョン 3.10 で実行した前回の記事の JupyterLab のファイル |
marubatsu312.ipynb | Python のバージョン 3.12 で実行した前回の記事の JupyterLab のファイル |
marubatsu313.ipynb | Python のバージョン 3.13 で実行した前回の記事の JupyterLab のファイル |
marubatsu.py | 本記事で更新した marubatsu_new.py |
ai.py | 本記事で更新した ai_new.py |
次回の記事
近日公開予定です
-
前回の記事では処理回数と記述しましたが、対戦回数のほうがふさわしいと思いましたので修正しました ↩