推荐算法+Spark MLlib代码Demo

引言

推荐算法在日常生活中无处不在,例如如今很多平台都会利用推荐引擎给用户推荐某些电影,如果你是科幻迷,那可能推荐给你的更多是科幻片,事实上科幻片也可以有很多种,如果算法更细致点的话,最后推荐给你的科幻片还可以是最符合你个人喜好的。所以推荐结果很准的话,那就会吸引用户持续使用这个平台的服务。当然实际上,推荐算法的应用并不限于电影,也可以是各种各样的物品。所以其实可以看出,推荐算法最终试图对用户与某些物品之间进行联系,从而建立起一个推荐模型,基于这个模型给用户进行推荐。


一、协同过滤

1.1 基本概念

协同过滤(Collaborative Filtering, 简称 CF)是推荐系统最为流行的一种实现思路,它是一种借助集体智慧的方法,即利用大量已有的用户偏好来估计用户对其未接触过的物品的喜好程度,其内在思想就是相似度的定义。

按电影推荐的例子来说,如果你现在想看一部电影,但你不知道具体看哪部,你会怎么做?大部分的人会问问周围的朋友,看看最近有什么好看的电影推荐,而我们一般更倾向于从口味比较类似的朋友那里得到推荐。这就是协同过滤的核心思想。

协同过滤可分为两种方法进行推荐:基于用户的方法、基于物品的方法。

1.2 基于用户(User-based)的协同过滤

在这种方法中,如果有一群用户对物品有相似的喜好,那可认为他们是类似的用户。要对他们其中的一个用户推荐未知物品,就可根据这群相似用户喜好计算出对各个物品的综合得分,并根据得分高低来推荐物品。

比如,你一直不知道有什么电影好看,那就可以去询问你身边跟你偏好差不多的朋友,让他们对一些你没看过的电影进行评分,你就可以基于这些评分进行一个综合判断,以决定最终要看哪一部电影。

用户/喜好电影 电影A 电影B 电影C 电影D
小李    
小赵    
小张  

如上表,假设小李作为被推荐的对象。从用户的角度来看(也就是行向量),可以很明显看到小李和小张的偏好更接近,因为他们都喜欢电影A和C(红色√)。那么也就可以推荐小张同样也喜欢的电影D给小李。

算法步骤:

  • 寻找与被推荐对象相似的用户,也就是需要计算用户的相似度
  • 得到由多个相似度组成的列表,按照从高到底进行排序
  • 将列表中排名靠前的那些用户作为推荐的参考对象,按照他们的喜好给被推荐对象推荐物品

1.3 基于物品(Item-based)的协同过滤

这种方法通常根据现有用户对物品的偏好或是评级情况,来计算物品之间的相似度。也就是说某种物品跟你喜欢的物品越相似,那么这种物品也越符合你的口味。

举个例子,你超喜欢《钢铁侠》系列,然后不知道看什么电影的时候,那基于《钢铁侠》的相似度来讲,《美国队长》也应该会是你所喜欢的。因为这两个系列的电影相似度很高:都是漫威宇宙的、都是科幻英雄大片等等。

用户/喜好电影 电影A 电影B 电影C 电影D
小李    
小赵    
小张  
小孙  
小王    

在这个表中,以小王为推荐对象。从电影的角度来看(列向量),电影A和C是最为相似的,因为它们同样被小李、小赵和小孙所喜欢(红色√)。而小王也喜欢电影A,那么对于相似的电影C应该也会有高分评价。

扫描二维码关注公众号,回复: 5898783 查看本文章

算法步骤:

  • 寻找相似的物品,也就是要计算物品间的相似度
  • 根据相似度,进一步预测被推荐用户对物品的评分

