1.灌水部分
1.1 推荐系统的意义
随着移动互联网的飞速发展,人们已经处于一个信息过载的时代。在这个时代中,信息的生产者很难将信息呈现在对它们感兴趣的信息消费者面前,而对于信息消费者也很难从海量的信息中找到自己感兴趣的信息。推荐系统就是一个将信息生产者和信息消费者连接起来的桥梁。平台往往会作为推荐系统的载体,实现信息生产者和消费者之间信息的匹配。上述提到的平台方、信息生产者和消费者可以分别用平台方(如:腾讯视频、淘宝、网易云音乐等)、物品(如:视频、商品、音乐等)和用户和来指代。下面分别从这三方需求出发,介绍推荐系统的存在的意义。
平台方
平台方一般是为信息生产者提供物品展示的位置,然后通过不同的方式吸引用户来到平台上寻找他们感兴趣的物品。平台通过商家对物品的展示以及用户的浏览、观看或下单等行为,就产生了所谓的"流量"。
对平台方而言,流量的高效利用是推荐系统存在的重要原因。以典型的电商网站一般具有如图所示的树状拓扑结构,树状结构在连通性方面有着天然的劣势,阻碍这流量的高效流通。推荐系统的出现使得原本的树状结构变成网络拓扑结构,大大增强了整个网络的连通性。推荐模块不仅使用户在当前页面有了更好的选择路径,同时也给了每个商品增加入口和展示机会,进而提高了成交概率。而推荐质量的好坏,直接决定了用户选择这条路径的可能性,进而影响着流量的利用效率。
推荐系统解决产品能够最大限度地吸引用户、留存用户、增加用户粘性、提高用户转化率的问题,从而达到平台商业目标增长的目的。不同平台的目标取决于其商业的盈利目的,例如对于YouTube,其商业目标是最大化视频被点击(点击率)以及用户观看的时长(完播率),同时也会最大化内置广告的点击率;对于淘宝等电商平台,除了最大化商品的点击率外,最关键的目标则是最大化用户的转化率,即由点击到完成商品购买的指标。推荐系统能够平台带来丰厚的商业价值 。
信息生产者(物品)
因为在互联网大数据时代下,物品的长尾性和二八原则是非常严重的。具体来说,对于一个平台而言80%的销售额可能是那些最畅销20%的物品。但是那20%的物品其实只能满足一小部分人的需求,对于绝大多数的用户的需求需要从那80%的长尾物品中去满足。虽然长尾物品的总销售额占比不大,但是因为长尾物品的数量及其庞大,如果可以充分挖掘长尾物品,那这些长尾物品的销售额的总量有可能会超过热门商品。
物品只是信息生产者的产物,对于信息生产者而言,例如商家、视频创作者等,他们也更希望自己生产的内容可以得到更多的曝光,尤其是对于新来的商家或者视频创作者,这样可以激发他们创作的热情,进而创作出更多的商品或者视频,让更多的用户的需求得到满足。
对于一个平台而言,无论是否靠平台上的物品直接盈利,其将平台上的内容与用户进行匹配的能力都是衡量平台的重要标准之一,推荐系统的好坏很大程度上决定了平台匹配需求和供给的能力。推荐系统匹配需求和供给的能力决定了其最终的商业价值。
信息消费者(用户)
推荐系统对于用户而言,除了将平台上的需求和供给进行匹配外,还需要尽可能地提高用户的体验,但是对于一个平台来说,影响用户体验的因素非常多(产品设计、广告数量等)。对于一个有明确需求的用户来说,用户在平台上可以直接通过搜索来快速满足自己的需求,但这也仅仅是一个平台最基础的用户体验(平台做的好是应该的,但是做的不好可能会被喷)。对于一个没有明确需求的用户来说,用户会通过浏览平台上的推荐页来获取一些额外的惊喜需求。因为用户没有明确的需求,也就对推荐页浏览的内容没有明确的预期,但是并不说明用户没有期待。我们每天都希望自己的一天中充满惊喜,这样生活才会感觉更加的多姿多彩。推荐系统可以像为用户准备生日礼物一样,让呈现的内容给用户带来惊喜,进而增强用户对平台的依赖。此外,在给用户带来惊喜的同时,也会提高平台的转化率(例如成交额)
推荐和搜索的区别
搜索和推荐都是解决互联网大数据时代信息过载的手段,但是它们也存在着许多的不同:
- 用户意图:搜索时的用户意图是非常明确的,用户通过查询的关键词主动发起搜索请求。对于推荐而言,用户的需求是不明确的,推荐系统在通过对用户历史兴趣的分析给用户推荐他们可能感兴趣的内容。
- 个性化程度:对于搜索而言,由于限定的了搜索词,所以展示的内容对于用户来说是有标准答案的,所以搜索的个性化程度较低。而对于推荐来说,推荐的内容本身就是没有标准答案的,每个人都有不同的兴趣,所以每个人展示的内容,个性化程度比较强。
- 优化目标:对于搜索系统而言,更希望可以快速地、准确地定位到标准答案,所以希望搜索结果中答案越靠前越好,通常评价指标有:归一化折损累计收益(NDCG)、精确率(Precision)和召回率(Recall)。对于推荐系统而言,因为没有标准的答案,所以优化目标可能会更宽泛。例如用户停留时长、点击、多样性,评分等。不同的优化目标又可以拆解成具体的不同的评价指标。
- 马太效应和长尾理论:对于搜索系统来说,用户的点击基本都集中在排列靠前的内容上,对于排列靠后的很少会被关注,这就是马太效应。而对于推荐系统来说,热门物品被用户关注更多,冷门物品不怎么被关注的现象也是存在的,所以也存在马太效应。此外,在推荐系统中,冷门物品的数量远远高于热门物品的数量,所以物品的长尾性非常明显。
1.2 常规算法
推荐系统是一个非常大的框架,有非常多的模块在里面,完整的一套推荐系统体系里,不仅会涉及到推荐算法工程师、后台开发工程师、数据挖掘/分析工程师、NLP/CV工程师还有前端、客户端甚至产品、运营等支持。我们作为算法工程师,需要掌握的技术栈主要就是在算法和工程两个区域了,所以这篇文章将会分别从算法和工程两个角度出发,结合两者分析当前主流的一些推荐算法技术栈。

为了帮助新手在后文方便理解,首先简单介绍这些模块的功能主要是:
- 召回:从推荐池中选取几千上万的item,送给后续的排序模块。由于召回面对的候选集十分大,且一般需要在线输出,故召回模块必须轻量快速低延迟。由于后续还有排序模块作为保障,召回不需要十分准确,但不可遗漏(特别是搜索系统中的召回模块)。目前基本上采用多路召回解决范式,分为非个性化召回和个性化召回。个性化召回又有content-based、behavior-based、feature-based等多种方式。
- 粗排:粗拍的原因是有时候召回的结果还是太多,精排层速度还是跟不上,所以加入粗排。粗排可以理解为精排前的一轮过滤机制,减轻精排模块的压力。粗排介于召回和精排之间,要同时兼顾精准性和低延迟。一般模型也不能过于复杂
- 精排:获取粗排模块的结果,对候选集进行打分和排序。精排需要在最大时延允许的情况下,保证打分的精准性,是整个系统中至关重要的一个模块,也是最复杂,研究最多的一个模块。精排系统构建一般需要涉及样本、特征、模型三部分。
- 重排:获取精排的排序结果,基于运营策略、多样性、context上下文等,重新进行一个微调。比如三八节对美妆类目商品提权,类目打散、同图打散、同卖家打散等保证用户体验措施。重排中规则比较多,但目前也有不少基于模型来提升重排效果的方案。
- 混排:多个业务线都想在Feeds流中获取曝光,则需要对它们的结果进行混排。比如推荐流中插入广告、视频流中插入图文和banner等。可以基于规则策略(如广告定坑)和强化学习来实现。
召回/粗排
推荐系统的召回阶段可以理解为根据用户的历史行为数据,为用户在海量的信息中粗选一批待推荐的内容,挑选出一个小的候选集的过程。粗排用到的很多技术与召回重合,所以放在一起讲,粗排也不是必需的环节,它的功能对召回的结果进行个粗略的排序,在保证一定精准的前提下,进一步减少往后传送的物品数量,这就是粗排的作用。

召回模块面对几百上千万的推荐池物料规模,候选集十分庞大。由于后续有排序模块作为保障,故不需要十分准确,但必须保证不要遗漏和低延迟。目前主要通过多路召回来实现,一方面各路可以并行计算,另一方面取长补短。可以看到各类同类竞品的系统虽然细节上多少存在差异,但不约而同的采取了多路召回的架构,这类设计考虑如下几点问题:
- 考虑用户层面:用户兴趣的多元化,用户需求与场景的多元化:例如:新闻需求,重大要闻,相关内容沉浸阅读等等
- 考虑系统层面:增强系统的鲁棒性;部分召回失效,其余召回队列兜底不会导致整个召回层失效;排序层失效,召回队列兜底不会导致整个推荐系统失效
- 系统多样性内容分发:图文、视频、小视频;精准、试探、时效一定比例;召回目标的多元化,例如:相关性,沉浸时长,时效性,特色内容等等
- 可解释性推荐一部分召回是有明确推荐理由的:很好的解决产品性数据的引入;
介绍了召回任务的目的和场景后,接下来分析召回层面主要的技术栈,因为召回一般都是多路召回,从模型角度分析有很多召回算法,这种一般是在召回层占大部分比例点召回,除此之外,还会有探索类召回、策略运营类召回、社交类召回等。接下来我们着重介绍模型类召回。
经典模型召回
随着技术发展,在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等。
精排
排序模型是推荐系统中涵盖的研究方向最多,有非常多的子领域值得研究探索,这也是推荐系统中技术含量最高的部分,毕竟它是直接面对用户,产生的结果对用户影响最大的一层。目前精排层深度学习已经一统天下了,这是王喆老师《深度学习推荐算法》书中的精排层模型演化线路。具体来看分为DNN、Wide&Deep两大块,实际深入还有序列建模,以及没有提到的多任务建模都是工业界非常常用的,所以我们接下来具体谈论其中每一块的技术栈。

