ARTICLE DETAIL

资讯详情

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

电影推荐系统毕业设计:Python ItemCF协同过滤全流程实战

电影推荐系统毕业设计:Python ItemCF协同过滤全流程实战 简介一套基于推荐算法的电影推荐系统毕业设计资料包面向计算机相关专业学生和初入推荐系统领域的开发者用于完成课程设计、期末大作业或毕业设计。资料内含完整可运行的源码与配套论文已本地编译运行通过评审分为98分难度适中覆盖用户画像、协同过滤、内容推荐等典型模块从数据表设计、服务接口封装到前端交互形成完整链路适合从零搭建并理解推荐流程。压缩包共706个文件约13.03MB以Python源码、Vue页面、JavaScript脚本、CSS样式及SQL数据库脚本为主同时包含HTML页面、GIF演示图、一键安装/运行/构建的BAT脚本以及docx/doc论文文档。目录结构清晰前端、后端、配置与文档分层明确便于按模块查找和学习。已有228人学习下载可直接参考其推荐算法实现思路、服务接口设计和数据库建表逻辑配合论文说明快速梳理项目脉络是较完整的实践型毕业设计参考资料。1. 一个电影推荐系统毕业设计真正要交付的是什么答辩现场老师最常问的一句话是“你这个电影推荐系统和直接按评分排序有什么区别”很多同学在这一问上卡壳因为推荐算法不是堆两个余弦相似度函数就能交差它要求把数据、算法、评估三件事完整串起来。这套方案用Python实现技术路线是毕业设计里最常见的ItemCF协同过滤加基于内容兜底从MovieLens评分表出发构建物品相似度矩阵再做召回、排序、去热门最后接到Flask页面上。适合正在写代码或已经跑通但效果不理想的同学照着复现。特别提醒网络级推荐里那套深度模型在本科毕设的数据规模下更像黑匣子可解释性和答辩效果反而不如经典的协同过滤。2. 推荐算法选型为什么电影场景首选ItemCF而不是UserCF2.1 UserCF与ItemCF的适用条件对比用户少物品多时别选UserCF推荐算法选型先看两个维度对象规模和兴趣稳定性。UserCF基于用户的协同过滤先找与当前用户评分行为最接近的一批人再用这批人看过的电影来推荐。ItemCF基于物品的协同过滤先算电影与电影的相似度然后根据用户自己看过的电影去寻找相似的没看过的电影。从矩阵上看两者互为转置但从行为数据特点看差别非常大。电影评分场景的典型特征是用户数量多于物品数量而单个物品的评分非常稀疏。以MovieLens小数据集为例用户是几百人电影是几千部用户兴趣会随着观影量变化但电影本身的类型、导演、年代等属性长期稳定。UserCF需要实时计算用户之间的相似度用户量一旦涨上去每次推荐都扫描全量用户延迟和内存都压不住。ItemCF则把最重的计算转移到物品相似度矩阵上这个矩阵可以离线算好、低频更新线上只做查表和加权求和响应稳定得多。动手前可以先用两个数判断数据更适合哪一类用户人均评分条数、电影平均被评次数。如果用户人均评分只有不到10条用户相似度矩阵会稀疏到出现大量全零行而电影平均被评次数哪怕只有几十次物品相似度矩阵的连通性也比用户矩阵高一个量级。这个初判花不了几分钟但能避免写完整个项目再换方向的悲剧。维度UserCFItemCF相似度对象用户与用户物品与物品更新方式用户行为变化后需频繁重算物品相似度离线低频更新适用规模用户少、物品多用户多、物品少物品较稳定冷启动敏感度新用户无历史评分时不可用新用户只要少量评分即可推荐可解释口吻“跟你相似的人也在看”“因为你看过A所以推荐B”典型场景资讯、社区电商、视频、电影2.2 余弦相似度与皮尔逊相关系数用户均值中心化才是关键选型定下来之后相似度的计算方式决定推荐质量的上限。电影评分数据里有一个隐蔽问题不同用户的打分明细尺度不一样。有人习惯3到4分之间有人5分起步有人把不喜欢的片直接打2分。如果用原始评分算余弦相似度会把“个人评分风格”当成“电影内容相关”最后得到一堆虚假关联。解决办法是均值中心化对每个用户先减掉该用户自己的平均分让评分变成“相对个人平均水平的偏高或偏低”再做物品相似度计算。这与皮尔逊相关系数的思路等价但在稀疏矩阵上实现要小心不能直接做稠密的mat - mean否则零元素会全部变成负数矩阵瞬间失去稀疏性。import numpy as np from scipy.sparse import csr_matrix # rating_mat: shape(n_users, n_items)dtypenp.float32 def center_user_mean(rating_mat): mat rating_mat.tocsr().copy() user_cnt np.diff(mat.indptr) # 每个用户实际评了多少部 user_sum np.asarray(mat.sum(axis1)).ravel() # 每个用户评分总和 user_mean np.where(user_cnt 0, user_sum / user_cnt, 0.0) rows, _ mat.nonzero() mat.data mat.data - user_mean[rows] # 只修改非零位置 return mat这段代码的逻辑是先用indptr差分算出每行非零个数再按行求和得到均值最后对非零元素原地减均值。参数上要注意两个地方一是user_cnt过小时均值不稳定我一般要求用户最少10条评分小于10的直接在清洗阶段过滤二是中心化之后矩阵出现负值后续算余弦相似度时如果直接对全矩阵做L2归一化再点乘结果近似皮尔逊相关这是推荐算法里的常规近似开源实现也大多直接这么用。2.3 为什么不用SVD或深度学习本科毕设图的是可控和可解释很多同学选型时会纠结要不要上SVD、NMF或者双塔网络。从纯指标上看矩阵分解在MovieLens这类显式评分数据上的RMSE通常比ItemCF低一点点但代价有三个需要额外处理缺失值、验证集划分和随机种子复现调参过程的坑比ItemCF多一个量级隐向量无法直接解释“为什么推荐这部片”。如果一定要做对比实验我建议用surprise库里的SVD当baseline把RMSE、训练时间、可解释性三列资料放进论文的对比表而不是把主力方案换成SVD。主力用ItemCF论文里解释“选型理由是稳定性、可解释性和数据规模适配”就站得住代码也能一条条讲清楚不会被老师追问到崩溃。3. 数据准备与稀疏矩阵构建从MovieLens评分表到可计算的相似度矩阵3.1 MovieLens数据集字段与清洗users、movies、ratings三张表怎么处理MovieLens是这类项目使用频率最高的公开数据集常见版本叫ml-latest-small。里面三张表结构不同movies.csv有movieId、title、genresgenres是竖线分隔的类型串ratings.csv有userId、movieId、rating、timestamprating范围从0.5到5.0users.csv在ml-1m版本里才有带年龄、职业、性别。只做电影推荐的话通常用前两张表就够了。清洗阶段我一般做四步去掉评分不在0.5到5.0区间的脏数据过滤评分数量过少的用户和电影把userId、movieId重新映射成连续的稠密序号把timestamp换算成天数为后面排序阶段的时间衰减做准备。先过滤再重映射这个顺序不能反否则稀疏矩阵里会留出大片空洞列白白浪费内存。import pandas as pd ratings pd.read_csv(ml-latest-small/ratings.csv) movies pd.read_csv(ml-latest-small/movies.csv) user_act ratings.groupby(userId)[rating].count() movie_act ratings.groupby(movieId)[rating].count() valid_user user_act[user_act 10].index valid_movie movie_act[movie_act 5].index ratings ratings[ ratings[userId].isin(valid_user) ratings[movieId].isin(valid_movie) ].copy() # 业务id转连续稠密id后续建矩阵不会出现空洞 ratings[uid] ratings[userId].astype(category).cat.codes ratings[mid] ratings[movieId].astype(category).cat.codes n_users ratings[uid].max() 1 n_items ratings[mid].max() 1两个阈值要配合数据集调10和5是我在ml-latest-small上习惯用的起点。把阈值调大数据更干净但覆盖率下降调小矩阵更稠密但噪声增加。建议在论文里把这组阈值单独列一个参数表然后做一次“阈值从5到20Recall10怎么变”的小实验这是很加分的细节。3.2 用scipy.sparse构建评分矩阵为什么不能用pandas的pivot_table新手最常见的做法是用pivot_table把ratings转成“用户行、电影列”的DataFrame空位填0。对几百用户几千电影这个规模DataFrame勉强能跑但会产生两个问题未评分位置填0会被当成真实评分参与相似度计算把余弦公式的分母撑大转成numpy数组后内存白白膨胀好几倍。稳妥做法是保存为scipy的稀疏矩阵未评分位置不占内存计算时也只在非零元素上操作。from scipy.sparse import lil_matrix # 先按坐标赋值再转csr效率和内存都更可控 rating_mat lil_matrix((n_users, n_items), dtypenp.float32) rating_mat[ratings[uid].values, ratings[mid].values] ratings[rating].values rating_mat rating_mat.tocsr() print(矩阵形状:, rating_mat.shape) print(非零评分个数:, rating_mat.nnz) sparsity 100 * rating_mat.nnz / (n_users * n_items) print(稀疏度: %.4f%% % sparsity)lil_matrix适合按行索引赋值赋值完成后转成csr_matrix后续做矩阵乘法、行切片和按行统计都会快很多。打印出来的稀疏度是一个答辩时可以展示的数据MovieLens小数据集一般在3%到5%之间。这个数直接说明为什么协同过滤绕不开稀疏性问题和冷启动问题。3.3 物品相似度矩阵只保留TopK近邻给召回和冷启动留空间评分矩阵中心化并转成物品视角后就算物品相似度矩阵。矩阵里每行是一部电影每列是一个用户先对每行的非零向量做L2归一化再和自身转置相乘得到的就是余弦相似度矩阵。from sklearn.preprocessing import normalize from scipy.sparse import csr_matrix # center_mat: 均值中心化后的用户-物品稀疏矩阵 (n_users, n_items) # 物品向量是矩阵的列转置后按行归一化 item_vec center_mat.T.tocsr().astype(np.float32) item_vec normalize(item_vec, norml2, axis1) sim_mat item_vec item_vec.T # 得到物品×物品相似度直接保存sim_mat会翻车9000部电影就是8100万个数float64占近650MB。所以下一步只能对每行保留前面K个最大的相似度其余置零再转回稀疏矩阵。K 50 sim_dense sim_mat.toarray() # 小数据集可接受用于演示取TopK逻辑 topk_idx np.argpartition(-sim_dense, K, axis1)[:, :K] topk_val np.take_along_axis(sim_dense, topk_idx, axis1) row np.repeat(np.arange(sim_dense.shape[0]), K) col topk_idx.ravel() val topk_val.ravel() item_sim csr_matrix((val, (row, col)), shapesim_dense.shape) item_sim item_sim.multiply(item_sim 0) # 负相似度直接清零注意argpartition只保证前K个数是无序的后面如果还要按相似度排TopK得再对topk_val做一次排序否则推荐列表的顺序会不稳定。两个参数值得单独调K50决定召回候选集上限调大提高召回率但降低精度调小结果更保守但多样性更好清零负相似度是因为负相关的电影拉进候选集后只会污染排序。矩阵算完顺手做一个诊断统计“近邻数为0的孤立电影”有多少这是第5章会展开说的一个常见坑。row_nnz item_sim.getnnz(axis1) print(孤立物品数:, (row_nnz 0).sum()) print(平均近邻数:, row_nnz.mean())4. 推荐引擎实现召回、排序、去热门的完整链路4.1 召回阶段从用户历史评分出发把相似物品的并集拉回来有了TopK近邻表之后推荐就是标准的三段式召回、排序、截断。召回的输入是用户看过且打过高分的电影集合输出是一个去重后的候选电影集合。候选集的质量决定整个推荐的天花板如果候选里根本没有用户喜欢的电影排序部分再精细也救不回来。def recall_items(user_id, rating_mat, item_sim, top_n200): user_row rating_mat[user_id].toarray().ravel() rated_items np.where(user_row 0)[0] # 只取评分最高的20部电影作为兴趣锚点 strong_items rated_items[np.argsort(-user_row[rated_items])[:20]] candidates set() for item in strong_items: neighbors item_sim[item].tocoo() for pos, sim in zip(neighbors.col, neighbors.data): if pos in rated_items: continue candidates.add(pos) if len(candidates) top_n: break return list(candidates)这里有个容易忽略的细节锚点取“历史评分最高的20部”而不是全部已看影片。因为低分影片和年代久远影片的近邻关系里噪声更大只取高置信度兴趣点能明显减少无关候选。如果发现推荐结果太杂第一反应是把锚点从20缩到10或8而不是去调排序权重。top_n200是候选池上限我习惯不小于150太小会让排序阶段无米下锅。4.2 排序加权求和公式、评分区间与时间衰减召回完成后对候选集合打分。ItemCF的经典打分公式是候选电影c的得分等于它在用户已评分物品集合里的相似度按用户对锚点物品的评分加权求和再除以相似度权重之和。除以权重和是必要的归一化否则看过200部电影的用户算出来的分数永远大于只看过20部的用户排序结果会被重度用户历史主导。def score_candidates(user_id, candidates, rating_mat, item_sim): user_row rating_mat[user_id].toarray().ravel() rated_items np.where(user_row 0)[0] user_time user_time_map.get(user_id, {}) # (mid - 天数) score {} for c in candidates: total 0.0 w_sum 0.0 for j in rated_items: sim_ij item_sim[c, j] if sim_ij 0: continue r_uj user_row[j] # 简单时间衰减超过180天的影响减半 age_days max(user_time.get(j, 0), 0) decay 1.0 / (1.0 age_days / 180) total sim_ij * r_uj * decay w_sum abs(sim_ij) score[c] total / w_sum if w_sum 0 else 0.0 return score时间衰减有两种常见写法这里用的是“分数随天数线性减半”的近似另一种是指数衰减exp(-t / tau)效果接近但多一个参数要调。如果不想在论文里解释时间衰减完全可以省略这个环节只保留加权求和公式代码解释更简单。评分区间本身是0.5到5加权结果天然落在这个区间里不需要再做0到1归一化但如果后续要继续接排序模型就必须把特征标准化。4.3 去热门惩罚为什么相似度最高的电影全是复联系列ItemCF有一个很难消除的偏置热门电影出现的次数多和大量电影存在共现关系所以相似度普遍偏高最后推荐列表里全是票房头部影片。单纯调K值没有用常规做法是在排序分数上乘一个流行度惩罚项。def popularity_penalty(score, movie_popularity, alpha0.7): final {} for c, s in score.items(): pop movie_popularity.get(c, 1) pen 1.0 / (1.0 np.log1p(pop)) ** alpha final[c] s * pen return finalmovie_popularity是每部电影被评分的次数在上一步清洗时就能统计出来。alpha控制惩罚强度0表示不惩罚结果完全被热门霸榜1表示流行度每翻一倍分数明显下降。调这个参数时你会发现一个经典权衡不加惩罚时Recall50很高但推荐列表几乎全是热门加到0.5到0.8覆盖率明显上升而Recall掉得不多。这个权衡值得写进论文的数值实验部分它是推荐系统“准确性vs多样性”最直观的体现。5. 常见问题避坑协同过滤在电影数据上的典型现场5.1 数据规模与矩阵实现的三类翻车全空推荐、内存溢出、孤立物品现象一算完相似度矩阵推荐列表全是空的。原因清洗阶段把低交互用户过滤掉之后仍然有部分用户只看过两三部电影而这几个电影恰好没有足够的共现评分近邻表里查不到任何东西。解决在召回函数开头加一个最低交互数判断历史评分少于3条的直接走热门兜底答辩汇报时把这个情况归到冷启动策略里比被老师现场问出来体面得多。现象二一开页面就MemoryError报错堆栈指向矩阵乘法那一行。原因item_vec item_vec.T返回的是稠密矩阵几千部电影就是几千万个数再转成float64就上千兆了。解决算完TopK近邻后立刻转稀疏并释放中间变量全程用np.float32而不是默认的float64Paper里写清楚你用稀疏矩阵存储这是一个加分实现细节。现象三老电影能被推荐但新上映的电影永远不在候选里。原因新电影评分数量少甚至没有任何用户同时给它和新电影打过分的共现记录它就成了孤立物品行。解决item_sim.getnnz(axis1)统计近邻数为0的物品数量然后对这部分物品走基于内容的兜底用类型、导演、关键词算相似度。电影数据里有genres字段可以用movies[genres].str.get_dummies(|)生成类型向量L2归一化后直接做余弦相似度不依赖任何评分。注意类型向量相似度只能解“新电影没有评分”的问题不能替代协同过滤的精度所以通常只作为ItemCF候选不足时的补充通道。5.2 效果评估与调参的三个陷阱热门榜、RMSE失真、缓存不失效现象四推荐结果和热门榜单没区别用户画像完全没体现。原因ItemCF的相似度矩阵偏向高频共现物品热门电影的连接度天然高。解决按4.3的方式乘流行度惩罚alpha从0.3开始试每次隔0.2往上加观察覆盖率随之变化的曲线。不要写死“排除全局评分Top100电影”的规则那会把真正适合用户的头部作品也误杀。现象五RMSE只有0.8但推荐列表一点新意也没有。原因RMSE衡量的是预测分和真实分的差值它奖励的是“把用户打过分但没展示过的电影猜准”而推荐列表的成败看的是排序。解决改用排序类评估指标至少算RecallK和覆盖率。RecallK看用户真正喜欢的电影有多少进入TopK覆盖率看推荐列表中不同电影所占比例这两个数才能真正指导调参。现象六改了相似度算法推荐结果还是跟之前一模一样。原因项目把相似度矩阵序列化成pickle文件后服务启动时直接加载旧文件根本没有重新计算。解决给模型文件一个版本号包含数据清洗时间、K值、alpha值三个字段启动时校验版本号不一致就重新训练。这个小问题在项目验收演示时一旦出现几分钟之间会摧毁评委对你代码成熟度的信任。5.3 答辩前的最后补救没有baseline就没有说服力现象七老师问“你的算法比随机推荐好多少”你拿不出对比数据。问题本质是所有推荐算法在统计学意义上都必须“显著优于此数据集上的最简基线”而这个基线通常是两个随机推荐和全局热门推荐。解决用第6章的留一法评估脚本至少跑出三个数字——随机推荐Recall10、热门推荐Recall10、ItemCF加惩罚后Recall10画成柱状图放进论文第四章。这一页PPT准备好老师在这块就挑不出问题。6. 效果验证与答辩准备留一法Recall和Flask页面的一处关键缓存6.1 一个50行以内的留一法评估脚本评估脚本不要设计得太复杂核心思想是把每个用户最后一次评分挖掉用剩余数据训练再检查被挖掉的那部电影是否出现在推荐TopK里。def leave_one_out_eval(rating_mat, item_sim, k10): hits, total 0, 0 for u in range(rating_mat.shape[0]): row rating_mat[u].toarray().ravel() pos np.where(row 0)[0] if len(pos) 2: continue test_item pos[-1] # 最后一次评分当测试 train_items pos[:-1] recs recommend_from_items(train_items, item_sim, top_kk) total 1 if test_item in recs: hits 1 return hits / max(total, 1)recommend_from_items就是第4章的排序函数去掉时间衰减后的精简版。答辩时至少报两个数Recall10和覆盖率。如果覆盖率只有不到5%说明热门惩罚太弱报告数值之前先自查。6.2 模型预热与页面响应相似度矩阵进内存前先做一次裁剪Flask页面最容易犯的错是每次请求都现算一遍近邻响应时间直接破秒。正确做法是服务启动时把item_sim和movie_popularity加载到全局变量推荐接口里按用户做进程内缓存同一个用户10分钟内的请求直接返回。from flask import Flask, jsonify import time app Flask(__name__) rec_cache {} app.route(/api/recommend/int:user_id) def recommend_api(user_id): now time.time() cached rec_cache.get(user_id) if cached and now - cached[0] 600: return jsonify({items: cached[1]}) items recommend_for_user(user_id, top_k10) rec_cache[user_id] (now, items) return jsonify({items: items})这段路由做了两个取舍缓存键是用户ID命中后600秒内不再重算缓存放在进程内字典里服务重启自动清空省掉Redis依赖。答辩演示时先点几个用户让缓存预热再给老师看页面秒开效果体验会好很多。这类项目我做过不止一次现在养成的习惯是动手写推荐算法之前先把“热门推荐”这个最简陋的baseline跑出来后面所有算法的提升都是在跟它比。没有baseline的效果数字调参就是玄学有了它每个改动是好是坏一眼就能判断。这个习惯在写论文和答辩时救了我很多次希望帮到你。本文还有配套的精品资源点击获取
返回列表