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を一から作成する その189 コンピューターの環境やPythonのバージョンの違いによる処理速度の影響と各直線上のマークの数を記録することの利点

Posted at

目次と前回の記事

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.103.123.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_judgetest_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 を利用する方法

下記の手順を行って下さい。

  1. Anaconda Navigator を立ち上げる
  2. 左の Environments をクリックして表示される仮想環境の一覧から marubatsu をクリックすると、右に marubatsu の仮想環境にインストールされているモジュールの一覧が表示される
  3. 上のメニューから「installed」を選択し、その右の「Search packages」と表示されたテキストボックスに「python」を入力する
  4. 下に python が表示され、右に現在インストールされている Python のバージョンが表示される。バージョンが青字で表示され、左に矢印が表示されている場合は新しいバージョンがあることを表しているので、バージョンの部分をクリックして下の「Apply」ボタンをクリックする。
  5. 「Update packages」というパネルが表示され、しばらく待つと「Apply」ボタンがクリックできるようになるのでクリックする。

conda を利用する方法

下記の手順を行って下さい。

  1. スタートメニューから、「Anaconda Powershell Prompt (anaconda3)」を探して実行する
  2. conda activate marubatsuを入力してエンターキー、marubatsu の仮想環境に入る
  3. conda install python を入力してエンターキーを押す
  4. インストールが開始され、メッセージが大量に表示される。[y/n]? のようなメッセージが表示された場合は y を入力してエンターキーを押す

Python のバージョンを指定してインストールしたい場合は、上記の手順 3 で下記のように = の後に Python のバージョンを入力してエンターキーを押してください。

conda install python=3.12

別の仮想環境を作成する方法

仮想環境の Python のバージョンを変える とこれまで動作していた プログラムが正しく動作しなくなる可能性 がまれにあります。その点が心配な方は、新しい仮想環境を作成 すると良いでしょう。仮想環境の作成の際 に Python の バージョンを指定できる のでそこで 3.13 のバージョンを指定します。なお、新しい仮想環境 には本記事でこれまでにインストールした ipykernelmatplotlib、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_matchmb = 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 文の処理 は条件式が単純であれば 処理時間はほとんどかからない ため、目に見えて処理時間増える ようなことは ありません

例えば、下記のプログラムのように aFalse を代入した後で if 文で aTrue かどうかを判定 するプログラムの処理時間を %%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)

以前の記事で計測したように、ai2sai14s処理時間の平均約 58 ~ 362 μs78 ~ 530 μs で、if 文の処理と比較 して 1000 倍以上 の時間がかかります。そのため、if 文を数か所追加したくらい では、全体の処理時間が 目に見えて増えるようなことはありません

もちろん、if 文の処理を数十個以上追加したり、if 文の条件式に時間のかかる複雑な式を設定した場合は処理時間に大きな影響を与えるので、そのような場合は処理時間が目に見えて増える場合があります。

各直線上のマークの数を記録する処理の利点

Python の バージョン 3.13 では残念ながら 各直線上のマークの数を記録する という方法は ai2s VS ai2s の対戦では 処理速度が悪化 してしまいますが、ai14s VS ai14s の場合は ある工夫を行なうことで 各直線上のマークの数を記録したほうが 処理速度を速く することができます。これが、上記で各直線上のマークの数を記録するかどうかを 選択できるようにした理由 です。どのような工夫を行なえば良いかについて少し考えてみて下さい。

修正前の処理時間の計測

修正による 処理速度を比較 するために、修正前の ai14s VS ai14s の 10000 回の対戦 を各直線上のマークの数を 記録しない場合する場合で計測 することにします。

下記は 各直線上のマークの数を記録しない 場合の対戦を行うプログラムです。ai14sai2s と比べて複雑な処理を行っているため 処理時間が長い ので、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 文の直後に記述した変数に代入される

上記の例では、繰り返しのたびlistalistb の先頭の要素から順番に、(1, 4)(2, 5)(3, 6) という tuple が取り出され、その tuple の要素が ab に代入 されます。

組み込み関数 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 文を記述するのは面倒なので、下記のプログラムのように 行、列、斜めのデータを記録する変数要素として持つ listfor 文で繰り返す ことで 処理をまとめる ことができます。実行結果から 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_linemarkTrue の場合は上記の方法で各直線上のマークの数を計算し、10 ~ 13 行目で直前の手番を考慮して計算したマークのパターンの数を数える
  • 14 ~ 26 行目count_linemarkFalse の場合はこれまでと同じ処理を行う
 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

次回の記事

近日公開予定です

  1. 前回の記事では処理回数と記述しましたが、対戦回数のほうがふさわしいと思いましたので修正しました

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?