一、应用背景
假设现在有一个美食平台,经营着多种不同风味的美食,在系统中维护着一个原始打分表,其中行代表用户,列代表各种菜品在每个用户在对其消费过后进行的打分,分数为1-5分。假如顾客没有消费该商品,则默认是0分。
这次,我们尝试应用奇异值分解(SVD)以及协同过滤算法进行如何基于用户的打分矩阵太对餐品进行向量化的描述,并基于这些打分向量来衡量菜品之间的相似性。再通过顾客对菜品已经有的打分和菜品之间的先死刑来估计出顾客未消费过的菜品可能的打分情况,然后有针对性地进行菜品推荐,以求最大化消费地可能性。
在本次项目中,我个人从身边朋友收集了大约29位对不同菜品的打分评价,原始的打分表如图所示:
首先我们得把Excel中的数据转化为矩阵的形式:
import numpy as np import xlrd def excel_to_Matrix(path): #读excel数据转为矩阵函数 data = xlrd.open_workbook(path) table = data.sheets()[0] #获取excel中第一个sheet表 nrows = table.nrows #行数 ncols = table.ncols #列数 datamatrix = np.zeros((nrows, ncols)) for x in range(ncols): cols = table.col_values(x) cols1 = np.matrix(cols) #把list转换为矩阵进行矩阵操作 datamatrix[:, x] = cols1 #把数据进行存储 return datamatrix print(np.mat(excel_to_Matrix("评分表.xlsx")))
[[4. 2. 2. 5. 1. 4. 5. 1. 1. 3. 3. ] [1. 1. 0. 1. 1. 1. 1. 0. 1. 1. 1. ] [4. 0. 4. 4. 0. 3. 5. 4. 0. 3. 4. ] [3. 0. 2. 4. 0. 3. 5. 0. 2. 2. 3. ] [0. 0. 2. 0. 0. 5. 0. 0. 0. 5. 3. ] [4. 0. 1. 4. 1. 1. 2. 0. 1. 1. 1. ] [0. 0. 0. 4. 0. 0. 0. 0. 3. 0. 5. ] [0. 0. 5. 2. 2. 4. 4. 5. 2. 2. 0. ] [3. 0. 0. 3. 4. 5. 5. 3. 4. 3. 5. ] [3. 5. 5. 3. 5. 2. 4. 3. 1. 2. 5. ] [4. 3. 5. 5. 5. 5. 5. 5. 5. 5. 3. ] [0. 0. 5. 1. 0. 5. 5. 0. 3. 1. 4. ] [4. 0. 4. 4. 1. 4. 5. 4. 5. 2. 4. ] [4. 0. 3. 3.5 0. 4. 4. 0. 0. 2. 4. ] [3. 0. 5. 5. 0. 4. 5. 5. 5. 4. 5. ] [0. 4. 3. 2. 3. 5. 3. 0. 2. 3. 5. ] [5. 0. 4. 5. 0. 4. 4. 5. 3. 3.5 5. ] [4. 0. 4.5 5. 0. 3. 4. 2. 2. 3. 4. ] [3. 3. 5. 4. 4. 5. 5. 4. 5. 1. 5. ] [4. 3. 5. 3. 3. 5. 4. 4. 3. 4. 5. ] [2. 3. 4. 2. 0. 3.5 3.5 1. 1. 1.5 4. ] [2. 0. 3. 3. 3. 3. 5. 2. 2. 4. 5. ] [4. 4. 4. 5. 0. 2. 4. 3. 2. 1. 5. ] [1. 0. 1. 1. 0. 1. 2. 1. 3. 0. 3. ] [3. 0. 5. 3. 3. 5. 3. 3. 2. 3. 5. ] [5. 0. 5. 3. 3. 2. 5. 0. 5. 2. 1. ] [4. 0. 0. 4. 0. 3. 4. 0. 4. 3. 5. ] [2. 0. 5. 4. 5. 1. 5. 2. 3. 2. 0. ]] Process finished with exit code 0
二、整体思路
那么我们主要是推荐什么呢?在本次例子中,我们优先聚焦那些用户没有消费过的菜品(也就是那些用户打0分即没有消费过的菜品),通过模型估计,分析出某个具体用户可能会喜欢的菜品,然后推荐给ta。达到引导最大化消费。
为此,我们需要知道这个用户会有多喜欢某个特定没有消费过的菜品。所以我们可以采用协同过滤的思路,先通过其他用户的评价记录,来衡量出这个菜品和该用户评价过的其他菜品的相似程度,利用该用户对于其他菜品的已评分数和菜品之间的相似程度,估计出该用户会对这个未评分的菜品打出多少分。
这样就可以得到该用户所有未消费过的菜品的估计得分,拿出估计分数最高的菜品推荐给用户就可以了。总结出关键的技术点有以下三点:
(1) 衡量菜品之间的相似性。
(2)评分估计。
(3)稀疏评分矩阵的处理。
三、如何衡量菜品之间的相似性?
两个菜品,我们通过部队用户对其的打分,将其量化成一个分数向量,然后将其量化成分数向量,然后通过对两个菜品的分数向量进行分析比较,定量地进行两个菜品的相似度计算。计算相似度的方法有很多,比如说:欧拉距离、皮尔逊相关系数、余弦相似度等……
本次只是初次接触协同过滤,所以简单只用余弦相似度这何种方法来分析两个菜品的相似度。
对于两个指定向量:向量和,二者的余弦相似度就是用两者夹角 的余弦值来表示,即 。此时余弦相似度取值范围是-1 - 1,为了更直观,我们进行归一化处理。通过将余弦相似度范围划到 0 - 1 之间。越接近1相似度越高。
下面我们取收集到的数据中的一部分案例进行示例:
用户\菜品 叉烧肠粉 新疆手抓饭 四川火锅 粤式叉烧饭 小林 4 2 2 5 邹 1 1 0 1 飞机 4 0 4 4 阿罗 3 0 2 4 每道菜可以用一个四维列向量来进行描述,分别是:
叉烧肠粉 = ,新疆手抓饭 = ,四川火锅 = ,粤式叉烧饭 = 。
下面我们对它们两两之间进行余弦相似度的计算,来分析这四道菜相似度(被喜欢的程度)
import numpy as np sourceData = np.mat([[4,2,2,5], [1,1,0,1], [4,0,4,4], [3,0,2,4],]) def cosSim(vector_1,vector_2): dotProd = float(np.dot(vector_1.T,vector_2)) normProd = np.linalg.norm(vector_1) * np.linalg.norm(vector_2) return 0.5 + 0.5 * (dotProd / normProd) print(cosSim(sourceData[:,0],sourceData[:,1])) #叉烧肠粉与新疆手抓饭相似度 print(cosSim(sourceData[:,0],sourceData[:,2])) #叉烧肠粉与四川火锅相似度 print(cosSim(sourceData[:,0],sourceData[:,3])) #叉烧肠粉与粤式叉烧饭相似度 print(cosSim(sourceData[:,1],sourceData[:,2])) #新疆手抓饭与四川火锅相似度 print(cosSim(sourceData[:,1],sourceData[:,3])) #新疆手抓饭与粤式叉烧饭相似度 print(cosSim(sourceData[:,2],sourceData[:,3])) #四川火锅与粤式叉烧饭
0.8105295017040594 0.972455591261534 0.9963950503147785 0.6825741858350554 0.8229711207330869 0.9556478273060629 Process finished with exit code 0
从结果我们进行下一步分析,发现以下结论:
①我们可以看到第三条数据余弦相似度最高,对应的是叉烧肠粉和粤式叉烧饭,这也符合我们的常识,这两道菜都是有名的粤式菜品。
②接下来比较高的是叉烧肠粉与四川火锅、四川火锅与粤式叉烧饭的余弦相似度比较高,其实这两道菜本来是不同系列的一种菜品,从数据看来我们只能解释为这两种菜品受欢迎程度、受接受程度比较高。
③剩下的就是叉烧肠粉与新疆手抓饭、新疆手抓饭的四川火锅的余弦相似度比较低,不是一系列菜品。
④计算相似度的时候由于有的人没有吃过相应的菜品,打分为0,所以进行余弦相似度计算的结果不是特别准。
四、稀疏数据矩阵的降维处理
我们在计算两道菜品之间的余弦相似度的时候,必须找到同时吃过这两道菜的所有顾客对其的打分。也就是说,参与相似度计算的分数向量的每个元素都尽可能是非零,且来自于几个相同的顾客。
在原始手机到的数据矩阵中,由于不可能每个人吃过平台上的所有菜品,因此这个矩阵一定是一个稀疏矩阵(含有大量的0元素)。因此这个矩阵从表面上看维度会很高,会很臃肿,而且0元素对余弦相似度计算出来的结果不那么具有普遍性。
因此,决定对原始数据矩阵进行降维处理,利用奇异值分解(SVD)对其进行降维,再进行余弦相似度的处理。避免稀疏矩阵对余弦相似度计算的不普遍性造成的影响。代码如下:
sourceData = excel_to_Matrix("评分表.xlsx") U,sigma,VT = np.linalg.svd(sourceData) print(sigma)
[51.78989447 11.21916001 10.19347516 8.82526959 8.24322388 7.57124573 7.06098341 4.68680125 4.63873177 4.01588949 3.14669109] Process finished with exit code 0
从结果上看,我们从大到小取了11个特征值。由于我们这次的目的是压缩,根据奇异值分解中取前k个特征值,取决于至少需要多少个奇异值的平方和才能达到所有平方和的90%。
sigmaSum = 0 for k in range(len(sigma)): sigmaSum = sigmaSum + sigma[k] * sigma[k] if float(sigmaSum) / float(np.sum(sigma ** 2)) > 0.9: print(sigma[:k+1]) break
[51.78989447 11.21916001 10.19347516] Process finished with exit code 0
从结果可以看到,我们仅需要3个奇异值就可以达到主成分贡献率的90%,大大压缩了维度。我们将原始分数矩阵的行从28维压缩到了3维,避免叙述矩阵的情况。
通过奇异值分解的方式对矩阵进行压缩,在行压缩的基础上,推荐算法中通常还需要再乘以奇异值的方阵,赋予其对应的权重值,最终获取降维之后的3 x 11 行压缩矩阵sourceDataRC。
sigma_k = np.mat(np.eye(3) * sigma[:3]) sourceDataRC = sigma_k * U.T[:3,:] * sourceData print(sourceDataRC)
[[ -787.50460515 -300.79982035 -954.04899041 -930.90676527 -472.30212663 -937.00068393 -1088.49457557 -646.861947 -718.10443035 -682.10374914 -1010.68758027] [ -34.81284791 35.65695144 46.74686964 -35.44993223 77.53216152 -1.181489 -1.13704039 35.68781417 3.05482072 -4.24753674 -51.01867736] [ 40.04239755 -35.27812763 3.10207349 34.0081839 2.86052423 -46.75789934 21.38908451 17.92687131 26.04084415 -20.68402537 -51.9925774 ]] Process finished with exit code 0
在后续分析过程中就利用这个sourceDataRC压缩矩阵来进行各个菜品之间的余弦相似度进行计算。
五、如何进行评分估计?
当获取菜品两两之间的余弦相似度之后,就可以基于此进行某顾客没有尝试过的菜品进行评分估计。
思路:利用该顾客评过分的菜品分数,来估计某一个没有评分的菜品分值。
假设需要估计的菜品分值是:,已经苹果分的菜品是,分数分别是。
这三个菜品与的余弦相似度分别是:。
利用相似度加权的方式,来估计的评分值
由此可以估计出该顾客所有没有买过的菜品的评分,然后去估计值最高的某个菜品(或者n个)作为推荐推荐给用户。用这个方法估计一下第26行琦宝,在没有吃过的菜品中最喜欢可能是哪道菜。下面是一个评分估计的函数:
def estimateScore(sourceData,sourceDataRC,userIndex,itemIndex): n = np.shape(sourceData)[1] #获取菜品总数 scoreMutipleCosSimSum = 0 #加权相似度之和 cosSimSum = 0 #itemIndex菜品与其他菜品两两相似度之和 for i in range(n): userScore = sourceData[userIndex,i] if userScore ==0 or i == itemIndex: continue weight = cosSim(sourceData[:,i],sourceDataRC[:,itemIndex]) #利用SVD后的矩阵 #itemIndex与第i个菜品的相似度 cosSimSum = float(cosSimSum + weight) scoreMutipleCosSimSum = scoreMutipleCosSimSum + userScore * weight if cosSimSum == 0 : return 0 return scoreMutipleCosSimSum / cosSimSum
六、查看分析推荐结果
用这个方法估计一下第26行琦宝,在没有吃过的菜品中最喜欢可能是哪道菜。
n = np.shape(sourceData)[1] userIndex = 25 for i in range(n): userScore = sourceData[25,i] if userScore != 0 : continue print("index:{},score:{}".format(i,estimateScore(sourceData,sourceDataRC,userIndex,i)))
index:1,score:3.4429690244554774 index:7,score:3.445806452328425 Process finished with exit code 0
我们可以看到index:1 的新疆手抓饭,跟index:7 的重庆辣子鸡确实是没有吃过的菜品,根据得分index:7 的score:3.445806452328425 要 > index:1 的score:3.4429690244554774
因此我们可以将重庆辣子鸡推荐给琦宝。
从逻辑上讲,琦宝在四川火锅以及剁椒鱼头上打出了5分,看来她比较喜欢口味偏辣的菜品,所以推荐结果也是合理有效的。
七、总结
(1)获取原始用户打分矩阵sourcaData
(2)利用奇异值分解处理sourceData矩阵,得到sourceDataRC
(3)指定第userIndex个用户以及指定第itemIndex个未打分菜品,基于sourceDataRC压缩矩阵的数据,采用余弦相似度算法计算出该菜品与已经有的打分菜品的相似度。
(4)利用公式计算出指定菜品的预估分数。
(5)计算出userIndex用户所有未打分菜品的估值,将估值最高的菜品(或前n个高分)推荐给TA
八、源代码
import numpy as np import xlrd def excel_to_Matrix(path): #读excel数据转为矩阵函数 data = xlrd.open_workbook(path) table = data.sheets()[0] #获取excel中第一个sheet表 nrows = table.nrows #行数 ncols = table.ncols #列数 datamatrix = np.zeros((nrows, ncols)) for x in range(ncols): cols = table.col_values(x) cols1 = np.matrix(cols) #把list转换为矩阵进行矩阵操作 datamatrix[:, x] = cols1 #把数据进行存储 return datamatrix sourceData = excel_to_Matrix("评分表.xlsx") def cosSim(vector_1,vector_2): dotProd = float(np.dot(vector_1.T , vector_2)) normProd = np.linalg.norm(vector_1) * np.linalg.norm(vector_2) return 0.5 + 0.5 * (dotProd / normProd) def estimateScore(sourceData,sourceDataRC,userIndex,itemIndex): n = np.shape(sourceData)[1] #获取菜品总数 scoreMutipleCosSimSum = 0 #加权相似度之和 cosSimSum = 0 #itemIndex菜品与其他菜品两两相似度之和 for i in range(n): userScore = sourceData[userIndex,i] if userScore ==0 or i == itemIndex: continue weight = cosSim(sourceDataRC[:, i],sourceDataRC[:, itemIndex]) #利用SVD后的矩阵 #itemIndex与第i个菜品的相似度 cosSimSum = float(cosSimSum + weight) scoreMutipleCosSimSum = scoreMutipleCosSimSum + userScore * weight if cosSimSum == 0 : return 0 return scoreMutipleCosSimSum / cosSimSum U, sigma, VT = np.linalg.svd(sourceData) sigmaSum = 0 k_num = 0 for k in range(len(sigma)): sigmaSum = sigmaSum + sigma[k] * sigma[k] if float(sigmaSum) / float(np.sum(sigma ** 2)) > 0.9: k_num = k+1 break sigma_k = np.mat(np.eye(3) * sigma[:3]) sourceDataRC = sigma_k * U.T[:3,:] * sourceData n = np.shape(sourceData)[1] userIndex = 25 for i in range(n): userScore = sourceData[25,i] if userScore != 0 : continue print("index:{},score:{}".format(i,estimateScore(sourceData,sourceDataRC,userIndex,i)))