
简介这是一套面向计算机相关专业毕业设计场景的Python电影推荐系统完整项目基于协同过滤推荐算法实现适合正在准备毕设、课程设计或期末大作业的学生以及需要项目实战练习的学习者直接参考使用。资源包含项目源码与配套论文说明经过严格调试可运行后作为毕设方案落地。压缩包共约2000个文件整体28.03MB其中1159个py文件构成核心业务与算法逻辑124个html与40个js、16个css搭建前端页面另有148个jpg、11个png等静态素材以及csv、json、xml等数据与配置文件并附带pdf论文文档目录结构完整、模块划分清晰。目前已有218人学习关注。读者可从中获得协同过滤推荐流程的完整实现思路、可复用的源码框架、论文写作参考与项目说明便于快速理解推荐算法在电影场景中的落地方式并在此基础上完成二次开发与答辩准备。1. 从零手写协同过滤一个毕业设计为什么值得你死磕推荐系统很多同学做毕业设计时第一反应是找个现成的电影推荐系统源码改改界面就交差结果答辩时被老师一句“协同过滤的相似度你是怎么算的”问得哑口无言。这个标题背后真正要解决的问题不是“怎么跑起来一个网站”而是“怎么用 Python 把协同过滤推荐算法从数学公式落到可运行的代码并且能写进论文里讲清楚”。它适合两类人一是计算机相关专业、需要一份能拿高分且经得起追问的毕业设计的同学二是刚入门推荐系统、想通过一个完整项目理解召回与排序基本逻辑的开发者。电影推荐系统是协同过滤最经典的落地场景用户对电影的评分矩阵天然稀疏正好能暴露算法的核心难点。接下来我会按“数据怎么来、算法怎么算、系统怎么搭、论文怎么写”的顺序把这条链路完整走一遍。2. 数据准备与评分矩阵构建MovieLens 到底怎么用才不翻车2.1 为什么选 MovieLens 而不是自己爬数据做电影推荐系统的毕业设计数据源的选择直接决定你后面能不能把算法讲透。常见做法是用 MovieLens 数据集它由 GroupLens 实验室维护有 100K、1M、20M 等不同规模。对于毕业设计我一般推荐用 ml-latest-small 或者 ml-100k原因很实际数据量小本地跑得快调试算法时不用等太久同时它包含 ratings.csv、movies.csv、users.csv 三个核心文件字段干净不需要花大量时间做清洗。自己爬豆瓣或者猫眼的数据听起来很酷但你会遇到反爬、字段缺失、评分分布偏斜等问题最后论文里一半篇幅在讲数据清洗反而把协同过滤的核心冲淡了。MovieLens 的 ratings.csv 结构是 userId、movieId、rating、timestamp 四列rating 是 0.5 到 5.0 的浮点数。这里有个容易被忽略的点时间戳字段在基础协同过滤里用不到但如果你想在论文里加一点“时间衰减”的改进它就是现成的素材。movies.csv 里有 title 和 genresgenres 是用竖线分隔的多标签比如 “Action|Adventure|Sci-Fi”这个字段在基于内容的推荐里能派上用场但纯协同过滤阶段可以先放一边。2.2 用 pandas 构建用户-物品评分矩阵协同过滤的第一步是把长表变成矩阵。用户-物品评分矩阵的行是用户列是电影值是评分没有评分的位置就是缺失值。这个矩阵通常非常稀疏MovieLens 100K 的稀疏度大概在 93% 以上也就是说绝大多数格子是空的。理解这一点很重要因为后面算相似度时两个用户共同评过分的电影可能只有几部这就是协同过滤的“黑匣子”难点。import pandas as pd import numpy as np # 读取评分数据指定列名避免把第一行当表头 ratings pd.read_csv(ml-latest-small/ratings.csv) movies pd.read_csv(ml-latest-small/movies.csv) # 构建用户-物品评分矩阵缺失值填 0 方便后续计算 # 注意填 0 只适用于基于内存的相似度计算不代表用户真的打了 0 分 user_item_matrix ratings.pivot_table( indexuserId, columnsmovieId, valuesrating ).fillna(0) print(f矩阵形状: {user_item_matrix.shape}) print(f稀疏度: {1 - (ratings.shape[0] / (user_item_matrix.shape[0] * user_item_matrix.shape[1])):.4f})这段代码的逻辑很直接pivot_table 把长表透视成矩阵fillna(0) 把缺失值补零。参数上唯一需要留意的是 fillna 的值用 0 是因为后续余弦相似度计算时0 表示“没有交互”不会影响向量方向。但如果你用的是皮尔逊相关系数填 0 会引入偏差因为皮尔逊会减去均值0 会被当成真实评分参与计算。所以更稳妥的做法是保留 NaN在计算相似度时只取共同评分项。我一般会在论文里明确写清楚这一点答辩时老师很可能会问“缺失值你怎么处理的”这就是加分项。2.3 训练集与测试集的划分策略毕业设计里常见的翻车点是把所有数据拿去算相似度然后拿同一批数据做推荐最后报告一个高得离谱的准确率。正确的做法是留出法把每个用户的评分按时间排序取最后 20% 作为测试集前 80% 作为训练集。这样能模拟“用历史行为预测未来兴趣”的真实场景。代码上可以用 groupby 加 apply 实现注意要按 timestamp 排序不然随机划分会泄露未来信息。# 按用户分组按时间戳排序取前 80% 做训练 ratings_sorted ratings.sort_values([userId, timestamp]) train_list, test_list [], [] for uid, group in ratings_sorted.groupby(userId): n len(group) split int(n * 0.8) train_list.append(group.iloc[:split]) test_list.append(group.iloc[split:]) train pd.concat(train_list) test pd.concat(test_list) print(f训练集评分数量: {len(train)}, 测试集评分数量: {len(test)})这里有个边界情况如果某个用户只有 1 条评分split 会是 0训练集为空。实际处理时可以过滤掉评分少于 5 条的用户或者在论文里说明这类用户不参与评估。这个细节写进论文的“数据预处理”章节能体现你对数据质量的理解。3. 协同过滤核心算法相似度计算与评分预测的代码落地3.1 用户相似度与物品相似度的选择依据协同过滤分两大方向UserCF 和 ItemCF。UserCF 是“跟你兴趣相似的人还看了什么”ItemCF 是“跟你喜欢的电影相似的电影”。毕业设计里我建议两个都实现然后在论文里对比。选型理由很简单UserCF 更依赖用户群体的规模用户越多越准但电影推荐场景下用户增长慢ItemCF 通常更稳定因为物品的相似度相对静态可以离线算好。MovieLens 这种数据集上ItemCF 的覆盖率往往更好因为热门电影的相似电影容易找冷门电影则两个方法都吃力。相似度计算常用余弦相似度和皮尔逊相关系数。余弦相似度只看向量方向对评分尺度不敏感皮尔逊会减去用户均值能消除“有人习惯打高分、有人习惯打低分”的偏差。实际写代码时我一般用余弦相似度做基线再用皮尔逊做对比实验。注意计算相似度时只考虑共同评分过的物品不要被 fillna(0) 后的矩阵误导否则两个没有共同评分的用户也会因为大量 0 而算出虚高的相似度。3.2 用 numpy 实现余弦相似度矩阵下面这段代码是 UserCF 的核心先基于训练集构建矩阵再算用户之间的余弦相似度。这里没有用 sklearn 的 cosine_similarity而是手写了一遍目的是让你在论文里能讲清楚公式怎么落到代码。from numpy.linalg import norm def cosine_similarity_matrix(matrix): # matrix: 行是用户列是物品值为评分 # 先归一化避免量纲影响 norm_matrix matrix / np.linalg.norm(matrix, axis1, keepdimsTrue) # 处理除零如果某个用户所有评分都是 0范数为 0会产生 NaN norm_matrix np.nan_to_num(norm_matrix) # 余弦相似度 归一化后的矩阵乘以其转置 sim np.dot(norm_matrix, norm_matrix.T) return sim # 基于训练集构建矩阵 train_matrix train.pivot_table(indexuserId, columnsmovieId, valuesrating).fillna(0) user_sim cosine_similarity_matrix(train_matrix.values) print(f用户相似度矩阵形状: {user_sim.shape})逻辑说明np.linalg.norm 按行求 L2 范数keepdimsTrue 保持二维形状方便广播除法。np.nan_to_num 处理全零行避免 NaN 传播。最后矩阵乘法一次性算出所有用户对的相似度比双重循环快得多。参数上axis1 表示按行归一化如果你要做 ItemCF就把矩阵转置让行变成物品。3.3 基于相似用户的评分预测与 Top-N 推荐有了相似度矩阵下一步是预测用户对未评分电影的评分。公式是预测评分 相似用户评分的加权平均权重是相似度。实际写的时候要过滤掉相似度太低或者没有共同评分的用户否则会引入噪声。def predict_rating(user_id, movie_id, user_sim, train_matrix, k20): # 找到对该电影有评分的用户 movie_ratings train_matrix[:, movie_id] rated_users np.where(movie_ratings 0)[0] if len(rated_users) 0: return 0 # 冷启动返回默认值 # 取相似度最高的 k 个用户 sim_scores user_sim[user_id, rated_users] top_k_idx np.argsort(sim_scores)[-k:] top_users rated_users[top_k_idx] top_sims sim_scores[top_k_idx] # 加权平均 if np.sum(top_sims) 0: return 0 pred np.sum(top_sims * movie_ratings[top_users]) / np.sum(top_sims) return pred # 为 userId1 生成 Top-10 推荐 user_id 0 # 注意矩阵索引从 0 开始 scores [] for movie_id in range(train_matrix.shape[1]): if train_matrix[user_id, movie_id] 0: # 只推荐未看过的 pred predict_rating(user_id, movie_id, user_sim, train_matrix.values) scores.append((movie_id, pred)) scores.sort(keylambda x: x[1], reverseTrue) top_n scores[:10] print(Top-10 推荐电影ID:, [m for m, _ in top_n])参数 k 控制参与预测的邻居数量k 太小容易受个别极端评分影响k 太大又会引入不相关用户。我一般会在验证集上试 10、20、40 几个值画一条 RMSE 随 k 变化的曲线论文里放这张图很直观。注意 movie_id 这里是矩阵列索引不是原始 movieId映射关系要提前存好不然推荐出来的 ID 对不上电影名。4. 系统搭建与评估从离线指标到可演示的 Web 界面4.1 离线评估指标RMSE、PrecisionK 与 RecallK毕业设计的论文里必须有量化评估不然老师会认为你只是“跑通了一个 demo”。RMSE 衡量评分预测的误差越小越好PrecisionK 和 RecallK 衡量 Top-N 推荐的质量前者看推荐列表里有多少是用户真正喜欢的后者看用户喜欢的电影有多少被推荐出来了。计算时要注意测试集里的评分需要设一个阈值比如评分大于等于 4 才算“喜欢”。from sklearn.metrics import mean_squared_error import math def evaluate_rmse(test, train_matrix, user_sim, k20): preds, actuals [], [] for _, row in test.iterrows(): uid row[userId] - 1 # 转成矩阵索引 mid row[movieId] if mid not in train_matrix.columns: continue midx list(train_matrix.columns).index(mid) pred predict_rating(uid, midx, user_sim, train_matrix.values, k) if pred 0: preds.append(pred) actuals.append(row[rating]) rmse math.sqrt(mean_squared_error(actuals, preds)) return rmse rmse evaluate_rmse(test, train_matrix, user_sim, k20) print(fRMSE: {rmse:.4f})这段代码的坑在于索引转换userId 从 1 开始矩阵索引从 0 开始movieId 是原始 ID需要映射到列索引。如果测试集里的电影没在训练集出现过直接跳过否则会报 KeyError。RMSE 一般在 0.85 到 1.0 之间算正常如果低于 0.7 就要检查是不是数据泄露了。4.2 用 Flask 搭一个能演示的推荐接口论文写完后答辩现场通常需要演示。用 Flask 写一个简单的接口输入用户 ID返回推荐电影列表足够撑起演示环节。前端可以用最朴素的 HTML 表格重点是后端逻辑清晰。from flask import Flask, request, jsonify app Flask(__name__) app.route(/recommend, methods[GET]) def recommend(): user_id int(request.args.get(user_id, 1)) - 1 scores [] for movie_id in range(train_matrix.shape[1]): if train_matrix.iloc[user_id, movie_id] 0: pred predict_rating(user_id, movie_id, user_sim, train_matrix.values) scores.append((movie_id, pred)) scores.sort(keylambda x: x[1], reverseTrue) top_n scores[:10] # 映射回电影名 movie_ids [train_matrix.columns[m] for m, _ in top_n] titles movies[movies[movieId].isin(movie_ids)][title].tolist() return jsonify({user_id: user_id 1, recommendations: titles}) if __name__ __main__: app.run(debugTrue)启动后访问 /recommend?user_id1 就能看到推荐结果。注意 user_sim 和 train_matrix 要在启动时加载好不要每次请求都重算否则演示时会卡到怀疑人生。这个接口虽然简单但把“数据→算法→服务”的链路串起来了论文的系统设计章节可以画一张架构图把离线计算和在线推荐分开讲。4.3 冷启动与数据稀疏的缓解手段电影推荐系统绕不开冷启动新用户没有评分新电影没有交互。毕业设计里不需要做完整的工业级方案但至少要在论文里提到几种思路。常见做法是新用户注册时让他选几部喜欢的电影用基于内容的推荐过渡新电影则用 genres 字段算内容相似度补充到协同过滤的候选集里。另一种做法是混合推荐把热门电影作为默认推荐兜底。这些不需要全部实现但写进“不足与改进”章节能体现你的思考深度。5. 避坑与排查毕业设计里最容易翻车的 5 个点5.1 现象RMSE 异常低接近 0.3原因训练集和测试集没有按时间划分而是随机划分导致同一用户的评分同时出现在两边模型“见过”测试数据。解决严格按 timestamp 排序后切分确保测试集的时间戳晚于训练集。如果数据集没有时间戳就按用户分组后随机留出但要在论文里说明这是简化处理。5.2 现象推荐结果全是热门电影冷门电影从不出现原因余弦相似度在稀疏矩阵上偏向热门物品因为热门电影被更多用户评分向量更长相似度更容易被算高。解决对热门电影做惩罚比如在相似度分母上加一个与流行度相关的项或者改用皮尔逊相关系数。另一个简单办法是限制每个用户推荐列表里同一类型的电影数量。5.3 现象Flask 接口第一次请求特别慢后面正常原因相似度矩阵和评分矩阵在第一次请求时才初始化或者每次请求都重新计算。解决把矩阵计算放在应用启动时用全局变量存好。如果内存吃紧可以把相似度矩阵存成 npy 文件启动时用 np.load 加载比重新算快得多。5.4 现象测试集里的 movieId 在训练集矩阵里找不到报 KeyError原因训练集和测试集划分后某些电影只在测试集出现。解决在评估循环里加 try-except 或者先判断 movieId 是否在 train_matrix.columns 里不在就跳过。这个细节要在论文的数据预处理部分写清楚说明你处理了冷启动物品。5.5 现象论文里的公式和代码对不上答辩被追问原因论文写的是皮尔逊相关系数代码用的是余弦相似度或者公式里的符号和代码变量名完全脱节。解决写论文时对着代码逐行核对公式里的每一个符号都在代码里有对应变量。我一般会在论文附录里贴关键代码片段并在正文里标注“对应代码第 X 行”这样老师翻到代码时能直接对上。6. 论文写作与答辩加分项把协同过滤讲成自己的东西6.1 论文框架怎么搭才不像模板很多毕业设计论文的目录一眼就能看出是套模板绪论、相关技术、需求分析、系统设计、系统实现、测试、总结。要拿高分得在“相关技术”和“系统实现”之间加一章“算法设计与实验”把协同过滤的选型、公式推导、参数调优、对比实验单独成章。这一章是你自己的东西老师也最愿意看。具体结构可以是问题定义→算法选择理由→相似度计算→评分预测→实验设置→结果分析。实验部分至少放三张图RMSE 随邻居数 k 的变化、UserCF 和 ItemCF 的 Precision10 对比、不同相似度度量下的 Recall10 对比。6.2 答辩时怎么解释“协同过滤的黑匣子”老师常问的一个问题是“你为什么觉得这个推荐结果是合理的”不要只回答“因为相似度高”。可以这样组织先说明相似度衡量的是用户评分向量的方向一致性再举一个具体例子比如用户 A 和用户 B 都对《星球大战》和《帝国反击战》打了高分余弦相似度接近 1所以把 B 喜欢的《绝地归来》推荐给 A。如果老师追问“为什么不用深度学习”就回答毕业设计的篇幅和算力有限协同过滤的可解释性更强能逐行讲清楚每个评分怎么来的深度学习调参和训练成本高反而不好在论文里展开。这个回答既诚实又体现了你对边界条件的认知。6.3 一个能写进论文的改进技巧加权相似度基础协同过滤把所有共同评分一视同仁但实际上两个用户共同评分的电影数量越多相似度越可信。可以在相似度上乘一个置信度权重比如共同评分数量除以一个常数或者用 sigmoid 映射。代码改动很小在 cosine_similarity_matrix 返回后加一步# 计算共同评分数量矩阵 binary_matrix (train_matrix.values 0).astype(float) common_count np.dot(binary_matrix, binary_matrix.T) # 置信度权重共同评分越多权重越接近 1 confidence 1 - np.exp(-common_count / 10) user_sim_weighted user_sim * confidence这个改进在论文里可以作为“基于置信度加权的协同过滤”单独一节实验部分对比加权前后的 RMSE通常能有 0.02 到 0.05 的提升。提升不大但足以说明你做了优化而且代码量小不会引入新的 bug。6.4 我踩过的坑与最后建议我当年做类似项目时最大的教训是过早追求“系统好看”花了一周调前端样式结果算法部分只用了最简单的余弦相似度答辩时被问“皮尔逊和余弦的区别”直接卡住。后来我重新分配时间算法和实验占 60%系统实现占 30%前端占 10%。论文里的每一张图、每一个表格都要能回答“这个数字是怎么来的”。如果你现在还在纠结选什么题目我的习惯是先把核心算法的代码写出来跑通一个最小闭环再往外扩系统。这样即使时间不够算法部分也是完整的。希望帮到你。本文还有配套的精品资源点击获取