1.4 User-based和Item-based的对比

  • 最明显的区别:前者基于用户,需计算用户间的相似度;后者基于物品,需计算物品间的相似度。
  • User-based有两个缺陷:
    • (1)数据稀疏性,即矩阵上有很多零值(矩阵缺省部分一般赋为零值)。如:一个大型的电影评分平台会有大量的电影,而单独用户不可能去评价所有的电影。在“用户-电影”矩阵中,对于没有评价的那部分都为零值,这样用户向量(如上表中每一行)重叠可能性比较低,也就是说寻找相似的用户比较困难(上面例子中因为电影数目少,而且只是打钩不是评分,所以比较容易找到相似的用户,但是数据少肯定准确性也很低)
    • (2)算法扩展性。可想而知,如果每多一部电影,对于每个用户向量就会多一个元素,相似度计算量也会随着增加(为什么会增加?看下面的各种相似度计算公式就可知了),这样就不适合数据量大的情况了。
  • 物品直接的相似性相对就比较固定,所以可以预先在线下计算好不同物品之间的相似度,把结果存在列表中,当推荐时进行查表,计算用户可能的评分,那就可以同时解决上面两个问题。比如两部电影的相似度在最开始就确定了之后,不会因为添加多一个用户对这两部电影的不同评分,而影响到它们的相似度。

二、相似度计算

通过对协同过滤的基本介绍,可知推荐的一个关键点在于计算用户向量或者物品向量的相似度。那么下面就简要说明几种相似度的计算。

2.1 欧几里德距离(Euclidean Distance)

欧几里德距离也即欧式距离,表示在n维空间中两个点之间的距离。在这里两个n维向量即可认为是在n维空间的两个点,假设两个向量分别为(x_{1},x_{2},...,x_{n})(y_{1},y_{2},...,y_{n}),那么欧式距离则可表示为:

                               d(x,y) =\sqrt{(x_{1}-y_{1})^{2}+(x_{2}-y_{2})^{2}+...+(x_{n}-y_{n})^{2}}=\sqrt{\sum_{i=1}^{n}(x_{i}-y_{i})^{2}}

当用欧式距离表示相似度时,则换算为:sim(x,y)=\frac{1}{1+d(x,y)}

说明欧式距离越大,两个向量的相似度越小,。当两个向量完全重合,则距离为0,那么相似度即等于1。

2.2 皮尔逊相关系数(Pearson’s Correlation Coefficient)

皮尔逊相关系数可以衡量两个变量之间的相似度,公式可表示为:

                             p(x,y)=\frac{\sum x_{i}y_{i}-n\bar{xy}}{(n-1)S_{x}S_{y}}=\frac{n\sum x_{i}y_{i}-\sum x_{i}\sum y_{i}}{\sqrt{n\sum x_{i}^{2}-(\sum x_{i})^{2}}\sqrt{n\sum y_{i}^{2}-(\sum y_{i})^{2}}}

该公式度量了两个变量之间线性关系强度,是用协方差(对于协方差不懂的可以看该博客)除以两个变量的标准差得到的(S_{x} 和S_{y}分别代表xy的标准差),虽然协方差能反映两个随机变量的相关程度(协方差大于0的时候表示两者正相关,小于0的时候表示两者负相关),但其数值上受量纲的影响很大,不能简单地从协方差的数值大小给出变量相关程度的判断。为了消除这种量纲的影响,于是就有了相关系数的概念。

该式的取值范围为[-1,1],当p(x,y)> 0时,则两个向量正相关;当p(x,y)<0时,两个变量负相关,当p(x,y)=0,两个变量不具有相关性。

下图(源自《数据挖掘导论》)很形象表明了相关系数大小和相关性的关系:

               

2.3 余弦相似度(Cosine Similarity)

余弦相似度是两个向量在n维空间里两者夹角的度数。根据点积计算公式x\cdot y=\left \| x \right \|\left \| y \right \|cos\theta,可得余弦相似度C(x,y)为:

                                                        C(x,y)=cos\theta =\frac{x\cdot y}{\left \| x \right \|\left \| y \right \|}=\frac{\sum_{i}^{n} x_{i}y_{i}}{\sqrt{\sum_{i}^{n} x_{i}^{2}}\sqrt{\sum_{i}^{n} y_{i}^{2}}}

所以余弦相似度等于两个向量的点积与各向量范数乘积的商(向量范数类似于线段长度的概念,余弦相似度用的是L2范数)。该相似度的取值为[-1,1],当C(x,y)=1时,表示两个向量完全相似;当C(x,y)=0时,表示两个向量无相似性;当C(x,y)=-1时,不仅表示两者不相关,还表示它们性质完全相反。

2.4 欧式距离与余弦相似度的区别

欧式距离能够体现个体数值特征的绝对差异,所以更多的用于需要从维度的数值大小中体现差异的分析,如使用用户行为指标分析用户价值的相似度或差异。

余弦距离更多的是从方向上区分差异,而对绝对的数值不敏感,更多的用于使用用户对内容评分来区分兴趣的相似度和差异,同时修正了用户间可能存在的度量标准不统一的问题(因为余弦距离对绝对数值不敏感)。

借助三维坐标系来看下欧氏距离和余弦距离的区别:

                                                         

2.5 相似度的选择

当不同用户对不同商品评价标准的范围不一样时(比如有人觉得电影7分以上就是好看的,有人觉得电影9分以上才算好看的),使用皮尔逊相关系数;

当数据稠密,且属性值大小十分重要,使用欧式距离;

当数据稀疏,存在很多零值,考虑余弦相似度。


三、Spark MLlib推荐算法

如上对于协同过滤的讲解中,无论是基于用户还是物品的方法,它们最终评分取决于若干用户或是物品之间依据相似度所构成的集合(即邻居),这种构建出来的模型称为最近邻模型

而在Spark推荐模型库中,当前只包含基于矩阵分解(Matrix Factorization)的实现,由此我们也将重点关注这类模型,这类模型也在协同过滤中表现十分出色。

3.1 显式矩阵分解  

要找到和“用户-物品”矩阵近似的k维(低阶)矩阵,最终要求出如下两个矩阵:一个用于表示用户的U\times k维矩阵,以及一个表征物品的I \times k维矩阵。这两个矩阵也称作因子矩阵。它们的乘积便是原始评级矩阵的一个近似。值得注意的是,原始评级矩阵通常很稀疏,但因子矩阵却是稠密的。

特点:因子分解类模型的好处在于,一旦建立了模型,对推荐的求解便相对容易。但也有弊端,即当用户和物品的数量很多时,其对应的物品或是用户的因子向量可能达到数以百万计。这将在存储和计算能力上带来挑战。另一个好处是,这类模型的表现通常都很出色。

3.2 隐式矩阵分解

隐式模型(关联因子不确定,可能随时会变化)仍然会创建一个用户因子矩阵和一个物品因子矩阵。但是,模型所求解的是偏好矩阵而非评级矩阵的近似。类似地,此时用户因子向量和物品因子向量的点积所得到的分数也不再是一个对评级的估值,而是对某个用户对某一物品偏好的估值(该值的取值虽并不严格地处于0到1之间,但十分趋近于这个区间)

3.3 最小二乘法

最小二乘法(Alternating Least Squares    ALS)是解决矩阵分解的最优化方法。ALS的实现原理是迭代式求解一系列最小二乘回归问题。在每一次迭代时,固定用户因子矩阵或是物品因子矩阵中的一个,然后用固定的这个矩阵以及评级数据来更新另一个矩阵。之后,被更新的矩阵被固定住,再更新另外一个矩阵。如此迭代,直到模型收敛(或是迭代了预设好的次数)。

                            

3.4 基于用户的Spark MLlib代码

训练数据:用户ID  影片ID  星级  时间戳

196    242    3    881250949
186    302    3    891717742
22    377    1    878887116
244    51    2    880606923
166    346    1    886397596
298    474    4    884182806
115    265    2    881171488
253    465    5    891628467

......

package learn.recommend
import org.apache.spark.mllib.recommendation.{ALS, Rating}
import org.apache.spark.{SparkConf, SparkContext}
/**
  * 协同过滤最小二乘法demo,基于用户推荐:根据用户的相似度来为某个用户推荐物品
  */
object UserCFDemo {

  /**
    * 解析String,获取Rating
    * @param str
    * @return
    */
  def parseRating(str:String):Rating={
    val fields = str.split("\t")
    Rating(fields(0).toInt,fields(1).toInt,fields(2).toDouble)
  }

  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("UserCFDemo").setMaster("local[*]")
    val sc = new SparkContext(conf)
    //用户评价数据:用户ID  影片ID  星级  时间戳
    val ratingData = sc.textFile("E:\\test\\ml-100k\\u.data")
    //读取数据,生成RDD并转换成Rating对象
    val ratingsRDD = ratingData.map(parseRating(_))
    //隐藏因子数(也即隐藏特征,理论讲因子数越多效果越好,但太多也会造成训练模型和保存时所需的内存开销,所以一般可取50~200之间)
    val rank=50
    //最大迭代次数(每次迭代都能降低评价矩阵的重建误差,但一般经少数次迭代后ALS模型已能收敛成一个比较合理的好模型)
    val maxIter=10
    //正则化因子(该参数控制模型的正则化过程,从而控制模型的过拟合情况)
    val lambda=0.01
    //训练模型
    val model = ALS.train(ratingsRDD,rank,maxIter,lambda)

    //从电影ID到标题的映射
    val movies = sc.textFile("E:\\test\\ml-100k\\u.item")
    val titles = movies.map(line=>line.split("\\|")).map(array=>(array(0).toInt,array(1))).collectAsMap()

    //推荐物品(电影)数量
    val K=10
    //用户1
    val user1=66
    //推荐结果
    val topKRecs = model.recommendProducts(user1,K)
    println("用户"+user1)
    topKRecs.foreach(rec=>{
      val movie = titles(rec.product)
      val rating = rec.rating
      println(s"推荐电影:$movie ,预测评分:$rating")
    })
  }
}

上面代码假设为用户ID=66的用户推荐10部电影,推荐结果以及该用户对推荐电影的预测评分,并按预测评分从高到低排序如下所示:

3.5 基于物品的Spark MLlib代码

输入的训练数据跟上面一样

package learn.recommend
import org.apache.spark.mllib.recommendation.{ALS, Rating}
import org.apache.spark.{SparkConf, SparkContext}
import org.jblas.DoubleMatrix
/**
  * 协同过滤demo2,基于物品的推荐:根据物品的相似度给某个用户推荐物品
  */
object ItemCFDemo {

  def parseRating(str:String):Rating={
    val fields = str.split("\t")
    Rating(fields(0).toInt,fields(1).toInt,fields(2).toDouble)
  }

  /**
    * 计算两个向量的余弦相似度,1为最相似,0为不相似,-1为相反
    * 余弦相似度=向量的点积/各向量范数的乘积     值域为[-1,1]
    * @param vec1 向量1
    * @param vec2 向量2
    * @return
    */
  def cosineSimilarity(vec1:DoubleMatrix,vec2:DoubleMatrix)={
    vec1.dot(vec2)/(vec1.norm2()*vec2.norm2())
  }


  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local[*]").setAppName("ItemCFDemo")
    val sc = new SparkContext(conf)
    //构建ALS模型
    val ratingData = sc.textFile("E:\\test\\ml-100k\\u.data")
    val ratingsRDD = ratingData.map(parseRating(_))
    //四个参数:评级RDD、
    val model = ALS.train(ratingsRDD,50,10,0.01)

    //从电影ID到标题的映射
    val movies = sc.textFile("E:\\test\\ml-100k\\u.item")
    val titles = movies.map(line=>line.split("\\|")).map(array=>(array(0).toInt,array(1))).collectAsMap()

    //获取给定物品在模型中对应的因子,并构建成向量
    val itemId=567
    val itemFactor: Array[Double] = model.productFeatures.lookup(itemId).head
    val itemVector = new DoubleMatrix(itemFactor)

    //求各个物品的余弦相似度
    val sims = model.productFeatures.map {
      case (id, factor) =>
        val factorVector = new DoubleMatrix(factor)
        val sim = cosineSimilarity(factorVector, itemVector)
        (id, sim)
    }
    //取出余弦相似度最高的10个,即为跟给定物品最相似的10种物品
    val sortedSims: Array[(Int, Double)] = sims.top(10)(Ordering.by[(Int,Double),Double]{case (id,similarity)=>similarity})

