Evaluation of NLP Systems

NLP evaluation asks whether a text system does the task correctly under the ambiguity, noise, and cost structure of its use case. A text classification router, sequence labelling tagger, named entity recognition model, summarization system, and urgency classification policy need different metrics and examples.

How NLP tasks are scored

Different NLP tasks call for different metrics, because the unit being judged changes:

TaskTypical metricWhat it measures
Classificationprecision / recall / F1correct label assignment
Sequence labeling / NERspan-level F1 (exact match)correct span boundary and type
Generation (translation, summary)BLEU / ROUGE plus human reviewoverlap with references, plus quality
Semantic similaritycorrelation with human scoresagreement of ranked similarity

For classification, precision, recall, and F1 for class are

Macro-F1 averages classes equally; micro-F1 aggregates counts. For NLP, also separate span correctness, label correctness, factual correctness, latency, abstention, and downstream utility. Bootstrap intervals communicate how unstable a small evaluation set is.

For generated text, BLEU compares a candidate output with one or more references using modified n-gram precision and a brevity penalty:

where is modified -gram precision, is the weight for each order, and penalizes overly short candidates. It is most useful for corpus-level machine translation comparisons where many acceptable phrasings are represented in references. BLEU is a weak proxy for summarization quality, factuality, citation support, or instruction following, so generated text systems should pair it with task-specific review.

Worked example

This snippet computes macro-F1 for a small classifier, bootstraps a confidence interval, and reports per-label F1 scores.

import numpy as np
from sklearn.metrics import f1_score
 
np.random.seed(7)
y_true = np.array(["urgent", "normal", "urgent", "low", "normal", "urgent", "low", "normal"])
y_pred = np.array(["urgent", "normal", "normal", "low", "urgent", "urgent", "low", "normal"])
rng = np.random.default_rng(7)
boots = []
for _ in range(1000):
    idx = rng.integers(0, len(y_true), len(y_true))
    boots.append(f1_score(y_true[idx], y_pred[idx], average="macro"))
lo, hi = np.percentile(boots, [2.5, 97.5])
print("macro_f1", round(f1_score(y_true, y_pred, average="macro"), 3))
print("bootstrap_95_ci", (round(float(lo), 3), round(float(hi), 3)))
print("per_label", {label: round(float(score), 3) for label, score in zip(["low", "normal", "urgent"], f1_score(y_true, y_pred, labels=["low", "normal", "urgent"], average=None))})

Observed output:

macro_f1 0.778
bootstrap_95_ci (0.444, 1.0)
per_label {'low': 1.0, 'normal': 0.667, 'urgent': 0.667}

The point estimate looks respectable, but the confidence interval is wide because there are only eight examples. That is a signal to collect more labelled cases before making production claims.

Caveats

Aggregate metrics can hide minority-language failures, rare entity misses, or costly false negatives. Generated text needs factuality and citation checks, not only overlap metrics. Evaluation sets must freeze annotation rules, preprocessing, prompts, and thresholds; otherwise a score change may reflect the harness rather than the NLP model.

References