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を一から作成する その49 評価値の計算方法

Last updated at Posted at 2024-01-28

目次と前回の記事

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

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

これまでに作成した AI

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

関数名 アルゴリズム
ai1 左上から順空いているマス を探し、最初に見つかったマス着手 する
ai2
ai2s
ランダム なマスに 着手 する
ai3
ai3s
真ん中 のマスに 優先的着手 する
既に 埋まっていた場合ランダム なマスに 着手 する
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

評価値を利用した ルール 1 で着手を行う AI の定義

前回の記事では、評価値を利用 した アルゴリズムルール 23 で着手を行う AI を定義 しました。今回記事では、ルール 14 で着手を行う AI を定義 する事にします。

まず、下記の ルール 1 で着手を行う ai1s を定義します。

  • 左上から順空いているマス を探し、最初に見つかったマス着手 する

このルールに従って、評価値をどのように計算するかについて少し考えてみて下さい。

このルールは、下記のようなルールに 置き換える ことができます。

  1. (0, 0) のマスが空いていればそこに着手する
  2. (1, 0) のマスが空いていればそこに着手する
  3. (2, 0) のマスが空いていればそこに着手する
  4. (0, 1) のマスが空いていればそこに着手する
  5. (1, 1) のマスが空いていればそこに着手する
  6. (2, 1) のマスが空いていればそこに着手する
  7. (0, 2) のマスが空いていればそこに着手する
  8. (1, 2) のマスが空いていればそこに着手する
  9. (2, 2) のマスが空いていればそこに着手する

従って、評価値を利用 した アルゴリズム の場合は、上記の条件のうち、上に記述 されている マス着手した局面評価値 が、下に記述 されている マス着手した局面評価値より大きくなる ように、それぞれのマスの 評価値を設定 すればよいことが分かります。

一例 として、下図 のように 評価値を設定 することができます。本記事では下図の評価値を採用しますが、上記の条件が満たされていれば、他の評価値設定 しても かまいません

本記事では、上記の評価値計算 する ai1s評価関数定義 する いくつかの方法紹介 します。どのように定義すればよいかについて少し考えてみて下さい。

なお、マスの 座標 から 評価値計算 する 処理 は、以前の記事で紹介した 数値座標xy 座標変換 する処理の なので、忘れた方は、そちらを 復習 して下さい。

ai1s の定義 その 1(if 文を使う方法)

下記は、if 文 を使ってそれぞれの 座標 に対する 評価値計算 する ai1s のプログラムです。初心者でも思いつくことができる方法だと思いますので、説明は省略します。

from ai import ai_by_score

def ai1s(mb, debug=False):
    def eval_func(mb):
        if mb.last_move == (0, 0):
            return 9
        elif mb.last_move == (1, 0):
            return 8
        elif mb.last_move == (2, 0):
            return 7
        elif mb.last_move == (0, 1):
            return 6
        elif mb.last_move == (1, 1):
            return 5
        elif mb.last_move == (2, 1):
            return 4
        elif mb.last_move == (0, 2):
            return 3
        elif mb.last_move == (1, 2):
            return 2
        elif mb.last_move == (2, 2):
            return 1

    return ai_by_score(mb, eval_func, debug=debug)

動作の確認

ai1s正しく動作 するかどうかを 確認 するために、ai1対戦 を行います。以前の記事で説明したように、ai1 VS ai1 では、ai1アルゴリズム には ランダム性はない ので、10000 回の すべての対戦〇 が勝利 します。実行結果 から、ai1s正しく実装 できていることが 確認 できました。

from ai import ai_match, ai1, ai4

ai_match(ai=[ai1s, ai1])

実行結果

ai1s VS ai1
count     win    lose    draw
o       10000       0       0
x           0   10000       0
total   10000   10000       0

ratio     win    lose    draw
o      100.0%    0.0%    0.0%
x        0.0%  100.0%    0.0%
total   50.0%   50.0%    0.0%

ai1s の定義 その 2(list によるテーブルを使う方法)

以前の記事で、Excel 座標 から xy 座標 への 対応づけ を、excel_to_xy_table という dict を使った テーブル(表)で行いました。それと 同様の方法 で、マスの座標 とそのマスに着手した場合の 評価値対応づける ことができます。ただし、その方法を利用するためには、dictキー利用 できる データの種類 を理解する必要があります。

ハッシュ可能なオブジェクトとハッシュ値

これまでのプログラムでは、dictキー には、主に 文字列型 のデータを利用してきましたが、dictキー には ハッシュ可能なオブジェクト(hashable)を利用することができます。ハッシュ可能なオブジェクト とは、以下性質 を持つ オブジェクト です。

  • その オブジェクト存在 している は、決して 変更されることがないハッシュ値(hash value)と呼ばれる 整数型データ持つ
  • ハッシュ可能なオブジェクトどうし== 演算子で 等しいか どうかを 比較 することができ、==判定結果True になる場合は、必ず 同じハッシュ値持つ

細かい話になりますが、誤解されがち な点についていくつか 補足 します。ただし、下記の補足は、ハッシュ値直接扱う ようなプログラムを記述する 場合でなければ気にする必要ありません

ハッシュ可能なオブジェクト必ず == 演算子で 比較 することができますが、ハッシュ可能 でない オブジェクトを == 演算子で 比較できるか どうかは、その オブジェクトの種類 によって 異なります。例えば listdict はハッシュ可能 でない オブジェクトですが、== 演算子で 比較 することが できます

