機械学習の初心者が某ゲームの防御貫通率を機械学習で算出してみる

# きっかけ
私は対人コンテンツがある某スマホゲームをやっています。
このゲームはキャラ育成に大量のリソース（時間もしくはお金）[^1]を使うため、「最小限のリソースで相手に勝つにシュミレートができるといいのではないだろうか」と思ったのがきっかけです。

[^1]: 1キャラクターを無課金でフル強化する場合、1年程度かかります。お金で解決しようとすると数万円かかります。

# 対人コンテンツの概要

- 戦闘システム：12キャラ vs 12キャラでの自動戦闘。交互にキャラクターが各々の特殊スキルで攻撃
- 勝利条件：相手陣営12キャラのHPを全て0にする
- 戦闘への影響が大きいもの：レベル・キャラ自体の強化度（ステータス）・装備の強化度・キャラクターの並び順

# 最終的に作りたいもの
時間とお金を可能な限り削減したい[^4]ため、「相手に勝つために何をすればいいか」を導き出せるシュミレーターを作成します。
ハードルが高いので、下記のステップを刻もうと思います。

[^4]: ゲームをやめるのが一番削減できる、という話もあります。

1. **【本記事の内容】各種データの収集と戦闘に使用される各種パラメータの算出**<br>このゲームはステータスとレベルから戦闘に使用されるXX率が算出されます。<br>このXX率はステータスの単純比例ではなく、全く同じ値でもレベルが異なるとXX率が変わります。<br>最適な行動をシュミレートするには、レベルアップとステータスの強化でXX率がどうなるかを算出する必要があります。
1. 戦闘プログラムの再現
1. 各キャラクターの作成
2. キャラクターと最低限のステータスを入力すると戦闘を再現できるWebコンテンツ
1. 相手と自分のPT情報を入力すると、勝つために必要な今後の行動（強化・キャラ取得）が表示されるシュミレーター

（参考）ステータス画面[^3]
![IMG_1288.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321300/cea9142e-f2f6-728f-6215-fe70e6e7a635.jpeg)

[^3]: 分かる人にはこれだけで、私が誰か特定されそうです…

# 各種パラメータの算出方法
データを集めた後でプロットしてみたところ、どうにもわからない…
頑張って計算式を自分で見つけるのではなく、興味があった機械学習でどうにかすることにしました。

# 開発環境の構築
## Google Colaboratoryのセットアップ
機械学習やディープラーニングを使うとなった場合、マシンリソースが必要となります。
しかし、手持ちでは開発環境もマシンリソースが無いため、無料で使えるGoogle Colaboratory（以後、Google Colabと表記）を使うことにしました。
下記手順でGoogle Colabをセットアップします。

1. [Google Colab](https://colab.research.google.com/notebooks/welcome.ipynb#recent=true)のページにアクセスし、Welcomeページを開きます
![スクリーンショット 2019-12-20 13.02.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321300/98f2c4de-a5c3-448c-a4e8-4489bd3045ce.png)
2. メニューバーで[ファイル]→[Python3の新しいノートブック]をクリックします
![スクリーンショット 2019-12-20 13.21.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321300/9319ee04-8ae4-6034-0d9d-4f5b269e77b2.png)
3. Googleへのログインを要求されるため、表示内容に従ってログインします
![スクリーンショット 2019-12-20 13.05.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321300/68362ea4-83d2-c339-6a0a-bb161d06d0b3.png)
3. Jupyter Notebookライクな開発画面が表示されます
![スクリーンショット 2019-12-20 13.14.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321300/4b204bb5-1de6-b5f3-5956-7cf613f97cc5.png)

セットアップは以上です。
Google Colabのノートブックは、デフォルトではGoogle Driveのマイドライブ直下の「Colab Notebooks」に保存されます。

# データ収集
Google Driveでspreadsheetを作成し、スマホ片手にひたすら入力します。
【スマホの操作】キャラクター選択→【スマホの操作】ステータス画面表示→【PCの操作】spreadsheet入力→（以後繰り返し）で1データ30秒、全150データを入れるには失敗時のやり直しも含めて2時間くらいかかりました。
![スクリーンショット 2019-12-20 13.18.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321300/2d3a0c6b-ca29-e404-8289-8690b66676d9.png)

# 防御貫通率算出のプログラム作成
いよいよ本番の防御貫通率算出のプログラム作成です。

## Google Driveのマウント
プログラムとのデータ入出力にはGoogle Driveを使用するため、Google Driveをマウントします。[^2]

[^2]: 後述のgspreadの使用によりこの手順は後々不要になりましたが、備忘録として残しておきます。

```Python
# Google Driveのマウント
from google.colab import drive
drive.mount('/content/drive')
```

## Google spreadsheetから直接データロード
下記の手順でデータを取り込んでいたのですが、データの入力→プログラムの実行を何度も繰り返していると、楽をしたくなりました。

1. Google Driveのspreadsheetにデータを入力する
2. preadsheetをcsv形式でダウンロードする
3. ダウンロードしたファイルを、Google Driveにそのままアップロードする

### gspreadのインストール
Google Colabから直接spreadsheetを扱うため、gspreadの使用を組み込みます。
Google Colabは標準ではgspreadのライブラリがインストールされていません。下記のコマンドでgspreadをインストールします。

```shell
!pip install --upgrade -q gspread
```
### Google Colabからのアクセスを認証
Google Colabからspreadsheetに直接アクセスするには認証情報が必要となります。
下記を実行し、クレデンシャルを取得します。

```Python
### Google Colabからのアクセス認証
from google.colab import auth
import gspread
from oauth2client.client import GoogleCredentials
auth.authenticate_user()
gc = gspread.authorize(GoogleCredentials.get_application_default())
```

## 線形回帰モデルを使って分析
機械学習のプログラミングは初めてで、どのモデルを使えばいいのかさっぱりわかりませんでした。
[sckit-learnのチートシート](https://scikit-learn.org/stable/tutorial/machine_learning_map/)のフローをたどり、なんとなくスタンダードな線形回帰モデルで分析することにしました。

```Python
# Google Driveのマウント
from google.colab import drive
drive.mount('/content/drive')

# Google スプレッドシートからデータを読み込むための認証
from google.colab import auth
import gspread
from oauth2client.client import GoogleCredentials
auth.authenticate_user()
gc = gspread.authorize(GoogleCredentials.get_application_default())

# データ分析用のライブラリ
import pandas as pd
from sklearn import linear_model
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, precision_score, recall_score

# モデルの初期化
clf = linear_model.LinearRegression()

# Google スプレッドシートからデータを読み込み
worksheet = gc.open("DefPenetration").sheet1
rows = worksheet.get_all_records()
pene = pd.DataFrame.from_records(rows)

# 目的変数と説明変数に仕分け
features = pene.drop("penetration", axis=1).values
labels = pene['penetration'].values

# テストデータを分離
x_train, x_test, y_train, y_test = train_test_split(features, labels, test_size=0.8)

# 予測モデルを作成
clf.fit(x_train, y_train)

# モデル評価
predict = clf.predict(x_test)
print("\nモデル評価")
print(clf.score(x_test, y_test))

# 手入力された値を予測
print("\nレベルと防御貫通値を入れ、防御貫通率を予測")
get_value = list(map(int,input().split()))
clf.predict([get_value])
```
実行結果した結果、正答率が低く、実データと予測値も乖離しています。

| 項目 | 値 |
|:-:|:-:|
|正答率|85%|
|レベル（実データ）|157|
|防御貫通値（実データ）|1651|
|防御貫通率（予測値）|41.8%|
|防御貫通率（実データ）|48.8%|

```
モデル評価
0.8764746820833647

レベルと防御貫通値を入れ、防御貫通率を予測
157	1651
array([41.88932495])
```

## 分析前にデータをべき変換
データを増やしても（150→188）正答率や予測値の精度があがらないため、何かがおかしいと気付きました。

機械学習の「き」の字も知らないままで突っ走るのをやめ、公式ドキュメント[9. scikit-learn 入門](https://tutorials.chainer.org/ja/09_Introduction_to_Scikit-learn.html)を参照してみたところ、以下の記載がありました。

>9.2.1. Step 1 の改善：前処理
>前処理 (preprocessing) とは、欠損値の補完、外れ値の除去、特徴量選択、正規化などの処理を訓練を開始する前にデータセットに対して行うことです。
>
>手法やデータに合わせた前処理が必要となるため、適切な前処理を行うためには手法そのものについて理解している必要があるだけでなく、使用するデータの特性についてもよく調べておく必要があります。
>
>今回のデータは、入力値の値の範囲が CRIM, ZN, INDUS, ... といった指標ごとに大きく異なっています。 そこで、各入力変数ごとに平均が 0、分散が 1 となるように値をスケーリングする標準化 (standardization) をおこなってみましょう。

防御貫通率の計算で使用する2つのデータ（「レベル（level）」は144〜180、「防御貫通値（value）」は60〜7318）も上記と同様に値の範囲が異なっています。
正答率と予測値の精度をあげるため、ドキュメントにならってべき変換を入れてみることにしました。

```Python
# データ分析用のライブラリ
from sklearn.preprocessing import PowerTransformer

# べき変換するための初期化
scaler = PowerTransformer()
```
〜中略〜

```Python
# べき変換
scaler.fit(x_train)
x_train_scaled = scaler.transform(x_train)
x_test_scaled = scaler.transform(x_test)

# 予測モデルを作成
clf.fit(x_train_scaled, y_train)

# モデル評価
predict = clf.predict(x_test_scaled)
print("\nモデル評価")
print(clf.score(x_test_scaled, y_test))

# 手入力された値を予測
print("\nレベルと防御貫通値を入れ、防御貫通率を予測")
get_value = list(map(int,input().split()))
clf.predict(scaler.transform([get_value]))
```

実行結果した結果、正答率が高く、実データも予測値と近くなりました。

| 項目 | 値 |
|:-:|:-:|
|正答率|98%|
|レベル（実データ）|157|
|防御貫通値（実データ）|1651|
|防御貫通率（予測値）|47.7%|
|防御貫通率（実データ）|48.8%|

```
モデル評価
0.9859441237439409

レベルと防御貫通値を入れ、防御貫通率を予測
157	1651
array([47.70776249])
```

## 最終形態
下記がコードの最終形態です。

```Python:Penetrate.ipynb
# Google Driveのマウント
from google.colab import drive
drive.mount('/content/drive')

# Google スプレッドシートからデータを読み込むための認証
from google.colab import auth
import gspread
from oauth2client.client import GoogleCredentials
auth.authenticate_user()
gc = gspread.authorize(GoogleCredentials.get_application_default())

# グラフ表示要のライブラリ
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

# データ分析用のライブラリ
import pandas as pd
from sklearn import linear_model
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, precision_score, recall_score
from sklearn.preprocessing import PowerTransformer

# モデルの初期化
clf = linear_model.LinearRegression()

# べき変換するための初期化
scaler = PowerTransformer()

# Google スプレッドシートからデータを読み込み
worksheet = gc.open("DefPenetration").sheet1
rows = worksheet.get_all_records()
pene = pd.DataFrame.from_records(rows)
print("スプレッドシートの内容")
print(pene)

# 読み込んだデータを3次元プロット
X = pene['level'].values
Y = pene['value'].values
Z = pene['penetration'].values
fig = plt.figure()
ax = Axes3D(fig)
ax.plot(X,Y,Z,marker="o",linestyle='None')

# 目的変数と説明変数に分ける
features = pene.drop("penetration", axis=1).values
labels = pene['penetration'].values

# テストデータを分離
x_train, x_test, y_train, y_test = train_test_split(features, labels, test_size=0.8)

# べき変換
scaler.fit(x_train)
x_train_scaled = scaler.transform(x_train)
x_test_scaled = scaler.transform(x_test)

# 予測モデルを作成
clf.fit(x_train_scaled, y_train)

# モデル評価
predict = clf.predict(x_test_scaled)
print("\nモデル評価")
print(clf.score(x_test_scaled, y_test))

# 偏回帰係数
print("\n偏回帰係数")
print(pd.DataFrame({"Name":pene.drop("penetration", axis=1).columns,
                    "Coefficients":clf.coef_}).sort_values(by='Coefficients') )
 
 # 切片 (誤差)
print("\n切片 (誤差)")
print(clf.intercept_)

# 手入力された値を予測
print("\nレベルと防御貫通値を入れ、防御貫通率を予測")
get_value = list(map(int,input().split()))
clf.predict(scaler.transform([get_value]))
```

下記が最終形態のコードの実行結果です。
![スクリーンショット 2019-12-20 2.02.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321300/15bb9db4-3280-6367-10a0-15105bd39344.png)

# 最後に
今までは書籍に書いてある難しい数式で早々に挫折してしまい、実際のプログラミングまではやっていませんでした。

**どうやってプログラムするの？ 
なんで数学が必要なの？
そもそも機械学習ってなに？ディープラーニングとどう違うの？
何からやればいいんだー！
そもそも、そんな大量のデータもってないよ！**

作り始めた頃は、上記のような感じでさっぱりでした。

作り終わった後、**「データの前処理で何をするか（べき変換など）」**と**「何のモデル（線形回帰モデルなど）を使うのか」**が大事だとわかりました。
そして、それらを判断するには**数学の知識**がやっぱり必要なんだと痛感しました。

ゴールのシュミレーターはもっと強敵です。時間がかかっても、一歩ずつ着実に必要な知識を身に着け、完走したいと思います。

# 参考にした書籍
- ゼロから作れるDeep Learning
- sckit-learnとTensorFlowによる実践機械学習