diff --git a/classification/README.md b/binary-classification/README.md similarity index 100% rename from classification/README.md rename to binary-classification/README.md diff --git a/classification/data/winequality-red.csv b/binary-classification/data/winequality-red.csv similarity index 100% rename from classification/data/winequality-red.csv rename to binary-classification/data/winequality-red.csv diff --git a/classification/data/winequality-white.csv b/binary-classification/data/winequality-white.csv similarity index 100% rename from classification/data/winequality-white.csv rename to binary-classification/data/winequality-white.csv diff --git a/classification/notebooks/databricks.py b/binary-classification/notebooks/databricks.py similarity index 100% rename from classification/notebooks/databricks.py rename to binary-classification/notebooks/databricks.py diff --git a/classification/notebooks/jupyter.ipynb b/binary-classification/notebooks/jupyter.ipynb similarity index 100% rename from classification/notebooks/jupyter.ipynb rename to binary-classification/notebooks/jupyter.ipynb diff --git a/classification/profiles/databricks.yaml b/binary-classification/profiles/databricks.yaml similarity index 100% rename from classification/profiles/databricks.yaml rename to binary-classification/profiles/databricks.yaml diff --git a/classification/profiles/local.yaml b/binary-classification/profiles/local.yaml similarity index 100% rename from classification/profiles/local.yaml rename to binary-classification/profiles/local.yaml diff --git a/classification/recipe.yaml b/binary-classification/recipe.yaml similarity index 100% rename from classification/recipe.yaml rename to binary-classification/recipe.yaml diff --git a/classification/steps/custom_metrics.py b/binary-classification/steps/custom_metrics.py similarity index 100% rename from classification/steps/custom_metrics.py rename to binary-classification/steps/custom_metrics.py diff --git a/classification/steps/ingest.py b/binary-classification/steps/ingest.py similarity index 100% rename from classification/steps/ingest.py rename to binary-classification/steps/ingest.py diff --git a/classification/steps/split.py b/binary-classification/steps/split.py similarity index 100% rename from classification/steps/split.py rename to binary-classification/steps/split.py diff --git a/classification/steps/train.py b/binary-classification/steps/train.py similarity index 100% rename from classification/steps/train.py rename to binary-classification/steps/train.py diff --git a/classification/steps/transform.py b/binary-classification/steps/transform.py similarity index 100% rename from classification/steps/transform.py rename to binary-classification/steps/transform.py diff --git a/classification/tests/__init__.py b/binary-classification/tests/__init__.py similarity index 100% rename from classification/tests/__init__.py rename to binary-classification/tests/__init__.py diff --git a/classification/tests/ingest_test.py b/binary-classification/tests/ingest_test.py similarity index 100% rename from classification/tests/ingest_test.py rename to binary-classification/tests/ingest_test.py diff --git a/classification/tests/split_test.py b/binary-classification/tests/split_test.py similarity index 100% rename from classification/tests/split_test.py rename to binary-classification/tests/split_test.py diff --git a/classification/tests/test_sample.parquet b/binary-classification/tests/test_sample.parquet similarity index 100% rename from classification/tests/test_sample.parquet rename to binary-classification/tests/test_sample.parquet diff --git a/classification/tests/train_test.py b/binary-classification/tests/train_test.py similarity index 100% rename from classification/tests/train_test.py rename to binary-classification/tests/train_test.py diff --git a/classification/tests/transform_test.py b/binary-classification/tests/transform_test.py similarity index 100% rename from classification/tests/transform_test.py rename to binary-classification/tests/transform_test.py diff --git a/multi-class-classification/README.md b/multi-class-classification/README.md new file mode 100644 index 0000000..093616d --- /dev/null +++ b/multi-class-classification/README.md @@ -0,0 +1,26 @@ +# Binary classification: Is this bottle of wine red or white? +This is the root directory for an example project for the +[MLflow Classification Recipe](https://mlflow.org/docs/latest/recipes.html#classification-recipe). +Follow the instructions [here](../README.md) to set up your environment first, +then use this directory to create a classifier and evaluate its performance, +all out of box! + + +In this [notebook](notebooks/jupyter.ipynb) ([the Databricks version](notebooks/databricks.py)), +we show how to build and evaluate a very simple classifier step by step, +following the best practices of machine learning engineering. +By the end of this example, +you will learn how to use MLflow Recipes to +- Ingest the raw source data. +- Splits the dataset into training/validation/test. +- Create an identity transformer and transform the dataset. +- Train a linear model (classifier) to tell if a bottle of wine is red. +- Evaluate the trained model, and improve it by iterating through the `transform` and `train` steps. +- Register the model for production inference. + +All of these can be done with Jupyter notebook or on the Databricks environment. +Finally, challenge yourself to build a better model. Try the following: +- Find a better data source with more training data and more raw feature columns. +- Clean the dataset to make it less noisy. +- Find better feature transformations. +- Fine tune the hyperparameters of the model. diff --git a/multi-class-classification/data/iris.csv b/multi-class-classification/data/iris.csv new file mode 100644 index 0000000..5586785 --- /dev/null +++ b/multi-class-classification/data/iris.csv @@ -0,0 +1,151 @@ +sepal length,sepal width,petal length,petal width,class +5.1,3.5,1.4,0.2,setosa +4.9,3,1.4,0.2,setosa +4.7,3.2,1.3,0.2,setosa +4.6,3.1,1.5,0.2,setosa +5,3.6,1.4,0.2,setosa +5.4,3.9,1.7,0.4,setosa +4.6,3.4,1.4,0.3,setosa +5,3.4,1.5,0.2,setosa +4.4,2.9,1.4,0.2,setosa +4.9,3.1,1.5,0.1,setosa +5.4,3.7,1.5,0.2,setosa +4.8,3.4,1.6,0.2,setosa +4.8,3,1.4,0.1,setosa +4.3,3,1.1,0.1,setosa +5.8,4,1.2,0.2,setosa +5.7,4.4,1.5,0.4,setosa +5.4,3.9,1.3,0.4,setosa +5.1,3.5,1.4,0.3,setosa +5.7,3.8,1.7,0.3,setosa +5.1,3.8,1.5,0.3,setosa +5.4,3.4,1.7,0.2,setosa +5.1,3.7,1.5,0.4,setosa +4.6,3.6,1,0.2,setosa +5.1,3.3,1.7,0.5,setosa +4.8,3.4,1.9,0.2,setosa +5,3,1.6,0.2,setosa +5,3.4,1.6,0.4,setosa +5.2,3.5,1.5,0.2,setosa +5.2,3.4,1.4,0.2,setosa +4.7,3.2,1.6,0.2,setosa +4.8,3.1,1.6,0.2,setosa +5.4,3.4,1.5,0.4,setosa +5.2,4.1,1.5,0.1,setosa +5.5,4.2,1.4,0.2,setosa +4.9,3.1,1.5,0.2,setosa +5,3.2,1.2,0.2,setosa +5.5,3.5,1.3,0.2,setosa +4.9,3.6,1.4,0.1,setosa +4.4,3,1.3,0.2,setosa +5.1,3.4,1.5,0.2,setosa +5,3.5,1.3,0.3,setosa +4.5,2.3,1.3,0.3,setosa +4.4,3.2,1.3,0.2,setosa +5,3.5,1.6,0.6,setosa +5.1,3.8,1.9,0.4,setosa +4.8,3,1.4,0.3,setosa +5.1,3.8,1.6,0.2,setosa +4.6,3.2,1.4,0.2,setosa +5.3,3.7,1.5,0.2,setosa +5,3.3,1.4,0.2,setosa +7,3.2,4.7,1.4,versicolor +6.4,3.2,4.5,1.5,versicolor +6.9,3.1,4.9,1.5,versicolor +5.5,2.3,4,1.3,versicolor +6.5,2.8,4.6,1.5,versicolor +5.7,2.8,4.5,1.3,versicolor +6.3,3.3,4.7,1.6,versicolor +4.9,2.4,3.3,1,versicolor +6.6,2.9,4.6,1.3,versicolor +5.2,2.7,3.9,1.4,versicolor +5,2,3.5,1,versicolor +5.9,3,4.2,1.5,versicolor +6,2.2,4,1,versicolor +6.1,2.9,4.7,1.4,versicolor +5.6,2.9,3.6,1.3,versicolor +6.7,3.1,4.4,1.4,versicolor +5.6,3,4.5,1.5,versicolor +5.8,2.7,4.1,1,versicolor +6.2,2.2,4.5,1.5,versicolor +5.6,2.5,3.9,1.1,versicolor +5.9,3.2,4.8,1.8,versicolor +6.1,2.8,4,1.3,versicolor +6.3,2.5,4.9,1.5,versicolor +6.1,2.8,4.7,1.2,versicolor +6.4,2.9,4.3,1.3,versicolor +6.6,3,4.4,1.4,versicolor +6.8,2.8,4.8,1.4,versicolor +6.7,3,5,1.7,versicolor +6,2.9,4.5,1.5,versicolor +5.7,2.6,3.5,1,versicolor +5.5,2.4,3.8,1.1,versicolor +5.5,2.4,3.7,1,versicolor +5.8,2.7,3.9,1.2,versicolor +6,2.7,5.1,1.6,versicolor +5.4,3,4.5,1.5,versicolor +6,3.4,4.5,1.6,versicolor +6.7,3.1,4.7,1.5,versicolor +6.3,2.3,4.4,1.3,versicolor +5.6,3,4.1,1.3,versicolor +5.5,2.5,4,1.3,versicolor +5.5,2.6,4.4,1.2,versicolor +6.1,3,4.6,1.4,versicolor +5.8,2.6,4,1.2,versicolor +5,2.3,3.3,1,versicolor +5.6,2.7,4.2,1.3,versicolor +5.7,3,4.2,1.2,versicolor +5.7,2.9,4.2,1.3,versicolor +6.2,2.9,4.3,1.3,versicolor +5.1,2.5,3,1.1,versicolor +5.7,2.8,4.1,1.3,versicolor +6.3,3.3,6,2.5,virginica +5.8,2.7,5.1,1.9,virginica +7.1,3,5.9,2.1,virginica +6.3,2.9,5.6,1.8,virginica +6.5,3,5.8,2.2,virginica +7.6,3,6.6,2.1,virginica +4.9,2.5,4.5,1.7,virginica +7.3,2.9,6.3,1.8,virginica +6.7,2.5,5.8,1.8,virginica +7.2,3.6,6.1,2.5,virginica +6.5,3.2,5.1,2,virginica +6.4,2.7,5.3,1.9,virginica +6.8,3,5.5,2.1,virginica +5.7,2.5,5,2,virginica +5.8,2.8,5.1,2.4,virginica +6.4,3.2,5.3,2.3,virginica +6.5,3,5.5,1.8,virginica +7.7,3.8,6.7,2.2,virginica +7.7,2.6,6.9,2.3,virginica +6,2.2,5,1.5,virginica +6.9,3.2,5.7,2.3,virginica +5.6,2.8,4.9,2,virginica +7.7,2.8,6.7,2,virginica +6.3,2.7,4.9,1.8,virginica +6.7,3.3,5.7,2.1,virginica +7.2,3.2,6,1.8,virginica +6.2,2.8,4.8,1.8,virginica +6.1,3,4.9,1.8,virginica +6.4,2.8,5.6,2.1,virginica +7.2,3,5.8,1.6,virginica +7.4,2.8,6.1,1.9,virginica +7.9,3.8,6.4,2,virginica +6.4,2.8,5.6,2.2,virginica +6.3,2.8,5.1,1.5,virginica +6.1,2.6,5.6,1.4,virginica +7.7,3,6.1,2.3,virginica +6.3,3.4,5.6,2.4,virginica +6.4,3.1,5.5,1.8,virginica +6,3,4.8,1.8,virginica +6.9,3.1,5.4,2.1,virginica +6.7,3.1,5.6,2.4,virginica +6.9,3.1,5.1,2.3,virginica +5.8,2.7,5.1,1.9,virginica +6.8,3.2,5.9,2.3,virginica +6.7,3.3,5.7,2.5,virginica +6.7,3,5.2,2.3,virginica +6.3,2.5,5,1.9,virginica +6.5,3,5.2,2,virginica +6.2,3.4,5.4,2.3,virginica +5.9,3,5.1,1.8,virginica diff --git a/multi-class-classification/notebooks/databricks.py b/multi-class-classification/notebooks/databricks.py new file mode 100644 index 0000000..deb9db4 --- /dev/null +++ b/multi-class-classification/notebooks/databricks.py @@ -0,0 +1,99 @@ +# Databricks notebook source +# MAGIC %md +# MAGIC # MLflow Classification Recipe Databricks Notebook +# MAGIC This notebook runs the MLflow Classification Recipe on Databricks and inspects its results. +# MAGIC +# MAGIC For more information about the MLflow Classification Recipe, including usage examples, +# MAGIC see the [Classification Recipe overview documentation](https://mlflow.org/docs/latest/recipes.html#classification-recipe) +# MAGIC and the [Classification Recipe API documentation](https://mlflow.org/docs/latest/python_api/mlflow.recipes.html#module-mlflow.recipes.classification.v1.recipe). + +# COMMAND ---------- + +# MAGIC %pip install -r ../../requirements.txt +# MAGIC %pip install git+https://github.com/mshtelma/mlflow.git@multiclassclassification + +# COMMAND ---------- + +# MAGIC %md ### Start with a recipe: + +# COMMAND ---------- + +from mlflow.recipes import Recipe + +r = Recipe(profile="databricks") + +# COMMAND ---------- + +r.clean() + +# COMMAND ---------- + +# MAGIC %md ### Inspect recipe DAG: + +# COMMAND ---------- + +r.inspect() + +# COMMAND ---------- + +# MAGIC %md ### Ingest the dataset: + +# COMMAND ---------- + +r.run("ingest") + +# COMMAND ---------- + +# MAGIC %md ### Split the dataset into train, validation and test: + +# COMMAND ---------- + +r.run("split") + +# COMMAND ---------- + +r.run("transform") + +# COMMAND ---------- + +# MAGIC %md ### Train the model: + +# COMMAND ---------- + +r.run("train") + +# COMMAND ---------- + +# MAGIC %md ### Evaluate the model: + +# COMMAND ---------- + +r.run("evaluate") + +# COMMAND ---------- + +# MAGIC %md ### Register the model: + +# COMMAND ---------- + +r.run("register") + +# COMMAND ---------- + +r.inspect("train") + +# COMMAND ---------- + +training_data = r.get_artifact("training_data") +training_data.describe() + +# COMMAND ---------- + +training_data[:1].to_json() + +# COMMAND ---------- + +trained_model = r.get_artifact("model") +print(trained_model) + +# COMMAND ---------- diff --git a/multi-class-classification/notebooks/jupyter.ipynb b/multi-class-classification/notebooks/jupyter.ipynb new file mode 100644 index 0000000..ac8e0cc --- /dev/null +++ b/multi-class-classification/notebooks/jupyter.ipynb @@ -0,0 +1,192 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# MLflow Classification Recipe Notebook\n", + "\n", + "This notebook runs the MLflow Classification Recipe on Databricks and inspects its results. For more information about the MLflow Classification Recipe, including usage examples, see the [Classification Recipe overview documentation](https://mlflow.org/docs/latest/recipes.html#classification-recipe) the [Classification Recipe API documentation](https://mlflow.org/docs/latest/python_api/mlflow.recipes.html#module-mlflow.recipes.classification.v1.recipe)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from mlflow.recipes import Recipe\n", + "\n", + "r = Recipe(profile=\"local\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r.clean()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r.inspect()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r.run(\"ingest\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Perform some EDA on the ingested dataset.\n", + "import matplotlib.pyplot as plt\n", + "import seaborn as sns\n", + "\n", + "ingested_data = r.get_artifact(\"ingested_data\")\n", + "\n", + "dims = (3, 4)\n", + " \n", + "f, axes = plt.subplots(dims[0], dims[1], figsize=(25, 15))\n", + "axis_i, axis_j = 0, 0\n", + "for col in ingested_data.columns:\n", + " if col == \"is_red\":\n", + " continue # Box plots cannot be used on indicator variables\n", + " sns.boxplot(x=ingested_data[\"is_red\"], y=ingested_data[col], ax=axes[axis_i, axis_j])\n", + " axis_j += 1\n", + " if axis_j == dims[1]:\n", + " axis_i += 1\n", + " axis_j = 0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r.run(\"split\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r.run(\"transform\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r.run(\"train\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r.run(\"evaluate\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r.run(\"register\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r.inspect(\"train\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "training_data = r.get_artifact(\"training_data\")\n", + "training_data.describe()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "trained_model = r.get_artifact(\"model\")\n", + "print(trained_model)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "interpreter": { + "hash": "c462df992c775797bd5d542b280333286dbcc2ffa1f781b674f30e76741ca83d" + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.9.12" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/multi-class-classification/profiles/databricks.yaml b/multi-class-classification/profiles/databricks.yaml new file mode 100644 index 0000000..fcaa542 --- /dev/null +++ b/multi-class-classification/profiles/databricks.yaml @@ -0,0 +1,32 @@ +# [Recommended] Uncomment fields below to set an MLflow experiment to track the recipe execution. +# If unset, a default experiment based on runtime context will be created. +# experiment: + # name: "/Shared/sklearn_classification_experiment" + +# Set the registry server URI. This property is especially useful if you have a registry +# server that’s different from the tracking server. +# Profile could be created using https://github.com/databricks/databricks-cli#installation +model_registry: + # registry_uri: "databricks://PROFILE_NAME" + # FIXME::REQUIRED: Specifies the name of the Registered Model to use when registering a trained + # model to the MLflow Model Registry. + model_name: "iris_classifier" + +# Override the default train / validation / test dataset split ratios +SPLIT_RATIOS: [0.75, 0.125, 0.125] + +INGEST_CONFIG: + # For different options please read: https://github.com/mlflow/recipes-classification-template#ingest-step + using: csv + loader_method: load_file_as_dataframe + location: "./data/iris.csv" + +INGEST_SCORING_CONFIG: + # For different options please read: https://github.com/mlflow/recipes-classification-template#batch-scoring + using: csv + location: "./data/iris.csv" + +PREDICT_OUTPUT_CONFIG: + # For different options please read: https://github.com/mlflow/recipes-classification-template#predict-step + using: table + location: "iris_out" diff --git a/multi-class-classification/profiles/local.yaml b/multi-class-classification/profiles/local.yaml new file mode 100644 index 0000000..5af9855 --- /dev/null +++ b/multi-class-classification/profiles/local.yaml @@ -0,0 +1,31 @@ +experiment: + name: "sklearn_classification_experiment" + tracking_uri: "sqlite:///metadata/mlflow/mlruns.db" + artifact_location: "./metadata/mlflow/mlartifacts" + +# Set the registry server URI. This property is especially useful if you have a registry +# server that’s different from the tracking server. +model_registry: + # registry_uri: "sqlite:////tmp/registry.db" + # FIXME::REQUIRED: Specifies the name of the Registered Model to use when registering a trained + # model to the MLflow Model Registry. + model_name: "iris_classifier" + +# Override the default train / validation / test dataset split ratios +SPLIT_RATIOS: [0.80, 0.10, 0.10] + +INGEST_CONFIG: + # For different options please read: https://github.com/mlflow/recipes-classification-template#ingest-step + using: csv + location: "./data/iris.csv" + loader_method: load_file_as_dataframe + +INGEST_SCORING_CONFIG: + # For different options please read: https://github.com/mlflow/recipes-classification-template#batch-scoring + using: csv + location: "./data/iris.csv" + +PREDICT_OUTPUT_CONFIG: + # For different options please read: https://github.com/mlflow/recipes-classification-template#predict-step + using: parquet + location: "./data/iris_prediction_out.parquet" diff --git a/multi-class-classification/recipe.yaml b/multi-class-classification/recipe.yaml new file mode 100644 index 0000000..b249528 --- /dev/null +++ b/multi-class-classification/recipe.yaml @@ -0,0 +1,69 @@ +# `recipe.yaml` is the main configuration file for an MLflow Recipe. +# Required recipe parameters should be defined in this file with either concrete values or +# variables such as {{ INGEST_DATA_LOCATION }}. + +# Variables must be dereferenced in a profile YAML file, located under `profiles/`. +# See `profiles/local.yaml` for example usage. One may switch among profiles quickly by +# providing a profile name such as `local` in the Recipe object constructor: +# `p = Recipe(profile="local")` +# +# NOTE: YAML does not support tabs for indentation. Please use spaces and ensure that all YAML +# files are properly formatted. + +recipe: "classification/v1" +# Specifies the name of the column containing targets / labels for model training and evaluation +target_col: "class" +# Specifies the value of `target_col` which is considered the positive class: red wine in this case. +#positive_class: 1 +# Sets the primary metric to use to evaluate model performance. This primary metric is used +# to sort MLflow Runs corresponding to the recipe in the MLflow Tracking UI +primary_metric: "f1_score_macro" +steps: + ingest: {{INGEST_CONFIG}} + split: + # Train/validation/test split ratios + split_ratios: {{SPLIT_RATIOS|default([0.75, 0.125, 0.125])}} + # Specifies the method to use to perform additional cleaning on split datasets + # Note that arbitrary transformations should go into the transform step + # post_split_filter_method: create_dataset_filter + transform: + using: custom + # Specifies the method that defines the data transformations to apply during model inference + transformer_method: transformer_fn + train: + using: custom + # Specifies the method that defines the estimator type and parameters to use for model training + estimator_method: estimator_fn + tuning: + enabled: true + max_trials: 2 + sample_fraction: 0.5 + parameters: + alpha: + distribution: "uniform" + low: 0.0 + high: 0.01 + penalty: + values: ["l2", "l1"] + eta0: + distribution: "normal" + mu: 0.01 + sigma: 0.0001 + evaluate: + # Sets performance thresholds that a trained model must meet in order to be eligible for + # registration to the MLflow Model Registry + validation_criteria: + - metric: f1_score_macro + threshold: 0.6 + - metric: precision_score + threshold: 0.6 + - metric: recall_score + threshold: 0.6 + register: + # Indicates whether or not a model that fails to meet performance thresholds should still + # be registered to the MLflow Model Registry + allow_non_validated_model: false + ingest_scoring: {{INGEST_SCORING_CONFIG}} + predict: + output: {{PREDICT_OUTPUT_CONFIG}} + # model_uri: "models/model.pkl" diff --git a/multi-class-classification/steps/custom_metrics.py b/multi-class-classification/steps/custom_metrics.py new file mode 100644 index 0000000..f28511b --- /dev/null +++ b/multi-class-classification/steps/custom_metrics.py @@ -0,0 +1 @@ +# Created by MLflow Pipeliens diff --git a/multi-class-classification/steps/ingest.py b/multi-class-classification/steps/ingest.py new file mode 100644 index 0000000..a1e20be --- /dev/null +++ b/multi-class-classification/steps/ingest.py @@ -0,0 +1,36 @@ +""" +This module defines the following routines used by the 'ingest' step of the regression recipe: + +- ``load_file_as_dataframe``: Defines customizable logic for parsing dataset formats that are not + natively parsed by MLflow Recipes (i.e. formats other than Parquet, Delta, and Spark SQL). +""" + +import logging + +from pandas import DataFrame + +_logger = logging.getLogger(__name__) + + +def load_file_as_dataframe(file_path: str, file_format: str) -> DataFrame: + """ + Load content from the specified dataset file as a Pandas DataFrame. + + This method is used to load dataset types that are not natively managed by MLflow Recipes + (datasets that are not in Parquet, Delta Table, or Spark SQL Table format). This method is + called once for each file in the dataset, and MLflow Recipes automatically combines the + resulting DataFrames together. + + :param file_path: The path to the dataset file. + :param file_format: The file format string, such as "csv". + :return: A Pandas DataFrame representing the content of the specified file. + """ + + if file_format == "csv": + import pandas + + df = pandas.read_csv(file_path, sep=",") + df["class"] = df["class"].astype("category").cat.codes + return df + else: + raise NotImplementedError diff --git a/multi-class-classification/steps/split.py b/multi-class-classification/steps/split.py new file mode 100644 index 0000000..ce1e766 --- /dev/null +++ b/multi-class-classification/steps/split.py @@ -0,0 +1,19 @@ +""" +This module defines the following routines used by the 'split' step of the regression recipe: +- ``create_dataset_filter``: Defines customizable logic for filtering the training, validation, + and test datasets produced by the data splitting procedure. Note that arbitrary transformations + should go into the transform step. +""" + +from pandas import DataFrame, Series + + +def create_dataset_filter(dataset: DataFrame) -> Series(bool): + """ + Mark rows of the split datasets to be additionally filtered. This function will be called on + the training, validation, and test datasets. + :param dataset: The {train,validation,test} dataset produced by the data splitting procedure. + :return: A Series indicating whether each row should be filtered + """ + + return Series(True, index=dataset.index) diff --git a/multi-class-classification/steps/train.py b/multi-class-classification/steps/train.py new file mode 100644 index 0000000..7dcfe87 --- /dev/null +++ b/multi-class-classification/steps/train.py @@ -0,0 +1,20 @@ +""" +Show resolved +This module defines the following routines used by the 'train' step: +- ``estimator_fn``: Defines the customizable estimator type and parameters that are used + during training to produce a model recipe. +""" +from typing import Dict, Any + + +def estimator_fn(estimator_params: Dict[str, Any] = None): + """ + Returns an *unfitted* estimator that defines ``fit()`` and ``predict()`` methods. + The estimator's input and output signatures should be compatible with scikit-learn + estimators. + """ + from sklearn.linear_model import SGDClassifier + + if estimator_params is None: + estimator_params = {} + return SGDClassifier(random_state=42, **estimator_params) diff --git a/multi-class-classification/steps/transform.py b/multi-class-classification/steps/transform.py new file mode 100644 index 0000000..c7f215b --- /dev/null +++ b/multi-class-classification/steps/transform.py @@ -0,0 +1,16 @@ +""" +This module defines the following routines used by the 'transform' step of the regression recipe: + +- ``transformer_fn``: Defines customizable logic for transforming input data before it is passed + to the estimator during model inference. +""" + + +def transformer_fn(): + """ + Returns an *unfitted* transformer that defines ``fit()`` and ``transform()`` methods. + The transformer's input and output signatures should be compatible with scikit-learn + transformers. + """ + + return None diff --git a/multi-class-classification/tests/__init__.py b/multi-class-classification/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/multi-class-classification/tests/ingest_test.py b/multi-class-classification/tests/ingest_test.py new file mode 100644 index 0000000..0bc744d --- /dev/null +++ b/multi-class-classification/tests/ingest_test.py @@ -0,0 +1,10 @@ +import pytest +import os +import pandas as pd + + +@pytest.fixture +def sample_data(): + return pd.read_parquet( + os.path.join(os.path.dirname(__file__), "test_sample.parquet") + ) diff --git a/multi-class-classification/tests/split_test.py b/multi-class-classification/tests/split_test.py new file mode 100644 index 0000000..4e66411 --- /dev/null +++ b/multi-class-classification/tests/split_test.py @@ -0,0 +1,20 @@ +import pytest +import os +import pandas as pd +from pandas import Series +from steps.split import create_dataset_filter + + +@pytest.fixture +def sample_data(): + return pd.read_parquet( + os.path.join(os.path.dirname(__file__), "test_sample.parquet") + ) + + +def test_split_step_outcome(sample_data): + processed = create_dataset_filter(sample_data) + assert isinstance(processed, Series) + assert not processed.empty + assert sample_data.size == 60 + assert sample_data[processed].size == 60 diff --git a/multi-class-classification/tests/test_sample.parquet b/multi-class-classification/tests/test_sample.parquet new file mode 100644 index 0000000..692734d Binary files /dev/null and b/multi-class-classification/tests/test_sample.parquet differ diff --git a/multi-class-classification/tests/train_test.py b/multi-class-classification/tests/train_test.py new file mode 100644 index 0000000..e69de29 diff --git a/multi-class-classification/tests/transform_test.py b/multi-class-classification/tests/transform_test.py new file mode 100644 index 0000000..2c1f062 --- /dev/null +++ b/multi-class-classification/tests/transform_test.py @@ -0,0 +1,10 @@ +from steps.transform import transformer_fn + + +def test_tranform_fn_returns_object_with_correct_spec(): + # pylint: disable=assignment-from-none + transformer = transformer_fn() + # pylint: enable=assignment-from-none + if transformer: + assert callable(getattr(transformer, "fit", None)) + assert callable(getattr(transformer, "transform", None)) diff --git a/requirements.txt b/requirements.txt index 4ea66c6..ffb7600 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ mlflow>=2.0 -scikit-learn>=1.1 +scikit-learn==1.2.0 ipykernel>=6.12 ipython>=7.32 +flaml