Kakapoを用いたDatabricksにおける外れ値検知の実践

こちらの続編です。ノートブックをウォークスルーします。

https://qiita.com/taka_yayoi/items/fa12fb46ecee06d86ebc

ノートブックはこちらのものを。自分の勉強がてら一部変更して実行しています。あと、わかりやすさのために特徴量の数は2にしています。

https://d1r5llqwmkrl74.cloudfront.net/notebooks/PUB/rare-event-inspection/index.html

```py
%pip install databricks-kakapo pyod --quiet
```

```py
from hyperopt import fmin, tpe, hp, Trials, SparkTrials, STATUS_OK
from hyperopt.pyll.base import scope
from pyspark.sql.functions import struct, col, when, array
from pyspark.sql.types import DoubleType
from pyod.utils.data import generate_data

import mlflow
import kakapo
import uuid
import sys
```

ハイパーパラメーター探索の結果を追跡するためのMLflowエクスペリメントを作成します。

```py
user = spark.sql("select current_user()").take(1)[0][0]
mlflow.set_experiment(f"/Users/{user}/rare_events")
```
```
<Experiment: artifact_location='dbfs:/databricks/mlflow-tracking/2646050860752536', creation_time=1720592744550, experiment_id='2646050860752536', last_update_time=1720593860261, lifecycle_stage='active', name='/Users/takaaki.yayoi@databricks.com/rare_events', tags={'mlflow.experiment.sourceName': '/Users/takaaki.yayoi@databricks.com/rare_events',
 'mlflow.experimentType': 'MLFLOW_EXPERIMENT',
 'mlflow.ownerEmail': 'takaaki.yayoi@databricks.com',
 'mlflow.ownerId': '7459477216523290'}>
```

# 1. 合成データの生成

2つの特徴量と特定の割合の"外れ値"を持つシンプルな合成データを作成するために、PyODの組み込みメソッド`generate_data`を使用し、トレーニングセット、テストセットにデータを分割します。

```py
contamination = 0.1  # 外れ値の割合
n_train = 2000  # トレーニングポイントの数
n_test = 500  # テスト用ポイントの数

# サンプルデータの生成
X_train, X_test, y_train, y_test = \
    generate_data(n_train=n_train,
                  n_test=n_test,
                  n_features=2,
                  contamination=contamination,
                  random_state=42)
```

## 合成データの確認

```py
import pandas as pd

# nparrayからpandasのDataFrameに変換
dataset = pd.DataFrame({'Column1': X_train[:, 0], 'Column2': X_train[:, 1], 'anomaly': y_train})
display(dataset)
```

散布図で表示します。わかりやすい外れ値。
![chart.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/65e923b8-8e42-3b9e-33b7-82f7d4c6e2b8.png)

可視化の設定はこちらです。
![Screenshot 2024-07-10 at 17.26.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/a0841f73-1087-bc09-b264-195b2f24bf6b.png)


# 2. Kakapoのデフォルト設定を用いたモデルユニバースとパラメーター探索空間の定義

モデルスペースとハイパーパラメーター探索空間にKakapoのデフォルト設定を用いて、複数の外れ値検知モデルをトレーニングし、ハイパーパラメーターの探索を実行します。

```py
# デフォルトのモデル空間をロード
model_space = kakapo.get_default_model_space()
print("Default model space: {}".format(model_space))
```
```
Default model space: {'ecod': <class 'pyod.models.ecod.ECOD'>, 'abod': <class 'pyod.models.abod.ABOD'>, 'iforest': <class 'pyod.models.iforest.IForest'>, 'copod': <class 'pyod.models.copod.COPOD'>, 'inne': <class 'pyod.models.inne.INNE'>}
```

```py
# デフォルトのハイパーパラメーター探索空間をロード
search_space = kakapo.get_default_search_space()
print("Default search space: {}".format(search_space))
```
```
Default search space: [{'type': 'iforest', 'n_estimators': <hyperopt.pyll.base.Apply object at 0x7fd91727b110>, 'max_features': <hyperopt.pyll.base.Apply object at 0x7fd917a78e90>}, {'type': 'inne', 'n_estimators': <hyperopt.pyll.base.Apply object at 0x7fd9172ceb10>, 'max_samples': <hyperopt.pyll.base.Apply object at 0x7fd9149aafd0>}, {'type': 'abod', 'n_neighbors': <hyperopt.pyll.base.Apply object at 0x7fd9172c0c10>}, {'type': 'ecod'}, {'type': 'copod'}]
```

```py
# 探索空間をhyperoptにロード
space = hp.choice('model_type', search_space)
```

```py
# 外れ値モデルを評価するために正解データがあるか無いかを制御
GROUND_TRUTH_OD_EXISTS = True

# MLFlowエクスペリメントを保存する際のユニークなランID
uid = uuid.uuid4().hex
```

