10.1 协同过滤
在2012年初,爆出了这样一则新闻故事:一位男子进入一家Target[1]商店,挥舞着手中的一叠优惠券,这些都是Target邮寄给他还在读高中的女儿的。他来的目的是谴责经理,因为这套优惠券都是诸如婴儿服装、配方奶和幼儿家具这类商品专享的。
听到顾客的投诉,经理再三道歉。他感觉很糟糕,想在几天后通过电话跟进,解释这是怎么回事。这个时候,反而是这位父亲在电话里进行了道歉。看来他的女儿确实是怀孕了。她的购物习惯泄露了她的这个秘密。
出卖这位女生的算法很可能是,至少部分是,基于协同过滤。
那么,什么是协同过滤?
协同过滤(collaborative filtering)是基于这样的想法,在某处总有和你趣味相投的人。假设你和趣味相投的人们评价方式都非常类似,而且你们都已经以这种方式评价了一组特定的项目,此外,你们每个人对其他人尚未评价的项目也有过评价。正如已经假设的那样,你们的口味是类似的,因此可以从趣味相投的人们那里,提取具有很高评分而你尚未评价的项目,作为给你的推荐,反之亦然。在某种程度上,这点和数字化配对非常相像,但结果是你喜欢的歌曲或产品,而不是与异性的约会。
对于怀孕的高中生这个案例,当她购买了无味的乳液、棉球和维生素补充剂[2]之后,她可能就和那些稍后继续购买婴儿床和尿布的人匹配上了。
10.1.1 基于用户的过滤
让我们通过一个例子来看看实践中这是如何运作的。
我们将从被称为效用矩阵(utility matrx)的东西开始。它和词条-文档矩阵相类似,不过这里我们表示的是产品和用户,而不再是词条和文档。
这里,我们假设有顾客A到D,以及他们所评分的一组产品,评分从0到5,如表10-1所示。
表10-1
Snarky's Potato Chips | SoSo SmoothLotion | DufflyBeer | BetterTapWater | XXLargeLivin' Football Jersey | SnowyCottonBalls | Disposos'Diapers | |
A | 4 | 5 | 3 | 5 | |||
B | 4 | 4 | 5 | ||||
C | 2 | 2 | 1 | ||||
D | 5 | 3 | 5 | 4 |
之前我们看到,当想要查找类似的项目时,可以使用余弦相似度。让我们在这里试试。我们将为用户A发现最相似的其他顾客。由于这里的向量是稀疏的,包含了许多未评分的项目,我们将在这些缺失的地方输入一些默认值。这里填入0。我们从用户A和用户B的比较开始。
from sklearn.metrics.pairwise import cosine_similarity cosine_similarity(np.array([4,0,5,3,5,0,0]).reshape(1,-1),\ np.array([0,4,0,4,0,5,0]).reshape(1,-1))
上述代码生成图10-1的输出。
图10-1
我们可以看到,这两者没有很高的相似性,这是有道理的,因为他们没有多少共同的评分。
现在来看看用户C与用户A的比较。
cosine_similarity(np.array([4,0,5,3,5,0,0]).reshape(1,-1),\ np.array([2,0,2,0,1,0,0]).reshape(1,-1))
上述代码生成图10-2的输出。
图10-2
这里,我们看到他们有很高的相似度(记住1是完美的相似度), 尽管他们对同样产品的评价有所不同。为什么得到了这样的结果?[3]问题在于我们对没有评分的产品,选择使用0分。它表示强烈的(负的)一致性。在这种情况下,0不是中性的。
那么,如何解决这个问题?
我们可以做的是重新生成每位用户的评分,并使得平均分变为0或中性,而不是为缺失值简单地使用0。我们拿出每位用户的评分,将其减去该用户所有打分的平均值。例如,对于用户A,他打分的平均值为17/4,或4.25。然后我们从用户A提供的每个单独评分中减去这个值。
一旦完成,我们继续找到其他用户的平均值,从他们的每个评分中减去该均值,直到对每位用户完成该项操作。
这个过程后将产生表10-2。请注意,每行的用户评分总和为0 (这里忽略四舍五入带来的问题)。
表10-2
Snarky's Potato Chips | SoSo SmoothLotion | DufflyBeer | BetterTapWater | XXLargeLivin' Football Jersey | SnowyCottonBalls | Disposos'Diapers | |
A | - 0 .25 | 0 .75 | -1.25 | 0 .75 | |||
B | - 0 .33 | - 0 .33 | 0 .66 | ||||
C | 0 .33 | 0 .33 | - 0 .66 | ||||
D | 0 .75 | -1.25 | 0 .75 | - 0 .25 |
让我们在新的数据集上尝试余弦相似度。再次将用户A和用户B、C进行比较。
首先,A和B之间的比较如下。
cosine_similarity(np.array([-.25,0,.75,-1.25,.75,0,0])\ .reshape(1,-1),\ np.array([0,-.33,0,-.33,0,.66,0])\ .reshape(1,-1))
上述代码生成图10-3的输出。
图10-3
现在,我们试试看A和C。
cosine_similarity(np.array([-.25,0,.75,-1.25,.75,0,0])\ .reshape(1,-1),\ np.array([.33,0,.33,0,-.66,0,0])\ .reshape(1,-1))
上述代码生成图10-4的输出。
图10-4
我们可以看到,A和B之间的相似度略有增加,而A和C之间的相似度显著下降。这正是我们所希望的。
这种中心化的过程除了帮助我们处理缺失值之外,还有其他好处,例如帮助我们处理不同严苛程度的打分者,现在每位打分者的平均分都是0了。注意,这个公式等价于Pearson相关系数,取值落在−1和1之间。
让我们现在采用这个框架,使用它来预测产品的评分。我们将示例限制为三位用户X、Y和Z,我们将预测X尚未评价,而和X非常相似的Y和Z已经评过的产品,对于X而言会得到多少分。
我们先从每位用户的基本评分开始,如表10-3所示。
表10-3
Snarky's Potato Chips | SoSo SmoothLotion | DufflyBeer | BetterTapWater | XXLargeLivin' Football Jersey | SnowyCottonBalls | Disposos'Diapers | |
X | 4 | 3 | 4 | ||||
Y | 3.5 | 2.5 | 4 | 4 | |||
Z | 4 | 3.5 | 4.5 | 4.5 |
接下来,我们将中心化这些评分,如表10-4所示。
表10-4
Snarky's Potato Chips | SoSo SmoothLotion | DufflyBeer | BetterTapWater | XXLargeLivin' Football Jersey | SnowyCottonBalls | Disposos'Diapers | |
X | 0 .33 | - 0 .66 | 0 .33 | ? | |||
Y | 0 | -1 | 0 .5 | 0 .5 | |||
Z | - 0 .125 | - 0 .625 | 0 .375 | 0 .375 |
现在,我们想知道用户X会给Disposos' Diapers打多少分。我们可以根据用户评分中心化之后的余弦相似度获得权重,并通过这些权重对用户Y和用户Z的评分进行加权计算。
让我们先得到用户Y和X的相似度。
user_x = [0,.33,0,-.66,0,33,0] user_y = [0,0,0,-1,0,.5,.5] cosine_similarity(np.array(user_x).reshape(1,-1),\ np.array(user_y). reshape(1,-1))
上述代码生成图10-5的输出。
图10-5
现在计算用户Z和X的相似度。
user_x = [0,.33,0,-.66,0,33,0] user_z = [0,-.125,0,-.625,0,.375,.375] cosine_similarity(np.array(user_x).reshape(1,-1),\ np.array(user_z).reshape(1,-1))
上述代码生成图10-6的输出。
图10-6
因此,我们现在有一个用户X和用户Y之间的相似度(0.42447212),以及用户A和用户Z之间的相似度(0.46571861)。
将它们整合起来,我们通过每位用户与X之间的相似度,对每位用户的评分进行加权,然后除以总相似度。
(0.42447212 × (4) +0.46571861 × (4.5)) / (0.42447212 +0.46571861) = 4.26
我们可以看到用户X对Disposos' Diapers 的预估评分为4.26(不低啊,最好发张优惠券!)。
10.1.2 基于项目的过滤
到目前为止,我们只了解了基于用户的协同过滤,但还有一个可用的方法。在实践中,这种方法远优于基于用户的过滤[4],它被称为基于项目的过滤。这是它的工作原理:每个被评分项目与所有其他项目相比较,找到最相似的项,而不是根据评分历史将每位用户和所有其他用户相匹配。同时,也是使用中心化余弦相似度。
让我们来看看它是如何工作的。
再次,我们有一个效用矩阵。这一次,我们将看看用户对歌曲的评分。每一列是一位用户,而每一行是一首歌曲,如表10-5所示。
表10-5
U1 | U2 | U3 | U4 | U5 | |
S1 | 2 | 4 | 5 | ||
S2 | 3 | 3 | |||
S3 | 1 | 5 | 4 | ||
S4 | 4 | 4 | 4 | ||
S5 | 3 | 5 |
现在,假设我们想知道U3对于S5的评分。这里,我们会根据用户对歌曲的评分来寻找类似的歌曲,而不是寻找类似的用户。
让我们来看一个例子。
首先,我们从每行歌曲的中心化开始,并计算其他每首歌曲和目标歌曲(即S5)的余弦相似度,参见表10-6。
表10-6
U1 | U2 | U3 | U4 | U5 | CntrdCoSim | |
S1 | -1.66 | 0 .33 | 1.33 | 0 .98 | ||
S2 | 0 | 0 | 0 | |||
S3 | -2.33 | 1.66 | 0 .66 | 0 .72 | ||
S4 | 0 | 0 | 0 | 0 | ||
S5 | -1 | ? | 1 | 1 |
你可以看到,最右边的列是其他每行相对行S5的中心化余弦相似度。
接下来需要选择一个数字,k,这是我们为预测U3对歌曲的评分,所要使用的最近邻居数量。在这个简单的例子中,我们使用k = 2。
我们可以看到对于歌曲S5,S1和S3是和它最相似的,所以我们将使用U3对这两首歌的评分(分别为4和5)。
现在让我们计算评分。
(0.98 × (4) +0.72 × (5)) / (0.98 +0.72) = 4.42
因此,通过基于项目的协同过滤,我们可以看到U3很可能给S5打出高分4.42。
之前,我提到基于用户的过滤不如基于项目的过滤有效。这是为什么呢?
很有可能,你的朋友和你有共同的爱好,但是你们每个人都有自己喜欢,而别人毫无兴趣的领域。
例如,也许你们都喜欢“权力的游戏”这部电视剧,但你的朋友也喜欢Norwegian death metal重金属乐队。而你死也不愿意听这种音乐。如果你们在许多方面类似——除了death metal——那么基于用户的推荐,你仍然会看到很多关于乐队的推荐,其名称都包括火焰、斧头、头骨和大头棒这样的字眼。使用基于项目的过滤,很可能会避免让你看到这些推荐。
让我们用快速的代码示例,来总结这个问题的讨论。
首先,我们将创建DataFrame示例。
import pandas as pd import numpy as np from sklearn.metrics.pairwise import cosine_similarity df = pd.DataFrame({'U1':[2 , None, 1, None, 3], 'U2': [None, 3, None, 4, None],\ 'U3': [4, None, 5, 4, None], 'U4': [None, 3, None, 4, None], 'U5': [5, None, 4, None, 5]}) df.index = ['S1', 'S2', 'S3', 'S4', 'S5'] df
上述代码生成图10-7的输出。
图10-7
我们现在将创建一个函数,它将读取用户和项目的评分矩阵。对于给定的项目和用户,该函数将返回基于协同过滤的预测评分。
def get_sim(ratings, target_user, target_item, k=2): centered_ratings = ratings.apply(lambda x: x - x.mean(), axis=1) csim_list = [] for i in centered_ratings.index: csim_list.append(cosine_similarity(np.nan_to_num(centered_ratings.loc[i,:]. values).reshape(1, -1), np.nan_to_num(centered_ratings.loc[target_item,:]).reshape(1, -1)).item()) new_ratings = pd.DataFrame({'similarity': csim_list, 'rating': ratings[target_user]}, index=ratings.index) top = new_ratings.dropna().sort_values('similarity', ascending=False)[:k].copy() top['multiple'] = top['rating'] * top['similarity'] result = top['multiple'].sum()/top['similarity'].sum() return result
现在可以传入我们的值,并获得用户对项目的预测评分。
get_sim(df, 'U3', 'S5', 2)
上述代码生成图10-8的输出。
图10-8
我们可以看到这与之前的分析相符。
到目前为止,我们在进行比较时,将用户和项目作为整个的实体,但是现在,让我们继续了解另一种方法,它将我们的用户和项目分解为所谓的特征集合。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论