华来知识

前面我们介绍了各种知识抽取技术,当你抽取好三元组之后就该将其存储起来了,知识图谱的数据存储一般使用图数据库(当然你要愿意,也可以不用图数据库),比较流行的有jena、titan、neo4j、NEST 等,本文主要讲neo4j 的使用,在之后的项目中,我们也将其作为主要存储数据库。

Neo4j使用专有的查询语句 Cypher查询数据,查询效率高、可以进行多跳查询,它是目前使用率最高的图数据库,它拥有活跃的社区,而且系统本身的查询效率高,但唯一的不足就是不支持准分布式。相反,OrientDB和JanusGraph(原Titan)支持分布式,但这些系统相对较新,社区不如Neo4j活跃,也就是你遇到问题时不好找到解决方案。


一、创建节点

使用merge,如果节点不存在,该方法创建一个节点;如果存在,则忽略。

merge(n:实体类型{属性:属性值, …})

例子:

merge(n:电影{title:’我是路人甲’})

使用 create,不管节点存不存在,都创建

create (n:实体类型{属性:属性值, …})

例子:

create (n:电影{title:’我是路人甲’})

知识图谱数据库与MySQL数据库差别_属性值

二、给节点添加属性

match(n: 实体类型)where ID(n) = 0 –这里可以是其他判断条件 set n.属性1=属性值1,n.属性2=属性值2,return n

比如我们给刚才创建的电影节点添加评分、类型、时长、别名、上映时间等属性:

match(n: 电影)where n.title ="我是路人甲"set n.rate=7.4, n.language="汉语普通话", n.showtime=2015,n.lenth=134.0, n.othername= "I Am Somebody"return

如果某个属性有多个属性值,那么可以这样操作

match(n:电影)where n.title ="我是路人甲"set n.language=["汉语普通话",'浙江方言']return n

三、删除节点属性

match(n: 实体类型)where ID(n) = 0remove n.属性1return n

四、创建关系

match(n:实体类别),(b:实体类别)where n.属性=属性值 and b.属性=属性值create(n)-[r:关系名称{属性:属性值,.., }]->(b) return n,r,b

比如我们创建一个执导关系和主演关系,它们都是电影和人之间的关系,因此先要有这两个类型的实体节点。

match (n:人物),(b:电影)where n.name='尔冬升' andb.create (n)-[r:direct]->(b)return r
match (n:人物),(b:电影)where n.name='万国鹏' andb.create (n)-[r:act{role:'mainActor'}]->(b)return r

知识图谱数据库与MySQL数据库差别_知识图谱数据库与MySQL数据库差别_02

五、修改关系的属性

match(n)-[r]-(b)where ID(r) = 12513set r.属性=属性值

六、删除节点和节点之间的关系

match(n)-[r]-(b)where n.name=属性值1  and b.name=属性值2delete rreturn r

七、删除所有节点和关系

首先需要删除关系,不然无法删除节点

match(p)-[r]->(q)delete rreturn p,q

然后再删除所有节点

match(n)delete nreturn n

好了,neo4j 的基础使用方法已经介绍完了,当然还有其他增删改查语句,大家有需要的可以自行去相关网站找一些教程。可以看到上面的cypher语句都需要去neo4j界面上输入使用,但是我们有很多三元组,难道都要一个一个输入吗,当然不是,我们可以使用Python脚本来批量操作呀,另外可以使用 py2neo 这个包来操作。


现在我们来把之前介绍的那个豆瓣电影数据集进行三元组抽取,然后存储到neo4j数据库中。由于这个数据是半结构化的,所以我们不需要训练知识抽取模型,而只需写脚本将其解析成符合要求的三元组就够了,代码如下。

# -*- coding:utf-8 -*-import pandas as pd import jsonimport refrom py2neo import Graph# 实体person_list = set()movie_list = set()district_list = set()# 关系direct_relation = []act_relation = [] # main_actorscenarist_relation = []release_relation = []# 属性# 电影属性url_attribute = []rate_attribute = []language_attribute = [] #属性值为listshowtime_attribute = []lenght_attribute =[]othername_attribute = [] # listcategory_attribute = [] # list# 人物属性profession_attribute = [] #属性值为listdef extract_knowledge(data):    title = data['title']    movie_list.add(title)    # 电影属性提取    url_attribute.append([title,'url',data['url']])    rate_attribute.append([title,'rate',data['rate']])    showtime_attribute.append([title,'showtime',data['showtime']])    lenght_attribute.append([title,'length',data['length']])    category_attribute.append([title,'category',data['category']])    if data['othername']:        othername_attribute.append([title,'othername',data['othername']])    language_attribute.append([title,'language',data['language']])    # 人实体、属性提取,关系提取    for direct in data['director']:        direct_relation.append([direct, 'direct', title])        person_list.add(direct)        profession_attribute.append([direct,'profession','director'])    for scenarist in data['composer']:        scenarist_relation.append([scenarist, 'scenarist', title])        person_list.add(scenarist)        profession_attribute.append([scenarist,'profession','composer'])    for i, actor in enumerate(data['actor']):        if i < 2:            act_relation.append([actor, 'act-mainActor', title])        else:            act_relation.append([actor, 'act', title])        person_list.add(actor)        profession_attribute.append([actor,'profession','actor'])    #发行地区关系提取、地区实体提取    for district in data['district']:        district_list.add(district)        release_relation.append([title,'district',district])graph = Graph(        "http://localhost:7474/",        user="neo4j",        password="123456")def create_node(node_list,node_type):    for node in node_list:        cql = "MERGE(p:{node_type}{{name:'{node}'}})".format(node_type=node_type,node=node)        try:            graph.run(cql)        except:            print(node)def set_single_attribute(attr_triples,node_type):    for attr in attr_triples:        node = attr[0]        attribute = attr[1]        attribute_value = attr[2]        cql = "MATCH (n:{node_type}) WHERE n.name='{node}' SET n.{attribute}='{attribute_value}'".format(node_type=node_type,            node=node,attribute=attribute,attribute_value=attribute_value)        graph.run(cql)def set_multi_attribute(attr_triples,node_type):    for attr in attr_triples:        node = attr[0]        attribute = attr[1]        attribute_value = attr[2]        cql = "MATCH (n:{node_type}) WHERE n.name='{node}' SET n.{attribute}={attribute_value}".format(node_type=node_type,            node=node,attribute=attribute,attribute_value=attribute_value)        try:            graph.run(cql)        except:            print(attr)def add_attribute(node_list,node_type):    for node in node_list:        cql = "MATCH(n:{node_type}) WHERE n.name='{node}' SET n.".format(node_type=node_type,node=node)        graph.run(cql)def create_relation(rela_triples,node1_type,node2_type):    for rela in rela_triples:        node1 = rela[0]        node2 = rela[2]        relation = rela[1]        if '-' in relation:            relation_name ,relation_attr = relation.split('-')            cql = "MATCH (p:{node1_type}),(q:{node2_type}) WHERE p.name='{node1}' AND q.name='{node2}' CREATE (p)-[r:{relation_name}{{role:'{relation_attr}'}}]->(q)".format(                node1_type=node1_type,node2_type=node2_type,node1=node1,node2=node2,relation_name=relation_name,relation_attr=relation_attr)        else:            cql = "MATCH (p:{node1_type}),(q:{node2_type}) WHERE p.name='{node1}' AND q.name='{node2}' CREATE (p)-[r:{relation_name}]->(q)".format(                node1_type=node1_type,node2_type=node2_type,node1=node1,node2=node2,relation_name=relation)        try:            graph.run(cql)        except:            print(rela)def load_data(data_path):    data = None    with open(data_path,'r',encoding='utf8') as f:        for line in f.readlines():            data = line.strip().replace('null','[]')    data = re.sub('}, {', '}##{',data[1:-1])    data = data.split('##')    # data = json.loads(data_path)    return dataif __name__ == '__main__':    data_path = './data/dbmovies.json'    data_list = load_data(data_path)    print(len(data_list))    for i,data in enumerate(data_list):        data = re.sub(r"(.*?)", '', data)        data = re.sub("|", '', data)        data = data.replace("'",' ')        data = eval(data)        extract_knowledge(data)    #创建节点    create_node(district_list,'国家地区')    create_node(person_list,'人物')    create_node(movie_list,'电影')    # 节点的属性    set_single_attribute(rate_attribute,'电影')    set_single_attribute(url_attribute,'电影')    set_single_attribute(showtime_attribute,'电影')    set_single_attribute(lenght_attribute,'电影')    set_multi_attribute(language_attribute,'电影')    set_multi_attribute(othername_attribute,'电影')    set_multi_attribute(category_attribute,'电影')    df = pd.DataFrame(profession_attribute,columns=['name','xxx','profession'])    profession_attribute = []    persons = df['name'].unique()    for p in persons:        profession = df.loc[df['name']==p,'profession'].tolist()        profession_attribute.append([p,'profession',list(set(profession))])    set_multi_attribute(profession_attribute,'人物')    #关系创建    create_relation(act_relation,'人物','电影')    create_relation(scenarist_relation,'人物','电影')    create_relation(direct_relation,'人物','电影')    create_relation(release_relation,'电影','国家地区')    add_attribute(movie_list,'电影')

我们写入了27525个实体,54844条关系。

知识图谱数据库与MySQL数据库差别_role知识图谱_03

知识图谱数据库与MySQL数据库差别_查询语句_04


这个脚本是将三元组依次写入neo4j,速度难免比较慢。我们可以将三元组导出成CSV 格式,然后将CSV文件直接加载到数据库中。

首先将三元组安装关系、属性分类分别存储,然后将这些文件放入D:\neo4j-community-3.5.7\import 文件夹(就是这个import文件夹),然后使用下面的语句进行加载:

LOAD CSV FROM "file:///导演关系.csv"AS line MERGE(p:人物{name:line[0]})MERGE(q:电影{name:line[2]})MERGE(p)-[r: direct]->(q)

这样就将同样的关系全部都导入数据库中了,而且导关系前不需要事先建立节点,只需要指定节点类型是哪一列。其他关系、属性csv文件可以依葫芦画瓢。

所有知识都存储起来了,那么怎么应用呢?比如我想知道刘德华都演了哪些电影,那么我可以使用以下查询语句进行查询:

match(p:人物)-[r:act]->(q:电影)where p.name='刘德华'return q

结果:

知识图谱数据库与MySQL数据库差别_知识图谱数据库与MySQL数据库差别_05

如果在返回时加上count() 就能查询总共出演了多少部电影:

知识图谱数据库与MySQL数据库差别_知识图谱数据库与MySQL数据库差别_06

当然这里显示38部的原因只是我的数据库里只有38部记录,数据不全导致的。

我想知道评分最高的喜剧电影是哪一部?

查询语句:

match (n:电影)where '喜剧' in n.categoryreturn n.title,n.rateorder by n.rate desc

结果:

知识图谱数据库与MySQL数据库差别_role知识图谱_07

但是知识图谱在应用时不能让用户自己来写查询语句或者让一个程序员在后台时刻待命吧,这里就涉及到自然语言理解了,因为用户的查询需求肯定是以文字或者语音来给出的,所以我们需要理解用户的文字或语音的意图,然后将该意图自动转化成Cypher 查询语句,最后执行查询并将结果返回给用户。有时用户要查询的知识可能比较“难”,所以我们还得利用知识图谱给他做一定的知识推理才能回答他的问题。

所以,知识图谱的搭建还远未完成!在后续文章中我们将继续介绍自然语言理解和知识推理相关的知识。