Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 68 additions & 1 deletion flaml/automl/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@
import numpy as np
import sklearn
from sklearn.dummy import DummyClassifier, DummyRegressor
from sklearn.ensemble import ExtraTreesClassifier, ExtraTreesRegressor, RandomForestClassifier, RandomForestRegressor
from sklearn.ensemble import (
ExtraTreesClassifier,
ExtraTreesRegressor,
IsolationForest,
RandomForestClassifier,
RandomForestRegressor,
)
from sklearn.exceptions import ConvergenceWarning
from sklearn.linear_model import ElasticNet, LassoLars, LogisticRegression, SGDClassifier, SGDRegressor
from sklearn.preprocessing import Normalizer
Expand Down Expand Up @@ -1484,7 +1490,68 @@ def _preprocess(self, X):
X = X.to_numpy()
return X

class IsolationForestEstimator(SKLearnEstimator):
"""The class for tuning IsolationForest for anomaly detection."""

@classmethod
def search_space(cls, data_size, task, **params):
upper = max(5, min(32768, int(data_size[0])))
return {
"n_estimators": {
"domain": tune.lograndint(lower=4, upper=upper),
"init_value": 100,
Comment thread
rashidrao-pk marked this conversation as resolved.
Outdated
"low_cost_init_value": 4,
},
"max_features": {
"domain": tune.uniform(lower=0.5, upper=1.0),
"init_value": 1.0,
},
"bootstrap": {
"domain": tune.choice([False, True]),
"init_value": False,
},
}

@classmethod
def size(cls, config):
return config.get("n_estimators", 100)

@classmethod
def cost_relative2lgbm(cls):
return 1.0

def config2params(self, config: dict) -> dict:
params = super().config2params(config)
params["contamination"] = params.get("contamination", "auto")
params["max_samples"] = params.get("max_samples", "auto")
params.pop("n_jobs", None)
return params

def __init__(self, task="anomaly_detection", **config):
super().__init__(task, **config)
random_seed = self.params.pop("random_seed", config.get("random_seed", 10242048))
if "random_state" not in self.params:
self.params["random_state"] = random_seed
self.estimator_class = IsolationForest

def fit(self, X_train, y_train=None, budget=None, free_mem_ratio=0, **kwargs):
kwargs.pop("is_retrain", None)
return super().fit(
X_train,
None,
budget=budget,
free_mem_ratio=free_mem_ratio,
**kwargs,
)

def score_samples(self, X):
X = self._preprocess(X)
return self._model.score_samples(X)

def decision_function(self, X):
X = self._preprocess(X)
return self._model.decision_function(X)

class LGBMEstimator(BaseEstimator):
Comment thread
rashidrao-pk marked this conversation as resolved.
"""The class for tuning LGBM, using sklearn API."""

Expand Down
8 changes: 7 additions & 1 deletion flaml/automl/task/generic_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def estimators(self):
TransformersEstimatorModelSelection,
XGBoostLimitDepthEstimator,
XGBoostSklearnEstimator,
IsolationForestEstimator,
)

self._estimators = {
Expand Down Expand Up @@ -97,6 +98,7 @@ def estimators(self):
"svc_spark": SparkLinearSVCEstimator,
"gbt_spark": SparkGBTEstimator,
"aft_spark": SparkAFTSurvivalRegressionEstimator,
"isolation_forest": IsolationForestEstimator,
}
return self._estimators

Expand Down Expand Up @@ -1298,7 +1300,9 @@ def default_estimator_list(self, estimator_list: List[str], is_spark_dataframe:
"estimators are removed."
)
return estimator_list
if self.is_rank():
if self.is_anomaly_detection():
estimator_list = ["isolation_forest"]
elif self.is_rank():
Comment thread
rashidrao-pk marked this conversation as resolved.
estimator_list = ["lgbm", "xgboost", "xgb_limitdepth", "lgbm_spark"]
elif self.is_nlp():
estimator_list = ["transformer"]
Expand Down Expand Up @@ -1364,6 +1368,8 @@ def default_metric(self, metric: str) -> str:
return "mape"
elif self.is_rank():
return "ndcg"
elif self.is_anomaly_detection():
return "ap"
Comment thread
rashidrao-pk marked this conversation as resolved.
Comment on lines +1377 to +1378
else:
return "r2"

Expand Down
6 changes: 6 additions & 0 deletions flaml/automl/task/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
TS_FORECASTCLASSIFICATION,
TS_FORECASTPANEL,
)

ANOMALY_DETECTION = "anomaly_detection"

CLASSIFICATION = (
"binary",
"multiclass",
Expand Down Expand Up @@ -296,6 +299,9 @@ def is_nlp(self) -> bool:
def is_nlg(self) -> bool:
return self.name in NLG_TASKS

def is_anomaly_detection(self) -> bool:
return self.name == ANOMALY_DETECTION

def is_classification(self) -> bool:
return self.name in CLASSIFICATION

Expand Down
38 changes: 38 additions & 0 deletions test/automl/test_anomaly_detection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import numpy as np
from sklearn.datasets import make_blobs
from sklearn.metrics import roc_auc_score

from flaml.automl.model import IsolationForestEstimator
Comment on lines +5 to +6


def test_isolation_forest_anomaly_estimator():
X_normal, _ = make_blobs(
n_samples=100,
centers=1,
cluster_std=0.5,
random_state=42,
)

rng = np.random.RandomState(42)
X_anomaly = rng.uniform(low=6, high=8, size=(20, 2))

X_test = np.vstack([X_normal, X_anomaly])
y_test = np.array([0] * len(X_normal) + [1] * len(X_anomaly))

model = IsolationForestEstimator(
n_estimators=50,
contamination=0.15,
random_state=42,
)

model.fit(X_normal)

preds = model.predict(X_test)
scores = -model.decision_function(X_test)

assert preds.shape == y_test.shape
assert scores.shape == y_test.shape
assert set(preds).issubset({-1, 1})

auc = roc_auc_score(y_test, scores)
assert auc > 0.9