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


前言:
基于用户画像的个性化推荐是有缺陷的,因为它不会做用户兴趣的升级。无法发现新知识,推荐的候选集永远圈定在你的兴趣标签维度内,做不到认知的升级,而实际上认知是会进行升级的,特别是随着你捕获的知识信息越多的情况下,你就越会对更上层的其他知识感兴趣,不断的深入下去。

举个例子:
比如,你对大数据的东西很感兴趣,于是系统根据你的兴趣偏好天天给你推Hadoop、大数据各种技术框架等信息,在某个时间段可能是合理,比如我对大数据领域已经熟知了呢?你还给我天天推送大数据相关的信息。而我实际上是需要寻求大数据关联的信息,甚至是升级的信息,比如基于大数据的机器学习、数据挖掘相关的东西,这个机制是无法做到这一层的。

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

基于协同过滤推荐架构图:
在这里插入图片描述

(1)基于用户协同过滤思想

背后的思想:

你有没有过这种感觉,你遇到一个人,你发现他喜欢的书、喜欢的电影也基本上都是你喜欢的,从此以后,你就老是想问他:还有什么好推荐的,最近又看了什么书,最近又看了什么电影?甚至不惜和他撞衫,和他穿一个风格的衣服。

这个感觉非常地自然直接,它就是基于用户的协同过滤背后思想。

详细来说就是:先根据历史消费行为帮你找到一群和你口味很相似的用户;然后根据这些和你很相似的用户再消费了什么新的、你没有见过的物品,都可以推荐给你。

在这里插入图片描述
应用场景:

基于用户的协同过滤有两个产出:

  • 相似用户列表
  • 基于用户的推荐结果

所以我们不但可以推荐物品,还可以推荐用户!比如我们在一些社交平台上看到:“相似粉丝”“和你口味类似的人”等等都可以这样计算。对于这个方法计算出来的推荐结果本身,由于是基于口味计算得出,所以在更强调个人隐私场景中应用更佳,在这样的场景下,不受大V影响,更能反应真实的兴趣群体,而非被煽动的乌合之众。

(2)用户协同过滤—用户相似度计算

如何构建基于用户的协同过滤一余弦相似度算法

我们来说一说基于用户的协同过滤具体是怎么做的。其核心是用户物品的关系矩阵,这个矩阵是最原始的材料。

基于用户原始行为数据,如何构建矩阵
将用户的行为(如:点击、点赞、收藏、推荐等)进行权重分配,转化成具体的分值
比如总评分是5分,我们来设置点击、点赞、收藏、推荐这四个指标的权重分分别为:0.5分、1分、1.5分、2分假如user_1对movie_1的行为分:点击+点赞=0.5+1=1.5,那么可视为user_1对movie_1的评分值为1.5。其他的数据以此类推来计算

在这里插入图片描述

第一步,准备用户向量,从这个矩阵中,理论上可以给每一个用户得到一个向量。

这个向量有这么三个特点:

  • 向量的维度就是物品的个数;
  • 向量是稀疏的,也就是说并不是每个维度上都有数值,原因当然很简单,这个用户并不是消费过所有物品,(如果全部消费过,我们也就不用推荐了);
  • 向量维度上的取值可以是简单的0或者1,也就是布尔值,1表示喜欢过,0表示没有,当然因为是稀疏向量,所以取值为0的就忽略了。

在这里插入图片描述
计算User3与其他用户的相似值:【余弦相似度】
User_1 = 【7,6, 7, 4,5,4】
User_3 =【0,3, 3, 1, 1, 0】
余弦相似度缺失评分值默认为0;

在这里插入图片描述

第二步,通过User_3与其他用户两两计算之间的相似度

在这里插入图片描述
在这里插入图片描述

(3)用户协同过滤—预测评分

第三步,为User_3用户产生推荐结果一预测评分

把和他“臭味相投”的用户们看过的电影汇总起来,去掉用户自己近看过的电影,剩下的排序输出就是推荐结果。
具体的汇总方式我们可以通过加权平均值公式来预测出准备为用户User_3推荐的电影的排序结果

在这里插入图片描述
这个公式也是很简单的。等号左边就是计算一个物品i和一个用户u的匹配分数,等号右边是这个分数的计算过程,分母是把和用户u相似的n个用户的相似度加起来,分子是把这n个用户各自对物品i的态度,按照相似度加权求和。

通过公式,我们可以预测出User_3 对movie_1 和 movie_6的评分,从用户的相似值来看,user_3与user_1和user_4 相似度最高,所以我们通过user_1和user_4的评分值和相似值来预测

在这里插入图片描述
通过计算结果,movie_1应该先于movie_6被推荐给user_3
在这里插入图片描述

(4)用户协同过滤—考虑问题及策略

问题一:如果用户的向量很长,计算一个相似度则耗时很久,怎么办?

首先是单个相似度计算问题,如果碰上向量很长,无论什么相似度计算方法,都要遍历向量,如果用循环实现就更可观了,所以通常降低相似度计算复杂度的办法有两种。

第一种
对向量采样计算。道理很简单,两个一百维的向量计算出的相似度是0.7,我现在忍受一些精度的损失,不用100维计算,随机从中取出10维计算,得到相似度是0.72,显然用100维计算出的0.7更可信一些,但是在计算复杂度降低十倍的情形下,0.72和它误差也不大,后者更经济。这个算法由 Twitter提出,叫做DIMSUM 算法,已经在Spark中实现了。

第二种
向量化计算。与其说这是一个小技巧,不如说这是一种思维方式。在机器学习领域,向量之间的计算是家常便饭,难道向量计算都要用循环实现吗?并不是,现代的线性代数库都支持直接的向量运算,比循环快很多。也就是我们在任何地方,都要想办法把循环转换成向量来直接计算,一般像常用的向量库都天然支持的,比如 Python的NumPy。

Spark中的MLlib的向量(Vectors) 和分布式矩阵(Matrix )

问题二:如果用户量很大,两两之间计算代价就很大

有两个办法来缓解这个问题:
第一个办法是:
将相似度计算拆成Map Reduce 任务,将原始矩阵Map成键为用户对,值为两个用户对同一个物品的评分之积,Reduce 阶段对这些乘积再求和,Map Reduce 任务结束后再对这些值归一化;

第二个办法是:
不用基于用户的协同过滤。

问题三:在计算推荐时,看上去要为每一个用户计算他和每一个物品的分数,又是一个大坑,怎么办?

得到了用户之间的相似度之后。接下来还有一个硬骨头,计算推荐分数。显然,为每一个用户计算每一个物品的推荐分数,计算次数是矩阵的所有元素个数,这个代价,你当然不能接受啊。
这时候,你注意回想一下前面那个汇总公式,有这么几个特点我们可以来利用一下:

  • 只有相似用户喜欢过的物品需要计算,这个大大的赞,这个数量相比全部物品少了很多;
  • 把计算过程拆成Map Reduce 任务。

(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 CosUser {
    
    
  /**
   * 需求:基于用户协同过滤通过余弦相似度算法计算用户相似性
   * @param args
   */
  def main(args: Array[String]): Unit = {
    
    

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

    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.transpose().toIndexedRowMatrix().columnSimilarities().entries
    cosinsim.foreach(println)

    /**
     * MatrixEntry(1,4,0.838615105554155)
     * MatrixEntry(4,5,0.9338592095470356)
     * MatrixEntry(3,5,0.3651483716701108)
     * MatrixEntry(2,3,0.557773351022717)
     * .....
     */

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

    res_df.where("user_id = 3 or sim_user_id = 3").sort(res_df("score").desc).show(false)

    /**
     * +-------+-----------+------------------+
     * |user_id|sim_user_id|score             |
     * +-------+-----------+------------------+
     * |1      |3          |0.7766217620286883|
     * |3      |4          |0.6137949055234262|
     * |2      |3          |0.557773351022717 |
     * |3      |5          |0.3651483716701108|
     * +-------+-----------+------------------+
     */

  }

}

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

为什么修正余弦相似度?

基于余弦的相似度计算方法未考虑用户的差异性打分情况,每个人标准不一样,有的标准严苛,有的宽松,所以减去用户的均值可以在一定程度上仅仅保留了偏好,去掉了主观成分。
修正的余弦相似度计算就是用用户均值中心化后的向量进行余弦相似度计算,因为中心化后的值才相对真实反映用户的喜好,即把矩阵中的分数,减去对应用户分数的均值;先计算每一个用户的评分均值,然后把他打过的所有分数都减去这个均值一用户中心化

调整余弦相似度计算公式
在这里插入图片描述
修正的余弦系数分子是两个用户共同(因为余弦缺省值为0,向量相乘因为0而结果为共同评分集)的评分集,分母是两个用户各自的评分集。

修正余弦相似度计算公式说明

在这里插入图片描述

余弦相似度与修正余弦相似度对比
示例两个用户对两部电影的评分,user1的评分相对较低,user2评分相对较高,从主观的角度看user1和user2应该最不相似,但是通过余弦相似度计算,得出的结果是相似的,如果通过调整余弦相似度计算,得出的结果正是我们所预期的,因此可以得出通过调整余弦相似度计算更加客观更加准确

        movie-1     movie-2

user1:  1           2
user2:  5           4


        movie-1     movie-2

user1:  -0.5        0.5      (1.5)
user2:  0.5         -0.54    (4.5)

余弦相似度 consin = 0.91
调整余弦相似度 adjConsin = -1

通过余弦相似度计算user_3与其他用户的相似度
在这里插入图片描述
重新通过调整余弦相似度计算User_3与其他用户两两计算之间的相似度

在这里插入图片描述
得出的结果有所差异,在余弦相似度计算中user3和user1最相似,但在调整余弦相似度计算中user3和user2最相似

通过程序验证:

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 CosUserOpt {
    
    
  /**
   * 需求:基于用户协同过滤通过调整余弦相似度算法计算用户相似性
   * @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 usermean = movieRatings.groupBy("user").mean("rate").toDF("user","mean_rate")
    usermean.show(false)

    /**
     * +----+----+
     * |user|rate|
     * +----+----+
     * |1   |5.5 |
     * |3   |2.0 |
     * |5   |2.0 |
     * |4   |2.5 |
     * |2   |4.8 |
     * +----+----+
     */

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

    user_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()

    // 通过分布式矩阵计算相似性
    // 将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,1.5)
     * MatrixEntry(1,2,0.5)
     * MatrixEntry(1,3,1.5)
     * MatrixEntry(1,4,-1.5)
     * MatrixEntry(1,5,-0.5)
     * MatrixEntry(1,6,-1.5)
     * ...
     */


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

    /**
     * user/item
     * 1  1.5 0.5 1.5 -1.5 -0.5 -1.5
     * 2 .....
     */

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

    /**
     * MatrixEntry(1,4,-0.8992288030258972)
     * MatrixEntry(4,5,0.8528028654224418)
     * MatrixEntry(3,5,-0.5)
     * MatrixEntry(2,3,0.7302967433402214)
     * MatrixEntry(2,4,-0.7006490497453706)
     * ....
     */

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

    res_df.where("user_id = 3 or sim_user_id = 3").sort(res_df("score").desc).show(false)

    /**
     * +-------+-----------+--------------------+
     * |user_id|sim_user_id|score               |
     * +-------+-----------+--------------------+
     * |2      |3          |0.7302967433402214  |
     * |1      |3          |0.6488856845230502  |
     * |3      |4          |-0.42640143271122083|
     * |3      |5          |-0.5                |
     * +-------+-----------+--------------------+
     */

  }

}

User_3用户产生推荐结果,通过均值中心化重新对R31和 R35进行评分预测,与之对应的预测公式为:
在这里插入图片描述
通过公式,我们可以预测出User_3 对movie_1和 movie_6的评分,从用户的相似值来看,user_3与user_1和user_2 相似度最高,所以我们通过user_1和user_2的评分值和相似值来预测
在这里插入图片描述
通过计算结果,movie_1应该先于movie_6被推荐给user_3
在这里插入图片描述

(6)用户协同过滤—基于Spark完成用户推荐开发

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

数据集下载:
https://grouplens.org/datasets/movielens/

在这里插入图片描述

  • 需求:基于用户协同过滤通过余弦相似度算法推荐电影
  • 原理:假设要为380用户推荐电影,找到与380用户相似性的用户580,找出380用户和580用户的电影差集,即作为推荐电影推荐给380用户
     * 通过余弦相似度计算,与用户380最形似的userID为561
     * |380    |561        |0.45039759078670477|
     *
     * 第一步:找出与380用户最相似的用户,目前是561用户
     *
     * 第二步:找出与380用户最相似的用户(561)看过的所有电影
     *
     * 第三步:找出380用户看过的所有电影
     *
     * 第四步:求380用户和561用户看过电影集合的差值
     *
     * 第五步:求出的差值电影集合就是推荐给380用户的电影集合

程序开发:

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.{
    
    DoubleType, IntegerType, LongType, StructField, StructType}
import org.apache.spark.sql.{
    
    Row, SparkSession}

object CosUserRecommend {
    
    
  /**
   * 需求:基于用户协同过滤通过余弦相似度算法推荐电影
   * 原理:假设要为380用户推荐电影,找到与380用户相似性的用户580,找出380用户和580用户的电影差集,即作为推荐电影推荐给380用户
   * @param args
   */
  def main(args: Array[String]): Unit = {
    
    

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

    import spark.implicits._

    // 用户评分数据源
    val movieRatings = spark.read.textFile("/Users/caizhengjie/Desktop/大数据项目实战三/movieLess/ratings.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| 4.0|
     * |   1|   3| 4.0|
     * |   1|   6| 4.0|
     * |   1|  47| 5.0|
     * |   1|  50| 5.0|
     * |   1|  70| 3.0|
     * |   1| 101| 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)
    }

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

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

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

    // 找出与380用户相似的用户
    res_df.where("user_id = 380").sort(res_df("score").desc).show(false)

    /**
     * 通过余弦相似度计算,与用户380最形似的userID为561
     * |380    |561        |0.45039759078670477|
     *
     * 第一步:找出与380用户最相似的用户,目前是561用户
     *
     * 第二步:找出与380用户最相似的用户(561)看过的所有电影
     *
     * 第三步:找出380用户看过的所有电影
     *
     * 第四步:求380用户和561用户看过电影集合的差值
     *
     * 第五步:求出的差值电影集合就是推荐给380用户的电影集合
     *
     */

    // 第一步:找出与380用户最相似的用户,目前是561用户
    val user380_sim = cosinsim.filter(x => x.i == 380).map(line => (line.j,line.value))
    // user380_sim.foreach(println)
    val user380BestSsim = user380_sim.sortBy(_._2,ascending = false).take(1)
    val bestSimUser = user380BestSsim(0)._1
    // println(bestSimUser)

    // 第二步:找出与380用户最相似的用户(561)看过的所有电影
    val bestSimUserMovies = movieRatings.rdd.filter(_.get(0) == bestSimUser).map(x => x.get(1))
    // bestSimUserMovies.foreach(println)

    // 第三步:找出380用户看过的所有电影
    val user380Movies = movieRatings.rdd.filter(_.get(0) == 380).map(x => x.get(1))
    // user380Movies.foreach(println)

    // 第四步:求380用户和561用户看过电影集合的差值
    val user380SubMovies = bestSimUserMovies.subtract(user380Movies).distinct().map(x => Row(x))
    user380SubMovies.foreach(println)

    // 生成380用户和561用户看过电影集合的差值表
    val user380SubMoviesDF = Array(
      StructField("movie_id",IntegerType,nullable = true)
    )
    val structType = StructType(user380SubMoviesDF)
    val subMovie = spark.createDataFrame(user380SubMovies,structType)

    // 第五步:求出的差值电影集合就是推荐给380用户的电影集合
    val recommendResult = subMovie.join(movieData,"movie_id")
    recommendResult.show(false)

    /**
     * +--------+---------------------------+--------------------------------------+
     * |movie_id|movie_name                 |genres                                |
     * +--------+---------------------------+--------------------------------------+
     * |39      |Clueless (1995)            |Comedy|Romance                        |
     * |4848    |Mulholland Drive (2001)    |Crime|Drama|Film-Noir|Mystery|Thriller|
     * |5630    |Red Dragon (2002)          |Crime|Mystery|Thriller                |
     * |5956    |Gangs of New York (2002)   |Crime|Drama                           |
     * |54503   |Superbad (2007)            |Comedy                                |
     * ....
     */

  }

}

(6.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.{
    
    IntegerType, StructField, StructType}
import org.apache.spark.sql.{
    
    Row, SparkSession}

object CosUserOptRecommend {
    
    
  /**
   * 需求:基于用户协同过滤通过调整余弦相似度算法推荐电影
   * 原理:假设要为380用户推荐电影,找到与380用户相似性的用户580,找出380用户和580用户的电影差集,即作为推荐电影推荐给380用户
   * @param args
   */
  def main(args: Array[String]): Unit = {
    
    

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

    import spark.implicits._

    // 用户评分数据源
    val movieRatings = spark.read.textFile("/Users/caizhengjie/Desktop/大数据项目实战三/movieLess/ratings.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| 4.0|
     * |   1|   3| 4.0|
     * |   1|   6| 4.0|
     * |   1|  47| 5.0|
     * |   1|  50| 5.0|
     * |   1|  70| 3.0|
     * |   1| 101| 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 usermean = movieRatings.groupBy("user").mean("rate").toDF("user","mean_rate")
    // usermean.show(false)

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

    user_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()

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

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

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

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

    res_df.where("user_id = 380").sort(res_df("score").desc).show(false)

    /**
     * +-------+-----------+-------------------+
     * |user_id|sim_user_id|score              |
     * +-------+-----------+-------------------+
     * |380    |610        |0.23806705825325608|
     * |380    |596        |0.18474663605898786|
     * |380    |599        |0.18005107150744507|
     * |380    |414        |0.16796304129963588|
     * |380    |448        |0.16273965812801094|
     * ....
     */

    /**
     * 通过调整余弦相似度计算,与用户380最形似的userID为610
     * |380    |610        |0.23806705825325608|
     *
     * 第一步:找出与380用户最相似的用户,目前是610用户
     *
     * 第二步:找出与380用户最相似的用户(610)看过的所有电影
     *
     * 第三步:找出380用户看过的所有电影
     *
     * 第四步:求380用户和610用户看过电影集合的差值
     *
     * 第五步:求出的差值电影集合就是推荐给380用户的电影集合
     *
     */

    // 第一步:找出与380用户最相似的用户,目前是610用户
    val user380_sim = cosinsim.filter(x => x.i == 380).map(line => (line.j,line.value))
    // user380_sim.foreach(println)
    val user380BestSsim = user380_sim.sortBy(_._2,ascending = false).take(1)
    val bestSimUser = user380BestSsim(0)._1
    println(bestSimUser)

    // 第二步:找出与380用户最相似的用户(610)看过的所有电影
    val bestSimUserMovies = movieRatings.rdd.filter(_.get(0) == bestSimUser).map(x => x.get(1))
    // bestSimUserMovies.foreach(println)

    // 第三步:找出380用户看过的所有电影
    val user380Movies = movieRatings.rdd.filter(_.get(0) == 380).map(x => x.get(1))
    // user380Movies.foreach(println)

    // 第四步:求380用户和610用户看过电影集合的差值
    val user380SubMovies = bestSimUserMovies.subtract(user380Movies).distinct().map(x => Row(x))
    user380SubMovies.foreach(println)

    // 生成380用户和561用户看过电影集合的差值表
    val user380SubMoviesDF = Array(
      StructField("movie_id",IntegerType,nullable = true)
    )
    val structType = StructType(user380SubMoviesDF)
    val subMovie = spark.createDataFrame(user380SubMovies,structType)

    // 第五步:求出的差值电影集合就是推荐给380用户的电影集合
    val recommendResult = subMovie.join(movieData,"movie_id")
    recommendResult.show(false)

    /**
     * +--------+------------------------------------------+-----------------------------------------------------------------------------+
     * |movie_id|movie_name                                |genres                                                                       |
     * +--------+------------------------------------------+-----------------------------------------------------------------------------+
     * |1084    |Bonnie and Clyde (1967)                   |Crime|Drama                                                                  |
     * |98633   |My Lucky Stars (Fuk sing go jiu) (1985)   |Action|Comedy                                                                |
     * |77931   |Stingray Sam (2009)                       |Comedy|Musical|Sci-Fi|Western                                                |
     * |4159    |3000 Miles to Graceland (2001)            |Action|Thriller                                                              |
     * |81132   |Rubber (2010)                             |Action|Adventure|Comedy|Crime|Drama|Film-Noir|Horror|Mystery|Thriller|Western|
     * |44828   |Slither (2006)                            |Comedy|Horror|Sci-Fi                                                         |
     * ....
     */

  }

}

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

猜你喜欢

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