简单版推荐系统

此项目主要是基于ALS,

主流推荐算法,包括基于用户的协同过滤 UserCF,基于物品的协同过滤ItemCF

UserCF:推荐和你相似用户所购买的物品

ItemCF:推荐和你买过或浏览过或搜索过或购物车中相似的物品

 

两者各有优缺点

UserCF:适用于 用户量<物品量 如新闻资讯数量往往远大于用户数量

ItemCF:适用于 用户量>物品量 

ALS:兼具两者User,Item,具体后面讲解

 

关键点在于:相似度的计算

首先构建一个用户-物品矩阵:用户(ABCDE),物品(101,102,103,104,105)

相似度计算,一般采用以下几种方式:

1杰卡德系数,jaccard系数,

 

N(u)为用户U感兴趣的商品,N(v)为用户V感兴趣的商品,在此表示为用户U,V的物品数量,比如说AB 分子为交集

都购买过 101 102 103 则分子交集为3

分母为两者的并集,A用户只有101 102 103 B用户有101 102 103 104 所以并集数量为4 

分母为两者交集的数量

2余弦相似度,

3还有一个皮尔逊,公式太复杂

 

UserItemCF完整的公式为:

公式解释为:计算用户U对物品i的感兴趣程度

S(u,k)包含了和用户u兴趣最接近的k个用户,N(i)表示为对物品I有过打分记录的用户集合,

Wuv表示计用户u和用户v的相似度,Rvi表示用户v对商品i的评分

得到了用户对物品的感兴趣程度后,将最高得分的前几个物品推荐给用户

 

ItemCF:

也是先计算物品与物品之间的相似度

余弦相似度为

i表示在用户物品矩阵中所有用户对商品 I 的打分,若没有则为零构成的向量,

j表示矩阵中所有的用户对商品I打分构成的向量

以后在补充,

 

ALS(交替式最小二乘法): 

ALS算法属于User-Item CF混合CF,同时考虑了用户和物品两方面,可以抽象成一个三元组<User,Item,Rating> <用户,物品,评分>用户对此物品的评分,假设一批用户数据,包含m个user,n个Item,定义评分矩阵Rmn,如Rui表示第U个用户对第I个Item的评分,实际情况情况下,矩阵会非常大,传统的矩阵分解会非常困难,另一方面,一个用户也不可能对所有的物品进行评分,稀疏矩阵

假设用户和商品之间存在若干关联维度(年龄,性别,外观等)

为了使低秩矩阵X和Y尽可能地逼近R,需要最小化下面的平方误差损失函数:

 

 

   考虑到矩阵的稳定性问题,使用Tikhonov regularization,则上式变为:

l2正则化

ALS算法的缺点在于:

    1、它是一个离线算法。

    2、无法准确评估新加入的用户或商品。这个问题也被称为Cold Start问题。

 

ALS推导:

两个维度,先固定一个在计算另外一个,

因此整个优化迭代的过程为:

  1.随机生成X、Y。(相当于对迭代算法给出一个初始解。) 
  Repeat until convergence { 
    2.固定Y,使用公式3更新xu。 
    3.固定X,使用公式4更新yi。 
  }

因此也被称之为交替式最小二乘法

交替优化X和Y

一般采用RMSE 均方误差  累加(真实值-实际值)平方/数量 在开方,当RMSE值变化很小时可以认为结果以收敛

 

隐式反馈:用户给商品评分是个非常简单粗暴的行为,在实际的电商网站中,还有大量的用户行为,同样能够间接反映用户的喜好,比如用户的购买记录、搜索关键字,甚至是鼠标的移动。我们将这些间接用户行为称之为隐式反馈(implicit feedback),以区别于评分这样的显式反馈(explicit feedback)。

 

隐式反馈有以下几个特点:

  1、没有负面反馈(negative feedback)。用户一般会直接忽略不喜欢的商品,而不是给予负面评价。

  2、隐式反馈包含大量噪声。比如,电视机在某一时间播放某一节目,然而用户已经睡着了,或者忘了换台。

  3、显式反馈表现的是用户的喜好(preference),而隐式反馈表现的是用户的信任(confidence)。比如用户最喜欢的一般是电影,但观看时间最长的却是连续剧。大米购买的比较频繁,量也大,但未必是用户最想吃的食物。

  4、隐式反馈非常难以量化。

 

