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

来源:互联网 发布:美国网络存储公司 编辑:程序博客网 时间:2024/05/16 06:35

在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则是求取推荐值与实际值的平方值。

0 0
原创粉丝点击