推荐引擎之协同过滤算法——深度剖析及源码实现

来源:互联网 发布:java jdk1.6 32位下载 编辑:程序博客网 时间:2024/05/16 10:58

前言

在当今这个信息爆炸的大数据互联网时代,信息的爆炸使得人们按照传统方式寻找需要的信息会显得乏力而不准确。除了应用先进的搜索技术使得用户主动搜索外,个性化的推荐技术也能准确地将信息定位到用户,并取得非常好的效果。本文首先介绍传统的推荐引擎技术,接下来重点讲解当下流行的,个性化协同过滤推荐算法并附上源码实现。


传统推荐技术

推荐引擎,是建立在对每一个用户的信息和行为深刻了解的基础之上,为用户提供个人化信息的技术,它不是被动等待用户的搜索请求,而是为用户主动推送最相关的信息。

推荐引擎工作原理

下图展示了推荐系统(或称推荐引擎)的工作原理。它的输入是推荐的数据源,一般情况下,推荐引擎所需要的数据源包括:

  • 要推荐物品或内容的元数据,例如关键字,基本描述等;
  • 系统用户的基本信息,例如性别,年龄等;
  • 用户对物品或者信息的偏好,例如用户对物品的评分,用户查看物品的记录,用户的购买记录等。最近,我在阿里参与的一个项目就是需要综合用户的这些特征来进行推荐。

这里写图片描述

推荐引擎根据不同的推荐机制可能用到数据源中的一部分或全部,然后在这些数据的基础上结合推荐算法将不同信息推荐给不同用户。

推荐机制分类

在推荐系统中,常见的推荐机制有如下几种:

  • 基于人口统计学的推荐;
  • 基于内容的推荐;
  • 基于协同过滤的推荐;
  • 混合的推荐机制。

由于篇幅限制,在这里,博主只是列举出来并没有逐一展开。如果有机会,在后面会作为一个专栏给大家讲解。


协同过滤推荐算法

协同过滤推荐(Collaborative Filtering recommendation,后文简称CF)在信息过滤和信息系统中正迅速成为一项很受欢迎的技术。与传统的基于内容过滤直接分析内容进行推荐不同,协同过滤分析用户兴趣,在用户群中找到指定用户的相似(兴趣)用户,综合这些相似用户对某一信息的评价,形成系统对该指定用户对此信息的喜好程度预测。

从上面的定义可以看出,协同过滤是一种集体智慧的体现,也就是基于兴趣群体的推荐技术。例如,你现在想观看一部新电影,但并不知道看哪一部。这种时候,大部分人都会选择询问有相同电影偏好的朋友,让他们推荐一两部电影。这便是协同过滤的集体智慧体现。

从上面咨询朋友进行电影推荐的例子中,我们可以粗略总结出协同过滤推荐的步骤:

  1. 根据历史资料总结出用户偏好或者资料特征;
  2. 计算需要推荐的对象相似度;
  3. 进行推荐。

不难发现,整个算法有两个难点

  1. 如何计算对象间的相似度;
  2. 相似度计算完之后,如何找到对象的邻居。

相似度计算

一般而言,在进行数据处理的时候,我们会将对象特征归一化为向量表示,然后计算相似度,距离越近相似度越高。相似度的计算有以下几种方式,
1. 欧几里得距离(Euclidean Distance)
这里写图片描述
采用欧几里得距离计算时,一般使用如下公式进行相似度转换,
这里写图片描述
可以看出,欧氏距离越小时,相似度越近。

2. 皮尔逊相关系数(Pearson Correlation Coefficient)
这里写图片描述
Pearson相关系数一般用于计算两个定距变量间联系的紧密程度,它的取值在 [-1,+1] 之间。它有一个特点,在计算两个数列的相似度时忽略平均值的差异。比如说有的用户对商品评分普遍偏低,有的用户评分普遍偏高,而实际上他们具有相同的爱好,他们的Pearson相关系数会比较高。举例说明,用户A对某三个商品的评分是X=(1,2,3),用户B对这三个商品的评分是Y=(4,5,6),则X和Y的Pearson相关系数是0.865,相关性仍然比较高。

