Skip to content

fix sklearn raw model extraction#9050

Merged
serena-ruan merged 7 commits into
mlflow:inf_paramsfrom
serena-ruan:fix_sklearn
Jul 21, 2023
Merged

fix sklearn raw model extraction#9050
serena-ruan merged 7 commits into
mlflow:inf_paramsfrom
serena-ruan:fix_sklearn

Conversation

@serena-ruan

@serena-ruan serena-ruan commented Jul 12, 2023

Copy link
Copy Markdown
Collaborator

Related Issues/PRs

Fix

What changes are proposed in this pull request?

If model_impl is not a _SklearnModelWrapper we directly return it.

How is this patch tested?

  • Existing unit/integration tests
  • New unit/integration tests
  • Manual tests (describe details, including test results, below)

Does this PR change the documentation?

  • No. You can skip the rest of this section.
  • Yes. Make sure the changed pages / sections render correctly in the documentation preview.

Release Notes

Is this a user-facing change?

  • No. You can skip the rest of this section.
  • Yes. Give a description of this change to be included in the release notes for MLflow users.

(Details in 1-2 sentences. You can just refer to another PR with a description if this PR is part of a larger change.)

What component(s), interfaces, languages, and integrations does this PR affect?

Components

  • area/artifacts: Artifact stores and artifact logging
  • area/build: Build and test infrastructure for MLflow
  • area/docs: MLflow documentation pages
  • area/examples: Example code
  • area/model-registry: Model Registry service, APIs, and the fluent client calls for Model Registry
  • area/models: MLmodel format, model serialization/deserialization, flavors
  • area/recipes: Recipes, Recipe APIs, Recipe configs, Recipe Templates
  • area/projects: MLproject format, project running backends
  • area/scoring: MLflow Model server, model deployment tools, Spark UDFs
  • area/server-infra: MLflow Tracking server backend
  • area/tracking: Tracking Service, tracking client APIs, autologging

Interface

  • area/uiux: Front-end, user experience, plotting, JavaScript, JavaScript dev server
  • area/docker: Docker use across MLflow's components, such as MLflow Projects and MLflow Models
  • area/sqlalchemy: Use of SQLAlchemy in the Tracking Service or Model Registry
  • area/windows: Windows support

Language

  • language/r: R APIs and clients
  • language/java: Java APIs and clients
  • language/new: Proposals for new client languages

Integrations

  • integrations/azure: Azure and Azure ML integrations
  • integrations/sagemaker: SageMaker integrations
  • integrations/databricks: Databricks integrations

How should the PR be classified in the release notes? Choose one:

  • rn/breaking-change - The PR will be mentioned in the "Breaking Changes" section
  • rn/none - No description will be included. The PR will be mentioned only by the PR number in the "Small Bugfixes and Documentation Updates" section
  • rn/feature - A new user-facing feature worth mentioning in the release notes
  • rn/bug-fix - A user-facing bug fix worth mentioning in the release notes
  • rn/documentation - A user-facing documentation change worth mentioning in the release notes

Comment thread mlflow/models/evaluation/default_evaluator.py
@github-actions github-actions Bot added the rn/none List under Small Changes in Changelogs. label Jul 12, 2023
@mlflow-automation

mlflow-automation commented Jul 12, 2023

Copy link
Copy Markdown
Contributor

Documentation preview for 9f2b21e will be available here when this CircleCI job completes successfully.

More info

Comment on lines +2215 to +2226
def test_extract_raw_model_for_sklearn():
mlflow_model = Model()
model_impl = Pipeline([("predict", LogisticRegression())])
mlflow.pyfunc.add_to_model(mlflow_model, loader_module="mlflow.sklearn")
pyfunc_model = mlflow.pyfunc.PyFuncModel(model_meta=mlflow_model, model_impl=model_impl)
_, raw_model = _extract_raw_model(pyfunc_model)
assert raw_model == model_impl

model_impl = _SklearnModelWrapper(model_impl)
pyfunc_model = mlflow.pyfunc.PyFuncModel(model_meta=mlflow_model, model_impl=model_impl)
_, raw_model = _extract_raw_model(pyfunc_model)
assert raw_model == model_impl

@harupy harupy Jul 12, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of testing _extract_raw_model, can we test mlflow.evaluate?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just because _extract_raw_model works fine doesn't mean it works fine in mlflow.evaluate.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, we should test a public API, instead of implementatio details like _extract_raw_model.

@harupy harupy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@serena-ruan

Copy link
Copy Markdown
Collaborator Author

@harupy

if (
self.raw_model
and not is_multinomial_classifier
and not isinstance(self.raw_model, sk_Pipeline)
):
# For mulitnomial classifier, shap.Explainer may choose Tree/Linear explainer
# for raw model, this case shap plot doesn't support it well, so exclude the
# multinomial_classifier case here.
explainer = shap.Explainer(
self.raw_model, sampled_X, feature_names=self.feature_names
)
I think this part depends on raw_model to be a sklearn model instead of the wrapper? If we return the wrapper, evaluate doesn't log those images. Should we return .sklearn_model or update the logic in default_evaluator to be compatible with the wrapper instead?

Signed-off-by: Serena Ruan <[email protected]>
@harupy

harupy commented Jul 12, 2023

Copy link
Copy Markdown
Member

Does shap.Explainer reject the wrapper instance?

@serena-ruan

Copy link
Copy Markdown
Collaborator Author

Does shap.Explainer reject the wrapper instance?

