LoginSignup
2
1

Pythonで〇×ゲームのAIを一から作成する その40 AI の強さの評価方法

Last updated at Posted at 2023-12-29

目次と前回の記事

これまでに作成したモジュール

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

これまでに作成した AI

これまでに作成した AI の アルゴリズム は以下の通りです。

関数名 アルゴリズム
ai1 左上から順に空いているマスを探し、最初に見つかったマスに着手する
ai2 ランダムなマスに着手する

AI の強さの評価方法

本記事ではさまざまな 〇×ゲームの AI を作成しますが、その際に、作成した AI の 強さを評価 する必要があります。何らかの競技で強さを評価する 一般的な方法 として、複数回対戦 を行い、勝率を比較 するという方法があります。本記事でも 作成した AI どうし複数回対戦 を行うことで AI の 強さを評価 することにします。

対戦の回数

対戦の回数 が 1 回や数回のように 少ない 場合は、弱い AI が 強い AI に 偶然勝ち越す ような、実際の強さ とは 異なる結果 がでる 可能性高く なりますが、対戦の 回数を増やせば 増やすほど、勝率実際の AI の強さより正確反映する ようになります。このような法則の事を 大数の法則 と呼びます。大数の法則の具体例として良く挙げられる例として、投げた時に 表と裏 がそれぞれ 50 % の確率で出る コインを何回か投げた時に、投げる 回数が少なければ 表が出る割合が 50% から大きく かけ離れる場合がある が、投げる 回数を多くすれば するほど表が出る割合が 50 % に近づいていく というものがあります。

参考までに大数の法則の Wikipedia へのリンクを下記に挙げておきます。

大数の法則 から、AI どうし の対戦の 回数を多くすれば するほど、計測された勝率 が、実際の AI どうしの 強さより正確に表す ようになりますが、対戦回数一定以上多くなる精度 がほとんど 上がらなく なります。また、対戦回数多くすれば するほど 対戦時間がかかる ようになるので、本記事では AI どうしの 対戦一万回 行うことにします。たたし、複雑な処理を行う AI のどうしの対戦などで、一万回の対戦 を行うと 時間がかかりすぎる 場合は、1 分程度終わる ような 回数の対戦 を行うことにします。

以下に、AI どうしの 対戦回数 に関する 方針 をまとめます。

  • AI どうしの 対戦の回数 は、原則 として 一万回 行う
  • 一万回の対戦に 時間がかかりすぎる場合 は、1 分で行える回数 の対戦を行う

AI のランダム性

上記で複数回の対戦を行うと説明しましたが、対戦する 双方 の AI の アルゴリズムランダム性がなければ複数回の対戦 を行う 必要ありません。その理由は、ランダム性のない アルゴリズムの AI は、同じ局面 で必ず 同じ着手 を行うからです。実際に、前回の記事で説明したように、ランダム性のない ai1 どうしの対戦 では、常に同じ対戦結果 になります。そのため、ランダム性のない アルゴリズムの AI どうしの対戦 から、AI の 強さの評価 を行うことは できません。その場合は 1 回の対戦 のみを行い、勝敗参考結果 とします。

対戦の組み合わせ

複数AI強さを評価 する方法は一つではありません。そこで、それぞれの方法 について 考察 し、どの方法を選択するか決める ことにします。

総当たり方式

複数AI強さを評価 する 最も正確な方法 は、すべての AI どうし対戦 を行う、総当たり方式(リーグ戦)で 対戦を行う という方法です。〇× ゲームは、〇 の手番× の手番有利不利大きく異なる ゲームなので、例えば ai1ai2強さを評価 する場合は、〇 と ×担当入れ替えたai1 VS ai2ai2 VS ai1両方の対戦行う必要 があります1。さらに、〇 と × で どれくらい有利不利があるか調べる ために、 ai1 VS ai1 のような、同じ AI どうし の対戦を 含めた場合 は、$n$ 個 の AI どうしの 総当たり戦組み合わせ は、$n ^ 2$ 通り になります。そのため、作成した AI の数増えていく に従い、対戦の 組み合わせ急激に増える ため、AI どうしの 対戦 を行うことが 困難 になります。例えば AI の数100 の場合は、対戦の組み合わせ は $100^2$ = 1 万 通りになります。

総当たり方式利点と欠点 は以下の通りです。

利点最も正確強さを評価 できる
欠点:AI の 増える と、対戦の 組み合わせ急激に増える

実際に総当たり方式で AI の評価を行うと、対戦に時間がかかりすぎるよう になってしまうため、本記事では 総当たり方式採用しない ことにします。

トーナメント方式

高校野球のような、何千以上の多くの チームの 優劣を決める方法 として、トーナメント方式 があります。トーナメント方式では、1 回の試合1 つ のチームが 敗退 し、最終的 には 1 つのチーム優勝する ので、n 個 のチームでトーナメント方式で試合を行うと、全部で n - 1 回試合 を行います。従って、チーム数が 増えても 試合数が 急激に増える ことは ありません。そのため、トーナメント方式で AI どうしの強さを評価すればよいと思う人がいるかもしれませんが、以下の理由 で本記事では トーナメント方式採用しません

  • トーナメント方式は、あらかじめ 強さの評価を行いたい AIすべて揃っている 場合は利用できるが、本記事で行うように、新しい AI次々に作成していく ような場合では、新しい AI を作成するたびトーナメント毎回やり直す 必要が生じてしまう
  • 下記で具体例を示すように、トーナメント方式は、本当は 強い AI ではない が、AI どうしの相性 がよかったせいで 勝ち残ってしまう可能性 がある

トーナメント方式利点と欠点 は以下の通りです。

利点:AI の 増えても、対戦の 組み合わせ急激に増えない
欠点:新しい AI が増える とトーナメントを やり直す必要 がある
   最も強い AI が 優勝 するとは 限らない

相性のせいで弱い AI がトーナメント方式で優勝する確率が高い例

AI どうし相性 のせいで、強くない AI が 勝ち残る可能性が高い 具体例を紹介します。

じゃんけんの AI で、下記の 4 つの AI を、下図の トーナメント方式 で戦わせた場合、それぞれの AI優勝する確率 を考えてみて下さい。なお、あいこに なった場合は、決着がつくまで 手を出し続けるものとします。

AI の名前 アルゴリズム
AI1 常にグーを出す
AI2 常にパーを出す
AI3 常にチョキを出す
AI4 ランダムに手を出す