上記の 2 つ目性質 の、「同じハッシュ値 を持つ オブジェクト は、==判定結果True になる」は必ずしも 正しい とは 限りません同じハッシュ値 を持つ オブジェクト等しくない判定 される事を ハッシュ値の衝突 と呼びます。

ハッシュ可能なオブジェクト詳細 については、下記のリンク先を参照して下さい。

dict のキーにハッシュ可能なオブジェクトのみが利用できる理由

dictキーハッシュ可能なオブジェクトのみ利用 できる 理由 は、dict は、キーの値 を、キーハッシュ値利用 して 管理 しているからです。具体的な仕組については説明が長くなるので省略しますが、簡単にいうと、下記の理由から、ハッシュ値利用 することで、効率 よく dictキーキーの値管理 することが できるようになる からです。

  • キー指定 して、dict から その キーの値取り出す処理 を行う際には、dict登録 されている キー の中 から指定したキー等しいキー探す必要 がある
  • ハッシュ値利用 することで、ハッシュ可能なオブジェクトどうし等しいか どうかを 効率的に判定 することができる

逆に言うと、dictキー にハッシュ可能 でない オブジェクトを 利用できる ように してしまう と、dict処理かなり遅く なってしまいます。

ハッシュ という 概念 は、コンピューター に関する 技術 の中で、かなり 重要な概念 なので、興味がある方は調べてみると良いでしょう。例えば、SNS などで良く使われている ハッシュタグ のハッシュと、ハッシュ値 のハッシュは、同じ概念 の用語です。本記事でも、直接ハッシュ値を扱う機会があれば詳しく解説しようと思います。

参考までに、wikipedia のハッシュのリンクを下記に示します。

ハッシュ可能なオブジェクトの種類

Pythonハッシュ可能なオブジェクト は以下の通りです。

  1. 数値型文字列型論理型None 型 などの、単一のデータ を扱う、単一データ型イミュータブルオブジェクト
  2. Python任意のデータ型 を、複数組み合わせ てデータを表現する 複合データ型 のうち、そのデータ型 および、すべての要素イミュータブルオブジェクト
  3. 自分で定義 した クラスインスタンス

上記の 12 は、以前の記事で説明した、疑似的な複製 を行うことができるデータです。

上記の 3疑似的な複製 を行うことが できない 場合がありますが、自分で定義 した クラスインスタンス は、インスタンス扱う データの 内容に関わらずインスタンスごと異なるハッシュ値計算される と考えて下さい1

従って、〇×ゲーム合法手 を表す (1, 2) という tuple は、それ自身すべての要素ハッシュ可能なオブジェクト なので、ハッシュ可能なオブジェクト分類 されます。

ハッシュ値の取得方法

以前の記事で説明した組み込み関数 id同様 に、一般的 なプログラムで hash を使って ハッシュ値を計算 する 必要 がある 場面 はあまり 多くないと 思いますので、興味がない方は、ハッシュ値に関する以下の説明は読み飛ばしても構いません。

オブジェクトハッシュ値 は、組み込み関数 hash実引数オブジェクトを記述 して呼び出すことで 取得する ことができます。また、hash実引数 にハッシュ可能 でない オブジェクトを 記述 して呼び出すと エラーが発生 します。

組み込み関数 hash についての詳細は、下記のリンク先を参照して下さい。

下記のプログラムは、いくつかデータ型ハッシュ値表示 しています。

from marubatsu import Marubatsu
mb = Marubatsu()
print(hash(1))                 # 数値型のデータ
print(hash("abc"))             # 文字列型のデータ
print(hash(True))              # 論理型のデータ
print(hash(None))              # None 型のデータ
print(hash((1, "abc", True)))  # すべての要素がハッシュ可能な tuple
print(hash(mb))                # Marubatsu クラスのインスタンス(この値は毎回異なる)

実行結果

1
5133467580067694764
1
8795299436207
3819214611466916399
145277155213

listミュータブル なので、ハッシュ可能なオブジェクト では ありません。従って hash実引数に記述 して呼び出すと、下記のプログラムのように エラーが発生 します。

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

実行結果

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[4], line 1
----> 1 print(hash([1, 2, 3]))

TypeError: unhashable type: 'list'

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

  • TypeError
    データ型(type)に関するエラー
  • unhashable type: 'list'
    list は ハッシュ可能でない(unhashable)データ型(type)である

tupleイミュータブル なデータですが、その 要素ミュータブル なデータが 含まれている 場合も、下記のプログラムのように エラーが発生 します。

print(hash((1, [2, 3])))  # 要素にミュータブルな list が含まれている

実行結果

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[5], line 1
----> 1 print(hash((1, [2, 3])))

TypeError: unhashable type: 'list'

ハッシュ値の性質

この説明も 当面は利用 しない 知識 なので、興味がない方は読み飛ばしてもかまいません。

Python の、ハッシュ可能組み込み型 のデータは、同じ内容 であれば、常に 同じハッシュ値 を持ちます。例えば、下記のプログラムの ab には 同一の要素の値 を持つ tuple代入 されています。ab異なるオブジェクト なので、下記のプログラムのように、id を表示すると 異なる値 が表示されますが、ハッシュ値同一 になります。

a = (1, 2)
b = (1, 2)
print(id(a))
print(id(b))
print(hash(a))
print(hash(b))

実行結果(異なる数値が表示される場合があります)

2324448500864
2324448507456
-3550055125485641917
-3550055125485641917

自分で定義 した クラスインスタンス の場合は、基本的1には作成した インスタンスごと異なるハッシュ値 が表示されます。下記のプログラムは、Marubastu クラスの 2 つの インスタンスハッシュ値表示 していますが、どちらもゲーム開始直後表します が、異なるインスタンス異なるハッシュ値 が付けられていることがわかります。

mb1 = Marubatsu()
mb2 = Marubatsu()
print(hash(mb1))
print(hash(mb2))

実行結果(異なる値が表示される場合があります)

145278027657
145277956725

テーブルを使った ai1s の定義

ゲーム盤の座標 を表す tupleキー とし、その キーの値キーの座標着手 した局面の 評価値代入 することで、ai1s下記 のプログラムのように 定義 できます。なお、先程の ai1s と修正点が多すぎるので、修正箇所は省略します。

  • 3 ~ 7 行目座標 から 評価値対応 づける テーブル を表す dict のデータを記述し、ローカル変数 score_table代入 する
  • 8 行目テーブルから 着手した 座標評価値 を求めて 返り値 として 返す
 1  def ai1s(mb, debug=False):
 2      def eval_func(mb):
 3          score_table = {
 4              (0, 0): 9, (1, 0): 8, (2, 0): 7,
 5              (0, 1): 6, (1, 1): 5, (2, 1): 4,
 6              (0, 2): 3, (1, 2): 2, (2, 2): 1,
 7          }
 8          return score_table[mb.last_move]
 9
10    return ai_by_score(mb, eval_func, debug=debug)
行番号のないプログラム
def ai1s(mb, debug=False):
    def eval_func(mb):
        score_table = {
            (0, 0): 9, (1, 0): 8, (2, 0): 7,
            (0, 1): 6, (1, 1): 5, (2, 1): 4,
            (0, 2): 3, (1, 2): 2, (2, 2): 1,
        }
        return score_table[mb.last_move]

    return ai_by_score(mb, eval_func, debug=debug)

動作の確認

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

ai_match(ai=[ai1s, ai1])

実行結果

ai1s VS ai1
count     win    lose    draw
o       10000       0       0
x           0   10000       0
total   10000   10000       0

ratio     win    lose    draw
o      100.0%    0.0%    0.0%
x        0.0%  100.0%    0.0%
total   50.0%   50.0%    0.0%

ai1s の定義 その 3(式で計算する方法)

評価値 を、下図のように、規則正しく設定 した場合は、 を使って 評価値計算 することが できます。どのような式を記述すればよいかについて少し考えてみて下さい。

以前の記事で、下図の マス数値座標 から、xy 座標計算 する を紹介しました。

具体的には、実引数 に、上図のマスの 数値座標 を記述して呼び出すと、xy 座標 を表す tuple返り値 として返す num_to_xy を下記のプログラムのように 定義 しました。

def num_to_xy(coord):
    x = coord % 3
    y = coord // 3
    return x, y

一旦 評価値 の事を 忘れて上記の関数逆の処理、すなわち xy 座標 から 数値座標計算 する 処理 を行うプログラムを考えることにします。上図から、数値座標 は、x 座標1 増える1 増える ことが分かります。また、y 座標1 増える3 増える ことが分かります。従って、xy 座標 から 数値座標計算 する は、下記 のように 記述 できます。

coord = x * 1 + y * 3

x * 1x同じ なので、上記の式は 下記 のように 記述 できます。

coord = x + y * 3

次に、下記の 評価値 と、数値座標 の図を 並べて みて、それぞれ同じ座標マスの数字どのような関係あるか について考えてみて下さい。

 

よく見れば、同じ座標マスの数値合計 するといずれも 9 になる ことが分かります。従って、評価値数値座標 の関係には、下記の等式成り立ち ます。

$数値座標 + 評価値 = 9$

この等式を変形すると、以下のようになります。

$評価値 = 9 - 数値座標$

従って、評価値score という 変数 で表すと、評価値下記の式計算 できます。

score = 9 - (x + y * 3)

上記から、ai1s は下記のプログラムのように 定義 できます。なお、先程の ai1s と修正点が多すぎるので、修正箇所は省略します。このように、評価値規則正しい値設定 することで 簡単な式評価値計算できる場合 があります。

  • 3 行目着手 した 座標 から、x 座標y 座標取り出しxy代入 する
  • 4 行目先ほどの式 を使って、座標 から 評価値計算 し、その値を 返す
1  def ai1s(mb, debug=False):
2      def eval_func(mb):
3          x, y = mb.last_move
4          return 9 - (x + y * 3)
5
6      return ai_by_score(mb, eval_func, debug=debug)
行番号のないプログラム
def ai1s(mb, debug=False):
    def eval_func(mb):
        x, y = mb.last_move
        return 9 - (x + y * 3)

    return ai_by_score(mb, eval_func, debug=debug)

デバッグ表示による動作の確認

今回は、念のため に本当に 正しく実装 できているかどうかを、下記のプログラムで、デバッグ表示 を行うことで 確認 することにします。実行結果score の表示から、左上のマス から 98、・・・、1 の順で、正しい評価値計算 されることが 確認 できます。

from marubatsu import Marubatsu

mb = Marubatsu()
ai1s(mb, debug=True)

実行結果

Start ai_by_score
Turn o
...
...
...

legal_moves [(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1), (0, 2), (1, 2), (2, 2)]
====================
move (0, 0)
Turn x
O..
...
...

score 9 best score -inf
UPDATE
  best score 9
  best moves [(0, 0)]
====================
move (1, 0)
Turn x
.O.
...
...

