ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

医疗实体识别落地实战:病历文本如何变为结构化数据

医疗实体识别落地实战:病历文本如何变为结构化数据

上个季度我接手了一个实际落地需求:把病历里一段段非结构化描述,自动拆成“疾病、症状、药物、检查”几类可统计的字段。一开始我以为这只是个普通的文本抽取问题,真正做进去才发现,医疗实体识别(Medical Entity Recognition,很多团队也直接叫医疗NER)是个典型的“看着简单、做起来处处是坑”的任务。这篇文章不打算写成教科书,而是想把我在这个方向从选型、标注到训练、部署的完整过程拆给你看,尤其是那些没有写在论文里的坑。

1. 医疗实体识别在真实业务里管什么用

1.1 先从一句出院小结说起

如果你没有处理过原始病历数据,可能会觉得文本抽取是件很自然的事。举个例子,出院小结里常出现这样一句话:

患者因“反复咳嗽、咳痰伴发热一周”入院,查体双肺呼吸音粗,胸部CT提示右上肺磨玻璃结节。

这句话医生看得懂,但你要是把它塞进结构化数据库,就得先回答几个问题:“咳嗽”和“咳痰”是症状;“发热”是症状;“双肺呼吸音粗”是查体发现;“右上肺磨玻璃结节”是影像表现。医疗实体识别做的事情,就是把这些具有特定医学语义的片段在原文里标出来,再贴上类型标签,形成类似“文本片段+类型+位置”的结构化数据。

很多医院信息科、医疗数据公司、保险理赔系统,本质上都需要这层能力。病案首页的自动填充、临床科研队列的筛选、辅助诊断系统的输入标准化,全都要靠这一层把自由文本变成机器可用的字段。所以说医疗NER不是什么锦上添花的炫技,而是一大批医疗信息化系统的地基。

1.2 一个任务定义:从语言到结构化字段

从技术角度看,医疗NER是序列标注问题。输入是一句话,输出是每个token的标签,例如“B-Symptom”“I-Symptom”“O”。模型的任务是给每个字或词分配一个位置和类别都正确的标签。注意这里有两个维度:一是边界,要从“右上肺磨玻璃结节”里正确划出完整的实体范围;二是类型,要判断这个实体属于疾病还是影像表现。

这跟通用NER(识别人名、地名、机构名)最大的区别在于:医疗实体类型高度专业化,而且文本充满了说明性的修饰语。一个没有医学背景的标注员,可能连“磨玻璃结节”到底算影像表现还是疾病都说不清。所以医疗NER项目的成败,往往在标注规范阶段就已经决定了大半,而不是在模型阶段。

1.3 下游场景和用户

医疗实体识别不是终点,而是管道中的一环。真实业务里常见的下游使用方式有三种:

  • 病历质控系统:抽取出诊断、手术、用药等关键实体,再与诊疗规范对比,检查是否存在漏诊、错写等问题。
  • 临床科研数据提取:从几千份出院小结里自动筛出满足纳排标准的患者,比如“所有使用了某类药物且影像提示结节的病例”。
  • 医疗文本检索与问答:先通过实体识别把非结构化文本转成结构化索引,再做精确检索。

如果你只是在校验集上跑出来一个很高的F1分数,但不知道下游要用这些实体做什么,项目基本会死在交付阶段。反过来,先想清楚下游需要哪些字段,再反推标签体系,项目会顺利很多。

2. 为什么通用NER那套直接搬到医疗行业行不通

2.1 医疗文本不是标准书面语

通用NER的很多技巧依赖相对规范的语言环境:新闻有完整句子,社交文本虽然随意但实体边界通常比较清楚。医疗文本完全不是这样。一份真实的病历里可能有大量语序颠倒、缺主语、无标点的压缩表达:“咳嗽咳痰3天伴发热,自行口服阿莫西林效果不佳。”这句话里“阿莫西林”之前连“服用”都省略了。