ALS-WR算法

  针对隐式反馈,有ALS-WR算法(ALS with Weighted-λ-Regularization)。

  首先将用户反馈分类:

                

  但是喜好是有程度差异的,因此需要定义程度系数:

                   

  这里的rui表示原始量化值,比如观看电影的时间;

  这个公式里的1表示最低信任度,α表示根据用户行为所增加的信任度。

 

     最终,损失函数变为:

 

 

 

 

 

冷启动问题:

1 用户冷启动:一个全新的用户来到一个网站,网站对其没有任何的记录。 办法:推荐当前系统的最新最热的商品给新用户,然后在观察其后期反馈情况

2 物品冷启动:一个全新的商品刚上架,办法:根据物品本身具有的属性将其纳入相应的类别,如肖申克的救赎属于励志之类的将其纳入励志电影中

3 系统冷启动:暂时还没有好办法,只能等慢慢积累数据量,或者直接推荐想要推荐的东西,然后记录反馈

 

 

数据链接:  http://59.80.44.50/files.grouplens.org/datasets/movielens/ml-1m.zip

代码部分主要是spark调用mllib中的als算法来进行简单的推荐系统设计

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>recommendSystem</groupId>
    <artifactId>recommendSystem</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <spark.version>2.3.2</spark.version>
        <scala.version>2.11</scala.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-core_${scala.version}</artifactId>
            <version>${spark.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-streaming_${scala.version}</artifactId>
            <version>${spark.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-sql_${scala.version}</artifactId>
            <version>${spark.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-hive_${scala.version}</artifactId>
            <version>${spark.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-mllib_${scala.version}</artifactId>
            <version>${spark.version}</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-client</artifactId>
            <version>2.8.4</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.scala-tools</groupId>
                <artifactId>maven-scala-plugin</artifactId>
                <version>2.15.2</version>
            </plugin>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.6.0</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.19</version>
            </plugin>
        </plugins>
    </build>
</project>

 

 

import org.apache.spark.mllib.evaluation.RegressionMetrics
import org.apache.spark.mllib.recommendation.{ALS, MatrixFactorizationModel, Rating}
import org.apache.spark.rdd.RDD
import org.apache.spark.sql._

// A simple movie recommend system


//伴生对象 singleton
object movieRecommend {

  //样例类 用来封装数据,类似于java bean 但有更多的功能,类如模式匹配
  case class User(userId: Int, gender: String, age: Int, occupation: Int)

  //movie    MovieID::Title::Genres

  case class MovieAll(movieId: Int, title: String, genres: String)

  case class Movie(movieId: Int, title: String)

  //data cleaner actually this is reduadant
  // def function name (coef:type) :returnType


  def MovieAllDataDeal(movie: String): MovieAll = {
    val movieField = movie.split("::")
    //判断是否满足条件
    assert(movieField.size == 3)
    //封装进样例类里面 注意类型的匹配
    //最后就是返回值
    MovieAll(movieField(0).toInt, movieField(1), movieField(2))
  }


  def MovieDataDeal(movie: String): Movie = {
    val movieField = movie.split("::")
    //判断是否满足条件
    assert(movieField.size == 3)
    //封装进样例类里面 注意类型的匹配
    //最后就是返回值
    Movie(movieField(0).toInt, movieField(1))
  }

  def UserDataDeal(user: String): User = {
    val userField = user.split("::")
    assert(userField.size == 5)
    User(userField(0).toInt, userField(1), userField(2).toInt, userField(3).toInt)
  }

  //Rating 是mllib封装好的样例类
  //case class Rating(user : scala.Int, val product : scala.Int, rating : scala.Double)
  def RatingDataDeal(rating: String): Rating = {
    val ratingField = rating.split("::")
    Rating(ratingField(0).toInt, ratingField(1).toInt, ratingField(2).toDouble)
  }

  //Rmse均方根误差 root mean square error
  //为预测值与实际值的差的平方和与次数的比值的平方根,可以用来评判模型的好坏
  //计算均方根误差,模型 测试集需要格式为RDD[Rating]
  //当然rmse越小越好
  def RmseComputer(model: MatrixFactorizationModel, dataOfTest: RDD[Rating]): Double = {
    //data 测试集为原来的rating随机分割的25% 格式为UserID::MovieID::Rating::Timestamp
    //但模型预测只需要知道user 和 product即可 所以需要map操作转换一下,截取也可用下面的直接用case class
    //预测返回结果包括user product rating
    val predictResult = model.predict(dataOfTest.map(x => (x.user, x.product)))
    //此步比较复杂,主要是用来将预测值和测试值组成一个map然后比较预测的评分值和实际值
    //最后的格式为 ((user,product),(prediction,actuallValue)).value
//    val predJoinTest = predictResult.map(x => ((x.user, x.product), x.rating)).join(dataOfTest.map(x => ((x.user, x.product), x.rating))).values
    val predJoinTest =predictResult.map(x=>((x.user,x.product),x.rating)).join(dataOfTest.map(x=>((x.user,x.product),x.rating))).values
    //直接调用库函数需要传入一个(prediction,actuallValue)
    val evalutor = new RegressionMetrics(predJoinTest)
    evalutor.meanAbsoluteError
  }

  //main function
  def main(args: Array[String]) {

    println("Welcome to zl MovieRecommendSystem ")
    println("please wait a minute,and then will recommend.......")
    //统一新的入口,include more context 本地模式运行 .enableHiveSupport
    //core-site.xml hive-site.xml hdfs-site.xml /conf resources
    val spark = SparkSession.builder
      .master("local[*]")
      .appName("MovieRecommend")
      .getOrCreate()

    import spark.implicits._

    //rating   UserID::MovieID::Rating::Timestamp
    //user     UserID::Gender::Age::Occupation::Zip-code
    //movie    MovieID::Title::Genres

    //new way to read data because use usually so cache to memory or persist
    val moviesData = spark.read.textFile("/home/zl/gMoviesData/movies.txt").map(MovieDataDeal).cache()
    val moviesAllData = spark.read.textFile("/home/zl/gMoviesData/movies.txt").map(MovieAllDataDeal).cache()
    val ratingsData = spark.read.textFile("/home/zl/gMoviesData/ratings.txt").map(RatingDataDeal).cache()

    // convert to DataFrame
    //因为predict返回的值为Rating格式的 UserID::MovieID::Rating::Timestamp
    //而如果需要知道返回的电影的具体的名字,就需要做一个映射即map操作,所以需要将movie装换成df
    //df其实和关系表结构差不多
    //其实实际就只是返回一个id值然后去库查前端展示
    val moviesDF = moviesData.toDF()
    val movieTitles = moviesDF.as[(Int, String)].rdd.collectAsMap()


    //因为内部为case class 规则 所以想要提取,得转换成df df schema 字段名对应于case class
    //转换成df后提取出评分为5的电影id,然后按照出现次数排序(即多次被评分为5),取出前100个
    //    val FiveRating100 = ratingsData.toDF().filter($"rating" > 4).select("product").rdd.map((_, 1)).reduceByKey(_ + _).sortBy(_._2, false).map(_._1).take(100)

    //sql("select product,count(*) from a group by product")

    //注册成临时表 左表 分组聚合
    ratingsData.toDF().filter($"rating" > 4).groupBy("product").count().createTempView("A")

    moviesAllData.toDF().createTempView("B")


    //  返回值为df  movieId count(被评为5分的次数) title genres(类型)
    val Top10RankMovie = spark.sql("select title from  (select * from A left join B on A.product=B.movieId) temp order by temp.count desc limit 10 ")


    // split to data set and test set
    val tempData = ratingsData.randomSplit(Array(0.72, 0.28))
    val trainingSetOfRatingsData = tempData(0).cache().rdd
    val testSetOfRatingData = tempData(1).cache().rdd

    //多重迭代法求最佳参数模型


    //迭代次数
    val numIters = List(10, 20)

    //隐含因子
    val numRanks = List(8, 12)

    //惩罚值
    val numLambdas = List(0.1, 10.0)

    // Initial variable random value
    //need set var because iteration so can  variable
    var bestModel: Option[MatrixFactorizationModel] = None
    var bestRanks = -1
    var bestIters = 0
    var bestLambdas = -1.0
    var bestRmse = Double.MaxValue

    //共2*2*2种组合,每种组合迭代次数又不一样,所以在此会消耗大量时间
    for (rank <- numRanks; iter <- numIters; lambdas <- numLambdas) {
      //als参数为 训练集合 隐含因子 迭代次数 惩罚因子
      val model = ALS.train(trainingSetOfRatingsData, rank, iter, lambdas)
      val validationRmse = RmseComputer(model, testSetOfRatingData)
      //类似与梯度下降,逐步迭代
      if (validationRmse < bestRmse) {
        //model 为option类型
        bestModel = Some(model)
        bestRmse = validationRmse
        bestIters = iter
        bestLambdas = lambdas
        bestRanks = rank
      }
    }

    println("一些系统参数\t" + "最小RMSE为" + bestRmse + "\t" + "最佳迭代次数" + bestIters + "\t" + "最佳惩罚值" + bestLambdas)

    // training model 隐含因子 迭代次数 run  训练集
    //简单版模型直接指定参数
    //    val recomModel = new ALS().setRank(20).setIterations(10).run(trainingSetOfRatingsData)

    //    val recomResult = recomModel.recommendProducts(1000, 10)

    //because model is option type so need .get 为用户id为888推荐10部感兴趣的电影
    println("请输入需要推荐用户的ID,Enter结束")
    val input = Console.readInt()
    //    assert(0<input<6041)
    if (input > 6041 || input < 0) {
      println("您输入的用户不合法或不存在!!!")
      println("那我向您推荐下评分最高的十部电影吧")
      Top10RankMovie.rdd.foreach(x => println(x))
      println("bye bye see you next time ")
      spark.stop()
    }
    val recomResult = bestModel.get.recommendProducts(input, 10)
    println(s"精心为 $input 用户挑选了以下10部电影,快点去欣赏吧")

    //    println(recomResult.mkString("\n"))


    //推荐包括userId productId rating 就是一个case class Rating
    //处理之后会得到(电影名称,rating)的map
    val recommendMoviesWithTitle = recomResult.map(rating => (movieTitles(rating.product), rating.rating))
    println(recommendMoviesWithTitle.mkString("\n"))

    while (true) {
      println("请问您还需要对哪个用户做推荐吗?如果需要请输入用户Id编号回车结束,如果不需要请输入exit")
      val input2 = Console.readInt()
      if (input2 > 6041 || input2 < 0) {
        println("您输入的用户不合法或不存在!!!")
        println("那我向您推荐下评分最高的十部电影吧")
        Top10RankMovie.rdd.foreach(x => println(x))
        println("\n")
        println("bye bye see you next time ")
        return
      }
      if (input2 == "exit") {
        println("bye bye see you next time ")
        return
      }
      val reco = bestModel.get.recommendProducts(input2.toInt, 10)
      val recoWithTitle = reco.map(x => (movieTitles(x.product), x.rating))
      println(s"这是精心为 $input2 准备的10部电影快去欣赏吧!!!")
      println(recoWithTitle.mkString("\n"))
    }
    //    之前的简单版的求 MAE
    //    val predictResultOfTestSet = recomModel.predict(testSetOfRatingData.map {
    //      case Rating(user, product, rating) => (user, product)
    //    })
    //
    //    val formatResultOfTestSet = testSetOfRatingData.map {
    //      case Rating(user, product, rating) => ((user, product), rating)
    //    }
    //
    //    val formatResultOfPredictionResult = predictResultOfTestSet.map {
    //      case Rating(user, product, rating) => ((user, product), rating)
    //    }
    //
    //    val finalResultForComparison = formatResultOfPredictionResult.join(formatResultOfTestSet)
    //    //    finalResultForComparison.foreach(println)
    //    val MAE = finalResultForComparison.map {
    //      case ((user, product), (ratingOfTest, ratingOfPrediction)) =>
    //        val error = (ratingOfTest - ratingOfPrediction)
    //        Math.abs(error)
    //    }.mean()

    //    println(s"mean error: $MAE")

    spark.stop()

  }


}
 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值