82

附Spark案例 | 推04,融合了用户兴趣的推荐系统才更具个性

 6 years ago
source link: http://mp.weixin.qq.com/s/-wSomojPICm2o6_Acqc0Jg
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

附Spark案例 | 推04,融合了用户兴趣的推荐系统才更具个性

Original HCY崇远 数据虫巢 2017-12-18 11:19 Posted on

收录于合集 #推荐广告系列 45个

Image

作者 | HCY崇远

本文为《数据虫巢,推荐系统系列》第四篇,也是案例实践的第二篇,不懂前情提要的,请关注公号『数据虫巢』,阅读《推01,你们是不是都感觉自己少了个推荐系统?》《推02,就算是非技术人员也都有必要了解的一些推荐系统常识》《推03,最最最简单的推荐系统是什么样的 | 附Spark实践案例》

接上一篇,我们给了一个最最最简单的推荐系统机制,即基于内容属性的相似或者相关推荐,我们知道这种推荐机制基本只基于内容本身的属性进行推荐,与用户没有半毛钱关系,所以,当然也就说不上个性化了。

01 个性化与用户画像

在说具体的情况之前,我们先来思考一个问题,什么是个性化?个性化一定是与人相关的,只有人才有个性,每个人可能都有自己的个性,推送的信息如果能满足用户的个性,才是一个好的推荐系统,才具有足够的智能。

而今天,我们要讨论的就是,如何让推荐从非智能的过程演变为知晓用户个性,基于用户偏好进行推荐,从而变得更“聪明”点,也就是智能化。

要实现推荐个性化,那么先需要对用户进行分析,分析用户的偏好,然后根据偏好来做推荐,就顺其自然了。而要分析用户的偏好,那么自然就少不了对用户行为的分析。

所以,核心还是用户画像的分析,然后我们再基于用户画像属性进行推荐,由于用户画像体现的是每个用户的偏好数据,所以,不管怎么样,这种推荐机制或多或少都是能体现一些个性化的东西的。

Image

沿着这个路径,我们依然是结合实际数据以及代码案例来分解这个个性化推荐的过程。

02 Spark代码实践案例

整个案例代码的逻辑是,我们先根据行为数据,进行用户的画像描述抽取,然后再结合用户的画像数据为用户进行信息推荐,注意,这里与之前的实例不同的是,我们是基于用户进行推荐的,而上个实例是在浏览某个内容的时候,进行相关内容推荐,这里以及进化到了根据人进行推荐了。

实践数据源

关于数据源,依然使用的是上个案例中的实验数据,不清楚的见上一篇《推03,最最最简单的推荐系统是什么样的 | 附Spark实践案例》的说明,从上次的数据源说明情况看,实际上打标行为数据有10万条,评分数据有100万条,相对于电影内容数据实体来说,其实已经算不少了,所以,不用担心,针对于有行为记录的用户,或多或少还是能描述出他们各自的一些行为偏好的。

用户兴趣标签提取

基于上个小节的流程图,所以,在实践中,我们首先需要做的就是用户兴趣标签的提取。我们核心拥有的就是用户对电影的打标签数据,以及用户对电影的评分数据。

所以,从上面两个行为数据集中,我们可以尝试提取以下几个维度的用户偏好数据:

  • 用户对电影类目偏好数据,即用户对那些类目偏好。

  • 用户的偏好兴趣标签,即通过用户对电影的偏好标签打标行为,进一步可以提取用户的兴趣标签。

  • 用户的偏好年份,通过打分数据,描述用户偏好哪个年代的电影。

我们先解决用户的偏好标签问题,我们已有的是用户对电影的打标行为数据,实际上这是电影层级的标签,所以我们需要在这个基础上,最终直接为用户映射上这些特征标签。

所以,我们需要对单个用户的所有打标数据进行合并(标签会做预处理),然后如果用户对刚好打标的电影有评分的话,还需要带上评分权重,最终合并这些标签,形成用户带权重(本身的频度、对应电影的评分)的标签集,这就是用户的一些兴趣点。

对于类目偏好,说起来就简单了,比如通过评分表,我们把对应所有的电影都取出来,然后分析其类目,将评分作为对应类目的权重,然后将类目进行合并,最终求取用户的类目偏好合集。

对于电影年份,过程与上述取类目的过程类似,最终获取到年份偏好。

电影数据的处理

假设在上面的基础上,我们已经获取了用户层级的画像属性信息,比如偏好的电影类别,偏好的特征标签,偏好的某些年份的电影(同个时代电影具有一些相同的电影,比如10年前的电影风格与现在的俨然不同,年份在某种程度上说还是有影响的,虽然很弱)。

