《Spark机器学习》笔记——基于MovieLens数据集使用SparkMLib机器学习库构建电影推荐引擎

一、前置知识

《Spark机器学习》笔记——基于MovieLens数据集使用Spark进行电影数据分析


二、

import org.apache.spark.mllib.evaluation.{RankingMetrics, RegressionMetrics}
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.mllib.recommendation.{ALS, Rating}
import org.jblas.DoubleMatrix
object MovieLens100K {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setAppName("MovieLens100K").setMaster("local")//设置在本地模式运行
    //val BASEDIR = "hdfs://pc1:9000/ml-100k/"
    //HDFS文件
    val BASEDIR = "file:///home/chenjie/ml-100k/"//本地文件
    //val sparkConf = new SparkConf().setAppName("MovieLens100K-cluster").setMaster("spark://pc1:7077").setJars(List("untitled2.jar"))
    //设置在集群模式运行
    val sc = new SparkContext(sparkConf)//初始化sc
    val rawData = sc.textFile(BASEDIR + "u.data")//加载评分数据
    println("rawData.first()=" + rawData.first())//打印第一条
    //rawData.first()=196	242	3	881250949
    val rawRatings = rawData.map(_.split("\t").take(3))//提取评分数据前3条,即用户ID,电影ID,评分
    println("rawRatings.first()=" + rawRatings.first().mkString("\t"))//显示第一条
    //rawRatings.first()=196	242	3
    val ratings = rawRatings.map{ case Array(user, movie, rating) => Rating(user.toInt, movie.toInt, rating.toInt)}
    //将文本的评分数据转为ALS模型需要的由Rating类型记录构成的RDD,Rating类则是对用户ID、影片ID和实际星级这些参数的封装,我们可以调用map方法将
    //原来的各ID和星级的数组转换为对应的Rating对象,从而创建所需的评级数据集。
    //这里需要使用toInt或者toDouble来将原来的评级数据(它从文本文件生成,类型为String)转换为Int或者Double类型的数值输入。
    //另外这里使用了case语句来提取各属性对应的变量名并直接使用它们,这样就不用使用val user = ratings(0)之类的表达
    println(ratings.first())//打印第一条
    //Rating(196,242,3.0)

    //-------------------------------------------------------------------------------------------------------------------
    //使用MovieLens100K数据集训练模型
    val model = ALS.train(ratings, 50, 10, 0.01)//使用ALS模型推荐模型
    //其他参数说明:
    //rank:对应ALS模型中的因子个数,也就是在低阶近似矩阵中的隐含特征个数。因子个数一般越多越好。
    //但它也会直接影响模型训练和保存时所需的内存开销,尤其是在用户和物品很多的时候。
    //因此实践中该参数常作为训练效果与系统开销之间的调节参数。
    //通常的合理取值为10-200

    //model.userFeatures.foreach(item=>println(item._1 + "\t" + item._2.mkString(",")))
    //输入model的用户特征矩阵看看
    //1	-0.6535316109657288,-0.6037510633468628,0.6332072019577026,0.18737606704235077,0.3375983238220215,0.0073736789636313915,-0.12045715749263763,-0.5812652111053467,0.29880088567733765,-0.13728629052639008,-0.09697286039590836,-0.14637534320354462,3.515775315463543E-4,-0.22065740823745728,0.13513575494289398,0.076859250664711,-0.2094135582447052,0.24555955827236176,-0.15405645966529846,-0.3045920133590698,0.10059460252523422,-0.020618092268705368,0.14765027165412903,0.10473044216632843,0.02196660079061985,0.02531788870692253,0.22764955461025238,0.31171050667762756,0.045034412294626236,0.769920825958252,0.07801490277051926,-0.344120591878891,-0.43034592270851135,-0.03714103251695633,-0.6188069581985474,-0.28790971636772156,-0.055499736219644547,-0.36045560240745544,0.7146272659301758,-0.5376596450805664,0.1656997948884964,-0.5694717764854431,0.21997453272342682,-0.07728519290685654,0.02702999860048294,0.2283659130334854,0.6393778920173645,0.14596056938171387,0.3659607768058777,0.425907164812088
    //...
    //943	-0.7511817812919617,-0.3436143398284912,-0.19400890171527863,-0.2635320723056793,-0.17646446824073792,0.11344016343355179,0.3668082058429718,0.32604292035102844,0.4723926782608032,0.011148825287818909,0.2238609343767166,-1.360995888710022,-0.08611772209405899,-0.033340465277433395,0.025612173601984978,0.42844778299331665,0.31124842166900635,-0.7533812522888184,0.18900763988494873,0.16926845908164978,0.10810736566781998,0.5141794085502625,0.09619955718517303,-0.7954093217849731,-1.2010024785995483,0.5492983460426331,-0.22642870247364044,0.4203435182571411,0.08060871064662933,0.2756498456001282,-0.5100610256195068,-0.12525323033332825,0.4025658965110779,0.4532598853111267,-0.030040228739380836,-0.030269257724285126,-0.12580832839012146,-0.11279704421758652,0.20144516229629517,-0.21323147416114807,0.3972542881965637,0.10191260278224945,0.08378230035305023,-0.7289848327636719,-0.476211816072464,0.05527716130018234,0.15178674459457397,0.2481926828622818,-0.06736160069704056,-0.03512599319219589

    //---------------------------用户推荐-----------------------------------------------------------------------------


    val predictedRating = model.predict(789,123)  //预测用户789对电影123的评级
    println("predictedRating of user 789 to movie 123 is ",predictedRating)//输出预测评分
    //(predictedRating of user 789 to movie 123 is ,4.067846258282337)

    val userId = 789//设置用户ID为789
    val K = 10//设置前K个物品
    val movies = sc.textFile(BASEDIR + "u.item")//加载电影元数据
    val titles = movies.map(line => line.split("\\|").take(2)).map(array => (array(0).toInt, array(1))).collectAsMap()
    //将电影元数据映射为电影ID到电影标题的map
    println("电影123的标题如下:")
    println(titles(123))//查看电影ID为123的电影标题
    //电影123的标题如下:
    //Frighteners, The (1996)

    val movieForUser = ratings.keyBy(_.user).lookup(789)
    //先将评分数据按user的ID进行分组,然后查找ID为789的用户评分过的所有Rating
    println("用户789评分过的电影如下:")
    movieForUser.foreach(println)//打印用户789评分过的电影
   /* 用户789评分过的电影如下:
      Rating(789,1012,4.0)
    Rating(789,127,5.0)
      ...
    Rating(789,124,4.0)*/

    println("用户789评分过的电影好评top10如下:")
    movieForUser.sortBy(-_.rating).take(10).map(rating => (titles(rating.product), rating.rating)).foreach(println)
    //用户789评分过的电影好评top10
    /*用户789评分过的电影好评top10如下:
    (Godfather, The (1972),5.0)
    (Trainspotting (1996),5.0)
    (Dead Man Walking (1995),5.0)
    (Star Wars (1977),5.0)
    (Swingers (1996),5.0)
    (Leaving Las Vegas (1995),5.0)
    (Bound (1996),5.0)
    (Fargo (1996),5.0)
    (Last Supper, The (1995),5.0)
    (Private Parts (1997),4.0)*/

    val topKRecs = model.recommendProducts(userId, K)//使用模型推荐前K个
    println("用户789的前10个推荐列表如下:")
    topKRecs.map(rating => (titles(rating.product) , rating.rating)).foreach(println)//输出推荐列表
    /*用户789的前10个推荐列表如下:
      (Citizen Kane (1941),5.669431031297325)
    (Terminator 2: Judgment Day (1991),5.519670826047749)
    (GoodFellas (1990),5.41276454771554)
    (High Noon (1952),5.39490260467592)
    (Raiders of the Lost Ark (1981),5.354431039491439)
    (Terminator, The (1984),5.294466167905896)
    (Godfather: Part II, The (1974),5.277987612685323)
    (Treasure of the Sierra Madre, The (1948),5.273160739938701)
    (Mars Attacks! (1996),5.267151565330534)
    (Clockwork Orange, A (1971),5.220534392433441)*/

    //--------------物品推荐--------------------------------------------------------------------------------
    val itemId = 567//物品ID为567
    val itemFactor = model.productFeatures.lookup(itemId).head
    //从模型中取出ID为567的物品的物品特征,lookup返回一个数组而我们只需要第一个值,实际上数组也只有一个值,也就算该物品的因子向量
    val itemVector = new DoubleMatrix(itemFactor)//使用该物品的因子向量构造一个DoubleMatrix对象
    println(consineSimilarity(itemVector, itemVector))//输出该对象与自己的余弦相似度
    //1.0

    val sims = model.productFeatures.map{case (id, factor) =>
        val factorVercotr = new DoubleMatrix(factor)
        val sim = consineSimilarity(factorVercotr, itemVector)
      (id, sim)
    }//计算模型的物品特征里的每一个物品与567号物品的相似度
    val sortedSims = sims.top(K)(Ordering.by[(Int,Double), Double] {
      case (id, similarity) => similarity}
    )//取相似度最大的前K个
    println("与567号电影最相似的top10部电影:")
    sortedSims.foreach(println)//打印相似度最大的前K个
    /*(567,1.0)
    (219,0.6510899594146597)
    (1471,0.6471364877750735)
    (123,0.6464577245603846)
    (563,0.6423806214176864)
    (10,0.6407111280859906)
    (1108,0.6330301769850339)
    (257,0.6322292970826289)
    (448,0.6284841201267528)
    (271,0.6261819085585016)*/
    //观察到与567相似度最大的是它自己,值为1.0,因此想求真正的top10需要求top11


    //下面将ID与title对应起来,便于观察

    println("567号电影:" + titles(itemId))//打印567号电影标题
    //567号电影:Wes Craven's New Nightmare (1994)
    println("相似电影:")
    val sortedSims2 = sims.top(K+1)(Ordering.by[(Int,Double), Double] {
      case (id, similarity) => similarity})
    //取top11
    sortedSims2.map{case (id, sim) => (titles(id), sim)}.foreach(println)
    //关键在于titles(id)将id对应的title查出来

    /*相似电影:
    (Wes Craven's New Nightmare (1994),1.0)
    (Nightmare on Elm Street, A (1984),0.6510899594146597)
    (Hideaway (1995),0.6471364877750735)
    (Frighteners, The (1996),0.6464577245603846)
    (Stephen King's The Langoliers (1995),0.6423806214176864)
    (Richard III (1995),0.6407111280859906)
    (Feast of July (1995),0.6330301769850339)
    (Men in Black (1997),0.6322292970826289)
    (Omen, The (1976),0.6284841201267528)
    (Starship Troopers (1997),0.6261819085585016)
    (Ruby in Paradise (1993),0.6194921020102429)*/

    //----------推荐模型效果评估:均方差-------------------------------------------------------------------------------
    val actualRating = movieForUser.take(1)(0);//取789号用户的评分电影里的第一个评分信息
    println("实际评分:",actualRating)
    //(实际评分:,Rating(789,1012,4.0))
    val predictRating = model.predict(789, actualRating.product)//取模型对789号对其第一条评分电影的预测评分
    println("模型预测得分:", predictedRating)
    //模型预测得分:,3.563209742016148
    val squaredError = math.pow(predictedRating - actualRating.rating, 2.0)//计算平方误差
    println("平方误差=" + squaredError)
    //平方误差=0.19078572946959985

    //下面要计算整个数据集上的MSE,需要对每一条(user,movie,actual rating,predicted rating)记录都记录该平方误差,然后求和,再除以总的评级次数
    val userProducts = ratings.map{ case Rating(user, product, rating) => (user, product)}
    //将用户评分记录映射为用户-物品记录
    val predictions = model.predict(userProducts).map{ case Rating(user, product, rating) => ((user, product), rating)}
    //使用模型预测所有用户-物品记录,将预测得到的Rating对象映射为 用户,物品 - 评分
    val ratingsAndPredictions = ratings.map{
      case Rating(user, product, rating) => ((user, product), rating)
    }.join(predictions)
    //将用户评分记录映射为 用户,物品 - 评分(实际)
    //然后将其与预测得到的 用户,物品 - 评分(预测) 进行连接
    //用户,物品 相同的 用户,物品 - 评分(实际) 和  用户,物品 - 评分(预测) 会进行连接为  用户,物品 - 评分(实际),评分(预测)
    val MSE = ratingsAndPredictions.map{
      case ((user, product), (actual, predicted)) => math.pow(actual - predicted, 2)
    }.reduce(_ + _) / ratingsAndPredictions.count
    //将上步的 用户,物品 - 评分(实际),评分(预测) 的每行计算平方误差
    //然后将每行的平方误差相加
    //总和 / 总数 = 平方误差
    println("平方误差=" + MSE)//输出平方误差
    val RMSE = math.sqrt(MSE)//计算根方误差
    println("根方误差=" + RMSE)//输出根方误差

    //----------推荐模型效果评估:K值平均准确率-------------------------------------------------------------------------------
    /***
      * K值平均准确率(MAPK)的意思是整个数据集上的K值平均准确率的均值。
      * APK是信息检索中常用的一个指标,用于衡量针对某个查询所返回的前K个文档的平均相关性。对于每次查询,我们会将结果中的前K个与实际相关的文档进行比较
      * 用APK指标计算时,结果中文档的排名十分重要。如果结果中文档的实际相关性越高且排名也更靠前,那APL分值也越高。因此它很适合评估推荐的好坏。
      * 因为推荐系统会计算前K个推荐物,然后呈现给用户。如果在预测结果中得分更高(在推荐列表中排名也更靠前)的物品实际上也与用户更相关,那这个模型自然就更好
      * APK所试图衡量的是模型对用户感兴趣和会去接触的物品的预测能力。
      */

    val actualMovies = movieForUser.map(_.product)//提取789号用户实际评级过的电影ID
    val predictedMovies = topKRecs.map(_.product)//提前推荐的物品列表
    val apk10 = avgPrecisionK(actualMovies, predictedMovies, 10)//计算平均准确率
    println("apk 10:" + apk10)//输出APK
    //apk 10:0.0
    
    //下面求全局MAPK。要计算对每一个用户的APK得到,再求其平均。这就要为每一个用户都生成对应的推荐列表。
    
    val itemFactors = model.productFeatures.map{  case (id, factor) => factor}.collect()
    val itemMatrix = new DoubleMatrix(itemFactors)
    //取回物品因子向量并用它构建一个DoubleMatrix对象
    println(itemMatrix.rows,itemMatrix.columns)//打印该itemMatrix对象的维度
    //(1682,50)
    //表明有1682部电影,因子维数为50
    
    
    val imBroadcast = sc.broadcast(itemMatrix)//将该矩阵以一个广播变量的方式分发出去,以便每个工作节点都能访问到
    val allRecs = model.userFeatures.map{ case (userId, array) => //将用户ID和用户因子向量进行映射
      val userVector = new DoubleMatrix(array)//将用户因子向量构造为一个DoubleMatrix对象便于计算
      val scores = imBroadcast.value.mmul(userVector)//将用户因子矩阵和电影因子矩阵做乘积
      //其结果为一个表示各个电影预计评分的向量(长度为1682,即电影数目)
      val sortedWithId = scores.data.zipWithIndex.sortBy(-_._1)//将预计评分与下标进行绑定后按照预计评分从大到小进行排序
      val recommendedIds = sortedWithId.map(_._2 + 1).toSeq//将下标+1,因为下标是从0开始的,电影编号是从1开始的
      (userId, recommendedIds)//返回该用户的推荐列表
    }
    val userMovies = ratings.map{ case Rating(user, product, rating) =>
      (user, product)
    }.groupBy(_._1)//将所有用户的评分记录按照用户ID进行排序,这样用户ID依次从1到1682
    val MAPK = allRecs.join(userMovies).map{ case (userId, (predicted, actualWithIds)) =>
      val actual =  actualWithIds.map(_._2).toSeq
      avgPrecisionK(actual, predicted, K)
    }.reduce(_ + _) / allRecs.count
    //先将所有用户的推荐列表与用户的评分记录进行连接
    //然后再计算每个用户的推荐列表与评分的APK
    //然后再将所有用户的APK求和
    //最后除以用户数,得到MAPK
    println("指定K值时的平均准确度:" + MAPK)
    //指定K值时的平均准确度:0.0651096635189955

    //----------推荐模型效果评估:使用MLib内置的评估函数-------------------------------------------------------------------------------
    /*
      前面我们从零开始对模型进行了MSE、RMSE和MAPK三方面的评估。同样,MLib下的RegressionMetrics和类也提供了相应的函数以方便模型评估
     */
    //1、RMSE和MSE
    val predictedAndTure = ratingsAndPredictions.map{ case((user, product), (predicted, actual)) => (predicted, actual)}
    val regressionMetrics = new RegressionMetrics(predictedAndTure)
    println("平方误差:" + regressionMetrics.meanSquaredError)
    //平方误差:0.08512350197646647
    println("均方根误差:" + regressionMetrics.rootMeanSquaredError)
    //均方根误差:0.2917593220043988
    
    //2、MAP
    //MLib的RankingMetrics类来计算基于排名的评估指标。类似地,需要向我们之前的平均准确率函数传入一个键值对类型的RDD
    //其键为给定用户预测的推荐物品的ID数组,值则是实际的物品ID数组
    val predictedAndTureForRanking = allRecs.join(userMovies).map{  case (userId, (predicted, actualWithIds)) =>
      val actual = actualWithIds.map(_._2)
      (predicted.toArray, actual.toArray)
    }
    val rankingMetrics = new RankingMetrics(predictedAndTureForRanking)
    println("Mean Average Precision =" + rankingMetrics.meanAveragePrecision)

    val MAPK2000 = allRecs.join(userMovies).map{  case (userId, (predicted, actualWithIds)) =>
      val actual = actualWithIds.map(_._2).toSeq
      avgPrecisionK(actual, predicted, 2000)
    }.reduce(_ + _) / allRecs.count
    println("Mean Average Precision = " + MAPK2000)
  }

  /***
    * 计算两个向量之间的余弦相似度
    * @param vec1 向量1
    * @param vec2 向量2
    * @return 余弦相似度
    */
  def consineSimilarity(vec1: DoubleMatrix, vec2: DoubleMatrix):Double={
    vec1.dot(vec2) / (vec1.norm2() * vec2.norm2())
    //            x.y
    //cos<x,y>= --------
    //           |x||y|
  }

  /***
    * 计算APK(K值平均准确率)
    * @param actual
    * @param predicted
    * @param k
    * @return
    */
  def avgPrecisionK(actual: Seq[Int], predicted: Seq[Int], k: Int): Double ={
    val predK = predicted.take(k)
    var score = 0.0
    var numHits = 0.0
    for ((p, i) <- predK.zipWithIndex){
      if(actual.contains(p)){
        numHits += 1.0
        score += numHits / (i.toDouble + 1.0)
      }
    }
    if(actual.isEmpty){
      1.0
    }
    else{
      score / scala.math.min(actual.size, k).toDouble
    }
  }
}


  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值