4. 实体抽取基础:命名实体识别(NER)、基于词典的抽取、基于序列标注的抽取

好,咱们今天聊聊实体抽取。说白了,就是让机器能从一段话里,把「谁、在哪、什么时候、什么东西」这些关键信息给拎出来。我刚开始做对话系统那会儿,觉得这玩意儿不就是关键词匹配嘛,后来踩了不少坑才明白——这里头门道深着呢。

4.1 什么是命名实体识别?

命名实体识别,英文叫 NER(Named Entity Recognition)。它的任务很简单:给定一段文本,找出里面的人名、地名、机构名、时间、数字等等。你想想看,用户说「帮我订一张明天从北京到上海的机票」,系统得知道「明天」是时间,「北京」是出发地,「上海」是目的地。没有 NER,对话系统就是个瞎子。

我个人习惯把实体分成两大类:

  • 通用实体:人名、地名、机构名、时间、日期、货币等。这些在大部分场景下都通用。
  • 领域实体:比如医疗领域的「药品名」「症状名」,金融领域的「股票代码」「公司名」。这些得根据具体业务来定义。

核心要点:NER 不是简单的字符串匹配。同一个词在不同语境下可能是不同实体。比如「苹果」——在水果店是水果,在科技新闻里是公司。这就是 NER 的难点所在。

4.2 基于词典的抽取:简单粗暴但有效

最朴素的方法,就是拿一本词典去文本里找。词典里有什么词,就抽什么词。我在项目中遇到过一个小需求:从客服对话里提取产品型号。产品型号就那么几百个,用词典匹配又快又准。

具体怎么做呢?

# 一个简单的词典匹配示例
entity_dict = {
    "地点": ["北京", "上海", "广州", "深圳"],
    "人名": ["张三", "李四", "王五"],
    "产品": ["iPhone 15", "MacBook Pro", "AirPods"]
}

def dict_ner(text):
    results = []
    for entity_type, words in entity_dict.items():
        for word in words:
            if word in text:
                results.append((word, entity_type))
    return results

# 测试
text = "张三想买一台MacBook Pro,送到北京"
print(dict_ner(text))
# 输出:[('张三', '人名'), ('MacBook Pro', '产品'), ('北京', '地点')]

这种方法的好处是:

  • 速度快:不需要模型,纯字符串匹配
  • 可解释性强:抽出来就是词典里的词,不会莫名其妙
  • 容易维护:加个新词,改个词典就行

但缺点也很明显:

  • 词典覆盖不全:用户说「魔都」你词典里只有「上海」,那就抽不到
  • 歧义问题:「苹果」到底是水果还是公司?词典匹配解决不了
  • 变体问题:「MacBook Pro 2023款」和「MacBook Pro」是不是同一个东西?

我的建议:词典匹配适合做「冷启动」。刚开始没有标注数据时,先用词典搭个基础版本。等数据积累多了,再上模型。我曾经在一个项目里,词典匹配撑了三个月,效果还不错。

4.3 基于序列标注的抽取:让模型学会「看上下文」

词典匹配搞不定的事情,就得靠序列标注了。什么是序列标注?说白了,就是把文本里的每个字或词,打上一个标签。比如:

输入:我 想 去 北 京 玩
标签:O  O  O  B-LOC  I-LOC  O

这里的 B-LOC 表示「地名的开始」,I-LOC 表示「地名的中间或结尾」,O 表示「不是实体」。这种标注方式叫 BIO 标注,是最常用的。

常用的序列标注模型有:

  • BiLSTM-CRF:经典组合,双向 LSTM 提取特征,CRF 层保证标签的合理性
  • BERT + CRF:用预训练模型做编码,效果更好,但速度慢一些
  • Transformer + CRF:纯 Transformer 架构,适合长文本

我给大家看一个简单的训练流程:

# 伪代码:序列标注训练流程
1. 准备数据:每个句子 + 对应的 BIO 标签序列
2. 构建模型:Embedding层 -> BiLSTM -> CRF
3. 训练:用交叉熵损失 + CRF 损失
4. 预测:Viterbi 解码,输出最优标签序列

# 举个例子
句子 = "张三在北京买了台iPhone"
标签 = ["B-PER", "I-PER", "O", "B-LOC", "I-LOC", "O", "O", "B-PROD", "I-PROD"]
# 抽取出:张三(PER), 北京(LOC), iPhone(PROD)

注意:序列标注需要大量标注数据。我见过不少团队,标注了 1 万条数据,效果还是不行。为什么?因为标注质量太差。标注不一致、漏标、错标,都会让模型学歪。我曾经花了两周时间,专门做标注规范的培训,效果立竿见影。

4.4 两种方法的对比与选择

维度 基于词典 基于序列标注
开发成本 低,有词典就能跑 高,需要标注数据
维护成本 低,改词典就行 高,需要重新训练
泛化能力 差,只能识别词典里的词 强,能识别未见过的实体
歧义处理 差,无法区分上下文 好,能利用上下文信息
速度 极快 较慢(尤其用 BERT 时)
适用场景 实体数量少、变化少 实体数量多、变化多

我个人习惯的做法是:先词典后模型,两者结合。具体来说:

  • 用词典匹配做第一轮抽取,速度快,覆盖常见实体
  • 用序列标注模型做第二轮抽取,处理词典没覆盖到的实体
  • 如果两者冲突,可以设置优先级,或者用规则做后处理

避坑指南:我曾经在一个项目里,只用了序列标注模型,结果用户说「我要去浦东」——模型死活抽不出「浦东」是地点。为什么?因为训练数据里「浦东」出现太少。后来我加了个地点词典做兜底,问题就解决了。所以,别迷信模型,也别迷信词典,两者互补才是王道。

4.5 实战中的小技巧

最后分享几个我在实战中总结的小技巧:

  1. 实体边界要清晰:标注时,「北京大学」是一个实体还是两个?我建议按语义来,「北京大学」是一个机构名,不要拆成「北京」和「大学」。
  2. 嵌套实体要小心:「上海市浦东新区」——「上海市」是城市,「浦东新区」是区,两者有嵌套关系。序列标注模型一般处理不了嵌套,需要特殊设计。
  3. 实体归一化很重要:用户说「魔都」和「上海」,抽出来都是地点,但最好归一化成「上海」。不然下游系统会疯掉。
  4. 别忘了负样本:训练序列标注模型时,除了正样本(有实体的句子),也要加一些负样本(没有实体的句子)。不然模型会过度自信,啥都当成实体。

嗯,实体抽取这块,今天就聊到这儿。说白了,没有银弹。词典匹配简单但局限,序列标注强大但费数据。我的建议是:从简单开始,逐步迭代。先跑起来,再优化。别一开始就想搞个大模型,容易翻车。