Skip to content

Commit e2abbe7

Browse files
authored
Merge 6b86f08 into e79e43c
2 parents e79e43c + 6b86f08 commit e2abbe7

File tree

7 files changed

+382
-1
lines changed

7 files changed

+382
-1
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,13 @@ Then, run the script with the desired function as an argument:
188188
- video: [43. Example 1. Custom model (multimodel)](https://youtu.be/ttm--W1OBVU)
189189
- video: [44. Example 1. Custom model (multimodel) Part 2](https://youtu.be/yqvkXNADsYU)
190190

191+
### Local Model Serving Use cases.
192+
193+
In this section, we explore how MLflow enables local model serving. You'll learn how to launch a local MLflow server to deploy and serve ML models, making them accessible for real-time predictions via REST API endpoints. This approach is useful for testing, prototyping, and integrating models into local applications before moving to production environments.
194+
195+
#### Iris classifier.
196+
197+
191198

192199
## Contributing
193200

examples/iris_classifier/data.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from sklearn.datasets import load_iris
2+
from sklearn.model_selection import train_test_split
3+
from typing import Optional
4+
from typing import Tuple
5+
import pandas as pd
6+
7+
8+
def get_train_test_data(
9+
test_size: Optional[float] = 0.2, random_state: Optional[int] = 42
10+
) -> Tuple[pd.DataFrame, pd.DataFrame, pd.Series, pd.Series]:
11+
"""
12+
Load the iris dataset and split it into training and testing sets.
13+
The function returns the training and testing data as pandas DataFrames.
14+
15+
:param test_size: The proportion of the dataset to include in the test split (default is 0.2).
16+
:param random_state: Controls the shuffling applied to the data before applying the split (default is 42).
17+
:return: A tuple containing the training features (X_train), testing features (X_test),
18+
training labels (y_train), and testing labels (y_test).
19+
"""
20+
iris = load_iris(as_frame=True)
21+
X = iris.data
22+
y = iris.target
23+
X_train, X_test, y_train, y_test = train_test_split(
24+
X, y, test_size=test_size, random_state=random_state
25+
)
26+
return X_train, X_test, y_train, y_test
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import mlflow
2+
from examples.utils.decorators import mlflow_tracking_uri
3+
from examples.iris_classifier.data import get_train_test_data
4+
5+
6+
@mlflow_tracking_uri
7+
def main():
8+
"""
9+
Main function to run the batch inference process.
10+
"""
11+
# Load the model from the specified path
12+
_, x_test, _, _ = get_train_test_data()
13+
registered_model_name = "Iris_Classifier_Model"
14+
model_path = f"models:/{registered_model_name}@production"
15+
model = mlflow.sklearn.load_model(model_path)
16+
17+
# Perform inference on the test data
18+
predictions = model.predict(x_test)
19+
x_test["predictions"] = predictions
20+
print(x_test.head())
21+
print("Batch inference completed successfully.")
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from examples.iris_classifier.data import get_train_test_data
2+
import httpx
3+
import json
4+
import pandas as pd
5+
6+
7+
def get_predictions_from_response(response):
8+
"""
9+
Process the response from the REST API.
10+
11+
:param response: The response object from the HTTP request.
12+
:return: The JSON content of the response.
13+
"""
14+
if response.status_code == 200:
15+
json_response = response.json()
16+
predictions = json_response.get("predictions")
17+
if predictions is not None:
18+
return predictions
19+
else:
20+
raise Exception("No predictions found in the response.")
21+
else:
22+
raise Exception(f"Error: {response.status_code} - {response.text}")
23+
24+
25+
def get_payload(samples: int) -> dict:
26+
"""
27+
Get the payload for online inference.
28+
29+
:param samples: Number of samples to include in the payload.
30+
:return: Dictionary containing the payload for online inference.
31+
"""
32+
_, x_test, _, y_test = get_train_test_data()
33+
# Uncomment the following line to make the api call fail
34+
# x_test["sepal length (cm)"] = ["" for _ in range(len(x_test))]
35+
payload = {
36+
"dataframe_split": x_test.iloc[0:samples].to_dict(orient="split"),
37+
}
38+
return payload, y_test.iloc[0:samples]
39+
40+
41+
def main() -> None:
42+
"""
43+
Perform online inference using a REST API.
44+
45+
To deploy the model in the local server, run the following command:
46+
`poetry run mlflow models serve -m models:/Iris_Classifier_Model@production --no-conda`
47+
48+
"""
49+
payload, labels = get_payload(1)
50+
url = "http://127.0.0.1:5000/invocations"
51+
52+
print(payload)
53+
headers = {"Content-Type": "application/json"}
54+
response = httpx.post(url, data=json.dumps(payload), headers=headers)
55+
predictions = get_predictions_from_response(response)
56+
print(
57+
pd.DataFrame(
58+
{
59+
"predictions": predictions,
60+
"labels": labels,
61+
}
62+
)
63+
)

examples/iris_classifier/train.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from sklearn.ensemble import RandomForestClassifier
2+
from examples.iris_classifier.data import get_train_test_data
3+
from examples.utils.decorators import mlflow_tracking_uri
4+
from examples.utils.decorators import mlflow_client
5+
from examples.utils.decorators import mlflow_experiment
6+
from typing import Optional
7+
from typing import Dict
8+
from mlflow.models import infer_signature
9+
import mlflow
10+
11+
12+
def train(x_train, y_train, params: Optional[Dict[str, str]]) -> RandomForestClassifier:
13+
"""
14+
Train a Random Forest Classifier on the provided training data.
15+
The function returns the trained model.
16+
17+
:param x_train: The training features (input data).
18+
:param y_train: The training labels (target data).
19+
:return: The trained Random Forest Classifier model.
20+
"""
21+
clf = RandomForestClassifier(**params)
22+
clf.fit(x_train, y_train)
23+
return clf
24+
25+
26+
@mlflow_tracking_uri
27+
@mlflow_experiment(name="iris_classifier")
28+
@mlflow_client
29+
def main(**kwargs) -> None:
30+
# Example usage of the train function
31+
x_train, x_test, y_train, y_test = get_train_test_data()
32+
params = {"n_estimators": 1, "max_depth": 10}
33+
model = train(x_train, y_train, params)
34+
model_signature = infer_signature(x_train, y_train)
35+
36+
eval_data = x_test.copy()
37+
eval_data["target"] = y_test
38+
client = kwargs["mlflow_client"]
39+
registered_model_name = "Iris_Classifier_Model"
40+
with mlflow.start_run(run_name="training-rfc-model") as run:
41+
# log parameters.
42+
mlflow.log_params(model.get_params())
43+
44+
# log model
45+
mlflow.sklearn.log_model(
46+
sk_model=model,
47+
artifact_path="model",
48+
signature=model_signature,
49+
input_example=x_train.iloc[0:3],
50+
registered_model_name=registered_model_name,
51+
)
52+
53+
# set model version alias to "production"
54+
model_version = mlflow.search_model_versions(
55+
filter_string=f"name='{registered_model_name}'", max_results=1
56+
)[0]
57+
client.set_registered_model_alias(
58+
name=registered_model_name,
59+
version=model_version.version,
60+
alias="production",
61+
)
62+
63+
# model uri
64+
model_uri = f"runs:/{run.info.run_id}/model"
65+
mlflow.evaluate(
66+
model=model_uri,
67+
data=eval_data,
68+
model_type="classifier",
69+
targets="target",
70+
)
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"id": "7e44f382",
6+
"metadata": {},
7+
"source": [
8+
"# Local Model Serving with MLflow"
9+
]
10+
},
11+
{
12+
"cell_type": "code",
13+
"execution_count": 37,
14+
"id": "f7c4b8ed",
15+
"metadata": {},
16+
"outputs": [],
17+
"source": [
18+
"import httpx \n",
19+
"import json\n",
20+
"import mlflow\n",
21+
"from mlflow_for_ml_dev.src.utils.folder_operations import get_project_root\n",
22+
"\n",
23+
"# set mlflow tracking uri\n",
24+
"mlflow.set_tracking_uri(uri=(get_project_root() / 'mlruns').as_uri())"
25+
]
26+
},
27+
{
28+
"cell_type": "markdown",
29+
"id": "94833c27",
30+
"metadata": {},
31+
"source": [
32+
"```shell\n",
33+
"mlflow models serve --options\n",
34+
"```\n",
35+
"\n",
36+
"To run the code below make sure you deploy the model using.\n",
37+
"\n",
38+
"`poetry run mlflow models serve --model-uri models:/Iris_Classifier_Model@production --env-manager local`"
39+
]
40+
},
41+
{
42+
"cell_type": "markdown",
43+
"id": "358ba627",
44+
"metadata": {},
45+
"source": [
46+
"## Scoring Iris Classifier Model"
47+
]
48+
},
49+
{
50+
"cell_type": "code",
51+
"execution_count": 50,
52+
"id": "096a2fa0",
53+
"metadata": {},
54+
"outputs": [],
55+
"source": [
56+
"url = \"http://127.0.0.1:5001/invocations\"\n",
57+
"\n",
58+
"payload = {'dataframe_split':\n",
59+
" {\n",
60+
" 'columns': ['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)'], \n",
61+
" 'data': [[6.1, 2.8, 4.7, 1.2]]\n",
62+
" }\n",
63+
" } \n",
64+
"headers = {\"Content-Type\": \"application/json\"}\n",
65+
"response = httpx.post(url, data=json.dumps(payload), headers=headers)\n"
66+
]
67+
},
68+
{
69+
"cell_type": "code",
70+
"execution_count": 51,
71+
"id": "4cee092a",
72+
"metadata": {},
73+
"outputs": [
74+
{
75+
"data": {
76+
"text/plain": [
77+
"<Response [200 OK]>"
78+
]
79+
},
80+
"execution_count": 51,
81+
"metadata": {},
82+
"output_type": "execute_result"
83+
}
84+
],
85+
"source": [
86+
"response"
87+
]
88+
},
89+
{
90+
"cell_type": "markdown",
91+
"id": "2ce72d97",
92+
"metadata": {},
93+
"source": [
94+
"## Signature validation"
95+
]
96+
},
97+
{
98+
"cell_type": "code",
99+
"execution_count": 52,
100+
"id": "cba325a0",
101+
"metadata": {},
102+
"outputs": [],
103+
"source": [
104+
"url = \"http://127.0.0.1:5001/invocations\"\n",
105+
"\n",
106+
"payload = {'dataframe_split':\n",
107+
" {\n",
108+
" 'columns': ['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)'], \n",
109+
" 'data': [[6.1, 2.8, 4.7, \"string\"]] # invalid data type\n",
110+
" }\n",
111+
" } \n",
112+
"headers = {\"Content-Type\": \"application/json\"}\n",
113+
"response = httpx.post(url, data=json.dumps(payload), headers=headers)"
114+
]
115+
},
116+
{
117+
"cell_type": "code",
118+
"execution_count": 53,
119+
"id": "dadf245c",
120+
"metadata": {},
121+
"outputs": [
122+
{
123+
"data": {
124+
"text/plain": [
125+
"<Response [400 BAD REQUEST]>"
126+
]
127+
},
128+
"execution_count": 53,
129+
"metadata": {},
130+
"output_type": "execute_result"
131+
}
132+
],
133+
"source": [
134+
"response"
135+
]
136+
},
137+
{
138+
"cell_type": "code",
139+
"execution_count": 54,
140+
"id": "da6b0d77",
141+
"metadata": {},
142+
"outputs": [
143+
{
144+
"data": {
145+
"text/plain": [
146+
"{'error_code': 'BAD_REQUEST',\n",
147+
" 'message': 'Invalid input. Data is not compatible with model signature. Failed to convert column petal width (cm) to type \\'float64\\'. Error: \\'ValueError(\"could not convert string to float: \\'string\\'\")\\''}"
148+
]
149+
},
150+
"execution_count": 54,
151+
"metadata": {},
152+
"output_type": "execute_result"
153+
}
154+
],
155+
"source": [
156+
"response.json()"
157+
]
158+
},
159+
{
160+
"cell_type": "code",
161+
"execution_count": null,
162+
"id": "0b064f0c",
163+
"metadata": {},
164+
"outputs": [],
165+
"source": []
166+
}
167+
],
168+
"metadata": {
169+
"kernelspec": {
170+
"display_name": ".venv",
171+
"language": "python",
172+
"name": "python3"
173+
},
174+
"language_info": {
175+
"codemirror_mode": {
176+
"name": "ipython",
177+
"version": 3
178+
},
179+
"file_extension": ".py",
180+
"mimetype": "text/x-python",
181+
"name": "python",
182+
"nbconvert_exporter": "python",
183+
"pygments_lexer": "ipython3",
184+
"version": "3.11.9"
185+
}
186+
},
187+
"nbformat": 4,
188+
"nbformat_minor": 5
189+
}

0 commit comments

Comments
 (0)