AIはガチマッチの勝敗をどれくらい当てられるか

ガチマッチの勝敗予想

単にキル数多いほうが勝つんじゃね?と思っているそこのアナタ!

確かに実際にはそうかもしれない。特にガチエリアなんて塗らなきゃいけないので死んでたらこれはもう負けなんです。

では、まず超単純なAIとして「単にキル数が多いチームを勝利だと予想する」ものを考えたいと思います。

キルが多いチームが勝つと予想精度
ガチエリア78.78%
ガチヤグラ76.01%
ガチホコバトル71.94%
ガチアサリ70.65%

データをダウンロードする

まずは解析するデータを集めなければいけません。

PHPでstatinkからデータを集めることができるコードを書いたので公開しておきます。

<?php
$st = START ID;
$ed = LAST ID;

for ($i = $st; $st < $ed; $i += 50) {
    $fp = fopen(($i) . ".json", "w");
    $url = "https://stat.ink/api/v2/battle?older_than=" . $i . "&amp;count=50";
    echo ("Downloading JSON file...\n");
    $json = file_get_contents($url);
    echo ("Download Complete and Writing JSON file " . (int)(($i - $st) / 50) . "/" . (int)(($ed - $st) / 50) . "\n");
    fwrite($fp, $json);
    fclose($fp);
    echo ("Done!\n");
    usleep(5000000);
}

かなり適当なんですが、API側が上手いこと動作してくれるのでこれでちゃんと動きます。

サイトへの負荷を軽減するために必ずスリープ処理は挟んでください。

このコードは一分間におよそ600件のデータを取ってこれるので一時間で36000件のデータを取得できます。

なお、このAPIを叩いて取得できるリザルトデータは50件で1.7MB近くもあるので数万件単位でデータを保存すると数GBとかなり容量を食います。

なので、ダウンロードした生データから必要なデータだけを抜き出す必要があります。

データ抽出

以下のコードはPythonで動作し、フォルダ内の全てのJSONファイルを読み込んでガチマッチのデータだけを抽出するコードです。

これでJSONファイルをrankedbattle.csvというCSVファイルに変換できます。

# coding:utf-8
import json
import csv
import glob

# Write csv File
f = open("rankedbattle.csv", "w", newline="")
w = csv.writer(f)

# Getting File List and Sorting
file = glob.glob("json/*.json")
file.sort()

count = 0
total = int(len(file) / 100)
for path in file:
    if count % total == 0:
        print(count/total, "% Done!")
    f = open(path, "r", encoding="utf-8")
    data = json.loads(f.read())

    for res in data:
        result = []
        try:
            if res["lobby"]["key"] == "standard" and res["automated"] and res["mode"]["key"] == "gachi":
                result.append(res["id"])
                result.extend([res["game_version"], res["map"]["splatnet"], res["rule"]["name"]["en_US"],
                               res["rank"]["key"], res["knock_out"], res["my_team_count"], res["his_team_count"]])
                power = res["estimate_x_power"] if res["rank"]["key"] == "x" else res["estimate_gachi_power"]
                result.append(power)
                result.append(res["end_at"]["time"] - res["start_at"]["time"])
                for player in res["players"]:
                    result.extend(
                        [player["weapon"]["splatnet"], player["kill"], player["kill_or_assist"], player["death"], player["special"], player["point"]])
                w.writerow(result)
        except TypeError:
            print(res["id"])
    count += 1

20000件のファイル(100万件のリザルトデータ)に対して実行すると平気で十分くらいかかるので気をつけてください。

抽出の結果、275,212件のガチマッチのデータを得ることができました。

また、必要最小限のデータしか取り出していないので33GBあったファイルサイズが50MBになりました。

GZIPなどで圧縮すれば20MBくらいになるので、これなら手軽に扱えそうですね。

ガチホコバトルの記録のみを抽出

さて、ここからはいよいよガチホコバトルの記録を分析していきたいと思います。

