Spark MLlib协同过滤推荐算法实现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_41455420/article/details/89849621

一、算法概述

通过分析用户兴趣,在用户群中找到指定用户的相似用户,综合这些相似用户对某一信息的评价,形成系统关于该指定用户对此信息的喜好程度预测。
实现协同过滤需要以下3个步骤:

  • 收集用户偏好
  • 找到相似的用户或物品
  • 计算推荐

二、用户评分

从用户的行为和偏好中发现规律,并基于此进行推荐,所以收集用户的偏好信息成为系统推荐效果最基础的决定因素。用户有很多方式向系统提供自己的偏好信息,如:评分、投票、转发、购买、点击流、页面停留时间等。
以上用户行为都是通用的,在实际设计中添加一些特定的用户行为,并用他们表示用户对物品的喜好程度。如何组合用户的行为基本有如下两种方式。

  1. 将不同的行为分组
    一般分为查看和购买,然后基于不同用户的行为,计算不用用户或物品的相似度。类似“购买了该书的人还购买了”“查看了该书的人还查看了”
  2. 对不同行为加权

对于不同行为产生的用户喜好进行加权,然后求出用户对物品的总体喜好。
收集完数据后要进行数据预处理核心工作是减噪和归一化

  • 减噪:数据中可能存在大量噪声和误操作
  • 归一化:不同行为数据差别比较大,通过归一化

经过以上处理得到如下一张二维表,值是用户对商品的喜好:

用户/物品 物品A 物品B 物品C
用户A 0.1 0.8 1
用户B 0.1 0 0.02
用户C 0.5 0.3 0.1

三、相似度计算

协同过滤中分为基于用户的协同过滤和基于物品的协同过滤。相似度计算现有的几种方法是基于向量的,即计算两个向量的距离,距离越近相似度越大。在与用户-物品偏好二维矩阵中将一个用户对所有物品的偏好作为一个向量来计算用户之间的相似度,或者将所有用户对某个物品的偏好作为一个向量来计算物品之间的相似度。简要介绍如下5种。

  1. 同现相似度
    物品i和物品j的同相似度公式定义
    在这里插入图片描述
    其中,分母是喜欢物品i的用户数,而分子则是同时喜欢物品i和物品j的用户数。因此,上述公式可用理解为喜欢物品i的用户有多少比例的用户也喜欢j (和关联规则类似)但上述的公式存在一个问题,如果物品j是热门物品,有很多人都喜欢,则会导致Wij很大,接近于1。因此会造成任何物品都和热门物品交有很大的相似度。为此我们用如下公式进行修正:

在这里插入图片描述
这个格式惩罚了物品j的权重,因此减轻了热门物品和很多物品相似的可能性。

  1. 欧式距离
    最初用于计算欧几里得空间中的两个点的距离,假设x、y是n维空间的两个点,它们之间的欧几里得距离是:
    在这里插入图片描述

可以看出,当n=2时,欧几里得距离是平面上的两点的距离。当用欧几里得距离表示相似度时,一般采用一下公式进行转换:距离越小,相似度越大。
在这里插入图片描述

  1. 皮尔逊相关系数
    皮尔逊相关系数一般用于计算两个定距变量间联系的紧密程度,它的取值在[-1,+1]之间。
    在这里插入图片描述
  2. Cosine相似度
    Cosine相似度被广泛应用于计算文档数据的相似度:
    在这里插入图片描述
  3. Tanimoto系数
    Tanimoto系数由Jaccard系数扩展而来,多用于文档计算,详细介绍
    在这里插入图片描述

四、推荐计算

  1. 基于用户的CF(userCF)
    基于用户对物品的偏好找到相邻的用户,然后将邻居用户喜欢的推荐给当前用户。在计算上将一个用户对所有物品的偏好作为一个向量计算用户之间相似度,找到K邻居后,根据邻居的相似度权重及其对物品的偏好,预测当前用户没有偏好的未涉及物品,计算得到一个排序的物品列表作为推荐。
  2. 基于物品的CF(itemCF)
    基于物品的CF的原理和基于用户的CF类似,在计算邻居时采用物品本身。

五、训练数据

1,1,1
1,2,1
2,1,1
2,3,1
3,3,1
3,4,1
4,2,1
4,4,1
5,1,1
5,2,1
5,3,1
6,4,1

六、实战代码

相似度计算类ItemSimilarity,支持同现相似度、余弦相似度、欧氏距离相似度

package itemCF

import scala.math._
import org.apache.spark.rdd.RDD


/**
  * 物品相似度计算类
  * 通过设置模型参数后执行Similarity方法,进行相似度计算,返回物品与物品相似度RDD
  * 相似度计算支持:同现相似度、余弦相似度、欧氏距离相似度
  *
  */

/**
  * 用户评分
  *
  * @param userid 用户
  * @param itemid 评分物品
  * @param pref   评分
  */
case class ItemPref(
                     val userid: String,
                     val itemid: String,
                     val pref: Double
                   ) extends Serializable

/**
  * 用户推荐
  *
  * @param userid 用户
  * @param itemid 推荐物品
  * @param pref   评分
  */
case class UserRecomm(
                       val userid: String,
                       val itemid: String,
                       val pref: Double
                     ) extends Serializable

/**
  * 相似度
  *
  * @param itemid1 物品
  * @param itemid2 物品
  * @param similar 相似度
  */
case class ItemSimi(
                     val itemid1: String,
                     val itemid2: String,
                     val similar: Double
                   ) extends Serializable

/**
  * 相似度计算
  * 支持同现相似度、余弦相似度、欧氏距离相似度
  */
class ItemSimilarity extends Serializable {

  /**
    * 相似度计算
    *
    * @param user_rdd 用户评分
    * @param stype    计算相似度方式
    * @return 返回物品相似度
    */
  def Similarity(user_rdd: RDD[ItemPref], stype: String): (RDD[ItemSimi]) = {
    val simil_rdd = stype match {
      case "cooccurrence" => ItemSimilarity.CooccurrenceSimilarity(user_rdd)
      case "cosine" => ItemSimilarity.CosineeSimilarity(user_rdd)
      case "euclidean" => ItemSimilarity.EuclideanDistanceSimilarity(user_rdd)
      case _ => ItemSimilarity.CooccurrenceSimilarity(user_rdd)
    }
    simil_rdd
  }

}

object ItemSimilarity {
  /**
    * 同现相似度矩阵计算
    * w(i,j)=N(i)∩N(j)/sqrt(N(i)*N(j))
    *
    * @param user_rdd 用户评分
    * @return 返回物品相似度
    */
  def CooccurrenceSimilarity(user_rdd: RDD[ItemPref]): (RDD[ItemSimi]) = {
    //1.数据准备
    val user_rdd1: RDD[(String, String, Double)] = user_rdd.map(f => (f.userid, f.itemid, f.pref))
    val user_rdd2: RDD[(String, String)] = user_rdd1.map(f => (f._1, f._2))
    //2.(用户,物品)笛卡尔积操作=>物品与物品组合
    val user_rdd3: RDD[(String, (String, String))] = user_rdd2.join(user_rdd2)
    val user_rdd4: RDD[((String, String), Int)] = user_rdd3.map(f => (f._2, 1))
    //3.(物品,物品,频次)
    val user_rdd5: RDD[((String, String), Int)] = user_rdd4.reduceByKey((x, y) => x + y)
    //4.对角矩阵
    val user_rdd6: RDD[((String, String), Int)] = user_rdd5.filter(f => f._1._1 == f._1._2)
    //5.非对角矩阵
    val user_rdd7: RDD[((String, String), Int)] = user_rdd5.filter(f => f._1._1 != f._1._2)
    //6.计算同现相似度(物品1,物品2,同现频次)
    val user_rdd8: RDD[(String, ((String, String, Int), Int))] = user_rdd7.
      map(f => (f._1._1, (f._1._1, f._1._2, f._2))).join(user_rdd6.map(f => (f._1._1, f._2)))
    val user_rdd9: RDD[(String, (String, String, Int, Int))] = user_rdd8.map(f => (f._2._1._2, (f._2._1._1, f._2._1._2, f._2._1._3, f._2._2)))
    val user_rdd10: RDD[(String, ((String, String, Int, Int), Int))] = user_rdd9.join(user_rdd6.map(f => (f._1._1, f._2)))
    val user_rdd11: RDD[(String, String, Int, Int, Int)] = user_rdd10.map(f => (f._2._1._1, f._2._1._2, f._2._1._3, f._2._1._4, f._2._2))
    val user_rdd12: RDD[(String, String, Double)] = user_rdd11.map(f => (f._1, f._2, (f._3 / sqrt(f._4 * f._5))))
    //7.结果返回
    user_rdd12.map(f => ItemSimi(f._1, f._2, f._3))
  }

  /**
    * 余弦相似度矩阵计算
    * T(x,y)=∑x(i)y(i)/sqrt(∑(x(i)*y(i))*∑(y(i)*y(i)))
    *
    * @param user_rdd 用户评分
    * @return 返回物品相似度
    */
  def CosineeSimilarity(user_rdd: RDD[ItemPref]): (RDD[ItemSimi]) = {
    //1.数据准备
    val user_rdd1: RDD[(String, String, Double)] = user_rdd.map(f => (f.userid, f.itemid, f.pref))
    val user_rdd2: RDD[(String, (String, Double))] = user_rdd1.map(f => (f._1, (f._2, f._3)))
    //2.(用户,物品,评分)笛卡尔积操作=>(物品1,物品2,评分1,评分2)组合
    val user_rdd3: RDD[(String, ((String, Double), (String, Double)))] = user_rdd2.join(user_rdd2)
    val user_rdd4: RDD[((String, String), (Double, Double))] = user_rdd3.map(f => ((f._2._1._1, f._2._2._1), (f._2._1._2, f._2._2._2)))
    //3.(物品1,物品2,评分1,评分2,)组合=>(物品1,物品2,评分1*评分2)组合并累加
    val user_rdd5: RDD[((String, String), Double)] = user_rdd4.map(f => (f._1, f._2._1 * f._2._2)).reduceByKey(_ + _)
    //4.对角矩阵
    val user_rdd6: RDD[((String, String), Double)] = user_rdd5.filter(f => f._1._1 == f._1._2)
    //5.非对角矩阵
    val user_rdd7: RDD[((String, String), Double)] = user_rdd5.filter(f => f._1._1 != f._1._2)
    //6.计算相似度
    val user_rdd8: RDD[(String, ((String, String, Double), Double))] = user_rdd7.map(f => (f._1._1, (f._1._1, f._1._2, f._2))).join(user_rdd6.map(f => (f._1._1, f._2)))
    val user_rdd9: RDD[(String, (String, String, Double, Double))] = user_rdd8.map(f => (f._2._1._2, (f._2._1._1, f._2._1._2, f._2._1._3, f._2._2)))
    val user_rdd10: RDD[(String, ((String, String, Double, Double), Double))] = user_rdd9.join(user_rdd6.map(f => (f._1._1, f._2)))
    val user_rdd11: RDD[(String, String, Double, Double, Double)] = user_rdd10.map(f => (f._2._1._1, f._2._1._2, f._2._1._3, f._2._1._4, f._2._2))
    val user_rdd12: RDD[(String, String, Double)] = user_rdd11.map(f => (f._1, f._2, (f._3 / sqrt(f._4 * f._5))))
    //7.结果返回
    user_rdd12.map(f => ItemSimi(f._1, f._2, f._3))
  }

  /**
    * 欧氏距离相似度矩阵计算
    * d(x,y)=sqrt(∑((x(i)-y(i))*(x(i)-y(i))))
    * sim(x,y)=n/(1+d(x,y))
    *
    * @param user_rdd 用户评分
    * @return 返回物品相似度
    */
  def EuclideanDistanceSimilarity(user_rdd: RDD[ItemPref]): (RDD[ItemSimi]) = {
    //1.数据准备
    val user_rdd1: RDD[(String, String, Double)] = user_rdd.map(f => (f.userid, f.itemid, f.pref))
    val user_rdd2: RDD[(String, (String, Double))] = user_rdd1.map(f => (f._1, (f._2, f._3)))
    //2.(用户,物品,评分)笛卡尔积操作=>(物品1,物品2,评分1,评分2)组合
    val user_rdd3: RDD[(String, ((String, Double), (String, Double)))] = user_rdd2 join user_rdd2
    val user_rdd4: RDD[((String, String), (Double, Double))] = user_rdd3.map(f => ((f._2._1._1, f._2._2._1), (f._2._1._2, f._2._2._2)))
    //3.(物品1,物品2,评分1,评分2)组合=>(物品1,物品2,评分1-评分2)组合并累加
    val user_rdd5: RDD[((String, String), Double)] = user_rdd4.map(f => (f._1, (f._2._1 - f._2._2) * (f._2._1 - f._2._2))).reduceByKey(_ + _)
    //4.(物品1,物品2,评分1,评分2)组合=>(物品1,物品2,1)组合计算物品1和物品2的重叠度
    val user_rdd6: RDD[((String, String), Int)] = user_rdd4.map(f => (f._1, 1)).reduceByKey(_ + _)
    //5.非对角矩阵
    val user_rdd7: RDD[((String, String), Double)] = user_rdd5.filter(f => f._1._1 != f._1._2)
    //6.相似度计算
    val user_rdd8: RDD[((String, String), (Double, Int))] = user_rdd7.join(user_rdd6)
    val user_rdd9: RDD[(String, String, Double)] = user_rdd8.map(f => (f._1._1, f._1._2, f._2._2 / (1 + sqrt(f._2._1))))
    //7.结果返回
    user_rdd9.map(f => ItemSimi(f._1, f._2, f._3))
  }


}

推荐计算类RecommendedItem,推荐计算根据物品相似度和用户评分进行推荐物品计算,并过滤用户已有物品及过滤最大过滤推荐数量。

package itemCF

import org.apache.spark.rdd.RDD

import scala.collection.mutable

/**
  * 物品推荐计算类
  * 通过设置模型参数,执行Recommend方法进行推荐计算,返回用户的推荐物品RDD
  * 推荐计算根据物品相似度和用户评分进行推荐物品计算,并过滤用户已有物品及过滤最大过滤推荐数量
  */
class RecommendedItem {

  /**
    * 用户推荐计算
    *
    * @param items_similar 物品相似度
    * @param user_prefer   用户评分
    * @param r_number      推荐数量
    * @return 返回用户推荐物品
    */
  def Recommend(items_similar: RDD[ItemSimi], user_prefer: RDD[ItemPref], r_number: Int): (RDD[UserRecomm]) = {
    //1.数据准备
    val rdd_app1_R1: RDD[(String, String, Double)] = items_similar.map(f => (f.itemid1, f.itemid2, f.similar))
    val user_prefer1: RDD[(String, String, Double)] = user_prefer.map(f => (f.userid, f.itemid, f.pref))
    //2.矩阵计算(i行j列join)
    val rdd_app1_R2: RDD[(String, ((String, Double), (String, Double)))] = rdd_app1_R1.map(f => (f._1, (f._2, f._3))).join(user_prefer1.map(f => (f._2, (f._1, f._3))))
    //3.矩阵计算(i行j列相乘)
    val rdd_app1_R3: RDD[((String, String), Double)] = rdd_app1_R2.map(f => ((f._2._2._1, f._2._1._1), f._2._2._2 * f._2._1._2))
    //4.矩阵计算(用户:元素累加求和)
    val rdd_app1_R4: RDD[((String, String), Double)] = rdd_app1_R3.reduceByKey((x, y) => x + y)
    //5.矩阵计算(用户:对结果过滤已有物品)
    val rdd_app1_R5: RDD[(String, (String, Double))] = rdd_app1_R4.leftOuterJoin(user_prefer1.map(f => ((f._1, f._2), 1))).filter(f => f._2._2.isEmpty).map(f => (f._1._1, (f._1._2, f._2._1)))
    //6.矩阵计算(用户:用户对结果排序,过滤)
    val rdd_app1_R6: RDD[(String, Iterable[(String, Double)])] = rdd_app1_R5.groupByKey()
    val rdd_app1_R7: RDD[(String, Iterable[(String, Double)])] = rdd_app1_R6.map(f => {
      val i2: mutable.Buffer[(String, Double)] = f._2.toBuffer
      val i2_2: mutable.Buffer[(String, Double)] = i2.sortBy(_._2)
      if (i2_2.length > r_number) i2_2.remove(0, (i2_2.length - r_number))
      (f._1, i2_2.toIterable)
    })
    val rdd_app1_R8: RDD[(String, String, Double)] = rdd_app1_R7.flatMap(f => {
      val id2: Iterable[(String, Double)] = f._2
      for (w <- id2) yield (f._1, w._1, w._2)
    })
    rdd_app1_R8.map(f => UserRecomm(f._1, f._2, f._3))
  }

