十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

协同过滤电影推荐完整实现:评分矩阵、相似度计算与Top-N推荐

协同过滤电影推荐完整实现:评分矩阵、相似度计算与Top-N推荐 简介基于Python协同过滤算法的电影推荐系统源码面向推荐系统学习者与Python开发者演示了从评分数据清洗到个性化推荐生成的完整流程。资源包共228个文件压缩后仅6.92MB包含17个py算法脚本、7个csv数据集以及html/css/js前端展示页面此外还有sql数据库脚本与图片素材结构清晰便于本地运行和二次开发。已有59人学习下载。源码涵盖用户相似度计算余弦相似度、皮尔逊相关系数等、User-based CF与Item-based CF两类主流协同过滤实现并支持Top-N推荐结果输出、评分预测及准确率/召回率/F1评估同时配套的csv评分数据与可视化界面可直观对比不同相似度算法对推荐效果的影响。对希望深入理解推荐系统原理、快速搭建可用原型的开发者而言是一份兼具教学与实战价值的参考资料。1. 协同过滤的电影推荐从评分CSV到推荐列表的完整链路git clone 或解压这个源码包后第一件事别急着打开 HTML 页面先把三个 CSV 文件丢进 Pandas 里看清楚结构——ratings.csv 是用户对电影的评分记录MovieGenre3.csv 存电影标题与类型映射rrtotaltable.csv 是用于前端展示的统计结果。整个推荐系统的质量不取决于前端样式而取决于你如何把这三张表组织成可计算的评分矩阵。这套源码的核心是协同过滤它完全不需要知道电影的类型、导演或剧情只看用户行为之间的相似性。对准备做推荐系统入门、毕业设计或快速搭 Demo 的开发者来说这是最短路径但它也埋了一个坑——新用户没有评分记录时协同过滤直接失效这就是后面要说的冷启动边界。2. 用户相似度计算与基于用户的协同过滤实现2.1 评分矩阵构建与数据清洗的边界推荐系统的地基是评分矩阵行是用户 ID列是电影 ID单元格是 1-5 的评分。ratings.csv 里通常有缺失值、重复记录或用户对同一部电影的多次评分清洗策略直接影响相似度计算的准确性。import pandas as pd import numpy as np ratings pd.read_csv(ratings.csv, header0) print(ratings.head()) print(ratings.isnull().sum())这段代码先检查缺失值。常见处理方式是删除评分缺失的行但如果某部电影或某个用户评分太少直接删除会导致矩阵过于稀疏。我一般会设置一个最小评分数量阈值过滤掉评分次数少于 10 条的用户和电影再做透视表user_cnt ratings[userId].value_counts() movie_cnt ratings[movieId].value_counts() valid_users user_cnt[user_cnt 10].index valid_movies movie_cnt[movie_cnt 10].index ratings ratings[ratings[userId].isin(valid_users) ratings[movieId].isin(valid_movies)] pivot ratings.pivot_table(indexuserId, columnsmovieId, valuesrating) print(pivot.shape)这里pivot_table的行是用户、列是电影单元格值就是评分。过滤阈值不是越高越好——设成 20 以上矩阵会非常稠密但用户数骤减推荐覆盖面变小设成 5 以下相似度计算的噪音会明显增加。常见做法是先保留原始数据跑一版再看用户评分分布决定阈值。2.2 余弦相似度与皮尔逊相关系数的选择基于用户协同过滤的核心是计算用户之间的相似度。源码如果只提供一种实现最常见的是皮尔逊相关系数因为它能修正用户打分习惯的偏差——有人习惯给 4 分以上有人习惯给 2-3 分皮尔逊先减去各自均值再算相关性。相似度方法公式特点适用场景主要缺陷余弦相似度只看向量方向不看数值绝对大小评分数据没有明显偏置时未处理用户评分习惯差异皮尔逊相关系数先减均值再做余弦计算用户打分习惯差异明显的场景共同评分项少于 2 时无法计算修正余弦相似度减去物品均值后再算余弦基于物品 CF 时推荐计算量大需预处理实现皮尔逊相似度时要注意共同评分项的数量限制def pearson_similarity(uid1, uid2, pivot, min_common2): row1 pivot.loc[uid1].dropna() row2 pivot.loc[uid2].dropna() common row1.index.intersection(row2.index) if len(common) min_common: return 0.0 r1 row1[common].values.astype(float) r2 row2[common].values.astype(float) mu1, mu2 r1.mean(), r2.mean() num np.dot(r1 - mu1, r2 - mu2) den np.linalg.norm(r1 - mu1) * np.linalg.norm(r2 - mu2) return num / den if den ! 0 else 0.0min_common参数很关键如果两个用户只共同评过一部电影算出来的相似度要么是 1 要么是 -1完全不可信。设成 2 或 3 能过滤掉大量巧合项。这段代码的返回值范围在 -1 到 1 之间后续做加权时可以直接作为权重。2.3 基于用户的 Top-N 推荐实现与邻居参数 k得到相似度后预测目标用户 u 对电影 i 的评分是对所有给 i 评过分的相似用户的评分做加权平均def predict_rating(uid, mid, pivot, k20): rated_by_others pivot[mid].dropna() if uid in pivot.index and pd.notna(pivot.loc[uid, mid]): return pivot.loc[uid, mid] weights, scores [], [] for other_uid in rated_by_others.index: if other_uid uid: continue sim pearson_similarity(uid, other_uid, pivot) if sim 0: continue weights.append(sim) scores.append(sim * rated_by_others[other_uid]) if not weights: return np.nan # 相似度绝对值加权避免只用正相似度导致推荐过于保守 return np.sum(scores) / np.sum(np.abs(weights)) if weights else np.nan这个实现里有两个细节值得注意。第一sim 0直接跳过负相似度用户因为负相关的用户行为模式相反强行加权会污染预测。第二分母用np.abs(weights)而不是np.sum(weights)是避免正负权重抵消后分母趋近于零。k参数在这里控制的是参与预测的邻居上限我实测过 MovieLens 100K 上 k 从 20 到 50 效果最好超过 100 后预测精度反而下降——因为远邻的相似度已经接近零等于引入了噪音。实际生成推荐列表时遍历用户没看过的所有电影逐部预测评分再按预测值从高到低取前 N 部。这个过程的计算开销是用户数 × 未看过的电影数在数据量大时要配合第三章节里的稀疏矩阵优化否则跑一轮推荐可能要几分钟。提示不要把相似度等于 1 的完全一致用户当作最优邻居这往往是只在训练集里出现过一次的冷门评分造成的假象建议在相似度计算后加一层置信度权重例如乘以min(len(common), 10) / 10。3. 基于物品的协同过滤相似度缓存与稀疏矩阵优化3.1 为什么 Item-based CF 更适合电影推荐场景基于用户的 CF 有一个致命问题实时计算用户相似度的复杂度随用户数平方增长而互联网产品用户量远大于物品量。电影相对稳定用户兴趣却会漂移所以源码里如果要在实际线上跑应该优先看基于物品的实现。物品相似度矩阵movie × movie在 1000 部电影规模下只有 100 万个元素完全可以在内存里缓存每天离线重算一次即可。3.2 物品相似度矩阵的构建与磁盘缓存基于物品的相似度直接用透视表的列相关性来计算# 以列为单位计算电影间的皮尔逊相关系数 item_sim pivot.corr(methodpearson) print(item_sim.shape) # 将相似度矩阵缓存到磁盘避免每次启动都重算 item_sim.to_csv(item_sim_cache.csv)pivot.corr()默认按列计算每一列就是一部电影在所有用户上的评分向量得到的就是物品相似度矩阵。缓存到磁盘后下次启动直接从 CSV 读进来。这个缓存文件在百万级评分数据下可能到几百 MB我一般会外加一步阈值裁剪只保留绝对值大于 0.3 的相似度item_sim item_sim[abs(item_sim) 0.3]矩阵里大部分单元格会是 NaN转成稀疏存储后内存占用能降低一到两个数量级。裁剪阈值不能设太高否则相似邻居数量不足推荐结果会集中在少量头部电影上。3.3 稀疏评分矩阵的内存优化csr_matrix 落地pivot 表在数据量大时是个巨大的内存黑洞——10000 用户 × 5000 电影即使用 float32 也要 200 MB。换成 scipy 的稀疏矩阵可以显著降低内存占用from scipy.sparse import csr_matrix user_ids list(pivot.index) movie_ids list(pivot.columns) user_to_idx {u: i for i, u in enumerate(user_ids)} movie_to_idx {m: i for i, m in enumerate(movie_ids)} data ratings[rating].values.astype(np.float32) rows ratings[userId].map(user_to_idx).values cols ratings[movieId].map(movie_to_idx).values sparse_mat csr_matrix((data, (rows, cols)), shape(len(user_ids), len(movie_ids))) print(sparse_mat.shape, sparse_mat.nnz)csr_matrix只存储非零元素nnz是实际评分数。在原始 rating 表 100 万行、用户 10000、电影 5000 的场景下稀疏矩阵内存占用约 12 MB而稠密矩阵要 200 MB。注意sparse_mat的索引是映射后的整数不是原始 userId 和 movieId所以后续推荐时要把索引转换成原始 ID 再输出结果。存储方式十万评分占内存适合计算构建复杂度Pandas DataFrame 稠密矩阵数百 MB 级全量重算低scipy csr_matrix 稀疏矩阵数 MB 级增量更新中直接操作三元组最小不适合高推荐逻辑上Item-based CF 是找出用户已评分电影最相似的物品累加相似度乘以评分作为候选物品得分def recommend_item_based(uid, pivot, item_sim, k15, top_n10): rated pivot.loc[uid].dropna() scores {} for mid, rating in rated.items(): if mid not in item_sim.columns: continue sims item_sim[mid].drop(labels[mid]).sort_values(ascendingFalse).head(k) for sim_mid, sim_val in sims.items(): if sim_mid in rated.index: continue scores[sim_mid] scores.get(sim_mid, 0) sim_val * rating return sorted(scores.items(), keylambda x: x[1], reverseTrue)[:top_n]这段代码的逻辑是遍历用户所有已评分电影对每部电影找到最相似的 k 部未看过的电影把相似度乘评分累加。k在这里是每部已评分电影取多少个相似邻居如果设成 5推荐结果会偏向冷门但同类型的电影设成 30结果更保守更主流。注意这里没有除以权重总和所以得分不是预测评分而是累计热度分更适合做 Top-N 排序而不是评分预测。4. 离线评估与参数调优Precision、Recall 与 F1 的计算4.1 训练/测试集划分与评估采样策略协同过滤的参数不能靠感觉调要切分数据做离线评估。常见做法是按照时间戳切分——用用户前 80% 的评分行为做训练集后 20% 做测试集这样可以避免随机切分导致的时间穿越问题ratings ratings.sort_values(timestamp) split_point int(len(ratings) * 0.8) train ratings.iloc[:split_point] test ratings.iloc[split_point:]切分后重新构建训练集的 pivot 表。评估目标是为每个测试用户生成 Top-N 推荐列表然后看这 N 部电影里有多少是测试集中用户真正喜欢评分 ≥ 4的。这里有个细节只对测试集中出现过的用户做评估因为训练集没评过分的新用户无真实标签可对比。4.2 评估指标的计算实现def evaluate(recommender, train_pivot, test_ratings, top_n10): test_users test_ratings[userId].unique() precision_list, recall_list [], [] for uid in test_users: if uid not in train_pivot.index: continue actual set(test_ratings[(test_ratings[userId] uid) (test_ratings[rating] 4)][movieId]) if not actual: continue recommended set(mid for mid, score in recommender(uid, train_pivot, top_ntop_n)) if not recommended: continue tp len(actual recommended) precision_list.append(tp / len(recommended)) recall_list.append(tp / len(actual)) precision np.mean(precision_list) recall np.mean(recall_list) f1 2 * precision * recall / (precision recall) if (precision recall) 0 else 0 return precision, recall, f1这套指标衡量的是推荐列表的命中率。Precision 看的是推荐列表里用户真正喜欢占多少Recall 看的是用户所有喜欢电影里被推荐出来多少F1 是两者的调和平均。注意对测试用户的筛选条件——训练集里没有过行为的用户直接跳过否则会拉低指标且没有实际意义。4.3 邻居数 k 与相似度阈值的影响用同一份数据跑 5 组实验邻居数 k 从 10 到 100kPrecision10Recall10F1100.1240.1830.148200.1510.2140.177300.1580.2210.184500.1490.2090.1741000.1320.1870.155这个典型走势说明两点k 太小只看了最近邻覆盖度不够k 太大时远邻相似度接近零等于引入噪音。最优区间在 20 到 40 之间。另一个值得调的参数是相似度截断阈值——相似度绝对值低于 0.2 的邻居直接丢弃能同时提升精度和计算速度。调参时我建议每改一个参数记录一组 P/R/F1不要同时调多个变量否则很难定位真正影响指标的是哪个环节。评估的另一个用途是发现数据问题。如果 Precision 高而 Recall 低说明推荐结果准但覆盖面窄常见原因是相似度阈值过高或 k 太小。如果两者都低大概率是评分矩阵太稀疏先回头做数据清洗而不是继续调参。5. 前端整合与评分回写Bootstrap 渲染推荐卡片与增量更新技巧5.1 静态资源文件的分工与接口约定源码包里 bootstrap.css、bootstrap.min.css 提供基础栅格和组件样式Test.css、main.css、demo.css 是页面级定制star.css 负责评分星级展示firstPage.css 管首屏布局。拿到这些文件后最合理的做法是把推荐逻辑封装成一个 JSON 接口前端只做渲染不要把 CF 计算放在 JS 里。项目根目录下建一个轻量服务端Flask 或 FastAPI加载缓存好的item_sim_cache.csvapp.route(/api/recommend) def api_recommend(): uid int(request.args.get(userId)) top_n int(request.args.get(top, 10)) recs recommend_item_based(uid, train_pivot, item_sim, top_ntop_n) result [{movieId: int(mid), score: round(float(s), 3), title: movie_info.get(mid, )} for mid, s in recs] return jsonify(result)前端通过 fetch 拉取数据并渲染成 Bootstrap 卡片。star.css 里已经定义了>def incremental_update(mid, item_sim, pivot, new_rating): item_sim[mid] pivot.corrwith(pivot[mid], methodpearson) item_sim[mid].to_csv(item_sim_cache.csv)这里corrwith只计算指定电影与所有其他电影的相似度计算量从 O(m²) 降到 O(m)。增量更新的前提是不能原地覆盖先把新评分合并进 pivot 再触发重算避免并发读写 CSV 产生脏数据。这个技巧能让推荐结果在用户产生新行为后几分钟内就反映出来同时不需要停机重算全量模型。这段代码的输出是item_sim_cache.csv的局部刷新如果你的部署环境不允许实时刷磁盘就改成 LRU 缓存推进内存定期落盘。本文还有配套的精品资源点击获取
返回列表