用Python实现协同过滤的教程

发表于 5年以前  | 总阅读数:468 次

协同过滤

在 用户 ―― 物品(user - item)的数据关系下很容易收集到一些偏好信息(preference),比如评分。利用这些分散的偏好信息,基于其背后可能存在的关联性,来为用户推荐物品的方法,便是协同过滤,或称协作型过滤(collaborative filtering)。

这种过滤算法的有效性基础在于:

用户的偏好具有相似性,即用户是可分类的。这种分类的特征越明显,推荐的准确率就越高  
物品之间是存在关系的,即偏好某一物品的任何人,都很可能也同时偏好另一件物品

不同环境下这两种理论的有效性也不同,应用时需做相应调整。如豆瓣上的文艺作品,用户对其的偏好程度与用户自身的品位关联性较强;而对于电子商务网站来说,商品之间的内在联系对用户的购买行为影响更为显著。当用在推荐上,这两种方向也被称为基于用户的和基于物品的。本文内容为基于用户的。
影评推荐实例

本文主要内容为基于用户偏好的相似性进行物品推荐,使用的数据集为 GroupLens Research 采集的一组从 20 世纪 90 年代末到 21 世纪初由 MovieLens 用户提供的电影评分数据。数据中包含了约 6000 名用户对约 4000 部电影的 100万条评分,五分制。数据包可以从网上下载到,里面包含了三个数据表――users、movies、ratings。因为本文的主题是基于用户偏好的,所以只使用 ratings 这一个文件。另两个文件里分别包含用户和电影的元信息。

本文使用的数据分析包为 pandas,环境为 IPython,因此其实还默认携带了 Numpy 和 matplotlib。下面代码中的提示符看起来不是 IPython 环境是因为 Idle 的格式发在博客上更好看一些。
数据规整

首先将评分数据从 ratings.dat 中读出到一个 DataFrame 里:


    >>> import pandas as pd
    >>> from pandas import Series,DataFrame
    >>> rnames = ['user_id','movie_id','rating','timestamp']
    >>> ratings = pd.read_table(r'ratings.dat',sep='::',header=None,names=rnames)
    >>> ratings[:3]
     user_id movie_id rating timestamp
    0  1  1193  5 978300760
    1  1  661  3 978302109
    2  1  914  3 978301968

    [3 rows x 4 columns]

ratings 表中对我们有用的仅是 user_id、movie_id 和 rating 这三列,因此我们将这三列取出,放到一个以 user 为行,movie 为列,rating 为值的表 data 里面。(其实将 user 与 movie 的行列关系对调是更加科学的方法,但因为重跑一遍太麻烦了,这里就没改。)


    >>> data = ratings.pivot(index='user_id',columns='movie_id',values='rating')
    >>> data[:5]
    movie_id 1 2 3 4 5 6 
    user_id                  
    1   5 NaN NaN NaN NaN NaN ...
    2  NaN NaN NaN NaN NaN NaN ...
    3  NaN NaN NaN NaN NaN NaN ...
    4  NaN NaN NaN NaN NaN NaN ...
    5  NaN NaN NaN NaN NaN 2 ...

可以看到这个表相当得稀疏,填充率大约只有 5%,接下来要实现推荐的第一步是计算 user 之间的相关系数,DataFrame 对象有一个很亲切的 .corr(method='pearson', min_periods=1) 方法,可以对所有列互相计算相关系数。method 默认为皮尔逊相关系数,这个 ok,我们就用这个。问题仅在于那个 min_periods 参数,这个参数的作用是设定计算相关系数时的最小样本量,低于此值的一对列将不进行运算。这个值的取舍关系到相关系数计算的准确性,因此有必要先来确定一下这个参数。

相关系数是用于评价两个变量间线性关系的一个值,取值范围为 [-1, 1],-1代表负相关,0 代表不相关,1 代表正相关。其中 0~0.1 一般被认为是弱相关,0.1~0.4 为相关,0.4~1 为强相关。

min_periods 参数测定

测定这样一个参数的基本方法为统计在 min_periods 取不同值时,相关系数的标准差大小,越小越好;但同时又要考虑到,我们的样本空间十分稀疏,min_periods 定得太高会导致出来的结果集太小,所以只能选定一个折中的值。

这里我们测定评分系统标准差的方法为:在 data 中挑选一对重叠评分最多的用户,用他们之间的相关系数的标准差去对整体标准差做点估计。在此前提下对这一对用户在不同样本量下的相关系数进行统计,观察其标准差变化。

首先,要找出重叠评分最多的一对用户。我们新建一个以 user 为行列的方阵 foo,然后挨个填充不同用户间重叠评分的个数:


    >>> foo = DataFrame(np.empty((len(data.index),len(data.index)),dtype=int),index=data.index,columns=data.index)
    >>> for i in foo.index:
      for j in foo.columns:
       foo.ix[i,j] = data.ix[i][data.ix[j].notnull()].dropna().count()

这段代码特别费时间,因为最后一行语句要执行 4000*4000 = 1600万遍;(其中有一半是重复运算,因为 foo 这个方阵是对称的)还有一个原因是 Python 的 GIL,使得其只能使用一个 CPU 线程。我在它执行了一个小时后,忍不住去测试了一下总时间,发现要三个多小时后就果断 Ctrl + C 了,在算了一小半的 foo 中,我找到的最大值所对应的行列分别为 424 和 4169,这两位用户之间的重叠评分数为 998:


    >>> for i in foo.index:
      foo.ix[i,i]=0#先把对角线的值设为 0

    >>> ser = Series(np.zeros(len(foo.index)))
    >>> for i in foo.index:
      ser[i]=foo[i].max()#计算每行中的最大值

    >>> ser.idxmax()#返回 ser 的最大值所在的行号
    4169

    >>> ser[4169]#取得最大值
    998

    >>> foo[foo==998][4169].dropna()#取得另一个 user_id
    424  4169
    Name: user_id, dtype: float64

我们把 424 和 4169 的评分数据单独拿出来,放到一个名为 test 的表里,另外计算了一下这两个用户之间的相关系数为 0.456,还算不错,另外通过柱状图了解一下他俩的评分分布情况:


    >>> data.ix[4169].corr(data.ix[424])
    0.45663851303413217
    >>> test = data.reindex([424,4169],columns=data.ix[4169][data.ix[424].notnull()].dropna().index)
    >>> test
    movie_id 2  6  10 11 12 17 ...
    424    4  4  4  4  1  5 ...
    4169    3  4  4  4  2  5 ...

    >>> test.ix[424].value_counts(sort=False).plot(kind='bar')
    >>> test.ix[4169].value_counts(sort=False).plot(kind='bar')

201548154049025.png \(371×261\)

201548154118207.png \(370×261\)

对这俩用户的相关系数统计,我们分别随机抽取 20、50、100、200、500 和 998 个样本值,各抽 20 次。并统计结果:


     >>> periods_test = DataFrame(np.zeros((20,7)),columns=[10,20,50,100,200,500,998])
    >>> for i in periods_test.index:
      for j in periods_test.columns:
       sample = test.reindex(columns=np.random.permutation(test.columns)[:j])
       periods_test.ix[i,j] = sample.iloc[0].corr(sample.iloc[1])


    >>> periods_test[:5]
      10  20  50  100  200  500  998
    0 -0.306719 0.709073 0.504374 0.376921 0.477140 0.426938 0.456639
    1 0.386658 0.607569 0.434761 0.471930 0.437222 0.430765 0.456639
    2 0.507415 0.585808 0.440619 0.634782 0.490574 0.436799 0.456639
    3 0.628112 0.628281 0.452331 0.380073 0.472045 0.444222 0.456639
    4 0.792533 0.641503 0.444989 0.499253 0.426420 0.441292 0.456639

    [5 rows x 7 columns]
    >>> periods_test.describe()
        10   20   50   100  200  500 #998略
    count 20.000000 20.000000 20.000000 20.000000 20.000000 20.000000 
    mean 0.346810 0.464726 0.458866 0.450155 0.467559 0.452448 
    std  0.398553 0.181743 0.103820 0.093663 0.036439 0.029758 
    min -0.444302 0.087370 0.192391 0.242112 0.412291 0.399875 
    25%  0.174531 0.320941 0.434744 0.375643 0.439228 0.435290 
    50%  0.487157 0.525217 0.476653 0.468850 0.472562 0.443772 
    75%  0.638685 0.616643 0.519827 0.500825 0.487389 0.465787 
    max  0.850963 0.709073 0.592040 0.634782 0.546001 0.513486 

    [8 rows x 7 columns]

