From e4e76f6aada45945047c211cc49c805e0ecf67c6 Mon Sep 17 00:00:00 2001 From: Stefan Krawczyk Date: Tue, 30 Apr 2024 14:03:39 -0700 Subject: [PATCH] Adds Hamilton UI Example Updates docs and references for it. --- docs/concepts/ui.rst | 31 ++++- examples/hamilton_ui/README.md | 49 ++++++++ examples/hamilton_ui/components/__init__.py | 0 .../components/feature_transforms.py | 108 ++++++++++++++++++ .../hamilton_ui/components/iris_loader.py | 35 ++++++ .../hamilton_ui/components/model_fitting.py | 78 +++++++++++++ examples/hamilton_ui/components/models.py | 57 +++++++++ examples/hamilton_ui/iris.parquet | Bin 0 -> 5562 bytes examples/hamilton_ui/requirements.txt | 4 + examples/hamilton_ui/run.py | 83 ++++++++++++++ .../Project/ProjectLogInstructions.tsx | 2 +- 11 files changed, 442 insertions(+), 5 deletions(-) create mode 100644 examples/hamilton_ui/README.md create mode 100644 examples/hamilton_ui/components/__init__.py create mode 100644 examples/hamilton_ui/components/feature_transforms.py create mode 100644 examples/hamilton_ui/components/iris_loader.py create mode 100644 examples/hamilton_ui/components/model_fitting.py create mode 100644 examples/hamilton_ui/components/models.py create mode 100644 examples/hamilton_ui/iris.parquet create mode 100644 examples/hamilton_ui/requirements.txt create mode 100644 examples/hamilton_ui/run.py diff --git a/docs/concepts/ui.rst b/docs/concepts/ui.rst index d08542dee..1b9377e84 100644 --- a/docs/concepts/ui.rst +++ b/docs/concepts/ui.rst @@ -13,6 +13,12 @@ The UI consists of the following features: Overview -------- +If you'd like a video walkthrough on getting set up, you can watch the following video: + +.. raw:: html + + + Getting Started --------------- @@ -100,7 +106,9 @@ Then, navigate to the project page (dashboard/projects), in the running UI, and Remember the project ID -- you'll use it for the next steps. -Add the following adapter to your code: +Existing Hamilton Code +______________________ +Add the following adapter to your code if you have existing Hamilton code: .. code-block:: python @@ -123,12 +131,27 @@ Add the following adapter to your code: Then run your DAG, and follow the links in the logs! +I need some Hamilton code to run +________________________________ +If you don't have Hamilton code to run this with, you can run Hamilton UI example under `examples/hamilton_ui `_: + +.. code-block:: bash + + # we assume you're in the Hamilton repository root + cd examples/hamilton_ui + # make sure you have the right python packages installed + pip install -r requirements.txt + # run the pipeline providing the email and project_id you created in the UI + python run.py --email --project_id + +You should see links in the `logs to the UI `_, where you can see the DAG run + the data summaries captured. -Exploring in the UI +Exploring the UI ------------------- -Once you get to the UI, you will be navigated to the projects page. After you create one + log, -you can navigate to `runs/history` for a history of runs. You can select by tags, date, etc... +Once you get to the UI, you can navigate to the projects page (left hand nav-bar). Assuming you have created a project +and logged to it, you can then navigate to view it and then more details about it. E.g. versions, code, lineage, catalog, execution runs. +See below for a few screenshots of the UI. ---- diff --git a/examples/hamilton_ui/README.md b/examples/hamilton_ui/README.md new file mode 100644 index 000000000..001c9080f --- /dev/null +++ b/examples/hamilton_ui/README.md @@ -0,0 +1,49 @@ +# machine\_learning + +This template shows a ML pipeline. + +It shows a few things: + +1. It shows how one could split up functions into modules. E.g. loading, vs features, vs fitting. +2. It also shows how to use `@subdag` to fit different models in the same DAG run and reuse the same fitting code. +3. It shows how to use data loaders and data savers to load and save data, that also then emit extra metadata +that can be used to track lineage in the UI. +4. It shows how to use the HamiltonTracker to integrate with the Hamilton UI. + +## Getting started + +To get started, you need to have the Hamilton UI running. + +1. See https://hamilton.dagworks.io/en/latest/concepts/ui/ for details, here are the cliff notes: + + ```bash + git clone https://github.com/dagworks-inc/hamilton + cd hamilton/ui/deployment + ./run.sh + ``` + Then go to http://localhost:8242 and create (1) an email, and (2) a project. + See [this video](https://youtu.be/DPfxlTwaNsM) for a walkthrough. + +2. Ensure you have the right python dependencies installed. +```bash +cd hamilton/examples/hamilton_ui +pip install -r requirements.txt +``` + +2. Run the `run.py` script. Providing the email, and project ID to be able to log to the Hamilton UI. +```bash +python run.py --email --project_id +``` +Once you've run that, run this: +```bash +python run.py --email --project_id --load-from-parquet True +``` +Then you can go see the difference in the Hamilton UI. Find your project under http://localhost:8242/dashboard/projects. + +## Things to try: + +1. Place an error in the code and see how it shows up in the Hamilton UI. e.g. `raise ValueError("I'm an error")`. +2. In `models.py` change `"data_set": source("data_set_v1"),` to `"data_set": source("data_set_v2"),`, along with +what is requested in `run.py` (i.e. change/add saving `data_set_v2`) and see how the lineage changes in the Hamilton UI. +3. Add a new feature and propagate it through the pipeline. E.g. add a new feature to `features.py` and then to a dataset. +Execute it and then compare the data observed in the Hamilton UI against a prior run. diff --git a/examples/hamilton_ui/components/__init__.py b/examples/hamilton_ui/components/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/hamilton_ui/components/feature_transforms.py b/examples/hamilton_ui/components/feature_transforms.py new file mode 100644 index 000000000..829d5b8f1 --- /dev/null +++ b/examples/hamilton_ui/components/feature_transforms.py @@ -0,0 +1,108 @@ +""" +Module to transform iris data into features. +""" + +import numpy as np +import pandas as pd + +from hamilton.function_modifiers import parameterize_sources + +RAW_FEATURES = ["sepal_length_cm", "sepal_width_cm", "petal_length_cm", "petal_width_cm"] + +# Here is more terse code that does the same thing as the below *_log functions. +# Any `@parameterize*` decorator is just a less verbose way of defining functions that differ +# slightly. We don't see anything wrong with verbose code - so we recommend err'ing on the side of +# verbosity, but otherwise for this example show the terser code. +# @parameterize_sources(**{f"{col}_log": {"col": col} for col in RAW_FEATURES}) +# def log_value(col: pd.Series) -> pd.Series: +# """Log value of {col}.""" +# return np.log(col) + + +def sepal_length_cm_log(sepal_length_cm: pd.Series) -> pd.Series: + """Log value of sepal_length_cm_.""" + return np.log(sepal_length_cm) + + +def sepal_width_cm_log(sepal_width_cm: pd.Series) -> pd.Series: + """Log value of sepal_width_cm_.""" + return np.log(sepal_width_cm) + + +def petal_length_cm_log(petal_length_cm: pd.Series) -> pd.Series: + """Log value of petal_length_cm_.""" + return np.log(petal_length_cm) + + +def petal_width_cm_log(petal_width_cm: pd.Series) -> pd.Series: + """Log value of petal_width_cm_.""" + return np.log(petal_width_cm) + + +@parameterize_sources(**{f"{col}_mean": {"col": col} for col in RAW_FEATURES}) +def mean_value(col: pd.Series) -> float: + """Mean of {col}.""" + return col.mean() + + +@parameterize_sources(**{f"{col}_std": {"col": col} for col in RAW_FEATURES}) +def std_value(col: pd.Series) -> float: + """Standard deviation of {col}.""" + return col.std() + + +@parameterize_sources( + **{ + f"{col}_normalized": {"col": col, "col_mean": f"{col}_mean", "col_std": f"{col}_std"} + for col in RAW_FEATURES + } +) +def normalized_value(col: pd.Series, col_mean: float, col_std: float) -> pd.Series: + """Normalized column of {col}.""" + return (col - col_mean) / col_std + + +def data_set_v1( + sepal_length_cm_normalized: pd.Series, + sepal_width_cm_normalized: pd.Series, + petal_length_cm_normalized: pd.Series, + petal_width_cm_normalized: pd.Series, + target_class: pd.Series, +) -> pd.DataFrame: + """Explicitly define the feature set we want to use.""" + return pd.DataFrame( + { + "sepal_length_cm_normalized": sepal_length_cm_normalized, + "sepal_width_cm_normalized": sepal_width_cm_normalized, + "petal_length_cm_normalized": petal_length_cm_normalized, + "petal_width_cm_normalized": petal_width_cm_normalized, + "target_class": target_class, + } + ) + + +def data_set_v2( + sepal_length_cm_normalized: pd.Series, + sepal_width_cm_normalized: pd.Series, + petal_length_cm_normalized: pd.Series, + petal_width_cm_normalized: pd.Series, + sepal_length_cm_log: pd.Series, + sepal_width_cm_log: pd.Series, + petal_length_cm_log: pd.Series, + petal_width_cm_log: pd.Series, + target_class: pd.Series, +) -> pd.DataFrame: + """Explicitly define the feature set we want to use. This one adds `log` features.""" + return pd.DataFrame( + { + "sepal_length_cm_normalized": sepal_length_cm_normalized, + "sepal_width_cm_normalized": sepal_width_cm_normalized, + "petal_length_cm_normalized": petal_length_cm_normalized, + "petal_width_cm_normalized": petal_width_cm_normalized, + "sepal_length_cm_log": sepal_length_cm_log, + "sepal_width_cm_log": sepal_width_cm_log, + "petal_length_cm_log": petal_length_cm_log, + "petal_width_cm_log": petal_width_cm_log, + "target_class": target_class, + } + ) diff --git a/examples/hamilton_ui/components/iris_loader.py b/examples/hamilton_ui/components/iris_loader.py new file mode 100644 index 000000000..5aa75408a --- /dev/null +++ b/examples/hamilton_ui/components/iris_loader.py @@ -0,0 +1,35 @@ +""" +Module to load iris data. +""" + +import pandas as pd +from sklearn import datasets, utils + +from hamilton.function_modifiers import config, extract_columns, load_from + +RAW_COLUMN_NAMES = [ + "sepal_length_cm", + "sepal_width_cm", + "petal_length_cm", + "petal_width_cm", +] + + +@config.when(case="api") +def iris_data_raw__api() -> utils.Bunch: + return datasets.load_iris() + + +@extract_columns(*(RAW_COLUMN_NAMES + ["target_class"])) +@config.when(case="api") +def iris_df__api(iris_data_raw: utils.Bunch) -> pd.DataFrame: + _df = pd.DataFrame(iris_data_raw.data, columns=RAW_COLUMN_NAMES) + _df["target_class"] = [iris_data_raw.target_names[t] for t in iris_data_raw.target] + return _df + + +@extract_columns(*(RAW_COLUMN_NAMES + ["target_class"])) +@load_from.parquet(path="iris.parquet") +@config.when(case="parquet") +def iris_df__parquet(iris_data_raw: pd.DataFrame) -> pd.DataFrame: + return iris_data_raw diff --git a/examples/hamilton_ui/components/model_fitting.py b/examples/hamilton_ui/components/model_fitting.py new file mode 100644 index 000000000..778436312 --- /dev/null +++ b/examples/hamilton_ui/components/model_fitting.py @@ -0,0 +1,78 @@ +"""This module contains basic code for model fitting.""" + +from typing import Dict + +import numpy as np +import pandas as pd +from sklearn import base, linear_model, metrics, svm +from sklearn.model_selection import train_test_split + +from hamilton import function_modifiers + + +@function_modifiers.config.when(clf="svm") +def prefit_clf__svm(gamma: float = 0.001) -> base.ClassifierMixin: + """Returns an unfitted SVM classifier object. + + :param gamma: ... + :return: + """ + return svm.SVC(gamma=gamma) + + +@function_modifiers.config.when(clf="logistic") +def prefit_clf__logreg(penalty: str) -> base.ClassifierMixin: + """Returns an unfitted Logistic Regression classifier object. + + :param penalty: One of {'l1', 'l2', 'elasticnet', None}. + :return: + """ + return linear_model.LogisticRegression(penalty) + + +@function_modifiers.extract_fields( + {"X_train": pd.DataFrame, "X_test": pd.DataFrame, "y_train": pd.Series, "y_test": pd.Series} +) +def train_test_split_func( + data_set: pd.DataFrame, + test_size_fraction: float, + shuffle_train_test_split: bool, +) -> Dict[str, np.ndarray]: + """Function that creates the training & test splits. + + It this then extracted out into constituent components and used downstream. + + :param data_set: + :param test_size_fraction: + :param shuffle_train_test_split: + :return: + """ + assert "target_class" in data_set.columns, "target_class column must be present in the data set" + feature_set = data_set[[col for col in data_set.columns if col != "target_class"]] + target_class = data_set["target_class"] + X_train, X_test, y_train, y_test = train_test_split( + feature_set, target_class, test_size=test_size_fraction, shuffle=shuffle_train_test_split + ) + return {"X_train": X_train, "X_test": X_test, "y_train": y_train, "y_test": y_test} + + +def fit_clf( + prefit_clf: base.ClassifierMixin, X_train: pd.DataFrame, y_train: pd.Series +) -> base.ClassifierMixin: + """Calls fit on the classifier object; it mutates it.""" + prefit_clf.fit(X_train, y_train) + return prefit_clf + + +def training_accuracy( + fit_clf: base.ClassifierMixin, X_train: pd.DataFrame, y_train: pd.Series +) -> float: + """Returns accuracy on the training set.""" + return metrics.accuracy_score(fit_clf.predict(X_train), y_train) + + +def testing_accuracy( + fit_clf: base.ClassifierMixin, X_test: pd.DataFrame, y_test: pd.Series +) -> float: + """Returns accuracy on the test set.""" + return metrics.accuracy_score(fit_clf.predict(X_test), y_test) diff --git a/examples/hamilton_ui/components/models.py b/examples/hamilton_ui/components/models.py new file mode 100644 index 000000000..6c4e056de --- /dev/null +++ b/examples/hamilton_ui/components/models.py @@ -0,0 +1,57 @@ +"""This module contains specific incarnations of models.""" + +from sklearn import base + +from hamilton.function_modifiers import source, subdag + +try: + import model_fitting +except ImportError: + from . import model_fitting + + +@subdag( + model_fitting, + inputs={ + "data_set": source("data_set_v1"), + }, + config={"clf": "svm", "shuffle_train_test_split": True, "test_size_fraction": 0.2}, +) +def svm_model( + fit_clf: base.ClassifierMixin, training_accuracy: float, testing_accuracy: float +) -> dict: + return { + "svm": fit_clf, + "training_accuracy": training_accuracy, + "testing_accuracy": testing_accuracy, + } + + +@subdag( + model_fitting, + inputs={ + "data_set": source("data_set_v1"), + }, + config={ + "clf": "logistic", + "shuffle_train_test_split": True, + "test_size_fraction": 0.2, + "penalty": "l2", + }, +) +def lr_model( + fit_clf: base.ClassifierMixin, training_accuracy: float, testing_accuracy: float +) -> dict: + return { + "logistic": fit_clf, + "training_accuracy": training_accuracy, + "testing_accuracy": testing_accuracy, + } + + +def best_model(svm_model: dict, lr_model: dict) -> dict: + """Returns the best model based on the testing accuracy.""" + if svm_model["testing_accuracy"] > lr_model["testing_accuracy"]: + return svm_model + else: + return lr_model diff --git a/examples/hamilton_ui/iris.parquet b/examples/hamilton_ui/iris.parquet new file mode 100644 index 0000000000000000000000000000000000000000..53925c03f53191a00bed277bb5e507176c8d0b14 GIT binary patch literal 5562 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 XzTApi_QOA~q<_$VH3*Hs|Jwc^b^QIx literal 0 HcmV?d00001 diff --git a/examples/hamilton_ui/requirements.txt b/examples/hamilton_ui/requirements.txt new file mode 100644 index 000000000..dd988575c --- /dev/null +++ b/examples/hamilton_ui/requirements.txt @@ -0,0 +1,4 @@ +click +pandas +scikit-learn +sf-hamilton[sdk] diff --git a/examples/hamilton_ui/run.py b/examples/hamilton_ui/run.py new file mode 100644 index 000000000..cfacb8877 --- /dev/null +++ b/examples/hamilton_ui/run.py @@ -0,0 +1,83 @@ +import click +from components import feature_transforms, iris_loader, models +from hamilton_sdk import adapters + +from hamilton import driver as h_driver +from hamilton.io.materialization import to +from hamilton.lifecycle import PrintLnHook + + +@click.command() +@click.option("--load-from-parquet", is_flag=False) +@click.option("--email", help="Email for the Hamilton UI", type=str, required=True) +@click.option( + "--project-id", help="Project ID to log to for the Hamilton UI", type=int, required=True +) +def run(load_from_parquet: bool, email: str, project_id: int): + """ + Runs the machine_learning hamilton DAG emitting metadata to the Hamilton UI. + + Prerequisite: + - You need to have the Hamilton UI running. + + :param load_from_parquet: If true, the DAG will not be logged to DAGWorks + :param email: Email for the Hamilton UI + :param project_id: Project ID to log to for the Hamilton UI + """ + if load_from_parquet: + config = {"case": "parquet"} + else: + config = {"case": "api"} + dag_name = "machine_learning_dag" + + # create tracker object + tracker = adapters.HamiltonTracker( + username=email, + project_id=project_id, + dag_name=dag_name, + tags={ + "template": "machine_learning", + "loading_data_from": "parquet" if load_from_parquet else "api", + "TODO": "add_more_tags_to_find_your_run_later", + }, + ) + + # create driver object + dr = ( + h_driver.Builder() + .with_config(config) # this shapes the DAG + .with_modules(iris_loader, feature_transforms, models) + .with_adapters(tracker, PrintLnHook(verbosity=1)) + .build() + ) + inputs = {} + # execute the DAG and materialize a few things from it + metadata, result = dr.materialize( + # This approach helps centralize & standardize how objects are read/written and also how metadata + # about them is captured. This is useful for tracking lineage and provenance. + to.parquet( + id="data_set_v1_saver", + path="data_set_v1.parquet", + dependencies=["data_set_v1"], + ), + to.pickle( + id="svm_model_saver", + path="svm_model.pkl", + dependencies=["svm_model"], + ), + to.pickle( + id="lr_model_saver", + path="lr_model.pkl", + dependencies=["lr_model"], + ), + additional_vars=["best_model"], + inputs=inputs, + ) + print(metadata) # metadata from the materialized artifacts + print(result) # contains result of the best model + + +if __name__ == "__main__": + # import logging + # logging.basicConfig(level=logging.DEBUG) + run() diff --git a/ui/frontend/src/components/dashboard/Project/ProjectLogInstructions.tsx b/ui/frontend/src/components/dashboard/Project/ProjectLogInstructions.tsx index 045edf607..b5941a9d0 100644 --- a/ui/frontend/src/components/dashboard/Project/ProjectLogInstructions.tsx +++ b/ui/frontend/src/components/dashboard/Project/ProjectLogInstructions.tsx @@ -68,7 +68,7 @@ export const NotUsingHamiltonYet = (props: { const initCode = `\ git clone https://github.com/DAGWorks-Inc/hamilton.git cd hamilton/examples/hamilton_ui -# modify the project ID and username in run.py. +# provide the project ID and username for run.py. `; const pipCode = `pip install -r requirements.txt`; const runCode = `python run.py`;