Introduction to MLOps
Understand the ML lifecycle, set up experiment tracking with MLflow, and build your first reproducible ML pipeline.
The ML Lifecycle Problem
A data scientist trains a model in a notebook, emails the .pkl file to the engineering team, and deploys it. Six months later:
- The model accuracy has degraded but nobody knows
- Nobody can reproduce the training because the exact Python versions and data snapshot aren’t recorded
- A new team member wants to improve it but can’t tell which of the 40 experiments was the one that shipped
- The input data distribution has shifted but there are no alerts
MLOps solves all of these with tooling and process.
The ML Lifecycle
Data Collection → Feature Engineering → Experiment (train/evaluate)
↓ ↓
Data Versioning Experiment Tracking
↓ ↓
Feature Store Model Registry
↓
CI/CD for ML
↓
Model Serving
↓
Monitoring & Drift Detection
↓
Retraining Pipeline
Setting Up MLflow
pip install mlflow scikit-learn pandas
mlflow ui # start the tracking UI at http://localhost:5000
Experiment Tracking
import mlflow
import mlflow.sklearn
import numpy as np
import pandas as pd
from sklearn.datasets import load_wine
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import accuracy_score, f1_score, roc_auc_score
from sklearn.preprocessing import StandardScaler
# Set the tracking server (default: local ./mlruns directory)
mlflow.set_tracking_uri("http://localhost:5000") # or leave empty for local
mlflow.set_experiment("wine-classification")
X, y = load_wine(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train)
X_test_s = scaler.transform(X_test)
# Experiment 1: Random Forest
with mlflow.start_run(run_name="random-forest-baseline"):
# Log parameters — hyperparameters used for this run
n_estimators = 200
max_depth = 10
mlflow.log_param("n_estimators", n_estimators)
mlflow.log_param("max_depth", max_depth)
mlflow.log_param("model_type", "RandomForest")
# Train
model = RandomForestClassifier(n_estimators=n_estimators, max_depth=max_depth, random_state=42)
model.fit(X_train_s, y_train)
# Evaluate
y_pred = model.predict(X_test_s)
y_prob = model.predict_proba(X_test_s)
accuracy = accuracy_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred, average="macro")
cv_score = cross_val_score(model, X_train_s, y_train, cv=5).mean()
# Log metrics
mlflow.log_metric("accuracy", accuracy)
mlflow.log_metric("f1_macro", f1)
mlflow.log_metric("cv_accuracy", cv_score)
# Log the model — stores it as an artifact with schema
mlflow.sklearn.log_model(model, artifact_path="model",
registered_model_name="wine-classifier")
print(f"RF Accuracy: {accuracy:.4f}, F1: {f1:.4f}")
Comparing Experiments Programmatically
import mlflow
import pandas as pd
mlflow.set_tracking_uri("http://localhost:5000")
# Fetch all runs from an experiment
experiment = mlflow.get_experiment_by_name("wine-classification")
runs = mlflow.search_runs(
experiment_ids=[experiment.experiment_id],
order_by=["metrics.accuracy DESC"],
)
# runs is a DataFrame — easy to filter and compare
summary = runs[["run_id", "params.model_type", "params.n_estimators",
"metrics.accuracy", "metrics.f1_macro", "metrics.cv_accuracy"]].head(10)
print(summary.to_string(index=False))
# Get the best run
best_run = runs.iloc[0]
print(f"\nBest model: {best_run['params.model_type']}")
print(f"Accuracy: {best_run['metrics.accuracy']:.4f}")
print(f"Run ID: {best_run['run_id']}")
Model Registry
import mlflow
from mlflow.tracking import MlflowClient
client = MlflowClient()
# Register a model from a run
run_id = "your-run-id-here"
model_uri = f"runs:/{run_id}/model"
registered = mlflow.register_model(model_uri, "wine-classifier")
print(f"Model version: {registered.version}")
# Transition through stages: None → Staging → Production → Archived
client.transition_model_version_stage(
name="wine-classifier",
version=registered.version,
stage="Staging",
archive_existing_versions=False,
)
# Load the production model anywhere in your codebase
model = mlflow.sklearn.load_model("models:/wine-classifier/Production")
predictions = model.predict(X_test_s)
Reproducible Training Script
A production training script — not a notebook — is the foundation of MLOps.
import argparse
import logging
import mlflow
import mlflow.sklearn
import numpy as np
from sklearn.datasets import load_wine
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import accuracy_score, classification_report
from sklearn.preprocessing import StandardScaler
import joblib
import json
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def parse_args():
parser = argparse.ArgumentParser(description="Train wine classifier")
parser.add_argument("--n-estimators", type=int, default=200)
parser.add_argument("--learning-rate", type=float, default=0.05)
parser.add_argument("--max-depth", type=int, default=4)
parser.add_argument("--test-size", type=float, default=0.2)
parser.add_argument("--random-state", type=int, default=42)
return parser.parse_args()
def train(args):
mlflow.set_experiment("wine-classification")
with mlflow.start_run():
# Log all params
mlflow.log_params(vars(args))
# Load and split data
X, y = load_wine(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=args.test_size, random_state=args.random_state, stratify=y
)
logger.info(f"Train: {X_train.shape}, Test: {X_test.shape}")
# Preprocessing
scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train)
X_test_s = scaler.transform(X_test)
# Train
model = GradientBoostingClassifier(
n_estimators=args.n_estimators,
learning_rate=args.learning_rate,
max_depth=args.max_depth,
random_state=args.random_state,
)
model.fit(X_train_s, y_train)
# Evaluate
cv_scores = cross_val_score(model, X_train_s, y_train, cv=5)
y_pred = model.predict(X_test_s)
accuracy = accuracy_score(y_test, y_pred)
mlflow.log_metric("accuracy", accuracy)
mlflow.log_metric("cv_mean", cv_scores.mean())
mlflow.log_metric("cv_std", cv_scores.std())
# Log artifacts
report = classification_report(y_test, y_pred, output_dict=True)
mlflow.log_dict(report, "classification_report.json")
# Log model + preprocessor together
mlflow.sklearn.log_model(model, "model")
joblib.dump(scaler, "/tmp/scaler.pkl")
mlflow.log_artifact("/tmp/scaler.pkl", "preprocessing")
logger.info(f"Accuracy: {accuracy:.4f}, CV: {cv_scores.mean():.4f}")
if __name__ == "__main__":
train(parse_args())
Run it with different hyperparameters from the command line:
python train.py --n-estimators 100 --learning-rate 0.1 --max-depth 3
python train.py --n-estimators 500 --learning-rate 0.01 --max-depth 6 Frequently Asked Questions
What is MLOps and why does it matter?
MLOps (Machine Learning Operations) applies DevOps principles to machine learning. It solves the problems that arise when moving ML from a notebook to production: reproducibility, versioning, monitoring, and automated retraining. Without MLOps, models degrade silently, experiments are unreproducible, and deployments are manual and risky.
What is the difference between an experiment and a run in MLflow?
An experiment is a named container for related runs — like a project or hypothesis. A run is a single training execution within an experiment, containing parameters, metrics, artifacts, and metadata. You might have an experiment called 'churn-model-v2' with 50 runs testing different hyperparameters.