いちいちJSONファイルにアクセスすると毎回30GBも読み込まないといけないので、先程のCSVファイルを読み込めばいいわけです。

# coding:utf-8
import csv
import pandas as pd
import numpy as np

df = pd.read_csv("rankedbattle.csv", encoding="utf-8", header=None)

df = df[(df[3] == "Rainmaker") &amp; (df[8] >= 1900)]
data = np.asarray(df)

f = open("train.csv", "w", newline="")
w = csv.writer(f)

csv = []
for line in data:
    tmp = []
    win = True if line[6] > line[7] else False
    tmp.append(win)

    sum = line[11] + line[17] + line[23] + line[29] + \
        line[35] + line[41] + line[47] + line[53]
    try:
        tmp.extend([round(line[11] / sum, 3), round(line[17] / sum, 3),
                    round(line[23] / sum, 3), round(line[30] / sum, 3)])
    except ZeroDivisionError:
        tmp.extend([0.0, 0.0, 0.0, 0.0])
    csv.append(tmp)
for line in csv:
    w.writerow(line)

今回はガチパワー1900以上のガチホコバトルの試合のみで調査したいと思います。

今回は正規化したキル率を抽出しました。

キル率については後述します。

train.csv

これでrankedbattle.csvから解析用のtrain.csvが出力されます。

DLで解析

DLっていうのはDeep Learningのことです、つまり最近流行りの言葉でいうとAIというやつです。

実際にはDL=AIというわけではない(むしろそうではない場合が多い)のですが…

まあ、せっかくWindowsでCUDAを使える環境を整えたんですからGPUを使って解析してみましょう!

キル率のみで勝敗を予測

さて、今回は一番難しそうなキル率のみで勝敗を予想してみましょう。

キル率というのはアシストを除いた八人合計のキル数で各プレイヤーのキル数を除したものです。

つまり、八人合計が1になるように正規化するわけです。これをやらないとうまく学習できません。

単純パーセプトロンで予測

from pandas import Series, DataFrame
from keras.layers import Dense, Dropout
from keras.models import Sequential
from keras.datasets import fashion_mnist
import keras
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
from sklearn import svm
import numpy as np
import pandas as pd

# Setting
import tensorflow as tf
from keras.backend.tensorflow_backend import set_session
config = tf.ConfigProto()
config.gpu_options.per_process_gpu_memory_fraction = 0.55
set_session(tf.Session(config=config))

dataset = pd.read_csv("train.csv", sep=",", header=None)

x = DataFrame(dataset.drop(0, axis=1))
y = DataFrame(dataset[0])

x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.1)

model = Sequential()
model.add(Dense(input_dim=4, units=1, activation='sigmoid'))
model.summary()

model.compile(loss='binary_crossentropy',
              optimizer='rmsprop', metrics=['accuracy'])
model.fit(x_train, y_train, batch_size=100, epochs=30,
          verbose=1, validation_data=(x_test, y_test))
history = model.fit(x_train, y_train,
                    batch_size=100,
                    epochs=30,
                    verbose=1,
                    validation_data=(x_test, y_test))
score = model.evaluate(x_test, y_test, verbose=0)
print('Test loss:', score[0])
print('Test accuracy:', score[1])

さて、最初はニューラルネットワークの中でも最も単純な一層の単純パーセプトロンを使ってみましょう。

これはもう単純なベクトル演算をしているだけです。

単純パーセプトロン

DLはX1, X2, X3, X4の値を決めて入力値(各プレイヤーのキル率)からRを計算します。

Rが0.5を超えていれば勝ち、それ未満なら負けという感じで判定します。

上手くXの値を設定すれば実際の勝敗と高い確率で一致するようにRを求められるはずです、さてその結果は?

正解率69.85%を達成!!
エポックごとの正解率

TensorBoardで結果を見るとこんな感じ、エポック数4あたりから既に学習がカンストしているのか調整するごとに正解率が下がっていっています。

エポックごとのロス

ロスもまだまだ漸近していないのでこれは上手く学習できていないようです。

