距離相似度方法
mahout的推薦主要是基於協同過濾,協同過濾是通過了解使用者與物品之間的關係,也就是使用者對物品的偏好來總結經驗(無需瞭解物品的屬性),從而進行推薦。
而協同過濾又分為基於使用者和基於物品。
基於使用者是尋找使用者與使用者之間的相似度,如A使用者和B使用者都喜歡a,b,c物品,那麼可以認為A和B使用者很相似,如果A還喜歡d物品,那麼B很有可能也喜歡d物品,因此可以推薦d物品給B使用者。
基於物品是尋找物品與物品之間的相似度。如a和b物品同時被A,B,C使用者喜歡,則可以認為a和b物品很相似,如果D使用者還喜歡a物品,那麼D使用者也很有可能也喜歡b物品。
mahout的推薦系統由多個組建混搭而成,我們可以根據自己的需求和場景選擇最適合自己的組建來搭配。下面介紹基於使用者的,通常包括如下組建:
資料模型:由DataModel的子類實現
使用者間的相似性度量:由UserSimilarity的子類實現。
使用者領域的定義:由UserNeighborhood的子類實現。
推薦引擎:由Recommender的子類實現。
下面開始介紹計算使用者相似度的各種演算法:
首先有資料如下:
橫排是物品,豎列是使用者,中間的是使用者對物品的評分,5分是很喜歡,3分是一般,1分是討厭。 沒有得到使用者對該物品的偏好。
1,101,5
1,102,3
1,103,2.5
2,101,2
2,102,2.5
2,103,5
2,104,2
3,101,2.5
3,104,4
3,105,4.5
3,107,5
4,101,5
4,103,3
4,104,4.5
4,106,4
5,101,4
5,102,3
5,103,2
5,104,4
5,105,3.5
5,106,4
1. 基於皮爾遜相關係數的相似度
皮爾遜相關係數是一個介於-1和1之間的數,它度量兩個一一對應的數列之間的線性關係度量,也就是說,它表示兩個數列中對應數字一起增大或一起減小的可能性。當接近於1時,表明他們數字變化趨勢一致,當接近於0時,變化趨勢沒什麼關係,當接近於-1時,變化趨勢是相反的。
公式如下:
從最下面的公式能看得出來,pearson就是把兩個向量減去他們的平均值後,計算他們夾角的cos。當然夾角越小,兩個向量的方向越一致。
在基於使用者的協同過濾中,我們把一個使用者對所有物品的評價聯合起來,看作向量。
如計算上面使用者1和5的相似度。他們都評價了101,102,103,把這些評價組合成向量,則使用者1的向量是(5,3,2.5),使用者5的向量是(4,3,2),然後套入上面的公式,向量1的平均值是3.5,向量5的平均值是3. 分子是向量的點乘得出是2.5.分母是兩個向量的長度相乘是2.64.再相除得到pearson相似係數為0.945。
使用者1和3只共同評價了一個物品,所以不能得出他們間的pearson相關係數。
缺點:沒有考慮兩個使用者同時給出偏好值的物品數量。如1和4只同時給出了兩個物品的偏好,但他們的pearson相似度還高於5(5和1同時給出了3個物品的偏好),這跟直觀感覺不一致。
1.不能計算兩個使用者只同時給出了一個物品的偏好值。2.如果一個使用者給所有物品的偏好都一樣,也不能算他與別人的相似度(代入上面的公式就知道)。因此在小的或疏鬆的資料集中,比較有問題。
為了解決上面的問題,可以加入權重(weighting),即基於較多物品計算相關係數時,使正相關值向1靠近,使負相關值向-1靠近。基於較少物品時,使相關係數向均值靠近。
2. 基於歐式距離定義相似度
可以將使用者想象為多維空間的點(維數等於物品數),偏好值是他們的座標。這種相似度量就是計算兩個使用者點間的歐式距離d。他們越遠代表越不相似,越近越相似。
公式為:
可以先看看平面兩點距離計算公式,就容易看懂了。實際應用中將1/(1+d)作為相似度,那麼就是越接近於1越相似,越接近於0越不相似。
3.基於餘弦相似性度量
和歐式距離一樣,也將使用者偏好值視為空間的點,不過這次不是計算他們的距離了,而是從原點出發,分別連線這兩點,把他們看作向量,計算他們夾角的餘弦。他們的夾角越小則越相似,夾角越大則越不相似。
可以看出來,餘弦相似度就是兩個向量的點乘除以他們長度的乘積。跟pearson相似度的公式差不多,只是沒有減去他們的均值。
餘弦相似度對數值的絕對值不敏感,例如A使用者對兩個物品的偏好是1,2。B使用者的偏好是4,5。這時分兩種情況,每個人都有自己的一套評分標準,有的人喜歡打高分,有的人喜歡打低分。那麼如果這裡A是喜歡打低分的人,B是喜歡打高分的人,雖然他們這裡打的分有點懸殊,但實際上他們的評價還是比較一致的,那麼他們比較相似。這是一種情況,另外的一種情況就是他們打分的標準都差不多,那麼這裡顯然A使用者是不喜歡這兩個物品,而B使用者是喜歡他們的。
但是這裡的餘弦相似度卻高達0.98,因此需要對資料去中心化的處理,也就是對數值減去他們的均值,對於上面說的第一種情況,如果A使用者給出所有物品的評分均值是1(因為他喜歡打低分),那麼他對這兩個物品的評分會調整為0,1,B使用者給出所有物品的評分均值是4,那麼他對這兩個物品的評分會調整為0,1。這時候再計算餘弦相似度就會達到1了,說明這兩個使用者確實很相似。對於第二種情況,如果A和B使用者給出所有評分均值都是3,那麼分別調整為(-2,-1)和(1,2)。這時他的餘弦相似是-0.8了。這樣就知道他們確實不相似了。
mahout會對資料輸入進行中心化處理(也就是減去均值),因此mahout餘弦相似度的實現和pearson相關係數是一樣的。不過也同樣給出了一個沒有中心化處理的餘弦相似度實現UncenteredCosineSimilarity。
4.斯皮爾曼相關係數
這個是pearson相關係數的一個變體,他根據使用者的評分對該使用者喜歡的物品做了個排序,然後用這個排序重寫他的偏好值。如排最後一名的物品重新給偏好值為1.,倒數第二的重給偏好值為2,以此類推,最喜歡的偏好值最高。因此這種演算法丟棄了使用者偏好的具體值,而僅僅保留了其順序。
然後再用spearson進行計算。如下:
這種計算方式效能很差,因為需要為每個使用者進行排序,資料規模大的時候不理想,意義不大。
5.古本系數計算相似度
這個演算法不關心使用者對一個物品的偏好是高是低,他只關心兩個使用者一起評論了多少個物品。
他認為兩個人同時評價了200部電影比同時評價了2部電影的相似度要高,哪怕他們給這兩部電影的評分很一致,而給200部評分的不太一致。
他的公式如下:
分子是他們的交集的數量,分母是他們並集的數量。
如使用者1和使用者5,交集數為3個物品,並集數為6個物品,那麼相似度就是0.5。
6.對數似然比
跟谷本相似度差不多,也不考慮偏好值的多少,只關心同時有的偏好。
不過它試圖反映兩個使用者由於機緣巧合發生重疊的不可能性。也就是說兩部電影兩個使用者評論了,但是就只有他們兩個評論了,那麼這是機緣巧合他們一起評論的可能性就很低,因為別人都沒評論,也就相似了。如果這兩部電影也很多人別人評論了,那麼這是機緣巧合的概率就大了,因為大家都評論了,他們也就不相似了。
總結
使用GroupLens網站的資料做了實驗,使用1000萬的使用者對電影的偏好資料。
下面是實驗結果的資料:
得分是推薦結果與實際結果的誤差大小,數值越小,正確率越高。可以看出用了權重的歐式距離在這個實驗中正確率最高。
使用的測試程式碼如下:
public class GroupLens10MEvalIntro
{
static DataModel model;
static RecommenderEvaluator evaluator;
static RecommenderBuilder recommenderBuilder;
static String similarityName;
@BeforeClass
public static void onStart() throws IOException
{
RandomUtils.useTestSeed();
model = new GroupLensDataModel(new File(FilePath.CF_10M));
evaluator = new AverageAbsoluteDifferenceRecommenderEvaluator();
}
@Test
public void pearson()
{
similarityName="PearsonCorrelationSimilarity";
recommenderBuilder = new RecommenderBuilder()
{
@Override
public Recommender buildRecommender(DataModel model) throws TasteException
{
UserSimilarity similarity = new PearsonCorrelationSimilarity(model);
UserNeighborhood neighborhood = new NearestNUserNeighborhood(100, similarity, model);
return new GenericUserBasedRecommender(model, neighborhood, similarity);
}
};
}
@Test
public void pearsonWithWeight()
{
similarityName="PearsonCorrelationSimilarityWithWeight";
recommenderBuilder = new RecommenderBuilder()
{
@Override
public Recommender buildRecommender(DataModel model) throws TasteException
{
UserSimilarity similarity = new PearsonCorrelationSimilarity(model, Weighting.WEIGHTED);
UserNeighborhood neighborhood = new NearestNUserNeighborhood(100, similarity, model);
return new GenericUserBasedRecommender(model, neighborhood, similarity);
}
};
}
@Test
public void euclidean()
{
similarityName="EuclideanDistanceSimilarity";
recommenderBuilder = new RecommenderBuilder()
{
@Override
public Recommender buildRecommender(DataModel model) throws TasteException
{
UserSimilarity similarity = new EuclideanDistanceSimilarity(model);
UserNeighborhood neighborhood = new NearestNUserNeighborhood(100, similarity, model);
return new GenericUserBasedRecommender(model, neighborhood, similarity);
}
};
}
@Test
public void euclideanWithWeight()
{
similarityName="EuclideanDistanceSimilarityWithWeight";
recommenderBuilder = new RecommenderBuilder()
{
@Override
public Recommender buildRecommender(DataModel model) throws TasteException
{
UserSimilarity similarity = new EuclideanDistanceSimilarity(model, Weighting.WEIGHTED);
UserNeighborhood neighborhood = new NearestNUserNeighborhood(100, similarity, model);
return new GenericUserBasedRecommender(model, neighborhood, similarity);
}
};
}
@Test
public void spearman()
{
similarityName="SpearmanCorrelationSimilarity";
recommenderBuilder = new RecommenderBuilder()
{
@Override
public Recommender buildRecommender(DataModel model) throws TasteException
{
UserSimilarity similarity = new SpearmanCorrelationSimilarity(model);
UserNeighborhood neighborhood = new NearestNUserNeighborhood(100, similarity, model);
return new GenericUserBasedRecommender(model, neighborhood, similarity);
}
};
}
@Test
public void tanimoto()
{
similarityName="TanimotoCoefficientSimilarity";
recommenderBuilder = new RecommenderBuilder()
{
@Override
public Recommender buildRecommender(DataModel model) throws TasteException
{
UserSimilarity similarity = new TanimotoCoefficientSimilarity(model);
UserNeighborhood neighborhood = new NearestNUserNeighborhood(100, similarity, model);
return new GenericUserBasedRecommender(model, neighborhood, similarity);
}
};
}
@Test
public void loglike()
{
similarityName="LogLikelihoodSimilarity";
recommenderBuilder = new RecommenderBuilder()
{
@Override
public Recommender buildRecommender(DataModel model) throws TasteException
{
UserSimilarity similarity = new LogLikelihoodSimilarity(model);
UserNeighborhood neighborhood = new NearestNUserNeighborhood(100, similarity, model);
return new GenericUserBasedRecommender(model, neighborhood, similarity);
}
};
}
@After
public void evaluator() throws TasteException
{
double score = evaluator.evaluate(recommenderBuilder, null, model, 0.95, 0.05);
System.out.println(similarityName + ":" + score);
}
}