有的地方译的磕磕巴巴,需要的凑合着看吧。
Mahout in Action学习笔记
目录
Part1 Recommendations 3
2 Introducing recommenders 3
2.3 评价推荐算法 3
2.3.1 训练数据和得分 3
2.3.2 运行推荐系统评估工具(RecommenderEvaluator) 4
2.2.3 获取评价结果 5
2.4 准确率和召回率 5
2.4.1 运行RecommenderIRStatsEvaluator 6
2.4.2 准确性和召回率带来的问题 6
3 Representing recommender data 7
3.1 偏好数据的表示 7
3.1.1 表示偏好信息的对象 7
3.1.2 PreferenceArray及其实现 8
3.1.3 加速集合对象(collections) 9
3.1.4 FastByIDMap和FastIDSet类 9
5 Taking recommenders to production 10
5.1 分析约会网站的数据 10
5.2 选择合适的推荐算法 10
5.2.1 基于用户的(User-based)推荐 11
5.2.2 基于项目(Item-based)推荐 12
5.2.3 Slope-one推荐 13
5.2.4 评价准确率和召回率 13
5.2.5 性能评估 14
5.3 添加领域相关信息 14
5.3.1 使用性别相似性的度量方法 14
5.3.2 基于内容的推荐 15
5.3.3 用IDRescorer改善推荐 16
5.3.4 在IDRescorer中使用性别信息 17
5.3.5 打包订制的推荐系统 19
5.4 向匿名用户推荐信息 20
5.5 创建Web推荐引擎 20
5.5.1 打包WAR文件 20
5.5.2 测试已部署的系统 20
5.6 更新和监测推荐系统 20
6 Distributing recommendation computations 22
6.1 分析维基百科的数据集 22
6.1.1 与大规模数据的斗争 23
6.1.2 评价分布式计算的优缺点 23
6.2 设计基于项目的推荐算法 23
6.2.1 构建共生矩阵 23
6.2.2 计算用户向量 24
6.2.3 产生推荐 25
6.2.4 对结果的理解 25
6.2.5 向分布计算进发 26
6.3 用MapReduce实现一个分布式算法 26
6.3.1 MapReduce简介 26
6.3.2 用MapReduce生成用户向量 26
6.3.3 用MapReduce计算共生矩阵 28
6.3.4 用MapReduce实现矩阵相乘 29
6.3.5 用MapReduce实现矩阵的部分相乘 29
6.3.6 用MapReduce实现推荐 29
6.4 在Hadoop上运行MapReduce 29
6.5 伪分布推荐系统 29
6.6 Looking beyond first steps with recommendations 29
6.7 总结 29
Part3 Classification 30
13 Introduction to classification 30
14 Train a classifier 30
14.1 抽取特征以构建Mahout分类器 30
14.2 将生数据转化为可分类数据 32
14.2.1 转化生数据 32
14.2.2 营销计算的例子 33
Part1 Recommendations
2 Introducing recommenders
2.3 评价推荐算法
推荐引擎作为一个工具—一个回答问题的工具。对于用户来说,什么样的推荐才是最好的?在探求答案之前最好还是探究下问题(什么样的推荐引擎最好)。到底什么样的推荐引擎才是最好的。用户如何知晓推荐引擎如何才能产生满足需要的答案?本章的余下部分将介绍如何评价推荐引擎,因为,在实现一个特殊的推荐系统时其评价标准相当重要。
最好的推荐引擎可能需要有一种“通灵”能力,在某种程度上,它可以在用户的行为发生之前,就对用户的某些偏好做出预测,预测某些用户对某一项目(item)的偏好。推荐系统可以预测将来你对所有项目的实际偏好。这样的推荐系统才可能是一种好的推荐系统。
事实上,大多数的推荐引擎实际上也就是对所有项目(item)的评分进行预测。所以评价推荐引擎的性能指标就是要评估其对项目的预测精度如何,即如果预测值和实际值之间的误差越小,则推荐系统的准确性越好。
2.3.1 训练数据和得分
虽然这些确切的评分偏好并不存在。也没有人知道(包括你自己)未来你对一个新的项目(item)评分是多少。所以,在推荐引擎中,用一部分真实的数据作为测试数据,来测试预测值和真实值之间的误差。在真实的测试数据中,需要去掉一部分已经被评分的项目偏好数据。然后将去除特定评分项的数据输入推荐系统,让推荐系统预测缺失项目的评分,这样就可以用预测值和真实值进行对比,就可以知道推荐系统的精度了。
按照以上的叙述,测试推荐引擎的性能就很简单了。比如,可以很方便的计算预测值和真实值之间的平均误差。平均误差越低越好,因为那意味着估计值更靠近真实值。如果误差为0.0,那说明估计值的结果是相当完美的—估计值和真实值之间没有误差。
有时候也使用均方根误差作为误差评价标准:即预测值和真实值差的平方和取平均值后再开方。依然是,这个值越小越好。表2.1展示了相关结果。
表2.1 预测值和真实值间的平均误差和均方根误差
表2.1展示了一系列真实值和预测值之间的差异,从中可以看到如何计算平均误差和均方根误差的。均方误差计算方法对偏离程度大的项目(此处item2偏离度较大)有更大的惩罚(即误差越大,相比较而言均方误差的值比平均误差的值要大),在某些时候这种惩罚是符合实际需要的。For example, an estimate that’s off by two whole stars is probably more than twice as bad as one off by just one star。因为均方误差计算方法更直观,物理意义上也更好理解,所以后续的例子中将使用平均误差作为误差测量方法。
2.3.2 运行推荐系统评估工具(RecommenderEvaluator)
让我们再一次使用代码,在数据集上进行计算,如下
RandomUtils.useTestSeed();//产生可重复的结果
DataModel model = new FileDataModel (new File("intro.csv"));
RecommenderEvaluator evaluator =
new AverageAbsoluteDifferenceRecommenderEvaluator ();//构建推荐器
RecommenderBuilder builder = new RecommenderBuilder() {
@Override
public Recommender buildRecommender(DataModel model)
throws TasteException {UserSimilarity similarity = new PearsonCorrelationSimilarity (model);
UserNeighborhood neighborhood =
new NearestNUserNeighborhood (2, similarity, model);
return
new GenericUserBasedRecommender (model, neighborhood, similarity);
}
};
double score = evaluator.evaluate(builder, null, model, 0.7, 1.0);
System.out.println(score);
【注意:evaluate的最后两个参数需要注意,Mahout in Action中的描述不清楚,跟踪其代码后,发现其意义如下:0.7表示项目(item)百分比,在此处项目共有7个,ID为:101-107,从中随机去除30%的项目,这些去除的项目是要靠推荐系统预测出来的,预测出的值会与真实值进行比较,至于如何去除这30%的项目,Mahout的代码已经实现好了调用即可;最后一个参数1.0控制的是整个数据集中被使用的用户的百分比,此例子中,用户共有5人,ID为1-5,此处1.0表示使用全部的数据,如果1.0改为0.8,则使用80%的数据,即从1-5个用户中取出4个数据,在这四个用户中,有些用户对项目的评分的会被删除,具体哪些评分被删除由前一个参数0.7控制;还有一个问题,4个用户中有些用户的评分是完整的,有些用户的评分是要被删除的,因为要做测试,哪些用户的评分要被删除由系统决定。】
大多数的计算行为都发生在evaluate()方法中:RecommenderEvaluator将数据分割成训练集和测试集,并构建一个新的用于训练的数据模型DataModel和用于测试的推荐器Recommender,最后将推荐器估计的值与实际的偏好值进行比较,得到误差值。
注意,在evaluate的参数中没有Recommender类型的参数。那是因为在相关方法的内部会自动为训练数据模型DataModel创建一个新的Recommender。调用evaluate时必须要提供能够为训练数据模型DataModel创建推荐器Recommender的对象实例,即RecommenderBuilder对象。此处,创建的RecommenderBuilder在之前的章节中已经有所介绍了。
2.2.3 获取评价结果
在2..2.2中的程序打印出最后的计算结果:一个表示推荐器性能的数字。在2.2.2的例子中的结果数据位1.0。即使在2.2.2的例子中随机的选择测试数据,但是最终的计算结果依然为1.0,因为每次运行时,RandonUtils.useTestSeed()会强迫程序使用相同的随机数。只有在这个例子中才可以这样使用随机数,因为单元测试时要保证结果是可以重复验证的。在实际的应用中不要这样使用。
最后输出的数据(此处为1.0)的意义会根据不同的实现不同而不同,这里的表示的是平均绝对误差—由AverageAbsoluteDifferenceRecommenderEvaluator实现。这里的1.0表示,平均意义上来说,推荐器估计的偏好值与实际值的误差为1.0。
偏好的取值范围为1-5,1.0的误差并不算大。计算结果也许会不一样,因为数据是被随机拆分的,所以每次的训练数据和测试数据可能会不同。
可以将这种平均绝对误差评价方法应用到其它推荐器Recommender和数据模型DataModel上。也可以不采用平均绝对误差而采用均方误差计算方法,只要将类AverageAbsoluteDifferenceRecommenderEvaluator换成RMSRecommenderEvaluator类即可。
也可以将evaluate()中的参数null换成DataModelBuilder的实例,其可以用来控制如何从训练数据中创建训练的DataModel。一般来说缺省值就可以了,it may not be if you’re using a specialized implementation of DataModel in your deployment—a DataModelBuilder is how you would inject it into the evaluation process.
1.0这个参数控制整个数据集中被用于测试的数据的百分比。这里的1.0表示100%使用这些数据。如果数据集很大时,可以使用这个参数控制用于测试的数据数量,这样做虽然会导致误差但是速度会很快。如果选择0.1表示有10%的数据用于测试,剩下的90%都被忽略了。如果你想测试数据稍有变化时结果如何时,这个参数就很重要了。
2.4 准确率和召回率
对用户偏好的估计并不仅仅是推荐系统要做的唯一的事,还有其它一些方面用来衡量推荐系统的好坏。一般来说推荐系统只要能够向用户推荐出一系列按照相关性从大到小顺序排列的结果就可以了,并不需要向用户显示预测的偏好是多少。事实上,大多数情况下推荐结果列表的顺序如何并无太大的关系,关键是结果列表中要有对用户有用的项目。
更一般的观点,可以将经典的信息检索度量方法:准确度和召回率,用来衡量推荐系统。这两个指标的典型应用时搜索引擎,搜索引擎返回与查询串相关的一系列可能的结果。
搜索引擎不应该返回与查询不相关的结果,但是其需要尽可能多的返回与查询相关的结果。精度是在搜索结果中与查询相关的结果与整个搜索结果的比例。精度等于10表明有在搜索结果中有10条结果与查询相关。召回率指的是查询结果中与查询相关的结果与整体相关结果的比例。图2.3展示了这两者的示意图。
图2.3 搜索引擎中准确率和召回率的关系
推荐系统的准确率指的是推荐结果中符合要求的结果与整个结果列表数量的比值,召回率指的是推荐结果中符合要求的结果(good recommendations)与整体符合要求的结果的比例。下节将介绍什么是符合要求的结果(good recommendations)。
2.4.1 运行RecommenderIRStatsEvaluator
Mahout提供了相当简单的接口计算推荐器的准确率和召回率,代码如下
RandomUtils.useTestSeed();
DataModel model = new FileDataModel (new File("intro.csv"));
RecommenderIRStatsEvaluator evaluator =
new GenericRecommenderIRStatsEvaluator ();
RecommenderBuilder recommenderBuilder = new RecommenderBuilder()
{
@Override
public Recommender buildRecommender(DataModel model)throws TasteException
{
UserSimilarity similarity = new PearsonCorrelationSimilarity (model);
UserNeighborhood neighborhood =new NearestNUserNeighborhood (2, similarity, model);
return new GenericUserBasedRecommender (model, neighborhood, similarity);
}
};
IRStatistics stats = evaluator.evaluate(recommenderBuilder, null, model, null, 2,
GenericRecommenderIRStatsEvaluator.CHOOSE_THRESHOLD,
1.0);
System.out.println(stats.getPrecision());
System.out.println(stats.getRecall());
如果不使用RandomUtils.useTestSeed()计算的结果将会有很大的不同,因为训练集合测试集是随机选择的。在这个例子中数据集很小,计算的结果如下
0.75
1.0
。。。。。。。。。。。。。。。。。。。。。。。
2.4.2 准确性和召回率带来的问题
3 Representing recommender data
这一章将介绍:
Mahout如何表示推荐数据的
DataModel的实现和使用
无偏好值的数据处理
推荐系统的质量(性能)与推荐数据的量和质量有很大关系,垃圾数据进垃圾数据出,是对推荐系统的最真实描述(所以要想有好的推荐效果,就要求推荐的数据质量要高)。所有如果能拥有大量高质量的数据,将是一件非常好的事情。
从本质上说,推荐算法对数据是很敏感的,它通过计算获得有用的信息。推荐系统的运行时效率受数据的质量和表示形式影响很大。从数量级和数据规模方面着手,灵活的选择数据结构能够提高推荐系统的性能。
本章将介绍Mahout中用于表示和访问推荐数据的Java类。你将会看到,为了达到高效和可扩展性,在Mahout中用户(users)项目(items)以及它们的偏好值要采取特殊的存储形式。本章还会介绍用于访问DataModel的关键抽象方法。
最后将讨论,当用户和项目的偏好值为Boolean时,将会出现什么样的问题,以及如何处理这种类型的数据。
第一部分介绍基本的推荐数据单元,用户-项目的评分表示。
3.1 偏好数据的表示
输入推荐引擎的是一系列偏好数据—包括:谁喜欢什么,喜欢的程度如何。这意味着输入Mahout的数据是用户ID,物品ID,和用户对物品偏好值的三元组集合,当然这种集合的数量应该很大。有时候一些偏好值也可能被遗漏。
3.1.1 表示偏好信息的对象
Preference是最基本的抽象类,它表示了用户ID,项目ID,和偏好值。一个实例化的对象就表示了用户对项目的偏好。Preference是一个接口,它的一个实现是GenericPreference,这也是最常用的一个类。例如下面的代码就表示用户123对项目456的偏好值为3.0。
new GenericPreference(123, 456, 3.0f)
如何表示Preference的集合哪?如果你的答案是用Collection<Preference>或者Preference[]来存储集合,在Mahout的许多API中,这种回答都是错误的。在存储大量Preference对象时,Collections和数组的效率是很低的。如果你还没有研究过Java中的对象,那准备受惊吧。
一个GenericPreference对象包含20-byte的有用数据:8-byte用户ID数据(Java long),8-byte项目ID(long),4-byte偏好值(float)。一个Java对象的存在需要高达28-byte的开销。其中8-byte是由于对象引用和对象对其所造成的开销,剩下的20-byte代表对象的本身。因此一个GenericPreference对象消耗的资源是其实际需要的1.4倍。
【注意:实际消耗的资源量根据JVM的实现稍有不同,上面的资源消耗计算方法是针对苹果的Mac OS X10.6操作系统,64位Java6虚拟机的】
在推荐算法中为了存储于用户或者项目相关的偏好信息,要经常使用集合类型的数据结构。在集合数据结构中,对于所有的Preference对象,用户ID和项目ID都是一样的,这样看来用户ID和项目ID是有冗余的。
3.1.2 PreferenceArray及其实现
在PreferenceArray类中,使用类数组(array-like)的API实现了能够表示偏好信息的集合(collection)。GenericUserPreferenceArray类表示一个用户的所有偏好。在GenericUserPreferenceArray内部,包括一个用户ID,一个用户存储项目ID的数组和用户存储偏好信息的数组。在这种表示形式下,存储一个偏好信息所需要的开销为12-byte(8-byte存储用户ID,4-byte存储偏好值)。如果不采用这种存储形式,而是完全存储一个Preference对象,则需要48-byte的开销。GenericUserPreferenceArray的存储形式节省了将近4倍的存储空间,同时在性能方面也有一些改进,因为系统需要分配的对象少了,随之而来的就是垃圾处理器需要处理的垃圾回收工作也就少了。通过对比图3.1和3.2能够发现时如何实现节省开销的。
图3.1 效率较低的偏好表示方式,其使用了Preference对象数组存储每个偏好信息,灰色部分表示对象的开销。白色部分表示数据和对象的引用
图3.2 GenericUserPreferenceArray高效的表示方式
下面的代码展示如何构造和访问PreferenceArray对象。
PreferenceArray user1Prefs = new GenericUserPreferenceArray(2);
user1Prefs.setUserID(0, 1L);//设置第0个元素存储的用户ID为1
user1Prefs.setItemID(0, 101L);//设置第0个元素存储的项目ID为101
user1Prefs.setValue(0, 2.0f);//设置第0个元素的偏好为2.0
user1Prefs.setItemID(1, 102L); //设置第1个元素存储的项目ID为102
user1Prefs.setValue(1, 3.0f); //设置第1个元素的偏好为3.0
Preference pref = user1Prefs.get(1);//访问第一个元素所存储的偏好信息
GenericItemPreferenceArray类与GenericUserPreferenceArray类相似。它存储的信息正好是与GenericUserPreferenceArray相反的,存储了每个项目对所有用户的偏好信息,它的用法和GenericUserPreferenceArray相似。
3.1.3 加速集合对象(collections)
你可能会想,真不错Mahout已经重新实现了Java的对象。请系好安全带,因为旅程还没有结束。我们是否已经说过数据的规模是很重要的?希望你已经相信将要处理的数据将是巨大的,其生产的结果也是非同寻常的。
PreferenceArray类及其实现存在一定的复杂性,但是与其节省的资源相比还是值得的。25%的资源节省,不仅仅意味着两兆字节—它可能会节省高达数十GB的存储空间。如果你目前的硬件无法存储太大数据的话,能够节省数十GB的资源可能意味着巨大的意义。节省25%的资源,可能就会让你不用投入更多的RAM或者64位的操作系统。25%虽然是个小数字,但它确实能节省不少的存储空间。
3.1.4 FastByIDMap和FastIDSet类
Mahout推荐引擎中使用了大量的键值对和集合类型的数据结构,但是它没有使用Java中实现的TreeSet和HashMap。而是实现了自己的API:FastMap,FastByIDMap,FastIDSet。这些数据结构和Map和Set的类型相似,但是它们是为Mahout的推荐系统单独实现的。实现这些数据结构的目的是为了减少内存占用,而不是为了显著提高其性能。
这里没有直接使用Java框架下集合类型的数据结构,并不能说Java框架不好。相反,在设计集合类型的数据结构时要考虑其广阔的应用背景。而不能只考虑其特殊的应用模式。Mahout的需求是相当具体的,而且其应用有很强的特殊性。主要的差异如下:
和HashMap类似,FastByIDMap也是基于哈希的。为了解决哈希冲突问题,FastByIDMap采用线性探测而不是独立链表。对于每个入口来说这将会避免额外的Map.Enry对象的开销。从前面的讨论知道,每个对象消耗的资源是惊人的。
5 Taking recommenders to production
这一章节只要包括:
从真实数据网站中分析数据
设计和优化推荐引擎解决方案
发布基于Web的推荐服务
到目前为止,本书已经向读者展示了推荐算法以及Apache Mahout的其它方面,同时讨论了如何评价推荐器的精度和性能。下一步就是要介绍在真实数据的基础上,如何用上面所介绍的推荐算法实现一个真实、高效的推荐引擎。你可以创建一个基于真实数据的推荐引擎,并可以将其发布成为Web服务。
因为存在各种特定的数据和特定的问题,所以为了解决这些问题,就需要用不同的方法创建不同的推荐器,才能解决这些问题。数据必须要表示成用户(users)和项目(iterms)之间的关联关系(用户和项目之间可能有很多东西)。对于不同问题,输入到推荐算法的数据会有很大不同。对于不同的问题背景,如何选择最好的推荐引擎以使其满足特定的输入数据?毫无疑问,这就需要对数据进行实践探索、实验、评估。
本章将用几个例子,展示在特定的数据集上,如何使用Mahout开发一个完整的推荐系统。你可能需要多次的收集数据和评价结果。可能很多尝试的最后结果都令人失望,但其依然是有意义的(证明了哪些事情是不可能也是不错的结果)。这种常规的计算方法是合适的,因为在Mahout中其是一种相对无害的计算方法。毕竟,对于一个特点的数据集哪一种方法是最合适的还不是很清楚。
5.1 分析约会网站的数据
这个例子将使用一个新的婚介网站(http://libimseti.cz/)的数据集。这个网站上的用户可以用数字1-10为其它人的简介(profiles)打分。1分意味着不喜欢,10分意味着喜欢。 从网站上的简介资料可以得到用户的诉求、吸引力和可以交友的程度。为了研究需要,数据集中的真实姓名都没隐藏了,数据集由Vaclav Petricek整理发布,可以在http://www.occamslab.com/petricek/data/下载,可以从http://www.occamslab.com/petricek/data/libimseticomplete.zip下载完整的数据集,本章将使用此数据集。
在数据集中共有17,359,346项评分,几乎是之前数据集GroupLens的两倍。数据集中包括了用户对项目(items)的评分。这些项目是其它用户的描述。这意味着基于此数据集的推荐系统向用户推荐的将是“人”。这表明推荐系统有广泛的应用,而不是仅仅局限于推荐书或者是DVD。
构建推荐系统的第一步是要分析数据,同时选定将要使用的推荐算法。在257MB的数据集中包括了用逗号分隔的用户ID,简介ID和评分。每一行表示一个用户对其它用户的评分,数据集中的数据是被有意打乱的,所以数据集中的用户ID并不是真正的网站上用户的ID…………………………….待添加
5.2 选择合适的推荐算法
需要根据数据集,然后再从Mahout中选择合适的推荐算法。推荐的准确率和推荐的性能(速度快慢)是推荐算法的两个最重要因素。在这两个因素中准确率又更为重要,而推荐的实时性次之。After all, what’s the use in producing bad answers quickly?
如果仅仅是简单的观察数据,是难以从中发现规律的,所以需要对数据做一些实验性的测试。在低2章中介绍了推荐算法的评价框架,下一步就是要用这些评价方法对数据进行处理,以发现哪些实现是适合于处理这些数据的。
5.2.1 基于用户的(User-based)推荐
毫无疑问基于用户的推荐是首选算法。在Mahout中,提供了几种相似度度量和定义近邻方法。如果想查看哪些算法是有效的哪些是无效的,需要将多种情况组合计算最后查看效果如何。在表5.1和5.2,图5.1和5.2中列出了一些实验结果。
表5.1 采用基于用户的推荐算法时,不同相似性计算方法与不同近邻数组合时的预测值与真实值的平均绝对误差
表5.2 采用基于用户的推荐算法时,不同相似性计算方法与采用由阈值控制近邻时预测值与真实值之间的平均绝对误差,表中有些元素没有值。
这些数据都还不坏。这些推荐算法的估计误差平均在1.12到1.56之间,整个的取值范围是1到10。
在图中可以发现某些趋势,不过也许会有极个别的点与这一趋势不符。从这些数据中可以发现,虽然各个算法的趋势都差不多,但是欧拉距离会比皮尔逊距离稍微好点。同时还可以发现使用近邻数少时的效果比使用近邻数多的的效果要好,当使用2近邻时的效果最好。这也许是因为用户的偏好更具个性化,所以计算相似性时用的近邻越多效果不一定最好。
图5.1 表5.1中的数据
图5.2 表5.2中的数据
如何解释在用Tanimoto相似度量相似性时,产生的NaN值?在此处展示该方法是为了说明此方法的微妙之处。虽然相似性度量方法返回的是一个-1到1之间的值,其越大表示相似性越大,但是不同算法的计算结果可能代表不同的意义。This is generally true, and not an artifact of how Mahout works。例如,用皮尔逊相关度算得的相似性为0.5,其只代表中度的相似性。如果用Tanimoto方法算得的相关度为0.5,其就代表两个有很强的相似性:of all items known to either of them, half are known to both。
即使对于其它方法0.7到0.95的阈值是合适的,但是对于Tanimoto方法这一阈值范围就比较高了。对于Tanimoto方法,t的每个取值都比较大,这就导致在计算近邻时无近邻可以选择。在这里如果使用0.4或者比0.4更小的测试阈值更合适。事实上,最好的阈值选择为0.3,得到的结果评分误差为1.2。
在近邻选择算法中,可以发现存在最好的近邻数n,其使预测误差最小,但是在阈值测试的例子中,无法选定一个阈值使预测误差最小。比如,欧拉距离度量方法看起来随着阈值的增加预测误差会变小。Perhaps the most valuable users to include in the neighborhood have a Euclidean-based similarity of over 0.95. What happens at 0.99? Or 0.999? The evaluation result goes down to about 1.35; not bad, but not apparently the best recommender.
你可以继续寻找更好的影响效果的参数。但在本例中,只要考虑如下因素即可:
基于用户的推荐算法
欧拉相似性度量方法
近邻数位2
5.2.2 基于项目(Item-based)推荐
基于项目的推荐只需要计算项目之间的相似性。尝试每个相似性计算方法。结果如表5.3。
表5.3 几种不同方法计算的项目与项目之间相似度的预测值与正确值之间的误差
这里的数据表明,结果很差,因为预测值和真实值之间的平均误差已经超过了2。说明对于被用于测试的数据,不适合采用基于项目的算法。为什么哪?在基于用户的相似性计算中,通过用户对其它用户的描述进行评分来计算相似性的,而现在是通过其它人如何对某一描述进行评分达到计算项目的相似性的目的:也许基于用户的评价比基于项目的评价包含了更多的信息。
无论如何解释,从表5.3的数据中发现,基于项目的推荐算法都是不合适的。
5.2.3 Slope-one推荐
回忆下Slope-one推荐算法,在数据模型中其构造了项目与项目之间的差值。在这里采用的数据集中,含有168,791个项目(项目、描述)。这意味着要存储280亿个差值数据—这些数据无法直接存放到内存中。将这些差值数据存储到数据库中是可能的,但它的的性能将非常的差。
幸运的是,可以使用计算框架来存储1千万量级数的差值个数。Mahout计算框架会选择更有用的差值存储。这里的“有用”指的是,如果一对项目同时出现在很多人的评价列表中,那么这对项目的差值就是有用的。例如,如果有上百人对项目A和B进行了评价,这些项目的平均差值就是重要的且“有用”的。如果只有一个人对A和B进行了评价,那么这种数据对推荐来说就没有什么价值。
DiffStorage diffStorage = new MemoryDiffStorage(
model, Weighting.WEIGHTED, 10000000L);
return new SlopeOneRecommender(
model, Weighting.WEIGHTED, Weighting.WEIGHTED, diffStorage);
通过查看Mahout的log文件,发现用这种方法处理数据集时需要大约1.5GB的内存。同时也可以发现Slope-one的速度如何,在工作站上进行测试发现平均的推荐时间是10毫秒,而其它算法要200毫秒。
计算结果的值约为1.41。这样的结果还不算坏,但是它没有基于用户的推荐结果好,对于数据集libimseti,Slope-one方法还不是最优的,所以不使用它。
5.2.4 评价准确率和召回率
前面的例子中,介绍了用Tanimoto方法和对数方法(log-likelihood)计算相似性,这两种方法都没有使用偏好值进行计算。没有对这两种方法进行评价。像以上这两种方法,无法使用准确率和召回率进行评价,因为其没有使用偏好值,就更谈不上使用估计的偏好值与真实的偏好值进行对比了。
不过可以使用RecommenderIRStatsEvaluator来实现其与最佳方案(基于用户的推荐,采用欧拉距离,采用2近邻)进行对比,以评估其准确率和召回率。
。。。。。。。。。。。。。。。。待添加
5.2.5 性能评估
对运行时的性能评估是很重要的。因为推荐系统要求实时性,如果要花费分钟量级的时间实现一次推荐的话,可能用户体验的效果就会很差了。
可以使用LoadEvaluator类获取一次推荐所需要的时间。用如下命令在数据集上运行推荐系统
-server -d64 -Xmx2048m -XX:+UseParallelGC -XX:+UseParallelOldGC
测试发现在数据集libimseti上推荐的平均时间为218毫秒。消耗了10亿字节的内存(大约1GB)。不同的应用和硬件情况,对推荐的速度性能和内存情况有不同的要求。对于许多应用来说,以上介绍的配置(5.2.1-5.2.5)已经能够满足要求。
到目前为止,使用Mahout标准的推荐算法(没有做特定的定制)在数据集上做了测试。但是,如果想为一个特定的数据集构建一个最好的推荐系统,就需要使用所有可用的信息。那么这就需要改变Mahout中标准实现,以使用特殊的数据属性。在下一节,将介绍为了能够使用数据集中的特殊属性,如何修改Mahout中的标准实现,最后达到提高推荐系统性能的目的。
5.3 添加领域相关信息
到目前为止,推荐系统没有使用任何其它与领域相关的知识。它没有使用一个用户替另一个用户打分的这一事实。与给书或者水果打分一样,也可以给用户的概括进行评分。但是为了推荐的效果更好,在推荐过程中还是需要考虑其它一些因素。
在以下部分,将介绍如何将一项重要的信息加入到推荐系统中,以提高推荐系统的性能。构建一个能够度量性别相似性的类ItemSimilarity。同时考虑如何不向用户推荐不合适的其它用户。
5.3.1 使用性别相似性的度量方法
因为每个人的性别是知道的,而每个人都有自己的描述信息,所以可以用性别的相似性表示每个人描述信息的相似性。每个人的描述信息可以看成是一系列的项目(items),所以使用ItemSimilarity计算描述信息之间的相似性。
例如,我们认为同性(男性或者女性)的相似性为1.0,所以其描述信息相似性也就为1.0。同理,认为异性之间的相似性为-1.0,也就说明异性的描述信息(profiles)的相似性为-1.0。如果在计算两人性别相似性时结果为0.0,表明其中的一个性别或者两个性别都未知的。
这种想法也许过于简单。它的计算速度也许很快,但它可能会忽略掉所有的与打分相关的信息。在下面的实验中采用基于项目的推荐算法(item-based),如下:
public class GenderItemSimilarity implements ItemSimilarity {
private final FastIDSet men;
private final FastIDSet women;
public GenderItemSimilarity(FastIDSet men, FastIDSet women) {
this.men = men;
this.women = women;
}
public double itemSimilarity(long profileID1, long profileID2) {
Boolean profile1IsMan = isMan(profileID1);
if (profile1IsMan == null) {
return 0.0;
}
Boolean profile2IsMan = isMan(profileID2);
if (profile2IsMan == null) {
return 0.0;
}
return profile1IsMan == profile2IsMan ? 1.0 : -1.0;
}
public double[] itemSimilarities(long itemID1, long[] itemID2s) {
double[] result = new double[itemID2s.length];
for (int i = 0; i < itemID2s.length; i++) {
result[i] = itemSimilarity(itemID1, itemID2s[i]);
}
return result;
}
...
private Boolean isMan(long profileID) {
if (men.contains(profileID)) {
return Boolean.TRUE;
}
if (women.contains(profileID)) {
return Boolean.FALSE;
}
return null;
}
public void refresh(Collection<Refreshable> alreadyRefreshed) {
// do nothing
}
}
可以在GenericItemBasedRecommender方法中使用ItemSimilarity度量方法(在此处实际为GenderItemSimilarity,其实现了ItemSimilarity),依然可以测试推荐系统的精度。这种计算方法很有趣,但是2.35的计算误差可能没有其它方法计算的结果好。如果在每个人的描述信息中包括了更多的个人爱好、兴趣等信息,就可以构造信息量更大的相似性计算方法,产生的结果也就会更好。
这个例子中介绍了基于项目(item-based)的推荐:它将有关项目自身的信息(这里指的是每个人的描述信息)加入到推荐系统中,在推荐问题中,这些项目信息都是已知的。从计算结果可以发现这种基于项目的相似性计算非常快,因为它的相似性非常容易计算,在作者的测试机上推荐的平均时间为15毫秒。
5.3.2 基于内容的推荐
上节介绍的推荐是基于内容的。因为在计算项目(item,这里指的是每个人的描述信息)相似性时没有用到用户的评分信息,而仅仅使用项目本身的信息,即每个人的描述信息。前面已经介绍了,Mathou没有实现基于内容的推荐,但Mahout提供了相关扩展功能和API,用这些扩展功能和API可以实现基于内容的推荐。
基于内容的推荐是对协同过滤(仅使用用户的评分)的有力补充。你可以将自己对项目的认知加入到推荐中,提高推荐精度。
不幸的是,上面计算项目相似性的方法具有很强的局限性。对于推荐食物、推荐电影、推荐旅游的推荐系统来说可能没有什么用处。这就是为什么没有把基于内容的推荐做成Mahout框架的一部分。但是Mahout框架的设计方式是可行和有效的,在任何时候,它都可以处理与特定领域内容相关的项目相似性。
5.3.3 用IDRescorer改善推荐
Recommender.recommend()方法最后一个参数是可选的,其类型为IDRescorer。recommend(long userID, int howMany, IDRescorer rescorer)(默认的调用形式为recommend(long userID, int howMany))。在Mahout的推荐相关的API中这个接口IDRescorer经常出现。该接口的功能是,按照一定的逻辑关系,可以将推荐引擎中的一些值变换为另一些值,它也可以过滤掉一些无用的值。比如,可以用IDRescorer任意改变推荐系统对项目(item)的估计值。也可以从用于推荐的数据中去掉某些项目(item,这里的项目指的是:如果每行数据表示一个人,列元素就表示人对项目的评分,这些项目就可以通过IDRescorer进行选择,去掉一些不想使用的项)。
假如你想通过电子商务网站,向某些用户推荐书。当前用户正在浏览推理小说,所以当向这个用户推荐书时,你可能希望所有的推理小说的评分都很高(这样的话才能向这个用户推荐更多的其它推理小说,这是我的理解)。你也肯定希望别把脱销书推荐给用户。IDRescorer可以做到这一点。下面的例子中展示了IDRescorer的实现过程,该实现封装了相应的逻辑过程,在该实现用还用到了一些预定义的类如Genre—来自于推理小说的销售商。
public class GenreRescorer implements IDRescorer
{
private final Genre currentGenre;
public GenreRescorer(Genre currentGenre)
{
this.currentGenre = currentGenre;
}
public double rescore(long itemID, double originalScore)
{
Book book = BookManager.lookupBook(itemID);
if (book.getGenre().equals(currentGenre))
{
return originalScore * 1.2;
}
return originalScore;
}
public boolean isFiltered(long itemID)
{
Book book = BookManager.lookupBook(itemID);
return book.isOutOfStock();
}
}
Rescore()方法使对推理小说的评分变高。isFiltered()方法演示了另一种使用IDRescorer方法的应用:它能够过滤掉脱销的图书。
这仅仅是个例子而与我们使用的交友网站的数据无关。让我们将这种想法应用到性别数据上。
5.3.4 在IDRescorer中使用性别信息
IDRescorer可以过滤无用项目(items)和用户的描述信息。通过猜测用户对性别的偏好信息,以及用户对性别偏好的历史信息,可以达到过滤异性的描述信息的目的,代码如下
public class GenderRescorer implements IDRescorer {
private final FastIDSet men;
private final FastIDSet women;
private final FastIDSet usersRateMoreMen;
private final FastIDSet usersRateLessMen;
private final boolean filterMen;
public GenderRescorer(FastIDSet men,
FastIDSet women,
FastIDSet usersRateMoreMen,
FastIDSet usersRateLessMen,
long userID, DataModel model)
throws TasteException {
this.men = men;
this.women = women;
this.usersRateMoreMen = usersRateMoreMen;
this.usersRateLessMen = usersRateLessMen;
this.filterMen = ratesMoreMen(userID, model);
}
public static FastIDSet[] parseMenWomen(File genderFile)
throws IOException {
FastIDSet men = new FastIDSet(50000);
FastIDSet women = new FastIDSet(50000);
for (String line : new FileLineIterable(genderFile)) {
int comma = line.indexOf(',');
char gender = line.charAt(comma + 1);
if (gender == 'U') {
continue;
}
long profileID = Long.parseLong(line.substring(0, comma));
if (gender == 'M') {
men.add(profileID);
} else {
women.add(profileID);
}
}
men.rehash();
women.rehash();
return new FastIDSet[] { men, women };
}
private boolean ratesMoreMen(long userID, DataModel model)
throws TasteException {
if (usersRateMoreMen.contains(userID)) {
return true;
}
if (usersRateLessMen.contains(userID)) {
return false;
}
PreferenceArray prefs = model.getPreferencesFromUser(userID);
int menCount = 0;
int womenCount = 0;
for (int i = 0; i < prefs.length(); i++) {
long profileID = prefs.get(i).getItemID();
if (men.contains(profileID)) {
menCount++;
} else if (women.contains(profileID)) {
womenCount++;
}
}
boolean ratesMoreMen = menCount > womenCount;
if (ratesMoreMen) {
usersRateMoreMen.add(userID);
} else {
usersRateLessMen.add(userID);
}
return ratesMoreMen;
}
public double rescore(long profileID, double originalScore) {
return isFiltered(profileID)
? Double.NaN : originalScore;
}
public boolean isFiltered(long profileID) {
return filterMen ? men.contains(profileID) : women.contains(profileID);
}
}
在以上代码中,paraMenWomen()方法解析gender.dat文件,并创建两个描述信息ID的集合,这些ID要么是男性的要么是女性的。处理过程需要通过GenderRescorer实例单独实现,因为需要反复使用数据集gender.dat。rateMoreMen()方法用来检测和记录是否对男性或者女性的描述信息进行了评价。这些结果信息都缓存在两个数据集中。通过rescore()返回NaN,isFiltered()返回true,达到使用GenderPescorer实例过滤男性和女性的目的。
这可能只有很小的影响,当对分类器的性能应该是有帮助的。按照假设,可以向对男性描述信息进行评分的女性推荐男性的描述,因为可能对男性进行打分的女性具有相似性。为了达到只向女性推荐男性的目的,可以在结果中将女性信息过滤掉。这就导致推荐器不会对被过滤的女性进行评分,因为这种评分猜测的成分相当的大,所以评分可能是错的。当然,IDRescorer的作用受到数据的限制,因为只知道一半男女信息。
5.3.5 打包订制的推荐系统
将IDRescorer加入到推荐引擎中,并进行打包。这可以让我们发布独立的推荐引擎。
以下代码实现了前面部分介绍的基于用户的推荐引擎。
public class LibimsetiRecommender implements Recommender {
private final Recommender delegate;
private final DataModel model;
private final FastIDSet men;
private final FastIDSet women;
private final FastIDSet usersRateMoreMen;
private final FastIDSet usersRateLessMen;
public LibimsetiRecommender() throws TasteException, IOException {
this(new FileDataModel(
readResourceToTempFile("ratings.dat"));
}
public LibimsetiRecommender(DataModel model)
throws TasteException, IOException {
UserSimilarity similarity =
new EuclideanDistanceSimilarity(model);
UserNeighborhood neighborhood =
new NearestNUserNeighborhood(2, similarity, model);
delegate =
new GenericUserBasedRecommender(model, neighborhood, similarity);
this.model = model;
FastIDSet[] menWomen = GenderRescorer.parseMenWomen(
readResourceToTempFile("gender.dat"));
men = menWomen[0];
women = menWomen[1];
usersRateMoreMen = new FastIDSet(50000);
usersRateLessMen = new FastIDSet(50000);}
public List<RecommendedItem> recommend(long userID, int howMany)
throws TasteException {
IDRescorer rescorer = new GenderRescorer(
men, women, userID, usersRateMoreMen, usersRateLessMen,
userID, model);
return delegate.recommend(userID, howMany, rescorer);
}
public List<RecommendedItem> recommend(long userID,
int howMany,
IDRescorer rescorer)
throws TasteException {
return delegate.recommend(userID, howMany, rescorer);
}
public float estimatePreference(long userID, long itemID)
throws TasteException {
IDRescorer rescorer = new GenderRescorer(
men, women, userID, usersRateMoreMen, usersRateLessMen,
userID, model);
return (float) rescorer.rescore(
itemID, delegate.estimatePreference(userID, itemID));
}
public void setPreference(long userID, long itemID, float value)
throws TasteException {
delegate.setPreference(userID, itemID, value);
}
public void removePreference(long userID, long itemID)
throws TasteException {
delegate.removePreference(userID, itemID);
}
public DataModel getDataModel() {
return delegate.getDataModel();
}
public void refresh(Collection<Refreshable> alreadyRefreshed) {
delegate.refresh(alreadyRefreshed);
}
}
这是一个完整、独立的推荐引擎包。其精度大约为1.18,实际上来说这个误差是不会改变的,这种机制的存在,可以避免一些严重的推荐错误。运行时间增加到500毫秒the rescoring has added significant overhead.
For our purposes here, this tradeoff is acceptable, and LibimsetiRecommender
is the final implementation for this dating site.
5.4 向匿名用户推荐信息
5.5 创建Web推荐引擎
5.5.1 打包WAR文件
5.5.2 测试已部署的系统
5.6 更新和监测推荐系统
现在有一个基于web的推荐服务正在运行,但是这个系统不是静态的、固定的系统。它是一个动态的服务,能够获取新的信息,并实时的给出推荐信息。这很自然的就会想到,如何更新服务,并对服务进行监测。
在实际的推荐引擎系统中,用于推荐的数据是一直在改变的。标准的DataModel实现将自动的从潜在的数据源中获取最新数据用于推荐,so, at a high level, there’s nothing special that needs to be done to cause the recommender engine to incorporate new data.例如,如果推荐引擎的数据是存储在数据库中的,可以使用JDBCDataModel类实现推荐引擎的自动更新,只要实时的更新底层数据库的数据表,推荐引擎就会自动的更新推荐信息。
推荐引擎的性能和组件缓存信息和实时计算有很大的关系。在更新推荐信息时,最终是需要更新缓存信息的,这就意味着新的数据信息无法实时的影响推荐信息。可以通过调用Recommender.refresh()强制清空缓存,而refresh方法的调用可以通过基于SOAP的接口实现,这个SOAP的接口可以通过web应用暴露出来。如果需要,SOAP的接口可以通过其它的软件框架实现。
通过FileDataModel可以访问基于文件的偏好数据信息,这里务必做些特殊的说明。可以更新或者覆盖偏好信息文件,达到更新推荐系统中推荐信息的目的。当偏好数据文件被更新后,FileDataModel会在很短时间内检测到偏好信息文件被更新,并重新加载此文件,获取最新的推荐信息。
加载偏好数据文件可能会很慢,对内存的操作的频率也会变大,因为新的数据模型和旧的数据模型将会同时驻留在内存中。现在是重新复习第3章中3.2.4节的好时候,因为其介绍了如何更新文件。不要直接替换或者更新主要的数据文件,而是将新增加的数据文件直接添加到原来主文件所在的文件系统中(同一个路径中)。新添加的文件就像是微小的增量,当将新增加的文件加入到主文件所在的路径中时,它将成为主文件的一部分,再加上合适的命名,这些新增加的文件将很快被检测到,并且很快的能够影响到内存中的原始数据模型,进而影响到推荐信息,达到更新推荐信息的目的。
例如,有一个应用程序,它通过定位在过去一小时或者更长时间内,所新建、删除或者更改过的文件,这样每过一个小时,应用程序就可以创建一个经过更新的偏好文件,并把更新的偏好文件复制到主文件所在的目录中。同时,如果从效率方面进行考虑,要对这些文件进行压缩处理。
尽管Mahout框架本身没有对推荐系统服务进行检测的模块,这并不能说明对推荐服务监测的模块是可有可无的。通过访问服务的URL及判断服务返回数据的有效性,任何一种能通过HTTP协议检测服务运行状况的工具,也都可以用来检测推荐服务的运行状况。同时还要求检测工具能够给出服务的响应时间,如果服务的访问性能突然下降了要能够给出警告。通常来说,推荐服务的响应时间是相当固定的,不可能有很大的改变。
6 Distributing recommendation computations
本章的内容包括:
分析从维基百科获取的大数据
介绍基于Hadoop和分布式算法的推荐系统
介绍伪发布模式的推荐系统
从这本书中可以看到,数据集的规模逐渐在增加:从10多个偏好数据到1000.000再到1千万最后到1.7千万的数据量。在世界级的推荐系统,千万级的数据量还是属于中等规模的。本章将再次处理1.3亿量级的数据集,该数据集来自维基百科的大型语料库,数据信息的格式是文章与文章的链接。在这个数据集中,文章由用户和一系列的项目(items)组成,与传统的基于上下文的推荐相比,其展示了在Mahout框架下如何有效的应用推荐系统。
虽然处理1.3亿的数据集并不难,但是如果采用目前所介绍的方法来处理的话还是有些困难。这就需要Mahout提供一个基于分布式计算的推荐算法,它能够工作在Hadoop上并能够以MapReduce的模式工作。
首先查看维基百科的数据集,了解其对分布式推荐计算的意义。你将会学习到一个简单的分布式推荐系统是如何设计的,因为其与非分布式系统相比会有很大的不同。在Mahout中你将会看到,这种分布式的设计思想是如何基于MapReduce和Hadoop实现的。最后你将会运行一个完整的基于Hadoop的推荐系统作业并查看最终结果。
6.1 分析维基百科的数据集
首先看下维基百科的数据集,会发现,因为规模很大,现在的数据集不像以前的那些数据集好处理。下面将对数据进行备份,并会看到分布式计算是如何处理高级问题的。
维基百科(http://wikipedia.org)是著名的在线百科全书,它的内容可以由用户进行编辑和维护。据统计,到2010年五月维基百科已经包括了320万的英文文章。维基百科游离盐基提取项目(http://download.freebase.com/wex/)约有42GB的英文文章。维基百科是基于网络的,它的一篇文章可以链接到其它文章上。对这些链接的研究会非常有趣。将一些文章看成用户(users),将被这些文章指向的其它文章看成项目(items)。
幸运的是,不需要下载整理好的游离盐基维基百科的数据,并对其进行链接抽取。研究人员Henry Haselgrove已经从中抽取了文章间的链接关系,这些链接信息发布在http://users.on.net/~henry/home/wikipedia.htm。其中包括诸如文章的讨论页面、图片等链接信息。在数据集中用数字而不是题目代表文章的ID,这种表示方法很有用,因为Mahout用ID表示所有用户和项目。
在继续进行之前,需要从Haselgrove的主页上下载并解压数据links-simple-sorted.zip。数据中包括了130.160.392个链接,这些链接连接了5.706.070篇文章并指向另外3.773.865篇文章的。注意,这里没有明显的偏好信息或者评分信息,有的只是文章之间的链接关系。但是,这些链接关系可以表示成Boolean型偏好信息。链接的关联关系是单向的,链接从A指向B并不能够表示B与A有关联关系。由于项目和用户的数量差不多,所以基于用户的推荐和基于项目的推荐的性能不会有太大的区别。如果使用的算法中需要进行相似性计算,不是基于偏好值的LogLikehoodSimilarity方法应该是不错的选择。
从直觉角度看,这些数据有什么意义,想从中获取什么样的推荐结果哪?一个链接从A指向B表示,B提供了与A有关的信息,即B为A提供了相关背景知识或者参考信息。基于维基百科链接的推荐系统将推荐一些被其它文章指向的文章,被推荐的文章也是A指向的文章。。。。。。。。。。。。。。
6.1.1 与大规模数据的斗争
可以证明,在大数据集上的非分布式的推荐系统是很难运行的。使用Mahout时光数据就要占用2GB的内存,应用程序运行时需要的整个内存空间可能要到2.5GB。在32位操作系统和JVM上,这将超过最大的内存访问量,这就意味着需要64位的操作系统和JVM。推荐系统的推荐时间也会超过1秒钟,对于一个能够支撑现代web应用的推荐引擎来说超过1秒的运算时间无法满足实时计算的要求。
如果有足够的硬件设备,推荐系统的性能也许尚可接受。但是当数据增加到10亿数量级的时候,对内存的需求量将增加到32GB。如果数据继续增加,结果又会如何哪?有时候,可以丢掉逐渐增加的噪声数据,达到降低数据规模的目的。但是如何判断噪声又会带来一些精度和数据规模的问题。
。。。。。。。。。。。。。。。。。。。
6.1.2 评价分布式计算的优缺点
处理大数据问题的解决方案是:用许多小的机器代替一个大的计算服务器,许多原因都表明用一个大的服务器处理大数据问题是不可行的。有些机构也许已经拥有许多小的计算集群,这些集群还没有完全被使用上,这些多余的计算资源可能用在推荐系统上,如图6.1所示。目前通过云计算可以获得多机器的计算资源。如亚马逊的EC2服务(http://aws.amazon.com)。
图6.1 分布式计算助手。将一个大的计算问题拆分成几个小的计算问题进行计算
分布式推荐计算彻底改变了推荐引擎问题
6.2 设计基于项目的推荐算法
对于本章中的数据规模,需要利用分布式推荐系统。首先,本节会介绍基于项目推荐系统的分布式变量。这和非分布式的方式有些类似。但是肯定会有差别,因为非分布式的推荐系统无法直接转化为分布式的推荐系统。
6.2.1 构建共生矩阵
利用简单的矩阵操作知识,构建共生矩阵的算法很好解释也很好实现。如果你只是在很久之前看过矩阵方面的知识,别担心,因为最复杂的矩阵操作莫过于矩阵相乘。这里并不需要行列式、行删除,或者特征值计算等复杂的矩阵运算。
回想一下,基于项目的推荐系统依赖于ItemSimilarity类,该类能够计算任意两个项目之间的相似度。计算任意两个项目之间的相似度,并把它放到一个大的矩阵中。这个大矩阵是个方阵,它的行数和列数等于链接数据中项目(item)的个数。没一行(或者一列)代表一个项目于其它所有项目的相似度。把这些行数据或者列数据看成是向量是很有用的。这个数据矩阵式对称的,因为项目X和项目Y的相似性等于项目Y与项目X的相似性,X行Y列对应的数据和Y行X列对应的数据是相同的。
【注意:这个矩阵描述了项目之间的关联关系,与用户无关。它不是用户-项目(user-item)矩阵。用户-项目矩阵是不对称的;它的行数和列数等于数据模型中用户的个数和项目的个数,一般是不相等的。】
这里的算法需要构造共生矩阵。在构造共生矩阵时,不需要计算项目与项目之间的相似性,只需要统计待计算的项目对同时出现在同一用户偏好列表中的次数。例如有9个用户,他们对X和Y进行评价,那么X和Y共出现了9次,那么共生矩阵中X和Y的值就是9。如果两个项目没有同时出现在同一个用户的评分列表中,那么它们共同出现的次数就为0。And, conceptually, each item cooccurs with itself every time any user expresses a preference for it, though this count won’t be useful。
共生的意思和相似性类似;两个项目出现在一起的次数越多,它们的相似性就越大。共生矩阵的角色和基于项目的非分布式系统中ItemSimilarity的计算方法相似。
产生共生矩阵很简单,就是进行数数。注意,共生矩阵中的相似值和用户的评分无关。稍后的计算中会用到共生矩阵中的值。
表6.1 由简单数据集构造的共生矩阵,第一行和第一列不属于矩阵的内容,就是个标记项
表6.1是第二章中的小数据构造的共生矩阵。
共生矩阵是对称的。有7个项目,所以共生矩阵是7×7方阵,对角线上的元素是无用的,但是为了完整性还是把他们包括在矩阵中了。
6.2.2 计算用户向量
将前面介绍的推荐器转化为基于矩阵的分布式计算系统,以此构造用户偏好向量。在讨论欧拉距离的时候,实际上已经讨论过这种转化了,将用户看成空间中的点,他们的距离就代表了他们的相似性。
类似的,在数据模型中有n个项目,用户的评分就像是一个n维的向量,每个项目都有一维。向量中的值是用户对项目的偏好评分。如果用户没有对某个项目进行评分则对应的元素就为0。这样的向量是稀疏的,大多数都为0,因为用户只会对很少的项目进行评分。
例如,在第二章的样例数据集中,用户3的偏好向量是[2.0, 0.0, 0.0, 4.0, 4.5, 0.0, 5.0]。为了进行推荐,每个用户都需要一个这样的向量。
6.2.3 产生推荐
为了给用户3推荐项目,需要将用户3的向量看成是列向量并和共生矩阵相乘即可,如表6.2。
如果矩阵相乘的知识不熟悉,可以查询相关书籍。共生矩阵和用户向量的乘积还是一个向量,它的数量等于数据中项目的个数。本例中的结果存储在R中,利用R中的信息就可以做推荐了,R中的最大值对应的项目索引就是最好的推荐结果。
表6.2 共生矩阵和用户向量相乘产生的向量R可以用于推荐
表6.2展示了为用户3所做的推荐过程,结果向量为R。可以忽略R中的101,104,105,和107,因为这几项是用户3已经评价过的项目,就不需要推荐了。剩下的项目中103的得分是最高的,为24.5,自然应该作为首先推荐的项目,其次是102和106。
6.2.4 对结果的理解
让我们来看看上一小节中发生了什么。为什么R中的最大值对应的项目可以作为推荐项。R中的每一项类似于项目的偏好估计值,但是为什么可以将每个值类比为偏好值?
回顾下以上的计算过程,例如R中的第三个元素是用共生矩阵中的第三行的数据和列向量U3相乘的结果。将相对应的元素相乘并累加得:
4(2.0) + 3(0.0) + 4(0.0) + 3(4.0) + 1(4.5) + 2(0.0) + 0(5.0) = 24.5
共生矩阵中的第三行的数据表示和103同时出现的其它项的次数。从直观角度看,如果用户对于103同时出现的其它项目有偏好,那么某种程度上其对103也有偏好。上面的计算方法是将共生矩阵和用户的偏好值相乘并累加。如果和103同时出现的其它项目评分较高时,最终的求和结果中也会包括出现次数较高的项和高评分项的乘积。这就导致最终的求和结果变大,这个求和所得到的结果也是R中的元素。这就是为什么挑选R中的最大元素所对应的项目作为最有的推荐结果。
注意,R中的值并不代表对项目偏好的估计值,它们的值太大了。如果需要可以对R中的值进行归一化处理,作为对每个项目的偏好值。但是在此处,不需要对其进行归一化处理,因为最终的推荐顺序是重点,至于每个项目的确切评分是不重要的。
6.2.5 向分布计算进发
分布式计算是非常有趣的,但是,这种算法是不是适合大规模分布式计算哪。
任何时候,算法的每个计算单元仅仅会处理一部分数据。例如,创建用户向量的时候,仅仅需要收集一个用户所评过分的所有项目的偏好值,用这些偏好值组成。计算共生矩阵时,每次只需要处理一个向量。计算最后的推荐结果R时,每次只需要加载每行或者没列矩阵。Further, many elements of the computation just rely on collecting related data into one place efficiently—for example, creating user vectors from all the individual preference values.
MapReduce的运行模式就是为处理以上问题所设计的。
6.3 用MapReduce实现一个分布式算法
现在可以使用MapReduce编程模式和Apache Hadoop将算法转化为分布式运行方式。Hadoop是一个很流行的分布式计算框架,它包括了两部分:Hadoop分布式文件系统和MapReduce模式的编程实现。
下面将会一步一步的介绍几个MapReduce阶段,最后组成一个管道,达到推荐的目的。每个MapReduce都做相对独立的工作。下面将会介绍输入、输出和每个MapReduce的功能。请做好学习的准备,因为下面介绍的内容可能比较多,即使这个简单的推荐算法也由5个MapReduce组成,这是在Mahout中最简单的一个算法形式。本节结束时,你将会看到一个点对点的基于Hadoop的分布式推荐系统。
注意,本章将使用0.20.2版本的Hadoop的API。在Mahout中可以找到其完整的代码,所以运行环境要求是Hadoop0.20.2或者更新的版本0.20.x。更详细的代码可以参考org.apache.mahout.cf.taste.hadoop.item.RecommenderJob,它包括了下面步骤的所有实现。
6.3.1 MapReduce简介
MapReduce是一种计算模式,它以一种统一的方式使在多台机器上的分布式计算变得简单易行。MapReduce的计算流程如下:
1 输入数据应该是key-value(K1,V1)形式,存储在HDFS文件系统上。
2 每个map函数处理一个(K1,V1),这就会产生许多不同的键-值对(K2,V2)。
3 K2所对应的V2被合并到一起。
4 对于每个K2都有一个reduce函数与之对应,并且该函数能够处理V2的值,最终的reduce输出不同的键-值对(K3,V3),并将其写到HDFS文件系统中。
这种计算模式听起来有点古怪,但是许多问题都可以抽象成这种计算模式,或者一系列的计算链。以这种MapReduce计算框架对问题进行分解,将会使基于Hadoop和HDFS分布式计算变得相当高效。
更多介绍Hadoop应用的见http://hadoop.apache.org/common/docs/r0.20.2/mapred_tutorial.html,对应的版本为0.20.2。
6.3.2 用MapReduce生成用户向量
在本例中,首先从链接数据文件开始计算。链接数据文件的每一行不是userID,itemID,preference这样的数据格式。而是userID:itermID1,itemID2,itemID3……。链接数据文件需要上传到HDFS文件系统上,这样Hadoop才能使用链接数据文件,更多的内容下面会介绍。
第一个MapReduce的功能是构造用户矩阵:
将输入的文件看成(Long,String)类型的键-值对,Long表示文件中的位置,String表示文件中的每一行数据。例如,239(健)/98955:590 22 9059(值)。
通过map函数,将每一行解析成一个用户ID和几个项目的ID。同时map函数将用户ID和每个项目的ID组合输出新的键-值对。例如98955(用户ID) / 590(项目ID)。
Hadoop框架会收集所有的项目ID,把这些项目ID映射到同一个用户ID上。
reduce函数会构建一个向量,用于保存每个用户与所有项目ID的关系,如果对每个项目评价,则对应的ID为1.0,否则为0.0。例如:98955 / [590:1.0, 22:1.0, 9059:1.0]
为了实现以上的流程,下面的两段代码用Hadoop的MapReduce实现了Mapper和Reducer接口。这是MapReduce的典型计算模式。为了实现以上介绍的内容,只需要完成这两个函数,其它的工作交给Hadoop的框架完成。
map函数的实现:
public class WikipediaToItemPrefsMapper extends Mapper<LongWritable,Text,VarLongWritable,VarLongWritable>
{
private static final Pattern NUMBERS = Pattern.compile("(\\d+)");
public void map(LongWritable key,Text value,Context context)throws IOException, InterruptedException
{
String line = value.toString();
Matcher m = NUMBERS.matcher(line);
m.find();
VarLongWritable userID =
new VarLongWritable(Long.parseLong(m.group()));
VarLongWritable itemID = new VarLongWritable();
while (m.find())
{
itemID.set(Long.parseLong(m.group()));
context.write(userID, itemID);
}
}
}
reduce函数的实现:
public class WikipediaToUserVectorReducer extends
Reducer<VarLongWritable,VarLongWritable,VarLongWritable,VectorWritable>
{
public void reduce(VarLongWritable userID,Iterable<VarLongWritable> itemPrefs,Context context)throws IOException, InterruptedException
{
Vector userVector = new RandomAccessSparseVector(
Integer.MAX_VALUE, 100);
for (VarLongWritable itemPref : itemPrefs)
{
userVector.set((int)itemPref.get(), 1.0f);
}
context.write(userID, new VectorWritable(userVector));
}
}
以上两段小的例子,只是Mahout中最简单的实现版本。其中没有包括优化和配置信息项,但是其可以运行,并产生可以使用的结果。
6.3.3 用MapReduce计算共生矩阵
下面介绍,依然使用MapReduce模式,利用6.3.2的结果计算共生矩阵。
输入的数据格式是6.3.2中reduce的输出结果。例如:98955(用户ID) / [590:1.0,22:1.0,9059:1.0](项目ID列表)。
map函数检测每个用户中所出现的项目ID,并构造键-值对(项目ID,项目ID)作为输出。例如590(项目ID)/22(项目ID)。
Hadoop框架收集与每个项目同时出现的其它项目。
reduce函数计算与每个项目ID同时出现的其它项目ID的次数,并构建新的向量,用来表示一个项目ID与其同时出现的其它项目ID的次数。这些向量可以作为共生矩阵的一行或者一列。例如:
590 / [22:3.0,95:1.0,...,9059:1.0,...]
计算完成后,产生的结果就是共生矩阵。下面的Mapper和Reduce代码,实现了以上的想法。
map函数:
public class UserVectorToCooccurrenceMapper extends
Mapper<VarLongWritable,VectorWritable,IntWritable,IntWritable>
{
public void map(VarLongWritable userID,VectorWritable userVector,Context context)
throws IOException, InterruptedException
{
Iterator<Vector.Element> it =
userVector.get().iterateNonZero();
while (it.hasNext()) {
int index1 = it.next().index();
Iterator<Vector.Element> it2 =
userVector.get().iterateNonZero();
while (it2.hasNext())
{
int index2 = it2.next().index();
context.write(new IntWritable(index1),
new IntWritable(index2));
}
}
}
reduce函数:
public class UserVectorToCooccurrenceReducer extends
Reducer<IntWritable,IntWritable,IntWritable,VectorWritable>
{
public void reduce(IntWritable itemIndex1,
Iterable<IntWritable> itemIndex2s,
Context context)
throws IOException, InterruptedException
{
Vector cooccurrenceRow =
new RandomAccessSparseVector(Integer.MAX_VALUE, 100);
for (IntWritable intWritable : itemIndex2s)
{
int itemIndex2 = intWritable.get();
cooccurrenceRow.set(
itemIndex2,
cooccurrenceRow.get(itemIndex2) + 1.0);
}
context.write(itemIndex1,new VectorWritable(cooccurrenceRow));
}
}
6.3.4 用MapReduce实现矩阵相乘
现在使用MapReduce将6.3.2中产生的用户向量和6.3.3中产生的共生矩阵相乘,得到推荐向量,用推荐向量就可以进行推荐了。
矩阵相乘有不同的实现方法,这里介绍适合MapReduce运行模式的高效计算方法。这里介绍的方法不同于传统的矩阵计算方法,传统的矩阵计算方法用共生矩阵的每行乘用户向量(列向量),结果作为R中的一个元素,计算过程如下:
for each row i in the co-occurrence matrix
compute dot product of row vector i with the user vector
assign dot product to ith element of R
为什么不使用我们在学校中学过的矩阵相乘的计算方法?原因就是:性能,这也是一个很好的机会,让我们重新考虑在设计大矩阵和向量相乘时,如何获得高性能。传统的方法需要访问整个共生矩阵,因为需要用共生矩阵的每行与用户矩阵相乘。任何访问整个矩阵的做法都是不好的,因为输入可能是相当大的数据甚至连它的局部数据都无法获得。注意到矩阵相乘可以是共生矩阵中列向量的函数
assign R to be the zero vector
for each column i in the co-occurrence matrix
multiply column vector i by the ith element of the user vector
add this vector to R
也许可以花点时间说服你自己,对于一个小的数据来说这依然是一个正确的矩阵相乘的方法。到目前为止,依然没有改善计算的性能,因为依然要访问整个共生矩阵,只不过是按照列的方式进行的。
如果用户向量中,第i个元素为0,在循环计算的过程中研究可以跳过0元素,因为0元素与其它项相乘依然为0,不会对结果产生影响。所以每次循环的时候只需要计算非零元素。按照以上的分析,需要加载的共生矩阵的列数等于用户向量中非零元素的个数,当用户向量的数据是稀疏的时候,需要加载的共生矩阵的列数应该是个很小的值。
按照以上的讨论过程,矩阵计算可以实现高效的分布式计算。Column vector i can be output along with all the elements it needs to be multiplied against. The products can be computed and saved independently of the handling of
all other column vectors.
6.3.5 用MapReduce实现矩阵的部分相乘
从6.3.3中可以获得共生矩阵。共生矩阵是对称的,行和列对应相等
6.3.6 用MapReduce实现推荐
6.4 在Hadoop上运行MapReduce
6.5 伪分布推荐系统
6.6 Looking beyond first steps with recommendations
6.7 总结
Part3 Classification
13 Introduction to classification
14 Train a classifier
这一章节包括:
从文本中抽取特征
将特征转化为Mahout可以使用的形式
向量两个Mahout的分类器
如何从Mahout的学习算法中选择合适的算法
这一章介绍分类器使用中的第一个步骤:训练模型。开发一个分类器是一个动态的过程,你需要思考,如何找出一种方法来描述数据中的特征,如何将这些特征训练你的分类器。有些数据的格式本身就是为分类器准备的(不需要太多的处理特征),而有些数据并不是单独为分类器准备的,对这些数据的特征抽取就会有很大的挑战性,这中挑战性的过程也许会给你很多回报,也许会让你感到气馁,也许会让你对这些数据更加感兴趣。
在这章中你将会学到,如何有效的抽取特征来创建一个Mahout分类器。与13中你所看到的简介步骤相比,特征抽取包括了更多的复杂工作。在这里,我们将详细的介绍特征抽取过程,包括:如何处理生数据使其成为可分类的数据,如何将可分类的数据转换为Mahout分类算法可以使用的向量数据。我们将用产品销售的计算问题作为例子,讲解如何训练从数据库中获取的数据。
如果你已经明白了如何为分类算法准备数据,待到14.4中时,你将会使用标准数据集-20 newsgroups来构建Mahout中的SGD(stochastic gradient descent)分类器。
在14.5中,我们将会看到Mahout中各种学习算法的特征,如何选择一种算法使其符合一个特殊的项目。在设计和训练分类器时,抽取特征和选择算法是至关重要的两个步骤,并且能够让你明白如何设置算法的参数,我们将会看到用不同算法对20 newsgroups数据集的处理效果。
这一章开始的时候,将会介绍如何将数据用在训练的例子中。
14.1 抽取特征以构建Mahout分类器
将生数据处理成分类器可用的数据格式是很复杂的,也很费时。在这部分我们会看到数据处理的概况。在13章的图13.2中,你已经看到了训练和使用分类器模型的简图;在那张图中只是展示了从训练的示例数据到分类模型训练的简单过程。但是,在现实中,实际情况是非常复杂的,在13章中的没有介绍的重要细节,将会在这里介绍,见图14.1。
图13.2中展示从训练的示例数据到分类模型训练的简单过程。但在现实中,首先要收集生数据,接着要将其处理成可分类哦分类数据。图14.1中的例子就是可分类的数据—在这章中我们将预处理后的生数据转化成可分类的数据,更多细节将会在15和16章中介绍。
一旦生数据被处理成可分类的数据格式,就需要从这些处理好的数据中选取预测变量和目标变量,并把预测变量和目标变量编码成Mahout分类器所要求的向量形式。从13章中得知可以用作预测变量的特征的值的形式有如下四种:
连续性
分类型
词类型
文本类型
图14.1 该图详解了图13.2中部分信息。这张图描述了在输入训练器之前都要对生数据做哪些处理
图14.2 分类算法的输入格式是向量形式。为了将数据编码成向量形式,生数据必须以单条记录的形式存储成可分类形式,然后进行分词处理和向量化
总体来说,图14.2中的描述是正确的,但是如果要求训练数据能够训练其它任何分类算法的话,这些数据必须要以向量的格式,存储在内存中或者磁盘上。
对比图14.1中和图14.2中的描述。可以发现,原始数据经过了一些改变,变成了向量形式,以满足训练算法输入的需要。在两个地方数据发生了改变:1)预处理阶段,将原始数据处理成可分类数据;2)将可分类数据转化为向量格式。
如图14.2所示,对训练算法预处理数据主要包括以下两个步骤:
1、预处理生数据,将生数据预处理成一些列记录的形式,这些记录与特定的域相关。这些域可以是:连续型、分类型、词类型、文本类型。
2、将预处理完的数据转化成向量,使用一些工具,如Lucene的分词器和Mahout的向量化编码器对可分类数据进行解析。有些Mahout的分类器也包括向量化的代码。
以上的第二步中包括两个步骤,分词和向量化。对于连续变量,解析(分词)这一步可能不太重要,所以在处理时也就不需要考虑在内,对于其它三种变量类型,向量化是必须要做的。
在本章的以下部分,将详细讨论预处理分类器输入数据的两个步骤。
14.2 将生数据转化为可分类数据
在特征抽取的第一阶段,要仔细研究数据,并从中找出特定的特征作为分类的预测变量。按照分类目标,首先选择一个目标变量,然后通过挑选或删除特征来查看特征对分类目标的影响。在特征选择方面没有什么好的方法,其主要依靠以往的经验对特征做出明智的选择。当你完成本章学习的时候,你将会获得如何选择特征的经验。
这一部分将会给出如何预处理数据的梗概。其主要包括收集数据或者重新组织数据使其成为一条单独的记录,并从生数据中收集一些见解信息(比如,将ZIP代码转化为三位数代码,将生日转化为确定的年龄)。在这一章的样例中预处理并不是非常重要的部分。因为这章中所使用到的数据已经经过了预处理。在16和17章中数据的预处理将是很重要的部分。
14.2.1 转化生数据
一旦你选定了将要使用的特征,你必须要将其转化为一种可分类的形式。这就需要将数据转化为一个单一位置,并把单一位置转化为合适的不变形式(This involves rearranging the data into a single location and transforming it into an appropriate and consistent form)
注意:可分类数据由特定域的记录组成,域的类型包括:连续型,类别型,类词型,类文本型。每条记录都包含一个完全非规范化的训练实例描述。
乍一看,生数据转化这一步已经完成了:如果数据看起来是类文本类型的,那么这些数据是不是一定就由词组成的?如果数据看起像是数值类型的,那么这些数据是不是一定就是连续的?。但是就像你在13章中看到的,第一印象可能会给你带来误导。比如ZIP代码看起来像是数值类型的,但实际上其是类别型的,这种类别型数据可以作为预测分类的标记。如果数据包括很多词语那么这种数据就可能是类词类型的数据,或者也可以将这种数据认为是类别型或者文本型的。用户或产品的ID代号,看起来像数字型,类别型或者类词型的数据,but more commonly, they should be denormalized away in favor of the characteristics of the user or product that they link to.
下面营销计算的例子,可以作为对生数据预处理的一次练习。
14.2.2 营销计算的例子
假如你想创建一个分类模型,这个模型可以用来决定用户是否会买商家的某些产品。这不是一个推荐系统,因为其将使用用户和产品的特征来对样本进行分类,而不是使用各个用户行为的相似性来对样本进行分类。
这个例子中,信息存储在数据库中的几张表中,如图14.3所示。该图是高度简化的零售系统数据库。其中,一张表记录了用户信息,一张表记录了商品信息,一张表记录了商品上市信息(何时上市或者何时用户可以采购到该商品),一张表记录了采购信息。这种数据库中的数据无法作为训练数据或者从事数据,因为其横跨了好几张表。
图14.3 以上的表结构包括了不同的数据类型,因为在任何一张表中都不可能包括训练分类器的所有记录,本图的生数据组织形式无法直接作为分类器的训练数据
在图14.3中描述的营销数据信息包括了多种数据类型。User表中记录了用户生日和性别,Product表中记录了商品的类型和颜色,Offers表中记录了商品供应商信息,在Purchase表中记录了购买信息。
图14.2中展示了这些数据如何变成分类器可以使用的数据。可能需要一条记录来表示Offer表中的所有记录,但是如何将用户ID和商品ID与User表和Product表结合起来哪?在处理过程中,将用户生日要转化为年龄。使用外部节点表示供应和购买之间的延迟,同时添加一个标志信息,表示购买行为是否已经发生。
为了将数据库表中的数据转化为图14.4的形式,需要将数据库中各个表的数据重新组织。为了达到这样的目的,需要使用如下的SQL语句,如下:
select
now()-birthDate as age, gender,
typeId, colorId, price, discount, offerTime,
ifnull(purchase.time, 0, purchase.time - offer.time) as purchaseDelay,
ifnull(purchase.time, 0, 1) as purchased
from
offer
join user using (userId)
join product using (productId)
left outer join purchase using (offerId);
使用以上SQL语句就可以将数据库中的各个表中的数据组合成一条包含所有必要信息的记录。Offer表是这里的基础表(最先被访问的表),userID,productID和offerID的外键特性,要求要有一条确切的记录与offer表中的每条记录想对应。Note how the join against the purchase table is an outer join. This allows the ifnull expressions involving purchase.time to produce 0s if there is never a purchase.
图14.4 为了得到分类数据,训练样例中的多种数据被重新组合成一条记录,有些变了被变换成另一种形式,如,生日被表示为年龄,采购时间被表示成延时。
注意:有时候年龄用作分类特征比较好,有时生日用作分类特征比较好。就汽车保险数据来说用年龄作为特征可能是更好的选择,因为与一个人所属于的时代相比,人的年龄段与汽车事故的发生有更大的关系。另一方面,当人们购买音乐时,生日可能是更令人关注的特征,因为随着年龄的增长,人们常常会保持早期的音乐偏爱。人们对音乐的偏爱常常与他们所在的时代相关。
由查询语句产生的记录已经是可分类数据的形式了,下面就可以对其分析和向量化了。下一步可以使用你自己编写的代码进行向量化,或者将数据转换成Mahout分类器可以使用的数据格式供Mahout使用(Mahout包括了它自己的解析和向量化代码)。下面章节和样例中主要介绍向量化过程。