赞
踩
最近因为工作原因暂时停止机器学习方面知识的学习,研究了一段KBQA。,下面是一个简单的关于中小学生需要掌握的诗词的demo,各位看官有兴趣的可以瞅瞅,欢迎来信一起交流。
1. 原理
KBQA简单讲就是将问题带入提前准备好的知识库寻求答案的一种基于知识库的问答系统。该问答系统可以解析输入的自然语言问句,主要运用REFO库的对象正则表达式匹配得到结果,然后利用对应的SPARQL查询语句,请求后台基于TDB知识谱图数据库的服务,最终得到我们想要的结果。
2. 流程
1.实体检测,获取问题的关键词,比如问题“李白写了哪些诗?”,那么首先必须找到李白,才可以进行下一步。
2.目的获取,一个问题,我们只获取了实体还不够,比如上面,只有李白,还要有目的,不然可能我是想问李白是哪个朝代的人,哪里的人等等,所以需要找到问题的真实目的。
3.关系预测,有了实体和目的,那么我们就需要在知识库里面寻找双方的关系,想办法联系起来。
4.查询构建,将处理好的三元组带入知识库搜索答案。
3. 知识图谱
1.介绍
知识图谱由google于2012年率先提出,其初衷是用以增强自家的搜索引擎的功能和提高搜索结果质量,使得用户无需通过点击多个连接就可以获取结构化的搜索结果,并且提供一定的推理功能。这里我还是用《将进酒》这首诗举个例子,很多人看到《将进酒》,估计第一时间想不到这是哪个年代的诗歌,但是不妨看看作者,李白,很多人对李白就比较耳熟能详了,那么就来了,很多人都知道李白是唐朝人(这里假设没人不知道哈),那么自然而然就知道《将进酒》这首诗写在唐朝了。说了这些,我们发现,如果知识库里面有这些信息,那么就很容易找到答案。如果我们再多加一些相关属性,就可以构成一张简单的知识图了,如下图,具体的我就不描述了,大家可以看看知识图谱介绍这篇文章,讲述的非常明白,我就不在关公门前耍大刀了。
2. 数据格式
在知识图谱中,数据一般以RDF形式的三元组表示。
RDF(Resource Description Framework)即资源描述框架,其本质是一个数据模型。它提供了一个统一的标准,用于描述实体/资源。简单来说,就是表示事物的一种方法和手段。RDF形式上表示为SPO三元组,知识图谱中我们也称其为一条知识。RDF由节点和边组成,节点表示实体/资源、属性,边则表示了实体和实体之间的关系以及实体和属性的关系。如下图所示:
RDF数据集方式主要有以下几种,主要使用Turtle。
1、RDF/XML,用XML的格式来表示RDF数据。之所以提出这个方法,是因为XML的技术比较成熟,有许多现成的工具来存储和解析XML。然而,对于RDF来说,XML的格式太冗长,也不便于阅读,通常我们不会使用这种方式来处理RDF数据。
2、N-Triples,即用多个三元组来表示RDF数据集,是最直观的表示方法。在文件中,每一行表示一个三元组,方便机器解析和处理。开放领域知识图谱DBpedia通常是用这种格式来发布数据的。
3、Turtle, 应该是使用得最多的一种RDF序列化方式了。它比RDF/XML紧凑,且可读性比N-Triples好。
4、RDFa, 是HTML5的一个扩展,在不改变任何显示效果的情况下,让网站构建者能够在页面中标记实体,像人物、地点、时间、评论等等。也就是说,将RDF数据嵌入到网页中,搜索引擎能够更好的解析非结构化页面,获取一些有用的结构化信息。
5、JSON-LD,用键值对的方式来存储RDF数据。
但是RDF的表达能力有限,无法区分类和对象,也无法定义和描述类的关系/属性,这个时候就有人提出RDFS和OWL这两种技术或者说模式语言/本体语言来解决了RDF表达能力有限的困境,详细介绍参考知识图谱基础之RDF,RDFS与OWL。后面的实例分析就是使用OWL来存储数据。
4. 实例分析
知识图谱这个专栏讲的非常详细,我也是参考这位前辈的专栏实现的小demo,所以我就不在这里花过多的时间描述相关方面的知识了,避免理解错误,误导大家。下面就直接开始我自己的实例。本demo实现是为了展示知识图谱,所以将数据分开,其实也可以直接将诗词名、作者、朝代、诗词内容放在一起,全部作为属性,不需要在SQL中创建多个数据表,所以希望大家不要觉得麻烦,感兴趣的可以使用一张表试着做一下。
数据准备
数据是中小学必背诗词,其中信息有诗词名、作者、朝代、诗词内容。
作者信息:姓名、朝代
诗词信息:诗词名、诗词内容
诗句使用scrapy在百度上爬取,如果想自己动手的,可以参考一下前面的博客scrapy学习(一):scrapy框架(爬古诗词)。
数据建模
这步是非必须的,但是为了后面数据映射更容易理解,我还是在这里简述一下。构建数据结构一般使用工具protégé,构建过程参考本体建模,根据我们自己的功能需要,创建三个类Poem(诗词)、Poet(诗人)、Verse(诗句):
上面的类和推理都创建好了,接下来就需要定义每个类里面的属性:poemContent(诗词内容)、poemName(诗词名)、poetDynasty(诗人朝代)、poetName(诗人名)、sentenceId(诗句ID)、verseId(诗词ID)、sentenceContent(诗句内容)、verseLen(诗词长度)。同样右下角也需要定义属性,Domain为属于哪个类,Range与前面关系的Domain有区别,这里表示的是数据类型。
类、关系、属性都定义好之后,就组成了一个简单的数据模型,点击"Window–>Tabs–>OntoGraf",就可以在protégé中很明了的看出相互之间的关系。
将我们构建好的关系导出备用,导出格式如下,文件名随意,我这里取为poem_kbqa.owl
map:poem a d2rq:ClassMap;
d2rq:dataStorage map:database;
d2rq:uriPattern "poem/@@poem.poem_id@@";
d2rq:class :Poem; # 类名
d2rq:classDefinitionLabel "poem"; # sql数据表
.
map:poem_title a d2rq:PropertyBridge;
d2rq:belongsToClassMap map:poem;
d2rq:property :poemName;
d2rq:propertyDefinitionLabel "poem title";
d2rq:column "poem.title";
.
很多人看到上面的语句就懵逼了,但是别急,这可以直接通过D2RQ来实现,D2RQ是以RDF图的方式访问关系数据库的,将RDF的查询等操作翻译成SQL语句,最终在TDB上实现对应操作。用户可以在数据库自动生成的映射文件上修改,从而将数据映射到自己的本体上。
进入D2RQ目录,执行下列语句生成映射文件,我这里取名poem_demo_mapping.ttl,大家自己随意就好。
$ ./generate-mapping -u root -p 123456 -o poem_demo_mapping.ttl jdbc:mysql://127.0.0.1:3306/poem_kbqa_demo?serverTimezone=UTC
# Table poem #Poem类 map:poem a d2rq:ClassMap; d2rq:dataStorage map:database; d2rq:uriPattern "poem/@@poem.poem_id@@"; d2rq:class vocab:poem; d2rq:classDefinitionLabel "poem"; . map:poem__label a d2rq:PropertyBridge; d2rq:belongsToClassMap map:poem; d2rq:property rdfs:label; d2rq:pattern "poem #@@poem.poem_id@@"; . map:poem_poem_id a d2rq:PropertyBridge; d2rq:belongsToClassMap map:poem; d2rq:property vocab:poem_poem_id; d2rq:propertyDefinitionLabel "poem poem_id"; d2rq:column "poem.poem_id"; d2rq:datatype xsd:integer; . map:poem_title a d2rq:PropertyBridge; d2rq:belongsToClassMap map:poem; d2rq:property vocab:poem_title; d2rq:propertyDefinitionLabel "poem title"; d2rq:column "poem.title"; . map:poem_content a d2rq:PropertyBridge; d2rq:belongsToClassMap map:poem; d2rq:property vocab:poem_content; d2rq:propertyDefinitionLabel "poem content"; d2rq:column "poem.content"; .
belongsToClassMap为类名,property为属性名,propertyDefinitionLabel为数据描述,column数据表的标签。这里我们发现这和我们上面数据建模的关系很相似,belongsToClassMap–>domain,这里将上面的映射文件修改成我们在数据建模里使用的定义,结合关系图,这样看起来更明白。
# Table poem map:poem a d2rq:ClassMap; d2rq:dataStorage map:database; d2rq:uriPattern "poem/@@poem.poem_id@@"; d2rq:class :Poem; d2rq:classDefinitionLabel "poem"; . map:poem_title a d2rq:PropertyBridge; d2rq:belongsToClassMap map:poem; d2rq:property :poemName; d2rq:propertyDefinitionLabel "poem title"; d2rq:column "poem.title"; . map:poem_content a d2rq:PropertyBridge; d2rq:belongsToClassMap map:poem; d2rq:property :poemContent; d2rq:propertyDefinitionLabel "poem content"; d2rq:column "poem.content"; .
PREFIX : <http://www.poem.com#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX owl: <http://www.w3.org/2002/07/owl#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
PREFIX vocab: <http://localhost:2020/resource/vocab/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX map: <http://localhost:2020/resource/#>
PREFIX db: <http://localhost:2020/resource/>
SELECT ?o WHERE {
?s :poemName '将进酒'.
?s :poemContent ?o.
}
SELECT指定我们要查询的变量。这里需要查询内容,用?o代替。
WHERE指定我们要查询的图模式。意思上和SQL的WHERE没有区别。
?s、?o为三元组的实体表示。
:poemContent、:poemName为关系
最终查询出来的结果为:
"君不见,黄河之水天上来,奔流到海不复回,君不见,高堂明镜悲白发,朝如青丝暮成雪,人生得意须尽欢,莫使金樽空对月,天生我材必有用,千金散尽还复来,烹羊宰牛且为乐,会须一饮三百杯,岑夫子,丹丘生,将进酒,杯莫停,与君歌一曲,请君为我倾耳听,钟鼓馔玉不足贵,但愿长醉不复醒,古来圣贤皆寂寞,惟有饮者留其名,陈王昔时宴平乐,斗酒十千恣欢谑,主人何为言少钱,径须沽取对君酌,五花马,千金裘,呼儿将出换美酒,与尔同销万古愁,"
$ ./d2r-server.bat poem_demo_mapping.ttl
from SPARQLWrapper import SPARQLWrapper, JSON sparql = SPARQLWrapper("http://localhost:2020/sparql") sparql.setQuery(""" PREFIX : <http://www.poem.com#> PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> PREFIX owl: <http://www.w3.org/2002/07/owl#> PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> PREFIX vocab: <http://localhost:2020/resource/vocab/> PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> PREFIX map: <http://localhost:2020/resource/#> PREFIX db: <http://localhost:2020/resource/> SELECT ?o WHERE { ?s :poemName '将进酒'. ?s :poemContent ?o. } """) sparql.setReturnFormat(JSON) results = sparql.query().convert() for result in results["results"]["bindings"]: for x in results["head"]["vars"]: print(result[x]["value"])
最终查询出来的结果为:
"君不见,黄河之水天上来,奔流到海不复回,君不见,高堂明镜悲白发,朝如青丝暮成雪,人生得意须尽欢,莫使金樽空对月,天生我材必有用,千金散尽还复来,烹羊宰牛且为乐,会须一饮三百杯,岑夫子,丹丘生,将进酒,杯莫停,与君歌一曲,请君为我倾耳听,钟鼓馔玉不足贵,但愿长醉不复醒,古来圣贤皆寂寞,惟有饮者留其名,陈王昔时宴平乐,斗酒十千恣欢谑,主人何为言少钱,径须沽取对君酌,五花马,千金裘,呼儿将出换美酒,与尔同销万古愁,"
tdb支持RDF数据,所以需要讲我们的映射文件转为RDF文件。进入D2RQ目录:
$./dump-rdf.bat -o poem_kbqa.nt poem_demo_mapping.ttl
poem_demo_mapping.ttl是建模后的映射文件。其支持导出的RDF格式有“TURTLE”, “RDF/XML”, “RDF/XML-ABBREV”, “N3”, 和“N-TRIPLE”。“N-TRIPLE”是默认的输出格式。poem_kbqa.nt就是我们生成的RDF文件。
tdb数据存放,进入apache-jena\bat目录下:
$.\tdbloader.bat --loc="C:\KBQA\apache-jena\tdb" "C:\d2rq-0.8.1\poem_kbqa.nt"
--loc为数据存放路径,poem_kbqa.nt为RDF文件。
进入入Fuseki文件夹,运行fuseki-server.bat,然后退出。程序会为我们在当前目录自动创建run文件夹。将我们前面在数据建模时产生的文件poem_kbqa.owl移动到run文件夹下的databases文件夹中,并将owl后缀名改为ttl。在run文件夹下的configuration中,我们创建名为fuseki_conf.ttl的文本文件(取名随意),加入如下内容:
@prefix : <http://base/#> . @prefix tdb: <http://jena.hpl.hp.com/2008/tdb#> . @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> . @prefix ja: <http://jena.hpl.hp.com/2005/11/Assembler#> . @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . @prefix fuseki: <http://jena.apache.org/fuseki#> . <#service3> rdf:type fuseki:Service ; fuseki:name "poem_kbqa" ; # RDF文件表 fuseki:serviceQuery "sparql" ; # SPARQL query service fuseki:dataset <#dataset> ; . <#dataset> rdf:type tdb:DatasetTDB ; tdb:location "C:/KBQA/apache-jena/tdb" ; # tdb数据路径 # Query timeout on this dataset (1s, 1000 milliseconds) ja:context [ ja:cxtName "arq:queryTimeout" ; ja:cxtValue "1000" ] ; # Make the default graph be the union of all named graphs. ## tdb:unionDefaultGraph true ; .
再次执行命令:
$ ./fuseki-server.bat
关于jena-fuseki SPARQL query版本问题的解决方案
默认端口是 3030,在浏览器输入“http://127.0.0.1:3030/”, 可以看到如下界面。
实践篇(四):Apache jena SPARQL endpoint及推理这里讲述了规则推理,个人觉得可以不使用,直接利用语句推理即可,各位看官根据自己的喜好来决定。
5. 总结
使用知识图谱,优缺点同样明显,在推理和关系上非常方便,但是如果在实际项目中使用,如果数据有更新,那么就需要重新修改数据库和映射,需要重新走一遍全部流程,反而麻烦,所以下篇博客我会换种方式再次实现一次相同效果的demo。
本博客是根据知识图谱-给AI装个大脑模拟写出来的,在这里非常感谢该博主的无私分享。如果各位看官发现博客中有什么不对的,或者跟自己想法不一样的,可以留信探讨一下,纯属个人观点,不喜勿喷。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。