推荐系统学习笔记

一. 概要
推荐系统基本概念
可以将一个 推荐系统 简单地理解为 给用户推荐实体或 非实体物品的系统。其任务是根据用户和物品的特征,使用某种或某些推荐算法预测任意用户对任意物品的兴趣得分,并按照预测的得分顺序,将排在前列的物品展示给用户。
Tip:以小红书为例,物品为小红书笔记,后续统一使用物品进行描述。
转化流程
根据不同公司的不同产品,一般有不同的转化流程。流程中的动作意味着 用户对物品感兴趣,这些动作意味着 用户和物品产生了交互,可以作为 推荐系统使用的推荐依据。 转化流程一般分为三步,下图以小红书为例:

Tip:抖音没有点击流程。
常见的推荐系统评测指标
| 消费指标(短期消费指标) | 北极星指标(长期消费指标) |
|---|---|
| 基于点击的指标:点击率 | 基于用户规模:日活用户数、月活用户数 |
| 基于反馈的指标:点赞率、收藏率、转发率 | 基于消费用户:人均使用推荐时长、人均阅读笔记数量 |
| 基于特定产品的指标:阅读完成率(文章) | 基于发布用户:发布渗透率、人均发布量 |
消费指标并不是推荐系统追求的根本目标,考察的关键应该放在北极星指标上。只关注追求极致的消费指标提升,最直接的影响就是导致推荐给用户的物品都是相似或相同的,显著减少了推荐的多样性,用户很难接触到感兴趣但新鲜的物品,长久以往用户活性降低,用户不断流失。在实践中,消费指标降低 但是北极星指标提升也被认为是一种正向的趋势。
Tip:发布是小红书的核心竞争力。
推荐系统的实验流程
实验流程:离线实验 → 小流量 A/B 测试 → 全流量上线
离线实验:收集历史数据在其上做模型训练和测试,算法没有部署到产品中,没有跟用户交互。
小流量 A/B 测试:把算法部署到产品中,设置实验组与对照组使用新旧策略,算法与部分用户做交互。
全流量上线:A/B 测试的新策略优于旧策略,就加大交互的用户流量,最后推全。
推荐系统的链路
推荐系统目标:从几亿物品中选取几十个物品推荐给用户。
推荐系统的链路:

召回:用多条召回通道(协同过滤、双塔模型、关注的作者等),先快速取回几千物品。
粗排:用小规模神经网络,给几千篇物品打分,选出分数 Top 几百的物品。
精排:用大规模神经网络,给粗排选出的几百篇物品重新打分排序。
重排:多样性抽样从几百篇物品中挑选几十篇,依规则打散,并插入广告和运营物品,根据生态再调整排序。
Tip:召回通道实际上就是实现召回的模型方法,第二章节会详细介绍协同过滤、双塔模型和其他召回通道。
推荐系统的 A/B 测试
召回团队实现了一种新的召回通道,离线实验结果是正向的,下一步就需要做线上的小流量 A/B 测试。A/B 测试启到如下作用:
- 考察新的召回通道对线上指标的影响;
- 测试选择模型的最优参数。
随机分桶
A/B 测试需要设置对照组和实验组,对照组和实验组的实验通过随机分桶来实现。不妨设有全部 位用户,分成 个桶,每个桶有 位用户。随机分桶实现原理如下:
- 首先使用哈希函数将用户 ID 映射成某个区间内的整数,然后把这些整数均匀随机分成 个桶;
- 可以取其中若干个桶作为多个实验组采用不同的召回通道,再另取一个新桶为对照组使用原策略;
- 计算每个桶的业务指标;
- 如果某个实验组显著优于对照组,则说明对应的策略有效,值得推全。
Tip:从统计学角度出发,如果样本足够大,可以认为各个桶的特征相同。
分层实验
互联网大厂这种信息流公司有很多的团队和部门,需要同时负责推荐系统(召回、粗排、精排、重排)、用户界面、广告等业务的 A/B 测试。此时会遇到用户流量不够用的情况,分层实验是一项很好的举措:
分层:分成召回、粗排、精排、重排、用户界面、广告等等多个层。
同层互斥:某召回实验占用了召回层的 个桶,其他召回实验只能使用剩余的 个桶。
不同层正交:每一层独立随机对用户做分桶。每一层都可以独立使用全部桶的用户做实验。
基于以上原理,不妨以召回和召回的下一层(粗排层)为例说明。假设该系统有 个用户,召回层将用户分成了 个桶:,均匀打散后,粗排层可以也将用户分为 个桶: ,符合不同层正交的原理。那么有:
- 任意的召回桶 和粗排桶 满足 ;
- 同层互斥,不同的召回桶 和 的交集为 ;
- 不同层正交,不同的召回桶 和粗排桶 的交集大小为 。
Tip:这里的集合是桶的用户集合,取模就是计算桶中用户数量,2 中互斥的两个集合交集自然为空集,显然成立。对于 3 中等式左边的目标是求上下不同层的任意两个桶中的相同用户数量,如下分析:上一层每个桶中的用户被均匀分到下一层 个桶中,相当于上下任意两桶中只有 比例的用户是相同的,再考虑到任意桶中只有 个用户,所以上下两层任意桶中只有 个相同用户。
既然分层实验可以有效处理流量不足问题,那么是否存在所有实验都正交,则可以同时做无数组实验的情况呢?答案是否定的,原因如下:
- 同类的策略(比如召回层的不同通道)天然互斥,对于同一用户只能使用其中一种;
- 同类的策略可能相互抵消或相互增强,互斥可以避免同类策略相互干扰;
- 不同类的策略(比如添加召回通道和优化粗排模型)会通常不会相互干扰,才可以做正交的两层。
Holdout 机制
公司需要考察部门(如推荐系统)在一段时间内对业务指标的总体提升,需要每个实验(召回、粗排、精排、重排)独立汇报对业务指标的提升贡献,因此设置了 Holdout 机制,基本思想为:
- 取 10%的用户作为 Holdout 桶,推荐系统使用剩余 90%的用户做实验,两者互斥;
- 考察 10%Holdout 桶与 90%实验桶的差异(需归一化)作为整个部门的业务指标收益;
- 每个考核周期结束之后,清除 Holdout 桶,让推全实验从 90%用户扩大到 100%用户;
- 重新随机划分用户,得到 Holdout 桶和实验桶,开始下一轮考核周期;
- 初始的新 Holdout 桶与实验桶的各种业务指标的差异接近 0,随着召回、粗排、精排、重排实验上线和推全,差异会逐渐扩大。
实验推全和反转实验
实验推全: 在 Holdou 机制基础上,观测到某个策略对某个桶有明显提升,关闭 A/B 测试,将策略推全到整个 90%用户上。需要在召回层之前新建一个推全层,该层与其他层正交。
反转实验: 为了尽快推全新策略和长期观测实验指标,需要设立反转实验。故需要在实验推全的新层中设立一个旧策略的桶,长期观测实验指标。
- 譬如点击、交互这些指标会立刻受到新策略的影响,而譬如留存这类指标具有滞后性,需要长期观测。
- 同时,实验观测到显著收益后需要尽快推全全新策略,腾出桶给其他实验使用或基于新策尽快进行开发。
Tip:如下图所示,推全层中推全新策略的桶和使用旧策略的反转桶是互斥的。新推全层是与 Holdout 桶无关的,考核周期结束后清除 Holdout 桶不会影响反转桶。反转实验结束后关闭反转桶,真正做到推全到 100% 用户上。

本章小结
推荐系统是给用户推荐实体或非实体物品的系统。
基于此,本章首先依次介 绍了 推荐系统的基本概念、推荐依据、评价指标、链路、实验流程。
然后针对实验流程中的 A/B 测试 进行了知识扩充,详解了 随机分桶、分层实验、Holdout 机制、实验推全 和 反转实验 的概念与思想,这些基础知识将贯穿后续所有章节内容。
二. 召回
召回是推荐系统链路中的第一个流程,目的是从几亿的 item 中选出几千 item。进行召回的模型方法是多种多样,包括但不仅限于基于统计学的、基于规则的和基于神经网络的。本章先从最基础的协同过滤开始介绍。
基于物品的协同过滤-ItemCF
ItemCF 原理与实现
ItemCF 的基本思想在于:如果用户喜欢物品 ,而物品 与物品 相似,那么用户很可能喜欢物品 。根据该思想,ItemCF 的实现需要基于以下步骤:
- 基于转化流程中的动作得到某用户对物品的兴趣分数 ;
- 离线计算得到的物品之间的相似度 ;
- 如下图所示,线上预估用户对候选物品的兴趣 。

