首页 / 资讯中心 / 文章详情

基于协同过滤的电影推荐系统:Django+MySQL毕业设计实战拆解

基于协同过滤的电影推荐系统:Django+MySQL毕业设计实战拆解 ★ FEATURED ARTICLE
简介这套基于协同过滤的电影推荐系统毕业设计源码面向计算机相关专业学生与推荐系统初学者完整实现了基于用户与基于物品两种推荐算法涵盖相似度计算、评分预测与 Top-N 推荐等完整流程。项目基于 Django 2.2.1、Python 3.7 与 MySQL/SQLite 搭建集成 Bootstrap 3 前端使用 MovieLens 数据集测试并附带毕业论文、爬虫脚本、数据库文件及详细 readme 技术文档可直接用于毕设答辩或二次开发。压缩包共 83 个文件以 Python 源码、HTML 模板、CSV 数据与配置文件为主含 PDF 论文、SQLite 数据库及若干辅助脚本整体约 8.41MB。工程目录包含 models、views、serializers、templates、migrations 等 Django 标准模块另有 populate_movielens.py、douban_crawler.py 等数据导入与采集脚本登录注册、电影展示、推荐结果等前端页面齐全结构清晰便于按模块阅读改造。已有 10612 人学习下载适合需要借鉴完整项目框架、算法实现与部署流程的开发者。1. 基于协同过滤的电影推荐系统python3.7django2.2.1mysql 毕业设计的落地拆解做毕业设计选电影推荐系统的人很多但真正把协同过滤从论文里的公式跑到网页上的人不多。这份资源给我的第一印象是它不只是一个 Django 项目而是一条完整的「数据 → 算法 → Web 展示」链路。技术栈明确锁定在 python3.7、django2.2.1、mysql 这套组合上适合课程设计收尾或者毕业设计打底。如果你正在纠结“协同过滤到底怎么落到代码里”“推荐结果为什么总是空的”“Django 怎么和算法模块衔接”这篇拆解能帮你把这套资源看透照着改也能跑出属于自己的版本。2. 协同过滤的选型逻辑为什么电影推荐用它最顺手2.1 协同过滤在电影场景里的工作原理协同过滤的核心假设很简单如果 A 和 B 过去对一批电影的评分相似那 A 喜欢的、B 没看过的电影大概率 B 也会喜欢。电影这个领域特别适合这个假设因为用户的观影口味相对稳定而且评分数据天然存在——豆瓣、IMDb、MovieLens 都提供了大量真实评分。这份资源里的实现走的是经典路线从 MySQL 的评分表里捞出所有用户对电影的评分记录把评分记录构造成「用户 × 电影」的评分矩阵计算用户之间的相似度或电影之间的相似度找最近邻加权汇总出目标用户对未看过的电影的预测分数取 Top-N 输出到页面上。这套流程本身不复杂难点在矩阵稀疏时会遇到各种边界情况这也是毕业设计答辩时老师最容易追问的地方。2.2 UserCF、ItemCF 与基于模型的选型对比我拆过几个推荐系统项目发现很多人一上来就想直接上 SVD 或者深度学习最后卡在数据量不够、训练时间太长、解释不清楚模型原理这三座大山前。这份资源实际采用的是 UserCF基于用户的协同过滤和 ItemCF基于物品的协同过滤相结合的方式而不是一次性梭哈一种算法。维度UserCFItemCF基于模型SVD 等原理找相似用户推荐他们喜欢的找相似电影推荐用户历史偏好的近邻矩阵分解学习隐向量计算量用户数量增长时急剧变大物品数量相对稳定更适合电影场景需要训练调参成本高可解释性强“因为你关注的人喜欢”强“因为你喜欢过同类片”弱黑匣子冷启动新用户无历史无法计算新电影无评分无法计算同样有冷启动问题毕业设计友好度高公式直观高实现简单中答辩容易深挖数学细节我做这类项目一般建议以 ItemCF 为主体、UserCF 做对比实验因为电影数量通常远小于用户数量ItemCF 在性能和效果之间更平衡。这份资料里两种算法都留了实现入口跑对比实验时直接切换配置项就能出两组结果。2.3 这份资源的数据流设计从评分表到推荐结果先把整条链路打通后面看代码才不会迷路MySQL 评分表 → Django ORM 查询 → 构建评分矩阵 → 计算相似度 → 生成推荐列表 → 写入上下文 → 模板渲染资源里 Movie、Rating、User 三张表是核心其中 Rating 表绑定了用户和电影的外键同时记录了评分值和时间戳。这里要特别留意时间戳字段——有些推荐系统项目会把「最近评分」和「历史评分」同等对待导致推荐结果偏向老片。这份资源在数据预处理阶段保留了时间字段为后续做时间衰减留了接口。Django 2.2.1 在 Python 3.7 下跑这套逻辑很稳关键点在于 ORM 查询不能写出 N1 问题。比如一次性取出所有评分记录再在内存里构建矩阵比循环里逐条查 Rating 表要快一个数量级。后面第 4 章我会把这块代码拆开说明。3. Django 2.2.1 项目初始化与数据库建模先把架子搭对3.1 虚拟环境与项目脚手架搭建拿到资源包后第一步不是看推荐算法而是先把 Flask 还是 Django 的事定下来。这份资源明确了用 Django 2.2.1所以虚拟环境里必须锁版本。我习惯用 virtualenv 而不是直接装全局依赖避免系统里其他项目把 Django 版本顶掉。这里给出一套完整初始化命令# 创建并激活虚拟环境Windows 下激活命令略有不同 python3.7 -m venv venv source venv/bin/activate # Windows: venv\Scripts\activate # 安装核心依赖锁版本 pip install django2.2.1 pip install mysqlclient2.0.1 pip install numpy1.19.5 pip install pandas1.1.5 # 检查版本确认环境没问题 python -m django --version这里为什么要特意锁 numpy 和 pandas 的版本因为 Python 3.7 搭配 numpy 1.19.5 是兼容性最好的组合再高的 numpy 版本在 3.7 下会出现编译警告甚至在导入时报错。pandas 同理1.1.5 能用 DataFrame 做评分数据的聚合分析又不会引入 python 3.8 才有的语法特性。mysqlclient 2.0.1 对应 Django 2.2 的 ORM 适配别用 1.x 版本连接 MySQL 8 的时候会有认证插件问题。3.2 电影与评分模型外键、索引与联合约束django-admin 创建项目骨架后核心在 models.py。电影推荐系统最忌讳的是「一个用户对同一部电影评了两次分」。数据表设计阶段要把这条约束做死。from django.db import models from django.contrib.auth.models import User class Movie(models.Model): title models.CharField(电影名, max_length128) genres models.CharField(类型, max_length128) release_year models.IntegerField(上映年份, default0) avg_rating models.FloatField(平均分, default0.0) class Meta: db_table movie ordering [-release_year] class Rating(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, related_nameratings) movie models.ForeignKey(Movie, on_deletemodels.CASCADE, related_nameratings) score models.FloatField(评分, default0.0) timestamp models.DateTimeField(评分时间, auto_now_addTrue) class Meta: db_table rating unique_together (user, movie) indexes [ models.Index(fields[user, movie]), ]重点看 unique_together 这行——它保证同一用户对同一电影只能有一条评分。没有这个约束后面算相似度时矩阵里出现重复值推荐结果就是一团乱麻。indexes 是为了让按 user 或 movie 查询评分时走索引不然数据量一上来ORM 查询会直接卡死。on_deletemodels.CASCADE 的意思是用户或电影删除后对应评分也一并删掉保证 MySQL 里不残留孤儿数据。related_name 让你可以通过 user.ratings.all() 直接拿到一个用户的全部评分记录这在构建评分矩阵时非常顺手。3.3 用 Django ORM 造测试数据快速验证链路还没接入真实数据集之前我一般会先用 ORM 写一段造数脚本塞模拟评分进去验证后端接口能通。import random from django.contrib.auth.models import User from movie.models import Movie, Rating # 创建 20 个测试用户 for i in range(20): User.objects.create_user(ftest_user_{i}, passwordtest123) # 创建 50 部测试电影 genres_pool [Action, Comedy, Sci-Fi, Romance] for j in range(50): Movie.objects.create( titlefTest Movie {j}, genresrandom.choice(genres_pool), release_year1990 j ) # 随机生成评分保证大部分用户有 10-30 条记录 users list(User.objects.all()) movies list(Movie.objects.all()) for u in users: sample_movies random.sample(movies, random.randint(10, 30)) for m in sample_movies: Rating.objects.create( useru, moviem, scorerandom.uniform(1.0, 5.0) )这段脚本的值在于把「测试数据生成」和「算法调试」分开。协同过滤最容易踩的坑是数据太稀疏导致相似度全是 0有了 20 个用户、50 部电影、每人 10-30 条评分的密度推荐结果至少能跑出来。之后换用真正的 MovieLens-1M 数据集时只需要把数据导入 MySQL算法模块不用改。random.sample 的用法注意一下它保证不重复取样所以这段脚本生成的数据天然满足 unique_together 约束。如果你想测试重复评分时的异常处理可以手动往 Rating 里插入重复记录看看 ORM 会不会给你抛 IntegrityError。这在答辩时是一个非常好的演示点。4. 协同过滤算法模块实现从 MySQL 到推荐列表的完整代码路径4.1 评分矩阵构建避免 N1 查询很多人在这一步翻车写成双层循环去查数据库# 错误示范每算一个相似度就查一次库结果几十个用户跑出几千条 SQL for u1 in user_list: for u2 in user_list: ratings_1 Rating.objects.filter(useru1) ratings_2 Rating.objects.filter(useru2)正确做法是一次性把数据全部拉进内存用字典和矩阵结构组织好import numpy as np def build_rating_matrix(): # 一次性查出所有评分避免逐用户查库 ratings Rating.objects.all().values(user_id, movie_id, score) # 收集所有用户和电影的唯一 ID user_ids sorted(set(r[user_id] for r in ratings)) movie_ids sorted(set(r[movie_id] for r in ratings)) # 建立映射原始 ID - 矩阵行列下标 user_idx {uid: i for i, uid in enumerate(user_ids)} movie_idx {mid: j for j, mid in enumerate(movie_ids)} # 初始化空矩阵缺失值先填 0后续也可以填均值 matrix np.zeros((len(user_ids), len(movie_ids))) for r in ratings: i user_idx[r[user_id]] j movie_idx[r[movie_id]] matrix[i][j] r[score] return matrix, user_ids, movie_ids, user_idx, movie_idx逻辑说明values() 指定只要三列避免 ORM 把整行对象实例化省去时间set() 自动去重后排序保证映射稳定numpy 二维数组承载矩阵默认填 0后面可以在计算相似度时跳过 0 值的坐标。参数说明user_idx 和 movie_idx 是字典负责把 MySQL 里的自增主键映射成连续的矩阵下标。注意这里必须同时返回五个值后续算相似度时要用 movie_idx 把预测结果映射回真实电影 ID否则最终展示到页面上的是矩阵行列号而不是电影标题。4.2 UserCF 核心实现皮尔逊相似度与 Top-N 推荐资源里的相似度计算用的是皮尔逊相关系数因为用户评分习惯不同——有人喜欢打 5 分下限有人从 3 分起打。皮尔逊系数会先减去用户自己的评分均值消除这种「打分尺子不一样」的偏差。import numpy as np def pearson_similarity(matrix, user_i, user_j): 计算两个用户之间的皮尔逊相关系数 # 取两行数据找到两者都有评分的电影 i_ratings matrix[user_i] j_ratings matrix[user_j] # 只保留双方都有评分的列评分 0 表示有评分 mask (i_ratings 0) (j_ratings 0) if np.sum(mask) 0: return 0.0 common_i i_ratings[mask] common_j j_ratings[mask] # 减去各自均值消除打分习惯差异 i_centered common_i - np.mean(common_i) j_centered common_j - np.mean(common_j) # 分母为 0 的情况比如只有一条共同评分 denom np.sqrt(np.sum(i_centered ** 2) * np.sum(j_centered ** 2)) if denom 0: return 0.0 return np.sum(i_centered * j_centered) / denom def recommend_for_user(matrix, user_idx, user_ids, movie_idx, top_n10): 对目标用户生成 Top-N 推荐列表 n_users matrix.shape[0] target_user user_idx # 计算目标用户与所有其他用户的相似度 similarities [] for u in range(n_users): if u target_user: continue sim pearson_similarity(matrix, target_user, u) similarities.append((sim, u)) # 按相似度降序取前 K 个邻居 similarities.sort(reverseTrue, keylambda x: x[0]) k_neighbors similarities[:20] # 过滤掉相似度为 0 的邻居 k_neighbors [(s, u) for s, u in k_neighbors if s 0] if not k_neighbors: return [] # 对目标用户没看过的电影用邻居评分加权预测 target_ratings matrix[target_user] unwatched np.where(target_ratings 0)[0] scores {} for movie in unwatched: weighted_sum 0.0 sim_sum 0.0 for sim, u in k_neighbors: score matrix[u][movie] if score 0: weighted_sum sim * score sim_sum abs(sim) if sim_sum 0: scores[movie] weighted_sum / sim_sum # 按预测分数排序取 Top-N ranked sorted(scores.items(), keylambda x: x[1], reverseTrue)[:top_n] # 把矩阵下标映射回真实电影 ID movie_ids [movie_idx[m] for m, _ in ranked] # 这里需改成反向映射 return movie_ids逻辑说明外层循环遍历所有用户内层调用 pearson_similarity 算两两相似度k_neighbors 取了相似度最高的 20 个邻居再过滤掉相似度为 0 的防止把毫无关联的用户拉进来投票预测分数采用加权平均相似度高的邻居对最终分数影响更大。参数说明top_n10 决定最终推荐几个电影k_neighbors20 决定参考多少个邻居。这两个参数是调优的关键——邻居太多会把低质量评分引入邻居太少则推荐结果随机性大。我一般建议在 15-30 之间做网格搜索。4.3 冷启动兜底热度榜与随机探索协同过滤有个硬伤新用户没有评分皮尔逊公式里找不到任何共同评分项返回空列表。资源里给了一种务实方案——对无历史行为用户推荐全局热度最高的电影。def cold_start_recommendation(threshold4.0, top_n10): 冷启动场景返回平均分高且评分人数多的电影 movies Movie.objects.annotate( rating_countmodels.Count(ratings), avg_scoremodels.Avg(ratings__score) ).filter( rating_count__gte5, avg_score__gtethreshold ).order_by(-rating_count)[:top_n] return moviesannotate 搭配 Count 和 Avg 是 Django ORM 里做聚合的标准姿势。rating_count__gte5 确保电影不是只有一个用户打分刷出来的孤儿数据avg_score__gtethreshold 保证质量底线最后的 order_by(-rating_count) 把热度放在第一位相当于用「最多人看」来兜底。这段代码放到推荐接口的入口处判断用户评分记录数为 0 时就调用能避免推荐列表为空的尴尬。5. 避坑手册协同过滤 Django 实战的五个典型翻车记录5.1 推荐结果永远是空列表现象接口正常返回 200但推荐列表是空的。用户有评分数据代码也没报错。原因评分矩阵过于稀疏。比如 100 个用户、2000 部电影但每个用户只评了 3-5 部。皮尔逊相似度要求「两个用户必须有共同评分的电影」稀疏场景下两个用户碰巧都看过的电影数趋近于 0算出来的相似度全是 0最后 k_neighbors 为空推荐自然为空。解决冷启动兜底不是只给新用户用的当 k_neighbors 过滤后为空时也调用热度榜方案。资源里的 cold_start_recommendation 要接在推荐函数返回空列表之前def get_recommendations(request, user_id): recs recommend_for_user(...) if not recs: recs cold_start_recommendation() # 标记为冷启动推荐前端可以展示不同文案 return JsonResponse({recommendations: recs, is_cold_start: True})5.2 中文电影名入库报错现象MySQL 写入中文数据时报错Incorrect string value: \xe5\x93\x94...。原因MySQL 数据库或数据表默认字符集是 latin1Django 连接串也没有指定 utf8mb4。解决建库时指定字符集同时修改 Django 的 DATABASES 配置# settings.py DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: movie_recommend, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, }, } }MySQL 建库时执行CREATE DATABASE movie_recommend CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;双保险。5.3 Python 3.7 下 Django 运行时 ctypes 报错现象django.core.exceptions.ImproperlyConfigured: Error loading MySQLdb module或类似底层库加载失败。原因mysqlclient 版本和 Python 3.7 不匹配或者系统里没有编译依赖如 libmysqlclient-dev。这在 Linux 环境下特别常见。解决直接用 pip 装 mysqlclient 大概率会触发源码编译。Debian/Ubuntu 系统先执行sudo apt-get install python3.7-dev default-libmysqlclient-dev pip install mysqlclient2.0.1装完之后再用python -c import MySQLdb验证导入成功再启动 Django。5.4 推荐结果重复且没有多样性现象每次刷新推荐列表返回的电影都差不多类型全是一个方向的。原因相似度计算只用了单一维度比如只看电影类型字段完全匹配或者评分矩阵里热门电影占了主导权重。加权预测时热门电影因为评分数多天然容易拿到更高的加权分数。解决在推荐结果里增加多样性控制——按类型做分组抽样或者对已被大量用户看过的电影做降权# 假设 movie 表里有 rating_count 字段 movie_score predicted_score * (1 / (1 0.1 * rating_count))降权系数 0.1 可以调数值越大对热门电影的惩罚越强。这是我实际调优时最喜欢改的参数之一。5.5 Django 模板里渲染推荐列表出现重复 id现象页面输出推荐电影时有重复比如同一部电影出现在两个推荐位。这个通常在 debug 时才发现。原因推荐算法生成的 movie_ids 里出现了重复元素可能是邻居列表中同一部电影的评分被多次累积进 weighted_sum。解决在返回前对 movie_ids 做去重同时保留分数排序unique_movie_ids list(dict.fromkeys(movie_ids))[:top_n]dict.fromkeys 会保留第一次出现的顺序去重复并且不打乱排序比 set() 更合适。6. 验证推荐质量离线指标和一个小小的 A/B 实验技巧推荐系统做完不是一个页面能跑就完事毕业设计答辩时老师大概率会问一句「你怎么知道推荐得准不准」我一般推荐用两条线来回应这个质疑。第一条是离线指标用历史评分数据做训练集和测试集切分from sklearn.model_selection import train_test_split # 把每个用户的评分按 8:2 切分 train_data, test_data train_test_split( all_ratings, test_size0.2, random_state42 ) # 用 train_data 算推荐检查 test_data 里用户实际喜欢的电影 # 是否出现在推荐列表里 def precision_at_k(recommended_ids, actual_ids, k10): recommended_k set(recommended_ids[:k]) hits len(recommended_k set(actual_ids)) return hits / k def recall_at_k(recommended_ids, actual_ids, k10): recommended_k set(recommended_ids[:k]) hits len(recommended_k set(actual_ids)) return hits / len(actual_ids) if actual_ids else 0 avg_precision 0.0 for user in test_users: rec_ids recommend_for_user(...) actual_ids test_user_ratings[user] avg_precision precision_at_k(rec_ids, actual_ids) avg_precision / len(test_users)precision_at_k 的意义是「推荐 10 个电影里有几个是你后来真正看了的」recall_at_k 是「你真正看了的 10 部电影里有几部被推荐出来了」。对电影场景precision 比 recall 重要——用户不介意错过几部好片但很介意推荐列表里都是垃圾。第二条线是产品层面的 A/B 测试。在 Django 里给推荐接口加一个实验参数,把用户随机分到两组一组用 ItemCF一组用 UserCF后端记录两组用户的点击次数。# views.py 里加一个简单的分流逻辑 def recommend_view(request): user_id request.user.id bucket A if user_id % 2 0 else B if bucket A: recs recommend_by_itemcf(user_id) else: recs recommend_by_usercf(user_id) # 记录曝光和点击日志 RecommendationLog.objects.create( user_iduser_id, bucketbucket, shown_moviesrecs ) return render(request, recommend.html, { recommendations: recs, bucket: bucket })这个实验特别适合答辩展示运行一周后统计两个桶的平均点击率用数据说明哪种算法在你的数据集上更优。资源里已经包含了 ItemCF 和 UserCF 的接口切分流只需要在视图里多做一层判断。验证维度还可以加一个「惊喜度」指标——推荐列表里有多少电影不是用户历史评分类别里的。但毕业设计做到 precision、recall 加 A/B 分流已经超出大多数同题目的深度了这个资源里代码跑通后把这两块补上是性价比最高的加分项。做完这些验证后我在之后每隔一段时间把这个二十几行的评估脚本跑一遍对比参数微调前后的指标变化。从那以后我每次做推荐系统项目都会强制先跑离线指标再调参不凭感觉动阈值。这套流程你按着走一遍答辩时至少能撑住五六轮追问。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站