Apache Mahout中推薦算法Slope one源碼分析
關于推薦引擎
如今的互聯網中,無論是電子商務還是社交網絡,對數據挖掘的需求都越來越大了,而推薦引擎正是數據挖掘完美體現;通過分析用戶歷史行為,將他可能喜歡內容推送給他,能產生相當好的用戶體驗,這就是推薦引擎。
推薦算法Slope one的原理
首先Slope one是一種基于項目的協同過濾算法(Item-based Recommendation),簡單介紹這種算法(若理解有誤,歡迎大家更正,I am just a beginner):根據用戶們對產品的喜好程度,來將產品分類;舉個簡單例子:比如有10個用戶,其中有9個人即喜歡產品A,也喜歡產品B,但只有2個人喜歡產品C;于是可以推斷產品A和產品B是屬于同類的,而產品C可能跟它們不是一類。
好了話不多講,讓我們看看Slope one吧!
Slope one是通過用戶們對每個產品的評分,來計算產品間的一個差值;這種計算是通過 線性回歸 f(x) = ax + b得到的,其中a = 1,正如它的名字Slope one(斜率為一);另外用戶的評分,在Slope one中是必不可少的。這里舉
例看看它的計算方式:下面是一張用戶對書籍的評分表
|
書 1 |
書 2 |
書 3 |
用戶A |
5 |
3 |
2 |
用戶B |
3 |
4 |
未評分 |
用戶C |
未評分 |
2 |
5 |
書1是否適合推薦給用戶C,需要通過Slope one 計算出一個值來判定:首先得到書1和書2之間的平均差值X = ((5-3)+(3-4))/ 2 = 0.5,然后通過用戶C對書2的打分得到相應的推薦值 2+0.5 = 2.5 (推薦引擎會通過推薦值的高低來選擇要推薦的物品),這里只是通過書2來計算用戶C對書1的推薦值,實際的Slope one算法中若要得到用戶C對書1的推薦值,會把用戶C評分過的所有書按此方法依次對書1(為評分的書)算推薦值,然后取平均值得到,放到表中如下:
(((5-3)+(3-4))/ 2 +2 + (5 - 2)/ 1 + 5 )/ 2 = 5.25
實際應用中你還可以設權值,這里就不深入了。
以上是Slope one的原理,接下來看看它在Mahout中是如何設計與實現的。
Mahout中Slope one的設計思路以及代碼實現
首先我們需要基礎數據,即用戶對產品的評分,這部分數據可以來自數據庫也可以來自文件,Mahout中對此設計了一個簡單的數據庫表,SQL如下:
CREATE TABLE taste_preferences ( user_id BIGINT NOT NULL, item_id BIGINT NOT NULL, preference FLOAT NOT NULL, PRIMARY KEY (user_id, item_id), INDEX (user_id), INDEX (item_id) )
其次,Mahout在啟動時,會對這部分數據進行處理,算出每對產品間的平均評分差值,已Map<ItemId, Map<ItemId, Average>>的數據結構存放在內存中(當然這幫牛人沒有用Java中Map的實現,自己寫了一個叫FastByIDMap的類)。處理基礎數據的計算代碼如下:
1. 首先獲取所有評過分的用戶id (7,而dataModel就是用于存放我上面提到的基礎)
2. 然后依次計算每個用戶評分過的產品間的平均評分差值 (9,具體在processOneUser中實現)
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(); } }
3. 首先取出該用戶所有評分過的項目和評分值(4)
4. 依次計算這些項目間的平均評分差值(6 ~ 26),并存儲在內存中。
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); FastByIDMap<RunningAverage> aMap = averageDiffs.get(itemIDA); if (aMap == null) { aMap = new FastByIDMap<RunningAverage>(); averageDiffs.put(itemIDA, aMap); } for (int j = i + 1; j < length; j++) { // This is a performance-critical block long itemIDB = userPreferences.getItemID(j); 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); } } RunningAverage itemAverage = averageItemPref.get(itemIDA); if (itemAverage == null) { itemAverage = buildRunningAverage(); averageItemPref.put(itemIDA, itemAverage); } itemAverage.addDatum(prefAValue); } return averageCount; }以上是啟動時做的事,而當某個用戶來了,需要為他計算推薦列表時,就快速許多了(是一個空間換時間的思想),下面的方法是某一個用戶對其某一個他未評分過的產品的推薦值,參數UserId:用戶ID;ItemId:為評分的產品ID
1. 再次取出該用戶評分過的所有產品(4):PreferenceArray prefs中保存著ItemID和該用戶對它的評分
2. 取得上一步得到的prefs中的所有物品與itemID代表的物品之間的平均評分差值(5),其中DiffStorage diffStorage
對象中存放中每對產品間的平均評分差值(而上面啟動時的計算都是在MySQLJDBCDiffStorage中實現的,計算后的
值也存于其中,它是DiffStorage接口的實現),所以取得的流程很簡單,這里不貼代碼了
3. 最后就是依次推算評分過的產品到未評分的產品的一個推薦值 = 平均評分差值(兩者間的) + 已評分的分值(用
戶對其中一個評分),然后將這些推薦值取個平均數(7 ~ 37),其中11行判斷是否要考慮權重。
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); int size = prefs.length(); 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; } // If stdev is NaN, then it is because count is 1. Because we're weighting by count, // the weight is already relatively low. We effectively assume stdev is 0.0 here and // that is reasonable enough. Otherwise, dividing by NaN would yield a weight of NaN // and disqualify this pref entirely // (Thanks Daemmon) } 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); } }
Slope one 的源碼已分析完畢。
其實Slope one推薦算法很流行,被很多網站使用,包括一些大型網站;我個人認為最主要的原因是它具備如下優勢:
1. 實現簡單并且易于維護。
2. 響應即時(只要用戶做出一次評分,它就能有效推薦,根據上面代碼很容易理解),并且用戶的新增評分對推薦數據的改變量較小,應為在內存中存儲的是物品間的平均差值,新增的差值只需累加一下,切范圍是用戶評分過的產品。
3. 由于是基于項目的協同過濾算法,適用于當下火熱的電子商務網站,原因電子商務網站用戶量在幾十萬到上百萬,產品量相對于之則要小得多,所以對產品歸類從性能上講很高效。
分析至此,祝大家周末愉快。
參考資料:
1. Slope one http://zh.wikipedia.org/wiki/Slope_one
2. 探索推薦引擎內部的秘密,第 2 部分: 深入推薦引擎相關算法 - 協同過濾
http://www.ibm.com/developerworks/cn/web/1103_zhaoct_recommstudy2/index.html
3. Apache Mahout 源代碼