mahout之數據承載

推薦數據的處理是大規模的,在集羣環境下一次要處理的數據可能是數GB,所以Mahout針對推薦數據進行了優化。

Preference

在Mahout中,用戶的喜好被抽象爲一個Preference,包含了userId,itemId和偏好值(user對item的偏好)。Preference是一個接口,它有一個通用的實現是GenericPreference。


 

但因爲用戶的喜好數據是大規模的,我們通常會選擇把它放入集合或者數組。但是因爲Java的對象的內存消耗機制,在大數據量下使用Collection<Preference>和Preference[]是非常低效的。爲什麼呢?
     
在Java中,一個對象佔用的字節數 = 基本的8字節 + 基本數據類型所佔的字節 + 對象引用所佔的字節
(1)先說這基本的8字節
在JVM中,每個對象(數組除外)都有一個頭,這個頭有兩個字,第一個字存儲對象的一些標誌位信息,如:鎖標誌位、經歷了幾次gc等信息;第二個字節是一個引用,指向這個類的信息。JVM爲這兩個字留了8個字節的空間。
這樣一來的話,new Object()就佔用了8個字節,那怕它是個空對象
(2) 基本類型所佔用的字節數
    byte/boolean 1bytes
    char/short 2bytes
    int/float 4bytes
    double/long 8bytes
(3)對象引用所佔用的字節數
    reference 4bytes
注:實際中,有數據成員的話,要把數據成員按基本類型和對象引用分開統計。基本類型按(2)進行累加,然後對齊到8個倍數;對象引用按每個4字節進行累加,然後對齊到8的倍數。
class test {
    Integer i;
    long l;
    byte b;
}
佔 8(基本) + 16(數據成員——基本類型:8 + 1,對齊到8) + 8(數據成員——對象引用Integer,4,對齊到8) = 32字節

如此一來的話,一個GenericPreference的對象就需要佔用28個字節,userId(8bytes) + itemId(8bytes) + preference(4bytes) + 基本的8bytes = 28。如果我們使用了Collection<Preference>和Preference[],就會浪費很多這基本的8字節。設想如果我們的數據量是上GB或是上TB,這樣的開銷是很難承受的。

爲此Mahout封裝了一個PreferenceArray,用於保存一組用戶喜好數據,爲了優化性能,Mahout給出了兩個實現類:GenericUserPreferenceArray和GenericItemPreferenceArray,分別按照用戶和物品本身對用戶偏好進行組裝,這樣就可以壓縮用戶ID或者物品ID的空間。
PreferenceArray

GenericUserPreferenceArray

我們看到,GenericUserPreferenceArray包含了一個userId,一個itemId的數組long[],一個用戶的喜好評分數據float[],而不是一個Preference對象的集合,它只有較少的對象需要被創建和gc的檢查。

用《Mahout in action》一書中的原話“mahout has alreadly reinvented an 'array of Java objects'”——"mahout已經重新改造了Java對象數組"。PreferenceArray和它的具體實現減少的內存開銷遠遠比它的的複雜性有價值,它減少了近75%的內存開銷(相對於Java的對象集合)

除了PreferenceArray,Mahout中還大量使用了像Map和Set這些非常典型的數據結構,但是Mahout沒有直接使用像HashMap和TreeSet這些常用的Java集合實現,取而代之的是專門爲Mahout推薦的需要實現了兩個API,FastByIDMap和FastIDSet,之所以專門封裝了這兩個數據結構,主要目的是爲了減少內存的開銷,提高性能。它們之間主要有以下區別:
* 和HashMap一樣,FastByIDMap也是基於hash的。不過FastByIDMap使用的是線性探測來解決hash衝突,而不是分離鏈;
* FastByIDMap的key和值都是long類型,而不是Object,這是基於節省內存開銷和改善性能所作的改良;
* FastByIDMap類似於一個緩存區,它有一個“maximum size”的概念,當我們添加一個新元素的時候,如果超過了這個size,那些使用不頻繁的元素就會被移除。

FastByIDMap和FastIDSet在存儲方面的改進非常顯著。FastIDSet的每個元素平均佔14字節,而HashSet而需要84字節;FastByIDMap的每個entry佔28字節,而HashMap則需要84字節。

DataModel
Mahout推薦引擎實際接受的輸入是DataModel,它是對用戶喜好數據的壓縮表示。DataModel的具體實現支持從任意類型的數據源抽取用戶喜好信息,可以很容易的返回輸入的喜好數據中關聯到一個物品的用戶ID列表和count計數,以及輸入數據中所有用戶和物品的數量。具體實現包括內存版的GenericDataModel,支持文件讀取的FileDataModel和支持數據庫讀取的JDBCDataModel。
DataModel

GenericDataModel是DataModel的內存版實現。適用於在內存中構造推薦數據,它僅只是作爲推薦引擎的輸入接受用戶的喜好數據,保存着一個按照用戶ID和物品ID進行散列的PreferenceArray,而PreferenceArray中對應保存着這個用戶ID或者物品ID的所有用戶喜好數據。
GenericDataModel

FileDataModel支持文件的讀取,Mahout對文件的格式沒有太多嚴格的要求,只要滿足一下格式就OK:
* 每一行包含一個用戶Id,物品Id,用戶喜好
* 逗號隔開或者Tab隔開
* *.zip 和 *.gz 文件會自動解壓縮(Mahout 建議在數據量過大時採用壓縮的數據存儲)
FileDataModel從文件中讀取數據,然後將數據以GenericDataModel的形式載入內存,具體可以查看FileDataModel中的buildModel方法。

JDBCDataModel支持對數據庫的讀取操作,Mahout提供了對MySQL的默認支持MySQLJDBCDataModel,它對用戶喜好數據的存儲有以下要求:
* 用戶ID列需要是BIGINT而且非空
* 物品ID列需要是BIGINT而且非空
* 用戶喜好值列需要是FLOAT
* 建議在用戶ID和物品ID上建索引

有的時候,我們會忽略用戶的喜好值,僅僅只關心用戶和物品之間存不存在關聯關係,這種關聯關係在Mahout裏面叫做“boolean preference”。 之所以會有這類喜好,是因爲用戶和物品的關聯要麼存在,要麼不存在,記住只是表示關聯關係存不存在,不代表喜歡和不喜歡。實際上一條“boolean preference”可有三個狀態:喜歡、不喜歡、沒有任何關係。

在喜好數據中有大量的噪音數據的情況下,這種特殊的喜好評定方式是有意義的。 同時Mahout爲“boolean preference”提供了一個內存版的DataModel——GenericBooleanPrefDataModel
GenericBooleanPrefDataModel

可以看到,GenericBooleanPrefDataModel沒有對喜好值進行存儲,僅僅只存儲了關聯的userId和itemId,注意和GenericDataModel的差別,GenericBooleanPrefDataModel採用了FastIDSet,只有關聯的Id,沒有喜好值。因此它的一些方法(繼承自DataModel的)如getItemIDsForUser()有更好的執行速度,而getPreferencesFromUser()的執行速度會更差,因爲GenericBooleanPrefDataModel本來就沒存儲喜好值,它默認用戶對物品的喜好值都是1.0
@Override
public Float getPreferenceValue(long userID, long itemID) throws NoSuchUserException {
  FastIDSet itemIDs = preferenceFromUsers.get(userID);
  if (itemIDs == null) {
    throw new NoSuchUserException(userID);
  }
  if (itemIDs.contains(itemID)) {
    return 1.0f;
  }
  return null;
}
發佈了53 篇原創文章 · 獲贊 8 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章