基於Mahout構建推薦引擎

什麼是Mahout?

        Mahout 是 Apache旗下的一個開源項目,基於由以前的研究人員研究的經典推薦算法,它抽象和實現理論研究成果,將理論知識中的複雜運算進行封裝,並支持多種數據源格式(文件,數據庫等)並提供了多種可擴展的選項,將其集成到框架中去,使得開發人員能夠通過API直接使用該算法實現,通過工具集隱藏其複雜的底層實現,幫助我們創建智能應用程序更快,更簡單。此外,通過使用 Apache Hadoop 庫,Mahout 可以有效地擴展到雲中。

mahout封裝的基本功能

 Mahout 當前已實現的三個具體的機器學習任務。它們正好也是實際應用程序中相當常見的三個領域:

  • 協作篩選
  • 集羣
  • 分類

Mahout核心推薦引擎架構

 Apache Mahout的核心部分是taste組件庫,它實現了算法的推薦引擎,它提取成一個工具集,並規範了程序的開發過程:

  1. common:公共組件,這裏提供了全局異常TasteException,數據刷新接口Refreshable,權重常量Weighting;
  2. eval: 構造器接口。用於創建推薦和數據模型,採用的工廠模式的思想;
  3. Hadoop:提供hadoop組件生態支持;
  4. Impl: 實現類;
  5. Model:數據模型;
  6. Neighborhood: 鄰近算法;
  7. Recommender: 推薦算法;
  8. 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());
            }
        }
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章