更麻烦的是时序和否定信息。医生写“无发热”“未见明显阳性体征”,一个通用NER模型可能把“发热”当成正常实体抽出来,但下游做症状统计时就会把阴性描述当成阳性事件,导致数据失真。医疗文本里的每个实体往往还附带属性:部位、程度、时长、阴阳性。通用NER的“实体类型+位置”输出格式根本接不住这种复杂度。

2.2 标注成本被严重低估

通用NER可以众包标注,普通人分得清“北京”是地名、“张三”是人名。医疗实体识别如果真按众包来做,质量会惨不忍睹。“阿托伐他汀钙片”是药物,“冠脉粥样硬化”是疾病诊断,“心前区不适”是症状,这些需要医学背景才能准确判断。

实际操作中,一个合格的医疗文本标注团队至少需要三重保障:一位有临床经验的成员负责制定规则和最终仲裁,几个经过培训的标注员做初标,再加上定期的抽检和一致性计算。这样一支团队一个小时可能只能标注几十个句子。成本高之外,隐私合规也切掉了一大块外包通道——原始病历不能随便出医院。项目的组织方式往往是被合规问题提前锁死的。

2.3 医学知识密度和实体变体

同一个疾病,在病历里可能写作“冠心病”,也可能写作“冠状动脉粥样硬化性心脏病”“CHD”“冠脉病变”。通用NER可以用词典匹配兜底,但医疗场景里实体变体极多,同义词关系复杂,还涉及层级体系——轻症肺炎和重症肺炎、小叶性肺炎和大叶性肺炎之间存在本体关系。单一模型如果缺乏领域知识,很容易把“肺炎”和“间质性肺炎”当成完全无关的两个实体。

所以医疗NER的完整方案通常不是单模型,而是一个“预训练语言模型+医学词典+规则后处理”的混合管道。模型负责理解上下文和捕捉变体,词典负责找回模型漏掉的标准化名词,规则后处理负责否定、程度、剂量等属性。只靠其中任何一个都会瘸腿。

3. 一套能落地的医疗NER链路怎么搭

3.1 标签体系设计是第一步

我建议标签体系不要照着公开数据集抄,因为公开数据集的类型设计不一定匹配你的业务。某公开数据集把实体分成疾病、药物、手术、实验室检验,但你实际要处理的是影像报告,那么你需要的是病灶描述、部位、影像征象,而不是手术。直接在业务语料上重新设计体系更靠谱。

设计时要遵守几个原则:第一,类型数量不要贪多,我见过有人一上来定了20多个类型,结果标注一致性掉到0.6左右,模型也分不清“病理结果”和“实验室检查”的边界。先从8个以内的核心类型开始,跑通再扩展。第二,每个类型都要有明确的边界示例。例如“右上肺磨玻璃结节”,若算作“影像所见”,那“磨玻璃”算不算实体?“结节”算不算?规范必须写清楚最小标注单元。第三,要考虑否定、疑似等修饰,是做成额外标签维度,还是后处理规则,一开始就要定。

我用的一个比较稳的类型表达是:

Symptom(症状) Disease(疾病诊断) Drug(药物) Examination(检查项目) Finding(检查所见/查体发现) Procedure(手术/操作)

同时给每个实体附带可选属性:negation(否定/阳性)、uncertainty(疑似/排除)、body_part(部位)、temporal(时间)。这样输出既满足下游统计,也不会给标注带来过重的负担,属性可以后续规则补充。

3.2 数据准备和标注质量检查

训练语料不可能全靠人工从零标注,成本太高。通用的做法是先用词典加规则做预标注,把能命中的实体先标记好,然后让人工在预标注结果上修改。医学词典可以从公开的药品说明书、ICD-10编码库、症状词表里整理,再经过同义词清洗。预标注最大的作用是提升效率,医生抄写的文本里大量标准术语能直接命中,人工只需要处理模型没标对的部分。

