by 小戴
伴随着Web2.0概念的普及,我们正在广泛地享受推荐系统给我们带来的便利。
现代的电子商务、SNS社区等应用大量地使用了推荐系统。通过推荐系统,人人网帮我们找到多年未见的老友,亚马逊总能知道我们偏好什么样的商品,而豆瓣网更是将算法和产品完美结合的最佳典范之一。
通过大量的用户数据和充满智慧的推荐系统,豆瓣网已经成为了我寻找读物、电影和志趣相投的朋友的不二场所。
最近我阅读了《集体智慧编程》一书,开始接触到一点推荐系统的算法。于是在这里我便把一些我学到的东西分享给大家。
在下面的算法里,我们利用一组如下所示的输入数据,它们是用PHP代码描述的。这组数据保存了7位用户以及他们对若干部电影的评价。所有的评价值都介于1到5之间,这有点像我们常常见到的1至5颗星的评价方式。这种数据组织的方式是很贴近于我们现实的应用的。
01 | $critics = array ( |
02 | 'arale' => array ( |
03 | '盗梦空间' => 2.5, |
04 | '阿甘正传' => 3.5, |
05 | '线人' => 3.0, |
06 | '阿凡达' => 3.5, |
07 | '在云端' => 2.5, |
08 | '人在囧途' => 3.0), |
09 | 'johnson' => array ( |
10 | '盗梦空间' => 3.0, |
11 | '阿甘正传' => 3.5, |
12 | '线人' => 1.5, |
13 | '阿凡达' => 5.0, |
14 | '人在囧途' => 3.0, |
15 | '在云端' => 3.5), |
16 | 'white' => array ( |
17 | '盗梦空间' => 2.5, |
18 | '阿甘正传' => 3.0, |
19 | '阿凡达' => 3.5, |
20 | '人在囧途' => 4.0), |
21 | 'richard' => array ( |
22 | '阿甘正传' => 3.5, |
23 | '线人' => 3.0, |
24 | '人在囧途' => 4.5, |
25 | '阿凡达' => 4.0, |
26 | '在云端' => 2.5), |
27 | 'hillary' => array ( |
28 | '盗梦空间' => 3.0, |
29 | '阿甘正传' => 4.0, |
30 | '线人' => 2.0, |
31 | '阿凡达' => 3.0, |
32 | '人在囧途' => 3.0, |
33 | '在云端' => 2.0), |
34 | 'colin' => array ( |
35 | '盗梦空间' => 3.0, |
36 | '阿甘正传' => 4.0, |
37 | '人在囧途' => 3.0, |
38 | '阿凡达' => 5.0, |
39 | '在云端' => 3.5), |
40 | 'rock' => array ( |
41 | '阿甘正传' => 4.5, |
42 | '在云端' => 1.0, |
43 | '阿凡达' => 4.0)); |
在推荐系统算法的实现中,我们要用到皮尔逊相关度系数这一方法来利用已有的数据计算两位用户或者是两件产品之间的相关程度。皮尔逊相关度系数是一个介于1到-1之间的值,其中,1表示两个变量完全正相关,0表示无关而-1则表示完全负相关。下面我们给出皮尔逊相关度系数的计算公式。有关皮尔逊相关度系数更详细的数学原理,大家可以Google到很多资料。
用PHP代码可以很容易地描述出皮尔逊系数的实现。
01 | function getPearson( $prefs , $person1 , $person2 ) |
02 | { |
03 | if (! is_array ( $prefs )) { |
04 | return false; |
05 | } |
06 |
07 | // 得到双方都评价过的物品列表 |
08 | $similar = array (); |
09 | foreach ( $prefs [ $person1 ] as $item => $value ) |
10 | { |
11 | if ( array_key_exists ( $item , $prefs [ $person2 ])) |
12 | { |
13 | $similar [ $item ] = 1; |
14 | } |
15 | } |
16 |
17 | // 计算列表元素的个数 |
18 | $nums = count ( $similar ); |
19 | if ( $nums <= 0) { |
20 | return 0; |
21 | } |
22 |
23 | // 对所有的偏好求和 |
24 | $sum1 = getSum( $similar , $prefs [ $person1 ]); |
25 | $sum2 = getSum( $similar , $prefs [ $person2 ]); |
26 |
27 | // 求平方和 |
28 | $sum1Sq = getSum( $similar , $prefs [ $person1 ], 2); |
29 | $sum2Sq = getSum( $similar , $prefs [ $person2 ], 2); |
30 |
31 | // 求乘积之和 |
32 | $pSum = getSum( $similar , $prefs [ $person1 ], 1, $prefs [ $person2 ]); |
33 |
34 | // 计算皮尔逊评价值 |
35 | $numerator = $pSum - ( $sum1 * $sum2 / $nums ); // 分子 |
36 | $denominator = sqrt(( $sum1Sq - pow( $sum1 , 2) / $nums ) * ( $sum2Sq - pow( $sum2 , 2) / $nums )); // 分母 |
37 | if ( $denominator == 0) { |
38 | return 0; |
39 | } |
40 | return $numerator / $denominator ; |
41 | } |
其中用到的getSum()函数描述如下。
01 | function getSum( $similar , $person , $power = 1, $person2 = null) |
02 | { |
03 | if (! is_array ( $similar ) || ! is_array ( $person ) || $power < 0) |
04 | { |
05 | return false; |
06 | } |
07 | $sum = 0; |
08 | foreach ( $similar as $item => $value ) |
09 | { |
10 | if ( is_array ( $person2 )) |
11 | { |
12 | $sum += $person [ $item ] * $person2 [ $item ]; |
13 | } |
14 | else |
15 | { |
16 | $sum += pow( $person [ $item ], $power ); |
17 | } |
18 | } |
19 | return $sum ; |
20 | } |
如此以来,我们便可以使用皮尔逊相关度系数,计算出与我趣味相投的朋友。在这里我们再构建一个topMatches()函数,我们向它传入之前那组数据,然后指定一位用户。topMatches()函数能为我们逐个计算其余用户与该用户之间的相关度,并保存到一个数组中返回。
01 | function topMatches( $prefs , $person ) |
02 | { |
03 | $scores = array (); |
04 | foreach ( $prefs as $other => $subArry ) |
05 | { |
06 | if ( $other != $person ) |
07 | { |
08 | $scores [ $other ] = getPearson( $prefs , $person , $other ); |
09 | } |
10 | } |
11 | arsort( $scores ); |
12 | return $scores ; |
13 | } |
如果我们写下如下这样一段代码的话,我们要找到跟rock最相近的用户:
1 | print_r(topMatches( $critics , 'rock' )); |
就可以得到如下的输出:
1 | Array |
2 | ( |
3 | [arale] => 0.99124070716193 |
4 | [hillary] => 0.9244734516419 |
5 | [richard] => 0.89340514744156 |
6 | [colin] => 0.66284898035987 |
7 | [johnson] => 0.38124642583151 |
8 | [white] => -1 |
9 | ) |
数组每个项的值就是该用户和rock之间的相似度。
这就有点类似我们在SNS社区或者是微博系统中常见的“你可能感兴趣的人”这一功能。
当然,如果是电子商务网站的话,可能更希望向用户推荐用户感兴趣的商品而不是拥有同样喜好的其他用户。
那么我们可以实现下面这样一个函数。
01 | function getRecommendations( $prefs , $person ) |
02 | { |
03 | $totals = array (); |
04 | $simSums = array (); |
05 |
06 | foreach ( $prefs as $other => $subArry ) |
07 | { |
08 | // 不要和自己做比较 |
09 | if ( $other == $person ) |
10 | { |
11 | continue ; |
12 | } |
13 | $sim = getPearson( $prefs , $person , $other ); |
14 |
15 | // 忽略评价值为零或小于零的情况 |
16 | if ( $sim < 0) |
17 | { |
18 | continue ; |
19 | } |
20 | foreach ( $prefs [ $other ] as $item => $rating ) |
21 | { |
22 | // 只对自己还未曾看过的影片进行评价 |
23 | if (! array_key_exists ( $item , $prefs [ $person ]) || $prefs [ $person ][ $item ] == 0) |
24 | { |
25 | // 相似度 * 评价值; |
26 | $totals [ $item ] = isset( $totals [ $item ]) ? $totals [ $item ] : 0; |
27 | $totals [ $item ] += $rating * $sim ; |
28 |
29 | // 相似度之和 |
30 | $simSums [ $item ] = isset( $simSums [ $item ]) ? $simSums [ $item ] : 0; |
31 | $simSums [ $item ] += $sim ; |
32 | } |
33 | } |
34 | } |
35 |
36 | // 建立一个归一化的列表 |
37 | $rankings = array (); |
38 | foreach ( $totals as $item => $total ) |
39 | { |
40 | $rankings [ $item ] = $total / $simSums [ $item ]; |
41 | } |
42 |
43 | arsort( $rankings ); |
44 | return $rankings ; |
45 | } |
同样,我们写下这么一条语句,我们要向rock推荐电影:
1 | print_r(getRecommendations( $critics , 'rock' )); |
我们可以得到下面这样的输出:
1 | Array |
2 | ( |
3 | [人在囧途] => 3.3477895267131 |
4 | [盗梦空间] => 2.8325499182642 |
5 | [线人] => 2.5309807037656 |
6 | ) |
大家肯定已经猜到了那一串串数字就是程序计算得到的rock对该电影可能的喜好程度。
这样,一个简单的推荐系统就构建完成了。
但是,富有经验的程序员马上会提出质疑,咱们这个程序效率太低下了!
这种方法对于一个小型的应用完全是没有问题的,但如果是类似于豆瓣这种动辄几千万用户的应用来说,将一个用户和其他所有用户进行比较,然后再对每位用户评分过的商品进行比较,你会发现我们的系统已经不给力了。
在下一篇文章中,我们来讨论另外一种解决方案,在拥有大量数据的情况下,那种方法可以表现得更好。