mlops-rag-agent / knowledge_base /model_monitoring.txt
atulkrs's picture
Add full RAG pipeline: agent, rag_engine, generator, knowledge_base, full Gradio UI
223d01b verified
Raw
History Blame Contribute Delete
12.9 kB
Model Monitoring and Observability Guide
=========================================
## Overview
Model monitoring ensures that ML models in production continue to perform as expected over time. Models degrade due to data drift (input distribution changes), concept drift (relationship between inputs and outputs changes), and infrastructure issues. A robust monitoring system detects these problems early and triggers alerts or automated retraining.
## Types of Model Drift
### Data Drift (Covariate Shift)
The input feature distribution changes between training and serving time.
```python
# Detecting data drift with Evidently AI
from evidently.report import Report
from evidently.metric_preset import DataDriftPreset, DataQualityPreset
report = Report(metrics=[DataDriftPreset(), DataQualityPreset()])
report.run(reference_data=training_data, current_data=production_data)
report.save_html("data_drift_report.html")
# Check if drift was detected
result = report.as_dict()
for metric in result['metrics']:
if metric['metric'] == 'DataDriftTable':
drifted_features = [
col for col, stats in metric['result']['drift_by_columns'].items()
if stats['drift_detected']
]
print(f"Drifted features: {drifted_features}")
```
### Concept Drift
The relationship P(y|X) changes — the model's predictions become incorrect even when inputs look normal.
```python
from evidently.metric_preset import TargetDriftPreset
# Monitor target distribution drift
target_report = Report(metrics=[TargetDriftPreset()])
target_report.run(
reference_data=training_data[['features', 'target']],
current_data=production_data[['features', 'target']]
)
```
### Prediction Drift
The distribution of model outputs shifts, indicating the model is seeing different types of inputs.
```python
from evidently.metrics import ColumnDriftMetric
prediction_drift_report = Report(metrics=[
ColumnDriftMetric(column_name="prediction"),
ColumnDriftMetric(column_name="prediction_confidence")
])
```
## Alibi Detect for Drift Detection
```python
import alibi_detect
from alibi_detect.cd import KSDrift, MMDDrift, ChiSquareDrift
from alibi_detect.cd import TabularDrift
import numpy as np
# Kolmogorov-Smirnov test for continuous features
ks_detector = KSDrift(
x_ref=X_train,
p_val=0.05,
preprocess_fn=None
)
result = ks_detector.predict(X_production, drift_type='batch', return_p_val=True)
print(f"Drift detected: {result['data']['is_drift']}")
print(f"P-values per feature: {result['data']['p_val']}")
# Maximum Mean Discrepancy for complex distributions
mmd_detector = MMDDrift(
x_ref=X_train,
backend='pytorch',
p_val=0.05,
n_permutations=100
)
# Combined tabular drift (handles mixed types)
tabular_detector = TabularDrift(
x_ref=X_train,
p_val=0.05,
categories_per_feature={0: None, 1: None, 2: 3, 3: 5} # feature_idx: n_categories (None = continuous)
)
# Save detector
from alibi_detect.utils.saving import save_detector, load_detector
save_detector(ks_detector, './ks_detector')
loaded_detector = load_detector('./ks_detector')
```
## Prometheus and Grafana for ML Metrics
### Custom Metrics with prometheus-client
```python
from prometheus_client import Counter, Histogram, Gauge, start_http_server
import time
# Define metrics
inference_requests = Counter('ml_inference_requests_total', 'Total inference requests', ['model_version', 'status'])
inference_latency = Histogram('ml_inference_duration_seconds', 'Inference request latency',
buckets=[0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0])
model_confidence = Histogram('ml_prediction_confidence', 'Model prediction confidence scores',
buckets=[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0])
data_drift_score = Gauge('ml_data_drift_score', 'Current data drift score per feature', ['feature_name'])
active_model_version = Gauge('ml_active_model_version', 'Currently active model version')
# Instrumented prediction function
def predict_with_monitoring(request_data):
start_time = time.time()
try:
result = model.predict(request_data)
confidence = float(result['confidence'])
# Record metrics
inference_requests.labels(model_version="1.2", status="success").inc()
inference_latency.observe(time.time() - start_time)
model_confidence.observe(confidence)
# Alert on low confidence
if confidence < 0.6:
low_confidence_predictions.inc()
return result
except Exception as e:
inference_requests.labels(model_version="1.2", status="error").inc()
raise
# Start metrics server
start_http_server(8000)
```
### Grafana Dashboard for ML Models
```json
{
"dashboard": {
"title": "ML Model Monitoring",
"panels": [
{
"title": "Inference Requests per Second",
"type": "graph",
"targets": [{"expr": "rate(ml_inference_requests_total[5m])"}]
},
{
"title": "P99 Inference Latency",
"type": "graph",
"targets": [{"expr": "histogram_quantile(0.99, rate(ml_inference_duration_seconds_bucket[5m]))"}]
},
{
"title": "Model Accuracy (last hour)",
"type": "stat",
"targets": [{"expr": "ml_model_accuracy"}]
},
{
"title": "Data Drift Score by Feature",
"type": "bar",
"targets": [{"expr": "ml_data_drift_score"}]
},
{
"title": "Prediction Confidence Distribution",
"type": "histogram",
"targets": [{"expr": "rate(ml_prediction_confidence_bucket[5m])"}]
}
]
}
}
```
### AlertManager Rules
```yaml
# alerting-rules.yml
groups:
- name: ml-model-alerts
rules:
- alert: HighInferenceLatency
expr: histogram_quantile(0.99, rate(ml_inference_duration_seconds_bucket[5m])) > 1.0
for: 5m
labels:
severity: warning
annotations:
summary: "P99 inference latency exceeds 1 second"
description: "Model {{ $labels.model_version }} P99 latency is {{ $value }}s"
- alert: ModelAccuracyDegradation
expr: ml_model_accuracy < 0.80
for: 10m
labels:
severity: critical
annotations:
summary: "Model accuracy dropped below 80%"
- alert: DataDriftDetected
expr: ml_data_drift_score > 0.3
for: 15m
labels:
severity: warning
annotations:
summary: "Significant data drift detected in feature {{ $labels.feature_name }}"
- alert: HighErrorRate
expr: rate(ml_inference_requests_total{status="error"}[5m]) / rate(ml_inference_requests_total[5m]) > 0.05
for: 5m
labels:
severity: critical
annotations:
summary: "Model error rate exceeds 5%"
```
## Great Expectations for Data Quality
```python
import great_expectations as gx
from great_expectations.core.batch import RuntimeBatchRequest
context = gx.get_context()
# Create expectation suite
suite = context.add_or_update_expectation_suite("ml_training_data")
# Connect to data
datasource = context.sources.add_pandas("pandas_datasource")
data_asset = datasource.add_dataframe_asset("training_data")
batch_request = data_asset.build_batch_request(dataframe=df)
validator = context.get_validator(batch_request=batch_request, expectation_suite_name="ml_training_data")
# Define expectations
validator.expect_column_to_exist("feature_1")
validator.expect_column_values_to_not_be_null("feature_1")
validator.expect_column_values_to_be_between("age", min_value=0, max_value=120)
validator.expect_column_values_to_be_in_set("category", value_set=["A", "B", "C"])
validator.expect_column_mean_to_be_between("income", min_value=20000, max_value=200000)
validator.expect_column_stdev_to_be_between("income", min_value=5000, max_value=50000)
validator.expect_table_row_count_to_be_between(min_value=1000, max_value=10000000)
validator.save_expectation_suite()
# Run checkpoint
checkpoint = context.add_or_update_checkpoint(
name="ml_data_checkpoint",
validations=[{
"batch_request": batch_request,
"expectation_suite_name": "ml_training_data"
}]
)
results = checkpoint.run()
assert results.success, "Data quality check failed!"
```
## Automated Retraining Triggers
```python
import boto3
from datetime import datetime, timedelta
class AutoRetrainingSystem:
def __init__(self, drift_threshold=0.2, accuracy_threshold=0.85):
self.drift_threshold = drift_threshold
self.accuracy_threshold = accuracy_threshold
self.cloudwatch = boto3.client('cloudwatch')
self.sagemaker = boto3.client('sagemaker')
def check_drift_and_trigger_retraining(self, model_name):
# Get current drift score from CloudWatch
response = self.cloudwatch.get_metric_statistics(
Namespace='MLMonitoring',
MetricName='DataDriftScore',
Dimensions=[{'Name': 'ModelName', 'Value': model_name}],
StartTime=datetime.now() - timedelta(hours=24),
EndTime=datetime.now(),
Period=3600,
Statistics=['Average']
)
drift_score = max([dp['Average'] for dp in response['Datapoints']], default=0)
if drift_score > self.drift_threshold:
print(f"Drift score {drift_score:.3f} exceeds threshold {self.drift_threshold}. Triggering retraining.")
self._trigger_retraining_pipeline(model_name, reason=f"drift_score={drift_score:.3f}")
def _trigger_retraining_pipeline(self, model_name, reason):
# Start SageMaker Pipeline execution
self.sagemaker.start_pipeline_execution(
PipelineName=f"{model_name}-retraining-pipeline",
PipelineExecutionDisplayName=f"auto-retrain-{datetime.now().strftime('%Y%m%d-%H%M')}",
PipelineParameters=[
{'Name': 'TriggerReason', 'Value': reason},
{'Name': 'ModelName', 'Value': model_name}
]
)
# Schedule this to run hourly
scheduler = AutoRetrainingSystem(drift_threshold=0.25, accuracy_threshold=0.82)
scheduler.check_drift_and_trigger_retraining("customer-churn-model")
```
## Canary and Shadow Deployments for Safe Rollouts
```python
# Canary deployment — serve new model to 10% of traffic
class CanaryRouter:
def __init__(self, champion_model, challenger_model, challenger_weight=0.1):
self.champion = champion_model
self.challenger = challenger_model
self.challenger_weight = challenger_weight
def route_request(self, request):
import random
if random.random() < self.challenger_weight:
result = self.challenger.predict(request)
self._log_prediction(result, model_type="challenger")
else:
result = self.champion.predict(request)
self._log_prediction(result, model_type="champion")
return result
def promote_challenger(self):
self.champion = self.challenger
self.challenger_weight = 0.0
print("Challenger promoted to champion!")
```
## MLflow Model Registry with Monitoring
```python
import mlflow
from mlflow.tracking import MlflowClient
client = MlflowClient()
# Set model tags with monitoring thresholds
client.set_model_version_tag("MyModel", "1", "accuracy_threshold", "0.85")
client.set_model_version_tag("MyModel", "1", "drift_threshold", "0.2")
client.set_model_version_tag("MyModel", "1", "latency_sla_ms", "100")
# Automated model health check
def check_model_health(model_name, version):
model_version = client.get_model_version(model_name, version)
tags = model_version.tags
accuracy_threshold = float(tags.get("accuracy_threshold", 0.8))
current_accuracy = get_current_model_accuracy(model_name)
if current_accuracy < accuracy_threshold:
client.transition_model_version_stage(
name=model_name,
version=version,
stage="Archived",
archive_existing_versions=False
)
print(f"Model {model_name} v{version} archived due to accuracy degradation: {current_accuracy:.3f}")
trigger_retraining(model_name)
```
## Best Practices
1. Monitor both data quality and model performance — they're both early warning systems
2. Set up alerts before deploying to production, not after an incident
3. Store all prediction inputs and outputs for retrospective analysis
4. Use statistical tests (KS, PSI, MMD) for rigorous drift detection, not just visual inspection
5. Implement shadow mode testing before promoting new models
6. Track feature importance changes alongside accuracy metrics
7. Set SLOs for inference latency and establish error budget policies
8. Automate retraining triggers but require human approval for production promotions
9. Store monitoring baselines in version control alongside model artifacts
10. Create runbooks for each alert type so on-call engineers know how to respond