如何使用spaCy处理冲突实体与分散偏移并提取BILUO标签
spaCy处理临床文本标注的两个核心问题解决方案
一、不连续位置索引的解析方法
你拿到的location字段是字符串序列化的列表,其中带分号的条目对应不连续实体的多段文本跨度,不要直接把原始字符串传入spaCy接口,先做统一格式解析即可。
解析逻辑参考如下代码:
import ast from spacy.tokens import Span def parse_entity_locations(loc_str): # 把字符串格式的列表转为Python原生列表 raw_loc_list = ast.literal_eval(loc_str) parsed_spans = [] for loc_entry in raw_loc_list: # 拆分分号分隔的多段不连续位置 seg_list = loc_entry.split(";") entry_segs = [] for seg in seg_list: start_idx, end_idx = map(int, seg.strip().split()) entry_segs.append((start_idx, end_idx)) parsed_spans.append(entry_segs) return parsed_spans
注意spaCy默认的Doc.ents容器不直接支持多段拼接的不连续实体,根据任务需求选处理方式即可:
- 如果是训练常规NER模型、生成BILUO标签,直接把每一段跨度作为对应实体类型的独立Span传入即可,不要强行跨非实体文本拼接长Span,避免标注错位
- 如果需要保留「多段同属一个实体」的关联关系,不要把Span存入
doc.ents,改用SpanGroup存储,该结构原生支持不连续、跨跨度的实体绑定。
二、重叠实体冲突(E103报错)的解决
这个报错的本质原因是spaCy默认的doc.ents容器要求单个token只能归属一个实体,不支持同位置多标签。针对你遇到的同一片段对应多个实体的场景,根据任务目标选对应方案即可:
- 标签合并方案:如果重叠的实体语义高度相近(比如你示例中位置
289 314对应的「Insomnia」和「Lack of Sleep」本身就是强关联症状),可以在预处理阶段把这类重叠标签合并为组合标签(例如Insomnia|Lack of Sleep),模型训练时直接学习合并后的标签,适合标签重合度高、语义关联强的场景。 - SpanGroup多组存储方案:不要把所有实体都塞入
doc.ents,而是将不同类型的实体存入doc.spans下独立命名的SpanGroup,该结构完全支持同位置多实体、不连续实体存储,不会触发冲突。后续需要生成BILUO标签时,按SpanGroup分别生成即可,这也是目前医疗文本NER处理多标签重叠的通用方案。 - 冲突过滤方案:如果你的任务要求单token单实体标签,可以提前制定过滤规则(比如优先保留文本粒度更细的实体、标注频次更高的实体),预处理阶段剔除冲突位置的冗余实体,保证每个token仅对应一个标签。
调用offsets_to_biluo_tags的注意事项
传入偏移前一定要做边界对齐校验,你的数据文本里包含
\r\n换行符,很容易出现字符计数偏差导致标签错位。可以在创建Span时调用doc.char_span方法并传入alignment_mode="contract"参数,自动收缩对齐到合法的token边界,过滤无效偏移。
完整的最小处理流程参考:
import spacy from spacy.training import offsets_to_biluo_tags nlp = spacy.blank("en") # sample_data 为你贴出的JSON格式样本 doc = nlp(sample_data["pn_history"]) doc.spans["custom_ents"] = [] # 解析所有实体存入SpanGroup for ent in sample_data["entities"]: all_seg_groups = parse_entity_locations(ent["location"]) for seg_group in all_seg_groups: for start, end in seg_group: # 自动对齐token边界 span = doc.char_span( start, end, label=ent["feature_text"], alignment_mode="contract" ) if span: # 跳过对齐失败的无效跨度 doc.spans["custom_ents"].append(span) # 如果需要生成单标签BILUO用于常规NER训练,做冲突去重 sorted_spans = sorted( doc.spans["custom_ents"], key=lambda x: (x.end - x.start), # 长实体优先保留 reverse=True ) used_token_ids = set() valid_ents = [] for span in sorted_spans: token_range = set(range(span.start, span.end)) if not token_range & used_token_ids: valid_ents.append(span) used_token_ids.update(token_range) doc.ents = valid_ents # 生成最终BILUO标签 biluo_tags = offsets_to_biluo_tags( doc, [(s.start_char, s.end_char, s.label_) for s in doc.ents] )
内容的提问来源于stack exchange,提问作者tanmay
相关产品推荐
相关产品推荐

