1. 程式人生 > >【mahout筆記】初步理解userCF(基於使用者的推薦演算法)在mahout的實現

【mahout筆記】初步理解userCF(基於使用者的推薦演算法)在mahout的實現

昨天嘗試在java中搭建了一個mahout的小demo,實現的就是基於使用者的推薦演算法。程式碼如下(更多程式碼和測試資料庫)參見前一篇:

public class RecommendTest {
	
	final static int NEIGHBORHOOD_NUM = 2;
    final static int RECOMMENDER_NUM = 3;
 
    public static void main(String[] args) throws TasteException, IOException {
        String file = "/Users/zjgy/Documents/dataset/item.csv";
        DataModel dataModel = new FileDataModel(new File(file));
        System.out.println(dataModel.toString());
        userCF(dataModel);
       // slopeOne(dataModel);
    }
    
    public static void userCF(DataModel dataModel) throws TasteException {
    		UserSimilarity userSimilarity = RecommendFactory.userSimilarity(RecommendFactory.SIMILARITY.EUCLIDEAN, dataModel);
        UserNeighborhood userNeighborhood = RecommendFactory.userNeighborhood(RecommendFactory.NEIGHBORHOOD.NEAREST, userSimilarity, dataModel, NEIGHBORHOOD_NUM);
        RecommenderBuilder recommenderBuilder = RecommendFactory.userRecommender(userSimilarity, userNeighborhood, true);
 
        RecommendFactory.evaluate(RecommendFactory.EVALUATOR.AVERAGE_ABSOLUTE_DIFFERENCE, recommenderBuilder, null, dataModel, 0.7);
        RecommendFactory.statsEvaluator(recommenderBuilder, null, dataModel, 2);
 
        LongPrimitiveIterator iter = dataModel.getUserIDs();
        while (iter.hasNext()) {
            long uid = iter.nextLong();
            List list = recommenderBuilder.buildRecommender(dataModel).recommend(uid, RECOMMENDER_NUM);
            RecommendFactory.showItems(uid, list, true);
        }

    }

}

我的資料是存在csv檔案裡的,也就是如下格式:

1,101,5.0
1,102,3.0
1,103,2.5
2,101,2.0
2,102,2.5
2,103,5.0
2,104,2.0
3,101,2.5
3,104,4.0
3,105,4.5
3,107,5.0
4,101,5.0
4,103,3.0
4,104,4.5
4,106,4.0
5,101,4.0
5,102,3.0
5,103,2.0
5,104,4.0
5,105,3.5
5,106,4.0

每一行為一條記錄,列之間用“,”號分隔。

第一列為使用者id,第二列為物品id,第三列為評分(float型)

1. 讀取資料

我們得到了檔案的路徑後,使用FileDataModel從File物件中獲取資料,語句如下:

DataModel dataModel = new FileDataModel(new File(file));

實際上這條語句的原始碼如下:

因為FileDataModel實際上是繼承了AbstractDataModel類,而AbstractDataModel又是DataModel(介面類)的一個實現,所以我們可以用一個FileDataModel對DataModel賦值。

呼叫了FileDataModel的單引數建構函式,但是這個建構函式實際上是呼叫了自己的另一個建構函式,並添加了幾個預設函式:

另一個建構函式如下:

從上圖可以看出,這個資料讀取的方法,對我們傳入的資料是有要求的,也就如我們上面說的:

必須是至少有三列,且前兩列必須是id,第三列必須是評分。倒是物品id和使用者id到底誰第一列誰第二列,要求不高,因為可以通過transpose這個引數來進行調整。

說回正題:這部分程式碼基本是先校驗了一下引數有沒有問題,然後取出資料的第一行並判斷資料檔案是用哪種符號來分隔列的(自然此處是用,),然後對第一行檔案做了校驗,確定確實有三列(或以上,但有用的也只有前三列)資料,且評分資料不為空,就把hasPrefValues這個值置為true。

然後就進入了reload()部分的函式:

這裡用了鎖,我猜測這部分是有多執行緒進行的,但是具體是在哪裡我沒有看到。。。可能是防止多個檔案在同時讀取資料。

上面已經說過了FileDataModel是DataModel的其中一個實現的繼承,實際上這個FileDataModel的結構如下:

而buildModel()應該是給FileDataModel中的一個DataModel物件賦值。

繼續檢視buildModel()的程式碼:

先不考慮評分不存在的情況,我們先看滿足三列資料的情況下資料是怎麼處理的。很明顯,無論是第一次取資料還是追加資料,使用的都是processFile()這個方法和GenericDataModel的建構函式兩個方法。如果是processFile(),唯二的區別是如果是:1. 追加資料,我們需要先把已經讀出的在delegate裡的資料取出然後作為第三個引數傳入方法,而第一次取的話則傳一個空的容器進去;2. 第四個引數一個是第一次是false,追加是true。如果是GenericDataModel()那差別也就是是空容器還是有資料的data傳入的差別。

值得一提的是findUpdateFilesAfter()這個方法,這個方法決定了我們是否需要先呼叫processFile()方法,看名字的意思是判斷是否在我們建立File物件之後檔案有更新過,程式碼如下:

程式碼很簡單,大概就是返回更新過的檔案容器。如果更新過則執行processFile():

processFile()方法主要一行一行的對需要讀取的檔案進行處理,processLine()的程式碼很長,但是不用詳細去看他,這個方法是將

1,101,5.0

這樣的一行資料轉換成

{1=[GenericPreference[userID: 1, itemID:101, value:5.0]]}

這樣的格式。因為是一個使用者id對應一個物件陣列,所以如果一個使用者對應有多條記錄,就會持續新增到這個這個數組裡,而不會覆蓋記錄。

{
    1=[GenericPreference[userID: 1, itemID:101, value:5.0], GenericPreference[userID: 1, itemID:102, value:3.0], GenericPreference[userID: 1, itemID:103, value:2.5]],
    2=[GenericPreference[userID: 2, itemID:101, value:2.0], GenericPreference[userID: 2, itemID:102, value:2.5], GenericPreference[userID: 2, itemID:103, value:5.0], GenericPreference[userID: 2, itemID:104, value:2.0]],
    3=[GenericPreference[userID: 3, itemID:101, value:2.5], GenericPreference[userID: 3, itemID:104, value:4.0], GenericPreference[userID: 3, itemID:105, value:4.5], GenericPreference[userID: 3, itemID:107, value:5.0]],
    4=[GenericPreference[userID: 4, itemID:101, value:5.0], GenericPreference[userID: 4, itemID:103, value:3.0], GenericPreference[userID: 4, itemID:104, value:4.5], GenericPreference[userID: 4, itemID:106, value:4.0]],
    5=[GenericPreference[userID: 5, itemID:101, value:4.0], GenericPreference[userID: 5, itemID:102, value:3.0], GenericPreference[userID: 5, itemID:103, value:2.0], GenericPreference[userID: 5, itemID:104, value:4.0], GenericPreference[userID: 5, itemID:105, value:3.5], GenericPreference[userID: 5, itemID:106, value:4.0]]
}

最後我們的資料就會被處理成如上圖的形式。

然後當做引數傳給GenericDataModel的建構函式:

GenericDataModel同樣是繼承自AbstractDataModel的子類。他有這些屬性(如上圖),這個類會收集所有的使用者id和物品id,然後基於使用者做一個偏好map,基於物品也有一個偏好map。

手機所有物品id陣列和物品評分陣列生成一個GenericUserPreferenceArray物件,每一個下標的id和評分一一對應。

將傳入的data引數的值替換為這個新的物件,也就是一個使用者id對應一個GenericUserPreferenceArray物件,即如下:

{
1=GenericUserPreferenceArray[userID:1,{101=5.0,102=3.0,103=2.5}],
2=GenericUserPreferenceArray[userID:2,{101=2.0,102=2.5,103=5.0,104=2.0}],
3=GenericUserPreferenceArray[userID:3,{101=2.5,104=4.0,105=4.5,107=5.0}],
4=GenericUserPreferenceArray[userID:4,{101=5.0,103=3.0,104=4.5,106=4.0}],
5=GenericUserPreferenceArray[userID:5,{101=4.0,102=3.0,103=2.0,104=4.0,105=3.5,106=4.0}]
}

和上面的不同對不對,最後將這個data物件賦值給我們最早提到的delegate(就是那個DataModel物件)的preferenceFromUsers,用同樣的方法給preferenceForItems賦值。

這個時候我們就要回到之前的那張圖了:

我們剛剛演示完了資料是三列的情況,如果資料小於三列的話,程式碼如下:

在這裡我不詳細展開,只說這裡是處理只有兩列的情況的程式碼,會增加一個評分是否存在的判空,我覺得是用於處理矩陣拆解後的資料的(是猜的,如果有大神指導可以告訴我),同樣是最後整理出一個這樣的物件賦值給delegate。

複製完以後解鎖。

到這步位置讀資料的操作做完了,實際上就是給各種屬性賦值並且處理資料放到delegate裡面。

2. 生成一個UserSimilarity物件

UserSimilarity userSimilarity = RecommendFactory.userSimilarity(RecommendFactory.SIMILARITY.EUCLIDEAN, dataModel);

RecommendFactory這個類是我們自己寫好的(在上一篇文件裡有),我們可以生成很多種UserSimilarity物件,我們選擇的EUCLIDEAN,也就是歐幾里得距離相似度。

簡單說一下這一步就是各種賦值生成一個歐幾里得相似度(隨便翻譯的)的物件,沒有太多計算的東西。

3. 生成一個使用者近鄰物件

UserNeighborhood userNeighborhood = RecommendFactory.userNeighborhood(RecommendFactory.NEIGHBORHOOD.NEAREST, userSimilarity, dataModel, NEIGHBORHOOD_NUM);

我們選擇的是最近鄰。

各種校驗和賦值,是不是和之前那個很接近?其實就是生成一個空物件然後賦值屬性而已。

4. 生成RecommenderBuilder物件

這次我希望不要繼續是各種校驗賦值了。。結果毫無區別,一臉血,就是建立物件。順便說一下的是這裡直接用的是預設策略,預設策略是PreferredItemsNeighborhoodCandidateItemsStrategy:

5. 評價這個模型

RecommendFactory.evaluate(RecommendFactory.EVALUATOR.AVERAGE_ABSOLUTE_DIFFERENCE, recommenderBuilder, null, dataModel, 0.7);

本來這行程式碼我以為是用來計算資料了,結果看了一下好像是在劃分各種資料集,然後嘗試得到評價評分,這個評分是越小越好。

第一步是根據我們傳入的引數,新建了一個評價器物件,如下圖:

然後對現有的資料集進行一個劃分,大家都知道在機器學習中,我們需要通過訓練集訓練我們的模型,然後通過交叉驗證來判斷模型是否可用,最後用測試集來進行測試,在推薦演算法裡交叉驗證這步可以省略,因為我們目前僅有這一種模型,所以並不需要用到交叉驗證。

我們傳入的trainingPercentage是用來控制資料集分割的百分比,比如我們傳入0.7,那麼70%為訓練集,30%為測試集。但是在實際操作中,程式碼並不是簡單的前70%和剩下部分這樣劃分,為了讓樣板更均勻,我們實際上是採用隨機數對比的方法。當我們對資料物件(其中有三條記錄)進行操作時,

GenericUserPreferenceArray[userID:1,{101=5.0,102=3.0,103=2.5}]

我們依次取出記錄,然後隨機生成小數,如果這個隨機數<0.7則扔入訓練集,如果大於等於則歸於測試集,並將所有使用者的所有記錄都進行一邊這個操作,這樣對於同一使用者就會有部分資料在測試集有部分資料在訓練集(因為這是基於使用者的協同,所以要對比使用者之間的相似度,如果一個使用者的所有資料都在測試集則起不到效果)。

最後對於使用者1的資料的劃分如下:

對於使用者1來說:
測試集:{1=GenericUserPreferenceArray[userID:1,{102=3.0}]}
訓練集:{1=GenericUserPreferenceArray[userID:1,{101=5.0,103=2.5}]}

對於所有資料的劃分如下:

訓練集:
{
1=GenericUserPreferenceArray[userID:1,{101=5.0,103=2.5}],
2=GenericUserPreferenceArray[userID:2,{102=2.5,103=5.0,104=2.0}],
3=GenericUserPreferenceArray[userID:3,{101=2.5}],
4=GenericUserPreferenceArray[userID:4,{104=4.5,106=4.0}],
5=GenericUserPreferenceArray[userID:5,{101=4.0,102=3.0,103=2.0,104=4.0,106=4.0}]}
測試集:
{
1=GenericUserPreferenceArray[userID:1,{102=3.0}],
2=GenericUserPreferenceArray[userID:2,{101=2.0}],
3=GenericUserPreferenceArray[userID:3,{104=4.0,105=4.5,107=5.0}],
4=GenericUserPreferenceArray[userID:4,{101=5.0,103=3.0}],
5=GenericUserPreferenceArray[userID:5,{105=3.5}]}

隨後根據我們分割出的訓練集和測試集,生成一個GenericDataModel類的訓練模型,其實主要做的工作是:剛才傳入的訓練集是基於使用者做的一個偏好map,所以現在我們要對應的生成訓練集的基於物品的偏好map,並對這個類的屬性maxPreference,minPreference還有其他屬性(如userIds, itemIds)賦值。程式碼如下:

最後生成了基於使用者id的偏好map:

{
1=GenericUserPreferenceArray[userID:1,{101=5.0,103=2.5}],
2=GenericUserPreferenceArray[userID:2,{102=2.5,103=5.0,104=2.0}],
3=GenericUserPreferenceArray[userID:3,{101=2.5}],
4=GenericUserPreferenceArray[userID:4,{104=4.5,106=4.0}],
5=GenericUserPreferenceArray[userID:5,{101=4.0,102=3.0,103=2.0,104=4.0,106=4.0}]}

基於物品id的偏好map:

{
104=GenericItemPreferenceArray[itemID:104,{2=2.0,4=4.5,5=4.0}],
106=GenericItemPreferenceArray[itemID:106,{4=4.0,5=4.0}],
101=GenericItemPreferenceArray[itemID:101,{1=5.0,3=2.5,5=4.0}],
102=GenericItemPreferenceArray[itemID:102,{2=2.5,5=3.0}],
103=GenericItemPreferenceArray[itemID:103,{1=2.5,2=5.0,5=2.0}]}

並基於這個模型生成一個推薦類,中間的過程就是各種屬性賦值,看看程式碼就好讓我們略過:

下一步是使用測試集對這個模型進行評價,評價的過程我沒有看的很明白,但是中間使用的多執行緒來提高效率,並且最後會返回一個評價分數result:

具體解析參看:

參考了大神的解析,大概明白這些程式碼是做了什麼事情了,上面的截圖基本上是根據trainPercentage劃分了訓練集和測試集,然後在getEvaluation()方法裡挨個計算測試集的預測分數:

計算方法是先判斷訓練集中有沒有已存在的評分,如果有則直接返回真實評分(這真是作弊我想舉報),如果沒有就先用getUserNeighborhood()來獲取對應使用者的近似使用者。

生成預測器以後用預測器對測試使用者進行預測,從所有使用者中獲取2(預設配置)個最接近的”近鄰“

遍歷所有的使用者(過濾使用者自己),並分別計算其他使用者與該使用者的相似度。

