1. import numpy as np
    2. # 历史打分表 (18,11) - 11个样本(11道菜品的历史打分),每个样本有18个特征维度(18个用户)。菜品打分区间为0~5,0代表未消费过
    3. scoreData = np.mat([
    4. [5,2,1,4,0,0,2,4,0,0,0],
    5. [0,0,0,0,0,0,0,0,0,3,0],
    6. [1,0,5,2,0,0,3,0,3,0,1],
    7. [0,5,0,0,4,0,1,0,0,0,0],
    8. [0,0,0,0,0,4,0,0,0,4,0],
    9. [0,0,1,0,0,0,1,0,0,5,0],
    10. [5,0,2,4,2,1,0,3,0,1,0],
    11. [0,4,0,0,5,4,0,0,0,0,5],
    12. [0,0,0,0,0,0,4,0,4,5,0],
    13. [0,0,0,4,0,0,1,5,0,0,0],
    14. [0,0,0,0,4,5,0,0,0,0,3],
    15. [4,3,1,4,0,0,2,4,0,0,0],
    16. [0,1,4,2,2,1,5,0,5,0,0],
    17. [0,0,0,0,0,4,0,0,0,4,0],
    18. [2,5,0,0,4,0,0,0,0,0,0],
    19. [5,0,0,0,0,0,0,4,2,0,0],
    20. [0,2,4,0,4,3,4,0,0,0,0],
    21. [0,3,5,1,0,0,4,1,0,0,0],
    22. ])
    23. print("scoreData:",scoreData.shape)

    image.png

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

    image.png

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

    image.png