此项目主要是基于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()
}
}