前置知识: 自然语言处理

命名实体识别

00:00
11 min Intermediate

理解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-GPEYork I-GPECity 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") 返回 xXxxxxword_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)

c1c2 是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

练习

  1. 简单 实现 bio_to_spansspans_to_bio的逆)并在10个句子验证往返一致性
  2. 中等。 在CoNLL-2003英语NER数据集上训练sklearn-crfsuite CRF。使用 seqeval 报告每实体F1。典型结果:约84 F1。
  3. 困难。领域特定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高估准确

延伸阅读

知识检测

学习进度

-- 已学文档
--% 知识覆盖率

学习推荐

专注模式