搜广推学习之路(三)

推荐系统课程

王树森推荐系统课程

概要

基本概念

下图为小红书中推荐系统的转化流程 推荐系统转化流程

下图为一些消费指标,这些指标可以反映用户对于推荐是否满意。

  • 点击率=点击次数/曝光次数
  • 点赞率=点赞次数/点击次数
  • 收藏率=收藏次数/点击次数
  • 转发率=转发次数/点击次数
  • 阅读完成率=滑动到底次数/点击次数 x f(笔记长度)

对于阅读完成率来说,存在一个潜在问题:短笔记天然更容易获得高完成率,而长笔记即使质量很高,完成率也可能较低。所以我们就使用一个归一化函数f( ⋅ )将笔记长度归一化,乘该归一化长度可以一定程度上消除笔记长度带来的偏差。

上述消费指标都是一些短期指标,这些指标有意义,但并不是衡量推荐系统好坏的根本指标,这是因为如果只关注短期消费指标,那么给用户推荐的内容就会呈现同质化严重的现象,会导致用户迅速审美疲劳,从而降低用户粘性。所以短期消费指标不是最终的指标,最重要的指标是北极星指标:

  • 用户规模:日活用户数(DAU)、月活用户数(MAU)
  • 消费:人均使用推荐的时长、人均阅读笔记的数量
  • 发布:发布渗透率、人均发布量

用户今天使用了小红书,那么就给小红书提供了一个DAU;用户这个月使用了小红书,不论是登陆了一天还是30天,都给小红书提供了一个MAU。用户规模与推荐系统的好坏是强相关的,推荐系统越好,用户规模越大。

下图为推荐系统的实验流程: 实验流程

先使用离线实验可以大致反映算法的好坏,但离线实验没有线上实验可靠,之前的北极星指标都是指线上指标,线上实验我们首先进行小流量AB测试,将用户随机分为实验组和对照组,实验组用新策略,对照组用旧策略,对照两组指标,判断实验组是否会优于旧策略,如果实验组显著优于旧策略就可以加大流量最终推全。

推荐系统链路

推荐系统目标:从几亿物品中选取几十个物品推荐给用户。

通过下图链路,实现该筛选过程: 推荐系统的链路

首先是召回,我们通过召回从数据库中取出一些笔记,在实践中通常有许多条召回通道,而召回通道包括有协同过滤、双塔模型、关注的作者等等。在小红书中,有几十条召回通道,每条召回通道取回几十到几百篇笔记,这些召回通道一共返回几千篇笔记,然后推荐系统会融合这些笔记,并进行去重和过滤的操作(这里过滤是指去掉用户不喜欢的作者、不喜欢的笔记)。

在召回之后,下一步是排序。通常将排序分为粗排和精排,粗排使用比较简单的模型快速对几千篇笔记进行打分,保留分数最高的几百篇笔记,精排是使用一个较大的神经网络对几百篇笔记打分。精排相对粗排使用的模型更大、特征更多,相对应的计算也会更复杂。结果会更准确。所以为了平衡计算量和准确度我们先粗排后再进行精排。

如下图所示,粗排和精排都是在神经网络中输入用户特征、物品特征和统计特征信息,然后经过神经网络训练,输出预测的点击率、点赞率、收藏率和转发率,再将这些指标进行融合就可以得到排序分数,这也是我们进行筛选和排序的依据。 推荐系统的排序

在排序后,每篇笔记都有一个分数,表示用户对笔记的兴趣有多高。然后还需要再从几百篇笔记中进行随机抽样,抽取几十篇笔记然后利用规则将内容相似的笔记进行打散,并插入广告和运营物品,根据生态再调整排序。其中多样性抽样(如MMR、DPP)是最重要的。

A/B测试

A/B测试就相当于是使用一小份样本来做预实验,以此来判断改进的真实效果,同时用于确定网络参数的最优取值。

随机分桶:做A/B测试,需要对用户进行随机分桶,具体操作如下图所示: 随机分桶 随机分桶

分层实验:分层目标解决“流量不够用”的问题。在一个公司中会有许多团队和部门,都需要做A/B测试,比如推荐系统(召回、粗排、精排、重排)团队、用户界面团队和广告团队。每个团队都需要做实验,那么可能需要同时做成千上百个实验,但将用户随机分为10组,1组作为对照组,那么最多同时只能做9个实验,显然这无法达到需求。所以我们就需要做分层实验。下面具体介绍分层实验。

首先将实验分为很多层,比如召回、用户界面等等,同层之间实验是互斥的,都是召回实验,A策略占据了1号桶,那么其他策略就不可以再使用1号桶了。而不同层之间的实验是正交的。(用户同时参与不同层的实验,我们也能通过统计方法分离出每个实验的独立效果) 分层实验

如下例所示,假设系统有n个用户,召回层将用户分为了m个桶,均匀打散后,粗排层也可将用户分为m个桶。当我们评估粗排层的实验效果时,对于粗排层的实验组和对照组来说,它们包含召回组各个桶用户的比例都是相同的,在这种情况下,我们可以认为粗排组实验组相较于对照组的变化是由于粗排组自身策略的不同造成的。 分层实验

当然也不是所有实验都可以正交:

  1. 同类的策略(比如召回层的不同通道)天然互斥,对于同一用户只能使用其中一种;
  2. 同类的策略可能相互抵消或相互增强,互斥可以避免同类策略相互干扰;
  3. 不同类的策略(比如添加召回通道和优化粗排模型)会通常不会相互干扰,才可以做正交的两层。

Holdout机制

Holdout机制解决的是长期效果评估的问题,比如说一家公司不停进行召回、粗排、精排和重排等实验,一段时间后,如果管理层询问这些所有层的优化累积起来,核心业务指标提升了多少,如果没有Holdout,我们无法评估这个问题,因为每个用户在A层为对照组,在B层也可能是对照组,所以所有用户都或多或少被实验影响,我们没有“未受干扰”的基准进行比较。所以Holdout机制就是提供这个基准。 Holdout

Holdout机制的基本思想如下:

  1. 取10%的用户作为Holdout桶,推荐系统使用剩余90%的用户做实验,两者互斥;
  2. 考察10%Holdout桶与90%实验桶的差异(需归一化)作为整个部门的业务指标收益;
  3. 每个考核周期结束之后,清除Holdout桶,让推全实验从90%用户扩大到100%用户;
  4. 重新随机划分用户,得到Holdout桶和实验桶,开始下一轮考核周期;
  5. 初始的新Holdout桶与实验桶的各种业务指标的差异接近0,随着召回、粗排、精排、重排实验上线和推全,差异会逐渐扩大。

实验推全和反转实验

实验推全:在Holdou机制基础上,观测到某个策略对某个桶有明显提升,关闭A/B测试,将策略推全到整个90%用户上。需要在召回层之前新建一个推全层,该层与其他层正交。 实验推全

但实验推全还存在一个问题,有些指标(点击、交互)可以立刻收到薪策略的影响,短期内给出反馈。但有一些指标(留存)具有滞后性,短期内是无法给出反馈的,需要长期观测。但一方面我们希望早日推全新策略,以空出桶来供其他实验进行,但另一方面我们也不希望忽略这些滞后性的指标。那么我们就引入了反转实验。

反转实验:在实验推全的新层中设立一个旧策略的桶,长期观测实验指标。且holdout桶与反转桶也无关。在考核周期结束后,清除holdout桶,推全策略时,反转桶仍然不变保持旧策略,只有在反转实验结束后才会关闭反转桶,真正将新策略推全至100%的用户。 反转实验

召回 (Retrieval)

基于物品的协同过滤 (ItemCF)

ItemCF的核心思想:用户的兴趣具有一定的连贯性,喜欢某个物品的用户往往也会对相似的物品感兴趣。比如用户A喜欢item1,而item2item1相似,那么很大可能用户A也喜欢item2

那么我们如何判断两个物品是相似的?可以用过重叠用户来判断,比如喜欢item1的用户中70%的用户还同时item2,那么我们认为item2item2相似。当然这只是一个通俗的解释,下面介绍具体算法。

  1. 量化用户对于交互过的物品的兴趣,给出兴趣分数like(user, itemj)(这一步是根据用户的交互行为进行打分的,比如点赞、收藏等行为);
  2. 计算新物品与交互过的物品的相似度sim(itemj, item)
  3. 预估用户对于候选物品的兴趣 like(user, itemj) × sim(itemj, item)
ItemCF

物品的相似度:基本思路是两个物品的受众重合度越高,那么两个物品的相似度也越高。所以初步定义物品相似度为公式(1),但该公式过于简单没有考虑用户对物品的喜欢程度,所以我们对该公式进一步优化得到公式(2)。其实我们将喜欢程度简化为0和1时,公式(2)也就是公式(1)了。 物品相似度

公式(2)其实就是常见的余弦相似度表达式,我们将W1集合每个元素对i1的喜欢程度看作一个向量,W2集合每个元素对i2的喜欢程度看作一个向量。分子非同时喜欢两个物品的用户,相乘为0直接忽略,即为公式中V集合的用户对两个物品的喜欢程度相乘。 物品相似度

下面介绍ItemCF召回的完整流程

  1. 事先做离线计算
    1. 建立“用户->物品”的索引
      • 记录用户最近点击交互过的物品,并计算兴趣分数,根据索引可以得到用户近期感兴趣的列表
      • {用户,(物品ID,兴趣分数)}
    2. 建立“物品->物品”的索引
      • 计算物品之间的两两相似度,对于每个物品,可以根据索引找到它最相似的k个物品
      • {物品,(物品ID,相似度)}
  2. 线上做召回
    1. 给定用户ID,通过“用户->物品”索引,找到用户近期感兴趣的物品列表 (last-n)
    2. 对于 last-n 列表中的每个物品,通过“物品->物品”的索引,找到 top-k 相似物品
    3. 对于取回的相似物品 (最多有 nk 个),用公式预估用户对物品的兴趣分数
    4. 返回分数最高的 100 个物品,作为推荐结果

离线计算 线上召回

线上召回时索引可以避免暴力枚举所有物品。虽然离线计算量大,但是有效减少了线上计算量。

Swing召回通道

Swing召回通道是ItemCF的变体,二者的区别在于定义物品的相似度。

对于ItemCF算法,在计算两个物品相似度时,我们认为两个物品的受众重合度越高,那么这两个物品的相似度就越高。但这就存在一个问题,如图所示,“护肤品打折”和“字节裁员”,这两个毫不相关的话题,受众重合度应该很低。但如果这两个帖子被分享到同一个微信群聊,那么这个群聊中的用户都会同时与这两个帖子产生交互。按照ItemCF算法,会错误地认为这两个是相似的物品,与事实相悖。针对这个问题,就产生了Swing召回算法。 ItemCF相似度的不足

Swing的直觉来源是,如果大量用户同时喜欢两个物品,且这些用户之间的相关性低,那么这两个物品一定是强关联。所以如下图公式所示,两个用户的重合度越高,那么这两个用户在计算两个物品相似度中的贡献就越小。这样就有效避免了上述问题。注意分母中的α是一个调节因子,防止分母为0。 Swing Swing

基于用户的协同过滤 (UserCF)

UserCF与ItemCF也类似,ItemCF是通过相似物品来推荐,而UserCF是通过相似用户来推荐。UserCF的核心是:如果user1和user2相似,那么user2喜欢的物品A,我们认为user1很大可能也喜欢。

同ItemCF,在UserCF中,也可以通过相似的用户和这些用户对于物品A的兴趣分数,得到user对一个新物品的兴趣分数。

在UserCF中,我们需要求解的就是两个用户之间的用户相似度。两个用户喜欢的物品重合度越高,我们认为这两个用户的相似度越高,所以产生了公式1。但这个公式具有一个问题就是没有考虑热门物品的影响,比如哈利波特,这个IP太过于经典,即使用户A和B是相差很大的两个用户,他们也可能同时喜欢哈利波特。所以我们要降低热门物品的权重,改进得到公式2。 用户相似度

下面介绍UserCF召回的完整流程

  1. 事先做离线计算
    1. 建立“用户->物品”的索引
      • 记录用户最近点击交互过的物品,并计算兴趣分数,根据索引可以得到用户近期感兴趣的列表
      • {用户,(物品ID,兴趣分数)}
    2. 建立“用户->用户”的索引
      • 计算用之间的两两相似度,对于每个用户,可以根据索引找到他最相似的k个用户
      • {用户,(用户ID,相似度)}
  2. 线上做召回
    1. 给定用户ID,通过“用户->用户”的索引,找到 top-k 相似物品 2.对于 top-k 列表中的每个用户,通过“用户->物品”索引,找到用户近期感兴趣的物品列表 (last-n)
    2. 对于取回的 nk 个 相似物品,用公式预估用户对每个物品的兴趣分数
    3. 返回分数最高的 100 个物品,作为召回结果
UserCF

离散特征处理

把序号转化为向量,one-hot不介绍了,因为在数据样本很多的情况下,使用one-hot编码是不现实的,我们下面简单介绍Embedding(嵌入)的方法。

在Embedding中,参数以矩阵的形式保存,矩阵的大小是向量为度x类别数量。 Embedding

如果训练的好,是可以通过embedding看出物品的特点,如下图所示,位于第一象限的物品都为动画片,位于第二象限的物品为漫威影片。 Embedding

Embedding是业界常用的一种方式。TensorFlow、PyTorch等提供Embedding层,所以更详细的就不再介绍。

矩阵补充

(这种方法已不再使用) 如图是基于Embedding做推荐的模型,即为矩阵补充模型。输入为用户ID和物品ID,输出为用户对物品的兴趣分数。 矩阵补充模型

训练模型参数,首先需要考虑数据集的问题,矩阵补充模型中的数据集如下: 矩阵补充模型数据集

然后通过Embedding层,将用户ID和物品ID映射为向量,第u号用户映射为au,第i号物品映射为bi,由于 < au, bi>是第u号用户对第i号物品的兴趣分数预估值,所以我们训练模型的优化目标就是令该预估值尽量接近兴趣分数真实值y,所以得到优化目标公式。 矩阵补充模型

矩阵补充模型

如下图所示,绿色部分即为我们数据集中所包含的部分,我们通过绿色部分对模型进行训练,然后再使用训练好的模型对灰色部分进行预测,从而将该矩阵补充完整,然后就可以根据兴趣分数的大小对用户进行推荐。 矩阵补充模型

但该模型在实践中效果并不好,原因如下图所示: 矩阵补充模型

线上服务

矩阵补充模型训练好后可作为召回通道,所以我们需要将模型存储好。在矩阵补充模型中需要存储的为矩阵A和B。用户矩阵A存储到key-value表中,key是用户ID,value为A的一列,给定用户ID,返回向量(用户的embedding)。物品矩阵B的存储比较复杂,不是简单的key-value存储。

将embedding向量存储好之后,可以进行线上服务。用户使用小红书时,小红书后台会做召回,将用户ID作为key,查询key-value表,得到该用户对应的向量,记作a。然后查找用户最有可能感兴趣的k个物品(最近邻查找),但不可能枚举所有物品,一一计算用户对该物品的兴趣分数( < a, bi>)。所以我们需要加速最近邻查找,避免枚举。

下面给出一个例子: 近似最近邻查找的例子 1. 物品ID通过embedding得到物品向量,如下图所示,将其向量分布区域划分为n个小区域(余弦划分为扇形区域), 每个区域使用一个单位向量表示; 2. 以该区域的表示向量作为key,区域中所有物品的列表value,n个区域就有n个索引; 3. 在线上快速做推荐,分别计算用户向量与所有索引向量的相度; 4. 找到相似度最高的索引,再分别计算用户向量与该索引区域包含的所有物品向量的相似度; 5. 选取最相似的k个点,作为召回结果。

近似最近邻查找

在工业界中,Milvus、Faiss、HnswLib等向量数据库都支持近似最近邻查找。

双塔模型

在矩阵补全模型的基础上,双塔模型不是简单考虑了用户和物品的ID,还包括用户和物品的特征,如离散特征(用户所在城市)和连续特征(用户的年龄)。在计算用户对物品的兴趣分数时使用余弦相似度取代内积。 双塔模型 双塔模型

需要注意上图,适用于召回的双塔模型是先分别通过神经网络生成表征,再计算两个表征之间的相似度(兴趣分数),称为后期融合。而在精排和粗排中,则需要使用前期融合,如下图所示,也就是先融合物品和用户的特征向量,再通过神经网络输出兴趣分数。

接下来介绍双塔模型的训练方式,双塔模型有三种训练方式:

  • Pointise: 独立看待每个正样本、负样本,做简单的二元分类;
  • Pairwise: 每次取一个正样本、负样本组成二元组;
  • Listwise: 每次取一个正样本、多个负样本组成一个list,训练方式类似于多元分类。

正样本指的是用户感兴趣的物品,即用户点击过的物品,而负样本指的就是用户不感兴趣的物品。了解这个之后,接下来我们具体介绍这三种训练方式。

Pointwise训练:

  1. 把召回看作二元分类任务
  2. 鼓励用户与正样本的余弦相似度 cos(a, b) 接近 +1
  3. 鼓励用户与负样本的余弦相似度 cos(a, b) 接近 -1
  4. 控制正负样本的数量为 1:2 或者 1:3 (经验值)

Pairwise训练:

如下图所示,pairwise同时取一个正样本和一个负样本,得到用户对这组正样本的余弦相似度 cos(a, b+)、负样本的余弦相似度 cos(a, b)。由于我们同时拥有一个正、负样本,所以我们可以将训练目标设置为cos(a, b+) > cos(a, b),我们要设置一个边界m,令cos(a, b+)cos(a, b)之间的差值至少大于等于m。因此我们也可以得到Pairwise的损失函数,训练时以最小化损失函数为训练目标。 Pairwise训练 Pairwise训练

Listwise训练:

Listwise方法是将用户对正负样本的余弦相似度通过softmax函数转化为0-1范围内的数值,然后正样本的标签为1,负样本的标签为0.然后最小化交叉熵损失函数。 Listwise训练

综上所述,三种训练方法本质都是要将用户与正样本的余弦相似度接近1,用户与负样本的余弦相似度接近0。只是输入的正负样本数量不同。

在上述过程。我们还存在一个遗留问题,正样本是指用户感兴趣的物品、负样本是用户不敢兴趣的物品。那我们要怎么选择正负样本?

这个样本选择较为简单,我们选择曝光且有点击的“用户-物品”二元组作为正样本即可。但由于28法则,少部分热门物品会占据大量的点击,这就会导致我们得到的正样本多是热门物品样本,使得那些用户感兴趣但冷门的物品被遗漏。从而造成热门物品更热、冷门物品更冷。

对此我们采用的解决方案是过采样冷门物品,或者降采样热门物品。过采样是指一个样本出现多次,也就是增加冷门物品被采样的概率。降采样是指抛弃一些样本,也就是抛弃一些热门物品,抛弃概率与热门物品的点击次数正相关。我们通过这样的方法手动降低数据偏见对模型训练造成的影响,从而更加多元化地为用户推荐物品。

选择负样本即为选择用户不感兴趣的样本,也可以理解为是在推荐系统链路每一步中被淘汰的物品。但要注意,在召回过程淘汰的物品大概率是用户不感兴趣的物品,这部分被称为简单负样本;在排序过程被淘汰的物品是用户具有一定兴趣,但不是最感兴趣的样本,这部分被称为困难负样本;而最终曝光但未被点击的样本并不可以作为负样本。因为这些已经是筛选出来的用户感兴趣的物品,没被点击到是其他因素的影响。

简单负样本也可以分为全体物品和batch内负样本。

首先介绍从全体物品中抽取简单负样本。

因为在几亿个物品中只有几千个物品被召回,所以我们可以近似认为全体物品就是未被召回的物品。我们只需要在全体物品中进行抽样作为负样本。

但是抽样时我们如果采用均匀抽样,由于少部分物品为热门物品,大部分都为冷门物品,会导致负样本中大部分都为冷门物品,这也是不公平的,同样会导致热门物品更热、冷门物品更冷。

所以我们需要采用的时非均匀抽样抽样,负样本抽样概率与物品的热门程度正相关。抽样概率正比于点击次数的0.75次方,这里的0.75是经验值。

接下来介绍batch内负样本。

