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?

More than 1 year has passed since last update.

Pythonで〇×ゲームのAIを一から作成する その47 評価値を利用するアルゴリズム

0
Last updated at Posted at 2024-01-21

目次と前回の記事

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

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

これまでに作成した AI

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

関数名 アルゴリズム
ai1 左上から順 に 空いているマス を探し、最初に見つかったマス に 着手 する
ai2 ランダム なマスに 着手 する
ai3 真ん中 のマスに 優先的 に 着手 する
既に 埋まっていた場合 は ランダム なマスに 着手 する
ai4 真ん中、隅 のマスの 順 で 優先的 に 着手 する
既に 埋まっていた場合 は ランダム なマスに 着手 する
ai5 勝てる場合 に 勝つ
そうでない場合は ランダム なマスに 着手 する
ai6 勝てる場合 に 勝つ
そうでない場合は 相手の勝利 を 阻止 する
そうでない場合は ランダム なマスに 着手 する
ai7 真ん中 のマスに 優先的 に 着手 する
そうでない場合は 勝てる場合 に 勝つ
そうでない場合は 相手の勝利 を 阻止 する
そうでない場合は ランダム なマスに 着手 する

基準となる ai2 との 対戦結果(単位は %)は以下の通りです。太字 は ai2 VS ai2 よりも 成績が良い 数値を表します。欠陥 の列は、アルゴリズム に 欠陥 があるため、ai2 との 対戦成績 が 良くても強い とは 限らない ことを表します。欠陥の詳細については、関数名のリンク先の説明を見て下さい。

関数名 o 勝 o 負 o 分 x 勝 x 負 x 分 勝 負 分 欠陥
ai1 78.1 17.5 4.4 44.7 51.6 3.8 61.4 34.5 4.1 あり
ai2 58.7 28.8 12.6 29.1 58.6 12.3 43.9 43.7 12.5
ai3 69.3 19.2 11.5 38.9 47.6 13.5 54.1 33.4 12.5
ai4 83.0 9.5 7.4 57.2 33.0 9.7 70.1 21.3 8.6 あり
ai5 81.2 12.3 6.5 51.8 39.8 8.4 66.5 26.0 7.4
ai6 88.9 2.2 8.9 70.3 6.2 23.5 79.6 4.2 16.2
ai7 95.8 0.2 4.0 82.3 2.4 15.3 89.0 1.3 9.7

これまでのアルゴリズムの欠点

これまで作成 してきた AI は、ルール 7 のように、ルールに 複数の条件 が存在する場合は、「それぞれの条件 を 順番に判定 し、最初 にみつかった 条件を満たす着手 を 選択 する」という アルゴリズム で 実装 してきましたが、この アルゴリズム には 欠点 があります。

また、詳細は次回以降の記事で説明しますが、その 欠点 があるため、これまでに作成した最強の AI である ai7 より強い AI を、このアルゴリズム で 作成 することは 困難 です。

そこで、今回の記事では、このアルゴリズム の 欠点 を 説明 し、その 欠点を解消 できる、評価値を利用する という アルゴリズム を 説明 します。また、これまでに実装 してきた AI を、評価値を利用 する アルゴリズム で 実装する方法 について 説明 します。

条件を判定する順番に関する問題点

これまで の アルゴリズム では、ルール の中に 複数 の 条件がある 場合は、条件 の 優先順位 に従って、判定の順番 を 正しく記述 する必要があります。例えば、前回の記事で説明したように、ルール 6 のアルゴリズムの 手順 を 入れ替える と、間違った処理 が行われます。

この点に関しては、この後で説明する、評価値を利用 する アルゴリズム の場合でも、評価値 をルールに従って 正しく計算 する 必要がある ので、大きく変わるわけではありません。

問題は、何らかの理由 で 条件 の 優先順位 を 変える場合 で、判定 を行うプログラムの 記述の順番 を正しく 入れ替える 必要があるため、プログラムの 修正が大変 です。

次回の記事で説明しますが、評価値を利用 する アルゴリズム では、条件の 優先順位 を 変える 際に、評価値 の 計算式 を 修正 するという手法をとるため、処理の 記述の順番 を 入れ替える 必要は ありません。そのため、プログラムの 修正 を 比較的簡単 に行えます。

条件を満たす着手が複数ある場合の問題点

ルール 5 の、「勝てる場合 に 勝つ」という条件は、下記の局面の 2 つ の青いマスのように、複数の合法手 がその 条件を満たす 場合があります。

上記の局面に対し、ai5 は、最初に見つかった 1 のマスを 必ず選択 してしまいます。もちろん、1 のマスを 選択 すれば 〇 が勝利 するので、強い AI を作成 するという 意味 では全く 問題ありません が、人間にとって は、1 と 2 を ランダムに選択 する AI のほうが 面白味が高い のではないでしょうか。しかし、最初に見つかった合法手 を 選択 するという アルゴリズム では、そのような ランダム性 のある 着手 を行うことは できません。

結果が True、False 以外の条件を考慮したい場合の問題点

これまで の ルール の 条件 は、いずれも「真ん中のマスに着手したかどうか」のように、結果 が True か、False の いずれか になるものばかりでした。

それに対し、例えば 参考書を買う 時に、「ページ数 が 最も多い 本を 選ぶ」のような、数値の大きさ を 比較 するような 条件 の ルール は、すべての参考書 を 調べなければ どれが最も小さいかを 判定 することは できない ので、これまでの、「最初に見つかったもの を 選択 する」という アルゴリズム は 利用できません。

複数の条件を組み合わせて考慮したい場合の問題点

これまで の 〇×ゲームの ルール では、この 問題は起きない ので、別の例 で 説明 します。

お店で ヨーグルトを買う 時に、「値段」、「容量」、「賞味期限」の 3 つ を 判断基準 にして どの商品を買うか を 選択 する場合の事を考えてみて下さい。

例えば、それぞれについて、下記 のような 判断基準 を 設定 することにします。

  1. 値段 が 200 円以下
  2. 容量 が 200 g 以上
  3. 賞味期限 が 3 日後以降

「それぞれの条件 を 順番に判定 し、最初 にみつかった 条件を満たす商品 を 選択 する」場合は、上記の 判断基準 の 優先順位 を 設定 し、その 順番で判定 を行うことになります。

例えば、下記の表の 3 種類 の 商品 に対して、上記の 1、2、3 の 優先順位 で 商品を選択 する場合、最初 に 値段が 200 円以下 であることが 判明 した 商品 B が 選択 されます。

値段 容量 賞味期限 条件 1 条件 2 条件 3 条件を満たす数
商品 A 230 300 5 〇 〇 2
商品 B 180 150 1 〇 1
商品 C 160 120 2 〇 1

しかし、この方法 では、商品 B より後 の商品は チェックされない ので、商品 B より 安い、商品 C が 選択されない という 問題 があります。

他のルール として、上記の 判断基準 を 最も多く満たす 商品を 選択する というルールが考えられます。例えば、上記の場合、商品 C が 最も多く の 判断基準を満たす ので 商品 C を 選択 します。しかし、最も多く の 判断基準 を満たす商品を 見つけるため には、すべての商品 を 調べる必要 があるので、「それぞれの条件 を 順番に判定 し、最初 にみつかった 条件を満たす商品 を 選択 する」方法で このルール を 作成 することは できません。

