ARTICLE DETAIL

资讯详情

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

基于Python与Django的电影推荐系统:协同过滤、Mysql优化与工程实践

基于Python与Django的电影推荐系统:协同过滤、Mysql优化与工程实践 简介基于PythonDjangoMySQL的协同过滤电影推荐系统完整源码包定位于个人大作业级项目面向计算机类专业学生、毕业设计者及入门开发者可直接用于期末课程设计、实训答辩或推荐系统原理学习。资源共726个文件整体约19.68MB包含Python业务逻辑与pyc文件、MySQL数据库SQL脚本、Django应用结构、Vue前端组件及HTML/CSS/JS交互页面另含多种图片、图标、字体等静态资源与安装/运行/构建批处理脚本目录划分清晰便于定位不同模块。项目经严格调试评审分95分以上解压后配合脚本即可快速启动演示。目前已有785人学习下载。借助这套源码可以直观理解协同过滤算法的实现细节、Django与MySQL的数据交互方式以及前后端联调流程适合在真实课程项目中参考复用或二次开发。1. 拆解“基于python Django Mysql协同过滤的电影推荐系统”下载一个标注为“源码 数据库”的电影推荐系统 zip 包解压后最常碰到的不是算法不工作而是数据库导入失败、连接串写错以及几万条评分数据下推荐接口慢到不可用。标题里的四个词恰好构成了一套完整约束python 负责算法层与脚本逻辑Django 负责后台、接口和页面渲染Mysql 负责评分数据的持久化协同过滤负责从历史行为中找出“和你口味相近的人”或“跟你喜欢的电影相似的作品”。这套组合是课程设计、毕业设计和中小规模业务最常见的模板但很多人把协同过滤当默认解忽略了表结构、相似度函数、TopN 生成之间的耦合关系。下文顺着数据建模、算法落地、Django 查询集成的顺序把这套代码的关键点讲透最后给出离线评测与单用户回归验证的手段适合准备答辩、二次开发或准备把这套原型往生产方向迁移的工程师。2. 电影推荐系统的Mysql表设计users、movies、ratings与索引边界2.1 三张基础表的字段与约束标题里既然出现 Mysql第一步就是把数据模型定清楚。常见做法是建三张表用户表、电影表、评分表。评分表是整个推荐系统的核心它的写入质量直接决定协同过滤的结果。下面是一份最小可用的建表语句。CREATE TABLE users ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(32) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE movies ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, title VARCHAR(128) NOT NULL, genres VARCHAR(64) NOT NULL DEFAULT , release_year SMALLINT UNSIGNED NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE ratings ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id INT UNSIGNED NOT NULL, movie_id INT UNSIGNED NOT NULL, score TINYINT UNSIGNED NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_movie (user_id, movie_id), KEY idx_movie_user (movie_id, user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;评分表上的uk_user_movie联合唯一索引是一个比“代码里判断是否已评分”更可靠的防线。用户多次提交同一部电影评分时数据库层会把冲突拦截下来配合 Django 的get_or_create或IntegrityError捕获即可维持幂等。idx_movie_user是容易被漏掉的逆查询索引——ItemCF 阶段需要“某部电影被哪些人评过分”没有这个索引Mysql 只能走全表扫描。score用TINYINT UNSIGNED是刻意选择MySQL 里TINYINT只占 1 字节范围 0 到 255装 1 到 5 分的评分完全够用比INT省三倍空间对几百万行评分表来说这份空间节省会直接反映到 InnoDB 缓冲池的命中率上。2.2 从 zip 里的数据库文件导入 Mysql标题里的“数据库.zip”拿到手后通常是一个.sql文件而不是可以直接拷进data目录的物理文件。导入时先在 Mysql 里建库再把 sql 灌进去最后做行数核对。mysql -u root -p -e CREATE DATABASE movie_reco CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; mysql -u root -p movie_reco movie_reco.sql mysql -u root -p -e USE movie_reco; SHOW TABLES; SELECT COUNT(*) FROM ratings;CHARACTER SET utf8mb4必须显式指定很多二手项目里的 sql 文件只写了utf8遇到生僻片名或 emoji 会导致导入报警告。COLLATE utf8mb4_unicode_ci是通用排序规则对用户名和电影名做模糊匹配时行为更稳定。第三步的SHOW TABLES和SELECT COUNT(*)不是可选步骤——导入失败经常发生在中途表存在但行数不完整后面协同过滤算出来的相似度会缺一大块。2.3 数据量增长时的索引策略与 EXPLAIN 验证评分表是典型的 append-only 型数据行数会随用户行为线性上涨。当评分行数超过十万时索引选择就比算法本身更影响体验。下面是需要被优先满足的查询方向以及对应的索引结构。查询方向推荐索引常见误用某用户评过哪些电影(user_id, movie_id)只在 user_id 上建单列索引某电影被哪些用户评过(movie_id, user_id)依赖联合索引的倒序扫描按时间取最近评分(user_id, created_at)在 created_at 上建单列索引跑离线统计热门电影(movie_id, score)在 score 上建单列索引写完索引后不要凭感觉判断直接交给优化器验证。对“某个用户最近 20 条评分”这个最频繁的查询执行下面的命令看type和key两列。EXPLAIN SELECT * FROM ratings WHERE user_id 42 ORDER BY created_at DESC LIMIT 20;如果key列显示NULL说明没命中索引需要回头检查字段类型是否一致或者联合索引的字段顺序是否匹配查询条件。type列出现ALL表示全表扫描在十万行上就是几十毫秒到上百毫秒的差别在百万行上会直接把推荐接口拖垮。2.4 视图和触发器在推荐系统里的边界有些源码会顺手在 Mysql 里建视图把“用户最近评分 电影基本信息”预拼成一张宽表方便 Django 里一行查询。视图在数据量小、表结构稳定的前提下没问题但评分表持续写入时视图每次引用都要实时聚合性能会明显劣化。触发器同样要谨慎如果每次插入评分都触发一次相似度重算或统计字段更新批量灌入测试数据时会被放大数倍。把这类联动逻辑放到 Django 的应用层由信号或任务队列执行可读性和可控性都更好。3. 协同过滤算法在python里的落地相似度计算、评分预测与TopN生成3.1 选 UserCF 还是 ItemCF先看用户和物品的比例协同过滤包含两条路线基于用户的 UserCF 和基于物品的 ItemCF。电影推荐系统的典型特征是用户量远大于电影量一个几百人的小站点可能有上万部电影用户评分矩阵极度稀疏。此时 ItemCF 的工程收益更直接物品之间的相似度可以先离线算好存表线上只做“查相似电影 按评分加权排序”两步。UserCF 更适合用户量远小于物品量、且用户兴趣变化快的场景比如新闻资讯。维度UserCFItemCF计算开销用户两两相似度物品两两相似度线上预测找邻居用户再聚合找相似物品再聚合可解释性“和你品味相似的人喜欢”“因为你看过这部”电影场景适用度用户稀疏时差物品相对少时更好更新频率用户行为变化敏感物品关系相对稳定3.2 相似度计算余弦与皮尔逊的取舍相似度是协同过滤的地基。常用做法是对每行评分向量算余弦相似度但对评分尺度敏感的用户要先做均值归一化也就是皮尔逊相关系数。下面这段 python 代码同时实现了两种写法。import math from collections import defaultdict def cosine_sim(vec_a, vec_b): # vec_a / vec_b 是 {movie_id: score} 字典 inter set(vec_a) set(vec_b) if not inter: return 0.0 dot sum(vec_a[m] * vec_b[m] for m in inter) norm_a math.sqrt(sum(v * v for v in vec_a.values())) norm_b math.sqrt(sum(v * v for v in vec_b.values())) if norm_a 0 or norm_b 0: return 0.0 return dot / (norm_a * norm_b) def pearson_sim(vec_a, vec_b): inter set(vec_a) set(vec_b) if len(inter) 2: return 0.0 mean_a sum(vec_a[m] for m in inter) / len(inter) mean_b sum(vec_b[m] for m in inter) / len(inter) diff_a {m: vec_a[m] - mean_a for m in inter} diff_b {m: vec_b[m] - mean_b for m in inter} dot sum(diff_a[m] * diff_b[m] for m in inter) norm_a math.sqrt(sum(v * v for v in diff_a.values())) norm_b math.sqrt(sum(v * v for v in diff_b.values())) if norm_a 0 or norm_b 0: return 0.0 return dot / (norm_a * norm_b)len(inter) 2的判定是皮尔逊实现里最容易被忽略的细节只有一两个共同评分时算出的相关系数要么是 1 要么是 -1噪声极大。inter这个交集集合同时被用于均值计算和点积计算少一次遍历在数十万评分数据上能省下可观的耗时。这套代码不依赖 numpy纯 python 实现好处是方便断点调试坏处是百万级数据下会慢一个数量级——如果评分表超过 50 万行建议把norm_a、norm_b和评分均值做成预计算字段而不是每次实时算。3.3 评分预测公式与 TopN 生成相似度算完下一步是预测用户对未看过电影的评分。ItemCF 的常见做法是找到用户评过分的电影取与目标电影相似度最高的 K 部做加权平均。下面这段代码直接产出 TopN。def recommend_for_user(user_ratings, movie_sim, top_k10): # user_ratings: {movie_id: score} # movie_sim: {(movie_a, movie_b): similarity} scores defaultdict(float) weight_sum defaultdict(float) for movie_a, score in user_ratings.items(): for (m1, m2), sim in movie_sim.items(): if m1 ! movie_a: continue target m2 if target in user_ratings: continue if sim 0: continue scores[target] sim * score weight_sum[target] abs(sim) ranked [(scores[m] / weight_sum[m], m) for m in scores if weight_sum[m] 0] ranked.sort(reverseTrue) return ranked[:top_k]这里的加权公式sim * score / sum(sim)是 ItemCF 最简单可用的形态。abs(sim)只用于累加分母能保证权重符号不会把结果带偏。if sim 0的过滤值得多说一句负数相似度在电影推荐里通常意味着“看过这个就不看那个”把它硬塞进加权平均会拉低排序质量直接丢弃是最稳妥的工程做法。3.4 冷启动兜底与混合策略协同过滤对冷启动用户基本无能为力——一个只评过两三条分的用户邻居集合小得可怜。常见做法是准备一份热门榜兜底按评分人数和平均分加权排序取热度 Top50 放进推荐结果末尾。伪代码如下。hot_sql SELECT movie_id, AVG(score) AS avg_score, COUNT(*) AS cnt FROM ratings GROUP BY movie_id HAVING cnt 10 ORDER BY avg_score * LOG(cnt) DESC LIMIT 50 使用avg_score * LOG(cnt)而不是简单按平均值排序是为了防止一部电影只有 5 个人全打 5 分就冲上榜首。加上HAVING cnt 10之后热门榜就变成了一个有最低票数门槛的贝叶斯近似。协同过滤结果和热门榜做一个简单的比例混合比如前 8 条来自协同过滤后 2 条来自热门榜能同时照顾新用户和资深用户。4. Django连接Mysql的查询集成ORM调优与推荐接口的IO防线4.1 settings.py 里的 Mysql 连接参数拿到 zip 包之后第一件要改的事就是把 Django 默认的sqlite3配置切到 Mysql。DATABASES 配置里除了常规的主机、端口、账号还有几个参数对性能影响很大。DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: movie_reco, USER: reco_user, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, CONN_MAX_AGE: 60, OPTIONS: { charset: utf8mb4, connect_timeout: 5, init_command: SET sql_modeSTRICT_TRANS_TABLES, }, } }CONN_MAX_AGE 60表示连接在 60 秒内被复用避免每个请求都重新握手建连。对于推荐这种读多写少的接口连接复用的收益非常明显。connect_timeout 5是防止 Mysql 假死时 Django 请求线程被无限挂起。STRICT_TRANS_TABLES保证写入评分时类型不合法会直接抛错而不是被静默截断成 0 或空字符串——这点配合上一章的TINYINT字段能避免大量脏数据进入协同过滤训练集。4.2 ORM 查询用 select_related 和 prefetch_related 干掉 N1Django 的 ORM 惰性查询机制会在遍历外键时逐条补发 SQL这就是 N1 问题的来源。推荐接口要展示“你可能喜欢”列表每部电影都要带出标题、年份和类型如果写成movie.genres形式的访问10 条推荐会产生 11 条 SQL。改造方式如下。# 不推荐循环中访问外键字段 ratings Rating.objects.filter(user_iduser_id)[:100] for r in ratings: print(r.movie.title) # 推荐一次性 join 出电影信息 ratings ( Rating.objects .filter(user_iduser_id) .select_related(movie)[:100] ) for r in ratings: print(r.movie.title)select_related适用于外键和一对一关系它通过 SQL JOIN 把关联表的数据一次取回。多对多关系比如电影和导演、电影和演员应改用prefetch_related它会先查主表再查关联表最后在 python 层做合并避免产生笛卡尔积。判断标准很简单看 ORM 生成的 SQL 数量用django.db.connection.queries打印请求周期内的所有语句超过两条就要检查关联查询。4.3 推荐接口里的聚合查询与索引配合热门榜和“最近高分电影”这两类数据适合用 Django 的聚合函数直接从 Mysql 取而不是把全表拉进 python 再计算。下面的写法只发一条带GROUP BY的 SQL。from django.db.models import Count, Avg from movies.models import Movie hot_movies ( Movie.objects .annotate(cntCount(rating), avg_scoreAvg(rating__score)) .filter(cnt__gte10) .order_by(-avg_score, -cnt)[:30] )这个查询等价于前面的hot_sql但在 Django 内部更容易和分页、权限、缓存装饰器配合使用。注意filter(cnt__gte10)写在annotate之后对应 SQL 的HAVING子句写在annotate之前则变成WHERE语义完全不同——这个顺序问题是 Django ORM 使用里最隐蔽的坑之一。为了让GROUP BY movie_id走索引评分表上的(movie_id, score)联合索引在 Mysql 8.0 的优化器里比单列索引更能支撑这类聚合。4.4 离线计算与在线计算的边界协同过滤的相似度矩阵如果每次请求都实时算在 10 万条评分数据下一次矩阵运算动辄数百毫秒接口必然慢。常见做法是按数据量分档评分少于 5 万行每次请求实时计算先把相似度结果缓存到内存设置 5 分钟过期。评分在 5 万到 50 万行之间用 Django management command 把相似度矩阵写入一张movie_similarity表定时任务每天重算一次。超 50 万行把计算任务交给 Celery 异步队列算完再回写数据库。class Command(BaseCommand): def handle(self, *args, **options): # 伪代码遍历评分表计算相似度后写入 movie_similarity sim_rows compute_all_similarities() MovieSimilarity.objects.bulk_create(sim_rows, batch_size2000) self.stdout.write(similarity updated)bulk_create配合batch_size2000是写入相似度表时最保险的做法一次性创建几万行对象会导致 Django 生成超大 INSERT 语句容易触发 Mysql 的max_allowed_packet上限。重算任务放在凌晨低峰期执行接口只读movie_similarity表响应时间能稳定压在 50 毫秒以内。线上若需要更高吞吐再在movie_similarity表前加一层 Redis 缓存。5. 推荐结果的离线评测与单用户回归验证5.1 用留一法算 RMSE先有个量化基线推荐系统的第一版上线之前先用历史行为做一次离线的留一评测。把每个用户的评分随机留一条作为测试集其余进训练集预测被留出的评分并计算误差。from sklearn.metrics import mean_squared_error predictions [] ground_truth [] for user in test_users: for movie, actual in test_ratings[user].items(): pred predict_score(user, movie) # 你自己的预测函数 predictions.append(pred) ground_truth.append(actual) rmse mean_squared_error(ground_truth, predictions, squaredFalse) print(RMSE:, rmse)RMSE 对离群误差惩罚重能暴露“给某用户预测出 1 分但实际打了 5 分”这类严重偏差。多数电影评分在 3 到 5 分之间RMSE 跑到 1.0 以内算可接受超过 1.2 就说明相似度或评分归一化某一步存在系统性偏差。别只盯着 RMSE还要并行统计召回率 Recall10否则会出现“分数预测准但用户根本没兴趣看”的尴尬结果。5.2 单用户回归验证手动插 5 条评分看输出离线指标只能反映整体上线前要做一次最粗糙的手工验证选一个测试账号手动插入几条指向明确类型的评分然后观察推荐列表是否符合直觉预期。比如构造一个用户只评分《盗梦空间》《星际穿越》《黑客帝国》三部全部给 5 分推荐的 Top5 如果全是科幻片说明 ItemCF 的相似度链路基本正常。验证时用 Django shell 或一条管理命令直接打印推荐结果python manage.py shell -c from reco.models import *; print(recommend_for_user(42))这个命令输出的是推荐电影 id 列表回填到页面上直接比对。如果出现 disallowed 内容、热门片占一半以上优先检查冷启动兜底是不是把热门榜的权重调太高。最后再执行EXPLAIN确认线上推荐接口的查询走索引这套系统的核心链路就稳了。本文还有配套的精品资源点击获取
返回列表