ブループロトコルのベンチマークスコアを機械学習で予測し、XAIで分析してみる

# はじめに

みなさん、BLUE PROTOCOL（ブループロトコル）やってますか？
サービス開始当初は日本最高の同接数を記録したリリースしたばかりのオンライン3Dアクションゲームです。こういったゲームには「ベンチマークソフト」があり、自分のPC環境での動作をテストできます。

リリース前にこのソフトが出たのですが、その時に「最適なグラフィック設定ってなんだろう」と思い、データ分析をやってみました。

結論から言いますとあまり上手くいきませんでした。
分析結果は出ましたが、その結果からの仮説をもとに設定しても思ったようなスコアは出せませんでした。

なので結果に関しては参考程度にみてもらって、あとは技術的な記事として参考にしてもらえればと思います。

# 目次
- 細かい背景情報
- PC環境
- 使用ライブラリ
- 自動データ取得
- 機械学習・スコア予測
- XAIによる分析
- 結論・まとめ

# 簡単な前提知識

## ブループロトコルについて
バンナムから出てるオンラインRPGです。Project Sky Blueが開発運営をしています。

## ベンチマークソフトについて
そのゲームが自身のPCでどれくらい快適に遊べるかをスコア化したもの。

## 機械学習について
非エンジニア向けに書くと、昨今流行りの「AI」というやつ。人間が学習するのを、機械に学習させてるみたいなイメージで捉えてもらえれば良いです。

## XAI（Expalinable AI: 説明可能なAI）について
AIの判断は高精度なモデルほど複雑で、その根拠の説明が難しいです。それを可視化して理解できるようにするための技術です。

# PC環境

|パーツ|内容|
|-|-|
|メモリ|16GB|
|CPU|Intel Core i7 9700K|
|GPU|NVIDIA RTX 2070 SUPER|

# 使用ライブラリ

|ライブラリ名|概要|
|-|-|
|LightGBM|Microsoftの開発した勾配ブースティング決定木ベースの機械学習モデル|
|SHAP|協力ゲーム理論をベースとしたAIの予測に対する特徴量の影響力を分析する|
|PyAutoGUI|PythonでGUIの自動操作が可能なライブラリ|
|その他|Optuna、Pandas など|

# 自動でデータを取得する
今回は以下の設定で回帰予測を行います。

|説明変数|目的変数|
|-|-|
|グラフィック設定項目|ベンチマークスコア|

## データの自動取得

データは公式のベンチマークソフトを使用して取得します。
説明変数のグラフィック項目は⚪︎個あり、全ての組み合わせをとっているとデータ数が数百になり、手動で取ることが困難です。そこで自動取得できるようにプログラムを作成します。
※ちなみに、以下の方法はベンチマークソフトなら利用規約的にOKですが、本編のゲームでは利用規約的にNGですのでそこは気をつけましょう。

流れとしては、PyAutoGUIでGUIを自動操作し、設定内容とスコアをベンチソフトのスコア記録機能を利用しテキストデータとして出力します。

ここで問題なのがパラメータの多さになります...
この記事では解説しませんが、ざっくりと各パラメータを変化させ、SHAPの分析で影響度の高いとされたパラメータ上位を取り出し、それを細かく変化させてデータ取得を行いました。
最終的に集めたパラメータは以下のとおりです。

|パラメータ|
|-|
|スケーリング解像度|
|シャドウ|
|ポストプロセス|
|草の量|

このパラメータが取りうる組み合わせを全部探索します。
```python
import itertools
parameter = {
    'スケーリング解像度': list(range(50,110,10)),
    'ポストプロセス': list(range(0,5)),
    'シャドウ': list(range(0,5)),
    '草の量': list(range(0,5))
}
pattern_list = list(itertools.product(
                    parameter['スケーリング解像度'],
                    parameter['ポストプロセス'],
                    parameter['シャドウ'],
                    parameter['草の量'],
                ))
print(len(pattern_list), len(pattern_list)*290/3600/24)
# 750 通りの組み合わせになる
```

これらのパターンをPyAutoGUIで操作し、すべて試していきます。大文字になってる部分はディスプレイの座標を入れるところなので、試す場合は各自で設定してください。

```python
# 参考：私の環境での座標
SET_CHANGE_POS = (864, 789)
SET_GRAPHIC_POS = (457, 242)
SET_SCAL_POS = (1047, 374)
SET_VIEW_POS = (1045, 406)
SET_ANTI_POS = (1046, 439)
SET_POST_POS = (1046, 469)
SET_SHAD_POS = (1048, 500)
SET_TEXT_POS = (1045, 533)
SET_EFFE_POS = (1046, 566)
SET_GRAS_POS = (1046, 600)
SET_WINDOW_CLOSE_POS = (1121, 218)
```

