Deploy an MLflow PyFunc
model with Model Serving
In this notebook, learn how to deploy a custom MLflow PyFunc model to a serving endpoint. MLflow pyfunc offers greater flexibility and customization to your deployment. You can run any custom model, add preprocessing or post-processing logic, or execute any arbitrary Python code. While using the MLflow built-in flavor is recommended for optimal performance, you can use MLflow PyFunc models where more customization is required.
Install and import libraries
[13]:
%pip install --upgrade mlflow scikit-learn -q
213.32s - pydevd: Sending message related to process being replaced timed-out after 5 seconds
[2]:
import json
import warnings
import numpy as np
import pandas as pd
import requests
from sklearn.ensemble import RandomForestRegressor
import mlflow
from mlflow.models import infer_signature
warnings.filterwarnings("ignore")
[3]:
DOW_MODEL_NAME_PREFIX = "DOW_model_"
MME_MODEL_NAME = "MME_DOW_model"
1 - Create Some Sample Models
1.1 - Create Dummy Data
[4]:
def create_weekly_dataset(n_dates, n_observations_per_date):
rng = pd.date_range(start="today", periods=n_dates, freq="D")
df = pd.DataFrame(
np.random.randn(n_dates * n_observations_per_date, 4),
columns=["x1", "x2", "x3", "y"],
index=np.tile(rng, n_observations_per_date),
)
df["dow"] = df.index.dayofweek
return df
df = create_weekly_dataset(n_dates=30, n_observations_per_date=500)
print(df.shape)
df.head()
(15000, 5)
[4]:
x1 | x2 | x3 | y | dow | |
---|---|---|---|---|---|
2024-01-26 18:30:42.810981 | -1.137854 | 0.165915 | 0.711107 | 0.046467 | 4 |
2024-01-27 18:30:42.810981 | 0.475331 | -0.749121 | 0.318395 | 0.520535 | 5 |
2024-01-28 18:30:42.810981 | 2.525948 | 1.019708 | 0.038251 | -0.270675 | 6 |
2024-01-29 18:30:42.810981 | 1.113931 | 0.376434 | -1.464181 | -0.069208 | 0 |
2024-01-30 18:30:42.810981 | -0.304569 | 1.389245 | -1.152598 | -1.137589 | 1 |
1.2 - Train Models for Each Day of the Week
[5]:
for dow in df["dow"].unique():
# Create dataset corresponding to a single day of the week
X = df.loc[df["dow"] == dow]
X.pop("dow") # Remove DOW as a predictor column
y = X.pop("y")
# Fit our DOW model
model = RandomForestRegressor().fit(X, y)
# Infer signature of the model
signature = infer_signature(X, model.predict(X))
with mlflow.start_run():
model_path = f"model_{dow}"
# Log and register our DOW model with signature
mlflow.sklearn.log_model(
model,
model_path,
signature=signature,
registered_model_name=f"{DOW_MODEL_NAME_PREFIX}{dow}",
)
mlflow.set_tag("dow", dow)
Successfully registered model 'DOW_model_4'.
Created version '1' of model 'DOW_model_4'.
Successfully registered model 'DOW_model_5'.
Created version '1' of model 'DOW_model_5'.
Successfully registered model 'DOW_model_6'.
Created version '1' of model 'DOW_model_6'.
Successfully registered model 'DOW_model_0'.
Created version '1' of model 'DOW_model_0'.
Successfully registered model 'DOW_model_1'.
Created version '1' of model 'DOW_model_1'.
Successfully registered model 'DOW_model_2'.
Created version '1' of model 'DOW_model_2'.
Successfully registered model 'DOW_model_3'.
Created version '1' of model 'DOW_model_3'.
1.3 - Test inference on our DOW models
[6]:
# Load Tuesday's model
tuesday_dow = 1
model_name = f"{DOW_MODEL_NAME_PREFIX}{tuesday_dow}"
model_uri = f"models:/{model_name}/latest"
model = mlflow.sklearn.load_model(model_uri)
# Perform inference using our training data for Tuesday
predictor_columns = [column for column in df.columns if column not in {"y", "dow"}]
head_of_training_data = df.loc[df["dow"] == tuesday_dow, predictor_columns].head()
tuesday_fitted_values = model.predict(head_of_training_data)
print(tuesday_fitted_values)
[-0.8571552 0.61833952 0.61625155 0.28999143 0.49778144]
2 - Create an MME Custom PyFunc Model
2.1 - Create a Child Implementation of mlflow.pyfunc.PythonModel
[7]:
class DOWModel(mlflow.pyfunc.PythonModel):
def __init__(self, model_uris):
self.model_uris = model_uris
self.models = {}
@staticmethod
def _model_uri_to_dow(model_uri: str) -> int:
return int(model_uri.split("/")[-2].split("_")[-1])
def load_context(self, context):
self.models = {
self._model_uri_to_dow(model_uri): mlflow.sklearn.load_model(model_uri)
for model_uri in self.model_uris
}
def predict(self, context, model_input, params):
# Parse the dow parameter
dow = params.get("dow")
if dow is None:
raise ValueError("DOW param is not passed.")
# Get the model associated with the dow parameter
model = self.models.get(dow)
if model is None:
raise ValueError(f"Model {dow} version was not found: {self.models.keys()}.")
# Perform inference
return model.predict(model_input)
2.2 - Test our Implementation
[8]:
head_of_training_data
[8]:
x1 | x2 | x3 | |
---|---|---|---|
2024-01-30 18:30:42.810981 | -0.304569 | 1.389245 | -1.152598 |
2024-02-06 18:30:42.810981 | 0.521323 | 0.814452 | 0.115571 |
2024-02-13 18:30:42.810981 | 0.229761 | -1.936210 | 0.139201 |
2024-02-20 18:30:42.810981 | -0.865488 | 1.024857 | -0.857649 |
2024-01-30 18:30:42.810981 | -1.454631 | 0.462055 | 0.703858 |
[9]:
# Instantiate our DOW MME
model_uris = [f"models:/{DOW_MODEL_NAME_PREFIX}{i}/latest" for i in df["dow"].unique()]
dow_model = DOWModel(model_uris)
dow_model.load_context(None)
print("Model URIs:")
print(model_uris)
# Perform inference using our training data for Tuesday
params = {"dow": 1}
mme_tuesday_fitted_values = dow_model.predict(None, head_of_training_data, params=params)
assert all(tuesday_fitted_values == mme_tuesday_fitted_values)
print("\nTuesday fitted values:")
print(mme_tuesday_fitted_values)
Model URIs:
['models:/DOW_model_4/latest', 'models:/DOW_model_5/latest', 'models:/DOW_model_6/latest', 'models:/DOW_model_0/latest', 'models:/DOW_model_1/latest', 'models:/DOW_model_2/latest', 'models:/DOW_model_3/latest']
Tuesday fitted values:
[-0.8571552 0.61833952 0.61625155 0.28999143 0.49778144]
2.3 - Register our Custom PyFunc Model
[10]:
with mlflow.start_run():
# Instantiate the custom pyfunc model
model = DOWModel(model_uris)
model.load_context(None)
model_path = "MME_model_path"
signature = infer_signature(
model_input=head_of_training_data,
model_output=tuesday_fitted_values,
params=params,
)
print(signature)
# Log the model to the experiment
mlflow.pyfunc.log_model(
model_path,
python_model=model,
signature=signature,
pip_requirements=["scikit-learn=1.3.2"],
registered_model_name=MME_MODEL_NAME, # also register the model for easy access
)
# Set some relevant information about our model
# (Assuming model has a property 'models' that can be counted)
mlflow.log_param("num_models", len(model.models))
inputs:
['x1': double (required), 'x2': double (required), 'x3': double (required)]
outputs:
[Tensor('float64', (-1,))]
params:
['dow': long (default: 1)]
Successfully registered model 'MME_DOW_model'.
Created version '1' of model 'MME_DOW_model'.
3 - Serve our Model
To test our endpoint, let’s serve our model on our local machine. 1. Open a new shell window in the root containing mlruns
directory e.g. the same directory you ran this notebook. 2. Ensure mlflow is installed: pip install --upgrade mlflow scikit-learn
3. Run the bash command printed below.
[11]:
PORT = 1234
print(
f"""Run the below command in a new window. You must be in the same repo as your mlruns directory and have mlflow installed...
mlflow models serve -m "models:/{MME_MODEL_NAME}/latest" --env-manager local -p {PORT}"""
)
Run the below command in a new window. You must be in the same repo as your mlruns directory and have mlflow installed...
mlflow models serve -m "models:/MME_DOW_model/latest" --env-manager local -p 1234
4 - Query our Served Model
[12]:
def score_model(pdf, params):
headers = {"Content-Type": "application/json"}
url = f"http://127.0.0.1:{PORT}/invocations"
ds_dict = {"dataframe_split": pdf, "params": params}
data_json = json.dumps(ds_dict, allow_nan=True)
response = requests.request(method="POST", headers=headers, url=url, data=data_json)
response.raise_for_status()
return response.json()
print("Inference on dow model 1 (Tuesday):")
inference_df = head_of_training_data.reset_index(drop=True).to_dict(orient="split")
print(score_model(inference_df, params={"dow": 1}))
Inference on dow model 1 (Tuesday):
{'predictions': [-0.8571551951905747, 0.618339524354309, 0.6162515496343108, 0.2899914313294642, 0.4977814353066934]}
[ ]: