Contents Working with Large Language Models Code Course home

Chapter 22Evaluating Generated Text

The measurement problem

Chapter 14 evaluated a classifier and the job was straightforward, because there is one right label, the model gives one label, and you compare. Generation breaks that, since there are many correct outputs, they do not resemble each other as strings, and nobody can enumerate them.

Take a translation of “Il pleut”, where It is raining, It’s raining and Rain is falling are all correct, so a metric that credits only the first is measuring the wrong thing. Everything in this chapter is an attempt to work around that, and none of the attempts fully succeeds.

When the answer is a label

Where the output space is closed, use Chapter 14’s machinery unchanged, meaning per-class precision, recall and F1F_1, macro-averaged, with a confusion matrix to say what is failing. Two cautions carry over from there, since accuracy hides which class is wrong and micro-F1F_1 equals accuracy on single-label tasks, so reporting both tells you nothing twice. The rest of the chapter is about the open case.

Overlap metrics

The oldest workable idea is to compare nn-grams against one or more references and count the matches.

Precision has to be clipped

Start with unigram precision, which asks what fraction of the candidate’s words appear in a reference. It is trivially gameable, so take the references “the cat is on the mat” and “there is a cat on the mat”, along with the candidate:

the the the the the the the

Every one of those seven words appears in a reference, so unclipped precision is 7/7=1.00007/7 = 1.0000, which is a perfect score for a sentence that says nothing. Modified precision caps each nn-gram at the most any single reference contains, and reference one has the twice, so the ceiling is two.

candidate clipped total p1p_1
the the the …(7 times) 2 7 0.2857
the cat is on the mat 6 6 1.0000

Clipping is not a refinement, because without it the metric can be won by repeating one word and would measure nothing at all.

The brevity penalty

Precision still rewards saying less, as it does against the reference “the cat is on the mat”:

candidate length p1p_1 BP p1×p_1 \times BP
the 1 1.0000 0.0067 0.0067
the cat 2 1.0000 0.1353 0.1353
the cat is on 4 1.0000 0.6065 0.6065
the cat is on the mat 6 1.0000 1.0000 1.0000
the cat is on the mat today for a while 10 0.6000 1.0000 0.6000

The first row is one word, perfectly precise and useless, and precision alone would rank it top. The brevity penalty fixes that:

BP={1,c>r,exp⁡(1−r/c),c≤r,(22.1)\begin{equation} \text{BP} \;=\; \begin{cases} 1, & c > r, \\ \exp\!\big(1 - r/c\big), & c \le r, \end{cases} \label{eq:bp} \quad\text{(22.1)} \end{equation}

where cc is the candidate length and rr the closest reference length.

It is deliberately asymmetric, so that too short is punished while too long is not, because a long candidate is already punished by precision, as the last row shows.

BLEU, end to end

BLEU (Papineni et al. 2002) combines clipped precisions for n=1n = 1 to 44, using their geometric mean, times the brevity penalty.

candidate p1p_1 p2p_2 p3p_3 p4p_4 BLEU-4
the cat is on the mat 1.0000 1.0000 1.0000 1.0000 1.0000
a cat is on the mat 1.0000 1.0000 0.7500 0.6667 0.8409
the cat sat on the mat 0.8333 0.6000 0.2500 0.0000 0.0000
on mat the cat is the 1.0000 0.4000 0.2500 0.0000 0.0000

Three things are worth reading off that table. The geometric mean means one zero sends the whole score to zero, so the third candidate differs from a reference by a single word and scores 00. That severity is why BLEU is reported over a corpus rather than a sentence, since on one sentence it is close to useless.

The last candidate is the reference words in scrambled order, where the unigram precision is perfect and the higher nn-grams collapse, and that is the only way BLEU sees word order at all.

ROUGE, and the other direction

BLEU asks how much of what you said was in the reference, which is precision, and it suits translation.

Summarisation cares about the opposite. Did you leave out the important part? That is recall, and it is what ROUGE (Lin 2004) measures.

One bookkeeping note before the table, because it changes the numbers. The BLEU table above scored against two references, as BLEU classically does, whereas the table below scores against the single reference “the cat is on the mat”. So the BLEU column here is not the BLEU column there, and only the exact match scores 11 in both.

candidate BLEU-4 ROUGE-1 ROUGE-2
the cat is on the mat 1.0000 1.0000 1.0000
the cat 0.0000 0.3333 0.2000
the cat is on the mat and also on a fine rug 0.4172 1.0000 1.0000
the feline rests upon the rug 0.0000 0.3333 0.0000

Row three is padded and ROUGE gives it a perfect score, because every reference nn-gram is present. Recall alone can be won by saying everything, which is why ROUGE is normally reported with an FF-measure variant.

The row that matters

Row four is a correct paraphrase of the reference and it shares no content word with it. BLEU gives it 0.00000.0000, ROUGE-2 gives it 0.00000.0000, and ROUGE-1 gives it 0.33330.3333, every point of which comes from the word the. Not one content word was credited, and the candidate is right.

Neither metric has any notion of meaning. They count string overlap and a paraphrase overlaps in the wrong places, so that single row is the argument for everything that came after.

Try it yourself. code/worked_examples/bleu.py produces every table here. --clip builds the degenerate candidate, --brevity sweeps the length term, --bleu scores four candidates in full, and --rouge runs the paraphrase.

Run it in ColabNotebookSource

Beyond surface overlap

There are two families of response, and both trade a known weakness for a new one.

Embedding-based metrics compare meaning rather than strings, so BERTScore (Zhang et al. 2020) matches candidate and reference tokens by the cosine similarity of their contextual embeddings from Chapter 15. The paraphrase row above now scores well, which is the point, and the cost is that the metric is now a model with its own training data and its own blind spots, whose number cannot be recomputed by hand.

Model-as-judge asks a strong language model to rate an output against a rubric (Zheng et al. 2023). It handles open-ended tasks nothing else can score and correlates reasonably with human preference, while inheriting every bias of the judge model, including a documented preference for longer answers and for its own outputs. A judge is a measuring instrument that has opinions, so it is useful and it must be calibrated against human judgement rather than trusted.

Human evaluation

Human judgement remains the standard everything else is validated against, and it has its own methodology. Pairwise comparison is more reliable than absolute rating, because people are better at ranking two things than at scoring one, and multiple annotators with a reported agreement statistic are necessary, because a single annotator’s number is unmeasurable. It is slow and expensive, which is why the field keeps trying to automate it and keeps having to come back.

The metric that becomes the target

One closing caution, and it applies to every number in this chapter.

When a measure becomes a target, it ceases to be a good measure.

That is Goodhart’s law in Strathern’s phrasing (Strathern 1997), and benchmark history is a long demonstration of it. Benchmarks saturate, test sets leak into pretraining corpora, and models are tuned against the leaderboard until the leaderboard stops measuring the thing it was built to measure.

No better metric defends against this. The discipline of Chapter 14 does: state your metric, hold out your test set, report the runs that failed, and look at the actual outputs. A number is a summary of an experiment and not a substitute for having looked.

Further reading.

For the original definitions, read Papineni et al. (2002) on BLEU and Lin (2004) on ROUGE. Zhang et al. (2020) introduce BERTScore. Zheng et al. (2023) study language models as judges and document their biases carefully. Jurafsky and Martin (2025) cover evaluation across tasks with more examples. In practice BLEU comes from sacrebleu, which exists because BLEU scores were for years incomparable between papers, and ROUGE from rouge_score, while nltk.translate.bleu_score is the one closest to the arithmetic above.

Break unigram precision. Construct a candidate that scores 1.01.0 on unclipped unigram precision and is obviously useless. Then compute its clipped precision and its brevity penalty, and report the final score.

Why geometric. BLEU uses the geometric mean of four precisions. Compute both the arithmetic and geometric means of (0.83,0.60,0.25,0.00)(0.83, 0.60, 0.25, 0.00). Argue for the one BLEU uses, then argue against it, and say which argument you find stronger for a single sentence.

Asymmetric by design. Explain why the brevity penalty punishes short candidates and not long ones. Construct a candidate three times the reference length and show what stops it from scoring well.

The paraphrase. Write a correct paraphrase of “the cat is on the mat” sharing no content word with it. Compute BLEU-4 and ROUGE-1. Then explain precisely what an embedding-based metric would do differently.

Judge the judge. You are using a language model to score summaries. Design a procedure that would detect the length bias, and one that would detect self-preference. What would you do if you found either?

Goodhart in practice. Choose a benchmark from any chapter of this book and describe a way to score well on it without possessing the underlying ability. Then propose one change to the benchmark that would close your loophole, and name what your change breaks.

The default that reports failure. Score the four candidates of this chapter with nltk.translate.bleu_score.sentence_bleu against the single reference “the cat is on the mat”. You will get the BLEU column exactly, 1.00001.0000, 0.00000.0000, 0.41720.4172 and 0.00000.0000. Now score “the cat sits on the mat”, which is one word from perfect, and watch it return 0.00000.0000 as well. Pass smoothing_function=SmoothingFunction().method1 and it becomes 0.25410.2541. Explain which of the two numbers you would report, why the function is off by default, and what it means that the two zeros in the printed table arrive for different reasons.

Code for this chapter

Each one reproduces the tables above. Open it in Colab, change an input and watch which numbers move.

From Natural Language Processing: From Corpus Statistics to Grounded LLMs, by Ramaseshan Ramachandran.