第一章 推荐系统概述
1.1推荐系统的意义
推荐系统就是一个将信息生产者和信息消费者连接起来的桥梁。平台往往会作为推荐系统的载体,实现信息生产者和消费者之间信息的匹配。上述提到的平台方、信息生产者和消费者可以分别用平台方(如:腾讯视频、淘宝、网易云音乐等)、物品(如:视频、商品、音乐等)和用户和来指代。下面分别从这三方需求出发,介绍推荐系统的存在的意义。
平台方
平台方一般是为信息生产者提供物品展示的位置,然后通过不同的方式吸引用户来到平台上寻找他们感兴趣的物品。平台通过商家对物品的展示以及用户的浏览、观看或下单等行为,就产生了所谓的”流量”。
对平台方而言,流量的高效利用是推荐系统存在的重要原因。以典型的电商网站一般具有如图所示的树状拓扑结构,树状结构在连通性方面有着天然的劣势,阻碍这流量的高效流通。推荐系统的出现使得原本的树状结构变成网络拓扑结构,大大增强了整个网络的连通性。推荐模块不仅使用户在当前页面有了更好的选择路径,同时也给了每个商品增加入口和展示机会,进而提高了成交概率。而推荐质量的好坏,直接决定了用户选择这条路径的可能性,进而影响着流量的利用效率。
推荐和搜索的区别
搜索和推荐都是解决互联网大数据时代信息过载的手段,但是它们也存在着许多的不同:
- 用户意图:搜索时的用户意图是非常明确的,用户通过查询的关键词主动发起搜索请求。对于推荐而言,用户的需求是不明确的,推荐系统在通过对用户历史兴趣的分析给用户推荐他们可能感兴趣的内容。
- 个性化程度:对于搜索而言,由于限定的了搜索词,所以展示的内容对于用户来说是有标准答案的,所以搜索的个性化程度较低。而对于推荐来说,推荐的内容本身就是没有标准答案的,每个人都有不同的兴趣,所以每个人展示的内容,个性化程度比较强。
- 优化目标:对于搜索系统而言,更希望可以快速地、准确地定位到标准答案,所以希望搜索结果中答案越靠前越好,通常评价指标有:归一化折损累计收益(NDCG)、精确率(Precision)和召回率(Recall)。对于推荐系统而言,因为没有标准的答案,所以优化目标可能会更宽泛。例如用户停留时长、点击、多样性,评分等。不同的优化目标又可以拆解成具体的不同的评价指标。
- 马太效应和长尾理论:对于搜索系统来说,用户的点击基本都集中在排列靠前的内容上,对于排列靠后的很少会被关注,这就是马太效应。而对于推荐系统来说,热门物品被用户关注更多,冷门物品不怎么被关注的现象也是存在的,所以也存在马太效应。此外,在推荐系统中,冷门物品的数量远远高于热门物品的数量,所以物品的长尾性非常明显。
1.2推荐系统的架构
推荐和搜索系统核心的的任务是从海量物品中找到用户感兴趣的内容。在这个背景下,推荐系统包含的模块非常多,每个模块将会有很多专业研究的工程和研究工程师,作为刚入门的应届生或者实习生很难对每个模块都有很深的理解,实际上也大可不必,我们完全可以从学习好一个模块技术后,以点带面学习整个系统,虽然正式工作中我们放入门每个人将只会负责的也是整个系统的一部分。但是掌握推荐系统最重要的还是梳理清楚整个推荐系统的架构,知道每一个部分需要完成哪些任务,是如何做的,主要的技术栈是什么,有哪些局限和可以研究的问题,能够对我们学习推荐系统有一个提纲挈领的作用。
所以这篇文章将会从系统架构和算法架构两个角度出发解析推荐系统通用架构。系统架构设计思想是大数据背景下如何有效利用海量和实时数据,将推荐系统按照对数据利用情况和系统响应要求出发,将整个架构分为离线层、近线层、在线层三个模块。然后分析这三个模块分别承担推荐系统什么任务,有什么制约要求。这种角度不和召回、排序这种通俗我们理解算法架构,因为更多的是考虑推荐算法在工程技术实现上的问题,系统架构是如何权衡利弊,如何利用各种技术工具帮助我们达到想要的目的的,方便我们理解为什么推荐系统要这样设计。
而算法架构是从我们比较熟悉的召回、粗排、排序、重排等算法环节角度出发的,重要的是要去理解每个环节需要完成的任务,每个环节的评价体系,以及为什么要那么设计。还有一个重要问题是每个环节涉及到的技术栈和主流算法,这部分非常重要而且篇幅较大,所以我们放在下一个章节讲述。
架构设计是一个非常大的话题,设计的核心在于平衡和妥协。在推荐系统不同时期、不同的环境、不同的数据,架构都会面临不一样的问题。网飞官方博客有一段总结:
We want the ability to use sophisticated machine learning algorithms that can grow to arbitrary complexity and can deal with huge amounts of data. We also want an architecture that allows for flexible and agile innovation where new approaches can be developed and plugged-in easily. Plus, we want our recommendation results to be fresh and respond quickly to new data and user actions. Finding the sweet spot between these desires is not trivial: it requires a thoughtful analysis of requirements, careful selection of technologies, and a strategic decomposition of recommendation algorithms to achieve the best outcomes for our members. “我们需要具备使用复杂机器学习算法的能力,这些算法要可以适应高度复杂性,可以处理大量数据。我们还要能够提供灵活、敏捷创新的架构,新的方法可以很容易在其基础上开发和插入。而且,我们需要我们的推荐结果足够新,能快速响应新的数据和用户行为。找到这些要求之间恰当的平衡并不容易,需要深思熟虑的需求分析,细心的技术选择,战略性的推荐算法分解,最终才能为客户达成最佳的结果。”
所以在思考推荐系统架构考虑的第一个问题是确定边界:知道推荐系统要负责哪部分问题,这就是边界内的部分。在这个基础上,架构要分为哪几个部分,每一部分需要完成的子功能是什么,每一部分依赖外界的什么。了解推荐系统架构也和上文讲到的思路一样,我们需要知道的是推荐系统要负责的是怎么问题,每一个子模块分别承担了哪些功能,它们的主流技术栈是什么。从这个角度来阅读本文,将会有助于读者抓住重点。
系统架构
推荐系统架构,首先从数据驱动角度,对于数据,最简单的方法是存下来,留作后续离线处理,离线层就是我们用来管理离线作业的部分架构。在线层能更快地响应最近的事件和用户交互,但必须实时完成。这会限制使用算法的复杂性和处理的数据量。离线计算对于数据数量和算法复杂度限制更少,因为它以批量方式完成,没有很强的时间要求。不过,由于没有及时加入最新的数据,所以很容易过时。
个性化架构的关键问题,就是如何以无缝方式结合、管理在线和离线计算过程。近线层介于两种方法之间,可以执行类似于在线计算的方法,但又不必以实时方式完成。这种设计思想最经典的就是Netflix在2013年提出的架构,整个架构分为
- 离线层:不用实时数据,不提供实时响应;
- 近线层:使用实时数据,不保证实时响应;
- 在线层:使用实时数据,保证实时在线服务;
设计思想
网飞的这个架构提出的非常早,其中的技术也许不是现在常用的技术了,但是架构模型仍然被很多公司采用。
这个架构为什么要这么设计,本质上是因为推荐系统是由大量数据驱动的,大数据框架最经典的就是lambda架构和kappa架构。而推荐系统在不同环节所使用的数据、处理数据的量级、需要的读取速度都是不同的,目前的技术还是很难实现一套端到端的及时响应系统,所以这种架构的设计本质上还是一种权衡后的产物,所以有了下图这种模型:


整个数据部分其实是一整个链路,主要是三块,分别是客户端及服务器实时数据处理、流处理平台准实时数据处理和大数据平台离线数据处理这三个部分。
看到这里,一个很直观的问题就是,为什么数据处理需要这么多步骤?这些步骤都是干嘛的,存在的意义是什么?
我们一个一个来说,首先是客户端和服务端的实时数据处理。这个很好理解,这个步骤的工作就是记录。将用户在平台上真实的行为记录下来,比如用户看到了哪些内容,和哪些内容发生了交互,和哪些没有发生了交互。如果再精细一点,还会记录用户停留的时间,用户使用的设备等等。除此之外还会记录行为发生的时间,行为发生的session等其他上下文信息。
这一部分主要是后端和客户端完成,行业术语叫做埋点。所谓的埋点其实就是记录点,因为数据这种东西需要工程师去主动记录,不记录就没有数据,记录了才有数据。既然我们要做推荐系统,要分析用户行为,还要训练模型,显然需要数据。需要数据,就需要记录。
第二个步骤是流处理平台准实时数据处理,这一步是干嘛的呢,其实也是记录数据,不过是记录一些准实时的数据。很多同学又会迷糊了,实时我理解,就是当下立即的意思,准实时是什么意思呢?准实时的意思也是实时,只不过没有那么即时,比如可能存在几分钟的误差。这样存在误差的即时数据,行业术语叫做准实时。那什么样的准实时数据需要记录呢?在推荐领域基本上只有一个类别,就是用户行为数据。也就是用户在观看这个内容之前还看过哪些内容,和哪些内容发生过交互。理想情况这部分数据也需要做成实时,但由于这部分数据量比较大,并且逻辑也相对复杂,所以很难做到非常实时,一般都是通过消息队列加在线缓存的方式做成准实时。
最后我们看第三个步骤,叫做离线数据处理,离线也就是线下处理,基本上就没有时限的要求了。
一般来说,离线处理才是数据处理的大头。所有“脏活累活”复杂的操作都是在离线完成的,比如说一些join操作。后端只是记录了用户交互的商品id,我们需要商品的详细信息怎么办?需要去和商品表关联查表。显然数据关联是一个非常耗时的操作,所以只能放到离线来做。
接下来详细介绍一下这三个模块。
离线层
离线层是计算量最大的一个部分,它的特点是不依赖实时数据,也不需要实时提供服务。需要实现的主要功能模块是:
- 数据处理、数据存储;
- 特征工程、离线特征计算;
- 离线模型的训练;
这里我们可以看出离线层的任务是最接近学校中我们处理数据、训练模型这种任务的,不同可能就是需要面临更大规模的数据。离线任务一般会按照天或者更久运行,比如每天晚上定期更新这一天的数据,然后重新训练模型,第二天上线新模型。

离线层优势和不足
离线层面临的数据量级是最大的,面临主要的问题是海量数据存储、大规模特征工程、多机分布式机器学习模型训练。目前主流的做法是HDFS,收集到我们所有的业务数据,通过HIVE等工具,从全量数据中抽取出我们需要的数据,进行相应的加工,离线阶段主流使用的分布式框架一般是Spark。所以离线层有如下的优势:
- 可以处理大量的数据,进行大规模特征工程;
- 可以进行批量处理和计算;
- 不用有响应时间要求;
但是同样的,如果我们只使用用户离线数据,最大的不足就是无法反应用户的实时兴趣变化,这就促使了近线层的产生。
近线层
近线层的主要特点是准实时,它可以获得实时数据,然后快速计算提供服务,但是并不要求它和在线层一样达到几十毫秒这种延时要求。近线层的产生是同时想要弥补离线层和在线层的不足,折中的产物。
它适合处理一些对延时比较敏感的任务,比如:
- 特征的事实更新计算:例如统计用户对不同type的ctr,推荐系统一个老生常谈的问题就是特征分布不一致怎么办,如果使用离线算好的特征就容易出现这个问题。近线层能够获取实时数据,按照用户的实时兴趣计算就能很好避免这个问题。
- 实时训练数据的获取:比如在使用DIN、DSIN这行网络会依赖于用户的实时兴趣变化,用户几分钟前的点击就可以通过近线层获取特征输入模型。
- 模型实时训练:可以通过在线学习的方法更新模型,实时推送到线上;
近线层的发展得益于最近几年大数据技术的发展,很多流处理框架的提出大大促进了近线层的进步。如今Flink、Storm等工具一统天下。

在线层
在线层,就是直接面向用户的的那一层了。最大的特点是对响应延时有要求,因为它是直接面对用户群体的,你可以想象你打开抖音淘宝等界面,几乎都是秒刷出来给你的推荐结果的,不会说还需要让你等待几秒钟时间。所有的用户请求都会发送到在线层,在线层需要快速返回结果,它主要承担的工作有:
- 模型在线服务;包括了快速召回和排序;
- 在线特征快速处理拼接::根据传入的用户ID和场景,快速读取特征和处理;
- AB实验或者分流:根据不同用户采用不一样的模型,比如冷启动用户和正常服务模型;
- 运筹优化和业务干预:比如要对特殊商家流量扶持、对某些内容限流;
典型的在线服务是用过RESTful/RPC等提供服务,一般是公司后台服务部门调用我们的这个服务,返回给前端。具体部署应用比较多的方式就是使用Docker在K8S部署。而在线服务的数据源就是我们在离线层计算好的每个用户和商品特征,我们事先存放在数据库中,在线层只需要实时拼接,不进行复杂的特征运算,然后输入近线层或者离线层已经训练好的模型,根据推理结果进行排序,最后返回给后台服务器,后台服务器根据我们对每一个用户的打分,再返回给用户。
在线层最大的问题就是对实时性要求特别高,一般来说是几十毫秒,这就限制了我们能做的工作,很多任务往往无法及时完成,需要近线层协助我们做。
算法架构
我们在入门学习推荐系统的时候,更加关注的是哪个模型AUC更高、topK效果好,哪个模型更加牛逼的问题,从基本的协同过滤到点击率预估算法,从深度学习到强化学习,学术界都始终走在最前列。一个推荐算法从出现到在业界得到广泛应用是一个长期的过程,因为在实际的生产系统中,首先需要保证的是稳定、实时地向用户提供推荐服务,在这个前提下才能追求推荐系统的效果。