AI1 VS AI4 は、AI1 の 勝ち負けあいこ確率いずれも 1/3 ですが、あいこの場合どちらかが勝つまで 手を出し続けるので、AI1勝ち負け確率いずれも 50 % になります。これは、AI2 VS AI4AI3 と AI4 の場合も 同様 です。従って、上図のトーナメントの それぞれの対戦結果 は以下のようになります。

  • 1 回戦の AI1 VS AI2 は、グーに対してパーを出す AI2100 % 勝つ
  • 1 回戦の AI3 VS AI4 は、お互い勝率は 50 % となる
  • 1 回戦で AI3 が勝利 した場合は、AI2 VS AI3 となり、チョキを出す AI3100 % 勝つ
  • 1 回戦で AI4 が勝利 した場合は、AI2 VS AI4 となり、お互い勝率は 50 % となる

従って、それぞれの AI の勝率は以下の表のようになり、常にチョキを出す AI3 が 50 % の確率最も優勝確率が高く なり、AI2AI425 %AI1 は決して 優勝できません

AI の名前 1 回戦の勝率 決勝の勝率 優勝確率
AI1 0 % - 0 %
AI2 100 % 相手が AI3 の場合は 0 %
相手が AI4 の場合は 50 %
1 * 0.5 * 0 + 1 * 0.5 * 0.5 = 25 %
AI3 50 % 100 % 0.5 * 1 = 50 %
AI4 50 % 50 % 0.5 * 0.5 = 25 %

じゃんけんの グーチョキパー3 すくみ の関係なので、優劣ありません が、上記の トーナメント では、グー を出す AI1 の 優勝確率0 %パー を出す AI 2 の 優勝確率25 %チョキ を出す AI3 の 優勝確率50 % のように、偏った 優勝確率になります。

また、じゃんけんを遊んだことがあれば、上記の トーナメント で最も 優勝確率が高い、常にチョキを出す AI3強いAIではない ことはすぐにわかるはずです。実際に 人間と AI3対戦 した場合は、AI3 はすぐに 勝てなくなる はずです。一方、人間 とランダムな手を出す AI4対戦 した場合は、お互い勝率は約 50 % になるので、人間と対戦する場合 は、AI4 のほう が AI3 より 強い という、上記の トーナメント異なる結果 になります。

トーナメント方式 でこのようなことがおきるのは、対戦相手の相性 によって 強い AI が必ずしも 優勝する確率高くならない場合がある からです。

総当たり方式の場合

参考までに、上記を 総当たり方式 で対戦した場合の 勝率 を以下の表で示します。表は、左の列 の AI からみた勝率 です。また、AI1AI2AI3同じ AI どうし対戦 すると永遠に 決着がつかない ので、それらの組み合わせの対戦は 省略 します。

AI1 AI2 AI3 AI4 平均
AI1 0 % 100 % 50 % 50 %
AI2 100 % 0 % 50 % 50 %
AI3 0 % 100 % 50 % 50 %
AI4 50 % 50 % 50 % 50 % 50 %

表から、総当たり方式 の場合は すべての AI平均勝率が 50 % になってしまうので、総当たり方式でも正しく AI の強さが評価できない思う人がいる かもしれませんが、そうではありません。確かに、上記の 4 つの AI どうしの対戦の中には、どちらかが 100 % 勝利する という 極端な相性が存在 しますが、この 4 つの AI どうしランダムな組み合わせ対戦 を行うと、どの AI も勝率が 50 % になる ことは 間違ってはいない のです。また、すべて組み合わせ対戦が行われる ので、個々AI どうしの相性確認 することができます。

なお、総当たり方式やトーナメント方式に限らず、すべての場合で共通する ことですが、参加した AI の中 での 強さを測る こと しかできない という 限界がある 点に 注意が必要 です。分かりやすい例でいうと、小学生 の野球の 総当たり戦で優勝 したからといって、そのチームが 中学生 の野球の 総当たり戦で優勝 できることはほとんど あり得ないない でしょう。本当の強さ測る ためには、多種多様な相手対戦 することが 重要 になるのです。

基準となる AI との比較

他の方法として、基準となる AI を用意し、新しい AI を作成した際に、基準となる AI と対戦 させた結果で AI の強さを評価 するという方法が考えられます。この方法は、新しい AI を作成する際に、基準となる AI対戦 させる だけで良い ので、評価に手間がかからない という 利点 があります。一方で、基準となる AI偏った性質 を持つ場合に、正しい評価行えない という 欠点 があります。例えば、先程の例の、じゃんけんで 常にグーを出す ような AI を 基準としてはいけない ことは誰でもすぐに理解できるのではないかと思います。

これがベストな選択であると断言することはできませんが、ランダムな着手 を行う ai2 は、ランダム という性質から、あまり偏っていない AI であると 考えること ができます。そこで、本記事では ai2基準となる AI とし、新しい AI を作成 するたびに ai2 と対戦 させることで、作成した AI の 強さの評価 を行うことにします。

なお、ai1 を基準となる AI として 選ばなかった理由 の一つは、ai1アルゴリズムランダム性がない からです。先程説明したように、ランダム性のない AI を作成して ai1 と対戦 させても 毎回同じ結果 になってしまうため、強さの評価 を行うことは できません

もう一つの理由は、ai1アルゴリズム が、じゃんけんの 常に同じ手を出す AI同様 に、偏っている からです。また、本記事の最後で説明しますが、ai1ai2 は、客観的に考えるai2 の方が強い と言えますが、ai1ai2 に対して 相性が良い ので、ai1ai2対戦 させると ai1 のほうが勝率が高く なってしまいます。

強い AI との比較

基準となる AI との 比較 は、作成した AI弱いうちは有効 ですが、AI が ある程度以上強くなる と、基準となる AI と対戦しても、勝率差が ほとんど 生じなく なります。

そのため、AI がある程度以上 強くなった場合 は、その時点までで 作成した強い AI どうし対戦 することで、AI の強さ評価する 必要が生じます。

そこで、本記事では、新しい AI を作成 した際に、基準となる AI だけでなく、それまでに作成 した AI の中で 最も強い と思われる いくつかのAI との対戦 も行うことにします。その際に、どの AI と対戦 させるかについては、状況に応じて その都度 判断 することにします。

本記事で行う対戦の組み合わせ

本記事では、新しい AI を作成するたびに、下記の AI と対戦を行うことにします。

  • ランダムな着手を行う ai2
  • それまでに作成 した AI の中で 最も強い AI と思われるのうちの いくつか

なお、上記で説明した以外にも AI どうしの強さをよりよく評価できる方法は実際にあると思います。そのような方法を思いついた人がいれば、それを採用してもかまいません。

AI どうしの対戦処理

AI の強さの評価を行う方法が決まったので、その方法に従って、指定した AI どうし複数回対戦 させて、その 通算成績集計 する必要があります。例えば ai1ai2対戦2 回 行う場合は、下記のプログラムのように、play メソッドを for 文 を使って 2 回呼び出せば良い のですが、play メソッドを 実行 しても、勝敗結果 は画面に winner o のように 表示 される だけ なので、このまま では 通算成績集計 することは できません

from marubatsu import Marubatsu
from ai import ai1, ai2

mb = Marubatsu()

for _ in range(2):
    mb.play(ai=[ai1, ai2])

実行結果(実行結果はランダムなので下記とは異なる場合があります)

Turn o
...
...
...

略

winner o
oox
oox
xxO

Turn o
...
...
...

略

winner o
ooO
x..
..x

play メソッドの修正 1(対戦結果を返すようにする)

プログラムplay メソッドの 対戦結果集計 するためには、play メソッドが、対戦結果返り値 として 返す ように 修正 する必要があります。対戦結果 は、status 属性代入 されるので、下記のプログラムの 11 行目に return self.status を追加 することで、play メソッドが 対戦結果を返す ようになります。

 1  def play(self, ai):
 2      # 〇×ゲームを再起動する
 3      self.restart()
 4
元と同じなので省略
 5
 6      # 決着がついたので、ゲーム盤を表示する
 7      print(self)
 8      # 勝敗結果を返り値として返す
 9      return self.status
10
11  Marubatsu.play = play
行番号のないプログラム
def play(self, ai):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # ゲーム盤の表示
        print(self)
        # 現在の手番を表す ai のインデックスを計算する
        index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ai が着手を行うかどうかを判定する
        if ai[index] is not None:
            x, y = ai[index](self)
        else:
            # キーボードからの座標の入力
            coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
            # "exit" が入力されていればメッセージを表示して関数を終了する
            if coord == "exit":
                print("ゲームを終了します")
                return       
            # x 座標と y 座標を要素として持つ list を計算する
            xylist = coord.split(",")
            # xylist の要素の数が 2 ではない場合
            if len(xylist) != 2:
                # エラーメッセージを表示する
                print("x, y の形式ではありません")
                # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
                continue
            x, y = xylist
        # (x, y) に着手を行う
        try:
            self.move(int(x), int(y))
        except:
            print("整数の座標を入力して下さい")

    # 決着がついたので、ゲーム盤を表示する
    print(self)
    return self.status

Marubatsu.play = play
修正箇所
def play(self, ai):
    # 〇×ゲームを再起動する
    self.restart()

元と同じなので省略

    # 決着がついたので、ゲーム盤を表示する
    print(self)
    # 勝敗結果を返り値として返す
+   return self.status

Marubatsu.play = play

下記のプログラムを実行し、実行結果winner: o のような表示が行われることから、play メソッドが 勝敗結果を返す ようになったことが 確認 できます。

for _ in range(2):
    print("winner: ", mb.play(ai=[ai1, ai2]))

実行結果(実行結果はランダムなので下記とは異なる場合があります)

Turn o
...
...
...

略

winner x
oox
oox
x.X

winner:  x
Turn o
...
...
...

略

winner o
ooO
x.x
...

winner:  o

勝敗結果Marubatsu クラスのインスタンスの status 属性代入 されるので、play メソッドを 修正しなくても、下記のプログラムのように記述すれば良いと思った人がいるかもしれません。なお、実行結果は上記と同様なので省略します。

for _ in range(2):
    mb.play(ai=[ai1, ai2])
    print("winner: ", mb.status)

確かに、上記のプログラム でも 正しく動作 しますが、上記のプログラムを 記述するため には、Marubatsu クラス に、ゲームの 勝敗結果 を表す status という 属性が存在する ことを 知っている必要 があります。もちろん、Marubatsu クラスを定義した作者はそのことは知っているはずですが、作者以外 の人が Marubatsu クラス利用 する場合は、そのこと を何らかの方法で 学ぶ必要 があります。そのため、play メソッドの 返り値 として、勝敗結果返す ようにしたほうが 親切 です。

他にも、話が長くなるので今回の記事では説明しませんが、インスタンスの属性 に対する 処理(値の参照や代入)は、クラスの メソッドの外 では なるべく行わないほうが良い という理由から、mb.status と記述して勝敗結果を参照するよりは、play メソッドの返り値として受け取ったほうが良いでしょう。

play メソッドの修正 2(途中経過の非表示)

先程のプログラムでは、画面 に AI どうしの 対戦途中経過表示 されていましたが、AI の 強さを評価するため対戦複数回行う際重要 となるのは、勝敗結果だけ で、途中経過 の情報は 必要ではありません。また、例えば 100 回の対戦 を行った結果、100 回分 の対戦の 途中経過 が画面に 表示 されてしまうと、「画面 がその表示で 埋め尽くされてしまう」、「表示の処理時間がかかる」などの 問題が発生 します。ちなみに、先程 のプログラムで、対戦回数2 回 にした 理由 は、実行結果表示少なくするため でした。

play メソッド 実行した際 に、対戦途中経過表示しない ように 修正 すれば良いと 思う人がいるかもしれません が、そのようplay メソッドを 修正 してしまうと、今度は 人間と AI の対戦や、人間どうし対戦 の際に、途中経過表示されなく なってしまい、人間遊ぶことができなく なってしまうという 欠点 が生じます。

そこで、play メソッド 実行 した際に、途中経過表示するかどうか選択 できるように 改良 することにします。そのために、そのことを 選択するため仮引数追加 します。

画面に メッセージ表示するかどうか選択 するための 仮引数名前 として、画面に表示される メッセージデバッグのため のメッセージである場合は debug良く使われます。他にも「言葉数の多い」、「多弁な」という意味を表す verbose という 名前良く使われる ようです。途中経過 の表示は デバッグ のためのメッセージ ではない ので、本記事では 仮引数の名前verbose とし、下記のプログラムのように、True が代入されていた場合は 途中経過を表示 し、False が代入されていた場合は 途中経過を表示しない ように play メソッドを 修正 します。なお、play メソッドで 〇×ゲームを 人間が遊ぶ際 に、毎回 実引数verbose=True記述 するのは 面倒 なので、verboseデフォルト引数値True とする デフォルト引数 とします。

  • 1 行目デフォルト引数値Trueデフォルト引数 verbose追加
  • 7、12 行目verboseTrue場合のみゲームの画面を表示 するように修正

なお、人間が座標を入力 する場合は、メッセージを表示 しなければ ゲームを遊べない ので、人間が座標を入力 する 処理の中メッセージ常に表示 するようにしています。

 1  def play(self, ai, verbose=True):
 2      # 〇×ゲームを再起動する
 3      self.restart()
 4      # ゲームの決着がついていない間繰り返す
 5      while self.status == Marubatsu.PLAYING:
 6          # ゲーム盤の表示
 7          if verbose:
 8              print(self)
 9
元と同じなので省略
10
11      # 決着がついたので、ゲーム盤を表示する
12      if verbose:
13          print(self)
14      return self.status
15
16  Marubatsu.play = play
行番号のないプログラム
def play(self, ai, verbose=True):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # ゲーム盤の表示
        if verbose:
            print(self)
        # 現在の手番を表す ai のインデックスを計算する
        index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ai が着手を行うかどうかを判定する
        if ai[index] is not None:
            x, y = ai[index](self)
        else:
            # キーボードからの座標の入力
            coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
            # "exit" が入力されていればメッセージを表示して関数を終了する
            if coord == "exit":
                print("ゲームを終了します")
                return       
            # x 座標と y 座標を要素として持つ list を計算する
            xylist = coord.split(",")
            # xylist の要素の数が 2 ではない場合
            if len(xylist) != 2:
                # エラーメッセージを表示する
                print("x, y の形式ではありません")
                # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
                continue
            x, y = xylist
        # (x, y) に着手を行う
        try:
            self.move(int(x), int(y))
        except:
            print("整数の座標を入力して下さい")

    # 決着がついたので、ゲーム盤を表示する
    if verbose:
        print(self)
    return self.status

Marubatsu.play = play
修正箇所
def play(self, ai, verbose=True):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # ゲーム盤の表示
-       print(self)
+       if verbose:
+           print(self)

元と同じなので省略      

    # 決着がついたので、ゲーム盤を表示する
-   print(self)
+   if verbose:
+       print(self)
    return self.status

Marubatsu.play = play

下記のプログラムのように、実引数verbose=False を記述して play を実行することで、途中経過表示されなくなった ことが確認できます。

for _ in range(2):
    print("winner: ", mb.play(ai=[ai1, ai2], verbose=False))

実行結果(実行結果はランダムなので下記とは異なる場合があります)

winner:  o
winner:  o

勝敗結果の集計

勝敗結果 は、〇 の勝利× の勝利引き分け3 種類 があるので、下記のプログラムのように、3 つの変数 と、if 文 を使って それぞれの回数集計 することができます。

  • 1 ~ 3 行目〇 の勝利× の勝利引き分け回数を数える ための circlecrossdraw という 3 つの変数0 を代入 して 初期化 する
  • 4 行目for 文100 回 の繰り返しを行う
  • 5 行目ai1ai2対戦 を行い、結果winner代入 する
  • 6 ~ 11 行目if 文 を使って、勝敗結果応じて回数を数える変数1 を足す
  • 13 行目繰り返し処理終了した時点集計が完了 するので、結果を表示 する

実行結果からわかるように、ai1 VS ai2 で 100 回対戦した結果は、ai1 の 72 勝 18 敗 10 分 でした。なお、ai2 には ランダム性がある ので、結果は下記と 異なる場合 があります。

 1  circle = 0
 2  cross = 0
 3  draw = 0
 4  for _ in range(100):
 5      winner = mb.play(ai=[ai1, ai2], verbose=False)
 6      if winner == Marubatsu.CIRCLE:
 7          circle += 1
 8      elif winner == Marubatsu.CROSS:
 9          cross += 1
10      else:
11          draw += 1
12
13  print("circle", circle, "cross", cross, "draw", draw)
行番号のないプログラム
circle = 0
cross = 0
draw = 0
for _ in range(100):
    winner = mb.play(ai=[ai1, ai2], verbose=False)
    if winner == Marubatsu.CIRCLE:
        circle += 1
    elif winner == Marubatsu.CROSS:
        cross += 1
    else:
        draw += 1

print("circle", circle, "cross", cross, "draw", draw)

実行結果(実行結果はランダムなので下記とは異なる場合があります)

circle 72 cross 18 draw 10

dict を使った集計

上記のプログラムでは、3 つの変数 と、if 文 を使って 集計 を行いましたが、dict を使うことで 集計 をより 簡潔に記述 することができます。

勝敗結果 を表す play メソッドの 返り値データ型文字列型 で、dictキー として 文字列型 のデータを 利用 することが できる ので、下記のプログラムのように、play メソッドの 返り値dict のキー として 利用 し、その キーの値 を使って 回数を数えます

  • 1 ~ 5 行目〇 の勝利× の勝利引き分け の回数を 数える ための、Marubatsu.CIRCLEMarubatsu.CROSSMarubatsu.EMPTYキー として持ち、それぞれの キーの値0 である dictcount という 変数 に __代入__する
  • 6 行目for 文100 回 の __繰り返し__を行う
  • 7 行目ai1ai2対戦 を行い、結果winner代入 する
  • 8 行目countwinnerキーの値1 を足して勝敗結果 に応じた 回数を数える
  • 10、11 行目count のそれぞれの キーその値 を、items メソッドと for 文による 繰り返し処理 を使って 表示 することで、集計結果を表示 する

実行結果から、ai1ai2 で 100 回対戦した結果は、ai1 の 75 勝 21 負 4 分 であり、先程の結果と 大きく変わらない ことから正しく集計されていることが確認できます。

 1  count = {
 2     Marubatsu.CIRCLE: 0,
 3     Marubatsu.CROSS: 0,
 4     Marubatsu.DRAW: 0,
 5  }
 6  for _ in range(100):
 7      winner = mb.play(ai=[ai1, ai2], verbose=False)
 8      count[winner] += 1
 9
10  for key, value in count.items():
11     print(key, value)
行番号のないプログラム
count = {
    Marubatsu.CIRCLE: 0,
    Marubatsu.CROSS: 0,
    Marubatsu.DRAW: 0,
}
for _ in range(100):
    winner = mb.play(ai=[ai1, ai2], verbose=False)
    count[winner] += 1

for key, value in count.items():
    print(key, value)

実行結果(実行結果はランダムなので下記とは異なる場合があります)

o 75
x 21
draw 4

defaultdict を使った集計

先程のプログラムでは、1 ~ 5 行目で 0 が代入 された 3 つのキー を持つ dictcount に代入しましたが、その 記述が面倒 だと思った人はいませんか?

先程のプログラムを、下記のように、count空の dict代入 するように 記述できれば プログラムが 簡潔 になりますが、下記のプログラムを実行すると エラーが発生 します。

count = {}
for _ in range(100):
    winner = mb.play(ai=[ai1, ai2], verbose=False)
    count[winner] += 1

for key, value in count.items():
    print(key, value)

実行結果

---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[9], line 4
      2 for _ in range(100):
      3     winner = mb.play(ai=[ai1, ai2], verbose=False)
----> 4     count[winner] += 1
      6 for key, value in count.items():
      7     print(key, value)

KeyError: 'o'

上記のような エラーが発生 する 理由 は、下記のプログラムのように、値が一度も代入されていない dictキーを参照 しようとすると KeyError という エラーが発生 するからです。

