【お勉強】ポケモン対戦の自動シミュレーションと勝敗予測
お勉強記事に近い。けどせっかく分かりやすい結果があるので軽くブログを書いてみる。
最近流行りの機械学習(ただのマイブーム)を使って、ポケモンの組み合わせからシングルバトルの勝敗を予測する予測器を作成してみたお話です。
まだ実用性のあるものはできていませんが…
背景
Percymonというポケモンの簡単なAIを使って対面相性の計算や、選出予測などをしていた。
今まではとりあえず1匹vs1匹 の対面相性値だけ計算して、その他(PTバランスとか)は独自でアルゴリズムを考えてやりくりしていたのだが、普通に3匹vs3匹でシミュレーション対戦させまくればいいんじゃね?と思ったので対戦データの収集と、機械学習を使った予測をやろうと思った。
対戦データの収集
(1)あらかじめ型を定義したポケモンリストの中からランダムで3匹ずつ選ぶ
(2)Percymon上で、[自ポケ1, 自ポケ2, 自ポケ3] vs [相手ポケ1, 相手ポケ2, 相手ポケ3]の対戦インスタンスを作る。
(3)対戦インスタンスの各ターンで、自分視点のMinimax計算を行う
(4)Minimax計算の結果からお互いの最善手を選び実行する。
(5)以上を対戦が終了するまで繰り返し
(6)勝敗データをPTポケモンの情報とともにDB保存
(7)以上を気が済むまで自動で繰り返し
PTバランス計算用にPostgresSQL(DBソフトウェアのことです)にポケモンの種族・型のデータ構造を作ってあったので、勝敗データ保存用のテーブルだけ新規作成してやる。
ちなみにポケモンの型情報の方は単にPokemon Showdownでエクスポートできる文字列を整理して保存してあるだけ。最終的にPercymonに入力してShowdownライブラリを流用したシミュレーション対戦ができる。
(4)のMinimax計算結果からお互いの最善手を抽出するのはちょっと理論的におかしい部分があるのだが、今回はまだテスト目的なのでそんなに気にしないことにした。
ちなみにもっと適当にやるならコマンド選択は全部ランダムで、というやり方になるがこれだとさすがにPT同士の有利不利が全然現れなかった(ほぼ5分5分)のでやめた。
[ウインディ, シャンデラ, ドラパルト] vs [ホルード, ウオノラゴン, パッチラゴン]
お互いランダム選択の場合
プレイヤー1 Wins: 48
プレイヤー2 Wins: 52
平均ステップ(≒ターン)数: 13.74
お互いMinimax(深さ1)の場合
プレイヤー1 Wins: 90
プレイヤー2 Wins: 10
平均ステップ(≒ターン)数: 12.6
今回は、
・全7匹からランダムで3匹vs3匹を選択
・63232試合分
データを作成しておいた。ポケモンの種類はもっと増やせるが、機械学習の計算量を考えてとりあえず今回は7匹に絞った。
決定的識別モデル ~うまくいかない~
最初に作ったのは
入力: 自分PTのポケモンの組み合わせ
出力: 予測される勝敗(0か1か)
となる予測器である。機械学習の分野では初歩的な教師あり学習だと思う。
この分野ではおなじみの、pythonとscikit-learnを使った。ライブラリの充実さにビビり倒してしまった。Pandasでエクセルシート読み込みからカラムの切り出しまで数行で実現できるのは便利すぎる。
Python公式リファレンスには非プログラマー向けの簡単なチュートリアルが置いてあったり、そもそも公式リファレンス読まなくても日本語でググってるだけである程度できそうなのは初心者に優しいですね。
ということでほぼライブラリ関数に突っ込んだだけなのだが、入力データ(特徴量)の整形だけちょっと書いておく。図を見ればわかりそうなので文字はちっちゃくしておく。
特定のポケモンの種類の組み合わせを入力として、勝ち負けの値が答えとしてあるデータなので、カテゴリカル変数から2値のクラス分類をするモデルになる。
順序や量を持たないカテゴリカルな変数なので、One-Hotエンコーディングと呼ばれる、対応する成分だけ1となるようなベクトルの形に変換する。
ただ、今回は同じリストから選択されるポケモンが3匹まで存在するので、同じ次元のベクトルで、1となる成分が3個存在するベクトルを入力値とした。(それぞれのOne-Hotベクトルを足し合わせる)
(一般的になんというエンコード手法かわからないけど、たぶんこれでいいと思う)
このベクトルを自分PT、相手PTからそれぞれ作成できるのだが、
最初は同一の相手PTに対する勝敗データを使い、自分PTのみを入力値をした。
スモールステップは大事
あとは、このベクトル1つ1つを対応する勝敗と組み合わせて、教師データとして学習関数に突っ込む。
そして何も考えずにSVC(Support Vector Classifier)に突っ込んだ結果の学習曲線がこちら。
Scoreが正解率を表す。その他は機械学習の用語になるが雰囲気で感じ取っていただきたい。
正解率が0.75付近で早々に収束してしまうのである。
正解率が100%にならない理由は、想像がついた。今回PercymonとMinimax計算を使って行った対戦シミュレーションには勝敗にある程度の揺らぎがあり、同じPTの組み合わせでも3回に1回くらいは勝敗が逆転したりする。
それで正解率は0.75ぐらいが限界だと。実際それは正しいと思う。
しかし、翌朝ふと思いついて予測器にある入力を入れてみると、
対戦シミュレーションで100%勝利していたPTに対しても、「負け」と分類している!
このときの分類器の気持ちはこうである
「この相手PT強くて、大抵負けるから、どの自分PTが来ても負けって言っておけばだいたい当たるんじゃね」
上は混同行列と言われるもの。
教師データに混在する誤り(勝敗の揺らぎ)と全体としての勝敗の偏りのせいで、とりあえず「負け」と宣言するだけの予測器になってしまった?と考えている。
確率的識別モデルとROC曲線
最初に作った分類器は少し無理があったのにお気づきだろうか。
予測する勝敗を0か1かで答えろというのは、少し雑すぎる。
ということで機械学習の教材を少し復習して、タイトルにあるようなものを出力するようにした。
確率的識別モデルとは、予測を(0か1かではなく)確率で表現する。今回だと「勝つ確率が0.34で、負ける確率が0.66」といった具合。分類器の中で「確信の度合い」を表すなんらかの連続値が出てくるということ。
そしてこのモデルの性能を評価するのにはよくROC曲線という評価方法が使われる。
ROC曲線についてはネット上にもたくさん情報があるが、最近PCR検査の性能を論じるときにもちょっと話題になった。今回の予測モデルで説明すると、次の相反する性能を同時に評価する。
縦軸: 本当に負けな試合を負けと判定できる割合(感度)
横軸: 本当は勝ちな試合を負けと判定してしまう割合(偽陽性率)
負けがPositive、勝ちがNegativeになってしまったのでわかりにくい
分類器の気持ちになって考えると、たとえば、
・全部に負けと言っておけば感度は100%になるが偽陽性率も100%になる(グラフの一番右上に対応する)
・全部に勝ちと言っておけば偽陽性率は0%だが感度も0%になる(グラフ左下)
→グラフ左上にプロットされる性能をもつ分類器が最高。
確率的識別モデルの場合、出力は0から1までの連続値が出てくるので、どこからどこまでを負け/勝ちと解釈するかは後から決められる。ということでそのしきい値を連続的に変化させていって、それぞれの(感度, 偽陽性率)をプロットしていったものがROC曲線になる。
学習したモデル全体としては、この曲線より右下の領域の面積を計算して評価値としたりする。
直感的にはこのROC曲線の真ん中ぐらいの感度/偽陽性率 (となるしきい値)を採用したいところだが、今回のようにテストデータに偏りがある場合、単純に正解率だけを追い求めるとROC曲線上で端っこの点に対応するような分類器が出来上がってしまう。(で合ってる?)
ということでこのROC曲線を使って性能評価した結果が今回の最終結果です。
右下のarea = 0.XX の数字が大きいほど良いと思ってもらっていいです。
SVC (Support Vector Classifier) のprobabilityオプションをTrueにして計算しました。
[SVC、相手PT固定のデータ、N=7242]
[SVC、C調整、相手PT固定のデータ、N=7242]
ここまでの学習は、同一相手PTに対するデータで行っていた。
だいたいうまくいったので、満を辞して自分PT、相手PT両方を入力値として学習させてみる。
[SVC、C調整、相手PT変動、N=20000]
特に問題なし。相手PTが変動するので、データの偏りも少なく学習しやすかったはず。
これを今回のまとめにしたいと思う。つまり、たとえば図の赤丸に対応するしきい値を採用して最終的な勝敗を解釈したとすると、
負け試合を70%の確率で正解できて、
勝ち試合を60%の確率で正解できる分類器ができる。
微妙すぎるって??
教師データをきれいにすればちゃんと予測できるんですよ
次回へつづく (たぶん)
[SVC、C調整、相手PT固定、勝敗の矛盾を削除※、N=7242]
※同じ(自分PT、相手PT)の組み合わせについてはつねに同じ勝ち/負けの結果となるように、勝敗ラベルを多数決で決定し直した