多層パーセプトロン

model = Sequential()
model.add(Dense(64, activation='relu', input_shape=(4,)))
model.add(Dropout(0.2))
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.2))
model.add(Dense(units=1, activation='relu'))
model.summary()

活性化関数をシグモイドからreluに変えて、三層(っていう数え方でOK?)にしてみたんですが、大幅な精度アップは見込めず。

まあ最初からたった四つの変数で勝率を90%近く当てるつもりはありません。

むしろ単純な純粋キル数だけで70%近く当てられたことは「ガチホコバトルの勝敗を決めるのは七割がキル」といってもいいことになります。

いいですか、スプラはキルゲーです!!

キル率+デス率で勝敗を予測!!

キル率だけじゃなくてデス率も使えばもっと精度良く調べられるのでは?と思ったのですが、キル率とデス率は基本的に相関があるので(キルが多ければデスは少ない)、有意な差は見られませんでした。

コードはめんどくさいし、記事が長くなるので載せません!!

というわけで、ここまでの勝率予想を表にするとこんな感じです

単純なキル数だけで72%前後は当てられるみたいですね。

キルデスキルデス塗り
SLP72.38%71.75%71.48%62.20%
MLP72.00%73.07%72.98%64.28%

単純にキルが多いチームが勝つと予想した場合の精度が71.94%もあったので72.98%が精度がいいと言えるかというとうーんっていう感じですね。

少なくとも+5%くらいは欲しいところです。

あとは、塗り要素だけで64.28%も勝敗予想できるのはちょっと驚きでした。

やられていると塗れないので、「チームとしてキル勝ちしている=>塗り勝ちできている」という関係があるので、ある程度予想できると思っていましたが65%近くあるのはびっくりしました。

他のルールと比較してみた

多層パーセプトロンのネットワークでキル率・アシスト率・デス率・スペシャル率から勝敗を予想してみた結果がこちら。

MLPキル数のみ
ガチエリア75.69%78.78%
ガチヤグラ75.96%76.01%
ガチホコバトル72.98%71.94%
ガチアサリ70.63%70.65%

これを見るとやはり戦場が固定されているガチヤグラとガチエリアが特にキルなどが重要であることがわかった。

あと、ほとんどキル数のみで予想したものと変わらないことがわかった。

キルデスなどだけで他のルールよりも5%近く勝率予想の精度が高いということは、それだけ依存しているということだからだ。

ガチアサリは「ガチホコがステージのいろんなところにあるようなルール」と言われるように、勝率予想の精度はほとんどガチホコバトルと変わらなかった。

今後ルールが増えたとしてもオブジェクトの位置が固定されているかどうかでキルの寄与率は変わってくると考えて良さそうだ。

要するに

よく考えてみればどう頑張ってAIに学習させたとしても単にキル数で勝ったか負けたか判定するのとさほど変わらないのは当たり前である。

というのも、キルしていたらデス数は減るし、デスしなかったらSP打てる回数は増えるし塗りも増えるからだ。

入力パラメータ間に強い相関があるので、いくらパラメータを増やしても変わらないのだ。

もしももっとパラメータが与えられるなら例えばガチマッチ五分間の一秒ごとの情報があれば正確に勝率を求められると思う。

ある秒数Tでどのプレイヤーがいきているか、ホコはカウントいくらのところにあるか、SPは貯まっているかなどである。

まあこれはパケットでもキャプチャしないと無理だろうが…

しかし、逆にいえばキル数だけでは80%も勝敗を予測できないことがわかった。

おまけ

でもこれ相手チームも数に入れてるやん、セコくない?と思ったので自チームのキル数などだけで勝敗を予想してみた。

要するに「めっちゃ強い一人+三人」と「そこそこの四人」は明確に違いがあるかどうかということである。

調べてみた結果、MLPで精度55.17%をだすことができた。

つまり、これらにはある程度の差があるということである。ちなみに、どっちが優れているのかはわかりません(棒)

シェアする