接下来,我们需要绘制候选集电影的属性(取之前,做一些初筛过滤操作,减少计算量),对应用户的属性,同样是三个,其中年份、类目都是直接存放于电影表中,唯一需要额外处理的就是特征Tag了,由于不同人对不同的电影进行Tag标记,上面进行用户画像绘制的时候,是以人为维度的,现在已电影为维度,进行标签合并,最终同样可以形成电影维度的标签集。

关联推荐计算

每个维度分别进行计算相似度或者相关度,然后不同维度进行合并归一计算最终的用户与电影的相关度。最外层我们依然以权重模型去做,以经验来看,类目是最重要的,其次是Tag,最后才是年份属性,至于最终怎么调整还是需要根据实际反馈数据来做微调,现在就拍脑袋吧。

Image

我们来看代码逻辑

至于说取原始数据的过程,就不多说了,具体的可以看上面那个案例,这里就不贴代码片了,这里所有表的数据都会用到,所以都要获取。

第一步,先进行movie候选集的处理,包括Tag预处理,合并,以及类目年份的获取

我们进行相似tag合并操作,返回的数据形态是(mvieid,tag)集合,但tag会做提前进行预处理,过程依然跟上次一样,进行编辑距离相近的词合并。

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)
} else {
f
}
}

我们先将预处理之后的movie-tag数据进行统计频度,直接作为tag权重,形成(movie,tagList(tag,score))这种数据集形态。

val movieTagList = tagsSimi.map(f=>((f._1,f._2),1)).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)
}

接着进行genre类别以及抽取电影属性的年份属性,其中涉及的正则方法见上一个实例,这里就不重复给出了。

val moviesGenresYear = moviesData.rdd.map{
f=>
val movieid = f.get(0)
val genres = f.get(2)
val year = movieYearRegex.movieYearReg(f.get(1).toString)
val rate = f.get(3).asInstanceOf[java.math.BigDecimal].doubleValue()
(movieid,(genres,year,rate))
}

最终将三种不同的属性进行合并,形成电影的处理过的候选集,当然还有电影的平均评分rate属性,这是判断电影基本水平的标志。

val movieContent = movieTagList.join(moviesGenresYear).filter(f=>f._2._2._3 < 2.5).sortBy(f=>f._2._2._3,false).map{
f=>
//userid,taglist,genre,year,rate
   (f._1,f._2._1,f._2._2._1,f._2._2._2,f._2._2._3)
}.collect()

第二步,我们进行用户画像属性的获取

先通过rating评分表与tags表进行关联join,获取用户直接与tag的关联关系,这样评分数据就可以当成单个tag的权重进行计算了,并且通过DataFrame的API操作会更方便,所以可以先将之前处理的tagsSimi转换成DF,然后直接可以使用类似SQL的逻辑关系了。

val schemaString = "movieid tag"
val schema = StructType(schemaString.split(" ").map(fieldName=>StructField(fieldName,StringType,true)))
val tagsSimiDataFrame = sparkSession.createDataFrame(tagsSimi.map(f=>Row(f._1,f._2.toString.trim)),schema)
//对rating(userid,movieid,rate),tags(movieid,tag)进行join,以movieid关联
//join步骤,将(userId, movieId, rate)与(movieId, tag)按照movieId字段进行连接
val tagRateDataFrame = ratingData.join(tagsSimiDataFrame,ratingData("movieid")===tagsSimiDataFrame("movieid"),"inner").select("userid","tag","rate")

接着进行类似reduce操作,在SQL中就是分组合并,将(userId, tag, rate)中(userId, tag)相同的分数rate相加。

val userPortraitTag = tagRateDataFrame.groupBy("userid","tag").sum("rate").rdd.map{
f=>
(f.get(0),f.get(1),f.get(2).asInstanceOf[java.math.BigDecimal].doubleValue())
}.groupBy(f=>f._1).map{
f=>
val userid = f._1
val tagList = f._2.toList.sortBy(_._3)
.reverse.map(k=>(k._2,k._3)).take(20)
(userid,tagList.toMap)
}

在处理完用户的兴趣Tag之后,处理其他属性,Year属性。

val userPortraitYear = userYear.rdd.map(f=>(f.get(0),f.get(1),f.get(2))).groupBy(f=>f._1).map{
f=>
val userid = f._1
val yearList = f._2.map(f=>(f._2,f._3.asInstanceOf[java.math.BigDecimal].doubleValue())).toList.take(10)
(userid,yearList)
}

