Chapter 14 · Section 14.7 · 45 min

Evaluating a fine-tuned model

Your fine-tuned model looks wonderful in the demo. It answers in a warm, confident voice, formats its answers with headings and bullet points, and the team's favorite prompts all come out better than before. Then it ships. A customer asks for "a summary in under 50 words" and gets 180. A dashboard shows MMLU down four points from the base model. And an automatic judge that the team trusted rates it far above the previous version, mostly, it later turns out, because its answers got longer.

Each of those failures is a measurement you did not take, or took badly. This section builds the evaluation that should sit around every fine-tuning run. Verifiable instruction-following checks, regression tests on base capabilities with error bars that account for sampled answers and grouped questions, LLM judges with their biases and a statistically sound way to correct them with a few human labels, human review that measures what you care about, and a small, sharp eval set of your own. It closes the chapter, and its tools will reappear in every chapter of Part IV, because a post-training method is only as good as the evaluation that says it worked.

What to measure

A fine-tuned model can be good or bad in several independent ways, and no single number sees all of them.

questionhow to measure itexamples
Does it follow explicit instructions?programmatic checks of verifiable constraintsIFEval, your own format checks
Are its open-ended answers good?pairwise or graded comparison by a strong judge, or by humansMT-Bench, AlpacaEval (length-controlled), Arena-Hard, Chatbot Arena
Did it keep what the base model could do?the same benchmarks as the base, same prompts, same scoringMMLU, GSM8K, HumanEval, your domain tests
Is it safe, without refusing too much?harmful-request suites and over-refusal suitesrefusal evals and tests such as XSTest (§19.4)
Does it behave mechanically well?statistics of its outputsstop-token rate, length distribution, repetition, template compliance
Is it good at your task?a small eval set built from real usagethe last part of this section

The first and the third rows are cheap, objective, and catch most regressions, so they should run on every checkpoint (§14.4). The second is where chat models are actually compared, and it is the easiest to fool. The last is what decides whether the model is useful to you.

Verifiable instruction following

Asking a judge whether a response "follows the instructions" is slow and noisy. Zhou et al. (2023) noticed that many instructions are verifiable by code, such as "write more than 400 words", "mention the keyword AI at least 3 times", "do not use any commas", "wrap your entire answer in JSON", or "end your response with this exact phrase". Their benchmark, IFEval, uses 25 types of such instructions across 541 prompts, each prompt carrying one or more of them, and scores responses with no model in the loop.

It reports four numbers. Prompt-level accuracy counts a prompt as passed only if every instruction in it is followed. Instruction-level accuracy counts instructions individually. Each comes in a strict version, the raw check, and a loose version that also accepts the response after any combination of three transformations, removing markdown emphasis markers (* and **), dropping the first line (an intro such as "Sure, here it is"), and dropping the last line (an outro such as "Hope this helps"). With the identity, that makes 8 variants, and an instruction passes loosely if any of them passes. Loose scoring reduces false negatives from harmless decoration at the cost of some false positives.

Python · cell 1

The emphasis markers make the strict "ends with" check fail on a response that clearly followed the instruction, and the loose version accepts it. The word limit and the comma rule both fail strictly because of the chatty first line (its words, and its comma in "Sure, here it is"), and both pass loosely once the first and last lines are dropped. That is reasonable for the intro, and it is also the false-positive risk of loose scoring, since the dropped last line here was part of the answer the user asked for. IFEval became a standard part of post-training suites (Tulu 3 used it as an evaluation and as a source of verifiable rewards for RL, §18.3), and the idea generalizes. Any constraint your product depends on, a JSON schema, a length limit, a required disclaimer, a language, should have a checker that runs on every checkpoint.

Interview

IFEval-style scoring, strict and loose