計算相似度的方法看起來複雜,但是實際上非常簡單,取出兩個使用者的評分矩陣,如果訓練集中兩個矩陣有一個為空矩陣,則直接返回Double.NaN。如果兩個矩陣都不為空,則分別取出第一個item的id,如果id相同,則分別取出各自的評分算出各種值和差值,如果各自矩陣的itemId沒有取完,則分別取下一個;如果id不同,則保留較大的那個,較小的跳過取下一個Itemid,直到其中一個矩陣的itemid被取空。相似度的計算公式為1/(1+sqrt((x1-y1)^2+(x2-y2)^2+...+(xn-yn)^2))/sqrt(n)。x y分別是不同使用者的分值。其實sqrt(n)更像一個正則量,是根據數量多少進行控制的。

 @Override
  public double userSimilarity(long userID1, long userID2) throws TasteException {
    DataModel dataModel = getDataModel();
    PreferenceArray xPrefs = dataModel.getPreferencesFromUser(userID1);
    PreferenceArray yPrefs = dataModel.getPreferencesFromUser(userID2);
    int xLength = xPrefs.length();
    int yLength = yPrefs.length();
    
    if (xLength == 0 || yLength == 0) {
      return Double.NaN;
    }
    
    long xIndex = xPrefs.getItemID(0);
    long yIndex = yPrefs.getItemID(0);
    int xPrefIndex = 0;
    int yPrefIndex = 0;
    
    double sumX = 0.0;
    double sumX2 = 0.0;
    double sumY = 0.0;
    double sumY2 = 0.0;
    double sumXY = 0.0;
    double sumXYdiff2 = 0.0;
    int count = 0;
    
    boolean hasInferrer = inferrer != null;
    boolean hasPrefTransform = prefTransform != null;
    
    while (true) {
      int compare = xIndex < yIndex ? -1 : xIndex > yIndex ? 1 : 0;
      if (hasInferrer || compare == 0) {
        double x;
        double y;
        if (xIndex == yIndex) {
          // Both users expressed a preference for the item
          if (hasPrefTransform) {
            x = prefTransform.getTransformedValue(xPrefs.get(xPrefIndex));
            y = prefTransform.getTransformedValue(yPrefs.get(yPrefIndex));
          } else {
            x = xPrefs.getValue(xPrefIndex);
            y = yPrefs.getValue(yPrefIndex);
          }
        } else {
          // Only one user expressed a preference, but infer the other one's preference and tally
          // as if the other user expressed that preference
          if (compare < 0) {
            // X has a value; infer Y's
            x = hasPrefTransform
                ? prefTransform.getTransformedValue(xPrefs.get(xPrefIndex))
                : xPrefs.getValue(xPrefIndex);
            y = inferrer.inferPreference(userID2, xIndex);
          } else {
            // compare > 0
            // Y has a value; infer X's
            x = inferrer.inferPreference(userID1, yIndex);
            y = hasPrefTransform
                ? prefTransform.getTransformedValue(yPrefs.get(yPrefIndex))
                : yPrefs.getValue(yPrefIndex);
          }
        }
        sumXY += x * y;
        sumX += x;
        sumX2 += x * x;
        sumY += y;
        sumY2 += y * y;
        double diff = x - y;
        sumXYdiff2 += diff * diff;
        count++;
      }
      if (compare <= 0) {
        if (++xPrefIndex >= xLength) {
          if (hasInferrer) {
            // Must count other Ys; pretend next X is far away
            if (yIndex == Long.MAX_VALUE) {
              // ... but stop if both are done!
              break;
            }
            xIndex = Long.MAX_VALUE;
          } else {
            break;
          }
        } else {
          xIndex = xPrefs.getItemID(xPrefIndex);
        }
      }
      if (compare >= 0) {
        if (++yPrefIndex >= yLength) {
          if (hasInferrer) {
            // Must count other Xs; pretend next Y is far away            
            if (xIndex == Long.MAX_VALUE) {
              // ... but stop if both are done!
              break;
            }
            yIndex = Long.MAX_VALUE;
          } else {
            break;
          }
        } else {
          yIndex = yPrefs.getItemID(yPrefIndex);
        }
      }
    }
    
    // "Center" the data. If my math is correct, this'll do it.
    double result;
    if (centerData) {
      double meanX = sumX / count;
      double meanY = sumY / count;
      // double centeredSumXY = sumXY - meanY * sumX - meanX * sumY + n * meanX * meanY;
      double centeredSumXY = sumXY - meanY * sumX;
      // double centeredSumX2 = sumX2 - 2.0 * meanX * sumX + n * meanX * meanX;
      double centeredSumX2 = sumX2 - meanX * sumX;
      // double centeredSumY2 = sumY2 - 2.0 * meanY * sumY + n * meanY * meanY;
      double centeredSumY2 = sumY2 - meanY * sumY;
      result = computeResult(count, centeredSumXY, centeredSumX2, centeredSumY2, sumXYdiff2);
    } else {
      result = computeResult(count, sumXY, sumX2, sumY2, sumXYdiff2);
    }
    
    if (similarityTransform != null) {
      result = similarityTransform.transformSimilarity(userID1, userID2, result);
    }
    
    if (!Double.isNaN(result)) {
      result = normalizeWeightResult(result, count, cachedNumItems);
    }
    return result;
  }

