dongjun111111 / blog

BLOG
36 stars 5 forks source link

推荐系统Mahout(Java)介绍 #55

Open dongjun111111 opened 8 years ago

dongjun111111 commented 8 years ago

推荐系统Mahout(Java)介绍

首先看第二章,推荐系统介绍。 Mahout可以处理的User ID和Item ID需要是整数,打分(Preference)可以是任意数值,大的数值代表更喜欢。 输入文件需要是csv格式,每行三个数字分别代表:User ID, Item ID, Preference 例如:

1,101,5.0
2,101,2.0

代表1号用户给101号物品打5.0分,2号用户给101号物品打2.0分。 有了数据文件,就可以利用Mahout写一个简单的推荐系统代码了。

DataModel model = new FileDataModel(new File("intro.csv"));
//建立DataModel对象存储数据文件
UserSimilarity similarity = new PearsonCorrelationSimilarity(model);
//计算用户相似度矩阵
UserNeighborhood neighborhood = new NearestNUserNeighborhood (2, similarity, model);
//对每个用户找出2个最相似用户
Recommender recommender =
 new GenericUserBasedRecommender(model, neighborhood, similarity);
//建立UserCF推荐系统
List recommendations = recommender.recommend(1, 1);
//为用户1推荐1个Item

以上是一个简单例子,留个印象,可以先不用纠结。 然后就是需要一个推荐系统精确度衡量标准,衡量标准有MAE(误差绝对值的平均值)和RMSE(误差平方求平均再开根)等。 可以同样看一个简单例子,Mahout衡量推荐系统精确度的。

RandomUtils.useTestSeed();
//用来使得每次随机都一样,仅限用于测试,不要在实际中使用
DataModel model = new FileDataModel(new File("intro.csv"));
RecommenderEvaluator evaluator = new AverageAbsoluteDifferenceRecommenderEvaluator();
//使用MAE作为衡量标准,如果用RMSE则是 RMSRecommenderEvaluator
RecommenderBuilder builder = new RecommenderBuilder() {
  @Override
  public Recommender buildRecommender(DataModel model) throws TasteException {
    UserSimilarity similarity = new PearsonCorrelationSimilarity (model);
    UserNeighborhood neighborhood = new NearestNUserNeighborhood (2, similarity, model);
    return new GenericUserBasedRecommender (model, neighborhood, similarity);
  }
};
double score = evaluator.evaluate(builder, null, model, 0.7, 1.0);
//随机使用70%数据做为训练,另外30%作为测试,
//最后一个参数1.0表示使用全部的数据,如果数据太大,
//可以只用一部分数据去近似衡量推荐系统准确度,
//在仅有小改动之后做部分数据测试比较快捷。

然后除了这种精确度衡量方式,Mahout还提供其他方式。这主要是因为:在实际推荐系统中,很多情况下并没有必要准确预测打分,而只需要对待推荐物品排序,由最好到最差,就足够了。另外还有一个原因是,对于部分问题,输入数据是布尔类型的,也即我们只知道用户是否偏好,但是没有打分。 Mahout提供了精确率(Precision)和召回率(recall)两种方案:精确率是指推荐排名前列中,有多少是好的推荐(good recommendation);召回率是指好的推荐中,有多少个排在推荐排名前列。 这样我们还需要定义什么是好的推荐。 先来看看代码:

RecommenderIRStatsEvaluator evaluator = new GenericRecommenderIRStatsEvaluator ();
IRStatistics stats = evaluator.evaluate(
    recommenderBuilder, null, model, null, 2, //推荐2个,计算准确率和召回率
    GenericRecommenderIRStatsEvaluator.CHOOSE_THRESHOLD, 
    1.0);
System.out.println(stats.getPrecision());//输出准确率
System.out.println(stats.getRecall()); //输出召回率

假设有这样的一组数据:

5,101,4.0
5,102,3.0
5,103,2.0
5,104,4.0
5,105,3.5
5,106,4.0

再假设前三行出现在测试集中,则由于101物品的分数高,所以他是好的推荐(good recommendation),而102和103都是有效的,但不是好的。但是这又有一个问题,分数到多少算分数高?你可以传进一个参数自己指定一个阈值(threshold),也可以让系统指定。 系统指定阈值的方案为:计算用户打分的均值和标准差,则阈值=均值+标准差。这样通常有大约16%的打分被选中(假设打分呈正态分布)。 文中指出了这种方式存在的问题,推荐的物品其实并不必要是已知的才说明推荐得好。一个更复杂的情况是如果输入数据是布尔类型(只知道是否喜欢),则无法选择阈值,只能是从喜欢的物品中随机选择一些用于测试。

这是Mahout in action一书的第三章。

推荐系统的质量很大程度上依赖于数据,所以先讨论数据的存储。 如下代码生成一个打分数据:

new GenericPreference(123, 456, 3.0f)
//用户123对物品456的偏好为3.0

但是表示一组打分的时候通常不会使用 Collection 或者 Preference[] 这种方式。 然后我们讨论一些原因。 一个GenericPreference占用了20字节,这包括8字节的User ID (long),8字节的Item ID (long),4字节的打分(float)。 但在实际代码中它会占用28字节。这里包含了一些数据对齐之类的原因。所以实际使用内存是真正需要的28/20=140%,浪费非常严重。(实际数据对齐还和操作系统有关) 另外数据在大多数算法中其实都是需要以“某一个用户的所有打分”或者“某一个物品的所有打分”的这种形式列出来。在这种情况下,User ID和Item ID都只需要存储一次。因此,Mahout给了其他的更合适的选择。 PreferenceArray就是一个类似数组的形式存储。比如GenericUserPreferenceArray表示的是和一个用户有关的所有打分,它存储了一个User ID,一个Item ID数组还有一个打分数组。示例代码:

PreferenceArray user1Prefs = new GenericUserPreferenceArray(2);
user1Prefs.setUserID(0, 1L);
user1Prefs.setItemID(0, 101L);
user1Prefs.setValue(0, 2.0f);
user1Prefs.setItemID(1, 102L);
user1Prefs.setValue(1, 3.0f);
Preference pref = user1Prefs.get(1);

类似的,还有GenericItemPreferenceArray存储和某一个物品相关的所有打分信息。 除了数组以外,Mahout还有类似map和set的实现。这包括FastMap, FastByIDMap, FastIDSet。他们主要是为了提高速度,同时兼顾内存占用。直接用Java的HashSet占用内存较大。 FastByIDMap是基于hash的,使用线性探查法(linear probing)解决冲突,他就像是一个Cache,因为超过大小的话最不常用的一个会被替换掉。看一个示例:

FastByIDMap preferences = 
  new FastByIDMap();
PreferenceArray prefsForUser1 = new GenericUserPreferenceArray(10);
prefsForUser1.setUserID(0, 1L);
prefsForUser1.setItemID(0, 101L);
prefsForUser1.setValue(0, 3.0f);
prefsForUser1.setItemID(1, 102L);
prefsForUser1.setValue(1, 4.5f);
//...(8 more)
preferences.put(1L, prefsForUser1);
DataModel model = new GenericDataModel(preferences);

在GenericDataModel中,每个评分大约占用28字节内存,这除了数据还包括数据结构的需要(索引等)。 除了这种形式以外,数据的建立还可以基于文件。文件除了上一章说的可以是csv那种用逗号分隔的,也可以是tab分割的。 refresh()这个方法用来在读取文件后更新一下,使得数据结构按照需要的顺序存储。

DataModel dataModel = new FileDataModel(new File("input.csv");
Recommender recommender = new SlopeOneRecommender(dataModel);
...
recommender.refresh(null);

FileDataModel还支持更新文件,允许读取一个更新文件来修改之前读过的数据。比如:

1,108,3.0
1,103,

第一行表示添加(或者更新)用户1对物品108的打分为3.0,第二行表示删除用户1对物品103的打分。 更新文件必须和主文件在同一个目录下,他们在第一个.之前的文件名必须一致,例如主文件是foo.txt.gz,更新文件是 foo.1.txt.gz和 foo.2.txt.gz(这也说明数据文件是可以使用压缩文件的)。 由于数据量可能很大,Mahout提供了数据库接口,需要进行配置。由于不详细研究如何使用Mahout,所以这部分跳过。 然后讨论如何处理无打分的数据。例如一个网站统计用户浏览文章的记录,用于推荐其他文章。网站只有用户浏览的记录,一般不会让用户打分。另外,即使有打分,很多时候忽略打分的取值是更好的选择。比如用户1给电影103打了4.5分,我们可以只考虑用户1和电影103有关系。 在Mahout中,这种情况成为布尔偏好(Boolean preferences)。文中称缺少合适的词汇来表达这个意思,因为实际上这不代表只有yes和no两种偏好,应当是三种情况:喜欢,不喜欢,不详。 如果改为使用布尔偏好,则能大大减少内存消耗。在Mahout中使用GenericBooleanPrefDataModel存储布尔偏好的数据(代替GenericDataModel),它只存了FastIDSet,没有打分。要注意的是,getPreferenceValue()这个方法依然是可以用的,不会抛出异常。他返回的总是同一个数值:1.0 下面是使用GenericBooleanPrefDataModel的例子:

DataModel model = new GenericBooleanPrefDataModel(
    new FileDataModel(new File("ua.base")));
RecommenderIRStatsEvaluator evaluator =
  new GenericRecommenderIRStatsEvaluator();
RecommenderBuilder recommenderBuilder = new RecommenderBuilder() {
  @Override
  public Recommender buildRecommender(DataModel model) {
    UserSimilarity similarity = new LogLikelihoodSimilarity(model);
    UserNeighborhood neighborhood =
      new NearestNUserNeighborhood(10, similarity, model);
    return new GenericUserBasedRecommender(
        model, neighborhood, similarity);
  }
};
DataModelBuilder modelBuilder = new DataModelBuilder() {
  @Override
  public DataModel buildDataModel(
      FastByIDMap trainingData) {
    return new GenericBooleanPrefDataModel(
      GenericBooleanPrefDataModel.toDataMap(trainingData));
//GenericBooleanPrefDataModel使用FastIDSet,
//而不是PreferenceArray,所以,用toDataMap进行转换
  }
};
IRStatistics stats = evaluator.evaluate(
    recommenderBuilder, modelBuilder, model, null, 10,
    GenericRecommenderIRStatsEvaluator.CHOOSE_THRESHOLD,
    1.0);
System.out.println(stats.getPrecision());
System.out.println(stats.getRecall());

原文先卖关子给了个有bug的程序,这里就不卖关子了…… 但是那个bug是要注意的:PearsonCorrelationSimilarity是不能使用的,因为没有打分。类似的,EuclideanDistanceSimilarity也不能用。 这里计算相似度用的是LogLikelihoodSimilarity,以后再详细讨论这些相似度。 上面的代码还是不太好。GenericUserBasedRecommender虽然是可以用的,但是因为相似度计算的问题,在布尔偏好的时候不好使。应当换用 GenericBooleanPrefUserBasedRecommender FileDataModel还是可以用的,只要文件每行只有两个数值User ID和Item ID,他会正确处理。 布尔和非布尔偏好是不能混用的,要么删掉所有打分,要么把缺失的打分用某种平均值估计出来。

参考

http://diaorui.net/archives/305、321