简介:基于协同过滤的电影推荐系统毕业设计,是一套面向计算机相关专业毕业生及Django推荐系统初学者的完整项目资料。系统基于Django 2.2与Python 3.7搭建,支持MySQL/SQLite数据库,并用Bootstrap 3完成前端页面;项目中同时实现了基于用户(UserCF)和基于物品(ItemCF)两种协同过滤算法,在MovieLens数据集上完成训练与推荐,配有在线预览地址及完整的论文、技术文档。
压缩包共83个文件,大小仅8.41MB。主体为Python源码py和Django模板html,另有CSV数据集、SQLite数据库、PDF论文、readme说明文档等,pyc为编译缓存,整体目录结构清晰,便于按功能模块查阅与二次开发,从模型、视图到模板均有迹可循。
学习者可获得毕业论文PDF、详细readme与运维说明、电影数据导入脚本、豆瓣爬虫脚本及评分填充脚本,能对照理解协同过滤从数据清洗、特征处理到推荐输出的完整链路;用户管理、电影展示与推荐模块均有对应实现,适合课程设计、毕业设计或面试项目复盘。目前已有10610人学习下载。
1. 基于协同过滤的电影推荐系统:一份能跑通毕设的 Django 全栈参考
最近帮 A同学调一个基于协同过滤的电影推荐系统,用的是 Python 3.7 + Django 2.2.1 + MySQL,一看就是典型的本科毕业设计工程量级。虽然界面朴素了点,但里面该有的东西一样不少:前台用户看电影列表、电影详情、评分、收藏,后台管理电影/标签/用户数据,推荐模块则藏着两套协同过滤实现——基于用户的 UserCF 和基于物品的 ItemCF,算法再训练、冷启动兜底这些也都有。它适合两类人:一是拿它当毕业设计框架,想省掉数据库设计和登录注册这些重复劳动的本科生;二是刚入门推荐系统、想读一份非玩具级代码的开发者。这份项目最大的反直觉点在于:协同过滤的算法实现才几十行,真正让新手翻车的是 Django 的版本适配、MySQL 字符集配置和命令行参数解析。
2. 系统架构与模块设计:Django 2.2 的 MTV 怎么接住推荐引擎
2.1 分层思路:视图层、服务层、算法层分离
这个项目的代码没有把推荐逻辑直接塞进 Django views.py,而是按职责拆成了三层。最顶层是 Django 的 MTV 结构,views.py 只负责收请求、调服务、渲染模板;中间层是推荐服务模块,负责把用户行为数据从数据库里读出来,组装成算法需要的用户-物品评分矩阵;最底层才是协同过滤算法本身,输入是矩阵,输出是推荐列表。严格来说,这算是一种轻量级的 service 层设计,比把算法代码粘在视图函数里干净得多。
分层的好处很多。调试的时候直接在 Python shell 里 import 算法模块,喂一份小规模数据集就能验证相似度矩阵对不对,不用走 HTTP 请求;换算法的时候也只需要替换传入的推荐器对象,视图层根本不用动。对毕设来说,这种清晰的结构还能直接在论文架构图里复用,答辩时说自己做了模块解耦,比讲 CRUD 有说服力。
日志和异常处理也放在服务层,比如用户没有评分记录时就抛出冷启动信号,算法层捕获后降级为热度推荐。这避免了视图层到处 try-except 的丑代码,也算是一个进阶设计点。
2.2 目录结构:一眼看清这份代码的骨架
拿到源码后,先别急着跑,花五分钟看目录结构能省下后面两小时的迷茫。这份项目的核心目录大概长这样:
movie_reco/ ├── manage.py ├── movie_reco/ # 主配置目录 │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── apps/ │ ├── users/ # 用户模块:注册、登录、收藏 │ │ ├── models.py │ │ ├── views.py │ │ └── urls.py │ ├── movies/ # 电影模块:列表、详情、评分 │ │ ├── models.py │ │ ├── views.py │ │ └── urls.py │ └── recommend/ # 推荐模块:算法核心 │ ├── algorithms/ │ │ ├── user_cf.py │ │ ├── item_cf.py │ │ └── hybrid.py │ ├── services.py # 数据组装 + 推荐入口 │ └── urls.py ├── templates/ │ ├── base.html │ ├── index.html │ ├── movie_detail.html │ └── login.html └── static/ ├── css/ ├── js/ └── images/recommend 这个 app 是整个项目的重头戏。algorithms 目录下三份算法文件各有分工:user_cf.py 负责找相似用户,item_cf.py 负责找相似物品,hybrid.py 把两者的推荐结果按权重合并。services.py 是唯一被 views.py 调用的入口,内部做了评分矩阵构建和数据清洗。
有些新手会疑惑为什么不把推荐模块放在主配置目录 movie_reco 里,而是单独建一个 apps 包。这是在为以后扩展做准备,比如想加一个基于内容的推荐引擎,只需要在 apps 下新建模块即可,不会污染系统核心配置。
2.3 数据库设计:用户、电影、评分三张表打底
数据库模型在 Django 里用 models.py 定义,migrate 后自动生成表。核心是这三张:User(继承自 Django 自带 AbstractUser 扩展出昵称和头像字段)、Movie(电影名、导演、主演、海报 URL、分类、上映时间、简介)、Rating(用户 ID、电影 ID、评分值、时间戳)。评分表是典型的多对多中间表,UniqueConstraint 限制了同一用户对同一部电影只能有一条评分记录,这条约束很重要——如果没有它,运行 UserCF 构建矩阵时会出现重复项,直接影响余弦相似度的计算。
class Rating(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='ratings') movie = models.ForeignKey(Movie, on_delete=models.CASCADE, related_name='ratings') score = models.FloatField(verbose_name='评分') create_time = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = '影视评分' db_table = 'movie_rating' constraints = [ models.UniqueConstraint(fields=['user', 'movie'], name='unique_user_movie_rating') ]这里有个值得注意的点:score 用了 FloatField 而不是 IntegerField。原因是算法层需要计算平均评分和加权总和,浮点数可以避免后续除法和求均值时反复类型转换。但 FloatField 也会带来一个小麻烦,Django 渲染表单时可能接受任意浮点数,所以表单校验和 API 校验时都要限定分值范围 0.5 到 5.0。
Movie 表里专门留下了 tags 字段(用逗号分隔字符串存储),很多毕设会在后期加标签推荐,这个字段就是给 Tag-based CF 预留的。第一个用到这个字段的功能往往是后台,比如在 /admin 里手动给电影挂上"科幻""悬疑"标签,然后算法层做关联度计算。
3. 协同过滤核心算法拆解:一个请求从进入到返回的完整链路
3.1 用户相似度计算:从评分矩阵到余弦距离
推荐系统的入口在 recommend/urls.py,用户访问 /recommend/?type=user 时,Django 先路由到推荐视图,然后进入 services.py。services 层会从数据库拉取所有用户的评分记录,构建成 Python 字典形式的数据结构。这一步不做成复杂的 pandas DataFrame,因为大部分毕设数据量不到十万条,纯 Python 足够用,而且避免引入重依赖。
基于用户的协同过滤找的是「和我口味相似的人」。算法分三步:遍历用户集合,找当前用户的邻居用户;用余弦相似度计算当前用户与每个邻居的相似度;取相似度最高的 K 个用户,把他们看过且当前用户没看过的电影按加权评分排序。
def user_similarity(ratings_matrix): # ratings_matrix: dict[user_id] = {movie_id: score} similarity = {} for user_a in ratings_matrix: sim_row = {} for user_b in ratings_matrix: if user_a == user_b: continue common_movies = set(ratings_matrix[user_a].keys()) & set(ratings_matrix[user_b].keys()) if len(common_movies) < 2: sim_row[user_b] = 0 continue vec_a = [ratings_matrix[user_a][m] for m in common_movies] vec_b = [ratings_matrix[user_b][m] for m in common_movies] dot = sum(x * y for x, y in zip(vec_a, vec_b)) norm_a = sum(x ** 2 for x in vec_a) ** 0.5 norm_b = sum(x ** 2 for x in vec_b) ** 0.5 if norm_a == 0 or norm_b == 0: sim_row[user_b] = 0 else: sim_row[user_b] = dot / (norm_a * norm_b) similarity[user_a] = sim_row return similarity这里有个细节值得注意:只取共同评过分的电影做向量。如果两个玩家都看过某部电影,才认为他们在该维度上有可比性。否则会出现一种尴尬情况——A 只给一部电影打了 5 分,B 给一百部电影打了分,但两人共同评分电影数为零,相似度直接为 0,这是合理的结果。
另一个细节是阈值过滤:common_movies 长度小于 2 就跳过。原因很简单,只有一部共同电影时,余弦相似度要么是 1 要么是 -1,噪声太大。这个阈值可以按数据量调整,数据稀疏时降到 1,数据稠密时升到 5。
3.2 预测评分与 TopN 推荐生成:加权排序决定最终列表
相似度矩阵只是半成品,真正给用户看到的是推荐列表。ItemCF 和 UserCF 最后的输出逻辑其实一致:对候选电影做加权累加,分数越高排名越靠前。UserCF 的候选集来自最近邻用户看过的电影,预测公式是把每个邻居对该电影的评分乘以邻居与当前用户的相似度,再除以相似度总和。
def predict_user_cf(user_id, ratings_matrix, similarity_matrix, k=10): if user_id not in ratings_matrix: return [] user_rated = set(ratings_matrix[user_id].keys()) all_movies = set() for movie_id in ratings_matrix[user_id]: all_movies.add(movie_id) neighbors = sorted(similarity_matrix[user_id].items(), key=lambda x: x[1], reverse=True)[:k] scores = {} for movie_id in all_movies: if movie_id in user_rated: continue total_sim = 0 total_score = 0 for neighbor_id, sim in neighbors: if sim <= 0: continue movie_score = ratings_matrix[neighbor_id].get(movie_id) if movie_score is not None: total_score += sim * movie_score total_sim += sim if total_sim > 0: scores[movie_id] = total_score / total_sim return sorted(scores.items(), key=lambda x: x[1], reverse=True)[:10]参数 k 表示取多少个邻居。k 太小(比如 2),预测结果波动大,容易把冷门电影的评分预测得很极端;k 太大(比如 50),会把低相似度用户的噪声也带进来。这个项目默认 k=10,在公开数据集上算是一个折中值。实际测试时可以写一个简单的评估脚本,计算预测评分和真实评分的均方根误差 RMSE,然后选区最小的 k。
代码里还加了 sim <= 0 的过滤逻辑。余弦相似度可能出现负值,说明两个用户口味相反,这种情况下邻居的评分对当前用户没有参考价值,反而会拉低推荐质量。把负相似度邻居直接排除,是推荐系统中非常常见的 trick。
3.3 冷启动与混合策略:当算法遇到零数据用户
协同过滤最大的天敌是冷启动。一个刚注册的新用户没有评分记录,相似度矩阵里没有他的行,predict_user_cf 直接返回空列表。这个项目写了一个 hybrid 模块来处理这种场景,逻辑是先查用户评分记录数量,如果小于 5 条就认为数据量不足,改走热度榜推荐。
def hybrid_recommend(user_id, top_n=10): rating_count = Rating.objects.filter(user_id=user_id).count() if rating_count < 5: return recommend_by_hot() else: return recommend_by_item_cf(user_id, top_n)热度榜的排序依据是电影的平均评分和评分人数组合,这个项目采用了一个很朴素的公式:热度分 = 平均评分 * log2(评分人数 + 1)。之所以加 log2 是因为要抑制「只有 3 个人评了满分」这种小众神片霸榜。评分人数 10、平均分 5.0 的电影热度分约 17.2;而评分人数 100、平均分 4.5 的电影热度分约 31.5,后者更能代表大众口味。
混合策略在代码层面体现为 ItemCF 和 UserCF 的加权合并:同一部电影在 UserCF 得到分数 0.72,在 ItemCF 得到分数 0.65,最后总分 = 0.4 * 0.72 + 0.6 * 0.65 = 0.678。权重系数可以放在配置文件里,这个项目默认 ItemCF 权重更高,因为电影推荐场景下"看了 A 的人还看了 B"通常比"和你相似的人爱看 C"更直观。
4. 项目运行与配置:从零跑通 Django 推荐系统
4.1 环境准备:Python 版本和依赖锁死
项目技术栈明确要求 Python 3.7 和 Django 2.2.1,这两个版本属于「必须锁死」的类型。Python 3.8 到 3.10 虽然也能跑 Django,但项目里用到的某些第三方库可能没有适配新版解释器,最典型的就是 MySQL 客户端驱动。Django 2.2 使用 mysqlclient 时,编译安装会遇到一些系统依赖问题,装不上就启动不了。
建议用虚拟环境隔离:
# 创建虚拟环境 python3.7 -m venv venv source venv/bin/activate # 安装依赖 pip install django==2.2.1 pip install mysqlclient==1.4.2 pip install djangorestframework==3.11.0MySQL 驱动这一环最容易翻车,Windows 上如果 pip install mysqlclient 报编译错误,一个常见做法是下载对应版本的 .whl 文件再安装。不过这不属于项目代码的范畴,属于环境适配问题,我在第 5 章会单独讲。
4.2 修改配置文件:数据库连接和时区
拿到源码后第一件事不是 migrate,而是把 settings.py 里的数据库配置改成自己本地的 MySQL 账号。默认配置里写的是测试环境信息,需要手动替换成自己的密码和库名。
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'movie_reco_db', 'USER': 'root', 'PASSWORD': 'yourpassword', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': { 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'", 'charset': 'utf8mb4', } } }charset 参数设置成 utf8mb4,这个很关键。MySQL 的 utf8 编码只支持三个字节的字符,而电影简介里经常出现的中文生僻字、emoji 表情都需要四个字节,如果默认 utf8 会导致数据插入报错。Django 的表结构定义里没有显式声明编码,所以要靠数据库连接层的 charset 参数兜底。
TIME_ZONE 和 LANGUAGE_CODE 也建议改成本地配置,至少把通过 Django admin 添加电影记录时的日期时间显示调正确。
4.3 数据库初始化和加载种子数据
数据库配置好以后,执行迁移命令生成数据表。注意先创建数据库本身,用 MySQL 命令行执行 CREATE DATABASE movie_reco_db DEFAULT CHARACTER SET utf8mb4,否则 Django migrate 会报 Unknown database。
python manage.py makemigrations users movies recommend python manage.py migrate python manage.py createsuperusermakemigrations 时要指定 app 名称,因为项目里有多个 app,只执行不带参数的 makemigrations 可能会漏掉某些新添加的模型变更。这一步做完后,后台账号也顺手创建好了。
种子数据在项目里以 SQL 文件或 JSON 文件形式存在,通常放在 data/ 目录下。导入方式有几种,最常见的是 mysql 命令行执行 SQL 文件,或者用 Django 的 loaddata 命令加载 fixture。这里给出推荐的导入方式:
# 方式一:直接导入 SQL 备份 mysql -u root -p movie_reco_db < ./data/movie_data.sql # 方式二:Django fixture 方式 python manage.py loaddata data/fixtures/ratings.jsonSQL 方式适合整库导入,能直接导入电影数据和用户评分;fixture 方式更灵活,但 JSON 格式需要手动构造外键 ID,容易因自增 ID 冲突报错。我一般建议直接用 SQL 导入,省事且不容易错。
4.4 启动开发服务器并验证推荐接口
一切就绪后启动项目测试完整链路:
python manage.py runserver 8000浏览器访问 http://127.0.0.1:8000 能看到首页电影列表。用一个刚注册的账号给几部电影打分,再点我的推荐,观察返回的列表是否有变化。这里有一个验证技巧:用两个不同的账号,A 账号只给科幻片打高分,B 账号也给同样的科幻片打高分,然后查看 A 账号的推荐结果中是否出现了 B 账号看过且 A 没看过的电影。如果出现了,说明 UserCF 的链路是通的。
5. 避坑指南:运行时最常见的四个翻车现场
5.1 现象:migrate 时报错 Error loading MySQLdb module
一位同学在 Windows 上执行 python manage.py runserver 时,直接报 ModuleNotFoundError: No module named 'MySQLdb'。原因是 pip install mysqlclient 编译失败后,他又装了 pymysql,但没做 Django 兼容声明。解决方式有两种。第一种是把 pymysql 的安装脚本放进来,在项目主目录的init.py 里写一段兼容代码:
import pymysql pymysql.install_as_MySQLdb()第二种是我更推荐的,直接安装编译好的 mysqlclient 二进制的 .whl 文件。安装成功后不需要任何额外代码,Django 默认就能连上 MySQL。检查依赖是否正确的命令是 python -c "import MySQLdb",如果导入无报错就算装好了。
5.2 现象:推荐接口返回极慢,查询耗时 30 秒以上
这个现象本质上不是代码 bug,而是数据量变大后协同过滤算法的时间复杂度出了问题。UserCF 需要遍历每个用户、每个用户的所有邻居、每个邻居的所有电影,在评分量过万以后,纯 Python 双重循环的计算时间会翻倍上涨。解决方法是在服务层加一个简单的缓存。常见做法是把推荐结果存到临时表或 Redis 里,设置过期时间。这个项目有一个简化的缓存方案:在 Rating 表中新增一条评分时,删除该用户的推荐缓存记录,下次访问推荐页时重新计算。
在没有 Redis 的情况下,推荐结果也可以落在数据库缓存表中。缓存表设计成 user_id、recommend_json、create_time 三个字段,读取时先查缓存,命中就直接返回。这样虽然要多维护一张表,但对毕设项目是足够实用的。
5.3 现象:MySQL 里中文电影名变成乱码
乱码问题的根源通常是数据库表默认字符集不是 utf8mb4,或者是 Django 连接时没有指定 charset。75% 的乱码场景都是数据库字符集不对,而不是代码文件的问题。登录 MySQL 后执行:
SHOW CREATE TABLE movie_movie;如果看到 DEFAULT CHARSET=utf8,说明表建错了。要彻底解决,需要在建库时声明编码格式。已建的表可以通过 ALTER TABLE 修改编码,但这种操作比较脏,建议在开发初期就确定正确字符集配置,重跑迁移反而最干净。
5.4 现象:迁移时提示字段冲突或表已存在
这个项目的原始作者在开发过程中会反复修改 models.py,可能遗留了一些旧的迁移文件,导致新环境 makemigrations 时提示 "field named 'create_time' already exists" 之类的错误。踩坑记录是:不要删除整个 migrations 目录,因为 Django 根据迁移文件记录表结构状态,直接删文件会破坏迁移链。正确做法是保留已有迁移文件,修改模型的字段后重新执行 makemigrations,它会自动生成追加迁移。
如果是同步迁移失败导致的表结构异常,可以用 python manage.py migrate --fake 标记为已迁移,然后手工用 SQL 调整字段,这种做法适合数据结构已经不可逆的情况。
6. 二次开发与改造思路:把毕设升级成可展示的作品
6.1 替换或增强算法层:给推荐引擎留好接口
推荐服务层在设计时已经做了一件事——views.py 调用的是 recommend_service 而不是具体算法类,这为替换算法留出了空间。改造时可以做的是在 recommend/apps.py 里定义一个推荐器工厂,按配置文件读取推荐策略。比如 settings.py 中设定 RECOMMEND_ENGINE = 'item_cf',服务层根据这个值动态加载算法模块。
如果你想让项目在答辩时更有亮点,可以把纯 Python 实现换成基于矩阵分解的 SVD 推荐。Django 项目里引入 numpy 的代价不高,直接把评分矩阵转为 numpy 数组,调用已有的 SVD 方法计算低维向量,再通过向量内积生成 TopN 推荐。改造范围只涉及 algorithms/ 下的代码,模板和接口完全不用动,这一点是模块化设计带来的最大红利。
6.2 增加推荐解释功能:让推荐结果不再黑匣子
很多初学者在展示项目时会发现一个问题:推荐页只显示电影列表,用户不知道"为什么推荐这部片"。加一个解释字段可以显著提升作品的完成度。实现思路是:在 UserCF 推荐方法里,不仅返回 movie_id 和预测分数,还同时返回贡献最大的邻居用户及其共同评分电影。
具体来说,预测循环里维护一个 max_contributor 字典,记录每个候选电影贡献分数最高的邻居 ID。然后在前端模板里渲染"因为 A 用户也喜欢《星际穿越》,为你推荐了《降临》"。这个功能代码量不大,但对答辩展示来说非常加分,因为它直观体现了协同过滤的核心思想。
6.3 前端体验升级:从模板渲染到轻量搜索
原始的模板有登录、注册、电影列表和推荐区四个页面,交互比较传统。如果想给项目增加现代感,不需要做完整的前后端分离,用 Vue.js 或者 Alpine 在现有 Django 模板基础上做局部交互即可。例如电影详情页的评分输入可以改成点击星星组件,通过 fetch 发 POST 请求到评分接口,不用刷新页面。Django 端提供 JSON 响应的 URL 已经存在,只需要改模板里的 JavaScript。
最后还有一条我的个人习惯。在做完上述任一个改动后,我会强制自己跑一遍全部验证流程:先用 manage.py check 检查配置,再用 test 命令执行项目里自带的测试用例,如果连测试都没有,至少手动完成"注册用户 → 评分三部电影 → 查看推荐 → 换账号验证"四步链路。从那以后我每次改完算法或改完数据库,都强制走一遍这个流程,不敢跳过,因为推荐系统最大的坑就是"改了模型但忘了清缓存,看起来像是没改"。希望帮到你——如果你正在写类似的推荐系统或毕设,照着这份拆解把推荐链路捋一遍,能省不少时间。
本文还有配套的精品资源,点击获取