基於用戶的協同過濾推薦的基本原理是,根據全部用戶對物品或者信息的偏好,發現與當前用戶口味和偏好類似的「鄰居」用戶羣,在通常的應用中是採用計算「K- 鄰居」的算法;而後,基於這 K 個鄰居的歷史偏好信息,爲當前用戶進行推薦。原理以下圖所示: java
上圖示意出基於用戶的協同過濾推薦機制的基本原理,假設用戶 A 喜歡物品 A,物品 C,用戶 B 喜歡物品 B,用戶 C 喜歡物品 A ,物品 C 和物品 D;從這些用戶的歷史喜愛信息中,咱們能夠發現用戶 A 和用戶 C 的口味和偏好是比較相似的,同時用戶 C 還喜歡物品 D,那麼咱們能夠推斷用戶 A 可能也喜歡物品 D,所以能夠將物品 D 推薦給用戶 A。 算法
要從用戶的行爲和偏好中發現規律,並基於此給予推薦,如何收集用戶的偏好信息成爲系統推薦效果最基礎的決定因素。用戶有不少方式向系統提供本身的偏好信息,並且不一樣的應用也可能大不相同。 app
用戶行爲和用戶偏好表: ide
用戶行爲 | 類型 | 特徵 | 做用 |
評分 | 顯式 | 整數量的偏好,可能的值是[0,n];通常爲5或10 | 可精確獲得用戶的偏好。 |
投票 | 顯式 | 布爾量化的偏好,取值0或1 | 能夠較精確的獲得用戶的偏好。 |
轉發 | 顯式 | 布爾量化的偏好,取值0或1 | 若是是站內,能夠同時推理獲得被轉發人的偏好(不精確) |
保存書籤 | 顯式 | 布爾量化的偏好,取值0或1 | 用戶將該項存爲書籤說明他對這個項目感興趣 |
標記標籤(Tag) | 顯式 | 一些單詞,須要對單詞進行分析,獲得偏好 | 經過分析用戶的標籤,能夠獲得用戶對項目的理解,同時能夠分析出用戶的情感:喜歡仍是討厭 |
評論 | 顯式 | 一段文字,須要進行文本分析,獲得偏好 | 經過分析用戶的評論,能夠獲得用戶的情感:喜歡仍是討厭 |
點擊流(查看) | 隱式 | 一組用戶的點擊,用戶對物品感興趣,須要進行分析,獲得偏好 | 用戶的點擊必定程度上反映了用戶的注意力,因此他也能夠從必定程度上反應用戶的喜愛。 |
頁面停留時間 | 隱式 | 一組時間信息,噪聲大,須要進行去噪,分析,獲得偏好 | 用戶的頁面停留時間必定程度上反映了用戶的注意力和喜愛,但噪聲大,很差利用。 |
購買 | 隱式 | 布爾量化的偏好,取值是0或1 | 用戶購買行爲很明確地說明他對這個項目感興趣。 |
預處理最核心的工做就是減噪和歸一化。
減噪:用戶行爲數據是用戶在使用應用過程當中產生的,它可能存在大量的噪音和用戶的誤操做,咱們能夠經過經典的數據挖掘算法過濾掉行爲數據中的噪音,這樣能夠是咱們的分析更加精確。
歸一化:如前面講到的,在計算用戶對物品的喜愛程度時,可能須要對不一樣的行爲數據進行加權。但能夠想象,不一樣行爲的數據取值可能相差很大,好比,用戶的查看數據必然比購買數據大的多,如何將各個行爲的數據統一在一個相同的取值範圍中,從而使得加權求和獲得的整體喜愛更加精確,就須要咱們進行歸一化處理。最簡單的歸一化處理,就是將各種數據除以此類中的最大值,以保證歸一化後的數據取值在 [0,1] 範圍中。
進行的預處理後,根據不一樣應用的行爲分析方法,能夠選擇分組或者加權處理,以後咱們能夠獲得一個用戶偏好的二維矩陣,一維是用戶列表,另外一維是物品列表,值是用戶對物品的偏好,通常是 [0,1] 或者 [-1, 1] 的浮點數值。 源碼分析
關於類似度的計算,現有的幾種基本方法都是基於向量(Vector)的,其實也就是計算兩個向量的距離,距離越近類似度越大。在推薦的場景中,在用戶 - 物品偏好的二維矩陣中,咱們能夠將一個用戶對全部物品的偏好做爲一個向量來計算用戶之間的類似度,或者將全部用戶對某個物品的偏好做爲一個向量來計算物品之間的類似度。
經常使用的類似度計算方法主要有歐幾里得距離、皮爾遜相關係數、Cosine類似度、Jaccad係數,下面逐一進行介紹: idea
類似鄰居的計算包括固定數量的鄰居和固定類似度門檻的鄰居。
固定數量的鄰居(K-neighborhoods):
不論鄰居的「遠近」,只取最近的 K 個,做爲其鄰居。以下圖 中的 A,假設要計算點 1 的 5- 鄰居,那麼根據點之間的距離,咱們取最近的 5 個點,分別是點 2,點 3,點 4,點 7 和點 5。但很明顯咱們能夠看出,這種方法對於孤立點的計算效果很差,由於要取固定個數的鄰居,當它附近沒有足夠多比較類似的點,就被迫取一些不太類似的點做爲鄰居,這樣就影響了鄰居類似的程度,能夠看出圖中,點 1 和點 5 其實並非很類似。
固定類似度門檻的鄰居(Threshold-based neighborhoods):
與計算固定數量的鄰居的原則不一樣,基於類似度門檻的鄰居計算是對鄰居的遠近進行最大值的限制,落在以當前點爲中心,距離爲 K 的區域中的全部點都做爲當前點的鄰居,這種方法計算獲得的鄰居個數不肯定,但類似度不會出現較大的偏差。下圖中的 B,從點 1 出發,計算類似度在 K 內的鄰居,獲得點 2,點 3,點 4 和點 7,這種方法計算出的鄰居的類似度程度比前一種優,尤爲是對孤立點的處理。
spa
首先來看下GenericUserBasedRecommender的代碼。基於用戶協同過濾的核心算法在doEstimatePreference這個方法中。 code
protected float doEstimatePreference(long theUserID,long[] theNeighborhood, long itemID) throws TasteException { if (theNeighborhood.length == 0) { return Float.NaN; } DataModel dataModel = getDataModel(); double preference = 0.0; double totalSimilarity = 0.0; int count = 0; for (long userID : theNeighborhood) { if (userID != theUserID) { // See GenericItemBasedRecommender.doEstimatePreference() too Float pref = dataModel.getPreferenceValue(userID, itemID); if (pref != null) { double theSimilarity = similarity.userSimilarity(theUserID, userID); if (!Double.isNaN(theSimilarity)) { preference += theSimilarity * pref;//Sum(Su(u_k,u_a)*x_a,m) where x_a,m != null and Su(u_k, u_a) is not NaN totalSimilarity += theSimilarity;//Sum(Su(u_k,u_a)) count++; } } } } if (count <= 1) { return Float.NaN; } float estimate = (float) (preference / totalSimilarity);//Sum(Su(u_k,u_a)*x_a,m)/Sum(Su(u_k, u_a)) if (capper != null) { estimate = capper.capEstimate(estimate);//結果標準化min(x_a,m) <= x_k,m <= max(x_a,m) } return estimate; }
(注:爲了更好的封裝,主要的計算並無在override方法——estimate()中實現,而是在estimate()方法中調用doEstimate()方法,並用protected修飾doEstimate()方法。)
經過上面的代碼能夠看出mahout中User-Based CF算法用的是基本的User-Based CF算法。其數學模型以下: 排序
其中,表示Recommender對user_u,item_k的預測評分,Su(uk,ua)表示uk,ua的類似度。 接口
數學模型:
能夠看出改進的User-Based CF考慮到了不一樣用戶間打分的差別(有的人偏向於打高分,有的人偏向於打低分)於是預測要更爲準確。
源碼以下:
protected float doEstimatePreference(long theUserID, long[] theNeighborhood, long itemID) throws TasteException { if (theNeighborhood.length == 0) { return Float.NaN; } DataModel dataModel = getDataModel(); double preference = 0.0; double totalSimilarity = 0.0; int count = 0; float userAveragePreference = averageUserPreferences(theUserID); for (long userID : theNeighborhood) { if (userID != theUserID) { // See GenericItemBasedRecommender.doEstimatePreference() too Float pref = dataModel.getPreferenceValue(userID, itemID); float neighborhoodAveragePreference = averageUserPreferences(userID); if (pref != null) { double theSimilarity = similarity.userSimilarity(theUserID, userID); if (!Double.isNaN(theSimilarity)) { preference += theSimilarity * (pref - neighborhoodAveragePreference); totalSimilarity += theSimilarity; count++; } } } } if (count <= 1) { return Float.NaN; } float estimate = (float) (preference / totalSimilarity); estimate += userAveragePreference; if (capper != null) { estimate = capper.capEstimate(estimate); } return estimate; } /** * 改進的 User-Based CF算法 * @param userID * @return 用戶平均打分 * @throws TasteException */ protected float averageUserPreferences(long userID) throws TasteException { float averageUserPreferences = 0; float totalUserPreferences = 0; int numUserPreferences = 0; DataModel dataModel = getDataModel(); PreferenceArray userPreferences = dataModel.getPreferencesFromUser(userID); for(int i = 0; i < userPreferences.length(); i++) { float userPreference = userPreferences.getValue(i); if(!Float.isNaN(userPreference)){ totalUserPreferences += userPreference; numUserPreferences++; } } averageUserPreferences = totalUserPreferences / numUserPreferences; return averageUserPreferences; }