NLP Pipelines

Classical NLP transforms raw text into structured features for machine learning. Even with LLMs dominant in production, understanding the full pipeline — tokenization, feature extraction, model training, evaluation — is essential for designing systems, debugging failures, and knowing when simpler approaches outperform expensive generative models.

Key Takeaways

  • TF-IDF remains a strong baseline for text classification; always benchmark before reaching for transformers.
  • Word embeddings (Word2Vec, GloVe) capture semantic similarity; sentence transformers (SBERT) encode full-sentence meaning for retrieval and similarity tasks.
  • Text preprocessing choices (stopword removal, stemming vs. lemmatization) affect downstream model performance and should be tuned like hyperparameters.
  • For classification: sklearn TF-IDF + logistic regression pipeline is fast, interpretable, and often competitive with fine-tuned transformers on short-text tasks.
  • NER evaluation uses token-level F1 (strict or partial match); generation evaluation uses BLEU (precision-based) or ROUGE (recall-based).
  • Fine-tuning a pre-trained transformer (BERT, RoBERTa) on domain-specific data typically gives the best accuracy; zero-shot classification via LLMs works when labeled data is scarce.

1. Text Preprocessing

Preprocessing converts raw text into a form suitable for feature extraction. Choices here are hyperparameters — they affect downstream performance and should be evaluated empirically.

Tokenization

  • Whitespace tokenization: split on spaces/punctuation. Simple but misses sub-word structure.
  • WordPiece / BPE: sub-word tokenization used by BERT/GPT; handles OOV by splitting rare words into known pieces.