count = {}
print(count[Marubatsu.CIRCLE])

実行結果

---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[10], line 2
      1 count = {}
----> 2 print(count[Marubatsu.CIRCLE])

KeyError: 'o'

下記のプログラムの 4 行目の count[winner] += 1 という式は、count[winner] = count[winner] + 1 という処理を行う式です。1 行目の処理によって、count空の dict代入された状態この式を実行 しようとすると、代入文の = の右の count[winner] + 1計算する際 に、値が一度も代入されていない dict の キーの値を参照 する必要があるため、このプログラムを実行すると KeyError という エラーが発生 します。

count = {}
for _ in range(100):
    winner = mb.play(ai=[ai1, ai2], verbose=False)
    count[winner] += 1

collection モジュールと defaultdict

この問題は、Python の collections という モジュール の中で定義された、defaultdict という クラス を利用することで 解決 することができます。

Python では、list、tuple、dict などの、複数のデータを扱う ことができる データ型 の事を コンテナデータ型 と呼びます。collections は、Python が 標準的に用意 する、list、tuple、dict、set 以外特殊なコンテナデータ型定義 された モジュール です。

以前の記事 で、複合データ型 という用語を下記のように定義しました。

Python の 任意のデータ型 を、複数組み合わせて データを表現するデータ型

この定義と コンテナデータ型似ているように見える かもしれませんが、微妙に異なり ます。その理由は、文字のみ しか 格納 できない 文字列型 のデータが、コンテナ型に分類 されるからです2。実際に、collections モジュールの中に、文字列に関する UserString という コンテナデータ型 のクラスが 存在します。このことが、以前の記事で、わざわざ 複合データ型 という、独自の用語定義した理由 です。

collections モジュールの詳細については、下記のリンク先を参照して下さい。

defaultdict の使い方

defaultdict は、「一度も値が代入されていないキーを参照 しようとした場合に、エラーとせず に、特定の値 をその キーの値に代入 する」以外 では、dict と同じ性質 を持ちます3。本記事では、その特定の値 の事を、defaultdict の 規定値 と表記することにします。

defaultdictクラス で、以下のように記述することで インスタンスを作成 します。作成された の defaultdict の インスタンス には、空の dict と同様に、キー存在しません

defaultdict(default_factory)

実引数 default_factory には、defaultdict の 規定値作成するため呼び出す関数 または クラス記述 します。例えば、既定値 として、整数型0代入 したい場合は、下記のプログラムのように default_factoryint を記述 します。

int記述 すると 既定値0 になる理由 については 後述 します。また、実引数に、既定値である 0直接記述しない理由 についても 後述 します。

  • 1 行目collections モジュールから defaultdict をインポート する
  • 3 行目defaultdict実引数int を記述することで、一度も値が代入されていないキー参照 しようとした場合に、既定値 として 0 がその キーの値代入 される

4 行目 を実行した時点で count には Marubatsu.CIRCLE という キーは存在しない ので、count[Marubatsu.CIRCLE]0 が代入 され、その結果 0 が表示 されます。

from collections import defaultdict

count = defaultdict(int)
print(count[Marubatsu.CIRCLE])

実行結果

0

先程のプログラムは、下記のプログラムのように defaultdict簡潔に記述 できます。

count = defaultdict(int)
for _ in range(100):
    winner = mb.play(ai=[ai1, ai2], verbose=False)
    count[winner] += 1

for key, value in count.items():
    print(key, value)    
修正箇所
-count = {
-    Marubatsu.CIRCLE: 0,
-    Marubatsu.CROSS: 0,
-    Marubatsu.DRAW: 0,
-}
+count = defaultdict(int)
for _ in range(100):
    winner = mb.play(ai=[ai1, ai2], verbose=False)
    count[winner] += 1

for key, value in count.items():
    print(key, value)

実行結果(実行結果はランダムなので下記とは異なる場合があります)

x 20
o 78
draw 2

defaultdict に関する補足

for 文 に記述された count.items() は、defaultdict登録された順キーその値取り出す ので、例えば 最初mb.play()返り値Marubatsu.CROSS だった場合は、上記の実行結果のように、最初x の勝利の回数表示 される 場合 があります。

また、一度も引き分けにならなかった 場合は、Marubatsu.DRAW という キーcount登録されない ので、下記のように draw実行結果表示されない場合 もあります。

o 80
x 20

ただし、キーの 登録の順番処理に影響を与える のは、items メソッドなどで キーその値順番に取り出す場合だけ なので、それ以外の場合は 気にする必要はない でしょう。

defaultdict が行う処理

defaultdict(default_factory)default_factoryint を記述すると、既定値 として 0 が設定 される 理由 について説明します。defaultdict に対して、一度も値が代入されていないキー参照 しようとした場合は、下記の手順処理 が行われます。

  1. default_factory呼び出される。具体的には default_factory() が実行 される
  2. default_factory()返り値 が、その キーの値 として 代入 される

default_factoryデータの種類 によって、規定値 は以下のようになります。

  • クラス を記述した場合は、その クラスのインスタンス既定値 となる
  • 関数 を記述した場合は、その 関数返り値規定値 となる

default_factory は、defaultdict__init__ メソッドの 仮引数の名前 で、由来既定(default)値作成 する 工場 (factory)のような 役割 を持つことです。

default_factoryint記述 した場合、int() という 処理が実行 されます。int組み込み型のクラス で、int() のように、実引数何も記述せず呼び出した場合 は、下記のプログラムのように、整数型0 を表す インスタンスが作成 されます。

これが、defaultdict(int) によって、既定値0defaultdict作成される理由 です。

print(int())

defaultdict では、既定値 として 空の list指定 することが 良くあります。その場合は、下記のプログラムのように、default_factorylist を記述 します。これは、list組み込み型のクラス で、list() のように、実引数何も記述せず呼び出した場合 は、空の list を表す インスタンスが作成 されるからです。

count = defaultdict(list)
print(count[Marubatsu.CIRCLE])

実行結果

[]

defaultdict規定値整数型の 0指定 したい場合は defaultdict(int) を、空の list指定 したい場合は defaultdict(list) と記述すれば良い。

任意の値を規定値とする defaultdict

今回の記事では利用しませんが、0 や、空の list 以外の 任意の値規定値 とする defaultdict作成 することが できます。その場合は、規定値 として 設定したい値返す仮引数存在しない関数定義 し、その関数default_factory記述 します。

例えば、下記は、整数型1規定値 とする defaultdict作成 するプログラムです。5 行目の実行結果から、既定値 として 1 が代入 されることが 確認 できます。

  • 1、2 行目:常に整数型の 1 を返す create_one という名前の関数を定義する
  • 4 行目create_onedefaultdict実引数記述 して呼び出すことで、規定値1defaultdict作成 して、count に代入 する
