跳转到内容

11.4.5 NER 实战

NER 项目实体评估闭环图

  • 学会定义一个最小 NER 项目边界
  • 学会从 token 标签恢复实体
  • 学会做实体级别错误分析
  • 通过可运行示例建立信息抽取项目骨架

NER 实战更适合按“标签 -> 实体 -> 评估 -> 迭代”的顺序理解:

flowchart LR
A["定义实体类型"] --> B["设计 BIO 标签"]
B --> C["模型输出标签序列"]
C --> D["恢复实体 span"]
D --> E["做实体级评估和错例分析"]

所以这节真正想解决的是:

  • NER 项目为什么不只是“标签预测”
  • 为什么实体恢复和错误分析才更像真实项目

输入:

  • 一段简历或候选人简介文本

输出:

  • 姓名
  • 学校
  • 技能

为什么这比“随便抽点实体”更适合练手?

Section titled “为什么这比“随便抽点实体”更适合练手?”

因为它边界清楚:

  • 类别数不多
  • 实体类型明确
  • 结果很容易做业务解释

第一个关键点不是模型,而是标签体系

Section titled “第一个关键点不是模型,而是标签体系”

例如:

  • 张三 -> B-NAME
  • 清华大学 -> B-SCHOOL I-SCHOOL ...
  • Python -> B-SKILL

这一步一旦含糊,后面模型和评估都会一起乱。

你可以把 NER 想成:

  • 在一段文字里拿荧光笔圈重点信息

难点不只是“圈出来”,而是:

  • 从哪里开始圈
  • 到哪里结束
  • 这一段到底属于哪一类

这样理解后,为什么 NER 特别容易卡在边界问题,就会自然很多。


二、先做一个可运行标注与解码闭环

Section titled “二、先做一个可运行标注与解码闭环”

下面这个示例会做三件事:

  1. 准备一个小型样本
  2. 把 BIO 标签解码成实体
  3. 做简单的预测对比与错误分析
samples = [
{
"tokens": ["张三", "毕业于", "清华大学", "", "熟悉", "Python", "", "PyTorch"],
"gold_tags": ["B-NAME", "O", "B-SCHOOL", "O", "O", "B-SKILL", "O", "B-SKILL"],
"pred_tags": ["B-NAME", "O", "B-SCHOOL", "O", "O", "B-SKILL", "O", "B-SKILL"],
},
{
"tokens": ["李四", "来自", "北京大学", "", "掌握", "Java"],
"gold_tags": ["B-NAME", "O", "B-SCHOOL", "O", "O", "B-SKILL"],
"pred_tags": ["B-NAME", "O", "O", "O", "O", "B-SKILL"],
},
]
def decode_entities(tokens, tags):
entities = []
current_tokens = []
current_type = None
for token, tag in zip(tokens, tags):
if tag == "O":
if current_tokens:
entities.append(("".join(current_tokens), current_type))
current_tokens = []
current_type = None
continue
prefix, entity_type = tag.split("-", 1)
if prefix == "B":
if current_tokens:
entities.append(("".join(current_tokens), current_type))
current_tokens = [token]
current_type = entity_type
elif prefix == "I" and current_type == entity_type:
current_tokens.append(token)
else:
if current_tokens:
entities.append(("".join(current_tokens), current_type))
current_tokens = [token]
current_type = entity_type
if current_tokens:
entities.append(("".join(current_tokens), current_type))
return entities
for sample in samples:
gold_entities = decode_entities(sample["tokens"], sample["gold_tags"])
pred_entities = decode_entities(sample["tokens"], sample["pred_tags"])
print("tokens:", sample["tokens"])
print("gold :", gold_entities)
print("pred :", pred_entities)
print("miss :", [x for x in gold_entities if x not in pred_entities])
print()

预期输出:

Terminal window
tokens: ['张三', '毕业于', '清华大学', ',', '熟悉', 'Python', '和', 'PyTorch']
gold : [('张三', 'NAME'), ('清华大学', 'SCHOOL'), ('Python', 'SKILL'), ('PyTorch', 'SKILL')]
pred : [('张三', 'NAME'), ('清华大学', 'SCHOOL'), ('Python', 'SKILL'), ('PyTorch', 'SKILL')]
miss : []
tokens: ['李四', '来自', '北京大学', ',', '掌握', 'Java']
gold : [('李四', 'NAME'), ('北京大学', 'SCHOOL'), ('Java', 'SKILL')]
pred : [('李四', 'NAME'), ('Java', 'SKILL')]
miss : [('北京大学', 'SCHOOL')]

NER gold pred miss 漏实体运行结果图

第二条样本漏掉了学校实体。这正说明 NER 项目不能只看 token 标签,而要看最终恢复出来的实体。

这段代码为什么是“项目最小闭环”?

Section titled “这段代码为什么是“项目最小闭环”?”

因为它已经包含了:

  • 数据表示
  • 预测结果
  • 实体恢复
  • 错误分析

这比只打印一串标签更接近真实项目形态。

为什么这里按实体比较,而不是只按 token 比较?

Section titled “为什么这里按实体比较,而不是只按 token 比较?”

因为业务真正关心的通常是:

  • 实体有没有抽出来
  • 类型对不对

而不是某一个 token 单点是否打对。

再看一个最小“实体日志”示例

Section titled “再看一个最小“实体日志”示例”
sample = {
"tokens": ["李四", "来自", "北京大学", "", "掌握", "Java"],
"gold_tags": ["B-NAME", "O", "B-SCHOOL", "O", "O", "B-SKILL"],
"pred_tags": ["B-NAME", "O", "O", "O", "O", "B-SKILL"],
}
def decode_entities(tokens, tags):
entities = []
current_tokens = []
current_type = None
for token, tag in zip(tokens, tags):
if tag == "O":
if current_tokens:
entities.append(("".join(current_tokens), current_type))
current_tokens = []
current_type = None
continue
prefix, entity_type = tag.split("-", 1)
if prefix == "B":
if current_tokens:
entities.append(("".join(current_tokens), current_type))
current_tokens = [token]
current_type = entity_type
elif prefix == "I" and current_type == entity_type:
current_tokens.append(token)
if current_tokens:
entities.append(("".join(current_tokens), current_type))
return entities
gold_entities = decode_entities(sample["tokens"], sample["gold_tags"])
pred_entities = decode_entities(sample["tokens"], sample["pred_tags"])
print(
{
"text": "".join(sample["tokens"]),
"gold_entities": gold_entities,
"pred_entities": pred_entities,
}
)

预期输出:

Terminal window
{'text': '李四来自北京大学,掌握Java', 'gold_entities': [('李四', 'NAME'), ('北京大学', 'SCHOOL'), ('Java', 'SKILL')], 'pred_entities': [('李四', 'NAME'), ('Java', 'SKILL')]}

这个日志特别适合初学者,因为它会把一个抽象的标签任务,变成更像真实项目的输出:

  • 原文本是什么
  • 正确实体有哪些
  • 系统到底漏了什么

三、NER 项目最该先看什么指标?

Section titled “三、NER 项目最该先看什么指标?”

这是最常见也最有意义的一组指标。

因为序列里往往很多都是:

  • O

只看 token accuracy 很容易显得“很高”, 但真正的实体抽取效果可能并不好。

samples = [
{
"tokens": ["张三", "毕业于", "清华大学", "", "熟悉", "Python", "", "PyTorch"],
"gold_tags": ["B-NAME", "O", "B-SCHOOL", "O", "O", "B-SKILL", "O", "B-SKILL"],
"pred_tags": ["B-NAME", "O", "B-SCHOOL", "O", "O", "B-SKILL", "O", "B-SKILL"],
},
{
"tokens": ["李四", "来自", "北京大学", "", "掌握", "Java"],
"gold_tags": ["B-NAME", "O", "B-SCHOOL", "O", "O", "B-SKILL"],
"pred_tags": ["B-NAME", "O", "O", "O", "O", "B-SKILL"],
},
]
def decode_entities(tokens, tags):
entities = []
current_tokens = []
current_type = None
for token, tag in zip(tokens, tags):
if tag == "O":
if current_tokens:
entities.append(("".join(current_tokens), current_type))
current_tokens = []
current_type = None
continue
prefix, entity_type = tag.split("-", 1)
if prefix == "B":
if current_tokens:
entities.append(("".join(current_tokens), current_type))
current_tokens = [token]
current_type = entity_type
elif prefix == "I" and current_type == entity_type:
current_tokens.append(token)
if current_tokens:
entities.append(("".join(current_tokens), current_type))
return entities
def entity_recall(gold_entities, pred_entities):
if not gold_entities:
return 1.0
hit = sum(entity in pred_entities for entity in gold_entities)
return hit / len(gold_entities)
for sample in samples:
gold_entities = decode_entities(sample["tokens"], sample["gold_tags"])
pred_entities = decode_entities(sample["tokens"], sample["pred_tags"])
print(entity_recall(gold_entities, pred_entities))

