☰
医疗实体识别落地全流程:从任务定义、数据标注到模型训练与上线排查
2026/10/18 8:54:57 网站建设 项目流程

医疗实体识别这个方向,我前后做了不少时间,也见过太多项目死在同一个地方:模型没调好倒在其次,把“医疗实体识别”当成一个普通NER任务来做,才是真正的大坑。它要做的事情听起来很简单——把一段临床文本里的疾病、症状、药品、检查项目、部位这些实体找出来,标成结构化字段,但真上手之后你会碰到边界模糊、嵌套实体、缩写满天飞、错别字一堆、标注口径不统一等一整套医学文本特有的问题。这篇文章我会从任务定义、技术选型、数据标注、模型训练到上线排查,把我自己的方案、踩过的坑、以及最后跑通的完整流程全部摊开讲。不管你是在做电子病历结构化、医学文本挖掘,还是想把手头临床语料变成数据资产,都值得往下看。

1. 先搞明白:医疗实体识别到底在解什么题

1.1 从一段临床文本到结构化字段,中间隔着一道高墙

很多人第一次接触医疗实体识别,是被“实体识别”这几个字骗进来的。以为和通用NER一样,把人名、地名、组织名换成疾病名、药名就行了。实际完全不是一回事。通用NER的文本相对规范,实体边界有很强的通用语言规律,而医学文本,尤其是临床病历、出院小结、检验报告,本质上是医生用最快速度记录下来的“工作语言”,天生自带三个特征:高度简写、高度口语化、高度不统一。

举个我实际处理过的例子:“患者自述有‘老胃病’,近一周胃胀伴嗳气,既往有‘高血圧’史。”这句话里“老胃病”要对应到慢性胃病,“高血圧”是错别字,实际是高血压,如果系统只按标准医学词典做字符串匹配,这两个实体直接漏掉。再比如“左肺上叶占位性病变伴肺门淋巴结肿大”,如果实体类别定义里有“症状”和“部位”,那“肺门淋巴结肿大”应该整体作为一个实体,还是分成“肺门淋巴结”和“肿大”?这个问题的答案在文档里没有,只能靠项目组自己去定。

还有更麻烦的:“右上肺腺癌根治术后”这句话,如果实体类别是“手术”,那“肺腺癌根治术”是手术实体;如果实体类别里有“疾病”,那“肺腺癌”又需要被抽出来。一个“肺腺癌”同时嵌套在另一个长实体里,是拆开标还是合并标,直接决定了模型的任务难度和评测方式。

所以我的第一个建议是:动模型之前,先花两周时间和临床、业务、下游数据使用方一起把实体类别和标注规则定死。实体类别的定义不是“参考某某论文”,而是“下游结构化表格里到底需要哪些字段”。比如你想做药品不良反应监测,那“药品”“症状”“实验室检查”就是核心标签;你想做DRG分组相关的结构化,那“诊断”“手术”“操作”这几种标签就必须拆得很细。定义越清晰,后期返工越少,模型效果也越容易做好。

1.2 为什么医疗机构比通用场景更难:边界、嵌套与上下文三重暴击

通用NER难在歧义消解,医疗实体识别难在“边界不唯一”和“嵌套关系天然存在”。这两点会让所有通用的命名实体识别方案直接失效。

边界不唯一来自医学知识本身的层级结构。以“高血压病3级(极高危)”为例,如果实体类别只定义了“疾病”,你应该标出“高血压病3级(极高危)”整条,还是“高血压病”和“3级(极高危)”分开标,后者算不算实体?从临床语义看,“3级”和“极高危”是诊断严重程度的分层,并不属于病名,但对于做患者分层的下游任务来说,它们恰恰是最有价值的信息。类似地,“2型糖尿病伴周围神经病变”,“周围神经病变”是一个并发症实体,但它和“糖尿病”有因果关系,是并列标还是内嵌标,需要标注规范给出明确规则。

