使用Python的NLP:知识图谱

933 阅读12分钟

在这篇文章中,我将展示如何用Python和自然语言处理构建知识图谱。

image.png

A 网络图是一种数学结构,用于显示各点之间的关系,可以用无向/有向图结构进行可视化。它是一种映射链接节点的数据库形式。

A 知识库是一个来自不同来源的统一的信息库,如维基百科

A 知识图谱是一个使用图形结构的数据模型的知识库。用简单的话来说,它是一种特殊类型的网络图,显示现实世界的实体、事实、概念和事件之间的定性关系。谷歌在2012年首次使用 "知识图谱 "这个术语来介绍他们的模型

image.png 目前,大多数公司正在建立数据湖,这是一个中央数据库,他们把从不同来源获得的各种类型(即结构化和非结构化)的原始数据扔在里面。因此,人们需要工具来理解所有这些不同的信息。知识图谱正在变得流行,因为它们可以简化对大型数据集的探索和洞察力的发现。换句话说,知识图谱连接了数据和相关的元数据,所以它可以用来建立一个组织的信息资产的综合代表。例如,知识图谱可以取代你为了找到某个特定信息而必须翻阅的所有成堆的文件。

知识图谱被认为是自然语言处理领域的一部分,因为为了建立 "知识",你必须经历一个叫做 "语义丰富"的过程。由于没有人愿意手动操作,我们需要机器和NLP算法来为我们执行这项任务。

我将介绍一些有用的Python代码,这些代码可以很容易地应用于其他类似的情况(只需复制、粘贴、运行),并通过每一行代码的注释进行讲解,以便你可以复制这个例子(链接到下面的完整代码)。

github.com/mdipietro09…

我将解析维基百科并提取一个页面,作为本教程的数据集(链接如下)。

俄乌战争

特别是,我将通过:

  • 设置:用网络搜刮读取包和数据,用 维基百科-API.
  • 使用NLP SpaCy: 句子分割, POS标签, 依赖性分析, NER.
  • 实体及其关系的提取 文本.
  • 网络图的构建 网络X.
  • 使用时间线图的 DateParser.

设置

首先,我需要导入以下库:

## for data  
import pandas as pd #1.1.5  
import numpy as np #1.21.0  
  
## for plotting  
import matplotlib.pyplot as plt #3.3.2  
  
## for text  
import wikipediaapi #0.5.8  
import nltk #3.8.1  
import re  
  
## for nlp  
import spacy #3.5.0  
from spacy import displacy  
import textacy #0.12.0  
  
## for graph  
import networkx as nx #3.0 (also pygraphviz==1.10)  
  
## for timeline  
import dateparser #1.1.7

Wikipedia-api 是一个Python包装器,可以让你轻松地解析维基百科的页面。我将提取我想要的页面,排除底部的所有 "注释 "和 "书目":

image.png

来自维基百科

我们可以简单地写出该页面的名称:

topic = "俄乌战争"  
  
wiki = wikipediaapi.Wikipedia( 'en' )  
page = wiki.page(topic)  
txt = page.text[:page.text.find( "另见" )]  
txt[ 0 : 500 ] + " ..."

image.png

在这个用例中,我将尝试通过识别和提取文本中的主题-行动-对象(所以行动就是关系)来映射历史事件。

NLP

为了建立一个知识图谱,我们首先需要识别实体和它们的关系。因此,我们需要用NLP技术来处理文本数据集。

目前,这类任务最常用的库是SpaCy,这是一个利用Cython (C+Python)的高级NLP的开源软件。SpaCy 使用预先训练好的语言模型对文本进行标记,并将其转化为一个通常称为 "文档"的对象,基本上是一个包含模型预测的所有注释的类。

#python -m spacy 下载 en_core_web_sm  
  
nlp = spacy.load( "en_core_web_sm" )  
doc = nlp(txt)

NLP模型的第一个输出是 句子分割:决定一个句子在哪里开始和结束的问题。通常情况下,它是通过根据标点符号来分割段落来完成的。让我们看看SpaCy 把文本分成了多少个句子:

# 从文本到句子列表  
lst_docs = [sent for sent in doc.sents]  
print ( "tot sentences:" , len (lst_docs))

image.png

图片由作者提供

现在,对于每个句子,我们要提取实体和它们的关系。为了做到这一点,首先我们需要了解 语音部分(POS)标记语法标签**:** 用适当的语法标签来标记句子中的每个词的过程。以下是可能的标签的完整列表(到今天为止):

  • ADJ:形容词,例如:大、老、绿、不可理解、第一
  • ADP:介词(介词/后置词),例如:在,到,在
  • ADV:副词,例如:非常、明天、下降、哪里、那里
  • AUX: 助词,例如:是、已(做)、将(做)、应(做)
  • 句子:连词,如:和、或、但
  • CCONJ:协调连词,如和、或、但是
  • DET:定语从句,例如:a, an, the
  • INTJ: 感叹词,例如:psst, ouch, bravo, hello
  • 名词: 名词,例如:女孩、猫、树、空气、美女
  • : 数字,例如:1, 2017, one, seventy-seven, IV, MMXIV
  • 部分: 粒子,例如:'s, not
  • PRON:代词,例如:我、你、他、她、我自己、他们、某人
  • 代词(PROPN):专有名词,例如:Mary, John, London, NATO, HBO
  • 标点符号:标点符号,例如:...,(,),?
  • SCONJ:从属连词,例如:if, while, that
  • SYM符号:符号,如$, %, §, ©, +, -, ×, ÷, =, :), emojis
  • 动词:动词,如:run, runs, running, eat, ate, eating
  • X:其他,例如:sfpksdpsxmsa
  • 空间: 空间,例如

仅有POS标签是不够的,该模型还试图理解成对的词之间的关系。这项任务被称为 依赖性(DEP)解析.以下是可能的标签的完整列表(到今天为止):

- ACL: clausal modifier of noun
- ACOMP: 形容词补语
- ADVCL: 副词句修饰语
- ADVMOD: 副词修饰语
- AGENT: 代理人
- AMOD: 形容词修饰语
- APPOS: 附属修饰语
- ATTR: 属性
- AUX: 辅助词
- AUXPASS: auxiliary (passive)
- CASE: case marker
- CC:协调连词
- CCOMP: 分句补语
- COMPOUND: 复合修饰语
- CONJ: 连词
- CSUBJ: 分词主语
- ***CSUBJPASS:***分词主语(被动)
- ***DATIVE:***助词
- ***DEP:***未分类的从属关系
- DET: 限定词
- DOBJ: 直接宾语
- EXPL: 感叹词
- INTJ: 感叹词
- MARK: 标记
- META: 元修饰语
- NEG: 否定修饰语
- NOUNMOD: 名词的修饰语
- NPMOD: 名词短语作为副词修饰,
- NSUBJ: 名词性主语
- NSUBJPASS: 名义主语(被动)
- NUMMOD: 数字修饰词
- OPRD: 宾语谓语
- PARATAXIS: parataxis
- PCOMP: 介词的补语
- POBJ: 介词的宾语
- POSS: 占有修饰语
-PRECONJ: 前关系连词
- PREDET: 前定语
- PREP: 介词修饰语
- PRT: 粒子
- PUNCT: 标点符号
- QUANTMOD: 量词的修饰词
- RELCL: 相对句修饰语
- ROOT:
- XCOMP: 开放式句子补语

让我们做一个例子来理解POS标签和DEP解析:

# take a sentencei = 3lst_docs[i]

让我们检查一下NLP模型所预测的POS和DEP标签:

for token in lst_docs[i]:    print(token.text, "-->", "pos: "+token.pos_, "|", "dep: "+token.dep_, "")

图片由作者提供

SpaCy还提供了一个图形工具来可视化这些注释:

from spacy import displacydisplacy.render(lst_docs[i], style="dep", options={"distance":100})

image.png 最重要的标记是动词*(POS=VERB*),因为它是句子中意义的根*(DEP=ROOT*)。

image.png 辅助粒子,如副词和形容词*(POS=ADV/ADP*),通常作为修饰语与动词相连*(DEP=*mod*),因为它们可以修改动词的意义。例如,"前往"和 "来自"有不同的含义,尽管词根是相同的("旅行")。

