intermediate ~90 min updated 2026-06-01
MLOps Pipeline Basics
Build a minimal but real MLOps workflow: train a scikit-learn classifier, track parameters and metrics with MLflow, register the best model, and serve it as a REST API locally.
Objective
Train and compare scikit-learn models with MLflow tracking, promote the best run to the Model Registry, and serve it for real-time predictions. This is the core experiment-to-production loop every MLOps stack automates.
Prerequisites
- Python 3.10 or newer
- pip and the venv module
- Basic Python and ML familiarity (train/test split, accuracy)
- About 2 GB free disk for the virtualenv and model environments
Architecture
A training script runs experiments against the Iris dataset, logging parameters, metrics, and the serialized model to a local MLflow tracking server backed by SQLite. The best run is registered in the Model Registry under a version and alias. mlflow models serve then loads the registered model behind a REST scoring endpoint.
train.py (scikit-learn)
| log params/metrics/model
v
+----------------------+ register +-------------------+
| MLflow Tracking | ------------------> | Model Registry |
| server :5000 | | iris-clf@champion |
| backend: sqlite | +---------+---------+
| artifacts: ./mlruns | | load
+----------+-----------+ v
^ mlflow models serve :5001
UI in browser POST /invocations -> prediction
Steps
1. Set up the environment and tracking server
mkdir mlops-lab && cd mlops-lab
python3 -m venv .venv && source .venv/bin/activate
pip install mlflow scikit-learn pandas
mlflow server \
--backend-store-uri sqlite:///mlflow.db \
--default-artifact-root ./mlruns \
--host 127.0.0.1 --port 5000 &
sleep 5
Open http://127.0.0.1:5000 to confirm the UI loads.
2. Write the training script
# train.py
import sys
import mlflow
import mlflow.sklearn
from sklearn.datasets import load_iris
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, f1_score
from sklearn.model_selection import train_test_split
mlflow.set_tracking_uri("http://127.0.0.1:5000")
mlflow.set_experiment("iris-classification")
X, y = load_iris(return_X_y=True, as_frame=True)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)
model_name = sys.argv[1] if len(sys.argv) > 1 else "rf"
if model_name == "rf":
n_estimators = int(sys.argv[2]) if len(sys.argv) > 2 else 100
model = RandomForestClassifier(n_estimators=n_estimators, random_state=42)
params = {"model": "random_forest", "n_estimators": n_estimators}
else:
model = LogisticRegression(max_iter=500)
params = {"model": "logistic_regression", "max_iter": 500}
with mlflow.start_run():
mlflow.log_params(params)
model.fit(X_train, y_train)
preds = model.predict(X_test)
acc = accuracy_score(y_test, preds)
f1 = f1_score(y_test, preds, average="macro")
mlflow.log_metric("accuracy", acc)
mlflow.log_metric("f1_macro", f1)
mlflow.sklearn.log_model(model, name="model",
input_example=X_train.head(2))
print(f"{params['model']}: accuracy={acc:.4f} f1={f1:.4f}")
3. Run several experiments
python train.py logreg
python train.py rf 50
python train.py rf 200
Compare runs in the UI: experiment iris-classification, sort by accuracy.
4. Register the best model
# register.py
import mlflow
from mlflow.tracking import MlflowClient
mlflow.set_tracking_uri("http://127.0.0.1:5000")
client = MlflowClient()
exp = client.get_experiment_by_name("iris-classification")
best = client.search_runs(
[exp.experiment_id], order_by=["metrics.accuracy DESC"], max_results=1
)[0]
print("Best run:", best.info.run_id, "accuracy:", best.data.metrics["accuracy"])
mv = mlflow.register_model(f"runs:/{best.info.run_id}/model", "iris-clf")
client.set_registered_model_alias("iris-clf", "champion", mv.version)
print(f"Registered iris-clf version {mv.version} as @champion")
python register.py
5. Serve the registered model
export MLFLOW_TRACKING_URI=http://127.0.0.1:5000
mlflow models serve -m "models:/iris-clf@champion" \
--port 5001 --env-manager local &
sleep 10
6. Score live predictions
curl -s -X POST http://127.0.0.1:5001/invocations \
-H "Content-Type: application/json" \
-d '{
"dataframe_split": {
"columns": ["sepal length (cm)","sepal width (cm)","petal length (cm)","petal width (cm)"],
"data": [[5.1, 3.5, 1.4, 0.2], [6.7, 3.0, 5.2, 2.3]]
}
}'
Expected output
$ python train.py rf 200
random_forest: accuracy=0.9111 f1=0.9106
$ python register.py
Best run: 4f3a2b1c0d9e8f7a accuracy: 0.9333
Registered iris-clf version 1 as @champion
$ curl -s -X POST http://127.0.0.1:5001/invocations ...
{"predictions": [0, 2]}
Class 0 is setosa and class 2 is virginica — both correct for those measurements.
Troubleshooting
Connection refusedfrom train.py: the tracking server is not up. Check the background job (jobs), and re-run themlflow servercommand; SQLite locks can occur if a previous server is still running.RESOURCE_ALREADY_EXISTSwhen registering: the model name exists from a previous attempt — that is fine;register_modelcreates a new version automatically. Delete old versions in the UI if you want a clean slate.mlflow models servefails resolving the environment: omit env recreation with--env-manager local(as shown) so it uses your active virtualenv.Invalid input400 from /invocations: the JSON must use thedataframe_split(orinstances) key and column names matching training. Copy the request body exactly.- UI shows no experiment: train.py used a different tracking URI. Ensure
mlflow.set_tracking_uri("http://127.0.0.1:5000")matches the server port.
Cleanup
kill %1 %2 2>/dev/null || true # stop server and model endpoint
deactivate
cd .. && rm -rf mlops-lab