評価値を利用するアルゴリズム

上記の問題 を 解決 する方法に、下記の 評価値を利用 する アルゴリズム があります。手順 2 から、このアルゴリズムで、ランダム性 のある 選択を行う ことが できます。

以下の手順で、複数のもの の中 から 、一つを選択 する。

  1. それぞれ の もの に対して 決められた計算 を行うことで、点数 をつける。その 点数 の事を 評価値 と 呼ぶ
  2. 最も高い評価値 がつけられた ものを選択 する。最高点 の 評価値 がつけられたものが 複数ある 場合は、その中から ランダムに 一つを 選択 する

ランダム性 が 重要でない 場合は、手順 2 で、最も高い評価値 がつけられたものが 複数ある場合 は、最初にみつかったもの を 選択 するアルゴリズムもあります。

最も優秀 なものを 評価値を利用 する アルゴリズム で 選択 することは、現実の世界 でも、下記の表 のように 良く行われています。

評価値
テスト 点数
マラソン ゴールするまでに要した時間
野球などのスポーツ 得点

先程の ページ数 が 最も多い 参考書を選択する例では、参考書の ページ数 が 評価値 です。

先程の ヨーグルト を 選択 する例では、条件 を 満たす数 が 評価値 で、下記の場合は 最も評価値が高い、商品 A が 選択 されることになります。

値段 容量 賞味期限 条件 1 条件 2 条件 3 条件を満たす数
商品 A 230 300 5 〇 〇 2
商品 B 180 150 1 〇 1
商品 C 160 120 2 〇 1

このように、評価値を利用 することで、数値の大きさ を 条件 とする 選択 や、複数の条件 を 組み合わせ て 考慮 する 選択 を行うことが できる ようになります。

また、評価値を利用 することで、先ほど説明した「条件を判定 する 順番 に関する 問題点」を 改善 することができます。具体例については次回の記事で説明します。

評価値を利用した 〇× ゲームの AI の記述

評価値を利用 した アルゴリズム による 〇× ゲーム の AI は、下記のプログラムのように、現在の局面 に対して それぞれの合法手 を 着手 した 局面の評価値 を 計算 し、その中で 最も評価値が高くなる合法手 から 選択 するという 処理を記述 します。なお、コメントの部分は、この後で具体的なプログラムを記述します。

現在の局面 に対して、それぞれの合法手 を 着手 する 処理 は、ルール 5 の AI で行う 処理と同じ なので、2 ~ 5 行目 の処理の 記述 は、ai5 のプログラムと 同様 です。

 1  def ai(mb_orig):
 2      legal_moves = mb_orig.calc_legal_moves()
 3      for move in legal_moves:
 4          mb = deepcopy(mb_orig)
 5          x, y = move
 6          mb.move(x, y)
 7          # mb の局面の評価値を計算する
 8          # 最も高い評価値が得られる合法手の一覧を計算する
 9
10      # 最も高い評価値が得られる合法手の一覧の中からランダムに着手を選択する

評価値を利用した ルール 2 の AI の実装

これまでに作成 してきた AI は、いずれも評価値を利用 した アルゴリズム で 実装 することが できます。最初に、下記の ルール 2 の AI を、評価値を利用 した アルゴリズム で 実装 する 方法 を紹介します。なお、ルール 1 より先に ルール 2 を実装するのは、ルール 2 のほうが 簡単に実装できる からです。ルール 1 の実装方法については次回の記事で紹介します。

  • ランダム なマスに 着手 する

評価値を利用 する アルゴリズム で、ルール 2 を 実装 する場合は、現在の局面 に対して 合法手 を 着手 した すべて の 局面 の 評価値 を 同じ値 にします。その 理由 は、評価値を利用 する アルゴリズム では、先程説明したように、「最高点 の 評価値 がつけられたものが 複数ある場合 は、その中 から ランダムに 一つを 選択 する」という処理を行うからです。

関数の名前について

評価値 の事を、英語 で score と 表記 するので、その 頭文字 をとって、評価値 を 利用 して ルール x の処理を行う 関数の名前 を aixs のように 命名 することにします。例えば、これから実装する ルール 2 の処理を行う 関数 は、ai2s という 名前 で 定義 します。

ai2s は、先程のプログラムの コメント を 順番に埋めていく という手順で 実装 します。

評価値の計算処理の記述

下記は、評価値 を 利用 した アルゴリズム で ルール 2 の処理を行う 関数 ai2s を 定義 するプログラムです。下記のプログラムは、先程の 評価値 を 利用 した プログラム の、7 行目 の 「mb の局面の評価値を計算する」 の コメントの処理 を 11 行目に 記述 しています。

すべての局面 に対して 同じ評価値 を 計算 すれば良いので、その 評価値 には どのような数値 を 設定 しても かまいません が、おそらく 0 が 最も分かりやすい のではないかと思いますので、本記事では 0 を設定 することにしました。また、計算した 評価値 を 代入 する 変数の名前 は score にしました。

なお、後で利用 するので 2 行目 で choice を インポート しています。

 1  from copy import deepcopy
 2  from random import choice
 3
 4  def ai2s(mb_orig):
 5      legal_moves = mb_orig.calc_legal_moves()
 6      for move in legal_moves:
 7          mb = deepcopy(mb_orig)
 8          x, y = move
 9          mb.move(x, y)
10          # mb の局面の評価値を計算する
11          score = 0
12          # 最も高い評価値が得られる合法手の一覧を計算する
13
14      # 最も高い評価値が得られる合法手の一覧の中からランダムに着手を選択する
行番号のないプログラム
from copy import deepcopy
from random import choice
 
def ai2s(mb_orig):
    legal_moves = mb_orig.calc_legal_moves()
    for move in legal_moves:
        mb = deepcopy(mb_orig)
        x, y = move
        mb.move(x, y)
        # mb の局面の評価値を計算する
        score = 0
        # 最も高い評価値が得られる合法手の一覧を計算する

    # 最も高い評価値が得られる合法手の一覧の中からランダムに着手を選択する
修正箇所
from copy import deepcopy
from random import choice

def ai2s(mb_orig):
    legal_moves = mb_orig.calc_legal_moves()
    for move in legal_moves:
        mb = deepcopy(mb_orig)
        x, y = move
        mb.move(x, y)
        # mb の局面の評価値を計算する
+       score = 0
        # 最も高い評価値が得られる合法手の一覧を計算する

    # 最も高い評価値が得られる合法手の一覧の中からランダムに着手を選択する

次に、上記のプログラムの 12 行目 のコメントの、「最も高い評価値 が得られる 合法手 の 一覧を計算 する」という 処理 を 記述 します。どのように記述すれば良いかについて少し考えてみて下さい。

なお、ルール 2 では、すべての局面 の 評価値 が 等しい ので、最も高い評価値 を 計算 する 必要はない のではないかと思う人がいるかもしれませんが、今後、局面 によって 評価値 が 異なるルール を 実装 する際に 必要になる ので、この機会に 実装する ことにします。

ai2s では、着手 に 関係なく、評価値 が 常に 0 になるので、合法手 の 着手 の 処理 を行う、上記のプログラムの 7 ~ 9 行目 を 記述 する 必要 は ありません。ただし、これは ルール 2 が 例外 なだけで、局面 の 状況に応じて、評価値を計算 する ルール の場合は 記述 する 必要 があるので、本記事 でも 記述する ことにします。

最も高い評価値の記録

「最も高い評価値 が得られる 合法手 の 一覧を計算 する」という 処理 を行うためには、最も高い評価値 が 何であるか を 記録 する 必要 があります。そこで、最も高い 評価値、すなわち 最高(best)の評価値を best_score という 名前 の 変数 に 記録 することにします。

次に、この best_score の 初期化処理 を 記述 する 必要 があります。その処理を どこに記述 し、どのような値 で 初期化 すればよいかについて少し考えてみて下さい。

記述 する 場所 は、合法手 を 着手 した 局面 の 評価値を計算 する より前 のどこかです。ただし、for 文 の ブロックの中 にその処理を 記述 すると、繰り返しのたび に best_score が 初期化されてしまう ので、for 文より前 に 記述 する 必要 があります。具体的には、legal_moves = mb_orig.calc_legal_moves() の 前後 の どちらか の行に 記述すれば良い でしょう。本記事 では その後の行 に 記述 することにします。

次に、best_score を どのような値 で 初期化 するかを 決めます。best_score は、それまでの局面 の 評価値 の中の 最高値 を表しますが、初期化 した 時点 では、まだ一度も 評価値 は 計算されていません。そこで、下記のプログラムの 3 行目 のように、とりあえず、データ が 存在しない ことを表す None で 初期化 することにします。

 1  def ai2s(mb_orig):
 2      legal_moves = mb_orig.calc_legal_moves()
 3      best_score = None
 4      for move in legal_moves:
 5          mb = deepcopy(mb_orig)
 6          x, y = move
 7          mb.move(x, y)
 8          # mb の局面の評価値を計算する
 9          score = 0
10          # 最も高い評価値が得られる合法手の一覧を計算する
11
12      # 最も高い評価値が得られる合法手の一覧の中からランダムに着手を選択する    
行番号のないプログラム
def ai2s(mb_orig):
    legal_moves = mb_orig.calc_legal_moves()
    best_score = None
    for move in legal_moves:
        mb = deepcopy(mb_orig)
        x, y = move
        mb.move(x, y)
        # mb の局面の評価値を計算する
        score = 0
        # 最も高い評価値が得られる合法手の一覧を計算する

    # 最も高い評価値が得られる合法手の一覧の中からランダムに着手を選択する  
修正箇所
def ai2s(mb_orig):
    legal_moves = mb_orig.calc_legal_moves()
+   best_score = None
    for move in legal_moves:
        mb = deepcopy(mb_orig)
        x, y = move
        mb.move(x, y)
        # mb の局面の評価値を計算する
        score = 0
        # 最も高い評価値が得られる合法手の一覧を計算する

    # 最も高い評価値が得られる合法手の一覧の中からランダムに着手を選択する  

最も高い評価値の判定方法

次に、10 行目の「最も高い評価値 が得られる 合法手 の 一覧を計算 する」という 処理 を 記述 します。どのような アルゴリズム で記述すれば良いかについて少し考えてみて下さい。

それまで の 局面 の中で 最も高い評価値 を表す best_score と、move を 着手 した 局面 での 評価値 を表す score の 関係 を 整理 すると、下記の表 のようになります。

関係を表す式 関係が True の場合の意味
best_score is None 最初 の 合法手 を 着手 した 局面
best_score < score score がそれまでの 評価値 の中の 最大値 より 大きい
best_score == score score がそれまでの 評価値 の中の 最大値 と 等しい
best_score > score score がそれまでの 評価値 の中の 最大値 より 小さい

上記の表の それぞれの場合 に行う 処理 は以下のようになります。

最初の合法手を着手した局面

評価値 を 計算 する 最初の局面 なので、その 評価値 が 最も高い評価値 になります。また、「最も高い評価値 が得られる 合法手 の 一覧」は、その時に 着手 した move のみ です。従って、下記の処理 を行う必要があります。

  • best_score を score の 値 で 更新 する
  • 「最も高い評価値 が得られる 合法手 の 一覧」を、その時に 着手 した move で 更新 する

score がそれまでの評価値の最大値より大きい

この場合も、その 評価値 が 最も高い評価値 になります。また、それまでの「最も高い評価値 が得られる 合法手 の 一覧」を 破棄 して、その時に 着手 した move のみ で 上書き する必要があります。それらの処理は、最初の合法手を着手した局面 で行う処理と 同じ です。

score がそれまでの評価値の最大値と等しい

この場合は、最も高い評価値 の値は 変化しない ので、下記の処理のみが必要です。

  • 「最も高い評価値 が得られる 合法手 の 一覧」に、その時に 着手 した move を 追加 する

score がそれまでの評価値の最大値より小さい

この場合は、特に 何もする必要はありません。

プログラムの記述

上記 から、最も高い評価値 を 判定 する 処理 は、下記のプログラムのように記述できます。

  • 10、11 行目:「最初 の 合法手 を 着手 した 局面」 または 「score がそれまでの 評価値 の 最大値 より 大きい 場合」の処理を記述する
  • 14、15 行目:score がそれまでの 評価値 の 最大値 と 等しい 場合の処理を記述する
  • 16 行目:Python では、ブロックの中 に プログラムを 1 つも記述しない と エラー になるので、そのような場合は pass を記述 する 必要 がある

なお、score がそれまでの 評価値 の 最大値 より 小さい 場合は、何もする必要はない ので、その場合のプログラムを 記述 する 必要 は ありません。

 1  def ai2s(mb_orig):
 2      legal_moves = mb_orig.calc_legal_moves()
 3      best_score = None
 4      for move in legal_moves:
 5          mb = deepcopy(mb_orig)
 6          x, y = move
 7          mb.move(x, y)
 8          score = 0
 9          # 最初の合法手を着手した局面 または score がそれまでの評価値の中の最大値より大きい
10          if best_score is None or best_score < score:
11              best_score = score
12              # 「最も高い評価値が得られる合法手の一覧」を、その時に着手した move で更新する
13          # score がそれまでの評価値の中の最大値と等しい
14          elif best_score == score:
15              # 「最も高い評価値が得られる合法手の一覧」に、その時に着手した move を追加する
16              pass
17
18      # 最も高い評価値が得られる合法手の一覧の中からランダムに着手を選択する   
行番号のないプログラム
def ai2s(mb_orig):
    legal_moves = mb_orig.calc_legal_moves()
    best_score = None
    for move in legal_moves:
        mb = deepcopy(mb_orig)
        x, y = move
        mb.move(x, y)
        score = 0
        # 最初の合法手を着手した局面 または score がそれまでの評価値の中の「最大値より大きい」
        if best_score is None or best_score < score:
            best_score = score
            # 「最も高い評価値が得られる合法手の一覧」を、その時に着手した move で更新する
        # score がそれまでの評価値の中の「最大値と等しい」
        elif best_score == score:
            # 「最も高い評価値が得られる合法手の一覧」に、その時に着手した move を追加する
            pass

    # 最も高い評価値が得られる合法手の一覧の中からランダムに着手を選択する 
