为豆瓣电影实现User-based协同过滤的推荐系统

使用mahout实现user-based cf的电影推荐

协同过滤(Collaborative Filtering),简单来说是利用某兴趣相投、拥有共同经验之群体的喜好来推荐使用者感兴趣的信息,个人透过合作的机制给予信息相当程度的反馈(如评分)并记录下来以达到过滤的目的进而帮助别人筛选信息,反馈不一定局限于特别感兴趣的,特别不感兴趣信息的纪录也相当重要,比如浏览信息,收藏,分享,点击等。

在前一篇文章 使用Spark MLlib给豆瓣用户推荐电影 中,在那篇文章我我介绍了使用Spark MLlib实现了model-based 的系统过滤之推荐系统。但是Spark并没有提供user-based, item-based这两种memory-based传统的协同过滤算法,所以接下来的这两篇文章我会介绍使用Mahout实现user-based和item-based的电影推荐系统,数据还是基于豆瓣用户对电影的评论数据集。

Mahout是Apache的实现大规模的高性能的机器学习框架。它提供了很多的机器学习的算法和工具,以及利用Hadoop实现分布式的计算,本文将使用它的协同过滤算法(CF)实现非分布式的单机程序。

user-based协同过滤推荐算法就是通过不同用户对item的评分来评测用户之间的相似性,基于用户之间的相似性做出推荐。
下一篇文章中介绍item-based协同过滤推荐算法是通过用户对不同item的评分来评测item之间的相似性,基于item之间的相似性做出推荐。

User-based 优点

  • 能够过滤机器难以自动内容分析的信息,如艺术品,音乐等。
  • 共用其他人的经验,避免了内容分析的不完全或不精确,并且能够基于一些复杂的,难以表述的概念(如信息质量、个人品味)进行过滤。
  • 有推荐新信息的能力。可以发现内容上完全不相似的信息,使用者对推荐信息的内容事先是预料不到的。可以发现使用者潜在的但自己尚未发现的兴趣偏好。
  • 推荐个性化、自动化程度高。能够有效的利用其他相似使用者的反馈信息。加快个性化学习的速度。

缺点

  • 新使用者问题(New User Problem) 系统开始时推荐质量较差
  • 新项目问题(New Item Problem) 质量取决于历史资料集
  • 稀疏性问题(Sparsity)
  • 系统延伸性问题(Scalability)。

下面根据代码介绍具体的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class DoubanUserBasedRecommender {
public static Map<Long, String> getMovies(String base) {
Map<Long, String> movies = new HashMap<>();
try {
File file = new File(base + "hot_movies.csv");
FileLineIterator iterator = new FileLineIterator(file, false);
String line = iterator.next();
while (!line.isEmpty()) {
String[] m = line.split(",");
movies.put(Long.parseLong(m[0]), m[2]);
line = iterator.next();
}
Closeables.close(iterator, true);
} catch (Exception ex) {
}
return movies;
}
......
}

