ARTICLE DETAIL

资讯详情

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

基于Python协同过滤的电影推荐系统:从相似度矩阵到混合推荐实现

基于Python协同过滤的电影推荐系统:从相似度矩阵到混合推荐实现 简介基于Python与协同过滤算法的电影推荐系统毕业设计面向计算机相关专业本科生或需要完成推荐系统课题的开发者可用于理解协同过滤算法落地与Django Web系统开发流程。压缩包共689个文件涵盖Python后端源码、Vue前端页面、JavaScript与CSS样式、SQL数据库脚本以及视频演示、论文文档和启动脚本等包体约21.77MB。项目已通过本机编译调试功能覆盖管理员与用户双角色包含用户管理、电影分类、电影信息管理、电影评分管理等模块可帮助学习者快速掌握协同过滤推荐机制的实现思路。当前已有73人学习下载配套内容中包含安装运行批处理和关键页面组件适合毕设参考、课设扩展或推荐系统入门实践。1. 基于 Python 和协同过滤算法做电影推荐系统为什么适合当毕业设计每年毕设季推荐系统都是计算机、软件工程专业的高频选题。原因很直白它既不像 Spring Boot 增删改查那样被写烂也不像深度强化学习那样动不动就卡在显存和训练时间上。基于 Python 和协同过滤算法的电影推荐系统恰好踩在“完整闭环”和“实现可控”的交点上——用户评分数据、相似度计算、Top-N 推荐、离线评估全部可以用 pandas 和 numpy 在本地跑通剩下的时间可以安心投入源码整理、论文写作和视频演示。这个题目对应两种常见做法一是直接调用推荐算法库把精力放在 Web 端二是从零手写 UserCF 与 ItemCF把算法细节讲透。对需要源码和万字论文的毕业设计而言我一般推荐后者因为答辩时老师大概率会追问“相似度怎么算”“K 值怎么选”手写实现能让你回答得有底气。2. 协同过滤的数据基石用户-物品矩阵与相似度计算协同过滤的核心假设是“相似的人喜欢相似的东西”它不关心电影的类型、导演和演员只关心用户和物品之间的历史交互。把交互关系整理成矩阵再在矩阵上做相似度计算就是整个电影推荐系统的引擎。很多 python 入门者在拿到数据集后第一件事就是写推荐函数这是不合适的数据形态没定好后面所有算法都会跟着变形。2.1 UserCF 与 ItemCF 的选型边界协同过滤算法在电影推荐场景里有两条主路径基于用户的协同过滤UserCF和基于物品的协同过滤ItemCF。两者在原理上是镜像关系一个从用户角度找“志趣相投的人”一个从物品角度找“相关电影”。选哪条路径取决于你的业务约束和离线计算能力。对比维度UserCF 基于用户ItemCF 基于物品相似度对象用户之间的评分向量电影之间的评分向量适用场景用户规模小、兴趣变化快物品规模小、用户行为密集离线开销用户数增长时矩阵爆炸物品数增长时矩阵爆炸冷启动表现新用户无邻居可用新电影无相似物品可解释性“和你相似的人喜欢”“因为你喜欢某电影”在电影推荐系统里用户数量通常远大于电影数量ItemCF 的离线计算压力更小推荐结果也更稳定。但毕业设计为了展示完整的协同过滤原理我建议两条路径都实现再做对比实验这样论文里可以多一张对比表。2.2 用 pandas 加载电影评分数据并构造稀疏矩阵经典 MovieLens 100K 数据集的结构是ratings.csv保存用户对电影的评分movies.csv保存电影标题和类型。如果自己做毕设完全可以按同样的字段格式组织本地数据也可以用爬虫抓取豆瓣或 TMDB 的公开接口补一批数据但离线实验阶段直接用 CSV 文件最省事。import pandas as pd import numpy as np # ratings.csv 至少需要三个字段用户ID、电影ID、评分 ratings pd.read_csv(ratings.csv, names[user_id, movie_id, rating, timestamp], header0) # movies.csv 记录电影元数据用于把 movie_id 映射成标题 movies pd.read_csv(movies.csv, names[movie_id, title, genres], header0) # 过滤掉评分次数过少的用户和电影减少矩阵稀疏度 user_count ratings[user_id].value_counts() movie_count ratings[movie_id].value_counts() ratings ratings[ratings[user_id].isin(user_count[user_count 5].index)] ratings ratings[ratings[movie_id].isin(movie_count[movie_count 5].index)]加载完成后把宽表转成“用户-物品”矩阵。这里有一个关键操作pandas 的pivot_table会把缺失的评分填成 NaN而协同过滤里这些位置本质是“未评分”而不是“评了 0 分”处理时要用 0 填充但语义上要记得它们是未知项。这段代码里我过滤了评分数少于 5 的用户和电影这一步能显著降低矩阵稀疏度并且减少后面相似度计算的内存压力。pivot_table的index和columns参数分别决定矩阵的行和列values是评分缺失位置自动变成 NaN再用fillna(0)统一填充。2.3 矩阵化的余弦相似度计算有了评分矩阵相似度计算就可以用线性代数一次性算完。余弦相似度衡量的是两个向量之间的夹角夹角越小越相似公式是A·B / (|A| * |B|)。在 numpy 里把矩阵X归一化后转置相乘就能得到所有用户或所有物品的两两相似度矩阵这比写双层 for 循环快得多。# 稀疏矩阵归一化后做矩阵乘法一次性得到全部用户相似度 matrix user_item_matrix.values norm_matrix matrix / np.linalg.norm(matrix, axis1, keepdimsTrue) user_sim norm_matrix norm_matrix.T np.fill_diagonal(user_sim, 0) # 自己和自己的相似度设为 0矩阵版相似度计算的本质是“归一化后点积”一句话就能说清楚。先说为什么归一化如果不除以模长评分数多的人天然得分高相似度会被活跃度污染。np.linalg.norm默认求 L2 范数axis1表示按行即每个用户计算keepdimsTrue是为了保持维度对齐才能参与下一次除法。是矩阵乘法norm_matrix乘它的转置得到行列都为用户数的方阵第 i 行第 j 列就是用户 i 和用户 j 的余弦相似度。最后用np.fill_diagonal清零对角线的自相似度以免在 K 近邻选取时把自己选进去。如果要在论文里写得更有理论深度可以再把余弦相似度换成皮尔逊相关系数差别在于皮尔逊计算前先按用户均值做中心化能消除用户打分习惯的偏移——有人喜欢打高分有人习惯给低分皮尔逊对这种情况更鲁棒。但在工程实现上中心化和余弦计算合并成一行即可代价是多一次按行减均值。3. 基于用户的协同过滤实现从相似邻居到 Top-N 推荐理论立住之后必须落到能跑的代码上。基于用户的协同过滤UserCF实现思路分四步构建用户物品矩阵、计算用户相似度、选出 K 个最近邻、预测目标用户对未看过的电影的评分并排序。3.1 评分预测公式均值偏差才是关键直接加权求平均有一个经典问题两个用户评分习惯完全不同。A 用户给所有电影平均打 4 分B 用户平均打 2 分但两人的评分次序一致直接用绝对评分算相似度会被带偏预测值也会失真。常见的做法是在计算平均分时引入基准线b_u和b_i把用户偏差和物品偏差都扣掉。这里给出最常用的预测公式pred(u, i) r_u_bar Σ sim(u, v) * (r_v,i - r_v_bar) / Σ sim(u, v)r_u_bar是用户 u 的历史平均评分r_v,i是邻居 v 对物品 i 的原始评分r_v_bar是邻居 v 的平均评分sim(u, v)是余弦相似度。简单理解就是先看邻居 v 对电影 i 的打分偏离他自己平均分多少再用相似度加权汇总最后加到目标用户平均分上。3.2 手写 UserCF 核心代码邻居选取与评分预测下面给出一段完整的实现直接喂pandas.DataFrame就能跑出针对某个用户的推荐列表。def predict_rating(user_id, movie_id, user_item_matrix, user_sim, k20): 预测 user_id 对 movie_id 的评分 if movie_id not in user_item_matrix.columns: return None if user_id not in user_item_matrix.index: return None # 当前用户的评分平均值 b_u user_vec user_item_matrix.loc[user_id].values user_mean user_vec[user_vec 0].mean() if user_vec.sum() 0 else 0 # 找出该电影已评分的用户集合 movie_ratings user_item_matrix[movie_id] rated_users movie_ratings[movie_ratings 0].index # 从相似度矩阵中取出当前用户和这些已评分用户的相似度 sims user_sim.loc[user_id, rated_users] sims sims[sims 0].sort_values(ascendingFalse).head(k) numerator, denominator 0.0, 0.0 for neighbor_id, sim in sims.items(): neighbor_vec user_item_matrix.loc[neighbor_id].values neighbor_mean neighbor_vec[neighbor_vec 0].mean() r_vi movie_ratings[neighbor_id] numerator sim * (r_vi - neighbor_mean) denominator sim if denominator 0: return user_mean return user_mean numerator / denominator这段代码里有两个容易写错的地方。第一个是rated_users的选取必须只保留对该电影评过分的用户否则相似度矩阵里大量为 0 的项会拉低预测。第二个是相似度阈值sims 0负相似度邻居不应该参与加权否则结果会出现反向叠加。参数k是邻居数它对预测准确度的影响比相似度算法本身还大。我一般设 10 到 30 之间离线实验时多做几组对比k5、k10、k20、k40画出准确率曲线后选拐点。拐点通常出现在 20 附近太小模型偏向局部太大则把大量低相似度用户引入投票结果趋向平均分。调用预测函数生成 Top-N 推荐时常见做法是先把目标用户没看过的电影全部预测一遍然后按分数从高到低取前 N 个。def recommend(user_id, N10, k20): watched user_item_matrix.loc[user_id] watched watched[watched 0].index candidates [m for m in user_item_matrix.columns if m not in watched] scored [(m, predict_rating(user_id, m, user_item_matrix, user_sim, k)) for m in candidates] scored [s for s in scored if s[1] is not None] scored.sort(keylambda x: x[1], reverseTrue) return scored[:N]推荐列表生成时有一个性能陷阱候选电影全部跑一遍预测复杂度是 O(未看物品数 × 邻居数)对 100K 数据集完全够用但如果换成 100 万评分的大数据集这个循环就会很慢。到那时可以考虑“先粗筛后精排”的策略或改用矩阵向量化的方式批量预测但毕设场景通常不需要。3.3 UserCF 的离线优化相似度矩阵的存储与更新用户数从 600 涨到 10 万时用户相似度矩阵会从 3MB 膨胀到 80GB 以上这是 UserCF 最现实的瓶颈。毕业设计里解决这个问题有两个思路一是记录更新日志只在用户产生新评分时局部重算他与其他用户的相似度行二是干脆不存储完整矩阵计算 Top-N 时实时取评分相似的用户配合倒排索引减少候选集规模。论文里把这个点写透可以明显增加“系统设计”部分的份量。4. 基于物品的协同过滤与冷启动兜底策略把矩阵转置基于用户的协同过滤立刻变成基于物品的协同过滤ItemCF。电影的数量通常少于用户数量所以 ItemCF 的相似度矩阵更小、离线计算更频繁是很多线上电影推荐系统的真实选择。4.1 ItemCF 的推荐逻辑与电商差异ItemCF 不再关心“谁和谁兴趣相同”而是计算“哪些电影容易被同一批人喜欢”。它的优势在于可解释性和稳定性用户看完《蝙蝠侠黑暗骑士》系统推荐《盗梦空间》理由是“喜欢这部电影的人也会喜欢那部”评委一看就懂。这里有一个容易混淆的点线上电影网站的 ItemCF 和购物网站的“看了又看”有本质差别购物场景强调实时同现电影场景更依赖长期评分矩阵关联所以离线计算的批次更新频率可以放得很低。代码实现和 UserCF 几乎对称相似度计算时只要把矩阵转置其余步骤完全共享。def build_item_similarity(user_item_matrix): 基于物品的协同过滤离线阶段计算物品间的相似度矩阵 返回 DataFrame行索引和列索引都是 movie_id matrix user_item_matrix.values # 按列物品归一化再乘转置得到物品相似度 norm_matrix matrix / np.linalg.norm(matrix, axis0, keepdimsTrue) item_sim norm_matrix.T norm_matrix np.fill_diagonal(item_sim, 0) return pd.DataFrame(item_sim, indexuser_item_matrix.columns, columnsuser_item_matrix.columns)这段代码与第 2 章的唯一差别在axis0——归一化的方向从“按用户”变成“按电影”。逻辑上行是用户、列是电影axis0就是把每部电影看成一个向量再通过矩阵乘法得到电影两两相似度。如果你同时实现了 UserCF 和 ItemCF建议把相似度计算抽成一个公共函数参数用axis控制两份代码维护一份核心逻辑代码量能省很多。4.2 基于物品的评分预测与 Top-N 生成物品相似度矩阵建好后预测评分就不需要查看邻居用户了只需要用户自己的历史评分记录。def predict_rating_item(user_id, movie_id, item_sim, user_item_matrix, k20): 利用 ItemCF 预测评分取用户已评电影中最相似的 k 部加权 if movie_id not in item_sim.columns: return None user_rated user_item_matrix.loc[user_id] user_rated user_rated[user_rated 0] sims item_sim[movie_id].loc[user_rated.index] sims sims[sims 0].sort_values(ascendingFalse).head(k) numerator, denominator 0.0, 0.0 for rated_movie, sim in sims.items(): numerator sim * user_rated[rated_movie] denominator sim return numerator / denominator if denominator 0 else user_rated.mean()这里不做均值修正因为 ItemCF 的加权对象是同一个用户的评分偏移自然抵消。注意head(k)选的是“与目标电影最相似的 k 部已看过的电影”不是任意相似电影所以要限定在user_rated.index里取交集之后再做排序顺序不要写反。生成推荐列表的方式与 UserCF 相同遍历用户未看过的电影并打分排序。性能上 ItemCF 有明显优势相似度矩阵的规模从“用户数 × 用户数”缩小到“电影数 × 电影数”而且电影新增速度远低于用户注册速度离线构建一次可以服务本周所有在线请求。4.3 冷启动兜底与混合推荐策略协同过滤的硬伤是冷启动新注册的用户没有评分记录无法计算相似度刚上线的电影没有任何交互永远进不了候选池。毕设系统的常见做法是叠加一个“热度榜”作为兜底层评分预测函数返回 None 时直接返回全局热门电影列表。def hybrid_recommend(user_id, N10, k20, alpha0.8): alpha 为协同过滤结果占比(1-alpha) 为热门兜底占比 cf_result recommend(user_id, N2*N, kk) # 多取一倍混合后截断 hot_pool ratings.groupby(movie_id)[rating].mean().sort_values(ascendingFalse) hot_list hot_pool.head(30).index seen set() result [] # 交错式混合协同过滤结果和热门结果交叉放置最终截断到 N for cf_item, hot_item in zip(cf_result, hot_list): if cf_item[0] not in seen: result.append(cf_item) seen.add(cf_item[0]) if hot_item not in seen: result.append((hot_item, None)) seen.add(hot_item) if len(result) N: break return resultalpha是混合权重但这段代码用的是更简单的“位置交错”策略前 N 个位置里协同过滤和热门各占一部分避免热门完全淹没个性化结果。对于新用户可以直接设定alpha0全走热门推荐等评分行为积累到 5 条以上再切换到alpha0.8。混合策略的调参空间很大论文里可以把它作为一个实验变量处理。5. 毕业设计落地评估指标、论文图表与演示技巧最后一公里的工作分三块量化推荐效果、把结果变成论文图表、准备一个不会翻车的现场演示。5.1 离线评估分割训练集并计算准确率与召回率推荐系统的评估不能靠“看起来合适”要用数学指标。最直接的方案是留出法把评分数据按 8:2 划分成训练集和测试集在训练集上构建矩阵并预测测试集中电影的评分这里用 RMSE 度量评分预测误差用 Precision 和 Recall 度量 Top-N 推荐命中率。from sklearn.model_selection import train_test_split train, test train_test_split(ratings, test_size0.2, random_state42) def evaluate_recall(user_item_matrix, user_sim, k20): topn {u: [m for m, _ in recommend(u, N10, kk)] for u in user_item_matrix.index[:200]} # 抽样 200 用户加速 test_user_map test[test[user_id].isin(topn.keys())] hits, total 0, 0 for _, row in test_user_map.iterrows(): if row[movie_id] in topn[row[user_id]]: hits 1 total 1 return hits / total if total 0 else 0抽样评估是一种折中手段因为 600 多万次预测轮流跑一遍太慢。抽样数量写在论文方法部分即可评委一般认可 100300 个用户上的统计结果。评估时还要注意不要把测试集里用户“已经看过的物品”混进候选池否则精确率虚高写论文时容易露怯。5.2 论文里的三张必放图表展示算法效果时第一张放 K 值敏感度曲线横轴是 K5/10/20/40纵轴是 Precision 和 Recall两条折线画在一起第二张放相似度分布的直方图可以看到绝大多数用户相似度集中在 00.2说明矩阵稀疏度极高第三张放展示系统的三个截图登录页、评分交互、推荐列表变化前后对比。这三张图基本能覆盖面试官对“工作量”的疑问。5.3 让演示不翻车的现场脚本演示前把环境变量、依赖包都冻结好不要在答辩现场临时跑pip install。准备一组固定账号用脚本预埋好 20 条评分记录第一次演示先展示“评分前”的推荐列表再现场点开 35 部电影打分然后重新推荐让评委直观看到列表变化。如果担心算法稳定性就把 K 值固定在 20关闭阈值过滤避免同一账号二次推荐结果波动过大。演示时优先展示用户给不同电影打分后推荐列表的变化过程这比任何静态截图都更能让评委直观理解协同过滤的“人以群分”。本文还有配套的精品资源点击获取
返回列表