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

Mahout提供推荐系统引擎是模块化的,分为5个主要部分组成:

1. 数据模型

2. 相似度算法

3. 近邻算法

4. 推荐算法

5. 算法评分器

今天好好看了看关于推荐算法以及算法评分部分的源码。

http://blog.csdn.net/jianjian1992/article/details/46582713

里边数据的为例进行实验。

整体流程的代码如下,依照上面的5个模块,看起来倒是很简单呀。

public static RecommenderBuilder userRecommender(final UserSimilarity us, final UserNeighborhood un, boolean pref) throws TasteException {
            return pref ? new RecommenderBuilder() {

                public Recommender buildRecommender(DataModel model) throws TasteException {
                    //user对每个item都有评价preference的时候使用
                    return new GenericUserBasedRecommender(model, un, us);
                }
            } : new RecommenderBuilder() {

                public Recommender buildRecommender(DataModel model) throws TasteException {
                    //user对item没有评价preference的时候使用
                    return new GenericBooleanPrefUserBasedRecommender(model, un, us);
                }
            };
        }
final static int NEIGHBORHOOD_NUM = 3;
final static int RECOMMENDER_NUM = 2;
public static void main(String[] args) throws IOException, TasteException {
        String file = "item.csv";
        //1.构建数据模型
        DataModel model = new FileDataModel(new File(file));
        //2.相似度算法,采用欧式距离
        UserSimilarity user = new EuclideanDistanceSimilarity(model);
        //3.近邻算法,采用K = NEIGHBORHOOD_NUM最近邻
        NearestNUserNeighborhood neighbor = new NearestNUserNeighborhood(NEIGHBORHOOD_NUM, user, model);
        //4.基于以上三个模块构建推荐器,构造好之后就已经确定了
        UserBasedRecommender r = new GenericUserBasedRecommender(model, neighbor, user);
        //5.a推荐器的构造方法,用于评价时根据新采集的训练集生成推荐器
        RecommenderBuilder recommenderBuilder = userRecommender(user, neighbor, false);
        //5.b评价算法,采用平均绝对值距离,也即L1距离
        RecommenderEvaluator evalutor = new AverageAbsoluteDifferenceRecommenderEvaluator();
        System.out.println("eval:"+ evalutor.evaluate(recommenderBuilder, null, model, 0.5, 1));
        //5.c评价算法,计算推荐系统的准确率,召回率等指标
        RecommenderIRStatsEvaluator evaluator = new GenericRecommenderIRStatsEvaluator();
        IRStatistics stats = evaluator.evaluate(recommenderBuilder, null, model, null, 2, GenericRecommenderIRStatsEvaluator.CHOOSE_THRESHOLD, 1.0);
        System.out.printf("Recommender IR Evaluator: [Precision:%s,Recall:%s,F1:%s,FallOut:%s,nDCG:%s]\n", stats.getPrecision(), stats.getRecall(), stats.getF1Measure(), stats.getFallOut(), stats.getNormalizedDiscountedCumulativeGain());

运行结果如下:

eval:2.368176281452179
Recommender IR Evaluator: [Precision:0.5,Recall:1.0,F1:0.6666666666666666,FallOut:0.16666666666666666,nDCG:1.0]

RecommenderIRStatsEvaluator源码解析

RecommenderIRStatsEvaluator是一个接口,用于得到推荐系统的准确率,召回率等统计指标。

它定义的函数如下

/**
   * @param recommenderBuilder
   * 它通过public Recommender buildRecommender(DataModel model)定义推荐系统创建的方式;       

   * @param dataModelBuilder
   * 数据模型创建的方式,如果已经创建好了数据模型,一般这个参数可以为null

   * @param dataModel
   * 推荐系统使用的数据模型
   *
   * @param rescorer
   * 推荐排序的方式,一般情况下可以为null
   *
   * @param at
   * 这个参数起的名字挺奇怪的,它用来定义计算准确率的时候,一个user可以拥有的相关项items的最大个数,相关项item定义为user对这个item的评价是超过了relevanceThreshold的

   * @param relevanceThreshold
   * 和at一起使用定义一个user的相关项
   *
   * @return {@link IRStatistics} with resulting precision, recall, etc.
   * @throws TasteException
   *           if an error occurs while accessing the {@link DataModel}
   */
IRStatistics evaluate(RecommenderBuilder recommenderBuilder,
                        DataModelBuilder dataModelBuilder,
                        DataModel dataModel,
                        IDRescorer rescorer,
                        int at,
                        double relevanceThreshold,
                        double evaluationPercentage) throws TasteException;

在代码中是使用

RecommenderIRStatsEvaluator evaluator = new GenericRecommenderIRStatsEvaluator();

来创建这个评价器的。

关于GenericRecommenderIRStatsEvaluator的介绍是

For each user, these implementation determine the top {@code n} preferences, then evaluate the IR statistics based on a {@link DataModel} that does not have these values. This number {@code n} is the “at” value, as in “precision at 5”. For example, this would mean precision evaluated by removing the top 5 preferences for a user and then finding the percentage of those 5 items included in the top 5 recommendations for that user.

看起来很拗口啊,似乎是说,对每一个用户,都找出他的最高的at个preferences(即对item的评价)作为训练数据训练出一个推荐系统,之后基于剩下的preferences来做评价。

下面来看看代码里边具体是怎么做的吧。

GenericRecommenderIRStatsEvaluator源码解析

因为使用的数据是有用户对item的评价,所以采用这一个具体的评价器。

我们来看看它的evaluate函数是怎么实现的。

首先检查参数,这是编程好习惯。

接着定义了四个统计指标:

RunningAverage precision = new FullRunningAverage();
RunningAverage recall = new FullRunningAverage();
RunningAverage fallOut = new FullRunningAverage();
RunningAverage nDCG = new FullRunningAverage();

RunningAverage是个神奇的结构,可以新加入或删除数据,然后动态更新所有数据的平均值。这里会对每个user都求一次这4个指标,最后求所有user指标的平均值作为整个推荐系统的结果。

接着定义了三个变量

int numItems = dataModel.getNumItems();
int numUsersRecommendedFor = 0;
int numUsersWithRecommendations = 0;

然后就开始对数据模型中的每个user进行循环了!

在这里使用到了参数evaluationPercentage,这个参数按照名字就是评价比率,也即在所有users中选择用于评价的比率。

这里通过取个随机double值,如果大于这个参数,则这个user不加入评价。这个random的取值应该是在[0,1]范围均等概率取值才ok的哦!

LongPrimitiveIterator it = dataModel.getUserIDs();
    while (it.hasNext()) {

      long userID = it.nextLong();

      if (random.nextDouble() >= evaluationPercentage) {
        // Skipped
        continue;
      }
      ...评价部分
   }

下面来看看循环里边的评价部分

    PreferenceArray prefs = dataModel.getPreferencesFromUser(userID);

      // List some most-preferred items that would count as (most) "relevant" results
      double theRelevanceThreshold = Double.isNaN(relevanceThreshold) ? computeThreshold(prefs) : relevanceThreshold;
      FastIDSet relevantItemIDs = dataSplitter.getRelevantItemsIDs(userID, at, theRelevanceThreshold, dataModel);

      int numRelevantItems = relevantItemIDs.size();
      if (numRelevantItems <= 0) {
        continue;
      }

PreferenceArray的内容

首先得到数据模型中这个userID的所有评价项。PreferenceArray里边的内容是怎样的呢?

看如下代码,显示了uid = 3的结果,可以看到,对于pref里边的每一项,它们的getUserID结果都是3,每一项包括了uid对一个prefs.getItemID(i)的评价prefs.getValue(i)。

        int uid = 3;
        PreferenceArray prefs = model.getPreferencesFromUser(uid);
        //prefs.sortByValueReversed();
        for (int i = 0; i < prefs.length(); i++){
            System.out.println("user id" + prefs.getUserID(i));
            System.out.println("Item id value:" + prefs.getItemID(i) + ":" + prefs.getValue(i));
        }
user id3
Item id value:101:2.5
user id3
Item id value:104:4.0
user id3
Item id value:105:4.5
user id3
Item id value:107:5.0

RelevanceThreshold的计算

按照代码,接下来要根据threshold来选择相关项,那么代码里边的computeThreshold(prefs)是如何计算的呢?

  private static double computeThreshold(PreferenceArray prefs) {
    if (prefs.length() < 2) {
      // Not enough data points -- return a threshold that allows everything
      return Double.NEGATIVE_INFINITY;
    }
    RunningAverageAndStdDev stdDev = new FullRunningAverageAndStdDev();
    int size = prefs.length();
    for (int i = 0; i < size; i++) {
      stdDev.addDatum(prefs.getValue(i));
    }
    return stdDev.getAverage() + stdDev.getStandardDeviation();
  }

stdDev是用来计算一组数据的平均值以及样本标准差的。

样本标准差和群体标准差的计算是有差别的,样本标准差中是除以n-1,大概理解应该是一堆样本中,取一个为标准,其它随意,所以自由度就为n-1了吧。

不过这个计算出来的阈值感觉很坑啊,以我的数据为例,用uid = 3计算,最后得到的阈值结果为5.080123449734644,(⊙﹏⊙)b这样子就没有任何一个preference满足要求了。

不知道是不是数据很大的时候效果会好些~~~

relevantItemIDs相关项的确定

dataSplitter选用的是GenericRelevantItemsDataSplitter。

RelevantItemsDataSplitter dataSplitter = new GenericRelevantItemsDataSplitter();

FastIDSet relevantItemIDs = dataSplitter.getRelevantItemsIDs(userID, at, theRelevanceThreshold, dataModel);

getRelevantItemsIDs的实现,首先

1.得到userID的Preferences

2.创建一个FastIDSet用来保存相关项的id,大小为at

3.prefs根据值大小排序

3.遍历pref,如果user对这个item的评价prefs.getValue(i)不小于相关阈值,则将这个item的加入相关项,最多取at个满足条件的item

public FastIDSet getRelevantItemsIDs(long userID,
                                       int at,
                                       double relevanceThreshold,
                                       DataModel dataModel) throws TasteException {
    PreferenceArray prefs = dataModel.getPreferencesFromUser(userID);
    FastIDSet relevantItemIDs = new FastIDSet(at);
    prefs.sortByValueReversed();
    for (int i = 0; i < prefs.length() && relevantItemIDs.size() < at; i++) {
      if (prefs.getValue(i) >= relevanceThreshold) {
        relevantItemIDs.add(prefs.getItemID(i));
      }
    }
    return relevantItemIDs;
  }

下面来看看效果,仍旧以uid = 3 为例,如果relevanceThreshold为4,则结果如下

        double relevanceThreshold = 4;//computeThreshold(prefs);
        FastIDSet relevantItemIDs = new FastIDSet(at);
        prefs.sortByValueReversed();

        for (int i = 0; i < prefs.length() && relevantItemIDs.size() < at; i++) {
          if (prefs.getValue(i) >= relevanceThreshold) {
            relevantItemIDs.add(prefs.getItemID(i));
          }
        }
        long[] itemIDs = relevantItemIDs.toArray();
        for (int i = 0; i < itemIDs.length; i++){
            System.out.println("Revevant Item id:" + itemIDs[i]);
        }

结果和上面的结果也正好符合哦!

Revevant Item id:104
Revevant Item id:105
Revevant Item id:107

相关项个数<=0的话,这个user就没有评价的价值了,所以会跳过进入下一个循环。

trainingUsers的构造

得到这个user的相关项之后,接着为它构造一个推荐用的训练集。

这个训练集的构造规则便是:

1.对于其它的用户,将他们所有的(item,preference)都加入训练集;

2.对于这个用户user,将它的除了相关项之外的其它项的喜好加入训练集;

    //得到训练集
    FastByIDMap<PreferenceArray> trainingUsers = new FastByIDMap<PreferenceArray>(dataModel.getNumUsers());
      LongPrimitiveIterator it2 = dataModel.getUserIDs();
      while (it2.hasNext()) {
        dataSplitter.processOtherUser(userID, relevantItemIDs, trainingUsers, it2.nextLong(), dataModel);
      }
     //构造训练模型
      DataModel trainingModel = dataModelBuilder == null ? new GenericDataModel(trainingUsers)
          : dataModelBuilder.buildDataModel(trainingUsers);
      //如果这个user的所有项都变为了相关项,那也没评价的必要了,因为训练集里边都没有了这个user,怎么计算它与其它用户之间的距离呢?
      try {
        trainingModel.getPreferencesFromUser(userID);
      } catch (NoSuchUserException nsee) {
        continue; // Oops we excluded all prefs for the user -- just move on
      }

仍旧以uid = 3为例,我们来看看运行结果,相关项系数为4,

RelevantItemsDataSplitter dataSplitter = new GenericRelevantItemsDataSplitter();
        FastByIDMap<PreferenceArray> trainingUsers = new FastByIDMap<PreferenceArray>(model.getNumUsers());
        LongPrimitiveIterator it2 = model.getUserIDs();
        while (it2.hasNext()) {
          dataSplitter.processOtherUser(uid, relevantItemIDs, trainingUsers, it2.nextLong(), model);
        }
        for (Entry<Long, PreferenceArray> entry : trainingUsers.entrySet()) {
            Long userid = entry.getKey();
            PreferenceArray pref = entry.getValue();
            System.out.println("user id is " + userid + ":" + pref.getUserID(0));
            for (int i = 0; i < pref.length(); i++){
                System.out.println("item id -> value is :" + pref.getItemID(i) + "->" + pref.getValue(i));
            }
        }

如下结果中,除了uid = 3,其它user的所有项都加了进来;uid则只加了非相关项。

user id is 1:1
item id -> value is :101->5.0
item id -> value is :102->3.0
item id -> value is :103->2.5
user id is 2:2
item id -> value is :101->2.0
item id -> value is :102->2.5
item id -> value is :103->5.0
item id -> value is :104->2.0
user id is 3:3
item id -> value is :101->2.5
user id is 4:4
item id -> value is :101->5.0
item id -> value is :104->4.5
item id -> value is :106->4.0
item id -> value is :103->3.0
user id is 5:5
item id -> value is :101->4.0
item id -> value is :106->4.0
item id -> value is :104->4.0
item id -> value is :105->3.5
item id -> value is :102->3.0
item id -> value is :103->2.0

接下来又有一个判断,size是user的相关项个数+训练集中user的项的个数,也即user本身有的preferences个数,这里判定at的大小的两倍,也即相关项最大个数的2倍应该不大于user本身的item个数时才会对这个user进行评价,挺严格的啊!

      int size = numRelevantItems + trainingModel.getItemIDsFromUser(userID).size();
      if (size < 2 * at) {
        // Really not enough prefs to meaningfully evaluate this user
        continue;
      }

IR Statics指标

首先用上面得到的训练模型构建推荐器,之后对这个user进行推荐,得到recommendedItems。

之后求intersectionSize,也即推荐项与相关项之间相同的个数。

Recommender recommender = recommenderBuilder.buildRecommender(trainingModel);

      int intersectionSize = 0;
      List<RecommendedItem> recommendedItems = recommender.recommend(userID, at, rescorer);
      for (RecommendedItem recommendedItem : recommendedItems) {
        if (relevantItemIDs.contains(recommendedItem.getItemID())) {
          intersectionSize++;
        }
      }

准确率(Precision)与召回率

首先先介绍下准确率(Precision)与召回率,也叫找全率(Recall)。

一般都从分类的角度来说明的,类别有正类和负类。

                   预测为正类的样本中真正属于正类的样本个数
准确率     =  ------------------------------------------------
                          所有预测为正类的样本的个数

                   预测为正类的样本中真正属于正类的样本个数
召回率     =  ------------------------------------------------
                          所有真正为正类的样本的个数

放到推荐系统中便是

                       推荐给user的Items中属于user相关项的个数
准确率     =  ------------------------------------------------
                          推荐给user的Items的总个数

                   推荐给user的Items中属于user相关项的个数
召回率     =  ------------------------------------------------
                          user的所有相关项item个数

所以上面的intersectionSize便是 推荐给user的Items中属于user相关项的个数。

    // 推荐给user的Items的总个数
    int numRecommendedItems = recommendedItems.size();

      // Precision
      if (numRecommendedItems > 0) {
        precision.addDatum((double) intersectionSize / (double) numRecommendedItems);
      }

      // Recall
      recall.addDatum((double) intersectionSize / (double) numRelevantItems);

另外还有三个指标:

Fall-out

                   预测为正类的样本中真正属于负类的样本个数
Fall-out     =  ------------------------------------------------
                         所有为负类的样本的个数

                    推荐给user的Items中不属于user相关项的个数
Fall-out     =  ------------------------------------------------
                       模型中所有不为user相关项的item总个数
// Fall-out
      if (numRelevantItems < size) {
        fallOut.addDatum((double) (numRecommendedItems - intersectionSize)
                         / (double) (numItems - numRelevantItems));
      }

nDCG

概述搜索排序算法的评价指标NDCG(Normalized Discounted Cumulative Gain)

N指的是归一化,D指的是衰减率,C指的累加,G指的是熵,其实这个公式的关键就是就是熵,再多了三个形容词:归一化的,带有衰减函数的,再带有累加的熵即是了

具体可以查看http://eletva.com/tower/?p=159

// nDCG
      // In computing, assume relevant IDs have relevance 1 and others 0
      double cumulativeGain = 0.0;
      double idealizedGain = 0.0;
      for (int i = 0; i < numRecommendedItems; i++) {
        RecommendedItem item = recommendedItems.get(i);
        double discount = 1.0 / log2(i + 2.0); // Classical formulation says log(i+1), but i is 0-based here
        if (relevantItemIDs.contains(item.getItemID())) {
          cumulativeGain += discount;
        }
        // otherwise we‘re multiplying discount by relevance 0 so it doesn‘t do anything

        // Ideally results would be ordered with all relevant ones first, so this theoretical
        // ideal list starts with number of relevant items equal to the total number of relevant items
        if (i < numRelevantItems) {
          idealizedGain += discount;
        }
      }
      if (idealizedGain > 0.0) {
        nDCG.addDatum(cumulativeGain / idealizedGain);
      }

reach

也即所有待推荐的user中成功推荐的user比例

// Reach
      numUsersRecommendedFor++;
      if (numRecommendedItems > 0) {
        numUsersWithRecommendations++;
      }
    (double) numUsersWithRecommendations / (double) numUsersRecommendedFor

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

时间: 2024-07-29 00:51:56

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

Android中的ViewRootImpl类源码解析

转载请注明出处 http://blog.csdn.net/qianhaifeng2012/article/details/51737370 ViewRoot目前这个类已经没有了,是老版本中的一个类,在Android2.2以后用ViewRootImpl代替ViewRoot,对应于ViewRootImpl.java,他是链接WindowManager和DecorView的纽带,另外View的绘制也是通过ViewRootImpl来完成的. 它的主要作用我的总结为如下: A:链接WindowManage

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

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

[ASPX] 模版引擎XTemplate与代码生成器XCoder(源码)

模版引擎XTemplate是一个仿T4设计的引擎,功能上基本与T4一致(模版语法上完全兼容T4,模版头指令部分兼容). 自己设计模版引擎,就是为了代码生成器.网站模版.邮件模版等多种场合,也就是要能拿出来单独使用.功能强大并且容易控制的.T4是个很好的引擎,但是它的设计基本上倾向于vs,几乎不顾别的场合. XTemplate特点如下: 1,完全使用C#作为模版语言.跟ASP.ASP.Net页面的解析一样,把<##>标签外的文本内容当作字符串,用一个 StringBuilder,标签内作为C#原

eclipse中导入jdk源码、SpringMVC注解@RequestParam、SpringMVC文件上传源码解析、ajax上传excel文件

eclipse中导入jdk源码:http://blog.csdn.net/evolly/article/details/18403321, http://www.codingwhy.com/view/799.html. ------------------------------- SpringMVC注解@RequestParam:http://825635381.iteye.com/blog/2196911. --------------------------- SpringMVC文件上传源

转-----在Xcode中使用Git进行源码版本控制

在Xcode中使用Git进行源码版本控制 http://www.cocoachina.com/ios/20140524/8536.html 本文翻译自Understanding Git Source Control in Xcode (译者myShire)欢迎您加入我们的翻译小组. 在应用程序开发过程中,很重要的一部分工作就是如何进行源码的版本控制.当代码出现问题时,我们就需要将代码恢复到原先正常的版本.如果是多个人共同开发一个项目,那么代码的控制就会非常复杂.幸运的是,开发者不需要自己控制这些

Scala 深入浅出实战经典 第48讲:Scala类型约束代码实战及其在Spark中的应用源码解析

王家林亲授<DT大数据梦工厂>大数据实战视频 Scala 深入浅出实战经典(1-64讲)完整视频.PPT.代码下载:百度云盘:http://pan.baidu.com/s/1c0noOt6 腾讯微云:http://url.cn/TnGbdC 360云盘:http://yunpan.cn/cQ4c2UALDjSKy 访问密码 45e2 技术爱好者尤其是大数据爱好者 可以加DT大数据梦工厂的qq群 DT大数据梦工厂① :462923555 DT大数据梦工厂②:437123764 DT大数据梦工厂③

68:Scala并发编程原生线程Actor、Cass Class下的消息传递和偏函数实战解析及其在Spark中的应用源码解析

今天给大家带来的是王家林老师的scala编程讲座的第68讲:Scala并发编程原生线程Actor.Cass Class下的消息传递和偏函数实战解析 昨天讲了Actor的匿名Actor及消息传递,那么我们今天来看一下原生线程Actor及CassClass下的消息传递,让我们从代码出发: case class Person(name:String,age:Int)//定义cass Class class HelloActor extends Actor{//预定义一个Actor  def act()

Apache Spark源码走读之22 -- Spark MLLib中拟牛顿法L-BFGS的源码实现

欢迎转载,转载请注明出处,徽沪一郎. 概要 本文就拟牛顿法L-BFGS的由来做一个简要的回顾,然后就其在spark mllib中的实现进行源码走读. 拟牛顿法 数学原理 代码实现 L-BFGS算法中使用到的正则化方法是SquaredL2Updater. 算法实现上使用到了由scalanlp的成员项目breeze库中的BreezeLBFGS函数,mllib中自定义了BreezeLBFGS所需要的DiffFunctions. runLBFGS函数的源码实现如下 def runLBFGS( data:

安卓中的事件分发机制源码解析

安卓中的事件分发机制主要涉及到两类控件,一类是容器类控件ViewGroup,如常用的布局控件,另一类是显示类控件,即该控件中不能用来容纳其它控件,它只能用来显示一些资源内容,如Button,ImageView等控件.暂且称前一类控件为ViewGroup类控件(尽管ViewGroup本身也是一个View),后者为View类控件. 安卓中的事件分发机制主要涉及到dispatchTouchEvent(MotionEvent ev).onInterceptTouchEvent(MotionEvent e