MLOps for Public Health AI
Versioning, reproducibility, testing, and release controls for public health AI systems. The material is maintained separately so each operational question has a stable, focused reference.
- Identify the evidence and controls relevant to this decision area
- Distinguish technical performance from operational and population impact
- Apply the included framework without extending claims beyond the cited evidence
Introduction
This focused reference is part of the broader MLOps overview. It preserves the detailed methods, examples, and exercises while reducing page size and improving direct navigation.
MLOps Fundamentals
What Is MLOps?
MLOps (Machine Learning Operations) applies DevOps principles to machine learning systems, enabling reliable and efficient deployment, monitoring, and maintenance.
Kreuzberger et al., 2023, IEEE Access define MLOps as:
“A set of practices that aims to deploy and maintain machine learning models in production reliably and efficiently.”
Core principles:
1. Reproducibility - Every model run can be exactly recreated - Version control for code, data, models, environment - Deterministic training procedures
2. Automation - Minimize manual steps (human error) - CI/CD pipelines for testing and deployment - Automated retraining when needed
3. Versioning - Track all artifacts: data, models, code - Enable rollback to previous versions - Compare performance across versions
4. Monitoring - Continuous performance measurement - Detect drift, degradation, anomalies - Alert on issues before they cause harm
5. Collaboration - Bridge data scientists, engineers, clinicians - Shared understanding of system behavior - Clear ownership and accountability
For comprehensive MLOps frameworks, see Treveil et al., 2020, Introducing MLOps and Alla & Adari, 2021, Beginning MLOps with MLflow.
The MLOps Lifecycle
┌─────────────────────────────────────────────────────────────┐
│ MLOps Lifecycle │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Data │──▶│ Model │──▶│ Deploy │ │
│ │ Pipeline │ │ Training │ │ to Prod │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │ │ │ │
│ └─────────────────┴─────────────────┘ │
│ │ │
│ ┌────▼────┐ │
│ │ Monitor │ │
│ │& Alert │ │
│ └────┬────┘ │
│ │ │
│ ┌────▼────┐ │
│ │Retrain │ │
│ │Decision │ │
│ └─────────┘ │
└─────────────────────────────────────────────────────────────┘
Key stages:
1. Data Pipeline - Automated data extraction from EHR - Quality validation and cleaning - Feature engineering - Version-controlled datasets
2. Model Training - Reproducible training environment - Hyperparameter tracking - Performance metrics logging - Model registration
3. Deployment - Automated testing (unit, integration, performance) - Gradual rollout (canary, blue-green) - Rollback capability - Documentation generation
4. Monitoring - Performance tracking by subgroup - Data drift detection - Concept drift detection - System health metrics
5. Retraining Decision - Triggered by performance degradation - Scheduled periodic retraining - New data availability - Regulatory requirements
Version Control for Machine Learning
What to version:
1. Code (Git, GitHub, GitLab)
git commit -m "Add calibration layer to sepsis model"
git tag -a v1.2.0 -m "Production release with improved calibration"
git push origin v1.2.0# Track data with DVC
dvc add data/training_set_2024_Q1.csv
git add data/training_set_2024_Q1.csv.dvc
git commit -m "Add Q1 2024 training data"
# Retrieve specific data version
dvc checkout data/training_set_2024_Q1.csv.dvc3. Models (MLflow, Weights & Biases)
import mlflow
import mlflow.sklearn
from datetime import datetime
# Set experiment
mlflow.set_experiment("sepsis-prediction-v2")
# Start run with automatic logging
with mlflow.start_run(run_name=f"rf_model_{datetime.now().strftime('%Y%m%d_%H%M')}"):
# Log parameters
params = {
'n_estimators': 100,
'max_depth': 10,
'min_samples_split': 5,
'class_weight': 'balanced',
'random_state': 42
}
mlflow.log_params(params)
# Train model
from sklearn.ensemble import RandomForestClassifier
model = RandomForestClassifier(**params)
model.fit(X_train, y_train)
# Evaluate and log metrics
from sklearn.metrics import roc_auc_score, recall_score, precision_score
train_auc = roc_auc_score(y_train, model.predict_proba(X_train)[:, 1])
val_auc = roc_auc_score(y_val, model.predict_proba(X_val)[:, 1])
val_sensitivity = recall_score(y_val, model.predict(X_val))
val_specificity = recall_score(1-y_val, 1-model.predict(X_val))
val_ppv = precision_score(y_val, model.predict(X_val))
mlflow.log_metrics({
'train_auc': train_auc,
'val_auc': val_auc,
'val_sensitivity': val_sensitivity,
'val_specificity': val_specificity,
'val_ppv': val_ppv
})
# Log model
mlflow.sklearn.log_model(
model,
"model",
registered_model_name="sepsis_predictor",
signature=mlflow.models.infer_signature(X_val, model.predict(X_val))
)
# Log artifacts (plots, feature importance)
import matplotlib.pyplot as plt
from sklearn.metrics import roc_curve, auc
# ROC curve
fpr, tpr, _ = roc_curve(y_val, model.predict_proba(X_val)[:, 1])
plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, label=f'AUC = {val_auc:.3f}')
plt.plot([0, 1], [0, 1], 'k--')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC Curve')
plt.legend()
plt.savefig('roc_curve.png', dpi=150, bbox_inches='tight')
mlflow.log_artifact('roc_curve.png')
plt.close()
# Feature importance
feature_names = X_train.columns
importances = model.feature_importances_
indices = np.argsort(importances)[::-1][:10]
plt.figure(figsize=(10, 6))
plt.barh(range(len(indices)), importances[indices])
plt.yticks(range(len(indices)), [feature_names[i] for i in indices])
plt.xlabel('Feature Importance')
plt.title('Top 10 Most Important Features')
plt.tight_layout()
plt.savefig('feature_importance.png', dpi=150, bbox_inches='tight')
mlflow.log_artifact('feature_importance.png')
plt.close()
print(f"[OK] Run ID: {mlflow.active_run().info.run_id}")
print(f"[OK] Validation AUC: {val_auc:.3f}")Promote model to production:
from mlflow.tracking import MlflowClient
client = MlflowClient()
# Search for best model in experiment
experiment = client.get_experiment_by_name("sepsis-prediction-v2")
runs = client.search_runs(
experiment_ids=[experiment.experiment_id],
filter_string="metrics.val_auc > 0.80", # Minimum acceptable performance
order_by=["metrics.val_auc DESC"],
max_results=1
)
if len(runs) == 0:
print("[ERROR] No models meet minimum performance threshold")
else:
best_run = runs[0]
best_run_id = best_run.info.run_id
best_auc = best_run.data.metrics['val_auc']
print(f"[OK] Best model - Run ID: {best_run_id}, AUC: {best_auc:.3f}")
# Get current production model for comparison
try:
prod_versions = client.get_latest_versions("sepsis_predictor", stages=["Production"])
if prod_versions:
prod_auc = float(prod_versions[0].tags.get('val_auc', 0))
print(f"Current production AUC: {prod_auc:.3f}")
# Only promote if better
if best_auc > prod_auc:
print("[OK] New model is better, promoting...")
else:
print("[WARNING] New model not better than production, not promoting")
exit(0)
except:
print("[INFO] No current production model, will promote")
# Register model version
model_uri = f"runs:/{best_run_id}/model"
model_details = mlflow.register_model(model_uri, "sepsis_predictor")
# Add tags
client.set_model_version_tag(
name="sepsis_predictor",
version=model_details.version,
key="val_auc",
value=str(best_auc)
)
# Transition to production
client.transition_model_version_stage(
name="sepsis_predictor",
version=model_details.version,
stage="Production",
archive_existing_versions=True
)
print(f"[OK] Model version {model_details.version} promoted to Production!")4. Environment (Docker, Conda)
FROM python:3.9-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
g++ \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY api.py .
COPY models/ ./models/
# Create non-root user
RUN useradd -m -u 1000 mluser && chown -R mluser:mluser /app
USER mluser
# Expose port
EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import requests; requests.get('http://localhost:8000/health')"
# Run application
CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "8000"]5. Experiments (MLflow, Neptune, Weights & Biases)
For comprehensive model tracking best practices, see Zaharia et al., 2018, IEEE Data Eng. Bull. and the MLflow documentation.
CI/CD for Machine Learning
Continuous Integration/Continuous Deployment adapted for ML workflows.
Sato et al., 2019, IEEE Software - “Continuous Delivery for Machine Learning”
Typical ML CI/CD pipeline:
# .github/workflows/ml-pipeline.yml
name: ML Model CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
- cron: '0 2 * * 0' # Weekly retraining check
jobs:
data-validation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install dependencies
run: |
pip install -r requirements.txt
- name: Validate Data Quality
run: |
python scripts/validate_data.py --input data/latest/training_data.csv
- name: Check Data Drift
run: |
python scripts/check_drift.py \
--reference data/baseline/training_data.csv \
--current data/latest/training_data.csv \
--threshold 0.05
- name: Upload drift report
if: always()
uses: actions/upload-artifact@v3
with:
name: drift-report
path: reports/drift_analysis.html
model-training:
needs: data-validation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install dependencies
run: |
pip install -r requirements.txt
- name: Train Model
env:
MLFLOW_TRACKING_URI: ${{ secrets.MLFLOW_TRACKING_URI }}
run: |
python train.py \
--data data/latest/training_data.csv \
--experiment-name sepsis-prediction-ci \
--run-name ci-run-${{ github.run_number }}
- name: Evaluate Model
run: |
python evaluate.py \
--model-uri runs:/${{ steps.train.outputs.run_id }}/model \
--test-data data/latest/test_data.csv \
--output-path reports/evaluation.json
- name: Compare with Production
run: |
python scripts/compare_models.py \
--candidate-uri runs:/${{ steps.train.outputs.run_id }}/model \
--production-uri models:/sepsis_predictor/Production
- name: Upload evaluation report
uses: actions/upload-artifact@v3
with:
name: evaluation-report
path: reports/
model-testing:
needs: model-training
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install pytest pytest-cov
- name: Unit Tests
run: |
pytest tests/unit/ -v --cov=src --cov-report=xml
- name: Integration Tests
run: |
pytest tests/integration/ -v
- name: Performance Tests
run: |
python tests/performance_test.py \
--model-uri models:/sepsis_predictor/Staging \
--target-latency-ms 100 \
--target-throughput-qps 50
- name: Upload coverage
uses: codecov/codecov-action@v3
fairness-audit:
needs: model-training
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Fairness Assessment
run: |
python scripts/fairness_audit.py \
--model-uri runs:/${{ steps.train.outputs.run_id }}/model \
--test-data data/latest/test_data.csv \
--protected-attributes race,gender,age_group
- name: Upload fairness report
uses: actions/upload-artifact@v3
with:
name: fairness-report
path: reports/fairness_audit.html
security-scan:
needs: model-testing
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Scan dependencies for vulnerabilities
run: |
pip install safety
safety check --file requirements.txt
- name: Docker image security scan
uses: aquasecurity/trivy-action@master
with:
image-ref: 'sepsis-predictor:latest'
format: 'sarif'
output: 'trivy-results.sarif'
deploy-staging:
needs: [model-testing, fairness-audit, security-scan]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop'
steps:
- uses: actions/checkout@v3
- name: Build Docker Image
run: |
docker build -t sepsis-predictor:${{ github.sha }} .
docker tag sepsis-predictor:${{ github.sha }} sepsis-predictor:staging
- name: Push to Registry
env:
DOCKER_REGISTRY: ${{ secrets.DOCKER_REGISTRY }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
echo $DOCKER_PASSWORD | docker login $DOCKER_REGISTRY -u $DOCKER_USERNAME --password-stdin
docker push sepsis-predictor:${{ github.sha }}
docker push sepsis-predictor:staging
- name: Deploy to Kubernetes Staging
env:
KUBECONFIG: ${{ secrets.KUBECONFIG_STAGING }}
run: |
kubectl set image deployment/sepsis-predictor \
sepsis-predictor=sepsis-predictor:${{ github.sha }} \
-n staging
kubectl rollout status deployment/sepsis-predictor -n staging
- name: Smoke Tests
run: |
python tests/smoke_test.py \
--endpoint https://staging.sepsis-predictor.hospital.org \
--timeout 300
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment:
name: production
url: https://sepsis-predictor.hospital.org
steps:
- uses: actions/checkout@v3
- name: Promote to Production in MLflow
env:
MLFLOW_TRACKING_URI: ${{ secrets.MLFLOW_TRACKING_URI }}
run: |
python scripts/promote_to_production.py \
--run-id ${{ steps.train.outputs.run_id }}
- name: Tag Docker Image
run: |
docker tag sepsis-predictor:${{ github.sha }} sepsis-predictor:production
docker push sepsis-predictor:production
- name: Blue-Green Deployment to Production
env:
KUBECONFIG: ${{ secrets.KUBECONFIG_PRODUCTION }}
run: |
# Deploy green version
kubectl apply -f k8s/production/deployment-green.yaml
kubectl wait --for=condition=available --timeout=300s \
deployment/sepsis-predictor-green -n production
# Run validation tests
python tests/production_validation.py \
--endpoint https://green.sepsis-predictor.hospital.org
# Switch traffic from blue to green
kubectl patch service sepsis-predictor \
-p '{"spec":{"selector":{"version":"green"}}}' \
-n production
# Monitor for 10 minutes
echo "Monitoring green deployment for 10 minutes..."
sleep 600
# If successful, scale down blue
kubectl scale deployment sepsis-predictor-blue --replicas=0 -n production
echo "[OK] Production deployment complete!"
- name: Notify Team
if: always()
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: 'Production deployment: ${{ job.status }}'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}For comprehensive CI/CD for ML, see Zhou et al., 2020, IEEE ICAICE - “Towards MLOps: A Case Study of ML Pipeline Platform”.