进行用户的genre偏好处理。

val userPortraitGenre = userGenre.rdd.map(f=>(f.get(0),f.get(1),f.get(2))).groupBy(f=>f._1).map{
f=>
val userid = f._1
val genreList = f._2.map(f=>(f._2,f._3.asInstanceOf[java.math.BigDecimal].doubleValue())).toList.take(10)
(userid,genreList)
}

对于每一个用户来说,在计算待推荐列表时,都需要移除自身已经看过的电影,先获取用户的观看列表。

val userMovieGet = ratingData.rdd.map(f=>(f.get(0),f.get(1))).groupByKey()

第三步,进行电影画像与用户画像的匹配计算

在实际的计算过程中,每个同纬度的属性进行相似计算,最终外层通过权重模型进行打分,然后重新排序,获取每个用户的对应的待推荐电影TopN,记得要移除自身已看过的电影列表。

val portraitBaseReData = userPortraitTag.join(userPortraitYear).join(userPortraitGenre).join(userMovieGet).map{
f=>
val userid = f._1
val userTag = f._2._1._1._1
val userYear = f._2._1._1._2
val userGenre = f._2._1._2
//用于做差集计算,移除已经看过的电影
   val userMovieList = f._2._2.toList
val movieRe = movieContent.map{
ff=>
val movieid = ff._1
val movieTag = ff._2
val movieGenre = ff._3
val movieYear = ff._4
val movieRate = ff._5
val simiScore = getSimiScore(userTag ,movieTag,userGenre,movieGenre,userYear,movieYear,movieRate)
(movieid,simiScore)
}.diff(userMovieList).sortBy(k=>k._2).reverse.take(20)
(userid,movieRe)
}.flatMap(f=>f._2.map(ff=>(f._1,ff._1,ff._2)))

其中函数getSimiScore相关的计算逻辑如下。

def getSimiScore(userTag:Map[Any,Double],movieTag:Map[Any,Int],
                userGenre:List[(Any,Double)],movieGenre:Any,
                userYear:List[(Any,Double)],movieYear:Any,
                movieRate:Double): Double ={
val tagSimi = getCosTags(userTag,movieTag)
val genreSimi = getGenreOrYear(userGenre,movieGenre)
val yearSimi = getGenreOrYear(userYear,movieYear)
val rateSimi = getRateSimi(movieRate)
val score = 0.4*genreSimi + 0.3*tagSimi + 0.1*yearSimi + 0.2*rateSimi
score
}

至于每个维度的计算过程,这里就不列了,大同小异,只要逻辑走的通,具体可见源代码。

第四步,对结果进行存储。

最后,将计算的结果保存下来,同样,需要先进行表结构定义。

val schemaPortraitStr = "userid movieid score"
val schemaPortrait = StructType(schemaPortraitStr.split(" ").map(fieldName=>StructField(fieldName,if (fieldName.equals("score")) DoubleType else  StringType,true)))
val portraitBaseReDataFrame = sparkSession.createDataFrame(portraitBaseReData.map(f=>Row(f._1,f._2,f._3)),schemaPortrait)
//将结果存入hive
val portraitBaseReTmpTableName = "mite_portraitbasetmp"
val portraitBaseReTableName = "mite8.mite_portrait_base_re"
portraitBaseReDataFrame.registerTempTable(portraitBaseReTmpTableName)
sparkSession.sql("insert into table " + portraitBaseReTableName + " select * from " + portraitBaseReTmpTableName)

至此,所有代码主体逻辑已经清晰了,其实说白了就是一个计算用户画像的过程,然后画像与待推荐主体之间的关联性。

03 实操依然还有很多需要处理的问题

如上,基于用户画像的推荐机制在实际操作中,其实还有很多需要考虑的地方,并没有想象中简单。

比如,用户的行为并没有我们想象中靠谱。

所谓没想象中靠谱是说,一方面用户的行为数据,有时候并不是其兴趣特点所表现,这点很显然,比如如果系统把一些信息故意放在很显眼的位置,那么对于一般用户来说,不点也得点了,所以就会造成这种用户数据其实是不那么靠谱的。

另一方面是如果用户产生了行为数据,但是行为数据并不足够多,那么这个时候其实这些行为数据是有置信度的考量的,行为数据不够产生的描述是有可能形成偏差的,再根据有偏差的数据去做推荐,那结果只能是更离谱了。

用户兴趣时效性问题。

