Conditional Random Fields › CRF for Named Entity Recognition
Language AI HandbookAll chapters Conditional Random Fields › CRF for Named Entity RecognitionOne section · Sections in this chapter7 / 13 ↓
Reading tools Definitions & inline links
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
Let's build a more complete NER system using a larger dataset. We'll use the CoNLL-2003 format, a standard for NER evaluation.
from nltk.corpus import conll2002
# Download the corpus if needed (run once locally, then comment out)
# nltk.download("conll2002", quiet=True)
# Load Spanish NER data (smaller than English, good for demonstration)
train_sents = list(conll2002.iob_sents("esp.train"))[
:500
] # Use subset for speed
test_sents = list(conll2002.iob_sents("esp.testa"))[:100]CoNLL-2002 Spanish NER Dataset: ================================================== Training sentences: 500 Test sentences: 100 Example sentence: -------------------------------------------------- Izquierda NC B-ORG Unida AQ I-ORG de SP I-ORG Santander NC I-ORG presentó VMI O hoy RG O su DP O nuevo AQ O ... (38 tokens total)
def word_to_ner_features(sent, i):
"""
Extract comprehensive features for NER.
sent: list of (word, pos, ner) tuples
i: position
"""
word = sent[i][0]
pos = sent[i][1]
features = {
"bias": 1.0,
"word.lower": word.lower(),
"word[-3:]": word[-3:],
"word[-2:]": word[-2:],
"word[:3]": word[:3],
"word[:2]": word[:2],
"word.isupper": word.isupper(),
"word.istitle": word.istitle(),
"word.isdigit": word.isdigit(),
"word.length": len(word),
"postag": pos,
"postag[:2]": pos[:2],
}
# Previous word features
if i > 0:
word_prev = sent[i - 1][0]
pos_prev = sent[i - 1][1]
features.update(
{
"-1:word.lower": word_prev.lower(),
"-1:word.istitle": word_prev.istitle(),
"-1:word.isupper": word_prev.isupper(),
"-1:postag": pos_prev,
"-1:postag[:2]": pos_prev[:2],
}
)
else:
features["BOS"] = True
# Next word features
if i < len(sent) - 1:
word_next = sent[i + 1][0]
pos_next = sent[i + 1][1]
features.update(
{
"+1:word.lower": word_next.lower(),
"+1:word.istitle": word_next.istitle(),
"+1:word.isupper": word_next.isupper(),
"+1:postag": pos_next,
"+1:postag[:2]": pos_next[:2],
}
)
else:
features["EOS"] = True
return features
def sent_to_ner_features(sent):
return [word_to_ner_features(sent, i) for i in range(len(sent))]
def sent_to_ner_labels(sent):
return [ner for word, pos, ner in sent]
# Prepare data
X_train_ner = [sent_to_ner_features(s) for s in train_sents]
y_train_ner = [sent_to_ner_labels(s) for s in train_sents]
X_test_ner = [sent_to_ner_features(s) for s in test_sents]
y_test_ner = [sent_to_ner_labels(s) for s in test_sents]
# Train CRF
crf_ner = sklearn_crfsuite.CRF(
algorithm="lbfgs",
c1=0.1,
c2=0.1,
max_iterations=100,
all_possible_transitions=True,
)
crf_ner.fit(X_train_ner, y_train_ner)
# Predict
y_pred_ner = crf_ner.predict(X_test_ner)
# Get labels (excluding O for focused evaluation)
labels = list(crf_ner.classes_)
labels.remove("O")NER Performance on Test Set:
============================================================
Overall F1 (weighted): 0.5292
Per-entity metrics:
------------------------------------------------------------
precision recall f1-score support
B-LOC 0.675 0.500 0.574 54
B-ORG 0.625 0.738 0.677 61
B-PER 0.460 0.657 0.541 35
I-PER 0.519 0.667 0.583 42
B-MISC 0.667 0.200 0.308 20
I-ORG 0.527 0.617 0.569 47
I-LOC 0.333 0.207 0.255 29
I-MISC 0.833 0.286 0.426 35
micro avg 0.560 0.533 0.546 323
macro avg 0.580 0.484 0.492 323
weighted avg 0.586 0.533 0.529 323The per-entity metrics show how well the CRF recognizes each entity type. Precision measures how many predicted entities are correct, while recall measures how many actual entities were found. The F1 score balances both. Performance varies by entity type, with some entities being easier to recognize due to more distinctive features or more training examples.
Sample Predictions: ============================================================ Sentence 1: Sao gold: B-LOC pred: B-LOC ✓ Paulo gold: I-LOC pred: I-LOC ✓ Brasil gold: B-LOC pred: B-ORG ✗ EFECOM gold: B-ORG pred: B-ORG ✓ ... (12 tokens) Sentence 2: Sentence 3: Telefónica gold: B-ORG pred: B-ORG ✓ ... (51 tokens)
The sample predictions show the CRF in action on real text. Checkmarks indicate correct predictions, while crosses highlight errors. Common error patterns include confusing similar entity types or missing entity boundaries, especially for entities not well-represented in training data.