The oldest and most useful NLP task. You have text and a fixed set of categories, and you want the category. Sentiment, spam, complaint type, priority, intent — all the same shape underneath.
What makes this task worth understanding first is that the modern deep learning answer is not always the right answer. A linear model over word counts remains competitive on straightforward problems, runs in milliseconds on a CPU, and can tell you exactly which words drove the decision. In a regulated environment that last property is often worth more than two points of accuracy.
Model families 6
Sub-categories within this task. The distinction that
matters is not which is best, but which fits your constraints.
Classical: TF-IDF plus a linear model
Kilobytes. CPU only.
Use when
Your baseline, always. Categories are distinguishable by vocabulary, you have a few thousand labelled examples, and you need to explain the decision or serve it in single-digit milliseconds.
Avoid when
Meaning depends on word order or negation. 'Not a fraudulent charge' and 'a fraudulent charge' look nearly identical to a bag of words.
The default deep learning answer. You have 1,000+ labelled examples and word order matters. Fine-tuning a pretrained encoder on your labels is the highest accuracy per unit of effort available.
Avoid when
You have no labels yet, or you need to change the category set frequently — every change means retraining.
Your text is full of jargon the general models never saw. A model further pretrained on financial filings understands that 'writedown' and 'impairment' are related; a general one does not.
Avoid when
Your text is ordinary business English. The domain advantage disappears and you have narrowed your options for nothing.
Examples
ProsusAI/finbertyiyanghkust/finbert-tone
Zero-shot via natural language inference
400M–900M. Wants a GPU.
Use when
You have no labelled data at all. You pass your category names in with the query and get an answer immediately. Excellent for bootstrapping a labelled set you then train on properly.
Avoid when
High volume or low latency. It runs one forward pass per candidate label, so ten categories means ten times the work per document.
Scoring how well a document answers a query. Technically classification, but the job is search relevance — the second stage of a RAG pipeline, after a fast retriever narrows the field.
Avoid when
As a general classifier. These are trained on query-document relevance, not on your categories.
Categories are subtle, involve reasoning, or change often. No training, and you can revise the category definitions by editing text.
Avoid when
Volume is high (cost), latency matters, or you need the same input to reliably produce the same output. Also the hardest option to validate for model risk purposes.
Examples
Any instruct-tuned LLM
In a bank 5
The third line in each is the part that makes it
harder than the textbook version.
Complaint categorisation and routing
Incoming complaints across channels get tagged by product and issue type, then routed to the right team with a severity flag.
The wrinkleComplaint handling is directly supervised. Misrouting a complaint that should have been escalated is a compliance failure, not just a service one, so the cost of a false negative on the 'urgent' class is far higher than a false positive. Tune the threshold accordingly rather than optimising overall accuracy.
Adverse media screening for KYC
Classify news articles mentioning a customer as relevant or not, and if relevant, by risk category — financial crime, sanctions, litigation.
The wrinkleEnormous class imbalance and a very low tolerance for missed hits. Most teams run this as a recall-first filter feeding human review rather than an automated decision.
Transaction narrative triage in AML
Free-text payment references and SWIFT narratives classified for suspicious patterns before a case is opened.
The wrinkleNarratives are short, abbreviated and multilingual. Character-level or multilingual models often beat English word-piece models here.
Research desk sentiment on earnings and news
Score analyst notes, earnings call transcripts and news for directional sentiment on a name.
The wrinkleGeneral sentiment models are trained on product reviews and get finance backwards — 'debt' is not negative in a financing context. This is the clearest case for a domain-pretrained model.
Email and service ticket intent
Route inbound mail to the right queue and pre-fill the case type.
The wrinkleMostly a latency and volume problem, not an accuracy one. This is where the classical baseline usually wins.
Code 4
Named functions and the arguments that actually
change the outcome. Comments flag the traps.
Baseline first: TF-IDF and logistic regression
Run this before anything else. If it gets you to 0.90 F1, a transformer that gets 0.92 may not be worth the serving cost and the explainability loss.
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.metrics import classification_report
import numpy as np
clf = Pipeline([
# word ngrams catch phrases; sublinear_tf dampens repeated terms
("tfidf", TfidfVectorizer(ngram_range=(1, 2), min_df=3,
sublinear_tf=True, strip_accents="unicode")),
# class_weight matters: complaint data is always imbalanced
("clf", LogisticRegression(max_iter=1000, class_weight="balanced")),
])
clf.fit(X_train, y_train)
print(classification_report(y_test, clf.predict(X_test)))
# The explainability that transformers cannot give you cheaply:
# which words push toward each class.
vec = clf.named_steps["tfidf"]
lr = clf.named_steps["clf"]
terms = np.array(vec.get_feature_names_out())
for i, label in enumerate(lr.classes_):
top = np.argsort(lr.coef_[i])[-12:][::-1]
print(label, "<-", ", ".join(terms[top]))
Use a pretrained classifier as-is
Three lines, no training, no endpoint. This is what the SageMaker JumpStart notebook does in thirty cells.
from transformers import pipeline
# General sentiment, trained on movie reviews
clf = pipeline("text-classification",
model="distilbert-base-uncased-finetuned-sst-2-english")
# Financial sentiment - note how differently these two score the same text
fin = pipeline("text-classification", model="ProsusAI/finbert")
text = "The bank took on additional debt to fund the acquisition."
print(clf(text)) # likely NEGATIVE - it sees "debt"
print(fin(text)) # likely neutral - it understands financing
# top_k returns the full distribution instead of just the argmax,
# which you need if you plan to threshold rather than take the top label
print(fin(text, top_k=None))
Fine-tune an encoder on your own labels
The core deep learning workflow. Everything else in this task is a variation on it.
from transformers import (AutoTokenizer, AutoModelForSequenceClassification,
TrainingArguments, Trainer)
from datasets import Dataset
import numpy as np, evaluate
MODEL = "distilbert-base-uncased"
labels = ["fees", "fraud", "service", "technical"]
tok = AutoTokenizer.from_pretrained(MODEL)
model = AutoModelForSequenceClassification.from_pretrained(
MODEL,
num_labels=len(labels),
# These two make the saved model self-describing - without them your
# endpoint returns LABEL_0 and nobody can remember what that meant.
id2label={i: l for i, l in enumerate(labels)},
label2id={l: i for i, l in enumerate(labels)},
)
ds = Dataset.from_dict({"text": texts, "label": label_ids})
ds = ds.map(lambda b: tok(b["text"], truncation=True, max_length=256),
batched=True)
ds = ds.train_test_split(test_size=0.2, seed=42)
f1 = evaluate.load("f1")
def metrics(p):
# macro F1, not accuracy - accuracy flatters imbalanced data
return f1.compute(predictions=np.argmax(p.predictions, axis=1),
references=p.label_ids, average="macro")
trainer = Trainer(
model=model,
args=TrainingArguments(output_dir="out", num_train_epochs=3,
per_device_train_batch_size=16,
learning_rate=2e-5, # 2e-5 to 5e-5 is the band
eval_strategy="epoch",
load_best_model_at_end=True,
metric_for_best_model="f1"),
train_dataset=ds["train"], eval_dataset=ds["test"],
compute_metrics=metrics,
)
trainer.train()
The same fine-tune, run on SageMaker
Identical training script. The difference is that AWS runs it on a machine you rent by the minute, and logs what data and code produced the artefact.
from sagemaker.huggingface import HuggingFace
estimator = HuggingFace(
entry_point="train.py", # the Trainer code above, as a script
source_dir="./scripts",
role=role,
instance_type="ml.g5.xlarge", # ~$1.20/hr; do NOT use p3 for this
instance_count=1,
transformers_version="4.36",
pytorch_version="2.1",
py_version="py310",
hyperparameters={"epochs": 3, "train_batch_size": 16,
"model_name": "distilbert-base-uncased"},
)
estimator.fit({"train": "s3://your-bucket/complaints/train",
"test": "s3://your-bucket/complaints/test"})
# Training shuts itself down. Endpoints do not - delete when finished.
predictor = estimator.deploy(initial_instance_count=1,
instance_type="ml.m5.xlarge")
print(predictor.predict({"inputs": "I was charged twice for the same payment"}))
predictor.delete_endpoint()
What catches people out 5
Accuracy is the wrong metric almost every time
Complaint and fraud data is imbalanced. A model that predicts the majority class for everything can score 95% accuracy and be useless. Report macro F1 and per-class recall, and decide which class you cannot afford to miss before you start.
512 tokens is the ceiling on most encoders
BERT-family models truncate at 512 word pieces, roughly 380 English words. Longer complaints get silently cut. Either chunk and pool, or use a long-context model. Check whether your signal lives at the end of the document — in complaints it usually does.
Calibration, not just ranking
A softmax output of 0.94 does not mean 94% likely to be correct. Neural classifiers are systematically overconfident. If you route on a threshold, calibrate first with Platt scaling or isotonic regression on a held-out set.
Split by time, not at random
Complaint language shifts with product launches and rate changes. A random split leaks the future into training and flatters your metrics. Train on older records, test on newer ones — that is the setting you will actually deploy into.
Model risk management applies
Supervisory guidance on model risk (SR 11-7 in the US) covers models used in business decisions, and internal validation teams will ask for conceptual soundness, ongoing monitoring and outcome analysis. A fine-tuned transformer is harder to get through that process than a linear model, which is a genuine cost to weigh, not a reason to avoid it.