标注质量控制方面,我强烈建议做两件事:一是标注一致性计算,拿同一批文本让两个标注员分别标,算双方实体边界和类型完全吻合的比例,低于某个阈值就说明规范有问题,需要重新培训;二是每周抽检一批已标数据,由有临床背景的人复核。实测中,很多问题不是标错,而是标得不一致——一个人把“高血压”标为Disease,另一个人标为Symptom,模型训练时就会互相打架。

3.3 模型选型与训练配置

这个方向的模型演进大致经历了三个阶段:早期是词典匹配和规则,后来是条件随机场(CRF),再后来是BiLSTM-CRF,现在的主流是预训练语言模型加CRF解码层。

CRF在这里面的作用是很多人容易忽略的:它是一个序列解码层的约束器,能保证预测出的标签序列在逻辑上合法。比如“I-Drug”前面必须是“B-Drug”或“I-Drug”,“O”后面不能直接跟“I-Symptom”。有了CRF,模型不会输出“症状到一半突然跳到药物”这种荒谬的标签序列。它像一个巡逻的保安,检查整条生产线上的标签顺序是否合规。

如果你数据量在几千条句子这个级别,我推荐的做法:底座用中文通用预训练模型,最好再找一个在生物医学语料上继续预训练过的版本(这类模型通常叫“生物医学领域预训练BERT变体”);编码层接一个线性分类器;解码用CRF。训练配置可以这样起步:

lr: 5e-5 batch_size: 16 max_seq_len: 128 epochs: 15 warmup_ratio: 0.1

同时加入早停策略,监控验证集上的实体级F1分数。这里注意,只看loss是远不够的,loss下降不代表实体边界准确。医疗NER里常见的情况是训练LOSS已经很低,但实际抽出来的实体整段偏移——“右上肺结节”被抽成“肺结节”,下称需求下,这种边界错误和完全没抽出来一样严重。

3.4 评估指标的选择

通用NER经常用token级别的准确率,即每个token的标签是否正确。但医疗场景直接用实体级别更合理:一个实体边界错一个字,就算整段错误。我用实体匹配的精确率、召回率和F1做主线指标,同时把边界从宽匹配(实体类型对、重叠即算对)和严格匹配(边界完全一致)分开统计。从宽匹配告诉你模型是否找对了位置,严格匹配告诉你模型是否找准了边界,两个分数的差距越大,说明边界模糊问题越严重。

还需要特别注意数据划分。如果是病历数据,必须保证同一患者的所有文本只出现在训练集或测试集里,不能既出现在训练集又出现在测试集。否则模型可能靠患者特有的写作习惯“背下”答案,测试分数虚高。我当时就吃了一个亏,用句子级随机切分时F1到0.91,换成患者级切分后直接掉到0.82,后者才是真实水平。

4. 我踩过的几个坑,每个都是标注和模型的双重陷阱

4.1 品类界的“垃圾桶效应”

第一次设计标签体系时,我加了一个叫“Other(其他)”的类别,想着把不属于任何已知类型的医学短语都收进去。结果这个类别变成了垃圾桶,模型倾向于把所有识别不清的片段都标成Other。标注员也苦不堪言,因为规范里没写清哪些算Other,只能凭感觉。后来我把Other取消,所有不属于核心类型的实体一律不标,只标O。模型指标反而涨了好几个百分点。

这个坑的教训是:不要给模型留模糊地带。一个实体要么明确属于某个类型,要么不标,任何“或者都行”的设计都会变成标注噪声和训练噪声。

4.2 否定和不确定性处理

病历里充满“未见明显异常”“无发热”“排除了脑梗死”。如果不处理否定词,模型抽出“发热”“脑梗死”后,下游队列筛选用这些实体筛出来的病例全是错的。我最早试过用规则后处理,比如在实体前面若干字节内匹配“无”“未见”“排除”等词,然后给实体打上否定标记。这个方法简单但不够:否定词和被否定的实体之间可能隔着很长一段修饰语,比如“患者目前无头痛恶心呕吐等症状”,否定词“无”后面跟了三个症状。

后来我把这个问题建模成两个任务:先做实体识别,再做实体级别的属性分类。属性分类的输入是实体上下文窗口,输出是否定/肯定/疑似,这样模型能学到整个上下文里否定词的长距离作用。效果比纯规则好很多,代价是标注体系里追加了一个维度。

4.3 缩写歧义与多科室语境

医疗缩写歧义是模型部署时最容易被低估的问题。同样的缩写,在呼吸科是“肺栓塞”,在普通科室可能指“体格检查”。我遇到过最典型的一个词是“PE”,一篇报告里是“体格检查未见异常”,另一篇里是“PE(肺栓塞)可能性大”。同一个词,两类完全不同的实体,通用模型默认按训练集里的高频分布去猜,结果必然导致高频一类胜出,低频一类彻底丢失。

我用了两个手段缓解:一是在模型输入里额外拼接科室信息,把科室类型作为文本特征喂进去,帮助模型把“呼吸科+PE”和“体检报告+PE”区分开;二是为高频缩写维护一份带科室标签的消歧词典,在模型输出后做强制修正。混合方案的效果比单纯挣扎调参好得多。

4.4 边界判断不一致带来的模型噪声

一个很容易被忽略的点是标注员的边界标准不一致。同一份病历,“上呼吸道感染”有人标成“呼吸道感染”,有人标成“上呼吸道感染”;“右上肺结节”有人把“右上肺”含进去,有人只标“结节”。训练数据里有这两类答案,模型学到的是噪声,最终输出也会摇摆不定。

我的解决方案是写一份边界规范,核心规则就是“部位修饰词是否包含在实体内,取决于下游需要不需要这个部位信息”。如果下游统计按部位拆解,那“右上肺结节”整体作为“Finding”更合适;如果只关心疾病本身,那“结节”作为实体体型最小单元。规范定了之后,还要在标注工具里给标注员看示例,尤其标注示例数至少覆盖每个类型的5种边界变体。标注一致性在规范修订后明显回升,模型严格匹配的F1也同步涨了。

4.5 嵌套实体和组合词

医疗文本里嵌套结构特别多:“左肺上叶见磨玻璃影”,如果要求抽部位实体(左肺上叶)和影像实体(磨玻璃影),同时保留组合关系,扁平NER就没法做。类似问题在药物实体上也有:“阿莫西林胶囊0.5g口服”这类文本里,药物名和剂量是一体的还是分开的?如果拆成“阿莫西林胶囊”和“0.5g”,下游用药统计又得重新判断剂量是谁的。如果不拆,补充药品库标准化时药物成分又没法提取。

对于嵌套问题,我当时的做法是给实体类型增加组合粒度。比如“药物使用”实体里允许包含“药物成分”+“剂量”子实体;“影像所见”实体里允许包含“部位”+“所见描述”子实体。模型先抽粗粒度实体,再在实体内部做细粒度分类。这相当于把嵌套识别改成两阶段管道,实测比用单模型硬解嵌套可靠。

4.6 脏文本和OCR错误

出院小结、入院记录很多来自扫描件或语音转录。扫描件的OCR结果经常把“CT”识别成“C丁”,把“血常规”里的汉字识别成相似字形。语音转录更是惨不忍睹,医生口述“同型半胱氨酸”,转录出来可能变成“同心半光氨酸”。模型对这类错误非常敏感,一个“C丁”就足以让“C丁扫描”这个实体完全消失。

