协同过滤推荐之基于近邻协同过滤(二)

(1)基于物品协同过滤的思想与原理

核心思想:
根据所有用户对物品或者信息的评价,发现物品和物品之间的相似度,然后根据用户的历史偏好信息将类似的物品推荐给该用户

在这里插入图片描述
基于物品协同过滤的原理:
在基于物品的协同过滤出现之前,信息过滤系统最常使用的是基于用户的协同过滤。基于用户的协同过滤首先计算相似用户,然后再根据相似用户的喜好推荐物品,这个算法有这么几个问题:

  • 用户数量往往比较大,计算起来非常吃力,成为瓶颈;
  • 用户的口味其实变化还是很快的,不是静态的,所以兴趣迁移问题很难反应出来;
  • 数据稀疏,用户和用户之间有共同的消费行为实际上是比较少的,而且一般都是一些热门物品,对发现用户兴趣帮助也不大。

和基于用户的不同,基于物品的协同过滤首先计算相似物品,然后再根据用户消费过、或者正在消费的物品为其推荐相似的,基于物品的算法怎么就解决了上面这些问题呢?

  • 首先,物品的数量,或者严格的说,可以推荐的物品数量往往少于用户数量;所以一般计算物品之间的相似度就不会成为瓶颈。
  • 其次,物品之间的相似度比较静态,它们变化的速度没有用户的口味变化快;所以完全解耦了用户兴趣迁移这个问题。
  • 最后,物品对应的消费者数量较大,对于计算物品之间的相似度稀疏度是好过计算用户之间相似度的。

基于物品协同过滤的构建过程

协同过滤最最依赖的是用户物品的关系矩阵,基于物品的协同过滤算法也不能例外,它的基本步骤是这样的:

  • 构建用户物品的关系矩阵,矩阵元素可以是用户的消费行为,也可以是消费后的评价,还可以是对消费行为的某种量化如时间、次数、费用等;
  • 假如矩阵的行表示物品,列表示用户的话,那么就两两计算行向量之间的相似度,得到物品相似度矩阵,行和列都是物品;
  • 产生推荐结果,根据推荐场景不同,有两种产生结果的形式。一种是为某一个物品推荐相关物品,另一种是在个人首页产生类似“猜你喜欢”的推荐结果。

(2)基于物品协同过滤的相似度计算

从用户物品关系矩阵中得到的物品向量长什么样子呢?我来给你描述一下

  • 它是一个稀疏向量;
  • 向量的维度是用户,一个用户代表向量的一维,这个向量的总共维度是总用户数量;
  • 向量各个维度的取值是用户对这个物品的消费结果,可以是行为本身的布尔值,也可以是消费行为量化如时间长短、次数多少、费用大小等,还可以是消费的评价分数;
  • 没有消费过的就不再表示出来,所以说是一个稀疏向量。
        user-1      user-2      user-3      user-4      user-5
movie-1 7           6           0           1           1             DenseVector(7,6,0,1,1)

movie-2 6           7           3           2           0             DenseVector(6,7,3,2,0)

movie-3 7           0           3           2           1

movie-4 4           4           1           3           2

movie-5 5           3           1           3           3

movie-6 4           4           0           4           3



movie-1:[7,6,0,1,1]
movie-2:[6,7,3,2,0]


余弦相似度 = 7*6 + 6*7 + 0*3 + 1*2 + 1*0 / 根号(7*7 + 6*6 + 1*1 +1*1) * 根号(6*6 + 7*7 + 3*3 + 2*2) = 0.93

接下来就是如何两两计算物品的相似度了,一般选择余弦相似度,当然还有其他的相似度计算法方法也可以。计算公式如下:
在这里插入图片描述
分母是计算两个物品向量的长度,求元素值的平方和再开方。分子是两个向量的点积,相同位置的元素值相乘再求和。

在这里插入图片描述

使用调整余弦相似度算法
物品中心化

把矩阵中的分数,减去的是物品分数的均值;先计算每一个物品收到评分的均值,然后再把物品向量中的分数减去对应物品的均值。这样做的目的是什么呢?去掉物品中铁杆粉丝群体的非理性因素,例如一个流量明星的电影,其脑残粉可能会集体去打高分,那么用物品的均值来中心化就有一定的抑制作用。

计算公式:
在这里插入图片描述
求平均分
在这里插入图片描述
均值化之后的分数
在这里插入图片描述
计算相似度
在这里插入图片描述

(3)基于物品协同过滤的评分预测策略

方式一:相似度加权汇总(离线计算场景)

这个公式描述一下,核心思想就和基于用户的推荐算法一样,用相似度加权汇总。

要预测一个用户u对一个物品i的分数,遍历用户u评分过的所有物品,假如一共有m个,每一个物品和待计算物品i的相似度乘以用户的评分,这样加权求和后,除以所有这些相似度总和,就得到了一个加权平均评分,作为用户u对物品i的分数预测。

和基于物品的推荐一样,我们在计算时不必对所有物品都计算一边,只需要按照用户评分过的物品,逐一取出和它们相似的物品出来就可以了。

这个过程都是离线完成后,去掉那些用户已经消费过的,保留分数最高的k个结果存储。当用户访问首页时,直接查询出来即可。

方式二: Slope One相关推荐算法(实时计算场景)【了解】

场景:
这类推荐不需要提前合并计算,当用户访问一个物品的详情页面时,或者完成一个物品消费的结果面,直接获取这个物品的相似物品推荐,就是“看了又看”或者“买了又买”的推荐结果了。

场景:
经典的基于物品推荐,相似度矩阵计算无法实时更新,整个过程都是离线计算的,而且还有另一个问题,相似度计算时没有考虑相似度的置信问题。例如,两个物品,他们都被同一个用户喜欢了,且只被这一个用户喜欢了,那么余弦相似度计算的结果是1,这个1在最后汇总计算推荐分数时,对结果的影响却最大。

Slope One算法针对这些问题有很好的改进。在2005年首次问世,Slope One算法专门针对评分矩阵,不适用于行为矩阵。Slope One算法计算的不是物品之间的相似度,而是计算的物品之间的距离,相似度的反面

场景:
该算法适用于物品更新不频繁,数量相对较稳定并且物品数目明显小于用户数的场景。依赖用户的用户行为日志和物品偏好的相关内容。

优势:
算法很简单,易于实现,执行效率高,同时推荐的准确性相对较高。Slope One算法是基于不同物品之间的评分差的线性算法,预测用户对物品评分的个性化算法。

在这里插入图片描述

(4)相似度算法详解

(4.1)相似度本质详解

推荐系统中,推荐算法分为两个门派,一个是机器学习派,另一个就是相似度门派。机器学习派是后起之秀,而相似度派则是泰山北斗,以致撑起来推荐系统的半壁江山。

近邻推荐顾名思义就是在地理位置上住得近。如果用户有个邻居,那么社交软件上把邻居推荐给他在直观上就很合理。这里说的近邻,并不一定只是在三维空间下的地理位置的近邻,在任意高维空间都可以找到近邻,尤其是当用户和物品的特征维度都很高时,要找到用户隔壁的邻居,就不是那么直观,需要选择好用适合的相似度度量办法。

近邻推荐的核心就是相似度计算方法的选择,由于近邻推荐并没有采用最优化思路,所以效果通常取决于矩阵的量化方式和相似度的选择。

推荐算法中的相似度门派,实际上有这么一个潜在假设:如果两个物体很相似,也就是距离很近,那么这两个物体就很容易产生一样的动作。

如果两篇新闻很相似,那么他们很容易被同一个人先后点击阅读,如果两个用户很相似,那么他们就很容易点击同一个新闻。这种符合直觉的假设,大部分时候很奏效。

属于另一门派的推荐算法一机器学习中,也有很多算法在某种角度看做是相似度度量。例如,逻辑回归或者线性回归中,一边是特征向量,另一边是模型参数向量,两者的点积运算,就可以看做是相似度计算,只不过其中的模型参数向量值并不是人肉指定的,而是从数据中由优化算法自动总结出来的。

在近邻推荐中,最常用的相似度是余弦相似度。然而可以选用的相似度并不只是余弦相似度,还有欧氏距离、皮尔逊相关度、自适应的余弦相似度

数据分类
在真正开始巡视相似度计算方法前,我先给你把度量对象做个简单分类。相似度计算对象是向量,或者叫做高维空间下的坐标,一个意思。那表示这个向量的数值就有两种:

  • 实数值;
  • 布尔值,也就是0或者1。

(4.2)欧氏距离、余弦相似度、皮尔逊相关度详解

(4.2.1)欧氏距离

