推荐系统不相信眼泪,但此算法会给你些安慰

版权声明:本文为原创文章,未经允许不得转载!

众所周知,解决信息过载的方式主要有类目导航、搜索、推荐,还有目前大热的聊天机器人(chatbot),但其本质也是基于推荐系统和知识图谱实现的。推荐不同于或者优于搜索的地方在于:搜索需要用户知道自己需要什么,而推荐则可以做到帮助用户发现自己需要什么或者让你需要的信息主动找到你,而且更加个性化,甚至能做到“比你自己更了解你自己”。

本文介绍的算法侧重解决实际58赶集招聘推荐场景中遇到的问题,针对大数据离线和实时推荐在海量、精准、实时等方面的挑战,提出了分布式的离线加实时增量更新的协同过滤推荐算法,使推荐效果得到了明显的提升。该方法已应用在包括职位、简历等多个业务和十几个推荐场景中,推荐效果均得到显著提升。基于该算法的推荐系统现每天处理百亿条用户行为日志,支撑千万级用户请求。

本文的组织逻辑如下:首先,简单介绍58赶集招聘业务线现在的推荐系统现状;然后,详细讲述本文基于招聘推荐场景实现的离线加实时的Item based CF算法分布式设计与实现;其次,给出了实际线上AB实验的推荐效果分析;最后,简述了该算法的未来可优化点。

1.1 招聘推荐系统简介

招聘的推荐系统主要由数据收集模块、数据存储模块、召回模块、过滤模块、排序模块和推荐解释模块组成,详见图1,其中红色圈住的部分为推荐系统的核心。另外,关于招聘推荐系统的详细介绍可参考[1]。本文提出的算法目前主要用于召回模块,该部分主要功能为从海量的原始数据集中筛选出符合当前推荐场景的候选集,主要评价指标包括候选集的覆盖度和准确性等。常见的召回策略大体可分为三类:个性化召回策略、基于群体行为的召回策略、全局召回策略(如热门召回等)。其中,本文优化的Item based CF算法属于基于群体行为的召回策略。

没有基于实际推荐场景和数据特点做出的算法都是空中楼阁。所以,在介绍算法之前,首先介绍58招聘业务线的数据特点。

image
图 1

1.1.1 招聘推荐场景

招聘业务线目前在三端(pc、m、app)上线的推荐位大概有40多个,由推荐引入的流量已占总流量的近20%。简单来讲,就是给职位推荐相关度高的职位,向简历推荐相关度高的简历。其中,职位的主要推荐场景包括职位列表页、职位详情页、职位发布成功页等场景,简历的主要推荐场景包括简历列表页、简历详情页、个人中心等场景。

1.1.2 招聘数据特点

58招聘数据无论是职位还是简历(后文统称为Item),都没有企业用户或者普通用户(统称为User)对他们的评分数据,即非missing值均为1,但是拥有User对Item的各类行为操作数据,可以将User对Item的操作行为漏斗深度看作为该User对该Item的偏好层度,但在某种层度上也可以理解为评分。所以,本文介绍的Item based CF算法是基于用户多行为的,这也是本算法的核心优化点,后文会做详细介绍。基于用户多行为的策略,不但解决了上述非missing值为1的情况,也实现了对用户行为偏好更为精准的刻画(用户对Item操作的行为漏斗越深,代表用户偏好度越高)。下面详细介绍一下简历和职位的行为操作漏斗。

针对简历,企业用户(招聘者) 的主要行为漏斗为:浏览简历详情页行为、投递打开行为、点击查看联系方式行为、下载简历行为和发送面试邀请行为。在设置这5种行为的行为权重时也是按此先后顺序递增的。

针对职位(帖子),普通用户(求职者)的主要行为漏斗为:浏览详情页行为 、点击查看企业详情行为、点击查看电话行为 、IM沟通行为、电话沟通行为、点击地理位置行为、职位申请行为、简历投递行为和面邀回复行为。这些行为的行为权重也是按此先后顺序递增的。

1.2 Item based CF算法的离线与实时的分布式设计实现

