Phase 05 - Lesson 06
Reconhecimento de Entidades Nomeadas
This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.
Extraia os nomes. Parece fácil, até você ter que lidar com fronteiras ambíguas, entidades aninhadas e jargão de domínio.
Tipo: Build Linguagens: Python Pré-requisitos: Fase 5 · 02 (BoW + TF-IDF), Fase 5 · 03 (Word Embeddings) Tempo: ~75 minutos
O Problema
"Apple sued Google over its iPhone search deal in the US." Cinco entidades: Apple (ORG), Google (ORG), iPhone (PRODUCT), search deal (talvez), US (GPE). Um bom sistema de NER extrai todas elas com os tipos corretos. Um ruim erra o iPhone, confunde a Apple fruta com a Apple empresa e rotula "US" como PERSON.
NER é o burro de carga por trás de todo pipeline de extração estruturada. Análise de currículos, varredura de logs de compliance, anonimização de prontuários médicos, entendimento de consultas de busca, embasamento (grounding) de respostas de chatbots, extração de contratos jurídicos. Você quase nunca o vê; você sempre depende dele.
Esta lição percorre o caminho clássico (baseado em regras, HMM, CRF) até o moderno (BiLSTM-CRF e depois transformers). Cada passo resolve uma limitação específica do anterior. O padrão é a lição.
O Conceito
Tagging BIO (ou BILOU) transforma extração de entidades em um problema de rotulagem de sequências. Rotule cada token com B-TYPE (início da entidade), I-TYPE (dentro da entidade) ou O (fora de qualquer entidade).
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
Entidades de múltiplos tokens encadeiam: New B-GPE, York I-GPE, City I-GPE. Um modelo que entende BIO consegue extrair spans arbitrários.
A progressão de arquitetura:
- Baseado em regras. Regex + buscas em gazetteer. Alta precisão em entidades conhecidas, cobertura zero em novas.
- HMM. Modelo Oculto de Markov (Hidden Markov Model). Probabilidade de emissão do token dada a tag, probabilidade de transição de tag para tag. Decodificação por Viterbi. Treinado com dados rotulados.
- CRF. Campo Aleatório Condicional (Conditional Random Field). Como o HMM, mas discriminativo, então você pode misturar features arbitrárias (formato da palavra, capitalização, palavras vizinhas). Ainda é o burro de carga clássico de produção em 2026 para implantações de baixos recursos.
- BiLSTM-CRF. Features neurais em vez de manuais. A LSTM lê a frase nas duas direções, e a camada CRF no topo impõe sequências de tags consistentes.
- Baseado em transformers. Faça fine-tuning do BERT com uma cabeça de classificação de tokens. Melhor acurácia. Maior custo computacional.
Construa
Passo 1: helpers de tagging 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')]
Passo 2: features manuais
Para NER clássico (não neural), as features são o jogo. Algumas úteis:
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") retorna xXxxxx. word_shape("USA-2024") retorna XXX-dddd. Padrões de capitalização são de alto sinal para nomes próprios.
Passo 3: um baseline simples baseado em regras + dicionário
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
Gazetteers de produção têm milhões de entradas extraídas da Wikipedia e da DBpedia. A cobertura é boa. A desambiguação (Apple empresa vs fruta) é terrível. É por isso que os modelos estatísticos venceram.
Passo 4: o passo do CRF (esboço, não implementação completa)
Um CRF completo do zero em 50 linhas não é esclarecedor sem os fundamentos da teoria de probabilidade. Use sklearn-crfsuite no 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 e c2 são regularizações L1 e L2. all_possible_transitions=True permite que o modelo aprenda que sequências ilegais (por exemplo, I-ORG depois de O) são improváveis, que é como um CRF impõe a consistência BIO sem você escrever a restrição.
Passo 5: o que um BiLSTM-CRF adiciona
As features passam a ser aprendidas. Entradas: embeddings de tokens (GloVe ou fastText). A LSTM lê da esquerda para a direita e da direita para a esquerda. Os estados ocultos concatenados passam por uma camada de saída CRF. O CRF ainda impõe a consistência da sequência de tags; a LSTM substitui as features manuais por features 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 a camada CRF, use torchcrf.CRF (pip install pytorch-crf). O ganho sobre o CRF manual é mensurável, mas menor do que você espera, a menos que você tenha dezenas de milhares de frases rotuladas.
Use
O spaCy entrega NER de nível de produção pronto para uso.
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
Note que iPhone foi rotulado como ORG em vez de PRODUCT — o modelo pequeno do spaCy tem cobertura fraca de entidades de produto. O modelo grande (en_core_web_lg) se sai melhor. O modelo transformer (en_core_web_trf) se sai ainda melhor.
Hugging Face para NER baseado em 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" mescla tokens contíguos B-X, I-X em um span. Sem isso, você obtém rótulos no nível de token e tem que fazer a mesclagem você mesmo.
NER baseado em LLM (a opção de 2026)
NER com LLM zero-shot e few-shot agora é competitivo com modelos com fine-tuning em muitos domínios, e dramaticamente melhor quando há escassez de dados rotulados.
- Prompting zero-shot. Dê ao LLM uma lista de tipos de entidade e um esquema de exemplo. Peça saída em JSON. Funciona de imediato; a acurácia é moderada em domínios novos.
- Prompting no estilo ZeroTuneBio. Decomponha a tarefa em extração de candidatos → explicação de significado → julgamento → reverificação. Um prompt em múltiplos estágios (não one-shot) eleva a acurácia substancialmente em NER biomédico. O mesmo padrão funciona para os domínios jurídico, financeiro e científico.
- Prompting dinâmico com RAG. Recupere os exemplos rotulados mais similares de um pequeno conjunto-semente anotado para cada chamada de inferência; construa o prompt few-shot na hora. Em benchmarks de 2026, isso eleva o F1 do GPT-4 em NER biomédico em 11-12% sobre o prompting estático.
- Decomposição por tipo de entidade. Para documentos longos, uma única chamada que extrai todos os tipos de entidade de uma vez perde recall conforme o comprimento cresce. Execute uma passada de extração por tipo de entidade. Maior custo de inferência, acurácia substancialmente maior. Esse é o padrão padrão para notas clínicas e contratos jurídicos.
Recomendação de produção em 2026: comece com um baseline LLM zero-shot antes de coletar dados de treinamento. Muitas vezes o F1 é bom o suficiente para você nunca precisar fazer fine-tuning.
Onde o NER clássico ainda vence
Mesmo com LLMs disponíveis, o NER clássico vence quando:
- O orçamento de latência é inferior a 50ms.
- Você tem milhares de exemplos rotulados e precisa de 98%+ de F1.
- O domínio tem uma ontologia estável onde um CRF ou BiLSTM pré-treinado transfere bem.
- Restrições regulatórias exigem um modelo on-prem, não generativo.
Onde ele desmorona
- Mudança de domínio (domain shift). NER treinado em CoNLL aplicado a contratos jurídicos tem desempenho pior que um gazetteer. Faça fine-tuning no seu domínio.
- Entidades aninhadas. "Bank of America Tower" é simultaneamente uma ORG e uma FACILITY. O BIO padrão não consegue representar spans sobrepostos. Você precisa de NER aninhado (modelos multi-passada ou baseados em span).
- Entidades longas. "United States Federal Deposit Insurance Corporation." Modelos no nível de token às vezes dividem isso. Use
aggregation_strategyou pós-processe. - Tipos esparsos. Rótulos de NER médico como DRUG_BRAND, ADVERSE_EVENT, DOSE. Modelos de propósito geral não fazem ideia. Scispacy e BioBERT são os pontos de partida aí.
Entregue
Salve 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.
Exercícios
- Fácil. Implemente
bio_to_spans(o inverso despans_to_bio) e verifique a consistência de ida e volta em 10 frases. - Médio. Treine o CRF do sklearn-crfsuite acima no dataset de NER em inglês CoNLL-2003. Reporte o F1 por entidade usando
seqeval. Resultado típico: ~84 F1. - Difícil. Faça fine-tuning do
distilbert-base-casedem um dataset de NER específico de domínio (médico, jurídico ou financeiro). Compare com o modelo pequeno do spaCy. Documente as verificações de vazamento de dados (data leakage) e descreva o que te surpreendeu.
Termos-chave
| Termo | O que as pessoas dizem | O que realmente significa |
|---|---|---|
| NER | Extrair nomes | Rotular spans de tokens com tipos (PERSON, ORG, GPE, DATE, ...). |
| BIO | Esquema de tagging | B-X inicia, I-X continua, O fora. |
| BILOU | BIO melhorado | Adiciona L-X (último), U-X (unidade) para fronteiras mais limpas. |
| CRF | Classificador estruturado | Modela transições entre rótulos, não apenas emissões. Impõe sequências válidas. |
| Nested NER | Entidades sobrepostas | Um span é uma entidade diferente de um sub-span dele. O BIO não consegue expressar isso. |
| F1 no nível de entidade | Métrica correta de NER | O span previsto deve casar exatamente com o span verdadeiro. O F1 no nível de token superestima a acurácia. |
Leitura Adicional
- Lample et al. (2016). Neural Architectures for Named Entity Recognition — o artigo do BiLSTM-CRF. Canônico.
- Devlin et al. (2018). BERT: Pre-training of Deep Bidirectional Transformers — introduz o padrão de classificação de tokens que se tornou padrão.
- spaCy linguistic features — named entities — referência prática para cada atributo de
Doc.entseSpan. - seqeval — a biblioteca de métrica correta. Use-a sempre.