ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Python的电影推荐系统毕业设计:协同过滤算法与MySQL数据库实战

基于Python的电影推荐系统毕业设计:协同过滤算法与MySQL数据库实战 简介这是一套基于Python的电影推荐系统毕业设计完整源码与数据库面向计算机、通信、人工智能、自动化等专业的学生与教师可用于毕业设计、课程大作业或期末项目参考。项目已通过调试测试答辩评审分达98分基础较好的学习者还能在此基础上修改扩展实现个性化推荐等不同功能。资源包共726个文件约19.54MB涵盖39个py后端逻辑文件、41个vue与164个js前端组件、53个css样式、2个sql数据库脚本及若干html、json、md说明文档前后端结构完整便于按模块阅读与二次开发。目前已有239人浏览学习具备一定参考热度。整体代码分层清晰推荐算法、用户管理、影片数据与前端交互均有对应实现适合作为从需求分析到系统落地的完整学习范本帮助读者快速理解推荐系统架构与工程组织方式。1. 电影推荐系统毕业设计从协同过滤到可运行源码的完整落地路径很多同学做毕业设计时选题定了“基于 Python 的电影推荐系统”结果卡在第一步——推荐算法到底选哪个、数据库怎么建、前后端怎么串起来。这个标题背后其实是一条完整的工程链路数据采集与清洗、推荐算法选型与实现、数据库表结构设计、Web 服务搭建、前后端联调。它解决的核心问题是给定用户对电影的评分行为预测用户可能喜欢但还没看过的电影并按推荐分数排序输出。适合正在做计算机毕业设计、需要一套能跑通、能答辩、能写论文的本科生也适合想入门推荐系统但不知道从哪下手的新手。我见过太多人一上来就搞深度学习结果连协同过滤的矩阵都没构造对血泪经验告诉我们先把基于用户的协同过滤跑通再谈其他。2. 推荐算法选型为什么协同过滤仍然是毕业设计的首选2.1 协同过滤、内容推荐与混合推荐的适用边界推荐系统的算法大致分三类。协同过滤Collaborative Filtering只依赖用户-物品交互矩阵不关心电影本身是什么类型、谁演的核心假设是“相似的人喜欢相似的东西”。基于内容的推荐Content-Based依赖物品特征比如把电影的类型、导演、演员做成特征向量再匹配用户历史偏好。混合推荐则是把两者加权或级联。对于毕业设计协同过滤是最稳妥的起点。原因有三第一MovieLens 数据集天然就是用户-评分-电影的三元组直接能构造矩阵第二算法逻辑清晰论文里好写公式答辩时好解释第三代码量可控一个基于用户的协同过滤核心逻辑不到 100 行。内容推荐需要额外处理电影元数据特征工程一展开就容易失控。混合推荐听起来高级但调权重的过程在论文里很难说清楚除非你有大量实验对比。我一般会建议主算法用基于用户的协同过滤UserCF或基于物品的协同过滤ItemCF论文里再补一个基于内容的推荐做对比实验这样既有深度又有广度。2.2 用 Python 实现基于用户的协同过滤核心逻辑下面这段代码是 UserCF 的最小可运行版本输入是用户-电影评分字典输出是目标用户的 TopN 推荐列表。import math from collections import defaultdict def load_ratings(): # 模拟 MovieLens 格式{user_id: {movie_id: rating}} return { u1: {m1: 5, m2: 3, m3: 4}, u2: {m1: 4, m2: 5, m4: 2}, u3: {m2: 2, m3: 5, m4: 4}, u4: {m1: 5, m3: 3, m4: 4}, } def cosine_similarity(user_a, user_b): # 只计算共同评分过的电影 common set(user_a.keys()) set(user_b.keys()) if not common: return 0.0 dot sum(user_a[m] * user_b[m] for m in common) norm_a math.sqrt(sum(user_a[m] ** 2 for m in common)) norm_b math.sqrt(sum(user_b[m] ** 2 for m in common)) if norm_a 0 or norm_b 0: return 0.0 return dot / (norm_a * norm_b) def recommend(target_user, ratings, top_n3, sim_k2): # 1. 计算目标用户与其他用户的相似度 sims [] for other in ratings: if other target_user: continue sim cosine_similarity(ratings[target_user], ratings[other]) if sim 0: sims.append((other, sim)) # 2. 取最相似的 K 个用户 sims.sort(keylambda x: x[1], reverseTrue) neighbors sims[:sim_k] # 3. 加权预测目标用户对未看电影的评分 scores defaultdict(float) sim_sum defaultdict(float) for neighbor, sim in neighbors: for movie, rating in ratings[neighbor].items(): if movie not in ratings[target_user]: scores[movie] sim * rating sim_sum[movie] sim # 4. 归一化并排序 predictions [] for movie in scores: if sim_sum[movie] 0: predictions.append((movie, scores[movie] / sim_sum[movie])) predictions.sort(keylambda x: x[1], reverseTrue) return predictions[:top_n] if __name__ __main__: data load_ratings() result recommend(u1, data, top_n2, sim_k2) print(推荐结果:, result)逻辑说明cosine_similarity只对两个用户共同评分的电影计算余弦相似度避免把未评分当成 0 分导致偏差。recommend先找相似邻居再用相似度加权邻居的评分来预测目标用户对未看电影的分数。参数top_n控制返回几条推荐sim_k控制参与预测的邻居数量。sim_k设太小容易受个别用户影响设太大又会引入不相似的用户噪声一般取 5 到 20 之间小数据集取 2 到 5 即可。2.3 相似度计算与邻居选择的参数调优余弦相似度不是唯一选择。皮尔逊相关系数会减去用户平均分能缓解不同用户打分尺度不一致的问题——有人习惯全打 4 分以上有人只打 1 到 3 分。调整余弦相似度Adjusted Cosine则从物品角度减去平均分更适合 ItemCF。在毕业设计里我建议至少对比两种相似度在论文里放一张对比表。邻居数量 K 的调优可以用离线实验把数据集按 8:2 切成训练集和测试集在训练集上算相似度在测试集上算 RMSE 或 PrecisionN。K 从 5 试到 50画一条曲线选拐点。这个过程写进论文就是完整的实验设计。注意如果用户-电影矩阵非常稀疏MovieLens 100K 的稀疏度超过 93%UserCF 的相似度计算会大量为 0此时 ItemCF 通常更稳定因为物品之间的共同评分用户更多。3. 数据库设计MySQL 表结构与 Python 连接池配置3.1 电影推荐系统需要的五张核心表数据库不是把 CSV 导进去就完事。推荐系统需要支持用户管理、电影信息、评分记录、推荐结果缓存四类数据。下面是我在毕业设计里常用的表结构用 MySQL 8.0 实现。-- 用户表 CREATE TABLE users ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password_hash VARCHAR(128) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 电影表 CREATE TABLE movies ( movie_id INT PRIMARY KEY, title VARCHAR(200) NOT NULL, genres VARCHAR(200), release_year INT, avg_rating DECIMAL(3,2) DEFAULT 0.00 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 评分表核心交互数据 CREATE TABLE ratings ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, movie_id INT NOT NULL, rating DECIMAL(2,1) NOT NULL CHECK (rating 0.5 AND rating 5.0), rated_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_movie (user_id, movie_id), INDEX idx_movie (movie_id), FOREIGN KEY (user_id) REFERENCES users(user_id), FOREIGN KEY (movie_id) REFERENCES movies(movie_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 推荐结果缓存表 CREATE TABLE recommendations ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, movie_id INT NOT NULL, score DECIMAL(6,4) NOT NULL, generated_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_score (user_id, score DESC), FOREIGN KEY (user_id) REFERENCES users(user_id), FOREIGN KEY (movie_id) REFERENCES movies(movie_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 用户行为日志表可选用于论文里的冷启动分析 CREATE TABLE user_logs ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INT, action VARCHAR(20), movie_id INT, log_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;ratings表的UNIQUE KEY uk_user_movie保证一个用户对同一部电影只能有一条评分避免重复数据污染矩阵。recommendations表加INDEX idx_user_score是为了查询某个用户的推荐列表时能直接走索引排序不用全表扫描。user_logs表在论文里可以用来分析用户活跃度与推荐效果的关系属于加分项。3.2 用 SQLAlchemy 连接 MySQL 并批量导入 MovieLens 数据Python 连接 MySQL 常见做法是 SQLAlchemy 加 PyMySQL 驱动。下面这段代码完成三件事建连接池、批量插入电影和评分数据、验证导入结果。from sqlalchemy import create_engine, text from sqlalchemy.orm import sessionmaker import pandas as pd # 连接池配置pool_size 控制常驻连接数max_overflow 控制峰值溢出 engine create_engine( mysqlpymysql://root:passwordlocalhost:3306/movie_rec?charsetutf8mb4, pool_size5, max_overflow10, pool_recycle3600, echoFalse ) Session sessionmaker(bindengine) session Session() def import_movies(csv_path): df pd.read_csv(csv_path) # MovieLens 的 genres 用 | 分隔转成逗号方便展示 df[genres] df[genres].str.replace(|, ,) records df[[movieId, title, genres]].rename( columns{movieId: movie_id} ).to_dict(records) session.execute( text(INSERT IGNORE INTO movies (movie_id, title, genres) VALUES (:movie_id, :title, :genres)), records ) session.commit() print(f导入电影 {len(records)} 条) def import_ratings(csv_path): df pd.read_csv(csv_path) records df[[userId, movieId, rating]].rename( columns{userId: user_id, movieId: movie_id} ).to_dict(records) # 分批插入避免单次 SQL 过大 batch_size 5000 for i in range(0, len(records), batch_size): session.execute( text(INSERT IGNORE INTO ratings (user_id, movie_id, rating) VALUES (:user_id, :movie_id, :rating)), records[i:ibatch_size] ) session.commit() print(f导入评分 {len(records)} 条) if __name__ __main__: import_movies(ml-latest-small/movies.csv) import_ratings(ml-latest-small/ratings.csv) count session.execute(text(SELECT COUNT(*) FROM ratings)).scalar() print(f数据库评分总数: {count})参数说明pool_size5表示连接池保持 5 个连接max_overflow10允许高峰期额外创建 10 个连接pool_recycle3600让连接每小时回收一次防止 MySQL 的wait_timeout断开空闲连接。批量插入时batch_size设 5000 是经验值太小会导致频繁提交太大可能触发max_allowed_packet限制。INSERT IGNORE配合唯一索引可以跳过重复数据适合反复调试时使用。3.3 评分矩阵的 SQL 查询与内存加载策略推荐算法需要把评分数据加载成矩阵。数据量小的时候可以全量加载到内存数据量大就要分页或只加载活跃用户。-- 查询评分数量最多的前 500 个用户用于构建训练矩阵 SELECT user_id, movie_id, rating FROM ratings WHERE user_id IN ( SELECT user_id FROM ratings GROUP BY user_id HAVING COUNT(*) 20 ORDER BY COUNT(*) DESC LIMIT 500 );这个查询先用子查询找出评分次数不少于 20 次的活跃用户再取前 500 个。HAVING COUNT(*) 20是为了过滤掉只评了一两部电影的噪声用户他们的相似度计算没有统计意义。在 Python 里用pd.read_sql执行这个查询得到 DataFrame 后用pivot_table转成用户-电影矩阵缺失值填 0 或 NaN 取决于算法。提示如果 MySQL 的sql_mode开启了ONLY_FULL_GROUP_BY子查询里的ORDER BY COUNT(*)可能报错改成ORDER BY COUNT(user_id) DESC即可。4. 避坑与排查毕业设计里最容易翻车的五个地方4.1 现象推荐结果全是已经看过的电影原因预测评分时没有过滤掉目标用户已经评分过的电影。协同过滤的预测逻辑是对“未评分”物品打分如果代码里直接对全部电影排序已看过的电影因为相似邻居评分高会排在前面。解决在生成推荐列表前加一层过滤if movie not in target_user_ratings。上面第 2 章的代码里已经做了这个判断但很多同学自己写的时候会漏掉。4.2 现象MySQL 导入中文电影名变成乱码原因建表时字符集用了latin1或者连接字符串没指定charsetutf8mb4。MovieLens 的movies.csv里有非 ASCII 字符比如法语、西班牙语片名。解决建表语句统一用DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ciSQLAlchemy 连接串加?charsetutf8mb4。已经建错表的用ALTER TABLE movies CONVERT TO CHARACTER SET utf8mb4;修复。4.3 现象相似度计算报 ZeroDivisionError原因两个用户没有共同评分的电影common集合为空norm_a或norm_b为 0除法直接崩。解决在cosine_similarity开头判断if not common: return 0.0并且检查norm_a 0 or norm_b 0的情况。这个坑在稀疏数据集上几乎必然遇到不加判断程序跑不完。4.4 现象推荐接口响应超过 5 秒原因每次请求都重新计算全量用户的相似度矩阵。MovieLens 100K 有 600 多个用户两两计算相似度是 O(n²)每次请求都算一遍数据库和 CPU 都扛不住。解决把相似度矩阵离线计算好存到recommendations表或 Redis 里接口只做查询。离线任务可以用定时脚本每天跑一次或者手动触发。毕业设计里用 MySQL 存推荐结果就够了不必上 Redis。4.5 现象论文里的 RMSE 和实际推荐效果对不上原因RMSE 衡量的是评分预测误差但推荐系统实际关心的是 TopN 推荐列表里有多少是用户真正喜欢的。两者优化目标不一致RMSE 低不代表推荐列表好。解决论文里同时报告 RMSE 和 Precision10、Recall10。Precision10 是推荐的前 10 部电影里用户实际评分高于 4 分的比例Recall10 是用户所有高分电影里被推荐出来的比例。两个指标一起看才能说明推荐效果。5. 从离线评估到 Web 演示Flask 接口与推荐结果缓存5.1 用 Flask 暴露推荐接口的最小实现毕业设计答辩时老师通常要求现场演示。一个最简单的 Flask 接口就能把推荐结果展示出来。from flask import Flask, jsonify, request from sqlalchemy import create_engine, text app Flask(__name__) engine create_engine( mysqlpymysql://root:passwordlocalhost:3306/movie_rec?charsetutf8mb4, pool_size3, max_overflow5 ) app.route(/recommend/int:user_id) def get_recommend(user_id): top_n request.args.get(top_n, 10, typeint) with engine.connect() as conn: rows conn.execute( text( SELECT r.movie_id, m.title, r.score FROM recommendations r JOIN movies m ON r.movie_id m.movie_id WHERE r.user_id :uid ORDER BY r.score DESC LIMIT :n ), {uid: user_id, n: top_n} ).fetchall() result [ {movie_id: row[0], title: row[1], score: float(row[2])} for row in rows ] return jsonify({user_id: user_id, recommendations: result}) if __name__ __main__: app.run(debugTrue, port5000)这个接口直接从recommendations表读缓存结果不在请求时做算法计算。top_n参数通过 URL 查询字符串传入默认 10 条。返回 JSON 格式前端用 fetch 或 axios 调用即可。debugTrue只在开发时用部署时要关掉。5.2 离线推荐任务与在线接口的衔接方式离线任务负责算推荐结果并写入recommendations表。下面是一个定时执行的脚本框架。import schedule import time from datetime import datetime def generate_recommendations(): # 1. 从数据库加载评分矩阵 # 2. 计算用户相似度 # 3. 为每个活跃用户生成 TopN 推荐 # 4. 清空旧推荐批量写入新推荐 print(f[{datetime.now()}] 推荐任务执行完成) # 每天凌晨 2 点执行 schedule.every().day.at(02:00).do(generate_recommendations) if __name__ __main__: generate_recommendations() # 启动时先跑一次 while True: schedule.run_pending() time.sleep(60)衔接的关键是在线接口只读recommendations表离线任务只写这张表。两者通过数据库解耦互不阻塞。答辩演示时先手动跑一次离线任务再启动 Flask就能看到推荐结果。5.3 用 Precision10 验证推荐列表是否值得展示离线评估不能只看 RMSE。下面这段代码计算 Precision10用来判断推荐列表里有多少是用户真正喜欢的。def precision_at_k(recommendations, ground_truth, k10): # recommendations: [(movie_id, score), ...] # ground_truth: {movie_id, ...} 用户实际高分电影集合 top_k [movie for movie, _ in recommendations[:k]] hits len(set(top_k) ground_truth) return hits / k # 示例用户 u1 的推荐列表和实际高分电影 recs [(m4, 4.8), (m5, 4.5), (m6, 4.2)] truth {m4, m7, m8} print(fPrecision3: {precision_at_k(recs, truth, k3):.2f})precision_at_k的逻辑是取推荐列表前 K 个看有多少落在用户实际高分集合里。这个指标比 RMSE 更贴近推荐场景。论文里可以画一张表对比 UserCF 和 ItemCF 在不同 K 值下的 Precision10说明算法选择的依据。我自己的习惯是先把离线指标跑出来再启动 Web 接口人工看几条推荐结果。如果离线指标不错但人工看觉得离谱通常是数据里混了噪声用户或者电影 ID 映射错了。这个交叉验证的习惯帮我省了很多后悔药。希望帮到你。本文还有配套的精品资源点击获取
返回列表