用户对候选物品的兴趣:2×0.1+1×0.4+4×0.2+3×0.6=3.2
物品相似度
一般来说,ItemCF 的思想认为两个物品的受众重合度越高,两个物品越相似。所以计算物品相似度是基于与两个物品交互的用户群体重合度的:
- 设喜欢物品 的用户群体为集合 ,喜欢物品 的用户群体为集合 ;
- 定义这两个集合的交集 ,那么简单计算两个物品的相似度为 。
- 考虑到用户对于不同物品的喜欢程度 对相似度的影响:
,实际上这就是常用的 余弦相似度 的表达形式。
ItemCF 召回的完整流程
在线上进行 ItemCF 召回之前,需要 事先做离线计算,存储计算结果并建立索引:
- 建立”用户 → 物品“的索引:用户物品,兴趣分数
(1)记录每个用户最近点击、交互过的物品 ID;
(2)给定任意用户 ID,可以利用索引找到该用户近期感兴趣的物品列表。- 建立”物品 → 物品“的索引:物品最相似的个物品相似度
(1)计算物品之间的两两相似度;
(2)对于每个物品,索引该物品最相似的 个物品;
(3)给定任意物品 ID,可以利用索引快速找到该物品最相似的 个物品。
基于事先建立的索引,实现 线上召回:
- 给定用户 ID,通过”用户 → 物品“索引,找到用户近期感兴趣的物品列表 ;
- 对于 列表中的每个物品,通过”物品 → 物品“的索引,找到 相似的物品;
- 对于所有取回的相似物品(最多有 个),用公式预估用户对物品的兴趣分数;
- 返回分数最高的若干个物品作为召回结果。
Tip:线上召回时索引可以避免暴力枚举所有物品。虽然离线计算量大,但是有效减少了线上计算量。
Swing 召回通道
前面说明过,ItemCF 计算物品相似度是基于与两个物品交互的用户群体重合度的,如果重合的用户是一个小圈子(比如微信群),受众完全不同的两个物品也容易被误判为相似度较高,实际落地应用效果较差。因此需要考虑用户之间的物品重合度大小,降低小圈子的权重。故此在 ItemCF 的基础上提出了 Swing 模型:
- 设用户 喜欢的物品集合为 ,用户 喜欢的物品集合为 ,喜欢物品 的用户群体为集合 ,喜欢物品 的用户群体为集合 ;
- 定义两个 用户的重合度 为: ,如果用户 和 的重合度高,则他们可能来自一个小圈子,计算物品相似度时需要降低他们的权重;
- 定义交集 ,则两个物品的相似度计算为
,其中 为超参数。这一公式体现了用户群体的重合度越大时,小圈子对物品相似度的贡献越小。
基于用户的协同过滤-UserCF
UserCF 原理与实现
UserCF 的基本思想在于:如果用户 与用户 相似,而且 喜欢某物品 ,那么 很可能也喜欢该物品。根据该思想,UserCF 的实现需要基于以下步骤:
- 基于转化流程中的动作得到用户对某物品的兴趣分数 ;
- 离线计算得到的用户之间的相似度 ;
- 如下图所示,线上预估用户对候选物品的兴趣 。

用户对物品的兴趣:0.9×0+0.7×1+0.7×3+0.4×0=2.8 。
用户相似度
类似于 ItemCF 和物品相似度,UserCF 认为两个用户喜欢的物品重合度越高,两个用户越相似。所以计算物品相似度的计算方式如下:
- 设用户 喜欢的物品集合为 ,用户 喜欢的物品集合为 。
- 定义这两个集合的交集 ,那么简单计算两个用户的相似度为 。
- 上述公式等价于: ,该公式表示该用户的相似度计算时并没有区别对待冷门和热门物品,给予了它们同样的权重来影响相似度。由于热门物品只占所有物品的很小部分,而且很容易出现不同用户有相同的热门物品重合度,故越热门的物品越无法反映用户的独特的兴趣。
- 所以,为了 降低热门物品的权重,用户的相似度 计算公式更新为: ,其中 是喜欢物品 的用户数量,能够反映物品的热门程度。(对数中+1 的作用是为了避免负数的出现,相似度的值域 。)
UserCF 召回的完整流程
同样的,在线上进行 UserCF 召回之前,需要 事先做离线计算,存储计算结果并建立索引:
- 建立”用户 → 物品“的索引:用户物品,兴趣分数
(1)记录每个用户最近点击、交互过的物品 ID;
(2)给定任意用户 ID,可以利用索引找到该用户近期感兴趣的物品列表 。- 建立”用户 → 用户“的索引:用户最相似的个用户相似度
(1)计算用户之间的两两相似度;
(2)对于每个用户,索引该用户最相似的 个用户 ;
(3)给定任意用户 ID,可以利用索引快速找到该用户最相似的 个用户。
基于事先建立的索引,实现 线上召回:
- 给定用户 ID,通过”用户 → 用户“索引,找到 相似的用户;
- 对于每个 相似用户,通过”用户 → 物品“的索引,找到用户近期感兴趣的物品列表 ;
- 对于所有取回的相似物品(最多有 个),用公式预估用户对每个物品的兴趣分数;
- 返回分数最高的若干个物品作为召回结果。
Tip:进行线上召回时,ItemCF 先通过用户 ID 索引该用户的感兴趣物品,再利用物品 ID 索引所有相似物品;UserCF 则先通过用户 ID 所有相似用户,再利用用户 ID 索引用户感兴趣的物品。两者的召回索引使用顺序有所区别,最后都是利用公式计算兴趣分数排序得到召回结果。
矩阵补全与最近邻查找
离散特征处理
在介绍矩阵补全模型之 前,先说明在推荐系统中做离散特征处理的常见方法:
- 建立字典:把离散特征的所有类别映射成序号。
- 向量化:把序号映射成向量,包括:
(1)one-hot 编码:把序号映射成高维稀疏向量;
(2)embedding(嵌入):把序号映射成低维稠密向量。
在前面通过 ItemCF 和 UserCF 进行召回时,在计算相似度时就是将用户和物品表示成了稀疏向量。但是在实际应用中,用户和物品的数量是极其巨大的,过于稀疏的向量计算复杂、计算量过大、存储困难,所以 embedding 是业界常用的一种方式。TensorFlow、PyTorch 等提供 embedding 层,这里不再介绍。
矩阵补全
矩阵补全, 顾名思义就是将一个含有缺失值的矩阵通过一定的方法将其补全为一个完全的矩阵。那么在推荐系统里,要用什么方式补全何种矩阵? 如下所示是基于 embedding 做推荐的模型:

模型训练所用数据集为(用户 ID,物品 ID,兴趣分数)的集合,记作 ,模型基本思想和训练流程为:
- 用户 embedding 参数矩阵记为 ,第 号用户对应矩阵第 列,用户 ID 通过 embedding 层映射为向量 ;物品 embedding 参数矩阵记为 ,第 号物品对应矩阵第 列,物品 ID 通过 embedding 层映射为向量 ;
- 计算内积 ; 即是第 号用户对第 号物品兴趣的预估值;
- 训练模型的目的是学习矩阵 和 ,使得预估值 拟合 真实观测的兴趣分数,所以需要求解如下最优化问题,得到 和 的参数: 。
由此,可分析何为矩阵补全模型:

如图所示,该矩阵即是通过求内积得到的用户对物品的兴趣分数矩阵。因为系统曝光给用户的物品只有很小部分,所以这个矩阵中只有绿色位置有用户对物品的兴趣分数;而因为系统没有曝光给用户某些物品,对应灰色位置没有分数。矩阵补全 就是利用绿色位置的数据集来训练模型,利用训练好的模型再来预估灰色位置的兴趣分数,以实现补全用户对物品的兴趣分数矩阵。换言之,系统想要预估用户对未曝光物品的兴趣分数,可以通过已有用户对曝光物品的兴趣分数数据集训练模型,利用这个模型实现预估。
但是,矩阵补全模型在实际落地效果并不理想,所以才有后续譬如双塔模型的提出。其主要问题在于:
- 仅仅使用 ID 做 embedding,没有考虑物品和用户的属性(多维特征)
- 负样本的选取方式不对。将曝光之后没有点击交互的物品当作负样本是错误的。在双塔模型里会详细讲解常用的正负样本选取方式。
- 做训练的方法不对。使用内积不如计算余弦相似度;损失函数用平方损失(预估值拟合真实观测分数,回归问题)不如使用交叉熵损失(分类问题)。
最近邻查找
在矩阵补充模型做完模型训练后,需要保存训练得到的矩阵 和 ,一般用 key-value 表(字典)存储矩阵 的列为 用户: ,而矩阵 的存储和索引比较复杂。存储好模型后就需要展开线上服务,在线进行召回,主要流程为:
- 根据用户 ID 作为 key 查询 key-value 表,得到该用户的向量 ;
- 最近邻查找:查找用户最有可能感兴趣的 个物品,作为召回结果。
(1)第 号物品的 embedding 向量为 ;
(2)内积 是用户对第 号物品兴趣的预估;
(3)返回内积最大的 个物品。
Milvus、Faiss、HnswLib 等系统是支持最近邻查找的,衡量最近邻查找的标准也不仅仅限于欧式距离最小、向量内积最小、向量夹角余弦最大。
但最近邻查找存在显著的问题,它暴力枚举的所有方法,时间复杂度正比于物品数量,在实际落地很难应用(小红书的笔记数量以亿为单位)。所以,前人提出了诸多加速最近邻查找的算法。王老师在课程中以余弦相似度为例,说明了一种加速算法的思路:
近似最近邻查找的例子
- 有 个物品通过 embedding 得到物品向量,如下图所示,将其向量分布区域划分为 个小区域(余弦划分为扇形区域), 每个区域使用一个单位向量表示;
- 以该区域的表示向量作为 key,区域中所有物品(数量为 )的 列表为 value, 个区域就有 个索引;
- 在线上快速做推荐,分别计算用户向量 与所有索引向量的相似度(时间复杂度为 );
- 找到相似度最高的索引,再分别计算用户向量 与该索引区域包含的所有 个物品向量的相似度(时间复杂度为 );
- 选取最相似的 个点,作为召回结果。
显然在计算过程中,使用暴力枚举的时间复杂度为 ;加速算法将 (几亿)个物品分别划分到 个(几万)区域,每个区域只有 个(几万)物品,时间复杂度为 ,计算量远远小于暴力枚举。