嵌套实体就更常见了。拿到“小细胞肺癌化疗方案”,如果同时做“实体识别”和“关系抽取”,“肺癌”是疾病实体,“小细胞肺癌”也是疾病实体,而且“小细胞”作为修饰限定语根本不应该单独成实体。再比如“行右侧胸腔闭式引流术”,“胸腔闭式引流术”是手术实体,“右侧胸腔”是部位实体,前者嵌套了后者。通用序列标注里标准做法是给每个词一个标签,天然不支持一个词同时属于两个实体,遇到嵌套就只能做单层简化,而单层简化必然有信息损失。

再加上医学文本还有上下文依赖的问题。“患者无发热”,这里的“发热”是症状实体,但前面有个“无”字,到底是阳性症状还是阴性症状?如果只做实体抽取,“发热”会被正确标出;但如果下游需要区分“有/无”,就必须在实体抽取之后单独做否定与不确定性检测。也就是说,医疗实体识别往往不是终点,而是一条更长的信息抽取链路里的第一环,割裂地看待它,后续付出的整改成本会非常高。

2. 技术路线怎么选:规则、CRF、深度学习还是大模型

2.1 词典与正则:永远别扔掉的第一个环节

我见过不少算法同学上来就训BERT,然后把词典匹配贬低成“老古董”。但真实落地项目里,词典和规则往往是最稳、最好排查问题的部分。一个健壮的医疗实体识别系统,第一层通常是“高召回词典召回”。

具体做法是先整理一份目标实体类别的术语表,尽可能覆盖标准名、别名、缩写、英文、错别字。比如“高血压”这一条,至少要放进“高血压”“高血圧”“HTN”“essential hypertension”“原发性高血压”几个变体。然后用最长匹配、双向最大匹配、正则表达式(例如“^[0-9]+mmHg”捕捉血压值)做召回。

这里有个关键点:词典匹配的目标不是直接出最终实体,而是生成候选实体和弱标签,喂给后续模型。原因很简单,纯词典的准确率一定不够,比如“胃病”在口语里可能指消化不良,也可能指器质性胃病,必须靠上下文模型判断;但纯词典的召回率可以做到很夸张,正好补齐模型对实体变体、长尾词汇不敏感的短板。所以工程上“词典召回+模型精排”是性价比最高的组合。

还有一类简单规则值得做:针对数字、单位、剂量这类强模式实体。比如“3次/日”“10mg”“2.5cm”,用正则就能拿得很准,完全不需要模型。把这类规则单独抽出来做成一个功能模块,可以显著减少模型需要学习的目标空间,让模型把精力集中在疾病、症状这些语义实体上。

2.2 主流方案仍是序列标注:BiLSTM+CRF到预训练模型+CRF

医疗实体识别本质上是一个序列标注任务。把一段文本按字或词切分,给每个token分配一个实体标签,比如B-Disease、I-Disease、O,这就是最经典的做法。为什么这个范式能一直稳到现在?因为它天然贴合“一个token属于一个标签”的解码方式,配合CRF层能保证输出路径的合法性,训练、推理都很成熟。

早年间做医疗NER,很多人用BiLSTM+CRF,特征靠手工拼,包括词性、词典命中标记、左右邻字、前缀后缀等。这个方案在数据量小、算力有限的时代是有意义的,但到今天它的能力上限明显不够:依赖特征工程、无法充分利用上下文语义、对医学同义替换基本无解。到了BERT类预训练模型成熟之后,业界默认的基线方案基本变成“预训练模型+CRF”或“预训练模型+Softmax”。区别在于后者每个token的输出是独立的,可能出现“B-Disease后面直接跟B-Symptom”这种不合理的标签序列,而CRF通过转移矩阵约束了相邻标签关系,比如“I标签前面必须是同类型的B标签”,最终解码用维特比算法找全局最优路径。

从实测效果来看,在中等规模(几千到几万条标注文本)的数据上,“通用中文预训练模型+CRF”的F1值,通常比“BiLSTM+CRF”高出8到15个百分点。如果再换成在医学语料上继续预训练的专用模型,还能再提升2到4个点。成本无非是显存和推理时长,在绝大多数医疗场景里都是可以接受的。

2.3 大模型生成式抽取:现阶段只能做辅助拍档

ChatGPT这类大模型流行之后,很多人跑来问:医疗实体识别是不是可以直接用Prompt搞定?我的答案是:可以玩,但不建议直接上生产。大模型在少样本、复杂指令理解上有天然优势,比如你给它一段病历,让它按JSON格式输出“疾病”“药物”“症状”三个数组,它能出个像样的结果。但医疗实体识别的痛点是准确率和可控性:模型可能编造出原文根本没提到的实体,术语版本也不稳定,长病历的token开销还很高。

我实际测试过的一个结论是:在一个有1万条标注数据的中文病历任务上,微调后的BERT+CRF方案的F1值大约为88到92,而直接调用通用大模型做零样本抽取,F1通常只有60出头。即使加了精心写的Prompt和Few-shot示例,也只能到70到75左右。如果拿大模型的输出做训练数据的伪标签,再蒸馏回小模型,这个路线倒是很实用,能把人工标注从1万条降到两三千条。

所以我的选型建议是:生产系统以“词典规则召回+预训练模型+CRF”为核心,大模型作为冷启动数据生成器、难例分析器、以及实体标准化的小助手。等推理成本降下来、输出稳定性问题解决之后,再逐步扩大大模型的使用比例,这是目前最稳妥也最贴合实际的路线。

3. 数据是最大的工程:标注体系、一致性控制与类别均衡

3.1 BIO还是BIOES:标签粒度直接决定解码难度

数据标注之前先要定标签体系。最常见的两套是BIO和BIOES。BIO把每个token标记为Begin、Inside、Outside,BIOES在此基础上增加了End和Single。同样是“高血压”三个字,BIO的标注是“B-Disease、I-Disease、I-Disease”,而BIOES是“B-Disease、I-Disease、E-Disease”。如果实体是单个字“痛”,BIO里是“B-Symptom”,BIOES里则是“S-Symptom”。

区别在于,BIOES对实体边界的信息更明确,模型理论上更容易学到“E之后不再是实体内部”的规律,在边界判断上往往比BIO高一点。但代价是标签数量多,类别多的时候标签空间翻倍,收敛速度慢。我自己在医学场景下的经验是:如果实体类别少于8类、样本量充足,优先用BIOES;如果实体类别多到10类以上,或者标注时间紧张,直接用BIO,把边界问题交给后处理规则去修正。更多时候,BIO和BIOES之间的差距不会超过1个F1点,远没有标注一致性带来的影响大。

还有一个容易被忽略的细节:序列切分长度。我习惯按字切分而不是按词切分。中文医学文本里有大量未登录词和英文缩写,基于分词器切割经常把“高血圧”拆得乱七八糟,字级输入可以天然规避分词错误。对应地,标签也是字级标签,这个习惯沿用至今,效果一直很稳。

3.2 标注规范与一致性校验:最脏最累但最值得的一步

标注规范如果写得不够细,模型效果天花板就被锁死了。我在项目里遇到过最典型的争议是:伴随症状算不算独立实体。比如“恶心呕吐伴发热”,有的标注员把“恶心、呕吐、发热”标成三个症状实体,有的把“恶心呕吐”合在一起作为一个复合症状。两种标法各有道理,但如果不统一,训练数据就自带噪声,模型学到的边界就飘忽不定。

解决办法是双人标注+仲裁机制。同一份文本至少由两个标注员独立标注,然后用F1分数计算两人之间的标注一致性。如果某个类别的双人F1低于85,说明标注规则里那条定义是模糊的,需要组织标注员重新讨论、细化规则,再对已标注数据进行修正。这个过程在项目初期要反复跑,直到所有类别的双人F1都稳定在90以上,再开始大批量标注。

医学文本还有一个特殊问题:很多文本里实体边界本身带有医生的个人习惯。比如“血压偏高”到底是“血压”还是“血压偏高”是症状实体,还是“偏高”单独算指标异常?这种靠“规范描述+示例”能解决70%,剩下30%必须在一致性校验阶段通过仲裁兜底。对于实在无法统一的案例,我的原则是“宁可漏标,不可乱标”:漏标只是召回率低,乱标会把模型学歪。

