最近做实验,瞄了一眼MostPopularRecommender推荐的结果,与我想象中的结果不一样,我想象的是这个算法给每个人推荐一模一样的列表,但是这里显示的结果并不是这样,遂仔细研究了下,以下是我粗浅的分析。
一、算法思想
MostPopularRecommender,顾名思义,给每个用户推荐最流行的物品。主要做法是直接统计每个物品在训练集中出现的次数,然后按照物品出现次数的高低进行排序,把该排序推荐给每一个用户。
虽然这个方法不具有个性化,对每一个用户的推荐列表都是一样的,但毫无疑问,这个方法在推荐算法中是最为简单但又不失效果的一个算法!!!它经常被用于推荐系统初期的冷启动阶段,以及是各种互联网应用必不可少的一个推荐算法,比如微博的热搜、比如音乐的榜单等。
二、MostPopularRecommender 代码走读
以下是 MostPopularRecommender.java 代码,这个推荐算法继承的基类是 MatrixRecommender 类, 如果看过我写的这一篇文章的话:如何利用已有轮子 LibRec 库实现自己的推荐算法,大家就会知道,在 LibRec 中实现一个推荐算法只需要重写继承的基类中的三个方法即可实现,它们分别是setup方法、trainModel方法以及predict方法。
package net.librec.recommender.baseline;
import com.google.common.collect.Maps;
import net.librec.common.LibrecException;
import net.librec.recommender.MatrixRecommender;
import java.util.HashMap;
import java.util.Map;
/**
* Baseline: items are weighted by the number of ratings they received.
*/
public class MostPopularRecommender extends MatrixRecommender {
/**
* most popular items
*/
private Map<Integer, Integer> itemPops;
@Override
protected void setup() throws LibrecException {
super.setup();
itemPops = Maps.newConcurrentMap();
}
@Override
protected void trainModel() throws LibrecException {
}
/**
* The rated count as the predictive ranking score for user userIdx on item itemIdx.
*
* @param userIdx user index
* @param itemIdx item index
* @return predictive rating for user userIdx on item itemIdx
* @throws LibrecException if error occurs during predicting
*/
@Override
protected double predict(int userIdx, int itemIdx) throws LibrecException {
if (!itemPops.containsKey(itemIdx))
itemPops.put(itemIdx, trainMatrix.column(itemIdx).size());
return itemPops.get(itemIdx);
}
}
- 在这里的 setup 方法中定义了一个 itemPops 变量,用于保存每个物品在训练集中出现的次数。
- 由于推荐 MostPopular 的物品并不需要进行训练,所以这里的 trainModel 为空。
- 这里对 predict 方法进行了改写,本来 predict 方法完成的任务是预测用户 userIdx 对 itemIdx
的 rating,这里直接返回物品在训练集中出现的次数。
三、MostPolularTestCase 代码走读
这是 MostPolularRecommender的 测试代码 MostPolularTestCase.java:
package net.librec.recommender.baseline;
import net.librec.BaseTestCase;
import net.librec.common.LibrecException;
import net.librec.conf.Configuration.Resource;
import net.librec.job.RecommenderJob;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
/**
* Most Popular Test Case correspond to MostPopularRecommender
* {@link net.librec.recommender.baseline.MostPopularRecommender}
*
* @author liuxz
*/
public class MostPolularTestCase extends BaseTestCase {
@Override
@Before
public void setUp() throws Exception {
super.setUp();
}
/**
* Test the whole process of Most Popular Recommender
*
* @throws ClassNotFoundException
* @throws LibrecException
* @throws IOException
*/
@Test
public void testRecommender() throws ClassNotFoundException, LibrecException, IOException {
Resource resource = new Resource("rec/baseline/mostpopular-test.properties");
conf.addResource(resource);
RecommenderJob job = new RecommenderJob(conf);
job.runJob();
}
}
四、推荐的时候,是否需要剔除用户已经消费过的物品?
下面是 MostPop 的推荐结果,每一行代表用户ID,推荐物品ID,以及预测分数(这里是指训练集中出现次数),对于每个用户,返回前 N 个物品(这里是10个物品)。
其实大家从结果中也可以看到,这里并没有给每个用户都推荐一模一样的列表,这也是我一直疑惑的地方,后来发现原来是因为在该方法继承的基类 MatrixRecommender 中的 推荐物品RecommendedList () 方法中,对每一个用户进行推荐的时候,事先排除了已经在训练集中出现过的物品。
/**
* recommend
* * predict the ranking scores in the test data
*
* @return predictive rating matrix
* @throws LibrecException if error occurs during recommending
*/
public RecommendedList recommendRank(LibrecDataList<AbstractBaseDataEntry> dataList) throws LibrecException {
LOG.info("begin recommend");
int numDataEntries = dataList.size();
RecommendedList recommendedList = new RecommendedList(numUsers);
List<Integer> contextList = new ArrayList<>();
for (int contextIdx = 0; contextIdx < numDataEntries; ++contextIdx) {
contextList.add(contextIdx);
recommendedList.addList(new ArrayList<>());
}
contextList.parallelStream().forEach((Integer contextIdx) -> {
BaseRankingDataEntry baseRankingDataEntry = (BaseRankingDataEntry) dataList.getDataEntry(contextIdx);
int userIdx = baseRankingDataEntry.getUserId();
int[] items = trainMatrix.row(userIdx).getIndices();
List<KeyValue<Integer, Double>> itemValueList = new ArrayList<>(numItems);
for (int itemIdx = 0, trainItemIndex = 0; itemIdx < numItems; ++itemIdx) {
if (trainItemIndex < items.length && items[trainItemIndex] == itemIdx) {//排除训练集中已经出现过的物品
trainItemIndex++;
continue;
}
double predictRating = 0;
try {
predictRating = predict(userIdx, itemIdx);
} catch (LibrecException e) {
System.out.println(userIdx + ", " + itemIdx + ": "+ predictRating);
e.printStackTrace();
} catch (Exception e) {
System.out.println(userIdx + ", " + itemIdx + ": "+ predictRating);
e.printStackTrace();
}
if (Double.isNaN(predictRating)) {
continue;
}
itemValueList.add(new KeyValue<>(itemIdx, predictRating));
}
recommendedList.setList(contextIdx, itemValueList);//用户,[物品,评分]
recommendedList.topNRankByIndex(contextIdx, topN);//返回正序的N个
});
if (recommendedList.size() == 0) {
throw new IndexOutOfBoundsException("No item is recommended, " +
"there is something error in the recommendation algorithm! Please check it!");
}
LOG.info("end recommend");
return recommendedList;
}
所以尽管所有人对所有物品的预测评分(物品在训练集中的出现次数)都是一致的,推荐的内容还是不一样的。
但我总觉得怪怪的,这样的做法放在其他方法上,我当然同意,但是推荐最流行的物品,每个人的推荐物品列表就应该是一模一样的,不应该去除已经在训练集中出现过的物品。因为微博热搜难道会因为我看过某个热搜就不显示这个热搜给我了吗?音乐榜单难道会因为我经常听这首音乐,就不把这个音乐显示在给我推荐的热门排行榜上吗?
当然不是!!!
以上第四点是我自己的一个小疑惑,先放在这,以后看看其他人是怎么解释这个的!