Design and Implementation of a Batch Semantic Analysis Automation Tool for Electronic Medical Records — A Hands-on Blog
关键词:电子病历;命名实体识别;语义分析;临床自然语言处理;自动化工具;Flask;正则抽取
写在前面:我为什么要写这篇长文
如果你在医疗信息化、临床科研,或者单纯对"怎么让机器读懂病历"感兴趣,那么这篇文章大概率对你有用。
事情的缘起很简单。几个月前,我手头积攒了一批中文电子病历(EMR),少说几十份,多则上千份,全部是自由文本或半结构化文本——主诉、现病史、既往史、查体、诊断、处置,洋洋洒洒几段话,信息密度极高,但机器根本读不懂。我想做几件事:把里面的疾病、症状、用药、检查都抽出来;按科室归个类;看看哪些病历病情危重需要优先处理;最好还能给每份病历自动写一句话摘要;最后把这些结果做成图表,一眼看清这批病历的整体画像。
市面上不是没有工具。深度学习模型精度高,但训练要标注语料、部署要吃显卡,对一个想快速验证想法的人来说门槛不低;商用系统功能全,但价格不菲、还要走采购流程;开源脚本倒是不少,可大多只解决"抽实体"这一个点,缺界面、缺批量、缺可视化。
于是我动手做了一个轻量、零外部依赖、即开即用的原型:一个基于领域词典与正则模式的电子病历批量语义分析自动化工具。核心引擎只用 Python 标准库,前端是原生 HTML/CSS/JS 配 Chart.js,后端是 Flask。它能批量读入病历、自动抽实体、分科室、评严重度、写摘要、出图表、导结果。
这篇文章不是一篇八股论文,而是一篇能跟着动手的博客:我会把设计思路、模块实现、完整代码、实验结果、逐份病历案例、部署教程、与其他方案的对比,以及踩过的坑,全都摊开来讲。如果你只想看结论,可以跳到最后的"结论与展望";如果你想自己复现,跟着"部署与运维指南""使用教程"两步就能跑起来。
目 录
写在前面:我为什么要写这篇长文
第 1 章 为什么需要电子病历语义分析
- 1.1 医疗数据的"富矿"与"荒原"
- 1.2 电子病历的真实结构与被低估的痛点
- 1.3 临床自然语言处理能带来什么
- 1.4 本文要解决的三个具体问题
- 1.5 本章小结
第 2 章 工具总览:它能做什么
- 2.1 一句话定位
- 2.2 功能全景清单
- 2.3 一张图看懂工作流
- 2.4 适合谁 / 不适合谁
- 2.5 技术栈一览
- 2.6 本章小结
第 3 章 系统总体设计
- 3.1 设计目标与原则
- 3.2 总体架构(分层图)
- 3.3 技术选型与理由
- 3.4 实体类型配色与词条规模
第 4 章 医学实体知识库详解
- 4.1 七大类实体:设计哲学
- 4.2 词条来源与选取三原则
- 4.3 跨类别术语的妥善处理
- 4.4 科室关键词与严重度词表
- 4.5 如何扩充你自己的知识库
第 5 章 实体识别引擎:从正则到工程
- 5.1 为什么是规则化而不是深度学习
- 5.2 长词优先策略
- 5.3 预编译与性能优化
- 5.4 结构化与扁平化双路输出
- 5.5 引擎核心代码片段
第 6 章 科室分类模块
- 6.1 关键词加权评分机制
- 6.2 一个真实的"合并症干扰"案例
- 6.3 改进思路
第 7 章 严重程度评估模块
- 7.1 三级关键词阈值
- 7.2 0–100 评分公式
- 7.3 可视化呈现
第 8 章 自动摘要生成
- 8.1 模板填充而非生成
- 8.2 为什么宁可"死板"也不要"幻觉"
第 9 章 批量分析与可视化系统
- 9.1 批量处理流程
- 9.2 跨病历统计聚合
- 9.3 仪表盘布局与图表
- 9.4 详情页高亮与自定义分析
第 10 章 完整代码走读
- 10.1 实体字典 entities.py
- 10.2 分析引擎 analyzer.py
- 10.3 后端服务 app.py 关键路由
第 11 章 实验与逐份病历案例
- 11.1 实验数据与设置
- 11.2 批量分析结果汇总
- 11.3 12 份病历逐份解读
- 11.4 性能与准确性讨论
第 12 章 与主流方案的对比
- 12.1 规则化 vs 深度学习 vs 商用
- 12.2 一张对比表看懂取舍
第 13 章 部署与运维指南
- 13.1 环境准备
- 13.2 启动服务
- 13.3 接入你自己的病历数据
- 13.4 二次开发指引
第 14 章 使用教程:五分钟跑通
第 15 章 讨论与局限性
- 15.1 规则化方法的适用边界
- 15.2 与深度学习方法的互补关系
- 15.3 其他工程局限
第 16 章 结论与展望
第 17 章 附录
- 17.1 常见问题 FAQ
- 17.2 术语表
- 17.3 参考资源与文献
- 17.4 免责声明
第 1 章 为什么需要电子病历语义分析
1.1 医疗数据的"富矿"与"荒原"
医院每天产生的数据里,有一类是结构化的:化验单的数值、生命体征的曲线、医保结算的字段,它们整齐、标准、易于计算。但还有一类——也是占比极大的一类——是自由文本:门诊病历里医生写的一段段叙述,住院病历里洋洋洒洒的病程记录,会诊意见里夹叙夹议的讨论。
这类文本是一座"富矿"。一份普通的出院小结,可能包含主诊断、十余个既往诊断、一长串用药、若干检查结果,以及医生对病情转归的判断。如果把这些非结构化信息全部变成结构化字段,你就能做很多事:按疾病统计科室负荷、追踪某种药物的使用趋势、筛查高危患者、构建科研队列、做病种质控、自动生成上报材料……
但它同时是一片"荒原"。富矿之所以是荒原,是因为机器读不懂它。你没法在 SQL 里SELECT disease FROM emr WHERE severity > 80——因为disease和severity压根不存在于数据库里,它们埋在一段中文叙述的某个句子中间。要从这片荒原里挖出金子,靠人肉阅读是行不通的(量太大),靠简单的关键词搜索也不行(同义词、缩写、表述变体太多)。于是,"临床自然语言处理"这门技术就成了连接富矿与荒原的桥。
1.2 电子病历的真实结构与被低估的痛点
我们先把电子病历的结构摊开看。一份典型的门诊/住院病历,至少包含以下字段:
- 主诉:患者本次就诊最主要的症状及持续时间,例如"反复胸闷 3 年,加重伴气短 1 周"。
- 现病史:本次疾病发生、发展、诊治的全过程。
- 既往史:过去的健康状况、慢性病、手术史、过敏史。
- 体格检查:医生查体所见,包括生命体征与专科查体。
- 辅助检查:化验、影像、超声等结果。
- 诊断:门急诊或出院诊断,常分主要诊断与次要诊断。
- 处置 / 医嘱:用药、手术、会诊、随访等。
看起来字段清晰,对吧?但痛点恰恰藏在"清晰"之下:
- 术语不统一。同样是"高血压",可能写作"高血压病"“原发性高血压”“HBP”“HTN”;同样是"心肌梗死",可能写"心梗"“AMI”“急性心肌梗死”。
- 表述高度自由。同样一个"糖尿病",有人写"2 型糖尿病多年",有人写"糖尿病史 10 年,口服二甲双胍控制可",有人写"发现血糖升高 5 年"。
- 缩写与口语化。“房颤”“室上速”“呼衰”"肾衰"随处可见。
- 否定与不确定表述。“否认肝炎结核史”“未见明显积液”“不排除肿瘤可能”——这些表述如果不做语义处理,很容易把"未见"当成"见"。
- 多诊断并存。一份病历往往主诊断之外还挂着三五个既往诊断,且分不清主次。
这些痛点叠加起来的结果是:你手里明明有一座金矿,却只能用"人眼 + Ctrl+F"这种原始工具去挖。这正是本文工具的出发点。
1.3 临床自然语言处理能带来什么
临床自然语言处理(Clinical NLP)是医学信息学与自然语言处理的交叉领域,目标是从临床文本中抽取、组织并推理医学知识。它通常包含这样一串任务:
- 命名实体识别(NER):把文本里的"疾病"“症状”“药物”“检查”“手术”“部位”"指标"等实体框出来。
- 关系抽取:识别实体之间的关系,例如"药物 A 用于治疗 疾病 B"“症状 C 发生于 部位 D”。
- 文本分类:给整篇病历打标签,例如科室、病种、是否手术、是否危重。
- 否定/不确定检测:判断某个实体是"存在"还是"被否定/不确定"。
- 摘要生成:把长篇叙述压成核心信息。
本文工具聚焦于其中最基础也最实用的一层——实体识别 + 文本分类(科室/严重度)+ 摘要,并把它们打包成"批量 + 可视化 + 可导出"的完整链路。换句话说,它不追求做全所有 Clinical NLP 任务,而是把"从病历文本到结构化结果"这条最刚需的主线做扎实。
1.4 本文要解决的三个具体问题
把上面说的揉成三个具体问题,也就是本文工具要回答的:
- 怎么把一份病历"拆解"成结构化字段?——通过实体识别引擎,把疾病、症状、用药、检查等七大类实体从叙述中抽出来,并能在原文上高亮标注。
- 怎么给一批病历"画像"?——通过科室分类、严重度评估、批量统计聚合,让管理者从宏观层面看清这批病历的科室分布、病情梯度、高频病种与用药规律。
- 怎么让人"一眼看懂"并"拿去用"?——通过 Web 仪表盘、详情页高亮、自定义即时分析,以及 JSON/CSV 导出,把结果从"机器输出"变成"人能用的资产"。
这三个问题,分别对应工具的三层价值:结构化(看得清)、聚合化(看得全)、可用化(拿得走)。
1.5 本章小结
本章交代了写作缘起与问题背景。医疗自由文本既是富矿也是荒原,痛点在于术语不统一、表述自由、缩写口语化、否定表述、多诊断并存。Clinical NLP 是挖矿的桥,而本文工具聚焦最刚需的"实体识别 + 分类 + 摘要",并以"批量、可视化、可导出"的形式交付。下一章,我们直接看这个工具长什么样、能干什么。
第 2 章 工具总览:它能做什么
2.1 一句话定位
一个零依赖、即开即用的中文电子病历批量语义分析自动化工具:上传一批病历,自动抽实体、分科室、评严重度、写摘要、出图表、可导出。
注意三个关键词:零依赖(核心引擎只用 Python 标准库,不装 PyTorch/TensorFlow)、即开即用(一条命令起服务,浏览器打开就能用)、批量(不是一次分析一份,而是一批一起分析并聚合统计)。
2.2 功能全景清单
逐项列一下工具的能力边界,避免你产生不切实际的期待:
| 能力 | 说明 | 输入 | 输出 |
|---|---|---|---|
| 实体识别(NER) | 抽取 7 类医学实体 | 病历文本 | 带类别与位置的实体列表 |
| 实体原文高亮 | 在病历原文上彩色标注 | 实体 + 原文 | HTML 高亮片段 |
| 科室分类 | 12 个科室关键词加权推断 | 全文字段 | 推断科室 + 得分明细 |
| 严重程度评估 | 危重/重度/中度/轻度四级 | 全文字段 | 等级 + 0–100 评分 |
| 自动摘要 | 主诉/诊断/用药/检查四槽位 | 字段 + 实体 | 一行紧凑摘要 |
| 批量分析 | 一次分析全量病历 | 病历集合 | 逐条结果 + 缓存 |
| 跨病历统计 | 分布/高频/均值聚合 | 逐条结果 | 统计指标集合 |
| 可视化仪表盘 | 4 卡片 + 4 图表 + 3 列表 | 统计指标 | 网页仪表盘 |
| 详情页 | 左原文高亮 + 右信息栏 | 单条病历 | 详情网页 |
| 自定义分析 | 粘贴任意文本即时分析 | 自由文本 | 即时结果 |
| 结果导出 | JSON 全量 / CSV 摘要 | 分析结果 | 下载文件 |
可以看到,工具覆盖了"从原始文本到可用资产"的完整链路,而不是只做其中一环。
2.3 一张图看懂工作流
病历数据 (JSON / 自定义文本) │ ▼ ┌──────────────────┐ │ 实体识别引擎 │ 7 类词典 + 正则预编译 └──────────────────┘ │ 实体(结构化 + 扁平化) ▼ ┌──────────────────┐ ┌──────────────────┐ │ 科室分类(加权) │ │ 严重度评估(分级) │ └──────────────────┘ └──────────────────┘ │ │ ▼ ▼ ┌──────────────────┐ │ 自动摘要(模板) │ └──────────────────┘ │ ▼ ┌──────────────────┐ │ 批量统计聚合 │ 科室/严重度/实体/高频 └──────────────────┘ │ ▼ ┌────────────────────────────────────┐ │ 表现层:仪表盘 / 详情 / 自定义 / 导出 │ └────────────────────────────────────┘整条流水线是无状态的纯函数式处理:给定一份病历,经过"识别 → 分类 → 评估 → 摘要 → 统计"五个环节,产出一份结构化结果。批量只是把单份处理套了一层循环,再做一次聚合。
2.4 适合谁 / 不适合谁
适合:
- 医院科室、质控部门想快速对一批病历做结构化摸底的;
- 临床科研团队要构建科研队列、做病种统计的;
- 缺少深度学习工程能力,但想先跑通原型的个人或小团队;
- 想把这个工具作为 AI Agent 工作流里"语义处理组件"的开发者。
不适合:
- 追求高精度、要对接生产级 ICD 编码、需要模型持续迭代的大型系统(请直接上深度学习方案);
- 需要否定检测、关系抽取、时序分析等高阶语义能力的场景(本文工具暂未实现);
- 对隐私合规、权限管理、审计有强要求的正式生产环境(本工具为原型,未含鉴权)。
一句话:它是"快速验证想法"和"轻量辅助"的利器,不是"替代专业医疗 AI 系统"的银弹。
2.5 技术栈一览
| 层 | 技术 | 说明 |
|---|---|---|
| 核心引擎 | Python 3 + 标准库(re / json / collections) | 零第三方依赖 |
| 后端 | Flask 3.x | 轻量 Web 框架 |
| 前端 | 原生 HTML / CSS / JavaScript | 无构建工具 |
| 图表 | Chart.js | CDN 引入,环形/柱状图 |
| 存储 | JSON 文件 | 易读易改,可换数据库 |
技术选型的核心逻辑是"能少依赖就少依赖"。核心引擎只用标准库,意味着你可以在任何装了 Python 的机器上直接跑分析逻辑,不必担心环境冲突;Flask 负责把引擎能力暴露成网页和 API;前端三件套保证零构建、好维护。
2.6 本章小结
本章把工具的定位、功能清单、工作流、适用边界和技术栈一次性讲清。它是一个"零依赖、即开即用、批量处理"的轻量工具,覆盖从实体识别到可视化的完整链路,适合快速验证与轻量辅助,但不是生产级医疗 AI 的替代品。下一章进入系统总体设计,看它内部是如何分层的。
(本文未完,第 3 章起见后续章节。)
第 3 章 系统总体设计
如果说第 2 章是"这个工具长什么样",那本章就是"它内部是怎么搭起来的"。设计阶段最重要的不是写代码,而是想清楚原则、结构与取舍。
3.1 设计目标与原则
工具在动手前先确立了五条设计原则,它们像一把尺子,后面每个模块的取舍都用它来量:
批量自动化
支持一次性导入并分析多份病历,自动完成从原始文本到结构化结果与统计报告的全流程,减少人工干预。这里的关键词是"全流程"——不是分析完丢给你一堆 JSON 就完了,而是连统计、图表、导出都一并做好。
多维度语义提取
不只抽实体,还提供科室分类、严重度评估、自动摘要,形成对一份病历的"多层次语义画像"。单一维度的抽取往往不够用:你知道一份病历有"高血压、糖尿病",但不知道它该归哪个科、危不危重、主线是什么。多维度才能画像。
可解释性优先
所有抽取结果都对应明确的词典词条或关键词规则,用户能追溯每个实体、每个判定的来源。这一点在医疗场景尤其重要——你不能告诉医生说"模型觉得这病历危重",却不给理由。规则化方法的天然优势就是"白盒"。
轻量零依赖
核心引擎只用 Python 标准库,不引入深度学习框架,普通计算环境即开即用。零依赖带来的好处不仅仅是部署省事,更是可移植性与可审计性:别人拿到代码,不需要配环境就能读懂、能改、能跑。
可视化与可导出
提供 Web 仪表盘与实体高亮详情,并支持 JSON/CSV 结果导出,便于二次分析与系统对接。工具的价值最终要落到"人能用、系统能接",所以表现层与导出能力不是锦上添花,而是交付闭环的必要一环。
这五条原则可以浓缩成一句话:让非技术背景的临床/管理人员也能用,让技术背景的开发者也能改。
3.2 总体架构(分层图)
系统采用经典的分层架构,自底向上依次为数据层、引擎层、服务层、表现层。分层的好处是职责清晰、耦合度低,任何一层都能单独替换而不影响其他层。
┌─────────────────────────────────────────────────────────┐ │ 表现层 (Presentation) │ │ 仪表盘 · 详情页 · 自定义分析页 · JSON/CSV 导出 │ ├─────────────────────────────────────────────────────────┤ │ 服务层 (Service) │ │ Flask 路由 · 批量分析 API · 单条分析 API · 导出 API │ ├─────────────────────────────────────────────────────────┤ │ 引擎层 (Engine) │ │ 实体识别 · 科室分类 · 严重度评估 · 自动摘要 · 批量统计 │ │ ┌──────────────────────────────────────┐ │ │ │ 医学实体知识库 (346 条词条) │ │ │ │ 疾病/症状/药物/检查/手术/部位/检验 │ │ │ └──────────────────────────────────────┘ │ ├─────────────────────────────────────────────────────────┤ │ 数据层 (Data) │ │ 样本病历 JSON · 自定义输入文本 │ └─────────────────────────────────────────────────────────┘各层职责:
- 数据层:负责病历数据的加载与持久化。本原型用 JSON 文件存储,好处是可读可改;生产环境可无缝替换为数据库。
- 引擎层:系统的"大脑",封装全部语义分析逻辑(识别、分类、评估、摘要、统计),并依赖内置的医学实体知识库。这一层完全不依赖 Web,可以脱离 Flask 单独作为库调用。
- 服务层:基于 Flask 的 HTTP 路由,把引擎能力以页面和 API 形式对外暴露。批量分析、单条分析、导出,都是在这里接线的。
- 表现层:通过模板渲染 + 前端 JavaScript 实现可视化交互,包括仪表盘、详情页、自定义分析页与文件导出。
这种"引擎与 Web 解耦"的结构非常关键:你把engine/目录拷到任何 Python 项目里,import之后就能用,不必起一个 Web 服务。
3.3 技术选型与理由
技术选型不是追新,而是"够用且省心":
- Python + 标准库做引擎:Python 在文本处理与正则匹配上表达力强、生态成熟;标准库(
re、json、collections)足以覆盖全部需求,避免引入重型依赖。 - Flask 做后端:轻量、灵活、学习成本低,适合中小型 Web 服务,不会因为框架本身的复杂度拖慢开发。
- 原生 HTML/CSS/JS + Chart.js 做前端:不引入 React/Vue 与构建工具链,降低维护门槛;Chart.js 通过 CDN 引入,几行配置就能画出专业图表。
- JSON 做存储:病历样本与导出结果都用 JSON,人类可读、便于调试,也易于后续接入数据库或大数据平台。
可能有读者会问:为什么不用更高级的 NLP 库,比如jieba分词、HanLP或者上 BERT?答案是刻意的克制。分词对中文 NER 有帮助,但本工具基于"词典 + 正则"的精确匹配,不依赖分词,反而避免因分词错误导致的实体切分问题;而深度学习模型虽然精度更高,却违背"零依赖、即开即用"的原则。选型始终服务于设计原则,而不是反过来。
3.4 实体类型配色与词条规模
为了让人在看结果时能"一眼区分",七类实体在可视化中各分配了一种区分色。同时,下表也给出了每类词典的词条数量——这是整个知识库的"体量账本"。
| 类型标识 | 中文名称 | 配色 | 词条数 |
|---|---|---|---|
| disease | 疾病诊断 | #e74c3c 红 | 78 |
| symptom | 症状 | #e67e22 橙 | 55 |
| medication | 药物 | #3498db 蓝 | 77 |
| examination | 检查项目 | #9b59b6 紫 | 41 |
| procedure | 手术/操作 | #1abc9c 青 | 30 |
| body_part | 解剖部位 | #34495e 深灰 | 34 |
| lab_value | 实验室指标 | #16a085 墨绿 | 31 |
| 合计 | — | — | 346 |
这张表有三层含义:第一,七类覆盖均衡,疾病与药物最多(临床最核心的两类),手术与实验室指标相对较少(术语集中度高);第二,配色是功能性的,红色疾病、橙色症状、蓝色药物,符合"疾病警示、症状提醒、药物信息"的直觉;第三,346 这个数字不是拍脑袋,它来自对真实病历高频术语的统计归纳,后续扩充知识库时,这张表就是你的基准线。
第 4 章 医学实体知识库详解
如果把整个工具比作一个人,那引擎是大脑,而医学实体知识库就是记忆和常识。没有它,再聪明的算法也只是空转。本章把这块"记忆"拆开看。
4.1 七大类实体:设计哲学
为什么是这七类,而不是五类或十类?这背后有一套设计哲学:
- 疾病诊断(disease):最核心的实体。一份病历的诊断决定了它的归属、危重度与用药逻辑。
- 症状(symptom):患者主观不适与客观体征表现,是连接"主诉"与"诊断"的桥梁。
- 药物(medication):治疗的核心手段,用药规律直接反映病种与诊疗路径。
- 检查项目(examination):影像、内镜、功能检查等,反映医生的诊断思路。
- 手术/操作(procedure):治疗手段的另一极,区分内科与外科病历的关键信号。
- 解剖部位(body_part):症状与疾病的解剖定位,常作为关系抽取的基础。
- 实验室指标(lab_value):化验数值与项目名(如"糖化血红蛋白"“肌酐”),是量化病情的依据。
这七类不是凭空定的,而是对应临床病历的信息维度:诊断维度、症状维度、用药维度、检查维度、操作维度、定位维度、量化维度。覆盖这七类,一份病历的"语义骨架"就立起来了。
4.2 词条来源与选取三原则
词典里的 346 条词条从哪来?主要来自两类渠道:一是临床诊疗常见术语的归纳(参考病历书写规范、常见慢病与急症术语);二是从样本病历中反向提取高频词。选取时遵守三条原则:
- 临床高频性:优先纳入真实病历里反复出现的术语。比如"高血压""糖尿病"几乎每份内科病历都出现,必须进库;而某个罕见病即便术语规范,若极少出现,可暂缓收录。
- 语义独立性:词条应能独立承载一个明确医学概念。例如"胸痛"是一个独立症状,"左侧"不是实体,它只是部位修饰语,不单独收录。
- 边界清晰性:尽量让词条不与其它类别大量重叠混淆。当然,完全不重叠做不到(见 4.3),但原则上是能分就分。
这三条原则保证了知识库"小而准":它不求覆盖全部医学术语,但求覆盖高频且 unambiguous的那部分,先让工具在常见场景跑得稳。
4.3 跨类别术语的妥善处理
现实中医学术语常有"多重身份",这是建词典时绕不开的坑。举两个例子:
- “糖化血红蛋白”:它是一项实验室检验指标(测血糖控制的金标准),也常作为"检查项目"被医生写进医嘱。
- “冠状动脉造影”:既是检查(用于诊断冠脉狭窄),也是手术操作(介入治疗的前置步骤)。
如果强行让一个术语只属于一类,要么会漏抽,要么会错分。本文的做法是允许同一术语出现在多个类别词典中,抽取时按类别独立匹配——也就是说,"糖化血红蛋白"会同时被疾病/检验与检查两类命中,保留其多重语义属性。代价是统计时该术语会在不同类别各计一次,但换来的是不丢信息。对于原型系统,不丢信息比强行归类更重要。
4.4 科室关键词与严重度词表
除了实体词典,知识库还藏着两组"隐形"词表,它们不直接作为实体输出,却是分类与评估的关键依据:
科室关键词映射(12 个科室):每个科室关联一组具有区分度的关键词。例如神经内科关联"脑梗死、脑出血、癫痫、眩晕";心血管内科关联"冠心病、心肌梗死、心律失常、高血压";呼吸内科关联"肺炎、慢阻肺、哮喘、咳嗽"。系统对全文字段统计各科室关键词命中数,加权求和取最高者。
严重程度关键词表(三级 30 词):
- 危重级(13 词):如"昏迷、休克、呼吸衰竭、心力衰竭、心肌梗死、DIC、脑出血"等,命中即判为最严重;
- 重度级(11 词):如"急诊、入院、手术、溶栓、抢救"等;
- 中度级(6 词):如"住院、复查、随访"等。
这两组词表的规模(12 科室、30 严重度词)同样来自对样本病历的归纳,是后续分类与评估准确率的"地基"。
4.5 如何扩充你自己的知识库
工具的价值在于可演进。扩充知识库是最高频的二次开发动作,这里给出一套可操作的步骤:
- 收集术语:从你的目标病历中导出高频未识别词,或从专科术语表(如某病种诊疗指南)批量取词。
- 分类归位:按 4.1 的七类维度把术语归入对应词典;跨类术语就放多类。
- 去重与校验:检查是否与已有词条重复或构成子串(避免"糖尿病"和"2 型糖尿病"同时命中造成重复,引擎已用长词优先策略缓解,但仍建议人工核对)。
- 补充科室/严重度词:新增病种往往要同步补充对应科室关键词;若涉及危重症,加入严重度词表。
- 增量测试:用新词典重新跑一份代表性病历,确认召回提升且未引入错误匹配。
- 版本管理:把词典文件纳入 Git,每次扩充留 commit,便于回溯。
一个实用建议:先扩充疾病与药物两类,因为这两类对统计结果影响最大;手术、检查等类别术语集中、增量慢,可以放到后面。
本章小结(第 3–4 章)
第 3 章确立了"批量、多维、可解释、轻量、可导出"五原则,给出四层架构与零依赖技术选型,并用配色表亮出了 346 条词条的体量账本。第 4 章深入知识库:七类实体对应病历的七个信息维度,词条选取遵循高频、独立、清晰三原则,跨类术语允许多重归属,科室与严重度两组隐形词表是分类评估的地基,最后给出了可操作的扩充六步法。下一章进入引擎核心,看这些词典如何被真正"用起来"。
(本文未完,第 5 章起见后续章节。)
第 5 章 实体识别引擎:从正则到工程
前面几章把"词典"和"架构"讲完了,本章进入最硬核的部分——引擎怎么把词典变成实际的抽取结果。我会把设计取舍、关键技巧和核心代码一并摊开。
5.1 为什么是规则化而不是深度学习
开门见山:本文工具 deliberately 选择了规则化(词典 + 正则),而非深度学习。这不是因为不会用 BERT,而是经过权衡后的主动选择。理由有四条:
- 零依赖即零门槛。深度学习 NER 需要 PyTorch/TensorFlow、预训练权重、GPU 推理环境,对只想快速验证想法的人而言是重资产。规则化只用标准库,任何装了 Python 的机器都能跑。
- 白盒可解释。每条抽取都对应一个明确词条,医生能追问"为什么识别成这个",你能立刻指出是词典里的哪一条。深度学习是黑盒,出了问题难溯源。
- 对已知术语召回近 100%。只要词条在库里,正则匹配是确定性的,不会"偶尔抽不到"。在高频术语覆盖到位的前提下,核心场景的准确率极高。
- 迭代成本低。发现漏词,往词典里加一条就行,不用重新训练、不用标注、不用调参。
当然,规则化有它的代价——对未收录术语的泛化弱、语义消歧能力有限。这些局限我们会在第 15 章专门讨论。但在"轻量原型 + 高频术语覆盖"的定位下,规则化的性价比最高。
5.2 长词优先策略
正则匹配有个经典坑:短词会截断长词。举个例子,词典里同时有"糖尿病"和"2 型糖尿病"。如果按默认顺序把短词放前面,正则交替表达式可能是(?:糖尿病|2 型糖尿病|...)。当文本出现"2 型糖尿病"时,正则引擎从左到右扫描,可能先命中"糖尿病"三个字,于是只抽出"糖尿病",丢掉"2 型"这个关键修饰——结果错把"2 型糖尿病"识别成普通的"糖尿病",丢失了分型信息。
解决办法很朴素也极有效:把词条按字符串长度降序排列,优先匹配长词。这样交替表达式变成(?:2 型糖尿病|糖尿病|...),遇到"2 型糖尿病"时,长词先被匹配消费掉,短词不再有机会截断它。
这一步看似微小,却是实体识别准确率的关键。它本质上是在模拟人类"先认长的、再认短的"的阅读直觉。
5.3 预编译与性能优化
另一个工程细节是正则的预编译。一份病历要用七类词典各匹配一遍,如果每次分析都现场拼接正则字符串,开销会随调用次数线性累积。本文在引擎初始化时,就把每一类的交替表达式用re.compile预编译成模式对象缓存起来:
foretype,infoinself.entity_types.items():terms=sorted(info["dict"],key=len,reverse=True)# 长词优先self._patterns[etype]=re.compile(r"(?:"+"|".join(re.escape(t)fortinterms)+r")")re.escape的作用是:词条里若含有正则元字符(如括号、点号),转义后不会破坏正则语法。预编译带来的好处在批量场景下尤为明显——12 份病历的分析里,正则对象只编译一次,后续每次匹配都是 O(文本长度) 的线性扫描,这也是端到端耗时低于 50 毫秒的关键之一。
5.4 结构化与扁平化双路输出
同一份抽取结果,在不同场景下需要两种形态:
- 结构化输出(按类聚合):把识别出的实体按类别归并,统计每类每词的命中频次。形态大致是
{"disease": {"高血压": 2, "糖尿病": 1}, "medication": {...}}。它适合做统计报表、侧栏展示、高频 Top 10。 - 扁平化输出(带位置):保留每个匹配实体的文本、类别、起始与结束位置偏移,按起始位置排序。形态是
[{"text":"高血压","type":"disease","start":12,"end":15}, ...]。它适合前端把原文和实体精确对齐、做彩色高亮。
为什么要两路?因为结构化丢了位置(无法高亮),扁平化丢了聚合(不利于统计)。工具同时产出两者,让"统计"和"高亮"各取所需。扁平化输出时还要处理重叠:极少数情况下不同类别的正则可能覆盖同一段文本(如跨类术语),引擎按起始位置和长度做去重,保留最匹配的那个,保证前端高亮不混乱。
位置信息还带来一个隐性价值:可核验。你点开详情页,看到"高血压"被标红,是因为引擎在原文的第 12–15 个字符确实匹配到了这个词条——所见即所得,不存在黑盒幻觉。
5.5 引擎核心代码片段
把 5.2–5.4 串起来,实体识别引擎的核心逻辑(简化版)如下:
importrefromcollectionsimportdefaultdictclassEntityRecognizer:def__init__(self,entity_types):# entity_types: {"disease": {"dict": [...]}, ...}self._patterns={}foretype,infoinentity_types.items():terms=sorted(info["dict"],key=len,reverse=True)# 长词优先self._patterns[etype]=re.compile(r"(?:"+"|".join(re.escape(t)fortinterms)+r")")defrecognize(self,text):flat=[]# 扁平化输出struct=defaultdict(lambda:defaultdict(int))# 结构化输出foretype,patinself._patterns.items():forminpat.finditer(text):span=(m.start(),m.end())token=m.group()flat.append({"text":token,"type":etype,"start":span[0],"end":span[1]})struct[etype][token]+=1flat.sort(key=lambdax:x["start"])# 按位置排序returnflat,dict(struct)这段代码只有二十来行,却浓缩了规则化 NER 的全部精髓:长词优先、预编译、双路输出。它不依赖任何第三方库,拷到任何 Python 环境都能跑。下一章我们看,光有实体还不够,怎么把一份病历"归到科室"。