Phase 05 - Lesson 06
Reconocimiento de Entidades Nombradas
This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.
Extrae los nombres. Suena fácil, hasta que tienes que lidiar con límites ambiguos, entidades anidadas y jerga de dominio.
Tipo: Build Lenguajes: Python Prerrequisitos: Fase 5 · 02 (BoW + TF-IDF), Fase 5 · 03 (Word Embeddings) Tiempo: ~75 minutos
El Problema
"Apple sued Google over its iPhone search deal in the US." Cinco entidades: Apple (ORG), Google (ORG), iPhone (PRODUCT), search deal (quizás), US (GPE). Un buen sistema de NER las extrae todas con los tipos correctos. Uno malo se pierde iPhone, confunde Apple la fruta con Apple la empresa y etiqueta "US" como PERSON.
NER es el caballo de batalla detrás de todo pipeline de extracción estructurada. Análisis de currículums, escaneo de logs de cumplimiento, anonimización de historiales médicos, comprensión de consultas de búsqueda, fundamentación (grounding) de respuestas de chatbots, extracción de contratos legales. Casi nunca lo ves; siempre dependes de él.
Esta lección recorre el camino clásico (basado en reglas, HMM, CRF) hacia el moderno (BiLSTM-CRF, y luego transformers). Cada paso resuelve una limitación específica del anterior. El patrón es la lección.
El Concepto
Etiquetado BIO (o BILOU) convierte la extracción de entidades en un problema de etiquetado de secuencias. Etiqueta cada token con B-TYPE (comienzo de entidad), I-TYPE (dentro de entidad) u O (fuera de cualquier entidad).
Apple B-ORG
sued O
Google B-ORG
over O
its O
iPhone B-PRODUCT
search O
deal O
in O
the O
US B-GPE
. O
Las entidades de múltiples tokens se encadenan: New B-GPE, York I-GPE, City I-GPE. Un modelo que entiende BIO puede extraer spans arbitrarios.
La progresión de arquitectura:
- Basado en reglas. Regex + búsquedas en gazetteer. Alta precisión en entidades conocidas, cobertura cero en las nuevas.
- HMM. Modelo Oculto de Markov (Hidden Markov Model). Probabilidad de emisión del token dada la etiqueta, probabilidad de transición de etiqueta a etiqueta. Decodificación con Viterbi. Entrenado con datos etiquetados.
- CRF. Campo Aleatorio Condicional (Conditional Random Field). Como el HMM pero discriminativo, así que puedes mezclar características arbitrarias (forma de la palabra, capitalización, palabras vecinas). Sigue siendo el caballo de batalla clásico de producción en 2026 para despliegues de bajos recursos.
- BiLSTM-CRF. Características neuronales en lugar de manuales. La LSTM lee la frase en ambas direcciones, y la capa CRF encima impone secuencias de etiquetas consistentes.
- Basado en transformers. Haz fine-tuning de BERT con una cabeza de clasificación de tokens. Mejor precisión. Mayor costo de cómputo.
Constrúyelo
Paso 1: helpers de etiquetado BIO
def spans_to_bio(tokens, spans):
labels = ["O"] * len(tokens)
for start, end, label in spans:
labels[start] = f"B-{label}"
for i in range(start + 1, end):
labels[i] = f"I-{label}"
return labels
def bio_to_spans(tokens, labels):
spans = []
current = None
for i, label in enumerate(labels):
if label.startswith("B-"):
if current:
spans.append(current)
current = (i, i + 1, label[2:])
elif label.startswith("I-") and current and current[2] == label[2:]:
current = (current[0], i + 1, current[2])
else:
if current:
spans.append(current)
current = None
if current:
spans.append(current)
return spans
>>> tokens = ["Apple", "sued", "Google", "over", "iPhone", "sales", "."]
>>> labels = ["B-ORG", "O", "B-ORG", "O", "B-PRODUCT", "O", "O"]
>>> bio_to_spans(tokens, labels)
[(0, 1, 'ORG'), (2, 3, 'ORG'), (4, 5, 'PRODUCT')]
Paso 2: características manuales
Para el NER clásico (no neuronal), las características son el juego. Algunas útiles:
def token_features(token, prev_token, next_token):
return {
"lower": token.lower(),
"is_upper": token.isupper(),
"is_title": token.istitle(),
"has_digit": any(c.isdigit() for c in token),
"suffix_3": token[-3:].lower(),
"shape": word_shape(token),
"prev_lower": prev_token.lower() if prev_token else "<BOS>",
"next_lower": next_token.lower() if next_token else "<EOS>",
}
def word_shape(word):
out = []
for c in word:
if c.isupper():
out.append("X")
elif c.islower():
out.append("x")
elif c.isdigit():
out.append("d")
else:
out.append(c)
return "".join(out)
word_shape("iPhone") devuelve xXxxxx. word_shape("USA-2024") devuelve XXX-dddd. Los patrones de capitalización son de alta señal para los nombres propios.
Paso 3: una baseline simple basada en reglas + diccionario
ORG_GAZETTEER = {"Apple", "Google", "Microsoft", "OpenAI", "Meta", "Amazon", "Netflix"}
GPE_GAZETTEER = {"US", "USA", "UK", "India", "Germany", "France"}
PRODUCT_GAZETTEER = {"iPhone", "Android", "Windows", "ChatGPT", "Claude"}
def rule_based_ner(tokens):
labels = []
for token in tokens:
if token in ORG_GAZETTEER:
labels.append("B-ORG")
elif token in GPE_GAZETTEER:
labels.append("B-GPE")
elif token in PRODUCT_GAZETTEER:
labels.append("B-PRODUCT")
else:
labels.append("O")
return labels
Los gazetteers de producción tienen millones de entradas extraídas de Wikipedia y DBpedia. La cobertura es buena. La desambiguación (Apple la empresa vs la fruta) es terrible. Por eso ganaron los modelos estadísticos.
Paso 4: el paso del CRF (esbozo, no implementación completa)
Un CRF completo desde cero en 50 líneas no es esclarecedor sin los fundamentos de la teoría de la probabilidad. Usa sklearn-crfsuite en su lugar:
import sklearn_crfsuite
def to_features(tokens):
out = []
for i, tok in enumerate(tokens):
prev = tokens[i - 1] if i > 0 else ""
nxt = tokens[i + 1] if i + 1 < len(tokens) else ""
out.append({
"word.lower()": tok.lower(),
"word.isupper()": tok.isupper(),
"word.istitle()": tok.istitle(),
"word.isdigit()": tok.isdigit(),
"word.suffix3": tok[-3:].lower(),
"word.shape": word_shape(tok),
"prev.word.lower()": prev.lower(),
"next.word.lower()": nxt.lower(),
"BOS": i == 0,
"EOS": i == len(tokens) - 1,
})
return out
crf = sklearn_crfsuite.CRF(algorithm="lbfgs", c1=0.1, c2=0.1, max_iterations=100, all_possible_transitions=True)
X_train = [to_features(s) for s in sentences_tokenized]
crf.fit(X_train, bio_labels_train)
c1 y c2 son la regularización L1 y L2. all_possible_transitions=True permite que el modelo aprenda que las secuencias ilegales (por ejemplo, I-ORG después de O) son improbables, que es como un CRF impone la consistencia BIO sin que tú escribas la restricción.
Paso 5: qué agrega un BiLSTM-CRF
Las características pasan a ser aprendidas. Entradas: embeddings de tokens (GloVe o fastText). La LSTM lee de izquierda a derecha y de derecha a izquierda. Los estados ocultos concatenados pasan por una capa de salida CRF. El CRF aún impone la consistencia de la secuencia de etiquetas; la LSTM reemplaza las características manuales por características aprendidas.
import torch
import torch.nn as nn
class BiLSTM_CRF_Head(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_dim, n_labels):
super().__init__()
self.embed = nn.Embedding(vocab_size, embed_dim)
self.lstm = nn.LSTM(embed_dim, hidden_dim, bidirectional=True, batch_first=True)
self.fc = nn.Linear(hidden_dim * 2, n_labels)
def forward(self, token_ids):
e = self.embed(token_ids)
h, _ = self.lstm(e)
emissions = self.fc(h)
return emissions
Para la capa CRF, usa torchcrf.CRF (pip install pytorch-crf). La ganancia sobre el CRF manual es medible, pero menor de lo que esperas a menos que tengas decenas de miles de frases etiquetadas.
Úsalo
spaCy entrega NER de nivel de producción listo para usar.
import spacy
nlp = spacy.load("en_core_web_sm")
doc = nlp("Apple sued Google over its iPhone search deal in the US.")
for ent in doc.ents:
print(f"{ent.text:20s} {ent.label_}")
Apple ORG
Google ORG
iPhone ORG
US GPE
Observa que iPhone fue etiquetado como ORG en lugar de PRODUCT — el modelo pequeño de spaCy tiene cobertura débil de entidades de producto. El modelo grande (en_core_web_lg) lo hace mejor. El modelo transformer (en_core_web_trf) lo hace aún mejor.
Hugging Face para NER basado en BERT:
from transformers import pipeline
ner = pipeline("ner", model="dslim/bert-base-NER", aggregation_strategy="simple")
print(ner("Apple sued Google over its iPhone in the US."))
[{'entity_group': 'ORG', 'word': 'Apple', ...},
{'entity_group': 'ORG', 'word': 'Google', ...},
{'entity_group': 'MISC', 'word': 'iPhone', ...},
{'entity_group': 'LOC', 'word': 'US', ...}]
aggregation_strategy="simple" fusiona tokens contiguos B-X, I-X en un span. Sin esto, obtienes etiquetas a nivel de token y tienes que hacer la fusión tú mismo.
NER basado en LLM (la opción de 2026)
El NER con LLM zero-shot y few-shot ahora es competitivo con los modelos con fine-tuning en muchos dominios, y dramáticamente mejor cuando los datos etiquetados son escasos.
- Prompting zero-shot. Dale al LLM una lista de tipos de entidad y un esquema de ejemplo. Pide salida en JSON. Funciona de inmediato; la precisión es moderada en dominios novedosos.
- Prompting al estilo ZeroTuneBio. Descompón la tarea en extracción de candidatos → explicación de significado → juicio → reverificación. Un prompt de múltiples etapas (no one-shot) eleva la precisión sustancialmente en NER biomédico. El mismo patrón funciona para los dominios legal, financiero y científico.
- Prompting dinámico con RAG. Recupera los ejemplos etiquetados más similares de un pequeño conjunto-semilla anotado para cada llamada de inferencia; construye el prompt few-shot al vuelo. En benchmarks de 2026, esto eleva el F1 de GPT-4 en NER biomédico en un 11-12% sobre el prompting estático.
- Descomposición por tipo de entidad. Para documentos largos, una sola llamada que extrae todos los tipos de entidad a la vez pierde recall a medida que crece la longitud. Ejecuta una pasada de extracción por tipo de entidad. Mayor costo de inferencia, precisión sustancialmente mayor. Este es el patrón estándar para notas clínicas y contratos legales.
Recomendación de producción en 2026: comienza con una baseline LLM zero-shot antes de recolectar datos de entrenamiento. A menudo el F1 es suficientemente bueno como para que nunca necesites hacer fine-tuning.
Dónde el NER clásico aún gana
Incluso con LLMs disponibles, el NER clásico gana cuando:
- El presupuesto de latencia es inferior a 50ms.
- Tienes miles de ejemplos etiquetados y necesitas 98%+ de F1.
- El dominio tiene una ontología estable donde un CRF o BiLSTM preentrenado transfiere bien.
- Las restricciones regulatorias exigen un modelo on-prem, no generativo.
Dónde se desmorona
- Cambio de dominio (domain shift). El NER entrenado en CoNLL aplicado a contratos legales rinde peor que un gazetteer. Haz fine-tuning en tu dominio.
- Entidades anidadas. "Bank of America Tower" es simultáneamente una ORG y una FACILITY. El BIO estándar no puede representar spans superpuestos. Necesitas NER anidado (modelos de múltiples pasadas o basados en spans).
- Entidades largas. "United States Federal Deposit Insurance Corporation." Los modelos a nivel de token a veces dividen esto. Usa
aggregation_strategyo posprocesa. - Tipos escasos. Etiquetas de NER médico como DRUG_BRAND, ADVERSE_EVENT, DOSE. Los modelos de propósito general no tienen idea. Scispacy y BioBERT son los puntos de partida ahí.
Entrégalo
Guarda como outputs/skill-ner-picker.md:
---
name: ner-picker
description: Pick the right NER approach for a given extraction task.
version: 1.0.0
phase: 5
lesson: 06
tags: [nlp, ner, extraction]
---
Given a task description (domain, label set, language, latency, data volume), output:
1. Approach. Rule-based + gazetteer, CRF, BiLSTM-CRF, or transformer fine-tune.
2. Starting model. Name it (spaCy model ID, Hugging Face checkpoint ID, or "custom, trained from scratch").
3. Labeling strategy. BIO, BILOU, or span-based. Justify in one sentence.
4. Evaluation. Use `seqeval`. Always report entity-level F1 (not token-level).
Refuse to recommend fine-tuning a transformer for under 500 labeled examples unless the user already has a pretrained domain model. Flag nested entities as needing span-based or multi-pass models. Require a gazetteer audit if the user mentions "production scale" and labels are unchanged from CoNLL-2003.
Ejercicios
- Fácil. Implementa
bio_to_spans(el inverso despans_to_bio) y verifica la consistencia de ida y vuelta en 10 frases. - Medio. Entrena el CRF de sklearn-crfsuite de arriba en el dataset de NER en inglés CoNLL-2003. Reporta el F1 por entidad usando
seqeval. Resultado típico: ~84 F1. - Difícil. Haz fine-tuning de
distilbert-base-caseden un dataset de NER específico de dominio (médico, legal o financiero). Compáralo con el modelo pequeño de spaCy. Documenta las verificaciones de fuga de datos (data leakage) y describe qué te sorprendió.
Términos Clave
| Término | Lo que dice la gente | Lo que realmente significa |
|---|---|---|
| NER | Extraer nombres | Etiquetar spans de tokens con tipos (PERSON, ORG, GPE, DATE, ...). |
| BIO | Esquema de etiquetado | B-X comienza, I-X continúa, O fuera. |
| BILOU | BIO mejorado | Agrega L-X (último), U-X (unidad) para límites más limpios. |
| CRF | Clasificador estructurado | Modela transiciones entre etiquetas, no solo emisiones. Impone secuencias válidas. |
| Nested NER | Entidades superpuestas | Un span es una entidad diferente de un sub-span suyo. El BIO no puede expresar esto. |
| F1 a nivel de entidad | Métrica correcta de NER | El span predicho debe coincidir exactamente con el span verdadero. El F1 a nivel de token sobreestima la precisión. |
Lectura Adicional
- Lample et al. (2016). Neural Architectures for Named Entity Recognition — el artículo del BiLSTM-CRF. Canónico.
- Devlin et al. (2018). BERT: Pre-training of Deep Bidirectional Transformers — introduce el patrón de clasificación de tokens que se volvió estándar.
- spaCy linguistic features — named entities — referencia práctica para cada atributo de
Doc.entsySpan. - seqeval — la biblioteca de métrica correcta. Úsala siempre.