上面一段代码是生成电影ID和名称的字典,这个文件的每一行代表一条热门电影,如20645098,8.2,小王子。这样在我们输出结果的时候,可以方便的查看电影名,因为电影名比ID更有意义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class DoubanUserBasedRecommender {
......
public static void main(String[] args) throws Exception {
String base = args[0];
File file = new File(base + "user_movies.csv");
DoubanFileDataModel model = new DoubanFileDataModel(file);
//皮尔逊相似度
UserSimilarity similarity = new PearsonCorrelationSimilarity(model);
UserNeighborhood neighborhood = new NearestNUserNeighborhood(2, similarity, model);
Recommender recommender = new GenericUserBasedRecommender(model, neighborhood, similarity);
Recommender cachingRecommender = new CachingRecommender(recommender);
//查看一些结果
Map<Long, String> movies = getMovies(base);
for (long userID = 0; userID < 100; userID++) {
String userName = model.userIDAndNameMapping.get(userID);
List<RecommendedItem> recommendations = cachingRecommender.recommend(userID, 2);
System.out.print("为用户 " + userName + " 推荐电影:");
for (RecommendedItem recommendation : recommendations) {
System.out.print(recommendation.getItemID() + "," + movies.get(recommendation.getItemID()) + " ");
}
System.out.println();
}
//输出结果到文件
PrintWriter writer = new PrintWriter(base + "result.csv", "UTF-8");
for (long userID = 0; userID < model.userIDAndNameMapping.size(); userID++) {
String userName = model.userIDAndNameMapping.get(userID);
List<RecommendedItem> recommendations = cachingRecommender.recommend(userID, 5);
if (recommendations.size() > 0) {
String line = userName + ",";
for (RecommendedItem recommendation : recommendations) {
line += recommendation.getItemID() + ":" + movies.get(recommendation.getItemID()) + ",";
}
if (line.endsWith(","))
line = line.substring(0, line.length() - 1);
writer.println(line);
}
}
writer.close();

6行读入数据模型,因为我们的数据文件中用户的id是字符串类型的,我们需要将它转换成一个Long类型的数据,所以实现了一个定制的类DoubanFileDataModel
8行我们使用PearsonCorrelationSimilarity计算相似度,Mahout还提供了其它的计算相似度的算法:

1
2
3
4
5
6
7
8
9
10
11
12
//曼哈顿相似度
//UserSimilarity similarity = new org.apache.mahout.cf.taste.impl.similarity.CityBlockSimilarity(model);
//欧几里德相似度
//UserSimilarity similarity = new org.apache.mahout.cf.taste.impl.similarity.EuclideanDistanceSimilarity(model);
//对数似然相似度
//UserSimilarity similarity = new org.apache.mahout.cf.taste.impl.similarity.LogLikelihoodSimilarity(model);
//斯皮尔曼相似度
//UserSimilarity similarity = new org.apache.mahout.cf.taste.impl.similarity.SpearmanCorrelationSimilarity(model);
//Tanimoto 相似度
//UserSimilarity similarity = new org.apache.mahout.cf.taste.impl.similarity.TanimotoCoefficientSimilarity(model)
//Cosine相似度
//UserSimilarity similarity = new org.apache.mahout.cf.taste.impl.similarity.UncenteredCosineSimilarity();

10生成UserBased Recommender类。
13行到第22行我们为前100个用户生成推荐结果,并输出到终端窗口,这样我们可以先检查一下推荐的结果。
因为矩阵是很稀疏的,这种user-based算法可能没有提供给用户推荐的电影,有的用户可能不到5个推荐电影。
这段代码剩下的部分就是把所有的用户推荐结果都输出到一个文件中。

DoubanFileDataModel类的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class DoubanFileDataModel extends FileDataModel {
public static Map<String,Long> userNameAndIDMapping = new HashMap<>();
public static Map<Long,String> userIDAndNameMapping = new HashMap<>();
private static long userID = 0;
public DoubanFileDataModel(File dataFile) throws IOException {
super(dataFile);
}
public DoubanFileDataModel(File dataFile, String delimiterRegex) throws IOException {
super(dataFile, delimiterRegex);
}
public DoubanFileDataModel(File dataFile, boolean transpose, long minReloadIntervalMS) throws IOException {
super(dataFile, transpose, minReloadIntervalMS);
}
public DoubanFileDataModel(File dataFile, boolean transpose, long minReloadIntervalMS, String delimiterRegex) throws IOException {
super(dataFile, transpose, minReloadIntervalMS, delimiterRegex);
}
@Override
protected long readUserIDFromString(String value) {
value = value.trim();
if (userNameAndIDMapping.containsKey(value)) {
return userNameAndIDMapping.get(value);
}
userNameAndIDMapping.put(value, userID);
userIDAndNameMapping.put(userID, value);
userID++;
return (userID -1);
}
@Override
protected long readItemIDFromString(String value) {
value = value.trim();
return super.readItemIDFromString(value);
}
}

下面是推荐结果的片段:

1
2
3
4
5
6
7
8
9
10
yuan521123,1866473:蚁人
58472013,26289144:滚蛋吧!肿瘤君,2973079:霍比特人3:五军之战,23761370:速度与激情7,25746375:我是路人甲
65532408,26021055:栀子花开
68333051,10533913:头脑特工队,10741643:我的个神啊,10827341:疯狂外星人,25823833:天将雄师,26289144:滚蛋吧!肿瘤君
80755814,11624706:小黄人大眼萌,10533913:头脑特工队,25723907:捉妖记,26289144:滚蛋吧!肿瘤君,24879839:道士下山
129734802,25908042:横冲直撞好莱坞
127252296,10741643:我的个神啊,25723907:捉妖记,2973079:霍比特人3:五军之战,6126442:一步之遥
aellr,6846893:超能查派,25779218:匆匆那年,24879839:道士下山
67656730,3338862:终结者:创世纪,6845667:秘密特工,6873042:明日世界,25752261:女间谍,25823833:天将雄师
ravinenoravine,25838463:像素大战

比如用户 52973703,我们为ta推荐:

user-based CF算法适用于:

  • 用户较少的场合,否则用户相似度矩阵计算代价很大
  • 适合时效性较强,用户个性化兴趣不太明显的领域
  • 对新用户不友好,对新物品友好,因为用户相似度矩阵不能实时计算
  • 很难提供令用户信服的推荐解释

item-based CF算法适用于:

  • 物品数明显小于用户数的场合,否则物品相似度矩阵计算代价很大
  • 适合长尾物品丰富,用户个性化需求强的领域
  • 对新用户友好,对新物品不友好,因为物品相似度矩阵不需要很强的实时性
  • 利用用户历史行为做推荐解释,比较令用户信服

推荐阅读项亮的推荐系统实践,了解推荐系统的基础知识。

代码下载: github