简介:这套基于协同过滤的电影推荐系统毕业设计源码,面向计算机相关专业学生与推荐系统初学者,完整实现了基于用户与基于物品两种推荐算法,涵盖相似度计算、评分预测与 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.7+django2.2.1+mysql 毕业设计的落地拆解做毕业设计选电影推荐系统的人很多,但真正把协同过滤从论文里的公式跑到网页上的人不多。这份资源给我的第一印象是:它不只是一个 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 查询不能写出 N+1 问题。比如一次性取出所有评分记录再在内存里构建矩阵,比循环里逐条查 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 django==2.2.1 pip install mysqlclient==2.0.1 pip install numpy==1.19.5 pip install pandas==1.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_length=128) genres = models.CharField('类型', max_length=128) release_year = models.IntegerField('上映年份', default=0) avg_rating = models.FloatField('平均分', default=0.0) class Meta: db_table = 'movie' ordering = ['-release_year'] 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('评分', default=0.0) timestamp = models.DateTimeField('评分时间', auto_now_add=True) class Meta: db_table = 'rating' unique_together = ('user', 'movie') indexes = [ models.Index(fields=['user', 'movie']), ]重点看 unique_together 这行——它保证同一用户对同一电影只能有一条评分。没有这个约束,后面算相似度时矩阵里出现重复值,推荐结果就是一团乱麻。indexes 是为了让按 user 或 movie 查询评分时走索引,不然数据量一上来,ORM 查询会直接卡死。on_delete=models.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(f'test_user_{i}', password='test123') # 创建 50 部测试电影 genres_pool = ['Action', 'Comedy', 'Sci-Fi', 'Romance'] for j in range(50): Movie.objects.create( title=f'Test Movie {j}', genres=random.choice(genres_pool), release_year=1990 + 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( user=u, movie=m, score=random.uniform(1.0, 5.0) )这段脚本的值在于把「测试数据生成」和「算法调试」分开。协同过滤最容易踩的坑是数据太稀疏导致相似度全是 0,有了 20 个用户、50 部电影、每人 10-30 条评分的密度,推荐结果至少能跑出来。之后换用真正的 MovieLens-1M 数据集时,只需要把数据导入 MySQL,算法模块不用改。random.sample 的用法注意一下:它保证不重复取样,所以这段脚本生成的数据天然满足 unique_together 约束。如果你想测试重复评分时的异常处理,可以手动往 Rating 里插入重复记录,看看 ORM 会不会给你抛 IntegrityError。这在答辩时是一个非常好的演示点。4. 协同过滤算法模块实现:从 MySQL 到推荐列表的完整代码路径4.1 评分矩阵构建:避免 N+1 查询很多人在这一步翻车,写成双层循环去查数据库:# 错误示范:每算一个相似度就查一次库,结果几十个用户跑出几千条 SQL for u1 in user_list: for u2 in user_list: ratings_1 = Rating.objects.filter(user=u1) ratings_2 = Rating.objects.filter(user=u2)正确做法是一次性把数据全部拉进内存,用字典和矩阵结构组织好: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_n=10): """对目标用户生成 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(reverse=True, key=lambda 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(), key=lambda x: x[1], reverse=True)[:top_n] # 把矩阵下标映射回真实电影 ID movie_ids = [movie_idx[m] for m, _ in ranked] # 这里需改成反向映射 return movie_ids逻辑说明:外层循环遍历所有用户,内层调用 pearson_similarity 算两两相似度;k_neighbors 取了相似度最高的 20 个邻居,再过滤掉相似度为 0 的,防止把毫无关联的用户拉进来投票;预测分数采用加权平均,相似度高的邻居对最终分数影响更大。参数说明:top_n=10 决定最终推荐几个电影,k_neighbors=20 决定参考多少个邻居。这两个参数是调优的关键——邻居太多会把低质量评分引入,邻居太少则推荐结果随机性大。我一般建议在 15-30 之间做网格搜索。4.3 冷启动兜底:热度榜与随机探索协同过滤有个硬伤:新用户没有评分,皮尔逊公式里找不到任何共同评分项,返回空列表。资源里给了一种务实方案——对无历史行为用户,推荐全局热度最高的电影。def cold_start_recommendation(threshold=4.0, top_n=10): """冷启动场景:返回平均分高且评分人数多的电影""" movies = Movie.objects.annotate( rating_count=models.Count('ratings'), avg_score=models.Avg('ratings__score') ).filter( rating_count__gte=5, avg_score__gte=threshold ).order_by('-rating_count')[:top_n] return moviesannotate 搭配 Count 和 Avg 是 Django ORM 里做聚合的标准姿势。rating_count__gte=5 确保电影不是只有一个用户打分刷出来的孤儿数据;avg_score__gte=threshold 保证质量底线;最后的 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 数据库或数据表默认字符集是 latin1,Django 连接串也没有指定 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 mysqlclient==2.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_size=0.2, random_state=42 ) # 用 train_data 算推荐,检查 test_data 里用户实际喜欢的电影 # 是否出现在推荐列表里 def precision_at_k(recommended_ids, actual_ids, k=10): recommended_k = set(recommended_ids[:k]) hits = len(recommended_k & set(actual_ids)) return hits / k def recall_at_k(recommended_ids, actual_ids, k=10): 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_id=user_id, bucket=bucket, shown_movies=recs ) return render(request, 'recommend.html', { 'recommendations': recs, 'bucket': bucket })这个实验特别适合答辩展示:运行一周后统计两个桶的平均点击率,用数据说明哪种算法在你的数据集上更优。资源里已经包含了 ItemCF 和 UserCF 的接口,切分流只需要在视图里多做一层判断。验证维度还可以加一个「惊喜度」指标——推荐列表里有多少电影不是用户历史评分类别里的。但毕业设计做到 precision、recall 加 A/B 分流已经超出大多数同题目的深度了,这个资源里代码跑通后,把这两块补上是性价比最高的加分项。做完这些验证后,我在之后每隔一段时间把这个二十几行的评估脚本跑一遍,对比参数微调前后的指标变化。从那以后我每次做推荐系统项目都会强制先跑离线指标再调参,不凭感觉动阈值。这套流程你按着走一遍,答辩时至少能撑住五六轮追问。希望帮到你。本文还有配套的精品资源,点击获取
简介:这套基于协同过滤的电影推荐系统毕业设计源码,面向计算机相关专业学生与推荐系统初学者,完整实现了基于用户与基于物品两种推荐算法,涵盖相似度计算、评分预测与 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.7+django2.2.1+mysql 毕业设计的落地拆解做毕业设计选电影推荐系统的人很多,但真正把协同过滤从论文里的公式跑到网页上的人不多。这份资源给我的第一印象是:它不只是一个 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 查询不能写出 N+1 问题。比如一次性取出所有评分记录再在内存里构建矩阵,比循环里逐条查 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 django==2.2.1 pip install mysqlclient==2.0.1 pip install numpy==1.19.5 pip install pandas==1.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_length=128) genres = models.CharField('类型', max_length=128) release_year = models.IntegerField('上映年份', default=0) avg_rating = models.FloatField('平均分', default=0.0) class Meta: db_table = 'movie' ordering = ['-release_year'] 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('评分', default=0.0) timestamp = models.DateTimeField('评分时间', auto_now_add=True) class Meta: db_table = 'rating' unique_together = ('user', 'movie') indexes = [ models.Index(fields=['user', 'movie']), ]重点看 unique_together 这行——它保证同一用户对同一电影只能有一条评分。没有这个约束,后面算相似度时矩阵里出现重复值,推荐结果就是一团乱麻。indexes 是为了让按 user 或 movie 查询评分时走索引,不然数据量一上来,ORM 查询会直接卡死。on_delete=models.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(f'test_user_{i}', password='test123') # 创建 50 部测试电影 genres_pool = ['Action', 'Comedy', 'Sci-Fi', 'Romance'] for j in range(50): Movie.objects.create( title=f'Test Movie {j}', genres=random.choice(genres_pool), release_year=1990 + 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( user=u, movie=m, score=random.uniform(1.0, 5.0) )这段脚本的值在于把「测试数据生成」和「算法调试」分开。协同过滤最容易踩的坑是数据太稀疏导致相似度全是 0,有了 20 个用户、50 部电影、每人 10-30 条评分的密度,推荐结果至少能跑出来。之后换用真正的 MovieLens-1M 数据集时,只需要把数据导入 MySQL,算法模块不用改。random.sample 的用法注意一下:它保证不重复取样,所以这段脚本生成的数据天然满足 unique_together 约束。如果你想测试重复评分时的异常处理,可以手动往 Rating 里插入重复记录,看看 ORM 会不会给你抛 IntegrityError。这在答辩时是一个非常好的演示点。4. 协同过滤算法模块实现:从 MySQL 到推荐列表的完整代码路径4.1 评分矩阵构建:避免 N+1 查询很多人在这一步翻车,写成双层循环去查数据库:# 错误示范:每算一个相似度就查一次库,结果几十个用户跑出几千条 SQL for u1 in user_list: for u2 in user_list: ratings_1 = Rating.objects.filter(user=u1) ratings_2 = Rating.objects.filter(user=u2)正确做法是一次性把数据全部拉进内存,用字典和矩阵结构组织好: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_n=10): """对目标用户生成 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(reverse=True, key=lambda 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(), key=lambda x: x[1], reverse=True)[:top_n] # 把矩阵下标映射回真实电影 ID movie_ids = [movie_idx[m] for m, _ in ranked] # 这里需改成反向映射 return movie_ids逻辑说明:外层循环遍历所有用户,内层调用 pearson_similarity 算两两相似度;k_neighbors 取了相似度最高的 20 个邻居,再过滤掉相似度为 0 的,防止把毫无关联的用户拉进来投票;预测分数采用加权平均,相似度高的邻居对最终分数影响更大。参数说明:top_n=10 决定最终推荐几个电影,k_neighbors=20 决定参考多少个邻居。这两个参数是调优的关键——邻居太多会把低质量评分引入,邻居太少则推荐结果随机性大。我一般建议在 15-30 之间做网格搜索。4.3 冷启动兜底:热度榜与随机探索协同过滤有个硬伤:新用户没有评分,皮尔逊公式里找不到任何共同评分项,返回空列表。资源里给了一种务实方案——对无历史行为用户,推荐全局热度最高的电影。def cold_start_recommendation(threshold=4.0, top_n=10): """冷启动场景:返回平均分高且评分人数多的电影""" movies = Movie.objects.annotate( rating_count=models.Count('ratings'), avg_score=models.Avg('ratings__score') ).filter( rating_count__gte=5, avg_score__gte=threshold ).order_by('-rating_count')[:top_n] return moviesannotate 搭配 Count 和 Avg 是 Django ORM 里做聚合的标准姿势。rating_count__gte=5 确保电影不是只有一个用户打分刷出来的孤儿数据;avg_score__gte=threshold 保证质量底线;最后的 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 数据库或数据表默认字符集是 latin1,Django 连接串也没有指定 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 mysqlclient==2.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_size=0.2, random_state=42 ) # 用 train_data 算推荐,检查 test_data 里用户实际喜欢的电影 # 是否出现在推荐列表里 def precision_at_k(recommended_ids, actual_ids, k=10): recommended_k = set(recommended_ids[:k]) hits = len(recommended_k & set(actual_ids)) return hits / k def recall_at_k(recommended_ids, actual_ids, k=10): 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_id=user_id, bucket=bucket, shown_movies=recs ) return render(request, 'recommend.html', { 'recommendations': recs, 'bucket': bucket })这个实验特别适合答辩展示:运行一周后统计两个桶的平均点击率,用数据说明哪种算法在你的数据集上更优。资源里已经包含了 ItemCF 和 UserCF 的接口,切分流只需要在视图里多做一层判断。验证维度还可以加一个「惊喜度」指标——推荐列表里有多少电影不是用户历史评分类别里的。但毕业设计做到 precision、recall 加 A/B 分流已经超出大多数同题目的深度了,这个资源里代码跑通后,把这两块补上是性价比最高的加分项。做完这些验证后,我在之后每隔一段时间把这个二十几行的评估脚本跑一遍,对比参数微调前后的指标变化。从那以后我每次做推荐系统项目都会强制先跑离线指标再调参,不凭感觉动阈值。这套流程你按着走一遍,答辩时至少能撑住五六轮追问。希望帮到你。本文还有配套的精品资源,点击获取