【mahout笔记】初步理解slopeOne在mahout的实现

 

参考大神的博客:https//blog.csdn.net/wolvesqun/article/details/52757772

以下为大佬博客对slopeOne的介绍:


这个算法在mahout-0.8版本中,已经被@Deprecated。

SlopeOne是一种简单高效的协同过滤算法。通过均差计算进行评分.SlopeOne论文下载(PDF

1)举例说明:
用户X,Y,Z,对于物品A,B进行打分,如下表,求ž对乙的打分是多少?

slopeone

Slope one算法认为:平均值可以代替某两个未知个体之间的打分差异,事物A对事物B的平均差是:((5 - 4)+(4 - 2))/ 2 = 1.5,就得到Z对B的打分是,3-1.5 = 1.5。

Slope one算法将用户的评分之间的关​​系看作简单的线性关系:

Y = mX + b

2)平均加权计算:
用户X,Y,Z,对于物品A,B,C进行打分,如下表,求ž对甲的打分是多少?

slopeone2

  • 1.计算A和B的平均差,((5-3)+(3-4))/ 2 = 0.5
  • 2.计算A和C的平均差,(5-2)/ 1 = 3
  • 3. Z对A的评分,通过AB得到,2 + 0.5 = 2.5
  • 4. Z对A的评分,通过AC得到,5 + 3 = 8
  • 5.通过加权平均计算Z对A的评分:A和B都有评价的用户数为2,A和C都有评价的用户数为1,权重为别是2和1,(2 * 2.5 + 1 * 8)/(2 + 1)= 13/3 = 4.33

通过这种简单的方式,我们可以快速计算出一个评分项,完成推荐过程!


可以看出这种算法的原理真的非常简单,而且计算速度应该会很快,但是光看这个原理,我觉得计算结果应该不会特别准确_(:з」∠)_。

在前两篇博客中,我从头到尾的分析了ItemCF和userCF两种算法的流程和具体代码,在看源码的过程中,我们可以看出实际上mahout的算法源码实际上是把计算的过程整理成了两个过程,一个是通用的计算流程,比如使用多线程挨个算分,另一个是每个算法不同的逻辑代码通常都分布在推荐器的各个实现类里。所以在这篇博客里,我主要分析代码不同的数据处理和逻辑部分,公共的部分就不再多花时间了。

因为这个算法与用户还有物品的相似度都无关,所以我们并不需要在开始对算法做评价之前生成好相似度的对象,而是只需要生成一个空的斜率推荐器就可以了。

我们先看一下完整的测试代码


 public static void main(String[] args) throws TasteException, IOException {
    		// 读取数据
        String file = "/Users/zjgy/Documents/dataset/item.csv";
        DataModel dataModel = new FileDataModel(new File(file));
        System.out.println(dataModel.toString());
        
        // 推荐算法
        //itemCF(dataModel);
         //userCF(dataModel);
        slopeOne(dataModel);
    }

public static void slopeOne(DataModel dataModel) throws TasteException {
        RecommenderBuilder recommenderBuilder = RecommendFactory.slopeOneRecommender();

        RecommendFactory.evaluate(RecommendFactory.EVALUATOR.AVERAGE_ABSOLUTE_DIFFERENCE, recommenderBuilder, null, dataModel, 0.7);
        RecommendFactory.statsEvaluator(recommenderBuilder, null, dataModel, 2);
 
        LongPrimitiveIterator iter = dataModel.getUserIDs();
        while (iter.hasNext()) {
            long uid = iter.nextLong();
            List list = recommenderBuilder.buildRecommender(dataModel).recommend(uid, RECOMMENDER_NUM);
            RecommendFactory.showItems(uid, list, true);
        }
    }

1.评价

在评价算法里使用随机数来区分训练集和测试集的方式和之前一模一样,就不做评价了。同样对训练数据集生成一个训练集的DataModel的,同样的对数据做处理生成itemIds和用户id,已经基于用户的评分矩阵和基于物品的评分矩阵,这些都不再赘言。

然后初始化一个斜率的推荐器类。

public SlopeOneRecommender(DataModel dataModel) throws TasteException {
    this(dataModel,
         Weighting.WEIGHTED,
         Weighting.WEIGHTED,
         new MemoryDiffStorage(dataModel, Weighting.WEIGHTED, Long.MAX_VALUE));
  }