我的对策是三层:首先,在数据进入OCR前尽量用高质量的医疗报告系统直接导出的结构化文本流转渠道;其次,在预处理管线里加一个面向医学词汇的纠错词典,把高频OCR错误映射回正确术语;最后,训练时故意在文本中注入少量字符扰动模拟噪声,让模型对偶发错字有一定容忍度。医疗文本质量参差不齐,“开箱即用”的模型基本都会在脏数据上崩掉,这条必须自己处理。

5. 从训练到部署,还需要迈过这几道坎

5.1 推理速度和资源预算

医疗场景的部署环境大多不是高性能GPU服务器,尤其是医院内网的服务器,可能只有CPU资源。一个标准的预训练模型在CPU上推理一句病历可能耗时几百毫秒到数秒不等。企业内部的病历批量导入虽然不要求毫秒级响应,但也不会接受几百万份出院小结跑上一周。

我的建议是分两级处理:第一级用规则和词典快速过滤,能命中的直接出结果,减少模型的输入量;第二级把模型压缩成量化版或蒸馏版再部署。实测中,量化到8位整数后模型体积降成四分之一,推理速度翻倍,实体F1掉不了太多,业务上完全可接受。如果你有几十万条标注数据,还可以直接训练一个小尺寸蒸馏模型,效果接近大模型但速度快到秒级。

5.2 词典后处理与标准编码映射

实体识别只是拿到了“看到的词”,但下游业务往往还要求把实体映射到标准编码。比如识别出“冠脉支架植入术”还不够,需要映射到手术编码库里的对应条目,才能入病案系统。这一层叫实体标准化或链指,在医疗场景里几乎是必须的。

我的标准做法是:实体识别模型输出原文片段,然后按实体类型去对应标准库做匹配。匹配时优先精确匹配,其次用同义词表做模糊匹配,还不行就进入人工审核队列。初期这个审核队列会比较大,但跑一段时间后,高频实体的标准化规则积累出来,队列会越来越小。医院数据的特点是长尾极多,标准库根本覆盖不完,所以一定要留一个兜底的人工审核通道,而不是试图追求百分之百自动标准化。

5.3 模型迭代和反馈闭环

医疗数据是不断更新的,新药、新疾病、新诊断写法一直在出现。一个上线后不迭代的NER模型,三个月后效果就会明显退化。我见过太多项目上线后就没有动静了,原因不是不想迭代,而是缺一个反馈闭环。

我的建议是从第一天就建好“线上错误收集机制”:每一条进入系统的文本,如果人工修正过结果,修正记录自动进入待标注库;每过一段时间,从错误库里抽样一批数据,人工审核后增量进训练集,然后做增量训练。整个过程不需要大规模重标,更重要的是错误的修正操作本身就在持续产生高质量训练数据。这样形成一个运转小闭环,模型才活得久。

5.4 数据安全与隐私底线

医疗数据是受严格管制的数据,这一点从项目立项第一天就得刻在脑子里。原始病历不能随意发送到第三方云端标注平台,不能出现在外部大模型API的调用日志里。我见过一个项目为了快速出效果,把科室内部的小批量数据传到一个公开标注平台,结果直接踩了合规红线,整个项目停摆。

合规上要注意的几条硬规矩:数据流转尽量在院内网和受控环境内完成;外包标注只能使用脱敏后的文本,患者姓名、身份证号、联系方式必须去除;模型推理最好部署在本地环境,不要让原始文本经过外部服务;所有干预和处理日志要留痕,方便审计。这不是形式主义,医疗信息化项目的验收里,数据安全往往是比模型F1更优先的检查项。

最后分享一个我现在还在用的习惯。每次接到新的医疗文本类型,我都不急着训练,而是先随机抽50条原始文本,手工把实体标一遍,再和标注团队开一次规范讨论会。这个动作看起来浪费时间,实际上会把后面整个项目的返工量压到最低。医疗实体识别这个方向,模型已经不太是瓶颈,真正决定项目成败的,永远是标注规范、类别设计和部署闭环这些“看不见的功夫”。把这层打好,后面每一个环节都会顺很多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进