Evaluation & Analytics

Evaluating Generative Models
LLM · VLM · Diffusion

Classification has an answer key. Generation has a taste problem — and here is the math that turns taste into numbers you can trust: pass@k derived from scratch, Bradley-Terry leaderboards, FID by hand, judge-bias calibration, and the discipline that keeps a 58%-vs-42% headline from being a lie.

Prerequisites: Metric Design (what to measure). For the interval/bootstrap/p-value machinery used informally here, see The Statistics of Evaluation. That's it.
11
Chapters
11
Live Simulations
0
Assumed Knowledge

Chapter 0: No Ground Truth — The Taste Problem

Here are two poems, each written by a different model. One begins "The frost writes cursive on the pane;" the other, "Cold glass. A ghost of breath. Then gone." Which is better?

You have an opinion. Now let me ask the question that unravels the whole field: would your opinion survive me swapping which poem you read first? For a lot of readers, on a lot of pairs, it would not — and that instability is not a quirk. It is the central obstacle this entire lesson exists to defeat.

Start with the contrast the whole Evaluation & Analytics family hinges on. A classifier emits an output that is right or wrong against a label — "cat" versus the truth "dog," a crisp checkmark or cross. A generative model emits a poem, an image, a code diff, a caption — and there is no label. There is no single string that is the correct poem. Evaluation stops being comparison to a key and becomes measurement of preference, and preference is a noisy, biased, human (or model) signal.

Enumerate exactly what breaks the moment the answer key disappears. Accuracy is undefined — there is nothing to be accurate against. There are many valid outputs — a caption, a proof, a refactor can each be correct in a thousand different ways. Quality is multi-axis — fluency competes with faithfulness competes with style, and a single scalar cannot see all three. And most treacherous: the measuring instrument itself is biased — the human rater, or the LLM judge you hire to replace them, brings its own systematic errors to every reading.

So this lesson does not teach one metric. It teaches three measurement families, and the map is worth memorizing because every generative-eval problem you ever face routes into one of them:

When you have…FamilyInstrumentChapter
a verifier (unit tests, checkable answer)Execution / verifierpass@kCh2
only pairwise comparisonsPreference aggregationBradley-Terry / Elo + judge calibrationCh3–4, 7
only samples (no prompts, no votes)Distribution matchingFID / precision-recallCh5

The three families, indexed by what evidence you actually have. Chapter 8 runs them against a rigged judge and rescues the leaderboard.

Now watch the instability concretely, because it is the reason the discipline exists. Suppose ten raters are truly indifferent between two poems — a genuine 50/50 tie — but each has a mild habit of favoring whatever they read first. Show poem A on top and A "wins." Show A on the bottom and A "loses." The leaderboard is an artifact of the page layout, not of the poetry. Play with the simulation below: drag the bias up, run ten raters, then swap the order and watch the verdict flip while the hidden true quality (the dashed line at 50/50) never moves an inch.

The Order-Flip Illusion

Ten simulated raters vote between two equal-quality outputs. Each rater has a first-position bias you control. Run them, then hit Swap order & re-run — the verdict banner flips even though the true quality (dashed line) stays pinned at 50/50. Randomize per rater shows the cure.

First-position bias +15pp

Name the discipline that this lesson is really about. Every number here arrives with a triple: (a) an estimator — the formula that turns raw observations into the quantity you care about; (b) a bias analysis of that estimator — where it is systematically wrong; and (c) an uncertainty quantification — the error bar. Vibes with extra steps have none of the three. Evaluation has all three.

Misconception: "More raters fixes it." You might think averaging over 100 or 1000 raters washes the order effect out. But bias is not variance. Every added rater carries the same +15pp tilt, so the sample mean converges more and more confidently to the wrong number (0.65, not the true 0.50). Only randomizing the order — a design fix, not a sample-size fix — removes it. This distinction is the first law of the whole lesson: bias is a design problem; variance is a budget problem; you fix them in that order.

Position this lesson against its siblings so you always know which door to open. ai-evaluation owns the harness process — datasets, regression suites, dashboards, judge deployment. agent-evaluation owns agent benchmarks and trajectory grading. cs336-12-evaluation owns perplexity and benchmark contamination mechanics. This lesson owns the math — the estimators themselves, and their biases, and their error bars.

Let us make the poem question answerable with real numbers — a worked example you can carry into an interview. Two models of equal true quality: each rater truly prefers A with probability 0.50. Each of 10 raters has a +15 percentage-point first-position bias, so P(vote for whichever is shown first) = 0.65. What verdict does each fixed ordering produce?

A shown first → P(vote A) = 0.50 + 0.15 = 0.65
expected A votes = 10 × 0.65 = 6.5
A shown second → P(vote A) = 0.50 − 0.15 = 0.35
expected A votes = 10 × 0.35 = 3.5

Expected votes are not the verdict — the verdict is who gets a strict majority (≥ 6 of 10). For that we need the binomial tail. With A shown first, each vote is a coin with p = 0.65, and P(X ≥ 6) for X ~ Binomial(10, 0.65):

P(A wins | A first) = P(X ≥ 6),  X ~ Binomial(10, 0.65) = 0.7515

About three runs in four crown A. Flip the layout: A shown second means each vote is a coin with p = 0.35, and the same tail becomes:

P(A wins | A second) = P(X ≥ 6),  X ~ Binomial(10, 0.35) = 0.0949

Same two models. Same ten raters. Opposite verdicts — a 65-point swing (75.1% vs 9.5%) produced entirely by which poem sat on top. And here is the fix, previewed now and fully derived in Chapter 4: randomize the order per rater. Then

P(vote A) = 0.5 × 0.65 + 0.5 × 0.35 = 0.325 + 0.175 = 0.50

The +0.15 and −0.15 terms cancel exactly. The measured rate recovers the true 50/50. No estimate of the bias was ever needed — the design killed it. Let's confirm every number in code.

python
# From scratch: simulate 1000 panels of 10 biased raters under each fixed
# layout, and under randomized order. Bias does NOT average out; design does.
import numpy as np
rng = np.random.default_rng(0)

def panel_verdict(true_pref, bias, layout, n_raters=10):
    if layout == "A_first":   p = true_pref + bias
    elif layout == "A_second": p = true_pref - bias
    else:  # randomize order per rater
        first = rng.random(n_raters) < 0.5
        p = np.where(first, true_pref + bias, true_pref - bias)
    votes_A = (rng.random(n_raters) < p).sum()
    return votes_A >= 6          # A wins a strict majority of 10

for layout in ["A_first", "A_second", "rand"]:
    rate = np.mean([panel_verdict(0.50, 0.15, layout) for _ in range(1000)])
    print(layout, round(float(rate), 3))
# A_first ~0.75   A_second ~0.09   rand ~0.30 (near a coin-flip tie)
python
# Library: the exact majority probabilities, no simulation needed.
from scipy import stats
print(round(1 - stats.binom.cdf(5, 10, 0.65), 4))   # 0.7515  (A first)
print(round(1 - stats.binom.cdf(5, 10, 0.35), 4))   # 0.0949  (A second)
print(0.5*0.65 + 0.5*0.35)                       # 0.5     (randomized: bias cancels)
Interview lens. "Your team ran a 500-vote human preference study; model A won 58% to 42%. The PM wants to ship. What is the FIRST question you ask?" The first question is about the measurement design, not the sample size: was presentation order — and every identifying cue: model name, formatting, response speed — randomized per vote? A 58/42 split is well within the swing a modest position or formatting bias produces with zero true quality difference, and 500 votes make a biased design look more convincing, not less. A strong answer then asks for the per-order breakdown (win rate when A was first versus second) as a built-in bias audit, and only then discusses confidence intervals. Distinguishing bias (design) from variance (sample size) unprompted is the staff-level tell.

The payoff is set. By Chapter 8 you will run a simulated judging pipeline with tunable biases, watch a leaderboard invert under a systematic tilt, and repair it live with the exact techniques derived along the way. The poem question is not unanswerable — it is answerable at the population level, with randomization, aggregation models, and agreement statistics. Taste becomes a measurable quantity with error bars.

Two equal models are compared by raters who slightly favor whichever output appears first. What happens as you add more raters with A always shown first?

We opened with a poem and no answer key. The most tempting "objective" score for a generative model is not a preference vote at all — it is likelihood: "how probable does the model find good data?" That is perplexity, and it is where the naive intuition goes to die. Next chapter.

Chapter 1: Likelihood Is Not Quality

The most natural score for a generative model seems to sidestep the taste problem entirely. Instead of asking humans "which is better," ask the model itself: how probable does it find good, human-written data? A model that assigns high probability to good text must be a good model of language — right? So why does the lowest-perplexity sampler so often produce the dullest text?

Define the likelihood view in words. A language model is a probability distribution over sequences — given a context, it outputs a probability for every possible next token. Perplexity is the exponentiated average negative log-probability per token: intuitively, "the effective branching factor the model is choosing among" at each step. Perplexity 1 means it is certain; perplexity 50 means it is choosing as if among 50 equally likely tokens. (We keep this at intuition depth — the full mechanics of tokenizer normalization, sliding windows, and bits-per-byte belong to cs336-12-evaluation.)

Now the three blind spots, because each one is a way perplexity lies to you.

Blind spot 1 — likelihood is about the DATA, generation is about the MODEL's samples. Perplexity asks "how surprised is the model by human text?" It never asks "is the model's own output any good?" A model can score human text beautifully and still emit garbage under its own decoding settings. This is the likelihood trap: the single most-probable continuation is typically degenerate — repetitive, safe, dull — because real language spreads probability across many good continuations, and greedily grabbing the top one lands you in the boring valley between them.

Blind spot 2 — the fidelity-diversity tradeoff. A model that only ever emits one perfect poem has flawless fidelity and zero diversity. A model that emits wildly varied nonsense has the reverse. A single scalar — perplexity, or any per-sample quality score — cannot see the diversity axis at all. Plant the flag now: Chapter 5's precision/recall for generative models is the two-number instrument that finally measures both axes at once.

Blind spot 3 — memorization is the ultimate likelihood exploit. A model that regurgitates its training data assigns that data near-probability-1 and achieves near-perfect perplexity on that data, while having learned nothing generalizable. Evaluating on contaminated or memorized data is not measuring generation — it is measuring storage. This is why the 2023–2025 contamination findings (models scoring far above chance on "held-out" benchmarks whose items leaked into pretraining) forced the field toward fresh/private test sets like LiveBench's rolling questions.

Play the two axes before we compute anything. The simulation below shows the model's sample cloud against the true data distribution. Morph it from mode collapse (all samples in one tight blob — high fidelity, no diversity) through matched to hallucinating (samples spraying outside the data). Watch the two readouts move in opposition — and hit Memorize to see both scores hit a perfect, worthless 1.0.

Fidelity vs Diversity

Gray = true data (two crescents). Purple = model samples. Quality = fraction of model samples near data (fidelity); Coverage = fraction of data near model samples (diversity). Memorize snaps model dots onto the data — both hit 1.0 with zero new content.

Collapse ↔ Match ↔ Hallucinate match
Sample count 200

Now work the arithmetic in both directions. A tiny model assigns these probabilities to the 5 tokens of a held-out sentence: 0.5, 0.25, 0.5, 0.125, 0.25. Compute perplexity by hand.

Step 1 — take natural logs of each probability.

ln(0.5) = −0.6931,  ln(0.25) = −1.3863,  ln(0.5) = −0.6931,
ln(0.125) = −2.0794,  ln(0.25) = −1.3863

Step 2 — sum, then average over the 5 tokens.

Σ = −0.6931 − 1.3863 − 0.6931 − 2.0794 − 1.3863 = −6.2383
mean log-prob = −6.2383 / 5 = −1.2477

Step 3 — perplexity is exp of the negated mean.

PPL = exp(−mean log-prob) = exp(1.2477) = 3.482

Step 4 — sanity-check with powers of two, because these probabilities are all powers of ½. The product of the probabilities is 2−1 × 2−2 × 2−1 × 2−3 × 2−2 = 2−9, so perplexity is the geometric mean's reciprocal:

PPL = (2−9)−1/5 = 29/5 = 21.8 = 3.482

Same answer. The model behaves as if choosing among about 3.5 equally likely tokens at each step — that is what a perplexity of 3.5 means.

Step 5 — now the memorizer. A model that stored a training string assigns p = 0.99 to every token of it. Its perplexity on that string:

PPL = exp(−ln 0.99) = 1 / 0.99 = 1.0101

A near-perfect score — and utterly worthless as a signal of creative quality. Ask this model for a new poem and the metric has told you nothing. Likelihood on data it stored is storage, not generation.

python
# From scratch: accumulate log-probs in a loop, printing every intermediate
# so it mirrors the hand arithmetic exactly.
import math
def perplexity(probs):
    running = 0.0
    for p in probs:
        lp = math.log(p)
        running += lp
        print(f"  p={p:.3f}  log={lp:+.4f}  running={running:+.4f}")
    mean = running / len(probs)
    print(f"  mean log-prob = {mean:.4f}")
    return math.exp(-mean)

print("held-out:",  round(perplexity([0.5, 0.25, 0.5, 0.125, 0.25]), 4))  # 3.4822
print("memorized:", round(perplexity([0.99]*5), 4))                     # 1.0101
python
# Library: perplexity in one line.
import numpy as np
p = [0.5, 0.25, 0.5, 0.125, 0.25]
print(round(float(np.exp(-np.mean(np.log(p)))), 4))   # 3.4822
print(round(2**1.8, 4))                                 # 3.4822  (power-of-two check)

Two connections close the chapter. For diffusion and images, exact likelihoods exist only for some model families (autoregressive, normalizing flows); VAEs and diffusion give only a bound (the ELBO). Worse, likelihood in pixel space correlates notoriously poorly with visual quality — one more reason the field moved to sample-based distribution metrics (Chapter 5). And the chapter's law: likelihood metrics are cheap, differentiable, and excellent as a training signal and a regression alarm — but they are proxy metrics for generation quality. Every serious eval stack pairs them with sample-based and preference-based measurements.

Misconception: "Lower perplexity model = better generator, always." But perplexity is computed on other people's text — it never looks at the model's own samples. A model can be an excellent judge of human text and a terrible author (the likelihood trap), and a memorizer achieves spectacular perplexity on anything it stored. Perplexity ranks models on the likelihood axis only; the quality and diversity of samples need their own instruments.
Interview lens. "Your new 7B model beats the old one by 0.3 perplexity, but humans prefer the old model's outputs 60/40. Give three hypotheses and the experiment for each." Hypothesis 1 — decoding mismatch: perplexity scores the distribution, humans see samples under a specific temperature/top-p; sweep decoding params and re-run preferences. Hypothesis 2 — contamination: the new model saw part of the eval set; run n-gram/substring overlap checks and recompute on verified-fresh text. Hypothesis 3 — axis orthogonality: the quality humans care about (helpfulness, formatting, tone) is nearly orthogonal to next-token likelihood; inspect preference rationales or run a rubric eval. The staff note: perplexity stays useful as a cheap regression alarm even while it fails as a quality ranker.
A model achieves perplexity 1.01 on an evaluation set. What is the most likely explanation?

Likelihood failed because it never looks at the model's own output. But sometimes generation does come with a partial answer key — code that must pass unit tests. There, quality per sample is binary again, and we can do far better than likelihood. Getting the estimator right takes one subtle piece of combinatorics, and the obvious approach gets it wrong. That is pass@k.

Chapter 2: pass@k, Derived — The Unbiased Estimator

Code generation hands us back an answer key: unit tests. Either the generated function passes the tests or it does not — a crisp checkmark. So evaluation is easy again, right? Only if you get one subtle piece of combinatorics right, and the natural approach gets it wrong in a way that has appeared in shipped papers.

Set the stage. For tasks with a verifier (code + unit tests, math with checkable answers), per-sample quality is binary. But generation is stochastic: one model call is one draw from the model's distribution, and calling it twice gives two different programs. Users care about a specific quantity: "if I let the model try k times and keep any attempt that passes, what is the chance at least one works?" That is pass@k. (Choosing pass@k as your metric is a definition decision — metric-design's territory; here we own its estimation.)

Derive the population quantity first. If each independent sample solves the problem with probability p, then all k failing has probability (1−p)k, so:

pass@k = 1 − (1 − p)k

Intuitively, k is a compute-for-reliability dial: with p moderate, pass@k climbs fast as you spend more attempts. A model that succeeds 30% of the time per try succeeds 1 − 0.75 = 83% of the time given five tries.

The naive approach. Draw n samples, observe c successes, estimate p̂ = c/n, and plug in: 1 − (1 − c/n)k. This looks unimpeachable — the right formula with the best estimate of p. It is biased, and here is exactly why. The function f(p) = 1 − (1−p)k is strictly concave (it curves downward). By Jensen's inequality, the expected value of a concave function of a random estimate is less than the function of the true value:

E[f(p̂)] < f(p)  ⇒  the plug-in systematically underestimates pass@k

The gap is largest exactly where you tend to operate — small n, k near n. We will see it hit 7 points at n=10, p=0.3, k=5.

Build the unbiased estimator from first principles. Given the n draws you actually made (c of them passing), imagine choosing k of them uniformly at random — that is itself a valid "run the model k times" experiment. The chance your chosen k contains no success is the number of failure-only subsets over all subsets:

P(no pass in a random k-subset) = C(n − c, k) / C(n, k)

So the estimator is one minus that:

pass@k̂ = 1 − C(n − c, k) / C(n, k)

Why it is unbiased, in one line of intuition. Each of the C(n,k) subsets of your n i.i.d. samples is a legitimate k-shot trial; averaging the all-fail indicator over all of them is an average of unbiased estimates, hence unbiased. (The scipy check below confirms E[estimator] = 1 − (1−p)k to the digit.)

Play the bias reveal before the arithmetic. Draw ten samples from a hidden success rate p; the pass@k curve is drawn three ways — the true curve, the plug-in, and the unbiased estimator. Then hit Run 500 experiments and watch the plug-in curve sag below truth while the unbiased curve lands right on it.

pass@k and the Plug-in Bias

Top: n=10 sample slots fill green/red from a hidden rate p. Bottom: pass@k vs k drawn as true (dashed), plug-in, unbiased. Run 500 experiments overlays the average of each estimator — the plug-in visibly sags low.

Hidden success rate p 0.30
Highlight k 5

The flagship worked example: n = 10 samples, c = 3 pass. Estimate pass@5.

Step 1 — count the failure-only subsets. There are n − c = 7 failures. The number of ways to pick 5 of them:

C(7, 5) = 7! / (5! · 2!) = (7 × 6) / 2 = 21

Step 2 — count all subsets of size 5 from the 10 samples.

C(10, 5) = (10 × 9 × 8 × 7 × 6) / (5 × 4 × 3 × 2 × 1) = 30240 / 120 = 252

Step 3 — the probability a random 5-subset contains no pass.

21 / 252 = 1/12 = 0.08333

Step 4 — pass@5 is one minus that.

pass@5 = 1 − 21/252 = 231/252 = 0.91667

