【上】王树森《小红书推荐系统公开课》- 课程笔记(推荐系统基础、召回、排序)

写在前面

本文为王树森老师《小红书推荐系统公开课》的课程笔记

由于篇幅较长,分为【上】【下】两篇文章来记录。其中【上】包括推荐系统基础、召回、排序,【下】包括特征交叉、行为序列、重排、物品冷启动、涨指标的方法

【上】部分,内容导航如下:


(一)推荐系统基础

1、推荐系统的基础概念

打开小红书APP,默认打开 "发现" 页面,展示推荐系统分发给你的内容,即用户自己创作的笔记,将它们展示给其他用户,形成陌生人社交的社区

小红书推荐系统的转化流程

推荐系统决定给用户曝光什么内容,用户自己决定是否点击、滑动到底、...

抖音没有曝光和点击,用户下滑一次只能看到一个视频

短期消费指标

反映出用户对推荐是否满意

  • 点击率 = 点击次数 / 曝光 次数
    • 越高说明推荐越精准
    • 不能作为唯一优化目标(标题党)
  • 点赞率 = 点赞次数 / 点击 次数
    • 收藏率转发率同理
  • 阅读完成率 = 滑动到底次数 / 点击 次数 * 归一化函数(跟笔记长度有关)
    • 笔记越长,完成阅读的比例就越低
    • 若没有归一化函数,对长笔记会不公平

短期消费指标容易竭泽而渔,用户很快会失去兴趣,不再活跃。故还需关注多样性(用户没看过的话题),有助于提高用户黏性,留住用户,让用户更活跃

北极星指标

衡量推荐系统好坏最重要 的指标,根本标准

  • **用户规模:**日活用户数DAU(只要今天登录了一次小红书就增加了一次DAU)、月活用户数MAU(只要这个月登录了一次小红书就增加了一次MAU)
  • 消费: 人均使用推荐的时长、人均阅读笔记的数量
    • 通常与点击率涨跌一致。若矛盾,应以北极星指标为准,如增加多样性,点击率不一定涨,但时长可能变多
  • 发布: 发布渗透率、人均发布量
    • 激励作者发布通常是由冷启动来负责

实验流程

  • 做离线实验不需要把算法部署到产品中,没有跟用户实际交互,没有占用线上流量
  • 离线实验结果没有线上实验结果可靠(北极星指标都是线上指标,只能通过线上实验获得)
  • **AB测试:**把用户随机分成实验组和对照组,实验组用新策略,对照组用旧策略,对比两者的业务指标,判断新策略是否会显著优于旧策略。若是,可以加大流量,最终推全

2、推荐系统的链路

推荐系统的目标是从物品的数据库中选出几十个物品展示给用户

召回(几亿->几千)

从物品的数据库中快速取回一些物品

几十条召回通道,每条召回通道取回几十~几百个物品,一共取回几千个物品作为候选集

  • 召回通道:协同过滤、双塔模型、关注的作者等
  • 融合、去重、过滤(排除掉用户不喜欢的作者、笔记、话题等)

让排序决定该把哪些物品曝光给用户,以及展示的顺序。为了解决计算量的问题,将排序分为粗排和精排(二者非常相似,但精排模型更大,用的特征更多)

粗排(几千->几百)

规模较小的机器学习模型给几千个物品逐一打分,按照分数做排序和截断,保留分数最高的几百个物品送入精排(也会用一些规则,保证进入精排的笔记具有多样性)

  • 排序分为粗排和精排,平衡计算量和准确性
  • 召回和粗排是最大的漏斗

精排(几百->几百)

大规模深度神经网络给几百个物品逐一打分,做排序和截断(截断可选)

  • 精排相比粗排用的特征更多,计算量更大,模型打分更可靠
  • 把多个预估值做融合(比如加权和)得到最终的分数,分数决定会不会展示给用户,以及展示的位置

重排(几百->几十)

根据精排分数和多样性分数做随机抽样,得到几十个物品。用规则把相似内容打散,并插入广告和运营推广内容,根据生态要求调整排序,即为最终展示给用户的结果

  • 重排主要是做多样性抽样 (如MMR、DPP)、规则打散、插入广告和运营内容
    • 做多样性抽样(比如MMR、DPP),从几百个物品中选出几十个
    • 用规则打散相似物品
    • 插入广告、运营推广内容,根据生态要求调整排序

整条链路上,召回和粗排是最大的漏斗(候选物品从几亿->几千->几百)

3、AB测试

目的:

  • 离线实验的指标有提升,不代表线上实验也会有收益。判断新策略能带来多大的业务指标收益
  • 模型中有一些参数,需要用AB测试选取最优参数,如GNN网络的深度可以是1/2/3层

离线实验结果正向,下一步是做线上的小流量AB测试(一般10%用户)

随机分桶

若用户数量足够大,每个桶的DAU/留存/点击率等指标都是相等的

例:召回团队实现了一种GNN召回通道,离线实验结果正向。下一步是做线上的小流量A/B测试,考虑新的召回通道对线上指标的影响。模型中有一些参数,比如GNN的深度取值∈{1, 2, 3},需要用A/B测试选取最优参数

  • 1~3桶的GNN深度分别为1~3层,4号桶没有用GNN做召回
  • 计算每个桶的业务指标,比如DAU、人均使用推荐的时长、点击率等
  • 如果某个实验组指标显著优于对照组,则说明对应的策略有效,如2桶的业务指标与对照组的diff有显著性,说明2层的GNN召回通道是有效果的,值得推全(把流量扩大到100%,给所有用户都使用2层GNN召回通道)

分层实验

目标:解决流量不够用的问题

  • 信息流产品的公司有很多部门和团队,都需要做AB测试
    • 推荐系统(召回、粗排、精排、重排)
    • 用户界面
    • 广告
  • 如果把用户随机分成10组,1组做对照,9组做实验,那么只能同时做9组实验,无法满足产品迭代需求

分层实验:召回、粗排、精排、重排、用户界面、广告...(例如GNN召回通道属于召回层)

  • 同层互斥: GNN实验占了召回层的4个桶,其他召回实验只能用剩余的6个桶
    • 两个召回实验不会同时作用到同一个用户上
    • 避免一个用户同时被两个召回实验影响,若两个实验相互干扰,实验结果将变得不可控
  • 不同层正交: 每一层独立随机对用户做分桶,每一层都可以独立用100%的用户做实验
    • 一个用户可以同时受一个召回实验和一个精排实验的影响,因为它们的效果不容易相互增强或抵消

互斥 vs 正交:

  • 如果所有实验都正交,则可以同时做无数组实验
  • 同类的策略(例如精排模型的两种结构)天然互斥,对于一个用户,只能用其中一种
  • 同类的策略(例如添加两条召回通道)效果会相互增强或相互抵消。互斥可以避免同类策略相互干扰
  • 不同类型的策略(例如添加召回通道、优化粗排模型)通常不会相互干扰,可以作为正交的两层

Holdout机制

  • 每个实验(召回、粗排、精排、重排)独立汇报对业务指标的提升
  • 公司考察一个部门(比如推荐系统)在一段时间内对业务指标总体的提升
  • 取10%的用户作为holdout桶(对照组),推荐系统使用剩余90%的用户做实验,两者互斥
  • 10% holdout桶 vs 90% 实验桶的diff(需要对指标做归一化)为整个部门的业务指标收益
  • 每个考核周期结束之后,清除holdout桶,让推全实验从90%用户扩大到100%用户
  • 重新随机划分用户,得到holdout桶和实验桶,开始下一轮考核周期
  • 新的holdout桶与实验桶各种业务指标的diff接近0
  • 随着召回、粗排、精排、重排实验上线和推全,diff会逐渐扩大

实验推全

若业务指标的diff显著正向,则可以推全实验。如重排策略实验,取一个桶作为实验组,一个桶作为对照组,实验影响了20%用户,若观测到显著正向的业务收益,则可以推全

  • 把重排层实验关掉,把两个桶空出来给其他实验
  • 推全时新开一层,新策略会影响全部90%用户
  • 在小流量阶段,新策略会影响10%用户,会微弱提升实验桶和hold桶的diff
  • 推全后,新策略作用到90%用户上,diff会扩大9倍
  • 如ab测试发现新策略会提升点击率9个万分点,小流量实验只作用10%用户上,所以只能把跟holdout桶的diff提升1个万分点,推全后,理论上可以把diff提升到9个万分点,跟ab实验得到的结果一致

反转实验

  • 有的指标(如点击、交互)立刻受到新策略影响,有的指标(留存)有滞后性,需要长期观测
  • 实验观测到显著收益后需要尽快推全新策略。目的是腾出桶供其他实验开展,或需要基于新策略做后续的开发
  • 用反转实验解决上述矛盾,既可以尽快推全,也可以长期观测实验指标
  • 在推全的新层中开一个旧策略的桶,可以把反转桶保留很久,长期观测实验指标
  • 一个考核周期结束之后,会清除holdout桶,会把推全新策略用到holdout用户上,不影响反转桶;当反转实验完成时,新策略会用到反转桶用户上,实验真正推全,对所有用户生效
  • **分层实验:**同层互斥(不允许两个实验同时影响同一位用户)、不同层正交(实验有重叠的用户)
  • **Holdout:**保留10%的用户完全不受实验影响,可以考虑整个部门对业务指标的贡献
  • **实验推全:**扩大到90%流量上,新建一个推全层,与其它层正交
  • **反转实验:**为了在尽早推全新策略的同时还能长期观测各种指标,在新的推全层上,保留一个小的反转桶,反转桶使用旧策略。反转桶可以保留很久,长期观测新旧策略的diff

(二)召回

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

ItemCF的原理

我喜欢看《笑傲江湖》,《笑傲江湖》与《鹿鼎记》相似,我没看过《鹿鼎记》------> 给我推荐《鹿鼎记》

推荐系统如何知道《笑傲江湖》与《鹿鼎记》相似?

  • 基于知识图谱:
    • 两本书的作者相同,故两本书相似
  • 基于全体用户的行为:
    • 看过《笑傲江湖》的用户也看过《鹿鼎记》
    • 给《笑傲江湖》好评的用户也给《鹿鼎记》好评

ItemCF的实现

假设点击、点赞、收藏、转发四种行为各1分
算出来=3.2

逐一计算用户对候选物品的兴趣分数,返回分数高的topn个物品

物品相似度

两个物品的受众重合度越高,两个物品越相似

  • 例如,喜欢《射雕英雄传》和《神雕侠侣》的读者重合度很高,可以认为它们相似

相似度介于0~1,数值越大表示两个物品越相似

上述公式只要是喜欢就看作1,不喜欢就看作0,没有考虑用户喜欢的程度
集合v这部分用户占比越大,两个物品的相似度就越高

考虑用户喜欢的程度,如点击、点赞、收藏、转发各自算1分,用户对物品的喜欢程度最多是4分

  • 分子表示同时喜欢两个物品的用户v(如果兴趣分数的取值是0或1,那么分子就是同时喜欢两个物品的人数)
  • 分母第一项表示用户对物品i1的兴趣分数,关于所有用户求连加,然后开根号
  • 把一个物品表示成一个向量,向量每个元素表示一个用户,元素的值就是用户对物品的兴趣分数,两个向量的夹角的余弦就是这个公式

相似度介于0~1

**ItemCF的基本思想:**根据物品的相似度做推荐

  • 如果用户喜欢物品Item1,而且物品Item1和Item2相似
  • 那么用户很可能喜欢物品Item2

预估用户对候选物品的兴趣:

计算两个物品的相似度:

  • 把每个物品表示为一个稀疏向量,向量每个元素对应一个用户
  • 相似度sim就是两个向量夹角的余弦

ItemCF召回的完整流程

为了能在线上实时推荐,必须要事先做离线计算,建立两个索引

事先做离线计算

建立 "用户->物品" 的索引

  • 记录每个用户最近点击、交互过的物品ID
  • 给定任意用户ID,可以找到他近期感兴趣的物品列表

建立 "物品->物品" 的索引

  • 计算物品之间两两相似度
  • 对于每个物品,索引它最相似的k个物品
  • 给定任意物品ID,可以快速找到它最相似的k个物品
线上做召回
  1. 给定用户ID,通过 "用户->物品" 的索引,找到用户近期感兴趣的物品列表(last-n)
  2. 对于last-n列表中每个物品,通过"物品->物品" 的索引,找到top-k相似物品
  3. 对于取回的相似物品(最多有nk个),用公式预估用户对物品的兴趣分数
  4. 返回分数最高的100个物品,作为推荐结果(即ItemCF召回通道的输出,会跟其他召回通道的输出融合起来并排序,最终展示给用户)

索引的意义在于避免枚举所有的物品

  1. 记录用户最近感兴趣的n=200个物品
  2. 取回每个物品最相似的k=10个物品
  3. 给取回的nk=2000个物品打分(用户对物品的兴趣)
  4. 返回分数最高的100个物品作为ItemCF通道的输出

用索引,离线计算量大(需要更新2个索引),线上计算量小(不需访问上亿个物品)

ItemCF召回通道的输出,会跟其他召回通道的输出融合起来做排序

如果取回的物品ID有重复的,就去重,并把分数加起来

ItemCF的原理:

  • 用户喜欢物品i1,那么用户喜欢与物品i1相似的物品i2
  • 物品相似度:
    • 不是根据物品的内容判定物品相似,而是根据用户行为
    • 如果喜欢i1、i2的用户有很大的重叠,那么i1与i2相似

ItemCF召回通道:

  • 维护两个索引:
    • 用户->物品列表:用户最近交互过的n个物品
    • 物品->物品列表:相似度最高的k个物品
  • 线上做召回:
    • 利用两个索引,每次取回nk个物品
    • 预估用户对每个物品的兴趣分数:
    • 返回分数最高的100个物品,作为召回结果

2、Swing召回通道(ItemCF的变种)

与ItemCF非常像,区别就是如何定义物品的相似度

ItemCF:

ItemCF的问题: 两篇笔记受众不同,但由于被分享到一个小圈子,导致很多用户同时交互过这两篇笔记。需要降低小圈子用户的权重

Swing模型即给用户设置权重,解决小圈子问题

α是个人工设置的参数;overlap为u1和u2的重叠,重叠大说明两个人是一个小圈子,对相似度的贡献应减小

Swing与ItemCF唯一的区别在于物品相似度

  • **ItemCF:**两个物品重合的用户比例高,则判定两个物品相似
  • Swing: 额外考虑重合的用户是否来自一个小圈子
    • 同时喜欢两个物品的用户记作集合v
    • 对于v中的用户u1和u2,重合度记作overlap(u1, u2)
    • 两个用户重合度大,则可能来自一个小圈子,权重降低

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

UserCF原理

有很多跟我兴趣非常相似的网友,其中某个网友对某笔记点赞、转发,而我没看过这篇笔记,那么可能给我推荐这篇笔记

推荐系统如何找到跟我兴趣非常相似的网友呢?

  • 点击、点赞、收藏、转发的笔记有很大的重合
  • 关注的作者有很大的重合

UserCF实现

0代表用户没有看过物品,或对物品不感兴趣
计算出=2.8

用户相似度

用户有共同的兴趣点,即喜欢的物品有重合
介于0-1,越接近1表示两个用户越相似

上述公式同等对待热门和冷门的物品,需降低热门物品的权重

物品越热门, 越大,分子(即物品的权重)越小

UserCF的基本思想:

  • 如果用户user1跟用户user2相似,而且user2喜欢某物品,那么用户user1也很可能喜欢该物品

预估用户user对候选物品item的兴趣:

计算两个用户的相似度:

  • 把每个用户表示为一个稀疏向量,向量每个元素对应一个物品
  • 如果用户对物品不感兴趣,向量元素为0;若感兴趣,元素为1,或1除以物品的热门程度
  • 相似度sim就是两个向量夹角的余弦

UserCF召回的完整流程

为了能在线上实时推荐,必须要事先做离线计算,建立两个索引

事先做离线计算

建立 "用户->物品" 的索引

  • 记录每个用户最近点击、交互过的物品ID
  • 给定任意用户ID,可以找到他近期感兴趣的物品列表

建立 "用户->用户" 的索引

  • 对于每个用户,索引他最相似的k个用户
  • 给定任意用户ID,可以快速找到他最相似的k个用户
线上做召回
  1. 给定用户ID,通过 "用户->用户" 的索引,找到topk相似用户
  2. 对于每个top-k相似用户,通过**"用户->物品" 的索引**,找到用户近期感兴趣的物品列表(last-n)
  3. 对于取回的nk个相似物品,用公式预估用户对每个物品的兴趣分数
  4. 返回分数最高的100个物品,作为召回结果

若取回的物品有重复的,去重,并把分数加起来

UserCF的原理:

  • 用户u1跟用户u2相似,而且u2喜欢某物品,那么u1也可能喜欢该物品
  • 用户相似度:
    • 如果用户u1和u2喜欢的物品有很大的重叠,那么u1和u2相似
    • (分母是做归一化,让相似度分数介于0~1之间)

UserCF召回通道:

  • 维护两个索引:
    • 用户->物品列表:用户近期交互过的n个物品
    • 用户->用户列表:相似度最高的k个用户
  • 线上做召回:
    • 利用两个索引,每次取回nk个物品
    • 预估用户user对每个物品item的兴趣分数:
    • 返回分数最高的100个物品,作为召回结果

--------------------------------------------- 分割线,上面都是协同过滤召回 ---------------------------------------

4、离散特征处理

  • 协同过滤召回(以上内容)
  • 向量召回(之后内容)

离散特征

如性别、国籍、英文单词、物品ID、用户ID

处理:

  • **建立字典:**把类别映射成序号
  • 向量化: 把序号映射成向量
    • One-hot编码:把序号映射成高维稀疏向量,序号对应位置的元素为1,其他位置元素都是0
    • Embedding:把序号映射成低维稠密向量

One-Hot编码

**性别:**男、女两种类别

  • 字典:男->1,女->2
  • One-hot编码:用2维向量表示性别
    • 未知 -> 0 -> [0, 0]
    • 男 -> 1 -> [1, 0]
    • 女 -> 2 -> [0, 1]

**国籍:**中国、美国、印度等200种类别

  • 字典:中国->1,美国->2,印度->3,...
  • One-hot编码:用200维稀疏向量表示国籍
    • 未知 -> 0 -> [0, 0, 0, 0, ..., 0]
    • 中国 -> 1 -> [1, 0, 0, 0, ..., 0]
    • 美国 -> 2 -> [0, 1, 0, 0, ..., 0]
    • 印度 -> 3 -> [0, 0, 1, 0, ..., 0]

局限:类别数量太大时,通常不用one-hot编码

Embedding(嵌入)

把每个类别映射为一个低维的稠密向量

参数数量 = 向量维度 × 类别数量

  • 设Embedding得到的向量都是4维的
  • 一共有200个国籍
  • 参数数量=800

编程实现:TensorFlow、Pytorch提供Embedding层

  • 参数以矩阵的形式保存,矩阵大小是向量维度×类别数量
  • 输入是序号,比如美国的序号是2
  • 输出是向量,比如美国对应参数矩阵的第2列

两者联系

Embedding其实就是矩阵向量乘法,类似全连接层 **离散特征处理:**One-hot编码、embedding

类别数量很大时,用embedding,如:

  • word embedding
  • 用户ID embedding
  • 物品ID embedding

------------------------------------------------- 分割线,下面都是向量召回 ------------------------------------------

5、矩阵补充(Matrix Completion)、最近邻查找

矩阵补充(Matrix Completion)是向量召回最简单的一种,已经不太常用,是为了帮助理解双塔模型

矩阵补充模型结构

输出是一个实数(用户对物品兴趣的预估值),越大表示用户对物品越感兴趣

  • 左边的Embedding Layer:矩阵列的数量为用户数量,每一列都是向量a这样的对用户的表征
  • 右边的Embedding Layer
  • 两个Embedding Layer不共享参数

矩阵补充模型训练

(1)基本想法:
矩阵A和B是Embedding层的参数

**(2)数据集:**是很多三元组的集合

**(3)训练:**让模型的输出拟合兴趣分数

  • (u, i, y)三元组 是训练集中的一条数据,表示 用户u 对 物品i 的 真实兴趣分数y
  • 对目标函数求min,可以用随机梯度下降,每次更新A和B的一列

(4)直观解释:

绿色位置 的数据训练模型,预估所有灰色位置的分数。给定一个用户,选出用户对应行中分数较高的物品,推荐给用户

矩阵补充的缺点

矩阵补充在实践中效果不好,在工业界不work

  • 缺点1: 仅用ID Embedding,没利用物品、用户属性
    • 物品属性:类目、关键词、地理位置、作者信息
    • 用户属性:性别、年龄、地理定位、感兴趣的类目
    • 双塔模型可以看做矩阵补充的升级版,会结合物品属性和用户属性
  • 缺点2: 负样本的选取方式不对
    • 样本:用户-物品的二元组,记作(u, i)
    • 正样本:曝光之后,有点击、交互(正确的做法)
    • 负样本:曝光之后,没有点击、交互(错误的做法)
  • 缺点3: 做训练的方法不好
    • 内积 <au, bi> 不如余弦相似度
    • 用平方损失(回归),不如用交叉熵损失(分类)

矩阵补充线上服务

模型存储:

  1. 训练得到矩阵A和B。A的每一列对应一个用户,B的每一列对应一个物品
  2. 把矩阵A的列存储到key-value表,key是用户ID,value是A的一列。给定用户ID,返回一个向量(用户Embedding)
  3. 矩阵B的存储和索引比较复杂,不能简单地用key-value存储

线上服务:

近似最近邻查找(Approximate Nearest Neighbor Search)

避免暴力枚举(暴力枚举计算量正比于物品数量),近似最近邻查找的结果未必最优,但不会比最优差多少

支持最近邻查找的系统:

  • 系统:Milvus、Faiss、HnswLib,等
  • 衡量最近邻的标准:
    • 欧式距离最小(L2距离)
    • 向量内积最大(内积相似度):矩阵补充用的就是这个
    • 向量夹角余弦最大(cosine相似度):最常用,即夹角最小

有些系统不支持余弦相似度,可以把所有向量都做归一化,让它们的二范数等于1,那么内积就等于余弦相似度
每个点是一个物品的Embedding向量,a为用户的Embedding向量

在做线上服务前,把数据划分为很多区域:

  • cos相似度,划分结果为扇形
  • 欧式距离,划分结果为多边形

划分后,每个区域用一个向量表示,这些向量的长度都是1。划分区域后,建立索引,key为每个区域的向量,value为区域中所有点的列表

首先计算用户a与索引向量的相似度,通过索引找出该区域内的所有点,再计算用户a与该区域所有点的相似度

矩阵补充:

  • 把物品ID、用户ID做embedding,映射成向量
  • 两个向量的内积<au, bi>作为 用户u 对 物品i 兴趣的预估
  • 让<au, bi>拟合真实观测的兴趣分数,用回归的方式学习模型的embedding层参数
  • 矩阵补充模型有很多缺点,效果不好。工业界不用矩阵补充,而是用双塔模型

线上召回:

  • 把用户向量a作为query,查找使得<a, bi>最大化的物品i(内积最大的 top k 物品i)
  • 暴力枚举速度太慢,实践中用近似最近邻查找
  • Milvus、Faiss、HnswLib等开源向量数据库支持近似最近邻查找

6、双塔模型(DSSM)

双塔模型可以看做是矩阵补充模型的升级

模型结构

用户离散特征:

  • 所在城市、感兴趣的话题等:Embedding,不同的离散特征要用不同的Embedding层得到向量
  • 性别:对类别数量很少的离散特征,用One-hot编码即可,可以不做Embedding

**用户连续特征:**年龄、活跃程度、消费金额

  • 归一化:让特征的均值为0,标准差为1
  • 有些长尾分布的连续特征需要特殊处理,如取log、分桶

神经网络可以是简单的全连接网络,也可以是更复杂的深度交叉网络

物品的表征类似处理

双塔模型使用了ID之外的多种特征作为输入,输出介于-1~1的余弦相似度(余弦相似度相当于先对两个向量做归一化,再求内积,比内积更常用

正负样本的选择

  • **正样本:**历史记录显示用户喜欢的物品。如用户点击的物品
  • 负样本: 用户不感兴趣的物品
    • 没有被召回的?
    • 召回但是被粗排、精排淘汰的?
    • 曝光但是未点击的? ------ 不能作为召回的负样本,可以作为排序的负样本

训练方法

pairwise: Jui-Ting Huang et al. Embedding-based Retrieval in Facebook Search. In KDD, 2020.(Facebook)

listwise:
Xinyang Yi et al. Sampling-Bias-Corrected Neural Modeling for Large Corpus Item
Recommendations. In RecSys , 2019.(YouTube)

  1. **pointwise:**独立看待每个正样本、负样本,做简单的二元分类。即把正样本和负样本组成一个数据集,在数据集上做随机梯度下降训练双塔模型
  2. **pairwise:**每次取一个正样本、一个负样本,组成一个二元组,损失函数为triplet hinge loss或triplet logistic loss
  3. **listwise:**每次取一个正样本、多个负样本,组成一个list,训练方式类似于多元分类
pointwise训练
  • 把召回看做二元分类任务
  • 对于正样本,鼓励 cos(a,b) 接近+1
  • 对于负样本,鼓励 cos(a,b) 接近-1
  • 控制正负样本数量为1:2或者1:3(行业经验)
pairwise训练

输入一个三元组,用户的特征向量为a,两个物品的特征向量为b+和b-。两个物品塔共享参数

让用户对正样本的兴趣分数尽量高(最好接近+1),对负样本的兴趣分数尽量低(最好接近-1)

**(1)Triplet Hinge Loss:**孪生网络
m是个超参数

**(2)Triplet Logistic Loss:**让cos(a, b-)尽量小,让cos(a,b+)尽量大
σ是个>0的超参数,控制损失函数的形状

listwise训练

最大化正样本的余弦相似度,最小化负样本的余弦相似度

不适用于召回的模型

上图为粗排/精排的模型,不能应用于召回

  • 前期融合,在进入全连接层之前就把特征向量拼起来了 ------ 排序模型
  • 双塔模型属于后期融合,两个塔在最终输出相似度时才融合 ------ 召回模型

前期融合的模型不适用于召回,否则要把所有物品的特征挨个输入模型,预估用户对所有物品的兴趣,无法使用近似最近邻查找来加速计算。通常用于排序(从几千个物品中选出几百个)

双塔模型:

  • 用户塔、物品塔各输出一个向量
  • 两个向量的余弦相似度作为兴趣的预估值,越大,用户越有可能对物品感兴趣
  • 三种训练方式:
    • pointwise:每次用一个用户、一个物品(可正可负)
    • pairwise:每次用一个用户、一个正样本、一个负样本。训练目标是最小化triplet loss,即让正样本余弦相似度尽量大,负样本余弦相似度尽量小
    • listwise:每次用一个用户、一个正样本、多个负样本。训练时用softmax激活+交叉熵损失,让正样本余弦相似度尽量大,负样本余弦相似度尽量小

正负样本

选对正负样本的作用 > 改进模型结构

正样本:

  • **正样本:**曝光而且有点击的用户-物品二元组(用户对物品感兴趣)
  • **问题:**二八法则,少部分物品占据大部分点击,导致正样本大多是热门物品。拿过多热门物品当正样本,会对冷门物品不公平,让热门更热、冷门更冷
  • 解决方案: 过采样冷门物品,或降采样热门物品
    • 过采样(up-sampling):一个样本出现多次
    • 降采样(down-sampling):一些样本被抛弃。如以一定概率抛弃热门物品,抛弃的概率与样本的点击次数正相关

**负样本:**用户不感兴趣的物品,或链路上每一步被淘汰的物品

简单负样本:未被召回物品

(1)全体物品:从全体物品中做非均匀抽样,作为负样本

未被召回的物品,大概率是用户不感兴趣的。几亿个物品里只有几千个被召回,故未被召回的物品 ≈ 全体物品
0.75是个经验值

使用非均匀抽样,热门物品成为负样本的概率大

(2)batch内负样本

图中是一个batch内的样本。训练时要鼓励正样本的余弦相似度尽量大,负样本的余弦相似度尽量小

一个物品成为负样本的概率越大,模型对这个物品打压就会越狠
修正偏差(纠偏),避免过分打压热门物品 这样理解Batch内负样本的修正:p越小,-logp越大,softmax结果s更高。如果是正样本(s>0.5),那导数会偏小(输出已经很接近正确标签,不希望模型做出太大的调整);如果是负样本,那导数会偏大。就形成了对低频物品(p小)更强的负样本的倾向,相当于是放大了冷门负样本的梯度

-logp算是物品的先验,模型实际上是非常容易拟合先验的,所以要debias掉

注意,线上召回时,还是用原本的余弦相似度,不用做这种调整,不用减掉 logpi

困难负样本:粗排/精排淘汰
  • 被粗排淘汰的物品 (比较困难)
  • 精排分数靠后的物品 (非常困难)

训练双塔模型,其实是对正负样本做二元分类:

  • 全体物品(简单)分类准确率高
  • 被粗排淘汰的物品 (比较困难) 容易分错
  • 精排分数靠后的物品 (非常困难) 更容易分错
训练数据

混合几种负样本,50%的负样本是从全体物品随机非均匀抽样得到(简单负样本),50%的负样本是没通过粗排精排的物品,即从粗/精排淘汰的物品中随机抽样得到(困难负样本)

常见的错误

选择负样本的原理:

  • 召回的目标: 快速找到用户可能感兴趣的物品,再交给后面的排序逐一甄别
    • 全体物品(easy): 绝大多数是用户根本不感兴趣的
    • 被排序淘汰(hard): 用户可能感兴趣,但是不够感兴趣。这样的负样本做分类时比较难以区分,所以是困难负样本
    • 有曝光没点击(不能作为负样本): 用户感兴趣,可能碰巧没有点击【 可以作为排序的负样本,不能作为召回的负样本 】

**正样本:**曝光而且有点击的用户-物品二元组

简单负样本:

  • 全体物品中做非均匀随机抽样
  • batch内负样本

**困难负样本:**被召回,但是被排序淘汰(跟正样本很像,做分类会有困难)

错误: 曝光但是未点击的物品做召回的负样本。这类负样本只能用于排序,不能用于召回

线上服务/线上召回

训练好的两个塔,分别提取用户特征和物品特征

  • 向量数据库存储物品特征向量和ID的二元组,用作最近邻查找
  • 不要事先计算和存储用户向量。当用户发起推荐请求时,调用神经网络现算一个特征向量a,然后把向量a作为query,去向量数据库中检索查找最近邻

**离线存储:**把物品向量b存入向量数据库

  • 完成训练之后,用物品塔计算每个物品的特征向量b
  • 把几亿个物品向量b存入向量数据库,比如Milvus、Faiss、HnswLib
  • 向量数据库建索引(把向量空间划分成很多区域,每个区域用一个向量表示),以便加速最近邻查找

**线上召回:**查找用户最感兴趣的k个物品

  • 给定用户ID和画像,线上用神经网络算用户向量a
  • **最近邻查找:**把向量a作为query,调用向量数据库做最近邻查找,返回余弦相似度最大的k个物品,作为召回结果
事先存储物品向量b,线上现算用户向量a,为什么?
  • 每做一次召回,用到一个用户向量a,几亿物品向量b(线上算物品向量的代价过大)
  • 用户兴趣动态变化,而物品特征相对稳定 (可以离线存储用户向量,但不利于推荐效果)

训练好双塔模型之后,在开始线上服务之前,先要做离线存储,在向量数据库建好索引之后,可以做线上召回

跟ItemCF、Swing、UserCF等召回通道的结果融合

模型更新

全量更新(天级别)

今天凌晨,用昨天全天的数据训练模型

  • 在昨天模型参数的基础上做训练(不是随机初始化)
    • 用昨天全天的数据(要先random shuffle打乱,然后打包成TFRecord)训练1个epoch,即每条数据只过一遍
  • 训练完成后,发布新的用户塔神经网络(在线上实时计算用户向量,作为召回的query)和物品向量(存入向量数据库,向量数据库会重新建索引,在线上做最近邻查找),供线上召回使用
  • 全量更新对数据流、系统的要求比较低
    • 不需要实时的数据流,对生成训练数据的速度没有要求,延迟一两个小时也没关系。只需要把每天的数据落表,在凌晨做个批处理,把数据打包成TFRecord格式即可
    • 每天只需要把神经网络和物品向量发布一次即可
  • 只有做全量更新时才更新全连接层
增量更新(分钟级别)

做online learning更新模型参数,每隔几十分钟就要把新的模型参数发布出去,刷新线上的用户塔Embedding层参数

  • 用户兴趣会随时发生变化
  • 实时收集线上数据,做流式处理,实时生成训练模型用的TFRecord文件
  • 对模型做online learning,梯度下降增量更新ID Embedding参数 (不更新神经网络其他部分的参数,全连接层是锁住的,不做增量更新;只有全量更新会更新全连接层参数)
  • 发布用户ID Embedding(哈希表),供用户塔在线上计算用户向量,可以捕捉用户最新的兴趣点。这个过程会有延迟,但可以被缩短到几十分钟
全量 vs 增量

今天凌晨的全量更新,是基于昨天凌晨全量训练出来的模型,而不是用下面增量训练出来的模型。在完成这次全量之后,下面增量训练出来的模型就可以扔掉了

能否只做增量更新,不做全量更新?
  • 小时级数据有偏,统计值跟全天数据差别很大(不同时间段用户的行为不同);分钟级数据偏差更大
  • 全量更新: random shuffle (为了消除偏差)一天的数据,做 1 epoch 训练
  • **增量更新:**按照数据 从早到晚的顺序 ,做 1 epoch 训练
  • 随机打乱 优于 按顺序排列数据 , 全量训练 优于 增量训练

全量训练的模型更好,而增量训练可以实时捕捉用户的兴趣

双塔模型:

  • 用户塔、物品塔各输出⼀个向量,两个向量的余弦相似度作为兴趣的预估值,越大越好
  • 三种训练的方式:pointwise、pairwise、listwise
  • 正样本:用户点击过的物品
  • 负样本:从全体物品中随机抽样 (简单负样本)、被排序淘汰的物品 (困难负样本)

召回:

  • 做完训练,把物品向量存储到向量数据库,供线上最近邻查找
  • 线上召回时,给定用户ID、用户画像,调用训练好的用户塔神经网络 现算用户向量a
  • 把a 作为query,查询物品的向量数据库,找到余弦相似度最高的k个物品向量,返回k个物品ID

**更新模型:**定期全量+实时增量

  • 全量更新: 今天凌晨,用昨天的数据训练整个神经网络,做 1 epoch 的随机梯度下降(每条数据只用一遍)
  • 增量更新:用实时数据训练神经网络,只更新 ID Embedding,锁住全连接层不更新
  • 实际的系统:全量更新(每天) & 增量更新(实时) 相结合。每隔几十分钟,发布最新的用户 ID Embedding,供用户塔在线上计算用户向量(好处:捕捉用户的最新兴趣点)

7、双塔模型+自监督学习

自监督学习: 改进双塔模型的方法,能提升业务指标,目的是把物品塔训练的更好

双塔模型的问题

  • 推荐系统的头部效应 严重:
    • 少部分物品占据大部分点击
    • 训练双塔模型时用点击数据作为正样本,模型学习物品的表征靠的就是点击行为
    • 大部分物品的点击次数不高
  • 高点击物品的表征学得好,长尾物品的表征学的不好(长尾物品的曝光和点击次数太少,训练的样本数量不够)
  • 自监督学习:对物品做data augmentation,更好地学习长尾物品的向量表征

双塔模型的训练(同时训练用户塔和物品塔)

用batch内负样本

  • 一个batch内有n对正样本
  • 组成n个list,每个list中有1对正样本和n-1对负样本
listwise训练

用户a,物品b

让模型给正样本打的分数尽量高,给负样本打的分数尽量低

**损失函数:**考虑batch内第i个用户和全部n个物品
listwise训练双塔模型的损失函数

纠偏

batch内负样本会过度打压热门物品,造成偏差。如果用batch内负样本,需要纠偏,使热门物品不至于被过分打压。训练结束,在线上做召回时,还是用原本的余弦相似度

训练双塔模型
  • i:第i个用户
  • n:batch内一共有n个用户

自监督学习(训练物品塔)

  • 双塔模型的listwise训练方式,同时训练用户塔和物品塔
  • 自监督学习训练物品塔

Tiansheng Yao et al. Self-supervised Learning for Large-scale Item Recommendations. In CIKM, 2021. 【Google】

尽管对应不同的特征变换,但同一物品的两个向量表征应有较高的相似度

不同物品的向量表征的分布应尽量分散开

如何变换物品特征

四种方法:

  1. Random Mask(随机选一些离散特征,将它们的值换为default)
  2. Dropout(仅对多值离散特征生效)
  3. 互补特征(将物品的多个特征随机分组,鼓励两组特征形成的物品表征向量相似)
  4. Mask一组关联的特征(计算互信息来衡量特征两两之间的关联。随机选一个特征,mask该特征及其最相关的k/2种特征,其余保留)

(1)Random Mask

一个物品可以有多个类目

  • 如果不做random mask,正常的特征处理方法是对数码和摄影分别做embedding,得到两个向量,再做求和或平均,得到一个向量,表征物品类目
  • 如果对类目特征做mask,这个物品的类目特征就变成了default。default表示默认的缺失值,对default做embedding,得到一个向量表征类目。也就是说,做mask后,物品的类目特征直接被丢掉(数码和摄影都没了)

(2)Dropout(仅对多值离散特征生效)

**多值离散特征:**离散特征 + 一个物品可以有多个该特征的取值

random mask是对整个类目特征的取值都丢掉,dropout只丢掉类目特征取值的50%

(3)互补特征(complementary)
由于是对同一物品的表征,训练时要鼓励cos相似度尽量大

正常的做法是把四种特征的值分别做embedding,然后拼起来输入物品塔,最终得到物品的向量表征

(4)mask一组关联的特征

特征之间有较强的关联,遮住一个特征并不会损失太多的信息,模型可以从其他强关联特征中学到遮住的特征

两个特征关联越强,p(u,v)就比较大,它们的互信息就越大

  • **好处:**效果好,推荐的主要指标比random mask、dropout、互补特征等方法效果更好
  • **坏处:**方法复杂,实现的难度大,不容易维护(每添加一个新的特征,都需要重新计算所有特征的MI)
如何用变换后的特征训练模型

每个物品被表征为2个向量

这里冷热门物品被抽样到的概率是相同的(注意跟训练双塔的区别。训练双塔得到的数据是根据点击行为抽样的,热门物品被抽到的概率大。双塔抽的是用户-物品二元组,这里只抽物品)

考虑batch中第i个物品的特征向量bi',和全部m个物品的特征向量b'':

**训练时希望向量si接近yi,说明物品塔训练的好,即使做随机特征变换,对物品的向量表征也影响不大。**用yi和si的交叉熵作为损失函数

双塔模型存在的问题:

  • 双塔模型学不好低曝光物品的向量表征(其实这是数据的问题,而不是双塔模型的问题。真实推荐系统都存在头部效应,小部分物品占据了大部分的曝光和点击)

Google提出一种自监督学习的方法,用在双塔模型上效果很好(低曝光物品、新物品的推荐变得更准

  • 对一个物品用两种特征变换,物品输出两个特征向量
  • 同一物品的两个不同特征变换得到的向量应该有高相似度
  • 不同物品应让物品的向量表征尽量分散在整个特征空间上

训练:

  • 对点击做随机抽样,得到n对用户-物品二元组作为一个batch。这个batch用来训练双塔,包括用户塔和物品塔。根据点击做抽样,热门物品被抽到的概率高
  • 从全体物品中均匀抽样,得到m个物品作为另一个batch。热门和冷门物品被抽到的概率是相同的,这个batch用来做自监督学习,只训练物品塔
  • 做梯度下降使损失函数减小。α决定自监督学习起到的作用

8、Deep Retrieval召回

Deep Retrieval(字节):
Weihao Gao et al. Learning A Retrievable Structure for Large-Scale Recommendations. In
CIKM , 2021.
TDM(阿里): Han Zhu et al. Learning Tree-based Deep Model for Recommender Systems. In KDD , 2018.

  • 经典的双塔模型把用户、物品表示为向量,线上做向量最近邻查找
  • Deep Retrieval把物品表征为路径(path),线上查找用户最匹配的路径,从而召回一批物品(类似于阿里的TDM,但更简单)

Deep Retrieval和TDM都用深度学习,但都不做向量最近邻查找

索引:关联路径和物品

L1~L3表示结构的3层(深度depth),每一层里面有K个节点(宽度width)

路径就是几个节点连接起来,路径可以有重合的节点

**物品->路径的索引:**训练神经网络时要用到

  • 一个物品对应多条路径
  • 用3个节点表示一条路径:path = [a,b,c]

**路径->物品的索引:**线上做召回时要用到

  • 一条路径对应多个物品(给定一条路径,会取回很多个物品作为召回的结果)

预估模型-神经网络(寻找用户感兴趣的路径)

给定用户特征,预估用户对路径的兴趣分数(根据用户特征,召回多条路径)

预估用户对路径的兴趣:

假设结构有3层

过程:
Deep Retrieval结构:3层,每层K个节点

  • 如果结构的每一层有k个节点,那么p1就是一个k维向量
  • 向量p1对应L1层,向量p1的k个元素是神经网络对L1层k个节点打的分。分数越高,节点就越有可能被选中(用Beam Search的方法。假设选中节点a)
  • 向量x不变,直接作为下一层的输入,对节点a做embedding,与x做concat,输入另一个神经网络

3层神经网络不共享参数

  • 假设根据向量p1,从k个节点中选出节点a。把向量x和节点a一起送入下一层神经网络,输出k维向量p2(对应结构中的第二层)
  • 同理选出节点b,把向量x和节点a、b一起输入下一层神经网络。从3层中各选出一个节点,组成路径 [a,b,c]

线上召回:user->path->item

给定用户特征,召回一批物品

用Beam Search召回一批路径:

beam size(每层选的节点数量)越大,计算量越大,但同时search的结果也会越好(最简单的情况即beam size=1)

Beam Search相当于贪心算法,选中的节点分别最大化p1、p2、p3,但独立对p1、p2、p3求最大化,未必会最大化这三项的乘积
p1、p2、p3分别是三层神经网络的输出

选中路径path=[5,4,1]

size大一些,结果会比贪心算法好一些,但计算量也变大

步骤:

  • 第一步:给定用户特征,用神经网络做预估,用beam search召回一批路径
  • 第二步:利用索引,召回一批物品
    • 查看索引path->item(在线上做召回之前,线下已经把路径和物品匹配好了)
    • 每条路径对应多个物品
  • 第三步:对物品做排序,选出一个子集作为最终召回结果
    • 用小的排序模型来打分排序,如双塔模型,防止物品超出召回通道的配额

离线训练(同时学习神经网络参数和物品表征)

神经网络 可以判断用户对物品的兴趣,物品表征则是把物品映射到路径

训练时只用正样本,用户-物品二元组,只要用户点过物品就算正样本

学习神经网络参数

判断用户对路径有多感兴趣
x是用户的特征

学习物品表征

**解释:**图中的用户都点击过左边的物品,如果其中很多用户也对路径感兴趣(兴趣分数是神经网络预估出来的),我们就判断物品和路径有很强的关联,可以把路径作为物品的表征
用户是物品和路径之间的中介

最小化损失,相当于根据score对path做排序,排序结果的top j,即对于每个物品item,选择score最高的j条路径,作为物品的表征

为了防止非常多的物品集中在一条路径上的情况,希望每条path上的item数量比较平衡,需要用正则项约束path。如果某条path上已经有了很多item,这条path就会受到惩罚,避免关联到更多物品

物品与J条路径的相关性越高,损失函数就越小;正则reg控制路径l上的物品不要太多

交替训练神经网络和物品表征

训练神经网络时,把物品当做中介,将用户和路径关联起来

**线上召回:**用户->路径->物品(两阶段,先用神经网络寻找用户感兴趣的路径,路径长度与神经网络层数相关;再取回路径上的物品)

这里也说明了为什么要用正则项控制一条path上的item不能太多,否则有时候召回的物品数量会特别多,有时候会特别少
Deep Retrieval召回 的本质是用路径作为用户和物品之间的中介

双塔模型召回 的本质是用向量表征作为用户和物品之间的中介
离线训练: 同时学习 用户-路径物品-路径 的关系

避免让索引失去平衡

9、其他召回通道

地理位置召回

GeoHash召回(附近召回)

用户可能对附近发生的事感兴趣

  • GeoHash: 对经纬度的编码(把经纬度编码为二进制哈希码),地图上一个长方形区域(以GeoHash作为索引,记录地图上一个长方形区域内的优质笔记)
  • **索引:**GeoHash -> 优质笔记列表(按时间倒排,即最新的笔记排在最前面)
  • 召回时,给定用户的GeoHash,会取回这个区域内比较新的一些优质笔记

召回纯粹只看地理位置,完全不考虑用户兴趣(就是因为没有个性化,才需要考虑优质笔记。笔记本身质量好,即使没有个性化,用户也很可能会喜欢看;反之,既没有个性化,又不是优质笔记,召回的笔记大概率通不过粗精排)

同城召回

用户可能对同城发生的事感兴趣

  • 根据用户所在城市和曾经生活过的城市做召回
  • 索引:城市 -> 优质笔记列表(按时间倒排)

这条召回通道没有个性化

作者召回

关注的作者召回

用户对关注的作者发布的笔记感兴趣

  • **索引:**用户->关注的作者,作者->发布的笔记(按时间顺序倒排,新发布的笔记排在最前面)
  • **召回:**用户->关注的作者->最新的笔记
有交互的作者召回

即使没关注作者,如果推送有交互作者发布的笔记也可能会感兴趣而继续看

如果用户对某笔记感兴趣(点赞、收藏、转发),那么用户可能对该作者的其他笔记感兴趣

  • **索引:**用户->有交互的作者
  • **召回:**用户->有交互的作者->最新的笔记

作者列表需要定期更新,保留最新交互的作者,删除一段时间没有交互的作者

相似作者召回

如果用户喜欢某作者(感兴趣的作者包括用户关注的作者+用户有交互的作者),那么用户喜欢相似的作者。取回每个作者最新的一篇笔记

相似性的计算类似于ItemCF:如果两个作者的粉丝有很大重合,那么两个作者相似

缓存召回

**想法:**复用前n次推荐精排的结果(精排前50但是没有曝光的,缓存起来,作为一条召回通道)

  • 精排输出几百篇笔记,送入重排;重排做多样性的随机抽样(如DPP),选出几十篇
  • 精排结果一大半没有曝光,被浪费

**问题:**缓存大小固定(比如最多存100篇笔记),需要退场机制。有很多条规则作为退场机制,如

  • 一旦笔记成功曝光,就从缓存退场
  • 如果超出缓存大小,就移除最先进入缓存的笔记
  • 笔记最多被召回10次,达到10次就退场
  • 每篇笔记最多被保存3天,达到3天就退场

还能再细化规则,如想要扶持曝光比较低的笔记,那么可以根据笔记的曝光次数 来设置规则,让低曝光的笔记在缓存中存更长时间

三大类、六条召回通道:

  • 地理位置召回: 用户对自己附近的人和事感兴趣
    • GeoHash召回、同城召回
  • 作者召回:
    • 关注的作者、有交互的作者、相似的作者
  • **缓存召回:**把精排中排名高、但是没有成功曝光的笔记缓存起来,再多尝试几次

10、曝光过滤 & Bloom Filter

曝光过滤通常在召回阶段 做,具体的方法就是用Bloom Filter(给用户曝光过等价于用户看过

曝光过滤问题

实验表明,重复曝光同一个物品会损害用户体验(抖音、小红书);但YouTube这样的长视频就没有曝光过滤,看过的可以再推荐

在小红书,n和r的量级都是几千,暴力对比的计算量太大。实践中不做暴力对比,而是用Bloom Filter

Bloom Filter

判断一个物品ID是否在已曝光的物品集合中

  • 如果判断为no,那么该物品一定不在集合中
  • 如果判断为yes,那么该物品很可能在集合中(可能误伤,错误判断未曝光物品为已曝光,将其过滤掉)

**概括一下:**根据Bloom Filter的判断做曝光过滤,肯定能干掉所有已经曝光的物品,用户绝对不可能重复看到同一个物品;但是有一定概率会误伤,召回的物品明明没有曝光过,却被Bloom Filter给干掉了

Burton H. Bloom. Space/time trade-offs in hash coding with allowable
errors. Communications of the ACM, 1970.

Bloom Filter是一种数据结构

  • 把物品集合表征为一个m维二进制向量(每个元素是一个bit,要么是0要么是1)
  • 每个用户有一个曝光物品的集合,表征为一个向量,需要m bits的存储
  • 有k个哈希函数,每个哈希函数把物品ID映射成介于 0~m-1 之间的整数
  • k和m都是需要设置的参数

最简单的情况:k=1(只有1个哈希函数)
初始向量全0

向量的某个位置已经是1的话,不需要修改数值

  • 如果Bloom Filter认为没有曝光,那么这个物品肯定没有曝光(不可能把已曝光的物品误判为未曝光)
  • 如果Bloom Filter认为已曝光,有可能是正确的,有可能是误判(有一定概率把未曝光物品认为已曝光,导致未曝光物品被过滤掉)

k=3,用3个不同的哈希函数把物品ID映射到3个位置上,把3个位置的元素都置为1。同样地,如果向量的某个位置元素已经是1,不需要修改数值
每个曝光物品只需要占几个bits的存储

如果物品已经曝光,那么其3个位置肯定都是1,如果其中某个位置为0,说明该物品实际未曝光

Bloom Filter误伤的概率:

用户历史记录上有n个曝光物品 ,只需要 m=10n bits ,就可以把误差概率降低到1%以下

推荐系统中曝光过滤的链路
  • **推荐系统的链路:**多路召回、粗排、精排、重排,最终选出一批物品曝光给用户
  • **曝光过滤的链路:**把曝光的物品记录下来,更新Bloom Filter,用于过滤召回的物品

APP前端有埋点,所有曝光的物品都会被记录下来。但这个落表的速度要足够快,在用户推荐界面下一刷之前,就要把本次曝光的结果写到Bloom Filter上,否则下一刷很可能会出重复的物品,所以要用实时流处理,比如把曝光物品写入Kafka消息队列,用Flink做实时计算

Flink实时读取Kafka消息队列,计算曝光物品的哈希值,把结果写入Bloom Filter的二进制向量。用这样的实时数据链路,在曝光发生几秒之后,这位用户的Bloom Filter就会被修改,之后就能避免重复曝光

曝光过滤用在召回完成之后。召回服务器请求曝光过滤服务 ,曝光过滤服务把用户的二进制向量发送给召回服务器 ,在召回服务器上用Bloom Filter计算召回物品的哈希值,再跟二进制向量对比,把已经曝光的物品过滤掉,剩余的物品都是未曝光的,发送给排序服务器

Bloom Filter缺点

小红书首页只推荐年龄小于1个月的物品

只需要计算出k个哈希值(k是哈希函数的数量)

删除物品,不能简单把这个物品对应的k个元素从1改为0,否则会影响其他物品。 这是因为向量的元素是所有物品共享的,如果把向量的一个元素改为0,相当于把很多物品都移除掉了。想要删除一个物品,需要重新计算整个集合的二进制向量

推荐系统的曝光过滤问题、Bloom Filter数据结构


(三)排序

1、多目标排序模型

推荐系统的链路

  • 有很多条召回通道,从几亿篇笔记中取出几千篇
  • 从中选出用户最感兴趣的,要用到粗排和精排,粗排给召回的笔记逐一打分,保留分数最高的几百篇
  • 然后用精排 模型给粗排选出的几百篇笔记打分,**不做截断,**让几百篇笔记全都带着精排分数进入重排
  • 重排 做多样性抽样,把相似内容打散,最终有几十篇笔记被选中,展示给用户

粗排、精排原理基本相同,只是粗排模型小,特征少,效果差一些,粗排的目的是做快速的初步筛选(如果不用粗排,直接把很大的精排模型用在几千篇候选笔记上,计算代价大)

排序的依据

以小红书为例,排序的主要依据是用户对笔记的兴趣,兴趣可以反映在用户与笔记的交互上。对于每篇笔记,系统记录以下统计量:

  • 曝光次数(impression):一篇笔记被展示给多少用户
  • 点击次数(click):一篇笔记被多少用户点开
  • 点赞次数(like)
  • 收藏次数(collect)
  • 转发(share)

可以用点击率之类的消费指标衡量一篇笔记受欢迎的程度

  • 点击率 = 点击次数 / 曝光次数
  • 点赞率 = 点赞次数 / 点击次数(收藏率、转发率同理)。用户点开笔记之后才会发生点赞、收藏、转发等行为,故分母是点击次数,而不是曝光次数

排序依据:

  • 在把笔记展示给用户之前,需要事先预估用户对笔记的兴趣(排序模型预估点击率、点赞率、收藏率、转发率等多种分数);
  • 融合这些预估分数,比如加权和(比如点击率的权重是1,点赞率、收藏率、转发率的权重是2),权重是做ab测试调出来的;
  • 根据融合的分数做排序、截断,保留分数高的笔记

简单的多目标模型

排序模型的输入是各种各样的特征

  • **用户特征:**用户ID、用户画像
  • **物品特征:**物品ID、物品画像、作者信息(笔记的作者)
  • 统计特征:
    • ​​​​​​​​​​​​​​用户统计特征(用户在过去30天中一共曝光/点击...了多少篇笔记)
    • 物品统计特征(候选物品在过去30天中一共获得了多少次曝光/点击...机会)
  • 场景特征: 随着用户请求传过来的,包含
    • 当前时间(如当前是否是周末或节假日)
    • 用户所在地点(候选物品和用户是否在同一城市)

神经网络可以是全连接,或Wide&Deep,等等

输出4个预估值,都介于0~1之间,排序主要依靠这4个预估值

模型训练:

4个任务,每个任务都是一个二元分类,如判断用户是否会点击物品。用交叉熵损失函数
权重αi是根据经验设置的

实际训练中会有很多困难,如类别不平衡(正样本少,负样本多)

  • 每100次曝光,约有10次点击、90次无点击
  • 每100次点击,约有10次收藏、90次无收藏

解决方案: 负样本降采样(down-sampling)

  • 保留一小部分负样本
  • 让正负样本数量平衡,节约计算(比如原本一天积累的数据需要在集群上训练10h,做降采样后,负样本减少很多,训练只需3h)

预估值校准

给定用户特征和物品特征,用神经网络预估出点击率、点赞率等分数之后,要对这些预估分数做校准,做完校准之后才能把预估值用于排序

为什么要做校准?

α(采样率)越小,负样本越少,模型对点击率的高估就会越严重

Xinran He et al. Practical lessons from predicting clicks on ads at Facebook. In the 8th International Workshop on Data Mining for Online Advertising.

对预估点击率的校准公式: 左边的 表示校准之后的点击率,右边是对预估点击率 做的变换,α为采样率

在线上做排序时,首先让模型预估点击率,然后用上述公式做校准,拿校准之后的点击率做排序的依据

排序的多目标模型,用于估计点击率、点赞率等指标。做完预估之后,要根据负采样率对预估值做校准

2、Multi-gate Mixture-of-Experts(MMoE)

模型结构

Google的论文提出MMoE模型:Jiaqi Ma et al. Modeling Task Relationships in Multi-task Learning with Multi-gate Mixture-of-Experts. In KDD, 2018.

模型的输入是一个向量,包含用户特征、物品特征、统计特征、场景特征

  • 3个神经网络结构相同 ,都是由很多全连接层组成,但它们不共享参数
  • 3个神经网络各输出一个向量,即3个"专家"(实践中通常用4个或8个。专家神经网络的数量是个超参数,需要手动调)

左、右边的神经网络也有多个全连接层

  • softmax输出向量的3个元素都>0且相加为1
  • p1~p3分别对应3个专家神经网络,之后会将这3个元素作为权重,对向量x1~x3做加权平均。q1~q3同理

神经网络可以有一个或多个全连接层,其输出取决于具体的任务,比如输出对点击率的预估,是一个介于0~1之间的实数

这里假设多目标模型只有点击率和点赞率两个目标,所以只有两组权重。

对神经网络输出的向量做加权平均,用加权平均得到的向量去预估某个业务指标。若多目标模型有n个目标,就要用n组权重

MMoE问题:极化现象

YouTube的论文提出极化问题的解决方案:Zhe Zhao et al. Recommending What Video to Watch Next: A Multitask Ranking System. In RecSys, 2019.

softmax会发生极化(Polarization):softmax输出值一个接近1,其余接近0

如左边softmax输出值 ≈ [0,0,1],说明左边的预估点击率任务只使用了第三号专家神经网络(没有让三个专家神经网络的输出融合,只是简单使用了一个专家)

那么MMoE就相当于一个简单的多目标模型,不会对专家做融合,失去了MMoE的优势

解决极化问题:

如果有n个专家神经网络,那么每个softmax的输入和输出都是n维向量。不希望看到其中一个输出的元素接近1,其余n-1个元素接近0

在训练时,对softmax的输出使用dropout,这样会强迫每个任务根据部分专家做预测

  • softmax输出的n个数值被mask的概率都是10%
  • 每个专家被随机丢弃的概率都是10%

如果用dropout,不太可能会发生极化,否则预测的结果会特别差。假如发生极化,softmax输出的某个元素接近1,万一这个元素被mask,预测的结果会错得离谱。为了让预测尽量精准,神经网络会尽量避免极化的发生,避免softmax输出的某个元素接近1。用了dropout,基本上能避免极化

用MMoE不一定会有提升,有可能是实现不够好,也有可能是不适用于特定的业务场景

3、预估分数的融合公式

多目标模型输出对点击率、点赞率等指标的预估。怎么融合多个预估分数?介绍工业界排序的几种融分公式

加权和

Pclick表示模型预估的点击率,权重为1;其他以此类推

Pclick*1就是预估的点击率,Pclick*Plike是曝光之后用户点赞的概率

海外某短视频APP的融分公式

Ptime是指预估短视频的观看时长(比如预测用户会观看10s),w1和α1都是超参数,通过线上ab选出合适的值

国内某短视频APP(快手)的融分公式

不是直接用预估的分数,而是用每个分数的排名

α和β都是需要调的超参数。预估的播放时长越长,排名越靠前,越小,最终得分就越高

某电商的融分公式

指数α1~α4是超参数,需要调。若都为1,该公式就表示电商的营收,有很明确的物理意义

4、视频播放建模

播放时长&完播率2个指标

指标1:视频播放时长

  • 图文笔记排序的主要依据:点击、点赞、收藏、转发、评论...
  • 视频排序的依据还有播放时长和完播

视频播放时长是连续变量,但直接用回归拟合效果不好,建议用YouTube的时长建模

Paul Covington, Jay Adams, & Emre Sargin. Deep Neural Networks for YouTube Recommendations. In RecSys, 2016.

神经网络叫做Share Bottom(被所有任务共享),每个全连接层对应一个目标(比如点击、点赞、收藏、播放时长),这里只关心播放时长的预估:

对z(实数,可正可负)做sigmoid变换得到p=sigmoid(z),让p拟合y = t / (1+t)( t表示用户实际观看视频的时长,t越大,y也越大),训练中用p和y的交叉熵作为损失函数【训练完毕后,p就没有用了

CE(y, p) = y*logp + (1-y)*log(1-p)

可以用exp(z)作为播放时长的预估【线上做推理时,只用z的指数函数,把它作为对播放时长t的预估

总结一下,右边的全连接层输出z,对z做sigmoid变换得到p,在训练中用p

用**y(= t / (1+t),其中t为播放时长)**和p的交叉熵作为损失函数训练模型,训练完成后p就没有用了;

线上做推理时,只用z的指数函数,把它作为对播放时长t的预估

建模

实践中把分母1+t去掉也没问题,相当于给损失函数做加权,权重是播放时长

最终把**exp(z)**作为融分公式中的一项,它会影响到视频的排序

指标2:视频完播率

2种建模方法:回归;二元分类

回归方法

y的大小介于0~1之间

二元分类方法

需要算法工程师自己定义完播指标

预估的播放率会跟点击率等指标一起作为排序的依据

有利于短视频,而对长视频不公平

视频越长,函数值f越小,把调整之后的分数记作,可以反映出用户对视频的兴趣,而且对长、短视频是公平的

与播放时长、点击率、点赞率等指标一起,决定视频的排序

视频的排序;视频有两个独特的指标:播放时长完播率

5、排序模型的特征

排序所需特征

用户画像特征(User Profile)

召回和排序的模型中都有用户属性,用户属性记录在用户画像中

  • 用户ID (在召回、排序中做embedding)
    • 用户ID本身不携带有用的信息,但作用很大,通常用32 or 64维向量
  • **人口统计学属性:**性别、年龄等
  • 账号信息: 新老(用户注册时间)、活跃度...
    • 模型需要专门针对新用户和低活用户做优化
  • 感兴趣的类目、关键词、品牌
    • 属于用户兴趣信息,这些信息可以是用户自己填写的,也可以是算法自动提取的
笔记画像特征(Item Profile)
  • 物品ID(在召回、排序中作embedding)
  • 发布时间(或者年龄)
    • 在小红书,一篇笔记发布时间越久,价值就越低,尤其是强时效性话题
  • GeoHash(经纬度编码)、所在城市
    • GeoHash表示地图上一个长方形的区域
  • 标题、类目、关键词、品牌...
    • 笔记的内容,通常对这些离散的内容特征做Embedding
  • 字数、图片数、视频清晰度、标签数...
    • 笔记自带的属性,反映出笔记的质量,笔记的点击和交互指标与之相关
  • 内容信息量、图片美学...
    • 算法打的分数,事先用人工标注的数据训练cv和nlp模型,当新笔记发布时,用模型给笔记打分,把分数写到物品画像中,可以作为排序模型的特征
统计特征(用户/笔记)

(1)用户统计特征:

  • 用户最近30天 (7天、1天、1小时) 的曝光数、点击数、 点赞数、收藏数...
    • 各种时间粒度 ,可以反映出用户的实时 兴趣、短期 兴趣和中长期兴趣
  • 按照笔记 图文/视频 分桶 (比如最近7天,该用户对图文笔记的点击率、对视频笔记的点击率)
    • 对图文、视频两类笔记分开做统计,可以反映出用户对两类笔记的偏好
  • 按照笔记类目分桶 (比如最近30天,用户对美妆笔记的点击率、对美食笔记的点击率、对科技数码笔记的点击率)

(2)笔记统计特征:

  • 笔记最近30天 (7天、1天、1小时) 的曝光数、点击数、点赞数、收藏数...
    • 统计量反映出笔记的受欢迎程度。如果点击率、点赞率等指标都很高,说明笔记质量高,算法应该给这样的笔记更多的流量
    • 时间粒度是针对笔记的实效性。若笔记已经过时(如30天指标高,但1天指标低),不应该给更多的流量
  • **笔记受众:**按照用户性别、年龄、地域分桶
  • 作者特征: 发布笔记数、粉丝数、消费指标(曝光数、点击数、点赞数、收藏数)
    • 作者统计特征反映了作者的受欢迎程度以及作品的平均品质
场景特征(Context)

随着推荐请求传来的,不用从用户画像、笔记画像等数据库中获取

  • 用户定位GeoHash (经纬度编码) 、城市
    • 用户可能对自己附近的事感兴趣
  • 当前时刻 (分段,做embedding)
    • 一个人在同一天不同时刻的兴趣可能有所区别
  • 是否是周末或节假日
    • 周末或节假日用户可能对特定的话题感兴趣
  • 手机品牌、手机型号、操作系统
    • 安卓or苹果

特征处理

**离散特征:**做embedding

  • 用户ID、笔记ID、作者ID(数量大,消耗内存大)
  • 类目、关键词、城市、手机品牌

**连续特征:**做分桶,变成离散特征

  • 年龄(如把连续的年龄变为十个年龄段,做one-hot或Embedding)、笔记字数、视频长度

**连续特征:**其他变换

  • 曝光数、点击数、点赞数等数值做log(1+x),可以解决异常值问题
    • 曝光数等(大多数笔记只有几百次,少数笔记能有上百万次)为长尾分布,计算会出现异常(训练梯度会很离谱,推理的预估值会很奇怪)
  • 或者把点击 、点赞 等转化为点击 、点赞 等值,并做平滑(去掉偶然性造成的波动)
  • 实际处理中,两种变换之后的连续特征都会作为模型的输入,如log(1+点击数)、平滑之后的点击率都会被用到

特征覆盖率

特征工程需要注重特征覆盖率

  • 理想情况下,每个特征都能覆盖100%样本,即不存在特征缺失
  • 但实际很多特征无法覆盖100%样本,例:
    • 很多用户不填年龄,因此用户年龄特征的覆盖率远小于100%
    • 很多用户设置隐私权限,APP不能获得用户地理定位,因此场景特征有缺失

提高特征覆盖率,可以让精排模型更准;另外还要考虑,当特征缺失时,用什么作为默认值

数据服务(线上服务的简化版系统架构)

  • 用户画像(User Profile)
  • 物品画像(Item Profile)
  • 统计数据

以上3个数据源都存储在内存数据库中。 线上服务时,排序服务器会从3个数据源取回所需的数据,然后把读取的数据做处理,作为特征喂给模型,模型就能预估出点击率、点赞率等指标

当用户刷小红书时,用户请求会被发送到推荐系统的主服务器 上,主服务器会把请求发送到召回服务器 上。做完召回后,召回服务器会把几十路召回的结果做归并,把几千篇笔记的ID返回给主服务器(召回需要调用用户画像)

主服务器 把笔记ID、用户ID、场景特征(1个用户ID+几千个笔记ID。笔记ID是召回的结果,用户ID和场景特征都是从用户请求中获取的,场景特征包括当前时刻、用户所在地点以及手机的型号和操作系统)发送给排序服务器

接下来,排序服务器 要从多个数据源中取回排序所需的特征,主要是这3个数据源:用户画像、物品画像、统计数据

  • 用户画像 数据库线上压力小,因为每次只读1个用户的特征
    • 性别、年龄几乎不变
    • 用户活跃度、兴趣标签等为天级别的刷新
  • 物品画像 数据库压力大,因为粗排要给几千篇笔记排序,读取几千篇笔记的特征
    • 物品自身属性、算法给物品打的标签在很长一段时间内不会发生变化

对用户画像和物品画像最重要的是读取速度快,而不太需要考虑时效性,因为它们都是静态的,甚至可以储存在排序服务器本地,让读取变得更快

  • 统计数据: 存用户统计值的数据库压力小,存物品统计值的数据库压力大
    • 统计数据不能在本地缓存,因为它动态变化,时效性很强

在收集到排序所需的特征之后,排序服务器 把特征打包传给TF Serving 。TensorFlow会给笔记打分,把分数返回给排序服务器,排序服务器会用融合的分数、多样性分数、业务规则给笔记排序,把排名最高的几十篇笔记返回给主服务器,这就是最终给用户曝光的笔记

6、粗排模型

粗排 vs 精排

粗排 牺牲准确性,以保证线上推理的速度快(目的是做初步筛选,从几千篇笔记中选出几百篇,而不是真正决定把哪些笔记曝光给用户);精排的模型足够大,牺牲更多的计算,确保预估的准确性足够高

精排模型 & 双塔模型

Shared Bottom 含义为被多个任务共享【精排模型的代价主要在这,因为它很大,神经网络也很复杂】

精排模型 属于前期融合 (先对所有特征做concatenation,再输入神经网络),但线上推理代价大(如果有n篇候选笔记,整个大模型要做n次推理)

两个向量的余弦相似度表明用户是否对物品感兴趣

在训练好模型之后,把物品向量b存储在向量数据库,在线上不需要用物品塔做计算 。线上推理只需要用到用户塔,每做一次推荐,用户塔只做一次推理,计算出一个向量a【双塔模型的计算代价很小,适合做召回】

双塔模型属于后期融合 (把用户、物品特征分别输入不同的神经网络,不对用户、物品特征做融合,直到最后才计算a和b的相似度),线上计算量小(用户塔只需要做一次线上推理,计算用户表征a;物品表征b事先储存在向量数据库中,物品塔在线上不做推理),但预估准确性不如精排模型

粗排的三塔模型

小红书的粗排是三塔模型,效果介于双塔和精排之间

借鉴【阿里妈妈】Zhe Wang et al. COLD: Towards the Next Generation of Pre-Ranking System. In DLP- KDD , 2020

交叉特征是指用户特征和物品特征做交叉

训练粗排模型的方法就是正常的端到端训练,跟精排完全一样

三塔模型介于前期融合(把底层特征做concatenation)和后期融合之间,是把三个塔输出的向量做concatenation

  • 物品的属性相对比较稳定,可以把物品塔输出向量缓存在PS,每隔一段时间刷新一次。由于做了缓存,物品塔在线上几乎不用做推理,只有遇到新物品时才需要做推理。粗排给几千个物品打分,物品塔实际只需要做几十次推理,所以物品塔的规模可以比较大
  • 统计特征会实时动态变化 (每当一个用户发生点击等行为,其统计特征都发生变化;每当一个物品获得曝光和交互,其点击次数、点击率等指标都会发生变化)。由于交叉塔的输入会实时发生变化,不应该缓存交叉塔输出的向量,交叉塔在线上的推理避免不掉,故交叉塔必须足够小、计算够快(通常来说交叉塔只有一层,宽度也比较小)

模型的上层结构:

模型上层做n次推理,代价大于交叉塔的n次推理

三塔模型的线上推理
  • 物品塔输出的向量事先缓存在Parameter Server上,只有当没有命中缓存时,才需要物品塔做推理(实际上缓存命中率非常高,99%的物品都能命中缓存,不需要做推理。给几千个候选物品做粗排,物品塔只需要做几十次推理)
  • 交叉塔的输入都是动态特征,不能做缓存
  • 三个塔各输出一个向量,融合起来作为上层网络的输入

粗排大部分的计算量都在上层网络

**粗排模型的设计理念:**尽量减小推理的计算量,使得模型可以在线上给几千篇笔记打分

相关推荐
威化饼的一隅1 小时前
【多模态】swift-3框架使用
人工智能·深度学习·大模型·swift·多模态
机器学习之心2 小时前
BiTCN-BiGRU基于双向时间卷积网络结合双向门控循环单元的数据多特征分类预测(多输入单输出)
深度学习·分类·gru
MorleyOlsen3 小时前
【Trick】解决服务器cuda报错——RuntimeError: cuDNN error: CUDNN_STATUS_NOT_INITIALIZED
运维·服务器·深度学习
愚者大大5 小时前
1. 深度学习介绍
人工智能·深度学习
liuming19925 小时前
Halcon中histo_2dim(Operator)算子原理及应用详解
图像处理·人工智能·深度学习·算法·机器学习·计算机视觉·视觉检测
长风清留扬6 小时前
机器学习中的密度聚类算法:深入解析与应用
人工智能·深度学习·机器学习·支持向量机·回归·聚类
程序员非鱼6 小时前
深度学习任务简介:分类、回归和生成
人工智能·深度学习·分类·回归·生成
γ..7 小时前
基于MATLAB的图像增强
开发语言·深度学习·神经网络·学习·机器学习·matlab·音视频
机器学习之心7 小时前
LSTM-SVM时序预测 | Matlab基于LSTM-SVM基于长短期记忆神经网络-支持向量机时间序列预测
神经网络·支持向量机·lstm
落魄君子7 小时前
SVM分类-支持向量机(Support Vector Machine)
神经网络·算法·支持向量机·分类