推荐算法实战-8-粗排

 

本文是推荐算法实战系列第八篇文章。

前面文章包括:

  1. 推荐系统简介
  2. 特征工程
  3. embedding
  4. 精排
  5. 召回(1):传统召回以及召回中的loss设计
  6. 召回(2):word2vec召回、FM召回和双塔召回
  7. 召回(3):图卷积

如果说召回和精排是推荐系统的两朵红花,那么粗排和重排就是两片绿叶。

粗排在大型推荐系统中,召回之后进一步过滤候选集,减少精排模型的打分数量,节约精排模型的线上时延。

重排模型是为了解决相似内容扎堆现象。

粗排环节并不是必须的,视候选集大小而定。重排环节通常是必要的,只不过在小型推荐系统中,基于规则的重排(e.g.打散)策略就够用了,不必上模型。

粗排的设计是“速度”与“精度”的又一次折衷

  • 与召回相比,粗排的候选集小很多(百万量级vs万级),所以粗排模型可以比召回模型更复杂。
  • 与精排相比,粗排的候选集大了一个数量级(10K vs K),所以粗排模型不能像精排那么复杂。

下面从模型结构、目标函数、样本空间等方面介绍粗排模型

1、模型结构:双塔仍是主力

1.1、粗排双塔与召回双塔的异同

双塔模型前面已经介绍,这里主要介绍粗排双塔和召回双塔的异同:

相同点

  • 训练时,用户塔、物料塔隔离,解耦。不允许使用target attention结构和“用户-物料”lookup交叉特征。
  • 部署时,物料向量离线生成并缓存,每个物料生成一个向量,不同用户复用。用户向量线上实时生成,每来一次用户请求,生成一次。个性化完全靠用户向量体现,物料向量没有个性化。
  • 正样本样本选择上,都可以用“用户点击过的物料”作正样本。

不同之处包括四个方面:物料向量存储方式、(负)样本选择、损失函数设计、用户向量与物料向量的交互方式等

  • 物料向量存储方式

召回时,候选级百万、千万量级,拿用户向量逐一与它们计算相似性不可行。所以,需要将物料向量存入向量数据库,线上通过近似近邻搜索(ANN)快速查找,无需遍历整个候选集调用一次DNN推理,K次近邻查找(K«N,N = 百万级)。

粗排时,候选集万级,无需通过ANN近似查找。将物料向量按照 “itemid-embedding”缓存后,线上请求时,遍历粗排候选集,逐一从缓存中取出物料向量,与用户向量通过点积运算计算粗排得分。调用一次DNN推理,计算N次点积。(N:~10000)

  • 样本选择

负样本选择上,召回不能只拿曝光未点击作负样本,必须使用随机采样作主力负样本。粗排可以和精排一样,拿曝光未点击作负样本。当然,与召回一样,这里存在样本选择偏差。如何纠正偏差是粗排优化的一大方向

  • 损失函数设计

召回双塔模型通常使用in-batch sampled softmax loss。输入的一个batch只有正样本。loss的目标是使ti被ui选中的概率最大。负样本上的预测值$E_{ui} \cdot E_{tj}$只出现在计算正样本概率的分母上,未直接加入负样本label的约束。

\[\frac{exp(E_{ui} \cdot E_{ti})}{exp(E_{ui} \cdot E_{ti}) + \sum_{j \in B, j \neq i}exp(E_{ui} \cdot E_{tj})}\]

粗排的样本由(ui, ti, yi)组成,其中yi=0或1,代表来自用户的真实反馈。因此,与召回双塔的损失函数只关心正样本不同,粗排双塔中的正负样本都需要被用户真实反馈y约束。粗排双塔中的loss就是每个样本上的BCE,二元交叉熵。另外,粗排中ui只需要与ti做点积,不需要与batch内其余物料做点积。 (备注:双塔模型中,粗排的输出logit是用户向量和物料向量的点积,并不是概率形式,如何计算BCE loss?)

可见,粗排的样本构造和损失函数与精排更相似。

  • 用户向量与物品向量交互

召回线上预测时,需要拿用户向量到向量数据库做ANN查找。受ANN限制,召回双塔中的用户向量与物料向量只能用点积来实现交叉(cosine也可以转化成点积)。

粗排双塔不需要ANN查找,所以没有这个限制。粗排的主流做法仍然是用向量点积来表示用户-物料匹配度,但是,粗排也可以将这两个向量继续喂入DNN实现更复杂的交叉。只是,这个DNN要简单、轻量,以保证粗排能够实时处理较大规模的候选集。

1.2、双塔改进的技术路线

双塔模型的最大缺点在于,用户侧信息与物料侧信息交叉太晚,只有得到用户向量和物料向量后才能交叉。但是此时,用户信息和物料信息已经高度浓缩,一些细粒度信息在塔中自底向上传递时损耗了,失去了与对侧塔交互的机会。

所以,改进双塔的一条技术主线就是:如何保留更多的信息在每个塔的最终输出向量中,从而有机会和对侧塔交叉

业界围绕这一路线做了若干优化方案。它们同样适用于召回双塔。

1.3、双塔重地,闲人免进

这一思路认为,要想在双塔最终的输出向量中保留更多有用的信息,应该在源头减少噪音的输入。

基于此,借鉴CV领域的SENet(Squeeze-and-Excitation Network),在每个塔的输入embedding后、塔身DNN之前,接一个SENet,对每个特征filed做动态调权,有选择性地放大、抑制某些信息,实现过滤噪音的效果。

第一步,Squeeze。将每个特征field embedding经过压缩函数$F_{sq}$压缩成一个标量数字,代表这个field的信息。

  • 输入:f * d,f个embedding向量。

  • 输出:f * 1,f个标量。记作Z向量。

  • $F_{sq}$:Pooling函数,比如average pooling。

第二步,Excitation。将上一步的Z向量,经过一个小网络$F_{ex}$,例如两层MLP,得到f * 1 的向量A:

\[A = F_{ex}(Z) = \sigma_2(W_2 \sigma_1(W_1Z))\]
  • A:f * 1。代表原始f个特征field的重要性。它与静态特征重要性不一样,受当前样本的特征值影响。不同样本的不同特征的重要性是动态变化的,所以称为动态调权
  • 第一层FC:输出宽度为r,通常与f不同,让Z的各元素之间发生交叉,建模各field之间的依赖关系备注:为什么交叉就能建模依赖关系,能建模何种依赖关系,不能建模何种依赖关系?)
  • 第二层FC:输出宽度重新变回f。

第三步,reweight。将权重向量A与原始特征field embedding相乘,得到f个新的field embedding,再接入上层DNN网络。

  • 如果 $\sigma_2(x) = 2 \times sigmoid(x)$,A[i] > 1表示第i个field重要,应该提权;A[i] < 1表示第i个field不重要,应该降权
  • 如果 $\sigma_2(x)$采用ReLU函数,对于不重要的特征,直接压制成0,相当于过滤噪声。

这种范式的核心是拿到各输入特征的动态重要性,SENet并非唯一方式。

1.4、重要信息,一步登顶

每个塔的输入包含了各种细粒度信息,可能有成千上万维,但是输入向量通常只有128维。

信息在塔中自底向上流动过程逐渐压缩,不可避免带来损耗。何不让重要信息走捷径,直接送到离塔顶更近的位置,从而防止丢失?

提到“抄近路”,很自然想到ResNet结构

\[x_1 = f(x_0) + x_0\]

x1中既包含了经过DNN高度浓缩的信息,也包含了原始细粒度信息。

接下来的问题是,从哪里开始抄近路,抄到哪里? 有两种方式。

第一种,把每个中间层的输出都抄近路送到最后一层,并concat成一个大向量。再经过一层FC压缩成双塔的输出用户、物料向量。

第二种,将重要信息不仅接入最底层FC,还接入各个上层FC,这样重要信息不会在中间环节损耗。这种方式导致每一层的输入维度增加。设计时需要考虑哪些信息值得抄近路。

通常需要关注那些最能体现 “个性化”的特征(例如UserId、ItemId),以及体现人群、物群等群体差异性的特征(例如,新老客、作品语言等)。

1.5、条条大路通塔顶

这一思路有两个出发点:

  • 与SENet类似:为了避免各种信息沿着塔身向上流动时互相干扰,SENet靠“堵”:在输入塔之前,过滤掉噪音信息。另外一个思路是“疏”:不同信息沿着不同塔向上流动,最后每个小塔的embedding在塔顶聚合成User、物料embedding,再与对侧塔点积交叉
  • 与抄近路思路类似:不再迷信DNN的拟合能力,即使是相同的信息,也可以沿着不同的网络结构信息通道向上流动。最后各通道的embedding聚合成一个向量。(备注,与思路一类似,都是先得到多个embedding再聚合)

注意,不管网络结构怎么改变,最后塔顶必须保留两个embedding向量内积的形式

沿着这一思路,第一个优化点是天猫提出的“双塔+FM粗排模型”。

其主体结构仍然是DNN双塔。分别得到用户向量和物料向量,再点积得到logit: \(logit_{tower} = UE_{tower} \cdot TE_{tower}\)

用户点击过的ItemId序列 $S=[ht_1,ht_1…,ht_N]$,与当前候选物料的ItemId “ct”之间的交叉是超级重要的特征组合。

但是在传统DNN双塔网络中,它们无法直接交叉,只能融入最终的embedding向量点积交叉。

为了确保序列S与候选ct之间的直接交叉,增加FM来辅助双塔的学习

\(\begin{aligned} logit_{FM} &= E_{ht1} \cdot E_{ct} + ... + E_{htN} \cdot E_{ct} \\ &=(\sum_{i=1}^N E_{hti}) \cdot E_{ct} \\ &= UE_{FM} \cdot TE_{FM} \end{aligned}\) 最后,粗排的打分由双塔logit和FM logit线性组合而成,其中w为待学习的权重: \(logit = w_{tower} logit_{tower} + w_{FM} logit_{FM}\) 为了方便线上预测,需要进一步拆解成两个向量点积的形式: \(\begin{aligned} logit &= w_{tower} logit_{tower} + w_{FM} logit_{FM} \\ &= w_{tower} UE_{tower} \cdot TE_{tower} + w_{FM} UE_{FM} \cdot TE_{FM} \\ &= concat(w_{tower}UE_{tower}, w_{FM}UE_{FM}) \cdot concat(TE_{tower}, TE_{FM}) \\ &= UE \cdot TE \\ UE &= concat(w_{tower}UE_{tower}, w_{FM}UE_{FM}) \\ TE &= concat(TE_{tower}, TE_{FM}) \end{aligned}\)

最终的用户向量和物料向量可由每个通道的用户向量、物料向量分别concat而得

注意,此处物料向量与权重无关,由两部分物料向量直接concat而得,可以离线计算并缓存。用户向量与w相关,需要线上根据用户请求实时推理得到。

当信息传递的通道越来越多,上述直接concat融合的方式给计算、存储带来较大压力。

一种解法是,拼接后的长向量再经过FM映射成较短的向量。

Facebook提出了一种Simple Attention Fusion的方式,相比“先拼接再映射”的方式取得了更好的效果:

  • 假设一共使用了N种通道,每个通道得到一个d维向量$E_i \in R^d$。(备注:每个通道向量维度必须一样吗?)
  • 先把Ei拼接成一个大向量$E = [E_1, E_2, …E_N] \in R^{Nd}$。
  • 再把E 映射成各个通道的权重$A = softmax(WE), W \in R^{N \times Nd}, A \in R^N$,A[i]是标量,表示第i个通道的权重。
  • 塔输出的最终向量是各通道向量的加权和:$E_{tower} = \sum_i^N A[i] E_i$。维度为d。

假设用户侧有m个通道,输出m个用户向量,$UE_i, i \in [1, m]$;物料侧有n个通道,输出n个物料向量,$TE_j$。

主损失函数为: \(L_{main} = Loss(UE_1 \oplus ... \oplus UE_m, TE_1 \oplus ... \oplus TE_n )\)

  • $\oplus$表示融合函数,例如Simple Attention Fusion。
  • Loss表示计算损失的函数,比如BCE。

为了防止个别通道滥竽充数,Facebook还拿用户(物料)塔中的每一个通道向量与对侧塔的最终向量,做点积并计算loss,作为辅助目标与主目标一起优化。 \(\begin{aligned} L_i^{user} &= Loss(UEi, TE_1 \oplus ... \oplus TE_n), i \in [1, ... m] \\ L_j^{item} &= Loss(UE_1 \oplus ... \oplus UE_m, TE_j), j \in [1, ..., n] \end{aligned}\)

2、目标函数:拜精排为师

2.1、“知识蒸馏”简介

“蒸馏”涉及两个模型:

  • Teacher:模型更复杂,特征更多元,表达能力更强。记作$F_t(\pmb X; \pmb W_t)$,$\pmb W_t$是待学习参数。
  • Student:模型更简单,特征更少,记作$F_s(\pmb X; \pmb W_s)$,$\pmb W_s$是待学习参数。

Student模型的损失函数由两部分组成:

  • 首先,拟合真实目标y,表示为$L_s(y, F_s(\pmb X; \pmb W_s))$
  • 其次,拟合Teacher模型的输出,表示成$L_d(F_t(\pmb X, \pmb W_t), F_s(\pmb X; \pmb W_s))$。也就是说Student要模仿Teacher的一举一动,这个过程就是“知识蒸馏”

最后,student模型的优化目标是优化以上两种损失的加权平均。$\lambda$是可调的超参。 \(min_{W_s} \Big[ (1 - \lambda) \times L_s(y, F_s(\pmb X; \pmb W_s)) + \lambda \times L_d(F_t(\pmb X, \pmb W_t), F_s(\pmb X; \pmb W_s)) \Big]\) 训练完毕,只将Student模型部署上线。

“知识蒸馏”的意义在于:

  • Teacher模型虽然能力强,效果好,但是不便上线。既有可能是因为它耗时多,无法满足线上要求,也有可能是它使用了线上无法拿到的特征(例如曝光位置)。
  • Student模型易于上线,但是能力不足。
  • “知识蒸馏”让两个模型取长(性能)补短(表达能力)。让Student模仿Teacher的一举一动,在“真实答案”(y)和学霸的参考答案($F_t(\pmb X; \pmb W_t)$)双重指导下,学渣Student的成绩也大幅提高。
  • 备注:实践中,蒸馏模型迭代比较困难,需要迭代两套模型,不便于维护)

2.2、“知识蒸馏”用于粗排

“知识蒸馏”非常适用于粗排场景:

  • Teacher:精排模型
  • Student:粗排模型

它不仅可以提升粗排模型的表达能力,还能提高“粗排精排一致性”。如果粗排的品味与精排差异太大,它挑选出的结果很难被精排模型排在前面。

实现蒸馏有两种方式:联合训练法和“两阶段蒸馏”。

联合训练法优化目标如下:

\(min_{W_t,W_s} \Big[ (1 - \lambda) L_s(y, F_s(\pmb X; \pmb W_s)) + \lambda L_d(F_t(\pmb {X,X^*}; \pmb W_t), F_s(\pmb X; \pmb W_s)) + L_t(y, F_t(\pmb{X,X^*;W_t})) \Big]\)

  • $F_s$: 粗排模型。
  • $F_t$: 精排模型,其中的$X^* $ 表示粗排模型不能用,只能为精排模型使用的特征
  • Lt: 精排loss,拟合用户真实反馈。
  • Ls: 粗排loss,拟合用户真实反馈。
  • Ld: 蒸馏loss,拟合精排打分,最小化粗排、精排打分差距。

联合训练时注意:

  • 起始阶段,精排模型没有收敛,蒸馏loss不生效;
  • 蒸馏loss不参与更新精排模型。
  • 精排模型只受Lt更新;粗排模型受Ls和Ld更新不受Lt更新。 (备注:此处联合训练的多个loss和多任务训练时的多个loss有何不同?多任务训练时多个loss融合成一个loss计算梯度,联合训练时精排loss和粗排loss能相互完全隔离吗?最后各自独立更新参数吗?)

联合训练实现起来有诸多困难:

  • 模型包含粗排、精排的参数,异常庞大,即便让二者共用一部分参数,如何保证它们更新同一部分参数时不会相互干扰?
  • 粗排、精排耦合在一起,不方便维护,牵一发而动全身。

更符合大厂实际的是“二阶段蒸馏法”:

线上已经有了正在运行的精排模型,且精排打分已经记录在日志中。让粗排模型直接去拟合样本日志的精排得分。其优化目标如下: \(L = \frac{1}{|B|}\sum_{(X_i,s_i,y_i) \in B} (1-\lambda)L_{CTR}(y_i, F_{pr}(X_i)) + \lambda L_d(s_i, F_{pr}(X_i))\)

  • B: batch。每条样本包括输入特征X,用户反馈y和精排打分s
  • $F_{pr}$: 粗排模型。
  • $L_{CTR}$: 主损失函数,拟合用户真实反馈y。
  • $L_d$: 蒸馏loss,拟合精排打分。Ld可以用MSE,也可以用BCE。用BCE时,label不是0/1整数,而是$s_i \in[0,1]$。备注:BCEloss的label可以是连续数值吗?)
  • $\lambda$: 0-1之间的超参数,Ld的权重。

3、样本选择:纠偏Debias

只拿曝光未点击作为负样本会导致样本选择偏差问题。

粗排的结果进入精排后,只有少部分有曝光机会,大部分被精排过滤掉了,没有曝光机会。

为了缓解SSB问题,可以引入未曝光样本,方法如下:

  • 精排排序后,选择若干头部物料组成集合$S_{top}$,若干尾部物料组成$S_{bottom}$。
  • 将头部集合与尾部集合中的物料两两配对,再搭配上用户u,组成一系列三元组$T={(u,t_p,t_n)}, t_p \in S_{top}, t_n \in S_{bottom}$。
  • 因为尾部物料没有真实用户反馈,在这些三元组上,我们采用pairwise LTR的方式建模。建模目标是$F_{pr}(u,t_p) » F_{pr}(u, t_n)$。即头部物料排在前面。loss可以使用BPR或Marginal hinge loss。 (备注:可以从未曝光样本中随机采样负样本吗?)

具体实践中,头部物料能够曝光,具有用户真实反馈,可以使用BCE loss,让粗排拟合用户真实反馈。完整的loss如下: \(L_{pr} = \frac{1}{|T|}\sum_{(u,t_p,t_n) \in T}L_{LTR}\Big( F_{pr}(u,t_p), F_{pr}(u,t_n) \Big) + \frac{\lambda}{|S_{top}|}\sum_{(u,t,y) \in S_{top}} L_{CTR}\Big(F_{pr}(u,t),y \Big)\)

  • $F_{pr}(u,t)$: 粗排模型打分,表示用户u和物料t的匹配度得分。
  • T: 按照上述方法构造的三元组
  • $L_{LTR}$: pairwise LTR loss。也是一种蒸馏学习。只不过建模的是精排模型的相对顺序,而精排打分
  • $S_{top}$: 精排输出的头部物料,有曝光。
  • $L_{CTR}$: 粗排CTR预估,BCEloss,拟合用户真实反馈。

4、模型结构:轻量级全连接,简化精排当粗排

前面介绍的粗排模型,以双塔为主,与召回更像,突出体现在用户塔与物料塔解耦。

在此基础上一种优化思路是,在双塔得到用户向量和物品向量后,接入一个小型DNN做深度交叉,以提高模型表达能力。实践表明这种方法提升不大。

另一种思路是“去双塔”,即简化精排模型当粗排,以阿里的COLD为代表:

  • 特征筛选。只保留重要的特征,减少模型大小。先训练一版含有SENet的模型,根据SENet给出的特征重要性,挑选头部特征。筛选之后的模型作为粗排模型,它可以不用SENet。
  • 精简网络规模,减少层数,需要实验验证。
  • 工程优化,用Float16替代Float32。
  • 扩展:避免重复计算。例如,对用户行为序列做self- attention比较耗时(时间复杂度是O(N* N),N:序列长度),但是和候选物料无关,只需要进行一次,结果缓存起来,为请求中的所有候选物料共用。同理,一些频繁访问的物料特征的embedding也可以缓存起来,为多个用户请求复用。(备注:如何跨请求复用信息?)

这种简化精排当粗排的思路有一些缺点:

  • 双塔模型只有最后的用户向量与物品向量的点积运算与候选集大小线性相关;最耗时的生成用户向量和物品向量的步骤,都只调用一次,与候选集规模无关。所以双塔模型可以轻松应对候选集扩容需求。
  • 简化版精排方案中,预测过程与候选集大小线性相关,对召回扩容的弹性很低。

本文总访问量