3.3 少样本类别与伪标注:别让模型永远看不到手术实体

医疗实体类别分布天然不均衡。症状、体征这类实体数量巨大,而手术、解剖部位、医疗器械这类实体数量很少。直接拿原始标注数据训练,模型会对高频类拟合得很好,却把低频类几乎全部吞掉。我的经验处理手段有两个。

第一,按实体类别做下采样或加权损失。训练时每个batch尽量保证每个类别都有一定数量的样本;损失函数里给低频实体类别更高的权重,例如手术类别的损失乘1.5到2倍。第二,用词典命中+已有模型做伪标注,把低频类别的弱监督样本扩充进来,然后交给标注员修正,而不是从头标注。伪标注能极大降低人工标注工作量,但使用前提是伪标注数据只作为辅助训练集,不直接进最终测试集,否则测试分数的可信度会打折扣。

还有一个小技巧值得分享:把“跟着原文缩写的实体”和“完整写法”做等价替换来增强。比如“高血压”扩展为“HTN”“高血圧”,把“慢性阻塞性肺疾病”扩展为“慢阻肺”“COPD”,这种同义替换增强在医学领域特别有效,因为它基本不改变语义,模型又能学到更多变体。但千万不要做随意的词序打乱或跨实体替换,医学表达有固定语序,乱换可能生成一个完全错误的训练样本。

4. 实操全流程:从数据预处理到可评测的基准系统

4.1 数据准备与BIOES编码:把文本变成模型能吃的张量

我先说一种我自己最常用的标注数据格式,每行一个字符加一个标签,空行分隔句子:

右 O 上 O 肺 B-Disease 腺 I-Disease 癌 I-Disease 根 B-Surgery 治 I-Surgery 术 I-Surgery 后 O

拿到这类数据之后,第一步是把它转换成模型输入。这里我给出一个序列标注转换工具函数,它的作用是把BIOES标签序列转成实体span列表,方便后续评测:

def parse_entities(labels, tokens): entities = [] i = 0 n = len(labels) while i < n: label = labels[i] if label.startswith("B-") or label.startswith("S-"): etype = label[2:] if label.startswith("B-"): start = i i += 1 while i < n and labels[i] == "I-" + etype: i += 1 entities.append((etype, start, i)) else: entities.append((etype, i, i + 1)) i += 1 else: i += 1 return entities

和常见版本稍有不同,这个函数直接返回实体类型和起止下标,不会把“I-Disease”和“E-Disease”混在一起处理。如果你用的是BIO,就把上面的判断换成从“B-”开始、后面接连续“I-”即可。

数据划分上我的习惯是:训练集70%、验证集20%、测试集10%,而且划分前一定要检查文本来源。如果同一用户的多次就诊记录出现在不同集合里,会导致模型“过拟合”到患者的表达习惯上,线上效果被高估。所以原始数据里如果有患者ID,务必按患者维度切分,而不是随机切分文本行。

4.2 模型训练的关键参数:从加载预训练模型到微调

现在主体模型我基本直接用PyTorch配合Hugging Face生态来做。一个典型的中文医学实体识别训练回路大概是这样的:

from transformers import AutoTokenizer, AutoModelForTokenClassification model_name = "pretrained-base-model" # 通用中文或医学预训练模型均可 tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForTokenClassification.from_pretrained( model_name, num_labels=len(label_list), id2label=id2label, label2id=label2id )

这里的label_list是BIOES展开后的全部标签。如果你有6个实体类别,BIOES就是6×5+1=31个标签,BIO则是13个。不同标签数决定模型输出层的维度,代码里num_labels要仔细核对,别写错。

训练参数方面我给几组经过验证的默认值:学习率3e-5到5e-5之间,batch size 16到32,max_length 256到512,优化器用AdamW,warmup比例0.1,epoch数3到5。关键在于,医学文本较长时不要一上来就把max_length设成512,很多句子里的关键实体在前后两段,一截断就可能把手术相关的内容切掉;如果条件允许,优先把max_length设为512,推理时对超长文本再用滑窗分段。

损失函数部分,最基础的实现是每个token计算交叉熵,忽略所有padding位置的loss。如果你的实体类别很不均衡,可以给低频实体标签的loss乘以权重。但我不建议用类别权重解决高频衰弱问题,它容易让模型在验证集上分数好看,却在真实数据上疯狂把普通词汇误判成实体。优先做数据层面的均衡,损失加权只是辅助。

训练过程中的关键监控指标是“验证集span-level F1”,不是token-level accuracy。我见过太多人盯着loss曲线和准确率,loss降到一定程度后不再下降,就以为模型吃饱了,实际span F1还在爬升。所以我的习惯是每个epoch结束都计算验证集合的精确匹配F1,保存效果最好的那一版权重,最终模型就是它,而不是最后一轮epoch的输出。

4.3 解码、评测与“F1虚高”的真相

模型输出每个token的标签概率后,解码有两类常见做法。一是简单的argmax,逐token取最高分标签;二是维特比解码,基于CRF层的转移矩阵找整个序列的最佳标签路径。argmax的好处是简单、快,坏处是可能输出非法标签序列;维特比的好处是约束了B后只能跟同类型I、S后不能再接I,工程上我会优先选择维特比。即使是纯Softmax模型,也可以通过后处理规则把非法序列修正为合法序列,比如把孤立的I标签强行改成B标签。

评测指标上,必须分清楚两种分数。token-level准确率是指预测标签与真实标签逐token一致的比例,这个数字通常高得离谱,因为文本里大量token是O标签,随便预测为O都能拿高分。真正的硬指标是span-level的精确率、召回率、F1,要求预测出的实体span和真实span完全一致,包括起始位置、结束位置、实体类型,都相同才算一个正确命中。一个90%以上token准确率的模型,span-level F1可能只有80出头,这一差值很常见。

拿一个实际项目做例子,使用大约8000条标注文本、6个实体类别的实验里,我得到过这样一组结果:

方案精确率召回率F1(span-level)
词典匹配55.268.461.1
BiLSTM+CRF74.672.173.3
通用预训练模型+CRF86.784.285.4
医学预训练模型+CRF89.390.189.7

注意,这组数是按“精确匹配”算的,也就是预测实体的起止和类型必须完全一致。如果放宽到实体类型正确但边界允许左右偏移一个字,F1普遍能再高七八个点。上线前一定要问清楚业务方接受哪种评测口径,否则开发阶段满心欢喜的90分,到了验收现场可能只剩82分,这就是“F1好看但产品难用”的真相。

5. 上线前后最常踩的6个坑与排查技巧

5.1 边界整体偏移:模型总是把词头词尾吞进去

这是我碰到频率最高的问题。症状类实体尤其明显,比如“阵发性心悸”“持续性胸痛”,模型经常把“阵发性”“持续性”这类修饰语连同实体一起预测出来,或者反过来把“心悸”预测成“心悸伴”。排查思路有两步:先看是不是标注本身不统一,有些标注员把修饰语标进实体,有些没标;如果标注是统一的,再看是不是训练数据里B边界特征太弱,此时可以尝试BIOES标签和多分类CRF约束。

对于仍然出现的偏移,我在后处理里加了一个“边界修正”规则:预测span的首尾token如果属于常见修饰词表,比如“重度”“轻度”“阵发”“持续”“急性”“慢性”,就将其剥离。这个规则在实体抽取场景能拉回3到5个点的F1差,成本极低。

5.2 嵌套实体与重叠实体:后处理合并同样关键

只要实体类别里同时包含“疾病”和“手术”,嵌套实体就躲不掉。比如“胸腔镜下肺叶切除术”,按标准答案,“胸腔镜手术”是手术,“肺叶”可能是部位,“肺叶切除术”也是手术。模型在一个序列标注框架里只能给每个字一个标签,必然顾此失彼。

我的策略是针对强相关类别做“多标签头+后处理合并”。具体来说,训练两个模型:一个负责抽“手术/操作”这类粗粒度实体,一个负责抽“疾病/部位/症状”细粒度实体,推理时再把两个模型输出合并,遇到嵌套关系时用规则保留最外层实体和最有价值的内层实体。这个方法比单模型硬解嵌套稳定得多,工程上也更容易定位是哪一层出了问题。

5.3 测试集高分,真实病历表现稀烂:两个典型诱因

第一个诱因是训练数据和上线数据不一致。公开语料和真实病历的组织结构、语言风格差距很大,比如公开语料里常见“入院后查体见……”,真实病历可能是扫码录入的PDF转写文本,“见”字变成“建”,标点全乱。这种差异会让模型在“干净文本”上表现很好,在“脏文本”上全面退化。解决办法是做领域适配微调:把大量未标注的真实病历拿来,用已有模型做预测,再挑高置信度结果作为伪标签继续训练。

第二个诱因是长文本截断导致的漏实体。一篇完整的出院小结可能一千多字,max_length=256时一大半实体在截断范围之外。改进方案就是把长文本按句子或段落滑窗切分,同时保留重叠窗口,推理结束后合并相同位置的实体,取分数高的那个。注意合并时要对出现在两个窗口里的重复实体去重,否则下游会收到两条一模一样的记录。

5.4 同词不同义、同义不同词的标准化问题

医疗实体识别做到后期,拦路虎往往不是模型,而是实体标准化。模型识别出来的“阿司匹林”“拜阿司匹林”“阿司匹林肠溶片”可能是同一个药物,“慢支”“慢性支气管炎”是同一个疾病,但如果下游只拿原始字符串做统计,这些实体会被当成不同实体处理,数据质量会明显受损。所以正式上线前最好加一层实体链接或归一化:维护一张从常见变体到标准术语的映射表,配合规则和向量召回,把模型输出统一到标准词典编码。我一般用“精确映射优先,词向量相似度召回兜底”的组合,映射表命中不到时用相似度大于0.8的候选取最高频标准名。

5.5 常见问题速查表

症状可能原因排查方向
span-level F1一直低于80标注一致性差、实体类别定义模糊重算双人标注F1,细化标注规范
低频实体类几乎不识别类别不均衡按类别的batch采样、伪标注扩充
预测实体首尾偏移BIO/BIOES粒度不足、修饰词边界切BIOES、加边界修正规则
嵌套实体丢失单标签序列标注不支持重叠分类别建模、后处理合并
线上实体漏掉明显词词典召回没覆盖变体补充同义词表、缩写表
长文本后半段全漏输入截断滑窗分段、提升max_length
输出结果不稳定解码没有约束改用CRF+维特比

5.6 上线后的监控:别只看总量指标

系统上线别只看整体F1。医疗实体识别需要按实体类别拆分监控,比如手术类F1下降5个点,可能原因是最近输入的数据里出现了一种新手术术式的写法。我建议在监控面板上按疾病、症状、药品、手术、部位、检查六类分别展示F1和置信度分布,这样一旦出现业务投诉,能马上定位是哪个类别问题、哪一类文本导致的。

另一个容易被忽略的逻辑是置信度阈值不能一刀切。不同实体类别的可接受置信度本来就不同,例如“药品”类错误代价高,阈值可以调到0.9;而“部位”类可接受一定误召回,阈值0.7就够。根据每类实体的错报成本分别设阈值,比统一调一个全局阈值要合理得多。

我个人实际做下来,医疗实体识别项目能不能成,70%的功夫都在数据定义和工程链路上,模型本身反而是最成熟、最不容易出问题的一环。先把实体类型和下游字段对齐,再搭好词典召回和序列标注的双层链路,然后认真做一致性校验和评测口径管理,这个系统即使模型不换,分数都能涨一截。后面如果还想再扩,就把实体标准化、否定检测、时间属性抽取一层层加上去,一个能做结构化病历的信息抽取管线就已经初见雏形了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询