Skip to main content

Evaluating NLP Systems

An NLP evaluation starts with the decision and data-generating process: target population, label policy, unit of splitting, error costs, and deployment threshold. Aggregate accuracy alone can hide class imbalance, duplicate leakage, length effects, poor calibration, and failures concentrated in a language or product slice.

Prerequisites​

Know confusion matrices, precision/recall, predicted probabilities, and train/validation/test roles.

Learning goals​

  • report confusion and macro metrics alongside class support.
  • inspect calibration and a length slice without using test data for tuning.
  • retain stable example IDs for qualitative error review and leakage checks.

Worked example​

This fixed six-item test result includes one error per class. The positive label is technical. Expected calibration error (ECE) here uses two declared probability bins; it is descriptive and unstable on such a tiny sample.

import numpy as np
from sklearn.metrics import accuracy_score, confusion_matrix, f1_score

ids = np.array(["test-1", "test-2", "test-3", "test-4", "test-5", "test-6"])
y_true = np.array([0, 0, 0, 1, 1, 1])
probability = np.array([0.10, 0.30, 0.65, 0.80, 0.60, 0.40])
y_pred = (probability >= 0.50).astype(int)
lengths = np.array([3, 5, 4, 5, 6, 3])

bins = np.array([0.0, 0.5, 1.0])
ece = 0.0
for lower, upper in zip(bins[:-1], bins[1:]):
selected = (probability >= lower) & (probability < upper if upper < 1 else probability <= upper)
ece += selected.mean() * abs(probability[selected].mean() - y_true[selected].mean())

short = lengths <= 4
errors = ids[y_true != y_pred].tolist()
print(f"support={np.bincount(y_true).tolist()} confusion={confusion_matrix(y_true, y_pred).tolist()}")
print(f"accuracy={accuracy_score(y_true, y_pred):.3f} macro_f1={f1_score(y_true, y_pred, average='macro'):.3f}")
print(f"ece_2_bins={ece:.3f} short_accuracy={accuracy_score(y_true[short], y_pred[short]):.3f} long_accuracy={accuracy_score(y_true[~short], y_pred[~short]):.3f}")
print(f"error_ids={errors}")

Output:

support=[3, 3] confusion=[[2, 1], [1, 2]]
accuracy=0.667 macro_f1=0.667
ece_2_bins=0.042 short_accuracy=0.333 long_accuracy=1.000
error_ids=['test-3', 'test-6']

The length difference is a prompt to inspect data, not a conclusion from three items. Calibration requires enough independent examples and should be assessed after a threshold-free probability model is fixed. Exact and near-duplicate detection must happen across splits before evaluation; template siblings and paraphrases may leak even when normalized strings differ.

Common mistakes​

  • Choosing a threshold, slice definition, or model after seeing test outcomes.
  • Reporting macro F1 without class support, confusion, or error costs.
  • Treating a coarse, tiny-bin ECE as proof that probabilities are calibrated.
  • Ignoring document authors, threads, time, or templates when defining split units.
  • Publishing sensitive raw text during qualitative error review.

Practice​

Move the threshold to 0.7 and recompute both class recalls. Before looking at results, write which error is costlier and whether threshold selection belongs on training, validation, or test data.

Next steps​

Project: Build a Text Classification Pipeline joins leakage checks, a local baseline, validation selection, and one-time test evaluation.

Primary sources​

Checked 2026-10-16: scikit-learn classification metrics, scikit-learn calibration guide, Datasheets for Datasets, and Data Statements for NLP.