BLEU, ROUGE, and perplexity are the three metrics you’ll encounter most often when evaluating LLM outputs, but they measure fundamentally different things. BLEU and ROUGE compare generated text against reference answers, making them suitable for translation, summarization, and any task with ground truth. Perplexity measures how surprised a model is by a sequence, which makes it a training-time diagnostic and a proxy for fluency — not correctness. Understanding these differences prevents you from optimizing the wrong signal.
What each metric actually measures
BLEU: precision-oriented n-gram overlap
BLEU (Bilingual Evaluation Understudy) computes the geometric mean of n-gram precisions (typically 1-gram through 4-gram) between candidate and reference texts, multiplied by a brevity penalty that discourages short outputs. It was designed for machine translation where multiple valid translations exist, and it correlates reasonably with human judgment when you have multiple references.
from nltk.translate.bleu_score import sentence_bleu, SmoothingFunction
reference = [["the", "cat", "sat", "on", "the", "mat"]]
candidate = ["the", "cat", "is", "on", "the", "mat"]
# Smoothing prevents zero scores when higher-order n-grams don't match
chencherry = SmoothingFunction()
score = sentence_bleu(reference, candidate, smoothing_function=chencherry.method1)
print(f"BLEU: {score:.4f}") # ~0.58
BLEU’s precision focus means it rewards conservative outputs that repeat high-confidence n-grams. It penalizes hallucination indirectly — if you generate tokens not in the reference, precision drops — but it doesn’t directly measure factuality.
ROUGE: recall-oriented n-gram and sequence overlap
ROUGE (Recall-Oriented Understudy for Gisting Evaluation) comes in several variants. ROUGE-N measures n-gram recall (like BLEU but recall instead of precision). ROUGE-L uses longest common subsequence (LCS) to capture sentence-level structure. ROUGE-S adds skip-bigram co-occurrence statistics. For summarization, ROUGE-L and ROUGE-1/2 are standard.
from rouge_score import rouge_scorer
scorer = rouge_scorer.RougeScorer(['rouge1', 'rouge2', 'rougeL'], use_stemmer=True)
reference = "The cat sat on the mat and looked around."
candidate = "The cat is on the mat looking around."
scores = scorer.score(reference, candidate)
for metric, score in scores.items():
print(f"{metric}: P={score.precision:.3f} R={score.recall:.3f} F1={score.fmeasure:.3f}")
ROUGE’s recall orientation means it rewards coverage of reference content. A verbose summary that includes everything in the reference (plus fluff) scores well on ROUGE but poorly on BLEU. This asymmetry matters: if your task penalizes missing key facts more than verbosity, ROUGE aligns better.
Perplexity: model uncertainty, not output quality
Perplexity is the exponentiated average negative log-likelihood of a sequence under a model: PPL = exp(-1/N Σ log P(token_i | context)). Lower perplexity means the model assigns higher probability to the observed tokens. It’s a property of the model on a dataset, not a direct comparison between candidate and reference.
import torch
import torch.nn.functional as F
def perplexity(logits, targets, ignore_index=-100):
"""
logits: (batch, seq_len, vocab_size)
targets: (batch, seq_len)
"""
log_probs = F.log_softmax(logits, dim=-1)
# Gather log probs of target tokens
target_log_probs = log_probs.gather(-1, targets.unsqueeze(-1)).squeeze(-1)
mask = (targets != ignore_index).float()
nll = -(target_log_probs * mask).sum() / mask.sum()
return torch.exp(nll).item()
# During evaluation, you compute perplexity on a held-out dataset
# Lower = model is less "surprised" by the data
Perplexity correlates with fluency and grammaticality, but a low-perplexity model can still hallucinate confidently. It’s primarily a training diagnostic — tracking validation perplexity tells you if the model is learning — and a model selection signal. It does not measure task performance.
Head-to-head comparison
| Dimension | BLEU | ROUGE | Perplexity |
|---|---|---|---|
| Primary signal | N-gram precision vs reference(s) | N-gram/LCS recall vs reference(s) | Model probability of held-out tokens |
| Reference required | Yes (1+, more is better) | Yes (1+, more is better) | No (evaluates model on corpus) |
| Task fit | Translation, constrained generation | Summarization, QA with ground truth | Model selection, training monitoring |
| Correlates with | Human adequacy judgments (with multi-ref) | Human informativeness judgments | Fluency, not factuality |
| Sensitive to | Length (brevity penalty), n-gram order | Coverage, verbosity | Token distribution, vocabulary |
| Gameable | Yes — short, safe outputs | Yes — verbose outputs covering refs | Yes — memorization, overfitting |
| Compute cost | Negligible | Negligible | Requires forward pass on eval set |
| Interpretability | 0-1 scale, higher better | 0-1 scale (F1), higher better | Positive real, lower better (typ. 10-100) |
| Multi-reference support | Native (geometric mean) | Native (max over refs) | N/A |
Where each metric fails
BLEU’s blind spots
BLEU correlates poorly with human judgment on open-ended generation. A fluent, factually correct answer that uses different phrasing than the reference scores near zero. The brevity penalty helps but doesn’t fix semantic equivalence. BLEU also ignores word order beyond 4-grams — “cat the mat on sat the” and “the cat sat on the mat” share many 4-grams.
For code generation, BLEU is particularly misleading. Two functionally equivalent programs with different variable names or formatting score poorly. Use CodeBLEU or execution-based evaluation instead.
ROUGE’s blind spots
ROUGE rewards extraction over abstraction. An extractive summary that copies sentences verbatim from the source often beats a well-written abstractive summary that paraphrases. ROUGE-L’s LCS component partially addresses this but still favors surface overlap.
ROUGE also can’t detect hallucination that happens to use words from the reference. If the reference says “revenue increased 5%” and your summary says “revenue decreased 5%”, ROUGE-1/2/L all score highly because the n-grams overlap.
Perplexity’s blind spots
Perplexity is the most dangerous metric to optimize directly. A model that memorizes the training set achieves near-zero perplexity but generalizes poorly. A model that outputs “I don’t know” for everything gets low perplexity on uncertain tokens but provides zero utility.
Perplexity also depends heavily on tokenization. A model with a larger vocabulary typically shows lower perplexity on the same text because each token carries more information. Comparing perplexity across different tokenizers is meaningless without normalization (bits per character or bits per byte).
Practical evaluation workflows
For translation: BLEU + chrF + human
# sacreBLEU provides standardized, reproducible BLEU with tokenization
pip install sacrebleu
# Evaluate with standard tokenization (intl for non-English)
sacrebleu -t wmt19 -l de-en --echo "model_output.txt" > results.json
Always report chrF++ alongside BLEU — it’s character-level F-score that correlates better with human judgment on morphologically rich languages. And budget for human evaluation on a 100-200 sample subset; automatic metrics are proxies, not ground truth.
For summarization: ROUGE + factuality + human
# Combine ROUGE with a factuality checker
from rouge_score import rouge_scorer
# Factuality: use a separate NLI model or LLM-as-judge
# e.g., entailment score between summary and source
scorer = rouge_scorer.RougeScorer(['rouge1', 'rouge2', 'rougeL'], use_stemmer=True)
rouge_scores = scorer.score(source_doc, generated_summary)
# Factuality check (pseudo-code)
# factuality = nli_model.entailment_score(source_doc, generated_summary)
ROUGE tells you coverage. You need a separate factuality metric — either an NLI model checking entailment from source to summary, or an LLM judge prompted to verify claims. n4n.ai’s routing can direct factuality checks to a specialized model while keeping the primary generation on a cost-efficient one.
For open-ended generation: perplexity (training) + LLM-as-judge (inference)
During training, track validation perplexity with early stopping:
# Training loop snippet
best_ppl = float('inf')
patience = 3
patience_counter = 0
for epoch in range(max_epochs):
train_epoch(model, train_loader)
val_ppl = evaluate_perplexity(model, val_loader)
if val_ppl < best_ppl:
best_ppl = val_ppl
save_checkpoint(model)
patience_counter = 0
else:
patience_counter += 1
if patience_counter >= patience:
break
At inference time, perplexity is useless for comparing outputs. Use LLM-as-judge with a rubric:
JUDGE_PROMPT = """Rate the response on a 1-5 scale for:
1. Instruction following
2. Factual accuracy
3. Reasoning quality
4. Clarity
Response: {response}
Reference: {reference}
Output JSON: {{"instruction_following": int, "factual_accuracy": int, ...}}"""
This correlates far better with human preference than any n-gram metric.
Which to choose
Choose BLEU when:
- You have multiple reference translations for the same source
- The task is translation or similarly constrained generation
- You need a fast, deterministic, reproducible metric for regression testing
- You’re comparing systems on standard benchmarks (WMT, IWSLT) where BLEU is established
Choose ROUGE when:
- The task is summarization, headline generation, or any compression task
- Recall (coverage of key information) matters more than precision (conciseness)
- You have reference summaries written by humans
- You need ROUGE-L for structural similarity or ROUGE-S for skip-gram flexibility
Choose perplexity when:
- Monitoring training dynamics — validation perplexity should decrease smoothly
- Selecting between model checkpoints or architectures pre-deployment
- Comparing tokenization schemes (normalize to bits-per-byte)
- Diagnosing distribution shift: test perplexity >> train perplexity indicates OOD data
Choose none of the above when:
- Evaluating open-ended chat, creative writing, or reasoning — use LLM-as-judge with a rubric
- Evaluating code — use execution accuracy (pass@k) or CodeBLEU
- Evaluating factual QA — use exact match / F1 on answer spans, or LLM judge for free-form
- Evaluating RAG — use retrieval metrics (recall@k, MRR) + generation faithfulness (NLI)
Combining metrics in practice
No single metric suffices. A production evaluation pipeline typically layers:
- Automatic metrics (BLEU/ROUGE/perplexity) for CI/CD regression gates — fast, deterministic, catch catastrophic regressions
- Model-based metrics (NLI for factuality, embedding similarity for semantic equivalence) for nightly evaluation on larger samples
- Human evaluation (side-by-side or absolute rating) on a small but representative sample weekly or per release
# Example: gated deployment check
def should_deploy(new_model, baseline_model, eval_set, thresholds):
results = {}
# Fast automatic metrics
results['bleu'] = compute_bleu(new_model, eval_set)
results['rouge_l'] = compute_rouge_l(new_model, eval_set)
# Model-based factuality (sample 200)
results['factuality'] = compute_factuality(new_model, eval_set.sample(200))
# Regression gates
if results['bleu'] < thresholds['bleu'] * 0.98:
return False, "BLEU regression"
if results['factuality'] < thresholds['factuality'] * 0.95:
return False, "Factuality regression"
# Human eval triggered automatically on borderline cases
if results['bleu'] < thresholds['bleu'] * 1.02:
queue_human_eval(new_model, baseline_model, eval_set.sample(50))
return True, "Passed automatic gates"
The metrics you optimize determine the behavior you get. BLEU gives you conservative translations. ROUGE gives you extractive summaries. Perplexity gives you fluent but potentially hollow text. Choose the metric that aligns with your actual quality criteria, and always validate the correlation on your specific domain.