178 lines
6.0 KiB
Python
178 lines
6.0 KiB
Python
import importlib
|
|
import logging
|
|
from typing import TYPE_CHECKING, Any, Callable
|
|
|
|
import pandas as pd
|
|
|
|
if TYPE_CHECKING:
|
|
from sklearn.base import BaseEstimator
|
|
|
|
import mlflow
|
|
from mlflow import MlflowException
|
|
from mlflow.models import EvaluationMetric
|
|
from mlflow.models.evaluation.evaluators.classifier import _get_binary_classifier_metrics
|
|
from mlflow.models.evaluation.evaluators.regressor import _get_regressor_metrics
|
|
from mlflow.recipes.utils.metrics import RecipeMetric, _load_custom_metrics
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
_AUTOML_DEFAULT_TIME_BUDGET = 600
|
|
_MLFLOW_TO_FLAML_METRICS = {
|
|
"mean_absolute_error": "mae",
|
|
"mean_squared_error": "mse",
|
|
"root_mean_squared_error": "rmse",
|
|
"r2_score": "r2",
|
|
"mean_absolute_percentage_error": "mape",
|
|
"f1_score": "f1",
|
|
"f1_score_micro": "micro_f1",
|
|
"f1_score_macro": "macro_f1",
|
|
"accuracy_score": "accuracy",
|
|
"roc_auc": "roc_auc",
|
|
"roc_auc_ovr": "roc_auc_ovr",
|
|
"roc_auc_ovo": "roc_auc_ovo",
|
|
"log_loss": "log_loss",
|
|
}
|
|
|
|
# metrics that are not supported natively in FLAML
|
|
_SKLEARN_METRICS = ["recall_score", "precision_score"]
|
|
|
|
|
|
def get_estimator_and_best_params(
|
|
X,
|
|
y,
|
|
task: str,
|
|
extended_task: str,
|
|
step_config: dict[str, Any],
|
|
recipe_root: str,
|
|
evaluation_metrics: dict[str, RecipeMetric],
|
|
primary_metric: str,
|
|
) -> tuple["BaseEstimator", dict[str, Any]]:
|
|
return _create_model_automl(
|
|
X, y, task, extended_task, step_config, recipe_root, evaluation_metrics, primary_metric
|
|
)
|
|
|
|
|
|
def _create_custom_metric_flaml(
|
|
task: str, metric_name: str, coeff: int, eval_metric: EvaluationMetric
|
|
) -> Callable:
|
|
def calc_metric(X, y, estimator) -> dict[str, float]:
|
|
y_pred = estimator.predict(X)
|
|
builtin_metrics = (
|
|
_get_regressor_metrics(y, y_pred, sample_weights=None)
|
|
if task == "regression"
|
|
else _get_binary_classifier_metrics(y_true=y, y_pred=y_pred)
|
|
)
|
|
res_df = pd.DataFrame()
|
|
res_df["prediction"] = y_pred
|
|
res_df["target"] = y if task == "classification" else y.values
|
|
return eval_metric.eval_fn(res_df, builtin_metrics)
|
|
|
|
def custom_metric(
|
|
X_val,
|
|
y_val,
|
|
estimator,
|
|
labels,
|
|
X_train,
|
|
y_train,
|
|
weight_val=None,
|
|
weight_train=None,
|
|
*args,
|
|
):
|
|
val_metric = coeff * calc_metric(X_val, y_val, estimator)
|
|
train_metric = calc_metric(X_train, y_train, estimator)
|
|
main_metric = coeff * val_metric
|
|
return main_metric, {
|
|
f"{metric_name}_train": train_metric,
|
|
f"{metric_name}_val": val_metric,
|
|
}
|
|
|
|
return custom_metric
|
|
|
|
|
|
def _create_sklearn_metric_flaml(metric_name: str, coeff: int, avg: str = "binary") -> Callable:
|
|
def sklearn_metric(
|
|
X_val,
|
|
y_val,
|
|
estimator,
|
|
labels,
|
|
X_train,
|
|
y_train,
|
|
weight_val=None,
|
|
weight_train=None,
|
|
*args,
|
|
):
|
|
custom_metrics_mod = importlib.import_module("sklearn.metrics")
|
|
eval_fn = getattr(custom_metrics_mod, metric_name)
|
|
val_metric = coeff * eval_fn(y_val, estimator.predict(X_val), average=avg)
|
|
train_metric = coeff * eval_fn(y_train, estimator.predict(X_train), average=avg)
|
|
return val_metric, {
|
|
f"{metric_name}_train": train_metric,
|
|
f"{metric_name}_val": val_metric,
|
|
}
|
|
|
|
return sklearn_metric
|
|
|
|
|
|
def _create_model_automl(
|
|
X,
|
|
y,
|
|
task: str,
|
|
extended_task: str,
|
|
step_config: dict[str, Any],
|
|
recipe_root: str,
|
|
evaluation_metrics: dict[str, RecipeMetric],
|
|
primary_metric: str,
|
|
) -> tuple["BaseEstimator", dict[str, Any]]:
|
|
try:
|
|
from flaml import AutoML
|
|
except ImportError:
|
|
raise MlflowException("Please install FLAML to use AutoML!")
|
|
|
|
try:
|
|
if primary_metric in _MLFLOW_TO_FLAML_METRICS and primary_metric in evaluation_metrics:
|
|
metric = _MLFLOW_TO_FLAML_METRICS[primary_metric]
|
|
if primary_metric == "roc_auc" and extended_task == "classification/multiclass":
|
|
metric = "roc_auc_ovr"
|
|
elif primary_metric in _SKLEARN_METRICS and primary_metric in evaluation_metrics:
|
|
metric = _create_sklearn_metric_flaml(
|
|
primary_metric,
|
|
-1 if evaluation_metrics[primary_metric].greater_is_better else 1,
|
|
"macro" if extended_task in ["classification/multiclass"] else "binary",
|
|
)
|
|
elif primary_metric in evaluation_metrics:
|
|
metric = _create_custom_metric_flaml(
|
|
task,
|
|
primary_metric,
|
|
-1 if evaluation_metrics[primary_metric].greater_is_better else 1,
|
|
_load_custom_metrics(recipe_root, [evaluation_metrics[primary_metric]])[0],
|
|
)
|
|
else:
|
|
raise MlflowException(
|
|
f"There is no FLAML alternative or custom metric for {primary_metric} metric."
|
|
)
|
|
|
|
automl_settings = step_config.get("flaml_params", {})
|
|
automl_settings["time_budget"] = step_config.get(
|
|
"time_budget_secs", _AUTOML_DEFAULT_TIME_BUDGET
|
|
)
|
|
automl_settings["metric"] = metric
|
|
automl_settings["task"] = task
|
|
# Disabled Autologging, because during the hyperparameter search
|
|
# it tries to log the same parameters multiple times.
|
|
mlflow.autolog(disable=True)
|
|
automl = AutoML()
|
|
automl.fit(X, y, **automl_settings)
|
|
mlflow.autolog(disable=False, log_models=False)
|
|
if automl.model is None:
|
|
raise MlflowException(
|
|
"AutoML (FLAML) could not train a suitable algorithm. "
|
|
"Maybe you should increase `time_budget_secs`parameter "
|
|
"to give AutoML process more time."
|
|
)
|
|
return automl.model.estimator, automl.best_config
|
|
except Exception as e:
|
|
_logger.warning(e, exc_info=e, stack_info=True)
|
|
raise MlflowException(
|
|
f"Error has occurred during training of AutoML model using FLAML: {e!r}"
|
|
)
|