67

附Spark案例 | 推05,论推荐系统之经典,还得数协同

 6 years ago
source link: http://mp.weixin.qq.com/s/VzZ8qjN3KqC7CkwaGa6yew
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案例 | 推05,论推荐系统之经典,还得数协同

Original HCY崇远 数据虫巢 2017-12-20 16:57 Posted on

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

“这张贴图如何?”

作者 | HCY崇远

本文为《数据虫巢,推荐系统系列》第五篇,也是案例实践的第三篇,不懂前情提要的,请关注公号『数据虫巢』(ID:blogchong),阅读《推01,你们是不是都感觉自己少了个推荐系统?》《推02,就算是非技术人员也都有必要了解的一些推荐系统常识》《推03,最最最简单的推荐系统是什么样的 | 附Spark实践案例》《附Spark案例 | 推04,融合了用户兴趣的推荐系统才更具个性》

接上一篇文章,我们大致Get到了一个点,那就是如果要达到推荐个性化的目的,核心还是用户的行为数据,只有用户各自的行为数据才能反馈其与其他人所不一样的特性,从而有针对性的进行推荐。按上个章节的原话,大致就是这样的:

实际上基于用户画像的个性化推荐依然是有缺陷的,比如他不会做用户兴趣的升级,而实际上一些知识本身就是具有一定的阶梯性的。

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

而我实际上是需要寻求大数据关联的信息,甚至是升级的信息,比如基于大数据的机器学习、数据挖掘相关的东西,这个机制是无法做到这一层的。

说白了其实就是基于用户画像的推荐,他无法发现新知识,所谓新知识就是,与你之前的兴趣爱好相对比,推荐的候选集永远圈定在你的兴趣标签维度内,做不到认知的升级,而实际上认知是会进行升级的,特别是随着你捕获的知识信息越多的情况下,你就越会对更上层的其他知识感兴趣,不断的深入下去。

而基于协同过滤的推荐,或多或少能解决一点这类问题,最起码能够结合本身用户的行为,让你触达新的知识信息,并且这种递进是通过协同关系得到的,意味着是大部分人的共同选择,所以还是具有一定合理性的。

01 协同过滤原理先过一遍

对于基于协同过滤的推荐,可谓是推荐系统中的经典推荐算法了,记得好像就是亚马逊推广出来的,然后大放光彩。协同过滤又分为基于用户的协同(UserCF)、基于物品的协同(ItemCF),以及基于模型的协同(ModelCF)。

基于用户的协同过滤推荐(UserCF)。

基于用户的协同过滤,即我们希望通过用户之间的关系来达到推荐物品的目的,于是,给某用户推荐物品,即转换为寻找为这个用户寻找他的相似用户,然后相似用户喜欢的物品,那么也可能是这个用户喜欢的物品(当然会去重)。

来看一个表格:

用户/物品

物品A

物品B

物品C

物品D

用户A

用户B

用户C

//其中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

用户B

用户C

//其中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模型,即矩阵分解;图模型,又称为社会网络图模型。

(1) 最近邻模型

最近邻模型,即使用用户的偏好信息,我们计算当前被推荐用户与其他用户的距离,然后根据近邻进行当前用户对于物品的评分预测。

典型如K最近邻模型,假如我们使用皮尔森相关系数,计算当前用户与其他所有用户的相似度sim,然后在K个近邻中,通过这些相似用户,预测当前用户对于每一个物品的评分,然后重新排序,最终推出M个评分最高的物品推荐出去。需要注意的是,基于近邻的协同推荐,较依赖当前被推荐用户的历史数据,这样计算出来的相关度才更准确。

(2) SVD矩阵分解

我们把用户和物品的对应关系可以看做是一个矩阵X,然后矩阵X可以分解为X=A*B。而满足这种分解,并且每个用户对应于物品都有评分,必定存在与某组隐含的因子,使得用户对于物品的评分逼近真实值,而我们的目标就是通过分解矩阵得到这些隐性因子,并且通过这些因子来预测还未评分的物品。