I think so. I added an Exception here:
image

It returns directly, so these three images are not logged.

Comment thread tests/evaluate/test_default_evaluator.py
:param data: Model input data.
:param params: Additional parameters to pass to the model for inference.
def predict_proba(self, *args, **kwargs):
if hasattr(self.sklearn_model, "predict_proba"):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this condition?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means this function returns None, which can lead to a hard-to-debug error. Just throwing sklearn_model has no attribute predict_proba might easier to debug.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, for this test: https://github.com/mlflow/mlflow/actions/runs/5527963781/jobs/10084241551#step:8:3032
on commit e06bf73

FAILED tests/evaluate/test_default_evaluator.py::test_svm_classifier_evaluation_disable_logging_metrics_and_artifacts - AttributeError: 'LinearSVC' object has no attribute 'predict_proba'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, how did this test pass before you made the inference params change?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because self.predict_fn, self.predict_proba_fn = _extract_predict_fn(model, self.raw_model). If the raw_model is the underlying sklearn_model, then from here it is the sklearn model's predict_proba function, instead of the wrapper's predict_proba. But if raw_model is a _SklearnModelWrapper, then self.predict_prob_fn always exist, we might need to update the logic here again since predict_proba_fn = getattr(raw_model, "predict_proba", None) is not None even the function itself returns None 🤔

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or it's fine, because

if self.predict_proba_fn is not None:
    self.y_probs = self.predict_proba_fn(self.X.copy_to_avoid_mutation())
else:
    self.y_probs = None

in the first situation it returns None as well

Comment on lines +2216 to +2227
def test_extract_raw_model_for_sklearn():
mlflow_model = Model()
model_impl = Pipeline([("predict", LogisticRegression())])
mlflow.pyfunc.add_to_model(mlflow_model, loader_module="mlflow.sklearn")
pyfunc_model = mlflow.pyfunc.PyFuncModel(model_meta=mlflow_model, model_impl=model_impl)
_, raw_model = _extract_raw_model(pyfunc_model)
assert raw_model == model_impl

model_impl = _SklearnModelWrapper(model_impl)
pyfunc_model = mlflow.pyfunc.PyFuncModel(model_meta=mlflow_model, model_impl=model_impl)
_, raw_model = _extract_raw_model(pyfunc_model)
assert raw_model == model_impl.sklearn_model

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this test.

@serena-ruan
serena-ruan marked this pull request as draft July 13, 2023 08:33
Signed-off-by: Serena Ruan <[email protected]>
return model_loader_module, model._model_impl.sklearn_model
if isinstance(model._model_impl, _SklearnModelWrapper):
return model_loader_module, model._model_impl.sklearn_model
return model_loader_module, model._model_impl

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the case it is not _SklearnModelWrapper instance ? sklearn model._model_impl should be returned by sklearn flavor _load_pyfunc

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a test case in universe/automl that directly construct a PyFuncModel by passing _model_impl which uses sklearn Pipeline model.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

emm... is it a legal usage ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess other flavor might have similar issue if we directly construct _model_impl, but _model_impl is internal attribute managed by pyfunc if I understand it correctly

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if someone want to customize _model_impl attribute in sklearn pyfunc model,

then it should not be regarded as sklearn pyfunc model but he can create a custom pyfunc model that inherits PythonModel base class.

@serena-ruan

Copy link
Copy Markdown
Collaborator Author

Will not merge this PR to master, the failing test will be fixed automatically due to the revert PR: #9059

@serena-ruan
serena-ruan changed the base branch from master to inf_params July 14, 2023 09:42
@serena-ruan
serena-ruan marked this pull request as ready for review July 20, 2023 09:29
@serena-ruan

Copy link
Copy Markdown
Collaborator Author

As we discussed, in _extract_raw_model function, we still need to check

if isinstance(model._model_impl, _SklearnModelWrapper):
return model_loader_module, model._model_impl.sklearn_model

We can refactor this part later if we wanna add a Base wrapper :)

@serena-ruan
serena-ruan requested a review from harupy July 20, 2023 09:34
Comment on lines +83 to +84
if isinstance(model._model_impl, _SklearnModelWrapper):
return model_loader_module, model._model_impl.sklearn_model

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a comment on why we need this?

@harupy harupy Jul 20, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious about what happens if we evaluate a non-sklearn model like xgboost.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there're two problems:

  1. predict_proba_fn = getattr(raw_model, "predict_proba", None)
    try:
    import xgboost
    if isinstance(raw_model, xgboost.XGBModel):
    # Because shap evaluation will pass evaluation data in ndarray format
    # (without feature names), if set validate_features=True it will raise error.
    predict_fn = partial(predict_fn, validate_features=False)
    if predict_proba_fn is not None:
    predict_proba_fn = partial(predict_proba_fn, validate_features=False)
    The raw_model will never be xgboost.XGBModel as it will be the wrapper, and predict_prob_fn will be None for all wrappers as they're not implemented.
  2. explainer = shap.Explainer(
    self.raw_model, sampled_X, feature_names=self.feature_names
    )
    Looks like shap.Explainer doesn't accept wrappers

Signed-off-by: Serena Ruan <[email protected]>
@serena-ruan
serena-ruan merged commit e7e4852 into mlflow:inf_params Jul 21, 2023
@serena-ruan
serena-ruan deleted the fix_sklearn branch July 21, 2023 06:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rn/none List under Small Changes in Changelogs.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants