写在前面:本文通过构建一个电影推荐系统,深入浅出的介绍推荐系统相关的概念、算法,让读者朋友能够在对推荐系统有比较全面的认识的基础之上,能够轻松地构建出自己的推荐系统。
1. 什么是推荐系统(Recommendation System)
推荐系统是指根据一个群体的偏好,来为群体中的成员提供推荐的系统。现实生活中这样的例子很多,比如豆瓣(Douban.com)读书中的“豆瓣猜”功能,它根据你看过的一些书和相关评价,与整个豆瓣社区其它会员看过的书与评价经过一系列的计算,就能给你推荐一些你没有读过的,但有可能感兴趣的书(如下图所示):
这是我读过的或者正在读的书:
这是豆瓣给我推荐的书:
通过上面的例子,相信大家对推荐系统都有了一个初步的认识。其实生活中还有许许多多的这样的例子,像在线购物中的商品推荐、在线视频播放网站的视频推荐等。
2. 推荐系统相关理论
(1)推荐系统通常可以分为两类:一类是基于人的推荐系统,它利用人与人之间的相似度来进行推荐;一类是基于物品的推荐,它利用物品之间的相似度来进行推荐。通俗地讲,基于人的推荐,是通过分析人与人喜欢的物品,计算出人与人之间的相似度,然后做推荐;而基于物品的推荐,是通过分析某人喜欢的物品与其它物品的相似度,然后来为其做推荐。
(2)推荐系统中比较关键的算法就是相似度的计算,有人与人之间的相似度计算,也有物品与物品之间的相似度计算。相似度计算函数要满足如下特点:拥有同样的函数签名,以一个浮点数做为返回值,其数值越大代表相似度越大。下面介绍几个算法:
a. 欧几里德距离(Euclidean Distance),我们知道两个人的喜好越相似,他们的欧几里德距离值越小,所以需要将欧几里德距离转化一下,这里介绍一个简单的转化:1/(1 + dist), 这样就能保证越相似取值越大,而且取值范围在(0, 1]
b. 皮求逊相关系数(Pearson Correlation Coefficient), 取值范围[-1, 1], 越相似值越大,满足条件
c. Tanimoto系统 (Tanimoto Coefficient),最值范围[0, 1], 越相似值越大,满足条件
(3)在推荐系统里,需要注意:
a. 没有对某物品进行评价的人不能对该物品的推荐打分产生影响
b. 不能因为某人的偏执喜好(打很高或者很低的分)对推荐打分产生明显的影响
为了避免上面的问题,通常采用加权平均的方法来计算某物品的推荐打分(详见第三部分算法实现)
3. 动手构建一个推荐系统
本部分通过构建一个真实的电影推荐系统,来介绍构建一个推荐的基本步骤与方法。
(1)数据集
本系统采用的数据是来自http://www.grouplens.org/node/73的数据,本次实验采用的是如下图所示的第三份数据,总共有6040个用户和3952部电影,以及1000209条相关评价。
(2)核心:推荐函数
typedef double (*ScoreFunc)(const double *, const double *, size_t); void GetRecommendation( const double ** allCritics, //[in] 所有人的打分表 size_t personNum, //[in] 所有人的个数 size_t size, //[in] 打分表大小 size_t myIndex, //[in] 我在打分表中下标 ScoreFunc scorer, //[in] 打分函数 size_t recNum, //[out] 被推荐项个数 int * recItems, //[out] 被推荐项列表 double * recScores //[out] 被推荐项得分 ) { double * allRels = new double[personNum]; ::memset(allRels, 0, sizeof(double)*personNum); ///计算所有的相关度 for (size_t idx = 0; idx < personNum; ++ idx) { if (idx == myIndex) { continue; //it's me, just continue } allRels[idx] = scorer(allCritics[myIndex], allCritics[idx], size); } double * rels = new double[personNum]; double * critics = new double[personNum]; std::multimap mapScores; for (size_t itemIdx = 0; itemIdx < size; ++ itemIdx) { ::memset(rels, 0, sizeof(double)*personNum); ::memset(critics, 0, sizeof(double)*personNum); ///获取有效的相关度和对应的评分 for (size_t personIdx = 0; personIdx < personNum; ++ personIdx) { if (allCritics[personIdx][itemIdx] <= 0)//invalid score { rels[personIdx] = 0; critics[personIdx] = 0; } else { rels[personIdx] = allRels[personIdx]; critics[personIdx] = allCritics[personIdx][itemIdx]; } } ///计算加权打分 double score = GetWeightedMead(critics, critics + personNum, rels, rels + personNum); mapScores.insert(std::make_pair(score, itemIdx)); } ///获取最终的推荐列表和对应的打分 std::multimap::reverse_iterator oIt = mapScores.rbegin(); for (size_t count = 0; count < recNum; ++ count) { recItems[count] = oIt->second; recScores[count] = oIt->first; ++ oIt; } delete [] critics; delete [] rels; delete [] allRels; }
double GetEuclideanScore(double dist) { return (1 / (1 + dist)); } double GetPearsonScore(double coef) { return coef; } double GetTanimotoScore(double coef) { return coef; } double GetEuclideanScore(const double * myCritics, const double * hisCritics, size_t size) { double dist = GetEuclideanDistance(myCritics, myCritics + size, hisCritics, hisCritics + size); return GetEuclideanScore(dist); } double GetPearsonScore(const double * myCritics, const double * hisCritics, size_t size) { double coef = GetPearsonCorrelationCoefficient(myCritics, myCritics + size, hisCritics, hisCritics + size); return GetPearsonScore(coef); } double GetTanimotoScore(const double * myCritics, const double * hisCritics, size_t size) { double coef = GetTanimotoCoefficient(myCritics, myCritics + size, hisCritics, hisCritics + size); return GetTanimotoScore(coef); }
(3)实验结果
我们用三种不同的打分函数,为第1000个用户,推荐20部电影,来对比一下推荐的结果:
a. Euclidean结果:
b. Pearson结果:
c. Tanimoto结果:
从上面的结果中,可以得出如下结果:
a. 采用Euclidean打分推荐和采用Pearson打分推荐的结果中,有16个是相同的
b. 采用Pearson打分推荐和采用Tanimoto打分推荐的结果中,有15个是相同的
c. 采用Tanimoto打分推荐和采用Euclidean打分推荐的结果中,有16个是相同的
从上面的数据可以看出,虽说采用不同的打分函数进行推荐的结果存在一定的差异,但是整体上是一致的,不同的结果的相互覆盖率都超过了75%, 这说明我们的打分函数都还是比较有效的。