8-Answer 系列-语义解析模块(三)

上一篇文章中已经介绍了语义图算法及其实现,这一篇介绍实体消歧和谓语消歧。

语义陈述

在介绍实体消歧义和谓语消歧义之前,需要讲解一下 Answer 中的语义陈述的概念。

在 Jena 的官方文档《An Introduction to RDF and the Jena RDF API》 中,将本体库的 RDF 模型中的每一个箭头称为一个陈述,而每一个陈述声明了关于某个资源的某个事实。

一个陈述主要由三个部分组成,如下:

  • 主体:箭头出发的资源
  • 谓语:标识箭头的属性(可以简单理解为箭头本身)
  • 客体:箭头指向的终点资源或文本
    因为一个陈述由上述三部分组成,所以陈述也被称为三元组。

在构建 SPARQL 查询语句对本体库进行查询时,其实并可以看做是以一个个陈述为单位进行查询。例如对于问句“美人鱼的导演是谁?”,构建的查询语句例1如下:

1
2
3
4
Select ?y where {
美人鱼 有导演 ?x,
?x 是 ?y
}

如上面所说的,在进行本体查询时,我们将会以陈述为单位进行构建查询语句。而根据上一篇文章中的语义图算法,我们得到的输出是一个语义有向图,当然我们可以一层层的将这个结果传递下去,在本体查询模块处再进行转换。

但是考虑到后续的实体消歧、谓语消歧等也会需要查询本体库,那么如果一直以语义图为单位进行传递,那么每次与本体库交互可能都需要进行一次对语义图的解析,然后将其转换成陈述,再进行本体查询,这无疑增加了系统的复杂度,同时也会降低系统的处理效率。

那么我们可以在构建语义图之后,对语义图进行一次解析,将语义图构建成陈述,本人将其称为语义陈述,后续的操作也将以陈述为单位进行。

以 “美人鱼的导演是谁?” 为例,我们解析语义图并将其构造成如下的陈述集合,如下图所示:

2018-09-11-8-Answer-pic-1.png

上述的语义陈述集合不仅仅能表达原来语义图所能表达的语义,也和本体查询时的操作单位相一致,同时无论是遍历还是更新等操作都要比原来的语义图更为方便,也更易于理解。

而根据陈述集合构建查询语句的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// answer-knowledge-analysis 模块
// cn.lcy.knowledge.analysis.ontology.query.service.QueryServiceImpl.java
public String createSparql(List<AnswerStatement> answerStatements) {
// 断言集合
int size = answerStatements.size();
// 取出最后一个断言
AnswerStatement lastStatement = answerStatements.get(size - 1);
String queryMask = lastStatement.getObject().getDisambiguationName();
String SPARQL = "SELECT " + queryMask + " WHERE {\n";
for (AnswerStatement answerStatement : answerStatements) {
// 如果谓语是名词
if (NounsTagEnum.isIncludeEnumElement(answerStatement.getPredicate().getCpostag())) {
SPARQL += answerStatement.getSubject().getDisambiguationName() + " " + answerStatement.getPredicate().getDisambiguationName() + " " + answerStatement.getObject().getDisambiguationName() + ".\n";
} else {
SPARQL += answerStatement.getSubject().getDisambiguationName() + " " + answerStatement.getPredicate().getDisambiguationName() + " " + answerStatement.getObject().getDisambiguationName() + ".\n";
}
}
SPARQL += "}";
return SPARQL;
}

结合之前介绍过的 5-Answer 系列-本体查询模块 ,我们就可以将构建成的查询语句传入本体查询模块进行查询。

实体消歧

在之前的文章中已经提到过实体别名的概念,即一个实体具有多个名称。

例如 “周星驰” 这个实体就具有 “星爷”、“星仔”、“阿星” 等多个不同的别名。