这一步在之前的两种算法里都只是简单的校验和赋值,但是SlopeOneRecommender推荐器基于自己的算法特性,需要根据传入的DataModel中进行初始化DiffStorage对象。

private final DiffStorage diffStorage;

DiffStorage实际上是一个接口,里面只是定义了一些方法名,而代码实际上是用它的一个实现类MemoryDiffStorage给他赋值。

 public MemoryDiffStorage(DataModel dataModel,
                           Weighting stdDevWeighted,
                           long maxEntries) throws TasteException {
    Preconditions.checkArgument(dataModel != null, "dataModel is null");
    Preconditions.checkArgument(dataModel.getNumItems() >= 1, "dataModel has no items");
    Preconditions.checkArgument(maxEntries > 0L, "maxEntries must be positive");
    this.dataModel = dataModel;
    this.stdDevWeighted = stdDevWeighted == Weighting.WEIGHTED;
    this.maxEntries = maxEntries;
    this.averageDiffs = new FastByIDMap<FastByIDMap<RunningAverage>>();
    this.averageItemPref = new FastByIDMap<RunningAverage>();
    this.buildAverageDiffsLock = new ReentrantReadWriteLock();
    this.allRecommendableItemIDs = new FastIDSet(dataModel.getNumItems());
    this.refreshHelper = new RefreshHelper(new Callable<Object>() {
      @Override
      public Object call() throws TasteException {
        buildAverageDiffs();
        return null;
      }
    });
    refreshHelper.addDependency(dataModel);
    buildAverageDiffs(); // 重点分析这一步
  }

MemoryDiffStorage的构造函数,实际上对属性进行了一些空对象的赋值之后,重点在buildAverageDiffs这一步,会对的DataModel里面存储的评分数据进行处理,形成一个初始化好的数据结构。让我们继续往下看。

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();
    }
  }

代码取出了存放在数据模型里的用户id,也就是所有的用户的ID,然后进行遍历,依次对单个用户进行处理,处理代码如下。

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++) { // 依次取出该用户对单个物品A的评分
      float prefAValue = userPreferences.getValue(i); // 取出当前用户对该物品A的评分
      long itemIDA = userPreferences.getItemID(i); // 取出当前物品A的id
      FastByIDMap<RunningAverage> aMap = averageDiffs.get(itemIDA); // 如果这个物品A已经有对应的RunningAverage对象的FastByIDMap了的话就从this属性中直接取出
      if (aMap == null) { // 如果没有,就新建一个
        aMap = new FastByIDMap<RunningAverage>();
        averageDiffs.put(itemIDA, aMap); // 把这个新建的对象直接放入averageDiffs,因为这是一个对地址的引用,我们后面对amap的所有修改都能在averageDiffs上有所体现
      }
      for (int j = i + 1; j < length; j++) { // 遍历该用户评过分的物品,从该物品A的后一个开始(应该是要计算相对另一物品的均分差值,如原理里提到的那样)
        // This is a performance-critical block
        long itemIDB = userPreferences.getItemID(j); // 取出另一物品B的id
        RunningAverage average = aMap.get(itemIDB); // 如果已有对应记录则取出,没有则新建(实际上虽然我们按照用户挨个计算,但是均值差和单一用户是没有关系的,所以遍历后面的用户的时候可能会取出前面用户存着的计算类)
        if (average == null && averageCount < maxEntries) {
          average = buildRunningAverage();
          aMap.put(itemIDB, average);
          averageCount++;
        }
        if (average != null) {
          average.addDatum(userPreferences.getValue(j) - prefAValue); // 插入的是同一用户对其他物品的评分对应该物品评分(同一用户A和B评分)的差值
        }
      }
      RunningAverage itemAverage = averageItemPref.get(itemIDA); // 为单个物品A算均分,然后如果均分有意义则放入保存起来。
      if (itemAverage == null) {
        itemAverage = buildRunningAverage();
        averageItemPref.put(itemIDA, itemAverage);
      }
      itemAverage.addDatum(prefAValue);
    }
    return averageCount;
  }

