mahout推荐14-实用化

这是一个约会网站,首先需要下载
http://www.occamslab.com/petricek/data/libimseti-complete.zip :

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

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

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

1.1基于用户的推荐程序

141664  Euclidean 1.17 1.12 1.23 1.25 1.25 1.33 1.33 1.48
Pearson 1.30 1.19 1.27 1.30 1.26 1.35 1.38 1.47
Log-likelihood 1.33 1.38 1.33 1.35 1.33 1.29 1.33 1.49
Tanimoto 1.32 1.33 1.43 1.32 1.30 1.39 1.37 1.41
0.950.850.75  Euclidean 1.33 1.37 1.39 1.43 1.41 1.47
Pearson 1.47 1.4 1.42 1.4 1.38 1.37
Log-likelihood 1.37 1.46 1.56 1.52 1.51 1.43
Tanimoto Nan Nan Nan Nan Nan Nan

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

1.2基于物品的推荐程序

   Euclidean 2.36
Pearson 2.32
Log-likelihood 2.38
Tanimoto 2.40

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

1.3关于precision和recall

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

2.引入特定域的信息

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

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

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

  • ItemSimilarity similarity = new PearsonCorrelationSimilarity(model);

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

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

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

  • public final class PearsonCorrelationSimilarity extends AbstractSimilarity
  • abstract class AbstractSimilarity extends AbstractItemSimilarity implements UserSimilarity
  • public interface UserSimilarity extends Refreshable

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

  • import java.util.Collection;
  • import org.apache.mahout.cf.taste.common.Refreshable;
  • import org.apache.mahout.cf.taste.common.TasteException;
  • import org.apache.mahout.cf.taste.impl.common.FastIDSet;
  • import org.apache.mahout.cf.taste.similarity.ItemSimilarity;
  • public class GenderItemSimilarity implements ItemSimilarity {
  • private final FastIDSet men;
  • private final FastIDSet women;
  • //给men和women集合赋初值
  • public GenderItemSimilarity(FastIDSet men, FastIDSet women) {
  • this.men = men;
  • this.women = women;
  • }
  • //判断两个ID是否同一性别
  • public double itemSimilarity(long profileID1, long profileID2) throws TasteException {
  • Boolean profile1IsMan = isMan(profileID1);
  • if (profile1IsMan == null) {
  • return 0.0;
  • }
  • Boolean profile2IsMan = isMan(profileID2);
  • if (profile2IsMan == null) {
  • return 0.0;
  • }
  • return profile1IsMan == profile2IsMan ? 1.0 : -1.0;
  • }
  • //判断是否是男性
  • private Boolean isMan(long profileID) {
  • if (men.contains(profileID)) {
  • return Boolean.TRUE;
  • }
  • if (women.contains(profileID)) {
  • return Boolean.FALSE;
  • }
  • return null;
  • }
  • //计算相似度的,调用方法itemSimilarity(long profileID1, long profileID2)
  • public double[] itemSimilarities(long itemID1, long[] itemID2s) throws TasteException{
  • double[] result = new double[itemID2s.length];
  • for (int i = 0; i < itemID2s.length; i++) {
  • result[i] = itemSimilarity(itemID1, itemID2s[i]);
  • }
  • return result;
  • }
  • public long[] allSimilarItemIDs(long l) throws TasteException {
  • throw new UnsupportedOperationException("Not supported yet.");
  • }
  • public void refresh(Collection<Refreshable> clctn) {
  • throw new UnsupportedOperationException("Not supported yet.");
  • }
  • }

2.2利用IDRescorer修改推荐结果

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

  • double rescore(long id, double originalScore);
  • boolean isFiltered(long id);

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

  • import java.io.File;
  • import java.io.IOException;
  • import org.apache.mahout.cf.taste.common.TasteException;
  • import org.apache.mahout.cf.taste.impl.common.FastIDSet;
  • import org.apache.mahout.cf.taste.model.DataModel;
  • import org.apache.mahout.cf.taste.model.PreferenceArray;
  • import org.apache.mahout.cf.taste.recommender.IDRescorer;
  • import org.apache.mahout.common.iterator.FileLineIterable;
  • public class GenderRescorer implements IDRescorer {
  • private final FastIDSet men;//存放当前数据模型对应的所有male selectableUser
  • private final FastIDSet women;//存放当前数据模型对应的所有female selectableUser
  • private final FastIDSet usersRateMoreMen;//
  • private final FastIDSet usersRateLessMen;
  • private final boolean likeMen;//表明针对一个用户(userID定义)一个profileID是否应该过滤
  • public GenderRescorer(
  • FastIDSet men,
  • FastIDSet women,
  • long userID, DataModel model)
  • throws TasteException {
  • this.men = men;
  • this.women = women;
  • this.usersRateMoreMen = new FastIDSet();
  • this.usersRateLessMen = new FastIDSet();
  • this.likeMen = ratesMoreMen(userID, model);
  • }
  • //产生数据对应的men和women集合
  • public static FastIDSet[] generateMenWomen(File genderFile)
  • throws IOException {
  • FastIDSet men = new FastIDSet(50000);
  • FastIDSet women = new FastIDSet(50000);
  • for (String line : new FileLineIterable(genderFile)) {
  • int comma = line.indexOf(',');
  • char gender = line.charAt(comma + 1);
  • if (gender == 'U') {
  • continue;
  • }
  • long profileID = Long.parseLong(line.substring(0, comma));
  • if (gender == 'M') {
  • men.add(profileID);
  • } else {
  • women.add(profileID);
  • }
  • }
  • men.rehash();
  • women.rehash();
  • return new FastIDSet[]{men, women};
  • }
  • //判断userID对应的用户是不是更喜欢男性,从他/她评过分的那些用户的性别来统计
  • private boolean ratesMoreMen(long userID, DataModel model)
  • throws TasteException {
  • if (usersRateMoreMen.contains(userID)) {
  • return true;
  • }
  • if (usersRateLessMen.contains(userID)) {
  • return false;
  • }
  • PreferenceArray prefs = model.getPreferencesFromUser(userID);
  • int menCount = 0;
  • int womenCount = 0;
  • for (int i = 0; i < prefs.length(); i++) {
  • long profileID = prefs.get(i).getItemID();
  • if (men.contains(profileID)) {
  • menCount++;
  • } else if (women.contains(profileID)) {
  • womenCount++;
  • }
  • }
  • boolean ratesMoreMen = menCount > womenCount;
  • if (ratesMoreMen) {
  • usersRateMoreMen.add(userID);
  • } else {
  • usersRateLessMen.add(userID);
  • }
  • return ratesMoreMen;
  • }
  • //对于需要过滤的推荐,设置其值为NaN,这是因为他们不是不能推荐的,而是最差的推荐
  • public double rescore(long profileID, double originalScore) {
  • if(originalScore<100)
  • System.out.println(profileID+" "+originalScore);
  • return isFiltered(profileID) ? Double.NaN : originalScore;
  • }
  • //如果一个用户是喜欢男性的,而推荐的又是女性,则这个推荐是应该过滤掉的,反之亦然
  • public boolean isFiltered(long profileID) {
  • return likeMen ? women.contains(profileID) : men.contains(profileID);
  • }
  • }

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

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

  • public List<RecommendedItem> recommend(long userID, int topN)
  • import java.io.File;
  • import java.io.IOException;
  • import java.util.Collection;
  • import java.util.List;
  • import org.apache.mahout.cf.taste.common.Refreshable;
  • import org.apache.mahout.cf.taste.common.TasteException;
  • import org.apache.mahout.cf.taste.impl.common.FastIDSet;
  • import org.apache.mahout.cf.taste.impl.model.file.FileDataModel;
  • import org.apache.mahout.cf.taste.impl.neighborhood.NearestNUserNeighborhood;
  • import org.apache.mahout.cf.taste.impl.recommender.GenericUserBasedRecommender;
  • import org.apache.mahout.cf.taste.impl.similarity.EuclideanDistanceSimilarity;
  • import org.apache.mahout.cf.taste.model.DataModel;
  • import org.apache.mahout.cf.taste.neighborhood.UserNeighborhood;
  • import org.apache.mahout.cf.taste.recommender.IDRescorer;
  • import org.apache.mahout.cf.taste.recommender.RecommendedItem;
  • import org.apache.mahout.cf.taste.recommender.Recommender;
  • import org.apache.mahout.cf.taste.similarity.UserSimilarity;
  • public class LibimsetiRecommender implements Recommender {
  • private final Recommender libimsetiRecommender;
  • private final DataModel model;
  • private final FastIDSet men;
  • private final FastIDSet women;
  • //构造函数:一般而言,一个普适的自定义推荐器的输入应该是:DataModel和额外的知识
  • //应该将独立于数据的东西构建好:基本的pure CF推荐器
  • public LibimsetiRecommender() throws TasteException, IOException {
  • this((DataModel) new FileDataModel(new File("/Users/ericxk/Downloads/recommenderdata/libimseti/ratings.dat")));
  • }
  • //应该将独立于数据的东西构建好:基本的pure CF推荐器,即将libimsetiRecommender设为pure CF
  • public LibimsetiRecommender(DataModel model) throws TasteException, IOException {
  • UserSimilarity similarity = new EuclideanDistanceSimilarity(model);
  • UserNeighborhood neighborhood =
  • new NearestNUserNeighborhood(2, similarity, model);
  • libimsetiRecommender = new GenericUserBasedRecommender(model, neighborhood, similarity);
  • this.model = model;
  • FastIDSet[] menWomen = GenderRescorer.generateMenWomen(
  • new File(("/Users/ericxk/Downloads/recommenderdata/libimseti/gender.dat")));
  • men = menWomen[0];
  • women = menWomen[1];
  • }
  • //用libimsetiRecommender进行推荐时就加入了由gender信息定义的GenderRescorer
  • public List<RecommendedItem> recommend(long userID, int topN) throws TasteException {
  • IDRescorer rescorer = new GenderRescorer(men, women, userID, model);
  • return libimsetiRecommender.recommend(userID, topN, rescorer);
  • }
  • //用libimsetiRecommender也提供了自定义IDRescorer进行推荐的方法
  • public List<RecommendedItem> recommend(long userID, int topN, IDRescorer idr) throws TasteException {
  • return libimsetiRecommender.recommend(userID, topN, idr);
  • }
  • //这里要注意,由于libimsetiRecommender真正进行preference的估计是要受到GenderRescorer的rescore的影响的
  • public float estimatePreference(long userID, long itemID) throws TasteException {
  • IDRescorer rescorer = new GenderRescorer(men, women, userID, model);
  • return (float) rescorer.rescore(
  • itemID, libimsetiRecommender.estimatePreference(userID, itemID));
  • }
  • //这个可以直接借助于libimsetiRecommender的setPreference
  • public void setPreference(long userID, long itemID, float value) throws TasteException {
  • libimsetiRecommender.setPreference(userID, itemID, value);
  • }
  • //这个可以直接借助于libimsetiRecommender的removePreference
  • public void removePreference(long userID, long itemID) throws TasteException {
  • libimsetiRecommender.removePreference(userID, itemID);
  • }
  • //这个可以直接借助于libimsetiRecommender的getDataModel
  • public DataModel getDataModel() {
  • return libimsetiRecommender.getDataModel();
  • }
  • //这个可以直接借助于libimsetiRecommender的refresh
  • public void refresh(Collection<Refreshable> alreadyRefreshed) {
  • libimsetiRecommender.refresh(alreadyRefreshed);
  • }
  • }

3.为匿名用户做推荐

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

  • import java.io.File;
  • import java.io.IOException;
  • import java.util.List;
  • import org.apache.mahout.cf.taste.common.TasteException;
  • import org.apache.mahout.cf.taste.impl.model.GenericUserPreferenceArray;
  • import org.apache.mahout.cf.taste.impl.model.PlusAnonymousUserDataModel;
  • import org.apache.mahout.cf.taste.impl.model.file.FileDataModel;
  • import org.apache.mahout.cf.taste.model.DataModel;
  • import org.apache.mahout.cf.taste.model.PreferenceArray;
  • import org.apache.mahout.cf.taste.recommender.RecommendedItem;
  • public class LibimsetiWithAnonymousRecommender extends LibimsetiRecommender {
  • private final PlusAnonymousUserDataModel plusAnonymousModel;
  • public LibimsetiWithAnonymousRecommender()
  • throws TasteException, IOException {
  • this((DataModel) new FileDataModel(new File("data/dating/ratings.dat")));
  • }
  • public LibimsetiWithAnonymousRecommender(DataModel model)
  • throws TasteException, IOException {
  • //调用父类LibimsetiRecommender的构造函数
  • super(new PlusAnonymousUserDataModel(model));
  • //得到PlusAnonymousUserDataModel对象
  • plusAnonymousModel =
  • (PlusAnonymousUserDataModel) getDataModel();
  • }
  • //设计这个推荐器的recommend方法:输入:匿名用户的评分信息 输出:对此匿名用户的推荐
  • public synchronized List<RecommendedItem> recommend(
  • PreferenceArray anonymousUserPrefs, int topN)
  • throws TasteException {
  • //利用PlusAnonymousUserDataModel对象的setTempPrefs方法为将匿名用户加入到数据中,
  • //并且利用PlusAnonymousUserDataModel.TEMP_USER_ID作为其userID
  • plusAnonymousModel.setTempPrefs(anonymousUserPrefs);
  • //调用父类LibimsetiRecommender的recommend方法
  • //userID现在被PlusAnonymousUserDataModel.TEMP_USER_ID所代替了
  • List<RecommendedItem> recommendations =
  • recommend(PlusAnonymousUserDataModel.TEMP_USER_ID, topN, null);
  • //删除PlusAnonymousUserDataModel.TEMP_USER_ID与匿名用户的关联
  • plusAnonymousModel.clearTempPrefs();
  • return recommendations;
  • }
  • //创建当前匿名用户的伪数据
  • public PreferenceArray creatAnAnonymousPrefs() {
  • PreferenceArray anonymousPrefs =
  • new GenericUserPreferenceArray(3);
  • anonymousPrefs.setUserID(0, PlusAnonymousUserDataModel.TEMP_USER_ID);
  • anonymousPrefs.setItemID(0, 123L);
  • anonymousPrefs.setValue(0, 1.0f);
  • anonymousPrefs.setItemID(1, 123L);
  • anonymousPrefs.setValue(1, 3.0f);
  • anonymousPrefs.setItemID(2, 123L);
  • anonymousPrefs.setValue(2, 2.0f);
  • return anonymousPrefs;
  • }
  • public static void main(String[] args) throws Exception {
  • LibimsetiWithAnonymousRecommender recommender =
  • new LibimsetiWithAnonymousRecommender();
  • List<RecommendedItem> recommendations =
  • recommender.recommend(recommender.creatAnAnonymousPrefs(), 10);
  • System.out.println(recommendations);
  • }
  • }

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会马上注意到这个更新。
    数据文件重新加载很占资源,这个时候可以用更新文件,而不是对整个数据文件进行替换。

 

 

 

参考:http://blog.csdn.net/feitongxunke/article/details/38338455

他的这篇相当于将第5章的内容全部弄下来了。

到了这一章就是真刀实枪的开始了。

posted @ 2014-08-05 14:26  jseven  阅读(432)  评论(0编辑  收藏  举报