Amazon SageMaker Autopilot を利用したゲームログのチート検出

[AWS & Game Advent Calendar 2020](https://qiita.com/advent-calendar/2020/aws-game) の10日目の記事です。
## はじめに
ゲームにおけるチート行為は大変厄介なものです。チート行為を野放しにすることは、他のゲームユーザーの体験を著しく損なう上に、ゲームの運営自体にも損害を与えかねません。

しかし、これらのチート行為の検出は、従来、ゲーム運営を熟知したエンジニアが統計処理などを駆使して人力で検知を行う、非常に属人的で効率の悪いものでした。また、不正検知の自動化といっても、特定の閾値を定めただけの単純なスクリプトを流す程度の対策が取られてきました。
それなら、近年、半ば魔法の言葉となりつつある、機械学習を用いてある程度チート検出が自動化できたらどんなに効率的なことでしょう。

しかし、機械学習といっても、これまた、機械学習や統計処理を熟知したエンジニアやデータサイエンティストが必要になってきます。そのようなスキルを持ったエンジニアは、どこの会社にもいるわけではありません。

そこで、本記事では、近年注目を集めている `AutoML` [^1] を用いた異常検知技術を使って、ゲームログからチートユーザーの検出をしてみることにします。

[^1]: AutoML (Automated Machine Learning) は、自動化された機械学習のことであり、機械学習や深層学習への深い知識がなくても強力な機械学習モデルの構築を可能にするフレームワークやサービスのこと。

## 前提条件
### 利用サービス
* [Amazon SageMaker Autopilot](https://aws.amazon.com/jp/sagemaker/autopilot/)
* [Amazon Athena](https://aws.amazon.com/jp/athena/)
* [Machine Learning (ML) with Amazon Athena (Preview)] (https://docs.aws.amazon.com/ja_jp/athena/latest/ug/querying-mlmodel.html)

### 今回利用するデータ
全て[こちら](https://github.com/zentahori/sagemaker-autopilot-cheat-detection-sample)の github リポジトリから参照できます。

### 手順の大まかな流れ
* 擬似的なゲームログを用意してあるので、最初にそれをダウンロードします
    * ゲームログは学習用データと検証用データのデータセットに分かれています
* 学習用のデータセットを用いて、AWSのAutoMLサービスであるAmazon SageMaker Autopilotにてログからチートを検出する機械学習モデルを作成します
* 最適なモデルをデプロイしたあと、検証用のデータセットを用いてモデルの精度を確認します
* 検証用のデータを運用中の新規のゲームログと見立てて、Amazon Athenaを使って作成した機械学習モデルを呼び出し、ゲームログに不正が含まれていないかを推論します

### 今回利用するゲームログ
今回、チート検出に利用するゲームログの内容を下記に示します。
こちらのログは、実際のゲームのログではなく、あるプログラムにより機械的に作成されたログであり、一定(全体の26%程度)のチートによる不正データを含むログとなっています。一般的なRPGやパズルゲーム、音ゲーなどのスコアや得点を伴う時系列のログを想定しています。ゲームログの内容を下記に簡単に示します。

* `player_id` : プレイヤーごとに付与されている一意のユーザーID。
* `level`、`score`: ゲーム内でのユーザーのレベルやスキルを示す。
* `gold`や`potions`: ユーザーの所有するゲーム内のアイテム数を示すカラム。
* `cheat_label`: あらかじめゲームのイベントログが不正(チート)なのか、正常なのかラベリングしたカラム。`fraud`は不正を表し、チートのレコードであることを示す。 `legit`は正常なレコードであることを示す。
* `timestamp`: 各ユーザーのアクションやイベントの発生日時。
![スクリーンショット 2020-12-10 19.21.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44650/d582943b-4761-096f-e794-5221b1b34835.png)

ここで重要なのは、`cheat_label`カラムに示されるように、学習用のデータセットにはあらかじめ各レコードの情報が不正なのか(`fraud`)、正常なのか(`legit`)という正解のラベリングがされていることです。これからモデルを作成する際に利用するAutoMLであるSageMaker Autopilotは教師あり学習により機械学習モデルを作成するサービスであるため、学習用データセットへの事前のラベルづけは必須になってきます。

## AutoMLによる推論モデル作成手順
### Amazon SageMaker Studio のセットアップ
こちらの詳細手順は割愛します。詳しくは、[こちら](https://docs.aws.amazon.com/ja_jp/sagemaker/latest/dg/gs-studio-onboard.html)を参照してください。

### ゲームログのデータセットをダウンロード
SageMaker Studio上からnotebookを開いて下記のコードを貼り付けて実行します。今回のチート検出に使う学習用のデータセットをダウンロードします。

```sh:sh
%%sh
wget https://raw.githubusercontent.com/zentahori/sagemaker-autopilot-cheat-detection-sample/main/dataset/sample_playerlog_for_training.csv
```

続いて、同じように下記のコードを貼り付けて実行します。こちらは、モデル検証用のデータセットのダウンロードになります。

```sh:sh
%%sh
wget https://raw.githubusercontent.com/zentahori/sagemaker-autopilot-cheat-detection-sample/main/dataset/sample_playerlog_for_validation.csv
```

次に下記のコードをnotebook上で実行して、ダウンロードしたデータセットの中身を確認しておきましょう。

```python:python
import pandas as pd
cheat = pd.read_csv('sample_playerlog_for_training.csv')
cheat[:10]
```

```python:python
validation_data = pd.read_csv('sample_playerlog_for_validation.csv')
validation_data[:10]
```

### 学習用のデータセットをS3にアップロード
次に、SageMaker Autopilotでデータセットを読み込んでモデルを作成するために、一旦学習のデータセットをS3にアップロードします。下記のコードをnotebook上で実行します。

```python:python
import sagemaker

prefix = 'sagemaker/cheat-detection/input'
sess   = sagemaker.Session()

uri = sess.upload_data(path="./sample_playerlog_for_training.csv", key_prefix=prefix)
print(uri)
```

こちらを実行するとアップロード先のS3バケットのURLが表示されるので、控えておきます。
あとでこちらの　バケットのURLは使用します。今回使用しているS3バケットは、デフォルトのS3バケットであることに注意してください。

```
s3://sagemaker-us-west-2-ACCOUNT_NUMBER/sagemaker/cheat-detection/input/sample_playerlog_for_training.csv
```

### SageMaker Autopilotによるチート検出モデルの自動作成
注意: 詳細については、[こちら](https://docs.aws.amazon.com/sagemaker/latest/dg/autopilot-automate-model-development-create-experiment.html)を参考にしてください。

SageMaker StudioのLauncherから「Build models automatically」をクリックします。
![スクリーンショット 2020-12-09 23.51.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44650/0f0d1a5c-3365-af45-b23c-001bc2772b45.png)
`AUTOPILOT EXPERIMENT SETTINGS` が開くので、下記の設定値をそれぞれ入力します。

* `Experiment Name`: cheat-detection-sample
* `CONNECT YOUR DATA`
    * 入力の `S3 bucket address`: 先ほど上記の手順で表示した S3 URL 
        * (例 s3://sagemaker-us-east-2-[ACCOUNT-NUMBER]/sagemaker/cheat-detection/input/sample_playerlog_for_training.csv)
    * `Target`: cheat_label
* `Output data location (S3 bucket)`
    * 出力先の`S3 bucket address`: s3://sagemaker-us-east-2-[ACCOUNT-NUMBER]/sagemaker/cheat-detection/output

そのほかの値は全てデフォルトのままで、最後に `Create Experiment`をクリックして実験を開始します。

![スクリーンショット 2020-12-10 0.26.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44650/7c0c8ac8-b1c2-b2cb-51f9-ef6d58afc048.png)
![スクリーンショット 2020-12-10 0.26.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44650/ebc19bfa-a738-6058-998f-3b23055e6b9a.png)

実験が開始されると、下記のようにAutopilotが自動でデータを解析して、必要な前処理や特徴量エンジニアリングやハイパーパラメータの最適化までを全て自動で実行します。
処理時間はデータ量にも依存しますが、今回のデータ量では終了までにおよそ３時間ほどかかりました。
![スクリーンショット 2020-12-10 1.05.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44650/fc9a21ae-1933-d6a6-7ee3-9f54e5dadfb4.png)

### 最適なモデルをデプロイする
実験完了後、 `Trials` タブを開くと、Experimentが様々なアルゴリズムや条件を適用して複数の異なるチューニングのモデルを作成したモデル一覧が見られます。その中で、上部に☆印付きで `Best` と書かれた精度の高い最適なチューニングをされたモデルがあるので、選択状態にして `Deploy model` をクリックします。

![スクリーンショット 2020-12-10 11.12.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44650/fc284ca1-67c2-8b65-bdca-4e60e68d1c19.png)

そのまま `Deploy model` というタブが開くので、エンドポイントに名前を付け (`cheat-detection-sample`など)、すべての設定をデフォルトのままにします。`Deploy model` をクリックします。モデルが、Amazon SageMaker によって管理される HTTPS エンドポイントにデプロイされます。
![スクリーンショット 2020-12-10 11.25.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44650/6a8464b9-e8e9-d55a-b6fd-4b1f89914a9c.png)

### モデルの精度を検証する
モデルのデプロイが完了したら、検証用のデータセットを用いてモデルの精度を検証してみます。検証用のデータセットは、学習用のデータセットとは別でモデルに対しては未知のデータのはずです。
ここでは boto3 SDK の `invoke_endpoint` API を使用して、機械学習モデルの精度を表す重要なメトリクスである正確性、適合率、再現率、F1 スコアを計算します。

SageMaker StudioでNotebookを開き、以下のコードをコピーして貼り付け、実行します。

```python:python
import boto3, sys

ep_name = 'cheat-detection-sample'
sm_rt = boto3.Session().client('runtime.sagemaker')

tn=tp=fn=fp=count=0

with open('sample_playerlog_for_validation.csv') as f:
    lines = f.readlines()
    # length = len(lines)
    for l in lines[1:2000]:  # Skip header
        l = l.split(',')      # Split CSV line into features
        label = l[8]          # Store 'yes'/'no' label
        del l[8]              # Remove label
        l = ','.join(l)       # Rebuild CSV line without label
                
        response = sm_rt.invoke_endpoint(EndpointName=ep_name, 
                                         ContentType='text/csv',       
                                         Accept='text/csv', Body=l)

        response = response['Body'].read().decode("utf-8")
        #print ("label %s response %s" %(label,response))

        if 'fraud' in label:
            # Sample is positive
            if 'fraud' in response:
                # True positive
                tp=tp+1
            else:
                # False negative
                fn=fn+1
        else:
            # Sample is negative
            if 'legit' in response:
                # True negative
                tn=tn+1
            else:
                # False positive
                fp=fp+1
        count = count+1
        if (count % 100 == 0):   
            sys.stdout.write(str(count)+' ')
            
print ("Done")

accuracy  = (tp+tn)/(tp+tn+fp+fn)
precision = tp/(tp+fp)
recall    = tp/(tp+fn)
f1        = (2*precision*recall)/(precision+recall)

print ("%.4f %.4f %.4f %.4f" % (accuracy, precision, recall, f1))
print ("tp:%d fn:%d tn:%d fp:%d" % (tp, fn, tn, fp))
```

データ2000ポイントの集計が終わると、以下のような出力が表示されます。

```python:
100 200 300 400 500 600 700 800 900 1000 1100 1200 1300 1400 1500 1600 1700 1800 1900 Done
0.9995 0.9981 1.0000 0.9990
tp:525 fn:0 tn:1473 fp:1
```

これを表にまとめると下記のようになります。

| メトリクス | 数値 | 説明 |
| :-- | :-: | :-- |
| Accuracy (正解率) | 0.9995 |チートあり、なしと予測したデータのうち、実際に正解したものの割合:  accuracy  = (tp+tn)/(tp+tn+fp+fn)|
| Precision (適合率) | 0.9981 | チートと予測したデータのうち実際にチートであるものの割合: precision = tp/(tp+fp) |
| Recall (再現性) | 1.0000 | 実際にチートであるデータのうち、チートと予測されたものの割合: recall    = tp/(tp+fn)|
| F1 スコア | 0.9990 | 適合率と再現率の調和平均: f1 = (2*precision*recall)/(precision+recall)|

![スクリーンショット 2020-12-10 13.42.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44650/3fc9729d-80d5-8286-f053-724692af6089.png)

この表から分かるように、Recall (再現性) は100%であり、全て取りこぼしなく不正データ(チートであるとラベルされたデータ)は不正である、とこのモデルは正しく推論できていることを示しています。

## Athenaを使ってゲームログからチート予測をする
Machine Learning (ML) with Amazon Athena を利用すれば、直接Athenaのクエリからチート予測の推論モデルを呼び出し、S3に保存されているログをスキャンして、チートの予測をすることが可能です。
ここでは、検証用のデータセットをS3に保存して、Athenaを用いてチート検出する手順を見て行きます。

### 分析データとデータソースの準備
Notebookへ下記のコードをコピーして検証用のデータセットをS3へアップロードしておきます。この際に出力された、S3のURLをメモしておきます。

```python:python
prefix = 'sagemaker/cheat-detection/sample-data'
sess   = sagemaker.Session()

uri = sess.upload_data(path="./sample_playerlog_for_validation.csv", key_prefix=prefix)
print(uri)
```

Management consoleのAmazon Athenaの画面に移動して、AWS Glue Crawlerを用いて新規テーブルを作成します。 Glue Crawlerを用いたテーブルの作成詳細については[こちら](https://docs.aws.amazon.com/ja_jp/athena/latest/ug/creating-tables.html#creating-tables-how-to)を参照してください。
![スクリーンショット 2020-12-10 16.06.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44650/9a4073a8-be1f-1efe-8f9d-6c75902fb2de.png)

Glue Crawlerに `cheat-detection-sample` などと名前をつけて次に進みます。
![スクリーンショット 2020-12-10 16.09.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44650/73ebdb79-63ee-822e-a6a3-21dc0f140287.png)

`Add a data store` のステップでは下記のように `Include Path`に先ほどメモした検証用のデータのアップロード先のパスを指定します。
![スクリーンショット 2020-12-10 16.52.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44650/4bbdcbf9-54c0-237d-214f-ed939acbbaed.png)

最後にクローラーの出力先のデータベースを設定します。新規にデータベースを作成して、データベース名を`cheat-detection-sample`とします。
![スクリーンショット 2020-12-10 16.14.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44650/ae84e8af-89e9-6ccb-ca1b-7751af111c97.png)

Crawlerの設定が完了したら、最後に `Run crawler` を実行してデータソースからテーブルを作成します。クーロールが完了すると`cheat-detection-sample`という名前のデータベースとその中に `sample_data` というテーブルが出来上がっているはずです。
![スクリーンショット 2020-12-10 16.53.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44650/e0e0961a-8d63-7ef9-7835-62befbaca0ff.png)

### モデルを使用してAthenaでチート分析を行う
さて、データソースが準備できたところで、Athenaの画面に戻りましょう。
下記のクエリをコピーして、Athenaのクエリエディタに貼り付け、クエリを実行してみます。

```SQL:SQL
USING FUNCTION predict_cheating_log(player_id BIGINT, level BIGINT, score BIGINT, units BIGINT, gold BIGINT, potions BIGINT, friends BIGINT, inv_items BIGINT, timestamp VARCHAR) 
    RETURNS VARCHAR TYPE 
    SAGEMAKER_INVOKE_ENDPOINT WITH (sagemaker_endpoint = 'cheat-detection-sample') 
SELECT *, predict_cheating_log(player_id, level, score, units, gold, potions, friends, inv_items, timestamp) AS cheat
     FROM "cheat-detection-sample"."sample_data" 
     WHERE predict_cheating_log(player_id, level, score, units, gold, potions, friends, inv_items, timestamp) = 'fraud' limit 100;
```

実行すると、作成たモデルがチートと判断したユーザーのレコードの最初の100件を取ってきて表示します。
cheat列に記載されているのが推論によりチートと判断されたレコードです。`fraud` ならチート、`legit`なら正常です。
推論によるcheat列の結果と事前にラベルづけされた正解データを示すcheat_labelの情報が一致しいていることが確認できると思います。
![スクリーンショット 2020-12-10 17.51.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44650/23562bfb-3f07-0368-6273-d2261586a09b.png)

結果の一番最初に表示されいてる`player_id` が`15316`のユーザーが怪しいと思えば、下記のクエリを実行して、このユーザーの行動ログを全て表示してみます。
そうすると、他の時間帯においても、常にチート行為をしていることがわかります。

```SQL:SQL
USING FUNCTION predict_cheating_log(player_id BIGINT, level BIGINT, score BIGINT, units BIGINT, gold BIGINT, potions BIGINT, friends BIGINT, inv_items BIGINT, timestamp VARCHAR) 
    RETURNS VARCHAR TYPE 
    SAGEMAKER_INVOKE_ENDPOINT WITH (sagemaker_endpoint = 'cheat-detection-sample') 
SELECT *, predict_cheating_log(player_id, level, score, units, gold, potions, friends, inv_items, timestamp) AS cheat
     FROM "cheat-detection-sample"."sample_data" 
     WHERE player_id = 15316;
```
![スクリーンショット 2020-12-10 18.03.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44650/8acee13c-89de-f167-41f4-4e8be32d5d5d.png)

### お片づけ
最後に、今回利用したリソースを削除しておきます。SageMaker Studioのnotebook上へ下記のコードをコピーしてリソースを削除します。

```pyhton:python
sess.delete_endpoint(endpoint_name=ep_name)
```
```sh
%%sh
aws s3 rm --recursive s3://sagemaker-us-east-2-ACCOUNT_NUMBER/sagemaker/cheat-detection/
```