知识图谱

挖个坑先。最近对知识图谱有个入门级的了解,希望以后还会有项目继续探究。年初实践了下怎么搭建一个最基本的知识图谱,顺便尝试了下使用Neo4j 搭建图数据库,在此稍作记录。入门选手可以看这篇科普帖:知识图谱的技术与应用

介绍

知识图谱到底是个什么东西?说到底就是拥有一定知识,可以进行相关知识问答的系统,这个系统可以不停地输入知识,也可以输出知识。其实,每个人都是一个行走的知识图谱。我们从小就被问“几岁啦?”,“叫什么名字呀?”,当我们进行回答时就是知识输出的过程。

本质上知识图谱的建立有如下几个过程:知识存储、知识融合、知识验证、知识计算。

  • 知识存储: 信息获取及存储的过程,比如3岁的时候学会背唐诗,8岁的时候学会加减乘除,15岁开始学物理,可以源源不断地获取不同的知识,语文、数学、英文等等。
  • 知识融合: 很多知识都是相互关联的,比如方程求解时会用到四则运算,求微分积分也会用到四则运算,很多知识都需要不断整合。
  • 知识验证: 对知识的正确性、实时性、整个体系的逻辑一致性的检验。比如2018年对于“小朋友你今年几岁啦?”的回答是3岁,那么到2019年这个回答的知识要更新为4岁了。
  • 知识计算: 一个知识体系是很庞大的,有很多很多的关系。比如“小朋友今年几岁了?”,需要对语义进行计算,明白问的问题是年龄相关,而且是当前年龄,需要计算当前时间与出生时间的差值。计算问题要的是什么样的答案,而不是问年龄要数字时返回一个“天空的颜色”的答案。

先分享下两个有趣的可视化图谱:

  1. 漫威英雄的知识图谱可视化 [demo link]

  2. 星际战争知识图谱可视化 [demo link]

搭建图数据库

虽然这两demo很大程度是前端写的好,对于我这种前端白痴,市面上已经有了相似的很好上手的开源工具(图数据库Neo4j),多用于金融的反欺诈,社交关系图谱等等。接下来我也尝试搭建一个基础的知识图谱。

需要思考几个基本问题:

  1. 哪里来的数据?
  2. 选择哪种数据库?数据存储格式
  3. 如何将数据导入数据库?
  4. 如何做到知识融合?进行数据库的增删改查?
  5. 如何做知识计算?

数据来源

介绍一个基于中文的知识图谱开源网站:开放的中文知识图谱 openkg.cn

这个网站里有图谱数据,图谱工具,资源涵盖:常识、金融、农业、社交、物联网、气象、生活、出行、科教、医疗等等。总之是比较全面的中文知识图谱开源库了。比如我用过的是四大名著里的西游记人物关系数据:中国四大名著人物关系知识图谱和OWL本体 ,在数据与资源那边可自行下载。然后解压即可,就有csv格式的人物关系表格。

顺便介绍个openkg里的一个比较成型的知识图谱工具:思知(OwnThink) ,体验入口:思知机器人

数据库选择

虽然我一开始已经选定了图数据库 ,这里还是将各种 数据结构 和 数据库 做个简单的对比:

数据类型可能是:

  • 多元 结构化表格: 就像excel表格一样整整齐齐
  • JSON 键值 : key-value 组
  • RDF 三元组: 三元组就是两个实体,中间有一个关系。比如“中国的首都是北京”,两个实体分别是“中国”和“北京”,它们之间的关系是“的首都”,这两个实体加一个关系就是三元组。

不同的数据库:

  • MySQL: 适合结构化数据,像excel表格
  • MongoDB: 适合json格式数据
  • Neo4j: 图数据库,可视化,适合三元组,并且实体和关系可以包含属性。

因此,选择什么样的数据库,是基于你的数据打算以什么样的格式存储。选择什么样的数据库以及怎么设计 schema。选关系数据库还是NoSQL 数据库?要不要用内存数据库?要不要用图数据库?这些都需要根据数据场景慎重选择。西游记的人物关系数据是三元组合格,这里我们选择尝试用Neo4j 图数据库。

图数据库:Neo4j

Neo4j 是一个图数据库,主要包括节点和关系。节点和关系都可以包含属性。介绍文档 [doc], 社区[community]。

安装

社区版下载链接:neo4j-community-3.5.2

解压后在bin目录下运行命令:neo4j console

1
2
3
4
5
6
7
8
D:\>cd D:\neo4j-community-3.5.2\bin

D:\neo4j-community-3.5.2\bin>neo4j console
2019-04-23 12:23:39.692+0000 INFO ======== Neo4j 3.5.2 ========
2019-04-23 12:23:39.708+0000 INFO Starting...
2019-04-23 12:23:42.950+0000 INFO Bolt enabled on 127.0.0.1:7687.
2019-04-23 12:23:44.544+0000 INFO Started.
2019-04-23 12:23:45.696+0000 INFO Remote interface available at http://localhost:7474/

浏览器Chrome访问:localhost:7474,第一次访问会提示修改密码。

第一次玩可以参考这个入门小教程:Neo4j 简单入门
更详细的安装以及其他信息:neo4j 安装

Cypher 语法总结

Cypher入门:tutorial

cypher就像MySQL的sql语言,demo示例:neo4j_movie_graph.cypher 。cypher可对数据库做增删改查。更多的操作参考:Cypher Basic ,可以自己跑几行命令找找感觉。

Py2neo [doc]

毕竟写sql或者写cypher不是我擅长的事,所幸可以通过Python API py2neo 来访问neo4j。

py2neo使用教程-1
py2neo使用教程-2
py2neo使用教程-3

贴一些使用过的代码,要配合合适的数据集一起使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
import pandas as pd
import numpy as np
import itertools
from py2neo import Graph,Node,Relationship,NodeMatcher

# 终端运行 neo4j console之后就可以从py2neo连接neo4j数据库
graph = Graph("http://localhost:7474",username="neo4j",password="neo4j")
matcher = NodeMatcher(graph)

# graph.delete_all() # 清空本地数据库的命令,慎用

# ============================================================
# Demo1: Product
# 目的:把能展示的应用都展示出来

prod = pd.read_excel('type.xlsx')
list(prod)

# 设置基础信息
for i in np.unique(prod['分组类型']):
node_a = Node('Type', name=i)

df = prod[prod['分组类型'] == i]
_p = np.unique(df['产品类型'])
for j in _p:
node_b = Node('Product',name=j)
rela = Relationship(node_a,'includes',node_b)

graph.create(rela)


# 因为点Node需要定义性质,所以建议 key-value 和 rela-node分开建表
# add property/relationship时需要第一列的产品已存在,否则需要更多信息
# ------------------------------------------------------------
# add property
prop = pd.read_excel('data/add_property.xlsx')

for _prod in np.unique(prop['product']):

df = prop[prop['product'] == _prod].reset_index(drop=True)

# 如果库内没有该产品,进行Node建立
if matcher.match(name=_prod).first() is None:
node_target = Node('Product', name = _prod) # 'Product'是固定的
else:
node_target = matcher.match(name=_prod).first() # 'Product'是固定的

for idx in range(len(df)):
node_target[df['key'][idx]] = df['value'][idx]
graph.push(node_target)


# add relationship
rel = pd.read_excel('add_relationship.xlsx')
# 检查是否存在节点 !!!希望后期的制表规则完善后可以省去检查这一步
# 如果不存在,就创建
# head 与 tail 如何区分

#nodes = list(itertools.chain.from_iterable([np.unique(rel['product']), np.unique(rel['node_name'])]))

for ind in range(len(rel)):
# 如果不想重复遍历,就建chunk df
if matcher.match(name=rel['node_head'][ind]).first() is None:
node_head = Node(rel['head_type'][ind], name = rel['node_head'][ind])

else:
node_head = matcher.match(name=rel['node_head'][ind]).first()
node_tail = matcher.match(name=rel['node_tail'][ind]).first()
r = rel['rela'][ind]
# 检查关系是否存在,若不存在,建立关系
cmd = "MATCH a=()-[:%s]->( {name: '%s'}) RETURN a" %(r,rel['node_tail'][ind])
if len(graph.run(cmd).data()) == 0:
relatp = Relationship(node_head,r,node_tail)
graph.create(relatp)


# ==============================================================
# Demo2: Room

room = pd.read_excel('room.xlsx')

for i in np.unique(room['owner']):
node_a = Node('Owner',name=i)
df = room[room['owner'] == i]

_r = np.unique(df['room_name'])
for j in _r:
node_b = Node('Room',name=j)
rela = Relationship(node_a,'lives',node_b)
graph.create(rela)


_df = df[df['room_name']==j]
_p = np.unique(_df['product_id'])
for k in _p:
node_c = Node('Prod',name=k)
relatp = Relationship(node_b,'has',node_c)
graph.create(relatp)

for i in np.unique(room['product_id']):
# 这些点已经存在,只需要匹配
node_a = matcher.match('Prod',name=i).first()
df = room[room['product_id'] == i]

_c = np.unique(df['control'])
for j in _c:
node_b = Node('Ctrl',name=j)
# 把node的答案作为属性填充上去,然后,方便用作cypher查询
rela = Relationship(node_a,'controls',node_b)

graph.create(rela)

# ==============================================================
# Demo3: 西游记
xyj = pd.read_csv('./西游记/triples.csv')

#所有人物创建完,直接搜索建关系
n = [np.unique(xyj['head']),np.unique(xyj['tail'])]
for i in n:
for p in i:
node = Node('Person', name=p)
graph.create(node)

for idx in range(len(xyj)):
print(idx)
node_a = matcher.match('Person',name=xyj['tail'][idx]).first()
node_b = matcher.match('Person',name=xyj['head'][idx]).first()
relatp = Relationship(node_a,xyj['label'][idx],node_b)
graph.create(relatp)


# 查询 '孙悟空的师傅是谁'

len(graph.nodes)
len(graph.nodes.match("Person"))

# 'PERSON'根据命名实体匹配
test = matcher.match('Person',name='唐僧').first()

for rel in matcher.match(start_node=test, rel_type="徒弟"):
print(rel.end_node()["name"])

拓展

大批量数据导入参考:

csv文件导入Neo4j(包括结点和关系的导入)
neo4j批量导入neo4j-import
如何将大规模数据导入Neo4j

删空属性

一般节点和关系可以通过py2neo删空,但是属性会存留:
Neo4j - How to delete unused property keys from browser?

别人开发的有趣的图谱成果 [refer]

would you buy me a coffee☕~
0%