其实虽然在上上个代码块中,可以看到代码是以用户为单位挨个处理数据,但是最后处理完的数据是物品对物品的平均差值地图对象,和用户本人是没什么关系的,最后处理完的数据如下:

{
104={105=0.5,NaN,106=-0.25,0.3535533905932738,107=1.0,NaN},
105={107=0.5,NaN},101={104=0.5,1.4142135623730951,105=2.0,NaN,106=-1.0,NaN,107=2.5,NaN,102=0.5,NaN,103=0.25,3.8890872965260113},
102={106=1.0,NaN,103=0.75,2.4748737341529163,104=1.0,NaN},
103={106=2.0,NaN,104=2.0,NaN}
}

然后进入pruneInconsequentialDiffs()方法的代码:

private void pruneInconsequentialDiffs() {
    // Go back and prune inconsequential diffs. "Inconsequential" means, here, only represented by one
    // data point, so possibly unreliable
    Iterator<Map.Entry<Long,FastByIDMap<RunningAverage>>> it1 = averageDiffs.entrySet().iterator();
    while (it1.hasNext()) {
      FastByIDMap<RunningAverage> map = it1.next().getValue();
      Iterator<Map.Entry<Long,RunningAverage>> it2 = map.entrySet().iterator();
      while (it2.hasNext()) {
        RunningAverage average = it2.next().getValue();
        if (average.getCount() <= 1) {
          it2.remove();
        }
      }
      if (map.isEmpty()) {
        it1.remove();
      } else {
        map.rehash();
      }
    }
    averageDiffs.rehash();
  }

这块代码其实蛮简单的就是把只有一条对应关系的数据删除,比只有一个用户同时评价了阿和乙物品,那么这个物品的均分计算起来就不准确,在源码的逻辑上即是不认为有计算价值,所以把键为Aid的List中的B相关的对象移除了。只留下有计算价值的对象,即数据样本至少在2个或以上的。

处理过的数据就变成了这样:

{
    101={103=0.25,3.8890872965260113,104=0.5,1.4142135623730951},
    102={103=0.75,2.4748737341529163},
    104={106=-0.25,0.3535533905932738}
}

如此一来数据缺失变的干净多了,接下来进行updateAllRecommendableItems()方法:

// 简而言之就是遍历averageDiffs的key把所有key存到ids里。
private void updateAllRecommendableItems() throws TasteException {
    FastIDSet ids = new FastIDSet(dataModel.getNumItems());
    for (Map.Entry<Long,FastByIDMap<RunningAverage>> entry : averageDiffs.entrySet()) {
      ids.add(entry.getKey());
      LongPrimitiveIterator it = entry.getValue().keySetIterator();
      while (it.hasNext()) {
        ids.add(it.next());
      }
    }
    allRecommendableItemIDs.clear();
    allRecommendableItemIDs.addAll(ids);
    allRecommendableItemIDs.rehash();
  }

在这一步里把所有数据处理完以后还留在averageDiffs提到的物品ID(一级和二级都算)存放到IDS里,做为可推荐的IDS列表(因为他们是可以被计算的)。

到这一步为止,推荐器就被生成好了,和之前两款推荐器的差别实际上就是在传入数据集并进行构造的过程中,推荐器就已经把物品之间的平均差值计算完成了,这样为之后的推荐是加上是形成了便利,之后进行评价的主体部分实际上与前两种算法没有太大区别:

double result = getEvaluation(testPrefs, recommender);

我们主要讲述一下,slopeOne算法是如何进行估分的:

estimatedPreference = recommender.estimatePreference(testUserID, realPref.getItemID());

也就是这步代码进去之后的内容,因为这部分内容实际上是在SlopeOneRecommender推荐器类里单独实现的,与其他推荐器逻辑不同的部分,当然下面这块虽然也是在推荐器类里的,但是到目前为止的三款推荐器这部分代码完全相同:

public float estimatePreference(long userID, long itemID) throws TasteException {
    DataModel model = getDataModel();
    Float actualPref = model.getPreferenceValue(userID, itemID);
    if (actualPref != null) {
      return actualPref;
    }
    return doEstimatePreference(userID, itemID); // 重点说明
  }

必须详细说明的实际上是doEstimatePreference()方法:

private float doEstimatePreference(long userID, long itemID) throws TasteException {
    double count = 0.0;
    double totalPreference = 0.0;
    PreferenceArray prefs = getDataModel().getPreferencesFromUser(userID); // 取出该用户的评分矩阵
    RunningAverage[] averages = diffStorage.getDiffs(userID, itemID, prefs);  // 取出传入itemId(假设为102)的物品与用户(假设为用户1评价过的物品的对应的平均差值,这个差值是根据训练集数据处理得到的,用户1在此处能拿出的给过分的物品决定不包括102本身,因为102在测试集里,而用户评价给过的物品,不一定每个都与102在averageDiffs对象里有平均差值存储)
// 因为根据上面的代码逻辑,如果没有用户同事给102和某一个物品(假设103)打分,或者只有一个用户同时给102和103打分,那么因为数据太少,我们是把这条对应关系给排除了的。
// average: [0.5(差均值),1.4142135623730951(差值的标准差), null, null] 
    int size = prefs.length();
    for (int i = 0; i < size; i++) {
      RunningAverage averageDiff = averages[i]; // 获取对应物品的averageDiff
      if (averageDiff != null) { // 如上所述,如果两条数据(102和103)没有可用的对应差值就直接跳过。如有,则计算
        double averageDiffValue = averageDiff.getAverage(); // 获取差均值
        if (weighted) { // 是否需要另外计算权值,由参数传入。但是evaluate方法里创建推荐类的时候就是默认是true。
          double weight = averageDiff.getCount(); // 如果需要计算权值,则先获取一共有多少个用户同时对这两个物品进行评分
          if (stdDevWeighted) { // 是否需要用差值标准差来做权值计算,这个也是由参数传入,要不要这个逻辑由个人自己决定。但是evaluate方法里创建推荐类的时候就是默认是true。
            double stdev = ((RunningAverageAndStdDev) averageDiff).getStandardDeviation();
            if (!Double.isNaN(stdev)) {
              weight /= 1.0 + stdev; // weight = weight/(1.0+stdev)
            }
            // If stdev is NaN, then it is because count is 1. Because we're weighting by count, 假如差值的标准差是NaN,那么一定是因为count是1,所以我们就直接用count来计算权重
            // the weight is already relatively low. We effectively assume stdev is 0.0 here and 权重已经是低关联的了。我们假设差值的标准差是0,这样的计算方法也是足够合理的。
            // that is reasonable enough. Otherwise, dividing by NaN would yield a weight of NaN 否则,NaN 会使得权重也为NaN,会让整个计算都变的没有意义。
            // and disqualify this pref entirely
            // (Thanks Daemmon)
          }
          totalPreference += weight * (prefs.getValue(i) + averageDiffValue);  // 用(每个关联物品的差值均值+该用户对关联物品的评分)* 权重 的总和(所有关联物品都算在一起)
          count += weight; // 计算中用到的总权重
        } else { // 如果不用计算权重,就直接每个权重都简单粗暴的定为1
          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); // 总分/总权重
    }
  }

估分的逻辑大致是这样的,我们知道我们要预测的用户(假设为1)对该物品(假设为101)在训练集中的肯定是没有评分的(不然在上上个代码块就会直接返回了),然后我们会拿出训练集中所有用户1评分过的物品,然后到我们之前初始化好的差值均分的地图里进行查找,找到对应的数据也可能找不到)。如果需要用(差值的平方差计算权重,那么就根据权重来,如果不用计算权重,那么默认每一条评分的权重都为1之后的算法就是((PREF1 + DIFF1)*重量1 + ... +(prefn + diffn )*的Weightn)/(重量1 + ... +的Weightn)。这样的算法来计算估分。

之后的评分其实和之前一样是,所有估分和实际分值之间差值绝对值的平均值,再说评分一遍越小越好。

2.准确率和召回率

准确率和召回率计算过程中,公共的逻辑如下:

每个用户依次循环 {

1. 根据trainPercentage先计算出整相关评分的阈值。

2. 取出实际评分中的正相关记录。

3. 划分训练集(实际上每个用户的训练集就是除了他的正相关记录以外的他的评分和其他人的所有记录)

4. 按照训练集对训练集对该用户没有评分过的物品进行估分,并进行推荐(取评分最高N个)

5. 假设推荐数据即是预测正相关,用计算公式计算单个用户的准确率和召回率

}

6. 计算总体的准确率和召回率


按照上述的过程中,只有估分阶段是按照每个算法的特定逻辑来的,我们先看一下这套公共逻辑的源码:

@Override
  public IRStatistics evaluate(RecommenderBuilder recommenderBuilder,
                               DataModelBuilder dataModelBuilder,
                               DataModel dataModel,
                               IDRescorer rescorer,
                               int at,
                               double relevanceThreshold,
                               double evaluationPercentage) throws TasteException {

    Preconditions.checkArgument(recommenderBuilder != null, "recommenderBuilder is null");
    Preconditions.checkArgument(dataModel != null, "dataModel is null");
    Preconditions.checkArgument(at >= 1, "at must be at least 1");
    Preconditions.checkArgument(evaluationPercentage > 0.0 && evaluationPercentage <= 1.0,
      "Invalid evaluationPercentage: %s", evaluationPercentage);

    int numItems = dataModel.getNumItems();
    RunningAverage precision = new FullRunningAverage();
    RunningAverage recall = new FullRunningAverage();
    RunningAverage fallOut = new FullRunningAverage();
    RunningAverage nDCG = new FullRunningAverage();
    int numUsersRecommendedFor = 0;
    int numUsersWithRecommendations = 0;

   
    LongPrimitiveIterator it = dataModel.getUserIDs();
    while (it.hasNext()) { // 对每个用户进行遍历

      long userID = it.nextLong();

      if (random.nextDouble() >= evaluationPercentage) {
        // Skipped
        continue;
      }

      long start = System.currentTimeMillis();

      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;
      }

    // 划分该用户的训练集(该用户非正相关数据+其他用户所有数据)
      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);
      try {
        trainingModel.getPreferencesFromUser(userID);
      } catch (NoSuchUserException nsee) {
        continue; // Oops we excluded all prefs for the user -- just move on
      }

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

    // 用特定的算法进行估分推荐,取估分最高前N个
      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++;
        }
      }

      int numRecommendedItems = recommendedItems.size();

      // Precision 计算单个用户的准确率
      if (numRecommendedItems > 0) {
        precision.addDatum((double) intersectionSize / (double) numRecommendedItems);
      }

      // Recall 计算单个用户的召回率
      recall.addDatum((double) intersectionSize / (double) numRelevantItems);

      // Fall-out
      if (numRelevantItems < size) {
        fallOut.addDatum((double) (numRecommendedItems - intersectionSize)
                         / (double) (numItems - numRelevantItems));
      }

      // 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 < recommendedItems.size(); i++) {
        RecommendedItem item = recommendedItems.get(i);
        double discount = i == 0 ? 1.0 : 1.0 / log2(i + 1);
        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 < relevantItemIDs.size()) {
          idealizedGain += discount;
        }
      }
      nDCG.addDatum(cumulativeGain / idealizedGain);
      
      // Reach
      numUsersRecommendedFor++;
      if (numRecommendedItems > 0) {
        numUsersWithRecommendations++;
      }

      long end = System.currentTimeMillis();

      log.info("Evaluated with user {} in {}ms", userID, end - start);
      log.info("Precision/recall/fall-out/nDCG: {} / {} / {} / {}", new Object[] {
          precision.getAverage(), recall.getAverage(), fallOut.getAverage(), nDCG.getAverage()
      });
    }

    double reach = (double) numUsersWithRecommendations / (double) numUsersRecommendedFor;

      // 返回
    return new IRStatisticsImpl(
        precision.getAverage(),
        recall.getAverage(),
        fallOut.getAverage(),
        nDCG.getAverage(),
        reach);
  }

