了解常用的基于矩阵分解的协同过滤推荐算法的基本原理,掌握Spark MLlib中对基于模型的协同过滤算法的封装函数的使用,对Spark中机器学习模块内容加深理解。
1. 实验提供数据集,包含用户数据、电影数据、电影评分数据以及我的评分数据;
2. 根据提供的电影评分数据,利用Spark进行训练,得到一个最佳推荐模型;
3. 用实际数据和平均值这两方面评价该模型的准确度;
4. 根据我的评分数据向我推荐10部电影。
协同过滤算法按照数据使用,可以分为:
(1)基于用户(UserCF);
(2)基于商品(ItemCF);
(3)基于模型(ModelCF)。
按照模型,又可以分为:
(1)最近邻模型:基于距离的协同过滤算法;
(3)Latent Factor Mode(SVD):基于矩阵分解的模型;
(3)Graph:图模型,社会网络图模型。
本次实验,使用的协同过滤算法是基于矩阵分解的模型,就是基于样本的用户喜好信息,训练一个推荐模型,然后根据实时的用户喜好的信息进行预测,计算推荐。
ALS是alternating least squares的缩写,意为交替最小二乘法,该方法常用于基于矩阵分解的推荐系统中。对于一个R(观众对电影的一个评价矩阵)可以分解为U(观众的特征矩阵)和V(电影的特征矩阵),在这个矩阵分解的过程中,评分缺失项得到了填充,也就是说我们可以基于这个填充的评分来给用户最适合的商品推荐了。
MLlib支持基于模型的协同过滤算法,其中user和product对应图中的user和movie,user和product之间有一些隐藏因子。MLlib使用ALS(alternating least squares)来学习得到这些潜在因子。
图39-1
图39-1中原始矩阵R可能是非常稀疏的,但乘机UV是稠密的,即使该矩阵存在非零元素,非零元素的数量也非常少。因此模型只是对R的一种近似。原始矩阵R中大量元素是缺失的(元素值为0),算法为这些缺失元素生成(补全)了一个值,从这个角度讲,我们可以把算法称为模型。根据这个补全的矩阵,我们就可以从知道user也就知道了movies,或者知道movie也就知道了users,这就是以下实验推荐算法的基本原理。
构建模型的第一步是了解数据,对数据进行解析或转换,以便在Spark中作分析。Spark MLlib的ALS算法要求用户和产品的ID必须都是数值型,并且是32位非负整数,以下准备的数据集完全符合Spark MLlib的ALS算法要求,不必进行转换,可直接使用。
在本地目录/root/data/39/movie下有本次实验数据集,文件列表如图39-2所示。
图39-2
各文件数据格式如下(详细见README文件):
(1)用户数据(users.dat)
用户ID::性别::年龄::职业编号::邮编。
6031::F::18::0::45123
6032::M::45::7::55108
6033::M::50::13::78232
6034::M::25::14::94117
6035::F::25::1::78734
6036::F::25::15::32603
6037::F::45::1::76006
6038::F::56::1::14706
6039::F::45::0::01060
6040::M::25::6::11106
(2)电影数据(movies.dat)
电影ID::电影名称::电影种类。
3943::Bamboozled (2000)::Comedy
3944::Bootmen (2000)::Comedy|Drama
3945::Digimon: The Movie (2000)::Adventure|Animation|Children's
3946::Get Carter (2000)::Action|Drama|Thriller
3947::Get Carter (1971)::Thriller
3948::Meet the Parents (2000)::Comedy
3949::Requiem for a Dream (2000)::Drama
3950::Tigerland (2000)::Drama
3951::Two Family House (2000)::Drama
3952::Contender, The (2000)::Drama|Thriller
(3)评分数据(ratings.dat)
用户ID::电影ID::评分::时间。
6040::2022::5::956716207
6040::2028::5::956704519
6040::1080::4::957717322
6040::1089::4::956704996
6040::1090::3::956715518
6040::1091::1::956716541
6040::1094::5::956704887
6040::562::5::956704746
6040::1096::4::956715648
6040::1097::4::956715569
(4)我的评分数据(test.dat)
用户ID::电影ID::评分::时间。
0::780::4::1409495135
0::590::3::1409495135
0::1210::4::1409495135
0::648::5::1409495135
0::344::3::1409495135
0::165::4::1409495135
0::153::5::1409495135
0::597::4::1409495135
0::1580::5::1409495135
将以上数据文件上传到HDFS文件系统:
cd /usr/cstor/hadoop/bin
hdfs dfs -copyFromLocal /root/data/39/movie/ /
为防止shell端INFO日志刷屏,影响查看打印信息,修改打印日志级别,进入Spark安装的conf目录下,将log4j.properties.template文件复制一份,命名log4j.properties文件,然后将文件如下配置项:
log4j.rootCategory=WARN, console
进入Spark安装目录下bin目录,启动spark-shell:
cd /usr/cstor/spark/bin
./spark-shell --master spark://master:7077
具体代码如下:
/** 导入Spark机器学习推荐算法相关包 **/
import org.apache.spark.mllib.recommendation.{ALS, Rating, MatrixFactorizationModel}
import org.apache.spark.rdd.RDD
/** 定义函数,校验集预测数据和实际数据之间的均方根误差,后面会调用此函数 **/
def computeRmse(model:MatrixFactorizationModel,data:RDD[Rating],n:Long):Double = {
val predictions:RDD[Rating] = model.predict((data.map(x => (x.user,x.product))))
val predictionsAndRatings = predictions.map{ x =>((x.user,x.product),x.rating)}
.join(data.map(x => ((x.user,x.product),x.rating))).values
math.sqrt(predictionsAndRatings.map( x => (x._1 - x._2) * (x._1 - x._2)).reduce(_+_)/n)
}
/** 加载数据 **/
//1、我的评分数据(test.dat),转成Rating格式,即用户id,电影id,评分
val myRatingsRDD = sc.textFile("/movie/test.dat").map {
line => val fields = line.split("::")
// format: Rating(userId, movieId, rating)
Rating(fields(0).toInt, fields(1).toInt, fields(2).toDouble)
}
//2、样本评分数据(ratings.dat),其中最后一列Timestamp取除10的余数作为key,Rating为值,
即(Int,Rating),以备后续数据切分
val ratings = sc.textFile("/movie/ratings.dat").map {
line => val fields = line.split("::")
// format: (timestamp % 10, Rating(userId, movieId, rating))
(fields(3).toLong % 10, Rating(fields(0).toInt, fields(1).toInt, fields(2).toDouble))
}
//3、电影数据(movies.dat)(电影ID->电影标题)
val movies = sc.textFile("/movie/movies.dat").map {
line => val fields = line.split("::")
// format: (movieId, movieName)
(fields(0).toInt, fields(1))
}.collect().toMap
/** 统计所有用户数量和电影数量以及用户对电影的评分数目 **/
val numRatings = ratings.count()
val numUsers = ratings.map(_._2.user).distinct().count()
val numMovies = ratings.map(_._2.product).distinct().count()
println("total number of rating data: " + numRatings)
println("number of users participating in the score: " + numUsers)
println("number of participating movie data: " + numMovies)
/** 将样本评分表以key值切分成3个部分,分别用于训练(60%,并加入我的评分数据)、
校验(20%)以及测试(20%) **/
//定义分区数,即数据并行度
val numPartitions = 4
//因为以下数据在计算过程中要多次应用到,所以cache到内存
//训练数据集,包含我的评分数据
val training = ratings.filter(x => x._1 < 6).values.union(myRatingsRDD).repartition(numPartitions).persist()
//验证数据集
val validation = ratings.filter(x => x._1 >= 6 && x._1 < 8).values.repartition(numPartitions).persist()
//测试数据集
val test = ratings.filter(x => x._1 >= 8).values.persist()
//统计各数据集数量
val numTraining = training.count()
val numValidation = validation.count()
val numTest = test.count()
println("the number of scoring data for training) (including my score data): " + numTraining)
println("number of rating data as validation: " + numValidation)
println("number of rating data as a test: " + numTest)
/** 训练不同参数下的模型,获取最佳模型 **/
//设置训练参数及最佳模型初始化值
//模型的潜在因素的个数,即U和V矩阵的列数,也叫矩阵的阶
val ranks = List(8, 12)
//标准的过拟合参数
val lambdas = List(0.1, 10.0)
//矩阵分解迭代次数,次数越多花费时间越长,分解的结果也可能会更好
val numIters = List(10, 20)
var bestModel: Option[MatrixFactorizationModel] = None
var bestValidationRmse = Double.MaxValue
var bestRank = 0
var bestLambda = -1.0
var bestNumIter = -1
//根据设定的训练参数对训练数据集进行训练
for (rank <- ranks; lambda <- lambdas; numIter <- numIters) {
//计算模型
val model = ALS.train(training, rank, numIter, lambda)
//计算针对校验集的预测数据和实际数据之间的均方根误差
val validationRmse = computeRmse(model, validation, numValidation)
println("Root mean square: " + validationRmse + " Parameter: --rank = "
+ rank + " --lambda = " + lambda + " --numIter = " + numIter + ".")
//均方根误差最小的为最佳模型
if (validationRmse < bestValidationRmse) {
bestModel = Some(model)
bestValidationRmse = validationRmse
bestRank = rank
bestLambda = lambda
bestNumIter = numIter
}
}
/** 用训练的最佳模型预测评分并评估模型准确度 **/
//训练完成后,用最佳模型预测测试集的评分,并计算和实际评分之间的均方根误差(RMSE)
val testRmse = computeRmse(bestModel.get, test, numTest)
println("Optimal model parameters --rank = " + bestRank + " --lambda = " + bestLambda
+ " --numIter = " + bestNumIter +
" \nThe root mean square between the predicted data and the real data under the optimal model: " + testRmse + ".")
//创建一个用均值预测的评分,并与最好的模型进行比较,这个mean()方法在DoubleRDDFunctions中,求平均值
val meanRating = training.union(validation).map(_.rating).mean
val baselineRmse = math.sqrt(test.map(x => (meanRating - x.rating) * (meanRating - x.rating))
.reduce(_ + _) / numTest)
println("Root mean square between mean prediction data and real data: " + baselineRmse + ".")
val improvement = (baselineRmse - testRmse) / baselineRmse * 100
println("The accuracy of the prediction data of the best model with respect to the mean prediction data: " + "%1.2f".format(improvement) + "%.")
//向我推荐十部最感兴趣的电影
val recommendations = bestModel.get.recommendProducts(0,10)
//打印推荐结果
var i = 1
println("10 films recommended to me:")
recommendations.foreach { r => println("%2d".format(i) + ": " + movies(r.product))
i += 1
}
代码执行过程中打印日志信息如下:
图39-3 所有数据数量统计图
图39-4 评分数据切分的各数据集统计图
图39-5 训练时的参数及对应的误差图
图39-6 最佳模型的参数及对应的误差图
图39-7 均值预测的误差图
图39-8 最佳模型预测相比均值预测比较图
图39-9 最佳模型下向我推荐的电影图
步骤1:搭建Spark集群
前提:1、请自行配置各节点之间的免密登录,并在/etc/hosts中写好hostname与IP的对应,这样方便配置文件的相互拷贝。2、因为下面实验涉及Spark集群使用HDFS,所以按照之前的实验预先部署好HDFS。
在master机上操作:确定存在spark。
[root@master ~]# ls /usr/cstor
spark/
[root@master ~]#
在master机上操作:进入/usr/cstor目录中。
[root@master ~]# cd /usr/cstor
[root@master cstor]#
进入配置文件目录/usr/cstor/spark/conf, 先拷贝并修改slave.templae为slave。
[root@master ~]# cd /usr/cstor/spark/conf
[root@master cstor]# cp slaves.template slaves
然后用vim命令编辑器编辑slaves文件
[root@master cstor]# vim slaves
编辑slaves文件将下述内容添加到slaves文件中。
slave1
slave2
slave3
上述内容表示当前的Spark集群共有三台slave机,这三台机器的机器名称分别是slave1~3。
在spark-conf.sh中加入JAVA_HOME。
[root@master cstor]# vim /usr/cstor/spark/sbin/spark-config.sh
加入以下内容
export JAVA_HOME=/usr/local/jdk1.7.0_79
将配置好的Spark拷贝至slaveX、client。(machines在目录/root/data/2下,如果不存在则自己新建一个)
使用for循环语句完成多机拷贝。
[root@master ~]# cd /root/data/2
[root@master ~]# cat machines
slave1
slave2
slave3
client
[root@master ~]# for x in `cat machines` ; do echo $x ; scp -r /usr/cstor/spark/ $x:/usr/cstor/; done;
在master机上操作:启动Spark集群。
[root@master local]# /usr/cstor/spark/sbin/start-all.sh
配置Spark集群使用HDFS:
首先关闭集群(在master上执行)
[root@master ~]# /usr/cstor/spark/sbin/stop-all.sh
将Spark环境变量模板复制成环境变量文件。
[root@master ~]# cd /usr/cstor/spark/conf
[root@master conf]# cp spark-env.sh.template spark-env.sh
修改Spark环境变量配置文件spark-env.sh。
[root@master conf]$ vim spark-env.sh
在sprak-env.sh配置文件中添加下列内容。
export HADOOP_CONF_DIR=/usr/cstor/hadoop/etc/hadoop
重新启动spark
[root@master local]# /usr/cstor/spark/sbin/start-all.sh
步骤2:上传数据文件至HDFS
步骤3:启动Spark-shell
步骤4:编写训练程序