Mahout in Action 读书笔记chapter5 让推荐程序实用化

来源:互联网 发布:守望先锋安娜数据 编辑:程序博客网 时间:2024/05/21 03:56

到了这一章就是真刀实枪的开始了。这是一个约会网站,首先需要下载
http://www.occamslab.com/petricek/data/libimseti-complete.zip :

  • 这个里面包含了用户对其他人档案的评分,针对评分那个文件,事先经历了数据的预处理:提出了生成评分个数不到20个的用户,还排除了几乎对每个档案都给出相同分值的用户,因为这有可能是垃圾信息和不严肃的评分。
  • 还有个文件是表示用户的性别,其中U是代表为知。书中说了句:把女性推荐给一个仅对男性感兴趣的用户,这必然是一个糟糕的推荐,而且会冒犯用户,反之一样。

​1.找到一个有效的推荐程序

根据前一章的内容,首先我们要找到合适的推荐程序,这里尝试了基于用户的推荐,基于物品的推荐,对几种相似度度量的标准都一一进行了评测,根据评测出来的结果来选择合适的相似度度量方式。

1.1基于用户的推荐程序

n=1248163264128Euclidean1.171.121.231.251.251.331.331.48Pearson1.301.191.271.301.261.351.381.47Log-likelihood1.331.381.331.351.331.291.331.49Tanimoto1.321.331.431.321.301.391.371.41n=0.950.90.850.80.750.7Euclidean1.331.371.391.431.411.47Pearson1.471.41.421.41.381.37Log-likelihood1.371.461.561.521.511.43TanimotoNanNanNanNanNanNan

上图是分别是基于n个最近邻和基于阈值的评测结果。

1.2基于物品的推荐程序

 ScoreEuclidean2.36Pearson2.32Log-likelihood2.38Tanimoto2.40

上图是基于物品推荐的结果。

1.3关于precision和recall

上面的谷本系数和对数似然比是无法进行评价的,因为这个无法得到评价值,只能进行precision和recall的计算。而且这里还有个很重要的问题,我们现在是采用的用户对物品打分的机制,但是用户不一定只对打分高的感兴趣。这种约会网站,更重要的不是仅仅推荐打分高的,因此这里我们可以采用布尔型来做推荐,书中接下来也是采用了布尔型,发现准确率和召回率高了很多。

2.引入特定域的信息

接下来会引入一个性别这个信息,基于性别可以定制一个ItemSimilarity这个度量,目的是避免推荐性别不当的用户。

2.1采用一个定制的物品相似性度量

下面代码是一个基于性别的物品相似度度量,书中说这个ItemSimilarity可以和标准的GenericItemBasedRecommender一起使用,进行评估。关于这点我并没有找到一起使用的方法,这里可以大致说下:
常见的是这个语句:

  1. ItemSimilarity similarity = new PearsonCorrelationSimilarity(model);

现在我们的GenderItemSimilarity继承ItemSimilarity接口,如果要使用GenderItemSimilarity,需要将
PearsonCorrelationSimilarity替换掉,GenderItemSimilarity里面最主要的方法是:

  1. public double[] itemSimilarities(long itemID1, long[] itemID2s) throws TasteException

我去查看了下PearsonCorrelationSimilarity父类,关系如下:

  1. public final class PearsonCorrelationSimilarity extends AbstractSimilarity
  2. abstract class AbstractSimilarity extends AbstractItemSimilarity implements UserSimilarity
  3. public interface UserSimilarity extends Refreshable

其中最主要的是AbstractSimilarity,其中Gender里面的很多方法,其中AbstractSimilarity都有所记载,但是我直接用GenderItemSimilarity继承AbstractSimilarity,也出了很多问题。继承AbstractSimilarity的原因很简单就是为了实现GenderItemSimilarity(model),这里出现这么多问题,以后再看吧。
下面是GenderItemSimilarity的代码:

  1. import java.util.Collection;
  2. import org.apache.mahout.cf.taste.common.Refreshable;
  3. import org.apache.mahout.cf.taste.common.TasteException;
  4. import org.apache.mahout.cf.taste.impl.common.FastIDSet;
  5. import org.apache.mahout.cf.taste.similarity.ItemSimilarity;
  6. public class GenderItemSimilarity implements ItemSimilarity {
  7. private final FastIDSet men;
  8. private final FastIDSet women;
  9. //给men和women集合赋初值
  10. public GenderItemSimilarity(FastIDSet men, FastIDSet women) {
  11. this.men = men;
  12. this.women = women;
  13. }
  14. //判断两个ID是否同一性别
  15. public double itemSimilarity(long profileID1, long profileID2) throws TasteException {
  16. Boolean profile1IsMan = isMan(profileID1);
  17. if (profile1IsMan == null) {
  18. return 0.0;
  19. }
  20. Boolean profile2IsMan = isMan(profileID2);
  21. if (profile2IsMan == null) {
  22. return 0.0;
  23. }
  24. return profile1IsMan == profile2IsMan ? 1.0 : -1.0;
  25. }
  26. //判断是否是男性
  27. private Boolean isMan(long profileID) {
  28. if (men.contains(profileID)) {
  29. return Boolean.TRUE;
  30. }
  31. if (women.contains(profileID)) {
  32. return Boolean.FALSE;
  33. }
  34. return null;
  35. }
  36. //计算相似度的,调用方法itemSimilarity(long profileID1, long profileID2)
  37. public double[] itemSimilarities(long itemID1, long[] itemID2s) throws TasteException{
  38. double[] result = new double[itemID2s.length];
  39. for (int i = 0; i < itemID2s.length; i++) {
  40. result[i] = itemSimilarity(itemID1, itemID2s[i]);
  41. }
  42. return result;
  43. }
  44. public long[] allSimilarItemIDs(long l) throws TasteException {
  45. throw new UnsupportedOperationException("Not supported yet.");
  46. }
  47. public void refresh(Collection<Refreshable> clctn) {
  48. throw new UnsupportedOperationException("Not supported yet.");
  49. }
  50. }

2.2利用IDRescorer修改推荐结果

在Recommender.recommend()方法中有一个类型为IDRescorer的用final修饰的可选参数,在这里可以调用recommend(long userID,int howMany,IDRescorer rescorer),IDRescorer这里是一个接口,里面有两个方法,

  1. double rescore(long id, double originalScore);
  2. boolean isFiltered(long id);

一个是用来重新打分,一个是用来过滤的。下面给出书中代码:

  1. import java.io.File;
  2. import java.io.IOException;
  3. import org.apache.mahout.cf.taste.common.TasteException;
  4. import org.apache.mahout.cf.taste.impl.common.FastIDSet;
  5. import org.apache.mahout.cf.taste.model.DataModel;
  6. import org.apache.mahout.cf.taste.model.PreferenceArray;
  7. import org.apache.mahout.cf.taste.recommender.IDRescorer;
  8. import org.apache.mahout.common.iterator.FileLineIterable;
  9. public class GenderRescorer implements IDRescorer {
  10. private final FastIDSet men;//存放当前数据模型对应的所有male selectableUser
  11. private final FastIDSet women;//存放当前数据模型对应的所有female selectableUser
  12. private final FastIDSet usersRateMoreMen;//
  13. private final FastIDSet usersRateLessMen;
  14. private final boolean likeMen;//表明针对一个用户(userID定义)一个profileID是否应该过滤
  15. public GenderRescorer(
  16. FastIDSet men,
  17. FastIDSet women,
  18. long userID, DataModel model)
  19. throws TasteException {
  20. this.men = men;
  21. this.women = women;
  22. this.usersRateMoreMen = new FastIDSet();
  23. this.usersRateLessMen = new FastIDSet();
  24. this.likeMen = ratesMoreMen(userID, model);
  25. }
  26. //产生数据对应的men和women集合
  27. public static FastIDSet[] generateMenWomen(File genderFile)
  28. throws IOException {
  29. FastIDSet men = new FastIDSet(50000);
  30. FastIDSet women = new FastIDSet(50000);
  31. for (String line : new FileLineIterable(genderFile)) {
  32. int comma = line.indexOf(',');
  33. char gender = line.charAt(comma + 1);
  34. if (gender == 'U') {
  35. continue;
  36. }
  37. long profileID = Long.parseLong(line.substring(0, comma));
  38. if (gender == 'M') {
  39. men.add(profileID);
  40. } else {
  41. women.add(profileID);
  42. }
  43. }
  44. men.rehash();
  45. women.rehash();
  46. return new FastIDSet[]{men, women};
  47. }
  48. //判断userID对应的用户是不是更喜欢男性,从他/她评过分的那些用户的性别来统计
  49. private boolean ratesMoreMen(long userID, DataModel model)
  50. throws TasteException {
  51. if (usersRateMoreMen.contains(userID)) {
  52. return true;
  53. }
  54. if (usersRateLessMen.contains(userID)) {
  55. return false;
  56. }
  57. PreferenceArray prefs = model.getPreferencesFromUser(userID);
  58. int menCount = 0;
  59. int womenCount = 0;
  60. for (int i = 0; i < prefs.length(); i++) {
  61. long profileID = prefs.get(i).getItemID();
  62. if (men.contains(profileID)) {
  63. menCount++;
  64. } else if (women.contains(profileID)) {
  65. womenCount++;
  66. }
  67. }
  68. boolean ratesMoreMen = menCount > womenCount;
  69. if (ratesMoreMen) {
  70. usersRateMoreMen.add(userID);
  71. } else {
  72. usersRateLessMen.add(userID);
  73. }
  74. return ratesMoreMen;
  75. }
  76. //对于需要过滤的推荐,设置其值为NaN,这是因为他们不是不能推荐的,而是最差的推荐
  77. public double rescore(long profileID, double originalScore) {
  78. if(originalScore<100)
  79. System.out.println(profileID+" "+originalScore);
  80. return isFiltered(profileID) ? Double.NaN : originalScore;
  81. }
  82. //如果一个用户是喜欢男性的,而推荐的又是女性,则这个推荐是应该过滤掉的,反之亦然
  83. public boolean isFiltered(long profileID) {
  84. return likeMen ? women.contains(profileID) : men.contains(profileID);
  85. }
  86. }

2.3封装一个定制的推荐系统

下面是封装前面IDRescorer的推荐系统,当然也可以载入自己定义的IDRescorer,代码还是很简单,调用很方便。到时候直接调用即可

  1. public List<RecommendedItem> recommend(long userID, int topN)
  1. import java.io.File;
  2. import java.io.IOException;
  3. import java.util.Collection;
  4. import java.util.List;
  5. import org.apache.mahout.cf.taste.common.Refreshable;
  6. import org.apache.mahout.cf.taste.common.TasteException;
  7. import org.apache.mahout.cf.taste.impl.common.FastIDSet;
  8. import org.apache.mahout.cf.taste.impl.model.file.FileDataModel;
  9. import org.apache.mahout.cf.taste.impl.neighborhood.NearestNUserNeighborhood;
  10. import org.apache.mahout.cf.taste.impl.recommender.GenericUserBasedRecommender;
  11. import org.apache.mahout.cf.taste.impl.similarity.EuclideanDistanceSimilarity;
  12. import org.apache.mahout.cf.taste.model.DataModel;
  13. import org.apache.mahout.cf.taste.neighborhood.UserNeighborhood;
  14. import org.apache.mahout.cf.taste.recommender.IDRescorer;
  15. import org.apache.mahout.cf.taste.recommender.RecommendedItem;
  16. import org.apache.mahout.cf.taste.recommender.Recommender;
  17. import org.apache.mahout.cf.taste.similarity.UserSimilarity;
  18. public class LibimsetiRecommender implements Recommender {
  19. private final Recommender libimsetiRecommender;
  20. private final DataModel model;
  21. private final FastIDSet men;
  22. private final FastIDSet women;
  23. //构造函数:一般而言,一个普适的自定义推荐器的输入应该是:DataModel和额外的知识
  24. //应该将独立于数据的东西构建好:基本的pure CF推荐器
  25. public LibimsetiRecommender() throws TasteException, IOException {
  26. this((DataModel) new FileDataModel(new File("/Users/ericxk/Downloads/recommenderdata/libimseti/ratings.dat")));
  27. }
  28. //应该将独立于数据的东西构建好:基本的pure CF推荐器,即将libimsetiRecommender设为pure CF
  29. public LibimsetiRecommender(DataModel model) throws TasteException, IOException {
  30. UserSimilarity similarity = new EuclideanDistanceSimilarity(model);
  31. UserNeighborhood neighborhood =
  32. new NearestNUserNeighborhood(2, similarity, model);
  33. libimsetiRecommender = new GenericUserBasedRecommender(model, neighborhood, similarity);
  34. this.model = model;
  35. FastIDSet[] menWomen = GenderRescorer.generateMenWomen(
  36. new File(("/Users/ericxk/Downloads/recommenderdata/libimseti/gender.dat")));
  37. men = menWomen[0];
  38. women = menWomen[1];
  39. }
  40. //用libimsetiRecommender进行推荐时就加入了由gender信息定义的GenderRescorer
  41. public List<RecommendedItem> recommend(long userID, int topN) throws TasteException {
  42. IDRescorer rescorer = new GenderRescorer(men, women, userID, model);
  43. return libimsetiRecommender.recommend(userID, topN, rescorer);
  44. }
  45. //用libimsetiRecommender也提供了自定义IDRescorer进行推荐的方法
  46. public List<RecommendedItem> recommend(long userID, int topN, IDRescorer idr) throws TasteException {
  47. return libimsetiRecommender.recommend(userID, topN, idr);
  48. }
  49. //这里要注意,由于libimsetiRecommender真正进行preference的估计是要受到GenderRescorer的rescore的影响的
  50. public float estimatePreference(long userID, long itemID) throws TasteException {
  51. IDRescorer rescorer = new GenderRescorer(men, women, userID, model);
  52. return (float) rescorer.rescore(
  53. itemID, libimsetiRecommender.estimatePreference(userID, itemID));
  54. }
  55. //这个可以直接借助于libimsetiRecommender的setPreference
  56. public void setPreference(long userID, long itemID, float value) throws TasteException {
  57. libimsetiRecommender.setPreference(userID, itemID, value);
  58. }
  59. //这个可以直接借助于libimsetiRecommender的removePreference
  60. public void removePreference(long userID, long itemID) throws TasteException {
  61. libimsetiRecommender.removePreference(userID, itemID);
  62. }
  63. //这个可以直接借助于libimsetiRecommender的getDataModel
  64. public DataModel getDataModel() {
  65. return libimsetiRecommender.getDataModel();
  66. }
  67. //这个可以直接借助于libimsetiRecommender的refresh
  68. public void refresh(Collection<Refreshable> alreadyRefreshed) {
  69. libimsetiRecommender.refresh(alreadyRefreshed);
  70. }
  71. }

3.为匿名用户做推荐

