ARTICLE DETAIL

资讯详情

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

Django+Python电影推荐系统:从协同过滤到Web开发实战

Django+Python电影推荐系统:从协同过滤到Web开发实战 简介一套基于Django框架与Python语言的电影推荐系统设计源码面向希望掌握协同过滤算法、Django全栈开发及MySQL数据操作的开发者可作为课程设计、毕业设计或个人项目练手的完整参考。压缩包内共725个文件约37.65MB包括Python后端代码、Vue组件、JavaScript脚本、CSS样式、HTML页面以及SVG、GIF、PNG等常见界面素材另附SQL脚本与bat安装启动命令目录按功能划分得较为清晰便于快速定位所需模块。当前已有449人学习下载。源码完整实现了从电影数据管理、用户行为记录到协同过滤推荐生成的流程前后端分层明确适合边读边改、按模块拆解学习也可在此基础上二次扩展是理解推荐系统与Web项目整合方式的良好范本。1. 电影推荐系统从零到能跑为什么选 Django Python 而不是现成推荐平台课程设计要交源码、毕业设计要跑通演示、求职作品集要能讲清楚推荐链路——知乎和 GitHub 上一搜「电影推荐系统」排名靠前的多是 Django Python 的完整工程包。这个组合能火不是没有道理Django 把用户、电影、评分这三类数据的增删改查安排得明明白白Python 又恰好是写协同过滤最顺手的语言前后端不用分离一个项目就能从数据库一路通到网页。对需要交付「设计源码」的人来说它是最容易跑通也最容易讲清原理的选型。这篇笔记不讲花哨算法只讲怎么把一套基于 Django 框架的电影推荐系统真正落地目录怎么拆、数据表怎么建、推荐引擎怎么写、上线前哪里最容易翻车。2. 先搭骨架项目结构、数据模型与 Django 推荐系统的三个基础配置很多人拿到推荐系统的源码包第一件事就是python manage.py runserver结果要么模块找不到要么数据表没迁移要么登录页直接 500。我一般接到这种需求先不碰算法把工程骨架立住。骨架对了后面推荐算得再漂亮都能跑起来骨架错了光找依赖就能耗掉一下午。这一章按「建项目 → 配设置 → 建表」的顺序来每一步都给出能直接抄的代码和参数说明。2.1 建项目还是建 appDjango 工程拆分与目录约定标题里的「设计源码」意味着你要交付的是一整个可运行的工程而不是单文件脚本。常见做法是用django-admin先建项目再按业务边界拆出两个 app一个管用户一个管电影与推荐。这样拆的好处是后面接登录、接后台管理、接 API 的时候互不干扰。django-admin startproject movie_recommend cd movie_recommend python manage.py startapp account python manage.py startapp recommend拆完之后的目录长这样movie_recommend/ ├── manage.py ├── movie_recommend/ │ ├── settings.py # 全局配置 │ ├── urls.py # 根路由 │ └── wsgi.py ├── account/ │ ├── models.py # 自定义用户表 │ ├── views.py │ └── ... └── recommend/ ├── models.py # 电影表、评分表 ├── services/ │ └── recommend_service.py # 推荐算法独立成 service ├── views.py ├── urls.py └── ...manage.py startapp是 Django 创建 app 的标准命令这个命令在官方文档里叫「django创建app」也是新手最容易跳过的一步——所有模型都塞进默认工程目录会越写越乱。这里我把recommend下额外加了一层services/目录专门放推荐算法。这个目录是整个源码的核心亮点视图里只做 HTTP 处理和模板渲染协同过滤、相似度计算全部收进 service 层。面试时讲「推荐算法和 Web 层解耦」这句话比贴一堆代码更有说服力。2.2 三个必配项INSTALLED_APPS / 数据库 / 静态资源settings.py 是 Django 项目的总开关对推荐系统这种偏展示型的项目90% 的启动失败都出在下面三块配置。第一块是注册 app不注册account和recommend后面的迁移命令根本找不到模型第二块是数据库课程设计和毕设直接用 SQLite 就够了第三块是静态资源不然 CSS 和图片全部 404。# movie_recommend/settings.py INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, account, recommend, ] DATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: BASE_DIR / db.sqlite3, } } AUTH_USER_MODEL account.User STATIC_URL /static/ STATICFILES_DIRS [BASE_DIR / static] MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media参数说明INSTALLED_APPS里新增的两个 app 名必须和startapp创建的目录名一致AUTH_USER_MODEL是自定义用户表的开关必须在第一次迁移前就配好后面会单独讲这个坑SQLite 的NAME用BASE_DIR / db.sqlite3表示数据库文件放在项目根目录方便整体打包交付。如果你打算部署到云服务器上提供给多人使用SQLite 并发写会有锁竞争建议换成 MySQL但本地开发和 Demo 演示 SQLite 足够。下面是两种数据库的选型对比做技术方案说明时可以直接引用数据库适用场景注意事项SQLite课程设计、毕业设计、单机演示零安装文件即数据库不适合高并发写入MySQL正式部署、多用户访问需要额外建库注意字符集用 utf8mb4静态资源这一项很多人忽略STATICFILES_DIRS指向的目录需要手动创建。推荐系统的海报图、前端 CSS、评分按钮的样式都放这里模板里用{% static css/style.css %}引用。没有这一行配置项目跑起来页面是纯文本观感差一大截。2.3 用 Django ORM 建 User-Movie-Rating 三张表电影推荐系统的数据模型非常固定核心就是三张表用户、电影、评分。用户表直接继承 Django 自带的AbstractUser省去重写登录注册的功夫电影表存标题、类型、年份评分表是用户和电影的多对多关系表也是推荐算法唯一需要的数据源。# account/models.py from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): nickname models.CharField(max_length50, blankTrue, verbose_name昵称) def __str__(self): return self.username# recommend/models.py from django.db import models from django.conf import settings class Movie(models.Model): title models.CharField(max_length200, verbose_name电影名) genres models.CharField(max_length100, blankTrue, verbose_name类型) release_year models.IntegerField(nullTrue, blankTrue, verbose_name上映年份) avg_rating models.FloatField(default0, verbose_name平均评分) def __str__(self): return self.title class Rating(models.Model): user models.ForeignKey( settings.AUTH_USER_MODEL, 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)字段设计和逻辑说明Movie表里的genres用逗号分隔的字符串存类型如「动作,冒险,科幻」不做第三张类型表因为推荐算法暂时用不上复杂的关系查询这样最简单avg_rating是展示用的平均分可以定期统计写入不必每次请求都现算。Rating表通过两个外键关联用户和电影related_nameratings让user.ratings和movie.ratings能反向查出全部记录。unique_together保证同一个用户对同一部电影只能有一条评分记录这是推荐系统的硬约束否则矩阵构建会重复计算。建好模型后执行迁移python manage.py makemigrations python manage.py migrate python manage.py createsuperusermakemigrations的作用是把模型变化生成迁移文件migrate才真正落库。createsuperuser创建管理员账号用于登录 Django 自带的后台你可以直接在后台录入电影数据比手写 SQL 快得多。此时把三张表建好Django 的骨架就立住了下一步才是真正的推荐算法。3. 推荐引擎怎么写从协同过滤到内容推荐的 Django 实现骨架搭完接下来是标题里最核心的部分推荐引擎。这一章从算法选型讲到我实际落地的代码实现最后把算法收进 service 层。电影推荐系统最常用的算法不是深度学习而是协同过滤原因只有一个它只需要评分数据不需要电影的内容特征冷启动和数据要求都最低。对设计源码来说协同过滤的代码量小、可解释性强老师或面试官一眼就能看懂你在算什么。3.1 协同过滤为什么是电影系统的最优解推荐算法的常见分支有基于内容的推荐、基于知识的推荐和协同过滤。基于内容推荐需要给每部电影打标签、做词向量工作量全在特征工程上协同过滤则完全依赖用户行为——用户看过什么、给多少分。电影推荐系统刚好是协同过滤的经典适用场景用户数量几百到几千电影数量几千到几万评分矩阵虽然稀疏但用户之间和电影之间都存在明显的相似性。协同过滤又分两类UserCF基于用户的协同过滤和 ItemCF基于物品的协同过滤。UserCF 找和你口味相似的用户把他们看过而你还没看的电影推荐给你ItemCF 找和你历史上喜欢的电影相似的其他电影。两者对比下来电影场景更适合 ItemCF因为电影的数量相对稳定物品相似度可以离线算好存起来而用户的兴趣会漂移UserCF 每次都要重新算用户相似度成本高且用户少时相似度极不稳定。所以我在这个项目里选 ItemCF。3.2 基于物品的协同过滤ItemCF核心实现ItemCF 的计算分四步第一步构建「用户-电影」评分矩阵第二步统计电影两两间的共现次数计算相似度第三步用用户已评分的电影作为权重对候选电影加权求和第四步过滤掉用户已经看过的输出 Top-N。下面这段代码就是这个项目的核心算法我把它放在recommend/services/itemcf.py里import math from collections import defaultdict def build_user_movie_matrix(ratings): ratings: list of dict每条包含 user_id, movie_id, score user_movies defaultdict(dict) for item in ratings: user_movies[item[user_id]][item[movie_id]] item[score] return user_movies def calc_movie_similarity(user_movies): 基于共现矩阵计算电影间余弦相似度 movie_count defaultdict(int) co_occur defaultdict(lambda: defaultdict(int)) for movies in user_movies.values(): for m1 in movies: movie_count[m1] 1 for m2 in movies: if m1 ! m2: co_occur[m1][m2] 1 similarity defaultdict(dict) for m1, related in co_occur.items(): for m2, cnt in related.items(): similarity[m1][m2] cnt / math.sqrt(movie_count[m1] * movie_count[m2]) return similarity def recommend_for_user(user_id, user_movies, similarity, top_n10): 给指定用户生成 Top-N 推荐列表 scored defaultdict(float) user_ratings user_movies.get(user_id, {}) for m1, score in user_ratings.items(): for m2, sim in similarity.get(m1, {}).items(): if m2 in user_ratings: continue scored[m2] score * sim ranked sorted(scored.items(), keylambda x: x[1], reverseTrue) return [movie_id for movie_id, _ in ranked[:top_n]]逻辑说明build_user_movie_matrix把扁平评分记录转成嵌套字典外层键是用户 ID内层是电影 ID 到评分的映射这是后面所有计算的数据基础。calc_movie_similarity是关键先统计每部电影的评分人数movie_count再统计两部电影共同被多少用户评过共现次数co_occur最后用cnt / sqrt(movie_count[m1] * movie_count[m2])做归一化。这个公式本质是余弦相似度的简化版用共现次数除以两个物品流行度的几何平均能有效抑制热门电影和所有电影都相似的问题。recommend_for_user里用用户已有评分的分数乘以相似度累加等于「我喜欢《黑客帝国》9分它和《盗梦空间》相似度 0.6那《盗梦空间》得分就是 5.4」非常直观。参数说明top_n默认 10实际项目里我一般设为 12 或 20因为网页一行展示 4 部电影12 部正好三行m1 ! m2的判断是防止把自己和自己算成相似。这段代码的复杂度是 O(用户数 × 电影数²)个人项目能跑通但用户量上万后必须加后续章节的过滤优化否则一次计算要几分钟。3.3 把推荐结果做成 Django 可复用的 service 层算法写好后最忌讳的就是在views.py里直接调用这些函数。电影推荐系统的业务里推荐结果可能被页面、API、后台三个地方用到所以我要在recommend/services/recommend_service.py里包一层统一入口from recommend.models import Rating, Movie from recommend.services.itemcf import ( build_user_movie_matrix, calc_movie_similarity, recommend_for_user, ) _SIMILARITY_CACHE None _CACHE_KEY None def get_top_movies_for_user(user, top_n12): 对外暴露的统一推荐入口登录用户走个性化未登录走热门兜底 if not user.is_authenticated: return Movie.objects.order_by(-avg_rating)[:top_n] ratings Rating.objects.filter(useruser) if not ratings.exists(): return Movie.objects.order_by(-avg_rating)[:top_n] all_ratings list( Rating.objects.all().values(user_id, movie_id, score) ) user_matrix build_user_movie_matrix(all_ratings) similarity calc_movie_similarity(user_matrix) rec_ids recommend_for_user(user.id, user_matrix, similarity, top_n) # 保持推荐顺序不用 filter(id__in...) movies list(Movie.objects.filter(id__inrec_ids)) movies.sort(keylambda m: rec_ids.index(m.id)) return movies这段代码有几个容易忽略的点。第一get_top_movies_for_user接收的是 Django 的user对象内部用user.is_authenticated判断登录状态未登录用户直接取全站平均分最高的电影这就是冷启动兜底策略。第二Rating.objects.all().values(...)把 ORM 查询转成纯 Python 字典列表是为了把数据喂给上一章的算法函数避免在算法内部混用 Django 查询集。第三filter(id__inrec_ids)返回的 QuerySet 不保证顺序和rec_ids一致所以最后用sort按推荐列表的顺序重排这个细节不处理页面展示的排序就会乱跳。参数top_n在 service 层默认 12业务层可以按场景覆盖。4. 把推荐结果变成网页视图、模板与用户行为采集推荐算法算出来只是半成品用户要通过网页看到结果、打上评分系统才能真正闭环。这一章讲视图层怎么接 service、模板怎么写、用户打分怎么回填。电影推荐系统能不能作为完整源码交付就看这一环通不通。4.1 视图层 View 与接口设计Django 的推荐系统视图不需要复杂逻辑核心就是「拿到 user调 service渲染模板」。我用类视图View而不是函数视图因为后续如果要加POST处理打分逻辑类视图可以用get/post方法分开写代码更整洁。# recommend/views.py from django.shortcuts import render from django.views import View from django.http import JsonResponse import json from recommend.models import Movie, Rating from recommend.services.recommend_service import get_top_movies_for_user class HomeView(View): template_name index.html def get(self, request): movies get_top_movies_for_user(request.user, top_n12) # 顺便查一下用户已评过分的电影模板里用来打勾 rated_ids set() if request.user.is_authenticated: rated_ids set( Rating.objects.filter(userrequest.user) .values_list(movie_id, flatTrue) ) return render(request, self.template_name, { movies: movies, rated_ids: rated_ids, })路由配置用path(, HomeView.as_view())挂到根路径登录用户访问首页看到的就是个性化推荐结果。rated_ids这个查询用values_list(flatTrue)直接取出一串 movie_id转成set后模板里判断是否已评分就非常快不用每次循环查库。这个小优化对推荐系统页面的体验影响很明显不然用户每点开一部电影都要多一次数据库查询。配套的还有评分接口负责接收用户打出的分数def rate_movie(request): if request.method ! POST: return JsonResponse({code: 1, msg: 仅支持POST}) data json.loads(request.body) movie_id data.get(movie_id) score float(data.get(score, 0)) if not request.user.is_authenticated: return JsonResponse({code: 1, msg: 请先登录}) movie Movie.objects.get(idmovie_id) Rating.objects.update_or_create( userrequest.user, moviemovie, defaults{score: score} ) return JsonResponse({code: 0, msg: 评分成功})逻辑说明评分接口用update_or_create而不是create是因为用户可能反复调整对同一部电影的评分这个方法存在就更新、不存在就插入正好对应Rating表上的unique_together约束。json.loads(request.body)是解析前端传来的 JSON 数据所以前端请求头里必须带Content-Type: application/json。float(data.get(score, 0))做了类型转换防止数据库报错。4.2 模板渲染与推荐列表展示推荐结果最终落在templates/index.html上。Django 模板引擎的变量引用和条件判断在这里展示得最完整{{ movie.title }}输出电影名{% for %}循环列表{% empty %}处理空推荐的情况。如果新用户一部电影都没评分get_top_movies_for_user会返回热门电影兜底所以empty分支实际很少触发但写上是给代码健壮性加分。{# templates/index.html 核心片段 #} {% load static %} !DOCTYPE html html langzh-CN head meta charsetUTF-8 title电影推荐系统/title link relstylesheet href{% static css/style.css %} /head body div classcontainer h1为你推荐的电影/h1 div classmovie-grid {% for movie in movies %} div classmovie-card h3{{ movie.title }}/h3 p{{ movie.genres }}/p p豆瓣参考{{ movie.avg_rating|default:暂无 }}/p {% if movie.id in rated_ids %} button classrated-btn disabled已评分/button {% else %} button classrate-btn>// static/js/main.js function getCookie(name) { let cookieValue null; if (document.cookie document.cookie ! ) { const cookies document.cookie.split(;); for (let i 0; i cookies.length; i) { const cookie cookies[i].trim(); if (cookie.substring(0, name.length 1) (name )) { cookieValue decodeURIComponent(cookie.substring(name.length 1)); break; } } } return cookieValue; } document.querySelectorAll(.rate-btn).forEach(btn { btn.addEventListener(click, function () { const movieId this.dataset.movieId; const score prompt(给这部电影打分1-10); if (!score) return; fetch(/api/rate/, { method: POST, headers: { X-CSRFToken: getCookie(csrftoken), Content-Type: application/json }, body: JSON.stringify({ movie_id: movieId, score: score }) }) .then(res res.json()) .then(data { if (data.code 0) { alert(打分成功推荐将更新); location.reload(); } else { alert(data.msg); } }); }); });这端 JS 的逻辑说明getCookie是从浏览器 cookie 里读出 Django 写入的csrftokenDjango 渲染模板时如果页面包含表单或{% csrf_token %}标签就会自动种下这个 cookie。fetch请求头里带上X-CSRFToken服务器校验才能通过。>CREATE DATABASE movie_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;同时检查settings.py里数据库配置的OPTIONS加一行charset: utf8mb4。已经建好的表可以用ALTER TABLE recommend_movie CONVERT TO CHARACTER SET utf8mb4;补救。字符集这类问题玄学居多排查时先看数据库连接是否指定了字符集再看表和字段的默认字符集。5.3 协同过滤内存爆炸全量循环卡死现象数据量不大几百个用户、几千部电影但点击首页后要等十几秒甚至直接超时。日志显示算法计算在calc_movie_similarity的嵌套循环里一直跑不完。原因用户评分矩阵虽稀疏但calc_movie_similarity对每个用户的电影列表做了两两组合。假设一个用户评了 300 部电影他一个人就贡献了 300 × 299 次共现统计100 个这样的用户循环次数就到了千万级别。个人项目能跑但演示机器性能不足时立刻卡死。解决在构建相似度矩阵前加一道过滤——只保留评分人数超过阈值的电影冷门电影不参与相似度计算。我在calc_movie_similarity外面包了一层get_popular_movies(min_rating_count20)先用一个Counter统计每部电影的评分人数过滤后再送进算法。阈值不要设太高20 左右比较合适。另外把相似度矩阵结果缓存到内存文件里重复计算只在数据变化时发生。这个优化做完计算时间能从十几秒降到一秒以内。5.4 POST 403CSRF 验证失败现象前端点击「我来打分」按钮浏览器 Network 面板显示 POST 请求返回403 Forbidden后端日志提示CSRF verification failed。原因Django 的 CSRF 中间件默认开启模板页面里没有渲染{% csrf_token %}或者前端 JS 的fetch请求头没带X-CSRFToken。我见过有人用csrf_exempt装饰器把接口的保护关掉这样确实能跳过 403但源码里出现csrf_exempt是很明显的安全隐患答辩时被问到会很被动。解决模板的form里加{% csrf_token %}异步请求从 cookie 读取csrftoken并放进请求头就是 4.3 节那段getCookie函数的用途。fetch请求必须同时设置X-CSRFToken头和Content-Type: application/json两者缺一都会失败。前端请求如果带了 token 仍然 403检查 cookie 是否真的叫csrftoken——域名不一致时 Django 可能换了名字。5.5 推荐结果永远不变数据没进算法 or 缓存未失效现象用户反复打分刷新页面后推荐列表顺序一动不动。原因一般是两个方向的问题。第一评分数据没有真正写入Rating表update_or_create因为字段名不对或外键查不到而静默失败页面弹了「评分成功」但库里没数据。第二算法层的内存缓存没有失效相似度矩阵还是旧数据新评分根本没参与后续计算。解决先在 Django 后台或python manage.py shell里查Rating表是否有新记录排除写入问题。再看算法层如果用了模块级缓存变量需要在rate_movie视图里清掉缓存。我的做法是给缓存设一个过期时间戳推荐算法每次先检查缓存是否过期_SIMILARITY_CACHE None _SIMILARITY_CACHE_TIME 0 _CACHE_TTL 60 * 60 * 6 # 6 小时过期 def get_similarity(): global _SIMILARITY_CACHE, _SIMILARITY_CACHE_TIME import time if _SIMILARITY_CACHE and time.time() - _SIMILARITY_CACHE_TIME _CACHE_TTL: return _SIMILARITY_CACHE # 重新计算相似度...这个逻辑把 TTL 作为可调参数演示时可以临时改成 10 秒打完分刷新立刻能看到推荐变化讲原理时说「生产环境 6 小时更新一次」也说得通。6. 推荐算法调优与性能验证从能跑到能用能跑通只是及格推荐质量才是源码的加分项。最后一章讲三个具体技巧相似度计算怎么选参数、冷启动和缓存怎么处理、用什么指标验证推荐效果。6.1 相似度计算的三种方式与参数选择前面用的是简化版余弦相似度实际做推荐系统时还有两种常见选择皮尔逊相关系数和 Jaccard 相似度。三者适用场景区别明显相似度方式适用场景特点余弦相似度评分数据完整度一般不受用户打分离散程度影响最常用皮尔逊相关系数用户评分习惯差异大去中心化抗「有人只给高分、有人只给低分」Jaccard 相似度只有行为没有评分只看是否看过不看分高低参数选择上课程设计和毕设用余弦相似度就够如果你发现推荐结果总是偏向热门电影可以试试皮尔逊——它先减去用户平均分再算相似度能削弱用户打分尺度的影响。Jaccard 适合那种只收集了「点击/收藏」行为的数据集。6.2 冷启动处理与缓存优化推荐系统上线第一天最怕「无数据可推」这就是冷启动。我的方案分两层用户冷启动新用户没有评分直接返回全站平均分最高的电影电影冷启动新入库的电影没有评分在热门推荐列表里把release_year比较新的电影加权提升让新片有机会曝光。缓存优化上面已经提过核心是把相似度矩阵这种重计算任务的 TTL 拉到小时级而不是每次用户请求都重新算。个人源码里能做到这一步已经比一堆直接runserver的示例工程专业得多。6.3 用简单的评估指标验证推荐质量推荐效果不能靠感觉说「还行」要能量化。离线评估最简单的方法是留一法把每个用户的评分数据按时间排序用前 80% 的评分去推荐看后 20% 的评分里有多少部电影被推荐命中。命中率就是召回率。实际操作先手动给几部电影打高评分刷新首页看推荐列表是否包含同类电影这一步人工验证能快速发现算法方向对不对。等数据量积累到几百条评分后再用代码做离线评估。def precision_at_n(recommended_ids, heldout_ids, n10): hits set(recommended_ids[:n]) set(heldout_ids) return len(hits) / n我自己的体会是电影推荐系统的源码算法调参不是最难的最难的是让数据闭环——用户行为能采回来、算法能吃到新数据、推荐结果能随行为变化。以前我做课程设计只盯着推荐函数跑通页面展示好看就收工后来被问「你怎么证明推荐是有效的」才意识到离线评估和冷启动策略才是源码的护城河。从那以后我养成了一个习惯任何推荐改动上线前先跑一次离线命中率把评估脚本当成项目的一部分提交到源码里。你在这个项目里看到的precision_at_n就是那次踩坑后加的。希望帮到你。本文还有配套的精品资源点击获取
返回列表