双塔模型
模型介绍
上一小节介绍了矩阵补全模型,但是它存在诸多问题。因此业界提出了落地有效的双塔模型:

双塔模型左右分别是 用户塔 和 物品塔,分别用于生成表示用户和物品的特征向量;充分使用了 用户和物品的多维特征属性;使用 余弦相似度 替代内积计算预估用户对物品的兴趣分数。如下图所示(以用户塔为例),这两个塔的结构一致,仅是特征变换的方法和神经网络的参数存在差异。

值得注意的是,通过大量实践证明,适用于 召回 的双塔模型是先分别通过神经网络生成表征,再计算两个表征之间的相似度(兴趣分数),称之为 后期融合,;而在 精排和粗排 中,则需要使用 前期融合,也就是先融合物品和用户的特征向量,再通过神经网络输出兴趣分数。
模型训练
小红书推荐系统的模型训练方法主要包括如下表所示三种:
| 训练方法 | 选取样本 | 基本思想(训练目标) | 损失函数 |
|---|---|---|---|
| Pointwise | 独立看待每个正负样本 | 1. 把召回看作二元分类任务 2. 鼓励用户与正样本的余弦相似度接近+1; 3. 鼓励用户与负样本的余弦相似度接近-1。 | 交叉熵损失 |
| Pairwise | 每次取一个正样本、一个负样本 | 鼓励用户与正样本的余弦相似度大于鼓励用户与负样本的余弦相似度。 | 合页损失 |
| Listwise | 每次取一个正样本、多个负样本 | 1. 鼓励用户与正样本的余弦相似度尽量大; 2. 鼓励用户与每个负样本的余弦相似度都尽量小。 | Sampled Softmax Loss |
这里在解释其中 合页损失 和 Sampled Softmax Loss 的注释内容前,先做如下定义:
- 某个用户的特征向量,记作 ;
- 某个物品正样本的特征向量,记作 ;某个物品负样本的特征向量,记作 ;
- 个物品负样本的特征向量分别记作 ;
- 函数用于计算用户和物品余弦相似度。
现逐条分析注释内容:
合页损失:
- 根据 Pairwise 的思想 ,希望实现 。如果 时,没有损失;否则损失等于 。由此推理的合页损失为 Triplet Hinge Loss,公式表达为: ,其中 是用户和物品样本之间的差距(margin)。
- 另有一种被称为 Triplet Logistic Loss 的损失函数也可以实现 ,公式如下: 。
Sampled Softmax Loss:如下图所示,实际上 Listwise 也是将召回看成二分类任务。 是模型输出的余弦相似度通过 Softmax 激活函数转化的结果; 是样本的真实标签,正样本标 签为 1,负样本标签为 0。推理得到的损失函数为:

最后,使用梯度下降法,减少损失函数。
正负样本
在推荐系统中,一般将 曝光而且有点击的“用户-物品” 二元组作为训练的 正样本(用户对物品感兴趣)。但是存在一个实际问题,少部分物品受到了用户的大部分点击(热门物品),导致了正样本大多都是热门物品。所以,在选择正样本时,需要 过采样冷门物品,或者 降采样热门物品。
但是负样本的选取需要合理且正确,错误的负样本选取会影响模型召回效果。负样本 主要根据其分类难度,被分为 简单负样本 和 困难负样本,如下表所示:
| 负样本类型 | 负样本来源 | 选取思想 |
|---|---|---|
| 简单负样本 | 全体物品 | 1. 未被召回的物品,大概率是用户不感兴趣的,分类准确较高(简单)。因为被召回的物品只是很小一部分,所以未被召回的物品近似等 于全体物品。 2. 非均匀抽样,打压热门物品。抽样概率正比为为点击次数的 0.75 次方。 |
| 简单负样本 | Batch 内负样本 | 1. 一个 Batch 内有 n 个正样本(用户-物品对)。任意选取一个用户和 n-1 个物品组成负样本,则该 Batch 内有 n(n-1)个负样本。 2. 一个物品出现在 Batch 内的概率正比于点击次数,增大了热门物品成为负样本的概率,必须进行纠偏*。 |
| 困难负样本 | 被排序淘汰的物品 | 用户感兴趣但是兴趣不强的物品,分类比较困难。 |
| 困难负样本 | 精排分数靠后的物品 | 用户非常感兴趣但是排名靠后的物品,分类非常困难。 |
| 无用负样本(排序可用) | 曝光但没有点击的物品 | 该物品作为负样本训练召回是错误的,但是可以作为排序的负样本。 |
纠偏*:因为在 Batch 内物品 被抽样的概率 正比于点击次数,需要通过纠 偏来降低热门物品成为负样本的概率。设预估用户对物品 的兴趣分数为 ,则仅在训练时,将其调整为 。
在进行训练时,常见的作法是 混合 以上几种简单和困难的负样本,比如:
- 50%的负样本是全体物品(简单负样本);
- 50%的负样本是就没有通过排序的物品(困难负样本)。
线上召回和更新
与前面介绍的其他模型相同,在进行线上召回之前,双塔模型需要先事先进行计算、建立索引,通过数据库 离线存储物品向量:
- 完成训练后,用物品塔计算每个物品的特征向量 ;
- 把几亿个物品向量 存入向量数据库;
- 向量数据库建立索引,以便加速最近邻查找。
再进行 线上召回,查找用户最感兴趣的 个物品:
- 给定用户 ID 和画像(用户多维特征属性),线上用神经网络(用户塔)计算用户向量 ;
- 最近邻查找:
(1)把向量 作为 query,调用向量数据库做最近邻查找;
(2)返回余弦相似度最大的 个物品,作为召回结果。
这里需要提前计算存储物品向量 而只需线上计算用户向量 的原因在于:
- 每做一次召回所用到的用户向量数量为 1,而物品向量数量为几亿,线上计算物品向量的代价太大。
- 用户的兴趣是动态变化的,而物品特征相对稳定。虽然可以离线存储用户向量,但是不利于实时的推荐结果。
部署好模型后并非一劳永逸了,还需要根据前一段时间的数据结果做模型的更新,更新主要有 全量更新 和 增量更新 两种,如下表所示:
| 更新类型 | 全量更新 | 增量更新 |
| 基本思想 | 今天凌晨,用昨天全体的数据,在昨天模型参数的基础上做模型训练。 | 用户兴趣会随时变化,几十分钟就会更新用户兴趣。故做 online learing 更新模型参数。 |
| 基本流程 | 1. 用昨天的数据训练 1 个 epoch,每天数据只用一遍; 2. 发布新的用户塔神经网络和物品向量,供线上使用; | 1. 实时收集线上数据,做流式处理,生成 TFRecord 文件; 2. 对模型做 online learning,增量更新用户 ID embedding 参数; 3. 发布用户 ID embedding,供用户塔在线上计算用户向量。 |
| 注意事项 | 1. 基于使用昨天的全体的数据以及昨天的全量模型,不是随机初始化模型,也不是昨天最后的增量模型。 2. 全量更新对数据量、系统的要求比较低。 | 增量更新只更新用户 ID embedding 层参数,不更新神经网络其他部分参数。 |
那么可以只做增量更新而不做全量更新吗?显然不能:
- 增量更新不同时间段的用户行为不一样,从统计学的角度出发,以天为 总体,小时级的数据是有偏的,而分钟级别的数据偏差更大;
- 全量更新 random shuffle 了一天的数据,而增量更新按照数据从早到晚的顺序做训练。一般来说,训练时,随机打乱是优于按顺序排列的数据。故全量训练往往优于增量训练
自监督学习
基于双塔模型的推荐系统存在严重的头部效应:因为少部分物品占据大部分点击次数,大部分物品点击次数不多,所以推荐系统对高点击物品的表征学的好,对长尾物品的表征学习困难。所以,引用自监督学习的思想,做 data argumentation,可以更好地学习长尾物品的表征。
基本思想与训练目标