score 8 best score 9
====================
move (2, 0)
Turn x
..O
...
...

score 7 best score 9
====================
move (0, 1)
Turn x
...
O..
...

score 6 best score 9
====================
move (1, 1)
Turn x
...
.O.
...

score 5 best score 9
====================
move (2, 1)
Turn x
...
..O
...

score 4 best score 9
====================
move (0, 2)
Turn x
...
...
O..

score 3 best score 9
====================
move (1, 2)
Turn x
...
...
.O.

score 2 best score 9
====================
move (2, 2)
Turn x
...
...
..O

score 1 best score 9
====================
Finished
best score 9
best moves [(0, 0)]

対戦による動作の確認

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

ai_match(ai=[ai1s, ai1])

実行結果

ai1s VS ai1
count     win    lose    draw
o       10000       0       0
x           0   10000       0
total   10000   10000       0

ratio     win    lose    draw
o      100.0%    0.0%    0.0%
x        0.0%  100.0%    0.0%
total   50.0%   50.0%    0.0%

ai1s の定義 その 4(最初の最善手を採用する)

下記の ルール 1 には ランダム性ありません

  • 左上から順空いているマス を探し、最初に見つかったマス着手 する

このような場合は、前回の記事のノートで説明したように、評価値を利用 した アルゴリズム手順 2 を「最も高い評価値 がつけられたものが 複数ある場合 は、最初にみつかったもの選択」にした アルゴリズム利用 することが できます

具体的には、下記 のような アルゴリズム処理 を行います。

  • これまでai1s異なりすべての着手評価値同じ値 にする
  • ai_by_score で計算した 最も高い評価値合法手の一覧 を表す best_moves の、先頭の要素合法手採用 する

評価値 は、すべて同じ であれば どのような値 でも 構わない ので、0設定 します。

次に、ai_by_scorebest_moves の中から ランダムな合法手選択 するか、先頭の合法手選択 するかを、選べるように改良 する 必要 があります。

ai_by_score の改良

具体的には、下記のプログラムのように、ai_by_score修正 します。

  • 5 行目最善手の一覧 の中から ランダム な着手を 選ぶか どうかを 表す 仮引数 rand追加 する。この仮引数を、デフォルト値Trueデフォルト引数 とすることで、これまでに作成した AI変更しなくても良い ようにした
  • 6、7 行目randTrue の場合は これまで通りランダム な合法手を 選択して返す 処理を行う
  • 8、9 行目randFalse の場合は、先頭の合法手 を表す 0 番の要素返す

仮引数の名前random にしなかったのは、random という 名前 の本記事でも利用している、良く使われる モジュールがある からです。

1  from copy import deepcopy
2  from random import choice
3  from ai import dprint
4
5  def ai_by_score(mb_orig, eval_func, debug=False, rand=True):
元と同じなので省略
6      if rand:   
7          return choice(best_moves)
8      else:
9          return best_moves[0]
行番号のないプログラム
from copy import deepcopy
from random import choice
from ai import dprint

def ai_by_score(mb_orig, eval_func, debug=False, rand=True):
    dprint(debug, "Start ai_by_score")
    dprint(debug, mb_orig)
    legal_moves = mb_orig.calc_legal_moves()
    dprint(debug, "legal_moves", legal_moves)
    best_score = float("-inf")
    best_moves = []
    for move in legal_moves:
        dprint(debug, "=" * 20)
        dprint(debug, "move", move)
        mb = deepcopy(mb_orig)
        x, y = move
        mb.move(x, y)
        dprint(debug, mb)
        
        score = eval_func(mb)
        dprint(debug, "score", score, "best score", best_score)
        
        if best_score < score:
            best_score = score
            best_moves = [move]
            dprint(debug, "UPDATE")
            dprint(debug, "  best score", best_score)
            dprint(debug, "  best moves", best_moves)
        elif best_score == score:
            best_moves.append(move)
            dprint(debug, "APPEND")
            dprint(debug, "  best moves", best_moves)

    dprint(debug, "=" * 20)
    dprint(debug, "Finished")
    dprint(debug, "best score", best_score)
    dprint(debug, "best moves", best_moves)
    if rand:   
        return choice(best_moves)
    else:
        return best_moves[0]
修正箇所
from copy import deepcopy
from random import choice
from ai import dprint

def ai_by_score(mb_orig, eval_func, debug=False, random=True):
元と同じなので省略
    if rand:   
        return choice(best_moves)
    else:
        return best_moves[0]

ai1s の修正

次に、ai1s を下記のプログラムのように 修正 します。

  • 3 行目評価値常に 0返す ようにする
  • 5 行目キーワード引数 rand=False記述 することで、ai_by_score先頭の合法手返す ようにする
def ai1s(mb, debug=False):
    def eval_func(mb):
        return 0

    return ai_by_score(mb, eval_func, debug=debug, rand=False)

動作の確認

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

ai_match(ai=[ai1s, ai1])

実行結果

ai1s VS ai1
count     win    lose    draw
o       10000       0       0
x           0   10000       0
total   10000   10000       0

ratio     win    lose    draw
o      100.0%    0.0%    0.0%
x        0.0%  100.0%    0.0%
total   50.0%   50.0%    0.0%

仮引数 rand の処理の確認

rand=False正しく動作するか どうかが心配な方は、下記のプログラムのように、ai1sai_by_score実引数 から、rand=False削除 した、ai1s_buggy定義 して、ai1 と対戦 すると良いでしょう。実行結果 から、通算成績勝率敗率異なる ので、rand=False によって ai_by_score処理正しく切り替わる ことが 確認 できます。

def ai1s_buggy(mb, debug=False):
    def eval_func(mb):
        return 0

    return ai_by_score(mb, eval_func, debug=debug)

ai_match(ai=[ai1s_buggy, ai1])    

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

ai1s_buggy VS ai1
count     win    lose    draw
o        5151    4455     394
x        1748    7825     427
total    6899   12280     821

ratio     win    lose    draw
o       51.5%   44.5%    3.9%
x       17.5%   78.2%    4.3%
total   34.5%   61.4%    4.1%

評価値を利用した ルール 4 で着手を行う AI の定義

次は、下記の、ルール 4 で着手を行う AI を定義 します。

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

評価値の設定

ルール 4 は、(1, 1)(0, 0)(2, 0)(0, 2)(2, 2)順番優先的着手 を行います。ルール 1 の場合と 同様の方法 でそれらのマスの 評価値設定する ことにした場合、それぞれ のマスに 着手 した 局面評価値54321 になります。

上記以外マス しか 残っていない 場合は、ランダムな着手 を行うので、それらの 評価値 は、上記の場合どの評価値 よりも 低い1 未満同じ値設定 すればよいことが分ります。そこで、その 評価値0 にすることにします。

下図 は、上記 に従って、それぞれのマス着手 した場合の局面の 評価値 です。

ai4s の定義の方法

この 評価値計算 する ai4s定義 として、先程と同様if 文 を使う方法と、テーブル を使う方法が考えられますが、どちらも プログラムの 記述がかなり面倒 です。なお、どちらも面倒ではありますが、特に難しい点はない のでプログラムの 記述は省略 します。興味と時間に余裕ががある方は実際に記述してみて下さい。また、ルール 4ランダム性がある ので 先程実装 した ai_by_scorerand=False処理利用 することは できません

そこで本記事では、計算 を使って ai4s実装 する 方法 について説明します。

真ん中四隅それ以外 のマスを 分けて処理 する 必要 があるので、その部分if 文記述 する 必要 があります。そのため、ai4s を下記のプログラムのように記述します。

  • 3、4 行目真ん中 のマスに 着手 していた場合は 5返す
  • 5、6 行目四隅 のマスに 着手 していた場合は、その 評価値式で計算 して 返す
  • 7、8 行目それ以外 の場合は 0返す

下記の 5、6 行目の日本語で記述 されている部分に、どのようなプログラムを記述す場良いかについて少し考えてみて下さい。

 1  def ai4s(mb, debug=False):
 2      def eval_func(mb):
 3          if mb.last_move == (1, 1):
 4              return 5
 5          elif 四隅のマスの場合:
 6              return 四隅の場合の評価値を計算する式
 7          else:
 8             return 0
 9
10      return ai_by_score(mb, eval_func, debug=debug)

四隅のマスであることの判定方法

単純考える のであれば、下記 のような 条件式記述 すれば良いのですが、このプログラムは 長い ので、入力が大変入力ミス可能性が高くなる などの 欠点 があります。

elif mb.last_move == (0, 0) or mb.last_move == (2, 0) or \
     mb.last_move == (0, 2) or mb.last_move == (2, 2):

このような場合は、四隅のマス座標 と、それ以外 のマスの 座標 を下記の のように 並べ四隅 のマスに あってそれ以外 のマスに ない特長を探す と良いでしょう。なお、真ん中 のマスは 最初判定済 なので 考慮 する必要は ありません

位置 座標
四隅 (0, 0)(2, 0)(0, 2)(2, 2)
それ以外 (1, 0)(0, 1)(2, 1)(1, 2)

上記をよく見ると、以下の性質 があることが分かります。

  • 四隅xy 座標 は、いずれも 0 または 2 である
  • それ以外xy 座標上記の性質満たさない

四隅を判定する式の記述方法

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

  • 3 行目着手 した 座標 から、x 座標y 座標取り出しxy代入 する
  • 6 行目x 座標y 座標いずれも 0 または 2 であることを 判定 する。or 演算子and 演算子より優先順位が低い ので、()囲う必要がある 点に 注意 すること
 1  def ai4s(mb, debug=False):
 2      def eval_func(mb):
 3          x, y = mb.last_move
 4          if mb.last_move == (1, 1):
 5              return 5
 6          elif (x == 0 or x == 2) and (y == 0 or y == 2):
 7              return 四隅の場合の評価値を計算する式
 8          else:
 9              return 0
10
11      return ai_by_score(mb, eval_func, debug=debug)
修正箇所
def ai4s(mb, debug=False):
    def eval_func(mb):
+       x, y = mb.last_move
        if mb.last_move == (1, 1):
            return 5
-       elif 四隅のマスの場合:
+       elif (x == 0 or x == 2) and (y == 0 or y == 2):
            return 四隅の場合の評価値を計算する式
        else:
            return 0

    return ai_by_score(mb, eval_func, debug=debug)

四隅を判定する式の別の記述方法

数字慣れた 方は、上記の性質一般化 した 下記の性質思いつく かもしれません。

  • 四隅xy 座標 は、いずれも偶数 である
  • それ以外xy 座標上記の性質満たさない

整数偶数あるか どうかは、2割った余り0 であるか どうかで 判定 できます。また、Python では % という 演算子 を使って、割り算余り計算 することができます。従って、ai4s は、下記のプログラムのように、より 簡潔に記述 できます。

  • 6 行目余りを計算 することで x 座標y 座標いずれも偶数 であることを 判定 する
