☰
协同过滤推荐系统实战:用户-物品矩阵构建与优化
2026/10/13 17:22:59 网站建设 项目流程

简介:本资源是一套面向计算机相关专业在校学生与初学者的电影推荐系统课程设计项目,基于协同过滤算法与用户-物品评分矩阵实现个性化推荐,适用于毕业设计、课程设计、期末大作业及推荐系统入门实践。压缩包共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(..., metric='correlation')返回距离,需转为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, metric='correlation') 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, k=20): """ 预测 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}")
关键参数说明:
  • k=20:经验性参数。K 太小(如 5)易受噪声影响;K 太大(如 100)会引入低相似度用户降低精度。课程设计建议从 10 开始测试,在验证集上用 RMSE 选择最优 K;
  • np.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, n=10, k=20): """ 为 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(key=lambda x: x[1], reverse=True) return predictions[:n] # 为用户 123 生成 top-5 推荐 top5 = get_top_n_recommendations(user_item_matrix, user_similarity, 123, n=5) 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, n=10): """为无评分新用户推荐热门电影""" # 统计每部电影的评分次数(非零元素个数) movie_popularity = np.array(user_item_matrix.sum(axis=0)).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, n=5) 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_components=50, random_state=42) 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_components=50:隐因子数。课程设计中 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(nopython=True) def predict_rating_numba(user_ratings, user_similarity_row, movie_idx, k=20): """ 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_size=0.2, random_state=42): """ 构建留一法验证集 :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(n=1, random_state=random_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(f"RMSE: {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, n=10) all_recs.extend([movie_idx for movie_idx, _ in recs]) coverage = calculate_coverage(all_recs, list(movie_to_idx.keys())) print(f"Coverage: {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是否设得太小。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询