最後取出分值最高(也就是相似度最高的)的兩個使用者作為相似使用者,然後根據這兩個使用者與該使用者的相似度和兩個使用者分別對物品的評分來預測該使用者對物品的評分(如果這兩個近鄰中,只有一個對該物品評分,則預測作廢)。計算公式如下假設使用者a對物品的評分為x,相似度為sa, b使用者對物品的評分為y,相似度為sb,則測試使用者的預測評分公式為, (x*sa+y*sb)/(sa+sb).

如果能計算出預測值,那麼將真實值矩陣x和預測值矩陣y放在一起計算,算出真實值和預測值差值的絕對值|x1-y1|,計算所有預測值和真實值差值的平均值。這個平均值就是我們最後得到的評價分數。

因此這個數實際上是越小越好的。

6. 計算準確率和召回率

在詳細解讀程式碼之前我先回顧一下準確率和召回率在機器學習中的定義,實際上,我自己都快要忘記他們是什麼了。

更詳細一點的劃分如下圖:

對於推薦系統來說,定義可能需要做相應改變:

好了,以上圖片均截自其它大神的部落格,並非我所獨創(地址忘了,就不標明瞭,侵刪)

之後就讓我們回到原始碼本身:

RecommendFactory.statsEvaluator(recommenderBuilder, null, dataModel, 2);

因為我們是預測評分來進行我們的推薦策略的,我們先算出每一個使用者對已評價物品的均分和標準差,將兩者進行相加,並將這個值作為判斷該物品是否真為正相關的一個閾值。評分大於這個值的物品即為正相關,否則即為負相關。然後根據真實的分數查找出和這個使用者正相關的物品的個數。取出這個正相關的物品,把資料集中的其他評分作為訓練集。假設我們需要獲取n個物品,那麼我們則需要使用者對不少於n*2的物品做出評價,否則資料太少則無法計算準確率和召回率。只有當資料多於這個數的時候,我們才根據已有的訓練集,按照之前評價過程中提到過的,獲取最相似的2個使用者,然後根據這兩個使用者的相似度,預測該使用者所有物品的評分,取出評分最高的2項(這個數量是傳入引數控制的)把他們當做預測的正相關項,與真實正相關的進行對比,計算單個使用者的準確率和召回率。最後所有使用者的準確略和召回率取平均值返回。

