我们来看代码逻辑
至于说取原始数据的过程,就不多说了,具体的可以看上面那个案例,这里就不贴代码片了,这里所有表的数据都会用到,所以都要获取。
第一步,先进行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)varx =0val loop =newBreaks 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}
}</pre>
我们先将预处理之后的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._1val 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._1val genreList = f._2.map(f=>(f._2,f._3.asInstanceOf[java.math.BigDecimal].doubleValue())).toList.take(10)(userid,genreList)
}
对于每一个用户来说,在计算待推荐列表时,都需要移除自身已经看过的电影,先获取用户的观看列表。
<pre style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial; widows: 1; color: rgb(169, 183, 198); font-family: 宋体; font-size: 10.5pt; background-color: rgb(43, 43, 43);">val userMovieGet = ratingData.rdd.map(f=>(f.get(0),f.get(1))).groupByKey()</pre>
第三步,进行电影画像与用户画像的匹配计算
在实际的计算过程中,每个同纬度的属性进行相似计算,最终外层通过权重模型进行打分,然后重新排序,获取每个用户的对应的待推荐电影TopN,记得要移除自身已看过的电影列表。
val portraitBaseReData = userPortraitTag.join(userPortraitYear).join(userPortraitGenre).join(userMovieGet).map{
f=>
val userid = f._1val userTag = f._2._1._1._1val userYear = f._2._1._1._2val userGenre = f._2._1._2//用于做差集计算,移除已经看过的电影val userMovieList = f._2._2.toListval 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.4genreSimi + 0.3tagSimi + 0.1yearSimi + 0.2rateSimi
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)
至此,所有代码主体逻辑已经清晰了,其实说白了就是一个计算用户画像的过程,然后画像与待推荐主体之间的关联性。
0.5.3 实操中的注意事项
如上,基于用户画像的推荐机制在实际操作中,其实还有很多需要考虑的地方,并没有想象中简单。
比如,用户的行为并没有我们想象中靠谱。
所谓没想象中靠谱是说,一方面用户的行为数据,有时候并不是其兴趣特点所表现,这点很显然,比如如果系统把一些信息故意放在很显眼的位置,那么对于一般用户来说,不点也得点了,所以就会造成这种用户数据其实是不那么靠谱的。
另一方面是如果用户产生了行为数据,但是行为数据并不足够多,那么这个时候其实这些行为数据是有置信度的考量的,行为数据不够产生的描述是有可能形成偏差的,再根据有偏差的数据去做推荐,那结果只能是更离谱了。
用户兴趣时效性问题。
在上面的实验逻辑中,我们知道我们并没有对用户的行为数据做更多的过滤,而实际的操作中,用户的兴趣是有一定时效性的。举个例子,我在一年前看电影的记录,还适合放到现在做我的画像分析吗?不一定的,因为我的兴趣可能已经随时间偏移了,过去我所喜欢的东西,现在我已经不喜欢了。
所以,在一般实际操作的过程中,一定需要分辨用户的兴趣数据的有效性,一般情况下,我们会进行长期兴趣和短期兴趣的区分,人在一定时间内其兴趣是固定的,并且在一些很短暂的时间段内,比如一两天、甚至是一天内,其关注点是有一定意义的,这个时候其短期兴趣就生效了。
所以,我们在实际操作的时候,长期兴趣、短期兴趣的具体的应用就需要结合实际的场景的区分了,已经我们需要注意原始数据是否适合做兴趣描述的来源数据,是否已经失效。
冷启动的问题。
所有涉及到行为数据的推荐算法,都绕不开冷启动的问题,即一个用户是个新手,没有任何行为记录留下,这意味着我们就无法分析其画像了,这个时候就称之为该用户的冷启动。
在上上个章节中,我们有提到过一些解决冷启动的机制,比如基于内容推荐(见上个章节),进行热点内容推荐(比如把最热门的一些电影推给该用户),还比如根据整体数据做关联推荐(这个后面再讲),方式很多,效果不一,需要根据具体情况来看了,再不行就想办法在用户注册的时候尽可能的收集用户的静态数据,再根据用户的静态画像数据来推荐,总比乱推的好。
匹配计算的问题。
在上面的例子中,我们其实并没有做过多匹配计算逻辑的讲解,只是简单描述同纬度的进行相似计算,然后上层做权重模型,其实就是一种很普通的匹配计算的过程。准不准,难在于外层权重的合理性,具体过程见第二篇文章,这里就不过多阐述。
其实这算是我们有意为之了,如果有些时候没法让不同主体(用户&内容)形成同一个维度矩阵的时候,这个时候其实就要有比较合理的映射机制了,能让内容与用户的属性做关联计算。
0.5.4 信息补充
写到这里,结合实际的数据,Spak工程代码,我们成功的从呆板的属性推荐过渡到基于用户画像的推荐,并为推荐附上了个性化的能力,但实际上基于用户画像的个性化推荐依然是有缺陷的,比如他不会做用户兴趣的升级,而实际上一些知识本身就是具有一定的阶梯性的。
举个例子就很容易理解了,比如,你对大数据的东西很感兴趣,于是系统根据你的兴趣偏好天天给你推Hadoop、大数据各种技术框架等信息,在某个时间段可能是合理,比如我对大数据领域已经熟知了呢?你还给我天天推送大数据相关的信息。
而我实际上是需要寻求大数据关联的信息,甚至是升级的信息,比如基于大数据的机器学习、数据挖掘相关的东西,这个机制是无法做到这一层的。所以,学完了这个,还没完事,下个章节,我们将学习另一个推荐机制,这种推荐机制可以为你推送一些基于你兴趣之外的东西。
06 经典的协同推荐
接上一个章节,我们大致Get到了一个点,那就是如果要达到推荐个性化的目的,核心还是用户的行为数据,只有用户各自的行为数据才能反馈其与其他人所不一样的特性,从而有针对性的进行推荐。按上个章节的原话,大致就是这样的:
实际上基于用户画像的个性化推荐依然是有缺陷的,比如他不会做用户兴趣的升级,而实际上一些知识本身就是具有一定的阶梯性的。
举个例子就很容易理解了,比如,你对大数据的东西很感兴趣,于是系统根据你的兴趣偏好天天给你推Hadoop、大数据各种技术框架等信息,在某个时间段可能是合理,比如我对大数据领域已经熟知了呢?你还给我天天推送大数据相关的信息。
而我实际上是需要寻求大数据关联的信息,甚至是升级的信息,比如基于大数据的机器学习、数据挖掘相关的东西,这个机制是无法做到这一层的。
说白了其实就是基于用户画像的推荐,他无法发现新知识,所谓新知识就是,与你之前的兴趣爱好相对比,推荐的候选集永远圈定在你的兴趣标签维度内,做不到认知的升级,而实际上认知是会进行升级的,特别是随着你捕获的知识信息越多的情况下,你就越会对更上层的其他知识感兴趣,不断的深入下去。
而基于协同过滤的推荐,或多或少能解决一点这类问题,最起码能够结合本身用户的行为,让你触达新的知识信息,并且这种递进是通过协同关系得到的,意味着是大部分人的共同选择,所以还是具有一定合理性的。
0.6.1 协同的原理拆解
对于基于协同过滤的推荐,可谓是推荐系统中的经典推荐算法了,记得好像就是亚马逊推广出来的,然后大放光彩。协同过滤又分为基于用户的协同(UserCF)、基于物品的协同(ItemCF),以及基于模型的协同(ModelCF)。
基于用户的协同过滤推荐(UserCF)。
基于用户的协同过滤,即我们希望通过用户之间的关系来达到推荐物品的目的,于是,给某用户推荐物品,即转换为寻找为这个用户寻找他的相似用户,然后相似用户喜欢的物品,那么也可能是这个用户喜欢的物品(当然会去重)。
来看一个表格:
|
用户/****物品
|
物品A
|
物品B
|
物品C
|
物品D
|
|
用户A
|
Y
|
?
|
Y
|
?
|
|
用户B
|
|
Y
|
|
|
|
用户C
|
Y
|
|
Y
|
Y
|
//其中Y表示对应用户喜欢对应物品,-表示无交集,?表示需不需要推荐。
这是一个最简单的例子,其实目的很简单,我们需要给用户A推荐物品,而且可以看到,用户已经喜欢了物品A和物品C,其实剩下也就B和D了,要么是B,要么是D。那么根据UserCF算法,我们先计算用户A与用户BC之间的相似度,计算相似,我们前文说了,要么距离,要么余弦夹角。
假如我们选择计算夹角(四维):cosAB=0(90度的夹角),cosAC=0.8199(角度自己算吧)。所以相比来说,我们会发现用户A与用户C的相似度是稍微大一些的。于是,我们观察用户C都喜欢了哪些物品,然后与用户的去重,然后会发现该给用户A推荐物品D。
简单来讲,UserCF就是如上过程,但在实际的过程中,数据量肯定不止这么点,于是我们需要做的是为用户计算出相似用户列表,然后在相似用户中经过去重之后,计算一个推荐的物品列表(在计算推荐物品的时候,可以叠加用户的相似程度进一步叠加物品的权重)。
然后在喜欢物品的表达形式上,可以是如上的这种二值分类,即Yes Or No,也可以是带有评分的程度描述,比如对于某个物品打多少分的这种表现形式。这样的话,针对于后一种情况,我们就需要在求在计算相似度时,加入程度的权重考量。
基于物品的协同推荐(ItemCF)
不同于基于用户的协同,这里,我们计算的是物品之间的相似度,但是,请注意,我们计算物品相似度的时候,与直接基于物品相似度推荐不同是,我们所用的特征并不是物品的自身属性,而依然是用户行为。
|
用户/****物品
|
物品A
|
物品B
|
物品C
|
|
用户A
|
Y
|
|
Y
|
|
用户B
|
Y
|
Y
|
Y
|
|
用户C
|
Y
|
?
|
?
|
//其中Y表示对应用户喜欢对应物品,-表示无交集,?表示需不需要推荐。
同样,这是一个简单实例。目的也明确,我们在知道用户AB喜欢某些物品情况,以及在用户C已经喜欢物品C的前提下,为用户C推荐一个物品。看表格很简单嘛。只有两个选项,要么物品B,要么物品C。那么到底是物品B还是物品C呢?
我们来计算物品A与其他两种物品的相似度,计算向量夹角。对于用户A,物品A与物品B,则对于AB向量为(1,0),(1,1),对于AC向量为(1,1),(1,1),分别计算夹角cosAB=0.7,cosAC=1。或者用类似关联规则的方法,计算两者之间的共现,例如AB共现1次,AC共现2次。通过类似这种方式,我们就知道物品A与物品C在某种程度上是更相似的。
我要说的就是类似共现类做计算的这种方式,在大规模数据的情况下是很有效的一种方式,基于统计的方法在数据量足够的时候,更能体现问题的本质。
基于模型的协同推荐(ModelCF)。
除了我们熟悉的基于用户以及基于物品的协同,还有一类,基于模型的协同过滤。基于模型的协同过滤推荐,基于样本的用户偏好信息,训练一个模型,然后根据实时的用户喜好信息进行预测推荐。常见的基于模型推荐又有三种:最近邻模型,典型如K最近邻;SVD模型,即矩阵分解;图模型,又称为社会网络图模型。
最近邻模型
最近邻模型,即使用用户的偏好信息,我们计算当前被推荐用户与其他用户的距离,然后根据近邻进行当前用户对于物品的评分预测。
典型如K最近邻模型,假如我们使用皮尔森相关系数,计算当前用户与其他所有用户的相似度sim,然后在K个近邻中,通过这些相似用户,预测当前用户对于每一个物品的评分,然后重新排序,最终推出M个评分最高的物品推荐出去。需要注意的是,基于近邻的协同推荐,较依赖当前被推荐用户的历史数据,这样计算出来的相关度才更准确。
SVD矩阵分解
我们把用户和物品的对应关系可以看做是一个矩阵X,然后矩阵X可以分解为X=A*B。而满足这种分解,并且每个用户对应于物品都有评分,必定存在与某组隐含的因子,使得用户对于物品的评分逼近真实值,而我们的目标就是通过分解矩阵得到这些隐性因子,并且通过这些因子来预测还未评分的物品。
有两种方式来学习隐性因子,一为交叉最小二乘法,即ALS;而为随机梯度下降法。首先对于ALS来说,首先随机化矩阵A,然后通过目标函数求得B,然后对B进行归一化处理,反过来求A,不断迭代,直到A*B满足一定的收敛条件即停止。
对于随机梯度下降法来说,首先我们的目标函数是凹函数或者是凸函数,我们通过调整因子矩阵使得我们的目标沿着凹函数的最小值,或者凸函数的最大值移动,最终到达移动阈值或者两个函数变化绝对值小于阈值时,停止因子矩阵的变化,得到的函数即为隐性因子。
使用分解矩阵的方式进行协同推荐,可解释性较差,但是使用RMSE(均方根误差)作为评判标准,较容易评判。
并且,我们使用这种方法时,需要尽可能的让用户覆盖物品,即用户对于物品的历史评分记录需要足够的多,模型才更准确。
社会网络图模型
所谓社会网络图模型,即我们认为每个人之间都是有联系的,任何两个用户都可以通过某种或者多个物品的购买行为而联系起来,即如果一端的节点是被推荐用户,而另一端是其他用户,他们之间通过若干个物品,最终能联系到一起。
而我们基于社会网络图模型,即研究用户对于物品的评分行为,获取用户与用户之间的图关系,最终依据图关系的距离,为用户推荐相关的物品。
目前这种协同推荐使用的较少。
0.6.2 基于Spark的协同过滤实践
老规矩,大致过完了理论,我们来走一遭代码实践,数据源的解释不就多说了,依然还是那份电影数据,不清楚的见上上上一章节的的数据说明,这次我们只用到涉及到评分的数据,共100万条,我们通过评分行为来做协同过滤。
截止Spark2.X系列,Spark的MlLib只实现了基于矩阵分解的协同(也就是经典的基于ALS协同过滤),没有实现更常规的基于物品或者基于用户的协同过滤,但从上面的原理我们知道,其实基于物品基于用户的协同核心就在于构建基础向量矩阵以及计算相似的两个方面,我这边也是实现了,但基于篇幅这里,就只介绍基于ALS的实践过程了,其他两个案例,需要的话请联系我。
由于MlLib实现了算法模型,所以从敲代码的维度上来说,代码量反而会远远低于基于用户、基于物品的协同,甚至会少于之前的基于物品相似或者基于用户画像的推荐了,顺带说一句,基于ALS的推荐代码,其实网上很容易找,算法MlLib中的经典算法了,很多人都实现了,不过万变不离其宗(变个毛线,API接口就那几个,参数也就那几个,能怎么变)。
先Hive数据表中,将rating评分数据取出来(当然,如果你的机子跑不动,就limit一下简单取些数,跑通模型就得啦)。
val ratingDataOrc = sparkSession.sql("select userid,movieid,rate,timestame from mite8.mite_ratings limit 50000")
将取出的评分数据,以时间构建Key-value键值对,形成(Int,Ratings)格式的数据,其实这是一个中间处理过程,方便后续的数据输入。
val ratings = ratingDataOrc.rdd.map(f =>
(java.lang.Long.parseLong(f.get(3).toString)%10,
Rating(java.lang.Integer.parseInt(f.get(0).toString),
java.lang.Integer.parseInt(f.get(1).toString),
f.get(2).asInstanceOf[java.math.BigDecimal].doubleValue())))
这里,鉴于计算能力,我就不进行全局用户的候选集推荐计算了,只拿ID=1的用户当成实验,获取ID=1的用户候选推荐列表,先取该用户的行为数据。
val personalRatingsData = ratingDataOrc.where("userid = 1").rdd.map{
f=>
Rating(java.lang.Integer.parseInt(f.get(0).toString),
java.lang.Integer.parseInt(f.get(1).toString),
f.get(2).asInstanceOf[java.math.BigDecimal].doubleValue())
}
基于上上面的K-V中间数据,我们以取余的方式,将数据分成6:2:2,三个比例,分别进行模型训练,数据校验,以及结果测试。
val training = ratings.filter(x => x._1 < 6).values
.union(personalRatingsData).repartition(numPartions).persist()
val validation = ratings.filter(x => x._1 >=6 && x._1 < 8).values
.repartition(numPartions).persist()
val test = ratings.filter(x => x._1 > 8).values.persist()
ALS的推荐效果评估,一般我们是以均方根差来离线衡量推荐的准确度,所以,这里涉及到了ALS参数调优的问题,我们通过数据来最终确定参数,并确定最终的Model,分别取ranks、lambdas、numIters作为调优对象。
var count = 0 //进行三层循环遍历,找最佳的Rmse值,对应的model for (rank <- ranks; lambda <- lambdas; numIter <- numIters) {
val model = ALS.train(training, rank, numIter, lambda)
//计算均根方差值,传入的是model以及校验数据
val validationRmse = computeRmse(model, validation, numValidation)
count += 1
//选取最佳值,均方根误差越小越OK
if (validationRmse < bestValidationRmse) {
bestModel = Some(model)
bestValidationRmse = validationRmse
bestLambda = lambda
bestRank = rank
bestNumIter = numIter
}
}
基于上面最终选择的参数,输出Model,我们基于这个模型,去做最后的推荐,注意需要去除ID=1的用户已经观看过的电影。
//推荐前十部最感兴趣的电影,注意需要剔除该用户(userid=1)已经评分的电影,即去重 val myRatedMovieIds = personalRatingsData.map(f=>f.product).collect().toSet
val candidates = movies.keys.filter(!myRatedMovieIds.contains())
//为用户1推荐十部movies,我们只做用户ID=1的推荐 val candRDD: RDD[(Int, Int)] = candidates.map((1, ))
val recommendations:RDD[Rating] = bestModel.get.predict(candRDD)
val recommendations = recommendations.collect().sortBy(-
.rating).take(20)
存储推荐的结果,主要Row需要先进行格式化。
//结果存储用户1的推荐结果
val alsBaseReDataFrame = sparkSession.sparkContext
.parallelize(recommendations_.map(f=> (f.user,f.product,f.rating)))
.map(f=>Row(f._1,f._2,f._3))
//DataFrame格式化申明
val schemaString = "userid movieid score"
val schemaAlsBase = StructType(schemaString.split(" ")
.map(fieldName=>StructField(fieldName,if (fieldName.equals("score")) DoubleType else IntegerType,true)))
val movieAlsBaseDataFrame = sparkSession.createDataFrame(alsBaseReDataFrame,schemaAlsBase)
//将结果存入hive
val itemBaseReTmpTableName = "mite_alsbasetmp"
val itemBaseReTableName = "mite8.mite_als_base_re"
movieAlsBaseDataFrame.registerTempTable(itemBaseReTmpTableName)
sparkSession.sql("insert into table " + itemBaseReTableName + " select * from " + itemBaseReTmpTableName)
最后再补上求均方根差的函数。
def computeRmse(model:MatrixFactorizationModel,data:RDD[Rating],n:Long):Double = {
//调用model的predict预测方法,把预测数据初始化model中,并且生成预测rating
val predictions:RDD[Rating] = model.predict((data.map(x => (x.user, x.product))))
val dataTmp = data.map(x => ((x.user, x.product), x.rating))
//通过join操作,把相同user-product的value合并成一个(double,double)元组,前者为预测值,后者为实际值
val predictionsAndRatings = predictions.map{
x => ((x.user, x.product), x.rating)
}.join(dataTmp).values
//均方根误差能够很好的反应出测量的精密度,对于偏离过大或者过小的测量值较为敏感
//计算过程为观测值与真实值偏差的平方,除于观测次数n,然后再取平方根
//reduce方法,执行的是值累加操作
math.sqrt(predictionsAndRatings.map(x => (x._1 - x._2) * (x._1 - x._2)).reduce( _ + _ )/n)
}
至此,整个代码逻辑就结束了,其实我们不难发现,被框架封装的算法,其实使用起来更加的简单,如果抛开校验以及优化模型的过程,总共代码都没有几行。
最后再补充一个点。
这里大家可能对为什么协同能够发现新物品,而基于用户兴趣的画像推荐不能,原则上说基于画像会将思维局限于画像兴趣的偏好内,但兴趣本身就会升级的,这是通过历史的单个用户的行为所不能推测的。
而基于协同不一样,他一方面考虑的用户的历史行为,另一方面他参考了该用户的周围协同的行为,而对于大部分人来说,共有的行为轨迹其实很多时候能够一定程度上体现用户的自我认知,以及认知升级的过程,这意味着物品之间的关联性本身就通过共有的用户行为天然关联,而协同就是要挖掘这种潜在的关联性,这无关物品之间的属性差异。
所以,从这个维度上说,协同是容易产生惊喜推荐的一种机制。
07 从推荐策略算法到系统,到数据架构,再到产品思维设计
接推荐系统系列的上一个章节,开篇2个章节我们从概念,从应用场景出发,大概的把推荐系统基础知识给大伙儿普及了一遍,接下来的三个章节,分别由浅到深,从理论到代码案例讲解几种不同推荐系统的策略或者推荐算法,看着整个系列从理论到案例,该有的都有了,其实不然,之前就有说过,推荐策略或者算法与推荐系统是有本质的不同的,而这一篇就是要把前面的东西进行收拢,从整体上进行收官。
虽然不再从策略维度再进一步深入,即这里不会再从理论到代码案例再深入讲策略,但是实际上推荐的策略是远不止如此的,并且从应用以及系统的角度来说,并没有说固定的策略可言。
** 0.7.1 推荐策略以及算法的百家齐放**
承上,我们讲了最基础的基于内容属性本身的相似关系进行针对物品的推荐,再到基于用户的兴趣属性进行推荐,再过渡到基于协同关系进行推荐,其实这些都算是推荐的策略,说的更技术点就是推荐的算法。
而推荐策略的想象力其实无限的,并不局限于某种固定的策略,只要从业务的角度走的通,其实都是可以的,当然具体的选择以及搭配问题,后面我会讲到。
我们来看已经归属于大腾讯的“起点中文网”的推荐。
image
PS:截图是我随意点击的一本小说《飞剑问道》,不重要,可以不care。
从他的推荐理由“喜欢这本书的人还喜欢”来看,显然是通过用户之间的阅读关联性来做的这次推荐,说的更通俗易懂点就是购物篮分析:买这个商品的人还经常一起买其他商品。
是不是逻辑关系很像?当然实际上到底是不是这种推荐策略,就需要起点的算法工程师出来讲话了,我个人只是从业务层往下进行追溯从而得出的结论。你看,我说的对不对,策略一层是没有定式,购物篮分析的逻辑依然是可以用在看文学站的推荐逻辑上,没毛病。
我们再来看一个策略,依然是腾讯的,腾讯社交广告一直对外宣称的技术“Lookalike”,翻译成技术语言就是“人群扩散算法”。简单的逻辑是,先找种子用户,然后基于用户画像和关系链(这是腾讯强项)挖掘相似用户,然后再将受众进行扩大。
具体示意图如下:
image
你觉得这是推荐?看着更像是广告投放,但广告的投放谁说不是一次信息主体的推荐呢?本质上应该是没有这么大的差距的,只是一个从业务的角度去描述,一个是更偏技术的角度的说法。
随着阿法狗诸如此类的人工智能应用的推广(造势),以及近几年计算能力的大幅提升,使得依赖于大计算能力的神经网络的相关算法得以大放光彩,基于神经网络的一些推荐算法或者策略也是一个大的研究方向。
综上,不必举过多的推荐策略或者算法例子了,核心想说的就是,其实策略层本身就是百花齐放的状态,甚至你随意光顾一些平台网站,都能遇到不同的策略和算法,甚至是搭配组合,没有限定的策略和算法层,只有不同不适应的应用场景,以及从策略到推荐系统层,其实还是有很多东西的。
0.7.2 从推荐策略算法到推荐系统
接上面的话题,从策略算法层到系统层,差的有哪些东西:
1.首先是策略并不等于系统,这是明确的,推荐的整体逻辑也未必是一种逻辑在里头,也可能是多种的策略组合。
2.其次,如何选择策略,如何组合策略,如何去评判,如何追踪效果,这点尤其重要。
3.对于整个系统级的支撑,在工程化,以及数据架构上如何去实现,才能支持上层的算法逻辑层。
4.产品层的策略对整个推荐系统的影响有多大。
如上四个问题都是从推荐系统的角度出发进行分析的,从这里我们知道,光知道策略或者推荐算法,是不是离推荐系统的构建还差那么好几个量级,02这个小节,我们先从1/2两个小点进行分析。
策略!=系统,这点是明确的,并且选择哪些策略去做推荐,本身就有严格的选择机制以及评判机制在里头的。
image
这张图片很有意思,是别人在脉脉上调侃各大大厂的推荐系统的话,挺有意思,另一方面也是可以侧面验证各大长的一些侧重点(不过有点为鹅厂说好话的嫌疑),不管,我们随拿一些实例来看看。
首先是豆瓣(当前主页是魔戒1的主页):
image
从直观的的角度讲,同类推荐的因素一定的在,比如《指环王》其他,比如哈利波特,加勒比海盗,但诸如大鱼、角斗士、勇敢的心、与狼共舞这些的逻辑就不得而知了。
从用户的角度上看,个人使用豆瓣电影也不算少,但几乎没有账号(但如果说没有账号就体系就推不准,那这个公司可以死球去了,有很多可以做类似唯一用户的判别的方式,诸如浏览器、电脑硬件地址、cookie等等),但从我的个人感知来看,推荐的效果一般般。
再换一个,这是某宝的(当前是一个iphoneX的购买主页):
image
诚如调侃所言,我吃完两馒头,再问我要不要两馒头,我搜索iphoneX,他问我要不要iphone从6到6s到8s,挨个问,也够累的,反正我是不喜欢这种格调。
再回到大腾讯,这是之前文章就涉及到的,腾讯视频的推荐:
image
当前页面为《海上牧云记》的播放页面,个人是腾讯视频的中长期会员,再看看他给我推荐的是什么?大部分都是类似的奇幻古装剧,而老实讲,我对这种剧着实不喜欢,拍的tmd的假,而我只是好奇点击进去的,So...
再说到腾讯的朋友圈广告的投放推荐,前段时间一直给我这种孩子都快打酱油的人推婚纱摄影,这是几个意思?
再多的例子这里就不举了,很多看其推荐的列表大致能猜测一些其推荐的策略重点,其实或多或少还真是与调侃的有一些相似之处,那从表面看起来大部分的推荐系统都不像那么高大上,问题出在哪,是他们的策略就是这么Low?是他们的算法工程师太菜?
其实核心问题在于推荐系统的评价机制,在实际的场景中,一切以效果评价为导向,将策略,甚至是组合推荐的策略往评价得分高的方向进行调整,对于整个系统来说才是有意义的,并不是说算法高深就一定好。
那么,具体什么是合理的评价方式呢,大部分来说都是为了让用户的挺溜时间加长,最直接的表现就是点击转化,但并不是完全绝对的。以百度的调侃为例,你要的是馒头,人家给你推荐的是馒头制造机。
这跟其商业模式是有一定的关联的,百度之前的核心就是关键词竞价广告,那么,他必然要衡量几个方面的东西,第一是关键词与搜索词的相关性,离太远太扯淡的不行;第二相关广告的竞价。
于是,他就需要衡量准与商业价值之间的关系了,所以,并不是一味地追求准确,而是追求在其中最佳的平衡点,能给百度带来最佳的广告收益转化。
再说其他的之前我所体验的推荐系统,他们就一定不准吗?或许以我个人的角度讲,他们推荐的并不是很符合我的口味,但是如果他们是从整体转化进行评判呢?这种机制是他们所有目前方案中的最佳转化方案,那为什么不能用?少数的个体badcase并不会影响整体的策略,也不用管low还是不low,抓住核心问题。
当然,不可否认的是,如果能满足所有的人的意愿认为它很准,又能让整体系统的转化做到最大化,那当然最好了。
所以,从推荐策略算法到推荐系统,最核心的一个东西就是评估机制,构建起完善并且合理的评估机制,有利于整体推荐系统的优化和改进。说到评价,那不得不说的就是AB测试了,一个好的推荐系统,一定是会带AB测试的,能够很好的进行策略对比,进行流量分配,效果展示等等。
0.7.2 推荐系统数据架构
前几天记得分享过朋友的一篇文章,核心就是讲推荐系统架构的。对于整个推荐系统来说,系统的架构设计会严重影响到整个系统的效果与上层应用的体验。
在第05个章节中,记得大致提到过基于用户画像推荐的短期兴趣与长期兴趣,其实长期兴趣的挖掘还好,基本基于离线的计算结果依然还是可行的,但是对于很多推荐机制来说,就是短期兴趣,更切确的说是你当前行为的兴趣表现。
这意味着,我需要实时的对你的浏览行为进行兴趣分析,然后实时的反馈给你推荐列表,这种机制看似简单脑残,但往往很有效,因为他足够实时,而在段时间内,人的注意力一般只会放在很垂直的某个点上,所以往往就很有效。
但是,看似简单的机制,对于这种需要支持实时分析,实时反馈的机制来说,架构的设计就是一个挑战。此外,在整体的系统构建过程中,你需要考虑算法逻辑层可迭代性的问题,即通过反馈数据能够不断的自动调整你的算法策略,从而让效果更佳,这些都是需要数据架构进行支撑的。
此外,就是上面说的AB测试,效果反馈机制,都是需要集成至整个推荐系统中,再有承接上层应用,你需要考虑好计算的效率与结果输出的效率问题,所以缓存的设计与缓存更新的机制又是个问题。
从整个架构层来说,其实是相对繁杂的,与我们之前所说的策略算法层,这是另外一个维度上的东西,需要我们从整体系统级别去考量。
归纳来说,其实从推荐系统架构设计的角度来说,需要考虑以下几个因素或者说有以下几个重要组成部分:
数据的输入层,承接特征处理层,作为算法策略层的输入。
推荐策略以及算法实现层,这就是我们说的百家齐放的那层。
基于推荐算法的结果候选集,进行策略组合、排序以及召回,最终系统层吐出去的是这部分的结果,而非算法策略层的直接结果。
推荐系统的数据反馈回收机制,不管是隐性的还是显性反馈。
AB测试分流机制。
基于反馈数据以及AB测试结果的算法层动态迭代部分,包括基础的推荐算法,以及排序召回等部分。
部分系统需要考虑实时数据的反馈流通应用问题。
至于说具体的推荐系统架构,相信大家随便一搜都能搜到很多架构图,可能略有偏差,但是个人感觉只要不违背如上的一些基本原则,大体上结合自身的场景去调整,是没有大毛病的,所以,这里就不给具体的架构逻辑图了。
0.7.3 从系统到产品策略层
说完推荐策略,再到推荐系统,再到系统架构,这些看似都是与数据、与算法严密相关的东西,单纯的以产品思维角度出发,你觉得在设计或者做一个推荐系统时,有什么需要考虑的吗?这个层面是很多技术人员很容易忽略的一部分。
其实只要用点心,就算不太care算法策略,也是大有可为的,以上面贴过的一张图为例。
image
我们来看他的左上角标题“喜欢这本书的人还喜欢”,其实这就是一种推荐解释,同理,我们可看亚马逊的推荐解释“买过这个商品的人还购买了”。
这就是所谓的推荐结果的可解释性,人往往信任一些可以解释、容易理解的东西,这也就是为何很多推荐系统都愿意去给出这种类似的推荐解释,因为这种行为可以提升可信度,而可信度可以增加用户的点击转化,所以,可信度也是推荐系统设计中的一个重要参考因素。
从上面这么多推荐案例中,我们不难发现一个共同特征,那就是右上角的“换一批”按钮,我们来思考一下这个按钮存在的意义。
任何一次用户点击这个换一批按钮,这就意味着我们收集到了用户的反馈行为,至于说这个反馈行为到底是正向的还是负向的,就得看具体分析了。比如一个用户一个推荐项都没有点,不断的切换推荐列表,直至放弃,这显然你的推荐列表不如人意,但该用户又是一个迫切需要推荐场景的用户。如果某个用户,在不断点击推荐项的同时,又在不断的切换列表,这意味着这个用户很乐意使用推荐的场景,并且推荐的列表还是可以的。怕就怕那种不点击,也不切换的用户,我们无法获取到更多的主动反馈了。
说到主动反馈,另外一个纯产品层的设计思路就是推荐项的主动反馈了,这点也是亚马逊首创,每个推荐项用户都可以打分,或者直接评判说喜欢与不喜欢。通过这种方式,不断的收集用户的喜好,然后融入策略算法层,才能让你的推荐系统更加的合理,体验更好。
所以,单纯的从产品层,也是有很多东西可以去研究的,对于整体推荐系统而言,他就是一个应用,无非是更偏向于数据、算法等维度的一个产品而已,我们可以从算法层去着手,也可以试图从产品层去优化。
07 总结补充
到这里,整个推荐长文基本就结束了,从整个文章的的逻辑我们知道,如果你要架设一个推荐系统,那么首先对于推荐系统的一些基本概念需要熟悉,然后了解不同的推荐策略,然后结合场景分析最佳的一些推荐策略算法,然后基于架构的考虑(不同层级的分离),搭建整个推荐系统,然后从产品的思维角度去优化,最终产出符合你业务特征的推荐系统。看着容易,其实操作起来还是有一定困难的。
推荐系统在一般业务规模小的时候,其实鸟用不大的,只有在业务有了一定规模之后,那么就到了锱铢必较的阶段了,使用推荐哪怕增加了5个点的增长转化,也是极好的,更何况可能远远不止呢。
目前很多主流推荐系统都是基于用户的画像、兴趣爱好推荐的(这是一种相对靠谱,又容易在大规模用户场景中使用的策略),你越是被他推荐的东西牵着走,你后续就会越陷入其中,最终导致了你所获取的信息一直都是圈定在某个范围内的,这就是所谓的“信息茧房”。
其实要形成信息茧房一方面是由于推荐机制导致的,另一方面跟场景也是有很大关系的,比如如果用户被你所推荐的东西所推动,那么就容易陷入这种状态,如果用户获取信息的渠道有多种(比如导航、搜索等等),那么就不那么容易。
典型如今日头条,如果在前期你不小心点击了一些比较low的内容,然后它就越给你推类似的文章,结果你越看,它就越推,于是你所看到的东西都是一大坨类似离谱八卦了。从直观的角度看,今日头条重度依赖于用户的阅读行为,而头条又是一个重推荐场景的产品,所以会相对容易陷入“信息茧房”的这种情况。
从目前看,头条解决这个问题的途径是,给出热度频道,这个逻辑一定程度上降低用户的兴趣偏爱分析,这样用户能够接触到信息面就会更广,进而促使用户能够调整其兴趣,不断的更新其兴趣。
单纯从转化的角度看来,短期内可能对于系统侧来说是正向的,因为他才不会关注到底是不是“信息茧房”,他只关注转化有没有提升,但长期来说,对于用户就是一种损害。所以,我们在考虑设计推荐策略算法的时候,多多少少都会考虑推荐的新颖性。
但新颖性这东西就是一个双刃剑,新的东西意味着不确定,不确定意味着可能低的转化,所以好的推荐系统一定是在确保你兴趣的同时,又会考虑新颖,并且这是一种顺其自然的推荐信息主体的过渡,构建起你偏好信息与新信息之间的关联性,让你同样有欲望去点击那些新的东西,不过这就很难是了。
转自:https://mp.weixin.qq.com/s/3UjZFfCcRLRYHEvhWtNFHQ
作者:daos
链接:https://www.jianshu.com/p/db181f4b5f2b
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。