今回の目標
前回までで、ランダム・探索・機械学習・深層学習でのオセロが完成しました。
今回はそれらを対戦させ性能評価をしていきます。
ここから本編
まずエントリー選手を紹介します。
- random ランダムに石を置きます。
- 5hand 5ターン先で最も自分の石の数が大きくなる場所に置きます。
- 5hand_custom 5ターン先で最も評価値が高くなる場所に置きます。
- 2least 次の次の相手のターンで最も相手の手数が少なくなる場所に置きます。
- 2most 次の次の自分のターンで最も自分の手数が多くなる場所に置きます。
- Ridge 機械学習モデルで次のターンの盤面から最終結果を予測し、その予測値が最も高かった場所に置きます。
- NN 深層学習モデルで次のターンの盤面から最終結果を予測し、その予測値が最も高かった場所に置きます。
クラス構造
クラスの継承が複雑になってきたのでまずはそれについて説明します。
図に表すと下のようになっています。
各クラスについても説明します。
- osero オセロをするための基本クラス。盤面を表す変数などを格納。
- model_run 機械学習モデルを動かすためのクラス。
- osero_AI 機械学習モデルに従って石を置くメソッドを置いたクラス。
- osero_deep_AI 深層学習モデルに従って石を置くメソッドを置いたクラス。
- battle 上述したエントリー選手たちを戦わせるためのクラス
また、このbattleクラスを用いてrun.ipynbで実際に対戦を行い、analyse.ipynbでデータの分析を行います。
この記事ではbattleクラスを保存しているbattle.pyと、上述した2つのipynbファイルのみ詳しく記載します。
なお、名前がかぶってしまったため、osero_AI内のreshapeメソッドをdict_to_DataFrameに、osero_deep_AI内のpredictメソッドをforwardに、reshapeメソッドをdict_to_ndarrayに改名しています。
battle.py
上で説明した通り、battleクラスを持つファイルです。
インポート・宣言部分
上述したクラスをインポートし、機械学習モデルが保存されているディレクトリ名を指定し、機械学習に必要な配列を定義し、評価値を定義しています。
from BitBoard import osero
from osero_AI import osero_AI as Ridge
from osero_deep_AI import osero_deep_AI as NN
dir_name = "model"
turn_vari = [i for i in range(1, 61)]
alpha_arr = [i for i in range(3, 13)]
eva = [1 for i in range(64)]
eva_custom = [
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
]
コンストラクタ
oseroクラスのコンストラクタを呼ぶとともに、機械学習と深層学習を思考方法として追加しています。
また、それぞれの学習済みモデルのセットアップも行います。
class battle(Ridge, NN):
def __init__(self):
osero.__init__(self, 0, 0)
osero.PLAY_WAY["Ridge"] = len(osero.PLAY_WAY)
self.think.append(self.osero_AI)
osero.PLAY_WAY["NN"] = len(osero.PLAY_WAY)
self.think.append(self.osero_deep_AI)
Ridge.model_setup(self, dir_name, turn_vari, alpha_arr)
NN.model_setup(self)
play
機械学習用にturn_numという変数を使っている以外はいつも通りのplayメソッドです。
def play(self) -> list:
can, old_can = True, True
can = self.check_all()
self.turn_num = 0
while can or old_can:
if can:
self.turn_num += 1
if self.turn:
self.think[self.black_method]()
else:
self.think[self.white_method]()
self.turn = not self.turn
old_can = can
can = self.check_all()
return self.count_last()
count_last
試合結果を出力するメソッド。
def count_last(self) -> list:
black = self.popcount(self.bw["b_u"])\
+ self.popcount(self.bw["b_d"])
white = self.popcount(self.bw["w_u"])\
+ self.popcount(self.bw["w_d"])
return [black, white, int(black > white), int(white > black)]
run.ipynb
インポート・定義部分
battleをインポート、各手法を定義し、結果を格納する辞書を作成しました。
import re
import pandas as pd
import battle
run = battle.battle()
################################
methods_name = [
"random",
"5hand",
"5hand_custom",
"2least",
"2most",
"Ridge",
"NN"
]
results = {
"black_method": [],
"white_method": [],
"black_score": [],
"white_score": [],
"black_win": [],
"white_win": []
}
対戦部分
本当は同じ手法同士の対戦を除き、総当たりで10回ずつ行う予定でしたがあまりに時間がかかりすぎるので取りやめ、1回ずつとしました。
上で定義した思考方法をrunオブジェクト内で設定し、その後盤面の初期化と試合、試合結果の保存を行っています。
# for i in range(10):
# print("\r%d/%d" % (i + 1, 10), end="")
i = 1
for black in methods_name:
print("\r%d/%d" % (i, len(methods_name)))
if re.search(r"hand_custom", black):
run.black_method = run.PLAY_WAY["nhand_custom"]
run.eva[1] = battle.eva_custom
elif re.search(r"hand", black):
run.black_method = run.PLAY_WAY["nhand"]
run.eva[1] = battle.eva
elif re.search(r"least", black):
run.black_method = run.PLAY_WAY["nleast"]
elif re.search(r"most", black):
run.black_method = run.PLAY_WAY["nmost"]
else:
run.black_method = run.PLAY_WAY[black]
try:
b_num = int(re.match(r"\d", black).group())
except:
b_num = 1
run.read_goal[1] = b_num
for white in methods_name:
if black == white:
continue
if re.search(r"hand_custom", white):
run.white_method = run.PLAY_WAY["nhand_custom"]
run.eva[0] = battle.eva_custom
elif re.search(r"hand", white):
run.white_method = run.PLAY_WAY["nhand"]
run.eva[0] = battle.eva
elif re.search(r"least", white):
run.white_method = run.PLAY_WAY["nleast"]
elif re.search(r"most", white):
run.white_method = run.PLAY_WAY["nmost"]
else:
run.white_method = run.PLAY_WAY[white]
try:
w_num = int(re.match(r"\d", white).group())
except:
w_num = 1
run.read_goal[0] = w_num
run.setup()
results_ele = run.play()
results["black_method"].append(black)
results["white_method"].append(white)
results["black_score"].append(results_ele[0])
results["white_score"].append(results_ele[1])
results["black_win"].append(results_ele[2])
results["white_win"].append(results_ele[3])
i += 1
データ保存部分
上で記録したデータを保存しています。
results_df = pd.DataFrame(results)
results_df.to_csv("data.csv")
analyse.ipynb
run.ipynbで作成したデータを見てみます。
なお、試合数42ということで、あまり客観的なデータは得られないとは思いますので参考程度。
たった42試合ですが一時間以上かかりました、おそらくRidgeと探索に時間がかかったためでしょう。
import pandas as pd
data = pd.read_csv("data.csv")
methods_name = data["black_method"].unique()
################################
def plot(x, y, xlabel, ylabel, fig_name, dir_name):
fig = plt.figure(figsize=(10, 10))
plt.bar(x, y)
plt.xlabel(xlabel)
plt.ylabel(ylabel)
plt.title(fig_name)
plt.savefig(dir_name + "/" + fig_name)
plt.clf()
plt.close()
勝ち星の数
y = []
for method in methods_name:
num = 0
num += len(data.query("black_method=='%s'" % method).query("black_win==1"))
num += len(data.query("white_method=='%s'" % method).query("white_win==1"))
y.append(num)
print("%s win num:\t%d" % (method, num))
plot(method, y, "method name", "win num", "win num each method name", "fig")
実行結果はこちら。
random win num: 4
5hand win num: 8
5hand_custom win num: 7
2least win num: 10
2most win num: 2
Ridge win num: 6
NN win num: 3
期待していたNNはワースト二位でした。
トップを誇ったのは2least。
負け星の数
負け星の数も求めました。
全部で七手法が、自分以外の全員と戦っているので黒の時6試合+白の6試合で12試合ずつ行っています。そのため勝ち星と足したときに12になっています。
プログラムは省略して結果のみ載せます。
random lose num: 8
5hand lose num: 4
5hand_custom lose num: 5
2least lose num: 2
2most lose num: 10
Ridge lose num: 6
NN lose num: 9
こちらもNNは2mostに続くワースト2位。
leastとmostで両極端な戦績となりました。
獲得石数
最終的に得られた石の数の合計を比較してみました。
y = []
for method in methods_name:
num = 0
num += data.query("black_method=='%s'" % method)["black_score"].sum()
num += data.query("white_method=='%s'" % method)["white_score"].sum()
y.append(num)
print("%s score sum:\t%d,\tmean:\t%.4f" % (method, num, num / 12))
plot(methods_name, y, "method name", "score num", "score num each method name", "fig")
random score sum: 335, mean: 27.9167
5hand score sum: 442, mean: 36.8333
5hand_custom score sum: 408, mean: 34.0000
2least score sum: 505, mean: 42.0833
2most score sum: 321, mean: 26.7500
Ridge score sum: 395, mean: 32.9167
NN score sum: 281, mean: 23.4167
こちらは勝ち星ほどの差はありませんでした。
気になったのは2mostですね、勝ち星は最も少ないですが石の獲得数ではNNを上回っています。それ以外はほぼ、勝ち星と獲得石数は比例した関係にあるといえそうです。randomも勝ち星こそ少ないものの石の獲得数はそんなに悪くありませんでした。
まとめ
相手の手数を奪うという戦法は非常に有効であることがわかりました。
また、5hand_customが5handよりも勝利数・獲得石数ともに負けていたことも意外でした。
方針から考え直す必要がありそうです。
フルバージョン
機械学習モデルは量が多すぎるためアップロードしませんでした。
29と全く同じものを使用しています。
次回は
今回で課題も見つかりましたが、ひとまず置いておいて、画像処理でオセロAIを作ってみたいと思います。