文本相关性问题就是判断两个文本之间的相关程度,也是搜广推中的重点问题之一。本篇文章主要粗略总结这方面的一些实践经验,包括特征、模型以及部署上需要注意的问题,都是本人在工作中积累的实际经验,非综述类文章,只总结用过的。
总体包括特征和模型两个方面,特征一般指类似 BM25、tf-idf 之类的统计特征,模型则是指各类双塔、交互深度学习模型。
——————————————————————————————————————————————
词频(TF),即一个term在一个文档中出现的次数,一般来说,在某个文档中反复出现的term,往往能够表征文档的主题信息,即TF值越大,越能代表文档所反映的内容。
TF的计算方式如下:
IDF代表的是文档集合范围内term的相对重要性,公式如下:
I
D
F
k
=
log
N
n
k
I D F_{k}=\log \frac{N}{n_{k}}
IDFk=lognkN 其中的
N
N
N 代表文档集合中总共有多少个文档,而
n
k
n_k
nk 代表特征
t
e
r
m
k
term k
termk 在其中多少个文档中出现过,即文档频率。IDF 就是衡量不同 term 对文档的区分能力的,其值越高,则其区分不同文档差异的能力越强,反之则区分能力越弱。
考虑到一些特殊情况可能导致分母为 0 等问题,一般会对 IDF 进行平滑处理,平滑的方式有很多,一种最常见的平滑后的 IDF 公式为:
IDF
(
x
)
=
log
N
+
1
N
(
x
)
+
1
+
1
\operatorname{IDF}(x)=\log \frac{N+1}{N(x)+1}+1
IDF(x)=logN(x)+1N+1+1
这两个特征是最基础也是最常用的,一般来说以维护一个词表的形式来应用,线上服务通过将词表加载成 key-value 的形式来直接获取词的 tf 和 idf
BM25 是一种常见用来做相关度打分的公式,思路比较简单,就是计算一个 query 里面所有 term 和文档的相关度,然后在把分数做加权求和, 而每个词的相关度分数受到 tf/idf 的影响。公式如下:
score
(
Q
,
d
)
=
∑
t
∈
Q
W
t
R
(
t
,
d
)
\operatorname{score}(Q, d)=\sum_{t \in Q} W_{t} R(t, d)
score(Q,d)=t∈Q∑WtR(t,d)其中:
R
(
t
,
d
)
R(t,d)
R(t,d) 是每个词和文档的相关度值;
t
t
t 代表 query 中的 term;
d
d
d 代表相关的文档;
W
t
W_t
Wt 是词
t
t
t 的权重,可由外部设置,默认是 IDF 值。
下面是
R
(
t
,
d
)
R(t,d)
R(t,d) 的公式:
R
(
t
,
d
)
=
t
f
(
t
)
(
k
1
+
1
)
t
f
(
t
)
+
K
(
d
)
q
f
(
t
)
(
k
2
+
1
)
q
f
(
t
)
+
k
2
R(t, d)=\frac{t f(t)\left(k_{1}+1\right)}{t f(t)+K(d)} \frac{q f(t)\left(k_{2}+1\right)}{q f(t)+k_{2}}
R(t,d)=tf(t)+K(d)tf(t)(k1+1)qf(t)+k2qf(t)(k2+1)
K
(
d
)
=
k
1
(
1
−
b
+
b
d
l
a
v
g
d
l
)
K(d)=k_{1}\left(1-b+b \frac{d l}{a v g d l}\right)
K(d)=k1(1−b+bavgdldl)其中:
k
1
k1
k1,
k
2
k2
k2,
b
b
b 都是调节因子,一般
k
1
=
2
k1=2
k1=2,
k
2
=
1
k2=1
k2=1,
b
=
0.75
b=0.75
b=0.75;
t
f
(
t
)
tf(t)
tf(t) 是词 t 在文档中的次数,
q
f
(
t
)
qf(t)
qf(t) 代表词在查询句里的次数;
d
l
dl
dl 是文档长度,
a
v
g
d
l
avgdl
avgdl 是文档平均长度;
乘积的左边因数代表词在文档中的次数关系,乘积的右边因数代表词在查询语句中的次数关系。绝大多数情况下,查询词在查询语句里面出现一次,所以
q
f
(
t
)
qf(t)
qf(t) 可以看成是 1,又因为
k
2
k2
k2 为 1,所以右边因数其实就等于 1。最终公式可化简为下面这样:
score
(
Q
,
d
)
=
∑
t
∈
Q
IDF
(
t
)
t
f
(
t
)
(
k
1
+
1
)
t
f
(
t
)
+
k
1
(
1
−
b
+
b
d
l
avgdl
)
\operatorname{score}(Q, d)=\sum_{t \in Q} \operatorname{IDF}(t) \frac{t f(t)\left(k_{1}+1\right)}{t f(t)+k_{1}\left(1-b+b \frac{d l}{\text { avgdl }}\right)}
score(Q,d)=t∈Q∑IDF(t)tf(t)+k1(1−b+b avgdl dl)tf(t)(k1+1)
从最终公式中可以看出:
一般情况下,一篇文章是分为多个部分的,如 title,content,description,anchor 等,在 BM25F 算分公式中,这些部分被称为域(Field)。对于两篇,一篇文章的 title 部分与 query 的 BM25 相关性得分为 a, 另一篇文章 content 部分与 query 的 BM25 相关性得分也为 a。假设不考虑这两篇文章其他部分与 query 的相关性情况,根据经验,一般第一篇文章应该应该比第一篇文章更相关, 因为一般 title 是比 content 更重要的。BM25F 引入了文章的每个域的信息,它将每个 term 在文章中的每个域中的相关性进行了处理。公式如下:
score
(
Q
,
d
)
=
∑
t
∈
Q
IDF
(
t
)
w
(
t
,
d
)
k
1
+
w
(
t
,
d
)
\operatorname{score}(Q, d)=\sum_{t \in Q} \operatorname{IDF}(t) \frac{w(t, d)}{k_{1}+w(t, d)}
score(Q,d)=t∈Q∑IDF(t)k1+w(t,d)w(t,d)
w
(
t
,
d
)
=
∑
f
∈
d
t
f
(
t
,
f
,
d
)
×
boost
f
1
−
b
f
+
b
f
len
(
f
,
d
)
avglen(f)
w(t, d)=\sum_{f \in d} \frac{t f(t, f, d) \times \text { boost }_{f}}{1-b_{f}+b_{f} \frac{\operatorname{len}(f, d)}{\text { avglen(f) }}}
w(t,d)=f∈d∑1−bf+bf avglen(f) len(f,d)tf(t,f,d)× boost f
大部分变种原理上差不多,细微处改变,这里不赘述,贴一些论文链接
【1】OkaTP:Y. Rasolofo and J. Savoy. Term Proximity Scoring for Keyword-Based Retrieval Systems. In Proceedings of the 25th European Conference on IR Research (ECIR 2003) pages 207–218, April 2003
【2】BM25TP: S. B uttcher, C. L. A. Clarke, and B. Lushman. Term proximity scoring for ad-hoc retrieval on very large text collections. In Proc. SIGIR, pages 621- 622, 2006.
【3】newTP:Song R , Taylor M J , Wen J R , et al. Viewing Term Proximity from a Different Perspective[C]// European Conference on Information Retrieval. Springer, Berlin, Heidelberg, 2008.
BM25 类的特征一般在 rank 部分使用,注意事项就是在前期要把分词信息带过来,到用的时候即时分词服务的耗时肯定起飞。
再一个是相比直接把分词信息的文本信息裸的带过来,设计一些数据结构把分词信息的词频等提前计算好带过来会更好,这些都可以在前期服务中找地方异步完成。
即 cover query ratio 和 cover title ratio,两个覆盖率特征,设 Q 代表 query,T 代表 title,cqr 和 ctr 的计算公式如下:
s
i
m
c
q
r
=
∣
Q
∩
T
∣
∣
Q
∣
sim_{cqr} = \frac{|Q \cap T|}{|Q|}
simcqr=∣Q∣∣Q∩T∣
s
i
m
c
t
r
=
∣
Q
∩
T
∣
∣
T
∣
sim_{ctr} = \frac{|Q \cap T|}{|T|}
simctr=∣T∣∣Q∩T∣
线上直接计算即可
SimRank 是一种基于图的推荐算法。思想与协同过滤算法相似:
将点击日志构成二部图,通过二部图进行向量传播,收敛之后,query 和 doc (类比 item)将在同一空间上生成词向量,如此一来,就可以通过相似度的计算方式得到文本相关性。对于未曾有过点击行为的 query 和 doc,也可以进行该空间词向量的估计。
假设语料库长度为 V V V ,则 Query 构成的矩阵为: ∣ Q u e r y ∣ ∗ V |Query|*V ∣Query∣∗V,Doc 构成的矩阵为 ∣ D o c ∣ ∗ V |Doc|*V ∣Doc∣∗V ,我们的目标是计算这两个矩阵。这里的语料指的就是Query 和 Doc 共处的向量空间,即由 Query 分词后的 term,或者 Doc 中的title/content 分词后的 term 集合构建。
若从 Query 侧开始迭代,将 Query 向量初始化为
Q
i
0
Q^0_i
Qi0,0 表示第一次迭代,迭代公式如下:
D
j
(
n
)
=
1
∥
∑
i
=
1
∣
Query
∣
C
i
,
j
⋅
Q
i
(
n
−
1
)
∥
2
∑
i
=
1
∣
Query
∣
C
i
,
j
⋅
Q
i
(
n
−
1
)
D_{j}^{(n)}=\frac{1}{\left\|\sum_{i=1}^{\mid \text {Query } \mid} C_{i, j} \cdot Q_{i}^{(n-1)}\right\|_{2}} \sum_{i=1}^{\mid \text {Query } \mid} C_{i, j} \cdot Q_{i}^{(n-1)}
Dj(n)=∥
∥∑i=1∣Query ∣Ci,j⋅Qi(n−1)∥
∥21i=1∑∣Query ∣Ci,j⋅Qi(n−1)
Q
i
(
n
)
=
1
∥
∑
j
∣
D
o
c
∣
C
i
,
j
⋅
D
j
(
n
)
∥
2
∑
j
=
1
∣
D
o
c
∣
C
i
,
j
⋅
D
i
(
n
)
Q_{i}^{(n)}=\frac{1}{\left\|\sum_{j}^{|\mathcal{D} o c|} C_{i, j} \cdot D_{j}^{(n)}\right\|_{2}} \sum_{j=1}^{|\mathcal{D o c}|} C_{i, j} \cdot D_{i}^{(n)}
Qi(n)=∥
∥∑j∣Doc∣Ci,j⋅Dj(n)∥
∥21j=1∑∣Doc∣Ci,j⋅Di(n)若从 Doc 侧开始迭代也是类似的。
这类特征在 bert 这种大型预训练模型出现后意义不大了,但是由于历史原因还是存在于一些搜索系统中。
tips:
计算过程均可在离线完成,结果可以以 key-value 的形式放入 redis,线上即用即取。
这个名字是我乱起的,原理是搜到过(曝光过)这个 content 的所有历史 query,与当前 query 做相似度,方法为计算 bm25。
上文提到的历史 query,可以通过离线挖掘的方式挖出来放到 redis(或将其分词结果放入 redis),点击率等信息也可以随着放入。
线上服务用的时候请求 redis,拿到这些 query 利用上文提到过的 bm25 公式计算即可,其点击率可以替换 bm25 中的权重。
———————————————————————————————————————————————
模型类特征现在一般分为两类,表示型和交互型。在 bert 出现之前,存在 dssm 之类的模型,但现在一般都使用 bert,本人的经验也是利用 bert 的经验,所以以下以 bert 的视角阐述。
表示模型一般是双塔,即两个共享参数的 bert,一座用来编码 query,一座用来编码 content/title,或者可以理解为一个 bert 按顺序先后编码了 query 和 content/title。输出的 embedding 直接拿 cls 或者利用每个 token 进行 pooling 差别不大,query 和 content/title 的 embedding 可以直接做点积或者上层加一些 dense 层。
标注数据一般是人工标注的相关性程度,量化到具体的几档,loss 一般为 pointwise/pairwise。
content/title 训练所得 embedding 直接放 redis 当 cache,线上只需要实时推理 query 的 embedding,热门query 集的 embedding 也可以做一些相应的 cache 策略。
表示模型一般 12 层的 bert-base 较为合适,6 层效果不好,24 层提升不大。12 层 bert 也可以利用 24 层蒸馏得到,直接蒸馏最后的结果就行,同时蒸隐层收益不大。输入长度最长不要超过 128,否则线上压力比较大。
bert 上线一定要 GPU 机器,否则 CPU 推理不仅 QPS 上来扛不住,单条样本推理的耗时也非常高。bert 模型本身比较吃内存,建议单独成一个独立的推理服务,方便管理。
交互模型即将样本拼成 query | title 的样子直接一行句子输入 bert,末端直接拿 cls 的 embedding 过若干 dense 得到相关性分数。由于 bert 输入长度的限制,一般只用于 query 和 title 的相关性。
交互模型一般是搜索相关性特征中最强的一个单体特征,值得好好做这个模型。
交互模型需要推理每一个 query 和候选doc title 的拼接体,所以推理量一定是比表示模型大的,输入长度一般限制在 50 以下,层数一般用 6 层 bert,否则线上压力极大。
由于 6 层 bert 参数量相对少,这里一般是通过训练 24 层 bert 然后蒸馏给 6 层 bert。初始化方式直接把 24 层 bert 的前 6 层参数拿过来初始化,亲测跳着层初始化效果很差。。。蒸馏也是直接蒸最后的结果,软硬 label 的权重实验中调整,蒸隐层亲测收益不大(甚至毫无收益
一定要用 GPU 机器,CPU 完全没有机会,一般交互模型的 QPS 还是不小的。