PyAutoGUIの操作をしやすいように以下の関数を定義しています。

```python
def move_to(pos):
    pg.moveTo(pos[0], pos[1])
```

データを取得します。
（実行時間がすごく長いです）

```python
import pyautogui as pg
import time

for i in range(current, len(pattern_list)):
    
    move_to(SET_CHANGE_POS)      # 設定変更
    pg.click()
    move_to(SET_GRAPHIC_POS)     # グラフィック設定に移動
    pg.click()
    
    pt = pattern_list[i]
    print(f'\r{i}/{len(pattern_list)-1}: {pt}', end='')
    
    move_to(POS_DICT['スケーリング解像度']) # 今回の設定項目へ移動
    pg.click()
    type_list = ['backspace']*3+list(str(pt[0]))
    pg.typewrite(type_list)      # 値を入力
    
    move_to(POS_DICT['ポストプロセス']) # 今回の設定項目へ移動
    pg.click()
    type_list = ['backspace']*3+list(str(pt[1]))
    pg.typewrite(type_list)      # 値を入力
    
    move_to(POS_DICT['シャドウ']) # 今回の設定項目へ移動
    pg.click()
    type_list = ['backspace']*3+list(str(pt[2]))
    pg.typewrite(type_list)      # 値を入力
    
    move_to(POS_DICT['草の量']) # 今回の設定項目へ移動
    pg.click()
    type_list = ['backspace']*3+list(str(pt[3]))
    pg.typewrite(type_list)      # 値を入力
    
    move_to(SET_WINDOW_CLOSE_POS)# 設定ウィンドウを閉じる
    pg.click()
    move_to(START_POS)           # ベンチスタートボタンへ移動
    pg.click()
    time.sleep(BENTCH_TIME)      # ベンチを行う時間待機
    with pg.hold('esc'):         # ベンチを終了する
        time.sleep(2)
    time.sleep(3)
    move_to(OUT_REPORT_POS)      # レポートを出力する
    pg.click()
```

以下のようなデータが溜まって行きます。

```
﻿BLUE PROTOCOL ベンチマーク 
実行日時: 2023/03/21 15:22:13

SCORE: 27382
評価: 極めて快適
- 平均フレームレート: 201.434
- 最低フレームレート: 43.983

画面解像度: 1920 x 1080
スクリーンモード: 仮想フルスクリーン
DirectX 12

グラフィクス プリセット: カスタム
- スケーリング解像度: 50
- 表示距離: 4
- アンチエイリアシング: 4
- ポストプロセス: 0
- シャドウ: 0
- テクスチャ: 4
- エフェクト: 4
- 草の量: 0

システム情報
Microsoft Windows 11 Home (ver.10.0.22621)
Intel(R) Core(TM) i7-9700K CPU @ 3.60GHz
NVIDIA GeForce RTX 2070 SUPER ( 8003 MB )

このベンチマークは BLUE PROTOCOL Windows版の動作を保証するものではありません。

BLUE PROTOCOL ベンチマーク Version 1.0.0
公式サイト: https://blue-protocol.com/
© 2019 Bandai Namco Online Inc. © 2019 Bandai Namco Studios Inc.
```

## データクレンジング

出力されたテキストファイルを読み込み、テーブルデータとしてCSVに保存します。
こんな感じです↓

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/456646/ced3bb05-cf22-7197-8578-ced3d30557c7.png)

# 正規化してみる

データ内で乖離があるので正規化をしてスケールを揃えましょう。

# LightGBMで予測してみる

データは用意できたのでLightGBMに入れてみましょう。

```python
def bayes_objevtive(trial):
    model = lgb.LGBMRegressor()
    
    params = {
        "learning_rate": trial.suggest_float("learning_rate", 0.01, 1.0, log=True),      
        "reg_alpha": trial.suggest_float("reg_alpha", 0.001, 10, log=True),       
        "reg_lambda": trial.suggest_float("reg_lambda", 0.001, 10, log=True),
        "colsample_bytree": trial.suggest_float("colsample_bytree", 0.1, 1.0),
        "subsample": trial.suggest_float("subsample", 0.1, 1.0),        
        "max_depth": trial.suggest_int("max_depth", 3, 10, 1),
        "num_leaves": trial.suggest_int("num_leaves", 2, 100, 1),
        "min_child_samples": trial.suggest_int("min_child_samples", 2, 100, 1),
        "subsample_freq": trial.suggest_int("subsample_freq", 1, 10, 1)
    }
    
    model.set_params(**params)
    
    scores = cross_val_score(model, X_train, y_train, cv=5,
                            scoring='neg_root_mean_squared_error')
    score = scores.mean()
    return score

X = df.loc[:, 'スケーリング解像度':'草の量']
y = df[['スコア']]

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)
study = optuna.create_study(direction='maximize',
                           sampler=optuna.samplers.TPESampler())
study.optimize(bayes_objevtive, n_trials=100)

best_optuna_lgb = lgb.LGBMRegressor(learning_rate = study.best_trial.params["learning_rate"], 
                                    reg_alpha  = study.best_trial.params["reg_alpha"], 
                                    reg_lambda = study.best_trial.params["reg_lambda"],
                                    colsample_bytree  = study.best_trial.params["colsample_bytree"],
                                    subsample         = study.best_trial.params["subsample"],
                                    max_depth         = study.best_trial.params["max_depth"],
                                    num_leaves        = study.best_trial.params["num_leaves"],
                                    min_child_samples = study.best_trial.params["min_child_samples"],
                                    subsample_freq    = study.best_trial.params["subsample_freq"],
                                    random_state=0)
best_optuna_lgb.fit(X_train, y_train)

y_test_pred = best_optuna_lgb.predict(X_test)

r2_test = r2_score(y_test, y_test_pred)
rmse_test = mean_squared_error(y_test, y_test_pred, squared=False)
print('R2', r2_test) # 0.9848989694108436
print('rmse', rmse_test) # 332.13318367205636
```