从 std 这一行来看,理想的 min_periods 参数值应当为 200 左右。可能有人会觉得 200 太大了,这个推荐算法对新用户简直没意义。但是得说,随便算出个有超大误差的相关系数,然后拿去做不靠谱的推荐,又有什么意义呢。
算法检验

为了确认在 min_periods=200 下本推荐算法的靠谱程度,最好还是先做个检验。具体方法为:在评价数大于 200 的用户中随机抽取 1000 位用户,每人随机提取一个评价另存到一个数组里,并在数据表中删除这个评价。然后基于阉割过的数据表计算被提取出的 1000 个评分的期望值,最后与真实评价数组进行相关性比较,看结果如何。


    >>> check_size = 1000
    >>> check = {}
    >>> check_data = data.copy()#复制一份 data 用于检验,以免篡改原数据
    >>> check_data = check_data.ix[check_data.count(axis=1)>200]#滤除评价数小于200的用户
    >>> for user in np.random.permutation(check_data.index):
      movie = np.random.permutation(check_data.ix[user].dropna().index)[0]
      check[(user,movie)] = check_data.ix[user,movie]
      check_data.ix[user,movie] = np.nan
      check_size -= 1
      if not check_size:
       break


    >>> corr = check_data.T.corr(min_periods=200)
    >>> corr_clean = corr.dropna(how='all')
    >>> corr_clean = corr_clean.dropna(axis=1,how='all')#删除全空的行和列
    >>> check_ser = Series(check)#这里是被提取出来的 1000 个真实评分
    >>> check_ser[:5]
    (15, 593)  4
    (23, 555)  3
    (33, 3363) 4
    (36, 2355) 5
    (53, 3605) 4
    dtype: float64

接下来要基于 corr_clean 给 check_ser 中的 1000 个 用户-影片 对计算评分期望。计算方法为:对与用户相关系数大于 0.1 的其他用户评分进行加权平均,权值为相关系数:


    >>> result = Series(np.nan,index=check_ser.index)
    >>> for user,movie in result.index:#这个循环看着很乱,实际内容就是加权平均而已
      prediction = []
      if user in corr_clean.index:
       corr_set = corr_clean[user][corr_clean[user]>0.1].dropna()#仅限大于 0.1 的用户
      else:continue
      for other in corr_set.index:
       if not np.isnan(data.ix[other,movie]) and other != user:#注意bool(np.nan)==True
        prediction.append((data.ix[other,movie],corr_set[other]))
      if prediction:
       result[(user,movie)] = sum([value*weight for value,weight in prediction])/sum([pair[1] for pair in prediction])


    >>> result.dropna(inplace=True)
    >>> len(result)#随机抽取的 1000 个用户中也有被 min_periods=200 刷掉的
    862
    >>> result[:5]
    (23, 555)  3.967617
    (33, 3363) 4.073205
    (36, 2355) 3.903497
    (53, 3605) 2.948003
    (62, 1488) 2.606582
    dtype: float64
    >>> result.corr(check_ser.reindex(result.index))
    0.436227437429696
    >>> (result-check_ser.reindex(result.index)).abs().describe()#推荐期望与实际评价之差的绝对值
    count 862.000000
    mean  0.785337
    std  0.605865
    min  0.000000
    25%  0.290384
    50%  0.686033
    75%  1.132256
    max  3.629720
    dtype: float64