```py
# hyperoptを用いたモデルトレーニング & ハイパーパラメーターチューニングの並列実行
with mlflow.start_run(run_name=uid):
  best_params = fmin(
    trials=SparkTrials(parallelism=10),
    fn = lambda params: kakapo.train_outlier_detection(params, model_space, X_train, X_test, y_test, GROUND_TRUTH_OD_EXISTS),
    space=space,
    algo=tpe.suggest,
    max_evals=50
  )
```

## テストデータに対する推論

```py
# ベストなパフォーマンスモデルとベストなモデルを選択するために用いるメトリックをピックアップするために、最新のランあるいはカスタムの親のランを使用
metric = "loss"
parentRunId = "\"" + mlflow.search_runs(filter_string="tags.model_type != ''").iloc[0]["tags.mlflow.parentRunId"] +"\""

# 現在のエクスペリメントの全ての子供のランを取得
runs = mlflow.search_runs(filter_string=f"tags.mlflow.parentRunId = {parentRunId}", order_by=[f"metrics.{metric} ASC"])
runs = runs.where(runs['status'] == 'FINISHED')

# ベストなランのIDを取得
best_run_id = runs.loc[0,'run_id']

print(best_run_id)
```

```py
# オリジナルのデータセットからSparkデータフレームを作成
X_test_spark_df = spark.createDataFrame(X_test.tolist(), schema = ["col1", "col2"])
X_test_spark_df.display()
```
![Screenshot 2024-07-10 at 17.25.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/e54d6ddc-2a53-3222-7dfc-01bcc0da3a4c.png)

```py
logged_model = f'runs:/{best_run_id}/model'

# Spark UDFとしてモデルをロード。モデルがdoubleの値を返却しない場合にはresult_typeを上書き
loaded_model = mlflow.pyfunc.spark_udf(spark, model_uri=logged_model, result_type='double')

# Sparkデータフレームに対する予測の実行
X_test_spark_df = X_test_spark_df.withColumn('predictions', loaded_model(struct(*map(col, X_test_spark_df.columns))))
```

```py
# 計算された予測値のカラムを持つデータフレームを表示
X_test_spark_df.display()
```
いい感じで検知できています。
![chart (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/3610d8b7-9a92-6531-c9e0-139ec4c46cd1.png)

## (参考)トレーニングデータに対する推論

念の為にトレーニングデータにもモデルを適用してみます。

```py
# オリジナルのデータセットからSparkデータフレームを作成
X_train_spark_df = spark.createDataFrame(X_train.tolist(), schema = ["col1", "col2"])

# Sparkデータフレームに対する予測の実行
X_train_spark_df = X_train_spark_df.withColumn('predictions', loaded_model(struct(*map(col, X_train_spark_df.columns))))

# 計算された予測値のカラムを持つデータフレームを表示
X_train_spark_df.display()
```

さすが教師あり学習。
![chart (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/f58a3a4a-6439-181c-3ae7-c3e20f1dc7c7.png)

次はアルゴリズムと探索空間を拡張して、教師なしで学習します。

# 3. Kakapoのデフォルト探索空間設定の拡張

Kakapoのデフォルト設定を拡張し、モデル空間に新たなモデルを追加し、探索空間に関連するハイパーパラメーターのオプションを追加します。

```py
# 我々の空間に追加したい新規のモデルのインポート
from pyod.models.cof import COF
```

```py
# デフォルトのモデル空間を補強
model_space = kakapo.enrich_default_model_space({"cof": COF})
print("Enriched model space: {}".format(model_space))
```
```
Enriched model space: {'ecod': <class 'pyod.models.ecod.ECOD'>, 'abod': <class 'pyod.models.abod.ABOD'>, 'iforest': <class 'pyod.models.iforest.IForest'>, 'copod': <class 'pyod.models.copod.COPOD'>, 'inne': <class 'pyod.models.inne.INNE'>, 'cof': <class 'pyod.models.cof.COF'>}
```

```py
additional_params = [
  {
    'type': 'cof',
    'n_neighbors': scope.int(hp.quniform('cof.n_neighbors', 5, 20, 5))
  }
]

# デフォルトのハイパーパラメーター探索空間を補強
search_space = kakapo.enrich_default_search_space(additional_params)
print("Enriched search space: {}".format(search_space))
```
```
Enriched search space: [{'type': 'iforest', 'n_estimators': <hyperopt.pyll.base.Apply object at 0x7fd9170eaf10>, 'max_features': <hyperopt.pyll.base.Apply object at 0x7fd9170ebe50>}, {'type': 'inne', 'n_estimators': <hyperopt.pyll.base.Apply object at 0x7fd917094690>, 'max_samples': <hyperopt.pyll.base.Apply object at 0x7fd9170e82d0>}, {'type': 'abod', 'n_neighbors': <hyperopt.pyll.base.Apply object at 0x7fd9170fb150>}, {'type': 'ecod'}, {'type': 'copod'}, {'type': 'cof', 'n_neighbors': <hyperopt.pyll.base.Apply object at 0x7fd9170f2a50>}]
```

```py
# Hyperoptに補強した探索空間をロード
space = hp.choice('model_type', search_space)
```

```py
# 外れ値モデルを評価するために正解データがあるか無いかを制御
GROUND_TRUTH_OD_EXISTS = False 

# MLFlowエクスペリメントを保存する際のユニークなランID
uid = uuid.uuid4().hex
```

```py
# hyperoptを用いたモデルトレーニング & ハイパーパラメーターチューニングの並列実行
with mlflow.start_run(run_name=uid):
  best_params = fmin(
    trials=SparkTrials(parallelism=10),
    fn = lambda params: kakapo.train_outlier_detection(params, model_space, X_train, X_test, None, GROUND_TRUTH_OD_EXISTS),
    space=space,
    algo=tpe.suggest,
    max_evals=50
  )
```

# 4. 推論にモデルを使用

トレーニングした異常検知モデルの一つをロードし、データセットに対する予測を実行します。

`mlflow.search_runs()`に`display`関数使えたんですね。

```py
# MLflowモデルのランを探索
display(mlflow.search_runs())
```
![Screenshot 2024-07-10 at 17.33.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/17925ba6-d538-6f1a-44d9-5a74355a2003.png)

すでに、教師あり学習のランが記録されているので、今回は明示的に親のランのIDを指定します。

```py
# ベストなパフォーマンスモデルとベストなモデルを選択するために用いるメトリックをピックアップするために、最新のランあるいはカスタムの親のランを使用
metric = "loss"
#parentRunId = "\"" + mlflow.search_runs(filter_string="tags.model_type != ''").iloc[0]["tags.mlflow.parentRunId"] +"\""

# 明示的に親のランIDを指定
parentRunId = "\"0e537644adc24d8d93b1f5b17020e113\""

# 現在のエクスペリメントの全ての子供のランを取得
runs = mlflow.search_runs(filter_string=f"tags.mlflow.parentRunId = {parentRunId}", order_by=[f"metrics.{metric} ASC"])
runs = runs.where(runs['status'] == 'FINISHED')

# ベストなランのIDを取得
best_run_id = runs.loc[0,'run_id']

print(best_run_id)
```

```py
# オリジナルのデータセットからSparkデータフレームを作成
X_test_spark_df = spark.createDataFrame(X_test.tolist(), schema = ["col1", "col2"])
X_test_spark_df.display()
```

```py
logged_model = f'runs:/{best_run_id}/model'

# Spark UDFとしてモデルをロード。モデルがdoubleの値を返却しない場合にはresult_typeを上書き
loaded_model = mlflow.pyfunc.spark_udf(spark, model_uri=logged_model, result_type='double')

# Sparkデータフレームに対する予測の実行
X_test_spark_df = X_test_spark_df.withColumn('predictions', loaded_model(struct(*map(col, X_test_spark_df.columns))))
```

```py
# 計算された予測値のカラムを持つデータフレームを表示
X_test_spark_df.display()
```

教師なしでもなかなか頑張ってますが、教師ありには敵わないですかね。
![chart (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/e2aed534-47dd-d77c-2b8d-26f94e2b0e81.png)


ただ、[こちら](https://qiita.com/taka_yayoi/items/fa12fb46ecee06d86ebc#ml%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B5%E3%82%A4%E3%82%AF%E3%83%AB%E7%AE%A1%E7%90%86%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8Bmlflow%E3%81%AE%E3%83%99%E3%82%B9%E3%83%88%E3%83%97%E3%83%A9%E3%82%AF%E3%83%86%E3%82%A3%E3%82%B9)でも述べているように、必ずしもラベルデータが入手できるわけではなく、その際に手も足も出ないということがないだけでも意味があると思います。ぜひご活用ください！

> ラベルなしのデータ資産のサポートを通じて、大規模なデータドメインにおける大きなペインポイントに対応しています。数百のデータセットを含むかもしれないデータドメインで、ラベル付きデータを提供することは困難です。領域全体での外れ値を分析する手段を持つことは、説明されたアプローチの非常に大きなメリットとなっています。

なお、エクスペリメントには全てのトレーニング結果が記録されています。
![Screenshot 2024-07-10 at 17.55.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/801a5248-c0c9-3bbf-279c-2042bf9405ae.png)


### はじめてのDatabricks

[はじめてのDatabricks](https://qiita.com/taka_yayoi/items/8dc72d083edb879a5e5d)

### Databricks無料トライアル

[Databricks無料トライアル](https://databricks.com/jp/try-databricks)