public IRStatistics evaluate(RecommenderBuilder recommenderBuilder,
                               DataModelBuilder dataModelBuilder,
                               DataModel dataModel,
                               IDRescorer rescorer,
                               int at,
                               double relevanceThreshold,
                               double evaluationPercentage) throws TasteException {

    Preconditions.checkArgument(recommenderBuilder != null, "recommenderBuilder is null");
    Preconditions.checkArgument(dataModel != null, "dataModel is null");
    Preconditions.checkArgument(at >= 1, "at must be at least 1");
    Preconditions.checkArgument(evaluationPercentage > 0.0 && evaluationPercentage <= 1.0,
      "Invalid evaluationPercentage: %s", evaluationPercentage);

    int numItems = dataModel.getNumItems();
    RunningAverage precision = new FullRunningAverage();
    RunningAverage recall = new FullRunningAverage();
    RunningAverage fallOut = new FullRunningAverage();
    RunningAverage nDCG = new FullRunningAverage();
    int numUsersRecommendedFor = 0;
    int numUsersWithRecommendations = 0;

    LongPrimitiveIterator it = dataModel.getUserIDs();
    while (it.hasNext()) {

      long userID = it.nextLong();

      if (random.nextDouble() >= evaluationPercentage) {
        // Skipped
        continue;
      }

      long start = System.currentTimeMillis();

      PreferenceArray prefs = dataModel.getPreferencesFromUser(userID);

      // List some most-preferred items that would count as (most) "relevant" results
      double theRelevanceThreshold = Double.isNaN(relevanceThreshold) ? computeThreshold(prefs) : relevanceThreshold;
      FastIDSet relevantItemIDs = dataSplitter.getRelevantItemsIDs(userID, at, theRelevanceThreshold, dataModel);

      int numRelevantItems = relevantItemIDs.size();
      if (numRelevantItems <= 0) {
        continue;
      }

      FastByIDMap<PreferenceArray> trainingUsers = new FastByIDMap<PreferenceArray>(dataModel.getNumUsers());
      LongPrimitiveIterator it2 = dataModel.getUserIDs();
      while (it2.hasNext()) {
        dataSplitter.processOtherUser(userID, relevantItemIDs, trainingUsers, it2.nextLong(), dataModel);
      }

      DataModel trainingModel = dataModelBuilder == null ? new GenericDataModel(trainingUsers)
          : dataModelBuilder.buildDataModel(trainingUsers);
      try {
        trainingModel.getPreferencesFromUser(userID);
      } catch (NoSuchUserException nsee) {
        continue; // Oops we excluded all prefs for the user -- just move on
      }

      int size = relevantItemIDs.size() + trainingModel.getItemIDsFromUser(userID).size();
      if (size < 2 * at) {
        // Really not enough prefs to meaningfully evaluate this user
        continue;
      }

      Recommender recommender = recommenderBuilder.buildRecommender(trainingModel);

      int intersectionSize = 0;
      List<RecommendedItem> recommendedItems = recommender.recommend(userID, at, rescorer);
      for (RecommendedItem recommendedItem : recommendedItems) {
        if (relevantItemIDs.contains(recommendedItem.getItemID())) {
          intersectionSize++;
        }
      }

      int numRecommendedItems = recommendedItems.size();

      // Precision
      if (numRecommendedItems > 0) {
        precision.addDatum((double) intersectionSize / (double) numRecommendedItems);
      }

      // Recall
      recall.addDatum((double) intersectionSize / (double) numRelevantItems);

      // Fall-out
      if (numRelevantItems < size) {
        fallOut.addDatum((double) (numRecommendedItems - intersectionSize)
                         / (double) (numItems - numRelevantItems));
      }

      // nDCG
      // In computing, assume relevant IDs have relevance 1 and others 0
      double cumulativeGain = 0.0;
      double idealizedGain = 0.0;
      for (int i = 0; i < recommendedItems.size(); i++) {
        RecommendedItem item = recommendedItems.get(i);
        double discount = i == 0 ? 1.0 : 1.0 / log2(i + 1);
        if (relevantItemIDs.contains(item.getItemID())) {
          cumulativeGain += discount;
        }
        // otherwise we're multiplying discount by relevance 0 so it doesn't do anything

        // Ideally results would be ordered with all relevant ones first, so this theoretical
        // ideal list starts with number of relevant items equal to the total number of relevant items
        if (i < relevantItemIDs.size()) {
          idealizedGain += discount;
        }
      }
      nDCG.addDatum(cumulativeGain / idealizedGain);
      
      // Reach
      numUsersRecommendedFor++;
      if (numRecommendedItems > 0) {
        numUsersWithRecommendations++;
      }

      long end = System.currentTimeMillis();

      log.info("Evaluated with user {} in {}ms", userID, end - start);
      log.info("Precision/recall/fall-out/nDCG: {} / {} / {} / {}", new Object[] {
          precision.getAverage(), recall.getAverage(), fallOut.getAverage(), nDCG.getAverage()
      });
    }

    double reach = (double) numUsersWithRecommendations / (double) numUsersRecommendedFor;

    return new IRStatisticsImpl(
        precision.getAverage(),
        recall.getAverage(),
        fallOut.getAverage(),
        nDCG.getAverage(),
        reach);
  }

7. 真正userCF推薦

其實之前真的花了很多時間在分析怎麼讀取資料,資料處理的時候是什麼樣的結構,我們怎麼對模型做評價,怎麼計算準確率和召回率。到了真正推薦的演算法的時候,幾乎已經沒有什麼新的東西了。

多麼熟悉的演算法,其實這裡的程式碼我們在5和6兩步中都已經看過了,第一步是使用現有的資料集得到所有使用者的相關性取出最接近的兩個使用者,然後取出所有該使用者沒有評價過的商品,最後對使用者對這些商品的評分進行預測,取出前兩名。

具體的內容在這裡就不多做描述了,參見上面的步驟。

8. 總結

實際上,userCF的演算法並沒有很複雜,步驟就只有2步:1. 根據現有評分計算使用者相關度,取出最相似的n個使用者;2.根據取出的相似使用者對使用者沒評分的產品預測評分,取出前m個。