Conditional Random Fields › CRF for Named Entity Recognition

Michael BrenndoerferUpdated May 6, 20253 min read
Language AI HandbookAll chapters
Part 6 of 67Sequence Labeling
Chapter 42Conditional Random Fields › CRF for Named Entity Recognition
Reading section 7 of 13 · CRF for Named Entity Recognition · 3 minSection 7 of 13 · 3 min

Book contents

Section 7 of 13 · 3 min read

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.

In[29]:
Code
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]
Out[30]:
Console
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)

Now let's define richer features for NER and train a CRF:

In[31]:
Code
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")
Out[32]:
Console
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       323

The 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.

Out[33]:
Console

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.