简介:面向中文医学知识图谱构建的实体关系标注工具CMeKG-labelingPlatform-main,适合NLP研究者、医疗信息开发者及知识图谱入门者,用于高效解决医学文本中的命名实体识别与关系抽取标注难题。资源包共472个文件,核心类型包括js和jsx前端交互脚本、py模型训练与标注算法、json数据配置、html页面、css样式及字体图标,整体约10.34MB,便于本地部署与二次开发。平台提供可视化标注界面、批量数据管理、多人协作校验以及API接口,可结合BiLSTM+CRF、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个字符,所以start=11,end=13。按偏移量存储的最大好处是后续做BIO序列标注转换完全无损耗,文本一旦被分词工具重新切分,可以直接用偏移量映射回去。
3. 部署与初始化:CMeKG-labelingPlatform的服务配置和语料导入
3.1 依赖环境与配置文件要点
实体关系标注平台采用前后端分离是常见工程形态,后端处理数据存取和API,前端做文本可视化与快捷键交互,标注数据落库。数据库方面MySQL或PostgreSQL都能胜任,标注平台本身读写压力不大,真正要留意的是两个地方:任务领取的批量大小和上传语料的格式约束。
下面的配置文件是这类平台的典型形态:
server: host: 0.0.0.0 port: 8080 debug: false database: dialect: mysql+pymysql 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-8"batch_size值得单独说。一个标注员的单次领取量如果是20个句子,能把上下文牢牢锁在工作台内;设置得太大比如100句,不仅前端渲染卡顿,标注员的注意力也会分散。max_retry控制审校退回到标注员手里的次数上限,超过三次这个文档会被列为争议文档,转给仲裁人处理。auto_assign打开后,系统会按界面里的待办数做简单负载均衡,避免部分标注员排不上队、另一部分闲等。
| 配置项 | 推荐值 | 影响 |
|---|---|---|
| batch_size | 20 | batch过小频繁取件、过大加载和注意力都下降 |
| max_retry | 3 | 用于阻断低质量标注的无休止返工 |
| auto_assign | true | 按待办数自动分配,避免忙闲不均 |
| upload.max_size_mb | 10 | 控制单次批量导入的语料体积 |
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", encoding="utf-8") as fp: text = fp.read() payload = { "task_name": task_name, "docs": [ { "text": text, "meta": {"source": path} } ] } resp = requests.post(f"{API}/tasks/import", json=payload) 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(f"task_id: {tid}")这里的task_name不是随便起的,建议按科室或文档来源分类命名,方便后续按任务维度统计质量和追溯。meta字段里放来源路径、科室、采集时间,导出时这些信息会跟着走,是做质量回溯和错误样例分析的第一手材料。
4. 标注实操:实体打标、关系连线与审核返修
4.1 工作台布局与快捷键操作节奏
实体标注工作台的核心区域是文本面板,一般分上下两栏:上栏是当前句子和上下文预览,下栏是实体列表。文本里的实体用底色块标出,悬浮显示类型名。标注员的操作路径是:读句子,选中文本,按实体类型快捷键,继续下一条。判断时间主要在读句子上,操作本身要压缩到一秒以内,所以快捷键设计直接决定产能。
下面是整理过的快捷键映射表,可以作为前端交互默认值:
| 快捷键 | 功能 | 说明 |
|---|---|---|
| d / s / m / e / b / o | 标记实体 | 分别对应六种实体类型,按下即完成标注 |
| Shift+左右方向键 | 微调边界 | 修正选中范围多一个字或少一个字 |
| R | 关系连线模式 | 进入后先点主语实体再点宾语实体 |
| Esc | 取消当前操作 | 连线和选中状态一键回退 |
| Ctrl+Z / Ctrl+Y | 撤销/重做 | 支持按步骤回退 |
| 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 标准类型 | 附加映射字段 |
|---|---|---|
| 疾病 | disease | icd10_code |
| 症状体征 | symptom | 作为 disease 下位概念处理 |
| 药物 | drug | atc_code |
| 检查检验 | examination | 关联到检查项目本体 |
| 解剖部位 | body_part | FMA_ID 映射 |
| 手术操作 | operation | 手术分类码 |
映射过程不需要全部自动化。常见实体可以先做自动映射,自动匹配不到或置信度低的实体进入人工确认队列。这个“自动加人工”的流程,是知识图谱构建中最可靠的方式,比完全依赖规则匹配或者在标注界面增加大量字段都省力。
5.3 三元组生成脚本
导出结果到SPO(subject-predicate-object)三元组的转换,核心是处理实体ID到标准化名词的解析。实体列表和关系列表分开存储,关系里只有entity id,转换时要先建实体字典再遍历关系。
import json def to_spo(export_path: str): with open(export_path, "r", encoding="utf-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(f"missing 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和来源字段,这样种子词表本身就沉淀成了一份高质量术语资源,后续的预标注和实体标准化也都会跟着受益。
本文还有配套的精品资源,点击获取