3. Cosine 相似度(Cosine Similarity)
这里写图片描述
Cosine 相似度被广泛应用于文档数据的相似度计算。

计算相似邻居

上一小节,我们讨论了如何计算两个对象的相似度。这一节我们将继续讨论协同过滤算法的第二个难点,如何找到相似的邻居。

常用的计算相似邻居的算法有两种:固定数量的邻居和基于相似度门槛的邻居。
1. 固定数量的邻居:K-neighborhoods 或者 Fix-size neighborhoods
不论邻居的“远近”,只取最近的 K 个,作为其邻居。如图 1 中的 A,假设要计算点 1 的 5- 邻居,那么根据点之间的距离,我们取最近的 5 个点,分别是点 2,点 3,点 4,点 7 和点 5。但很明显我们可以看出,这种方法对于孤立点的计算效果不好,因为要取固定个数的邻居,当它附近没有足够多比较相似的点,就被迫取一些不太相似的点作为邻居,这样就影响了邻居相似的程度,比如图 1 中,点 1 和点 5 其实并不是很相似。

2. 基于相似度门槛的邻居:Threshold-based neighborhoods
与计算固定数量的邻居的原则不同,基于相似度门槛的邻居计算是对邻居的远近进行最大值的限制,落在以当前点为中心,距离为 K 的区域中的所有点都作为当前点的邻居,这种方法计算得到的邻居个数不确定,但相似度不会出现较大的误差。如图 1 中的 B,从点 1 出发,计算相似度在 K 内的邻居,得到点 2,点 3,点 4 和点 7,这种方法计算出的邻居的相似度程度比前一种优,尤其是对孤立点的处理。

图 1. 相似邻居计算示意图
这里写图片描述

协同过滤分类

协同过滤刚开始使用的时候是基于用户(User-based)的推荐,后来亚马逊根据自己的业务改进,发明了基于物品(Item-based)的推荐算法。这两种算法在下文逐一介绍。

基于用户的协同过滤算法
基于用户的 CF 根据用户对物品的偏好找到邻居用户,然后将邻居用户喜欢的物品推荐给当前用户。计算上,就是将一个用户对所有物品的偏好作为一个向量来计算用户之间的相似度,找到 K 邻居后,根据邻居的相似度权重以及他们对物品的偏好,预测当前用户没有偏好的未涉及物品,计算得到一个排序的物品列表作为推荐。下图给出了一个例子,对于用户 A,根据用户的历史偏好,这里只计算得到一个邻居用户 C,然后将用户 C 喜欢的物品 D 推荐给用户 A。
这里写图片描述

基于物品的协同过滤算法
基于物品的 CF 的原理和基于用户的 CF 类似,只是在计算邻居时采用物品本身,而不是从用户的角度,即基于用户对物品的偏好找到相似的物品,然后根据用户的历史偏好,推荐相似的物品给用户。从计算的角度看,就是将所有用户对某个物品的偏好作为一个向量来计算物品之间的相似度,得到物品的相似物品后,根据用户历史的偏好预测当前用户还没有表示偏好的物品,计算得到一个排序的物品列表作为推荐。下图给出了一个例子,对于物品 A,根据所有用户的历史偏好,喜欢物品 A 的用户都喜欢物品 C,得出物品 A 和物品 C 比较相似,而用户 C 喜欢物品 A,那么可以推断出用户 C 可能也喜欢物品 C。
这里写图片描述

两种协同过滤算法的比较

社交类网站如,微博,facebook。这类网站有一个特点,用户数目到达一定程度之后会保持稳定,但物品(这里指网站上的各类新闻资讯,应用等)的数目却一直保持快速增长,否则肯定会造成用户流失。对于这类网站,User CF比较适用,因为Item CF的计算量由于物品的不断增加而十分大。反而User CF计算量会小很多。

而对于非社交类网站如电子商务购物平台,用户的数量往往大大超过物品的数量,同时物品的数据相对稳定,因此计算物品的相似度不但计算量较小,同时也不必频繁更新。

此外这两种算法也存在不同的使用场景,举例说明。当给用户推荐一个新款包包的时候,根据User CF给用户的提示是:与你有相同爱好的“XXX”用户也喜欢这个包包,这时用户的反应是“XXX”是谁,跟我有什么关系。不同的是,根据Item CF给用户的提示会是:根据您以前的购物记录我们猜你喜欢这个包包,这时用户也许就会点击进去浏览甚至购买。

当然除了上述两种应用方式,Item CF对于推荐结果的多样性会比User CF差不少。因此在应用协同过滤算法的时候,开发人员需要结合实际应用场景选择不同算法,甚至选择混合协同过滤,这样会取得更好的推荐效果,所带来的收益也会增加。


源码实现(Java版)

这里给出了基于用户的协同过滤算法简单实现,在相似度计算上采用皮尔逊相关系数。

/** * @name:CF algorithm * @author:希慕_North * @function:User-based Collaborative Filtering algorithm. * @date:09/04/2015 */package RS;import java.util.ArrayList;import java.util.Collections;import java.util.Comparator;import java.util.HashMap;import java.util.List;import java.util.Map;import java.util.Map.Entry;public class CF {    public static void main(String[] args) {        Map<String, Map<String, Integer>> userPerfMap = new HashMap<String, Map<String, Integer>>();        Map<String, Integer> pref1 = new HashMap<String, Integer>();        pref1.put("A", 3);        pref1.put("B", 4);        pref1.put("C", 3);        pref1.put("D", 5);        pref1.put("E", 1);        pref1.put("F", 4);        userPerfMap.put("p1", pref1);        Map<String, Integer> pref2 = new HashMap<String, Integer>();        pref2.put("A", 2);        pref2.put("B", 4);        pref2.put("C", 4);        pref2.put("D", 5);        pref2.put("E", 3);        pref2.put("F", 2);        userPerfMap.put("p2", pref2);        Map<String, Integer> pref3 = new HashMap<String, Integer>();        pref3.put("A", 3);        pref3.put("B", 5);        pref3.put("C", 4);        pref3.put("D", 5);        pref3.put("E", 2);        pref3.put("F", 1);        userPerfMap.put("p3", pref3);        Map<String, Integer> pref4 = new HashMap<String, Integer>();        pref4.put("A", 2);        pref4.put("B", 2);        pref4.put("C", 3);        pref4.put("D", 4);        pref4.put("E", 3);        pref4.put("F", 2);        userPerfMap.put("p4", pref4);        Map<String, Integer> pref5 = new HashMap<String, Integer>();        pref5.put("A", 4);        pref5.put("B", 4);        pref5.put("C", 4);        pref5.put("D", 5);        pref5.put("E", 1);        pref5.put("F", 0);        userPerfMap.put("p5", pref5);        Map<String, Double> simUserSimMap = new HashMap<String, Double>();        System.out.println("皮尔逊相关系数:");        for (Entry<String, Map<String, Integer>> userPerfEn : userPerfMap.entrySet()) {            String userName = userPerfEn.getKey();            if (!"p5".equals(userName)) {                double sim = getUserSimilar(pref5, userPerfEn.getValue());                System.out.println("    p5与" + userName + "之间的相关系数:" + sim);                simUserSimMap.put(userName, sim);            }        }        Map<String, Map<String, Integer>> simUserObjMap = new HashMap<String, Map<String, Integer>>();        Map<String, Integer> pobjMap1 = new HashMap<String, Integer>();        pobjMap1.put("玩命速递", 3);        pobjMap1.put("环太平洋", 4);        pobjMap1.put("变形金刚", 3);        simUserObjMap.put("p1", pobjMap1);        Map<String, Integer> pobjMap2 = new HashMap<String, Integer>();        pobjMap2.put("玩命速递", 5);        pobjMap2.put("环太平洋", 1);        pobjMap2.put("变形金刚", 2);        simUserObjMap.put("p2", pobjMap2);        Map<String, Integer> pobjMap3 = new HashMap<String, Integer>();        pobjMap3.put("玩命速递", 2);        pobjMap3.put("环太平洋", 5);        pobjMap3.put("变形金刚", 5);        simUserObjMap.put("p3", pobjMap3);        System.out.println("推荐结果:" + getRecommend(simUserObjMap, simUserSimMap));    }    //Claculate Pearson Correlation Coefficient    public static double getUserSimilar(Map<String, Integer> pm1, Map<String, Integer> pm2) {        int n = 0;// 数量n        int sxy = 0;// Σxy=x1*y1+x2*y2+....xn*yn        int sx = 0;// Σx=x1+x2+....xn        int sy = 0;// Σy=y1+y2+...yn        int sx2 = 0;// Σx2=(x1)2+(x2)2+....(xn)2        int sy2 = 0;// Σy2=(y1)2+(y2)2+....(yn)2        for (Entry<String, Integer> pme : pm1.entrySet()) {            String key = pme.getKey();            Integer x = pme.getValue();            Integer y = pm2.get(key);            if (x != null && y != null) {                n++;                sxy += x * y;                sx += x;                sy += y;                sx2 += Math.pow(x, 2);                sy2 += Math.pow(y, 2);            }        }        // p=(Σxy-Σxy/n)/Math.sqrt((Σx2-(Σx)2/n)(Σy2-(Σy)2/n));        double sd = sxy - sx * sy / n;        double sm = Math.sqrt((sx2 - Math.pow(sx, 2) / n) * (sy2 - Math.pow(sy, 2) / n));        return Math.abs(sm == 0 ? 1 : sd / sm);    }    //get recommendation results    public static String getRecommend(Map<String, Map<String, Integer>> simUserObjMap,            Map<String, Double> simUserSimMap) {        Map<String, Double> objScoreMap = new HashMap<String, Double>();        for (Entry<String, Map<String, Integer>> simUserEn : simUserObjMap.entrySet()) {            String user = simUserEn.getKey();            double sim = simUserSimMap.get(user);            for (Entry<String, Integer> simObjEn : simUserEn.getValue().entrySet()) {                double objScore = sim * simObjEn.getValue();                String objName = simObjEn.getKey();                if (objScoreMap.get(objName) == null) {                    objScoreMap.put(objName, objScore);                } else {                    double totalScore = objScoreMap.get(objName);                    objScoreMap.put(objName, totalScore + objScore);                }            }        }        List<Entry<String, Double>> enList = new ArrayList<Entry<String, Double>>(objScoreMap.entrySet());        Collections.sort(enList, new Comparator<Map.Entry<String, Double>>() {            public int compare(Map.Entry<String, Double> o1, Map.Entry<String, Double> o2) {                Double a = o1.getValue() - o2.getValue();                if (a == 0) {                    return 0;                } else if (a > 0) {                    return 1;                } else {                    return -1;                }            }        });        return enList.get(enList.size() - 1).getKey();    }}

参考资料及推荐阅读

  1. 推荐系统实践,项亮著;
  2. http://www.cs.umd.edu/~samir/498/Amazon-Recommendations.pdf
  3. http://files.grouplens.org/papers/www10_sarwar.pdf
  4. http://www.ibm.com/developerworks/cn/web/1103_zhaoct_recommstudy2/index.html;
  5. http://www.ibm.com/developerworks/cn/web/1103_zhaoct_recommstudy2/index.html

(by老杨,新浪微博:@老杨0511)

0 0
原创粉丝点击