- 召回
召回层的主要目标时从推荐池中选取几千上万的item,送给后续的排序模块。由于召回面对的候选集十分大,且一般需要在线输出,故召回模块必须轻量快速低延迟。由于后续还有排序模块作为保障,召回不需要十分准确,但不可遗漏(特别是搜索系统中的召回模块)。
如果没有召回层,每个User都能和每一个Item去在线排序阶段预测目标概率,理论上来说是效果最好,但是是不现实的,线上不延迟允许,所以召回和粗排阶段就要做一些候选集筛选的工作,保证在有限的能够给到排序层去做精排的候选集的时间里,效果最大化。另一个方面就是通过这种模型级联的方式,可以减少用排序阶段拟合多目标的压力,比如召回阶段我们现在主要是在保证Item质量的基础上注重覆盖率多样性,粗排阶段主要用简单的模型来解决不同路的召回和当前用户的相关性问题,最后截断到1k个以内的候选集,这个候选集符合一定的个性化相关性、视频质量和多样性的保证,然后做ranking去做复杂模型的predict。
目前基本上采用多路召回解决范式,分为非个性化召回和个性化召回。个性化召回又有content-based、behavior-based、feature-based等多种方式。
召回主要考虑的内容有:
- 考虑用户层面:用户兴趣的多元化,用户需求与场景的多元化:例如:新闻需求,重大要闻,相关内容沉浸阅读等等
- 考虑系统层面:增强系统的鲁棒性;部分召回失效,其余召回队列兜底不会导致整个召回层失效;排序层失效,召回队列兜底不会导致整个推荐系统失效
- 系统多样性内容分发:图文、视频、小视频;精准、试探、时效一定比例;召回目标的多元化,例如:相关性,沉浸时长,时效性,特色内容等等
- 可解释性推荐一部分召回是有明确推荐理由的:很好的解决产品性数据的引入;
- 粗排
粗排的原因是有时候召回的结果还是太多,精排层速度还是跟不上,所以加入粗排。粗排可以理解为精排前的一轮过滤机制,减轻精排模块的压力。粗排介于召回和精排之间,要同时兼顾精准性和低延迟。目前粗排一般也都模型化了,其训练样本类似于精排,选取曝光点击为正样本,曝光未点击为负样本。但由于粗排一般面向上万的候选集,而精排只有几百上千,其解空间大很多。
粗排阶段的架构设计主要是考虑三个方面,一个是根据精排模型中的重要特征,来做候选集的截断,另一部分是有一些召回设计,比如热度或者语义相关的这些结果,仅考虑了item侧的特征,可以用粗排模型来排序跟当前User之间的相关性,据此来做截断,这样是比单独的按照item侧的倒排分数截断得到更加个性化的结果,最后是算法的选型要在在线服务的性能上有保证,因为这个阶段在pipeline中完成从召回到精排的截断工作,在延迟允许的范围内能处理更多的召回候选集理论上与精排效果正相关。
- 精排
精排层,也是我们学习推荐入门最常常接触的层,我们所熟悉的算法很大一部分都来自精排层。这一层的任务是获取粗排模块的结果,对候选集进行打分和排序。精排需要在最大时延允许的情况下,保证打分的精准性,是整个系统中至关重要的一个模块,也是最复杂,研究最多的一个模块。
精排是推荐系统各层级中最纯粹的一层,他的目标比较单一且集中,一门心思的实现目标的调优即可。最开始的时候精排模型的常见目标是ctr,后续逐渐发展了cvr等多类目标。精排和粗排层的基本目标是一致的,都是对商品集合进行排序,但是和粗排不同的是,精排只需要对少量的商品(即粗排输出的商品集合的topN)进行排序即可。因此,精排中可以使用比粗排更多的特征,更复杂的模型和更精细的策略(用户的特征和行为在该层的大量使用和参与也是基于这个原因)。
精排层模型是推荐系统中涵盖的研究方向最多,有非常多的子领域值得研究探索,这也是推荐系统中技术含量最高的部分,毕竟它是直接面对用户,产生的结果对用户影响最大的一层。目前精排层深度学习已经一统天下了,精排阶段采用的方案相对通用,首先一天的样本量是几十亿的级别,我们要解决的是样本规模的问题,尽量多的喂给模型去记忆,另一个方面时效性上,用户的反馈产生的时候,怎么尽快的把新的反馈给到模型里去,学到最新的知识。
- 重排
常见的有三种优化目标:Point Wise、Pair Wise 和 List Wise。重排序阶段对精排生成的Top-N个物品的序列进行重新排序,生成一个Top-K个物品的序列,作为排序系统最后的结果,直接展现给用户。重排序的原因是因为多个物品之间往往是相互影响的,而精排序是根据PointWise得分,容易造成推荐结果同质化严重,有很多冗余信息。而重排序面对的挑战就是海量状态空间如何求解的问题,一般在精排层我们使用AUC作为指标,但是在重排序更多关注NDCG等指标。
重排序在业务中,获取精排的排序结果,还会根据一些策略、运营规则参与排序,比如强制去重、间隔排序、流量扶持等、运营策略、多样性、context上下文等,重新进行一个微调。重排序更多的是List Wise作为优化目标的,它关注的是列表中商品顺序的问题来优化模型,但是一般List Wise因为状态空间大,存在训练速度慢的问题。
由于精排模型一般比较复杂,基于系统时延考虑,一般采用point-wise方式,并行对每个item进行打分。这就使得打分时缺少了上下文感知能力。用户最终是否会点击购买一个商品,除了和它自身有关外,和它周围其他的item也息息相关。重排一般比较轻量,可以加入上下文感知能力,提升推荐整体算法效率。比如三八节对美妆类目商品提权,类目打散、同图打散、同卖家打散等保证用户体验措施。重排中规则比较多,但目前也有不少基于模型来提升重排效果的方案。
- 混排
多个业务线都想在Feeds流中获取曝光,则需要对它们的结果进行混排。比如推荐流中插入广告、视频流中插入图文和banner等。可以基于规则策略(如广告定坑)和强化学习来实现。
总结
整篇文章从系统架构梳理了Netfliex的经典推荐系统架构,整个架构更多是偏向实时性能和效果之间trade off的结果。如果从另外的角度看推荐系统架构,比如从数据流向,或者说从推荐系统各个时序依赖来看,就是我们最熟悉的召回、粗排、精排、重排、混排等模块了。这种角度来看是把推荐系统从前往后串起来,其中每一个模块既有在离线层工作的,也有在在线层工作的。而从数据驱动角度看,更能够看到推荐系统的完整技术栈,推荐系统当前面临的局限和发展方向。
召回、排序这些里面单拿出任何一个模块都非常复杂。这也是为什么大家都说大厂拧螺丝的原因,因为很可能某个人只会负责其中很小的一个模块。许多人说起自己的模块来如数家珍,但对于全局缺乏认识,带来的结果是当你某天跳槽了或者是工作内容变化了,之前从工作当中的学习积累很难沉淀下来,这对于程序员的成长来说是很不利的。
所以希望这篇文章能够帮助大家在负责某一个模块,优化某一个功能的时候,除了能够有算法和数据,还能能够考虑对整个架构带来的影响,如何提升整体的一个性能,慢慢开阔自己的眼界,构建出一个更好的推荐系统。
1.3推荐系统的技术栈
推荐系统是一个非常大的框架,有非常多的模块在里面,完整的一套推荐系统体系里,不仅会涉及到推荐算法工程师、后台开发工程师、数据挖掘/分析工程师、NLP/CV工程师还有前端、客户端甚至产品、运营等支持。我们作为算法工程师,需要掌握的技术栈主要就是在算法和工程两个区域了,所以这篇文章将会分别从算法和工程两个角度出发,结合两者分析当前主流的一些推荐算法技术栈。
算法
首先我们从推荐系统架构出发,一种分法是将整个推荐系统架构分为召回、粗排、精排、重排、混排等模块。它的分解方法是从一份数据如何从生产出来,到线上服务完整顺序的一个流程。因为在不同环节,我们一般会考虑不同的算法,所以这种角度出发我们来研究推荐系统主流的算法技术栈。

为了帮助新手在后文方便理解,首先简单介绍这些模块的功能主要是:
- 召回:从推荐池中选取几千上万的item,送给后续的排序模块。由于召回面对的候选集十分大,且一般需要在线输出,故召回模块必须轻量快速低延迟。由于后续还有排序模块作为保障,召回不需要十分准确,但不可遗漏(特别是搜索系统中的召回模块)。目前基本上采用多路召回解决范式,分为非个性化召回和个性化召回。个性化召回又有content-based、behavior-based、feature-based等多种方式。
- 粗排:粗拍的原因是有时候召回的结果还是太多,精排层速度还是跟不上,所以加入粗排。粗排可以理解为精排前的一轮过滤机制,减轻精排模块的压力。粗排介于召回和精排之间,要同时兼顾精准性和低延迟。一般模型也不能过于复杂
- 精排:获取粗排模块的结果,对候选集进行打分和排序。精排需要在最大时延允许的情况下,保证打分的精准性,是整个系统中至关重要的一个模块,也是最复杂,研究最多的一个模块。精排系统构建一般需要涉及样本、特征、模型三部分。
- 重排:获取精排的排序结果,基于运营策略、多样性、context上下文等,重新进行一个微调。比如三八节对美妆类目商品提权,类目打散、同图打散、同卖家打散等保证用户体验措施。重排中规则比较多,但目前也有不少基于模型来提升重排效果的方案。
- 混排:多个业务线都想在Feeds流中获取曝光,则需要对它们的结果进行混排。比如推荐流中插入广告、视频流中插入图文和banner等。可以基于规则策略(如广告定坑)和强化学习来实现。
画像层
首先是推荐系统的物料库,这部分内容里,算法主要体现在如何绘制一个用户画像和商品画像。这个环节是推荐系统架构的基础设施,一般可能新用户/商品进来,或者每周定期会重新一次整个物料库,计算其中信息,为用户打上标签,计算统计信息,为商品做内容理解等内容。其中用户画像是大家比较容易理解的,比如用户年龄、爱好通常APP会通过注册界面收集这些信息。而商品画像形式就非常多了,比如淘宝主要推荐商品,抖音主要是短视频,所以大家的物料形式比较多,内容、质量差异也比较大,所以内容画像各家的做法也不同,当前比较主流的都会涉及到一个多模态信息内容理解。下面我贴了一个微信看一看的内容画像框架,然后我们来介绍下在这一块主要使用的算法技术。

一般推荐系统会加入多模态的一个内容理解。我们用短视频形式举个例子,假设用户拍摄了一条短视频,上传到了平台,从推荐角度看,首先我们有的信息是这条短视频的作者、长度、作者为它选择的标签、时间戳这些信息。但是这对于推荐来说是远远不够的,首先作者打上的标签不一定准确反映作品,原因可能是我们模型的语义空间可能和作者/现实世界不一致。其次我们需要更多维度的特征,比如有些用户喜欢看小姐姐跳舞,那我希望能够判断一条视频中是否有小姐姐,这就涉及到封面图的基于CV的内容抽取或者整个视频的抽取;再比如作品的标题一般能够反映主题信息,除了很多平台常用的用“#”加上一个标签以外,我们也希望能够通过标题抽取出基于NLP的信息。还有更多的维度可以考虑:封面图多维度的多媒体特征体系,包括人脸识别,人脸embedding,标签,一二级分类,视频embedding表示,水印,OCR识别,清晰度,低俗色情,敏感信息等多种维度。
这里面涉及的任务主要是CV的目标检测、语义分割等任务,NLP中的情感分析、摘要抽取、自然语言理解等任务。但是这部分算法一般团队都会有专门负责的组,不需要推荐算法工程师来负责,他们会有多模态的语意标签输出,主要形式是各种粒度的Embedding。我们只需要在我们的推荐模型中引入这些预训练的Embedding。
文本理解
这应该是用的最多的模态信息,包括item的标题、正文、OCR、评论等数据。这里面也可以产生不同粒度的信息,比如文本分类,把整个item做一个粗粒度的分类。
这里的典型算法有:RNN、TextCNN、FastText、Bert等;
关键词标签
相比文本分类,关键词是更细粒度的信息,往往是一个mutil-hot的形式,它会对item在我们的标签库的选取最合适的关键词或者标签。
这里典型的算法有:TF-IDF、Bert、LSTM-CRF等。
内容理解
在很多场景下,推荐的主题都是视频或者图片,远远多于仅推荐文本的情况,这里视频/图片item中的内容中除了文本的内容以外,更多的信息其实来源于视频/图片内容本身, 因此需要尝试从多种模态中抽取更丰富的信息。主要包括分类信息、封面图OCR的信息、视频标签信息等
这里典型的算法有:TSN、RetinaFace、PSENet等。
知识图谱
知识图谱作为知识承载系统,用于对接内外部关键词信息与词关系信息;内容画像会将原关系信息整合,并构建可业务应用的关系知识体系,其次,依赖业务中积累用户行为产生的实体关系数据,本身用户需求的标签信息,一并用于构建业务知识的兴趣图谱,基于同构网络与异构网络表示学习等核心模型,输出知识表示与表达,抽象后的图谱用于文本识别,推荐语义理解,兴趣拓展推理等场景,直接用于兴趣推理的冷启场景已经验证有很不错的收益。
这方面的算法有:KGAT、RippleNet等。
召回/粗排
推荐系统的召回阶段可以理解为根据用户的历史行为数据,为用户在海量的信息中粗选一批待推荐的内容,挑选出一个小的候选集的过程。粗排用到的很多技术与召回重合,所以放在一起讲,粗排也不是必需的环节,它的功能对召回的结果进行个粗略的排序,在保证一定精准的前提下,进一步减少往后传送的物品数量,这就是粗排的作用。

召回模块面对几百上千万的推荐池物料规模,候选集十分庞大。由于后续有排序模块作为保障,故不需要十分准确,但必须保证不要遗漏和低延迟。目前主要通过多路召回来实现,一方面各路可以并行计算,另一方面取长补短。可以看到各类同类竞品的系统虽然细节上多少存在差异,但不约而同的采取了多路召回的架构,这类设计考虑如下几点问题:
- 考虑用户层面:用户兴趣的多元化,用户需求与场景的多元化:例如:新闻需求,重大要闻,相关内容沉浸阅读等等
- 考虑系统层面:增强系统的鲁棒性;部分召回失效,其余召回队列兜底不会导致整个召回层失效;排序层失效,召回队列兜底不会导致整个推荐系统失效
- 系统多样性内容分发:图文、视频、小视频;精准、试探、时效一定比例;召回目标的多元化,例如:相关性,沉浸时长,时效性,特色内容等等
- 可解释性推荐一部分召回是有明确推荐理由的:很好的解决产品性数据的引入;
介绍了召回任务的目的和场景后,接下来分析召回层面主要的技术栈,因为召回一般都是多路召回,从模型角度分析有很多召回算法,这种一般是在召回层占大部分比例点召回,除此之外,还会有探索类召回、策略运营类召回、社交类召回等。接下来我们着重介绍模型类召回。
经典召回模型
随着技术发展,在Embedding基础上的模型化召回是一个技术发展潮流方向。这种召回的范式是通过某种算法,对user和item分别打上Embedding,然后user与item在线进行KNN计算实时查询最近邻结果作为召回结果,快速找出匹配的物品。需要注意的是如果召回采用模型召回方法,优化目标最好和排序的优化目标一致,否则可能被过滤掉。
在这方面典型的算法有:FM、双塔DSSM、Multi-View DNN等。
序列召回模型
推荐系统主要解决的是基于用户的隐式阅读行为来做个性化推荐的问题,序列模型一些基于神经网络模型学习得到Word2Vec模型,再后面的基于RNN的语言模型,最先用的最多的Bert,这些方法都可以应用到召回的学习中。
用户在使用 APP 或者网站的时候,一般会产生一些针对物品的行为,比如点击一些感兴趣的物品,收藏或者互动行为,或者是购买商品等。而一般用户之所以会对物品发生行为,往往意味着这些物品是符合用户兴趣的,而不同类型的行为,可能代表了不同程度的兴趣。比如购买就是比点击更能表征用户兴趣的行为。在召回阶段,如何根据用户行为序列打 embedding,可以采取有监督的模型,比如 Next Item Prediction 的预测方式即可;也可以采用无监督的方式,比如物品只要能打出 embedding,就能无监督集成用户行为序列内容,例如 Sum Pooling。
这方面典型的算法有:CBOW、Skip-Gram、GRU、Bert等。
用户序列拆分
上文讲了利用用户行为物品序列,打出用户兴趣 Embedding 的做法。但是,另外一个现实是:用户往往是多兴趣的,比如可能同时对娱乐、体育、收藏感兴趣。这些不同的兴趣也能从用户行为序列的物品构成上看出来,比如行为序列中大部分是娱乐类,一部分体育类,少部分收藏类等。那么能否把用户行为序列物品中,这种不同类型的用户兴趣细分,而不是都笼统地打到一个用户兴趣 Embedding 里呢?用户多兴趣拆分就是解决这类更细致刻画用户兴趣的方向。
本质上,把用户行为序列打到多个 embedding 上,实际它是个类似聚类的过程,就是把不同的 Item,聚类到不同的兴趣类别里去。目前常用的拆分用户兴趣 embedding 的方法,主要是胶囊网络和 Memory Network,但是理论上,很多类似聚类的方法应该都是有效的,所以完全可以在这块替换成你自己的能产生聚类效果的方法来做。
这方面典型的算法有:Multi-Interest Network with Dynamic Routing for Recommendation at Tmall等。
知识图谱
知识图谱有一个独有的优势和价值,那就是对于推荐结果的可解释性;比如推荐给用户某个物品,可以在知识图谱里通过物品的关键关联路径给出合理解释,这对于推荐结果的解释性来说是很好的,因为知识图谱说到底是人编码出来让自己容易理解的一套知识体系,所以人非常容易理解其间的关系。知识图谱的可解释性往往是和图路径方法关联在一起的,而 Path 类方法,很多实验证明了,在排序角度来看,是效果最差的一类方法,但是它在可解释性方面有很好的效果,所以往往可以利用知识图谱构建一条可解释性的召回通路。
这方面的算法有:KGAT、RippleNet等。
图模型
推荐系统中User和Item相关的行为、需求、属性和社交信息具有天然的图结构,可以使用一张复杂的异构图来表示整个推荐系统。图神经网络模型推荐就是基于这个想法,把异构网络中包含的结构和语义信息编码到结点Embedding表示中,并使用得到向量进行个性化推荐。知识图谱其实是图神经网络的一个比较特殊的具体实例,但是,知识图谱因为编码的是静态知识,而不是用户比较直接的行为数据,和具体应用距离比较远,这可能是导致两者在推荐领域表现有差异的主要原因。
这方面典型的算法有:GraphSAGE、PinSage等。
精排
排序模型是推荐系统中涵盖的研究方向最多,有非常多的子领域值得研究探索,这也是推荐系统中技术含量最高的部分,毕竟它是直接面对用户,产生的结果对用户影响最大的一层。目前精排层深度学习已经一统天下了,这是王喆老师《深度学习推荐算法》书中的精排层模型演化线路。具体来看分为DNN、Wide&Deep两大块,实际深入还有序列建模,以及没有提到的多任务建模都是工业界非常常用的,所以我们接下来具体谈论其中每一块的技术栈。

特征交叉模型
在深度学习推荐算法发展早期,很多论文聚焦于如何提升模型的特征组合和交叉的能力,这其中既包含隐式特征交叉Deep Crossing也有采用显式特征交叉的探究。本质上是希望模型能够摆脱人工先验的特征工程,实现端到端的一套模型。
在早期的推荐系统中,基本是由人工进行特征交叉的,往往凭借对业务的理解和经验,但是费时费力。于是有了很多的这方面的研究,从FM到GBDT+LR都是引入模型进行自动化的特征交叉。再往后就是深度模型,深度模型虽然有万能近似定理,但是真正想要发挥模型的潜力,显式的特征交叉还是必不可少的。
这方面的经典研究工作有:DCN、DeepFM、xDeepFM等;
序列模型
在推荐系统中,历史行为序列是非常重要的特征。在序列建模中,主要任务目标是得到用户此刻的兴趣向量(user interest vector)。如何刻画用户兴趣的广泛性,是推荐系统比较大的一个难点,用户历史行为序列建模的研究经历了从Pooling、RNN到attention、capsule再到transformer的顺序。在序列模型中,又有很多细分的方向,比如根据用户行为长度有研究用户终身行为序列的,也有聚焦当下兴趣的,还有研究如何抽取序列特征的抽取器,比如研究attention还是胶囊网络。
这方面典型的研究工作有:DIN、DSIN、DIEN、SIM等;
多模态信息融合
在上文我们提到算法团队往往会利用内容画像信息,既有基于CV也有基于NLP抽取出来的信息。这是非常合理的,我们在逛抖音、淘宝的时候关注的不仅仅item的价格、品牌,同样会关注封面小姐姐好不好看、标题够不够震惊等信息。除此之外,在冷启动场景下,我们能够利用等信息不够多,如果能够使用多模态信息,能很大程度上解决数据稀疏的问题。
传统做法在多模态信息融合就是希望把不同模态信息利用起来,通过Embedding技术融合进模型。在推荐领域,主流的做法还是一套非端到端的体系,由其他模型抽取出多模态信息,推荐只需要融合入这些信息就好了。同时也有其他工作是利用注意力机制等方法来学习不同模态之间的关联,来增强多模态的表示。
比较典型的工作有:Image Matters: Visually modeling user behaviors using Advanced Model Server、UMPR等。
多任务学习
很多场景下我们模型优化的目标都是CTR,有一些场景只考虑CTR是不够的,点击率模型、时长模型和完播率模型是大部分信息流产品推荐算法团队都会尝试去做的模型。单独优化点击率模型容易推出来标题党,单独优化时长模型可能推出来的都是长视频或长文章,单独优化完播率模型可能短视频短图文就容易被推出来,所以多目标就应运而生。信息流推荐中,我们不仅希望用户点进我们的item,还希望能有一个不错的完播率,即希望用户能看完我们推荐的商品。或者电商场景希望用户不仅点进来,还希望他买下或者加入购物车了。这些概率实际上就是模型要学习的目标,多种目标综合起来,包括阅读、点赞、收藏、分享等等一系列的行为,归纳到一个模型里面进行学习,这就是推荐系统的多目标学习。
这方面比较典型的算法有:ESSM、MMoE、DUPN等。
强化学习
强化学习与一般有监督的深度学习相比有一些很显著的优势,首先强化学习能够比较灵活的定义优化的业务目标,考虑推荐系统长短期的收益,比如用户留存,在深度模型下,我们很难设计这个指标的优化函数,而强化学习是可以对长期收益下来建模。第二是能够体现用户兴趣的动态变化,比如在新闻推荐下,用户兴趣变化很快,强化学习更容易通过用户行为动态产生推荐结果。最后是EE也就是利用探索机制,这种一种当前和长期收益的权衡,强化学习能够更好的调节这里的回报。
这方面比较典型的算法有:DQN、Reinforcement Learning for Slate-based Recommender Systems: A Tractable Decomposition and Practical Methodology;
跨域推荐
一般一家公司业务线都是非常多的,比如腾讯既有腾讯视频,也有微信看一看、视频号,还有腾讯音乐,如果能够结合这几个场景的数据,同时进行推荐,一方面对于冷启动是非常有利的,另一方面也能补充更多数据,更好的进行精确推荐。
跨域推荐系统相比一般的推荐系统要更加复杂。在传统推荐系统中,我们只需要考虑建立当前领域内的一个推荐模型进行分析;而在跨域推荐中,我们更要关心在不同领域间要选择何种信息进行迁移,以及如何迁移这些信息,这是跨域推荐系统中非常关键的问题。
这方面典型的模型有:DTCDR、MV-DNN、EMCDR等;
重排序
我们知道常见的有三种优化目标:Point Wise、Pair Wise 和 List Wise。重排序阶段对精排生成的Top-N个物品的序列进行重新排序,生成一个Top-K个物品的序列,作为排序系统最后的结果,直接展现给用户。重排序的原因是因为多个物品之间往往是相互影响的,而精排序是根据PointWise得分,容易造成推荐结果同质化严重,有很多冗余信息。而重排序面对的挑战就是海量状态空间如何求解的问题,一般在精排层我们使用AUC作为指标,但是在重排序更多关注NDCG等指标。
重排序在业务中,还会根据一些策略、运营规则参与排序,比如强制去重、间隔排序、流量扶持等,但是总计趋势上看还是算法排序越来越占据主流趋势。重排序更多的是List Wise作为优化目标的,它关注的是列表中商品顺序的问题来优化模型,但是一般List Wise因为状态空间大,存在训练速度慢的问题。这方面典型的做法,基于RNN、Transformer、强化学习的都有,这方面因为不是推荐的一个核心,所以没有展开来讲,而且这一块比较依赖实际的业务场景。
这里的经典算法有:MRR、DPP、RNN等;
工程
推荐系统的实现需要依托工程,很多研究界Paper的idea满天飞,却忽视了工业界能否落地,进入工业界我们很难或者很少有组是做纯research的,所以我们同样有很多工程技术需要掌握。下面列举了在推荐中主要用到的工程技术:
- 编程语言:Python、Java(scala)、C++、sql、shell;
- 机器学习:Tensorflow/Pytorch、GraphLab/GraphCHI、LGB/Xgboost、SKLearn;
- 数据分析:Pandas、Numpy、Seaborn、Spark;
- 数据存储:mysql、redis、mangodb、hive、kafka、es、hbase;
- 相似计算:annoy、faiss、kgraph
- 流计算:Spark Streaming、Flink
- 分布式:Hadoop、Spark
上面那么多技术,我内容最重要的就是加粗的三部分,第一是语言:必须掌握的是Python,C++和JAVA中根据不同的组使用的是不同的语言,这个如果没有时间可以等进组后慢慢学习。然后是机器学习框架:Tensorflow和Pytorch至少要掌握一个吧,前期不用纠结学哪个,这个迁移成本很低,基本能够达到触类旁通,而且面试官不会为难你只会这个不会那个。最后是数据分析工具:Pandas是我们处理单机规模数据的利器,但是进入工业界,Hadoop和Spark是需要会用的,不过不用学太深,会用即可。
总结
本文从算法和工程两个角度分析了推荐系统的一个技术栈,但是还有很多方向遗漏,也有很多方向受限于现在的技术水平深度不够和有错误的情况,后续会不断补充和更正。
所以技术栈我列出的是一个非常广度的技术,实际上每一个技术钻研下去都需要非常多时间,而且不一定是你实际工作中会遇到的,所以不要被那么多技术吓到,也要避免陷入技术细节的海洋中。
我和非常多的大厂面试官讨论过技术深度和广度的问题,得出来的结论是对于入门的推荐算法工程师而言,实际上深度和广度的要求取决于你要去的组,有些组有很深的推荐技术沉淀,有很强的工程师团队,这样的组就会希望候选者能够在某个方面有比较深入的研究,这个方面既包含工程方面也包含研究方面。但是如果是比较新的组、或者技术沉淀不深、推荐不是主要任务的组,对深度要求就不会很高。总而言之,我认为对于应届生/实习生来说,在推荐最重要的工程技术/研究方向,至少在召回和排序模块,需要选一个作为方向,是需要较深钻研。对于其他技术/研究方向需要有一定了解,比如可以没用过强化学习,但是要知道强化学习能够在推荐中解决什么问题,剩下的可以等到真实遇到需要后再去学习。
参考资料
- 万字入门推荐系统
- 张俊林:技术演进趋势:召回->排序->重排
- 微信”看一看”多模型内容策略与召回
- 多目标学习在推荐系统中的应用
- 强化学习在美团“猜你喜欢”的实践
- 推荐系统技术演进趋势:重排篇
- 阿里强化学习重排实践
第二章 推荐系统算法基础
2.1经典召回模型
2.1.1基于协同过滤的召回
核心思想
协同过滤(Collaborative Filtering)推荐算法是最经典、最常用的推荐算法。基本思想是:
- 根据用户之前的喜好以及其他兴趣相近的用户的选择来给用户推荐物品。
- 基于对用户历史行为数据的挖掘发现用户的喜好偏向, 并预测用户可能喜好的产品进行推荐。
- 一般是仅仅基于用户的行为数据(评价、购买、下载等), 而不依赖于项的任何附加信息(物品自身特征)或者用户的任何附加信息(年龄, 性别等)。
- 目前应用比较广泛的协同过滤算法是基于邻域的方法,主要有:
- 基于用户的协同过滤算法(UserCF):给用户推荐和他兴趣相似的其他用户喜欢的产品。
- 基于物品的协同过滤算法(ItemCF):给用户推荐和他之前喜欢的物品相似的物品。
不管是 UserCF 还是 ItemCF 算法, 重点是计算用户之间(或物品之间)的相似度。
相似度度量方法
杰卡德(Jaccard)相似系数
Jaccard系数是衡量两个集合的相似度一种指标,计算公式如下:
- 其中 $N(u)$,$N(v)$分别表示用户$u$和用户交$v$互物品的集合。
对于用户$u$ 和$v$,该公式反映了两个交互物品交集的数量占这两个用户交互物品并集的数量的比例。
由于杰卡德相似系数一般无法反映具体用户的评分喜好信息,所以常用来评估用户是否会对某物品进行打分, 而不是预估用户会对某物品打多少分。
余弦相似度 余弦相似度衡量了两个向量的夹角,夹角越小越相似。余弦相似度的计算如下,其与杰卡德(Jaccard)相似系数只是在分母上存在差异:
从向量的角度进行描述,令矩阵$A$ 为用户-物品交互矩阵,矩阵的行表示用户,列表示物品。
设用户和物品数量分别为$m$, $n$,交互矩阵$A$就是一个$m$行$n$列的矩阵。
矩阵中的元素均为$ 0/1$。若用户$i$对物品$j$存在交互,那么 $A_{ij} =1$,否则为 $0$。
那么,用户之间的相似度可以表示为:
向量 $u$,$v$在形式都是 one-hot 类型,$u \cdot v$表示向量点积。
上述用户-物品交互矩阵在现实中是十分稀疏的,为了节省内存,交互矩阵会采用字典进行存储。在
sklearn中,余弦相似度的实现:1
2
3
4
5
6from sklearn.metrics.pairwise import cosine_similarity
i = [1, 0, 0, 0]
j = [1, 0, 1, 0]
cosine_similarity([i, j])
皮尔逊相关系数
在用户之间的余弦相似度计算时,将用户向量的内积展开为各元素乘积和:
- 其中,$r{ui},r{vi}$分别表示用户$u$和用户$v$对物品 $i$是否有交互(或具体评分值)。
皮尔逊相关系数与余弦相似度的计算公式非常的类似,如下:
- 其中,$r{ui}, r{vi}$分别表示用户$u$和用户$i$对物品$i$是否有交互(或具体评分值);
- $\bar r_u, \bar r_v$分别表示用户$u$和用户$v$交互的所有物品交互数量或者评分的平均值;
相较于余弦相似度,皮尔逊相关系数通过使用用户的平均分对各独立评分进行修正,减小了用户评分偏置的影响。在
scipy中,皮尔逊相关系数的实现:1
2
3
4
5
6from scipy.stats import pearsonr
i = [1, 0, 0, 0]
j = [1, 0.5, 0.5, 0]
pearsonr(i, j)
适用场景
- Jaccard 相似度表示两个集合的交集元素个数在并集中所占的比例 ,所以适用于隐式反馈数据(0-1)。
- 余弦相似度在度量文本相似度、用户相似度、物品相似度的时候都较为常用。
- 皮尔逊相关度,实际上也是一种余弦相似度。不过先对向量做了中心化,范围在 −1−1 到 11。
- 相关度量的是两个变量的变化趋势是否一致,两个随机变量是不是同增同减。
- 不适合用作计算布尔值向量(0-1)之间相关度。
基于用户的协同过滤
核心思想
基于用户的协同过滤(UserCF):
例如,我们要对用户$A$进行物品推荐,可以先找到和他有相似兴趣的其他用户。
然后,将共同兴趣用户喜欢的,但用户$A$未交互过的物品推荐给 $A$。

计算过程
以下图为例,给用户推荐物品的过程可以形象化为:预测用户对物品进行打分的任务,表格里面是5个用户对于5件物品的一个打分情况,就可以理解为用户对物品的喜欢程度。

UserCF算法的两个步骤:
- 首先,根据前面的这些打分情况(或者说已有的用户向量)计算一下 Alice 和用户1, 2, 3, 4的相似程度, 找出与 Alice 最相似的 n 个用户。
- 根据这 n 个用户对物品 5 的评分情况和与 Alice 的相似程度会猜测出 Alice 对物品5的评分。如果评分比较高的话, 就把物品5推荐给用户 Alice, 否则不推荐。
具体过程
计算用户之间的相似度
- 根据 1.2 节的几种方法, 我们可以计算出各用户之间的相似程度。对于用户 Alice,选取出与其最相近的$N$个用户。
计算用户对新物品的评分预测
- 常用的方式之一:利用目标用户与相似用户之间的相似度以及相似用户对物品的评分,来预测目标用户对候选物品的评分估计:
其中,权重 $w{u, s}$是用户$u$和用户$s$ 的相似度, $R{s, p}$是用户$s$对物品$p$的评分。
另一种方式:考虑到用户评分的偏置,即有的用户喜欢打高分, 有的用户喜欢打低分的情况。公式如下:
- 其中,$\bar R_s$表示用户$s$对物品的历史平均评分。
对用户进行物品推荐
- 在获得用户$u$ 对不同物品的评价预测后, 最终的推荐列表根据预测评分进行排序得到。
手动计算
根据上面的问题, 下面手动计算 Alice 对物品 5 的得分:
计算 Alice 与其他用户的相似度(基于皮尔逊相关系数)
- 手动计算 Alice 与用户 1 之间的相似度:
用户向量$Alice:(5,3,4,4),user1:(3,1,2,3),user2:(4,3,4,3),user3:(3,3,1,5),user4:(1,5,5,2)Alice:(5,3,4,4),user1:(3,1,2,3),user2:(4,3,4,3),user3:(3,3,1,5),user4:(1,5,5,2)$
计算Alice与user1的余弦相似性:
- 计算Alice与user1皮尔逊相关系数:
- 计算均值:$Alice{ave}=4, user{1ave}=2.25$
- 各向量减去均值:$Alice:(1,-1,0,0), user_1:(0.75,-1.25,-0.25,0.75)$
- 最后计算两个新向量的余弦相似度,与上面的计算过程一致,结果是0.852
基于 sklearn 计算所有用户之间的皮尔逊相关系数。可以看出,与 Alice 相似度最高的用户为用户1和用户2。

根据相似度用户计算 Alice对物品5的最终得分 用户1对物品5的评分是3, 用户2对物品5的打分是5, 那么根据上面的计算公式, 可以计算出 Alice 对物品5的最终得分是
根据用户评分对用户进行推荐
- 根据 Alice 的打分对物品排个序从大到小:
- 如果要向 Alice 推荐2款产品的话, 我们就可以推荐物品 1 和物品 5 给 Alice。
UserCF编程实现
建立实验使用的数据表:
1
2
3
4
5
6
7
8
9
10
11
12
13import numpy as np
import pandas as pd
def loadData():
users = {'Alice': {'A': 5, 'B': 3, 'C': 4, 'D': 4},
'user1': {'A': 3, 'B': 1, 'C': 2, 'D': 3, 'E': 3},
'user2': {'A': 4, 'B': 3, 'C': 4, 'D': 3, 'E': 5},
'user3': {'A': 3, 'B': 3, 'C': 1, 'D': 5, 'E': 4},
'user4': {'A': 1, 'B': 5, 'C': 5, 'D': 2, 'E': 1}
}
return users- 这里使用字典来建立用户-物品的交互表。
- 字典
users的键表示不同用户的名字,值为一个评分字典,评分字典的键值对表示某物品被当前用户的评分。 - 由于现实场景中,用户对物品的评分比较稀疏。如果直接使用矩阵进行存储,会存在大量空缺值,故此处使用了字典。
- 字典
- 这里使用字典来建立用户-物品的交互表。
计算用户相似性矩阵
- 由于训练数据中共包含 5 个用户,所以这里的用户相似度矩阵的维度也为$5 * 5$。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23user_data = loadData()
similarity_matrix = pd.DataFrame(
np.identity(len(user_data)), #创建对角矩阵,对角线为1,其余为0
index=user_data.keys(),
columns=user_data.keys(),
)
# 遍历每条用户-物品评分数据
for u1, item1 in user_data.items():
for u2, item2 in user_data.items():
if u1 == u2:
continue
vec1, vec2 = [], []
for item, rating1 in item1.items():
rating2 = item2.get(item, -1)
if rating2 == -1:
continue
vec1.append(rating1)
vec2.append(rating2)
# 计算不同用户之间的皮尔逊相关系数
similarity_matrix[u1][u2] = np.corrcoef(vec1, vec2)[0][1]
print(similarity_matrix)
1 | 1 2 3 4 5 |
计算与 Alice 最相似的
num个用户1
2
3
4
5target_user = 'Alice'
num = 2
# 由于最相似的用户为自己,忽略本身
sim_users = similarity_matrix[target_user].sort_values(ascend=False)
print(f'与用户{target_user}最相似的{num}个用户为:{sim_users}')1
与用户 Alice 最相似的2个用户为:['user1', 'user2']
预测用户 Alice 对物品
E的评分1
2
3
4
5
6
7
8
9
10
11
12
13
14
15weighted_scores = 0.
cor_values_sum = 0.
target_item = 'E'
# 基于皮尔逊相关稀疏预测用户评分
for user in sim_users:
corr_value = similaity_matrix[target_user][user]
user_mean_rating = np.mean(list(user_data[user].values()))
weighted_scores += corr_value * (user_data[user][target_item] - user_mean_rating)
corr_values_sum += corr_value
target_user_mean_rating = np.mean(list(user_data[target_user].values()))
target_item_pred = target_user_mean_rating + weighted_scores / corr_values_sum
print(f'用户{target_user}对物品{target_item}的预测评分为:{target_item_pred}')1
用户 Alice 对物品E的预测评分为:4.871979899370592
UserCF优缺点
User-based算法存在两个重大问题:
- 数据稀疏性
- 一个大型的电子商务推荐系统一般有非常多的物品,用户可能买的其中不到1%的物品,不同用户之间买的物品重叠性较低,导致算法无法找到一个用户的邻居,即偏好相似的用户。
- 这导致UserCF不适用于那些正反馈获取较困难的应用场景(如酒店预订, 大件物品购买等低频应用)。
- 算法扩展性
- 基于用户的协同过滤需要维护用户相似度矩阵以便快速的找出$TopN$相似用户, 该矩阵的存储开销非常大,存储空间随着用户数量的增加而增加。
- 故不适合用户数据量大的情况使用。
由于UserCF技术上的两点缺陷, 导致很多电商平台并没有采用这种算法, 而是采用了ItemCF算法实现最初的推荐系统。
算法评估
由于UserCF和ItemCF结果评估部分是共性知识点, 所以在这里统一标识。
召回率
对用户$u$推荐 $N$个物品记为,$R(u)$ 令用户在$u$测试集上喜欢的物品集合为$T(u)$, 那么召回率定义为:
- 含义:在模型召回预测的物品中,预测准确的物品占用户实际喜欢的物品的比例。
精确率
精确率定义为:
- 含义:推荐的物品中,对用户准确推荐的物品占总物品的比例。
- 如要确保召回率高,一般是推荐更多的物品,期望推荐的物品中会涵盖用户喜爱的物品。而实际中,推荐的物品中用户实际喜爱的物品占少数,推荐的精确率就会很低。故同时要确保高召回率和精确率往往是矛盾的,所以实际中需要在二者之间进行权衡。
覆盖率
覆盖率反映了推荐算法发掘长尾的能力, 覆盖率越高, 说明推荐算法越能将长尾中的物品推荐给用户。
- 含义:推荐系统能够推荐出来的物品占总物品集合的比例。
- 其中$I$表示所有物品的个数;
- 系统的用户集合为$U$;
- 推荐系统给每个用户推荐一个长度为$N$的物品列表$R(u)$.
- 覆盖率表示最终的推荐列表中包含多大比例的物品。如果所有物品都被给推荐给至少一个用户, 那么覆盖率是100%。
新颖度
用推荐列表中物品的平均流行度度量推荐结果的新颖度。 如果推荐出的物品都很热门, 说明推荐的新颖度较低。 由于物品的流行度分布呈长尾分布, 所以为了流行度的平均值更加稳定, 在计算平均流行度时对每个物品的流行度取对数。
- O’scar Celma 在博士论文 “Music Recommendation and Discovery in the Long Tail “ 中研究了新颖度的评测。
基于物品的协同过滤
基本思想
基于物品的协同过滤(ItemCF):
- 预先根据所有用户的历史行为数据,计算物品之间的相似性。
- 然后,把与用户喜欢的物品相类似的物品推荐给用户。
举例来说,如果用户 1 喜欢物品 A ,而物品 A 和 C 非常相似,则可以将物品 C 推荐给用户1。ItemCF算法并不利用物品的内容属性计算物品之间的相似度, 主要通过分析用户的行为记录计算物品之间的相似度, 该算法认为, 物品 A 和物品 C 具有很大的相似度是因为喜欢物品 A 的用户极可能喜欢物品 C。

计算过程
基于物品的协同过滤算法和基于用户的协同过滤算法很像, 所以我们这里直接还是拿上面 Alice 的那个例子来看。

如果想知道 Alice 对物品5打多少分, 基于物品的协同过滤算法会这么做:
- 首先计算一下物品5和物品1, 2, 3, 4之间的相似性。
- 在Alice找出与物品 5 最相近的 n 个物品。
- 根据 Alice 对最相近的 n 个物品的打分去计算对物品 5 的打分情况。
手动计算:
- 手动计算物品之间的相似度
物品向量:
物品1(3,4,3,1),物品2(1,3,3,5),物品3(2,4,1,5),物品4(3,3,5,2),物品5(3,5,4,1)
- 下面计算物品 5 和物品 1 之间的余弦相似性:
- 皮尔逊相关系数类似。
- 基于
sklearn计算物品之间的皮尔逊相关系数:

- 根据皮尔逊相关系数, 可以找到与物品5最相似的2个物品是 item1 和 item4, 下面基于上面的公式计算最终得分:
ItemCF编程实现
构建物品-用户的评分矩阵
1
2
3
4
5
6
7
8
9
10
11
12
13import numpy as np
import pandas as pd
def loadData():
items = {
'A': {'Alice': 5.0, 'user1': 3.0, 'user2': 4.0, 'user3': 3.0, 'user4': 1.0},
'B': {'Alice': 3.0, 'user1': 1.0, 'user2': 3.0, 'user3': 3.0, 'user4': 5.0},
'C': {'Alice': 4.0, 'user1': 2.0, 'user2': 4.0, 'user3': 1.0, 'user4': 5.0},
'D': {'Alice': 4.0, 'user1': 3.0, 'user2': 3.0, 'user3': 5.0, 'user4': 2.0},
'E': {'user1': 3.0, 'user2': 5.0, 'user3': 4.0, 'user4': 1.0}
}
return items- 计算物品间的相似度矩阵
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23item_data = loadData()
simiality_matrix = pd.DataFrame(
np.identity(len(item_data)),
index=item_data.keys(),
columns=item_data.keys(),
)
#遍历每条物品-用户评分数据
for i1, user1 in item_data.items():
for i2, user2 in item_data.items():
if i1 == i2:
continue
vec1, vec2 = [], []
for user, rating1 in unser1.items();
rating2 = user2.get(user, -1)
if rating2 == -1:
continue
vec1.append(rating1)
vec2.append(rating2)
similarity_matrix[i1][i2] = np.corrcoef(vec1, vec2)[0][1]
print(similarity_matrix)1
2
3
4
5
6
7A B C D E
A 1.000000 -0.476731 -0.123091 0.532181 0.969458
B -0.476731 1.000000 0.645497 -0.310087 -0.478091
C -0.123091 0.645497 1.000000 -0.720577 -0.427618
D 0.532181 -0.310087 -0.720577 1.000000 0.581675
E 0.969458 -0.478091 -0.427618 0.581675 1.000000- 从 Alice 购买过的物品中,选出与物品
E最相似的num件物品。
1
2
3
4
5
6
7
8
9
10
11
12
13
14target_user = 'Alice'
target_item = 'E'
num = 2
sim_items = []
sim_items_list = similarity_matrix[target_item].sort_values(ascending=False).index.tolist()
for item in sim_items_list:
# 如果target_user对物品item评分过
if target_user in item_data[item]:
sim_items.append(item)
if len(sim_items) == num:
break
print(f'与物品{target_item}最相似的{num}个物品为:{sim_items}')1
与物品E最相似的2个物品为:['A', 'D']
- 预测用户 Alice 对物品
E的评分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15target_user_mean_rating = np.mean(list(item_data[target_item].values()))
weighted_scores = 0.
corr_values_sum = 0.
target_item = 'E'
for item in sim_items:
corr_value = similarity_matrix[target_item][item]
user_mean_rating = np.mean(list(item_data[item].values()))
weighted_scores += corr_value * (item_data[item][target_user] - user_mean_rating)
corr_values_sum += corr_value
target_item_pred = target_user_mean_rating + weighted_scores / corr_values_sum
print(f'用户{target_user}对物品{target_item}的预测评分为:{target_item_pred}')1
用户 Alice 对物品E的预测评分为:4.6
协同过滤算法的权重改进
base 公式
- 该公式表示同时喜好物品$i$和物品$j$的用户数,占喜爱物品$i$的比例。
- 缺点:若物品$j$为热门物品,那么它与任何物品的相似度都很高。
对热门物品进行惩罚
- 根据 base 公式在的问题,对物品$j$进行打压。打压的出发点很简单,就是在分母再除以一个物品$j$被购买的数量。
- 此时,若物品品$j$为热门物品,那么对应的$N(j)$也会很大,受到的惩罚更多
控制对热门物品的惩罚力度
- 除了第二点提到的办法,在计算物品之间相似度时可以对热门物品进行惩罚外。
- 可以在此基础上,进一步引入参数a ,这样可以通过控制参数a来决定对热门物品的惩罚力度。
对活跃用户的惩罚
在计算物品之间的相似度时,可以进一步将用户的活跃度考虑进来。
对于异常活跃的用户,在计算物品之间的相似度时,他的贡献应该小于非活跃用户。
协同过滤算法的问题分析
协同过滤算法存在的问题之一就是泛化能力弱:
- 即协同过滤无法将两个物品相似的信息推广到其他物品的相似性上。
- 导致的问题是热门物品具有很强的头部效应, 容易跟大量物品产生相似, 而尾部物品由于特征向量稀疏, 导致很少被推荐。
比如下面这个例子:

- 左边矩阵中,𝐴,𝐵,𝐶,𝐷A,B,C,D 表示的是物品。
- 可以看出,D是一件热门物品,其与 A、B、C的相似度比较大。因此,推荐系统更可能将D推荐给用过 A、B、C的用户。
- 但是,推荐系统无法找出 A、B、C之间相似性的原因是交互数据太稀疏, 缺乏相似性计算的直接数据。
所以这就是协同过滤的天然缺陷:推荐系统头部效应明显, 处理稀疏向量的能力弱。
为了解决这个问题, 同时增加模型的泛化能力。2006年,矩阵分解技术(Matrix Factorization, MF)被提出:
- 该方法在协同过滤共现矩阵的基础上, 使用更稠密的隐向量表示用户和物品, 挖掘用户和物品的隐含兴趣和隐含特征。
- 在一定程度上弥补协同过滤模型处理稀疏矩阵能力不足的问题。
课后思考
- 什么时候使用UserCF,什么时候使用ItemCF?为什么?
(1)UserCF
- 由于是基于用户相似度进行推荐, 所以具备更强的社交特性, 这样的特点非常适于用户少, 物品多, 时效性较强的场合。
- 比如新闻推荐场景, 因为新闻本身兴趣点分散, 相比用户对不同新闻的兴趣偏好, 新闻的及时性,热点性往往更加重要, 所以正好适用于发现热点,跟踪热点的趋势。
- 另外还具有推荐新信息的能力, 更有可能发现惊喜, 因为看的是人与人的相似性, 推出来的结果可能更有惊喜,可以发现用户潜在但自己尚未察觉的兴趣爱好。
(2)itemCF
- 这个更适用于兴趣变化较为稳定的应用, 更接近于个性化的推荐, 适合物品少,用户多,用户兴趣固定持久, 物品更新速度不是太快的场合。
- 比如推荐艺术品, 音乐, 电影。
- 协同过滤在计算上有什么缺点?有什么比较好的思路可以解决(缓解)?
该问题答案参考上一小节的协同过滤算法的问题分析。
- 上面介绍的相似度计算方法有什么优劣之处?
cosine相似度计算简单方便,一般较为常用。但是,当用户的评分数据存在 bias 时,效果往往不那么好。
- 简而言之,就是不同用户评分的偏向不同。部分用户可能乐于给予好评,而部分用户习惯给予差评或者乱评分。
- 这个时候,根据cosine 相似度计算出来的推荐结果效果会打折扣。
举例来说明,如下图(
X,Y,Z表示物品,d,e,f表示用户):
- 如果使用余弦相似度进行计算,用户 d 和 e 之间较为相似。但是实际上,用户 d 和 f 之间应该更加相似。只不过由于 d 倾向于打高分,e 倾向于打低分导致二者之间的余弦相似度更高。
- 这种情况下,可以考虑使用皮尔逊相关系数计算用户之间的相似性关系。
- 协同过滤还存在其他什么缺陷?有什么比较好的思路可以解决(缓解)?
- 协同过滤的优点就是没有使用更多的用户或者物品属性信息,仅利用用户和物品之间的交互信息就能完成推荐,该算法简单高效。
- 但这也是协同过滤算法的一个弊端。由于未使用更丰富的用户和物品特征信息,这也导致协同过滤算法的模型表达能力有限。
- 对于该问题,逻辑回归模型(LR)可以更好地在推荐模型中引入更多特征信息,提高模型的表达能力。
Swing(Graph-based)
动机
大规模推荐系统需要实时对用户行为做出海量预测,为了保证这种实时性,大规模的推荐系统通常严重依赖于预先计算好的产品索引。产品索引的功能为:给定种子产品返回排序后的候选相关产品列表。

相关性产品索引主要包含两部分:替代性产品和互补性产品。例如图中的不同种类的衬衫构成了替代关系,而衬衫和风衣裤子等构成了互补关系。用户通常希望在完成购买行为之前尽可能看更多的衬衫,而用户购买过衬衫之后更希望看到与之搭配的单品而不是其他衬衫了。
之前方法的局限性
- 基于 Cosine, Jaccard, 皮尔逊相关性等相似度计算的协同过滤算法,在计算邻居关联强度的时候只关注于 Item-based (常用,因为item相比于用户变化的慢,且新Item特征比较容易获得),Item-based CF 只关注于 Item-User-Item 的路径,把所有的User-Item交互都平等得看待,从而忽视了 User-Item 交互中的大量噪声,推荐精度存在局限性。
- 对互补性产品的建模不足,可能会导致用户购买过手机之后还继续推荐手机,但用户短时间内不会再继续购买手机,因此产生无效曝光。
贡献
提出了高效建立产品索引图的技术。 主要包括:
- Swing 算法利用 user-item 二部图的子结构捕获产品间的替代关系。
- Surprise 算法利用商品分类信息和用户共同购买图上的聚类技术来建模产品之间的组合关系。
Swing算法
Swing 通过利用 User-Item-User 路径中所包含的信息,考虑 User-Item 二部图中的鲁棒内部子结构计算相似性。
什么是内部子结构? 以经典的啤酒尿布故事为例,张三同时购买了啤酒和尿布,这可能是一种巧合。但两个甚至多个顾客都同时购买了啤酒尿布,这就证明啤酒和尿布具有相关关系。这样共同购买啤酒和尿布的用户越多,啤酒和尿布的相关度就会越高。图中的红色四边形就是一种Swing子结构,这种子结构可以作为给王五推荐尿布的依据。

通俗解释:若用户$u$和用户$v$之间除了购买过$i$外,还购买过商品$j$ ,则认为两件商品是具有某种程度上的相似的。也就是说,商品与商品之间的相似关系,是通过用户关系来传递的。为了衡量物品$i$和$j$ 的相似性,比较同时购买了物品$i$和$j$ 的用户$u$和用户$v$, 如果这两个用户共同购买的物品越少,即这两个用户原始兴趣不相似,但仍同时购买了两个相同的物品$i$和$j$, 则物品$i$和$j$的相似性越高。
计算公式
其中$U_i$是点击过商品$i$的用户集合,$I_u$是用户$u$点击过的商品集合,$a$是平滑系数。
$w_u=\frac{1}{\sqrt{I_u}},w_v=\frac{1}{\sqrt{I_v}$是用户权重参数,来降低活跃用户的影响。
代码实现
Python (建议自行debug方便理解)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57from itertools import combinations
import pandas as pd
alpha = 0.5
top_k = 20
def load_data(train_path):
train_data = pd.read_csv(train_path, sep="\t", engine="python", name=["userid", "itemid", "rate"]) #提取用户交互记录数据
print(train_data.head(3))
return train_data
def get_uitems_iusers(train):
u_items = dict()
i_users = dict()
for index, row in train.iterrows():#处理用户交互记录
u_items.setdefault(row["userid"], set())
i_users.setdefault(row["itemid"], set())
u_items[row["userid"]].add(row["itemid"])#得到user交互过的所有item
i_users[row["itemid"]].add(row["userid"])#得到item交互过的所有user
print("使用的用户个数为:{}".format(len(u_items)))
print("使用的item个数为:{}".format(len(i_users)))
return u_items, i_users
def swing_model(u_items, i_users):
# print([i for i in i_users.values()][:5])
# print([i for i in u_items.values()][:5])
item_pairs = list(combinations(i_users.keys(), 2)) #全排列组合对
print("item pairs length:{}".format(len(item_pairs)))
item_sim_dict = dict()
for (i, j) in item_pairs:
user_pairs = list(combinations(i_users[i] & i_users[j], 2)) #item_i和item_j对应的user取交集后全排列 得到user对
result = 0
for (u, v) in user_pairs:
result += 1 / (alpha + list(u_items[u] & u_items[v]).__len__()) #分数公式
if result != 0 :
item_sim_dict.setdefault(i, dict())
item_sim_dict[i][j] = format(result, '.6f')
return item_sim_dict
def save_item_sims(item_sim_dict, top_k, path):
new_item_sim_dict = dict()
try:
writer = open(path, 'w', encoding='utf-8')
for item, sim_items in item_sim_dict.items():
new_item_sim_dict.setdefault(item, dict())
new_item_sim_dict[item] = dict(sorted(sim_items.items(), key = lambda k:k[1], reverse=True)[:top_k])#排序取出 top_k个相似的item
writer.write('item_id:%d\t%s\n' % (item, new_item_sim_dict[item]))
print("SUCCESS: top_{} item saved".format(top_k))
except Exception as e:
print(e.args)
if __name__ == "__main__":
train_data_path = "./ratings_final.txt"
item_sim_save_path = "./item_sim_dict.txt"
top_k = 10 #与item相似的前 k 个item
train = load_data(train_data_path)
u_items, i_users = get_uitems_iusers(train)
item_sim_dict = swing_model(u_items, i_users)
save_item_sims(item_sim_dict, top_k, item_sim_save_path)Spark(仅为核心代码需要补全配置才能跑通)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23object Swing {
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder()
.appName("test")
.master("local[2]")
.getOrCreate()
val alpha = 1 //分数计算参数
val filter_n_items = 10000 //想要计算的item数量 测试的时候取少点
val top_n_items = 500 //保存item的score排序前500个相似的item
val model = new SwingModel(spark)
.setAlpha(alpha.toDouble)
.setFilter_N_Items(filter_n_items.toInt)
.setTop_N_Items(top_n_items.toInt)
val url = "file:///usr/local/var/scala/common/part-00022-e17c0014.snappy.parquet"
val ratings = DataLoader.getRatingLog(spark, url)
val df = model.fit(ratings).item2item()
df.show(3,false)
// df.write.mode("overwrite").parquet(dest_url)
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116/**
* swing
* @param ratings 打分dataset
* @return
*/
def fit(ratings: Dataset[Rating]): SwingModel = {
def interWithAlpha = udf(
(array_1: Seq[GenericRowWithSchema],
array_2: Seq[GenericRowWithSchema]) => {
var score = 0.0
val u_set_1 = array_1.toSet
val u_set_2 = array_2.toSet
val user_set = u_set_1.intersect(u_set_2).toArray //取交集得到两个item共同user
for (i <- user_set.indices; j <- i + 1 until user_set.length) {
val user_1 = user_set(i)
val user_2 = user_set(j)
val item_set_1 = user_1.getAs[Seq[String]]("_2").toSet
val item_set_2 = user_2.getAs[Seq[String]]("_2").toSet
score = score + 1 / (item_set_1
.intersect(item_set_2)
.size
.toDouble + alpha.get)
}
score
}
)
val df = ratings.repartition(defaultParallelism).cache()
val groupUsers = df
.groupBy("user_id")
.agg(collect_set("item_id")) //聚合itme_id
.toDF("user_id", "item_set")
.repartition(defaultParallelism)
println("groupUsers")
groupUsers.show(3, false)//user_id|[item_id_set]: 422|[6117,611,6117]
val groupItems = df
.join(groupUsers, "user_id")
.rdd
.map { x =>
val item_id = x.getAs[String]("item_id")
val user_id = x.getAs[String]("user_id")
val item_set = x.getAs[Seq[String]]("item_set")
(item_id, (user_id, item_set))
}//i_[user(item_set)]
.toDF("item_id", "user")
.groupBy("item_id")
.agg(collect_set("user"), count("item_id"))
.toDF("item_id", "user_set", "count")
.filter("size(user_set) > 1")//过滤掉没有交互的
.sort($"count".desc) //根据count倒排item_id数量
.limit(filter_n_items.get)//item可能百万级别但后面召回的需求量小所以只取前n个item进行计算
.drop("count")
.repartition(defaultParallelism)
.cache()
println("groupItems") //得到与itme_id有交互的user_id
groupItems.show(3, false)//item_id|[[user_id,[item_set]],[user_id,[item_set]]]: 67|[[562,[66, 813, 61, 67]],[563,[67, 833, 62, 64]]]
val itemJoined = groupItems
.join(broadcast(groupItems))//内连接两个item列表
.toDF("item_id_1", "user_set_1", "item_id_2", "user_set_2")
.filter("item_id_1 > item_id_2")//内连接 item两两配对
.withColumn("score", interWithAlpha(col("user_set_1"), col("user_set_2")))//将上面得到的与item相关的user_set输入到函数interWithAlpha计算分数
.select("item_id_1", "item_id_2", "score")
.filter("score > 0")
.repartition(defaultParallelism)
.cache()
println("itemJoined")
itemJoined.show(5)//得到两两item之间的分数结果 item_id_1 item_id_2 score
similarities = Option(itemJoined)
this
}
/**
* 从fit结果,对item_id进行聚合并排序,每个item后截取n个item,并返回。
* @param num 取n个item
* @return
*/
def item2item(): DataFrame = {
case class itemWithScore(item_id: String, score: Double)
val sim = similarities.get.select("item_id_1", "item_id_2", "score")
val topN = sim
.map { x =>
val item_id_1 = x.getAs[String]("item_id_1")
val item_id_2 = x.getAs[String]("item_id_2")
val score = x.getAs[Double]("score")
(item_id_1, (item_id_2, score))
}
.toDF("item_id", "itemWithScore")
.groupBy("item_id")
.agg(collect_set("itemWithScore"))
.toDF("item_id", "item_set")//item_id |[[item_id1:score],[item_id2:score]]
.rdd
.map { x =>
val item_id_1 = x.getAs[String]("item_id")
val item_set = x //对itme_set中score进行排序操作
.getAs[Seq[GenericRowWithSchema]]("item_set")
.map { x =>
val item_id_2 = x.getAs[String]("_1")
val score = x.getAs[Double]("_2")
(item_id_2, score)
}
.sortBy(-_._2)//根据score进行排序
.take(top_n_items.get)//取top_n
.map(x => x._1 + ":" + x._2.toString)
(item_id_1, item_set)
}
.filter(_._2.nonEmpty)
.toDF("id", "sorted_items")
topN
}
}
Surprise算法
首先在行为相关性中引入连续时间衰减因子,然后引入基于交互数据的聚类方法解决数据稀疏的问题,旨在帮助用户找到互补商品。互补相关性主要从三个层面考虑,类别层面,商品层面和聚类层面。
类别层面 首先通过商品和类别的映射关系,我们可以得到 user-category 矩阵。随后使用简单的相关性度量可以计算出类别$i,j$的相关性。
即,$N(c_{i,j})$为在购买过$i$之后购买$j$类的数量,$c_j$为购买$j$类的数量。
由于类别直接的种类差异,每个类别的相关类数量存在差异,因此采用最大相对落点来作为划分阈值。

例如图(a)中T恤的相关类选择前八个,图(b)中手机的相关类选择前三个。
商品层面 商品层面的相关性挖掘主要有两个关键设计:
- 商品的购买顺序是需要被考虑的,例如在用户购买手机后推荐充电宝是合理的,但在用户购买充电宝后推荐手机是不合理的。
- 两个商品购买的时间间隔也是需要被考虑的,时间间隔越短越能证明两个商品的互补关系。
- 最终商品层面的互补相关性被定义为:$s1(i,j)=\frac{\sum{u \in Ui \cap U_j}\frac{1}{1+|t{ui}-t{uj}|}}}{||U_i \dot U_j||}$
聚类层面
- 如何聚类? 传统的聚类算法(基于密度和 k-means )在数十亿产品规模下的淘宝场景中不可行,所以作者采用了标签传播算法。
- 在哪里标签传播? Item-item 图,其中又 Swing 计算的排名靠前 item 为邻居,边的权重就是 Swing 分数。
- 表现如何? 快速而有效,15分钟即可对数十亿个项目进行聚类。 最终聚类层面的相关度计算同上面商品层面的计算公式
线性组合: $s(i,j)=ws_1(i,j)+(1-w)s_2(i,j)$,其中$w=0.8$是作者设置的权重超参数。 Surprise算法利用类别信息和标签传播技术解决了用户共同购买图上的稀疏性问题。
参考资料
矩阵分解
隐语义模型与矩阵分解
协同过滤算法的特点:
- 协同过滤算法的特点就是完全没有利用到物品本身或者是用户自身的属性, 仅仅利用了用户与物品的交互信息就可以实现推荐,是一个可解释性很强, 非常直观的模型。
- 但是也存在一些问题,处理稀疏矩阵的能力比较弱。
为了使得协同过滤更好处理稀疏矩阵问题, 增强泛化能力。从协同过滤中衍生出矩阵分解模型(Matrix Factorization, MF)或者叫隐语义模型:
- 在协同过滤共现矩阵的基础上, 使用更稠密的隐向量表示用户和物品。
- 通过挖掘用户和物品的隐含兴趣和隐含特征, 在一定程度上弥补协同过滤模型处理稀疏矩阵能力不足的问题。
隐语义模型
隐语义模型最早在文本领域被提出,用于找到文本的隐含语义。在2006年, 被用于推荐中, 它的核心思想是通过隐含特征(latent factor)联系用户兴趣和物品(item), 基于用户的行为找出潜在的主题和分类, 然后对物品进行自动聚类,划分到不同类别/主题(用户的兴趣)。
以项亮老师《推荐系统实践》书中的内容为例:
如果我们知道了用户A和用户B两个用户在豆瓣的读书列表, 从他们的阅读列表可以看出,用户A的兴趣涉及侦探小说、科普图书以及一些计算机技术书, 而用户B的兴趣比较集中在数学和机器学习方面。 那么如何给A和B推荐图书呢? 先说说协同过滤算法, 这样好对比不同:
- 对于UserCF,首先需要找到和他们看了同样书的其他用户(兴趣相似的用户),然后给他们推荐那些用户喜欢的其他书。
- 对于ItemCF,需要给他们推荐和他们已经看的书相似的书,比如作者B看了很多关于数据挖掘的书,可以给他推荐机器学习或者模式识别方面的书。
而如果是隐语义模型的话, 它会先通过一些角度把用户兴趣和这些书归一下类, 当来了用户之后, 首先得到他的兴趣分类, 然后从这个分类中挑选他可能喜欢的书籍。
隐语义模型和协同过滤的不同主要体现在隐含特征上, 比如书籍的话它的内容, 作者, 年份, 主题等都可以算隐含特征。
以王喆老师《深度学习推荐系统》中的一个原理图为例,看看是如何通过隐含特征来划分开用户兴趣和物品的。
音乐评分实例
假设每个用户都有自己的听歌偏好, 比如用户 A 喜欢带有小清新的, 吉他伴奏的, 王菲的歌曲,如果一首歌正好是王菲唱的, 并且是吉他伴奏的小清新, 那么就可以将这首歌推荐给这个用户。 也就是说是小清新, 吉他伴奏, 王菲这些元素连接起了用户和歌曲。
当然每个用户对不同的元素偏好不同, 每首歌包含的元素也不一样, 所以我们就希望找到下面的两个矩阵:
- 潜在因子—— 用户矩阵Q 这个矩阵表示不同用户对于不同元素的偏好程度, 1代表很喜欢, 0代表不喜欢, 比如下面这样:
向量的召回
FM模型结构
FM 模型用于排序时,模型的公式定义如下:
- 其中,$i$表示特征的序号,$n$表示特征的数量;$x_i \in R$表示第$i$个特征的值。
- $vi,v_j \in R^k$分别表示特征$x_i,x_j$对应的隐语义向量(Embedding向量),$\langle v_i,v_j \rangle := \sum{f=1}^k v{i,f} \cdot v{j,f}$。
- $w_0,w_i \in R$表示需要学习的参数。
FM的一阶交互特征
在 FM 的表达式中,前两项为特征的一阶交互项。将其拆分为用户特征和物品特征的一阶特征交互项,如下:
- 其中,$U$表示用户相关特征集合,$I$表示物品相关特征集合。
FM 的二阶特征交互

- 公式变换后,计算复杂度由$O(kn^2)$降到$O(kn)$。
由于本文章需要将 FM 模型用在召回,故将二阶特征交互项拆分为用户和物品项。有:

FM用于召回
2.2经典排序模型
2.2.3Wide&Deep系列
2.2.1GBDT+LR简介
前面介绍的协同过滤和矩阵分解存在的劣势就是仅利用了用户与物品相互行为信息进行推荐, 忽视了用户自身特征, 物品自身特征以及上下文信息等,导致生成的结果往往会比较片面。 而这次介绍的这个模型是2014年由Facebook提出的GBDT+LR模型, 该模型利用GBDT自动进行特征组合和选择, 进而生成新的离散特征向量, 再把该特征向量当做LR模型的输入, 来产生最后的预测结果, 该模型能够综合利用用户、物品和上下文等多种不同的特征, 生成较为全面的推荐结果, 在CTR点击率预估场景下使用较为广泛。
下面首先会介绍逻辑回归和GBDT模型各自的原理及优缺点, 然后介绍GBDT+LR模型的工作原理和细节。
逻辑回归模型
逻辑回归模型非常重要, 在推荐领域里面, 相比于传统的协同过滤, 逻辑回归模型能够综合利用用户、物品、上下文等多种不同的特征生成较为“全面”的推荐结果, 关于逻辑回归的更多细节, 可以参考下面给出的链接,这里只介绍比较重要的一些细节和在推荐中的应用。
逻辑回归是在线性回归的基础上加了一个 Sigmoid 函数(非线形)映射,使得逻辑回归成为了一个优秀的分类算法, 学习逻辑回归模型, 首先应该记住一句话:逻辑回归假设数据服从伯努利分布,通过极大似然的方法,运用梯度下降来求解参数,来达到将数据二分类的目的。
相比于协同过滤和矩阵分解利用用户的物品“相似度”进行推荐, 逻辑回归模型将问题看成了一个分类问题, 通过预测正样本的概率对物品进行排序。这里的正样本可以是用户“点击”了某个商品或者“观看”了某个视频, 均是推荐系统希望用户产生的“正反馈”行为, 因此逻辑回归模型将推荐问题转化成了一个点击率预估问题。而点击率预测就是一个典型的二分类, 正好适合逻辑回归进行处理, 那么逻辑回归是如何做推荐的呢? 过程如下:
- 将用户年龄、性别、物品属性、物品描述、当前时间、当前地点等特征转成数值型向量
- 确定逻辑回归的优化目标,比如把点击率预测转换成二分类问题, 这样就可以得到分类问题常用的损失作为目标, 训练模型
- 在预测的时候, 将特征向量输入模型产生预测, 得到用户“点击”物品的概率
- 利用点击概率对候选物品排序, 得到推荐列表
推断过程可以用下图来表示:
这里的关键就是每个特征的权重参数$w$, 我们一般是使用梯度下降的方式, 首先会先随机初始化参数$w$, 然后将特征向量(也就是我们上面数值化出来的特征)输入到模型, 就会通过计算得到模型的预测概率, 然后通过对目标函数求导得到每个$w$的梯度, 然后进行更新$w$
求导之后:
这样通过若干次迭代, 就可以得到最终的𝑤w了, 关于这些公式的推导,可以参考下面给出的文章链接, 下面我们分析一下逻辑回归模型的优缺点。
优点
- LR模型形式简单,可解释性好,从特征的权重可以看到不同的特征对最后结果的影响。
- 训练时便于并行化,在预测时只需要对特征进行线性加权,所以性能比较好,往往适合处理海量id类特征,用id类特征有一个很重要的好处,就是防止信息损失(相对于范化的 CTR 特征),对于头部资源会有更细致的描述
- 资源占用小,尤其是内存。在实际的工程应用中只需要存储权重比较大的特征及特征对应的权重。
- 方便输出结果调整。逻辑回归可以很方便的得到最后的分类结果,因为输出的是每个样本的概率分数,我们可以很容易的对这些概率分数进行cutoff,也就是划分阈值(大于某个阈值的是一类,小于某个阈值的是一类)
当然, 逻辑回归模型也有一定的局限性
- 表达能力不强, 无法进行特征交叉, 特征筛选等一系列“高级“操作(这些工作都得人工来干, 这样就需要一定的经验, 否则会走一些弯路), 因此可能造成信息的损失
- 准确率并不是很高。因为这毕竟是一个线性模型加了个sigmoid, 形式非常的简单(非常类似线性模型),很难去拟合数据的真实分布
- 处理非线性数据较麻烦。逻辑回归在不引入其他方法的情况下,只能处理线性可分的数据, 如果想处理非线性, 首先对连续特征的处理需要先进行离散化(离散化的目的是为了引入非线性),如上文所说,人工分桶的方式会引入多种问题。
- LR 需要进行人工特征组合,这就需要开发者有非常丰富的领域经验,才能不走弯路。这样的模型迁移起来比较困难,换一个领域又需要重新进行大量的特征工程。
所以如何自动发现有效的特征、特征组合,弥补人工经验不足,缩短LR特征实验周期,是亟需解决的问题, 而GBDT模型, 正好可以自动发现特征并进行有效组合
GBDT模型
GBDT全称梯度提升决策树,在传统机器学习算法里面是对真实分布拟合的最好的几种算法之一,在前几年深度学习还没有大行其道之前,gbdt在各种竞赛是大放异彩。原因大概有几个,一是效果确实挺不错。二是即可以用于分类也可以用于回归。三是可以筛选特征, 所以这个模型依然是一个非常重要的模型。
GBDT是通过采用加法模型(即基函数的线性组合),以及不断减小训练过程产生的误差来达到将数据分类或者回归的算法, 其训练过程如下:
gbdt通过多轮迭代, 每轮迭代会产生一个弱分类器, 每个分类器在上一轮分类器的残差基础上进行训练。 gbdt对弱分类器的要求一般是足够简单, 并且低方差高偏差。 因为训练的过程是通过降低偏差来不断提高最终分类器的精度。 由于上述高偏差和简单的要求,每个分类回归树的深度不会很深。最终的总分类器是将每轮训练得到的弱分类器加权求和得到的(也就是加法模型)。
关于GBDT的详细细节,依然是可以参考下面给出的链接。这里想分析一下GBDT如何来进行二分类的,因为我们要明确一点就是gbdt 每轮的训练是在上一轮的训练的残差基础之上进行训练的, 而这里的残差指的就是当前模型的负梯度值, 这个就要求每轮迭代的时候,弱分类器的输出的结果相减是有意义的, 而gbdt 无论用于分类还是回归一直都是使用的CART 回归树, 那么既然是回归树, 是如何进行二分类问题的呢?
GBDT 来解决二分类问题和解决回归问题的本质是一样的,都是通过不断构建决策树的方式,使预测结果一步步的接近目标值, 但是二分类问题和回归问题的损失函数是不同的, 关于GBDT在回归问题上的树的生成过程, 损失函数和迭代原理可以参考给出的链接, 回归问题中一般使用的是平方损失, 而二分类问题中, GBDT和逻辑回归一样, 使用的下面这个:
其中, $y_i$是第$i$个样本的观测值, 取值要么是0要么是1, 而是$p_i$第$i$个样本的预测值, 取值是0-1之间的概率,由于我们知道GBDT拟合的残差是当前模型的负梯度, 那么我们就需要求出这个模型的导数, 即$\frac{dL}{dp_i}$, 对于某个特定的样本, 求导的话就可以只考虑它本身, 去掉加和号, 那么就变成了$\frac{dl}{dp_i}$, 其中$l$如下:
如果对逻辑回归非常熟悉的话,$log(\frac{p_i}{1-p_i})$ 一定不会陌生吧, 这就是对几率比取了个对数, 并且在逻辑回归里面这个式子会等于$\theta X$,所以才推出了$p_i=\frac{1}{1+e^{- \theta X}}$的那个形式。 这里令$\eta_i=\frac{p_i}{1-p_i}$,即$p_i=\frac{\eta_i}{1+\eta_i}$,则上面这个式子变成了:
这时候,我们对$log(\eta_i)$求导, 得
这样, 我们就得到了某个训练样本在当前模型的梯度值了, 那么残差就是$y_i-p_i$。GBDT二分类的这个思想,其实和逻辑回归的思想一样,逻辑回归是用一个线性模型去拟合$P(y=1|x)$这个事件的对数几率$log\frac{p}{1-p}=\theta^Tx$, GBDT二分类也是如此, 用一系列的梯度提升树去拟合这个对数几率, 其分类模型可以表达为:
初始化GBDT 和回归问题一样, 分类 GBDT 的初始状态也只有一个叶子节点,该节点为所有样本的初始预测值,如下:
上式里面,$F$代表GBDT模型, $F_0$是模型的初始状态, 该式子的意思是找到一个,$\gamma$使所有样本的 Loss 最小,在这里及下文中,$\gamma$都表示节点的输出,即叶子节点, 且它是一个$log(\eta_i)$形式的值(回归值),在初始状态,$\gamma=F_0$。
下面看例子(该例子来自下面的第二个链接), 假设我们有下面3条样本:

我们希望构建 GBDT 分类树,它能通过「喜欢爆米花」、「年龄」和「颜色偏好」这 3 个特征来预测某一个样本是否喜欢看电影。我们把数据代入上面的公式中求Loss:
为了令其最小, 我们求导, 且让导数为0, 则:
于是, 就得到了初始值$p=\frac{2}{3}, \gamma=log(\frac{p}{1-p})=0.69$,模型的初始状态$F_0(x) = 0.69$
说了一大堆,实际上你却可以很容易的算出该模型的初始值,它就是正样本数比上负样本数的 log 值,例子中,正样本数为 2 个,负样本为 1 个,那么:
循环生成决策树 这里回忆一下回归树的生成步骤, 其实有4小步, 第一就是计算负梯度值得到残差, 第二步是用回归树拟合残差, 第三步是计算叶子节点的输出值, 第四步是更新模型。 下面我们一一来看:
计算负梯度得到残差
此处使用$m-1$棵树的模型, 计算每个样本的残差,$r_{im}$ 就是上面的$y_i- p_i$, 于是例子中, 每个样本的残差:

使用回归树来拟合$\gamma_{jm}$, 这里的表$i$示样本哈,回归树的建立过程可以参考下面的链接文章,简单的说就是遍历每个特征, 每个特征下遍历每个取值, 计算分裂后两组数据的平方损失, 找到最小的那个划分节点。 假如我们产生的第2棵决策树如下:

对于每个叶子节点$j$, 计算最佳残差拟合值
意思是, 在刚构建的树$m$中, 找到每个节点$j$的输出$\gamma_{jm}$, 能使得该节点的loss最小。 那么我们看一下这个$\gamma$的求解方式, 这里非常的巧妙。 首先, 我们把损失函数写出来, 对于左边的第一个样本, 有
这个式子就是上面推导的𝑙l, 因为我们要用回归树做分类, 所以这里把分类的预测概率转换成了对数几率回归的形式, 即$log(\eta_i)$, 这个就是模型的回归输出值。而如果求这个损失的最小值, 我们要求导, 解出令损失最小的$\gamma$。 但是上面这个式子求导会很麻烦, 所以这里介绍了一个技巧就是使用二阶泰勒公式来近似表示该式, 再求导, 还记得伟大的泰勒吗?
这里就相当于把$L(y1,F{m-1}(x_1))$当作常量$f(x)$,$\gamma$ 作为变量$\Delta x$,将$f(x)$二阶展开:
这时候再求导就简单了
Loss最小的时候, 上面的式子等于0, 就可以得到$\gamma$:
因为分子就是残差(上述已经求到了), 分母可以通过对残差求导,得到原损失函数的二阶导:

这时候, 就可以算出该节点的输出
$$
\gamma_{11}=\frac{r_{11}}{p_{10}(1-p_{10})}=\frac{0.33}{0.67 * 0.33}=1.49
$$
这里的下面$\gamma_{jm}$表示第$m$棵树的第$j$个叶子节点。 接下来是右边节点的输出, 包含样本2和样本3, 同样使用二阶泰勒公式展开:

求导, 令其结果为0,就会得到, 第1棵树的第2个叶子节点的输出:

可以看出, 对于任意叶子节点, 我们可以直接计算其输出值:
- 更新模型$F_m(x)$
下面分析一下GBDT的优缺点:
我们可以把树的生成过程理解成自动进行多维度的特征组合的过程,从根结点到叶子节点上的整个路径(多个特征值判断),才能最终决定一棵树的预测值, 另外,对于连续型特征的处理,GBDT 可以拆分出一个临界阈值,比如大于 0.027 走左子树,小于等于 0.027(或者 default 值)走右子树,这样很好的规避了人工离散化的问题。这样就非常轻松的解决了逻辑回归那里自动发现特征并进行有效组合的问题, 这也是GBDT的优势所在。
但是GBDT也会有一些局限性, 对于海量的 id 类特征,GBDT 由于树的深度和棵树限制(防止过拟合),不能有效的存储;另外海量特征在也会存在性能瓶颈,当 GBDT 的 one hot 特征大于 10 万维时,就必须做分布式的训练才能保证不爆内存。所以 GBDT 通常配合少量的反馈 CTR 特征来表达,这样虽然具有一定的范化能力,但是同时会有信息损失,对于头部资源不能有效的表达。
所以, 我们发现其实GBDT和LR的优缺点可以进行互补。
GBDT+LR模型
2014年, Facebook提出了一种利用GBDT自动进行特征筛选和组合, 进而生成新的离散特征向量, 再把该特征向量当做LR模型的输入, 来产生最后的预测结果, 这就是著名的GBDT+LR模型了。GBDT+LR 使用最广泛的场景是CTR点击率预估,即预测当给用户推送的广告会不会被用户点击。
DeepFM
动机
对于CTR问题,被证明的最有效的提升任务表现的策略是特征组合(Feature Interaction), 在CTR问题的探究历史上来看就是如何更好地学习特征组合,进而更加精确地描述数据的特点。可以说这是基础推荐模型到深度学习推荐模型遵循的一个主要的思想。而组合特征大牛们研究过组合二阶特征,三阶甚至更高阶,但是面临一个问题就是随着阶数的提升,复杂度就成几何倍的升高。这样即使模型的表现更好了,但是推荐系统在实时性的要求也不能满足了。所以很多模型的出现都是为了解决另外一个更加深入的问题:如何更高效的学习特征组合?
为了解决上述问题,出现了FM和FFM来优化LR的特征组合较差这一个问题。并且在这个时候科学家们已经发现了DNN在特征组合方面的优势,所以又出现了FNN和PNN等使用深度网络的模型。但是DNN也存在局限性。
- DNN局限 当我们使用DNN网络解决推荐问题的时候存在网络参数过于庞大的问题,这是因为在进行特征处理的时候我们需要使用one-hot编码来处理离散特征,这会导致输入的维度猛增。这里借用AI大会的一张图片:

这样庞大的参数量也是不实际的。为了解决DNN参数量过大的局限性,可以采用非常经典的Field思想,将OneHot特征转换为Dense Vector

此时通过增加全连接层就可以实现高阶的特征组合,如下图所示:

但是仍然缺少低阶的特征组合,于是增加FM来表示低阶的特征组合。
- FNN和PNN 结合FM和DNN其实有两种方式,可以并行结合也可以串行结合。这两种方式各有几种代表模型。在DeepFM之前有FNN,虽然在影响力上可能并不如DeepFM,但是了解FNN的思想对我们理解DeepFM的特点和优点是很有帮助的。

FNN是使用预训练好的FM模块,得到隐向量,然后把隐向量作为DNN的输入,但是经过实验进一步发现,在Embedding layer和hidden layer1之间增加一个product层(如上图所示)可以提高模型的表现,所以提出了PNN,使用product layer替换FM预训练层。
- Wide&Deep FNN和PNN模型仍然有一个比较明显的尚未解决的缺点:对于低阶组合特征学习到的比较少,这一点主要是由于FM和DNN的串行方式导致的,也就是虽然FM学到了低阶特征组合,但是DNN的全连接结构导致低阶特征并不能在DNN的输出端较好的表现。看来我们已经找到问题了,将串行方式改进为并行方式能比较好的解决这个问题。于是Google提出了Wide&Deep模型(将前几章),但是如果深入探究Wide&Deep的构成方式,虽然将整个模型的结构调整为了并行结构,在实际的使用中Wide Module中的部分需要较为精巧的特征工程,换句话说人工处理对于模型的效果具有比较大的影响(这一点可以在Wide&Deep模型部分得到验证)。

2.2.2特征交叉
DCN
动机
Wide&Deep模型的提出不仅综合了“记忆能力”和“泛化能力”, 而且开启了不同网络结构融合的新思路。 所以后面就有各式各样的模型改进Wide部分或者Deep部分, 而Deep&Cross模型(DCN)就是其中比较典型的一个,这是2017年斯坦福大学和谷歌的研究人员在ADKDD会议上提出的, 该模型针对W&D的wide部分进行了改进, 因为Wide部分有一个不足就是需要人工进行特征的组合筛选, 过程繁琐且需要经验, 而2阶的FM模型在线性的时间复杂度中自动进行特征交互,但是这些特征交互的表现能力并不够,并且随着阶数的上升,模型复杂度会大幅度提高。于是乎,作者用一个Cross Network替换掉了Wide部分,来自动进行特征之间的交叉,并且网络的时间和空间复杂度都是线性的。 通过与Deep部分相结合,构成了深度交叉网络(Deep & Cross Network),简称DCN。
模型结构及原理

这个模型的结构也是比较简洁的, 从下到上依次为:Embedding和Stacking层, Cross网络层与Deep网络层并列, 以及最后的输出层。下面也是一一为大家剖析。
Embedding和Stacking层
Embedding层我们已经非常的熟悉了吧, 这里的作用依然是把稀疏离散的类别型特征变成低维密集型。
其中对于某一类稀疏分类特征(如id),$X{embed,i}$是第$i$个分类值(id序号)的embedding向量。$W{embed,i}$是embedding矩阵,$n_e \times n_v$维度,$n_e$是embedding维度,$n_v$是该类特征的唯一取值个数。$x_i$属于该特征的二元稀疏向量(one-hot)编码的。【实质上就是在训练得到的Embedding参数矩阵中找到属于当前样本对应的Embedding向量】。其实绝大多数基于深度学习的推荐模型都需要Embedding操作,参数学习是通过神经网络进行训练。
最后,该层需要将所有的密集型特征与通过embedding转换后的特征进行联合(Stacking):
一共𝑘k个类别特征, dense是数值型特征, 两者在特征维度拼在一块。 上面的这两个操作如果是看了前面的模型的话,应该非常容易理解了。
Cross NetWork
这个就是本模型最大的亮点了【Cross网络】, 这个思路感觉非常Nice。设计该网络的目的是增加特征之间的交互力度。交叉网络由多个交叉层组成, 假设第$l$层的输出向量$xl$, 那么对于第$l+1$层的输出向量$x{l+1}$表示为:
可以看到, 交叉层的二阶部分非常类似PNN提到的外积操作, 在此基础上增加了外积操作的权重向量$w_l$, 以及原输入向量和$x_l$偏置向量$b_l$。 交叉层的可视化如下:

可以看到, 每一层增加了一个$n$维的权重向量$w_l$(n表示输入向量维度), 并且在每一层均保留了输入向量, 因此输入和输出之间的变化不会特别明显。关于这一层, 原论文里面有个具体的证明推导Cross Network为啥有效, 不过比较复杂,这里我拿一个式子简单的解释下上面这个公式的伟大之处:
我们根据上面这个公式, 尝试的写前面几层看看:
$l=0:\mathbf{x_1}=\mathbf{x_0}\mathbf{x_0^T}\mathbf{w_0}+\mathbf{b_0}+\mathbf{x_0}$
$l=1:\mathbf{x_2}=\mathbf{x_0}\mathbf{x_1^T}\mathbf{w_1}+\mathbf{b_1}+\mathbf{x_1}=\mathbf{x_0}[\mathbf{x_0}\mathbf{x_0^T}\mathbf{w_0}+\mathbf{b_0}+\mathbf{x_0}]^T\mathbf{w_1}+\mathbf{b_1}+\mathbf{x_1}$
$l=2:\mathbf{x_3}=\mathbf{x_0}\mathbf{x_2^T}\mathbf{w_2}+\mathbf{b_2}+\mathbf{x_2}=\mathbf{x_0}[\mathbf{x_0}[\mathbf{x_0}\mathbf{x_0^T}\mathbf{w_0}+\mathbf{b_0}+\mathbf{x_0}]^T\mathbf{w_1}+\mathbf{b_1}+\mathbf{x_1}]^T\mathbf{w_2}+\mathbf{b_2}+\mathbf{x_2}$
我们暂且写到第3层的计算, 我们会发现什么结论呢? 给大家总结一下:
$x_1$中包含了所有的$x_0$的1,2阶特征的交互,$x_2$包含了所有的$x_1,x_0$的1、2、3阶特征的交互,$x_3$中包含了所有的$x_2,x_1,x_0$的交互,$x_0$的1、2、3、4阶特征交互。 因此, 交叉网络层的叉乘阶数是有限的。第$l$层特征对应的最高的叉乘阶数$l+1$
Cross网络的参数是共享的, 每一层的这个权重特征之间共享, 这个可以使得模型泛化到看不见的特征交互作用, 并且对噪声更具有鲁棒性。 例如两个稀疏的特征$x_i,x_j$,它们在数据中几乎不发生交互,那么学习$x_i,x_j$的权重对于预测没有任何的意义。
计算交叉网络的参数数量。 假设交叉层的数量是$L_c$,特征$x$的维度是$n$, 那么总共的参数是:
这个就是每一层会有$w$和$b$。且$w$维度和$x$的维度是一致的。
交叉网络的时间和空间复杂度是线性的。这是因为, 每一层都只有$w$和$b$ 没有激活函数的存在,相对于深度学习网络, 交叉网络的复杂性可以忽略不计。
Cross网络是FM的泛化形式, 在FM模型中, 特征$xi$的权重是$v_i$,那么交叉项$x_i,x_j$的权重为$\langle x_i,x_j \rangle$。在DCN中,$x_i$的权重为${W{K}^{(i)^l}}{k=1}$, 交叉项$x_i,x_j$的权重是参数${W{K}^{(i)^l}}{k=1}$和${W{K}^{(j)^l}}_{k=1}$的乘积,这个看上面那个例子展开感受下。因此两个模型都各自学习了独立于其他特征的一些参数,并且交叉项的权重是相应参数的某种组合。FM只局限于2阶的特征交叉(一般),而DCN可以构建更高阶的特征交互, 阶数由网络深度决定,并且交叉网络的参数只依据输入的维度线性增长。
还有一点我们也要了解,对于每一层的计算中, 都会跟着$x_0$, 这个是咱们的原始输入, 之所以会乘以一个这个,是为了保证后面不管怎么交叉,都不能偏离我们的原始输入太远,别最后交叉交叉都跑偏了。
$\mathbf{x_{l+1}}=f(\mathbf{x_l},\mathbf{w_l}.\mathbf{b_l})+\mathbf{x_l}$, 这个东西其实有点跳远连接的意思,也就是和ResNet也有点相似,无形之中还能有效的缓解梯度消失现象。
好了, 关于本模型的交叉网络的细节就介绍到这里了。这应该也是本模型的精华之处了,后面就简单了。
Deep Network
这个就和上面的D&W的全连接层原理一样。这里不再过多的赘述。
具体的可以参考W&D模型。
组合输出层
这个层负责将两个网络的输出进行拼接, 并且通过简单的Logistics回归完成最后的预测:
其中$\mathbf{x{L_1}^T},\mathbf{h{L_2}^T}$分别表示交叉网络和深度网络的输出。 最后二分类的损失函数依然是交叉熵损失:
Cross&Deep模型的原理就是这些了,其核心部分就是Cross Network, 这个可以进行特征的自动交叉, 避免了更多基于业务理解的人工特征组合。 该模型相比于W&D,Cross部分表达能力更强, 使得模型具备了更强的非线性学习能力。
2.2.4序列模型
DIN
动机
Deep Interest Network(DIIN)是2018年阿里巴巴提出来的模型, 该模型基于业务的观察,从实际应用的角度进行改进,相比于之前很多“学术风”的深度模型, 该模型更加具有业务气息。该模型的应用场景是阿里巴巴的电商广告推荐业务, 这样的场景下一般会有大量的用户历史行为信息, 这个其实是很关键的,因为DIN模型的创新点或者解决的问题就是使用了注意力机制来对用户的兴趣动态模拟, 而这个模拟过程存在的前提就是用户之前有大量的历史行为了,这样我们在预测某个商品广告用户是否点击的时候,就可以参考他之前购买过或者查看过的商品,这样就能猜测出用户的大致兴趣来,这样我们的推荐才能做的更加到位,所以这个模型的使用场景是非常注重用户的历史行为特征(历史购买过的商品或者类别信息),也希望通过这一点,能够和前面的一些深度学习模型对比一下。
在个性化的电商广告推荐业务场景中,也正式由于用户留下了大量的历史交互行为,才更加看出了之前的深度学习模型(作者统称Embeding&MLP模型)的不足之处。如果学习了前面的各种深度学习模型,就会发现Embeding&MLP模型对于这种推荐任务一般有着差不多的固定处理套路,就是大量稀疏特征先经过embedding层, 转成低维稠密的,然后进行拼接,最后喂入到多层神经网络中去。
这些模型在这种个性化广告点击预测任务中存在的问题就是无法表达用户广泛的兴趣,因为这些模型在得到各个特征的embedding之后,就蛮力拼接了,然后就各种交叉等。这时候根本没有考虑之前用户历史行为商品具体是什么,究竟用户历史行为中的哪个会对当前的点击预测带来积极的作用。 而实际上,对于用户点不点击当前的商品广告,很大程度上是依赖于他的历史行为的,王喆老师举了个例子
假设广告中的商品是键盘, 如果用户历史点击的商品中有化妆品, 包包,衣服, 洗面奶等商品, 那么大概率上该用户可能是对键盘不感兴趣的, 而如果用户历史行为中的商品有鼠标, 电脑,iPad,手机等, 那么大概率该用户对键盘是感兴趣的, 而如果用户历史商品中有鼠标, 化妆品, T-shirt和洗面奶, 鼠标这个商品embedding对预测“键盘”广告的点击率的重要程度应该大于后面的那三个。
这里也就是说如果是之前的那些深度学习模型,是没法很好的去表达出用户这广泛多样的兴趣的,如果想表达的准确些, 那么就得加大隐向量的维度,让每个特征的信息更加丰富, 那这样带来的问题就是计算量上去了,毕竟真实情景尤其是电商广告推荐的场景,特征维度的规模是非常大的。 并且根据上面的例子, 也并不是用户所有的历史行为特征都会对某个商品广告点击预测起到作用。所以对于当前某个商品广告的点击预测任务,没必要考虑之前所有的用户历史行为。
这样, DIN的动机就出来了,在业务的角度,我们应该自适应的去捕捉用户的兴趣变化,这样才能较为准确的实施广告推荐;而放到模型的角度, 我们应该考虑到用户的历史行为商品与当前商品广告的一个关联性,如果用户历史商品中很多与当前商品关联,那么说明该商品可能符合用户的品味,就把该广告推荐给他。而一谈到关联性的话, 我们就容易想到“注意力”的思想了, 所以为了更好的从用户的历史行为中学习到与当前商品广告的关联性,学习到用户的兴趣变化, 作者把注意力引入到了模型,设计了一个”local activation unit”结构,利用候选商品和历史问题商品之间的相关性计算出权重,这个就代表了对于当前商品广告的预测,用户历史行为的各个商品的重要程度大小, 而加入了注意力权重的深度学习网络,就是这次的主角DIN, 下面具体来看下该模型。
DIN模型结构及原理
在具体分析DIN模型之前, 我们还得先介绍两块小内容,一个是DIN模型的数据集和特征表示, 一个是上面提到的之前深度学习模型的基线模型, 有了这两个, 再看DIN模型,就感觉是水到渠成了。
特征表示
工业上的CTR预测数据集一般都是multi-group categorial form的形式,就是类别型特征最为常见,这种数据集一般长这样:
这里的亮点就是框出来的那个特征,这个包含着丰富的用户兴趣信息。
对于特征编码,作者这里举了个例子:[weekday=Friday, gender=Female, visited_cate_ids={Bag,Book}, ad_cate_id=Book], 这种情况我们知道一般是通过one-hot的形式对其编码, 转成系数的二值特征的形式。但是这里我们会发现一个visted_cate_ids, 也就是用户的历史商品列表, 对于某个用户来讲,这个值是个多值型的特征, 而且还要知道这个特征的长度不一样长,也就是用户购买的历史商品个数不一样多,这个显然。这个特征的话,我们一般是用到multi-hot编码,也就是可能不止1个1了,有哪个商品,对应位置就是1, 所以经过编码后的数据长下面这个样子:
这个就是喂入模型的数据格式了,这里还要注意一点 就是上面的特征里面没有任何的交互组合,也就是没有做特征交叉。这个交互信息交给后面的神经网络去学习。
基线模型
这里的base 模型,就是上面提到过的Embedding&MLP的形式, 这个之所以要介绍,就是因为DIN网络的基准也是他,只不过在这个的基础上添加了一个新结构(注意力网络)来学习当前候选广告与用户历史行为特征的相关性,从而动态捕捉用户的兴趣。
基准模型的结构相对比较简单,我们前面也一直用这个基准, 分为三大模块:Embedding layer,Pooling & Concat layer和MLP, 结构如下:
前面的大部分深度模型结构也是遵循着这个范式套路, 简介一下各个模块。
Embedding layer:这个层的作用是把高维稀疏的输入转成低维稠密向量, 每个离散特征下面都会对应着一个embedding词典, 维度是$D \times K$, 这里的$D$表示的是隐向量的维度, 而表$K$示的是当前离散特征的唯一取值个数, 这里为了好理解,这里举个例子说明,就比如上面的weekday特征:
假设某个用户的weekday特征就是周五,化成one-hot编码的时候,就是[0,0,0,0,1,0,0]表示,这里如果再假设隐向量维度是D, 那么这个特征对应的embedding词典是一个$D\times7$的一个矩阵(每一列代表一个embedding,7列正好7个embedding向量,对应周一到周日),那么该用户这个one-hot向量经过embedding层之后会得到一个𝐷×1D×1的向量,也就是周五对应的那个embedding,怎么算的,其实就是$embedding矩阵*[0,0,0,0,1,0,0]^T$。其实也就是直接把embedding矩阵中one-hot向量为1的那个位置的embedding向量拿出来。 这样就得到了稀疏特征的稠密向量了。其他离散特征也是同理,只不过上面那个multi-hot编码的那个,会得到一个embedding向量的列表,因为他开始的那个multi-hot向量不止有一个是1,这样乘以embedding矩阵,就会得到一个列表了。通过这个层,上面的输入特征都可以拿到相应的稠密embedding向量了。
pooling layer and Concat layer: pooling层的作用是将用户的历史行为embedding这个最终变成一个定长的向量,因为每个用户历史购买的商品数是不一样的, 也就是每个用户multi-hot中1的个数不一致,这样经过embedding层,得到的用户历史行为embedding的个数不一样多,也就是上面的embedding列表$t_i$不一样长, 那么这样的话,每个用户的历史行为特征拼起来就不一样长了。 而后面如果加全连接网络的话,我们知道,他需要定长的特征输入。 所以往往用一个pooling layer先把用户历史行为embedding变成固定长度(统一长度),所以有了这个公式:
这里的$e_{ij}$是用户历史行为的那些embedding。就$e_i$变成了定长的向量, 这里的$e_i$表示第$i$个历史特征组(是历史行为,比如历史的商品id,历史的商品类别id等), 这里的$k$表示对应历史特种组里面用户购买过的商品数量,也就是历史embedding的数量,看上面图里面的user behaviors系列,就是那个过程了。 Concat layer层的作用就是拼接了,就是把这所有的特征embedding向量,如果再有连续特征的话也算上,从特征维度拼接整合,作为MLP的输入。
MLP:这个就是普通的全连接,用了学习特征之间的各种交互。
Loss: 由于这里是点击率预测任务, 二分类的问题,所以这里的损失函数用的负的log对数似然:
这就是base 模型的全貌, 这里应该能看出这种模型的问题, 通过上面的图也能看出来, 用户的历史行为特征和当前的候选广告特征在全都拼起来给神经网络之前,是一点交互的过程都没有, 而拼起来之后给神经网络,虽然是有了交互了,但是原来的一些信息,比如,每个历史商品的信息会丢失了一部分,因为这个与当前候选广告商品交互的是池化后的历史特征embedding, 这个embedding是综合了所有的历史商品信息, 这个通过我们前面的分析,对于预测当前广告点击率,并不是所有历史商品都有用,综合所有的商品信息反而会增加一些噪声性的信息,可以联想上面举得那个键盘鼠标的例子,如果加上了各种洗面奶,衣服啥的反而会起到反作用。其次就是这样综合起来,已经没法再看出到底用户历史行为中的哪个商品与当前商品比较相关,也就是丢失了历史行为中各个商品对当前预测的重要性程度。最后一点就是如果所有用户浏览过的历史行为商品,最后都通过embedding和pooling转换成了固定长度的embedding,这样会限制模型学习用户的多样化兴趣。
那么改进这个问题的思路有哪些呢? 第一个就是加大embedding的维度,增加之前各个商品的表达能力,这样即使综合起来,embedding的表达能力也会加强, 能够蕴涵用户的兴趣信息,但是这个在大规模的真实推荐场景计算量超级大,不可取。 另外一个思路就是在当前候选广告和用户的历史行为之间引入注意力的机制,这样在预测当前广告是否点击的时候,让模型更关注于与当前广告相关的那些用户历史产品,也就是说与当前商品更加相关的历史行为更能促进用户的点击行为。 作者这里又举了之前的一个例子:
想象一下,当一个年轻母亲访问电子商务网站时,她发现展示的新手袋很可爱,就点击它。让我们来分析一下点击行为的驱动力。
展示的广告通过软搜索这位年轻母亲的历史行为,发现她最近曾浏览过类似的商品,如大手提袋和皮包,从而击中了她的相关兴趣
第二个思路就是DIN的改进之处了。DIN通过给定一个候选广告,然后去注意与该广告相关的局部兴趣的表示来模拟此过程。 DIN不会通过使用同一向量来表达所有用户的不同兴趣,而是通过考虑历史行为的相关性来自适应地计算用户兴趣的表示向量(对于给的广告)。 该表示向量随不同广告而变化。下面看一下DIN模型。
DIN模型架构
上面分析完了base模型的不足和改进思路之后,DIN模型的结构就呼之欲出了,首先,它依然是采用了基模型的结构,只不过是在这个的基础上加了一个注意力机制来学习用户兴趣与当前候选广告间的关联程度, 用论文里面的话是,引入了一个新的local activation unit, 这个东西用在了用户历史行为特征上面, 能够根据用户历史行为特征和当前广告的相关性给用户历史行为特征embedding进行加权。我们先看一下它的结构,然后看一下这个加权公式。
这里改进的地方已经框出来了,这里会发现相比于base model, 这里加了一个local activation unit, 这里面是一个前馈神经网络,输入是用户历史行为商品和当前的候选商品, 输出是它俩之间的相关性, 这个相关性相当于每个历史商品的权重,把这个权重与原来的历史行为embedding相乘求和就得到了用户的兴趣表示𝑣𝑈(𝐴)v*U(A*), 这个东西的计算公式如下:
第四章 推荐系统算法面经
4.1ML与DL基础
机器学习
- 介绍一个最熟悉的机器学习算法
参考解析
机器学习
介绍一个最熟悉的机器学习算法
LR:逻辑回归是假设数据服从伯努利分布,通过极大似然估计方法,使用梯度下降来求解参数,达到二分类目的的一个模型。我们在考虑把广义线性模型用于分类的时候,需要如何确定逻辑边界,感知机模型用的是阶跃函数,但是阶跃函数不可导,不能作为广义线性模型的联系函数。逻辑回归对数几率函数代替阶跃函数。因为对数几率函数是单调可微的一个函数,所以可以作为联系函数。所以逻辑回归本质上还是广义线性模型。
LR的优缺点:
- 形式简单,可解释性好;
- 它直接对分类概率进行建模,不需要知道真实数据的分布,这和生成式模型相区别,避免了假设错误带来的问题;
- 不仅能够预测出类别,还能够预测出概率,能够用于很多场景,比如ctr排序中;
- 对数几率函数任意阶数可导,能够很容易优化;
- 可以获得特征权重,方便我们进行特征筛选;
- 训练速度快;
- 它对稀疏特征效果比较好,因为使用的是w1 w2 w3本质上的线性模型,稀疏数据能够筛选出不稀疏的重要特征。
- 模型表达能力有限;
- 样本不均衡很难处理;
- 在非线性可分数据集上性能有限;
LR推导:

决策树怎么建树,基尼系数公式
决策树建树算法有三种ID3、C4.5、CART,每个算法主要考虑的事情主要有三个问题:
- 选什么特征来当条件?
- 条件判断的属性值是什么?
- 什么时候停止分裂,达到我们需要的决策?
CART
CART树采用基尼系数进行最优特征的选择,构造过程中假设有K类,则样本属于第K类的概率为pk,则定义样本分布的基尼系数为:
根据基尼系数定义,可以得到样本集合D的基尼指数,其中ck表述样本集合中第k类的子集:
如果数据集D根据特征A在某一取值a上进行分割,得到D1,D2两部分后,那么在特征A下集合D的基尼系数如下所示。其中基尼系数Gini(D)表示集合D的不确定性,基尼系数Gini(D,A)表示A=a分割后集合D的不确定性。基尼指数越大,样本集合的不确定性越大。
选什么特征作为最优特征分割:当我们计算完所有特征基尼指数后,选择其中最小所在特征的作为分裂特征;
条件判断的属性值是什么:判断特征属性是否为为此最指数来分裂;
什么时候停止分裂,达到我们需要的决策:分裂的最小收益小于我们的划定的阈值,或者树的深度达到我们的阈值。
Adaboost拟合目标是什么
- Adaboost中每训练完一个弱分类器都就会调整权重,上一轮训练中被误分类的点的权重会增加,在本轮训练中,由于权重影响,本轮的弱分类器将更有可能把上一轮的误分类点分对,如果还是没有分对,那么分错的点的权重将继续增加,下一个弱分类器将更加关注这个点,这是adaboost目标。
Adaboost介绍一下,每个基学习器的权重怎么得到的
介绍下GBDT
介绍XGBoost
介绍下LightGBM
LightGBM相对于XGBoost的改进
GBDT中的梯度是什么,怎么用
GBDT如何计算特征重要性
GBDT讲一下,GBDT拟合残差,是真实的误差嘛,在什么情况下看做是真实的误差
references:
- ‘datawhale-FunRec)‘
- https://mp.weixin.qq.com/s/ZtnaQrVIpVOPJpqMdLWOcw
- https://chenk.tech/posts/8ad63d9d.html
- B站黑马推荐系统实战课程