Common Preprocessing Steps

  • Lowercasing: reduces vocabulary size; skip for proper-noun-heavy or case-sensitive tasks.
  • Punctuation removal: removes noise; be careful with negation (don't → dont changes meaning).
  • Stopword removal: removes high-frequency low-signal words (the, a). Skip for sentiment/negation tasks where words like "not" matter.
  • Stemming (Porter, Snowball): aggressively strips suffixes (running → run, studies → studi). Fast but may produce non-words.
  • Lemmatization (spaCy): returns valid base forms using vocabulary + POS (studies → study, running → run if verb). Slower but accurate and interpretable.
  • Regex cleaning: remove URLs, HTML tags, special characters before tokenization.
import spacy

nlp = spacy.load("en_core_web_sm")

def preprocess(text: str, remove_stopwords: bool = True) -> list[str]:
    doc = nlp(text.lower())
    tokens = []
    for token in doc:
        if token.is_punct or token.is_space:
            continue
        if remove_stopwords and token.is_stop:
            continue
        tokens.append(token.lemma_)
    return tokens

# Example
preprocess("The studies were running behind schedule.")
# ['study', 'run', 'behind', 'schedule']

2. Feature Extraction — TF-IDF and Embeddings

TF-IDF

$$\text{TF-IDF}(t, d) = \text{TF}(t,d) \times \log\frac{N}{df(t)}$$

  • TF(t, d): frequency of term t in document d (often log-scaled).
  • IDF: penalizes terms that appear in many documents — the $\log(N / df(t))$ term is small when $df(t)$ is large, down-weighting common words like "the" that appear in nearly every document.
  • Result: sparse, high-dimensional, interpretable matrix. Strong baseline for classification.
from sklearn.feature_extraction.text import TfidfVectorizer

corpus = ["The cat sat on the mat", "Dogs are loyal animals", "Cats and dogs are pets"]
vec = TfidfVectorizer(max_features=5000, ngram_range=(1, 2), sublinear_tf=True)
X = vec.fit_transform(corpus)  # sparse matrix (n_docs, vocab_size)

Word2Vec / GloVe

Dense embeddings trained on large corpora. Each word maps to a 100–300 dimensional vector. Semantic similarity is measured via cosine distance (king - man + woman ≈ queen).

OOV problem: words not in training vocabulary have no vector. Mitigations: sub-word models (fastText), WordPiece tokenization.

Document embedding via averaging: average the word vectors for all tokens in a document. Fast but loses word order and can conflate antonyms and negations.

import numpy as np

def doc_embedding(tokens: list[str], model) -> np.ndarray:
    vecs = [model[t] for t in tokens if t in model]
    if not vecs:
        return np.zeros(model.vector_size)
    return np.mean(vecs, axis=0)

Sentence Transformers (SBERT)

Encode entire sentences into a single dense vector using a fine-tuned transformer. Better than averaging for multi-word meaning, negation, and sentence-level semantics. Use for: semantic search, clustering, similarity scoring.

from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity

model = SentenceTransformer("all-MiniLM-L6-v2")
sentences = ["How do I reset my password?", "I forgot my login credentials"]
embeddings = model.encode(sentences)  # shape (2, 384)

sim = cosine_similarity([embeddings[0]], [embeddings[1]])[0][0]
print(f"Similarity: {sim:.3f}")  # ~0.82 — semantically close

3. Text Classification Pipeline

sklearn Pipeline (Fast Baseline)

from sklearn.pipeline import Pipeline
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score

pipeline = Pipeline([
    ("tfidf", TfidfVectorizer(max_features=50000, ngram_range=(1, 2),
                               sublinear_tf=True, strip_accents="unicode")),
    ("clf", LogisticRegression(max_iter=1000, C=1.0, class_weight="balanced")),
])

# Cross-validation
scores = cross_val_score(pipeline, X_train, y_train, cv=5, scoring="f1_macro")
print(f"CV macro F1: {scores.mean():.3f} ± {scores.std():.3f}")

pipeline.fit(X_train, y_train)

Handling class imbalance: use class_weight="balanced" in LogisticRegression. SMOTE is not directly applicable to sparse TF-IDF vectors — prefer class weights or oversampling raw documents before vectorization.

Fine-tuning a HuggingFace Transformer

from transformers import (AutoTokenizer, AutoModelForSequenceClassification,
                           TrainingArguments, Trainer)
from datasets import Dataset

tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased")

def tokenize(batch):
    return tokenizer(batch["text"], truncation=True,
                     padding="max_length", max_length=128)

dataset = Dataset.from_dict({"text": X_train, "label": y_train})
dataset = dataset.map(tokenize, batched=True)

model = AutoModelForSequenceClassification.from_pretrained(
    "distilbert-base-uncased", num_labels=num_classes
)

args = TrainingArguments(
    output_dir="./results",
    num_train_epochs=3,
    per_device_train_batch_size=32,
    evaluation_strategy="epoch",
    load_best_model_at_end=True,
)

trainer = Trainer(model=model, args=args, train_dataset=dataset)
trainer.train()

When to use BERT vs. TF-IDF + LR: BERT wins on accuracy with sufficient data (>1K examples) and handles context and semantics. TF-IDF wins on latency, interpretability, and cost. Always benchmark both before deciding.

4. Named Entity Recognition (NER)

NER identifies and classifies named entities (persons, organizations, locations, dates) in text.

IOB / BIO Tagging Scheme

Each token is assigned a label:

  • B-TYPE: Beginning of an entity of the given type
  • I-TYPE: Inside (continuation) of an entity
  • O: Outside any entity

Example: "Apple Inc. was founded in Cupertino"
→ Apple/B-ORG Inc./I-ORG was/O founded/O in/O Cupertino/B-LOC

IOB tagging is needed because entity spans can be multiple tokens; without the B/I distinction, adjacent same-type entities would be ambiguous.

spaCy Out-of-the-Box NER

import spacy

nlp = spacy.load("en_core_web_sm")
text = "Tim Cook announced Apple's new product in San Francisco on Monday."
doc = nlp(text)

for ent in doc.ents:
    print(f"{ent.text!r:30s} {ent.label_:10s} {spacy.explain(ent.label_)}")
# 'Tim Cook'       PERSON     People, including fictional
# 'Apple'          ORG        Companies, agencies, institutions
# 'San Francisco'  GPE        Geopolitical entity
# 'Monday'         DATE       Absolute or relative dates

Fine-tuning for Custom Entities

Use spaCy's training CLI or HuggingFace token-classification task with a dataset in IOB format. Annotate ~500–1000 examples per entity type for reasonable performance.

NER Evaluation

  • Exact match (strict) F1: entity span and type must both match exactly.
  • Partial match F1: overlapping spans receive partial credit.
  • Token-level F1: treats each token label independently; generally higher and less informative than entity-level F1.

5. Evaluation Metrics

Task Metric Notes
Text classification Precision, Recall, F1 Macro = average per class; Micro = global TP/FP/FN
Text classification (imbalanced) Macro F1 Penalizes abandoning minority classes
NER Entity-level exact F1 Strict: span + type must match
NER Token-level F1 Less strict; commonly reported alongside entity-level
Summarization ROUGE-1, ROUGE-2, ROUGE-L Recall of unigrams, bigrams, longest common subsequence
Machine translation BLEU Precision of n-grams (1–4); includes brevity penalty
Semantic similarity Cosine similarity Of sentence embeddings (SBERT, etc.)

ROUGE (Recall-Oriented Understudy for Gisting Evaluation): measures how much of the reference summary appears in the generated output. ROUGE-L uses longest common subsequence to capture fluency. Used for summarization.

BLEU (Bilingual Evaluation Understudy): precision-focused — what fraction of the generated n-grams appear in the reference. A brevity penalty prevents short outputs from gaming precision. Limitations: no fluency signal, no paraphrase credit, sensitive to tokenization.

When metrics mislead: ROUGE/BLEU reward surface overlap and miss semantically equivalent paraphrases. Supplement with BERTScore (embedding-based similarity) or human evaluation for high-stakes tasks.

Practice Questions

Q1. What is the difference between stemming and lemmatization? When does the choice matter?

Stemming aggressively strips suffixes (running → run, studies → studi) without linguistic knowledge; fast but may produce non-words. Lemmatization uses vocabulary and POS tags to return the base word form (studies → study, running → running if noun, run if verb); slower but accurate.

Choice matters when: (1) you need interpretable features — lemmas are readable, stems are not; (2) your domain has morphologically rich text (medical, legal) where stemming loses important distinctions; (3) downstream evaluation uses exact word matching (BLEU, ROUGE). For a fast TF-IDF baseline on general text, either works; lemmatization is preferred for production.

Q2. Your TF-IDF + logistic regression classifier achieves 88% accuracy but a fine-tuned BERT gets 91%. Would you always choose BERT?

Not necessarily. Consider: (1) Latency — BERT inference is 10–100× slower than logistic regression on TF-IDF features; if the classifier is in a real-time API, 88% at 2ms may beat 91% at 200ms; (2) Training data scale — BERT benefits most from >1K labeled examples; below that, logistic regression often competes; (3) Interpretability — for regulated domains, TF-IDF features (visible words) are more defensible; (4) Cost — BERT requires GPU inference at scale; (5) Maintenance — BERT requires periodic retraining on new vocabulary/domain shifts. The 3% accuracy gap may or may not justify these tradeoffs. Always benchmark both on held-out data before deciding.

Q3. How does BLEU score work and what are its limitations?

BLEU (Bilingual Evaluation Understudy) measures precision of n-gram overlaps between the generated text and one or more reference texts, with a brevity penalty for short outputs. It computes a weighted geometric mean of 1-gram to 4-gram precisions.

Limitations: (1) It only measures precision, not recall — a short output that cherry-picks matching words can score high; (2) It doesn't capture fluency or grammaticality — nonsensical word salad can match n-grams; (3) It rewards word-for-word matches, missing semantically equivalent paraphrases; (4) Highly sensitive to tokenization choices. ROUGE is better for summarization (measures recall of reference content). For modern NLP evaluation, BLEU/ROUGE are often supplemented with BERTScore (embedding-based similarity) or human evaluation.

Q4. You need to classify customer support tickets into 50 categories with limited labeled data (200 examples per category). What approach would you use?

With 200 examples per category (10K total), fine-tuning a pre-trained transformer (BERT, RoBERTa) is the best single approach — pre-training has already encoded rich language representations, and fine-tuning on 10K examples is feasible. Steps:

  1. Baseline: TF-IDF + logistic regression — fast to train, sets a ceiling to beat.
  2. Fine-tune: DistilBERT (speed-optimized) via HuggingFace Trainer.
  3. Class imbalance: weighted cross-entropy loss.
  4. Evaluate: macro F1 (50 balanced classes — macro ensures no class is abandoned).
  5. Additional baseline: few-shot classification via an LLM — with 200 examples per class, in-context learning may be competitive.

If some categories have fewer examples, consider hierarchical classification (coarse-to-fine) or data augmentation via back-translation.

Action Items

  • Explain TF-IDF from first principles: why $\log(N/df)$ — what does it penalize?
  • Build a sklearn Pipeline with TfidfVectorizer + LogisticRegression from memory.
  • Describe the IOB tagging scheme for NER and why it's needed.
  • Know BLEU vs. ROUGE: which is recall-based, which is precision-based, and when each is used.
  • Explain why averaging word embeddings to get document embeddings can fail (antonyms, negation).
  • Know when to use a fine-tuned transformer vs. TF-IDF + logistic regression.