对于问句 “周星驰是什么时候出生的?”,一些系统能很容易的回答,但如果是 “星爷是什么时候出生的?” 或者 “星仔是什么时候出生的?” 这类问句,系统就需要先准确识别出 “星爷”、“星仔”、“阿星” 等其实指的是同一个实体,即 “周星驰” 这个实体,这也就意味着,为了提高系统回答的准确度,我们需要解决实体别名的问题。

PS: 星爷也有可能不仅是某个实体的别名,又是另一个实体的本名。这时候就要解决同名实体的问题了。同名实体的解决方案可见下一篇文章。

解决实体别名的问题,也就是能将“星爷”、“星仔”等消歧为“周星驰”这个实体,这个过程可称为实体消歧。而在 Answer 系统中进行实体消歧的方法是在本体构建时,添加实体的等价实体。例如在构建 “周星驰” 这个实体时,我们将相关的别名 “星爷”、“星仔” 等作为周星驰的等价实体添加到本体知识库中,如下图所示:

2018-09-11-8-Answer-pic-2.png

那么在查询 “星爷的出生日期” 这类句子时,我们先查询本体库,将 “星爷” 这个别名消歧为 “周星驰” 这个本名,实体消歧后的问句并可以转换成 “周星驰的出生日期?” 这个问句了,此时系统并能正确识别语义并返回正确的答案。
实体消歧算法描述如下:

  1. 创建实体消歧陈述集合
  2. 遍历语义陈述集合
  3. 取出主体对象,如果该主体对象是别名实体,查询该主体对象的本名实体(通过UUID查询)
  4. 更新该主体消歧后的UUID
  5. 将更新后陈述加入到实体消歧陈述集合中
  6. 返回实体消歧陈述集合

实体消歧的主要代码如下所示:

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
// answer-knowledge-analysis 模块
// cn.lcy.knowledge.analysis.ontology.query.service.QueryServiceImpl.java
/**
* 实体消岐
*/
public List<AnswerStatement> individualsDisambiguation(List<AnswerStatement> answerStatements) {
List<AnswerStatement> individualsDisambiguationStatements = new ArrayList<AnswerStatement>();

for (AnswerStatement individualsDisambiguationStatement : answerStatements) {

// 获取激活的等价实体(激活的概念是为解决同名实体现象,可见下一篇文章的内容)
if (individualsDisambiguationStatement.getSubject().acquireActiveEntity() != null) {
// 如果主语是问号的话 就不需要进行实体消岐了
if (individualsDisambiguationStatement.getSubject().getPosition() < 1024) {
// 查询本体库中是否有该实体
// TODO 改为注入 这里是为测试方便
// boolean individualExist = new JenaDAOImpl().individualExist(myStatement.getSubject().LEMMA);
// 设置实体消岐前的实体名 方便前端读取
System.out.println("进入的实体:" + individualsDisambiguationStatement.getSubject().getName());
// TODO 改为注入 这里是为测试方便 individualDisambiguation为UUID

String individualDisambiguationName = null;
String sameEntityUUID = null;
if (individualsDisambiguationStatement.getSubject().acquireActiveEntity() != null) {
if (individualsDisambiguationStatement.getSubject().acquireActiveEntity().getIsAliases().equals("0")) { // 如果该实体为实体别名
sameEntityUUID = queryDAO.querySameIndividual(individualsDisambiguationStatement.getSubject().acquireActiveEntity().getUUID());
}

}
String entityUUID = sameEntityUUID == null ? individualsDisambiguationStatement.getSubject().acquireActiveEntity().getUUID() : sameEntityUUID;

individualDisambiguationName = queryDAO.queryIndividualComment(entityUUID); // TODO 修改为注入 本体库中的comment表示该实体的实体名
individualsDisambiguationStatement.getSubject().setDisambiguationName(individualDisambiguationName);
individualsDisambiguationStatement.getSubject().setDisambiguationUUID(entityUUID);
System.out.println("消岐后的实体:" + individualsDisambiguationStatement.getSubject().getDisambiguationName());
// 设置实体消岐后的实体名 方便前端读取
//myStatement.setSubjectDisambiguation(myStatement.getSubject().LEMMA);
}
}
individualsDisambiguationStatements.add(individualsDisambiguationStatement);
}
return individualsDisambiguationStatements;
}