因为在正常使用的情况,会有许多新用户没有历史记录,这个时候有一种方法是生成临时用户,并将所有的匿名用户当做一个用户。有一个类的名字叫PlusAnonymousUserDataModel,这个类是在DataModel上的一个封装。下面是代码:

  1. import java.io.File;
  2. import java.io.IOException;
  3. import java.util.List;
  4. import org.apache.mahout.cf.taste.common.TasteException;
  5. import org.apache.mahout.cf.taste.impl.model.GenericUserPreferenceArray;
  6. import org.apache.mahout.cf.taste.impl.model.PlusAnonymousUserDataModel;
  7. import org.apache.mahout.cf.taste.impl.model.file.FileDataModel;
  8. import org.apache.mahout.cf.taste.model.DataModel;
  9. import org.apache.mahout.cf.taste.model.PreferenceArray;
  10. import org.apache.mahout.cf.taste.recommender.RecommendedItem;
  11. public class LibimsetiWithAnonymousRecommender extends LibimsetiRecommender {
  12. private final PlusAnonymousUserDataModel plusAnonymousModel;
  13. public LibimsetiWithAnonymousRecommender()
  14. throws TasteException, IOException {
  15. this((DataModel) new FileDataModel(new File("data/dating/ratings.dat")));
  16. }
  17. public LibimsetiWithAnonymousRecommender(DataModel model)
  18. throws TasteException, IOException {
  19. //调用父类LibimsetiRecommender的构造函数
  20. super(new PlusAnonymousUserDataModel(model));
  21. //得到PlusAnonymousUserDataModel对象
  22. plusAnonymousModel =
  23. (PlusAnonymousUserDataModel) getDataModel();
  24. }
  25. //设计这个推荐器的recommend方法:输入:匿名用户的评分信息 输出:对此匿名用户的推荐
  26. public synchronized List<RecommendedItem> recommend(
  27. PreferenceArray anonymousUserPrefs, int topN)
  28. throws TasteException {
  29. //利用PlusAnonymousUserDataModel对象的setTempPrefs方法为将匿名用户加入到数据中,
  30. //并且利用PlusAnonymousUserDataModel.TEMP_USER_ID作为其userID
  31. plusAnonymousModel.setTempPrefs(anonymousUserPrefs);
  32. //调用父类LibimsetiRecommender的recommend方法
  33. //userID现在被PlusAnonymousUserDataModel.TEMP_USER_ID所代替了
  34. List<RecommendedItem> recommendations =
  35. recommend(PlusAnonymousUserDataModel.TEMP_USER_ID, topN, null);
  36. //删除PlusAnonymousUserDataModel.TEMP_USER_ID与匿名用户的关联
  37. plusAnonymousModel.clearTempPrefs();
  38. return recommendations;
  39. }
  40. //创建当前匿名用户的伪数据
  41. public PreferenceArray creatAnAnonymousPrefs() {
  42. PreferenceArray anonymousPrefs =
  43. new GenericUserPreferenceArray(3);
  44. anonymousPrefs.setUserID(0, PlusAnonymousUserDataModel.TEMP_USER_ID);
  45. anonymousPrefs.setItemID(0, 123L);
  46. anonymousPrefs.setValue(0, 1.0f);
  47. anonymousPrefs.setItemID(1, 123L);
  48. anonymousPrefs.setValue(1, 3.0f);
  49. anonymousPrefs.setItemID(2, 123L);
  50. anonymousPrefs.setValue(2, 2.0f);
  51. return anonymousPrefs;
  52. }
  53. public static void main(String[] args) throws Exception {
  54. LibimsetiWithAnonymousRecommender recommender =
  55. new LibimsetiWithAnonymousRecommender();
  56. List<RecommendedItem> recommendations =
  57. recommender.recommend(recommender.creatAnAnonymousPrefs(), 10);
  58. System.out.println(recommendations);
  59. }
  60. }

4.创建一个支持Web访问的推荐程序

(这个时候可以下载官方的源代码:https://github.com/tdunning/MiA )
利用Mahout很容易将推荐程序捆绑成可部署的WAR文件。这一组件能很好地部署在Java servlet容器中,比如Tomcat,Resin。
首先需要封装WAR文件,在部署之前,需要把编译后的代码和数据文件打包为一个JAR文件。将数据集复制到/src/main/resources目录下,再用下面的命令制作出JAR文件:mvn package
然后进入Mahout发布包中的taste-web/模块目录,并从书中实例把target/mia-0.1.jar复制到lib子目录中。再编辑recommender.properties将推荐程序命名为索要采用的名称。如果你是用的是与实例相同的Java包名,正确的值应为mia.recommender.ch05.LibimsetiRecommender。现在再次执行mvn package,这个时候可以生成一个webapp-0.5.war的文件。这个文件可以立刻部署在Tomcat这种容器中。

5.更新和监控推荐程序

处于对性能的考虑,许多组件会生成缓存信息和中间的计算结果,这个时候有以下的处理方法:

  • 调用Recommender.refresh()强制情况所有缓存
  • 调用refresh方法来实现
    基于文件偏好数据是通过FileDataModel来访问的,这个时候部署更新信息时,我们可以对这个文件进行更新或者覆盖,而FileDataModel会马上注意到这个更新。
    数据文件重新加载很占资源,这个时候可以用更新文件,而不是对整个数据文件进行替换。
0 0