Implement check_inst(resp, inst) for instructions given as dicts with a "kind" and parameters, and ifeval_scores(responses, instructions).

  • Kinds are "max_words" ("n", at most n words, counting re.findall(r"\b\w+\b", resp)), "min_words" ("n", at least n), "no_commas", "lowercase" (no uppercase letters at all), "ends_with" ("text", after stripping whitespace from the response), and "keyword" ("word" and "n", the word appears at least n times, case-insensitive, as a whole word).
  • instructions[i] is the list of instructions for response i. Return a dict with "prompt_strict", "inst_strict", "prompt_loose", and "inst_loose". Loose applies IFEval's 8 transformations (any combination of removing every *, dropping the first line, dropping the last line) and an instruction passes loosely if any variant passes.
Hidden tests, hints, and AI help, from $1.99 a month.

Regression testing on base capabilities

Fine-tuning can quietly cost knowledge and skills (§14.4), so every fine-tuned model should be compared with its base on a fixed suite of capability benchmarks, chosen to cover what you must not lose (knowledge, math, code, reasoning, the languages you serve). Two details decide whether the comparison means anything.

Score the models the same way. A base model is usually evaluated few-shot with a plain prompt, and an assistant zero-shot inside its chat template. Comparing those two numbers measures the formats as much as the models. Run both models with the same prompts, and for the fine-tuned model also report its score in its chat template, since that is how it will be used. Likelihood-based multiple choice (§6.6) is less sensitive to format than generation followed by answer extraction, which can fail on a model that phrases its answers differently.

Put error bars on the difference. Section 0.11 built the statistics, and here is the short version. An accuracy on nn independent questions has standard error p^(1−p^)/n\sqrt{\hat p(1-\hat p)/n}, about 1.6 points at n=1,000n = 1{,}000 near 50%. Two models answer the same questions, so compare them through the per-question differences di=ai−bid_i = a_i - b_i of their scores aia_i and bib_i, whose mean has standard error SE⁡(dˉ)=sd/n\operatorname{SE}(\bar d) = s_d/\sqrt n, with sds_d the standard deviation of the did_i and sd2=Var⁡(a)+Var⁡(b)−2Cov⁡(a,b)s_d^2 = \operatorname{Var}(a) + \operatorname{Var}(b) - 2\operatorname{Cov}(a, b). Questions differ in difficulty, so the covariance is large and pairing removes much of the noise. The paired bootstrap and McNemar's test (Exercise 0.11e) turn this into an interval and a p-value. And to detect a true gain Δ\Delta with 80% power you need about n=((1.96+0.84) sd/Δ)2n = \big((1.96 + 0.84)\,s_d/\Delta\big)^2 questions (Exercise 0.11f), twice as many as it takes to measure the difference to within ±Δ\pm\Delta. The widget shows both views on simulated runs of an eval.

How many eval items does it take to trust a small improvement?
Two models answer the same items. Per-item correctness is correlated because items differ in difficulty. Each dot is one simulated run of the eval, its observed difference in accuracy.
Loading interactive figure…

So a fine-tuned model that scores 1.5 points above its base on a 1,000-question benchmark has shown nothing until the paired interval excludes zero. That much is true of any classifier. Evaluating a generative model that you just fine-tuned adds three sources of noise that a textbook interval leaves out, and each one makes the honest error bar wider than the naive one.

Sampled answers, clustered questions, and training seeds

Sampled answers. Run a model at a temperature above zero and each question's grade is itself random. Say you draw KK answers per question and average their grades into a per-question score xix_i. If question ii has true pass rate pip_i, then given pip_i the score xix_i is a mean of KK Bernoulli draws, with E[xi∣pi]=pi\mathbb{E}[x_i \mid p_i] = p_i and Var⁡(xi∣pi)=pi(1−pi)/K\operatorname{Var}(x_i \mid p_i) = p_i(1-p_i)/K (§0.4). Averaging over questions, Var⁡(xi)=E[xi2]−E[xi]2=E[Var⁡(xi∣pi)+pi2]−E[pi]2\operatorname{Var}(x_i) = \mathbb{E}[x_i^2] - \mathbb{E}[x_i]^2 = \mathbb{E}\big[\operatorname{Var}(x_i \mid p_i) + p_i^2\big] - \mathbb{E}[p_i]^2, which is the law of total variance, and it splits the spread of xix_i across questions into two parts,

Var⁡(xi)=Var⁡(pi)⏟σb2, between questions+E[pi(1−pi)]K⏟σw2/K, within a question,SE⁡(xˉ)=σb2+σw2/Kn.\operatorname{Var}(x_i) = \underbrace{\operatorname{Var}(p_i)}_{\sigma_b^2,\ \text{between questions}} + \underbrace{\frac{\mathbb{E}\big[p_i(1 - p_i)\big]}{K}}_{\sigma_w^2/K,\ \text{within a question}}, \qquad \operatorname{SE}(\bar x) = \sqrt{\frac{\sigma_b^2 + \sigma_w^2/K}{n}} .

In plain words, more samples per question shrink only the sampling term, and more questions shrink both. You can estimate σw2\sigma_w^2 as the average variance of the KK grades within each question, and σb2\sigma_b^2 as the variance of the xix_i minus σw2/K\sigma_w^2/K, which tells you whether to buy questions or samples (Miller 2024). The common mistake is to treat all nKnK grades as independent, which divides by nK\sqrt{nK} and makes the error bar too small, because samples of the same question share its difficulty. Greedy decoding removes the sampling term but measures a different quantity, the model's single most likely answer, and even greedy outputs are not perfectly reproducible on GPUs, where batched kernels change the order of floating-point sums.

Clustered questions. Many benchmarks group questions, several about one reading passage, one coding problem with many tests, or one generated template with different numbers. Questions in a group succeed or fail together, so they carry less information than the same number of independent questions. The clustered standard error sums the residuals within each cluster cc before squaring,

SE⁡cl(xˉ)=1n∑c(∑i∈c(xi−xˉ))2,\operatorname{SE}_{\text{cl}}(\bar x) = \frac{1}{n}\sqrt{\sum_{c}\Big(\sum_{i \in c} (x_i - \bar x)\Big)^2},

which equals the plug-in standard error ∑i(xi−xˉ)2/n\sqrt{\sum_i (x_i - \bar x)^2}/n when every cluster has one question, and grows when questions in a cluster move together. For two models, apply it to the differences did_i.

Training seeds. Every interval above measures the noise of the questions. The fine-tuning run is random too. Dodge et al. (2020) fine-tuned BERT hundreds of times on each of four small GLUE tasks, changing only the random seeds that set the new classifier's initialization and the order of the training data. The scores moved so much that the best of many reruns of plain BERT rivaled results reported for newer models, and the two seeds contributed comparably to the spread. When a decision rests on a gain of a point or two, train two or three seeds and report their spread next to the question-level interval.

Here are the first two effects on a simulated reading benchmark, 100 passages with 5 questions each, graded on 4 sampled answers per question.

Python · cell 2

Treating the 2,000 sampled answers as independent claims an error bar of 1.1 points. The honest per-question value is 1.5, and once the passages are accounted for it is 2.7, two and a half times the naive number. The sampling term is worth reducing up to a point. Going from 1 sample per question to 4 shrinks the per-question error from 2.2 to 1.5 points, while 64 samples reach only 1.3, because the between-question term does not budge. Past a handful of samples, buy questions.

Interview

Error bars for sampled answers and clustered questions

Grades come as an array S of shape (n, K), the 0/1 (or real-valued) grades of KK sampled answers to each of nn questions, and clusters, an integer array of length nn naming each question's cluster. Implement eval_error_bars(S, clusters) returning a dict with

  • "mean", the mean of the per-question scores xix_i (the row means of S),
  • "se", the per-question standard error ∑i(xi−xˉ)2/n\sqrt{\sum_i (x_i - \bar x)^2}/n (the plug-in variance, dividing by nn),
  • "se_clustered", the clustered standard error above,

and se_for_k(S, k_new), the per-question standard error predicted for k_new samples per question, (σ^b2+σ^w2/knew)/n\sqrt{(\hat\sigma_b^2 + \hat\sigma_w^2/k_{\text{new}})/n}, with σ^w2\hat\sigma_w^2 the mean over questions of the within-question variance (ddof=1, so K≥2K \ge 2), and σ^b2\hat\sigma_b^2 the plug-in variance of the xix_i minus σ^w2/K\hat\sigma_w^2/K, clipped at 0.

Hidden tests, hints, and AI help, from $1.99 a month.

LLM judges and their biases

Open-ended answers have no checker, so the field grades them with a strong model. Zheng et al. (2023) introduced MT-Bench, 80 multi-turn questions in 8 categories (writing, roleplay, extraction, reasoning, math, coding, and two kinds of knowledge), graded by GPT-4, and showed that GPT-4's judgments agreed with human experts' over 80% of the time, the same level at which humans agreed with each other. Judges come in two forms. Single-answer grading asks for a score, say 1 to 10, and is cheap but drifts in scale. Pairwise comparison shows two responses and asks which is better, which is more reliable and is what AlpacaEval and Chatbot Arena-style evaluations report as win rates.

The same paper catalogued the biases, and every one of them matters for SFT.

  • Position bias. Judges tend to prefer the response shown first (or, for some judges, second). The fix is to judge every pair in both orders and count a win only when the verdicts agree, or average the two.
  • Verbosity bias. Judges prefer longer answers, even when the extra length adds nothing. SFT data and tricks such as NEFTune (§14.4) often make answers longer, so a judge can reward the length rather than the content. AlpacaEval's length-controlled win rate fits a regression of the judge's preference on the length difference and reports the win rate with that term set to zero, which raised its Spearman correlation with Chatbot Arena's human rankings from 0.94 to 0.98 (Dubois et al. 2024).
  • Self-enhancement bias. A judge may favor responses from its own model family, so do not let a model judge its own outputs or its distillation students without checking against humans.
  • Limited reasoning. A judge can be fooled on math and code it cannot solve itself. Reference-guided grading, giving the judge a correct answer, helps.

Here is a simulated judge with a position bias and a length bias, comparing a fine-tuned model A that is genuinely better against a model B that writes 30% longer.

Python · cell 3

Shown first, A wins 61% of comparisons, and shown second only 41.5%, a 19-point swing from position alone. Averaging both orders removes the position bias but leaves 51%, nearly a tie, because B's extra length cancels A's better content. Controlling for length recovers a clear preference for A, 55%, close to the 56% that quality alone would give. Judges are indispensable and treacherous in equal measure, and the full treatment of building and validating them is a subject of its own, using language models as evaluators. Correcting a judge with a few hundred human labels is covered later in this section.

Core

Pairwise judging with both orders

For each of nn prompts a judge compared the responses of models A and B twice. ab[i] is the verdict with A shown first and ba[i] with B shown first, each one of "first", "second", or "tie". Implement pairwise_summary(ab, ba) returning a dict with

  • "win_rate", A's win rate counting a prompt as a win for A only if both orders favor A, a loss only if both favor B, and a tie otherwise (ties count half),
  • "inconsistent", the fraction of prompts where the two orders disagree (a tie in either order counts as disagreement unless both are ties),
  • "first_bias", the fraction of all 2n2n verdicts that chose the first position.
Hidden tests, hints, and AI help, from $1.99 a month.

Correcting a judge with a few human labels

Swapping orders and controlling for length remove the biases you can name. A judge also makes errors you cannot name, and those are not symmetric. If it wrongly sides with model A more often than with model B, its win rate for A is biased however many prompts you judge, and more prompts only make the wrong number more precise. Humans are unbiased by definition here (their preference is what we want to estimate), but expensive. The way out is to use both.

Judge all NN prompts, and have humans label a random subset of nn of them as well. On that subset you can see the judge's errors, and their average is exactly the judge's bias. So estimate the win rate as the judge's average over everything, corrected by its average error on the labeled subset,

θ^PP=1N∑i=1Nji+1n∑i∈labeled(hi−ji),SE⁡(θ^PP)≈Var⁡(j)N+Var⁡(h−j)n,\hat\theta_{\text{PP}} = \frac{1}{N}\sum_{i=1}^{N} j_i + \frac{1}{n}\sum_{i \in \text{labeled}} (h_i - j_i), \qquad \operatorname{SE}(\hat\theta_{\text{PP}}) \approx \sqrt{\frac{\operatorname{Var}(j)}{N} + \frac{\operatorname{Var}(h - j)}{n}},

with jij_i the judge's verdict on prompt ii (1 if it prefers A) and hih_i the human's. In plain words, the judge supplies the volume and the humans supply the correction. The first term has mean equal to the judge's win rate, the second has mean equal to the human rate minus the judge's, so the sum has mean equal to the human rate, whatever the judge's bias. This is prediction-powered inference (Angelopoulos et al. 2023), and Boyeau et al. (2024) applied it to model evaluation. The better the judge agrees with humans, the smaller Var⁡(h−j)\operatorname{Var}(h - j), and the more each human label is worth. A useless judge leaves you exactly where humans alone would.

Python · cell 4

The judge alone is precise and wrong, 62% for a true 55%, and no amount of judging fixes it. The 150 human labels alone are right on average but uncertain by 4.2 points. Together they are right on average with a spread of 3.2 points, as good as about 260 human labels, and the interval covers the truth at its promised rate. This judge agrees with humans only 84% of the time. A judge that agrees 95% of the time would make each human label worth several, which is why teams invest in improving the judge and keep a human-labeled slice of every evaluation.

Research

A prediction-powered win rate

Implement ppi_mean(judge_all, judge_lab, human_lab, z=1.96). judge_all holds the judge's scores on all NN prompts, and judge_lab and human_lab the judge's and the humans' scores on the nn labeled prompts (in the same order). Return a dict with "estimate", θ^PP\hat\theta_{\text{PP}}, "se", the standard error above with sample variances (ddof=1), and "ci", the interval θ^PP±z SE\hat\theta_{\text{PP}} \pm z\,\text{SE} as a tuple.

Hidden tests, hints, and AI help, from $1.99 a month.

Human review

Humans remain the reference for what "good" means, and they are expensive and noisy in their own ways. Gudibande et al. (2023) showed the classic failure. Crowd raters judged imitation models nearly as good as ChatGPT because the style was right, and missed that the facts were not (§14.2). Good human evaluation is designed to avoid exactly that.

  • Blind and randomized. Raters see two anonymized responses in random order, never model names.
  • A rubric, not a vibe. Define what to judge (correctness, completeness, instruction following, tone, safety) and in what priority, with examples of each grade. Ask for correctness checks on factual and technical prompts, by raters who can check them.
  • Calibration and agreement. Start with a calibration round on shared items, measure inter-annotator agreement, and revise the guidelines until it is acceptable. Cohen's κ\kappa corrects raw agreement for the agreement two raters would reach by chance,
κ=po−pe1−pe,\kappa = \frac{p_o - p_e}{1 - p_e},

where pop_o is the observed fraction of items on which they agree and pe=∑cp1c p2cp_e = \sum_c p_{1c}\,p_{2c} is the agreement expected if each rater labeled independently with their own label frequencies p1cp_{1c} and p2cp_{2c}. In plain words, κ=0\kappa = 0 is chance and 1 is perfect agreement. Two raters who both say "A is better" 90% of the time agree 82% of the time by chance alone, so 85% raw agreement is barely above chance.

  • Enough items. A win rate from 100 comparisons has a standard error of about 5 points, so a difference of a few points needs several hundred.
