mahout的推荐引擎Taste的学习笔记(四)

三、Slope One协同过滤

1、Slope One原理

为了大大减少过适(过拟合)的发生,提升算法简化实现, Slope One 系列易实现的Item-based协同过滤算法被提了出来。本质上,该方法运用更简单形式的回归表达式() 和单一的自由参数,而不是一个项目评分和另一个项目评分间的线性回归 ()。 该自由参数只不过就是两个项目评分间的平均差值。甚至在某些实例当中,它比线性回归的方法更准确[2],而且该算法只需要一半(甚至更少)的存储量。

:

  1. User A 对 Item I 评分为1 对Item J.评分为1.5
  2. User B 对 Item I 评分为2.
  3. 你认为 User B 会给 Item J 打几分?
  4. Slope One 的答案是:2.5 (1.5-1+2=2.5).


举个更实际的例子,考虑下表:

评分数据库样本 顾客 项目 1 项目 2 项目 3
John 5 3 2
Mark 3 4 未评分
Lucy 未评分 2 5

在本例中,项目2和1之间的平均评分差值为 (2+(-1))/2=0.5. 因此,item1的评分平均比item2高0.5。同样的,项目3和1之间的平均评分差值为3。因此,如果我们试图根据Lucy 对项目2的评分来预测她对项目1的评分的时候,我们可以得到 2+0.5 = 2.5。同样,如果我们想要根据她对项目3的评分来预测她对项目1的评分的话,我们得到 5+3=8.

如果一个用户已经评价了一些项目,可以这样做出预测:简单地把各个项目的预测通过加权平均值结合起来。当用户两个项目都评价过的时候,权值就高。在 上面的例子中,项目1和项目2都评价了的用户数为2,项目1和项目3 都评价了的用户数为1,因此权重分别为2和1. 我们可以这样预测Lucy对项目1的评价:

于是,对“n”个项目,想要实现 Slope One,只需要计算并存储“n”对评分间的平均差值和评价数目即可。

 

2、mahout对Slope One的实现

调用代码很简单:

				 
 //In-Memory Recommender 
 DiffStorage diffStorage = new MemoryDiffStorage(model, Weighting.UNWEIGHTED, false, 
 Long.MAX_VALUE)); 
 Recommender recommender = new SlopeOneRecommender(model, Weighting.UNWEIGHTED, 
 Weighting.UNWEIGHTED, diffStorage);  

 //Database-based Recommender 
 AbstractJDBCDataModel model = new MySQLJDBCDataModel(); 
 DiffStorage diffStorage = new MySQLJDBCDiffStorage(model); 
 Recommender recommender = new SlopeOneRecommender(model, Weighting.WEIGHTED, 
 Weighting.WEIGHTED, diffStorage); 

 

下边详细的分析代码

 

先看SlopeOneRecommender的主要功能函数:

 

@Override
  public List<RecommendedItem> recommend(long userID, int howMany, IDRescorer rescorer) throws TasteException {
    Preconditions.checkArgument(howMany >= 1, "howMany must be at least 1");
    log.debug("Recommending items for user ID '{}'", userID);

    //先得到在DiffStorage中的分差计算中,所有计算到的物品列表
    //在这个列表中再将userID用户评过分的物品去掉,得到可能的物品列表
    FastIDSet possibleItemIDs = diffStorage.getRecommendableItemIDs(userID);
    //又是一个评估器
    TopItems.Estimator<Long> estimator = new Estimator(userID);
    //最后是获取得分最高的howMany个item
    List<RecommendedItem> topItems = TopItems.getTopItems(howMany, possibleItemIDs.iterator(), rescorer,
      estimator);

    log.debug("Recommendations are: {}", topItems);
    return topItems;
  }

 

1、计算物品间平均分差

DiffStorage接口类,这里以MemoryDiffStorage为例分析,这个类的主要作用是对源数据进行处理,计算出每一个物品对其他物品的平均分差:

 

//
private void buildAverageDiffs() throws TasteException {
    log.info("Building average diffs...");
    try {
      buildAverageDiffsLock.writeLock().lock();
      averageDiffs.clear();
      long averageCount = 0L;
      //获取数据源中的所有用户
      LongPrimitiveIterator it = dataModel.getUserIDs();
      while (it.hasNext()) {
        //对每一个用户进行分析,计算物品间的分差
        averageCount = processOneUser(averageCount, it.nextLong());
      }
     
      pruneInconsequentialDiffs();
      updateAllRecommendableItems();
     
    } finally {
      buildAverageDiffsLock.writeLock().unlock();
    }
  }

 

 

 具体计算平均分差的函数为processOneUser() :

其中  RunningAverage是一个存储平均分差和统计分差个数的类

 

//这个类一是计算各物品间的平均分差,二是统计各物品的平均得分 
private long processOneUser(long averageCount, long userID) throws TasteException {
    log.debug("Processing prefs for user {}", userID);
    // Save off prefs for the life of this loop iteration
    PreferenceArray userPreferences = dataModel.getPreferencesFromUser(userID);
    int length = userPreferences.length();
    for (int i = 0; i < length - 1; i++) {
      float prefAValue = userPreferences.getValue(i);//评分
      long itemIDA = userPreferences.getItemID(i);//物品id
      //获取tiemIDA物品与其他物品的评分差Map(key是itemID,value是平均分差)
      FastByIDMap<RunningAverage> aMap = averageDiffs.get(itemIDA);
      if (aMap == null) {
        aMap = new FastByIDMap<RunningAverage>();
        averageDiffs.put(itemIDA, aMap);
      }
      //itemIDA物品对物品列表中排位于自己之后的所有物品计算评分差,并逐个加入到RunningAverage中
      for (int j = i + 1; j < length; j++) {
        // This is a performance-critical block
        long itemIDB = userPreferences.getItemID(j);
        RunningAverage average = aMap.get(itemIDB);//获取itemIDA对itemIDB的平均分差类
        if (average == null && averageCount < maxEntries) {
          average = buildRunningAverage();
          aMap.put(itemIDB, average);
          averageCount++;
        }
        if (average != null) {
          average.addDatum(userPreferences.getValue(j) - prefAValue);
        }
      }
      //averageItemPref是个Map(key是itemID,value是该物品的平均得分)
      //计算itemIDA的平均得分
      RunningAverage itemAverage = averageItemPref.get(itemIDA);
      if (itemAverage == null) {
        itemAverage = buildRunningAverage();
        averageItemPref.put(itemIDA, itemAverage);
      }
      itemAverage.addDatum(prefAValue);
    }
    return averageCount;
  }

 

 

2、评估器Estimator

对于userID的用户,给定某个itemID的物品,要想获得此物品的评估分的话,首先获取itemID与userID用户所评价过的所有的物品之间的平均评分差,然后逐个加权,然后再取平均值,这个平均值就是itemID的评估分

 private final class Estimator implements TopItems.Estimator<Long> {   
    @Override
    public double estimate(Long itemID) throws TasteException {
      return doEstimatePreference(userID, itemID);
    }
  }

private float doEstimatePreference(long userID, long itemID) throws TasteException {
    double count = 0.0;
    double totalPreference = 0.0;
    PreferenceArray prefs = getDataModel().getPreferencesFromUser(userID);
    //获取itemID与userID用户所评价过的所有的物品之间的 平均评分差
    RunningAverage[] averages = diffStorage.getDiffs(userID, itemID, prefs);
    int size = prefs.length();
    //将userID用户所评价过的物品与itemID物品之间的平均评分差,逐个加权(加权之前还要加上该评价过物品的得分)
    //然后再取平均值,这个平均值就是itemID的评估分
    for (int i = 0; i < size; i++) {
      RunningAverage averageDiff = averages[i];
      if (averageDiff != null) {
        double averageDiffValue = averageDiff.getAverage();
        if (weighted) {
          double weight = averageDiff.getCount();
          if (stdDevWeighted) {
            double stdev = ((RunningAverageAndStdDev) averageDiff).getStandardDeviation();
            if (!Double.isNaN(stdev)) {
              weight /= 1.0 + stdev;
            }
          }
          //加权(weight),权值就是参与RunningAverage的个数,其实就是同时对这两个物品进行过评价的用户个数
          totalPreference += weight * (prefs.getValue(i) + averageDiffValue);
          count += weight;
        } else {
          totalPreference += prefs.getValue(i) + averageDiffValue;
          count += 1.0;
        }
      }
    }
    if (count <= 0.0) {
      RunningAverage itemAverage = diffStorage.getAverageItemPref(itemID);
      return itemAverage == null ? Float.NaN : (float) itemAverage.getAverage();
    } else {
      return (float) (totalPreference / count);
    }
  }

 

3、获取得分最高的howMany个item

跟物品CF和用户CF一样,最后一步都是调用TopItems.getTopItems()

public static List<RecommendedItem> getTopItems(int howMany,
                                                  LongPrimitiveIterator possibleItemIDs,
                                                  IDRescorer rescorer,
                                                  Estimator<Long> estimator) throws TasteException {
    Preconditions.checkArgument(possibleItemIDs != null, "argument is null");
    Preconditions.checkArgument(estimator != null, "argument is null");

    Queue<RecommendedItem> topItems = new PriorityQueue<RecommendedItem>(howMany + 1,
      Collections.reverseOrder(ByValueRecommendedItemComparator.getInstance()));
    boolean full = false;
    double lowestTopValue = Double.NEGATIVE_INFINITY;
    while (possibleItemIDs.hasNext()) {
      long itemID = possibleItemIDs.next();
      if (rescorer == null || !rescorer.isFiltered(itemID)) {
        double preference;
        try {
          //得到该item的平均得分作为user的预测评分
          preference = estimator.estimate(itemID);
        } catch (NoSuchItemException nsie) {
          continue;
        }
        double rescoredPref = rescorer == null ? preference : rescorer.rescore(itemID, preference);
        if (!Double.isNaN(rescoredPref) && (!full || rescoredPref > lowestTopValue)) {
          topItems.add(new GenericRecommendedItem(itemID, (float) rescoredPref));
          if (full) {
            topItems.poll();
          } else if (topItems.size() > howMany) {
            full = true;
            topItems.poll();
          }
          lowestTopValue = topItems.peek().getValue();
        }
      }
    }
    int size = topItems.size();
    if (size == 0) {
      return Collections.emptyList();
    }
    List<RecommendedItem> result = Lists.newArrayListWithCapacity(size);
    result.addAll(topItems);
    Collections.sort(result, ByValueRecommendedItemComparator.getInstance());
    return result;
  }

 

最后加上几句从别人文章中抄过来的话:

其实Slope one推荐算法很流行,被很多网站使用,包括一些大型网站;我个人认为最主要的原因是它具备如下优势:

        1. 实现简单并且易于维护。

        2. 响应即时(只要用户做出一次评分,它就能有效推荐,根据上面代码很容易理解),并且用户的新增评分对推荐数据的改变量较小,应为在内存中存储的是物品间的平均差值,新增的差值只需累加一下,切范围是用户评分过的产品。

        3. 由于是基于项目的协同过滤算法,适用于当下火热的电子商务网站,原因电子商务网站用户量在几十万到上百万,产品量相对于之则要小得多,所以对产品归类从性能上讲很高效。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(Mahout)