    println("与"+titles(itemId)+"最为相似的10部电影:")
    sortedSims.map{case (id,sim)=>(titles(id),sim)}.foreach(tuple=>println("电影:"+tuple._1+",相似度:"+tuple._2))
  }
}

基于物品的推荐,就要找出一样相似的那些物品,如上代码为物品ID=567(代码也实现了电影ID到电影名称的映射,该电影名称为《Wes Craven's New Nightmare》)的电影找出相似的10部电影,并按照相似度从高到低排序,输出结果:


四、推荐模型效果的评估

如何知道训练出来的模型是一个好模型?这就需要某种方法来评估它的预测结果。

评估指标(evaluation metric)指那些衡量模型预测能力或准确度的方法,提供了同一模型在不同参数下,又或是不同模型之间进行比较的标准方法。通过这些指标,人们可以从待选的模型中找出表现最好的那个模型。

均方差(Mean Squared Error,MSE)直接衡量“用户-物品”评级矩阵的重建误差。它也是一些模型里所采用的的最小化目标函数,特别是许多矩阵分解类方法,比如ALS。因此,它常用于显式评级的情形。它的定义为各平方误差的和与总数目的商。其中平方误差是指预测到的评级与真实评级的差值的平方。公式:

                                                                  MSE=\frac{1}{n}\sum_{i=1}^{m}w_{i}(y_{i}-\hat{y_{i}})^2

均方根误差(Root Mean Squared Error,RMSE)的使用也很普遍,其计算只需在MSE上取平方根即可,它等同于求预计评级和实际评级的差值的标准差,即:

                                                           RMSE=\sqrt{MSE}=\sqrt{\frac{1}{n}\sum_{i=1}^{m}w_{i}(y_{i}-\hat{y_{i}})^2}

代码如下:

package learn.recommend
import learn.recommend.ItemCFDemo.parseRating
import org.apache.spark.mllib.evaluation.RegressionMetrics
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.mllib.recommendation.{ALS, Rating}

/**
  * 均方误差测试:均方误差越小,模型越好
  */
object MSEDemo {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local[*]").setAppName("ItemCFDemo")
    val sc = new SparkContext(conf)
    val ratingData = sc.textFile("E:\\test\\ml-100k\\u.data")
    val ratingsRDD = ratingData.map(parseRating(_))
    //训练ALS模型
    val model = ALS.train(ratingsRDD,50,10,0.01)


    val usersProducts = ratingsRDD.map{
      case Rating(user, product, rating)=>(user,product)
    }
    //获取所有预测评级
    val predictions = model.predict(usersProducts).map {
      case Rating(user, product, rating) => ((user, product), rating)
    }

    //所有真实评级
    val ratings = ratingsRDD.map{case Rating(user,product,rating)=>((user,product),rating)}

    //关联两个RDD,得到((user,product),(真实评级,预测评级)) RDD
    val ratingsAndPredictions = ratings.join(predictions)

//    val MSE = ratingsAndPredictions.map{
//      case ((user,product),(actual,predicted))=>
//        math.pow((actual-predicted),2)}.reduce(_+_)/ratingsAndPredictions.count

    val predictedAndTrue = ratingsAndPredictions.map {
      case ((user, product), (predicted, real)) => (predicted, real)
    }
    //求解MSE和RMSE
    val regressionMetrics = new RegressionMetrics(predictedAndTrue)
    val MSE = regressionMetrics.meanSquaredError
    val RMSE = regressionMetrics.rootMeanSquaredError

    println("Mean Squared Error="+MSE)
    println("Root Mean Squared Error="+RMSE)
  }
}

输出结果:

Mean Squared Error=0.0838857007108934
Root Mean Squared Error=0.2896302827932421

从定义可知,MSE(或RMSE)越小,则说明模型越好,越贴合实际。

猜你喜欢

转载自blog.csdn.net/qq_42267603/article/details/88667314
今日推荐