由 小戴
在之前的利用皮尔逊相关度系数构建一个简单的推荐系统一文中,我们一起构建了一个简单的电影推荐系统。
在那篇文章中我们使用基于用户的协作型过滤(user-based collaborative filtering)技术,利用来自以往每一位用户对电影的评价来构造样本数据集,然后借助皮尔逊相关度系数对新用户作出推荐。
在文章的末尾我们提到,这种方法对于一个小型的应用是完全没有问题的,但当站点的规模逐渐扩大的时候,我们也许将会需要一些更为智慧的解决方案。
那么,在本文中让我们一起来讨论利用基于物品的协作型过滤(item-based collaborative filtering)技术来改进我们的推荐系统。
在这里我们还将沿用之前文章中使用的样本数据。基于物品的协作型过滤的总体思路是对每件物品预先计算出与其最为相近的其他物品(在我们的示例中是电影),当我们需要为某位用户提供推荐时,就可以查看他曾经评分过的物品,并从中选出该用户比较喜好的,最后构造出一个列表,其中包含了与这些选中物品最为相近的其他物品。
此种方法的优越性在于,虽然我们同样需要检查所有的物品数据,但是物品间的比较不会像用户间的比较那么频繁变化。这也就是说,我们通常不需要不停地计算与每件物品最相近的其他物品。我们可以把这项任务放在网络流量不是那么大的时候进行,或者是在独立于主应用之外的另一台机器上单独执行。
下面我们就开始着手改进之前构建的推荐系统。
首先,我们要定义一个函数 transformPrefs,这个函数的作用是对我们使用的样本数据数组进行倒置处理,从而得到一个有关物品及其用户评价的数组。在具体的实现过程中,我们仅需要将样本数据中的用户与物品对换即可,这样就可以复用以前所写的方法了。
01 | function transformPrefs( $prefs ) |
04 | foreach ( $prefs as $person => $items ) |
06 | foreach ( $items as $item => $rating ) |
09 | $result [ $item ][ $person ] = $rating ; |
现在,调用我们在之前文章中已经定义过的 topMatches 函数,可以得到一组与《阿凡达》最为相近的影片:
1 | print_r(topMatches(transformPrefs( $critics ), '阿凡达' )); |
可以得到如下结果:
3 | [在云端] => 0.65795169495977 |
4 | [盗梦空间] => 0.48795003647427 |
5 | [阿甘正传] => 0.11180339887499 |
6 | [人在囧途] => -0.17984719479905 |
7 | [线人] => -0.42289003161103 |
在本例中,可能存在着一些相关评价值为负的情况,例如上述结果中的《人在囧途》和《线人》,这表明那些喜欢《阿凡达》的用户,存在不喜欢《人在囧途》和《线人》的倾向(当然,这仅仅是个示例而已,我选用的样本数据也都是我臆造的:P)。
第二步我们构造一个包含相近物品的完整数据集,我们使用到下面的函数。
再次强调,这项工作无须在每次向用户提供推荐的时候都重复一次,在构建完一次数据集后我们可以在需要的时候重复使用之。
01 | function calculateSimilarItems( $pref ) |
07 | $itemPrefs = transformPrefs( $pref ); |
08 | foreach ( $itemPrefs as $item => $ratings ) |
11 | $scores = topMatches( $itemPrefs , $item ); |
12 | $result [ $item ] = $scores ; |
该函数首先利用了第一步定义过的 transformPrefs 函数,获得有关物品及其用户评价的数组。然后我们在一个循环中遍历每项物品,并将用户对各物品评价的数组传入 topMatches 函数中,求得最为相近的物品及其相似度评价值。最后,该函数返回一个包含物品及其最相近物品列表的数组。
我们可以通过运行以下代码测试一下我们的程序:
1 | print_r(calculateSimilarItems( $critics )); |
得到类似于这样的返回信息:
05 | [阿甘正传] => 0.76376261582598 |
06 | [阿凡达] => 0.48795003647427 |
07 | [在云端] => 0.33333333333333 |
08 | [人在囧途] => -0.61237243569579 |
09 | [线人] => -0.94491118252307 |
14 | [盗梦空间] => 0.76376261582598 |
15 | [阿凡达] => 0.11180339887499 |
16 | [线人] => -0.33333333333333 |
17 | [人在囧途] => -0.56635211395485 |
18 | [在云端] => -0.6454972243679 |
25 | [线人] => 0.55555555555556 |
26 | [阿凡达] => -0.17984719479905 |
28 | [阿甘正传] => -0.56635211395485 |
29 | [盗梦空间] => -0.61237243569579 |
虽然不需要在每次向用户提供推荐的时候都运行一次这个函数,但我们仍然需要及时执行该函数以确保物品的相似度不至于过期。通常我们需要在用户基数和评分数量还不是很大的时候频繁执行这一函数,而随着用户数量的不断增长,物品间的相似度评价值通常会越来越趋向稳定。
现在,我们已经可以在不遍历整个样本数据集的情况下,利用反映物品相似度的数组来给出推荐了。我们先取到用户评价过的所有物品,找出其相近物品,再根据相似度对其进行加权。我们可以很容易地根据物品数组来得到相似度。
影片 | 评分 | 人在囧途 | 评分x人在囧途 | 盗梦空间 | 评分x盗梦空间 | 线人 | 评分x线人 |
---|
阿甘正传 | 4.5 | -0.566 | -2.547 | 0.764 | 3.438 | -0.333 | -1.4985 |
阿凡达 | 4.0 | -0.18 | -0.72 | 0.488 | 1.952 | -0.423 | -1.692 |
在云端 | 1.0 | -0.25 | -0.25 | 0.333 | 0.333 | -0.486 | -0.486 |
总计 | | -0.996 | -3.517 | 1.585 | 5.723 | -1.242 | -3.6765 |
归一化结果 | | | 3.531 | | 3.610 | | 2.960 |
表格的每一行都列出的一部用户曾经评价过的影片,以及对该片的具体评分。对于每一部该用户尚未看过的影片,相应有一列指出其与已观看影片的相近程度。例如:影片《阿凡达》和《人在囧途》之间的相似度评价值为 -0.18。以“评分x”开头的列给出了用户对影片的评价值乘以相似度之后的结果,由于该用户对《阿凡达》的评分是 4.0,所以“人在囧途”的最后一列对应取值为:4.0×-0.18=-0.72。
总计一行给出了每部影片相似度评价值的总计值及其“评分x”列的总计值。为了预测用户对每部影片的评分情况,只要将“评分x”列的总计值除以相似度一列的总计值即可。用户对影片《人在囧途》的评分情况为:-3.517/-0.996=3.531。
通过下面的代码,可以实现上述功能:
01 | function getRecommendedItems( $prefs , $itemMatch , $user ) |
03 | $userRatings = $prefs [ $user ]; |
08 | foreach ( $userRatings as $item => $rating ) |
11 | foreach ( $itemMatch [ $item ] as $item2 => $similarity ) |
14 | if ( array_key_exists ( $item2 , $userRatings )) |
20 | $scores [ $item2 ] = isset( $scores [ $item2 ]) ? $scores [ $item2 ] : 0; |
21 | $scores [ $item2 ] += $similarity * $rating ; |
24 | $totalSim [ $item2 ] = isset( $totalSim [ $item2 ]) ? $totalSim [ $item2 ] : 0; |
25 | $totalSim [ $item2 ] += $similarity ; |
29 | foreach ( $scores as $item => $score ) |
31 | $rankings [ $item ] = ( $totalSim [ $item ] != 0) ? $score / $totalSim [ $item ] : 0; |
我们可以测试一下这个函数,向其传入以前构造好的相似度数据集,在这里我们为 rock 提供一个新的推荐结果:
1 | $itemsim = calculateSimilarItems( $critics ); |
2 | print_r(getRecommendedItems( $critics , $itemsim , 'rock' )); |
运行结果如下:
3 | [盗梦空间] => 3.6100310668022 |
4 | [人在囧途] => 3.531395034186 |
5 | [线人] => 2.9609998607243 |
与之前文章中得到的推荐相比,《盗梦空间》和《人在囧途》的排列位置有了些许变化,但他们仍然排在《线人》的前面。而且更为重要的是,在调用 getRecommendedItems 函数时,我们不必再为所有其他评论者计算相似度评价值,因为物品相似度数据集已经事先构造好了。
在针对大规模的样本数据集进行推荐时,基于物品进行过滤的方式明显要比基于用户的过滤更快,不过它却有维护物品相似度列表的额外开销。同时,这种方法根据数据集“稀疏”程度上的不同也存在精准度上的差异。对于稀疏数据集,基于物品的过滤方法通常要优于基于用户的过滤方法,而对于密集数据集而言,两者的效果几乎是一样的。