第二章 召回
1 基于物品的协同过滤(ItemCF)
1.1 ItemCF的原理
1.1.1 ItemCF的实现
量化用户对物品的兴趣like(user,itemj)like(user,item_j)like(user,itemj);
量化物品之间的相似度sim(itemj,item)sim(item_j,item)sim(itemj,item);
预估用户对候选物品的兴趣:∑jlike(user,itemj)×sim(itemj,item)\sum_jlike(user,item_j)\times sim(item_j,item)∑jlike(user,itemj)×sim(itemj,item)
1.1.2 物品相似度
两个物品的受众重合度越高,则两个物品越相似。(不是根据物品内容判断,而是根据用户判断)
- 喜欢物品i1i_1i1的用户记作集合W1W_1W1
- 喜欢物品i2i_2i2的用户记作集合W2W_2W2
- 定义交集V=W1∩W2V=W_1\cap W_2V=W1∩W2
- 两个物品的相似度(cosine similarity):sim(i1,i2)=∣V∣∣W1∣⋅∣W2∣sim(i_1, i_2) = \frac{|V|}{\sqrt{|W_1|\cdot|W_2|}}sim(i1,i2)=∣W1∣⋅∣W2∣∣V∣
上述公式没有用到用户喜欢的程度
- 改进相似度:sim(i1,i2)=∑v∈Vlike(v,i1)⋅like(v,i2)∑u1∈W1like2(u1,i1)⋅∑u2∈W2like2(u2,i2)sim(i_1, i_2) = \frac{\sum_{v\in V} like(v, i_1) \cdot like(v, i_2)}{\sqrt{\sum_{u_1\in W_1} like^2(u_1, i_1)\cdot \sum_{u_2\in W_2}like^2(u_2, i_2)}}sim(i1,i2)=∑u1∈W1like2(u1,i1)⋅∑u2∈W2like2(u2,i2)∑v∈Vlike(v,i1)⋅like(v,i2)
1.2 ItemCF召回的完整流程
事先做离线计算:建立”用户->物品“的索引(最近点击、交互、近期感兴趣);建立”物品->物品“的索引(先计算两两相似度,索引最相似的k个物品)
线上做召回:给定用户ID,通过用户->物品索引,找到近期感兴趣的物品列表(last-n);对于last-n的每一个物品,通过物品->物品索引找到top-k相似物品;对于取回的nk相似物品,用公式预估用户对物品的兴趣分数;返回分数最高的100个物品,作为推荐结果。
-
索引的意义在于避免枚举所有的物品。
-
离线计算量大,在线计算量小。
-
如果取回的相似物品有相似的,需要去重。
2 Swing召回通道
ItemCF的缺点:如果两个物品重合的用户是一个小圈子,例如两篇笔记被分享到了同一个微信群,会对物品相似度判断产生影响。
2.1 Swing模型(额外考虑重合的用户是否来自一个小圈子)
- 用户u1u_1u1喜欢的物品记作集合J1J_1J1
- 用户u2u_2u2喜欢的物品记作集合J2J_2J2
- 计算两个用户的重合度overlap(u1,u2)=∣J1∩J2∣overlap(u_1, u_2)=|J_1\cap J_2|overlap(u1,u2)=∣J1∩J2∣
- 用户u1u_1u1和u2u_2u2的重合度高,说明他们可能来自一个小圈子,要降低他们的权重
- 喜欢物品i1i_1i1的用户记作集合W1W_1W1
- 喜欢物品i2i_2i2的用户记作集合W2W_2W2
- 定义交集V=W1∩W2V=W_1\cap W_2V=W1∩W2
- 两个物品的相似度:sim(i1,i2)=∑u1∈V∑u2∈V1α+overlap(u1,u2)sim(i_1, i_2) = \sum_{u_1\in V}\sum_{u_2\in V} \frac{1}{\alpha+overlap(u_1,u_2)}sim(i1,i2)=∑u1∈V∑u2∈Vα+overlap(u1,u2)1(α\alphaα是一个预先设置的参数,平滑项)
3 基于用户的协同过滤(UserCF)
3.1 UserCF的原理
推荐系统发现另一个用户和当前用户兴趣非常相似,另一个用户对笔记进行点赞转发,可以给当前用户推荐这篇笔记。
如何找到兴趣相似的用户?
(1)点击、点赞、收藏、转发重合度高;(2)关注的作者重合度大。
3.1.1 UserCF的实现
量化用户之间的相似度sim(user,userj)sim(user,user_j)sim(user,userj);
量化用户对物品的兴趣like(userj,item)like(user_j,item)like(userj,item);
预估用户对候选物品的兴趣:∑jsim(user,userj)×like(userj,item)\sum_jsim(user,user_j)\times like(user_j,item)∑jsim(user,userj)×like(userj,item)
3.1.2 用户相似度(衡量用户感兴趣物品集合的重合度)
- 用户u1u_1u1喜欢的物品记作集合J1J_1J1
- 用户u2u_2u2喜欢的物品记作集合J2J_2J2
- 定义交集I=J1∩J2I=J_1\cap J_2I=J1∩J2
- 定义两个用户的相似度:sim(u1,u2)=∣I∣∣J1∣⋅∣J2∣sim(u_1,u_2) = \frac{|I|}{\sqrt{|J_1|\cdot |J_2|}}sim(u1,u2)=∣J1∣⋅∣J2∣∣I∣
上述公式没有考虑热门物品的影响(大部分用户都喜欢)
- 需要降低热门物品权重,相似度计算改为sim(u1,u2)=∑l∈I1log(1+nl)∣J1∣⋅∣J2∣sim(u_1,u_2) = \frac{\sum_{l\in I} \frac{1}{log(1+n_l)}}{\sqrt{|J_1|\cdot |J_2|}}sim(u1,u2)=∣J1∣⋅∣J2∣∑l∈Ilog(1+nl)1,其中nln_lnl表示物品的热门程度。
3.2 UserCF召回的完整流程
事先做离线计算:建立”用户->物品“的索引(最近点击、交互、近期感兴趣);建立”用户->用户“的索引(先计算两两相似度,索引最相似的k个用户)
线上做召回:给定用户ID,通过用户->用户索引找到top-k相似用户;对于每个top-k相似用户,通过用户->物品索引,找到用户近期感兴趣的物品列表(last-n);对于召回的nk个相似物品,用公式预估用户对每个物品的兴趣分数;返回分数最高的100个物品,作为召回结果。
4 离散特征提取(向量召回的基础)
4.1 离散特征处理
1、建立字典:把类别映射成序号
2、向量化:把序号映射为向量(one-hot编码;embedding)
embedding=参数矩阵×ont−hot向量embedding=参数矩阵\times ont-hot向量embedding=参数矩阵×ont−hot向量
5 矩阵补充(Matrix Completion)
- 用户ID -> Embedding Layer -> a
- 物品ID -> Embedding Layer -> b
- 内积:⟨a,b⟩\langle a,b\rangle⟨a,b⟩是某用户对某物品兴趣的预估值
5.1 训练
训练模型的目的是学习矩阵A和B,是的预估值拟合真是观测的兴趣分数。
-
数据集:(用户ID,物品ID,兴趣分数)的集合 - Ω=(u,i,y)\Omega = {(u,i,y)}Ω=(u,i,y)
-
兴趣分数是系统记录的,曝光但没点击:0分,点击、点赞、收藏、转发各算1分
-
求解优化问题:
minA,B∑(u,i,y)∈Ω(y−⟨au,bi⟩)2 \mathop{min}\limits_{A,B}\sum_{(u,i,y)\in\Omega}(y-\langle a_u,b_i\rangle)^2 A,Bmin(u,i,y)∈Ω∑(y−⟨au,bi⟩)2
5.2 矩阵补充
这里提到的矩阵指的是所有用户对所有物品的兴趣分数的矩阵,训练时只有部分数据是存在的,作为训练集,而当训练完成后,即可预测空白区域,即矩阵补充。
矩阵补充在实践中效果不好:
- 矩阵补充仅用ID embedding,没有利用用户、物品的属性(双塔模型可以看成矩阵补充的优化版本)
- 负样本的选取方式不对(矩阵补充选取的负样本:曝光了但是没有点击,这是不对的)
- 作训练的方法不好(内积不如余弦相似度,用平方损失做回归不如用交叉熵损失做分类)
5.3 线上服务
5.3.1 模型存储
训练得到的A和B的列数非常大,可以考虑存储A为key-value表(key为用户Id,value为对应的embedding向量),而B的存储和索引比较复杂。
5.3.2 线上服务
- 查询key-value表,得到用户ID对应的向量aaa
- 最近邻查找:查找用户最有可能感兴趣的k个物品,作为召回结果(逐一枚举,时间复杂度正比于物品数量)
如何加速最近邻查找?
- 近似最近邻查找:支持的系统有Milvus、Faiss、HnswLib等等。
- 衡量最近邻的标准:欧式距离最小、向量内积最大、向量夹角余弦最大
- 可行的加速方案:以向量夹角余弦最大为例,将物品的embedding划分为一系列的扇形区域,每个扇形区域可以有一个长度为1的向量作为key,里面所有物品的列表作为value。查找时,先用aaa和所有key做内积,找到最接近的,然后再从value列表里逐个计算。
6 双塔模型:模型与训练
6.1 双塔模型
用户ID(embedding)、用户离散特征(embedding(少类别的可以直接one-hot))、用户连续特征(归一化、分桶(例如长尾特征)等),三种特征输入到神经网络中,得到用户的表征。
物品ID、物品离散特征、物品连续特征
-> 用户和物品组成双塔
- 余弦相似度:cos(a,b)=⟨a,b⟩∣∣a∣∣2⋅∣∣b∣∣2cos(\textbf{a},\textbf{b})=\frac{\langle \textbf{a},\textbf{b}\rangle}{||\textbf{a}||_2\cdot ||\textbf{b}||_2}cos(a,b)=∣∣a∣∣2⋅∣∣b∣∣2⟨a,b⟩
6.2 双塔模型的训练
-
Pointwise(独立看待每个正样本、负样本,做简单的二元分类)
-
Pairwise(每次取一个正样本、多个负样本)
-
Listwise(每次取一个正样本、多个负样本)
6.2.1 正负样本的选取
- 正样本:用户点击的物品
- 负样本:没有被召回的?召回但是没有被排序的?曝光但是没有被点击的?
6.2.2 Pointwise训练
- 对于正样本,鼓励相似度接近1;对于负样本,鼓励相似度接近-1
- 控制正负样本的数量比为1:2或1:3
6.2.3 Pairwise训练
-
物品正样本b+\textbf{b}^+b+、物品负样本b−\textbf{b}^-b−、用户a\textbf{a}a
-
鼓励cos(a,b+)>cos(a,b−)cos(a,b^+)>cos(a,b^-)cos(a,b+)>cos(a,b−)->**损失函数 **
-
Triplet hinge loss:
L(a,b+,b−)=max{0,cos(a,b−)+m−cos(a,b+)} L(a,b^+,b^-)=max\{0,cos(a,b^-)+m-cos(a,b^+)\} L(a,b+,b−)=max{0,cos(a,b−)+m−cos(a,b+)} -
Triplet logistic loss:
-
L(a,b+,b−)=log(1+exp[σ⋅(cos(a,b−)−cos(a,b+))]) L(a,b^+,b^-)=log(1+exp[\sigma\cdot(cos(a,b^-) - cos(a, b^+))]) L(a,b+,b−)=log(1+exp[σ⋅(cos(a,b−)−cos(a,b+))])
6.2.4 Listwise训练
- cos(a,b+)cos(a,b^+)cos(a,b+)、cos(a,b1−)cos(a,b^-_1)cos(a,b1−)、…、cos(a,bn−)cos(a,b_n^-)cos(a,bn−) 经过softmax激活函数分别得到s+、s1−、...、sn−s^+、s_1^-、...、s_n^-s+、s1−、...、sn−
- 使用CrossEntropyLoss:−logs+-logs^+−logs+ (为什么这里不考虑si−s_i^-si−)
不适合召回的模型:前期融合(特征向量concat之后输入神经网络)适合排序,而找回需要后期融合(先各自输入各自的神经网络,再计算相似度) (召回物品太多,神经网络训练太多次,计算量太大,没有办法用近似最近邻方法训练)
6.3 正负样本的选取
6.3.1 正样本(曝光且有点击的用户-物品二元组)
- 问题:少部分物品占据大部分点击,导致正样本大部分是热门物品(二八法则)
- 解决方案:过采样冷门物品,或降采样热门物品
6.4.2 负样本
-
简单负样本:未被召回的物品(≈全体样本),可以直接从全体物品中做抽样(非均抽采样:打压热门物品–抽样概率\propto (点击次数)^{0.75}$);也可以使用Batch内负样本(用户1点击物品1,用户2点击物品2,…,用户1可以和物品2组成一个负样本)
- Batch内负样本的问题:一个物品出现在batch内的概率∝点击次数\propto点击次数∝点击次数,而本应该是∝(点击次数)0.75\propto(点击次数)^{0.75}∝(点击次数)0.75 -> 热门物品成为负样本的概率过大(打压得太狠了)
- Youtube论文的建议:训练时相似度计算调整为cos(a,bi)−log(pi)cos(a,b_i) - log(p_i)cos(a,bi)−log(pi)(避免过分打压热门物品),线上召回时还是按照cos(a,bi)cos(a,b_i)cos(a,bi)
-
困难负样本:
- 被粗排淘汰的物品(比较困难)
- 精排分数靠后的物品(非常困难)
对正负样本做二元分类:全体物品分类准确率高、被粗排淘汰的物品容易分错、精排分数靠后的物品更容易分错。
混合负样本(50% 简单负样本 + 50% 困难负样本)
-
错误负样本(不能用!!!):曝光但是没有点击
训练召回模型不能用这类负样本 (排序可以用)
- 原因:曝光了说明已经很匹配用户兴趣了,并不能将其作为用户不感兴趣
6.4 线上召回和更新
6.4.1 线上召回
- 离线存储:把⟨特征向量b,物品ID⟩\langle特征向量b,物品\text{ID}\rangle⟨特征向量b,物品ID⟩存储到向量数据库
- 线上召回:给定用户ID和特征,在线上计算向量aaa,使用最近邻查找查找感兴趣笔记,计算相似度结果
为什么物品向量离线存储,用户向量线上召回?
- 线上计算物品向量代价过大
- 可以离线存储用户向量,但是不利于推荐效果(用户兴趣动态变化,物品特征相对稳定)
6.4.2 更新
-
全量更新:今天凌晨,用昨天全天的数据训练模型(在昨天模型基础上训练),训练1 epoch;发布新的用户塔神经网络和物品向量,供线上召回使用。
- 全量更新对数据流、系统的要求比较低。
-
增量更新:做online learning更新数据模型(用户兴趣随时发生变化)。
-
实时收集线上数据,做流式处理,生成TFRecord文件。
-
对模型做onlen learning,增量更新ID Embedding参数(不更新神经网络其他部分的参数)
-
发布用户ID Embedding,供用户塔在线上计算用户向量。
-
Q:全量更新+增量更新?
A:全量更新和增量更新同时使用时,全量更新只在前一天的模型的基础上调整,每天内使用增量更新。
Q:只做增量更新的时候?
A:小时级的数据有偏,分钟级更大,全量更新进行了random shuffle,而增量更新按照数据从早到晚的顺序做训练。
6.5 双塔模型 + 自监督学习
6.5.1 双塔模型的问题
- 推荐系统的头部效应严重(少部分物品占据大部分点击)
- 高点击物品的表征学得好,长尾物品的表征学得不好
自监督学习:做data augmentation,更好地学习物品的向量表征
6.5.2 自监督学习
-
特征变换
- Random Mask(随机选一些离散特征,把它们都遮住,default替代体)
- Dropout(仅对多值离散特征生效)
- 互补特征/complementary(把特征随机分为两组,两组分别mask得到两组特征)
- Mask一组关联的特征(用互信息计算特征两两之间的关联:MI(U,V)=∑u∈U∑v∈Vp(u,v)⋅logp(u,v)p(u)⋅p(v)MI(U,V) = \sum_{u\in U}\sum_{v\in V}p(u,v)\cdot \log\frac{p(u,v)}{p(u)\cdot p(v)}MI(U,V)=∑u∈U∑v∈Vp(u,v)⋅logp(u)⋅p(v)p(u,v))
k种特征,得到k×\times×k的矩阵,随机选一个特征作为种子,找到与种子最相关的k/2种特征mask掉
-
训练模型
-
损失函数(softmax后取交叉熵):
Lself[i]=−log(exp(cos(bi′,bi′′))∑j=1mexp(cos(bi′,bj′′))) L_\text{self}[i]=-\log(\frac{\exp(\cos(b_i^\prime, b_i^{\prime\prime}))}{\sum_{j=1}^m\exp(\cos(b_i^\prime, b_j^{\prime\prime}))}) Lself[i]=−log(∑j=1mexp(cos(bi′,bj′′))exp(cos(bi′,bi′′))) -
总损失(一个batch中有m个样本):
1m∑i=1mLself[i] \frac{1}{m}\sum_{i = 1}^mL_\text{self}[i] m1i=1∑mLself[i]
-
7 Deep Retrieval
Deep Retrieval把物品表征为路径,线上查找用户最匹配的路径,类似阿里的TDM。
7.1 索引
- 物品->路径
- 路径->物品
7.2 预估模型
7.2.1 预估用户对路径的兴趣(举例)
-
用3个节点表示一条路径:path=[a,b,c]\text{path}=[a,b,c]path=[a,b,c]
-
给定用户特征x\textbf{x}x,预估用户对节点aaa的兴趣p1(a∣x)p_1(a|\textbf{x})p1(a∣x)
-
给定用户特征x\textbf{x}x和aaa,预估用户对节点aaa的兴趣p2(b∣a;x)p_2(b|a;\textbf{x})p2(b∣a;x)
-
给定用户特征x\textbf{x}x,aaa,bbb,预估用户对节点aaa的兴趣p3(c∣a,b;x)p_3(c|a,b;\textbf{x})p3(c∣a,b;x)
-
预估用户对path=[a,b,c]path=[a,b,c]path=[a,b,c]的兴趣:
p(a,b,c∣x)=p1(a∣x)×p1(b∣a;x)×p3(c∣a,b;x) p(a,b,c|\textbf{x})=p_1(a|\textbf{x})\times p_1(b|a;\textbf{x})\times p_3(c|a,b;\textbf{x}) p(a,b,c∣x)=p1(a∣x)×p1(b∣a;x)×p3(c∣a,b;x)
7.2.2 模型具体流程
用户特征->神经网络->softmax->p1p_1p1->选择aaa
用户特征concat emb(a)concat~emb(a)concat emb(a)->神经网络->softmax->p2p_2p2->选择bbb
用户特征concat emb(a) concat emb(b)concat~emb(a)~concat~emb(b)concat emb(a) concat emb(b)->神经网络->softmax->p3p_3p3->选择ccc
7.3 线上召回
召回:用户->路径->物品
- 给定用户特征,用beam search召回一批路径。
- 利用索引path−>List⟨item⟩path->List\langle item\ranglepath−>List⟨item⟩ 召回一批物品(每条路径对应多个物品)。
- 对物品做打分和排序(排序模型没有限制),选出一个子集。
7.3.1 Beam Search
- 假设有3层,每层KKK个节点,那么一共有K3K^3K3条路径
- 用神经网络给所有K3K^3K3条路径打分,计算量太大,用beam search可以减小计算量
- beam search需要设置超参数beam size\text{beam size}beam size(*类似贪心算法,每一层选取前k=beam sizek=\text{beam size}k=beam size*个节点)
7.4 训练
同时学习神经网络参数和物品表征。
- 训练时只使用正样本:(user,item):=click(user,item)=1(user,item):=click(user,item)=1(user,item):=click(user,item)=1
7.4.1 学习神经网络参数(用户-路径的关系)
所需的数据:item->List<Path>的索引和用户点击过的物品
- 物品表征为JJJ条路径:[a1,b1,c1],...,[aJ,bJ,cJ][a_1,b_1,c_1],...,[a_J,b_J,c_J][a1,b1,c1],...,[aJ,bJ,cJ]
- 用户对路径[a,b,c][a,b,c][a,b,c]的兴趣:p(a,b,c∣x)p(a,b,c|\textbf{x})p(a,b,c∣x)
- 如果用户点击过物品,则说明用户对第JJJ条路径感兴趣
- 优化目标:最大化∑j=1Jp(aj,bj,cj∣x)\sum_{j=1}^J p(a_j,b_j,c_j|\textbf{x})∑j=1Jp(aj,bj,cj∣x)- > 损失函数:L=−log(∑j=1Jp(aj,bj,cj∣x))L=-\log(\sum_{j=1}^J p(a_j,b_j,c_j|\textbf{x}))L=−log(∑j=1Jp(aj,bj,cj∣x))
7.4.2 学习物品表征(物品-路径的关系)
所需的数据:神经网络对路径的评分和用户点击过的物品
-
用户对路径的兴趣记作p(path∣user)=p(a,b,c∣x)p(path|user) = p(a,b,c|\textbf{x})p(path∣user)=p(a,b,c∣x)
-
物品item和路径path的相关性:
score(item,path)=∑userp(path∣user)×click(user,item) \text{score}(item,path)=\sum_{user}p(path|user)\times\text{click}(user,item) score(item,path)=user∑p(path∣user)×click(user,item) -
选出JJJ条路径Π=path1,...,pathJ\Pi={path_1,...,path_J}Π=path1,...,pathJ,作为物品的表征
-
损失函数:L=−log(∑j=1Jscore(item,pathj))L=-\log(\sum_{j=1}^J\text{score}(item,path_j))L=−log(∑j=1Jscore(item,pathj))
-
正则项(避免过多的item集中在一条path):reg(pathj)=(number of items on pathj)4\text{reg}(path_j)=(number~of~items~on~path_j)^4reg(pathj)=(number of items on pathj)4
-> 用贪心算法更新路径:假设已经把物品表征为JJJ条路径,每次固定{pathi}i≠l\{path_i\}_{i\neq l}{pathi}i=l,并从未被选中的路径中,选出一条作为新的pathlpath_lpathl:
pathl←argminpathlL+α⋅reg(pathl)
path_l \larr \arg\min_{path_l}L+\alpha\cdot \text{reg}(path_l)
pathl←argpathlminL+α⋅reg(pathl)
8 其他找回通道
8.1 地理位置召回
- GeoHash召回(索引:GeoHash->优质笔记列表(时间倒排),这条召回通道没有个性化)
- 同城召回(索引:城市->优质笔记列表(时间倒排))
8.2 作者召回
- 关注作者召回(索引:用户->关注的作者;作者->发布的笔记)
- 有交互的作者召回(索引:用户->有交互的作者)
- 相似作者交互(索引:作者->相似作者)
8.3 缓存召回(复用前nnn次推荐精排的结果)
缓存大小固定,需要退场机制。
9 曝光过滤 & Bloom Filter
9.1 曝光过滤问题
如果用户看过某个物品,则不再把该物品曝光给该用户。对于每个用户,记录已经曝光给他的物品。对于每个召回的物品,判断是否已经曝光给用户,排除掉曾经曝光过的物品。
暴力对比的时间复杂度太高。
9.2 布隆过滤器(Bloom Filter)
布隆过滤器判断不存在,则一定不存在;判断存在,则可能存在,也可能不存在。
曝光集合大小为nnn,二进制向量维度为mmm,使用kkk个哈希函数,则Bloom filter误伤概率为δ=(1−exp(−knm)k\delta=(1-\exp(-\frac{kn}{m})^kδ=(1−exp(−mkn)k
- 缺点:只支持添加物品、不支持删除物品。
9.3 曝光过滤的链路
召回->排序->物品->实时流处理(Kafka+Flink)->曝光过滤服务(Bloom Filter)->召回