作者 | HCY崇远
01 前言
本文源自于前阵子连续更新的推荐系统系列,前段时间给朋友整理一个关于推荐系统相关的知识教学体系,刚好自身业务中,预计明年初随着业务规模增长,估摸着又要启动推荐相关的项目了,所以也是趁机把相关的知识结构梳理了一遍。这这里重新做整理,并额外做了一些增减,让整体逻辑会更通顺一点。
整个文章的结构逻辑,先从推荐系统的基础知识结构讲起,然后由浅入深过渡到几个推荐策略算法上,并且为每个推荐策略算法提供一些简单的入门Spark案例代码,再从策略过渡到系统层级,包括数据架构、策略组合、效果评估等,最终再从上层产品设计的角度去补充整个系统知识结构。
整体来看,通篇并没有涉及到特别高深的推荐算法(部分专门讲这部分的文章,只有有一定基础的朋友才更容易接受,本文章的逻辑略有不同),大多都是常规的策略模型,核心在于对整个推荐系统的知识结构进行解构,让那些对于推荐系统感兴趣的朋友能快速的建立起对于推荐系统的知识体系结构,甚至能够通过文章中的算法案例,做简单的实践,从而达到快速入门的目的。
友情提示:据不完全统计,包含代码片,通篇大约接近1万字,预计阅读时间?我也不知道,哈哈,可以当成短篇小说来读了。
02 推荐系统场景需求
本章节从场景的需求出发,试图来分析推荐系统需求的必须性,以及很多平台言必谈个性化推荐的现状。
0.2.1 先从人工智能话题出发
说推荐系统之前先掰掰人工智能,这个词估计大家能能听得懂,毕竟是风口上的名词,想没听过也难。那么问题来了,你觉得推荐系统与人工智能有什么关系?
或许大半的人会认为没有半毛钱的关系,这让我想到了前几天周末在知乎上怼的一个问题,问题的核心就是:“现在大数据都很low了,大家都是去搞人智能了”。
这典型就是对于人工智能定义认知的问题,个人认为人工智能就是一个偏业务的定义,多维度多学科交叉的概念,压根儿就不好以技术维度去对比去评判。其核心的三要素就是:算法、计算能力以及数据。
围绕大量的基础数据,对基础数据进行特征处理,然后构建有用的业务算法模型,然后基于分布式的基础架构计算能力,将算法模型的用于实际的生产环境,以机器替代人工的作业,以提升效果与效率,达到机器智能化的目标。
那再回到推荐系统的话题,在过去传统的门户网站或者其他领域,也是有推荐场景的,不过大部分都是基于编辑或者运营手动进行配置推送,随着对数据、对算法模型的进一步应用,才逐渐有算法机器替代人工进行推荐,并且达到诸如“千人千面”、“个性化”推荐的效果。
所以,追究其本质,其实也是算法模型+计算过程+基础数据的流程,并且最终达到了机器自动化、智能化的效果,从广义的角度来说,或许复杂一些的推荐系统或许也能纳入人工智能的范畴了(真心怕那种一说到人工智能=神经网络的选手)。
0.2.2 推荐与检索两种信息获取的方式
说到推荐系统,就不得不说一下搜索引擎。不管是搜索也好、推荐也好,他们都是信息获取的一种机制,核心区别在于主动与被动。
搜索引擎是典型的主动触发的形态,即用户已经有明确的信息获取意图,渴望得到自身既定的目标信息,让后通过搜索规则进行最终信息的获取。
比如,你好奇什么是人工智能,那么你就会用诸如谷歌、或者国产大百度去搜索,然后获取到相关网页,去点击查看,最终完成你了解人工智能这个信息获取的目的。这就是检索的机制,你先要告诉系统你的意图,然后在给你筛选你要的信息。
而推荐系统则大大的不同,它是一种系统主动的行为,对于用户来说是一个被动的行为,被动的接受系统推送过来的信息。那这样强扭是不是很尴尬呢?怎么有这么SHA叉的机制?
其实不是的,尴尬的是推的不对,东西推对了就尴尬了,比如你正在浏览一个信息,正在愁这个信息还没解决你的问题的时候,系统啪丢给你几个新增的信息,说这个几个信息可能能解决的问题,你一看我凑,这正是我要的,感谢万能的推荐系统!
所以,推荐核心解决的还是用户额外信息获取的问题,以及提升用户的进一步转化,停留时间的延长(只要停留时间延长,商业转化机会就会加大,也是粘性提升的体现),而问题的核心就是要推的准,推的恰到好处,不然就是反作用。
因为推荐要解决的就是海量信息冗余,用户在目的不算很明确的情况,进一步帮助其确定目标,主动筛选信息的过程,推的不好那对于用户来说就更冗余了。
关于信息的获取,其实还有一种常见的形态,那就是结构化导航,比如电商平台的分门别类罗列,门户网站的结构化频道信息。它是通过把信息进行结构化了,构建脉络结构,帮助你去获取你要的信息。不过,这个就不在我们的讨论范围内了。
0.2.3 推荐系统的场景
说了这么多篇逻辑理论的东西,或许很多朋友依然对推荐系统没有一个很场景化的认知,比如具体什么场景?具体什么形态?
image
这是我在腾讯视频上截的图,这就是典型的视频推荐场景,我不是鹅厂腾讯视频业务的算法工程师,所以我无法回答你他们的推荐机制,但我可以告诉你,当时我的观看主体是“地球脉动”,结合推荐列表,大伙儿可以揣摩一下他的推荐机制。当前观看的属性相关?导演关联性?我的观看记录偏好?从我个人的评估来看,这些因素应该都有。
顺带说一下的就是,一个完整的好的推荐系统,一定不会单纯的依赖于某个推荐算法,虽然这个系列的后面文章中,我会讲一些推荐机制或者算法逻辑,甚至附上简单的案例代码,但还是要提前说一下这个问题。
我们再来看几个同样是腾讯系的产品推荐场景:
image
QQ音乐平台的推荐,分析来看应该跟我当前主页音乐的风格、以及我的历史浏览相关。
image
这是阅文网站的小说小说推荐,即当你浏览一本小说时,下面会给这个推荐列表,从其描述以及个人分析来看,好像与个人的行为相关性会小一些,应该是基于大盘用户的浏览轨迹做的关联分析,进而进行关联推荐。
image
最后是电商平台的典型案例,即你在浏览商品时,一般都有猜你喜欢模块,并且推荐系统得以大放光彩,成为应用领域里典型的应用场景,还是得益于亚马逊。当年亚马逊使用推荐算法帮助其提升了XX(具体多少忘了)的年度利润,从此一炮而红,基本上电商平台中的推荐系统就成了标配。
0.2.4 推荐系统的一些坑
看了这么多例子,再结合自己身边实际的体验,确实不难发现,各色各样的产品、平台,都在打造自己的推荐产品,恨不得用户一直点下去,永不跳出。鉴于这种情况,那些尚未为自己产品或者平台开发推荐逻辑的,是不是感觉自己少了个推荐系统,哈哈。
其实核心还是那句话,推荐本身就是个双刃剑,用的不好只会让用户徒增烦恼,这里所说的好不好,不单纯是说准不准的问题,准是前提,即推荐给用户切身所需肯定是好的,但这还不够,你还需要在他需要的时候给他推,时机不对、场景不对,即使你推的东西再准,那也是瞎比推。
所以,即使你觉得你少了个推荐系统,也是需要慎重,或许跟完这个系列会好点?正如上面说的,一些坑还是需要注意的。上面所说的推荐时机以及场景就不再重复了。
第一,好的推荐系统一般情况下很依赖于用户的行为数据,因为从用户行为中自然能一窥用户的一些偏好所在,但实际情况是,用户的行为数据并不是这么容易的,当用户行为数据不够的时候,基于用户行为的分析结论就是个伪命题,甚至会把你带向错误的方向。
第二,用户的偏好一定是会时间偏移进行转变的,所以用户行为的有效性又会是一个问题。
第三,假设这个是新用户呢?完全没有轨迹信息,怎么破。
第四,实际影响用户的选择的因素太多,我们容易陷入主观臆断的误区,综合性考虑是一个完善推荐系统的必须思考的地方。
第五,产品层面的逻辑有时候比底层算法更有效,典型如上面阅文的截图例子,“喜欢这本书的人也喜欢”,这就是一种策略,也是一种推荐解释,可解释性会提升推荐的可信度,诸如还有一些交互方式、产品形态都是对推荐转化有影响的。
03 推荐系统的基础知识
基于上面章节的内容,我们对于推荐系统的常见场景有了一个大概的认知,这个章节,我们从推荐系统本身的基础知识进行拆解,帮我们从理论上掌握更多关于推荐系统相关的知识。
0.3.1 推荐系统概述
在上个章节,我们也大致的提到过,需要先明确的一点就是推荐算法或者推荐机制并不严格等同推荐系统,推荐系统是一个相对复杂的业务系统,里头涉及到数据的处理、架构的构成、推荐的逻辑机制,反馈数据的回收、效果的跟踪、AB测试等等。
并且,很多我们耳熟能详的推荐算法,他只是解决的某种特定情况下的推荐机制问题,而整个系统很多时候是复合了多种算法结果,综合呈现的一种结果。但可以肯定的是,各种理论逻辑、算法机制是构建推荐系统的核心支撑,所以,学习推荐系统,首先学习各种推荐算法并没有毛病。
推荐算法概述-基于内容属性相似的推荐
从原始数据依赖的层面来说,常见的有基于内容属性的推荐机制,这种推荐逻辑很简单,只是单纯的依赖物品之间的属性相似来构建推荐关系,容易理解,有时间还是有一定效果的,但实际上很多时候会存在这几种情况,导致了这种原始推荐失效。
如果用户浏览当前的物品本身就不是用户的菜,甚至是一个非优质信息(当前主体不可控),再基于当前物品进行推荐就是个伪命题。
基于上面这条,即使当前主体是用户的目标,但再推类似主体会造成信息冗余,即当前主体信息已经解决了用户的问题。
所以,由于用户行为的不可控,基于内容属性相似的推荐,风险还是挺高的,这是导致了这种原始直接的机制并不会得到广泛的推广。但与乱推荐相比,还是有一定正向作用的,毕竟用户浏览的主体是自身选择的结果,本身用户对于其选择的信息主体是有一定偏好性的。
推荐算法概述-基于用户画像的推荐
基于物品本身属性的推荐,其实与个性化是没有半毛钱的关系的,毕竟推荐候选集只跟物品主体有关,与用户无关,就谈不上什么个性化了。
而基于用户画像(更多人喜欢用基于用户标签)的推荐,则更大程度上依赖于用户的画像属性来推荐,这就体现了用户偏好信息,根据偏好信息来选择候选集。
这是一种很通用的做法,并且在大规模数据集情况下,很多实际的产生过程中喜欢使用这种机制。而用户的画像,或者更具体点用户的兴趣标签如何构建呢?其实就是依赖用户累积的行为数据了,通过行为数据生成用户的兴趣标签。
这看似是一种相对靠谱的做法,毕竟如果把用户的爱好都分析清楚了,主动给用户做推荐不就显得很个性化了吗?在实际的场景中,首先,并不是所有用户的行为都足够用来表征其兴趣偏好的,即我们会高估用户的行为集合,从而产生有偏差的画像属性,更甚者,如果用户完全没有行为怎么办呢?
其次,通常来说,用户的兴趣爱好是会随时间迁移而改变的,所以,把我用户的兴趣程度以及其变化并不是一个容易的事情,更何况用户实际的选择还会受很多因素影响,比如,我当前查找的一个信息并不是我之前掌握的信息,那意味着这些信息偏好在我的历史轨迹中都体现不出来,那单纯的通过我的兴趣去推荐就显得不靠谱了。
但不管怎么说,根据用户的偏好来做推荐,大方向肯定是没有问题的。
推荐算法概述-基于协同过滤的推荐
协同过滤,或许了解过推荐系统的的朋友,多多少少都能听过一些,并且协同推荐可是作为推荐领域典型案例的存在。
协同过滤同样不会去研究物品的本身属性,甚至也没有空去构建用户的画像标签,正如他的名字描述的一样,他严重依靠于用户的行为以及其周边用户的协同行为。举个例子,为一个用户推荐信息,那么我只需要参考其周边用户在看什么信息,就给他推荐什么信息就好了。
重点在于,如何限定周边这个范围,比如根据两个用户的行为,去构建相关关系,从而判断用户之间的相似程度,把相似用户的行为推荐给当前用户,这就是协同中典型的基于用户推荐。
而如果以物品为维度,以用户的购买或者观看记录为向量,则可以构建物品的相似度量,针对于每一个待推荐选项,用户的历史轨迹就是其向量构成,就可以判断该用户历史的轨迹信息与当前的待选物品的向量相关度了,从而判断是否要推荐,这就是基于物品的协同逻辑。
与基于用户画像的推荐对比,这种推荐有一定几率可以发现新物品,即并不严格依赖用户的兴趣。举个例子,假设几个信息的层级是ABC,并且ABC是层级递进关系,并不是同一个东西,对于一个用户来说,他掌握的是A,则意味着他的兴趣偏好大多偏向于A,根据兴趣标签,其实是很难推荐这种递进相关的信息。
但是,如果其他用户的学习轨迹都是A->B->C这种轨迹,这意味着ABC三者之间本身就有前后潜在逻辑关系存在的,基于协同,即可为该用户在掌握A的基础上,推荐BC的内容,这也是基于兴趣所做不到的地方。
当前,基于协同行为的推荐,除了基于物品还有基于用户,还有其他诸如基于模型的协同,典型如最近邻模型、基于矩阵分解、以及基于图关系模型的构建的推荐机制。
推荐算法概述-其他
其实在我们实际的操作过程中,并不会严格的依赖于这种条条框框、只要合理即可行,比如我们完全可以把推荐问题转化为分类问题,针对于每个待选项,他都是YES OR NO的问题,即一个二值分类。
又比如在上一篇我们学习的一个场景,即阅文网站的小说推荐,还记得他的推荐标题吗?“喜欢这本书的人还喜欢”,这就是典型的“啤酒与尿布”的解法,即货架思维,关联销售,也是可以作为一种推荐机制而存在的。
在比如微信朋友圈,微信一定是会研究用户的朋友圈关系的,比如你对哪类朋友点赞、互动行为最多,它是不是会考虑推荐你欣赏的朋友偏好内容给你?毕竟微信是一个典型的熟人社交模型。
所以,推荐算法看似重要,但其实想想又没有这么重要,很多时候并不是一成不变的,都要我们根据场景去考虑,最最最重要的是,需要我们用实际效果来选择机制,也或许是多种机制共同生效的结果。
0.3.2 相似度量
在我们上面的推荐算法机制中,有个不得不提的操作处理就是各种相似相关度的计算,我们来简单分享一下几种相似或者相关度量的方式。
欧几里得距离(Euclidean Distance)
最常见的距离度量方式,衡量多维空间中两点之间的绝对距离,要求维度的统一。
明可夫斯基距离(Minkowski Distance)
明氏距离是欧氏距离的扩展,是对多个距离度量公式的概括性的表述(可以看到,当p=2时,其实就是欧式距离)。
曼哈顿距离(Manhattan Distance)
曼哈顿距离来源于城市区块距离,是将多个维度上的距离进行求和后的结果,即当上面的明氏距离中p=1时得到的距离度量。
//还有其他的一些距离度量,但是都不太常用,最常用的依然是欧式距离度量。
向量空间余弦相似度(Cosine Similarity)
余弦相似度用向量空间中两个向量夹角的余弦值作为衡量两个个体间差异的大小。相比距离度量,余弦相似度更加注重两个向量在方向上的差异,而非距离或长度上。
皮尔森相关系数(Pearson Correlation Coefficient)
即相关分析中的相关系数r,分别对X和Y基于自身总体标准化后计算空间向量的余弦夹角。基于内容的推荐,还有一点需要注意的就是,对于物品自身属性,如果属性值过少,我们需要适当进行扩大维度,如果维度过多,则需要进行降维。
关于降维和升维,都是一个很大的研究方向,大体上可以说一下几种常见的方式。例如降维,我们可以进行维度聚类、主题抽取,进一步把相关维度进行合并,进一步减少维度;而对于升维,我们可以把维度进行矩阵化,例如假设物品X有A和B两个维度属性,那么我们通过生成AB矩阵的方式,把维度扩充到AB个维度。
0.3.3 冷启动问题的解决
所谓冷启动,即在推荐系统初期时,没有任何用户与物品的交集信息,即无用户的行为轨迹,无法通过类似协同或者用户偏好等方式进行推荐,这种时候,我们就称推荐系统处于冷启动状态。
这种情况,我们需要尽快的累积起第一批用户行为轨迹。我们可以通过基于内容的推荐,或者做一些其他类似的操作,快速有效的进行物品推荐。一段时间后,累积到一定的用户行为时,整个系统就能够正常使用协同过滤等方式进行推荐了。
但是,针对于新加入的用户,或者新加入的物品,同样也是出于冷启动状态的,这个时候,我们通过需要对这种物品或者用户做特殊的处理。
除了基于内容属性的推荐,我们还有其他的一些策略用于弥补这种行为数据不足的情况,比如典型的热度模型,推荐热点信息这种行为虽然low,但是从整体的反馈来看,还是有一定效果的,此外,还可以根据一些统计学上的结论,进行基于统计分析结论的推荐。
除此之外,我们也可以通过其他渠道收集用户的数据,比如用户注册的时候所填写的个人资料,这些都是可以作为推荐的原始依赖数据。
0.3.4 马太效应
马太效应或者说长尾效应,即热者愈热,实际举例来说就是,在实际的购买场景中,由于你推荐的次数越多,部分优质的商品购买或者点击的次数就越多,形成的用户购买轨迹就越多,所以得到的推荐机会就越多,进而产生的推荐也越多,变得越热。
随着不断迭代,子子孙孙无穷尽也,这样得到推荐的商品就会集中在少部分商品中,而大部分长尾商品是沉寂的,一个推荐系统如果长时间处于长尾效应中,造成推荐疲劳,其推荐效果就会减弱。
所以,一个好的推荐系统,要考虑到适当的挖掘长尾商品,通过真的个性化,把适当的长尾商品送到真正需要他们的人手里,在实际的操作过程中,我们可以适当的进行热度降权,从而让一些中下层的商品得到更多的曝光机会,当然前提是保证点击率的情况下。
另外一个场景会形成马太效应的是热度模型,即我们的热度榜单,长时间的高居榜首,一定会获得更多的点击,而点击越多其热度越高,但我们的信息是需要保持新鲜度的,不然点击率迟早会下架的。
所以,我们使用一些机制让处于头部的商品或者信息降权,时间衰减是一个比较通用的做法,即随着时间的迁移,其整体热度会不断的下降,至于说下降的方式,速率就看模型的设计了。
0.3.5 AB测试
关于推荐的效果,之前我们说过其核心的考核标准就是点击率,点击的越多说明推荐的越准确,用户的停留时长也会越长,只要把用户留在平台中,机会总是会有的。其实就是一层漏斗嘛?这一层的基数越大,下一层转换的量就会越高,这也是推荐系统的核心存在意义。
并且之前也说到过,一个不好的推荐系统有时间反而会形成反向作用,所以,一个推荐系统的迭代更新至关重要。离线的效果评估一定是要做的,最起码在离线实验的阶段需要保证当前的效果优于线上效果,才能进行迭代。
但是,实际情况是复杂的,对于推荐的模型来说,离线的实验其实并没有想象中靠谱,那么,就丢到线上去真多真枪的实验一把,就知道效果了。但是,实际的生产环境中,任何一点转化波动的影响都是极其严重的,谁也不敢拿实际生产开玩笑。
于是,就有了AB测试机制的产生,所谓AB测试机制,即将流量分为AB两类,A流量走原始的旧模型,B流量走新模型,同步测试同步对比,效果一目了然。
当然,在实际的AB测试流程中,首先流量是可以自由分配的,一般情况下新模型在最终确认之前流量一定是少量的,随着模型逐渐被验证,流量比重会逐渐加大,最终确认后流量全部导向新模型,完成新模型的正式上线。
并且,通常,在实际的环境中,或许我们会同时有十多个甚至是几十个新模型在同时实验,每个模型调整的因子都不一样,最终选择最适合的因素进行调整,达到效果最优,这也就是AB测试机制的魅力所在。
所以,打造一个好的AB测试系统,首先流量是需要可控的,其次模型的迭代上线是需要高度灵活的,最后,肯定是需要有完整的数据回收、数据分析对比机制存在的。
04 基于内容属性的推荐策略算法
从这个章节开始,我们将从理论进一步过渡到具体的推荐策略算法,我们先从最简单的基于内容属性相关的策略算法着手。
0.4.1 最简单的推荐机制
如标题,既然是“最最最简单”的推荐系统,其实也不能说是推荐系统,之前也说了,系统是一个复合的完整系统,所以这里说推荐机制可能会更恰当些。结合之前大致陈述的一些推荐机制,最最最简单的推荐机制,无疑是基于主体属性相似或者相关推荐了,连个性化都说不上,铁定最最最简单了。
说到这,说不定有些人不愿意干了,既然如此简陋的推荐机制,不看也罢。BUT,真的不要小看基于内容相似的推荐,存在即合理。我们在进行信息获取时,其实本身就是具有一定识别能力的,这意味着我们最终选择查看的信息都是经过我们大脑大致思考的结论,这意味着这些信息是有参考价值的。
并且,在很多时候,我们是需要同类信息进行我们获取到的信息进行补全的,完善我们对目标信息的获取程度。基于这个考量,基于内容属性的推荐其实是说的过去的。
别不服,我们来上例子,还是以前面文章那个腾讯视频的推荐场景图为例。
image.gif
图是在使用腾讯视频观看视频时,我亲手截推荐栏位的内容,补充一下背景(很重要,请注意):
1 我当时观看的是应该是《蓝色星球第二季》纪录片
2 我经常在腾讯视频上看的一般是大片,并且一般是国外的
3 由于是VIP账号,梓尘兄也经常用这个账号看动画片,诸如《小猪佩奇》之类的
4 在腾讯视频上,纪录片中,我只看过《地球脉动》和《蓝色星球》,并且,我真不是纪录片的爱好者,只是喜欢这两部而已
基于上面我提供的个人行为数据,再结合看这批推荐列表,不难发现,上面有很多的纪录片,你觉得跟我们当时正在浏览的内容有没有关系?或者你认为我行为记录中很多纪录片的记录?又或者是我是纪录片的狂热者,导致了腾讯视频给我猛推纪录片。
所以,连腾讯视频都会考虑基于当前浏览内容的属性进行推荐(并且是大范围),你还觉得这种做法十分之LOW吗?当然你也可以认为腾讯视频推的不准,瞎J吧推,也是可以的,我也认同,不是非常准(哈哈,《地球脉动》所有我都看过了,还给我瞎推,上面给推的没几个有欲望去点的,给腾讯视频推荐的开发兄弟们打脸了,不好意思)。
我只想表达的是,这种简单的推荐机制,在整个推荐系统中真的是不可缺少的部分,很多时候简单并不代表无效,类似上面这种情况,我可以举出太多有名有姓的实际案例来,说多了没意义,所以,咱继续。
0.4.2 过程并没有这么简单
从直接的推荐机制来看,整个实现流程看着真的很简单,但是在实际的操作过程中,还是有一些东西值得探讨以及注意的。
第一、首先是,相似计算的过程
之前文章有大致提到过,相似或者相关计算还是有很多可以选择的,他们每一种都有各自的特点以及适应性。以相似计算中使用最多的欧式距离与余弦相似为例,专业点的说法就是余弦夹角可以有效规避个体相同认知中不同程度的差异表现,更注重维度之间的差异,而不注重数值上的差异,而欧式距离则是对个体异常数值会比较敏感。
这意味着,在我们需要区分异常样本时,使用距离计算会更恰当,聚个栗子,比如电商领域中高价值与低价值用户的区分,其实我们核心是想把他们的差异性拉大的,得以体现出对比,这个时候使用余弦就是不合理的。
在回归到距离上说,市面上除了欧式距离,还有好几种距离度量,诸如马氏、曼哈顿距离等等,其实其度量侧重都是不一样的,我们需要结合实际的场景去使用。还有更偏向于相关度量的皮尔森相关系数等。
第二、需要解决相似计算中,计算矩阵过大的问题
按照标准流程,假设有1万个物品,则对于每个物品来说,需要与其他物品计算与其的相似度或者相关度,然后再排个序,取TopN形成自身的待推荐列表。那么,简单的数学题来了10000*10000=10000万次计算,这显然是不合理的。
所以,优化这个过程是必然的,关键是如何优化。核心思想其实就是初筛嘛!把那些完全没啥多大鸟关系的直接干掉,省掉计算相似的过程,节省资源。如何筛选?一个比较常见的做法是,寻找核心关键影响因素,保证关键因素的相关性。
比如,在实际的生产操作过程中,很多时候会通过关键属性是否基本匹配作为判断依据,或者直接通过搜索构建进行检索初筛,把大致相关的先过滤一遍,通过这种简单而粗暴的方式,其实已经能把大部分相关度很低的候选集给过滤掉,对于整体计算量级来说,计算复杂度直接下降。
第三、多个因子如何权衡影响权重
基于属性计算相似,从整体上来看,其实一般主体都不止一个属性,那么计算相关的时候到底看那个属性呢?或者说哪些属性应该占有更高的权重,哪些因素是次要因素。
还是以上面的腾讯视频的推荐为例,从结果上来反推相似推荐的部分(当然,实际情况不详哈,只是推断而已),显然当前视频的类别占了很大的权重(记录片),除此之外包括导演啊,一些其他特征属性应该也会参考在内的。
回到常规问题,如何确定影响权重是个操作难题。最简单并且实际上还挺有效的一种方式就是专家评判法,即通过权威经验来划定影响因子的权重,还有就是通过标注的样本进行反向拟合每种因素的占比权重。除此之外还有一些其他学术上的方法,包括什么主成分分析法,层次分析法,还有什么熵权法,其实都是找因子影响能力的主次关系。
最终确定好了影响因素,在实际上线回收到数据之后,依然是需要逐步的进行权重影响调整的,我们可以通过结果的样本数据,进行LR的回归拟合,寻找最合适的权重配比。
0.4.3 最简单的推荐策略算法实践
说了这么多理论,不能光说不练,标题上写着“附Spark案例”,很多人都是冲着这来的呢,前面BB了这么多屁话,还不见代码。来,我们这就上正文。
不过不用期待过多,毕竟这只是一个简单的相似计算的过程而已,所以权当属性实验数据以及Spark开发了,高手可以略过了。
一、实验数据简介
image.gif
其实看到这三部分数据的简介,一些老手估计已经知道是什么数据了,是的,就是那份有名的电影数据集(MovieLens开放数据),并且取的是完全版的那份,简直成了推荐系统的标配实验数据了。
三个文件,其中电影数据集共1万多个电影基础数据,评分数据集最大共100万条评分数据,以及10万条的用户对电影的打标签数据,总大小约为几百兆,不大,但是用来做实验玩玩那是相当足够了。
二、推荐机制逻辑
我们的核心计算逻辑还是内容属性上的相似嘛,所以核心是看看围绕电影,有哪些属性是可以抽取出来的,并且参与计算的。
第一,电影的类别,基于上面腾讯视频的考虑,其实这个显然很重要,而电影的类别信息存储于电影数据集中,并且是一对多的关系,即一个电影可以对应多个类目,我们需要进行切割,由于计算这个维度相似的时候,是多对多的关系,天然的计算相似或者相关的特征。
第二、电影的播放年份,电影的年份其实有种潜在的关联关系,举个例子可以说明,比如说零几年的电影与现状的电影风格是不同的,当时间跨度有一定差距时,这个还是蛮明显的。关于电影的年份数据,从数据样本可以知道,它隐藏在电影的名字中,写个正则过滤出来即可。至于说如何计算这个维度的相关,我们可以用两者差距来算相关,比如年份绝对值越远,这个值越小,越近甚至是重叠就越大。
第三,电影的标签,电影本身是没有标签属性的,但它有用户对他打的标签信息,所以我们需要进一步处理,把它变成电影的属性,需要清洗、规整以及处理。标签本身也是多对多的关系,同样可以计算相似度,比如欧式或者余弦。
第四、电影的名称,名称上进行寻找关联性,听上去很扯,但其实有一定的逻辑在里头,比如我在视频网站搜索“三国”,显然我期望从名称上寻找三国相关题材的视频,他们就是在名称上建立起关联关系的,所以,名称从某种程度上来说,可以体现相关性。在计算相似或者相关方式上,我们可以进行分词,去除停词,然后再以词维度进行余弦计算。
第五、候选集电影的评分,对于做推荐来说,首先需要保证的推荐的候选集一定是优质的,从这个维度上说,抛开其他因素,那么就是整体评分高的电影是相对优质的电影。在处理的过程中,由于一个电影对应多个评分,所以,我们需要进行进行归一计算,最简单的做法就是计算整体评分的平均值,作为电影的评分数据,评分过低的数据直接从候选集中干掉,又大大的降低了计算次数。
三、代码逻辑
Spark2.0之后,不用再构建sparkcontext了,以创建一个复合多功能的SparkSession替代,可以正常的从HDFS读取文件,也可以从Hive中获取DataFrame等等。
val sparkSession = SparkSession
.builder()
.appName("base-content-Recommand") //spark任务名称
.enableHiveSupport()
.getOrCreate()
那三个表可以先load到Hive中,然后spark直接从Hive中读取,形成DataFrame。
//从hive中,获取rating评分数据集,最终形成如下格式数据(movie,avg_rate)
val movieAvgRate = sparkSession.sql("select movieid,round(avg(rate),1) as avg_rate from tx.tx_ratings group by movieid").rdd.map{
f=>
(f.get(0),f.get(1))
}
//获取电影的基本属性数据,包括电影id,名称,以及genre类别
val moviesData = sparkSession.sql("select movieid,title,genre from tx.tx_movies").rdd
//获取电影tags数据,这里取到所有的电影tag
val tagsData = sparkSession.sql("select movieid,tag from tx.tx_tags").rdd
对tag进行处理,很多tag其实说的是同一个东西,我们需要进行一定程度上的合并,这样才能让tag更加的合理(有朋友有意见了,就一个实验案例而已,搞这么复杂),举个简单例子,blood、bloods、bloody其实都是想说这个电影很血腥暴力,但是不同的人使用的词不同的(这点大伙儿可以自由查看实验数据),所以我们需要进行一定程度上的合并。
val tagsStandardizeTmp = tagsStandardize.collect()
val tagsSimi = tagsStandardize.map{
f=>
var retTag = f._2
if (f.2.toString.split(" ").size == 1) {
var simiTmp = ""
val tagsTmpStand = tagsStandardizeTmp
.filter(
._2.toString.split(" ").size != 1 )
.filter(f._2.toString.size < _.2.toString.size)
.sortBy(
._2.toString.size)
var x = 0
val loop = new Breaks
tagsTmpStand.map{
tagTmp=>
val flag = getEditSize(f._2.toString,tagTmp._2.toString)
if (flag == 1){
retTag = tagTmp._2
loop.break()
}
}
((f._1,retTag),1)
} else {
((f._1,f._2),1)
}
}
其中getEditSize是求取,两个词的编辑距离的,编辑距离在一定时候,进行合并,具体逻辑见代码了,不复杂。
def getEditSize(str1:String,str2:String): Int ={
if (str2.size > str1.size){
0
} else {
//计数器
var count = 0
val loop = new Breaks
//以较短的str2为中心,进行遍历,并逐个比较字符
val lengthStr2 = str2.getBytes().length
var i = 0
for ( i <- 1 to lengthStr2 ){
if (str2.getBytes()(i) == str1.getBytes()(i)) {
//逐个匹配字节,相等则计数器+1
count += 1
} else {
//一旦出现前缀不一致则中断循环,开始计算重叠度
loop.break()
}
}
//计算重叠度,当前缀重叠度大于等于2/7时,进行字符串合并,从长的往短的合并
if (count.asInstanceOf[Double]/str1.getBytes().size.asInstanceOf[Double] >= (1-0.286)){
1
}else{
0
}
}
}
继续对tag进行处理,统计tag频度,取TopN个作为电影对应的tag属性。
val movieTag = tagsSimi.reduceByKey(+).groupBy(k=>k._1._1).map{
f=>
(f._1,f._2.map{
ff=>
(ff._1._2,ff.2)
}.toList.sortBy(
._2).reverse.take(10).toMap)
}
接下来处理年龄、年份和名称,这个会简单点,进行分词处理的话,怎么简单怎么来了,直接使用第三方的HanLP进行关键词抽取作为分词结果,直接屏蔽了停用词。
val moviesGenresTitleYear = moviesData.map{
f=>
val movieid = f.get(0)
val title = f.get(1)
val genres = f.get(2).toString.split("|").toList.take(10)
val titleWorlds = HanLP.extractKeyword(title.toString, 10).toList
val year = movieYearRegex.movieYearReg(title.toString)
(movieid,(genres,titleWorlds,year))
}
取年份的正则函数如下,是个Java写的精通工具类(Scala和Java混写,简直无比美妙)。
package utils;
import java.util.regex.Matcher;
import java.util.regex.Pattern; /**
Desc: 抽取年份公式
/ public class movieYearRegex {
private static String moduleType = ".
\(([1-9][0-9][0-9][0-9])\).*";
public static void main(String[] args){
System.out.println(movieYearReg("GoldenEye (1995)"));
}
public static int movieYearReg(String str){
int retYear = 1994;
Pattern patternType = Pattern.compile(moduleType);
Matcher matcherType = patternType.matcher(str);
while (matcherType.find()) {
retYear = Integer.parseInt(matcherType.group(1));
}
return retYear;
}
}
通过join进行数据合并,生成一个以电影id为核心的属性集合。
val movieContent = movieTag.join(movieAvgRate).join(moviesGenresTitleYear).map{
f=>
//(movie,tagList,titleList,year,genreList,rate)
(f._1,f._2._1._1,f._2._2._2,f._2._2._3,f._2._2._1,f._2._1._2)
}
相似计算开始之前,还记得我们之前说的吗,可以进行候选集阉割,我们先根据一些规则裁剪一下候选集。
val movieConetentTmp = movieContent.filter(f=>f._6.asInstanceOf[java.math.BigDecimal].doubleValue() < 3.5).collect()
然后真正的开始计算相似,使用余弦相似度计算,取排序之后的Top20作为推荐列表。
val movieContentBase = movieContent.map{
f=>
val currentMoiveId = f._1val currentTagList = f._2//[(tag,score)]val currentTitleWorldList = f._3val currentYear = f._4val currentGenreList = f._5val currentRate = f._6.asInstanceOf[java.math.BigDecimal].doubleValue()val recommandMovies = movieConetentTmp.map{ ff=> val tagSimi = getCosTags(currentTagList,ff._2) val titleSimi = getCosList(currentTitleWorldList,ff._3) val genreSimi = getCosList(currentGenreList,ff._5) val yearSimi = getYearSimi(currentYear,ff._4) val rateSimi = getRateSimi(ff._6.asInstanceOf[java.math.BigDecimal].doubleValue()) val score =0.4*genreSimi +0.25*tagSimi +0.1*yearSimi +0.05*titleSimi +0.2*rateSimi (ff._1,score)}.toList.sortBy(k=>k._2).reverse.take(20)(currentMoiveId,recommandMovies)
}.flatMap(f=>f._2.map(k=>(f._1,k._1,k._2))).map(f=>Row(f._1,f._2,f._3))
最后,将结果存入Hive中,Hive中提前建好结果表。
//我们先进行DataFrame格式化申明
val schemaString2 = "movieid movieid_recommand score"
val schemaContentBase = StructType(schemaString2.split(" ")
.map(fieldName=>StructField(fieldName,if (fieldName.equals("score")) DoubleType else StringType,true)))
val movieContentBaseDataFrame = sparkSession.createDataFrame(movieContentBase,schemaContentBase)
//将结果存入hive,需要先进行临时表创建
val userTagTmpTableName = "mite_content_base_tmp"
val userTagTableName = "mite8.mite_content_base_reco"
movieContentBaseDataFrame.registerTempTable(userTagTmpTableName)
sparkSession.sql("insert into table " + userTagTableName + " select * from " + userTagTmpTableName)
到这里,基本大的代码逻辑就完了,可能还有一些边边角角的代码遗漏了,但不妨碍主干了。
05 融合用户兴趣的推荐才个性
接上个章节,我们给了一个最最最简单的推荐系统机制,即基于内容属性的相似或者相关推荐,我们知道这种推荐机制基本只基于内容本身的属性进行推荐,与用户没有半毛钱关系,所以,当然也就说不上个性化了。
0.5.1 个性化与用户画像
在说具体的情况之前,我们先来思考一个问题,什么是个性化?个性化一定是与人相关的,只有人才有个性,每个人可能都有自己的个性,推送的信息如果能满足用户的个性,才是一个好的推荐系统,才具有足够的智能。
而今天,我们要讨论的就是,如何让推荐从非智能的过程演变为知晓用户个性,基于用户偏好进行推荐,从而变得更“聪明”点,也就是智能化。
要实现推荐个性化,那么先需要对用户进行分析,分析用户的偏好,然后根据偏好来做推荐,就顺其自然了。而要分析用户的偏好,那么自然就少不了对用户行为的分析。
所以,核心还是用户画像的分析,然后我们再基于用户画像属性进行推荐,由于用户画像体现的是每个用户的偏好数据,所以,不管怎么样,这种推荐机制或多或少都是能体现一些个性化的东西的。
need-to-insert-img
image
沿着这个路径,我们依然是结合实际数据以及代码案例来分解这个个性化推荐的过程。
0.5.2 基于用户画像的个性化推荐策略
整个案例代码的逻辑是,我们先根据行为数据,进行用户的画像描述抽取,然后再结合用户的画像数据为用户进行信息推荐,注意,这里与之前的实例不同的是,我们是基于用户进行推荐的,而上个实例是在浏览某个内容的时候,进行相关内容推荐,这里以及进化到了根据人进行推荐了。
实践数据源
关于数据源,依然使用的是上个案例中的实验数据,不清楚的见上一个章节(最简单的推荐系统)的原始数据说明,从上次的数据源说明情况看,实际上打标行为数据有10万条,评分数据有100万条,相对于电影内容数据实体来说,其实已经算不少了,所以,不用担心,针对于有行为记录的用户,或多或少还是能描述出他们各自的一些行为偏好的。
用户兴趣标签提取
基于上个小节的流程图,所以,在实践中,我们首先需要做的就是用户兴趣标签的提取。我们核心拥有的就是用户对电影的打标签数据,以及用户对电影的评分数据。
所以,从上面两个行为数据集中,我们可以尝试提取以下几个维度的用户偏好数据:
用户对电影类目偏好数据,即用户对那些类目偏好。
用户的偏好兴趣标签,即通过用户对电影的偏好标签打标行为,进一步可以提取用户的兴趣标签。
用户的偏好年份,通过打分数据,描述用户偏好哪个年代的电影。
我们先解决用户的偏好标签问题,我们已有的是用户对电影的打标行为数据,实际上这是电影层级的标签,所以我们需要在这个基础上,最终直接为用户映射上这些特征标签。
所以,我们需要对单个用户的所有打标数据进行合并(标签会做预处理),然后如果用户对刚好打标的电影有评分的话,还需要带上评分权重,最终合并这些标签,形成用户带权重(本身的频度、对应电影的评分)的标签集,这就是用户的一些兴趣点。
对于类目偏好,说起来就简单了,比如通过评分表,我们把对应所有的电影都取出来,然后分析其类目,将评分作为对应类目的权重,然后将类目进行合并,最终求取用户的类目偏好合集。
对于电影年份,过程与上述取类目的过程类似,最终获取到年份偏好。
电影数据的处理
假设在上面的基础上,我们已经获取了用户层级的画像属性信息,比如偏好的电影类别,偏好的特征标签,偏好的某些年份的电影(同个时代电影具有一些相同的电影,比如10年前的电影风格与现在的俨然不同,年份在某种程度上说还是有影响的,虽然很弱)。
接下来,我们需要绘制候选集电影的属性(取之前,做一些初筛过滤操作,减少计算量),对应用户的属性,同样是三个,其中年份、类目都是直接存放于电影表中,唯一需要额外处理的就是特征Tag了,由于不同人对不同的电影进行Tag标记,上面进行用户画像绘制的时候,是以人为维度的,现在已电影为维度,进行标签合并,最终同样可以形成电影维度的标签集。
关联推荐计算
每个维度分别进行计算相似度或者相关度,然后不同维度进行合并归一计算最终的用户与电影的相关度。最外层我们依然以权重模型去做,以经验来看,类目是最重要的,其次是Tag,最后才是年份属性,至于最终怎么调整还是需要根据实际反馈数据来做微调,现在就拍脑袋吧。
image
作者:daos
链接://www.greatytc.com/p/db181f4b5f2b
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。