电影推荐系统-整体总结(四)离线推荐

Posted on 2020-10-17 18:00  MissRong  阅读(748)  评论(0)    收藏  举报

电影推荐系统-整体总结(四)离线推荐

一、Scala代码实现

1.自定义数据类--Model.scala

package offlineRecommender

/**
  * @Author : ASUS and xinrong
  * @Version : 2020/9/4
  *  数据格式转换类
  *  ---------------电影表------------------------
  *  1
  *  Toy Story (1995)
  *
  *  81 minutes
  *  March 20, 2001
  *  1995
  *  English
  *  Adventure|Animation|Children|Comedy|Fantasy
  *  Tom Hanks|Tim Allen|Don Rickles|Jim Varney|Wallace Shawn|John Ratzenberger|Annie Potts|John Morris|Erik von Detten|Laurie Metcalf|R. Lee Ermey|Sarah Freeman|Penn Jillette|Tom Hanks|Tim Allen|Don Rickles|Jim Varney|Wallace Shawn
  *  John Lasseter
  */
case class Movie(val mid:Int,val name:String,val descri:String,
                 val timelong:String,val cal_issue:String,val shoot:String,
                 val language:String,val genres :String,val actors:String,val directors:String)
/**
  * -----用户对电影的评分数据集--------
  * 1,31,2.5,1260759144
  */
case class MovieRating(val uid:Int,val mid:Int,val score:Double,val timestamp:Int)

/**
  * --------用户对电影的标签数据集--------
  * 15,339,sandra 'boring' bullock,1138537770
  */
case class Tag(val uid:Int,val mid:Int,val tag:String,val timestamp:Int)

/**
  *
  * MongoDB配置对象
  * @param uri
  * @param db
  */
case class MongoConfig(val uri:String,val db:String)

/**
  * ES配置对象
  * @param httpHosts
  * @param transportHosts:保存的是所有ES节点的信息
  * @param clusterName
  */
case class EsConfig(val httpHosts:String,val transportHosts:String,val index:String,val clusterName:String)

/**
  * recs的二次封装数据类
  * @param mid
  * @param res
  */
case class Recommendation(mid:Int,res:Double)

/**
  * Key-Value封装数据类
  * @param genres
  * @param recs
  */
case class GenresRecommendation(genres:String,recs:Seq[Recommendation])
//注:Seq-Sequence是一个特质,可以理解成一个列表;Recommendation是一个实现类

/**
  * 基于用户的推荐-用户相似度
  * @param uid
  * @param recs
  */
case class UserRecs(uid:Int,recs:Seq[Recommendation])

/**
* 定义电影相似度
* @param mid
* @param recs
* 注:Seq-Sequence是一个特质,可以理解成一个列表;Recommendation是一个自定义实现类
*/
case class MovieRecs(mid:Int,recs:Seq[Recommendation])

2.离线推荐算法实现类--OfflineRecommender

package offlineRecommender

import org.apache.spark.SparkConf
import org.apache.spark.mllib.recommendation.{ALS, Rating}
import org.apache.spark.sql.SparkSession
import org.jblas.DoubleMatrix

/**
  * @Author : ASUS and xinrong
  * @Version : 2020/9/20
  *         离线推荐算法
  */
object OfflineRecommender {

   //声明要用的表
  val MONGODB_RATING_COLLECTION="Rating"
  val MONGODB_MOVIE_COLLECTION="Movie"
  //将要取前几位的数量定义成常量,便于修改
  val USER_MAX_RECOMMENDATION=10
  //指定表用于存储基于用户的推荐
  val MONGODB_USER_RECE="UserRecs"
  //指定表用于存储两个电影的相似度相关数据
  val MONGODB_MOVIE_RECS="MovieRecs"

  def main(args: Array[String]): Unit = {

    val conf=Map(
      "spark.core"->"local[2]",
      "mongodb.uri"->"mongodb://192.168.212.21:27017/recom",
      "mongodb.db"->"recom"
    )
    //1.创建Spark运行的环境(第二种创建环境的表现形式)
    //因为涉及到计算,所以这里设置一下executor的内存的大小和driver的内存大小
    val sparkConf=new SparkConf().setAppName("OfflineRecommender").setMaster(conf("spark.core"))
      .set("spark.executor.memory","6G")
      .set("spark.driver.memory","2G")
    val sparkSession = SparkSession.builder().config(sparkConf).getOrCreate()
    //2.获取MongoDB中的数据-----------------------------------------------------------------
    //1)获取MongoDB的连接信息
    val mongoConfig = new MongoConfig(conf("mongodb.uri"),conf("mongodb.db"))
    //2)获取数据
    import sparkSession.implicits._
    //将DataFrame数据转换成RDD数据,需要先.as[MovieRating]再.rdd()
    //(1)获取Rating的数据
    val ratingRDD=sparkSession
      .read
      .option("uri",mongoConfig.uri)
      .option("collection",MONGODB_RATING_COLLECTION)
      .format("com.mongodb.spark.sql")
      .load()
      .as[MovieRating] //Dataset[MovieRating]
      .rdd  //RDD[MovieRating]
      .map(x=>(x.uid,x.mid,x.score)) //RDD[(Int,Int,Double)]
      .cache()

    //(2)获取Movie的数据
    val movieRDD=sparkSession
      .read
      .option("uri",mongoConfig.uri)
      .option("collection",MONGODB_MOVIE_COLLECTION)
      .format("com.mongodb.spark.sql")
      .load()
      .as[Movie]
      .rdd
      .map(_.mid)
      .cache()

    //3.训练ALS(Alternatingleast squares)-交替最小二乘法模型-------------------------------------
    /**
      * ALS算法属于User-Item CF,也叫做混合CF。它同时考虑了User和Item两个方面。
      * ALS模型需要4个参数
      * 1)trainData-训练数据(训练集):Rating(Spark MLlib提供的)对象的RDD--包含用户ID、物品ID,偏好值
      * 2)Rank-特征维度(特征值,它越大越好):50
      * 3)Iterations-迭代次数(也是有越多越好):5次(根据自己的算力和用户需求来设置)
      * 4)Lambda-防过拟合参数(跨度):0.01
      */
    //1)构建训练数据集、得到训练模型----------------------------------------------------------------------------------
    //下面这行中的Rating是Spark Mllib里面提供的-包含三种数据如下:
    //userId-用户ID、product-物品ID、rating-偏好值(他们三个统一在一起就成为Rating)
    val trainData=ratingRDD.map(x=>Rating(x._1,x._2,x._3))
    //    val rank=50 //定义维度
    //    val iterations=5 //定义迭代次数
    //    val lambda=0.01 //设置跨度
    //如果有多个数据,建议用元组的方式将他们钉在一起
    //这样更方便
    val (rank,iteration,lambda)=(50,5,0.01)
    val model=ALS.train(trainData,rank,iteration,lambda)
    //2)计算用户推荐矩阵--(基于用户的推荐)-----------------------------------------------------------------------------
    val userRDD=ratingRDD.map(_._1).distinct().cache()
    println("*********** userRDD **********")
    userRDD.toDF().show()
    //(1)将 含有所有用户名的数据集合 和 含有所有电影名的数据集合 做笛卡儿积(RDD之间的笛卡儿积运算)--RDD[(Int,Int)]
    val userWithMovie=userRDD.cartesian(movieRDD.distinct())
    println("************ userMovies ***********")
    userWithMovie.toDF().show()
    //(2)连带模型和做好笛卡儿积之后的表得出预测数据
    //predict()方法可以用来对新的数据点或数据点组成的RDD应用该模型进行预测
    val predictData = model.predict(userWithMovie)

    //4.将数据写入MongoDB-------------------------------------------------------------
    //1)Filter--将相关性强的数据写入,这里就是将偏好值大于0的数据写入
    val userRecs=predictData
      .filter(_.rating>0)
    //Spark Mllib 提供的Rating是RDD类型的,这里将其转换成Double类型的
    //用户ID-user作为Key,(物品ID-product、偏好值-rating)作为value
    //rating123就是preRatings里面的Rating(注:preRatings的类型为:RDD[Rating])
      .map{
      rating123=>(rating123.user,(rating123.product,rating123.rating))
    //2)groupBy
    }.groupByKey()
    //3)将数据转换成MongoDB能够接收的形式--需要自定义数据类
     .map{
     case(uid,predict)=>
       //在Model里面新建自定义数据类
      UserRecs(uid,predict.toList.sortWith(_._2>_._2).take(USER_MAX_RECOMMENDATION).map(x=>Recommendation(x._1,x._2)))
    }
    //转成DataFrame再写入MongoDB
      .toDF()
    //4)写入MongoDB
      userRecs
      .write
      .option("uri",mongoConfig.uri)
      .option("collection",MONGODB_USER_RECE)
      .mode("overwrite")
      .format("com.mongodb.spark.sql")
      .save()

    //5.计算电影相似度矩阵-(基于物品的推荐)-----------------------------------------------------------------------------------------
    //1)获取电影的50维向量的特征矩阵(productFeatures)-S(q属于S)
      //并将它的features转换成DoubleMatrix便于之后基于org.jblas计算余弦相似度
      //DoubleMatrix就是来自org.jblas包,
      //movieFeatures是RDD[(Int-mid,DoubleMatrix-feature)],这两列数据
    val movieFeatures =model.productFeatures.map{
      //(电影ID,产品特征)  ,      将特征-feature矩阵化
      case(mid,feature)=> (mid,new DoubleMatrix(feature))
    }

    //2)自己和自己做笛卡儿积--求出每一个电影和所有电影的相似性(可能会是稀疏的)
    val movieMatrix = movieFeatures.cartesian(movieFeatures)
    //过滤前--movieRecs:RDD[(Int,DoubleMatrix),(Int,DoubleMatrix)]
    //过滤掉自己和自己的相似度数据
    val movieRecs=movieMatrix.filter{
      case(a,b)=>
        a._1!=b._1
    } //3)求余弦相似度、格式变换
      .map{
      case(a,b)=>
        //电影相似性评分--余弦相似性,自定义方法
        //(电影1,(电影2,电影1和电影2的相似度))
        //(Int,(Int,Double))
        (a._1,(b._1,this.consinSim(a._2,b._2))) //这个this写不写均可
    }// 4)取出相似度大于0.6的数据(余弦值越接近1,就越相似)
      .filter(_._2._2>0.6)
    //5)分组 (Int,Iterable[(Int,Double)])
      .groupByKey()
    //6)转换成MongoDB可以存储的形式-通过自定义数据类
      .map{
      case(mid,recs)=>
      //自定义数据类
        //recs在.toList之前数据类型是:Iterable[(Int,Double)]
        //recs在.toList之后数据类型是:List[(Int,Double)]
      MovieRecs(mid,recs.toList.map(x=>Recommendation(x._1,x._2)))
    } //7)转成DataFrame
      .toDF()
      // 8)写入MongoDB
      movieRecs
        .write
      .option("uri", mongoConfig.uri)
      .option("collection", MONGODB_MOVIE_RECS)
      .mode("overwrite")
      .format("com.mongodb.spark.sql")
      .save()

    //6.关闭Spark---------------------------------------------------------------------
    sparkSession.close()
  }
  /**
    * 计算两个电影间的余弦相似度
    *
    * @param movie1 在求余弦相似度的时候,将其看成一个向量
    * @param movie2 在求余弦相似度的时候,将其看成一个向量
    * @return
    */
  def consinSim(movie1: DoubleMatrix, movie2: DoubleMatrix)= {
    //点乘:dot()、模:norm2()
    movie1.dot(movie2)/(movie1.norm2()*movie2.norm2())
  }
}

