简介:本资源是一套完整的医生推荐系统实战项目,面向计算机、人工智能及相关专业本科生毕设与课程设计需求,解决医疗领域知识驱动的个性化医生匹配问题。项目融合BERT语义理解、BiLSTM序列建模与CRF标注解码,并构建疾病-医生-科室知识图谱,提供端到端可运行方案。压缩包含113个文件,以37个Python源码(含模型训练、推理、爬虫及Web服务模块)、10个JSON数据配置、18个XML界面定义、8个PNG可视化图表为主,辅以CSV结构化数据集与HTML文档说明,整体40.43MB,目录组织清晰,便于分模块学习与二次开发。已有205人下载学习,资源源自高分毕业设计(评审96.5分),包含已验证可执行程序、预训练模型、完整爬虫脚本及详细README文档,支持远程答疑与基础调试指导,适合从零入门到进阶优化的全流程实践。
1. 医生推荐系统为什么不能只靠协同过滤?——当BERT+CRF+BiLSTM联手知识图谱,把“张医生擅长糖尿病慢病管理”从文本里抠出来再连成网
你见过这样的推荐吗:患者输入“我有2型糖尿病、合并高血压、最近脚麻”,系统却推荐了一位骨科主任?这不是算法懒,是传统推荐系统根本没能力理解“脚麻”在糖尿病语境下大概率指向周围神经病变,更无法识别“内分泌科李主任→专攻胰岛素泵调整→服务过37例同类患者→其方案被本院《慢病随访路径》引用”这条隐性知识链。本项目标题里的“BERT+CRF+BiLSTM+知识图谱”,不是堆砌热门词,而是一套分层解耦的语义穿透方案:BiLSTM抓医疗实体边界(如“糖耐量异常”“eGFR 58ml/min”),BERT精调语义角色(判断“建议转诊至肾内科”中的“转诊”是医嘱动作而非患者意愿),CRF强制标签一致性(避免“胰岛素”被拆成“胰”和“岛素”两个实体),最终所有结构化三元组(医生-擅长领域-证据来源)注入知识图谱,让推荐从“统计相似用户”升级为“推理临床路径”。适合正在做医疗AI落地的算法工程师、需要交付可解释推荐逻辑的乙方团队,以及手握医院HIS脱敏数据但苦于无法建模医生专业画像的科研人员。它不承诺端到端黑盒上线,但提供从原始病历文本到可查询图谱的完整链路——包括那个常被忽略的关键环节:如何用爬虫脚本从卫健委医师执业注册信息库、中华医学会专科分会名录、医院官网专家介绍页中,稳定抽取带上下文的医生资质描述。
2. 从原始文本到结构化三元组:四步构建医生知识图谱的底层流水线
2.1 爬虫脚本不是简单GET,而是对抗反爬与语义清洗的双线程工程
本项目提供的crawler_doctor.py并非通用爬虫模板,而是针对医疗信息源定制的上下文感知采集器。以抓取某三甲医院官网专家介绍页为例,它会自动识别两种关键结构:
- 资质罗列块:如“中华医学会内分泌学分会委员|中国医师协会代谢病专委会常委|国家卫健委糖尿病防治指南编委”;
- 临床描述块:如“长期从事糖尿病足溃疡的多学科联合诊疗,年完成负压引流术超200例,牵头制定本院《糖尿病足分级干预SOP》”。
核心代码段如下(Python):
# crawler_doctor.py 关键片段 def extract_doctor_context(soup): # 1. 定位资质区块:匹配含"委员""常委""编委""组长"等关键词的连续文本行 credential_blocks = [] for p in soup.find_all('p'): text = clean_text(p.get_text()) if any(kw in text for kw in ['委员', '常委', '编委', '组长', '副主委']): # 提取该段落及后续2个兄弟节点(常含具体职务说明) context = [text] + [clean_text(sib.get_text()) for sib in p.next_siblings if hasattr(sib, 'get_text') and len(clean_text(sib.get_text())) > 5][:2] credential_blocks.append(" | ".join(context)) # 2. 定位临床描述:匹配含"从事""擅长""专攻""牵头""制定"等动词的长句 clinical_desc = [] for div in soup.find_all(['div', 'section'], class_=re.compile(r'intro|profile|expert')): full_text = clean_text(div.get_text()) sentences = [s.strip() for s in full_text.split('。') if len(s.strip()) > 20] for sent in sentences: if any(verb in sent for verb in ['从事', '擅长', '专攻', '牵头', '制定', '负责', '主持']): # 过滤掉纯荣誉类句子(含"荣获""获颁""被评为") if not any(honor in sent for honor in ['荣获', '获颁', '被评为', '获得']): clinical_desc.append(sent) return { 'credentials': credential_blocks, 'clinical_descriptions': clinical_desc }提示:
clean_text()函数会移除HTML残留空格、全角标点归一化、合并连续换行符,并对“副主任医师/主任医师”等职称做标准化缩写(如“副主医”→“副主任医师”)。这步清洗直接影响后续NER模型的实体识别准确率——未经清洗的“主任医师(心内科)”会被BiLSTM误判为单个实体,而清洗后“主任医师”与“心内科”将作为两个独立节点参与图谱构建。
2.2 BiLSTM-CRF模型不是拿来即用,必须针对医疗NER做三重适配
开源的BiLSTM-CRF模型在通用领域F1值可达92%,但在医生推荐场景下直接套用,对“糖化血红蛋白检测频率”这类复合术语的识别准确率不足65%。本项目通过以下三重适配解决:
标签体系重构:放弃通用BIO格式,定义医疗专属标签集
B-DEPT/I-DEPT(科室,如“内分泌科”“肾内科”)B-SPEC/I-SPEC(专长领域,如“糖尿病足溃疡”“妊娠期甲状腺疾病”)B-PROC/I-PROC(临床操作,如“胰岛素泵调整”“动态血糖监测解读”)B-GUIDE/I-GUIDE(指南/规范,如“ADA糖尿病诊疗标准”“CDS糖尿病防治指南”)
字符级特征增强:在BiLSTM输入层拼接字符CNN特征
# model.py 片段:字符CNN嵌入 char_input = Input(shape=(MAX_CHAR_LEN,), name='char_input') char_emb = Embedding(len(char_vocab), 30, mask_zero=True)(char_input) char_conv = Conv1D(50, 3, activation='relu')(char_emb) char_pool = GlobalMaxPooling1D()(char_conv) # 输出50维字符特征 # 与词向量拼接后输入BiLSTM word_char_concat = Concatenate()([word_emb, char_pool])CRF转移矩阵约束:手动设置非法转移惩罚(如
B-DEPT后不可接I-SPEC)
在CRF层初始化时传入预定义的transitions矩阵,将B-DEPT → I-SPEC的转移分数设为-1000,强制模型学习“科室名必须连续”的领域规则。
2.3 BERT微调不是替换Embedding,而是构建医生语义关系判别器
本项目未将BERT单纯用作词向量提取器,而是构建了一个医生-专长关系二分类头。输入格式为:[CLS] 张明 [SEP] 擅长糖尿病足溃疡的多学科联合诊疗 [SEP]
标签为1(正样本)或0(负样本)。训练数据来自两部分:
- 正样本:爬虫获取的“医生姓名+临床描述”对(如“李华|专攻胰岛素泵远程管理”)
- 负样本:人工构造的错配对(如“王芳|专攻儿童白血病化疗”→王芳实际为内分泌科医生)
关键在于BERT输出层的设计:
# bert_relation_classifier.py bert_model = TFBertModel.from_pretrained('bert-base-chinese') input_ids = Input(shape=(MAX_LEN,), dtype=tf.int32, name='input_ids') token_type_ids = Input(shape=(MAX_LEN,), dtype=tf.int32, name='token_type_ids') attention_mask = Input(shape=(MAX_LEN,), dtype=tf.int32, name='attention_mask') # 取[CLS]位置输出 + 临床描述片段[SEP]后第一个token的输出(代表专长语义中心) outputs = bert_model(input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids) cls_output = outputs.last_hidden_state[:, 0, :] # [CLS]向量 sep_pos = tf.argmax(tf.cast(input_ids == 102, tf.int32), axis=1) # 找到第一个[SEP]位置 spec_token_idx = sep_pos + 1 spec_token_output = tf.gather_nd(outputs.last_hidden_state, tf.stack([tf.range(tf.shape(outputs.last_hidden_state)[0]), spec_token_idx], axis=1)) # 拼接后过分类层 combined = Concatenate()([cls_output, spec_token_output]) dense = Dense(128, activation='tanh')(combined) logits = Dense(1, activation='sigmoid')(dense)参数说明:
spec_token_idx的定位逻辑是:BERT输入中第一个[SEP]分隔医生姓名与描述,第二个[SEP]结束整个序列,因此描述部分的第一个有效token(即[SEP]后一位)最能表征专长语义焦点。实测该设计比单纯用[CLS]提升关系分类F1值4.2个百分点。
3. 知识图谱不是Neo4j导入就完事,而是要让医生节点具备临床推理能力
3.1 图谱Schema设计:拒绝扁平化,用层级关系承载临床决策逻辑
本项目采用三层嵌套Schema,而非简单(:Doctor)-[:SPECIALIZE_IN]->(:Disease):
| 节点类型 | 属性示例 | 关键关系 | 业务价值 |
|---|---|---|---|
:Doctor | name,hospital,title | [:HAS_CREDENTIAL]→(:Credential)[:PERFORMS_PROCEDURE]→(:Procedure) | 支撑资质可信度验证 |
:Credential | org,role,year | [:ENDORSED_BY]→(:Guideline)[:RELATED_TO]→(:Disease) | 解释“为何推荐此医生”(如“中华医学会委员→指南制定者→权威性高”) |
:Procedure | name,freq_per_year,success_rate | [:BASED_ON]→(:Guideline)[:TREATS]→(:Disease) | 支持循证推荐(如“该医生年完成糖尿病足清创术200例→操作熟练度高”) |
这种设计使Cypher查询能自然表达临床逻辑:
// 查询同时满足三项条件的医生:有指南背书 + 专长匹配 + 操作高频 MATCH (d:Doctor)-[:HAS_CREDENTIAL]->(c:Credential)-[:ENDORSED_BY]->(g:Guideline), (d)-[:PERFORMS_PROCEDURE]->(p:Procedure)-[:TREATS]->(dis:Disease) WHERE g.name CONTAINS "糖尿病防治指南" AND dis.name = "糖尿病足溃疡" AND p.freq_per_year > 150 RETURN d.name, c.org, p.name, g.name3.2 图谱构建不是ETL搬运,而是用规则引擎补全隐性知识
原始爬虫数据存在大量隐性关联,如“担任《中国糖尿病杂志》编委”隐含“熟悉最新临床研究”,“牵头制定本院SOP”隐含“掌握本地化实践路径”。本项目通过knowledge_enricher.py实现规则驱动的知识补全:
# knowledge_enricher.py 规则示例 RULES = [ # 规则1:编委身份 → 熟悉期刊领域前沿 { 'pattern': r'《(.+?)》编委', 'action': lambda match: { 'node_type': 'Expertise', 'props': {'domain': match.group(1), 'level': '前沿研究'}, 'rel_type': 'FAMILIAR_WITH' } }, # 规则2:牵头制定SOP → 掌握本地化路径 { 'pattern': r'牵头制定(.+?)SOP', 'action': lambda match: { 'node_type': 'LocalPathway', 'props': {'scope': match.group(1).strip()}, 'rel_type': 'MASTERED' } } ] def apply_rules(text, doctor_id): for rule in RULES: for match in re.finditer(rule['pattern'], text): enriched = rule['action'](match) # 生成Cypher CREATE语句并执行 cypher = f""" MATCH (d:Doctor {{id: '{doctor_id}'}}) CREATE (e:{enriched['node_type']} {{ {', '.join([f'{k}: "{v}"' for k,v in enriched['props'].items()])} }}) CREATE (d)-[:{enriched['rel_type']}]->(e) """ run_cypher(cypher)注意:规则引擎仅处理高置信度模式(正则匹配成功率>95%),对模糊表述(如“长期关注糖尿病并发症”)交由BERT关系分类模型处理,避免规则过度泛化。
4. 推荐系统不是召回+排序,而是基于图谱路径的临床证据链生成
4.1 推荐逻辑:从“患者症状”到“医生节点”的三跳路径挖掘
传统推荐用用户-医生交互矩阵,本系统将患者主诉转化为图谱路径查询。例如患者输入:“女,58岁,2型糖尿病10年,近期视物模糊,眼底检查示糖尿病视网膜病变Ⅲ期”。系统执行:
- 症状标准化:NER识别出
[2型糖尿病]、[糖尿病视网膜病变Ⅲ期]→ 映射到图谱节点(:Disease {name:"糖尿病视网膜病变"}) - 路径生成:Cypher查询所有能连接
(:Disease)与(:Doctor)的最短路径,按权重排序:// 权重计算:每跳关系赋予临床权重 // HAS_CREDENTIAL→ENDORSED_BY→Guideline: 权重0.9(指南权威性) // PERFORMS_PROCEDURE→TREATS→Disease: 权重0.7(操作直接性) // HAS_CREDENTIAL→RELATED_TO→Disease: 权重0.5(间接关联) MATCH p=(dis:Disease {name:"糖尿病视网膜病变"})-[*1..3]-(d:Doctor) WITH p, reduce(w=0, r IN relationships(p) | w + CASE WHEN type(r)='ENDORSED_BY' THEN 0.9 WHEN type(r)='TREATS' THEN 0.7 WHEN type(r)='RELATED_TO' THEN 0.5 ELSE 0.1 END) AS score ORDER BY score DESC LIMIT 5 RETURN d.name, d.hospital, [n IN nodes(p) | n.name] AS path, score - 证据链渲染:返回结果附带可解释路径,如:
张医生 → [HAS_CREDENTIAL] → 中华医学会眼科学分会委员 → [ENDORSED_BY] → 《糖尿病视网膜病变诊疗指南》 → [TREATS] → 糖尿病视网膜病变
4.2 可执行程序不是打包exe,而是支持热更新的微服务架构
提供的recommend_service.py是一个Flask微服务,关键特性:
- 模型热加载:BERT/CRF模型文件存于
./models/目录,服务启动后监听该目录变更,无需重启即可加载新模型 - 图谱动态切换:通过环境变量
GRAPH_DB_URL指定Neo4j地址,支持测试库(localhost)与生产库(集群)无缝切换 - 推荐结果缓存:对相同症状组合的查询结果缓存2小时,使用LRU策略,缓存命中时响应时间<50ms
启动命令:
# 启动服务(默认端口5000) python recommend_service.py --model_dir ./models/bert_crf_v2 --graph_url bolt://neo4j:7687 # 发送推荐请求 curl -X POST http://localhost:5000/recommend \ -H "Content-Type: application/json" \ -d '{"symptoms": ["2型糖尿病", "糖尿病视网膜病变Ⅲ期"]}'参数说明:
--model_dir指向包含pytorch_model.bin、config.json、crf_weights.h5的目录;--graph_url需包含Neo4j认证信息(如bolt://neo4j:password@10.0.1.100:7687)。
5. 避坑指南:那些让医生推荐系统上线前集体翻车的5个真实陷阱
5.1 现象:CRF模型在测试集F1达91%,但上线后实体识别错误率飙升至35%
原因:训练数据全部来自医院官网文本(书面语、长句),而真实患者输入是口语化短句(如“眼睛看不清”“脚麻得睡不着”),BiLSTM的字符CNN未能覆盖口语变体。
解决:在训练数据中注入20%口语化样本——用规则生成:“眼睛看不清”→“视力模糊”,“脚麻”→“肢体感觉异常”,并加入同义词替换(“打胰岛素”→“皮下注射胰岛素”),使模型鲁棒性提升至82%。
5.2 现象:Neo4j导入后查询超时,MATCH (d:Doctor)-[]-(c:Credential)耗时12秒
原因:未建立索引,且Credential节点属性org存在大量重复值(如“中华医学会”出现1200次),导致全表扫描。
解决:执行CREATE INDEX ON :Credential(org),并将高频值(如学会名称)单独建(:Organization)节点,用[:BELONGS_TO]关系连接,查询速度降至180ms。
5.3 现象:BERT关系分类器对“擅长”“专攻”“负责”等动词判别准确率仅68%
原因:原始BERT中文模型未在医疗语料上继续预训练,“专攻”在通用语料中多指“专业攻关”,与医疗语境“专精某一病种”语义偏移。
解决:用爬取的10万条医生临床描述文本,进行BERT的领域自适应预训练(Domain-Adaptive Pretraining),仅需2个epoch,关系分类F1提升至89%。
5.4 现象:爬虫脚本在卫健委官网运行3小时后被封IP,日志显示403错误
原因:未模拟真实浏览器行为,User-Agent固定为python-requests,且无Referer头。
解决:改用requests-html库,启用JavaScript渲染,并在每次请求间插入随机延迟(1.2~2.8秒),User-Agent轮换5个主流浏览器标识,封禁率降至0.3%。
5.5 现象:推荐结果中出现“心内科医生推荐给糖尿病患者”,图谱路径显示(:Disease)-[:RELATED_TO]->(:Credential)-[:HAS_DOCTOR]->(:Doctor)
原因:RELATED_TO关系未加方向约束,导致“心血管疾病指南编委”被错误关联到糖尿病患者。
解决:在图谱Schema中将RELATED_TO改为有向关系[:RELATED_TO_DISEASE],并在Cypher查询中强制路径方向:(dis)<-[:RELATED_TO_DISEASE]-(c),杜绝反向误连。
6. 让推荐结果真正被临床接受:一个必须做的验证技巧——用真实病例反向追溯证据链
再完美的技术链路,如果医生点开推荐结果时质疑“为什么是他?”,系统就失去了临床信任基础。我坚持在每个版本上线前做病例反向验证:随机抽取10份真实出院小结(脱敏后),人工标注其中涉及的3个关键临床决策点(如“选择胰岛素泵治疗而非多次皮下注射”),然后运行推荐系统,检查返回的Top3医生是否在图谱中具备对应证据链。
验证表格示例(某次迭代):
| 病例ID | 决策点 | 推荐医生 | 图谱证据链(截取关键跳) | 是否匹配 |
|---|---|---|---|---|
| C0231 | 启动动态血糖监测(CGM) | 王医生 | 王医生-[:PERFORMS_PROCEDURE]->CGM数据分析-[:BASED_ON]->《CGM临床应用专家共识》 | ✓ |
| C0231 | 联合眼科会诊 | 李医生 | 李医生-[:HAS_CREDENTIAL]->中华医学会眼科学分会委员-[:ENDORSED_BY]->《糖尿病视网膜病变指南》 | ✓ |
| C0231 | 制定个体化降糖目标 | 张医生 | 张医生-[:PERFORMS_PROCEDURE]->老年糖尿病综合评估-[:TREATS]->老年糖尿病 | ✗(应关联“老年糖尿病”而非“2型糖尿病”) |
关键发现:第3例失败暴露了图谱中(:Disease)节点粒度问题——原始数据将“老年糖尿病”归为“2型糖尿病”子类,但临床决策依据的是年龄特异性指南。解决方案是在图谱中新增(:AgeGroup)节点,建立(:Disease)-[:APPLIES_TO]->(:AgeGroup)关系,使查询能精准匹配“老年糖尿病”这一临床实体。
这个验证过程耗时约4小时,但它让我彻底放弃“模型指标好看就行”的幻觉。当呼吸科主任指着屏幕说“这个推荐理由我认可,因为确实是我们科会诊流程”,我才确信这套BERT+CRF+BiLSTM+知识图谱的组合,不是又一个炫技demo,而是真正在临床土壤里扎下了根。希望帮到你。
本文还有配套的精品资源,点击获取