import numpy as np# 历史打分表 (18,11) - 11个样本(11道菜品的历史打分),每个样本有18个特征维度(18个用户)。菜品打分区间为0~5,0代表未消费过scoreData = np.mat([ [5,2,1,4,0,0,2,4,0,0,0], [0,0,0,0,0,0,0,0,0,3,0], [1,0,5,2,0,0,3,0,3,0,1], [0,5,0,0,4,0,1,0,0,0,0], [0,0,0,0,0,4,0,0,0,4,0], [0,0,1,0,0,0,1,0,0,5,0], [5,0,2,4,2,1,0,3,0,1,0], [0,4,0,0,5,4,0,0,0,0,5], [0,0,0,0,0,0,4,0,4,5,0], [0,0,0,4,0,0,1,5,0,0,0], [0,0,0,0,4,5,0,0,0,0,3], [4,3,1,4,0,0,2,4,0,0,0], [0,1,4,2,2,1,5,0,5,0,0], [0,0,0,0,0,4,0,0,0,4,0], [2,5,0,0,4,0,0,0,0,0,0], [5,0,0,0,0,0,0,4,2,0,0], [0,2,4,0,4,3,4,0,0,0,0], [0,3,5,1,0,0,4,1,0,0,0],])print("scoreData:",scoreData.shape)

# 关键1: 如何衡量菜品之间的相似性。 # 相似度的方法有很多,如欧式距离、皮尔逊相关系数、余弦相似度等。def cosSim(v1,v2): """ 基于余弦相似度,即两个向量之间的夹角的余弦:cos𝜃,另外进行归一化处理后为:0.5+0.5cos𝜃,相似度取值范围是0~1。 v1,v2 是两种菜品的不同用户的打分矩阵,均为(-1,1)的尺寸。 """ cos_theta = float(v1.T@v2)/(np.linalg.norm(v1)* np.linalg.norm(v2)) return 0.5 + 0.5*cos_theta
# 关键2:稀疏数据矩阵的降维处理,以有助于衡量菜品之间的相似度# 原始数据有很多零,原因是很多人没吃过那么多菜,这里可以用svd对行降维,类似于“去掉一些信息量比较小的人,# 使得剩下的人都是吃过比较多的菜的人”,如此便有助于衡量菜品之间的相似度。U,Sigma,VT = np.linalg.svd(scoreData)print("U:",U.shape)print("Sigma:",Sigma.shape)print("VT:",VT.shape)# 为了确定选择多少个最大的奇异值进行空间压缩,提出了主成分贡献率的概念:人为选择的奇异值的平方和能达到所有奇异值的平方和的90%def find_k_for_PC_contribution_rate(Sigma,rate): """ Sigma: 从大到小排列的所有奇异值的列表 rate: 需要达到的主成分贡献率 返回:k,表示选择最大的k个奇异值。 """ pc_contri = 0. all_contri = np.sum(np.array(Sigma)**2) for k in range(0,len(Sigma)): pc_contri += Sigma[k]**2 if pc_contri / all_contri > rate: k = k+1 return kk = find_k_for_PC_contribution_rate(Sigma,0.9)print("k:",k) # 拿到目标奇异值矩阵,并进行行压缩。注意:推荐算法中,通常还需要对行乘以对应的奇异值,给予权重,即乘以奇异值方阵。U_k = U[:,:k] # 选择最大k个奇异值对应的特征向量(列向量)scoreDataRC = U_k.T@scoreDataSigma_k = np.diag(Sigma[:6])scoreDataRC = Sigma_k@scoreDataRCprint("U_k:",U_k.shape)print("Sigma_k",Sigma_k.shape)print("socoreData -> scoreDataRC:",scoreData.shape,"->",scoreDataRC.shape) # 行降维

# 关键3:评分估计。 # 基本思想:利用该顾客已评分的菜品分值,来估计某个未评分的菜品的分值。# score = np.sum([userScore_list[i]*sim_list[i] for i in range(len(sim_list))]) / np.sum(sim_list)def estScore(scoreData,scoreDataRC,userIndex,itemIndex): """ 函数作用:估计指定用户对未打分的指定菜品的打分 scoreData: 原始的用户打分表 scoreDataRC: 对scoreData的行降维,类似于去掉一些信息量少的人。用于计算菜品相似度。 userIndex:指定某用户 itemIndex: 指定某菜品(该菜品未被指定用户打分) """ n = np.shape(scoreData)[1] # 菜品数 sim_list = [] userScore_list = [] simSumScore = 0. for i in range(n): # 遍历菜品,对”指定用户打过分的菜品“与“为指定用户估分的菜品”进行相似度计算 userScore = scoreData[userIndex,i] if userScore == 0 or i == itemIndex : continue userScore_list.append(userScore) # 计算:”不是为指定用户估分的菜品i“ 与 “为指定用户估分的菜品itemIndex” 之间的相似度 sim = cosSim(scoreDataRC[:,i],scoreDataRC[:,itemIndex]) sim_list.append(sim) if np.sum(sim_list) == 0: return 0 # 评分估计的公式 score = np.sum([userScore_list[i]*sim_list[i] for i in range(len(sim_list))]) / np.sum(sim_list) return score
# 应用环节:对17号用户进行推荐菜品userIndex = 17index_list = []score_list = []for itemIndex in range(np.shape(scoreData)[1]): # 遍历所有菜品, 但忽略指定用户已打分的菜品 if scoreData[userIndex,itemIndex] != 0: continue index_list.append(itemIndex) score_list.append(estScore(scoreData,scoreDataRC,userIndex,itemIndex))print("\n该用户的以下菜品的估分如下:")for index,score in zip(index_list,score_list): print("index",index,"score:",score)print("最推荐该用户去尝试的菜品是:",index_list[np.argmax(score_list)])
