阅读本文将get到以下几点:
- I2I在推荐系统中的作用
- 工业推荐系统I2I计算的常用算法
- 有哪些常用的trick
- 在大数据平台上抽象出计算框架
- 基于Hive+UDF的工业级代码实现
基于I2I的推荐系统召回

推荐系统在召回阶段,有两大主流的检索形式。一种是构建物品之间的I2I关系,通过协同过滤算法或content-base方法,将物品的相似物品找到,它在线存储的方式是KV/KKV或倒排索引,用户的历史行为作为trigger(行为触发),查询索引服务,拿到相似的物品集合,然后聚合得到用户的topk召回集合。另一种是向量召回,将用户和物品通过双塔的方式进行监督学习,产出一个或多个embedding,将用户侧的图拆出为在线服务,item侧图离线跑出候选集合的embedding,使用类似faiss的库进行K近邻索引,在线召回时给user侧的图灌入特征,得到embeddding,然后查询K近邻索引,得到topk的召回集合。当然,还有其它的思路。比如,阿里TDM通过大根堆,可以算的上第三种形式了,但毕竟因计算复杂,ROI低,不算主流,不再赘述。
基于I2I的召回,通过开源的中间件构建起来非常容易,详见:
I2I是电商召回的基石
I2I基于的物品协同过滤算法,无论是启发式还是model-base,这种算法只需要用户和物品之间的行为关系就可以计算得到,可谓简单至极。如今发展出各种各样的基于深度学习的双塔向量召回模型,复杂至极,那简单的I2I还有没有生存空间,答案是有的,至少在电商场景是这样的。
为什么说强调电商场景?据说在非电商的内容场景下,如新闻、短视频等场景,主流的方法是U2I的向量召回。推荐系统无非也就这两大类,电商与内容。电商用户是购物兴趣主导的,在特定的时间特征的用户需求下,用户的兴趣集中度非常高,如果你推荐出了兴趣无关的热门商品,那就是badcase;内容场景下,用户的兴趣则比较泛华,没有那么强的兴趣,国家大事、娱乐新闻,这种高热内容,怎么推都不过分,反而是如果兴趣过于集中,用户会疲劳,反而增加用户的跳失概率。电商需要围绕用户购物意图精准推荐,基于协同过滤的I2I能达到这个目标。想想item-base的cf就是亚马逊提出的,人家是搞电商的。
I2I的精准,很难被其他算法替代。向量召回在高维向量空间里,有很强的泛化性,它满足用户泛化的那部分诉求,但精准兴趣的那部分诉求上,也很难击败I2I。在深度学习时代之前,就有基于矩阵分解的MF算法,它可以算是先今深度学习方法的始祖,一脉相同,表现出了相同的性质。早在那时就有人对两种方法做过对比分析,并融合使用。
Neighborhood models are most effective at detecting very localized relationships, but unable to capture the totality of weak signals encompassed in all of a user’s ratings. Latent factor models are generally effective at estimating overall structure that relates simultaneously to most or all items. However, these models are poor at detecting strong associations among a small set of closely related items.
其大意是说,基于邻域的方法(I2I)擅长捕捉localized的关系,但是那种弱的全局的信息则难以捕捉。而基于隐变量的模型(向量)擅长捕捉global的物品关系,但在局部上一个相似的物品子集上的强关系则难以捕捉。举个例子,假如在你的场景数据里,一小部分人同时喜欢啤酒和尿布,但啤酒和白酒、尿布和抹布在两大人群的行为中共现了,而这两个人群有明显的区别,基于邻域的算法里能捕捉到啤酒和尿布,它只需要在物品之间增加一个边即可,但基于向量的高维空间方法,要保证白酒和啤酒空间相近,尿布和抹布相近,同时白酒和抹布又离得足够远,则丢失掉了啤酒和尿布的关系。(选段来自《Factorization Meets The Neighborhood: a Multifaceted Collaborative Filtering Model》)
再换一个角度,从参数规模上来讲,I2I也是更大更强。I2I可以在每一个物品之间建立关系,其参数空间的上限是物品空间的平方级别,平均参数大小可以是物品空间的上百倍。在DeepMatch的model里,参数空间主要集中在embedding上,未必有I2I的大。
有哪些经典的I2I算法
item-CF
亚马逊提出的经典方法,在用户和物品的矩阵上,每一个列向量代表一个物品的表示,列向量之间可以计算余弦距离,则为物品的相似度。基于这样的思想,可以扩展到Jaccard距离、欧式距离等任意的距离公式上。
在这个公式里分子是同时访问了两个物品的用户数,分母是访问过物品的用户数乘积的开方。直观的理解就是,如果两个物品共线的用户session数越多,那么他们之间相似度越高,当然如果一个物品的热度高,共现数也会高,会造成所有物品都跟热门商品相似的badcase,所以在分母上进行了热度惩罚。
Adamic-Adar
这个方法最早来源于社交网络节点间相似性计算,含义也非常简单,那就是物品之间的相似度区取决于共现的用户数量,但在权重上有所区分,用户的物品集合越大,说明它兴趣越广泛,其中物品的两两相似度就越低。
这个方法在公开数据集测评上效果很惨,但是工业界在某些场景可以取得比item-CF更好的效果。主要的原因在于它没有热度抑制,倾向于在热门集合里找相似。
item-CF-IUF
增加了IUF(Inverse User Frequence)修正,用户越活跃,它下面的物品共现传达的相似性越低。看看它是不是Adamic和item-cf的合体。
Swing

我们把基于邻域的item-CF想象成一张图,物品连接用户再连接物品(I-U-I),上述所有的方法,都是在累计一条边上的权重,算法都侧重在一条边上如何定义权重,Swing的奇思妙想是在考虑双边,双边会形成一个秋千形状(四边形),因而得名。如果两个物品之间有n个边(用户),那么累加的权重个数是 ,权重在双边上定义为两个用户共同访问物品数的倒数。
Swing在公开数据集上表现很差,可能也只有淘宝这样的地方,超大规模的商品集合,超大的用户行为量,以搜索点击为主的优质行为session,才能诞生Swing这样牛逼的算法。
LLR(log-likelihood ratio)

计算似然比,用于度量两个物品被访问事件的不独立性,也就是相关性。上图方框代表整个事件空间,昨天的椭圆A代表物品a被用户点击的事件,B代表物品b被用户点击的事件。k11是用户同时点击ab的数量,k12是用户点击a不点击b的数量,k21是用户不点击a点击b的数量,k22是用户两个物品都没点击的数量。我们把这四个数写到一个表格里,计算行熵、列熵和矩阵熵,最终的相似分为-2(行熵+列熵-矩阵熵)。
在计算上,它很简单。在数学上,并不好理解,我们举例子说明一下。在一般的物品协同方法里,我们会累计边权,这就像是在看k11的数量,k11的数量越大,说明两个物品越相似。但如果k11大的原因,是因为k11+k12大,也就是说a是个热门物品,即便k11(共现)很大,也没有什么意义。同理,如果是因为k11+k21大,也就是说b是一个热门物品,同样ab共现的价值也减小了。
Slim (Sparse Linear Methods)

上述都是启发式的方法,而Slim考虑如何通过机器学习构建出物品间的相似关系,其采用的办法是对于用户的行为序列,mask掉其中一个物品,用其他物品乘以权重的累加和拟合这个物品的得分,限制物品间权重为正并进行稀疏化,得到的权重就是I2I的相似分数。
Item2Vec&DeepI2I
对物品向量化,然后通过K近邻查找,也可以用来构建I2I。
Item2Vec的思路是在用户行为序列上通过skip-gram和负采样构建样本,类似word2vec学习到物品的向量表示。DeepI2I则引入了side-infomation和深度神经网络,样本的构建方式则大同小异,side-infomation的引入和向量空间的近邻增加了其泛化性,缓解了物品的冷启问题。
对物品向量化后,通过K近邻查找得到topk相似物品。可以参考离线大规模I2I的构建工具:
物品邻域I2I统一的计算框架

先说说物品邻域I2I的计算框架。如果用MapReduce来计算,仅仅需要一个MapReduce过程就能完成,在Hive平台上,也可以用一个UDTF、一个UDAF函数来替代。
数据准备,将用户行为聚合成一条记录。
step1,maper或UDTF中,将数据构造成trigger_item->item_neighbors的格式,举个例子,如果用户行为集合包含A、B、C,那么会得到A->B、C,B->A、C,C->A、B。
step2,reducer或UDAF中,将数据以trigger_item为key聚合,得到物品的邻域集合,它所需要的计算物料都集中在单机内存里了,可以进行任意的算子运算了,比如item-CF,你需要过一遍邻域集合,累加权重,比如Swing,你需要双重for循环邻域集合,计算双边的权重。计算好之后,拿小根堆取topk即可落盘。
代码库:
你可能需要一些tricks
trick往往是跟工业实践及实际场景强相关的。
跨域I2I
什么叫跨域?比如你的推荐场景是内容的feeds,但是你是在电商的app里,很多你场景里的新用户,实际上是电商的老用户,可以用电商行为做trigger召回内容,这就需要构建电商->内容的I2I。
在数据准备阶段,聚合好用户的电商行为和内容行为,在计算框架的step1中,构建电商到内容邻域的pair即可,其余的无需改变,得到的结果就是每个商品的topk相似内容。
在滑动窗口里构建物品和邻域pair
用户的行为集合很大,这时候不仅在mapper阶段构造邻域数据会有性能问题,算法效果上也存在问题,用户很大的行为集合意味着可能包含多种多样的兴趣,我们在构造物品和邻域pair的过程中,可以考虑用一个以时间或数量为准的滑动窗口,在按时间排序的行为序列中滑动并在窗口内构造邻域pair。这样既减小了数据运算量,有能增厚算法效果,用户在电商场景内往往短时间内是兴趣集中的。当然,物品间的行为时间间隔也可以加入边权计算,进一步提升效果。
对物品的邻域集合进行采样
有的物品热度很高,在step2中需要以trigger_item为key进行聚合,如果物品被点击了100w次,那么它将进入reducer,性能也无法承受,可以再构造时就进行丢弃。我们在数据准备阶段就计算好物品的UV数量,step1中构造时生成一个0-1间随机数,如果随机数小于1k/uv,则进行构建,否则丢弃,它的含义是,如果这个物品的uv是小于1k的,那么1k/uv>1,则不作丢弃,如果物品uv是大于1k的,按概率丢弃,最终进入reducer的大约就是1k。
边分强弱,采样时优先保留强边
物品和用户之间的关系也有强弱之分。比如同样都是商品点击,有的在详情页停留了10s,有的则停留了100s,显然100s的更能表达用户强烈的兴趣,在上面丢弃邻域的过程中,可以计算一个权重,1k*weight/uv,100s给更高的权重,丢弃时优先丢弃弱的边,在reducer中再取强边邻域集合进行运算。当然,边的强弱也可以纳入权重计算。
trigger时间衰减
在聚合用户的topk召回结果时,每一个trigger的权重是不同的,一般时间越靠近现在,这个兴趣点越重要。可以使用牛顿冷却公式,进行trigger的时间衰减。
当前是时间为 ,trigger的行为时间为 , 是个衰减权重。比如t的单位是小时, 设置为0.01,当前权重是1,24小时权重衰减为0.787,七天衰减为0.186。调整 控制合适的衰减强度。
引用