资讯动态

协同过滤电影推荐系统实战:ItemCF算法与Python/Vue/MySQL全栈实现

发布时间:2026/9/16 5:56:14 来源:尧图企业网站定制
简介一套基于协同过滤的电影推荐系统完整毕业设计资源包含可直接运行的Python源码、配套数据库与论文文档适合计算机相关专业学生完成推荐系统类课题或毕业设计参考。项目难度适中代码经过本地编译验证评审分达95分以上说明具备较高的完整性和规范性。压缩包共688个文件大小21.9MB主要包含py/pyc后端逻辑、vue前端页面、js/svg/css等静态资源、html页面、sql数据库脚本以及doc论文和mp4演示视频文件类型覆盖项目开发与文档撰写所需的主流格式。目前已有145人学习下载资源附带了安装运行脚本安装.bat、运行.bat以及多个备份配置文件方便快速启动和二次修改。通过这套资源可以获得从数据库设计到推荐算法实现、管理后台与论文撰写的整套思路适合需要系统学习协同过滤实战项目或短期完成毕设方案的人群。1. 二十年前的算法凭什么还能拿高分毕设如果你以为推荐系统等于深度学习那这个基于协同过滤的电影推荐系统可能会让你改观。它就是建立在“相似的人会喜欢相似的东西”这条朴素经验上却在数据规模不大的场景里效果和稳定性都超过大部分花哨模型。这个项目是一个完整的前后端分离应用Python 后端计算推荐结果Vue 管理端维护电影和用户数据MySQL 持久化所有评分记录四份批处理脚本把安装、启动、打包流程固化成一条命令。对正在做毕业设计、或者想快速搭一个可演示推荐链路的工程师来说它的价值在于算法理论、工程实现、可视化验证三者齐全评分记录、用户行为、推荐输出都能在页面上看到真实数据流。相比只给一个算法的课程作业这套东西更像是能直接跑起来的推荐系统最小闭环。2. 协同过滤的底层逻辑与 UserCF / ItemCF 选型分析2.1 UserCF 与 ItemCF 的算法差异协同过滤Collaborative Filtering分基于记忆和基于模型两大类这个毕设的主体属于基于记忆的方法也就是直接拿用户历史评分数据做相似度计算。核心问题只有一个给用户 u 预测他对电影 i 的评分时参考谁的判断。UserCF 的思路是找到和 u 口味最接近的一批用户用这批人给 i 的评分来预测 u 的评分。用数学表达就是pred(u, i) avg(u) Σ( sim(u, v) * ( r(v, i) - avg(v) ) ) / Σ( |sim(u, v)| )其中sim(u, v)是用户 u 和 v 的相似度avg(u)是 u 对所有电影评分的均值r(v, i)是用户 v 对电影 i 的实际评分。先做均值归一化是为了消除不同用户打分标准不一致的问题——有人喜欢全给 5 分有人最高只给 4 分不归一化的话相似度会被个人习惯主导。ItemCF 则是反过来。先算电影与电影之间的相似度然后基于用户历史评分过的电影去推荐与他们相似的未看过的电影pred(u, i) Σ( sim(i, j) * r(u, j) ) / Σ( |sim(i, j)| )这里sim(i, j)是电影 i 和 j 的相似度r(u, j)是用户 u 对电影 j 的历史评分。注意 ItemCF 通常不需要再减均值因为电影本身没有“打分习惯”直接对评分加权求和再归一化即可。这两种方式在同样一份评分数据上跑出来的推荐结果差别很大。UserCF 更偏社交你推荐给用户的电影来自“和你相似的人”适合新闻推荐、社区内容推荐这种物品更新快、用户相对稳定的场景。ItemCF 更偏物品本身用户看到推荐时会觉得“我喜欢的电影确实和这些类似”解释性更强适合电影、电商、图书这类物品属性稳定、数量不太大的场景。2.2 相似度计算的三种方式与选择相似度是整个协同过滤的命根子。这个项目里我看到了余弦相似度、皮尔逊相关系数和 Jaccard 系数都有讨论空间但实际工程里最常用的是前两种。余弦相似度把用户对 n 部电影的评分看成 n 维向量然后计算两个向量夹角的余弦值cos(u, v) (u · v) / (||u|| * ||v||)优点是实现简单、计算稳定缺点是它不考虑用户评分的均值差异。用户 A 喜欢打 5 分用户 B 同样喜欢那部电影但只打 4 分余弦相似度会觉得两人“方向”一致但幅度有差异结果数值会偏低。皮尔逊相关系数等于先对两个用户分别做中心化再算余弦pearson(u, v) Σ( (r(u,i) - avg(u)) * (r(v,i) - avg(v)) ) / ( sqrt(Σ(r(u,i) - avg(u))²) * sqrt(Σ(r(v,i) - avg(v))²) )这个公式直接解决了打分习惯不同的问题。这个项目里推荐模块用皮尔逊而不是余弦是有道理的因为电影评分天然带有用户主观倾向中心化之后再算相似度抓的是“口味曲线”而不是“绝对分数”。Jaccard 系数只关心交集jaccard(u, v) |Iu ∩ Iv| / |Iu ∪ Iv|它完全忽略评分值只看是否共同看过。在评分数据稀疏、置信度不高的时候可以用来做粗筛但作为主相似度计算方式会丢失太多信息一般只作为冷启动阶段的补充手段。2.3 这个项目的选型判断从工程文件来看项目里前端有完整的 Vue 管理后台说明它面向的是“运营后台 用户前台”的双端架构。在这个结构下以 ItemCF 为主是合理决策。原因在于用户数量通常大于电影数量用户-电影评分矩阵极度稀疏UserCF 需要实时计算用户与所有用户的相似度每次请求都要遍历整个用户维度性能差而 ItemCF 的电影相似度矩阵可以离线算好线上推荐时只需要查表取前 K 个相似电影再对用户历史评分做加权聚合就行。常见做法是先离线计算物品相似度矩阵存储为稀疏矩阵或内存字典推荐请求进来时 O(1) 查相似物品再聚合排序取 Top-N。项目里的架构基本是这么走的理解了这个主线后面看代码会非常顺。3. 推荐引擎 Python 实现从 MySQL 抽数到输出 Top-N3.1 数据加载与评分矩阵构建推荐引擎第一步是把数据库里的评分记录变成算法能算的矩阵。项目里后端用 Python 实现数据从 MySQL 读取常见的做法是直接用 PyMySQL 拉数再用 pandas 做透视表转成用户-物品矩阵import pymysql import pandas as pd import numpy as np conn pymysql.connect( host127.0.0.1, port3306, userroot, password123456, databasemovie_rec, charsetutf8mb4 ) ratings_df pd.read_sql(SELECT user_id, movie_id, score FROM ratings, conn) conn.close() # 透视成 用户 x 电影 的评分矩阵缺失值填 0 rating_matrix ratings_df.pivot_table( indexuser_id, columnsmovie_id, valuesscore, fill_value0 )注意fill_value0这一步。缺失值填 0 不等于用户打了 0 分只是为了让矩阵稠密化方便矩阵运算。计算皮尔逊相关系数时需要对矩阵按行或者按列做中心化0 值在中心化之后仍然参与分母计算会拉低相似度得分。处理方式有两种一种是计算相似度之前把 0 值 masked 掉另一种是直接用 sklearn 的pairwise_distances配合nan_euclidean_distances处理。毕设场景下直接用 0 填充、计算完再筛选共现数大于阈值的相似对是最快的做法。构建评分矩阵时要留一个映射表记录行索引对应哪个用户 ID、列索引对应哪个电影 ID否则后续推荐结果没法翻译回业务 IDuser_ids rating_matrix.index.tolist() movie_ids rating_matrix.columns.tolist()3.2 相似度矩阵计算与 Top-N 推荐核心代码这个项目推荐模块的核心逻辑可以浓缩成一段完整的 Python 代码。皮尔逊相似度手写不复杂但考虑到矩阵规模不大直接用 pandas 的.corr()是一个稳妥选择# 计算电影之间的皮尔逊相关系数转置矩阵使行电影 movie_sim pd.DataFrame( rating_matrix.T.corr(methodpearson) ).values # 构建电影原始ID - 矩阵列索引的映射 movie_id_to_idx {mid: i for i, mid in enumerate(movie_ids)} idx_to_movie_id {i: mid for i, mid in enumerate(movie_ids)} def recommend_for_user(user_ratings: dict, top_k10, sim_threshold0.3): user_ratings: { movie_id: score }即当前用户的评分记录 top_k: 每个候选电影聚合时取多少个相似电影 sim_threshold: 相似度过滤阈值低于此值的电影对不参与计算 scores {} weight_sum {} for mid, score in user_ratings.items(): if mid not in movie_id_to_idx: continue idx movie_id_to_idx[mid] # 取与当前电影相似度最高的 top_k 部电影 sim_scores movie_sim[idx] candidate_indices np.argsort(sim_scores)[::-1][:top_k 1] for cand_idx in candidate_indices: sim_val sim_scores[cand_idx] if sim_val sim_threshold: continue cand_movie idx_to_movie_id[cand_idx] if cand_movie in user_ratings: continue scores[cand_movie] scores.get(cand_movie, 0) sim_val * score weight_sum[cand_movie] weight_sum.get(cand_movie, 0) abs(sim_val) ranked [ (movie_id, scores[movie_id] / weight_sum[movie_id]) for movie_id in scores if weight_sum[movie_id] 0 ] ranked.sort(keylambda x: x[1], reverseTrue) return ranked[:10]逻辑拆解如下外层遍历用户看过的每一部电影对每部电影找到最相似的 K 个邻居内层过滤掉用户已经看过的电影避免重复推荐权重sim_val * score的意思是“这部电影越像我看过的电影、我给那个电影的打分越高推荐权重越大”。最后用abs(sim_val)做归一化是因为皮尔逊系数可能出现负值直接求和会导致权重抵消影响排序的稳定性。参数top_k和sim_threshold是整个系统最关键的两个调节阀门后面会讲怎么调。3.3 稀疏矩阵与冷启动的处理手段评分矩阵的稀疏程度直接影响计算结果。真实场景里一个几百部电影的系统用户覆盖度能够到 30% 已经很不错绝大多数用户只看过 520 部电影。直接用 dense 矩阵做.corr()在数据量小的时候没问题但一旦用户量过万光存储就接近 1GB 内存这时候必须换成 scipy.sparse 稀疏矩阵格式只存有值的位置。项目里还有一层冷启动兜底逻辑常见做法是在推荐引擎里加一个 fallback 分支def recommend_with_fallback(user_ratings, use_itemcfTrue): if len(user_ratings) 3: return get_popular_movies(top_n10) return recommend_for_user(user_ratings)新注册用户没有评分记录ItemCF 无从算起直接给全局热门电影榜是最稳妥的方案。热门榜用 SQL 聚合生成见下一章的数据设计。这个 fallback 分支看起来简单却是决定系统能不能从小规模演示走向真实上线的关键设计——没有兜底新用户进来只能看到空推荐连页面都填不满。4. 数据库设计与 Vue 管理端的数据流通路4.1 三张核心表的建表 SQL这个项目的数据库设计很克制三张表就支撑起了整个系统用户表、电影表、评分表。电影信息里冗余存了平均分和评分人数这两个字段看起来多余但对推荐结果和前台展示至关重要CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE movies ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, genre VARCHAR(100), release_year INT, rating_avg DECIMAL(3,1) DEFAULT 0, rating_count INT DEFAULT 0, INDEX idx_genre (genre) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE ratings ( user_id INT NOT NULL, movie_id INT NOT NULL, score INT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (user_id, movie_id), INDEX idx_movie (movie_id), INDEX idx_user (user_id), CONSTRAINT fk_ratings_user FOREIGN KEY (user_id) REFERENCES users(id), CONSTRAINT fk_ratings_movie FOREIGN KEY (movie_id) REFERENCES movies(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;ratings表的主键设计为(user_id, movie_id)是刻意的。同一用户对同一电影只能有一条评分记录这个联合主键天然约束了数据唯一性应用层不用再写重复判断。索引上同时保留idx_movie和idx_user是因为推荐引擎要按用户查历史评分、也要按电影聚合热门双向查询都有覆盖。movies表里的rating_avg和rating_count是典型的空间换时间设计。每次前端要展示电影列表都去ratings表做聚合在百万级评分数据下会拖垮主库冗余字段保证几十毫秒内能出结果。代价是写入评分时要同步更新这两个字段项目里是在 Python 后端做事务里一起提交的。4.2 管理端 Vue 页面与后端接口的交互从文件列表里可以看到IndexMain.vue、IndexAsideStatic.vue、IndexHeader.vue、BreadCrumbs.vue、update-password.vue这一组文件拼起来是一个标准后台管理界面左边静态侧边栏导航顶部操作栏中间内容区右下角是面包屑导航。.bak后缀表示这是改动前的备份版本不影响运行。管理端与推荐引擎的关系是管理端负责维护数据用户端负责消费推荐。运营人员通过管理端新增电影、查看用户列表、手动调整评分这些数据全部落入上面三张表推荐引擎下次拉数据时自然感知变化。接口按 REST 风格组织模块接口路径方法作用用户管理/api/usersGET/POST用户列表 / 新增用户电影管理/api/moviesGET/POST/PUT/DELETE电影增删改查评分管理/api/ratingsGET/POST查看评分 / 手动添加评分推荐引擎/api/recommend/user_idGET获取用户推荐列表热门榜单/api/movies/popularGET冷启动兜底热门榜前端 Vue 组件通过 axios 调用这些接口拿到 JSON 数据后渲染成表格。整个数据流路径是MySQL → Python Flask 路由 → JSON → Vue 组件 data → 页面 DOM。理解这条链路后想加一个“查看某用户推荐详情”的功能只需要在 Vue 里加一个路由页面然后调用/api/recommend/user_id接口即可。4.3 推荐引擎相关的关键 SQL 写法推荐系统里最常用的三条查询语句直接决定了后端几个页面的数据来源-- 查询用户的评分历史用于推荐引擎实时计算 SELECT movie_id, score FROM ratings WHERE user_id %s; -- 热门电影榜冷启动兜底推荐源 SELECT movie_id, AVG(score) AS avg_score, COUNT(*) AS rating_count FROM ratings GROUP BY movie_id HAVING rating_count 5 ORDER BY avg_score DESC, rating_count DESC LIMIT 10; -- 新增评分后同步电影表的冗余统计字段 UPDATE movies m SET m.rating_avg ( SELECT AVG(r.score) FROM ratings r WHERE r.movie_id m.id ), m.rating_count ( SELECT COUNT(*) FROM ratings r WHERE r.movie_id m.id ) WHERE m.id %s;第二条 SQL 里的HAVING rating_count 5和ORDER BY avg_score DESC, rating_count DESC是冷启动榜单的精髓。只按平均分排序会导致只有一个人打了 5 分的电影霸榜加上评分人数门槛和人数降序排出来的榜单才是真正有说服力的热门。第三条 SQL 在项目里会放在评分事务提交后执行保证冗余字段的最终一致性。5. 从 .bat 脚本到浏览器部署运行与推荐参数调优5.1 四个批处理脚本的启动顺序与含义项目根目录下的安装.bat、运行.bat、2-run.bat、3-build.bat把整个项目的生命周期拆成了几个固定阶段。在 Windows 环境下拿到这个项目执行顺序有讲究。先看安装脚本通常内容是把 Python 依赖和前端 npm 依赖一次性装齐pip install -r requirements.txt cd frontend npm install运行.bat对应后端服务启动等价于python app.py这里有个项目常见的坑如果后端跑在 5000 端口、前端开发服务器跑在 8080 端口浏览器里打开 Vue 项目访问后端接口会面临跨域问题。Vue 开发环境下通过vue.config.js配置代理转发是最省事的方案module.exports { devServer: { port: 8080, proxy: { /api: { target: http://127.0.0.1:5000, changeOrigin: true } } } }配置之后前端代码里所有/api/xxx请求都会被 webpack-dev-server 代理到 Flask 后端浏览器层面看不到跨域。2-run.bat和3-build.bat是一对前者启动前端开发服务器用于日常调试等价于npm run serve后者是最终打生产包等价于npm run build。数字前缀表示执行顺序上的推荐顺序——先起后端、再跑前端开发模式、最后才打包部署。注意一点Vue 项目里看到update-password.vue.bak这类.bak文件说明项目经历过修改。部署时.bak文件不影响运行但如果IndexMain.vue和IndexMain.vue.bak同时存在而IndexMain.vue恰好是空文件就要检查一下是不是备份覆盖过正式文件。稳妥做法是直接把.bak后缀文件移出工作目录只保留未带.bak的正式文件。5.2 推荐参数的调优区间与效果对照代码里两个核心参数top_k相似邻居数量和sim_threshold相似度阈值直接决定推荐质量。经验值是这样一组对照参数值表现top_k5推荐结果聚焦但覆盖窄冷门电影几乎不会出现top_k20平衡点覆盖和准确度都不错top_k50覆盖广但噪音大相似度低的电影混进来sim_threshold0.1几乎所有电影都相互关联推荐结果偏随机sim_threshold0.3过滤掉明显不相关结果推荐列表开始有解释性sim_threshold0.7只有高度相似才推荐结果太少用户很快看腻这个项目默认给到top_k10、sim_threshold0.3处于保守和激进之间适合评分数据量中等的场景。调参的方向取决于你要什么想要惊喜度就降阈值、降 Top-K想要准确率就升阈值、升 Top-K。注意调整参数后不需要改代码把两个值抽成配置文件或环境变量是最省事的方式项目里通常写在config.py或启动参数里。5.3 运行失败的常见定位点拿到项目跑不起来的状况按概率从高到低排查这几个位置Python 版本不匹配Vue 后端依赖 Flask 2.x 时 Python 需要 3.8 以上、MySQL 密码和app.py里连接配置不一致、端口被占用、前端 node_modules 未安装成功。最快的定位方式是先看控制台报错——如果是连接数据库失败错误信息里会直接给出 host 和 port 信息如果是ModuleNotFoundError说明安装.bat没有完整执行。数据库导入也和安装脚本的比例相关。项目通常附带.sql文件导入方式在 Windows 命令行下是mysql -u root -p movie_rec movie_rec.sql导入前要先建库然后另一条 SQL 也可以选择让 SQL 文件自带建库语句。导入后重点验证三张表的数据量是否合理如果ratings表为空推荐接口返回的就是空列表管理端页面也看不到评分记录。6. 离线评测与上线前的最后验证6.1 用 MAE 和 RMSE 量化推荐准确度项目跑起来之后如何证明推荐“准”不能只靠肉眼浏览推荐列表要上离线评测。把评分数据按用户维度切出训练集和测试集在训练集上算相似度并预测测试集的评分再对比预测值与真实值的偏差。MAE 是绝对误差均值RMSE 是均方根误差后者对离群点更敏感import numpy as np def evaluate_mae_rmse(predictions, ground_truth): predictions: 预测评分列表 ground_truth: 真实评分列表 pred np.array(predictions) truth np.array(ground_truth) mae np.mean(np.abs(pred - truth)) rmse np.sqrt(np.mean((pred - truth) ** 2)) return mae, rmseMAE 小于 0.8、RMSE 小于 1.0 在这个数据规模下是不错的成绩。比绝对数值更重要的指标是调整参数前后的对比如果top_k从 10 调到 20MAE 明显变大说明相似邻居太多引入了噪音反方向调回去就对了。6.2 Top-N 推荐的命中率与覆盖率验证评分预测误差只能反映数值拟合能力推荐列表是否被用户接受还要看命中率。把用户真实看过的电影和推荐列表做交集取前 N 个推荐看是否命中def precision_at_n(recommended, actual, n10): rec_set set(recommended[:n]) act_set set(actual) if not rec_set: return 0.0 return len(rec_set act_set) / n同时观察另一个指标覆盖率。推荐列表里涉及的电影数量除以电影总量如果覆盖率只有 5%说明系统永远只在头部电影里打转尾部电影没有曝光机会。项目里的覆盖率和命中率是一对矛盾指标覆盖率太高命中率必然下降上线时按业务目标取舍就好。6.3 上线前最值得检查的一个点整个系统如果只允许检查一个地方先去查ratings表的数据分布。协同过滤的效果上限由数据质量决定算法只能逼近这个上限。执行一下聚合查询SELECT COUNT(*) AS rating_total, COUNT(DISTINCT user_id) AS user_cnt, COUNT(DISTINCT movie_id) AS movie_cnt, COUNT(*) / (COUNT(DISTINCT user_id) * COUNT(DISTINCT movie_id)) AS density FROM ratings;如果密度值低于 1%ItemCF 还能勉强出结果UserCF 几乎不可用如果每个用户平均评分低于 5 条任何协同过滤算法的输出都有较强随机性。看到极低密度时先补评分数据再调参调sim_threshold也救不回来。这是项目运行到后期最容易被忽略、却最影响推荐质量的一环。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价