What Does Reliable AI Mean?
A reliable AI system performs as intended across the full range of expected conditions — and fails safely when it encounters conditions outside its design envelope.
Dimensions of Reliability
| Dimension | Definition | Example Failure |
|---|---|---|
| Accuracy | Correct performance on in-distribution data | 94% accuracy drops to 70% on new data |
| Robustness | Performance under perturbations and adversarial inputs | Face recognition fooled by adversarial patch |
| Calibration | Confidence scores match actual accuracy | Model says 95% confident but is right only 60% of the time |
| Stability | Consistent predictions across similar inputs | Small text change flips sentiment prediction |
| Reliability under shift | Performance under distribution change | Medical model fails on patients from different demographics |
| Graceful degradation | Acceptable behavior when uncertain | Hands off to human when confidence < threshold |
ISO 42001 — A.9 (AI System Robustness)
Control A.9.3 requires that AI systems:
- Are tested under conditions including edge cases and adversarial scenarios
- Have mechanisms to detect and handle inputs outside the expected distribution
- Provide appropriate outputs (including abstaining) when uncertainty is high
- Are monitored continuously for performance degradation in production
Safety vs Security
| Safety | Security | |
|---|---|---|
| Threat source | Environmental, accidental | Adversarial, intentional |
| Examples | Distribution shift, edge cases | Adversarial attacks, model poisoning |
| Defense | Robustness testing, uncertainty quantification | Adversarial training, input sanitization |
| Standards | ISO 42001, IEC 61508 | ISO 27001, NIST CSF |
Adversarial Robustness
Attack Types
White-box attacks (attacker knows model architecture and weights):
- FGSM (Fast Gradient Sign Method):
- PGD (Projected Gradient Descent): iterated FGSM with projection back onto the -ball; strongest attack
- C&W Attack: finds minimum perturbation that causes misclassification (constrained optimization)
Black-box attacks (attacker only has API access):
- Transfer attacks: craft adversarial example against a surrogate model; often transfers
- Query-based attacks: iteratively query the model and use output scores to estimate gradient (ZOO, SQUARE)
- Boundary attacks: start from a misclassified example and move toward the original
Defenses
| Defense | How It Works | Effectiveness |
|---|---|---|
| Adversarial training | Include adversarial examples in training set | Strongest known; computationally expensive |
| Input preprocessing | Feature squeezing, image smoothing, JPEG compression | Partially effective; can be bypassed |
| Certified defenses | Provable robustness bounds (randomized smoothing) | Formal guarantees but accuracy cost |
| Ensemble methods | Aggregate predictions from multiple models | Harder to attack all simultaneously |
| Anomaly detection | Reject inputs flagged as adversarial | Useful but introduces new attack surface |
Adversarial training objective (Madry et al.):
The inner maximization finds the worst-case perturbation; the outer minimization trains the model to be robust against it.
Distribution Shift
Models trained on data from distribution may encounter test data from :
- Covariate shift: but unchanged — e.g., different demographics
- Concept drift: changes — e.g., fraud patterns evolve after model deployment
- Dataset shift: both change simultaneously
Detecting Shift
| Method | Approach | Best For |
|---|---|---|
| KS-test | Statistical test on feature distributions | Univariate continuous features |
| Population Stability Index (PSI) | Binned distribution divergence | Credit/risk models, regulatory |
| MMD (Maximum Mean Discrepancy) | Kernel-based distribution distance | High-dimensional embeddings |
| Embedding distance | Cosine distance between rolling mean embeddings | Text/image models |
| Drift detector (Evidently) | Automated feature-level monitoring | Production deployment |
PSI < 0.1: no significant drift; 0.1–0.2: moderate drift (investigate); > 0.2: major drift (retrain).
Uncertainty Quantification
A model that says "I don't know" when it doesn't know is safer than one that confidently hallucinate answers.
Monte Carlo Dropout
During inference, keep dropout active and run forward passes — variance in predictions estimates uncertainty:
Conformal Prediction
Conformal prediction provides statistically valid uncertainty sets: a set of predictions that contains the true label with probability (e.g., 90%), regardless of the true distribution:
- Compute non-conformity scores on calibration set: (how "surprising" is the true label?)
- Find the quantile of scores:
- At test time, prediction set:
This gives a rigorous coverage guarantee — no distributional assumptions needed.
# pip install torch torchvision adversarial-robustness-toolbox evidently
# ─── 1. FGSM Adversarial Attack ──────────────────────────────
import torch
import torch.nn as nn
import numpy as np
from torchvision import models, transforms
from PIL import Image
def fgsm_attack(model, loss_fn, image, label, epsilon=0.03):
"""Fast Gradient Sign Method adversarial attack."""
image.requires_grad = True
output = model(image)
loss = loss_fn(output, label)
model.zero_grad()
loss.backward()
perturbation = epsilon * image.grad.data.sign()
adversarial = torch.clamp(image + perturbation, 0, 1)
return adversarial
def pgd_attack(model, loss_fn, image, label, epsilon=0.03, alpha=0.01, num_steps=40):
"""Projected Gradient Descent — strongest first-order attack."""
adv = image.clone().detach().requires_grad_(True)
for _ in range(num_steps):
output = model(adv)
loss = loss_fn(output, label)
loss.backward()
with torch.no_grad():
adv_update = adv + alpha * adv.grad.sign()
# Project back onto epsilon-ball
adv_update = image + torch.clamp(adv_update - image, -epsilon, epsilon)
adv_update = torch.clamp(adv_update, 0, 1)
adv = adv_update.detach().requires_grad_(True)
return adv.detach()
# Simple CNN for demonstration
class SmallCNN(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(1, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),
nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),
)
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Linear(64 * 7 * 7, 256), nn.ReLU(), nn.Dropout(0.5),
nn.Linear(256, num_classes),
)
def forward(self, x):
return self.classifier(self.features(x))
model = SmallCNN()
loss_fn = nn.CrossEntropyLoss()
# Demo: measure accuracy under attack on a batch
batch_size = 32
x = torch.rand(batch_size, 1, 28, 28) # synthetic MNIST-like
y = torch.randint(0, 10, (batch_size,))
with torch.no_grad():
clean_preds = model(x).argmax(dim=1)
clean_acc = (clean_preds == y).float().mean()
print(f"Clean accuracy: {clean_acc:.3f}")
adv_x = fgsm_attack(model, loss_fn, x.clone().requires_grad_(True), y, epsilon=0.1)
with torch.no_grad():
adv_preds = model(adv_x).argmax(dim=1)
adv_acc = (adv_preds == y).float().mean()
print(f"Accuracy under FGSM (ε=0.1): {adv_acc:.3f}")
print(f" Attack success rate: {1 - adv_acc:.3f}")
# ─── 2. Monte Carlo Dropout Uncertainty ──────────────────────
def mc_dropout_predict(model, x, n_passes=50):
"""Enable dropout during inference for uncertainty estimation."""
model.train() # Keep dropout active
preds = torch.stack([
torch.softmax(model(x), dim=1) for _ in range(n_passes)
]) # (n_passes, batch, classes)
mean_pred = preds.mean(dim=0) # Mean prediction
uncertainty = preds.var(dim=0).sum(dim=1) # Epistemic uncertainty
return mean_pred, uncertainty
with torch.no_grad():
mean_preds, uncertainties = mc_dropout_predict(model, x[:10])
print("\nMC Dropout Uncertainty Estimation:")
for i in range(5):
pred_class = mean_preds[i].argmax().item()
confidence = mean_preds[i].max().item()
unc = uncertainties[i].item()
flag = " ⚠ HIGH UNCERTAINTY" if unc > 0.3 else ""
print(f" Sample {i}: pred={pred_class}, conf={confidence:.3f}, uncertainty={unc:.4f}{flag}")
# ─── 3. Conformal Prediction ──────────────────────────────────
from sklearn.datasets import make_classification
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
import numpy as np
X_all, y_all = make_classification(n_samples=2000, n_features=20, n_classes=3,
n_informative=15, random_state=42)
X_train, X_temp, y_train, y_temp = train_test_split(X_all, y_all, test_size=0.4)
X_cal, X_test, y_cal, y_test = train_test_split(X_temp, y_temp, test_size=0.5)
clf = RandomForestClassifier(n_estimators=100, random_state=42)
clf.fit(X_train, y_train)
# Conformal calibration: compute non-conformity scores on calibration set
cal_probs = clf.predict_proba(X_cal)
cal_scores = 1 - cal_probs[np.arange(len(y_cal)), y_cal] # how "wrong" the model is on true class
# At alpha=0.1 → 90% coverage guarantee
alpha = 0.1
q_hat = np.quantile(cal_scores, 1 - alpha)
print(f"\nConformal Prediction (α={alpha}, target coverage={100*(1-alpha):.0f}%):")
print(f" Calibrated quantile q̂ = {q_hat:.4f}")
# Generate prediction sets for test instances
test_probs = clf.predict_proba(X_test)
prediction_sets = (test_probs >= 1 - q_hat) # include class if score ≥ threshold
# Evaluate coverage and set size
covered = prediction_sets[np.arange(len(y_test)), y_test].mean()
avg_set_size = prediction_sets.sum(axis=1).mean()
print(f" Empirical coverage: {covered:.4f} (should be ≥ {1-alpha:.2f})")
print(f" Average prediction set size: {avg_set_size:.2f} (smaller = more informative)")
# Show examples
print("\n Example prediction sets:")
for i in range(3):
pset = [j for j in range(3) if prediction_sets[i, j]]
correct = y_test[i] in pset
print(f" Sample {i}: set={pset}, true={y_test[i]}, covered={correct}")
# ─── 4. Fail-Safe Circuit Breaker Pattern ────────────────────
import time
from dataclasses import dataclass
from typing import Callable, Optional
class CircuitState:
CLOSED = "CLOSED" # Normal operation
OPEN = "OPEN" # Failing — reject requests, use fallback
HALF_OPEN = "HALF_OPEN" # Testing recovery
@dataclass
class CircuitBreaker:
"""Circuit breaker for AI model inference — fail fast and safely."""
name: str
failure_threshold: int = 5 # failures before opening
recovery_timeout: float = 60.0 # seconds before trying half-open
success_threshold: int = 2 # successes in half-open before closing
failure_count: int = 0
success_count: int = 0
state: str = CircuitState.CLOSED
last_failure_time: float = 0.0
def call(self, fn: Callable, fallback: Callable, *args, **kwargs):
"""Call fn; use fallback if circuit is open."""
if self.state == CircuitState.OPEN:
if time.time() - self.last_failure_time > self.recovery_timeout:
self.state = CircuitState.HALF_OPEN
print(f" [Circuit {self.name}] HALF-OPEN — testing recovery")
else:
print(f" [Circuit {self.name}] OPEN — using fallback")
return fallback(*args, **kwargs)
try:
result = fn(*args, **kwargs)
self._on_success()
return result
except Exception as e:
self._on_failure(e)
print(f" [Circuit {self.name}] Error: {e} — using fallback")
return fallback(*args, **kwargs)
def _on_success(self):
if self.state == CircuitState.HALF_OPEN:
self.success_count += 1
if self.success_count >= self.success_threshold:
self.state = CircuitState.CLOSED
self.failure_count = 0
self.success_count = 0
print(f" [Circuit {self.name}] CLOSED — recovered")
elif self.state == CircuitState.CLOSED:
self.failure_count = 0 # reset on success
def _on_failure(self, error):
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = CircuitState.OPEN
print(f" [Circuit {self.name}] OPEN after {self.failure_count} failures")
# AI model with circuit breaker + uncertainty-based escalation
call_count = 0
def ai_model_predict(input_data: dict) -> dict:
global call_count
call_count += 1
if call_count % 3 == 0: # simulate occasional failures
raise RuntimeError("Model inference timeout")
uncertainty = np.random.uniform(0, 1)
pred = np.random.randint(0, 2)
return {"prediction": pred, "confidence": 1 - uncertainty, "uncertainty": uncertainty}
def rule_based_fallback(input_data: dict) -> dict:
"""Simple rule-based fallback when AI model fails."""
return {"prediction": 0, "confidence": 0.5, "source": "FALLBACK_RULES", "escalate": True}
circuit = CircuitBreaker("LoanModel", failure_threshold=3, recovery_timeout=5.0)
UNCERTAINTY_THRESHOLD = 0.7
HUMAN_REVIEW_THRESHOLD = 0.5
print("\nFail-Safe AI System (circuit breaker + uncertainty escalation):")
for i in range(8):
result = circuit.call(ai_model_predict, rule_based_fallback, {"applicant_id": f"APP-{i}"})
if result.get("source") == "FALLBACK_RULES" or result.get("escalate"):
action = "→ HUMAN REVIEW (fallback triggered)"
elif result["uncertainty"] > UNCERTAINTY_THRESHOLD:
action = f"→ HUMAN REVIEW (high uncertainty={result['uncertainty']:.2f})"
elif result["confidence"] < HUMAN_REVIEW_THRESHOLD:
action = f"→ CONFIDENCE CHECK (conf={result['confidence']:.2f})"
else:
action = f"→ AUTO-APPROVE (conf={result['confidence']:.2f})"
print(f" Request {i+1}: {action}")Fail-Safe Design Patterns
Pattern 1: Uncertainty-Based Escalation
Define confidence thresholds for automatic decisions vs human review:
if model.confidence > 0.95:
auto_approve() # High confidence → automated
elif model.confidence > 0.70:
assisted_review() # Medium → human sees AI recommendation
else:
human_review_required() # Low confidence → human decides independently
Pattern 2: Circuit Breaker
Automatically stop calling a failing model and switch to fallback logic — prevents cascading failures in production systems. States: Closed → Open (after threshold failures) → Half-Open (recovery test) → Closed.
Pattern 3: Shadow Mode Testing
Run a new model in parallel with the production model, logging its predictions without acting on them. Compare outputs before cutover. Reveals distributional differences and edge case behaviors safely.
Pattern 4: Staged Rollout (Canary)
- 1%: monitor for errors, latency, confidence distribution
- 10%: fairness check on representative sample
- 50%: broader monitoring
- 100%: full deployment if all metrics pass
Red Teaming
AI Red Teaming involves adversarial stress-testing by a dedicated team trying to break the system:
- Functional: can we make the model fail on important cases?
- Safety: can we elicit harmful outputs from an LLM?
- Fairness: are there demographic groups where the model performs unacceptably?
- Security: can we extract training data, reverse-engineer the model, or poison it?
ISO 42001 A.6.2.5 recommends adversarial testing as part of the AI system validation process.
Knowledge check
A medical AI system gives a high-confidence prediction (98%) that a patient's scan is benign. The patient is later found to have cancer. What reliability failure does this illustrate?
Summary
- Adversarial attacks (FGSM, PGD) expose fundamental ML model vulnerabilities; adversarial training is the strongest known defense but computationally expensive
- Distribution shift is the most common real-world reliability failure; use PSI and embedding distance for continuous monitoring
- Uncertainty quantification (MC Dropout, conformal prediction) allows models to say "I don't know" — enabling safe escalation to human review
- Conformal prediction provides formal coverage guarantees without distributional assumptions — critical for high-stakes applications
- Fail-safe patterns (circuit breakers, staged rollouts, uncertainty thresholds) prevent cascading failures and ensure graceful degradation
- Red teaming proactively finds failures before users do — ISO 42001 A.6.2.5 recommends adversarial testing for all high-risk systems
Next: Privacy & Security — protecting data throughout the AI lifecycle with differential privacy and federated learning.