Step 5 — the numerically stable product form, which real implementations (HumanEval's reference code) use to dodge gigantic factorials:

1 − (1 − 5/8)(1 − 5/9)(1 − 5/10) = 1 − (3/8)(4/9)(5/10) = 1 − 60/720 = 1 − 1/12 = 0.91667

Same value, factorial-free.

Step 6 — see the plug-in bias at the population level. Take the true rate p = 0.3, so true pass@5 = 1 − 0.75 = 1 − 0.16807 = 0.83193. Average the plug-in estimator over c ~ Binomial(10, 0.3):

E[1 − (1 − c/10)5] = 0.75974  (underestimates by 7.2 points!)
E[combinatorial estimator] = 0.83193  (exactly unbiased)

Boundary behaviors interviewers love. c = 0 gives pass@k = 0 exactly (no successes to catch). When n − c < k, we have C(n−c, k) = 0, so pass@k = 1 exactly — you cannot pick k samples without hitting a success. k = n returns the raw any-success indicator. And k = 1 gives c/n exactly — the plug-in is unbiased at k=1, because f is linear there.

python
# From scratch: pass@k three ways, all printing intermediates and agreeing.
from math import comb
import numpy as np

def pass_at_k(n, c, k):
    if n - c < k:                       # fewer failures than k -> guaranteed hit
        return 1.0
    combo = 1.0 - comb(n - c, k) / comb(n, k)          # explicit ratio
    prod  = 1.0 - np.prod(1.0 - k / np.arange(n - c + 1, n + 1))  # stable form
    print(f"  C({n}-{c},{k})={comb(n-c,k)}  C({n},{k})={comb(n,k)}")
    return combo, prod

print(pass_at_k(10, 3, 5))   # (0.91667, 0.91667)
print(pass_at_k(10, 3, 8))   # (1.0, 1.0)  -- boundary: only 7 failures
python
# Library: the official HumanEval estimator (Chen et al., 2021).
import numpy as np
n, c, k = 10, 3, 5
print(round(1.0 - np.prod(1.0 - k / np.arange(n - c + 1, n + 1)), 5))   # 0.91667

Practical estimation discipline. Choose n comfortably larger than the largest k you will report (HumanEval used n=200 for k up to 100). Report pass@1 alongside pass@k so readers can separate per-sample quality from sampling budget. And never compare models at different temperatures without saying so — temperature moves p per sample and the sample diversity that pass@k exploits. (How the harness executes untrusted code, sandboxing, and suite curation is ai-evaluation's job; contamination for these suites is cs336-12's; this chapter owns the estimator and its bias math.)

Misconception: "The plug-in estimator is close enough — it uses the right formula with the best estimate of p." But f(p) = 1 − (1−p)k is concave, so Jensen guarantees systematic underestimation, and the error is worst exactly where you operate (small n, k near n): at n=10, p=0.3, k=5 it is 7 points low on average. The combinatorial estimator costs one comb() call and is exactly unbiased — there is no reason to ever ship the plug-in.
Interview lens. "You have 1000 model calls to evaluate a code model on 100 problems, and leadership wants pass@10. How do you allocate, and what estimator and error bar?" Allocate n=10 per problem — but note that with k=10 the estimator degenerates to the highest-variance any-success indicator. Better: report pass@5 with n=10 so the combinatorial estimator averages over C(10,5)=252 subsets per problem, cutting variance at the same cost. State the estimator: mean over problems of 1 − C(n−ci,k)/C(n,k). For the error bar, bootstrap over problems (the problem set is the sampling unit; per-problem samples are dependent). If k=10 is non-negotiable, lobby for n > k (say n=20 on a stratified subset), and fix and disclose the temperature.
With n=10 samples and c=3 passing, what does the unbiased estimator give for pass@8?

pass@k worked because a verifier gave us binary per-sample truth. But most generative quality has no verifier — only preferences: thousands of A-vs-B votes across many models. Votes are pairwise; a leaderboard is a single ordered list with error bars. Turning a tournament into a ranking is the next instrument.

Chapter 3: Pairwise Votes to Rankings — Bradley-Terry and Elo

You now have thousands of A-vs-B votes across ten models. But votes are pairwise, and your leaderboard must be a single ordered list. How do you turn a tournament into a ranking — with error bars?

Motivate the model with the flaw it fixes. Raw win rate against "whoever you happened to fight" is confounded by opponent strength: a model that only battled weak opponents looks great, a model that fought only champions looks mediocre. We need a latent-strength model that accounts for who you beat.

Bradley-Terry (1952) assigns each model a positive strength si, and models every battle as:

P(i beats j) = si / (si + sj)

Equivalently, with ratings ri = log si, this is P(i beats j) = σ(ri − rj), a sigmoid of the rating difference. Only differences matter, so we pin one model (or the sum of strengths) as an anchor — ratings are defined up to a shift.

Write the likelihood. For battle counts wij (times i beat j), the log-likelihood is Σ wij log(si/(si+sj)). It is concave in the ratings r — there is one global optimum — and fitting it is a weighted logistic regression whose feature vector is +1 for model i, −1 for model j. That is exactly how modern LLM leaderboards (LMSYS / Chatbot Arena, since Dec 2023) fit their ratings: BT via logistic regression on the full battle log, not the online Elo update.

Fit it by hand with the classic MM (minorization-maximization) update:

sinew = Wi / Σj≠i nij / (si + sj)

where Wi is model i's total wins and nij the battles between i and j. From uniform strengths it converges to the maximum-likelihood fit. Let's watch it, then work the numbers.

The Bradley-Terry Arena

Three models on an Elo-scale axis. Edit the win-matrix steppers, press Fit BT to animate the MM iterations, then Bootstrap ×200 to grow confidence bands. Drag battle counts small and watch A and B's bands overlap — a "statistical tie." Replay as online Elo shows order-dependence.

A>B A>C B>C battles/pair

The worked example: three models, 10 battles per pair. A beat B 7/10, A beat C 8/10, B beat C 6/10.

Step 1 — total wins. WA = 7 + 8 = 15, WB = 3 + 6 = 9, WC = 2 + 4 = 6. Each nij = 10.

Step 2 — one MM iteration from uniform s = (⅓, ⅓, ⅓). Each pair's denominator term is 10 / (⅓ + ⅓) = 10 / (⅔) = 15, and each model faces two others, so each denominator = 15 + 15 = 30:

sA = 15/30 = 0.50,  sB = 9/30 = 0.30,  sC = 6/30 = 0.20

(Already normalized — they sum to 1.) One iteration already ranks them correctly.

Step 3 — iterate to convergence (the code does this in a loop):

sA = 0.5971,  sB = 0.2453,  sC = 0.1576

Step 4 — read the fitted strengths through the BT lens. The converged model reconciles all three pairwise rates at once, smoothing each using all the data:

P(A beats B) = 0.5971/(0.5971+0.2453) = 0.5971/0.8424 = 0.7088 (observed 0.70)
P(A beats C) = 0.5971/0.7547 = 0.7912 (observed 0.80)
P(B beats C) = 0.2453/0.4029 = 0.6088 (observed 0.60)

Step 5 — convert to leaderboard (Elo) units. The Chatbot-Arena scale is ri = 400 log10(si/sref), where a 400-point gap means 10:1 odds. Relative to the weakest model C:

rA = 400 log10(0.5971/0.1576) = 400 log10(3.789) = 400 × 0.5786 = +231.4
rB = 400 log10(0.2453/0.1576) = 400 × 0.1922 = +76.9,  rC = 0

An Elo-scale number is just a reparameterized BT strength.

Step 6 — the online Elo update, by hand. Elo is BT fit one battle at a time. The expected score is EA = 1 / (1 + 10(RB−RA)/400); after a result S ∈ {0, 0.5, 1}, RA ← RA + K(S − EA). Take RA = 1200, RB = 1000, K = 32, and let A lose (an upset):

EA = 1/(1 + 10(1000−1200)/400) = 1/(1 + 10−0.5) = 1/(1 + 0.31623) = 0.75975
RA ← 1200 + 32 × (0 − 0.75975) = 1200 − 24.31 = 1175.69
RB ← 1000 + 32 × (1 − 0.24025) = 1000 + 24.31 = 1024.31

Ratings are zero-sum under equal K. K is a learning rate: big K tracks fast-changing players (fine for chess humans), but for fixed model checkpoints Elo's order-dependence is pure noise — which is why leaderboards refit BT offline on all battles.

Step 7 — the uncertainty reality check. The standard error of a single 7/10 win rate:

SE = √(0.7 × 0.3 / 10) = √0.021 = 0.1449

A 14.5-point standard error on a win percentage! Ten battles per pair order the medals but prove almost nothing. Rank confidence comes from bootstrapping over battles: resample the battle log with replacement, refit BT each time, report the 2.5–97.5 percentile band per model plus P(model i ranks first). Two models whose bands overlap are statistically tied, whatever the point estimates say. (The bootstrap machinery itself belongs to eval-statistics; here we apply it to battles.)

python
# From scratch: the MM loop, printing per-iteration strengths, then Elo utils.
import numpy as np
W = {('A','B'):7,('B','A'):3,('A','C'):8,('C','A'):2,('B','C'):6,('C','B'):4}
m = ['A','B','C']

def fit_bt(W, m, iters=2000):
    pi = np.ones(len(m)) / len(m)
    for _ in range(iters):
        new = np.zeros_like(pi)
        for i, a in enumerate(m):
            wins = sum(W.get((a,b), 0) for b in m if b != a)
            den  = sum((W.get((a,b),0)+W.get((b,a),0))/(pi[i]+pi[j])
                       for j, b in enumerate(m) if b != a)
            new[i] = wins / den
        pi = new / new.sum()
    return pi

pi = fit_bt(W, m)
print(pi.round(4), round(pi[0]/(pi[0]+pi[1]), 4))   # [0.5971 0.2453 0.1576] 0.7088
print((400*np.log10(pi/pi[2])).round(1))            # [231.4 76.8 0.]

def elo_update(Ra, Rb, score, K=32):
    Ea = 1 / (1 + 10**((Rb - Ra)/400))
    return Ea, Ra + K*(score - Ea), Rb + K*((1-score) - (1-Ea))
print(tuple(round(x, 2) for x in elo_update(1200, 1000, 0)))  # (0.76, 1175.69, 1024.31)
python
# Library: BT is logistic regression on +1/-1 model-indicator features over
# battles -- exactly how Chatbot Arena fits its leaderboard.
from sklearn.linear_model import LogisticRegression
# X row per battle: +1 in winner column, -1 in loser column; y = 1. Fit, then
# coefficients are the ratings (up to the anchor). Bootstrap rows for CIs.
Misconception: "Elo and Bradley-Terry are competing systems — chess uses Elo, arenas use BT." They are the same probability model (a sigmoid of a rating difference). Elo is just a noisy online gradient step toward the BT fit, and its result depends on battle order. For fixed model checkpoints there is no drift to track, so the order-dependence is pure estimation noise — which is why serious leaderboards refit BT on the full battle log offline and report bootstrap intervals instead of running updates.
Interview lens. "Your arena shows model A at 1265 and B at 1258 after 2,000 battles across 8 models. The team wants to announce 'A is our best model.' Does the data support it?" A 7-point Elo gap implies P(A beats B) = 1/(1+10−7/400) = 51% — a one-point edge. With 2,000 battles over 28 pairs, A and B may share only ~150 direct or indirect comparisons, nowhere near the thousands needed to resolve 1pp (a ±5pp margin alone needs ~384 direct battles; 1pp needs ~9,600). The move: bootstrap the battle log, report each model's interval and P(rank 1); the intervals almost certainly overlap and P(A first) is maybe 0.55–0.65 — announce a statistical tie or collect targeted A-vs-B battles. Staff candidates also flag prompt-distribution and style confounds as a separate validity threat beyond sampling error.
Model X has a 90% win rate but only battled the weakest model; model Y has a 60% win rate earned against the strongest. What does Bradley-Terry do that raw win rate cannot?

We turned votes into a ranking with error bars — assuming the votes themselves are honest. Human votes cost dollars and days. A judge model costs cents and seconds. But your new measuring instrument has opinions of its own, and before you trust it you calibrate it — like any lab instrument. That is the next chapter.

Chapter 4: LLM-as-Judge, Measured — Position, Verbosity, Self-Preference

Human votes cost dollars and days; a judge model costs cents and seconds. The catch: your new measuring instrument has opinions of its own. Before you trust it, you calibrate it — exactly as you would a thermometer or a scale.

Frame the shift. LLM-as-judge replaces the human rater in the Chapter 3 pipeline. Everything downstream — BT fitting, leaderboards, bootstrap CIs — is unchanged. What changes is the error model of the vote itself. The MT-Bench / LLM-as-judge work (Zheng et al., NeurIPS 2023) showed that strong judges agree with humans ~80%+ of the time — comparable to human-human agreement — but only after specific biases are handled. This chapter is how to measure those biases. (Where judges sit in the harness is ai-evaluation's territory.)

Bias 1 — position bias, and its calibration experiment. Judge every pair twice, with positions swapped. Define per-pair outcomes: consistent-A (A wins both orders), consistent-B, and flip (the verdict follows position). The flip rate is your instrument's position sensitivity. The position effect is half the gap between "A wins when first" and "A wins when second." The correction: aggregate over both orders (equivalently, randomize per query) — the +b and −b terms cancel by symmetry, exactly like Chapter 0.

Bias 2 — verbosity bias, and its experiment. Take pairs where a known-equal (or known-worse) answer is padded — restatements, bullet expansions, no new content — and measure the win-rate lift per 100 added tokens. The cleanest causal design: the same answer at two lengths; the judge should be indifferent, so any systematic preference is the bias. Corrections: rubric instructions that demote padding, length-stratified reporting, or post-hoc length-controlled modeling (regress judge preference on length difference, read the quality coefficient at zero length gap — the idea behind Length-Controlled AlpacaEval, 2024).

Bias 3 — self-preference, and its experiment. Have judge J score outputs from J's own model family versus others, with authorship hidden; compare J's own-family preference against a panel of other judges (or humans) on the same pairs. The self-preference score is the gap between J's own-model win rate and the panel consensus. Mitigations: never judge your own family for headline numbers; use judge juries of different families; report per-judge results.

Formalize with a simple measurement-error model:

judged pref = true pref + (position term) + γ · (length diff) + (self term) + noise

Each calibration experiment isolates one term by design: swap isolates position; same-content-two-lengths isolates γ; authorship blinding isolates the self term. This is instrument calibration, not vibes.

Run all three calibrations on the bench below. Set hidden bias dials, run the swap / pad / blind experiments, and watch each measured bias recover the planted dial value — the whole point of a designed experiment.

The Judge Calibration Bench

Set the hidden bias dials, then run each experiment: Swap (100 pairs both orders → flip rate), Pad (win-rate vs length), Blind (own vs other family). Reveal true dials checks your measured values against ground truth.

Position bias b 0.08
Verbosity γ /100tok 0.05
Self-preference s 0.06

The worked example: judge 100 response pairs twice each, swapping positions. When A is first, A wins 60/100; when A is second, A wins 44/100. Per-pair breakdown: A wins both orders 40; A wins only-when-first 20; A wins only-when-second 4; B wins both orders 36.

Step 1 — consistency-check the breakdown. A-first wins = 40 + 20 = 60 ✓. A-second wins = 40 + 4 = 44 ✓. Total pairs = 40 + 20 + 4 + 36 = 100 ✓.

Step 2 — flip rate. The verdict flipped on the 20 + 4 discordant pairs:

flip rate = (20 + 4)/100 = 0.24  (a quarter of verdicts decided by seating, not content)

Step 3 — position effect. Half the gap between the two orders:

position effect = (0.60 − 0.44)/2 = 0.16/2 = 0.08 (being placed first buys +8pp)

Step 4 — the debiased win rate, averaging over both orders so the position term cancels:

A's order-symmetrized win rate = (0.60 + 0.44)/2 = 0.52

Step 5 — is 52% actually above 50%? Treat the 200 judgments as independent for a first cut:

SE = √(0.52 × 0.48 / 200) = √0.001248 = 0.0353
z = (0.52 − 0.50)/0.0353 = 0.566  (far below 1.96)

After debiasing there is no significant preference. The raw "60% win rate" headline was manufactured entirely by seating order. (The rigorous version pairs the two orders of each pair and tests only the 24 discordant pairs — a McNemar-style test, flagged for eval-statistics.)

Step 6 — the instrument verdict. A 24% flip rate fails a <15% acceptance bar. This judge needs a stronger rubric or both-order aggregation baked in before its votes feed any leaderboard.

Rubric-based judging reduces both variance and bias. Instead of "which is better?", score named criteria (correctness, groundedness, formatting), each with anchored 1–5 descriptions, then combine per a declared weighting (the weighting itself belongs to metric-design). Rubrics shrink the judge's freedom to indulge biases and raise agreement — but a rubric can encode the wrong values, so validate rubric scores against a human-labeled slice.

When is a judge "good enough"? Operational criterion: judge-human agreement on a held-out human-labeled set ≥ human-human agreement on the same set, and flip rate below your threshold (e.g. <15%), and no significant self-preference on the model families being compared. Below that bar, use the judge as a filter (triage obvious cases) and send the rest to humans.

python
# From scratch: the swap calibrator, printing every tally.
def calibrate_position(a_first_wins, a_second_wins, flip_only_first,
                       flip_only_second, n=100):
    flip_rate = (flip_only_first + flip_only_second) / n
    pos_effect = (a_first_wins/n - a_second_wins/n) / 2
    debiased  = (a_first_wins/n + a_second_wins/n) / 2
    print(f"  flips={flip_only_first+flip_only_second}  "
          f"first={a_first_wins}  second={a_second_wins}")
    return flip_rate, pos_effect, debiased

print(calibrate_position(60, 44, 20, 4))   # (0.24, 0.08, 0.52)

import math
se = math.sqrt(0.52*0.48/200)
print(round(se, 4), round((0.52-0.50)/se, 3))   # 0.0353 0.566
python
# Library: order-symmetrized win rate and flip rate over vote arrays.
import numpy as np
# v_first[i]  = 1 if judge picked A when A shown first for pair i
# v_second[i] = 1 if judge picked A when A shown second for pair i
debiased = (v_first.mean() + v_second.mean()) / 2
flip_rate = (v_first != v_second).mean()
Misconception: "A judge that agrees with itself is a good judge — it gave the same answer twice, so it's reliable." But self-consistency is not validity. A judge with a hard position habit is perfectly consistent (always picks the first slot) and perfectly wrong. Reliability (low noise) and validity (measuring true quality) are different failures; the swap test measures a validity failure that no amount of re-asking the same prompt will ever reveal.
Interview lens. "Design a complete acceptance test that decides whether GPT-class judge J may replace human raters." Four components. (1) Ground-truth slice: 300–500 pairs with 3+ human labels each; require judge-human agreement ≥ human-human agreement (you cannot beat the label noise). (2) Position calibration: judge every pair in both orders; require flip rate under a stated bar (10–15%) and deploy with per-query order randomization regardless. (3) Verbosity calibration: same-content length-padded pairs; require win-rate lift per 100 tokens below an epsilon, else add length-controlled scoring. (4) Self-preference: exclude J's own family from judged comparisons or show no significant own-family lift against a multi-family jury. Bonus: an ongoing weekly human-audited subsample, since judge behavior shifts with model updates — and acceptance is per-task, not global.
A judge gives A a 60% win rate with A always first. After swapping, A wins only 44% when second. Best estimate of A's true (position-free) win rate?

Judges gave us cheap, calibratable votes for text. But an image has no preference vote at scale — just two clouds of samples, real and generated. How far apart are two clouds? That question has an exact classical answer, and it runs the entire diffusion literature. Next chapter.

Chapter 5: Image and Diffusion Metrics — FID From Scratch

For images there is not even a preference vote to lean on at scale — just two clouds of samples, real and generated. How far apart are two clouds? That question has an exact classical answer, and it runs the diffusion literature.

Set up the distribution-matching view. A generative image model is good if its sample distribution matches the data distribution — and this single object captures both fidelity and diversity (Chapter 1's two axes reappear). We cannot compare raw pixel clouds meaningfully (two pixels apart is not two "units" of quality), so first embed every image with a fixed feature network — classically Inception-v3's 2048-dimensional pool layer — then compare the feature clouds.

The Gaussian simplification. Fit a Gaussian N(μ, Σ) to each cloud — just the empirical mean vector and covariance matrix. The Fréchet distance (the 2-Wasserstein distance) between two Gaussians has a closed form:

FID = ‖μ1 − μ2‖² + Tr(Σ1 + Σ2 − 2(Σ1Σ2)1/2)

Read each term in words. The mean term asks "are the clouds centered on the same place?" The trace term asks "do they have the same spread and shape?" — and the matrix square root is what correctly accounts for correlated dimensions.

Demystify the matrix square root. For diagonal covariances the matrix square root is just the elementwise square root of the diagonal. The correlated case needs scipy.linalg.sqrtm. An implementation gotcha: sqrtm of a near-singular product can return tiny imaginary parts — production FID code takes the real part.

Play with FID's behavior before computing. Move the model cloud's mean and reshape its covariance; the FID readout decomposes into its two terms, computed every frame from the actual closed form. Hit Mode collapse and watch the trace term explode while the mean term barely moves.

The Two Clouds — Live FID

Gray = real data cloud, purple = model samples, with 1σ / 2σ ellipses. The mean term + trace term stack to the live FID. Mode collapse crushes the purple cloud to a dot — mean term ~0, trace term (and FID) large.

Mean shift dx1.0
Mean shift dy1.0
Spread-x2.0
Spread-y1.0

The worked example in a 2D feature space. Real data: μ1 = (0, 0), Σ1 = [[1,0],[0,1]]. Model: μ2 = (1, 1), Σ2 = [[4,0],[0,1]].

Step 1 — the mean term.

‖μ1 − μ2‖² = (0−1)² + (0−1)² = 1 + 1 = 2

Step 2 — the product inside the square root. Since Σ1 = I:

Σ1Σ2 = I · [[4,0],[0,1]] = [[4,0],[0,1]]

It is diagonal, so its matrix square root is the elementwise sqrt of the diagonal:

1Σ2)1/2 = [[2,0],[0,1]]

Step 3 — assemble the trace term.

Σ1 + Σ2 = [[5,0],[0,2]],   2(Σ1Σ2)1/2 = [[4,0],[0,2]]
difference = [[1,0],[0,0]],   Tr = 1 + 0 = 1

Step 4 — add the terms.

FID = 2 + 1 = 3

Interpretation: 2 of the 3 units come from the clouds being centered a diagonal step apart; 1 unit comes from the model over-spreading the x-direction (std 2 vs 1) while matching y exactly (its y-contribution: 1 + 1 − 2×1 = 0).

Step 5 — the correlated case, which is no longer hand-friendly — this is why sqrtm exists. With Σ2 = [[2, 0.6],[0.6, 1]], scipy returns sqrtm with off-diagonal 0.2544 and:

FID = 2.2835

Step 6 — the Inception Score mini example. IS = exp(E[KL(p(y|x) ‖ p(y))]): samples should be individually recognizable (sharp conditionals p(y|x)) and collectively varied (uniform marginal p(y)). Two images, two classes: p(y|x1) = (0.9, 0.1), p(y|x2) = (0.1, 0.9). The marginal p(y) = (0.5, 0.5).

KL(image 1) = 0.9 ln(0.9/0.5) + 0.1 ln(0.1/0.5)
= 0.9(0.5878) + 0.1(−1.6094) = 0.5290 − 0.1609 = 0.3681

Image 2 is identical by symmetry, so:

IS = exp(mean KL) = exp(0.3681) = 1.445

Sharp and varied, so IS exceeds 1. Had both images produced (0.9, 0.1), the marginal would equal each conditional, KL = 0, IS = 1 — the varied-ness is gone.

python
# From scratch: 2x2 FID via eigendecomposition for the sqrt, printing the
# eigenvalues and reassembled root, plus the Inception Score mini example.
import numpy as np
def fid_2x2(mu1, S1, mu2, S2):
    mean_term = float(np.sum((mu1 - mu2)**2))
    prod = S1 @ S2
    vals, vecs = np.linalg.eigh(prod)
    root = vecs @ np.diag(np.sqrt(np.clip(vals, 0, None))) @ vecs.T
    print(f"  eig(S1 S2)={vals.round(3)}  sqrt={np.sqrt(vals).round(3)}")
    trace_term = float(np.trace(S1 + S2 - 2*root))
    return mean_term, trace_term, mean_term + trace_term

print(fid_2x2(np.zeros(2), np.eye(2),
             np.ones(2),  np.diag([4., 1.])))   # (2.0, 1.0, 3.0)

pyx = np.array([[0.9,0.1],[0.1,0.9]]); py = pyx.mean(0)
kl = (pyx * np.log(pyx/py)).sum(1)
print(round(float(kl[0]),4), round(float(np.exp(kl.mean())),4))  # 0.3681 1.4449
python
# Library: the real thing uses scipy.linalg.sqrtm and takes the real part.
import numpy as np
from scipy.linalg import sqrtm
mu1, mu2 = np.zeros(2), np.ones(2)
S1 = np.eye(2); S2 = np.array([[2, 0.6], [0.6, 1.]])
fid = np.sum((mu1-mu2)**2) + np.trace(S1 + S2 - 2*np.real(sqrtm(S1 @ S2)))
print(round(float(fid), 4))   # 2.2835  (correlated case)

FID's fine print — the things papers get burned by. It is biased upward at small n (fewer samples inflate FID; compare only at matched counts, conventionally 50k vs 50k). It depends entirely on the feature network (Inception features encode ImageNet-classification semantics — hence CLIP-feature FID variants in recent text-to-image work; see contrastive-clip). And a single-number FID hides where the mismatch is.

Inception Score's obsolescence, and two more instruments. IS has a fatal flaw: it never looks at real data — a model producing one perfect image per class maxes IS while ignoring the data distribution. The field moved to FID for exactly this reason. CLIPScore handles text-conditioned generation: cosine similarity between CLIP embeddings of the prompt and the generated image, measuring prompt alignment — orthogonal to FID's distribution match (a model can ace FID while ignoring prompts). And precision/recall for generative models (Kynkäänniemi et al.) factorizes FID's single number into the two Chapter-1 axes: precision = fraction of generated samples inside the real manifold (fidelity); recall = fraction of real samples covered (diversity). Mode collapse shows up as high precision / low recall; hallucination as the reverse.

Misconception: "FID measures image quality — low FID = pretty pictures." FID measures distribution match in a frozen feature space. A model emitting flawless portraits when the data is 50% portraits / 50% landscapes gets a terrible FID (recall failure inflates the trace term), while a model matching the data's statistics including its artifacts can score well. And because the features are Inception's, FID is partially blind to whatever Inception ignores — faces, text rendering, fine anatomy — which is why 2024-era text-to-image papers pair it with CLIP-feature distances and human preference studies.
Interview lens. "Team A reports FID 8.2 on 10k samples; Team B reports 9.1 on 50k samples, same dataset and extractor. Which model is better, and how do you make it fair?" The comparison is invalid as stated: FID is biased upward at smaller n, so Team A's 10k-sample 8.2 could easily exceed 9.1 if recomputed at 50k. The fix is mechanical — recompute both at the same n (convention: 50k generated vs the full reference set, same preprocessing/resize, same feature layer, several seeds for a variance estimate). Staff-level additions: identical reference-statistics files (mismatched μ/Σ caches are a classic silent bug), identical image resizing/anti-aliasing (a known source of cross-paper FID discrepancies), and precision/recall to see which axis differs before declaring a winner.
A diffusion model mode-collapses: every sample is one photorealistic, razor-sharp image. What happens to FID and why?

FID scored whole sample distributions. But vision-language outputs — captions, VQA answers — are graded against many acceptable answers, one image at a time. Ask ten people what's in a photo and you get ten different, mostly-correct sentences. Grading a VLM means grading against that fuzzy cloud. Next chapter.

Chapter 6: Evaluating VLMs — VQA, Captions, Hallucination

Ask ten people what's in a photo and you'll get ten different — mostly correct — sentences. Grading a vision-language model means grading against that fuzzy cloud of acceptable answers, not one gold string.

Make the many-right-answers problem concrete. For one image, "a man riding a horse on a beach," "someone on horseback by the sea," and "a rider gallops along the shore" are all correct captions sharing almost no exact words. Exact-match dies immediately. Every VLM metric is a scheme for partial credit against multiple references.

VQA accuracy — the consensus protocol. The VQA v2 dataset collects 10 human answers per question. A model answer scores min(#matching humans / 3, 1): agree with 3+ humans for full credit, partial credit below. (The official metric averages this over all 10-choose-9 annotator subsets; we teach the simple form.) The design rationale: answer normalization plus the /3 threshold encode "consensus defines correctness" when no single string is the answer.

The yes-bias trap. Yes/no questions are ~40% of VQA, and models learn the prior ("is there a …" is usually "yes" in crowdsourced questions). A model can score deceptively well by exploiting answer priors without looking at the image — which motivated VQA v2's balanced complementary pairs and leads directly into POPE below.

BLEU, built up. Modified n-gram precision = clipped candidate n-gram counts / total candidate n-grams (clipping stops "the the the" from farming credit); geometric mean across n-gram orders; a brevity penalty punishes short outputs, since precision alone rewards saying almost nothing. Play with a live caption grader before the arithmetic.

The Caption-Grading Bench

A simple scene (horse + rider + beach) with three reference captions. Pick a preset candidate and watch matched n-grams glow, clipped duplicates flash, and the BLEU-2 numerator/denominator counters update. Presets show each failure mode.

The worked example, three parts. Part 1 — BLEU-2 for candidate "the cat sat on the mat" vs reference "the cat is on the mat."

Step 1 — unigram precision with clipping. Candidate tokens: the, cat, sat, on, the, mat (6 tokens; "the" appears twice). Clip each match against the reference's counts (the×2, cat, is, on, mat):

the → min(2,2)=2,  cat → 1,  sat → 0,  on → 1,  mat → 1
clipped total = 5,   p1 = 5/6 = 0.8333

Step 2 — bigram precision. Candidate bigrams: (the cat), (cat sat), (sat on), (on the), (the mat) = 5. Reference bigrams: (the cat), (cat is), (is on), (on the), (the mat). Matches: (the cat), (on the), (the mat) = 3.

p2 = 3/5 = 0.6

Step 3 — brevity penalty. Candidate length 6 = reference length 6, so BP = 1.

Step 4 — combine (geometric mean).

BLEU-2 = BP × √(p1 × p2) = √(0.8333 × 0.6) = √0.5 = 0.7071

Part 2 — VQA accuracy. The model answers "yellow," which 2 of 10 annotators gave:

VQA acc = min(2/3, 1) = 0.6667 (partial credit: a real minority agrees)

Part 3 — POPE hallucination probing. POPE turns "does it hallucinate objects?" into balanced binary probes: "Is there a dog?" over object-present sets (from ground-truth annotations) and object-absent sets (negatives sampled to catch prior-exploitation). On 50 present and 50 absent probes, the model says yes 45 and 15 times:

Precision = 45/(45+15) = 45/60 = 0.75
Recall = 45/50 = 0.90
F1 = 2 × 0.75 × 0.90 / (0.75 + 0.90) = 1.35/1.65 = 0.8182
Yes-rate = (45+15)/100 = 0.60

On a 50/50-balanced probe set, an image-grounded model's yes-rate should be near 0.50. Here it is 0.60 — the model says yes 10 points too often; recall is being purchased with hallucinations, exactly the pattern POPE's balanced design exposes.

Why BLEU fails captions, and the two fixes. BLEU treats "a man rides a horse" vs "a person on horseback" as near-zero overlap (synonyms invisible), and rewards safe generic captions that share stopwords with every reference. CIDEr fixes credit assignment by weighting each n-gram with TF-IDF across the reference corpus — distinctive consensus phrases ("riding a horse") count heavily, boilerplate ("there is a") counts near zero; the score is average cosine similarity of TF-IDF n-gram vectors (it needs corpus statistics, so scores are corpus-relative). SPICE fixes surface-form dependence by parsing candidate and references into scene graphs and computing F1 over object/attribute/relation tuples — "a young girl" matches "a small child" at the tuple level — but inherits every parser error.

python
# From scratch: BLEU-2 with clipping, VQA soft accuracy, POPE scores.
from collections import Counter
import math

def ngrams(toks, n): return Counter(zip(*[toks[i:] for i in range(n)]))
def bleu2(cand, ref):
    c, r = cand.split(), ref.split()
    p = []
    for n in (1, 2):
        cn, rn = ngrams(c, n), ngrams(r, n)
        clipped = sum(min(v, rn[g]) for g, v in cn.items())  # clip = Counter intersect
        p.append(clipped / sum(cn.values()))
        print(f"  {n}-gram: {clipped}/{sum(cn.values())} = {p[-1]:.4f}")
    bp = 1.0 if len(c) >= len(r) else math.exp(1 - len(r)/len(c))
    return bp * math.exp(sum(math.log(x) for x in p)/len(p))

print(round(bleu2("the cat sat on the mat", "the cat is on the mat"), 4))  # 0.7071
print(round(min(2/3, 1), 4))                                          # 0.6667 VQA
p, r = 45/60, 45/50
print(p, r, round(2*p*r/(p+r), 4), (45+15)/100)                    # 0.75 0.9 0.8182 0.6
python
# Library: the full pipeline via sacrebleu / pycocoevalcap (BLEU/CIDEr/SPICE).
from pycocoevalcap.cider.cider import Cider
from pycocoevalcap.spice.spice import Spice
# scorer.compute_score({id: [refs]}, {id: [candidate]}) -> corpus-level score
Misconception: "A higher-order metric always beats a lower one — CIDEr replaced BLEU, so only report CIDEr." Each rung fixes one failure and adds one dependency: CIDEr fixes boilerplate-farming but imports corpus statistics (scores are corpus-relative and not comparable across datasets); SPICE fixes synonym-blindness but inherits every parser error; judge-based scoring fixes both but imports Chapter 4's whole bias catalog. Report the ladder (BLEU/CIDEr/SPICE + a validated judge), and when two rungs disagree, that disagreement is diagnostic signal, not noise.
Interview lens. "Your VLM's CIDEr improved 12% after fine-tuning, but users report more hallucinated details. How can both be true, and what eval would have caught it?" CIDEr rewards matching consensus reference n-grams; fine-tuning on caption corpora teaches the model to emit statistically-plausible distinctive phrases ("holding a frisbee," "wearing a red shirt") that often apply to similar images — matching references more while being wrong about this image more. N-gram consensus cannot verify grounding. The missing eval is hallucination-specific: POPE-style balanced object probes (report precision on absent objects and yes-rate), a CHAIR-style measure (fraction of caption-mentioned objects absent from ground truth), and a human/judge faithfulness rubric on a slice. The staff generalization: every fluency-or-consensus metric needs a paired groundedness metric, because optimizing one silently trades against the other — Goodhart in action (cross-link metric-design).
On POPE-style balanced probing (50% present, 50% absent), a VLM answers "yes" 85% of the time with recall 0.98. What is the diagnosis?

Every metric so far — judges, VQA consensus, caption scores — is ultimately calibrated against humans. So what calibrates the humans? Two raters agreeing 70% of the time sounds decent, until you compute how much of that agreement is pure luck. Next chapter.

Chapter 7: Human Eval Done Right — Agreement, Protocol, Cost

Humans are the gold standard that judges and metrics are calibrated against — so what calibrates the humans? Two raters agreeing 70% of the time sounds decent, until you compute how much of that agreement is luck.

Reframe humans as instruments. Everything in Chapters 3–4 treated the vote as data; this chapter asks how good the votes are. The core insight: raw percent agreement is inflated by chance agreement. Two raters who both label "acceptable" 90% of the time agree 82% of the time by coin-flipping alone. Any honest agreement statistic must subtract the luck.

Derive Cohen's kappa from that requirement. Let po be the observed agreement (fraction of items where the raters match) and pe the expected-by-chance agreement (the probability they'd match if each rated independently at their own base rates — a sum over categories of the product of the two marginals). Then:

κ = (po − pe) / (1 − pe)

In words: "the fraction of the achievable beyond-chance agreement that was actually achieved." If pe already gives you agreement, only what you earn above it counts, scaled by how much room there was.

Play with the agreement matrix before the arithmetic. Drag the four cells; watch po (the diagonal), the ghost "chance matrix" pe, and κ on the gauge. The presets include the worked example and the notorious kappa paradox.

The Kappa Lab

A 2×2 agreement matrix (Rater 1 × Rater 2). The diagonal drives po; the chance overlay is pe; the gauge shows κ. Drag the sliders and watch skewed marginals drag the same 70% raw agreement from κ 0.4 down below zero.

Both "acceptable"40
R1 acc, R2 unacc20
R1 unacc, R2 acc10

The worked example: two raters label 100 model responses acceptable/unacceptable. Both say acceptable: 40. Both say unacceptable: 30. R1 acceptable / R2 unacceptable: 20. R1 unacceptable / R2 acceptable: 10.

Step 1 — observed agreement is the diagonal.

po = (40 + 30)/100 = 0.70

Step 2 — marginals. R1 says acceptable 40 + 20 = 60 times (0.60); R2 says acceptable 40 + 10 = 50 times (0.50). So R1 unacceptable 0.40, R2 unacceptable 0.50.

Step 3 — chance agreement.

pe = P(both acc by chance) + P(both unacc by chance)
= 0.60 × 0.50 + 0.40 × 0.50 = 0.30 + 0.20 = 0.50

Half their agreement was free.

Step 4 — kappa.

κ = (0.70 − 0.50)/(1 − 0.50) = 0.20/0.50 = 0.40

Of the 0.50 of agreement available beyond luck, the raters achieved 0.20 — 40%. "Moderate" on the Landis-Koch convention, and typical for open-ended quality judgments.

Step 5 — the kappa paradox. Keep po = 0.70 but make marginals extreme (both raters say acceptable 90%):

pe = 0.9 × 0.9 + 0.1 × 0.1 = 0.82
κ = (0.70 − 0.82)/(1 − 0.82) = −0.12/0.18 = −0.6667

The same raw agreement is now worse than chance. Marginals matter as much as matches.

Calibrate the scale honestly. The Landis-Koch adjectives (0.2 slight … 0.8 almost perfect) are conventions, not laws — report the number. Real generative-eval kappas are humbling: open-ended quality judgments often land at 0.3–0.5 between trained raters. And that number is the ceiling for any automated metric: a judge cannot agree with humans more than humans agree with each other. So measure human-human kappa first — it is the denominator of every "our judge matches humans" claim.

Kappa's edge cases and upgrades. High raw agreement with skewed marginals gives shockingly low kappa (the paradox). Cohen's kappa is for two raters on nominal labels. The upgrades, with triggers: Fleiss' kappa (3+ raters), weighted kappa (ordinal scales where off-by-one disagreement is milder), Krippendorff's alpha (any number of raters, missing data, any scale — the Swiss-army option). Full derivations belong to eval-statistics.

Protocol — three pillars. (1) Blinding: raters must not know which system produced an output; strip model names, formatting tells, watermarks, and even indirect tells (characteristic phrasing, emoji habits). Position randomization per item is blinding's twin for pairwise tasks. (2) Instructions and rubrics: anchor every scale point with a concrete example ("3 = factually correct but omits a key caveat"); pilot the rubric on 20 items, measure kappa, and revise the rubric before scaling — low pilot agreement is a rubric bug, not a rater bug. (3) Attention checks and honeypots: seed known-answer items, track per-rater honeypot accuracy and per-rater kappa-vs-pool, drop or retrain raters below threshold. On crowdsourcing platforms expect to filter 10–30% of raters; cap session length and shuffle item order per rater to decorrelate fatigue from difficulty.

The cost-quality frontier, with real arithmetic: raters × items × wage. 1000 pairs × 3 raters × $0.50 = $1,500 and days of latency, versus ~$4 of judge calls for the same volume (Chapter 9 does this arithmetic) — the entire economic case for Chapter 4's calibrated judges. The design pattern that gets both: small, protocol-rigorous human sets to (a) measure the human-human ceiling and (b) calibrate/audit the judge; the judge then scales to millions. Agreement statistics are the exchange rate between the two layers.

python
# From scratch: Cohen's kappa with the confusion table, marginals, pe, kappa.
def cohens_kappa(both_acc, r1acc_r2un, r1un_r2acc, both_un, n=100):
    po = (both_acc + both_un) / n
    r1_acc = (both_acc + r1acc_r2un) / n            # rater-1 marginal
    r2_acc = (both_acc + r1un_r2acc) / n            # rater-2 marginal
    pe = r1_acc*r2_acc + (1-r1_acc)*(1-r2_acc)      # chance agreement
    print(f"  po={po:.2f}  pe={pe:.2f}")
    return (po - pe) / (1 - pe)

print(round(cohens_kappa(40, 20, 10, 30), 4))   # 0.4
print(round(cohens_kappa(63, 27, 3, 7), 4))     # -0.6667  (paradox: po=0.7, skewed)
python
# Library: sklearn agrees; krippendorff.alpha for the general case.
from sklearn.metrics import cohen_kappa_score
r1 = [1]*40 + [1]*20 + [0]*10 + [0]*30
r2 = [1]*40 + [0]*20 + [1]*10 + [0]*30
print(round(cohen_kappa_score(r1, r2), 4))   # 0.4
Misconception: "70% raw agreement means the labels are 70% trustworthy." Raw agreement has no baseline. Two raters who both say "acceptable" 90% of the time agree 82% while paying zero attention (pe = 0.82), and 70% raw agreement under those marginals is worse than random (κ = −0.67). Always report kappa (or alpha) alongside raw agreement — and always compare your judge's human-agreement against the human-human number, never against 100%.
Interview lens. "You have $2,000 and one week for trustworthy human eval of a creative-writing model vs the incumbent. Design it end to end, including how you'll know the labels are trustworthy." Use pairwise preference (creative quality has no stable absolute scale), ~500 prompt pairs from real usage, blinded outputs with per-item position randomization, 3 raters per pair, a 2-page anchored rubric piloted on 20 items first. Trust machinery: 5% honeypot items with known verdicts, per-rater agreement-vs-pool tracking with a drop threshold, and Fleiss' kappa reported alongside the win rate — if pilot kappa is below ~0.3, fix the rubric before spending. Budget: 500 × 3 × ~$0.90 ≈ $1,350, leaving room for the pilot and re-rating dropped raters. Deliverable: debiased win rate with a bootstrap CI over items, plus the kappa so downstream judge calibration (Ch4) has its ceiling.
Your LLM judge achieves kappa 0.42 against human labels. Trained humans achieve kappa 0.45 against each other on the same task. The right conclusion?

You have now derived every instrument — verifiers, preference aggregation, judge calibration, distribution metrics, and the human ceiling. Time to run the whole pipeline against a rigged judge and rescue the leaderboard yourself. The showcase.

Chapter 8: Showcase — The Judge-Bias Lab

You have derived every instrument. Now run the whole pipeline against a rigged judge and rescue the leaderboard yourself. Unlike the real world, this laboratory knows the ground truth — and that is what makes the lesson visible.

The laboratory contract. Model A truly beats model B on 55% of prompts (a planted quality gap). The judge is simulated with three tunable corruptions from Chapter 4: position bias (+b to whichever answer is first), verbosity bias (+γ per 100 extra tokens, and model B writes longer), and self-preference (+s when judging its own family — B shares the judge's family). Your job: measure the truth through the corrupted instrument.

The showcase runs the full pipeline live. Set the red bias dials, flip the green repair switches, and move the battle-count slider; the leaderboard re-simulates every change, with bootstrap CI whiskers and a truth marker. The status banner grades your pipeline: CONFIDENTLY WRONG, NOISY, or TRUSTWORTHY.

The Judge-Bias Lab

Truth: A wins 55% (dashed marker). The bias dials corrupt the judge; the repair switches fix it by design. With b active and A-always-second, B tops the board with a tight CI — flip "Randomize order" and A re-centers above B. Run swap audit reads the flip rate.

Position bias b0.10
Verbosity γ0.04
Self-pref s0.03
Battles1000

Walk the six acts. The worked numbers: true P(A beats B) = 0.55, judge position bias b = 0.10, layout A-always-second.

Act 1 — naive pipeline, confident lie. A's answers are always placed second (alphabetical-by-run-id, the classic accidental systematic layout). A is never first, so:

measured A rate = 0.55 − b = 0.55 − 0.10 = 0.45

B tops the leaderboard, and with enough battles the CI excludes 0.5 — statistically significant, systematically wrong. The flip threshold: the ranking inverts whenever b > 0.05 (the true margin). A bias as small as 6pp beats a 5pp true quality gap.

Act 2 — the flip-rate audit. Run the Chapter 4 swap experiment inside the lab; the flip rate and position effect surface immediately. The instrument fails acceptance; every leaderboard it produced is quarantined. The audit costs 2× judging on a few hundred pairs — run it before the 50,000-battle campaign, not after.

Act 3 — repair 1, randomization. Per-battle coin-flip ordering makes the expected position term cancel:

0.5(0.55 + b) + 0.5(0.55 − b) = 0.55  for ANY b

The measured rate re-centers on truth as the toggle flips; the position dial now moves nothing. Design beats correction — no estimate of b was ever needed.

Act 4 — repair 2, length. Verbosity bias reintroduces a tilt through B's longer answers. Two remedies: (i) length-matched subsetting (only battles with |length diff| < threshold — unbiased but discards data), or (ii) length-controlled adjustment (fit judged preference against length difference, read the quality term at zero gap — the LC-AlpacaEval idea). Either holds the leaderboard steady as the verbosity dial sweeps.

Act 5 — repair 3, jury. With self-preference s active, a single same-family judge inflates B. Swap in a 3-judge jury (different families, majority vote) or exclude the same-family judge; the s dial's leverage collapses. Multi-judge juries and family-exclusion rules are now standard practice in serious eval reports.

Act 6 — now, and only now, sample size. With bias handled, how many battles until the CI separates 0.55 from 0.50? The 95% two-sided margin at worst-case variance (p = 0.5):

n = 1.96² × 0.25 / 0.05² = (3.8416 × 0.25)/0.0025 = 0.9604/0.0025 = 384.2 → 385 battles

The sobering leaderboard version: a 10-Elo-point gap implies win probability 1/(1 + 10−10/400) = 1/(1 + 0.9441) = 0.5144, so the margin to resolve is 0.0144:

n = 0.9604 / 0.0144² = 0.9604 / 0.000207 ≈ 4,640 head-to-head battles

That is 12× the ±5pp case for a gap 3.5× smaller — because n scales as 1/margin². This is why arena leaderboards need hundreds of thousands of votes to order a dense model field.

The order of operations, numerically. At n = 5,000 with the rigged layout, the CI around the biased 0.45 has half-width:

1.96 × √(0.45 × 0.55/5000) = 0.0138 → [0.436, 0.464]

The interval excludes 0.5 decisively, "proving" the wrong winner. More data made the lie stronger. Bias first, then n.

python
# The full lab: simulate one battle, run a campaign, bootstrap the CI.
import numpy as np
rng = np.random.default_rng(0)

def battle(true_p, b, g, s, randomize, length_ctrl, jury):
    a_first = rng.random() < 0.5 if randomize else False  # A second by default
    p = true_p + (b if a_first else -b)          # position term
    if not length_ctrl: p -= g                    # B is longer -> tilts against A
    if not jury:        p -= s                    # same-family judge favors B
    return rng.random() < min(max(p, 0), 1)

def campaign(n, **kw):
    wins = np.array([battle(0.55, **kw) for _ in range(n)])
    boot = [rng.choice(wins, n).mean() for _ in range(1000)]
    return wins.mean(), np.percentile(boot, [2.5, 97.5])

rate, ci = campaign(5000, b=0.10, g=0, s=0,
                    randomize=False, length_ctrl=True, jury=True)
print(round(rate, 3), ci.round(3))   # ~0.45 [0.436 0.464] -- confidently WRONG
python
# The sample-size arithmetic every eval-design answer needs.
print(round(1.96**2 * 0.25 / 0.05**2, 1))          # 384.2  (+/-5pp)
E = 1 / (1 + 10**(-10/400))
print(round(E, 4), round(1.96**2 * 0.25/(E - 0.5)**2))   # 0.5144 4640
Misconception: "A tight confidence interval means the leaderboard is right." Act 1 produces a 95% CI of [0.436, 0.464] around a systematically wrong 0.45 — honest about variance, silent about bias. A CI answers "if I reran this exact (possibly rigged) pipeline, how much would the number wobble?" — it never certifies that the pipeline measures what you think. Audit the instrument (swap test, length test, jury test) before you spend samples narrowing the interval.
Interview lens. "A vendor's report shows their model beating yours 61% to 39% over 10,000 LLM-judged comparisons, CI ±1%. You suspect the pipeline. Name the four audits you run, in order." (1) Layout audit — was order randomized per comparison? Ask for the win rate conditional on position; a large first-slot advantage reveals position bias that 10,000 samples only entrenched. (2) Judge-identity audit — is the judge from the vendor's family? Rerun 500 pairs with a multi-family jury; self-preference shows up as the gap. (3) Length audit — compare answer-length distributions; if theirs is systematically longer, recompute with length-controlled scoring. (4) Prompt-distribution audit — who chose the prompts, do they match your traffic? A cherry-picked mix is a bias no judging fix repairs (hand off to experiment-design). The frame: the ±1% CI is irrelevant until all four systematic terms are ruled out. Precision is not accuracy.
In the lab, true P(A wins) = 0.55 and the judge has position bias b with A always shown second. For which b does the leaderboard invert?

The lab just used Chapter 0's flip demo, Chapter 3's BT/win-rate machinery, Chapter 4's calibration experiments, and Chapter 7's protocol thinking against one adversary. The same drill generalizes — FID pipelines get "rigged instrument" failures too (mismatched preprocessing = a rigged feature extractor). Next, we compress all of it into interview-ready form.

Chapter 9: Interview Arsenal — Generative Eval Under Fire

Everything you derived is now a target. Staff interviews probe exactly the places where naive answers and correct answers diverge — plug-in pass@k, raw win rates, tight CIs on biased pipelines.

The interview landscape clusters into four archetypes: (1) derive/compute the estimator (pass@k, kappa, FID term-by-term), (2) this number looks great, why might it be a lie? (the bias catalog), (3) design the eval for product X (protocol + cost), (4) metrics disagree, arbitrate (metric semantics). Each maps to the chapter that armed you.

The two-minute whiteboard drills and their canonical numbers, rehearsed until muscle memory — interviewers watch for hesitation at pe and at C(n−c, k):

DrillGivenAnswer
pass@k combinatorialn=10, c=3, k=5231/252 = 0.917
Elo expected + update1200 vs 1000, upset, K=32E=0.76 → 1175.7
Cohen's kappapo=0.70, pe=0.500.40
FID 2×2 diagonalμ gap (1,1), Σ2=diag(4,1)2 + 1 = 3
Position debiasing60/44 both orderseffect 8pp, truth 52%
Sample size±5pp at 95%385 battles
The Drill Dealer

Deals a random whiteboard drill and starts a five-second clock — the interview pressure the table cannot simulate. Answer in your head before it hits zero, then Reveal to check the worked steps. Deal shuffles the next card.

The bias catalog as a recitable checklist, each with its one-line detection experiment: position, verbosity, self-preference (judges → swap/pad/blind); contamination/memorization (likelihood → n-gram overlap); small-n bias (FID and plug-in pass@k → matched-n); opponent-strength confounding (raw win rates → BT fit); chance agreement (raw rater agreement → kappa); yes-prior exploitation (VQA/POPE → balanced probes, yes-rate).

The ceiling argument as a reusable move: any claim "our judge/metric achieves X% agreement with humans" is meaningless without the human-human number; the ratio (e.g., kappa 0.42/0.45 = 93%) is the honest statistic. Interviewers reward candidates who ask for the denominator unprompted.

The full Q&A bank — each with the follow-up an interviewer would actually ask next. Open each for the model answer.

Q. Derive the unbiased pass@k estimator and explain precisely why the plug-in 1−(1−c/n)k is biased. When does the difference matter most?
Model answer

Model each of n samples as independent Bernoulli with probability p; pass@k = 1−(1−p)k. The plug-in substitutes p̂ = c/n, but f(p) = 1−(1−p)k is strictly concave, so Jensen gives E[f(p̂)] < f(p): systematic underestimation. The unbiased estimator treats every k-subset of your n actual samples as a valid k-shot experiment: the fraction of all-fail subsets is C(n−c,k)/C(n,k), so pass@k̂ = 1 − C(n−c,k)/C(n,k) — an average of unbiased subset indicators, hence exactly unbiased. Numerically at p=0.3, n=10, k=5 the plug-in's expectation is 0.760 vs a truth of 0.832 — a 7-point systematic error. Worst when n is small relative to k and p is moderate; it vanishes at k=1 (f is linear) and shrinks as n grows. Implementations use the stable product form 1 − ∏(1 − k/i) for i in [n−c+1, n].

Follow-up: Your eval reports pass@10 computed with n=10 samples. What's wrong, and what would you change?

Q. Explain how Chatbot-Arena-style leaderboards turn pairwise votes into ratings, and why two models 7 Elo points apart usually should not be called "ranked."
Model answer

Each battle is Bradley-Terry: P(i beats j) = si/(si+sj), equivalently a sigmoid of a rating difference; ratings are fit by maximum likelihood on the full battle log (logistic regression with ±1 model indicators), then mapped to an Elo-like scale where 400 points means 10:1 odds. Uncertainty comes from bootstrapping the battle log: resample, refit, report per-model rating intervals and P(rank 1). A 7-point gap implies a head-to-head win probability of about 51%; resolving a 1-point margin at 95% needs on the order of ten thousand direct comparisons, far more than adjacent models typically have. So adjacent positions are usually statistical ties — the honest statement is overlapping intervals, not ranks. This is also why arenas moved off online Elo updates: for frozen checkpoints, update-order noise adds only variance.

Follow-up: How would style or verbosity confounds bias the BT fit even with infinite votes, and what did LMSYS deploy about it in 2024?

Q. You must approve or reject an LLM judge for production. Specify the full acceptance battery with numeric bars.
Model answer

Four tests. Ceiling test: on 300–500 human-labeled pairs, require judge-human kappa ≥ ~90% of the human-human kappa on the same items; a judge cannot meaningfully exceed the label-noise floor, so the ceiling ratio is the honest statistic. Position calibration: judge every pair in both orders; require flip rate under a preset bar (10–15%) and deploy with per-query order randomization permanently regardless, since symmetrization cancels any additive position term for free. Verbosity calibration: same-content pairs at different lengths must show win-rate lift per 100 tokens below a small epsilon, else add length-controlled scoring. Self-preference: exclude the judge's own model family from judged comparisons, or show no significant own-family lift against a multi-family jury. Approval is task-scoped, with a standing human re-audit (quarterly 300-pair slices) against drift.

Follow-up: The judge passes all bars, then its provider ships a model update. What breaks and what is your monitoring plan?

Q. Compute and interpret FID for a 2×2 case, and explain what FID catches that per-sample quality scores cannot.
Model answer

With μ1=(0,0), Σ1=I and μ2=(1,1), Σ2=diag(4,1): mean term = 1²+1² = 2; the matrix square root of Σ1Σ2=diag(4,1) is diag(2,1); trace term = Tr(diag(5,2) − diag(4,2)) = 1; FID = 3. The mean term asks whether the clouds are centered together; the trace term asks whether their spread and correlation structure match, with sqrtm handling correlated dimensions. The unique power is diversity detection: per-sample scores evaluate each image alone, so a mode-collapsed model emitting one flawless image per prompt maxes them — but its collapsed covariance leaves the full data covariance unmatched in the trace term, and FID explodes. Caveats: FID is biased upward at small n (compare at matched n, conventionally 50k), depends entirely on the frozen feature network (CLIP/DINO-feature variants exist), and hides where the mismatch is — pair it with generative precision/recall.

Follow-up: Two papers report FIDs of 8.2 and 9.1 on the same dataset — name three implementation differences that could account for it before any model difference does.

Q. Why did captioning metrics evolve BLEU → CIDEr → SPICE, and what does each rung fix and break?
Model answer

BLEU computes clipped n-gram precision with a brevity penalty — built for translation, where references are tight. For captions it fails twice: synonyms score zero ("person on horseback" vs "man riding a horse"), and generic captions farm credit from stopword overlap with every reference. CIDEr fixes credit assignment by weighting n-grams with TF-IDF across the reference corpus — distinctive consensus phrases count heavily, boilerplate near zero — but imports corpus statistics, making scores corpus-relative and gameable by learning to emit statistically-plausible distinctive phrases without grounding. SPICE fixes surface-form dependence by parsing into scene graphs and F1-ing object/attribute/relation tuples, so synonymy matches semantically — but inherits every parser error and misses fluency. The modern stack reports CIDEr and SPICE together, adds hallucination probes (POPE, CHAIR) because all overlap metrics are grounding-blind, and increasingly adds calibrated judge scoring — which reintroduces the Ch4 bias catalog.

Follow-up: Your fine-tune gains 12% CIDEr while users report more hallucinated details — reconcile, and name the metric that would have caught it.

Q. Two raters agree on 70% of items. Walk through why that alone is uninterpretable, compute kappa for the 40/20/10/30 table, and state how skewed marginals change the answer.
Model answer

Raw agreement has no chance baseline: raters with matching skewed base rates agree often while conveying nothing. Kappa subtracts the luck. For the table: po = (40+30)/100 = 0.70. Marginals: rater 1 acceptable 60%, rater 2 acceptable 50%; pe = 0.6×0.5 + 0.4×0.5 = 0.50 — half was free. Kappa = (0.70−0.50)/(1−0.50) = 0.40: 40% of the achievable beyond-chance agreement, typical for open-ended judgments. The paradox: hold po = 0.70 but let both raters say acceptable 90% — pe = 0.82 and kappa = (0.70−0.82)/0.18 = −0.67, worse than chance with identical raw agreement. Practically: report kappa (or Krippendorff's alpha for multiple raters/missing data), measure human-human kappa first because it is the ceiling for any judge, and treat low pilot kappa as a rubric bug to fix before scaling.

Follow-up: Your task has three ordinal quality levels rather than two nominal ones — what replaces Cohen's kappa and why does weighting matter?

Q. A 10,000-comparison judge-based benchmark shows 61–39 with ±1% CI. Argue why this can be "confidently wrong," with the arithmetic.
Model answer

The CI quantifies sampling variance only — how much the number would wobble if the same pipeline reran. Systematic bias passes through untouched and is amplified in credibility by large n. Concretely: true 55–45 preference, judge position bias 10 points, losing model systematically placed second (an alphabetical run-id is enough) → measured rate 45%; at n=5,000 the CI half-width is 1.96√(0.45×0.55/5000) = 1.4 points, so [43.6, 46.4] excludes 50 decisively — significant, directionally wrong, the ranking inverted by a bias only twice the true margin (any b above 5 points flips it). Audits in cost order: position-conditional win rates (layout bias), multi-family jury rerun (self-preference), length-distribution comparison + length-controlled rescoring (verbosity), prompt-provenance review (cherry-picking). Only after all four does the ±1% mean anything. Doctrine: bias is a design problem, variance a budget problem — fix them in that order.

Follow-up: Which single audit would you run first if you could only afford one, and why?

Q. Design the evaluation for a code-generation model where correctness matters but so do readability and efficiency — mixing verifier- and preference-based instruments.
Model answer

Layer by what has ground truth. Correctness: unit-test execution with the unbiased pass@k estimator — n comfortably above the largest reported k (e.g. n=20 for pass@1/5/10), fixed disclosed temperature, bootstrap over problems; contamination screening is a prerequisite (cs336-12). Efficiency: partially verifiable — measure runtime/memory on passing solutions against references, report distributions not means. Readability: no verifier, so pairwise preferences among passing solutions only (never let style votes rescue broken code): a calibrated judge with both-order judging and length control, licensed against a human-labeled slice via the kappa-ceiling test, BT-fit if >2 models. Publish the three axes separately — a composite hides the tradeoff and invites Goodharting (weighting is metric-design's; harness/CI wiring is ai-evaluation's). The signature: verifier first wherever possible, preferences confined to axes verifiers cannot reach, every preference number carrying its bias audit.

Follow-up: Your pass@10 went up but pass@1 went down after RL fine-tuning. What happened, and which number should gate the release?

Q. Perplexity improved but human preference worsened. Give the complete diagnostic playbook.
Model answer

Three hypothesis families, each with a decisive experiment. Decoding mismatch: perplexity scores the distribution while humans see samples at a particular temperature/top-p — sweep decoding and rerun a small preference eval; if preference recovers, the model is fine and the sampler was mis-tuned. Contamination/memorization: the perplexity gain may be storage — run substring/n-gram overlap between eval and training data, recompute on verified post-cutoff text, check for suspiciously low perplexity on specific documents (the 0.99-per-token signature drives PPL toward 1.01). Axis orthogonality: next-token likelihood weights every token equally while human preference concentrates on helpfulness, formatting, factuality, tone — collect preference rationales or run rubric-scored judging to find which axis regressed; often an RLHF-adjacent change traded likelihood for preference. Conclusion: keep perplexity as a cheap regression alarm and training diagnostic, never gate a launch on it alone; the gate is a debiased preference eval with error bars plus task-specific verifiers.

Follow-up: Would you ever ship a model whose perplexity got WORSE? Give a real scenario.

The cost-architecture drill every eval-design answer needs. Compare evaluating 1,000 response pairs three ways.

Human layer: 1000 × 3 raters × $0.50 = $1,500 (days; yields gold labels + human-human kappa ceiling)
Judge layer: 1000 × 2 calls (both orders) × $0.002 = $4.00 (minutes)
Cost ratio = 1500 / 4 = 375× cheaper
Ceiling check: judge-vs-human kappa 0.42 / human-human 0.45 = 0.933 → pass (≥ ~90%)

The resulting three-layer architecture: (i) free automated metrics on every commit as regression alarms, (ii) the calibrated judge on 10k–100k pairs for leaderboards and launch decisions, (iii) the $1,500-grade human protocol quarterly and on disagreement hot-spots to re-measure the ceiling and re-audit the judge. Both ratios (375×, 93.3%) belong in the interview answer.

Three debate prompts — know both defensible sides.

Debate 1: Arena leaderboards should be the primary public measure of LLM quality. PRO: preferences over real prompts are the closest thing to ground truth for open-ended quality; BT with bootstrap CIs is statistically principled; scale (millions of votes) beats any curated benchmark's coverage; fresh prompts are contamination-proof. CON: the voting population and prompt mix are unrepresentative and unstable; style/verbosity/recency confounds (the 2024-25 style-control updates, the Llama 4 Maverick episode) show ranks are gameable by submission strategy; votes measure single-turn charm, not reliability, safety, or long-horizon utility; public leaderboards create field-wide Goodhart pressure. Middle ground: arenas as one instrument in a battery, with style controls, provenance rules, and interval-aware reporting.
Debate 2: LLM judges should replace human evaluation entirely once they hit the human-human ceiling. PRO: at 90%+ of the kappa ceiling, adding a human adds mostly label noise at 375× the cost; judges are consistent, instantly rerunnable, calibratable; the human budget is better spent on ceiling measurement and audits. CON: the ceiling argument assumes the human labels define the target — but judges share failure modes with the models they grade (same training distributions), so correlated blind spots (subtle factual errors, sycophancy toward confident prose) never surface in agreement stats; drift after judge updates is silent; adversarial settings (models optimizing against a known judge) invalidate calibration. Synthesis: judges for routine scale, humans as a permanent adversarial audit layer whose job is to find where judge and truth diverge.
Debate 3: FID should be retired as the headline metric for image generation. PRO: Inception features encode 2015 ImageNet semantics — blind to faces, text rendering, and prompt fidelity users care about; small-n bias and preprocessing sensitivity make cross-paper numbers unreliable; preference models and CLIP scores track perceived quality better. ANTI: FID is the only cheap, deterministic, distribution-level check — preference scores are per-sample and diversity-blind, so retiring FID reopens the door to mode collapse that preference optimization actively encourages; the fix is upgrading the feature space (CLIP/DINO-feature Fréchet distances) and reporting precision/recall alongside, not abandoning distribution matching. The debate turns on whether "quality" means matching a data distribution or pleasing a user — force both sides to state which.
Misconception: "Interview prep here means memorizing metric definitions." The archetypes show otherwise: interviewers hand you a correct definition attached to a broken pipeline (tight CI + biased judge, great CIDEr + hallucinations, low perplexity + contamination) and watch whether you attack the instrument or admire the number. The skill being tested is the audit reflex, and it only comes from having computed the failure modes yourself.
An interviewer says: "Our judge agrees with human raters 78% of the time — is that good?" Single best immediate response?

You can now derive every estimator under time pressure. The last step is knowing which neighboring lesson to open when a real eval problem exceeds this one's borders. Connections.

Chapter 10: Connections — Where This Math Plugs In

You now own the math. The last step is knowing which neighboring lesson to open when a real eval problem exceeds this one's borders.

Draw the family map from your position. This lesson sits in the middle of the Evaluation & Analytics family: metric-design (prereq) decided what to measure; this lesson derived how to estimate it for generative outputs; eval-statistics generalizes the CI/bootstrap/power machinery used informally here; eval-plots renders these numbers honestly; experiment-design takes preference data online (A/B); regression-testing-ml turns any of these metrics into CI gates; metrics-ladder rolls them up for execs.

The decision procedure to close the lesson — drag a scenario onto the tree and follow the branches. This is the mental model for any new generative-eval problem.

The Instrument Picker

Tap a scenario, then answer each decision node (verifier? preferences? samples-only? conditioned?). The card lands on an instrument panel listing the estimator and its bias checklist. Shuffle deals edge cases.

The cross-link table — which sibling owns each neighboring concern.

LessonOwns
ai-evaluationThe eval harness: datasets, regression suites, dashboards, judge deployment — wraps around these estimators.
agent-evaluationAgent benchmarks and trajectory graders; when they report pass@k or judge preferences, the estimator math comes from here.
cs336-12-evaluationPerplexity mechanics, MC benchmark scoring, contamination — Ch1 defers likelihood-metric depth there.
metric-designPrereq: what to measure (definitions, Goodhart, weighting); this lesson estimates those quantities.
eval-statisticsThe general CI / bootstrap / power machinery used in Ch3, Ch4, Ch8.
eval-plotsHonest plotting of pass@k curves, Elo/BT intervals, FID trends.
regression-testing-mlWires pass@k, judge-bias, and generative metrics into release gates and drift monitors.
metrics-ladderCapstone — treats these estimators as the module rung and climbs to product and business metrics.
contrastive-clipCLIP embedding machinery behind CLIPScore and CLIP-feature FID variants (Ch5).
diffusion · vlmWhat the models being scored actually do internally (Ch5, Ch6).

Where the field is moving, so the lesson ages well: style- and length-controlled preference modeling (2024-25 arena updates), multi-judge juries replacing single judges, verifier-based rewards blurring eval into training (RLVR), and distribution metrics migrating from Inception to CLIP/DINO features — all are refinements of the estimators here, not replacements. The audit mindset transfers unchanged.

One judge-adequacy recap that ties Ch4 + Ch7 + Ch9 into a reusable rule. Judge-vs-human kappa 0.42, human-human kappa 0.45, post-randomization flip rate 4%. May the judge replace humans?

Ceiling ratio = 0.42/0.45 = 0.9333 ≥ 0.90 bar → PASS
Flip rate 4% < 15% bar → PASS
Residual human role = quarterly re-calibration: 300 × 3 × $0.50 = $450/quarter

Generalize: license = (ceiling ratio ≥ 0.9) AND (all Ch4 bias audits within bars) AND (a standing human re-audit budget). Any "can we automate this eval?" question in any modality reduces to instantiating these three checks.

The cheat sheet — every key formula, what each symbol means, and when to use it.

InstrumentFormulaUse when
Perplexityexp(−mean log-prob)Likelihood axis; cheap regression alarm (not a quality ranker)
pass@k (unbiased)1 − C(n−c,k)/C(n,k)A verifier exists; stochastic sampling
Bradley-TerryP(i>j) = si/(si+sj)Pairwise votes → global ranking
Elo scaler = 400 log10(s/sref)Human-readable leaderboard units (400 = 10:1)
Position debias(winfirst + winsecond)/2Any pairwise judge; cancels additive order bias
FID‖μ1−μ2‖² + Tr(Σ12−2(Σ1Σ2)1/2)Only samples; want fidelity + diversity in one number
VQA soft accmin(#match/3, 1)Consensus grading against 10 annotators
BLEU-2BP × √(p1 p2)n-gram overlap baseline (report with CIDEr/SPICE)
Cohen's kappa(po − pe)/(1 − pe)Two nominal raters; chance-corrected agreement / ceiling
Sample sizen = 1.96² · 0.25 / margin²After bias is killed — buy variance down with n
Misconception: "Finishing this lesson means you can evaluate any generative system solo." The boundaries are the point: the estimator is never the whole eval — datasets and harness (ai-evaluation), task realism (agent-evaluation), statistical depth (eval-statistics), and metric intent (metric-design) each fail independently, and a perfect estimator on a contaminated dataset or a mis-designed metric produces confident nonsense. Mastery here is knowing exactly which failure belongs to which layer — and which lesson to open.
Interview lens. "Sketch the complete evaluation stack — instruments, cadences, owners — for a multimodal assistant (text chat + image generation), from per-commit CI to quarterly exec reporting." Per-commit: automated regression metrics — perplexity deltas on a fixed suite, pass@k on a verifier subset, FID/CLIPScore on a frozen image prompt set (this lesson's estimators inside regression-testing-ml's gates). Nightly/weekly: calibrated-judge pairwise evals with order randomization and length control (Ch4/Ch8), BT-fitted internal leaderboard with bootstrap CIs (Ch3). Monthly: human preference panels with kappa-tracked raters (Ch7) to re-measure ceilings and audit the judge. Quarterly: exec-facing rollups — metrics-ladder-owned product KPIs tied back to these instruments, error bars preserved (eval-plots). The differentiating move is naming the handoffs explicitly and insisting every automated layer has a standing human audit above it.
Your team must evaluate a new SQL-generating model. Which instrument family does the decision procedure select FIRST, and why?

The send-off: you can now derive pass@k under time pressure, fit a leaderboard from votes, price a human study, calibrate a judge, and — most importantly — refuse to trust any generative-eval number until you know its estimator, its bias exposure, and its error bar. That refusal is the whole discipline.

What I cannot create, I do not understand. — Feynman. You built every estimator in this lesson from scratch, saw exactly where it lies, and put an error bar on it. Taste became a number you can defend.