
简介面向中文医学知识图谱构建的实体关系标注工具CMeKG-labelingPlatform-main适合NLP研究者、医疗信息开发者及知识图谱入门者用于高效解决医学文本中的命名实体识别与关系抽取标注难题。资源包共472个文件核心类型包括js和jsx前端交互脚本、py模型训练与标注算法、json数据配置、html页面、css样式及字体图标整体约10.34MB便于本地部署与二次开发。平台提供可视化标注界面、批量数据管理、多人协作校验以及API接口可结合BiLSTMCRF、BERT等深度学习模型实现自动或半自动标注辅助构建“疾病-症状”“药物-治疗”等医学关系显著减少人工标注成本。代码中包含完整的前端展示与后端算法逻辑目录结构清晰覆盖数据上传、标注、校验、导出全流程已有840人学习下载既适合医疗知识图谱、临床决策支持等方向的研发参考也可用于课程设计与教学实践。1. 医学文本的实体关系标注为什么需要专门平台医学文本的实体关系标注不是“选中文本、打个标签然后导出”这么简单。一句“患者因反复咳嗽、咳痰伴发热3天入院”里面要分辨的实体包括症状、时间词、修饰程度句子与句子之间还挂着“伴随”“诱发”“缓解”这类关系。通用标注工具能做到的只是把标签钉在文本上不校验“咳嗽”该归症状还是疾病不阻止标注员把“药物→症状”的关系连成“症状→药物”更不能在导出时替你完成实体名词的标准化。CMeKG实体关系标注平台CMeKG-labelingPlatform的设计目标是把医学知识图谱构建里对标注层的硬约束固化在工具内部实体类型体系、关系连线合法性、审校流程、导出格式都围绕同一份schema运作。要在中文医学语料上训练NER和关系抽取模型的团队这类平台决定的不只是打标速度更决定数据质量的可控性。2. 实体关系标注Schema设计医学实体类型与关系约束2.1 实体类型体系先定层级还是先定平铺医学语料里实体类型的完整体系如果全量展开可以分出上百个叶子类型。CMeKG项目定义的核心类型其实比较收敛疾病、症状体征、药物、检查检验、手术操作、解剖部位需要再扩展到基因、微生物时再追加。平台设计第一件事不是写接口而是确定这份类型体系以什么形态呈现在标注员面前。我一般建议界面层保持平铺。标注员面对六个以内的主类型层级归属放到导出阶段再映射到受控词表。平铺的好处有两点标注速度明显更快面对连续五个实体时不需要在层级下拉框里做判断一致性更好让两个标注员都认定“这是疾病”比认定“这是继发性高血压”容易得多。如果项目确实要区分亚型不要在标注界面加三级下拉改为快捷键后再弹一层候选列表比如按了d之后出现疾病亚型四选一拿不准的时候标注员也可以跳过。界面层一旦出现二级菜单标注失误率会明显上升高频操作时几乎没人会逐级展开菜单大多数人只会在第一屏的平铺按钮上完成动作。一份可以直接导入平台的schema定义大致长这样{ entity_types: [ {id: dis, name: 疾病, shortcut: d, color: #E63946}, {id: sym, name: 症状体征, shortcut: s, color: #F4A261}, {id: drg, name: 药物, shortcut: m, color: #2A9D8F}, {id: exm, name: 检查检验, shortcut: e, color: #457B9D}, {id: bpd, name: 解剖部位, shortcut: b, color: #8D99AE}, {id: opr, name: 手术操作, shortcut: o, color: #6D6875} ], relation_types: [ { id: treat, name: 治疗, subject_types: [drg, opr], object_types: [dis, sym] }, { id: present, name: 表现为, subject_types: [dis], object_types: [sym] }, { id: locate, name: 发生于, subject_types: [dis, sym], object_types: [bpd] }, { id: detect, name: 检查发现, subject_types: [exm], object_types: [dis, sym] } ] }schema里每个字段都有明确用途。shortcut和键盘事件绑定高频类型选用最顺手的键位color是给前端界面用的实体底色长句子标注时靠颜色区分类型比靠文字标签快得多subject_types和object_types是关系连线的白名单后端每保存一条关系都要拿这个白名单做校验。2.2 实体边界判定与容易踩的标注坑类型定了之后真正影响数据质量的是文本里实体边界怎么切。医学长句里面经常出现嵌套实体“右肺下叶团块影”中“右肺下叶”是部位实体“团块影”是征象但“右肺下叶”五个字本身还附带方位修饰和器官名。如果标注员不加约定地把“右肺下叶”的每一个修饰词都包含进去下一句出现的“右肺中叶”就会在边界标注上产生分歧。实体类型边界约定常见错误疾病不包含分期和严重度限定词把“肺癌晚期”整体标成疾病症状体征程度副词不纳入把“剧烈头痛”标成“剧烈头痛”药物商品名和化学名视为同一实体同一文档里有的标“泰诺”有的标“对乙酰氨基酚”检查检验只标检查项目不标结果把“CT示团块影”整体当成检查实体解剖部位方位部位整体标注“右肺上叶”拆成“右肺”和“上叶”这些边界规则必须写成书面的标注规范同时最好在平台里用高亮提示或示例弹窗同步给标注员。标注平台存在的意义不只是把文本和标签存下来而是把这类规则变成操作流程的一部分。2.3 标注数据的存储模型位置偏移量比文本片段可靠在存储层要做的决定是实体用“原文片段”还是用“偏移量”。用片段有一个隐蔽的坑同一实体可能在文本里出现多次只存片段无法定位到具体哪一次出现第二次出现和第一次出现需要分别标注时片段字典就乱了。通用做法是文档按句子拆分句子存原文本实体在句子内部用start和end字符偏移量表示。{ doc_id: doc_0001, sentence: 患者因反复咳嗽、咳痰伴发热3天入院。, entities: [ {id: e1, type: sym, start: 11, end: 13, mention: 咳嗽}, {id: e2, type: sym, start: 15, end: 17, mention: 咳痰}, {id: e3, type: sym, start: 22, end: 24, mention: 发热} ], relations: [ {id: r1, type: present, subject: e1, object: e3} ] }偏移量字段要约定清楚start是闭区间end是开区间。上面这个例子里“咳嗽”位于句子中的第11到第12个字符所以start11end13。按偏移量存储的最大好处是后续做BIO序列标注转换完全无损耗文本一旦被分词工具重新切分可以直接用偏移量映射回去。3. 部署与初始化CMeKG-labelingPlatform的服务配置和语料导入3.1 依赖环境与配置文件要点实体关系标注平台采用前后端分离是常见工程形态后端处理数据存取和API前端做文本可视化与快捷键交互标注数据落库。数据库方面MySQL或PostgreSQL都能胜任标注平台本身读写压力不大真正要留意的是两个地方任务领取的批量大小和上传语料的格式约束。下面的配置文件是这类平台的典型形态server: host: 0.0.0.0 port: 8080 debug: false database: dialect: mysqlpymysql host: 127.0.0.1 port: 3306 name: cmeg_labeling user: label password: ${DB_PASSWORD} task: batch_size: 20 max_retry: 3 auto_assign: true upload: allowed_ext: [.txt, .json, .csv] max_size_mb: 10 export: format: jsonl encoding: utf-8batch_size值得单独说。一个标注员的单次领取量如果是20个句子能把上下文牢牢锁在工作台内设置得太大比如100句不仅前端渲染卡顿标注员的注意力也会分散。max_retry控制审校退回到标注员手里的次数上限超过三次这个文档会被列为争议文档转给仲裁人处理。auto_assign打开后系统会按界面里的待办数做简单负载均衡避免部分标注员排不上队、另一部分闲等。配置项推荐值影响batch_size20batch过小频繁取件、过大加载和注意力都下降max_retry3用于阻断低质量标注的无休止返工auto_assigntrue按待办数自动分配避免忙闲不均upload.max_size_mb10控制单次批量导入的语料体积3.2 数据库初始化和启动命令初始化步骤通常分两步先建库建表然后导入schema定义。命令行操作我习惯直接写在部署脚本里方便后面在第二台机器上复制环境# 建库建表通常用SQLAlchemy的metadata.create_all实现 python manage.py init_db # 导入上面编写好的schema覆盖实体类型和关系白名单 python manage.py load_schema --file configs/schema_medical.json # 启动后端服务生产环境建议用gunicorn python manage.py runserver --host 0.0.0.0 --port 8080 # 前端开发环境启动 cd frontend npm install npm run dev需要提醒的是schema的导入不是简单的insert应做幂等处理同样的schema重复导入时只能更新不能产生重复记录。不少标注项目会在迭代中调整实体类型或关系定义如果不做幂等数据库里会积攒大量废弃类型导出阶段还要再写一层过滤。3.3 批量导入待标注文本标注平台一般提供REST接口接收文本。txt、csv、json三种格式里txt最直接但丢掉了文档元信息json最便于携带来源和科室等属性。导入接口的调用方式大致如下import requests API http://127.0.0.1:8080/api def import_task(path: str, task_name: str) - str: with open(path, r, encodingutf-8) as fp: text fp.read() payload { task_name: task_name, docs: [ { text: text, meta: {source: path} } ] } resp requests.post(f{API}/tasks/import, jsonpayload) resp.raise_for_status() task_id resp.json()[task_id] return task_id if __name__ __main__: tid import_task(data/raw/medical_case_001.txt, med-001) print(ftask_id: {tid})这里的task_name不是随便起的建议按科室或文档来源分类命名方便后续按任务维度统计质量和追溯。meta字段里放来源路径、科室、采集时间导出时这些信息会跟着走是做质量回溯和错误样例分析的第一手材料。4. 标注实操实体打标、关系连线与审核返修4.1 工作台布局与快捷键操作节奏实体标注工作台的核心区域是文本面板一般分上下两栏上栏是当前句子和上下文预览下栏是实体列表。文本里的实体用底色块标出悬浮显示类型名。标注员的操作路径是读句子选中文本按实体类型快捷键继续下一条。判断时间主要在读句子上操作本身要压缩到一秒以内所以快捷键设计直接决定产能。下面是整理过的快捷键映射表可以作为前端交互默认值快捷键功能说明d / s / m / e / b / o标记实体分别对应六种实体类型按下即完成标注Shift左右方向键微调边界修正选中范围多一个字或少一个字R关系连线模式进入后先点主语实体再点宾语实体Esc取消当前操作连线和选中状态一键回退CtrlZ / CtrlY撤销/重做支持按步骤回退F2合并同义实体把同一句中同一实体的不同写法合并工作台里最容易忽略的是当前作业单位。把整个文档一次性铺在界面上对长文本并不友好我一般会把导入的文本先按句号、问号、感叹号切分一个句子是一个作业单元句间关系的跨句标注在句子上下文中用特殊底色提示。医学文本的句子普遍较长再细分为半句可能是更好的选择取决于平台前端渲染的宽度。4.2 关系连线与类型校验关系标注的核心不是连线这个动作而是连线背后的类型约束。一份标好的数据里如果出现了“检查→症状”的“治疗”关系这一条就会成为模型训练时的噪声。解决办法在schema层已经铺好了每个关系类型都声明了subject_types和object_types后端保存时做白名单校验。class RelationValidator: def __init__(self, relation_types: list): self._allowed set() for rel in relation_types: for s in rel[subject_types]: for o in rel[object_types]: self._allowed.add((rel[id], s, o)) def check(self, relation_id: str, subj_type: str, obj_type: str) - bool: if (relation_id, subj_type, obj_type) in self._allowed: return True return False这段代码只有十行但它是标注质量的关键防线。save接口接收relations数组时逐条调用check()发现不合法直接返回400和具体错误信息比如“治疗关系不允许检查→部位的连线”。前端在用户进入连线模式时也应该把可选实体提前过滤一遍让标注员根本选不到不合法的实体而不是等到保存才报错。4.3 审核、返修与争议处理标注流程不能只有一个打标签的环节。生产级的标注平台会区分标注员和审核员两种角色标注员完成一批文档后提交审核员逐条检查审核不通过退回时填写理由标注员按理由返修同一文档被退回超过max_retry次就进入争议池由项目管理员直接裁决。审核界面需要显示实体边界、关系连线和原始句子必要的时候可以把标准答案或同句的其他标注结果并排对比。争议池的处理结果也是标注规范迭代的重要依据每次争议都说明规则里有没覆盖到的情况。经过这套流程处理的数据导入训练集之前就有了质量基线。5. 导出与CMeKG对齐从标注结果到知识图谱三元组5.1 标注结果导出的标准结构标注完成后需要导出给训练或入库。导出格式最关键的要求是保留原始文本、实体偏移量和关系类型三者之间的对应关系。JSONL比单纯的JSON更适合大批量导出每一行是一条完整句子的标注结果既方便分片读取也方便后续按行过滤坏数据。{ task_id: med-001, doc_id: doc_0001, text: 患者因反复咳嗽、咳痰伴发热3天入院。, entities: [ {id: e1, type: sym, start: 11, end: 13, mention: 咳嗽}, {id: e2, type: sym, start: 15, end: 17, mention: 咳痰}, {id: e3, type: sym, start: 22, end: 24, mention: 发热} ], relations: [ {id: r1, type: present, subject: e1, object: e3} ] }这个结构其实和数据库里存的差别不大导出时真正要做的额外工作是实体标准化把mention字段里的口语写法、缩写映射到受控词表。比如“高血压病”“高血压症”“HTN”在知识图谱中应指向同一个概念节点。映射关系可以在标注平台里做成实体浮层上的一个下拉框也可以放在导出脚本中用术语表做替换。5.2 实体标准化与CMeKG类型映射CMeKG的实体分类和标注平台界面上的类型不完全是一对一关系。界面层为了标注效率做了平铺设计导出到知识图谱时需要按标准分类扩充属性。界面类型CMeKG 标准类型附加映射字段疾病diseaseicd10_code症状体征symptom作为 disease 下位概念处理药物drugatc_code检查检验examination关联到检查项目本体解剖部位body_partFMA_ID 映射手术操作operation手术分类码映射过程不需要全部自动化。常见实体可以先做自动映射自动匹配不到或置信度低的实体进入人工确认队列。这个“自动加人工”的流程是知识图谱构建中最可靠的方式比完全依赖规则匹配或者在标注界面增加大量字段都省力。5.3 三元组生成脚本导出结果到SPOsubject-predicate-object三元组的转换核心是处理实体ID到标准化名词的解析。实体列表和关系列表分开存储关系里只有entity id转换时要先建实体字典再遍历关系。import json def to_spo(export_path: str): with open(export_path, r, encodingutf-8) as fp: data [json.loads(line) for line in fp if line.strip()] triples [] for doc in data: entities {e[id]: e for e in doc[entities]} for rel in doc[relations]: subj entities.get(rel[subject]) obj entities.get(rel[object]) if subj is None or obj is None: print(fmissing entity in {doc[doc_id]}: {rel}) continue triples.append({ subject: subj.get(normalized, subj[mention]), predicate: rel[type], object: obj.get(normalized, obj[mention]), doc_id: doc[doc_id] }) return triples triples to_spo(export/med_task.jsonl) print(triple count:, len(triples))这段脚本里打印missing entity的分支尤其重要。关系指向的实体如果不存在或已被删就要回查标注平台而不是直接丢弃。三元组生成后还要按谓词统计分布一个谓词独占90%的数据说明schema设计或者标注规范在语义覆盖上出了问题。6. 实体关系标注提质提速预标注、一致性验证与长尾实体补齐6.1 预标注先让模型打底稿平台标注几十篇之后就可以训练一版草稿模型用它对剩余文本自动打标签只保留置信度高于0.8的预测结果写入预标注字段。标注员进入文档时看到的是被高亮过的实体判断“对不对”比从头“找实体”要快边界修正用Shift方向键微调就行。预标注模型的标签不能被直接自动确认必须由人工过一遍否则错误边界会被模型自我强化。预标注的收益在长文档上尤其明显长文档的实体密度通常更高。6.2 双人标注与一致性核验标注质量不能只看审核返修率还需要用双人标注的一致性来验证标注规范本身的清晰度。计算方式是两个标注员在同批文档上独立标注然后按实体的边界和类型对比。def cohens_kappa(a1: set, a2: set) - float: inter len(a1 a2) n len(a1 | a2) if n 0: return 1.0 p0 inter / n pe (len(a1) / n) * (len(a2) / n) if pe 1.0: return 0.0 return (p0 - pe) / (1 - pe) # 示例两份标注结果集合元素格式为 (sym, 11, 13) annotator_1 {(sym, 11, 13), (sym, 15, 17), (sym, 22, 24)} annotator_2 {(sym, 11, 13), (sym, 15, 17), (sym, 22, 24)} print(cohens_kappa(annotator_1, annotator_2))kappa值只是辅助判断。如果两名标注员的kappa长期低于0.6要回去查边界规则和类型定义如果只有某一个实体类型的边界出现系统性分歧就一定要修订标注规范让边界更明确。6.3 长尾实体的补齐方法医学知识图谱的高价值很大一部分来自低频但关键的长尾实体比如罕见药名、生僻手术名称。对这类实体最稳的方式是维护一份种子词表先做词典匹配粗召回再由标注员确认并补录同义词。反复循环之后种子词表会越滚越大模型对这些实体的识别率也会逐步上行。平台里为这种情况留一个专门的手工录入入口新增实体时可以同时录别名、概念ID和来源字段这样种子词表本身就沉淀成了一份高质量术语资源后续的预标注和实体标准化也都会跟着受益。本文还有配套的精品资源点击获取