有两种方式来学习隐性因子,一为交叉最小二乘法,即ALS;而为随机梯度下降法。首先对于ALS来说,首先随机化矩阵A,然后通过目标函数求得B,然后对B进行归一化处理,反过来求A,不断迭代,直到A*B满足一定的收敛条件即停止。

对于随机梯度下降法来说,首先我们的目标函数是凹函数或者是凸函数,我们通过调整因子矩阵使得我们的目标沿着凹函数的最小值,或者凸函数的最大值移动,最终到达移动阈值或者两个函数变化绝对值小于阈值时,停止因子矩阵的变化,得到的函数即为隐性因子。

使用分解矩阵的方式进行协同推荐,可解释性较差,但是使用RMSE(均方根误差)作为评判标准,较容易评判。

并且,我们使用这种方法时,需要尽可能的让用户覆盖物品,即用户对于物品的历史评分记录需要足够的多,模型才更准确。

(3) 社会网络图模型

所谓社会网络图模型,即我们认为每个人之间都是有联系的,任何两个用户都可以通过某种或者多个物品的购买行为而联系起来,即如果一端的节点是被推荐用户,而另一端是其他用户,他们之间通过若干个物品,最终能联系到一起。

而我们基于社会网络图模型,即研究用户对于物品的评分行为,获取用户与用户之间的图关系,最终依据图关系的距离,为用户推荐相关的物品。

目前这种协同推荐使用的较少。

02 基于Spark的ALS协同过滤推荐案例

老规矩,大致过完了理论,我们来走一遭代码实践,数据源的解释不就多说了,依然还是那份电影数据,不清楚的见上一篇《推03,最最最简单的推荐系统是什么样的 | 附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)
}

至此,整个代码逻辑就结束了,其实我们不难发现,被框架封装的算法,其实使用起来更加的简单,如果抛开校验以及优化模型的过程,总共代码都没有几行。

最后再补充一个点。

这里大家可能对为什么协同能够发现新物品,而基于用户兴趣的画像推荐不能,原则上说基于画像会将思维局限于画像兴趣的偏好内,但兴趣本身就会升级的,这是通过历史的单个用户的行为所不能推测的。

而基于协同不一样,他一方面考虑的用户的历史行为,另一方面他参考了该用户的周围协同的行为,而对于大部分人来说,共有的行为轨迹其实很多时候能够一定程度上体现用户的自我认知,以及认知升级的过程,这意味着物品之间的关联性本身就通过共有的用户行为天然关联,而协同就是要挖掘这种潜在的关联性,这无关物品之间的属性差异。

所以,从这个维度上说,协同是容易产生惊喜推荐的一种机制。

03 写在最后

结合实际的数据,Spak工程代码,我们成功的从呆板的属性推荐过渡到基于用户画像的推荐,并为推荐附上了个性化的能力,再从基于用户画像的推荐再过渡到经典的推荐模型,协同过滤,让你的推荐系统能够发现新事物,产生惊喜推荐。

这个系列从开头到现在,已经有五篇了,从理论到实践,算是对得良心了,不过还未完,之前我们也说过,上溯三个实践,其实只能算是一种推荐机制,远不能达到推荐系统的程度,而推荐系统是一个复杂的工程,绝壁不是一个算法模型可以解决的,在一篇里,我们将会将思维发散,进一步透析什么是推荐系统,如何构建一个灵活多变的推荐系统。

此外,在这个篇幅中,我们只提供了ALS的大致思路,以及核心的代码片,但就以解释性来说,其实是基于物品以及基于用户的协同推荐比较容易理解的。如果你需要完整的代码工程、实验数据,以及基于用户、物品的协同过滤的实践代码,甚至是代码的详解,可以微信联系我(记得发我100大洋的红包,我不会客气的)。

关于我:

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

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

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

系列文章:

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

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

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

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

《推05,论推荐系统之经典,还得数协同 | 附Spark实践案例》(本文)

欢迎关注数据虫巢(ID:blogchong),这里有很多关于大数据的原创文章,如果你觉得文章有用,欢迎转发,也不介意你打赏一杯深夜写文的咖啡,谢谢(赏完了咖啡可以找我要代码文件,非工程包 哈哈)。

Image

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK