Mahout 是 Apache旗下的一个开源项目,基于由以前的研究人员研究的经典推荐算法,它抽象和实现理论研究成果,将理论知识中的复杂运算进行封装,并支持多种数据源格式(文件,数据库等)并提供了多种可扩展的选项,将其集成到框架中去,使得开发人员能够通过API直接使用该算法实现,通过工具集隐藏其复杂的底层实现,帮助我们创建智能应用程序更快,更简单。此外,通过使用 Apache Hadoop 库,Mahout 可以有效地扩展到云中。
Mahout 当前已实现的三个具体的机器学习任务。它们正好也是实际应用程序中相当常见的三个领域:
Apache Mahout的核心部分是taste组件库,它实现了算法的推荐引擎,它提取成一个工具集,并规范了程序的开发过程:
总结一下,主要分为三个模块:数据模型,相似度算法(用户相似度,项目相似度),推荐算法。推荐系统架构下图所示:
1. 协作筛选(CF)
它使用评分、单击和购买等用户信息为其他站点用户提供推荐产品。应用程序根据用户和项目历史向系统的当前用户提供推荐。生成推荐的 4 种典型方法如下:
所有 CF 方法最终都需要计算用户及其评分项目之间的相似度。可以通过许多方法来计算相似度,并且大多数 CF 系统都允许插入不同的指标,以便确定最佳结果。
2. 集群
对于大型数据集来说,无论它们是文本还是数值,一般都可以将类似的项目自动组织,或 集群,到一起。
与 CF 类似,集群计算集合中各项目之间的相似度,但它的任务只是对相似的项目进行分组。在许多集群实现中,集合中的项目都是作为矢量表示在 n维度空间中的。通过矢量,开发人员可以使用各种指标(比如说曼哈顿距离、欧氏距离或余弦相似性)来计算两个项目之间的距离。然后,通过将距离相近的项目归类到一起,可以计算出实际集群。
可以通过许多方法来计算集群,每种方法都有自己的利弊。一些方法从较小的集群逐渐构建成较大的集群,还有一些方法将单个大集群分解为越来越小的集群。在发展成平凡集群表示之前(所有项目都在一个集群中,或者所有项目都在各自的集群中),这两种方法都会通过特定的标准退出处理。流行的方法包括 k-Means 和分层集群。如下所示,Mahout 也随带了一些不同的集群方法。
3. 分类
分类(通常也称为 归类)的目标是标记不可见的文档,从而将它们归类不同的分组中。分类功能的特性可以包括词汇、词汇权重(比如说根据频率)和语音部件等。
Mahout 目前提供了一些工具,可用于通过 Taste 库建立一个推荐引擎 —针对 CF 的快速且灵活的引擎。Taste 支持基于用户和基于项目的推荐,并且提供了许多推荐选项,以及用于自定义的界面。Taste 包含 5 个主要组件,用于操作 用户
、项目
和 首选项
:
DataModel
:用于存储 用户
、项目
和 首选项
UserSimilarity
:用于定义两个用户之间的相似度的界面ItemSimilarity
:用于定义两个项目之间的相似度的界面Recommender
:用于提供推荐的界面UserNeighborhood
:用于计算相似用户邻近度的界面,其结果随时可由 Recommender
使用在mahout推荐引擎中,除了基础推荐算法的封装,它还提供了用于评估推荐效果的指标,即召回率(recall)和查准率(precision),这些指标由封装在mahout中的GenericRecommenderIRStatsEvaluator类实现。
本次毕设的主题是旅游推荐系统,通过webMagic爬虫进行旅游数据的爬取并持久化,希望通过mahout所提供的几种经典的推荐算法实现,从而达到根据用户,项目,计算相似用户邻近度等方式来实现旅游资讯的推荐。
本次以maven形式引入依赖
org.apache.mahout
mahout-core
0.9
org.apache.mahout
mahout-integration
0.11.1
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> 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>recommendScenicByUserBased(Integer userId);
public MahoutResultModel> recommendScenicByitemBased(Integer userId);
public MahoutResultModel> 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 recommendScenics(int userId, int size, String recommendType) {
List 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 userBasedRecommender(long userID, int size) {
// step:1 构建模型 2 计算相似度 3 查找k紧邻 4 构造推荐引擎
List 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 myItemBasedRecommender(long userID,int size){
List 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 mySlopeOneRecommender(long userID,int size){
List 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> doRecommendScenicBy(Integer userId, String recommendType) {
List recommendedItems = this.recommendScenics(userId,
MahoutConstant.RECOMMEND_SIZE, recommendType);
Predicate> recommendedItemsCheck =
scenicList -> scenicList != null && scenicList.size() > 0;
if (recommendedItemsCheck.test(recommendedItems)) {
Integer recommendedCountsByMahout = recommendedItems.size();
List 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>(this.compensateData(recommendScenics),
recommendedCountsByMahout);
}
return new MahoutResultModel>(this.compensateData(MahoutConstant.EMPTY_RESULT),
MahoutConstant.EMPTY_RESULT_SIZE);
}
5.数据补偿
为防止推荐结果集较小,若推荐量小于20,则进行数据补偿,按景点热度(业务需要)
/**
* 推荐数据补偿(若推荐数量小于20,则进行补偿:按热度值降序)
*
* 业务逻辑:
* 1.先将已推荐数据集放入Set
* 2.将补偿集放入Set集合中,每次注意判断集合的大小是否达到20,达到则不再放入;
* 3.将set集合转换成list返回
*
* @param recommendScenics
* @return
*/
private List compensateData(List recommendScenics) {
if (Optional.ofNullable(recommendScenics).isPresent()) {
Set 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 {
/**
* 推荐数据集(包含补偿数据)
*/
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());
}
}
}
}