在上面的实验逻辑中,我们知道我们并没有对用户的行为数据做更多的过滤,而实际的操作中,用户的兴趣是有一定时效性的。举个例子,我在一年前看电影的记录,还适合放到现在做我的画像分析吗?不一定的,因为我的兴趣可能已经随时间偏移了,过去我所喜欢的东西,现在我已经不喜欢了。

所以,在一般实际操作的过程中,一定需要分辨用户的兴趣数据的有效性,一般情况下,我们会进行长期兴趣和短期兴趣的区分,人在一定时间内其兴趣是固定的,并且在一些很短暂的时间段内,比如一两天、甚至是一天内,其关注点是有一定意义的,这个时候其短期兴趣就生效了。

所以,我们在实际操作的时候,长期兴趣、短期兴趣的具体的应用就需要结合实际的场景的区分了,已经我们需要注意原始数据是否适合做兴趣描述的来源数据,是否已经失效。

冷启动的问题。

所有涉及到行为数据的推荐算法,都绕不开冷启动的问题,即一个用户是个新手,没有任何行为记录留下,这意味着我们就无法分析其画像了,这个时候就称之为该用户的冷启动。

在上上个文章《推02,就算是非技术人员也都有必要了解的一些推荐系统常识》,我们有提到过一些解决冷启动的机制,比如基于内容推荐(见上个文章),进行热点内容推荐(比如把最热门的一些电影推给该用户),还比如根据整体数据做关联推荐(这个后面再讲),方式很多,效果不一,需要根据具体情况来看了,再不行就想办法在用户注册的时候尽可能的收集用户的静态数据,再根据用户的静态画像数据来推荐,总比乱推的好。

匹配计算的问题。

在上面的例子中,我们其实并没有做过多匹配计算逻辑的讲解,只是简单描述同纬度的进行相似计算,然后上层做权重模型,其实就是一种很普通的匹配计算的过程。准不准,难在于外层权重的合理性,具体过程见第二篇文章,这里就不过多阐述。

其实这算是我们有意为之了,如果有些时候没法让不同主体(用户&内容)形成同一个维度矩阵的时候,这个时候其实就要有比较合理的映射机制了,能让内容与用户的属性做关联计算。

04 写在最后

写到这里,结合实际的数据,Spak工程代码,我们成功的从呆板的属性推荐过渡到基于用户画像的推荐,并为推荐附上了个性化的能力,但实际上基于用户画像的个性化推荐依然是有缺陷的,比如他不会做用户兴趣的升级,而实际上一些知识本身就是具有一定的阶梯性的。

举个例子就很容易理解了,比如,你对大数据的东西很感兴趣,于是系统根据你的兴趣偏好天天给你推Hadoop、大数据各种技术框架等信息,在某个时间段可能是合理,比如我对大数据领域已经熟知了呢?你还给我天天推送大数据相关的信息。

而我实际上是需要寻求大数据关联的信息,甚至是升级的信息,比如基于大数据的机器学习、数据挖掘相关的东西,这个机制是无法做到这一层的。所以,学完了这个,还没完事,下个章节,我们将学习另一个推荐机制,这种推荐机制可以为你推送一些基于你兴趣之外的东西。

想要进一步研究代码逻辑以及实际跑一跑这个Spark实验案例的,可以加我,从我这要实验数据以及完整的代码文件,甚至是各种逻辑讲解。

当然,如果你是初学者,想要向我了解更多推荐相关的东西,以及代码逻辑进行详细的讲解,甚至是代码整体工程,以及跑到Spark环境中的整体流程,这个是要收咨询费的,哈哈,老老实实给我打100大洋的红包,包服务到家(没办法穷),联系微信见下。

关于我:

大数据行业半个老鸟,我家梓尘兄的超级小弟,会敲代码、会写文章,还会泡奶粉哄小屁孩。

想和我交流的,可以加我个人微信mute88,可以拉你入交流群,但请注明身份and来意~

--2017年12月18号凌晨1点

系列文章:

《推01,你们是不是都觉得自己少了个推荐系统?》

《推02,就算非技术人员也有必要了解的推荐系统常识》

《推03,最最最简单的推荐系统是什么样的 | 附Spark实践案例》

《推04,融合了用户兴趣的推荐系统才会更具个性 | 附Spark实践案例 》(本文)

欢迎关注数据虫巢(ID:blogchong),这里有很多关于大数据的原创文章,如果你觉得文章有用,欢迎转发,也不介意你打赏一杯深夜写文的咖啡,谢谢。

Image

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK