news 2026/10/3 6:53:34

从零手写协同过滤:User-Based与Item-Based算法实现与选型指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零手写协同过滤:User-Based与Item-Based算法实现与选型指南

简介:这份资源用Python实现了基于物品与基于用户两种协同过滤推荐算法,面向推荐系统入门者、进阶学习者以及需要完成课程设计、大作业或毕设项目的同学,帮助理解相似度计算、邻居选择与评分预测等核心环节。压缩包共4个文件,包含2个py脚本分别对应Item_CF与User_CF两套算法实现,1个csv数据文件用于读取用户-物品评分矩阵,另有1个gitignore配置文件,整体约6KB,结构精简,便于快速定位与调试。目前已有449人学习下载,可作为推荐算法入门的参考范例。读者可从中获得两种协同过滤思路的完整代码框架、数据组织方式与算法落地流程,并在此基础上自行调整相似度度量、补充功能或迁移到自己的数据集,适合具备一定Python基础、能独立调试代码的学习者参考使用。

1. 从零手写协同过滤:为什么我不建议你直接调库

很多做推荐系统的朋友第一次接触协同过滤,都是被“调个 surprise 库三行出结果”带进门的。但真到了业务里,你会发现库封得太死:相似度算法想换、冷启动想加规则、评分矩阵想稀疏化处理,处处掣肘。我见过太多人拿着现成 API 跑通 MovieLens 就以为掌握了推荐,结果一上真实数据就翻车——用户量一上来,内存直接爆掉。所以这篇笔记,我打算用 Python 从零把基于物品的协同过滤和基于用户的协同过滤各实现一遍,不依赖任何推荐专用库,只用 numpy 和 pandas。你会看到相似度矩阵怎么算、邻居怎么选、评分怎么预测,以及两个算法在什么场景下该选谁。适合已经会 Python 基础语法、想真正搞懂推荐算法内部运转逻辑的工程师,也适合正在做校园项目、电商 demo 需要落地推荐模块的同学。读完你手里会有一套能直接跑、能改参数、能接自己数据的代码骨架。

2. 基于用户的协同过滤:找和你口味相似的人

2.1 核心逻辑与相似度选型

基于用户的协同过滤,英文叫 User-Based Collaborative Filtering,思路非常直白:如果用户 A 和用户 B 对一批物品的历史评分高度一致,那 A 喜欢但 B 没看过的物品,就可以推给 B。这里的关键是“高度一致”怎么量化。常见做法是余弦相似度、皮尔逊相关系数、修正余弦相似度三种。余弦相似度只看向量夹角,对评分绝对值不敏感;皮尔逊会减去用户平均分,能抵消不同用户打分尺度差异——有人习惯全打 4 分,有人习惯全打 2 分,皮尔逊能把这层偏差去掉。我一般会优先用皮尔逊,因为真实评分数据里用户偏置太常见了。但皮尔逊有个坑:如果两个用户只共同评过一个物品,相关系数无法计算,分母为零。所以工程上要么设最小共同评分阈值,要么回退到余弦。

下面这段代码构建用户-物品评分矩阵,并计算用户间皮尔逊相似度:

import numpy as np import pandas as pd from sklearn.metrics.pairwise import cosine_similarity # 构造示例评分数据:user_id, item_id, rating data = { 'user_id': [1,1,1,2,2,2,3,3,3,4,4,4], 'item_id': [101,102,103,101,102,104,101,103,104,102,103,104], 'rating': [5,3,4,4,2,5,2,4,3,3,5,4] } df = pd.DataFrame(data) # 构建用户-物品评分矩阵,缺失值填0 rating_matrix = df.pivot_table( index='user_id', columns='item_id', values='rating' ).fillna(0) print("评分矩阵形状:", rating_matrix.shape) print(rating_matrix) # 皮尔逊相似度:对每行用户向量计算相关系数 user_sim = np.corrcoef(rating_matrix) user_sim_df = pd.DataFrame( user_sim, index=rating_matrix.index, columns=rating_matrix.index ) print("\n用户相似度矩阵:") print(user_sim_df.round(3))

这段代码里pivot_table把长表转成宽表,行是用户、列是物品,缺失评分填 0。注意填 0 不是“评了 0 分”,而是“没评过”,后续预测时要靠掩码把未评分位置排除掉。np.corrcoef直接对矩阵行向量算皮尔逊系数,得到对称相似度矩阵。参数上,fillna(0)是最粗暴的做法,数据稀疏时会导致大量零向量参与计算,相似度失真。更稳的做法是只对共同评分物品计算,后面避坑章节会展开。

2.2 邻居选择与评分预测

拿到相似度矩阵后,预测用户 u 对物品 i 的评分,公式是:取 u 最相似的 K 个用户中评过 i 的那些人,用他们的评分做加权平均,权重就是相似度。K 的选择很讲究:K 太小,邻居代表性不够,预测抖动大;K 太大,会把不相似的人也拉进来稀释信号。经验值在 20 到 50 之间,数据量大可以到 100。另外要设一个相似度下限,比如只取相似度大于 0.3 的邻居,否则负相关或接近零的用户会污染结果。

def predict_user_cf(user_id, item_id, rating_matrix, user_sim_df, K=3): """基于用户的协同过滤评分预测""" if user_id not in rating_matrix.index: return rating_matrix[item_id].mean() # 冷启动回退 # 取出该用户与其他用户的相似度,排除自己 sims = user_sim_df[user_id].drop(user_id) # 只保留评过目标物品的用户 rated_users = rating_matrix[rating_matrix[item_id] > 0].index sims = sims[sims.index.isin(rated_users)] # 按相似度降序取前K个 top_k = sims.sort_values(ascending=False).head(K) if len(top_k) == 0 or top_k.sum() == 0: return rating_matrix[item_id].mean() # 加权平均 numerator = sum( top_k[u] * rating_matrix.loc[u, item_id] for u in top_k.index ) denominator = top_k.abs().sum() return numerator / denominator # 预测用户1对物品104的评分 pred = predict_user_cf(1, 104, rating_matrix, user_sim_df, K=3) print(f"用户1对物品104的预测评分: {pred:.2f}")

函数先做冷启动判断:如果用户不在矩阵里,直接返回物品全局均分。然后从相似度序列里排除自身,再筛出对目标物品有评分的人。top_k.abs().sum()用绝对值求和是为了处理负相似度的情况,否则正负抵消会导致分母异常。这里 K=3 只是演示,真实场景要交叉验证调参。注意rating_matrix[item_id] > 0这个条件依赖填 0 的约定,如果评分范围包含 0 分,需要改用掩码矩阵单独记录“是否评分”。

3. 基于物品的协同过滤:找和你买过的东西相似的物品

3.1 物品相似度为什么通常比用户相似度更稳

基于物品的协同过滤,Item-Based CF,逻辑是:如果物品 A 和物品 B 被同一批用户喜欢,那喜欢 A 的人大概率也会喜欢 B。它和 User-Based 最大的区别在于相似度矩阵的维度——用户数通常远大于物品数,而且用户兴趣会漂移,今天喜欢数码明天可能看母婴,但物品之间的关联相对稳定。所以工业界推荐系统里,Item-Based 的占比远高于 User-Based。亚马逊早年那篇经典论文就是把 Item-to-Item 推上台面的。计算物品相似度时,我一般用修正余弦相似度,因为它能减去用户平均评分,消除用户打分偏置。公式是在余弦基础上,每个评分先减去该用户对所有物品的平均分。

# 构建物品-用户矩阵(评分矩阵转置) item_user_matrix = rating_matrix.T print("物品-用户矩阵形状:", item_user_matrix.shape) # 修正余弦:先减用户均分 user_means = rating_matrix.replace(0, np.nan).mean(axis=1) adjusted = rating_matrix.copy() for u in rating_matrix.index: mask = rating_matrix.loc[u] > 0 adjusted.loc[u, mask] = rating_matrix.loc[u, mask] - user_means[u] # 转置后计算物品间余弦相似度 item_sim = cosine_similarity(adjusted.T) item_sim_df = pd.DataFrame( item_sim, index=rating_matrix.columns, columns=rating_matrix.columns ) print("\n物品相似度矩阵:") print(item_sim_df.round(3))

这里先把评分矩阵转成物品-用户视角,然后对每个用户的评分减去他的平均分,得到修正后的矩阵。replace(0, np.nan).mean(axis=1)只对真实评分求均值,避免 0 拉低平均数。修正后再转置算余弦,得到物品间相似度。参数上,cosine_similarity默认按行计算,所以要先转置让物品成为行向量。如果数据量很大,这个全量相似度矩阵会占 O(n²) 内存,后面会讲怎么用稀疏矩阵和 Top-N 截断来优化。

3.2 基于物品相似度的推荐生成

预测用户 u 对物品 i 的评分时,Item-Based 的做法是:找到 i 最相似的 K 个物品,看用户 u 对这些相似物品的评分,用相似度加权平均。和 User-Based 的区别在于,这里遍历的是物品邻居,而不是用户邻居。好处是物品相似度矩阵可以离线算好、定期更新,线上只做查表和加权,响应速度极快。

def predict_item_cf(user_id, item_id, rating_matrix, item_sim_df, K=3): """基于物品的协同过滤评分预测""" if item_id not in rating_matrix.columns: return rating_matrix.loc[user_id].replace(0, np.nan).mean() # 取目标物品与其他物品的相似度 sims = item_sim_df[item_id].drop(item_id) # 只保留该用户评过分的物品 user_rated = rating_matrix.loc[user_id] rated_items = user_rated[user_rated > 0].index sims = sims[sims.index.isin(rated_items)] top_k = sims.sort_values(ascending=False).head(K) if len(top_k) == 0 or top_k.sum() == 0: return user_rated.replace(0, np.nan).mean() numerator = sum( top_k[i] * user_rated[i] for i in top_k.index ) denominator = top_k.abs().sum() return numerator / denominator # 预测用户2对物品103的评分 pred_item = predict_item_cf(2, 103, rating_matrix, item_sim_df, K=3) print(f"用户2对物品103的预测评分: {pred_item:.2f}") # 给用户2生成Top-N推荐 def recommend_top_n(user_id, rating_matrix, item_sim_df, N=2, K=3): """给用户生成Top-N推荐列表""" unrated = rating_matrix.columns[rating_matrix.loc[user_id] == 0] scores = {} for item in unrated: scores[item] = predict_item_cf( user_id, item, rating_matrix, item_sim_df, K ) ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True) return ranked[:N] recs = recommend_top_n(2, rating_matrix, item_sim_df, N=2) print("用户2的Top-2推荐:", recs)

predict_item_cf的结构和 User-Based 版本对称,只是把“相似用户”换成“相似物品”。recommend_top_n遍历用户未评分的物品,逐个预测打分再排序。这里有个性能隐患:如果物品有上万件,每次推荐都要循环调用预测函数,线上肯定扛不住。常见优化是离线预计算物品相似度 Top-K 列表,线上只对候选集打分。参数 K 控制邻居数量,N 控制推荐条数,两个要分开调。

4. 两个算法怎么选:场景、数据量和效果对比

4.1 用户维度与物品维度的成本账

选 User-Based 还是 Item-Based,第一笔账是计算成本。假设平台有 M 个用户、N 个物品。User-Based 要算 M×M 的用户相似度矩阵,Item-Based 要算 N×N 的物品相似度矩阵。电商场景里 M 通常是 N 的几十倍甚至上百倍,所以 Item-Based 的矩阵小得多,内存和计算都更省。第二笔账是更新频率:新用户不断注册,用户相似度矩阵需要频繁重算;但物品上新速度相对慢,物品相似度可以一天甚至一周更新一次。第三笔账是解释性:Item-Based 能直接说“因为你买了 A,所以推荐相似的 B”,用户容易理解;User-Based 说“和你相似的人还买了 C”,解释链路更长。

对比维度User-Based CFItem-Based CF
相似度矩阵规模用户数²物品数²
更新频率高,用户增长快低,物品相对稳定
冷启动表现新用户差新物品差
可解释性较弱较强
适用场景用户少、物品多用户多、物品少
线上响应需实时算邻居可离线预计算

这张表不是绝对的。我见过用户量只有几千的垂直社区,User-Based 效果反而更好,因为用户兴趣集中、共同评分多。所以选型前先看数据稀疏度:共同评分比例低于 1% 时,两个算法都容易退化,得先做降维或引入内容特征。

4.2 用同一份数据跑通两套流程

为了让你直观对比,我用同一份评分数据把两个算法的推荐结果都跑出来。下面代码把前面的函数串起来,输出每个用户的推荐列表:

# 对每个用户分别用两种算法生成推荐 for uid in rating_matrix.index: user_recs = recommend_top_n(uid, rating_matrix, item_sim_df, N=2) print(f"\n用户{uid} 的 Item-Based 推荐: {user_recs}") # User-Based 推荐生成 def recommend_user_cf(user_id, rating_matrix, user_sim_df, N=2, K=3): unrated = rating_matrix.columns[rating_matrix.loc[user_id] == 0] scores = {} for item in unrated: scores[item] = predict_user_cf( user_id, item, rating_matrix, user_sim_df, K ) ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True) return ranked[:N] for uid in rating_matrix.index: user_recs = recommend_user_cf(uid, rating_matrix, user_sim_df, N=2) print(f"用户{uid} 的 User-Based 推荐: {user_recs}")

跑完你会发现,两个算法给出的推荐列表可能有重叠,也可能完全不同。重叠部分通常是数据里信号最强的关联,差异部分则反映两个算法的偏好:Item-Based 更偏向推荐与用户历史物品相似的,User-Based 更偏向推荐相似用户群里的热门物品。实际业务里可以做成融合推荐,各取 Top-N 后按权重合并去重。参数 N 和 K 需要根据业务指标调,比如点击率、转化率,不能只凭离线 RMSE 决定。

5. 避坑与排查:协同过滤落地时最容易翻车的五个点

5.1 稀疏矩阵填 0 导致相似度失真

现象:相似度矩阵里大量用户或物品之间相似度接近 0 或 NaN,推荐结果随机。原因:评分矩阵极度稀疏,填 0 后零向量参与余弦或皮尔逊计算,分母被拉大,真实关联被淹没。解决:不要全局填 0,改用掩码矩阵记录有效评分,相似度只对共同评分项计算。或者用 scipy 稀疏矩阵存储,计算时只遍历非零元素。

5.2 热门物品霸榜推荐列表

现象:不管什么用户,推荐结果里总是那几个爆款。原因:热门物品被评分次数多,与任何物品的共同评分都高,相似度天然偏大。解决:在相似度计算时对热门物品做惩罚,比如除以物品评分人数的对数,或者用 TF-IDF 思路降低高频物品权重。也可以在推荐排序阶段加多样性重排。

5.3 新用户和新物品冷启动无解

现象:新注册用户没有评分记录,推荐接口返回空或全站热门。原因:协同过滤完全依赖历史行为,没有行为就没有向量。解决:新用户走热门推荐或引导选择兴趣标签,用内容特征做过渡;新物品用属性相似度找邻居,或者给少量曝光收集初始评分。工程上常见做法是混合推荐,协同过滤只负责老用户老物品。

5.4 相似度矩阵内存溢出

现象:用户量到十万级时,程序 OOM 崩溃。原因:全量相似度矩阵是稠密的,十万用户就是 100 亿个浮点数,内存根本放不下。解决:用稀疏矩阵只存 Top-K 邻居,或者用矩阵分解降维后再算相似度。线上服务一般只加载目标用户的邻居列表,不加载全量矩阵。

5.5 离线指标好线上效果差

现象:离线 RMSE 很低,上线后点击率没涨甚至跌。原因:离线评估用的是已评分数据,但线上推荐的是未评分物品,存在选择偏差;而且离线没考虑位置、时效、多样性。解决:离线只做粗筛,线上做 A/B 测试,关注点击率、转化率、停留时长等业务指标。推荐列表里混入一定比例的新物品做探索。

6. 进阶技巧:用稀疏矩阵和 Top-K 截断把性能压下来

前面代码为了可读性用了稠密矩阵,真实数据上必须换稀疏存储。scipy 的csr_matrix可以只存非零评分,内存占用跟评分数量成正比而不是用户数乘物品数。相似度计算也可以用sklearn的pairwise_distances配合稀疏输入,或者自己写只遍历共同评分项的循环。另一个技巧是 Top-K 截断:算完相似度后,每个物品只保留最相似的 K 个邻居,其余置零,这样相似度矩阵也变成稀疏的。K 一般取 20 到 100,具体看数据密度。

from scipy.sparse import csr_matrix # 转稀疏矩阵 sparse_matrix = csr_matrix(rating_matrix.values) print("稀疏矩阵非零元素数:", sparse_matrix.nnz) print("稠密矩阵元素数:", rating_matrix.size) print("内存压缩比:", rating_matrix.size / sparse_matrix.nnz) # Top-K 截断示例:每个物品只保留相似度最高的K个邻居 def top_k_similarity(sim_df, K=2): result = pd.DataFrame(0, index=sim_df.index, columns=sim_df.columns) for idx in sim_df.index: row = sim_df.loc[idx].drop(idx) top_k = row.nlargest(K) result.loc[idx, top_k.index] = top_k.values return result truncated_sim = top_k_similarity(item_sim_df, K=2) print("\n截断后的物品相似度矩阵:") print(truncated_sim.round(3))

csr_matrix把稠密矩阵转成压缩稀疏行格式,nnz是非零元素个数。示例数据里压缩比可能不明显,但真实场景评分矩阵稀疏度通常在 95% 以上,压缩比能到几十倍。top_k_similarity函数遍历每个物品,只保留相似度最高的 K 个邻居,其余填零。这样后续预测时只查这几个邻居,计算量大幅下降。注意截断会损失一部分长尾关联,K 太小推荐会变得保守,需要根据业务容忍度调。

我自己的习惯是:任何推荐算法上线前,先用稀疏矩阵跑一遍全量数据,看内存和耗时能不能接受,再决定要不要上分布式。协同过滤不是银弹,但它是最容易理解、最容易解释、最容易改的推荐基线。把这套代码吃透,后面上矩阵分解或深度模型时,你才知道每一步在优化什么。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 6:53:13

LabVIEW+FlexRIO实战:三个月搭建质谱仪高速数据采集系统

质谱仪这东西,做过的人都知道,硬件只是入场券,真正吃时间的是数据采集链路和上位机软件的联调。我手上这个项目,从立项到系统能跑出第一张合格的质谱图,前后正好三个月。用的核心架构就是 LabVIEW 加 FlexRIO&#xff…

作者头像 李华
网站建设 2026/10/3 6:53:10

Codium Windsurf 实战:用 TaoToken 统一 Key 打通 Cursor 对手的 API 通道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 6:52:53

AI写嵌入式驱动代码的翻车陷阱与安全开发工作流

1. 从一块变砖的板子说起:AI写驱动到底哪里不靠谱去年冬天,一个做工业网关的朋友半夜给我打电话,说他们小批量试产的二十块板子,烧完固件之后有七块直接起不来,串口一片死寂,连Bootloader的打印都看不到。他…

作者头像 李华
网站建设 2026/10/3 6:52:52

ClaudeCode稳定备用方案:API接入详解与TaoToken统一通道实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 6:51:38

用Rust驱动reTerminal E1002六色墨水屏:从SPI到OPC UA的完整实践

拿到 reTerminal E1002 这台板子的时候,我第一反应不是去跑官方自带的 demo,而是想搞清楚一件事:这块 7.3 英寸彩色墨水屏,能不能被 Rust 干净利落地驱动起来。reTerminal E1002 是 Seeed 基于 Raspberry Pi CM4 做的工业级 HMI&a…

作者头像 李华