如下图所示,在这样一个用户点击物品的batch内,用户与其点击的物品作为正样本,那么用户大概率对于其他物品是不感兴趣的,我们将其作为负样本。这样一个batch内会有n(n-1)个负样本。但也会出现一个问题,一个物品出现在batch内的概率与其点击次数成正比,那么也就意味着物品在batch内成为负样本的概率与其点击次数成正比。这与我们希望的点击次数的0.75次方是不一致的。这种情况下会导致对热门物品的打击过大,造成偏差,因此我们要采取措施进行纠偏,防止过分打压热门物品,如下图所示。采取该措施可以保证人们物品的cos不会过小。该措施只需要在训练时加入,线上召回时无需调整。

batch内负样本 batch内负样本 batch内负样本

困难负样本是被排序淘汰的样本,分为被粗排淘汰的物品(比较困难)和精排中分数靠后的物品(非常困难)。因为这两者都是用户对其有一定兴趣但不是最感兴趣的,如果不够精确,很容易被划分为正样本,所以对他们进行分类是困难的。

在实际训练中,我们常常使用混合负样本,比如50%为简单负样本,50%为困难负样本。

接下来介绍双塔模型的线上召回和更新

在训练好双塔模型的两个塔后,我们先使用物品塔提取物品特征,离线存储物品向量。对于几亿个物品,用物品塔计算每个物品的特征向量b,把<物品向量,物品ID>存入向量数据库,以便加速最近邻查找。

然后进行线上召回,查找用户最感兴趣的k个物品。首先根据给定的用户ID和画像,使用用户塔线上计算用户向量a,然后把a作为query,调用向量数据库做最近邻查找,返回余弦相似度最大的k个物品,作为召回结果。

由于用户的兴趣变化是动态变化的,所以我们选择线上实时计算用户向量。而物品的特征相对稳定,且物品的数量过于庞大,所以我们选择线上召回之前,离线存储物品向量。

在实践中,需要对模型不断进行更新。更新也分为“全量更新”和“增量更新”。

全量更新较为简单,每天凌晨(一天结束时),在原先模型参数的基础上,使用最新一天的数据训练1epoch,得到新的用户塔神经向量和物品向量,供线上召回使用。全量更新对数据流、系统的要求较低。

而增量更新相对复杂一些。因为增量更新是实时进行更新,以确保能够与用户的兴趣完全契合,比如用户早上刷小红书产生了新的兴趣点,那么小红书就要通过增量更新实时更新,让用户在中午或者更早时候就可以刷到与新的兴趣点相关的内容。增量更新需要实时收集线上数据,做流式处理,生成TFRecord文件,对模型做online learning,增量更新用户ID Embedding参数,注意不对神经网络其它部分(全连接层)的参数进行更新。然后发布用户ID Embedding,供用户塔线上计算用户向量。

当然在事件中,我们将全量更新和增量更新结合起来,如下图所示。

双塔模型+自监督学习

在完全了解双塔模型后,我们将双塔模型和自监督学习结合起来,可以提升业务指标。自监督学习是为了将物品塔训练的更好,以得到更具代表性、更通用、更健壮的物品特征。

自监督学习的基本思想如下,我们使用不同的特征表示方法对物品进行表示,同一物品经不同变化得到的特征向量仍有高相似度,而不同物品之间的特征向量具有低相似度。

下面为常见的四种特征变换方法: RandomMask Dropout 互补特征 互补特征 互补特征

下面为自监督学习的训练流程: 自监督学习 自监督学习 自监督学习

下面将自监督学习和双塔模型结合起来。

其他召回通道

  1. 地理位置召回:GeoHash召回、同城召回
    • 地理位置召回是基于用户可能对附近发生的事情感兴趣,且该种召回与个性化无关,只是根据地理位置推荐当前位置的优质笔记。GeoHash 和同城的区别在于,GeoHash 是根据经纬度编码表示地图上一个长方形区域,索引为GeoHash -> 优质笔记列表(按时间倒排);同城召回是根据所在和曾在的城市,索引为城市 -> 优质笔记列表。
  2. 作者召回:关注的作者召回、有交互的作者召回、相似的作者召回
    • 关注的作者召回:用户对关注的作者发布的笔记感兴趣。索引:用户 -> 关注的作者,作者 -> 发布的笔记。召回:用户 -> 关注的作者 -> 最新发布的笔记。
    • 有交互的作者召回:用户对某篇笔记感兴趣,那么也可能对该作者的其他笔记感兴趣。索引:用户 -> 有交互的作者。召回:用户 -> 有交互的作者 -> 最新发布的笔记。注意在索引时会及时更新最近有交互的作者,长时间未交互的作者删去。
    • 相似的作者召回:类似于ItemCF,用户喜欢一个作者那么大概率也对与其相似的作者感兴趣。索引:作者 -> 相似作者。召回:用户 -> 感兴趣的作者 -> 相似的作者 -> 最新发布的笔记。
  3. 缓存召回
    • 复用前几次推荐精排的结果。精排的结果已经是用户最感兴趣的了,但在送入重排后做多样性抽样,只有几十篇笔记会被挑选出来,剩下的大部分笔记没有被曝光,被浪费了。所以我们直接将这些位于精排前50但没有曝光的笔记缓存起来,作为一条召回通道。在用户下次刷新小红书时将缓存通道中的笔记取出来作为一路召回的结果。但显然缓存大小是固定的,所以需要退场机制,下面为常见的退场规则:一旦笔记成功曝光,就从缓存退场;如果超过缓存大小,就移除最先进入缓存的笔记;笔记最多被召回若干次,达到这个次数就退场;每篇笔记最多保存若干天,达到这个天数就退场;想让低曝光笔记缓存更长时间,基于曝光次数设置退场规则。

曝光过滤 & Bloom Filter

曝光过滤是指如果用户看过某个物品的话,我们就不再将该物品曝光给该用户。所以我们记录曝光给每个用户的物品(只需要记录一个月以内的,因为小红书只召回一个月内发布的笔记),对于每个召回的物品,判断它是否已经曝光给该用户,将曝光过的物品排除掉。 但在判断找回物品是否曝光时,如果暴力对比时间复杂度很大,所以采用Bloom Filter。

Bloom Filter 判断一个物品ID是否在已曝光的物品集合中,如果结果为no,那么该物品一定不在集合中,如果结果为yes,那么该物品可能在集合中。

  1. Bloom Filter 把物品集合表征为一个 m 维的二进制向量
  2. 每个用户有一个曝光物品的集合,表征为一个向量,需要 m bit的存储空间;
  3. Bloom filter 有 k 个哈希函数,每个哈希函数把物品ID映射成介于 0 和 m-1 之间的整数;
  4. 如图所示,已曝光物品ID(ID1, 2, 3, 4, 5, 6)通过哈希函数映射进了二进制向量中对应的位置,如果该位置为0则调整为1,若为1无需调整;
  5. 对于召回的物品ID,通过哈希函数映射,若对应位置为1,则说明该物品已曝光,否则未曝光。如ID8发生哈希碰撞,就会导致误判,但已曝光的一个也不会遗漏。 Bloom Filter

还有k=3的情况,此时只有三个位置都为1才认为是已曝光,这样误判概率会降低但依然存在。 Bloom Filter

如下图即为曝光过滤的过程,曝光物品会被记录下来,进行实时处理计算曝光物品的哈希值,把结果写入Bloom Filter的二进制向量,这个过程一定要快,不然用户刷新可能就会看到已看过的物品,小红书使用Kafka + Flink进行处理。召回结束后,召回服务器请求曝光过滤服务,曝光过滤服务将二进制向量发送给召回服务器,召回服务器利用Bloom Filter计算召回物品的哈希值与二进制向量进行对比,将已曝光的物品过滤掉。剩余未曝光物品发送给排序服务器。 曝光过滤

Bloom Filter 还有一个缺点。在每次往集合内添加一个物品,只需要把向量对应的k个位置置为1。但删除一个物品时,无法直接将k个位置置为0,因为会影响其他物品的信息。所以,Bloom Filter 只支持添加物品,不支持删除物品。如果从集合中移除物品,则无法消除它对向量已经发生过的影响。但为了减少Bloom Filter的误判率,对于年龄大于一个月的物品,我们要移除掉(大于一个月的物品不会被召回,没必要记录在Bloom Filter中,使得二进制向量中1过多)。因为Bloom Filter不支持删除,所以我们只能每天重新计算一次二进制向量。很麻烦。


搜广推学习之路(三)
http://hsilverbullet.github.io/Post3/
Author
HSilverbullet
Posted on
November 28, 2025