读书笔记:构建基于Spark的推荐引擎

来源:互联网 发布:windows ad域管理工具 编辑:程序博客网 时间:2024/05/16 01:32

感想

本章的练习是在spark shell上进行的,由于我下载的是spark-2.2.0-bin-hadoop2.7,所以在操作上稍稍有点区别,但是代码基本上都是可以运行的,其中要注意加上一个包,因为我在运行import org.jblas.DoubleMatrix的时候,提示找不到jblas,所以要提前下载下来,放到spark的jars目录里,然后重启shell才能生效,其他的实验我都做了,要理解这些代码是需要一定Scala基础和mapreduce基础的,不过跑通这些代码没什么技术含量。
jblas地址:
链接: https://pan.baidu.com/s/1bUV6Aa 密码: 557q

介绍

推荐引擎背后的想法是预测人们可能喜好的物品并通过探寻物品之间的联系来辅助这个过程。从这点上说,它和同样也做预测的搜索引擎互补。但与搜索引擎不同,推荐引擎试图向人们呈现的相关内容并不一定就是人们所搜索的,其返回的某些结果甚至人们都没听说过。

一般来讲,推荐引擎试图对用户与某类物品之间的联系建模。比如MovieStream的案例中,我们使用推荐引擎来告诉用户有哪些电影他们可能会喜欢。如果这点做得很好,就能吸引用户持续使用我们的服务。这对双方都有好处。同样,如果能准确告诉用户有哪些电影与某一电影相似,就能方便用户在站点上找到更多感兴趣的信息。这也能提升用户的体验,参与度以及站点内容对用户的吸引力。

                                                      

实际上,推荐引擎的应用并不限于电影,书籍或是产品。本章内容同样适用于用户与物品关系或社交网络中用户与用户之间的关系。比如向用户推荐他们可能认识或关注的用户。

 

推荐引擎很适合如下两类常见场景:

·            可选项众多:可选的物品越多,用户就越难找到想要的物品。如果用户知道他们想要什么,那搜索能有所帮助。然而最适合的物品往往并不为用户所事先知道。这时,通过向用户推荐相关物品,其中某些可能用户事先不知道,将能帮助他们发现新物品。

·            当人们主要根据个人喜好来选择物品时,推荐引擎能利用集体智慧,根据其他有类似喜好的用户的信息来帮助他们发现所需物品。


1.推荐模型分类

推荐系统的研究已经相当广泛,也存在很多设计方法。最为流行的两种方法时基于内容的过滤和协同过滤。另外,排名模型等近期也受到不少关注。实践中的方案很多是综合性的,它们将多种方法的元素合并到一个模型中或是进行组合。

1.1 基于内容的过滤

基于内容的过滤利用物品的内容或是属性信息以及某些相似度定义,来求出与该物品类似的物品。这些属性值通常是文本内容(比如标题,名称,标签及该物品的其他元数据)。对多媒体来说,可能还涉及从音频或视频中提取的其他属性。

类似地,对用户的推荐可以根据用户的属性或是描述得出,之后再通过相同的相似度定义来与物品属性做匹配。比如,用户可以表示为他所接触过的各物品属性的综合。该表示可作为该用户的一种描述。之后可以用它来与物品的属性进行比较以找出符合用户描述的物品。


1.2 协同过滤

协同过滤是一种借助众包智慧的途径。它利用大量已有的用户偏好来估计用户对其未接触过的物品的喜好程度。其内在思想是相似度的定义。

在基于用户的方法中,如果两个用户表现出相似的偏好(即对相同物品的偏好大体相同),那就认为他们的兴趣类似。要针对他们中的一个用户推荐一个未知物品,便可选取若干与其类似的用户并根据他们的喜好计算出对各个物品的综合得分,再以得分来推荐物品。其整体的逻辑是,如果其他用户也偏好某些物品,那这些物品很可能值得推荐。

