【mahout笔记】初步理解userCF(基于用户的推荐算法)在mahout的实现

昨天尝试在java中搭建了一个mahout的小demo,实现的就是基于用户的推荐算法。代码如下(更多代码和测试数据库)参见前一篇:

public class RecommendTest {
	
	final static int NEIGHBORHOOD_NUM = 2;
    final static int RECOMMENDER_NUM = 3;
 
    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());
        userCF(dataModel);
       // slopeOne(dataModel);
    }
    
    public static void userCF(DataModel dataModel) throws TasteException {
    		UserSimilarity userSimilarity = RecommendFactory.userSimilarity(RecommendFactory.SIMILARITY.EUCLIDEAN, dataModel);
        UserNeighborhood userNeighborhood = RecommendFactory.userNeighborhood(RecommendFactory.NEIGHBORHOOD.NEAREST, userSimilarity, dataModel, NEIGHBORHOOD_NUM);
        RecommenderBuilder recommenderBuilder = RecommendFactory.userRecommender(userSimilarity, userNeighborhood, true);
 
        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);
        }

    }

}

我的数据是存在csv文件里的,也就是如下格式:

1,101,5.0
1,102,3.0
1,103,2.5
2,101,2.0
2,102,2.5
2,103,5.0
2,104,2.0
3,101,2.5
3,104,4.0
3,105,4.5
3,107,5.0
4,101,5.0
4,103,3.0
4,104,4.5
4,106,4.0
5,101,4.0
5,102,3.0
5,103,2.0
5,104,4.0
5,105,3.5
5,106,4.0

每一行为一条记录,列之间用“,”号分隔。

第一列为用户id,第二列为物品id,第三列为评分(float型)

1. 读取数据

我们得到了文件的路径后,使用FileDataModel从File对象中获取数据,语句如下:

DataModel dataModel = new FileDataModel(new File(file));

实际上这条语句的源码如下:

因为FileDataModel实际上是继承了AbstractDataModel类,而AbstractDataModel又是DataModel(接口类)的一个实现,所以我们可以用一个FileDataModel对DataModel赋值。

调用了FileDataModel的单参数构造函数,但是这个构造函数实际上是调用了自己的另一个构造函数,并添加了几个默认函数:

另一个构造函数如下:

从上图可以看出,这个数据读取的方法,对我们传入的数据是有要求的,也就如我们上面说的:

必须是至少有三列,且前两列必须是id,第三列必须是评分。倒是物品id和用户id到底谁第一列谁第二列,要求不高,因为可以通过transpose这个参数来进行调整。

说回正题:这部分代码基本是先校验了一下参数有没有问题,然后取出数据的第一行并判断数据文件是用哪种符号来分隔列的(自然此处是用,),然后对第一行文件做了校验,确定确实有三列(或以上,但有用的也只有前三列)数据,且评分数据不为空,就把hasPrefValues这个值置为true。

然后就进入了reload()部分的函数:

这里用了锁,我猜测这部分是有多线程进行的,但是具体是在哪里我没有看到。。。可能是防止多个文件在同时读取数据。

上面已经说过了FileDataModel是DataModel的其中一个实现的继承,实际上这个FileDataModel的结构如下:

而buildModel()应该是给FileDataModel中的一个DataModel对象赋值。

继续查看buildModel()的代码:

先不考虑评分不存在的情况,我们先看满足三列数据的情况下数据是怎么处理的。很明显,无论是第一次取数据还是追加数据,使用的都是processFile()这个方法和GenericDataModel的构造函数两个方法。如果是processFile(),唯二的区别是如果是:1. 追加数据,我们需要先把已经读出的在delegate里的数据取出然后作为第三个参数传入方法,而第一次取的话则传一个空的容器进去;2. 第四个参数一个是第一次是false,追加是true。如果是GenericDataModel()那差别也就是是空容器还是有数据的data传入的差别。

值得一提的是findUpdateFilesAfter()这个方法,这个方法决定了我们是否需要先调用processFile()方法,看名字的意思是判断是否在我们创建File对象之后文件有更新过,代码如下:

代码很简单,大概就是返回更新过的文件容器。如果更新过则执行processFile():

processFile()方法主要一行一行的对需要读取的文件进行处理,processLine()的代码很长,但是不用详细去看他,这个方法是将

1,101,5.0

这样的一行数据转换成

{1=[GenericPreference[userID: 1, itemID:101, value:5.0]]}

这样的格式。因为是一个用户id对应一个对象数组,所以如果一个用户对应有多条记录,就会持续添加到这个这个数组里,而不会覆盖记录。

{
    1=[GenericPreference[userID: 1, itemID:101, value:5.0], GenericPreference[userID: 1, itemID:102, value:3.0], GenericPreference[userID: 1, itemID:103, value:2.5]],
    2=[GenericPreference[userID: 2, itemID:101, value:2.0], GenericPreference[userID: 2, itemID:102, value:2.5], GenericPreference[userID: 2, itemID:103, value:5.0], GenericPreference[userID: 2, itemID:104, value:2.0]],
    3=[GenericPreference[userID: 3, itemID:101, value:2.5], GenericPreference[userID: 3, itemID:104, value:4.0], GenericPreference[userID: 3, itemID:105, value:4.5], GenericPreference[userID: 3, itemID:107, value:5.0]],
    4=[GenericPreference[userID: 4, itemID:101, value:5.0], GenericPreference[userID: 4, itemID:103, value:3.0], GenericPreference[userID: 4, itemID:104, value:4.5], GenericPreference[userID: 4, itemID:106, value:4.0]],
    5=[GenericPreference[userID: 5, itemID:101, value:4.0], GenericPreference[userID: 5, itemID:102, value:3.0], GenericPreference[userID: 5, itemID:103, value:2.0], GenericPreference[userID: 5, itemID:104, value:4.0], GenericPreference[userID: 5, itemID:105, value:3.5], GenericPreference[userID: 5, itemID:106, value:4.0]]
}

最后我们的数据就会被处理成如上图的形式。

然后当做参数传给GenericDataModel的构造函数:

GenericDataModel同样是继承自AbstractDataModel的子类。他有这些属性(如上图),这个类会收集所有的用户id和物品id,然后基于用户做一个偏好map,基于物品也有一个偏好map。

手机所有物品id数组和物品评分数组生成一个GenericUserPreferenceArray对象,每一个下标的id和评分一一对应。

将传入的data参数的值替换为这个新的对象,也就是一个用户id对应一个GenericUserPreferenceArray对象,即如下:

{
1=GenericUserPreferenceArray[userID:1,{101=5.0,102=3.0,103=2.5}],
2=GenericUserPreferenceArray[userID:2,{101=2.0,102=2.5,103=5.0,104=2.0}],
3=GenericUserPreferenceArray[userID:3,{101=2.5,104=4.0,105=4.5,107=5.0}],
4=GenericUserPreferenceArray[userID:4,{101=5.0,103=3.0,104=4.5,106=4.0}],
5=GenericUserPreferenceArray[userID:5,{101=4.0,102=3.0,103=2.0,104=4.0,105=3.5,106=4.0}]
}

和上面的不同对不对,最后将这个data对象赋值给我们最早提到的delegate(就是那个DataModel对象)的preferenceFromUsers,用同样的方法给preferenceForItems赋值。

这个时候我们就要回到之前的那张图了:

我们刚刚演示完了数据是三列的情况,如果数据小于三列的话,代码如下:

在这里我不详细展开,只说这里是处理只有两列的情况的代码,会增加一个评分是否存在的判空,我觉得是用于处理矩阵拆解后的数据的(是猜的,如果有大神指导可以告诉我),同样是最后整理出一个这样的对象赋值给delegate。

复制完以后解锁。

到这步位置读数据的操作做完了,实际上就是给各种属性赋值并且处理数据放到delegate里面。

2. 生成一个UserSimilarity对象

UserSimilarity userSimilarity = RecommendFactory.userSimilarity(RecommendFactory.SIMILARITY.EUCLIDEAN, dataModel);

RecommendFactory这个类是我们自己写好的(在上一篇文档里有),我们可以生成很多种UserSimilarity对象,我们选择的EUCLIDEAN,也就是欧几里得距离相似度。

简单说一下这一步就是各种赋值生成一个欧几里得相似度(随便翻译的)的对象,没有太多计算的东西。

3. 生成一个用户近邻对象

UserNeighborhood userNeighborhood = RecommendFactory.userNeighborhood(RecommendFactory.NEIGHBORHOOD.NEAREST, userSimilarity, dataModel, NEIGHBORHOOD_NUM);

我们选择的是最近邻。

各种校验和赋值,是不是和之前那个很接近?其实就是生成一个空对象然后赋值属性而已。

4. 生成RecommenderBuilder对象

这次我希望不要继续是各种校验赋值了。。结果毫无区别,一脸血,就是创建对象。顺便说一下的是这里直接用的是默认策略,默认策略是PreferredItemsNeighborhoodCandidateItemsStrategy:

5. 评价这个模型

RecommendFactory.evaluate(RecommendFactory.EVALUATOR.AVERAGE_ABSOLUTE_DIFFERENCE, recommenderBuilder, null, dataModel, 0.7);

本来这行代码我以为是用来计算数据了,结果看了一下好像是在划分各种数据集,然后尝试得到评价评分,这个评分是越小越好。

第一步是根据我们传入的参数,新建了一个评价器对象,如下图:

然后对现有的数据集进行一个划分,大家都知道在机器学习中,我们需要通过训练集训练我们的模型,然后通过交叉验证来判断模型是否可用,最后用测试集来进行测试,在推荐算法里交叉验证这步可以省略,因为我们目前仅有这一种模型,所以并不需要用到交叉验证。

我们传入的trainingPercentage是用来控制数据集分割的百分比,比如我们传入0.7,那么70%为训练集,30%为测试集。但是在实际操作中,代码并不是简单的前70%和剩下部分这样划分,为了让样板更均匀,我们实际上是采用随机数对比的方法。当我们对数据对象(其中有三条记录)进行操作时,

GenericUserPreferenceArray[userID:1,{101=5.0,102=3.0,103=2.5}]

我们依次取出记录,然后随机生成小数,如果这个随机数<0.7则扔入训练集,如果大于等于则归于测试集,并将所有用户的所有记录都进行一边这个操作,这样对于同一用户就会有部分数据在测试集有部分数据在训练集(因为这是基于用户的协同,所以要对比用户之间的相似度,如果一个用户的所有数据都在测试集则起不到效果)。

最后对于用户1的数据的划分如下:

对于用户1来说:
测试集:{1=GenericUserPreferenceArray[userID:1,{102=3.0}]}
训练集:{1=GenericUserPreferenceArray[userID:1,{101=5.0,103=2.5}]}

对于所有数据的划分如下:

训练集:
{
1=GenericUserPreferenceArray[userID:1,{101=5.0,103=2.5}],
2=GenericUserPreferenceArray[userID:2,{102=2.5,103=5.0,104=2.0}],
3=GenericUserPreferenceArray[userID:3,{101=2.5}],
4=GenericUserPreferenceArray[userID:4,{104=4.5,106=4.0}],
5=GenericUserPreferenceArray[userID:5,{101=4.0,102=3.0,103=2.0,104=4.0,106=4.0}]}
测试集:
{
1=GenericUserPreferenceArray[userID:1,{102=3.0}],
2=GenericUserPreferenceArray[userID:2,{101=2.0}],
3=GenericUserPreferenceArray[userID:3,{104=4.0,105=4.5,107=5.0}],
4=GenericUserPreferenceArray[userID:4,{101=5.0,103=3.0}],
5=GenericUserPreferenceArray[userID:5,{105=3.5}]}

随后根据我们分割出的训练集和测试集,生成一个GenericDataModel类的训练模型,其实主要做的工作是:刚才传入的训练集是基于用户做的一个偏好map,所以现在我们要对应的生成训练集的基于物品的偏好map,并对这个类的属性maxPreference,minPreference还有其他属性(如userIds, itemIds)赋值。代码如下:

最后生成了基于用户id的偏好map:

{
1=GenericUserPreferenceArray[userID:1,{101=5.0,103=2.5}],
2=GenericUserPreferenceArray[userID:2,{102=2.5,103=5.0,104=2.0}],
3=GenericUserPreferenceArray[userID:3,{101=2.5}],
4=GenericUserPreferenceArray[userID:4,{104=4.5,106=4.0}],
5=GenericUserPreferenceArray[userID:5,{101=4.0,102=3.0,103=2.0,104=4.0,106=4.0}]}

基于物品id的偏好map:

{
104=GenericItemPreferenceArray[itemID:104,{2=2.0,4=4.5,5=4.0}],
106=GenericItemPreferenceArray[itemID:106,{4=4.0,5=4.0}],
101=GenericItemPreferenceArray[itemID:101,{1=5.0,3=2.5,5=4.0}],
102=GenericItemPreferenceArray[itemID:102,{2=2.5,5=3.0}],
103=GenericItemPreferenceArray[itemID:103,{1=2.5,2=5.0,5=2.0}]}

并基于这个模型生成一个推荐类,中间的过程就是各种属性赋值,看看代码就好让我们略过:

下一步是使用测试集对这个模型进行评价,评价的过程我没有看的很明白,但是中间使用的多线程来提高效率,并且最后会返回一个评价分数result:

具体解析参看:

(Mahout推荐系统引擎RecommenderEvaluator源码解析-https://blog.csdn.net/jianjian1992/article/details/47344367

参考了大神的解析,大概明白这些代码是做了什么事情了,上面的截图基本上是根据trainPercentage划分了训练集和测试集,然后在getEvaluation()方法里挨个计算测试集的预测分数:

计算方法是先判断训练集中有没有已存在的评分,如果有则直接返回真实评分(这真是作弊我想举报),如果没有就先用getUserNeighborhood()来获取对应用户的近似用户。

生成预测器以后用预测器对测试用户进行预测,从所有用户中获取2(默认配置)个最接近的”近邻“

遍历所有的用户(过滤用户自己),并分别计算其他用户与该用户的相似度。

计算相似度的方法看起来复杂,但是实际上非常简单,取出两个用户的评分矩阵,如果训练集中两个矩阵有一个为空矩阵,则直接返回Double.NaN。如果两个矩阵都不为空,则分别取出第一个item的id,如果id相同,则分别取出各自的评分算出各种值和差值,如果各自矩阵的itemId没有取完,则分别取下一个;如果id不同,则保留较大的那个,较小的跳过取下一个Itemid,直到其中一个矩阵的itemid被取空。相似度的计算公式为1/(1+sqrt((x1-y1)^2+(x2-y2)^2+...+(xn-yn)^2))/sqrt(n)。x y分别是不同用户的分值。其实sqrt(n)更像一个正则量,是根据数量多少进行控制的。

 @Override
  public double userSimilarity(long userID1, long userID2) throws TasteException {
    DataModel dataModel = getDataModel();
    PreferenceArray xPrefs = dataModel.getPreferencesFromUser(userID1);
    PreferenceArray yPrefs = dataModel.getPreferencesFromUser(userID2);
    int xLength = xPrefs.length();
    int yLength = yPrefs.length();
    
    if (xLength == 0 || yLength == 0) {
      return Double.NaN;
    }
    
    long xIndex = xPrefs.getItemID(0);
    long yIndex = yPrefs.getItemID(0);
    int xPrefIndex = 0;
    int yPrefIndex = 0;
    
    double sumX = 0.0;
    double sumX2 = 0.0;
    double sumY = 0.0;
    double sumY2 = 0.0;
    double sumXY = 0.0;
    double sumXYdiff2 = 0.0;
    int count = 0;
    
    boolean hasInferrer = inferrer != null;
    boolean hasPrefTransform = prefTransform != null;
    
    while (true) {
      int compare = xIndex < yIndex ? -1 : xIndex > yIndex ? 1 : 0;
      if (hasInferrer || compare == 0) {
        double x;
        double y;
        if (xIndex == yIndex) {
          // Both users expressed a preference for the item
          if (hasPrefTransform) {
            x = prefTransform.getTransformedValue(xPrefs.get(xPrefIndex));
            y = prefTransform.getTransformedValue(yPrefs.get(yPrefIndex));
          } else {
            x = xPrefs.getValue(xPrefIndex);
            y = yPrefs.getValue(yPrefIndex);
          }
        } else {
          // Only one user expressed a preference, but infer the other one's preference and tally
          // as if the other user expressed that preference
          if (compare < 0) {
            // X has a value; infer Y's
            x = hasPrefTransform
                ? prefTransform.getTransformedValue(xPrefs.get(xPrefIndex))
                : xPrefs.getValue(xPrefIndex);
            y = inferrer.inferPreference(userID2, xIndex);
          } else {
            // compare > 0
            // Y has a value; infer X's
            x = inferrer.inferPreference(userID1, yIndex);
            y = hasPrefTransform
                ? prefTransform.getTransformedValue(yPrefs.get(yPrefIndex))
                : yPrefs.getValue(yPrefIndex);
          }
        }
        sumXY += x * y;
        sumX += x;
        sumX2 += x * x;
        sumY += y;
        sumY2 += y * y;
        double diff = x - y;
        sumXYdiff2 += diff * diff;
        count++;
      }
      if (compare <= 0) {
        if (++xPrefIndex >= xLength) {
          if (hasInferrer) {
            // Must count other Ys; pretend next X is far away
            if (yIndex == Long.MAX_VALUE) {
              // ... but stop if both are done!
              break;
            }
            xIndex = Long.MAX_VALUE;
          } else {
            break;
          }
        } else {
          xIndex = xPrefs.getItemID(xPrefIndex);
        }
      }
      if (compare >= 0) {
        if (++yPrefIndex >= yLength) {
          if (hasInferrer) {
            // Must count other Xs; pretend next Y is far away            
            if (xIndex == Long.MAX_VALUE) {
              // ... but stop if both are done!
              break;
            }
            yIndex = Long.MAX_VALUE;
          } else {
            break;
          }
        } else {
          yIndex = yPrefs.getItemID(yPrefIndex);
        }
      }
    }
    
    // "Center" the data. If my math is correct, this'll do it.
    double result;
    if (centerData) {
      double meanX = sumX / count;
      double meanY = sumY / count;
      // double centeredSumXY = sumXY - meanY * sumX - meanX * sumY + n * meanX * meanY;
      double centeredSumXY = sumXY - meanY * sumX;
      // double centeredSumX2 = sumX2 - 2.0 * meanX * sumX + n * meanX * meanX;
      double centeredSumX2 = sumX2 - meanX * sumX;
      // double centeredSumY2 = sumY2 - 2.0 * meanY * sumY + n * meanY * meanY;
      double centeredSumY2 = sumY2 - meanY * sumY;
      result = computeResult(count, centeredSumXY, centeredSumX2, centeredSumY2, sumXYdiff2);
    } else {
      result = computeResult(count, sumXY, sumX2, sumY2, sumXYdiff2);
    }
    
    if (similarityTransform != null) {
      result = similarityTransform.transformSimilarity(userID1, userID2, result);
    }
    
    if (!Double.isNaN(result)) {
      result = normalizeWeightResult(result, count, cachedNumItems);
    }
    return result;
  }

最后取出分值最高(也就是相似度最高的)的两个用户作为相似用户,然后根据这两个用户与该用户的相似度和两个用户分别对物品的评分来预测该用户对物品的评分(如果这两个近邻中,只有一个对该物品评分,则预测作废)。计算公式如下假设用户a对物品的评分为x,相似度为sa, b用户对物品的评分为y,相似度为sb,则测试用户的预测评分公式为, (x*sa+y*sb)/(sa+sb).

如果能计算出预测值,那么将真实值矩阵x和预测值矩阵y放在一起计算,算出真实值和预测值差值的绝对值|x1-y1|,计算所有预测值和真实值差值的平均值。这个平均值就是我们最后得到的评价分数。

因此这个数实际上是越小越好的。

6. 计算准确率和召回率

在详细解读代码之前我先回顾一下准确率和召回率在机器学习中的定义,实际上,我自己都快要忘记他们是什么了。

更详细一点的划分如下图:

对于推荐系统来说,定义可能需要做相应改变:

好了,以上图片均截自其它大神的博客,并非我所独创(地址忘了,就不标明了,侵删)

之后就让我们回到源码本身:

RecommendFactory.statsEvaluator(recommenderBuilder, null, dataModel, 2);

因为我们是预测评分来进行我们的推荐策略的,我们先算出每一个用户对已评价物品的均分和标准差,将两者进行相加,并将这个值作为判断该物品是否真为正相关的一个阈值。评分大于这个值的物品即为正相关,否则即为负相关。然后根据真实的分数查找出和这个用户正相关的物品的个数。取出这个正相关的物品,把数据集中的其他评分作为训练集。假设我们需要获取n个物品,那么我们则需要用户对不少于n*2的物品做出评价,否则数据太少则无法计算准确率和召回率。只有当数据多于这个数的时候,我们才根据已有的训练集,按照之前评价过程中提到过的,获取最相似的2个用户,然后根据这两个用户的相似度,预测该用户所有物品的评分,取出评分最高的2项(这个数量是传入参数控制的)把他们当做预测的正相关项,与真实正相关的进行对比,计算单个用户的准确率和召回率。最后所有用户的准确略和召回率取平均值返回。

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

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

 

7. 真正userCF推荐

其实之前真的花了很多时间在分析怎么读取数据,数据处理的时候是什么样的结构,我们怎么对模型做评价,怎么计算准确率和召回率。到了真正推荐的算法的时候,几乎已经没有什么新的东西了。

多么熟悉的算法,其实这里的代码我们在5和6两步中都已经看过了,第一步是使用现有的数据集得到所有用户的相关性取出最接近的两个用户,然后取出所有该用户没有评价过的商品,最后对用户对这些商品的评分进行预测,取出前两名。

具体的内容在这里就不多做描述了,参见上面的步骤。

8. 总结

实际上,userCF的算法并没有很复杂,步骤就只有2步:1. 根据现有评分计算用户相关度,取出最相似的n个用户;2.根据取出的相似用户对用户没评分的产品预测评分,取出前m个。

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值