拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于Python协同过滤的电影推荐系统:原理、实现与优化

简介一份完整的基于Python协同过滤算法的电影推荐系统源码面向推荐系统初学者、毕业设计或课程实践者。项目覆盖数据预处理、余弦相似度/皮尔逊相关系数等用户相似度计算、基于用户User-based与基于物品Item-based的协同过滤、推荐结果排序与评估优化等关键环节同时提供简单前端交互界面方便直接运行观察推荐效果。压缩包共228个文件约6.92MB以17个Python源码与14个pyc编译文件为核心辅以7个csv评分/类型数据、1个sql数据库脚本、12个js、7个css及2个html前端资源并包含大量jpg/png图片素材以及md/txt/xml等项目说明与配置文档目录组织清晰便于按模块定位代码。已有59人学习下载对于毕业设计、课程设计或推荐系统入门学习者而言能够从中获得从数据处理、相似度计算到协同过滤实现与结果评估的完整项目经验理解推荐系统从数据到落地的核心思路。1. 基于Python协同过滤算法的电影推荐系统不只是毕业设计拿到一个叫“基于Python协同过滤算法的电影推荐系统源码.zip”的项目包大多数人第一反应是“又一个课程设计”。但如果你打算直接跑起来就交差很大概率会在数据格式、评分稀疏和相似度计算上卡住。这个标题背后其实是一整套完整的推荐链路从用户行为数据到评分矩阵从相似度度量到Top-N推荐再到离线评估。电影推荐恰好是协同过滤最经典的落地场景因为它有天然的评分数据、明确的物品集合和可量化的效果指标比电商、资讯推荐更适合用来理解算法原理。作为一线工程师我建议你把这份源码当成“最小可用的推荐系统骨架”来拆而不是一个黑盒。协同过滤本身不复杂复杂的是数据清洗、相似度计算的内存效率和评估口径。下文会从原理、实现、参数调优和常见坑四个层面讲清楚保证你拿到代码后每一步都能对上号改起来也知道动了哪里。2. 协同过滤算法原理与Python选型评分矩阵、相似度度量和冷启动边界2.1 为什么电影推荐默认用协同过滤而不是内容推荐协同过滤的核心假设是“与你相似的用户喜欢的东西你也可能喜欢”或者“与你喜欢过的物品相似的物品值得推荐给你”。它不需要理解电影内容只需要用户对电影的评分、收藏、播放等交互行为。这个特性在电影域特别合适因为电影的画风、题材、导演等元数据覆盖不全时行为数据反而是最可靠的信号。在Python的世界里做协同过滤没有唯一正解。常见选择是纯NumPy手写相似度矩阵适合小数据集和教学结构透明。Pandas做分组和透视表处理CSV和评分统计非常顺手。scikit-learn的pairwise_distances或cosine_similarity省去手写距离函数。Surprise库封装了完整的协同过滤算法和评估工具适合快速出结果。这份源码如果你拿到的是纯Python实现大概率是NumPyPandas组合因为这是“从零实现”课程的典型写法。如果你需要生产级性能应该上Spark或Faiss但作为理解和面试项目Python原生实现完全够用。2.2 评分矩阵的构建与稀疏性透视表不是万能钥匙协同过滤的输入是三元组userId, movieId, rating。最常见的错误是直接把Pandas的pivot_table结果当评分矩阵用结果发现大量NaN。实际处理时你需要分清“缺失值”和“0”。协同过滤里缺失值表示“用户没看过”而不是“用户给了0分”所以通常不填充为0而是在相似度计算时只考虑共同评分的物品。下面是构建评分矩阵的常见做法import pandas as pd import numpy as np df pd.read_csv(ratings.csv) # 只保留必要字段并剔除重复评分 df df[[userId, movieId, rating]].drop_duplicates() # 生成用户-物品矩阵未评分的位置用NaN表示 rating_matrix df.pivot_table( indexuserId, columnsmovieId, valuesrating, aggfuncmean # 同一用户对同一电影多次评分取均值 ) print(rating_matrix.shape) print(矩阵稀疏度: {:.2f}%.format(100 * (1 - rating_matrix.notna().sum().sum() / rating_matrix.size)))aggfuncmean很关键因为原始数据里可能同一用户对同一电影有多条评分记录比如不同时间看的取均值比取最后一跳更符合“整体偏好”的直觉。稀疏度的意义在于如果稀疏度高达98%以上说明大多数用户只看过极少电影此时基于用户的协同过滤会非常脆弱。2.3 相似度计算余弦相似度与皮尔逊相关系数的取舍协同过滤的“过滤”核心就是把用户或物品映射成向量然后算向量夹角。余弦相似度对评分的绝对值不敏感只关心方向。皮尔逊相关系数则先减去用户平均分再去算相似度能缓解评分尺度不同的问题——比如A用户打分普遍是3-5B用户是1-4直接算余弦会被偏差带偏。指标公式向量表示适用场景Python实现方式余弦相似度cos(u,v) (u·v) / (u皮尔逊系数中心化后的余弦用户评分习惯差异大np.corrcoef或手写中心化余弦实际写代码时我不会直接用np.corrcoef因为它处理NaN的方式不够透明。更稳的方法是在有效共同评分上计算def pearson_score(matrix, idx_a, idx_b): common matrix.loc[idx_a].notna() matrix.loc[idx_b].notna() if common.sum() 5: # 共同评分太少相似度不可靠 return 0.0 a matrix.loc[idx_a][common].values.astype(float) b matrix.loc[idx_b][common].values.astype(float) a_centered a - a.mean() b_centered b - b.mean() denom np.sqrt((a_centered ** 2).sum() * (b_centered ** 2).sum()) if denom 0: return 0.0 return np.dot(a_centered, b_centered) / denom参数common.sum() 5就是“最小共同评分数量”这个值决定相似度计算是否可靠。数据量大时可以调到10数据稀疏时保持3-5。如果源码里没有这个阈值你会看到很多用户因为一两个共同电影就被推荐了杂七杂八的内容这就是典型的稀疏性噪声。2.4 冷启动问题协同过滤的边界在哪里协同过滤没法给新用户或新电影做推荐因为矩阵里没有它们的评分。这份源码如果直接跑冷启动用户会得到空推荐列表。常见补救办法是用全局热门榜兜底比如推荐平均分最高且评论数大于阈值的电影。新物品上线时用基于内容的相似同导演、同题材来粗匹配等积累到10条以上评分再进协同过滤模型。在工程上我会把冷启动逻辑写在推荐函数的最前面如果用户评分记录少于5条直接返回热门榜。这个“5条”是一个可调参数源码里通常叫MIN_INTERACTIONS。3. 从代码理清协同过滤实现链路数据划分、Top-N推荐与离线评估3.1 训练集与测试集划分按时间还是按比例随机推荐系统评估和分类任务不一样不能简单随机打乱后切分。因为用户偏好有时间演化用前80%时间段的评分训练后20%评分测试更贴近真实场景。但很多课程源码会直接train_test_split随机切这在面试时会被追问你要知道差异在哪里。from sklearn.model_selection import train_test_split # 方式1随机切仅适合快速验证 train, test train_test_split(df, test_size0.2, random_state42) # 方式2按时间切更符合推荐场景 df df.sort_values(timestamp) # 需要原始数据里有时间戳 train df.groupby(userId).head(int(0.8 * df.groupby(userId).size().max()))上面写法有问题groupby.head是按每用户前n条但不同用户数量不同不能统一n。正确做法是给每个用户打排名再按比例筛选df[rating_rank] df.groupby(userId)[timestamp].rank(methodfirst, ascendingTrue) df[user_total] df.groupby(userId)[timestamp].transform(count) train df[df[rating_rank] 0.8 * df[user_total]] test df[df[rating_rank] 0.8 * df[user_total]]这里methodfirst处理时间戳相同的情况。按时间切分以后训练集和测试集里不能再出现“同一个用户对同一部电影的评分同时出现在两边”上面的排名逻辑已经保证了这一点。3.2 基于用户的协同过滤UserCF实现UserCF分三步算用户相似度、找最近邻、对候选物品加权打分。代码要处理的核心问题是不要一次性生成全用户相似度矩阵否则2万用户就会产生4亿个浮点数内存直接爆掉。import numpy as np from scipy.spatial.distance import cosine as cosine_dist def user_cf_recommend(user_id, train_df, k10, top_n20): # 构建用户-物品矩阵这里仅对训练集 matrix train_df.pivot_table(indexuserId, columnsmovieId, valuesrating) # 取目标用户的评分向量 if user_id not in matrix.index: return [] # 冷启动交给兜底策略 target_vec matrix.loc[user_id] neighbors [] for other_id in matrix.index: if other_id user_id: continue # 只取共同评分的电影 common target_vec.notna() matrix.loc[other_id].notna() if common.sum() 3: continue vec_a target_vec[common].values vec_b matrix.loc[other_id][common].values sim 1 - cosine_dist(vec_a, vec_b) # scipy的cosine返回距离需要取反 neighbors.append((other_id, sim)) # 按相似度降序取前k个邻居 neighbors.sort(keylambda x: x[1], reverseTrue) neighbors neighbors[:k] # 对邻居看过的但目标用户没看过的电影加权求和 watched target_vec.dropna().index score_map {} for nid, sim in neighbors: n_vec matrix.loc[nid] for movie, rating in n_vec.dropna().items(): if movie in watched: continue score_map[movie] score_map.get(movie, 0) sim * rating # 排序返回Top-N top_items sorted(score_map.items(), keylambda x: x[1], reverseTrue)[:top_n] return [movie_id for movie_id, _ in top_items]这里的k10是最近邻数量top_n20是最终推荐数量。注意相似度近邻筛选我把共同评分小于3的直接跳过这比硬编码0更好因为0分和缺失值处理语义完全不同。如果源码里用的是矩阵全量相似度运行慢是很正常的超过1万用户建议改成「先筛选候选用户再细算」。3.3 基于物品的协同过滤ItemCF与离线评估对比ItemCF更适合电影推荐这类物品数量少、用户数量多的场景。它预先计算物品间的相似度线上推荐时直接查表把用户评分过的电影的相似电影拉出来聚合响应速度快。这也是为什么很多源码里ItemCF效果看起来比UserCF好因为电影数量通常几千部而用户是几万人。def item_cf_similarity(train_df): # 构建物品-用户逆表加快相似度计算 item_users train_df.groupby(movieId)[userId].apply(list).to_dict() # 统计物品共同被评分次数 co_occur {} for users in item_users.values(): for i in range(len(users)): for j in range(i 1, len(users)): a, b users[i], users[j] co_occur[(a, b)] co_occur.get((a, b), 0) 1 co_occur[(b, a)] co_occur.get((b, a), 0) 1 # 再除以物品被评分数量的平方根做惩罚 ...现实中我不会从零写这种双层循环而是用Pandas自连接。推荐系统成熟的库如Surprise或Implicit已经把这些封装好了但你读源码时一定要能看懂这个逻辑面试经常会让你手写一个简易ItemCF。评估时建议同时跑UserCF和ItemCF在测试集上算均方根误差RMSE和Top-N命中率再用表格对比算法RMSETop-20命中率内存占用适合场景UserCF0.924.8%高新闻、社交推荐ItemCF0.876.2%中电影、电商表格里的数字是示意值不代表你的数据集结果。但规律通常是在电影评分数据集上ItemCF的离线精确度高于UserCF因为电影的热门分布更集中相似关系更稳定。3.4 评估代码用PrecisionN和覆盖率看推荐质量不要只看RMSE推荐列表的多样性和命中比例也很重要。下面这段代码计算Top-N命中率代码逻辑是“如果推荐列表里有测试集里用户评过且评分4的电影就算命中”。def precision_at_n(recommend_func, test_df, train_df, n20): test_positive test_df[test_df[rating] 4].groupby(userId)[movieId].apply(set) hit 0 total 0 for uid in test_positive.index: recs recommend_func(uid, train_df, top_nn) if not recs: continue total 1 if set(recs) test_positive[uid]: hit 1 return hit / total if total else 0.0这个函数把推荐问题简化成“用户是否有任何一部高分电影被推荐到”比严格计算每个位置的命中更宽容。如果你想追求严谨可以改成hit / (total * n)但那样数字太小不利于横向对比。源码里如果用“推荐列表前10命中率”做指标记得问清楚分母是总用户数还是有推荐结果的用户数两种算法结果会差一倍以上。4. 把源码跑起来参数设置、命令行封装与MovieLens数据集适配4.1 最小复现步骤从zip解压到首条推荐拿到基于Python协同过滤算法的电影推荐系统源码.zip先别急着双击运行main.py。我习惯先看目录结构unzip 基于Python协同过滤算法的电影推荐系统源码.zip cd 源码目录 find . -maxdepth 2 -type f -name *.py | head -20重点看有没有requirements.txt和data/目录。如果没有依赖文件常见依赖是numpy,pandas,scikit-learn,scipy。安装时用国内镜像会更快pip install numpy pandas scikit-learn scipy -i https://pypi.tuna.tsinghua.edu.cn/simple这份源码配套的数据集大概率是MovieLens的ratings.csv和movies.csv。如果源码自带了数据文件直接跑如果没有去MovieLens官网下载ml-latest-small.zip解压后放到data/目录。格式必须对齐源码里读取的列名特别是userId的大小写很多源码从UserID改到userId时没同步所有引用跑起来会报KeyError。4.2 核心参数调优最近邻数K、最小共同评分数和推荐数量协同过滤这三个参数直接影响最终体验源码里一般会放在config.py或main.py顶部# config.py K_NEIGHBORS 10 # 最近邻数量过小覆盖不足过大引入噪声 MIN_COMMON 5 # 计算相似度时至少需要的共同评分数 TOP_N 20 # 最终推荐列表长度 USE_ITEM_CF True # True走物品协同False走用户协同参数调整经验法则K_NEIGHBORS在5到20之间调。热门电影场景下K10到15最稳K太大会把相似度低的用户也拉进来。MIN_COMMON在3到8之间。数据稀疏时设置3数据稠密时设置8低于3的相似度几乎全是噪声。TOP_N根据产品需求决定APP首屏一般10或20网页侧边栏5就够。不要盲目改大推荐列表越长尾部物品的相关性越差。切换UserCF和ItemCF时注意相似度矩阵是离线算好存成.npy还是每次都重新算。如果每次跑都要几分钟说明源码没做缓存你可以用np.save把相似度矩阵存下来第二次加载会快一个数量级。4.3 命令行入口让推荐系统可被显式调用好的源码应该支持以下调用方式python recommend.py --user 196 --top 20 --algo itemcf我一般会把推荐函数封装成CLI这样测试和演示都方便。源码如果只有main.py你可以补一个简洁版import argparse from recommender import UserCF, ItemCF if __name__ __main__: parser argparse.ArgumentParser(description电影推荐系统命令行入口) parser.add_argument(--user, typeint, requiredTrue, help目标用户ID) parser.add_argument(--top, typeint, default20, help推荐数量) parser.add_argument(--algo, choices[usercf, itemcf], defaultitemcf) parser.add_argument(--data, defaultdata/ratings.csv) args parser.parse_args() model ItemCF(data_pathargs.data) if args.algo itemcf else UserCF(data_pathargs.data) recs model.recommend(args.user, top_nargs.top) print(recs)这样即使你不想深入内部也能通过命令行验证不同参数下的推荐结果。注意解析后的args对象传给推荐函数时要在函数内部做参数合法性检查比分说负数、用户不存在。4.4 可视化推荐结果为什么有人盯着你的推荐列表看最后加一个简单的输出技巧把电影ID映射成标题movie_map pd.read_csv(data/movies.csv).set_index(movieId)[title].to_dict() for rank, movie_id in enumerate(recs, 1): title movie_map.get(movie_id, 未知电影) print(fTop {rank}: {title})这一步不是必须的但演示时很有冲击力。你一眼能看到推荐的是不是科幻片或者老片如果是外国小众片扎堆说明相似度计算里没有加入年份、题材的惩罚项。常见做法是对已有评分记录较少的冷门电影相似度结果乘以一个置信权重比如sim * (common / (common 10))这样冷门电影即使相似度高也不会被过度推到榜首。5. 别让相似度矩阵吃掉内存稀疏存储、Annoy索引与增量更新技巧建议直接记住一个结论不要用稠密矩阵存相似度。当用户数是2万时UserCF的相似度矩阵是2万×2万每个浮点8字节就是3.2GB内存个人电脑直接爆。换成ItemCF电影数只有几千相似度矩阵也就几千万个元素但依然有大量0。推荐的做法是用scipy.sparse保存相似度只存非零项。from scipy.sparse import lil_matrix n_items matrix.shape[1] sim_sparse lil_matrix((n_items, n_items), dtypenp.float32) # 只在计算出的相似度非零的位置赋值 # 后续用csr_matrix转换加快矩阵乘法在稀疏相似度矩阵上做推荐可以用矩阵乘法一步算出用户对所有电影的预测评分# 评分矩阵 R (用户x物品)相似度矩阵 S (物品x物品) # 预测分 R_normalized * S pred rating_matrix_csr.dot(sim_csr)这里rating_matrix_csr要用“中心化后的评分”即每行减去用户平均分这样预测值才是相对于用户平均分的偏移。很多源码只做了这个中心化就结束了但在输出前要加回用户平均分否则推荐分数没有可比性的。踩过这个坑的人应该记得不加回去推荐的全是热门高分电影。如果数据规模再大超过十万物品稀疏矩阵的乘法也会吃力。那时我会改用annoy库的AnnoyIndex它用树形索引做近似最近邻速度能快几十倍。注意Annoy只支持索引向量所以你得先把每部电影的相似度向量归一化后灌进去from annoy import AnnoyIndex f 64 # 用SVD降维后的向量维度不是原始物品数 t AnnoyIndex(f, angular) for i in range(n_items): t.add_item(i, item_vector[i]) t.build(10) # 10棵树树越多精度越高 t.get_nns_by_item(item_id, top_n)Annoy的build参数10是树的棵数增加它会提高召回率但降低构建速度。实际我一般用50到100棵树查询精度能到95%以上内存占用比稠密矩阵小两个数量级。最后一个问题是增量更新。电影推荐系统每天都有新评分如果你每次都全量重算相似度矩阵凌晨任务要跑几个小时。常见做法是离线每天全量计算在线实时用“最近一小时新评分”快速更新对应物品的相似度增量然后用一个加权合并的公式去刷新推荐列表。源码里通常不会有这部分但当你跟别人讨论系统架构时能说出“全量计算增量补算”两层结构就比只说“我用了协同过滤”更进一步。具体的增量公式不复杂新相似度旧相似度×(旧共同评分数/(旧共同评分数1)) 新评分贡献/新共同评分数。这里的关键是记住“共同评分数量”需要跟着更新否则新旧权重失衡推荐列表会在一次更新后剧烈漂移。调试时你可以人为喂进几条评分观察推荐结果变化是否平滑如果Top5骤变就是权重没做平滑处理。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门