查看全课大纲(第 62 / 69 节)
- 1.NLP 的研究领域、应用与学习路径
- 2.自然语言的基本术语
- 3.信息论基础:信息熵与交叉熵
- 4.语言的进化与语义变迁
- 5.主观概率、客观概率与条件概率
- 6.贝叶斯原理与推理
- 7.随机变量:二项式、期望方差与联合概率
- 8.常见概率分布
- 9.辛普森悖论与贝叶斯解题实例
- 10.语言模型:语言概率与词袋模型
- 11.n-gram 语言模型(二元 / 三元 / N-Gram)
- 12.语言模型评价:交叉熵、困惑度与插值
- 13.朴素贝叶斯文本分类:垃圾邮件
- 14.拼写检查
- 15.中文分词:构词法、最大匹配、最优路径与 Jieba
- 16.词表示问题与语义相似度
- 17.TF-IDF 权重处理
- 18.One-Hot 表达与词表示的局限
- 19.Word2Vec 核心思想
- 20.Word2Vec 训练过程
- 21.Skip-gram 模型详解
- 22.词性标注任务与标注方法(规则 + N-Gram)
- 23.EM 算法与混合模型
- 24.高斯混合模型 GMM
- 25.从混合模型到隐马尔可夫模型
- 26.隐马尔可夫模型(HMM)推导与维特比解码
- 27.线性链条件随机场 CRF
- 28.贝叶斯网络与图模型
- 29.隐马尔可夫链与 HMM 推导
- 30.PLSA 主题模型
- 31.LSI 与 SVD 向量表达
- 32.LDA 与狄利克雷分布
- 33.主题模型实验复现(困惑度 / 分类 / 相似度 / 延展)
- 34.上下文无关文法(CFG)与句法分析
- 35.概率上下文无关文法 PCFG
- 36.统计机器翻译:词对齐与翻译模型
- 37.统计机器翻译中的对齐模型与参数估计
- 38.从统计机器翻译到神经机器翻译
- 39.神经元与反向传播速览(指向深度学习课)
- 40.CNN 原理及其在 NLP 中的应用
- 41.循环神经网络的基本原理与 Elman / Jordan 网络
- 42.LSTM:长短期记忆网络的核心思想与分步实现
- 43.Encoder-Decoder 框架
- 44.Seq2Seq 与早期注意力机制
- 45.注意力机制的背景与动机
- 46.注意力模型实现原理与匹配函数
- 47.记忆网络:组成与计算
- 48.注意力机制的应用与历史延展
- 49.Transformer 结构:Encoder-Decoder 栈与 Self-Attention
- 50.Multi-Head Attention、FFNN 与位置编码
- 51.层归一化、损失函数与 Universal Transformer 延展
- 52.预训练范式:从 ELMo 到 BERT
- 53.BERT 与掩码语言模型
- 54.BERT 下游任务微调与 GLUE 口径
- 55.GPT 与自回归语言建模
- 56.GPT-1 到 GPT-4 的演进
- 57.指令微调与 RLHF
- 58.分词器:BPE、WordPiece 与中文词表
- 59.上下文窗口与长文本处理
- 60.能力边界、成本与适用场景
- 61.文本分类实战:从朴素贝叶斯到逻辑回归
- 62.命名实体识别实战
- 63.文本生成与摘要实战
- 64.语义检索与向量库实战
- 65.RAG 知识库问答
- 66.NLP 评估指标:BLEU / ROUGE / BERTScore 与人工评估
- 67.幻觉与事实性问题
- 68.文本与知识结合(知识图谱压缩)
- 69.跨媒体信息检索与未来展望
命名实体识别实战
小象实战讲义 · 自然语言处理
命名实体识别(NER)要回答的问题是:「这句话里有哪些人名、地名、机构名,它们各自从第几个字到第几个字?」注意这句话里有两个要素——是什么(类型)和在哪里(边界)。而模型输出的是逐字的标签序列,评估时又要把标签还原成实体。这中间的每一次转换都可能把分数算歪。
本节不搭模型,只做一件更基础也更常被跳过的事:把 NER 的评估算对。我们用 4 句话、46 个字符的微例,亲手跑出三个数——token 级准确率、实体级严格 F1、实体级宽松 F1——看清它们为什么差、差在哪里,以及这个差距在真实项目里意味着什么。
💡 核心导读
- token 级准确率的迷惑性:它把”少标一个字”和”类型标错”这类致命错误,摊薄成一两个字符的错误,看上去仍然”八成对”。
- 实体级评估的两把尺子:严格匹配(类型+边界全同)与宽松匹配(类型同、区间有重叠)。同一份预测,两把尺子量出来的 F1 能差十几个百分点,而模型一次都没改。
- 从微例到项目:边界规范、标签体系、嵌套实体、偏移对齐、长尾实体——这五件事必须在标注启动前写进指南,否则后面所有分数都不可信。
一、先看 token 级准确率有多”好看”
1.1 微例设定:4 句话,故意埋两个错
我们准备 4 条中文句子,用 BIO 标注(B- 实体开始、I- 实体内部、O 非实体),三类实体:人名 PER、机构 ORG、地点 LOC。然后人工构造一份”看起来不错”的预测,只埋两个错:
- 句子 1 埋边界错:金标「北京大学」占第 5–8 字,预测只标到第 7 字,把「学」丢了。
- 句子 2 埋类型错:金标「苹果公司」是 ORG,预测标成 LOC;后半句的「加州库比蒂诺」完全正确。
其余两句保持全对,这样能看清错误是怎么被稀释的。
SENTENCES = [
"李明访问了北京大学。",
"苹果公司位于加州库比蒂诺。",
"张华和李雷在清华大学读书。",
"微软的总部在西雅图。",
]
GOLD = [
['B-PER', 'I-PER', 'O', 'O', 'O', 'B-ORG', 'I-ORG', 'I-ORG', 'I-ORG', 'O'],
['B-ORG', 'I-ORG', 'O', 'O', 'O', 'B-LOC', 'I-LOC', 'I-LOC', 'I-LOC', 'O'],
['B-PER', 'I-PER', 'O', 'B-PER', 'I-PER', 'O', 'O', 'B-ORG', 'I-ORG', 'I-ORG', 'O'],
['B-ORG', 'I-ORG', 'O', 'O', 'O', 'O', 'B-LOC', 'I-LOC', 'O'],
]
PRED = [
['B-PER', 'I-PER', 'O', 'O', 'O', 'B-ORG', 'I-ORG', 'I-ORG', 'O', 'O'],
['B-LOC', 'I-LOC', 'O', 'O', 'O', 'B-LOC', 'I-LOC', 'I-LOC', 'I-LOC', 'O'],
['B-PER', 'I-PER', 'O', 'B-PER', 'I-PER', 'O', 'O', 'B-ORG', 'I-ORG', 'I-ORG', 'O'],
['B-ORG', 'I-ORG', 'O', 'O', 'O', 'O', 'B-LOC', 'I-LOC', 'O'],
]
def bio_to_entities(labels):
"""把 BIO 标签序列还原成 [(类型, 起始下标, 结束下标), ...],下标含尾。"""
ents, cur = [], None
for i, label in enumerate(labels):
if label == 'O':
if cur is not None:
ents.append(cur)
cur = None
elif label.startswith('B-'):
if cur is not None:
ents.append(cur)
cur = [label[2:], i, i]
elif label.startswith('I-') and cur is not None and label[2:] == cur[0]:
cur[2] = i
if cur is not None:
ents.append(cur)
return [tuple(e) for e in ents]
total_chars = sum(len(s) for s in SENTENCES)
correct_chars = 0
for sent, gold, pred in zip(SENTENCES, GOLD, PRED):
for g, p in zip(gold, pred):
if g == p:
correct_chars += 1
print(f"总字符数: {total_chars}|标签完全一致的字符数: {correct_chars}")
print(f"Token 级准确率: {correct_chars / total_chars:.4f}")
gold_entities, pred_entities = [], []
for sent, gold, pred in zip(SENTENCES, GOLD, PRED):
ge, pe = bio_to_entities(gold), bio_to_entities(pred)
gold_entities += [(sent, ) + e for e in ge]
pred_entities += [(sent, ) + e for e in pe]
print(sent)
print(f" 金标: {ge}")
print(f" 预测: {pe}")
print(f"金标实体总数: {len(gold_entities)}|预测实体总数: {len(pred_entities)}")总字符数: 46|标签完全一致的字符数: 37
Token 级准确率: 0.8043
李明访问了北京大学。
金标: [('PER', 0, 1), ('ORG', 5, 8)]
预测: [('PER', 0, 1), ('ORG', 5, 7)]
苹果公司位于加州库比蒂诺。
金标: [('ORG', 0, 1), ('LOC', 5, 8)]
预测: [('LOC', 0, 1), ('LOC', 5, 8)]
张华和李雷在清华大学读书。
金标: [('PER', 0, 1), ('PER', 3, 4), ('ORG', 7, 9)]
预测: [('PER', 0, 1), ('PER', 3, 4), ('ORG', 7, 9)]
微软的总部在西雅图。
金标: [('ORG', 0, 1), ('LOC', 6, 7)]
预测: [('ORG', 0, 1), ('LOC', 6, 7)]
金标实体总数: 9|预测实体总数: 91.2 80% 这个数字,把两种致命错误都藏起来了
先看第一行:46 个字里 37 个标签完全一致,准确率 80.43%。如果只看这个数,很容易给出一句”还行,八成对”。
但往下逐句看,错误长这样:
- 句子 1:预测
('ORG', 5, 7),金标('ORG', 5, 8)。实体类型对了,边界短了一位。落在 token 上只错 1 个字,看起来”几乎对”;落在业务上,抽出来的是「北京大」——这是个根本不存在的地名,下游填进表格就是脏数据。 - 句子 2:预测
('LOC', 0, 1),金标('ORG', 0, 1)。边界完美,类型错了。落在 token 上错的是 2 个字符的前缀(B-LOC/I-LOCvsB-ORG/I-ORG),看起来只是”两个字母不同”;落在业务上,把公司当成了地名,知识图谱里这条边直接连错。 - 句子 3、4:全对,各 3 个实体、2 个实体全部命中。
也就是说:37 个正确字符里,有 8 个字符(句子 1 的正确部分 + 句子 2 后半句)是”陪着错误一起被算进分母的”,而真正出错的两个实体,分别在 2 个字和 1 个字的尺度上”轻微扣分”。这就是 token 级准确率的机制性缺陷:它按字符数投票,实体却是按”个”计价的。
二、换成实体级:两把尺子量同一份预测
2.1 严格匹配:类型与边界必须全同
实体级评估的第一步是把标签序列还原成实体集合,然后逐个比对。严格匹配的规则很干脆:类型相同、起点相同、终点相同,才算命中。这里有个坑必须提前处理——集合的键要带上句子编号,否则不同句子里的相同片段会被合并掉(本例中 ('ORG', 0, 1) 在句子 2 和句子 4 都出现,键里不带句号就会把 9 个实体压成 7 个)。
SENTENCES = [
"李明访问了北京大学。",
"苹果公司位于加州库比蒂诺。",
"张华和李雷在清华大学读书。",
"微软的总部在西雅图。",
]
GOLD = [
['B-PER', 'I-PER', 'O', 'O', 'O', 'B-ORG', 'I-ORG', 'I-ORG', 'I-ORG', 'O'],
['B-ORG', 'I-ORG', 'O', 'O', 'O', 'B-LOC', 'I-LOC', 'I-LOC', 'I-LOC', 'O'],
['B-PER', 'I-PER', 'O', 'B-PER', 'I-PER', 'O', 'O', 'B-ORG', 'I-ORG', 'I-ORG', 'O'],
['B-ORG', 'I-ORG', 'O', 'O', 'O', 'O', 'B-LOC', 'I-LOC', 'O'],
]
PRED = [
['B-PER', 'I-PER', 'O', 'O', 'O', 'B-ORG', 'I-ORG', 'I-ORG', 'O', 'O'],
['B-LOC', 'I-LOC', 'O', 'O', 'O', 'B-LOC', 'I-LOC', 'I-LOC', 'I-LOC', 'O'],
['B-PER', 'I-PER', 'O', 'B-PER', 'I-PER', 'O', 'O', 'B-ORG', 'I-ORG', 'I-ORG', 'O'],
['B-ORG', 'I-ORG', 'O', 'O', 'O', 'O', 'B-LOC', 'I-LOC', 'O'],
]
def bio_to_entities(labels):
"""把 BIO 标签序列还原成 [(类型, 起始下标, 结束下标), ...],下标含尾。"""
ents, cur = [], None
for i, label in enumerate(labels):
if label == 'O':
if cur is not None:
ents.append(cur)
cur = None
elif label.startswith('B-'):
if cur is not None:
ents.append(cur)
cur = [label[2:], i, i]
elif label.startswith('I-') and cur is not None and label[2:] == cur[0]:
cur[2] = i
if cur is not None:
ents.append(cur)
return [tuple(e) for e in ents]
gold_set, pred_set = set(), set()
for si, (gold, pred) in enumerate(zip(GOLD, PRED)):
gold_set |= {(si, t, s, e) for t, s, e in bio_to_entities(gold)}
pred_set |= {(si, t, s, e) for t, s, e in bio_to_entities(pred)}
tp = len(gold_set & pred_set)
prec = tp / len(pred_set)
rec = tp / len(gold_set)
f1 = 2 * prec * rec / (prec + rec)
print(f"金标实体 {len(gold_set)} 个|预测实体 {len(pred_set)} 个|严格命中 {tp} 个")
print(f"Micro: P = {prec:.4f}|R = {rec:.4f}|F1 = {f1:.4f}")
print("--- 按类型拆开 ---")
f1_sum = 0.0
for t in ('PER', 'ORG', 'LOC'):
g = {x for x in gold_set if x[1] == t}
p = {x for x in pred_set if x[1] == t}
hit = len(g & p)
tp_ = hit / len(p) if p else 0.0
tr_ = hit / len(g) if g else 0.0
tf1 = 2 * tp_ * tr_ / (tp_ + tr_) if tp_ + tr_ else 0.0
f1_sum += tf1
print(f"{t}: 命中{hit}|预测{len(p)}|金标{len(g)}|P={tp_:.4f}|R={tr_:.4f}|F1={tf1:.4f}")
print(f"Macro-F1(三类算术平均)= {f1_sum / 3:.4f}")金标实体 9 个|预测实体 9 个|严格命中 7 个
Micro: P = 0.7778|R = 0.7778|F1 = 0.7778
--- 按类型拆开 ---
PER: 命中3|预测3|金标3|P=1.0000|R=1.0000|F1=1.0000
ORG: 命中2|预测3|金标4|P=0.6667|R=0.5000|F1=0.5714
LOC: 命中2|预测3|金标2|P=0.6667|R=1.0000|F1=0.8000
Macro-F1(三类算术平均)= 0.7905读数三件事:
- 9 个实体错 2 个,Micro F1 掉到 0.7778。注意分母是 9 而不是 46——这就是”按个计价”的含义:同样是”两个错误”,在字符口径里被摊成 8/46,在实体口径里是 2/9,后者才贴近业务体感。
- ORG 是真正的短板:金标 4 个、预测 3 个、命中 2 个,召回只有 0.5000。它同时挨了两次打——句子 1 的边界错(「北京大学」)和句子 2 的类型错(「苹果公司」被标成 LOC,从 ORG 里跑掉了,同时给 LOC 添了一个假正例)。所以 LOC 那边 P 也是 0.6667,被这个”越界”的预测拉低。
- Macro 0.7905 略高于 Micro 0.7778,原因是 PER 全对(F1=1.0)被算术平均”扶”了一把。类别数和样本数不一致时,Macro 与 Micro 一定会分岔,报告里必须写明用的是哪一个。
2.2 宽松匹配:同一份预测,换个口径就涨 11 个点
现实中很多团队验收时并不要”类型边界全同”,而是”类型对、位置大致对就算”。这种宽松口径用区间重叠来判断:类型相同 + 两个区间有交集即算命中。
SENTENCES = [
"李明访问了北京大学。",
"苹果公司位于加州库比蒂诺。",
"张华和李雷在清华大学读书。",
"微软的总部在西雅图。",
]
GOLD = [
['B-PER', 'I-PER', 'O', 'O', 'O', 'B-ORG', 'I-ORG', 'I-ORG', 'I-ORG', 'O'],
['B-ORG', 'I-ORG', 'O', 'O', 'O', 'B-LOC', 'I-LOC', 'I-LOC', 'I-LOC', 'O'],
['B-PER', 'I-PER', 'O', 'B-PER', 'I-PER', 'O', 'O', 'B-ORG', 'I-ORG', 'I-ORG', 'O'],
['B-ORG', 'I-ORG', 'O', 'O', 'O', 'O', 'B-LOC', 'I-LOC', 'O'],
]
PRED = [
['B-PER', 'I-PER', 'O', 'O', 'O', 'B-ORG', 'I-ORG', 'I-ORG', 'O', 'O'],
['B-LOC', 'I-LOC', 'O', 'O', 'O', 'B-LOC', 'I-LOC', 'I-LOC', 'I-LOC', 'O'],
['B-PER', 'I-PER', 'O', 'B-PER', 'I-PER', 'O', 'O', 'B-ORG', 'I-ORG', 'I-ORG', 'O'],
['B-ORG', 'I-ORG', 'O', 'O', 'O', 'O', 'B-LOC', 'I-LOC', 'O'],
]
def bio_to_entities(labels):
"""把 BIO 标签序列还原成 [(类型, 起始下标, 结束下标), ...],下标含尾。"""
ents, cur = [], None
for i, label in enumerate(labels):
if label == 'O':
if cur is not None:
ents.append(cur)
cur = None
elif label.startswith('B-'):
if cur is not None:
ents.append(cur)
cur = [label[2:], i, i]
elif label.startswith('I-') and cur is not None and label[2:] == cur[0]:
cur[2] = i
if cur is not None:
ents.append(cur)
return [tuple(e) for e in ents]
gold_set, pred_set = set(), set()
for si, (gold, pred) in enumerate(zip(GOLD, PRED)):
gold_set |= {(si, t, s, e) for t, s, e in bio_to_entities(gold)}
pred_set |= {(si, t, s, e) for t, s, e in bio_to_entities(pred)}
def matches(g, p, mode):
if g[0] != p[0] or g[1] != p[1]:
return False
if mode == 'strict':
return g[2] == p[2] and g[3] == p[3]
# loose: 同类型且区间有交集
return not (g[3] < p[2] or p[3] < g[2])
for mode, label in (('strict', '严格匹配(类型+边界全同)'), ('loose', '宽松匹配(类型同+区间有重叠)')):
hit = sum(1 for g in gold_set if any(matches(g, p, mode) for p in pred_set))
p_ = hit / len(pred_set)
r_ = hit / len(gold_set)
f_ = 2 * p_ * r_ / (p_ + r_) if p_ + r_ else 0.0
print(f"{label}: 命中 {hit} 个|P = {p_:.4f}|R = {r_:.4f}|F1 = {f_:.4f}")严格匹配(类型+边界全同): 命中 7 个|P = 0.7778|R = 0.7778|F1 = 0.7778
宽松匹配(类型同+区间有重叠): 命中 8 个|P = 0.8889|R = 0.8889|F1 = 0.8889这两个数放在一起,才是本节最该记住的一课:从 0.7778 到 0.8889,涨了 11.11 个百分点,而模型一个字都没改,改的只是评估口径。
具体是哪两处变化:
- 句子 1 的「北京大」与「北京大学」有交集,宽松口径判命中;严格口径判错。
- 句子 2 的「苹果公司」类型是 LOC,宽松口径要求类型相同,依然判错,这一处没有变化。
所以这 11 个点里,只有一个是”边界放宽”贡献的,另一个(类型错)无论怎么放宽都救不回来。反过来说:如果有人拿宽松口径的分数去汇报,你有权要求他明确写出”类型错不算错里的哪些”——本例中类型错恰好在两种口径下都没被放过,是因为我们要求类型必须相同。如果把类型也不要求,那 LOC 那条假正例和 ORG 的真金标就会因为区间重叠被判命中,分数还会再涨,那时指标就彻底失去意义了。
三、真实项目里必须先定下的五件事
微例只有 4 句,真实项目动辄数万句。上面暴露的问题在小数据上是”算错分”,在大数据上会变成”返工重标”。以下五件事必须在标注启动前写进指南。
3.1 标签体系:BIO 够用吗
B-/I-/O 的优点是简单,缺点是相邻同类型实体分不开。序列 [B-PER, I-PER, B-PER] 很清楚是两个实体,但 [B-PER, I-PER, I-PER] 既可能是一个三字人名,也可能是”张”+“华李”这种贴着写的情况,只能靠标注规范去人为约定。
引入 BIOES(增加 E- 结束、S- 单字实体)可以把这个歧义消掉:[B-PER, I-PER, E-PER] 就是一个三字实体,[S-PER, S-PER] 就是两个单字实体。选哪种要在项目第一天定下来,中途换体系等于全部重标,而且要重写所有评估脚本。
3.2 边界规范:中文没有空格可依赖
英文可以靠空格判断词边界,中文不行。“北京市海淀区”是 1 个 LOC 还是 2 个?“北京大学”里能不能再单独标出”北京”这个 LOC?“清华大学计算机系”里的”清华大学”要不要标?
这些都必须给出可直接照做的规则 + 每类至少 3 个正例反例,否则不同标注员会给出不同答案,模型学到的就是噪声。经验做法:规范里写”禁止”比写”建议”有效得多——例如”禁止把行政区划拆成上下级分别标注,一律标最长的连续片段”。
3.3 嵌套实体:单层标签装不下
“清华大学计算机系”是 ORG,其内部的”清华大学”也是 ORG。单层 BIOES 无法同时表达这两层,因为一个字符只能有一个标签。
路线有两条:一是换用层次化标注(多层标签序列,每层一套),二是明确规定只标某一层(通常标最外层,或按业务需要只标机构不标其子部分)。前者成本高但信息全,后者成本低但要接受信息损失。选哪条取决于下游要不要那个子实体,而不是取决于哪个”更先进”。
3.4 偏移对齐:分词之后的坑
如果预处理做了分词,标签是打在词上的,而业务要的是字符区间。此时必须显式维护”词 → 原始字符偏移”的映射表,否则即使标签全对,还原出的区间也是错的——而且是错得很隐蔽:句子看起来没问题,实体边界偏一两个字。
本节的微例直接在字符级操作(bio_to_entities 的 i 就是字符下标),规避了这个坑。真实项目里请把这条写进代码审查清单:任何把标签映射回原文的地方,都要有偏移的单测。
3.5 长尾实体与冷启动
产品型号、内部项目代号、罕见地名这类实体,标注样本极少,模型几乎学不到。可行的组合拳是:
- 规则/词典兜底:对格式固定的实体(编号、日期、型号)先用正则或词典抽一遍,模型只负责剩下的。
- 分类别看指标:像 2.1 那样按类型拆开报 P/R/F1,长尾类型单独盯,不要被 Micro 平均值掩盖。
- 主动学习挑样本:优先把模型最不确定的句子送人工标注,同样的人力能换来更高的边际提升。
四、从微例到真实项目:逻辑映射
把上面的微例往上抬一层,哪些是不变的,哪些是要换的,说清楚才不会误用。
不变的三条
- 计价单位是实体,不是字符。不管你用 BERT 还是规则,验收口径都应该是实体级 P/R/F1,并且写清是严格还是宽松。
- 评估脚本可以复用。模型换了、数据量涨了,
bio_to_entities+ 集合比对这套逻辑不变——只要标签体系不变。这就是为什么 3.1 那条”第一天定死”这么重要。 - 口径变化必须披露。0.7778 和 0.8889 是同一份预测,报告里只写一个数而不写口径,等同于误导。
要换的三处
- 数据规模:从 4 句到数万句,人工从零标不现实。常见做法是用现有模型预标注、人工只做修正(“预标注 + 审核”),并配套抽检机制盯标注一致性(比如同一批句子两人标、算 Kappa)。
- 模型:从手写规则到微调预训练模型。模型输出依然是逐字标签,所以下游评估链路一行都不用改——这正是把评估与模型解耦的价值。
- 部署:从离线脚本到在线服务。上线后要加的是词典热更新(新机构名不用重训模型就能识别)、以及每次模型迭代的回归集(固定一批句子,分数不能掉)。
一句话结论:NER 项目的分数,八成由”边界与类型怎么定义”决定,两成由模型决定;而分数可不可信,取决于你把口径写没写清楚。
📝 动手练一练
口径辨析:同一份预测,严格匹配 F1 = 0.7778,宽松匹配 F1 = 0.8889。请指出这两个数的差距具体来自哪一个实体,并说明另一个错误为什么在两种口径下都没被放过。
改代码:把 2.2 的
matches函数改成”类型也必须相同才判命中”以外的另一种口径——只要求区间有交集,不要求类型相同,重跑一遍看宽松 F1 变成多少,并解释这个数为什么不能用来验收。
👉 点击查看参考答案
1. 口径辨析
- 差距来自句子 1 的「北京大」:它的区间 (5, 7) 与金标「北京大学」的 (5, 8) 有交集,宽松口径判命中,严格口径判错。
- 句子 2 的「苹果公司」两种口径都判错:宽松口径仍要求”类型相同”,而它的类型是 LOC、金标是 ORG,所以无论边界多准都过不了。
- 因此 0.7778 → 0.8889 这一步,只放宽了”边界必须全同”这一条,没有放宽”类型必须相同”。这也说明:报分数时必须同时报口径,否则 11 个点的差距无法解释。
2. 改代码:类型也不要求
把 matches 的 if g[0] != p[0] or g[1] != p[1] 改成只判断句子编号 if g[0] != p[0],其余不变,重跑后宽松 F1 会到 1.0000(9 个金标实体全部找到有交集的预测)。
这个数不能用来验收,原因是它已经不区分实体类型了:把「苹果公司」标成 LOC 也算对,那”识别类型”这个任务本身就没有被评估。它的唯一用途是排查”是否存在边界漏检”,绝不能作为对外汇报的指标。
本章小结
本节没有训练任何模型,只把评估算对,结论有三条:
- token 级准确率会骗人。本例 46 个字符里错 9 个,准确率 80.43%,看上去”八成对”;但错误实际落在 2 个实体上(一个边界少一字、一个类型标错),实体级严格 F1 是 0.7778,两把尺子量的是完全不同的事。
- 口径决定分数。同一份预测,严格匹配 0.7778、宽松匹配 0.8889,差 11.11 个百分点,模型没动过。任何 NER 指标都必须带上”严格/宽松”的标签才能比较。
- 规范比模型重要。标签体系(BIO 还是 BIOES)、边界规则、嵌套处理、偏移对齐、长尾兜底——这五件事写进指南之前,标注数据不可用,后面所有分数都不可信。
📋 行动清单
- 先跑一遍本节三个代码块,确认你能复现 0.8043 / 0.7778 / 0.8889 这三个数,并说清每个数的分母是什么。
- 在你的项目评估脚本里加一个断言:实体统计的键必须包含句子编号,防止跨句合并把实体数算少。
- 起草《标注规范 V0.1》,至少写清四件事:用 BIO 还是 BIOES、边界取最长还是取最短、嵌套实体标哪一层、长尾实体是否走词典兜底。
- 报告模板里加一行:“匹配口径:严格/宽松(阈值 X)“,不允许留空。
—— 小象教研组