Mahout基于项目的协同过滤算法源码分析(5)--推荐

59 篇文章 0 订阅
19 篇文章 0 订阅

Mahout版本:0.7,hadoop版本:1.0.4,jdk:1.7.0_25 64bit。

接上篇,本篇分析该算法的最后一个job。

在上篇计算共生矩阵的乘积后,接下来又到了一个shouldRunNextPhase的方法了,这个方法里面含有三个prepareJob,但是本次只分析一个,为啥?因为在实战中没有设置filterFile,这里其实是可以设置filterFile的,设置这个参数的作用是什么,用源码中的英文解释就是过滤掉不不关心的用户,那我就会产生疑问了,在上个计算共生矩阵乘积的时候明明是可以设置一个文件,用于过滤掉我们不关心的用户了,这里还要过滤?所以这里不是很明白源码设计的思路。但是,这里是分析算法的,关于过滤,其实也可以这样说,把全部结果分析出来后,然后再进行过滤,全部结果的分析就是算法的大概,而后面进行过滤可做可不做(这种做法和原来设计的还是有点不同的,效率不一样,如果可以在前期过滤掉一些数据,那么后面的计算会加快)。整个系列blog都是分析算法,所以过滤这一块暂时不关心。

这里可以看到是有三个job调用的:

if (filterFile != null) {
        Job itemFiltering = prepareJob(new Path(filterFile), explicitFilterPath, TextInputFormat.class,
                ItemFilterMapper.class, VarLongWritable.class, VarLongWritable.class,
                ItemFilterAsVectorAndPrefsReducer.class, VarIntWritable.class, VectorAndPrefsWritable.class,
                SequenceFileOutputFormat.class);
//extract out the recommendations
      Job aggregateAndRecommend = prepareJob(
              new Path(aggregateAndRecommendInput), outputPath, SequenceFileInputFormat.class,
              PartialMultiplyMapper.class, VarLongWritable.class, PrefAndSimilarityColumnWritable.class,
              AggregateAndRecommendReducer.class, VarLongWritable.class, RecommendedItemsWritable.class,
              TextOutputFormat.class);
if (filterFile != null) {
        setS3SafeCombinedInputPath(aggregateAndRecommend, getTempPath(), partialMultiplyPath, explicitFilterPath);
      }
      setIOSort(aggregateAndRecommend);
      aggregateAndRecommendConf.set(AggregateAndRecommendReducer.ITEMID_INDEX_PATH,
              new Path(prepPath, PreparePreferenceMatrixJob.ITEMID_INDEX).toString());
      aggregateAndRecommendConf.setInt(AggregateAndRecommendReducer.NUM_RECOMMENDATIONS, numRecommendations);
      aggregateAndRecommendConf.setBoolean(BOOLEAN_DATA, booleanData);
      boolean succeeded = aggregateAndRecommend.waitForCompletion(true);
由于filterFile是null,所以这里只考虑第二个job,即aggregateAndRecommend。

这个job是有mapper和reducer的,下面一个个分析:

首先来看下这个job的输入文件,输入文件就是前面计算共生矩阵的输出,如下:

{102={106:0.1497250646352768,105:0.14328432083129883,104:0.12789210677146912,103:0.19754962623119354,102:NaN,101:0.14201472699642181}	[5, 1, 2]	[3.0, 3.0, 2.5], 
103={106:0.14243397116661072,105:0.11208890378475189,104:0.140376016497612,103:NaN,102:0.19754962623119354,101:0.15548737347126007}	[4, 1, 2, 5]	[3.0, 2.5, 5.0, 2.0], 
101={107:0.10275248438119888,106:0.14243397116661072,105:0.11584573984146118,104:0.1601526141166687,103:0.15548737347126007,102:0.14201472699642181,101:NaN}	[5, 1, 4, 2, 3]	[4.0, 5.0, 5.0, 2.0, 2.5], 
106={106:NaN,105:0.14201472699642181,104:0.1818181872367859,103:0.14243397116661072,102:0.1497250646352768,101:0.14243397116661072}	[4, 5]	[4.0, 4.0],
107={101:0.10275248438119888,107:NaN,105:0.22048120200634003,104:0.13472338020801544}	[3]	[5.0], 
104={107:0.13472338020801544,106:0.1818181872367859,105:0.16736577451229095,104:NaN,103:0.140376016497612,102:0.12789210677146912,101:0.1601526141166687}	[4, 2, 5, 3]	[4.5, 2.0, 4.0, 4.0], 
105={107:0.22048120200634003,106:0.14201472699642181,105:NaN,104:0.16736577451229095,103:0.11208890378475189,102:0.14328432083129883,101:0.11584573984146118}	[5, 3]	[3.5, 4.5]}

(1)mapper://PartialMultiplyMapper

(1.1)map:

 protected void map(VarIntWritable key,
                     VectorAndPrefsWritable vectorAndPrefsWritable,
                     Context context) throws IOException, InterruptedException {

    Vector similarityMatrixColumn = vectorAndPrefsWritable.getVector();
    List<Long> userIDs = vectorAndPrefsWritable.getUserIDs();
    List<Float> prefValues = vectorAndPrefsWritable.getValues();

    VarLongWritable userIDWritable = new VarLongWritable();
    PrefAndSimilarityColumnWritable prefAndSimilarityColumn = new PrefAndSimilarityColumnWritable();

    for (int i = 0; i < userIDs.size(); i++) {
      long userID = userIDs.get(i);
      float prefValue = prefValues.get(i);
      if (!Float.isNaN(prefValue)) {
        prefAndSimilarityColumn.set(prefValue, similarityMatrixColumn);
        userIDWritable.set(userID);
        context.write(userIDWritable, prefAndSimilarityColumn);
      }
    }
  }
首先初始化三个变量,分别获得输入的三个值,然后for循环输出,这里可以看到是以userid来进行输出的,比如针对这样的一条输入: {102={106:0.1497250646352768,105:0.14328432083129883,104:0.12789210677146912,103:0.19754962623119354,102:NaN,101:0.14201472699642181}   [5, 1, 2]   [3.0, 3.0, 2.5], 

那么,其输出应该是<key,value>  --> <5,[3.0,[{102={106:0.1497250646352768,105:0.14328432083129883,104:0.12789210677146912,103:0.19754962623119354,102:NaN,101:0.14201472699642181}   [5, 1, 2]   [3.0, 3.0, 2.5], ]]>  、<1,[3.0,[{102={106:0.1497250646352768,105:0.14328432083129883,104:0.12789210677146912,103:0.19754962623119354,102:NaN,101:0.14201472699642181}   [5, 1, 2]   [3.0, 3.0, 2.5], ]]> 、<2,[2.5,[{102={106:0.1497250646352768,105:0.14328432083129883,104:0.12789210677146912,103:0.19754962623119354,102:NaN,101:0.14201472699642181}   [5, 1, 2]   [3.0, 3.0, 2.5], ]]> ,即userIDs中有多少个用户就输出多少条记录。输出格式是:<key,value>  --> <userID,[prefValue,[itemid:simi,itemid:simi,...]]>   。

(2)reducer://AggregateAndRecommendReducer

(2.1)setup:

在setup中初始化了四个变量,recommendationsPerUser,这个在实战中设置的是3;booleanData,这个设置的是false;indexItemIDMap这个是读取ITEMID_INDEX,即第一个job的输出了,即VarIntWritable和VarLongWritable的映射;itemsToRecommendFor,这个是要设置一个itemFile的文件的,由于在实战中没有进行设置,所以这个itemsToRecommendFor就是为null了。
(2.2)reduce:

protected void reduce(VarLongWritable userID,
                        Iterable<PrefAndSimilarityColumnWritable> values,
                        Context context) throws IOException, InterruptedException {
    if (booleanData) {
      reduceBooleanData(userID, values, context);
    } else {
      reduceNonBooleanData(userID, values, context);
    }
  }
这个reduce超简单,但是其实他是调用函数而已,由于booleanData是false,所以调用的是reduceNonBooleanData函数:

private void reduceNonBooleanData(VarLongWritable userID,
                        Iterable<PrefAndSimilarityColumnWritable> values,
                        Context context) throws IOException, InterruptedException {
    /* each entry here is the sum in the numerator of the prediction formula */
    Vector numerators = null;
    /* each entry here is the sum in the denominator of the prediction formula */
    Vector denominators = null;
    /* each entry here is the number of similar items used in the prediction formula */
    Vector numberOfSimilarItemsUsed = new RandomAccessSparseVector(Integer.MAX_VALUE, 100);

    for (PrefAndSimilarityColumnWritable prefAndSimilarityColumn : values) {
      Vector simColumn = prefAndSimilarityColumn.getSimilarityColumn();
      float prefValue = prefAndSimilarityColumn.getPrefValue();
      /* count the number of items used for each prediction */
      Iterator<Vector.Element> usedItemsIterator = simColumn.iterateNonZero();
      while (usedItemsIterator.hasNext()) {
        int itemIDIndex = usedItemsIterator.next().index();
        numberOfSimilarItemsUsed.setQuick(itemIDIndex, numberOfSimilarItemsUsed.getQuick(itemIDIndex) + 1);
      }

      numerators = numerators == null
          ? prefValue == BOOLEAN_PREF_VALUE ? simColumn.clone() : simColumn.times(prefValue)
          : numerators.plus(prefValue == BOOLEAN_PREF_VALUE ? simColumn : simColumn.times(prefValue));

      simColumn.assign(ABSOLUTE_VALUES);
      denominators = denominators == null ? simColumn : denominators.plus(simColumn);
    }

    if (numerators == null) {
      return;
    }

    Vector recommendationVector = new RandomAccessSparseVector(Integer.MAX_VALUE, 100);
    Iterator<Vector.Element> iterator = numerators.iterateNonZero();
    while (iterator.hasNext()) {
      Vector.Element element = iterator.next();
      int itemIDIndex = element.index();
      /* preference estimations must be based on at least 2 datapoints */
      if (numberOfSimilarItemsUsed.getQuick(itemIDIndex) > 1) {
        /* compute normalized prediction */
        double prediction = element.get() / denominators.getQuick(itemIDIndex);
        recommendationVector.setQuick(itemIDIndex, prediction);
      }
    }
    writeRecommendedItems(userID, recommendationVector, context);
  }

这个函数好长呀,要一点点看才行。在前面已经分析其map输出的结果了,这里整合一下,因为在reducer中是把相同的key整合起来的,所以,这里也把相同的key放在一起,方便reducer的分析,这个mapper输出主要是通过log信息打印出来(其实直接分析就可以的,这里图省事,直接设置log进行打印而已):


上面的图中的
userIDWritable:5,prefAndSimilarityColumn:org.apache.mahout.cf.taste.hadoop.item.PrefAndSimilarityColumnWritable@5cb2666c
类似这样的就是最后map的输出结果了,这个结果怎么解读?首先key就是userIDWritable了,后面就是实际值;后面的prefAndSimilarityColumn打印的是一个地址,同时看到不同的用户id输出的地址竟然是一样的?比如针对第一条输出(即上面的输出),其输出其实应该是<key,value>  --> 
<5,[4.0,[107:0.10275248438119888,106:0.14243397116661072,105:0.11584573984146118,104:0.1601526141166687,103:0.15548737347126007,102:0.14201472699642181,101:NaN]]>

而第二条的输出其实是:<key,value>  --> 

<1,[5.0,[107:0.10275248438119888,106:0.14243397116661072,105:0.11584573984146118,104:0.1601526141166687,103:0.15548737347126007,102:0.14201472699642181,101:NaN]]>
通过上面的两条比较发现其实value只有prefValue的值不一样而已,但是为什么地址居然是一样的呢?这个是因为prefAndSimilarityColumn变量是在for循环外面定义好了,所以其地址不会变,同时因为每条数据设置值后直接写入了文件中,不存在后面设置的值会覆盖前面值的情况;整合后的map输出如下所示(只列出了用户1和2的数据):

1:
{[3.0,[106:0.1497250646352768,105:0.14328432083129883,104:0.12789210677146912,103:0.19754962623119354,102:NaN,101:0.14201472699642181]],
[2.5,[106:0.14243397116661072,105:0.11208890378475189,104:0.140376016497612,103:NaN,102:0.19754962623119354,101:0.15548737347126007]],
[5.0,[107:0.10275248438119888,106:0.14243397116661072,105:0.11584573984146118,104:0.1601526141166687,103:0.15548737347126007,102:0.14201472699642181,101:NaN]]
}
2:
{[2.5,[106:0.1497250646352768,105:0.14328432083129883,104:0.12789210677146912,103:0.19754962623119354,102:NaN,101:0.14201472699642181]],
[5.0,[106:0.14243397116661072,105:0.11208890378475189,104:0.140376016497612,103:NaN,102:0.19754962623119354,101:0.15548737347126007]],
[2.0,[107:0.10275248438119888,106:0.14243397116661072,105:0.11584573984146118,104:0.1601526141166687,103:0.15548737347126007,102:0.14201472699642181,101:NaN]],
[2.0,[107:0.13472338020801544,106:0.1818181872367859,105:0.16736577451229095,104:NaN,103:0.140376016497612,102:0.12789210677146912,101:0.1601526141166687]],
}
那么就针对用户1 的数据来进行reduceNonBooleanData的分析吧:

首先初始化三个向量,然后进行for循环,for循环遍历输入的values,这个values就是上面1后面大括号里面的内容,使用foreach进行遍历,首先prefAndSimilarityColumn遍历就会被赋值为[3.0,[106:0.1497250646352768,105:0.14328432083129883,104:0.12789210677146912,103:0.19754962623119354,102:NaN,101:0.14201472699642181]],然后针对上面的变量取出前面的prefValue和后面的similarityVector分别赋值给prefValue、simColumn,看while循环是干嘛的:

Iterator<Vector.Element> usedItemsIterator = simColumn.iterateNonZero();
		      while (usedItemsIterator.hasNext()) {
		        int itemIDIndex = usedItemsIterator.next().index();
		        numberOfSimilarItemsUsed.setQuick(itemIDIndex, numberOfSimilarItemsUsed.getQuick(itemIDIndex) + 1);
		      }
其实就是遍历1后面大括号中的全部similarityVector,如果项目出现一次那么就把numberOfSimilarItemsUsed中相应的项赋值为1次,所以变量numberOfSimilarItemsUsed最后(这里最后是指遍历完用户1的所有values)就会变为下面的样子[107:1.0,106:3.0,105:3.0,104:3.0,103:3.0,102:3.0,101:3.0],因为107出现了一次,所以107对应的位置就设置为1,其他依次类推。

接着是:

numerators = numerators == null
		          ? prefValue == BOOLEAN_PREF_VALUE ? simColumn.clone() : simColumn.times(prefValue)
		          : numerators.plus(prefValue == BOOLEAN_PREF_VALUE ? simColumn : simColumn.times(prefValue));
	
		      simColumn.assign(ABSOLUTE_VALUES);
		      denominators = denominators == null ? simColumn : denominators.plus(simColumn);
三目?好吧,还是两个三目一起用!!!晕。刚开始numerators肯定是null的,那么numerators就会等于prefValue == BOOLEAN_PREF_VALUE ? simColumn.clone() : simColumn.times(prefValue),这个又是一个三目,由于preValue不等于BOOLEAN_PREF_VALUE (等于1.0),所以numerators就等于simColumn.times(prefValue)(其实,这里不用三目吧,如果preValue等于1.0,那么simCoumn.clone()和simColumn.times(prefValue)不是相等的么?),然后等到第二次(即遍历用户1的values中的第二个value值时)numerator就会等于numerators.plus(prefValue == BOOLEAN_PREF_VALUE ? simColumn : simColumn.times(prefValue));,然后preValue又是不等于1.0,所以numerator就会等于原始值加上simColumn.times(preValue)的值,所以当这两个三目运算的作用就是:把每个value中的simi乘以prefValue然后相加。

接着是simColumn.assign(ABSOLUTE_VALUES);,额,好吧,说实话,这个我的确是不知道这个是啥意思,感觉也没啥作用的,并没有对simColumn进行任何的操作,这行代码运行前后simColumn的值并没有改变。

然后就是denominators了,这个也是一个三目,其实前面的两个三目分析后,这个就是小儿科了,这个的意思即是遍历用户1的values然后使用simi乘以prefValue的值全部相加即是变量denominators的值了。

然后for循环就结束了,接下来判断下numerators是否是null,如果是的话直接返回,即说明这个用户没有推荐的项目了,如果不为null,那么就是有推荐的项目,但是要做些处理才能输出,比如把得分最大的输出在第一个等等操作。

接下来的while循环就是求得分的算法了,主要是使用numerators除以denominators中对应的项,得到的值即是每个项目的得分了,但是这里还要使用前面的numberOfSimilarItemsUsed向量进行过滤,如果次数没有大于1,那么这个项目不用计算得分,那就是说这个项目不用输出了,根据上面的数据,用户1的输出如下:

106:3.491611584457462,105:3.4731628623748563,104:3.583812122426105,103:NaN,102:NaN,101:NaN}
其中numerators如下:

107:0.5137624219059944,106:1.5174299776554108,105:1.2893039211630821,104:1.5353794321417809,103:NaN,102:NaN,101:NaN
denominators如下:

107:0.10275248438119888,106:0.43459300696849823,105:0.3712189644575119,104:0.4284207373857498,103:NaN,102:NaN,101:NaN
这里可以看到NaN/NaN =NaN。

然后就是调用函数writeRecommendedItems进行输出了,看这个函数:

private void writeRecommendedItems(VarLongWritable userID, Vector recommendationVector, Context context)
      throws IOException, InterruptedException {

    TopK<RecommendedItem> topKItems = new TopK<RecommendedItem>(recommendationsPerUser, BY_PREFERENCE_VALUE);

    Iterator<Vector.Element> recommendationVectorIterator = recommendationVector.iterateNonZero();
    while (recommendationVectorIterator.hasNext()) {
      Vector.Element element = recommendationVectorIterator.next();
      int index = element.index();
      long itemID;
      if (indexItemIDMap != null && !indexItemIDMap.isEmpty()) {
        itemID = indexItemIDMap.get(index);
      } else { //we don't have any mappings, so just use the original
        itemID = index;
      }
      if (itemsToRecommendFor == null || itemsToRecommendFor.contains(itemID)) {
        float value = (float) element.get();
        if (!Float.isNaN(value)) {
          topKItems.offer(new GenericRecommendedItem(itemID, value));
        }
      }
    }

    if (!topKItems.isEmpty()) {
      context.write(userID, new RecommendedItemsWritable(topKItems.retrieve()));
    }
  }
代码首先定义了一个变量TopKItems,这个变量的输入参数有两个,一个是推荐的用户个数,还有一个是一个Comparator,如下定义:

private static final Comparator<RecommendedItem> BY_PREFERENCE_VALUE =
      new Comparator<RecommendedItem>() {
        @Override
        public int compare(RecommendedItem one, RecommendedItem two) {
          return Floats.compare(one.getValue(), two.getValue());
        }
      };
看到这个Comparator比的是value值,那么就可以想象,比如现在有一对值(项目和得分值)<102,3.3>、<103,3.5>,那么<103,3.5>就应该排在前面了;

看while循环里面就是把前面得到的得分向量加入TopKItems中,每次使用offer函数进行加入,offer函数:

public void offer(T item) {
    if (queue.size() < k) {
      queue.add(item);
    } else if (queueingComparator.compare(item, queue.peek()) > 0) {
      queue.add(item);
      queue.poll();
    }
  }
这里offer函数应该只是把它们加入而已吧,看这些加入后的topKItems变量值:

[RecommendedItem[item:105, value:3.473163], RecommendedItem[item:106, value:3.4916115], RecommendedItem[item:104, value:3.5838122]]
当user为2时,这个变量值是:

RecommendedItem[item:107, value:2.0], RecommendedItem[item:106, value:2.8146582], RecommendedItem[item:105, value:2.7573717]
从上面两个分析可以看到,这个应该是没有顺序的,只是加入了而已。

然后最后输出的时候使用了retrieve函数,看这个函数:

 public List<T> retrieve() {
    List<T> topItems = Lists.newArrayList(queue);
    Collections.sort(topItems, sortingComparator);
    return topItems;
  }
额,好吧,这个在最后才进行排序然后输出了,输出的具体值如下(含用户1和2):
1=[104:3.5838122,106:3.4916115,105:3.473163], 
2=[106:2.8146582,105:2.7573717,107:2.0]
可以看到这个已经是排过序的了,到这里全部的mahout源码基本分析完毕,下篇应该来一篇整体的数据流程才行,使用excel做一个表格上面写上公式,这样应该会便于理解这个算法。

分享,成长,快乐

转载请注明blog地址:http://blog.csdn.net/fansy1990



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值