三、Slope One协同过滤
1、Slope One原理
为了大大减少过适(过拟合)的发生,提升算法简化实现, Slope One 系列易实现的Item-based协同过滤算法被提了出来。本质上,该方法运用更简单形式的回归表达式() 和单一的自由参数,而不是一个项目评分和另一个项目评分间的线性回归 ()。 该自由参数只不过就是两个项目评分间的平均差值。甚至在某些实例当中,它比线性回归的方法更准确[2],而且该算法只需要一半(甚至更少)的存储量。
例:
- User A 对 Item I 评分为1 对Item J.评分为1.5
- User B 对 Item I 评分为2.
- 你认为 User B 会给 Item J 打几分?
- Slope One 的答案是:2.5 (1.5-1+2=2.5).
举个更实际的例子,考虑下表:
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. 由于是基于项目的协同过滤算法,适用于当下火热的电子商务网站,原因电子商务网站用户量在几十万到上百万,产品量相对于之则要小得多,所以对产品归类从性能上讲很高效。