現在的互聯網中,不管是電子商務仍是社交網絡,對數據挖掘的需求都愈來愈大了,而推薦引擎正是數據挖掘完美體現;經過分析用戶歷史行爲,將他可能喜歡內容推送給他,能產生至關好的用戶體驗,這就是推薦引擎。html
首先Slope one是一種基於項目的協同過濾算法(Item-based Recommendation),簡單介紹這種算法(若理解有誤,歡迎你們更正,I am just a beginner):根據用戶們對產品的喜愛程度,來將產品分類;舉個簡單例子:好比有10個用戶,其中有9我的即喜歡產品A,也喜歡產品B,但只有2我的喜歡產品C;因而能夠推斷產品A和產品B是屬於同類的,而產品C可能跟它們不是一類。java
好了話很少講,讓咱們看看Slope one吧!web
Slope one是經過用戶們對每一個產品的評分,來計算產品間的一個差值;這種計算是經過 線性迴歸算法
f(x) = ax + b到的,其中a = 1,正如它的名字Slope one(斜率爲一);另外用戶的評分,在Slope one中sql
是必不可少的。這裏舉例看看它的計算方式:下面是一張用戶對書籍的評分表數據庫
書 1網絡 |
書 2數據結構 |
書 3oop |
|
用戶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中對此設計了一個簡單的數據庫表,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 源代碼
另:原創blog,轉載請註明http://my.oschina.net/BreathL/blog/41063