机器学习(ML)(十二) — 推荐系统探析
概念介绍
推荐系统(Recommendation system
)的链路包括两个重要的步骤:检索(称召回)和排名(分为粗排,精排和重排)检索或召回主要用于衡量系统从全量信息中找出相关内容的能力。它的核心目的是在用户查询的背景下,尽可能多地返回与之相关的信息。如下图所示:
推荐系统(Recommendation system
)的目标是从物品的数据库中取出几十个物品展示给用户。推荐系统链路上的第一环是检索或召回,就是从数据库中快速取回一些物品,在实践中,推荐系统有很多条召回通道(如协同过滤、双塔模型、关注的作者等等),每条召回通道取回几百个物品。这些召回通道一共返回几千个物品,然后推荐系统会融合这些物品,并且做去重和过滤,过滤的意思是排除掉不喜欢的物品,召回几千个物品之后,下一步是排序,排序要用机器学习模型预估用户对物品的兴趣,保留评分最高的物品,如果用一个大规模神经网络逐一对几千个物品打分,花费的代价会很大,为了解决计算量的问题,通常把排序分为粗排和精排。粗排一般使用比较简单的模型,快速给几千个物品打分,保留分数最高的几百个物品;精排用一个较大的神经网络给几百个物品打分,不用做截断。精排模型比粗排模型大很多,用的特征也更多,所以精排模型打的分数也更可靠。但是精排模型的计算量也很大,这就是为什么我们用粗排模型先做筛选,然后才用精排模型,这样做可以很好的平衡计算量和准确性。做完粗排和精排得到几百个物品,每个物品都有一个分数,表示用户多物品的兴趣有多高。可以直接把物品按照打的分数排序,然后展示给用户,但此时的结果还是会存在一些不足,需要做一些调整,这一步叫做重排。重排主要是考虑多样性,要根据多样性进行随机抽样,从几百个物品中选择出几十个物品,然后还要使用规则把相似的物品打散,重排的结果就是展示给用户的物品。比如把top50
的物品(包含广告)展示给用户。如下图所示:
精排与粗排的唯一区别就是精排用的模型更大,特征更多,模型的输入包括用户特征、物品特征、统计特征。假如要判断用户对某个物品感兴趣,我们就要把该用户的特征、物品的特征和统计特征输入神经网络,神经网络会输出很多数值,比如点击率、点赞率、收藏率、转发率等等,这些数值都是神经网络对用户行为的预估。这些数值越大说明用户对该物品越感兴趣,最后把多个预估值做融合,得到最终的分数。比如求加权和,这个分数决定了这个物品会不会展示给用户,以及物品展示的位置是靠前还是靠后,请注意,这只是对一个物品的打分,粗排要对几千个物品打分,精排要对几百个物品打分。每个物品都用多个预估分数融合成一个分数作为给这个物品排序的依据。推荐系统链路上的最后一环是重排,重排最重要的能力是多样性抽样(比如MMR
、DPP
),需要从几百个物品中选出几十个物品。多样性抽样的时候有两个依据:一个依据是精排分数的大小,另一个依据是多样性。做完多样性抽样之后会根据规则将相似物品打散。重排的另一个目的是插入广告、运营推广的内容,根据生态要求调整排序。
A/B测试
做A/B
测试需要对用户做随机分桶,比如把用户随机分成10
个桶,每个桶中有10%
的用户,如果用户的数量足够大,那么每个桶的DAU
留存、点击率这些指标都是相等的。具体分桶的方法是:首先用哈希函数吧用户ID
映射成某个区间内的整数,然后把这些整数均匀随机分成10
个桶,每个桶有10%
的用户,接下来才用这样的桶来做A/B
测试。比如我们想要考察GNN
(图神经网络)对召回通道指标的影响。有1、2、3
号桶作为三个实验组,但是GNN
的参数不一样,三个桶的神经网络分别是一层、两层、三层。用4
号桶作为对照组,这四个桶唯一的区别就是前三个桶用了GNN
召回通道,4
号桶没有用GNN
。如果一个用户落在1
号桶,那么给它做推荐的时候,会使用一层的GNN
神经网络做召回;如果另一个用户落在了4
号桶,那么给它做推荐的时候,则不使用GNN
做召回。分别计算每个桶的业务指标,比如DAU
、人均使用推荐时长、点击率等等。如果过某个实验组的指标显著优于对照组,则说明对应的策略有效,值得推全(意思是把流量扩大到100%
)。
分层实验的目标就是解决流量不够用的问题。主要是因为信息流产品的公司有很多部门和团队,大家都需要做A/B
测试,比如:推荐系统(召回、粗排、精排、重排)、用户界面、广告等。如果把用户随机分成10
组,1
组作对照,9
组做实验,那么只能同时做9
组实验。这样会导致流量资源完全不够用。解决方案就是分层实验,把实验分成很多层,比如分成:召回、粗排、精排、重排、用户界面、广告等等(例如GNN
召回通道就属于召回层)。同层之间的实验需要互斥,例如:GNN
实验占了召回层的4
个桶,其它召回实验只能使用剩下的6
个桶。同层互斥的目的是避免一个用户同时被两个召回实验影响,加入两个实验相互干扰,实验结果会变的不可控;不同层之间流量正交:每一层独立随机对用户做分桶。每一层都可以独立用100%
的用户做实验。召回和粗排的用户是随机划分的。召回的2号桶和粗排的2号桶交集很小,举个例子召回层和精排层各自独立随机把用户分成十个桶,分别是1+1 > 2
)或相互抵消(1+1 < 2
),互斥可以避免同类策略相互干扰;不同类型的策略(例如添加召回通道、优化粗排模型)通常不会相互干扰(1+1 = 2
),可以作为正交的两层。
召回 — ItemCF
基于物品的协同过滤(ItemCF
)的基本思想:如果用户喜欢物品1
分,在这个例子中,用户对四个物品的兴趣分数分别是2,1,4,3
,这里有一个没有跟用户交互过得物品,我们要决定是否要把这个物品推荐给用户。假设我们知道物品两两之间的相似度,比如它们的相似度分别是:0.1、0.4、0.2、0.6
。使用如下公式:4
条路径,所以要计算4个分数,然后把它们相加,预估用户对候选物品的兴趣:
举个例子有2000
个候选物品,我们逐一计算用户对候选物品的兴趣分数,然后返回分数最高的100
个物品,如何计算两个物品之间的相似度?计算物品相似度的思路是这样的,两个物品受众重合度越高,两个物品越相似。我们可以从数据中挖掘出物品的相似度,把喜欢物品0~1
之间的数,值越大表示两个物品越相似。为什么相似度是介于0~1
之间的呢?这是集合1
,不喜欢都看做0
。如果想用到喜欢的程度,需要改一下这个公式。比如点击、点赞、收藏、转发各自算1分,喜欢物品的程度最多可以是4分。现在我们考虑用户喜欢物品的程度,则公式更改为0
或1
,那么分子就是同时喜欢两个物品的人数就是集合0~1
之间,表示两个物品的相似度,其实这个公式就是余弦相似度(cosine similatity
)。把一个物品表示为一个稀疏向量,向量每个元素对应一个用户,元素的值就是用户物品的兴趣分数,两个向量夹角的余弦就是这个这个公式。
基于物品的协同过滤(ItemCF
)召回的完整流程:为了能在线上做到实时的推荐,系统必须事先做离线计算,建立两个索引,一个索引是”用户 -> 物品“索引,记录每个用户最近点击、交互过的物品ID
。有了这个索引之后,给定任意用户ID
,可以找到它近期感兴趣的物品列表。另一个索引是”物品 -> 物品“索引,我们首先要计算物品之间两两相似度,这个计算量会比较大,对于每个物品,索引它最相似的ID
,可以快速的找到它最相似的
有了索引之后我们以在线上给用户做实时推荐,比如系统知道用户的ID
,首先查看用户—>物品的索引,可以快速找到用户近期感兴趣的物品列表(last-n
),对于last-n
列表中的每一个物品,利用物品—>物品的索引,找到top-k
相似物品。用户最近有top-k
相似物品。那么一共取回100
个物品作为推荐结果。这100
个物品就是这个ItemCF
召回通道的输出,会跟其它召回通道的输出融合起来,然后做排序,最终展示给用户。为什么要使用索引呢?数据库中有上亿个物品,如果挨个用公式计算对所用物品的兴趣分数,计算量会爆炸,索引的意义在避免枚举所有的物品。加入我们记录用户最近感兴趣的2000
个物品打分,也就是分别预估对这2000
个物品的兴趣分数,返回分数最高的100
个物品作为ItemCF
召回通道的输出。这样的计算量是很小的,可以做到在线实时计算。总结一下,用索引,离线计算量大,线上计算量小。好处是每次线上召回速度都很快,只需要给2000
个物品打分,不需要访问上亿个物品。
整个流程是用户登录之后,系统给用户做推荐,知道用户的ID
,利用”用户 -> 物品“的索引,找到用户近期感兴趣的物品列表,这个列表记录了用户ID
和用户对该物品的兴趣的分数。接下来我们要利用物品->物品的索引,找到每个物品的top-k
相似的物品,知道物品的ID我们要取回与它相似的top-k
相似物品ID
和相似度。用同样的方法,根据物品->物品的索引,找到每个物品的top-k
相似物品,top-k
相似可以召回很多物品,可以用公式top-100
的物品,做计算的时候需要用到这些列表里的数值,用到上面列表用户对物品的兴趣分数,用到下面列表物品对物品相似度分数,把兴趣分数和相似度分数相乘,如果取回的物品ID
有重复的,就选择去重,最后把分数加起来。线上召回流程如下图所示:
Swing模型
Swing
模型的原理就是给用户设置权重,解决小圈子的问题。把用户Swing
和ItemCF
是非常相似的方法,它们之间唯一的区别在于物品相似度。ItemCF
考察两个物品的重合的受众比例有多高,如果很多用户同时喜欢两个物品,则判定两个物品相似。ItemCF
会额外考虑重合的用户是否来自同一个小圈子,把同时喜欢两个物品的用户记作集合Swing
和ItemCF
的区别就是计算物品相似度的时候要降低用户小圈子的影响。
召回 — UserCF
基于用户的协同过滤(UserCF
)与基于物品的协同过滤(ItemCF
)有很多相似之处,ItemCF
是基于物品之间的相似性做推荐,而UserCF
是基于用户之间的相似性做推荐。UserCF
的原理:比如有很多跟我兴趣非常相似的网友,今天跟我兴趣相似的某个网友看了某个物品,他很感兴趣,对物品点赞、转发。于是系统就知道他喜欢这个物品,而我还没有看到这个物品,那么推荐系统就可能给我推荐这个物品。推荐的理由就是跟我兴趣爱好相似的网友喜欢这个物品。推荐系统如何找到跟我兴趣相似的网友呢?一种方法是:判断两个人感兴趣(点击、点赞、收藏、转发)的物品有多少重合,每个用户都有一个列表,上面存储了用户点击、点赞、收藏、转发的物品ID
,对比两个用户的列表,就知道有多大的重合,重合雨多则说明两个人的兴趣越相似。另一种方法是:不是看物品的重合,而是看关注的作者的重合,每个用户都会关注一些作者,对比两个用户关注作者的列表,就知道有多少关注的作者是重合的。关注的作者重合越多,说明两个人的兴趣越相似。
在用UserCF
做推荐之前,需要先离线算好每两个用户之间的相似度,例如我们在给左边的用户做推荐,中间是最相似的四个用户,这些分数1
分,四位用户对候选物品的兴趣分数分别是0
表示用户没有看过该物品,或者对物品不感兴趣。最后用这个公式4
个分数,然后把它们相加:2000
个候选物品,计算用户对候选物品的兴趣分数,然后返回分数最高的top-100
个物品,如下图所示:
先举例说明两个用户相似和不相似,这两个用户喜欢的物品没有重合,所以不相似。另外两个用户喜欢的物品重合度高,所以判定这两个用户相似。两个用户的相似度是这样计算的,把用户0~1
之间的数,数值越接近1表示两个用户越相似。刚才的公式有个不足之处,当公式同等对待热门和冷门的物品,这是不对的,拿书籍推荐举个例子:《哈利波特》是非常热门的物品,不论是大学教授还是中学生都喜欢看《哈利波特》,既然所有人都喜欢看哈利波特,那么哈利波特对于计算用户相似度是没有价值的,越热门的物品越无法反映出用户独特的兴趣,对计算相似就越没有用。重合的物品越冷门,越能反映出用户的兴趣,如果两个用都喜欢《Deep Learning
》这本书,两个人很可能是同行,如果两个用户都喜欢更冷门一些的书,比如《深度学习在语音识别中的应用》,说明两个人是小同行,为了更好的计算用户兴趣的相似度,我们需要降低热门物品的权重,计算用户相似度的公式可以改写为:1
的连加,其中的1
是物品的权重。所有物品的权重都相同,不论是冷门,还是热门,物品的重要应该跟物品的权重相关,越热门的物品权重应该更低,我们把分子中的1改写为
UserCF
的完整流程:事先做离线计算,先建立”用户—>物品“的索引,记录每个用户最近点击、交互过的物品ID
,给定任意用户ID
,可以找到它近期感兴趣的物品列表;接下来建立”用户—>用户“的索引,对于每个用户,索引他最相似的ID
,可以快速的找到最相似的
UserCF
的完整流程:线上做召回,有了索引之后,可以在线上做实时推荐。比如系统知道用户的ID
,首先通过”用户—>用户“的索引,快速找到与这位用户最相似Top-k
个用户,对于每个Top-k
相似用户,利用”用户—>物品“的索引,找到用户近期感兴趣的物品列表(last-n
),有了100
个物品,作为召回的结果。这100
个物品就是UserCF
召回通道的输出,如下图所示:
召回 - 离散特征处理
离散特征在机器学习很常见,性别(男,女)、国家(中国、美国、俄罗斯等)、英文单词、物品ID
、用户ID
是离散特征。推荐系统会把ID
映射成一个向量。离散特征处理分为两步:1、建立字典,把类别映射成序号;2、向量化,把序号映射成向量(One-hot
编码,把序号映射成高维稀疏向量;Embedding
,把序号映射成低维稠密向量)。例如性别特征:男、女两个类别,在字典里边,男—>1
,女—>2
。采用One-hot
编码,用2
维向量表示性别。如果用户不填写性别,那么就是”未知 —> 0
—> [0,0]
“;如果用户性别是男,那么”男 —> 1
—> [1,0]
“;如果性别是女,那么”女 —> 2
—> [0,1]
“。在实践中,类别数量太大时,通常不使用One-hot
编码。例如国籍特征,字典把每个国家映射成序号(Embedding
把每个序号映射成向量,这些向量都是低维向量,比如向量大小都是Embedding
得到的向量都是4维的,一共有200
个国籍,那么就有200
个向量,那么参数数量为
编程实现:可以直接使用TensorFlow、PyTorch
提供的Embedding
层。在训练神经网络的时候,会自动反向传播学习Embedding
层的参数,Embedding
层的参数是一个矩阵,矩阵的大小是向量维度 x 类别数量,Embedding
层的输入是序号,比如美国的序号是2
,Embedding
层的输出是个向量,比如美国对应参数矩阵的第二列。Embedding
= 参数矩阵 x One-hot
向量,Embedding
其实就是矩阵向量乘法,跟全连接层非常像。
召回 - 向量召回
矩阵补充(matrix completion
)是向量召回最简单的一种方法,现在基本不怎么使用了。Embedding
可以把用户ID
或者物品ID
映射成向量,如下图所示:
模型的输入是一个用户ID
和一个物品ID
,模型的输出是一个实数,是用户对物品兴趣的预估值。这个数越大,表示用户对物品越感兴趣。下面我们看一下模型的结构,左边的结构只有一个Embedding
层,把一个用户ID
映射到一个向量,记录向量Embedding
层的参数是一个矩阵,矩阵中列的数量是用户数量,每一列都是Embedding
层的参数数量 = 用户数量 x 向量Embedding
层,把一个物品ID
映射到一个向量,记录向量Embedding
层的参数是一个矩阵,输出的向量2
个Embedding
层,它们不共享参数,对向量
模型训练的基本思路:用户Embedding
层参数矩阵记作Embedding
层参数矩阵记作Embedding
层的参数。开始训练之前首先要准备一个数据集,数据集是(用户ID
,物品ID
,兴趣分数)的集合,记作ID
,ID
,0
分,点击、点赞、收藏、转发各算1
分,这些行为都说明用户对物品感兴趣,那么兴趣分数最低是0
,最高是4
。训练的目的就是让模型的输出去拟合兴趣分数。模型中有Embedding
层可以把用户ID
,物品ID
映射成向量,第Embedding
层的参数。优化公式:
在实践中并不会使用矩阵补充,其缺点:仅使用了ID embedding
,用两个Embedding
层,把物品ID
和用户ID
映射成两个向量,仅此而已,且没有利用用户(性别、年龄、地理定位、感兴趣的类目)、物品(类目、关键词、地理位置、作者信息)属性。知道这些属性,召回可以做的更精准;负样本的选取方式不对,样本指的是用户
在训练好模型之后,可以把模型应用在推荐系统中的召回通道,并且要把训练好的模型存储在正确的地方,便于做召回。训练得到矩阵Embedding
层的参数。key-value
表。key
是用户ID
,value
是矩阵ID
,在表中查找并返回一个向量(用户的embedding
)。另一个矩阵key-value
存储。在把Embedding
向量做存储之后,可以开始做线上服务,把用户ID
作为key
值,查询key-value
表,得到用户的向量,记作embedding
向量记作k
个物品,这些物品就是召回的结果。这种最近邻查找有一个严重的问题。如果逐一对比所有物品,时间复杂度正比于物品数量。这种巨大的计算量是不可接受的,根本做不到线上的实时计算。有很多算法可以加速最近邻查找,这些算法非常快,即使有几亿个物品最多也只需要几万次内积,这些算法的结果未必是最优的,但不会比最优结果差多少,快速最近邻查找已经被集成到了许多向量数据库(Milvus、Faiss、HnswLib
等等)当中。做最近邻查找需要定义什么是最近邻?也就是衡量最近邻的标准,比如最近邻可以定义为:欧式距离最小(L2
距离)、向量内积最大(内积相似度)、向量夹角余弦最大(cosine
相似度)。加速最近邻查找的方法:先做数据预处理,把数据划分成很多区域,数据如何划分取决于最近邻的标准,如果采用cosine
相似度,那么划分的结果就是扇形。如果使用欧几里得距离,划分的结果就是多边形。划分之后每个区域用一个向量来表示。划分区域之后,每个区域都用一个单位向量来表示,假如有一个点,划分成一万个区域,索引一共有一万个k
值,每个向量是一个区域的k
值,给定一个向量可以快速取回这个区域所有的点。有了这样一个索引就可以在线上快速做召回。在线上给一个用户做推荐,这个用户的embedding
向量记作
召回 - 双塔模型
双塔模型(DSSM
)的结构:我们知道用户ID
,还能从用户填写的资料、用户行为中获取许多特征,包括用户的离散特征、连续特征。所有这些特征不能直接输入到神经网络中,需要先做一些处理,比如用Embedding
层把一个用户映射到一个向量。用户还有很多离散特征,比如所在城市,感兴趣的话题等等,用Embedding
层把离散特征映射一个向量,对于每个离散特征用单独的Embedding
层得到一个向量,比如用户所在城市用一个Embedding
层、用户感兴趣的话题用另外一个Embedding
层,对于性别这样类别很少的离散特征可以用One-hot
编码就行,不做Embedding
层。用户还有很多连续特征,比如年龄、活跃程度、消费金额等,不同的连续特征有不同的处理方法。最简单方式归一化,让均值是0
,标准差是1
。有些长尾分布的连续特征需要特殊处理,比如取对数或做分桶等处理。做完特征处理得到很多特征向量,把这些向量都拼起来输入神经网络。神经网络可以是简单的全连接网络,也可以是更复杂的结构,比如是深度交叉网络,神经网络输出一个向量,这个向量就是用户的表征,做召回会用到这个向量。物品的特征也是用类似的方法来处理,用Embedding
层处理物品ID
和物品离散特征,用归一化、取对数或分桶等方法处理物品连续特征。把得到的特征输入一个神经网络,神经网络输出的向量就是物品的表征,用于做召回。如下图所示:
这样的模型就叫双塔模型(DSSM
),左边的塔提取用户的特征,右边的塔提取物品的特征。双塔模型的不同之处就是使用了ID之外的多种特征,作为双塔的输入。两个塔各输出一个向量,记作-1~1
之间。
这里有三种训练双塔模型的方式:
pointwise
训练:独立看待每个正、负样本,做简单的二元分类训练模型,把正样本和负样本组成一个数据集,在数据集上做随机梯度下降双塔模型。pairwise
训练:每次去一个正样本、一个负样本,组成一个二元组,损失函数为Triplet hinge loss
、Triplet logistic loss
。listwise
训练:每次取一个正样本、多个负样本组成一个列表,训练方式类似于多元分类。
训练的时候要用到正样本、负样本,该怎样选择正负样本呢?正样本就是用户点击过的物品,用户点击过一个物品,就说明用户对这个物品感兴趣;负样本的意思是用户不感兴趣的,在实践中负样本的选择是比较讲究的,有几种看起来比较合理的负样本,比如没有被召回的?召回的但是被粗排、精排淘汰的?曝光但是未点击?
pointwise
训练是最简单的训练方式,我们把召回看作是最简单的二元分类任务,正样本意思是历史记录显示用户对物品感兴趣,对于正样本,应该鼓励向量pointwise
训练,可以控制正负样本数量的比例为
pointwise
训练:做训练的时候每一组的输入是一个三元组,包括一个用户和两个物品,左边的物品是正样本,就是用户感兴趣的物品,右边的物品是负样本,是用户不感兴趣的物品。把用户的特征和物品的特征各自做变换,然后输入神经网络,最后输出三个向量,用户的特征向量记作Embedding
层和全连接层都用一样的参数,分别计算用户对两个物品的兴趣,用户对正样本物品的兴趣是向量
pairwise
训练:做pairwise
训练的基本想法是希望用户对正样本的兴趣尽量大,对负样本的兴趣尽量小,也就是Triplet hinge loss
):Triplet hinge loss
只是一种损失函数,还有Triplet logistic loss
,Triplet logistic loss
是这样定义的:pairwise
训练流程如下图所示:
listwise
训练:做listwise
训练的时候,每次取一个正样本和很多负样本,需要一个用户,把它的特征向量记作1~1
之间的实数,意思是用户对正样本兴趣的预估分数,这个分数越大越好,最好是接近1;这些余弦相似度softmax
激活函数,激活函数输出0~1
之间,最左边的分数softmax
函数输出listwise
训练流程如下图所示: