Text classification — assigning categories to text — is one of the most common NLP tasks in industry. Spam detection, sentiment analysis, support ticket routing, content moderation — all of these are text classification problems. This guide walks through the full pipeline from raw text to a deployed classifier in Python.
The Text Classification Pipeline
Every text classification project follows the same pipeline: text cleaning → feature extraction → model training → evaluation → deployment. The key choices are (1) what to use as features (TF-IDF, word embeddings, or transformer encodings) and (2) what classifier to use (logistic regression, gradient boosting, or fine-tuned BERT). We’ll cover all of these.
Text Preprocessing
import re
import nltk
from nltk.corpus import stopwords
from nltk.stem import WordNetLemmatizer
nltk.download(['stopwords', 'wordnet', 'punkt'])
stop_words = set(stopwords.words('english'))
lemmatizer = WordNetLemmatizer()
def preprocess(text: str) -> str:
text = text.lower()
text = re.sub(r'http\S+|www\S+', '', text) # remove URLs
text = re.sub(r'[^a-z\s]', '', text) # keep only letters
text = re.sub(r'\s+', ' ', text).strip()
tokens = text.split()
tokens = [lemmatizer.lemmatize(t) for t in tokens if t not in stop_words]
return ' '.join(tokens)
df['clean_text'] = df['text'].apply(preprocess)
Approach 1 – TF-IDF + Logistic Regression
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
X_train, X_test, y_train, y_test = train_test_split(
df['clean_text'], df['label'], test_size=0.2, random_state=42, stratify=df['label'])
pipeline = Pipeline([
('tfidf', TfidfVectorizer(max_features=50000, ngram_range=(1, 2),
min_df=2, sublinear_tf=True)),
('clf', LogisticRegression(max_iter=1000, C=1.0, class_weight='balanced')),
])
pipeline.fit(X_train, y_train)
y_pred = pipeline.predict(X_test)
print(classification_report(y_test, y_pred))
This simple pipeline achieves surprisingly strong results — often 85-92% accuracy on well-defined tasks. It’s fast, interpretable, and doesn’t need a GPU.
Understanding TF-IDF
TF-IDF (Term Frequency-Inverse Document Frequency) scores words by how often they appear in a document (TF) relative to how often they appear across all documents (IDF). Words that appear frequently in one document but rarely overall (like “arbitrage” in a finance article) get high TF-IDF scores. Common words like “the” get low scores. ngram_range=(1,2) includes both single words and two-word phrases, capturing context like “not good” instead of treating “not” and “good” separately.
Approach 2 – Word Embeddings + Gradient Boosting
import numpy as np
from sentence_transformers import SentenceTransformer
from sklearn.ensemble import GradientBoostingClassifier
# Encode texts as dense vectors (384 dimensions)
model = SentenceTransformer('all-MiniLM-L6-v2')
X_train_emb = model.encode(X_train.tolist(), show_progress_bar=True)
X_test_emb = model.encode(X_test.tolist(), show_progress_bar=True)
# Train classifier on embeddings
clf = GradientBoostingClassifier(n_estimators=200, learning_rate=0.1)
clf.fit(X_train_emb, y_train)
print(classification_report(y_test, clf.predict(X_test_emb)))
Approach 3 – Fine-Tuned BERT (Best Accuracy)
from transformers import pipeline as hf_pipeline
# Zero-shot (no training needed)
classifier = hf_pipeline("zero-shot-classification", model="facebook/bart-large-mnli")
result = classifier(
"This product broke after one week. Terrible quality.",
candidate_labels=["positive review", "negative review", "neutral review"])
print(result['labels'][0]) # negative review
# Fine-tuned (highest accuracy, needs labelled data)
from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments
tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased")
model = AutoModelForSequenceClassification.from_pretrained(
"distilbert-base-uncased", num_labels=len(df['label'].unique()))
Multi-Label Classification
from sklearn.preprocessing import MultiLabelBinarizer
from sklearn.multiclass import OneVsRestClassifier
# Each text can belong to multiple categories
mlb = MultiLabelBinarizer()
y_multi = mlb.fit_transform(df['labels']) # labels is a list of lists
pipeline_multi = Pipeline([
('tfidf', TfidfVectorizer(max_features=30000)),
('clf', OneVsRestClassifier(LogisticRegression(max_iter=500)))
])
pipeline_multi.fit(X_train, y_multi[train_idx])
Deployment as a REST API
import pickle
from fastapi import FastAPI
from pydantic import BaseModel
with open("text_classifier.pkl", "wb") as f:
pickle.dump(pipeline, f)
app = FastAPI()
class TextInput(BaseModel):
text: str
@app.post("/classify")
def classify(input: TextInput):
clean = preprocess(input.text)
label = pipeline.predict([clean])[0]
prob = pipeline.predict_proba([clean]).max()
return {"label": label, "confidence": round(float(prob), 4)}
Conclusion
For most text classification tasks, start with TF-IDF + logistic regression — it’s fast to train, easy to interpret, and achieves 85%+ accuracy on clean datasets. Move to sentence embeddings + gradient boosting when you need more accuracy without a GPU. Fine-tune BERT or DistilBERT when you need maximum accuracy and have enough labelled data (>1000 examples per class). The right choice depends on your data size, latency constraints, and infrastructure.


