简介这份资源用Python实现了基于物品与基于用户两种协同过滤推荐算法面向推荐系统入门者、进阶学习者以及需要完成课程设计、大作业或毕设项目的同学帮助理解协同过滤的核心思路与代码落地方式。压缩包共4个文件包含2个py脚本分别对应Item_CF与User_CF两套算法实现1个csv数据文件用于承载用户-物品评分数据另有1个gitignore配置项整体约6KB体量轻巧便于快速阅读与调试。目前已有449人学习下载说明其在同类教学资源中具备一定参考热度。读者可借此对照两种算法的相似度计算、邻居选取与评分预测流程理解用户维度与物品维度推荐结果的差异并在此基础上自行扩展数据规模、调整参数或补充评估指标适合作为推荐算法入门练手与项目原型搭建的参考代码。1. 从零手写 UserCF 与 ItemCF为什么我建议你先跑通再谈优化很多人第一次接触推荐系统是从 Python 协同过滤推荐算法开始的。标题里这两个词——基于物品的协同过滤和基于用户的协同过滤——看起来只是把「用户」和「物品」换了个位置实际写起来却是两套完全不同的计算路径和工程取舍。我见过太多人卡在第一步相似度矩阵算出来全是 NaN或者推荐结果永远推自己看过的东西。这篇笔记不讲空泛概念直接带你把 UserCF 和 ItemCF 两条链路在本地跑通从数据格式、相似度计算、评分预测到 TopN 生成每一步都有可复制的代码和参数说明。适合已经会 Python 基础语法、想动手实现推荐算法但不知道从哪下手的读者也适合做过协同过滤但结果不理想、想回头排查细节的人。读完你能得到一套能直接套到自己数据集上的最小实现以及几个我踩过的坑。2. 数据准备与相似度计算UserCF 和 ItemCF 的分岔路口2.1 为什么协同过滤的第一步永远是用户-物品评分矩阵协同过滤的核心假设很朴素相似的人喜欢相似的东西或者相似的东西被人以相似的方式喜欢。不管走哪条路你都需要一张用户-物品评分表。常见的数据格式是三元组用户 ID、物品 ID、评分。MovieLens、Amazon Review 这类公开数据集都是这个结构。我一般先用 pandas 读进来然后 pivot 成矩阵。import pandas as pd import numpy as np # 假设数据有三列user_id, item_id, rating df pd.read_csv(ratings.csv) # 构建用户-物品评分矩阵缺失值填 0 # 注意填 0 意味着未评分不是评分为 0 matrix df.pivot_table( indexuser_id, columnsitem_id, valuesrating ).fillna(0) print(matrix.shape) # (用户数, 物品数) print(matrix.head())这段代码的逻辑很直接pivot_table 把长表转成宽表行是用户列是物品单元格是评分。fillna(0) 把没评分的位置补零。这里有个关键参数选择填 0 还是填均值。填 0 的后果是未评分物品在计算相似度时会被当成评分为 0如果评分范围是 1 到 5这会引入偏差。更稳妥的做法是只在共同评分物品上计算相似度而不是把 0 当真实评分。我后面会讲怎么处理。矩阵规模是第一个要关注的数字。如果用户数一万、物品数千矩阵就是千万级单元格纯 Python 循环会非常慢。所以相似度计算必须用向量化操作或者矩阵运算不能写双重 for 循环。2.2 UserCF 的相似度用户之间的余弦相似度怎么算才不翻车基于用户的协同过滤第一步是算用户之间的相似度。最常用的是余弦相似度把每个用户对所有物品的评分当成一个向量两个用户向量的夹角越小越相似。from sklearn.metrics.pairwise import cosine_similarity # 计算用户之间的余弦相似度 # matrix 的每一行是一个用户的评分向量 user_sim cosine_similarity(matrix) # 转成 DataFrame 方便查表 user_sim_df pd.DataFrame( user_sim, indexmatrix.index, columnsmatrix.index ) # 把自己和自己的相似度设为 0避免推荐时把自己算进去 np.fill_diagonal(user_sim, 0)cosine_similarity 返回的是对称矩阵对角线是 1。np.fill_diagonal 把对角线置零这一步不做的话推荐结果里会出现用户已经评过分的物品因为自己和自己最相似。这是新手最常见的翻车点之一。参数方面cosine_similarity 没有需要调的参数但输入矩阵的质量决定一切。如果评分矩阵非常稀疏比如 95% 以上是 0余弦相似度会被大量零值稀释算出来的相似度普遍偏低且区分度差。这时候可以考虑只保留共同评分数量超过某个阈值的用户对或者改用皮尔逊相关系数。皮尔逊会减去用户平均分能抵消不同用户打分尺度不同的问题——有人习惯打高分有人习惯打低分余弦相似度会把这种尺度差异当成不相似。2.3 ItemCF 的相似度物品向量和用户向量到底差在哪基于物品的协同过滤相似度算的是物品和物品之间。把矩阵转置一下每一行变成一个物品在所有用户上的评分向量再算余弦相似度。# 转置矩阵行变成物品列变成用户 item_matrix matrix.T # 计算物品之间的余弦相似度 item_sim cosine_similarity(item_matrix) item_sim_df pd.DataFrame( item_sim, indexitem_matrix.index, columnsitem_matrix.index ) np.fill_diagonal(item_sim, 0)从代码上看只是转置了一下但工程含义完全不同。UserCF 的相似度矩阵大小是用户数乘用户数ItemCF 是物品数乘物品数。如果用户数远大于物品数ItemCF 的矩阵更小、计算更快、存储更省。这是选型时第一个要看的数字。另一个差异是稳定性。用户兴趣会漂移今天喜欢科幻明天可能看文艺用户相似度矩阵需要频繁更新。物品的属性相对稳定物品相似度矩阵可以离线算好、定期更新。所以工业界很多场景下 ItemCF 比 UserCF 更常用不是因为效果一定更好而是因为维护成本更低。但 ItemCF 也有自己的问题如果物品数量巨大且长尾严重很多物品之间没有共同评分用户相似度算出来全是 0 或接近 0推荐覆盖面会很窄。这时候需要做相似度惩罚或者引入内容特征但那是另一个话题了。3. 评分预测与 TopN 推荐从相似度矩阵到可解释的推荐列表3.1 UserCF 评分预测加权平均里权重怎么定有了用户相似度矩阵预测某个用户对某个未评分物品的评分思路是找到和这个用户最相似的 K 个用户看他们对这个物品的评分用相似度加权平均。def predict_user_cf(user_id, item_id, matrix, user_sim_df, k20): # 找到对该物品有评分的用户 rated_users matrix[item_id][matrix[item_id] 0].index # 排除自己 rated_users rated_users[rated_users ! user_id] if len(rated_users) 0: return 0 # 没人评过无法预测 # 取相似度最高的 k 个用户 sim_scores user_sim_df.loc[user_id, rated_users] top_k_users sim_scores.nlargest(k).index # 加权平均 numerator 0 denominator 0 for u in top_k_users: sim user_sim_df.loc[user_id, u] rating matrix.loc[u, item_id] numerator sim * rating denominator abs(sim) if denominator 0: return 0 return numerator / denominatork 是最关键的参数。k 太小推荐结果受个别邻居影响大噪声敏感k 太大会把不相似的用户也拉进来推荐精度下降。我一般从 10 到 50 之间试用交叉验证看 RMSE 或 MAE。另一个细节是分母用了 abs(sim)因为相似度可能为负直接求和会导致分母抵消。如果相似度都是正数abs 可以去掉。这段代码有个性能问题每次预测都要遍历所有对该物品评分的用户。实际使用时要预计算每个物品的评分用户列表或者用矩阵运算批量预测。3.2 ItemCF 评分预测物品相似度加权的直觉解释ItemCF 的预测逻辑反过来预测用户对某个物品的评分看这个用户评过分的物品中哪些和目标物品相似用物品相似度加权。def predict_item_cf(user_id, item_id, matrix, item_sim_df, k20): # 找到该用户评过分的物品 rated_items matrix.loc[user_id][matrix.loc[user_id] 0].index if len(rated_items) 0: return 0 # 取和目标物品最相似的 k 个物品 sim_scores item_sim_df.loc[item_id, rated_items] top_k_items sim_scores.nlargest(k).index numerator 0 denominator 0 for i in top_k_items: sim item_sim_df.loc[item_id, i] rating matrix.loc[user_id, i] numerator sim * rating denominator abs(sim) if denominator 0: return 0 return numerator / denominator直觉上ItemCF 的预测更可解释因为你喜欢 A而 A 和 B 很像所以推荐 B。UserCF 的解释是和你相似的人喜欢 B所以推荐 B。前者对用户来说更直观后者在社交场景下更有说服力。参数 k 的含义和 UserCF 一样但 ItemCF 的 k 通常可以设小一点因为物品相似度的区分度往往比用户相似度更高。我一般从 5 到 30 之间试。3.3 生成 TopN 推荐列表排序、过滤和冷启动处理预测评分只是中间步骤最终要输出 TopN 推荐列表。流程是对目标用户所有未评分的物品预测评分按分数降序排列取前 N 个。def recommend_top_n(user_id, matrix, sim_df, predict_func, n10, k20): # 找到用户未评分的物品 unrated_items matrix.columns[matrix.loc[user_id] 0] scores [] for item_id in unrated_items: score predict_func(user_id, item_id, matrix, sim_df, k) scores.append((item_id, score)) # 按分数降序排列 scores.sort(keylambda x: x[1], reverseTrue) return scores[:n]这段代码在真实数据集上会非常慢因为要对每个未评分物品调用一次预测函数。优化方向有两个一是用矩阵运算批量计算所有预测分数二是先做候选集筛选只对相似度较高的物品做预测。冷启动是另一个必须面对的问题。新用户没有评分记录UserCF 找不到相似用户ItemCF 找不到评过分的物品两种方法都失效。常见做法是回退到热门推荐或随机推荐等用户产生行为后再切换到协同过滤。新物品同理没有用户评分就无法计算物品相似度需要内容特征或人工运营介入。提示TopN 推荐列表里一定要过滤掉用户已经评过分的物品否则推荐结果毫无意义。这个过滤在 unrated_items 那一步已经做了但如果你从其他路径生成候选集记得手动加过滤。4. 避坑与排查协同过滤实现中最容易翻车的五个地方4.1 相似度矩阵全是零或 NaN现象算出来的相似度矩阵大部分是 0或者出现 NaN推荐结果为空。原因评分矩阵太稀疏两个用户或两个物品之间没有共同评分项余弦相似度分母为零。另外如果矩阵里有全零行或全零列也会导致计算异常。解决先检查矩阵稀疏度用(matrix 0).sum().sum() / matrix.size看比例。如果超过 95%考虑降低相似度计算的维度或者改用基于共同评分项数量的相似度。对全零行列在计算前过滤掉。NaN 可以用np.nan_to_num处理但更好的做法是定位到具体是哪些行列导致的。4.2 推荐结果全是用户已经看过的物品现象TopN 列表里全是用户已经评过分的物品推荐失去意义。原因相似度矩阵对角线没有置零或者预测时没有过滤已评分物品。解决np.fill_diagonal(sim_matrix, 0)必须在计算完相似度后立即执行。生成候选集时用matrix.loc[user_id] 0过滤。如果用了其他召回路径也要在合并结果后统一去重过滤。4.3 预测评分超出合理范围现象预测出来的评分是 7.8、-2.3 这种超出评分区间比如 1 到 5的值。原因加权平均的分子分母计算有误或者相似度出现负值导致分母抵消。解决检查分母是否用了 abs检查相似度是否在 [-1, 1] 范围内。如果评分范围是 1 到 5可以在预测后做截断max(1, min(5, score))。但截断只是掩盖问题根本原因通常是相似度计算或加权逻辑有 bug。4.4 计算速度慢到无法接受现象在几万条评分的数据集上跑一次推荐要几分钟甚至更久。原因用了双重循环逐对计算相似度或者逐物品调用预测函数。解决相似度计算用cosine_similarity或矩阵乘法不要手写循环。预测阶段用矩阵运算批量计算比如scores user_sim_df.loc[user_id].dot(matrix)一次算出所有物品的预测分。如果数据量再大考虑用稀疏矩阵scipy.sparse存储评分矩阵。4.5 UserCF 和 ItemCF 选错了场景现象算法跑通了但效果不好RMSE 很高或者推荐结果不相关。原因没有根据数据特点选型。用户数远大于物品数时用 UserCF相似度矩阵巨大且更新频繁物品数远大于用户数时用 ItemCF物品相似度矩阵稀疏且长尾严重。解决先看数据规模。用户数乘用户数远小于物品数乘物品数优先 UserCF反之优先 ItemCF。再看稳定性需求。用户兴趣变化快的场景ItemCF 更稳物品更新快的场景UserCF 更合适。没有绝对优劣只有场景匹配。5. 进阶技巧用矩阵运算把预测速度提升一个量级前面给的预测函数是逐物品调用的在真实数据集上慢得让人想砸键盘。我后来改成矩阵运算同样的数据量从几分钟降到几秒。核心思路是把加权平均写成矩阵乘法。以 UserCF 为例预测分数矩阵可以近似写成# user_sim_df 是用户相似度矩阵matrix 是评分矩阵 # 分子相似度加权评分和 numerator user_sim_df.dot(matrix) # 分母相似度绝对值之和对每个物品分别求和 # 这里用相似度矩阵的绝对值乘以评分矩阵的二值化版本 binary_matrix (matrix 0).astype(float) denominator np.abs(user_sim_df).dot(binary_matrix) # 避免除以零 denominator[denominator 0] 1 # 预测评分矩阵 pred_matrix numerator / denominator # 过滤已评分物品 pred_matrix[matrix 0] 0 # 取 TopN top_n pred_matrix.loc[user_id].nlargest(10)这段代码的关键在于user_sim_df.dot(matrix)一次性算出所有用户对所有物品的加权评分和np.abs(user_sim_df).dot(binary_matrix)算出对应的权重和。binary_matrix 把有评分的位置标为 1没有评分的位置标为 0这样分母只统计有评分的物品。最后把已评分位置置零再取 TopN。ItemCF 的批量预测同理把 user_sim_df 换成 item_sim_dfmatrix 转置一下即可。矩阵运算的代价是内存占用更高如果矩阵太大可以分块计算。验证方法上我一般用留一法对每个用户随机隐藏一条评分用剩余数据预测看预测值和真实值的 RMSE。RMSE 低于 0.9 通常说明模型基本可用低于 0.8 算不错。但 RMSE 低不代表推荐列表好还要看召回率和覆盖率。召回率衡量推荐列表里有多少是用户真正喜欢的覆盖率衡量推荐系统能覆盖多少物品。两个指标要一起看。我自己的习惯是每次改完参数或逻辑先跑一个小数据集比如 100 个用户、500 个物品验证流程通不通再上全量数据。小数据集上跑通了大数据集上出问题通常是性能和内存不是逻辑。这个习惯帮我省了很多后悔药。希望帮到你。本文还有配套的精品资源点击获取