协同过滤(英语:Collaborative Filtering,简称CF),简单来说是利用某兴趣相投、拥有共同经验之群体的喜好来推荐用户感兴趣的信息,个人透过合作的机制给予信息相当程度的回应(如评分)并记录下来以达到过滤的目的进而帮助别人筛选信息,回应不一定局限于特别感兴趣的,特别不感兴趣信息的纪录也相当重要。——维基百科
亚马逊、Netflix、Hulu、YouTube的推荐算法的基础都是基于物品的协同过滤算法。
基于用户的协同过滤算法有一些缺点。
- 随着网站的用户数目越来越大,计算用户兴趣相似度矩阵将越来越困难,其运算时间复杂度和空间复杂度的增长和用户数的增长近似于平方关系。
- 基于用户的协同过滤算法很难对推荐结果作出解释。
I t e m C F ItemCF ItemCF 算法给用户推荐那些和他们之前喜欢的物品相似的物品。
举个例子:
用户/物品 | 物品A | 物品B | 物品C |
---|---|---|---|
用户A | √ | √ | |
用户B | √ | √ | √ |
用户C | √ | 与物品A相似,推荐 |
I t e m C F ItemCF ItemCF 算法并不利用物品的内容属性计算物品之间的相似度,它主要通过分析用户的行为记录计算物品之间的相似度。该算法认为,物品 A A A 和物品 B B B 具有很大的相似度是因为喜欢物品 A A A 的用户大都也喜欢物品 B B B。
基于物品的协同过滤算法可以利用用户的历史行为给推荐结果提供推荐解释,比如给用户推荐《机器学习实战》的解释可以是因为用户之前买过《统计学习方法》。
定义物品的相似度为 w i j = ∣ N ( i ) ∩ N ( j ) ∣ ∣ N ( i ) ∣ w_{ij} = \frac{|N(i)\cap{N(j)|}}{{|N(i)|}} wij=∣N(i)∣∣N(i)∩N(j)∣此处分母 ∣ N ( i ) ∣ {{|N(i)|}} ∣N(i)∣ 是喜欢物品 i i i 的用户数,而分子 ∣ N ( i ) ∩ N ( j ) ∣ {|N(i)\cap{N(j)|}} ∣N(i)∩N(j)∣ 是同时喜欢物品 i i i 和物品 j j j 的用户数。因此,上述公式可以理解为喜欢物品 i i i 的用户中有多少比例的用户也喜欢物品 j j j。
但该公式存在一个问题,如果物品 j j j 很热门,很多人都喜欢,那么 w i j w_{ij} wij 就会很大,接近1。因此,该公式会造成任何物品都会和热门的物品有很大的相似度,这显然不是一个好的特性。为了避免推荐出热门的物品, 可以用下面的公式: w u v = ∣ N ( i ) ∩ N ( j ) ∣ ∣ N ( i ) ∣ ∣ N ( j ) ∣ w_{uv} = \frac{|N(i)\cap{N(j)|}}{\sqrt{|N(i)||{N(j)|}}} wuv=∣N(i)∣∣N(j)∣∣N(i)∩N(j)∣这个公式惩罚了物品 j j j 的权重,因此减轻了热门物品会和很多物品相似的可能性。
从上面的定义可以看到,在协同过滤中两个物品产生相似度是因为它们共同地被很多用户喜欢,也就是说每个用户都可以通过他们的历史兴趣列表给物品“贡献”相似度。
这里蕴涵着一个假设,就是每个用户的兴趣都局限在某几个方面,因此如果两个物品属于一个用户的兴趣列表, 那么这两个物品可能就属于有限的几个领域,而如果两个物品属于很多用户的兴趣列表,那么它们就可能属于同一个领域,因而有很大的相似度。
与 U s e r C F UserCF UserCF 算法类似,用 I t e m C F ItemCF ItemCF 算法计算物品相似度时也可以首先建立 用 户 − > 物 品 用户->物品 用户−>物品 倒排表(即对每个用户建立一个包含他喜欢的物品的列表),然后对于每个用户,将他物品列表中的物品两两在共现矩阵 C C C 中加 1 1 1。
代码如下:
def ItemSimilarity(train):
# 修改相似度矩阵
C = dict()
N = dict()
for u, items in train.items():
for i in users:
N[i] += 1
for j in users:
if i == j:
continue
C[i][j] += 1
# 计算相似度矩阵
W = dict()
for i, related_items in C.items():
for j, cij in related_items.items():
W[u][v] = cij / math.sqrt(N[i] * N[j])
return W
最左边是输入的用户行为记录,每一行代表一个用户感兴趣的物品集合。然后,对于每个物品集合,我们将里面的物品两两加一,得到一个矩阵。最终将这些矩阵相加得到最右边的 C C C 矩阵。其中 C [ i ] [ j ] C[i][j] C[i][j] 记录了同时喜欢物品 i i i 和物品 j j j 的用户数。最后,将 C C C 矩阵归一化可以得到物品之间的余弦相似度矩阵 W W W。
在得到物品之间的相似度后, I t e m C F ItemCF ItemCF 算法通过如下公式计算用户 u u u 对一个物品 j j j 的兴趣: p u j = ∑ i ∈ N ( u ) ∩ S ( j , K ) w j i r u i p_{uj}=\displaystyle \sum_{i\in{N(u)\cap{S(j,K)}}}w_{ji}r_{ui} puj=i∈N(u)∩S(j,K)∑wjirui
这里 N ( u ) N(u) N(u) 是用户喜欢的物品的集合, S ( j , K ) S(j,K) S(j,K) 是和物品 j j j 最相似的 K K K 个物品的集合, w j i w_{ji} wji 是物品 j j j 和 i i i 的相似度, r u i r_{ui} rui 是用户 u u u 对物品 i i i 的兴趣。(对于隐反馈数据集,如果用户 u u u 对物品 i i i 有过行为,即可令 r u i = 1 r_{ui}=1 rui=1)
该公式的含义是,和用户历史上感兴趣的物品越相似的物品,越有可能在用户的推荐列表中获得比较高的排名。
该公式的实现代码如下:
def Recommendation(train, user_id, W, K):
rank = dict()
ru = train[user_id]
for i, pi in ru.items():
for j, wj in sorted(W[i].items(), key=itengetter(1), reverse=True)[0:K]:
if j in ru:
continue
rank[j] += pi * wj
return rank
此处是一个基于物品推荐的简单例子。该例子中,用户喜欢《C++ Primer中文版》和《编程之美》两本书。然后 I t e m C F ItemCF ItemCF 会为这两本书分别找出和它们最相似的 5 5 5 本书,然后根据公式计算用户对每本书的感兴趣程度。
I t e m C F ItemCF ItemCF 给用户推荐《算法导论》,是因为这本书和《C++ Primer中文版》相似,相似度为 0.4 0.4 0.4,而且这本书也和《编程之美》相似,相似度是 0.5 0.5 0.5。用户对《C++ Primer中文版》的兴趣度是 1.3 1.3 1.3,对《编程之美》的兴趣度是 0.9 0.9 0.9,则用户对《算法导论》的兴趣度就是 1.3 × 0.4 + 0.9 × 0.5 = 0.97 1.3 × 0.4 + 0.9 × 0.5 = 0.97 1.3×0.4+0.9×0.5=0.97。 以此类推……
从上述例子可以看到, I t e m C F ItemCF ItemCF 的一个优势就是可以提供推荐解释,即利用用户历史上喜欢的物品为现在的推荐结果进行解释。
如下代码实现了带解释的 I t e m C F ItemCF ItemCF 算法:
def Recommendation(train, user_id, W, K):
rank = dict()
ru = train[user_id]
for i, pi in ru.items():
for j, wj in sorted(W[i].items(), key=itengetter(1), reverse=True)[0:K]:
if j in ru:
continue
rank[j].weight += pi * wj
rank[j].reason[i] = pi * wj
return rank
在协同过滤中两个物品产生相似度是因为它们共同出现在很多用户的兴趣列表中。换句话说,每个用户的兴趣列表都对物品的相似度产生贡献。
但不是每个用户的贡献都相同。
假设有一个开书店的用户,买了当当网上80%的书自己卖。则他的购物车里包含当当网80%的书。假设当当网有100万本书,则他买了80万本。这意味着由于存在该用户,有80万本书两两之间产生了相似度,那么内存里即将诞生一个80万乘80万的稠密矩阵。
另外,该用户虽然活跃,但买这些书并非出于自身兴趣,且这些书覆盖了当当网图书的很多领域,所以这个用户对于他所买书的两两相似度的贡献远远小于一个只买了十几本自己喜欢的书的文学青年。
为了解决上述问题,John S. Breese 提出了一个称为IUF(Inverse User Frequence),即用户活跃度对数的 倒数的参数,他认为活跃用户对物品相似度的贡献应该小于不活跃的用户,他提出应该增加IUF参数来修正物品相似度的计算公式: w i j = ∑ u ∈ N ( i ) ∩ N ( j ) 1 l o g 1 + ∣ N ( u ) ∣ ∣ N ( i ) ∣ ∣ N ( j ) ∣ w_{ij}=\frac{\displaystyle \sum_{u\in{N(i)\cap{N(j)}}}\frac{1}{log1+|N(u)|}}{\sqrt{|N(i)||N(j)|}} wij=∣N(i)∣∣N(j)∣u∈N(i)∩N(j)∑log1+∣N(u)∣1
将该算法记为 I t e m C F − I U F ItemCF-IUF ItemCF−IUF
上述公式对活跃用户做了一种软性的惩罚,但对于很多过于活跃的用户,为了避免相似度矩阵过于稠密,在实际计算中一般直接忽略其兴趣列表,不将其纳入到相似度计算的数据集中。
代码如下:
def ItemSimilarity(train):
# 统计相似度矩阵
C = dict()
N = dict()
for u, items in train.items():
for i in users:
N[i] += 1
for j in users:
if i == j:
continue
C[i][j] += 1 / math.1og(1 + len(items) * 1.0)
# 计算相似度矩阵
W = dict()
for i, related_items in C.items():
for j, cij in related_items.items():
W[u][v] = cij / math.sqrt(N[i] * N[j])
return W
如果将 I t e m C F ItemCF ItemCF 的相似度矩阵按最大值归一化,可以提高推荐的准确率。如果已经得到了物品相似度矩阵 w w w,可以用如下公式得到归一化之后的相似度矩阵 w ′ w' w′: w i j ′ = w i j m a x j w i j w'_{ij}=\frac{w_{ij}}{max_{j}\,w_{ij}} wij′=maxjwijwij
一般来说,物品总是属于很多不同的类,每一类中的物品联系比较紧密。
举一个例子,假设在某电影网站中,有 A A A 类片和 B B B 类片。那么, I t e m C F ItemCF ItemCF 算出来的相似度一般是 A A A 类片和 A A A 类片的相似度或者 B B B 类片和 B B B 类片的相似度大于 A A A 类片和 B B B 类片的相似度。但是 A A A 类片之间的相似度和 B B B 类片之间的相似度却不一定相同。
假设物品分为两类—— A A A 和 B B B, A A A 类物品之间的相似度为 0.5 0.5 0.5, B B B 类物品之间的相似度为 0.6 0.6 0.6,而 A A A 类物品和 B B B 类物品之间的相似度是 0.2 0.2 0.2。
在这种情况下, 如果一个用户喜欢了 5 5 5 个 A A A 类物品和 5 5 5 个 B B B 类物品,用 I t e m C F ItemCF ItemCF 给他进行推荐,推荐的就都是 B B B 类物品, 因为 B B B 类物品之间的相似度大。但如果归一化之后, A A A 类物品之间的相似度变成了 1 1 1, B B B 类物品之间的相似度也是 1 1 1,这种情况下,用户如果喜欢 5 5 5 个 A A A 类物品和 5 5 5 个 B B B 类物品,则他的推荐列表中 A A A 类物品和 B B B 类物品的数目也是大致相等的。
从该例子可以看出,相似度的归一化可以提高推荐的多样性。
对于两个不同的类,一般来说,热门的类其类内物品相似度一般比较大。如果不进行归一化,就会推荐比较热门的类里面的物品,而这些物品也是比较热门的。因此,推荐的覆盖率就比较低。相反,如果进行相似度的归一化,则可以提高推荐系统的覆盖率。
参考资料:《推荐系统实践》