📖 什么是命名实体识别
命名实体识别(Named Entity Recognition, NER)是从非结构化文本中识别出具有特定意义的实体,并分类到预定义类别(如人名、地名、组织名等)的技术。NER是知识图谱构建的第一步——没有实体,就没有知识图谱。
🎯 常见实体类型
| 类别 | 英文 | 示例 |
| 人名 | PER | 鲁迅、毛泽东、爱因斯坦 |
| 地名 | LOC | 北京、浙江省、太平洋 |
| 组织 | ORG | 北京大学、联合国、阿里巴巴 |
| 时间 | TIME | 2024年3月、春秋时期 |
| 数字/金额 | NUM | 300万、3.14 |
| 作品 | WORK | 《呐喊》、蒙娜丽莎 |
| 事件 | EVENT | 五四运动、二战 |
📐 NER方法演进
规则/词典→
HMM/CRF→
BiLSTM-CRF→
BERT-NER→
大模型NER
💻 Python实现:基于规则的NER
import re
from collections import defaultdict
class RuleBasedNER:
"""基于规则和词典的命名实体识别器"""
def __init__(self):
self.dictionaries = defaultdict(set)
self.patterns = []
self.prefixes = defaultdict(list)
def add_dictionary(self, entity_type, entities):
"""添加实体词典"""
for e in entities:
self.dictionaries[entity_type].add(e)
def add_pattern(self, regex, entity_type):
">>>添加正则模式"""
self.patterns.append((re.compile(regex), entity_type))
def add_prefix(self, entity_type, prefixes):
"""添加实体前缀(如'省'、'市')"""
self.prefixes[entity_type].extend(prefixes)
def recognize(self, text):
"""识别文本中的命名实体,返回 [(实体, 类型, 起始位置, 结束位置)]"""
results = []
for entity_type, entities in self.dictionaries.items():
for entity in sorted(entities, key=len, reverse=True):
start = 0
while True:
idx = text.find(entity, start)
if idx == -1:
break
if not any(s <= idx < e or s < idx + len(entity) <= e for _, _, s, e in results):
results.append((entity, entity_type, idx, idx + len(entity)))
start = idx + 1
for pattern, entity_type in self.patterns:
for match in pattern.finditer(text):
entity = match.group()
if not any(s <= match.start() < e or s < match.end() <= e for _, _, s, e in results):
results.append((entity, entity_type, match.start(), match.end()))
for entity_type, prefixes in self.prefixes.items():
for prefix in prefixes:
pattern = re.compile(r'[一-鿿]{1,4}' + re.escape(prefix))
for match in pattern.finditer(text):
entity = match.group()
if not any(s <= match.start() < e or s < match.end() <= e for _, _, s, e in results):
results.append((entity, entity_type, match.start(), match.end()))
results.sort(key=lambda x: x[2])
return results
def format_results(self, text, results):
"""格式化输出识别结果"""
lines = []
for entity, etype, start, end in results:
lines.append(f" [{etype}] {entity} (位置: {start}-{end})")
return "
".join(lines)
ner = RuleBasedNER()
ner.add_dictionary("PER", ["鲁迅", "老舍", "徐志摩", "巴金", "茅盾", "毛泽东"])
ner.add_dictionary("LOC", ["绍兴", "北京", "上海", "海宁", "浙江", "浙江省", "中国"])
ner.add_dictionary("ORG", ["北京大学", "清华大学", "浙江大学", "中华文学社"])
ner.add_dictionary("WORK", ["呐喊", "彷徨", "骆驼祥子", "再别康桥", "家", "子夜"])
ner.add_pattern(r'\d{4}年', "TIME")
ner.add_pattern(r'\d{1,2}月\d{1,2}日', "TIME")
ner.add_pattern(r'《[^》]+》', "WORK")
ner.add_prefix("LOC", ["省", "市", "区", "县", "镇"])
ner.add_prefix("ORG", ["大学", "学院", "公司", "集团"])
test_texts = [
"鲁迅出生于浙江省绍兴,1923年出版了《呐喊》。",
"老舍在北京大学任教,创作了骆驼祥子和茶馆。",
"徐志摩是海宁人,1931年11月19日因飞机失事遇难。",
"巴金原名李尧棠,出生于四川省成都市。",
]
for text in test_texts:
results = ner.recognize(text)
print(f"原文: {text}")
print(ner.format_results(text, results))
print()
原文: 鲁迅出生于浙江省绍兴,1923年出版了《呐喊》。
[PER] 鲁迅 (位置: 0-2)
[LOC] 浙江省 (位置: 4-7)
[LOC] 绍兴 (位置: 7-9)
[TIME] 1923年 (位置: 10-15)
[WORK] 《呐喊》 (位置: 18-22)
原文: 老舍在北京大学任教,创作了骆驼祥子和茶馆。
[PER] 老舍 (位置: 0-2)
[ORG] 北京大学 (位置: 3-7)
原文: 徐志摩是海宁人,1931年11月19日因飞机失事遇难。
[PER] 徐志摩 (位置: 0-3)
[LOC] 海宁 (位置: 4-6)
[TIME] 1931年 (位置: 8-13)
[TIME] 11月19日 (位置: 13-18)
原文: 巴金原名李尧棠,出生于四川省成都市。
[PER] 巴金 (位置: 0-2)
[LOC] 四川省 (位置: 10-13)
[LOC] 成都市 (位置: 13-16)
🧠 基于序列标注的NER
BIO标注方案是NER最常用的序列标注方法:
| 标签 | 含义 | 示例 |
| B-PER | 人名起始 | 鲁/B-PER |
| I-PER | 人名内部 | 迅/I-PER |
| B-LOC | 地名起始 | 绍/B-LOC |
| I-LOC | 地名内部 | 兴/I-LOC |
| B-ORG | 组织起始 | 北/B-ORG |
| O | 非实体 | 出/O 生/O |
class BIOTagger:
"""BIO序列标注与解码"""
def encode(self, text, entities):
"""将文本和实体标注转换为BIO标签序列"""
tags = ["O"] * len(text)
for entity, etype, start, end in entities:
tags[start] = f"B-{etype}"
for i in range(start + 1, end):
tags[i] = f"I-{etype}"
return list(zip(text, tags))
def decode(self, tagged_sequence):
"""从BIO标签序列解码出实体"""
entities = []
current_entity = ""
current_type = None
start = None
for i, (char, tag) in enumerate(tagged_sequence):
if tag.startswith("B-"):
if current_entity:
entities.append((current_entity, current_type, start, i))
current_type = tag[2:]
current_entity = char
start = i
elif tag.startswith("I-") and current_type == tag[2]:
current_entity += char
else:
if current_entity:
entities.append((current_entity, current_type, start, i))
current_entity = ""
current_type = None
start = None
if current_entity:
entities.append((current_entity, current_type, start, len(tagged_sequence)))
return entities
tagger = BIOTagger()
text = "鲁迅出生于绍兴"
entities = [("鲁迅", "PER", 0, 2), ("绍兴", "LOC", 4, 6)]
tagged = tagger.encode(text, entities)
print("=== BIO标注 ===")
for char, tag in tagged:
print(f" {char} → {tag}")
decoded = tagger.decode(tagged)
print("
=== BIO解码 ===")
for entity, etype, s, e in decoded:
print(f" [{etype}] {entity} ({s}-{e})")
=== BIO标注 ===
鲁 → B-PER
迅 → I-PER
出 → O
生 → O
于 → O
绍 → B-LOC
兴 → I-LOC
=== BIO解码 ===
[PER] 鲁迅 (0-2)
[LOC] 绍兴 (4-6)
📊 NER评估指标
def evaluate_ner(gold, pred):
"""评估NER结果
Args:
gold: 标注正确的实体集合 {(entity, type, start, end)}
pred: 预测的实体集合
"""
gold_set = set((e, t, s, en) for e, t, s, en in gold)
pred_set = set((e, t, s, en) for e, t, s, en in pred)
tp = len(gold_set & pred_set)
fp = len(pred_set - gold_set)
fn = len(gold_set - pred_set)
precision = tp / (tp + fp) if tp + fp > 0 else 0
recall = tp / (tp + fn) if tp + fn > 0 else 0
f1 = 2 * precision * recall / (precision + recall) if precision + recall > 0 else 0
return {"Precision": f"{precision:.4f}", "Recall": f"{recall:.4f}", "F1": f"{f1:.4f}"}
gold_entities = [("鲁迅", "PER", 0, 2), ("绍兴", "LOC", 4, 6), ("北京大学", "ORG", 8, 12)]
pred_entities = [("鲁迅", "PER", 0, 2), ("绍兴", "LOC", 4, 6), ("北京", "LOC", 8, 10)]
print("=== NER评估 ===")
metrics = evaluate_ner(gold_entities, pred_entities)
for k, v in metrics.items():
print(f" {k}: {v}")
=== NER评估 ===
Precision: 0.6667
Recall: 0.6667
F1: 0.6667
📝 实战练习
练习1:扩展NER系统
添加产品名、职位等新实体类型的词典和规则,测试识别效果。
练习2:实现简单的CRF特征
为每个字符提取词性、是否数字、是否大写等特征,用于CRF训练。
练习3:评估改进
添加更多词典条目,对比改进前后的P/R/F1。
🏷️
🏆 第6课成就解锁
实体识别工程师
🏷️ 词典NER
📋 BIO标注
📊 P/R/F1
🔍 正则模式