Model Serving Platform - Use Case: Predicting Wine Quality

This tutorial presents a use case of Model Serving Platform: Predicting Wine Quality.

Wine Quality is one of the most used datasets in Machine Learning to understand a wide range of regression models. The goal is to model wine quality based on physicochemical tests.

Requirements: - Databricks (it is a core service in Sidra) - Python notebook in Databricks

Register Model

This first step is just to register a new model by means of the Model Serving API. Due to recent release of Sidra Python SDK, this request can be executed in a Python script like a Databricks notebook.

First, we need to authenticate against Sidra API (the code to get a new token will be included in future releases of Sidra Python SDK)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from pysidra.api.auth.authentication import Authentication
from pysidra.api.client import Client

from pysidra.api.models.modelserving import Model

class Credentials:
  def __init__(self):
    self.auth_url = dbutils.secrets.get(
        scope="api", key="auth_url"
    ).lower()  # Authority
    self.client_id = dbutils.secrets.get(scope="api", key="client_id")  # ClientId
    self.scope = dbutils.secrets.get(scope="api", key="scope")  # Scope
    self.client_secret = dbutils.secrets.get(
        scope="api", key="client_secret"
    )  # ClientSecret
    self.api_url = dbutils.secrets.get(scope="api", key="api_url")


def get_token(credentials, json_error_sleep_t=30, json_error_retry_times=10):
  authenticated = False
  token = None
  while not authenticated and json_error_retry_times > 0:
    try:
      token = Authentication(
        base_url=credentials.auth_url,
        scope=credentials.scope,
        client_id=credentials.client_id,
        client_secret=credentials.client_secret,
      ).get_token()
      authenticated = True
    except json.JSONDecodeError as json_error:
      print(
      f"Error while authenticating against Sidra API. Trying in {json_error_sleep_t} seconds"
      )
      sleep(json_error_sleep_t)
      json_error_retry_times -= 1

  return token


def get_pysidra_client():
  credentials = Credentials()
  return Client(credentials.api_url, get_token(credentials))

Then, we can create a new Model, which is just a way to group experiments into the same problem to solve:

1
2
3
model = Model(name="wineClassifier", description="Example of model registered in Sidra ML platform")

model = pysidra_client.ModelServing.Model.create(model)

Track a new experiment

In the notebook, import required packages. In this example, ElasticNet approach is used to train the model:

1
2
3
4
5
6
7
8
    import pandas as pd
    import numpy as np
    from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
    from sklearn.model_selection import train_test_split
    from sklearn.linear_model import ElasticNet

    import mlflow
    import mlflow.sklearn

Next step consist on preparing data to train and evaluate the model. It is important to separate features from target column. To perform that, train_test_split method from Scikit-learn package is used:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
csv_url = 'http://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-red.csv'
data = pd.read_csv(csv_url, sep=';')

# Split the data into training and test sets. (0.75, 0.25) split.
train, test = train_test_split(data)

# The predicted column is "quality" which is a scalar from [3, 9]
train_x = train.drop(["quality"], axis=1)
test_x = test.drop(["quality"], axis=1)
train_y = train[["quality"]]
test_y = test[["quality"]]

Before training the model, it is necessary to configure the experiment in which MLflow will track all the information. In our case, below ML_Demo folder with the name of experiment:

1
mlflow.set_experiment('/ML_Demo/experiment')

Finally, it is necessary to create a new MLflow run to track parameters, metrics and the model. Model parameters, alpha and l1_ratio, are both set to 0.5 value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
alpha=0.5
l1_ratio=0.5

with mlflow.start_run():
    # Execute ElasticNet
    lr = ElasticNet(alpha=alpha, l1_ratio=l1_ratio, random_state=42)
    lr.fit(train_x, train_y)

    # Evaluate Metrics
    predicted_qualities = lr.predict(test_x)
    (rmse, mae, r2) = eval_metrics(test_y, predicted_qualities)

    # Print out metrics
    print("Elasticnet model (alpha=%f, l1_ratio=%f):" % (alpha, l1_ratio))
    print("  RMSE: %s" % rmse)
    print("  MAE: %s" % mae)
    print("  R2: %s" % r2)

    # Log parameter, metrics, and model to MLflow
    mlflow.log_param("alpha", alpha)
    mlflow.log_param("l1_ratio", l1_ratio)
    mlflow.log_metric("rmse", rmse)
    mlflow.log_metric("r2", r2)
    mlflow.log_metric("mae", mae)

    mlflow.sklearn.log_model(lr, "model")

