コンピュータとオセロ対戦29　～勝敗予測AIと対戦～

https://qiita.com/tt_and_tk/items/068f2afde6db637e189f

[前回](https://qiita.com/tt_and_tk/items/35ba790f0f285b170bb9)

# 今回の目標
[前回](https://qiita.com/tt_and_tk/items/35ba790f0f285b170bb9)作成した機械学習モデルに従うAIを作り、対戦します。
# ここから本編
今回の手順を簡単に書くと以下の通りです。

1. ターン数ごとに、alpha別に学習したモデルを保存する
1. モデルをロードし、nhandの要領で打ち返す

まず「ターン数ごとに、alpha別に学習したモデルを保存する」ですが、今まで5ターンごとにデータを収集し学習させていました。しかし、例えば5ターン目の時はそこから置く場所を探しますので、10ターン目のモデルを用いるため5ターン先まで探索しなければならなくなります。Pythonはc言語と違い低速ですし、しかもモデルから予測値をもらいながら5ターン先まで探索というのは現実的ではありません。そこで、毎ターンデータを収集して学習させることで、1ターン先の予測値を用いて駒を置くことにしました。これによってスムーズな対戦が可能になります。

次に「モデルをロードし、nhandの要領で打ち返す」ですが、具体的に

1. 今の盤面から置ける場所すべてをピックアップする
1. 置ける場所があった際、実際に置いてみて、その盤面から最終結果を予測する
1. 最終結果の予測値が最も高い位置に置く

という手順を踏んでいます。

今回使用するファイルは以下の通りです。

* BitBoard.py　オセロをするための基本クラス。
* osero_learn.py　オセロの対戦データを集めるためのクラス。
* run.ipynb　学習を行い、モデルを保存するプログラム。
* model_run.py　モデルをロードし、使うためのクラス。
* osero_AI.py　AIと対戦するためのクラス。

## BitBoard.py
コンストラクタにミスがあったため修正しました。
最後のif文の「self.eva[?]」の「?」に入る数字を逆にしていました。

```BitBoard.py
    def __init__(self, black_method, white_method,\
                 seed_num=0, read_goal=[1, 1], eva=[0, 0]):
        self.think = [\
            self.human,
            self.random,
            self.nhand,
            self.nhand_custom,
            self.nleast,
            self.nmost
        ]
        if (black_method == osero.PLAY_WAY["nhand_custom"]\
            or white_method == osero.PLAY_WAY["nhand_custom"])\
            and eva is None:
            raise ValueError("designate eva")
        self.black_method = black_method
        self.white_method = white_method
        self.read_goal = read_goal
        self.eva = eva
        if black_method == osero.PLAY_WAY["nhand"]:
            self.eva[1] = [1] * 64
        if white_method == osero.PLAY_WAY["nhand"]:
            self.eva[0] = [1] * 64
#         seed(seed_num)
        self.setup()
```

## osero_learn.py
変更点がないため省略。

## run.ipynb
学習するのが1ターンごとになったこと、またモデルの保存操作が追加されたこと、性能評価のプログラムがなくなったこと以外は特に変化ありませんが、一応全文載せます。

```run.py
from copy import deepcopy

import pandas as pd

from BitBoard import osero
from osero_learn import learn

PLAY_WAY = deepcopy(osero.PLAY_WAY)
del PLAY_WAY["human"]
PLAY_WAY = PLAY_WAY.values()

################################

eva = [[
     1.0, -0.6,  0.6,  0.4,  0.4,  0.6, -0.6,  1.0,
    -0.6, -0.8,  0.0,  0.0,  0.0,  0.0, -0.8, -0.6,
     0.6,  0.0,  0.8,  0.6,  0.6,  0.8,  0.0,  0.6,
     0.4,  0.0,  0.6,  0.0,  0.0,  0.6,  0.0,  0.4,
     0.4,  0.0,  0.6,  0.0,  0.0,  0.6,  0.0,  0.4,
     0.6,  0.0,  0.8,  0.6,  0.6,  0.8,  0.0,  0.6,
    -0.6, -0.8,  0.0,  0.0,  0.0,  0.0, -0.8, -0.6,
     1.0, -0.6,  0.6,  0.4,  0.4,  0.6, -0.6,  1.0
] for i in range(2)]

################################

df = pd.DataFrame({})

check_point = [i for i in range(1, 61)]

for i in range(10):
    print("\r[" + "#" * (i+1) + " " * (10-i+1) + "]", end="")
    for black in PLAY_WAY:
        for white in PLAY_WAY:
            run = learn(\
                black,
                white,
                check_point=check_point,
                seed_num=i,
                eva=eva
            )
            data = run.play()
            df = df.append(data, ignore_index=True)

print("\r[" + "#" * 10 + "]")

################################

from sklearn.model_selection import train_test_split
from sklearn.linear_model import Ridge

x_data = df.drop(["turn", "last_score"], axis=1)
y_data = df[["turn_num", "last_score"]]

turn_vari = df["turn_num"].unique()

################################

model = []
alpha_arr = [i for i in range(3, 13)]

for turn_num in turn_vari:
    model.append([])

    for alpha in alpha_arr:
        x_train, x_test, y_train, y_test = train_test_split(\
            x_data.query("turn_num==%d" % turn_num).drop("turn_num", axis=1),
            y_data.query("turn_num==%d" % turn_num).drop("turn_num", axis=1),
            random_state=0
        )

        model[-1].append(Ridge(\
            alpha=alpha,
            solver="lbfgs",
            positive=True,
            random_state=0
        ))
        model[-1][-1].fit(x_train, y_train)

################################

import pickle

for turn_num in range(len(turn_vari)):
    for alpha in range(len(alpha_arr)):
        with open(\
            "model/model%2d%2d.pickle" % (turn_vari[turn_num], alpha_arr[alpha]),
            mode="wb"
        ) as fp:
            pickle.dump(model[turn_num][alpha], fp)
```

## model_run.py
モデルをロードし、実行するクラス。

```model_run.py
import pickle

class model_run:
    def __init__(self):
        self.model = {}
    
    def model_setup(self, folda: str, turn_vari: list,\
                    alpha_arr: list) -> None:
        self.model = {}
        for turn_num in turn_vari:
            self.model[str(turn_num)] = {}
            for alpha in alpha_arr:
                with open(\
                    "%s/model%2d%2d.pickle" % (folda, turn_num, alpha),
                    mode="rb"
                ) as fp:
                    self.model[str(turn_num)][str(alpha)] = pickle.load(fp)
    
    def predict(self, turn_num: int, alpha: int, now_board: dict) -> int:
        predict = self.model[str(turn_num)][str(alpha)].predict(now_board)
        return predict[0][0]

```

## osero_AI.py
AIとの対戦を実装したクラス。

### 変数宣言など
turn_variやalpha_arrを宣言します。

```osero_AI.py
from copy import deepcopy
from random import randint

from pandas import DataFrame

from BitBoard import osero
from model_run import model_run

BLACK = 0
WHITE = 1

turn_vari = [i for i in range(1, 61)]
alpha_arr = [i for i in range(3, 13)]
```

### コンストラクタ
オセロのための基本クラスと、上述したモデル実行のためのクラスを継承しています。
基本クラス内の思考方法をまとめたクラスにAIを追加し、モデルセットアップを行います。

```osero_AI.py
class osero_AI(osero, model_run):
    def __init__(self, folda: str, player=BLACK, read_goal=[1, 1], eva=[0, 0],\
                 player_method=osero.PLAY_WAY["human"]):
        osero.PLAY_WAY["osero_AI"] = 6
        if player == BLACK:
            black_method = player_method
            white_method = osero.PLAY_WAY["osero_AI"]
        else:
            black_method = osero.PLAY_WAY["osero_AI"]
            white_method = player_method
        super().__init__(\
            black_method,
            white_method,
            read_goal=read_goal,
            eva=eva
        )
        self.think.append(self.osero_AI)
        self.model_setup(folda, turn_vari, alpha_arr)
```

### search_place
現在のターン数から、予測するためのターン数を割り出す関数です。
もともと5ターンごとにしかモデルを作成していなかったため必要なものでした。
今回は1ターンごとなので必要ないですが、今後必要になるかもしれませんので残しました。
最終ターンで調べるときは-1が返されます。

```osero_AI.py
    def search_place(self) -> int:
        for turn_num in turn_vari:
            if self.turn_num < turn_num:
                return turn_num
        
        return -1
```

### reshape
調べたい盤面を、学習済みモデルに投げられるよう成形して返す関数です。

```osero_AI.py
    def reshape(self, now_board: dict) -> DataFrame:
        data = {}
        if self.turn:
            my = ["b_u", "b_d"]
            opp = ["w_u", "w_d"]
        else:
            my = ["w_u", "w_d"]
            opp = ["b_u", "b_d"]
        for i in range(64):
            data["my%d" % i] = [int((now_board[my[i >= 32]] & (1 << i)) != 0)]
            data["opp%d" % i] = [int((now_board[opp[i >= 32]] & (1 << i)) != 0)]

        return DataFrame(data)
```

### predict_mean
それぞれのalphaでの予測値の平均を返す関数です。

```osero_AI.py
    def predict_mean(self, now_board: dict, read_turn: int) -> float:
        score = 0
        x = self.reshape(now_board)

        for alpha in alpha_arr:
            score += self.predict(\
                read_turn,
                alpha,
                x
            )
        
        return score / len(alpha_arr)
```

### osero_AI
思考を行う関数です。
ここでAI_think関数は、各alphaでの予測値の平均を返す関数だと思ってください。

アルゴリズムは以下の通り。

1. 最終ターンか調べる。最終ターンならランダムに置いて終了。
1. 盤面上のすべての場所について、置けるか調べる。
1. もし置けるなら、試しに置いてみて、そこでの予測値を調べる。
1. 予測値が最大になる場所をキープしておく。
1. 予測値が最大になる場所が複数存在する場合、その中からランダムに選ぶ。
1. 置く場所を出力する。
1. 置く。

```osero_AI.py
    def osero_AI(self) -> None:
        search_place = self.search_place()
        if search_place == -1:
            self.random()
            return
        
        max_score = -100
        line_ans, col_ans = [-1], [-1]
        place_num = 0
        
        for i in range(8):
            for j in range(8):
                if not self.check(i, j, self.bw, self.turn):
                    continue
                board_leaf = deepcopy(self.bw)
                self.put(i, j, board_leaf, self.turn)
                score =self.AI_think(\
                    board_leaf,
                    not self.turn,
                    self.turn_num + 1,
                    search_place
                )
                if score > max_score:
                    max_score = score
                    place_num = 0
                    line_ans = [i]
                    col_ans = [j]
                elif score == max_score:
                    place_num += 1
                    line_ans.append(i)
                    col_ans.append(j)
        
        if place_num:
            place = randint(0, place_num)
            line_ans[0] = line_ans[place]
            col_ans[0] = col_ans[place]
        
        print("line:\t%d\ncol:\t%d" % (line_ans[0]+1, col_ans[0]+1))
        self.put(line_ans[0], col_ans[0], self.bw, self.turn)
```

### AI_think
各alphaでの予測値の平均を返す関数です。
今回は一手先の予測値しか見ないので最初の四行ほどしか使いません。
やっていることは、求めたいターンにたどり着くまで、置ける場所に置いていき新たな盤面を作ることです。そして求めたいターンにたどり着いた時、そこでの予測値を返します。

```osero_AI.py
    def AI_think(self, now_board: dict, turn: bool,\
                 turn_num: int, read_goal: int) -> float:
        if turn_num == read_goal:
            return self.predict_mean(now_board, read_goal)
        
        score = 0
        place_num = 0
        for i in range(8):
            for j in range(8):
                if not self.check(i, j, now_board, turn):
                    continue
                place_num += 1
                board_leaf = deepcopy(now_board)
                self.put(i, j, board_leaf, turn)
                score += self.AI_think(\
                    board_leaf,
                    not turn,
                    turn_num + 1,
                    read_goal
                )
        
        if place_num:
            return score / place_num
        else:
            return self.predict_mean(now_board, read_goal)
```

### play
オセロをプレイするための関数。
現在のターン数などを知る必要があるため、作り直しました。

```osero_AI.py
    def play(self) -> None:
        can, old_can = True, True
        
        self.printb()
        self.turn_num = 0
        
        can = self.check_all()
        while can or old_can:
            if can:
                self.turn_num += 1
                if self.turn:
                    print("black turn")
                    self.think[self.black_method]()
                else:
                    print("white turn")
                    self.think[self.white_method]()
                self.printb()
            self.turn = not self.turn
            old_can = can
            can = self.check_all()
            
        self.count_last()
```

### 実行部分
ゲーム実行部分です。
引数を変えることでいろいろな条件での試合ができます。

```osero_AI.py
if __name__ == "__main__":
    a = osero_AI("model", player=BLACK, player_method=osero.PLAY_WAY["human"])
    a.play()
```

# 実際にやってみた
正直あまり強くなかったです。
数試合しかしていませんが、全勝しました。
random相手なら白星が多かったです。
# フルバージョン

https://github.com/TTaisei/osero/tree/main/mecha_learn
# 次回は
深層学習で同じことをやってみたいと思います。

[次回](https://qiita.com/tt_and_tk/items/eb2a0389626b160f32f0)

https://qiita.com/tt_and_tk/items/068f2afde6db637e189f