面向电子病历的批量语义分析自动化工具:从设计到实战(上)

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 医疗数据的"富矿"与"荒原"
    • 1.2 电子病历的真实结构与被低估的痛点
    • 1.3 临床自然语言处理能带来什么
    • 1.4 本文要解决的三个具体问题
    • 1.5 本章小结
  • [第 2 章 工具总览:它能做什么](#第 2 章 工具总览:它能做什么)

    • 2.1 一句话定位
    • 2.2 功能全景清单
    • 2.3 一张图看懂工作流
    • 2.4 适合谁 / 不适合谁
    • 2.5 技术栈一览
    • 2.6 本章小结
  • [第 3 章 系统总体设计](#第 3 章 系统总体设计)

    • 3.1 设计目标与原则
    • 3.2 总体架构(分层图)
    • 3.3 技术选型与理由
    • 3.4 实体类型配色与词条规模
  • [第 4 章 医学实体知识库详解](#第 4 章 医学实体知识库详解)

    • 4.1 七大类实体:设计哲学
    • 4.2 词条来源与选取三原则
    • 4.3 跨类别术语的妥善处理
    • 4.4 科室关键词与严重度词表
    • 4.5 如何扩充你自己的知识库
  • [第 5 章 实体识别引擎:从正则到工程](#第 5 章 实体识别引擎:从正则到工程)

    • 5.1 为什么是规则化而不是深度学习
    • 5.2 长词优先策略
    • 5.3 预编译与性能优化
    • 5.4 结构化与扁平化双路输出
    • 5.5 引擎核心代码片段
  • [第 6 章 科室分类模块](#第 6 章 科室分类模块)

    • 6.1 关键词加权评分机制
    • 6.2 一个真实的"合并症干扰"案例
    • 6.3 改进思路
  • [第 7 章 严重程度评估模块](#第 7 章 严重程度评估模块)

    • 7.1 三级关键词阈值
    • 7.2 0--100 评分公式
    • 7.3 可视化呈现
  • [第 8 章 自动摘要生成](#第 8 章 自动摘要生成)

    • 8.1 模板填充而非生成
    • 8.2 为什么宁可"死板"也不要"幻觉"
  • [第 9 章 批量分析与可视化系统](#第 9 章 批量分析与可视化系统)

    • 9.1 批量处理流程
    • 9.2 跨病历统计聚合
    • 9.3 仪表盘布局与图表
    • 9.4 详情页高亮与自定义分析
  • [第 10 章 完整代码走读](#第 10 章 完整代码走读)

  • [第 11 章 实验与逐份病历案例](#第 11 章 实验与逐份病历案例)

    • 11.1 实验数据与设置
    • 11.2 批量分析结果汇总
    • 11.3 12 份病历逐份解读
    • 11.4 性能与准确性讨论
  • [第 12 章 与主流方案的对比](#第 12 章 与主流方案的对比)

    • 12.1 规则化 vs 深度学习 vs 商用
    • 12.2 一张对比表看懂取舍
  • [第 13 章 部署与运维指南](#第 13 章 部署与运维指南)

    • 13.1 环境准备
    • 13.2 启动服务
    • 13.3 接入你自己的病历数据
    • 13.4 二次开发指引
  • [第 14 章 使用教程:五分钟跑通](#第 14 章 使用教程:五分钟跑通)

  • [第 15 章 讨论与局限性](#第 15 章 讨论与局限性)

    • 15.1 规则化方法的适用边界
    • 15.2 与深度学习方法的互补关系
    • 15.3 其他工程局限
  • [第 16 章 结论与展望](#第 16 章 结论与展望)

  • [第 17 章 附录](#第 17 章 附录)

    • 17.1 常见问题 FAQ
    • 17.2 术语表
    • 17.3 参考资源与文献
    • 17.4 免责声明

第 1 章 为什么需要电子病历语义分析

1.1 医疗数据的"富矿"与"荒原"

医院每天产生的数据里,有一类是结构化的:化验单的数值、生命体征的曲线、医保结算的字段,它们整齐、标准、易于计算。但还有一类------也是占比极大的一类------是自由文本:门诊病历里医生写的一段段叙述,住院病历里洋洋洒洒的病程记录,会诊意见里夹叙夹议的讨论。

这类文本是一座"富矿"。一份普通的出院小结,可能包含主诊断、十余个既往诊断、一长串用药、若干检查结果,以及医生对病情转归的判断。如果把这些非结构化信息全部变成结构化字段,你就能做很多事:按疾病统计科室负荷、追踪某种药物的使用趋势、筛查高危患者、构建科研队列、做病种质控、自动生成上报材料......

但它同时是一片"荒原"。富矿之所以是荒原,是因为机器读不懂它。你没法在 SQL 里 SELECT disease FROM emr WHERE severity > 80------因为 diseaseseverity 压根不存在于数据库里,它们埋在一段中文叙述的某个句子中间。要从这片荒原里挖出金子,靠人肉阅读是行不通的(量太大),靠简单的关键词搜索也不行(同义词、缩写、表述变体太多)。于是,"临床自然语言处理"这门技术就成了连接富矿与荒原的桥。

1.2 电子病历的真实结构与被低估的痛点

我们先把电子病历的结构摊开看。一份典型的门诊/住院病历,至少包含以下字段:

  • 主诉:患者本次就诊最主要的症状及持续时间,例如"反复胸闷 3 年,加重伴气短 1 周"。
  • 现病史:本次疾病发生、发展、诊治的全过程。
  • 既往史:过去的健康状况、慢性病、手术史、过敏史。
  • 体格检查:医生查体所见,包括生命体征与专科查体。
  • 辅助检查:化验、影像、超声等结果。
  • 诊断:门急诊或出院诊断,常分主要诊断与次要诊断。
  • 处置 / 医嘱:用药、手术、会诊、随访等。

看起来字段清晰,对吧?但痛点恰恰藏在"清晰"之下:

  1. 术语不统一。同样是"高血压",可能写作"高血压病""原发性高血压""HBP""HTN";同样是"心肌梗死",可能写"心梗""AMI""急性心肌梗死"。
  2. 表述高度自由。同样一个"糖尿病",有人写"2 型糖尿病多年",有人写"糖尿病史 10 年,口服二甲双胍控制可",有人写"发现血糖升高 5 年"。
  3. 缩写与口语化。"房颤""室上速""呼衰""肾衰"随处可见。
  4. 否定与不确定表述。"否认肝炎结核史""未见明显积液""不排除肿瘤可能"------这些表述如果不做语义处理,很容易把"未见"当成"见"。
  5. 多诊断并存。一份病历往往主诊断之外还挂着三五个既往诊断,且分不清主次。

这些痛点叠加起来的结果是:你手里明明有一座金矿,却只能用"人眼 + Ctrl+F"这种原始工具去挖。这正是本文工具的出发点。

1.3 临床自然语言处理能带来什么

临床自然语言处理(Clinical NLP)是医学信息学与自然语言处理的交叉领域,目标是从临床文本中抽取、组织并推理医学知识。它通常包含这样一串任务:

  • 命名实体识别(NER):把文本里的"疾病""症状""药物""检查""手术""部位""指标"等实体框出来。
  • 关系抽取:识别实体之间的关系,例如"药物 A 用于治疗 疾病 B""症状 C 发生于 部位 D"。
  • 文本分类:给整篇病历打标签,例如科室、病种、是否手术、是否危重。
  • 否定/不确定检测:判断某个实体是"存在"还是"被否定/不确定"。
  • 摘要生成:把长篇叙述压成核心信息。

本文工具聚焦于其中最基础也最实用的一层------实体识别 + 文本分类(科室/严重度)+ 摘要,并把它们打包成"批量 + 可视化 + 可导出"的完整链路。换句话说,它不追求做全所有 Clinical NLP 任务,而是把"从病历文本到结构化结果"这条最刚需的主线做扎实。

1.4 本文要解决的三个具体问题

把上面说的揉成三个具体问题,也就是本文工具要回答的:

  1. 怎么把一份病历"拆解"成结构化字段?------通过实体识别引擎,把疾病、症状、用药、检查等七大类实体从叙述中抽出来,并能在原文上高亮标注。
  2. 怎么给一批病历"画像"?------通过科室分类、严重度评估、批量统计聚合,让管理者从宏观层面看清这批病历的科室分布、病情梯度、高频病种与用药规律。
  3. 怎么让人"一眼看懂"并"拿去用"?------通过 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 设计目标与原则

工具在动手前先确立了五条设计原则,它们像一把尺子,后面每个模块的取舍都用它来量:

  1. 批量自动化

    支持一次性导入并分析多份病历,自动完成从原始文本到结构化结果与统计报告的全流程,减少人工干预。这里的关键词是"全流程"------不是分析完丢给你一堆 JSON 就完了,而是连统计、图表、导出都一并做好。

  2. 多维度语义提取

    不只抽实体,还提供科室分类、严重度评估、自动摘要,形成对一份病历的"多层次语义画像"。单一维度的抽取往往不够用:你知道一份病历有"高血压、糖尿病",但不知道它该归哪个科、危不危重、主线是什么。多维度才能画像。

  3. 可解释性优先

    所有抽取结果都对应明确的词典词条或关键词规则,用户能追溯每个实体、每个判定的来源。这一点在医疗场景尤其重要------你不能告诉医生说"模型觉得这病历危重",却不给理由。规则化方法的天然优势就是"白盒"。

  4. 轻量零依赖

    核心引擎只用 Python 标准库,不引入深度学习框架,普通计算环境即开即用。零依赖带来的好处不仅仅是部署省事,更是可移植性与可审计性:别人拿到代码,不需要配环境就能读懂、能改、能跑。

  5. 可视化与可导出

    提供 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 在文本处理与正则匹配上表达力强、生态成熟;标准库(rejsoncollections)足以覆盖全部需求,避免引入重型依赖。
  • 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 条词条从哪来?主要来自两类渠道:一是临床诊疗常见术语的归纳(参考病历书写规范、常见慢病与急症术语);二是从样本病历中反向提取高频词。选取时遵守三条原则:

  1. 临床高频性:优先纳入真实病历里反复出现的术语。比如"高血压""糖尿病"几乎每份内科病历都出现,必须进库;而某个罕见病即便术语规范,若极少出现,可暂缓收录。
  2. 语义独立性:词条应能独立承载一个明确医学概念。例如"胸痛"是一个独立症状,"左侧"不是实体,它只是部位修饰语,不单独收录。
  3. 边界清晰性:尽量让词条不与其它类别大量重叠混淆。当然,完全不重叠做不到(见 4.3),但原则上是能分就分。

这三条原则保证了知识库"小而准":它不求覆盖全部医学术语,但求覆盖高频且 unambiguous 的那部分,先让工具在常见场景跑得稳。

4.3 跨类别术语的妥善处理

现实中医学术语常有"多重身份",这是建词典时绕不开的坑。举两个例子:

  • "糖化血红蛋白":它是一项实验室检验指标(测血糖控制的金标准),也常作为"检查项目"被医生写进医嘱。
  • "冠状动脉造影":既是检查(用于诊断冠脉狭窄),也是手术操作(介入治疗的前置步骤)。

如果强行让一个术语只属于一类,要么会漏抽,要么会错分。本文的做法是允许同一术语出现在多个类别词典中 ,抽取时按类别独立匹配------也就是说,"糖化血红蛋白"会同时被疾病/检验与检查两类命中,保留其多重语义属性。代价是统计时该术语会在不同类别各计一次,但换来的是不丢信息。对于原型系统,不丢信息比强行归类更重要。

4.4 科室关键词与严重度词表

除了实体词典,知识库还藏着两组"隐形"词表,它们不直接作为实体输出,却是分类与评估的关键依据:

科室关键词映射(12 个科室):每个科室关联一组具有区分度的关键词。例如神经内科关联"脑梗死、脑出血、癫痫、眩晕";心血管内科关联"冠心病、心肌梗死、心律失常、高血压";呼吸内科关联"肺炎、慢阻肺、哮喘、咳嗽"。系统对全文字段统计各科室关键词命中数,加权求和取最高者。

严重程度关键词表(三级 30 词)

  • 危重级(13 词):如"昏迷、休克、呼吸衰竭、心力衰竭、心肌梗死、DIC、脑出血"等,命中即判为最严重;
  • 重度级(11 词):如"急诊、入院、手术、溶栓、抢救"等;
  • 中度级(6 词):如"住院、复查、随访"等。

这两组词表的规模(12 科室、30 严重度词)同样来自对样本病历的归纳,是后续分类与评估准确率的"地基"。

4.5 如何扩充你自己的知识库

工具的价值在于可演进。扩充知识库是最高频的二次开发动作,这里给出一套可操作的步骤:

  1. 收集术语:从你的目标病历中导出高频未识别词,或从专科术语表(如某病种诊疗指南)批量取词。
  2. 分类归位:按 4.1 的七类维度把术语归入对应词典;跨类术语就放多类。
  3. 去重与校验:检查是否与已有词条重复或构成子串(避免"糖尿病"和"2 型糖尿病"同时命中造成重复,引擎已用长词优先策略缓解,但仍建议人工核对)。
  4. 补充科室/严重度词:新增病种往往要同步补充对应科室关键词;若涉及危重症,加入严重度词表。
  5. 增量测试:用新词典重新跑一份代表性病历,确认召回提升且未引入错误匹配。
  6. 版本管理:把词典文件纳入 Git,每次扩充留 commit,便于回溯。

一个实用建议:先扩充疾病与药物两类,因为这两类对统计结果影响最大;手术、检查等类别术语集中、增量慢,可以放到后面。

本章小结(第 3--4 章)

第 3 章确立了"批量、多维、可解释、轻量、可导出"五原则,给出四层架构与零依赖技术选型,并用配色表亮出了 346 条词条的体量账本。第 4 章深入知识库:七类实体对应病历的七个信息维度,词条选取遵循高频、独立、清晰三原则,跨类术语允许多重归属,科室与严重度两组隐形词表是分类评估的地基,最后给出了可操作的扩充六步法。下一章进入引擎核心,看这些词典如何被真正"用起来"。


(本文未完,第 5 章起见后续章节。)

第 5 章 实体识别引擎:从正则到工程

前面几章把"词典"和"架构"讲完了,本章进入最硬核的部分------引擎怎么把词典变成实际的抽取结果。我会把设计取舍、关键技巧和核心代码一并摊开。

5.1 为什么是规则化而不是深度学习

开门见山:本文工具 deliberately 选择了规则化(词典 + 正则),而非深度学习。这不是因为不会用 BERT,而是经过权衡后的主动选择。理由有四条:

  1. 零依赖即零门槛。深度学习 NER 需要 PyTorch/TensorFlow、预训练权重、GPU 推理环境,对只想快速验证想法的人而言是重资产。规则化只用标准库,任何装了 Python 的机器都能跑。
  2. 白盒可解释。每条抽取都对应一个明确词条,医生能追问"为什么识别成这个",你能立刻指出是词典里的哪一条。深度学习是黑盒,出了问题难溯源。
  3. 对已知术语召回近 100%。只要词条在库里,正则匹配是确定性的,不会"偶尔抽不到"。在高频术语覆盖到位的前提下,核心场景的准确率极高。
  4. 迭代成本低。发现漏词,往词典里加一条就行,不用重新训练、不用标注、不用调参。

当然,规则化有它的代价------对未收录术语的泛化弱、语义消歧能力有限。这些局限我们会在第 15 章专门讨论。但在"轻量原型 + 高频术语覆盖"的定位下,规则化的性价比最高。

5.2 长词优先策略

正则匹配有个经典坑:短词会截断长词 。举个例子,词典里同时有"糖尿病"和"2 型糖尿病"。如果按默认顺序把短词放前面,正则交替表达式可能是 (?:糖尿病|2 型糖尿病|...)。当文本出现"2 型糖尿病"时,正则引擎从左到右扫描,可能先命中"糖尿病"三个字,于是只抽出"糖尿病",丢掉"2 型"这个关键修饰------结果错把"2 型糖尿病"识别成普通的"糖尿病",丢失了分型信息。

解决办法很朴素也极有效:把词条按字符串长度降序排列,优先匹配长词 。这样交替表达式变成 (?:2 型糖尿病|糖尿病|...),遇到"2 型糖尿病"时,长词先被匹配消费掉,短词不再有机会截断它。

这一步看似微小,却是实体识别准确率的关键。它本质上是在模拟人类"先认长的、再认短的"的阅读直觉。

5.3 预编译与性能优化

另一个工程细节是正则的预编译 。一份病历要用七类词典各匹配一遍,如果每次分析都现场拼接正则字符串,开销会随调用次数线性累积。本文在引擎初始化时,就把每一类的交替表达式用 re.compile 预编译成模式对象缓存起来:

python 复制代码
for etype, info in self.entity_types.items():
    terms = sorted(info["dict"], key=len, reverse=True)   # 长词优先
    self._patterns[etype] = re.compile(
        r"(?:" + "|".join(re.escape(t) for t in terms) + 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 串起来,实体识别引擎的核心逻辑(简化版)如下:

python 复制代码
import re
from collections import defaultdict

class EntityRecognizer:
    def __init__(self, entity_types):
        # entity_types: {"disease": {"dict": [...]}, ...}
        self._patterns = {}
        for etype, info in entity_types.items():
            terms = sorted(info["dict"], key=len, reverse=True)  # 长词优先
            self._patterns[etype] = re.compile(
                r"(?:" + "|".join(re.escape(t) for t in terms) + r")"
            )

    def recognize(self, text):
        flat = []          # 扁平化输出
        struct = defaultdict(lambda: defaultdict(int))  # 结构化输出
        for etype, pat in self._patterns.items():
            for m in pat.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] += 1
        flat.sort(key=lambda x: x["start"])   # 按位置排序
        return flat, dict(struct)

这段代码只有二十来行,却浓缩了规则化 NER 的全部精髓:长词优先、预编译、双路输出。它不依赖任何第三方库,拷到任何 Python 环境都能跑。下一章我们看,光有实体还不够,怎么把一份病历"归到科室"。


相关推荐
明志数科1 小时前
从实验室到工厂产线:具身智能训练数据的环境差异、分布偏移与工业级采集方案
人工智能·深度学习·计算机视觉
武子康1 小时前
我让 Qwen3.6-27B 真改了一次 Git 仓库:工具调用怎样形成 Agent 闭环
人工智能·后端·agent
Interview Aid1121 小时前
Akuna OA 题目拆解|三题思路复盘
linux·运维·算法·面试·职场和发展
嘻嘻的AI日记1 小时前
告别会后整理负担|智能会议系统,实现会纪要自动生成
人工智能
Harm灬小海1 小时前
DeepSeek 大模型本地部署与调用实战指南
自然语言处理·ai自动写文章
JJJennie7771 小时前
MAI Gateway技术揭秘:大模型网关有哪些功能?从原理到落地
大数据·人工智能·大模型·gateway·软件工程·ai网关
2601_954811821 小时前
AI通识课跨设备联调难题:协议兼容层设计与教学联动架构优化
人工智能·python·架构
Scott9999HH1 小时前
2026 年企业级生成式搜索引擎底层架构演进与生成式引擎优化系统推荐:从多路向量同步到全域自动化 GEO 中台的落地实战
搜索引擎·架构·自动化
云浪1 小时前
如何让大模型操作 MySQL 数据库?
javascript·人工智能·后端