简介这份资源是面向计算机相关专业在校学生、课程设计或毕业设计阶段的开发者准备的电影推荐系统完整源码基于协同过滤算法与用户推荐矩阵实现适合作为课程设计、期末大作业或初期项目立项的参考案例。压缩包共包含10个文件以data数据文件、py脚本、item与txt说明文档为主整体约21.55MB其中数据文件承载用户评分与相似度矩阵Python脚本负责预处理、相似度计算与推荐逻辑结构清晰便于按模块阅读。目前已有165人学习下载具备一定的参考热度。读者可从中获取一套可运行的推荐系统实现方案理解协同过滤中用户相似度计算、推荐点生成等核心环节并借助预处理脚本与说明文档快速梳理项目流程也可在此基础上修改扩展实现更丰富的推荐功能。1. 电影推荐系统课设协同过滤和用户推荐矩阵到底怎么落地做课程设计最怕选到一个“看起来简单、跑起来全是坑”的题目。电影推荐系统就是典型网上一搜全是“基于协同过滤”的标题真动手时发现用户评分矩阵稀疏得可怜相似度算出来全是零推荐结果要么是热门榜前二十要么干脆报错。这个项目的核心其实就两件事——用协同过滤算法从历史评分里挖出“和你口味相近的人”再用用户推荐矩阵把预测评分组织成可排序的推荐列表。它适合正在做课程设计、需要一套能跑通、能讲清楚、能写进报告的学生也适合想快速理解推荐系统最小闭环的开发者。Python 生态里 numpy、pandas、scikit-learn 足够撑起整个流程不需要上深度学习框架。下面按“数据怎么准备、矩阵怎么建、相似度怎么算、推荐怎么生成、坑怎么避”的顺序拆开讲。2. 用户推荐矩阵的构建从原始评分到可计算的稀疏矩阵2.1 为什么不能直接拿原始评分表做计算原始数据通常是一张长表每行是“某用户对某电影打了某分”字段大概是 userId、movieId、rating、timestamp。这张表直接拿来做协同过滤有两个问题第一计算用户相似度时需要对齐维度长表没法直接做向量运算第二电影数量动辄几千上万如果构建成稠密矩阵内存直接爆炸。常见做法是构建用户-物品评分矩阵行是用户、列是电影、值是评分没有评分的位置填 0 或 NaN。这个矩阵天然稀疏后续计算必须用稀疏矩阵或只对非零位置操作。我一般会先用 pandas 做透视再转成 numpy 数组或 scipy 稀疏矩阵。透视这一步有个细节如果某个用户只看过一部电影他在矩阵里只有一列有值算相似度时和谁都不像这类用户要么过滤掉要么在推荐阶段单独处理。import pandas as pd import numpy as np from scipy.sparse import csr_matrix # 假设 ratings_df 是从 csv 读入的评分数据 # 字段userId, movieId, rating ratings_df pd.read_csv(ratings.csv) # 过滤掉评分次数过少的用户和电影降低稀疏度 user_counts ratings_df[userId].value_counts() movie_counts ratings_df[movieId].value_counts() ratings_df ratings_df[ ratings_df[userId].isin(user_counts[user_counts 5].index) ratings_df[movieId].isin(movie_counts[movie_counts 5].index) ] # 构建用户-电影评分矩阵 user_movie_matrix ratings_df.pivot_table( indexuserId, columnsmovieId, valuesrating ).fillna(0) # 转成稀疏矩阵节省内存 sparse_matrix csr_matrix(user_movie_matrix.values) print(矩阵形状, user_movie_matrix.shape) print(稀疏度, 1 - sparse_matrix.nnz / (sparse_matrix.shape[0] * sparse_matrix.shape[1]))这段代码的关键在过滤阈值。user_counts 5和movie_counts 5是我常用的起步值低于这个数的用户或电影对相似度计算的贡献极小反而增加噪声。pivot_table默认用均值聚合但这里每个用户对每部电影只有一条评分所以不会触发聚合。fillna(0)把缺失评分填成 0注意 0 在评分体系里代表“未评分”不是“打了 0 分”后续计算余弦相似度时 0 不参与点积所以没问题。csr_matrix按行压缩存储适合按用户取行的场景。2.2 评分矩阵的归一化与去偏用户打分习惯差异很大有人习惯打 4 到 5 分有人只打 1 到 3 分。如果不做归一化相似度会被这种个人偏置带偏。常见做法是减去用户平均分把评分变成“高于/低于个人平均”的偏差值。这样两个用户即使绝对分数不同只要相对偏好一致相似度依然高。# 计算每个用户的平均分只对已评分项求平均 user_means np.true_divide( user_movie_matrix.sum(axis1), (user_movie_matrix ! 0).sum(axis1) ).replace([np.inf, -np.inf], 0) # 中心化已评分项减去用户平均分未评分项保持 0 centered_matrix user_movie_matrix.sub(user_means, axis0) centered_matrix[user_movie_matrix 0] 0 centered_sparse csr_matrix(centered_matrix.values)user_means的计算要小心除零如果某个用户所有评分都被过滤掉了分母为 0结果变成 inf。用replace把 inf 和 -inf 替换成 0 是兜底做法。中心化之后未评分位置仍然保持 0这样在余弦相似度计算中不会引入虚假信号。这一步在报告里可以写成“消除用户评分偏置”是加分项。2.3 相似度计算的选型余弦、皮尔逊还是调整余弦协同过滤里用户相似度常用三种余弦相似度、皮尔逊相关系数、调整余弦相似度。余弦相似度直接算两个向量的夹角对未评分项填 0 敏感皮尔逊先减去均值再算相关等价于中心化后的余弦调整余弦则是减去物品平均分。对于电影评分场景我一般用中心化后的余弦也就是先做 2.2 的归一化再算余弦效果比原始余弦稳。from sklearn.metrics.pairwise import cosine_similarity # 计算用户之间的相似度矩阵 user_similarity cosine_similarity(centered_sparse) np.fill_diagonal(user_similarity, 0) # 自己和自己不算相似 # 取某个用户最相似的 K 个邻居 def get_top_k_similar_users(user_index, similarity_matrix, k20): sim_scores list(enumerate(similarity_matrix[user_index])) sim_scores sorted(sim_scores, keylambda x: x[1], reverseTrue) return sim_scores[1:k1] # 跳过自己cosine_similarity接收稀疏矩阵返回稠密相似度矩阵。如果用户数上万这个矩阵会很大可以改用NearestNeighbors或分块计算。np.fill_diagonal把对角线置零避免推荐时把自己算成最相似邻居。get_top_k_similar_users里k20是经验值用户数少可以调到 10用户数多可以到 50但太大反而引入不相关邻居。3. 协同过滤推荐生成从相似用户到电影打分排序3.1 基于用户的协同过滤预测公式拿到 K 个相似用户后预测目标用户对某部未看电影的评分公式是预测分 用户平均分 相似度加权平均的邻居偏差。用数学写就是pred mean_u sum(sim * (rating_neighbor - mean_neighbor)) / sum(sim)。这个公式比直接加权平均原始评分更合理因为它考虑了邻居自己的打分偏置。def predict_rating(user_index, movie_index, user_movie_matrix, user_similarity, user_means, k20): # 找到看过这部电影的相似用户 movie_ratings user_movie_matrix[:, movie_index] rated_users np.where(movie_ratings 0)[0] if len(rated_users) 0: return user_means[user_index] # 没人看过返回用户平均分 # 取相似度最高的 k 个已评分用户 sim_scores [(u, user_similarity[user_index, u]) for u in rated_users] sim_scores sorted(sim_scores, keylambda x: x[1], reverseTrue)[:k] numerator 0.0 denominator 0.0 for neighbor, sim in sim_scores: if sim 0: continue rating user_movie_matrix[neighbor, movie_index] numerator sim * (rating - user_means[neighbor]) denominator abs(sim) if denominator 0: return user_means[user_index] return user_means[user_index] numerator / denominator这段代码有几个关键点。movie_ratings 0用来找看过这部电影的用户因为 0 代表未评分。sim 0的邻居直接跳过负相似度意味着口味相反加权进去会拉低预测质量。denominator用abs(sim)防止负相似度抵消。如果所有邻居相似度都是负的分母为 0回退到用户平均分。这个回退策略在报告里可以写成“冷启动兜底”。3.2 生成 Top-N 推荐列表预测完所有未看电影的评分后按分数排序取前 N 部。注意要排除用户已经看过的电影否则推荐列表里全是看过的用户会觉得系统坏了。def recommend_movies(user_index, user_movie_matrix, user_similarity, user_means, movie_ids, top_n10, k20): num_movies user_movie_matrix.shape[1] watched user_movie_matrix[user_index].nonzero()[1] predictions [] for movie_index in range(num_movies): if movie_index in watched: continue pred predict_rating(user_index, movie_index, user_movie_matrix, user_similarity, user_means, k) predictions.append((movie_index, pred)) predictions.sort(keylambda x: x[1], reverseTrue) top_movies predictions[:top_n] return [(movie_ids[idx], round(score, 2)) for idx, score in top_movies]watched用nonzero取已评分电影的列索引避免推荐已看内容。predictions列表存的是电影索引和预测分排序后取前 N。movie_ids是列索引到真实电影 ID 的映射输出时转回去。这个函数对每个用户都要遍历所有电影用户多的时候会慢可以先用相似度矩阵筛出候选电影集合再预测减少计算量。3.3 评估指标RMSE 和 Hit Rate 怎么算课程设计报告里通常要放评估。评分预测用 RMSE推荐列表用 Hit Rate 或 PrecisionK。RMSE 是预测分和真实分的均方根误差越低越好。Hit Rate 是推荐列表里有多少比例命中了用户实际看过的电影。from sklearn.model_selection import train_test_split # 划分训练集和测试集 train_df, test_df train_test_split(ratings_df, test_size0.2, random_state42) # 在训练集上构建矩阵和相似度在测试集上算 RMSE # ...省略矩阵构建和相似度计算同上 def compute_rmse(test_df, user_movie_matrix, user_similarity, user_means, user_id_to_index, movie_id_to_index): errors [] for _, row in test_df.iterrows(): u_idx user_id_to_index.get(row[userId]) m_idx movie_id_to_index.get(row[movieId]) if u_idx is None or m_idx is None: continue pred predict_rating(u_idx, m_idx, user_movie_matrix, user_similarity, user_means) errors.append((pred - row[rating]) ** 2) return np.sqrt(np.mean(errors))train_test_split按 8:2 划分random_state固定保证可复现。user_id_to_index和movie_id_to_index是 ID 到矩阵索引的映射字典构建矩阵时顺手生成。RMSE 计算时跳过训练集里没出现过的用户或电影否则索引对不上。Hit Rate 的计算类似对每个测试用户生成 Top-N 推荐看测试集里该用户看过的电影有多少落在推荐列表里。4. 避坑与排查协同过滤课设里最容易翻车的五个地方4.1 相似度矩阵全是零或 NaN现象算出来的user_similarity大部分是 0或者出现 NaN推荐结果全是热门电影。原因通常是评分矩阵太稀疏两个用户没有共同评分电影余弦相似度为 0或者某个用户所有评分都被过滤后中心化时除以 0 产生 NaN。解决降低过滤阈值比如从 5 降到 3检查user_means计算时是否有除零用np.nan_to_num兜底如果数据本身太稀疏考虑改用基于物品的协同过滤物品共同评分用户通常比用户共同评分电影多。4.2 推荐列表里全是已经看过的电影现象给用户推荐了 10 部电影用户说“这些我都看过了”。原因是在生成推荐时没有排除已评分电影或者排除逻辑写错了。解决用user_movie_matrix[user_index].nonzero()[1]取已看电影索引在循环里continue跳过。注意nonzero返回的是列索引不是电影 ID别搞混。4.3 预测评分超出 1 到 5 的范围现象预测分出现 6.2 或 -0.5排序时这些异常值排在最前面。原因是加权公式里邻居偏差可能很大加上用户平均分后越界。解决在predict_rating返回前做截断return max(1.0, min(5.0, pred))。这个截断在报告里可以写成“评分边界约束”是工程上常见的后处理。4.4 计算太慢跑一次要十几分钟现象用户数几千、电影数上万时cosine_similarity和逐电影预测耗时很长。原因是相似度矩阵是稠密的预测时对每部电影都遍历所有邻居。解决用NearestNeighbors的kneighbors只算 K 近邻不存完整相似度矩阵预测时只对候选电影集合计算候选集可以取相似用户看过的电影并集而不是全部电影。这样能把计算量降一个数量级。4.5 训练集和测试集划分后索引对不上现象RMSE 计算时报 KeyError或者预测时取到错误的用户。原因是划分后训练集里的用户 ID 和原始矩阵的索引映射不一致。解决先在完整数据上构建user_id_to_index和movie_id_to_index再划分训练测试集或者划分后重新构建映射但测试集里出现训练集没有的用户时直接跳过。我一般用前者映射关系稳定不容易出错。5. 进阶技巧用矩阵分解和混合推荐把效果再拉一档协同过滤跑通之后如果报告想拿高分可以加一个矩阵分解的对比实验。核心思路是把用户-物品评分矩阵分解成两个低秩矩阵的乘积用梯度下降最小化预测误差。Python 里可以用surprise库也可以手写 SGD。手写版本更能体现理解代码量也不大。import numpy as np def matrix_factorization(R, K20, steps500, alpha0.002, beta0.02): R: 评分矩阵K: 隐因子维度steps: 迭代次数 num_users, num_items R.shape P np.random.normal(scale1./K, size(num_users, K)) Q np.random.normal(scale1./K, size(num_items, K)) for step in range(steps): for i in range(num_users): for j in range(num_items): if R[i, j] 0: eij R[i, j] - np.dot(P[i, :], Q[j, :].T) P[i, :] alpha * (2 * eij * Q[j, :] - beta * P[i, :]) Q[j, :] alpha * (2 * eij * P[i, :] - beta * Q[j, :]) # 每 50 步打印一次损失 if step % 50 0: loss 0 for i in range(num_users): for j in range(num_items): if R[i, j] 0: loss (R[i, j] - np.dot(P[i, :], Q[j, :].T)) ** 2 loss beta/2 * (np.linalg.norm(P[i, :])**2 np.linalg.norm(Q[j, :])**2) print(fstep {step}, loss {loss:.2f}) return P, QK20是隐因子维度电影推荐常用 10 到 50太小欠拟合太大过拟合。alpha是学习率beta是正则化系数防止 P 和 Q 数值爆炸。这个实现是朴素 SGD每次只用一个评分更新收敛慢但代码直观。跑通后可以把预测分和协同过滤的预测分做加权融合比如final_score 0.6 * cf_score 0.4 * mf_score通常比单一方法稳。混合推荐在报告里可以写成“多模型融合策略”是加分项。验证方法上除了 RMSE还可以看推荐列表的多样性统计 Top-N 里不同电影类型的数量多样性太低说明系统只推热门。我一般会在推荐后加一个简单的重排把同一类型的电影限制在 3 部以内强制拉开多样性。这个技巧不需要改模型只在输出层做后处理性价比很高。最后说个血泪经验课设代码一定要把随机种子固定住np.random.seed(42)和random_state42都加上。否则每次跑出来的 RMSE 都不一样报告里的数字对不上答辩时被问“为什么两次结果不同”会很被动。另外矩阵分解的 SGD 对学习率很敏感alpha从 0.002 起步跑不通就降到 0.001别一上来就 0.01容易发散。希望帮到你。本文还有配套的精品资源点击获取