Text Classification Fundamentals
Imagine a mail server that must decide, in milliseconds, whether an incoming message is spam or legitimate. It has no human in the loop. It only has the words in the message and a decision to make. This is the essence of text classification: assigning a document to one of a fixed set of categories based on its content, automatically and repeatedly, at scale. Text classification underlies an enormous share of practical NLP systems. These include spam filters, sentiment analyzers for product reviews, and news topic taggers. They also include toxic-comment detectors, intent classifiers behind chatbots, and language identifiers. What unites all of these is a common supervised learning recipe. First, turn documents into feature vectors (building on the Bag-of-Words Model and TF-IDF Weighting). Then, learn a decision rule from labeled examples. Finally, evaluate that rule honestly on unseen data. The elegance of the framework is that once a problem is cast this way, decades of machine learning machinery become available. This article is about the scaffolding that makes classification work at all. This holds independent of which specific classifier is used. It covers how documents become feature vectors. It also covers how a dataset must be split to produce a trustworthy estimate of real-world performance. Finally, it covers how to measure success with metrics that are robust to class imbalance and error asymmetry. These are the ideas that any classifier is built on top of. This includes a simple rule, a Naive Bayes model, a logistic regression, or a neural network. By the end, the reader will understand precisely why a training/validation/test split is non-negotiable. They will also understand why raw accuracy can be deeply misleading. And they will see how precision, recall, and F1 quantify different notions of "being right." This groundwork sets up the next natural step: a concrete probabilistic classifier, Naive Bayes for Text, which turns these foundations into an actual learning algorithm.
- Text classification maps a document to one of a fixed set of labels using a learned decision function.
- Documents are converted into fixed-length feature vectors, typically bag-of-words or TF-IDF representations.
- Data must be split into training, validation, and test sets to get an honest estimate of generalization.
- Accuracy is often misleading under class imbalance; precision, recall, and F1 give a fuller picture.
- Confusion matrices reveal exactly which classes are confused with which, guiding model improvement.
- These foundations underlie every later classifier, from Naive Bayes to deep neural text models.
Text classification turns documents into feature vectors and learns a decision rule from labeled training data, whose quality must be measured on data the rule has never seen.
The idea
A text classification task is specified by a document space , a finite label set , and a target function that we want to approximate. In the supervised setting, we are given a labeled dataset of documents paired with their true labels, and the goal is to learn a hypothesis that generalizes well to unseen documents, not merely one that memorizes . Before any learning algorithm can act, a document must become a feature vector: a fixed-length numerical representation . The simplest such representation, bag-of-words, counts vocabulary term occurrences; TF-IDF reweights those counts to downweight terms common across the whole corpus and upweight terms distinctive to a document. Other common features include n-grams (capturing local word order), document length, presence of specific punctuation or capitalization patterns (useful for spam or sentiment), and, in modern systems, dense embeddings from a pretrained encoder. Feature engineering — deciding what captures — is often as consequential as the choice of classifier itself, because a classifier can only exploit information that survives the encoding step. Given a training set of feature vectors and labels, a classifier is any function that maps to a predicted label , usually by scoring each candidate label and picking the highest-scoring one. The classifier's parameters are fit by an optimization procedure that minimizes some loss on the training data — for example, maximizing the likelihood of the observed labels under a probabilistic model. Binary classification distinguishes two classes (spam vs. not-spam); multiclass classification distinguishes among mutually exclusive classes (topic tagging into "sports," "politics," "tech," ...); multilabel classification allows a document to receive several labels simultaneously (a news article can be both "politics" and "economy"). The evaluation methodology described here applies to all three settings, with small adjustments for multilabel scoring.
How it works
The end-to-end pipeline has four stages: representation, splitting, training, and evaluation. Each stage has specific responsibilities and specific failure modes if done carelessly.
1. Representation. Every document is passed through a fixed feature function , producing a vector of the same dimensionality for every document, regardless of the document's original length. This is what allows classifiers — which expect fixed-size numerical input — to operate uniformly over documents of varying length. The vocabulary used to build (which words map to which coordinates) must be fixed using only the training set; using test-set vocabulary is a subtle form of information leakage.
2. Splitting. The labeled dataset is partitioned into disjoint subsets:
- Training set — used to fit the classifier's parameters.
- Validation (development) set — used to tune hyperparameters and make modeling decisions (which features to use, how much regularization, when to stop training).
- Test set — touched exactly once, at the very end, to report a final unbiased performance estimate.
A common split is 80/10/10 or 70/15/15, though the right ratio depends on dataset size; with very large datasets, even a 98/1/1 split can leave thousands of validation/test examples. When labeled data is scarce, k-fold cross-validation replaces a single validation split: the training pool is divided into folds, and the model is trained times, each time validating on a different fold, with results averaged. This uses data more efficiently at the cost of the training time.
3. Training. The classifier's parameters are optimized on the training set only. For probabilistic classifiers this typically means maximizing the log-likelihood of the training labels (see Naive Bayes for Text for a worked instance); for margin-based classifiers, minimizing a hinge-loss objective; for neural classifiers, minimizing cross-entropy via gradient descent.
4. Evaluation. The trained classifier is applied to held-out data, and its predictions are compared against true labels using metrics discussed below. Validation-set performance guides model selection; test-set performance is the number that gets reported as the final result. ``` function TRAIN_AND_EVALUATE(labeled_data, feature_fn, classifier_class):
# Step 1: split data BEFORE any feature extraction that uses corpus statistics train, dev, test = SPLIT(labeled_data, ratios=(0.8, 0.1, 0.1), stratified=True)
# Step 2: fit feature extractor (e.g. vocabulary, IDF weights) on TRAIN ONLY feature_fn.fit(train.documents) best_model = None best_dev_score = -infinity for hyperparams in HYPERPARAMETER_GRID: model = classifier_class(hyperparams) X_train = [feature_fn.transform(doc) for doc in train.documents] model.fit(X_train, train.labels) X_dev = [feature_fn.transform(doc) for doc in dev.documents] predictions = model.predict(X_dev) score = MACRO_F1(predictions, dev.labels) if score > best_dev_score: best_dev_score = score best_model = model best_hyperparams = hyperparams
# Step 3: final, one-time evaluation on TEST X_test = [feature_fn.transform(doc) for doc in test.documents] test_predictions = best_model.predict(X_test) report = CLASSIFICATION_REPORT(test_predictions, test.labels) return best_model, best_hyperparams, report
function CLASSIFICATION_REPORT(predictions, gold_labels): classes = UNIQUE(gold_labels) confusion = ZERO_MATRIX(len(classes), len(classes)) for pred, gold in zip(predictions, gold_labels): confusion[INDEX(gold), INDEX(pred)] += 1 metrics = {} for c in classes: tp = confusion[INDEX(c), INDEX(c)] fp = SUM(confusion[:, INDEX(c)]) - tp # predicted c, actually not c fn = SUM(confusion[INDEX(c), :]) - tp # actually c, predicted not c precision = tp / (tp + fp) if (tp + fp) > 0 else 0 recall = tp / (tp + fn) if (tp + fn) > 0 else 0 f1 = 2 precision recall / (precision + recall) if (precision + recall) > 0 else 0 metrics[c] = (precision, recall, f1) accuracy = TRACE(confusion) / SUM(confusion) macro_f1 = MEAN([metrics[c][2] for c in classes]) return {"per_class": metrics, "accuracy": accuracy, "macro_f1": macro_f1, "confusion": confusion} ``` The `SPLIT` function should be stratified — preserving the proportion of each class in every split — so that a rare class is not accidentally absent from the validation or test set. Note the critical ordering: `feature_fn.fit` runs only on `train`, never on `dev` or `test`, so vocabulary and IDF statistics never see held-out documents.
Complexity & correctness
Feature extraction. For a bag-of-words or TF-IDF representation over a vocabulary of size , transforming a document with tokens takes time (a single pass to count terms), producing a sparse vector with at most nonzero entries. Over a corpus of documents with total length , feature extraction for the whole dataset costs time and space if stored sparsely (dense storage would cost , typically far larger since most documents use only a small fraction of the vocabulary).
Splitting. A stratified split over examples with classes costs time: bucket examples by label (), then partition each bucket proportionally ( total across buckets). Correctness of stratification follows from a simple invariant — after splitting, for every class the fraction of class- examples in each split matches the fraction in the full dataset, up to rounding. This invariant matters because it prevents a split from asymmetrically starving a class from training or test data, which would otherwise bias the estimated per-class metrics.
Why held-out evaluation is honest. The core statistical argument is this: if the test set is drawn independently from the same distribution as future, unseen data, and the classifier's parameters were fit without ever observing the test labels, then the test accuracy is an unbiased estimate of the true generalization accuracy, with error decreasing as by standard concentration bounds (e.g., a Hoeffding-style argument on the sample mean of a 0/1 correctness indicator). The moment any test example influences training — even indirectly, through vocabulary choice or hyperparameter tuning on test performance — this independence is broken, and the reported number becomes optimistically biased. This is why the pipeline enforces a strict separation: dev data may be touched many times during model development, but test data is touched exactly once.
Cross-validation's tradeoff. -fold cross-validation reduces the variance of the performance estimate by averaging over different train/validation partitions, at the cost of training the model times — an explicit time-for-statistical-reliability tradeoff. As (leave-one-out), the estimate becomes nearly unbiased for the "train on examples" regime but has high variance across folds and is often the most expensive option, since it requires up to full training runs.
Metric definitions and why accuracy misleads. Given per-class counts of true positives (), false positives (), and false negatives ():
Accuracy is . Consider a spam corpus with 990 legitimate emails and 10 spam emails. A classifier that predicts "not spam" for every message achieves accuracy while having zero recall on the class that actually matters. Precision and recall, computed per class, expose this failure immediately: recall for the spam class is . is the harmonic mean of precision and recall specifically because the harmonic mean punishes a large gap between the two — a classifier with precision and recall gets , not the misleadingly high arithmetic mean of . For multiclass problems, macro-averaging (unweighted mean of per-class ) treats every class equally regardless of frequency, while micro-averaging (pooling // across classes before computing one aggregate ) weights each example equally, so common classes dominate. The choice between them should match the actual cost structure of the application.
Worked example
Consider a tiny 3-class news-topic classifier with classes `sports`, `politics`, `tech`, evaluated on a test set of 10 documents. Suppose the trained classifier produces these predictions:
| Doc | True label | Predicted label |
|---|---|---|
| 1 | sports | sports |
| 2 | sports | sports |
| 3 | sports | politics |
| 4 | politics | politics |
| 5 | politics | politics |
| 6 | politics | tech |
| 7 | tech | tech |
| 8 | tech | tech |
| 9 | tech | sports |
| 10 | tech | tech |
Confusion matrix (rows = true, columns = predicted):
| true \ pred | sports | politics | tech |
|---|---|---|---|
| sports | 2 | 1 | 0 |
| politics | 0 | 2 | 1 |
| tech | 1 | 0 | 3 |
Per-class metrics: For `sports`: , column-sum minus , row-sum minus .
For `politics`: , , .
For `tech`: , , .
Aggregate metrics:
Here accuracy (0.70) and macro- (0.695) happen to be close because classes are fairly balanced (3, 3, 4 examples) and errors are spread evenly — but this closeness is a coincidence of a balanced toy example, not a general property, as the spam example above demonstrated. If class `politics` had been only 1 example out of 100 and the classifier had ignored it entirely, accuracy could stay near while macro- would collapse toward once that one class's zero recall is averaged in — this is precisely the imbalance sensitivity that macro-averaging is designed to surface.
Common pitfalls & traps
| Pitfall | Why it happens | Consequence |
|---|---|---|
| Fitting vocabulary/IDF on the full dataset before splitting | Convenience — one preprocessing pass over "all the data" | Test-set statistics leak into training; reported metrics are optimistically biased |
| Using raw accuracy on imbalanced data | Accuracy is simple to compute and intuitive | A trivial majority-class predictor looks like a strong model |
| Repeatedly tuning on the test set | No separate validation set was held out | Test set becomes an implicit training signal; final number no longer estimates generalization |
| Non-stratified random split with rare classes | Plain random sampling ignores label proportions | A rare class can vanish entirely from train or test, making its metrics undefined or zero by default |
| Reporting only micro-F1 for a rare-but-important class | Micro-averaging is dominated by frequent classes | Poor performance on the class that matters most (e.g., fraud, toxicity) is masked |
| Ignoring the confusion matrix | Aggregate metrics are faster to skim | Systematic confusions between two specific classes go undiagnosed |
Pioneers & history
Text categorization as a statistical learning problem crystallized in the information-retrieval community, where the practical need to route, filter, and index documents at scale made hand-written rules unworkable. The formal treatment of held-out evaluation and error metrics draws directly on statistical hypothesis testing and signal-detection theory developed decades earlier, then re-purposed for text.
Origins · 1 minClaude Elwood Shannon and how this was discovered
Where this matters
Every production text classifier — spam filters in email systems, content moderation pipelines flagging toxic or policy-violating posts, sentiment dashboards summarizing product reviews, language identifiers routing text to the correct downstream model, intent classifiers behind virtual assistants, and topic tags in news aggregation — relies on this same skeleton: features, a train/validation/test split, and metrics chosen to match the cost of different error types. Search engines use classification to detect query intent; e-commerce platforms use it to route customer support tickets; social platforms use it to prioritize moderation queues by predicted severity. Even as classifiers evolve from bag-of-words plus logistic regression to deep transformer encoders, the evaluation discipline described here — honest splits, stratification, precision/recall/F1 rather than raw accuracy alone — remains unchanged, because it addresses a statistical problem, not an architectural one. Understanding it is the prerequisite for evaluating any classifier correctly, including the probabilistic model introduced next, Naive Bayes for Text.
Ask the tutor: How would this evaluation pipeline change for a multilabel classification problem, where a document can carry several correct labels at once?
Why can't we just use accuracy for every classification problem?
Accuracy treats every misclassification identically and ignores class frequency, so on imbalanced datasets it can be dominated by the majority class. A classifier that always predicts "not spam" can reach 99% accuracy on a corpus that is 99% legitimate mail while completely failing to catch spam. Precision and recall, computed per class, expose this because they isolate how the classifier performs specifically on the class of interest, regardless of how rare it is. In applications where the minority class carries the real cost — fraud, disease diagnosis, toxic content — accuracy alone can hide the model's actual failure mode, which is why per-class precision, recall, and F1, or macro-averaged versions of them, are the standard reporting choice.
What is the difference between the validation (dev) set and the test set?
The validation set is used repeatedly during development: to choose hyperparameters, decide between feature sets, and select among candidate models based on their performance. The test set is reserved and used exactly once, after all such decisions are finalized, to produce an unbiased estimate of how the chosen model will perform on genuinely unseen data. If the test set were used the way the validation set is — repeatedly, to guide choices — the final reported number would become biased upward, because the model (or the researcher) would have implicitly "learned" from test-set feedback, violating the independence assumption that makes held-out evaluation statistically valid.
When should I use k-fold cross-validation instead of a single train/validation split?
Use cross-validation when labeled data is scarce enough that a single held-out validation split would be too small to give a reliable performance estimate, or too large to sacrifice from training. Cross-validation trains the model times, each time validating on a different fold, and averages the results, which reduces the variance of the estimate compared to relying on one arbitrary split. The tradeoff is computational: it costs roughly times as much training as a single split. With very large datasets, a single stratified split is usually preferred because both the validation and test sets are already large enough for statistically stable estimates, and the extra computation of cross-validation isn't worth the marginal gain.
What is stratified splitting and why does it matter for text classification?
Stratified splitting partitions a dataset so that the proportion of each class is preserved across the training, validation, and test sets, rather than relying on pure random sampling. This matters especially when some classes are rare: a purely random split could, by chance, place all examples of a minority class into the training set, leaving zero examples in the test set and making that class's test metrics undefined. Text classification datasets are frequently imbalanced — spam versus legitimate mail, or toxic versus non-toxic comments — so stratification is close to mandatory in practice, not merely a refinement, to ensure every split can meaningfully evaluate every class.
Is F1 always the right metric to optimize?
Not always — F1 assumes precision and recall should be weighted equally, which is not true for every application. In a spam filter, falsely blocking a legitimate email (a false positive) may be far more costly than letting one spam message through (a false negative), which argues for weighting precision more heavily, for instance via the score with . In a medical screening classifier, missing a true case (a false negative) is often far worse than a false alarm, arguing for weighting recall more heavily with . The right metric should reflect the actual asymmetric cost of the two error types in the deployment context, not a default assumption of symmetry.
How do features like TF-IDF interact with the train/test split?
TF-IDF weighting depends on corpus-level statistics — specifically, how many documents in the corpus contain each term, used to compute inverse document frequency. Those document-frequency counts must be computed from the training set alone; if they are computed over the combined training-plus-test corpus, information about which terms are distinctive specifically leaks from the test documents into the representation used at training time. In practice this means fitting a `TfidfVectorizer`-style transformer on the training documents only, then applying that fixed, frozen transformation to validation and test documents afterward, even if a test document introduces a term never seen during fitting.
What happens if a class in the test set never appeared in training?
The classifier has no learned signal for that label at all, so it can never correctly predict it, and its recall for that class will be exactly zero no matter how good the underlying model is. This is a data problem, not a modeling problem, and it typically indicates that the label set used during training did not match the full label set present in the deployment distribution. The fix is either to ensure every class is represented in training via a stratified split (or targeted oversampling), or to explicitly design the system to route unseen categories to a fallback or "unknown" mechanism rather than silently misclassifying them into a related class.