def create_one():
    return 1

count = defaultdict(create_one)
print(count[Marubatsu.CIRCLE])

実行結果

1

defaultdict の実引数に、規定値を直接記述しない理由

既定値1 とする defaultdict作成 する際に、下記のプログラムのように、defaultdict の 実引数既定値直接記述したい と思う人がいるかもしれませんが、下記のプログラムを実行すると エラーが発生 します。その 理由 は、defaultdictdefault_factory に対応する 実引数 は、関数クラス などの、呼び出すことができる オブジェクト でなければならない からで、整数型11() のように記述して 呼び出すことはできない からです。

count = defaultdict(1)
print(count[Marubatsu.count])

実行結果

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[16], line 1
----> 1 count = defaultdict(1)
      2 print(count[Marubatsu.count])

TypeError: first argument must be callable or None

上記のエラーメッセージは、以下のような意味を持ちます。

  • TypeError
    データ型(type)に関するエラー
  • first argument must be callable or None
    (defaultdict の)最初(first)の実引数(argument)は、(関数やクラスなどの)呼び出し可能(callable)なオブジェクトか、None でなければならない(must be)

defaultdict実引数 に、規定値直接記述しない 理由は、そのようにすると、作成した defaultdict の 規定値 が、すべてのキーで常に同じ値 になってしまうからです。例えば、既定値直接記述 するという 方法 では、defaultdict の 規定値 として、値が代入されていないキー の値を 参照した時刻設定 することは できません

一方、defaultdict実引数 に、既定値作成する ための 関数やクラス記述 する場合は、下記のプログラムのように、現在時刻返す関数定義 し、その関数 を defaultdict の 実引数に記述 することで、現在時刻 を表す 文字列既定値 として持つ defaultdict を作成 することが できます。なお、現在時刻 を表す 文字列 は、time という モジュール で定義された ctime という 関数 を呼び出すことで得ることができます。

time モジュールと ctime の詳細については、下記のリンク先を参照して下さい。

  • 1 行目time モジュールから、ctimeインポート する
  • 3、4 行目現在の時刻 を表す 文字列返すnow という関数を 定義 する
  • 6 行目now実引数 に記述して、defaultdict を作成 し、count代入 する

下記のプログラムを実行すると、count規定値 が、値が代入されていないキー の値を 参照 した時の 時刻 を表す 文字列 になるので、7 行目を実行すると、7 行目実行した時時刻を表す文字列表示 されます。

1  from time import ctime
2
3  def now():
4      return ctime()
5
6  count = defaultdict(now)
7  print(count(Marubatsu.CIRCLE))
行番号のないプログラム
from time import ctime

def now():
    return ctime()

count = defaultdict(now)
print(count[Marubatsu.CIRCLE])

実行結果

Fri Dec 29 11:39:50 2023

また、上記のプログラムを 実行した少し後 で、下記のプログラムを実行すると、異なるキー に対して、異なる規定値代入 されることが 確認 できます。

ctime秒単位時刻を表す するので、上記のプログラムと下記のプログラムを 間を空けず連続して実行 すると、同じ文字列が表示 されます。

print(count[Marubatsu.CROSS])

実行結果

Fri Dec 29 11:40:01 2023

defaultdict実引数既定値直接記述しない理由 は、実引数関数やクラス記述したほう が、状況に応じて値が変化する ような、柔軟な既定値設定 することが できるようになる ためです。

上記のプログラムでは、ctime()返り値 として 返すnow という 関数を定義 しましたが、ctime() は、ctime呼び出した時返り値 なので、下記のプログラムのように、ctime直接 defaultdict の 実引数に記述 することが できます

count = defaultdict(ctime)
print(count[Marubatsu.CIRCLE])

実行結果

Fri Dec 29 11:40:09 2023

ラムダ式

今回の記事では defaultdict規定値0 を設定 するため、defaultdict(int) とだけ記述すれば良いので、下記の説明の ラムダ式 を利用することはありませんが、ラムダ式実際に良く使われる ので説明することにします。

今回の記事 のプログラムでは、ラムダ式を使わない ので、分かりづらい と思った方は、そういうものがある ということだけを 覚えて おいて、読み飛ばしてかまいません

ラムダ式うまく使えば便利 ですが、むやみに使うとプログラムが わかりづらくなる という 欠点 があります。そのため、慣れないうち は、ラムダ式 を使うと 便利な例 として学んだ場合で のみで使う ことを 強くお勧めします

0 や 空の list 以外既定値 を持つ defaultdict利用 する際に、上記のような 規定値を返す関数定義 するのは 面倒 なので、defaultdictdefault_factory は、より 簡潔に記述 できる ラムダ(lambda)式 を使って 記述する のが 一般的 です。

ラムダ式 は、無名関数 という、名前の無い関数簡潔に定義 することができる で、下記のように記述します。仮引数存在しない場合 は、lambda : 式 のように記述します。

lambda 仮引数: 

上記のラムダ式 は、下記関数の定義同じ意味 を持ちます。なお、先ほど説明したように、ラムダ式名前の無い関数定義 するので、それに あわせて 下記のプログラムでは 関数の名前記述していません が、def による 関数の定義名前を記述しないエラーが発生する ので、実際に 下記のプログラムを記述して 実行 すると エラーが発生 します。
下記のプログラムは、ラムダ式の説明 を行うための 架空の記述 だと思ってください。

def (仮引数):
    return 

上記のような、def名前の無い関数定義 することが できない理由 は、名前の無い関数定義しても名前が存在しないため、その 関数を利用することができない からです。

ラムダ式の詳細については下記のリンク先を参照して下さい。

ラムダ式の利用方法

ラムダ式関数の定義 なので、ラムダ式を実行 しても def による 関数の定義同様 に、関数のブロックの処理行われません。また、ラムダ式 で定義した 関数 には 名前がない ので、ラムダ式記述しただけ では、その 関数利用 することは できません

そのため、ラムダ式は、以下の いずれかの方法利用 します。

  • 関数呼び出し実引数記述 する。この場合は、ラムダ式定義された関数 が、関数の 仮引数に代入 されるので、仮引数 を使って 利用 することができる
  • 変数 やインスタンスの 属性代入 し、代入した 変数属性 を使って 利用 する

後で具体的に説明しますが、defaultdict で利用する ラムダ式 は、前者 の利用方法です。

下記のプログラムは、後者の利用方法具体例 ですが、実際には、このような、プログラムの 複数の場所関数の名前直接記述 して 何度も呼び出す ような場合は ラムダ式 は一般的には 使われません。その 理由の一つ は、ラムダ式 で記述した関数は、def で定義した場合と 比べて定義した場所 と、見た目わかりづらくなる ためです。

後者有効な利用方法 については、必要になった時点で説明します。

add = lambda x, y: x + y
print(add(1, 2))
print(add(2, 3))

実行結果

3
5

ai2 は、ブロック内 で行う 処理 を、1 つの return 文記述 することが できる ので、ラムダ式 を使って、下記のプログラムのように 定義 することができます。その下の元のプログラムと見比べてみて下さい。

from random import choice

ai2 = lambda mb: choice(mb.calc_legal_moves())

元のプログラム

def ai2(mb):
    legal_moves = mb.calc_legal_moves()
    return choice(legal_moves)

下記のプログラムを実行することで、ai1 と上記の ai2 で対戦を行うことができることが確認できます。なお、実行結果は長くなるので省略します。

mb.play(ai=[ai1, ai2])

1 つの return 文記述できない ような AIラムダ式 では 記述できません。また、上記のラムダ式は わかりづらい ので、ai2元のプログラムを採用 します。

ラムダ式の制限事項

ラムダ式 は、関数を簡潔に定義できますが、以下のような 制限があります

  • 関数 が行う 処理 として、複数の文記述 することは できない。例えば、下記のような、複数の文 から 構成される関数ラムダ式記述 することは できない
def add(x, y):
    print(x, y)
    return x + y

ラムダ式は、1 つの式計算結果を返す ような 関数しか定義できない

  • 仮引数や返り値に対する 型アノテーションを記述 することが できない。例えば、下記のようなプログラムを記述するとエラーが発生する
lambda a:int : a -> int

1 つ目の 制限事項 から、ほとんどの関数ラムダ式記述 することは できません が、defaultdict など、一部の状況 では ラムダ式 を使うことでプログラムを 簡潔に記述できる ので、そのような 特別な場合ラムダ式使われます

ラムダ式を利用した defaultdict

ラムダ式利用 することで、先程の、規定値整数型の 1 となる defaultdict を作成 するプログラムを、下記のプログラムのように 簡潔に記述 できます。修正箇所 を見て、ラムダ式を使わない プログラムと 見比べて みて下さい。

count = defaultdict(lambda : 1)
print(count[Marubatsu.CIRCLE])

実行結果

1
修正箇所
-def create_one():
-   return 1
-count = defaultdict(create_one)
+count = defaultdict(lambda : 1)
print(count[Marubatsu.CIRCLE])

既定値0 の defaultdict は defaultdict(int) のように 簡潔に記述できる ので、わざわざ defaultdict(lambda : 0) のように 記述 する 必要はありません

ラムダ式 は、defaultdict 以外用途 でも 使われます。ラムダ式の他の具体的な利用方法については、必要になった時点で紹介します。

AI どうしの対戦を行う関数の定義

AI どうしの対戦 を行う際に、先程のようなプログラムを 毎回記述 するのは 大変 なので、AI どうしの対戦 を行う 関数を定義 する事にします。その際に、対戦時間がかかる可能性 があることを 考慮 して、対戦回数指定できる ようにします。

処理AI どうし複数回対戦 を行い、通算成績を表示 する

名前:AI どうしの 対戦(match)を行うので、ai_match という名前にする

入力

  • ai という 仮引数AI の処理 を行う 関数 を、play メソッドの仮引数 ai同じデータ構造代入 する
  • match_num という 仮引数対戦行う回数代入 する。また、10000デフォルト値 とする デフォルト引数 とする

出力:なし(通算成績文字で表示 する)

下記は、ai_match の定義 です。基本的 には、先程のプログラム で行っていた 処理ai_match の関数の ブロックに記述 していますが、下記の点異なります

  • 1 行目名前ai_match仮引数aimatch_num とし、match_numデフォルト値10000デフォルト引数 とする 関数を定義 する
  • 4 行目for 文 で、match_num繰り返し処理 を行う
  • 7、8 行目:先ほど説明したように、defaultdict を使った場合に for 文count.items() を利用して キーその値表示 すると、表示の順番 が 〇 の勝利、× の勝利、引き分けの 順にならない場合がある ので、必ずその順番表示する ように 修正 した
1  def ai_match(ai, match_num=10000):
2      mb = Marubatsu()
3      count = defaultdict(int)
4      for _ in range(match_num):
5          count[mb.play(ai, verbose=False)] += 1
6
7      print("o", count[Marubatsu.CIRCLE], "x", count[Marubatsu.CROSS],
8            "draw", count[Marubatsu.DRAW])
行番号のないプログラム
def ai_match(ai, match_num=10000):
    mb = Marubatsu()
    count = defaultdict(int)
    for _ in range(match_num):
        count[mb.play(ai, verbose=False)] += 1

    print("o", count[Marubatsu.CIRCLE], "x", count[Marubatsu.CROSS],
          "draw", count[Marubatsu.DRAW])
修正箇所

関数のブロックのインデントは修正箇所に含めません。

+def ai_match(ai, match_num=10000):
    mb = Marubatsu()
    count = defaultdict(int)
-   for _ in range(100):
+   for _ in range(match_num):
        count[mb.play(ai, verbose=False)] += 1

-for key, value in count.items():
-   print(key, value)   
+   print("o", count[Marubatsu.CIRCLE], "x", count[Marubatsu.CROSS],
          "draw", count[Marubatsu.DRAW])

下記のプログラムを実行することで、ai1 VS ai2対戦 を 1 行目で 1000 回、2 行目で 10000 回 行い、通算成績を表示 することができることが 確認 できます。

ai_match(ai=[ai1, ai2], match_num=1000)
ai_match(ai=[ai1, ai2])

実行結果(実行結果はランダムなので下記とは異なる場合があります)

o 759 x 207 draw 34
o 7834 x 1733 draw 433

ai1ai2 のすべての組み合わせの対戦とその評価

対戦を行うための関数が定義できたので、下記のプログラムで ai1ai2すべての組み合わせ対戦を行いそれぞれ通算成績を評価 することにします。

ai_match(ai=[ai1, ai1]) # ai1 VS ai1
ai_match(ai=[ai1, ai2]) # ai1 VS ai2
ai_match(ai=[ai2, ai1]) # ai2 VS ai1
ai_match(ai=[ai2, ai2]) # ai2 VS ai2

実行結果(実行結果はランダムなので下記とは異なる場合があります)

o 10000 x 0 draw 0
o 7727 x 1816 draw 457
o 5208 x 4392 draw 400
o 5851 x 2878 draw 1271

実行結果を ざっと眺める ことで、下記の事がわかります。

  • ai1 VS ai1 では、ai1アルゴリズム には ランダム性はない ので、10000 回の すべての対戦〇 が勝利 する
  • ai1 VS ai2ai1 VS ai2通算成績合計 すると、ai1 から見て $7727 + 4392 =$ 12119 勝、$1816 + 5208 =$ 7024 敗、$457 + 400 =$ 857 分 であり、ai1 のほうが ai2 より強く見える
  • ai2 VS ai2 から、ai2 どうし で対戦した場合は、〇 のほうが × の 約 2 倍勝利 する

ランダムな着手 を行う ai2 どうし対戦結果 から、〇×ゲームは 〇 のほうが かなり 有利 なゲームであると 推測 することができます。

ai1ai2 の強さ考察

上記の結果から、一見する と、ai1 のほうが ai2 より強い ように 思えるかも しれませんが、そのような 結論を出す のは 早計 です。それぞれの AI の 実際の強さとは別にai1 のアルゴリズムai2 のアルゴリズム に対して 相性が良い可能性 があるからです。

そこで、上記の 実行結果考慮せず に、ai1ai2アルゴリズムだけ から、どちらが強い AI であるかを 考察 してみることにします。

〇×ゲームのことを あまりよく知らない人間 と、ai1 が対戦する場合 のことを考えてみて下さい。ai1左上のマスから順番空いているマスを探して着手を行う だけなので、人間 であれば、初心者であっても すぐに ai1 に負けなくなる はずです。

一方、ai2ランダムな着手 を行うので、偶然 ai2最善手のみの着手 を行う 可能性 があり、そのような場合は 〇×ゲームの 初心者が負ける可能性 はあるでしょう。

上記から、人間相手対戦 を考えると、ai1 より ai2 のほうが強い考察 できます。

しかし、実際に ai1ai2 で対戦 した場合は、ai1大きく勝ち越し ます。そのような結果になる理由について少し考えてみて下さい。

ai1ai2 に勝ち越す理由

ai1〇 を担当 する場合は、その アルゴリズムからai2上の行着手を一度も行わなければ3 回の着手ai1上の行〇 を順番に着手 して 勝利 します。ai2ランダムな着手 を行うので、2 手連続上の行着手を行わない確率 は計算しなくても かなりありそう です。従って、ai1〇 を担当 する場合に、ai2 に勝つ確率 はかなりあると 推測 できます。ai1× を担当 する場合は ai23 手連続上の行着手を行わなければ、同様の理由で ai1 が勝利 しますが、その 確率 もそれほど 低くはなさそう です。従って、ai1ai2 に対して 相性が良い アルゴリズムであることが 推測 できます。

ただし、上記は 推測に過ぎない ので、実際にそうであるかどうかは、確率を計算 するなどの方法で 検証する必要 があります。そこで、ai1〇 を担当 する場合に、ai22 手連続上の行着手を行わない 確率の求めることにします。

ai1〇 を担当 する場合に、ai22 手連続上の行着手を行わない 確率

ai21、2 手目一行目に着手しない ことによって、ai1 が勝利 する 確率 は、下記の手順求める ことができます。

  • ai2初手空いているマスの数8 マス2 手目 では 6 マス なので、1, 2 手目ai2着手 を行うことができる マスの組み合わせ は $8 * 6 =$ 48 通り
  • ai1初手必ず (0, 0) のマスに 着手する ので、ai2初手1 行目以外空いているマス6 マス である
  • ai2初手一番上の行着手しない 場合に ai22 手目必ず (1, 0) のマスに 着手 するので、ai22 手目1 行目以外空いているマス5 マス である
  • 従って、ai2初手と 2 手目1 行目以外着手 を行うことができる マスの組み合わせ は $6 * 5 = $ 30 通り
  • 従って、ai2初手と 2 手目1 行目以外着手 を行って ai1 が勝利 する 確率 は、$30 / 48 = $ 62.5 % である

ai1上記以外 でも 勝利する場合がある ので、実際の勝率62.5 % 以上 になります。

下記の 考察 は、数学的 には 厳密でない 考察ですが、ai1 という AI は特に 重要な AI では ありません ので、ai1ai2 に対して相性が良いということを、厳密に証明 する 必要はない と思います。従って、下記のような雑な考察で 十分 でしょう。

この結果から、〇 を ai1 が担当 した場合は、先程の、ai1ai2 に対して 相性が良い アルゴリズムであるという 推測正しい ことが 裏付けられ ました。本当は他の場合ai1 が勝利する確率や、ai1 が × を担当 した場合の勝利の確率などを 計算したほうが良い のですが、先ほどの、人間相手 であれば ai2 のほうが ai1 よりも 強い という 考察 と、ai1ai2 の対戦 では ai1 のほうが ai2 よりも 強い という 結果 から、ai1アルゴリズム は、ai2アルゴリズム に対して 相性が良い結論付けても問題はない でしょう。

本記事では計算しませんが、興味と余裕がある方は、他の場合ai1勝利する確率 や、ai1 が × を担当 した場合の 勝利の確率 などを計算してみて下さい。

今回の 記事の最初 で、じゃんけんの AI の例を使って「基準 となる AI が 偏った性質を持つ 場合に、正しい評価行えない」ことを説明しましたが、〇×ゲームの AI の場合でも、ai1ai2 の例で そのことが説明 できます。ai1基準となる AIしなかった のは、ランダム性がないだけではなく、偏った性質 を持つ AI だからです。

今回の記事のまとめ

今回の記事では、以下のような、AI の強さの評価 を行う 方法 を決め、その評価を行うための ai_match という 関数を定義 しました。ただし、この関数は本格的な AI の評価を行うためには いくつかの問題 があるので、次回の記事では ai_match を改良 することにします。

  • 対戦の回数
    • AI どうしの 対戦の回数 は、原則 として 一万回 行う
    • 一万回の対戦に 時間がかかりすぎる場合 は、1 分で行える回数 の対戦を行う
  • 対戦の方法
    新しい AI を作成 した場合は、下記の AI と対戦を行う
    • ランダムな着手を行う ai2
    • それまでに作成 した AI の中で 最も強い AI と思われるうちの いくつか

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

以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。

以下のリンクは、今回の記事で更新した marubatsu.py です。

以下のリンクは、今回の記事で更新した ai.py です。なお、ai_match はこちらに保存しました。

次回の記事

  1. 前回の記事で説明したように、ai1 VS ai2 では、先に記述した ai1 が 〇を担当します

  2. Python の公式ドキュメントの 文字列型の説明 の所には、文字列型がコンテナ型であると明記されていませんが、他の文章でコンテナの説明に文字列型を含む説明を行っている例が実際に存在する点と、collections の公式ドキュメントの中に、UserString が含まれていることから、本記事では文字列型のデータをコンテナデータ型に含めることにします

  3. defaultdict という名前は、dict が持つ性質に加え、値が代入されていないキーの規定(default)値を設定することができるという性質を持つことが由来です

2
1
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
2
1