image.png 在与动词相连的词中,必须有一些名词*(POS=PROPN/NOUN*),作为句子的主语和宾语*(DEP=nsubj/*obj*)。

image.png 名词附近往往有一个形容词*(POS=ADJ*),作为其意义的修饰语*(DEP=amod*)。例如,在 "好人"和 "坏人"中,形容词赋予名词*"人 "以相反的含义。*

image.png SpaCy执行的另一项很酷的任务是 命名实体识别(NER).命名实体是一个 "现实世界的对象"(即人、国家、产品、日期),模型可以识别文档中的各种类型。以下是可能的标签的完整列表(截止到今天):

- 人: 人,包括虚构的。
NORP: 民族或宗教或政治团体。
FAC: 建筑物、机场、公路、桥梁等。
- ORG: 公司、机关、机构等。
- GPE: 国家、城市、州。
- LOC: 非GPE地点、山脉、水体。
- 产品: 物品、车辆、食品等(不是服务。)
- 事件: 被命名的飓风、战役、战争、体育赛事等。
- 艺术作品: 书籍、歌曲等的标题。
法律: 被命名为法律的文件。
- 语言: 任何指定的语言。
- 日期: 绝对或相对的日期或时期。
- 时间: 小于一天的时间。
- PERCENT: 百分比,包括"%"。
MONEY(金钱): 货币价值,包括单位。
- MONEY:货币价值。 数量: 测量,如重量或距离。
常数:
- "第一","第二",等等。 小数: 不属于其他类型的数字。

让我们看看我们的例子:

for tag in lst_docs[i].ents:    print(tag.text, f"({tag.label_})") 

图片由作者提供

或者使用SpaCy图形工具更好:

displacy.render(lst_docs[i], style="ent")

按作者分类的图片

如果我们想在我们的知识图谱中添加几个属性,这很有用。

继续,使用NLP模型预测的标签,我们可以提取实体和它们的关系。

实体和关系的提取

这个想法很简单,但实施起来却很棘手。对于每个句子,我们要提取主语和宾语,以及它们的修饰语、复合词和它们之间的标点符号。

这可以通过两种方式完成:

  1. 手工操作,你可以从基线代码开始,可能必须稍作修改,以适应你的特定数据集/用例。
def extract_entities ( doc ):  
a, b, prev_dep, prev_txt, prefix, modifier = "" , "" , "" , "" , "" , ""  
for token in doc:  
if token.dep_ != "punct" :  
## prexif --> prev_compound + compound  
if token.dep_ == "compound" :  
prefix = prev_txt + " " + token.text if prev_dep == "compound" 否则标记。text  
  
## 修饰符 --> prev_compound + %mod  
iftoken.dep_.endswith( "mod" ) == True :  
modifier = prev_txt + " " + token.text if prev_dep == "compound" else token.text  
  
## subject --> modifier + prefix + %subj  
if token. dep_.find( "subj" ) == True :  
a = modifier + " " + prefix + " " + token.text  
prefix, modifier, prev_dep, prev_txt = "" , "" , "" , ""  
  
## if object -->修饰符 + 前缀 + %obj  
if token.dep_.find("obj" ) == True :  
b = modifier + " " + prefix + " " + token.text  
  
prev_dep, prev_txt = token.dep_, token.text  
  
# clean  
a = " " .join([i for i in a. split()])  
b = " " .join([i for i in b.split()])  
return (a.strip(), b.strip())  
  
  
# 关系抽取需要基于规则的匹配工具,  
# 原始文本正则表达式的改进版本。  
def extract_relation ( doc, nlp ):  
matcher = spacy.matcher.Matcher(nlp.vocab)  
p1 = [{ 'DEP' : 'ROOT' },  
{ 'DEP' : 'prep' , 'OP' : “?” },  
{ 'DEP' : '代理' , 'OP' : “?” },  
{ 'POS' : 'ADJ' , 'OP' : “?” }]  
matcher.add(key= "matching_1" ,  
  
  
1 ]:matches[k][ 2 ]]  
返回span.text

让我们在这个数据集上试试,看看通常的例子:

## 提取实体  
lst_entities = [extract_entities(i) for i in lst_docs]  
  
## example  
lst_entities[i]

image.png

## 提取关系  
lst_relations = [extract_relation(i,nlp) for i in lst_docs]  
  
## example  
lst_relations[i]

image.png

## 提取属性 (NER)  
lst_attr = []  
for x in lst_docs:  
attr = ""  
for tag in x.ents:  
attr = attr+tag.text if tag.label_== "DATE" else attr+ ""  
lst_attr.append (属性)  
  
## 示例  
lst_attr[i]

image.png

2.另外,你也可以使用 文本分析,这是一个建立在SpaCy之上的库,用于扩展其核心功能。这对用户来说更方便,而且一般来说更准确。

## extract entities and relations  
dic = {"id":[], "text":[], "entity":[], "relation":[], "object":[]}  
  
for n,sentence in enumerate(lst_docs):  
lst_generators = list(textacy.extract.subject_verb_object_triples(sentence))  
for sent in lst_generators:  
subj = "_".join(map(str, sent.subject))  
obj = "_".join(map(str, sent.object))  
relation = "_".join(map(str, sent.verb))  
dic["id"].append(n)  
dic["text"].append(sentence.text)  
dic["entity"].append(subj)  
dic["object"].append(obj)  
dic["relation"].append(relation)  
  
  
## create dataframe  
dtf = pd.DataFrame(dic)  
  
## example  
dtf[dtf["id"]==i]

image.png 让我们也用NER标签(即日期)来提取属性:

## extract attributes  
attribute = "DATE"  
dic = {"id":[], "text":[], attribute:[]}  
  
for n,sentence in enumerate(lst_docs):  
lst = list(textacy.extract.entities(sentence, include_types={attribute}))  
if len(lst) > 0:  
for attr in lst:  
dic["id"].append(n)  
dic["text"].append(sentence.text)  
dic[attribute].append(str(attr))  
else:  
dic["id"].append(n)  
dic["text"].append(sentence.text)  
dic[attribute].append(np.nan)  
  
dtf_att = pd.DataFrame(dic)  
dtf_att = dtf_att[~dtf_att[attribute].isna()]  
  
## example  
dtf_att[dtf_att["id"]==i]

image.png

现在我们已经提取了 "知识",我们可以建立网络图了。

网络图

用于创建和操作图形网络的标准Python库是 NetworkX.我们可以从整个数据集开始创建图,但是,如果有太多的节点,可视化就会很混乱:

## create full graph  
G = nx.from_pandas_edgelist(dtf, source="entity", target="object",  
edge_attr="relation",  
create_using=nx.DiGraph())  
  
  
## plot  
plt.figure(figsize=(15,10))  
  
pos = nx.spring_layout(G, k=1)  
node_color = "skyblue"  
edge_color = "black"  
  
nx.draw(G, pos=pos, with_labels=True, node_color=node_color,  
edge_color=edge_color, cmap=plt.cm.Dark2,  
node_size=2000, connectionstyle='arc3,rad=0.1')  
  
nx.draw_networkx_edge_labels(G, pos=pos, label_pos=0.5,  
edge_labels=nx.get_edge_attributes(G,'relation'),  
font_size=12, font_color='black', alpha=0.6)  
plt.show()

image.png

知识图谱使我们有可能在一个大的层面上看到所有事物的关系,但这样做是非常无用的......所以最好根据我们要寻找的信息应用一些过滤器。在这个例子中,我将只取图中涉及最频繁实体的部分(基本上是连接最多的节点):

dtf["entity"].value_counts().head()

image.png

## filter  
f = "Russia"  
tmp = dtf[(dtf["entity"]==f) | (dtf["object"]==f)]  
  
  
## create small graph  
G = nx.from_pandas_edgelist(tmp, source="entity", target="object",  
edge_attr="relation",  
create_using=nx.DiGraph())  
  
  
## plot  
plt.figure(figsize=(15,10))  
  
pos = nx.nx_agraph.graphviz_layout(G, prog="neato")  
node_color = ["red" if node==f else "skyblue" for node in G.nodes]  
edge_color = ["red" if edge[0]==f else "black" for edge in G.edges]  
  
nx.draw(G, pos=pos, with_labels=True, node_color=node_color,  
edge_color=edge_color, cmap=plt.cm.Dark2,  
node_size=2000, node_shape="o", connectionstyle='arc3,rad=0.1')  
  
nx.draw_networkx_edge_labels(G, pos=pos, label_pos=0.5,  
edge_labels=nx.get_edge_attributes(G,'relation'),  
font_size=12, font_color='black', alpha=0.6)  
plt.show()

image.png

这样就好了。如果你想让它变成3D,可以使用下面的代码:

from mpl_toolkits.mplot3d import Axes3D  
  
fig = plt.figure(figsize=(15,10))  
ax = fig.add_subplot(111, projection="3d")  
pos = nx.spring_layout(G, k=2.5, dim=3)  
  
nodes = np.array([pos[v] for v in sorted(G) if v!=f])  
center_node = np.array([pos[v] for v in sorted(G) if v==f])  
  
edges = np.array([(pos[u],pos[v]) for u,v in G.edges() if v!=f])  
center_edges = np.array([(pos[u],pos[v]) for u,v in G.edges() if v==f])  
  
ax.scatter(*nodes.T, s=200, ec="w", c="skyblue", alpha=0.5)  
ax.scatter(*center_node.T, s=200, c="red", alpha=0.5)  
  
for link in edges:  
ax.plot(*link.T, color="grey", lw=0.5)  
for link in center_edges:  
ax.plot(*link.T, color="red", lw=0.5)  
  
for v in sorted(G):  
ax.text(*pos[v].T, s=v)  
for u,v in G.edges():  
attr = nx.get_edge_attributes(G, "relation")[(u,v)]  
ax.text(*((pos[u]+pos[v])/2).T, s=attr)  
  
ax.set(xlabel=None, ylabel=None, zlabel=None,  
xticklabels=[], yticklabels=[], zticklabels=[])  
ax.grid(False)  
for dim in (ax.xaxis, ax.yaxis, ax.zaxis):  
dim.set_ticks([])  
plt.show()

image.png

请注意,图表可能很有用,也很好看,但它不是本教程的重点。知识图谱最重要的部分是 "知识"(文本处理),然后结果可以显示在数据框、图表或不同的图上。例如,我可以用NER识别的日期来建立一个时间线图。

时间线图

首先,我必须将识别为 "日期 "的字符串转换为日期时间格式。这个库 日期分析器 可以解析网页上常见的几乎所有字符串格式的日期。

def utils_parsetime(txt):  
x = re.match(r'.*([1-3][0-9]{3})', txt) #<--check if there is a year  
if x is not None:  
try:  
dt = dateparser.parse(txt)  
except:  
dt = np.nan  
else:  
dt = np.nan  
return dt

让我们把它应用到属性的数据框架上:

dtf_att["dt"] = dtf_att["date"].apply(lambda x: utils_parsetime(x))  
  
## example  
dtf_att[dtf_att["id"]==i]

miro.medium.com/v2/resize:f…

现在,我将把它与实体-关系的主要数据框架结合起来:

tmp = dtf.copy()  
tmp["y"] = tmp["entity"]+" "+tmp["relation"]+" "+tmp["object"]  
  
dtf_att = dtf_att.merge(tmp[["id","y"]], how="left", on="id")  
dtf_att = dtf_att[~dtf_att["y"].isna()].sort_values("dt",  
ascending=True).drop_duplicates("y", keep='first')  
dtf_att.head()

image.png

最后,我可以绘制时间线。正如我们已经知道的,一个完整的图可能不会有用:

dates = dtf_att["dt"].values  
names = dtf_att["y"].values  
l = [10,-10, 8,-8, 6,-6, 4,-4, 2,-2]  
levels = np.tile(l, int(np.ceil(len(dates)/len(l))))[:len(dates)]  
  
fig, ax = plt.subplots(figsize=(20,10))  
ax.set(title=topic, yticks=[], yticklabels=[])  
  
ax.vlines(dates, ymin=0, ymax=levels, color="tab:red")  
ax.plot(dates, np.zeros_like(dates), "-o", color="k", markerfacecolor="w")  
  
for d,l,r in zip(dates,levels,names):  
ax.annotate(r, xy=(d,l), xytext=(-3, np.sign(l)*3),  
textcoords="offset points",  
horizontalalignment="center",  
verticalalignment="bottom" if l>0 else "top")  
  
plt.xticks(rotation=90)  
plt.show()

image.png 因此,最好是过滤一个特定的时间:

yyyy = "2022"  
dates = dtf_att[dtf_att["dt"]>yyyy]["dt"].values  
names = dtf_att[dtf_att["dt"]>yyyy]["y"].values  
l = [10,-10, 8,-8, 6,-6, 4,-4, 2,-2]  
levels = np.tile(l, int(np.ceil(len(dates)/len(l))))[:len(dates)]  
  
fig, ax = plt.subplots(figsize=(20,10))  
ax.set(title=topic, yticks=[], yticklabels=[])  
  
ax.vlines(dates, ymin=0, ymax=levels, color="tab:red")  
ax.plot(dates, np.zeros_like(dates), "-o", color="k", markerfacecolor="w")  
  
for d,l,r in zip(dates,levels,names):  
ax.annotate(r, xy=(d,l), xytext=(-3, np.sign(l)*3),  
textcoords="offset points",  
horizontalalignment="center",  
verticalalignment="bottom" if l>0 else "top")  
  
plt.xticks(rotation=90)  
plt.show()

image.png

正如你所看到的,一旦 "知识 "被提取出来,你可以用任何你喜欢的方式来绘制它。

总结

本文是一个关于如何用Python构建知识图谱 的教程 我在从维基百科解析的数据上使用了几种NLP技术来提取 "知识"(即实体和关系),并将其存储在一个网络图对象中。

现在你明白为什么公司正在利用NLP和知识图谱来映射来自多个来源的相关数据,并找到对业务有用的见解。试想一下,在与一个实体(即苹果公司)相关的所有文件(如财务报告、新闻、推特)上应用这种模型,可以提取多少价值。你可以迅速了解与该实体直接相关的所有事实、人物和公司。然后,通过扩展网络,甚至可以了解与起始实体没有直接联系的信息(A - > B - > C)。

我希望你喜欢它!如果有问题和反馈,或者只是分享你有趣的项目,请随时联系我。