知识图谱——用Python代码从文本中挖掘信息的强大数据科学技术
全文共6382字,预计学习时长20分钟
图源:Unsplash摄影:Clem Onojeghuo
概览
- 知识图谱是数据科学中最吸引人的概念之一
- 学习如何使用Wikipedia页面中的文本构建知识图谱
- 在Python中使用流行的spaCy库在Python中构建知识图谱
引言
不用过多介绍,大家都知道梅西。即使是那些不关注足球的人,也听说过这位最伟大球员在辉煌。下面是他的维基百科页面:
这个页面包含了很多信息!里面不仅有文本、大量的超链接,甚至还有音频片段。整个网页上有很多相关的和可能会有用的信息,将它们应用到实际生活中可能性是无穷的。
然而,还有一个小问题。这不是给机器提供数据的理想来源,至少不是以现在的这种形式。
是否能找到一种方法,使这些文本数据变为机器可读?能否将这些文本数据转录成既让机器可以使用,也能让我们可以轻松解释的东西?
答案是肯定的。我们可以借助知识图谱(KG),这是数据科学最吸引人的概念之一。我已经被知识图谱的巨大潜力和实际应用震惊了,我相信你也会和我一样。
在本文中,你将了解什么是知识图谱,以及它们的作用,然后我们将基于从维基百科中提取的数据,来构建知识图谱以用于深入研究代码。
目录
1. 什么是知识图谱?
2. 如何在图谱中表示知识?
○ 句子分割
○ 实体抽取
○ 关系抽取
3. 依靠文本数据构建知识图谱
什么是知识图谱?
先明确一个概念:在本文中经常出现的术语“图谱”,并不是指柱状图、饼状图或线状图,而是相互关联的实体,它们可以是人、地点、组织,甚至是一个事件。
不妨说,图谱是节点和边*的组合。
看看下面的数据:
*边(Edge)是节点间的连线,用于表示节点间关系。
这里的节点a和节点b是两个不同的实体,节点通过边连接。如图是我们可以构建的最小的知识图谱——它也被称为三元组(实体-关系-实体)。
知识图谱有多种形状和大小。例如,截至2019年10月,维基数据(Wikidata)的知识图谱有59,910,568个节点。
如何在图谱中表示知识?
在开始构建知识图谱前,我们需要了解信息或知识是如何嵌入到这些图谱中的。
举例来解释:如果节点a=普京,节点b=俄罗斯,那么边很可能是“俄罗斯总统”:
一个节点或实体也可以有不止一个关系。普京不仅是俄罗斯总统,他还曾为苏联安全机构克格勃工作。但是如何把这些关于普京的新信息,整合到上面的知识图谱中呢?
其实很简单。只需为新实体“克格勃”再添加一个节点:
新的关系不仅可以添加在第一个节点,而且可以出现在知识图谱中的任何节点,如下所示:
俄罗斯是亚太经济合作组织(APEC)的成员国
识别实体和他们的相互关系并不是一项困难的任务。但是,手动构建知识图谱是难以处理大量信息的。没有人会浏览成千上万的文档,然后提取出所有的实体和它们之间的关系。
因此,机器无疑是更好的选择,浏览成百上千的文件对它们来说简直小菜一碟。但是还有另外一个挑战——机器不懂自然语言。这就轮到自然语言处理 (Natural Language Processing,简称NLP) 技术出场了。
想要从文本中构建知识图谱,让机器能理解自然语言就至关重要。这可以通过使用自然语言处理(NLP)技术来实现,如句子分割、依存句法分析、词性标注和实体识别。下文将更详细地对它们进行探讨。
句子分割
构建知识图谱的第一步是将文本或文章分割成句子。然后,列出那些只有一个主语和宾语的句子。下面是示例文本:
“在最新的男子单打排名中,印度网球选手苏米特·纳加尔(Sumit Nagal)从135名上升了6个名次,达到职业生涯中的最好成绩129名。这位22岁的选手最近赢得了ATP挑战锦标赛。2019年美国网球公开赛中他首次亮相时,就在对阵费德勒的比赛中赢得了大满贯。纳加尔赢得了第一组比赛。(Indian tennis player Sumit Nagal moved up six places from 135to a career-best 129 in the latest men’s singles ranking. The 22-year-oldrecently won the ATP Challenger tournament. He madehis Grand Slam debut against Federer in the 2019 US Open. Nagal won the firstset.)”
我们把上面的段落分成几个句子:
1. 在最新的男子单打排名中,印度网球选手苏米特·纳加尔(SumitNagal)从135名上升了6个名次,达到职业生涯中的最好成绩129名。(Indian tennis player Sumit Nagal moved up six places from135 to a career-best 129 in the latest men’s singles ranking.)
2. 这位22岁的选手最近赢得了ATP挑战锦标赛。(The 22-year-old recently won the ATP Challengertournament))
3. 2019年美国网球公开赛中他首次亮相时,就在对阵费德勒比赛中赢得了大满贯。(Hemade his Grand Slam debut against Federer in the 2019 US Open.)
4. 纳加尔赢得了第一组比赛。(Nagalwon the first set.)
在这四个句子中,我们将选出第二个和第四个句子,因为每个句子都包含一个主语和一个宾语。第二句中,“22岁的选手(22-year-old)”是主语,宾语是“ATP挑战赛(ATP Challenger tournament)”。第四句中,主语是“纳加尔(Nagal)”,“第一组比赛(first set)”是宾语:
让机器理解文本是最大的挑战,特别是在主语和宾语不止一个的情况下。以上面两个句子为例,提取句子中的宾语有点棘手。你能想到解决这个问题的方法吗?
实体抽取
从句子中提取一个词实体并不是艰巨的任务。我们可以借助词性(POS)标签轻松做到这一点。名词和专有名词可以是我们的实体。
然而,当一个实体包括多个单词时,仅仅借助词性标记是不够的。我们需要解析句子间的复杂关系。先来获取其中一个句子的依存标记。使用流行的spaCy库来完成这个任务:
import spacy
nlp = spacy.load('en_core_web_sm')
doc = nlp('The 22-year-old recently won ATP Challenger tournament.')
for tok in doc:
print(tok.text, '...', tok.dep_)
输出:
The… det
22-year … amod
– … punct
old … nsubj
recently … advmod
won … ROOT
ATP … compound
Challenger … compound
tournament … dobj
. … punct
根据依存关系解析器,这个句子中的主语(nsubj)是“old”。这不是我们想要的实体。我们想要的是“22岁的选手(22-year-old)”。
“22岁的(22-year)”的依存关系标签是amod,这意味着它是(old)”的修饰语。因此,应该定义一个规则来提取这样的实体。
可以是这样的规则:提取主语/宾语及其修饰语,并提取它们之间的标点。
但是再看看句子中的宾语(dobj)。它只是“锦标赛(tournament)”而不是“ATP挑战锦标赛(ATPChallenger tournament)”。在这句里,没有修饰词,只有复合词。
复合词是指不同词组合起来所形成的新术语,它们有着和源词不同的意思。因此,我们可以更新上面的规则----提取主语/宾语及其修饰语、复合词,并提取它们之间的标点。
简而言之,我们使用依存句法分析来提取实体。
提取关系
实体提取仅仅是一半的工作。想要建立知识图谱,需要边来连接各个节点(实体)。这些边表示节点之间的关系。
回到上一节中的例子,选出几句话来构建一个知识图谱:
你能猜出这两个句子中主语和宾语的关系吗?
两个句子有相同的关系——“赢得(won)”。看看该如何提取这些关系。这里将再次使用依存项解析:
doc = nlp('Nagal won the first set.')
for tok in doc:
print(tok.text, '...', tok.dep_)
输出:
Nagal… nsubj
won … ROOT
the … det
first … amod
set … dobj
. … punct
为了提取关系,必须找到句子的根(root),它也是句子里的动词。因此,在这个句子里找到的关系就是“赢得(won)”。
最后,这两个句子的知识图谱是这样的:
从文本数据构建知识图谱
是时候进行代码操作了!打开Jupyter Notebooks(或者任何你喜欢的集成开发环境-IDE)。
使用一组从维基百科中找到的电影中的文本,从头开始构建一个知识图谱。我已经从500多篇维基百科文章中摘录了大约4300句话。每个句子都包含两个实体——一个主语和一个宾语。你可以点击这里下载这些句子。
推荐使用Google Colab,能加快计算的运行速度。
导入库
import re
import pandas as pd
import bs4
import requests
import spacy
from spacy import displacy
nlp = spacy.load('en_core_web_sm')
from spacy.matcher import Matcher
from spacy.tokens import Span
import networkx as nx
import matplotlib.pyplot as plt
from tqdm import tqdm
pd.set_option('display.max_colwidth', 200)
%matplotlib inline
读取数据
阅读包含维基百科句子的CSV文件:
# import wikipedia sentences
candidate_sentences = pd.read_csv('wiki_sentences_v2.csv')
candidate_sentences.shape
输出:(4318, 1)
来看几个例句:
candidate_sentences['sentence'].sample(5)
输出:
检查其中一个句子的主语和宾语。理想情况下,句子中应该有一个主语和一个宾语:
doc = nlp('the drawdown process is governed by astm standard d823')
for tok in doc:
print(tok.text, '...', tok.dep_)
输出:
很好!只有一个主语“过程”(process)和一个宾语“标准”(standard)。你可以用类似的方式检查其他句子。
实体对抽取
想要构架出一个知识图谱,最重要的是节点和它们之间的边。
这些节点是出现在维基百科语句中的实体。边是连接各个实体之间的关系。我们将以无监督的方式提取这些要素,也就是说,我们将依靠句子的语法。
其主要思想就是在浏览一个句子时,把遇到的主语和宾语提取出来。然而,还有其他挑战存在——实体可能不止一个单词,如“红酒(red wine)”,依存关系解析器只会将单个单词标记为主语或宾语。
因此,下面创建了一个函数来从一个句子中提取主语和宾语(也就是实体),同时解决了上面提到的挑战。方便起见,将代码分成了多个板块:
defget_entities(sent):
## chunk 1
ent1 =''
ent2 =''
prv_tok_dep ='' # dependency tag of previous token in the sentence
prv_tok_text ='' # previous token in the sentence
prefix =''
modifier =''
#############################################################
for tok in nlp(sent):
## chunk 2
# if token is a punctuation mark then move on to the next token
if tok.dep_ !='punct':
# check: token is a compound word or not
if tok.dep_ =='compound':
prefix = tok.text
# if the previous word was also a 'compound' then add the current word to it
if prv_tok_dep =='compound':
prefix = prv_tok_text '' tok.text
# check: token is a modifier or not
if tok.dep_.endswith('mod') ==True:
modifier = tok.text
# if the previous word was also a 'compound' then add the current word to it
if prv_tok_dep =='compound':
modifier = prv_tok_text '' tok.text
## chunk 3
if tok.dep_.find('subj') ==True:
ent1 = modifier '' prefix '' tok.text
prefix =''
modifier =''
prv_tok_dep =''
prv_tok_text =''
## chunk 4
if tok.dep_.find('obj') ==True:
ent2 = modifier '' prefix '' tok.text
## chunk 5
# update variables
prv_tok_dep = tok.dep_
prv_tok_text = tok.text
#############################################################
return [ent1.strip(), ent2.strip()]
我来解释一下上面函数中的代码板块:
板块1
我已经在这个板块中定义了一些空变量。prv_tok_dep 和 prv_tok_text将分别保存句子中前一个单词和它本身的依存标记。prefix和modifier将保存与主语或宾语有关联的文本。
板块2
接下来,循环浏览句子中的所有标记。首先要检查标记是不是标点符号。如果是,那么我们将忽略它看下一标记(token)。如果标记是复合词(依存标记=“compound”)中的一部分,将把它保存在“前缀”变量中。一个复合词是多个词的组合,它们联系在一起形成一个有新意义的词,例如“足球场”(“FootballStadium”),“动物爱人”(“animallover”)。
当我们在句子中遇到主语或宾语时,会给它加上这个前缀。修饰语同样,比如“漂亮的衬衫”、“大房子”等等。
板块3
在这里,如果标记是主语的话,它将被捕获为ent1变量中的第一个实体。诸如prefix, modifier, prv_tok_dep,和 prv_tok_text 这些变量将被重置。
板块4
在这里,如果标记是宾语,那么它将被捕获为ent2变量中的第二个实体。诸如prefix, modifier, prv_tok_dep,和 prv_tok_text 这些变量将再次被重置。
板块5
一旦在句子中捕获了主语和宾语,我们将更新前面的标记及其依存标记。
在一个句子上测试一下这个函数:
get_entities('the film had 200 patents')
输出: ['film’,'200 patents’]
很好,看起来一切都和预想的一样。在上面的句子中,“电影(film)”是主语,“200项专利(200 patents)”是宾语。
现在可以使用这个函数,提取数据中所有句子的实体对:
entity_pairs = []
for i in tqdm(candidate_sentences['sentence']):
entity_pairs.append(get_entities(i))
这个实体对名单包含维基百科句子中的所有主语-宾语对。来看看其中的几个:
entity_pairs[10:20]
输出:
在这些实体对中有一些代词,如“我们(we)”、“它(it)”、“她(she)”等等。我们希望用专有名词或名词来代替它们。也许可以进一步改进get_entities()函数来过滤代词。现在,让我们暂时保持现状,进入关系抽取部分。
关系/谓语提取
这将是本文非常有趣的一部分。假设是——谓语实际上是句子中的主要动词。
例如,在“1929年共有六十部好莱坞音乐剧被发行”这个句子中,动词是“被发行(released in)”,它也是本句得到的三重结构的谓语。
下面的函数能够从句子中捕获到这样的谓语。这里使用了spaCy库基于规则的匹配功能:
defget_relation(sent):
doc = nlp(sent)
# Matcher class object
matcher = Matcher(nlp.vocab)
#define the pattern
pattern = [{'DEP':'ROOT'},
{'DEP':'prep','OP':'?'},
{'DEP':'agent','OP':'?'},
{'POS':'ADJ','OP':'?'}]
matcher.add('matching_1', None, pattern)
matches = matcher(doc)
k =len(matches) -1
span = doc[matches[k][1]:matches[k][2]]
return(span.text)
函数中定义的模式试图找到句子中的根(root)词或主要动词。一旦确定了根,模式就会检查它后面是不是介词(“prep”)或功能词。如果是的话,就将它添加到根词。
这个函数是这样的:
get_entities('John completed the task')
输出: “completed
用同样的方法找出所有维基百科句子中的关系:
relations = [get_relation(i) for i in tqdm(candidate_sentences['sentence'])]
来看一看刚刚提取的最常见的关系或谓词:
pd.Series(relations).value_counts()[:50]
输出:
结果表明,“A现在是(is)B”和“A曾是(was)B”是最常见的关系。然而,有很多的关系与整个主题,即“围绕电影的生态系统”联系更紧密。其中一些例子是“由”、“发布”、“制作”、“编写”等等。
构建知识图谱
最后,从提取的实体(主语-宾语对)和谓语(实体之间的关系)构建知识图。
创建一个实体和谓语的数据框架:
# extract subject
source = [i[0] for i in entity_pairs]
# extract object
target = [i[1] for i in entity_pairs]
kg_df = pd.DataFrame({'source':source, 'target':target, 'edge':relations})
接下来,使用networkx库从这个数据框架创建一张网。节点表示实体,节点之间的边或连接表示节点之间的关系。
它将是一个有向图谱。换句话说,任何连接的节点对之间的关系都不是双向的,它只是从一个节点到另一个节点。例如,“约翰吃意大利面”:
# create a directed-graph from a dataframe
G=nx.from_pandas_edgelist(kg_df, 'source', 'target',
edge_attr=True, create_using=nx.MultiDiGraph())
描绘一下这个网:
plt.figure(figsize=(12,12))
pos = nx.spring_layout(G)
nx.draw(G, with_labels=True, node_color='skyblue', edge_cmap=plt.cm.Blues, pos= pos)
plt.show()
输出:
好吧,这并不是我们期待的结果,但看起来还是相当壮观的!
但已经构建了一个图谱,显示了所有的关系。很难想象将这么多关系或谓语可视化成一个图谱。
因此,建议只把一些重要的关系做成可视化图谱,我一次处理一个关系。从“由”组成的关系开始:
G=nx.from_pandas_edgelist(kg_df[kg_df['edge']=='composed by'], 'source', 'target',
edge_attr=True, create_using=nx.MultiDiGraph())
plt.figure(figsize=(12,12))
pos = nx.spring_layout(G, k=0.5) # k regulates the distance between nodes
nx.draw(G, with_labels=True, node_color='skyblue', node_size=1500, edge_cmap=plt.cm.Blues, pos= pos)
plt.show()
输出:
这是一个更清晰的图谱。这里箭头指向作曲家。例如,A.R.拉赫曼是一位著名的音乐作曲家,在上面的图谱中,他有诸如“电影原声带”、“电影配乐”和“音乐”这样的实体。
再看更多的关系。
因为编写在任何一部电影中都占重要角色,所以我想把“编写”关系的图谱可视化:
G=nx.from_pandas_edgelist(kg_df[kg_df['edge']=='written by'], 'source', 'target',
edge_attr=True, create_using=nx.MultiDiGraph())
plt.figure(figsize=(12,12))
pos = nx.spring_layout(G, k=0.5)
nx.draw(G, with_labels=True, node_color='skyblue', node_size=1500, edge_cmap=plt.cm.Blues, pos= pos)
plt.show()
输出:
太棒了!这张知识图谱给了我们一些非同寻常的信息。像Javed Akhtar、Krishna Chaitanya和Jaideep Sahni都是著名的作词家,这张图谱很好地捕捉到了这种关系。
看看另一个重要谓语的知识图谱,即“释放(发布于)”:
G=nx.from_pandas_edgelist(kg_df[kg_df['edge']=='released in'], 'source', 'target',
edge_attr=True, create_using=nx.MultiDiGraph())
plt.figure(figsize=(12,12))
pos = nx.spring_layout(G, k=0.5)
nx.draw(G, with_labels=True, node_color='skyblue', node_size=1500, edge_cmap=plt.cm.Blues, pos= pos)
plt.show()
输出:
我们可以在这个图谱中看到不少有趣的信息。例如这种关系:“20世纪80年代上映的几部动作恐怖电影”和“4844块屏幕上放映的格斗电影”。这些都是事实,这张图谱告诉我们,确实可以从文本中挖掘出这些事实。真是太神奇了!
结语
在本文中,我们学习了如何以三元组的形式,从给定文本中提取信息,并借此构建知识图谱。
然而,我们只使用恰好有两个实体的句子。即使这样,也能够建立信息量很大的知识图谱,所以想象一下它的潜力!
我鼓励大家探索这个领域的信息抽取,学习更复杂关系的提取。如果你有任何疑问或想要分享你的想法,请随时留言。