From c695813c5124dfd7dbffb18347ad97112c5d8ddd Mon Sep 17 00:00:00 2001 From: zilto Date: Thu, 6 Jun 2024 14:05:25 -0400 Subject: [PATCH 01/12] added MLFlow model materialziers with tests --- hamilton/plugins/mlflow_extensions.py | 165 ++++++++++++++++++++++ tests/plugins/test_mlflow_extension.py | 186 +++++++++++++++++++++++++ 2 files changed, 351 insertions(+) create mode 100644 hamilton/plugins/mlflow_extensions.py create mode 100644 tests/plugins/test_mlflow_extension.py diff --git a/hamilton/plugins/mlflow_extensions.py b/hamilton/plugins/mlflow_extensions.py new file mode 100644 index 000000000..340ed7a1d --- /dev/null +++ b/hamilton/plugins/mlflow_extensions.py @@ -0,0 +1,165 @@ +import dataclasses +import pathlib +from typing import Any, Callable, Collection, Dict, Literal, Optional, Tuple, Type, Union + +try: + import mlflow +except ImportError: + raise NotImplementedError("MLFlow is not installed.") + +from hamilton import registry +from hamilton.io.data_adapters import DataLoader, DataSaver + + +@dataclasses.dataclass +class MLFlowModelSaver(DataSaver): + def __init__( + self, + path: Union[str, pathlib.Path] = "model", + mode: Literal["save", "log"] = "save", + flavor: Optional[str] = None, + run_id: Optional[str] = None, + **kwargs, + ): + """ + :param path: Specify a filesystem path or model URI for MLFlow runs or registry + :param mode: `save` will store to local filesystem; `log` will add to MLFlow registry + :param flavor: sklearn, xgboost, etc. + :param run_id: Explicit run id used for `mode=log`. Otherwise, will use active run or create one. + :param kwargs: additional arguments to pass to `.save_model()` and `.log_model()`. + They can be flavor-specific. + """ + self.path = path + self.mode = mode + self.flavor = flavor + self.run_id = run_id + self.kwargs = kwargs + + @classmethod + def name(cls) -> str: + return "mlflow" + + @classmethod + def applicable_types(cls) -> Collection[Type]: + return [Callable] + + def save_data(self, data) -> Dict[str, Any]: + if self.flavor: + flavor = self.flavor + else: + # infer the flavor from the base module of the data class + # for example, extract `sklearn` from `sklearn.linear_model._base` + flavor, _, _ = data.__module__.partition(".") + + # retrieve the `mlflow.FLAVOR` submodule to use `.save_model()` and `.log_model()` + try: + flavor_module = getattr(mlflow, flavor) + except ImportError: + raise ImportError(f"Flavor {flavor} is unsupported by MLFlow") + + if self.mode == "save": + # .save_model() doesn't return anything + flavor_module.save_model(data, self.path, **self.kwargs) + metadata = dict(path=self.path, mode="save", flavor=flavor, **self.kwargs) + + elif self.mode == "log": + # handle `run_id` and active run conflicts + if mlflow.active_run() and self.run_id: + if mlflow.active_run().info.run_id != self.run_id: + raise RuntimeError( + "The MLFlowModelSaver `run_id` doesn't match the active `run_id`\n", + "Leave the `run_id` to None to save to the active MLFlow run.", + ) + + # save to active run + if mlflow.active_run(): + model_info = flavor_module.log_model(data, self.path, **self.kwargs) + # create a run with `run_id` and save to it + else: + with mlflow.start_run(run_id=self.run_id): + model_info = flavor_module.log_model(data, self.path, **self.kwargs) + + metadata = {k.strip("_"): v for k, v in model_info.__dict__.items()} + return metadata + + +# TODO handle loading from file, run, or registry + + +@dataclasses.dataclass +class MLFlowModelLoader(DataLoader): + def __init__( + self, + flavor: str, + path: Union[str, pathlib.Path] = "model", + model_uri: Optional[str] = None, + mode: Literal["filesystem", "runs", "registry"] = "filesystem", + run_id: Optional[str] = None, + model_name: Optional[str] = None, + version: Union[str, int] = "latest", + **kwargs, + ): + """ """ + self.flavor = flavor + self.path = path + self.model_uri = model_uri + self.mode = mode + self.run_id = run_id + self.model_name = model_name + self.version = version + self.kwargs = kwargs + + # if self.model_uri: + # if "runs:/" in self.model_uri: + # self.mode = "runs" + # # extract info from run model_uri + # _, _, remainder = self.model_uri.partition("runs:/") + # run_id, _, inferred_path = remainder.partition("/") + # self.run_id = run_id + # self.path = inferred_path + + # elif "models:/" in self.model_uri: + # self.mode = "registry" + # # extract info from registry model_uri + # _, _, remainder = self.model_uri.partition("models:/") + # model_name, _, version = remainder.partition("/") + # self.model_name = model_name + # self.model_version = version + if not self.model_uri: + if self.mode == "filesystem": + self.model_uri = pathlib.Path(self.path).as_uri() + elif self.mode == "runs": + self.model_uri = f"runs:/{self.run_id}/{self.path}" + elif self.mode == "registry": + self.model_uri = f"models:/{self.model_name}/{self.version}" + + @classmethod + def name(cls) -> str: + return "mlflow" + + @classmethod + def applicable_types(cls) -> Collection[Type]: + return [Callable] + + def load_data(self, type_: Type) -> Tuple[Any, Dict[str, Any]]: + try: + flavor_module = getattr(mlflow, self.flavor) + except ImportError: + raise ImportError(f"Flavor {self.flavor} is unsupported by MLFlow") + + model = flavor_module.load_model(model_uri=self.model_uri) + model_info = mlflow.models.model.get_model_info(self.model_uri) + metadata = {k.strip("_"): v for k, v in model_info.__dict__.items()} + return model, metadata + + +def register_data_loaders(): + """Function to register the data loaders for this extension.""" + for loader in [ + MLFlowModelSaver, + MLFlowModelLoader, + ]: + registry.register_adapter(loader) + + +register_data_loaders() diff --git a/tests/plugins/test_mlflow_extension.py b/tests/plugins/test_mlflow_extension.py new file mode 100644 index 000000000..e784eba22 --- /dev/null +++ b/tests/plugins/test_mlflow_extension.py @@ -0,0 +1,186 @@ +from pathlib import Path + +import mlflow +import numpy as np +import pytest +from sklearn.base import BaseEstimator +from sklearn.linear_model import LinearRegression + +from hamilton.plugins.mlflow_extensions import MLFlowModelLoader, MLFlowModelSaver + + +@pytest.fixture +def fitted_sklearn_model() -> BaseEstimator: + model = LinearRegression() + model.fit([[0]], [[0]]) + return model + + +def coefficients_are_equal(model1, model2) -> bool: + """Check if two linear models have the same coefficients""" + return np.allclose(model1.coef_, model2.coef_) and np.allclose( + model1.intercept_, model2.intercept_ + ) + + +def test_mlflow_save_model(fitted_sklearn_model: BaseEstimator, tmp_path: Path): + model_path = tmp_path / "sklearn_model" + saver = MLFlowModelSaver(path=model_path, mode="save", flavor="sklearn") + expected_files = ["model.pkl", "conda.yaml", "MLmodel", "requirements.txt", "python_env.yaml"] + + # using MLFlow saver + saver.save_data(fitted_sklearn_model) + created_files = [str(p.name) for p in model_path.iterdir()] + # loading the saved model + loaded_model = mlflow.sklearn.load_model(model_path) + + assert model_path.exists() + assert set(created_files) == set(expected_files) + assert coefficients_are_equal(fitted_sklearn_model, loaded_model) + + +def test_mlflow_log_model_to_active_run(fitted_sklearn_model: BaseEstimator, tmp_path: Path): + model_path = tmp_path / "sklearn_model" + saver = MLFlowModelSaver(mode="log", flavor="sklearn") + + mlflow.set_tracking_uri(model_path.as_uri()) + with mlflow.start_run(): + # save model + metadata = saver.save_data(fitted_sklearn_model) + # reload model + loaded_model = mlflow.sklearn.load_model(metadata["model_uri"]) + + assert np.allclose(fitted_sklearn_model.coef_, loaded_model.coef_) and np.allclose( + fitted_sklearn_model.intercept_, loaded_model.intercept_ + ) + + +def test_mlflow_log_model_to_specific_run(fitted_sklearn_model: BaseEstimator, tmp_path: Path): + model_path = tmp_path / "sklearn_model" + # create a "previous run" + mlflow.set_tracking_uri(model_path.as_uri()) + mlflow.start_run() + run_id = mlflow.active_run().info.run_id + mlflow.end_run() + saver = MLFlowModelSaver(mode="log", flavor="sklearn", run_id=run_id) + + # save model + metadata = saver.save_data(fitted_sklearn_model) + # reload model + loaded_model = mlflow.sklearn.load_model(metadata["model_uri"]) + + assert np.allclose(fitted_sklearn_model.coef_, loaded_model.coef_) and np.allclose( + fitted_sklearn_model.intercept_, loaded_model.intercept_ + ) + + +def test_mlflow_log_model_active_and_specific_run_ids_are_equal( + fitted_sklearn_model: BaseEstimator, tmp_path: Path +): + model_path = tmp_path / "sklearn_model" + + mlflow.set_tracking_uri(model_path.as_uri()) + with mlflow.start_run(): + run_id = mlflow.active_run().info.run_id + saver = MLFlowModelSaver(mode="log", flavor="sklearn", run_id=run_id) + # save model + metadata = saver.save_data(fitted_sklearn_model) + # reload model + loaded_model = mlflow.sklearn.load_model(metadata["model_uri"]) + + assert np.allclose(fitted_sklearn_model.coef_, loaded_model.coef_) and np.allclose( + fitted_sklearn_model.intercept_, loaded_model.intercept_ + ) + + +def test_mlflow_log_model_active_and_specific_run_ids_are_unequal( + fitted_sklearn_model: BaseEstimator, tmp_path: Path +): + model_path = tmp_path / "sklearn_model" + mlflow.set_tracking_uri(model_path.as_uri()) + mlflow.start_run() + run_id = mlflow.active_run().info.run_id + mlflow.end_run() + saver = MLFlowModelSaver(mode="log", flavor="sklearn", run_id=run_id) + + with mlflow.start_run(): + # save model + with pytest.raises(RuntimeError): + saver.save_data(fitted_sklearn_model) + + +def test_mlflow_load_local_model(fitted_sklearn_model: BaseEstimator, tmp_path: Path): + model_path = tmp_path / "sklearn_model" + mlflow.sklearn.save_model(fitted_sklearn_model, model_path) + loader = MLFlowModelLoader(path=model_path, flavor="sklearn") + + loaded_model, metadata = loader.load_data(LinearRegression) + + assert coefficients_are_equal(fitted_sklearn_model, loaded_model) + + +def test_mlflow_load_runs_model(fitted_sklearn_model: BaseEstimator, tmp_path: Path): + mlflow_path = tmp_path / "mlflow_path" + artifact_path = "model" + mlflow.set_tracking_uri(mlflow_path.as_uri()) + with mlflow.start_run(): + run_id = mlflow.active_run().info.run_id + mlflow.sklearn.log_model(fitted_sklearn_model, artifact_path=artifact_path) + + # specify run via model_uri + loader = MLFlowModelLoader(model_uri=f"runs:/{run_id}/{artifact_path}", flavor="sklearn") + loaded_model, metadata = loader.load_data(LinearRegression) + assert coefficients_are_equal(fitted_sklearn_model, loaded_model) + + # specify run via arguments + loader = MLFlowModelLoader(path=artifact_path, run_id=run_id, mode="runs", flavor="sklearn") + loaded_model, metadata = loader.load_data(LinearRegression) + assert coefficients_are_equal(fitted_sklearn_model, loaded_model) + + +def test_mlflow_load_registry_model(fitted_sklearn_model: BaseEstimator, tmp_path: Path): + mlflow_path = tmp_path / "mlflow_path" + artifact_path = "model" + model_name = "my_registered_model" + version = 1 + # track a model + mlflow.set_tracking_uri(mlflow_path.as_uri()) + with mlflow.start_run(): + run_id = mlflow.active_run().info.run_id + mlflow.sklearn.log_model(fitted_sklearn_model, artifact_path=artifact_path) + # register the model + run_model_uri = f"runs:/{run_id}/{artifact_path}" + mlflow.register_model(run_model_uri, model_name) + + # specify via model_uri + loader = MLFlowModelLoader(model_uri=f"models:/{model_name}/{version}", flavor="sklearn") + loaded_model, metadata = loader.load_data(LinearRegression) + assert coefficients_are_equal(fitted_sklearn_model, loaded_model) + + # specify via arguments + loader = MLFlowModelLoader( + mode="registry", model_name=model_name, version=version, flavor="sklearn" + ) + loaded_model, metadata = loader.load_data(LinearRegression) + assert coefficients_are_equal(fitted_sklearn_model, loaded_model) + + +def test_mlflow_infer_flavor(fitted_sklearn_model: BaseEstimator, tmp_path: Path): + model_path = tmp_path / "sklearn_model" + saver = MLFlowModelSaver(path=model_path) + + metadata = saver.save_data(fitted_sklearn_model) + + assert metadata["flavor"] == "sklearn" + + +def test_mlflow_handle_saver_kwargs(): + path = "tmp/path" + mode = "save" + flavor = "sklearn" + saver = MLFlowModelSaver(path=path, mode=mode, flavor=flavor, unknown_kwarg=True) + + assert saver.path == path + assert saver.mode == mode + assert saver.flavor == flavor + assert saver.kwargs.get("unknown_kwarg") is True From 3676d55e7147d9c654695db823cfd5d001d5513c Mon Sep 17 00:00:00 2001 From: zilto Date: Thu, 6 Jun 2024 15:53:41 -0400 Subject: [PATCH 02/12] added saving example and model registration --- examples/mlflow/train_dataflow.py | 55 ++++++++++ hamilton/function_modifiers/base.py | 1 + hamilton/plugins/mlflow_extensions.py | 141 +++++++++++++------------- 3 files changed, 127 insertions(+), 70 deletions(-) create mode 100644 examples/mlflow/train_dataflow.py diff --git a/examples/mlflow/train_dataflow.py b/examples/mlflow/train_dataflow.py new file mode 100644 index 000000000..8e4c06302 --- /dev/null +++ b/examples/mlflow/train_dataflow.py @@ -0,0 +1,55 @@ +from typing import Dict, Union + +import pandas as pd +from sklearn.base import BaseEstimator +from sklearn.datasets import fetch_openml +from sklearn.linear_model import LogisticRegression +from sklearn.model_selection import train_test_split + +from hamilton.function_modifiers import extract_fields + + +@extract_fields( + {"X_train": pd.DataFrame, "X_test": pd.DataFrame, "y_train": pd.Series, "y_test": pd.Series} +) +def dataset_splits() -> Dict[str, Union[pd.DataFrame, pd.Series]]: + """Load the titanic dataset and partition it in X_train, y_train, X_test, y_test""" + X, y = fetch_openml("titanic", version=1, as_frame=True, return_X_y=True) + + feature_cols = ["fare", "age"] + X = X[feature_cols].fillna(0) + + X_train, X_test, y_train, y_test = train_test_split(X[feature_cols], y) + return {"X_train": X_train, "X_test": X_test, "y_train": y_train, "y_test": y_test} + + +def trained_model( + X_train: pd.DataFrame, + y_train: pd.Series, +) -> BaseEstimator: + """Fit a binary classifier on the training data""" + model = LogisticRegression() + model.fit(X_train, y_train) + return model + + +if __name__ == "__main__": + import __main__ + + from hamilton import driver + from hamilton.io.materialization import to + + dr = ( + driver.Builder() + .with_modules(__main__) + .with_materializers( + to.mlflow( + id="trained_model__mlflow", + dependencies=["trained_model"], + ), + ) + .build() + ) + + results = dr.execute(["trained_model__mlflow"]) + print(results) diff --git a/hamilton/function_modifiers/base.py b/hamilton/function_modifiers/base.py index 2a876c21f..77c990143 100644 --- a/hamilton/function_modifiers/base.py +++ b/hamilton/function_modifiers/base.py @@ -40,6 +40,7 @@ "dlt", "kedro", "huggingface", + "mlflow", ] for plugin_module in plugins_modules: try: diff --git a/hamilton/plugins/mlflow_extensions.py b/hamilton/plugins/mlflow_extensions.py index 340ed7a1d..5601b26f3 100644 --- a/hamilton/plugins/mlflow_extensions.py +++ b/hamilton/plugins/mlflow_extensions.py @@ -1,5 +1,6 @@ import dataclasses import pathlib +import shutil from typing import Any, Callable, Collection, Dict, Literal, Optional, Tuple, Type, Union try: @@ -13,27 +14,33 @@ @dataclasses.dataclass class MLFlowModelSaver(DataSaver): - def __init__( - self, - path: Union[str, pathlib.Path] = "model", - mode: Literal["save", "log"] = "save", - flavor: Optional[str] = None, - run_id: Optional[str] = None, - **kwargs, - ): - """ - :param path: Specify a filesystem path or model URI for MLFlow runs or registry - :param mode: `save` will store to local filesystem; `log` will add to MLFlow registry - :param flavor: sklearn, xgboost, etc. - :param run_id: Explicit run id used for `mode=log`. Otherwise, will use active run or create one. - :param kwargs: additional arguments to pass to `.save_model()` and `.log_model()`. - They can be flavor-specific. - """ - self.path = path - self.mode = mode - self.flavor = flavor - self.run_id = run_id - self.kwargs = kwargs + """ + :param path: Specify a filesystem path or model URI for MLFlow runs or registry + :param mode: `save` will store to local filesystem; `log` will add to MLFlow registry + :param flavor: sklearn, xgboost, etc. + :param run_id: Explicit run id used for `mode=log`. Otherwise, will use active run or create one. + :param kwargs: additional arguments to pass to `.save_model()` and `.log_model()`. + They can be flavor-specific. + """ + + path: Union[str, pathlib.Path] = "model" + mode: Literal["filesystem", "runs"] = "filesystem" + flavor: Optional[str] = None + run_id: Optional[str] = None + overwrite: bool = False + register: bool = False + model_name: Optional[str] = None + kwargs: Optional[Dict[str, Any]] = None + # kwargs: Dict[str, Any] = dataclasses.field(default_factory=dict) + + # A lot of dancing around because dataclass doesn't accept kwargs + # and hamilton.function_modifiers.adapters throws `InvalidDecoratorException` for dataclasses.field() defaults + def __post_init__(self): + self.kwargs = self.kwargs if self.kwargs else {} + + # ensures that model_name is not None in case register=True + if self.model_name is None: + self.model_name = pathlib.Path(self.path).name @classmethod def name(cls) -> str: @@ -41,7 +48,7 @@ def name(cls) -> str: @classmethod def applicable_types(cls) -> Collection[Type]: - return [Callable] + return [Any] def save_data(self, data) -> Dict[str, Any]: if self.flavor: @@ -57,12 +64,16 @@ def save_data(self, data) -> Dict[str, Any]: except ImportError: raise ImportError(f"Flavor {flavor} is unsupported by MLFlow") - if self.mode == "save": + if self.mode == "filesystem": + # have to manually delete directory to avoid MLFlow exception + if self.overwrite is True: + shutil.rmtree(self.path) + # .save_model() doesn't return anything flavor_module.save_model(data, self.path, **self.kwargs) - metadata = dict(path=self.path, mode="save", flavor=flavor, **self.kwargs) + model_info = mlflow.models.get_model_info(self.path) - elif self.mode == "log": + elif self.mode == "runs": # handle `run_id` and active run conflicts if mlflow.active_run() and self.run_id: if mlflow.active_run().info.run_id != self.run_id: @@ -79,7 +90,15 @@ def save_data(self, data) -> Dict[str, Any]: with mlflow.start_run(run_id=self.run_id): model_info = flavor_module.log_model(data, self.path, **self.kwargs) - metadata = {k.strip("_"): v for k, v in model_info.__dict__.items()} + metadata = {k.strip("_"): v for k, v in model_info.__dict__.items()} + if self.register: + model_version = mlflow.register_model( + model_uri=metadata["model_uri"], name=self.model_name + ) + metadata["registered_model"] = { + k.strip("_"): v for k, v in model_version.__dict__.items() + } + return metadata @@ -88,50 +107,30 @@ def save_data(self, data) -> Dict[str, Any]: @dataclasses.dataclass class MLFlowModelLoader(DataLoader): - def __init__( - self, - flavor: str, - path: Union[str, pathlib.Path] = "model", - model_uri: Optional[str] = None, - mode: Literal["filesystem", "runs", "registry"] = "filesystem", - run_id: Optional[str] = None, - model_name: Optional[str] = None, - version: Union[str, int] = "latest", - **kwargs, - ): - """ """ - self.flavor = flavor - self.path = path - self.model_uri = model_uri - self.mode = mode - self.run_id = run_id - self.model_name = model_name - self.version = version - self.kwargs = kwargs - - # if self.model_uri: - # if "runs:/" in self.model_uri: - # self.mode = "runs" - # # extract info from run model_uri - # _, _, remainder = self.model_uri.partition("runs:/") - # run_id, _, inferred_path = remainder.partition("/") - # self.run_id = run_id - # self.path = inferred_path - - # elif "models:/" in self.model_uri: - # self.mode = "registry" - # # extract info from registry model_uri - # _, _, remainder = self.model_uri.partition("models:/") - # model_name, _, version = remainder.partition("/") - # self.model_name = model_name - # self.model_version = version - if not self.model_uri: - if self.mode == "filesystem": - self.model_uri = pathlib.Path(self.path).as_uri() - elif self.mode == "runs": - self.model_uri = f"runs:/{self.run_id}/{self.path}" - elif self.mode == "registry": - self.model_uri = f"models:/{self.model_name}/{self.version}" + flavor: str + path: Union[str, pathlib.Path] = "model" + model_uri: Optional[str] = None + mode: Literal["filesystem", "runs", "registry"] = "filesystem" + run_id: Optional[str] = None + model_name: Optional[str] = None + version: Union[str, int] = "latest" + kwargs: Optional[Dict[str, Any]] = None + # kwargs: Dict[str, Any] = dataclasses.field(default_factory=dict) + + # A lot of dancing around because dataclass doesn't accept kwargs + # and hamilton.function_modifiers.adapters throws `InvalidDecoratorException` for dataclasses.field() defaults + def __post_init__(self): + self.kwargs = self.kwargs if self.kwargs else {} + + if self.model_uri: + return + + if self.mode == "filesystem": + self.model_uri = pathlib.Path(self.path).as_uri() + elif self.mode == "runs": + self.model_uri = f"runs:/{self.run_id}/{self.path}" + elif self.mode == "registry": + self.model_uri = f"models:/{self.model_name}/{self.version}" @classmethod def name(cls) -> str: @@ -163,3 +162,5 @@ def register_data_loaders(): register_data_loaders() + +COLUMN_FRIENDLY_DF_TYPE = False From e82c887b102add3d783587093ff9b3e0b7112962 Mon Sep 17 00:00:00 2001 From: zilto Date: Mon, 10 Jun 2024 16:20:19 -0400 Subject: [PATCH 03/12] added MLFlowTracker; updated tests; updated materializer API --- examples/mlflow/README.md | 1 + examples/mlflow/requirements.txt | 5 + examples/mlflow/train_dataflow.py | 55 -- examples/mlflow/tutorial.ipynb | 662 +++++++++++++++++++++++++ hamilton/plugins/h_mlflow.py | 289 +++++++++++ hamilton/plugins/mlflow_extensions.py | 158 +++--- tests/plugins/test_mlflow_extension.py | 65 +-- 7 files changed, 1062 insertions(+), 173 deletions(-) create mode 100644 examples/mlflow/README.md create mode 100644 examples/mlflow/requirements.txt delete mode 100644 examples/mlflow/train_dataflow.py create mode 100644 examples/mlflow/tutorial.ipynb create mode 100644 hamilton/plugins/h_mlflow.py diff --git a/examples/mlflow/README.md b/examples/mlflow/README.md new file mode 100644 index 000000000..34efe08bb --- /dev/null +++ b/examples/mlflow/README.md @@ -0,0 +1 @@ +# MLFLow plugin for Hamilton diff --git a/examples/mlflow/requirements.txt b/examples/mlflow/requirements.txt new file mode 100644 index 000000000..4d5aed0a1 --- /dev/null +++ b/examples/mlflow/requirements.txt @@ -0,0 +1,5 @@ +mlflow +numpy +pandas +scikit-learn +sf-hamilton[visualization] diff --git a/examples/mlflow/train_dataflow.py b/examples/mlflow/train_dataflow.py deleted file mode 100644 index 8e4c06302..000000000 --- a/examples/mlflow/train_dataflow.py +++ /dev/null @@ -1,55 +0,0 @@ -from typing import Dict, Union - -import pandas as pd -from sklearn.base import BaseEstimator -from sklearn.datasets import fetch_openml -from sklearn.linear_model import LogisticRegression -from sklearn.model_selection import train_test_split - -from hamilton.function_modifiers import extract_fields - - -@extract_fields( - {"X_train": pd.DataFrame, "X_test": pd.DataFrame, "y_train": pd.Series, "y_test": pd.Series} -) -def dataset_splits() -> Dict[str, Union[pd.DataFrame, pd.Series]]: - """Load the titanic dataset and partition it in X_train, y_train, X_test, y_test""" - X, y = fetch_openml("titanic", version=1, as_frame=True, return_X_y=True) - - feature_cols = ["fare", "age"] - X = X[feature_cols].fillna(0) - - X_train, X_test, y_train, y_test = train_test_split(X[feature_cols], y) - return {"X_train": X_train, "X_test": X_test, "y_train": y_train, "y_test": y_test} - - -def trained_model( - X_train: pd.DataFrame, - y_train: pd.Series, -) -> BaseEstimator: - """Fit a binary classifier on the training data""" - model = LogisticRegression() - model.fit(X_train, y_train) - return model - - -if __name__ == "__main__": - import __main__ - - from hamilton import driver - from hamilton.io.materialization import to - - dr = ( - driver.Builder() - .with_modules(__main__) - .with_materializers( - to.mlflow( - id="trained_model__mlflow", - dependencies=["trained_model"], - ), - ) - .build() - ) - - results = dr.execute(["trained_model__mlflow"]) - print(results) diff --git a/examples/mlflow/tutorial.ipynb b/examples/mlflow/tutorial.ipynb new file mode 100644 index 000000000..210a62395 --- /dev/null +++ b/examples/mlflow/tutorial.ipynb @@ -0,0 +1,662 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext hamilton.plugins.jupyter_magic" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Model Training Dataflow" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1. Define" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "%3\n", + "\n", + "\n", + "cluster__legend\n", + "\n", + "Legend\n", + "\n", + "\n", + "\n", + "algo\n", + "\n", + "\n", + "\n", + "algo\n", + "logistic_regression\n", + "\n", + "\n", + "\n", + "load_data\n", + "\n", + "load_data\n", + "dict\n", + "\n", + "\n", + "\n", + "y\n", + "\n", + "y\n", + "Series\n", + "\n", + "\n", + "\n", + "load_data->y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X\n", + "\n", + "X\n", + "DataFrame\n", + "\n", + "\n", + "\n", + "load_data->X\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "y_train\n", + "\n", + "y_train\n", + "Series\n", + "\n", + "\n", + "\n", + "trained_model\n", + "\n", + "trained_model: algo\n", + "BaseEstimator\n", + "\n", + "\n", + "\n", + "y_train->trained_model\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X_train\n", + "\n", + "X_train\n", + "DataFrame\n", + "\n", + "\n", + "\n", + "X_train->trained_model\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "split_dataset\n", + "\n", + "split_dataset\n", + "dict\n", + "\n", + "\n", + "\n", + "y->split_dataset\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X_preprocessed\n", + "\n", + "X_preprocessed\n", + "DataFrame\n", + "\n", + "\n", + "\n", + "X_preprocessed->split_dataset\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "split_dataset->y_train\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "split_dataset->X_train\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X_test\n", + "\n", + "X_test\n", + "DataFrame\n", + "\n", + "\n", + "\n", + "split_dataset->X_test\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "y_test\n", + "\n", + "y_test\n", + "Series\n", + "\n", + "\n", + "\n", + "split_dataset->y_test\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "test_scatter_plot\n", + "\n", + "test_scatter_plot\n", + "Figure\n", + "\n", + "\n", + "\n", + "y_test_predictions\n", + "\n", + "y_test_predictions\n", + "Series\n", + "\n", + "\n", + "\n", + "y_test_predictions->test_scatter_plot\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "test_performance\n", + "\n", + "test_performance: algo\n", + "float\n", + "\n", + "\n", + "\n", + "y_test_predictions->test_performance\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "trained_model->y_test_predictions\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X_test->test_scatter_plot\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X_test->y_test_predictions\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X->X_preprocessed\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "y_test->test_scatter_plot\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "y_test->test_performance\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "_split_dataset_inputs\n", + "\n", + "test_size_fraction\n", + "float\n", + "\n", + "\n", + "\n", + "_split_dataset_inputs->split_dataset\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "config\n", + "\n", + "\n", + "\n", + "config\n", + "\n", + "\n", + "\n", + "input\n", + "\n", + "input\n", + "\n", + "\n", + "\n", + "function\n", + "\n", + "function\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%cell_to_module model_training --display --config algo=logistic_regression\n", + "from typing import Dict, Union\n", + "\n", + "import pandas as pd\n", + "import matplotlib.figure\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "from sklearn.base import BaseEstimator\n", + "from sklearn.datasets import fetch_openml\n", + "from sklearn.linear_model import LogisticRegression, LinearRegression\n", + "from sklearn.model_selection import train_test_split\n", + "from sklearn.metrics import balanced_accuracy_score, mean_absolute_error\n", + "from hamilton.function_modifiers import extract_fields, tag, config\n", + "\n", + "\n", + "@extract_fields(dict(X=pd.DataFrame, y=pd.Series))\n", + "def load_data() -> dict:\n", + " X, y = fetch_openml(\"titanic\", version=1, as_frame=True, return_X_y=True)\n", + " return dict(X=X, y=y)\n", + "\n", + "\n", + "def X_preprocessed(X: pd.DataFrame) -> pd.DataFrame:\n", + " column_selection = [\"fare\", \"age\"]\n", + " X = X[column_selection]\n", + " X = X.fillna(0)\n", + " return X \n", + "\n", + "\n", + "@extract_fields(dict(\n", + " X_train=pd.DataFrame,\n", + " y_train=pd.Series,\n", + " X_test=pd.DataFrame,\n", + " y_test=pd.Series,\n", + "))\n", + "def split_dataset(\n", + " X_preprocessed: pd.DataFrame,\n", + " y: pd.Series,\n", + " test_size_fraction: float = 0.3\n", + ") -> dict:\n", + " \"\"\"Load the titanic dataset and partition it in X_train, y_train, X_test, y_test\"\"\"\n", + " X_train, X_test, y_train, y_test = train_test_split(\n", + " X_preprocessed, y, test_size=test_size_fraction,\n", + " )\n", + " return dict(\n", + " X_train=X_train,\n", + " y_train=y_train,\n", + " X_test=X_test,\n", + " y_test=y_test,\n", + " )\n", + "\n", + "@tag(team=\"forecast\")\n", + "@config.when(algo=\"logistic_regression\")\n", + "def trained_model__loistic(X_train: pd.DataFrame, y_train: pd.Series, hparams: dict) -> BaseEstimator:\n", + " \"\"\"Fit a binary classifier on the training data\"\"\"\n", + " model = LogisticRegression()\n", + " model.fit(X_train, y_train)\n", + " return model\n", + "\n", + "\n", + "@tag(team=\"forecast\")\n", + "@config.when(algo=\"linear_regression\")\n", + "def trained_model__linear(X_train: pd.DataFrame, y_train: pd.Series) -> BaseEstimator:\n", + " \"\"\"Fit a binary classifier on the training data\"\"\"\n", + " model = LinearRegression()\n", + " model.fit(X_train, y_train)\n", + " return model\n", + "\n", + "def y_test_predictions(trained_model: BaseEstimator, X_test: pd.DataFrame) -> pd.Series:\n", + " return trained_model.predict(X_test)\n", + "\n", + "@config.when(algo=\"logistic_regression\")\n", + "def test_performance__logistic(y_test: pd.Series, y_test_predictions: pd.Series) -> float:\n", + " return balanced_accuracy_score(y_test, y_test_predictions)\n", + "\n", + "@config.when(algo=\"linear_regression\")\n", + "def test_performance__linear(y_test: pd.Series, y_test_predictions: pd.Series) -> float:\n", + " return mean_absolute_error(y_test, y_test_predictions)\n", + "\n", + "def test_scatter_plot(\n", + " X_test: pd.DataFrame,\n", + " y_test: pd.Series,\n", + " y_test_predictions: pd.Series,\n", + ") -> matplotlib.figure.Figure:\n", + " correctly_predicted = y_test == y_test_predictions\n", + " feature_1 = X_test.iloc[:, 0]\n", + " feature_2 = X_test.iloc[:, 1]\n", + "\n", + " fig = plt.figure()\n", + " plt.scatter(feature_1, feature_2, c=correctly_predicted)\n", + " return fig" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2. Assemble" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from hamilton import driver\n", + "from hamilton.io.materialization import to\n", + "\n", + "dr = (\n", + " driver.Builder()\n", + " .with_modules(model_training)\n", + " .with_config(dict(algo=\"logistic_regression\"))\n", + " .with_materializers(\n", + " to.mlflow(\n", + " id=\"trained_model__mlflow\",\n", + " dependencies=[\"trained_model\"],\n", + " mode=\"runs\",\n", + " register=True,\n", + " model_name=\"my_classifier\",\n", + " ),\n", + " )\n", + " .build()\n", + ")\n", + "dr" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3. Execute" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "final_vars = [\"trained_model__mlflow\", \"y_test_predictions\", \"test_performance\"]\n", + "results = dr.execute(final_vars)\n", + "dr.visualize_execution(final_vars)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# balanced accuracy on test set\n", + "print(results[\"test_performance\"])\n", + "print()\n", + "# metadata of stored model\n", + "results[\"trained_model__mlflow\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Model Inference Dataflow" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1. Define" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%cell_to_module model_inference --display\n", + "from typing import Dict, Union\n", + "\n", + "import pandas as pd\n", + "from sklearn.base import BaseEstimator\n", + "from sklearn.datasets import fetch_openml\n", + "from sklearn.linear_model import LogisticRegression\n", + "from sklearn.model_selection import train_test_split\n", + "from sklearn.metrics import balanced_accuracy_score\n", + "from hamilton.function_modifiers import extract_fields\n", + "\n", + "# import the preprocessing function from the module above\n", + "from model_training import X_preprocessed\n", + "\n", + "def preprocessed_inputs(user_input: dict) -> pd.DataFrame:\n", + " df = pd.DataFrame(user_input, index=[0])\n", + " df = X_preprocessed(df)\n", + " return df\n", + "\n", + "def prediction(preprocessed_inputs: pd.DataFrame, model: BaseEstimator) -> int:\n", + " return model.predict(preprocessed_inputs)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2. Assemble" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from hamilton import driver\n", + "from hamilton.io.materialization import from_\n", + "\n", + "dr = (\n", + " driver.Builder()\n", + " .with_modules(model_inference)\n", + " .with_materializers(\n", + " from_.mlflow(\n", + " target=\"model\",\n", + " mode=\"registry\",\n", + " model_name=\"my_classifier\",\n", + " ),\n", + " )\n", + " .build()\n", + ")\n", + "dr" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3. Execute" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "inputs = dict(user_input={\"fare\": 10.72, \"age\": 48})\n", + "\n", + "final_vars = [\"prediction\", \"load_data.model\"]\n", + "results = dr.execute(final_vars, inputs=inputs)\n", + "dr.visualize_execution(final_vars, inputs=inputs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(results[\"prediction\"])\n", + "results[\"load_data.model\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## MLFlowTracker" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "import importlib\n", + "from hamilton import driver\n", + "import hamilton.plugins.h_mlflow\n", + "from hamilton.io.materialization import to\n", + "from hamilton.plugins.h_mlflow import MLFlowTracker\n", + "importlib.reload(hamilton.plugins.h_mlflow)\n", + "\n", + "dr = (\n", + " driver.Builder()\n", + " .with_modules(model_training)\n", + " .with_config(dict(algo=\"logistic_regression\"))\n", + " .with_adapters(\n", + " hamilton.plugins.h_mlflow.MLFlowTracker()\n", + " )\n", + " .with_materializers(\n", + " to.mlflow(\n", + " id=\"trained_model__mlflow\",\n", + " dependencies=[\"trained_model\"],\n", + " mode=\"runs\",\n", + " register=True,\n", + " model_name=\"my_loom_video\",\n", + " ),\n", + " )\n", + " .build()\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/tjean/projects/dagworks/hamilton/venv/lib/python3.11/site-packages/_distutils_hack/__init__.py:11: UserWarning: Distutils was imported before Setuptools, but importing Setuptools also replaces the `distutils` module in `sys.modules`. This may lead to undesirable behaviors or errors. To avoid these issues, avoid using distutils directly, ensure that setuptools is installed in the traditional way (e.g. not an editable install), and/or make sure that setuptools is always imported before distutils.\n", + " warnings.warn(\n", + "/home/tjean/projects/dagworks/hamilton/venv/lib/python3.11/site-packages/_distutils_hack/__init__.py:26: UserWarning: Setuptools is replacing distutils.\n", + " warnings.warn(\"Setuptools is replacing distutils.\")\n", + "Registered model 'my_loom_video' already exists. Creating a new version of this model...\n", + "Created version '2' of model 'my_loom_video'.\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "inputs = dict(test_size_fraction=0.3)\n", + "results = dr.execute(\n", + " [\"trained_model__mlflow\", \"test_performance\", \"test_scatter_plot\"],\n", + " inputs=inputs\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.1" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/hamilton/plugins/h_mlflow.py b/hamilton/plugins/h_mlflow.py new file mode 100644 index 000000000..193bc68f9 --- /dev/null +++ b/hamilton/plugins/h_mlflow.py @@ -0,0 +1,289 @@ +import logging +import pickle +from typing import Any, Dict, List, Optional, Type, Union + +import mlflow +import mlflow.data + +from hamilton import graph_types +from hamilton.lifecycle import GraphConstructionHook, GraphExecutionHook, NodeExecutionHook + +FIGURE_TYPES = [] +try: + import matplotlib.figure + + FIGURE_TYPES.append(matplotlib.figure.Figure) +except ImportError: + pass + +try: + import plotly.graph_objects + + FIGURE_TYPES.append(plotly.graph_objects.Figure) +except ImportError: + pass + + +logger = logging.getLogger(__name__) + + +def get_path_from_metadata(metadata: dict) -> Union[str, None]: + """Retrieve the `path` attribute from DataSaver output metadata""" + path = None + if "path" in metadata: + path = metadata["path"] + elif "file_metadata" in metadata: + path = metadata["file_metadata"]["path"] + + return path + + +# NOTE `mlflow.client.MLFlowClient` is preferred to top-level `mlflow.` methods in MLFlowTracker +# because the latter relies on hard-to-debug global variables. Yet, we set an `active_run` by using +# `mlflow.start_run()` in pre_graph_execution to ensure the user-specified MLFlow code +# and MLFlow materializers log metrics and models to the same run as the MLFlowTracker +class MLFlowTracker( + NodeExecutionHook, + GraphExecutionHook, + GraphConstructionHook, +): + """Driver adapter logging Hamilton execution results to an MLFlow server.""" + + def __init__( + self, + tracking_uri: Optional[str] = None, + registry_uri: Optional[str] = None, + artifact_location: Optional[str] = None, + experiment_name: str = "Hamilton", + experiment_tags: Optional[dict] = None, + experiment_description: Optional[str] = None, + run_id: Optional[str] = None, + run_name: Optional[str] = None, + run_tags: Optional[dict] = None, + run_description: Optional[str] = None, + log_system_metrics: bool = False, + ): + """Configure the MLFlow client and experiment for the lifetime of the tracker + + :param tracking_uri: Destination of the logged artifacts and metadata. It can be a filesystem, database, or server. [reference](https://mlflow.org/docs/latest/getting-started/tracking-server-overview/index.html) + :param registry_uri: Destination of the registered models. By default it's the same as the tracking destination, but they can be different. [reference](https://mlflow.org/docs/latest/getting-started/registering-first-model/index.html) + :param artifact_location: Root path on tracking server where experiment is stored + :param experiment_name: MLFlow experiment name used to group runs. + :param experiment_tags: Tags to query experiments programmatically (not displayed). + :param experiment_description: Description of the experiment displayed + :param run_id: Run id to log to an existing run (every execution logs to the same run) + :param run_name: Run name displayed and used to query runs. You can have multiple runs with the same name but different run ids. + :param run_tags: Tags to query runs and appears as columns in the UI for filtering and grouping. It automatically includes serializable inputs and Driver config. + :param run_description: Description of the run displayed + :param log_system_metrics: Log system metrics to display (requires additonal dependencies) + """ + self.client = mlflow.client.MlflowClient(tracking_uri, registry_uri) + + # experiment setup + experiment_tags = experiment_tags if experiment_tags else {} + if experiment_description: + # mlflow.note.content is the description field + experiment_tags["mlflow.note.content"] = experiment_description + + # TODO link HamiltonTracker project and MLFlowTracker experiment + experiment = self.client.get_experiment_by_name(experiment_name) + if experiment: + experiment_id = experiment.experiment_id + # update tags and description of an existing experiment + if experiment_tags: + for k, v in experiment_tags.items(): + self.client.set_experiment_tag(experiment_id, key=k, value=v) + # create an experiment + else: + experiment_id = self.client.create_experiment( + name=experiment_name, + artifact_location=artifact_location, + tags=experiment_tags, + ) + self.experiment_id = experiment_id + + # run setup + # TODO link HamiltonTracker and MLFlowTracker run ids + self.mlflow_run_id = run_id + self.run_name = run_name + self.run_tags = run_tags if run_tags else {} + if run_description: + # mlflow.note.content is the description field + self.run_tags["mlflow.note.content"] = run_description + + self.log_system_metrics = log_system_metrics + + def run_after_graph_construction(self, *, config: dict[str, Any], **kwargs): + """Store the Driver config before creating the graph""" + self.config = config + + def run_before_graph_execution( + self, + *, + run_id: str, + final_vars: List[str], + inputs: Dict[str, Any], + graph: graph_types.HamiltonGraph, + **kwargs, + ): + """Create and start MLFlow run. Log graph version, run_id, inputs, overrides""" + # add Hamilton metadata to run tags + run_tags = self.run_tags + run_tags["hamilton_run_id"] = run_id # the Hamilton run_id + run_tags["code_version"] = graph.version + + # create Hamilton run + self.run = self.client.create_run( + experiment_id=self.experiment_id, + tags=run_tags, + run_name=self.run_name, + ) + self.run_id = self.run.info.run_id + # start run to set `active_run` and allow user-defined callbacks and materializers + # to log to the same run as the HamiltonTracker + mlflow.start_run( + run_id=self.run_id, + experiment_id=self.experiment_id, + tags=run_tags, + log_system_metrics=self.log_system_metrics, + ) + + # log config to artifacts + self.client.log_dict(self.run_id, self.config, "config.json") + + # log HamiltonGraph to reproduce the run + self.graph = graph + graph_as_json = {n.name: n.as_dict() for n in graph.nodes} + self.client.log_dict(self.run_id, graph_as_json, "hamilton_graph.json") + + # log config and inputs as `param` which creates columns in the UI to filter runs + # `log_param()` accepts `value: Any` and will stringify complex objects + for value_sets in [self.config, inputs]: + for node_name, value in value_sets.items(): + self.client.log_param(self.run_id, key=node_name, value=value) + + self.final_vars = final_vars + + # TODO log DataLoaders as MLFlow datasets + def run_after_node_execution( + self, + *, + node_name: str, + node_return_type: Type, + node_tags: dict, + node_kwargs: dict, + result: Any, + **kwargs, + ): + """Log materializers and final vars as artifacts""" + # log DataSavers as artifacts + if node_tags.get("hamilton.data_saver") is True: + # don't log mlflow materializers as artifact since they already create models + # instead, use the Materializer metadata to add metadata to registered models + if node_tags["hamilton.data_saver.sink"] == "mlflow": + # skip if not registered model + if "registered_model" not in result.keys(): + return + + # get the registered model name (param of MLFlowModelSaver) + model_name = result["registered_model"]["name"] + version = result["registered_model"]["version"] + materializer_node = self.graph[node_name] + # get the "materialized node" defining the model + materialized_node = self.graph[materializer_node.required_dependencies.pop()] + # add the materialized node docstring as description + # registered models have multiple versions + self.client.update_registered_model(model_name, materialized_node.documentation) + self.client.update_model_version( + model_name, version, materialized_node.documentation + ) + + # add the materialized node @tag values as tags + for k, v in materialized_node.tags.items(): + # skip internal Hamilton tags + if "hamilton." in k: + continue + self.client.set_registered_model_tag(model_name, key=k, value=v) + self.client.set_model_version_tag(model_name, version, key=k, value=v) + # TODO automatically collect model input signature; maybe simpler from user code + + # special case for matplotlib and plotly + # log materialized figure. Allows great degree of control over rendering format + # and also save interactive plotly visualization as HTML + elif node_tags["hamilton.data_saver.sink"] in ["plt", "plotly"]: + materializer_node = self.graph[node_name] + materialized_node = self.graph[materializer_node.required_dependencies.pop()] + figure = node_kwargs[materialized_node.name] + + path = get_path_from_metadata(result) + if path: + self.client.log_figure(self.run_id, figure, path) + else: + logger.warning( + f"Materialization result from node={node_name} has no recordable path: {result}. Materializer must have either " + f"'path' or 'file_metadata' keys." + ) + + else: + # log the materializer path as an artifact + path = get_path_from_metadata(result) + if path: + self.client.log_artifact(self.run_id, path, node_name) + else: + logger.warning( + f"Materialization result from node={node_name} has no recordable path: {result}. Materializer must have either " + f"'path' or 'file_metadata' keys." + ) + return + + # log final_vars as artifacts + if node_name not in self.final_vars: + return + + # log float and int as metrics + if node_return_type in [float, int]: + self.client.log_metric(self.run_id, key=node_name, value=float(result)) + + # log str as text in .txt format + elif isinstance(node_return_type, str): + file_path = f"{node_name}.txt" + with open(file_path, "w") as f: + f.write(result) + self.client.log_text(self.run_id, result, file_path) + + # log_dict (JSON) dictionary types; pickle if not json-serializable + elif isinstance(node_return_type, dict): + try: + file_path = f"{node_name}.json" + self.client.log_dict(self.run_id, result, file_path) + # not json-serializable + except TypeError: + file_path = f"{node_name}.pickle" + with open(file_path, "wb") as f: + pickle.dump(result, file=f) + self.client.log_dict(self.run_id, result, file_path) + + # this puts less burden on users by not having to define materializers + # for viz, but less control over rendering format + elif node_return_type in FIGURE_TYPES: + file_path = f"{node_name}.png" + self.client.log_figure(self.run_id, result, file_path) + + # default to log_artifact in .pickle format + else: + file_path = f"{node_name}.pickle" + with open(file_path, "wb") as f: + pickle.dump(result, f) + self.client.log_dict(self.run_id, result, file_path) + + def run_after_graph_execution(self, success: bool, *args, **kwargs): + """End the MLFlow run""" + # `status` is an enum value of mlflow.entities.RunStatus + if success: + self.client.set_terminated(self.run_id, status="FINISHED") + else: + self.client.set_terminated(self.run_id, status="FAILED") + mlflow.end_run() + + def run_before_node_execution(self, *args, **kwargs): + """Placeholder required to subclass NodeExecutionHook""" diff --git a/hamilton/plugins/mlflow_extensions.py b/hamilton/plugins/mlflow_extensions.py index 5601b26f3..c09789ebd 100644 --- a/hamilton/plugins/mlflow_extensions.py +++ b/hamilton/plugins/mlflow_extensions.py @@ -1,7 +1,6 @@ import dataclasses import pathlib -import shutil -from typing import Any, Callable, Collection, Dict, Literal, Optional, Tuple, Type, Union +from typing import Any, Collection, Dict, Literal, Optional, Tuple, Type, Union try: import mlflow @@ -14,34 +13,25 @@ @dataclasses.dataclass class MLFlowModelSaver(DataSaver): - """ - :param path: Specify a filesystem path or model URI for MLFlow runs or registry - :param mode: `save` will store to local filesystem; `log` will add to MLFlow registry - :param flavor: sklearn, xgboost, etc. - :param run_id: Explicit run id used for `mode=log`. Otherwise, will use active run or create one. - :param kwargs: additional arguments to pass to `.save_model()` and `.log_model()`. - They can be flavor-specific. + """Save model to the MLFlow tracking server using `.log_model()` + + :param path: Run relative path to store model. Will constitute the model URI. + :param register_as: If not None, register the model under the specified name. + :param flavor: Library format to save the model (sklearn, xgboost, etc.). Automatically inferred if None. + :param run_id: Log model to a specific run. Leave to `None` if using the `MLFlowTracker` + :param kwargs: Arguments for `.log_model()`. Can be flavor-specific. """ path: Union[str, pathlib.Path] = "model" - mode: Literal["filesystem", "runs"] = "filesystem" + register_as: Optional[str] = None + alias: Optional[str] = None flavor: Optional[str] = None run_id: Optional[str] = None - overwrite: bool = False - register: bool = False - model_name: Optional[str] = None - kwargs: Optional[Dict[str, Any]] = None - # kwargs: Dict[str, Any] = dataclasses.field(default_factory=dict) + kwargs: Dict[str, Any] = None - # A lot of dancing around because dataclass doesn't accept kwargs - # and hamilton.function_modifiers.adapters throws `InvalidDecoratorException` for dataclasses.field() defaults def __post_init__(self): self.kwargs = self.kwargs if self.kwargs else {} - # ensures that model_name is not None in case register=True - if self.model_name is None: - self.model_name = pathlib.Path(self.path).name - @classmethod def name(cls) -> str: return "mlflow" @@ -58,43 +48,38 @@ def save_data(self, data) -> Dict[str, Any]: # for example, extract `sklearn` from `sklearn.linear_model._base` flavor, _, _ = data.__module__.partition(".") - # retrieve the `mlflow.FLAVOR` submodule to use `.save_model()` and `.log_model()` + # retrieve the `mlflow.FLAVOR` submodule to `.log_model()` try: flavor_module = getattr(mlflow, flavor) except ImportError: raise ImportError(f"Flavor {flavor} is unsupported by MLFlow") - if self.mode == "filesystem": - # have to manually delete directory to avoid MLFlow exception - if self.overwrite is True: - shutil.rmtree(self.path) - - # .save_model() doesn't return anything - flavor_module.save_model(data, self.path, **self.kwargs) - model_info = mlflow.models.get_model_info(self.path) - - elif self.mode == "runs": - # handle `run_id` and active run conflicts - if mlflow.active_run() and self.run_id: - if mlflow.active_run().info.run_id != self.run_id: - raise RuntimeError( - "The MLFlowModelSaver `run_id` doesn't match the active `run_id`\n", - "Leave the `run_id` to None to save to the active MLFlow run.", - ) - - # save to active run - if mlflow.active_run(): + # handle `run_id` and active run conflicts + if mlflow.active_run() and self.run_id: + if mlflow.active_run().info.run_id != self.run_id: + raise RuntimeError( + "The MLFlowModelSaver `run_id` doesn't match the active `run_id`\n", + "Set `run_id=None` to save to the active MLFlow run.", + ) + + # save to active run + if mlflow.active_run(): + model_info = flavor_module.log_model(data, self.path, **self.kwargs) + # create a run with `run_id` and save to it + else: + with mlflow.start_run(run_id=self.run_id): model_info = flavor_module.log_model(data, self.path, **self.kwargs) - # create a run with `run_id` and save to it - else: - with mlflow.start_run(run_id=self.run_id): - model_info = flavor_module.log_model(data, self.path, **self.kwargs) + # create metadata from ModelInfo object metadata = {k.strip("_"): v for k, v in model_info.__dict__.items()} - if self.register: + + if self.register_as: model_version = mlflow.register_model( - model_uri=metadata["model_uri"], name=self.model_name + model_uri=metadata["model_uri"], name=self.register_as ) + # update metadata with the registered ModelVersion + # there's a contract between this key and the MLFlowTracker's + # post_node_execute() reads this metadata metadata["registered_model"] = { k.strip("_"): v for k, v in model_version.__dict__.items() } @@ -102,35 +87,61 @@ def save_data(self, data) -> Dict[str, Any]: return metadata -# TODO handle loading from file, run, or registry - - @dataclasses.dataclass class MLFlowModelLoader(DataLoader): - flavor: str - path: Union[str, pathlib.Path] = "model" + """Load model from the MLFlow tracking server or model registry using .load_model() + You can pass a model URI or the necessary metadata to retrieve the model + + :param model_uri: Model location starting as `runs:/` for tracking or `models:/` for registry + :param mode: `tracking` or registry`. tracking needs `run_id` and `path`. registry needs `model_name` and `version` or `version_alias`. + :param run_id: Run id of the model on the tracking server + :param path: Run relative path where the model is stored + :param model_name: Name of the registered model (equivalent to `register_as` in model saver) + :param version: Version of the registered model. Can pass as string `v1` or integer `1` + :param version_alias: Version alias of the registered model. Specify either this or `version` + :param flavor: Library format to load the model (sklearn, xgboost, etc.). Automatically inferred if None. + :param kwargs: Arguments for `.load_model()`. Can be flavor-specific. + """ + model_uri: Optional[str] = None - mode: Literal["filesystem", "runs", "registry"] = "filesystem" + mode: Literal["tracking", "registry"] = "tracking" run_id: Optional[str] = None + path: Union[str, pathlib.Path] = "model" model_name: Optional[str] = None - version: Union[str, int] = "latest" - kwargs: Optional[Dict[str, Any]] = None - # kwargs: Dict[str, Any] = dataclasses.field(default_factory=dict) + version: Optional[Union[str, int]] = None + version_alias: Optional[str] = None + flavor: Optional[str] = None + kwargs: Dict[str, Any] = None - # A lot of dancing around because dataclass doesn't accept kwargs - # and hamilton.function_modifiers.adapters throws `InvalidDecoratorException` for dataclasses.field() defaults + # __post_init__ is required to set kwargs as empty dict because + # can't set: kwargs: Dict[str, Any] = dataclasses.field(default_factory=dict) + # otherwise raises `InvalidDecoratorException` because materializer factory check + # for all params being set and `kwargs` would be unset until instantiation. def __post_init__(self): self.kwargs = self.kwargs if self.kwargs else {} if self.model_uri: return - if self.mode == "filesystem": - self.model_uri = pathlib.Path(self.path).as_uri() - elif self.mode == "runs": + if self.mode == "tracking": + if (not self.run_id) or (not self.path): + raise ValueError("Using `mode='tracking'` requires passing `run_id` and `path`") + self.model_uri = f"runs:/{self.run_id}/{self.path}" + elif self.mode == "registry": - self.model_uri = f"models:/{self.model_name}/{self.version}" + if not self.model_name: + raise ValueError("Using `mode='registry` requires passing `model_name`") + + if bool(self.version) and bool(self.version_alias): + raise ValueError( + "If using `mode='registry'` requires passing `version` OR `version_alias" + ) + + if self.version: + self.model_uri = f"models:/{self.model_name}/{self.version}" + elif self.version: + self.model_uri = f"models:/{self.model_name}@{self.version_alias}" @classmethod def name(cls) -> str: @@ -138,17 +149,28 @@ def name(cls) -> str: @classmethod def applicable_types(cls) -> Collection[Type]: - return [Callable] + return [Any] def load_data(self, type_: Type) -> Tuple[Any, Dict[str, Any]]: + model_info = mlflow.models.model.get_model_info(self.model_uri) + metadata = {k.strip("_"): v for k, v in model_info.__dict__.items()} + + flavor = self.flavor + # if flavor not explicitly passed, retrieve flavor from the ModelInfo + if flavor is None: + # prioritize the library specific flavor. Default to `pyfunc` if none available. + try: + flavor = next(f for f in metadata["flavors"].keys() if f != "python_function") + except StopIteration: + flavor = "pyfunc" + + # retrieve the `mlflow.FLAVOR` submodule to `.log_model()` try: - flavor_module = getattr(mlflow, self.flavor) + flavor_module = getattr(mlflow, flavor) except ImportError: - raise ImportError(f"Flavor {self.flavor} is unsupported by MLFlow") + raise ImportError(f"Flavor {flavor} is unsupported by MLFlow") model = flavor_module.load_model(model_uri=self.model_uri) - model_info = mlflow.models.model.get_model_info(self.model_uri) - metadata = {k.strip("_"): v for k, v in model_info.__dict__.items()} return model, metadata diff --git a/tests/plugins/test_mlflow_extension.py b/tests/plugins/test_mlflow_extension.py index e784eba22..860e108ab 100644 --- a/tests/plugins/test_mlflow_extension.py +++ b/tests/plugins/test_mlflow_extension.py @@ -23,25 +23,9 @@ def coefficients_are_equal(model1, model2) -> bool: ) -def test_mlflow_save_model(fitted_sklearn_model: BaseEstimator, tmp_path: Path): - model_path = tmp_path / "sklearn_model" - saver = MLFlowModelSaver(path=model_path, mode="save", flavor="sklearn") - expected_files = ["model.pkl", "conda.yaml", "MLmodel", "requirements.txt", "python_env.yaml"] - - # using MLFlow saver - saver.save_data(fitted_sklearn_model) - created_files = [str(p.name) for p in model_path.iterdir()] - # loading the saved model - loaded_model = mlflow.sklearn.load_model(model_path) - - assert model_path.exists() - assert set(created_files) == set(expected_files) - assert coefficients_are_equal(fitted_sklearn_model, loaded_model) - - def test_mlflow_log_model_to_active_run(fitted_sklearn_model: BaseEstimator, tmp_path: Path): model_path = tmp_path / "sklearn_model" - saver = MLFlowModelSaver(mode="log", flavor="sklearn") + saver = MLFlowModelSaver(flavor="sklearn") mlflow.set_tracking_uri(model_path.as_uri()) with mlflow.start_run(): @@ -50,9 +34,7 @@ def test_mlflow_log_model_to_active_run(fitted_sklearn_model: BaseEstimator, tmp # reload model loaded_model = mlflow.sklearn.load_model(metadata["model_uri"]) - assert np.allclose(fitted_sklearn_model.coef_, loaded_model.coef_) and np.allclose( - fitted_sklearn_model.intercept_, loaded_model.intercept_ - ) + assert coefficients_are_equal(fitted_sklearn_model, loaded_model) def test_mlflow_log_model_to_specific_run(fitted_sklearn_model: BaseEstimator, tmp_path: Path): @@ -62,16 +44,14 @@ def test_mlflow_log_model_to_specific_run(fitted_sklearn_model: BaseEstimator, t mlflow.start_run() run_id = mlflow.active_run().info.run_id mlflow.end_run() - saver = MLFlowModelSaver(mode="log", flavor="sklearn", run_id=run_id) + saver = MLFlowModelSaver(flavor="sklearn", run_id=run_id) # save model metadata = saver.save_data(fitted_sklearn_model) # reload model loaded_model = mlflow.sklearn.load_model(metadata["model_uri"]) - assert np.allclose(fitted_sklearn_model.coef_, loaded_model.coef_) and np.allclose( - fitted_sklearn_model.intercept_, loaded_model.intercept_ - ) + assert coefficients_are_equal(fitted_sklearn_model, loaded_model) def test_mlflow_log_model_active_and_specific_run_ids_are_equal( @@ -82,15 +62,13 @@ def test_mlflow_log_model_active_and_specific_run_ids_are_equal( mlflow.set_tracking_uri(model_path.as_uri()) with mlflow.start_run(): run_id = mlflow.active_run().info.run_id - saver = MLFlowModelSaver(mode="log", flavor="sklearn", run_id=run_id) + saver = MLFlowModelSaver(flavor="sklearn", run_id=run_id) # save model metadata = saver.save_data(fitted_sklearn_model) # reload model loaded_model = mlflow.sklearn.load_model(metadata["model_uri"]) - assert np.allclose(fitted_sklearn_model.coef_, loaded_model.coef_) and np.allclose( - fitted_sklearn_model.intercept_, loaded_model.intercept_ - ) + assert coefficients_are_equal(fitted_sklearn_model, loaded_model) def test_mlflow_log_model_active_and_specific_run_ids_are_unequal( @@ -101,7 +79,7 @@ def test_mlflow_log_model_active_and_specific_run_ids_are_unequal( mlflow.start_run() run_id = mlflow.active_run().info.run_id mlflow.end_run() - saver = MLFlowModelSaver(mode="log", flavor="sklearn", run_id=run_id) + saver = MLFlowModelSaver(flavor="sklearn", run_id=run_id) with mlflow.start_run(): # save model @@ -109,16 +87,6 @@ def test_mlflow_log_model_active_and_specific_run_ids_are_unequal( saver.save_data(fitted_sklearn_model) -def test_mlflow_load_local_model(fitted_sklearn_model: BaseEstimator, tmp_path: Path): - model_path = tmp_path / "sklearn_model" - mlflow.sklearn.save_model(fitted_sklearn_model, model_path) - loader = MLFlowModelLoader(path=model_path, flavor="sklearn") - - loaded_model, metadata = loader.load_data(LinearRegression) - - assert coefficients_are_equal(fitted_sklearn_model, loaded_model) - - def test_mlflow_load_runs_model(fitted_sklearn_model: BaseEstimator, tmp_path: Path): mlflow_path = tmp_path / "mlflow_path" artifact_path = "model" @@ -129,12 +97,12 @@ def test_mlflow_load_runs_model(fitted_sklearn_model: BaseEstimator, tmp_path: P # specify run via model_uri loader = MLFlowModelLoader(model_uri=f"runs:/{run_id}/{artifact_path}", flavor="sklearn") - loaded_model, metadata = loader.load_data(LinearRegression) + loaded_model, _ = loader.load_data(LinearRegression) assert coefficients_are_equal(fitted_sklearn_model, loaded_model) # specify run via arguments - loader = MLFlowModelLoader(path=artifact_path, run_id=run_id, mode="runs", flavor="sklearn") - loaded_model, metadata = loader.load_data(LinearRegression) + loader = MLFlowModelLoader(mode="tracking", path=artifact_path, run_id=run_id, flavor="sklearn") + loaded_model, _ = loader.load_data(LinearRegression) assert coefficients_are_equal(fitted_sklearn_model, loaded_model) @@ -154,33 +122,30 @@ def test_mlflow_load_registry_model(fitted_sklearn_model: BaseEstimator, tmp_pat # specify via model_uri loader = MLFlowModelLoader(model_uri=f"models:/{model_name}/{version}", flavor="sklearn") - loaded_model, metadata = loader.load_data(LinearRegression) + loaded_model, _ = loader.load_data(LinearRegression) assert coefficients_are_equal(fitted_sklearn_model, loaded_model) # specify via arguments loader = MLFlowModelLoader( mode="registry", model_name=model_name, version=version, flavor="sklearn" ) - loaded_model, metadata = loader.load_data(LinearRegression) + loaded_model, _ = loader.load_data(LinearRegression) assert coefficients_are_equal(fitted_sklearn_model, loaded_model) def test_mlflow_infer_flavor(fitted_sklearn_model: BaseEstimator, tmp_path: Path): - model_path = tmp_path / "sklearn_model" - saver = MLFlowModelSaver(path=model_path) + saver = MLFlowModelSaver(path="model") metadata = saver.save_data(fitted_sklearn_model) - assert metadata["flavor"] == "sklearn" + assert "sklearn" in metadata["flavors"].keys() def test_mlflow_handle_saver_kwargs(): path = "tmp/path" - mode = "save" flavor = "sklearn" - saver = MLFlowModelSaver(path=path, mode=mode, flavor=flavor, unknown_kwarg=True) + saver = MLFlowModelSaver(path=path, flavor=flavor, kwargs=dict(unknown_kwarg=True)) assert saver.path == path - assert saver.mode == mode assert saver.flavor == flavor assert saver.kwargs.get("unknown_kwarg") is True From be20b05310f3aed5d26cb3831b509f716740e15c Mon Sep 17 00:00:00 2001 From: zilto Date: Mon, 10 Jun 2024 18:40:45 -0400 Subject: [PATCH 04/12] removed alias from Saver --- hamilton/plugins/mlflow_extensions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/hamilton/plugins/mlflow_extensions.py b/hamilton/plugins/mlflow_extensions.py index c09789ebd..73f966751 100644 --- a/hamilton/plugins/mlflow_extensions.py +++ b/hamilton/plugins/mlflow_extensions.py @@ -24,7 +24,6 @@ class MLFlowModelSaver(DataSaver): path: Union[str, pathlib.Path] = "model" register_as: Optional[str] = None - alias: Optional[str] = None flavor: Optional[str] = None run_id: Optional[str] = None kwargs: Dict[str, Any] = None From 70e620bb7f31e18be0cde9fc1b26aaf872ab65e6 Mon Sep 17 00:00:00 2001 From: zilto Date: Mon, 10 Jun 2024 19:42:43 -0400 Subject: [PATCH 05/12] added README & tutorial; updated test requirements --- examples/mlflow/README.md | 34 + examples/mlflow/tutorial.ipynb | 1739 +++++++++++++++++++------ hamilton/plugins/h_mlflow.py | 6 + hamilton/plugins/mlflow_extensions.py | 2 +- requirements-test.txt | 1 + 5 files changed, 1406 insertions(+), 376 deletions(-) diff --git a/examples/mlflow/README.md b/examples/mlflow/README.md index 34efe08bb..ba5d49592 100644 --- a/examples/mlflow/README.md +++ b/examples/mlflow/README.md @@ -1 +1,35 @@ # MLFLow plugin for Hamilton + +[MLFlow](https://mlflow.org/) is an open-source Python framework for experiment tracking. It allows data science teams to store results, artifacts (machine learning models, figures, tables), and metadata in a principled way when executing data pipelines. + +The MLFlow plugin for Hamilton includes two sets of features: +- Save and load machine learning models with the `MLFlowModelSaver` and `MLFlowModelLoader` materializers +- Automatically track data pipeline results in MLFlow with the `MLFlowTracker`. + +This pairs nicely with the `HamiltonTracker` and the [Hamilton UI](https://hamilton.dagworks.io/en/latest/hamilton-ui/ui/) which gives you execution observability. + +We're looking forward to better link Hamilton "projects" with MLFlow "experiments" and runs from both projects. + +## Instructions +1. Create a virtual environment and activate it + ```console + python -m venv venv && . venv/bin/active + ``` + +2. Install requirements for the Hamilton code + ```console + pip install -r requirements.txt + ``` + +3. Explore the notebook `tutorial.ipynb` + + +4. Launch the MLFlow user interface to explore results + ```console + mlflow ui + ``` + +## Going further +- Learn the basics of Hamilton via the `Concepts/` [documentation section](https://hamilton.dagworks.io/en/latest/concepts/node/) +- Visit [tryhamilton.dev](tryhamilton.dev) for an interactive tutorial in your browser +- Visit the [DAGWorks blog](https://blog.dagworks.io/) for more detailed guides diff --git a/examples/mlflow/tutorial.ipynb b/examples/mlflow/tutorial.ipynb index 210a62395..e0b7ac547 100644 --- a/examples/mlflow/tutorial.ipynb +++ b/examples/mlflow/tutorial.ipynb @@ -1,5 +1,25 @@ { "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# MLFlow plugin tutorial\n", + "This notebook shows to use the MLFlow plugin for Hamilton. The first three sections present minimal examples to introduce the core functionalities:\n", + "1. Training and saving a model with `MLFlowModelSaver`\n", + "2. Loading a model for inference with `MLFlowModelLoader`\n", + "3. Automatically tracking execution results with `MLFlowTracker`\n", + "\n", + "The following sections give details about individual features. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Load the [notebook extension](https://github.com/DAGWorks-Inc/hamilton/tree/main/examples/jupyter_notebook_magic) for Hamilton. It allows us to define a dataflow in a code cell." + ] + }, { "cell_type": "code", "execution_count": 1, @@ -13,14 +33,17 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Model Training Dataflow" + "## 1. Training and saving a model" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### 1. Define" + "### 1.1 Define\n", + "We define a simple dataflow that loads the titanic dataset and trains a logistic regression to predict survival. The function parameters specify the dependencies between nodes of the dataflow.\n", + "\n", + "The first line of the cell `%%cell_to_module model_training --display` is related to the notebook extension and means this cell will define a self-contained Python module named `model_training`" ] }, { @@ -37,262 +60,1266 @@ "\n", "\n", - "\n", - "\n", + "\n", + "\n", + "%3\n", + "\n", + "\n", + "cluster__legend\n", + "\n", + "Legend\n", + "\n", + "\n", + "\n", + "load_data\n", + "\n", + "load_data\n", + "dict\n", + "\n", + "\n", + "\n", + "y\n", + "\n", + "y\n", + "Series\n", + "\n", + "\n", + "\n", + "load_data->y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X\n", + "\n", + "X\n", + "DataFrame\n", + "\n", + "\n", + "\n", + "load_data->X\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "trained_model\n", + "\n", + "trained_model\n", + "LogisticRegression\n", + "\n", + "\n", + "\n", + "y->trained_model\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X->trained_model\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "function\n", + "\n", + "function\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%cell_to_module model_training --display\n", + "import pandas as pd\n", + "from sklearn.base import BaseEstimator\n", + "from sklearn.datasets import fetch_openml\n", + "from sklearn.linear_model import LogisticRegression\n", + "from hamilton.function_modifiers import extract_fields\n", + "\n", + "# split the returned dictionary into 2 nodes: `X` and `y`\n", + "@extract_fields(dict(X=pd.DataFrame, y=pd.Series))\n", + "def load_data() -> dict:\n", + " \"\"\"Load the titanic dataset and split it in X and y. \n", + " Only keep the columns `fare` and `age` and fill null values.\n", + " \"\"\"\n", + " X, y = fetch_openml(\"titanic\", version=1, as_frame=True, return_X_y=True)\n", + " X = X[[\"fare\", \"age\"]].fillna(0)\n", + " return dict(X=X, y=y)\n", + "\n", + "def trained_model(X: pd.DataFrame, y: pd.Series) -> LogisticRegression:\n", + " \"\"\"Fit a binary classifier on the data\"\"\"\n", + " model = LogisticRegression()\n", + " model.fit(X, y)\n", + " return model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1.2 Assemble\n", + "To execute code, we build the `Driver` with the module `model_training` defined in the previous cell. \n", + "\n", + "The statement `to.mlflow()` creates a `MLFlowModelSaver` that registers the model returned by `trained_model()` as `my_predictor` in the MLFlow model registry. We add this to the `Driver` using\n", + "`.with_materializers()`. A new node will be displayed in the visualization" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", "%3\n", - "\n", + "\n", "\n", "cluster__legend\n", - "\n", - "Legend\n", + "\n", + "Legend\n", "\n", - "\n", + "\n", "\n", - "algo\n", - "\n", - "\n", - "\n", - "algo\n", - "logistic_regression\n", + "X\n", + "\n", + "X\n", + "DataFrame\n", + "\n", + "\n", + "\n", + "trained_model\n", + "\n", + "trained_model\n", + "LogisticRegression\n", + "\n", + "\n", + "\n", + "X->trained_model\n", + "\n", + "\n", "\n", "\n", "\n", "load_data\n", - "\n", - "load_data\n", - "dict\n", + "\n", + "load_data\n", + "dict\n", + "\n", + "\n", + "\n", + "load_data->X\n", + "\n", + "\n", "\n", "\n", "\n", "y\n", - "\n", - "y\n", - "Series\n", + "\n", + "y\n", + "Series\n", "\n", "\n", - "\n", + "\n", + "load_data->y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "trained_model__mlflow\n", + "\n", + "\n", + "trained_model__mlflow\n", + "MLFlowModelSaver\n", + "\n", + "\n", + "\n", + "trained_model->trained_model__mlflow\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "y->trained_model\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "function\n", + "\n", + "function\n", + "\n", + "\n", + "\n", + "materializer\n", + "\n", + "\n", + "materializer\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from hamilton import driver\n", + "from hamilton.io.materialization import to\n", + "\n", + "model_saver = to.mlflow(\n", + " id=\"trained_model__mlflow\", # name given to the saver\n", + " dependencies=[\"trained_model\"], # node returning the model\n", + " register_as=\"my_predictor\", # name of the model in the MLFlow registry\n", + ")\n", + "\n", + "dr = (\n", + " driver.Builder()\n", + " .with_modules(model_training)\n", + " .with_materializers(model_saver)\n", + " .build()\n", + ")\n", + "dr" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[0;31mInit signature:\u001b[0m\n", + "\u001b[0mMLFlowModelSaver\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mpath\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mUnion\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mpathlib\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mPath\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m'model'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mregister_as\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mflavor\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mrun_id\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mkwargs\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mDict\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mAny\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m->\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mDocstring:\u001b[0m \n", + "Save model to the MLFlow tracking server using `.log_model()`\n", + "\n", + ":param path: Run relative path to store model. Will constitute the model URI.\n", + ":param register_as: If not None, register the model under the specified name.\n", + ":param flavor: Library format to save the model (sklearn, xgboost, etc.). Automatically inferred if None.\n", + ":param run_id: Log model to a specific run. Leave to `None` if using the `MLFlowTracker`\n", + ":param kwargs: Arguments for `.log_model()`. Can be flavor-specific.\n", + "\u001b[0;31mFile:\u001b[0m ~/projects/dagworks/hamilton/hamilton/plugins/mlflow_extensions.py\n", + "\u001b[0;31mType:\u001b[0m ABCMeta\n", + "\u001b[0;31mSubclasses:\u001b[0m " + ] + } + ], + "source": [ + "# see the full API\n", + "from hamilton.plugins.mlflow_extensions import MLFlowModelSaver\n", + "MLFlowModelSaver?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1.3 Execute\n", + "We execute our dataflow by calling `Driver.execute()` and requesting node names. Requesting `trained_model` will train the model and return it. Requesting `trained_model__mlflow` will train the model, save it, and return metadata. We then visualize the execution path " + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Registered model 'my_predictor' already exists. Creating a new version of this model...\n", + "Created version '7' of model 'my_predictor'.\n" + ] + }, + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "%3\n", + "\n", + "\n", + "cluster__legend\n", + "\n", + "Legend\n", + "\n", + "\n", + "\n", + "load_data\n", + "\n", + "load_data\n", + "dict\n", + "\n", + "\n", + "\n", + "y\n", + "\n", + "y\n", + "Series\n", + "\n", + "\n", + "\n", "load_data->y\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "X\n", - "\n", - "X\n", - "DataFrame\n", + "\n", + "X\n", + "DataFrame\n", "\n", "\n", - "\n", + "\n", "load_data->X\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "y_train\n", - "\n", - "y_train\n", - "Series\n", + "\n", + "\n", + "trained_model__mlflow\n", + "\n", + "\n", + "trained_model__mlflow\n", + "MLFlowModelSaver\n", "\n", "\n", - "\n", + "\n", + "trained_model\n", + "\n", + "trained_model\n", + "LogisticRegression\n", + "\n", + "\n", + "\n", + "trained_model->trained_model__mlflow\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "y->trained_model\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X->trained_model\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "function\n", + "\n", + "function\n", + "\n", + "\n", + "\n", + "output\n", + "\n", + "output\n", + "\n", + "\n", + "\n", + "materializer\n", + "\n", + "\n", + "materializer\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "results = dr.execute([\"trained_model\", \"trained_model__mlflow\"])\n", + "dr.visualize_execution([\"trained_model\", \"trained_model__mlflow\"])" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'artifact_path': 'model',\n", + " 'flavors': {'python_function': {'model_path': 'model.pkl',\n", + " 'predict_fn': 'predict',\n", + " 'loader_module': 'mlflow.sklearn',\n", + " 'python_version': '3.11.1',\n", + " 'env': {'conda': 'conda.yaml', 'virtualenv': 'python_env.yaml'}},\n", + " 'sklearn': {'pickled_model': 'model.pkl',\n", + " 'sklearn_version': '1.5.0',\n", + " 'serialization_format': 'cloudpickle',\n", + " 'code': None}},\n", + " 'model_uri': 'runs:/67ebe9a6acdd402785428defe00b8c03/model',\n", + " 'model_uuid': '6641c1731cf549a08655dc0836ea51b1',\n", + " 'run_id': '67ebe9a6acdd402785428defe00b8c03',\n", + " 'saved_input_example_info': None,\n", + " 'signature_dict': None,\n", + " 'signature': None,\n", + " 'utc_time_created': '2024-06-10 23:38:25.769987',\n", + " 'mlflow_version': '2.13.2',\n", + " 'metadata': None,\n", + " 'registered_model': {'name': 'my_predictor',\n", + " 'version': 7,\n", + " 'creation_time': 1718062707186,\n", + " 'last_updated_timestamp': 1718062707186,\n", + " 'description': None,\n", + " 'user_id': None,\n", + " 'current_stage': 'None',\n", + " 'source': 'file:///home/tjean/projects/dagworks/hamilton/examples/mlflow/mlruns/0/67ebe9a6acdd402785428defe00b8c03/artifacts/model',\n", + " 'run_id': '67ebe9a6acdd402785428defe00b8c03',\n", + " 'run_link': None,\n", + " 'status': 'READY',\n", + " 'status_message': None,\n", + " 'tags': {},\n", + " 'aliases': []}}" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# we can inspect the model metadata\n", + "results[\"trained_model__mlflow\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Model Inference Dataflow" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.1 Define\n", + "We define a simple dataflow that uses a trained model to make predictions on user inputs. Parameters that point to no other functions (e.g, `user_input`) are called \"inputs\" as you see on the visualization.\n", + "\n", + "We annotate `model: BaseEstimator` to allow any scikit-learn model to be passed." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "%3\n", + "\n", + "\n", + "cluster__legend\n", + "\n", + "Legend\n", + "\n", + "\n", + "\n", + "preprocessed_inputs\n", + "\n", + "preprocessed_inputs\n", + "DataFrame\n", + "\n", + "\n", + "\n", + "prediction\n", + "\n", + "prediction\n", + "int\n", + "\n", + "\n", + "\n", + "preprocessed_inputs->prediction\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "_preprocessed_inputs_inputs\n", + "\n", + "user_input\n", + "dict\n", + "\n", + "\n", + "\n", + "_preprocessed_inputs_inputs->preprocessed_inputs\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "_prediction_inputs\n", + "\n", + "model\n", + "BaseEstimator\n", + "\n", + "\n", + "\n", + "_prediction_inputs->prediction\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "input\n", + "\n", + "input\n", + "\n", + "\n", + "\n", + "function\n", + "\n", + "function\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%cell_to_module model_inference --display\n", + "import pandas as pd\n", + "from sklearn.base import BaseEstimator\n", + "\n", + "def preprocessed_inputs(user_input: dict) -> pd.DataFrame:\n", + " df = pd.DataFrame(user_input, index=[0])\n", + " return df\n", + "\n", + "def prediction(preprocessed_inputs: pd.DataFrame, model: BaseEstimator) -> int:\n", + " return model.predict(preprocessed_inputs)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.2 Assemble\n", + "Again, we create a `Driver`, but this time we use `from_.mlflow()` to create a `MLFlowModelLoader` that looks for model `my_predictor` in the MLFlow registry. We pass this object through `.with_materializers()`" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "%3\n", + "\n", + "\n", + "cluster__legend\n", + "\n", + "Legend\n", + "\n", + "\n", + "\n", + "model\n", + "\n", + "model\n", + "BaseEstimator\n", + "\n", + "\n", + "\n", + "prediction\n", + "\n", + "prediction\n", + "int\n", + "\n", + "\n", + "\n", + "model->prediction\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "preprocessed_inputs\n", + "\n", + "preprocessed_inputs\n", + "DataFrame\n", + "\n", + "\n", + "\n", + "preprocessed_inputs->prediction\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "load_data.model\n", + "\n", + "load_data.model\n", + "Tuple\n", + "\n", + "\n", + "\n", + "load_data.model->model\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "_preprocessed_inputs_inputs\n", + "\n", + "user_input\n", + "dict\n", + "\n", + "\n", + "\n", + "_preprocessed_inputs_inputs->preprocessed_inputs\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "input\n", + "\n", + "input\n", + "\n", + "\n", + "\n", + "function\n", + "\n", + "function\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from hamilton import driver\n", + "from hamilton.io.materialization import from_\n", + "\n", + "model_loader = from_.mlflow(\n", + " target=\"model\",\n", + " mode=\"registry\",\n", + " model_name=\"my_predictor\",\n", + " version=1,\n", + ")\n", + "\n", + "dr = (\n", + " driver.Builder()\n", + " .with_modules(model_inference)\n", + " .with_materializers(model_loader)\n", + " .build()\n", + ")\n", + "dr" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[0;31mInit signature:\u001b[0m\n", + "\u001b[0mMLFlowModelLoader\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mmodel_uri\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mmode\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mLiteral\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'tracking'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'registry'\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m'tracking'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mrun_id\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mpath\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mUnion\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mpathlib\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mPath\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m'model'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mmodel_name\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mversion\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mUnion\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mint\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mNoneType\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mversion_alias\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mflavor\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mkwargs\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mDict\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mAny\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m->\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mDocstring:\u001b[0m \n", + "Load model from the MLFlow tracking server or model registry using .load_model()\n", + "You can pass a model URI or the necessary metadata to retrieve the model\n", + "\n", + ":param model_uri: Model location starting as `runs:/` for tracking or `models:/` for registry\n", + ":param mode: `tracking` or registry`. tracking needs `run_id` and `path`. registry needs `model_name` and `version` or `version_alias`.\n", + ":param run_id: Run id of the model on the tracking server\n", + ":param path: Run relative path where the model is stored\n", + ":param model_name: Name of the registered model (equivalent to `register_as` in model saver)\n", + ":param version: Version of the registered model. Can pass as string `v1` or integer `1`\n", + ":param version_alias: Version alias of the registered model. Specify either this or `version`\n", + ":param flavor: Library format to load the model (sklearn, xgboost, etc.). Automatically inferred if None.\n", + ":param kwargs: Arguments for `.load_model()`. Can be flavor-specific.\n", + "\u001b[0;31mFile:\u001b[0m ~/projects/dagworks/hamilton/hamilton/plugins/mlflow_extensions.py\n", + "\u001b[0;31mType:\u001b[0m ABCMeta\n", + "\u001b[0;31mSubclasses:\u001b[0m " + ] + } + ], + "source": [ + "# see the full API\n", + "from hamilton.plugins.mlflow_extensions import MLFlowModelLoader\n", + "MLFlowModelLoader?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.3 Execute\n", + "We simulate user inputs that match the `fare` and `age` columns of the training data. Then, we request `prediction` and `load_data.model` to return the loaded model." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "%3\n", + "\n", + "\n", + "cluster__legend\n", + "\n", + "Legend\n", + "\n", + "\n", + "\n", + "model\n", + "\n", + "model\n", + "BaseEstimator\n", + "\n", + "\n", + "\n", + "prediction\n", + "\n", + "prediction\n", + "int\n", + "\n", + "\n", + "\n", + "model->prediction\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "preprocessed_inputs\n", + "\n", + "preprocessed_inputs\n", + "DataFrame\n", + "\n", + "\n", + "\n", + "preprocessed_inputs->prediction\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "load_data.model\n", + "\n", + "load_data.model\n", + "Tuple\n", + "\n", + "\n", + "\n", + "load_data.model->model\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "_preprocessed_inputs_inputs\n", + "\n", + "user_input\n", + "dict\n", + "\n", + "\n", + "\n", + "_preprocessed_inputs_inputs->preprocessed_inputs\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "input\n", + "\n", + "input\n", + "\n", + "\n", + "\n", + "function\n", + "\n", + "function\n", + "\n", + "\n", + "\n", + "output\n", + "\n", + "output\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "inputs = dict(user_input={\"fare\": 10.72, \"age\": 48})\n", + "\n", + "results = dr.execute([\"prediction\", \"load_data.model\"], inputs=inputs)\n", + "dr.visualize_execution([\"prediction\", \"load_data.model\"], inputs=inputs)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['0']\n" + ] + }, + { + "data": { + "text/plain": [ + "(LogisticRegression(),\n", + " {'artifact_path': 'model',\n", + " 'flavors': {'python_function': {'env': {'conda': 'conda.yaml',\n", + " 'virtualenv': 'python_env.yaml'},\n", + " 'loader_module': 'mlflow.sklearn',\n", + " 'model_path': 'model.pkl',\n", + " 'predict_fn': 'predict',\n", + " 'python_version': '3.11.1'},\n", + " 'sklearn': {'code': None,\n", + " 'pickled_model': 'model.pkl',\n", + " 'serialization_format': 'cloudpickle',\n", + " 'sklearn_version': '1.5.0'}},\n", + " 'model_uri': 'models:/my_predictor/1',\n", + " 'model_uuid': '5cfda7f11ed440e6823c13a99dc47471',\n", + " 'run_id': '8099b8e575d04476b47960431d17f9f5',\n", + " 'saved_input_example_info': None,\n", + " 'signature_dict': None,\n", + " 'signature': None,\n", + " 'utc_time_created': '2024-06-10 22:43:07.254057',\n", + " 'mlflow_version': '2.13.2',\n", + " 'metadata': None})" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# we can inspect the prediction\n", + "# load_data.model returns a tuple (model, model metadata)\n", + "print(results[\"prediction\"])\n", + "results[\"load_data.model\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. MLFlowTracker\n", + "So far, we saved and loaded models, but the MLFlow metadata is almost empty. By adding the `MLFlowTracker()`, we can automatically track run configurations, metrics, figures, and other artifacts." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3.1 Define\n", + "We define a slightly more complex pipeline that splits the dataset into training and test sets. Then, we compute the model performance on each set and produce a scatter plot of features and correct/incorrect predictions.\n", + "\n", + "Notice that no `mlflow` statements is needed in our dataflow definition." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "%3\n", + "\n", + "\n", + "cluster__legend\n", + "\n", + "Legend\n", + "\n", + "\n", + "\n", + "test_scatter_plot\n", + "\n", + "test_scatter_plot\n", + "Figure\n", + "\n", + "\n", + "\n", + "train_performance\n", + "\n", + "train_performance\n", + "float\n", + "\n", + "\n", + "\n", + "y_test\n", + "\n", + "y_test\n", + "Series\n", + "\n", + "\n", + "\n", + "y_test->test_scatter_plot\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "test_performance\n", + "\n", + "test_performance\n", + "float\n", + "\n", + "\n", + "\n", + "y_test->test_performance\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "y_train\n", + "\n", + "y_train\n", + "Series\n", + "\n", + "\n", + "\n", + "y_train->train_performance\n", + "\n", + "\n", + "\n", + "\n", + "\n", "trained_model\n", - "\n", - "trained_model: algo\n", - "BaseEstimator\n", + "\n", + "trained_model\n", + "BaseEstimator\n", "\n", "\n", - "\n", + "\n", "y_train->trained_model\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "X_train\n", - "\n", - "X_train\n", - "DataFrame\n", + "\n", + "\n", + "test_predictions\n", + "\n", + "test_predictions\n", + "Series\n", "\n", - "\n", - "\n", - "X_train->trained_model\n", - "\n", - "\n", + "\n", + "\n", + "test_predictions->test_scatter_plot\n", + "\n", + "\n", "\n", - "\n", + "\n", + "\n", + "test_predictions->test_performance\n", + "\n", + "\n", + "\n", + "\n", "\n", + "X\n", + "\n", + "X\n", + "DataFrame\n", + "\n", + "\n", + "\n", "split_dataset\n", - "\n", - "split_dataset\n", - "dict\n", + "\n", + "split_dataset\n", + "dict\n", "\n", - "\n", - "\n", - "y->split_dataset\n", - "\n", - "\n", + "\n", + "\n", + "X->split_dataset\n", + "\n", + "\n", "\n", - "\n", - "\n", - "X_preprocessed\n", - "\n", - "X_preprocessed\n", - "DataFrame\n", + "\n", + "\n", + "load_data\n", + "\n", + "load_data\n", + "dict\n", "\n", - "\n", - "\n", - "X_preprocessed->split_dataset\n", - "\n", - "\n", + "\n", + "\n", + "load_data->X\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "y\n", + "\n", + "y\n", + "Series\n", + "\n", + "\n", + "\n", + "load_data->y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "split_dataset->y_test\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "split_dataset->y_train\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "split_dataset->X_train\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", "X_test\n", - "\n", - "X_test\n", - "DataFrame\n", + "\n", + "X_test\n", + "DataFrame\n", "\n", "\n", - "\n", + "\n", "split_dataset->X_test\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "y_test\n", - "\n", - "y_test\n", - "Series\n", + "\n", + "\n", + "X_train\n", + "\n", + "X_train\n", + "DataFrame\n", "\n", - "\n", - "\n", - "split_dataset->y_test\n", - "\n", - "\n", + "\n", + "\n", + "split_dataset->X_train\n", + "\n", + "\n", "\n", - "\n", - "\n", - "test_scatter_plot\n", - "\n", - "test_scatter_plot\n", - "Figure\n", + "\n", + "\n", + "train_predictions\n", + "\n", + "train_predictions\n", + "Series\n", "\n", - "\n", - "\n", - "y_test_predictions\n", - "\n", - "y_test_predictions\n", - "Series\n", + "\n", + "\n", + "train_predictions->train_performance\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "y_test_predictions->test_scatter_plot\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "test_performance\n", - "\n", - "test_performance: algo\n", - "float\n", + "trained_model->test_predictions\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "y_test_predictions->test_performance\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "trained_model->y_test_predictions\n", - "\n", - "\n", + "trained_model->train_predictions\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "X_test->test_scatter_plot\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "X_test->y_test_predictions\n", - "\n", - "\n", + "\n", + "\n", + "X_test->test_predictions\n", + "\n", + "\n", "\n", - "\n", - "\n", - "X->X_preprocessed\n", - "\n", - "\n", + "\n", + "\n", + "X_train->train_predictions\n", + "\n", + "\n", "\n", - "\n", - "\n", - "y_test->test_scatter_plot\n", - "\n", - "\n", + "\n", + "\n", + "X_train->trained_model\n", + "\n", + "\n", "\n", - "\n", - "\n", - "y_test->test_performance\n", - "\n", - "\n", + "\n", + "\n", + "y->split_dataset\n", + "\n", + "\n", "\n", "\n", "\n", "_split_dataset_inputs\n", - "\n", - "test_size_fraction\n", - "float\n", + "\n", + "test_size_fraction\n", + "float\n", "\n", "\n", - "\n", + "\n", "_split_dataset_inputs->split_dataset\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "config\n", - "\n", - "\n", - "\n", - "config\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "input\n", - "\n", - "input\n", + "\n", + "input\n", "\n", "\n", - "\n", + "\n", "function\n", - "\n", - "function\n", + "\n", + "function\n", "\n", "\n", "\n" ], "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -300,90 +1327,70 @@ } ], "source": [ - "%%cell_to_module model_training --display --config algo=logistic_regression\n", - "from typing import Dict, Union\n", - "\n", + "%%cell_to_module model_training_2 --display\n", "import pandas as pd\n", "import matplotlib.figure\n", "import matplotlib.pyplot as plt\n", - "import numpy as np\n", "from sklearn.base import BaseEstimator\n", "from sklearn.datasets import fetch_openml\n", "from sklearn.linear_model import LogisticRegression, LinearRegression\n", "from sklearn.model_selection import train_test_split\n", - "from sklearn.metrics import balanced_accuracy_score, mean_absolute_error\n", - "from hamilton.function_modifiers import extract_fields, tag, config\n", + "from sklearn.metrics import balanced_accuracy_score\n", + "from hamilton.function_modifiers import extract_fields\n", "\n", "\n", "@extract_fields(dict(X=pd.DataFrame, y=pd.Series))\n", "def load_data() -> dict:\n", + " \"\"\"Load the titanic dataset and split it in X and y. \n", + " Only keep the columns `fare` and `age` and fill null values.\n", + " \"\"\"\n", " X, y = fetch_openml(\"titanic\", version=1, as_frame=True, return_X_y=True)\n", + " X = X[[\"fare\", \"age\"]].fillna(0)\n", " return dict(X=X, y=y)\n", "\n", "\n", - "def X_preprocessed(X: pd.DataFrame) -> pd.DataFrame:\n", - " column_selection = [\"fare\", \"age\"]\n", - " X = X[column_selection]\n", - " X = X.fillna(0)\n", - " return X \n", - "\n", - "\n", "@extract_fields(dict(\n", - " X_train=pd.DataFrame,\n", - " y_train=pd.Series,\n", - " X_test=pd.DataFrame,\n", - " y_test=pd.Series,\n", + " X_train=pd.DataFrame, y_train=pd.Series,\n", + " X_test=pd.DataFrame, y_test=pd.Series,\n", "))\n", "def split_dataset(\n", - " X_preprocessed: pd.DataFrame,\n", + " X: pd.DataFrame,\n", " y: pd.Series,\n", " test_size_fraction: float = 0.3\n", ") -> dict:\n", - " \"\"\"Load the titanic dataset and partition it in X_train, y_train, X_test, y_test\"\"\"\n", + " \"\"\"Partition the dataset into training and testing sets.\"\"\"\n", " X_train, X_test, y_train, y_test = train_test_split(\n", - " X_preprocessed, y, test_size=test_size_fraction,\n", - " )\n", - " return dict(\n", - " X_train=X_train,\n", - " y_train=y_train,\n", - " X_test=X_test,\n", - " y_test=y_test,\n", + " X, y, test_size=test_size_fraction,\n", " )\n", + " return dict(X_train=X_train, y_train=y_train, X_test=X_test, y_test=y_test)\n", "\n", - "@tag(team=\"forecast\")\n", - "@config.when(algo=\"logistic_regression\")\n", - "def trained_model__loistic(X_train: pd.DataFrame, y_train: pd.Series, hparams: dict) -> BaseEstimator:\n", - " \"\"\"Fit a binary classifier on the training data\"\"\"\n", + "def trained_model(X_train: pd.DataFrame, y_train: pd.Series) -> BaseEstimator:\n", + " \"\"\"Binary classifier fitted on the training data\"\"\"\n", " model = LogisticRegression()\n", " model.fit(X_train, y_train)\n", " return model\n", "\n", + "def train_predictions(trained_model: BaseEstimator, X_train: pd.DataFrame) -> pd.Series:\n", + " return trained_model.predict(X_train)\n", "\n", - "@tag(team=\"forecast\")\n", - "@config.when(algo=\"linear_regression\")\n", - "def trained_model__linear(X_train: pd.DataFrame, y_train: pd.Series) -> BaseEstimator:\n", - " \"\"\"Fit a binary classifier on the training data\"\"\"\n", - " model = LinearRegression()\n", - " model.fit(X_train, y_train)\n", - " return model\n", + "def train_performance(y_train: pd.Series, train_predictions: pd.Series) -> float:\n", + " \"\"\"Balanced accuracy on the training set\"\"\"\n", + " return balanced_accuracy_score(y_train, train_predictions)\n", "\n", - "def y_test_predictions(trained_model: BaseEstimator, X_test: pd.DataFrame) -> pd.Series:\n", + "def test_predictions(trained_model: BaseEstimator, X_test: pd.DataFrame) -> pd.Series:\n", " return trained_model.predict(X_test)\n", "\n", - "@config.when(algo=\"logistic_regression\")\n", - "def test_performance__logistic(y_test: pd.Series, y_test_predictions: pd.Series) -> float:\n", - " return balanced_accuracy_score(y_test, y_test_predictions)\n", - "\n", - "@config.when(algo=\"linear_regression\")\n", - "def test_performance__linear(y_test: pd.Series, y_test_predictions: pd.Series) -> float:\n", - " return mean_absolute_error(y_test, y_test_predictions)\n", + "def test_performance(y_test: pd.Series, test_predictions: pd.Series) -> float:\n", + " \"\"\"Balanced accuracy on the training set\"\"\"\n", + " return balanced_accuracy_score(y_test, test_predictions)\n", "\n", "def test_scatter_plot(\n", " X_test: pd.DataFrame,\n", " y_test: pd.Series,\n", - " y_test_predictions: pd.Series,\n", + " test_predictions: pd.Series,\n", ") -> matplotlib.figure.Figure:\n", - " correctly_predicted = y_test == y_test_predictions\n", + " \"\"\"Scatter plot of fare and age with colors for correct/incorrect predictions\"\"\"\n", + " correctly_predicted = y_test == test_predictions\n", " feature_1 = X_test.iloc[:, 0]\n", " feature_2 = X_test.iloc[:, 1]\n", "\n", @@ -396,245 +1403,227 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### 2. Assemble" + "### 3.2 Assemble\n", + "This code is just like Section #1.2, but we add a `MLFlowTracker()` to the `Driver` by passing it to `.with_materializers()`. This objects accepts many arguments to set the right tracking and registry server, specify the experiment names, and set other metadata. Generally, the defaults are sufficient if you're developing locally. " ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "from hamilton import driver\n", "from hamilton.io.materialization import to\n", + "from hamilton.plugins.h_mlflow import MLFlowTracker\n", "\n", "dr = (\n", " driver.Builder()\n", - " .with_modules(model_training)\n", - " .with_config(dict(algo=\"logistic_regression\"))\n", + " .with_modules(model_training_2)\n", + " .with_adapters(MLFlowTracker())\n", " .with_materializers(\n", " to.mlflow(\n", " id=\"trained_model__mlflow\",\n", " dependencies=[\"trained_model\"],\n", - " mode=\"runs\",\n", - " register=True,\n", - " model_name=\"my_classifier\",\n", + " register_as=\"my_new_model\",\n", " ),\n", " )\n", " .build()\n", - ")\n", - "dr" + ")" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": 14, "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[0;31mInit signature:\u001b[0m\n", + "\u001b[0mMLFlowTracker\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mtracking_uri\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mregistry_uri\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0martifact_location\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mexperiment_name\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mstr\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m'Hamilton'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mexperiment_tags\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mdict\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mexperiment_description\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mrun_id\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mrun_name\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mrun_tags\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mdict\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mrun_description\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mlog_system_metrics\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mbool\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mDocstring:\u001b[0m Driver adapter logging Hamilton execution results to an MLFlow server.\n", + "\u001b[0;31mInit docstring:\u001b[0m\n", + "Configure the MLFlow client and experiment for the lifetime of the tracker\n", + "\n", + ":param tracking_uri: Destination of the logged artifacts and metadata. It can be a filesystem, database, or server. [reference](https://mlflow.org/docs/latest/getting-started/tracking-server-overview/index.html)\n", + ":param registry_uri: Destination of the registered models. By default it's the same as the tracking destination, but they can be different. [reference](https://mlflow.org/docs/latest/getting-started/registering-first-model/index.html)\n", + ":param artifact_location: Root path on tracking server where experiment is stored\n", + ":param experiment_name: MLFlow experiment name used to group runs.\n", + ":param experiment_tags: Tags to query experiments programmatically (not displayed).\n", + ":param experiment_description: Description of the experiment displayed\n", + ":param run_id: Run id to log to an existing run (every execution logs to the same run)\n", + ":param run_name: Run name displayed and used to query runs. You can have multiple runs with the same name but different run ids.\n", + ":param run_tags: Tags to query runs and appears as columns in the UI for filtering and grouping. It automatically includes serializable inputs and Driver config.\n", + ":param run_description: Description of the run displayed\n", + ":param log_system_metrics: Log system metrics to display (requires additonal dependencies)\n", + "\u001b[0;31mFile:\u001b[0m ~/projects/dagworks/hamilton/hamilton/plugins/h_mlflow.py\n", + "\u001b[0;31mType:\u001b[0m ABCMeta\n", + "\u001b[0;31mSubclasses:\u001b[0m " + ] + } + ], "source": [ - "### 3. Execute" + "MLFlowTracker?" ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "final_vars = [\"trained_model__mlflow\", \"y_test_predictions\", \"test_performance\"]\n", - "results = dr.execute(final_vars)\n", - "dr.visualize_execution(final_vars)" + "### 3.3 Execute\n", + "Like before, we request nodes for execution. But this time, all requested nodes will be logged in MLFlow, not just model savers! Inputs and other metadata will also be automatically available (see next section for details)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Registered model 'my_new_model' already exists. Creating a new version of this model...\n", + "Created version '5' of model 'my_new_model'.\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ - "# balanced accuracy on test set\n", - "print(results[\"test_performance\"])\n", - "print()\n", - "# metadata of stored model\n", - "results[\"trained_model__mlflow\"]" + "results = dr.execute(\n", + " [\"trained_model__mlflow\", \"train_performance\", \"test_performance\", \"test_scatter_plot\"],\n", + " inputs=dict(test_size_fraction=0.3)\n", + ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Model Inference Dataflow" + "## 4. Feature list" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### 1. Define" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%cell_to_module model_inference --display\n", - "from typing import Dict, Union\n", - "\n", - "import pandas as pd\n", - "from sklearn.base import BaseEstimator\n", - "from sklearn.datasets import fetch_openml\n", - "from sklearn.linear_model import LogisticRegression\n", - "from sklearn.model_selection import train_test_split\n", - "from sklearn.metrics import balanced_accuracy_score\n", - "from hamilton.function_modifiers import extract_fields\n", - "\n", - "# import the preprocessing function from the module above\n", - "from model_training import X_preprocessed\n", - "\n", - "def preprocessed_inputs(user_input: dict) -> pd.DataFrame:\n", - " df = pd.DataFrame(user_input, index=[0])\n", - " df = X_preprocessed(df)\n", - " return df\n", - "\n", - "def prediction(preprocessed_inputs: pd.DataFrame, model: BaseEstimator) -> int:\n", - " return model.predict(preprocessed_inputs)" + "Automatically tracks `.execute(inputs=...)` and `Builder().with_config()` as MLFlow params. This creates columns that you can use to filter runs in the UI." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### 2. Assemble" + "The run tag `code_version` is automatically added by the `MLFlowTracker`. This allows you to know exactly what code was executed and group runs that use the same code, but vary in terms of inputs. " ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "from hamilton import driver\n", - "from hamilton.io.materialization import from_\n", - "\n", - "dr = (\n", - " driver.Builder()\n", - " .with_modules(model_inference)\n", - " .with_materializers(\n", - " from_.mlflow(\n", - " target=\"model\",\n", - " mode=\"registry\",\n", - " model_name=\"my_classifier\",\n", - " ),\n", - " )\n", - " .build()\n", - ")\n", - "dr" + "Store the entire `HamiltonGraph` as an artifact `hamilton_graph.json`. This contains the source code of the executed dataflow." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### 3. Execute" + "Automatically log `plotly` and `matplotlib` figures as `.png` artifacts. For more control, you can use the `to.plotly()` and `to.plt()` savers. Notably, this allows you to save interactive plotly visualizations as HTML. " ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "inputs = dict(user_input={\"fare\": 10.72, \"age\": 48})\n", - "\n", - "final_vars = [\"prediction\", \"load_data.model\"]\n", - "results = dr.execute(final_vars, inputs=inputs)\n", - "dr.visualize_execution(final_vars, inputs=inputs)" + "Use the `MLFlowTracker` to specify experiment metadata and run metadata that will help you browse the MLFlow UI and programatic search. `experiment_description` and `run_description` accept markdown strings. " ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "metadata": {}, - "outputs": [], + "outputs": [ + { + "ename": "MlflowException", + "evalue": "Invalid experiment name: Ellipsis. Expects a string.", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mMlflowException\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[16], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mMLFlowTracker\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 2\u001b[0m \u001b[43m \u001b[49m\u001b[43mexperiment_name\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 3\u001b[0m \u001b[43m \u001b[49m\u001b[43mexperiment_description\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 4\u001b[0m \u001b[43m \u001b[49m\u001b[43mrun_name\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 5\u001b[0m \u001b[43m \u001b[49m\u001b[43mrun_tags\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 6\u001b[0m \u001b[43m \u001b[49m\u001b[43mrun_description\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 7\u001b[0m \u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/projects/dagworks/hamilton/hamilton/plugins/h_mlflow.py:98\u001b[0m, in \u001b[0;36mMLFlowTracker.__init__\u001b[0;34m(self, tracking_uri, registry_uri, artifact_location, experiment_name, experiment_tags, experiment_description, run_id, run_name, run_tags, run_description, log_system_metrics)\u001b[0m\n\u001b[1;32m 95\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mclient\u001b[38;5;241m.\u001b[39mset_experiment_tag(experiment_id, key\u001b[38;5;241m=\u001b[39mk, value\u001b[38;5;241m=\u001b[39mv)\n\u001b[1;32m 96\u001b[0m \u001b[38;5;66;03m# create an experiment\u001b[39;00m\n\u001b[1;32m 97\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m---> 98\u001b[0m experiment_id \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mclient\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcreate_experiment\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 99\u001b[0m \u001b[43m \u001b[49m\u001b[43mname\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mexperiment_name\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 100\u001b[0m \u001b[43m \u001b[49m\u001b[43martifact_location\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43martifact_location\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 101\u001b[0m \u001b[43m \u001b[49m\u001b[43mtags\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mexperiment_tags\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 102\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 103\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mexperiment_id \u001b[38;5;241m=\u001b[39m experiment_id\n\u001b[1;32m 105\u001b[0m \u001b[38;5;66;03m# run setup\u001b[39;00m\n\u001b[1;32m 106\u001b[0m \u001b[38;5;66;03m# TODO link HamiltonTracker and MLFlowTracker run ids\u001b[39;00m\n", + "File \u001b[0;32m~/projects/dagworks/hamilton/venv/lib/python3.11/site-packages/mlflow/tracking/client.py:1284\u001b[0m, in \u001b[0;36mMlflowClient.create_experiment\u001b[0;34m(self, name, artifact_location, tags)\u001b[0m\n\u001b[1;32m 1232\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mcreate_experiment\u001b[39m(\n\u001b[1;32m 1233\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[1;32m 1234\u001b[0m name: \u001b[38;5;28mstr\u001b[39m,\n\u001b[1;32m 1235\u001b[0m artifact_location: Optional[\u001b[38;5;28mstr\u001b[39m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[1;32m 1236\u001b[0m tags: Optional[Dict[\u001b[38;5;28mstr\u001b[39m, Any]] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[1;32m 1237\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m \u001b[38;5;28mstr\u001b[39m:\n\u001b[1;32m 1238\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Create an experiment.\u001b[39;00m\n\u001b[1;32m 1239\u001b[0m \n\u001b[1;32m 1240\u001b[0m \u001b[38;5;124;03m Args:\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 1282\u001b[0m \n\u001b[1;32m 1283\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m-> 1284\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_tracking_client\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcreate_experiment\u001b[49m\u001b[43m(\u001b[49m\u001b[43mname\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43martifact_location\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtags\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/projects/dagworks/hamilton/venv/lib/python3.11/site-packages/mlflow/tracking/_tracking_service/client.py:498\u001b[0m, in \u001b[0;36mTrackingServiceClient.create_experiment\u001b[0;34m(self, name, artifact_location, tags)\u001b[0m\n\u001b[1;32m 484\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"Create an experiment.\u001b[39;00m\n\u001b[1;32m 485\u001b[0m \n\u001b[1;32m 486\u001b[0m \u001b[38;5;124;03mArgs:\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 495\u001b[0m \n\u001b[1;32m 496\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 497\u001b[0m _validate_experiment_artifact_location(artifact_location)\n\u001b[0;32m--> 498\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstore\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcreate_experiment\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 499\u001b[0m \u001b[43m \u001b[49m\u001b[43mname\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mname\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 500\u001b[0m \u001b[43m \u001b[49m\u001b[43martifact_location\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43martifact_location\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 501\u001b[0m \u001b[43m \u001b[49m\u001b[43mtags\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m[\u001b[49m\u001b[43mExperimentTag\u001b[49m\u001b[43m(\u001b[49m\u001b[43mkey\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mvalue\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43m(\u001b[49m\u001b[43mkey\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mvalue\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mtags\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mitems\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m]\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mif\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mtags\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01melse\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43m[\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 502\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/projects/dagworks/hamilton/venv/lib/python3.11/site-packages/mlflow/store/tracking/file_store.py:391\u001b[0m, in \u001b[0;36mFileStore.create_experiment\u001b[0;34m(self, name, artifact_location, tags)\u001b[0m\n\u001b[1;32m 389\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mcreate_experiment\u001b[39m(\u001b[38;5;28mself\u001b[39m, name, artifact_location\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, tags\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m):\n\u001b[1;32m 390\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_check_root_dir()\n\u001b[0;32m--> 391\u001b[0m \u001b[43m_validate_experiment_name\u001b[49m\u001b[43m(\u001b[49m\u001b[43mname\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 392\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_validate_experiment_does_not_exist(name)\n\u001b[1;32m 393\u001b[0m experiment_id \u001b[38;5;241m=\u001b[39m _generate_unique_integer_id()\n", + "File \u001b[0;32m~/projects/dagworks/hamilton/venv/lib/python3.11/site-packages/mlflow/utils/validation.py:366\u001b[0m, in \u001b[0;36m_validate_experiment_name\u001b[0;34m(experiment_name)\u001b[0m\n\u001b[1;32m 361\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m MlflowException(\n\u001b[1;32m 362\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mInvalid experiment name: \u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mexperiment_name\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m\"\u001b[39m, error_code\u001b[38;5;241m=\u001b[39mINVALID_PARAMETER_VALUE\n\u001b[1;32m 363\u001b[0m )\n\u001b[1;32m 365\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m is_string_type(experiment_name):\n\u001b[0;32m--> 366\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m MlflowException(\n\u001b[1;32m 367\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mInvalid experiment name: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mexperiment_name\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m. Expects a string.\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 368\u001b[0m error_code\u001b[38;5;241m=\u001b[39mINVALID_PARAMETER_VALUE,\n\u001b[1;32m 369\u001b[0m )\n", + "\u001b[0;31mMlflowException\u001b[0m: Invalid experiment name: Ellipsis. Expects a string." + ] + } + ], "source": [ - "print(results[\"prediction\"])\n", - "results[\"load_data.model\"]" + "MLFlowTracker(\n", + " experiment_name=...,\n", + " experiment_description=...,\n", + " run_name=...,\n", + " run_tags=...,\n", + " run_description=...,\n", + ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## MLFlowTracker" + "The tags specified using the Hamilton decorator `@tag` on the model-producing function are stored in the MLFlow model registry " ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "import importlib\n", - "from hamilton import driver\n", - "import hamilton.plugins.h_mlflow\n", - "from hamilton.io.materialization import to\n", - "from hamilton.plugins.h_mlflow import MLFlowTracker\n", - "importlib.reload(hamilton.plugins.h_mlflow)\n", + "import pandas as pd\n", + "from hamilton.function_modifiers import tag\n", + "from sklearn.linear_model import LogisticRegression\n", "\n", - "dr = (\n", - " driver.Builder()\n", - " .with_modules(model_training)\n", - " .with_config(dict(algo=\"logistic_regression\"))\n", - " .with_adapters(\n", - " hamilton.plugins.h_mlflow.MLFlowTracker()\n", - " )\n", - " .with_materializers(\n", - " to.mlflow(\n", - " id=\"trained_model__mlflow\",\n", - " dependencies=[\"trained_model\"],\n", - " mode=\"runs\",\n", - " register=True,\n", - " model_name=\"my_loom_video\",\n", - " ),\n", - " )\n", - " .build()\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/tjean/projects/dagworks/hamilton/venv/lib/python3.11/site-packages/_distutils_hack/__init__.py:11: UserWarning: Distutils was imported before Setuptools, but importing Setuptools also replaces the `distutils` module in `sys.modules`. This may lead to undesirable behaviors or errors. To avoid these issues, avoid using distutils directly, ensure that setuptools is installed in the traditional way (e.g. not an editable install), and/or make sure that setuptools is always imported before distutils.\n", - " warnings.warn(\n", - "/home/tjean/projects/dagworks/hamilton/venv/lib/python3.11/site-packages/_distutils_hack/__init__.py:26: UserWarning: Setuptools is replacing distutils.\n", - " warnings.warn(\"Setuptools is replacing distutils.\")\n", - "Registered model 'my_loom_video' already exists. Creating a new version of this model...\n", - "Created version '2' of model 'my_loom_video'.\n" - ] - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "inputs = dict(test_size_fraction=0.3)\n", - "results = dr.execute(\n", - " [\"trained_model__mlflow\", \"test_performance\", \"test_scatter_plot\"],\n", - " inputs=inputs\n", - ")" + "@tag(team=\"forecast\", feature_set=\"v3\")\n", + "def trained_model(X_train: pd.DataFrame, y_train: pd.Series) -> LogisticRegression:\n", + " \"\"\"Fit a binary classifier on the training data\"\"\"\n", + " model = LogisticRegression()\n", + " model.fit(X_train, y_train)\n", + " return model\n", + "\n", + "# ...\n", + "\n", + "to.mlflow(\n", + " id=\"trained_model__mlflow\",\n", + " dependencies=[\"trained_model\"],\n", + " register_as=\"new_algo\",\n", + ")," ] } ], diff --git a/hamilton/plugins/h_mlflow.py b/hamilton/plugins/h_mlflow.py index 193bc68f9..75ccee81a 100644 --- a/hamilton/plugins/h_mlflow.py +++ b/hamilton/plugins/h_mlflow.py @@ -1,5 +1,6 @@ import logging import pickle +import warnings from typing import Any, Dict, List, Optional, Type, Union import mlflow @@ -8,6 +9,11 @@ from hamilton import graph_types from hamilton.lifecycle import GraphConstructionHook, GraphExecutionHook, NodeExecutionHook +# silence odd ongoing MLFlow issue that spams warnings +# GitHub Issue https://github.com/mlflow/mlflow/issues/8605 +warnings.filterwarnings("ignore", category=UserWarning) + + FIGURE_TYPES = [] try: import matplotlib.figure diff --git a/hamilton/plugins/mlflow_extensions.py b/hamilton/plugins/mlflow_extensions.py index 73f966751..396978422 100644 --- a/hamilton/plugins/mlflow_extensions.py +++ b/hamilton/plugins/mlflow_extensions.py @@ -132,7 +132,7 @@ def __post_init__(self): if not self.model_name: raise ValueError("Using `mode='registry` requires passing `model_name`") - if bool(self.version) and bool(self.version_alias): + if not (bool(self.version) ^ bool(self.version_alias)): raise ValueError( "If using `mode='registry'` requires passing `version` OR `version_alias" ) diff --git a/requirements-test.txt b/requirements-test.txt index a819c0455..9d79f161d 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -13,6 +13,7 @@ lightgbm lxml lz4 matplotlib +mlflow networkx openpyxl # for excel data loader pandera From 1a93b5790c228d984479aa1ed96b920fa7691aeb Mon Sep 17 00:00:00 2001 From: zilto Date: Tue, 11 Jun 2024 10:29:45 -0400 Subject: [PATCH 06/12] fix if condition --- hamilton/plugins/mlflow_extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hamilton/plugins/mlflow_extensions.py b/hamilton/plugins/mlflow_extensions.py index 396978422..2d78939b5 100644 --- a/hamilton/plugins/mlflow_extensions.py +++ b/hamilton/plugins/mlflow_extensions.py @@ -139,7 +139,7 @@ def __post_init__(self): if self.version: self.model_uri = f"models:/{self.model_name}/{self.version}" - elif self.version: + elif self.version_alias: self.model_uri = f"models:/{self.model_name}@{self.version_alias}" @classmethod From 047cc4e1c5193f3a37f4db15de5de0ea7d0bea5e Mon Sep 17 00:00:00 2001 From: zilto Date: Tue, 11 Jun 2024 10:31:46 -0400 Subject: [PATCH 07/12] fixed README typo --- examples/mlflow/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/mlflow/README.md b/examples/mlflow/README.md index ba5d49592..327cf7bfb 100644 --- a/examples/mlflow/README.md +++ b/examples/mlflow/README.md @@ -8,7 +8,7 @@ The MLFlow plugin for Hamilton includes two sets of features: This pairs nicely with the `HamiltonTracker` and the [Hamilton UI](https://hamilton.dagworks.io/en/latest/hamilton-ui/ui/) which gives you execution observability. -We're looking forward to better link Hamilton "projects" with MLFlow "experiments" and runs from both projects. +We're working on better linking Hamilton "projects" with MLFlow "experiments" and runs from both projects. ## Instructions 1. Create a virtual environment and activate it From 94308360a11e3cad5b16c3f959bf1dd779817091 Mon Sep 17 00:00:00 2001 From: zilto Date: Tue, 11 Jun 2024 10:33:24 -0400 Subject: [PATCH 08/12] updated README --- examples/mlflow/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/mlflow/README.md b/examples/mlflow/README.md index 327cf7bfb..4891302b5 100644 --- a/examples/mlflow/README.md +++ b/examples/mlflow/README.md @@ -6,7 +6,7 @@ The MLFlow plugin for Hamilton includes two sets of features: - Save and load machine learning models with the `MLFlowModelSaver` and `MLFlowModelLoader` materializers - Automatically track data pipeline results in MLFlow with the `MLFlowTracker`. -This pairs nicely with the `HamiltonTracker` and the [Hamilton UI](https://hamilton.dagworks.io/en/latest/hamilton-ui/ui/) which gives you execution observability. +This pairs nicely with the `HamiltonTracker` and the [Hamilton UI](https://hamilton.dagworks.io/en/latest/hamilton-ui/ui/) which gives you a way to explore your pipeline code, attributes of the artifacts produced, and execution observability. We're working on better linking Hamilton "projects" with MLFlow "experiments" and runs from both projects. From f3a2e0ec33ddc1e707453e74b6787e3d5f8e19f9 Mon Sep 17 00:00:00 2001 From: zilto Date: Tue, 11 Jun 2024 10:39:05 -0400 Subject: [PATCH 09/12] added a test for HamiltonTracker and DataSaver contract --- tests/plugins/test_mlflow_extension.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/plugins/test_mlflow_extension.py b/tests/plugins/test_mlflow_extension.py index 860e108ab..f9f2dadfc 100644 --- a/tests/plugins/test_mlflow_extension.py +++ b/tests/plugins/test_mlflow_extension.py @@ -8,6 +8,9 @@ from hamilton.plugins.mlflow_extensions import MLFlowModelLoader, MLFlowModelSaver +# TODO move these tests to `plugin_tests` because the required read-writes can get +# complicated and tests are time consuming. + @pytest.fixture def fitted_sklearn_model() -> BaseEstimator: @@ -149,3 +152,17 @@ def test_mlflow_handle_saver_kwargs(): assert saver.path == path assert saver.flavor == flavor assert saver.kwargs.get("unknown_kwarg") is True + + +def test_mlflow_registered_model_metadata(fitted_sklearn_model: BaseEstimator, tmp_path: Path): + """When registering a model through materializers, the metadata must contain the + key `registered_model` because the `hamilton.plugins.h_mlflow.MLFlowTracker` is expecting it. + """ + model_path = tmp_path / "sklearn_model" + saver = MLFlowModelSaver(flavor="sklearn", register_as="my_model") + + mlflow.set_tracking_uri(model_path.as_uri()) + with mlflow.start_run(): + metadata = saver.save_data(fitted_sklearn_model) + + assert metadata.get("registered_model") From f73d81eab7f4d04ac057224c37850ccd31dc4a77 Mon Sep 17 00:00:00 2001 From: zilto Date: Tue, 11 Jun 2024 11:01:55 -0400 Subject: [PATCH 10/12] updated tutorial notebook --- examples/mlflow/tutorial.ipynb | 986 +++++++++++++++++---------------- 1 file changed, 515 insertions(+), 471 deletions(-) diff --git a/examples/mlflow/tutorial.ipynb b/examples/mlflow/tutorial.ipynb index e0b7ac547..1e6dd950d 100644 --- a/examples/mlflow/tutorial.ipynb +++ b/examples/mlflow/tutorial.ipynb @@ -70,38 +70,12 @@ "\n", "Legend\n", "
\n", - "\n", - "\n", - "load_data\n", - "\n", - "load_data\n", - "dict\n", - "\n", - "\n", - "\n", - "y\n", - "\n", - "y\n", - "Series\n", - "\n", - "\n", - "\n", - "load_data->y\n", - "\n", - "\n", - "\n", "\n", - "\n", + "\n", "X\n", - "\n", - "X\n", - "DataFrame\n", - "\n", - "\n", - "\n", - "load_data->X\n", - "\n", - "\n", + "\n", + "X\n", + "DataFrame\n", "\n", "\n", "\n", @@ -110,17 +84,43 @@ "trained_model\n", "LogisticRegression\n", "\n", - "\n", + "\n", "\n", + "X->trained_model\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "y\n", + "\n", + "y\n", + "Series\n", + "\n", + "\n", + "\n", "y->trained_model\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", + "\n", + "\n", + "load_data\n", + "\n", + "load_data\n", + "dict\n", + "\n", + "\n", "\n", - "X->trained_model\n", - "\n", - "\n", + "load_data->X\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "load_data->y\n", + "\n", + "\n", "\n", "\n", "\n", @@ -132,7 +132,7 @@ "
\n" ], "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -199,12 +199,12 @@ "\n", "Legend\n", "\n", - "\n", + "\n", "\n", - "X\n", - "\n", - "X\n", - "DataFrame\n", + "y\n", + "\n", + "y\n", + "Series\n", "\n", "\n", "\n", @@ -213,37 +213,24 @@ "trained_model\n", "LogisticRegression\n", "\n", - "\n", - "\n", - "X->trained_model\n", - "\n", - "\n", + "\n", + "\n", + "y->trained_model\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "load_data\n", - "\n", - "load_data\n", - "dict\n", - "\n", - "\n", - "\n", - "load_data->X\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "y\n", - "\n", - "y\n", - "Series\n", + "X\n", + "\n", + "X\n", + "DataFrame\n", "\n", - "\n", - "\n", - "load_data->y\n", - "\n", - "\n", + "\n", + "\n", + "X->trained_model\n", + "\n", + "\n", "\n", "\n", "\n", @@ -254,16 +241,29 @@ "MLFlowModelSaver\n", "\n", "\n", - "\n", + "\n", "trained_model->trained_model__mlflow\n", "\n", "\n", "\n", - "\n", - "\n", - "y->trained_model\n", - "\n", - "\n", + "\n", + "\n", + "load_data\n", + "\n", + "load_data\n", + "dict\n", + "\n", + "\n", + "\n", + "load_data->y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "load_data->X\n", + "\n", + "\n", "\n", "\n", "\n", @@ -282,7 +282,7 @@ "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 3, @@ -364,7 +364,7 @@ "output_type": "stream", "text": [ "Registered model 'my_predictor' already exists. Creating a new version of this model...\n", - "Created version '7' of model 'my_predictor'.\n" + "Created version '8' of model 'my_predictor'.\n" ] }, { @@ -386,38 +386,25 @@ "\n", "Legend\n", "\n", - "\n", - "\n", - "load_data\n", - "\n", - "load_data\n", - "dict\n", - "\n", - "\n", - "\n", - "y\n", - "\n", - "y\n", - "Series\n", - "\n", - "\n", - "\n", - "load_data->y\n", - "\n", - "\n", - "\n", "\n", - "\n", + "\n", "X\n", - "\n", - "X\n", - "DataFrame\n", + "\n", + "X\n", + "DataFrame\n", "\n", - "\n", - "\n", - "load_data->X\n", - "\n", - "\n", + "\n", + "\n", + "trained_model\n", + "\n", + "trained_model\n", + "LogisticRegression\n", + "\n", + "\n", + "\n", + "X->trained_model\n", + "\n", + "\n", "\n", "\n", "\n", @@ -427,30 +414,43 @@ "trained_model__mlflow\n", "MLFlowModelSaver\n", "\n", - "\n", - "\n", - "trained_model\n", - "\n", - "trained_model\n", - "LogisticRegression\n", - "\n", "\n", - "\n", + "\n", "trained_model->trained_model__mlflow\n", "\n", "\n", "\n", + "\n", + "\n", + "load_data\n", + "\n", + "load_data\n", + "dict\n", + "\n", + "\n", + "\n", + "load_data->X\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "y\n", + "\n", + "y\n", + "Series\n", + "\n", + "\n", + "\n", + "load_data->y\n", + "\n", + "\n", + "\n", "\n", - "\n", + "\n", "y->trained_model\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "X->trained_model\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -475,7 +475,7 @@ "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 5, @@ -506,24 +506,24 @@ " 'sklearn_version': '1.5.0',\n", " 'serialization_format': 'cloudpickle',\n", " 'code': None}},\n", - " 'model_uri': 'runs:/67ebe9a6acdd402785428defe00b8c03/model',\n", - " 'model_uuid': '6641c1731cf549a08655dc0836ea51b1',\n", - " 'run_id': '67ebe9a6acdd402785428defe00b8c03',\n", + " 'model_uri': 'runs:/30c106f0915d43fda9f5974fb36cdc39/model',\n", + " 'model_uuid': '860cc1ea405d461d8c58db4b68d25158',\n", + " 'run_id': '30c106f0915d43fda9f5974fb36cdc39',\n", " 'saved_input_example_info': None,\n", " 'signature_dict': None,\n", " 'signature': None,\n", - " 'utc_time_created': '2024-06-10 23:38:25.769987',\n", + " 'utc_time_created': '2024-06-11 15:01:30.896479',\n", " 'mlflow_version': '2.13.2',\n", " 'metadata': None,\n", " 'registered_model': {'name': 'my_predictor',\n", - " 'version': 7,\n", - " 'creation_time': 1718062707186,\n", - " 'last_updated_timestamp': 1718062707186,\n", + " 'version': 8,\n", + " 'creation_time': 1718118092290,\n", + " 'last_updated_timestamp': 1718118092290,\n", " 'description': None,\n", " 'user_id': None,\n", " 'current_stage': 'None',\n", - " 'source': 'file:///home/tjean/projects/dagworks/hamilton/examples/mlflow/mlruns/0/67ebe9a6acdd402785428defe00b8c03/artifacts/model',\n", - " 'run_id': '67ebe9a6acdd402785428defe00b8c03',\n", + " 'source': 'file:///home/tjean/projects/dagworks/hamilton/examples/mlflow/mlruns/0/30c106f0915d43fda9f5974fb36cdc39/artifacts/model',\n", + " 'run_id': '30c106f0915d43fda9f5974fb36cdc39',\n", " 'run_link': None,\n", " 'status': 'READY',\n", " 'status_message': None,\n", @@ -572,79 +572,79 @@ "\n", "\n", - "\n", - "\n", + "\n", + "\n", "%3\n", - "\n", + "\n", "\n", "cluster__legend\n", - "\n", - "Legend\n", + "\n", + "Legend\n", "\n", - "\n", + "\n", "\n", - "preprocessed_inputs\n", - "\n", - "preprocessed_inputs\n", - "DataFrame\n", + "prediction\n", + "\n", + "prediction\n", + "int\n", "\n", - "\n", + "\n", "\n", - "prediction\n", - "\n", - "prediction\n", - "int\n", + "preprocessed_inputs\n", + "\n", + "preprocessed_inputs\n", + "DataFrame\n", "\n", "\n", - "\n", + "\n", "preprocessed_inputs->prediction\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "_preprocessed_inputs_inputs\n", - "\n", - "user_input\n", - "dict\n", + "_prediction_inputs\n", + "\n", + "model\n", + "BaseEstimator\n", "\n", - "\n", - "\n", - "_preprocessed_inputs_inputs->preprocessed_inputs\n", - "\n", - "\n", + "\n", + "\n", + "_prediction_inputs->prediction\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "_prediction_inputs\n", - "\n", - "model\n", - "BaseEstimator\n", + "_preprocessed_inputs_inputs\n", + "\n", + "user_input\n", + "dict\n", "\n", - "\n", + "\n", "\n", - "_prediction_inputs->prediction\n", - "\n", - "\n", + "_preprocessed_inputs_inputs->preprocessed_inputs\n", + "\n", + "\n", "\n", "\n", "\n", "input\n", - "\n", - "input\n", + "\n", + "input\n", "\n", "\n", "\n", "function\n", - "\n", - "function\n", + "\n", + "function\n", "\n", "\n", "\n" ], "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -704,27 +704,27 @@ "BaseEstimator\n", "\n", "\n", - "\n", + "\n", "prediction\n", "\n", "prediction\n", "int\n", "\n", "\n", - "\n", + "\n", "model->prediction\n", "\n", "\n", "\n", "\n", - "\n", + "\n", "preprocessed_inputs\n", "\n", "preprocessed_inputs\n", "DataFrame\n", "\n", "\n", - "\n", + "\n", "preprocessed_inputs->prediction\n", "\n", "\n", @@ -750,7 +750,7 @@ "dict\n", "\n", "\n", - "\n", + "\n", "_preprocessed_inputs_inputs->preprocessed_inputs\n", "\n", "\n", @@ -771,7 +771,7 @@ "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 8, @@ -867,98 +867,98 @@ "\n", "\n", - "\n", - "\n", + "\n", + "\n", "%3\n", - "\n", + "\n", "\n", "cluster__legend\n", - "\n", - "Legend\n", - "\n", - "\n", - "\n", - "model\n", - "\n", - "model\n", - "BaseEstimator\n", + "\n", + "Legend\n", "\n", "\n", - "\n", + "\n", "prediction\n", "\n", "prediction\n", "int\n", "\n", + "\n", + "\n", + "model\n", + "\n", + "model\n", + "BaseEstimator\n", + "\n", "\n", - "\n", + "\n", "model->prediction\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "preprocessed_inputs\n", - "\n", - "preprocessed_inputs\n", - "DataFrame\n", + "\n", + "preprocessed_inputs\n", + "DataFrame\n", "\n", "\n", - "\n", + "\n", "preprocessed_inputs->prediction\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", "load_data.model\n", - "\n", - "load_data.model\n", - "Tuple\n", + "\n", + "load_data.model\n", + "Tuple\n", "\n", "\n", - "\n", + "\n", "load_data.model->model\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", "_preprocessed_inputs_inputs\n", - "\n", - "user_input\n", - "dict\n", + "\n", + "user_input\n", + "dict\n", "\n", "\n", - "\n", + "\n", "_preprocessed_inputs_inputs->preprocessed_inputs\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", "input\n", - "\n", - "input\n", + "\n", + "input\n", "\n", "\n", "\n", "function\n", - "\n", - "function\n", + "\n", + "function\n", "\n", "\n", "\n", "output\n", - "\n", - "output\n", + "\n", + "output\n", "\n", "\n", "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 10, @@ -1055,271 +1055,271 @@ "\n", "\n", - "\n", - "\n", + "\n", + "\n", "%3\n", - "\n", + "\n", "\n", "cluster__legend\n", - "\n", - "Legend\n", + "\n", + "Legend\n", "\n", - "\n", + "\n", "\n", - "test_scatter_plot\n", - "\n", - "test_scatter_plot\n", - "Figure\n", + "y_train\n", + "\n", + "y_train\n", + "Series\n", + "\n", + "\n", + "\n", + "trained_model\n", + "\n", + "trained_model\n", + "BaseEstimator\n", + "\n", + "\n", + "\n", + "y_train->trained_model\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "train_performance\n", - "\n", - "train_performance\n", - "float\n", + "\n", + "train_performance\n", + "float\n", + "\n", + "\n", + "\n", + "y_train->train_performance\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "y_test\n", - "\n", - "y_test\n", - "Series\n", - "\n", - "\n", - "\n", - "y_test->test_scatter_plot\n", - "\n", - "\n", + "\n", + "y_test\n", + "Series\n", "\n", "\n", "\n", "test_performance\n", - "\n", - "test_performance\n", - "float\n", + "\n", + "test_performance\n", + "float\n", "\n", "\n", - "\n", + "\n", "y_test->test_performance\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "y_train\n", - "\n", - "y_train\n", - "Series\n", - "\n", - "\n", - "\n", - "y_train->train_performance\n", - "\n", - "\n", + "\n", + "\n", + "test_scatter_plot\n", + "\n", + "test_scatter_plot\n", + "Figure\n", "\n", - "\n", - "\n", - "trained_model\n", - "\n", - "trained_model\n", - "BaseEstimator\n", + "\n", + "\n", + "y_test->test_scatter_plot\n", + "\n", + "\n", "\n", - "\n", - "\n", - "y_train->trained_model\n", - "\n", - "\n", + "\n", + "\n", + "X_train\n", + "\n", + "X_train\n", + "DataFrame\n", "\n", - "\n", - "\n", - "test_predictions\n", - "\n", - "test_predictions\n", - "Series\n", + "\n", + "\n", + "X_train->trained_model\n", + "\n", + "\n", "\n", - "\n", - "\n", - "test_predictions->test_scatter_plot\n", - "\n", - "\n", + "\n", + "\n", + "train_predictions\n", + "\n", + "train_predictions\n", + "Series\n", "\n", - "\n", - "\n", - "test_predictions->test_performance\n", - "\n", - "\n", + "\n", + "\n", + "X_train->train_predictions\n", + "\n", + "\n", "\n", - "\n", - "\n", - "X\n", - "\n", - "X\n", - "DataFrame\n", + "\n", + "\n", + "y\n", + "\n", + "y\n", + "Series\n", "\n", "\n", "\n", "split_dataset\n", - "\n", - "split_dataset\n", - "dict\n", + "\n", + "split_dataset\n", + "dict\n", "\n", - "\n", + "\n", "\n", - "X->split_dataset\n", - "\n", - "\n", + "y->split_dataset\n", + "\n", + "\n", "\n", - "\n", - "\n", - "load_data\n", - "\n", - "load_data\n", - "dict\n", + "\n", + "\n", + "X\n", + "\n", + "X\n", + "DataFrame\n", "\n", - "\n", + "\n", "\n", - "load_data->X\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "y\n", - "\n", - "y\n", - "Series\n", - "\n", - "\n", - "\n", - "load_data->y\n", - "\n", - "\n", + "X->split_dataset\n", + "\n", + "\n", "\n", - "\n", - "\n", - "split_dataset->y_test\n", - "\n", - "\n", + "\n", + "\n", + "X_test\n", + "\n", + "X_test\n", + "DataFrame\n", "\n", - "\n", + "\n", "\n", - "split_dataset->y_train\n", - "\n", - "\n", + "X_test->test_scatter_plot\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "X_test\n", - "\n", - "X_test\n", - "DataFrame\n", + "test_predictions\n", + "\n", + "test_predictions\n", + "Series\n", "\n", - "\n", - "\n", - "split_dataset->X_test\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "X_test->test_predictions\n", + "\n", + "\n", "\n", - "\n", - "\n", - "X_train\n", - "\n", - "X_train\n", - "DataFrame\n", + "\n", + "\n", + "split_dataset->y_train\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "split_dataset->y_test\n", + "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "split_dataset->X_train\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "train_predictions\n", - "\n", - "train_predictions\n", - "Series\n", - "\n", - "\n", - "\n", - "train_predictions->train_performance\n", - "\n", - "\n", + "\n", + "\n", + "split_dataset->X_test\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "trained_model->test_predictions\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "trained_model->train_predictions\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "X_test->test_scatter_plot\n", - "\n", - "\n", + "\n", + "\n", + "load_data\n", + "\n", + "load_data\n", + "dict\n", "\n", - "\n", - "\n", - "X_test->test_predictions\n", - "\n", - "\n", + "\n", + "\n", + "load_data->y\n", + "\n", + "\n", "\n", - "\n", - "\n", - "X_train->train_predictions\n", - "\n", - "\n", + "\n", + "\n", + "load_data->X\n", + "\n", + "\n", "\n", - "\n", - "\n", - "X_train->trained_model\n", - "\n", - "\n", + "\n", + "\n", + "test_predictions->test_performance\n", + "\n", + "\n", "\n", - "\n", - "\n", - "y->split_dataset\n", - "\n", - "\n", + "\n", + "\n", + "test_predictions->test_scatter_plot\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "train_predictions->train_performance\n", + "\n", + "\n", "\n", "\n", "\n", "_split_dataset_inputs\n", - "\n", - "test_size_fraction\n", - "float\n", + "\n", + "test_size_fraction\n", + "float\n", "\n", "\n", - "\n", + "\n", "_split_dataset_inputs->split_dataset\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", "input\n", - "\n", - "input\n", + "\n", + "input\n", "\n", "\n", "\n", "function\n", - "\n", - "function\n", + "\n", + "function\n", "\n", "\n", "\n" ], "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -1490,7 +1490,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 15, "metadata": {}, "outputs": [ { @@ -1498,12 +1498,12 @@ "output_type": "stream", "text": [ "Registered model 'my_new_model' already exists. Creating a new version of this model...\n", - "Created version '5' of model 'my_new_model'.\n" + "Created version '6' of model 'my_new_model'.\n" ] }, { "data": { - "image/png": "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", + "image/png": "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", "text/plain": [ "
" ] @@ -1523,107 +1523,151 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 4. Feature list" + "## 4. Feature list\n", + "A list of included features and cool things possible with the MLFlow plugin (in no particular order)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Automatically tracks `.execute(inputs=...)` and `Builder().with_config()` as MLFlow params. This creates columns that you can use to filter runs in the UI." + "1. Automatically tracks `.execute(inputs=...)` and `Builder().with_config()` as MLFlow params. This creates columns that you can use to filter runs in the UI." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The run tag `code_version` is automatically added by the `MLFlowTracker`. This allows you to know exactly what code was executed and group runs that use the same code, but vary in terms of inputs. " + "2. The run tag `code_version` is automatically added by the `MLFlowTracker`. This allows you to know exactly what code was executed and group runs that use the same code, but vary in terms of inputs. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Store the entire `HamiltonGraph` as an artifact `hamilton_graph.json`. This contains the source code of the executed dataflow." + "3. Store the entire `HamiltonGraph` as an artifact `hamilton_graph.json`. This contains the source code of the executed dataflow." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Automatically log `plotly` and `matplotlib` figures as `.png` artifacts. For more control, you can use the `to.plotly()` and `to.plt()` savers. Notably, this allows you to save interactive plotly visualizations as HTML. " + "4. Automatically log `plotly` and `matplotlib` figures as `.png` artifacts. For more control, you can use the `to.plotly()` and `to.plt()` savers. Notably, this allows you to save interactive plotly visualizations as HTML. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Use the `MLFlowTracker` to specify experiment metadata and run metadata that will help you browse the MLFlow UI and programatic search. `experiment_description` and `run_description` accept markdown strings. " + "5. Use the `MLFlowTracker` to specify experiment metadata and run metadata that will help you browse the MLFlow UI and programatic search. `experiment_description` and `run_description` accept markdown strings.\n", + "\n", + " ```python\n", + " MLFlowTracker(\n", + " experiment_name=...,\n", + " experiment_description=...,\n", + " run_name=...,\n", + " run_tags=...,\n", + " run_description=...,\n", + " )\n", + " ```" ] }, { - "cell_type": "code", - "execution_count": 16, + "cell_type": "markdown", "metadata": {}, - "outputs": [ - { - "ename": "MlflowException", - "evalue": "Invalid experiment name: Ellipsis. Expects a string.", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mMlflowException\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[16], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mMLFlowTracker\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 2\u001b[0m \u001b[43m \u001b[49m\u001b[43mexperiment_name\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 3\u001b[0m \u001b[43m \u001b[49m\u001b[43mexperiment_description\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 4\u001b[0m \u001b[43m \u001b[49m\u001b[43mrun_name\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 5\u001b[0m \u001b[43m \u001b[49m\u001b[43mrun_tags\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 6\u001b[0m \u001b[43m \u001b[49m\u001b[43mrun_description\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 7\u001b[0m \u001b[43m)\u001b[49m\n", - "File \u001b[0;32m~/projects/dagworks/hamilton/hamilton/plugins/h_mlflow.py:98\u001b[0m, in \u001b[0;36mMLFlowTracker.__init__\u001b[0;34m(self, tracking_uri, registry_uri, artifact_location, experiment_name, experiment_tags, experiment_description, run_id, run_name, run_tags, run_description, log_system_metrics)\u001b[0m\n\u001b[1;32m 95\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mclient\u001b[38;5;241m.\u001b[39mset_experiment_tag(experiment_id, key\u001b[38;5;241m=\u001b[39mk, value\u001b[38;5;241m=\u001b[39mv)\n\u001b[1;32m 96\u001b[0m \u001b[38;5;66;03m# create an experiment\u001b[39;00m\n\u001b[1;32m 97\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m---> 98\u001b[0m experiment_id \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mclient\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcreate_experiment\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 99\u001b[0m \u001b[43m \u001b[49m\u001b[43mname\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mexperiment_name\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 100\u001b[0m \u001b[43m \u001b[49m\u001b[43martifact_location\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43martifact_location\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 101\u001b[0m \u001b[43m \u001b[49m\u001b[43mtags\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mexperiment_tags\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 102\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 103\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mexperiment_id \u001b[38;5;241m=\u001b[39m experiment_id\n\u001b[1;32m 105\u001b[0m \u001b[38;5;66;03m# run setup\u001b[39;00m\n\u001b[1;32m 106\u001b[0m \u001b[38;5;66;03m# TODO link HamiltonTracker and MLFlowTracker run ids\u001b[39;00m\n", - "File \u001b[0;32m~/projects/dagworks/hamilton/venv/lib/python3.11/site-packages/mlflow/tracking/client.py:1284\u001b[0m, in \u001b[0;36mMlflowClient.create_experiment\u001b[0;34m(self, name, artifact_location, tags)\u001b[0m\n\u001b[1;32m 1232\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mcreate_experiment\u001b[39m(\n\u001b[1;32m 1233\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[1;32m 1234\u001b[0m name: \u001b[38;5;28mstr\u001b[39m,\n\u001b[1;32m 1235\u001b[0m artifact_location: Optional[\u001b[38;5;28mstr\u001b[39m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[1;32m 1236\u001b[0m tags: Optional[Dict[\u001b[38;5;28mstr\u001b[39m, Any]] \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[1;32m 1237\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m \u001b[38;5;28mstr\u001b[39m:\n\u001b[1;32m 1238\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Create an experiment.\u001b[39;00m\n\u001b[1;32m 1239\u001b[0m \n\u001b[1;32m 1240\u001b[0m \u001b[38;5;124;03m Args:\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 1282\u001b[0m \n\u001b[1;32m 1283\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m-> 1284\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_tracking_client\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcreate_experiment\u001b[49m\u001b[43m(\u001b[49m\u001b[43mname\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43martifact_location\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtags\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[0;32m~/projects/dagworks/hamilton/venv/lib/python3.11/site-packages/mlflow/tracking/_tracking_service/client.py:498\u001b[0m, in \u001b[0;36mTrackingServiceClient.create_experiment\u001b[0;34m(self, name, artifact_location, tags)\u001b[0m\n\u001b[1;32m 484\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"Create an experiment.\u001b[39;00m\n\u001b[1;32m 485\u001b[0m \n\u001b[1;32m 486\u001b[0m \u001b[38;5;124;03mArgs:\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 495\u001b[0m \n\u001b[1;32m 496\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 497\u001b[0m _validate_experiment_artifact_location(artifact_location)\n\u001b[0;32m--> 498\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstore\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcreate_experiment\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 499\u001b[0m \u001b[43m \u001b[49m\u001b[43mname\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mname\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 500\u001b[0m \u001b[43m \u001b[49m\u001b[43martifact_location\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43martifact_location\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 501\u001b[0m \u001b[43m \u001b[49m\u001b[43mtags\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m[\u001b[49m\u001b[43mExperimentTag\u001b[49m\u001b[43m(\u001b[49m\u001b[43mkey\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mvalue\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43m(\u001b[49m\u001b[43mkey\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mvalue\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mtags\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mitems\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m]\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mif\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mtags\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01melse\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43m[\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 502\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[0;32m~/projects/dagworks/hamilton/venv/lib/python3.11/site-packages/mlflow/store/tracking/file_store.py:391\u001b[0m, in \u001b[0;36mFileStore.create_experiment\u001b[0;34m(self, name, artifact_location, tags)\u001b[0m\n\u001b[1;32m 389\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mcreate_experiment\u001b[39m(\u001b[38;5;28mself\u001b[39m, name, artifact_location\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, tags\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m):\n\u001b[1;32m 390\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_check_root_dir()\n\u001b[0;32m--> 391\u001b[0m \u001b[43m_validate_experiment_name\u001b[49m\u001b[43m(\u001b[49m\u001b[43mname\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 392\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_validate_experiment_does_not_exist(name)\n\u001b[1;32m 393\u001b[0m experiment_id \u001b[38;5;241m=\u001b[39m _generate_unique_integer_id()\n", - "File \u001b[0;32m~/projects/dagworks/hamilton/venv/lib/python3.11/site-packages/mlflow/utils/validation.py:366\u001b[0m, in \u001b[0;36m_validate_experiment_name\u001b[0;34m(experiment_name)\u001b[0m\n\u001b[1;32m 361\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m MlflowException(\n\u001b[1;32m 362\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mInvalid experiment name: \u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mexperiment_name\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m\"\u001b[39m, error_code\u001b[38;5;241m=\u001b[39mINVALID_PARAMETER_VALUE\n\u001b[1;32m 363\u001b[0m )\n\u001b[1;32m 365\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m is_string_type(experiment_name):\n\u001b[0;32m--> 366\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m MlflowException(\n\u001b[1;32m 367\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mInvalid experiment name: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mexperiment_name\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m. Expects a string.\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 368\u001b[0m error_code\u001b[38;5;241m=\u001b[39mINVALID_PARAMETER_VALUE,\n\u001b[1;32m 369\u001b[0m )\n", - "\u001b[0;31mMlflowException\u001b[0m: Invalid experiment name: Ellipsis. Expects a string." - ] - } - ], "source": [ - "MLFlowTracker(\n", - " experiment_name=...,\n", - " experiment_description=...,\n", - " run_name=...,\n", - " run_tags=...,\n", - " run_description=...,\n", - ")" + "6. The tags specified using the Hamilton decorator `@tag` on the model-producing function are stored in the MLFlow model registry\n", + " ```python\n", + " import pandas as pd\n", + " from hamilton.function_modifiers import tag\n", + " from sklearn.linear_model import LogisticRegression\n", + "\n", + " @tag(team=\"forecast\", feature_set=\"v3\")\n", + " def trained_model(X_train: pd.DataFrame, y_train: pd.Series) -> LogisticRegression:\n", + " \"\"\"Fit a binary classifier on the training data\"\"\"\n", + " model = LogisticRegression()\n", + " model.fit(X_train, y_train)\n", + " return model\n", + "\n", + " # ...\n", + "\n", + " to.mlflow(\n", + " id=\"trained_model__mlflow\",\n", + " dependencies=[\"trained_model\"],\n", + " register_as=\"new_algo\",\n", + " ),\n", + " ```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The tags specified using the Hamilton decorator `@tag` on the model-producing function are stored in the MLFlow model registry " + "7. Use the `MLFlowTracker` with the `HamiltonTracker`. You can link the two by matching:\n", + "\n", + " - `experiment_name` == `project_id`; You can manually create an `experiment_id`, but you can set its name.\n", + " - `run_name` == `dag_name`; You can have multiple MLFlow runs with the same name\n", + "\n", + " ```python\n", + " from hamilton import driver\n", + " from hamilton.io.materialization import to\n", + " from hamilton.plugins.h_mlflow import MLFlowTracker\n", + " from hamilton_sdk.adapters import HamiltonTracker\n", + "\n", + " project_id = 3\n", + " dag_name = \"titanic_classifier_training\"\n", + "\n", + " dr = (\n", + " driver.Builder()\n", + " .with_modules(model_training_2)\n", + " .with_adapters(\n", + " MLFlowTracker(\n", + " experiment_name=f\"hamilton-project-{project_id}\",\n", + " run_name=dag_name,\n", + " ),\n", + " HamiltonTracker(\n", + " username=\"my_username\",\n", + " project_id=project_id,\n", + " dag_name=dag_name,\n", + " )\n", + " )\n", + " .with_materializers(\n", + " to.mlflow(\n", + " id=\"trained_model__mlflow\",\n", + " dependencies=[\"trained_model\"],\n", + " register_as=\"my_new_model\",\n", + " ),\n", + " )\n", + " .build()\n", + " )\n", + " ```" ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "import pandas as pd\n", - "from hamilton.function_modifiers import tag\n", - "from sklearn.linear_model import LogisticRegression\n", + "8. Log model performance with nested runs (e.g., cross-validation, hyperparameter tuning). This will require adding `mlflow` code in your dataflow definition though.\n", + " ```python\n", + " import mlflow\n", + " from sklearn.model_selection import KFold\n", "\n", - "@tag(team=\"forecast\", feature_set=\"v3\")\n", - "def trained_model(X_train: pd.DataFrame, y_train: pd.Series) -> LogisticRegression:\n", - " \"\"\"Fit a binary classifier on the training data\"\"\"\n", - " model = LogisticRegression()\n", - " model.fit(X_train, y_train)\n", - " return model\n", + " def model_cross_validation(X: pd.DataFrame, y: pd.Series):\n", + " kfold = KFold(n_splits=3)\n", + "\n", + " for train, test in kf.split(X):\n", + " X_train, X_test, y_train, y_test = X[train], X[test], y[train], y[test]\n", + "\n", + " model = LogisticRegression()\n", + " model.fit(X_train, y_train)\n", "\n", - "# ...\n", + " test_pred = model.predict(X_test)\n", + " score = balanced_accuracy(y_test, test_pred)\n", "\n", - "to.mlflow(\n", - " id=\"trained_model__mlflow\",\n", - " dependencies=[\"trained_model\"],\n", - " register_as=\"new_algo\",\n", - ")," + " with mlflow.start_run(nested=True):\n", + " mlflow.log_metric(\"balanced_accuracy\", score)\n", + " # ... could log plots, hyperparams, etc.\n", + " ```\n" ] } ], From 6caf06ed577c334346af2436767360177b8113b4 Mon Sep 17 00:00:00 2001 From: zilto Date: Tue, 11 Jun 2024 11:28:51 -0400 Subject: [PATCH 11/12] added docs, added TODOs --- docs/integrations/index.rst | 1 + hamilton/plugins/h_mlflow.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/docs/integrations/index.rst b/docs/integrations/index.rst index 7fb9aaafb..3f5c68d6e 100644 --- a/docs/integrations/index.rst +++ b/docs/integrations/index.rst @@ -10,6 +10,7 @@ This section showcases how Hamilton integrates with popular frameworks. ibis/index streamlit dbt + MLFlow Airflow Amazon Web Services Burr diff --git a/hamilton/plugins/h_mlflow.py b/hamilton/plugins/h_mlflow.py index 75ccee81a..1119ced12 100644 --- a/hamilton/plugins/h_mlflow.py +++ b/hamilton/plugins/h_mlflow.py @@ -183,6 +183,7 @@ def run_after_node_execution( ): """Log materializers and final vars as artifacts""" # log DataSavers as artifacts + # TODO refactor if/else as `handle_materializers()` if node_tags.get("hamilton.data_saver") is True: # don't log mlflow materializers as artifact since they already create models # instead, use the Materializer metadata to add metadata to registered models @@ -246,6 +247,7 @@ def run_after_node_execution( if node_name not in self.final_vars: return + # TODO refactor if/else as `handle_final_vars()` # log float and int as metrics if node_return_type in [float, int]: self.client.log_metric(self.run_id, key=node_name, value=float(result)) From 98da37aaae326afc12bdc7445883878c41272e20 Mon Sep 17 00:00:00 2001 From: zilto Date: Wed, 12 Jun 2024 12:44:48 -0400 Subject: [PATCH 12/12] added metadata to registered models --- hamilton/plugins/h_mlflow.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/hamilton/plugins/h_mlflow.py b/hamilton/plugins/h_mlflow.py index 1119ced12..2d77815a0 100644 --- a/hamilton/plugins/h_mlflow.py +++ b/hamilton/plugins/h_mlflow.py @@ -205,6 +205,27 @@ def run_after_node_execution( model_name, version, materialized_node.documentation ) + # add node name as tag + self.client.set_registered_model_tag( + model_name, key="node_name", value=materialized_node.name + ) + self.client.set_model_version_tag( + model_name, version, key="node_name", value=materialized_node.name + ) + + # add origin function name as tag + self.client.set_registered_model_tag( + model_name, + key="function_name", + value=materialized_node.originating_functions[0].__name__, + ) + self.client.set_model_version_tag( + model_name, + version, + key="function_name", + value=materialized_node.originating_functions[0].__name__, + ) + # add the materialized node @tag values as tags for k, v in materialized_node.tags.items(): # skip internal Hamilton tags