def ai4s(mb, debug=False):
    def eval_func(mb):
        x, y = mb.last_move
        if mb.last_move == (1, 1):
            return 5
        elif x % 2 == 0 and y % 2 == 0:
            return 四隅の場合の評価値を計算する式
        else:
            return 0

    return ai_by_score(mb, eval_func, debug=debug)
修正箇所
def ai4s(mb, debug=False):
    def eval_func(mb):
        x, y = mb.last_move
        if mb.last_move == (1, 1):
            return 5
-       elif (x == 0 or x == 2) and (y == 0 or y == 2):
+       elif x % 2 == 0 and y % 2 == 0:
            return 四隅の場合の評価値を計算する式
        else:
            return 0

    return ai_by_score(mb, eval_func, debug=debug)

四隅のマスの評価値の計算

下図四隅 のマスに 着手 した局面の 評価値式で計算 するためには、xy使った式 で、下記の表 のような 評価値計算 する を考える必要があります。

 

x y 評価値
0 0 4
2 0 3
0 2 2
2 2 1

式を考える ために、xy評価値関係 を下記のように 考察 します。

  • xy共に 0 の場合の 評価値4 である
  • x2 増える と、評価値1 減る
  • y2 増える と、評価値2 減る

従って、評価値下記の式計算 できることが分かります。実際 に、上記xy の値をこの 式に代入 して 正しい評価値計算 できるかどうかを 確認 して下さい。

score = 4 - (x / 2 + y)

ai4s の定義と動作の確認

この式ai4s当てはめる と、下記のプログラムの 7 行目 のようになります。

 1  def ai4s(mb, debug=False):
 2      def eval_func(mb):
 3          x, y = mb.last_move
 4          if mb.last_move == (1, 1):
 5              return 5
 6          elif x % 2 == 0 and y % 2 == 0:
 7              return 4 - (x / 2 + y)
 8          else:
 9              return 0
10
11      return ai_by_score(mb, eval_func, debug=debug)
行番号のないプログラム
def ai4s(mb, debug=False):
    def eval_func(mb):
        x, y = mb.last_move
        if mb.last_move == (1, 1):
            return 5
        elif x % 2 == 0 and y % 2 == 0:
            return 4 - (x / 2 + y)
        else:
            return 0

    return ai_by_score(mb, eval_func, debug=debug)
修正箇所
def ai4s(mb, debug=False):
    def eval_func(mb):
        x, y = mb.last_move
        if mb.last_move == (1, 1):
            return 5
        elif x % 2 == 0 and y % 2 == 0:
-           return 四隅の場合の評価値を計算する式
+           return 4 - (x / 2 + y)
        else:
            return 0

    return ai_by_score(mb, eval_func, debug=debug)

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

ai_match(ai=[ai4s, ai4])

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

ai4s VS ai4
count     win    lose    draw
o        2520    4099    3381
x        4127    2404    3469
total    6647    6503    6850

ratio     win    lose    draw
o       25.2%   41.0%   33.8%
x       41.3%   24.0%   34.7%
total   33.2%   32.5%   34.2%

別の実装方法

上記で、四隅に着手 した際の 評価値改めて考える のが 面倒 だと思った方はいませんか?実は、四隅評価値 は、ルール 1同じ式計算 することが できます

その 理由 は、評価値を利用 する アルゴリズム では、「優先したいもの評価値 が、それより優先したくないもの評価値 よりも 大きくなる」ような 評価値 であれば、それぞれどのような評価値設定 しても 構わない からです。具体的には、四隅評価値下図左 のように、下図右ルール 1評価値同じ値設定 しても構いません。

 

ai4s の定義と動作の確認

従って、四隅評価値計算 する 式__は、ルール 1 の 評価値 を 計算 する 式 と 同じ式 を 利用 することが できる ので、ai4s__ は、下記のプログラムのように 記述 できます。

  • 7 行目四隅 のマスの 評価値計算式ai1s同じ ものにする
 1  def ai4s(mb, debug=False):
 2      def eval_func(mb):
 3          x, y = mb.last_move
 4          if mb.last_move == (1, 1):
 5              return 5
 6          elif x % 2 == 0 and y % 2 == 0:
 7              return 9 - (x + y * 3)
 8          else:
 9              return 0
10
11      return ai_by_score(mb, eval_func, debug=debug)
行番号のないプログラム
def ai4s(mb, debug=False):
    def eval_func(mb):
        x, y = mb.last_move
        if mb.last_move == (1, 1):
            return 5
        elif x % 2 == 0 and y % 2 == 0:
            return 9 - (x + y * 3)
        else:
            return 0

    return ai_by_score(mb, eval_func, debug=debug)
修正箇所
def ai4s(mb, debug=False):
    def eval_func(mb):
        x, y = mb.last_move
        if mb.last_move == (1, 1):
            return 5
        elif x % 2 == 0 and y % 2 == 0:
-           return 4 - (x / 2 + y)
+           return 9 - (x + y * 3)
        else:
            return 0

    return ai_by_score(mb, eval_func, debug=debug)

ai4s正しく動作 するかどうかを 確認 するために、ai4対戦 を行います。実行結果 から、通算成績勝率敗率異なる ので、ai4s を正しく実装 できていない ことが 確認 できました。どこに問題があるか について少し考えてみて下さい。

ai_match(ai=[ai4s, ai4])

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

ai4s VS ai4
count     win    lose    draw
o        6616    1698    1686
x        4167    2495    3338
total   10783    4193    5024

ratio     win    lose    draw
o       66.2%   17.0%   16.9%
x       41.7%   24.9%   33.4%
total   53.9%   21.0%   25.1%

バグの原因

バグの原因 は、真ん中 のマスに 着手 した場合の 評価値5 のまま だからです。そのため、上記の ai4s では、真ん中 のマスの (1, 1) の 5 より 評価値が 高い(0, 0)(2, 0)優先的着手 されてしまいます。このように、評価値を利用 する アルゴリズム では、評価値別の値修正 した際に、他の より 優先したい(または 優先したくない)ものの 評価値大小の関係変化しない ように 注意 する 必要 があります。

四隅 のマスに着手した際の 評価値先ほど のように 修正 した場合は、より優先 する 真ん中(1, 1)評価値 を、四隅評価値最大値 である 9 より大きな値設定し直す 必要があります。9 より 大きければ どのような値でも 構わない ので 10設定 することにします。なお、真ん中四隅以外 のマスの 評価値0 のまま問題はありません

下図は、修正後各マス評価値 を表します。

評価値修正 を行う際に、先程のような バグが発生 することを 避ける ためには、上記のような 図を作成 すると良いでしょう。他の方法としては、下記の のように、優先したい条件順番評価値範囲表にしたもの作製 して、優先したいものに 対して、より大きな評価値設定 されることが 確認できる ようにすると良いでしょう。

条件 評価値の範囲
真ん中のマスに着手 10
四隅のマスに着手 1 ~ 9
それ以外のマスに着手 0

他にも、評価値修正 した際に、大小の関係簡単変化しない ように、それぞれ条件評価値の範囲間隔大きめに設定 するという方法があります。例えば、真ん中 のマスの着手に対する 評価値100設定 することで、今後、四隅 のマスの着手に対する 評価値多少変えた場合 に、間違って 100 より大きくなる可能性低く なります。ただし、この方法は決して 完璧ではない ので、過信せず に、評価値修正 した場合は、必ず 全体の評価値値を確認 することを 強くお勧めします

ai4s の修正と動作の確認

修正 した 評価値計算 する ai4s は、下記のプログラムのように 記述 できます。

  • 5 行目(1, 1) のマスの 評価値10 にする
 1  def ai4s(mb, debug=False):
 2      def eval_func(mb):
 3          x, y = mb.last_move
 4          if mb.last_move == (1, 1):
 5              return 10
 6          elif x % 2 == 0 and y % 2 == 0:
 7              return 9 - (x + y * 3)
 8          else:
 9              return 0
10
11      return ai_by_score(mb, eval_func, debug=debug)
行番号のないプログラム
def ai4s(mb, debug=False):
    def eval_func(mb):
        x, y = mb.last_move
        if mb.last_move == (1, 1):
            return 10
        elif x % 2 == 0 and y % 2 == 0:
            return 9 - (x + y * 3)
        else:
            return 0

    return ai_by_score(mb, eval_func, debug=debug)
修正箇所
def ai4s(mb, debug=False):
    def eval_func(mb):
        x, y = mb.last_move
        if mb.last_move == (1, 1):
-           return 10
+           return 5
        elif x % 2 == 0 and y % 2 == 0:
            return 9 - (x + y * 3)
        else:
            return 0

    return ai_by_score(mb, eval_func, debug=debug)

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

ai_match(ai=[ai4s, ai4])

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

ai4s VS ai4
count     win    lose    draw
o        2499    4214    3287
x        4196    2414    3390
total    6695    6628    6677

ratio     win    lose    draw
o       25.0%   42.1%   32.9%
x       42.0%   24.1%   33.9%
total   33.5%   33.1%   33.4%

評価値を利用したアルゴリズムの利点

評価値を利用 した アルゴリズム利点 をいくつか紹介します。他の利点に関しては、その利点を活かした例が出てきた時に説明します。

評価値関数を記述するだけで、AI を定義できる

以前の記事定義 した、評価値を利用 した アルゴリズムひな形 となる ai_by_score定義 する事で、評価関数定義 するだけで、AI の関数定義できる ようになります。

ランダムな選択を行うことができる

以前の記事で説明した、条件を満たす着手が複数ある場合の問題点 を解消することができます。具体的には、最大評価値 を持つものが 複数 あった場合に、その中から ランダムに選択 する処理を行うことができます。

ルールの条件の優先順位を柔軟に変えることができる

以前の記事で説明した、条件を判定する順番に関する問題点 を解消することができます。

例えば、ルール 4 は、真ん中 のマス、 のマスの 優先順位着手 を行いますが、その 優先順位 を、 のマス、真ん中 のマスの 優先順位変える 場合に、「それぞれの条件 を 順番に判定 し、最初 にみつかった 条件を満たす着手選択 する」というアルゴリズムでは、下記の ai4 のプログラムを、その下 のプログラムのように、真ん中処理 を行うプログラムの 記述順番入れ替える 必要があります。


    # 真ん中のマスの場合の処理
    if mb.board[1][1] == Marubatsu.EMPTY:
        return 1, 1
    # 隅のマスの場合の処理
    for y in range(0, 3, 2):
        for x in range(0, 3, 2):
            if mb.board[x][y] == Marubatsu.EMPTY:
                return x, y

修正後のプログラム


    # 隅のマスの場合の処理
    for y in range(0, 3, 2):
        for x in range(0, 3, 2):
            if mb.board[x][y] == Marubatsu.EMPTY:
                return x, y
    # 真ん中のマスの場合の処理
    if mb.board[1][1] == Marubatsu.EMPTY:
        return 1, 1

