该模型架构在Poly-Encoders的基础上做了细微改动以适配搜索场景。Poly-Encoders是Facebook AI 2020年发表在ICLR上的一篇论文,主要在双塔模型的基础上增加了一个Ploy-Attention结构。在介绍文本的模型之前,先简要介绍下Bi-encoder、Cross-encoder、Poly-encoder这三种结构。
Bi-encoder其实就是我们常说的双塔模型,左右分别输入Q和D的token_ids,分别经过两个Transformer-base的Encoder,再得到每个token的representation,再使用某种聚合方式(例如:Max-pooling\Mean-pooling)分别得到Q和D的representation(Ctxt Emb和Cand Emb),最后输入两个representation计算得到Score(通常使用内积、余弦相似度等来计算相关性得分)。
Bi-encoder有一个弱势在于需要两个Encoder,并且在计算representation时Q和D是独立的,之间没有交互。Cross-encoder就很好地解决了这个问题,将Q和D同时输入一个Encoder,再聚合得到一个representation(Cand Emb),后面再接入全连接神经网络计算得到Score。这种模式就像BERT里面的句子对分类。
Poly-encoder综合了Bi-encoder和Cross-encoder的特性,既采用双塔结构,也增加了两个Encoder之间的交互。具体交互由Poly-Encoder模块完成。Doc通过Candidate Encoder
编码和 Candidate Aggregator
之后得到 Cand emb
y
c
a
n
d
i
y_{cand_{i}}
ycandi,Query通过 Content Encoder
之后得到
[
O
u
t
x
1
,
O
u
t
x
2
,
.
.
.
,
O
u
t
x
N
x
]
=
[
h
1
,
h
2
,
.
.
.
,
h
N
x
]
[Out_{x} 1, Out_{x} 2, ..., Out_{x} N_{x}] = [h_{1},h_{2},..., h_{N_{x}}]
[Outx1,Outx2,...,OutxNx]=[h1,h2,...,hNx] 。另外,设置
m
m
m 个可学习的向量(context codes)
[
c
1
,
c
2
,
.
.
.
,
c
m
]
[c_{1}, c_{2}, ..., c_{m}]
[c1,c2,...,cm]作为Attention中的Q,用于计算得到
m
m
m 个全局特征向量
[
y
c
t
x
t
1
,
.
.
.
,
y
c
t
x
t
m
]
[y_{ctxt}^{1},...,y_{ctxt}^{m}]
[yctxt1,...,yctxtm],其具体计算方式如下:
y
c
t
x
t
i
=
∑
j
w
j
c
i
h
j
where
(
w
1
c
i
,
.
,
w
N
x
c
i
)
=
softmax
(
c
i
⋅
h
1
,
.
.
,
c
i
⋅
h
N
x
)
y_{c t x t}^{i}=\sum_{j} w_{j}^{c_{i}} h_{j} \quad \text { where } \quad\left(w_{1}^{c_{i}}, ., w_{N_{x}}^{c_{i}}\right)=\operatorname{softmax}\left(c_{i} \cdot h_{1}, . ., c_{i} \cdot h_{N_{x}}\right)
yctxti=j∑wjcihj where (w1ci,.,wNxci)=softmax(ci⋅h1,..,ci⋅hNx)
为了得到最终的context的representation,使用Cand emb
作为Attention中的Q,来整合上述
m
m
m 个全局特征向量,计算公式如下:
y
c
t
x
t
=
∑
i
w
i
y
c
t
x
t
i
Where
(
w
1
,
.
.
,
w
m
)
=
softmax
(
y
c
a
n
d
i
⋅
y
c
t
x
t
1
,
.
.
,
y
c
a
n
d
i
⋅
y
c
t
x
t
m
)
y_{c t x t}=\sum_{i} w_{i} y_{c t x t}^{i} \quad \text { Where } \quad\left(w_{1}, . ., w_{m}\right)=\operatorname{softmax}\left(y_{c a n d_{i}} \cdot y_{c t x t}^{1}, . ., y_{c a n d_{i}} \cdot y_{c t x t}^{m}\right)
yctxt=i∑wiyctxti Where (w1,..,wm)=softmax(ycandi⋅yctxt1,..,ycandi⋅yctxtm)
最后,该Query和Doc的最终分数是
y
c
t
x
t
⋅
y
c
a
n
d
i
y_{c t x t}·y_{cand_{i}}
yctxt⋅ycandi,如Bi-encoder中一样。当
m
<
N
m
接下来,我们回到本文的模型结构,如下图:
对比之后可以发现,Cand emb
直接使用 [CLS]
token的representation
C
′
C^{'}
C′,训练和推理阶段的模型结构稍有不同。训练阶段,直接取
C
′
C^{'}
C′和
P
1
,
.
.
.
,
P
m
P_{1},...,P_{m}
P1,...,Pm 内积的最大值;而推理阶段,因为搜索引擎要提前保留Query的特征向量,需要提前计算保存,因此直接对
P
1
,
.
.
.
,
P
m
P_{1},...,P_{m}
P1,...,Pm 求平均池化。
随机初始化权重,使用万亿规模的数据(来自中文维基百科、百度新闻、百度百科、百度贴吧)来预训练ERNIE,使用MLM和NSP任务。
采用阶段一的权重,使用万亿规模的搜索日志,采用实体、短语级别的MLM和NSP任务,Doc是否点击作为NSP的label。
采用阶段二的权重,使用万亿规模的搜索日志(含点击和未点击的Doc),结合本文的模型结构进行微调。
采用阶段三的权重,使用小规模的更加准确且无偏的人工标注样本(每个Q-D对的得分介于0-4之间),结合本文的模型结构进行微调。
这种多阶段预训练和微调模式确实能在很多场景发挥作用,曾在一个NER竞赛中采用了类似的方法,能带来提升。
原理就是采用一个全连接神经网络对representation进行降维。
每个embedding中的值使用float32保存,可以通过转换将其变为uint8,虽然对精度可能有些损害,但大大节省了存储空间。实现细节如下,首先根据大规模验证数据集上的输出embedding,我们计算输出embedding的第
i
i
i 维的数据范围为
(
s
i
m
i
n
,
s
i
m
a
x
)
(s_{i}^{min}, s_{i}^{max})
(simin,simax),然后将该数据范围划分为
L
=
255
L=255
L=255 份,每一份都是等间隔
Q
i
=
(
s
i
m
a
x
−
s
i
m
i
n
)
/
L
Q_{i} = (s_{i}^{max} - s_{i}^{min})/L
Qi=(simax−simin)/L。对于输出embedding上第
i
i
i 维的值
r
i
r_{i}
ri,其量化后的索引为:
Q
I
i
(
r
i
)
=
⌊
(
r
i
−
s
i
min
)
/
Q
i
⌋
Q I_{i}\left(r_{i}\right)=\left\lfloor\left(r_{i}-s_{i}^{\min }\right) / Q_{i}\right\rfloor
QIi(ri)=⌊(ri−simin)/Qi⌋
该索引的范围是 [0, 255],可以使用8位整型表示。当线上推理时,可以还原其量化前的值为
r
i
~
\tilde{r_{i}}
ri~:
r
i
~
=
Q
I
i
(
r
i
)
∗
Q
i
+
Q
i
/
2
+
s
i
min
\tilde{r_{i}}=Q I_{i}\left(r_{i}\right) * Q_{i}+Q_{i} / 2+s_{i}^{\min }
ri~=QIi(ri)∗Qi+Qi/2+simin
上图分为两部分,先说左边部分;左边是离线数据库和索引的构建,就是事先计算好每个Doc title的representation,然后存入embedding数据库和建立ANN索引,便于后续使用;右图在早期只有Term匹配的workflow上增加了本文提出的ERNIE提升的workflow,主要在两个地方进行扩充,一是通过Embedding ANN召回了更多相关的Doc,二是在后检索过滤时引入了ERNIE相关性得分这个特征。
def model_encode_query(tokens):
all_embeds = ERNIE_encoder.get_all_outputs(tokens)
poly_embeds = poly_attention(all_embeds, context_codes)
return [fc_compression(poly_embeds[i]) for i in range(m)]
def model_encode_doc(tokens):
cls_embed = ERNIE_encoder.get_cls_output(tokens)
return fc_compression(cls_embed)
def train_interaction(q, pos, neg):
all_logits1, all_logits2 = [], []
for i in range(m):
# in-batch random negative sampling via matrix multiplication
pos_logits_with_rand_neg = matmul(q[i], pos.T)
neg_logits_with_rand_neg = matmul(q[i], neg.T)
all_logits1.append(pos_logits_with_rand_neg)
all_logits2.append(neg_logits_with_rand_neg)
max_logits1 = reduce_max(all_logits1)
max_logits2 = reduce_max(all_logits2)
final_logits = concat(max_logits1, max_logits2)
loss = softmax_with_cross_entropy(final_logits)
return loss
def predict_interaction(q, d):
avg_q = reduce_mean(q)
return reduce_sum(avg_q * d)
def train(query_tokens, pos_doc_tokens, neg_doc_tokens):
query_embed = model.encode_query(query_tokens)
pos_doc_embed = model_encode_doc(pos_doc_tokens)
neg_doc_embed = model_encode_doc(neg_doc_tokens)
loss = train_interaction(query_embed, pos_doc_embed, neg_doc_embed)
apply_optimization(loss)
def predict(query_tokens, doc_tokens):
query_embed = model_encode_query(query_tokens)
doc_embed = model_encode_doc(doc_tokens)
score = predict_interaction(query_embed, doc_embed)
return score
本文对论文中提到的Query和Doc相关性模型、四阶段训练范式、Embedding压缩和量化、基于ERNIE提升的检索系统workflow展开了介绍,负采样方法以及损失函数和对比学习方法中常用的比较类似,本文就没展开介绍,感兴趣的可以参考论文《SimCSE: Simple Contrastive Learning of Sentence Embeddings》。总之,本文立足于百度搜索引擎,很具有实践价值。