根据论文:Semantic Parsing on Freebase from Question-Answer Pairs,分析其代码和步骤,以加强对这一流程的深入理解,重点关注模型的输入、输出和具体方法。
前言
提供阅读本文的前提知识,引用自Semantic Parsing on Freebase from Question-Answer Pairs这篇论文。
语义解析
知识库由大量的三元组组成,并且这些三元组的实体和实体关系都是形式化的语言。
比如(BarackObama, PlaceOfBirth, Honolulu),给定一个自然语言的问题 “Where was Obama born?”,我们面临的第一个挑战就是,如何建立问题到知识库的映射?
语义解析KBQA的思路是通过对自然语言进行语义上的分析,转化成为一种能够让知识库理解的语义表示,进而通过知识库中的知识,进行推理(Inference)和查询(Query),得出最终的答案。
简而言之,语义解析就是将自然语言的问题,转化为一种能够让知识库“看懂”的语义表示,这种语义表示即逻辑形式(Logic Form)。
逻辑形式
为了能够对知识库进行查询,我们需要一种能够“访问”知识库的逻辑语言,Lambda-DCS是一种经典的逻辑语言,它用于处理逻辑形式而实际操作中我们经常用SPARQL,它通常可以在Virtuoso engine上对Freebase进行查询。如果把知识库看作是一个数据库,逻辑形式则可以看作是查询语句的表示。
我们用 z 表示一个逻辑形式,用 k 表示知识库,e 表示实体,p 表示实体关系(有的也称谓语或属性)。逻辑形式分为一元形式(unary)和二元形式(binary)。对于一个一元实体 e ,我们可以查询出对应知识库中的实体;给定一个二元实体关系 p ,可以查到它在知识库中所有与该实体关系 p 相关的三元组中的实体对。
并且,我们可以像数据库语言一样,进行连接Join,求交集Intersection和聚合Aggregate(如计数,求最大值等等)操作。具体来说,逻辑形式有以下形式和操作:
有了上面的定义,我们就可以把一个自然语言问题表示为一个可以在知识库中进行查询的逻辑形式,
比如对于问句 “Number of dramas starring Tom Cruise?”
它对应的逻辑形式是 count(Genre.Drama \cap Performance.Actor.TomCruise)
当自然语言问题转化为逻辑形式之后,通过相应的逻辑语言(转化为SPARQL query)查询知识库就可以得到答案。
所以接下来就是我们要学习和研究的重点:语义解析如何把自然语言问题正确地转化为相应的逻辑形式?
1. 概要
该论文该方法来自斯坦福的 Jonathan Berant,发表于2013年的EMNLP会议,是一个经典的语义解析baseline。
论文在Freebase上提出训练一个语义解析器,并基于该语义解析器进行KBQA,不再依赖于代价大的标注逻辑形式,而直接从问题-答案对中学习。给定自然语言问句,KBQA最棘手的问题就是存在相关的大量可能的逻辑谓词,对于怎么从问题句子中降低可能的逻辑谓词(logical predicates),作者提出了两种方法:
(1)短语到谓词的映射(mapping):通过将大型文本语料库与Freebase对齐,构建一个语句到谓词的粗糙映射
(2)桥接操作(bridging operation):基于相邻谓词生成与之兼容的逻辑谓词
2. 模型
该模型为使用本文所将方法而实现的一个小的demo模型,具体流程和框架与原论文基本一致,其中2.1解析器方法会重点介绍论文中使用的Sempre源代码和方法
2.1 模型架构
输入:英语自然语言查询
输出:从Freebase中检索出的自然语言答案
示例: Who did Obama marry? -> (SBARQ (WHNP (WP Who)) (SQ (VBD did) (NP (NNP Obama)) (VP (VB marry))) (. ?))
-> {'subject': 'Obama', 'property': 'marry'}
-> SELECT ?valLabel WHERE { { wd:Q76 p:P26 ?prop . ?prop ps:P26 ?val . } SERVICE wikibase:label { bd:serviceParam wikibase:language "en"} }
-> Michelle Obama
2.2 解析器方法—Sempre 1.0
源代码文件框架
主要源文件:
-
Main.java:Sempre 主类,包含了 Sempre 的入口点。
-
Master.java:Sempre 主控类,负责调度和管理解析任务。
-
Session.java:Sempre 会话类,表示一个解析任务的会话。
-
Builder.java:Sempre 构建器类,用于构建语义解析器的各个组件。
解析器相关:
-
Parsers.java:解析器管理类,用于管理不同类型的解析器。
-
Parser.java:解析器接口,定义了解析器的基本行为。
-
BeamParser.java:Beam 解析器类,实现了基于 Beam Search 的解析算法。
语法和特征提取:
-
Grammar.java:语法类,用于管理语法规则。
-
FeatureExtractor.java:特征提取器类,用于从输入中提取特征。
-
FeatureVector.java:特征向量类,用于表示特征。
执行器相关:
-
Executor.java:执行器接口,定义了执行器的基本行为。
-
SparqlExecutor.java:SPARQL 执行器类,用于执行 SPARQL 查询。 其他支持类:
-
LanguageInfo.java:语言信息类,用于表示语言相关信息。
-
StringCacheServer.java:字符串缓存服务器类,用于管理字符串缓存。
-
FreebaseSearch.java:Freebase 搜索类,用于在 Freebase 中进行实体搜索。
输入:给定 i)一个知识库K ii)问题-答案对训练集\{(x i, y i)\}_{i=1}^{n}
输出:通过潜在逻辑形式z和知识库K将问题x映射到答案y上的语义解析器
整体是一个自底向上构造语法树的过程,树的根节点,就是该自然语言问题最终的逻辑形式表达。可以分为两个步骤:
-
1. 词汇映射(Lexicon):即构造底层的语法树节点。将单个自然语言短语或单词映射到知识库实体或知识库实体关系所对应的逻辑形式。作者通过构造一个词汇表(Lexicon)来完成这样的映射。
-
2. 构建(Composition):即自底向上对树的节点进行两两合并,最后生成根节点,完成语法树的构建。这一步有很多种方法,诸如构造大量手工规则,组合范畴语法(Combinatory Categorical Grammars,CCG)等等,本方法中作者采用了最暴力的方法:对于任何两个节点都可以执行上面所谈到的连接Join,求交Intersection,聚合Aggregate三种操作。以及使用独创的桥接操作进行结点合并。缺点是这种合并方式复杂度是指数级的,最终会生成很多棵语法树,因此还需要通过对训练数据进行训练,训练一个分类器,对语法树进行筛选。
自然语言—>逻辑形式流程如下图所示:
- 红色部分即逻辑形式
- 绿色部分where was Obama born 为自然语言问题
- 蓝色部分为词汇映射(Lexicon)和构建(Composition)的操作
- 最终形成的语义解析树的根节点为语义解析结果。
针对上面的方法,有三个待解决的细节问题:(1)如何训练分类器?(2)如何构建词汇表?(3)什么是桥接操作?
2.2.1 训练分类器
分类器的任务是计算每一种语义解析结果d(Derivation)的概率,对于对每个 d ∈ D(x),使用判别对数线性模型discriminative log-linear model进行建模,使用Softmax进行概率归一化,公式如下:
其中 是b维的参数向量,它是一个从语义解析结果和x中提取出来的b维特征向量(该特征向量包含了构造该语法树所有操作的对应特征,每种操作的具体特征之后会提到),
对于训练数据对,训练的目标是最大化Log-likelihood损失函数,通过AdaGrad算法(一种动态调整学习率的随机梯度下降算法)进行参数更新。
2.2.2 构建词汇表
词汇表即自然语言与知识库实体或知识库实体关系的单点映射,这一操作也被称为对齐(Alignment)。
将自然语言实体映射到知识库实体相对比较简单,比如将“Obama was also born in Honolulu.”中的实体Obama映射为知识库中的实体BarackObama,可以使用一些简单的字符串匹配方式进行映射。
但是要将自然语言短语如“was also born in”映射到相应的知识库实体关系,如PlaceOfBirth, 则较难通过字符串匹配的方式建立映射。
可以进行统计,如果有较多的实体对(entity1,entity2)作为主语和宾语出现在was also born in的两侧,并且在知识库中,这些实体对也同时出现在包含PlaceOfBirth的三元组中,那么我们可以认为“was also born in”这个短语可以和PlaceOfBirth建立映射。简而言之就是:如果一个句子两端实体和知识库谓词两端实体重复很多,则将这两者对齐。
比如(“Barack Obama”,“Honolulu”),(“MichelleObama”,“Chicago”)等实体对在文档中经常作为“was also born in”这个短语的主语和宾语,并且它们也都和实体关系PlaceOfBirth组成三元组出现在知识库中。
本论文在ClueWeb09上抽取15millions个三元组构成一个数据集,如(“Obama”, “was also born in”, “August 1961”),可以看出三元组的实体和关系都是自然语言的形式,取出其中的一个三元组子集,对里面的每一个三元组的主语实体和宾语实体通过字符匹配的方式替换为知识库的实体,并对数据进行归一化。
如(“Obama”, “was also born in”, “August 1961”) 经过预处理后转化为 (BarackObama, “was also born in”, 1961-08)。
接着对每一个三元组中的自然语言短语r_{1} 两边的实体对(entity1,entity2)进行统计。
注意⚠️ 由于自然语言短语具有多义性,和知识库实体关系的对应关系是多对多的,比如“was also born in”可能对应PlaceOfBirth或DateOfBrith,我们需要对每一个进行区分。可以通过知识库查询到每一个实体的类型(type),比如1961-08的类型是date,而Honolulu的类型是place,我们对两边的实体类型进行查询可以得到主语实体的类型和宾语实体的类型t_{2},因此可以进一步表示为,我们对其所在三元组两边的实体进行统计,得到实体对集合。对齐构造主要如下图:
图中绿色字体为r_{1},蓝色字体为r_{2}。作者定义了词汇映射操作的三种特征用于训练分类器:对齐特征(Alignment features),文本相似度特征(Text similarity features),和词汇化特征(Lexicalized features),具体内容如下表所示:
其中文本相似度特征中的s_{2}指r_{2}的freebase name。
在实际使用中,我们可以通过词性标注(POS)和命名实体识别(NER)来确定哪些短语和单词需要被词汇映射(Lexicon),从而忽略对一些skipped words进行词汇映射。并且,作者还建立了18种手工规则,对问题词(question words)进行逻辑形式的直接映射,如“where,how many”映射为Type.Location 和 Count。
构建词汇表/对齐部分的代码位于文件:sempre/src/edu/stanford/nlp/sempre/paraphrase/Aligner.java
其中对齐操作在
align()
方法中:public Alignment align(ParaphraseExample example, Params params) {example.ensureAnnotated();Alignment alignment = new Alignment(example);alignment.buildAlignment(example, params);example.setAlignment(alignment);return alignment; }
构建对齐结果,包括计算同义词替换、删除、添加等操作在Alignment 内部类的
buildAlignment()
方法中:public void buildAlignment(ParaphraseExample example, Params params) {computeIdentityAlignments(example);computePhraseTableAlignments(example);computeSubstitutionsAlignment(example);computeDerivationsAlignment(example);//this needs to be done lastmarkDeletions(example);if(opts.useSyntax)computeSyntacticAlignment(example);if(opts.verbose>=1) {printFeaturesAndWeights(params);}score = featureVector.dotProduct(params);}
2.2.3 桥接操作
完成词汇表的构建后,仍然存在一些问题。比如,对于go,have,do这样的轻动词(light verb)难以直接映射到一个知识库实体关系上,其次,有些知识库实体关系极少出现,不容易通过统计的方式找到映射方式,还有一些词比如actress,实际上是两个知识库实体关系进行组合操作后的结果 。因此需要找到一个额外的二元关系来将当前的逻辑形式连接起来,这就是桥接。
举个具体的例子,比如 “Which college did Obama go to?”
假设“Obama”和“college”可被词汇映射映射为 BarackObama和 Type.University,这里"go to"却难以找到一个映射,所以这里我们需要去寻找一个中间二元关系b(即Education)使得上面的句子可以被解析为(Type.University Education.BarackObama),如下图所示:
具体来说就是给定两个一元逻辑形式,它们的类型分别为,对于每个二元关系b如果它的主语宾语类型和相同,则在b对应的实体类型满足的条件下,生成逻辑形式
桥接部分的代码位于:sempre/src/edu/stanford/nlp/sempre/BridgeFn.java,其中
bridgeUnary()
方法处理一元关系的桥接,它根据候选上下文和问题上下文的类型信息,以及二元关系的信息,将一元关系与二元关系连接起来;
bridgeEntity
方法处理实体的桥接操作,它根据问题上下文的类型信息和二元关系的信息,将实体与相应的二元关系连接起来;
injectIntoCvt
方法处理将实体注入到复合关系中的桥接操作,它找到复合关系中的待填充位置,并将实体与相应的二元关系连接起来。
3. 模型代码
模型主要实现代码位于nlquery代码的nlquery/nlquery/nlquery.py文件,其中:
NLQueryEngine类包含了整个查询流程的实现:
-
__init__
方法会创建一个StanfordServerParser对象,用于将自然语言问题解析成树状结构。 -
subject_query
方法的作用是将匹配到的上下文转换为查询参数,并执行查询操作。这个方法主要用于处理主语查询,即从自然语言问题中提取出主语、动词动作、属性等信息,然后根据这些信息执行查询操作,最终返回查询结果。 -
find_entity_query
方法的作用是将匹配到的上下文转换为查询参数,并执行查询以找到实体。该方法通常用于解析自然语言查询,提取查询中的实体、属性和限定条件,并将其转换为可以用于查询实体的参数。最终,该方法将返回一个包含查询结果的Answer
对象。 -
get_property
方法的作用是获取给定主题的属性。该方法通常用于根据给定的主题和属性查询相应的信息,比如查询某个人的年龄、职业等属性。根据查询的结果,该方法会返回一个包含查询结果的Answer
对象。 -
preprocess
方法的作用是对查询语句进行预处理,主要是添加必要的标点符号,确保查询语句的格式正确。通常情况下,查询语句应以问号结尾,如果查询语句缺少问号,则该方法会自动在末尾添加问号。
4.细节
-
对于知识库的存储,常见采用的是Virtuoso SPARQL engine,配合lambda-DCS进行查询。
-
知识库存储也可采用jena+sparql框架实现,环境搭建https://jena.apache.org/download/
3. 参考/代码资料
-
论文阅读:Semantic Parsing on Freebase from Question-Answer Pairs-CSDN博客
-
解析器Sempre第一版代码: Sempre 1.0 Webquestions training · Issue #47 · percyliang/sempre · GitHub
-
KBQA_Demo代码:GitHub - ayoungprogrammer/nlquery: Natural Language Engine on WikiData