协同过滤推荐系统实战:用户-物品矩阵构建与优化

发布时间:2026/9/10 8:51:24
协同过滤推荐系统实战:用户-物品矩阵构建与优化 简介本资源是一套面向计算机相关专业在校学生与初学者的电影推荐系统课程设计项目基于协同过滤算法与用户-物品评分矩阵实现个性化推荐适用于毕业设计、课程设计、期末大作业及推荐系统入门实践。压缩包共10个文件含3个核心Python脚本如相似度计算、推荐逻辑、数据预处理、4个数据文件users_similarity.data、u.data、u.item、movie_recom_point.data等以及README说明文档和辅助数据文件整体21.55MB结构清晰、模块分工明确便于理解推荐流程各环节。已有163人学习下载项目代码经实测可运行功能完整覆盖数据加载、用户/物品相似度计算、Top-N推荐生成等关键步骤附带详细注释与典型数据集支持在此基础上拓展冷启动处理、混合推荐或UI界面开发具备扎实的学习迁移与二次开发价值。1. 为什么用协同过滤用户推荐矩阵做电影推荐比直接调 sklearn 的 fit_predict 更可靠很多刚上手推荐系统的同学会直接pip install scikit-learn然后套用NearestNeighbors或cosine_similarity算完相似度就导出结果——但实际部署时发现冷启动用户推荐全空、热门电影扎堆出现、两个口味截然不同的用户被算出 0.98 的相似度。问题不在代码错而在没把协同过滤的数学约束和用户推荐矩阵的结构设计真正落地。本项目标题里明确点出“基于协同过滤算法和用户推荐矩阵”说明它不是调包演示而是从评分矩阵构建、稀疏性处理、相似度加权、预测值归一化到最终排序的完整链路。适合正在做课程设计、需要答辩展示逻辑闭环、或想把推荐模块嵌入 Django/Flask 后端的同学。核心价值在于你能讲清楚每一行矩阵运算对应什么业务含义比如user_item_matrix[i, j] 4.5是用户 i 给电影 j 打分而user_similarity[i, k] 0.72意味着用户 i 和 k 在 72% 的共评电影上打分趋势一致而不是只说“用了余弦相似度”。2. 构建用户-物品评分矩阵从原始数据到可计算的稀疏二维结构协同过滤的前提是存在用户对物品的显式反馈如电影评分。真实数据常以 CSV 表格形式存在例如ratings.csv包含三列user_id,movie_id,rating。但直接用pandas.read_csv()加载后得到的是长格式long format无法直接参与矩阵运算。必须转换为用户-电影二维稠密/稀疏矩阵这是后续所有计算的底座。2.1 数据清洗与 ID 对齐避免索引越界的关键一步原始数据中user_id和movie_id往往是非连续整数如用户 ID 从 101、205、307 开始若直接用作 NumPy 数组索引会导致内存爆炸。正确做法是重映射为从 0 开始的连续整数import pandas as pd import numpy as np from scipy.sparse import csr_matrix # 加载原始评分数据 df pd.read_csv(ratings.csv) # 对 user_id 和 movie_id 分别做 label encoding生成连续索引 user_to_idx {uid: idx for idx, uid in enumerate(df[user_id].unique())} movie_to_idx {mid: idx for idx, mid in enumerate(df[movie_id].unique())} df[user_idx] df[user_id].map(user_to_idx) df[movie_idx] df[movie_id].map(movie_to_idx) # 过滤掉映射失败的行确保所有 ID 都在字典中 df df.dropna(subset[user_idx, movie_idx]).astype({user_idx: int, movie_idx: int})提示dropna不可省略。若原始数据含缺失 ID 或拼写错误map()会返回NaN后续csr_matrix构造会报ValueError: row index exceeds matrix dimensions。这步排查能提前暴露数据质量问题。2.2 构造稀疏用户-物品矩阵用 csr_matrix 节省内存并加速计算电影推荐场景中用户数常达万级电影数达千级但平均每人只评过 20–50 部电影矩阵稀疏度 95%。用np.zeros((n_users, n_movies))创建稠密矩阵会占用数 GB 内存且大量零值参与计算拖慢速度。scipy.sparse.csr_matrix是工业级首选n_users len(user_to_idx) n_movies len(movie_to_idx) # 构造 CSR 矩阵行用户索引列电影索引值评分 user_item_matrix csr_matrix( (df[rating].values, (df[user_idx].values, df[movie_idx].values)), shape(n_users, n_movies) ) # 验证形状和非零元素数 print(f用户数: {n_users}, 电影数: {n_movies}) print(f总评分条目: {user_item_matrix.nnz}, 稀疏度: {1 - user_item_matrix.nnz / (n_users * n_movies):.3f})参数说明(df[rating].values, (df[user_idx].values, df[movie_idx].values))CSR 构造的三元组明确指定每个非零值的位置和大小shape(n_users, n_movies)强制定义矩阵维度防止因 ID 缺失导致维度错乱nnz属性返回非零元素个数是判断数据覆盖度的核心指标——若nnz n_users * 5说明多数用户评分过少需考虑冷启动策略。2.3 用户相似度矩阵的两种构建路径基于用户向量 vs 基于共现计数协同过滤分User-Based和Item-Based本项目标题强调“用户推荐矩阵”故聚焦 User-Based。关键是如何定义“用户相似度”方法计算逻辑适用场景Python 实现要点余弦相似度cos(θ) (u·v) / (‖u‖·‖v‖)对向量长度不敏感用户评分尺度差异大有人习惯打高分有人严苛必须先对每行做中心化减去用户均分否则相似度被全局偏置扭曲皮尔逊相关系数ρ cov(u,v)/(σ_u·σ_v)本质是中心化后的余弦更鲁棒天然消除用户打分偏差scipy.spatial.distance.pdist(..., metriccorrelation)返回距离需转为1 - distance实际项目中我一般选皮尔逊——因为电影评分天然存在用户偏差A 用户平均打 3.8 分B 用户平均打 4.2 分直接算余弦会误判相似性。代码如下from scipy.spatial.distance import pdist, squareform from sklearn.metrics.pairwise import pairwise_distances # 提取用户向量每行是一个用户的评分向量 user_vectors user_item_matrix.toarray() # 注意仅当内存允许时转稠密否则需分块计算 # 对每行做中心化减去该用户所有有效评分的均值 user_means np.array([row.mean() if row.nnz 0 else 0 for row in user_item_matrix]) # 实际中需用更健壮的中心化见 3.2 节 # 计算皮尔逊相似度矩阵注意pairwise_distances 默认返回距离需转换 user_similarity 1 - pairwise_distances(user_vectors, metriccorrelation) np.fill_diagonal(user_similarity, 0) # 自相似设为 0避免自己推荐自己注意user_item_matrix.toarray()在大数据集上会 OOM。生产环境应改用sklearn.metrics.pairwise.cosine_similarity的稀疏矩阵支持或手动实现分块计算——但课程设计阶段用toarray()更直观且能控制n_users 5000。3. 实现用户协同过滤推荐从相似用户筛选到加权预测有了用户相似度矩阵下一步是为目标用户 u找出最相似的 K 个用户K-Nearest Neighbors再聚合他们的评分预测 u 对未看过的电影的偏好。这不是简单取平均而是加权投票相似度越高其评分权重越大。3.1 目标用户未评分电影的识别布尔索引 矩阵切片假设目标用户索引为target_user 123需找出他没评过分的所有电影# 获取目标用户的评分行稀疏向量 target_ratings user_item_matrix[target_user].toarray().flatten() # 找出所有未评分的电影索引值为 0 或 NaN unrated_mask (target_ratings 0) | np.isnan(target_ratings) unrated_movies np.where(unrated_mask)[0] # 返回未评分电影的列索引数组 print(f用户 {target_user} 共有 {len(unrated_movies)} 部未评分电影)提示此处target_ratings 0依赖原始数据中未评分项填为 0。若原始数据用NaN或空字符串表示未评分需先统一填充如df[rating] df[rating].fillna(0)。课程设计中建议预处理时将缺失评分设为 0并在文档中注明此假设。3.2 相似用户筛选与评分聚合带置信度的加权求和对每个未评分电影j收集所有对 j 有评分的相似用户按其与目标用户的相似度加权平均def predict_rating(user_item_matrix, user_similarity, target_user, movie_idx, k20): 预测 target_user 对 movie_idx 的评分 :param user_item_matrix: CSR 矩阵shape(n_users, n_movies) :param user_similarity: 用户相似度矩阵shape(n_users, n_users) :param target_user: 目标用户索引整数 :param movie_idx: 目标电影索引整数 :param k: 取 top-k 相似用户参与预测 :return: 预测评分float # 获取所有对 movie_idx 有评分的用户索引 rated_by_users user_item_matrix[:, movie_idx].nonzero()[0] # 过滤掉目标用户自己避免数据泄露 rated_by_users rated_by_users[rated_by_users ! target_user] if len(rated_by_users) 0: return 0.0 # 无人评过分无法预测 # 获取这些用户的相似度并取 top-k similarities user_similarity[target_user, rated_by_users] top_k_indices np.argsort(similarities)[-k:][::-1] # 降序取 top-k top_k_users rated_by_users[top_k_indices] top_k_sims similarities[top_k_indices] # 获取这些用户的评分 ratings np.array([user_item_matrix[u, movie_idx] for u in top_k_users]) # 加权平均sum(sim * rating) / sum(|sim|) weighted_sum np.sum(top_k_sims * ratings) sim_sum np.sum(np.abs(top_k_sims)) return weighted_sum / sim_sum if sim_sum ! 0 else 0.0 # 示例预测用户 123 对电影 456 的评分 pred_score predict_rating(user_item_matrix, user_similarity, 123, 456) print(f预测评分: {pred_score:.2f})关键参数说明k20经验性参数。K 太小如 5易受噪声影响K 太大如 100会引入低相似度用户降低精度。课程设计建议从 10 开始测试在验证集上用 RMSE 选择最优 Knp.abs(top_k_sims)相似度可能为负表示口味相反取绝对值保证权重非负符合推荐直觉sim_sum ! 0防御性检查避免除零错误——当所有相似度为 0 时返回 0。3.3 生成 Top-N 推荐列表排序 过滤 截断单个预测值无业务价值需为用户生成可展示的推荐列表def get_top_n_recommendations(user_item_matrix, user_similarity, target_user, n10, k20): 为 target_user 生成 top-n 推荐电影列表 :return: list of tuples (movie_idx, predicted_rating) unrated_movies np.where(user_item_matrix[target_user].toarray().flatten() 0)[0] predictions [] for movie_idx in unrated_movies: pred predict_rating(user_item_matrix, user_similarity, target_user, movie_idx, k) if pred 0: # 只保留正向预测 predictions.append((movie_idx, pred)) # 按预测评分降序排列取 top-n predictions.sort(keylambda x: x[1], reverseTrue) return predictions[:n] # 为用户 123 生成 top-5 推荐 top5 get_top_n_recommendations(user_item_matrix, user_similarity, 123, n5) print(Top-5 推荐电影索引, 预测评分:) for movie_idx, score in top5: print(f 电影 {movie_idx}: {score:.2f})注意user_item_matrix[target_user].toarray().flatten() 0仍依赖“未评分0”的约定。若数据中用np.nan需改为np.isnan(...)。课程设计中保持一致性比追求绝对严谨更重要。4. 用户推荐矩阵的优化处理冷启动、稀疏性与性能瓶颈协同过滤最大的痛点不是算法本身而是现实数据的不完美新用户没评过分冷启动、小众电影没人评长尾问题、相似度矩阵计算慢O(n²) 复杂度。本节给出课程设计可落地的三类优化。4.1 冷启动用户处理用热门电影 分类标签兜底当目标用户target_user完全没评分user_item_matrix[target_user].nnz 0相似度计算失效。此时放弃协同过滤切换为基于内容的启发式推荐def handle_new_user(user_item_matrix, n10): 为无评分新用户推荐热门电影 # 统计每部电影的评分次数非零元素个数 movie_popularity np.array(user_item_matrix.sum(axis0)).flatten() # 获取评分次数最多的 n 部电影索引 top_movies np.argsort(movie_popularity)[-n:][::-1] # 若需更精细可结合电影类型需额外 genres.csv 文件 # 例如加载 genres.csv对 top_movies 取交集优先推荐“动作科幻”类 return top_movies.tolist() # 示例 if user_item_matrix[123].nnz 0: fallback_recs handle_new_user(user_item_matrix, n5) print(f新用户兜底推荐电影索引: {fallback_recs})热搜词关联“协同过滤算法旅游推荐系统” 的冷启动思路与此完全一致——旅游场景中新用户无历史订单就推荐“北京故宫”“杭州西湖”等高热度景点。用户推荐矩阵的扩展性正在于此它不排斥其他信号而是作为主干允许插入规则引擎兜底。4.2 稀疏性缓解用 SVD 降维替代原始相似度计算当用户数 10,000 时计算n_users × n_users相似度矩阵内存超限。解决方案是对用户-物品矩阵做 SVD 分解将用户映射到低维隐语义空间如 50 维再在此空间计算相似度from sklearn.decomposition import TruncatedSVD # 将稀疏矩阵转为适合 SVD 的格式无需 toarray svd TruncatedSVD(n_components50, random_state42) user_latent svd.fit_transform(user_item_matrix) # shape(n_users, 50) # 在隐空间计算余弦相似度快且省内存 from sklearn.metrics.pairwise import cosine_similarity user_similarity_svd cosine_similarity(user_latent)参数说明n_components50隐因子数。课程设计中 20–100 均可用验证集 RMSE 选择TruncatedSVD专为稀疏矩阵设计比PCA更高效此方法本质是 Item-Based CF 的变体但输出仍是用户相似度矩阵无缝接入现有推荐逻辑。4.3 性能瓶颈突破用 Numba 加速预测函数predict_rating函数在循环中反复索引矩阵Python 解释器慢。用numba.jit编译为机器码可提速 5–10 倍from numba import jit import numpy as np jit(nopythonTrue) def predict_rating_numba(user_ratings, user_similarity_row, movie_idx, k20): Numba 加速版输入为 numpy 数组禁止 Python 对象操作 user_ratings: shape(n_users, n_movies) 的稠密数组课程设计可接受 user_similarity_row: 目标用户的相似度行向量 n_users user_ratings.shape[0] rated_users [] ratings [] for u in range(n_users): if user_ratings[u, movie_idx] 0: rated_users.append(u) ratings.append(user_ratings[u, movie_idx]) if len(rated_users) 0: return 0.0 # 构建相似度子集 sims np.array([user_similarity_row[u] for u in rated_users]) ratings_arr np.array(ratings) # 取 top-k k_actual min(k, len(sims)) top_k_idx np.argsort(np.abs(sims))[-k_actual:][::-1] weighted_sum 0.0 sim_sum 0.0 for idx in top_k_idx: weighted_sum sims[idx] * ratings_arr[idx] sim_sum abs(sims[idx]) return weighted_sum / sim_sum if sim_sum ! 0 else 0.0提示Numba 要求输入为numpy.ndarray因此需提前user_item_matrix.toarray()。课程设计数据量小时这是性价比最高的加速方案——不用改架构一行jit注解即生效。5. 验证推荐效果用留一法评估 RMSE 与覆盖率课程设计答辩时光说“推荐了电影”不够要证明推荐质量可量化。最常用指标是 RMSE均方根误差和 Coverage覆盖率二者需在同一验证集上计算。5.1 构建留一法验证集模拟真实场景的预测任务留一法Leave-One-Out是推荐系统标准评估方式对每个用户随机隐藏其一条评分作为测试集其余用于训练。这样既保证每个用户都有测试样本又避免数据泄露from sklearn.model_selection import train_test_split def build_loo_dataset(df, test_size0.2, random_state42): 构建留一法验证集 :return: train_df, test_df (各含 user_id, movie_id, rating) # 对每个用户至少保留 2 条记录才能拆分 user_counts df.groupby(user_id).size() valid_users user_counts[user_counts 2].index df_filtered df[df[user_id].isin(valid_users)] # 按用户分组每组内随机取 1 条作测试 test_samples [] train_samples [] for _, group in df_filtered.groupby(user_id): # 随机选 1 条作测试 test_row group.sample(n1, random_staterandom_state) train_group group.drop(test_row.index) test_samples.append(test_row) train_samples.append(train_group) test_df pd.concat(test_samples) train_df pd.concat(train_samples) return train_df, test_df # 构建验证集 train_df, test_df build_loo_dataset(df) print(f训练集大小: {len(train_df)}, 测试集大小: {len(test_df)})5.2 计算 RMSE衡量预测评分的准确率RMSE 是回归任务黄金指标越小越好def calculate_rmse(predictions, actuals): predictions 和 actuals 是等长 list 或 array return np.sqrt(np.mean((np.array(predictions) - np.array(actuals)) ** 2)) # 在验证集上批量预测 test_predictions [] test_actuals [] for _, row in test_df.iterrows(): user_idx user_to_idx[row[user_id]] movie_idx movie_to_idx[row[movie_id]] actual_rating row[rating] pred_rating predict_rating(user_item_matrix, user_similarity, user_idx, movie_idx) test_predictions.append(pred_rating) test_actuals.append(actual_rating) rmse calculate_rmse(test_predictions, test_actuals) print(fRMSE: {rmse:.4f})5.3 计算 Coverage衡量推荐系统的覆盖能力Coverage 被推荐过的电影数 / 总电影数。值太低说明系统只推热门值太高可能牺牲精度def calculate_coverage(recommended_items, all_movie_ids): recommended_items: 所有用户推荐列表拼接成的 list recommended_set set(recommended_items) return len(recommended_set) / len(all_movie_ids) # 收集所有用户的 top-10 推荐 all_recs [] for user_id in user_to_idx.keys(): user_idx user_to_idx[user_id] recs get_top_n_recommendations(user_item_matrix, user_similarity, user_idx, n10) all_recs.extend([movie_idx for movie_idx, _ in recs]) coverage calculate_coverage(all_recs, list(movie_to_idx.keys())) print(fCoverage: {coverage:.3f})表格典型 RMSE 与 Coverage 参考范围课程设计合理区间数据集规模RMSE越小越好Coverage越高越好说明小型 1k 用户0.8 – 1.20.3 – 0.6MovieLens-100K 常见结果中型1k–5k 用户0.7 – 1.00.4 – 0.7加入 SVD 后 RMSE 可降 0.1–0.15未优化原始实现 1.3 0.2说明相似度计算或中心化有误提示若 RMSE 1.5优先检查用户均分中心化是否执行——这是课程设计中最常见的错误点。Coverage 0.15 则需确认get_top_n_recommendations是否过滤了所有预测分 ≤ 0 的项或k是否设得太小。本文还有配套的精品资源点击获取