预期输出:

Terminal window
1.0
0.6666666666666666

第一条样本实体全中;第二条样本只抽出了 3 个实体中的 2 个,所以实体级 recall 会下降,即使很多 O 位置仍然是对的。

第一次做 NER 项目时,最稳的默认顺序

Section titled “第一次做 NER 项目时,最稳的默认顺序”

更稳的顺序通常是:

  1. 先把实体类型收窄
  2. 先把标签标准写清楚
  3. 先做实体恢复和实体级评估
  4. 再去换更强模型

这样会比一开始就急着上 BERT 更容易把项目做稳。


例如学校名只抽了一半。

例如把技能识别成学校。

例如样本 2 里把 北京大学 漏掉了。

因为 NER 的错误通常很具体, 非常适合逐条看、逐类修。

一个新人很值得先用的错误分桶方式

Section titled “一个新人很值得先用的错误分桶方式”

第一次做错例分析时,最值得先分的通常就是:

  1. 边界错
  2. 类型错
  3. 漏实体

这三类已经足够帮助你判断:

  • 是数据标注问题
  • 是模型表示问题
  • 还是后处理规则不够

五、真实项目下一步该怎么走?

Section titled “五、真实项目下一步该怎么走?”

尤其是:

  • 长实体
  • 稀有实体
  • 容易混淆类型

从规则 / 经典模型升级到更强模型

Section titled “从规则 / 经典模型升级到更强模型”

例如:

  • BiLSTM + CRF
  • BERT token classification

很多业务项目里, 合理的后处理规则能明显提升实体质量。

如果把它做成项目,最值得展示什么

Section titled “如果把它做成项目,最值得展示什么”

最值得展示的通常不是:

  • 一串标签预测结果

而是:

  1. 原始文本
  2. gold 实体
  3. 预测实体
  4. 漏抽和错抽案例
  5. 你下一步准备优先修哪类错误

这样别人会更容易感觉到:

  • 你做的是信息抽取项目
  • 不只是训练了一个序列标注模型

NER 更该看实体级效果。

误区二:一开始就想覆盖所有实体类型

Section titled “误区二:一开始就想覆盖所有实体类型”

更稳的做法通常是:

  • 先选 2~4 类核心实体做透

误区三:标签体系一开始不定清楚

Section titled “误区三:标签体系一开始不定清楚”

标签边界不清,数据和评估都会一起发散。


学完这一页,至少保留这张证据卡:

模式
实体类型、BIO 标签,或序列标注规则
预测
词级标签和提取的片段
指标
实体精确率/召回率/F1 和边界情况
失败检查
跨度边界、嵌套实体、未知词或标注不一致
期望产出
金标与预测 span 对照表,至少包含一个漏判

这节最重要的是建立一个实战习惯:

做 NER 项目时,先把实体类型、标签体系、实体恢复和实体级错误分析做扎实,再去追求更复杂模型。

这样你留下的会是一个真正可解释、可改进的信息抽取项目,而不是只会跑训练脚本的半成品。


  1. 给示例再加一个 ORGTITLE 实体类型,扩展样本。
  2. 想一想:为什么 NER 项目更适合看实体级指标,而不是 token accuracy?
  3. 如果系统经常把长学校名只抽一半,你会优先改数据、改模型,还是加后处理?为什么?
  4. 你会如何把这个简历抽取项目进一步扩成作品集展示?
项目交付参考与讲解
  1. 增加 ORGTITLE 时,先定义边界规则:组织名、职位名和周围修饰词到底算不算实体。
  2. NER 更适合用实体级指标,因为用户拿到的是抽出的实体,不是孤立 token 标签。
  3. 长学校名只抽出一半时,先检查标注一致性,再补样本或后处理;只有目标清楚后才考虑换模型。
  4. 作品集展示应包含标签 schema、样例、实体级指标、错误分桶、修复方案和小型前后对比日志。