简单来讲,传统的推荐算法可分为:基于内容或标签的推荐算法(CB)、热门推荐算法、协同过滤推荐算法(CF)、矩阵分解推荐算法(MF)。其中,基于内容的推荐算法是基于算法设计者对业务的认知,利用一些分类器,制定一些规则做出的推荐,如同薪资、同地域、同行业等,该类算法实现简单,但是由个人或者team建立的强推荐标准未必能完全代表整个平台用户的行为喜好,所以推荐效果相对有限,因此该类算法常作为补足逻辑用于解决推荐系统的冷启动等问题。热门推荐算法则可用于丰富推荐结果,同时也可做为其他算法的补足逻辑以解决冷启动等相关问题。常见的MF算法包括经典的SVD(以及降低SVD计算复杂度的RSVD、SVD++等算法)、ALS等,但如果你的User-Item矩阵的非missing都为1而非打分值,则SVD就不不好使了,此时可以尝试用Topic Model,如LDA(以及为了获得更多topic的LDA的变种SparseLDA、AliasLDA、LightLDA等)、pLSA等。

当然,除了上面提到的这些传统的算法外,Google、Netflix和Linkedin等公司也在尝试将Deep Leaning算法应用到推荐领域,并取得了一定效果,例如,将word2vec、LSTM等用于处理推荐需要的文本数据,[2]论文做的音乐推荐,[3]论文用于YouTube的视频推荐,以及将受限波兹曼机(RBM)实现CF的Netflix Prize竞赛[4]等。

相较上述眼花缭乱的推荐算法,本文设计实现的算法是基于内容算法和基于Item based CF算法的混合算法,因为这两类算法的工程实现难度相对较易,易于维护,同时也是被工业界验证了的性价比最高的算法之一,而且这两类算法在某种程度上有一定互补[5]。当然,本算法也可以看作是Item based CF的优化算法,而且下文也是按照这个思路做展开介绍的。

众所周知,CF算法(更准确的说是memory-based CF)主要分为两步:1.计算Item间的相似度,2.预测User对Item的打分。此处需要着重强调的是,本文优化的Item based CF不涉及第2步,因为它不是针对具体用户的个性化推荐策略,而是基于所有用户行为的群体性推荐策略。

image
图 2

其中,本文优化的Item based CF的整体算法框架,如下图2所示。该算法实现框架,主要可分为行为接入模块、数据预处理模块、算法模块、数据融合模块(黄色模块)和存储模块(蓝色模块)。

各功能模块具体功能为:行为接入模块主要负责解析各类实时和离线的行为日志;数据预处理模块主要负责数据补全等工作,算法模块用于实现各类推荐算法;数据融合模块主要是将离线和实时生成的推荐结果做融合等工作;存储模块用于将推荐结果存入数据库,供线上服务调用。

下面将详细从基于招聘推荐场景的Item based CF算法优化和工程设计优化两个方面做详细介绍。

1.2.1 基于招聘推荐场景Item based CF算法优化

本节主要从Item based CF与User based CF的算法选择、Item画像设计、Item间相似度计算优化以及实时Item based CF算法的数据实时补足策略设计四个方面做详细介绍。

1. Item based CF与User based CF的算法选择

简单介绍一下为什么使用Item based CF,而不是User based CF,这是由招聘数据的特点决定的。58的招聘业务本质上属于工具类的生活服务,这造成了它的标签体系相对较弱,即其ID主要为低级ID(cookie和IMEI等),高级ID(如手机号、userid)的比例相对较小(当然后续可通过IDMapping一定程度上解决这个问题),所以就造成了算法使用的每个ID可能并非对应真实世界中的每个用户;再者,招聘的业务场景决定了每个ID的生命周期或活跃周期也有限,大概是一到两周左右的时间。基于上述两点,每个ID(对应CF算法的User)未必能真实的刻画某个特定真实用户,而且部门之前的业务实践也充分说明了这一点。所以本文采用基于Item based CF做推荐优化工作。

2. Item画像设计

Item画像设计遵循的原则为提炼出那些易于区分不同Item的显著性特征或标签。大致分为三类:基本属性标签、意向标签和活跃度标签。下面分别以简历和职位做详细介绍。

  • 简历:
    • 基本属性标签:简历ID、求职者性别、求职者年龄、求职者学历、求职者工作经验、婚姻情况、居住城市、手机认证情况、亮点个数、投递来源等。
    • 意向标签:简历的期望职位列表、求职地区列表等。
    • 活跃度标签:简历更新时间、简历完成度、期望薪资、主动申请职位个数、收到面试职位个数、简历被下载次数、简历被浏览次数、上传图片个数等。
  • 职位:
    • 基本属性标签:职位ID、帖子类别、职位类别、工作地址、薪资待遇、tags(用户自定义,如福利待遇等)、招聘人数、公司属性(行业、性质、规模、是否认证等)等。
    • 意向标签:学历要求、工作年限、技能要求、用户自定义等。
    • 活跃度标签:帖子更新时间、帖子浏览量等。

基于Item画像的相似度计算是Item间相似度计算中至关重要的一部分,也是增加Item间区分度的有效手段(因为无论是简历还是职位,同质化都比较严重),同时也通过这种方式将CB算法融入到了CF中。

注意:为了便于算法实现,在每个Item画像中还加入了行为类别标签(behavior type)、行为权重标签(behavior weight)和时间戳标签(timestamp)。

3. Item间相似度计算优化

这是Item based CF的核心,也是本文对其做优化的重点。具体如下:
1)假设用户\(u\)同时对Item \(ip_m\)和\(ip_n\)有操作行为,则基于用户\(u\)的\(ip_m\)和\(ip_n\)之间的相似度距离为:

\[d_{u}(ip_m,ip_n)=\sum_{p\in P}d_p(ip_{m}^{P},ip_{n}^{P})\cdot d_B
(ip_{m}^{B},ip_{n}^{B})\cdot d_T(ip_{m}^{T},ip_{n}^{T})\cdot d^u_{penalization},\]

如上式所示,该距离由四部分组成:
(1)基于Item画像的相似度距离\(d_p(ip_{m}^{P},ip_{n}^{P})\),其中\(P\)代表Item画像中的所有标签集合,以简历期望职位列表为例,计算公式如下:
\[d_p(ip_{m}^{t},ip_{n}^{t})=\frac{\left | ip_{m}^{t} \bigcap ip_{n}^{t} \right | + c}{\sqrt{\left | ip_{m}^{t} \right | \cdot \left | ip_{n}^{t} \right |}},\]
其中,\(ip_{m}^{t}\)和\(ip_{n}^{t}\)分别代表\(ip_{m}\)和\(ip_{n}\)的期望职位列表,\(\left | ip_{m}^{t} \right |\)和\(\left | ip_{n}^{t} \right |\)分别代表\(ip_{m}\)和\(ip_{n}\)的期望职位列表中期望职位的个数,常量\(c\)为了实现推荐结果多样性设计的,即当两简历的期望职位列表交集为空时,仍可以将其加入推荐的候选集合,注意\(c\)不宜设置过大。
(2)基于\(u\)对\(ip_{m}\)和\(ip_{n}\)的操作行为列表和的相似距离:
\[d_B(ip_{m}^{B},ip_{n}^{B})=\left\{\begin{matrix}
min\begin{Bmatrix}
min\begin{Bmatrix}
ip_m^B
\end{Bmatrix}, &
min\begin{Bmatrix}
ip_n^B
\end{Bmatrix}
\end{Bmatrix} ,& \left | ip_m^B \cap ip_n^B \right |=0 \\
\left | ip_m^B \cap ip_n^B \right |, & \left | ip_m^B \cap ip_n^B \right |\neq 0
\end{matrix}\right.,\]

其中,当\(ip_{m}^{B}\)和\(ip_{n}^{B}\)的交集为空时,不宜将\(d_B(ip_{m}^{B},ip_{n}^{B})\)置为0。以简历推荐为例,通过分析日志,发现存在大量类似如下case:\(u\)对\(ip_{m}\)只有简历浏览行为,而对\(ip_{n}\)却只有简历下载行为,而显然\(u\)对\(ip_{m}\)和\(ip_{n}\)都是有兴趣的,不应将它们间的相似度距离置为0,本文采取的方式是取\(ip_{m}^{B}\)和\(ip_{n}^{B}\)的操作行为列表中的所有行为权重的最小值。
(3)加入时间衰减因子:
\[d_T(ip_{m}^{T},ip_{n}^{T})=N_0 e^{-\lambda (t_{now}-t_{ip_{n}^{T}})}, \]
此时需要注意的是当为\(p_{m}\)推荐\(p_{n}\)时,只需要计算\(ip_{n}^{T}\)与当前时间的距离,反之亦然。加入该因子背后的逻辑为提升近期对Item操作行为的权重,同时降低旧的对Item操作行为的权重。
(4)加入惩罚项:
\[d^u_{penalization}=\frac{1}{{log_{a}}^{N_u+c}}, \]
其中,\(N_u\)表示用户\(u\)操作Item的个数,包括所有操作行为。背后的原理是用户\(u\)操作Item的个数越多,则其操作的Item间的相关性越弱。常量\(c\)的加入一是为了防止分母为0,二是为了更好的调节惩罚力度。
2)将所有\(u\)(\(u\in U\),\(U\)代表所有User的集合)的操作行为累加在一起,得到最终的Item间的相似度距离:
\[d_{item}(ip_m,ip_n)=\sum_{u\in U}d_{u}(ip_m,ip_n). \]

