Skip to main content
MLOps beginner Lesson 2 of 8

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.