命名实体识别
理解BIO标注、CRF、BiLSTM-CRF和Transformer在NER中的完整演进
命名实体识别
把名字提取出来。听起来简单,直到你面对歧义边界、嵌套实体和领域行话。
类型: 构建 语言: Python 前置条件: Phase 5 · 02(BoW + TF-IDF),Phase 5 · 03(词嵌入) 时间: ~75 分钟
问题
“Apple sued Google over its iPhone search deal in the US.” 五个实体:Apple (ORG)、Google (ORG)、iPhone (PRODUCT)、search deal (可能)、US (GPE)。好的NER系统提取所有实体并给出正确类型。差的漏掉iPhone,把水果Apple和公司Apple混淆,把”US”标记为PERSON。
NER是每个结构化提取流水线下的工作马。简历解析、合规日志扫描、医疗记录匿名化、搜索查询理解、聊天机器人响应接地、法律合同提取。你从不太看到它;你总是依赖它。
本课程从经典路径(基于规则、HMM、CRF)走到现代路径(BiLSTM-CRF,然后Transformer)。每一步解决前一步的特定限制。模式就是课程。
概念
BIO标注(或BILOU)将实体提取转化为序列标注问题。用 B-TYPE(实体开始)、I-TYPE(实体内部)或 O(不在任何实体内)标记每个token。
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
多token实体链式标注:New B-GPE、York I-GPE、City I-GPE。理解BIO的模型可以提取任意跨度。
架构演进:
- 基于规则。 正则 + 地名词典查找。已知实体高精度,新实体零覆盖。
- HMM。 隐马尔可夫模型。给定标签的token发射概率,标签间转移概率。Viterbi解码。在标注数据上训练。
- CRF。 条件随机场。像HMM但判别式,所以你可以混合任意特征(词形、大写、邻近词)。2026年低资源部署仍是的经典生产工作马。
- BiLSTM-CRF。 神经特征代替手工特征。LSTM双向读取句子,CRF层在顶部强制一致的标签序列。
- 基于Transformer。 用token分类头微调BERT。最佳准确率。最多计算。
构建它
步骤 1: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')]
步骤 2:手工特征
对于经典(非神经)NER,特征是关键。有用的特征:
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") 返回 xXxxxx。word_shape("USA-2024") 返回 XXX-dddd。大写模式对专有名词是高信号。
步骤 3:简单基于规则+字典的基线
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
生产地名词典有数百万条目,从Wikipedia和DBpedia抓取。覆盖良好。消歧(公司Apple vs 水果Apple)很糟。这就是统计模型获胜的原因。
步骤 4:CRF步骤(概要,非完整实现)
50行从零实现完整CRF没有概率论基础不够清晰。使用 sklearn-crfsuite:
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 和 c2 是L1和L2正则化。all_possible_transitions=True 让模型学习非法序列(如 O 后的 I-ORG)不太可能,这就是CRF如何在不写约束的情况下强制BIO一致性。
步骤 5:BiLSTM-CRF添加了什么
特征变成学习到的。输入:token嵌入(GloVe或fastText)。LSTM从左到右和从右到左读取。拼接的隐藏状态通过CRF输出层。CRF仍然强制标签序列一致性;LSTM用学习到的特征替代手工特征。
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
对于CRF层,使用 torchcrf.CRF(pip install pytorch-crf)。相比手工CRF的提升可测量但比你预期的小,除非你有数万标注句子。
使用它
spaCy开箱即用提供生产级NER。
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
注意 iPhone 被标为 ORG 而非 PRODUCT — spaCy小模型的产品实体覆盖较弱。大模型(en_core_web_lg)更好。Transformer模型(en_core_web_trf)更好。
Hugging Face的BERT NER:
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" 将连续的B-X、I-X token合并为跨度。没有它,你得到token级标签需要自己合并。
基于LLM的NER(2026年选项)
零样本和少样本LLM NER现在在许多领域与微调模型竞争,在标注数据稀缺时显著更好。
- 零样本提示。 给LLM一个实体类型列表和示例schema。要求JSON输出。开箱即用;新领域准确率中等。
- ZeroTuneBio风格提示。 将任务分解为候选提取 → 含义解释 → 判断 → 复查。多阶段提示(非一次)在生物医学NER上大幅提升准确率。同样模式适用于法律、金融和科学领域。
- 带RAG的动态提示。 每次推理调用从小的标注种子集中检索最相似的标注样本;动态构建少样本提示。2026年基准中,这比静态提示提升GPT-4生物医学NER F1 11-12%。
- 按实体类型分解。 对于长文档,一次提取所有实体类型的单次调用随长度增加丢失召回率。每种实体类型运行一次提取。推理成本更高,准确率显著更高。这是临床笔记和法律合同的标准模式。
2026年生产建议:在收集训练数据之前,从LLM零样本基线开始。通常F1足够好,你永远不需要微调。
经典NER仍然赢在哪里
即使有LLM,经典NER在以下情况赢:
- 延迟预算低于50ms。
- 你有数千标注样本且需要98%+ F1。
- 领域有稳定本体,预训练CRF或BiLSTM迁移良好。
- 监管约束要求本地部署、非生成式模型。
哪里会出问题
- 域偏移。 在CoNLL上训练的NER在法律合同上表现比地名词典差。在你的领域上微调。
- 嵌套实体。 “Bank of America Tower” 同时是ORG和FACILITY。标准BIO不能表示重叠跨度。你需要嵌套NER(多遍或基于跨度的模型)。
- 长实体。 “United States Federal Deposit Insurance Corporation.” token级模型有时会拆分这个。使用
aggregation_strategy或后处理。 - 稀疏类型。 医疗NER标签如DRUG_BRAND、ADVERSE_EVENT、DOSE。通用模型毫无概念。Scispacy和BioBERT是那里的起点。
交付它
将结果保存为 outputs/skill-ner-picker.md。
练习
- 简单。 实现
bio_to_spans(spans_to_bio的逆)并在10个句子上验证往返一致性。 - 中等。 在CoNLL-2003英语NER数据集上训练sklearn-crfsuite CRF。使用
seqeval报告每实体F1。典型结果:约84 F1。 - 困难。 在领域特定NER数据集(医疗、法律或金融)上微调
distilbert-base-cased。与spaCy小模型比较。记录数据泄漏检查并写下让你惊讶的地方。
关键术语
| 术语 | 通俗说法 | 实际含义 |
|---|---|---|
| NER | 提取名字 | 用类型(PERSON、ORG、GPE、DATE等)标注token跨度。 |
| BIO | 标注方案 | B-X 开始,I-X 继续,O 外部。 |
| BILOU | 更好的BIO | 添加 L-X(最后)、U-X(单位)用于更清晰的边界。 |
| CRF | 结构化分类器 | 建模标签间转移,不仅仅是发射。强制有效序列。 |
| 嵌套NER | 重叠实体 | 一个跨度是另一个子跨度的不同实体。BIO不能表达。 |
| 实体级F1 | 正确的NER指标 | 预测跨度必须完全匹配真实跨度。token级F1高估准确率。 |
延伸阅读
- Lample et al. (2016). Neural Architectures for Named Entity Recognition — BiLSTM-CRF论文。经典。
- Devlin et al. (2018). BERT: Pre-training of Deep Bidirectional Transformers — 引入成为标准的token分类模式。
- spaCy语言特征 — 命名实体 —
Doc.ents和Span上每个属性的实用参考。 - seqeval — 正确的指标库。始终使用。