欧氏距离,如名字所料,是一个欧式空间下度量距离的方法。两个物体,都在同一个空间下表示为两个点,假如叫做p和q,分别都是n个坐标。那么欧式距离就是衡量这两个点之间的距离,从p到q移动要经过的距离。欧式距离不适合布尔向量之间。
在这里插入图片描述
这个公式就是,每一个坐标上的取值相减,求平方和,最后输出方根。

如果我们将两个点分别记作(p1,p2,p3,p4.)和(q1,q2,q3,14,),则欧几里得距离的计算公式为:

二维平面上两点p(x1,y1)与q(x2,y2)
三维空间两点p(x1,y1,z1)与q(x2,y2,z2)
两个n维向量p(x11,x12,,,x1n)与q(x21,x22,,x2n)

在这里插入图片描述

(4.2.2)余弦相似度

大名鼎鼎的余弦相似度,度量的是两个向量之间的夹角,其实就是用夹角的余弦值来度量,所以名字叫余弦相似度。当两个向量的夹角为0度时,余弦值为1,当夹角为90度时,余弦值为0,为180度时,余弦值则为-1。

余弦相似度在度量文本相似度、用户相似度、物品相似度的时候都较为常用;但是在这里需要提醒你一点,余弦相似度的特点:它与向量的长度无关。因为余弦相似度计算需要对向量长度做归一化:

在这里插入图片描述
经过向量长度归一化后的相似度量方式,背后潜藏着这样一种思想:两个向量,只要方向一致,无论程度强弱,都可以视为“相似”。

比如,我用 140字的微博摘要了一篇5000字的博客内容,两者得到的文本向量可以认为方向一致,词频等程度不同,但是余弦相似度仍然认为他们是相似的。

在协同过滤中,如果选择余弦相似度,某种程度上更加依赖两个物品的共同评价用户数,而不是用户给予的评分多少。这就是由于余弦相似度被向量长度归一化后的结果。

(4.2.3)调整余弦相似度

余弦相似度对绝对值大小不敏感这件事,在某些应用上仍然有些问题

举个小例子,用户A对两部电影评分分别是1分和2分,用户B对同样这两部电影评分是5分和4分。用余弦相似度计算出来,两个用户的相似度达到0.98。这和实际直觉不符,用户A明显不喜欢这两部电影。
在这里插入图片描述
针对这个问题,对余弦相似度有个改进,改进的算法叫做调整的余弦相似度(Adjusted CosineSimilarity)。调整的方法很简单,就是先计算向量每个维度上的均值,然后每个向量在各个维度上都减去均值后,再计算余弦相似度。前面这个小例子,用调整的余弦相似度计算得到的相似度是 一1,呈现出两个用户口味相反,和直觉相符。

在这里插入图片描述

(4.2.4)皮尔逊相关度

皮尔逊相关度,实际上也是一种余弦相似度,不过先对向量做了中心化,向量p和q各自减去向量的均值后,再计算余弦相似度。

公式一:

在这里插入图片描述

皮尔逊相关度与修正余弦相似度的区别
在这里插入图片描述
两个公式中的分母不同,皮尔逊是对item中的i和j共同评分过的分数均值化;
调整余弦相似度是对item中的i和j各自评分过的分数均值化;

        user-1      user-2      user-3      user-4      user-5      均值
movie-1 7           6           0           1           1           3.75 

movie-2 6           7           3           2           0           4.5  

movie-3 7           0           3           2           1           3.25

均值中心化区别:

调整余弦相似度
movie-1:[3.25, 2.25, 0, -2.75, -2.75]
movie-2:[1.5, 2.5 , -1.5, -2.5, 0]

皮尔逊相关度(公式一)
movie-1:[3.25, 2.5, -2.75]
movie-2:[1.5, 2.5 , -2.5]

Spark官方API中也提供了pearson相关度计算方法,这个计算方法就是采用的公式二的计算。公式一与公式二的基本是相似的。当计算的的i和j不是用户共同评分的时候,结果不是很好
公式二:
在这里插入图片描述
在这里插入图片描述

(4.2.5)杰卡德相似度

杰卡德相似度,是两个集合的交集元素个数在并集中所占的比例。由于集合非常适用于布尔向量表示,所以杰卡德相似度简直就是为布尔值向量私人定做的。对应的计算方式是:
在这里插入图片描述

  • 分子是两个布尔向量做点积计算,得到的就是交集元素个数;
  • 分母是两个布尔向量做或运算,再求元素和。

余弦相似度适用于评分数据,杰卡德相似度适合用于隐式反馈数据。例如,使用用户的收藏行为,计算用户之间的相似度,杰卡德相似度就适合来承担这个任务。

(5)基于物品相似度开发

(5.1)余弦相似度计算

package com.similarity

import org.apache.spark.ml.recommendation.ALS.Rating
import org.apache.spark.mllib.linalg.distributed.{
    
    CoordinateMatrix, MatrixEntry}
import org.apache.spark.sql.{
    
    Row, SparkSession}

object CosItem {
    
    
  /**
   * 需求:基于物品协同过滤通过余弦相似度算法计算用户相似性
   * @param args
   */
  def main(args: Array[String]): Unit = {
    
    

    val spark: SparkSession = SparkSession
      .builder
      .appName(this.getClass.getName)
      .master("local[5]")
      .getOrCreate

    // 设置日志等级
    spark.sparkContext.setLogLevel("WARN")

    import spark.implicits._
    val movieRatings = spark.read.textFile("/Users/caizhengjie/Desktop/大数据项目实战三/movieLess/utou_main.csv")
      .map(line => {
    
    
        val field = line.split(",")
        Rating(field(0).toInt,field(1).toInt,field(2).toFloat)
      }).toDF("user","item","rate")

    movieRatings.show()

    /**
     * userid、movieid、rate
     * +----+----+----+
     * |user|item|rate|
     * +----+----+----+
     * |   1|   1| 7.0|
     * |   1|   2| 6.0|
     * |   1|   3| 7.0|
     * |   1|   4| 4.0|
     * |   1|   5| 5.0|
     * |   1|   6| 4.0|
     * |   2|   1| 6.0|
     * ....
     */

    // 通过分布式矩阵计算相似性
    // 将df转换成矩阵rdd
    val matrixRdd = movieRatings.rdd.map{
    
    
      case Row(user:Int,item:Int,rate:Float) =>
        MatrixEntry(user.toLong,item.toLong,rate.toDouble)
    }

    matrixRdd.foreach(println)

    /**
     * MatrixEntry(1,1,7.0)
     * MatrixEntry(1,2,6.0)
     * MatrixEntry(1,3,7.0)
     * MatrixEntry(1,4,4.0)
     * MatrixEntry(1,5,5.0)
     * MatrixEntry(1,6,4.0)
     * MatrixEntry(2,1,6.0)
     * MatrixEntry(2,2,7.0)
     * ...
     */

    // 转换成坐标矩阵
    val coordinateMatrix = new CoordinateMatrix(matrixRdd)

    /**
     * user/item
     * 1  7 6 7 4 5 4
     * 2  6 7 0 4 3 4
     * 3  0 3 3 1 1 0
     * .....
     */

    // 由于需要计算物品的相似度即基于列的相似性计算,所以需要将行列置换再计算余弦相似度
    val cosinsim = coordinateMatrix.toIndexedRowMatrix().columnSimilarities().entries
    cosinsim.foreach(println)

    /**
     * MatrixEntry(1,4,0.9010239053216177)
     * MatrixEntry(4,5,0.9721300918624586)
     * MatrixEntry(3,5,0.813372806253492)
     * MatrixEntry(1,6,0.8378281682565331)
     * MatrixEntry(2,3,0.6999699192944447)
     * ...
     */


    // 将计算余弦相似度结果主换成df
    val res_df = cosinsim.map(x => {
    
    
      (x.i,x.j,x.value)
    }).toDF("item_id","sim_item_id","score")

    res_df.where("item_id = 1").sort(res_df("score").desc).show(false)

    /**
     * +-------+-----------+------------------+
     * |item_id|sim_item_id|score             |
     * +-------+-----------+------------------+
     * |1      |2          |0.9313775963969384|
     * |1      |4          |0.9010239053216177|
     * |1      |5          |0.86886929617049  |
     * |1      |6          |0.8378281682565331|
     * |1      |3          |0.7023821120100268|
     * +-------+-----------+------------------+
     */

  }

}

计算结果:

+-------+-----------+------------------+
|item_id|sim_item_id|score             |
+-------+-----------+------------------+
|1      |2          |0.9313775963969384|
|1      |4          |0.9010239053216177|
|1      |5          |0.86886929617049  |
|1      |6          |0.8378281682565331|
|1      |3          |0.7023821120100268|
+-------+-----------+------------------+

(5.2)调整余弦相似度计算

package com.similarity

import org.apache.spark.ml.recommendation.ALS.Rating
import org.apache.spark.mllib.linalg.distributed.{
    
    CoordinateMatrix, MatrixEntry}
import org.apache.spark.sql.{
    
    Row, SparkSession}

object CosItermOpt {
    
    
  /**
   * 需求:基于物品协同过滤通过调整余弦相似度算法计算用户相似性
   * @param args
   */
  def main(args: Array[String]): Unit = {
    
    

    val spark: SparkSession = SparkSession
      .builder
      .appName(this.getClass.getName)
      .master("local[5]")
      .getOrCreate

    // 设置日志等级
    spark.sparkContext.setLogLevel("WARN")

    import spark.implicits._
    val movieRatings = spark.read.textFile("/Users/caizhengjie/Desktop/大数据项目实战三/movieLess/utou_main.csv")
      .map(line => {
    
    
        val field = line.split(",")
        Rating(field(0).toInt,field(1).toInt,field(2).toFloat)
      }).toDF("user","item","rate")

    // 数据源
    movieRatings.show()

    /**
     * userid、movieid、rate
     * +----+----+----+
      *|user|item|rate|
      *+----+----+----+
      *|   1|   1| 7.0|
      *|   1|   2| 6.0|
      *|   1|   3| 7.0|
      *|   1|   4| 4.0|
      *|   1|   5| 5.0|
      *|   1|   6| 4.0|
      *|   2|   1| 6.0|
     *....
     */


    // 均值中心化
    // 计算物品的平均评分
    val itemmean = movieRatings.groupBy("item").mean("rate").toDF("item","mean_rate")
    itemmean.show(false)

    /**
     * +----+---------+
     * |item|mean_rate|
     * +----+---------+
     * |1   |3.75     |
     * |6   |3.75     |
     * |3   |3.25     |
     * |5   |3.0      |
     * |4   |2.8      |
     * |2   |4.5      |
     * +----+---------+
     */


    // 将两个表合并
    val item_movie_rating = movieRatings.join(itemmean,"item").toDF("item","user","rate","mean_rate")
    item_movie_rating.show()

    /**
     * +----+----+----+---------+
     * |item|user|rate|mean_rate|
     * +----+----+----+---------+
     * |   1|   1| 7.0|     3.75|
     * |   2|   1| 6.0|      4.5|
     * |   3|   1| 7.0|     3.25|
     * |   4|   1| 4.0|      2.8|
     * |   5|   1| 5.0|      3.0|
     * |   6|   1| 4.0|     3.75|
     * |   1|   2| 6.0|     3.75|
     * ...
     */

    item_movie_rating.createOrReplaceTempView("ratings")

    // 通过spark sql均值中心化
    val res_movie_rating = spark.sql("select user,item,(rate - mean_rate) as rate from ratings")
    res_movie_rating.show()

    /**
     * +----+----+-------------------+
     * |user|item|               rate|
     * +----+----+-------------------+
     * |   1|   1|               3.25|
     * |   1|   2|                1.5|
     * |   1|   3|               3.75|
     * |   1|   4| 1.2000000000000002|
     * |   1|   5|                2.0|
     * |   1|   6|               0.25|
     * |   2|   1|               2.25|
     * ...
     */


    // 通过分布式矩阵计算相似性
    // 将df转换成矩阵rdd
    val matrixRdd = res_movie_rating.rdd.map{
    
    
      case Row(user:Int,item:Int,rate:Double) =>
        MatrixEntry(user.toLong,item.toLong,rate.toDouble)
    }

    matrixRdd.foreach(println)

    /**
     * MatrixEntry(1,1,3.25)
     * MatrixEntry(1,2,1.5)
     * MatrixEntry(1,3,3.75)
     * MatrixEntry(1,4,1.2000000000000002)
     * MatrixEntry(1,5,2.0)
     * MatrixEntry(1,6,0.25)
     * MatrixEntry(2,1,2.25)
     * ....
     */

    // 转换成坐标矩阵
    val coordinateMatrix = new CoordinateMatrix(matrixRdd)

    /**
     * user/item
     * 1  3.25 1.5 3.75 ...
     * 2 .....
     */

    // 由于需要计算物品的相似度即基于列的相似性计算,所以需要将行列置换再计算余弦相似度
    val cosinsim = coordinateMatrix.toIndexedRowMatrix().columnSimilarities().entries
    cosinsim.foreach(println)

    /**
     * MatrixEntry(1,4,0.5705279590697938)
     * MatrixEntry(4,5,0.8134892168199606)
     * MatrixEntry(3,5,0.6209204205650662)
     * MatrixEntry(1,6,0.5726371269248889)
     * MatrixEntry(2,3,0.4858468171093143)
     * MatrixEntry(2,4,0.651057165328784)
     * ...
     */


    // 将计算余弦相似度结果主换成df
    val res_df = cosinsim.map(x => {
    
    
      (x.i,x.j,x.value)
    }).toDF("item_id","sim_item_id","score")

    res_df.where("item_id = 1").sort(res_df("score").desc).show(false)

    /**
     * +--------+------------+-------------------+
     * |item_id |sim_item_id |score              |
     * +--------+------------+-------------------+
     * |1       |3           |0.8635228492837839 |
     * |1       |2           |0.7599373542607082 |
     * |1       |6           |0.5726371269248889 |
     * |1       |4           |0.5705279590697938 |
     * |1       |5           |0.41442486349116986|
     * +--------+------------+-------------------+
     */

  }

}

计算结果:

+--------+------------+-------------------+
|item_id |sim_item_id |score              |
+--------+------------+-------------------+
|1       |3           |0.8635228492837839 |
|1       |2           |0.7599373542607082 |
|1       |6           |0.5726371269248889 |
|1       |4           |0.5705279590697938 |
|1       |5           |0.41442486349116986|
+--------+------------+-------------------+

(5.3)皮尔逊相关度计算

(5.3.1)使用皮尔逊相关度公式二API计算(非共同评分)

package com.similarity

import org.apache.spark.ml.recommendation.ALS.Rating
import org.apache.spark.mllib.linalg.distributed.{
    
    CoordinateMatrix, MatrixEntry}
import org.apache.spark.mllib.stat.Statistics
import org.apache.spark.sql.{
    
    Row, SparkSession}

import scala.collection.mutable.ListBuffer

object PearsonItem {
    
    
  /**
   * 需求:基于物品协同过滤通过皮尔逊相似度算法计算用户相似性
   * @param args
   */
  def main(args: Array[String]): Unit = {
    
    

    val spark: SparkSession = SparkSession
      .builder
      .appName(this.getClass.getName)
      .master("local[5]")
      .getOrCreate

    // 设置日志等级
    spark.sparkContext.setLogLevel("WARN")

    import spark.implicits._
    val movieRatings = spark.read.textFile("/Users/caizhengjie/Desktop/大数据项目实战三/movieLess/utou_main.csv")
      .map(line => {
    
    
        val field = line.split(",")
        Rating(field(0).toInt,field(1).toInt,field(2).toFloat)
      }).toDF("user","item","rate")

    movieRatings.show()

    /**
     * userid、movieid、rate
     * +----+----+----+
     * |user|item|rate|
     * +----+----+----+
     * |   1|   1| 7.0|
     * |   1|   2| 6.0|
     * |   1|   3| 7.0|
     * |   1|   4| 4.0|
     * |   1|   5| 5.0|
     * |   1|   6| 4.0|
     * |   2|   1| 6.0|
     * ....
     */

    // 通过分布式矩阵计算相似性
    // 将df转换成矩阵rdd
    val matrixRdd = movieRatings.rdd.map{
    
    
      case Row(user:Int,item:Int,rate:Float) =>
        MatrixEntry(user.toLong,item.toLong,rate.toDouble)
    }

    matrixRdd.foreach(println)

    /**
     * MatrixEntry(1,1,7.0)
     * MatrixEntry(1,2,6.0)
     * MatrixEntry(1,3,7.0)
     * MatrixEntry(1,4,4.0)
     * MatrixEntry(1,5,5.0)
     * MatrixEntry(1,6,4.0)
     * MatrixEntry(2,1,6.0)
     * MatrixEntry(2,2,7.0)
     * ...
     */

    // 转换成坐标矩阵
    val coordinateMatrix = new CoordinateMatrix(matrixRdd)

    /**
     * user/item
     * 1  7 6 7 4 5 4
     * 2  6 7 0 4 3 4
     * 3  0 3 3 1 1 0
     * .....
     */

    // 转换成行索引矩阵
    val indexRowMatrix = coordinateMatrix.toIndexedRowMatrix()

    // 转换成向量
    val pearsonVector = indexRowMatrix.rows.toJavaRDD().rdd.map(x => x.vector)
    pearsonVector.foreach(println)
    /**
     * (7,[1,2,3,4,5,6],[1.0,2.0,2.0,3.0,3.0,4.0])
     * (7,[1,2,3,4,5,6],[7.0,6.0,7.0,4.0,5.0,4.0])
     * (7,[2,3,4,5],[3.0,3.0,1.0,1.0])
     * (7,[1,3,4,5,6],[1.0,1.0,2.0,3.0,3.0])
     * (7,[1,2,4,5,6],[6.0,7.0,4.0,3.0,4.0])
     */

    // 通过稀疏向量来计算相似性
    val pearsonSim = Statistics.corr(pearsonVector)

    println(pearsonSim)

    /**
     * 1.0  NaN                 NaN                  ... (7 total)
     * NaN  1.0                 0.8569515946486446   ...
     * NaN  0.8569515946486446  1.0                  ...
     * NaN  0.3712165082817225  0.26336216566171755  ...
     * NaN  0.8875895031209077  0.7054762001861277   ...
     * NaN  0.7637626158259733  0.368160520255362    ...
     * NaN  0.6236095644623235  0.3006018060210759   ...
     */

    val ma = pearsonSim.asML.toSparse;
    val nums = pearsonSim.asML.toSparse.numCols
    val list = new ListBuffer[Array[Double]]

    // 遍历与item为1的相似性结果
    for (x <- 1 until nums){
    
    
      list.append(Array(1,x,ma.apply(1,x)))
    }

    // 将结果生成df
    val res_df = list.map(x => {
    
    
      (x(0),x(1),x(2))
    }).toDF("item_id","sim_item_id","score")

    res_df.sort(res_df("score").desc).show(false)


    /**
     * +-------+-----------+------------------+
     * |item_id|sim_item_id|score             |
     * +-------+-----------+------------------+
     * |1.0    |1.0        |1.0               |
     * |1.0    |4.0        |0.8875895031209077|
     * |1.0    |2.0        |0.8569515946486446|
     * |1.0    |5.0        |0.7637626158259733|
     * |1.0    |6.0        |0.6236095644623235|
     * |1.0    |3.0        |0.3712165082817225|
     * +-------+-----------+------------------+
     */

计算出来的结果:

调整余弦相似度算法
+-------+-----------+-------------------+
|item_id|sim_item_id|score              |
+-------+-----------+-------------------+
|1      |3          |0.8635228492837839 |
|1      |2          |0.7599373542607082 |
|1      |6          |0.5726371269248889 |
|1      |4          |0.5705279590697938 |
|1      |5          |0.41442486349116986|
+-------+-----------+-------------------+

皮尔逊相关度算法
+-------+-----------+------------------+
|item_id|sim_item_id|score             |
+-------+-----------+------------------+
|1.0    |1.0        |1.0               |
|1.0    |4.0        |0.8875895031209077|
|1.0    |2.0        |0.8569515946486446|
|1.0    |5.0        |0.7637626158259733|
|1.0    |6.0        |0.6236095644623235|
|1.0    |3.0        |0.3712165082817225|
+-------+-----------+------------------+

似乎计算出来的结果有很大差异,在调整余弦相似度中1和3是最相似的,但是在皮尔逊相似度汇总1和3是最不相似的。因为Spark官方API中也提供了pearson相关度计算方法,这个计算方法就是采用的公式二的计算。公式一与公式二的基本是相似的。当计算的i和j不是用户共同评分的时候,结果不是很好

(5.3.2)使用皮尔逊相关度公式二计算(共同评分)

前面了解到使用Spark官方API中提供了pearson相关度计算有很大的误差,是因为使用的API用的是皮尔逊相似度计算公式二计算,没有做共同评分,因此要想提高计算相似度,我们需要修整向量的数据,取出共同的评分,再通过公式二计算。
在这里插入图片描述

行:movie_id 列:user_id value:rating
(1,(6,[1,2,4,5],[7.0,6.0,1.0,1.0]))
(3,(6,[1,3,4,5],[7.0,3.0,2.0,1.0]))

由此可见movie1中用户3没有做评分,movie3中用户2没有做评分

即可求出movie1和movie3公共评分
1,[1,4,5],[7,1,1]
3,[1,4,5],[7,2,1]


演变过程
movie1
(6,[1,2,4,5],[7.0,6.0,1.0,1.0])            -> Vector(1,2,4,5)                                                                             -> Vector(7.0,6.0,1.0,1.0)
                                                                 求出两个向量的交集 -> Vector(1,2,4,5) -> 通过遍历可以分别求出对应的值也就是共同评分                                在计算两个向量的相似度得出结果
movie4
(4,(6,[1,2,3,4,5],[4.0,4.0,1.0,3.0,2.0]))  -> Vector(1,2,3,4,5)                                                                           -> Vector(4.0, 4.0, 3.0, 2.0)

代码开发:

package com.similarity

import breeze.numerics.pow
import org.apache.spark.ml.recommendation.ALS.Rating
import org.apache.spark.mllib.linalg.distributed.{
    
    CoordinateMatrix, MatrixEntry}
import org.apache.spark.sql.{
    
    Row, SparkSession}

import scala.collection.mutable.ListBuffer

object PearsonTwoItemOpt {
    
    
  /**
   * 需求:基于物品协同过滤通过皮尔逊相似度算法(公式二:共同评分)计算用户相似性
   * @param args
   */
  def main(args: Array[String]): Unit = {
    
    

    val spark: SparkSession = SparkSession
      .builder
      .appName(this.getClass.getName)
      .master("local[5]")
      .getOrCreate

    // 设置日志等级
    spark.sparkContext.setLogLevel("WARN")

    import spark.implicits._
    val movieRatings = spark.read.textFile("/Users/caizhengjie/Desktop/大数据项目实战三/movieLess/utou_main.csv")
      .map(line => {
    
    
        val field = line.split(",")
        Rating(field(0).toInt,field(1).toInt,field(2).toFloat)
      }).toDF("user","item","rate")

    movieRatings.show()

    /**
     * userid、movieid、rate
     * +----+----+----+
     * |user|item|rate|
     * +----+----+----+
     * |   1|   1| 7.0|
     * |   1|   2| 6.0|
     * |   1|   3| 7.0|
     * |   1|   4| 4.0|
     * |   1|   5| 5.0|
     * |   1|   6| 4.0|
     * |   2|   1| 6.0|
     * ....
     */

    // 通过分布式矩阵计算相似性
    // 将df转换成矩阵rdd
    val matrixRdd = movieRatings.rdd.map{
    
    
      case Row(user:Int,item:Int,rate:Float) =>
        MatrixEntry(user.toLong,item.toLong,rate.toDouble)
    }

    matrixRdd.foreach(println)

    /**
     * MatrixEntry(1,1,7.0)
     * MatrixEntry(1,2,6.0)
     * MatrixEntry(1,3,7.0)
     * MatrixEntry(1,4,4.0)
     * MatrixEntry(1,5,5.0)
     * MatrixEntry(1,6,4.0)
     * MatrixEntry(2,1,6.0)
     * MatrixEntry(2,2,7.0)
     * ...
     */

    // 转换成坐标矩阵
    val coordinateMatrix = new CoordinateMatrix(matrixRdd)

    /**
     * user/item
     * 1  7 6 7 4 5 4
     * 2  6 7 0 4 3 4
     * 3  0 3 3 1 1 0
     * .....
     */

    // 转换成行索引矩阵,并做行列转换
    val indexRowMatrix = coordinateMatrix.transpose().toIndexedRowMatrix()

    /**
     * 行:movie_id 列:user_id value:rating
     * (4,(6,[1,2,3,4,5],[4.0,4.0,1.0,3.0,2.0]))
     * (1,(6,[1,2,4,5],[7.0,6.0,1.0,1.0]))
     * (6,(6,[1,2,4,5],[4.0,4.0,4.0,3.0]))
     * (3,(6,[1,3,4,5],[7.0,3.0,2.0,1.0]))
     * (5,(6,[1,2,3,4,5],[5.0,3.0,1.0,3.0,3.0]))
     * (2,(6,[1,2,3,4],[6.0,7.0,3.0,2.0]))
     */

    // 过滤出movie1的向量 -> (1,(6,[1,2,4,5],[7.0,6.0,1.0,1.0]))
    val myVector = indexRowMatrix.rows.filter(_.index == 1).map(x => {
    
    
      (x.index,x.vector)
    }).first()

    println(myVector._2.toString)

    // 通过自定义皮尔逊算法公式二计算出与movie1相似的电影
    val result_df = indexRowMatrix.rows.toJavaRDD().rdd.map(x => {
    
    

      // 求出movie1的user向量Vector(1, 2, 4, 5)
      val my_index_vector = myVector._2.toSparse.indices.toVector
      // 求出movie1的向量(6,[1,2,4,5],[7.0,6.0,1.0,1.0])
      val my_value_vector = myVector._2

      // 求出其他电影的user向量Vector 例如:movie4 的user Vector(1, 2, 3, 4, 5)
      val other_index_vector = x.vector.toSparse.indices.toVector
      // 求出其他电影的向量 例如:movie4 的 (6,[1,2,3,4,5],[4.0,4.0,1.0,3.0,2.0])
      val other_value_vector = x.vector

      // 求两个user向量的交集
      val index_vector_all = my_index_vector.intersect(other_index_vector)
      // println(index_vector_all.toString())

      /**
       * Vector(1, 2, 4, 5) 与movie的user Vector的交集
       * Vector(1, 2, 4, 5)
       * Vector(1, 2, 4, 5)
       * Vector(1, 4, 5)
       * Vector(1, 2, 4, 5)
       * Vector(1, 2, 4)
       */


      /**
       * 演变过程
       * movie1
       * (6,[1,2,4,5],[7.0,6.0,1.0,1.0])            -> Vector(1,2,4,5)                                                                             -> Vector(7.0,6.0,1.0,1.0)
       *                                                                  求出两个向量的交集 -> Vector(1,2,4,5) -> 通过遍历可以分别求出对应的值也就是共同评分                                在计算两个向量的相似度得出结果
       * movie4
       * (4,(6,[1,2,3,4,5],[4.0,4.0,1.0,3.0,2.0]))  -> Vector(1,2,3,4,5)                                                                           -> Vector(4.0, 4.0, 3.0, 2.0)
       */
      val my_list = new ListBuffer[Double]
      val other_list = new ListBuffer[Double]

      // 通过两个user向量的交集遍历可以分别求出对应的值也就是共同评分
      for (index <-  index_vector_all.indices){
    
    
        my_list.append(my_value_vector(index_vector_all(index)))
        other_list.append(other_value_vector(index_vector_all(index)))
      }

      val my_res_vector = my_list.toVector
      val other_res_vector = other_list.toVector

      // 以movie1和movie4为例,my_res_vector:Vector(7.0,6.0,1.0,1.0) other_res_vector:Vector(4.0, 4.0, 3.0, 2.0)
      println(my_res_vector.toString())
      println(other_res_vector.toString)

      // 生成计算出结果表df
      (myVector._1 , x.index,pearsonCorrelationSimilarity(my_res_vector,other_res_vector))
    }).toDF("item_id","sim_item_id","score")

    result_df.sort(result_df("score").desc).show(false)

    /**
     * +-------+-----------+------------------+
     * |item_id|sim_item_id|score             |
     * +-------+-----------+------------------+
     * |1      |1          |1.0               |
     * |1      |3          |0.987829161147262 |
     * |1      |2          |0.9406341620035445|
     * |1      |4          |0.8971499589146108|
     * |1      |5          |0.6767529681839596|
     * |1      |6          |0.5726371269248889|
     * +-------+-----------+------------------+
     */

  }

  /**
   * 重写皮尔逊算法公式二
   * @param vect1
   * @param vect2
   * @return
   */
  def pearsonCorrelationSimilarity(vect1 : Vector[Double], vect2 : Vector[Double]) : Double = {
    
    

    /**
     * 求vect1和vect2的和
     * 以以movie1和movie4为例:
     * vect1.sum = Vector(7.0,6.0,1.0,1.0).sum = 15
     * vect2.sum = Vector(4.0, 4.0, 3.0, 2.0).sum = 13
     */
    val sum_vect1 = vect1.sum
    val sum_vect2 = vect2.sum

    /**
     * 求vect1和vect2的平方和
     * 以以movie1和movie4为例:
     * vect1.map(x => x * x).sum = Vector(7.0,6.0,1.0,1.0).map(x => x * x).sum = 87
     * vect2.map(x => x * x).sum = Vector(4.0, 4.0, 3.0, 2.0).map(x => x * x).sum = 45
     */
    val square_sum_vect1 = vect1.map(x => x * x).sum
    val square_sum_vect2 = vect2.map(x => x * x).sum

    /**
     * 求vect1和vect2的乘积和
     * zipVec.map(x => x._1 * x._2).sum = 7 * 4 + 6 * 4 + 1 * 3 + 1 * 2 = 57
     */
    val zipVec  =vect1.zip(vect2)
    val product = zipVec.map(x => x._1 * x._2).sum

    /**
     * 求公式分子
     * product - (sum_vect1 * sum_vect2) / vect1.length = 57 - (15 * 13) / 4 = 8.25
     */
    val numerator = product - (sum_vect1 * sum_vect2) / vect1.length

    /**
     * 求分母公式
     * dominato = 根号((87 - 56.25) * (45 - 42.25)) = 根号(30.75 * 2.75) = 9.19
     */
    val dominato = pow((square_sum_vect1 - pow(sum_vect1,2) / vect1.length) * (square_sum_vect2 - pow(sum_vect2,2) / vect1.length),0.5)

    /**
     * 计算相似度 = numerator / dominato = 8.25 / 9.19 = 0.897
     */
    numerator / dominato
  }
}

计算的结果:

+-------+-----------+------------------+
|item_id|sim_item_id|score             |
+-------+-----------+------------------+
|1      |1          |1.0               |
|1      |3          |0.987829161147262 |
|1      |2          |0.9406341620035445|
|1      |4          |0.8971499589146108|
|1      |5          |0.6767529681839596|
|1      |6          |0.5726371269248889|
+-------+-----------+------------------+

(5.3.3)使用皮尔逊相关度公式一计算(共同评分)

通过皮尔逊算法公式一计算相似度,取共同评分:
公式一:

在这里插入图片描述
皮尔逊相关度公式一是一种余弦相似度,先是取出共同评分,在做了均值中心化,向量p和q各自减去向量的均值后,再计算余弦相似度,逻辑和公式二类似,只是公式计算不一样。

代码开发:

package com.similarity

import breeze.numerics.pow
import org.apache.spark.ml.recommendation.ALS.Rating
import org.apache.spark.mllib.linalg.distributed.{
    
    CoordinateMatrix, MatrixEntry}
import org.apache.spark.sql.{
    
    Row, SparkSession}

import scala.collection.mutable.ListBuffer

object PearsonOneItemOpt {
    
    
  /**
   * 需求:基于物品协同过滤通过皮尔逊相似度算法(公式一:共同评分)计算用户相似性
   * @param args
   */
  def main(args: Array[String]): Unit = {
    
    

    val spark: SparkSession = SparkSession
      .builder
      .appName(this.getClass.getName)
      .master("local[5]")
      .getOrCreate

    // 设置日志等级
    spark.sparkContext.setLogLevel("WARN")

    import spark.implicits._
    val movieRatings = spark.read.textFile("/Users/caizhengjie/Desktop/大数据项目实战三/movieLess/utou_main.csv")
      .map(line => {
    
    
        val field = line.split(",")
        Rating(field(0).toInt,field(1).toInt,field(2).toFloat)
      }).toDF("user","item","rate")

    movieRatings.show()

    /**
     * userid、movieid、rate
     * +----+----+----+
     * |user|item|rate|
     * +----+----+----+
     * |   1|   1| 7.0|
     * |   1|   2| 6.0|
     * |   1|   3| 7.0|
     * |   1|   4| 4.0|
     * |   1|   5| 5.0|
     * |   1|   6| 4.0|
     * |   2|   1| 6.0|
     * ....
     */

    // 通过分布式矩阵计算相似性
    // 将df转换成矩阵rdd
    val matrixRdd = movieRatings.rdd.map{
    
    
      case Row(user:Int,item:Int,rate:Float) =>
        MatrixEntry(user.toLong,item.toLong,rate.toDouble)
    }

    matrixRdd.foreach(println)

    /**
     * MatrixEntry(1,1,7.0)
     * MatrixEntry(1,2,6.0)
     * MatrixEntry(1,3,7.0)
     * MatrixEntry(1,4,4.0)
     * MatrixEntry(1,5,5.0)
     * MatrixEntry(1,6,4.0)
     * MatrixEntry(2,1,6.0)
     * MatrixEntry(2,2,7.0)
     * ...
     */

    // 转换成坐标矩阵
    val coordinateMatrix = new CoordinateMatrix(matrixRdd)

    /**
     * user/item
     * 1  7 6 7 4 5 4
     * 2  6 7 0 4 3 4
     * 3  0 3 3 1 1 0
     * .....
     */

    // 转换成行索引矩阵,并做行列转换
    val indexRowMatrix = coordinateMatrix.transpose().toIndexedRowMatrix()

    /**
     * 行:movie_id 列:user_id value:rating
     * (4,(6,[1,2,3,4,5],[4.0,4.0,1.0,3.0,2.0]))
     * (1,(6,[1,2,4,5],[7.0,6.0,1.0,1.0]))
     * (6,(6,[1,2,4,5],[4.0,4.0,4.0,3.0]))
     * (3,(6,[1,3,4,5],[7.0,3.0,2.0,1.0]))
     * (5,(6,[1,2,3,4,5],[5.0,3.0,1.0,3.0,3.0]))
     * (2,(6,[1,2,3,4],[6.0,7.0,3.0,2.0]))
     */

    // 过滤出movie1的向量 -> (1,(6,[1,2,4,5],[7.0,6.0,1.0,1.0]))
    val myVector = indexRowMatrix.rows.filter(_.index == 1).map(x => {
    
    
      (x.index,x.vector)
    }).first()

    println(myVector._2.toString)

    // 通过自定义皮尔逊算法公式二计算出与movie1相似的电影
    val result_df = indexRowMatrix.rows.toJavaRDD().rdd.map(x => {
    
    

      // 求出movie1的user向量Vector(1, 2, 4, 5)
      val my_index_vector = myVector._2.toSparse.indices.toVector
      // 求出movie1的向量(6,[1,2,4,5],[7.0,6.0,1.0,1.0])
      val my_value_vector = myVector._2

      // 求出其他电影的user向量Vector 例如:movie4 的user Vector(1, 2, 3, 4, 5)
      val other_index_vector = x.vector.toSparse.indices.toVector
      // 求出其他电影的向量 例如:movie4 的 (6,[1,2,3,4,5],[4.0,4.0,1.0,3.0,2.0])
      val other_value_vector = x.vector

      // 求两个user向量的交集
      val index_vector_all = my_index_vector.intersect(other_index_vector)
      // println(index_vector_all.toString())

      /**
       * Vector(1, 2, 4, 5) 与movie的user Vector的交集
       * Vector(1, 2, 4, 5)
       * Vector(1, 2, 4, 5)
       * Vector(1, 4, 5)
       * Vector(1, 2, 4, 5)
       * Vector(1, 2, 4)
       */


      /**
       * 演变过程
       * movie1
       * (6,[1,2,4,5],[7.0,6.0,1.0,1.0])            -> Vector(1,2,4,5)                                                                             -> Vector(7.0,6.0,1.0,1.0)
       *                                                                  求出两个向量的交集 -> Vector(1,2,4,5) -> 通过遍历可以分别求出对应的值也就是共同评分                                在计算两个向量的相似度得出结果
       * movie4
       * (4,(6,[1,2,3,4,5],[4.0,4.0,1.0,3.0,2.0]))  -> Vector(1,2,3,4,5)                                                                           -> Vector(4.0, 4.0, 3.0, 2.0)
       */
      val my_list = new ListBuffer[Double]
      val other_list = new ListBuffer[Double]

      // 通过两个user向量的交集遍历可以分别求出对应的值也就是共同评分
      for (index <-  index_vector_all.indices){
    
    
        my_list.append(my_value_vector(index_vector_all(index)))
        other_list.append(other_value_vector(index_vector_all(index)))
      }

      val my_res_vector = my_list.toVector
      val other_res_vector = other_list.toVector

      // 以movie1和movie4为例,my_res_vector:Vector(7.0,6.0,1.0,1.0) other_res_vector:Vector(4.0, 4.0, 3.0, 2.0)
      println(my_res_vector.toString())
      println(other_res_vector.toString)

      // 生成计算出结果表df
      (myVector._1 , x.index,pearsonCorrelationSimilarity(my_res_vector,other_res_vector))
    }).toDF("item_id","sim_item_id","score")

    result_df.sort(result_df("score").desc).show(false)

    /**
     * +-------+-----------+------------------+
     * |item_id|sim_item_id|score             |
     * +-------+-----------+------------------+
     * |1      |1          |0.9999999999999999|
     * |1      |3          |0.987829161147262 |
     * |1      |2          |0.9406341620035447|
     * |1      |4          |0.8971499589146108|
     * |1      |5          |0.6767529681839596|
     * |1      |6          |0.5726371269248889|
     * +-------+-----------+------------------+
     */

  }

  /**
   * 重写皮尔逊算法公式一
   * @param vect1
   * @param vect2
   * @return
   */
  def pearsonCorrelationSimilarity(vect1 : Vector[Double], vect2 : Vector[Double]) : Double = {
    
    

    /**
     * 求vect1和vect2的和
     * 以以movie1和movie4为例:
     * vect1.sum = Vector(7.0,6.0,1.0,1.0).sum = 15
     * vect2.sum = Vector(4.0, 4.0, 3.0, 2.0).sum = 13
     */
    val sum_vect1 = vect1.sum
    val sum_vect2 = vect2.sum

    /**
     * 求vect1和vect2的平均值
     * vect1_mean = vect1.sum / vect1.length = 3.75
     * vect2_mean = vect2.sum / vect2.length = 3.25
     */
    val vect1_mean = vect1.sum / vect1.length
    val vect2_mean = vect2.sum / vect2.length

    /**
     * 均值中心化并求vect1和vect2的平方和
     * 以movie1和movie4为例:
     * square_sum_vect1 = vect1.map(x => (x - vect1_mean) * (x - vect1_mean)).sum = 10.56 + 5.06 + 7.56 + 7.56 = 30.74
     * square_sum_vect2 = vect2.map(x => (x - vect2_mean) * (x - vect2_mean)).sum = 0.56 + 0.56 + 0.06+ 1.56 = 2.74
     */
    val square_sum_vect1 = vect1.map(x => (x - vect1_mean) * (x - vect1_mean)).sum
    val square_sum_vect2 = vect2.map(x => (x - vect2_mean) * (x - vect2_mean)).sum

    /**
     * 求vect1和vect2的乘积和
     * product = zipVec.map(x => (x._1 - vect1_mean) * (x._2 - vect2_mean)).sum = 2.43 + 1.68 + 0.68 + 3.43 = 8.22
     */
    val zipVec  =vect1.zip(vect2)
    val product = zipVec.map(x => (x._1 - vect1_mean) * (x._2 - vect2_mean)).sum

    /**
     * 求公式分子
     * numerator = product = 8.22
     */
    val numerator = product

    /**
     * 求分母公式
     * dominato = 根号(30.74) * 根号(2.68) = 5.44 * 1.65 = 8.976
     */
    val dominato = pow(square_sum_vect1 , 0.5) * pow(square_sum_vect2 , 0.5)
    /**
     * 计算相似度 = numerator / dominato = 8.22 / 8.976 = 0.915
     */
    numerator / dominato
  }
}

计算结果:

+-------+-----------+------------------+
|item_id|sim_item_id|score             |
+-------+-----------+------------------+
|1      |1          |0.9999999999999999|
|1      |3          |0.987829161147262 |
|1      |2          |0.9406341620035447|
|1      |4          |0.8971499589146108|
|1      |5          |0.6767529681839596|
|1      |6          |0.5726371269248889|
+-------+-----------+------------------+

(5.4)几种计算相似度算法总结

原数据:
在这里插入图片描述
从主观角度我们可以看出movie1和movie3最相似,movie1和movie6最不相似

几种计算相似度结果对比

余弦相似度
+-------+-----------+------------------+
|item_id|sim_item_id|score             |
+-------+-----------+------------------+
|1      |2          |0.9313775963969384|
|1      |4          |0.9010239053216177|
|1      |5          |0.86886929617049  |
|1      |6          |0.8378281682565331|
|1      |3          |0.7023821120100268|
+-------+-----------+------------------+

调整余弦相似度算法
+-------+-----------+-------------------+
|item_id|sim_item_id|score              |
+-------+-----------+-------------------+
|1      |3          |0.8635228492837839 |
|1      |2          |0.7599373542607082 |
|1      |6          |0.5726371269248889 |
|1      |4          |0.5705279590697938 |
|1      |5          |0.41442486349116986|
+-------+-----------+-------------------+

皮尔逊相关度算法(公式二API计算,非共同评分)
+-------+-----------+------------------+
|item_id|sim_item_id|score             |
+-------+-----------+------------------+
|1.0    |1.0        |1.0               |
|1.0    |4.0        |0.8875895031209077|
|1.0    |2.0        |0.8569515946486446|
|1.0    |5.0        |0.7637626158259733|
|1.0    |6.0        |0.6236095644623235|
|1.0    |3.0        |0.3712165082817225|
+-------+-----------+------------------+

皮尔逊相关度算法(公式二计算,共同评分)
+-------+-----------+------------------+
|item_id|sim_item_id|score             |
+-------+-----------+------------------+
|1      |1          |1.0               |
|1      |3          |0.987829161147262 |
|1      |2          |0.9406341620035445|
|1      |4          |0.8971499589146108|
|1      |5          |0.6767529681839596|
|1      |6          |0.5726371269248889|
+-------+-----------+------------------+

皮尔逊相关度算法(公式一计算,共同评分)
+-------+-----------+------------------+
|item_id|sim_item_id|score             |
+-------+-----------+------------------+
|1      |1          |0.9999999999999999|
|1      |3          |0.987829161147262 |
|1      |2          |0.9406341620035447|
|1      |4          |0.8971499589146108|
|1      |5          |0.6767529681839596|
|1      |6          |0.5726371269248889|
+-------+-----------+------------------+

由计算结果可以看出来调整余弦相似度和皮尔逊相关度算法计算出来的相似度较高。

总结

余弦相似度计算步骤:调用计算余弦相似度API

调整余弦相似度计算步骤:均值中心化,调用计算余弦相似度API

皮尔逊相似度计算步骤:

  • 皮尔逊公式一:自定义使用皮尔逊公式一,取共同评分、均值中心化、通过公式一计算
  • 皮尔逊公式二:
    • (1)自定义使用皮尔逊公式二,取共同评分、通过公式二计算
    • (2)通过官方提供的皮尔逊API(公式二),没有取共同评分,当计算的的i和j不是用户共同评分的时候,结果不是很好。

总结一句话:几种相似度计算方法都是与余弦相似度算法类似,万变不离其宗!

调整余弦相似度和皮尔逊相似度区别:调整余弦相似度没有取共同数据,皮尔逊相似度取共同数据

(6)基于Spark完成用户推荐开发

(1)基于余弦相似度算法推荐电影

package com.similarity

import org.apache.spark.ml.recommendation.ALS.Rating
import org.apache.spark.mllib.linalg.distributed.{
    
    CoordinateMatrix, MatrixEntry}
import org.apache.spark.sql.types.{
    
    IntegerType, LongType, StructField, StructType}
import org.apache.spark.sql.{
    
    Row, SparkSession}

object CosItemRecommend {
    
    
  /**
   * 需求:基于物品协同过滤通过调整余弦相似度算法推荐电影
   * @param args
   */
  def main(args: Array[String]): Unit = {
    
    

    val spark: SparkSession = SparkSession
      .builder
      .appName(this.getClass.getName)
      .master("local[5]")
      .getOrCreate

    // 设置日志等级
    spark.sparkContext.setLogLevel("WARN")

    import spark.implicits._
    // 用户评分数据源
    val movieRatings = spark.read.textFile("/Users/caizhengjie/Desktop/大数据项目实战三/movieLess/ratings_test.csv")
      .map(line => {
    
    
        val field = line.split(",")
        Rating(field(0).toInt,field(1).toInt,field(2).toFloat)
      }).toDF("user","item","rate")

    movieRatings.show()

    /**
     * +----+----+----+
     * |user|item|rate|
     * +----+----+----+
     * |   1|3100| 4.0|
     * |   1|3527| 4.0|
     * |   1|3578| 5.0|
     * |   1|3617| 4.0|
     * |   1|3639| 4.0|
     * |   1|3671| 5.0|
     * ....
     */


    // 电影数据源
    val movieData = spark.read.textFile("/Users/caizhengjie/Desktop/大数据项目实战三/movieLess/movies.csv")
      .map(line => {
    
    
        val field = line.split(",")
        (field(0).toInt,field(1).toString,field(2).toString)
      }).toDF("movie_id","movie_name","genres")

    movieData.show()

    /**
     * +--------+--------------------+--------------------+
     * |movie_id|          movie_name|              genres|
     * +--------+--------------------+--------------------+
     * |       1|    Toy Story (1995)|Adventure|Animati...|
     * |       2|      Jumanji (1995)|Adventure|Childre...|
     * |       3|Grumpier Old Men ...|      Comedy|Romance|
     * |       4|Waiting to Exhale...|Comedy|Drama|Romance|
     * |       5|Father of the Bri...|              Comedy|
     * ....
     */

    // 通过分布式矩阵计算相似性
    // 将df转换成矩阵rdd
    val matrixRdd = movieRatings.rdd.map{
    
    
      case Row(user:Int,item:Int,rate:Float) =>
        MatrixEntry(user.toLong,item.toLong,rate.toDouble)
    }

    matrixRdd.foreach(println)

    /**
     * MatrixEntry(1,3100,4.0)
     * MatrixEntry(1,3527,4.0)
     * MatrixEntry(1,3578,5.0)
     * MatrixEntry(1,3617,4.0)
     * MatrixEntry(1,3639,4.0)
     * MatrixEntry(1,3671,5.0)
     * ...
     */

    // 转换成坐标矩阵
    val coordinateMatrix = new CoordinateMatrix(matrixRdd)

    /**
     * user/item
     * 1  7 6 7 4 5 4
     * 2  6 7 0 4 3 4
     * 3  0 3 3 1 1 0
     * .....
     */

    // 由于需要计算物品的相似度即基于列的相似性计算,所以需要将行列置换再计算余弦相似度
    val cosinsim = coordinateMatrix.toIndexedRowMatrix().columnSimilarities().entries
    cosinsim.foreach(println)

    /**
     * MatrixEntry(3100,3729,0.89324598298771)
     * MatrixEntry(3527,3702,0.8299250027587322)
     * MatrixEntry(3617,3702,0.9304842103984707)
     * MatrixEntry(3100,3671,0.7131973913518905)
     * MatrixEntry(3527,3703,0.8017837257372732)
     * MatrixEntry(3617,3729,0.8975491104081189)
     * ....
     */


    // 将计算余弦相似度结果主换成df
    val res_df = cosinsim.map(x => {
    
    
      (x.i,x.j,x.value)
    }).toDF("item_id","sim_item_id","score")

    res_df.where("item_id = 3100").sort(res_df("score").desc).show(false)

    /**
     * +-------+-----------+------------------+
      |item_id|sim_item_id|score             |
      +-------+-----------+------------------+
      |3100   |3703       |0.9335200560186732|
      |3100   |3729       |0.89324598298771  |
      |3100   |3578       |0.8410373398892897|
      |3100   |3639       |0.7484811885651198|
      |3100   |3702       |0.7247137945655604|
      |3100   |3671       |0.7131973913518905|
      |3100   |3617       |0.6797294208444249|
      |3100   |3527       |0.6362090102803518|
      +-------+-----------+------------------+
     */



    /**
     * cosinsim这个矩阵中i是电影,j是相似的电影,value是相似度值
     *
     * 第一步:找一部电影作为我们的推荐需求,假如找与3100电影相似的电影
     *
     * 第二步:通过关联电影表,找出推荐电影
     *
     * 通过rdd 转换成df 再通过join
     */

    val movieSim3100  = cosinsim.filter(_.i == 3100).map(x => {
    
    
      Row(x.j)
    })

    movieSim3100.foreach(println)

    val SubMoviesDF = Array(
      StructField("movie_id",LongType,nullable = true)
    )
    val structType = StructType(SubMoviesDF)
    val subMovie = spark.createDataFrame(movieSim3100,structType)

    subMovie.show(false)

    val recommendResult = subMovie.join(movieData,"movie_id")
    recommendResult.show(false)


    /**
     * 通过spark sql完成上面操作
     */
    res_df.createOrReplaceTempView("similarity")
    movieData.createOrReplaceTempView("movie")

    val recommendResult1 = spark.sql("select b.movie_id,movie_name,genres " +
      "from (select sim_item_id as movie_id from similarity where item_id = 3100) " +
      "a left join movie b on a.movie_id = b.movie_id")
    recommendResult1.show(false)

    /**
     * +--------+------------------------+---------------------------+
     * |movie_id|movie_name              |genres                     |
     * +--------+------------------------+---------------------------+
     * |3729    |Shaft (1971)            |Action|Crime|Drama|Thriller|
     * |3671    |Blazing Saddles (1974)  |Comedy|Western             |
     * |3639    |"Man with the Golden Gun| The (1974)"               |
     * |3703    |"Road Warrior           | The (Mad Max 2) (1981)"   |
     * |3702    |Mad Max (1979)          |Action|Adventure|Sci-Fi    |
     * |3617    |Road Trip (2000)        |Comedy                     |
     * |3527    |Predator (1987)         |Action|Sci-Fi|Thriller     |
     * |3578    |Gladiator (2000)        |Action|Adventure|Drama     |
     * +--------+------------------------+---------------------------+
     */

  }

}

(2)基于调整余弦相似度算法推荐电影

package com.similarity

import org.apache.spark.ml.recommendation.ALS.Rating
import org.apache.spark.mllib.linalg.distributed.{
    
    CoordinateMatrix, MatrixEntry}
import org.apache.spark.sql.types.{
    
    LongType, StructField, StructType}
import org.apache.spark.sql.{
    
    Row, SparkSession}

object CosItermOptRecommend {
    
    
  /**
   * 需求:基于物品协同过滤通过调整余弦相似度算法计算用户相似性
   * @param args
   */
  def main(args: Array[String]): Unit = {
    
    

    val spark: SparkSession = SparkSession
      .builder
      .appName(this.getClass.getName)
      .master("local[5]")
      .getOrCreate

    // 设置日志等级
    spark.sparkContext.setLogLevel("WARN")

    import spark.implicits._
    // 用户评分数据源
    val movieRatings = spark.read.textFile("/Users/caizhengjie/Desktop/大数据项目实战三/movieLess/ratings_test.csv")
      .map(line => {
    
    
        val field = line.split(",")
        Rating(field(0).toInt,field(1).toInt,field(2).toFloat)
      }).toDF("user","item","rate")

    movieRatings.show()

    /**
     * +----+----+----+
     * |user|item|rate|
     * +----+----+----+
     * |   1|3100| 4.0|
     * |   1|3527| 4.0|
     * |   1|3578| 5.0|
     * |   1|3617| 4.0|
     * |   1|3639| 4.0|
     * |   1|3671| 5.0|
     * ....
     */


    // 电影数据源
    val movieData = spark.read.textFile("/Users/caizhengjie/Desktop/大数据项目实战三/movieLess/movies.csv")
      .map(line => {
    
    
        val field = line.split(",")
        (field(0).toInt,field(1).toString,field(2).toString)
      }).toDF("movie_id","movie_name","genres")

    movieData.show()

    /**
     * +--------+--------------------+--------------------+
     * |movie_id|          movie_name|              genres|
     * +--------+--------------------+--------------------+
     * |       1|    Toy Story (1995)|Adventure|Animati...|
     * |       2|      Jumanji (1995)|Adventure|Childre...|
     * |       3|Grumpier Old Men ...|      Comedy|Romance|
     * |       4|Waiting to Exhale...|Comedy|Drama|Romance|
     * |       5|Father of the Bri...|              Comedy|
     * ....
     */


    // 均值中心化
    // 计算物品的平均评分
    val itemmean = movieRatings.groupBy("item").mean("rate").toDF("item","mean_rate")
    itemmean.show(false)

    /**
     * +----+------------------+
     * |item|mean_rate         |
     * +----+------------------+
     * |3703|2.3333333333333335|
     * |3702|4.0               |
     * |3527|3.0               |
     * |3671|3.6666666666666665|
     * |3639|3.0               |
     * |3100|2.5               |
     * |3617|3.125             |
     * |3578|2.875             |
     * |3729|2.75              |
     * +----+------------------+
     */


    // 将两个表合并
    val item_movie_rating = movieRatings.join(itemmean,"item").toDF("item","user","rate","mean_rate")
    item_movie_rating.show()

    /**
     * +----+----+----+------------------+
     * |item|user|rate|         mean_rate|
     * +----+----+----+------------------+
     * |3100|   1| 4.0|               2.5|
     * |3527|   1| 4.0|               3.0|
     * |3578|   1| 5.0|             2.875|
     * |3617|   1| 4.0|             3.125|
     * |3639|   1| 4.0|               3.0|
     * |3671|   1| 5.0|3.6666666666666665|
     * ...
     */

    item_movie_rating.createOrReplaceTempView("ratings")

    // 通过spark sql均值中心化
    val res_movie_rating = spark.sql("select user,item,(rate - mean_rate) as rate from ratings")
    res_movie_rating.show()

    /**
     * +----+----+-------------------+
     * |user|item|               rate|
     * +----+----+-------------------+
     * |   1|3100|                1.5|
     * |   1|3527|                1.0|
     * |   1|3578|              2.125|
     * |   1|3617|              0.875|
     * |   1|3639|                1.0|
     * ....
     */


    // 通过分布式矩阵计算相似性
    // 将df转换成矩阵rdd
    val matrixRdd = res_movie_rating.rdd.map{
    
    
      case Row(user:Int,item:Int,rate:Double) =>
        MatrixEntry(user.toLong,item.toLong,rate.toDouble)
    }

    matrixRdd.foreach(println)

    /**
     * MatrixEntry(1,3100,1.5)
     * MatrixEntry(1,3527,1.0)
     * MatrixEntry(1,3578,2.125)
     * MatrixEntry(1,3617,0.875)
     * MatrixEntry(1,3639,1.0)
     * ...
     */

    // 转换成坐标矩阵
    val coordinateMatrix = new CoordinateMatrix(matrixRdd)

    /**
     * user/item
     * 1  3.25 1.5 3.75 ...
     * 2 .....
     */

    // 由于需要计算物品的相似度即基于列的相似性计算,所以需要将行列置换再计算余弦相似度
    val cosinsim = coordinateMatrix.toIndexedRowMatrix().columnSimilarities().entries
    cosinsim.foreach(println)

    /**
     * MatrixEntry(3100,3729,0.47809144373375756)
     * MatrixEntry(3527,3702,-0.5000000000000001)
     * MatrixEntry(3617,3702,-4.163336342344337E-17)
     * MatrixEntry(3100,3671,0.8660254037844388)
     * MatrixEntry(3527,3703,0.16666666666666666)
     * MatrixEntry(3617,3729,0.37142857142857144)
     * ...
     */


    // 将计算余弦相似度结果主换成df
    val res_df = cosinsim.map(x => {
    
    
      (x.i,x.j,x.value)
    }).toDF("item_id","sim_item_id","score")

    res_df.where("item_id = 3100").sort(res_df("score").desc).show(false)


    /**
     * +-------+-----------+-------------------+
      |item_id|sim_item_id|score              |
      +-------+-----------+-------------------+
      |3100   |3617       |0.956182887467515  |
      |3100   |3671       |0.8660254037844388 |
      |3100   |3527       |0.8660254037844388 |
      |3100   |3578       |0.7912565680749444 |
      |3100   |3703       |0.5773502691896258 |
      |3100   |3729       |0.47809144373375756|
      |3100   |3639       |0.0                |
      |3100   |3702       |0.0                |
      +-------+-----------+-------------------+
     */

    /**
     * cosinsim这个矩阵中i是电影,j是相似的电影,value是相似度值
     *
     * 第一步:找一部电影作为我们的推荐需求,假如找与3100电影相似的电影
     *
     * 第二步:通过关联电影表,找出推荐电影
     *
     * 通过rdd 转换成df 再通过join
     */

    val movieSim3100  = cosinsim.filter(_.i == 3100).map(x => {
    
    
      Row(x.j)
    })

    movieSim3100.foreach(println)

    val SubMoviesDF = Array(
      StructField("movie_id",LongType,nullable = true)
    )
    val structType = StructType(SubMoviesDF)
    val subMovie = spark.createDataFrame(movieSim3100,structType)

    subMovie.show(false)

    val recommendResult = subMovie.join(movieData,"movie_id")
    recommendResult.show(false)


    /**
     * 通过spark sql完成上面操作
     */
    res_df.createOrReplaceTempView("similarity")
    movieData.createOrReplaceTempView("movie")

    val recommendResult1 = spark.sql("select b.movie_id,movie_name,genres " +
      "from (select sim_item_id as movie_id from similarity where item_id = 3100) " +
      "a left join movie b on a.movie_id = b.movie_id")
    recommendResult1.show(false)


    /**
     * +--------+------------------------+---------------------------+
     * |movie_id|movie_name              |genres                     |
     * +--------+------------------------+---------------------------+
     * |3729    |Shaft (1971)            |Action|Crime|Drama|Thriller|
     * |3671    |Blazing Saddles (1974)  |Comedy|Western             |
     * |3639    |"Man with the Golden Gun| The (1974)"               |
     * |3703    |"Road Warrior           | The (Mad Max 2) (1981)"   |
     * |3702    |Mad Max (1979)          |Action|Adventure|Sci-Fi    |
     * |3617    |Road Trip (2000)        |Comedy                     |
     * |3527    |Predator (1987)         |Action|Sci-Fi|Thriller     |
     * |3578    |Gladiator (2000)        |Action|Adventure|Drama     |
     * +--------+------------------------+---------------------------+
     */

  }

}

以上内容仅供参考学习,如有侵权请联系我删除!
如果这篇文章对您有帮助,左下角的大拇指就是对博主最大的鼓励。
您的鼓励就是博主最大的动力!

猜你喜欢

转载自blog.csdn.net/weixin_45366499/article/details/113938597