简介这是一套面向计算机相关专业在校学生与教师的电影推荐系统项目源码基于协同过滤算法与用户推荐矩阵实现可作为毕业设计、课程设计、期末大作业或初期项目立项的参考方案也适合希望入门推荐算法的初学者进阶学习。压缩包共包含10个文件以data数据文件、py源码脚本、item与txt说明文档为主整体约21.55MB涵盖用户评分数据、相似度计算结果与推荐点数据等核心模块目录结构清晰便于按功能模块检索与二次开发。项目代码均经过实际运行测试功能完整可用读者可借此理解协同过滤的相似度计算、推荐矩阵构建与推荐结果生成流程并在此基础上修改添加实现个性化功能。目前已有165人学习关注适合需要完整推荐系统案例与算法实践素材的读者下载参考。1. 从零手搓一个电影推荐系统协同过滤和用户推荐矩阵到底怎么落地很多同学做课程设计时一看到“推荐系统”四个字就头大觉得必须上深度学习、上大模型才算高级。其实在电影推荐这个场景里协同过滤算法配合用户推荐矩阵是一套非常经典且容易复现的方案。它解决的核心问题是当用户数量成千上万、电影条目也成千上万时如何从稀疏的评分数据里找出“和你口味相似的人喜欢什么”以及“你可能会喜欢哪些没看过的电影”。这套方案适合做课程设计、毕业设计也适合刚入门推荐算法的开发者拿来练手。你不需要 GPU 集群一台普通笔记本就能跑通全流程。接下来我会把数据准备、相似度计算、推荐矩阵构建、冷启动处理这些环节拆开讲每一步都给出可运行的 Python 代码和参数说明。2. 协同过滤的两种路线为什么我最终选了用户推荐矩阵2.1 基于用户和基于物品的协同过滤到底差在哪协同过滤分两大流派UserCF 和 ItemCF。UserCF 的思路是“找到和你相似的用户把他们喜欢的电影推给你”ItemCF 的思路是“找到和你喜欢的电影相似的电影推给你”。两者没有绝对优劣但适用场景不同。UserCF 更适合用户数量少于物品数量的场景比如一个中小型电影平台用户几千人、电影几万部。它的推荐结果更偏向“社交化”容易推出现象级热门影片。ItemCF 则更适合物品数量少于用户数量的场景比如电商平台用户几千万、商品几百万。ItemCF 的推荐结果更稳定因为物品之间的相似度不会频繁变动。在电影推荐课程设计里我一般会选 UserCF 作为主路线原因是电影评分数据集通常用户数远小于电影数UserCF 计算相似度矩阵的维度更小跑起来更快而且用户推荐矩阵这个概念天然就是围绕“用户-用户”相似度展开的。当然如果你拿到的数据集用户数特别大比如百万级那 ItemCF 会更合适。2.2 用户推荐矩阵的数学本质从评分矩阵到相似度矩阵用户推荐矩阵不是凭空造出来的它建立在两个基础矩阵之上用户-物品评分矩阵和用户-用户相似度矩阵。假设有 m 个用户、n 部电影评分矩阵 R 的维度是 m×n其中 R[i][j] 表示用户 i 对电影 j 的评分。这个矩阵通常非常稀疏因为一个用户看过的电影可能只占总量的百分之几。相似度矩阵 S 的维度是 m×mS[i][j] 表示用户 i 和用户 j 的相似度。计算相似度常用的方法有余弦相似度、皮尔逊相关系数、调整余弦相似度。在电影评分场景里皮尔逊相关系数更常用因为它能消除不同用户打分尺度不同的问题——有人习惯打高分有人习惯打低分皮尔逊会把这种偏差归一化。有了相似度矩阵后预测用户 u 对电影 i 的评分公式是pred(u, i) mean(u) Σ(sim(u, v) × (R[v][i] - mean(v))) / Σ|sim(u, v)|其中 v 是那些对电影 i 评过分的用户mean(u) 是用户 u 的平均评分。这个公式的意思是用相似用户的评分偏差来修正目标用户的平均分。2.3 用 Python 构建评分矩阵和相似度矩阵下面这段代码展示了从原始评分数据到相似度矩阵的完整过程。我用的数据集格式是 CSV包含 userId、movieId、rating 三列。import numpy as np import pandas as pd from sklearn.metrics.pairwise import cosine_similarity # 读取评分数据 ratings pd.read_csv(ratings.csv) # 构建用户-电影评分矩阵缺失值填0 # pivot 后行是用户列是电影 rating_matrix ratings.pivot_table( indexuserId, columnsmovieId, valuesrating ).fillna(0) print(f评分矩阵维度: {rating_matrix.shape}) # 输出示例: (6040, 3706) 表示6040个用户3706部电影 # 计算用户之间的余弦相似度 # 注意余弦相似度对稀疏矩阵的零值敏感后面会讲优化 user_similarity cosine_similarity(rating_matrix) print(f相似度矩阵维度: {user_similarity.shape}) # 把相似度矩阵转成 DataFrame方便后续按用户ID索引 user_sim_df pd.DataFrame( user_similarity, indexrating_matrix.index, columnsrating_matrix.index )这段代码的逻辑很直接先用pivot_table把长表转成宽表行是用户、列是电影缺失的评分填 0。然后调用cosine_similarity一次性算出所有用户两两之间的相似度。参数方面fillna(0)是常见做法但要注意填 0 意味着“没看过”和“评分为 0”在数学上等价了这其实不准确。更严谨的做法是保留 NaN在计算相似度时只考虑共同评分的电影。不过对于课程设计来说填 0 加余弦相似度的方案足够跑通而且代码量少、容易理解。如果你想让相似度计算更准确可以把cosine_similarity换成皮尔逊相关系数。pandas 的corr方法默认就是皮尔逊但计算速度比 sklearn 的余弦相似度慢不少。我一般会先用余弦相似度快速验证流程等流程跑通了再换皮尔逊做精细调优。3. 从相似度到推荐结果预测评分和生成 Top-N 列表3.1 预测评分的实现细节与参数选择有了相似度矩阵下一步就是预测某个用户对某部没看过的电影的评分。这里的关键参数是“邻居数量 K”——用多少个最相似的用户来参与预测。K 太小预测结果容易受个别极端用户影响K 太大又会引入不相关的用户稀释推荐精度。我一般会从 K20 开始试然后在验证集上调整。下面是一个预测函数的实现def predict_rating(user_id, movie_id, rating_matrix, user_sim_df, k20): 预测用户对电影的评分 user_id: 目标用户ID movie_id: 目标电影ID rating_matrix: 用户-电影评分矩阵 user_sim_df: 用户相似度矩阵 k: 选取最相似的k个用户 # 如果用户已经评过这部电影直接返回真实评分 if rating_matrix.loc[user_id, movie_id] 0: return rating_matrix.loc[user_id, movie_id] # 找到对这部电影评过分的所有用户 rated_users rating_matrix[rating_matrix[movie_id] 0].index # 从这些用户中找出与目标用户最相似的k个 sim_scores user_sim_df.loc[user_id, rated_users].drop(user_id, errorsignore) top_k_users sim_scores.nlargest(k).index if len(top_k_users) 0: return rating_matrix.loc[user_id].mean() # 冷启动兜底 # 加权平均预测 numerator 0.0 denominator 0.0 user_mean rating_matrix.loc[user_id].replace(0, np.nan).mean() for neighbor in top_k_users: sim user_sim_df.loc[user_id, neighbor] neighbor_mean rating_matrix.loc[neighbor].replace(0, np.nan).mean() neighbor_rating rating_matrix.loc[neighbor, movie_id] numerator sim * (neighbor_rating - neighbor_mean) denominator abs(sim) if denominator 0: return user_mean return user_mean numerator / denominator这个函数里有几个关键点。第一rated_users只取对目标电影评过分的人避免用零值参与计算。第二drop(user_id)把用户自己排除掉否则相似度 1.0 会严重干扰结果。第三user_mean和neighbor_mean用的是用户已评分电影的均值而不是全矩阵均值这样能消除用户打分偏好的影响。第四k20是默认值实际调参时可以在 10 到 50 之间搜索。3.2 生成 Top-N 推荐列表的完整流程预测单个评分只是中间步骤最终要输出的是“给用户推荐哪 10 部电影”。流程是对用户没看过的每一部电影预测评分然后按预测分从高到低排序取前 N 部。def recommend_movies(user_id, rating_matrix, user_sim_df, top_n10, k20): 为用户生成Top-N电影推荐 # 找出用户没看过的电影 user_ratings rating_matrix.loc[user_id] unseen_movies user_ratings[user_ratings 0].index # 对每部没看过的电影预测评分 predictions [] for movie_id in unseen_movies: pred predict_rating(user_id, movie_id, rating_matrix, user_sim_df, k) predictions.append((movie_id, pred)) # 按预测评分降序排列取前N个 predictions.sort(keylambda x: x[1], reverseTrue) top_n_movies predictions[:top_n] return top_n_movies # 示例给用户1推荐10部电影 recommendations recommend_movies(1, rating_matrix, user_sim_df, top_n10, k20) for movie_id, score in recommendations: print(f电影ID: {movie_id}, 预测评分: {score:.2f})这段代码的逻辑是“暴力遍历所有未看过的电影”在电影数量几千部的时候完全可行跑一次大概几秒钟。但如果电影数量到了几万部就需要优化可以先通过相似度矩阵筛选出候选电影集合只对候选集做预测。具体做法是找到与目标用户最相似的 K 个用户把他们评过 4 分以上的电影收集起来作为候选集这样能把预测范围缩小到几百部。参数top_n10是推荐列表长度课程设计里一般要求 10 到 20 部。k20是邻居数量和预测函数保持一致。实际调参时这两个参数要一起调因为 Top-N 的准确率同时受它们影响。3.3 用 RMSE 和 PrecisionK 评估推荐效果推荐系统不能只看“能不能跑”还要看“推得准不准”。常用的评估指标有两个RMSE均方根误差和 PrecisionK前 K 个推荐的准确率。RMSE 衡量的是预测评分和真实评分的偏差越小越好。计算方式是把数据集切成训练集和测试集在训练集上算相似度在测试集上预测评分然后算 RMSE。from sklearn.model_selection import train_test_split from sklearn.metrics import mean_squared_error # 划分训练集和测试集 train_data, test_data train_test_split(ratings, test_size0.2, random_state42) # 用训练集构建评分矩阵和相似度矩阵代码同上略 # ... # 在测试集上评估 true_ratings [] pred_ratings [] for _, row in test_data.iterrows(): user_id row[userId] movie_id row[movieId] true_rating row[rating] # 如果用户或电影不在训练集中跳过 if user_id not in rating_matrix.index or movie_id not in rating_matrix.columns: continue pred predict_rating(user_id, movie_id, rating_matrix, user_sim_df, k20) true_ratings.append(true_rating) pred_ratings.append(pred) rmse np.sqrt(mean_squared_error(true_ratings, pred_ratings)) print(fRMSE: {rmse:.4f})PrecisionK 的计算稍微复杂一点对每个用户生成 Top-K 推荐列表然后看列表里有多少部电影是用户实际评过 4 分以上的。命中数除以 K 就是 PrecisionK。这个指标更贴近实际推荐场景因为用户关心的是“推给我的前几个准不准”而不是所有预测评分的平均误差。我一般会两个指标都看RMSE 用来调相似度算法和 K 值PrecisionK 用来调 Top-N 的 N 值。如果 RMSE 降了但 PrecisionK 没升说明模型在拟合评分尺度但没真正提升推荐质量。4. 避坑指南协同过滤电影推荐里最容易翻车的五个地方4.1 评分矩阵稀疏导致相似度计算失真现象跑出来的相似度矩阵里大量用户之间的相似度接近 0 或 1推荐结果要么全是热门电影要么完全随机。原因电影评分数据通常非常稀疏一个用户可能只看过几十部电影占总量的千分之几。当两个用户共同评分的电影只有一两部时余弦相似度会严重失真——要么因为共同评分太少而算出虚高的相似度要么因为大量零值参与计算而算出虚低的相似度。解决改用皮尔逊相关系数并且设置“共同评分阈值”。只有共同评分的电影数量超过阈值比如 5 部时才计算相似度否则相似度置为 0。代码上可以在相似度矩阵算完后加一层过滤# 计算共同评分数量矩阵 common_count np.zeros_like(user_similarity) for i in range(len(rating_matrix)): for j in range(i1, len(rating_matrix)): common ((rating_matrix.iloc[i] 0) (rating_matrix.iloc[j] 0)).sum() common_count[i][j] common common_count[j][i] common # 共同评分少于5部的相似度置0 user_similarity[common_count 5] 04.2 冷启动用户和冷启动电影的处理盲区现象新用户没有评分记录相似度矩阵里找不到邻居推荐函数直接报错或返回空列表。新电影没人评过分永远不会被推荐出去。原因协同过滤本质上是“用历史行为预测未来行为”没有历史行为就没有预测依据。这是协同过滤的固有缺陷不是代码 bug。解决冷启动用户走“热门推荐”兜底——直接推荐全局评分最高且评分人数最多的电影。冷启动电影走“内容推荐”兜底——根据电影的类型、导演、演员等信息找相似电影推荐给看过类似电影的用户。课程设计里至少要把兜底逻辑写上否则演示的时候新用户一登录就崩了。4.3 相似度矩阵内存溢出与计算超时现象用户数量超过 1 万时相似度矩阵占用内存超过 1GB计算时间超过 10 分钟笔记本风扇狂转。原因相似度矩阵是 m×m 的稠密矩阵m10000 时就是 1 亿个浮点数每个 8 字节总共 800MB。如果 m50000就是 20GB直接爆内存。解决三个方向。第一用稀疏矩阵存储scipy.sparse 可以把内存占用降到十分之一。第二分块计算相似度每次只算 1000 个用户与其他用户的相似度算完存磁盘。第三改用 ItemCF因为电影数量通常远小于用户数量相似度矩阵维度更小。课程设计里如果数据集不大用第一种方案就够了。4.4 推荐结果全是热门电影长尾电影永远出不来现象不管给哪个用户推荐Top-10 列表里总有那几部评分人数最多的电影小众高分电影一次都没出现过。原因相似度加权平均时热门电影因为评分人数多更容易被邻居用户评过分所以在预测阶段就占据了优势。这是协同过滤的“流行度偏差”问题。解决在预测评分后加一个“惩罚项”对热门电影降权。具体做法是统计每部电影的评分人数预测分除以 log(1 评分人数) 的某个幂次。或者更简单粗暴在生成候选集时直接过滤掉评分人数超过总用户数 50% 的电影。课程设计里用后者就够了代码改动小效果立竿见影。4.5 训练集和测试集划分不当导致评估结果虚高现象RMSE 跑出来只有 0.5看起来模型很准但实际推荐结果惨不忍睹。原因如果划分测试集时没有按用户分层可能导致某些用户在训练集里评分很多、在测试集里评分很少模型对这些用户的预测自然准。更严重的是如果测试集里的电影在训练集里也出现过模型相当于“开卷考试”。解决用train_test_split时加stratify参数按用户分层保证每个用户在训练集和测试集里的评分数量比例一致。另外测试集里的电影必须是训练集里出现过的否则预测函数找不到邻居。如果要做严格的“未来预测”评估应该按时间划分——用早期评分做训练用后期评分做测试。5. 进阶技巧让推荐结果从“能跑”到“好用”5.1 用矩阵分解做相似度矩阵的降维替代协同过滤的相似度矩阵在用户数量大时计算代价很高矩阵分解是更高效的替代方案。核心思想是把 m×n 的评分矩阵分解成 m×k 的用户隐向量矩阵和 k×n 的电影隐向量矩阵k 通常取 20 到 100。这样用户相似度可以直接用隐向量算余弦相似度维度从 m 降到 k计算量大幅减少。from scipy.sparse.linalg import svds # 对评分矩阵做SVD分解 # 注意svds要求输入是浮点稀疏矩阵 U, sigma, Vt svds(rating_matrix.values.astype(float), k50) # 用户隐向量矩阵 user_latent U # 维度: m x 50 # 电影隐向量矩阵 movie_latent np.dot(np.diag(sigma), Vt) # 维度: 50 x n # 用隐向量算用户相似度 user_sim_latent cosine_similarity(user_latent)参数 k50 是隐向量维度太小会丢失信息太大会过拟合。我一般会在 20 到 100 之间试看 RMSE 的变化曲线。SVD 的好处是不仅能算相似度还能直接预测评分user_latent movie_latent就是预测评分矩阵。课程设计里如果时间充裕可以把 SVD 作为协同过滤的对比方案展示两种方法的 RMSE 差异。5.2 混合推荐协同过滤加内容特征的简单融合纯协同过滤有个硬伤没法处理新电影。一个简单的改进是融合内容特征——用电影的类型、年份、导演等信息算一个“内容相似度矩阵”然后和协同过滤的相似度矩阵加权平均。# 假设有电影类型矩阵 movie_genre_matrix (n x g)g是类型数量 # 计算电影内容相似度 movie_content_sim cosine_similarity(movie_genre_matrix) # 协同过滤的电影相似度从用户相似度转过来或者直接用ItemCF算 # 这里假设已有 movie_cf_sim # 加权融合 alpha 0.7 # 协同过滤权重 movie_hybrid_sim alpha * movie_cf_sim (1 - alpha) * movie_content_simalpha 的取值需要实验确定我一般从 0.7 开始在验证集上微调。融合后的相似度矩阵既保留了用户行为信息又引入了内容特征对新电影更友好。5.3 一个容易被忽略的细节评分时间衰减用户三年前打的 5 分和昨天打的 5 分意义完全不同。三年前的偏好可能已经变了。加一个时间衰减因子能提升推荐的时效性import math from datetime import datetime def time_decay_weight(rating_time, current_time, half_life_days365): 计算时间衰减权重 half_life_days: 半衰期默认365天 days_diff (current_time - rating_time).days return math.exp(-days_diff * math.log(2) / half_life_days)这个权重可以乘在评分上也可以乘在相似度上。我一般会乘在相似度上因为评分本身有 1 到 5 的尺度限制乘权重后不好解释。乘在相似度上意味着“最近的共同评分比很久以前的共同评分更重要”。半衰期设 365 天是个经验值如果数据集时间跨度短可以设 180 天。5.4 我踩过的一个坑相似度矩阵的对称性陷阱最后说一个我实际踩过的坑。用cosine_similarity算出来的相似度矩阵是对称的S[i][j] 等于 S[j][i]。但在预测评分时我用的是“目标用户与邻居的相似度”如果邻居用户对目标电影的评分是 5 分但目标用户和邻居的相似度只有 0.1这个 5 分对预测结果的影响应该很小。我一开始忘了在加权平均时除以相似度绝对值之和导致预测评分直接爆到 10 分以上。后来加了denominator abs(sim)才正常。这个细节在公式里看起来不起眼但代码里漏掉就是灾难性的。每次改完预测函数我都会拿几个已知评分做回归测试确保预测值在 1 到 5 之间。希望帮到你。本文还有配套的精品资源点击获取