拓冰建站拓冰建站
首页 / 资讯中心 / 正文

协同过滤电影推荐系统:从算法原理到前后端分离工程实践

简介运用Python与协同过滤算法构建的电影推荐系统采用Vue实现前后端分离并集成Django与MySQL是一套面向计算机相关专业学生、适用于毕业设计与推荐算法入门实践的完整可运行项目。压缩包共688个文件约13.01MB主要包含38个Python源码、33个Vue组件、162个JavaScript脚本、CSS样式及SVG图标另有SQL数据库脚本、Markdown说明文档与论文文档代码结构覆盖前端页面、后端接口和数据库初始化。目前已有202人学习浏览项目代码经测试运行成功后才上传附有安装与运行脚本功能模块完整含管理员和用户两种角色。通过该资源可获得完整电影推荐系统源码、数据库文件、论文及配套文档电影分类、电影信息、评分、评论与收藏等模块均已实现协同过滤算法逻辑清晰便于在此基础上修改扩展也可直接作为课题演示或课设作业。1. 为什么用协同过滤做电影推荐还要坚持前后端分离如果你只是写一个“给用户推荐几部电影”的演示最省事的做法是后端模板渲染页面直接塞进一个 SQLite 文件里。但真实业务场景里电影推荐系统要面对的是用户行为日志、候选集更新、多端共用推荐接口、以及后续接入实时特征这种情况下“推荐算法”和“展示层”必须解耦。标题里提到的这个项目本质上是一套完整的工程化教学样板Python 负责协同过滤算法与推荐接口Vue 负责交互展示前后端通过 HTTP/JSON 通信SQL 文件负责初始化数据论文和文档说明则把算法选型、系统设计、测试结果串成一条可交付的链路。很多人拿到这类项目时容易卡在三个地方协同过滤的相似度矩阵怎么算、推荐接口返回的数据结构如何设计、Vue 怎么处理跨域和 token。这三个问题恰好就是前后端分离架构下的核心实践点。这篇文章直接按这三条线展开落到可以跑的代码、可以调的参数、以及你真正部署时会踩的坑。适合的目标人群是有 Python 和基础 SQL 经验、想把推荐算法从脚本变成一个 Web 服务的开发者如果你只是想在简历上写“协同过滤”那也得先搞清楚 UserCF 和 ItemCF 在工程上的取舍。2. 协同过滤算法原理与相似度计算选型2.1 基于用户与基于物品的协同过滤先决定你的场景协同过滤的核心假设是过去有相似偏好的人未来也会有相似偏好。基于用户的协同过滤UserCF先找“与我兴趣相似的用户”再把那些用户看过而我没看过的电影推荐给我基于物品的协同过滤ItemCF则先找“与我曾看过电影相似的电影”它的相似不是内容上的相似而是“被同一批人同时喜欢”的关系。在电影推荐场景中用户数量通常远远大于电影数量而且评分/行为的分布非常稀疏。UserCF 需要在每次推荐时实时计算用户之间的相似度当用户规模达到百万级时这个计算代价会变得不可接受。ItemCF 可以预先离线计算物品相似度矩阵把相似关系固化下来在线推荐时只需要查询目标用户的行为历史对应的 item 邻域即可。所以如果你的系统面向海量注册用户ItemCF 是更常见的工程选择如果是课程设计或中小规模实验UserCF 更直观也更容易讲清楚算法细节。标题里只写了“协同过滤算法”没有限定具体变体下面我会以 UserCF 为主线做完整实现同时给出切换到 ItemCF 的关键修改点。2.2 相似度度量皮尔逊相关系数 vs 余弦相似度在计算用户相似度之前得先把用户对电影的评分/行为表示成向量。假设有 5 部电影用户 A 的评分向量是 [5, 3, 0, 4, 0]用户 B 是 [4, 0, 0, 5, 2]0 表示未看。余弦相似度直接计算两个向量夹角的余弦值它不关注用户整体评分是偏严还是偏松。皮尔逊相关系数则在余弦相似度基础上减去了用户的平均评分可以消除“评分尺度”差异。比如 A 习惯打 3-5 分B 习惯打 1-3 分虽然他们口味相同但余弦相似度会被整体分差拉低皮尔逊则在中心化后能更准确反映偏好趋势。在 MovieLens 这类显式评分数值上皮尔逊通常表现更好在点击、收藏、播放这类隐式行为上数据往往只有 0/1余弦相似度更常用。下面代码里我会把两种度量都实现出来通过一个参数切换方便你在自己的数据上对比效果。2.3 用 Python 实现用户相似度并生成推荐候选集import numpy as np import pandas as pd def load_ratings(sql_pathratings.csv): # 实际项目中数据一般从 MySQL 的 user_movie_rating 表读取 # 这里用 CSV 演示字段与 SQL 表保持一致 df pd.read_csv(sql_path) return df def normalize_ratings(df): # 按用户减去平均分消除评分尺度影响 df[mean] df.groupby(user_id)[rating].transform(mean) df[norm] df[rating] - df[mean] return df def user_similarity_matrix(df, methodpearson): # 构造 用户-电影 矩阵行为 user_id列为 movie_id pivot df.pivot_table(indexuser_id, columnsmovie_id, valuesnorm).fillna(0) if method cosine: # 余弦相似度 denom np.linalg.norm(pivot.values, axis1, keepdimsTrue) sim (pivot pivot.T) / (denom denom.T) else: # 皮尔逊相关系数标准化后的向量点积就是相关系数 sim pivot pivot.T np.fill_diagonal(sim.values, 0) return sim, pivot这段代码的逻辑是先用transform(mean)把每个用户的评分均值算出来形成一个新列再用pivot_table把数据展开成二维矩阵缺失值填 0。皮尔逊相关系数在中心化之后点积结果恰好等于相关系数如果你需要严格的分母归一化可以再除以向量模长。np.fill_diagonal把用户自己和自己的相似度置为 0避免推荐结果包含自己。参数说明methodcosine时适合隐式反馈数据methodpearson适合显式评分。实际装置中这个矩阵可能非常稀疏建议先做一次“只看过至少 5 部电影的用户”过滤否则相似度会被长尾用户干扰。推荐候选集的生成见下面代码def recommend_for_user(sim_matrix, norm_df, target_user, top_k_users10, top_n_movies10): # 找和目标用户最相似的 K 个用户 sim_scores sim_matrix.loc[target_user].sort_values(ascendingFalse).head(top_k_users) # 取这些用户看过但目标用户没看过的电影 target_history set(norm_df[norm_df[user_id] target_user][movie_id]) candidates {} for other_user, sim_score in sim_scores.items(): other_history norm_df[norm_df[user_id] other_user] for _, row in other_history.iterrows(): if row[movie_id] not in target_history: # 加权累加相似度 * 该用户的偏好程度 candidates[row[movie_id]] candidates.get(row[movie_id], 0) sim_score * row[norm] ranked sorted(candidates.items(), keylambda x: x[1], reverseTrue)[:top_n_movies] return [movie_id for movie_id, _ in ranked]推荐分数的累加方式是“相似度乘以该用户对电影的中心化评分”这比单纯的“统计相似用户看过次数”更细粒度因为一个 5 分好评和一个 3 分普通评价对推荐的贡献应该不同。top_k_users和top_n_movies是两个最值得调的参数k 太小推荐结果受个别相似用户主导k 太大边缘用户把分数稀释。经验值是用户规模的 1%5%你可以直接做成 API 参数方便后续调优。2.4 回到工程算法模块与 Web 服务如何划分常见做法是把算法部分封装成一个独立的 Python 包不直接写在任何路由文件里。比如recommender/user_cf.py负责相似度矩阵计算与推荐候选生成recommender/item_cf.py负责物品相似度app.py只负责接收 HTTP 请求调用算法模块返回 JSON。这样做的目的是算法评估时可以单独跑脚本不依赖 Flask 或 Vue生产环境里也可以把推荐计算放到 Celery 异步任务里Web 服务只读 Redis 缓存。下面接口设计中我会把相似度矩阵的更新设计成“启动时计算 定时缓存”不在每个请求里重新计算。3. 使用 Flask 实现前后端分离推荐接口3.1 为什么选 Flask 而不是 FastAPI 或 Django在“前后端分离”这个项目里你需要的是一个轻量、能快速暴露 JSON 接口、同时允许你自由组织算法代码的 Web 框架。Django 自带 ORM 和 Admin但重量级较重FastAPI 支持异步和类型提示性能更好但很多教程里给出的协同过滤代码都是同步的FastAPI 的优势并不能充分发挥。Flask 的优势在于“零约束”你可以在同一个进程里既算矩阵又提供接口也可以把矩阵计算拆出去。对于课程设计或中小型推荐系统Flask 是最稳妥的选择。如果你确实想用 FastAPI后面的路由和响应结构几乎不用改只需把装饰器替换为app.get和app.post再加上 Pydantic 模型做参数校验即可。3.2 定义推荐接口的数据契约前后端分离最重要的一件事就是先定好 JSON 数据结构再写具体代码。下面给出一个推荐的电影接口响应示例{ code: 0, message: success, data: { user_id: 1, items: [ { movie_id: 101, title: 肖申克的救赎, score: 4.8, reason: 与你相似的用户 17, 23 看过 } ] } }这里用code: 0表示成功非 0 为错误码。reason字段给前端展示“推荐理由”用既能让页面显得更有说服力又方便你调试推荐结果是否符合直觉。前端拿到这个 JSON 后直接渲染卡片不需要知道后端是怎么算的。接口路径设计我一般这样约定GET /api/recommend/int:user_id获取推荐GET /api/movies/int:movie_id获取电影详情POST /api/rate提交用户评分。这样实现 Vue 路由时可以直接映射。3.3 用 Flask 暴露协同过滤结果from flask import Flask, jsonify, request from flask_cors import CORS from recommender.user_cf import load_ratings, user_similarity_matrix, recommend_for_user import pandas as pd app Flask(__name__) CORS(app, resources{r/api/*: {origins: *}}) # 启动时加载并计算相似度矩阵 RATINGS_DF load_ratings(data/ratings.csv) RATINGS_DF RATINGS_DF[RATINGS_DF.groupby(user_id)[user_id].transform(size) 5] SIM_MATRIX, PIVOT user_similarity_matrix(RATINGS_DF, methodpearson) app.route(/api/recommend/int:user_id, methods[GET]) def recommend(user_id): if user_id not in SIM_MATRIX.index: return jsonify({code: 1, message: user not found, data: None}), 404 movie_ids recommend_for_user(SIM_MATRIX, RATINGS_DF, user_id) movies [] for mid in movie_ids: movies.append({movie_id: mid, title: fmovie_{mid}, score: 0.0}) return jsonify({code: 0, message: success, data: {user_id: user_id, items: movies}}) app.route(/api/rate, methods[POST]) def rate_movie(): payload request.get_json() user_id payload.get(user_id) movie_id payload.get(movie_id) rating payload.get(rating) if not all([user_id, movie_id, rating]): return jsonify({code: 2, message: invalid params, data: None}), 400 # 实际项目中这里写入 MySQL并触发缓存失效 return jsonify({code: 0, message: success, data: None}) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)CORS(app, resources{...})配置了允许跨域访问。开发时 Vue 跑在localhost:8080Flask 跑在localhost:5000如果不配置 CORS浏览器会直接拦截请求。注意我把相似度矩阵放在模块全局变量里每次启动只算一次但用户新增评分后矩阵不会更新所以实际的系统里应该在rate_movie接口写库后删除 Redis 缓存让后台任务重算矩阵。这里没有使用debugTrue因为 Debug 模式会启用源码热加载在推荐场景中会让你每次请求都重新检查代码而影响性能。3.4 相似度矩阵更新与性能瓶颈当用户数到达上万时user_similarity_matrix里的点积计算需要的内存和耗时都会剧增。一种缓解办法是计算相似矩阵前先过滤掉行为少于 5 条的用户上面代码已经做了另一种更工业化的思路是使用 Spark 或 Faiss 做近邻搜索但在这个项目里没必要。你可以把计算好的矩阵用np.save或pandas.to_pickle存到本地文件启动时打个时间戳检查是否有新评分如果没有新数据就直接加载文件。推荐接口本身要控制在 200ms 内如果超过了优先检查相似度矩阵计算是否泄漏到请求路径中。4. Vue 前端实现电影推荐展示与前后端联调4.1 Vue 项目初始化与代理配置前端部分用 Vue 3 与 Vite 是当前最常见的组合但如果你是照着传统的课程设计在做Vue 2 Element UI 仍然可以跑。这里我按 Vue 3 Vite 来写组件结构与 Vue 2 差别不大。创建项目的命令是npm create vitelatest movie-front -- --template vue cd movie-front npm install npm install axios element-plus安装完成后在项目根目录创建一个.env.development文件写入VITE_API_BASE_URL/api。Vite 的开发服务器需要配置代理让/api开头的请求转发到 Flask 端口。这样 Vue 前端里可以直接写axios.get(/recommend/1)不会出现跨域报错。配置文件vite.config.js关键部分如下import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 8080, proxy: { /api: { target: http://localhost:5000, changeOrigin: true, // 后端接口本身就没有 /api 前缀时可以做重写 // rewrite: (path) path.replace(/^\/api/, ) } } } })这段配置让 Vite 在开发阶段把/api/recommend/1代理到http://localhost:5000/api/recommend/1。注意如果 Flask 里路由写的是/api/recommend/id就不需要 rewrite如果 Flask 写的是/recommend/id你就需要把 rewrite 打开。很多前后端分离联调失败就是因为少配了代理或没有理解代理的转发路径。4.2 Vue 页面调用推荐接口并处理 token实际的项目标题里提到“前后端分离 SQL 文件”但并没有特别写“登录”。实际系统一般会带一个简单登录注册这时候就需要在 Vue 里处理 token。常见做法是用户登录后后端返回 JWT前端存到 localStorage每次请求通过 axios 拦截器把 token 放到请求头中。import axios from axios import { ElMessage } from element-plus const service axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(access_token) if (token) { config.headers.Authorization Bearer ${token} } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 0) { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } return res }, error { ElMessage.error(网络异常) return Promise.reject(error) } )这段代码在请求发起前读取 localStorage 中的access_token响应拦截器根据code字段判断业务是否成功。这样recommend页面组件中直接调用service.get(/recommend/1)即可。这个 axios 封装写好后整个项目所有页面都复用一套 token 处理逻辑比在每个组件里手动写Authorization头干净得多。如果你后端使用的是 Flask-JWT-Extended返回的通常包含access_token与refresh_token前端在 401 时需要用 refresh_token 刷新令牌这里不做展开但你需要知道在拦截器里 401 分支写刷新逻辑。4.3 电影推荐卡片组件的实现Vue 页面结构通常分成顶部导航、推荐列表、评分弹窗。下面是最核心的一个推荐列表组件template div classmovie-recommend el-row :gutter16 el-col :span6 v-foritem in movieList :keyitem.movie_id el-card h4{{ item.title }}/h4 p推荐分{{ item.score.toFixed(2) }}/p p classreason{{ item.reason }}/p el-button typeprimary clickhandleRate(item)评分/el-button /el-card /el-col /el-row /div /template script setup import { ref, onMounted } from vue import { service } from ../utils/request const movieList ref([]) async function loadRecommend(userId) { const res await service.get(/recommend/${userId}) movieList.value res.data.items } function handleRate(item) { // 打开评分弹窗提交后刷新列表 console.log(rate movie, item.movie_id) } onMounted(() { const userId localStorage.getItem(user_id) || 1 loadRecommend(userId) }) /script这里使用的是 Vue 3 的script setup语法。注意item.score.toFixed(2)假设 score 是数字类型后端返回时不要把它序列化成字符串。播放 m3u8 之类的热词跟本场景没关系不需要在电影推荐界面硬凑视频播放功能如果你是做电影网站想要放映室资源可以在电影详情页单独加一个 video 组件但不要影响推荐主流程。4.4 跨域与 token 的常见坑开发和部署环境里最容易翻车的是三件事第一代理配了但后端 CORS 也开着导致重复的跨域头第二token 放在 localStorage 里存在 XSS 风险但你需要在论文里写清楚它的利弊第三前端打包后放在 Nginx 中Nginx 没有把/api的请求反向代理到 Flask页面能打开但数据加载不出来。下面是生产环境常见 Nginx 配置片段server { listen 80; server_name your-domain.com; root /var/www/movie-front/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }try_files $uri $uri/ /index.html这一行是 Vue Router 的 history 模式必需配置否则刷新页面时会 404。proxy_pass末尾带不带/含义不同http://127.0.0.1:5000;会把原始 URI 完整转发而http://127.0.0.1:5000/;会去掉/api前缀。你要根据自己的后端路由决定否则会出现路径多一段或少一段的问题。5. SQL 文件结构、数据初始化与推荐效果验证5.1 SQL 文件里需要哪几张表一个电影推荐系统的 SQL 文件至少要包含用户表、电影表、评分表。如果是基于 Tag 的冷启动策略还可以加电影类型表和用户偏好表。下面给出 MySQL 建表语句的骨架CREATE DATABASE IF NOT EXISTS movie_db DEFAULT CHARSET utf8mb4; USE movie_db; CREATE TABLE t_user ( user_id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password_hash VARCHAR(200) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE t_movie ( movie_id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(200) NOT NULL, genres VARCHAR(200), release_year INT, avg_score DECIMAL(3,1) DEFAULT 0.0 ) ENGINEInnoDB; CREATE TABLE t_rating ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, movie_id INT NOT NULL, rating TINYINT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_user_movie (user_id, movie_id), CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES t_user(user_id), CONSTRAINT fk_movie FOREIGN KEY (movie_id) REFERENCES t_movie(movie_id) ) ENGINEInnoDB;你需要把用户评分动作从算法演示扩展为一个真实的表。UNIQUE KEY uk_user_movie (user_id, movie_id)非常重要它保证一个用户对同一部电影只能有一条评分记录后端的INSERT ... ON DUPLICATE KEY UPDATE可以直接复用。TINYINT适合 1-5 分的评分范围DECIMAL(3,1)用来缓存平均分。索引方面评分表需要建立(user_id)和(movie_id)的普通索引否则推荐算法读取数据时会发生全表扫描在几十万条评分记录上就没法看了。5.2 用 SQL 文件生成初始化评分数据实际项目中SQL 文件里除了建表语句还需要准备一部分模拟数据才能让前后端一启动就有推荐效果。Twitter 或 GitHub 上常见的处理方式是导入 MovieLens 的 ratings.csv 后用LOAD DATA INFILE导入到 t_rating 中。但课程设计里往往要求直接提供一个movies.sql可以用下面这样的方式构造INSERT INTO t_movie (movie_id, title, genres, release_year) VALUES (1, 肖申克的救赎, Drama, 1994), (2, 这个杀手不太冷, Action|Crime|Drama, 1994), (3, 阿甘正传, Drama|Romance, 1994); INSERT INTO t_rating (user_id, movie_id, rating) VALUES (1, 1, 5), (1, 2, 4), (2, 1, 4), (2, 3, 5), (3, 2, 5), (3, 3, 4);这样构造数据的好处是可以直接人工验算推荐结果。比如用户 1 看过了电影 1 和 2UserCF 找到与它相似的用户 2因为用户 2 给电影 3 打了 5 分系统就会把电影 3 推荐给用户 1。这样的推荐结果在论文里解释时也更通顺你能画一张简单的表格说明每个相似用户对候选电影的贡献分怎么出来的。注意 SQL 文件里的评分分布要尽量模拟长尾现象少数热门电影有很多人评大多数冷门电影只有零星评分否则推荐算法就没太大发挥空间。5.3 后端如何读取 MySQL 数据而不是 CSV前面的 Python 代码演示用了read_csv真正接 MySQL 时需要改为 SQLAlchemy 读库。你可以在database.py里封装一个from sqlalchemy import create_engine, text import pandas as pd DB_URI mysqlpymysql://root:password127.0.0.1:3306/movie_db?charsetutf8mb4 engine create_engine(DB_URI) def load_ratings_from_db(): query text( SELECT user_id, movie_id, rating FROM t_rating WHERE rating IS NOT NULL ) return pd.read_sql(query, engine)如果你的 SQL 文件里存储的是 1-5 分rating列可以直接使用如果是隐式行为0/1你要在加载时决定是否把 1 转换成分数。参数说明create_engine的第一个参数是连接串pymysql是 Python 连接 MySQL 的驱动需要pip install pymysql sqlalchemy。使用text()查询能避免 SQL 注入风险但这里的查询条件全部是固定的没有用户参数所以不需要额外做参数化绑定。5.4 验证推荐系统效果的两个常用指标如果你只是把推荐结果展示出来论文里还可以附上离线评估把评分数据集按 8:2 切分训练集计算相似度矩阵测试集里移除每个用户最近的几条行为用推荐排序是否覆盖被移除的电影来评估。通常用 PrecisionK 和 RecallK 这两个指标。下面单独写一段评估数据和推荐排序的比对逻辑def precision_recall_k(predicted, held_out, k10): recommended set(predicted[:k]) real set(held_out) inter recommended real precision len(inter) / k recall len(inter) / len(real) if len(real) 0 else 0 return precision, recall当k增大时召回率一般会上升精确率下降。如果你看到精确率一直很低先检查是不是用户行为过于稀疏或者冷门电影根本没有机会被推荐。这时候一个常见补救办法是给相似度矩阵做“热门商品降权”也叫做 Inverse User Frequency即在计算相似度时给热门电影赋予更低的权重。你可以在算法模块里加一个参数popularity_penalty把热门电影的中心化评分乘以一个小于 1 的系数。这个调参过程可以写进论文的实验章节比堆各种深度学习模型要务实得多。5.5 让 SQL、推荐、Vue 在一条链路上跑通在一台开发机上把完整的项目拉起来步骤是这样先执行movie_db.sql初始化数据库然后编辑database.py里的数据库账号和密码运行python app.py看到 Flask 启动日志再npm run dev启动 Vue浏览器打开http://localhost:8080。如果页面报错按顺序查四层第一层看浏览器 Network 里请求返回的 HTTP 状态码是 401 还是 500第二层看 Flask 控制台有没有异常堆栈第三层看数据库是否能连上执行SELECT COUNT(*) FROM t_rating;第四层看 Vite 代理有没有生效直接在浏览器地址栏输入http://localhost:8080/api/recommend/1如果这里返回的是index.html说明代理没生效。最后一层往往最容易忽略Vue Router 会把/api/recommend/1当成前端路由处理必须确保代理规则在前面优先匹配。另外再补充一个约定项目里的文档说明和论文应该把 SQL 文件提交进来而不是只贴部分建表语句这样你拿到项目后可以一次性复现数据环境省掉很多手工造数据的时间。当你把相似度计算、Flask 接口、Vue 展示、MySQL 存储串联起来之后再回头改任何一个环节的细节比如把 UserCF 换成 ItemCF或者把相似度从皮尔逊换成余弦就只动半个算法文件剩下的接口和前端完全可以复用。这套“算法逻辑独立、接口契约固定、前端只管渲染”的架构才是标题中前后端分离真正想要传达的工程价值。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门