简介基于协同过滤的电影推荐系统毕业设计资源适合计算机专业学生用于课程设计、毕业设计或推荐系统入门学习。系统完整实现了基于用户与基于物品两种协同过滤算法后端使用Django框架搭配Python环境支持MySQL与SQLite数据库切换前端采用Bootstrap并使用Movielens公开数据集完成训练与演示。压缩包共83个文件大小约8.41MB文件类型以Python源码、HTML模板、CSV评分数据为主另含PDF论文、SQLite数据库、豆瓣爬虫脚本、依赖清单及详细readme文档目录结构清晰便于直接运行和二次开发。目前已有10610人学习下载适合毕业设计参考及推荐系统实战练习。附带的爬虫和评分填充脚本可采集豆瓣电影数据、生成个性化输入readme对运行环境、依赖安装、配置修改有详细说明能帮助从零搭建可在线预览的电影推荐平台并支持扩展算法比较或界面优化。1. 为什么把“协同过滤电影推荐”落在 Django 2.2.1 上A同学把毕设选题交上去时某导师只问了一句你打算怎么把它做出来这句话问住了不少人。网上搜到的电影推荐系统要么只讲算法不落代码要么做了个网页但推荐逻辑是摆设。我建议他把“基于协同过滤的电影推荐系统”放在 python3.7 django2.2.1 mysql 这套固定组合上算法用协同过滤避开深度学习这种自己都讲不透的黑匣子数据库用 MySQL 存用户、电影、评分三类数据Django 负责把注册、打分、展示推荐结果的完整链路串起来。这套方案适合想在毕设或课设里真正跑通全流程的人——能运行、能演示、能讲清楚每一步而不是交一堆没人看的代码。2. 总体架构设计Django 项目布局与四个核心模块的职责划分2.1 MTV 分层与请求流转逻辑Django 的 MTVModel-Template-View模式是这个项目的骨架。浏览器发起请求后根路由 urls.py 先做分发把地址交给对应应用的路由路由再调用视图函数视图里读写数据库、计算推荐算法最后把结果塞给模板渲染成页面。链路不长但模块边界一旦模糊后期改任何功能都会连坐。我搭这类项目时有个习惯算法逻辑绝对不写在 views.py 里单独放一个 algorithms.py。原因有两个一是算法会被多个视图复用比如推荐页用、电影详情页的“相似推荐”也用二是答辩或给导师讲代码时核心算法单独一个文件远比“打开视图找一段缩进很深的循环”好讲得多。2.2 四个核心模块的职责划分按 Django 的 app 概念把项目拆成 users、movies、ratings、recommend 四个应用每个应用只管自己那一块。应用核心功能涉及的模型/文件users注册、登录、退出User 表、sessionmovies电影列表、详情、搜索Movie 表、模板渲染ratings提交评分、我的评分记录Rating 表、表单处理recommend相似度计算、推荐结果组装algorithms.py、推荐视图划分原则是“谁的数据谁负责”。ratings 只负责评分的新增和查询不做推荐recommend 只读评分数据不写数据。这样职责单一出了问题直接定位到某个 app不用在一个文件里上下翻。2.3 项目目录结构与配置要点movie_recommend/ ├── manage.py ├── movie_recommend/ │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── users/ │ ├── models.py │ ├── views.py │ └── urls.py ├── movies/ │ ├── models.py │ ├── views.py │ └── urls.py ├── ratings/ │ ├── models.py │ ├── views.py │ └── urls.py └── recommend/ ├── algorithms.py ├── views.py └── urls.pysettings.py 里有两个配置最容易忽略。一是 INSTALLED_APPS 必须把四个 app 全部注册漏掉一个就报No module named之类的错二是数据库连接要写成 MySQL不能保留默认的 sqliteDATABASES { default: { ENGINE: django.db.backends.mysql, NAME: movie_db, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, } }这段配置决定了 Django 的 orm 把模型映射到哪台数据库。NAME 对应 MySQL 里提前建好的库如果没建库就直接 runserverDjango 只会报一个连接失败不会帮你自动建库。通常的做法是先手动执行CREATE DATABASE movie_db DEFAULT CHARSET utf8mb4;再跑迁移否则后续中文数据全是乱码。3. MySQL 数据库设计五张核心表与评分数据的建模方式3.1 表结构设计与字段说明这个项目不需要复杂的表核心就三类信息用户、电影、评分。按照“事实用行存”的原则把每次打分作为一条记录放到评分表里不要在一个用户字段里塞一长串电影 id那是给内存用的不是给关系型数据库用的。表名字段类型说明usersidINT 自增主键用户编号usersusernameVARCHAR(50) 唯一登录名userspasswordCHAR(64)存哈希值不存明文moviesidINT 自增主键电影编号moviestitleVARCHAR(200)电影标题moviesgenresVARCHAR(100)类型多类型用竖线分隔moviesrelease_yearSMALLINT上映年份ratingsidINT 自增主键评分记录编号ratingsuser_idINT 外键关联 users.idratingsmovie_idINT 外键关联 movies.idratingsscoreTINYINT评分值范围 1–5ratingscreated_atDATETIME评分时间3.2 建表语句与约束设计CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password CHAR(64) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE movies ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(200) NOT NULL, genres VARCHAR(100), release_year SMALLINT, poster_url VARCHAR(500) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE ratings ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, movie_id INT NOT NULL, score TINYINT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_movie (user_id, movie_id), CONSTRAINT fk_rating_user FOREIGN KEY (user_id) REFERENCES users(id), CONSTRAINT fk_rating_movie FOREIGN KEY (movie_id) REFERENCES movies(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有个细节值得在答辩时主动讲UNIQUE KEY uk_user_movie是联合唯一约束确保同一个用户对同一部电影只能有一条评分记录。用户反复点打分时Django 侧可以先查再更新也可以靠这个约束兜底否则评分表会出现一条电影被同一个人刷 5 次的脏数据最终污染相似度计算。score 用 TINYINT 而不是 INT是因为评分范围只有 1 到 5TINYINT 占 1 字节配合索引查询更快。外键约束保证了一点删用户时评分记录不会变成孤儿数据。但要注意删用户的操作在 Web 端往往不开放因为评分数据是推荐系统的原料删了就没有可算的东西了。3.3 公开数据集导入与数据校验本地做测试时通常的做法是导入公开电影数据集比如 MovieLens 的 ml-latest-small 版本里面大概有近万部电影和十万条左右评分对毕设来说规模刚刚好。导入前先看原始 CSV 的表头再写一次性脚本import csv import pymysql connection pymysql.connect( host127.0.0.1, userroot, passwordyour_password, databasemovie_db, charsetutf8mb4 ) cursor connection.cursor() with open(movies.csv, encodingutf-8) as f: reader csv.reader(f) next(reader) # 跳过表头 for row in reader: sql INSERT INTO movies (id, title, genres) VALUES (%s, %s, %s) cursor.execute(sql, (int(row[0]), row[1], row[2])) connection.commit() cursor.close() connection.close()为什么要保留原始数据集里的 id因为 ratings.csv 里关联的 movie_id 和 movies.csv 是同一套编号。如果自己重新自增生成 id评分数据的关联就全断了。导入完跑一条校验 SQLSELECT COUNT(*) FROM ratings;和原始数据行数做对比差了就说明有重复键被跳过或者外键失败。4. 协同过滤算法落地余弦相似度与两种推荐策略的实现4.1 从评分矩阵构建开始协同过滤算法的输入是“用户-物品”评分矩阵。行是用户列是电影值是我们打的分数。矩阵在 Python 里的最直观做法是用 pandas 的透视表import pandas as pd import pymysql def load_rating_matrix(): connection pymysql.connect( host127.0.0.1, userroot, passwordyour_password, databasemovie_db, charsetutf8mb4 ) sql SELECT user_id, movie_id, score FROM ratings df pd.read_sql(sql, connection) matrix df.pivot_table( indexuser_id, columnsmovie_id, valuesscore ) matrix matrix.fillna(0) connection.close() return matrixpivot_table会把缺失的评分变成 NaNfillna(0)让矩阵变成稠密矩阵方便算余弦相似度。这一步在设计上有个权衡原始评分表是稀疏的一个人一辈子看不了几千部电影直接算矩阵会占内存。毕设级数据量这么干没问题但要在答辩时说清楚“这是实验环境做法生产环境会改用稀疏矩阵存储”。4.2 基于用户的协同过滤UserCFUserCF 的思路是“和你口味相似的人喜欢什么你也大概率喜欢”。实现步骤分成三块算用户间相似度、找最近邻、加权汇总候选电影得分。import numpy as np from sklearn.metrics.pairwise import cosine_similarity def user_based_recommend(user_id, top_n10, k20): matrix load_rating_matrix() # step1: 计算用户间余弦相似度 user_sim cosine_similarity(matrix) user_sim_df pd.DataFrame( user_sim, indexmatrix.index, columnsmatrix.index ) # step2: 取目标用户最相似的 k 个用户排除自己 sim_users user_sim_df.loc[user_id].sort_values( ascendingFalse ).iloc[1:k1] # step3: 候选电影 邻居评过、目标用户没评过的加权累计 user_rated set(matrix.loc[user_id][matrix.loc[user_id] 0].index) candidates {} for other_user, sim in sim_users.items(): other_ratings matrix.loc[other_user][matrix.loc[other_user] 0] for movie_id, score in other_ratings.items(): if movie_id not in user_rated: candidates[movie_id] candidates.get(movie_id, 0) sim * score ranked sorted(candidates.items(), keylambda x: x[1], reverseTrue) return ranked[:top_n]注释里已经把三步标清楚了。k 是邻居数量取 20 的意思是不管全局有多少用户只参考最像的 20 个人。这个值不是越大越好太大了容易混入相似度低的人反而拉低推荐质量。候选电影的加权分直接用“相似度 × 邻居评分”累加没有做归一化因为我们要的是排序结果不是精确的预测分值不归一化不影响 top_n。4.3 基于物品的协同过滤ItemCFItemCF 换了个角度如果用户给《教父》打了高分那么和《教父》最相似的《疤面煞星》也值得推荐。实现上把矩阵转置一下让每行变成电影每列变成用户def item_based_recommend(user_id, top_n10): matrix load_rating_matrix().T # 转置成 电影-用户 矩阵 item_sim cosine_similarity(matrix) item_sim_df pd.DataFrame( item_sim, indexmatrix.index, columnsmatrix.index ) user_rated matrix[user_id][matrix[user_id] 0] scores {} for movie_id, score in user_rated.items(): # 与当前电影最相似的 10 部 sim_items item_sim_df.loc[movie_id].sort_values( ascendingFalse ).iloc[1:11] for other_movie, sim in sim_items.items(): if other_movie not in user_rated: scores[other_movie] scores.get(other_movie, 0) sim * score ranked sorted(scores.items(), keylambda x: x[1], reverseTrue) return ranked[:top_n]这段代码里的 “每部电影只看最相似的 10 部” 是一个实用主义剪枝。如果不剪枝每次推荐要把矩阵里所有电影都遍历一遍算得慢不说低相似度的噪声还会混进来。ItemCF 在电影数量远小于用户数量时性能通常比 UserCF 好因为物品之间的相似度可以提前离线算好用户访问推荐页时直接查结果。4.4 混合策略与参数调整两套算法单跑都有明显短板。UserCF 在用户量少时相似度不可靠ItemCF 对冷门电影的覆盖不够。通常的做法是做一个简单的混合开关默认走 ItemCF当目标用户的评分数少于 5 条时走 UserCF再少就直接给热门榜兜底。def recommend_for_user(user_id, top_n10): matrix load_rating_matrix() user_rated_count (matrix.loc[user_id] 0).sum() if user_rated_count 5: return item_based_recommend(user_id, top_n) elif user_rated_count 1: return user_based_recommend(user_id, top_n) else: return hot_movies(top_n) # 按平均分和评分人数排序这个分支逻辑在答辩时非常加分因为老师最常问的就是“冷启动怎么办”。把冷启动策略写成代码比嘴上说“我们用热门推荐兜底”有说服力得多。调参时k、top_n、相似物品数量建议在 settings.py 里定义成常量方便后面做实验对比不需要每次改代码里的魔法数字。5. 常见问题排查环境搭建、数据稀疏与冷启动的避坑经验5.1 mysqlclient 编译失败卡住毕设第一天的经典坑现象Windows 上执行pip install mysqlclient直接报错末尾一行是error: Microsoft Visual C 14.0 is required。原因Python 3.7 时代 mysqlclient 经常没有现成的 wheel 包需要本地编译而编译依赖 VC 构建工具。很多同学在这一步就放弃了误以为代码有问题其实是环境问题。解决跑pip install pymysql然后在 Django 项目的__init__.py里加两行import pymysql pymysql.install_as_MySQLdb()pymysql 是纯 Python 实现不需要编译装完就能当 MySQLdb 用。这个手段适合项目快速落地生产环境建议还是用 mysqlclient但毕设阶段没必要在编译环境上耗时间。5.2 中文乱码建表时一个字符集决定成败现象电影标题里的中文在页面上显示成问号或者直接变成一组乱码。检查 MySQL 命令行发现数据本身存进去就是乱码。原因建库时没指定字符集MySQL 用了默认的 latin1存不了中文。等到发现再改表里的数据已经坏了删了重建又要重导一遍属于典型的“前期偷懒后期翻车”。解决建库时执行CREATE DATABASE movie_db DEFAULT CHARSET utf8mb4;同时把 Django 连接字符串里的 charset 也带上。命令行下还不行就加一句SET NAMES utf8mb4;再操作。这套组合下来基本不会再看到乱码。5.3 全量相似度计算内存溢出现象数据集导入后跑item_based_recommend直接内存飙升电脑风扇狂转甚至进程被杀掉。原因余弦相似度对 n 部电影要生成 n×n 的相似度矩阵。电影数上万后这个矩阵光存储就接近 GB 级普通笔记本扛不住。解决不预先算全量矩阵改成惰性计算只算用户评分过的电影与其他电影的相似度同时在算相似度时加上“只保留前 50 个近邻”的剪枝逻辑。这一步能把时间复杂度从 O(n²) 降到 O(评分过的电影数×候选电影数)数据量大的情况下效果立竿见影。5.4 评分矩阵 0 填充带来的“伪相似”现象两个用户明明没有任何共同评分但算法算出来的相似度不为 0甚至偏高。推荐结果出现得莫名其妙。原因fillna(0)把“没看过”和“打了 0 分”混为一谈了。余弦相似度把 0 当成负向评分于是没看过的电影全部变成 0反而拉近了向量方向。解决评分体系里禁止出现 0 分然后计算相似度时只统计双方都有评分的维度或者对评分做中心化处理——每个用户减去自己的平均分让 0 代表“用户的中性偏好”。我在项目里用的是“只统计共同评分的电影”的方式因为实现最简单讲起来也直白。5.5 冷启动新用户打开推荐页只有一个空列表现象新注册的用户没有任何评分recommend_for_user走到最后返回空列表页面一片空白观感上像个 bug。原因评分矩阵里新用户的行全是 0和任何老用户的相似度都低取不到可靠邻居。解决按 4.4 的分支处理评分数为 0 时走热门榜。热门榜不要只看平均分否则一部电影只有一个人打了 5 分就会排到第一。我一般用加权分平均分乘以评分人数占比再加一个常数平滑避免冷门电影刷榜。这个细节解释给导师听对方立刻知道你懂推荐系统的常见陷阱。6. 验收与答辩演示路径设计与高频追问的应答准备6.1 演示脚本五分钟走完一条验收路径答辩演示不要临时点来点去找功能按固定脚本走既省时间又不容易出事故。我推荐的五分钟路径是注册一个新账号进入推荐页截图当前的热门榜兜底推荐随便挑三部电影打 5 分再刷一次推荐页看结果是否变化打开 MySQL 的 ratings 表展示刚才的三条评分记录存在证明数据真实落库切换到“基于用户”和“基于物品”两个标签页对比推荐列表的差异顺带说一句各自原理。这段脚本覆盖了 Web 基本功能、数据库、算法三个维度每一项都是答辩时可能被追问的点主动演示完老师再提问就有了上下文。6.2 高频追问与应答要点老师提问应答要点协同过滤原理是什么用一句话讲明白把用户的历史评分当成向量用余弦距离找相似用户或相似物品再按加权分排序取 top N为什么不用深度学习数据量不够深度学习容易过拟合且毕设强调可解释协同过滤每一条推荐都能讲出依据相似度为什么选余弦余弦只关注向量方向不受用户打分习惯影响比如有人全场打 3 分、有人全场打 5 分余弦能消除这种尺度偏差怎么证明推荐有效离线做留一法评估每次拿掉用户的一条真实评分看推荐列表前 10 里是否包含它统计命中率留一法可以在答辩前写一个轻量脚本跑出数值不需要做完整评测集取 100 个用户各留 1 条评分即可。就算精度只有百分之十几也远比口说“效果不错”强因为你有数字。有一次验收前夜我发现演示机上的 MySQL 还是 latin1页面标题全是问号整个推荐列表看起来像乱码。那次确实狼狈改表重导数据搞到凌晨。从那以后我每次拿到一个 Django 项目部署完环境第一件事就是强制走一遍“注册、打分、推荐、查库”四步不跑通不罢休。这条习惯帮我避掉了至少三次现场事故希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?