
简介基于Python的电影推荐系统完整项目整合物品协同过滤、用户协同过滤与基于内容的推荐三大算法适合毕业设计、课程设计或推荐系统入门实践需具备一定Python基础。压缩包共2000个文件体积约257.25MB包含11个Python源码文件、4个pyc编译文件、5个HTML页面以及近2000张电影图片素材另有CSS与JS负责样式和交互前后端功能完整。代码覆盖用户-物品评分矩阵构建、相似度计算、推荐结果生成与冷启动处理等关键环节三种算法分别从物品相似、用户相似和内容特征角度生成推荐便于对照学习与优化。前端页面涵盖推荐首页、电影详情、历史记录与评测等模块可直观展示推荐效果适合演示与调试。已有112人学习下载资源可直接运行也可作为二次开发蓝本帮助快速搭建电影推荐演示系统。1. 基于Python的电影推荐系统从压缩包里的五张页面看推荐算法落地如果不看后缀很多人会把这份“基于Python的电影推荐系统”当成一个普通的前端模板包——index.html、recommend.html、detail.html、history.html、evaluate.html加一张style.css和四张电影海报。但做过推荐系统的人一眼能看出这套文件的完整闭环首页承载评分采集recommend 页面输出推荐结果detail 在详情页承接用户点击history 回放历史行为evaluate 负责离线评估。配合物品协同过滤、用户协同过滤、基于内容的推荐三种算法它正好覆盖毕业设计和入门实践里最常被问到的三条技术路线。这篇文章就从这个压缩包的页面结构出发把三种算法从评分矩阵开始拆开落到底层参数、可复现代码、Web 集成和评估指标上。2. 评分矩阵与相似度计算协同过滤算法的基础设施2.1 用户-物品评分矩阵的稀疏存储协同过滤算法的起点是评分矩阵。行是用户列是电影单元格是用户对电影的评分。但真实场景里一个用户看过的电影占全部电影的比例通常不到 1%这个矩阵是极度稀疏的直接存成二维数组会浪费大量内存也没法交给 numpy 做快速运算。常见的做法是先用 Python 字典保存原始评分数据再转成 scipy 的稀疏矩阵。itemCF和userCF的相似度计算都基于这个矩阵。import pandas as pd from scipy.sparse import csr_matrix import numpy as np ratings pd.read_csv(ratings.csv) # 至少包含 userId, movieId, rating 三列 user_ids ratings[userId].astype(category) movie_ids ratings[movieId].astype(category) user_codes user_ids.cat.codes.values movie_codes movie_ids.cat.codes.values scores ratings[rating].values # 构建 CSR 稀疏矩阵行是用户编码列是电影编码 user_item_matrix csr_matrix( (scores, (user_codes, movie_codes)), shape(user_ids.cat.categories.size, movie_ids.cat.categories.size) )这里的核心在csr_matrix的参数三个数组分别表示非零评分值、用户编码、电影编码。shape 由用户总数和电影总数决定。转换成稀疏格式之后后续计算相似度时只遍历非零元素内存和耗时都能控制在可接受范围内。实际做毕设时建议把ratings.csv里的数据先做一次编码映射否则会出现用户 id 不连续导致的矩阵扩容问题。2.2 余弦、皮尔逊、杰卡德相似度的适用边界很多初学者一上来就套cosine_similarity但三个相似度指标的服务对象完全不同。相似度指标适用数据典型场景主要弱点余弦相似度稀疏评分向量物品协同过滤不考虑用户评分尺度差异皮尔逊相关系数中心化评分用户协同过滤计算量略大评分过少时不稳定杰卡德相似系数二值偏好看过/没看过冷启动时的兜底丢失评分强度信息在物品协同过滤里用户 A 给两部电影分别打了 4 分和 5 分用户 B 给同样两部电影打了 2 分和 3 分余弦相似度会把这两组向量视为“方向一致”但实际上 B 的口味明显更挑剔。皮尔逊相关系数会先减去各自均值再算相关性能修正这个偏差。用户协同过滤通常倾向皮尔逊物品协同过滤用余弦加均值中心化更稳。2.3 向量化计算相似度矩阵计算相似度矩阵最忌讳的是两层 for 循环。电影数量到几千部时双重循环的时间开销会直接卡死笔记本。正确做法是先把矩阵做行归一化然后用一次矩阵乘法得到余弦相似度。from sklearn.preprocessing import normalize def compute_cosine_similarity(matrix): # 对行向量做 L2 归一化再内积等价于余弦相似度 normalized normalize(matrix, norml2, axis1) sim normalized normalized.T # 将对角线强制归零排除物品与自身的相似度 np.fill_diagonal(sim, 0) return sim item_sim compute_cosine_similarity(user_item_matrix.T) # 物品相似度矩阵 user_sim compute_cosine_similarity(user_item_matrix) # 用户相似度矩阵这段代码用了“归一化后内积等于余弦相似度”的性质余弦公式的分母在归一化后被约掉剩下两个单位向量的点积。axis1表示按行处理对物品相似度需要把矩阵转置让每行代表一部电影的评分分布。fill_diagonal置零是协同过滤的标准做法防止推荐结果里混入“自己给自己推荐”的噪声。3. 三种推荐算法的工程化实现从公式到可运行代码3.1 物品协同过滤对用户的历史评分做加权求和物品协同过滤的核心假设是用户喜欢的东西和他以前喜欢的东西相似。实现分两步先找相似物品再按用户历史评分做加权预测。def item_based_predict(user_id, item_id, item_sim, user_item_matrix, k20): user_vec user_item_matrix[user_id].toarray().flatten() # 取目标物品的全部相似度 sim_scores item_sim[item_id] # 筛掉用户没评过分的物品 rated_items np.where(user_vec 0)[0] if len(rated_items) 0: return 0.0 # 在已评分的物品里找与目标物品最相似的 k 个 candidate [(sim_scores[j], user_vec[j], j) for j in rated_items if j ! item_id] candidate.sort(keylambda x: x[0], reverseTrue) candidate candidate[:k] weighted_sum sum(sim * rating for sim, rating, _ in candidate) sim_sum sum(abs(sim) for sim, _, _ in candidate) return weighted_sum / sim_sum if sim_sum 0 else 0.0加权求和时用相似度乘以用户原评分最后除以相似度绝对值之和目的是把预测值拉回和用户历史评分同一个量纲。这里有一个容易忽略的细节candidate里要排除目标物品本身否则相似度矩阵对角线虽然被置零仍可能在边界情况下出现自引用。k值决定了邻居数量k 太小预测依赖单部电影方差大k 太大低相似度物品会拖低预测精度一般从 20 开始调。3.2 用户协同过滤找到相似用户再聚合他们的偏好用户协同过滤换了一个视角找和当前用户口味最接近的 k 个用户把他们喜欢而目标用户没看过的电影收集起来按相似度加权后推荐。def user_based_recommend(user_id, user_sim, user_item_matrix, k30, top_n10): user_vec user_item_matrix[user_id].toarray().flatten() sim_scores user_sim[user_id] # 排除自己取相似度最高的 k 个用户 nearest np.argsort(sim_scores)[-k-1:-1][::-1] scores np.zeros(user_item_matrix.shape[1]) for neighbor in nearest: neighbor_vec user_item_matrix[neighbor].toarray().flatten() # 只看邻居看过而目标用户没看过的电影 unseen (neighbor_vec 0) (user_vec 0) scores[unseen] sim_scores[neighbor] * neighbor_vec[unseen] # 去除目标用户已经看过的电影 scores[user_vec 0] 0 top_items np.argsort(scores)[-top_n:][::-1] return [(int(item), float(scores[item])) for item in top_items if scores[item] 0]这段代码里的unseen布尔掩码是筛选关键邻居喜欢的电影集合中只有目标用户没看过的才进入候选。nearest的切片[-k-1:-1]避开了自己[::-1]把顺序改为相似度从高到低。用户协同过滤的问题在于用户行为稀疏矩阵里大量用户之间没有共同评分物品相似度为 0所以 k 值通常要比物品协同过滤取得更大30 到 60 都算正常区间。3.3 基于内容的推荐用电影特征向量绕过评分稀疏性问题基于内容的推荐不依赖用户打分它的逻辑是分析电影自身的属性——类型、导演、演员、简介——构造特征向量再和用户历史偏好做匹配。对于新电影即使没人评分也能参与推荐。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity # movies 表简化每部电影有一串组合好的文本特征 movie_texts [ Action Adventure Sci-Fi, Comedy Romance, Drama Mystery, Action Thriller ] vectorizer TfidfVectorizer(token_patternr\w) tfidf_matrix vectorizer.fit_transform(movie_texts) content_sim cosine_similarity(tfidf_matrix) def content_based_recommend(user_liked_movie_idx, top_n10): sim_scores content_sim[user_liked_movie_idx] top_items np.argsort(sim_scores)[-top_n-1:-1][::-1] return top_itemsTF-IDF 在这里做的是把文本特征转成稀疏数值向量TF 衡量词在单部电影里出现的频率IDF 衡量词在整个电影集合里的区分度。“Action”如果出现在大量电影里IDF 值会很低不会对相似度产生太大干扰而“Mystery”这类出现较少的词权重更高反而成为区分电影风格的关键信号。内容推荐的明显短板是“信息茧房”它只会推荐和你过去喜欢的电影相似的片子无法发现新类型。所以在混合系统里它更多承担冷启动期的兜底任务而不是主力。4. 混合推荐策略与冷启动兜底三种算法如何配合4.1 加权混合先归一化再线性组合混合推荐最直接的做法是把三种算法输出的分数做线性加权。但三个分数分布完全不同物品协同过滤的预测值在 15 之间用户协同过滤的输出是评分聚合值内容推荐的相似度在 01 之间直接相加会把数值量级大的算法的主导作用放大。所以第一步必须把每个分数归一化到同一区间。def normalize_scores(scores_dict): if not scores_dict: return {} min_val min(scores_dict.values()) max_val max(scores_dict.values()) if max_val min_val: return {key: 0.0 for key in scores_dict} return {key: (val - min_val) / (max_val - min_val) for key, val in scores_dict.items()} # 每个候选电影由三种算法各出一个分数 final_scores {} for movie_id in candidate_set: final_scores[movie_id] ( 0.4 * item_scores.get(movie_id, 0) 0.4 * user_scores.get(movie_id, 0) 0.2 * content_scores.get(movie_id, 0) )权重怎么定没有标准答案但有一个经验起点物品协同过滤最稳定用户协同过滤在行为数据充足时精度最高内容推荐在冷启动场景表现最好。常规权重的分配可以在 0.4 / 0.4 / 0.2 左右。等有评估结果之后再做网格搜索调整权重而不是一开始就拍脑袋。提示混合之前务必确认三种算法拿到的候选集尽量对齐。如果候选集本身差异很大加权混合就退化成“谁候选多谁赢”这时候更适合用级联策略。4.2 冷启动问题的三个回退方案冷启动是协同过滤绕不开的坑新电影没有评分物品协同过滤算不出相似度新用户没有历史行为用户协同过滤找不到近邻。压缩包里虽然只展示了前端页面但后端逻辑上通常需要预设回退路径。场景可用算法回退策略新用户基于内容 热门榜不依赖历史评分直接按内容相似度推新电影基于内容利用电影文本特征进入候选池新系统热门榜 统计规则按平均分、评论数、点击量排序实际写代码时推荐主流程要加一个用户/物品覆盖度判断如果用户没有评分记录直接跳过协同过滤分支进入内容推荐如果内容特征也为空就返回全局热门电影。4.3 混合策略的动态切换比固定权重更稳的做法是动态切换用户历史行为超过阈值比如 30 条评分时协同过滤权重上调行为不足时内容推荐权重上调。常见做法是定义一个覆盖系数让权重随行为数量变化。def adaptive_weights(user_id, history_len30): if history_len 10: return {item: 0.2, user: 0.2, content: 0.6} elif history_len 30: return {item: 0.4, user: 0.3, content: 0.3} else: return {item: 0.4, user: 0.4, content: 0.2}这个函数把混合策略的骨架表现出来冷启动期依赖内容推荐中期逐步过渡到协同过滤数据充足后用户协同过滤的比重提升。参数阈值不是固定的需要结合评分矩阵的稀疏程度微调但整体思路比固定权重更符合真实的推荐流程。5. 从 HTML 页面到 Flask 后端把推荐算法变成可演示的 Web 应用5.1 压缩包页面结构与 Flask 路由的映射关系压缩包里的前端文件并不是孤立的静态页面每个文件对应一个功能视图。后端用 Flask 承载时路由与模板的映射关系如下前端文件路由路径功能关键传参index.html/用户评分入口user_id, movie_id, ratingrecommend.html/recommend展示推荐结果movies, scores, algorithmsdetail.html/movie/id电影详情与相似电影movie, similar_movieshistory.html/history查看历史评分user_ratingsevaluate.html/evaluate离线评估指标展示precision, recall, rmse这里的关键是recommend.html需要在页面渲染前就拿到推荐结果所以 Flask 视图函数里必须完成“加载评分数据 - 调用推荐算法 - 组织模板数据”的完整链路。from flask import Flask, render_template, request app Flask(__name__) app.route(/) def index(): # 从数据库或 CSV 取出所有电影 movies load_movies() return render_template(index.html, moviesmovies) app.route(/recommend, methods[POST]) def recommend(): user_id int(request.form.get(user_id)) top_n int(request.form.get(top_n, 10)) item_scores item_based_recommend(user_id, top_n20) user_scores user_based_recommend(user_id, top_n20) content_scores content_based_recommend(user_id, top_n20) final_scores hybrid_merge(item_scores, user_scores, content_scores) movies [load_movie(mid) for mid, _ in final_scores[:top_n]] return render_template(recommend.html, moviesmovies, scoresfinal_scores[:top_n])request.form.get从 POST 表单里取用户输入render_template把 Python 数据结构直接传给 Jinja2 模板。index.html的表单提交到/recommendrecommend.html里用{% for movie in movies %}循环渲染候选项。参数top_n控制推荐数量一般默认值取 10展示层足够评分层不至于太拥挤。5.2 用户评分与历史记录的数据结构设计history.html要展示用户看过的电影和评分这要求后端在每次评分动作后把数据持久化而不是仅仅留在内存里。import sqlite3 def save_rating(user_id, movie_id, rating): conn sqlite3.connect(recommend.db) cursor conn.cursor() cursor.execute( INSERT INTO ratings (user_id, movie_id, rating, timestamp) VALUES (?, ?, ?, datetime(now)) , (user_id, movie_id, rating) ) conn.commit() conn.close()用 SQLite 做存储是毕业设计最稳妥的选择不需要安装数据库服务单文件即可运行recommend.db可以随项目打包提交。评分表字段里加一个timestamp后续做时间衰减或行为序列分析时不用回头改表结构。上线时把sqlite3.connect替换成 MySQL 或 PostgreSQL 驱动即可业务代码不用动。5.3 集成调试时最常遇到的问题静态文件路径是第一个坑。style.css和四张 jpg 放在 templates 同级目录很容易被 Flask 404。正确做法是把它们放进static/目录HTML 模板里用url_for(static, filenamestyle.css)引用。第二个坑是模型加载时机。每次请求都重新读 CSV、建矩阵、算相似度页面会很卡。常见做法是用模块级缓存服务启动时预计算一次相似度矩阵后续请求直接复用。_sim_cache None def get_similarity_matrices(): global _sim_cache if _sim_cache is None: ratings load_ratings() matrix build_user_item_matrix(ratings) _sim_cache { item_sim: compute_cosine_similarity(matrix.T), user_sim: compute_cosine_similarity(matrix) } return _sim_cache用None作为缓存哨兵值是简单的惰性加载模式。第一次访问时构建矩阵之后所有请求直接拿内存里的相似度矩阵配合 Flask 调试模式足够支撑演示。缓存失效策略在这个场景下不需要太复杂数据更新后重启进程即可。6. 离线评估与参数调优让 evaluate.html 的指标有说服力6.1 评估指标的计算口径evaluate.html不能只是个摆设它的核心作用是把算法效果量化成数字。推荐系统离线评估最常用的指标是 PrecionK、RecallK 和 RMSE。Precision 衡量推荐列表里有多少是用户真正喜欢的Recall 衡量用户真正喜欢的电影有多少被推荐出来。def evaluate(ratings_train, ratings_test, model, top_n10): hit 0 pred_total 0 rel_total 0 for user_id in ratings_test[userId].unique(): actual set(ratings_test[ratings_test[userId] user_id][movieId]) recommended model.recommend(user_id, top_ntop_n) hit len(set(recommended) actual) pred_total top_n rel_total len(actual) precision hit / pred_total recall hit / rel_total return precision, recall这个计算是全局指标先把所有用户的命中数累加再统一除以分母评估结果比逐用户平均更稳定。pred_total固定等于用户数乘以 top_nrel_total是测试集里真实评分项的总量。evaluate 页面把这两个值展示出来比单独一张“相似度热力图”更能回应答辩时“你的推荐效果怎么证明”的问题。6.2 三个直接影响结果的参数参数位置推荐起点判定标准K 近邻数三种算法共有2040recall 趋于平稳后停止增大相似度阈值候选过滤0.30.6过滤后候选集仍大于 top_n混合权重hybrid_merge0.4 / 0.4 / 0.2网格搜索后取 F1 最高点K 值调太大会引入大量弱相似的邻居Precision 下降调太小则推荐结果方差大。阈值的作用是砍掉低质量的相似关系但阈值过高会让候选电影数量不足top_n都凑不满。混合权重建议在参数调优阶段用三层循环做网格搜索每次只改一个因素。6.3 一个可复现的调参脚本把评估逻辑和参数搜索串起来写一个可以一键跑出多组对比结果的脚本调试效率会高很多。param_grid [ {k: 20, weight_item: 0.4, weight_user: 0.4, weight_content: 0.2}, {k: 30, weight_item: 0.4, weight_user: 0.4, weight_content: 0.2}, {k: 30, weight_item: 0.3, weight_user: 0.5, weight_content: 0.2}, {k: 40, weight_item: 0.4, weight_user: 0.4, weight_content: 0.2}, ] for cfg in param_grid: model HybridRecommender(**cfg) precision, recall evaluate(train, test, model) print(cfg, precision, recall)脚本输出的每行都对应一组参数组合的精确率和召回率可以直接拿去做答辩实验表格。实际操作时先固定混合权重扫 K找到 recall 的饱和点再把 K 固定下来扫权重。如果 K 从 20 升到 40 的过程中recall 提升低于 0.5%说明邻域信息已经饱和优先在混合权重上找优化空间而不是继续加大 K。本文还有配套的精品资源点击获取