因为公司业务需要,可能要用到知识图谱相关项目的落地经验,调研了一些比较有借鉴学习意义的知识图谱搭建项目,对知识图谱的搭建全流程有了一个清晰的认识,在这里做一下梳理和记录。

 一、流程简介

1.知识图谱基本结构

        知识图谱,是一种高度结构化的,用图形式存储的语义知识库。用于迅速描述物理世界中的概念及其相互关系,通过知识图谱能够将Web上的信息、数据以及链接关系聚集为知识,使信息资源更易于计算、理解以及评价,并能实现知识的快速响应和推理。其基本单元结构就是由“实体-关系-实体”构成的三元组,实体自身还包含一些属性信息,由“实体-属性-值”构成。本体则是实体的抽象,也可以理解为集合或者编程里面的类。

        例如:“梁朝伟-妻子-刘嘉玲”就是一个包含两个实体和其关系的三元组,它包含了二者关系的信息。而“梁朝伟-主演-无间道”则表示“梁朝伟”这个实体在“主演”这个属性下面的属性值是“无间道”(可能还具有年龄、性别等其他属性)。而“梁朝伟”、“刘嘉玲”都属于本体“演员”。

2.搭建基本流程

        目前使用较多的是一种自底向上的构建思路,也就是借助一定的技术手段,从互联网海量的信息资源或现有的数据集、数据库等提取出资源,选择其中置信度较高的信息,加入到知识库中,并进行整合、结构化。其技术流程分为以下几步:

数据搜集。也就是根据业务需求背景,从本地数据库或者互联网,利用数据库、爬虫等技术对构建知识图谱的潜在信息进行搜集,这些数据可能是结构化的(如Mysql等数据库里的数据)、半结构化的(如XML、json、exl文件等)或者非结构化的(如未经整理的图片、文字、视频等)。知识抽取。知识抽取又分为实体抽取、关系抽取、属性抽取。很容易理解,这三者分别对应构建知识图谱的基本构成要素。

实体抽取。就是从搜集到的数据中利用找到潜在的主体。如文本“18岁的三合会会员刘建明听从大哥韩琛的指示进入警校学习,成为警方卧底。”当中,可以提取到“三合会”、“刘建明”、“韩琛”、“警校”等实体。关系抽取。就是从搜集到的数据中,找出当中实体间的关系描述,并进行整合,组成实体间的关系网。如实体“韩琛”就是实体“刘建明”的“大哥”。属性抽取。就是从搜集到的数据中采集特定实体的属性信息。

知识融合。通过信息抽取,从原始语料中提取出了实体、关系与属性等知识要素,还需要经过知识融合,消除实体指称项与实体对象之间的歧义,得到一系列基本的事实表达。

实体消歧。专门用于解决同名实体产生歧义问题的技术,通过实体消歧,就可以根据当前的语境,准确建立实体链接,实体消歧主要采用聚类法。其实也可以看做基于上下文的分类问题,类似于词性消歧和词义消歧。共指消解。主要用于解决多个指称对应同一实体对象的问题。在一次会话中,多个指称可能指向的是同一实体对象。利用共指消解技术,可以将这些指称项关联(合并)到正确的实体对象,由于该问题在信息检索和自然语言处理等领域具有特殊的重要性,吸引了大量的研究努力。共指消解还有一些其他的名字,比如对象对齐、实体匹配和实体同义。

知识加工。知识加工主要有三个方面内容:本体抽取、知识推理和质量评估。

本体抽取。可以采取手工方式构建,也可以采用脚本自动化的方式构建(相似度计算等方式)。当前主流的全局本体库产品,都是从一些面向特定领域的现有本体库出发,采用自动构建技术逐步扩展得到的。知识推理。要构建一个比较体系较为完备全面的知识图谱,还需要深层次挖掘实体中的潜在关系和属性信息。例如知道A是B的父亲,B是C的父亲,则能得到A是C的爷爷这样的信息,而这个信息是原生语义信息中不包括的。这一块的算法主要可以分为3大类:基于知识表达的关系推理技术;基于概率图模型的关系推理技术路线示意图;基于深度学习的关系推理技术路线示意图。质量评估。质量评估也是知识库构建技术的重要组成部分,这一部分存在的意义在于:可以对知识的可信度进行量化,通过舍弃置信度较低的知识来保障知识库的质量。

二、医疗知识图谱问答项目

1.项目简介

        该项目立足医药领域,以垂直型医药网站为数据来源,以疾病为核心,构建起一个包含7类规模为4.4万的知识实体,11类规模约30万实体关系的知识图谱,并基于此知识图谱实现了基本的自动问答。个人觉得是一个很好的知识图谱学习、练手的项目。该项目地址:https://github.com/liuhuanyong/QASystemOnMedicalKG/tree/master。

2.项目框架

        项目整体框架如图所示(来自项目地址)。该框架基本也是项目整体的实现思路和流程。这里对源码学习过程作简单的记录。

2.1图谱搭建

        项目作者为了让广大学习者实现快速部署,将其从垂直网站爬取的医疗行业数据制作成了json格式,如果只是复现代码,那么只要按照项目主页的步骤配置好neo4j数据库及相应的python依赖包(py2neo等),运行build_medicalgraph.py文件将准备好的json文件导入图数据库,就可以启动chat_graph.py文件进行问答了。而作者自己进行数据的爬取、整理主要是prepare_data目录下的三个.py文件进行的,这里不做具体介绍。

        对build_medicalgraph.py文件进行解读。这个文件是将网上搜集的医疗数据json文件导入到本地图数据库,也就是依托neo4j图数据库构建知识图谱的具体实现。

        架构设计。在介绍后面的代码之前,必须要先看一下这个项目知识图谱的架构,这个架构的设计直接影响用户的使用体验,其设计方法值得学习参考。这个架构按我个人理解是项目团队根据医疗问答场景最常用的问题结构和回答的知识结构,分析总结得出来的,如xx病需要xx药物来治疗、xx病不能吃xx食物、xx病的一般治疗周期等等,这些都是最可能用于回答的句式。于是根据分析,该项目设计了一个以疾病为中心实体(其实逻辑上跟其他的实体是平行关系),再加上药品、食物、检查、科室、药品大类、疾病症状六类实体以及一系列这些实体两两之间关系的架构。其中疾病实体,还具有一些列属性信息。下面结合代码看看是如何实现的。

        代码结构比较简单易懂,主要通过定义一个 MedicalGraph类来实现。开头定义了本地数据源的地址(准备好的json文件)和将要构建的本地数据库接口信息。后面定义了一系列的方法来按照设计的架构构建图谱。

class MedicalGraph:

def __init__(self):

cur_dir = '/'.join(os.path.abspath(__file__).split('/')[:-1])

self.data_path = os.path.join(cur_dir, 'data/medical.json')

self.g = Graph(

host="127.0.0.1", # neo4j 搭载服务器的ip地址,ifconfig可获取到

http_port=7474, # neo4j 服务器监听的端口号

user="lhy", # 数据库user name,如果没有更改过,应该是neo4j

password="lhy123")

        信息提取。通过这个类下面的read_nodes方法,对json文件的信息进行提取。这里其实也算是知识图谱构建过程中信息抽取的具体实现,只不过这个项目的json文件信息已经是做了初步的信息过滤和整理的结果了。因此这个方法可以直接读取json文件,以json当中的键作为信息分类标识,键值经过加工作为具体内容。具体来说,根据架构设计定义了7类实体,即疾病、药品、食物、检查、科室、药品大类、疾病症状。又定义了一系列实体间的关系。

# 构建节点实体关系

rels_department = [] # 科室-科室关系

rels_noteat = [] # 疾病-忌吃食物关系

rels_doeat = [] # 疾病-宜吃食物关系

rels_recommandeat = [] # 疾病-推荐吃食物关系

rels_commonddrug = [] # 疾病-通用药品关系

rels_recommanddrug = [] # 疾病-热门药品关系

rels_check = [] # 疾病-检查关系

rels_drug_producer = [] # 厂商-药物关系

rels_symptom = [] #疾病症状关系

rels_acompany = [] # 疾病并发关系

rels_category = [] # 疾病与科室之间的关系

用字典存放每条json内容的疾病信息,最后保存到disease_info列表中,用来作为疾病的属性存储

disease_dict = {}

data_json = json.loads(data)

disease = data_json['name']

disease_dict['name'] = disease # 疾病名称

diseases.append(disease)

disease_dict['desc'] = '' # 疾病描述

disease_dict['prevent'] = '' # 预防方法

disease_dict['cause'] = '' # 成因

disease_dict['easy_get'] = '' # 易感人群

disease_dict['cure_department'] = '' # 治疗科室

disease_dict['cure_way'] = '' # 治疗方法

disease_dict['cure_lasttime'] = '' # 治疗时间

disease_dict['symptom'] = '' # 疾病症状

disease_dict['cured_prob'] = '' # 治愈率

然后就是对json文件的逐条遍历,有些是描述关系的添加到对应的关系列表中

if 'symptom' in data_json:

symptoms += data_json['symptom']

for symptom in data_json['symptom']:

rels_symptom.append([disease, symptom]) # 添加到疾病与症状的关系

if 'acompany' in data_json:

for acompany in data_json['acompany']:

rels_acompany.append([disease, acompany]) # 添加到疾病与疾病的关系

有些是描述疾病属性的,添加到疾病属性字典

if 'desc' in data_json:

disease_dict['desc'] = data_json['desc'] # 添加疾病描述到属性

if 'prevent' in data_json:

disease_dict['prevent'] = data_json['prevent'] # 添加预防信息到属性

        建立实体节点。实体节点的构建主要通过 create_node、create_diseases_nodes、create_graphnodes三个方法实现。create_node是节点构建的基本方法,主要通过py2neo中的Graph类实现。create_diseases_nodes则是对疾病实体进行构建,主要是添加每种疾病实体的属性,数据来源就是信息提取部分提到的disease_info。create_graphnodes中创建了其他六类实体的节点,数据来源为信息提取部分的六类实体的集合。

        建立实体关系边。实体关系边的构建是通过create_relationship和create_graphrels两个方法实现的。create_relationship是构建边的基本方法,也是通过py2neo实现。create_graphrels则是将read_nodes中的所有关系创建为实体关系边。

2.2问答系统

        问答系统逻辑结构也非常清晰。运行文件为项目根目录下的chatbot_graph.py。用户输入问题后先对问题进行分析归类,然后再将分类结果进行转化为数据库查询语句,最后从建好的知识图谱数据库中查询回答语料,整合输出。

问题分析

        这一步主要通过调用question_classifier.py当中的类方法来解决。其实这一步的目的,就是要搞清楚用户在问什么,也就是意图识别。这里关键的设计有两点:一是从用户输入的信息出发,找出其中所有的实体关键词,并对实体关键词进行分类。二是设计用户可能问的问题关键词列表,这个列表其实大体上是根据之前图谱当中的几类信息进行设计的,比如疾病的属性信息以及七类实体间的关系,然后将设想的用户问题与之做对应。(因为超出这个范围,图谱也不知道,设计也没意义0.0)。最后将这两者进行整合,判断出用户问了什么,最后汇总成一个列表。

        找出用户问句中的实体关键词并分类的实现方法。首先在check_medical方法中对问句进行过滤,只留下我们要的实体名词,这是通过构造一个“过滤树”来实现的。“过滤树”由预先制作好的7个知识图谱中的实体名称文件(dict文件夹下)生成,这里用到了pyahocorasick工具包。

'''构造actree,加速过滤'''

def build_actree(self, wordlist):

actree = ahocorasick.Automaton()

for index, word in enumerate(wordlist):

actree.add_word(word, (index, word))

actree.make_automaton()

return actree

'''问句过滤'''

def check_medical(self, question):

region_wds = []

for i in self.region_tree.iter(question):

wd = i[1][1]

region_wds.append(wd)

stop_wds = []

for wd1 in region_wds:

for wd2 in region_wds:

if wd1 in wd2 and wd1 != wd2:

stop_wds.append(wd1)

final_wds = [i for i in region_wds if i not in stop_wds]

final_dict = {i:self.wdtype_dict.get(i) for i in final_wds}

return final_dict

然后将找出的实体关键词进行分类

def build_wdtype_dict(self):

wd_dict = dict()

for wd in self.region_words:

wd_dict[wd] = []

if wd in self.disease_wds:

wd_dict[wd].append('disease')

if wd in self.department_wds:

wd_dict[wd].append('department')

if wd in self.check_wds:

wd_dict[wd].append('check')

if wd in self.drug_wds:

wd_dict[wd].append('drug')

if wd in self.food_wds:

wd_dict[wd].append('food')

if wd in self.symptom_wds:

wd_dict[wd].append('symptom')

if wd in self.producer_wds:

wd_dict[wd].append('producer')

return wd_dict

           构建提问关键词列表:

# 问句疑问词

self.symptom_qwds = ['症状', '表征', '现象', '症候', '表现']

self.cause_qwds = ['原因','成因', '为什么', '怎么会', '怎样才', '咋样才', '怎样会', '如何会', '为啥', '为何', '如何才会', '怎么才会', '会导致', '会造成']

self.acompany_qwds = ['并发症', '并发', '一起发生', '一并发生', '一起出现', '一并出现', '一同发生', '一同出现', '伴随发生', '伴随', '共现']

        最后是生成问题分类列表,为何是列表,因为很有可能一个问句输入对应多个问题,所以全都要查询反馈给用户。分类代码示例:

# 症状

if self.check_words(self.symptom_qwds, question) and ('disease' in types):

question_type = 'disease_symptom'

question_types.append(question_type)

if self.check_words(self.symptom_qwds, question) and ('symptom' in types):

question_type = 'symptom_disease'

question_types.append(question_type)

对上面这段代码做个简单解释,当中的逻辑也很有意思。怎么判定用户是在根据疾病名称问该病的症状呢,就是同时满足句子中包含疾病实体以及关于症状的提问词列表中的词出现在句子中,那么就判定这句话是根据疾病问症状,也就是'disease_symptom',一经判定则将这个问题类型添加到结果列表中。而判定用户是根据症状问该症状是什么疾病,则需要满足句子中有症状实体关键词并且关于症状的提问词列表中的词出现在句子中。

语句转化

        这里就是将上一步得到的问句中包含的所有问题类型转化成数据库查询语句。针对不同的问题类型设置不同的查询语句。具体是通过以下代码进行转化:

'''针对不同的问题,分开进行处理'''

def sql_transfer(self, question_type, entities):

if not entities:

return []

# 查询语句

sql = []

# 查询疾病的原因

if question_type == 'disease_cause':

sql = ["MATCH (m:Disease) where m.name = '{0}' return m.name, m.cause".format(i) for i in entities]

# 查询疾病的防御措施

elif question_type == 'disease_prevent':

sql = ["MATCH (m:Disease) where m.name = '{0}' return m.name, m.prevent".format(i) for i in entities]

查询输出

        这里用将上一步查询得到的查询语句,在知识图谱中查找对应的知识语料,并针对不同问题,加上一些辅助性的语句,使回答看上去更完整自然,最后整合所有问题类型的回答,形成最后回答的输出语句。

def answer_prettify(self, question_type, answers):

final_answer = []

if not answers:

return ''

if question_type == 'disease_symptom':

desc = [i['n.name'] for i in answers]

subject = answers[0]['m.name']

final_answer = '{0}的症状包括:{1}'.format(subject, ';'.join(list(set(desc))[:self.num_limit]))

elif question_type == 'symptom_disease':

desc = [i['m.name'] for i in answers]

subject = answers[0]['n.name']

final_answer = '症状{0}可能染上的疾病有:{1}'.format(subject, ';'.join(list(set(desc))[:self.num_limit]))

        至此,整个医疗知识图谱问答系统就设计完成了。

三、小结

       这个项目虽然工程体量不算大,但比较适合拿来作为知识图谱相关项目落地前作为入门的调研算法,可以让你对知识图谱的整体结构以及如何用python从零搭建有一个清晰认识,包括如何利用先验知识去设计图谱的schema ,问答系统的实现等等,对我们部署其他方向的知识图谱都有一定借鉴意义。

四、参考

    https://github.com/liuhuanyong/QASystemOnMedicalKG/tree/master

精彩文章

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: