Instead of one label per document, you get one per token. That lets you pull structured fields out of unstructured text: which words are a person, an organisation, a monetary amount, an account number.
The mechanical detail that trips everyone up is that transformers work on sub-word pieces, not words. 'Kensington' might become 'Ken', '##sing', '##ton'. Your labels are per word, so you have to align them to pieces, and that alignment code is where most NER bugs live.
Model families 4
Sub-categories within this task. The distinction that
matters is not which is best, but which fits your constraints.
Fine-tuned encoders with BIO tagging
66M–350M.
Use when
The standard approach. Entities follow patterns learnable from a few thousand annotated sentences, and you need it fast and cheap at inference.
Avoid when
You have no annotated data. Token-level annotation is far more expensive to produce than document labels.
The entity has a strict format — IBAN, sort code, SWIFT BIC, card number, LEI. A regular expression with a checksum beats any model, and it never drifts.
Avoid when
Entities are names or free-form descriptions.
Examples
Regex, spaCy EntityRuler, Aho-Corasick over a name list
Layout-aware models
125M–400M.
Use when
The document is a scanned form or statement where position carries meaning. These models take the bounding box of each token as an input alongside the text.
Avoid when
Plain text with no layout. You are paying for unused inputs.
Examples
microsoft/layoutlmv3-base
LLM extraction to a schema
7B and up.
Use when
Many entity types, frequently changing, or nested structure that BIO tagging cannot express. Ask for JSON, validate against a schema.
Avoid when
High volume, or you need character offsets back — generative models paraphrase and lose exact positions, which matters a great deal for redaction.
Examples
Any instruct LLM with structured output
In a bank 4
The third line in each is the part that makes it
harder than the textbook version.
PII detection and redaction before an LLM sees the text
Strip names, account numbers, addresses and card numbers from documents and chat logs before they leave the controlled environment.
The wrinkleThis is a recall problem with an asymmetric cost — a single missed account number is a data incident. Layer a model for names with deterministic regex for structured identifiers, and never rely on the model alone for the formatted ones.
Contract and ISDA term extraction
Pull counterparties, effective dates, notional amounts, governing law and termination clauses from legal agreements.
The wrinkleLegal documents run to hundreds of pages with entities that matter appearing once. Retrieval to find the relevant clause, then extraction on that span, works better than running NER over everything.
Sanctions and payment screening support
Identify person and organisation names in payment narratives so they can be matched against sanctions lists.
The wrinkleTransliteration and name-order variation across scripts. The extraction step is easier than the matching step that follows it, and false positives here become manual review queues that cost real money.
KYC document field extraction
Read identity documents, proof of address and incorporation certificates into structured fields.
The wrinkleOCR quality dominates everything downstream. A layout-aware model on a bad scan still fails. Measure the OCR separately before blaming the extractor.
Code 3
Named functions and the arguments that actually
change the outcome. Comments flag the traps.
Extract entities with a pretrained model
aggregation_strategy is the argument that matters — without it you get raw sub-word pieces instead of whole entities.
from transformers import pipeline
ner = pipeline("token-classification",
model="dslim/bert-base-NER",
aggregation_strategy="simple") # merges B- and I- pieces
text = ("Jane Okafor at Northwood Capital wired GBP 2,400,000 "
"to Meridian Holdings on 14 March.")
for e in ner(text):
print(f"{e['entity_group']:6} {e['word']:22} {e['score']:.3f} "
f"[{e['start']}:{e['end']}]")
# start/end are character offsets into the original string. Keep them -
# they are what lets you redact precisely instead of string-replacing,
# which would also hit unrelated occurrences of the same substring.
Redaction: model for names, regex for formatted IDs
The hybrid is not laziness. Structured identifiers have checksums, so a rule is both faster and more reliable than any model.
import re
PATTERNS = {
"IBAN": re.compile(r"\b[A-Z]{2}\d{2}[A-Z0-9]{11,30}\b"),
"SORT": re.compile(r"\b\d{2}-\d{2}-\d{2}\b"),
"CARD": re.compile(r"\b(?:\d[ -]*?){13,19}\b"),
"EMAIL": re.compile(r"\b[\w.+-]+@[\w-]+\.[\w.-]+\b"),
}
def redact(text, ner_pipe):
spans = []
# 1. deterministic identifiers
for tag, rx in PATTERNS.items():
spans += [(m.start(), m.end(), tag) for m in rx.finditer(text)]
# 2. model-detected names and organisations
for e in ner_pipe(text):
if e["entity_group"] in ("PER", "ORG") and e["score"] > 0.85:
spans.append((e["start"], e["end"], e["entity_group"]))
# replace right-to-left so earlier offsets stay valid
for s, e, tag in sorted(spans, key=lambda x: -x[0]):
text = text[:s] + f"[{tag}]" + text[e:]
return text
The alignment step, when you fine-tune
This is the part people get wrong. Sub-word pieces after the first must be masked with -100 so the loss ignores them; otherwise you train on labels that mean nothing.
def align_labels(examples, tokenizer, label2id):
enc = tokenizer(examples["tokens"], truncation=True,
is_split_into_words=True, max_length=256)
out = []
for i, tags in enumerate(examples["ner_tags"]):
word_ids = enc.word_ids(batch_index=i)
prev, labels = None, []
for wid in word_ids:
if wid is None:
labels.append(-100) # [CLS], [SEP], padding
elif wid != prev:
labels.append(tags[wid]) # first piece carries the label
else:
labels.append(-100) # continuation pieces ignored
prev = wid
out.append(labels)
enc["labels"] = out
return enc
What catches people out 3
Evaluate at entity level, not token level
Token accuracy is meaningless because most tokens are 'O'. Use seqeval, which scores a prediction correct only when the full span and its type both match. Getting 'Jane' but missing 'Okafor' is a failure, not a half-success.
Confidence thresholds are your main control
For redaction, lower the threshold and over-redact. For extraction into a system of record, raise it and route the uncertain cases to a human. Same model, opposite settings, driven entirely by which error costs more.
General NER models know four types
Most public models emit person, organisation, location and miscellaneous. Bank-specific types — product, fee type, regulation cited — do not exist off the shelf. That is a fine-tuning project with annotation attached, and the annotation is the expensive part.