推荐是Mahout机器学习算法的主题之一,它极大地渗透到了人们日常生活的方方面面,比如,购物、社交等。本节将从三个方面对其进行阐述,首先对推荐程序的定义等概念进行描述,方便读者从更加规范化的层次理解推荐程序;然后介绍Mahout中关于推荐部分的一些算法,讲解一个推荐程序是如何做到根据历史数据进行预测和推荐的;最后给出一个实例进行算法演示,示范如何利用Mahout进行数据分析,并得出对用户的推荐结果。
一、推荐的定义与评估
(一)推荐的定义
人们每天对各种事物形成各自的观点:喜欢、不喜欢或者毫不关心,这些过程看似都是在无意识中发生的,但却是有规律可循。例如,当你从书架上拿起了某本书,这一定是有某种潜在的原因——也许这本书本身的内容你是感兴趣的;也许是你的某位同事向你推荐了这本书;也许是因为它恰好放在了你感兴趣的书籍的旁边。
这些原因和策略虽然各不同,但是在发现新鲜事物上都是同等有效的。推而广之,倘若你想要找到你所感兴趣的事物,可以有两种策略:一种策略是观察与你兴趣相同的人喜欢什么,因为他们喜欢的对象和事物你也极有可能会喜欢。例如:倘若你是一个骑行爱好者,你自然会去关心其他喜太骑行的期友在关注什么品牌的自行车,在购买哪一类骑行装备,而此刻喜欢钓鱼的朋友在购买什么品牌的钓竿或者使用什么类型的鱼饵的信息则对你是无用的,这一类推荐被称为基于用户的推荐。另一种策略是观察哪些物品和事物与你喜欢的事物相类似,例如,你骑过一辆捷安特的自行车,在更换新车时自然会关注同品牌的自行车或者不同品牌同级别的自行车品牌。
实际上,以上两种策略揭示了最为典型的两种推荐模式,基于用户(User-based)的推荐和基于物品(Item-based)的推荐,Mahout 的推荐程序中应用最广的也就是这两类。严格说来这两种算法的推荐程序都是属于协同过滤。协同过滤就是通过用户和物品之间的关联进行推荐,算法并不关心物品和用户自身的属性,只关心这两者之间的对应关系,进而给出推荐结果。
当然,除此之外还有基于内容的推荐。基于内容的推荐必须与特定的领域相结合,它更关心的是物品和用户自身的属性,从而难以规整地成为一个框架。例如:向读者推荐图书时,可以通过书籍的主题内容、语言、出版商、价格、装订方式等来确定潜在的读者。但是,这些知识无法直接转换并应用于其他领域,这种对图书的推荐方法,对于骑行爱好者挑选骑行装备则是毫无用处的,因此, Mahout 中对基于内容的推荐所言甚少。
(二)推荐的评估
推荐只是一种工具和手段,但是,推荐的结果是否符合用户的需求呢?这就是评估一个推荐程序的优劣程度。一个优秀的推荐程序,应该是在用户行动之前就能准确地获知用户喜欢的每一种物品的可能性,而且这些物品是用户并没有见过或者没有对其表达过喜好意见的。准确预测用户所有喜好和行为的推荐程序还应该按照用户未来的喜好把对象进行排队,一个好的推荐结果就应该是这样呈现的。给出一个从优到劣的推荐列表可以满足大多数场景的需求,事实上,在大多数情况下,精确的列表也没有那么重要,只要给出几个好的推荐结果就足够了。因此,可以用经典的信息检索(Information Retrieval)中的度量标准——查准率和查全率来对推荐进行评估。
查准率(Precision Ratio)是在推荐结果中相关结果的比率,是衡量检索系统和检索者检出相关信息的能力;查全率(Recall Ratio)是指所有相关结果中被推荐结果所占比例,是衡量检索系统和检索者拒绝非相关信息的能力。我们通常用这两个参数来评估推荐系统的效率,但是这两者存在相反的依赖关系。即当一个系统输出的结果中正确的比例越高(如只输出一个,命中一个,此时查准率最高100%),那么,这个正确结果占所有正确结果的比例越低;反之,一个系统试图以输出所有正确结果为目的(追求最高的查全率),那么,它犯的错误就必然会越多(查准率越低)。
另一个重要的概念是相似性度量,在基于用户/物品进行推荐时,常常要度量两个用户(物品) 之间的相似程度,常用的相似性度量有以下几种:皮尔逊相关系数、欧氏距离、余弦相似性、斯皮尔曼相关系数、Jaccard系数(用于忽略了偏好值的数据)、对数似然比等,这些概念都有标准的定义和描述。
在Mahout中,推荐引擎通常需要输入用户偏好数据,Mahout使用preference对象标识一个数据对象,它中三部分构成,包括用户ID、物品ID和偏好值。Preference Array是一个接口,表示偏好(Preference)的聚合,类似于Java数组,但是进行了存储优化,分为基于用户的实现GenericUserPreferenceArray和基于物品的实现GenericltemPreferenceArray,两者在内存实现上分别面向单一用户和单一物品实现。
另一个常常使用的数据对象是DataModel,它用于封装输入数据(常以文件形式),各种推荐算法均要用到,DataModel可以提供输入数据中所有用户ID的计数或者列表,提供与某个物品相关的所有偏好,或者给出所有对一组物品ID表达过偏好的用户个数。常见的DataModel分为三种实现: 内存级GenericDataModel、基于文件FileDataModel和基于数据库JDBCDataModel。
二、Mahout中的常见推荐算法
(一)基于用户的推荐算法
基于用户的推荐本身的原理植根于用户之间的相似性,通过参考相似性最大的用户的偏好进行推荐,算法过程描述如下:
for 用户u尚未表达偏好的每个物品i for 对i有偏好的每个用户v 计算u v之间的相似度s 按权重s将v对i的偏好并人平均值 return 排序后最高值物品
理论上说,该算法是一个严谨的基于所有用户的推荐程序。对外层循环,将每个未表达偏好的物品作为潜在推荐项进行评估;内层循环则将所有其他用户对此物品偏好值按照用户相似度进行加权叠加。也就是说,相似度越大的用户,那么他在物品偏好值赋予时所占比例也就越大。
实际上,为了使算法性能提升,通常不会考虑所有用户,而是先计算出所有用户的相似度,在一个用户领域内进行偏好值叠加,对于用户u来说,算法描述如下:
for 除用户u外的其他用户w 计算用户u和用户w的相似度s 按相似度进行排序,得到用户邻域n for n中用户有偏好,u中用户无偏好的物品i for n中所有对i有偏好的用户v 计算用户u和用户v相似度s 按权重s将用户v对物品i的偏好并入平均值 return 值最高的物品
在Mahout中,采用的是后一种算法,其中,各种工具采用以下类实现:
(1)数据模型,由DataModel实现;
(2)用户相似性度量,由UserSimilarity实现;
(3)用户邻域,由UserNeighborhood实现;
(4)推荐引擎类,由类Recommender实现,在实际应用中常用其派生类GenericUserBasedRecommender 实现。
(二)基于物品的推荐算法
基于物品的推荐算法与基于用户的推荐类似,但该算法是以物品之间的相似度进行判定的。一个简单的逻辑就是,如果喜欢A物品的用户也喜欢B物品,那么,就表明A、B物品之间是具有一定联系的,这种联系构成了推荐引擎的核心,与之对应算法描述如下:
for 用户u未表达偏好的每个物品i for 用户u表达偏好的每个物品j 计算i,j之间的相u度s 按权重s将用户u对的偏好并入 return 值最高的物品
可以看出,基于物品和基于用户的算法很相似,但又不完全相同。首先体现在,随着物品数量的增长,基于物品推荐的程序运行时间也会随之增长,而基于用户的推荐程序运行时间是随着用户数量的增加而增加的,因此,在选择推荐引擎时,需要考虑用户和物品的数量与分布情况,一般而言,哪一个比较少,就基于哪一个做推荐,这是由推荐算法本身的复杂度决定的。
在Mahout实现中,采用的实现方式与前一个算法一样,除了推荐引擎,常常采用的Recommender是GenericltemBasedRecommender。值得注意的是,并没有ItemNeighborhood存在,因为对于物品来说,某些物品是用户已经表达过偏好的,相当于已经确定了物品的邻域,而计算各个用户的偏好物品的邻域是没有意义的。
(三)基于SVD的推荐算法
奇异值分解(Singular Value Decomposition)是线性代数中一种重要的矩阵分解,在很多领域有着重要的应用。在推荐系统中使用SVD并不需要用户了解其具体过程,但为了使概念不至于混淆,用一个例子来简单介绍其原理。假设要处理的数据是某音乐软件的曲库,要对用户进行推荐,其中的歌曲数量可能有数十亿,然而用户对某些类型的歌可能会有特殊偏好,例如,喜欢古典钢琴的用户,可能会出现几百条关于古典钢琴曲的偏好,其他类型歌曲的数量少一些,其实可以抽象成“用户-歌曲类型-偏好值”,这样物品就由歌曲提炼为音乐类型(音乐类型是歌曲的一个特征),数量会大大降低,但推荐效果并不会差太多。在这个过程中,SVD起到的作用就是将歌曲抽象提炼出类型,也就是说它可以从繁杂的物品列表中提炼出一种特征,这种特征可能更具有代表性,根据用户对物品的偏好性得到的这种特性往往具有一般性,这也往往是一个小得多的数据集。
Mahout中基于SVD 的推荐引擎实现如下:
new SVDRecommender(model,new ALSWRFactorizer(model,10,0.05,10))
构造方法中的第二个参数是一个Factorizer,可以选择不同的Factorizer 。以其子类ALSWRFactorizer为例,第二个参数10表示生成10条特征,第三个参数0.05是正则化分解其特征值,第四个参数是需要执行的训练步骤数,数目越大,训练时间将会越长。
(四)基于线性插值的推荐算法
Mahout中实现了一种基于物品的推荐方法,与传统的基于物品的推荐方法不同的是,它不再简单地使用用户表达过偏好的物品之间的相似度,而是使用一些代数技术计算出所有物品之间的最优权重集合,对权重进行优化。同时,它采用了与用户邻域相似的概念,选择了N个最邻近的物品邻域,以使得上述的数学计算量不会变得过于巨大,具体实现方式如下所示:
ItemSimilarity similarity = new LogLikeLihoodSimilarity(model); Optimizer optimizer = new NonNegativeQuadraticOptimizer(); return new KnnItemBasedRecommender(model, similarity, optimizer, 10);
在上述代码中,KnnltemBasedRecommender是核心函数,第二个参数是计算物品之间的相似度,采用对数似然相似性度量LogLikelLihoodSimilarity,第三个参数optimizer表明采用二次规划的方法来进行求解,最后一个参数10表明选择最邻近的10个物品进行插值计算,当然这一数字需要根据数据集实际情况来确定。
(五)基于聚类的推荐算法
目前,基于聚类的推荐算法是公认的效果最好的推荐算法,与传统的基于用户的推荐算法不同,基于聚类的推荐算法不再将推荐局限于某一个用户,而是将推荐结果推荐给相似的用户簇。因此,必须有一个划分用户簇的过程——聚类,它根据用户集划分到不同簇,对于每个簇的内部而言,般认为其中的用户是足够相似的,这样推荐出来结果会被尽可能多的用户所接受。
显而易见,这种方式的好处在于推荐过程会很快,推荐程序不需要再为每一个用户单独计算物品偏好序列。但是,缺点也十分明显,推荐的结果的个性化不强。当用户本身没有历史数据可供参考时,我们可以将他划分到一个聚簇中,往往会具有比较好的效果。根据对用户的不断了解和重新划分,推荐的结果也会越来越好。
在Mahout 中,基于聚类的推荐算法可以采用如下实现方式:
UserSimilarity similarity = new LogLikelihoodSimilarity(model); ClusterSimilarity clusterSimilarity = new FarthestNeighborClusterSimilarity (similarity); return new TreeClusteringRecommender(model,clusterSimilarity,10);
在上述代码中,实现基于聚类的推荐引擎由TreeClusteringRecommender构造,第一个参数是输入的数据,第二个参数为聚簇相似性clusterSimilarity,它由用户相似性similarity 通过构造函数FarthestNeighborClusterSimilarity(根据两个簇之间最不相似用户的相似度得出)得到,也可以采用NearestNeighborClusterSimilarity(根据两个簇之间最相似用户的相似度得出)进行构造。
三、对GroupLens数据集进行推荐与评价
这一节中,以GroupLens数据集为例来演示Mahout进行推荐的具体流程。这个数据集包括了很多用户对电影的评价,每一个数据由四个维度构成,用户编号、电影编号、评分和时间戳,对于推荐程序而言,前三个维度就已经足够,可以利用这些数据进行推荐评估。相关的数据集可以在http://files.grouplens.org/datasets/movielens/中下载,在这里使用100KB大小的数据集进行演示,在上述链接中下载ml-100k.zip即可,在解压文件中找到ua.base文件作为评估数据集。
(一)如何使用推荐器进行推荐
数据读取,这是一个以制表位分隔的数据文件,可以使用FileDataModel类进行读取。
DataModel model = new FileDataModel(new File ("ua.base"));
什么是FileDataModel呢?在推荐程序中,常常见到preference(用户偏好)数据,包含userld、itemld和偏好值(user对item的偏好),处理的数据一般是preference的集合,如果使用Collection<Preference或者Preference[]来处理偏好集合会十分低效。为了提高数据存储和使用效率,Mahout使用PreferenceArray和其他一些数据结构来改造前两者,使得对大量数据的存储变的高效。实际上Mahout接受的数据输入常常是DataModel,这是对PreferenceArray的进一步封装,提供了偏好数据中与用户ID相对应的count计数表,可以加快对具体用户偏好数据的访问。一般不直接使用DataModel,而是使用GenericDataModel、FileDataModel 和JDBCDataModel,它们分别是针对内存数据、文件数据和数据库数据而设计的。
推荐引擎通常需要计算用户与用户或者物品与物品之间的相似度,对于量级较大的数据源来说,Mahout提供了大量用于计算相似度的组件,如皮尔森相关度(PearsonCorrelationSimilarity)欧氏距离相似度(EuclideanDistanceSimilarity)等。在这个例子中,我们使用皮尔套相似度进行用户似性计算:
UserSimilarity userSimilarity = new PearsonCorrelationSimilarity(dataModel);
根据上面计算得出的相似度,选用一部分最相似的用户来作为推荐参考,即指定用户邻域:
UserNeighborhood userNeighborhood = new NearestNUserNeighborhood (2,userSimilarity,dataModel):
当得到了用户相似度和用户邻域后,就可以开始进行推荐了(这里对用户a推荐一个物品,a为用户编号,可以自己设置)
Recomrmender recommender = new GenericUserBasedRecommender (dataModel, userNeighborhood, userSimilarity); List<RecommendedItem>recommendedItems = recommender.recommend(a,1); for (RecommendedItem recommendedItem:recommendedItems) { System.out.println(recommendedItem) }
(二)如何评估推荐器的好坏
推荐程序编写完毕后,往往想要知道推荐程序推荐结果怎么样,Mahout使用RecommenderEvaluator类对象的evaluate方法。
double evaluate (RecommenderBuilder recommenderBuilder, DataModelBuilder dataModelBuilder, DataModel dataModel, double trainingPercentage, double evaluationPercentage)
evaluate方法对一个推荐器构造方法进行评价,参数说明如下。
(1)recommenderBuilder:推荐器的构造方法,下面会给出实例。
(2)dataModelBuilder:dataModel构造方法,可以为null。
(3)dataModel:评价时使用的数据集。
(4)trainingPercentage:训练样本比例,[0,1]区间取值。
(5)evaluationPercentage:评估样本比例,[0,1]区间取值。
当对之前的推荐器进行评价时,并不能直接使用recommender,而是要构造一个recommenderBuilder对象,这个对象描述了recommender是如何构造的,代码如下所示:
RecommenderBuilder builder = new RecommenderBuilder() public Recommender buildRecommender(DataModel dataModel) throws TasteException { UserSimilarity userSimilarity = new PearsonCorrelationSimilarity (dataModel); UserNeighborhood userNeighborhood = new NearestNUserNeighborhood(2, userSimilarity, dataModel); return new GenericUserBasedRecommender (dataModel,userNeighborhood,userSimilarity); } };
然后,创建一个RecommenderEvaluator对象对上面的builder进行评估,此时,程序输入评估的结果为0.8761682242990649,这意味着推荐程序所给出的估计值与实际值的偏差为0.8,因为输入样本偏好是从1到5,所以,0.8的偏差不算太大,但也不算很好。
RecommenderEvaluator evaluator = new AverageAbsoluteDifferenceRecommenderEvaluator(); double score = evaluator.evaluate (builder, null, dataModel, 0.7, 1.0); System.out.println("the simple evaluate score is:"+score);
这里使用的是基于用户的推荐程序(GenericUserBasedRecommender),还可以试着使用其他形式的推荐器,如slope-one推荐程序等,构造一个RecommenderBuilder对象并使用RecommenderEvaluator 对其进行推荐评价。