ARTICLE DETAIL

资讯详情

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

Python协同过滤推荐系统实战:从评分矩阵到TopN推荐

Python协同过滤推荐系统实战:从评分矩阵到TopN推荐 简介基于协同过滤推荐算法的电影推荐系统毕业设计项目已获导师指导并通过答辩评审分达97分。面向计算机专业毕业生、课程设计与期末大作业需求者下载后无需修改即可运行。压缩包内含688个文件整体大小约13.32MB其中既有Python源码、Vue前端组件、JavaScript脚本、CSS样式与SQL数据库脚本也包含一键安装、一键启动等辅助脚本搭建运行环境非常方便。目前已有50人学习下载。项目内容完整涵盖前端交互页面、后端接口服务、协同过滤算法实现、电影数据集与毕业论文。目录结构清晰可分别查看用户评分矩阵构建、相似度计算、推荐结果生成等关键模块既适合动手复现实验也便于在毕设论文中引用系统设计与实现细节。对希望快速搭建推荐系统或深入理解基于物品与用户的协同过滤流程的读者具有直接的参考价值。1. 引言毕业设计拿到“python基于协同过滤推荐算法的电影推荐系统源码全部数据论文毕设.zip”这样的压缩包第一反应是松了口气第二反应是不知道从哪下手。源码能跑数据能打开论文能读但轮到你把它讲清楚、调好参数、补上实验才发现又是一回事。这篇笔记把协同过滤这个推荐算法从原理拆到实现告诉你这个包里每个模块在干什么、评分数据怎么预处理、相似度公式怎么选、TopN推荐怎么做以及那些只跑一遍根本遇不到的坑。适合正在做毕设、课程设计或者想快速入门推荐系统的开发者。整个项目围绕一个核心用python把协同过滤推荐算法落到电影数据上产出可评估、可写进论文的结果。2. 协同过滤的原理与选型自己动手前先想清楚的三件事2.1 两个流派基于用户的协同过滤和基于物品的协同过滤协同过滤Collaborative Filtering是推荐算法里最经典的一类核心思想是“不用知道电影的内容只看用户行为”。再往下分就是标题里最常见的两个流派。基于用户的协同过滤UserCF的逻辑是“人以群分”找到和你兴趣相似的用户把那些相似用户看过而你没看过的电影推荐给你。它天生适合用户数量远小于物品数量的场景因为相似度矩阵的大小是用户数乘以用户数。在MovieLens 100K数据集上943个用户、1682部电影用户相似度矩阵只有943×943算起来毫无压力。这也是毕设项目里很多模板默认采用UserCF的原因代码好写、过程好讲、答辩好解释。基于物品的协同过滤ItemCF的逻辑是“物以类聚”给你推荐那些和你历史上喜欢过的电影相似的电影。它需要先计算物品相似度矩阵维度是物品数乘以物品数。在电影场景里电影数量相对稳定增量小物品相似度矩阵可以离线算好存起来所以是目前工业界落地最多的方案。但从做毕设的角度看ItemCF的实现难度和UserCF差不多只是解释逻辑要绕一下。我一般建议两个都实现。理由很现实论文里需要对比实验。单跑一个算法老师可能会问“为什么不用另一个”两个都实现你就能用数据说明选型理由还顺带撑起一章的篇幅。对比维度UserCFItemCF数学本质用户-用户相似度物品-物品相似度计算规模用户数²物品数²可解释性弱靠用户群体强“因为你喜欢A所以推荐B”更新成本用户行为变化快矩阵更新频繁物品相似度稳定离线算好即用适合场景用户少、物品多物品少、用户多2.2 三个相似度公式余弦、皮尔逊、杰卡德协同过滤的推荐质量一半由相似度公式决定。这里有三个公式需要搞懂选错了方向后面调参都是白费。余弦相似度Cosine Similarity把每个用户的评分看成一个向量计算两个向量夹角的余弦值。它的缺点是没考虑用户打分的尺度差异有的人习惯打高分一部电影给4分是“不喜欢”另一个人习惯打低分给4分是“很喜欢”。直接用原始评分算余弦这两个人会因为分数数值相近而被误判为相似。皮尔逊相关系数Pearson Correlation解决了这个问题。它先让每个用户减去自己的平均分再去算两个向量的相关性。减均值这一步相当于把用户的打分习惯拉齐了你平时打3分给某部电影打5分说明你特别喜欢我平时打5分给某部电影打5分说明只是还行。皮尔逊算出来的相似度才是真正意义上“口味一致”的程度。在显式评分1-5分的场景里皮尔逊是首选。杰卡德相似度Jaccard Similarity只看两个用户的评分集合的并集和交集的比例完全无视评分数值。它适合隐式反馈比如“看过/没看过”“收藏/没收藏”这种0/1数据。电影推荐如果只有行为日志没有评分就可以用杰卡德。2.3 做预测评分还是做TopN推荐同是协同过滤输出形态有两种一种是预测用户对某部未看电影的评分比如4.2分一种是生成一个长度为N的推荐列表。我建议毕设项目直接做TopN推荐而不是把重点放在预测评分上。原因有三个。第一TopN的评价指标——Precision、Recall、Coverage——是列表导向的计算简单和用户实际体验一致预测评分用的RMSE指标在电影推荐场景里说服力反而不如命中率直观。第二TopN结果可以直接把推荐列表打印出来答辩时给评委看“给用户12推荐了这10部电影”效果远比一个浮点数好。第三做预测评分必须处理用户打分偏置做TopN却可以绕过这个问题实现更稳。3. 数据准备从原始评分文件到能算相似度的矩阵3.1 先摸清数据集格式u.data和u.item分别装了啥拿到源码包后第一步不是读代码而是看数据。MovieLens 100K是最常见的教学数据集解压后能看到u.data和u.item两个核心文件。u.data是评分记录每一行是一个用户的打分行为字段依次是用户ID、电影ID、评分、时间戳用tab分隔。u.item放的是电影信息第一列电影ID第二列标题后面是一堆二进制的电影类型标注。先确认环境里有python和pandas然后动手看数据head -5 u.data wc -l u.datahead看到的是这样的行196 242 3 881250949 186 302 3 891717742 22 377 1 878887116四个字段都是数字没有表头。wc -l数是评分总条数100K数据集大概是10万条。这时再用pandas读进来import pandas as pd # MovieLens 100K 的 u.data 是 tab 分隔没有表头 ratings pd.read_csv( u.data, sep\t, names[user_id, item_id, rating, timestamp] ) print(ratings.head()) print(用户数:, ratings.user_id.nunique()) print(电影数:, ratings.item_id.nunique()) print(评分总数:, len(ratings))pandas的read_csv在这里只要关心两个参数sep和names。sep\t指定文件用tab分隔names手动指定四列名字。head()和nunique()一起用先把数据规模摸清楚。如果你连pandas都没装先执行pip install pandas scipy numpy这是整个项目的地基。数据只有十万条用pandas完全够不用上Spark那套重武器。3.2 构造用户-物品评分矩阵pivot_table和稀疏度检查协同过滤的计算基础是“用户-物品评分矩阵”行是用户列是电影交叉点是评分。这个矩阵用pandas的pivot_table一行就能生成# 转换成用户-物品评分矩阵行是用户列是电影 rating_matrix ratings.pivot_table( indexuser_id, columnsitem_id, valuesrating ) print(rating_matrix.shape) # 计算矩阵稀疏度判断数据够不够密 density ratings.shape[0] / (rating_matrix.shape[0] * rating_matrix.shape[1]) sparsity 1 - density print(f矩阵稀疏度: {sparsity:.2%})运行后会看到矩阵形状是(943, 1682)稀疏度约93.7%。这意味着矩阵里有近94%的位置是空值用户只评过极少数电影。这是一个必须接受的现实协同过滤就是要在这种高稀疏矩阵里找相似性。这里有个新手常踩的坑pivot_table默认把空位填成NaN保留NaN是有意义的后面算相似度时要跳过这些空值而不是填成0。如果你手贱写了fillna(0)那没评分的电影和评了1分的电影会被当成同一档相似度计算直接崩掉。所以第3.3节之前矩阵保持NaN原样。3.3 划分训练集和测试集别用随机切分很多网上的模板代码用的是train_test_split随机划分这在我看来是推荐系统项目里最典型的翻车点。随机切分会让同一部电影的评分以高概率同时出现在训练集和测试集里算法“见过”答案再考试离线指标虚高答辩时一旦被问到就露馅。正确的做法是按时间戳划分。推荐系统要模拟的是真实场景你用过去的数据训练模型预测未来的行为。代码如下# 按时间排序前80%时间的评分做训练集后20%做测试集 ratings ratings.sort_values(timestamp).reset_index(dropTrue) cutoff int(len(ratings) * 0.8) train ratings.iloc[:cutoff].copy() test ratings.iloc[cutoff:].copy() # 训练集里没有出现的用户或电影测试集里直接过滤 valid_users set(train.user_id.unique()) valid_items set(train.item_id.unique()) test test[ test.user_id.isin(valid_users) test.item_id.isin(valid_items) ] print(f训练集样本数: {len(train)}, 测试集样本数: {len(test)})按时间划分之后还要做一步过滤测试集里可能出现训练集中完全没见过的用户和电影这些样本对协同过滤来说是冷启动模型根本没法预测留着只会拉低指标。先把它们过滤掉后面避坑章节还会专门聊冷启动。排序和reset_index(dropTrue)这两步不能省。如果不重置索引后面iloc取切片时容易对不上行号。test集过滤后长度会变小这是正常的你的评估就是在一个“模型见过的人和物”的范围内做。4. 代码实现从相似度矩阵到TopN推荐列表4.1 基于用户的协同过滤皮尔逊相似度的完整实现准备工作做好之后进入核心模块。基于用户的协同过滤分三步走算用户相似度矩阵、给目标用户找K个近邻、用近邻的评分加权预测。先写相似度计算。这一段是整个项目里计算开销最大、最容易写错的地方import numpy as np import pandas as pd from scipy.stats import pearsonr def user_similarity(rating_matrix): 计算用户相似度矩阵使用皮尔逊相关系数 users rating_matrix.index.tolist() sim_matrix pd.DataFrame(0.0, indexusers, columnsusers) for i, u in enumerate(users): for v in users[i 1:]: # 找出两个用户都评过分的电影 u_ratings rating_matrix.loc[u].dropna() v_ratings rating_matrix.loc[v].dropna() common_items u_ratings.index.intersection(v_ratings.index) # 共同评分数小于2时皮尔逊相关系数没有统计意义 if len(common_items) 2: sim 0.0 else: sim, _ pearsonr( u_ratings[common_items].values, v_ratings[common_items].values ) sim_matrix.loc[u, v] sim sim_matrix.loc[v, u] sim return sim_matrix这里三个细节决定正确性。一是dropna()取出的序列要先求index交集保证两个序列按同一顺序对齐二是皮尔逊系数要求样本数不小于2只有一个共同评分时算出来是NaN要直接置0三是相似度矩阵是对称的填上三角时同步填下三角避免重复计算。这套双重循环的复杂度是O(用户数²×共同评分计算代价)在943个用户的MovieLens 100K上跑大概几十秒。如果你换成了MovieLens 1M数据集里面有6000多个用户这个双层循环会慢到让你怀疑人生。百万级数据量时可以用scipy的pdist或者先用稀疏矩阵筛掉没有共同评分的用户对再只算有交集的组合。4.2 用K近邻预测评分并生成推荐列表有了相似度矩阵就可以给用户做预测了。预测评分的逻辑是找出和目标用户最相似的K个用户用这K个用户对某部电影的评分做加权平均权重就是相似度。代码如下def predict_rating(user_id, item_id, rating_matrix, sim_matrix, k10): 预测user_id对item_id的评分k是近邻数量 # 对item_id评过分的所有用户及其评分 item_users rating_matrix[item_id].dropna() # 取出这些用户与目标用户的相似度 sims sim_matrix.loc[user_id, item_users.index] # 只保留正相似度负相关用户的口味相反不应参与加权 sims sims[sims 0] if len(sims) 0: # 没有正相似邻居时回退到该用户的全局平均分 return rating_matrix.loc[user_id].mean() # 取最相似的K个用户 sims sims.nlargest(k) item_users item_users[sims.index] # 加权平均相似度作为权重乘以评分后归一化 pred np.dot(sims.values, item_users.values) / sims.sum() return round(pred, 3)predict_rating里最关键的是负数相似度的处理。两个用户都看过某部奇怪电影但评分截然相反皮尔逊系数可能是负的这表示他们口味相反不能作为“相似用户”参与推荐。过滤掉负值后如果完全没有正相似邻居就回退到用户的全局平均分。这个回退逻辑很朴素但能显著减少评分预测的NaN数量。有了单点预测生成一个用户维度的TopN推荐列表就顺理成章了def recommend_for_user(user_id, rating_matrix, sim_matrix, n10, k10): 给user_id生成n条推荐返回(电影ID, 预测评分)列表 # 用户已经评过分的电影要排除 rated_items set(rating_matrix.loc[user_id].dropna().index) # 候选池K个近邻用户评过分的所有电影 sims sim_matrix.loc[user_id].dropna() sims sims[sims 0].nlargest(k) neighbor_ids sims.index candidate_scores {} # 遍历近邻用户的评分记录汇总候选电影 neighbor_ratings rating_matrix.loc[neighbor_ids].dropna(axis1, howall) for item in neighbor_ratings.columns: if item in rated_items: continue item_ratings neighbor_ratings[item].dropna() # 用相似度加权打分归一化后得到预测值 score np.dot(item_ratings.values, sims[item_ratings.index].values) / sims[item_ratings.index].sum() candidate_scores[item] score ranked sorted(candidate_scores.items(), keylambda x: x[1], reverseTrue) return ranked[:n]这段代码是后面所有实验的基础。注意两个小地方第一neighbor_ratings用dropna(axis1, howall)扔掉所有近邻都没评过分的空列把候选集缩小第二在遍历候选电影打分时只对真正有评分的近邻做加权平均分母也要跟着变否则相似度低的用户会稀释分数。4.3 基于物品的协同过滤换汤不换药的高效实现基于物品的协同过滤实现逻辑完全对称把用户相似度换成物品相似度就行。但有个重要的性能优化物品相似度矩阵只需要在训练集上算一次之后无论给多少用户做推荐都复用这份矩阵。这在毕设的数据量下可能感觉不到差距但手机上跑一遍对比实验时你会明显发现ItemCF在推荐阶段的速度快得多。def item_similarity(rating_matrix): 计算物品相似度矩阵使用调整的余弦相似度 items rating_matrix.columns.tolist() sim_matrix pd.DataFrame(0.0, indexitems, columnsitems) # 每个用户减去自己的平均分消除打分尺度差异 normalized rating_matrix.sub(rating_matrix.mean(axis1), axis0) for i, a in enumerate(items): for b in items[i 1:]: # 共同评分用户数少于2时跳过 common_users rating_matrix[a].dropna().index.intersection( rating_matrix[b].dropna().index ) if len(common_users) 2: sim 0.0 else: va normalized.loc[common_users, a].values vb normalized.loc[common_users, b].values denom np.linalg.norm(va) * np.linalg.norm(vb) sim np.dot(va, vb) / denom if denom 0 else 0.0 sim_matrix.loc[a, b] sim sim_matrix.loc[b, a] sim return sim_matrix注意normalized这一行每个用户先减自己的平均分再算余弦相似度。这个做法叫“调整的余弦相似度”效果等价于皮尔逊相关系数但实现路径不同。用这种矩阵再用之前写好的推荐函数换个入参就能直接得到ItemCF的推荐结果。4.4 参数怎么调K值和推荐列表长度的连锁反应协同过滤有两个核心参数K近邻数量和N推荐长度。K值决定你参考多少个邻居K太小参考样本少预测结果方差大K太大混入弱相关甚至负相关的用户推荐精度下降。我在MovieLens 100K上的经验是K在10到30之间表现较稳。毕设里最好做一个K值扫描实验把K5、10、20、30的结果画成曲线这既是最直观的调参依据也是论文里现成的实验图。N值则直接影响评价指标N越小Precision越高但Recall越低这是必然的权衡。做实验时固定N10来对比不同算法的精度是最常见的做法不会出错。代码写到这里一个能跑通的协同过滤推荐系统已经出来了。5. 避坑协同过滤实现里最常见的五个翻车现场5.1 现象皮尔逊相似度矩阵全是NaN跑完相似度计算打印矩阵发现到处是NaN。原因有两个一是两个用户之间没有共同评分项pearsonr返回NaN二是某个用户给所有电影打了同一个分评分方差为0导致相关系数算不出来。解决方法是双保险。计算前先判断共同评分数量小于2直接置0计算后统一调用sim_matrix.fillna(0)兜底。另外可以打印出每个用户的评分标准差找出哪些用户是“无差别打分”的这类用户对推荐没有贡献可以直接在相似度矩阵里放弃。5.2 现象推荐列表全是热门电影毫无个性这是协同过滤最著名的副作用——流行度偏差。一部电影被几百个人评过分它和几乎所有用户之间都有较高的相似度于是被反复推荐。结果就是推荐结果千篇一律全是《阿甘正传》这种大众片。原因是评分矩阵的稀疏性加上马太效应。解决方法是引入流行度惩罚对越热门的电影在最终打分时给一个折扣。import numpy as np def popularity_penalty(candidate_scores, item_popularity, alpha0.2): 对热门电影降权。 item_popularity: 字典{电影ID: 评分人数} alpha: 惩罚强度0.1~0.5之间调节 penalized {} for item, score in candidate_scores.items(): pop item_popularity.get(item, 1) # 用1log(pop)做惩罚因子压低大热的电影 factor 1.0 / (1 alpha * np.log(pop)) penalized[item] score * factor return penalized推荐前先统计每部电影的评分人数作为流行度然后调用这个函数对候选打分重新排序。alpha从0.1开始试调到推荐列表出现足够多有差异性的电影为止。这个改进虽然只有几行代码但效果显著放到论文里就是现成的“创新点”。5.3 现象新用户和新电影没有推荐结果测试集里如果没做过滤会出现完全没有评分记录的新用户协同过滤对这种人毫无办法——没有历史行为就没有相似邻居。这就是冷启动问题。解决分两层。第一层是数据预处理时把测试集里训练集没出现过的用户和物品过滤掉第3.3节已经做了。第二层是在推荐函数里加回退逻辑def safe_recommend(user_id, rating_matrix, sim_matrix, fallback_items, n10): 推荐函数带冷启动回退 if user_id not in rating_matrix.index: # 用户没有任何评分回退到全局热度榜 return fallback_items[:n] rec recommend_for_user(user_id, rating_matrix, sim_matrix, nn) if not rec: # 有评分但近邻没有正相似度也回退到热度榜 return fallback_items[:n] return recfallback_items是事先算好的全局热门电影列表。这个兜底方案在论文里值得用一个段落去写因为它在真实系统中是必须存在的模块答辩老师看到这个细节会认为你想过生产环境的问题。5.4 现象pivot_table一执行内存直接爆掉有的同学拿到的是MovieLens 1M或更大的数据一跑pivot_table,电脑风扇狂转内存报警。原因是pivot_table创建的是密集DataFrame用户数乘以电影数是几万乘几万每个空格都要占内存。解决方案是改用scipy的稀疏矩阵from scipy.sparse import coo_matrix, csr_matrix # 把用户ID和电影ID映射到连续的整数索引 user_mapping {uid: i for i, uid in enumerate(sorted(ratings.user_id.unique()))} item_mapping {iid: i for i, iid in enumerate(sorted(ratings.item_id.unique()))} rows ratings.user_id.map(user_mapping).values cols ratings.item_id.map(item_mapping).values data ratings.rating.values sparse_matrix coo_matrix((data, (rows, cols))).tocsr()之后所有矩阵运算都换成稀疏版本相似度计算的循环逻辑不用改但内存占用从几个GB降到几十MB。真实工程里不会有人用密集矩阵存评分数据这个认知比跑通代码更重要。5.5 现象离线指标很高实际推荐效果却很拉胯辛辛苦苦把Precision算出来发现高达30%心里暗爽但打开推荐列表一看全是些莫名其妙的电影。原因在于随机划分数据集造成的时间泄漏训练集和测试集来自同一时间段模型相当于“看见过答案”。解决办法就是第3.3节的时间戳按序划分。另一个常见误用是只看RMSE不看列表质量RMSE低不代表推荐列表用户喜欢。一定要同时算Precision、Recall、Coverage三个指标别只抱着一个数字写论文。我见过太多项目离线指标写得漂漂亮亮一追问数据划分方式就露馅。6. 论文落地把推荐结果做成能写进论文的实验证据6.1 三个评价指标的计算代码论文里要写清楚你用什么指标评价算法。推荐系统最常用的是PrecisionN、RecallN和Coverage。用一个函数就能全算出来def evaluate(test_set, train_rating_matrix, sim_matrix, n10): test_set: dict{用户ID: [电影ID列表]} 返回 precision, recall, coverage hits 0 total_rec 0 total_relevant 0 recommended_and_seen set() for user, actual_items in test_set.items(): rec_items recommend_for_user(user, train_rating_matrix, sim_matrix, nn) if not rec_items: continue rec_ids [item for item, _ in rec_items] hits len(set(rec_ids) set(actual_items)) total_rec len(rec_ids) total_relevant len(actual_items) recommended_and_seen.update(rec_ids) precision hits / total_rec if total_rec else 0.0 recall hits / total_relevant if total_relevant else 0.0 coverage len(recommended_and_seen) / train_rating_matrix.shape[1] return precision, recall, coverage三个指标各说一件事Precision是推荐列表里有多少电影是用户真的看过的衡量“准不准”Recall是用户看过的电影里有百分之几被推荐出来了衡量“全不全”Coverage是系统总共推荐了多少部不同的电影衡量“多样性”。论文里三个都列说明你考虑了推荐系统的立体效果而不只是单一精度。6.2 实验表格两个算法加一个改进的对比答辩之前你一定要有一张这样的表格。下面的数据是示意别照抄用你自己的划分跑一遍算法配置Precision10Recall10CoverageUserCF (K20)0.1830.1050.32ItemCF (K20)0.1760.0980.28UserCF 流行度惩罚0.1910.1120.47这个表格的读法是流行度惩罚后Precision和Recall小幅上升Coverage大幅提升。这说明推荐结果不再集中在头部爆款上多样性和个性都变好了。实验表格在论文里配一段解释就是完整的一小节。加一句提醒如果你的随机种子不同、数据切分不同数值会有浮动但Coverage提升这个趋势是稳定的。6.3 这些实验结果怎么变成论文章节论文的推荐算法章节最稳的结构是“算法原理—数据集说明—评价指标—实验结果与分析”。把第4章的代码流程图、第6.1节的评估代码、第6.2节的表格填进去这一章基本就成型了。如果还想加一点深度就把流行度惩罚作为一个独立改进点对比“改进前”和“改进后”两张指标表做一次消融分析。最后说句实在话我当年做毕设最后悔的是临近截止才意识到时间戳划分的重要性答辩时被老师一句“你如何避免数据泄漏”问得手心冒汗。现在回过头看协同过滤本身不复杂复杂的是把数据分割、稀疏处理、冷启动兜底这些细节考虑周全。你按上面这套流程把代码跑一遍挨个踩过这些坑论文和答辩都会顺很多。希望帮到你。本文还有配套的精品资源点击获取
返回列表