TAI BUI
Back to curriculum
CapstonePython

Perplexity and Calibration

Perplexity is how plausible the model finds held-out text. Calibration is whether its confidence matches how often it is right. A model that says 90 percent on a thousand answers and gets six hundred right is overconfident, and an accuracy number alone will not show it.

TIME: ~90 minPREREQUISITES: Phase 19 Track B foundations, lessons 70 and 71

Learning Objectives

  • Compute corpus perplexity on a held-out corpus from per-sequence summed negative log-likelihoods and token counts supplied by the model adapter.
  • Compute the expected calibration error (ECE) of a classifier or multiple-choice eval from binned predicted probabilities.
  • Compute the Brier score (mean squared error against the indicator of correctness) and explain when it does what ECE does not.
  • Build the reliability diagram data needed to plot a confidence-versus-accuracy curve.
  • Bundle ECE, Brier, and reliability data into a `CalibrationReport`, and keep perplexity in a separate `PerplexityResult`, so a calling runner can report all three numbers.

What perplexity tells you

Perplexity is the exponentiated average negative log-likelihood per token. Lower is better. A perplexity of one means the model assigns probability one to every actual token. A perplexity equal to the vocabulary size means the model is uniform and learned nothing. Real numbers fall in between: a strong 2026 base model on WikiText-103 sits around eight to twelve, and a bad one on the same text sits at fifty plus.

The harness does not compute log-probabilities itself, because those come from the model adapter. What the harness does is aggregate. It takes a list of per-sequence summed negative log-likelihoods and a matching list of token counts per sequence, and returns corpus perplexity.

The core arithmetic is below. In main.py, perplexity delegates to PerplexityResult.from_token_nll, which adds the validation.

python
def perplexity(neg_log_probs, token_counts):
    total_nll = sum(neg_log_probs)
    total_tokens = sum(token_counts)
    return math.exp(total_nll / total_tokens)

The implementation returns NaN when the total token count is zero, and from_token_nll raises a ValueError if any negative log-likelihood is negative (or if the two lists differ in length). The mistake I'd check for first is a forgotten negation. An adapter that returns log p instead of -log p produces a perplexity below one, which is impossible, so the function rejects it as a contract violation.

Check: What perplexity tells you0/7
1. What is perplexity?Define
2. In this harness, what is the negative log-likelihood the adapter supplies for one sequence?Define
3. A model gives every token in a vocabulary of V tokens the same probability. What is its perplexity, and what does that mean?Explain
4. Why is a perplexity below one impossible?Explain
5. Two sequences have summed NLLs of 30 and 70 and token counts of 10 and 40. What does perplexity return for them?Apply
6. What does perplexity([0.0], [10]) return?Apply
7. An adapter returns log p instead of -log p for every sequence. What happens when the values go into `PerplexityResult.from_token_nll`?Apply

What ECE measures

Expected calibration error (ECE) is the average gap between confidence and accuracy, taken over bins of predictions and weighted by bin size. It groups predictions by confidence into a fixed number of bins, then compares each bin's mean confidence to its observed accuracy.

Mermaid diagram

The standard formulation uses ten equal-width bins on [0, 1]. The implementation accepts any positive integer count, and the bins parameter (default 10) lets the runner pick a different count.

ECE is biased by bin count and sample size. With ten bins and a hundred predictions, a 0.02 ECE is hard to tell apart from noise. So the implementation returns the number of populated bins alongside the ECE, so the runner can decline to report a single number on too few samples.

Check: What ECE measures0/7
1. What does expected calibration error measure?Define
2. What does it mean for a model to be calibrated?Define
3. What does the populated-bins count returned beside ECE tell the runner?Define
4. Why does the lesson suggest reading the reliability arrays before trusting the ECE number?Explain
5. Ten predictions each have confidence 0.9, and six are correct. With ten bins, what does `expected_calibration_error` return?Apply
6. A runner computes ECE 0.02 from 100 predictions, and `populated_bins` is 3. What should it do?Analyze
7. Your eval has only 40 questions, so ECE with ten bins will be sparse. Which reporting design fits the lesson's ideas?Design

What Brier score does that ECE does not

The Brier score is the mean squared error between predicted probability and the actual outcome, scored per prediction. For binary outcomes it is mean((p_i - y_i)^2).

ECE compares each bin's mean confidence to its accuracy. Gaps in different bins add up as absolute values, so they never cancel each other. Inside a single bin, though, overconfident and underconfident predictions average out, and the bin can look calibrated. Brier scores every prediction against its outcome, so that spread shows up directly in the number.

Brier decomposes into reliability, resolution, and uncertainty. main.py provides both the score and brier_decomposition, which returns a dict with reliability, resolution, uncertainty, and brier. The decomposition is a standalone function: CalibrationReport does not include it, and main.py has no runner, so whatever calls the harness decides whether to log it.

python
def brier_score(confidences, correct):
    conf = np.asarray(confidences, dtype=np.float64)
    corr = np.asarray(correct, dtype=np.float64)
    _validate_probs(conf, corr)
    if conf.size == 0:
        return 0.0
    return float(np.mean((conf - corr) ** 2))
Check: What Brier score does that ECE does not0/7
1. What is the Brier score?Define
2. What does the Brier decomposition do?Define
3. How does what ECE compares differ from what the Brier score compares?Explain
4. Why can gaps in different bins not cancel each other in ECE?Explain
5. Ten predictions each have confidence 0.9, and six are correct. What does `brier_score` return?Apply
6. In one confidence bin, half the predictions are far too confident and half far too unsure, and they average out to the bin's accuracy. Which metric still reacts?Analyze
7. A predictor says 0.5 on every question and gets exactly half right, so its ECE is 0. Which number shows it is still not a strong predictor?Analyze

Reliability diagram data

A reliability diagram plots predicted confidence against empirical accuracy in each bin, and the diagonal marks perfect calibration. The function returns three arrays: per-bin average confidence, per-bin average accuracy, and per-bin count. The plotting code lives downstream, so the code here stops at the data shape.

Mermaid diagram

That tuple is what a calling layer needs to draw the plot or to compute a custom ECE variant (adaptive ECE, sweep ECE, and so on). The arrays come back as numpy arrays so downstream code skips the conversion.

Check: Reliability diagram data0/4
1. Which three arrays does the reliability diagram function return?Define
2. A reliability diagram puts confidence on the horizontal axis and accuracy on the vertical axis, with the diagonal marking perfect calibration. A bin's point sits well below the diagonal. What does that say about the bin?Explain
3. Confidences [0.05, 0.15, 0.85, 0.95] have correctness [0, 0, 1, 1]. With ten bins, what are the bin_count values of `reliability_diagram`?Apply
4. In the synthetic underconfident predictor, confidences lie in [0, 0.3] and each prediction is correct with probability 0.5 plus its confidence. On a reliability diagram with confidence on the horizontal axis, where do its populated bins sit relative to the diagonal?Analyze

Confidence sources

Confidence is any number in [0, 1] attached to a prediction, and the harness does not assume it comes from softmax. For multiple-choice tasks the natural choice is a softmax over option log-likelihoods. For free-text it is the model's self-reported probability or the exponential of the average log-likelihood. The eval consumes the number as given, and producing it is the adapter's job.

Check: Confidence sources0/4
1. In this harness, what is a confidence?Define
2. Where does the confidence for a free-text task come from?Explain
3. An ECE call fails with "confidences must lie in [0, 1]". What is the most likely cause?Analyze
4. A new free-text adapter will report confidence as exp(average log-likelihood) of its answer. Which change does the harness need?Design

Edge cases

  • All predictions wrong: ECE is the average confidence, Brier is high, and perplexity is whatever the model thinks of the text.
  • All predictions correct with high confidence: ECE and Brier both sit near zero.
  • A perfectly uncertain predictor at p=0.5: ECE is abs(0.5 - accuracy), and Brier is exactly 0.25.
  • Empty input: ECE returns (0.0, 0), Brier returns 0.0, and reliability returns zero-filled arrays. Perplexity returns NaN for the zero-token case. None of these paths emit a warning. The runner inspects the values and decides whether to report or skip.

The tests pin these cases. A real model on a real benchmark is unlikely to hit them, but a buggy adapter or a tiny sample can, and the runner should survive that.

Check: Edge cases0/4
1. What does the perplexity function return when the total token count is zero?Apply
2. A predictor outputs 0.5 on every prediction and exactly half are correct. What is its Brier score?Apply
3. In a test, four predictions at confidence 0.05 are all wrong and four at 0.95 are all correct. What ECE results?Apply
4. Four predictions at confidence 0.99 are all wrong. What ECE does the harness return?Apply

Dispatch

Calibration is a per-model report, where F1 is a per-task metric. The calling runner (which lives outside main.py) accumulates (confidence, correct) pairs across the whole eval and computes ECE, Brier, and reliability data once. Perplexity runs over a held-out text corpus, separate from the task-by-task scoring.

The interface is:

python
report = CalibrationReport.from_predictions(confidences, correct)
report.ece          # float
report.brier        # float
report.reliability  # tuple of three numpy arrays
report.populated_bins  # int

PerplexityResult.from_token_nll(neg_log_probs, token_counts) returns the perplexity, the average negative log-likelihood per token, and the total token count. CalibrationReport has no perplexity field, so the two results travel separately.

Check: Dispatch0/3
1. What does a CalibrationReport hold?Define
2. ECE is biased by sample size and bin count. Given that, why does the runner pool (confidence, correct) pairs over the whole eval instead of scoring calibration per task?Explain
3. Why are perplexity and the calibration numbers returned in separate result objects?Explain

What this lesson does not do

The code does not call a model and does not implement softmax. It does not estimate confidence from output tokens, since that is the adapter's job. Temperature scaling and Platt scaling are post-hoc fixes and belong in a different lesson. The goal here is narrower: make the three numbers (perplexity, ECE, Brier) trustworthy and reproducible.

Check: What this lesson does not do0/1
1. Which job sits outside this harness and belongs to the model adapter?Explain

How to read the code

main.py defines perplexity, expected_calibration_error, brier_score, brier_decomposition, reliability_diagram, and the CalibrationReport / PerplexityResult dataclasses. The demo runs on synthetic predictions where the ground truth is known: a well-calibrated model, an overconfident one, and an underconfident one. The tests in tests/test_calibration.py pin the edge cases plus reference values for the synthetic predictors.

Read main.py top to bottom. Functions run from scalar to vector to report. The functions carry no docstrings (only the module has one), so the math and the contract live in this lesson.

Key Terms

TermWhat it means
PerplexityThe exponentiated average negative log-likelihood per token; lower is better, and one is a perfect model
Negative log-likelihood (NLL)The summed -log p of the actual tokens, supplied per sequence by the model adapter
Expected calibration error (ECE)The average gap between confidence and accuracy over bins of predictions, weighted by bin size
CalibrationAgreement between a model's stated confidence and how often it is actually correct
Brier scoreMean squared error between predicted probability and the outcome, mean((p_i - y_i)^2)
Brier decompositionSplitting the Brier score into reliability, resolution, and uncertainty terms
Reliability diagramA plot of per-bin confidence against per-bin accuracy, where the diagonal marks perfect calibration
Populated binsThe count of bins that contain at least one prediction, returned beside ECE so a runner can skip thin samples
ConfidenceAny number in [0, 1] attached to a prediction; the adapter produces it, and the harness consumes it as given
CalibrationReportThe bundle of ECE, Brier, reliability data, and populated-bin count; perplexity travels separately in PerplexityResult
Author's Note

I'd read the reliability arrays before I trusted the ECE scalar. On a small sample the number can be mostly noise, and the per-bin counts show how thin each bin is. The ECE function returns the populated-bin count next to the value so the runner can decide whether the number is worth reporting, and I'd want that check in place before any report goes out.

I'd also keep ECE and Brier side by side instead of picking a favorite. ECE compares confidence to accuracy within bins, and Brier scores each prediction against its outcome, so they answer different questions. Before pointing the harness at a real model, I'd run the demo's calibrated, overconfident, and underconfident predictors and see what each number looks like when the ground truth is known.

Going further

Calibration is one of the least-reported axes in published eval. Many leaderboards report a single accuracy number and stop there. A model that wins on accuracy but loses on Brier can be a worse production deployment than one that scores a few points lower on accuracy and reports its uncertainty reliably. With the calibration plumbing in place, add temperature scaling on a held-out validation slice, recompute ECE, and watch the gap shrink. That is a separate lesson, but the floor lives here.

Warm-up0/3
1. A forecaster says "90% chance of rain" on 100 different days, and it rains on 60 of them. How would you describe the forecaster?Warm-up
2. For a probability p strictly between 0 and 1, what is true of -log(p)?Warm-up
3. A classifier gets 70 of 100 questions right. Which extra data would show whether its confidence scores can be trusted?Warm-up
Check: Going further0/1
1. Model A beats model B by three accuracy points but has a worse Brier score. Your product shows users the model's confidence. Which choice fits the lesson?Design

Code