
简介一套完整的基于Python的电影推荐系统项目集成物品协同过滤、用户协同过滤与基于内容的推荐三种主流算法适用于毕业设计、课程设计或推荐系统入门学习。资源包共2000个文件压缩后约257MB主体为1977张电影相关jpg图片、11个Python源码文件及5个HTML页面另含少量pyc、txt、css与js文件代码与前端展示结构清晰。已有112人学习下载项目涵盖数据预处理、特征提取、模型训练、评价方法等完整流程并通过前端页面提供交互式电影推荐展示。推荐系统在电商、影视等领域应用广泛该项目以电影数据为切入点演示了如何通过用户-物品评分矩阵计算物品相似度如何利用KNN寻找相似用户以及如何基于影片特征构建内容画像并针对冷启动问题提供了可参考的实现思路。学习者可对照源码理解SVD矩阵分解、特征工程等核心实现同时借助图片数据集和HTML界面快速跑通演示便于后续优化与毕业设计答辩展示。1. 从毕设答辩现场聊起这套电影推荐系统到底能给你什么如果你准备拿推荐系统做毕业设计大概率会被问到同一个问题“你用了什么算法为什么选它效果怎么验证” 网上能搜到的 Python 推荐系统源码不少但多数要么只有一种协同过滤要么代码和前端页面完全脱节答辩时根本没法演示。这篇笔记拆的是一套完整的电影推荐系统项目包含用户协同过滤、物品协同过滤和基于内容的推荐三种算法同时带完整的前端页面直接能跑起来做演示。它对两类人最有用一是拿来做毕设、需要完整展示“算法系统”的学生二是刚入行推荐系统、想对照源码搞懂三种算法实现差异的从业者。2. 整体架构与数据层先看工程目录再搞定评分矩阵拿到压缩包之后先别急着跑python main.py——我已经替你先趟过一遍了这套项目的前端页面是完整的 HTML 文件后端算法的核心逻辑才是重点。下面先把目录结构和数据流理清楚再讲怎么把评分数据变成算法能吃的稀疏矩阵。2.1 工程文件拆解前端页面与算法模块的对应关系解压之后你能看到 style.css、evaluate.html、history.html、detail.html、recommend.html、index.html 以及四张电影封面图。这不是随便堆在一起的文件每个文件名都有明确的职责划分。index.html 是系统入口负责登录和主展示逻辑recommend.html 承载推荐结果展示是答辩时最常被点到“演示一下”的页面history.html 记录用户的历史评分和浏览行为detail.html 展示单部电影的详情页evaluate.html 是评估页面用来展示算法准确率等指标。四张 jpg 图片则是电影封面资源对应推荐列表里的视觉元素。算法模块在项目里以 Python 文件形式存在。一个常见的工程化组织方式是数据加载模块读入评分文件预处理模块做去重、格式转换然后是三个独立的算法文件实现三种推荐逻辑最后是后端路由把算法结果与前端页面串联。下面给出一个标准化的加载方式import pandas as pd import numpy as np from scipy.sparse import csr_matrix # 常见的加载逻辑电影评分数据通常包含 userId, movieId, rating 三列 def load_ratings(file_path: str) - pd.DataFrame: df pd.read_csv(file_path, sep::, headerNone, names[userId, movieId, rating, timestamp], enginepython) # 去掉时间戳平时用不到留着还会增加内存 df df.drop(columns[timestamp]) # 转成稀疏矩阵防止内存被大量零值撑爆 users df[userId].astype(category) movies df[movieId].astype(category) rating_matrix csr_matrix((df[rating], (users.cat.codes, movies.cat.codes))) return df, rating_matrix, users, movies这段代码有两个关键点。第一用astype(category)把用户 ID 和电影 ID 转成连续的整数编码推荐系统里常见的坑是 ID 本身有空洞——比如用户 ID 是 1、2、5、100直接当矩阵下标就会产生巨大的稀疏浪费。第二用scipy.sparse.csr_matrix存储评分矩阵因为 MovieLens 这类数据集的评分密度通常只有百分之几稠密矩阵根本存不下。参数层面的建议如果你自己的数据量不大比如只有几千条评分直接用稠密的 DataFrame 透视表也行不必强行上稀疏矩阵。但如果你准备用 MovieLens 1M 数据集稀疏矩阵是必须的否则内存直接爆给你看。2.2 评分数据预处理归一化、去偏与训练集划分原始评分数据不能直接喂给协同过滤算法这一步做不好后续所有算法效果都会受影响。首先要处理的是用户评分偏差——有些人天生手松全都打 4 分以上有些人手紧最高给 3 分。如果不做归一化用户间的评分差异会被算法误读为偏好差异。from sklearn.model_selection import train_test_split def preprocess_ratings(df: pd.DataFrame) - pd.DataFrame: # 计算每个用户的平均评分用于去偏 user_mean df.groupby(userId)[rating].transform(mean) df[rating_adj] df[rating] - user_mean # 划分训练集和测试集分层抽样保证用户覆盖 train, test train_test_split(df, test_size0.2, stratifydf[userId], random_state42) return train, test, user_mean这里的核心逻辑是“去偏”把原始评分减去用户自己的平均分得到的是“相对偏好”而不是“绝对评分”。一个用户给 5 分可能只代表“还行”另一个用户给 3 分可能已经是“强烈推荐”去偏之后两者才可比。stratifydf[userId]保证训练集和测试集里都有每个用户的数据避免某些用户完全不存在于训练集中。注意训练集和测试集的划分方式会直接影响评估结果。如果你只是做演示按比例切分即可如果要做严肃的离线评估建议用时间序列切分——按时间戳取前 80% 做训练后 20% 做测试因为推荐系统的实际场景永远是预测用户未来的行为而不是回填历史。常见做法里random_state42固定随机种子是为了实验结果可复现答辩时老师如果追问这一点能加分。3. 物品协同过滤实现相似度计算与 SVD 降维的配合物品协同过滤ItemCF的核心思想是给用户推荐“和他喜欢过的物品相似的其他物品”。它的优势在于物品之间的相似度相对稳定可以离线计算、在线推荐所以工程上使用频率很高。这一章讲清楚两个关键环节物品相似度怎么算、矩阵太大时怎么用 SVD 降维。3.1 构建物品相似度矩阵三种相似度指标的选型物品协同过滤的第一步是把用户-物品评分矩阵转置成物品-用户矩阵然后逐对计算物品之间的相似度。源代码里常见的实现方式是余弦相似度和皮尔逊相关系数下面给出一个可以对照的 Python 实现from sklearn.metrics.pairwise import cosine_similarity def compute_item_similarity(rating_matrix): # rating_matrix 是 用户×物品 的稀疏矩阵 # 转置后变成 物品×用户计算每两件物品的余弦相似度 item_matrix rating_matrix.T.astype(np.float64) # 余弦相似度只关心方向一致性不关心绝对值大小 item_sim cosine_similarity(item_matrix) # 把自身相似度置零避免推荐时出现“自己推荐自己” np.fill_diagonal(item_sim, 0) return item_sim这里有个容易被忽略的细节np.fill_diagonal(item_sim, 0)是必须的。如果不把对角线上的 1 置零后面做 Top-N 推荐时相似度最高的永远是物品自己推荐列表就会被刷屏。选相似度指标时我的习惯是如果评分数据已做过去偏优先用皮尔逊相关系数因为它对用户评分尺度的差异有天然的容忍力如果数据极其稀疏余弦相似度更稳定因为杰卡德系数对评分值完全不敏感只适合处理 0/1 行为数据比如是否购买。下表是对比相似度指标适合场景数据要求注意点余弦相似度评分数据较完整评分值可比较受评分尺度影响皮尔逊相关系数用户评分偏差明显已做去偏或足够长向量冷门物品相似度波动大杰卡德系数购买/收藏行为0/1 表示无法利用评分深度信息3.2 SVD 降维与近似计算当物品数量大到算不动时当物品数量从几千涨到几十万直接计算两两相似度的复杂度是 O(n²)这个量级在单机上无法落地。常见的解决方案是借助 SVD 把用户-物品矩阵压缩成低维稠密矩阵再在压缩后的空间里计算相似度。from scipy.sparse.linalg import svds def svd_item_similarity(rating_matrix, num_components20): # 注意SVD 对缺失值敏感这里先用平均评分填充 0 matrix_dense rating_matrix.toarray() matrix_dense[matrix_dense 0] matrix_dense.mean() # 对填充后的矩阵做截断 SVDk 是降维维度 u, sigma, vt svds(matrix_dense, knum_components) # 用降维后的矩阵重建物品向量再算相似度 item_features np.dot(np.diag(sigma), vt) # 物品 的隐因子表示 item_sim cosine_similarity(item_features) np.fill_diagonal(item_sim, 0) return item_simSVD 在推荐系统里的作用可以理解为隐因子分析把“用户对物品的评分”拆解成“用户偏好”与“物品属性”的内积。num_components的选择值得多说一句设得太小隐因子数量不足以表达用户偏好推荐结果会过于粗糙设得太大计算量上升且容易过拟合。一般从 20 开始调MovieLens 1M 级别数据调到 50 左右通常效果不错。实际调参时看指标变化最可靠后面第 5 章会讲评估方法。3.3 ItemCF 推荐生成Top-N 输出与参数选择物品相似度算完之后推荐的生成逻辑就很直接了——找到用户历史评分过的物品每条历史物品找出相似物品加权汇总后取 Top-Ndef itemcf_recommend(user_ratings, item_sim, top_n10): # user_ratings: {movie_id: rating} 字典 # item_sim: 物品相似度矩阵 score {} for movie_id, rating in user_ratings.items(): sim_scores item_sim[movie_id] for sim_item, sim_value in enumerate(sim_scores): if sim_value 0 or sim_item movie_id: continue # 关键点用用户评分加权相似度喜欢的物品贡献更大 score[sim_item] score.get(sim_item, 0) sim_value * rating # 按汇总得分排序取前 N top_items sorted(score.items(), keylambda x: x[1], reverseTrue)[:top_n] return [item_id for item_id, _ in top_items]sim_value * rating这个加权方式值得展开如果用户给了一步电影 5 分那它的相似物品会被大幅抬升如果只给了 2 分相似物品虽然仍然可能被推荐但权重小得多。这比简单地“把所有看过电影的相似物品都推出来”更符合直觉。还有一种变体是把用户评分归一化后再加权能避免评分膨胀。4. 用户协同过滤与基于内容的推荐两种思路的完整落地如果说物品协同过滤是在“找相似的东西”用户协同过滤就是“找相似的人”。这一章把算法本身和常见落地操作一起讲完同时给出基于内容推荐的完整实现最后用一张侧面对比表把它们放到一个坐标系里。4.1 UserCFKNN 找邻居 评分预测公式用户协同过滤的核心步骤是三步计算用户之间的相似度找到目标用户的 K 个最近邻聚合邻居的评分来预测目标用户对未看物品的评分。from sklearn.neighbors import NearestNeighbors def usercf_predict(user_id, item_id, rating_matrix, k30): # 对评分矩阵转置每行代表一个用户的评分向量 user_vectors rating_matrix.toarray() # 训练 KNN 模型用余弦距离找最近邻 knn NearestNeighbors(n_neighborsk, metriccosine) knn.fit(user_vectors) # 找到目标用户的邻居索引和距离 distances, indices knn.kneighbors(user_vectors[user_id].reshape(1, -1)) # 加权投票邻居的评分按相似度加权汇总 neighbors_ratings user_vectors[indices[0]][:, item_id] neighbor_sims 1 - distances[0] # 余弦距离转相似度 # 只使用对目标物品评过分 的邻居 mask neighbors_ratings 0 if mask.sum() 0: return 0 # 所有邻居都没看过这个电影 score np.dot(neighbors_ratings[mask], neighbor_sims[mask]) / neighbor_sims[mask].sum() return score这里有个关键决策点K 值的选取。K 太小邻居数量不足以产生稳定的统计意义K 太大会把相似度很低的用户也纳进来稀释推荐精度。通常从 20-50 起步用验证集上的 RMSE 或 MAE 来选。另外注意代码里的mask neighbors_ratings 0这个判断在数据稀疏时尤其重要——如果不过滤均值会被一堆 0 向下拉预测值严重偏低。4.2 基于内容推荐特征工程与 TF-IDF 电影简介建模基于内容的推荐Content-Based不依赖用户之间的行为而是直接分析电影自身的属性。最简单的实现方式是提取电影类型、导演、演员等特征构成特征向量然后计算电影之间的相似度。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import linear_kernel def build_content_profile(movie_df): # movie_df 包含 movieId、title、genres、director、actors 等列 # 把多列特征合并成一行文本用空格分隔 def combine_features(row): return f{row[genres]} {row[director]} {row[actors]} movie_df[features] movie_df.apply(combine_features, axis1) # TF-IDF 向量化把文本转成权重向量 tfidf TfidfVectorizer(stop_wordsenglish) tfidf_matrix tfidf.fit_transform(movie_df[features]) # 线性核计算两两相似度等价于余弦相似度但更快 content_sim linear_kernel(tfidf_matrix, tfidf_matrix) np.fill_diagonal(content_sim, 0) return content_sim基于内容的推荐最大的优势是完全没有冷启动问题——新电影只要有元数据就能被推荐。但它的短板也很明显推荐结果容易趋同用户永远只看到和自己历史口味类似的电影缺少多样性。实际工程里纯粹用基于内容的很少大多数系统会用它做召回池的一个补充数据源然后靠协同过滤来打散和优化。4.3 三种算法的适用场景对比毕设答辩时这样说最稳三种算法在同一份数据上跑出来的结果各有侧重理解和表述上的差异直接决定答辩质量。下表是我整理的一版对比维度物品协同过滤用户协同过滤基于内容推荐核心思想物与物相似人与人相似物与物属性相似计算开销需离线算物品相似度用户量越大越慢只需文本特征向量冷启动能力新物品无法推荐新用户无邻居可用新物品有属性即可推荐可解释性中等弱强适用数据量级物品多则计算量大用户多则计算量大文本特征 丰富时效果最好答辩中关于“为什么选这些算法”的表述我的建议是不要只背定义说“我的项目覆盖了推荐系统两大类方法协同过滤里同时实现了 ItemCF 和 UserCF再结合基于内容的推荐来缓解冷启动问题”——这就够了再配一个实际案例说明切换逻辑。5. 混合推荐与算法评估离线跑分和三个高频踩坑记录三种算法单独讲完实际工程里不可能只用一个算法跑到底。混合推荐策略、评估指标和上线前的测试是真正决定这个系统作为毕设能拿多少分的地方。这一章的避坑部分全部来自我复现代码时真实踩进去的坑。5.1 混合推荐策略加权融合与级联切换的具体实现最常见的混合方式是加权融合给三种算法算出的分数分别乘以权重再汇总。需要注意的是不同算法产生的分数量纲不同ItemCF 的加权得分可能高达几百分UserCF 是 0-5 的评分预测而基于内容的是 0-1 的相似度。直接相加等于默认了背后的权重实际上忽略了量纲差异。归一化比权重本身更关键。def hybrid_recommend(user_id, rating_matrix, item_sim, user_sim, content_sim, user_rated_items): # 三种算法的推荐列表item_id: score 字典 itemcf_score itemcf_recommend(user_rated_items, item_sim, top_n50) # min-max 归一化把得分压到 0~1 def normalize(scores_dict): min_v min(scores_dict.values()) max_v max(scores_dict.values()) return {k: (v - min_v) / (max_v - min_v) for k, v in scores_dict.items()} # 加权融合权重根据验证集调优先平均起步 final_score {} for item_id, score in itemcf_score.items(): final_score[item_id] 0.4 * score for item_id, score in usercf_predict_list(user_id).items(): final_score[item_id] 0.3 * score for item_id, score in content_based_scores(user_rated_items, content_sim).items(): final_score[item_id] 0.3 * score return sorted(final_score.items(), keylambda x: x[1], reverseTrue)[:10]权重参数调优时如果你不想引入额外的机器学习库网格搜索是足够的——把权重按 0.1 的步长遍历每次在测试集上算一遍 RMSE选最优组合。5.2 评估指标用 MAE、RMSE 与 Top-N 命中率量化推荐效果评估环节是整个系统里最容易被忽略但又最影响答辩效果的部分。推荐系统的离线评估主要分成两类评分预测类指标和排序推荐类指标。evaluate.html 页面要展示的正是这些数字。from sklearn.metrics import mean_absolute_error, mean_squared_error import numpy as np import math def evaluate(predictions, ground_truth): # predictions 和 ground_truth 都是 预测分/真实分 的列表 mae mean_absolute_error(ground_truth, predictions) rmse math.sqrt(mean_squared_error(ground_truth, predictions)) # 命中率计算推荐列表里有多少出现在用户真实喜欢的物品中 hit_count 0 for user_id, recom_list in user_rec_list.items(): true_liked set(user_pos_items[user_id]) hits true_liked.intersection(set(recom_list)) if hits: hit_count len(hits) precision_at_n hit_count / (num_users * top_n) return {MAE: mae, RMSE: rmse, Precision10: precision_at_n}MAE 和 RMSE 都是越小越好但 RMSE 对大误差更敏感——它把误差平方后再平均再开方所以一个异常大的误差会造成显著影响。Precision10 衡量 Top-10 推荐列表中有多少比例是用户真实喜欢的更贴近实际使用体验。做评估时测试集不应该包含训练集出现过的任一条记录否则等于“开卷考试”分数虚高。5.3 避坑记录三个最容易翻车的细节第一个坑发生在数据处理阶段。复现时我用完整版 MovieLens 数据跑了一遍发现 UserCF 的预测值全部偏低一度怀疑代码写错了。检查后发现问题是评分矩阵中有大量用户只评了 1-2 部电影KNN 强行找邻居的结果是把完全不相干的人拉进来把预测分拉向均值。解决方法是统计每个用户的评分数量过滤掉评分数量少于 10 条的用户或者在 KNN 中给相似度加一个最小阈值。第二个坑是 SVD 的收敛问题。直接对原始评分矩阵做 SVD 时大量缺失评分被当成 0 处理导致隐向量偏向“默认低分”的方向。常见做法是先对矩阵做全局均值填充或用户均值填充再做 SVD。如果填充值本身不合理降维后的特征物理意义会很奇怪直接表现为推荐结果和常识严重不符。第三个坑是随机种子。这个问题最隐蔽三个人跑同一份代码得到三个不同的评估结果于是开始怀疑算法实现。其实原因很可能是训练集划分和 KNN 初始化里的随机性没有固定。解决办法是在所有涉及随机的函数里统一设置random_state42。排查顺序建议是先查数据划分再查算法内部的随机过程最后检查是否有隐式的 shuffle 操作。这个坑在毕设答辩前夜遇到时是最折磨人的因为现象看起来完全随机毫无规律可循。6. 进阶技巧把系统从“能跑”变成“能讲清楚”前面五章已经覆盖了三种算法从原理到实现的全过程。这一章说两个实战中真正拉开差距的技巧如何构造冷启动场景的解决方案以及如何把算法参数调优的过程变成答辩中的加分项。冷启动是推荐系统里被问得最多的问题也是这套项目最有发挥空间的部分。真实场景里新用户没有任何行为数据协同过滤无从下手。但基于内容的推荐可以做到只要用户注册时选择了几个感兴趣的标签比如“科幻”“动作”就能立刻返回与这些标签匹配的电影。工程上通常会把“用户注册期选择的标签”当作冷启动阶段的伪评分来构造用户向量。实现方式很简单给标签表中的每个标签分配一个虚拟物品用户选择标签就相当于给这个虚拟物品评了 5 分然后走标准的基于内容推荐流程。参数调优部分很多人把它做成网格搜索完事但答辩时最能体现工程能力的是你能否解释“为什么这个参数区间是合理的”。比如物品协同过滤的相似度阈值取 0.3 还是 0.5取决于数据稀疏程度——越稀疏的数据阈值应该设得越低否则几乎没有物品能超过相似度门槛推荐列表会变成纯热门推荐。这个解释比单纯说“我试了 0.3 效果最好”更有说服力。具体的调参过程建议用图表展示横轴是 K 值或阈值纵轴是 RMSE 或 PrecisionN直接截个图贴到论文或 PPT 里。最后说一个我自己的习惯每次跑完一组实验都强制把随机种子、数据版本、参数组合和评估指标记录成一个四元组的 log 文件命名格式类似log_20240215_itemcf_k50_seed42.json。因为推荐系统的实验极其依赖随机性没有这个习惯调试到第三天你会发现完全想不起来昨天“看起来效果不错”的结果是怎么跑出来的。希望帮到你。本文还有配套的精品资源点击获取