这里的物品塔是同一个塔,共享参数。后面会补上一个高相似度的线路
召回中的 自监督学习,更像是设立一个辅助任务,帮助系统学到更多已有数据的监督信息(调整塔的参数),有效解决长尾问题;同时应用不同的特征变化能得到更多的向量表征,起到了数据增强或扩充数据集的作用。它的训练目标为:
- 希望物品 的两个向量表征 和 有较高的相似度,鼓励 尽量大;
- 希望物品 和物品 的任意向量表征(如 和 )都有较低的相似度,鼓励 尽量小。
特征变换
在召回中也常用到深度学习中的多种特征变换方法,如下表所示:
| 特征变换 | 基本思想 | 示例 |
|---|---|---|
| Random Mask | 随机挑选一些离散特征,把它们遮住。 | 处理前物品的特征集合 u ={ID、类目};处理后 u ={default}。 |
| Dropout | 随机丢弃特征中 50%的值(仅对多值离散特征生效)。 | 处理前某特征的向量 a = [1, 2];处理后 a = [1, 0] 。 |
| 互补特征 | 将若干个特征随机分为两组,两组都是物品表征且特征互不重复。因为表示的都是同一物品,所以鼓励这两个特征向量相似。 | 某物品具有 ID、类目、关键词、城市 4 个特征,随机分成 {ID,default,关键词,default} 和 {default,类目,default,城市} 这两种物品表征。 |
| Mask 一组关联的特征 | 1. 使用互信息(MI,mutual information)衡量关联度,离线计算特征两两之间的关联矩阵,根据关联矩阵进行特征的随机 Mask。 2. 它比以上三种方法效果都要好,但是比较复杂,实现难度大且不容易维护。 | 1. 设一共有 k 种特征,离线计算特征两两之间的 MI,得到 k×k 的矩阵; 2. 随机选一个特征作为 seed,找到该 seed 最相关的 k/2 种特征; 3. Mask 该 seed 及其最相关的 k/2 种特征,保留其余 k/2 种特征。 |
互信息:设特征 1 的类别集合为 ,特征 2 的类别集合为 , 为某特征取值为 的概率, 为某特征取值为 、另一个特征取值为 的概率。则互信息计算公式: 。
模型训练
结合前文描述,自监督学习的训练流程如下:
- 从全体物品中均匀抽样,得到 个物品,作为一个 batch;
- 做两类特征变换,物品塔输出两组向量 和 ;
- 第 个物品的损失函数为: ;
- 做梯度下降,减小自监督学习的损失: 。
为了解决长尾问题,现将自监督学习加入到双塔模型的训练之中:
- 对点击物品做随机采样,得到 对“用户-物品”二元组,作为一个 batch;
- 从全体物品中均匀抽样,得到 个物品,作为一个 batch;
- 做梯度下降,使得损失(双塔模型损失+自监督学习损失)减小: ,其中 是超参数,用于调整自监督学习的影响程度。
Deep Retrieval
召回:用户 → 路径 → 物品
- 给定用户特征 ,用神经网络预估用户对路径 的兴趣,分数记作
- 用 beam search 寻找分数 最高的 s 条 path
- 利用索引
path->List<item>召回每条路径上的 n 个物品 - 一共召回 个物品,对物品做初步排序,返回分数最高的 若干物品
与双塔模型的区别:
- Deep Retrieval 是以路径作为用户和物品的中介
- 双塔模型则是以向量表征作为用户和物品的中介
训练
同时学习 用户-路径 和 物品和路径
- 一个物品被表征为 条路径:
- 如果用户点击过物品,则更新神经网络参数,使分数增大:
- 如果用户对路径的兴趣分数 较高,且用户点击过物品
item, 则item与path具有相关性高 - 寻找与 item 最相关的 条 path,且避免一条路径上物品过多
其他召回通道
在小红书的推荐系统中,还有如下表所示的几种简单召回通道:
| 召回通道 | 原理 | 索引 | 召回流程 |
|---|---|---|---|
| GeoHash 召回 | 用户可能对附近发生的事感兴趣,根据用户定位的 GeoHash 取回该地点最新的若干篇笔记。 | GeoHash→ 优质笔记列表(按时间倒排) | 无个性化召回(所以选择推荐优质笔记) |
| 同城召回 | 用户可能对同城发生的事情感兴趣。 | 城市 → 优质笔记列表(按时间倒排) | 无个性化召回 |
| 作者召回 | 用户对关注的作者发布的笔记感兴趣。 | 用户 → 关注的作者 作者 → 发布的笔记(按时间倒排) | 用户 → 关注的作者 → 最新的笔记 |
| 有交互的作者召回 | 如果用户对某笔记感兴趣(点赞、收藏、转发),那么用户可能对该作者的其他笔记也感兴趣。 | 1. 用户 → 有交互的作者 2*. 作者 → 发布的笔记(按时间倒排) | 用户 → 有交互的作者 → 最新的笔记 |
| 相似作者召回 | 如果用户喜欢(关注、交互)某作者,那么用户喜欢相似的作者(类似 ItemCF)。 | 1*. 用户 → 感兴趣的作者 2. 作者 → 相似作者 3*. 作者 → 发布的笔记(按时间倒排) | 用户 → 感兴趣的作者 → 相似作者 → 最新的笔记 |
Tip: 在视频中,有交 互和相似作者召回这两者召回通道的索引都只写了一条,个人根据召回理解进行了补充(带*号)。本人理解作者的意思在于,按照介绍顺序,先前的召回通道已经建立了这些索引,所以仅列举出新的召回通道需要添加的新索引。也可能在实际中,小红书的推荐系统无需这些这些索引,可以直接进行实时的线上计算。此处存在争议,请以视频为准。
除此之外,还有一种被称为缓存召回的召回方法。在精排到重排的过程中,从几百篇笔记中只筛选了几十篇作为推荐结果,大部分精排结果并没有被曝光。因此,缓存召回 希望复用前 次推荐精排但没有曝光的结果,将它们缓存起来,作为一条召回通道。但是缓存的大小固定,所以需要设置 退场机制,如下是几个简单的退场机制例子:
- 一旦笔记成功曝光,就从缓存退场;
- 如果超过缓存大小,就移除最先进入缓存的笔记;
- 笔记最多被召回若干次,达到这个次数就退场;
- 每篇笔记最多保存若干天,达到这个天数就退场;
- 想让低曝光笔记缓存更长时间,基于曝光次数设置退场规则。
曝光过滤 & Bloom Filter
曝光过滤
在小红书和抖音的推荐系统中,为了避免重复推荐物品,如果用户看过某个物品,则系统不再把该物品曝光给该用户,这个思想称之为 曝光过滤。一般来说,曝光过滤是在召回层实现的,它的基本流程如下:
- 对于每个用户,记录已经曝光给他的物品。(小红书只召回 1 个月以内的笔记,因此只需要记录每个用户最近 1 个月的曝光历史)
- 对于每个召回的物品,判断它是否已经给该用户曝光过,排除掉曾经曝光过的物品。

Bloom Filter
如果一位用户看过 个物品,本次召回 个物品。如果暴力对比需要的时间复杂度为 ,计算过于巨大。所以在实践中使用 Bloom Filter 来判断一个物品 ID 是否在已经曝光的物品集合中:
- 如果判断为 no,那么该物品一定不在集合中;
- 如果判断为 yes,那么该物品很可能在集合中(可能误伤,错误判断未曝光物品为已曝光,将其过滤掉)。
如下所示是 Bloom Filter 的实现原理:
- Bloom Filter 把物品集合表征为一个 维的二进制向量;
- 每个用户有一个曝光物品的集合,表征为一个向量,需要 bit 的存储空间;
- Bloom filter 有 个哈希函数,每个哈希函数把物品 ID 映射成介于 和 之间的整数;
- 以下图( )为例,已曝光物品 ID 通过哈希函数映射进了二进制向量中对应的位置,如果该位置为 0 则调整为 1,若为 1 无需调整;
- 对于召回的物品 ID,通过哈希函数映射,若对应位置全为 1,则说明该物品已曝光,否则未曝光。

根据前面的描述,Bloom Filter 明显会产生误判。设曝光物品集合大小为 ,二进制向量维度为 ,使用 个哈希函数,则 Bloom Filter 的误伤概率 为 :
- 越大,向量中的 1 越多 ,误伤概率越大(未曝光物品的个位置恰好都是 1 的概率大);
- 越大, 向量越长,越不容易发生哈希碰撞;
- 太大、太小都不好, 有最优取值;
- 若人为设定可容忍的误伤概率为 ,那么最优参数为: , 。这里的推导可 参考一位大佬的文章。
除了会产生误判外,Bloom Filter 还有一个缺点。在每次往集合内添加一个物品,只需要把向量 个位置的元素置为 1。因此如果删除一个物品时,就无法直接将 个位置的元素置为 0,这会删掉向量中其他物品的信息。所以,Bloom Filter 只支持添加物品,不支持删除物品。如果从集合中移除物品,则无法消除它对向量已经发生过的影响。