ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Django的协同过滤电影推荐系统:从算法到缓存实战

基于Django的协同过滤电影推荐系统:从算法到缓存实战 简介这份Python基于Django的电影推荐系统毕业论文适合计算机相关专业毕业生在完成毕业设计或撰写论文时参考。文档围绕电影推荐系统的分析、设计与实现展开涵盖业务分析、功能需求分析、数据流分析、非功能需求分析以及架构设计、功能模块设计、数据库设计和界面设计等内容并对Mysql数据库和Django框架的使用做了详细说明。资源为单个doc文件大小1.07MB属于完整论文Word版可直接查看或编辑便于复制文本和调整格式。目前已有177人学习浏览适合需要借鉴系统结构、论文框架或快速理解DjangoMysql开发流程的读者。文档同时包含中英文摘要、章节式目录和关键模块描述能够帮助读者梳理写作思路是完成毕业设计论文的实用参考资料。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 movies和startapp 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_length255, blankTrue, verbose_name偏好电影类型 ) class Movie(models.Model): title models.CharField(max_length200, db_indexTrue, verbose_name电影名) genres models.CharField(max_length255, verbose_name类型列表) release_year models.IntegerField(nullTrue, blankTrue) avg_rating models.FloatField(default0.0, verbose_name平均分) rating_count models.IntegerField(default0, verbose_name评分人数) 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(verbose_name评分) created_at models.DateTimeField(auto_now_addTrue) class Meta: unique_together (user, movie)unique_together是最容易被忽视的约束它在数据库层面禁止同一用户对同一部电影重复写入。只在前端做判断存在竞态窗口高并发写同一部电影时可能出现重复评分联合约束会直接拒绝后一条写入。title加db_indexTrue是因为电影搜索是高频操作外键索引 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 0001sqlmigrate会输出真实建表语句但不执行适合论文数据表设计小节直接引用对外键索引和联合约束做可视化说明。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, encodingutf-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(iduid).exists() movie_exists Movie.objects.filter(idmid).exists() if not (user_exists and movie_exists): continue batch.append( Rating(user_iduid, movie_idmid, scorefloat(row[score])) ) if len(batch) 5000: Rating.objects.bulk_create(batch, ignore_conflictsTrue) batch.clear() if batch: Rating.objects.bulk_create(batch, ignore_conflictsTrue)这里把外键存在性判断写在循环内虽然主键查询有索引但十万行数据会产生十万次查询速度并不理想。更快的方式是先读全表主键集合再用内存集合判断存在性先执行set(get_user_model().objects.values_list(id, flatTrue))之后用uid in user_ids能省掉大部分数据库往返。ignore_conflictsTrue负责处理评分数据集里常见的重复行它不是抹掉数据而是在撞上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_a、ratings_b而不是user1、user2函数之后也能复用到物品相似度计算上。相似度方法是否中心化适合数据类型计算开销皮尔逊相关系数是减去各自均分显式连续评分中等余弦相似度否隐式反馈、布尔行为高维特征更大Jaccard 系数只看交集比例极稀疏评分最低工程实践里可以先跑 Jaccard 版本把链路打通再替换成皮尔逊计算两种方法得到的结果可以组成论文实验的两个对照组。Jaccard 只看两个用户是否对同一部电影有过评分行为不关心分值代码比皮尔逊短得多但表达的语义也更粗。3.3 相似用户查询与推荐集合组装推荐服务拿到当前用户的评分字典后需要找出相似邻居再把邻居评过而当前用户没看过的电影按相似度加权汇总。# recommender/services.py from collections import defaultdict from movies.models import Rating def recommend_for_user(user_id, top_k20): target dict( Rating.objects.filter(user_iduser_id).values_list(movie_id, score) ) if not target: return [] rows Rating.objects.exclude(user_iduser_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(), keylambda x: x[1], reverseTrue) 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(), keylambda x: x[1], reverseTrue)[:10]参数top_k20和阈值0.3是主要调节点。top_k控制邻居个数20 到 30 是电影推荐里常用的范围再大容易把不相关用户卷进来阈值0.3低于常见经验值皮尔逊系数一般超过 0.3 才认为口味相近。exclude(user_iduser_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, typeint, default20) def handle(self, *args, **options): users ( get_user_model() .objects.filter(ratings__isnullFalse) .distinct() ) for user in users: reco recommend_for_user(user.id, top_koptions[topk]) cache.set(freco:{user.id}, reco, 3600)filter(ratings__isnullFalse)是反向关联过滤只留下至少有一条评分的用户避免空跑无意义的相似度计算distinct()防止多评分记录让同一用户被重复执行。缓存过期时间 3600 秒与夜间定时任务形成配合每天自动刷新一轮。策略计算时机缓存时间适用对象失败后果离线预计算定时任务360086400 秒有评分的全部用户缓存未命中时回退在线计算在线降级请求触发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(freco:{uid}) if reco is None: reco recommend_for_user(uid) cache.set(freco:{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(freco:{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_typetext/csv ) response[Content-Disposition] ( attachment; filenamerecommendations.csv ) return response这里content_type和Content-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, count5): if not user.is_authenticated: return {reco: []} reco cache.get(freco:{user.id}) or recommend_for_user(user.id) return {reco: reco[:count]}模板文件放在recommender/templates/recommender/reco_block.htmldiv classreco-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(cCount(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_size200): mae_sum 0.0 rmse_sum 0.0 count 0 for uid in user_ids[:sample_size]: ratings list( Rating.objects.filter(user_iduid).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 用 waitressnginx 部署时确认推荐接口超时兜底Windows 上开发好的 django 项目迁移到 Linux 服务器时架设 WSGI 进程可以用 waitress它不依赖 Unix 特有的 fork 机制跨平台行为一致。启动命令如下。pip install waitress waitress-serve --listen0.0.0.0:8000 moviereco.wsgi:applicationNginx 转发请求的配置段里要留出足够长的读取超时推荐接口一旦缓存未命中会触发在线降级计算。以下是配置示例。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 键是否过期而不是急着调算法参数。本文还有配套的精品资源点击获取
返回列表