这篇论文主要提出了attentive recurrent neural network with similarity matrix based convolutional neural network (AR-SMCNN) model,通过知识图谱回答single-relation问题。
[论文下载地址](https://arxiv.org/vc/arxiv/papers/1804/1804.03317v2.pdf),好几个人在评论说找不到论文?(今天2019-2-19还是可以下载)
本文主要翻译论文中的模型,以及研究作者开源的项目代码(地址见论文脚注)。
1、数据准备
SimpleQuestions数据集
包含108,442 个人工提出的简单问题,提取自freebase。其中70%为训练集(75910),10%为校验集(10845),剩余20%为测试集。
文件名:annotated_fb_data_{train, valid, test}.txt
每个文件中的每一行格式如下:Subject-entity [tab] relationship [tab] Object-entity [tab] question。
例:
www.freebase.com/m/0f3xg_ www.freebase.com/symbols/namesake/named_after www.freebase.com/m/0cqt90 Who was the trump ocean club international hotel and tower named after
FB2M、FB5M数据集
每一行描述一个事实。
文件名:freebase-{FB2M, FB5M}.txt
文件格式:Subject-entity [tab] relationship [tab] a list of Object-entities,其中多个宾语实体用空格分隔。
例:
www.freebase.com/m/0n1vy1h www.freebase.com/people/person/gender www.freebase.com/m/05zppz
FB5M.name数据集
每一行指示FB5M中的实体的名称。
文件名:FB5M.name.txt
例:
<fb:m.0f8v12b> <fb:type.object.name> "blue christmas" .
数据转换
将原数据集中的www
形式转换为fb:
形式。读取freebase-FB2M.txt
,写入FB2M.core.txt
。
例:
<fb:m.0crxxsx> <fb:film.film.genre> <fb:m.02kdv5l> .
写入数据库
将FB2M数据写入virtuoso数据库
预处理训练、校验、测试数据
开启多线程,将SimpleQuestions数据中的每一行用列表存储至QAdata中,再将数据序列化存储至文件。
源文件:annotated_fb_data_{train,valid,test}.txt
,目标文件:QAData.{train,valid,test}.pkl
。
目标文件中每一行数据包括:question, sub, rel, obj, length。其中question用“.”分隔,length表示词的数量。
QAdata中对问题和主体进行reverse_link()操作,即从问题中匹配最长的实体字符串,如果匹配到实体,则填写text_subject,并将该实体对应的tokens的索引标记到text_attention_indices中,再将匹配到的实体字符串替换为“X”,设置为question_pattern。
代码文件:process_rawdata.py
def reverse_link(question, subject):
……
for res in sorted(res_list, key = lambda res: len(res), reverse = True):
pattern = r'(^|\s)(%s)($|\s)' % (re.escape(res))
if re.search(pattern, question):
text_subject = res
text_attention_indices = get_indices(tokens, res.split())
break
将实体与关系的node_id的列表,用序列化的方式写入FB2M.{ent,rel}.pkl
中。再将上述列表按照字符排序后以字符串的形式写入FB2M.{ent,rel}.txt
中,用作字典。
例:
fb:m.05zppz
创建词典
下载glove.42B.300d.txt
读取FB2M.{ent,rel}.txt
,使用torch.save()方法分别写入vocab.{ent,rel}.pt
中。
读取FB2M.rel.txt
,将每一个关系分割为2部分,最后一个“.”之前为rel1,最后一个“.”之后为rel2,分别创建两部分的词典rel1_vocab, rel2_vocab,使用torch.save()方法将这两个词典写入vocab.rel.sep.pt
中。
分别读取QAData.{train,valid,test}.pkl
,将每一个question字符串按空格分割,得到词列表,都存入词典word_rel_vocab中。
读取FB2M.rel.pkl
,将每一个关系按照“.”和“_”分割得到的每一个词继续存入word_rel_vocab中。
再将word_rel_vocab用torch.save()方法写入vocab.word&rel.pt
中。
创建实体检测训练数据
加载词典vocab.word&rel.pt
源文件:QAData.{train,valid,test}.pkl
,目标文件:{train,valid,test}.entity_detection.pt
。同时将结果以日志形式存储至{train,valid,test}.entity_detection.txt
。
对每一个问题,分割为词,将词的tokens映射为词典中的id,并以torch.LongTensor存入seqs列表中。将text_attention_indices标记出的实体对应的tokens存入seq_labels中。最后将(seqs, seq_labels)保存到文件{train,valid,test}.entity_detection.pt
中。
2、训练实体检测模型
实体检测过程如下图所示。给定一个问题Q,我们将一个双向的LSTM网络像一个连续的标签任务一样训练,它可以被看作是一个二进制分类问题,它可以预测一个句子中的每个单词是否属于实体提及。然后,我们得到一组带有正标签的单词,它表示为C,这些词可能不是连续的,然后我们使用启发式方法得到实体提到X。
(1)在C中合并相邻的单词(忽略间隙<=1),形成一个子串S。如果有多个子串,保留最长的子串。
(2)找出所有在Freebase中名称或别名与S完全相等的实体。这些实体构成实体候选E,S被视为实体提及。如果没有匹配,继续步骤(3)。
(3)事实上,S中的词最有可能构成实体提及X,因此基于S生成X的可能性也就很高。所以我们以S为中心,在它相邻位置找X。具体地说,我们在S附近扩展或缩小至多2个单词以获得S’,再使用S’来查找相应的实体。一旦匹配,就确定了E和X。
文件entity_detection/predict.py
def get_candidate_sub(question_tokens, pred_tag):
……
sub_list = []
shift = [0,-1,1,-2,2]
pred_sub = []
for left in shift:
for right in shift:
for i in range(len(starts)):
if starts[i]+left < 0:continue
if ends[i]+1+right > len(question_tokens):continue
text = question_tokens[starts[i]+left:ends[i]+1+right]
subject = virtuoso.str_query_id(' '.join(text))
if left==0 and right==0:
pred_sub = subject
sub_list.extend(subject)
if sub_list:
return pred_sub, sub_list
(4)如果仍然没有找到匹配的对象,那么我们将使用S中的每个单词来搜索名称中包含单词的实体。我们将拥有最长公共子序列的实体作为实体候选E,而公共子序列将是X,这一步与之前的方法相似,但这里发生的概率小于0.2%。
我们假设如果一个词被预测为负标签,而它的相邻词却是正的,那么这个预测就是错的。基于这个假设,我们在步骤(1)中将分散的词组合在一起,再进行后续步骤可以提升召回率。主体在实体候选列表中是按照它连接的关系总数排序的。
经过上述4个步骤,我们可以得到一个实体候选集合,再据此生成问题模式。
3、训练关系检测模型
给定一个问题模式P,对于关系候选池R中的每个关系
r
k
r^k
rk,我们计算一个匹配得分
S
(
P
,
r
k
)
S(P,r^k)
S(P,rk)来表示它们之间的相关性,最终预测结果
r
^
k
\hat{r}^k
r^k表示为
r
^
k
=
arg
m
a
x
r
k
∈
R
(
S
(
P
,
r
k
)
)
\hat{r}^k =\underset{r^k\in R}{\arg max}(S(P,r^k))
r^k=rk∈Rargmax(S(P,rk))
我们的AR-SMCNN模型在semantic-level和literal-level两个粒度上都考虑到了P和
r
k
r^k
rk的相关性。
Semantic-level
为了在问题和关系之间进行语义层面的匹配,我们构建了一个attentive RNN作为encoder-compare框架。我们发现Freebase中的关系包含两个方面的信息,一个表示主体的类型,另一个描述了主体和客体之间的关系。例如,问题模式“which language is the film in”与<film.film.language >关系相关,关系的前两部分“film.film”表示主体的类型,最后一部分“language”表示主体和答案“German”之间的真实关系。这两部分与问题中的不同词相关联,因此每个词在编码过程中应该赋予不同的权重。鉴于此,我们对关系的这两部分分别进行编码,并且在问题编码器中使用注意力机制来匹配它们。
我们提出的AR-SMCNN模型如下图所示,左边是attentive BiGRU,右边是CNN on similarity matrix,最后将特征
z
1
,
z
2
,
z
3
,
z
4
z_1,z_2,z_3,z_4
z1,z2,z3,z4连接在一起并通过一个线性层得到最终评分
S
(
P
,
r
k
)
S(P,r^k)
S(P,rk)。
关系编码
对于R中的每个关系,我们将其分割成两个部分,并将每个部分转换成随机初始化的可训练的embedding,以得到它们的向量表示 r 1 r_1 r1和 r 2 r_2 r2。
问题模式编码
将问题中的每个词转换成word embedding { q 1 , ⋯   , q L } \{q_1,\cdots,q_L\} {q1,⋯,qL},然后将embedding送到双向GRU网络中以获得隐藏表示 H 1 : L = [ h 1 ; ⋯   ; h L ] H_{1:L}=[h_1;\cdots;h_L] H1:L=[h1;⋯;hL](每个向量 h i h_i hi是在时间 i i i的前向/后向表示之间的级联)。关系的每一部分对问题给予不同的关注,并决定问题的表示。注意力的程度被用作问题中每个词的权重。因此,对于关系表示 r i r_i ri,其对应的问题模式表示 p i p_i pi计算方法如下:
p i = ∑ j = 1 L α i j h j   p_i = \sum_{j=1}^{L}\alpha_{ij}h_j\, pi=j=1∑Lαijhj α i j = exp ( w i j ) ∑ k = 1 L exp ( w i k )   \alpha_{ij}=\frac{\exp(w_{ij})}{\sum_{k=1}^{L}\exp(w_{ik})}\, αij=∑k=1Lexp(wik)exp(wij) w i j = v T tanh ( W T [ h j ; r i ] + b )   w_{ij}=v^T\tanh(W^T[h_j;r_i]+b)\, wij=vTtanh(WT[hj;ri]+b)
其中 i ∈ { 1 , 2 } i\in\{1,2\} i∈{1,2}, α i j \alpha_{ij} αij是指关系表示 r i r_i ri在问题中第 j j j个词的注意力权重。 L L L是问题的长度。设 r i r_i ri的维度为 m m m, h i h_i hi的维度为 n n n,那么 W W W和 v v v是要进行训练的参数,有 W ∈ R c × ( m + n ) , v ∈ R 1 × c W\in R^{c\times(m+n)},v\in R^{1\times c} W∈Rc×(m+n),v∈R1×c,其中 c c c是超参数。
相似性度量
现在我们得到了问题模式和关系的表示,它们的相似度可以用以下公式计算,
z
i
=
p
i
⨂
r
i
        
(
i
=
1
,
2
)
z_i = p_i\bigotimes r_i\;\;\;\;(i=1,2)
zi=pi⨂ri(i=1,2)
这里的
⨂
\bigotimes
⨂是指两个向量的点积运算。我们在计算它们的相互关系时还使用了诸如余弦、双线性、曼哈顿距离和GESD方法,但是这些方法并不能提高准确率,反而会降低训练速度。所以对于这个任务来说,简单的点积足够了。
上图是相似性矩阵的两个例子。图中方格的颜色越深表示两个词的相似度越高。图中我们可以看到不同类型的匹配模式(红框标记),卷积核可以提取到这些特征。
Literal-level
在字面层面考虑它们的相似性时,可以把这个问题看做是文本匹配问题。我们发现一些词(或短语)即使表示同一个含义,但是在问题和关系中有着不同的表达方式(或顺序)。例如在上图中,词对(musical,music)(type,genre)(studio,companies)都有着相近主题。编码器比较模型不能捕捉这些词的交互信息,因为编码后的表示只保留高级语义信息。我们构造了一个相似性矩阵,它的元表示问题词和关系词之间的相似性,将其看作是一个二维图像,利用卷积层来捕捉匹配特征。
相似性矩阵
构造一个相似性矩阵
M
M
M,其中每个元素表示
u
i
u_i
ui和
j
v
j_v
jv之间的相似度(
u
i
u_i
ui表示问题中的第
i
i
i个词嵌入,
j
v
j_v
jv表示关系中的第
j
j
j个词嵌入),则有:
M
i
j
=
u
i
⨂
v
j
M_{ij} = u_i \bigotimes v_j
Mij=ui⨂vj
这里的
⨂
\bigotimes
⨂表示余弦计算以得到相似度。与严格的文本匹配不同,这个矩阵可以捕捉到有着不同形式的词的相似度。
卷积层
一个典型的卷积核可以提取到不同级别的匹配模式,如词级别或者短语级别。更确切的说,第 k k k个核 w k w^k wk扫描整个相似性矩阵 M M M来生成特征映射 g k g^k gk:
g
i
,
j
k
=
σ
(
∑
s
=
0
r
k
−
1
∑
t
=
0
r
k
−
1
)
w
s
,
t
k
⋅
M
i
+
s
,
j
+
t
+
b
k
)
g_{i,j}^k = \sigma(\sum_{s=0}^{r_k -1}\sum_{t=0}^{r_k -1})w_{s,t}^k \cdot M_{i+s,j+t} +b^k)
gi,jk=σ(s=0∑rk−1t=0∑rk−1)ws,tk⋅Mi+s,j+t+bk)
其中
r
k
r_k
rk表示第
k
k
k个核的大小,这里我们使用square kernels,激活函数
σ
\sigma
σ为ReLU。
双向最大池化层
我们在特征映射 g k g^k gk的顶部使用两个不同的池核,它们的大小分别为 1 × d 1 1\times d_1 1×d1和 d 2 × 1 d_2 \times 1 d2×1,其中 d 1 d_1 d1表示矩阵的列数, d 2 d_2 d2表示矩阵的行数。
y
i
(
1
,
k
)
=
max
0
⩽
t
<
d
1
g
i
,
t
k
y_i^{(1,k)} =\max_{0\leqslant t<d_1}g_{i,t}^k
yi(1,k)=0⩽t<d1maxgi,tk
y
j
(
2
,
k
)
=
max
0
⩽
t
<
d
2
g
t
,
j
k
y_j^{(2,k)} =\max_{0\leqslant t<d_2}g_{t,j}^k
yj(2,k)=0⩽t<d2maxgt,jk
其关键思想是从问题和关系的角度保持最大匹配特征。即对于问题中的每一个词,从关系词中找到最大匹配得分;对于关系中的每一个词,从问题词找到最大匹配得分。这种方法优于使用平方池内核,因为在这个任务里我们更加强调单个单词的最大匹配分数。
全连接层
我们使用一个多层感知机来得到最终特征。对于两个池的结果,我们采用两层感知器。
z
3
=
W
2
σ
(
w
1
[
y
(
1
,
0
)
;
y
(
1
,
K
)
]
+
b
1
)
+
b
2
,
z_3 =W_2 \sigma(w_1[y^{(1,0)};y^{(1,K)}]+b_1)+b_2,
z3=W2σ(w1[y(1,0);y(1,K)]+b1)+b2,
z
4
=
W
2
σ
(
w
1
[
y
(
2
,
0
)
;
y
(
2
,
K
)
]
+
b
1
)
+
b
2
,
z_4 =W_2 \sigma(w_1[y^{(2,0)};y^{(2,K)}]+b_1)+b_2,
z4=W2σ(w1[y(2,0);y(2,K)]+b1)+b2,
其中
K
K
K表示核的总数,
[
y
(
i
,
0
)
;
y
(
i
,
K
)
]
[y^{(i,0)};y^{(i,K)}]
[y(i,0);y(i,K)]是
K
K
K个池化层的输出的级联,
W
i
W_i
Wi是第
i
i
i个MLP层的权重,
σ
\sigma
σ是ReLU激活函数。
组合
经过前面两个层面的匹配,我们得到了四个特征 ( z 1 , z 2 , z 3 , z 4 ) (z_1,z_2,z_3,z_4) (z1,z2,z3,z4), z 1 z_1 z1和 z 2 z_2 z2分别表示主体类型和关系的语义相关性, z 3 z_3 z3和 z 4 z_4 z4表示问题和关系的字面匹配程度。我们用一个线性层来分别学习它们对于全局匹配得分的贡献:
S
(
P
,
r
k
)
=
S
i
g
m
i
o
d
(
W
T
[
z
1
;
z
2
;
z
3
;
z
4
]
+
b
)
S(P,r^k) = Sigmiod(W^T[z_1;z_2;z_3;z_4]+b)
S(P,rk)=Sigmiod(WT[z1;z2;z3;z4]+b)
这个模型通过排序损失来进行训练,以最大化关系候选池R中的最优关系
r
+
r^+
r+和其它关系
r
−
r^-
r−之间的差距。
l
o
s
s
(
P
,
r
+
,
r
−
)
=
∑
(
P
,
r
+
)
∈
D
max
(
0
,
γ
+
S
(
P
,
r
−
)
−
S
(
P
,
r
+
)
)
loss(P,r^+,r^-) = \sum_{(P,r^+)\in D}\max(0,\gamma +S(P,r^-)-S(P,r^+))
loss(P,r+,r−)=(P,r+)∈D∑max(0,γ+S(P,r−)−S(P,r+))
其中
γ
\gamma
γ是一个常数。
4、代码勘误
注:论文作者所用pytorch版本0.2,我用的pytorch版本为0.4
1)文件relation_ranking/attention.py,76行,pack_seq函数。
def pack_seq(seq):
"""
:param seq: (batch_size, max_len, seq_size)
:return: (batch_size * max_len, seq_size)
"""
#return seq.view(seq.size(0) * seq.size(1), -1)
return seq.contiguous().view(seq.size(0) * seq.size(1), -1)
报错:RuntimeError: invalid argument 2: View size is not compatible with input tensor’s size and stride
在view前加上contiguous才可正确运行。
2)文件relation_ranking/model.py,171行,dynamic_pooling_index函数。
def dynamic_pooling_index(self, len1, len2, max_len1, max_len2):
def dpool_index_(batch_idx, len1_one, len2_one, max_len1, max_len2):
#stride1 = 1.0 * max_len1 / len1_one
#stride2 = 1.0 * max_len2 / len2_one
stride1 = 1.0 * max_len1 / len1_one.item()
stride2 = 1.0 * max_len2 / len2_one.item()
idx1_one = [int(i/stride1) for i in range(max_len1)]
idx2_one = [int(i/stride2) for i in range(max_len2)]
return idx1_one, idx2_one
batch_size = len(len1)
index1, index2 = [], []
for i in range(batch_size):
idx1_one, idx2_one = dpool_index_(i, len1[i], len2[i], max_len1, max_len2)
index1.append(idx1_one)
index2.append(idx2_one)
index1 = torch.LongTensor(index1)
index2 = torch.LongTensor(index2)
if self.config.cuda:
index1 = index1.cuda()
index2 = index2.cuda()
return Variable(index1), Variable(index2)
报错RuntimeError: reciprocal is not implemented for type torch.LongTensor
给len1_one添加.item()才可正确运行。
3)文件relation_ranking/predict.py,64行,rel_pruned函数。
def rel_pruned(neg_score, data):
neg_rel = data.cand_rel
#print (neg_score.size)
if neg_score.size==1:
#neg_score=[neg_score]
pred_rel_scores =(neg_rel[0],neg_score.item())
#print (pred_rel_scores)
else:
pred_rel_scores = sorted(zip(neg_rel, neg_score), key=lambda i:i[1], reverse=True)
pred_rel = pred_rel_scores[0][0]
pred_sub = []
for i, rels in enumerate(data.sub_rels):
if pred_rel in rels:
pred_sub.append((data.cand_sub[i], len(rels)))
pred_sub = [sub[0] for sub in sorted(pred_sub, key = lambda sub:sub[1], reverse=True)]
return pred_rel, pred_rel_scores, pred_sub
当neg_score的大小为1时,传入zip方法会报错,经过以上修改后才可以正确运行。