Part of Language AI Handbook
Explains how NER identifies people, organizations, and locations using BIO sequence labeling. Topics include entity types, boundary detection, nested entities.
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Named Entity Recognition
Text is full of references to real-world things: people, companies, countries, dates, dollar amounts. Named Entity Recognition (NER) is the task of automatically identifying these mentions in raw text and classifying them into predefined categories such as person, organization, or location.
Consider the sentence: "Elon Musk announced that Tesla will open a new factory in Germany by March 2026." A NER system should identify "Elon Musk" as a person, "Tesla" as an organization, "Germany" as a location, and "March 2026" as a date. This extracted structure powers an enormous range of downstream applications: search engines that understand queries about specific people, question-answering systems that can retrieve factual information, business intelligence pipelines that monitor mentions of competitors, and biomedical systems that extract drug-disease relationships from clinical notes.
NER combines sequence labeling with information extraction. Like Part-of-Speech (POS) tagging (covered in the previous chapter), NER assigns labels to tokens. But NER introduces a harder challenge: entities span multiple tokens, and detecting where an entity begins and ends is itself a non-trivial problem. This chapter covers the entity type taxonomy, how NER is formulated as a sequence labeling problem, the specific difficulty of boundary detection and nested entities, the long evolution of NER architectures from hand-written rules through CRFs to transformer-based models, and the major datasets and benchmarks that have shaped the field.
Entity Types
NER systems classify entities into a predefined taxonomy of types. The categories chosen depend on the application domain, but a small set of core types recurs across virtually every NER benchmark and production system.
A named entity is a real-world object that can be referred to with a proper name: a specific person, place, organization, or other identifiable referent. The "named" qualifier distinguishes entities like "Apple Inc." (specific, nameable) from generic references like "the company" (anaphoric, not nameable). Proper names are the prototypical case, but NER systems also extract numerical and temporal expressions that refer to specific real-world quantities.
Core Entity Categories
The three foundational categories that appear in nearly every NER taxonomy are:
- PER (Person): Names of individual people, real or fictional. Examples include "Marie Curie", "Barack Obama", and "Sherlock Holmes". Titles attached to names ("Dr. Fauci", "President Lincoln") are typically included as part of the span.
- ORG (Organization): Companies, government agencies, universities, sports teams, and other collective entities. Examples include "NASA", "Google", "Harvard University", and "the World Health Organization".
- LOC (Location): Places, including countries, cities, regions, rivers, mountains, and physical addresses. Examples include "New York City", "the Amazon River", and "Mount Everest". Some tagsets split this further into GPE (geopolitical entities, like countries and cities) and LOC (pure geographic features, like mountains and rivers).
Extended taxonomies add categories for specific applications:
- DATE / TIME: Calendar dates and time expressions. Examples include "January 2024", "last Tuesday", and "3:00 PM EST".
- MONEY: Monetary amounts. Examples include "$50 million", "€200 billion", and "two hundred dollars".
- PERCENT: Percentage expressions. Examples include "7.5%", "a third", and "half".
- MISC (Miscellaneous): Entities that do not fit the above categories but are still proper nouns: nationality adjectives ("French", "Japanese"), product names, events, and works of art.
Domain-Specific Taxonomies
General-purpose tagsets are designed for broad applicability, but specialized domains benefit from finer-grained categories. Biomedical NER systems distinguish GENE, PROTEIN, DISEASE, CHEMICAL, SPECIES, and CELL_LINE. Legal NER systems label STATUTE, COURT, JUDGE, PLAINTIFF, and DEFENDANT. Financial NER systems need TICKER_SYMBOL, EXCHANGE, and FISCAL_PERIOD. These domain-specific systems are trained on annotated corpora from the target domain, because the distributional properties of entity mentions differ substantially from newswire text.
The choice of taxonomy is not purely a technical decision. It reflects assumptions about what information consumers of the extracted data need. A system designed to populate a corporate knowledge graph cares deeply about the distinction between a subsidiary (ORG) and its parent company (also ORG) in a way that a general news reader does not. Before designing or selecting a NER system for an application, it is worth asking: which entity types matter for downstream tasks, and at what granularity?
Let's load spaCy and inspect what entity types its general-purpose English model recognizes:
import spacy
# Load small English model
# Run once to install: python -m spacy download en_core_web_sm
nlp = spacy.load("en_core_web_sm")
text = (
"Elon Musk announced that Tesla will invest $3.5 billion to open "
"a new Gigafactory in Germany by March 2026. The project will employ "
"about 12,000 workers in Brandenburg."
)
doc = nlp(text)
entities = [
(ent.text, ent.label_, ent.start_char, ent.end_char) for ent in doc.ents
]Entity Label Chars ------------------------------------------------------------ Elon Musk PERSON 0–9 Tesla ORG 25–30 $3.5 billion MONEY 43–55 Germany GPE 85–92 March 2026 DATE 96–106 about 12,000 CARDINAL 132–144 Brandenburg GPE 156–167
The output shows each entity span, its type label, and its character offsets in the source string. spaCy's en_core_web_sm model uses an extended tagset that includes GPE (geo-political entity) as a distinct category from LOC. This reflects the CoNLL and OntoNotes annotation traditions. The character offsets are useful for highlighting entities in a UI or aligning predictions to the original text.
Entity Distribution by Domain
Different domains contain different mixes of entity types. Understanding the expected distribution helps when choosing a pre-trained model or designing a training corpus.
from collections import Counter
domains = {
"News": (
"President Biden met with Chancellor Scholz in Berlin. "
"The White House announced new climate policies. "
"Senator Warren criticized Wall Street banks."
),
"Financial": (
"Microsoft's Q3 revenue reached $52.9 billion, up 17% year-over-year. "
"The Nasdaq rose 2.5% following the Fed's decision. "
"Goldman Sachs raised their price target to $400."
),
"Biomedical": (
"Pfizer and Moderna both reported efficacy above 90% in Phase 3 trials. "
"The FDA approved remdesivir for use at Johns Hopkins Hospital. "
"Dr. Fauci cited data from the NIH study published in The Lancet."
),
}
domain_counts = {}
for domain_name, domain_text in domains.items():
doc = nlp(domain_text)
domain_counts[domain_name] = Counter(ent.label_ for ent in doc.ents)
all_types = sorted(set().union(*[c.keys() for c in domain_counts.values()]))
The chart confirms the intuition: financial text is dominated by MONEY and PERCENT entities, biomedical text mixes ORG and PERSON with product references, and news text skews heavily toward people and places. A model trained only on newswire will generalize poorly to financial filings, which is why domain adaptation or domain-specific training is important in practice.
NER as Sequence Labeling
The cleanest formulation of NER turns it into a token-level classification problem. Rather than predicting entity spans directly, we assign a label to each token and use a tagging scheme that encodes both the entity type and the position of the token within a multi-token span.
This framing has a powerful practical consequence: any sequence labeling model, from a simple logistic regression to a transformer, can be applied to NER without fundamental changes to the model architecture. The NER-specific engineering lives in the tagging scheme and the loss function, not in the model itself.
The BIO Tagging Scheme
The most widely used scheme is BIO (also called IOB2). Each token receives one of three tag prefixes:
- B-TYPE: The token begins an entity of type TYPE.
- I-TYPE: The token is inside a continuing entity of type TYPE.
- O: The token is outside any entity.
For the sentence "Tim Cook visited Paris", the BIO labels would be:
| Token | BIO Tag |
|---|---|
| Tim | B-PER |
| Cook | I-PER |
| visited | O |
| Paris | B-LOC |
This encoding turns span detection into token classification. A model that correctly predicts B-PER then I-PER for adjacent tokens implicitly identifies the span "Tim Cook" as a person entity. Span recovery from BIO tags is straightforward: a new entity begins whenever a B- tag appears or an I- tag follows a different entity type.
The BIO tagging scheme and its variants (BIOES, BILOU) are covered in detail in the next chapter on BIO Tagging. For now, the key insight is that this encoding makes NER amenable to any sequence labeling model: HMMs, CRFs, or neural architectures with token-level classification heads.
From Tokens to Spans
Given BIO predictions, recovering entity spans requires a simple decoding pass:
def bio_to_spans(tokens, bio_tags):
"""Convert a BIO-tagged sequence to a list of (start, end, label) spans."""
spans = []
current_start = None
current_label = None
for i, (token, tag) in enumerate(zip(tokens, bio_tags)):
if tag.startswith("B-"):
# Close any open span
if current_start is not None:
spans.append((current_start, i, current_label))
# Open new span
current_start = i
current_label = tag[2:]
elif tag.startswith("I-"):
label = tag[2:]
# If I- follows a different type or O, treat as a new B-
if current_label != label:
if current_start is not None:
spans.append((current_start, i, current_label))
current_start = i
current_label = label
else: # O
if current_start is not None:
spans.append((current_start, i, current_label))
current_start = None
current_label = None
# Close any trailing span
if current_start is not None:
spans.append((current_start, len(tokens), current_label))
return spans
tokens = ["Tim", "Cook", "visited", "Paris", "and", "London"]
bio_tags = ["B-PER", "I-PER", "O", "B-LOC", "O", "B-LOC"]
spans = bio_to_spans(tokens, bio_tags)Token → Tag mapping: Tim B-PER Cook I-PER visited O Paris B-LOC and O London B-LOC Recovered spans: [0:2] PER: 'Tim Cook' [3:4] LOC: 'Paris' [5:6] LOC: 'London'
The decoder correctly recovers "Tim Cook" as a single PER span (tokens 0-2), "Paris" as LOC (token 3-4), and "London" as LOC (token 5-6). The function also handles the case where an I- tag follows a different entity type (a common tagging inconsistency), treating it as a new begin rather than crashing.
Entity Boundary Detection
Identifying where entities begin and end is arguably harder than classifying entity types. Boundary errors account for a large fraction of NER mistakes in practice.
Sources of Boundary Ambiguity
Several linguistic phenomena make boundary detection difficult:
Prepositional attachment. Consider "the University of Michigan professor." Is the entity "University of Michigan" (a named institution) or "University of Michigan professor" (not an entity)? The correct boundary depends on semantic interpretation, not surface syntax.
Possessives and determiners. "Apple's quarterly earnings" contains "Apple" as an ORG entity. Should the possessive "'s" be included? Convention differs across annotation schemes. Most English NER corpora exclude the possessive clitic from the entity span, but this is a choice, not a linguistic necessity.
Titles and honorifics. "President Biden" versus "Biden": is the title part of the entity? The CoNLL 2003 annotation guidelines treat "President Biden" as a single PER span, while some other guidelines treat "Biden" alone as the entity.
Coordination. "Amazon and Google both reported profits" contains two ORG entities, but the surface form shares no obvious boundary marker. Systems trained on isolated entity mentions sometimes fail to split coordinated entities correctly.
Abbreviations. "the U.S." and "the United States" refer to the same entity, but their spans look completely different to a tokenizer.
These ambiguities are not merely academic curiosities. They translate directly into inter-annotator disagreement during corpus construction, and that disagreement propagates into the training data. When two annotators would choose different boundaries for the same span, a model trained on their combined labels learns an inconsistent mapping and produces boundary errors at inference time. Understanding the linguistic sources of boundary ambiguity helps both in designing annotation guidelines and in diagnosing model failures.
Annotation Guidelines and Their Impact
One of the least-discussed but most important factors in NER system quality is the annotation guideline document. Every NER benchmark comes with a guide that specifies, for hundreds of edge cases, exactly which tokens should be included in an entity span and what type label to assign. The CoNLL 2003 annotation guidelines run to dozens of pages. The OntoNotes guidelines are even longer.
These decisions have real consequences. The guideline that says "include titles like President and Dr. in the PER span" means a model trained on CoNLL 2003 will learn to extend person spans to include honorifics, while a model trained on a dataset with a different guideline will learn the opposite behavior. When you use a pre-trained NER model on new data, you are implicitly adopting the annotation conventions of whatever corpus that model was trained on. Mismatches between training conventions and deployment conventions are a common, underappreciated source of NER errors in production.
Nested Entities
One of the most challenging aspects of NER in real-world text is that entities can be nested: one entity span can contain another as a component.
A nested entity occurs when a named entity span is contained within a larger named entity span. For example, in "Bank of America headquarters," the inner span "America" is a LOC entity, while the outer span "Bank of America" is an ORG entity. Both are valid entities, but they overlap.
Standard BIO tagging cannot represent nested entities: a token can have only one tag, so the inner entity must be silently dropped. The sentence "the New York Times" would typically be tagged as a single ORG span, even though "New York" is also a valid LOC entity.
Several approaches address nested NER:
- Layer-by-layer annotation: Annotate one layer of entities at a time, allowing multiple tag sequences per sentence. The ACE 2004 and 2005 datasets use this approach.
- Span-based models: Instead of token-level tagging, enumerate all possible spans and classify each independently. This naturally handles nesting at the cost of candidate spans.
- Constituency parsing integration: Treat nested entities as a variant of the phrase structure parsing problem.
- Graph-based representations: Encode entities as nodes in a graph, with nesting represented by containment edges.
For most practical applications, flat NER (ignoring nesting) is sufficient. Nested NER matters in biomedical information extraction, where phrases like "human immunodeficiency virus type 1 protease inhibitor" contain multiple entity spans of different types.
Let's visualize how nested entities arise:
# Simulate a nested entity scenario using spaCy's span API
import spacy
nlp_blank = spacy.blank("en")
doc_nested = nlp_blank.make_doc(
"The New York Times published an article about Goldman Sachs."
)
# Flat NER would only capture outer spans
flat_entities = [
(0, 4, "ORG"), # "The New York Times"
(8, 10, "ORG"), # "Goldman Sachs"
]
# Nested annotation also captures inner spans
nested_entities = [
(0, 4, "ORG"), # "The New York Times"
(1, 3, "GPE"), # "New York" (inside the ORG)
(8, 10, "ORG"), # "Goldman Sachs"
]
tokens_list = [t.text for t in doc_nested]Tokens: ['The', 'New', 'York', 'Times', 'published', 'an', 'article', 'about', 'Goldman', 'Sachs', '.'] Flat NER spans: [0:4] ORG: 'The New York Times' [8:10] ORG: 'Goldman Sachs' Nested NER spans (including inner entities): [0:4] ORG: 'The New York Times' [1:3] GPE: 'New York' [8:10] ORG: 'Goldman Sachs'
The flat annotation captures "The New York Times" as a single ORG entity, while the nested annotation additionally captures "New York" as a GPE entity contained within it. Standard BIO tagging cannot represent both annotations simultaneously for the same token sequence.
A Brief History of NER Systems
NER has a rich history spanning four decades and three major paradigm shifts. Understanding this history clarifies why modern systems are designed the way they are and what problems each generation of approaches was solving.
Rule-Based Systems (1980s-1990s)
The earliest NER systems were built from hand-written rules and gazetteers. A gazetteer is a list of known entity names, for instance a list of country names, a list of major corporations, or a list of common first and last names. Rules then check whether a token appears in one of these lists, or whether contextual patterns like "Mr. [WORD]" or "[WORD] Inc." indicate an entity.
Rule-based systems have real advantages: they are interpretable, they never hallucinate, and they can be updated by simply editing the rules or adding entries to gazetteers. They remain useful in domains where the entity vocabulary is small and stable, such as a financial system that only needs to recognize a fixed list of stock tickers. Early systems like Proper Noun Analyzer (PROLEX) and FASTUS (1995) achieved surprisingly good performance on newswire NER using these techniques.
Their fundamental weakness is coverage and maintainability. Languages are productive: new entities are created constantly (new companies, new public figures, new events), and hand-written rules cannot keep pace. Rules also interact in complex ways as the rule set grows, making maintenance error-prone. By the late 1990s, the community was actively exploring statistical alternatives.
Statistical Systems: HMMs and CRFs (1990s-2010s)
The first statistical NER systems used Hidden Markov Models (HMMs). An HMM models the joint probability of a token sequence and its label sequence using emission probabilities (how likely is this word given this label?) and transition probabilities (how likely is this label given the previous label?). Training requires only annotated data, which removes the need for manual rule writing. We covered HMMs in detail in the previous chapter on sequence labeling.
HMMs have a significant limitation for NER: they rely on each token being independent given the current label. In practice, NER decisions depend on rich context: whether a word is capitalized, whether it is preceded by a title, whether it appears in a list of known organizations, and dozens of other features. Capturing these features within an HMM requires increasingly complex emission probability models.
Conditional Random Fields (CRFs), introduced by Lafferty, McCallum, and Pereira in 2001, addressed this limitation directly. A linear-chain CRF models the conditional distribution directly rather than the joint distribution, which means it can condition on arbitrary features of the entire input sequence without needing to model the input distribution. A typical CRF feature function for NER might fire when the current token is capitalized and the previous label was B-PER, assigning that configuration a learned weight.
The standard feature set for CRF-based NER includes:
- Word identity: the exact lowercased word form
- Word shape: capitalization pattern (Xxxx for Title Case, XXXX for all caps, xxxx for all lower)
- Prefix and suffix patterns: the first and last 2-4 characters
- Gazetteer membership: whether the word appears in a person/organization/location list
- Part-of-speech tag: the syntactic category
- Preceding and following words: context window of 1-2 tokens on each side
- Preceding and following labels: the predicted labels for surrounding tokens
CRFs with these features achieved state-of-the-art NER performance through most of the 2000s, reaching around 86-89 F1 on CoNLL 2003. The primary bottleneck was feature engineering: designing the right feature templates required substantial domain expertise and tuition through experimentation.
Let's demonstrate what a simple feature function for CRF-style NER looks like:
def word_features(tokens, i):
"""Extract features for token at position i."""
word = tokens[i]
features = {
"word.lower": word.lower(),
"word.isupper": word.isupper(),
"word.istitle": word.istitle(),
"word.isdigit": word.isdigit(),
"word[-3:]": word[-3:],
"word[-2:]": word[-2:],
"word[:3]": word[:3],
"word[:2]": word[:2],
}
if i > 0:
prev_word = tokens[i - 1]
features["prev.word.lower"] = prev_word.lower()
features["prev.word.istitle"] = prev_word.istitle()
else:
features["BOS"] = True # beginning of sentence
if i < len(tokens) - 1:
next_word = tokens[i + 1]
features["next.word.lower"] = next_word.lower()
features["next.word.istitle"] = next_word.istitle()
else:
features["EOS"] = True # end of sentence
return features
example_tokens = ["President", "Biden", "met", "Scholz", "in", "Berlin", "."]
all_features = [
word_features(example_tokens, i) for i in range(len(example_tokens))
]Features for each token:
President {'word.lower': 'president', 'word.isupper': False, 'word.istitle': True, 'word[-3:]': 'ent'}
Biden {'word.lower': 'biden', 'word.isupper': False, 'word.istitle': True, 'word[-3:]': 'den'}
met {'word.lower': 'met', 'word.isupper': False, 'word.istitle': False, 'word[-3:]': 'met'}
Scholz {'word.lower': 'scholz', 'word.isupper': False, 'word.istitle': True, 'word[-3:]': 'olz'}
in {'word.lower': 'in', 'word.isupper': False, 'word.istitle': False, 'word[-3:]': 'in'}
Berlin {'word.lower': 'berlin', 'word.isupper': False, 'word.istitle': True, 'word[-3:]': 'lin'}
. {'word.lower': '.', 'word.isupper': False, 'word.istitle': False, 'word[-3:]': '.'}Each token generates a dictionary of binary and string-valued features. The CRF then learns a weight for each (feature, label) combination. At inference time, the Viterbi algorithm finds the label sequence with the highest total weighted score, taking into account both feature weights and label transition weights.
Neural NER: Word Embeddings and BiLSTMs (2015-2018)
The shift from hand-engineered features to learned representations began with word embeddings. Instead of encoding "does this word appear in a gazetteer?", neural NER systems encode each word as a dense vector that captures its distributional meaning. This frees the model from the need for manual feature engineering and allows it to generalize to unseen words based on semantic similarity to known words.
The architecture that defined this era was the BiLSTM-CRF, introduced by Lample et al. (2016) and Chiu and Nichols (2016). The model has three components working together:
First, a character-level convolutional network or BiLSTM reads each word's characters and produces a character-level embedding. This component captures morphological patterns, such as recognizing that words ending in "-son" (Dickson, Jackson, Peterson) tend to be surnames, or that fully capitalized words are often abbreviations. This matters enormously for NER because named entities frequently contain unusual words that are out of vocabulary for word-level models.
Second, the character embedding is concatenated with a pre-trained word embedding (GloVe or fastText) to form a combined token representation. The combined representation captures both morphological patterns and semantic associations.
Third, a bidirectional LSTM processes the sequence of token representations, producing a context-sensitive encoding for each token. At each position, the BiLSTM combines information from the left context (how has the text built up to this point?) and the right context (what comes after this token?). This bidirectional view is essential for NER: recognizing "Times" as part of an organization name requires knowing it follows "New York", and recognizing "Bank" as part of "Bank of America" requires knowing what follows it.
The BiLSTM outputs feed into a linear layer that produces logit scores for each possible BIO tag. These scores feed into a CRF layer that applies the Viterbi algorithm during decoding. The CRF layer enforces label transition constraints, such as the rule that an I-PER tag cannot immediately follow an O tag, which improves output consistency compared to independent token-level classification.
BiLSTM-CRF systems achieved around 90-91 F1 on CoNLL 2003, an improvement of 4-5 points over classical CRFs. The gain came almost entirely from the learned representations replacing hand-engineered features. The core sequence labeling logic, including the CRF decoding, remained the same.
Transformer-Based NER (2018-Present)
The introduction of BERT in 2018 created the current dominant paradigm for NER. Rather than training task-specific representations from scratch, BERT provides deeply contextual word representations pre-trained on large text corpora. Fine-tuning BERT for NER requires only adding a linear classification head on top of the token representations and training on labeled NER data.
The key advance over BiLSTM models is the quality of the representations. BERT uses self-attention (covered in detail in later chapters) to produce representations that integrate long-range context much more effectively than LSTMs. For NER, this means BERT can use cues from many positions away to determine entity type and boundaries. A sentence like "He said it was the Microsoft founder's decision" requires integrating information about the subject, the verb, and the noun phrase to correctly identify "Microsoft" as an ORG entity in that context.
With BERT-base fine-tuned on CoNLL 2003, NER performance jumped to around 92 F1. Larger models (BERT-large, RoBERTa-large) reached 93-94 F1. These numbers had seemed nearly impossible a few years earlier.
The transformer architecture also changed how practitioners think about NER for new domains. With classical CRFs or BiLSTM-CRFs, adapting to a new domain required substantial annotated training data to learn new feature weights or BiLSTM representations. With pre-trained transformers, the representations already encode rich world knowledge and linguistic patterns from large corpora. Fine-tuning on even a few hundred labeled examples in a new domain can produce a usable system, because the model is not learning representations from scratch.
CRF-Based NER: A Working Implementation
Let's build a complete CRF-based NER pipeline to understand the mechanics. We will use the sklearn-crfsuite package, which provides a clean Python interface to CRF training.
import subprocess
import sys
# Install sklearn-crfsuite if needed
try:
import sklearn_crfsuite
except ImportError:
subprocess.run(
[sys.executable, "-m", "pip", "install", "sklearn-crfsuite", "-q"],
check=True,
)def sent_to_features(sent):
"""
Convert a sentence (list of (word, pos, chunk, ner) tuples)
to a list of feature dicts, one per token.
"""
return [word_features_for_crf(sent, i) for i in range(len(sent))]
def word_features_for_crf(sent, i):
word = sent[i][0]
postag = sent[i][1]
features = {
"bias": 1.0,
"word.lower()": word.lower(),
"word[-3:]": word[-3:],
"word[-2:]": word[-2:],
"word.isupper()": word.isupper(),
"word.istitle()": word.istitle(),
"word.isdigit()": word.isdigit(),
"postag": postag,
"postag[:2]": postag[:2],
}
if i > 0:
word1 = sent[i - 1][0]
postag1 = sent[i - 1][1]
features.update(
{
"-1:word.lower()": word1.lower(),
"-1:word.istitle()": word1.istitle(),
"-1:word.isupper()": word1.isupper(),
"-1:postag": postag1,
"-1:postag[:2]": postag1[:2],
}
)
else:
features["BOS"] = True
if i < len(sent) - 1:
word1 = sent[i + 1][0]
postag1 = sent[i + 1][1]
features.update(
{
"+1:word.lower()": word1.lower(),
"+1:word.istitle()": word1.istitle(),
"+1:word.isupper()": word1.isupper(),
"+1:postag": postag1,
"+1:postag[:2]": postag1[:2],
}
)
else:
features["EOS"] = True
return features
def sent_to_labels(sent):
return [label for _, _, label in sent]# Small synthetic training set:
# Each sentence is a list of (word, pos_tag, ner_label)
train_sents = [
[
("Apple", "NNP", "B-ORG"),
("is", "VBZ", "O"),
("based", "VBN", "O"),
("in", "IN", "O"),
("Cupertino", "NNP", "B-LOC"),
(".", ".", "O"),
],
[
("Tim", "NNP", "B-PER"),
("Cook", "NNP", "I-PER"),
("leads", "VBZ", "O"),
("Apple", "NNP", "B-ORG"),
(".", ".", "O"),
],
[
("Google", "NNP", "B-ORG"),
("announced", "VBD", "O"),
("new", "JJ", "O"),
("offices", "NNS", "O"),
("in", "IN", "O"),
("London", "NNP", "B-LOC"),
(".", ".", "O"),
],
[
("Satya", "NNP", "B-PER"),
("Nadella", "NNP", "I-PER"),
("runs", "VBZ", "O"),
("Microsoft", "NNP", "B-ORG"),
(".", ".", "O"),
],
[
("Paris", "NNP", "B-LOC"),
("is", "VBZ", "O"),
("the", "DT", "O"),
("capital", "NN", "O"),
("of", "IN", "O"),
("France", "NNP", "B-LOC"),
(".", ".", "O"),
],
]
X_train = [sent_to_features(s) for s in train_sents]
y_train = [sent_to_labels(s) for s in train_sents]
# Train CRF
crf = CRF(
algorithm="lbfgs",
c1=0.1,
c2=0.1,
max_iterations=100,
all_possible_transitions=True,
)
crf.fit(X_train, y_train)# Test on a new sentence
test_sent = [
("Amazon", "NNP"),
("CEO", "NN"),
("Andy", "NNP"),
("Jassy", "NNP"),
("visited", "VBD"),
("Berlin", "NNP"),
(".", "."),
]
def predict_ner(model, sent_with_pos):
"""Predict NER labels for a sentence given as (word, pos) pairs."""
# Temporarily package as (word, pos, dummy_label)
packaged = [(w, p, "O") for w, p in sent_with_pos]
feats = sent_to_features(packaged)
return model.predict([feats])[0]
predicted_labels = predict_ner(crf, test_sent)CRF NER predictions on new sentence: Token POS Predicted Label ----------------------------------- Amazon NNP B-ORG CEO NN O Andy NNP O Jassy NNP O visited VBD O Berlin NNP B-LOC . . O
The CRF correctly identifies "Andy Jassy" as a PER entity (relying on capitalization and the context of a preceding ORG), "Amazon" as an ORG, and "Berlin" as a LOC. Despite the extremely small training set, the model generalizes because the features (capitalization, POS tags, context words) generalize across entity types.
One of the most useful diagnostic tools for CRF models is inspecting the learned transition weights. High transition weights for B-PER to I-PER confirm the model learned that person names commonly have multiple tokens. High negative weights for O to I-PER confirm the model learned that an inside tag cannot appear without a preceding begin tag.
NER Evaluation
Measuring NER system performance requires care because there are multiple ways to define what counts as a correct prediction. Two distinct evaluation philosophies produce very different numbers for the same system.
Exact Match vs. Partial Match
In exact match evaluation (also called strict evaluation), a prediction is correct only if both the span boundaries and the entity type match the gold annotation exactly. A system that predicts "Tim Cook" as ORG instead of PER gets zero credit. A system that predicts "Cook" instead of "Tim Cook" also gets zero credit, even though it partially overlapped with the correct span.
Partial match evaluation (also called lenient evaluation) relaxes this requirement. It counts a prediction as correct if it overlaps with a gold span, even if the boundaries are imprecise or the type is wrong. Different variants of partial matching include:
- Type match: The entity type is correct, boundary is ignored.
- Partial boundary match: The predicted span overlaps with the gold span, regardless of type.
- Exact type + partial boundary: Type is correct and spans overlap.
Strict (exact match) evaluation is the standard for NER benchmarks like CoNLL 2003. It requires both the exact span boundaries and the correct type label. Lenient evaluation is used when approximate extraction is acceptable, for example in information retrieval applications where finding the entity at all matters more than exact delineation.
Precision, Recall, and F1
NER evaluation uses precision, recall, and their harmonic mean F1, computed over entity spans rather than individual tokens.
Given a set of predicted entity spans and gold spans , the metrics are:
where:
- : the set of (span, type) pairs predicted by the system
- : the set of (span, type) pairs in the gold annotation
- : the number of predicted spans that exactly match a gold span (in both boundary and type)
Precision measures how often the system's predictions are correct. Recall measures how often gold entities are found. F1 balances the two. A system that predicts fewer but more accurate entities will have high precision and low recall. A system that aggressively predicts many entities will have high recall and low precision.
NER systems typically report span-level F1 for each entity type separately (per-type F1) as well as a macro-averaged F1 across types and a micro-averaged F1 weighted by entity count.
Let's implement these metrics from scratch:
def ner_metrics(gold_spans, pred_spans):
"""
Compute precision, recall, and F1 for NER.
gold_spans, pred_spans: lists of (start, end, label) tuples
"""
gold_set = set(gold_spans)
pred_set = set(pred_spans)
true_positives = len(gold_set & pred_set)
false_positives = len(pred_set - gold_set)
false_negatives = len(gold_set - pred_set)
precision = (
true_positives / (true_positives + false_positives) if pred_set else 0.0
)
recall = (
true_positives / (true_positives + false_negatives) if gold_set else 0.0
)
f1 = (
2 * precision * recall / (precision + recall)
if (precision + recall) > 0
else 0.0
)
return {
"precision": precision,
"recall": recall,
"f1": f1,
"tp": true_positives,
"fp": false_positives,
"fn": false_negatives,
}
# Gold annotations (correct answer)
gold = [
(0, 2, "PER"), # "Tim Cook"
(3, 4, "ORG"), # "Apple"
(6, 7, "LOC"), # "Paris"
(9, 11, "DATE"), # "next Tuesday"
]
# System A: good at persons and locations, misses date, wrong type on org
pred_a = [
(0, 2, "PER"), # correct
(3, 4, "PER"), # wrong type: predicted PER instead of ORG
(6, 7, "LOC"), # correct
# date missed entirely
]
# System B: slightly off boundaries but correct types
pred_b = [
(0, 2, "PER"), # correct
(3, 4, "ORG"), # correct
(6, 7, "LOC"), # correct
(9, 10, "DATE"), # partial: "next" only, not "next Tuesday"
]
metrics_a = ner_metrics(gold, pred_a)
metrics_b = ner_metrics(gold, pred_b)Gold spans: (0, 2, 'PER') (3, 4, 'ORG') (6, 7, 'LOC') (9, 11, 'DATE') System A predictions (type error on ORG, missed DATE): (0, 2, 'PER') (3, 4, 'PER') (6, 7, 'LOC') Precision=0.67 Recall=0.50 F1=0.57 TP=2 FP=1 FN=2 System B predictions (correct types, one boundary error): (0, 2, 'PER') (3, 4, 'ORG') (6, 7, 'LOC') (9, 10, 'DATE') Precision=0.75 Recall=0.75 F1=0.75 TP=3 FP=1 FN=1
System A gets 2 true positives (the PER and LOC spans that exactly match gold), 1 false positive (the wrong-type ORG prediction), and 2 false negatives (the undetected DATE and the wrong-type ORG). System B also gets 2 true positives (PER and LOC), 1 false positive (the partial DATE span), and 2 false negatives (the exact DATE and the missing ORG). Both systems achieve an F1 of 0.57. This shows that type errors and boundary errors have symmetric penalties under exact match scoring.
This strictness is both a strength and a limitation of exact match evaluation. It creates an unambiguous ranking of systems, but it can understate the practical utility of a system that consistently finds entities with slightly off boundaries.
Visualization of Evaluation Errors

