一:先谈谈什么是ALS算法(基于RDD)

推荐算法中的ALS是指Alternating Least Squares(交替最小二乘法)算法。这是一种协同过滤推荐算法,主要用于解决推荐系统中的矩阵降维。

ALS算法的核心思想:将用户-物品评分矩阵分解为两个低维矩阵的乘积,即将用户-物品的关联关系表示为用户和物品的特征向量表示。具体而言,首先初始化一个因子矩阵,使用评分矩阵获取另外的因子矩阵,交替计算,直到满足终止条件(最大迭代次数 or 收敛条件),此时就可以得到两个因子矩阵,即模型Model。ALS算法构建模型最本质就是两个因子矩阵。(经典老图)

als算法spark_als算法spark

下图就展示了ALS算法进行矩阵降维的一个结果:

als算法spark_als算法spark_02

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。

als算法spark_迭代_03

       ...                            

我们交替重复进行上述两个步骤,直到达到某个收敛条件或者达到迭代次数

2.2.3 得到模型

经过若干轮迭代后,我们得到了优化后的用户特征矩阵U和物品特征矩阵V。这时,我们可以使用U和V的乘积来预测用户对未评分物品的评分。具体地,对于任意一个用户 u 和一个物品 i ,我们可以通过计算 U(u,:)  *  V(:,i) 来得到用户 u 对物品 i 的预测评分。可以看到:ALS算法构建模型最本质就是两个因子矩阵,用户矩阵U,物品矩阵V

als算法spark_迭代_04

2.2.4 结论

再回过头来看那三个需求:

  • 计算某个user1(用户)对item1(电影)的rating(评价); 即:u1 * i1
  • 给user1推荐可能喜欢的item;                             即:u1与所有物品in相乘(V矩阵的一整行)
  • 给item1推荐可能适合的user;                             即:i1与所有用户un相乘(U矩阵的一整列)

我们可以惊讶的发现,这三个问题在模型目前可以很简单的得到答案,这就是ALS在稀疏矩阵的魅力所在,ALS算法的优点在于它能够处理稀疏矩阵(即评分矩阵中大部分元素为空的情况),并且不需要事先知道用户和物品的特征向量维度。此外,ALS算法还可以通过迭代更新来逐步优化特征矩阵,每次迭代都可以减少预测误差。这使得ALS算法在推荐系统中得到了广泛的应用。

三:代码实现

数据都长这样,类似数据都能用:

als算法spark_als算法spark_05

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()
	}
	
}