Mahout推荐系统引擎RecommenderEvaluator源码解析

http://blog.csdn.net/jianjian1992/article/details/47304337里边有关于推荐系统的完整代码,其中有一个评价器RecommenderEvaluator用来评价推荐器的好坏。

RecommenderEvaluator evalutor = new AverageAbsoluteDifferenceRecommenderEvaluator();
System.out.println("eval:"+ evalutor.evaluate(recommenderBuilder, null, model, 0.5, 1));

在源码中对它定义的evaluate函数介绍如下:

* Evaluates the quality of a {@link org.apache.mahout.cf.taste.recommender.Recommender}‘s recommendations.
   * The range of values that may be returned depends on the implementation, but <em>lower</em> values must
   * mean better recommendations, with 0 being the lowest / best possible evaluation, meaning a perfect match.
   * This method does not accept a {@link org.apache.mahout.cf.taste.recommender.Recommender} directly, but
   * rather a {@link RecommenderBuilder} which can build the
   * {@link org.apache.mahout.cf.taste.recommender.Recommender} to test on top of a given {@link DataModel}.

    它是用来评价一个推荐器的质量的;
    它返回的值越小则代表推荐器推荐效果越好;
    评价方法并不直接接受一个推荐器作为参数,而是接受一个可以创建推荐器的推荐器创建方法来在给定的数据模型上进行测试;
* Implementations will take a certain percentage of the preferences supplied by the given {@link DataModel}
   * as "training data". This is typically most of the data, like 90%. This data is used to produce
   * recommendations, and the rest of the data is compared against estimated preference values to see how much
   * the {@link org.apache.mahout.cf.taste.recommender.Recommender}‘s predicted preferences match the user‘s
   * real preferences. Specifically, for each user, this percentage of the user‘s ratings are used to produce
   * recommendations, and for each user, the remaining preferences are compared against the user‘s real
   * preferences.

   它会从数据模型中选取一定比例的user对items的评价项作为训练数据;
   一般来说训练数据占所有数据的绝大部分,比如90%;
   至于剩下的数据则是用来做测试,通过比较预测值与实际值之间的差别来衡量推荐器的好坏;
   特别地,对于每个用户,这个不是很清楚,等下看源码就能弄明白是怎么回事了。
 For large datasets, it may be desirable to only evaluate based on a small percentage of the data.
   * {@code evaluationPercentage} controls how many of the {@link DataModel}‘s users are used in
   * evaluation.

对于一个大型数据库,测试它里边的一小部分数据是可取的;
evaluationPercentage参数用来控制数据模型中多少比例的用户用来参与评价,这个倒是和IRStatics那里很像啊!

   * <p>
   * To be clear, {@code trainingPercentage} and {@code evaluationPercentage} are not related. They
   * do not need to add up to 1.0, for example.

   trainingPercentage参数和evaluationPercentage参数并不相关,它们相加没规定一定是1啊。

evaluate函数定义如下

   * @param recommenderBuilder
   *          object that can build a {@link org.apache.mahout.cf.taste.recommender.Recommender} to test
   * @param dataModelBuilder
   *          {@link DataModelBuilder} to use, or if null, a default {@link DataModel}
   *          implementation will be used
   * @param dataModel
   *          dataset to test on
   * @param trainingPercentage
   *          percentage of each user‘s preferences to use to produce recommendations; the rest are compared
   *          to estimated preference values to evaluate
   *          {@link org.apache.mahout.cf.taste.recommender.Recommender} performance
   * @param evaluationPercentage
   *          percentage of users to use in evaluation
   * @return a "score" representing how well the {@link org.apache.mahout.cf.taste.recommender.Recommender}‘s
   *         estimated preferences match real values; <em>lower</em> scores mean a better match and 0 is a
   *         perfect match
   * @throws TasteException
   *           if an error occurs while accessing the {@link DataModel}
   */
     double evaluate(RecommenderBuilder recommenderBuilder,
                  DataModelBuilder dataModelBuilder,
                  DataModel dataModel,
                  double trainingPercentage,
                  double evaluationPercentage) throws TasteException;

那么这个评价器的evaluate函数是如何工作的呢?

首先,RecommenderEvaluator是一个接口,AverageAbsoluteDifferenceRecommenderEvaluator则是具体的类,但是在源码中,它并没有实现evaluate函数,实现RecommenderEvaluator接口的是AbstractDifferenceRecommenderEvaluator类。

所以来看看AbstractDifferenceRecommenderEvaluator里边evaluate的源码吧!

evaluate源码

首先依然是检测参数,

RecommenderBuilder和dataModel都不能为null,两个比例也该在0~1之间。

然后定义训练集和测试集,关于FastByIDMap<PreferenceArray>结构在之前的文章中介绍过,PreferenceArray也就是为一个user定义的由(item,value)组成的一个array,而FastByIDMap则是一个优化的Hash表。

比较奇怪的是,两个集合的大小都是预定义为1 + (int) (evaluationPercentage * numUsers))

evaluationPercentage定义的是用于评价的user的比例,所以 (int) (evaluationPercentage * numUsers)即用于评价的用户数,因为计算结果是double,转成int很可能会变小(比如4.1 -> 4),所以加个1。

    int numUsers = dataModel.getNumUsers();
    FastByIDMap<PreferenceArray> trainingPrefs = new FastByIDMap<PreferenceArray>(
        1 + (int) (evaluationPercentage * numUsers));
    FastByIDMap<PreferenceArray> testPrefs = new FastByIDMap<PreferenceArray>(
        1 + (int) (evaluationPercentage * numUsers));

接着构造训练集与测试集

对每个用户,取个随机double值,如果小于evaluationPercentage,则将他在数据模型中的Prefs根据trainingPercentage分割成为训练以及测试两部分分别加入训练测试集中;否则忽略这个用户。

因为是随机性,所以每次运行评价的user都有可能不同,运行得分也会不同。

LongPrimitiveIterator it = dataModel.getUserIDs();
    while (it.hasNext()) {
      long userID = it.nextLong();
      if (random.nextDouble() < evaluationPercentage) {
        splitOneUsersPrefs(trainingPercentage, trainingPrefs, testPrefs, userID, dataModel);
      }
    }

接着用训练集构造训练数据模型,并用这个模型创建推荐器,最后用测试集测试下得到结果啦。

DataModel trainingModel = dataModelBuilder == null ? new GenericDataModel(trainingPrefs)
        : dataModelBuilder.buildDataModel(trainingPrefs);

    Recommender recommender = recommenderBuilder.buildRecommender(trainingModel);

    double result = getEvaluation(testPrefs, recommender);

splitOneUsersPrefs源码

一个用户的Prefs是怎样进行分割的呢?

在splitOneUsersPrefs函数里边,

首先定义这个用户用来训练与测试的Pref的两个List,

然后从数据模型中取出这个用户的所有prefs。

List<Preference> oneUserTrainingPrefs = null;
    List<Preference> oneUserTestPrefs = null;
    PreferenceArray prefs = dataModel.getPreferencesFromUser(userID);

接着,遍历这个用户的prefs。

这时候就是trainingPercentage起作用的时候啦,依然是取个[0,1]的double随机值,如果小于trainingPercentage,则这个Preference进入训练,否则进入测试。

注意将Preference加入List的时候需要判断下List是否为null,如果为空,则需要新建下。不过我比较奇怪的是,为什么不事先写好oneUserTrainingPrefs = Lists.newArrayListWithCapacity(3);这样就不需要判断oneUserTrainingPrefs == null了,可以直接加入List,很奇怪啊!!

int size = prefs.length();
    for (int i = 0; i < size; i++) {
      Preference newPref = new GenericPreference(userID, prefs.getItemID(i), prefs.getValue(i));
      if (random.nextDouble() < trainingPercentage) {
        if (oneUserTrainingPrefs == null) {
          oneUserTrainingPrefs = Lists.newArrayListWithCapacity(3);
        }
        oneUserTrainingPrefs.add(newPref);
      } else {
        if (oneUserTestPrefs == null) {
          oneUserTestPrefs = Lists.newArrayListWithCapacity(3);
        }
        oneUserTestPrefs.add(newPref);
      }
    }

哦,原来如此,看这里的代码,根据两个List是否为null来判断是否要将这个user的数据加入最后的训练测试集中。因为有很大随机性,所以训练和测试的两个List都是有可能为null的哦!

if (oneUserTrainingPrefs != null) {
      trainingPrefs.put(userID, new GenericUserPreferenceArray(oneUserTrainingPrefs));
      if (oneUserTestPrefs != null) {
        testPrefs.put(userID, new GenericUserPreferenceArray(oneUserTestPrefs));
      }
    }

如何得到评价呢?

getEvaluation里边会执行如下的call方法,对测试集中的一个user的所有

有评价项的items进行预测并与测试集中的数据进行对比,所以call方法中循环为for (Preference realPref : prefs)。

整个处理过程有可能遇到2种异常:

1。因为EvaluationPercentage的原因,会有一部分users不在训练集中,这就导致在测试的时候,对于这些users,因为推荐系统中没有它们的相关信息(没有最近邻how to recommend?),所以无法进行推荐。

2。因为trainingPercentage的原因,会将一个user的所有Perferences分割为训练与测试两部分,这就可能会出现一个item只在测试集出现,而在所有的测试部分中都没出现(毕竟是随机选择,所以一切皆有可能),这样推荐系统中没有这个item的相关信息,所以也无法做出推荐。

得到一个user对一个item的estimatedPreference以及实际realPref之后,就由processOneEstimate来做处理了,而processOneEstimate则根据采用的不同距离来进行计算。

public Void call() throws TasteException {
for (Preference realPref : prefs) {
float estimatedPreference = Float.NaN;
try {
estimatedPreference = recommender.estimatePreference(testUserID, realPref.getItemID());
} catch (NoSuchUserException nsue) {
// It‘s possible that an item exists in the test data but not training data in which case
// NSEE will be thrown. Just ignore it and move on.
log.info("User exists in test data but not training data: {}", testUserID);
} catch (NoSuchItemException nsie)
{
log.info("Item exists in test data but not training data: {}", realPref.getItemID());
}
if (Float.isNaN(estimatedPreference)) { noEstimateCounter.incrementAndGet();
} else {
estimatedPreference = capEstimatedPreference(estimatedPreference); processOneEstimate(estimatedPreference, realPref);
}
}
return null;
}

在AverageAbsoluteDifferenceRecommenderEvaluator里边,processOneEstimate则是求取推荐值与实际值之间的绝对值abs,average变量则将这个绝对值加起来,最终结果则是average中所有数据的平均值。

使用一个average保存结果,对测试集的每一个user的每个item都进行一次测试,如果推荐结果不为空,则进入processOneEstimate处理,将推荐值与实际值的差距加入average,最后所有结果的平均值即为最终的评价结果。

private RunningAverage average;
protected void processOneEstimate(float estimatedPreference, Preference realPref) {
    average.addDatum(Math.abs(realPref.getValue() - estimatedPreference));
  }
  protected double computeFinalEvaluation() {
    return average.getAverage();
  }

如果是RMSRecommenderEvaluator,processOneEstimate则是求取推荐值与实际值的平方值。

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-09 10:47:22

Mahout推荐系统引擎RecommenderEvaluator源码解析的相关文章

只需一句话就能搞定NVelocity模板引擎,源码+解析+文档+资料+注释

好长时间不发技术方面的动态了,今天无聊就发篇关于NVelocity的技术文章吧,这门技术来源于java开源项目Velocity,比较好用,其他的我也不过多介绍了,没听过的在文章末尾会有介绍,下面我们就实战吧~ 咱们直接上最简单的方法,一句话搞定: DNTNvelocityHelper.NvelocityTemplate(context.Request.MapPath("~/NVelocity/templates/"), context, "Templater_index.dn

Flink 源码解析 —— 项目结构一览

Flink 源码项目结构一览 https://t.zsxq.com/MNfAYne 博客 1.Flink 从0到1学习 -- Apache Flink 介绍 2.Flink 从0到1学习 -- Mac 上搭建 Flink 1.6.0 环境并构建运行简单程序入门 3.Flink 从0到1学习 -- Flink 配置文件详解 4.Flink 从0到1学习 -- Data Source 介绍 5.Flink 从0到1学习 -- 如何自定义 Data Source ? 6.Flink 从0到1学习 --

Mahout推荐系统引擎UserCF中的IRStats部分源码解析

Mahout提供推荐系统引擎是模块化的,分为5个主要部分组成: 1. 数据模型 2. 相似度算法 3. 近邻算法 4. 推荐算法 5. 算法评分器 今天好好看了看关于推荐算法以及算法评分部分的源码. 以http://blog.csdn.net/jianjian1992/article/details/46582713 里边数据的为例进行实验. 整体流程的代码如下,依照上面的5个模块,看起来倒是很简单呀. public static RecommenderBuilder userRecommend

Java生鲜电商平台-电商中海量搜索ElasticSearch架构设计实战与源码解析

Java生鲜电商平台-电商中海量搜索ElasticSearch架构设计实战与源码解析 生鲜电商搜索引擎的特点 众所周知,标准的搜索引擎主要分成三个大的部分,第一步是爬虫系统,第二步是数据分析,第三步才是检索结果.首先,电商的搜索引擎并没有爬虫系统,因为所有的数据都是结构化的,一般都是微软的数据库或者 Oracle 的数据库,所以不用像百度一样用「爬虫」去不断去别的网站找内容,当然,电商其实也有自己的「爬虫」系统,一般都是抓取友商的价格,再对自己进行调整. 第二点,就是电商搜索引擎的过滤功能其实比

ExcelReport第二篇:ExcelReport源码解析

导航 目   录:基于NPOI的报表引擎--ExcelReport 上一篇:使用ExcelReport导出Excel 下一篇:扩展元素格式化器 概述 针对上一篇随笔收到的反馈,在展开对ExcelReport源码解析之前,我认为把编写该组件时的想法分享给大家是有必要的. 编写该组件时,思考如下: 1)要实现样式.格式与数据的彻底分离. 为什么要将样式.格式与数据分离呢?恩,你不妨想一想在生成报表时,那些是变的而那些又是不变的.我的结论是:变的是数据. 有了这个想法,很自然的想到用模板去承载不变的部

十七.jQuery源码解析之入口方法Sizzle(1)

函数Sizzle(selector,context,results,seed)用于查找与选择器表达式selector匹配的元素集合.该函数是选择器引擎的入口. 函数Sizzle执行的6个关键步骤如下: 1.解析选择器表达式,解析出块表达式和关系符. 2.如果存在位置伪类,则从左向右查找: a.查找第一个块表达式匹配的元素集合,得到第一个上下文元素集合. b.遍历剩余的块表达式和块间关系符,不断缩小上下文元素集合. 3.否则从右向左查找: a.查找最后一个块表达式匹配的元素集合,得到候选集,映射集

十五.jQuery源码解析之Sizzle总体结构.htm

Sizzle是一款纯javascript实现的css选择器引擎,它具有完全独立,无库依赖;小;易于扩展和兼容性好等特点. W3C Selectors API规范定义了方法querySelector()和querySelectorAll(),但是IE6,7不支持这两个方法. 在Sizzele内部,如果浏览器支持方法querySelectorAll(),则调用该方法查找元素,如果不支持,则模拟该方法的行为. Sizzle支持几乎所有的css3选择器,并且会按照文档位置返回结果. 上面截取的只是Siz

十六.jQuery源码解析之Sizzle设计思路.htm

为了便于后面的叙述,需要了解一些相关术语和约定. 并列选择器表达式:"div,p,a"====>div,p,a是并列的. 块表达式:"div>p"中的div和p就是两个块. 块表达式的类型:共8种.id,class,name,attr,tag,child,pos,pseudo(伪类表达式) 块间的关系符:共4种.">":父子关系,"+":紧挨着的兄弟关系,"~":后面的所有兄弟关系,&qu

黄聪:WordPress动作钩子函数add_action()、do_action()源码解析

WordPress常用两种钩子,过滤钩子和动作钩子.过滤钩子相关函数及源码分析在上篇文章中完成,本篇主要分析动作钩子源码. 然而,在了解了动作钩子的源码后你会发现,动作钩子核心代码竟然跟过滤钩子差不多!是的,至此,我不得不告诉你,动作钩子只是WP开发者为了区分概念而把过滤钩子另外命名的一种东西!当然,它们还是有一些细微的差别,下面我们将从源码来深入解读. 动作钩子概念:动作钩子是WP代码执行到某处或某个事件发生时触发的一系列函数,插件可以利用动作钩子API在WP代码执行的特定点之前插入一系列函数