JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Master Class: Calibration, Robustness, and Squeezing Out Every Point

A deployed classifier must not only be right — it must know when it might be wrong, handle the unexpected, and earn its last fraction of a percent.

What 'confident' should mean: calibration

Across this track you have learned to turn pixels into a label, to score a classifier honestly, to train one that generalizes, and to handle the hard cases. This final master-class is about the difference between a model that is merely right and one that is trustworthy. A deployed classifier reports not just a label but a number alongside it — 'cat, 0.99'. The first question of the master tier is: when the model says 0.99, is it actually right 99 times out of 100?

Surprisingly often the answer is no. Modern deep networks are highly accurate but systematically overconfident: when they output 99% they may be right only ~90% of the time. The number is inflated. This is the problem of calibration — making the reported confidence match the true probability of being correct. We can fix it cheaply with temperature scaling / calibration, which the next section covers, but first we have to see why miscalibration matters and how to measure it.

Calibration matters wherever the confidence number drives a decision rather than just decorating it. In medical triage, a 0.6 versus 0.95 cancer score changes whether a human radiologist is paged. In autonomous braking, the confidence that 'this blob is a pedestrian' decides whether the car stops. In any system that defers to a human when unsure, the threshold for deferral is meaningless if the confidences themselves are lies. An overconfident model never asks for help — and that is exactly when it is most dangerous.

We measure calibration with a reliability diagram. The recipe is simple: take all the predictions on a held-out set, sort them by their confidence, and drop them into buckets — say 0.0–0.1, 0.1–0.2, …, 0.9–1.0. For each bucket, compute two things: the average confidence the model claimed, and the actual accuracy it achieved on those examples. Plot accuracy (vertical) against confidence (horizontal). Perfect calibration is the 45-degree diagonal: in the bucket where the model said '0.8', it gets 80% right. A curve sagging below the diagonal means overconfidence — the usual disease of deep nets.

\mathrm{ECE}=\sum_{m=1}^{M}\frac{|B_m|}{N}\,\bigl|\,\mathrm{acc}(B_m)-\mathrm{conf}(B_m)\,\bigr|

Expected Calibration Error: the average gap between confidence and accuracy across bins.

This formula, the Expected Calibration Error (ECE), just turns the reliability diagram into a single number: the average vertical gap between the curve and the diagonal. Symbol by symbol: we split the N total predictions into M confidence bins; B_m is the set of predictions landing in bin m, and |B_m| is how many there are, so |B_m|/N is the fraction of all predictions in that bin (its weight). \mathrm{acc}(B_m) is the real accuracy on that bin, \mathrm{conf}(B_m) is the average confidence the model claimed there, and |\mathrm{acc}-\mathrm{conf}| is how far reality drifted from the claim. We weight each gap by the bin's size and add them up. A tiny worked example: suppose one bin holds 100 of 1000 predictions, the model claimed 0.95 average confidence but only got 85% right — that bin contributes (100/1000)\times|0.85-0.95| = 0.1\times0.10 = 0.01 to the ECE. Sum such terms over all bins; 0 means perfectly calibrated, and larger means the confidences are more out of touch with reality.

Temperature scaling: a one-knob fix

The good news is that overconfidence is usually fixable with a single number. Temperature scaling / calibration is the simplest and, empirically, the most effective post-hoc calibration method — 'post-hoc' meaning you apply it after training, without retraining the network. You divide all of the model's logits (the raw pre-softmax scores) by one learned scalar T before taking the softmax, and you choose T by minimizing calibration error on the validation set. One knob, fit once.

p_i=\frac{\exp(z_i/T)}{\sum_{j}\exp(z_j/T)}

The temperature-scaled softmax: every logit is divided by the same T before normalizing.

Reading it symbol by symbol: z_i is the logit for class i (the network's raw score before softmax), and T>0 is the single temperature shared across all classes. Dividing every logit by the same T uniformly shrinks or stretches the gaps between them. When T>1 we 'cool' the distribution: the gaps shrink, so after softmax the probabilities are flatter and the top confidence drops — the model becomes more humble. When T<1 we 'heat'/sharpen it: gaps grow, the distribution gets peakier, confidence rises. At exactly T=1 we recover the ordinary softmax. The denominator \sum_j \exp(z_j/T) is just the normalizer that makes the p_i sum to 1, as always.

A concrete walk-through. Suppose three logits z=[3,\,1,\,0]. At T=1 the softmax is about [0.84,\,0.11,\,0.04] — the model claims 84% on the top class. Now set T=2, so the scaled logits are [1.5,\,0.5,\,0] and the softmax becomes about [0.63,\,0.23,\,0.14] — the top class has dropped to 63%. The mass spread out; the model got more cautious. If validation said this model was right only ~65% of the time when it claimed 84%, then T=2 has made its confidence honest.

import numpy as np

def softmax_T(logits, T):
    z = np.asarray(logits) / T          # divide every logit by the temperature
    z = z - z.max()                      # numerical stability (does not change result)
    e = np.exp(z)
    return e / e.sum()

z = [3.0, 1.0, 0.0]
print(softmax_T(z, T=1))   # ~ [0.844, 0.114, 0.042]  -> 84% on class 0
print(softmax_T(z, T=2))   # ~ [0.629, 0.231, 0.140]  -> 63% on class 0

# argmax is class 0 in BOTH cases -> accuracy is untouched, only confidence changed
print(np.argmax(softmax_T(z, 1)), np.argmax(softmax_T(z, 2)))   # 0 0

# Fitting T: scan a small grid (or use gradient descent) to minimise
# validation NLL / ECE while keeping the model's weights frozen.
Temperature scaling in a few lines — and a check that the argmax never moves.

Test-time augmentation: vote with yourself

Here is the first technique for squeezing out extra accuracy. Test-time augmentation (TTA) is delightfully simple: at inference, instead of pushing the single original image through the model once, you create several augmented views of the same image — a horizontal flip, a couple of crops, maybe a small zoom — run each through the network, and average their predicted probability vectors before taking the argmax.

\bar{p}=\frac{1}{A}\sum_{a=1}^{A} f\bigl(t_a(x)\bigr)

TTA: average the softmax outputs over A augmented views, then argmax.

Symbol by symbol: x is the test image; t_a is the a-th augmentation (say, a flip or a particular crop), so t_a(x) is one transformed view; f(\cdot) is the network's softmax output, a full probability vector over the classes; and A is how many views you use. We sum the A probability vectors and divide by A to get the average \bar{p}, on which we then take the argmax. The intuition is the wisdom of crowds applied to one model: each view is a slightly noisy opinion, the noise points in different directions for different views, and averaging cancels much of it while the true signal — which class this really is — survives. More views reduce the variance of the estimate, but with diminishing returns: going from 1 to 4 views helps a lot, 4 to 16 much less.

The same image rendered as several augmented views. At training time these teach invariance; at test time we average the model's vote across them.

One source image transformed into several variants — a horizontal flip, two crops, and a color jitter — arranged side by side to illustrate augmentation.

A worked 3-view example. Suppose for a borderline image the three views produce softmax vectors [0.55,\,0.35,\,0.10], [0.45,\,0.45,\,0.10], and [0.62,\,0.28,\,0.10] over classes A, B, C. The middle view is a near-tie between A and B and could flip to the wrong label on its own. Averaging gives [(0.55{+}0.45{+}0.62)/3,\,(0.35{+}0.45{+}0.28)/3,\,(0.10{+}0.10{+}0.10)/3]=[0.54,\,0.36,\,0.10] — a clearer, steadier vote for A. Note this is exactly the same family of transforms you saw in Guide 3, but used for the opposite purpose: there, random augmentation during training taught the model to be invariant; here, deterministic augmentation during inference harvests that invariance to stabilize each prediction and nudge top-k accuracy upward.

Ensembling: many heads are better than one

Where TTA averages over views of one model, model ensembling for classification averages over several different models. You train M classifiers independently, run all of them on the test image, and average their predicted probabilities before the argmax. Ensembling is the single most reliable way to gain that last fraction of a percent — it is why competition leaderboards are dominated by ensembles.

Why does it work? Recall the bias–variance view from earlier ML tracks. A single trained model makes two kinds of error: a bias part (a systematic blind spot baked into the model class) and a variance part (jitter from this particular training run — the random seed, the data order, the augmentation draws). If you train several models that make uncorrelated variance errors, then when you average their predictions those errors point in random directions and largely cancel, while the shared signal — the part they all agree on, which is usually the correct answer — adds up. You can't average away bias, but you can crush variance.

Total error splits into bias and variance. Ensembling attacks the variance term: averaging diverse, independently-trained models shrinks it.

A decomposition diagram showing total error as the sum of a bias component and a variance component, with averaging reducing the variance part.

p_{\text{ens}}=\frac{1}{M}\sum_{m=1}^{M} p^{(m)}

Ensemble prediction: the simple average of M models' probability vectors.

Symbol by symbol: p^{(m)} is the probability vector from model m, M is the number of models, and p_{\text{ens}} is their simple average, on which we take the final argmax. A worked 3-model example over classes A, B, C: model 1 says [0.6,\,0.3,\,0.1], model 2 says [0.5,\,0.2,\,0.3], model 3 says [0.8,\,0.1,\,0.1]. The average is [0.63,\,0.20,\,0.17] — all three already favored A, and the ensemble is both correct and steadier. The math behind the gain: if each model's error is roughly independent with variance \sigma^2, then the variance of the average of M of them shrinks toward \sigma^2/M. With M=3 that is a third of the wobble — but only if the errors are independent. If you clone the same model three times the errors are perfectly correlated and you gain nothing, which is why diversity (different seeds, architectures, augmentation recipes, data subsets) is what makes the gain real. As a bonus, ensembles are usually better calibrated too, since averaging tempers any single model's overconfidence and tends to nudge top-k accuracy up.

Linear probing: measuring what a backbone learned

Step back from accuracy-chasing for a moment to ask a deeper question: how good are the features a network has learned, separate from its final classifier? Linear probing answers this. You take a pretrained feature extractor — the backbone — freeze all its weights so it can no longer learn, and train only a single linear classifier on top of the features it produces. Nothing else moves.

z = W\,h + b, \qquad h = g(x)\;\text{(frozen)}

A linear head on top of frozen backbone features.

Symbol by symbol: x is the input image; g(\cdot) is the frozen backbone, which maps the image to a feature vector h=g(x) (a list of, say, 2048 numbers summarizing the image); W and b are the weight matrix and bias of the linear head — and they are the only trainable parameters; and z are the resulting logits, fed to softmax as usual. Because g is frozen, training optimizes nothing but the linear map from features to classes. That is the whole point: if such a tiny linear layer can already separate the classes well, the features h must already carry the relevant structure — the backbone did the hard work, and a hyperplane is enough to finish.

A linear classifier draws a single hyperplane in feature space. Linear probing asks: are the classes already on opposite sides of such a plane?

Points of two classes in a feature space separated by a straight line, illustrating a linear decision boundary.

Linear probing plays two roles. First, it is a clean diagnostic of representation quality, which is how the field evaluates self-supervised and foundation models: a model trained with no labels at all can still be judged by freezing it and seeing whether a linear probe separates classes — high probe accuracy means the unsupervised pretraining learned genuinely useful structure. Second, it is a cheap, fast transfer method to a new label set. Imagine a backbone pretrained on ImageNet's 1000 classes that you want to repurpose for 10 medical categories: freeze it, attach a 10\times2048 head (W) plus 10 biases — about 20{,}500 trainable parameters versus the millions in the backbone — and train it in minutes on a small dataset. The geometric picture is exactly the image classification decision boundary you have built up all track: the linear head simply draws a hyperplane through the frozen feature space, and 'good features' means the classes already sit cleanly on opposite sides of such planes. Contrast this with full fine-tuning, where you unfreeze g too: that has far more capacity and usually reaches higher accuracy, but it needs much more data and compute and risks overfitting the small new set or forgetting what the backbone knew. Linear probing first, fine-tune only if you must.

Knowing what you don't know: open-set and OOD

Now we confront, head-on, the closed-world assumption baked into everything since Guide 1. Your classifier was trained on K known classes and softmax is built to output a probability over exactly those K — it cannot return anything else. But in deployment the model will meet inputs from classes it never saw: a species not in the dataset, a corrupted sensor frame, a photo of something entirely off-topic. The model still must answer, so it forces the input into one of its K boxes — and, being overconfident, often does so loudly. A digit classifier shown a photo of a cat will confidently declare it a '7'.

The cure is to give the model the option to say 'none of the above'. This is open-set / out-of-distribution classification: alongside its best guess, the system must be able to abstain when the input doesn't look like anything it knows. The simplest baseline is the maximum softmax probability detector: reject (flag as unknown) whenever the top confidence falls below a threshold. And here the previous sections pay off — this only works if confidences are trustworthy, so temperature scaling / calibration is a prerequisite, not a luxury. With miscalibrated, inflated confidences even genuine out-of-distribution inputs sail past the threshold.

\hat{y}=\begin{cases}\displaystyle\arg\max_i\,p_i & \text{if } \displaystyle\max_i\,p_i \ge \tau\\[4pt] \text{``unknown''} & \text{otherwise}\end{cases}

The abstain rule: commit to the top class only if its confidence clears the threshold.

Symbol by symbol: p_i is the (ideally calibrated) probability for class i; \max_i p_i is the model's top confidence — how sure it is about its single best guess; and \tau (tau) is a rejection threshold you tune on validation data. The rule says: if the top confidence clears \tau, output the argmax class as usual; otherwise output 'unknown' and let the system handle it (defer to a human, ask for another sensor reading, fall back to a safe default). A worked example: train on cats, dogs, birds; set \tau=0.7. A clear cat photo gives calibrated p=[0.93,\,0.05,\,0.02], so \max=0.93\ge0.7 → predict 'cat'. Now show it a car: with calibrated confidences the model spreads its mass, say p=[0.41,\,0.33,\,0.26], so \max=0.41<0.7 → 'unknown', correctly refusing to force the car into a known animal class.

A deployment checklist

Let's tie the whole track together. Shipping a classifier is not the moment you hit the highest validation number — it is the moment you can answer, honestly, a short list of questions. Each item below is a question to ask before you deploy, and each points back to a guide in this track. Treat it as the pre-flight checklist for a master-level classifier.

  1. Does my metric match the real goal? Re-read Guides 2 and 4: accuracy alone lies under class imbalance — choose precision/recall, per-class metrics, or a cost-weighted score that reflects what actually matters in your application.
  2. Did I train with a regularizing recipe? From Guide 3: augmentation, weight decay, early stopping, label smoothing / mixup — so the model generalizes and starts out less overconfident.
  3. Are my confidences honest? Fit temperature scaling on validation and check the reliability diagram / ECE. If anything downstream reads the confidence number, this is non-negotiable.
  4. What's my accuracy-vs-latency trade-off? Decide whether test-time augmentation and/or ensembling fit your latency and memory budget — and if not, reach for snapshot ensembles or weight averaging.
  5. Can the model say 'I don't know'? Set an abstain threshold \tau for open-set / out-of-distribution inputs, validated on held-out unknowns, so the system defers instead of guessing confidently on the unexpected.
  6. How will I notice the world moving? Monitor input statistics and live accuracy for distribution shift over time, and schedule retraining — a model frozen in the past silently rots as reality drifts away from its training data.

Notice the through-line connecting these checks. Guides 1–4 made the model right; this guide makes it honest about its uncertainty (calibration), squeezed for every safe point (TTA and ensembling), and humble about the unknown (open-set abstention and shift monitoring). Those three properties — right, honest, humble — are what separate a leaderboard model from one you would actually trust to make a decision in the world.