“猜你喜欢”的背后揭秘--10分钟教你用Python打造推荐系统
欲直接下载代码文件,关注我们的公众号哦!查看历史消息即可!
话说,最近的瓜实在有点多,从我科校友李雨桐怒锤某男、陈羽凡吸毒被捕、蒋劲夫家暴的三连瓜,到不知知网翟博士,再到邓紫棋解约蜂鸟、王思聪花千芳隔空互怼。
而最近的胜利夜店、张紫妍巨瓜案、最强大脑选手作弊丑闻,更是让吃瓜群众直呼忙不过来:瓜来的太快就像龙卷风,扶我起来,我还能吃!
说到底,这其实是一个信息过载的时代:公众号每天数十条的推送、朋友圈的晒娃晒旅游、各种新闻报道扑面而来令人眼花缭乱、目不暇接……
那么问题来了,怎么找到自己的关注点呢?俗话说得好,有问题,找度娘,输入关键词一回车就完事儿了。
然而,懒是人的天性,而有的人(比如小编)则连关键词都懒得搜,希望计算机能自动挖掘我们的兴趣点,并为我们推荐感兴趣的内容,所以我们就迫切地需要推荐系统来帮助我们了。那么现在我们就来讲讲推荐系统吧~
目录
01 什么是推荐系统
推荐系统相信大家并不陌生,从“我有歌也有热评”的云村里的每日歌曲推荐,淘宝的猜你喜欢,再到外卖APP和视频网站的推送,推荐系统似乎成了各种APP的宠儿(请忽略小编的老年人口味)。
热门app的推荐系统
简单来说,推荐系统就是根据用户的各种数据(历史行为数据、社交关系数据、关注点、上下文环境等)在海量数据中判断用户感兴趣的item并推荐给用户的系统。
“哇这个东西就是我想要的。”
“诶,这首歌还真好听。”
“emm这部电影还挺对我胃口的”
推荐系统对用户而言,能够简化搜寻过程、发现新鲜好玩令人惊喜的东西;而对商家而言,则是能够提供个性化服务,提高用户的信任度、粘度以及活跃度,从而提高营收。
02 推荐系统的评判标准
一个完整的推荐系统往往是十分复杂的。既然如此,仅仅通过准确率一个标准推荐系统作评测是远远不够的,为此我们需要定义多个标准,从多维度评价一个推荐系统的好坏。
notation
1.准确度
对打分系统(比如说淘宝的评论一到五星打分)而言,一说到评判标准,最先想到的肯定就是均方根误差RMSE和平均绝对误差MAE啦:
RMSE和MAE计算公式
对TOP N推荐(生成一个TOP N推荐列表)而言,Precision和Recall则是我们的关注点:
Precision和Recall计算公式
说人话版本:Precision就是指系统推荐的东西中用户感兴趣的有多少,Recall就是用户感兴趣的东西中你推荐了多少
举个栗子:
2.覆盖率
表示推荐系统对item长尾的发掘能力
科普知识
马太效应:强者愈强,弱者愈弱
长尾效应:大多数的需求会集中在头部(爆款商品),而分布在尾部的需求是个性化的、零散的、小量的需求(冷门商品)。但这部分差异化的、少量的需求会在需求曲线上面形成一条长长的“尾巴”。如果将所有非流行的市场累加起来就会形成一个比流行市场还大的市场。
由长尾效应可知,满足用户个性化的需求十分重要,所以推荐系统要尽可能地挖掘出合用户口味的冷门商品;如果推荐系统严重偏向推荐热门商品,那么只会造成热门商品越热门,冷门商品越冷门,对商家的营收十分不利。
所以这就要求推荐系统的覆盖率要高(推荐系统对所有用户推荐的item占item总数的比例):
覆盖率还有另一种计算方式:信息熵
其中,p(i)=第i件item被推荐次数/所有item总被推荐次数
科普时间:
“信息是用来消除随机不确定性的东西。”由高中化学知道,熵是用来衡量一个系统的混乱程度的,熵越大,混乱程度越高。而同样的,信息熵越大,对一件事情的不确定性就越大。
32支球队打世界杯,只有一队胜利,但是我们不知道关于比赛、关于球队的任何信息,也就是说每支球队获胜的概率为1/32。如果我们想要知道哪支队伍胜利,我们只能无任何根据地瞎猜,那么最要猜几次呢?通过折半查找法我们可以发现,我们顶多五次就能找到胜利的球队。所以说,这个问题的最大信息熵就是5bit(信息熵在p(i)全部相等时最大)。
如果这时候,我们知道了更多的信息,比如说是哪国球队(德国队和中国队你选哪个?)、球队的既往胜率,那么这时候,各个球队获胜的概率就发生了变动,而此时信息熵也得到了降低。
回到我们的推荐系统中来,信息熵越大,表明item之间的p(i)越接近,也就是说每个item被推荐的次数越接近, 即覆盖率越大。
3.多样性
表示推荐列表中物品之间的不相似性
Q:为什么需要多样性?
A:试想,一个喜欢裙子的用户,如果你给她推荐的十件商品都是裙子,那她可能也只会买一条最合心意的裙子,倒不如把一部分的推荐名额给其他种类的商品;另外,一位用户买了一台计算机,你还给他推荐另外的计算机吗?从商家的角度看,推荐鼠标、键盘等是最好的选择。
所以我们的推荐列表需要尽可能地拓宽种类,增加用户的购买欲望。
4.还有其他的评判标准
新颖度:新颖的推荐是指给用户推荐那些他们以前没有听说过的物品。
惊喜度:推荐结果和用户的历史兴趣不相似,但却让用户觉得满意。(而新颖性仅仅取决于用户是否听说过这个推荐结果。)
信任度:推荐系统给你推荐的依据是什么(“你的朋友也喜欢这首歌”比起“喜欢那首歌的人也喜欢这首歌”更能让用户信任)
03 算法(具体实现请看第四部分)
1. 协同过滤****(collaborative filtering)
回想一下,当你遇到剧荒、歌荒时,会怎么做?
“诶,小陈,最近有啥好看的电视剧,给我安利安利呗。”
相信大多数人首先想到的都是找一个趣味相投的朋友,问一下他们有啥好推荐的。
协同过滤也就是基于这样的思想。而协同过滤分为两种:user-based(基于用户,找到最相似的user)和item-based(基于商品,找到最相似的item)。
那么,协同过滤需要解决的核心问题是:
如何找到最相似的user/item?
因此我们需要衡量相似性的指标:
其实Pearson相似度是考虑了user/item之间的差异而来。
Q:为什么要减去mean?
A:比如,user1打分十分苛刻,3分已经是相当好的商品;而user2是一位佛系用户,无论商品有多差,打分时3分起步。那么我们可以说user1的3分和user2的5分是等价的。而Pearson相似度就是通过减去mean来进行相对地归一化。
user-based/item-based算法流程:
1. 计算目标user/item和其余user/item的相似度
2. 选择与目标user/item有正相似度的user/item
3. 加权打分
举个栗子(使用Pearson相似度和user-based):
我们有这样一个打分表,想要预测USER2对ITEM3的打分
2. 隐因子模型(Latent Factors Model)
我们有一个user对item的打分矩阵,但是有一些位置是空着的(我们候选推荐的item),所以我们要做的,就是把这些空位一网打尽,一次性填满。
隐因子模型的基本思想就是:
打分矩阵R只有两个维度:user和item,那么能否引入影响用户打分的隐藏因素(即隐因子,不一定是人可以理解的,如同神经网络一样的黑箱)在user和item之间搭一座桥(分解为多个矩阵,使得这些矩阵的乘积近似于R)
举个例子:
隐因子模型举例
仔细观察,在非0的部位上R和R’十分接近,而在0的部位上,R’得到了填充,而这可以作为我们推荐的依据。
Q:如何分解矩阵?
A:说到矩阵分解,首先想到的就是SVD了(Python AI 教学|SVD(Singular Value Decomposition)算法及应用)。
然而,SVD的时间复杂度为O(n3),在这里小编推荐另一种实现:梯度下降
算法流程:
隐因子模型的梯度下降实现算法流程
算法改进:
前面提到了用户之间评分标准的差异性(某些用户比较严格),而我们通过引入正则化项和偏差项(user bias和item bias)来进行优化。由于篇幅原因,本文不作具体解释,感兴趣的朋友麻烦自行百度啦~
3. 算法优缺点比较:
冷启动问题:
对于冷启动问题,一般分为三类:
用户冷启动:如何对新用户做个性化推荐。
物品冷启动:如何将新加进来的物品推荐给对它感兴趣的用户。
系统冷启动:新开发的网站如何设计个性化推荐系统。
在user-based和item-based之间,一般使用item-based,因为item-based稳定性高(user的打分标准、喜好等飘忽不定,有很大的不确定性),而且user太多时计算量大。
04 手把手打造一个推荐系统
都说女人心海底针,还在为选什么电影才能打动妹子烦恼吗?还在担心无法彰显自己的品味吗?相信下面的电影推荐系统代码一定能够回答你关于如何选择电影的疑惑。(捂脸,逃)
1. 数据源
小编通过问卷调查获取了朋友圈对15部电影的评分(1代表第一个选项即没看过,26表示15星)
2.代码
注:python有许多方便计算的函数,如norm()计算向量的模和corrcoef()计算pearson相似度,不过为了广大朋友们记忆深刻,小编这里自己来实现这些计算~
1# -*- coding: utf-8 -*-
2
3"""
4Created on Thu Mar 21 19:29:54 2019
5
6@author: o
7"""
8import pandas as pd
9import numpy as np
10from math import sqrt
11import copy
12
13def load_data(path):
14 df = pd.read_excel(path)
15 #去掉不需要的列
16 df = df[df.columns[5:]]
17 #1表示没看过,2~6表示1~5星
18 df.replace([1,2,3,4,5,6],[0,1,2,3,4,5],inplace = True)
19 columns = df.columns
20 df = np.array(df)
21 #测试过程中发现有nan存在,原来是因为有人恶作剧,全填了没看过,导致分母为0
22 #此处要删除全为0的行
23 delete = []
24 for i in range(df.shape[0]):
25 all_0 = (df[i] == [0]*15)
26 flag = False
27 for k in range(15):
28 if all_0[k] == False:
29 flag = True
30 break
31 if flag == False:
32 delete.append(i)
33 print(i)
34 df = np.delete(df,delete,0)
35 return df,columns
36
37
38#定义几种衡量相似度的标准
39#余弦相似度
40def cos(score,your_score):
41 cos = []
42 len1 = 0
43 for i in range(15):
44 len1 += pow(your_score[i],2)
45 len1 = sqrt(len1)
46 for i in range(score.shape[0]):
47 len2 = 0
48 for k in range(15):
49 len2 += pow(score[i][k],2)
50 len2 = sqrt(len2)
51 cos.append(np.dot(your_score,score[i])/(len1*len2))
52 return cos
53
54#欧氏距离
55def euclidean(score,your_score):
56 euclidean = []
57 for i in range(score.shape[0]):
58 dist = 0
59 for k in range(score.shape[1]):
60 dist += pow((score[i][k]-your_score[k]),2)
61 dist = sqrt(dist)
62 euclidean.append(dist)
63 return euclidean
64
65
66#pearson相似度
67#Python有内置函数corrcoef()可以直接计算,不过这里还是手写巩固一下吧~
68def pearson(score,your_score):
69 pearson = []
70 n = score.shape[1]
71 sum_y = 0
72 count = 0
73 #计算目标用户打分的均值
74 for i in range(n):
75 if your_score[i]!=0:
76 count += 1
77 sum_y += your_score[i]
78 mean_y = sum_y/count
79 print('\n')
80 print('你的平均打分为:',mean_y)
81 print('\n')
82 #计算目标用户打分向量的长度
83 len1 = 0
84 for i in range(n):
85 if your_score[i]!=0:
86 your_score[i] -= mean_y
87 len1 += pow(your_score[i],2)
88 len1 = sqrt(len1)
89 #print(len1,mean_y,your_score)
90
91 for i in range(score.shape[0]):
92 #计算其他用户打分的均值
93 # print(i,score[i])
94 count = 0
95 sum_x = 0
96 for k in range(n):
97 if score[i][k]!=0:
98 count += 1
99 sum_x += score[i][k]
100 mean_x = sum_x/count
101 #计算其他用户打分向量的长度
102 len2 = 0
103 for k in range(n):
104 if score[i][k]!=0:
105 score[i][k] -= mean_x
106 len2 += pow(score[i][k],2)
107 len2 = sqrt(len2)
108 #print(len2,mean_x,score[i],'\n','\n')
109 #分母不可为零,不然会产生nan
110 if len2 == 0:
111 pearson.append(0)
112 else:
113 pearson.append(np.dot(your_score,score[i])/len1/len2)
114 return pearson,mean_y
115
116
117#找到相似度最高的用户
118def find_nearest(sim):
119 index = [i for i in range(len(sim))]
120 #index和sim的元组列表
121 sorted_value = list(zip(index,sim))
122 #降序排序
123 sorted_value = sorted(sorted_value,key = lambda x : x[1],reverse = True)
124 return sorted_value
125
126
127#user-based collaborative_filtering
128def collaborative_filtering(score,your_score,movies):
129 #目标用户对15部电影有无看过的bool列表
130 seen1 = np.array([bool(i) for i in your_score])
131 #使用pearson过程中会改变score矩阵的值,需要用deepcopy复制一份
132 score1 = copy.deepcopy(score)
133 #几种相似度的衡量
134 #sim = cos(score,your_score)
135 #sim = euclidean(score,your_score)
136 sim, mean_target= pearson(score1,your_score)
137 #找到最相似的用户
138 sorted_value = find_nearest(sim)
139
140 #找到相似度>0的用户数量
141 count = 0
142 for i in range(score.shape[0]):
143 if sorted_value[i][1]<=0:
144 break
145 else:
146 count += 1
147 #取根值,去掉正相似度中偏低的user
148 count = int(sqrt(count))
149
150 #加权打分 进行推荐
151 print('使用user-based协同过滤进行加权预测打分:')
152 for i in range(score.shape[1]):
153 #如果目标用户没看过
154 if not seen1[i]:
155 #初始化分子分母
156 numerator = denominator = 0
157 for k in range(count):
158 index = sorted_value[k][0]
159 if score[index][i] != 0:
160 numerator += score[index][i]*sorted_value[k][1]
161 denominator += sorted_value[k][1]
162 if not denominator:
163 print(movies[i],'无相关度高的人看过,无法预测得分')
164 elif numerator/denominator > mean_target:
165 print(movies[i],':',numerator/denominator,'推荐观看')
166 else:
167 print(movies[i],":",numerator/denominator,'不推荐观看')
168 return None
169
170
171#梯度下降+隐因子模型
172def latent_factors(score,your_score,movies):
173 #目标用户的打分向量整合进打分矩阵
174 score1 = np.vstack([your_score,score])
175 #打分矩阵的维度
176 n,m = score1.shape[0],score1.shape[1]
177 #隐因子数量设为K
178 K = 15
179 #最大迭代次数
180 max_iteration = 1000
181 #学习速率和正则化因子
182 alpha = 0.01
183 beta = 0.01
184 #LossFunction改变值小于threshold就结束
185 threshold = 0.7
186 #迭代次数
187 count = 0
188 #初始化分解后的矩阵P、Q
189 p = np.random.random([n,K])
190 q = np.random.random([m,K])
191 #全体用户对15部电影有无看过的bool矩阵
192 bool_matrix = [[bool(k) for k in i] for i in score1]
193
194 while True:
195 count += 1
196 #更新P Q矩阵
197 for i in range(n):
198 for j in range(m):
199 if bool_matrix[i][j]:
200 eij = score1[i][j] - np.dot(p[i],q[j])
201 for k in range(K):
202 #同时更新pik和qjk
203 diff=[0,0]
204 diff[0] = p[i][k] + alpha*(2*eij*q[j][k]-beta*p[i][k])
205 diff[1] = q[j][k] + alpha*(2*eij*p[i][k]-beta*q[j][k])
206 p[i][k] = diff[0]
207 q[j][k] = diff[1]
208 #计算误差
209 error = 0
210 for i in range(n):
211 for j in range(m):
212 if bool_matrix[i][j]:
213 error += pow((score1[i][j]-np.dot(p[i],q[j])),2)
214 for k in range(K):
215 error += beta/2*(pow(p[i][k],2)+pow(q[j][k],2))
216 RMSE = sqrt(error/n)
217 print(count,'root_mean_square_error:',RMSE)
218 if RMSE<threshold or count>max_iteration:
219 break
220
221 #打印目标用户没看过的电影
222 seen1 = np.array([bool(i) for i in your_score])
223 print('你没看过的影片有:')
224 for i in range(m):
225 if not seen1[i]:
226 print(movies[i])
227 print('\n')
228
229 #输出梯度下降预测分数
230 predict = np.dot(p[0],q.T)
231 print('使用梯度下降+隐因子模型进行预测打分:')
232 for i in range(m):
233 if not seen1[i]:
234 print(movies[i],':',predict[i])
235 return None
236
237
238def recommend():
239 #请自行修改路径
240 path = r'C:\Users\o\Desktop\请给15部电影打分吧.xls'
241 #原始打分矩阵,电影名称列表
242 score, movies = load_data(path)
243 #必须转换成Float类型,不然会出现分母为0的情况
244 score = score.astype(float)
245 your_score = []
246 #构造目标用户打分向量
247 print('请依次输入你对15部电影的打分(0表示没看过,1~5表示1~5星,以空格分隔):')
248 print(movies)
249 str = input()
250 your_score = np.array([int(i) for i in str.split(' ')]).astype(float)
251 #进行预测
252 latent_factors(score,your_score,movies)
253 collaborative_filtering(score,your_score,movies)
254 return None
255
256
257if __name__=='__main__':
258 recommend()