基于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());
            }
        }
    }
}

 

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