After executing previous piece of code, a new experiment and run appears in MLflow UI:

ci-cd-ml-model

Then, it is necessary to access to that run to copy the run id, which appears both in url and upper section of the run's details.

ci-cd-ml-model

Create a ModelVersion and Docker image from MLflow experiment

Now it is time to create a ModelVersion to track the experiment into our platform.

It can be performed by means of two ways:

  1. Create a ModelVersion with POST request and create Docker image from that ModelVersion
  2. Create Docker image from MLflow run

It is obvious that the second choice is simpler, even though our platform allows going to the first one.

First we set the required information about the experiment:

1
2
3
4
5
6
client = MlflowClient()

run_id = 'd2c711431a504b018add3afb13c48117'
run = client.get_run(run_id)

experiment = client.get_experiment_by_name("/ML_example/experiment")

Finally, we can create the Docker image:

1
2
3
4
5
6
7
# It's mandatory to provide a Datalake in which everything is executed. By default it is the first one, but it can change in a deployment of multiple DSU units.
datalake = pysidra_client.Datacatalog.Datalake.get_list().items[0]

image_request = CreateImageRequest(runId=run_id, idModel=model.id, imageName="wclass")

# Another option is calling create_image_async but it is necessary to use job_status method to know whether the job is finished or not
modelversion = pysidra_client.ModelServing.ModelVersion.create_image(datalake['id'], image_request)

Deploy model

At this point, it is time to deploy our model. There are two choices: Azure Container Instances (ACI) and Azure Kubernetes Services (AKS). For the sake of simplicity, we go ahead with ACI deployment.

This is the easiest step in Model Serving Platform:

1
2
# Another option is calling deploy_async but it is necessary to use job_status method to know whether the job is finished or not
modelversion = pysidra_client.ModelServing.ModelVersion.deploy(datalake['id'], DeployRequest(modelVersionId = modelversion.id)) 

Now, it is possible to query our model by means of recently created web service.

Let's define a method to query the endpoint:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import requests
import json

def query_endpoint_example(scoring_uri, inputs, service_key=None):
  headers = {
    "Content-Type": "application/json",
  }
  if service_key is not None:
    headers["Authorization"] = "Bearer {service_key}".format(service_key=service_key)

  print("Sending batch prediction request with inputs: {}".format(inputs))
  response = requests.post(scoring_uri, data=json.dumps(inputs), headers=headers)
  preds = json.loads(response.text)
  print("Received response: {}".format(preds))
  return preds

And let's prepare data to request model:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Import various libraries including sklearn, mlflow, numpy, pandas
from sklearn import datasets
import numpy as np
import pandas as pd

csv_url = 'http://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-red.csv'
try:
  data = pd.read_csv(csv_url, sep=';')
except Exception as e:
  logger.exception("Unable to download training & test CSV, check your internet connection. Error: %s", e)

sample = data.drop(["quality"],axis=1).iloc[[0]]

query_input = sample.to_json(orient='split')
query_input = eval(query_input)
query_input.pop('index', None)

Finally, execute the request:

1
prod_prediction1 = query_endpoint_example(scoring_uri=modelversion.endPoint, inputs=query_input)

Undeploy model

In a normal scenario, a model can be undeployed if it is not used anymore or a new and improved version is deployed.

To undeploy a model, it is as simple as:

1
pysidra_client.ModelServing.ModelVersion.undeploy(modelversion, datalake["id"])

Delete ModelVersion

In addiction to the previous step, it is also possible to delete a Model or a ModelVersion. If a Model is deleted, all the related ModelVersion are also deleted. If a ModelVersion is deleted, all the Docker images and deployments are deleted. Besides, it is possible to remove MLflow experiments and runs, but this is an optional behaviour of this platform.

For example, to remove our ModelVersion without removing MLflow experiment or run:

1
2
3
4
5
NOTHING = 0
RUN = 1
ALL = 2

pysidra_client.ModelServing.ModelVersion.delete(modelversion, datalake["id"], deleteMode=NOTHING)

And finally, remove Model:

1
2
# We can ignore deleteMode parameter since its default value is 0
pysidra_client.ModelServing.Model.delete(model, datalake["id"])