## 予測結果

決定係数が0.98なのでかなり精度は良いですかね？
ただ、ロスを見ると微妙な感じもしますね。

# SHAPでXAIしよう！

予測結果に対して特徴量がどのくらい影響を与えているのかを表してみます。

## 全体的な評価
まずは予測に寄与した特徴量ですが、以下の順番で影響度が高いと言えます。
1. スケーリング解像度
2. シャドウ
3. ポストプロセス
4. 草の量

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/456646/f475781e-f044-6e27-ff0a-f4f4e87e33aa.png)

そして、すべて負の相関を示しているので、値を増やすほどにスコアが低くなります。
（つまり、設定を増やせばPCに負荷がかかるという当たり前のことを証明している。）
当たり前のことを証明したのは重要で、このモデルが予測した値は解釈として間違っていないとも言えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/456646/63f5d925-5a65-e0f5-bb98-5eb7afb97257.png)


## ポストプロセッシングの交互作用分析

交互作用をみていきます。読み方がわかりにくいですが、一応説明します（お急ぎの方は結論部分へ）。

### 読み方
まず，縦軸は指定した特徴量のSHAP値です．つまり，どのくらい予測に寄与したのか？ということを表しています．
一方で，横軸はその特徴量自身の値です．つまり，予測へ寄与したかどうかの相関関係をここだけで確認することができます．

そして交互作用として，カラースケールで特徴量をもう一つプロットしています．これはある一箇所で色の分離が起こっている部分において，交互作用が発生していると解釈できます．

例えば一枚目のグラフでは，スケーリング解像度が50の時，ポストプロセスが高い値を設定すると，スコアが上昇する．低い値を設定すると，スコアは減少する．このように解釈することができます．

### ポストプロセスとスケーリング解像度の解釈

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/456646/1f6e0d36-3e57-f51e-46a0-b8e71aa3cdc9.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/456646/01f9b44a-9b7e-4285-8f4a-0147d5427369.png)

1枚目，スケーリング解像度のグラフにポストプロセスをカラースケールとして入れたグラフ．50，60だと，スコアが高くなるとポストプロセスも高くなる．90，100だと，スコアが高くなるとポストプロセスは低くなる．つまり，スケーリング解像度が低い場合，スコアを上げるならポストプロセスは高く，スケーリング解像度が高い場合，スコアを上げるならポストプロセスを低く設定するとよいといえる．これは2枚目も同じことが言える．

###　ポストプロセスに対する，草の量とシャドウの解釈

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/456646/93a0269d-e21a-bbda-a02b-b78f05b47160.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/456646/749844a0-1705-9a18-d908-8a4dc8a174f3.png)

3，4枚目のシャドウ，草の量に対するポストプロセスのグラフからは，↑のとは逆のことがいえる．どちらも高いとスコアが高くなる．

# 結論！！

- スケーリング解像度，シャドウ，ポストプロセス，草の量の順番でスコアに影響する
- ポストプロセスは他のパラメータに影響されやすい↓
- スケーリング解像度とポストプロセスは，どちらかが高い時，どちらかを下げると，スコアが下がる
- シャドウ，草の量は，ポストプロセスに対して，高い時に，高くするとスコアが上がるし，下げるとスコアも下がる

## 上記の仮説を基に実際にデータを見て検証した結果
- 仮説通りになっていない
- おそらく他の変数との組み合わせによる複雑な関係性が現れている可能性があるため，一概にポストプロセスの影響が判断できない


# まとめ

結局仮説通りにうまくいかずという....
色々な影響が考えられますが，今回はこういった結果に終わりました．
SHAPの解釈などはかなりしやすい図なので何かの参考になれば．