Download kronos_ml/uq.py from KronosFE/kronos-ml: direct link, hf CLI and curl.
- Browser
- Download file 5.56 kB
-
https://huggingface.co/KronosFE/kronos-ml/resolve/main/kronos_ml/uq.py
- Command line
-
hf download hf://KronosFE/kronos-ml/kronos_ml/uq.py
-
curl -L -o uq.py https://huggingface.co/KronosFE/kronos-ml/resolve/main/kronos_ml/uq.py
5.56 kB
| """KHALO's reusable uncertainty engine — the shared layer every surrogate wraps. | |
| Two heads (GP posterior for regression, deep-ensemble for classification) plus | |
| the canonical calibration report from `metrics`. sklearn is imported lazily so | |
| `import kronos_ml` stays clean in a bare env. | |
| """ | |
| from __future__ import annotations | |
| import numpy as np | |
| from . import metrics | |
| SEED = 20260726 # the corpus-wide fixed seed | |
| class GPHead: | |
| """Gaussian-process regressor (Matern-5/2 x Constant + White), optionally | |
| delta-learning on a cheap baseline. Returns calibrated (mean, std).""" | |
| def __init__(self, baseline=None, length_scale=0.5, alpha=1e-8, n_restarts=2): | |
| self.baseline = baseline | |
| self.length_scale = length_scale | |
| self.alpha = alpha | |
| self.n_restarts = n_restarts | |
| self._gp = None | |
| self._xmu = self._xsd = None | |
| def _std_x(self, X): | |
| X = np.atleast_2d(np.asarray(X, float)) | |
| if self._xmu is None: | |
| self._xmu = X.mean(0); self._xsd = X.std(0) + 1e-12 | |
| return (X - self._xmu) / self._xsd | |
| def fit(self, X, y): | |
| from sklearn.gaussian_process import GaussianProcessRegressor | |
| from sklearn.gaussian_process.kernels import ( | |
| Matern, ConstantKernel, WhiteKernel) | |
| X = np.atleast_2d(np.asarray(X, float)); y = np.asarray(y, float) | |
| resid = y - self._base(X) if self.baseline is not None else y | |
| k = (ConstantKernel(1.0, (1e-3, 1e3)) | |
| * Matern(length_scale=self.length_scale, length_scale_bounds=(1e-2, 1e1), nu=2.5) | |
| + WhiteKernel()) | |
| self._gp = GaussianProcessRegressor( | |
| kernel=k, alpha=self.alpha, normalize_y=True, | |
| n_restarts_optimizer=self.n_restarts, random_state=SEED) | |
| self._gp.fit(self._std_x(X), resid) | |
| return self | |
| def _base(self, X): | |
| return np.asarray([self.baseline(xi) for xi in np.atleast_2d(X)], float) | |
| def predict(self, X): | |
| X = np.atleast_2d(np.asarray(X, float)) | |
| mean, std = self._gp.predict(self._std_x(X), return_std=True) | |
| if self.baseline is not None: | |
| mean = mean + self._base(X) | |
| return mean, std | |
| class EnsembleHead: | |
| """Soft-vote deep ensemble (HistGB + RandomForest + MLP) for the disruption / | |
| classifier path. Returns mean predict_proba.""" | |
| def __init__(self): | |
| self.models = None | |
| self.scaler = None | |
| def fit(self, X, y): | |
| from sklearn.ensemble import (HistGradientBoostingClassifier, | |
| RandomForestClassifier) | |
| from sklearn.neural_network import MLPClassifier | |
| from sklearn.preprocessing import StandardScaler | |
| X = np.asarray(X, float); y = np.asarray(y, int) | |
| self.scaler = StandardScaler().fit(X) | |
| Xs = self.scaler.transform(X) | |
| self.models = [ | |
| HistGradientBoostingClassifier(max_iter=120, max_depth=6, | |
| learning_rate=0.12, random_state=SEED).fit(Xs, y), | |
| RandomForestClassifier(n_estimators=120, max_depth=10, | |
| random_state=SEED).fit(Xs, y), | |
| MLPClassifier(hidden_layer_sizes=(48, 24), max_iter=180, | |
| early_stopping=True, random_state=SEED).fit(Xs, y), | |
| ] | |
| return self | |
| def predict_proba(self, X): | |
| Xs = self.scaler.transform(np.asarray(X, float)) | |
| return np.mean([m.predict_proba(Xs)[:, 1] for m in self.models], axis=0) | |
| class CalibratedClassifier: | |
| """Deep ensemble + isotonic probability calibration (the net-new KODEX layer | |
| on the disruption path). Fits the ensemble on a train split and the isotonic | |
| map on a held-out calibration split.""" | |
| def __init__(self, cal_frac=0.3): | |
| self.cal_frac = cal_frac | |
| self.ens = None | |
| self.iso = None | |
| def fit(self, X, y): | |
| from sklearn.isotonic import IsotonicRegression | |
| X = np.asarray(X, float); y = np.asarray(y, int) | |
| n = len(y); ntr = int((1 - self.cal_frac) * n) | |
| self.ens = EnsembleHead().fit(X[:ntr], y[:ntr]) | |
| p = self.ens.predict_proba(X[ntr:]) | |
| self.iso = IsotonicRegression(out_of_bounds="clip").fit(p, y[ntr:]) | |
| self._cal_X, self._cal_y = X[ntr:], y[ntr:] | |
| return self | |
| def predict_proba(self, X): | |
| return self.iso.predict(self.ens.predict_proba(np.asarray(X, float))) | |
| class MahalanobisGate: | |
| """Per-sample out-of-domain gate: Mahalanobis distance (LedoitWolf-shrunk | |
| precision) to the training distribution, thresholded to a target in-dist | |
| abstention rate. in_domain(x) is False when x is anomalous vs training.""" | |
| def __init__(self, abstain_rate=0.05): | |
| self.abstain_rate = abstain_rate | |
| self.mu = self.prec = self.thresh = None | |
| def fit(self, X): | |
| from sklearn.covariance import LedoitWolf | |
| X = np.asarray(X, float) | |
| self.mu = X.mean(0) | |
| self.prec = LedoitWolf().fit(X).precision_ | |
| d = self._d(X) | |
| self.thresh = float(np.quantile(d, 1 - self.abstain_rate)) | |
| return self | |
| def _d(self, X): | |
| D = np.atleast_2d(np.asarray(X, float)) - self.mu | |
| return np.sqrt(np.clip(np.einsum("ij,jk,ik->i", D, self.prec, D), 0, None)) | |
| def in_domain(self, x): | |
| return bool(self._d(x)[0] <= self.thresh) | |
| def calibrate(y_true, mean, std, levels=metrics.DEFAULT_LEVELS): | |
| """The first-class calibration object KHALO stamps on any surrogate output.""" | |
| return metrics.calibration_report(y_true, mean, std, levels) | |