今回の目標
前回作成した機械学習モデルに従うAIを作り、対戦します。
ここから本編
今回の手順を簡単に書くと以下の通りです。
- ターン数ごとに、alpha別に学習したモデルを保存する
- モデルをロードし、nhandの要領で打ち返す
まず「ターン数ごとに、alpha別に学習したモデルを保存する」ですが、今まで5ターンごとにデータを収集し学習させていました。しかし、例えば5ターン目の時はそこから置く場所を探しますので、10ターン目のモデルを用いるため5ターン先まで探索しなければならなくなります。Pythonはc言語と違い低速ですし、しかもモデルから予測値をもらいながら5ターン先まで探索というのは現実的ではありません。そこで、毎ターンデータを収集して学習させることで、1ターン先の予測値を用いて駒を置くことにしました。これによってスムーズな対戦が可能になります。
次に「モデルをロードし、nhandの要領で打ち返す」ですが、具体的に
- 今の盤面から置ける場所すべてをピックアップする
- 置ける場所があった際、実際に置いてみて、その盤面から最終結果を予測する
- 最終結果の予測値が最も高い位置に置く
という手順を踏んでいます。
今回使用するファイルは以下の通りです。
- BitBoard.py オセロをするための基本クラス。
- osero_learn.py オセロの対戦データを集めるためのクラス。
- run.ipynb 学習を行い、モデルを保存するプログラム。
- model_run.py モデルをロードし、使うためのクラス。
- osero_AI.py AIと対戦するためのクラス。
BitBoard.py
コンストラクタにミスがあったため修正しました。
最後のif文の「self.eva[?]」の「?」に入る数字を逆にしていました。
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ターンごとになったこと、またモデルの保存操作が追加されたこと、性能評価のプログラムがなくなったこと以外は特に変化ありませんが、一応全文載せます。
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
モデルをロードし、実行するクラス。
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を宣言します。
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を追加し、モデルセットアップを行います。
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が返されます。
def search_place(self) -> int:
for turn_num in turn_vari:
if self.turn_num < turn_num:
return turn_num
return -1
reshape
調べたい盤面を、学習済みモデルに投げられるよう成形して返す関数です。
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での予測値の平均を返す関数です。
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での予測値の平均を返す関数だと思ってください。
アルゴリズムは以下の通り。
- 最終ターンか調べる。最終ターンならランダムに置いて終了。
- 盤面上のすべての場所について、置けるか調べる。
- もし置けるなら、試しに置いてみて、そこでの予測値を調べる。
- 予測値が最大になる場所をキープしておく。
- 予測値が最大になる場所が複数存在する場合、その中からランダムに選ぶ。
- 置く場所を出力する。
- 置く。
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での予測値の平均を返す関数です。
今回は一手先の予測値しか見ないので最初の四行ほどしか使いません。
やっていることは、求めたいターンにたどり着くまで、置ける場所に置いていき新たな盤面を作ることです。そして求めたいターンにたどり着いた時、そこでの予測値を返します。
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
オセロをプレイするための関数。
現在のターン数などを知る必要があるため、作り直しました。
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()
実行部分
ゲーム実行部分です。
引数を変えることでいろいろな条件での試合ができます。
if __name__ == "__main__":
a = osero_AI("model", player=BLACK, player_method=osero.PLAY_WAY["human"])
a.play()
実際にやってみた
正直あまり強くなかったです。
数試合しかしていませんが、全勝しました。
random相手なら白星が多かったです。
フルバージョン
次回は
深層学習で同じことをやってみたいと思います。