同样也可以借助基于物品的方法来做推荐。这种方法通常根据现有用户对物品的偏好或是评级情况,来计算物品之间的某种相似度。这时,相似用户评级相同的那些物品会被认为更相近。一旦又了物品之间的相似度,便可用用户接触过的物品来表示这个用户,然后找出和这些已知物品相似的那些物品,并将这些物品推荐给用户。同样,与已有物品相似的物品被用来生成一个综合得分,而该得分用于评估未知物品的相似度。

基于用户或物品的方法的得分取决于若干用户或是物品之间依据相似度所构成的集合(即邻居),故它们也常被称为最近邻模型。

最后,也存在不少基于模型的方法是对“用户-物品”偏好建模。这样,对未知“用户-物品”组合上应用该模型可得出新的偏好。

1.3 矩阵分解

Spark推荐模型库当前只包含基于矩阵分解的实现,由此我们也将重点关注这类模型。它们有吸引人的地方。首先,这些模型在协同过滤中的表现十分出色。而在Netflix Prize等知名比赛中表现也很拔尖。

1.显式矩阵分解

当药处理的那些数据是由用户所提供的自身的偏好数据,这些数据被称作显式偏好数据。这类数据包括如物品评级,赞,喜欢等用户对物品的评价。

这些数据可以转换为以用户为行,物品为列的二维矩阵。矩阵的每一个数据表示某个用户对特定物品的偏好。大部分情况下单个用户只会和少部分物品接触,所以该矩阵只有少部分数据非零(即该矩阵很稀疏)。

假设我们有如下用户对电影的评级数据:

它们可转为如下评级矩阵:


对这个矩阵建模,可以采用矩阵分解(或矩阵补全)的方式。具体就是找出两个低维度的矩阵,使得它们的乘积是原始的矩阵。因此这也是一种降维技术。假设我们的用户和物品数目分别是U和I,那对应的“用户-物品”矩阵的维度为UxI,类似下图:


要找到和“用户-物品”矩阵近似的k维(低阶)矩阵,最终要求出如下两个矩阵:一个用于表示用户的Uxk维矩阵,以及一个表征物品的Ixk维矩阵。这两个矩阵也称作因子矩阵。它们的乘积便是原始评级矩阵的一个近似。值得注意的是,原始评级矩阵通常很稀疏,但因子矩阵却是稠密的。如下图:

这类模型试图发现对应“用户-物品”矩阵内在行为结构的隐含特征(这里表示因子矩阵),所以也把它们称为隐特征模型。隐含特征或因子不能直接解释,但它可能表示了某些含义,比如对电影的某个导演,种类,风格或某些演员的偏好。

由于是对“用户-物品”矩阵直接建模,用这些模型进行预测也相对直接:要计算给定用户对某个物品的预计评级,就从用户因子矩阵和物品因子矩阵分别选取相应的行(用户因子变量)与列(物品因子向量),然后计算两者的点积即可。

如图4-4中高亮部分为因子向量:


而对于物品之间相似度的计算,可以用最近邻模型中用到的相似度衡量方法。不同的是,这里可以直接利用物品因子向量,将相似度计算转换为两物品因子向量之间相似度计算,如图4-5:


因子分解类模型的好处在于,一旦建立了模型,对推荐的求解便相对容易。但也有弊端,即当用户和物品的数量很多时,其对应的物品或是用户的因子向量可能达到数以百万计。这将在存储和计算能力上带来挑战。另一个好处是,这类模型的表现都很出色。

因子分解类模型也存在某些弱点。相比最近邻模型,这类模型在理解和可解释性上难度都有所增加。另外,其模型训练阶段的计算量也很大。

2.隐式矩阵分解

上面针对的评级之类的显式偏好数据,但能收集到的偏好数据里也会包含大量的隐式反馈数据。在这类数据中,用户对物品的偏好不会直接给出,而是隐含在用户与物品的交互之中。二元数据(比如用户是否观看了某个电影或是否购买了某个商品)和计数数据(比如用户观看某电影的次数)便是这类数据。

处理了隐式数据的方法相当多。MLib实现了一个特定方法,它将输入的评级数据视为两个矩阵:一个二元偏好矩阵P以及一个信心权重矩阵C。

举例来说,假设之前提到的“用户-电影”评级实际上是各用户观看电影的次数,那上述两个矩阵会类似图4-6所示。其中,矩阵P表示用户是否看过某些电影,而矩阵C则以观看的次数来表示信心权重。一般来说,某个用户观看某个电影的次数越多,那我们对该用户的确喜欢该电影的信心也就越强。



隐式模型仍会创建一个用户因子矩阵和一个物品因子矩阵。但是,模型所求解的是偏好矩阵而非评级矩阵的近似。类似地,此时用户因子向量和物品因子向量的点积所得到的分数也不再是一个对评级的估值,而是对某个用户对某一物品偏好的估值(该值的取值虽并不严格地处于0到1之间,但十分趋近于这个区间)。

3. 最小二乘法

最小二乘法(Alternating least Squares,ALS)是一种求解矩阵分解问题的最优化方法。它功能强大,效果理想而且被证明相对容易并行化。这使得它很适合如Spark这样的平台。

ALS的实现原理是迭代式求解一系列最小二乘回归问题。在每一次迭代时,固定用户因子矩阵或是物品因子矩阵中的一个,然后用固定的这个矩阵以及评级数据来更新另一个矩阵。之后,被更新的矩阵被固定住,再更新另外一个矩阵。如此迭代,直到模型收敛(或者是迭代了预设好的次数)。

2. 提取有效特征

这里采用显式评级数据,而不使用其他用户或物品的元数据以及“用户-物品”交互数据。这样,所需的输入数据就只需包括每个评级对应的用户ID,影片ID和具体的星级。

从MovieLens 100k数据集提取特征

从Spark主目录启动Spark shell:

./bin/spark-shell

按照书上的,运行下列代码,我运行了,没什么问题:

val rawData = sc.textFile("/Users/eric/Documents/Spark/spark-machine-learning-book/ml-100k/u.data") rawData.first() val rawRatings = rawData.map(_.split("\t").take(3)) rawRatings.first() import org.apache.spark.mllib.recommendation.ALS import org.apache.spark.mllib.recommendation.Ratingval rawRatings=rawData.map(_.split("\t").take(3))val ratings = rawRatings.map { case Array(user, movie, rating) => Rating(user.toInt, movie.toInt, rating.toDouble) }ratings.first()

3.训练推荐模型

从原始数据提取出这些简单特征后,便可训练模型。MLib已实现模型训练的细节,这不需要我们担心。我们只需提供上述指定类型的新RDD以及其他所需参数来作为训练的输入即可。

3.1 使用MovieLens100k数据集训练模型

训练需要以下几个参数:

·            rank:对应ALS模型中的因子个数,也就是再低阶近似矩阵中的隐含特征个数。因子个数一般越多越好。但也会直接影响模型训练和保存时所需的内存开销,尤其是在用户和物品很多的时候。因此实践中该参数常作为训练效果与系统开销之间的调节参数。通常,其合理取值为10到200.

·            Iterations:对应运行时的迭代次数。ALS能确保每次迭代都能降低评级矩阵的重建误差,但一般经少数次迭代后ALS模型便已能收敛为一个比较合理的好模型。这样,大部分情况下都没必要迭代太多次(10次就行了)

·            Lambda:该参数控制模型的正则化过程,从而控制模型的过拟合情况。其值越高,正则化越严厉。该参数的赋值与实际数据的大小,特征和稀疏程度有关。和其他的机器学习模型一样,正则参数应该通过用非样本的测试数据进行交叉验证来调整。


MLib中ALS的实现里所用的操作都是延迟性的转换操作。所以,只在当用户因子或物品因子结果RDD调用了执行操作时,实际的计算才会发生。

这一小部分的代码如下,实验结果跟书上几乎一致,只是输出的log信息略有不同:

val model = ALS.train(ratings, 50, 10, 0.01)model.userFeaturesmodel.userFeatures.countmodel.productFeatures.count

3.2 使用隐式反馈数据训练模型

Mlib中标准的矩阵分解模型用于显式评级数据的处理。若要处理隐式数据,则可食用TrainImplicit函数。调用方式和标准的train模式类似,但多了一个可设置的alpha参数(正则化参数,lambda应通过测试和交叉验证法来设置)。

Alpha参数指定了信心权重所应达到的基准线。该值越高则所训练出的模型越认为用户与他所没评级过的电影之间没有相关性。

4.使用推荐模型

有了训练好的模型后便可用它来做预测。预测通常有两种:为某个用户推荐物品,或找出与某个物品相关或相似的其他物品。

4.1用户推荐

用户推荐是指向给定用户推荐物品。它通常以“前K个”形式展现,即通过模型求出用户可能喜好程度最高的前K个商品。这个过程通过计算每个商品得分并按照得分进行排序实现。

具体实现方法取决于所采用的模型。比如若采用基于用户的模型,则会利用相似用户的评级来计算对某个用户的推荐。而若采用基于物品的模型,则会依靠用户接触过的物品与候选物品之间的相似度来获得推荐。

利用矩阵分解方法时,是直接对评级数据进行建模,所以预计得分可视作相应用户因子向量和物品因子向量的点积。

1.从MovieLens 100k数据集生成电影推荐

MLib的推荐模型基于矩阵分解,因此可用模型所求的的因子矩阵来计算用户对物品的预计评级。下面只针对利用MovieLens中显式数据做推荐的情形,使用隐式模型时的方法与之相同。本节代码如下:

val predictedRating = model.predict(789, 123) val userId = 789 val K = 10 val topKRecs = model.recommendProducts(userId, K) println(topKRecs.mkString("\n"))

2.检验推荐内容

要直观地检验推荐的效果,可以简单比对下用户所评级过的电影的标题和被推荐的那些电影的电影名。

val movies = sc.textFile("/Users/eric/Documents/Spark/spark-machine-learning-book/ml-100k/u.item")val titles = movies.map(line => line.split("\\|").take(2)).map(array => (array(0).toInt,array(1))).collectAsMap()titles(123)val moviesForUser = ratings.keyBy(_.user).lookup(789)println(moviesForUser.size)moviesForUser.sortBy(-_.rating).take(10).map(rating => (titles(rating.product), rating.rating)).foreach(println)topKRecs.map(rating => (titles(rating.product), rating.rating)).foreach(println)

4.2 物品推荐

物品推荐是为回答如下问题:给定一个物品,有哪些物品与它最相似?这里,相似的确切定义取决于所使用的模型。大多数情况下,相似度是通过某种方式比较表示两个物品的向量而得到的。常见的相似度衡量方法包括皮尔森相关系数,针对实数向量的余弦相似度和针对二元向量的杰卡德相似系数。

1.从考文献kao we MovieLens 100k数据集生成相似电影

MatrixFactorizationModel当前的API不能直接支持物品之间相似度的计算。需要自己实现。

当我们运行这条语句的时候

importorg.jblas.DoubleMatrix

会出现错误信息:

<console>:25: error: object jblas is not a member of package org       import org.jblas.DoubleMatrix

这时解决方法见参考文献[2],只需要把jblas-1.2.4-SNAPSHOT.jar复制到spark目录的jars文件夹下,然后重启sparkshell可以了,亲测成功

import org.jblas.DoubleMatrixval aMatrix = new DoubleMatrix(Array(1.0, 2.0, 3.0))//余弦相似度def cosineSimilarity(vec1: DoubleMatrix, vec2: DoubleMatrix): Double = { vec1.dot(vec2) / (vec1.norm2() * vec2.norm2())    }val itemId = 567val itemFactor = model.productFeatures.lookup(itemId).headval itemVector = new DoubleMatrix(itemFactor)cosineSimilarity(itemVector, itemVector)val sims = model.productFeatures.map{  case (id, factor) =>  val factorVector = new DoubleMatrix(factor)  val sim = cosineSimilarity(factorVector, itemVector)  (id, sim)}val sortedSims = sims.top(K)(Ordering.by[(Int, Double), Double] { case (id, similarity) => similarity })println(sortedSims.take(10).mkString("\n")) 

上述代码里使用了Spark的top函数。相比使用collect函数将结果返回驱动程序然后再本地排序,它能分布式计算出“前K个”结果,因而更高效。(推荐系统要处理的用户和物品数目可能数以百万计)

2.检查推荐的相似物品

如再用户推荐中所做过的,我们可以看看推荐的那些电影名称是什么?从直观上检查一下基于物品推荐的结果。

val sortedSims2 = sims.top(K + 1)(Ordering.by[(Int, Double), Double] { case (id, similarity) => similarity })sortedSims2.slice(1, 11).map{ case (id, sim) => (titles(id), sim)}.mkString("\n")

代码的运行结果可能不一,因为模型的初始化是随机的。

5.推荐模型效果的评估

如何知道训练出来的模型是一个好模型?这就需要某种方式来评估它的预测效果。评估指标指那些衡量模型预测能力或准确度的方法。它们有些直接度量模型的预测目标变量的好坏(如均方差),有些则关注模型对那些其未针对性优化过但又十分接近真实应用场景数据的预测能力(比如平均准确率)。

评估指标提供了同一模型再不同参数下,又或是不同模型之间进行比较的标准方法。通过这些指标,人们可以从待选的模型中找出表现最好的那个模型。

这里将会演示如何计算推荐系统和协同过滤模型里常用的两个指标:均方差以及K值平均准确率。

5.1均方差

均方差(Mean Squared Error, MSE)直接衡量“用户-物品”评级矩阵的重建误差。它也是一些模型里所采用的最小化目标函数,特别是许多矩阵分解类方法,比如ALS。因此,它常用于显式评级的情形。

它的定义为各平方误差的和与总数目的商。其中平方误差是指预测到的评级与真实评级的差值的平方。

均方根误差的使用也很普遍,其计算只需在MSE上取平方根即可。它等同于求预计评级和实际评级的差值的标准差。

val actualRating = moviesForUser.take(1)(0)val predictedRating = model.predict(789, actualRating.product)val squaredError = math.pow(predictedRating - actualRating.rating, 2.0)val usersProducts = ratings.map{ case Rating(user, product, rating) => (user, product)}val predictions = model.predict(usersProducts).map{        case Rating(user, product, rating) => ((user, product), rating)    } val ratingsAndPredictions = ratings.map{  case Rating(user, product, rating) => ((user, product), rating) }.join(predictions)val MSE = ratingsAndPredictions.map{case ((user, product), (actual, predicted)) => math.pow((actual - predicted), 2)}.reduce(_ + _) / ratingsAndPredictions.countprintln("Mean Squared Error = " + MSE)val RMSE = math.sqrt(MSE)println("Root Mean Squared Error = " + RMSE)

5.2 K值平均准确率

K值平均准确率(MAPK)的意思是整个数据集上的K值平均准确率。APK是信息检索中常用的一个指标。它用于衡量针对某个查询所返回的“前K个”文档的平均相关性。对于每次查询,我们会将结果中的前K个与实际相关的文档进行比较。

用APK指标计算时,结果中文档的排名十分重要。如果结果中文档的实际相关性越高且排名也更靠前,那APK分值也就越高。由此,它也很适合评估推荐的好坏。因为推荐系统也会计算“前K个”推荐物,然后呈现给用户。如果在预测结果中得分更高(在推荐列表中排名靠前)的物品实际上也与用户更相关,那自然这个模型就更好。APK和其他基于排名的指标也更适合评估隐式数据集上的推荐。这里用MSE相对就不那么合适。 

当用APK来做评估推荐模型时,每一个用户相当于一个查询,而每一个“前K个”推荐物组成的集合则相当于一个查到的文档结果集合。用户对电影的实际评级便对应着文档的实际相关性。这样,APK所试图衡量的是模型对用户感兴趣和会去接触的物品的预测能力。

推荐任务的MAPK得分通常都较低,特别是当物品的数量极大时。

def avgPrecisionK(actual: Seq[Int], predicted: Seq[Int], k: Int): Double = { val predK = predicted.take(k)var score = 0.0var numHits = 0.0      for ((p, i) <- predK.zipWithIndex) {        if (actual.contains(p)) {          numHits += 1.0          score += numHits / (i.toDouble + 1.0)        }      }      if (actual.isEmpty) {1.0} else {        score / scala.math.min(actual.size, k).toDouble      }}val actualMovies = moviesForUser.map(_.product)val predictedMovies = topKRecs.map(_.product)val apk10 = avgPrecisionK(actualMovies, predictedMovies, 10)val itemFactors = model.productFeatures.map { case (id, factor) => factor }.collect()val itemMatrix = new DoubleMatrix(itemFactors)println(itemMatrix.rows, itemMatrix.columns)val imBroadcast = sc.broadcast(itemMatrix)val allRecs = model.userFeatures.map{ case (userId, array) =>  val userVector = new DoubleMatrix(array)  val scores = imBroadcast.value.mmul(userVector)  val sortedWithId = scores.data.zipWithIndex.sortBy(-_._1)  val recommendedIds = sortedWithId.map(_._2 + 1).toSeq  (userId, recommendedIds)}val userMovies = ratings.map{ case Rating(user, product, rating) =>    (user, product)}.groupBy(_._1)val K = 10val MAPK = allRecs.join(userMovies).map{ case (userId, (predicted, actualWithIds)) =>      val actual = actualWithIds.map(_._2).toSeq      avgPrecisionK(actual, predicted, K)    }.reduce(_ + _) / allRecs.count    println("Mean Average Precision at K = " + MAPK)

5.3 使用MLib内置的评估函数

前面我们从零开始对模型进行了MSE,RMSE和MAPK三方面的评估。这时一段很有用的练习。同样,MLib下的RegressionMetrics和RankingMetrics类也提供了相应的函数以方便模型的评估。

1.RMSE和MSE

import org.apache.spark.mllib.evaluation.RegressionMetricsval predictedAndTrue = ratingsAndPredictions.map { case ((user, product), (predicted, actual)) => (predicted, actual) }val regressionMetrics = new RegressionMetrics(predictedAndTrue)println("Mean Squared Error = " + regressionMetrics.meanSquaredError)println("Root Mean Squared Error = " + regressionMetrics.rootMeanSquaredError)

2.MAP

import org.apache.spark.mllib.evaluation.RankingMetricsval predictedAndTrueForRanking = allRecs.join(userMovies).map{ case (userId, (predicted, actualWithIds)) =>  val actual = actualWithIds.map(_._2)  (predicted.toArray, actual.toArray)}val rankingMetrics = new RankingMetrics(predictedAndTrueForRanking)println("Mean Average Precision = " + rankingMetrics.meanAveragePrecision)val MAPK2000 = allRecs.join(userMovies).map{ case (userId, (predicted, actualWithIds)) =>  val actual = actualWithIds.map(_._2).toSeq  avgPrecisionK(actual, predicted, 2000)}.reduce(_ + _) / allRecs.countprintln("Mean Average Precision = " + MAPK2000)

参考文献

[1]. Spark机器学推荐引擎(spark-shell)

[2].  解决Spark导入jblas包提示不存在的问题

[3].  1.Spark启动时hive出现another instance of derby may have alreadybooted the database的错误