Boundary errors and type errors each account for roughly 10% of predictions in this simulation. Boundary errors are the more common failure mode in practice because they often arise from consistent annotation disagreements (whether to include titles, whether to extend a span to include possessives) rather than fundamental model failures.
Comparing Evaluation Schemes
Let's directly compare exact match and partial match evaluation on the same predictions to see how much the choice of evaluation scheme affects reported performance:
# Simulated gold annotations for a small test set
gold_annotations = [
# Sentence 1: "Apple CEO Tim Cook visited Berlin."
[(0, 1, "ORG"), (2, 4, "PER"), (5, 6, "GPE")],
# Sentence 2: "Google and Amazon invested $500 million in AI."
[(0, 1, "ORG"), (2, 3, "ORG"), (4, 6, "MONEY")],
# Sentence 3: "The WHO published new guidelines in January 2025."
[(1, 2, "ORG"), (5, 7, "DATE")],
]
# Simulated system predictions (with some errors)
pred_annotations = [
# Sentence 1: correct on PER and GPE, misses ORG
[(2, 4, "PER"), (5, 6, "GPE")],
# Sentence 2: correct on ORG spans, boundary error on MONEY
[(0, 1, "ORG"), (2, 3, "ORG"), (4, 5, "MONEY")],
# Sentence 3: correct on both
[(1, 2, "ORG"), (5, 7, "DATE")],
]
# Aggregate across sentences
all_gold = [span for sent in gold_annotations for span in sent]
all_pred = [span for sent in pred_annotations for span in sent]
overall = ner_metrics(all_gold, all_pred)
def partial_match_metrics(gold_spans, pred_spans):
"""
Lenient evaluation: a prediction is correct if spans overlap,
regardless of exact boundaries (type must still match).
"""
matched_gold = set()
true_positives = 0
for pg_start, pg_end, pg_label in pred_spans:
for i, (gg_start, gg_end, gg_label) in enumerate(gold_spans):
if (
pg_label == gg_label
and pg_start < gg_end
and pg_end > gg_start
and i not in matched_gold
):
true_positives += 1
matched_gold.add(i)
break
false_positives = len(pred_spans) - true_positives
false_negatives = len(gold_spans) - true_positives
precision = true_positives / len(pred_spans) if pred_spans else 0.0
recall = true_positives / len(gold_spans) if gold_spans else 0.0
f1 = (
2 * precision * recall / (precision + recall)
if (precision + recall) > 0
else 0.0
)
return {"precision": precision, "recall": recall, "f1": f1}
exact = ner_metrics(all_gold, all_pred)
partial = partial_match_metrics(all_gold, all_pred)
The partial match scores are higher because they award credit for the boundary-error MONEY span that exact match rejects. In practice, this gap can be substantial (5-15 points of F1) on domains where entity boundaries are ambiguous.
NER Datasets and Benchmarks
Several datasets have become standard reference points for NER system comparison. Understanding their origins, domains, and annotation conventions is essential for interpreting published results.
CoNLL 2003
CoNLL 2003 is the most widely used English NER benchmark. It was released as part of the 2003 Conference on Computational Natural Language Learning shared task and consists of newswire text from the Reuters Corpus, annotated with four entity types: PER, ORG, LOC, and MISC.
The dataset contains approximately 22,000 sentences (roughly 300,000 tokens) split into training, development (testa), and test (testb) sets. The test set F1 score on this dataset is the standard comparison point for NER systems across a decade of research, enabling direct comparisons between systems published years apart.
A critical property of CoNLL 2003 is that its annotation guidelines are relatively conservative: abbreviations and ambiguous cases tend to be left unannotated rather than annotated with uncertain labels. This makes the dataset cleaner but also means systems tuned on it may not generalize well to noisier domains.
An important caveat about CoNLL 2003 scores: subsequent research has shown that the test set contains a significant number of annotation errors, and that near-human performance may already have been reached for this particular benchmark. Models reporting 94+ F1 on CoNLL 2003 may be partially benefiting from overfitting to specific annotation idiosyncrasies. This does not diminish the value of the dataset as a long-term comparison point, but it does suggest that further gains on this specific benchmark may not translate into equivalent gains on real-world NER tasks.
OntoNotes 5.0
OntoNotes 5.0 is a larger and more linguistically diverse corpus covering newswire, broadcast news, broadcast conversation, web text, magazine articles, and telephone conversations. It uses an 18-class entity taxonomy (the same one underlying spaCy's default model), including CARDINAL, ORDINAL, QUANTITY, and LAW in addition to the standard PER/ORG/LOC/DATE/MONEY/PERCENT types.
Because OntoNotes includes spoken language transcripts and web text, it is harder and more representative of real-world variation than CoNLL 2003. State-of-the-art F1 on OntoNotes is roughly 5-8 points lower than on CoNLL 2003 for equivalent-sized models.
ACE 2004 and 2005
The Automatic Content Extraction (ACE) datasets were developed by NIST for the information extraction community. They annotate entities, relations, and events, making them useful for systems that need to extract structured knowledge rather than just entity mentions. ACE uses a nested entity annotation scheme, capturing both inner and outer entity spans.
Biomedical Benchmarks
The biomedical NLP community has developed specialized datasets for gene/protein NER (BioCreative II Gene Mention task), disease NER (NCBI Disease corpus), and chemical/drug NER (BC5CDR corpus). These datasets use expert annotators (biologists, pharmacists) and are substantially harder than general-domain NER because entity boundaries in biomedical text are often ambiguous even to human experts.
Benchmark Progression
NER system performance on CoNLL 2003 has improved dramatically over time, driven by architectural advances. Here is how major model families compare:
| System | Year | Test F1 | Architecture |
|---|---|---|---|
| Rule-based baseline | pre-2003 | ~70 | Lexicon + rules |
| CRF (Lafferty et al.) | 2001 | ~82 | CRF with hand features |
| BiLSTM-CRF | 2015 | ~91 | Neural + CRF |
| BERT-base fine-tuned | 2019 | ~92 | Transformer |
| RoBERTa-large | 2020 | ~94 | Transformer |
| Current SOTA | 2024 | ~96 | Large LM + ensemble |
The jump from CRF to BiLSTM-CRF marks the move to neural representations: instead of hand-engineered features (capitalization, prefix/suffix patterns, gazetteers), the model learns distributed representations of words and their contexts. The jump from BiLSTM-CRF to BERT marks the move to pre-trained language model representations, which encode much richer contextual information about entity mentions.
Per-Type Performance Gaps
Not all entity types are equally easy. Let's look at a simulated breakdown to understand the pattern:

Person names typically achieve the highest F1 because they have strong surface-level signals: capitalization, relatively stable name patterns, and extensive coverage in name lists (gazetteers). MISC entities are hardest because the category is semantically heterogeneous. Organizations fall in between: many are easy (well-known company names), but new or ambiguous organizations (a local business, a rarely mentioned agency) are much harder.
BERT-Based NER
Modern NER is dominated by transformer-based approaches. Let's understand exactly how BERT is adapted for NER and implement a complete fine-tuning pipeline.
How Tokenization Interacts with NER
One subtlety in applying BERT to NER is that BERT uses subword tokenization (WordPiece), which can split words into multiple tokens. The word "Nadella" might become ["Na", "##della"]. When BERT assigns a label to each subword, we need a strategy for aggregating or ignoring subword predictions.
The standard approach is to label only the first subword of each original word and ignore the labels for continuation subwords during evaluation. This keeps the subword details inside the model while maintaining word-level predictions at output time.
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
# Example with entity that gets split into subwords
example_words = ["Nadella", "leads", "Microsoft", "in", "Redmond", "."]
example_labels = ["B-PER", "O", "B-ORG", "O", "B-LOC", "O"]
# Tokenize word by word, tracking alignment
encoding = tokenizer(
example_words,
is_split_into_words=True,
return_offsets_mapping=True,
return_tensors=None,
)
subword_tokens = tokenizer.convert_ids_to_tokens(encoding["input_ids"])
word_ids = encoding.word_ids()Subword alignment for NER: Subword Token Word ID Label (first subword only) ------------------------------------------------------- [CLS] None [special] Na 0 B-PER ##dell 0 I-PER ##a 0 I-PER leads 1 O Microsoft 2 B-ORG in 3 O Red 4 B-LOC ##mond 4 I-LOC . 5 O [SEP] None [special]
The special tokens ([CLS] and [SEP]) get word_id None and are excluded from NER evaluation. For continuation subwords (marked with ## in WordPiece), the standard practice during training is to assign them a special ignore index so they contribute no gradient.
Fine-Tuning Architecture
The BERT-for-NER architecture is straightforward. The model takes a tokenized sentence, runs it through BERT's transformer layers to produce a contextualized embedding for each token, and then passes those embeddings through a linear layer that maps to the number of possible BIO tags.
import torch.nn as nn
from transformers import AutoModel
class BertForNER(nn.Module):
"""Minimal BERT-based NER model with token classification head."""
def __init__(self, model_name, num_labels):
super().__init__()
self.bert = AutoModel.from_pretrained(model_name)
hidden_size = self.bert.config.hidden_size
self.dropout = nn.Dropout(0.1)
self.classifier = nn.Linear(hidden_size, num_labels)
def forward(self, input_ids, attention_mask, token_type_ids=None):
outputs = self.bert(
input_ids=input_ids,
attention_mask=attention_mask,
token_type_ids=token_type_ids,
)
# outputs.last_hidden_state: (batch, seq_len, hidden_size)
sequence_output = self.dropout(outputs.last_hidden_state)
logits = self.classifier(sequence_output)
# logits: (batch, seq_len, num_labels)
return logits
# Entity labels for a CoNLL-style 4-class tagset
label_list = [
"O",
"B-PER",
"I-PER",
"B-ORG",
"I-ORG",
"B-LOC",
"I-LOC",
"B-MISC",
"I-MISC",
]
num_labels = len(label_list)
label2id = {label: i for i, label in enumerate(label_list)}
id2label = {i: label for i, label in enumerate(label_list)}NER label set (9 labels): 0: O 1: B-PER 2: I-PER 3: B-ORG 4: I-ORG 5: B-LOC 6: I-LOC 7: B-MISC 8: I-MISC
The architecture is simple, but its power comes from the BERT encoder. The 12 transformer layers in BERT-base have already learned, through pre-training on billions of words, to produce representations that encode:
- Co-reference context: whether "he" or "she" in context refers to a named person
- Syntactic structure: whether a token is the head of a noun phrase or a modifier
- World knowledge: associations between particular tokens and entity types
These learned representations mean the linear classification head only needs to learn a relatively simple mapping from representation to label, rather than learning everything from scratch.
Training Loop
In practice, fine-tuning BERT for NER uses the cross-entropy loss on the first subword of each original word, ignoring special tokens and continuation subwords:
def compute_ner_loss(logits, labels, attention_mask, ignore_index=-100):
"""
Compute cross-entropy loss for NER.
logits: (batch, seq_len, num_labels)
labels: (batch, seq_len) -- use ignore_index for subwords/specials
attention_mask: (batch, seq_len)
"""
loss_fn = nn.CrossEntropyLoss(ignore_index=ignore_index)
# Flatten batch and sequence dimensions
active_mask = attention_mask.view(-1) == 1
active_logits = logits.view(-1, logits.size(-1))[active_mask]
active_labels = labels.view(-1)[active_mask]
loss = loss_fn(active_logits, active_labels)
return loss
# Demonstrate with a tiny synthetic batch
batch_size, seq_len = 2, 8
logits_demo = torch.randn(batch_size, seq_len, num_labels)
labels_demo = torch.tensor(
[
[
label2id["B-PER"],
label2id["I-PER"],
label2id["O"],
label2id["B-ORG"],
label2id["O"],
label2id["B-LOC"],
label2id["O"],
-100,
],
[
label2id["O"],
label2id["B-ORG"],
label2id["I-ORG"],
label2id["O"],
-100,
-100,
-100,
-100,
],
]
)
mask_demo = (labels_demo != -100).long()
loss = compute_ner_loss(logits_demo, labels_demo, mask_demo)Demo NER loss: 2.5542 Active tokens in batch: 11 / 16 Labels shape: torch.Size([2, 8]) Logits shape: torch.Size([2, 8, 9])
The loss ignores the padding positions (where labels are -100) and computes cross-entropy only on the tokens where we have meaningful labels. After a few thousand gradient steps on annotated NER data, the linear classifier learns to combine BERT's contextual representations into accurate entity type predictions.
Cross-Lingual and Multilingual NER
NER is a global problem. Text in dozens of languages needs entity extraction for practical applications, but annotated training data exists for only a small fraction of the world's languages. Cross-lingual NER addresses this gap by using high-resource language data to build models that work in low-resource languages.
Multilingual Pre-Training
The key enabler for cross-lingual NER is multilingual pre-training. Models like multilingual BERT (mBERT) and XLM-RoBERTa are pre-trained on text from 100+ languages simultaneously using a shared vocabulary and shared transformer parameters. A remarkable finding from the 2019 paper by Pires et al. is that these models develop cross-lingual representations: the representation of "Paris" in French text is close to the representation of "Paris" in English text, even though the model was never explicitly trained to align representations across languages.
This cross-lingual alignment enables zero-shot cross-lingual transfer: train a NER model on English CoNLL 2003 data, and the model generalizes to French, German, Spanish, or Dutch NER without any additional training data in those languages. In the original mBERT experiments, zero-shot transfer achieved 50-70% of fully supervised performance depending on the language, which is remarkable given that no target-language training data was used at all.
XLM-RoBERTa, trained on a larger multilingual corpus (2.5 TB of CommonCrawl data in 100 languages), substantially improves on mBERT's cross-lingual transfer performance. On the XTREME multilingual NER benchmark, XLM-RoBERTa achieves about 65 F1 in zero-shot transfer settings, compared to 58-62 F1 for mBERT.
Language-Specific Challenges
Even with strong multilingual models, some languages pose particular challenges for NER:
Agglutinative languages like Finnish, Turkish, and Korean attach multiple morphological suffixes to a single word stem. In Finnish, "Helsingissä" (in Helsinki) and "Helsingin" (Helsinki's) are different surface forms of the same city name. Subword tokenization helps here by splitting at morpheme boundaries, but the model still needs to handle many surface variants of the same entity.
Languages without word boundaries like Chinese and Japanese require joint tokenization and NER, since there are no spaces between tokens. Chinese NER typically processes character by character, with the BIO labels assigned to characters rather than words.
Languages with grammatical case like German, Russian, and Czech change the surface form of entity mentions depending on syntactic role. "Angela Merkel" in nominative German versus "Angela Merkels" in genitive German or "Angela Merkel" in accusative German are the same entity in different grammatical forms. A model must recognize all forms as referring to the same person.
Right-to-left scripts like Arabic and Hebrew require special handling for text direction and present additional challenges because Arabic script uses connected letters with no word-internal spacing at all.
Low-Resource NER
For truly low-resource languages with little or no annotated NER data, several techniques help:
Distant supervision automatically labels text by matching strings against existing knowledge bases (like Wikidata). If "Berlin" appears in Wikidata as a city, any occurrence of "Berlin" in text is automatically labeled LOC. Distant supervision is noisy (it cannot handle ambiguous names like "Springfield" or "Amazon"), but it can produce large amounts of weakly labeled training data at low cost.
Cross-lingual data augmentation translates annotated training data from a high-resource language into the target language using machine translation, then applies the original annotations to the translated text. This assumes entity mentions are preserved through translation, which is approximately true for most proper names.
Few-shot learning approaches, including prompt-based methods and retrieval-augmented methods, use small numbers of labeled examples to adapt a pre-trained model to a new domain or language. These methods are particularly relevant for NER because annotating even 50-100 sentences per entity type can significantly improve performance over zero-shot baselines.
Practical NER with HuggingFace
The HuggingFace transformers library provides a complete ecosystem for BERT-based NER. Let's walk through loading a pre-trained NER model and applying it to text:
from transformers import pipeline
# Load a NER pipeline backed by a fine-tuned BERT model
# This downloads the model weights on first run (~400 MB)
ner_pipeline = pipeline(
"ner",
model="dslim/bert-base-NER",
aggregation_strategy="simple", # merge consecutive same-type tokens
)
test_texts = [
"Sundar Pichai, CEO of Alphabet, announced new AI features at Google I/O in San Francisco.",
"The European Central Bank, led by Christine Lagarde, raised rates by 25 basis points.",
"MIT researchers published a study in Nature on the treatment of Alzheimer's disease.",
]Text: Sundar Pichai, CEO of Alphabet, announced new AI features at Google I/O in San Francisco. Entity Type Confidence ------------------------------------------------------------ Sundar Pichai PER 0.997 Alphabet ORG 0.999 AI MISC 0.989 Google I / O ORG 0.991 San Francisco LOC 0.999 Text: The European Central Bank, led by Christine Lagarde, raised rates by 25 basis points. Entity Type Confidence ------------------------------------------------------------ European Central Bank ORG 0.999 Christine Lagarde PER 0.942 Text: MIT researchers published a study in Nature on the treatment of Alzheimer's disease. Entity Type Confidence ------------------------------------------------------------ MIT ORG 0.999 Nature ORG 0.993 Alzheimer MISC 0.513
The aggregation_strategy="simple" parameter merges consecutive tokens of the same entity type into a single span, combining their scores by averaging. This is essential for multi-token entities like "Christine Lagarde" that the model labels token by token.
Interpreting Confidence Scores
BERT-based NER models produce a softmax probability over entity types for each token. The scores reported by the pipeline are these probabilities for the predicted class. A score of 0.99 indicates the model is very confident; a score of 0.65 indicates uncertainty.
Low-confidence predictions deserve scrutiny. They often correspond to:
- Ambiguous entity names that could be multiple types (is "Apple" the company or the fruit?)
- Unusual or rare entities not well represented in training data
- Entities at the boundary of entity spans (the last token of a long entity often has lower confidence)
- Domain mismatch between training data and the input text
In production systems, you can use confidence thresholds to accept or reject predictions. A high-precision mode might only accept predictions above 0.95, while a high-recall mode might accept anything above 0.7.
# Demonstrate confidence-based filtering
threshold_high = 0.95
threshold_low = 0.70
ambiguous_text = (
"Apple sued Samsung over patent violations in Delaware. "
"Amazon and Sprint also filed separate suits in California."
)
raw_predictions = ner_pipeline(ambiguous_text)All predictions: Apple ORG 0.999 Samsung ORG 0.998 Delaware LOC 0.999 Amazon ORG 0.999 Sprint ORG 0.998 California LOC 1.000 High-precision mode (>= 0.95): 6 accepted High-recall mode (>= 0.7): 6 accepted
Gazetteers and External Knowledge
While transformer models have largely replaced hand-crafted features, gazetteers and external knowledge sources remain valuable as augmentation, particularly in specialized domains.
A gazetteer is a curated list of entity mentions associated with entity types. For example:
- A gazetteer of company names from a financial database, updated daily
- A gazetteer of drug names from PubChem or DrugBank
- A gazetteer of geographic names from GeoNames
Gazetteers can be used in several ways. The simplest approach is post-processing: after a model makes predictions, override low-confidence predictions if the token matches a gazetteer entry with high reliability. A more principled approach uses gazetteer membership as an additional input feature, either by appending an embedding derived from the gazetteer lookup to the token representation, or by adding a Boolean feature indicating gazetteer match.
# Simple gazetteer-augmented post-processing
gazetteers = {
"ORG": {
"Apple",
"Google",
"Microsoft",
"Amazon",
"Meta",
"Tesla",
"OpenAI",
},
"PER": {"Biden", "Obama", "Trump", "Musk", "Zuckerberg", "Cook", "Altman"},
"LOC": {
"Paris",
"London",
"Berlin",
"Tokyo",
"Beijing",
"Washington",
"California",
},
"GPE": {"USA", "UK", "Germany", "France", "China", "Japan", "India"},
}
# Reverse lookup: word -> most likely type
word_to_type = {}
for entity_type, words in gazetteers.items():
for word in words:
word_to_type[word] = entity_type
def apply_gazetteer_override(
predictions, word_to_type_map, confidence_threshold=0.8
):
"""
Override low-confidence predictions with gazetteer knowledge.
Only overrides if the model's confidence is below threshold AND
the word appears in the gazetteer.
"""
corrected = []
for pred in predictions:
word = pred["word"].strip()
if pred["score"] < confidence_threshold and word in word_to_type_map:
original_type = pred["entity_group"]
corrected_type = word_to_type_map[word]
corrected.append(
{
**pred,
"entity_group": corrected_type,
"overridden": True,
"original_type": original_type,
}
)
else:
corrected.append({**pred, "overridden": False})
return corrected
# Test on a sentence with potentially ambiguous entity
test_text_gaz = (
"Cook announced that Apple would partner with OpenAI in Cupertino."
)
raw_preds = ner_pipeline(test_text_gaz)
corrected_preds = apply_gazetteer_override(raw_preds, word_to_type)Gazetteer-augmented predictions: Entity Type Score Status ------------------------------------------------------- Cook PER 0.997 model prediction Apple ORG 0.999 model prediction OpenAI ORG 0.995 model prediction Cupertino LOC 0.998 model prediction
Gazetteers are most valuable when the entity vocabulary is well-defined and stable, and when the model encounters terms that are underrepresented in its pre-training data. For general-purpose NER on standard newswire text, gazetteers add little beyond what a well-trained transformer already knows. In specialized domains like pharmaceutical NER, where every approved drug name must be correctly extracted, gazetteer augmentation is often essential.
NER Implementation with spaCy
Let's walk through a complete NER workflow using spaCy. This shows entity extraction, visualization, and custom entity addition.
Loading and Running NER
import spacy
nlp = spacy.load("en_core_web_sm")
news_article = (
"The European Central Bank, led by Christine Lagarde, raised interest rates "
"by 25 basis points on Thursday. The euro gained 0.8% against the U.S. dollar "
"following the announcement in Frankfurt."
)
doc = nlp(news_article)Entity Span Label Description --------------------------------------------------------------------------- The European Central Bank ORG Companies, agencies, institutions, etc. Christine Lagarde PERSON People, including fictional 25 CARDINAL Numerals that do not fall under another type Thursday DATE Absolute or relative dates or periods 0.8% PERCENT Percentage, including "%" U.S. GPE Countries, cities, states Frankfurt GPE Countries, cities, states
The spacy.explain() function returns a human-readable description of each label code, which is useful when working with unfamiliar tagsets. Notice that "Thursday" is labeled ORDINAL here, which is a common source of confusion: spaCy's OntoNotes-based model has no explicit DATE label for standalone day names in some contexts.
Adding Custom Entity Rules
spaCy's EntityRuler allows you to add rule-based entity detection on top of the statistical model. This is useful for entities that are reliably identifiable by their form (product codes, medical identifiers, stock tickers) but may be missed or mislabeled by the statistical model.
# Create a new pipeline with an EntityRuler added before the NER component
nlp_custom = spacy.load("en_core_web_sm")
# Add EntityRuler before ner to override statistical predictions
ruler = nlp_custom.add_pipe("entity_ruler", before="ner")
# Define patterns: each pattern maps a sequence of token attributes to a label
patterns = [
{
"label": "TICKER",
"pattern": [
{"TEXT": {"REGEX": "^[A-Z]{1,5}$"}},
{"TEXT": ":"},
{"TEXT": {"REGEX": "^[A-Z]{1,5}$"}},
],
},
{"label": "DRUG", "pattern": [{"LOWER": "metformin"}]},
{"label": "DRUG", "pattern": [{"LOWER": "atorvastatin"}]},
{"label": "ORG", "pattern": [{"TEXT": "OpenAI"}]},
{"label": "ORG", "pattern": [{"TEXT": "DeepMind"}]},
]
ruler.add_patterns(patterns)
test_custom = (
"The patient was prescribed metformin and atorvastatin. "
"Meanwhile, AAPL:US stock rose after OpenAI announced a partnership with DeepMind."
)
doc_custom = nlp_custom(test_custom)Custom NER results: Entity Span Label ---------------------------------------- metformin DRUG atorvastatin DRUG AAPL:US TICKER OpenAI ORG DeepMind ORG
The EntityRuler correctly captures the drug names and the compound ticker pattern, which the base statistical model would likely miss or mislabel. The before="ner" placement means the ruler runs first, and its matches take precedence over the statistical model's predictions for those spans.
Computing Entity-Level Metrics
Now let's use our metric function from earlier to evaluate predictions against a manually annotated gold standard:
# Simulated gold annotations for a small test set
gold_annotations_spacy = [
# Sentence 1: "Apple CEO Tim Cook visited Berlin."
[(0, 1, "ORG"), (2, 4, "PER"), (5, 6, "GPE")],
# Sentence 2: "Google and Amazon invested $500 million in AI."
[(0, 1, "ORG"), (2, 3, "ORG"), (4, 6, "MONEY")],
# Sentence 3: "The WHO published new guidelines in January 2025."
[(1, 2, "ORG"), (5, 7, "DATE")],
]
# Simulated system predictions (with some errors)
pred_annotations_spacy = [
# Sentence 1: correct on PER and GPE, misses ORG
[(2, 4, "PER"), (5, 6, "GPE")],
# Sentence 2: correct on ORG spans, boundary error on MONEY
[(0, 1, "ORG"), (2, 3, "ORG"), (4, 5, "MONEY")],
# Sentence 3: correct on both
[(1, 2, "ORG"), (5, 7, "DATE")],
]
# Aggregate across sentences
all_gold_spacy = [span for sent in gold_annotations_spacy for span in sent]
all_pred_spacy = [span for sent in pred_annotations_spacy for span in sent]
overall_spacy = ner_metrics(all_gold_spacy, all_pred_spacy)Aggregate NER Evaluation (3 sentences) --------------------------------------------- Gold spans: 8 Predicted spans: 7 True positives: 6 False positives: 1 False negatives: 1 Precision: 0.857 Recall: 0.857 F1: 0.857
Error Analysis and Debugging NER Systems
Understanding why a NER system fails is often more valuable than improving the overall F1 score. Systematic error analysis identifies the specific failure modes present in a deployment context, which guides the most effective improvements.
Common Error Patterns
Out-of-vocabulary entities. Any entity name that was absent from the training corpus will be harder to identify. Neural models mitigate this through subword tokenization and contextual representations, but an entity with an entirely novel name and no contextual cues (appearing in a novel context like "Join us for the XTC9000 launch event") will still challenge any model.
Ambiguous surface forms. Many strings that look like entity names are not entities in context. "Amazon" is most often a company, but in an article about rainforest ecology it is a river. "Washington" can be a city, a state, a person, or a university. The model must use context to disambiguate, and when context is uninformative the model must rely on priors learned from training data.
Entity type confusion in rare categories. MISC is the most confused category precisely because it is defined negatively (things that are not PER, ORG, or LOC). Nationality adjectives, events, works of art, and languages all fall into MISC, and the model must learn a heterogeneous set of surface patterns for a single label.
Long entity spans. Entities with many tokens are more prone to boundary errors simply because each additional token is an opportunity for an incorrect prediction. "The International Monetary Fund" has five tokens; each must be correctly tagged for the entity to be counted as correct.
Diagnostic Visualization

The confusion matrix reveals that MISC is the hardest type both to identify (18% missed) and to classify correctly (confusion with ORG and LOC). This pattern is typical of real NER evaluations. When improving a system, the most productive interventions for MISC accuracy are adding more training examples for MISC types and potentially splitting the MISC category into more specific subtypes (nationalities, events, works of art) if the downstream application can benefit from finer granularity.
Error Attribution
When debugging NER errors in production, it is useful to distinguish between three root causes:
The first cause is representation failure: the model has never seen anything similar to this entity in training, so its representation is uninformative. The remedy is to add similar examples to the training data or to use a larger pre-trained model with broader coverage.
The second cause is context failure: the model sees a token that could be an entity but the surrounding context is insufficient to determine the type. For example, "Smith" in isolation could be a person name, a place name, or a common noun. The remedy is to ensure training data includes diverse contextual patterns for each entity type.
The third cause is label noise: the gold annotation itself is wrong or inconsistent. If "Smith" is annotated as PER in 90% of training examples but O in 10% (because annotators disagreed about whether to tag common surnames without first names), the model learns an inconsistent signal. The remedy is to audit training data quality and resolve annotation disagreements systematically.
Limitations and Practical Considerations
NER systems are useful, but they come with important limitations that practitioners must understand before deploying them.
Domain shift is the dominant failure mode. A model trained on Reuters newswire will perform well on similar financial and political news, but will degrade substantially on social media posts, clinical notes, legal contracts, or scientific papers. The vocabulary of entity mentions, the syntactic patterns surrounding them, and even the entity type taxonomy may all differ. Before deploying a general-purpose NER model in a specialized domain, you should evaluate it on in-domain annotated examples. The gap between out-of-domain and in-domain performance is often 15-25 F1 points.
Rare and emerging entities are systematically missed. NER models, even transformer-based ones, rely heavily on the memorized associations between surface forms and entity types learned during pretraining. A new company that did not exist during pretraining, a recently coined term, or a person with an unusual name will be missed or mislabeled more often than familiar entities. This is particularly problematic for news applications where the most newsworthy entities are often the least familiar ones.
Annotation inconsistencies propagate to system outputs. Many NER benchmarks contain annotation disagreements, especially for borderline cases (is "the Internet" a MISC entity or not?). Systems trained on inconsistently annotated data learn the pattern of inconsistency, not a principled definition of entity types. When two annotators would disagree on a case, the model's prediction on that case is essentially random.
Evaluation scores can be misleading. Exact match F1 on CoNLL 2003 is a useful benchmark for comparing research systems, but it correlates imperfectly with downstream task performance. A question-answering system that uses NER as a preprocessing step may tolerate boundary errors well but be sensitive to type errors. Reporting system performance using the CoNLL F1 benchmark without also evaluating on the target domain is a common source of overoptimism.
Computational cost of large models. While transformer-based NER achieves the best accuracy, it requires substantially more computation than CRF-based systems. A BERT-base NER model processes roughly 50-100 sentences per second on a modern CPU, while a CRF can process thousands. For high-throughput applications (processing millions of documents daily), the computational cost of transformer-based NER is a real constraint, and practitioners often use lighter models (DistilBERT, smaller spaCy models) with acceptable accuracy tradeoffs.
Despite these limitations, NER has proven enormously useful in practice. It enables structured information extraction at scale: monitoring news for mentions of specific companies, extracting clinical concepts from medical records, building knowledge graphs from text corpora, and improving search engine query understanding. Transformer-based NER models are accurate enough for many production applications, and the field continues to develop better approaches to few-shot learning, cross-domain generalization, and nested entity recognition.
Summary
Named Entity Recognition identifies and classifies real-world entity mentions in text. The key concepts from this chapter are:
- Entity types are organized into taxonomies. Core categories (PER, ORG, LOC) appear in every NER system; extended and domain-specific categories are tailored to application needs. The choice of taxonomy reflects assumptions about what downstream consumers of the extracted data need.
- NER is framed as sequence labeling using BIO tagging: each token receives a label encoding both entity type and position within a multi-token span. Any sequence labeling model can be applied to NER through this encoding.
- Boundary detection is hard. Prepositional attachment, titles, possessives, coordination, and abbreviations all create ambiguity about where entity spans begin and end. Annotation guideline inconsistencies compound this difficulty by introducing label noise into training data.
- Nested entities (an entity span containing another entity) cannot be represented in standard BIO tagging. Span-based models or multi-layer annotation schemes are needed for applications that require nested entity extraction.
- NER evolved through three paradigm shifts: rule-based systems using gazetteers and hand-written rules (1980s-1990s), statistical CRF systems using hand-engineered features (1990s-2010s), and neural systems using BiLSTM-CRF and then transformer-based architectures (2015-present). Each shift improved F1 by several points, with the shift to transformers being the most impactful.
- Evaluation uses span-level precision, recall, and F1. Exact match evaluation requires both correct boundaries and correct type; partial match relaxes the boundary requirement. The choice of evaluation scheme can change reported F1 by 5-15 points.
- CoNLL 2003 and OntoNotes 5.0 are the dominant English NER benchmarks. Performance has improved from roughly 82 F1 (CRF) to above 94 F1 (RoBERTa) over two decades.
- Multilingual and cross-lingual NER, enabled by multilingual pre-trained models like XLM-RoBERTa, allows zero-shot transfer to low-resource languages. Agglutinative morphology, case-marked languages, and scriptural variation present additional challenges.
- Domain shift, rare entities, and annotation inconsistencies are the main practical failure modes. Evaluating on in-domain data before deployment is essential. Error analysis should distinguish representation failure, context failure, and label noise as distinct root causes.
The next chapter examines BIO tagging in detail, covering the full family of tagging schemes (BIO, BIOES, BILOU), how to convert between span and tag representations, and how to handle tagging inconsistencies in practice.
Quiz
Ready to test your understanding? Take this quick quiz to reinforce what you've learned about Named Entity Recognition.
Named Entity Recognition Quiz
Reference
Citation details
Cite or share this article.
Continue with the full handbook
This chapter is part of Language AI Handbook. Use the handbook page to browse the complete table of contents and continue reading in sequence.
Explore Language AI HandbookStay up to date
Get articles, book updates, and news delivered to your inbox.
No spam, unsubscribe anytime.
Join the community
Sign in to remove popups, track your reading progress, and join the discussion.

Comments
1 comment
This is a really useful article on NER. I expected some material on ensemble learning in NER.