n4nAI

Common language model evaluation metrics, explained

A practitioner's guide to the language model evaluation metrics that actually matter — perplexity, accuracy, F1, BLEU, BERTScore, LLM-as-judge, and major benchmark suites — with code snippets and guidance on when to use each.

n4n Team6 min read1,423 words

Audio narration

Coming soon — every post will get a voice note here.

If you’ve ever stared at a model card claiming “state-of-the-art performance” and wondered what that actually means for your production workload, you’re not alone. Language model evaluation metrics are the vocabulary we use to translate model behavior into engineering decisions. Some measure statistical fluency, others measure task correctness, and a growing category measures alignment with human preferences. This guide walks through the metrics you’ll encounter in papers, leaderboards, and production monitoring dashboards — with the context you need to pick the right ones for your use case.

1. Perplexity

Perplexity is the foundational metric for autoregressive language models. It measures how “surprised” a model is by a test sequence, expressed as the exponentiated average negative log-likelihood per token. Lower is better; a perplexity of 1 means the model assigns probability 1 to every token in the test set.

import math
import torch

def perplexity(log_probs: torch.Tensor, mask: torch.Tensor = None) -> float:
    """
    log_probs: [batch, seq_len] log probabilities of target tokens
    mask: optional [batch, seq_len] boolean mask (1 = valid token)
    """
    if mask is None:
        mask = torch.ones_like(log_probs, dtype=torch.bool)
    nll = -(log_probs * mask).sum() / mask.sum()
    return math.exp(nll.item())

Perplexity correlates with downstream task performance — up to a point. It’s sensitive to tokenization differences (a model with a larger vocabulary will typically show lower perplexity on the same text) and doesn’t capture factual correctness, reasoning, or instruction following. Use it for: comparing base model checkpoints during pretraining, detecting distribution shift in your data, or as a sanity check before running expensive evaluations. Don’t use it as your only metric for chat or RAG systems.

2. Accuracy and exact match

For classification, multiple-choice, and closed-form QA tasks, accuracy is the fraction of predictions that exactly match the ground truth. Exact match (EM) is the stricter variant used in extractive QA (e.g., SQuAD): the predicted answer span must match the gold span character-for-character.

def exact_match(pred: str, gold: str, normalize: bool = True) -> bool:
    if normalize:
        pred = pred.strip().lower()
        gold = gold.strip().lower()
    return pred == gold

def accuracy(predictions: list[str], references: list[str]) -> float:
    return sum(exact_match(p, g) for p, g in zip(predictions, references)) / len(predictions)

Accuracy is interpretable and cheap to compute. Its weakness: it treats “Paris” and “London” as equally wrong when the answer is “Berlin,” and it gives zero credit for partially correct responses. For generative tasks where multiple valid answers exist, accuracy alone is misleading. Pair it with softer metrics or human evaluation.

3. F1 score (token-level and span-level)

F1 balances precision and recall at the token level. In extractive QA, it measures overlap between predicted and gold answer spans after tokenization. In summarization or translation, it can be computed over n-grams (see ROUGE below). Token-level F1 is more forgiving than exact match and correlates better with human judgment on span prediction tasks.

from collections import Counter

def token_f1(pred: str, gold: str, tokenizer) -> float:
    pred_tokens = tokenizer(pred)
    gold_tokens = tokenizer(gold)
    common = Counter(pred_tokens) & Counter(gold_tokens)
    num_same = sum(common.values())
    if num_same == 0:
        return 0.0
    precision = num_same / len(pred_tokens)
    recall = num_same / len(gold_tokens)
    return 2 * precision * recall / (precision + recall)

Choose a tokenizer consistent with your evaluation benchmark (often whitespace or a specific subword tokenizer). F1 degrades gracefully with partial overlap, making it the default for SQuAD-style leaderboards. It still ignores semantic equivalence — “USA” vs “United States” scores zero overlap.

4. BLEU and ROUGE

BLEU (Bilingual Evaluation Understudy) and ROUGE (Recall-Oriented Understudy for Gisting Evaluation) are n-gram overlap metrics from machine translation and summarization. BLEU emphasizes precision (penalizing hallucinated n-grams); ROUGE-L emphasizes recall via longest common subsequence. Both compute geometric means of n-gram precisions (BLEU) or F1 scores (ROUGE) with a brevity penalty.

# Using sacrebleu for reproducible BLEU
import sacrebleu

def compute_bleu(predictions: list[str], references: list[list[str]]) -> float:
    """references is list of list: each prediction can have multiple references"""
    bleu = sacrebleu.corpus_bleu(predictions, references, tokenize='13a')
    return bleu.score  # 0-100 scale

# Using rouge-score for ROUGE-L
from rouge_score import rouge_scorer

def compute_rouge_l(predictions: list[str], references: list[str]) -> float:
    scorer = rouge_scorer.RougeScorer(['rougeL'], use_stemmer=True)
    scores = [scorer.score(ref, pred)['rougeL'].fmeasure
              for pred, ref in zip(predictions, references)]
    return sum(scores) / len(scores)

These metrics are fast, deterministic, and widely reported. They correlate poorly with human judgment on open-ended generation, creative writing, or factual QA. Use them for: translation, summarization, and constrained generation where n-gram fidelity matters. Avoid them as primary metrics for chat, coding, or reasoning tasks.

5. BERTScore and semantic similarity metrics

BERTScore computes token-level similarity using contextual embeddings from a pretrained transformer (typically RoBERTa or DeBERTa). It matches predicted and reference tokens via greedy cosine similarity, then reports precision, recall, and F1. Unlike n-gram metrics, it captures paraphrase equivalence (“car” ≈ “automobile”).

from bert_score import score as bert_score

def compute_bertscore(predictions: list[str], references: list[str], lang: str = 'en') -> dict:
    P, R, F1 = bert_score(predictions, references, lang=lang, verbose=False)
    return {
        'precision': P.mean().item(),
        'recall': R.mean().item(),
        'f1': F1.mean().item()
    }

BERTScore correlates better with human judgment on semantic adequacy than BLEU/ROUGE. It’s slower (requires a forward pass through an encoder) and introduces its own model dependency — scores shift if you change the backing encoder. Use it when: evaluating paraphrase generation, checking factual consistency in summarization, or as a supplement to n-gram metrics. Don’t treat it as ground truth; it can reward fluent but hallucinated text.

6. LLM-as-judge (pairwise and pointwise)

Using a strong LLM to evaluate another model’s outputs has become the de facto standard for open-ended tasks. Two common protocols: pairwise (model A vs model B, judge picks winner or tie) and pointwise (judge scores a single output on a rubric, e.g., 1-5 on helpfulness, correctness, style).

# Minimal pairwise judge prompt template
PAIRWISE_PROMPT = """You are an impartial evaluator. Compare two responses to the same prompt.

Prompt: {prompt}

Response A: {response_a}

Response B: {response_b}

Which response is better? Consider: instruction following, correctness, clarity, and safety.
Answer with exactly one word: "A", "B", or "Tie"."""

# Pointwise rubric example
POINTWISE_PROMPT = """Rate the following response on a scale of 1-5 for each criterion.

Prompt: {prompt}
Response: {response}

Criteria:
- Instruction following (1-5)
- Factual correctness (1-5)
- Clarity and structure (1-5)
- Safety (1-5)

Output JSON: {{"instruction_following": int, "factual_correctness": int, "clarity": int, "safety": int}}"""

LLM-as-judge scales to thousands of samples and captures nuance that n-gram and embedding metrics miss. Caveats: judge models have their own biases (verbosity bias, self-preference, position bias in pairwise), and results vary with prompt engineering. Mitigate by: randomizing order in pairwise, using multiple judges, calibrating against human annotations on a subset, and reporting confidence intervals. This is currently the best available proxy for human preference at scale.

7. Major benchmark suites (MMLU, GSM8K, HumanEval, BBH, etc.)

Leaderboards aggregate multiple tasks into a single headline number. The most cited:

Benchmark Domain Format Size Notes
MMLU General knowledge / reasoning 4-choice multiple choice ~16k questions 57 subjects, tests world knowledge + reasoning
GSM8K Grade-school math reasoning Free-form numeric answer 8.5k train / 1k test Multi-step reasoning, chain-of-thought helps
HumanEval Code generation (Python) Function completion + unit tests 164 problems Pass@k metric (k=1, 10, 100)
MBPP Basic Python programming Function completion + tests ~1k problems Simpler than HumanEval
BBH Big-Bench Hard Multiple choice / free form 6.5k examples 23 tasks where chain-of-thought helps
TruthfulQA Truthfulness / hallucination Multiple choice + generation 817 questions Adversarial, measures sycophancy
MT-Bench Chat / instruction following LLM-as-judge (GPT-4) 80 questions Multi-turn, pairwise scoring
# Example: computing Pass@k for HumanEval
import json
import subprocess
import tempfile
import os

def pass_at_k(samples: list[str], test_cases: list[dict], k: int = 1) -> float:
    """
    samples: list of k code completions per problem
    test_cases: list of dicts with 'test' (pytest string) and 'entry_point' (function name)
    """
    pass_counts = 0
    for problem_samples, tc in zip(samples, test_cases):
        passed = 0
        for sample in problem_samples[:k]:
            with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
                f.write(sample + "\n" + tc['test'])
                fname = f.name
            try:
                result = subprocess.run(['python', '-m', 'pytest', fname, '-v'],
                                        capture_output=True, timeout=10)
                if result.returncode == 0:
                    passed += 1
            finally:
                os.unlink(fname)
        if passed > 0:
            pass_counts += 1
    return pass_counts / len(test_cases)

No single benchmark captures production readiness. MMLU correlates with general capability but not with your specific domain. HumanEval measures algorithmic coding, not framework familiarity or debugging. Run benchmarks to track relative progress across model versions, but validate with your own eval set.

8. Calibration and confidence metrics

A well-calibrated model’s predicted probabilities match empirical frequencies: when it says 90% confidence, it’s correct 90% of the time. Calibration matters for: routing (send low-confidence predictions to humans), selective prediction (abstain when uncertain), and risk-sensitive applications.

import numpy as np
from sklearn.calibration import calibration_curve

def expected_calibration_error(probs: np.ndarray, labels: np.ndarray, n_bins: int = 10) -> float:
    """
    probs: predicted probabilities for the positive class (or max class prob for multiclass)
    labels: binary correctness (1 = correct, 0 = incorrect)
    """
    prob_true, prob_pred = calibration_curve(labels, probs, n_bins=n_bins, strategy='uniform')
    bin_counts = np.histogram(probs, bins=n_bins, range=(0, 1))[0]
    ece = np.sum(bin_counts * np.abs(prob_true - prob_pred)) / len(probs)
    return ece

def brier_score(probs: np.ndarray, labels: np.ndarray) -> float:
    return np.mean((probs - labels) ** 2)

ECE (Expected Calibration Error) and Brier score are the standard summaries. Most LLMs are poorly calibrated out of the box — they’re overconfident. Temperature scaling on a validation set helps. If you’re building a system that routes or abstains based on confidence, measure calibration on your actual task distribution, not a generic benchmark.

9. Operational metrics: latency, throughput, and cost

Production evaluation isn’t just quality — it’s the quality-latency-cost Pareto frontier. Key operational metrics:

  • TTFT (Time To First Token): latency from request send to first token received. Critical for streaming UX.
  • TPOT (Time Per Output Token): inverse of throughput during generation. Determines total latency for long responses.
  • Throughput (tokens/sec): aggregate across concurrent requests. GPU utilization proxy.
  • Cost per 1k tokens: blended across input/output, provider, and model tier.
# Simple latency measurement wrapper
import time
from dataclasses import dataclass
from typing import Iterator

@dataclass
class LatencyMetrics:
    ttft: float  # seconds
    tpot: float  # seconds per token
    total_tokens: int
    total_time: float

def measure_streaming_latency(stream: Iterator[str]) -> LatencyMetrics:
    start = time.perf_counter()
    first_token_time = None
    tokens = 0
    for chunk in stream:
        if first_token_time is None:
            first_token_time = time.perf_counter()
        tokens += 1
    end = time.perf_counter()
    ttft = first_token_time - start if first_token_time else 0
    total_time = end - start
    tpot = (total_time - ttft) / max(tokens - 1, 1) if tokens > 1 else 0
    return LatencyMetrics(ttft, tpot, tokens, total_time)

Track these per model, per provider, and per request priority tier. A model that scores 2% higher on MMLU but costs 10x more and has 3x TTFT may be the wrong choice for your chat endpoint. n4n.ai surfaces per-token usage metering and provider-level latency histograms so you can make these tradeoffs with data instead of guesses.

10. Robustness and out-of-distribution detection

Evaluation on clean test sets misses failure modes that appear in production: adversarial prompts, distribution shift, multilingual degradation, and context-length extrapolation. Robustness metrics include:

  • Adversarial accuracy: accuracy on perturbed inputs (typos, synonym swaps, prompt injections)
  • OOD detection AUROC: using model confidence or entropy to detect out-of-distribution inputs
  • Length generalization: performance vs. context length (needle-in-haystack, long-context QA)
  • Multilingual parity: gap**: performance delta between English and low-resource languages
# Needle-in-haystack test: can the model retrieve a fact buried in context?
def needle_in_haystack(model, tokenizer, context_length: int, needle: str, question: str) -> bool:
    # Build context: filler text + needle at random position + filler
    filler = "The quick brown fox jumps over the lazy dog. " * 100
    tokens = tokenizer.encode(filler)
    insert_pos = len(tokens) // 2
    needle_tokens = tokenizer.encode(needle)
    context_tokens = tokens[:insert_pos] + needle_tokens + tokens[insert_pos:]
    context_tokens = context_tokens[:context_length]
    context = tokenizer.decode(context_tokens)
    prompt = f"{context}\n\nQuestion: {question}\nAnswer:"
    # Generate and check if needle content appears in answer
    answer = model.generate(prompt, max_tokens=50)
    return needle.lower() in answer.lower()

Run these as regression tests in CI. They catch silent degradations when you swap models, update prompts, or change retrieval pipelines.

Summary: choosing your metric stack

Task type Primary metrics Supplemental Avoid as sole metric
Pretraining / base model selection Perplexity, downstream few-shot Scaling law projections Accuracy on any single benchmark
Classification / multiple-choice Accuracy, F1, AUROC Calibration (ECE) Perplexity
Extractive QA / span prediction Exact Match, Token F1 BERTScore BLEU/ROUGE
Summarization / translation ROUGE-L, BLEU, BERTScore LLM-as-judge (faithfulness) Perplexity, Accuracy
Code generation Pass@k (HumanEval, MBPP) LLM-as-judge (style, security) BLEU, Perplexity
Chat / instruction following LLM-as-judge (pairwise vs baseline) MT-Bench, AlpacaEval BLEU, ROUGE, Perplexity
RAG / grounded generation LLM-as-judge (groundedness, citation) BERTScore vs retrieved docs BLEU, Perplexity
Production monitoring Latency (TTFT, TPOT), cost, error rate Sampled LLM-as-judge, user feedback Offline benchmark scores

Start with one primary metric that reflects your actual user-facing quality bar. Add one operational metric (latency or cost). Run LLM-as-judge on a representative sample weekly. Benchmark suites are for model selection, not production monitoring. The metric that correlates with your business outcome is the only one that ultimately matters — everything else is a proxy.

Tagsevaluation-metricsperplexitybenchmarksglossary

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All perplexity & language model evaluation metrics posts →