Apache Mahout之協同過濾原理與實踐
讀書時期,選課是令人懷念的,因為自由,學生可以挑選自己喜愛的課程和老師!然而,過程並不是很美好,“系統繁忙,稍后重試!”屢有發生,於是大伙開心地約定今夜不戰不休。西門的七彩路,和網吧名一樣,我們從門口路過,進的卻是右旁的可媛。這里網頁同樣堅持“系統繁忙,稍后重試!”!去的人多了,也就組了局。痛並快樂着應該如此!那以后,我們這堆人中出了一批又一批的高手,操作極限,走位妖嬈,那都不是事兒!
戲后,一場深思悄然浮現:如果系統可以收羅大量數據,如學生性格特征、性別籍貫、課程成績、興趣愛好、喜愛書籍、歷史課表等等,然后消化這些數據原料,最后為每個學生呈現一份個性化的定制課表!多么美好!譬如,對於經常清晨借閱或閱讀歷史書籍的L,就可以推薦G老師上午時間講授的《清帝漫談》課程,對L來說,學習效果更佳!
如今,智能推薦無所不在。在亞馬遜買過書籍的朋友,可能會注意到,當在網站上買過幾次書籍后,下次再次購買一些新書籍時,網站會主動推薦一些你可能感興趣的書籍,等你來購!
當然,應用比較廣泛的推薦方法之一便是協同過濾(Collaborative Filter,簡稱CF)。今天,就和大家一起來揭秘她的神秘面紗。
一、什么是協同過濾
協同過濾基於的基本思想:如果用戶在過去有相同的偏好(比如他們瀏覽了相同的網頁信息或買過相同的書),那么他們在未來也會有相似的偏好(所謂江山易改,本性難移)。例如,如果用戶A和用戶B過去都購買過書籍a、b和c,而且用戶A最近新買了一本用戶B還不知道的書籍d,如表所示:
用戶 |
書籍 |
|||
A |
a |
b |
c |
d |
B |
a |
b |
c |
? |
那么我們基本的邏輯是向B推薦書籍d。而且我們還能看到用戶A和用戶B可能會成為很好的朋友。當然,如果他們性別相異,年齡相當,我推薦他們考慮建立成戀愛關系(具有相似偏好的人成為戀人的成功率更高,更何況他們看的書籍都那么的一致)。
向用戶B推薦可能感興趣的書籍涉及從大量的書籍集合中過濾出用戶B最可能感興趣的書籍,而且用戶A和用戶B的這種關系是一種協同,所以稱為協同過濾。
二、如何尋找與當前用戶具有相似偏好的用戶
1、用戶相似度度量
假設數據庫中存儲了用戶-物品-評分的數據alice.txt,格式如下:
1,101,5 1,102,3 1,103,4 1,104,4 2,101,3 2,102,1 2,103,2 2,104,3 2,105,3 3,101,4 3,102,3 3,103,4 3,104,3 3,105,5 4,101,3 4,102,3 4,103,1 4,104,5 4,105,4 5,101,1 5,102,5 5,103,5 5,104,2 5,105,1
第一列表示用戶ID={1,2,3,4,5},
第二列表示物品ID={101,102,103,104,105},
第三列表示用戶給物品的評分Score={1,2,3,4,5},
例如第一行數據表示的含義是用戶1給物品101的評分是5(最高分)。然而,數據庫有時只存儲了某個用戶交易了某個物品,或者瀏覽過某個物品,或者收藏過某個物品,我們通過這些數據同樣可以計算出用戶-物品-評分數據。為了清晰,我們將用戶-物品-評分數據轉換成用戶-物品-評分矩陣(本例是5*5大小的矩陣),如下表:
用戶/物品 |
101 |
102 |
103 |
104 |
105 |
1 |
5 |
3 |
4 |
4 |
? |
2 |
3 |
1 |
2 |
3 |
3 |
3 |
4 |
3 |
4 |
3 |
4 |
4 |
3 |
3 |
1 |
5 |
4 |
5 |
1 |
5 |
5 |
2 |
1 |
現在的問題是:是否應當向用戶1推薦物品105?我們先假設:如果用戶1給物品105評5分,那么應當推薦給用戶1,如果是1分,那么最好不要推薦!如何預測用戶1會給物品105評多少分呢?我們的邏輯是:參考那些與用戶1相似的用戶給物品105的評分,例如與用戶1相似的用戶都給105評5分,那么我們預測用戶1可能會給物品105評5分,也就當然將物品105推薦給用戶1了。所以,我們就需要尋找與用戶1具有相似偏好的那些用戶,那么究竟如何度量兩個用戶之間的相似度呢?比如用戶1和用戶2。事實上,用戶對所有物品的評分構成了一個n維向量(矩陣的每一行),例如,用戶1對應一個5維向量v1=(5,3,4,4,?),用戶2對應一個5維向量v2=(3,1,2,3,3),那么問題就轉換成如何度量這兩個向量的相似度,大家立即想到的是歐幾里得空間距離:
上式表示了用戶1和用戶2的歐幾里得空間距離,注意到用戶1對物品105沒有評分,計算時我們舍棄沒有數據的維數(向量降維),如下計算即可:
基於距離越近,用戶越相似的思想,用戶1和用戶2的相似度:
其中4表示向量維數,計算結果小數位舍棄。同理可以計算出用戶1分別與用戶3、4和5的相似度分別為:
所以與用戶1相似度按從高到低的用戶排序依次是:用戶3、用戶2、用戶4和用戶5。這里歐幾里得相似度只是提供了一種相似度度量方式。是否還有其他的度量方式呢?我們知道,在二維空間中,兩個向量v1和向量v2的夾角余弦計算如下:
那么,這個余弦值是否可以用來度量兩個用戶的相似度呢?如圖:
圖中展示了兩個向量的三種位置關系:
第一個圖表示一般情況:兩個向量有個夾角;
第二個圖表示兩個向量重合的情況;
第三個圖表示兩個向量垂直的情況。
我們先考慮兩個物品,如下:
用戶/物品 |
物品1 |
物品2 |
用戶1 |
3 |
4 |
用戶2 |
3 |
4 |
用戶1和用戶2對物品的評分一樣,我們認為這兩個用戶是非常相似的,甚至一樣,他們對應的向量為v1=(3,4),v2=(3,4),余弦值:
這種情況余弦值1確實可以作為用戶1和用戶2的相似度度量。
當用戶給物品的評分情況如下時:
用戶/物品 |
物品1 |
物品2 |
用戶1 |
5 |
0 |
用戶2 |
0 |
5 |
實際來看,兩個用戶截然不同,用戶1給物品1評5分高分時,用戶2卻給相同物品評低分,對物品2也是如此,他們對應的向量為v1=(5,0),v2=(0,5),余弦值:
這種情況余弦值0也可以作為用戶1和用戶2的相似度度量。我們的基本邏輯是:當兩個向量的夾角越小時,用戶相似度越高,反之,用戶相似度越低。所以我們可以直接使用余弦值作為用戶的相似度度量,稱為余弦相似度(記作CosineSimilarity)。我們使用案例數據計算出用戶1與其他用戶的相似度如下:
所以與用戶1相似度從高到低的用戶排序依次是:用戶3、用戶2、用戶4和用戶5,與歐幾里得相似度值雖然相差甚多,但結果卻一致。當然相似度度量還有很多方法,例如皮爾森相關系數,公式如下:
其中P表示所有物品的集合,例如案例中P={101,102,103,104,105},表示用戶a給物品p的評分,和分別表示用戶a和用戶b對所有物品的平均評分,例如用戶1給所有物品的平均評分是:
那么,用戶1和用戶2的相似度:
用戶1與其他用戶的相似度為:
所以與用戶1相似度從高到低的用戶排序依次是:用戶2、用戶3、用戶4和用戶5。與前面兩種度量方式結果不完全一致,這里用戶2與用戶1更相似一些。如果只選擇兩個最相似的用戶,那么結果卻是一致的。還有其他很多相似度度量方法,我們不再一一說明。到這里用戶相似度的度量問題已經得到解決。
2、用戶的最近鄰(k-最近鄰)
所謂某個用戶的k-最近鄰是指與該用戶最相似的k個用戶(不包括該用戶本身)。例如我們前面形成了用戶1的4-最近鄰 ={用戶3,用戶2,用戶4,用戶5}。如果我們只選擇兩個最相似用戶,那么就構成了用戶1的2-最近鄰 ={用戶3,用戶2},從2-最近鄰來看,前面的三種度量方式,結果是一致的。
我們已經找到了與當前用戶相似的那些用戶了,接下來就要參考這些用戶給物品105的評分來預測當前用戶給105的評分了。
三、預測評分
如何預測用戶1給物品105的評分?這就關系到該重視哪些近鄰的評分,如何重視?例如通過皮爾森相關系數,計算出的用戶1與其他用戶的相似度:用戶1和用戶2、用戶3相似度分別為0.85和0.70,相關性最大,與用戶4相似度值0,可以認為無關,與用戶五的相關系數值是-0.79,是個負值,可以認為兩個用戶可能偏好截然相反。所以我們應當選擇用戶2和用戶3作為用戶1的2-最近鄰來評分。下面公式考慮了用戶a的N近鄰與用戶a平均評分的偏差,預測用戶a對物品p的評分:
其中sim(a,b)表示用戶a和用戶b的相似度。所以用戶1給物品105的預測評分是:
當然,Apache Mahout中並不是這樣實現的,它未考慮平均評分,而是采用了如下簡化的預測公式:
計算出的預測評分為
四、推薦
無論用戶1給物品105的預測評分是4.87,還是3.90,都是一個比較高的分數,應當將物品105推薦給用戶1。
五、代碼實現
1、pom.xml
導入Hadoop Mahout算法庫,如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.leboop</groupId> <artifactId>mahout</artifactId> <version>1.0-SNAPSHOT</version> <properties> <!-- mahout版本號 --> <mahout.version>0.13.0</mahout.version> </properties> <dependencies> <!-- mahout --> <dependency> <groupId>org.apache.mahout</groupId> <artifactId>mahout-integration</artifactId> <version>${mahout.version}</version> </dependency> </dependencies> </project>
2、推薦程序
程序中,我們使用皮爾森系數計算用戶相似度,如下:
package com.leboop.recommendation; import org.apache.mahout.cf.taste.impl.common.LongPrimitiveIterator; import org.apache.mahout.cf.taste.impl.model.file.FileDataModel; import org.apache.mahout.cf.taste.impl.neighborhood.NearestNUserNeighborhood; import org.apache.mahout.cf.taste.impl.recommender.GenericUserBasedRecommender; import org.apache.mahout.cf.taste.impl.similarity.PearsonCorrelationSimilarity; import org.apache.mahout.cf.taste.model.DataModel; import org.apache.mahout.cf.taste.neighborhood.UserNeighborhood; import org.apache.mahout.cf.taste.recommender.RecommendedItem; import org.apache.mahout.cf.taste.recommender.Recommender; import org.apache.mahout.cf.taste.similarity.UserSimilarity; import java.io.File; import java.util.Arrays; import java.util.List; /** * 推薦思路 * 1、讀取用戶-物品-評分數據,轉換成推薦數據模型 * 2、基於用戶相似度計算用戶N-最近鄰 * 3、使用推薦引擎推薦物品 */ public class BasedUserRecommendationTest { public static void main(String[] args) { //用戶-物品-評分數據文件 String filePath = "data\\alice.txt"; //數據模型 DataModel dataModel = null; try { //文件數據轉換成數據模型 dataModel = new FileDataModel(new File(filePath)); /** * 用戶相似度定義 */ //余弦相似度 // UserSimilarity userSimilarity= new UncenteredCosineSimilarity(dataModel); //歐幾里得相似度 // UserSimilarity userSimilarity= new EuclideanDistanceSimilarity(dataModel); //皮爾森相似度 UserSimilarity userSimilarity = new PearsonCorrelationSimilarity(dataModel); //定義用戶的2-最近鄰 UserNeighborhood userNeighborhood = new NearestNUserNeighborhood(2, userSimilarity, dataModel); //定義推薦引擎 Recommender recommender = new GenericUserBasedRecommender(dataModel,userNeighborhood, userSimilarity); //從數據模型中獲取所有用戶ID迭代器 LongPrimitiveIterator usersIterator = dataModel.getUserIDs(); //通過迭代器遍歷所有用戶ID while (usersIterator.hasNext()) { System.out.println("================================================"); //用戶ID long userID = usersIterator.nextLong(); //用戶ID LongPrimitiveIterator otherusersIterator = dataModel.getUserIDs(); //遍歷用戶ID,計算任何兩個用戶的相似度 while (otherusersIterator.hasNext()) { Long otherUserID = otherusersIterator.nextLong(); System.out.println("用戶 " + userID + " 與用戶 " + otherUserID + " 的相似度為:" + userSimilarity.userSimilarity(userID, otherUserID)); } //userID的N-最近鄰 long[] userN = userNeighborhood.getUserNeighborhood(userID); //用戶userID的推薦物品,最多推薦兩個 List<RecommendedItem> recommendedItems = recommender.recommend(userID, 2); System.out.println("用戶 "+userID + " 的2-最近鄰是 "+ Arrays.toString(userN)); if (recommendedItems.size() > 0) { for (RecommendedItem item : recommendedItems) { System.out.println("推薦的物品"+ item.getItemID()+"預測評分是 "+ item.getValue()); } } else { System.out.println("無任何物品推薦"); } } } catch (Exception e) { e.printStackTrace(); } } }
執行上述程序,結果如下:
================================================
用戶 1 與用戶 1 的相似度為:0.9999999999999998
用戶 1 與用戶 2 的相似度為:0.8528028654224417
用戶 1 與用戶 3 的相似度為:0.7071067811865475
用戶 1 與用戶 4 的相似度為:0.0
用戶 1 與用戶 5 的相似度為:-0.7921180343813393
用戶 1 的2-最近鄰是 [2, 3]
推薦的物品105預測評分是 3.9065998
================================================
用戶 2 與用戶 1 的相似度為:0.8528028654224417
用戶 2 與用戶 2 的相似度為:1.0
用戶 2 與用戶 3 的相似度為:0.4677071733467446
用戶 2 與用戶 4 的相似度為:0.4899559349388647
用戶 2 與用戶 5 的相似度為:-0.9001487972234673
用戶 2 的2-最近鄰是 [1, 4]
無任何物品推薦
================================================
用戶 3 與用戶 1 的相似度為:0.7071067811865475
用戶 3 與用戶 2 的相似度為:0.4677071733467422
用戶 3 與用戶 3 的相似度為:1.0
用戶 3 與用戶 4 的相似度為:-0.16116459280507703
用戶 3 與用戶 5 的相似度為:-0.466569474815843
用戶 3 的2-最近鄰是 [1, 2]
無任何物品推薦
================================================
用戶 4 與用戶 1 的相似度為:0.0
用戶 4 與用戶 2 的相似度為:0.489955934938866
用戶 4 與用戶 3 的相似度為:-0.16116459280507558
用戶 4 與用戶 4 的相似度為:1.0
用戶 4 與用戶 5 的相似度為:-0.6415029025857746
用戶 4 的2-最近鄰是 [2, 1]
無任何物品推薦
================================================
用戶 5 與用戶 1 的相似度為:-0.7921180343813393
用戶 5 與用戶 2 的相似度為:-0.9001487972234682
用戶 5 與用戶 3 的相似度為:-0.466569474815843
用戶 5 與用戶 4 的相似度為:-0.6415029025857751
用戶 5 與用戶 5 的相似度為:1.0
用戶 5 的2-最近鄰是 [3, 4]
無任何物品推薦
Process finished with exit code 0
到這,我們已經成功為用戶1推薦了物品105。
六、基於物品的協同過濾推薦
1、基本思想
盡管基於用戶的協同過濾的方法已經成功應用在了不同領域,但在有着數以百萬計甚至上億用戶和物品的大型電子商務網站(例如亞馬遜Amazon)還是會存在很多嚴峻挑戰。這種方法很難做到實時推薦。下面談談與基於用戶協同過濾類似的另外一種推薦方法——基於物品的協同過濾推薦。
基於物品的協同過濾推薦主要思想是利用物品間相似度,而不是用戶間相似度來計算預測值。我們看用戶-物品-評分矩陣的某一列,得到物品101對應的向量v1=(5,3,4,3,1)和物品105對應的向量v5=(?,3,5,4,1),舍棄向量的第一個分量,通過余弦相似度計算出他們的相似度如下:
我們通過計算用戶1對所有與物品105相似物品的加權評分總和來預測用戶1對物品105的評分,公式如下:
N表示物品p的k-最近鄰,同用戶的k-最近鄰類似,實際中需要選擇k值,sim(t,p)表示物品t與物品p的相似度,表示用戶a給物品t的評分。Apache Mahout實現時,N采用與物品p相似的所有物品。所以,用戶1對物品105的預測評分為:
2、代碼實現
程序中,我們使用物品余弦相似度,如下:
package com.leboop.recommendation; import org.apache.mahout.cf.taste.impl.common.LongPrimitiveIterator; import org.apache.mahout.cf.taste.impl.model.file.FileDataModel; import org.apache.mahout.cf.taste.impl.neighborhood.NearestNUserNeighborhood; import org.apache.mahout.cf.taste.impl.recommender.GenericItemBasedRecommender; import org.apache.mahout.cf.taste.impl.recommender.GenericUserBasedRecommender; import org.apache.mahout.cf.taste.impl.similarity.EuclideanDistanceSimilarity; import org.apache.mahout.cf.taste.impl.similarity.PearsonCorrelationSimilarity; import org.apache.mahout.cf.taste.impl.similarity.UncenteredCosineSimilarity; import org.apache.mahout.cf.taste.model.DataModel; import org.apache.mahout.cf.taste.neighborhood.UserNeighborhood; import org.apache.mahout.cf.taste.recommender.RecommendedItem; import org.apache.mahout.cf.taste.recommender.Recommender; import org.apache.mahout.cf.taste.similarity.ItemSimilarity; import org.apache.mahout.cf.taste.similarity.UserSimilarity; import java.io.File; import java.util.Arrays; import java.util.List; /** * 推薦思路 * 1、讀取用戶-物品-評分數據,轉換成推薦數據模型 * 2、定義物品相似度(余弦相似度、皮爾森相似度等) * 3、預測評分 * 4、使用推薦引擎推薦物品 */ public class BasedItemRecommendationTest { public static void main(String[] args) { //用戶-物品-評分數據文件 String filePath = "data\\alice.txt"; //數據模型 DataModel dataModel = null; try { //文件數據轉換成數據模型 dataModel = new FileDataModel(new File(filePath)); /** * 物品相似度定義 */ //余弦相似度 ItemSimilarity itemSimilarity = new UncenteredCosineSimilarity(dataModel); //歐幾里得相似度 // ItemSimilarity itemSimilarity= new EuclideanDistanceSimilarity(dataModel); // //皮爾森相似度 // ItemSimilarity itemSimilarity = new PearsonCorrelationSimilarity(dataModel); //定義推薦引擎 Recommender recommender =new GenericItemBasedRecommender(dataModel, itemSimilarity); //獲取物品迭代器 LongPrimitiveIterator itemIDIterator = dataModel.getItemIDs(); //遍歷所有物品 while(itemIDIterator.hasNext()){ System.out.println("=================================================="); Long itermID=itemIDIterator.next(); LongPrimitiveIterator otherItemIDIterator=dataModel.getItemIDs(); //打印物品相似度 while (otherItemIDIterator.hasNext()){ Long otherItermID=otherItemIDIterator.next(); System.out.println("物品 "+itermID+" 與物品 "+otherItermID+" 的相似度為: "+itemSimilarity.itemSimilarity(itermID,otherItermID)); } } //獲取用戶迭代器 LongPrimitiveIterator userIDIterator =dataModel.getUserIDs(); //遍歷用戶 while(userIDIterator.hasNext()){ //獲取用戶 Long userID=userIDIterator.next(); //獲取用戶userID的推薦列表 List<RecommendedItem> itemList= recommender.recommend(userID,2); if(itemList.size()>0){ for(RecommendedItem item:itemList){ System.out.println("用戶 "+userID+" 推薦物品 "+item.getItemID()+",物品評分 "+item.getValue()); } }else { System.out.println("用戶 "+userID+" 無任何物品推薦"); } } } catch (Exception e) { e.printStackTrace(); } } }
執行上述程序,結果如下:
==================================================
物品 101 與物品 101 的相似度為: 0.9999999999999999
物品 101 與物品 102 的相似度為: 0.7802595923450996
物品 101 與物品 103 的相似度為: 0.8197822947299412
物品 101 與物品 104 的相似度為: 0.9433700705169152
物品 101 與物品 105 的相似度為: 0.9941002434954168
==================================================
物品 102 與物品 101 的相似度為: 0.7802595923450996
物品 102 與物品 102 的相似度為: 1.0
物品 102 與物品 103 的相似度為: 0.9420196895802699
物品 102 與物品 104 的相似度為: 0.8479844150302361
物品 102 與物品 105 的相似度為: 0.7388505791113108
==================================================
物品 103 與物品 101 的相似度為: 0.8197822947299412
物品 103 與物品 102 的相似度為: 0.9420196895802699
物品 103 與物品 103 的相似度為: 1.0
物品 103 與物品 104 的相似度為: 0.7840250892042882
物品 103 與物品 105 的相似度為: 0.7226101216384172
==================================================
物品 104 與物品 101 的相似度為: 0.9433700705169152
物品 104 與物品 102 的相似度為: 0.8479844150302361
物品 104 與物品 103 的相似度為: 0.7840250892042882
物品 104 與物品 104 的相似度為: 0.9999999999999999
物品 104 與物品 105 的相似度為: 0.9395584757365169
==================================================
物品 105 與物品 101 的相似度為: 0.9941002434954168
物品 105 與物品 102 的相似度為: 0.7388505791113108
物品 105 與物品 103 的相似度為: 0.7226101216384172
物品 105 與物品 104 的相似度為: 0.9395584757365169
物品 105 與物品 105 的相似度為: 0.9999999999999999
用戶 1 推薦物品 105,物品評分 4.0751815
用戶 2 無任何物品推薦
用戶 3 無任何物品推薦
用戶 4 無任何物品推薦
用戶 5 無任何物品推薦
Process finished with exit code 0
七、協同過濾推薦基本步驟
1、基於用戶的協同過濾
(1)采集用戶與物品之間的關聯數據,如瀏覽、購買或交易記錄,形成初始數據;
(2)分析用戶與物品的關聯數據形成用戶-物品-評分數據;
(3)依據用戶-物品-評分數據計算所有用戶間的相似度;
(4)選擇與當前用戶最相似的k個用戶,也就是用戶k-最近鄰。
(5)將這k個用戶加權評分最高且當前用戶沒有瀏覽過的n個物品推薦給當前用戶。
2、基於物品的協同過濾
(1)采集用戶與物品之間的關聯數據,如瀏覽、購買或交易記錄,形成初始數據;
(2)分析用戶與物品的關聯數據形成用戶-物品-評分數據;
(3)依據用戶-物品-評分數據計算所有物品間的相似度;
(4)對當前用戶沒瀏覽過的某個物品,選擇最相似的k個物品(k-最近鄰)
(5)基於這k個物品評分預測當前物品評分;
(6)將評分最高的n個物品推薦給當前用戶。
八、協同過濾之MapReduce
當用戶或者物品數以億計時,之前的程序是遠遠不夠的,此時,我們需要使用MapReduce來進行協同過濾推薦。
1、數據
准備用戶-物品-評分數據itemdata.data,如下:
將數據文件上傳至HDFS文件系統/input/mahout-demo/目錄下,如圖:
2、執行協同過濾MapReduce任務
在命令窗口執行如下命令,
hadoop jar mahout-examples-0.13.0-job.jar org.apache.mahout.cf.taste.hadoop.item.RecommenderJob -i /input/mahout-demo/itemdata.data -o /output/cf/item -s SIMILARITY_LOGLIKELIHOOD --tempDir /tmp/cf/item
部分執行過程如圖:
參數說明:
(1)-i:指定輸入數據文件路徑
(2)-o:指定最終輸出數據文件路徑
(3)-s:指定相似度度量方法,這里使用的是對數似然相似度
(4)--tempDir:指定任務執行的中間數據文件保存目錄
任務執行結束,推薦結果如下:
例如,第4行數據表明:優先推薦物品577給用戶1917281441163686119。