一方、評価値を利用 する アルゴリズム の場合は、計算する 評価値 を、下記の表 のように 変える ことで、条件優先順位変えるこ とができます。なお、下記の表では 四隅評価値101 ~ 109 にしましたが、10 より多ければ どのような値にしても構いません。

条件 評価値の範囲
四隅のマスに着手 101 ~ 109
真ん中のマスに着手 10
それ以外のマスに着手 0

上記の評価値計算 するために 必要ai4s修正 は、下記のプログラムの 7 行目 のように、四隅評価値100 を足すだけ簡単 に行えます。ただし、下記のプログラムは、ルール 4異なる処理 を行うので、関数の名前は ai4s_fake2としました。

 1  def ai4s_fake(mb, debug=False):
 2      def eval_func(mb):
 3          x, y = mb.last_move
 4          if mb.last_move == (1, 1):
 5              return 10
 6          elif x % 2 == 0 and y % 2 == 0:
 7              return 9 - (x + y * 3) + 100
 8          else:
 9              return 0
10
11      return ai_by_score(mb, eval_func, debug=debug)
行番号のないプログラム
def ai4s_fake(mb, debug=False):
    def eval_func(mb):
        x, y = mb.last_move
        if mb.last_move == (1, 1):
            return 10
        elif x % 2 == 0 and y % 2 == 0:
            return 9 - (x + y * 3) + 100
        else:
            return 0

    return ai_by_score(mb, eval_func, debug=debug)
修正箇所
-def ai4(mb, debug=False):
+def ai4s_fake(mb, debug=False):
    def eval_func(mb):
        x, y = mb.last_move
        if mb.last_move == (1, 1):
            return 10
        elif x % 2 == 0 and y % 2 == 0:
-           return 9 - (x + y * 3)
+           return 9 - (x + y * 3) + 100
        else:
            return 0

    return ai_by_score(mb, eval_func, debug=debug)

なお、ai4s_fake は、ルール 4 とは 異なる処理 を行うので、ai4 との 対戦は行いません

評価値を利用したアルゴリズムの欠点

評価値を利用 した アルゴリズム は、良いことしかないように思えるかもしれませんが、以下のような 欠点 があるので、その点には 注意 して下さい。

処理に時間がかかる場合がある

評価値を利用 した アルゴリズム は、最大評価値 を持つものを 調べる必要 があるので、必ず すべてのデータ評価値計算 する 必要 があります。一方、「それぞれの条件を順番に判定し、最初にみつかった条件を満たす着手を選択する」アルゴリズムでは、条件を満たす ものが 見つかった時点処理を終える ことができます。そのため、ルールによっては、評価値を利用 した アルゴリズム のほうが、処理時間がかかる場合 があります。

例えば、ai1ai1s は、同じルール 手を行う AI ですが、ai1s のほうが 処理時間がかかります。そのことは下記の 2 つのプログラム実行 することで 確認 できます。

下記の、ai1 どうし10000 回対戦 を行う処理は、筆者のパソコンでは 約 2.5 秒 かかりました。なお、実行結果はこれまでと同じなので省略します。

ai_match(ai=[ai1, ai1])

一方、下記の、ai1s どうし10000 回対戦 を行う処理は、筆者のパソコンでは 約 32.7 秒 のように、約 10 倍以上 の時間がかかりました。省略しますが、ai2sai3sai4s も、対応する AI の 約 10 倍処理時間 がかかります。興味がある方は実行してみて下さい。

ai_match(ai=[ai1s, ai1s])

実は、ai1ai1s約 10 倍以上処理時間異なる のは、ai1 で行う 処理 が、特定のマス空いているかどうか調べている のに対し、ai1s では、合法手着手 を行う際に Marubatsu クラスインスタンス深いコピー作成 するという、時間がかかる処理 を行っているからです。ルール 567 のように、着手を行う必要 がある 処理 の場合は、それほど 時間の差生じない ので、一般的 には 評価値を利用 した アルゴリズム処理速度大きく異なる ことは ありません。そのことは、次回の記事確認 します。

評価値の設定の間違いに関する欠点

評価値を利用 する アルゴリズム では、確かに評価値を変えることで、条件の優先順位を柔軟に変えることができますが、評価値設定間違える と、先程 ai4s の四隅の評価値を変えた時にバグが発生したように、簡単間違った処理 が行われます。また、間違っていること気づきにくい間違った場合原因を発見しづらい という 欠点 があります。

今回の記事のまとめ

今回の記事では、評価値を利用 した アルゴリズム での、評価値設定方法 と、計算方法 について 説明 し、ルール 14AI評価値を利用 した アルゴリズム で定義しました。

評価値を利用 した アルゴリズム では、「優先したいもの評価値 が、それより優先したくないもの評価値より大きくなる」という 条件を満たせば評価値どのような値設定 しても かまいません

このように、評価値を利用 した アルゴリズム では、評価値柔軟に設定 できますが、逆に言えば、間違った評価値簡単に設定できてしまう ため、その点に 気をつけないと簡単にバグが発生 してしまう点に 注意 する 必要 があります。

次回の記事では、評価値を利用 した アルゴリズム で、残りの ルール 5 ~ 7実装 する方法について説明します。

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

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

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

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

次回の記事

  1. 詳細は割愛しますが、インスタンスのハッシュ値を計算する処理をクラスの定義の中に、自分で記述することができます 2

  2. fake は、偽物という意味の英語です

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?