实际由上面不难看出,实体消歧的正确性主要取决于本体库的质量,即在构建本体库时是否正确构建了等价实体。根据之前对本体构建的介绍,可以根据百度百科使用规则的方式构建的本体库,其中的等价实体主要来自于百度百科词条中的别名这一属性,所以存在一定的局限性,但也具有一定的准确性。

谓语消歧

对于同一个问题,会有多种不同的提问方式,其中的一个原因就是词语存在同义词的情况,例如 “周星驰出演过哪些电影?” 和 “周星驰参演过哪些电影?” 表达的含义是一样的。

同义词现象广泛存在,我们无法将所有的谓语关系都存入知识库,这样将会使得知识库庞大而冗余,造成检索和操作效率低下,一个合理的做法便是只存一个谓语关系,在查询的时候,将同义谓语消歧为这个知识库中存有的谓语。

假设目前本体库中存有下图的实体关系:

2018-09-11-8-Answer-pic-3.png

现在有查询语句 “周星驰出演过哪些电影?”,我们先查询本体库,查询出周星驰相关的所有谓语——“主演”、“导演”、“参演”,然后我们找出与“出演”最为相似、最为同义的词 “参演”,查询语句最终消歧为 “周星驰参演过哪些电影?”,然后再用这条语句查询本体库并可获得所需要的答案,这一过程,我们并可以称为谓语消歧。

上述过程中关键的一点是找出与 “出演” 这个谓语最为相似的谓语。

在 Answer 系统中我们通过 HanLP 提供的语义相似度计算接口来确定各个谓语之间的谓语相似度。

HanLP 内部的实现原理,是基于哈工大社会计算与信息检索研究中心的《同义词词林扩展版》计算每个词的语义ID,两个词之间的语义近似程度通过两个词的语义ID 的距离计算而来。

这样通过调用 HanLP 提供的计算语义近似程度的方法,我们并可以计算比较用户提的问句中的谓语和本体库中每个谓语之间的语义近似程度,最终取出其语义距离最小的谓语,将问句中的谓语消歧为本体库中已有的谓语,谓语消歧后并可正确回答问题。

PS: 当然如果问句中的谓语在本体库中已存在,那么在比较相似度时,两者相同则相似度最高,消歧的结果为自身(即不变)。

谓语消歧不仅仅是可以提高问题回答的准确度,更是提供了对不同提问方式的回答能力。谓语消歧算法实现如下所示:

  1. 创建谓语消歧陈述集合
  2. 遍历实体消歧陈述集合
  3. 取出陈述中的主体,在本体库中查询该主体的所有本体库陈述(三元组)
  4. 取出查询到的陈述的谓语(predicate)的名称和当前要消歧的谓语做相似度计算
  5. 找出语义距离最短的谓语,将当前谓语消歧为该语义距离最短的谓语
  6. 将更新后的陈述添加到谓语消歧陈述集合中
  7. 返回谓语消歧陈述集合

