代码获取时间为2019.09.30
文档检索->句子检索->事实验证
特征提取(句子建模):基本思想是使用BERT模型[CLS]位置的隐向量作为输入序列的编码,获取evidence文本编码时,输入中也包含了claim,如下:
e
i
=
B
E
R
T
(
e
i
,
c
)
c
=
B
E
R
T
(
c
)
{\rm{e}}_i = BERT(e_i, c) \\ {\rm{c}} = BERT(c)
ei=BERT(ei,c)c=BERT(c)
相关代码:
# feature_extractor/extractor:87
#
# The convention in BERT is:
# (a) For sequence pairs:
# tokens: [CLS] is this jack ##son ##ville ? [SEP] no it is not . [SEP]
# type_ids: 0 0 0 0 0 0 0 0 1 1 1 1 1 1
# (b) For single sequences:
# tokens: [CLS] the dog is hairy . [SEP]
# type_ids: 0 0 0 0 0 0 0
通过设置不同标识来区分单句还是双句,之后将tokens转换为bert中的id,并通过1-0 padding得到mask,并丢给Bert预训练模型获取句子表示(特征向量),字典形式保存到文件
这部分了解到的新内容:
模型并行:torch.nn.parallel.DistributedDataParallel,torch.nn.DataParallel(后者是简单的单机多卡,数据并行?)
数据传递:TensorDataset,沿第一个维度取各个传入tensor的数据
特征加载:主要是对于候选证据个数做了限制,默认阈值为5。不足5条时,做zero-padding
模型:分为推理和整合两个阶段,推理是self-attention,整合是采用claim-aware的attention或者max-pooling或mean—pooling
推理中的每一步都是一个全连接attention,每一步即stacking的一层,论文中结果表明,nlayer=2时实验效果略好。由于每层输入输出尺寸都相同(获取的句子表示tensor shape都一样),因此forward的时候,循环实现步进
# gear/models.py:58
self.attentions = [AttentionLayer(nins, nfeat) for _ in range(nlayer)]
# gear/models.py:76
# 这里input是每个数据组中evidence的表示,claim是单独传入的
for i in range(self.nlayer):
inputs = self.attentions[i](inputs)
对于一个数据实例,其包含nins个证据表示,在特定某层推理时,这nins个向量,都要和全体组内向量做attention操作。SelfAttentionLayer输入向量长度应当为nhid * 2,因为论文中进行权重分数计算的公式中,首先对attention的query和key进行了concat,没有使用Dot、general两种方法
p
i
j
=
W
1
t
−
1
(
R
e
L
U
(
W
0
t
−
1
(
h
i
t
−
1
∣
∣
h
j
t
−
1
)
)
)
p_{ij} = {\rm{W}_{1}^{t-1}}({\rm{ReLU}}({\rm{W}_{0}^{t-1}}({\rm{h}_{i}^{t-1}}||{\rm{h}_{j}^{t-1}})))
pij=W1t−1(ReLU(W0t−1(hit−1∣∣hjt−1)))
h
i
{\rm{h_i}}
hi、
h
j
{\rm{h_j}}
hj、
p
i
j
p_{ij}
pij都是列向量
# gear/models.py:8
# self.project对应到上面的公式:线性层、ReLU、线性层,对应矩阵运算
class SelfAttentionLayer(nn.Module):
def __init__(self, nhid, nins):
super(SelfAttentionLayer, self).__init__()
self.nhid = nhid
self.nins = nins
self.project = nn.Sequential(
Linear(nhid, 64),
ReLU(True),
Linear(64, 1)
)
# own.repeat是为了下一步的concat操作
# attention.squeeze 将多个单独的权重值降维成为一个weight向量,又升维和inputs相乘
# 得到input中每个向量的加权贡献,求和得到own的attention表示
def forward(self, inputs, index, claims):
tmp = None
if index > -1:
idx = torch.LongTensor([index]).cuda()
own = torch.index_select(inputs, 1, idx)
own = own.repeat(1, self.nins, 1)
tmp = torch.cat((own, inputs), 2)
else:
claims = claims.unsqueeze(1)
claims = claims.repeat(1, self.nins, 1)
tmp = torch.cat((claims, inputs), 2)
# before
attention = self.project(tmp)
weights = F.softmax(attention.squeeze(-1), dim=1)
outputs = (inputs * weights.unsqueeze(-1)).sum(dim=1)
return outputs
# gear/models.py:41
# 对每一个query,声明一个attention结构
self.attentions = [SelfAttentionLayer(nhid=nhid * 2, nins=nins) for _ in range(nins)]
# gear/models.py:48
# 所有query的attention
outputs = torch.cat([self.attentions[i](inputs, i, None) for i in range(self.nins)], dim=1)
outputs = outputs.view(inputs.shape)
推理输入输出shape一致,输出的向量是进行过若干层信息传播的句子编码,用claim当query,使用self-attention,得到一个状态向量(或这里选择不再整合claim信息,直接max-pooling、mean-pooling)
# gear/models:65
self.aggregate = SelfAttentionLayer(nfeat * 2, nins)
# gear/models:80
inputs = self.aggregate(inputs, -1, claims)
这部分了解到的新内容:
需要掩码操作的时候,torch.index_select可以选择性地挑出需要的某几个维度,一般维度0是batch选择,维度1就是组内数据选择:torch.index_select(batch, 1, some_indices)