Skip to content
SDB
Generative AI

Chapter 09 · intermediate · 25 min

Key Evaluation Metrics

Measuring LLM performance across reasoning, generation quality, and task-specific benchmarks

Subhendu Datta BhowmikAI Tutorials

Why Evaluation Is Hard

Evaluating LLMs is harder than evaluating classical ML models. A classifier predicting churn has a clear ground truth. An LLM writing a summary, answering a question, or generating code has:

  • Multiple valid outputs: "Paris" and "The capital of France is Paris" are both correct
  • Context-dependent quality: a brief answer may be good for a chatbot, bad for a report
  • Emergent capabilities: new abilities appear at scale, requiring new tests
  • Gaming risk: models may overfit to benchmarks

Good evaluation requires combining automatic metrics for scale with human evaluation or LLM-as-judge for nuance.

Intrinsic Metrics

Perplexity

Perplexity measures how well a model predicts a held-out test set. Lower = better.

PPL(W)=P(w1,w2,...,wN)1/N=exp(1Ni=1NlogP(wiw<i))\text{PPL}(W) = P(w_1, w_2, ..., w_N)^{-1/N} = \exp\left(-\frac{1}{N}\sum_{i=1}^{N}\log P(w_i | w_{<i})\right)

  • Perplexity = 1: perfect prediction (every next token is certain)
  • Perplexity = V (vocab size): random guessing
  • Typical LLM perplexity on web text: 5–20

Limitations: Perplexity measures fluency, not factuality or usefulness. A model can have low perplexity while hallucinating. It's useful for comparing models on the same test set but not for downstream task performance.

Reference-Based Metrics

BLEU (Bilingual Evaluation Understudy)

Originally for machine translation; measures n-gram overlap between generated text and reference(s).

BLEU=BPexp(n=1Nwnlogpn)\text{BLEU} = BP \cdot \exp\left(\sum_{n=1}^{N} w_n \log p_n\right)

where p_n is the precision of n-grams (usually n=1..4) and BP is a brevity penalty.

  • Range: 0–1 (or 0–100)
  • Good BLEU score: depends heavily on task; >40 is often considered good for MT
  • Limitations: penalizes valid paraphrases; doesn't measure fluency or semantics

ROUGE (Recall-Oriented Understudy for Gisting Evaluation)

Common for summarization; measures recall of n-grams from reference.

VariantMeasures
ROUGE-1Unigram overlap
ROUGE-2Bigram overlap
ROUGE-LLongest common subsequence

BERTScore

Uses contextual embeddings (from BERT) to compute semantic similarity between generated and reference text — more robust than n-gram overlap.

P=1y^y^jy^maxyiyxy^jTxyiP = \frac{1}{|\hat{y}|}\sum_{\hat{y}_j \in \hat{y}} \max_{y_i \in y} \mathbf{x}_{\hat{y}_j}^T \mathbf{x}_{y_i}

Computing ROUGE and BERTScorepython
from rouge_score import rouge_scorer
from bert_score import score as bert_score

# Reference summaries and model outputs
references = [
    "The study found that exercise reduces the risk of heart disease by 30%.",
    "Renewable energy sources now account for 20% of global electricity.",
]
hypotheses = [
    "Research shows physical activity decreases heart disease risk by roughly a third.",
    "Green energy represents a fifth of worldwide power generation.",
]

# ROUGE
scorer = rouge_scorer.RougeScorer(['rouge1', 'rouge2', 'rougeL'], use_stemmer=True)
for ref, hyp in zip(references, hypotheses):
    scores = scorer.score(ref, hyp)
    print(f"ROUGE-1: {scores['rouge1'].fmeasure:.3f}  "
          f"ROUGE-2: {scores['rouge2'].fmeasure:.3f}  "
          f"ROUGE-L: {scores['rougeL'].fmeasure:.3f}")
# ROUGE-1: 0.286  ROUGE-2: 0.154  ROUGE-L: 0.214
# ROUGE-1: 0.231  ROUGE-2: 0.000  ROUGE-L: 0.231
# (low scores even though the hypotheses are semantically correct)

# BERTScore — captures semantic similarity better
P, R, F1 = bert_score(hypotheses, references, lang="en", rescale_with_baseline=True)
for i, (p, r, f) in enumerate(zip(P, R, F1)):
    print(f"Pair {i+1}: Precision={p:.3f}, Recall={r:.3f}, F1={f:.3f}")
# Pair 1: Precision=0.872, Recall=0.891, F1=0.881  (correctly high)
# Pair 2: Precision=0.845, Recall=0.862, F1=0.853  (correctly high)

LLM-as-a-Judge

A powerful modern approach: use a strong LLM to evaluate another LLM's outputs. Widely used because it:

  • Scales to thousands of examples cheaply
  • Handles diverse outputs without reference texts
  • Correlates well with human preferences (~70–85% agreement)

Single-Answer Grading

