命名实体识别(NER):从规则到深度学习的进化之路
命名实体识别,简称 NER,是信息抽取里最基础也最关键的环节。说白了,就是从文本里把人名、地名、机构名、时间、数字这些「实体」给揪出来。我刚开始做知识图谱的时候,花在 NER 上的时间占了整个项目一半还多。
这节课我们就聊聊 NER 的三种主流实现方式。每种方式各有长短,实际项目中怎么选,看场景说话。
基于规则的方法:简单直接但脆皮
规则方法是最早的 NER 实现方式。核心思路就是人工编写匹配模式。你想想看,这不就是写一堆正则表达式和词典吗?对,但也没那么简单。
我个人习惯用这种方式做快速原型验证。比如从学术文献里抽作者名,规则就很好使。
# 一个简单的基于规则的NER示例
import re
def extract_entities(text):
entities = []
# 中文人名模式
name_pattern = r'[\u4e00-\u9fa5]{2,4}(?:教授|博士|院士|研究员)'
# 机构名模式
org_pattern = r'(?:中国科学院|清华大学|北京大学|复旦大学)[\u4e00-\u9fa5]{0,10}(?:研究所|学院|实验室|中心)'
# 时间表达式
time_pattern = r'\d{4}年\d{1,2}月\d{1,2}日'
for match in re.finditer(name_pattern, text):
entities.append({
'text': match.group(),
'type': 'PERSON',
'start': match.start(),
'end': match.end()
})
for match in re.finditer(org_pattern, text):
entities.append({
'text': match.group(),
'type': 'ORGANIZATION',
'start': match.start(),
'end': match.end()
})
return entities
text = "中国科学院张伟教授于2024年3月15日在清华大学发表演讲"
entities = extract_entities(text)
for e in entities:
print(f"{e['text']} -> {e['type']}")
但是,规则方法有个致命问题——脆弱。我在项目中遇到过,换个领域的文本,规则就全废了。比如医学文献里「发热」是症状实体,放到金融新闻里就是噪音。
规则方法优缺点很鲜明:
- 优点:实现快、可解释性强、不需要标注数据
- 缺点:覆盖有限、维护成本高、跨领域能力差
基于统计的方法:数据驱动的进化
统计方法把 NER 看成一个序列标注问题。说白了,就是给每个字或词打一个标签,比如 B-PER(人名开始)、I-PER(人名中间)、O(非实体)。
这里有几种经典模型:
| 模型 | 核心思想 | 典型应用 |
|---|---|---|
| 隐马尔可夫模型 (HMM) | 假设状态依赖于前一状态,观测独立 | 早期 NLP 工具,任务单一 |
| 最大熵马尔可夫模型 (MEMM) | 用最大熵建模状态转移,克服独立性假设 | 信息抽取,对局部特征敏感 |
| 条件随机场 (CRF) | 全局归一化,考虑整个序列的依赖关系 | 经典 SOTA,至今仍用于 |
我个人比较推荐 CRF。为什么?它不像 HMM 那样假设观测独立,也不像 MEMM 那样存在「标注偏置」问题。CRF 是在整个序列上做条件概率建模,能学到标签之间的约束关系。比如「I-PER」不能直接跟在「O」后面,这种规则 CRF 自动就会学到。
# CRF 特征模板示例
def word2features(sent, i):
word = sent[i][0]
features = {
'bias': 1.0,
'word.lower()': word.lower(),
'word[-3:]': word[-3:],
'word[-2:]': word[-2:],
'word.isupper()': word.isupper(),
'word.istitle()': word.istitle(),
'word.isdigit()': word.isdigit(),
}
if i > 0:
word1 = sent[i-1][0]
features.update({
'-1:word.lower()': word1.lower(),
'-1:word.istitle()': word1.istitle(),
})
else:
features['BOS'] = True
if i < len(sent)-1:
word1 = sent[i+1][0]
features.update({
'+1:word.lower()': word1.lower(),
'+1:word.istitle()': word1.istitle(),
})
else:
features['EOS'] = True
return features
基于深度学习的方法:端到端的革命
深度学习的出现,把 NER 的准确率又推了一个台阶。核心变化是——不再需要手写特征了。模型自己从原始文本里学语义表示。
主流架构有这么几个:
- BiLSTM-CRF:双向 LSTM 编码上下文,CRF 层做解码。这是 2015-2019 年的标配。
- BERT + 全连接层:预训练模型提取特征,一个线性层做分类。简单粗暴。
- BERT + CRF:目前生产中常见的选择,用 BERT 替代 BiLSTM 做编码。
我建议刚入门的朋友先跑一遍 BiLSTM-CRF。代码量适中,能理解序列标注的整个 pipeline。之后再过渡到 BERT 微调。
# PyTorch 风格的 BiLSTM-CRF 核心结构
import torch
import torch.nn as nn
class BiLSTM_CRF(nn.Module):
def __init__(self, vocab_size, tag_to_ix, embedding_dim, hidden_dim):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embedding_dim)
self.lstm = nn.LSTM(embedding_dim, hidden_dim // 2,
num_layers=1, bidirectional=True, batch_first=True)
self.hidden2tag = nn.Linear(hidden_dim, len(tag_to_ix))
self.crf = CRF(len(tag_to_ix), batch_first=True)
def forward(self, x, tags=None, mask=None):
embeds = self.embedding(x)
lstm_out, _ = self.lstm(embeds)
emissions = self.hidden2tag(lstm_out)
if tags is not None:
loss = -self.crf(emissions, tags, mask=mask)
return loss
else:
return self.crf.decode(emissions, mask=mask)
这里要注意,BERT 虽然强,但也不是万能的。我做过对比实验:在相同标注数据(比如 1 万条)下,BERT 微调比 BiLSTM-CRF 高 3-5 个 F1 点。但如果数据只有几百条,BiLSTM-CRF 反而不输 BERT。
三种方法怎么选?
给你一个决策框架,纯属个人经验:
- 数据规模 < 500 条:规则方法打底,配合少量正则做兜底
- 数据 500 - 5000 条:CRF 或 BiLSTM-CRF,特征工程要花心思
- 数据 > 5000 条:大胆上 BERT 微调,注意推理效率
还有一种混合打法:用深度学习抽候选实体,用规则做合法性校验。我在金融文档抽取项目里就是这么干的,实体准确率从 92% 升到了 97%。说白了,深度模型解决召回,规则解决精度。