什麼是Mahout?
Mahout 是 Apache旗下的一個開源項目,基於由以前的研究人員研究的經典推薦算法,它抽象和實現理論研究成果,將理論知識中的複雜運算進行封裝,並支持多種數據源格式(文件,數據庫等)並提供了多種可擴展的選項,將其集成到框架中去,使得開發人員能夠通過API直接使用該算法實現,通過工具集隱藏其複雜的底層實現,幫助我們創建智能應用程序更快,更簡單。此外,通過使用 Apache Hadoop 庫,Mahout 可以有效地擴展到雲中。
mahout封裝的基本功能
Mahout 當前已實現的三個具體的機器學習任務。它們正好也是實際應用程序中相當常見的三個領域:
- 協作篩選
- 集羣
- 分類
Mahout核心推薦引擎架構
Apache Mahout的核心部分是taste組件庫,它實現了算法的推薦引擎,它提取成一個工具集,並規範了程序的開發過程:
- common:公共組件,這裏提供了全局異常TasteException,數據刷新接口Refreshable,權重常量Weighting;
- eval: 構造器接口。用於創建推薦和數據模型,採用的工廠模式的思想;
- Hadoop:提供hadoop組件生態支持;
- Impl: 實現類;
- Model:數據模型;
- Neighborhood: 鄰近算法;
- Recommender: 推薦算法;
- Similarity: 相似度算法;
總結一下,主要分爲三個模塊:數據模型,相似度算法(用戶相似度,項目相似度),推薦算法。推薦系統架構下圖所示:
基本概念
1. 協作篩選(CF)
它使用評分、單擊和購買等用戶信息爲其他站點用戶提供推薦產品。應用程序根據用戶和項目歷史向系統的當前用戶提供推薦。生成推薦的 4 種典型方法如下:
- 基於用戶:通過查找相似的用戶來推薦項目。由於用戶的動態特性,這通常難以定量。
- 基於項目:計算項目之間的相似度並做出推薦。項目通常不會過多更改,因此這通常可以離線完成。
- Slope-One:非常快速簡單的基於項目的推薦方法,需要使用用戶的評分信息(而不僅僅是布爾型的首選項)。
- 基於模型:通過開發一個用戶及評分模型來提供推薦。
所有 CF 方法最終都需要計算用戶及其評分項目之間的相似度。可以通過許多方法來計算相似度,並且大多數 CF 系統都允許插入不同的指標,以便確定最佳結果。
2. 集羣
對於大型數據集來說,無論它們是文本還是數值,一般都可以將類似的項目自動組織,或 集羣,到一起。
與 CF 類似,集羣計算集合中各項目之間的相似度,但它的任務只是對相似的項目進行分組。在許多集羣實現中,集合中的項目都是作爲矢量表示在 n維度空間中的。通過矢量,開發人員可以使用各種指標(比如說曼哈頓距離、歐氏距離或餘弦相似性)來計算兩個項目之間的距離。然後,通過將距離相近的項目歸類到一起,可以計算出實際集羣。
可以通過許多方法來計算集羣,每種方法都有自己的利弊。一些方法從較小的集羣逐漸構建成較大的集羣,還有一些方法將單個大集羣分解爲越來越小的集羣。在發展成平凡集羣表示之前(所有項目都在一個集羣中,或者所有項目都在各自的集羣中),這兩種方法都會通過特定的標準退出處理。流行的方法包括 k-Means 和分層集羣。如下所示,Mahout 也隨帶了一些不同的集羣方法。
3. 分類
分類(通常也稱爲 歸類)的目標是標記不可見的文檔,從而將它們歸類不同的分組中。分類功能的特性可以包括詞彙、詞彙權重(比如說根據頻率)和語音部件等。
基於Taste庫構建推薦引擎
Mahout 目前提供了一些工具,可用於通過 Taste 庫建立一個推薦引擎 —針對 CF 的快速且靈活的引擎。Taste 支持基於用戶和基於項目的推薦,並且提供了許多推薦選項,以及用於自定義的界面。Taste 包含 5 個主要組件,用於操作 用戶
、項目
和 首選項
:
DataModel
:用於存儲用戶
、項目
和首選項
UserSimilarity
:用於定義兩個用戶之間的相似度的界面ItemSimilarity
:用於定義兩個項目之間的相似度的界面Recommender
:用於提供推薦的界面UserNeighborhood
:用於計算相似用戶鄰近度的界面,其結果隨時可由Recommender
使用
推薦效果評估
在mahout推薦引擎中,除了基礎推薦算法的封裝,它還提供了用於評估推薦效果的指標,即召回率(recall)和查準率(precision),這些指標由封裝在mahout中的GenericRecommenderIRStatsEvaluator類實現。
基於Mahout進行推薦模塊開發
本次畢設的主題是旅遊推薦系統,通過webMagic爬蟲進行旅遊數據的爬取並持久化,希望通過mahout所提供的幾種經典的推薦算法實現,從而達到根據用戶,項目,計算相似用戶鄰近度等方式來實現旅遊資訊的推薦。
本次以maven形式引入依賴
<!--mahout-->
<dependency>
<groupId>org.apache.mahout</groupId>
<artifactId>mahout-core</artifactId>
<version>0.9</version>
</dependency>
<dependency>
<groupId>org.apache.mahout</groupId>
<artifactId>mahout-integration</artifactId>
<version>0.11.1</version>
</dependency>
mysql數據源的獲取
通過spring bean的方式獲取連接對象
/**
* mahout數據源
*
* @author [email protected]
* 2019-02-25 10:58
* @version 1.0.0
*/
@Component
@Slf4j
public class MyDataModel {
private DruidDataSource dataSource = (DruidDataSource) SpringUtil.getBean(DruidDataSource.class);
public JDBCDataModel myDataModel() {
JDBCDataModel dataModel = null;
try {
dataModel = new MySQLJDBCDataModel(dataSource, MahoutConstant.PREFERENCE_TABLE,
MahoutConstant.USERID_COLUMN, MahoutConstant.ITEMID_COLUMN,
MahoutConstant.PREFERENCE_COLUMN, MahoutConstant.TIMESTAMP_COLUMN);
} catch (Exception e) {
log.warn("【MyDataModel數據源獲取異常】");
e.printStackTrace();
throw new PenguinException(ResultEnum.DB_SOURCE_ERROR.getCode(), "【MyDataModel數據源獲取異常】");
}
return dataModel;
}
}
1.算法選擇:userBased,itemBased,slop one(考慮通過策略模式進行解耦)
/** 推薦模塊控制層
*
* @author [email protected]
* 2019-03-03 20:02
* @version 1.0.0
*/
@Controller
@RequestMapping("mahout")
public class MahoutController {
@Autowired
private MahoutService mahoutService;
@ResponseBody
@RequestMapping("recommendScenicBy")
public MahoutResultModel<List<Scenic>> recommendScenicBy(Integer userId,String recommendType){
if(MahoutConstant.USER_BASED.equals(recommendType)){
return mahoutService.recommendScenicByUserBased(userId);
}else if(MahoutConstant.ITEM_BASED.equals(recommendType)){
return mahoutService.recommendScenicByitemBased(userId);
}else if(MahoutConstant.SLOPE_ONE.equals(recommendType)){
return mahoutService.recommendScenicByBySlopeOne(userId);
}
return new MahoutResultModel<>(MahoutConstant.EMPTY_RESULT,userId);
}
}
2. 提供三個推薦算法接口
返回推薦景點數據集合
/**
* @author [email protected]
* 2019-03-03 15:23
* @version 1.0.0
*/
public interface MahoutService {
public MahoutResultModel<List<Scenic>>recommendScenicByUserBased(Integer userId);
public MahoutResultModel<List<Scenic>> recommendScenicByitemBased(Integer userId);
public MahoutResultModel<List<Scenic>> recommendScenicByBySlopeOne(Integer userId);
}
3.具體算法實現
依賴mahout模塊,依次返回MyUserBasedRecommender,MyItemBasedRecommender,MySlopeOneRecommender推薦器
/**
* Taste引擎核心推薦算法(1.基於用戶;2.基於項目;3.基於slop one)
*
* @param userId
* @param size
* @param recommendType
* @return
* @see cn.jyycode.mahout.constant.MahoutConstant
*/
private List<RecommendedItem> recommendScenics(int userId, int size, String recommendType) {
List<RecommendedItem> recommendation = null;
if (recommendType.equals(MahoutConstant.USER_BASED)) {
MyUserBasedRecommender mubr = new MyUserBasedRecommender();
recommendation = mubr.userBasedRecommender(userId, size);
} else if (recommendType.equals(MahoutConstant.ITEM_BASED)) {
MyItemBasedRecommender mibr = new MyItemBasedRecommender();
recommendation = mibr.myItemBasedRecommender(userId, size);
} else if (recommendType.equals(MahoutConstant.SLOPE_ONE)) {
MySlopeOneRecommender msor = new MySlopeOneRecommender();
recommendation = msor.mySlopeOneRecommender(userId, size);
}
return recommendation;
}
3.1 基於用戶相似度推薦
public class MyUserBasedRecommender {
private JDBCDataModel model;
public List<RecommendedItem> userBasedRecommender(long userID, int size) {
// step:1 構建模型 2 計算相似度 3 查找k緊鄰 4 構造推薦引擎
List<RecommendedItem> recommendations = null;
try {
DataModel model = new MyDataModel().myDataModel();//構造數據模型
UserSimilarity similarity = new PearsonCorrelationSimilarity(model);//用PearsonCorrelation 算法計算用戶相似度
UserNeighborhood neighborhood = new NearestNUserNeighborhood(100, similarity, model);//計算用戶的“鄰居”,這裏將與該用戶最近距離爲 3 的用戶設置爲該用戶的“鄰居”。
Recommender recommender = new CachingRecommender(new GenericUserBasedRecommender(model, neighborhood, similarity));//採用 CachingRecommender 爲 RecommendationItem 進行緩存
recommendations = recommender.recommend(userID, size);//得到推薦的結果,size是推薦接過的數目
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
recommendations.stream().forEach(System.out::print);
return recommendations;
}
}
3.2 基於項目協同推薦
public class MyItemBasedRecommender {
public List<RecommendedItem> myItemBasedRecommender(long userID,int size){
List<RecommendedItem> recommendations = null;
try {
DataModel model = new FileDataModel(new File("/home/huhui/movie_preferences.txt"));//構造數據模型,File-based
ItemSimilarity similarity = new PearsonCorrelationSimilarity(model);//計算內容相似度
Recommender recommender = new GenericItemBasedRecommender(model, similarity);//構造推薦引擎
recommendations = recommender.recommend(userID, size);//得到推薦結果
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
return recommendations;
}
}
3.3 基於slop one推薦(待完善)
public class MySlopeOneRecommender {
public List<RecommendedItem> mySlopeOneRecommender(long userID,int size){
List<RecommendedItem> recommendations = null;
try {
DataModel model = new FileDataModel(new File("/home/huhui/movie_preferences.txt"));//構造數據模型
/*Recommender recommender = new CachingRecommender(new SlopeOneRecommender(model));//構造推薦引擎
recommendations = recommender.recommend(userID, size);//得到推薦結果*/
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
return recommendations;
}
}
4.將結果集進行處理並返回
/**
* 推薦算法選擇和數據處理
*
* @param userId
* @param recommendType
* @return
* @see cn.jyycode.mahout.constant.MahoutConstant
*/
private MahoutResultModel<List<Scenic>> doRecommendScenicBy(Integer userId, String recommendType) {
List<RecommendedItem> recommendedItems = this.recommendScenics(userId,
MahoutConstant.RECOMMEND_SIZE, recommendType);
Predicate<List<RecommendedItem>> recommendedItemsCheck =
scenicList -> scenicList != null && scenicList.size() > 0;
if (recommendedItemsCheck.test(recommendedItems)) {
Integer recommendedCountsByMahout = recommendedItems.size();
List<Scenic> recommendScenics = scenicMapper.selectByPrimaryKeys(
recommendedItems.stream()
.map(recommendedItem -> Integer.parseInt(String.valueOf(recommendedItem.getItemID())))
.collect(Collectors.toList()));
recommendScenics = recommendScenics.stream().map(recommendScenic -> {
recommendedItems.stream().forEach(recommendedItem -> {
if(recommendedItem.getItemID() == recommendScenic.getScenicId()){
recommendScenic.setRecommened(true);
recommendScenic.setRecommendValue(recommendedItem.getValue());
}
});
return recommendScenic;
}).collect(Collectors.toList());
return new MahoutResultModel<List<Scenic>>(this.compensateData(recommendScenics),
recommendedCountsByMahout);
}
return new MahoutResultModel<List<Scenic>>(this.compensateData(MahoutConstant.EMPTY_RESULT),
MahoutConstant.EMPTY_RESULT_SIZE);
}
5.數據補償
爲防止推薦結果集較小,若推薦量小於20,則進行數據補償,按景點熱度(業務需要)
/**
* 推薦數據補償(若推薦數量小於20,則進行補償:按熱度值降序)
* <p>
* 業務邏輯:
* 1.先將已推薦數據集放入Set
* 2.將補償集放入Set集合中,每次注意判斷集合的大小是否達到20,達到則不再放入;
* 3.將set集合轉換成list返回
*
* @param recommendScenics
* @return
*/
private List<Scenic> compensateData(List<Scenic> recommendScenics) {
if (Optional.ofNullable(recommendScenics).isPresent()) {
Set<Scenic> scenicSet = new LinkedHashSet<>(recommendScenics);
scenicMapper.selectScenicOrderByHeatRate(
new PageSupport(MahoutConstant.DEFAULT_PAGE_INDEX, MahoutConstant.COMPENSTAE_SIZE))
.stream()
.forEach(scenic -> {
if (scenicSet.size() <= MahoutConstant.COMPENSTAE_SIZE) {
scenicSet.add(scenic);
}
});
return scenicSet.stream().collect(Collectors.toList());
} else {
return scenicMapper.selectScenicOrderByHeatRate(
new PageSupport(MahoutConstant.DEFAULT_PAGE_INDEX, MahoutConstant.COMPENSTAE_SIZE))
.stream()
.collect(Collectors.toList());
}
}
6.進行數據集的校驗
/**
* mahout service自定義統一返回處理結果
*
* @author [email protected]
* 2019-03-04 10:58
* @version 1.0.0
*/
@Data
@Slf4j
public class MahoutResultModel<T> {
/**
* 推薦數據集(包含補償數據)
*/
private T data;
/**
* 基於mahout計算出的數據集大小
*/
private Integer recommendedCountsByMahout;
/**
* 基於mahout計算出的推薦係數
*/
/*private double recommendValue;*/
public MahoutResultModel(T data, Integer recommendedCountsByMahout) {
this.data = data;
this.recommendedCountsByMahout = recommendedCountsByMahout;
if (Optional.ofNullable(data).isPresent()) {
if (((List) data).size() != MahoutConstant.RECOMMEND_SIZE) {
log.info("【警告:推薦集結果大小檢查異常】 size:{}",((List) data).size());
}
}
}
}