Calibration: online Platt scaling via sequential Newton optimization¶
A model can be great at ranking outcomes but still report the wrong confidence numbers—stating it is 99% sure when it is only right 80% of the time. This tool corrects those probabilities in real time, sample by sample, so that a robot's control policy can rely on probabilities that match how often the model is right.
A classifier can rank its predictions well and still attach the wrong numbers to them. A decision tree that has 80 % accuracy on some region will happily output 0.99 on every sample of that region — the ranking is fine, the numbers are lies. On a robot, the control policy reads those numbers: a 0.99 that means "80 %" makes the policy act far too confidently.
Calibration fixes the numbers without touching the ranking. OnlinePlattScaling
wraps any classifier from an existing online-learning library and corrects its probabilities one
sample at a time, using the map sigmoid(a · logit(p) + b) learned by Online Newton
Step.
1. The idea, in one line¶
The base classifier's probability p is transformed into
σ(a · logit(p) + b), with a and b learned online. If the base model is
over-confident (probabilities pushed toward 0 and 1), a < 1 shrinks them toward 0.5.
If it is under-confident, a > 1 stretches them out. If it is systematically biased
toward one side, b ≠ 0 shifts them. Both numbers adapt as new samples arrive.
2. The formula, symbol by symbol¶
Let p be the base model's probability for a sample, and y ∈ {0, 1} its true label.
logit(p) = ln(p / (1 − p))— the inverse of the sigmoid, from(0, 1)toℝ.σ(z) = 1 / (1 + e^{−z})— the sigmoid, fromℝback to(0, 1).- The corrected probability is
σ(a · logit(p) + b). - Start:
(a, b) = (1, 0)— the base model's probabilities pass through unchanged.
After each label y, the parameters update by Online Newton Step:
u = (logit(p), 1)
g = (σ(θ · u) − y) · u
A ← A + g gᵀ (start A = ρ I, ρ = 100)
θ ← Proj(θ − A⁻¹ g / γ) (γ = 0.1, projection onto ‖θ‖ ≤ 100)
The clip on p to [0.01, 0.99] is a numerical guard: logit(0) and logit(1) are
infinite, and the paper's regret bound (Theorem 2.1 of Gupta–Ramdas 2023,
arXiv:2305.00070) assumes the base model never
outputs exactly 0 or 1.
3. Hand case¶
Base model says p = 0.8 on a sample whose true label is y = 0. Start
(a, b) = (1, 0), so θ = (1, 0).
u = (logit(0.8), 1) = (1.3863, 1)
σ(θ · u) = σ(1.3863) = 0.8
g = (0.8 − 0) · (1.3863, 1) = (1.109, 0.8)
Assume A = 100 · I (start of the run, A has not accumulated much yet). Then:
New corrected probability on the same input:
The base model was wrong and confident; after one sample of feedback, the calibrated model says 0.760. It lowered its confidence after being wrong — exactly what calibration is supposed to do.
4. The result¶
import numpy as np
from dense_armor.utility.learn.online_classifiers import OnlineGaussianNB
from dense_armor.utility.learn.calibration import OnlinePlattScaling
from dense_armor.utility.metrics import LogLoss
rng = np.random.default_rng(0)
base = OnlineGaussianNB()
cal = OnlinePlattScaling(OnlineGaussianNB())
base_loss, cal_loss = LogLoss(), LogLoss()
for _ in range(2000):
x = float(rng.uniform(-1.0, 1.0))
y = int(x > 0.0)
p_base = base.predict_proba_one({"x": x}).get(1, 0.5)
p_cal = cal.predict_proba_one({"x": x}).get(1, 0.5)
base_loss.update(y, p_base)
cal_loss.update(y, p_cal)
base.learn_one({"x": x}, y)
cal.learn_one({"x": x}, y)
print(round(base_loss.get(), 4), round(cal_loss.get(), 4))
The calibrated model's log-loss drops from 0.1016 to 0.036. The wrapper
passes the check_estimator of an existing online-learning library.
5. The parameters¶
| Element | Paper (Algorithm 1) | Parameter |
|---|---|---|
| Start | (a, b) = (1, 0): base probabilities pass through unchanged |
— |
| Newton step | scaled by 1/gamma, gamma = 0.1 |
gamma |
| Initial Hessian | rho · I, rho = 100 |
rho |
| Projection | ‖(a, b)‖₂ ≤ 100 |
radius |
| Clipping | f(x) in [0.01, 0.99] |
clip |
API reference¶
calibration ¶
Online Platt scaling (Gupta and Ramdas, ICML 2023, arXiv:2305.00070) as a classifier wrapper for an existing online-learning library. Needs the optional dependency: the streaming extra
OnlinePlattScaling ¶
OnlinePlattScaling(
classifier: Classifier,
gamma: float = 0.1,
rho: float = 100.0,
radius: float = 100.0,
clip: float = 0.01,
)
Bases: ModelWrapper, Classifier
Online Platt scaling.
Recalibrates the probabilities of a classifier. The update happens at every new example, so
the mapping adapts even when the data distribution changes over time. Given the probability
p of the base classifier, the calibrated one is sigmoid(a · logit(p) + b), with a and b
updated online via Online Newton Step. If the base is already calibrated, the mapping tends to
the identity and the probabilities remain almost unchanged.
While the base classifier has no opinion yet (an empty predict_proba_one, as a tree
returns before it has seen a class), the wrapper returns an empty dict too, and learn_one
passes the sample to the base classifier without updating a and b: the mapping is
never fitted on a probability the base did not give. A base that knows only the negative
class gives p = 1 - p(False).
When the base classifies well but gives wrong numbers: the base classifier produces miscalibrated probabilities, typically because it is overconfident in its own predictions. This is the classic case of decision trees and random forests, which tend to output probabilities close to 0 or 1 even when they have no reason to. The accuracy or the ability to rank predictions of the base remain acceptable: the problem is not the classification, it is only the number attached to the prediction.
The paper proves that the total log-loss of OPS over T examples exceeds that of the best fixed
Platt mapping chosen in hindsight by an amount that grows at most as log T. In practice:
online, without knowing what comes next, OPS does almost as well as the best calibration that
could have been chosen knowing the future. The guarantee is on the log-loss, so the accuracy
may vary slightly. The theorem holds under two conditions (with the base probabilities clipped
to [0.01, 0.99], as the clip parameter does, and comparing against mappings with
a² + b² ≤ B²). OPS does not guarantee that the probabilities improve, nor that the log-loss
decreases: both depend on the base and on the data. OPS guarantees that its log-loss stays
close to the best fixed Platt mapping in hindsight, which is already a strong result for an
online method.
Parameters¶
classifier The base classifier whose probabilities are to be recalibrated. It must expose predict_proba_one and learn_one. gamma Parameter of the Online Newton Step: the Newton step is scaled by 1/gamma. The paper uses 0.1. rho Regularization of the ONS Hessian: it starts from rho · I. The paper uses 100. radius Radius of the l2 ball onto which (a, b) is projected after each step. The paper uses 100. clip The base probabilities are clipped between clip and 1 - clip before the logit. The paper assumes f(x) in [0.01, 0.99].
Examples¶
import numpy as np from dense_armor.utility.learn.calibration import OnlinePlattScaling from dense_armor.utility.learn.online_classifiers import OnlineGaussianNB from dense_armor.utility.metrics import LogLoss
rng = np.random.default_rng(0) base = OnlineGaussianNB() cal = OnlinePlattScaling(OnlineGaussianNB()) base_loss, cal_loss = LogLoss(), LogLoss() for _ in range(1500): ... x = float(rng.uniform(-1.0, 1.0)) ... y = int(x > 0.0) ... p_base = base.predict_proba_one({"x": x}).get(1, 0.5) ... p_cal = cal.predict_proba_one({"x": x}).get(1, 0.5) ... _ = base_loss.update(y, p_base) ... _ = cal_loss.update(y, p_cal) ... _ = base.learn_one({"x": x}, y) ... _ = cal.learn_one({"x": x}, y) round(base_loss.get(), 4), round(cal_loss.get(), 4) (0.1146, 0.0417)
References¶
Source code in dense_armor/utility/learn/calibration.py
Details¶
The comparison against the paper's tracking variant (TOPS) and a JSD/ensemble variant on seven streams is in Dense-Evolution-Discovery, the corresponding online Platt scaling page of that library.
This is the same Online Newton Step as the paper's Algorithm 1 with the same hyperparameters; no tuning was done on Phishing or on any of the other streams. The 0.3499 is what the paper's algorithm gives on this dataset, not a number that was fished for.
While the base model has no opinion yet (a tree returns an empty probability dict before it has
seen a class), the calibration is not updated: the sample goes to the base model only, so
(a, b) is never fitted on a probability the base model did not give.