  /**
    * 用户推荐计算
    *
    * @param items_similar 物品相似度
    * @param user_prefer   用户评分
    * @return 返回用户推荐物品
    */
  def Recommend(items_similar: RDD[ItemSimi], user_prefer: RDD[ItemPref]): (RDD[UserRecomm]) = {
    //1.数据准备
    val rdd_app1_R1: RDD[(String, String, Double)] = items_similar.map(f => (f.itemid1, f.itemid2, f.similar))
    val user_prefer1: RDD[(String, String, Double)] = user_prefer.map(f => (f.userid, f.itemid, f.pref))
    //2.矩阵计算(i行和j列join)
    val rdd_app1_R2: RDD[(String, ((String, Double), (String, Double)))] = rdd_app1_R1.map(f => (f._1, (f._2, f._3))).join(user_prefer1.map(f => (f._2, (f._1, f._3))))
    //3.矩阵计算(i行j列元素相乘)
    val rdd_app1_R3: RDD[((String, String), Double)] = rdd_app1_R2.map(f => ((f._2._2._1, f._2._1._1), f._2._2._2 * f._2._1._2))
    //4.矩阵计算(用户:元素累加求和)
    val rdd_app1_R4: RDD[((String, String), Double)] = rdd_app1_R3.reduceByKey((x, y) => x + y)
    //5.矩阵计算(用户:对结果过滤已有物品)
    val rdd_app1_R5: RDD[(String, (String, Double))] = rdd_app1_R4.leftOuterJoin(user_prefer1.map(f => ((f._1, f._2), 1))).filter(f => f._2._2.isEmpty).map(f => (f._1._1, (f._1._2, f._2._1)))
    //6.矩阵计算(用户:用户对结果排序,过滤)
    val rdd_app1_R6: RDD[(String, String, Double)] = rdd_app1_R5.map(f => (f._1, f._2._1, f._2._2)).sortBy(f => (f._1, f._3))
    rdd_app1_R6.map(f => UserRecomm(f._1, f._2, f._3))
  }

}

基于物品推荐类ItemCF

package itemCF

import org.apache.log4j.{Level, Logger}
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

//基于物品推荐
object ItemCF {
  def main(args: Array[String]): Unit = {
    //1.构建Spark对象
    val conf: SparkConf = new SparkConf().setAppName("ItemCF").setMaster("local[2]")
    val sc = new SparkContext(conf)
    Logger.getRootLogger.setLevel(Level.WARN)

    //2.读取数据
    val data_path = "hdfs://node-1:9000/spark_data/sample_itemCF.txt"
    val data: RDD[String] = sc.textFile(data_path)
    val user_data: RDD[ItemPref] = data.map(_.split(",")).map(f => (ItemPref(f(0), f(1), f(2).toDouble))).cache()

    //3.建立模型
    val mysimil = new ItemSimilarity()
    val simil_rdd1: RDD[ItemSimi] = mysimil.Similarity(user_data,"cooccurrence")
    val recommd = new RecommendedItem
    val recommd_rdd1: RDD[UserRecomm] = recommd.Recommend(simil_rdd1,user_data,30)

    //4.打印结果
    println(s"物品相似度矩阵(物品i,物品j,相似度):${simil_rdd1.count()}")
    simil_rdd1.collect().foreach{ ItemSimi =>
      println(ItemSimi.itemid1+","+ItemSimi.itemid2+","+ItemSimi.similar)
    }
    println(s"用户推荐列表(用户,物品,推荐值):${recommd_rdd1.count()}")
    recommd_rdd1.collect().foreach{
      UserRecomm=> println(UserRecomm.userid+","+UserRecomm.itemid+","+UserRecomm.pref)
    }

    sc.stop()
  }
}

七、运行结果

物品相似度矩阵(物品i,物品j,相似度):10
2,4,0.3333333333333333
3,4,0.3333333333333333
4,2,0.3333333333333333
3,2,0.3333333333333333
1,2,0.6666666666666666
4,3,0.3333333333333333
2,3,0.3333333333333333
1,3,0.6666666666666666
2,1,0.6666666666666666
3,1,0.6666666666666666
用户推荐列表(用户,物品,推荐值):11
4,3,0.6666666666666666
4,1,0.6666666666666666
6,2,0.3333333333333333
6,3,0.3333333333333333
2,4,0.3333333333333333
2,2,1.0
5,4,0.6666666666666666
3,2,0.6666666666666666
3,1,0.6666666666666666
1,4,0.3333333333333333
1,3,1.0

Process finished with exit code 0

喜欢就点赞评论+关注吧

在这里插入图片描述

感谢阅读,希望能帮助到大家,谢谢大家的支持!

展开阅读全文

没有更多推荐了,返回首页