4. 实时Item based CF算法的数据实时补足策略设计

本文除了实现了天粒度的离线分布式Item based CF算法,还设计了实时(或近实时)的秒粒度的算法实现,这部分工作主要是基于两方面的考虑:一是提升当天新增Item的推荐效果;二是越近期的Item操作行为,越能准确表达当前该Item的推荐价值。

但是实时计算不同于离线计算,它需要在分钟级别或者是秒级别执行完成整个推荐算法流程,所以它不可能将全量的离线数据导入后,与实时数据结合后,重新执行一遍,所以需要对离线算法逻辑做简化。其中,简化的思路无非两种:一是对User-Item矩阵做列扩充(补足),二是对User-Item矩阵做行扩充。

1)列扩充:通过特定User,查询并获取实时维护的该user历史操作Item的列表,以实现了对user的列扩充。此方法可消除了新增Item与老Items的数据断层,解决了Item间的关联广度不可拆分性;但同时需要消除老Items之间的关联对离线结果的影响,因为该User 在实时计算的这段时间里根本没有访问老Items。

2)行扩充:通过特定Item,查询并获取实时维护的访问该Item的User列表,实现了行扩充,但它带来的深度关联和广度关联,都是可拆解的,即实时和离线部分可分别计算,不存在数据断层现象,所以扩充意义不如列扩充。

基于上述两点的分析,本文采用的是列扩充的方式。为了实现列扩充,就需要实时维护当前每个User最新操作的Item列表,此时就需要与数据库做大量交互操作,经过长期是实践表明,这部分操作是实时Item based CF算法优化的主要工程瓶颈之一。

实时算法需要解决另一个关键问题为,如何对实时相关Item列表和离线相关Item列表做merge。主要有两种解决方式:首先,最简单的就是不做merge,当线上读取时,离线和实时各自分配一定的比例即可;其次,可在每次与离线数据融合时,对离线数据的similarity distance先做时间衰减再排序。

在介绍工程实现优化之前,首先需要说明:本文介绍的算法是基于Spark和Spark streaming,用scala语言开发实现的;离线和实时的推荐算法结果均采用redis做存储。

1.2.2 工程实现优化

本节主要介绍三部分内容:Item based CF算法分布式实现优化、redis存储优化和Spark streaming的性能调优(该部分内容相对较为繁杂,本篇文章暂不做介绍)。

1. Item based CF算法分布式实现优化

基于上述User-Item矩阵,若要实现两两Item之间的相似度距离计算,最简单的方式就是做笛卡尔积,Spark的RDD也恰好有这个算子(cartesian),但若使用这种方式,即使不考虑级别的计算复杂度,百万级别的Item做笛卡尔积后的相似度矩阵结果也是TB级别的存储量,显然是不可取的。当然,有人会说,在某些业务场景下,不需要做两两Item之间的相似度距离计算,只需要计算出最相近的top N,即KNN问题,此时就可以通过类似局部敏感哈希(LSH)的方式做ANN(approximate nearest neighbor,即近似KNN),如MinHash、SimHash等,这种方式是降低了计算复杂度,但需要先对每个Item向量做哈希化(矩阵乘法),增加了工程实现难度,尤其是在大数据场景下。所以,此时一种比较常用的方式是通过join实现,也是MR框架下常用的实现方式,简述如下:

1. left join <cookieId, infoList> 和 <cookieId, info> 得到 <cookieId, info, infoList>,如下:
(c1 i_j i_11,i_12,..., i_j, ...,i_1n)
(c2 i_j i_21,i_22,..., i_j, ...,i_2n)
...
(cm i_j i_m1,i_m2,..., i_j, ...,i_mn)
then reduceByKey(i_j)

2. reduceByKey(i12)

这种方式可以实现,但会造成大量是数据冗余,增加shuffle的数据量,甚至造成离线定时任务不稳定等问题,所以改进后的算法步骤如下:

1. 将每个<cookieId, infoList>的infoList 中的每个info做两两结合,如下:
(c1 (i11, i12), (i11, i13), ..., (i11, i1n), ..., (i1n-1, i1n))
(c2 (i21, i22), (i21, i23), ..., (i21, i2n), ..., (i2n-1, i2n))
...
(cn (in1, in2), (in1, in3), ..., (in1, inn), ..., (inn-1, inn))

2. reduceByKey(in +","+ im)

2. redis存储优化

1) 数据压缩
采用数据压缩的主要有三个原因:一是公司系统运维部为了便于维护redis等原因要求单个redis实例大小不能超过20G,这个存储量不符合我们的业务要求;二是为了降低与数据库交互时的读写带宽,提高读写效率;三是为了节省公司存储资源。
2) 分布式异步存储策略

  • 分布式存储:采用分布式存储的主要原因为:由于职位生成的离线数据较大,即使在序列化压缩后也是远远超过单个redis实例的大小,所以需要采用分布式存储。此时可采用两种方式:一是redis cluster模式,二是redis shard模式。虽然redis cluster模式扩展性更好,但其不支持pipeline,所以综合考虑之下,采用了redis shard模式。
  • 异步存储:如1.2.1节第4部分所述,实时Item based CF算法实现需要实时维护一个User-Item矩阵,其中Item为该User最近三天的有操作行为的前200个Item。此时若每来一条新的User-ItemList,就做一次redis读写操作,对Spark streaming这种实时计算来讲无疑是无法承受的,所以本文设计一个缓存数组,以实现采用异步的方式批量写入redis。

经过上述工程及算法优化,目前针对每天千万级的活跃User-Item矩阵生成离线相关Item列表只需要1个小时左右的时间,实时部分也基本实现了秒粒度更新维护的稳定服务。

1.3 线上AB实验评估

本文主要采用的评价指标包括点击量、点击率和投递量、投递率等。算法效果简述如下:

1. 简历推荐方面(原算法为基于简历下载行为的CF算法):

  • 离线:PC简历详情页推荐点击率提升12%;
  • 实时:PC简历详情页推荐点击率提升3%左右。该部分经过后续优化,效果略有提升。

2. 职位推荐方面(原算法为基于职位浏览或申请行为的CF算法)

  • 离线:PC简历详情页推荐点击率和投递率提升20%以上;
  • m简历详情页推荐点击率和投递率提升8%左右。

1.4 算法后续可优化点

  1. 优化实时Item based CF算法的设计策略[6],因为目前AB实验表明,实时算法部分效果提升有限。
  2. 降低热门Item的权重:热门简历,可能下载量或者浏览量已经很多,甚至可能已经找到工作,所以需要降权;热门职位,简历的投递量也已经很多,也可适当降权。
  3. 尝试进一步提高拥有更深行为漏斗Item的权重,以提升投递率等二跳转化率指标。
  4. 尝试Model based CF和RBM for CF。
  5. 引入Idmapping,获取更真实有效的UUID;进而可进一步尝试User based CF。
  6. 尝试将Word2Vec等算法用于Item画像间的相似度计算。
  7. 程序性能和可扩展性优化。

版权声明:本文为原创文章,未经允许不得转载!

参考文献:
[1] 感觉身体被掏空?推荐实战之框架实现(58招聘技术团队公众号往期文章)
[2] Hamel P, Lemieux S, Bengio Y, et al. Temporal Pooling and Multiscale Learning for Automatic Annotation and Ranking of Music Audio.[J]. Ismir, 2011:729-734.
[3] Covington P, Adams J, Sargin E. Deep Neural Networks for YouTube Recommendations[C]// ACM Conference on Recommender Systems. ACM, 2016:191-198.
[4] Salakhutdinov R, Mnih A, Hinton G. Restricted Boltzmann machines for collaborative filtering[C]// Machine Learning, Proceedings of the Twenty-Fourth International Conference. 2007:791-798.
[5] Amatriain X. The recommender problem revisited[C]// The, ACM SIGKDD International Conference. 2014:1971-1971.
[6] Huang Y, Cui B, Zhang W, et al. TencentRec: Real-time Stream Recommendation in Practice[J]. 2015:227-238.

4 thoughts on “推荐系统不相信眼泪,但此算法会给你些安慰

发表评论

电子邮件地址不会被公开。 必填项已用*标注