命名实体识别 本节摘要:把名字抠出来。听着容易,直到你碰上歧义边界、嵌套实体和领域行话。"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 是每个结构化抽取流水线底下的役马:简历解析、合规日志扫描、病历匿名、搜索查询理解、聊天机器人回答落地、法律合同抽取。你从来看不见它,却总依赖它。
本节摘要:把名字抠出来。听着容易,直到你碰上歧义边界、嵌套实体和领域行话。"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 的演进,每一步都在修上一步的具体局限——这条演进路径本身就是教训。
对应原课程:Phase 5 · Lesson 06 ·
named-entity-recognition(原英文phases/05-nlp-foundations-to-advanced/06-named-entity-recognition/docs/en.md)。前置依赖:第 02 节(词袋与 TF-IDF)、第 03 节(词嵌入)。
阅读完本节,你应当能够:
"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 是每个结构化抽取流水线底下的役马。简历解析、合规日志扫描、病历匿名、搜索查询理解、聊天机器人回答落地、法律合同抽取。你从来看不见它,却总依赖它。
BIO 标注(或 BILOU)把实体抽取变成序列标注问题。给每个 token 打标签:B-TYPE(实体开头)、I-TYPE(实体内部)、O(任何实体之外)。
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 的模型能抽任意长度的 span。
架构演进,每一步修上一步的局限:
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')]
古典(非神经)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。大小写模式对专有名词是高信号特征。
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
生产地名词典有数百万条目,从维基百科和 DBpedia 抓取。覆盖好,消歧(Apple 公司 vs 水果)糟透。这正是统计模型取胜的原因。
没有概率论基础,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 一致性的方式。
特征变成学出来的。输入是 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 合并成一个 span。不开它,你拿到 token 级标签,得自己合并。
零样本与少样本 LLM NER 如今在许多领域已可与微调模型竞争,标注数据稀缺时更是碾压。
💡 2026 年生产建议:在收集训练数据之前,先用 LLM 零样本打个基线。F1 往往好到你不需微调。
即便有 LLM,古典 NER 在以下情况仍赢:
aggregation_strategy 或后处理。保存为 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.
bio_to_spans(spans_to_bio 的逆),在 10 个句子上验证往返一致。seqeval 报每实体 F1。典型结果约 84 F1。distilbert-base-cased,对比 spaCy 小模型。记录数据泄漏检查,写下让你意外的发现。B-X 开头、I-X 内部、O 外,链起多 token 实体。xXxxxx)、大小写、邻近词、后缀。all_possible_transitions 学非法序列不太可能,无需手写 BIO 约束。aggregation_strategy 合并 span。下一节,我们从「抠实体」退一步看「标词性、画句法树」——进入「词性标注与句法分析」。