ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

基于Python协同过滤的电影推荐系统开发与答辩全解析

基于Python协同过滤的电影推荐系统开发与答辩全解析 简介这是一套基于Python与协同过滤算法的电影推荐系统完整毕设项目主要面向计算机相关专业需要完成课程设计或毕业设计的学生。系统采用Django框架与MySQL数据库具备管理员和用户双角色涵盖电影分类、信息管理、评分与推荐等核心模块能够有效演示协同过滤推荐逻辑在实际业务中的落地。资源包共690个文件大小21.8MB以Python源码、Vue前端页面、JavaScript、SQL数据库脚本为主同时包含大量SVG图标、CSS样式及多张操作截图便于理解整体架构和界面实现。包内附有万字论文、项目运行说明、自动安装与启动脚本、数据库备份文件以及完整的视频演示能够帮助使用者快速搭建环境、复现项目功能。当前已有58人学习下载适合作为高分毕业设计参考或推荐系统入门实战项目。1. 这个高分大作业到底在交付什么一份能答辩、能复现的电影推荐系统如果你拿到过这样的课程设计任务最贵的往往不是算法本身而是怎么把一套推荐系统包装成老师愿意给高分的东西。基于 Python 和协同过滤算法的电影推荐系统源码再配上万字论文、演示视频、数据库和文档这个标题几乎是高校大作业里最标准的组合拳。它解决的核心问题很直接用公开的电影评分数据通过协同过滤算法算出用户可能没看过的电影然后把“数据清洗 → 算法实现 → Web 接口 → 数据库落库 → 论文写作”整条链路串起来。适合的人群是计算机相关专业的学生以及想拿完整项目做作品集的转行开发者。这份工作的技术难度卡得刚刚好——比爬虫有理论深度比管理系统有说服力演示效果又足够直观。下面我按这个标题里的每一项把实现路径、参数细节和踩坑点拆开讲。2. 先立住协同过滤的原理评分矩阵、相似度公式和两种算法怎么选任何推荐系统都逃不开一个核心假设相似的人会喜欢相似的东西或者被某人喜欢的东西也可能被另一个人喜欢。协同过滤就是把这个假设变成可计算的矩阵运算。2.1 用 pandas 把评分表变成用户-电影矩阵算法第一步是把数据库里的评分表变成“用户行、电影列”的矩阵。常见的数据集是 MovieLens 的ratings.csv字段一般有userId、movieId、rating、timestamp评分范围是 0.5 到 5.0。原始表长这样是没法直接算相似度的需要做透视。import pandas as pd ratings pd.read_csv(data/ratings.csv) print(ratings.head()) # 每个用户至少评过 5 部每部电影至少被 5 个人评过缓解稀疏矩阵 user_cnt ratings[userId].value_counts() movie_cnt ratings[movieId].value_counts() ratings ratings[ratings[userId].isin(user_cnt[user_cnt 5].index)] ratings ratings[ratings[movieId].isin(movie_cnt[movie_cnt 5].index)] # 透视成用户 x 电影矩阵没评过的位置补 0 user_item ratings.pivot_table( indexuserId, columnsmovieId, valuesrating ).fillna(0) print(user_item.shape) user_item.to_pickle(data/user_item.pkl)这段代码里的fillna(0)只是为了把矩阵补成矩形后面计算相似度时必须小心0 不代表“不喜欢”而是“没看过”所以不能把它当成真实评分参与余弦相似度的分母。阈值 5 是经验值如果数据集很稀疏可以降到 3如果只是想快速演示也可以只保留 100 个用户和 200 部电影保证矩阵不至于全是 0。2.2 基于用户的协同过滤余弦相似度与评分加权公式基于用户的思路是先找到和你评分习惯最像的一批人再用他们的评分预测你没看过的电影。相似度计算最常用的是余弦相似度。import numpy as np import pandas as pd def cosine_similarity(a, b): # 只取两边都有评分的维度 mask (a 0) (b 0) if mask.sum() 0: return 0.0 a_, b_ a[mask], b[mask] return float((a_ b_) / (np.linalg.norm(a_) * np.linalg.norm(b_) 1e-9)) def predict_user_score(user_id, movie_id, matrix, top_k10): if movie_id not in matrix.columns: return None user_vec matrix.loc[user_id] sims [] for other_id, other_vec in matrix.iterrows(): if other_id user_id: continue sim cosine_similarity(user_vec, other_vec) if sim 0: sims.append((sim, other_id)) sims.sort(keylambda x: x[0], reverseTrue) best sims[:top_k] total_weight 0 total_score 0 for sim, other_id in best: score matrix.loc[other_id, movie_id] if score 0: total_score sim * score total_weight sim if total_weight 0: return matrix[movie_id].mean() return total_score / total_weight user_item pd.read_pickle(data/user_item.pkl) print(predict_user_score(1, 10, user_item, top_k10))这里我故意没有做“均值中心化”。实际评分里有人习惯给高分、有人习惯给低分加权平均会被用户偏好带偏。更稳的写法是先把每个用户的评分减去他自己的平均分最后预测时再加回来。top_k一般取 10数据量大可以取 20相似度小于等于 0 的邻居直接丢掉避免负相似度把结果拉向错误方向。2.3 基于物品的协同过滤为什么更适合电影场景基于用户的算法在用户量大的时候计算量会爆炸而且新用户没有评分记录时完全失效。于是更常见的做法是反过来先算出电影之间的相似度矩阵推荐时只看用户已经评过分的电影找到这些电影的“近亲”推荐出去。def item_based_recommend(user_id, matrix, top_k10, sim_k10): user_vec matrix.loc[user_id] rated_items user_vec[user_vec 0].index score_acc {} weight_acc {} for item in rated_items: # 找出与当前电影最相似的 sim_k 部电影 sim_list [] for other in matrix.columns: if other item: continue sim cosine_similarity(matrix[item], matrix[other]) if sim 0: sim_list.append((sim, other)) sim_list.sort(keylambda x: x[0], reverseTrue) for sim, other in sim_list[:sim_k]: if other in rated_items: continue score_acc[other] score_acc.get(other, 0) sim * user_vec[item] weight_acc[other] weight_acc.get(other, 0) sim if not weight_acc: return [] rank sorted( ((score_acc[m] / weight_acc[m], m) for m in weight_acc), reverseTrue ) return [m for _, m in rank[:top_k]] user_item pd.read_pickle(data/user_item.pkl) print(item_based_recommend(1, user_item, top_k5, sim_k10))实现中最容易出错的地方是权重归一化不能直接按加权和排序必须把相似度累加值作为分母除掉否则热门电影会因为被推荐次数多而霸榜。sim_k表示每部电影只看最相似的 10 部这样能在稀疏矩阵里控制噪声。为什么电影场景更适合用物品协同过滤因为电影数量远小于用户数量相似度矩阵可以离线算好存成.pkl线上推荐时只需要查表加乘加几毫秒就能出结果。2.4 两种算法的选取边界与冷启动处理基于用户适合社交类场景基于物品适合内容消费类场景。课程设计里我建议优先做基于物品因为演示时随便输入一个老用户都能稳定输出 Top-N而且你可以把“电影相似度检索”单拎出来作为一个亮点。冷启动问题必须提前处理新用户没有评分记录任何协同过滤都算不了常见做法是直接返回全局热度榜新电影没有评分相似度矩阵里它全是 0常见做法是用电影类型标签填充相似度而不是硬算评分。注意不要指望模型对冷门电影给出可靠推荐。答辩时如果老师问到直接承认这是协同过滤的固有边界然后说明你的兜底策略这个回答比硬扛更有分。3. 从源码到可运行系统最小项目怎么搭、推荐接口怎么写原理讲清楚之后接下来是把源码落成一个能演示的工程。很多大作业的源码问题不是算法写得差而是文件散成一堆老师打开分不清谁是谁。3.1 项目骨架分清楚源码、数据、SQL、docs 各管各的我一般会按这样的目录组织紧凑但不乱。movie_recommend/ ├── app.py # Flask 入口 ├── requirements.txt # 依赖清单 ├── data_prepare.py # 数据清洗与矩阵构建 ├── recommender/ │ ├── __init__.py │ ├── itemcf.py # 基于物品的协同过滤核心 │ └── data_loader.py # 读取和缓存数据 ├── data/ │ ├── ratings.csv │ ├── movies.csv │ └── user_item.pkl ├── database/ │ └── schema.sql # 建库建表脚本 ├── docs/ │ ├── 论文大纲.md │ └── 演示脚本.md └── output/ └── recommend_result.csvdata里放原始数据和预处理后的矩阵database里放 SQLoutput里放推荐结果。答辩时只需要把目录一展开老师就知道这个项目结构是完整的。3.2 准备评分数据清洗 MovieLens 的 ratings.csv上一步代码已经完成了核心清洗这里补充一个更容易被忽视的点时间戳异常。import pandas as pd # 时间戳过滤MovieLens 数据基本都在 2000 年之后 ratings pd.read_csv(data/ratings.csv) ratings ratings[ratings[timestamp] 946684800] # 把 userId 和 movieId 统一转成 int避免后续表连接时类型不匹配 ratings[userId] ratings[userId].astype(int) ratings[movieId] ratings[movieId].astype(int) ratings.to_csv(data/ratings_clean.csv, indexFalse)时间戳阈值 946684800 是 2000-01-01 的 Unix 时间如果你的数据源里混进了测试数据这个过滤能把明显异常的行去掉。转整数这一步很多人会漏掉CSV 读进来有时是字符串后面和数据库表做关联时就会报类型错误。3.3 把 ItemCF 封装成 30 行的核心类工程化并不是要写一堆抽象类而是把算法封装成能单独测试的类。# recommender/itemcf.py import numpy as np import pandas as pd class ItemCF: def __init__(self, matrix): self.matrix matrix self.item_ids list(matrix.columns) staticmethod def _cosine(a, b): mask (a 0) (b 0) if mask.sum() 0: return 0.0 a_, b_ a[mask], b[mask] return float((a_ b_) / (np.linalg.norm(a_) * np.linalg.norm(b_) 1e-9)) def recommend(self, user_id, top_n10): # 新用户兜底返回全局平均评分最高的电影 if user_id not in self.matrix.index: return self.matrix.mean(axis0).sort_values(ascendingFalse).index[:top_n].tolist() user_vec self.matrix.loc[user_id] rated user_vec[user_vec 0].index score_acc, weight_acc {}, {} for item in rated: for other in self.item_ids: if other item or other in rated: continue sim self._cosine(self.matrix[item], self.matrix[other]) if sim 0: continue score_acc[other] score_acc.get(other, 0) sim * user_vec[item] weight_acc[other] weight_acc.get(other, 0) sim if not weight_acc: return [] ranked sorted( ((score_acc[m] / weight_acc[m], m) for m in weight_acc), reverseTrue ) return [m for _, m in ranked[:top_n]]看到这里你可能会问两重循环是不是太慢了确实这个实现是 O(已评电影数 × 电影总数)对 100 部电影没问题对上万部就会明显变慢。课程设计里建议把数据规模控制在几百部电影算法跑得动老师也不会觉得太玩具。如果你要处理完整 MovieLens 1M 数据至少得先把相似度矩阵预处理保存。3.4 用 Flask 暴露推荐接口一个 GET 返回 Top-N演示系统需要一个可交互的入口。Flask 是最轻量的方案不需要写前端页面也能通过 URL 直接演示。# app.py import pandas as pd from flask import Flask, request, jsonify from recommender.itemcf import ItemCF app Flask(__name__) user_item pd.read_pickle(data/user_item.pkl) model ItemCF(user_item) app.route(/recommend, methods[GET]) def recommend(): user_id request.args.get(user_id, typeint) top_n request.args.get(top_n, default10, typeint) if user_id is None: return jsonify({error: user_id is required}), 400 try: movies model.recommend(user_id, top_n) return jsonify({user_id: user_id, movies: movies}) except Exception as e: return jsonify({error: str(e)}), 500 if __name__ __main__: app.run(host127.0.0.1, port5000, debugFalse)request.args.get会自动把 URL 参数转成 int参数缺失时返回 400。debugFalse是演示时的安全选项因为 Flask 调试模式会暴露代码交互工具答辩现场万一被点到错误页面观感很差。启动后访问下面这个地址就能看到结果。3.5 本地跑通的最小命令序列pip install flask pandas numpy python data_prepare.py python app.py curl http://127.0.0.1:5000/recommend?user_id1top_n5三条命令依次做完推荐接口就能返回 JSON 数组。先跑data_prepare.py生成user_item.pkl再启动 Flask最后用 curl 验证接口。如果 curl 返回[]不要急着改代码先确认这个用户是否在矩阵里评过电影很可能只是冷启动用户。4. 数据库与数据闭环MySQL 表设计、推荐结果缓存与演示数据来源标题里有“数据库”意味着数据库不能只是个摆设。最稳妥的做法是让数据从 CSV 进入 MySQL再从 MySQL 读出来做推荐和统计这样论文里可以写“系统采用 MySQL 持久化存储用户行为数据”。4.1 选型课程设计里 MySQL 为什么比 SQLite 更“显功”如果只交一个能跑的程序SQLite 完全够用但高分大作业通常需要展示 ER 图、Navicat 截图和 SQL 查询语句MySQL 在这方面的教学资源和可视化工具更成熟。我一般建议用 MySQL而不是为了省事退回 SQLite。注意字符集一定要用utf8mb4否则电影名里的冷门符号会乱码。4.2 建表与索引users、movies、ratings 三张核心表推荐系统只需要三张核心业务表再加一张推荐结果缓存表。CREATE DATABASE IF NOT EXISTS movie_rec DEFAULT CHARACTER SET utf8mb4; USE movie_rec; CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL ) ENGINEInnoDB; CREATE TABLE movies ( id INT PRIMARY KEY, title VARCHAR(200) NOT NULL, genres VARCHAR(100), release_year INT ) ENGINEInnoDB; CREATE TABLE ratings ( user_id INT NOT NULL, movie_id INT NOT NULL, rating DECIMAL(2,1) NOT NULL, rated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (user_id, movie_id), FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (movie_id) REFERENCES movies(id), INDEX idx_movie_id (movie_id) ) ENGINEInnoDB; CREATE TABLE recommendations ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, movie_id INT NOT NULL, rank TINYINT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_rank (user_id, rank) ) ENGINEInnoDB;联合主键(user_id, movie_id)能防止同一条评分重复插入idx_movie_id是给基于物品的算法准备的它需要频繁按电影查所有用户的评分。recommendations表里的唯一键是为了保证同一个用户不会写出两个第 1 名。rating用DECIMAL(2,1)而不是FLOAT因为评分是半分为单位的离散值用定点数更严谨。4.3 把推荐结果写回数据库离线缓存每次请求都现场跑相似度计算看似灵活但在演示时非常容易超时。更合理的做法是定时批量算好结果写入recommendations表接口直接查表。import pymysql import pandas as pd from recommender.itemcf import ItemCF user_item pd.read_pickle(data/user_item.pkl) model ItemCF(user_item) conn pymysql.connect( host127.0.0.1, userroot, password123456, databasemovie_rec, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor ) with conn.cursor() as cursor: cursor.execute(DELETE FROM recommendations) for uid in user_item.index.tolist(): rec_movies model.recommend(uid, top_n10) for rank, mid in enumerate(rec_movies, start1): cursor.execute( INSERT INTO recommendations(user_id, movie_id, rank) VALUES(%s, %s, %s), (uid, mid, rank) ) conn.commit() conn.close()先清空表再写入避免旧数据残留导致推荐排名对不上。enumerate(..., start1)让排名从 1 开始而不是 0和前端展示保持一致。如果是练习项目循环插入几百行就够了如果数据量大换成executemany。密码不要写在源码里答辩前改成读取本地配置文件并且视频演示时不暴露密码。4.4 演示视频里的数据来源让数据库和图片“长”在一个故事里论文里最需要的图表其实都可以从 MySQL 里直接查出来画。import pymysql import pandas as pd import matplotlib.pyplot as plt conn pymysql.connect( host127.0.0.1, userroot, password123456, databasemovie_rec, charsetutf8mb4 ) df pd.read_sql(SELECT rating, COUNT(*) AS cnt FROM ratings GROUP BY rating, conn) df.plot.bar(xrating, ycnt) plt.title(Rating Distribution) plt.savefig(docs/rating_dist.png, dpi150)这张评分分布图放进论文里能同时证明两件事数据量够、数据分布合理。演示视频里录一段 Navicat 查询ratings表的过程再切到这张图老师会觉得你的数据链路是完整的而不是只拿了一个 CSV 硬凑。5. 避坑清单这类系统最容易翻车的 5 个问题与排查方法协同过滤推荐系统的坑不在公式而在数据和工程细节。下面这五个问题是我帮别人改课设时见过的最高频翻车点。5.1 新用户一进来就报 KeyError现象接口输入一个不在矩阵里的用户 ID页面直接抛KeyError: 99。原因matrix.loc[user_id]找不到对应行代码没有做边界判断。解决在recommend()入口先判断user_id in self.matrix.index不在就直接返回热门榜。热门榜怎么算用matrix.mean(axis0)求每部电影的平均分排序取前 10。这个兜底逻辑我在第 3 章已经写进ItemCF.recommend()里把它当成标配而不是加分项。5.2 推荐列表全是同一部电影现象Top-N 结果里七八个位置都是同一部电影。原因权重没有归一化热门电影被反复累加相似度分数被次数放大。比如某部电影和很多高分电影都相似在score_acc里被加了 5 次最终数值碾压其他电影。解决维护一个weight_acc最终排序前用score / weight做加权平均。判断这个坑有个更简单的办法打印rank列表的前几项如果 score 数值明显高于平均值大概率没归一化。5.3 MySQL 插入中文电影名乱码现象Navicat 里看到“变形金刚”变成“????”。原因建库时没有指定字符集或者 PyMySQL 连接串里缺charsetutf8mb4又或者 CSV 文件本身是gbk编码。解决建库语句写清楚DEFAULT CHARACTER SET utf8mb4连接串加charsetutf8mb4读取 CSV 时用encodingutf-8。如果电影名里有特殊字符比如圆点、引号也可以把字段改成VARCHAR(200)不要用TEXT缩短长度。5.4 论文里写的算法和源码完全对不上现象论文大标题写“基于用户的协同过滤”代码里跑的是ItemCF答辩时老师一翻代码就穿帮。原因先写了论文框架再补代码或者代码改了版本但论文没同步。解决论文必须在代码跑通之后写流程图里的函数名直接用源码里的类名和方法名。最稳妥的写作顺序是先动手实现再根据实现结构反向梳理“数据来源 → 矩阵构建 → 相似度计算 → 推荐接口 → 数据库存储”每一步都能对应到具体文件和函数。5.5 演示现场等了 5 秒才出结果现象点击“推荐”之后页面转圈很久。原因每次请求都在循环里重新计算电影相似度矩阵一大就扛不住。解决启动时把相似度矩阵一次性算好保存到data/similarity.pkl请求时只做查表如果项目只演示少量用户还可以直接查recommendations表连算法都不用跑。演示环境数据量控制在 100 用户 / 200 电影以内本地机器性能非常有限别把互联网级数据塞进课设。6. 答辩前的最后一公里论文结构、视频演示节奏与验证技巧高分大作业的最后一道坎不是代码是展示。论文不需要写得多玄但必须让老师快速看到“你做了什么事、用了什么方法、怎么证明有效”。论文章节应写内容对应素材绪论与背景为什么做电影推荐协同过滤的价值冷启动问题切入数据获取与预处理MovieLens 数据、清洗阈值数据量统计图算法原理UserCF/ItemCF 公式、相似度定义相似度矩阵热力图系统设计ER 图、模块划分、接口说明Flask 页面截图实验与评价留一法 RMSE 或推荐覆盖率对比表格总结与展望冷启动、稀疏矩阵的改进方向未来方案视频演示的节奏比内容更重要。我建议控制在 3 到 5 分钟前 40 秒只展示目录结构中间留 2 分钟真实跑一次推荐最后 30 秒展示数据库里的结果。很多人的失败原因是前面讲算法讲太久真正演示接口时只剩下几十秒。不要背稿每次演示录完后自己看一遍重点检查有没有弹错误页面。关于验证最简单的方法是“留一法”随机删掉用户一个评分用剩下数据预测这个评分最后计算 RMSE。这个实验不需要大型计算却能让论文里的“实验”章节有真实数据支撑也让答辩组的老师看到一个完整的评估闭环。我自己当年做类似课设时吃过一个哑巴亏答辩前夜把项目调通后没有再完整跑一遍结果现场端口被占用页面打开是错误页。后来我养成了两个习惯一是所有服务都绑定127.0.0.1二是答辩前把流程从头到尾录三遍并把关键 SQL 查询和推荐结果截图打印成纸面备份。希望帮到你至少别在答辩前半小时修端口那种紧张感真的不值得体验。本文还有配套的精品资源点击获取
返回列表