修正箇所
def ai2s(mb_orig):
    legal_moves = mb_orig.calc_legal_moves()
    best_score = None
    for move in legal_moves:
        mb = deepcopy(mb_orig)
        x, y = move
        mb.move(x, y)
        score = 0
+       # 最初の合法手を着手した局面 または score がそれまでの評価値の中の「最大値より大きい」
+       if best_score is None or best_score < score:
+           best_score = score
+           # 「最も高い評価値が得られる合法手の一覧」を、その時に着手した move で更新する
+       # score がそれまでの評価値の中の「最大値と等しい」
+       elif best_score == score:
+           # 「最も高い評価値が得られる合法手の一覧」に、その時に着手した move を追加する
+           pass

    # 最も高い評価値が得られる合法手の一覧の中からランダムに着手を選択する   

最も高い評価値が得られる合法手の一覧の処理

最後に、最も高い評価値 が得られる 合法手 の 一覧 に関する 処理 を 記述 します。強い AI が行う 処理 は、合法手 の中で 最も良い手、すなわち 最善手(best move)を 求める ことなので、そのデータ を 代入 する 変数 の 名前 を best_moves と命名することにします。合法手 の 一覧 なので、best_moves に 代入 する データ には list を利用することにします。下記は、best_moves に関する 処理 を ai2s に 記述 したプログラムです。

  • 4 行目:for 文 による 繰り返し処理 の 前 で、best_moves を 空の list で 初期化 する

  • 13 行目:「最も高い評価値 が得られる 合法手 の 一覧」を、その時に 着手 した move で 更新 する処理は、best_moves に、move のみ を 要素 とする list を 代入 する処理なので、best_moves に [move] を 代入 する。move ではない 点に 注意 すること

  • 16 行目:「最も高い評価値 が得られる 合法手 の 一覧」に、その時に 着手 した move を 追加 する処理を、append メソッドを使って行う

  • 18 行目:choice を使って best_moves の中から ランダムな着手 を 返り値 として 返す

 1  def ai2s(mb_orig):
 2      legal_moves = mb_orig.calc_legal_moves()
 3      best_score = None
 4      best_moves = []
 5      for move in legal_moves:
 6          mb = deepcopy(mb_orig)
 7          x, y = move
 8          mb.move(x, y)
 9          score = 0
10          # 最初の合法手を着手した局面 または score がそれまでの評価値の中の「最大値より大きい」
11          if best_score is None or best_score < score:
12              best_score = score
13              best_moves = [move]
14          # score がそれまでの評価値の中の「最大値と等しい」
15          elif best_score == score:
16              best_moves.append(move)
17
18      return choice(best_moves) 
行番号のないプログラム
def ai2s(mb_orig):
    legal_moves = mb_orig.calc_legal_moves()
    best_score = None
    best_moves = []
    for move in legal_moves:
        mb = deepcopy(mb_orig)
        x, y = move
        mb.move(x, y)
        score = 0
        # 最初の合法手を着手した局面 または score がそれまでの評価値の中の「最大値より大きい」
        if best_score is None or best_score < score:
            best_score = score
            best_moves = [move]
        # score がそれまでの評価値の中の「最大値と等しい」
        elif best_score == score:
            best_moves.append(move)

    return choice(best_moves)
修正箇所
def ai2s(mb_orig):
    legal_moves = mb_orig.calc_legal_moves()
    best_score = None
+   best_moves = []
    for move in legal_moves:
        mb = deepcopy(mb_orig)
        x, y = move
        mb.move(x, y)
        score = 0
        # 最初の合法手を着手した局面 または score がそれまでの評価値の中の「最大値より大きい」
        if best_score is None or best_score < score:
            best_score = score
+           best_moves = [move]
        # score がそれまでの評価値の中の「最大値と等しい」
        elif best_score == score:
-           pass
+           best_moves.append(move)

+    return choice(best_moves)  

動作の確認

ai2s が 正しく動作 するかどうかを 確認 する 方法 の一つに、同じルール で 着手を選択 する ai2 と 対戦させる という方法があります。同じルール で 着手を選択 するのであれば、対戦した際の 通算成績 の 勝率 と 敗率 が ほぼ同じになる からです。

下記のプログラムの実行結果から、通算成績 の 勝率 と 敗率 が ほぼ同じ であることが 確認 できるので、ai2s を 正しく実装 できている 可能性が高い ことが 確認 できました。

この方法で 100% 正しく 実装できていることを 確認できる わけでは ありません が、精度は高い と思いますので、以後 は、この方法 で 正しく実装できているか を 確認 します。

from ai import ai_match, ai1, ai2, ai3

ai_match(ai=[ai2s, ai2])

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

ai2s VS ai2
count     win    lose    draw
o        5819    2919    1262
x        2938    5814    1248
total    8757    8733    2510

ratio     win    lose    draw
o       58.2%   29.2%   12.6%
x       29.4%   58.1%   12.5%
total   43.8%   43.7%   12.6%

負の無限大を利用した、None を使わない記述方法

先程のプログラムでは、11 行目の条件式に、None であることを 判定 する best_score is None を 記述 していますが、これを 記述しなくても済む 方法があります。

Python では、正の無限大 を表す 浮動小数点数型 のデータを float("inf")1、負の無限大 を表す 浮動小数点数型 のデータを float("-inf") のように 記述 することが できます。

inf とだけ記述すれば良いのではないかと思う人がいるかもしれませんが、inf は、そのような名前 の 変数 と みなされる ので うまくいきません。また、"inf" と記述すると、文字列型 のデータと みなされる ので うまくいきません。

正の無限大 は、数値型 のデータの中で 最も大きな値、負の無限大 は、数値型 のデータの中で 最も小さい値 であると みなされます。また、正の無限大どうし や、負の無限大どうし を 比較 すると 等しい とみなされます。下記のプログラムは、無限大の比較の例です。

print(float("inf") > 1000000000000000000000)   # 正の無限大とかなり大きな数字の比較
print(float("-inf") < -1000000000000000000000) # 正の無限大とかなり小さな数字の比較
print(float("inf") == float("inf"))            # 正の無限大どうしの比較
print(float("-inf") == float("-inf"))          # 負の無限大どうしの比較
print(float("-inf") < float("inf"))            # 負の無限大と正の無限大の比較

実行結果

True
True
True
True
True

この性質 を 利用 することで、ai2s を下記のプログラムのように 記述 できます。

  • 3 行目:best_score を 負の無限大 で 初期化 する
  • 11 行目:条件式 から best_score is None を 削除 する
 1  def ai2s(mb_orig):
 2      legal_moves = mb_orig.calc_legal_moves()
 3      best_score = float("-inf")
 4      best_moves = []
 5      for move in legal_moves:
 6          mb = deepcopy(mb_orig)
 7          x, y = move
 8          mb.move(x, y)
 9          score = 0
10          # 最初の合法手を着手した局面 または score がそれまでの評価値の中の「最大値より大きい」
11          if best_score < score:
12              best_score = score
13              best_moves = [move]
14          # score がそれまでの評価値の中の「最大値と等しい」
15          elif best_score == score:
16              best_moves.append(move)
17
18      return choice(best_moves) 
行番号のないプログラム
def ai2s(mb_orig):
    legal_moves = mb_orig.calc_legal_moves()
    best_score = float("-inf")
    best_moves = []
    for move in legal_moves:
        mb = deepcopy(mb_orig)
        x, y = move
        mb.move(x, y)
        score = 0
        # 最初の合法手を着手した局面 または score がそれまでの評価値の中の「最大値より大きい」
        if best_score < score:
            best_score = score
            best_moves = [move]
        # score がそれまでの評価値の中の「最大値と等しい」
        elif best_score == score:
            best_moves.append(move)

    return choice(best_moves)
修正箇所
def ai2s(mb_orig):
    legal_moves = mb_orig.calc_legal_moves()
-   best_score = None
+   best_score = float("-inf")
    best_moves = []
    for move in legal_moves:
        mb = deepcopy(mb_orig)
        x, y = move
        mb.move(x, y)
        score = 0
        # 最初の合法手を着手した局面 または score がそれまでの評価値の中の「最大値より大きい」
-       if best_score is None or best_score < score:
+       if best_score < score:
            best_score = score
            best_moves = [move]
        # score がそれまでの評価値の中の「最大値と等しい」
        elif best_score == score:
            best_moves.append(move)

    return choice(best_moves)

プログラムが正しく動作する理由

このプログラムが 正しく動作 する 理由 を説明します。

最初の局面 に対する 評価値 の 計算 を 行う前 の時点では、best_score と best_moves の 値 は、3、4 行目の 処理 から 変化していない ので、下記の表 のようになります。

変数 値
best_score float("-inf")
best_moves []

ai2s では、評価値 の値が 必ず 0 になりますが、今後作成 する AI のことを 考慮 して、評価値 の値が どのような数値 になっても 正しい処理 が 行われる ことを 示します。

最初の局面で評価値の値が負の無限大以外になった場合

最初の局面 に対する 評価値 の値が、負の無限大以外 の値になった場合は、11 行目 の if best_score < score: の 条件式 の 計算結果 が True になります。従って、12、13 行目の 処理 が行われますが、これは、修正前 の、best_score に None が 代入 されていた場合と 全く同じ処理 が行われるということを 意味 します。

従って、最初の局面 に対する 評価値 の値が、負の無限大以外 の値になった場合は、修正前 と 修正後 で、全く同じ処理 が行われ、best_score には、最初の局面 の 評価値 である score が、best_moves には [move] が 代入 されます。

最初の局面で評価値の値が負の無限大になった場合

最初の局面 に対する 評価値 の値が、負の無限大 になった場合は、11 行目 の if best_score < score: の 条件式 の 計算結果 が False になります。

その場合は、次の 15 行目 の elif best_score == score: が 実行 されますが、best_score と score には どちらも負の無限大 が 代入 されているので、その 条件式 の 計算結果 は True になります。そのため、16 行目の best_moves.append(move) が実行されますが、best_moves には 空の list が代入されているので、best_moves の値は [move] になります。

best_score の値は 変化しません が、元々代入 されていた 負の無限大 は、最初の局面 の 評価値 と 等しい ので、best_score の値は score と 等しく なります。

従って、最初の局面 に対する 評価値 の値が、負の無限大 の場合も、修正前 と 修正後 で、全く同じ処理 が行われます。以上の事から、最初の局面 の 評価値 が どのような値 であっても、修正前 と 修正後 で 同じ処理 が行われることが 示されました。

2 つ目以降の局面に値する処理

2 つ目以降 の 局面 に対する 処理の記述 は、修正後 と 修正前 で 変わらない ので、2 つ目以降 の 局面 に対しても、修正前 と 修正後 で 同じ処理 が行われることが分かります。以上の事から、修正前 と 修正後 のプログラムが、常に 同じ処理 を行うことが 示されました。

動作の確認

先程と 同様の方法 で、修正した ai2s が 正しく動作 するかどうかを 確認 します。実行結果から、ai2s を 正しく実装 できていることが 確認 できました。

ai_match(ai=[ai2s, ai2])

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

ai2s VS ai2
count     win    lose    draw
o        5874    2901    1225
x        2876    5890    1234
total    8750    8791    2459

ratio     win    lose    draw
o       58.7%   29.0%   12.2%
x       28.8%   58.9%   12.3%
total   43.8%   44.0%   12.3%

評価値を利用した ルール 3 の AI の実装

次に、評価値を利用 した アルゴリズム で ルール 3 の AI を 実装 する方法を 説明 します。なお、ルール 1 より先 に ルール 3 を 実装 するのは、ルール 2 の時と同様に、ルール 3 のほう が 簡単 に 実装できる からです。

下記は ルール 3 を 再掲 したものです。

  • 真ん中 のマスに 優先的 に 着手 する
  • 既に 埋まっていた場合 は ランダム なマスに 着手 する

評価値を利用 した アルゴリズム で上記の ルール 3 を 実装 するために、評価値 を どのように計算 すれば良いかについて少し考えてみて下さい。

評価値を利用 した アルゴリズム では 以下 のように 評価値 を 設定 します。

  • ルールの 条件 のうち、優先順位 が 高い 条件を満たすものの 評価値 を、それよりも 優先順位 が 低い 条件を満たすものの 評価値より も 高く設定 する
  • 優先順位 が 同じ 条件を満たす、ランダム に 選択 したいものの 評価値 を、すべて 同じ値 に 設定 する

ルール 3 の場合は、真ん中のマス に 優先的 に 着手 し、真ん中のマス が 埋まっていた場合 は、ランダムなマス に 着手 するので、評価値 は 下記 のように 設定 します。

  • 真ん中 のマスに 着手 した 局面 の 評価値 に、最も高い評価値 を 設定 する
  • 真ん中 のマス 以外 に 着手 した 局面 の 評価値 を、すべて 同じ値 に 設定 する

上記の条件 を 満たしていれば 、どのような評価値 を 設定 しても かまいません。本記事では、以下の表 のように 評価値 を 設定 することにします。

局面 評価値
真ん中 のマスに 着手 した 局面 1
真ん中 のマス 以外 に 着手 した 局面 0

他の値を設定したい人は、条件を満たす 好きな 値を設定 して下さい。

ai3s の定義

下記は、上記の表 の 評価値を計算 するように、ai3s を 定義 したプログラムです。

  • 9 ~ 12行目:着手 した 座標 が 真ん中 の (1, 1) の場合は score に 1 を、そうでない場合 は 0 を 代入 する
 1  def ai3s(mb_orig):
 2      legal_moves = mb_orig.calc_legal_moves()
 3      best_score = float("-inf")
 4      best_moves = []
 5      for move in legal_moves:
 6          mb = deepcopy(mb_orig)
 7          x, y = move
 8          mb.move(x, y)
 9          if x == 1 and y == 1:
10              score = 1
11          else:
12              score = 0
13          # 最初の合法手を着手した局面 または score がそれまでの評価値の中の「最大値より大きい」
14          if best_score < score:
15              best_score = score
16              best_moves = [move]
17          # score がそれまでの評価値の中の「最大値と等しい」
18          elif best_score == score:
19              best_moves.append(move)
20
21      return choice(best_moves)
行番号のないプログラム
def ai3s(mb_orig):
    legal_moves = mb_orig.calc_legal_moves()
    best_score = float("-inf")
    best_moves = []
    for move in legal_moves:
        mb = deepcopy(mb_orig)
        x, y = move
        mb.move(x, y)
        if x == 1 and y == 1:
            score = 1
        else:
            score = 0
        # 最初の合法手を着手した局面 または score がそれまでの評価値の中の「最大値より大きい」
        if best_score < score:
            best_score = score
            best_moves = [move]
        # score がそれまでの評価値の中の「最大値と等しい」
        elif best_score == score:
            best_moves.append(move)

    return choice(best_moves)
修正箇所
-def ai2s(mb_orig):
+def ai3s(mb_orig):
    legal_moves = mb_orig.calc_legal_moves()
    best_score = float("-inf")
    best_moves = []
    for move in legal_moves:
        mb = deepcopy(mb_orig)
        x, y = move
        mb.move(x, y)
-       score = 0
+       if x == 1 and y == 1:
+           score = 1
+       else:
+           score = 0
        # 最初の合法手を着手した局面 または score がそれまでの評価値の中の「最大値より大きい」
        if best_score < score:
            best_score = score
            best_moves = [move]
        # score がそれまでの評価値の中の「最大値と等しい」
        elif best_score == score:
            best_moves.append(move)

    return choice(best_moves)

動作の確認

ai3s が 正しく動作 するかどうかを 確認 するために、先程と同様 に ai3 と 対戦 を行います。実行結果 から、ai3s を 正しく実装 できていることが 確認 できました。

ai_match(ai=[ai3s, ai3])

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

ai3s VS ai3
count     win    lose    draw
o        6995    1839    1166
x        1911    6972    1117
total    8906    8811    2283

ratio     win    lose    draw
o       70.0%   18.4%   11.7%
x       19.1%   69.7%   11.2%
total   44.5%   44.1%   11.4%

tuple どうしの比較を利用した ai3s の記述

座標 を表す move に代入される データ は、x 座標 と y 座標 を 表す、2 つの要素 を持つ tuple です。先ほどのプログラムでは、move に 真ん中 のマスの 座標 を表す (1, 1) という tuple が 代入されているか どうかを、tuple の 各要素 を x と y に 代入 してから x == 1 and y == 1 という 条件式 を記述することで 判定 していましたが、この 判定 を行う 条件式 を、より 簡潔に記述 することができます。

具体的には、2 つ の tuple が、同じ値 の 要素 を 持つか どうかを、下記のプログラムの 2、3 行目のように、== 演算子 を使って 判定 することができます。

move = (1, 1)
print(move == (1, 1))
print(move == (1, 2))

実行結果

True
False

従って、ai3s は、下記のプログラムのように記述することができます。

  • 9 行目:move == (1, 1) によって、(1, 1) のマスに 着手したか どうかを 判定 する
 1  def ai3s(mb_orig):
 2      legal_moves = mb_orig.calc_legal_moves()
 3      best_score = float("-inf")
 4      best_moves = []
 5      for move in legal_moves:
 6          mb = deepcopy(mb_orig)
 7          x, y = move
 8          mb.move(x, y)
 9          if move == (1, 1):
10              score = 1
11          else:
12              score = 0
13          # 最初の合法手を着手した局面 または score がそれまでの評価値の中の「最大値より大きい」
14          if best_score < score:
15              best_score = score
16              best_moves = [move]
17          # score がそれまでの評価値の中の「最大値と等しい」
18          elif best_score == score:
19              best_moves.append(move)
20
21      return choice(best_moves)
行番号のないプログラム
def ai3s(mb_orig):
    legal_moves = mb_orig.calc_legal_moves()
    best_score = float("-inf")
    best_moves = []
    for move in legal_moves:
        mb = deepcopy(mb_orig)
        x, y = move
        mb.move(x, y)
        if move == (1, 1):
            score = 1
        else:
            score = 0
        # 最初の合法手を着手した局面 または score がそれまでの評価値の中の「最大値より大きい」
        if best_score < score:
            best_score = score
            best_moves = [move]
        # score がそれまでの評価値の中の「最大値と等しい」
        elif best_score == score:
            best_moves.append(move)

    return choice(best_moves)
修正箇所
def ai3s(mb_orig):
    legal_moves = mb_orig.calc_legal_moves()
    best_score = float("-inf")
    best_moves = []
    for move in legal_moves:
        mb = deepcopy(mb_orig)
        x, y = move
        mb.move(x, y)
        if x == 1 and y == 1:
            score = 1
        else:
            score = 0
        # 最初の合法手を着手した局面 または score がそれまでの評価値の中の「最大値より大きい」
        if best_score < score:
            best_score = score
            best_moves = [move]
        # score がそれまでの評価値の中の「最大値と等しい」
        elif best_score == score:
            best_moves.append(move)

    return choice(best_moves)

ai3s が 正しく動作 するかどうかを 確認 するために、ai3 と 対戦 を行います。実行結果 から、ai3s を 正しく実装 できていることが 確認 できました。

ai_match(ai=[ai3s, ai3])

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

ai3s VS ai3
count     win    lose    draw
o        6945    1909    1146
x        1948    6878    1174
total    8893    8787    2320

ratio     win    lose    draw
o       69.5%   19.1%   11.5%
x       19.5%   68.8%   11.7%
total   44.5%   43.9%   11.6%

tuple どうしの比較の注意点

== 演算子を使って tuple どうしを 比較 する際に、外側 の ( と ) を 省略 すると、下記のプログラムのような、意図しない計算 が 行われる 点に 注意 して下さい。

move = (1, 1)
print(move == 1, 1)  # print((1 == move, 1)) とみなされる
print(1, 1 == move)  # print((1, 1 == move)) とみなされる

実行結果

False 1
1 False

上記の計算が行われる 理由 は、2 行目 の move == 1, 1 という 式 は、1 つ目 の 要素 が move == 1、2 つ目 の 要素 が 1 である、(move == 1, 1) という tuple の 外側 の ( と ) が 省略された式 であると みなされる からです。move == 1 の計算結果は False なので、実行結果 には (False, 1) という tuple を 表す、「False 1」 が 表示 されます。3 行目も 同様 に、(1, 1 == move) という tuple と みなされます。

list どうしの比較と、注意点

list どうし の 比較 も、下記のプログラムの 2、3 行目 のように、tuple どうし の比較と 同様 に、== 演算子 を使って 行う ことが できます。

ただし、4 行目 のように、要素の値が同じ であっても、list と tuple を == 演算子で 比較 すると False になる点に 注意 して下さい。

move = [1, 1]
print(move == [1, 1])
print(move == [1, 2])
print(move == (1, 1))

実行結果

True
False
False

評価値を利用した AI のひな形となる関数の定義

今回の記事で実装した ai2s と ai3s は、評価値 を 計算 する 以外の処理 は 完全に共通 します。下記のプログラムは、ai2s と ai3s の 共通する部分 を 抜き出したもの です。なお、関数名は、仮の名前として ai としました。

共通しない のは、真ん中の「mb の局面の評価値を計算する処理」の 部分だけ です。

def ai(mb_orig):
    legal_moves = mb_orig.calc_legal_moves()
    best_score = float("-inf")
    best_moves = []
    for move in legal_moves:
        mb = deepcopy(mb_orig)
        x, y = move
        mb.move(x, y)

        # mb の局面の評価値を計算する処理
        
        if best_score < score:
            best_score = score
            best_moves = [move]
        elif best_score == score:
            best_moves.append(move)

    return choice(best_moves)

これは、ルール 2、3 以外 のルールを 評価値を利用 した アルゴリズム で 実装 する場合も 同様 です。その理由は、上記で抜き出したプログラムが行う処理が、評価値を利用 した アルゴリズム で 必ず行わなければならない、共通の処理 だからです。

このことから、評価値を利用 した アルゴリズム では、異なるルール の AI を実装する際に、局面 の 評価値 を 計算 する 処理のみ が 異なる ことがわかります。

評価値を利用 した アルゴリズム で AI の 関数を記述 する際に、毎回 上記の コメント以外 のプログラムを 記述 するのは 大変 なので、上記の 共通する部分 の処理を行う、ひな形 となる 関数を定義 し、評価値の計算 を行う 処理のみ を 記述 することで、評価値を利用 した アルゴリズム の AI を 簡潔に実装 できるようにします。

関数名の設定と、仮引数の追加

まず、ひな形 となる 関数の名前 を決める必要があります。評価値(score)を利用する AI なので、ai_by_score という 名前にする ことにします。

ひな形 となる 関数の中 で、「mb の局面の評価値を計算する処理」は、関数の 仮引数 に 代入 した 関数 を 呼び出して行う ことにします。これは、Marubatsu クラスの play メソッドや、ai_match の 実引数 に、AI の処理 を行う 関数 を 記述 することで、play メソッドや ai_match の中で その関数 による 処理を行う ことができるようにした手法と 同じ です。

評価値 を 計算 する 関数 のことを、評価関数(evaluate function)と呼ぶので、その 関数を代入 する 仮引数の名前 は、それらの単語を略してつなげた eval_func と命名しました。

下記は、先程のプログラムの 関数名 を ai_by_score に 変更 し、仮引数 に eval_func を 追加 したプログラムです。

def ai_by_score(mb_orig, eval_func):
    legal_moves = mb_orig.calc_legal_moves()
    best_score = float("-inf")
    best_moves = []
    for move in legal_moves:
        mb = deepcopy(mb_orig)
        x, y = move
        mb.move(x, y)

        # eval_func を使って、mb の局面の評価値を計算する処理
        
        if best_score < score:
            best_score = score
            best_moves = [move]
        elif best_score == score:
            best_moves.append(move)

    return choice(best_moves)
修正箇所
-def ai(mb_orig):
+def ai_by_score(mb_orig, eval_func):
    legal_moves = mb_orig.calc_legal_moves()
    best_score = float("-inf")
    best_moves = []
    for move in legal_moves:
        mb = deepcopy(mb_orig)
        x, y = move
        mb.move(x, y)

-       # mb の局面の評価値を計算する処理
+       # eval_func を使って、mb の局面の評価値を計算する処理
        
        if best_score < score:
            best_score = score
            best_moves = [move]
        elif best_score == score:
            best_moves.append(move)

    return choice(best_moves)

評価関数の仕様

ai_by_score の中で、評価関数 を 利用する処理 を 記述 するためには、評価関数 の 入力(仮引数)と 出力(返り値)の 仕様 を決める必要があります。

評価関数 は、局面 の 評価値を計算 する 処理 を行うので、仮引数 に 現在の局面 を表す Marubatsu クラス の インスタンス を 代入 し、返り値 として 評価値を返す ことにします。

評価関数 の 仕様 が 決まった ので、ai_by_score の「eval_func を使って、mb の局面の評価値を計算する処理」の部分には、下記のプログラムの 10 行目 のように、eval_func の 実引数 に mb を 記述 して呼び出し、その 返り値 を score に代入 する処理を記述します。

 1  def ai_by_score(mb_orig, eval_func):
 2      legal_moves = mb_orig.calc_legal_moves()
 3      best_score = float("-inf")
 4      best_moves = []
 5      for move in legal_moves:
 6          mb = deepcopy(mb_orig)
 7          x, y = move
 8          mb.move(x, y)
 9
10          score = eval_func(mb)
11        
12          if best_score < score:
13              best_score = score
14              best_moves = [move]
15          elif best_score == score:
16              best_moves.append(move)
17
18      return choice(best_moves)
行番号のないプログラム
def ai_by_score(mb_orig, eval_func):
    legal_moves = mb_orig.calc_legal_moves()
    best_score = float("-inf")
    best_moves = []
    for move in legal_moves:
        mb = deepcopy(mb_orig)
        x, y = move
        mb.move(x, y)

        score = eval_func(mb)
        
        if best_score < score:
            best_score = score
            best_moves = [move]
        elif best_score == score:
            best_moves.append(move)

    return choice(best_moves)
修正箇所
def ai_by_score(mb_orig, eval_func):
    legal_moves = mb_orig.calc_legal_moves()
    best_score = float("-inf")
    best_moves = []
    for move in legal_moves:
        mb = deepcopy(mb_orig)
        x, y = move
        mb.move(x, y)

+       score = eval_func(mb)
        
        if best_score < score:
            best_score = score
            best_moves = [move]
        elif best_score == score:
            best_moves.append(move)

    return choice(best_moves)

ai_by_score を利用した、ルール 2 で着手を行う AI の定義

ai_by_score を 使って、ルール 2 で 着手 を行う AI の 定義 を行う 方法 を説明します。

まず、ルール 2 の 評価関数 を 定義 する必要があります。先ほど実装 した ai2s と同様に、 すべての局面 の 評価値 を 0 にすれば良いので、評価関数 は、下記のプログラムのように、常に 0 を返す関数 として 定義 します。

def eval_func(mb):
    return 0

ai2s は、ai_by_score と上記の eval_func を使って、下記のように 定義 できます。

def ai2s(mb):
    return ai_by_score(mb, eval_func)

このように、ひな形 となる ai_by_score を 定義 した事で、評価関数 の 定義 と、return ai_by_score(mb, eval_func) を 記述するだけ で AI を作成できる ようになります。

動作の確認

修正した ai2s が 正しく動作 するかどうかを 確認 するために、ai2 と 対戦 を行います。実行結果 から、ai2s を 正しく実装 できていることが 確認 できました。

ai_match(ai=[ai2s, ai2])

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

ai2s VS ai2
count     win    lose    draw
o        5774    2869    1357
x        2912    5830    1258
total    8686    8699    2615

ratio     win    lose    draw
o       57.7%   28.7%   13.6%
x       29.1%   58.3%   12.6%
total   43.4%   43.5%   13.1%

ai_by_score を利用した、ルール 3 で着手を行う AI の定義

次に、ai_by_score を使って、ルール 3 で着手を行う AI を 定義 することにします。

そのためには、ルール 3 の 評価関数 を 定義 する 必要 がありますが、ルール 2 の 評価関数 の 名前 に eval_func という 名前 を 使った ので、その関数の名前には 別の名前 を 付ける必要 があります。また、ai3s と 評価関数 の 定義 を 別々に記述 すると、その 2 つの関数 の 関係 が わかりづらくなる という 欠点 があります。そこで、それらの 問題 を 解決 する 方法 として、AI の関数 と 評価関数 を、1 つにまとめる方法 を紹介します。

ローカル関数

その方法は、評価関数 を AI の ローカル関数 として 定義 するというものです。ローカル関数 は、関数の中 に 定義した関数 の事で、以下 の ローカル変数 と 同様の性質 を持ちます。

  • ローカル関数 は その関数 の ブロックの中 でしか 利用できない
  • 異なる関数 の中で、同一の名前 の ローカル関数 を 異なる関数 として 定義できる

上記のような性質を持つ 理由 は、ローカル変数 も、ローカル関数 も、以前の記事で説明した、その 関数の、ローカル名前空間 で 管理 されるからです。

従って、ai2s は、下記のプログラムのように定義できます。

  • 2 行目:評価関数 を ai3s の ローカル関数 として eval_func という 名前 で 定義 する
  • 3、4 行目:直前の着手 は、mb の last_move 属性 に 代入 されるので、それが 真ん中 のマスを表す (1, 1) と 等しい 場合は 評価値 として 1 を 返す
  • 5、6 行目:それ以外の場合は、評価値 として 0 を 返す
  • 8 行目:ai_by_score の 実引数 に eval_func を記述して呼び出した 返り値 を 返す
1  def ai3s(mb):
2      def eval_func(mb):
3          if mb.last_move == (1, 1):
4              return 1
5          else:
6              return 0
7
8      return ai_by_score(mb, eval_func)
行番号のないプログラム
def ai3s(mb):
    def eval_func(mb):
        if mb.last_move == (1, 1):
            return 1
        else:
            return 0

    return ai_by_score(mb, eval_func)

2 行目 の 評価関数 の 名前 は、好きな名前 で定義しても 構いません が、その場合 は、8 行目 の 実引数 の 評価関数 に その名前 を 記述 する 必要 があります。

動作の確認

修正した ai3s が 正しく動作 するかどうかを 確認 するために、ai3 と 対戦 を行います。実行結果 から、ai3s を 正しく実装 できていることが 確認 できました。

ai_match(ai=[ai3s, ai3])

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

ai3s VS ai3
count     win    lose    draw
o        6915    1948    1137
x        1876    7001    1123
total    8791    8949    2260

ratio     win    lose    draw
o       69.2%   19.5%   11.4%
x       18.8%   70.0%   11.2%
total   44.0%   44.7%   11.3%

ローカル関数の利点

ローカル関数 を 利用 することで、以下のような 利点 を 得る ことができます。

  • AI の関数 と 評価関数 を、一つ の 関数の定義 で まとめる ことが できる
  • どの AI を 実装 する 場合 でも、評価関数 の 名前 として、常に eval_func という 共通の名前 を 利用できる ので、AI ごと に、異なる名前 を 考える必要 が なくなる

ローカル関数に関する補足

一般的 に 関数 は、特定の処理 を、プログラムの さまざまな場所 から 利用できるようするこ とが 目的 で 定義 しますが、その目的 で ローカル関数 を 利用 することは できません。従って、ローカル関数 は、今回の記事で紹介したような、ローカル関数の利点 を 有効に活用 できるような 場面以外 では あまり使われません。

ローカル関数 は、クラスの定義 の ブロックの中 で 定義する、メソッドに似ている ように 見えるかもしれません が、クラスの メソッド は、クラスの定義の ブロックの外 で、そのクラスの インスタンス から呼び出して 利用 することが できます が、ローカル関数 は、ローカル関数を定義した関数の ブロックの外 で 利用 することが できない 点が 異なります。

関数 の ブロックの中 で、ローカル関数 を 定義する前 で、そのローカル関数 を呼び出して 利用 することは できません。これは、以前の記事で説明した、グローバル関数 を 定義する前 で、そのグローバル関数 を呼び出して 利用 することが できない のと 同じ です。以前の記事 で説明した、名前空間の仕組み 正しく理解していれば、そのことは 明らか でしょう。

従って、下記のプログラムのように、ai3s_buggy を定義して、ai_match(ai=[ai3s, ai3]) を呼び出すと、下記の実行結果のように エラーが発生 します。なお、関数名 の一部の buggy は、バグのある という 意味 の英語です。

def ai3s_buggy(mb):
    return ai_by_score(mb, eval_func)

    def eval_func(mb):
        if mb.last_move == (1, 1):
            return 1
        else:
            return 0

ai_match(ai=[ai3s_buggy, ai3])        

実行結果

略
Cell In[19], line 2
      1 def ai3s_buggy(mb):
----> 2     return ai_by_score(mb, eval_func)
      4     def eval_func(mb):
      5         if mb.last_move == (1, 1):

UnboundLocalError: cannot access local variable 'eval_func' where it is not associated with a value

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

  • UnboundLocalError

ローカル(Local)変数(または関数)が名前空間に登録されていない(unbound)ことを表すエラー

  • cannot access local variable 'eval_func' where it is not associated with a value

値(vaule)が関連付けられていない(whiere it is not associated)eval_func というローカル(local)変数(または関数)(variable)をアクセス(利用)(access)できない(cannot)

ローカル関数を利用した、ルール 2 で着手を行う AI の定義

せっかくなので、下記のプログラムのように、ルール 2 についても、ローカル関数 を 利用 した ai2s の 定義 を行います。

def ai2s(mb):
    def eval_func(mb):
        return 0
        
    return ai_by_score(mb, eval_func)

動作の確認

修正した ai2s が 正しく動作 するかどうかを 確認 するために、ai2 と 対戦 を行います。実行結果 から、ai2s を 正しく実装 できていることが 確認 できました。

ai_match(ai=[ai2s, ai2])

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

ai2s VS ai2
count     win    lose    draw
o        5921    2857    1222
x        2829    5896    1275
total    8750    8753    2497

ratio     win    lose    draw
o       59.2%   28.6%   12.2%
x       28.3%   59.0%   12.8%
total   43.8%   43.8%   12.5%

今回の記事のまとめ

今回の記事では、評価値を利用 した アルゴリズム を 紹介 し、評価値を使った AI の ひな形 となる 処理 を行う ai_by_score を 定義 する事で、AI の処理 を行う 関数 を 評価関数 を 記述するだけ で、簡潔 に AI の関数 を 記述 する 方法 を 説明 しました。

また、実際 に ルール 2、3 を、評価値を利用 した アルゴリズム で 定義 しました。

今回の記事の内容からは、評価値を利用したアルゴリズムの 利点 が あまり感じられない かもしれません。次回の記事では引き続き、ルール 1、4 ~ 7 を評価値を利用したアルゴリズムで定義し、評価値を利用 した アルゴリズム の 利点と欠点 について 説明 します。

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

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

今回の記事では、marubatsu.py は修正していないので、marubatsu_new.py はありません。

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

次回の記事

  1. inf は、無限大を表す infinity の略です ↩

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?