Mahout in action-3.推荐器的数据表达-3.3~3.4
3.3 处理偏好值为空的数据(布尔偏好)
有时推荐引擎中出现偏好值为空的记录。它代表了用户和项目是关联的,但是并没有表现出关联程度。举了例子,一个新闻网站根据用户已阅读内容为用户推荐新闻。“已阅读”使一个用户和一个项目产生了关联,然而这是唯一能够获取的信息。一般网站也不会让用户去给文章做个排序,更不会让用户再做除了阅读之外的其他什么事了。所以我们仅仅知道用户和那些文章关联了,而再也没有其他的内容了。
面对这样的情形,我们别无选择。这里不会有偏好值。后续几章将会依然提供处理如此情形的技术和建议。然而有时我们忽略掉偏好值也未尝不是坏事,只要情形需要。
丢掉用户和项目之间的联系很容易,或者说我们可以直接忽略偏好值就可以办到。比如,你宁可考虑有没有看过一个电影,也不愿去给一个电影打分。换句话说,我们宁愿把数据改为“用户1和项目3有关系”,也不愿意写成“用户1喜欢项目103的程度为4.5”。下面是一个示意图,来说明二者之间的区别:
图3.4 带偏好值的数据(左)与布尔偏好数据(有)的区别
用Mahout的语言,没有不带偏好值的记录就是布尔偏好,因为一个关联只有两种值:要么有关联,要么没关联。这不代表偏好只有两个值:是和否,而是有三个:喜欢、不喜欢、不知道。
3.3.1 什么时候应该忽略偏好值
为何有时会忽略偏好值?因为会有这样的情形,用户喜欢或者不喜欢一个项目的程度相对一致,至少与那些和用户没有关联的项目相比。还记得那个例子吗?一个家伙不喜欢拉赫玛尼诺夫(Rachmaninoff),当然世上有很多曲子他没有听过(比如死亡金属音乐(Norwegian death metal))。这样我们只得到了他与这个作家的一个关联,恰巧他又喜欢另一个作家布拉姆斯,然后他把拉赫玛尼诺夫标为1,把布拉姆斯标为5,和其余未知作家相比,这样的偏好值是没有什么意义的,以为二者在某种程度上是对等的。所以我们索性不要这个偏好值,仅仅是喜欢、不喜欢、不知道或者说未知的关系。
你可能会说这都是用户的错。难道他就不能为拉赫玛尼诺夫打4分而为死亡金属打1分。也许可以,但你还是认了吧!因为事实上推荐所需要输入的数据是很难确定的。你或许还要提出反对,虽然对于所有流派都可以这样去推理,但是当你要向他推荐古典作曲家时该依据哪些数据呢?没错,在某领域中的好的解决方案一般不会公开出来。
3.3.2 布尔偏好在内存中的数据表达
抛去偏好值可以很大程度上化简数据表达,当然也会提升性能、占用更少的内存。像看我之前了解的,在Mahout中需要用4个字节的浮点数去存储偏好值,至少如果没有偏好值也就意味着我们每一条会少用4个字节。在内存方面的测试中也确实平均每条记录都减少了4个字节到24字节了。
这个测试用的是GenericDataModel的双胞胎兄弟GenericBooleanPrefDataModel。它看上去是一个内存版的实现,但是它没有偏好值。事实上,它仅仅用FastIDSets去存储一个用户和项目的关联。例如,只为每个用户存储和他关联的所有项目ID,这里不会再出现偏好值。
因为GenericBooleanPrefDataModel也是DataModel对象,所以它完全可以直接代替GenericDataModel。一些数据模型的方法在这种实现下会变得快速高效,比如:getItemIDsForUser(),因为该方法在它里面取项目ID是现成的。某些方法也会变慢,比如:getPreferencesFromUser(),因为它的实现没有使用PreferenceArrays,而且必须实现以下该方法。
你可能会好奇getPreferenceValue()在这里可以返回什么?因为这里不是没有偏好值吗?你调用该方法并不会抛出UnsupportedOperationException的异常,它会恒定的返回一个虚假值1.0。这一点值得注意,每个组件都会依赖于一个偏好值,它们必须从DataModel对象中获取一个。这些伪造而且固定的值也会产生一些微妙的问题。
让我们在文章最后来观察一下GroupLens例子的结果。下面是使用了GenericBooleanPrefDataModel的代码片段:
清单 3.7 用布尔偏好值创建和评估一个推荐器
DataModel model = new GenericBooleanPrefDataModel(
GenericBooleanPrefDataModel.toDataMap(
new FileDataModel(new File("ua.base")))); A
RecommenderEvaluator evaluator =
new AverageAbsoluteDifferenceRecommenderEvaluator();
RecommenderBuilder recommenderBuilder = new RecommenderBuilder() {
public Recommender buildRecommender(DataModel model)
throws TasteException {
UserSimilarity similarity = new PearsonCorrelationSimilarity(model);
UserNeighborhood neighborhood =
new NearestNUserNeighborhood(10, similarity, model);
return
new GenericUserBasedRecommender(model, neighborhood, similarity);
}
};
DataModelBuilder modelBuilder = new DataModelBuilder() {
public DataModel buildDataModel(
FastByIDMap<PreferenceArray> trainingData) {
return new GenericBooleanPrefDataModel(
GenericBooleanPrefDataModel.toDataMap(trainingData)); B
}
};
double score = evaluator.evaluate(
recommenderBuilder, modelBuilder, model, 0.9, 1.0);
System.out.println(score);
A 基于相同的数据,使用GenericBooleanPrefDataModel
B 建立一个GenericBooleanPrefDataModel
这里的转折是DataModelBuilder。它可以使评估程序为训练数据构造DataModel对象,而不是去构造一个GenericDataModel对象。GenericBooleanPrefDataModel用一种稍微不同的方式去获取数据——用一些FastIDSets而不是PreferenceArrays。而且它还提供了一个很方便的方法toDataMap()用来对二者进行转换。在进入下一小节时,尝试运行一下这个程序,他将不会很成功的执行完毕。
3.3.3 选择匹配的实现
你会发现在PearsonCorrelationSimilarity的构造方法中抛出了IllegalArgumentException。刚开始你可能觉得很不可思议,GenericBooleanPrefDataModel难道不也是DataModel对象吗?它与GenericDataModel的唯一区别不就是没有偏好值吗?
这个相似度度量对象EuclideanDistanceSimilarity拒绝在没有偏好值的情况下工作,因为这样产生的结果是无意义的。皮尔森相关系数(Pearson correlation)在两个数据集数据完全重复、一样时就是没有意义的。在这里DataModel的偏好值全部为1.0,那么计算出来的欧几里得距离只能在一个点空间(1.0, 1.0, …, 1.0)中进行,这样是毫无意义的,因为所有的相似度都是0。
大体上讲,不是所有的实现都可以在一起很好的匹配在一起,甚至都实现自一个接口的对象也不可能完全匹配。为了解决这个当前问题,我们需要替换掉这个计算相似度的类。LogLikelihoodSimilarity就是一个好的选择,因为它不需要偏好值,我们将马上讨论它。在程序中用它替换PearsonCorrelationSimilarity,这个结果就变成了0.0。很不错吧!这意味着它的结果正确了,不过这真的很好吗?
不幸的是,所有的偏好值都是1当然会导致所有的偏好相差为0了。这个测试本身没有什么意义,因为它将永远都是0。
然而,查准-召回评估还是依然有效的,让我们来试一下吧:
清单3.8 使用布尔数据评估查准率和召回率
DataModel model = new GenericBooleanPrefDataModel(
new FileDataModel(new File("ua.base")));
RecommenderIRStatsEvaluator evaluator =
new GenericRecommenderIRStatsEvaluator();
RecommenderBuilder recommenderBuilder = new RecommenderBuilder() {
@Override
public Recommender buildRecommender(DataModel model) {
UserSimilarity similarity = new LogLikelihoodSimilarity(model);
UserNeighborhood neighborhood =
new NearestNUserNeighborhood(10, similarity, model);
return new GenericBooleanPrefUserBasedRecommender( model, neighborhood, similarity);
}
};
DataModelBuilder modelBuilder = new DataModelBuilder() {
@Override
public DataModel buildDataModel(FastByIDMap<PreferenceArray> trainingData) {
return new GenericBooleanPrefDataModel(
GenericBooleanPrefDataModel.toDataMap(trainingData));
}
};
IRStatistics stats = evaluator.evaluate(
recommenderBuilder, modelBuilder, model, null, 10,
GenericRecommenderIRStatsEvaluator.CHOOSE_THRESHOLD, 1.0);
System.out.println(stats.getPrecision());
System.out.println(stats.getRecall());
评估结果为查准率和召回率均为15.5%,这个结果并不理想。这意味着推荐结果中6个只有1个是好的推荐,6个好的推荐中只推荐出了1个。
这引出了第三个问题,偏好值依然潜伏于GenericUserBasedRecommender对象之中。由于推荐结果按照偏好值排序,而偏好值全是1.0,所以这样的次序是完全随机的。所以我们引出GenericBooleanPrefUserBasedRecommender(是的,正如它名字一样,它可以办到)。它可以在推荐结果中产生一个很有意义的排序。它为每个项目关联于其他相似的用户加权重,用户越相似,那么权重也会越高。它没有提供一个加权平均数。替换之后在运行代码,结果大概在18%,好了一点,但没有好太多。这种结果预示着推荐系统对于这些数据不是非常有效,我们的目的不是要去修改它,仅仅是去看如何在Mahout中有效的部署这样带有“布尔偏好”的数据。
其他DataModel对象的布尔变种也是有的。FileDataModel在记录中没有偏好值的情况下内部会自动的使用一个GenericBooleanPrefDataModel的对象。类似的,MySQLBooleanPrefDataModel适合用在数据库中无偏好值的情形。它与之前的对象完全相似,而且它的实现可以充分的利用数据库提供的某些捷径来提升自身的性能。
最后,你如果好奇布尔型和非布尔型是不是可以混在一起处理,答案是否定的。在这种情形下,我们更倾向于按照存在偏好值的情形去处理,因为毕竟有些偏好值是存在的。那些没有偏好值的记录可能或者应该可以通过某些手段推测出它的偏好值,甚至我们可以吧平均偏好值填进去当做一个占位符。
3.4 总结
这一章我们看到了数据在Mahout的推荐器中如何去表达、呈现。这里包括了Preference对象,酷似集合实现的特殊数组PreferenceArray以及FastByIDMap。这些特定定制的对象很大程度上减少了内存的消耗。
再看看DataModel,它是推荐器输入数据的一种抽象封装。在FileDataModel读取完本地文件之后,GenericDataModel在内存中存储这些数据。JDBCDataModel用来访问数据库的数据,而且我们还在MySQL上特意进行了测试。
最后我们讨论了在无偏好值的情况下,这些模型该如何的变化。一般上述这些对象都会用到,当然存储上会稍微少一些。我们列举了一些标准对象与该类数据不匹配的例子,如PearsonCorrelationSimilarity。最后通过检查问题的原因所在而解决了问题,目的是为了构建一个基于布尔偏好数据的推荐器。