重排序
我们知道常见的有三种优化目标:Point Wise、Pair Wise 和 List Wise。重排序阶段对精排生成的Top-N个物品的序列进行重新排序,生成一个Top-K个物品的序列,作为排序系统最后的结果,直接展现给用户。重排序的原因是因为多个物品之间往往是相互影响的,而精排序是根据PointWise得分,容易造成推荐结果同质化严重,有很多冗余信息。而重排序面对的挑战就是海量状态空间如何求解的问题,一般在精排层我们使用AUC作为指标,但是在重排序更多关注NDCG等指标。
重排序在业务中,还会根据一些策略、运营规则参与排序,比如强制去重、间隔排序、流量扶持等,但是总计趋势上看还是算法排序越来越占据主流趋势。重排序更多的是List Wise作为优化目标的,它关注的是列表中商品顺序的问题来优化模型,但是一般List Wise因为状态空间大,存在训练速度慢的问题。这方面典型的做法,基于RNN、Transformer、强化学习的都有,这方面因为不是推荐的一个核心,所以没有展开来讲,而且这一块比较依赖实际的业务场景。
这里的经典算法有:MRR、DPP、RNN等;
2.协同过滤
2.1 基本思想
协同过滤(Collaborative Filtering)推荐算法是最经典、最常用的推荐算法。基本思想是:
- 根据用户之前的喜好以及其他兴趣相近的用户的选择来给用户推荐物品。
- 基于对用户历史行为数据的挖掘发现用户的喜好偏向, 并预测用户可能喜好的产品进行推荐。
- 一般是仅仅基于用户的行为数据(评价、购买、下载等), 而不依赖于项的任何附加信息(物品自身特征)或者用户的任何附加信息(年龄, 性别等)。
- 目前应用比较广泛的协同过滤算法是基于邻域的方法,主要有:
- 基于用户的协同过滤算法(UserCF):给用户推荐和他兴趣相似的其他用户喜欢的产品。
- 基于物品的协同过滤算法(ItemCF):给用户推荐和他之前喜欢的物品相似的物品。
不管是 UserCF 还是 ItemCF 算法, 重点是计算用户之间(或物品之间)的相似度。
2.2 相似性度量方法
- 杰卡德(Jaccard)相似系数
- 其中 N(u),N(v) 分别表示用户 u 和用户 v 交互物品的集合。
- 对于用户 u 和 v ,该公式反映了两个交互物品交集的数量占这两个用户交互物品并集的数量的比例。
Jaccard 系数是衡量两个集合的相似度一种指标,计算公式如下:$$sim_{uv}=\frac{|N(u) \cap N(v)|}{|N(u)| \cup|N(v)|}$$
由于杰卡德相似系数一般无法反映具体用户的评分喜好信息,所以常用来评估用户是否会对某物品进行打分, 而不是预估用户会对某物品打多少分。
- 余弦相似度 余弦相似度衡量了两个向量的夹角,夹角越小越相似。余弦相似度的计算如下,其与杰卡德(Jaccard)相似系数只是在分母上存在差异:
- 设用户和物品数量分别为 m,n,交互矩阵A就是一个 m 行 n 列的矩阵。
- 矩阵中的元素均为 0/1。若用户 i 对物品 j 存在交互,那么 A_{i,j}=1,否则为 0 。
- 那么,用户之间的相似度可以表示为:
- 向量 u,v 在形式都是 one-hot 类型,u\cdot v 表示向量点积。
$$sim_{uv}=\frac{|N(u) \cap N(v)|}{\sqrt{|N(u)|\cdot|N(v)|}}$$
从向量的角度进行描述,令矩阵 A 为用户-物品交互矩阵,矩阵的行表示用户,列表示物品。
上述用户-物品交互矩阵在现实中是十分稀疏的,为了节省内存,交互矩阵会采用字典进行存储。在
sklearn 中,余弦相似度的实现:from 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 和用户 v 对物品 i 是否有交互(或具体评分值);
- \bar r_u, \bar r_v 分别表示用户 u 和用户 v 交互的所有物品交互数量或者评分的平均值;
在用户之间的余弦相似度计算时,将用户向量的内积展开为各元素乘积和:
$$sim_{uv} = \frac{\sum_i r_{ui}*r_{vi}}{\sqrt{\sum_i r_{ui}^2}\sqrt{\sum_i r_{vi}^2}}$$
皮尔逊相关系数与余弦相似度的计算公式非常的类似,如下:
$$sim(u,v)=\frac{\sum_{i\in I}(r_{ui}-\bar r_u)(r_{vi}-\bar r_v)}{\sqrt{\sum_{i\in I }(r_{ui}-\bar r_u)^2}\sqrt{\sum_{i\in I }(r_{vi}-\bar r_v)^2}}$$
相较于余弦相似度,皮尔逊相关系数通过使用用户的平均分对各独立评分进行修正,减小了用户评分偏置的影响。在
scipy中,皮尔逊相关系数的实现:from scipy.stats import pearsonr i = [1, 0, 0, 0] j = [1, 0.5, 0.5, 0] pearsonr(i, j)
适用场景
- Jaccard 相似度表示两个集合的交集元素个数在并集中所占的比例 ,所以适用于隐式反馈数据(0-1)。
- 余弦相似度在度量文本相似度、用户相似度、物品相似度的时候都较为常用。
- 皮尔逊相关度,实际上也是一种余弦相似度。不过先对向量做了中心化,范围在 -1 到 1。
- 相关度量的是两个变量的变化趋势是否一致,两个随机变量是不是同增同减。
- 不适合用作计算布尔值向量(0-1)之间相关度。
2.3 基于用户的协同过滤
基本思想
基于用户的协同过滤(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 对物品的历史平均评分。
$$R_{\mathrm{u}, \mathrm{p}}=\bar{R}_{u} + \frac{\sum_{\mathrm{s} \in S}\left(w_{\mathrm{u}, \mathrm{s}} \cdot \left(R_{s, p}-\bar{R}_{s}\right)\right)}{\sum_{\mathrm{s} \in S} w_{\mathrm{u}, \mathrm{s}}}$$
对用户进行物品推荐
- 在获得用户 u 对不同物品的评价预测后, 最终的推荐列表根据预测评分进行排序得到。
2.4基于物品的协同过滤
基本思想
基于物品的协同过滤(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,41)
- 基于
sklearn计算物品之间的皮尔逊相关系数:

- 根据皮尔逊相关系数, 可以找到与物品5最相似的2个物品是 item1 和 item4, 下面基于上面的公式计算最终得分:
$$P_{Alice, 物品5}=\bar{R}_{物品5}+\frac{\sum_{k=1}^{2}\left(w_{物品5,物品 k}\left(R_{Alice, 物品k}-\bar{R}_{物品k}\right)\right)}{\sum_{k=1}^{2} w_{物品k, 物品5}} \\
=\frac{13}{4}+\frac{0.97*(5-3.2)+0.58*(4-3.4)}{0.97+0.58}=4.6$$
3.算法评估
由于UserCF和ItemCF结果评估部分是共性知识点, 所以在这里统一标识。
3.1 召回率
对用户 u 推荐 N 个物品记为 R(u), 令用户 u 在测试集上喜欢的物品集合为T(u), 那么召回率定义为:
$$\operatorname{Recall}=\frac{\sum_{u}|R(u) \cap T(u)|}{\sum_{u}|T(u)|}$$
- 含义:在模型召回预测的物品中,预测准确的物品占用户实际喜欢的物品的比例。
3.2 精确率
精确率定义为:
$$\operatorname{Precision}=\frac{\sum_{u} \mid R(u) \cap T(u)|}{\sum_{u}|R(u)|}$$
- 含义:推荐的物品中,对用户准确推荐的物品占总物品的比例。
- 如要确保召回率高,一般是推荐更多的物品,期望推荐的物品中会涵盖用户喜爱的物品。而实际中,推荐的物品中用户实际喜爱的物品占少数,推荐的精确率就会很低。故同时要确保高召回率和精确率往往是矛盾的,所以实际中需要在二者之间进行权衡。
3.3 覆盖率
覆盖率反映了推荐算法发掘长尾的能力, 覆盖率越高, 说明推荐算法越能将长尾中的物品推荐给用户。
$$\text { Coverage }=\frac{\left|\bigcup_{u \in U} R(u)\right|}{|I|}$$
- 含义:推荐系统能够推荐出来的物品占总物品集合的比例。
- 其中 |I| 表示所有物品的个数;
- 系统的用户集合为U;
- 推荐系统给每个用户推荐一个长度为 N 的物品列表R(u).
- 覆盖率表示最终的推荐列表中包含多大比例的物品。如果所有物品都被给推荐给至少一个用户, 那么覆盖率是100%。
3.4 新颖度
用推荐列表中物品的平均流行度度量推荐结果的新颖度。 如果推荐出的物品都很热门, 说明推荐的新颖度较低。 由于物品的流行度分布呈长尾分布, 所以为了流行度的平均值更加稳定, 在计算平均流行度时对每个物品的流行度取对数。
4.排序
4.1 GBDT+LR简介
前面介绍的协同过滤和矩阵分解存在的劣势就是仅利用了用户与物品相互行为信息进行推荐, 忽视了用户自身特征, 物品自身特征以及上下文信息等,导致生成的结果往往会比较片面。 而这次介绍的这个模型是2014年由Facebook提出的GBDT+LR模型, 该模型利用GBDT自动进行特征筛选和组合, 进而生成新的离散特征向量, 再把该特征向量当做LR模型的输入, 来产生最后的预测结果, 该模型能够综合利用用户、物品和上下文等多种不同的特征, 生成较为全面的推荐结果, 在CTR点击率预估场景下使用较为广泛。
下面首先会介绍逻辑回归和GBDT模型各自的原理及优缺点, 然后介绍GBDT+LR模型的工作原理和细节。
4.2 逻辑回归模型
逻辑回归模型非常重要, 在推荐领域里面, 相比于传统的协同过滤, 逻辑回归模型能够综合利用用户、物品、上下文等多种不同的特征生成较为“全面”的推荐结果, 关于逻辑回归的更多细节, 可以参考下面给出的链接,这里只介绍比较重要的一些细节和在推荐中的应用。
逻辑回归是在线性回归的基础上加了一个 Sigmoid 函数(非线形)映射,使得逻辑回归成为了一个优秀的分类算法, 学习逻辑回归模型, 首先应该记住一句话:逻辑回归假设数据服从伯努利分布,通过极大化似然函数的方法,运用梯度下降来求解参数,来达到将数据二分类的目的。
相比于协同过滤和矩阵分解利用用户的物品“相似度”进行推荐, 逻辑回归模型将问题看成了一个分类问题, 通过预测正样本的概率对物品进行排序。这里的正样本可以是用户“点击”了某个商品或者“观看”了某个视频, 均是推荐系统希望用户产生的“正反馈”行为, 因此逻辑回归模型将推荐问题转化成了一个点击率预估问题。而点击率预测就是一个典型的二分类, 正好适合逻辑回归进行处理, 那么逻辑回归是如何做推荐的呢? 过程如下:
- 将用户年龄、性别、物品属性、物品描述、当前时间、当前地点等特征转成数值型向量
- 确定逻辑回归的优化目标,比如把点击率预测转换成二分类问题, 这样就可以得到分类问题常用的损失作为目标, 训练模型
- 在预测的时候, 将特征向量输入模型产生预测, 得到用户“点击”物品的概率
- 利用点击概率对候选物品排序, 得到推荐列表
推断过程可以用下图来表示:
这里的关键就是每个特征的权重参数w, 我们一般是使用梯度下降的方式, 首先会先随机初始化参数w, 然后将特征向量(也就是我们上面数值化出来的特征)输入到模型, 就会通过计算得到模型的预测概率, 然后通过对目标函数求导得到每个w的梯度, 然后进行更新w
这里的目标函数长下面这样:
$$J(w)=-\frac{1}{m}\left(\sum_{i=1}^{m}\left(y^{i} \log f_{w}\left(x^{i}\right)+\left(1-y^{i}\right) \log \left(1-f_{w}\left(x^{i}\right)\right)\right)\right.$$
求导之后的方式长这样:
$$w_{j} \leftarrow w_{j}-\gamma \frac{1}{m} \sum_{i=1}^{m}\left(f_{w}\left(x^{i}\right)-y^{i}\right) x_{j}^{i}$$
这样通过若干次迭代, 就可以得到最终的w了, 关于这些公式的推导,可以参考下面给出的文章链接, 下面我们分析一下逻辑回归模型的优缺点。
优点:
- LR模型形式简单,可解释性好,从特征的权重可以看到不同的特征对最后结果的影响。
- 训练时便于并行化,在预测时只需要对特征进行线性加权,所以性能比较好,往往适合处理海量id类特征,用id类特征有一个很重要的好处,就是防止信息损失(相对于范化的 CTR 特征),对于头部资源会有更细致的描述
- 资源占用小,尤其是内存。在实际的工程应用中只需要存储权重比较大的特征及特征对应的权重。
- 方便输出结果调整。逻辑回归可以很方便的得到最后的分类结果,因为输出的是每个样本的概率分数,我们可以很容易的对这些概率分数进行cutoff,也就是划分阈值(大于某个阈值的是一类,小于某个阈值的是一类)
当然, 逻辑回归模型也有一定的局限性
- 表达能力不强, 无法进行特征交叉, 特征筛选等一系列“高级“操作(这些工作都得人工来干, 这样就需要一定的经验, 否则会走一些弯路), 因此可能造成信息的损失
- 准确率并不是很高。因为这毕竟是一个线性模型加了个sigmoid, 形式非常的简单(非常类似线性模型),很难去拟合数据的真实分布
- 处理非线性数据较麻烦。逻辑回归在不引入其他方法的情况下,只能处理线性可分的数据, 如果想处理非线性, 首先对连续特征的处理需要先进行离散化(离散化的目的是为了引入非线性),如上文所说,人工分桶的方式会引入多种问题。
- LR 需要进行人工特征组合,这就需要开发者有非常丰富的领域经验,才能不走弯路。这样的模型迁移起来比较困难,换一个领域又需要重新进行大量的特征工程。
所以如何自动发现有效的特征、特征组合,弥补人工经验不足,缩短LR特征实验周期,是亟需解决的问题, 而GBDT模型, 正好可以自动发现特征并进行有效组合
4.3 GBDT模型
GBDT全称梯度提升决策树,在传统机器学习算法里面是对真实分布拟合的最好的几种算法之一,在前几年深度学习还没有大行其道之前,gbdt在各种竞赛是大放异彩。原因大概有几个,一是效果确实挺不错。二是即可以用于分类也可以用于回归。三是可以筛选特征, 所以这个模型依然是一个非常重要的模型。
GBDT是通过采用加法模型(即基函数的线性组合),以及不断减小训练过程产生的误差来达到将数据分类或者回归的算法, 其训练过程如下:
gbdt通过多轮迭代, 每轮迭代会产生一个弱分类器, 每个分类器在上一轮分类器的残差基础上进行训练。 gbdt对弱分类器的要求一般是足够简单, 并且低方差高偏差。 因为训练的过程是通过降低偏差来不断提高最终分类器的精度。 由于上述高偏差和简单的要求,每个分类回归树的深度不会很深。最终的总分类器是将每轮训练得到的弱分类器加权求和得到的(也就是加法模型)。
关于GBDT的详细细节,依然是可以参考下面给出的链接。这里想分析一下GBDT如何来进行二分类的,因为我们要明确一点就是gbdt 每轮的训练是在上一轮的训练的残差基础之上进行训练的, 而这里的残差指的就是当前模型的负梯度值, 这个就要求每轮迭代的时候,弱分类器的输出的结果相减是有意义的, 而gbdt 无论用于分类还是回归一直都是使用的CART 回归树, 那么既然是回归树, 是如何进行二分类问题的呢?
GBDT 来解决二分类问题和解决回归问题的本质是一样的,都是通过不断构建决策树的方式,使预测结果一步步的接近目标值, 但是二分类问题和回归问题的损失函数是不同的, 关于GBDT在回归问题上的树的生成过程, 损失函数和迭代原理可以参考给出的链接, 回归问题中一般使用的是平方损失, 而二分类问题中, GBDT和逻辑回归一样, 使用的下面这个:
$$L=\arg \min \left[\sum_{i}^{n}-\left(y_{i} \log \left(p_{i}\right)+\left(1-y_{i}\right) \log \left(1-p_{i}\right)\right)\right]$$
其中, y_i是第i个样本的观测值, 取值要么是0要么是1, 而p_i是第i个样本的预测值, 取值是0-1之间的概率,由于我们知道GBDT拟合的残差是当前模型的负梯度, 那么我们就需要求出这个模型的导数, 即\frac{dL}{dp_i}, 对于某个特定的样本, 求导的话就可以只考虑它本身, 去掉加和号, 那么就变成了\frac{dl}{dp_i}, 其中l如下:
$$\begin{aligned}
l &=-y_{i} \log \left(p_{i}\right)-\left(1-y_{i}\right) \log \left(1-p_{i}\right) \\
&=-y_{i} \log \left(p_{i}\right)-\log \left(1-p_{i}\right)+y_{i} \log \left(1-p_{i}\right) \\
&=-y_{i}\left(\log \left(\frac{p_{i}}{1-p_{i}}\right)\right)-\log \left(1-p_{i}\right)
\end{aligned}$$
如果对逻辑回归非常熟悉的话, \left(\log \left(\frac{p_{i}}{1-p_{i}}\right)\right)一定不会陌生吧, 这就是对几率比取了个对数, 并且在逻辑回归里面这个式子会等于\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}, 则上面这个式子变成了:
$$\begin{aligned}
l &=-y_{i} \log \left(\eta_{i}\right)-\log \left(1-\frac{e^{\log \left(\eta_{i}\right)}}{1+e^{\log \left(\eta_{i}\right)}}\right) \\
&=-y_{i} \log \left(\eta_{i}\right)-\log \left(\frac{1}{1+e^{\log \left(\eta_{i}\right)}}\right) \\
&=-y_{i} \log \left(\eta_{i}\right)+\log \left(1+e^{\log \left(\eta_{i}\right)}\right)
\end{aligned}$$
这时候,我们对log(\eta_i)求导, 得
$$\frac{d l}{d \log (\eta_i)}=-y_{i}+\frac{e^{\log \left(\eta_{i}\right)}}{1+e^{\log \left(\eta_{i}\right)}}=-y_i+p_i$$
这样, 我们就得到了某个训练样本在当前模型的梯度值了, 那么残差就是y_i-p_i。GBDT二分类的这个思想,其实和逻辑回归的思想一样,逻辑回归是用一个线性模型去拟合P(y=1|x)这个事件的对数几率log\frac{p}{1-p}=\theta^Tx, GBDT二分类也是如此, 用一系列的梯度提升树去拟合这个对数几率, 其分类模型可以表达为:
$$P(Y=1 \mid x)=\frac{1}{1+e^{-F_{M}(x)}}$$
下面我们具体来看GBDT的生成过程, 构建分类GBDT的步骤有两个:
- 初始化GBDT 和回归问题一样, 分类 GBDT 的初始状态也只有一个叶子节点,该节点为所有样本的初始预测值,如下:
$$ F_{0}(x)=\arg \min _{\gamma} \sum_{i=1}^{n} L(y, \gamma)$$
上式里面, $F$代表GBDT模型, $F_0$是模型的初识状态, 该式子的意思是找到一个$\gamma$,使所有样本的 Loss 最小,在这里及下文中,$\gamma$都表示节点的输出,即叶子节点, 且它是一个 $log(\eta_i)$ 形式的值(回归值),在初始状态,$\gamma =F_0$。 下面看例子(该例子来自下面的第二个链接), 假设我们有下面3条样本:
我们希望构建 GBDT 分类树,它能通过「喜欢爆米花」、「年龄」和「颜色偏好」这 3 个特征来预测某一个样本是否喜欢看电影。我们把数据代入上面的公式中求Loss:
$$\operatorname{Loss}=L(1, \gamma)+L(1, \gamma)+L(0, \gamma)$$
为了令其最小, 我们求导, 且让导数为0, 则:
$$\operatorname{Loss}=p-1 + p-1+p=0$$
于是, 就得到了初始值p=\frac{2}{3}=0.67, \gamma=log(\frac{p}{1-p})=0.69, 模型的初识状态F_0(x)=0.69
- 循环生成决策树 这里回忆一下回归树的生成步骤, 其实有4小步, 第一就是计算负梯度值得到残差, 第二步是用回归树拟合残差, 第三步是计算叶子节点的输出值, 第四步是更新模型。 下面我们一一来看:
- 计算负梯度得到残差
- 使用回归树来拟合r_{im}, 这里的i表示样本哈,回归树的建立过程可以参考下面的链接文章,简单的说就是遍历每个特征, 每个特征下遍历每个取值, 计算分裂后两组数据的平方损失, 找到最小的那个划分节点。 假如我们产生的第2棵决策树如下:
- 对于每个叶子节点j, 计算最佳残差拟合值
- 更新模型F_m(x)
$$r_{i m}=-\left[\frac{\partial L\left(y_{i}, F\left(x_{i}\right)\right)}{\partial F\left(x_{i}\right)}\right]_{F(x)=F_{m-1}(x)}$$
此处使用m-1棵树的模型, 计算每个样本的残差r_{im}, 就是上面的y_i-pi, 于是例子中, 每个样本的残差:
$$\gamma_{j m}=\arg \min _{\gamma} \sum_{x \in R_{i j}} L\left(y_{i}, F_{m-1}\left(x_{i}\right)+\gamma\right)$$
意思是, 在刚构建的树m中, 找到每个节点j的输出\gamma_{jm}, 能使得该节点的loss最小。 那么我们看一下这个\gamma的求解方式, 这里非常的巧妙。 首先, 我们把损失函数写出来, 对于左边的第一个样本, 有
$$L\left(y_{1}, F_{m-1}\left(x_{1}\right)+\gamma\right)=-y_{1}\left(F_{m-1}\left(x_{1}\right)+\gamma\right)+\log \left(1+e^{F_{m-1}\left(x_{1}\right)+\gamma}\right)$$
这个式子就是上面推导的l, 因为我们要用回归树做分类, 所以这里把分类的预测概率转换成了对数几率回归的形式, 即log(\eta_i), 这个就是模型的回归输出值。而如果求这个损失的最小值, 我们要求导, 解出令损失最小的\gamma。 但是上面这个式子求导会很麻烦, 所以这里介绍了一个技巧就是使用二阶泰勒公式来近似表示该式, 再求导, 还记得伟大的泰勒吗?
$$f(x+\Delta x) \approx f(x)+\Delta x f^{\prime}(x)+\frac{1}{2} \Delta x^{2} f^{\prime \prime}(x)+O(\Delta x)$$
这里就相当于把L(y_1, F_{m-1}(x_1))当做常量f(x), \gamma作为变量\Delta x, 将f(x)二阶展开:
$$L\left(y_{1}, F_{m-1}\left(x_{1}\right)+\gamma\right) \approx L\left(y_{1}, F_{m-1}\left(x_{1}\right)\right)+L^{\prime}\left(y_{1}, F_{m-1}\left(x_{1}\right)\right) \gamma+\frac{1}{2} L^{\prime \prime}\left(y_{1}, F_{m-1}\left(x_{1}\right)\right) \gamma^{2}$$
这时候再求导就简单了
$$\frac{d L}{d \gamma}=L^{\prime}\left(y_{1}, F_{m-1}\left(x_{1}\right)\right)+L^{\prime \prime}\left(y_{1}, F_{m-1}\left(x_{1}\right)\right) \gamma$$
Loss最小的时候, 上面的式子等于0, 就可以得到\gamma:
$$\gamma_{11}=\frac{-L^{\prime}\left(y_{1}, F_{m-1}\left(x_{1}\right)\right)}{L^{\prime \prime}\left(y_{1}, F_{m-1}\left(x_{1}\right)\right)}$$
因为分子就是残差(上述已经求到了), 分母可以通过对残差求导,得到原损失函数的二阶导:
$$\begin{aligned}
L^{\prime \prime}\left(y_{1}, F(x)\right) &=\frac{d L^{\prime}}{d \log (\eta_1)} \\
&=\frac{d}{d \log (\eta_1)}\left[-y_{i}+\frac{e^{\log (\eta_1)}}{1+e^{\log (\eta_1)}}\right] \\
&=\frac{d}{d \log (\eta_1)}\left[e^{\log (\eta_1)}\left(1+e^{\log (\eta_1)}\right)^{-1}\right] \\
&=e^{\log (\eta_1)}\left(1+e^{\log (\eta_1)}\right)^{-1}-e^{2 \log (\eta_1)}\left(1+e^{\log (\eta_1)}\right)^{-2} \\
&=\frac{e^{\log (\eta_1)}}{\left(1+e^{\log (\eta_1)}\right)^{2}} \\
&=\frac{\eta_1}{(1+\eta_1)}\frac{1}{(1+\eta_1)} \\
&=p_1(1-p_1)
\end{aligned}$$
这时候, 就可以算出该节点的输出:
$$\gamma_{11}=\frac{r_{11}}{p_{10}\left(1-p_{10}\right)}=\frac{0.33}{0.67 \times 0.33}=1.49$$
这里的下面\gamma_{jm}表示第m棵树的第j个叶子节点。 接下来是右边节点的输出, 包含样本2和样本3, 同样使用二阶泰勒公式展开:
$$\begin{array}{l}
L\left(y_{2}, F_{m-1}\left(x_{2}\right)+\gamma\right)+L\left(y_{3}, F_{m-1}\left(x_{3}\right)+\gamma\right) \\
\approx L\left(y_{2}, F_{m-1}\left(x_{2}\right)\right)+L^{\prime}\left(y_{2}, F_{m-1}\left(x_{2}\right)\right) \gamma+\frac{1}{2} L^{\prime \prime}\left(y_{2}, F_{m-1}\left(x_{2}\right)\right) \gamma^{2} \\
+L\left(y_{3}, F_{m-1}\left(x_{3}\right)\right)+L^{\prime}\left(y_{3}, F_{m-1}\left(x_{3}\right)\right) \gamma+\frac{1}{2} L^{\prime \prime}\left(y_{3}, F_{m-1}\left(x_{3}\right)\right) \gamma^{2}
\end{array}$$
求导, 令其结果为0,就会得到, 第1棵树的第2个叶子节点的输出:
$$\begin{aligned}
\gamma_{21} &=\frac{-L^{\prime}\left(y_{2}, F_{m-1}\left(x_{2}\right)\right)-L^{\prime}\left(y_{3}, F_{m-1}\left(x_{3}\right)\right)}{L^{\prime \prime}\left(y_{2}, F_{m-1}\left(x_{2}\right)\right)+L^{\prime \prime}\left(y_{3}, F_{m-1}\left(x_{3}\right)\right)} \\
&=\frac{r_{21}+r_{31}}{p_{20}\left(1-p_{20}\right)+p_{30}\left(1-p_{30}\right)} \\
&=\frac{0.33-0.67}{0.67 \times 0.33+0.67 \times 0.33} \\
&=-0.77
\end{aligned}$$
可以看出, 对于任意叶子节点, 我们可以直接计算其输出值:
$$\gamma_{j m}=\frac{\sum_{i=1}^{R_{i j}} r_{i m}}{\sum_{i=1}^{R_{i j}} p_{i, m-1}\left(1-p_{i, m-1}\right)}$$
$$F_{m}(x)=F_{m-1}(x)+\nu \sum_{j=1}^{J_{m}} \gamma_{m}$$
这样, 通过多次循环迭代, 就可以得到一个比较强的学习器F_m(x)
下面分析一下GBDT的优缺点:
我们可以把树的生成过程理解成自动进行多维度的特征组合的过程,从根结点到叶子节点上的整个路径(多个特征值判断),才能最终决定一棵树的预测值, 另外,对于连续型特征的处理,GBDT 可以拆分出一个临界阈值,比如大于 0.027 走左子树,小于等于 0.027(或者 default 值)走右子树,这样很好的规避了人工离散化的问题。这样就非常轻松的解决了逻辑回归那里自动发现特征并进行有效组合的问题, 这也是GBDT的优势所在。
但是GBDT也会有一些局限性, 对于海量的 id 类特征,GBDT 由于树的深度和棵树限制(防止过拟合),不能有效的存储;另外海量特征在也会存在性能瓶颈,当 GBDT 的 one hot 特征大于 10 万维时,就必须做分布式的训练才能保证不爆内存。所以 GBDT 通常配合少量的反馈 CTR 特征来表达,这样虽然具有一定的范化能力,但是同时会有信息损失,对于头部资源不能有效的表达。
所以, 我们发现其实GBDT和LR的优缺点可以进行互补。
4.4 GBDT+LR模型
2014年, Facebook提出了一种利用GBDT自动进行特征筛选和组合, 进而生成新的离散特征向量, 再把该特征向量当做LR模型的输入, 来产生最后的预测结果, 这就是著名的GBDT+LR模型了。GBDT+LR 使用最广泛的场景是CTR点击率预估,即预测当给用户推送的广告会不会被用户点击。
有了上面的铺垫, 这个模型解释起来就比较容易了, 模型的总体结构长下面这样:
训练时,GBDT 建树的过程相当于自动进行的特征组合和离散化,然后从根结点到叶子节点的这条路径就可以看成是不同特征进行的特征组合,用叶子节点可以唯一的表示这条路径,并作为一个离散特征传入 LR 进行二次训练。
比如上图中, 有两棵树,x为一条输入样本,遍历两棵树后,x样本分别落到两颗树的叶子节点上,每个叶子节点对应LR一维特征,那么通过遍历树,就得到了该样本对应的所有LR特征。构造的新特征向量是取值0/1的。 比如左树有三个叶子节点,右树有两个叶子节点,最终的特征即为五维的向量。对于输入x,假设他落在左树第二个节点,编码[0,1,0],落在右树第二个节点则编码[0,1],所以整体的编码为[0,1,0,0,1],这类编码作为特征,输入到线性分类模型(LR or FM)中进行分类。
预测时,会先走 GBDT 的每棵树,得到某个叶子节点对应的一个离散特征(即一组特征组合),然后把该特征以 one-hot 形式传入 LR 进行线性加权预测。
这个方案应该比较简单了, 下面有几个关键的点我们需要了解:
- 通过GBDT进行特征组合之后得到的离散向量是和训练数据的原特征一块作为逻辑回归的输入, 而不仅仅全是这种离散特征
- 建树的时候用ensemble建树的原因就是一棵树的表达能力很弱,不足以表达多个有区分性的特征组合,多棵树的表达能力更强一些。GBDT每棵树都在学习前面棵树尚存的不足,迭代多少次就会生成多少棵树。
- RF也是多棵树,但从效果上有实践证明不如GBDT。且GBDT前面的树,特征分裂主要体现对多数样本有区分度的特征;后面的树,主要体现的是经过前N颗树,残差仍然较大的少数样本。优先选用在整体上有区分度的特征,再选用针对少数样本有区分度的特征,思路更加合理,这应该也是用GBDT的原因。
- 在CRT预估中, GBDT一般会建立两类树(非ID特征建一类, ID类特征建一类), AD,ID类特征在CTR预估中是非常重要的特征,直接将AD,ID作为feature进行建树不可行,故考虑为每个AD,ID建GBDT树。
- 非ID类树:不以细粒度的ID建树,此类树作为base,即便曝光少的广告、广告主,仍可以通过此类树得到有区分性的特征、特征组合
- ID类树:以细粒度 的ID建一类树,用于发现曝光充分的ID对应有区分性的特征、特征组合
5.关于本项目
5.1 赛题简介
此次比赛是新闻推荐场景下的用户行为预测挑战赛, 该赛题是以新闻APP中的新闻推荐为背景, 目的是要求我们根据用户历史浏览点击新闻文章的数据信息预测用户未来的点击行为, 即用户的最后一次点击的新闻文章, 这道赛题的设计初衷是引导大家了解推荐系统中的一些业务背景, 解决实际问题。
5.2 数据概况
该数据来自某新闻APP平台的用户交互数据,包括30万用户,近300万次点击,共36万多篇不同的新闻文章,同时每篇新闻文章有对应的embedding向量表示。为了保证比赛的公平性,从中抽取20万用户的点击日志数据作为训练集,5万用户的点击日志数据作为测试集A,5万用户的点击日志数据作为测试集B。具体数据表和参数, 大家可以参考赛题说明。下面说一下拿到这样的数据如何进行理解, 来有效的开展下一步的工作。
5.3 赛题理解
根据赛题简介,我们首先要明确我们此次比赛的目标: 根据用户历史浏览点击新闻的数据信息预测用户最后一次点击的新闻文章。从这个目标上看, 会发现此次比赛和我们之前遇到的普通的结构化比赛不太一样, 主要有两点:
- 首先是目标上, 要预测最后一次点击的新闻文章,也就是我们给用户推荐的是新闻文章, 并不是像之前那种预测一个数或者预测数据哪一类那样的问题
- 数据上, 通过给出的数据我们会发现, 这种数据也不是我们之前遇到的那种特征+标签的数据,而是基于了真实的业务场景, 拿到的用户的点击日志
所以拿到这个题目,我们的思考方向就是结合我们的目标,把该预测问题转成一个监督学习的问题(特征+标签),然后我们才能进行ML,DL等建模预测。那么我们自然而然的就应该在心里会有这么几个问题:如何转成一个监督学习问题呢? 转成一个什么样的监督学习问题呢? 我们能利用的特征又有哪些呢? 又有哪些模型可以尝试呢? 此次面对数万级别的文章推荐,我们又有哪些策略呢?
当然这些问题不会在我们刚看到赛题之后就一下出来答案, 但是只要有了问题之后, 我们就能想办法解决问题了, 比如上面的第二个问题,转成一个什么样的监督学习问题? 由于我们是预测用户最后一次点击的新闻文章,从36万篇文章中预测某一篇的话我们首先可能会想到这可能是一个多分类的问题(36万类里面选1), 但是如此庞大的分类问题, 我们做起来可能比较困难, 那么能不能转化一下? 既然是要预测最后一次点击的文章, 那么如果我们能预测出某个用户最后一次对于某一篇文章会进行点击的概率, 是不是就间接性的解决了这个问题呢?概率最大的那篇文章不就是用户最后一次可能点击的新闻文章吗? 这样就把原问题变成了一个点击率预测的问题(用户, 文章) --> 点击的概率(软分类), 而这个问题, 就是我们所熟悉的监督学习领域分类问题了, 这样我们后面建模的时候, 对于模型的选择就基本上有大致方向了,比如最简单的逻辑回归模型。
这样, 我们对于该赛题的解决方案应该有了一个大致的解决思路,要先转成一个分类问题来做, 而分类的标签就是用户是否会点击某篇文章,分类问题的特征中会有用户和文章,我们要训练一个分类模型, 对某用户最后一次点击某篇文章的概率进行预测。 那么又会有几个问题:如何转成监督学习问题? 训练集和测试集怎么制作? 我们又能利用哪些特征? 我们又可以尝试哪些模型? 面对36万篇文章, 20多万用户的推荐, 我们又有哪些策略来缩减问题的规模?如何进行最后的预测?
数据表
train_click_log.csv:训练集用户点击日志testA_click_log.csv:测试集用户点击日志articles.csv:新闻文章信息数据表articles_emb.csv:新闻文章embedding向量表示sample_submit.csv:提交样例文件字段表
5.4 算法流程概述(教程)
Baseline流程
多路召回
所谓的“多路召回”策略,就是指采用不同的策略、特征或简单模型,分别召回一部分候选集,然后把候选集混合在一起供后续排序模型使用,可以明显的看出,“多路召回策略”是在“计算速度”和“召回率”之间进行权衡的结果。其中,各种简单策略保证候选集的快速召回,从不同角度设计的策略保证召回率接近理想的状态,不至于损伤排序效果。如下图是多路召回的一个示意图,在多路召回中,每个策略之间毫不相关,所以一般可以写并发多线程同时进行,这样可以更加高效。
特征工程
我们先捋一下基于原始的给定数据, 有哪些特征可以直接利用:
- 文章的自身特征, category_id表示这文章的类型, created_at_ts表示文章建立的时间, 这个关系着文章的时效性, words_count是文章的字数, 一般字数太长我们不太喜欢点击, 也不排除有人就喜欢读长文。
- 文章的内容embedding特征, 这个召回的时候用过, 这里可以选择使用, 也可以选择不用, 也可以尝试其他类型的embedding特征, 比如W2V等
- 用户的设备特征信息
上面这些直接可以用的特征, 待做完特征工程之后, 直接就可以根据article_id或者是user_id把这些特征加入进去。 但是我们需要先基于召回的结果, 构造一些特征,然后制作标签,形成一个监督学习的数据集。
构造监督数据集的思路, 根据召回结果, 我们会得到一个{user_id: [可能点击的文章列表]}形式的字典。 那么我们就可以对于每个用户, 每篇可能点击的文章构造一个监督测试集, 比如对于用户user1, 假设得到的他的召回列表{user1: [item1, item2, item3]}, 我们就可以得到三行数据(user1, item1), (user1, item2), (user1, item3)的形式, 这就是监督测试集时候的前两列特征。
构造特征的思路是这样, 我们知道每个用户的点击文章是与其历史点击的文章信息是有很大关联的, 比如同一个主题, 相似等等。 所以特征构造这块很重要的一系列特征是要结合用户的历史点击文章信息。我们已经得到了每个用户及点击候选文章的两列的一个数据集, 而我们的目的是要预测最后一次点击的文章, 比较自然的一个思路就是和其最后几次点击的文章产生关系, 这样既考虑了其历史点击文章信息, 又得离最后一次点击较近,因为新闻很大的一个特点就是注重时效性。 往往用户的最后一次点击会和其最后几次点击有很大的关联。 所以我们就可以对于每个候选文章, 做出与最后几次点击相关的特征如下:
- 候选item与最后几次点击的相似性特征(embedding内积) --- 这个直接关联用户历史行为
- 候选item与最后几次点击的相似性特征的统计特征 --- 统计特征可以减少一些波动和异常
- 候选item与最后几次点击文章的字数差的特征 --- 可以通过字数看用户偏好
- 候选item与最后几次点击的文章建立的时间差特征 --- 时间差特征可以看出该用户对于文章的实时性的偏好
还需要考虑一下 5. 如果使用了youtube召回的话, 我们还可以制作用户与候选item的相似特征
当然, 上面只是提供了一种基于用户历史行为做特征工程的思路, 大家也可以思维风暴一下,尝试一些其他的特征。 下面我们就实现上面的这些特征的制作, 下面的逻辑是这样:
- 我们首先获得用户的最后一次点击操作和用户的历史点击, 这个基于我们的日志数据集做
- 基于用户的历史行为制作特征, 这个会用到用户的历史点击表, 最后的召回列表, 文章的信息表和embedding向量
- 制作标签, 形成最后的监督学习数据集
排序模型
通过召回的操作, 我们已经进行了问题规模的缩减, 对于每个用户, 选择出了N篇文章作为了候选集,并基于召回的候选集构建了与用户历史相关的特征,以及用户本身的属性特征,文章本省的属性特征,以及用户与文章之间的特征,下面就是使用机器学习模型来对构造好的特征进行学习,然后对测试集进行预测,得到测试集中的每个候选集用户点击的概率,返回点击概率最大的topk个文章,作为最终的结果。
排序阶段选择了三个比较有代表性的排序模型,它们分别是:
- LGB的排序模型
- LGB的分类模型
- 深度学习的分类模型DIN
得到了最终的排序模型输出的结果之后,还选择了两种比较经典的模型集成的方法:
- 输出结果加权融合
- Staking(将模型的输出结果再使用一个简单模型进行预测)
模型融合
加权融合:读取多个模型的交叉验证生成的结果文件,将多个模型输出的特征进行拼接。
def get_ensumble_predict_topk(rank_model, topk=5):
Staking: 定义一个逻辑回归模型再次拟合交叉验证产生的特征对测试集进行预测
finall_trn_ranker_feats = trn_lgb_ranker_feats[['user_id', 'click_article_id', 'label']] finall_tst_ranker_feats = tst_lgb_ranker_feats[['user_id', 'click_article_id']] for idx, trn_model in enumerate([trn_lgb_ranker_feats, trn_lgb_cls_feats, trn_din_cls_feats]): for feat in [ 'pred_score', 'pred_rank']: col_name = feat + '_' + str(idx) finall_trn_ranker_feats[col_name] = trn_model[feat] for idx, tst_model in enumerate([tst_lgb_ranker_feats, tst_lgb_cls_feats, tst_din_cls_feats]): for feat in [ 'pred_score', 'pred_rank']: col_name = feat + '_' + str(idx) finall_tst_ranker_feats[col_name] = tst_model[feat]
6.第二名的算法
作者自述
采用3种召回方式:itemcf 召回,binetwork 召回和基于 word2vec 的 i2i 召回。合并去重并删除没有召回到真实商品的用户数据后,利用特征工程+ LGB 二分类模型进行排序。
方案总结
为了构造稳定的线下验证,从训练集用户中随机采样5w个用户作为作为线下验证集用户,将验证集用户的最后一次点击记录从原训练集的点击日志中剔除。合并这时候的训练集点击日志和测试集点击日志作为总的历史点击记录,预测验证集用户的最后一次点击作为线下验证,实验证明该验证方式很稳定,和线上指标同增同减且相差不大。
召回
本方案采用了3路召回,通过不同策略的差异性提高整体新闻的召回率。新闻推荐有强热点依赖,用户往往会短时间内点击许多相关新闻,所以需要从历史点击序列中挖掘新闻之间的关系信息。
改进版 itemcf
原始的 itemcf 将用户点击过的新闻看做一个无序的集合,但在实际应用中,应该考虑点击次序带来的影响。在计算同一序列中两个新闻的相似度时,不仅需要考虑共现次数,也需要考虑两个新闻之间的次序关系。同一点击序列中两个新闻位置越远,相关性应该减小。新闻对顺序和逆序的权重也不同,在点击序列A,B,C中,”BC”这样的正序权重应该大于”BA”这样的逆序权重。更多关于 itemcf 的改进可以看之前 KDD CUP 的相关方案总结。
日志中所有出现过的新闻只有3w多个,而整个新闻库中却有30多万。用户可能会点击一些没有在日志数据中出现的新闻。从线下验证集来看,大概有12%待预测新闻没有出现在历史点击记录里。为了缓解冷启动问题,在计算新闻相似度的时候可以考虑加入内容相似度。但是基于数据集给的新闻 embedding 向量表示做向量召回的效果很差劲,所以最后在 itemcf 中没有引入新闻的内容相似度。
建立新闻的相似度关系后,进入到召回阶段,根据用户的历史点击新闻,结合相似度选择 TOP100 关联新闻。选取关联新闻时,除了考虑和历史点击新闻的相似度,还要加入位置距离衰减。新闻点击是强热点相关,所以历史点击新闻对下一次点击预测的影响传播不会太远。在实际测试中,利用所有历史点击新闻做召回,hitrate_5 指标只有0.20,限定只用最近点击的两个新闻来做召回的话,可以大幅提升至0.33。
基于网络关系的召回
该方法源自”Bipartite network projection and personal recommendation”,代码采用自论坛开源。该方法也分为两个阶段,新闻相似度计算和基于用户点击历史的新闻召回。在相似度计算阶段,通过用户将两个新闻连接起来。计算相似度时考虑两种因素:1)两个新闻的共同被点击用户过多,则相似度减少;2)共同被点击用户的点击新闻过多,相似度也要减少。基于用户点击历史的新闻召回和 itemcf 类似,不再赘述。类似地,在召回时只利用了最新一次点击的新闻做召回,相较于利用全量历史记录召回,效果提高了10%。
w2v 向量召回
除了使用人工规则从序列中提取相似度,我们还可以使用序列学习模型 Word2Vec 为新闻学习向量表示。将用户的新闻点击序列作为句子喂到 Word2Vec 模型,然后选取和用户最近点击新闻最相似的关联新闻。向量学习和寻找相似全部使用 gensim 库的 Word2Vec 实现。
# 模型训练 model = Word2Vec(sentences=sentences, size=256, window=3, min_count=1, sg=1, hs=0, seed=seed, negative=5, workers=10, iter=1)
召回合并
多路召回之间肯定存在重复召回的情况,这也是为什么召回策略讲究差异性,其实就是为了减少重复召回的数量。利用重复召回的新闻数量占该路召回数量之比做相似度参考,3路召回的相似度见热力图。
重复被召回的新闻在多个召回方式中的得分是不一样的,得分是排序时的强特,需要进行得分合并。对各个召回结果,以用户为单位进行最大最小归一化。分数合并测试了sum,mean和max,效果对比见下表。max丢失的消息较多,mean对重复次数多的新闻不公平。
去重之后,平均每个用户召回到153个新闻。删除没有召回到真实点击的验证集用户,减少了无用负样本的数量,也提高了后续排序模型的效果,但是在计算线下指标的时候这部分用户的数量要包括进去,否则指标会虚高。合并前后各召回方式指标如下表所示:
排序
本方案把排序建模为二分类任务,分为特征工程和二分类模型预测两部分。特征工程主要围绕召回策略进行,保证实时性召回的新闻能在排序阶段被推出去。模型采用 LGB 模型。
特征工程
特征工程分为3类:新闻特征,用户特征,用户-新闻交互特征。数据集本身给出的属性信息较少,所以特征工程主要围绕交互属性展开。
新闻特征包括:
- 新闻字数
- 新闻创建时间
- 新闻被阅读数量
用户特征包括:
- 用户点击新闻的创建时间差的平均值
- 用户点击新闻的点击时间差的平均值
- 用户点击新闻的点击-创建时间差的统计值:mean,std
- 用户点击新闻的 click_datetime_hour 统计值
- 用户点击新闻的字数统计值
- 用户点击新闻的创建时间统计值
- 用户点击新闻的点击时间统计值
- 用户新闻阅读数量
- 用户某种类新闻阅读数量
交互特征主要基于之前的召回策略进行,通过保存召回阶段的新闻相似度信息或向量,我们能够间接或直接得到用户对待预测新闻的评分。基于 itemcf, 网络关系和 w2v 的召回得到的只是新闻之间的相似度,需要和用户的历史点击新闻计算间接得到用户-新闻评分,采用如下方式:
- 待预测新闻和用户所有历史点击新闻相似度按次序加权求和
- 待预测新闻和用户最近一次点击新闻相似度
模型
模型采用单模 LGB 进行预测,特征重要性见下表。排序之后,hitrate_5 涨到0.4394406080778976,mrr_5 涨到0.26552279464123835。
算法流程
data.py
data_offline:随机采样出一部分样本,抽出行为数据最后一条作为线下验证集
data_online:
read_all_raw_data
get_item_dt_groupby_user
7.第三名的算法
作者自述
现象1:训练集和测试集中存在大量前后两次点击差值为30000的数据
现象2:训练集每个用户最后两次点击差值皆为30000
现象3:测试集每个用户最后两次点击差皆不为30000
题意分析
对于这道竞赛题,我们需要推荐的新闻是已经发生的事实,也就是该新闻App自己的推荐系统得出的,给用户的结果。
而这固定的30000时间差(30秒),我觉得很有可能是用户长期阅读新闻到达一定的阈值后,新闻App自动推送了下一篇新闻。
训练集中最后一次点击因为和前一次点击相差30秒,所以可以认为全部是新闻App推送的新闻,而非用户自己点击的。
作为佐证,测试集中(包括之后的测试集B),最后一次点击应该都是用户点击的,并且想要我们预测接下来新闻App会推送哪篇新闻。
制作标签
通过上述分析之后,我采取了和baseline不一样的标签设置方法。
baseline中统一选取了每个用户最后一次点击作为标签,而我选取了每个用户最后一次新闻App推送的文章作为标签。
例如,某个用户的点击顺序是X1, A, B, X2, X3 (其中A代表用户点击的文章,B代表新闻App推送的文章,X{n}代表其他没有30秒点击差的文章)
baseline会选择
X3作为标签,[X1, A, B, X2]作为训练数据,而我会选择B作为标签,[X1, A]作为训练数据如有某个用户有且仅有一次点击,那么他不会进入我的训练集中。(因为数据量过大,我缩小了训练范围,在原生的训练集和测试集中随机选取了10万名用户作为新的训练集。)
协同过滤策略
还是基于上述分析的理由,我采用了一种新的协同过滤方式:仅对上述点击A和点击B这种文章序列构成进行协同计算。
算法其实挺简单的,遍历所有点击A点击B的构成,将文章A->文章B出现的次数进行累加。(文章B->文章A这样倒过来的点击顺序同样也算在内,因为少部分用户有ABBA这样的构成)
召回的时候哪篇文章值大就召回哪个。(也就是对于文章A,新闻App曾经推荐过的历史中,哪篇文章推荐次数最多,召回排位中就排第一)
通过这样的协同过滤策略所得到的效果要略优于baseline的策略,而两者同时使用的情况下,精度没有得到提升。(因为重叠部分多,召回率没提高多少,但召回的件数却增加了。)
最后协同过滤部分我只使用了上述的策略。
其他召回策略
emb向量我尝试过,但召回率非常低,所以就放弃了。
热门文章因为是和协同过滤是两个维度,所以热门文章的策略我也用了。
点击时刻与文章投稿时刻
如果将训练集最后一次点击的点击时刻和点击文章的投稿时刻做差值,然后把这些差值图表化后观察它们的分布,就可以发现85%左右的数据都集中在3小时 ~ 27小时之间。0 ~ 2小时是一个攀升期,3小时到达顶峰开始下滑,但超过27小时后突然断崖式下跌。
我猜这可能是新闻App的推荐策略所致,新闻具有非常强的时效性,而新闻投稿后3小时可能是用户增长的一个爆发点,而27小时之后新闻App可能内部权重调整,使其推荐可能性变得更低了。
利用这个信息,我对上述的文章A与文章B的协同过滤召回,以及热门文章召回,都使用了时间限定来提高召回率。
- 协同过滤召回的范围为用户最后一次点击时刻之前的0~27小时内的文章
- 热门文章召回的范围为用户最后一次点击时刻之前的3~27小时内的文章
通过这个方法,召回率大概可以提升10~15%
召回件数
协同过滤我召回了25篇,热门文章我召回了10篇,按照我最后一次线下验证的结果,协同过滤的召回率为
65.78%,热门文章的召回率为40.15%,两个合并后总召回率为68.80%。我也尝试调整召回的篇数,要么持平要么下跌,最终还是选择了以上两个数值。
特征工程
特征工程我做得比较简单,除了文章的字数,分类等本来就有的特征以外,我还生成了以下的特征:
- 用户看过的文章的平均字数
- 用户看过的文章的最小字数
- 用户看过的文章的最大字数
- 最后两次点击的间隔,若不存在设为nan
- 最后两次点击的文章的投稿间隔,若不存在设为nan
- 最后一篇文章和平均字数的差值
- 最后一次点击时刻与文章投稿时刻的差值
数据分割
线下验证时,训练集被我分为8:1:1,8份训练,1份用于验证集防止过拟合,剩下1份不参与训练,仅仅通过预测后数值来检验MRR和Hit率,是否和参加验证集的那份比较相近。
线上预测时,训练集被我分为8:2,8份训练,2份用于验证集。模型训练完毕后即对测试集进行预测。
LightGBM的超参调整
这部分我也没有做太多的事情,只是增加了迭代次数至1000次,早停为50。
其他
- 计算相似度的地方,由于本来所花时间有点长,所以使用了multiprocessing库来做进程并行处理。
- 有些每次生成需要花大量时间(比如字典)的对象,生成完毕后保存成pkl文件,下次文件存在的话就直接读取文件。