1)离线推荐算法-ALS实现过程图:

 

 2)基于用户、基于物品的推荐功能实现的主要方法---解释过程图

 

其中model是训练好的ALS(交替最小二乘法模型):

 3.ALS优化Scala代码实现--ALSTrainer

package offlineRecommender

import breeze.numerics.sqrt
import org.apache.spark.SparkConf
import org.apache.spark.mllib.recommendation.{ALS, MatrixFactorizationModel, Rating}
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.SparkSession

/**
  * @Author : ASUS and xinrong
  * @Version : 2020/9/21
  *         优化ALS
  *         原理:遍历所有业务范围内的取值情况,找到最优的模型
  *         val(rank,iteration,lambda)=(50,5,0.01)
  *         模型的评价:预测值和实际的误差值最小
  */
object ALSTrainer {
   def main(args: Array[String]): Unit = {
    val conf=Map(
      "spark.core"->"local[2]",
      "mongodb.uri"->"mongodb://192.168.212.21:27017/recom",
      "mongodb.db"->"recom"
    )
    //一、创建Spark环境
  val sparkConf=new SparkConf().setAppName("ALSTrainer").setMaster(conf("spark.core"))
  val sparkSession = SparkSession.builder().config(sparkConf).getOrCreate()
    //二、加载评分数据
  import sparkSession.implicits._
  val mongoConfig = new MongoConfig(conf("mongodb.uri"),conf("mongodb.db"))
  val ratingRDD= sparkSession
    .read
    .option("uri", mongoConfig.uri)
    .option("collection", OfflineRecommender.MONGODB_RATING_COLLECTION)
    .format("com.mongodb.spark.sql") //格式化
    .load()
    .as[MovieRating] //Dataset
    .rdd
    .map(x => Rating(x.uid, x.mid, x.score))
    .cache()

    //数据切分[2/8分](不加也可)
  val arraySplit = ratingRDD.randomSplit(Array(0.8,0.2))
  val trainRDD=arraySplit(0)   // 8/10 的数据作为训练数据
  val testRDD=arraySplit(1)  // 2/10 的数据作为测试数据

    //三、模型训练--->输出最优参数
  adjectALSParams(trainRDD,testRDD)
    //四、关闭spark
  sparkSession.close()
  }

  /**
    * 自定义函数--输出最优参数
    *
    * @param traningRDD
    * @param testRDD
    */
  def adjectALSParams(traningRDD: RDD[Rating], testRDD: RDD[Rating]): Unit = {
    //迭代的次数不用太在意,因为他们和自身机器的资源有关
    //rank:特征维度
    //lambda:跨度
    val result=for(rank<-Array(30,40,50,60,70);lambda<-Array(1,0.1,0.01))
      yield {
        //训练模型
        val model = ALS.train(traningRDD,rank,5,lambda)
        //获取模型均方根误差
        val rmse=getRmase(model,testRDD)
        (rank,lambda,rmse)
      }
    //打印出误差最小的:
    //用第三个Double进行排序,然后得出第一个--最优参数
    println(result.sortBy(_._3).head)
  }

  /**
    * 自定义函数--获取模型均方根误差函数
    *
    * @param model
    * @param testRDD
    */
  def getRmase(model: MatrixFactorizationModel, testRDD: RDD[Rating]) = {
    //需要构造userProductsRDD--预测一下用户打的分数
    //1)先通过从MongoDB里面取出的testRDD得到第一行(商品)和第一列(用户)的数据
    val userWithProduct=testRDD.map(x=>(x.user,x.product))
    //2)再通过model、测试数据算出预期的predictRatingRDD
    val predictRatingRDD=model.predict(userWithProduct)
    //3)真实写的分数-从测试的数据里面取的
    val realRating = testRDD.map(x=>((x.user,x.product),x.rating))
    //4)预测的分数-预测的数据-predictRating
    val predictRating=predictRatingRDD.map(x=>((x.user,x.product),x.rating))
    //5)计算误差
    //最外面的sqrt--开平方
    sqrt {
      //join()--合并实际表和预测表(是为了将实际偏好值和预测偏好值放在一起好做差)
      //得到的是:(userId-int,produceId-int),(真实值-double,预测值-double)
      realRating.join(predictRating)
        .map { //模式匹配
          case((uid,mid),(real,pre))=>
            val err =real - pre //计算误差
            err * err //误差的平方
        }.mean() //求平均
    }
  }
}

1)ALS优化实现过程图

 

 

 

 

 2)ALS优化参数过程图:

 

博客园  ©  2004-2025
浙公网安备 33010602011771号 浙ICP备2021040463号-3