| """ |
| RoboMind VLA — Task 8: calibration.py |
| |
| Uncertainty calibration for the reward judge. |
| Computes temperature scaling and reliability metrics from validation predictions. |
| Runs on Modal CPU. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import modal |
|
|
| image = modal.Image.debian_slim(python_version="3.11").pip_install( |
| "numpy<2", |
| "scipy", |
| "scikit-learn", |
| "matplotlib", |
| ) |
|
|
| app = modal.App("robomind-calibration") |
| volume = modal.Volume.from_name("robomind-data", create_if_missing=True) |
|
|
| import json |
| import os |
| import numpy as np |
|
|
|
|
| def temperature_scale(logits: np.ndarray, temperature: float) -> np.ndarray: |
| """Apply temperature scaling to logits.""" |
| scaled = logits / temperature |
| exp_shifted = scaled - np.max(scaled, axis=-1, keepdims=True) |
| return np.exp(exp_shifted) / np.sum(np.exp(exp_shifted), axis=-1, keepdims=True) |
|
|
|
|
| def compute_calibration_metrics( |
| confidences: np.ndarray, |
| accuracies: np.ndarray, |
| n_bins: int = 10, |
| ) -> dict: |
| """Compute Expected Calibration Error (ECE) and related metrics.""" |
| bin_boundaries = np.linspace(0, 1, n_bins + 1) |
| ece = 0.0 |
| bin_data = [] |
|
|
| for i in range(n_bins): |
| mask = (confidences > bin_boundaries[i]) & (confidences <= bin_boundaries[i + 1]) |
| count = np.sum(mask) |
| if count == 0: |
| continue |
| avg_conf = np.mean(confidences[mask]) |
| avg_acc = np.mean(accuracies[mask]) |
| bin_data.append({ |
| "bin": f"({bin_boundaries[i]:.1f}, {bin_boundaries[i+1]:.1f}]", |
| "count": int(count), |
| "avg_confidence": float(avg_conf), |
| "avg_accuracy": float(avg_acc), |
| "gap": float(abs(avg_conf - avg_acc)), |
| }) |
| ece += (count / len(confidences)) * abs(avg_conf - avg_acc) |
|
|
| return { |
| "ece": float(ece), |
| "num_bins": n_bins, |
| "total_samples": int(len(confidences)), |
| "mean_confidence": float(np.mean(confidences)), |
| "mean_accuracy": float(np.mean(accuracies)), |
| "bins": bin_data, |
| } |
|
|
|
|
| def find_optimal_temperature( |
| confidences: np.ndarray, |
| accuracies: np.ndarray, |
| ) -> float: |
| """Find optimal temperature using NLL minimization.""" |
| from scipy.optimize import minimize_scalar |
|
|
| def nll(temperature): |
| if temperature <= 0: |
| return float("inf") |
| probs = temperature_scale(confidences, temperature) |
| nll_val = -np.mean( |
| accuracies * np.log(probs[:, 1] + 1e-8) |
| + (1 - accuracies) * np.log(probs[:, 0] + 1e-8) |
| ) |
| return nll_val |
|
|
| result = minimize_scalar(nll, bounds=(0.1, 10.0), method="bounded") |
| return float(result.x) |
|
|
|
|
| @app.function( |
| image=image, |
| volumes={"/data": volume}, |
| timeout=600, |
| ) |
| def calibrate(): |
| """Run calibration on stored predictions.""" |
| pred_path = "/data/calibration/predictions.jsonl" |
| if not os.path.exists(pred_path): |
| print("[cal] no predictions found, generating synthetic calibration data") |
| os.makedirs("/data/calibration", exist_ok=True) |
| generate_synthetic_data(pred_path) |
|
|
| confidences = [] |
| accuracies = [] |
|
|
| with open(pred_path, "r") as f: |
| for line in f: |
| row = json.loads(line) |
| confidences.append(row.get("confidence", 0.5)) |
| accuracies.append(row.get("correct", 0)) |
|
|
| confidences = np.array(confidences) |
| accuracies = np.array(accuracies) |
|
|
| print(f"[cal] loaded {len(confidences)} predictions") |
|
|
| metrics = compute_calibration_metrics(confidences, accuracies) |
| print(f"[cal] ECE: {metrics['ece']:.4f}") |
| print(f"[cal] Mean confidence: {metrics['mean_confidence']:.4f}") |
| print(f"[cal] Mean accuracy: {metrics['mean_accuracy']:.4f}") |
|
|
| for b in metrics.get("bins", []): |
| print(f" {b['bin']}: count={b['count']}, conf={b['avg_confidence']:.3f}, acc={b['avg_accuracy']:.3f}") |
|
|
| cal_result = { |
| "metrics": metrics, |
| "n_samples": len(confidences), |
| } |
|
|
| out_path = "/data/calibration/calibration_result.json" |
| with open(out_path, "w") as f: |
| json.dump(cal_result, f, indent=2) |
|
|
| volume.commit() |
| print(f"[cal] calibration result saved to {out_path}") |
| return cal_result |
|
|
|
|
| def generate_synthetic_data(path: str): |
| """Generate synthetic calibration data for initial setup.""" |
| import random |
| random.seed(42) |
| os.makedirs(os.path.dirname(path), exist_ok=True) |
|
|
| with open(path, "w") as f: |
| for _ in range(100): |
| confidence = random.uniform(0.3, 0.95) |
| correct = 1 if random.random() < confidence else 0 |
| f.write(json.dumps({ |
| "confidence": confidence, |
| "correct": correct, |
| "predicted_reward": random.uniform(0, 1), |
| "ground_truth_reward": random.uniform(0, 1), |
| }) + "\n") |
| print(f"[cal] generated 100 synthetic samples at {path}") |
|
|
|
|
| @app.local_entrypoint() |
| def main(): |
| result = calibrate.remote() |
| print("RESULT:", json.dumps(result, indent=2)) |
|
|