862 的样本量能达到 0.436 的相关系数,应该说结果还不错。如果一开始没有滤掉评价数小于 200 的用户的话,那么首先在计算 corr 时会明显感觉时间变长,其次 result 中的样本量会很小,大约 200+ 个。但因为样本量变小的缘故,相关系数可以提升到 0.5~0.6 。

另外从期望与实际评价的差的绝对值的统计量上看,数据也比较理想。
实现推荐

在上面的检验,尤其是平均加权的部分做完后,推荐的实现就没有什么新东西了。

首先在原始未阉割的 data 数据上重做一份 corr 表:


    >>> corr = data.T.corr(min_periods=200)
    >>> corr_clean = corr.dropna(how='all')
    >>> corr_clean = corr_clean.dropna(axis=1,how='all')

我们在 corr_clean 中随机挑选一位用户为他做一个推荐列表:


    >>> lucky = np.random.permutation(corr_clean.index)[0]
    >>> gift = data.ix[lucky]
    >>> gift = gift[gift.isnull()]#现在 gift 是一个全空的序列

最后的任务就是填充这个 gift:


    >>> corr_lucky = corr_clean[lucky].drop(lucky)#lucky 与其他用户的相关系数 Series,不包含 lucky 自身
    >>> corr_lucky = corr_lucky[corr_lucky>0.1].dropna()#筛选相关系数大于 0.1 的用户
    >>> for movie in gift.index:#遍历所有 lucky 没看过的电影
      prediction = []
      for other in corr_lucky.index:#遍历所有与 lucky 相关系数大于 0.1 的用户
       if not np.isnan(data.ix[other,movie]):
        prediction.append((data.ix[other,movie],corr_clean[lucky][other]))
      if prediction:
       gift[movie] = sum([value*weight for value,weight in prediction])/sum([pair[1] for pair in prediction])


    >>> gift.dropna().order(ascending=False)#将 gift 的非空元素按降序排列
    movie_id
    3245  5.000000
    2930  5.000000
    2830  5.000000
    2569  5.000000
    1795  5.000000
    981   5.000000
    696   5.000000
    682   5.000000
    666   5.000000
    572   5.000000
    1420  5.000000
    3338  4.845331
    669   4.660464
    214   4.655798
    3410  4.624088
    ...
    2833  1
    2777  1
    2039  1
    1773  1
    1720  1
    1692  1
    1538  1
    1430  1
    1311  1
    1164  1
    843   1
    660   1
    634   1
    591   1
    56   1
    Name: 3945, Length: 2991, dtype: float64

补充

上面给出的示例都是些原型代码,有很多可优化的空间。比如 data 的行列转换;比如测定 min_periods 时的方阵 foo 只需计算一半;比如有些 for 循环和相应运算可以用数组对象方法来实现(方法版比用户自己编写的版本速度快很多);甚至肯定还有一些 bug。另外这个数据集的体积还不算太大,如果再增长一个数量级,那么就有必要针对计算密集的部分(如 corr)做进一步优化了,可以使用多进程,或者 Cython/C 代码。(或者换更好的硬件)

虽然协同过滤是一种比较省事的推荐方法,但在某些场合下并不如利用元信息推荐好用。协同过滤会遇到的两个常见问题是

  1. 稀疏性问题――因用户做出评价过少,导致算出的相关系数不准确
  2. 冷启动问题――因物品获得评价过少,导致无"权"进入推荐列表中

都是样本量太少导致的。(上例中也使用了至少 200 的有效重叠评价数)因此在对于新用户和新物品进行推荐时,使用一些更一般性的方法效果可能会更好。比如给新用户推荐更多平均得分超高的电影;把新电影推荐给喜欢类似电影(如具有相同导演或演员)的人。后面这种做法需要维护一个物品分类表,这个表既可以是基于物品元信息划分的,也可是通过聚类得到的。

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237231次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8065次阅读
 目录