书名:推荐系统实践 作者:项亮
链接:微信读书链接
文章目录
用户行为反馈的分类
显性反馈行为
包含明确表示对用户喜好的行为,主要的方式是评分和喜欢/不喜欢
隐形反馈行为
用户操作行为数据及浏览行为数据
下面是显性反馈数据和隐形反馈数据的比较:
正反馈指用户的行为倾向于指用户喜欢该物品,而负反馈指用户的行为倾向于指用户不喜欢该物品。
下面是各代表网站中显性反馈数据和隐性反馈数据的例子
用户行为表示
用户行为表示为6部分,即产生行为的用户和行为的对象、行为的种类、产生行为的上下文、行为的内容和权重。
主要针对的是无上下文信息的隐性反馈数据集。即每一条行为记录仅仅包含用户ID和物品ID。
用户行为分析
用户活跃度和物品流行度的分布
很多互联网数据都满足Power Law 的分布,也称为长尾分布
f
(
x
)
=
α
x
k
f(x) = \alpha x^k
f(x)=αxk
令fu(k)为对k个物品产生过行为的用户数,令fi (k)为被k个用户产生过行为的物品数。那么,fu(k)和fi (k)都满足长尾分布。也就是说:
f
i
(
k
)
=
α
i
k
β
i
f_i(k)=\alpha_i k^{\beta_i}
fi(k)=αikβi
f
u
(
k
)
=
α
u
k
β
u
f_u(k)=\alpha_u k^{\beta_u}
fu(k)=αukβu
用户活跃度和物品流行度的关系
一般来说,不活跃的用户要么是新用户,要么是只来过网站一两次的老用户。那么,不同活跃度的用户喜欢的物品的流行度是否有差别?一般认为,新用户倾向于浏览热门的物品,因为他们对网站还不熟悉,只能点击首页的热门物品,而老用户会逐渐开始浏览冷门的物品。
其中横坐标是用户活跃度,纵坐标是具有某个活跃度的所有用户评过分的物品的平均流行度。图中曲线呈明显下降的趋势,这表明用户越活跃,越倾向于浏览冷门的物品。
仅仅基于用户行为数据设计的推荐算法一般称为协同过滤算法。比如基于邻域的方法(neighborhood-based)、隐语义模型(latent factor model)、基于图的随机游走算法(random walk on graph)等。在这些方法中,最著名的、在业界得到最广泛应用的算法是基于邻域的方法,而基于邻域的方法主要包含下面两种算法。
- 基于用户的协同过滤算法 这种算法给用户推荐和他兴趣相似的其他用户喜欢的物品。
- 基于物品的协同过滤算法 这种算法给用户推荐和他之前喜欢的物品相似的物品。
实验设计和算法评测
通过离线实验的方法评测提到的算法。首先确认用到的数据集,然后介绍采用的实验方法和评测指标。
数据集
数据集包含6000多用户对4000多部电影的100万条评分。该数据集是一个评分数据集,用户可以给电影评5个不同等级的分数(1~5分)。着重研究隐反馈数据集中的TopN推荐问题,因此忽略了数据集中的评分记录,也就是说,TopN推荐的任务是预测用户会不会对某部电影评分,而不是预测用户在准备对某部电影评分的前提下会给电影评多少分。
实验设计
协同过滤算法的离线实验一般如下设计。首先,将用户行为数据集按照均匀分布随机分成M份(取M=8),挑选一份作为测试集,将剩下的M-1份作为训练集。然后在训练集上建立用户兴趣模型,并在测试集上对用户行为进行预测,统计出相应的评测指标。为了保证评测指标并不是过拟合的结果,需要进行M次实验,并且每次都使用不同的测试集。然后将M次实验测出的评测指标的平均值作为最终的评测指标。
下面的Python代码描述了将数据集随机分成训练集和测试集的过程:
def SplitData(data, M, k, seed):
test = []
train = []
random.seed(seed)
for user, item in data:
if random.randint(0, M) == k:
test.append([user, item])
else:
train.append([user, item])
return train, test
这里,每次实验选取不同的k(0≤k≤M-1)和相同的随机数种子seed,进行M次实验就可以得到M个不同的训练集和测试集,然后分别进行实验,用M次实验的平均值作为最后的评测指标。这样做主要是防止某次实验的结果是过拟合的结果(over fitting),但如果数据集够大,模型够简单,为了快速通过离线实验初步地选择算法,也可以只进行一次实验。
评测指标
对用户u推荐N个物品(记为R(u)),令用户u在测试集上喜欢的物品集合为T(u),然后可以通过准确率/召回率评测推荐算法的精度:
召回率描述有多少比例的用户—物品评分记录包含在最终的推荐列表中,而准确率描述最终的推荐列表中有多少比例是发生过的用户—物品评分记录。下面两段代码给出了召回率和准确率的计算方法。
def Recall(train, test, N):
hit = 0
all = 0
for user in train.keys():
tu = test[user]
rank = GetRecommendation(user, N)
for item, pui in rank:
if item in tu:
hit += 1
all += len(tu)
return hit / (all * 1.0)
def Precision(train, test, N):
hit = 0
all = 0
for user in train.keys():
tu = test[user]
rank = GetRecommendation(user, N)
for item, pui in rank:
if item in tu:
hit += 1
all += N
return hit / (all * 1.0)
除了评测推荐算法的精度,还计算了算法的覆盖率,覆盖率反映了推荐算法发掘长尾的能力,覆盖率越高,说明推荐算法越能够将长尾中的物品推荐给用户。这里,我们采用最简单的覆盖率定义:
该覆盖率表示最终的推荐列表中包含多大比例的物品。如果所有的物品都被推荐给至少一个用户,那么覆盖率就是100%。如下代码可以用来计算推荐算法的覆盖率:
def Coverage(train, test, N):
recommend_items = set()
all_items = set()
for user in train.keys():
for item in train[user].keys():
all_items.add(item)
rank = GetRecommendation(user, N)
for item, pui in rank:
recommend_items.add(item)
return len(recommend_items) / (len(all_items) * 1.0)
最后,我们还需要评测推荐的新颖度,这里用推荐列表中物品的平均流行度度量推荐结果的新颖度。如果推荐出的物品都很热门,说明推荐的新颖度较低,否则说明推荐结果比较新颖。
def Popularity(train, test, N):
item_popularity = dict()
for user, items in train.items():
for item in items.keys()
if item not in item_popularity:
item_popularity[item] = 0
item_popularity[item] += 1
ret = 0
n = 0
for user in train.keys():
rank = GetRecommendation(user, N)
for item, pui in rank:
ret += math.log(1 + item_popularity[item])
n += 1
ret /= n * 1.0
return ret
这里,在计算平均流行度时对每个物品的流行度取对数,这是因为物品的流行度分布满足长尾分布,在取对数后,流行度的平均值更加稳定。
基于邻域的算法
基于邻域的算法分为两大类,一类是基于用户的协同过滤算法,另一类是基于物品的协同过滤算法。
基于用户的协同过滤算法
基础算法
在一个在线个性化推荐系统中,当一个用户A需要个性化推荐时,可以先找到和他有相似兴趣的其他用户,然后把那些用户喜欢的、而用户A没有听说过的物品推荐给A。这种方法称为基于用户的协同过滤算法。
从上面的描述中可以看到,基于用户的协同过滤算法主要包括两个步骤。
(1) 找到和目标用户兴趣相似的用户集合。
(2) 找到这个集合中的用户喜欢的,且目标用户没有听说过的物品推荐给目标用户。
步骤(1)的关键就是计算两个用户的兴趣相似度。这里,协同过滤算法主要利用行为的相似度计算兴趣的相似度。给定用户u和用户v,令N(u)表示用户u曾经有过正反馈的物品集合,令N(v)为用户v曾经有过正反馈的物品集合。那么,我们可以通过如下的Jaccard公式简单地计算u和v的兴趣相似度:
或者通过余弦相似度计算:
下面以图中的用户行为记录为例,举例说明UserCF计算用户兴趣相似度的例子。在该例中,用户A对物品{a, b, d}有过行为,用户B对物品{a, c}有过行为,利用余弦相似度公式计算用户A和用户B的兴趣相似度为:.
同理,我们可以计算出用户A和用户C、D的相似度:
以余弦相似度为例,实现该相似度可以利用如下的代码:
def UserSimilarity(train):
W = dict()
for u in train.keys():
for v in train.keys():
if u == v:
continue
W[u][v] = len(train[u] & train[v])
W[u][v] /= math.sqrt(len(train[u]) * len(train[v]) * 1.0)
return W
该代码对两两用户都利用余弦相似度计算相似度。这种方法的时间复杂度是
O
(
∣
U
∣
∗
∣
U
∣
)
O(|U|*|U|)
O(∣U∣∗∣U∣),这在用户数很大时非常耗时。事实上,很多用户相互之间并没有对同样的物品产生过行为,即很多时候
∣
N
(
u
)
∩
N
(
v
)
∣
=
0
|N(u) \cap N(v)|=0
∣N(u)∩N(v)∣=0。上面的算法将很多时间浪费在了计算这种用户之间的相似度上。如果换一个思路,我们可以首先计算出
∣
N
(
u
)
∩
N
(
v
)
∣
≠
0
|N(u) \cap N(v)| \ne 0
∣N(u)∩N(v)∣=0的用户对(u,v),然后再对这种情况除以分母
∣
N
(
u
)
∣
∣
N
(
v
)
∣
\sqrt{|N(u) ||N(v)|}
∣N(u)∣∣N(v)∣。
为此,可以首先建立物品到用户的倒排表,对于每个物品都保存对该物品产生过行为的用户列表。令稀疏矩阵
C
[
u
]
[
v
]
=
∣
N
(
u
)
∩
N
(
v
)
∣
C[u][v]=|N(u) \cap N(v)|
C[u][v]=∣N(u)∩N(v)∣。那么,假设用户u和用户v同时属于倒排表中K个物品对应的用户列表,就有C[u][v]=K。从而,可以扫描倒排表中每个物品对应的用户列表,将用户列表中的两两用户对应的C[u][v]加1,最终就可以得到所有用户之间不为0的C[u][v]。下面的代码实现了上面提到的算法:
def UserSimilarity(train):
# build inverse table for item_users
item_users = dict()
for u, items in train.items():
for i in items.keys():
if i not in item_users:
item_users[i] = set()
item_users[i].add(u)
#calculate co-rated items between users
C = dict()
N = dict()
for i, users in item_users.items():
for u in users:
N[u] += 1
for v in users:
if u == v:
continue
C[u][v] += 1
#calculate finial similarity matrix W
W = dict()
for u, related_users in C.items():
for v, cuv in related_users.items():
W[u][v] = cuv / math.sqrt(N[u] * N[v])
return W
下面结束上述算法:
首先,需要建立物品—用户的倒排表(如图所示)。然后,建立一个4×4的用户相似度矩阵W,对于物品a,将W[A][B]和W[B][A]加1,对于物品b,将W[A][C]和W[C][A]加1,以此类推。扫描完所有物品后,我们可以得到最终的W矩阵。这里的W是余弦相似度中的分子部分,然后将W除以分母可以得到最终的用户兴趣相似度。
得到用户之间的兴趣相似度后,UserCF算法会给用户推荐和他兴趣最相似的K个用户喜欢的物品。如下的公式度量了UserCF算法中用户u对物品i的感兴趣程度:
其中,S (u , K )包含和用户u兴趣最接近的K 个用户,N (i )是对物品i 有过行为的用户集合,wuv 是用户u和用户v的兴趣相似度,rvi 代表用户v对物品i的兴趣,因为使用的是单一行为的隐反馈数据,所以所有的rvi=1。
如下代码实现了上面的UserCF推荐算法:
def Recommend(user, train, W):
rank = dict()
interacted_items = train[user]
for v, wuv in sorted(W[u].items, key=itemgetter(1), \
reverse=True)[0:K]:
for i, rvi in train[v].items:
if i in interacted_items:
#we should filter items user interacted before
continue
rank[i] += wuv * rvi
return rank
利用上述算法,可以给上图中的用户A进行推荐。选取K=3,用户A对物品c、e没有过行为,因此可以把这两个物品推荐给用户A。根据UserCF算法,用户A对物品c、e的兴趣是:
p( A, c)=wAB+wAD=0.7416
p( A, e)=wAC+wAD=0.7416
表通过MovieLens数据集上的离线实验来评测基础算法的性能。UserCF只有一个重要的参数K,即为每个用户选出K个和他兴趣最相似的用户,然后推荐那K个用户感兴趣的物品。因此离线实验测量了不同K值下UserCF算法的性能指标。
为了反映该数据集上离线算法的基本性能,下表给出了两种基本推荐算法的性能。表中,Random算法每次都随机挑选10个用户没有产生过行为的物品推荐给当前用户,MostPopular算法则按照物品的流行度给用户推荐他没有产生过行为的物品中最热门的10个物品。这两种算法都是非个性化的推荐算法,但它们代表了两个极端。如下表所示,MostPopular算法的准确率和召回率远远高于Random算法,但它的覆盖率非常低,结果都非常热门。可见,Random算法的准确率和召回率很低,但覆盖度很高,结果平均流行度很低。
UserCF的准确率和召回率相对MostPopular算法提高了将近1倍。同时,UserCF的覆盖率远远高于MostPopular,推荐结果相对MostPopular不太热门。同时可以发现参数K是UserCF的一个重要参数,它的调整对推荐算法的各种指标都会产生一定的影响。
- 准确率和召回率 可以看到,推荐系统的精度指标(准确率和召回率)并不和参数K 成线性关系。在MovieLens数据集中,选择K =80左右会获得比较高的准确率和召回率。因此选择合适的K 对于获得高的推荐系统精度比较重要。当然,推荐结果的精度对K 也不是特别敏感,只要选在一定的区域内,就可以获得不错的精度。
- 流行度 可以看到,在3个数据集上K 越大则UserCF推荐结果就越热门。这是因为K 决定了UserCF在给你做推荐时参考多少和你兴趣相似的其他用户的兴趣,那么如果K 越大,参考的人越多,结果就越来越趋近于全局热门的物品。
- 覆盖率 可以看到,在3个数据集上,K 越大则UserCF推荐结果的覆盖率越低。覆盖率的降低是因为流行度的增加,随着流行度增加,UserCF越来越倾向于推荐热门的物品,从而对长尾物品的推荐越来越少,因此造成了覆盖率的降低。
用户相似度计算的改进
讨论一下上述的推荐性能
首先,以图书为例,如果两个用户都曾经买过《新华字典》,这丝毫不能说明他们兴趣相似,因为绝大多数中国人小时候都买过《新华字典》。但如果两个用户都买过《数据挖掘导论》,那可以认为他们的兴趣比较相似,因为只有研究数据挖掘的人才会买这本书。换句话说,两个用户对冷门物品采取过同样的行为更能说明他们兴趣的相似度。因此,John S. Breese在论文[插图]中提出了如下公式,根据用户行为计算用户的兴趣相似度:
可以看到,该公式通过惩罚了用户u和用户v共同兴趣列表中热门物品对他们相似度的影响。
将基于上述用户相似度公式的UserCF算法记为User-IIF算法。
下面的代码实现了上述用户相似度公式。
def UserSimilarity(train):
# build inverse table for item_users
item_users = dict()
for u, items in train.items():
for i in items.keys():
if i not in item_users:
item_users[i] = set()
item_users[i].add(u)
#calculate co-rated items between users
C = dict()
N = dict()
for i, users in item_users.items():
for u in users:
N[u] += 1
for v in users:
if u == v:
continue
C[u][v] += 1 / math.log(1 + len(users))
#calculate finial similarity matrix W
W = dict()
for u, related_users in C.items():
for v, cuv in related_users.items():
W[u][v] = cuv / math.sqrt(N[u] * N[v])
return W
在上一节的实验中,K=80时UserCF的性能最好,因此这里的实验同样选取K=80。
UserCF-IIF在各项性能上略优于UserCF。这说明在计算用户兴趣相似度时考虑物品的流行度对提升推荐结果的质量确实有帮助。
实际在线系统使用UserCF的例子
相比我们后面要讨论的基于物品的协同过滤算法(ItemCF),UserCF在目前的实际应用中使用并不多。其中最著名的使用者是Digg,它在2008年对推荐系统进行了新的尝试。Digg使用推荐系统的原因也是信息过载,它的研究人员经过统计发现,每天大概会有15000篇新的文章,而每个用户的精力是有限的,而且兴趣差别很大。因此Digg觉得应该通过推荐系统帮用户从这么多篇文章中找到真正令他们感兴趣的内容,同时使每篇文章都有机会被展示给用户。
Digg的推荐系统设计思路如下。用户在Digg中主要通过“顶”和“踩”,两种行为表达自己对文章的看法。当用户顶了一篇文章,Digg就认为该用户对这篇文章有兴趣,而且愿意把这篇文章推荐给其他用户。然后,Digg找到所有在该用户顶文章之前也顶了这一篇文章的其他用户,然后给他推荐那些人最近顶的其他文章。从这里的简单描述可以看到,Digg使用的是UserCF算法的简化版本。
Digg在博客中公布了使用推荐系统后的效果,主要指标如下所示。
- 用户反馈增加:用户“顶”和“踩”的行为增加了40%。
- 平均每个用户将从34个具相似兴趣的好友那儿获得200条推荐结果。
- 用户和好友的交互活跃度增加了24%。
- 用户评论增加了11%。
基于物品的协同过滤算法
基于物品的协同过滤(item-based collaborative filtering)算法是目前业界应用最多的算法。无论是亚马逊网,还是Netflix、Hulu、YouTube,其推荐算法的基础都是该算法。本节将从基础的算法开始介绍,然后提出算法的改进方法,并通过实际数据集评测该算法。
基础算法
基于用户的协同过滤算法在一些网站(如Digg)中得到了应用,但该算法有一些缺点。首先,随着网站的用户数目越来越大,计算用户兴趣相似度矩阵将越来越困难,其运算时间复杂度和空间复杂度的增长和用户数的增长近似于平方关系。其次,基于用户的协同过滤很难对推荐结果作出解释。因此,著名的电子商务公司亚马逊提出了另一个算法—基于物品的协同过滤算法。
基于物品的协同过滤算法(简称ItemCF)给用户推荐那些和他们之前喜欢的物品相似的物品。比如,该算法会因为你购买过《数据挖掘导论》而给你推荐《机器学习》。不过,ItemCF算法并不利用物品的内容属性计算物品之间的相似度,它主要通过分析用户的行为记录计算物品之间的相似度。该算法认为,物品A和物品B具有很大的相似度是因为喜欢物品A的用户大都也喜欢物品B。
基于物品的协同过滤算法可以利用用户的历史行为给推荐结果提供推荐解释,比如给用户推荐《天龙八部》的解释可以是因为用户之前喜欢《射雕英雄传》。Hulu在个性化视频推荐利用ItemCF给每个推荐结果提供了一个推荐解释,而用于解释的视频都是用户之前观看或者收藏过的视频。
基于物品的协同过滤算法主要分为两步。
(1) 计算物品之间的相似度。
(2) 根据物品的相似度和用户的历史行为给用户生成推荐列表。
购买了该商品的用户也经常购买的其他商品
从这句话的定义出发,我们可以用下面的公式定义物品的相似度:
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)∣这里,分母是喜欢物品i的用户数,而分子是同时喜欢物品i和物品j的用户数。因此,上述公式可以理解为喜欢物品i的用户中有多少比例的用户也喜欢物品j。
上述公式虽然看起来很有道理,但是却存在一个问题。如果物品j很热门,很多人都喜欢,那么Wij就会很大,接近1。因此,该公式会造成任何物品都会和热门的物品有很大的相似度,这对于致力于挖掘长尾信息的推荐系统来说显然不是一个好的特性。为了避免推荐出热门的物品,可以用下面的公式: w i j = ∣ N ( i ) ∩ N ( j ) ∣ ∣ N ( i ) ∣ ∣ N ( j ) ∣ w_{ij}=\frac {|N(i) \cap N(j)|}{\sqrt {|N(i)||N(j)|}} wij=∣N(i)∣∣N(j)∣∣N(i)∩N(j)∣这个公式惩罚了物品j的权重,因此减轻了热门物品会和很多物品相似的可能性。
从上面的定义可以看到,在协同过滤中两个物品产生相似度是因为它们共同被很多用户喜欢,也就是说每个用户都可以通过他们的历史兴趣列表给物品“贡献”相似度。这里面蕴涵着一个假设,就是每个用户的兴趣都局限在某几个方面,因此如果两个物品属于一个用户的兴趣列表,那么这两个物品可能就属于有限的几个领域,而如果两个物品属于很多用户的兴趣列表,那么它们就可能属于同一个领域,因而有很大的相似度。
和UserCF算法类似,用ItemCF算法计算物品相似度时也可以首先建立用户—物品倒排表(即对每个用户建立一个包含他喜欢的物品的列表),然后对于每个用户,将他物品列表中的物品两两在共现矩阵C中加1。详细代码如下所示:
def ItemSimilarity(train):
#calculate co-rated users between items
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
#calculate finial similarity matrix W
W = dict()
for i, related_items in C.items():
for j, cij in related_items.items():
W[i][j] = cij / math.sqrt(N[i] * N[j])
return W
图是一个根据上面的程序计算物品相似度的简单例子。图中最左边是输入的用户行为记录,每一行代表一个用户感兴趣的物品集合。然后,对于每个物品集合,我们将里面的物品两两加一,得到一个矩阵。最终将这些矩阵相加得到上面的C矩阵。其中C[i][j]记录了同时喜欢物品i和物品j的用户数。最后,将C矩阵归一化可以得到物品之间的余弦相似度矩阵W。
表展示了在MovieLens数据集上利用上面的程序计算电影之间相似度的结果。如表中结果所示,尽管在计算过程中没有利用任何内容属性,但利用ItemCF计算的结果却是可以从内容上看出某种相似度的。一般来说,同系列的电影、同主角的电影、同风格的电影、同国家和地区的电影会有比较大的相似度。
在得到物品之间的相似度后,ItemCF通过如下公式计算用户u对一个物品j的兴趣:
p
u
j
=
∑
i
∈
N
(
u
)
∩
S
(
j
,
K
)
w
j
i
r
u
i
p_{uj} = \sum_{i \in N(u) \cap S(j,K)}w_{ji}r_{ui}
puj=i∈N(u)∩S(j,K)∑wjirui这里N(u)是用户喜欢的物品的集合,S(j, K)是和物品j最相似的K个物品的集合,wji是物品j和i的相似度,rui是用户u对物品i的兴趣。(对于隐反馈数据集,如果用户u对物品i有过行为,即可令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=itemgetter(1), reverse=True)[0:K]:
if j in ru:
continue
rank[j] += pi * wj
return rank
图是一个基于物品推荐的简单例子。该例子中,用户喜欢《C++ Primer中文版》和《编程之美》两本书。然后ItemCF会为这两本书分别找出和它们最相似的3本书,然后根据公式的定义计算用户对每本书的感兴趣程度。比如,ItemCF给用户推荐《算法导论》,是因为这本书和《C++Primer中文版》相似,相似度为0.4,而且这本书也和《编程之美》相似,相似度是0.5。考虑到用户对《C++ Primer中文版》的兴趣度是1.3,对《编程之美》的兴趣度是0.9,那么用户对《算法导论》的兴趣度就是1.3× 0.4 +0.9×0.5 = 0.97。
从这个例子可以看到,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=itemgetter(1), reverse=True)[0:K]:
if j in ru:
continue
rank[j].weight += pi * wj
rank[j].reason[i] = pi * wj
return rank
表列出了在MovieLens数据集上ItemCF算法离线实验的各项性能指标的评测结果。该表包括算法在不同K值下的性能。根据表中的数据我们可以得出如下结论。
- 精度(准确率和召回率) 可以看到ItemCF推荐结果的精度也是不和K 成正相关或者负相关的,因此选择合适的K 对获得最高精度是非常重要的。
- 流行度 和UserCF不同,参数K 对ItemCF推荐结果流行度的影响也不是完全正相关的。随着K 的增加,结果流行度会逐渐提高,但当K 增加到一定程度,流行度就不会再有明显变化。
- 覆盖率K 增加会降低系统的覆盖率。
用户活跃度对物品相似度的影响
每个用户的兴趣列表都对物品的相似度产生贡献。那么,是不是每个用户的贡献都相同呢?
假设有这么一个用户,他是开书店的,并且买了当当网上80%的书准备用来自己卖。那么,他的购物车里包含当当网80%的书。假设当当网有100万本书,也就是说他买了80万本。从前面对ItemCF的讨论可以看到,这意味着因为存在这么一个用户,有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 {\sum_{u \in N(i) \cap N(j)} \frac {1}{log(1+|N(u)|)}}{\sqrt {|N(i)||N(j)|}}
wij=∣N(i)∣∣N(j)∣∑u∈N(i)∩N(j)log(1+∣N(u)∣)1当然,上面的公式只是对活跃用户做了一种软性的惩罚,但对于很多过于活跃的用户,比如上面那位买了当当网80%图书的用户,为了避免相似度矩阵过于稠密,我们在实际计算中一般直接忽略他的兴趣列表,而不将其纳入到相似度计算的数据集中。
def ItemSimilarity(train):
#calculate co-rated users between items
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.log(1 + len(items) * 1.0)
#calculate finial similarity matrix W
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
上面的算法记为ItemCF-IUF,下面我们用离线实验评测这个算法。在这里我们不再考虑参数K的影响,而是将K选为在前面实验中取得最优准确率和召回率的值10。
如表所示,ItemCF-IUF在准确率和召回率两个指标上和ItemCF相近,但ItemCF-IUF明显提高了推荐结果的覆盖率,降低了推荐结果的流行度。从这个意义上说,ItemCF-IUF确实改进了ItemCF的综合性能。
物品相似度的归一化
Karypis在研究中发现如果将ItemCF的相似度矩阵按最大值归一化,可以提高推荐的准确率。其研究表明,如果已经得到了物品相似度矩阵w,那么可以用如下公式得到归一化之后的相似度矩阵w':
w
i
j
′
=
w
i
j
m
a
x
j
w
i
j
w_{ij}^{'} = \frac {w_{ij}}{max_jw_{ij}}
wij′=maxjwijwij
其实,归一化的好处不仅仅在于增加推荐的准确度,它还可以提高推荐的覆盖率和多样性。一般来说,物品总是属于很多不同的类,每一类中的物品联系比较紧密。举一个例子,假设在一个电影网站中,有两种电影—纪录片和动画片。那么,ItemCF算出来的相似度一般是纪录片和纪录片的相似度或者动画片和动画片的相似度大于纪录片和动画片的相似度。但是纪录片之间的相似度和动画片之间的相似度却不一定相同。假设物品分为两类—A和B, A类物品之间的相似度为0.5, B类物品之间的相似度为0.6,而A类物品和B类物品之间的相似度是0.2。在这种情况下,如果一个用户喜欢了5个A类物品和5个B类物品,用ItemCF给他进行推荐,推荐的就都是B类物品,因为B类物品之间的相似度大。但如果归一化之后,A类物品之间的相似度变成了1, B类物品之间的相似度也是1,那么这种情况下,用户如果喜欢5个A类物品和5个B类物品,那么他的推荐列表中A类物品和B类物品的数目也应该是大致相等的。从这个例子可以看出,相似度的归一化可以提高推荐的多样性。
那么,对于两个不同的类,什么样的类其类内物品之间的相似度高,什么样的类其类内物品相似度低呢?一般来说,热门的类其类内物品相似度一般比较大。如果不进行归一化,就会推荐比较热门的类里面的物品,而这些物品也是比较热门的。因此,推荐的覆盖率就比较低。相反,如果进行相似度的归一化,则可以提高推荐系统的覆盖率。
表对比了ItemCF算法和ItemCF-Norm算法的离线实验性能。从实验结果可以看到,归一化确实能提高ItemCF的性能,其中各项指标都有了比较明显的提高。
UserCF和IthenCF的综合比较
UserCF的推荐结果着重于反映和用户兴趣相似的小群体的热点,而ItemCF的推荐结果着重于维系用户的历史兴趣。换句话说,UserCF的推荐更社会化,反映了用户所在的小型兴趣群体中物品的热门程度,而ItemCF的推荐更加个性化,反映了用户自己的兴趣传承。
从技术上考虑,UserCF需要维护一个用户相似度的矩阵,而ItemCF需要维护一个物品相似度矩阵。从存储的角度说,如果用户很多,那么维护用户兴趣相似度矩阵需要很大的空间,同理,如果物品很多,那么维护物品相似度矩阵代价较大。
表2-11从不同的角度对比了UserCF和ItemCF算法。同时,我们也将前面几节的离线实验结果展示在图2-13、图2-14和图2-15中。从图中可见,ItemCF算法在各项指标上似乎都不如UserCF,特别是其推荐结果的覆盖率和新颖度都低于UserCF,这一点似乎和我们之前讨论的不太符合。
哈利波特问题
亚马逊网的研究人员在设计ItemCF算法之初发现ItemCF算法计算出的图书相关表存在一个问题,就是很多书都和《哈利波特》相关。也就是说,购买任何一本书的人似乎都会购买《哈利波特》。后来他们研究发现,主要是因为《哈利波特》太热门了,确实是购买任何一本书的人几乎都会购买它。
回顾一下ItemCF计算物品相似度的经典公式:
w
i
j
=
∣
N
(
i
)
∩
N
(
j
)
∣
∣
N
(
i
)
∣
∣
N
(
j
)
∣
w_{ij}=\frac {|N(i) \cap N(j)|}{\sqrt {|N(i)||N(j)|}}
wij=∣N(i)∣∣N(j)∣∣N(i)∩N(j)∣前面说过,如果j非常热门,那么上面公式的分子就会越来越接近|N(i)|。尽管上面的公式分母已经考虑到了j的流行度,但在实际应用中,热门的j仍然会获得比较大的相似度。
哈利波特问题有几种解决方案。
第一种是最容易想到的,我们可以在分母上加大对热门物品的惩罚,比如采用如下公式::
w
i
j
=
∣
N
(
i
)
∩
N
(
j
)
∣
∣
N
(
i
)
∣
1
−
α
∣
N
(
j
)
∣
α
w_{ij}=\frac {|N(i) \cap N(j)|}{ |N(i)|^{1- \alpha }|N(j)|^\alpha}
wij=∣N(i)∣1−α∣N(j)∣α∣N(i)∩N(j)∣
其中
α
∈
[
0.5
,
1
]
\alpha \in[0.5,1]
α∈[0.5,1]通过提高α,就可以惩罚热门的j。
表2-12给出了选择不同的α惩罚热门物品后,ItemCF算法的推荐性能。这里,如果α=0.5就是标准的ItemCF算法。从离线实验结果可以看到,α只有在取值为0.5时才会导致最高的准确率和召回率,而无论α<0.5或者α>0.5都不会带来这两个指标的提高。但是,如果看覆盖率和平均流行度就可以发现,α越大,覆盖率就越高,并且结果的平均热门程度会降低。因此,通过这种方法可以在适当牺牲准确率和召回率的情况下显著提升结果的覆盖率和新颖性(降低流行度即提高了新颖性)。
不过,上述方法还不能彻底地解决哈利波特问题。每个用户一般都会在不同的领域喜欢一种物品。以电视为例,看新闻联播是父辈每天的必修课,他们每天基本就看新闻联播,而且每天不看别的新闻,就看这一种新闻。此外,他们很多都是电视剧迷,都会看央视一套8点的电视剧。那么,最终结果就是黄金时间的电视剧都和新闻联播相似,而新闻联播和其他新闻的相似度很低。
上面的问题换句话说就是,两个不同领域的最热门物品之间往往具有比较高的相似度。这个时候,仅仅靠用户行为数据是不能解决这个问题的,因为用户的行为表示这种物品之间应该相似度很高。此时,我们只能依靠引入物品的内容数据解决这个问题,比如对不同领域的物品降低权重等。这些就不是协同过滤讨论的范畴了。