Warm-up

Cohen's kappa

Implement cohen_kappa(r1, r2) for two equal-length lists of categorical labels. Compute pop_o, the fraction of items with equal labels, and pe=∑cp1cp2cp_e = \sum_c p_{1c}p_{2c} over all categories that either rater used. Return 1.0 when pe=1p_e = 1 (both raters always used the same single label).

Hidden tests, hints, and AI help, from $1.99 a month.

Building a small eval set of your own

Public benchmarks tell you how your model compares with others. A small, private eval set tells you whether it does your job. Every serious team has one, and it is worth more per item than anything public. Here is how to build it.

  1. Start from real usage. Sample prompts from production logs or user studies (with the privacy review that requires), or have the people who will use the model write the requests they actually make. Synthetic prompts are fine for coverage, but real ones carry the phrasing and the hard cases.
  2. Read outputs before you write metrics. Run the current model on a few hundred prompts, read the outputs, and write down every failure you see. Group the failures into categories. The categories become your eval's structure, and their frequencies tell you what to fix first. Practitioners describe this error analysis as the highest-value step in building evals.
  3. Stratify and size it. Give each category enough items to detect the differences you care about, using the sample-size formulas of §0.11. A few hundred items in total, with 30 to 100 per important category, is typical.
  4. Write the grader for each item. Prefer verifiable checks (exact answers, unit tests, format validators, the IFEval style). Where judgment is needed, write a rubric and a reference answer, and validate an LLM judge against human labels on a subset before relying on it, then keep that labeled subset to correct the judge's win rate as above.
  5. Include regressions and refusals. Keep items the model must keep doing well (formats, facts, behaviors that past versions got right), harmful requests it must decline, and benign ones that superficially resemble harmful requests, which it must not decline.
  6. Keep it clean and frozen. Decontaminate your training data against the eval set (§14.2), never train on it, version it, and add new items as new failures appear rather than editing old ones, so that scores stay comparable across versions.
Python · cell 5

Even with this pessimistic calculation (it ignores the correlation that pairing exploits), a category near 90% accuracy needs about 280 items to see a 5-point change, and one near 60% needs about 740. That is why teams track a few categories closely and accept wider error bars elsewhere, and why the paired analysis, which can cut those numbers substantially, is worth the trouble.

The real way

The evaluation harness most open models are scored with is EleutherAI's lm-evaluation-harness. It runs the same tasks for a base model and its fine-tuned version, with or without the chat template. It is shown, not run, because it downloads models and data.

# base model, few-shot, the way base models are usually reported
lm_eval --model hf --model_args pretrained=meta-llama/Llama-3.1-8B,dtype=bfloat16 \
        --tasks mmlu,gsm8k --num_fewshot 5 --batch_size auto --output_path results/base --log_samples
# the fine-tuned model, same tasks and shots, then again in its chat template
lm_eval --model hf --model_args pretrained=./my-sft-model,dtype=bfloat16 \
        --tasks mmlu,gsm8k --num_fewshot 5 --batch_size auto --output_path results/sft --log_samples
lm_eval --model hf --model_args pretrained=./my-sft-model,dtype=bfloat16 \
        --tasks ifeval,gsm8k --apply_chat_template --fewshot_as_multiturn --batch_size auto --output_path results/sft-chat --log_samples

--log_samples saves per-item results, which is what makes paired error bars possible. Compare models item by item, with the paired bootstrap or McNemar's test of §0.11 and the clustered errors above, rather than comparing two headline numbers.

Read more

Watch next

🎬 How To Build AI Evals · Hamel HusainError analysis first, then targeted checks and judges, the practitioner's method behind building a small eval set.
🎬 LLM as a Judge: Scaling AI Evaluation Strategies · IBM TechnologyA short overview of pointwise and pairwise judging and the biases to control for.