如公共逻辑如上图,特殊的逻辑只有初始化推荐其和估分这一节。

初始化推荐器我们实际上在评价的过程中已经对构建的过程做了分析,实际上估分这一过程,我们在评价的环节中也做了详细描述。

@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);

    // 获取所有该用户没有评价过的物品
    FastIDSet possibleItemIDs = diffStorage.getRecommendableItemIDs(userID);

    TopItems.Estimator<Long> estimator = new Estimator(userID);

    // 获取分支最高的n个物品(n作为参数传入)
    List<RecommendedItem> topItems = TopItems.getTopItems(howMany, possibleItemIDs.iterator(), rescorer,
      estimator);

    log.debug("Recommendations are: {}", topItems);
    return topItems;
  }
 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 {
          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;
  }

这一步和之前所有的推荐公用逻辑是一样,不一样的是估分的过程,让我们再回顾一遍:

private float doEstimatePreference(long userID, long itemID) throws TasteException {
    double count = 0.0;
    double totalPreference = 0.0;
    PreferenceArray prefs = getDataModel().getPreferencesFromUser(userID); // 取出该用户的评分矩阵
    RunningAverage[] averages = diffStorage.getDiffs(userID, itemID, prefs);  // 取出传入itemId(假设为102)的物品与用户(假设为用户1评价过的物品的对应的平均差值,这个差值是根据训练集数据处理得到的,用户1在此处能拿出的给过分的物品决定不包括102本身,因为102在测试集里,而用户评价给过的物品,不一定每个都与102在averageDiffs对象里有平均差值存储)
// 因为根据上面的代码逻辑,如果没有用户同事给102和某一个物品(假设103)打分,或者只有一个用户同时给102和103打分,那么因为数据太少,我们是把这条对应关系给排除了的。
// average: [0.5(差均值),1.4142135623730951(差值的标准差), null, null] 
    int size = prefs.length();
    for (int i = 0; i < size; i++) {
      RunningAverage averageDiff = averages[i]; // 获取对应物品的averageDiff
      if (averageDiff != null) { // 如上所述,如果两条数据(102和103)没有可用的对应差值就直接跳过。如有,则计算
        double averageDiffValue = averageDiff.getAverage(); // 获取差均值
        if (weighted) { // 是否需要另外计算权值,由参数传入。但是evaluate方法里创建推荐类的时候就是默认是true。
          double weight = averageDiff.getCount(); // 如果需要计算权值,则先获取一共有多少个用户同时对这两个物品进行评分
          if (stdDevWeighted) { // 是否需要用差值标准差来做权值计算,这个也是由参数传入,要不要这个逻辑由个人自己决定。但是evaluate方法里创建推荐类的时候就是默认是true。
            double stdev = ((RunningAverageAndStdDev) averageDiff).getStandardDeviation();
            if (!Double.isNaN(stdev)) {
              weight /= 1.0 + stdev; // weight = weight/(1.0+stdev)
            }
            // If stdev is NaN, then it is because count is 1. Because we're weighting by count, 假如差值的标准差是NaN,那么一定是因为count是1,所以我们就直接用count来计算权重
            // the weight is already relatively low. We effectively assume stdev is 0.0 here and 权重已经是低关联的了。我们假设差值的标准差是0,这样的计算方法也是足够合理的。
            // that is reasonable enough. Otherwise, dividing by NaN would yield a weight of NaN 否则,NaN 会使得权重也为NaN,会让整个计算都变的没有意义。
            // and disqualify this pref entirely
            // (Thanks Daemmon)
          }
          totalPreference += weight * (prefs.getValue(i) + averageDiffValue);  // 用(每个关联物品的差值均值+该用户对关联物品的评分)* 权重 的总和(所有关联物品都算在一起)
          count += weight; // 计算中用到的总权重
        } else { // 如果不用计算权重,就直接每个权重都简单粗暴的定为1
          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); // 总分/总权重
    }
  }

对估好的分数列表进行排序,去分值最高的n个,将这N个作为该用户预测的正相关数据,与之前获取的正相关数据进行对比计算。

完成公共计算部分以后,得出整个模型的准确率和召回了。

AVERAGE_ABSOLUTE_DIFFERENCE Evaluater Score:1.0416666269302368
Recommender IR Evaluator: [Precision:0.25,Recall:0.5]

必须说明的是,这个数据真的如我所料是这三个算法中准确率最低,分值最差的一个算法了。但是他简单并且快速。

3. 正式推荐逻辑

再一次没有任何新代码可以说,只是把计算准确率和召回率的收中间的推荐代码单独提出来,并且将所有数据作为训练集。先计算物品之间的平均差值,然后根据差值计算估分,取每个用户的估分前n个,作为推荐内容。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值