一:先谈谈什么是ALS算法(基于RDD)
推荐算法中的ALS是指Alternating Least Squares(交替最小二乘法)算法。这是一种协同过滤推荐算法,主要用于解决推荐系统中的矩阵降维。
ALS算法的核心思想:将用户-物品评分矩阵分解为两个低维矩阵的乘积,即将用户-物品的关联关系表示为用户和物品的特征向量表示。具体而言,首先初始化一个因子矩阵,使用评分矩阵获取另外的因子矩阵,交替计算,直到满足终止条件(最大迭代次数 or 收敛条件),此时就可以得到两个因子矩阵,即模型Model。ALS算法构建模型最本质就是两个因子矩阵。(经典老图)
下图就展示了ALS算法进行矩阵降维的一个结果:
ALS算法在推荐系统中的应用非常广泛,其优点在于能够高效地求解矩阵降维问题,并生成有意义的推荐结果。
ALS算法的缺点也非常明显,训练所需的数据必须真实可靠,才能得到好用的模型,如果是像tb、pdd那些存在刷单水军的情况,该算法的缺点就会被无限放大,即 数据不可靠,模型一团糟!
二:说说ALS的工作原理(很重要!)
ALS算法属于User-Item CF,也叫做混合CF。
它可以同时满足三方面的需求:
- 计算某个user(用户)对某个item(电影)的rating(评价);
- 给user推荐可能喜欢的item;
- 给item推荐可能适合的user;
用户和商品的关系,可以抽象为如下的三元组:<User,Item,Rating>。用电影评分来做例子:用户User对电影Item的评分Rating;
2.1 问题出现
举个例子,就比如用户在豆瓣给电影评分,在使用中,由于用户和电影的数量都十分庞大,R矩阵作为n * m的规模分分钟过千万、过亿。
- 一方面,在如此大数据的规模下,传统的矩阵分解方法已经是很难快速处理了。
- 另一方面,一个用户不可能给所有电影评分,一个电影也不可能被所有人评分,因此,R矩阵百分比是个极度稀疏矩阵。
显然,这样的一个稀疏矩阵中存在着很多空值,如果是电影评分那肯定会有很多0分,显然是会干扰到模型的有效性的,遇到问题就解决问题,不行就解决提出问题的人。
2.2 解决思想
主体思想:ALS算法的目标是将这个高维的评分矩阵,分解为两个低维矩阵的乘积。
我们把原始矩阵作为X,将用户矩阵作为U,物品矩阵作为V。既然整个大矩阵X中存在很多无效数据,那我们能不能把视野放窄一些,看看比X小一些的矩阵中,存不存在比较密集而且U和V的乘积尽可能接近X的矩阵!
2.2.1 初始化
在算法开始时,我们随机初始化用户矩阵U和物品矩阵V。这两个矩阵的维度通常远小于原始评分矩阵R的维度。例如,如果X是一个m行n列的矩阵,那么U可能是一个m行k列的矩阵,V可能是一个k行n列的矩阵,其中k是一个远小于m和n的数。
(k表示的是低维矩阵的隐含因子数,也就是分解后的用户特征矩阵U和物品特征矩阵V的隐含特征的维度。k值的大小会影响到算法的性能和推荐的准确性。k值太小,无法充分捕获用户和物品的特征,导致推荐效果不佳;k值太大,会增加计算的复杂度和过拟合的风险。)
(在实践中,通常可以通过交叉验证等方法来确定最优的k值。具体来说,可以将数据集分为训练集和验证集两部分,然后尝试不同的k值在训练集上进行训练,并在验证集上评估推荐的准确性。最终选择使得验证集上推荐准确性最高的k值作为最优值。)
2.2.2 交替优化
ALS算法通过交替优化U和V来逼近原始评分矩阵X。在每一步中,我们固定其中一个矩阵(例如U),然后优化另一个矩阵(例如V),使得U和V的乘积尽可能地接近。具体步骤如下:
1.优化物品矩阵V:当我们固定U时,我们可以根据U和X来计算V。这通常是通过最小化一个损失函数来实现的,该损失函数衡量了U * V与R之间的差异。我们可以使用最小二乘法等方法来求解这个优化问题,找到使损失函数最小的V。
2.优化用户矩阵U:然后,我们固定V,根据V和X来计算U。同样,我们使用最小二乘法等方法来求解这个优化问题,找到使损失函数最小的U。
...
我们交替重复进行上述两个步骤,直到达到某个收敛条件或者达到迭代次数。
2.2.3 得到模型
经过若干轮迭代后,我们得到了优化后的用户特征矩阵U和物品特征矩阵V。这时,我们可以使用U和V的乘积来预测用户对未评分物品的评分。具体地,对于任意一个用户 u 和一个物品 i ,我们可以通过计算 U(u,:) * V(:,i) 来得到用户 u 对物品 i 的预测评分。可以看到:ALS算法构建模型最本质就是两个因子矩阵,用户矩阵U,物品矩阵V。
2.2.4 结论
再回过头来看那三个需求:
- 计算某个user1(用户)对item1(电影)的rating(评价); 即:u1 * i1
- 给user1推荐可能喜欢的item; 即:u1与所有物品in相乘(V矩阵的一整行)
- 给item1推荐可能适合的user; 即:i1与所有用户un相乘(U矩阵的一整列)
我们可以惊讶的发现,这三个问题在模型目前可以很简单的得到答案,这就是ALS在稀疏矩阵的魅力所在,ALS算法的优点在于它能够处理稀疏矩阵(即评分矩阵中大部分元素为空的情况),并且不需要事先知道用户和物品的特征向量维度。此外,ALS算法还可以通过迭代更新来逐步优化特征矩阵,每次迭代都可以减少预测误差。这使得ALS算法在推荐系统中得到了广泛的应用。
三:代码实现
数据都长这样,类似数据都能用:
import org.apache.spark.mllib.evaluation.RegressionMetrics
import org.apache.spark.mllib.recommendation._
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
/**
* 使用MovieLens 电影评分数据集,调用Spark MLlib 中协同过滤推荐算法ALS建立推荐模型:
* -a. 预测 用户User 对 某个电影Product 评价
* -b. 为某个用户推荐10个电影Products
* -c. 为某个电影推荐10个用户Users
*/
object SparkAlsRmdMovie {
def main(args: Array[String]): Unit = {
// TODO: 1. 构建SparkContext实例对象
val sc: SparkContext = {
// a. 创建SparkConf对象,设置应用相关配置
val sparkConf = new SparkConf()
.setMaster("local[2]")
.setAppName(this.getClass.getSimpleName.stripSuffix("$"))
// b. 创建SparkContext
val context = SparkContext.getOrCreate(sparkConf)
// 设置检查点目录
context.setCheckpointDir(s"datas/ckpt/als-ml-${System.nanoTime()}")
// c. 返回
context
}
// TODO: 2. 读取 电影评分数据
val rawRatingsRDD: RDD[String] = sc.textFile("datas/als/ml-100k/u.data")
println(s"Count = ${rawRatingsRDD.count()}")
println(s"First: ${rawRatingsRDD.first()}")
// TODO: 3. 数据转换,构建RDD[Rating]
val ratingsRDD: RDD[Rating] = rawRatingsRDD
// 过滤不合格的数据
.filter(line => null != line && line.split("\\t").length == 4)
.map{line =>
// 字符串分割
val Array(userId, movieId, rating, _) = line.split("\\t")
// 返回Rating实例对象
Rating(userId.toInt, movieId.toInt, rating.toDouble)
}
// 划分数据集为训练数据集和测试数据集
val Array(trainRatings, testRatings) = ratingsRDD.randomSplit(Array(0.9, 0.1))
// TODO: 4. 调用ALS算法中显示训练函数训练模型
// 迭代次数为20,特征数为10
val alsModel: MatrixFactorizationModel = ALS.train(
ratings = trainRatings, // 训练数据集
rank = 10, // 特征数rank
iterations = 20 // 迭代次数
)
// TODO: 5. 获取模型中两个因子矩阵
/**
* 获取模型MatrixFactorizationModel就是里面包含两个矩阵:
* -a. 用户因子矩阵
* alsModel.userFeatures
* -b. 产品因子矩阵
* alsModel.productFeatures
*/
// userId -> Features
val userFeatures: RDD[(Int, Array[Double])] = alsModel.userFeatures
userFeatures.take(10).foreach{tuple =>
println(tuple._1 + " -> " + tuple._2.mkString(","))
}
println("=======================================================")
// productId -> Features
val productFeatures: RDD[(Int, Array[Double])] = alsModel.productFeatures
productFeatures.take(10).foreach{
tuple => println(tuple._1 + " -> " + tuple._2.mkString(","))
}
// TODO: 6. 模型评估,使用RMSE评估模型,值越小,误差越小,模型越好
// 6.1 转换测试数据集格式RDD[((userId, ProductId), rating)]
val actualRatingsRDD: RDD[((Int, Int), Double)] = testRatings.map{tuple =>
((tuple.user, tuple.product), tuple.rating)
}
// 6.2 使用模型对测试数据集预测电影评分
val predictRatingsRDD: RDD[((Int, Int), Double)] = alsModel
// 依据UserId和ProductId预测评分
.predict(actualRatingsRDD.map(_._1))
// 转换数据格式RDD[((userId, ProductId), rating)]
.map(tuple => ((tuple.user, tuple.product), tuple.rating))
// 6.3 合并预测值与真实值
val predictAndActualRatingsRDD: RDD[((Int, Int), (Double, Double))] = predictRatingsRDD.join(actualRatingsRDD)
// 6.4 模型评估,计算RMSE值
val metrics = new RegressionMetrics(predictAndActualRatingsRDD.map(_._2))
println(s"RMSE = ${metrics.rootMeanSquaredError}")
// TODO 7. 推荐与预测评分
// 7.1 预测某个用户对某个产品的评分 def predict(user: Int, product: Int): Double
val predictRating: Double = alsModel.predict(196, 242)
println(s"预测用户196对电影242的评分:$predictRating")
println("----------------------------------------")
// 7.2 为某个用户推荐十部电影 def recommendProducts(user: Int, num: Int): Array[Rating]
val rmdMovies: Array[Rating] = alsModel.recommendProducts(196, 10)
rmdMovies.foreach(println)
println("----------------------------------------")
// 7.3 为某个电影推荐10个用户 def recommendUsers(product: Int, num: Int): Array[Rating]
val rmdUsers = alsModel.recommendUsers(242, 10)
rmdUsers.foreach(println)
// TODO: 8. 将训练得到的模型进行保存,以便后期加载使用进行推荐
val modelPath = s"datas/als/ml-als-model-" + System.nanoTime()
alsModel.save(sc, modelPath)
// TODO: 9. 从文件系统中记载保存的模型,用于推荐预测
val loadAlsModel: MatrixFactorizationModel = MatrixFactorizationModel
.load(sc, modelPath)
// 使用加载预测
val loaPredictRating: Double = loadAlsModel.predict(196, 242)
println(s"加载模型 -> 预测用户196对电影242的评分:$loaPredictRating")
// 为了WEB UI监控,线程休眠
Thread.sleep(10000000)
// 关闭资源
sc.stop()
}
}