谓语消歧的主要代码如下所示:

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
// answer-knowledge-analysis 模块
// cn.lcy.knowledge.analysis.ontology.query.service.QueryServiceImpl.java
/**
* 谓语消岐
*/
public List<AnswerStatement> predicateDisambiguation(List<AnswerStatement> answerStatements) {
List<AnswerStatement> predicateDisambiguationStatements = new ArrayList<AnswerStatement>();
int index = 0;
for (AnswerStatement predicateDisambiguationStatement : answerStatements) {
Long minDistance = 9223372036854775807L;
// 问句中的谓语
String predicateOld = predicateDisambiguationStatement.getPredicate().getName();
String mostSimilarPredicate = predicateOld;

//String object = answerStatement.getObject().getName();
// 设置消岐前的谓语 方便前端读取
//answerStatement.setPredicateName(predicateOld);
// TODO 改为注入 这里是为测试方便
// 如果主语不是问号 则进行获取主语的相关断言
List<Statement> statements = null;
if (predicateDisambiguationStatement.getSubject().getPosition() < 1024) {
String subjectUUID = predicateDisambiguationStatement.getSubject().getDisambiguationUUID();
statements = queryDAO.getStatementsBySubject(subjectUUID);
} else {
// 如果主语是问号 则需要进行一次前断言的查询 然后消岐
List<AnswerStatement> preStatements = new ArrayList<AnswerStatement>();
AnswerStatement preStatement = answerStatements.get(index - 1);
AnswerStatement preStatementNew = new AnswerStatement();
try {
BeanUtils.copyProperties(preStatementNew, preStatement);
} catch (IllegalAccessException | InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Word subject = preStatement.getSubject();
Word predicate = preStatement.getPredicate();
Word object = preStatement.getObject();
Word subjectNew = new Word();
Word predicateNew = new Word();
Word objectNew = new Word();
try {
BeanUtils.copyProperties(subjectNew, subject);
BeanUtils.copyProperties(predicateNew, predicate);
BeanUtils.copyProperties(objectNew, object);
} catch (IllegalAccessException | InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
preStatementNew.setSubject(subjectNew);
preStatementNew.setPredicate(predicateNew);
preStatementNew.setObject(objectNew);
preStatements.add(preStatementNew);

List<AnswerStatement> preQueryStatements = new QueryServiceImpl().createQueryStatements(preStatements);
String SPARQL = this.createSparql(preQueryStatements);
QueryResult result = queryDAO.queryOntology(SPARQL);
Word subjectUpdate = predicateDisambiguationStatement.getSubject();
// 更新主语
subjectUpdate.setName(result.getAnswers().get(0).getContent().split(":")[1]);
predicateDisambiguationStatement.setSubject(subjectUpdate);
String subjectUpdateStr = predicateDisambiguationStatement.getSubject().getName();
statements = queryDAO.getStatementsBySubject(subjectUpdateStr);


/*statements = new JenaDAOImpl().getStatementsByObject(object);*/
}

// 迭代所有以Subject为主语的短语 寻找最相似的谓语
for (Statement statement : statements) {
// 本体库中的谓语
String predicate = statement.getPredicate().getURI().split("#")[1];
// 计算句子中谓语和本体库中谓语的语义相似度
String[] predicateArray = predicate.split("有");
if (predicateArray.length >= 2) {
Long distance = CoreSynonymDictionary.distance(predicateOld, predicateArray[1]);
if (distance < minDistance) {
minDistance = distance;
mostSimilarPredicate = predicateArray[1];
}
System.out.println(predicateOld + " 和 " + predicateArray[1] + ":" + distance);
} else {
Long distance = CoreSynonymDictionary.distance(predicateOld, predicateArray[0]);
if (distance < minDistance) {
minDistance = distance;
mostSimilarPredicate = predicateArray[0];
}
System.out.println(predicateOld + " 和 " + predicateArray[0] + ":" + distance);
}
System.out.println();
}
predicateDisambiguationStatement.getPredicate().setDisambiguationName(mostSimilarPredicate);
// 设置谓语消岐的结果 方便前端读取
//answerStatement.setPredicateDisambiguation(mostSimilarPredicate);
predicateDisambiguationStatements.add(predicateDisambiguationStatement);
System.out.println("谓语消岐距离:" + minDistance);
System.out.println("谓语消岐为:" + mostSimilarPredicate);
++index;
}
return predicateDisambiguationStatements;
}

本文所涉及到的源码可见 QueryServiceImpl.java

下一篇

本文介绍了实体消歧和谓语消歧,下一篇将讲解针对同名实体现象的解决方案。

汪.