基于Django的协同过滤电影推荐系统:从算法到缓存实战
2026/9/18 15:16:31 网站建设 项目流程

简介:这份Python基于Django的电影推荐系统毕业论文,适合计算机相关专业毕业生在完成毕业设计或撰写论文时参考。文档围绕电影推荐系统的分析、设计与实现展开,涵盖业务分析、功能需求分析、数据流分析、非功能需求分析,以及架构设计、功能模块设计、数据库设计和界面设计等内容,并对Mysql数据库和Django框架的使用做了详细说明。资源为单个doc文件,大小1.07MB,属于完整论文Word版,可直接查看或编辑,便于复制文本和调整格式。目前已有177人学习浏览,适合需要借鉴系统结构、论文框架或快速理解Django+Mysql开发流程的读者。文档同时包含中英文摘要、章节式目录和关键模块描述,能够帮助读者梳理写作思路,是完成毕业设计论文的实用参考资料。

1. 电影推荐系统的论文需求拆解与 django 技术选型

在一篇以 python 和 django 为核心栈的电影推荐系统毕业论文里,真正拉开评分差距的往往不是登录注册模块做得多细,而是推荐链路有没有被讲清楚、有没有在项目里真实跑通。许多同学把推荐当成一个孤立的算法题,在视图函数里临时取数据、算相似度,评分数据一多就超时;另一些又走到另一个极端,直接套重量级的机器学习框架,把 Django 自家模型层、缓存与模板渲染的能力晾在一边。这个标题看着像课程作业,实际考察的是三件事:数据建模有没有扩展余地、推荐算法能不能用 ORM 代码自圆其说、数据量上来后系统扛不扛得住。读者既包括正在选课题的学生,也包括想搭推荐原型的后端工程师。先用 Django 的 MTV 模式把工程骨架支起来,再把协同过滤落进查询与接口层,这条路最稳,做出来的系统也能完整演示。

2. django 项目初始化与电影评分的模型设计

2.1 用 django-admin 命令建立内外两层目录

推荐系统和普通内容管理系统的数据流有明显差异:后者的读写集中在单张表,前者的写入侧是用户行为日志,读取侧是聚合评分矩阵,两侧要同时支撑。把推荐逻辑和业务表放进同一个 app 会让目录越来越乱,所以常见做法是用两个 app 做边界划分:movies管理电影信息和评分录入,recommender负责相似度计算与推荐结果组装。

# 创建虚拟环境并激活 python -m venv venv source venv/bin/activate # Windows 用户执行 venv\Scripts\activate # 安装 django pip install django # 创建项目与两个业务应用 django-admin startproject moviereco cd moviereco python manage.py startapp movies python manage.py startapp recommender

从上往下解释这四个动作。venv 隔离出独立的 Python 运行时,避免系统级依赖污染论文复现环境;startproject moviereco生成外层配置文件;startapp moviesstartapp recommender生成内层业务模块,两层目录结构的边界就是 MTV 分离思想在工程上的体现。刚执行完还没有任何业务代码,需要先把两个 app 注册进INSTALLED_APPS才能继续。

2.1.1 用户、电影、评分三张表的字段约束

电影推荐系统的基础表可以拆成四张:用户表、电影表、评分表、操作日志表。用户表继承AbstractUser,额外增加偏好类型字段,供没有评分的冷启动用户做类型兜底;电影表放标题、类型、年份和两个冗余统计字段;评分表存用户与电影的关联和分值。

# movies/models.py from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): preferred_genres = models.CharField( max_length=255, blank=True, verbose_name="偏好电影类型" ) class Movie(models.Model): title = models.CharField(max_length=200, db_index=True, verbose_name="电影名") genres = models.CharField(max_length=255, verbose_name="类型列表") release_year = models.IntegerField(null=True, blank=True) avg_rating = models.FloatField(default=0.0, verbose_name="平均分") rating_count = models.IntegerField(default=0, verbose_name="评分人数") 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="评分") created_at = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ("user", "movie")

unique_together是最容易被忽视的约束,它在数据库层面禁止同一用户对同一部电影重复写入。只在前端做判断存在竞态窗口,高并发写同一部电影时可能出现重复评分,联合约束会直接拒绝后一条写入。titledb_index=True是因为电影搜索是高频操作;外键索引 Django 会自动生成,不需要手工声明。

模型关键字段索引与约束推荐链路中的作用
Userpreferred_genres不建索引冷启动类型兜底
Movietitle, avg_rating, rating_counttitle 普通索引详情页与热榜
Ratinguser, movie, scoreunique_together 联合约束相似度计算原料

接下来生成并执行迁移:

python manage.py makemigrations movies python manage.py migrate python manage.py sqlmigrate movies 0001

sqlmigrate会输出真实建表语句但不执行,适合论文数据表设计小节直接引用,对外键索引和联合约束做可视化说明。

2.2 用 bulk_create 导入评分数据并维护冗余统计字段

论文数据集通常来自公开评分集合或爬虫,行数从几千到几十万不等。逐条save()每条记录都要走一次完整 ORM 生命周期,几万条数据可能跑十几分钟。用bulk_create按批写入,批次大小设 5000,既能控制内存峰值又能减少事务次数。

# movies/management/commands/load_ratings.py import csv from django.core.management.base import BaseCommand from django.contrib.auth import get_user_model from movies.models import Movie, Rating class Command(BaseCommand): def handle(self, *args, **options): batch = [] with open("ratings.csv", encoding="utf-8") as f: for row in csv.DictReader(f): uid, mid = int(row["user_id"]), int(row["movie_id"]) user_exists = get_user_model().objects.filter(id=uid).exists() movie_exists = Movie.objects.filter(id=mid).exists() if not (user_exists and movie_exists): continue batch.append( Rating(user_id=uid, movie_id=mid, score=float(row["score"])) ) if len(batch) >= 5000: Rating.objects.bulk_create(batch, ignore_conflicts=True) batch.clear() if batch: Rating.objects.bulk_create(batch, ignore_conflicts=True)

这里把外键存在性判断写在循环内,虽然主键查询有索引,但十万行数据会产生十万次查询,速度并不理想。更快的方式是先读全表主键集合,再用内存集合判断存在性:先执行set(get_user_model().objects.values_list("id", flat=True)),之后用uid in user_ids,能省掉大部分数据库往返。ignore_conflicts=True负责处理评分数据集里常见的重复行,它不是抹掉数据,而是在撞上unique_together约束时跳过冲突记录而不中断整个批量任务。

数据导入完成后,要立刻做一次分布查询:

SELECT user_id, COUNT(*) AS cnt FROM movies_rating GROUP BY user_id ORDER BY cnt DESC LIMIT 20;

如果前几个用户的评分量在总数里占比过高,说明数据集偏斜严重,协同过滤会被这些重度用户主导,后面调参的效果也不会稳定。数据分布问题要在实验章节里单独说明,这才是论文该有的严谨度。

3. 基于用户的协同过滤推荐算法在 django 中的实现

3.1 UserCF 与 ItemCF 的选择逻辑

电影推荐场景里,用户口味相对稳定,观影行为没有资讯流那么强的时效性,更适合用 UserCF 刻画“相似的人”的影响。ItemCF 的直觉是“看过 A 的人常看 B”,它对短视频、资讯等兴趣快速变化的场景更有效,因为物品相似度能快速响应新热点。毕业论文选 UserCF 还有一个叙事优势:推荐理由可以直接写成“与你口味相似的用户也喜欢”,展示时每条输出都能对应到相似度计算过程,解释成本低。ItemCF 要解释为什么两件物品相似,则需要额外维护物品相似度表,论文篇幅和代码量都会增加。

3.2 皮尔逊相关系数与余弦相似度的取舍

评分尺度差异是 UserCF 面对的第一个问题。有人习惯只打 2 到 3 分,有人习惯给 4 到 5 分,直接用原始分差计算相似度,会把“分严的人”和“分松的人”误判为相差很远。皮尔逊相关系数先把每个用户的评分减去自身平均分再计算,相当于先拉平评分习惯再比较口味。

# recommender/similarity.py import math def pearson_similarity(ratings_a, ratings_b): """ratings_a、ratings_b 是 {movie_id: score} 字典""" common = set(ratings_a) & set(ratings_b) if len(common) < 2: return 0.0 n = len(common) mean_a = sum(ratings_a[m] for m in common) / n mean_b = sum(ratings_b[m] for m in common) / n numerator = sum( (ratings_a[m] - mean_a) * (ratings_b[m] - mean_b) for m in common ) denominator = math.sqrt( sum((ratings_a[m] - mean_a) ** 2 for m in common) ) * math.sqrt( sum((ratings_b[m] - mean_b) ** 2 for m in common) ) if denominator == 0: return 0.0 return numerator / denominator

两个保护条件值得单独说明。len(common) < 2防止两个用户只共同看过一部电影时算出相关系数接近 ±1,那是过度自信的噪声;denominator == 0防止某一方所有评分完全相同造成除零,真实数据里这类用户并不少见。参数名用ratings_aratings_b而不是user1user2,函数之后也能复用到物品相似度计算上。

相似度方法是否中心化适合数据类型计算开销
皮尔逊相关系数是,减去各自均分显式连续评分中等
余弦相似度隐式反馈、布尔行为高维特征更大
Jaccard 系数只看交集比例极稀疏评分最低

工程实践里,可以先跑 Jaccard 版本把链路打通,再替换成皮尔逊计算,两种方法得到的结果可以组成论文实验的两个对照组。Jaccard 只看两个用户是否对同一部电影有过评分行为,不关心分值,代码比皮尔逊短得多,但表达的语义也更粗。

3.3 相似用户查询与推荐集合组装

推荐服务拿到当前用户的评分字典后,需要找出相似邻居,再把邻居评过而当前用户没看过的电影按相似度加权汇总。

# recommender/services.py from collections import defaultdict from movies.models import Rating def recommend_for_user(user_id, top_k=20): target = dict( Rating.objects.filter(user_id=user_id).values_list("movie_id", "score") ) if not target: return [] rows = Rating.objects.exclude(user_id=user_id).values( "user_id", "movie_id", "score" ) profile = defaultdict(dict) for row in rows: profile[row["user_id"]][row["movie_id"]] = row["score"] weight_of = {} for other_id, scores in profile.items(): sim = pearson_similarity(target, scores) if sim > 0.3: weight_of[other_id] = sim sorted_neighbors = sorted(weight_of.items(), key=lambda x: x[1], reverse=True) neighbors = sorted_neighbors[:top_k] scores = defaultdict(float) for other_id, sim in neighbors: for movie_id, val in profile[other_id].items(): if movie_id in target: continue scores[movie_id] += val * sim return sorted(scores.items(), key=lambda x: x[1], reverse=True)[:10]

参数top_k=20和阈值0.3是主要调节点。top_k控制邻居个数,20 到 30 是电影推荐里常用的范围,再大容易把不相关用户卷进来;阈值0.3低于常见经验值,皮尔逊系数一般超过 0.3 才认为口味相近。exclude(user_id=user_id)用来排除自己,否则自己会出现在邻居列表里形成自环。返回值是(movie_id, predicted_score)的列表,接口层把它转成 JSON,模板层按需求切字段。

这段实现的瓶颈在profile构建,它把所有用户的评分全读进内存。几万用户、几十万评分时尚可接受,数据量到百万级就该换成矩阵分块计算或引入离线计算任务,这个边界在论文里写清楚也能体现对系统规模的理解。

4. 推荐结果的缓存策略与前端推荐位渲染

4.1 离线预计算与 Redis 缓存

UserCF 有一个特性:用户评分不变,推荐结果就不变。与其每次请求都重新算一遍,不如提前算好存进 Redis,接口只做读取。先在settings.py配置默认缓存后端。

# moviereco/settings.py CACHES = { "default": { "BACKEND": "django.core.cache.backends.redis.RedisCache", "LOCATION": "redis://127.0.0.1:6379/1", "TIMEOUT": 3600, } }

注意这里用的是 Django 自带的 Cache 框架,没有为 Redis 额外引入第三方库,因为django.core.cache.backends.redis从 Django 4.0 起就是内置能力。缓存键落在 1 号库,和业务数据分离,清空缓存时不会波及 MySQL。

接着写一个预计算命令,在低峰期遍历有评分行为的用户并将结果写入缓存。

# recommender/management/commands/precompute_recommendations.py from django.contrib.auth import get_user_model from django.core.cache import cache from django.core.management.base import BaseCommand from recommender.services import recommend_for_user class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument("--topk", type=int, default=20) def handle(self, *args, **options): users = ( get_user_model() .objects.filter(ratings__isnull=False) .distinct() ) for user in users: reco = recommend_for_user(user.id, top_k=options["topk"]) cache.set(f"reco:{user.id}", reco, 3600)

filter(ratings__isnull=False)是反向关联过滤,只留下至少有一条评分的用户,避免空跑无意义的相似度计算;distinct()防止多评分记录让同一用户被重复执行。缓存过期时间 3600 秒与夜间定时任务形成配合,每天自动刷新一轮。

策略计算时机缓存时间适用对象失败后果
离线预计算定时任务3600~86400 秒有评分的全部用户缓存未命中时回退在线计算
在线降级请求触发600 秒离线任务遗漏的用户响应延迟略增,结果仍可用
实时全量计算每次请求不缓存小数据调试期数据增大后接口超时

离线预计算和在线降级是互补的两层,不是二选一。表里的第三行是调试期的常见误用,数据量小看不出问题,评分量上来后再改结构就费劲了。

4.2 推荐接口封装与 StreamingHttpResponse 导出

接口层要做三件事:读缓存、缓存失效时兜底、返回结构统一。用 Django 原生 View 实现即可,论文项目不必为了一个接口引入 DRF。

# recommender/views.py from django.core.cache import cache from django.http import JsonResponse from django.views import View from recommender.services import recommend_for_user class RecommendView(View): def get(self, request): uid = request.user.id reco = cache.get(f"reco:{uid}") if reco is None: reco = recommend_for_user(uid) cache.set(f"reco:{uid}", reco, 600) data = [ {"movie_id": mid, "score": round(score, 4)} for mid, score in reco ] return JsonResponse({"code": 0, "data": data})

兜底写入的缓存时间从主任务的 3600 秒缩短到 600 秒,原因是该场景属于应急计算,下一次定时任务会覆盖,没必要长期占用 Redis 空间。返回结构统一为code/data,之后增加鉴权失败、参数错误等分支时不用改协议。

导出推荐结果为 CSV 时用 StreamingHttpResponse 更适合,列表很大时一次拼进内存会让临时占用翻倍,流式可以边生成边输出。

# recommender/views.py 追加 import csv import io from django.http import StreamingHttpResponse class RecoExportView(View): def get(self, request): uid = request.user.id reco = cache.get(f"reco:{uid}") or recommend_for_user(uid) def generate(): buffer = io.StringIO() writer = csv.writer(buffer) writer.writerow(["user_id", "movie_id", "predicted_score"]) for mid, score in reco: writer.writerow([uid, mid, round(score, 4)]) yield buffer.getvalue() response = StreamingHttpResponse( generate(), content_type="text/csv" ) response["Content-Disposition"] = ( "attachment; filename=recommendations.csv" ) return response

这里content_typeContent-Disposition两个响应头的职责不同:前者告诉客户端数据类型是 CSV,后者决定浏览器是内联打开还是触发下载。文件名不要加引号,某些浏览器会把引号解析进文件名导致后缀错乱。

4.3 用模板标签渲染推荐位

电影详情页下方的“为你推荐”是论文系统最容易出彩的展示位。用inclusion_tag实现最省事,它可以把用户 ID、预测分、电影链接一起交给模板。

# recommender/templatetags/reco_tags.py from django import template from django.core.cache import cache from recommender.services import recommend_for_user register = template.Library() @register.inclusion_tag("recommender/reco_block.html") def show_recommendations(user, count=5): if not user.is_authenticated: return {"reco": []} reco = cache.get(f"reco:{user.id}") or recommend_for_user(user.id) return {"reco": reco[:count]}

模板文件放在recommender/templates/recommender/reco_block.html

<div class="reco-block"> {% for movie_id, score in reco %} <a href="{% url 'movies:detail' movie_id %}"> 电影 ID {{ movie_id }},预测分 {{ score|floatformat:2 }} </a> {% empty %} <p>暂无推荐,不妨先看看排行榜</p> {% endfor %} </div>

在电影详情页里插入{% load reco_tags %}{% show_recommendations request.user %},模板标签会自己查缓存,命中就直接渲染,未命中才短暂触发算法。{% empty %}分支保证空结果时页面不会出现一整块灰屏,而是引导用户去看排行榜。

5. 验证推荐质量与数据边界检查

5.1 检查评分分布与 UserCF 的失效边界

推荐结果受数据分布影响极大,先做分布检查再进入调参阶段。用 ORM 聚合统计单评分用户占比。

from django.db.models import Count from movies.models import Rating row_counts = Rating.objects.values("user_id").annotate(c=Count("id")) total = max(row_counts.count(), 1) single_ratio = sum(1 for r in row_counts if r["c"] == 1) / total print("单评分用户占比:", round(single_ratio, 4))

单评分用户占比超过三成时,UserCF 的邻居集合抖动严重,皮尔逊系数的置信度很低。此时要在服务层做分层策略:只有一条评分的用户直接按那条评分的电影类型补全推荐,评分用户少的回退到热门榜,热门榜直接复用Movie表里的rating_count冗余字段排序。这个分层逻辑在论文里写一段“数据稀疏度分布与推荐兜底策略”,评审对它的认可度远高于笼统地完成推荐模块。

5.2 用留一法计算 MAE/RMSE 并定位 K 值

MAE 和 RMSE 是推荐论文必须出现的两个指标。留一法把每个用户的一条评分隐藏,用剩余数据预测隐藏评分,最后统计误差。

# recommender/evaluation.py import math from movies.models import Rating from recommender.services import recommend_for_user def evaluate_leave_one_out(user_ids, sample_size=200): mae_sum = 0.0 rmse_sum = 0.0 count = 0 for uid in user_ids[:sample_size]: ratings = list( Rating.objects.filter(user_id=uid).values_list("movie_id", "score") ) if len(ratings) < 3: continue hidden_movie, real_score = ratings[0] result = dict(recommend_for_user(uid)) if hidden_movie in result: error = result[hidden_movie] - real_score mae_sum += abs(error) rmse_sum += error * error count += 1 if count == 0: return {"mae": None, "rmse": None} return { "mae": mae_sum / count, "rmse": math.sqrt(rmse_sum / count), }

ratings[0]固定取第一条而不是随机抽样,是为了结果可复现;抽样 200 个用户已经能给出稳定估计。把这段代码跑在 K 从 5 到 50 的区间,画出 MAE 随 K 变化的曲线,取最低点对应的 K 值作为系统最终参数。

5.3 用 waitress+nginx 部署时确认推荐接口超时兜底

Windows 上开发好的 django 项目迁移到 Linux 服务器时,架设 WSGI 进程可以用 waitress,它不依赖 Unix 特有的 fork 机制,跨平台行为一致。启动命令如下。

pip install waitress waitress-serve --listen=0.0.0.0:8000 moviereco.wsgi:application

Nginx 转发请求的配置段里要留出足够长的读取超时,推荐接口一旦缓存未命中会触发在线降级计算。以下是配置示例。

server { listen 80; server_name movie.example.com; location /static/ { alias /var/www/moviereco/static/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 60s; } }

proxy_read_timeout 60s专门为缓存未命中的场景设置,如果在线降级计算超过 60 秒,Nginx 会提前断开。部署完成后直接在服务器上执行curl -w "%{time_starttransfer}"观察首字节时间,推荐接口响应若稳定在 80ms 以内说明缓存路径正常;一旦超过 500ms,优先检查 Redis 键是否过期,而不是急着调算法参数。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询