Ask the judge to score a single response on a rubric:

Judge system: You are an expert evaluator. Rate the response on:
  - Correctness (1-5)
  - Completeness (1-5)
  - Clarity (1-5)
Return JSON: {"correctness": N, "completeness": N, "clarity": N, "rationale": "..."}

Pairwise Comparison (A/B Testing)

Present two responses and ask which is better. More reliable than absolute scoring.

Limitations

  • Position bias: LLMs prefer response A over B regardless of quality
  • Self-enhancement: a model may prefer its own style
  • Mitigate: randomize order, use CoT reasoning before judging, use a different family model as judge
LLM-as-a-Judge Evaluationpython
import anthropic
import json

client = anthropic.Anthropic()

JUDGE_SYSTEM = """You are an expert evaluator for AI-generated answers.
Evaluate the response on three criteria (1-5 each):
- accuracy: Is the information correct and factual?
- helpfulness: Does it actually answer the question asked?
- clarity: Is it well-written and easy to understand?

Respond ONLY with valid JSON in this exact format:
{"accuracy": N, "helpfulness": N, "clarity": N, "rationale": "brief explanation"}"""

def evaluate_response(question: str, response: str) -> dict:
    """Use Claude to judge a response quality."""
    result = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=256,
        system=JUDGE_SYSTEM,
        messages=[{
            "role": "user",
            "content": f"Question: {question}\n\nResponse to evaluate:\n{response}"
        }],
    )
    return json.loads(result.content[0].text)

def pairwise_compare(question: str, response_a: str, response_b: str) -> dict:
    """Compare two responses to determine which is better."""
    result = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=256,
        system="""Compare two responses and determine which is better.
Return JSON: {"winner": "A" or "B" or "tie", "reasoning": "brief explanation"}""",
        messages=[{
            "role": "user",
            "content": f"Question: {question}\n\nResponse A:\n{response_a}\n\nResponse B:\n{response_b}"
        }],
    )
    return json.loads(result.content[0].text)

# Example usage
question = "What is the capital of Australia?"
good_response = "The capital of Australia is Canberra, not Sydney as often mistakenly believed."
bad_response = "Australia's capital is Sydney, which is also its largest city."

scores = evaluate_response(question, good_response)
print(f"Scores: {scores}")

comparison = pairwise_compare(question, good_response, bad_response)
print(f"Winner: {comparison['winner']} — {comparison['reasoning']}")

Standard Benchmarks

Knowledge & Reasoning

BenchmarkWhat It TestsFormat
MMLU57 subject areas (math, science, law, medicine, etc.)4-choice MCQ
HellaSwagCommon sense completion4-choice MCQ
ARCGrade-school science4-choice MCQ
TruthfulQATruthfulness, avoiding false beliefsGeneration

Math & Reasoning

BenchmarkWhat It Tests
GSM8KGrade school math word problems
MATHCompetition-level math (algebra, geometry, etc.)
BBH (BIG-Bench Hard)Difficult reasoning, logic, and multi-step tasks

Code

BenchmarkWhat It Tests
HumanEvalPython function completion (164 problems)
MBPPMostly basic Python problems
SWE-benchReal GitHub issues — requires working code changes

Comprehensive

  • HELM: 42 scenarios across tasks and metrics with standardized evaluation
  • LMSYS Chatbot Arena: real-world human preference ranking (ELO)
  • OpenLLM Leaderboard: open-weight model comparisons on MMLU, ARC, etc.

Task-Specific RAG Evaluation

For RAG systems, evaluation covers retrieval and generation separately:

Retrieval metrics:

  • Context Recall: fraction of relevant ground-truth facts found in retrieved chunks
  • Context Precision: fraction of retrieved chunks that are actually relevant

Generation metrics:

  • Faithfulness: does the answer only use information from retrieved context?
  • Answer Relevancy: how relevant is the answer to the question?

RAGAS is a popular framework for automated RAG evaluation using LLM-as-judge:

from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_recall, context_precision

results = evaluate(
    dataset=eval_dataset,
    metrics=[faithfulness, answer_relevancy, context_recall, context_precision],
)
print(results)

Knowledge check

Why does BERTScore often correlate better with human judgment than BLEU for text generation tasks?

Summary

  • Perplexity measures language modeling quality but not task performance or factuality
  • BLEU and ROUGE measure n-gram overlap — useful but penalize valid paraphrases
  • BERTScore uses semantic embeddings to capture meaning, correlating better with human judgment
  • LLM-as-judge scales evaluation cheaply and handles diverse outputs — watch for position and self-enhancement bias
  • Key benchmarks: MMLU (knowledge), GSM8K (math), HumanEval (code), LMSYS Arena (human preference)
  • RAG evaluation requires separate metrics for retrieval (context recall/precision) and generation (faithfulness, relevancy)

Final chapter: Guardrails — building safety and quality controls into production AI systems.

Generative AI