医学知识图谱竞赛工程拆解:从数据到模型融合
发布时间:2026/9/11 17:30:24来源:尧图网络
简介瑞金医院知识图谱大赛总决赛参赛源码与第四名项目说明面向医疗知识图谱、自然语言处理及竞赛实战学习者提供一套可运行的完整方案。包内共有4个文件包含Python主程序run.py用于执行核心流程txt与md文档说明参赛思路与复赛要点html文件可用于查看展示结果压缩包仅84KB轻量易部署。已有61人学习借鉴适合作为课程设计、期末作业或毕业设计的参考资料也能帮助备赛者理解竞赛中的实体识别、关系抽取及图谱构建等环节。代码结构清晰可直接运行调试配套说明对赛题背景、算法选型与关键实现进行梳理便于快速复现第四名队伍的实践路径。从数据预处理到模型推理均有对应代码支撑读者可结合自身数据替换调试深入掌握知识图谱落地方案。1. 一个竞赛压缩包里的知识图谱工程到底值多少信息量拿到“瑞金医院知识图谱大赛总决赛参赛源码项目说明第四名攻略.zip”这个压缩包的人多数是想复刻一条跑通医学知识图谱竞赛的完整路径。这类比赛通常要求参赛者在给定病历或临床文本上做实体识别、关系抽取最终产出可查询的知识图谱第四名的价值在于它既不是靠调大模型硬堆出来的方案也不会因为排名靠后而缺少细节。真正值得读的部分不是某个单点模型有多强而是从数据清洗、标注理解、模型融合到规则约束的整套取舍。这篇文章假定你已经知道知识图谱是什么直接讲我拿到这类竞赛项目文档之后会按什么顺序拆解、复现和二次改造成自己的方案适合准备入门医疗知识图谱构建的工程师、想从竞赛源码里找落地的算法同学也适合把知识图谱当长期技术方向的人先看一遍边界在哪。2. 拆比赛题目医学知识图谱数据长什么样先看三件事拿到源码不要急着看模型。医学知识图谱竞赛的标注数据和其他领域差异很大通常是“文本实体关系”的三层结构。先花半天把数据读明白比先跑通一个BERT模型重要得多。2.1 医学知识图谱竞赛考什么三元组抽取与图谱补全瑞金医院这个系列的比赛核心任务可以归成两类一类是纯文本抽取给一段主诉、现病史或检验报告要求抽出“药品-治疗-疾病”“疾病-表现-症状”“检验指标-增高-疾病”这类实体关系三元组另一类是做图谱补全给定已有的实体和部分关系预测缺失的边。源码包里如果同时出现ner/和re/两个目录说明主办方把任务拆成了两步如果只有graph/目录那重点就是链接预测和推理。我在复现任何医学知识图谱源码时都先把比赛任务映射成“实体边界关系类型”两个子问题而不是直接跑一个端到端生成模型。原因是医学实体嵌套严重“非小细胞肺癌”既是疾病又是修饰短语端到端模型很难区分边界而关系抽取只需要判断句子中两个已识别实体的语义关系拆开会稳定很多。命名实体识别和关系抽取分开建模还能各自调参、各自用不同的预训练模型这对竞赛刷分很重要。2.2 用一段Python把标注读出来先看标签再定模型大多数竞赛源码都会在data/raw下面放JSONL或标注文本。我一般第一件事是把标注文件加载出来统计实体类型和关系类型的分布再决定模型的分类头大小和是否需要降类。这里给一段可以直接跑的检查脚本竞赛复盘时最省时间的就是它。import json from collections import Counter def inspect_kg_data(path: str, top_k: int 25): ent_counter Counter() rel_counter Counter() sample_count 0 with open(path, r, encodingutf-8) as f: for line in f: item json.loads(line) sample_count 1 for ent in item.get(entities, []): ent_counter[ent[type]] 1 for rel in item.get(relations, []): rel_counter[rel[type]] 1 print(样本数:, sample_count) print(实体类型 top%d: % top_k, ent_counter.most_common(top_k)) print(关系类型 top%d: % top_k, rel_counter.most_common(top_k)) return ent_counter, rel_counter ent_cnt, rel_cnt inspect_kg_data(data/raw/train.jsonl, top_k25)逻辑说明item对应一条样本实体列表里存start、end、type三个字段关系列表里存head、tail、type。统计结果直接决定NER分类层的num_tags和关系分类层的num_relations。top_k25是因为医学数据里实体类型很容易超过30种比如检验指标、解剖部位、手术操作、药物、疾病、症状等先看前25个类型才能抓住主干剩下的再按长尾单独处理。2.3 关注长尾知识图谱标签多到只显示前25个时怎么办在医学知识图谱可视化工具里实体类型一旦超过25个界面通常只显示前25个标签剩下的即使画在图上也不可读。这个现象在训练阶段同样值得警惕类别不均衡会让少量高频类型主导损失低频实体直接学不到。我的处理方式是先看累计占比如果前10类实体占了90%以上就把后10类合并成OTHER如果关系类型超过20种只保留样本量前15的关系其余在验证阶段单独评估避免分类头参数被稀疏类白白占掉。检查项判断标准处理动作实体类型累计占比前10类达到90%以上低频类型合并为OTHER关系类型数量超过20个保留样本量前15其余忽略重叠实体比例超过30%改用嵌套NER或级联标注策略单条样本关系数平均大于3对关系抽取改用指针网络合并类型会牺牲召回但能让整体F1更稳。竞赛里“准确率优先”和“召回率优先”的取舍不同瑞金这类医疗场景的病历文本噪声大我更倾向于保住精确率因为后续图谱补全阶段会把漏掉的边补回来而错误边一旦进入图谱就难以清洗。3. 源码里的核心建模路径实体识别与关系抽取怎么同时做源码包里的模型部分通常是整份资料里最有含金量的地方。但许多第四名攻略代码的可读性并不好变量命名混乱、配置写死在训练脚本里。这一章我会把最通用的建模路径拆成可复现的模块来说。3.1 主线选型为什么是BERTBiLSTM-CRF而不是端到端生成近年知识图谱竞赛的主流做法是预训练模型微调而不是用大模型生成三元组。原因很直接NER任务对边界敏感生成模型容易把“非小细胞肺癌”生成成“肺癌”丢失限定语CRF层能显式建模标签转移B-Disease后面不允许直接跟O这类约束在医学文本里特别有效。这里的常见结构是BERT编码后接一层BiLSTM再接CRF虽然BiLSTM在长句上收益不大但能增加非线性对嵌套实体的边界修正有帮助。关系抽取不用生成式用分类式或指针网络。分类式适合实体对少的句子指针网络适合一对多场景。第四名攻略里如果有多个模型通常NER是BERTCRFRE是BERT分类两者共享同一个预训练模型但独立训练这样一处改动不会影响另一处。3.2 NER的最小可复现代码BERT编码加CRF解码下面是一个精简版NER模型PyTorch实现去掉了数据读取细节保留核心结构。竞赛源码里万变不离其宗改的只是num_tags和预训练模型名。import torch from torch import nn from transformers import BertModel, BertTokenizer class EntityCRF(nn.Module): def __init__(self, num_tags: int, pretrained: str bert-base-chinese): super().__init__() self.bert BertModel.from_pretrained(pretrained) self.dropout nn.Dropout(0.1) self.classifier nn.Linear(768, num_tags) self.crf CRF(num_tags, batch_firstTrue) def forward(self, input_ids, attention_mask, tagsNone): seq_out self.bert(input_ids, attention_maskattention_mask)[0] logits self.classifier(self.dropout(seq_out)) if tags is not None: return -self.crf(logits, tags, maskattention_mask.bool()) return self.crf.decode(logits, maskattention_mask.bool())逻辑说明BERT输出每个token的768维向量经过线性层得到num_tags个分数CRF层解码时考虑标签之间的转移概率。训练时返回负对数似然作为loss推理时decode直接给最优标签序列。attention_mask必须传入CRF的mask参数否则padding位置会算出虚假标签。num_tags的取值由第2章的统计决定一般是实体类型乘以2加O例如5类实体就是11个标签。注意PyTorch的CRF实现推荐用pytorch-crf库它是带mask的标准实现。别自己写转移矩阵容易在decode时陷入维特比边界错误。3.3 关系抽取的标注策略和5个必调参数关系抽取的输入构造有两种常见做法第一种把实体文本插到句子前后[CLS]实体1[SEP]实体2[SEP]原句交给BERT分类第二种在实体首尾加特殊标记如[E1]和[/E1]让模型关注实体位置。竞赛源码里常用第二种因为它对长句的鲁棒性更好。输出层就是num_relations类的线性分类关系为空也算一类。关系稀疏是医疗数据的老问题最容易涨分的不是换网络而是调分类阈值和损失函数权重。下面这5个参数是我每次都会先定下来的参数推荐值说明learning_rate2e-5微调BERT的常见起点超过5e-5容易震荡batch_size16显存受限时配梯度累积保持等效batch为32max_seq_len256病历文本长但超过512显存压力大先截断再观察warmup_ratio0.1前10%步数线性升学习率稳定BERT输出层relation_threshold0.3-0.5每个关系单独调见第4.1节max_seq_len256的截断策略要谨慎实体恰好落在[256, 300]区间时会被切掉导致这部分的实体永远无法识别。我一般先统计文本长度分布的90分位数再决定这个值而不是凭经验写死。4. 第四名攻略融合、规则约束与伪标签的工程顺序到了这一步单模型的上限基本固定能拉开差距的是后处理顺序。第四名的源码和第一名相比差的往往不是模型结构而是验证和融合的细致程度。4.1 交叉验证里的阈值搜索关系稀疏时必做关系分类的默认阈值是0.5但这个值在医学数据集上几乎总是次优的。一个关系类型只有50条正样本模型输出概率偏向0直接截断在0.5会丢掉大量真阳性。常见做法是5折交叉验证在每折的验证集上枚举0.1到0.9的阈值对每个关系单独选最优值。import numpy as np from sklearn.metrics import f1_score def find_best_threshold(y_true, y_prob, step0.05): best_th, best_f1 0.5, 0.0 for th in np.arange(0.1, 0.95, step): y_pred (y_prob th).astype(int) f1 f1_score(y_true, y_pred, averagemicro) if f1 best_f1: best_th, best_f1 th, f1 return best_th, best_f1逻辑说明y_true是0/1标签y_prob是模型输出的正类概率。阈值搜索的目标是让micro-F1最大而不是让准确率最大因为关系抽取任务里漏掉一条真实关系比误判一条更难被后续规则找回。参数step0.05够用了再细容易在验证集上过拟合。4.2 用领域规则约束三元组过滤模型的幻觉NER和RE模型都会犯错但医学知识图谱的实际应用场景不能容忍“药品与疾病之间是因果关系”这类低级错误。常见做法是在模型输出后加一层领域约束只保留类型组合合法的三元组。constraints { (Drug, treats, Disease), (Disease, has_symptom, Symptom), (Drug, contraindicates, Disease), (TestItem, elevated_in, Disease), } def rule_filter(triples, constraints): kept [] for s, p, o in triples: if (s[1], p, o[1]) in constraints: kept.append((s, p, o)) return kept逻辑说明每个三元组的s和o都是(实体文本, 实体类型)二元组s[1]和o[1]取出类型后与白名单比对。不在白名单里的候选直接丢弃相当于把模型的输出限制在领域专家认可的关系模式内。参数constraints可以按比赛关系定义表手工维护也可以从训练数据里统计top组合自动生成。这个规则过滤通常会让精确率提升3到5个百分点代价是召回略有下降。4.3 模型融合与伪标签的正确顺序融合不是把所有模型的输出直接平均。竞赛源码里一种可靠的做法是先用5折交叉验证得到每个模型的验证概率再在验证集上学习一个加权系数最后对测试集做加权融合。顺序上必须先融合再套规则如果先做规则过滤再做融合概率分布被改变了加权系数就失效了。融合策略实现方式适用场景概率平均多个模型logits取平均同结构模型最稳定投票法标签多数表决实体边界修正强模型会被拉低排序加权按单折F1得分加权模型差异大时阈值集成每个关系选最优单模型关系类型间差异大时伪标签的使用顺序比融合更敏感。我的做法是先训练NER和RE模型对无标注数据预测取概率高于0.9的样本加入训练集并控制伪标签数量不超过原训练集的30%。伪标签加在训练后期前几个epoch不动防止模型把自己早期的错误当成学习目标。4.4 源码工程结构怎么组织才能反复实验第四名攻略的源码能被大家认可很大程度是因为工程结构清晰。一个可复现的医学知识图谱项目目录应该长这样kg-competition/ ├── configs/ │ ├── ner.yaml │ └── re.yaml ├── data/ │ ├── raw/ │ ├── processed/ │ └── pseudo/ ├── src/ │ ├── ner/ │ ├── re/ │ └── graph/ ├── scripts/ │ ├── train_ner.sh │ ├── train_re.sh │ └── predict.sh └── output/ ├── models/ └── preds/configs目录用yaml存参数训练脚本从yaml读取禁止把参数写在代码里data/pseudo单独存放伪标签数据防止污染原始训练集output/preds按日期和折数命名方便回放。这样每次实验只需要改yaml不需要改代码竞赛后期迭代速度会快很多。5. 验证与复盘第四名和第一名之间通常差在哪到了这一章模型训练和融合已经结束剩下的是验证和复盘。这部分决定了你的最终名次是暂时靠运气还是稳定在前排。5.1 把预测结果灌进Neo4j用Cypher检查图谱质量把测试集预测出的三元组导入Neo4j用图查询能看到单独看指标发现不了的问题。比如孤立实体、关系度异常、同一个实体在不同句子中被标记成不同类型。下面这条Cypher可以找出没有连接到任何关系的孤立节点MATCH (e) WHERE NOT (e)-[]-() RETURN e.label, e.type, count(*) AS isolated ORDER BY isolated DESC LIMIT 20;逻辑说明(e)-[]-()会匹配与任意关系相邻的节点NOT取反后剩下的就是孤立实体。如果孤立实体超过预测三元组总量的5%说明NER结果里有大量实体没被关系抽取利用多半是实体类型判断不一致导致的。5.2 三个容易丢分的细节实体边界、重叠关系、数值归一竞赛复盘中反复出现的问题是实体边界错位“非小细胞肺癌”被拆成“非小细胞”和“肺癌”两个实体。处理方式是把NER预测结果和原文逐token对齐验证每个实体的首尾字符是否在原始文本中真实存在而不是由BERT subword拼接出来的。重叠关系指一句话里多对实体共享同一个主语或宾语比如“患者服用了阿司匹林和氯吡格雷胸痛缓解”这里有两对关系。关系分类模型很容易只抽取第一对就结束。我一般会对同一句话做多次推理每轮屏蔽已抽取的实体对直到无新关系产出。数值归一化针对化验指标类实体1000和1000在原始文本中是不同字符串但在知识图谱里应该归一化成同一个实体或至少同一实体属性。这个不做图谱会出现重复节点查询时召回率会下降。5.3 提交前保存每折输出用本地F1回放排行榜波动最后一件事是回放把5折验证的每一折预测结果都保存下来算出本地F1后再和线上排行榜分数做对照。如果本地F1涨了1个百分点而线上分数没涨说明有类型分布不一致的问题优先检查是否是类别合并策略在测试集上不适用如果本地没涨而线上涨了更可能是随机种子波动不要盲目认为模型变强了。比赛到后期排名的变化往往由这种微小差异决定源码里的模型结构反而不是关键变量。本文还有配套的精品资源点击获取
网站建设高端定制企业官网