实体识别落地:规则和模型不是二选一,是分层

📅 2026/7/7 10:49:06
实体识别落地:规则和模型不是二选一,是分层
实体识别落地规则和模型不是二选一是分层一、正则表达式就够了和BERT 肯定比规则好——两种声音都高估了自己的方案NER命名实体识别落地方案的争论通常在两个极端间摇摆纯规则派说我们的实体类型只有 15 种正则表达式的 F1 能到 85%引入 BERT 增加 100ms 延迟只提升 3 个点不值得。纯模型派说你看你的正则表达式漏掉了所有口语化的实体表达而且维护 200 条规则的人力成本比训练一个模型高得多。两派都没错——但他们都忽视了一个关键事实不同的实体类型适合不同的识别策略。日期、金额、身份证号天生适合规则产品名、昵称、疾病症状天生适合模型。成熟的 NER 方案必须分层。二、NER 分层架构的设计原理flowchart TB Input[输入文本] -- Layer1[第一层规则层br/高精度确定性匹配] Layer1 --|命中| Output1[规则识别的实体br/置信度 100%] Layer1 --|未命中| Layer2[第二层词典模糊匹配层br/中等精度模糊边界] Layer2 --|命中| Output2[词典匹配的实体br/置信度 80-95%] Layer2 --|未命中| Layer3[第三层模型推理层br/低精度高覆盖未知实体] Layer3 -- Output3[模型预测的实体br/置信度 70-90%] Output1 -- Merge[实体合并与冲突消解] Output2 -- Merge Output3 -- Merge Merge -- Final[最终实体列表] style Layer1 fill:#c8e6c9 style Layer2 fill:#fff9c4 style Layer3 fill:#e3f2fd分层逻辑规则层在最前面处理确定性实体模型层在最后面兜底未知实体。这不是按技术先进性排序而是按错误成本的梯度排序——规则层几乎不错但覆盖率有限模型层覆盖率高但可能产生假阳性。把两者的结果放在一起用置信度加权的冲突消解逻辑合并。三、分层 NER 系统的工程实现import re import json from typing import List, Dict, Tuple, Optional, Set from dataclasses import dataclass, field from enum import Enum class EntityType(Enum): 实体类型枚举——必须集中定义以避免跨层类型不匹配 DATE date MONEY money PERSON person ORG organization LOCATION location ID_NUMBER id_number PHONE phone PRODUCT product dataclass class Entity: NER 实体统一数据结构 所有层产出的实体都使用相同的数据结构。 这种设计确保各层之间可以无缝合并和对比 不会出现规则层用 dict、模型层用 namedtuple导致的接口混乱 text: str # 实体文本 entity_type: EntityType start: int # 在原文本中的起始位置 end: int # 在原文本中的结束位置 confidence: float # 置信度 [0, 1] source: str # 来源标识rule | dictionary | model property def span(self) - Tuple[int, int]: return (self.start, self.end) def overlaps_with(self, other: Entity) - bool: 判断两个实体是否在文本位置上重叠 return not (self.end other.start or other.end self.start) class RuleBasedExtractor: 规则层实体提取器 适用实体类型日期、金额、身份证、电话号码等高度格式化的实体。 这些实体有明确的、可被正则表达式精确捕获的形态特征。 错误模式漏召回而非误识别。规则层的假阳性率接近 0 但覆盖率受限于规则的完备程度。 # 规则字典实体类型 → (正则模式, 优先级) # 优先级用于冲突消解ID_NUMBER 可能被误识别为 DATE # 通过设置更高优先级来避免 PATTERNS: Dict[EntityType, Tuple[str, int]] { EntityType.DATE: ( r\d{4}[-/年]\d{1,2}[-/月]\d{1,2}[日号]?, 5 ), EntityType.MONEY: ( r(?:¥||USD|EUR)?\s*\d{1,3}(?:,\d{3})*(?:\.\d{1,2})?\s*(?:元|万|亿|万元|亿美元)?, 3 ), EntityType.ID_NUMBER: ( r\d{6}(?:19|20)\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])\d{3}[\dXx], 10 ), EntityType.PHONE: ( r(?:\86[-\s]?)?1[3-9]\d{9}, 8 ), } def extract(self, text: str) - List[Entity]: 从文本中提取规则匹配的实体 entities [] for entity_type, (pattern, priority) in self.PATTERNS.items(): for match in re.finditer(pattern, text): # 不使用 priority 字段直接但留作未来扩展 entities.append(Entity( textmatch.group(), entity_typeentity_type, startmatch.start(), endmatch.end(), confidence1.0, # 规则匹配的实体的置信度固定为 1.0 sourcerule, )) return entities class DictionaryExtractor: 词典层实体提取器 适用实体类型人名、地名、机构名等有一定集合范围的实体。 介于规则层和模型层之间——比规则层更灵活不需要形态规律 比模型层更快O(词条数 × 文本长) 的字符串匹配而非模型推理 瓶颈词典规模和质量。词典太小 → 漏召回词典太大 → 误召回 如苹果可能匹配水果而非公司名 def __init__(self): # 词典存储{实体类型: {词条: 置信度}} self._dictionaries: Dict[EntityType, Dict[str, float]] {} def load_dictionary(self, entity_type: EntityType, entries: Dict[str, float]): 加载词典 entries 格式{词条: 置信度}。 置信度不是二值0/1而是浮点数因为不同词条 的歧义程度不同北京作为地名的置信度高于朝阳 self._dictionaries[entity_type] entries def extract(self, text: str) - List[Entity]: 从文本中匹配词典实体最长匹配优先 entities [] for entity_type, entries in self._dictionaries.items(): for word, confidence in entries.items(): # 查找所有出现位置 start 0 while True: pos text.find(word, start) if pos -1: break entities.append(Entity( textword, entity_typeentity_type, startpos, endpos len(word), confidenceconfidence, sourcedictionary, )) start pos 1 return entities class ModelBasedExtractor: 模型层实体提取器抽象接口 实际的模型推理实现在子类中这里只定义接口。 这种设计允许在不修改提取管道的情况下 替换底层模型如 BERT → RoBERTa → 领域微调模型 def extract(self, text: str) - List[Entity]: 子类实现具体的模型推理 raise NotImplementedError class LayeredNER: 分层 NER 引擎——编排三层提取器的结果 合并策略Conflict Resolution 当两个实体在文本位置上重叠时按以下优先级选择 1. source 优先级rule dictionary model 2. 同 source 时confidence 高的胜出 3. 同 confidence 时更早出现的start 小的胜出 这个策略体现了 规则优先、模型兜底 的设计哲学 def __init__(self): self.rule_extractor RuleBasedExtractor() self.dict_extractor DictionaryExtractor() self.model_extractor: Optional[ModelBasedExtractor] None def set_model(self, extractor: ModelBasedExtractor): 注入模型提取器可选 self.model_extractor extractor def extract(self, text: str) - List[Entity]: 分层提取并合并实体 all_entities: List[Entity] [] # 第一层规则 all_entities.extend(self.rule_extractor.extract(text)) # 第二层词典 all_entities.extend(self.dict_extractor.extract(text)) # 第三层模型 if self.model_extractor: all_entities.extend(self.model_extractor.extract(text)) # 冲突消解 return self._resolve_conflicts(all_entities) def _resolve_conflicts(self, entities: List[Entity]) - List[Entity]: 实体冲突消解 处理重叠实体的核心逻辑 按 start 排序后遍历所有实体如果当前实体与 已接受的实体有重叠按优先级规则决定保留哪个 if not entities: return [] # 按起始位置排序 sorted_entities sorted(entities, keylambda e: (e.start, -len(e.text))) # Source 优先级映射 source_priority {rule: 0, dictionary: 1, model: 2} accepted: List[Entity] [] for candidate in sorted_entities: is_overlapping False for existing in accepted: if candidate.overlaps_with(existing): # 优先保留 source 优先级更高的 cand_pri source_priority.get(candidate.source, 99) exist_pri source_priority.get(existing.source, 99) if cand_pri exist_pri: # candidate 优先级更低 is_overlapping True break elif cand_pri exist_pri: # 同 source保留 confidence 更高的 if candidate.confidence existing.confidence: is_overlapping True break else: # candidate 更好替换 existing accepted.remove(existing) accepted.append(candidate) is_overlapping True break else: # candidate 优先级更高 accepted.remove(existing) accepted.append(candidate) is_overlapping True break if not is_overlapping: accepted.append(candidate) return accepted # # 使用示例 # if __name__ __main__: ner LayeredNER() # 加载词典 ner.dict_extractor.load_dictionary(EntityType.PERSON, { 张三: 0.95, 李四: 0.90, 王五: 0.92, }) ner.dict_extractor.load_dictionary(EntityType.ORG, { 阿里巴巴: 0.98, 腾讯: 0.97, }) text 张三于2025年7月6日在阿里巴巴消费¥1,500.00元 entities ner.extract(text) for entity in sorted(entities, keylambda e: e.start): print(f [{entity.source}] {entity.entity_type.value}: {entity.text} (confidence{entity.confidence})) ## 四、分层 NER 的系统边界与架构权衡 **规则层维护成本随复杂度指数增长**。 规则层的初始 F1 可能高达 95%但每增加一种新的实体变体形态就需要增加一条或多条正则规则。当规则数超过 100 条时规则间的冲突和覆盖关系变得难以管理。建议当规则层维护时间超过模型层微调时间的 50% 时考虑将该实体类型的识别工作全量移交模型层。 **词典层的覆盖天花板**。 词典的覆盖率受到实体集合有限性本身的限制。对于产品名、昵称、事件名等开放集合的实体类型词典永远无法达到 80% 以上的召回率。对于封闭集合如中国省份名、化学元素名词典可以达到接近 100% 的召回率——此时引入模型层不仅无益还会增加延迟和误召风险。 **模型层与规则层的协作模式**。 当前实现采用的是模型兜底策略规则没命中 → 模型推理但也可以采用模型增强策略规则和模型并行推理 → 置信度加权投票。前者的延迟低大多数实体被规则命中时不调用模型后者的准确率高两个独立的信号源可以相互纠正。选择取决于你的延迟预算和准确率要求。见证奇迹的时刻是一个被模型纠正的规则层假阴性——它证明了分层架构的价值不在于各层独立工作而在于它们相互校验。 ## 五、总结 分层 NER 的三个核心设计原则 1. **规则处理确定性、模型覆盖开放性**日期/金额/身份证号走规则层产品名/昵称/症状走模型层。 2. **Source 优先级决定冲突消解**规则优先级最高几乎不错模型优先级最低可能产生假阳性。 3. **选型判断依据是实体类型的集合性质**封闭集合省名、元素名一律用规则和词典开放集合人名、事件名必须引入模型。