跳转至

6.2 医疗知识图谱的搭建

医疗知识图谱的搭建

学习目标

  • 理解如何通过图数据库构建医疗知识图谱
  • 学会如何将结构化的医疗数据转化为知识图谱
  • 掌握如何使用Python代码与Neo4j交互,构建实体和关系

搭建图谱的基本步骤

  • 步骤1: 选择图数据库
  • 步骤2: 图谱搭建
  • 步骤3: 图谱展示

1 选择图数据库

  • 图数据库是存储和管理知识图谱的核心。选择合适的数据库工具对图谱的效率和可扩展性有重要影。
    • Neo4j:最常用的图数据库,支持Cypher查询语言,适合大规模医疗知识图谱的构建和查询。
    • RDF-based数据库(了解):如GraphDB、Virtuoso,适合处理语义网标准的知识图谱,如使用RDF、OWL等格式。
  • 相比传统Mysql等数据,图数据库的优势:
    • Neo4j图数据库能高效处理复杂关系、灵活的数据结构、简洁的查询语言、适合大规模图数据和推理能力等特点。而MySQL等关系型数据库在面对多层次、动态变化的复杂关联时,表现不佳,查询性能较差,且模型的扩展性较低。因此,构建医疗知识图谱时,选择Neo4j这样的图数据库会更有优势。

2 图谱搭建

  • 前面章节,我们已经学习了如何从文本中抽取实体、以及关系。那么如果要想搭建图谱,需要提前准备好相应的实体和关系等数据。
  • 说明:
    • 由于企业数据隐私性,本次搭建图谱我们将基于一个开源的医疗数据集来实现图谱的搭建。该数据集名称为:medical.json,对应存放位置:./NLP/MedicalKB/graph_data/medical.json
    • 整个图谱搭建的代码实现集中于一个py脚本,该脚本中共包含:一个方法和一个类;
      • 方法:用于查询medical.json的数量以及数据格式
      • 类:用于实现三元组抽取、实体插入、关系插入等
    • 代码路径:./NLP/MedicalKB/build_kg_utils.py

2.1 数据解析

  • 目标:读取并理解原始医疗数据(如JSON文件中的疾病、症状、药品等信息)。
  • 任务:解析medical.json文件,展示数据的结构,准备好用于构建图谱的实体和关系。
  • 构造方法:print_data_info()
  • 代码实现:

    def print_data_info(data_path):
        '''
        展示数据格式
        '''
        i = 0
        with open(data_path, 'r', encoding='utf8') as f:
            lines = f.readlines()
            print(f'数据长度:{len(lines)}') # 8808条样本
            for line in lines:
                data = json.loads(line)
                print(json.dumps(data, sort_keys=True, indent=4, separators=(', ', ': '), ensure_ascii=False))
                i += 1
                if i >= 5:
                    break
    
    # 运行该函数,展示数据内容:
    data_path = "./graph_data/medical.json"
    print_data_info(data_path)
    
  • 结果展示:

    • medical.json共包含8808条样本,每个样本格式为字典样式,每个键都代表了与医疗相关的信息,它们的中文意义解释如下:
    {
          # 数据库中的唯一标识符。
        "_id": {
            "$oid": "5bb578b6831b973a137e3ee8"
        }, 
          # 伴随疾病或并发症。
        "acompany": [
            "贫血"
        ], 
          # 疾病的分类,表明疾病所属的类别和科室。
        "category": [
            "疾病百科", 
            "急诊科"
        ], 
          # 疾病的病因
        "cause": "吸入苯蒸气或皮肤接触苯而引起的中毒,有急性、慢性之分。急性苯中毒主要对中枢神经系统产生麻醉作用,出现昏迷和肌肉抽搐;高浓度的苯对皮肤有刺激作用。长期接角低浓度的苯可引起慢性苯中毒,出现造血障碍,早期常见血白细胞数减低,进而出现血小板数减少和贫血。患者可有鼻出血、牙龈出血、皮下出血、月经过多等临床表现。\n急性苯中毒多为误服或急性吸入含苯的有机溶剂所致。这些有机溶剂包括油漆、稀料、工业胶水等。慢性苯中毒是长期吸入低浓度苯及代谢产物酚类所致。", 
          # 检查方法,列出确诊该疾病需要进行的医学检查项目。
        "check": [
            "血常规", 
            "骨髓象分析", 
            "先令氏指数"
        ], 
          # 推荐药品
        "common_drug": [
            "布美他尼片", 
            "十一味金色丸"
        ], 
          # 治疗费用,表示在不同医院治疗此疾病的费用范围。
        "cost_money": "根据不同病情,不同医院,收费标准不一致,市三甲医院约(5000——8000元)", 
          # 治疗科室,列出推荐治疗该疾病的医院科室
        "cure_department": [
            "急诊科"
        ], 
          # 治疗持续时间,表示从诊断到治愈所需的大概时间。
        "cure_lasttime": "根据不同病情一般为2-4周", 
          # 治疗方式,列出常用的治疗方案或方法。
        "cure_way": [
            "药物治疗", 
            "支持性治疗"
        ], 
          # 治愈概率,指治愈该疾病的预期成功率。
        "cured_prob": "约75%", 
          # 疾病描述,简要说明该疾病的概况和特征。
        "desc": "苯(benzene)是从煤焦油分馏及石油裂解所得的一种芳香烃化合物,系无色有芳香气味的油状液体。挥发甚速,易燃易爆。工业上用作溶剂、稀释剂和化工原料。苯属中等毒类,可引起急性或慢性中毒。\n急性苯中毒是指口服含苯的有机溶剂或吸入高浓度苯蒸气后,出现以中枢神经系统麻醉作用为主要表现的病理生理过程,主要症状轻者为醉酒状,步态不稳,哭笑失常,重者意识丧失,抽搐,可因呼吸中枢麻痹或循环衰竭死亡。慢性苯中毒是指苯及其代谢产物酚类影响了骨髓的造血功能,临床表现为白细胞计数和血小板立秋持续减少,最终发展为再生障碍性贫血或白血病。也可影响神经系统,表现为神经衰弱和自主神经功能紊乱。", 
          # 易吃食物
        "do_eat": [
            "鸡蛋", 
            "大豆", 
            "猪肉(瘦)", 
            "樱桃番茄"
        ], 
          # 药物详情,列出该疾病相关的推荐药物。
        "drug_detail": [
            "桂林南药布美他尼片(布美他尼片)", 
            "雄巴拉曲神水十一味金色丸(十一味金色丸)", 
            "西藏甘露仁青芒觉(仁青芒觉)", 
            "金诃藏药大月晶丸(大月晶丸)", 
            "金诃藏药仁青芒觉(仁青芒觉)"]
          # 容易患病
        "easy_get": "多见于制鞋工人及接触化工染料的工人", 
          # 患病概率,指该疾病在人群中的发生概率。
        "get_prob": "0.065%", 
          # 传播途径,描述该疾病的传染性和传播方式。
        "get_way": "无传染性", 
          # 疾病名称,表示该疾病的正式名称。
        "name": "苯中毒",
          # 忌吃食物
        "not_eat": [
            "海蟹", 
            "海虾", 
            "海参(水浸)", 
            "辣椒(青、尖)"
        ], 
          # 预防措施,列出预防该疾病的建议或注意事项。
        "prevent": "对于急性中毒患者,可以立即脱离现场至空气新鲜处,脱去污染的衣着,并用肥皂水或清水冲洗污染的皮肤。口服中毒者,要给患者洗胃,中毒者应卧床静息,并接受对症、支持治疗,可给予葡萄糖醛酸。要注意防止患者出现脑水肿,切记勿给心搏未停者使用肾上腺素,对由于苯中毒引起的再生障碍性贫血症患者,可给予小量多次输血及糖皮质激素治疗。", 
          # 推荐药物,列出治疗该疾病的常用药物。
        "recommand_drug": [
            "布美他尼片", 
            "十一味金色丸", 
            "注射用布美他尼", 
            "注射用呋塞米", 
            "盐酸利多卡因注射液"]
          # 推荐食物
        "recommand_eat": [
            "豆腐干炒韭菜", 
            "素炒小白菜", 
            "白菜蛋花粥"
        ], 
          # 症状,列出该疾病的典型临床症状。
        "symptom": [
            "恶心", 
            "抽搐", 
            "感觉障碍"
        ], 
          # 医保状态,指该疾病的治疗是否被纳入医保。
        "yibao_status": "否"
    }
    

2.2 三元组提取

  • 任务:从数据中提取实体及对应的关系
  • 构造类:class MedicalExtractor()
  • 说明:本次我们基于medical.json数据,提取了4种实体,以及4种关系。

    • 实体类型:

      疾病:diseases
      症状:symptoms
      食物:foods
      药品:drugs
      
    • 关系类型

      疾病-症状:[disease,'has_symptom', symptom]
      疾病-忌吃食物:[disease,'not_eat', food]
      疾病-易吃食物:[disease,'do_eat', food]
      疾病-推荐药品:[disease,'recommand_drug', drug]
      
  • 代码实现:

    class MedicalExtractor(object):
        def __init__(self):
            super(MedicalExtractor, self).__init__()
            self.graph = Graph("http://localhost:7474", auth=("neo4j", "12345"))
            # 共4类节点
            self.drugs = [] # 药品
            self.foods = [] # 食物
            self.diseases = [] #疾病
            self.symptoms = []#症状
    
            # 构建节点实体关系
            self.rels_noteat = [] # 疾病-忌吃食物关系
            self.rels_doeat = [] # 疾病-宜吃食物关系
            self.rels_recommanddrug = [] # 疾病-热门药品关系
            self.rels_symptom = []  # 疾病症状关系
    
        def extract_triples(self, data_path):
            print("从json文件中转换抽取三元组")
            with open(data_path,'r',encoding='utf8') as f:
                for line in tqdm(f.readlines(), ncols=80):
                    data_json = json.loads(line)
                    disease = data_json['name']
                    self.diseases.append(disease)
    
                    if 'symptom' in data_json:
                        self.symptoms += data_json['symptom']
                        for symptom in data_json['symptom']:
                            self.rels_symptom.append([disease,'has_symptom', symptom])
    
                    if 'acompany' in data_json:
                        for acompany in data_json['acompany']:
                            self.diseases.append(acompany)
    
                    if 'recommand_drug' in data_json:
                        recommand_drug = data_json['recommand_drug']
                        self.drugs += recommand_drug
                        for drug in recommand_drug:
                            self.rels_recommanddrug.append([disease,'recommand_drug', drug])
    
                    if 'not_eat' in data_json:
                        not_eat = data_json['not_eat']
                        for _not in not_eat:
                            self.rels_noteat.append([disease,'not_eat', _not])
                        self.foods += not_eat
    
                        do_eat = data_json['do_eat']
                        for _do in do_eat:
                            self.rels_doeat.append([disease,'do_eat', _do])
                        self.foods += do_eat
    
                    if 'drug_detail' in data_json:
                        for det in data_json['drug_detail']:
                            det_spilt = det.split('(')
                            if len(det_spilt) == 2:
                                p, d = det_spilt
                                d = d.rstrip(')')
                                self.drugs.append(d)
                            else:
                                d = det_spilt[0]
                                self.drugs.append(d)
    

2.3 实体插入

  • 任务:将提取的实体插入到Neo4j图数据库
  • 构造方法:在上述定义的类内,构造两个函数分别为write_nodes() 和create_nodes()
  • write_nodes函数实现:

    • 用于构造cypher语句,将实体写入数据库
    def write_nodes(self,entitys, entity_type):
        print("写入 {0} 实体".format(entity_type))
        for node in tqdm(set(entitys), ncols=80):
            cql = """MERGE(n:{label}{{name:'{entity_name}'}})""".format(
                label=entity_type, entity_name=node.replace("'",""))
            try:
                self.graph.run(cql)
            except Exception as e:
                print(e)
                print(cql)
    
  • create_nodes函数实现

    • 调用wirte_nodes函数,用于将所有实体统一写入数据库
    def create_entitys(self):
        self.write_nodes(self.drugs, '药品')
        self.write_nodes(self.symptoms, '菜谱')
        self.write_nodes(self.foods, '食物')
        self.write_nodes(self.diseases,'疾病')
    

2.4 关系插入

  • 任务:将实体之间的关系插入到Neo4j图数据库。
  • 构造方法:在上述定义的类内,构造两个函数分别为write_edges() 和 create_relations()
  • write_edges函数实现:

    • 用于构造cypher语句,将实体对应关系写入数据库
        def write_edges(self,triples,head_type,tail_type):
            print("写入 {0} 关系".format(triples[0][1]))
            for head,relation,tail in tqdm(triples,ncols=80):
                cql = """MATCH(p:{head_type}),(q:{tail_type})
                        WHERE p.name='{head}' AND q.name='{tail}'
                        MERGE (p)-[r:{relation}]->(q)""".format(
                       head_type=head_type,tail_type=tail_type,head=head.replace("'",""),
                            tail=tail.replace("'",""),relation=relation)
                try:
                    self.graph.run(cql)
                except Exception as e:
                    print(e)
                    print(cql)
    
  • create_relations()函数实现:

    • 调用write_edges函数,用于将所有实体关系统一写入数据库
    def create_relations(self):
        self.write_edges(self.rels_noteat, '疾病', '食物')
        self.write_edges(self.rels_doeat, '疾病', '食物')
        self.write_edges(self.rels_symptom, '疾病', '症状')
        self.write_edges(self.rels_recommanddrug, '疾病', '药品')
    

3 图谱展示

  • 下图是基于Neo4j数据库导入SPO三元组后展示效果:


小节总结

  • 本小节讲解了如何选择图数据库构建医疗知识图谱,并通过定义实体和关系、导入数据及使用查询语言进行操作,完成图谱的搭建和展示。